@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
@@ -8,7 +8,7 @@ export async function getBacklinks(db, noteId) {
8
8
  n.title
9
9
  FROM c_note_link l
10
10
  LEFT JOIN c_note n ON l.src_note_id = n.id
11
- WHERE l.dst_note_id = ? AND l.is_del = 0 AND (n.is_del = 0 OR n.is_del IS NULL)
11
+ WHERE l.dst_note_id = ? AND (l.is_del IS NULL OR l.is_del = 0) AND (n.is_del = 0 OR n.is_del IS NULL)
12
12
  ORDER BY l.created_at DESC`, [noteId]);
13
13
  const backlinks = rows.map((row) => ({
14
14
  note_id: row.src_note_id,
@@ -32,7 +32,7 @@ export async function getOutlinks(db, noteId) {
32
32
  n.title
33
33
  FROM c_note_link l
34
34
  LEFT JOIN c_note n ON l.dst_note_id = n.id
35
- WHERE l.src_note_id = ? AND l.is_del = 0 AND (n.is_del = 0 OR n.is_del IS NULL)
35
+ WHERE l.src_note_id = ? AND (l.is_del IS NULL OR l.is_del = 0) AND (n.is_del = 0 OR n.is_del IS NULL)
36
36
  ORDER BY l.created_at DESC`, [noteId]);
