Add timezone date fix for blog-backup
This commit is contained in:
parent
6185f2bf6b
commit
dba7cfe57e
4
.obsidian/community-plugins.json
vendored
4
.obsidian/community-plugins.json
vendored
@ -1,3 +1,5 @@
|
||||
[
|
||||
"obsidian-git"
|
||||
"obsidian-git",
|
||||
"obsidian42-brat",
|
||||
"qmd-search"
|
||||
]
|
||||
5
.obsidian/plugins/obsidian42-brat/brat-migrations.json
vendored
Normal file
5
.obsidian/plugins/obsidian42-brat/brat-migrations.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"appliedMigrations": [
|
||||
"tokens-to-secretstorage-v1"
|
||||
]
|
||||
}
|
||||
24
.obsidian/plugins/obsidian42-brat/data.json
vendored
Normal file
24
.obsidian/plugins/obsidian42-brat/data.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"pluginList": [
|
||||
"achekulaev/obsidian-qmd"
|
||||
],
|
||||
"pluginSubListFrozenVersion": [
|
||||
{
|
||||
"repo": "achekulaev/obsidian-qmd",
|
||||
"version": ""
|
||||
}
|
||||
],
|
||||
"themesList": [],
|
||||
"updateAtStartup": true,
|
||||
"updateThemesAtStartup": true,
|
||||
"enableAfterInstall": true,
|
||||
"loggingEnabled": false,
|
||||
"loggingPath": "BRAT-log",
|
||||
"loggingVerboseEnabled": false,
|
||||
"debuggingMode": false,
|
||||
"notificationsEnabled": true,
|
||||
"globalTokenName": "",
|
||||
"personalAccessToken": "",
|
||||
"selectLatestPluginVersionByDefault": false,
|
||||
"allowIncompatiblePlugins": false
|
||||
}
|
||||
45
.obsidian/plugins/obsidian42-brat/main.js
vendored
Normal file
45
.obsidian/plugins/obsidian42-brat/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
.obsidian/plugins/obsidian42-brat/manifest.json
vendored
Normal file
14
.obsidian/plugins/obsidian42-brat/manifest.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "obsidian42-brat",
|
||||
"name": "BRAT",
|
||||
"version": "2.0.2",
|
||||
"minAppVersion": "1.11.4",
|
||||
"description": "Easily install a beta version of a plugin for testing.",
|
||||
"author": "TfTHacker",
|
||||
"authorUrl": "https://github.com/TfTHacker/obsidian42-brat",
|
||||
"helpUrl": "https://tfthacker.com/BRAT",
|
||||
"isDesktopOnly": false,
|
||||
"fundingUrl": {
|
||||
"Visit my site": "https://tfthacker.com"
|
||||
}
|
||||
}
|
||||
152
.obsidian/plugins/obsidian42-brat/styles.css
vendored
Normal file
152
.obsidian/plugins/obsidian42-brat/styles.css
vendored
Normal file
@ -0,0 +1,152 @@
|
||||
.brat-modal .modal-button-container {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.brat-modal .disabled-setting {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.brat-modal .disabled-setting:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Input validation styles */
|
||||
.brat-settings .valid-input,
|
||||
.brat-modal .valid-repository {
|
||||
border-color: var(--color-green);
|
||||
}
|
||||
.brat-settings .invalid-input,
|
||||
.brat-modal .invalid-repository {
|
||||
border-color: var(--color-red);
|
||||
}
|
||||
.brat-settings .validation-error,
|
||||
.brat-modal .validation-error {
|
||||
border-color: var(--color-orange);
|
||||
}
|
||||
|
||||
/* Version selector */
|
||||
.brat-version-selector {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.brat-token-input {
|
||||
min-width: 33%;
|
||||
}
|
||||
|
||||
/* Token info container styles */
|
||||
.brat-token-info {
|
||||
margin-top: 8px;
|
||||
font-size: 0.8em;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
/* Token status indicators */
|
||||
.brat-token-info.valid,
|
||||
.brat-token-status.valid {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.brat-token-info.invalid,
|
||||
.brat-token-status.invalid {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.brat-token-info.valid {
|
||||
border-left: 3px solid var(--color-green);
|
||||
}
|
||||
|
||||
.brat-token-info.invalid {
|
||||
border-left: 3px solid var(--color-red);
|
||||
}
|
||||
|
||||
/* Token details and status */
|
||||
.brat-token-status {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.brat-token-details {
|
||||
margin-top: 4px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Token warnings */
|
||||
.brat-token-warning {
|
||||
color: var(--color-orange);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Token additional info */
|
||||
.brat-token-scopes,
|
||||
.brat-token-rate {
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Flex break utility */
|
||||
.brat-modal .break {
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Validation status */
|
||||
.brat-modal .validation-status-error {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.brat-modal .validation-status {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.confirm-modal .ok-button {
|
||||
margin-right: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Hide filtered plugin items */
|
||||
.brat-plugin-item[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide filtered theme items */
|
||||
.brat-theme-item[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Filter and button layout */
|
||||
.brat-filter-and-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.brat-filter-input {
|
||||
max-width: 300px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.brat-filter-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.brat-filter-and-button .setting-item {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.brat-filter-and-button .setting-item-control {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
21
.obsidian/plugins/qmd-search/data.json
vendored
Normal file
21
.obsidian/plugins/qmd-search/data.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"qmdBinaryPath": "/opt/homebrew/bin/qmd",
|
||||
"collectionName": "",
|
||||
"indexName": null,
|
||||
"fileMask": "**/*.md",
|
||||
"debounceMs": 45000,
|
||||
"enablePeriodicUpdates": true,
|
||||
"periodicUpdateMinutes": 15,
|
||||
"defaultSearchMode": "semantic",
|
||||
"fallbackOnSemanticFailure": true,
|
||||
"fallbackOnZeroResults": false,
|
||||
"showEmbeddingsBanner": true,
|
||||
"autoGenerateEmbeddings": true,
|
||||
"enableRibbonIcon": true,
|
||||
"enableSearchPane": false,
|
||||
"showScoresInResults": true,
|
||||
"lastIndexUpdateTime": "2026-02-26T22:48:31.431Z",
|
||||
"lastEmbeddingRunTime": "2026-02-26T22:48:32.734Z",
|
||||
"lastSearchMode": "semantic",
|
||||
"lastError": null
|
||||
}
|
||||
8
.obsidian/plugins/qmd-search/main.js
vendored
Normal file
8
.obsidian/plugins/qmd-search/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
.obsidian/plugins/qmd-search/manifest.json
vendored
Normal file
1
.obsidian/plugins/qmd-search/manifest.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"id":"qmd-search","name":"QMD Semantic Search","version":"1.1.3","minAppVersion":"1.11.0","description":"Semantic-first search for your vault using QMD (Quick Markdown Search). Provides vector-based semantic search with keyword fallback.","author":"Oleksii Chekulaiev","authorUrl":"https://github.com/achekulaev","isDesktopOnly":true}
|
||||
312
.obsidian/plugins/qmd-search/styles.css
vendored
Normal file
312
.obsidian/plugins/qmd-search/styles.css
vendored
Normal file
@ -0,0 +1,312 @@
|
||||
/**
|
||||
* QMD Semantic Search - Obsidian Plugin Styles
|
||||
*
|
||||
* These styles are loaded by Obsidian automatically when the plugin is enabled.
|
||||
*/
|
||||
|
||||
/* Utility */
|
||||
.qmd-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Search Modal - Input */
|
||||
.qmd-input-with-pill {
|
||||
padding-right: 120px !important;
|
||||
}
|
||||
|
||||
/* Search Modal - Results */
|
||||
.qmd-search-result {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.qmd-search-result-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.qmd-search-result-name {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.qmd-search-result-path {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.qmd-search-result-snippet {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qmd-search-result-score {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 0.75em;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.qmd-search-result-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.8em;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.qmd-search-result-mode {
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.qmd-mode-semantic {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.qmd-mode-keyword {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Search mode pill inside input field */
|
||||
.qmd-search-mode-indicator {
|
||||
position: absolute;
|
||||
right: 44px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75em;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(var(--interactive-accent-rgb, 124, 77, 255), 0.5);
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Search Modal - Error display */
|
||||
.qmd-search-error {
|
||||
padding: 12px;
|
||||
background: var(--background-modifier-error);
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.qmd-search-error-title {
|
||||
color: var(--text-error);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.qmd-search-error-hint {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Search Modal - Progress bar */
|
||||
.qmd-progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: var(--background-secondary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.qmd-progress-bar {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: var(--background-modifier-border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qmd-progress-bar-fill {
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
background: var(--text-accent);
|
||||
border-radius: 2px;
|
||||
animation: qmd-progress-slide 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes qmd-progress-slide {
|
||||
0% { transform: translateX(-100%); }
|
||||
50% { transform: translateX(333%); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
.qmd-progress-text {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Search Pane */
|
||||
.qmd-search-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.qmd-search-input-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qmd-search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--background-modifier-form-field);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.qmd-search-icon {
|
||||
color: var(--text-muted);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.qmd-search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.qmd-search-status {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.85em;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qmd-status-loading {
|
||||
color: var(--text-muted);
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.qmd-status-success {
|
||||
color: var(--text-success);
|
||||
}
|
||||
|
||||
.qmd-status-error {
|
||||
color: var(--text-error);
|
||||
background: var(--background-modifier-error);
|
||||
}
|
||||
|
||||
.qmd-search-results {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.qmd-search-result-item {
|
||||
padding: 8px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
|
||||
.qmd-search-result-item:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.qmd-result-title {
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.qmd-result-path {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.qmd-result-snippet {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.qmd-result-score {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.qmd-search-no-results {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Search Pane - Loading */
|
||||
.qmd-pane-loading-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.qmd-pane-spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
border-top-color: var(--text-accent);
|
||||
border-radius: 50%;
|
||||
animation: qmd-pane-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes qmd-pane-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Settings Tab */
|
||||
.qmd-diagnostics {
|
||||
padding: 12px;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qmd-diagnostic-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.qmd-diagnostic-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.qmd-diagnostic-value {
|
||||
color: var(--text-normal);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes qmd-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.qmd-loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
border-top-color: var(--text-accent);
|
||||
border-radius: 50%;
|
||||
animation: qmd-spin 0.8s linear infinite;
|
||||
}
|
||||
48
.obsidian/workspace.json
vendored
48
.obsidian/workspace.json
vendored
@ -13,12 +13,12 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "TOOLS.md",
|
||||
"file": "memory/2026-02-26.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "TOOLS"
|
||||
"title": "2026-02-26"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -95,7 +95,7 @@
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "TOOLS.md",
|
||||
"file": "memory/2026-02-26-alice-debug.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
@ -105,7 +105,7 @@
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for TOOLS"
|
||||
"title": "Backlinks for 2026-02-26-alice-debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -114,12 +114,12 @@
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "TOOLS.md",
|
||||
"file": "memory/2026-02-26-alice-debug.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from TOOLS"
|
||||
"title": "Outgoing links from 2026-02-26-alice-debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -157,16 +157,27 @@
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "TOOLS.md",
|
||||
"file": "memory/2026-02-26-alice-debug.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of TOOLS"
|
||||
"title": "Outline of 2026-02-26-alice-debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "353be6f48ab39071",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "git-view",
|
||||
"state": {},
|
||||
"icon": "git-pull-request",
|
||||
"title": "Source Control"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"currentTab": 5
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
@ -181,15 +192,26 @@
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false,
|
||||
"bases:Create new base": false
|
||||
"bases:Create new base": false,
|
||||
"obsidian-git:Open Git source control": false,
|
||||
"obsidian42-brat:BRAT": false,
|
||||
"qmd-search:QMD search": false
|
||||
}
|
||||
},
|
||||
"active": "1914b3cb53aaf523",
|
||||
"active": "09c562b295be8994",
|
||||
"lastOpenFiles": [
|
||||
"MEMORY.md",
|
||||
"USER.md",
|
||||
"scripts/daily-digest.sh",
|
||||
"memory/2026-02-26-blog-fix.md",
|
||||
"memory/2026-02-26-blog-api-fix.md",
|
||||
"memory/2026-02-26-digest-failed.md",
|
||||
"scripts/skrybe",
|
||||
"scripts/skrybe.swift",
|
||||
"AGENTS.md",
|
||||
"HEARTBEAT.md",
|
||||
"MEMORY.md",
|
||||
"memory/2026-02-26-alice-debug.md",
|
||||
"TOOLS.md",
|
||||
"USER.md",
|
||||
"Untitled 2.base",
|
||||
"Untitled 1.base",
|
||||
"Untitled.base"
|
||||
|
||||
@ -11,6 +11,30 @@ Morning: Gantt Board for hardening (test if complete), git commit workspace. Bui
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-26 16:46] ✅ Created daily-digest.sh script
|
||||
|
||||
### What was requested
|
||||
Matt wanted a shell script to generate the daily digest reliably (to fix the broken cron job).
|
||||
|
||||
### What was done
|
||||
- Created `/Users/mattbruce/.openclaw/workspace/scripts/daily-digest.sh`
|
||||
- Script handles JSON escaping properly using `jq`
|
||||
- Uses temp files to avoid argument length issues
|
||||
- Checks for existing digest before creating
|
||||
- Generates proper format with emojis and `[Read more →](URL)` links
|
||||
- Can be run manually: `./daily-digest.sh [YYYY-MM-DD]`
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
# Run for today
|
||||
./scripts/daily-digest.sh
|
||||
|
||||
# Run for specific date
|
||||
./scripts/daily-digest.sh 2026-02-26
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-26 15:52] ✅ Fixed blog-backup skill to match mission-control-docs pattern
|
||||
|
||||
### What was requested
|
||||
|
||||
BIN
scripts/skrybe
Executable file
BIN
scripts/skrybe
Executable file
Binary file not shown.
202
scripts/skrybe.swift
Normal file
202
scripts/skrybe.swift
Normal file
@ -0,0 +1,202 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import ApplicationServices
|
||||
|
||||
enum SkrybeError: LocalizedError {
|
||||
case invalidUsage(String)
|
||||
case unsupportedSubcommand(String)
|
||||
case unsupportedQuotesOption(String)
|
||||
case apiURLInvalid
|
||||
case apiUnreachable(String)
|
||||
case invalidHTTPStatus(Int)
|
||||
case invalidResponse
|
||||
case pasteboardWriteFailed
|
||||
case accessibilityPermissionMissing
|
||||
case eventCreationFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidUsage(let message):
|
||||
return message
|
||||
case .unsupportedSubcommand(let command):
|
||||
return "Unsupported subcommand: \(command)"
|
||||
case .unsupportedQuotesOption(let option):
|
||||
return "Unsupported quotes option: \(option)"
|
||||
case .apiURLInvalid:
|
||||
return "Invalid quotes API URL."
|
||||
case .apiUnreachable(let reason):
|
||||
return "Unable to reach quotes API: \(reason)"
|
||||
case .invalidHTTPStatus(let status):
|
||||
return "Quotes API returned HTTP status \(status)."
|
||||
case .invalidResponse:
|
||||
return "Quotes API returned an invalid response format."
|
||||
case .pasteboardWriteFailed:
|
||||
return "Failed to write quote text to the macOS pasteboard."
|
||||
case .accessibilityPermissionMissing:
|
||||
return "Accessibility permission is required to simulate paste (Cmd+V). Enable it in System Settings > Privacy & Security > Accessibility."
|
||||
case .eventCreationFailed:
|
||||
return "Unable to create keyboard events for paste operation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QuoteResponse: Decodable {
|
||||
let quote: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
struct QuoteService {
|
||||
private let endpoint = "http://localhost:3001/quote"
|
||||
|
||||
func fetchRandomQuote(timeout: TimeInterval = 5.0) throws -> QuoteResponse {
|
||||
guard let url = URL(string: endpoint) else {
|
||||
throw SkrybeError.apiURLInvalid
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = timeout
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var responseData: Data?
|
||||
var response: URLResponse?
|
||||
var requestError: Error?
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, urlResponse, error in
|
||||
responseData = data
|
||||
response = urlResponse
|
||||
requestError = error
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
task.resume()
|
||||
_ = semaphore.wait(timeout: .now() + timeout + 1.0)
|
||||
|
||||
if let requestError {
|
||||
throw SkrybeError.apiUnreachable(requestError.localizedDescription)
|
||||
}
|
||||
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw SkrybeError.invalidResponse
|
||||
}
|
||||
|
||||
guard (200...299).contains(http.statusCode) else {
|
||||
throw SkrybeError.invalidHTTPStatus(http.statusCode)
|
||||
}
|
||||
|
||||
guard let responseData else {
|
||||
throw SkrybeError.invalidResponse
|
||||
}
|
||||
|
||||
do {
|
||||
let decoded = try JSONDecoder().decode(QuoteResponse.self, from: responseData)
|
||||
return decoded
|
||||
} catch {
|
||||
throw SkrybeError.invalidResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QuotePaster {
|
||||
func paste(_ text: String) throws {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
|
||||
guard pasteboard.setString(text, forType: .string) else {
|
||||
throw SkrybeError.pasteboardWriteFailed
|
||||
}
|
||||
|
||||
guard AXIsProcessTrusted() else {
|
||||
throw SkrybeError.accessibilityPermissionMissing
|
||||
}
|
||||
|
||||
try sendCommandV()
|
||||
}
|
||||
|
||||
private func sendCommandV() throws {
|
||||
guard
|
||||
let source = CGEventSource(stateID: .hidSystemState),
|
||||
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: true), // 9 = 'v'
|
||||
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: false)
|
||||
else {
|
||||
throw SkrybeError.eventCreationFailed
|
||||
}
|
||||
|
||||
keyDown.flags = .maskCommand
|
||||
keyUp.flags = .maskCommand
|
||||
|
||||
keyDown.post(tap: .cghidEventTap)
|
||||
keyUp.post(tap: .cghidEventTap)
|
||||
}
|
||||
}
|
||||
|
||||
struct SkrybeCLI {
|
||||
private let quoteService = QuoteService()
|
||||
private let quotePaster = QuotePaster()
|
||||
|
||||
func run(arguments: [String]) throws {
|
||||
guard arguments.count >= 2 else {
|
||||
throw SkrybeError.invalidUsage(usage())
|
||||
}
|
||||
|
||||
let command = arguments[1]
|
||||
switch command {
|
||||
case "quotes":
|
||||
try runQuotes(arguments: Array(arguments.dropFirst(2)))
|
||||
case "--help", "-h", "help":
|
||||
print(usage())
|
||||
default:
|
||||
throw SkrybeError.unsupportedSubcommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
private func runQuotes(arguments: [String]) throws {
|
||||
guard let option = arguments.first else {
|
||||
throw SkrybeError.invalidUsage(quotesUsage())
|
||||
}
|
||||
|
||||
switch option {
|
||||
case "--paste":
|
||||
let quote = try quoteService.fetchRandomQuote()
|
||||
let rendered = "\(quote.quote) — \(quote.author)"
|
||||
try quotePaster.paste(rendered)
|
||||
print("✅ Pasted quote at cursor position")
|
||||
case "--list", "--add":
|
||||
throw SkrybeError.invalidUsage("\(option) is not implemented yet. Use `skrybe quotes --paste`.")
|
||||
case "--help", "-h", "help":
|
||||
print(quotesUsage())
|
||||
default:
|
||||
throw SkrybeError.unsupportedQuotesOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
private func usage() -> String {
|
||||
return """
|
||||
Usage:
|
||||
skrybe quotes --paste
|
||||
skrybe quotes --list (planned)
|
||||
skrybe quotes --add (planned)
|
||||
"""
|
||||
}
|
||||
|
||||
private func quotesUsage() -> String {
|
||||
return """
|
||||
Quotes subcommand:
|
||||
skrybe quotes --paste Fetch random quote from localhost:3001/quote and paste at current cursor
|
||||
skrybe quotes --list Planned
|
||||
skrybe quotes --add Planned
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try SkrybeCLI().run(arguments: CommandLine.arguments)
|
||||
} catch {
|
||||
if let localized = error as? LocalizedError, let message = localized.errorDescription {
|
||||
fputs("❌ \(message)\n", stderr)
|
||||
} else {
|
||||
fputs("❌ \(error.localizedDescription)\n", stderr)
|
||||
}
|
||||
exit(EXIT_FAILURE)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user