@dinoxx/dinox-cli 1.0.22 → 1.1.0

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.
Files changed (144) hide show
  1. package/README.md +29 -9
  2. package/dist/auth/userInfo.d.ts +2 -0
  3. package/dist/auth/userInfo.js +55 -14
  4. package/dist/cli.js +36 -2
  5. package/dist/commands/auth/index.d.ts +35 -0
  6. package/dist/commands/auth/index.js +382 -83
  7. package/dist/commands/boxes/index.js +2 -6
  8. package/dist/commands/boxes/model.d.ts +3 -0
  9. package/dist/commands/boxes/model.js +16 -1
  10. package/dist/commands/boxes/operations.js +18 -8
  11. package/dist/commands/boxes/repo.js +13 -11
  12. package/dist/commands/boxes/store.js +1 -1
  13. package/dist/commands/config/index.js +7 -5
  14. package/dist/commands/descriptor/register.js +9 -0
  15. package/dist/commands/doctor.d.ts +123 -0
  16. package/dist/commands/doctor.js +539 -0
  17. package/dist/commands/graph/index.d.ts +1 -1
  18. package/dist/commands/graph/index.js +35 -21
  19. package/dist/commands/graph/repo.js +92 -17
  20. package/dist/commands/meta/index.js +4 -4
  21. package/dist/commands/meta/repo.js +4 -3
  22. package/dist/commands/notes/actions.d.ts +1 -1
  23. package/dist/commands/notes/actions.js +121 -78
  24. package/dist/commands/notes/bulk.d.ts +1 -0
  25. package/dist/commands/notes/bulk.js +23 -17
  26. package/dist/commands/notes/contentEngine.js +69 -11
  27. package/dist/commands/notes/contentPatch.d.ts +4 -1
  28. package/dist/commands/notes/contentPatch.js +105 -41
  29. package/dist/commands/notes/contentPatchCommand.js +28 -11
  30. package/dist/commands/notes/descriptors.js +50 -20
  31. package/dist/commands/notes/dryRun.d.ts +1 -0
  32. package/dist/commands/notes/dryRun.js +4 -7
  33. package/dist/commands/notes/export.d.ts +6 -0
  34. package/dist/commands/notes/export.js +26 -6
  35. package/dist/commands/notes/metadataMutation.d.ts +1 -0
  36. package/dist/commands/notes/metadataMutation.js +14 -3
  37. package/dist/commands/notes/repo.d.ts +46 -4
  38. package/dist/commands/notes/repo.js +113 -28
  39. package/dist/commands/notes/runtime.d.ts +2 -1
  40. package/dist/commands/notes/runtime.js +17 -1
  41. package/dist/commands/notes/searchSql.js +13 -6
  42. package/dist/commands/notes/writeReceipt.d.ts +20 -0
  43. package/dist/commands/notes/writeReceipt.js +82 -0
  44. package/dist/commands/prompt/index.js +3 -3
  45. package/dist/commands/prompt/repo.d.ts +4 -1
  46. package/dist/commands/prompt/repo.js +17 -7
  47. package/dist/commands/runtime.d.ts +14 -2
  48. package/dist/commands/runtime.js +74 -15
  49. package/dist/commands/schema/renderSkills.js +5 -2
  50. package/dist/commands/schema/spec.js +4 -0
  51. package/dist/commands/storage/index.js +8 -2
  52. package/dist/commands/storage/repo.d.ts +4 -0
  53. package/dist/commands/storage/repo.js +356 -92
  54. package/dist/commands/sync.js +16 -12
  55. package/dist/commands/tags/content.js +9 -0
  56. package/dist/commands/tags/index.js +2 -6
  57. package/dist/commands/tags/operations.js +15 -10
  58. package/dist/commands/tags/repo.d.ts +2 -0
  59. package/dist/commands/tags/repo.js +18 -5
  60. package/dist/commands/tags/store.js +1 -1
  61. package/dist/commands/todo/contentModel.d.ts +4 -3
  62. package/dist/commands/todo/contentModel.js +78 -28
  63. package/dist/commands/todo/dryRun.d.ts +1 -0
  64. package/dist/commands/todo/dryRun.js +17 -13
  65. package/dist/commands/todo/index.d.ts +6 -0
  66. package/dist/commands/todo/index.js +89 -123
  67. package/dist/commands/todo/repo.d.ts +36 -1
  68. package/dist/commands/todo/repo.js +146 -37
  69. package/dist/commands/todo/taskIdentity.d.ts +2 -0
  70. package/dist/commands/todo/taskIdentity.js +64 -0
  71. package/dist/commands/todo/taskModel.js +27 -17
  72. package/dist/commands/update.d.ts +13 -1
  73. package/dist/commands/update.js +135 -23
  74. package/dist/commands/writeDryRun.d.ts +3 -2
  75. package/dist/commands/writeDryRun.js +4 -3
  76. package/dist/commands/writeDurability.d.ts +5 -0
  77. package/dist/commands/writeDurability.js +22 -0
  78. package/dist/config/paths.js +6 -1
  79. package/dist/config/resolve.d.ts +2 -0
  80. package/dist/config/resolve.js +23 -9
  81. package/dist/config/store.d.ts +3 -0
  82. package/dist/config/store.js +61 -14
  83. package/dist/config/types.d.ts +4 -0
  84. package/dist/daemon/client.d.ts +26 -1
  85. package/dist/daemon/client.js +236 -12
  86. package/dist/daemon/ensure.js +74 -12
  87. package/dist/daemon/index.d.ts +42 -0
  88. package/dist/daemon/index.js +202 -52
  89. package/dist/daemon/lifecycle.d.ts +42 -0
  90. package/dist/daemon/lifecycle.js +115 -0
  91. package/dist/daemon/pid.d.ts +19 -6
  92. package/dist/daemon/pid.js +147 -13
  93. package/dist/daemon/route.d.ts +14 -0
  94. package/dist/daemon/route.js +40 -0
  95. package/dist/daemon/server.d.ts +11 -0
  96. package/dist/daemon/server.js +201 -50
  97. package/dist/daemon/types.d.ts +29 -1
  98. package/dist/daemon/types.js +1 -0
  99. package/dist/dinox.js +25 -1
  100. package/dist/powersync/connector.js +31 -16
  101. package/dist/powersync/exclusiveRuntime.d.ts +19 -0
  102. package/dist/powersync/exclusiveRuntime.js +42 -0
  103. package/dist/powersync/runtime.d.ts +24 -4
  104. package/dist/powersync/runtime.js +234 -36
  105. package/dist/powersync/syncGate.d.ts +3 -1
  106. package/dist/powersync/syncGate.js +7 -1
  107. package/dist/powersync/tokenIndex.d.ts +11 -0
  108. package/dist/powersync/tokenIndex.js +159 -22
  109. package/dist/powersync/uploader.js +107 -52
  110. package/dist/utils/abort.js +0 -4
  111. package/dist/utils/asyncRelease.d.ts +2 -0
  112. package/dist/utils/asyncRelease.js +25 -0
  113. package/dist/utils/bidlinkCanonicalization.js +3 -0
  114. package/dist/utils/dryRun.d.ts +1 -1
  115. package/dist/utils/errors.d.ts +15 -1
  116. package/dist/utils/errors.js +223 -5
  117. package/dist/utils/output.d.ts +1 -0
  118. package/dist/utils/output.js +51 -15
  119. package/dist/utils/redact.js +1 -1
  120. package/dist/utils/sqliteLease.d.ts +8 -0
  121. package/dist/utils/sqliteLease.js +59 -0
  122. package/dist/utils/syncTimeout.d.ts +7 -1
  123. package/dist/utils/syncTimeout.js +8 -7
  124. package/dist/utils/terminalSanitization.js +6 -2
  125. package/dist/utils/tiptapMarkdown.js +49 -3
  126. package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
  127. package/dist/utils/tiptapMarkdownSyntax.js +249 -0
  128. package/dist/utils/tokenize.js +24 -2
  129. package/dist/utils/updateNotice.js +60 -12
  130. package/package.json +12 -10
  131. package/dist/commands/mcp/auth.d.ts +0 -4
  132. package/dist/commands/mcp/auth.js +0 -75
  133. package/dist/commands/mcp/index.d.ts +0 -2
  134. package/dist/commands/mcp/index.js +0 -86
  135. package/dist/commands/mcp/noteContentTools.d.ts +0 -222
  136. package/dist/commands/mcp/noteContentTools.js +0 -390
  137. package/dist/commands/mcp/server.d.ts +0 -34
  138. package/dist/commands/mcp/server.js +0 -424
  139. package/dist/commands/mcp/tools.d.ts +0 -2
  140. package/dist/commands/mcp/tools.js +0 -32
  141. package/dist/commands/mcp/types.d.ts +0 -39
  142. package/dist/commands/mcp/types.js +0 -1
  143. package/dist/utils/noteContentEngine.d.ts +0 -333
  144. package/dist/utils/noteContentEngine.js +0 -740
