@dinoxx/dinox-cli 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -139
- package/dist/auth/userInfo.d.ts +8 -1
- package/dist/auth/userInfo.js +38 -6
- package/dist/cli.js +1 -2
- package/dist/cliTypes.d.ts +1 -0
- package/dist/commands/auth/index.js +11 -4
- package/dist/commands/boxes/index.js +274 -54
- package/dist/commands/boxes/model.d.ts +34 -0
- package/dist/commands/boxes/model.js +78 -0
- package/dist/commands/boxes/operations.d.ts +86 -0
- package/dist/commands/boxes/operations.js +415 -0
- package/dist/commands/boxes/repo.d.ts +13 -6
- package/dist/commands/boxes/repo.js +272 -149
- package/dist/commands/boxes/store.d.ts +22 -0
- package/dist/commands/boxes/store.js +143 -0
- package/dist/commands/graph/index.js +11 -38
- package/dist/commands/mcp/noteContentTools.d.ts +222 -0
- package/dist/commands/mcp/noteContentTools.js +390 -0
- package/dist/commands/mcp/server.d.ts +3 -0
- package/dist/commands/mcp/server.js +43 -15
- package/dist/commands/mcp/tools.js +3 -1
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/meta/index.js +8 -24
- package/dist/commands/meta/repo.d.ts +0 -3
- package/dist/commands/meta/repo.js +1 -4
- package/dist/commands/notes/actions.d.ts +2 -0
- package/dist/commands/notes/actions.js +149 -55
- package/dist/commands/notes/bulk.d.ts +34 -0
- package/dist/commands/notes/bulk.js +204 -0
- package/dist/commands/notes/contentEngine.d.ts +95 -0
- package/dist/commands/notes/contentEngine.js +652 -0
- package/dist/commands/notes/contentPatch.d.ts +84 -0
- package/dist/commands/notes/contentPatch.js +337 -0
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +126 -0
- package/dist/commands/notes/descriptors.js +194 -42
- package/dist/commands/notes/export.d.ts +21 -0
- package/dist/commands/notes/export.js +224 -0
- package/dist/commands/notes/metadataMutation.d.ts +28 -0
- package/dist/commands/notes/metadataMutation.js +127 -0
- package/dist/commands/notes/repo.d.ts +20 -0
- package/dist/commands/notes/repo.js +135 -18
- package/dist/commands/notes/runtime.d.ts +3 -12
- package/dist/commands/notes/runtime.js +4 -30
- package/dist/commands/notes/searchSql.js +1 -1
- package/dist/commands/prompt/index.js +17 -47
- package/dist/commands/prompt/repo.js +2 -2
- package/dist/commands/runtime.d.ts +34 -0
- package/dist/commands/runtime.js +110 -0
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +21 -7
- package/dist/commands/schema/spec.d.ts +1 -2
- package/dist/commands/schema/spec.js +2 -3
- package/dist/commands/schema/types.d.ts +0 -13
- package/dist/commands/storage/index.js +32 -62
- package/dist/commands/sync.js +22 -5
- package/dist/commands/tags/content.d.ts +32 -0
- package/dist/commands/tags/content.js +243 -0
- package/dist/commands/tags/index.js +287 -51
- package/dist/commands/tags/model.d.ts +38 -0
- package/dist/commands/tags/model.js +100 -0
- package/dist/commands/tags/operations.d.ts +105 -0
- package/dist/commands/tags/operations.js +438 -0
- package/dist/commands/tags/repo.d.ts +5 -1
- package/dist/commands/tags/repo.js +142 -218
- package/dist/commands/tags/store.d.ts +28 -0
- package/dist/commands/tags/store.js +166 -0
- package/dist/commands/todo/index.js +32 -63
- package/dist/commands/writeDryRun.d.ts +25 -0
- package/dist/commands/writeDryRun.js +31 -1
- package/dist/config/resolve.js +2 -2
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +176 -58
- package/dist/powersync/runtime.d.ts +1 -0
- package/dist/powersync/runtime.js +16 -0
- package/dist/powersync/schema/content.d.ts +2 -0
- package/dist/powersync/schema/content.js +2 -0
- package/dist/powersync/schema/index.d.ts +2 -0
- package/dist/powersync/syncGate.d.ts +24 -0
- package/dist/powersync/syncGate.js +36 -0
- package/dist/skills/check.d.ts +1 -1
- package/dist/skills/check.js +27 -2
- package/dist/utils/authToken.d.ts +2 -0
- package/dist/utils/authToken.js +7 -2
- package/dist/utils/noteContentEngine.d.ts +333 -0
- package/dist/utils/noteContentEngine.js +740 -0
- package/dist/utils/tiptapMarkdownSchema.js +31 -0
- package/package.json +4 -8
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { registerCommandGroup } from '../descriptor/register.js';
|
|
2
|
-
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
3
|
-
import { runNoteCreate, runNoteDelete, runNoteDetail, runNoteGet, runNotePreview, runNoteSearch, runNoteStar, runNoteUnstar, runNoteUpdate, } from './actions.js';
|
|
4
|
-
|
|
2
|
+
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
3
|
+
import { runNoteCreate, runNoteDelete, runNoteDetail, runNoteGet, runNoteMove, runNotePreview, runNoteSearch, runNoteStar, runNoteTag, runNoteUnstar, runNoteUpdate, } from './actions.js';
|
|
4
|
+
import { runNoteBulk as runNoteBulkAction } from './bulk.js';
|
|
5
|
+
import { runNoteContentRead, runNotePatch } from './contentPatchCommand.js';
|
|
6
|
+
import { runNoteExport } from './export.js';
|
|
7
|
+
const NOTE_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
5
8
|
export const noteCommandGroup = {
|
|
6
9
|
name: 'note',
|
|
7
10
|
description: 'Manage notes',
|
|
@@ -16,7 +19,7 @@ export const noteCommandGroup = {
|
|
|
16
19
|
{ flags: '--to <date>', description: 'created_at end (YYYY-MM-DD or ISO datetime)', type: 'string' },
|
|
17
20
|
{ flags: '--days <n>', description: 'Recent N days by created_at; mutually exclusive with --from/--to', type: 'number' },
|
|
18
21
|
{ flags: '--starred <true|false>', description: 'Filter by starred status', type: 'string', enum: ['true', 'false'] },
|
|
19
|
-
{ flags: '--boxes <string|@file>', description: 'Box names (JSON array/comma list), or [] for empty boxes', type: 'string' },
|
|
22
|
+
{ flags: '--boxes <string|@file>', description: 'Box paths or unique names (JSON array/comma list), or [] for empty boxes', type: 'string' },
|
|
20
23
|
{ flags: '--sql <expr>', description: 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type/is_starred', type: 'string' },
|
|
21
24
|
{ flags: '--limit <n>', description: 'Maximum returned notes', type: 'number' },
|
|
22
25
|
{ flags: '--offset <n>', description: 'Result offset for pagination', type: 'number', defaultValue: 0 },
|
|
@@ -35,21 +38,6 @@ export const noteCommandGroup = {
|
|
|
35
38
|
fields: ['id', 'title', 'summary', 'tags', 'created_at', 'boxes', 'is_starred'],
|
|
36
39
|
metaFields: ['query', 'returned_count', 'total_count', 'stale', 'pagination', 'fields', 'applied_filters'],
|
|
37
40
|
},
|
|
38
|
-
mcp: {
|
|
39
|
-
toolName: 'dinox_search_notes',
|
|
40
|
-
description: 'Search notes by keyword, tags, date range, or SQL expression',
|
|
41
|
-
properties: [
|
|
42
|
-
{ name: 'query', type: 'string', description: 'Search keyword' },
|
|
43
|
-
{ name: 'tags', type: 'string', description: 'Tag expression (AND/OR/NOT)' },
|
|
44
|
-
{ name: 'from', type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
45
|
-
{ name: 'to', type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
46
|
-
{ name: 'days', type: 'number', description: 'Recent N days' },
|
|
47
|
-
{ name: 'boxes', type: 'string[]', description: 'Box names' },
|
|
48
|
-
{ name: 'limit', type: 'number', description: 'Max results (default: 50)' },
|
|
49
|
-
{ name: 'offset', type: 'number', description: 'Result offset (default: 0)' },
|
|
50
|
-
{ name: 'fields', type: 'string[]', description: 'Returned fields: id,title,summary,tags,created_at,boxes,is_starred' },
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
41
|
examples: [
|
|
54
42
|
'dino note search "AI" --days 7 --format json',
|
|
55
43
|
'dino note search --tags "(work OR life) AND NOT archived" --format json',
|
|
@@ -111,15 +99,59 @@ export const noteCommandGroup = {
|
|
|
111
99
|
summary: 'One full note object or an ordered array of note objects',
|
|
112
100
|
fields: ['id', 'title', 'content_md', 'content_text', 'content_json', 'tags', 'boxes', 'created_at', 'updated_at', 'is_del'],
|
|
113
101
|
},
|
|
114
|
-
mcp: {
|
|
115
|
-
toolName: 'dinox_get_note',
|
|
116
|
-
description: 'Get note details by ID',
|
|
117
|
-
properties: [{ name: 'id', type: 'string', description: 'Note ID', required: true }],
|
|
118
|
-
},
|
|
119
102
|
examples: ['dino note detail <id> --format json', 'dino note detail --ids @/tmp/note-ids.txt --format json'],
|
|
120
103
|
},
|
|
121
104
|
action: runNoteDetail,
|
|
122
105
|
},
|
|
106
|
+
{
|
|
107
|
+
name: 'export',
|
|
108
|
+
description: 'Export one or more notes as Markdown or JSON for backup and migration',
|
|
109
|
+
arguments: [{ syntax: '[id]', description: 'Optional note id when exporting a single note' }],
|
|
110
|
+
options: [
|
|
111
|
+
{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
|
|
112
|
+
{ flags: '--type <markdown|json>', description: 'Export format: markdown or json', type: 'string', defaultValue: 'markdown', enum: ['markdown', 'json'] },
|
|
113
|
+
{ flags: '--output <path>', description: 'Output file for one note, or output directory for multiple notes', type: 'string' },
|
|
114
|
+
{ flags: '--overwrite', description: 'Replace existing export files when --output is used', type: 'boolean' },
|
|
115
|
+
{ flags: '--include-deleted', description: 'Allow exporting soft-deleted notes', type: 'boolean' },
|
|
116
|
+
],
|
|
117
|
+
schema: {
|
|
118
|
+
path: 'note.export',
|
|
119
|
+
risk: 'read',
|
|
120
|
+
auth: 'required',
|
|
121
|
+
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
122
|
+
output: {
|
|
123
|
+
type: 'structured',
|
|
124
|
+
summary: 'Export payload or file write acknowledgement for note backup/migration',
|
|
125
|
+
fields: ['format', 'count', 'notes', 'files', 'stale'],
|
|
126
|
+
},
|
|
127
|
+
examples: [
|
|
128
|
+
'dino note export <id> --type markdown --output /tmp/dinox-export',
|
|
129
|
+
'dino note export --ids @/tmp/note-ids.txt --type json --output /tmp/dinox-export --format json',
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
action: runNoteExport,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'content-read',
|
|
136
|
+
description: 'Read note content context and issue a short-lived token required before content patching',
|
|
137
|
+
arguments: [{ syntax: '<id>', description: 'Note id' }],
|
|
138
|
+
options: [
|
|
139
|
+
{ flags: '--include-content', description: 'Include full markdown content in the response', type: 'boolean' },
|
|
140
|
+
],
|
|
141
|
+
schema: {
|
|
142
|
+
path: 'note.content-read',
|
|
143
|
+
risk: 'read',
|
|
144
|
+
auth: 'required',
|
|
145
|
+
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
146
|
+
output: {
|
|
147
|
+
type: 'structured',
|
|
148
|
+
summary: 'Note content read context with readToken, base hash, heading outline, block index, and derived metadata',
|
|
149
|
+
fields: ['noteId', 'title', 'readToken', 'readTokenExpiresAt', 'base', 'outline', 'blocks', 'derived', 'content', 'stale'],
|
|
150
|
+
},
|
|
151
|
+
examples: ['dino note content-read <id> --include-content --format json'],
|
|
152
|
+
},
|
|
153
|
+
action: runNoteContentRead,
|
|
154
|
+
},
|
|
123
155
|
{
|
|
124
156
|
name: 'create',
|
|
125
157
|
description: 'Create a new note from markdown content',
|
|
@@ -128,7 +160,7 @@ export const noteCommandGroup = {
|
|
|
128
160
|
{ flags: '--content <string|@file>', description: 'Markdown content', required: true, type: 'string' },
|
|
129
161
|
{ flags: '--type <note|crawl>', description: 'Note type: note or crawl', type: 'string', defaultValue: 'crawl', enum: ['note', 'crawl'] },
|
|
130
162
|
{ flags: '--tags <string|@file>', description: 'Tag list (JSON array or comma/newline-separated)', type: 'string' },
|
|
131
|
-
{ flags: '--boxes <string|@file>', description: 'Box names (JSON array or comma/newline-separated)', type: 'string' },
|
|
163
|
+
{ flags: '--boxes <string|@file>', description: 'Box paths or unique names (JSON array or comma/newline-separated)', type: 'string' },
|
|
132
164
|
{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
|
|
133
165
|
],
|
|
134
166
|
schema: {
|
|
@@ -141,29 +173,19 @@ export const noteCommandGroup = {
|
|
|
141
173
|
summary: 'Creation acknowledgement with the new note id',
|
|
142
174
|
fields: ['id', 'stale'],
|
|
143
175
|
},
|
|
144
|
-
mcp: {
|
|
145
|
-
toolName: 'dinox_create_note',
|
|
146
|
-
description: 'Create a new note',
|
|
147
|
-
properties: [
|
|
148
|
-
{ name: 'title', type: 'string', description: 'Note title', required: true },
|
|
149
|
-
{ name: 'content', type: 'string', description: 'Markdown content', required: true },
|
|
150
|
-
{ name: 'tags', type: 'string[]', description: 'Tags' },
|
|
151
|
-
{ name: 'boxes', type: 'string[]', description: 'Zettel boxes' },
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
176
|
examples: ['dino note create --title "Weekly note" --content @/tmp/note.md --type note --format json'],
|
|
155
177
|
},
|
|
156
178
|
action: runNoteCreate,
|
|
157
179
|
},
|
|
158
180
|
{
|
|
159
181
|
name: 'update',
|
|
160
|
-
description: 'Full-replace note
|
|
161
|
-
arguments: [{ syntax: '[id]', description: 'Optional note id for single-note
|
|
182
|
+
description: 'Full-replace note metadata for explicit note ids',
|
|
183
|
+
arguments: [{ syntax: '[id]', description: 'Optional note id for single-note full replacement' }],
|
|
162
184
|
options: [
|
|
163
185
|
{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
|
|
164
|
-
{ flags: '--tags <string|@file>', description: '
|
|
165
|
-
{ flags: '--boxes <string|@file>', description: '
|
|
166
|
-
{ flags: '--starred <true|false>', description: '
|
|
186
|
+
{ flags: '--tags <string|@file>', description: 'Replace the entire tag list; use [] to clear all tags', type: 'string' },
|
|
187
|
+
{ flags: '--boxes <string|@file>', description: 'Replace the entire box list; use [] to clear all boxes', type: 'string' },
|
|
188
|
+
{ flags: '--starred <true|false>', description: 'Replace the starred state', type: 'string', enum: ['true', 'false'] },
|
|
167
189
|
{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
|
|
168
190
|
],
|
|
169
191
|
schema: {
|
|
@@ -173,13 +195,143 @@ export const noteCommandGroup = {
|
|
|
173
195
|
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
174
196
|
output: {
|
|
175
197
|
type: 'structured',
|
|
176
|
-
summary: '
|
|
198
|
+
summary: 'Explicit-id full metadata replacement result with per-note status',
|
|
177
199
|
fields: ['total', 'updatedCount', 'results', 'stale'],
|
|
178
200
|
},
|
|
179
|
-
examples: [
|
|
201
|
+
examples: [
|
|
202
|
+
'dino note update <id> --tags "work,ai" --format json --dry-run',
|
|
203
|
+
'dino note update --ids @/tmp/note-ids.txt --boxes "Inbox" --starred true --format json',
|
|
204
|
+
],
|
|
180
205
|
},
|
|
181
206
|
action: runNoteUpdate,
|
|
182
207
|
},
|
|
208
|
+
{
|
|
209
|
+
name: 'tag',
|
|
210
|
+
description: 'Incrementally organize note tags for explicit note ids',
|
|
211
|
+
arguments: [{ syntax: '[id]', description: 'Optional note id for single-note tag organization' }],
|
|
212
|
+
options: [
|
|
213
|
+
{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
|
|
214
|
+
{ flags: '--add <string|@file>', description: 'Tags to add without removing existing tags', type: 'string' },
|
|
215
|
+
{ flags: '--remove <string|@file>', description: 'Tags to remove while preserving the rest', type: 'string' },
|
|
216
|
+
{ flags: '--replace <string|@file>', description: 'Replace the entire tag list; use [] to clear all tags', type: 'string' },
|
|
217
|
+
{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
|
|
218
|
+
],
|
|
219
|
+
schema: {
|
|
220
|
+
path: 'note.tag',
|
|
221
|
+
risk: 'write',
|
|
222
|
+
auth: 'required',
|
|
223
|
+
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
224
|
+
output: {
|
|
225
|
+
type: 'structured',
|
|
226
|
+
summary: 'Explicit-id incremental tag organization result with per-note status',
|
|
227
|
+
fields: ['field', 'operation', 'total', 'updatedCount', 'results', 'stale'],
|
|
228
|
+
},
|
|
229
|
+
examples: [
|
|
230
|
+
'dino note tag <id> --add "AI,work" --format json --dry-run',
|
|
231
|
+
'dino note tag --ids @/tmp/note-ids.txt --remove "Inbox" --format json --dry-run',
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
action: runNoteTag,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'move',
|
|
238
|
+
description: 'Incrementally organize note zettel boxes for explicit note ids',
|
|
239
|
+
arguments: [{ syntax: '[id]', description: 'Optional note id for single-note box organization' }],
|
|
240
|
+
options: [
|
|
241
|
+
{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
|
|
242
|
+
{ flags: '--add <string|@file>', description: 'Box paths or unique names to add without removing existing boxes', type: 'string' },
|
|
243
|
+
{ flags: '--remove <string|@file>', description: 'Box paths or unique names to remove while preserving the rest', type: 'string' },
|
|
244
|
+
{ flags: '--replace <string|@file>', description: 'Replace the entire box list; use [] to clear all boxes', type: 'string' },
|
|
245
|
+
{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
|
|
246
|
+
],
|
|
247
|
+
schema: {
|
|
248
|
+
path: 'note.move',
|
|
249
|
+
risk: 'write',
|
|
250
|
+
auth: 'required',
|
|
251
|
+
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
252
|
+
output: {
|
|
253
|
+
type: 'structured',
|
|
254
|
+
summary: 'Explicit-id incremental box organization result with per-note status',
|
|
255
|
+
fields: ['field', 'operation', 'total', 'updatedCount', 'results', 'stale'],
|
|
256
|
+
},
|
|
257
|
+
examples: [
|
|
258
|
+
'dino note move <id> --add "Work/Inbox" --format json --dry-run',
|
|
259
|
+
'dino note move --ids @/tmp/note-ids.txt --replace "Project/AI" --format json --dry-run',
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
action: runNoteMove,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'patch',
|
|
266
|
+
description: 'Patch note content structure after a required content-read token',
|
|
267
|
+
arguments: [{ syntax: '<id>', description: 'Note id' }],
|
|
268
|
+
options: [
|
|
269
|
+
{ flags: '--append-section <heading>', description: 'Append a new level-2 section at the end of the note', type: 'string' },
|
|
270
|
+
{ flags: '--append-to-heading <path>', description: 'Append content to an existing heading path', type: 'string' },
|
|
271
|
+
{ flags: '--replace-section <path>', description: 'Replace the body of an existing heading path', type: 'string' },
|
|
272
|
+
{ flags: '--replace-block', description: 'Replace one exact markdown block matched by --match', type: 'boolean' },
|
|
273
|
+
{ flags: '--match <string|@file>', description: 'Exact markdown to replace when using --replace-block', type: 'string' },
|
|
274
|
+
{ flags: '--content <string|@file>', description: 'Markdown content to insert', type: 'string' },
|
|
275
|
+
{ flags: '--read-token <token>', description: 'Required for real writes; returned by note content-read', type: 'string' },
|
|
276
|
+
{ flags: '--allow-protected-replace', description: 'Allow replace operations to affect media, table, container, or unknown blocks after reviewing content-read output', type: 'boolean' },
|
|
277
|
+
{ flags: '--dry-run', description: 'Preview the patch without writing', type: 'boolean' },
|
|
278
|
+
],
|
|
279
|
+
schema: {
|
|
280
|
+
path: 'note.patch',
|
|
281
|
+
risk: 'write',
|
|
282
|
+
auth: 'required',
|
|
283
|
+
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
284
|
+
output: {
|
|
285
|
+
type: 'structured',
|
|
286
|
+
summary: 'Structured content edit result with content hashes, block context, and synchronized derived metadata',
|
|
287
|
+
fields: ['noteId', 'title', 'operation', 'changed', 'saved', 'beforeHash', 'afterHash', 'target', 'derived', 'stale'],
|
|
288
|
+
},
|
|
289
|
+
examples: [
|
|
290
|
+
'dino note content-read <id> --format json',
|
|
291
|
+
'dino note patch <id> --append-section "复盘" --content @/tmp/section.md --read-token <token> --format json',
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
action: runNotePatch,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: 'bulk',
|
|
298
|
+
description: 'Bulk organize note tags or boxes using safe search filters',
|
|
299
|
+
arguments: [{ syntax: '[query]', description: 'Optional keyword filter for target notes' }],
|
|
300
|
+
options: [
|
|
301
|
+
{ flags: '--tags <expr>', description: 'Target filter: tag expression with AND/OR/NOT, or [] for empty tags', type: 'string' },
|
|
302
|
+
{ flags: '--from <date>', description: 'Target filter: created_at start (YYYY-MM-DD or ISO datetime)', type: 'string' },
|
|
303
|
+
{ flags: '--to <date>', description: 'Target filter: created_at end (YYYY-MM-DD or ISO datetime)', type: 'string' },
|
|
304
|
+
{ flags: '--days <n>', description: 'Target filter: recent N days by created_at; mutually exclusive with --from/--to', type: 'number' },
|
|
305
|
+
{ flags: '--starred <true|false>', description: 'Target filter: starred status', type: 'string', enum: ['true', 'false'] },
|
|
306
|
+
{ flags: '--boxes <string|@file>', description: 'Target filter: box paths or unique names, or [] for empty boxes', type: 'string' },
|
|
307
|
+
{ flags: '--all', description: 'Target all active notes; required when no other target filter is provided', type: 'boolean' },
|
|
308
|
+
{ flags: '--tag-add <string|@file>', description: 'Tags to add to every matched note', type: 'string' },
|
|
309
|
+
{ flags: '--tag-remove <string|@file>', description: 'Tags to remove from every matched note', type: 'string' },
|
|
310
|
+
{ flags: '--tag-replace <string|@file>', description: 'Replace the entire tag list on every matched note; use [] to clear all tags', type: 'string' },
|
|
311
|
+
{ flags: '--box-add <string|@file>', description: 'Box paths or unique names to add to every matched note', type: 'string' },
|
|
312
|
+
{ flags: '--box-remove <string|@file>', description: 'Box paths or unique names to remove from every matched note', type: 'string' },
|
|
313
|
+
{ flags: '--box-replace <string|@file>', description: 'Replace the entire box list on every matched note; use [] to clear all boxes', type: 'string' },
|
|
314
|
+
{ flags: '--expected-count <n>', description: 'Required for real writes; must equal the current matched note count', type: 'number' },
|
|
315
|
+
{ flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
|
|
316
|
+
{ flags: '--dry-run', description: 'Preview matched targets and changes without executing', type: 'boolean' },
|
|
317
|
+
],
|
|
318
|
+
schema: {
|
|
319
|
+
path: 'note.bulk',
|
|
320
|
+
risk: 'high-risk-write',
|
|
321
|
+
auth: 'required',
|
|
322
|
+
globalOptions: NOTE_GLOBAL_OPTIONS,
|
|
323
|
+
output: {
|
|
324
|
+
type: 'structured',
|
|
325
|
+
summary: 'Filter-based bulk metadata organization result with applied filters and per-note status',
|
|
326
|
+
fields: ['field', 'operation', 'total', 'updatedCount', 'filters', 'results', 'stale'],
|
|
327
|
+
},
|
|
328
|
+
examples: [
|
|
329
|
+
'dino note bulk --tags "Inbox" --tag-add "AI" --format json --dry-run',
|
|
330
|
+
'dino note bulk "project" --boxes "Work/Inbox" --box-replace "Projects/Active" --expected-count 12 --confirm --format json',
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
action: runNoteBulkAction,
|
|
334
|
+
},
|
|
183
335
|
{
|
|
184
336
|
name: 'delete',
|
|
185
337
|
description: 'Soft-delete a note by setting is_del=1',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CommandExecutionContext } from '../descriptor/types.js';
|
|
2
|
+
import { type NoteExportItem } from './repo.js';
|
|
3
|
+
export type NoteExportFormat = 'markdown' | 'json';
|
|
4
|
+
export type NoteExportFile = {
|
|
5
|
+
id: string;
|
|
6
|
+
path: string;
|
|
7
|
+
format: NoteExportFormat;
|
|
8
|
+
};
|
|
9
|
+
export type NoteExportResult = {
|
|
10
|
+
format: NoteExportFormat;
|
|
11
|
+
count: number;
|
|
12
|
+
notes: NoteExportItem[];
|
|
13
|
+
files: NoteExportFile[];
|
|
14
|
+
};
|
|
15
|
+
export declare function parseExportFormat(value: unknown): NoteExportFormat;
|
|
16
|
+
export declare function buildExportFileName(note: NoteExportItem, format: NoteExportFormat): string;
|
|
17
|
+
export declare function buildNoteExportStdout(args: {
|
|
18
|
+
notes: NoteExportItem[];
|
|
19
|
+
format: NoteExportFormat;
|
|
20
|
+
}): string;
|
|
21
|
+
export declare function runNoteExport({ command, options, args }: CommandExecutionContext): Promise<void>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
5
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanReadableBlock, printSuccess, toJson, } from '../../utils/output.js';
|
|
6
|
+
import { commandStale, } from '../runtime.js';
|
|
7
|
+
import { getNoteExportItems, } from './repo.js';
|
|
8
|
+
import { parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
9
|
+
export function parseExportFormat(value) {
|
|
10
|
+
if (value === undefined || value === null || value === '') {
|
|
11
|
+
return 'markdown';
|
|
12
|
+
}
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
throw new DinoxError('--type must be one of: markdown, json');
|
|
15
|
+
}
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
if (normalized === 'markdown' || normalized === 'md') {
|
|
18
|
+
return 'markdown';
|
|
19
|
+
}
|
|
20
|
+
if (normalized === 'json') {
|
|
21
|
+
return 'json';
|
|
22
|
+
}
|
|
23
|
+
throw new DinoxError('--type must be one of: markdown, json');
|
|
24
|
+
}
|
|
25
|
+
function normalizeFileNamePart(value) {
|
|
26
|
+
const normalized = value
|
|
27
|
+
.normalize('NFKD')
|
|
28
|
+
.replace(/[^\w\u4e00-\u9fff.-]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '')
|
|
30
|
+
.slice(0, 80);
|
|
31
|
+
return normalized || 'untitled';
|
|
32
|
+
}
|
|
33
|
+
export function buildExportFileName(note, format) {
|
|
34
|
+
const title = normalizeFileNamePart(note.title?.trim() ?? '');
|
|
35
|
+
const id = normalizeFileNamePart(note.id);
|
|
36
|
+
return `${title}-${id}.${format === 'markdown' ? 'md' : 'json'}`;
|
|
37
|
+
}
|
|
38
|
+
function serializeMarkdownExport(note) {
|
|
39
|
+
const frontmatter = stringifyYaml({
|
|
40
|
+
id: note.id,
|
|
41
|
+
title: note.title ?? '',
|
|
42
|
+
type: note.type ?? null,
|
|
43
|
+
created_at: note.created_at,
|
|
44
|
+
updated_at: note.updated_at,
|
|
45
|
+
source: note.source,
|
|
46
|
+
summary: note.summary,
|
|
47
|
+
tags: note.tags,
|
|
48
|
+
boxes: note.boxes,
|
|
49
|
+
box_ids: note.box_ids,
|
|
50
|
+
is_starred: note.is_starred,
|
|
51
|
+
is_del: note.is_del ?? 0,
|
|
52
|
+
}, {
|
|
53
|
+
lineWidth: 0,
|
|
54
|
+
indent: 2,
|
|
55
|
+
}).trimEnd();
|
|
56
|
+
return `---\n${frontmatter}\n---\n\n${note.content_md}`.trimEnd() + '\n';
|
|
57
|
+
}
|
|
58
|
+
function serializeJsonExport(notes) {
|
|
59
|
+
return toJson({
|
|
60
|
+
exported_at: new Date().toISOString(),
|
|
61
|
+
count: notes.length,
|
|
62
|
+
notes,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function assertNoMissingNotes(ids, notes) {
|
|
66
|
+
const found = new Set(notes.map((note) => note.id));
|
|
67
|
+
const missing = ids.filter((id) => !found.has(id));
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
throw new DinoxError(ids.length === 1 ? `Note not found: ${missing[0]}` : `Notes not found: ${missing.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function assertActiveNotes(notes, includeDeleted) {
|
|
73
|
+
if (includeDeleted) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const deleted = notes.filter((note) => typeof note.is_del === 'number' && note.is_del !== 0);
|
|
77
|
+
if (deleted.length > 0) {
|
|
78
|
+
throw new DinoxError(`Cannot export deleted notes without --include-deleted: ${deleted.map((note) => note.id).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function pathExists(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
await fs.stat(filePath);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function resolveOutputTargets(args) {
|
|
94
|
+
const outputPath = path.resolve(args.outputPath);
|
|
95
|
+
if (args.notes.length === 1) {
|
|
96
|
+
if (outputPath.endsWith(path.sep)) {
|
|
97
|
+
return [{
|
|
98
|
+
id: args.notes[0].id,
|
|
99
|
+
path: path.join(outputPath, buildExportFileName(args.notes[0], args.format)),
|
|
100
|
+
format: args.format,
|
|
101
|
+
}];
|
|
102
|
+
}
|
|
103
|
+
const stat = await fs.stat(outputPath).catch((error) => {
|
|
104
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
if (stat?.isDirectory()) {
|
|
110
|
+
return [{
|
|
111
|
+
id: args.notes[0].id,
|
|
112
|
+
path: path.join(outputPath, buildExportFileName(args.notes[0], args.format)),
|
|
113
|
+
format: args.format,
|
|
114
|
+
}];
|
|
115
|
+
}
|
|
116
|
+
return [{ id: args.notes[0].id, path: outputPath, format: args.format }];
|
|
117
|
+
}
|
|
118
|
+
const stat = await fs.stat(outputPath).catch((error) => {
|
|
119
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
if (stat && !stat.isDirectory()) {
|
|
125
|
+
throw new DinoxError('--output must be a directory when exporting multiple notes');
|
|
126
|
+
}
|
|
127
|
+
return args.notes.map((note) => ({
|
|
128
|
+
id: note.id,
|
|
129
|
+
path: path.join(outputPath, buildExportFileName(note, args.format)),
|
|
130
|
+
format: args.format,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
async function writeExportFiles(args) {
|
|
134
|
+
const targets = await resolveOutputTargets({
|
|
135
|
+
outputPath: args.outputPath,
|
|
136
|
+
notes: args.notes,
|
|
137
|
+
format: args.format,
|
|
138
|
+
});
|
|
139
|
+
for (const target of targets) {
|
|
140
|
+
if (!args.overwrite && await pathExists(target.path)) {
|
|
141
|
+
throw new DinoxError(`Refusing to overwrite existing file: ${target.path}`, {
|
|
142
|
+
suggestion: 'Use --overwrite to replace existing export files.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (args.notes.length > 1) {
|
|
147
|
+
await fs.mkdir(path.resolve(args.outputPath), { recursive: true, mode: 0o700 });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await fs.mkdir(path.dirname(targets[0].path), { recursive: true, mode: 0o700 });
|
|
151
|
+
}
|
|
152
|
+
const noteById = new Map(args.notes.map((note) => [note.id, note]));
|
|
153
|
+
for (const target of targets) {
|
|
154
|
+
const note = noteById.get(target.id);
|
|
155
|
+
if (!note) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const content = args.format === 'markdown'
|
|
159
|
+
? serializeMarkdownExport(note)
|
|
160
|
+
: toJson(note);
|
|
161
|
+
await fs.writeFile(target.path, content, { encoding: 'utf8', flag: args.overwrite ? 'w' : 'wx' });
|
|
162
|
+
}
|
|
163
|
+
return targets;
|
|
164
|
+
}
|
|
165
|
+
export function buildNoteExportStdout(args) {
|
|
166
|
+
if (args.format === 'json') {
|
|
167
|
+
return serializeJsonExport(args.notes);
|
|
168
|
+
}
|
|
169
|
+
if (args.notes.length === 1) {
|
|
170
|
+
return serializeMarkdownExport(args.notes[0]);
|
|
171
|
+
}
|
|
172
|
+
return args.notes.map((note) => serializeMarkdownExport(note)).join('\n');
|
|
173
|
+
}
|
|
174
|
+
export async function runNoteExport({ command, options, args }) {
|
|
175
|
+
const exportOptions = options;
|
|
176
|
+
const format = parseExportFormat(exportOptions.type);
|
|
177
|
+
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
178
|
+
await withNoteDatabase(command, async (runtime) => {
|
|
179
|
+
const { db } = runtime;
|
|
180
|
+
await syncBeforeNoteCommand(runtime);
|
|
181
|
+
const ids = await parseTargetNoteIds(args.id, exportOptions.ids, 'detail');
|
|
182
|
+
const notes = await getNoteExportItems(db, ids);
|
|
183
|
+
assertNoMissingNotes(ids, notes);
|
|
184
|
+
assertActiveNotes(notes, Boolean(exportOptions.includeDeleted));
|
|
185
|
+
if (exportOptions.output) {
|
|
186
|
+
const files = await writeExportFiles({
|
|
187
|
+
notes,
|
|
188
|
+
format,
|
|
189
|
+
outputPath: exportOptions.output,
|
|
190
|
+
overwrite: Boolean(exportOptions.overwrite),
|
|
191
|
+
});
|
|
192
|
+
const payload = {
|
|
193
|
+
format,
|
|
194
|
+
count: notes.length,
|
|
195
|
+
notes: structuredOutput ? notes : [],
|
|
196
|
+
files,
|
|
197
|
+
stale: commandStale(runtime),
|
|
198
|
+
};
|
|
199
|
+
if (structuredOutput) {
|
|
200
|
+
printSuccess(payload);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
printHumanReadableLine(`Exported ${notes.length} note(s) to ${exportOptions.output}`);
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
printHumanReadableLine(`- ${file.path}`);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (structuredOutput) {
|
|
210
|
+
printSuccess({
|
|
211
|
+
format,
|
|
212
|
+
count: notes.length,
|
|
213
|
+
notes,
|
|
214
|
+
stale: commandStale(runtime),
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (format === 'json') {
|
|
219
|
+
printHumanReadableBlock(serializeJsonExport(notes));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
printHumanReadableBlock(buildNoteExportStdout({ notes, format }));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
export type NoteMetadataListField = 'tags' | 'boxes';
|
|
3
|
+
export type NoteMetadataOperation = 'add' | 'remove' | 'replace';
|
|
4
|
+
export type NoteMetadataMutationInput = {
|
|
5
|
+
ids: string[];
|
|
6
|
+
field: NoteMetadataListField;
|
|
7
|
+
operation: NoteMetadataOperation;
|
|
8
|
+
values: string[];
|
|
9
|
+
displayValues?: string[];
|
|
10
|
+
};
|
|
11
|
+
export type NoteMetadataMutationItem = {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
before: string[];
|
|
15
|
+
after: string[];
|
|
16
|
+
changed: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type NoteMetadataMutationPlan = {
|
|
19
|
+
field: NoteMetadataListField;
|
|
20
|
+
operation: NoteMetadataOperation;
|
|
21
|
+
values: string[];
|
|
22
|
+
displayValues: string[];
|
|
23
|
+
total: number;
|
|
24
|
+
changedCount: number;
|
|
25
|
+
results: NoteMetadataMutationItem[];
|
|
26
|
+
};
|
|
27
|
+
export declare function planNoteMetadataMutation(db: AbstractPowerSyncDatabase, input: NoteMetadataMutationInput): Promise<NoteMetadataMutationPlan>;
|
|
28
|
+
export declare function applyNoteMetadataMutation(db: AbstractPowerSyncDatabase, plan: NoteMetadataMutationPlan): Promise<NoteMetadataMutationPlan>;
|