37
37
  const outlinks = rows.map((row) => ({
38
38
  note_id: row.dst_note_id,
@@ -46,27 +46,102 @@ export async function getOutlinks(db, noteId) {
46
46
  count: outlinks.length,
47
47
  };
48
48
  }
49
+ // Keep IN-list sizes under SQLite's bound-parameter limit when expanding a frontier.
50
+ const NOTE_LINK_QUERY_CHUNK_SIZE = 500;
51
+ function chunkIds(ids, size) {
52
+ const chunks = [];
53
+ for (let i = 0; i < ids.length; i += size) {
54
+ chunks.push(ids.slice(i, i + size));
55
+ }
56
+ return chunks;
57
+ }
58
+ /** Backlinks for many notes at once, grouped by the target (dst) note id. */
59
+ async function getBacklinksForNodes(db, ids) {
60
+ const grouped = new Map();
61
+ for (const group of chunkIds(ids, NOTE_LINK_QUERY_CHUNK_SIZE)) {
62
+ const placeholders = group.map(() => '?').join(', ');
63
+ const rows = await db.getAll(`SELECT
64
+ l.id,
65
+ l.src_note_id,
66
+ l.dst_note_id,
67
+ l.relation,
68
+ l.created_at,
69
+ n.title
70
+ FROM c_note_link l
71
+ LEFT JOIN c_note n ON l.src_note_id = n.id
72
+ WHERE l.dst_note_id IN (${placeholders}) AND (l.is_del IS NULL OR l.is_del = 0) AND (n.is_del = 0 OR n.is_del IS NULL)
73
+ ORDER BY l.created_at DESC`, group);
74
+ for (const row of rows) {
75
+ const list = grouped.get(row.dst_note_id) ?? [];
76
+ list.push({
77
+ note_id: row.src_note_id,
78
+ title: row.title || '',
79
+ relation: row.relation,
80
+ link_created_at: row.created_at,
81
+ });
82
+ grouped.set(row.dst_note_id, list);
83
+ }
84
+ }
85
+ return grouped;
86
+ }
87
+ /** Outlinks for many notes at once, grouped by the source (src) note id. */
88
+ async function getOutlinksForNodes(db, ids) {
89
+ const grouped = new Map();
90
+ for (const group of chunkIds(ids, NOTE_LINK_QUERY_CHUNK_SIZE)) {
91
+ const placeholders = group.map(() => '?').join(', ');
92
+ const rows = await db.getAll(`SELECT
93
+ l.id,
94
+ l.src_note_id,
95
+ l.dst_note_id,
96
+ l.relation,
97
+ l.created_at,
98
+ n.title
99
+ FROM c_note_link l
100
+ LEFT JOIN c_note n ON l.dst_note_id = n.id
101
+ WHERE l.src_note_id IN (${placeholders}) AND (l.is_del IS NULL OR l.is_del = 0) AND (n.is_del = 0 OR n.is_del IS NULL)
102
+ ORDER BY l.created_at DESC`, group);
103
+ for (const row of rows) {
104
+ const list = grouped.get(row.src_note_id) ?? [];
105
+ list.push({
106
+ note_id: row.dst_note_id,
107
+ title: row.title || '',
108
+ relation: row.relation,
109
+ link_created_at: row.created_at,
110
+ });
111
+ grouped.set(row.src_note_id, list);
112
+ }
113
+ }
114
+ return grouped;
115
+ }
49
116
  export async function getRelatedNotes(db, noteId, depth = 2) {
50
117
  const visited = new Set([noteId]);
51
118
  const related = new Map();
52
- async function traverse(currentId, currentDepth) {
53
- if (currentDepth > depth)
54
- return;
55
- const [backlinks, outlinks] = await Promise.all([
56
- getBacklinks(db, currentId),
57
- getOutlinks(db, currentId),
119
+ // Breadth-first by degree of separation: each level expands the whole frontier with
120
+ // two batched queries instead of two per node, so cost is O(depth) round-trips rather
121
+ // than O(visited nodes). BFS also attaches each note to the link on its shortest path.
122
+ let frontier = [noteId];
123
+ for (let level = 1; level <= depth && frontier.length > 0; level += 1) {
124
+ const [backlinksByDst, outlinksBySrc] = await Promise.all([
125
+ getBacklinksForNodes(db, frontier),
126
+ getOutlinksForNodes(db, frontier),
58
127
  ]);
59
- for (const link of [...backlinks.backlinks, ...outlinks.outlinks]) {
60
- if (!visited.has(link.note_id)) {
128
+ const nextFrontier = [];
129
+ for (const nodeId of frontier) {
130
+ const neighbors = [
131
+ ...(backlinksByDst.get(nodeId) ?? []),
132
+ ...(outlinksBySrc.get(nodeId) ?? []),
133
+ ];
134
+ for (const link of neighbors) {
135
+ if (visited.has(link.note_id)) {
136
+ continue;
137
+ }
61
138
  visited.add(link.note_id);
62
139
  related.set(link.note_id, link);
63
- if (currentDepth < depth) {
64
- await traverse(link.note_id, currentDepth + 1);
65
- }
140
+ nextFrontier.push(link.note_id);
66
141
  }
67
142
  }
143
+ frontier = nextFrontier;
68
144
  }
69
- await traverse(noteId, 1);
70
145
  return {
71
146
  note_id: noteId,
72
147
  related: Array.from(related.values()),
@@ -86,7 +161,7 @@ export async function getGraphStats(db) {
86
161
  FROM c_note_link l
87
162
  JOIN c_note src ON src.id = l.src_note_id
88
163
  JOIN c_note dst ON dst.id = l.dst_note_id
89
- WHERE l.is_del = 0
164
+ WHERE (l.is_del IS NULL OR l.is_del = 0)
90
165
  AND (src.is_del IS NULL OR src.is_del = 0)
91
166
  AND (dst.is_del IS NULL OR dst.is_del = 0)
92
167
  `, []),
@@ -99,7 +174,7 @@ export async function getGraphStats(db) {
99
174
  FROM c_note_link l
100
175
  JOIN c_note src ON src.id = l.src_note_id
101
176
  JOIN c_note dst ON dst.id = l.dst_note_id
102
- WHERE l.is_del = 0
177
+ WHERE (l.is_del IS NULL OR l.is_del = 0)
103
178
  AND (src.is_del IS NULL OR src.is_del = 0)
104
179
  AND (dst.is_del IS NULL OR dst.is_del = 0)
105
180
  UNION
@@ -107,7 +182,7 @@ export async function getGraphStats(db) {
107
182
  FROM c_note_link l
108
183
  JOIN c_note src ON src.id = l.src_note_id
109
184
  JOIN c_note dst ON dst.id = l.dst_note_id
110
- WHERE l.is_del = 0
185
+ WHERE (l.is_del IS NULL OR l.is_del = 0)
111
186
  AND (src.is_del IS NULL OR src.is_del = 0)
112
187
  AND (dst.is_del IS NULL OR dst.is_del = 0)
113
188
  ) all_linked
@@ -1,5 +1,5 @@
1
1
  import { registerCommandGroup } from '../descriptor/register.js';
2
- import { OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
2
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
3
3
  import { printSuccess } from '../../utils/output.js';
4
4
  import { commandStale, syncBeforeCommand, withCommandRuntime } from '../runtime.js';
5
5
  import { getMetaSchema, getMetaStats } from './repo.js';
@@ -26,7 +26,7 @@ export const metaCommandGroup = {
26
26
  path: 'meta.stats',
27
27
  risk: 'read',
28
28
  auth: 'required',
29
- globalOptions: [...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS],
29
+ globalOptions: [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS],
30
30
  output: {
31
31
  type: 'structured',
32
32
  summary: 'Metadata stats payload',
@@ -43,11 +43,11 @@ export const metaCommandGroup = {
43
43
  path: 'meta.schema',
44
44
  risk: 'read',
45
45
  auth: 'none',
46
- globalOptions: [],
46
+ globalOptions: FORMAT_OPTIONS,
47
47
  output: {
48
48
  type: 'structured',
49
49
  summary: 'Structured Dinox data schema payload',
50
- fields: ['tables', 'relations', 'command_schema'],
50
+ fields: ['version', 'entities'],
51
51
  },
52
52
  examples: ['dino meta schema --format json'],
53
53
  },
@@ -33,7 +33,7 @@ export function getMetaSchema() {
33
33
  version: 1,
34
34
  entities: {
35
35
  note_search_result: {
36
- fields: ['id', 'title', 'summary', 'tags', 'created_at', 'zettel_boxes'],
36
+ fields: ['id', 'title', 'summary', 'tags', 'created_at', 'boxes', 'is_starred'],
37
37
  meta_fields: [
38
38
  'query',
39
39
  'returned_count',
@@ -55,13 +55,14 @@ export function getMetaSchema() {
55
55
  'created_at',
56
56
  'updated_at',
57
57
  'is_del',
58
- 'zettel_boxes',
58
+ 'is_starred',
59
+ 'boxes',
59
60
  'links.backlinks',
60
61
  'links.outlinks',
61
62
  ],
62
63
  },
63
64
  graph_stats: {
64
- fields: ['notes_count', 'links_count', 'connected_notes_count', 'isolated_notes_count'],
65
+ fields: ['notes_count', 'links_count', 'connected_notes_count', 'isolated_notes_count', 'stale'],
65
66
  },
66
67
  meta_stats: {
67
68
  fields: ['notes_count', 'tags_count', 'boxes_count', 'latest_note_updated_at'],
@@ -1,5 +1,5 @@
1
1
  import type { CommandExecutionContext } from '../descriptor/types.js';
2
- export declare function runNoteSearch({ command, options, args }: CommandExecutionContext): Promise<void>;
2
+ export declare function runNoteSearch({ command, options, args, globals }: CommandExecutionContext): Promise<void>;
3
3
  export declare function runNoteGet({ command, options, args }: CommandExecutionContext): Promise<void>;
4
4
  export declare function runNotePreview({ command, options, args }: CommandExecutionContext): Promise<void>;
5
5
  export declare function runNoteDetail({ command, options, args }: CommandExecutionContext): Promise<void>;
@@ -5,14 +5,16 @@ import { generateId } from '../../utils/id.js';
5
5
  import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
6
6
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
7
7
  import { DinoxError } from '../../utils/errors.js';
8
- import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, waitForMutationUpload, } from '../runtime.js';
9
- import { createNote, getNote, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
8
+ import { commandStale, printStaleCacheWarning, resolveWriteDurabilityForRuntime, waitForMutationUpload, } from '../runtime.js';
9
+ import { createNote, executeNoteSearch, getNote, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForUpdate, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
10
+ import { routeRead } from '../../daemon/route.js';
10
11
  import { buildNoteCreateDryRunPreview, buildNoteDeleteDryRunPreview, buildNoteUpdateDryRunPreview, } from './dryRun.js';
11
12
  import { applyNoteMetadataMutation, planNoteMetadataMutation, } from './metadataMutation.js';
13
+ import { buildBatchWriteReceiptFields, readNoteRevisionReceipts, readNoteWriteReceipt, } from './writeReceipt.js';
12
14
  import { parseNoteSearchFields, projectNoteSearchFields } from './searchOutput.js';
13
15
  import { parseCreatedAtRange } from './searchTime.js';
14
16
  import { buildNoteSearchResponse, normalizeDaysForMeta } from './searchResponse.js';
15
- import { parseNoteType, parsePreviewLineCount, parseSearchLimit, parseSearchListInput, parseSearchOffset, parseStarredOption, parseStringInput, parseStringListInput, parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
17
+ import { parseMutationListInput, parseNoteType, parsePreviewLineCount, parseSearchLimit, parseSearchListInput, parseSearchOffset, parseStarredOption, parseStringInput, parseStringListInput, parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
16
18
  function toPublicNoteContext(note) {
17
19
  if (!note) {
18
20
  return null;
@@ -45,6 +47,7 @@ async function executeNoteUpdateOperation(args) {
45
47
  await withNoteDatabase(args.command, async (runtime) => {
46
48
  const { db, config, offline } = runtime;
47
49
  await syncBeforeNoteCommand(runtime);
50
+ const durability = resolveWriteDurabilityForRuntime(runtime, args.durability);
48
51
  const ids = await parseTargetNoteIds(args.id, args.idsOption, 'update');
49
52
  const hasTags = typeof args.tagsOption === 'string';
50
53
  const hasBoxes = typeof args.boxOption === 'string';
@@ -52,11 +55,20 @@ async function executeNoteUpdateOperation(args) {
52
55
  if (!hasTags && !hasBoxes && !hasStarred) {
53
56
  throw new DinoxError('At least one option is required: --tags, --boxes, or --starred');
54
57
  }
55
- const tags = hasTags
56
- ? await validateTagsForUpdate(db, await parseStringListInput(args.tagsOption, '--tags'))
58
+ const requestedTags = hasTags
59
+ ? await parseMutationListInput(args.tagsOption, '--tags')
57
60
  : undefined;
58
- const zettelBoxIds = hasBoxes
59
- ? await resolveZettelBoxIdsForUpdate(db, await parseStringListInput(args.boxOption, '--boxes'))
61
+ const requestedBoxNames = hasBoxes
62
+ ? await parseMutationListInput(args.boxOption, '--boxes')
63
+ : undefined;
64
+ const tagUserId = requestedTags
65
+ ? (await resolveAuthIdentity(config)).userId
66
+ : undefined;
67
+ const tags = requestedTags
68
+ ? await validateTagsForUpdate(db, requestedTags, tagUserId)
69
+ : undefined;
70
+ const zettelBoxIds = requestedBoxNames
71
+ ? await resolveZettelBoxIdsForUpdate(db, requestedBoxNames)
60
72
  : undefined;
61
73
  await assertNotesUpdatable(db, ids);
62
74
  if (args.dryRun) {
@@ -64,9 +76,10 @@ async function executeNoteUpdateOperation(args) {
64
76
  db,
65
77
  config,
66
78
  offline,
79
+ userId: tagUserId,
67
80
  ids,
68
81
  tagsInput: tags,
69
- zettelBoxNames: hasBoxes ? await parseStringListInput(args.boxOption, '--boxes') : undefined,
82
+ zettelBoxNames: requestedBoxNames,
70
83
  starred: hasStarred ? args.starred : undefined,
71
84
  commandName: args.commandName ?? 'dino note update',
72
85
  action: args.actionName ?? 'update_note',
@@ -84,20 +97,22 @@ async function executeNoteUpdateOperation(args) {
84
97
  }));
85
98
  }
86
99
  const updatedCount = results.filter((item) => item.updated).length;
87
- printUploadDisabledWarning(runtime);
88
- await waitForMutationUpload(runtime);
100
+ const upload = await waitForMutationUpload(runtime, { durability });
101
+ const stale = commandStale(runtime);
102
+ const revisions = await readNoteRevisionReceipts(db, results.map((item) => item.id), new Map(results.map((item) => [item.id, item.changed])));
89
103
  const payload = {
90
104
  ok: true,
91
105
  total: results.length,
92
106
  updatedCount,
107
+ ...buildBatchWriteReceiptFields(upload, stale),
93
108
  results: results.map((item) => ({
94
109
  id: item.id,
95
110
  updated: item.updated,
96
111
  tags: item.tags,
97
112
  boxes: item.zettelBoxIds,
98
113
  is_starred: item.is_starred,
114
+ ...revisions.get(item.id),
99
115
  })),
100
- stale: commandStale(runtime),
101
116
  };
102
117
  if (structuredOutput) {
103
118
  printSuccess(payload);
@@ -144,11 +159,15 @@ async function executeNoteMetadataListOperation(args) {
144
159
  await withNoteDatabase(args.command, async (runtime) => {
145
160
  const { db, config, offline } = runtime;
146
161
  await syncBeforeNoteCommand(runtime);
162
+ const durability = resolveWriteDurabilityForRuntime(runtime, args.options.durability);
147
163
  const ids = await parseTargetNoteIds(args.id, args.idsOption, 'update');
148
164
  const selected = selectMetadataOperation(args.options);
149
- const requestedValues = await parseStringListInput(selected.rawValues, selected.optionName);
165
+ const requestedValues = await parseMutationListInput(selected.rawValues, selected.optionName);
166
+ const tagUserId = args.field === 'tags'
167
+ ? (await resolveAuthIdentity(config)).userId
168
+ : undefined;
150
169
  const values = args.field === 'tags'
151
- ? await validateTagsForUpdate(db, requestedValues)
170
+ ? await validateTagsForUpdate(db, requestedValues, tagUserId)
152
171
  : await resolveZettelBoxIdsForUpdate(db, requestedValues);
153
172
  const plan = await planNoteMetadataMutation(db, {
154
173
  ids,
@@ -166,9 +185,6 @@ async function executeNoteMetadataListOperation(args) {
166
185
  summary: `Preview ${args.field} ${selected.operation} for ${plan.total} note(s); ${plan.changedCount} note(s) would change.`,
167
186
  warnings: [
168
187
  ...(offline ? ['Execution would run in offline mode and only use the local cache.'] : []),
169
- ...(!offline && !config.powersync.uploadBaseUrl
170
- ? ['Execution would be local-only because powersync.uploadBaseUrl is unset.']
171
- : []),
172
188
  ],
173
189
  inputs: {
174
190
  ids,
@@ -187,20 +203,22 @@ async function executeNoteMetadataListOperation(args) {
187
203
  return;
188
204
  }
189
205
  await applyNoteMetadataMutation(db, plan);
190
- printUploadDisabledWarning(runtime);
191
- await waitForMutationUpload(runtime);
206
+ const upload = await waitForMutationUpload(runtime, { durability });
207
+ const stale = commandStale(runtime);
208
+ const revisions = await readNoteRevisionReceipts(db, plan.results.map((item) => item.id), new Map(plan.results.map((item) => [item.id, item.changed ? [args.field] : []])));
192
209
  const payload = {
193
210
  ok: true,
194
211
  field: args.field,
195
212
  operation: selected.operation,
196
213
  total: plan.total,
197
214
  updatedCount: plan.changedCount,
215
+ ...buildBatchWriteReceiptFields(upload, stale),
198
216
  results: plan.results.map((item) => ({
199
217
  id: item.id,
200
218
  updated: item.changed,
201
219
  [args.field]: item.after,
220
+ ...revisions.get(item.id),
202
221
  })),
203
- stale: commandStale(runtime),
204
222
  };
205
223
  if (structuredOutput) {
206
224
  printSuccess(payload);
@@ -210,60 +228,66 @@ async function executeNoteMetadataListOperation(args) {
210
228
  printStaleCacheWarning(runtime);
211
229
  });
212
230
  }
213
- export async function runNoteSearch({ command, options, args }) {
231
+ export async function runNoteSearch({ command, options, args, globals }) {
214
232
  const searchOptions = options;
215
- await withNoteDatabase(command, async (runtime) => {
216
- const { db } = runtime;
217
- await syncBeforeNoteCommand(runtime);
218
- const keyword = args.query?.trim() ?? '';
219
- const tagsExpression = typeof searchOptions.tags === 'string' ? searchOptions.tags.trim() : '';
220
- const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
221
- from: searchOptions.from,
222
- to: searchOptions.to,
223
- days: searchOptions.days,
224
- });
225
- const starred = typeof searchOptions.starred === 'string'
226
- ? parseStarredOption(searchOptions.starred, '--starred')
227
- : undefined;
228
- const boxInput = typeof searchOptions.boxes === 'string' ? searchOptions.boxes : '';
229
- const parsedBoxInput = boxInput
230
- ? await parseSearchListInput(boxInput, '--boxes')
231
- : { values: [], isExplicitEmptyArray: false };
232
- const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, parsedBoxInput.values);
233
- const sqlExpression = typeof searchOptions.sql === 'string' ? searchOptions.sql.trim() : '';
234
- const limit = parseSearchLimit(searchOptions.limit);
235
- const offset = parseSearchOffset(searchOptions.offset);
236
- const fields = parseNoteSearchFields(searchOptions.fields, '--fields');
237
- const rows = await searchNotes(db, keyword, {
238
- includeDeleted: Boolean(searchOptions.includeDeleted),
239
- tagsExpression: tagsExpression || undefined,
240
- createdAtFrom,
241
- createdAtTo,
242
- starred,
243
- zettelBoxIds,
244
- matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
245
- sqlExpression: sqlExpression || undefined,
246
- });
247
- printSuccess(buildNoteSearchResponse({
248
- query: keyword,
249
- notes: projectNoteSearchFields(rows.slice(offset, offset + limit), fields),
250
- totalCount: rows.length,
251
- stale: commandStale(runtime),
252
- includeDeleted: Boolean(searchOptions.includeDeleted),
253
- limit,
254
- offset,
255
- fields,
256
- tagsExpression: tagsExpression || undefined,
257
- createdAtFrom,
258
- createdAtTo,
259
- days: normalizeDaysForMeta(searchOptions.days),
260
- starred,
261
- boxNames: parsedBoxInput.values,
262
- boxIds: zettelBoxIds,
263
- matchEmptyBoxes: parsedBoxInput.isExplicitEmptyArray,
264
- sqlExpression: sqlExpression || undefined,
265
- }));
233
+ // Option parsing and output formatting stay here, in one place; only the db-touching
234
+ // step (box resolution + search) runs through executeNoteSearch — locally or on the daemon.
235
+ const keyword = args.query?.trim() ?? '';
236
+ const tagsExpression = typeof searchOptions.tags === 'string' ? searchOptions.tags.trim() : '';
237
+ const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
238
+ from: searchOptions.from,
239
+ to: searchOptions.to,
240
+ days: searchOptions.days,
266
241
  });
242
+ const starred = typeof searchOptions.starred === 'string'
243
+ ? parseStarredOption(searchOptions.starred, '--starred')
244
+ : undefined;
245
+ const boxInput = typeof searchOptions.boxes === 'string' ? searchOptions.boxes : '';
246
+ const parsedBoxInput = boxInput
247
+ ? await parseSearchListInput(boxInput, '--boxes')
248
+ : { values: [], isExplicitEmptyArray: false };
249
+ const sqlExpression = typeof searchOptions.sql === 'string' ? searchOptions.sql.trim() : '';
250
+ const limit = parseSearchLimit(searchOptions.limit);
251
+ const offset = parseSearchOffset(searchOptions.offset);
252
+ const fields = parseNoteSearchFields(searchOptions.fields, '--fields');
253
+ const execParams = {
254
+ keyword,
255
+ includeDeleted: Boolean(searchOptions.includeDeleted),
256
+ tagsExpression: tagsExpression || undefined,
257
+ createdAtFrom,
258
+ createdAtTo,
259
+ starred,
260
+ boxValues: parsedBoxInput.values,
261
+ matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
262
+ sqlExpression: sqlExpression || undefined,
263
+ limit,
264
+ offset,
265
+ };
266
+ const result = await routeRead(globals, 'note.search', execParams, () => withNoteDatabase(command, async (runtime) => {
267
+ await syncBeforeNoteCommand(runtime);
268
+ const local = await executeNoteSearch(runtime.db, execParams);
269
+ return { ...local, stale: commandStale(runtime) };
270
+ }));
271
+ const stale = result.stale ?? false;
272
+ printSuccess(buildNoteSearchResponse({
273
+ query: keyword,
274
+ notes: projectNoteSearchFields(result.rows, fields),
275
+ totalCount: result.totalCount,
276
+ stale,
277
+ includeDeleted: Boolean(searchOptions.includeDeleted),
278
+ limit,
279
+ offset,
280
+ fields,
281
+ tagsExpression: tagsExpression || undefined,
282
+ createdAtFrom,
283
+ createdAtTo,
284
+ days: normalizeDaysForMeta(searchOptions.days),
285
+ starred,
286
+ boxNames: parsedBoxInput.values,
287
+ boxIds: result.zettelBoxIds,
288
+ matchEmptyBoxes: parsedBoxInput.isExplicitEmptyArray,
289
+ sqlExpression: sqlExpression || undefined,
290
+ }));
267
291
  }
268
292
  export async function runNoteGet({ command, options, args }) {
269
293
  const getOptions = options;
@@ -355,6 +379,7 @@ export async function runNoteCreate({ command, options }) {
355
379
  await withNoteDatabase(command, async (runtime) => {
356
380
  const { db, config, offline } = runtime;
357
381
  await syncBeforeNoteCommand(runtime);
382
+ const durability = resolveWriteDurabilityForRuntime(runtime, createOptions.durability);
358
383
  const identity = await resolveAuthIdentity(config);
359
384
  const contentMd = await parseStringInput(String(createOptions.content));
360
385
  const canonicalContentMd = await canonicalizeBidlinksInMarkdown(db, contentMd, identity.userId);
@@ -388,13 +413,20 @@ export async function runNoteCreate({ command, options }) {
388
413
  contentMd: canonicalContentMd,
389
414
  contentJson,
390
415
  contentText,
391
- tags: await validateTagsForCreate(db, tagsInput),
416
+ tags: await validateTagsForCreate(db, tagsInput, identity.userId),
392
417
  zettelBoxIds: await resolveZettelBoxIdsForCreate(db, boxNames),
393
418
  userId: identity.userId,
394
419
  });
395
- printUploadDisabledWarning(runtime);
396
- await waitForMutationUpload(runtime);
397
- const payload = { ok: true, id, stale: commandStale(runtime) };
420
+ const upload = await waitForMutationUpload(runtime, { durability });
421
+ const payload = {
422
+ ok: true,
423
+ id,
424
+ ...(await readNoteWriteReceipt(db, id, {
425
+ changed: ['title', 'type', 'content_md', 'content_json', 'content_text', 'tags', 'boxes'],
426
+ upload,
427
+ stale: commandStale(runtime),
428
+ })),
429
+ };
398
430
  if (structuredOutput) {
399
431
  printSuccess(payload);
400
432
  return;
@@ -413,14 +445,22 @@ export async function runNoteDelete({ command, options, args }) {
413
445
  await withNoteDatabase(command, async (runtime) => {
414
446
  const { db, config, offline } = runtime;
415
447
  await syncBeforeNoteCommand(runtime);
448
+ const durability = resolveWriteDurabilityForRuntime(runtime, deleteOptions.durability);
416
449
  if (Boolean(deleteOptions.dryRun)) {
417
450
  printDryRunPreview(await buildNoteDeleteDryRunPreview({ db, config, offline, id }));
418
451
  return;
419
452
  }
420
453
  await softDeleteNote(db, id);
421
- printUploadDisabledWarning(runtime);
422
- await waitForMutationUpload(runtime);
423
- const payload = { ok: true, id, stale: commandStale(runtime) };
454
+ const upload = await waitForMutationUpload(runtime, { durability });
455
+ const payload = {
456
+ ok: true,
457
+ id,
458
+ ...(await readNoteWriteReceipt(db, id, {
459
+ changed: ['is_del'],
460
+ upload,
461
+ stale: commandStale(runtime),
462
+ })),
463
+ };
424
464
  if (structuredOutput) {
425
465
  printSuccess(payload);
426
466
  return;
@@ -443,6 +483,7 @@ export async function runNoteUpdate({ command, options, args }) {
443
483
  tagsOption: updateOptions.tags,
444
484
  commandName: 'dino note update',
445
485
  actionName: 'update_note',
486
+ durability: updateOptions.durability,
446
487
  });
447
488
  }
448
489
  export async function runNoteTag({ command, options, args }) {
@@ -479,6 +520,7 @@ export async function runNoteStar({ command, options, args }) {
479
520
  staleVerb: 'starred',
480
521
  commandName: 'dino note star',
481
522
  actionName: 'star_note',
523
+ durability: starOptions.durability,
482
524
  });
483
525
  }
484
526
  export async function runNoteUnstar({ command, options, args }) {
@@ -493,5 +535,6 @@ export async function runNoteUnstar({ command, options, args }) {
493
535
  staleVerb: 'unstarred',
494
536
  commandName: 'dino note unstar',
495
537
  actionName: 'unstar_note',
538
+ durability: unstarOptions.durability,
496
539
  });
497
540
  }
@@ -16,6 +16,7 @@ type NoteBulkOptions = {
16
16
  boxReplace?: string;
17
17
  expectedCount?: string;
18
18
  confirm?: boolean;
19
+ durability?: unknown;
19
20
  dryRun?: boolean;
20
21
  };
21
22
  type NoteBulkMutationSelection = {