@@ -1,10 +1,29 @@
1
1
  import { registerCommandGroup } from '../descriptor/register.js';
2
2
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
3
+ import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
3
4
  import { runNoteCreate, runNoteDelete, runNoteDetail, runNoteGet, runNoteMove, runNotePreview, runNoteSearch, runNoteStar, runNoteTag, runNoteUnstar, runNoteUpdate, } from './actions.js';
4
5
  import { runNoteBulk as runNoteBulkAction } from './bulk.js';
5
6
  import { runNoteContentRead, runNotePatch } from './contentPatchCommand.js';
6
7
  import { runNoteExport } from './export.js';
7
8
  const NOTE_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
9
+ const NOTE_WRITE_RECEIPT_FIELDS = [
10
+ 'durability',
11
+ 'upload_queue_remaining',
12
+ 'version',
13
+ 'updated_at',
14
+ 'content_hash',
15
+ 'changed',
16
+ 'stale',
17
+ ];
18
+ const NOTE_BATCH_WRITE_RECEIPT_FIELDS = [
19
+ 'durability',
20
+ 'upload_queue_remaining',
21
+ 'results.version',
22
+ 'results.updated_at',
23
+ 'results.content_hash',
24
+ 'results.changed',
25
+ 'stale',
26
+ ];
8
27
  export const noteCommandGroup = {
9
28
  name: 'note',
10
29
  description: 'Manage notes',
@@ -161,6 +180,7 @@ export const noteCommandGroup = {
161
180
  { flags: '--type <note|crawl>', description: 'Note type: note or crawl', type: 'string', defaultValue: 'crawl', enum: ['note', 'crawl'] },
162
181
  { flags: '--tags <string|@file>', description: 'Tag list (JSON array or comma/newline-separated)', type: 'string' },
163
182
  { flags: '--boxes <string|@file>', description: 'Box paths or unique names (JSON array or comma/newline-separated)', type: 'string' },
183
+ WRITE_DURABILITY_OPTION,
164
184
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
165
185
  ],
166
186
  schema: {
@@ -170,10 +190,10 @@ export const noteCommandGroup = {
170
190
  globalOptions: NOTE_GLOBAL_OPTIONS,
171
191
  output: {
172
192
  type: 'structured',
173
- summary: 'Creation acknowledgement with the new note id',
174
- fields: ['id', 'stale'],
193
+ summary: 'Creation acknowledgement with write durability, queue state, and note revision receipt',
194
+ fields: ['id', ...NOTE_WRITE_RECEIPT_FIELDS],
175
195
  },
176
- examples: ['dino note create --title "Weekly note" --content @/tmp/note.md --type note --format json'],
196
+ examples: ['dino note create --title "Weekly note" --content @/tmp/note.md --type note --durability uploaded --format json'],
177
197
  },
178
198
  action: runNoteCreate,
179
199
  },
@@ -186,6 +206,7 @@ export const noteCommandGroup = {
186
206
  { flags: '--tags <string|@file>', description: 'Replace the entire tag list; use [] to clear all tags', type: 'string' },
187
207
  { flags: '--boxes <string|@file>', description: 'Replace the entire box list; use [] to clear all boxes', type: 'string' },
188
208
  { flags: '--starred <true|false>', description: 'Replace the starred state', type: 'string', enum: ['true', 'false'] },
209
+ WRITE_DURABILITY_OPTION,
189
210
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
190
211
  ],
191
212
  schema: {
@@ -195,8 +216,8 @@ export const noteCommandGroup = {
195
216
  globalOptions: NOTE_GLOBAL_OPTIONS,
196
217
  output: {
197
218
  type: 'structured',
198
- summary: 'Explicit-id full metadata replacement result with per-note status',
199
- fields: ['total', 'updatedCount', 'results', 'stale'],
219
+ summary: 'Explicit-id full metadata replacement result with per-note write receipts',
220
+ fields: ['total', 'updatedCount', 'results', ...NOTE_BATCH_WRITE_RECEIPT_FIELDS],
200
221
  },
201
222
  examples: [
202
223
  'dino note update <id> --tags "work,ai" --format json --dry-run',
@@ -214,6 +235,7 @@ export const noteCommandGroup = {
214
235
  { flags: '--add <string|@file>', description: 'Tags to add without removing existing tags', type: 'string' },
215
236
  { flags: '--remove <string|@file>', description: 'Tags to remove while preserving the rest', type: 'string' },
216
237
  { flags: '--replace <string|@file>', description: 'Replace the entire tag list; use [] to clear all tags', type: 'string' },
238
+ WRITE_DURABILITY_OPTION,
217
239
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
218
240
  ],
219
241
  schema: {
@@ -223,8 +245,8 @@ export const noteCommandGroup = {
223
245
  globalOptions: NOTE_GLOBAL_OPTIONS,
224
246
  output: {
225
247
  type: 'structured',
226
- summary: 'Explicit-id incremental tag organization result with per-note status',
227
- fields: ['field', 'operation', 'total', 'updatedCount', 'results', 'stale'],
248
+ summary: 'Explicit-id incremental tag organization result with per-note write receipts',
249
+ fields: ['field', 'operation', 'total', 'updatedCount', 'results', ...NOTE_BATCH_WRITE_RECEIPT_FIELDS],
228
250
  },
229
251
  examples: [
230
252
  'dino note tag <id> --add "AI,work" --format json --dry-run',
@@ -242,6 +264,7 @@ export const noteCommandGroup = {
242
264
  { flags: '--add <string|@file>', description: 'Box paths or unique names to add without removing existing boxes', type: 'string' },
243
265
  { flags: '--remove <string|@file>', description: 'Box paths or unique names to remove while preserving the rest', type: 'string' },
244
266
  { flags: '--replace <string|@file>', description: 'Replace the entire box list; use [] to clear all boxes', type: 'string' },
267
+ WRITE_DURABILITY_OPTION,
245
268
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
246
269
  ],
247
270
  schema: {
@@ -251,8 +274,8 @@ export const noteCommandGroup = {
251
274
  globalOptions: NOTE_GLOBAL_OPTIONS,
252
275
  output: {
253
276
  type: 'structured',
254
- summary: 'Explicit-id incremental box organization result with per-note status',
255
- fields: ['field', 'operation', 'total', 'updatedCount', 'results', 'stale'],
277
+ summary: 'Explicit-id incremental box organization result with per-note write receipts',
278
+ fields: ['field', 'operation', 'total', 'updatedCount', 'results', ...NOTE_BATCH_WRITE_RECEIPT_FIELDS],
256
279
  },
257
280
  examples: [
258
281
  'dino note move <id> --add "Work/Inbox" --format json --dry-run',
@@ -274,6 +297,7 @@ export const noteCommandGroup = {
274
297
  { flags: '--content <string|@file>', description: 'Markdown content to insert', type: 'string' },
275
298
  { flags: '--read-token <token>', description: 'Required for real writes; returned by note content-read', type: 'string' },
276
299
  { flags: '--allow-protected-replace', description: 'Allow replace operations to affect media, table, container, or unknown blocks after reviewing content-read output', type: 'boolean' },
300
+ WRITE_DURABILITY_OPTION,
277
301
  { flags: '--dry-run', description: 'Preview the patch without writing', type: 'boolean' },
278
302
  ],
279
303
  schema: {
@@ -283,8 +307,8 @@ export const noteCommandGroup = {
283
307
  globalOptions: NOTE_GLOBAL_OPTIONS,
284
308
  output: {
285
309
  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'],
310
+ summary: 'Structured content edit result with content hashes, block context, synchronized derived metadata, and write receipt',
311
+ fields: ['noteId', 'title', 'operation', 'content_changed', 'saved', 'beforeHash', 'afterHash', 'target', 'derived', ...NOTE_WRITE_RECEIPT_FIELDS],
288
312
  },
289
313
  examples: [
290
314
  'dino note content-read <id> --format json',
@@ -313,6 +337,7 @@ export const noteCommandGroup = {
313
337
  { flags: '--box-replace <string|@file>', description: 'Replace the entire box list on every matched note; use [] to clear all boxes', type: 'string' },
314
338
  { flags: '--expected-count <n>', description: 'Required for real writes; must equal the current matched note count', type: 'number' },
315
339
  { flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
340
+ WRITE_DURABILITY_OPTION,
316
341
  { flags: '--dry-run', description: 'Preview matched targets and changes without executing', type: 'boolean' },
317
342
  ],
318
343
  schema: {
@@ -322,8 +347,8 @@ export const noteCommandGroup = {
322
347
  globalOptions: NOTE_GLOBAL_OPTIONS,
323
348
  output: {
324
349
  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'],
350
+ summary: 'Filter-based bulk metadata organization result with applied filters and per-note write receipts',
351
+ fields: ['field', 'operation', 'total', 'updatedCount', 'filters', 'results', ...NOTE_BATCH_WRITE_RECEIPT_FIELDS],
327
352
  },
328
353
  examples: [
329
354
  'dino note bulk --tags "Inbox" --tag-add "AI" --format json --dry-run',
@@ -336,7 +361,10 @@ export const noteCommandGroup = {
336
361
  name: 'delete',
337
362
  description: 'Soft-delete a note by setting is_del=1',
338
363
  arguments: [{ syntax: '<id>', description: 'Note id' }],
339
- options: [{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' }],
364
+ options: [
365
+ WRITE_DURABILITY_OPTION,
366
+ { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
367
+ ],
340
368
  schema: {
341
369
  path: 'note.delete',
342
370
  risk: 'high-risk-write',
@@ -344,8 +372,8 @@ export const noteCommandGroup = {
344
372
  globalOptions: NOTE_GLOBAL_OPTIONS,
345
373
  output: {
346
374
  type: 'structured',
347
- summary: 'Deletion acknowledgement',
348
- fields: ['id', 'stale'],
375
+ summary: 'Deletion acknowledgement with write durability and note revision receipt',
376
+ fields: ['id', ...NOTE_WRITE_RECEIPT_FIELDS],
349
377
  },
350
378
  examples: ['dino note delete <id> --format json'],
351
379
  },
@@ -357,6 +385,7 @@ export const noteCommandGroup = {
357
385
  arguments: [{ syntax: '[id]', description: 'Optional note id for single-note update' }],
358
386
  options: [
359
387
  { flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
388
+ WRITE_DURABILITY_OPTION,
360
389
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
361
390
  ],
362
391
  schema: {
@@ -366,8 +395,8 @@ export const noteCommandGroup = {
366
395
  globalOptions: NOTE_GLOBAL_OPTIONS,
367
396
  output: {
368
397
  type: 'structured',
369
- summary: 'Mutation result with per-note starred status',
370
- fields: ['total', 'updatedCount', 'results', 'stale'],
398
+ summary: 'Mutation result with per-note starred status and write receipts',
399
+ fields: ['total', 'updatedCount', 'results', ...NOTE_BATCH_WRITE_RECEIPT_FIELDS],
371
400
  },
372
401
  examples: ['dino note star <id> --format json --dry-run'],
373
402
  },
@@ -379,6 +408,7 @@ export const noteCommandGroup = {
379
408
  arguments: [{ syntax: '[id]', description: 'Optional note id for single-note update' }],
380
409
  options: [
381
410
  { flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
411
+ WRITE_DURABILITY_OPTION,
382
412
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
383
413
  ],
384
414
  schema: {
@@ -388,8 +418,8 @@ export const noteCommandGroup = {
388
418
  globalOptions: NOTE_GLOBAL_OPTIONS,
389
419
  output: {
390
420
  type: 'structured',
391
- summary: 'Mutation result with per-note starred status',
392
- fields: ['total', 'updatedCount', 'results', 'stale'],
421
+ summary: 'Mutation result with per-note starred status and write receipts',
422
+ fields: ['total', 'updatedCount', 'results', ...NOTE_BATCH_WRITE_RECEIPT_FIELDS],
393
423
  },
394
424
  examples: ['dino note unstar <id> --format json --dry-run'],
395
425
  },
@@ -16,6 +16,7 @@ export declare function buildNoteUpdateDryRunPreview(args: {
16
16
  db: AbstractPowerSyncDatabase;
17
17
  config: ResolvedDinoxConfig;
18
18
  offline: boolean;
19
+ userId?: string;
19
20
  ids: string[];
20
21
  tagsInput?: string[];
21
22
  zettelBoxNames?: string[];
@@ -1,7 +1,7 @@
1
1
  import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
2
2
  import { canonicalizeBidlinksInMarkdown } from '../../utils/bidlinkCanonicalization.js';
3
3
  import { DinoxError } from '../../utils/errors.js';
4
- import { assertNotesUpdatable, getNoteDetails, parseStoredTags, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForUpdate, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
4
+ import { assertNotesUpdatable, getNoteDetails, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForUpdate, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
5
5
  function normalizeDistinctStrings(values) {
6
6
  return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
7
7
  }
@@ -21,15 +21,12 @@ function buildExecutionWarnings(config, offline) {
21
21
  if (offline) {
22
22
  warnings.push('Execution would run in offline mode and only use the local cache.');
23
23
  }
24
- if (!offline && !config.powersync.uploadBaseUrl) {
25
- warnings.push('Execution would be local-only because powersync.uploadBaseUrl is unset.');
26
- }
27
24
  return warnings;
28
25
  }
29
26
  export async function buildNoteCreateDryRunPreview(args) {
30
27
  const canonicalContentMd = await canonicalizeBidlinksInMarkdown(args.db, args.contentMd, args.userId);
31
28
  const { contentText } = convertMarkdownToNoteContent(canonicalContentMd);
32
- const tags = await validateTagsForCreate(args.db, args.tagsInput);
29
+ const tags = await validateTagsForCreate(args.db, args.tagsInput, args.userId);
33
30
  await resolveZettelBoxIdsForCreate(args.db, args.zettelBoxNames);
34
31
  return {
35
32
  dry_run: true,
@@ -59,14 +56,14 @@ export async function buildNoteCreateDryRunPreview(args) {
59
56
  export async function buildNoteUpdateDryRunPreview(args) {
60
57
  await assertNotesUpdatable(args.db, args.ids);
61
58
  const nextTags = Array.isArray(args.tagsInput)
62
- ? await validateTagsForUpdate(args.db, args.tagsInput)
59
+ ? await validateTagsForUpdate(args.db, args.tagsInput, args.userId ?? '')
63
60
  : undefined;
64
61
  if (Array.isArray(args.zettelBoxNames)) {
65
62
  await resolveZettelBoxIdsForUpdate(args.db, args.zettelBoxNames);
66
63
  }
67
64
  const details = await getNoteDetails(args.db, args.ids);
68
65
  const changes = details.map((note) => {
69
- const currentTags = parseStoredTags(note.tags);
66
+ const currentTags = note.tags;
70
67
  const nextBoxes = Array.isArray(args.zettelBoxNames) ? normalizeDistinctStrings(args.zettelBoxNames) : note.zettel_boxes;
71
68
  const tagsTo = nextTags ?? currentTags;
72
69
  const starredTo = typeof args.starred === 'boolean' ? args.starred : note.is_starred;
@@ -14,6 +14,12 @@ export type NoteExportResult = {
14
14
  };
15
15
  export declare function parseExportFormat(value: unknown): NoteExportFormat;
16
16
  export declare function buildExportFileName(note: NoteExportItem, format: NoteExportFormat): string;
17
+ export declare function writeExportFiles(args: {
18
+ notes: NoteExportItem[];
19
+ format: NoteExportFormat;
20
+ outputPath: string;
21
+ overwrite: boolean;
22
+ }): Promise<NoteExportFile[]>;
17
23
  export declare function buildNoteExportStdout(args: {
18
24
  notes: NoteExportItem[];
19
25
  format: NoteExportFormat;
@@ -22,13 +22,25 @@ export function parseExportFormat(value) {
22
22
  }
23
23
  throw new DinoxError('--type must be one of: markdown, json');
24
24
  }
25
+ function truncateUtf8(value, maxBytes) {
26
+ let result = '';
27
+ let bytes = 0;
28
+ for (const char of value) {
29
+ const charBytes = Buffer.byteLength(char, 'utf8');
30
+ if (bytes + charBytes > maxBytes) {
31
+ break;
32
+ }
33
+ result += char;
34
+ bytes += charBytes;
35
+ }
36
+ return result;
37
+ }
25
38
  function normalizeFileNamePart(value) {
26
39
  const normalized = value
27
40
  .normalize('NFKD')
28
41
  .replace(/[^\w\u4e00-\u9fff.-]+/g, '-')
29
- .replace(/^-+|-+$/g, '')
30
- .slice(0, 80);
31
- return normalized || 'untitled';
42
+ .replace(/^-+|-+$/g, '');
43
+ return truncateUtf8(normalized, 80).replace(/-+$/g, '') || 'untitled';
32
44
  }
33
45
  export function buildExportFileName(note, format) {
34
46
  const title = normalizeFileNamePart(note.title?.trim() ?? '');
@@ -91,9 +103,10 @@ async function pathExists(filePath) {
91
103
  }
92
104
  }
93
105
  async function resolveOutputTargets(args) {
106
+ const outputLooksLikeDirectory = /[\\/]$/.test(args.outputPath);
94
107
  const outputPath = path.resolve(args.outputPath);
95
108
  if (args.notes.length === 1) {
96
- if (outputPath.endsWith(path.sep)) {
109
+ if (outputLooksLikeDirectory) {
97
110
  return [{
98
111
  id: args.notes[0].id,
99
112
  path: path.join(outputPath, buildExportFileName(args.notes[0], args.format)),
@@ -130,7 +143,7 @@ async function resolveOutputTargets(args) {
130
143
  format: args.format,
131
144
  }));
132
145
  }
133
- async function writeExportFiles(args) {
146
+ export async function writeExportFiles(args) {
134
147
  const targets = await resolveOutputTargets({
135
148
  outputPath: args.outputPath,
136
149
  notes: args.notes,
@@ -158,7 +171,14 @@ async function writeExportFiles(args) {
158
171
  const content = args.format === 'markdown'
159
172
  ? serializeMarkdownExport(note)
160
173
  : toJson(note);
161
- await fs.writeFile(target.path, content, { encoding: 'utf8', flag: args.overwrite ? 'w' : 'wx' });
174
+ await fs.writeFile(target.path, content, {
175
+ encoding: 'utf8',
176
+ flag: args.overwrite ? 'w' : 'wx',
177
+ mode: 0o600,
178
+ });
179
+ if (process.platform !== 'win32') {
180
+ await fs.chmod(target.path, 0o600);
181
+ }
162
182
  }
163
183
  return targets;
164
184
  }
@@ -14,6 +14,7 @@ export type NoteMetadataMutationItem = {
14
14
  before: string[];
15
15
  after: string[];
16
16
  changed: boolean;
17
+ expectedVersion: number;
17
18
  };
18
19
  export type NoteMetadataMutationPlan = {
19
20
  field: NoteMetadataListField;
@@ -25,6 +25,10 @@ function sameOrdered(left, right) {
25
25
  function keySet(values) {
26
26
  return new Set(values.map((value) => value.trim().toLowerCase()).filter(Boolean));
27
27
  }
28
+ function normalizeVersion(value) {
29
+ const parsed = Number(value ?? 0);
30
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
31
+ }
28
32
  function applyListOperation(current, values, operation) {
29
33
  const normalizedValues = normalizeDistinct(values);
30
34
  if (operation === 'replace') {
@@ -59,7 +63,7 @@ async function loadNoteMetadataState(db, ids) {
59
63
  const batch = ids.slice(index, index + chunkSize);
60
64
  const placeholders = batch.map(() => '?').join(', ');
61
65
  const rows = await db.getAll(`
62
- SELECT id, title, tags, zettel_boxes, is_del
66
+ SELECT id, title, tags, zettel_boxes, is_del, version
63
67
  FROM c_note
64
68
  WHERE id IN (${placeholders})
65
69
  `, batch);
@@ -95,6 +99,7 @@ export async function planNoteMetadataMutation(db, input) {
95
99
  before,
96
100
  after,
97
101
  changed: !sameOrdered(before, after),
102
+ expectedVersion: normalizeVersion(note.version),
98
103
  };
99
104
  });
100
105
  return {
@@ -116,11 +121,17 @@ export async function applyNoteMetadataMutation(db, plan) {
116
121
  const timestamp = nowIsoUtc();
117
122
  for (const item of changed) {
118
123
  const column = plan.field === 'tags' ? 'tags' : 'zettel_boxes';
119
- await tx.execute(`
124
+ const result = await tx.execute(`
120
125
  UPDATE c_note
121
126
  SET ${column} = ?, updated_at = ?, version = COALESCE(version, 0) + 1
122
127
  WHERE id = ?
123
- `, [JSON.stringify(item.after), timestamp, item.id]);
128
+ AND COALESCE(version, 0) = ?
129
+ AND (is_del IS NULL OR is_del = 0)
130
+ RETURNING id
131
+ `, [JSON.stringify(item.after), timestamp, item.id, item.expectedVersion]);
132
+ if (result.rows?.length !== 1) {
133
+ throw new DinoxError(`Note changed concurrently: ${item.id}. Re-run the command with fresh data.`);
134
+ }
124
135
  }
125
136
  });
126
137
  return plan;
@@ -36,13 +36,16 @@ export type SearchedNote = {
36
36
  export type NoteDetail = {
37
37
  id: string;
38
38
  created_at: string | null;
39
+ updated_at: string | null;
39
40
  title: string | null;
40
41
  type: string | null;
41
42
  source: string | null;
42
- tags: string | null;
43
+ tags: string[];
43
44
  is_del: number | null;
44
45
  is_starred: boolean;
45
46
  content_md: string | null;
47
+ content_text: string | null;
48
+ content_json: unknown;
46
49
  zettel_boxes: string[];
47
50
  };
48
51
  export type NoteExportItem = {
@@ -90,11 +93,50 @@ export type NotePreview = {
90
93
  export type UpdateNoteResult = {
91
94
  id: string;
92
95
  updated: boolean;
96
+ changed: string[];
93
97
  tags: string[];
94
98
  zettelBoxIds: string[];
95
99
  is_starred: boolean;
96
100
  };
101
+ /**
102
+ * JSON-safe note-search request. Box *names/paths* are carried (not resolved ids) so the
103
+ * same payload can be executed either in-process or sent to the daemon over the wire;
104
+ * id resolution happens inside {@link executeNoteSearch} against whichever db runs it.
105
+ */
106
+ export type NoteSearchExecuteParams = {
107
+ keyword: string;
108
+ includeDeleted: boolean;
109
+ tagsExpression?: string;
110
+ createdAtFrom?: string;
111
+ createdAtTo?: string;
112
+ starred?: boolean;
113
+ boxValues: string[];
114
+ matchEmptyZettelBoxes: boolean;
115
+ sqlExpression?: string;
116
+ limit?: number;
117
+ offset?: number;
118
+ };
119
+ export type NoteSearchExecuteResult = {
120
+ rows: SearchedNote[];
121
+ totalCount: number;
122
+ /** Resolved zettel-box ids, surfaced so callers can echo them in response metadata. */
123
+ zettelBoxIds: string[];
124
+ };
125
+ /**
126
+ * Resolve box names to ids and run the note search. Shared by the local command path and
127
+ * the daemon RPC handler so both produce identical results from one implementation.
128
+ */
129
+ export declare function executeNoteSearch(db: AbstractPowerSyncDatabase, params: NoteSearchExecuteParams): Promise<NoteSearchExecuteResult>;
97
130
  export declare function searchNotes(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesOptions): Promise<SearchedNote[]>;
131
+ type SearchNotesPageOptions = SearchNotesOptions & {
132
+ limit?: number;
133
+ offset?: number;
134
+ };
135
+ type SearchNotesPageResult = {
136
+ rows: SearchedNote[];
137
+ totalCount: number;
138
+ };
139
+ export declare function searchNotesPage(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesPageOptions): Promise<SearchNotesPageResult>;
98
140
  export declare function parseZettelBoxIds(raw: string | null): string[];
99
141
  export declare function getNote(db: AbstractPowerSyncDatabase, id: string): Promise<NoteRow | null>;
100
142
  export declare function getNoteContext(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContext | null>;
@@ -116,9 +158,9 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
116
158
  }): Promise<{
117
159
  id: string;
118
160
  }>;
119
- export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
120
- export declare function validateTagsForUpdate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
121
- export declare function validateTagsForBulk(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
161
+ export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
162
+ export declare function validateTagsForUpdate(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
163
+ export declare function validateTagsForBulk(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
122
164
  export declare function parseStoredTags(raw: string | null): string[];
123
165
  export declare function updateNote(db: AbstractPowerSyncDatabase, input: {
124
166
  id: string;