@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
@@ -3,6 +3,7 @@ import { nowIsoUtc } from '../../utils/time.js';
3
3
  import { tokenizeWithJieba } from '../../utils/tokenize.js';
4
4
  import { canonicalizeBoxPath, listBoxes } from '../boxes/repo.js';
5
5
  import { listTags } from '../tags/repo.js';
6
+ import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
6
7
  import { buildNoteSearchSqlFilter } from './searchSql.js';
7
8
  const MAX_SEARCH_SUMMARY_CHARS = 200;
8
9
  function normalizeStoredStarred(value) {
@@ -14,7 +15,30 @@ function normalizeStoredStarred(value) {
14
15
  }
15
16
  return false;
16
17
  }
18
+ /**
19
+ * Resolve box names to ids and run the note search. Shared by the local command path and
20
+ * the daemon RPC handler so both produce identical results from one implementation.
21
+ */
22
+ export async function executeNoteSearch(db, params) {
23
+ const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, params.boxValues);
24
+ const result = await searchNotesPage(db, params.keyword, {
25
+ includeDeleted: params.includeDeleted,
26
+ tagsExpression: params.tagsExpression,
27
+ createdAtFrom: params.createdAtFrom,
28
+ createdAtTo: params.createdAtTo,
29
+ starred: params.starred,
30
+ zettelBoxIds,
31
+ matchEmptyZettelBoxes: params.matchEmptyZettelBoxes,
32
+ sqlExpression: params.sqlExpression,
33
+ limit: params.limit,
34
+ offset: params.offset,
35
+ });
36
+ return { rows: result.rows, totalCount: result.totalCount, zettelBoxIds };
37
+ }
17
38
  export async function searchNotes(db, query, options) {
39
+ return (await searchNotesPage(db, query, options)).rows;
40
+ }
41
+ export async function searchNotesPage(db, query, options) {
18
42
  const normalizedQuery = query.trim();
19
43
  const tagFilter = buildTagFilter(options.tagsExpression, 'n');
20
44
  const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
@@ -30,8 +54,11 @@ export async function searchNotes(db, query, options) {
30
54
  const ftsMatch = buildFtsMatchQuery(normalizedQuery);
31
55
  if (ftsMatch) {
32
56
  try {
33
- rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter, starredFilter, zettelBoxFilter, sqlFilter);
34
- return enrichSearchRows(db, rows);
57
+ const ftsResult = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter, starredFilter, zettelBoxFilter, sqlFilter, options.limit, options.offset);
58
+ return {
59
+ rows: await enrichSearchRows(db, ftsResult.rows),
60
+ totalCount: ftsResult.totalCount,
61
+ };
35
62
  }
36
63
  catch (error) {
37
64
  const message = error instanceof Error ? error.message : String(error);
@@ -54,14 +81,15 @@ export async function searchNotes(db, query, options) {
54
81
  ];
55
82
  const params = [];
56
83
  if (normalizedQuery.length > 0) {
57
- const like = `%${normalizedQuery}%`;
84
+ const like = `%${escapeLikePattern(normalizedQuery)}%`;
58
85
  whereClauses.push(`
59
86
  (
60
- (n.title IS NOT NULL AND n.title LIKE ?)
61
- OR (n.content_text IS NOT NULL AND n.content_text LIKE ?)
87
+ (n.title IS NOT NULL AND n.title LIKE ? ESCAPE '\\')
88
+ OR (n.content_text IS NOT NULL AND n.content_text LIKE ? ESCAPE '\\')
89
+ OR (n.content_md IS NOT NULL AND n.content_md LIKE ? ESCAPE '\\')
62
90
  )
63
91
  `);
64
- params.push(like, like);
92
+ params.push(like, like, like);
65
93
  }
66
94
  if (tagFilter) {
67
95
  whereClauses.push(tagFilter.sql);
@@ -83,6 +111,15 @@ export async function searchNotes(db, query, options) {
83
111
  whereClauses.push(sqlFilter.sql);
84
112
  params.push(...sqlFilter.params);
85
113
  }
114
+ const baseFromWhere = `
115
+ FROM c_note n
116
+ WHERE ${whereClauses.join('\n AND ')}
117
+ `;
118
+ const paginated = typeof options.limit === 'number';
119
+ const totalCount = paginated ? await countSearchRows(db, baseFromWhere, params) : null;
120
+ if (totalCount === 0) {
121
+ return { rows: [], totalCount };
122
+ }
86
123
  rows = await db.getAll(`
87
124
  SELECT
88
125
  n.id,
@@ -93,13 +130,13 @@ export async function searchNotes(db, query, options) {
93
130
  n.zettel_boxes,
94
131
  n.created_at,
95
132
  n.is_starred
96
- FROM c_note n
97
- WHERE ${whereClauses.join('\n AND ')}
98
- ORDER BY COALESCE(n.updated_at, n.created_at) DESC
99
- `, params);
100
- return enrichSearchRows(db, rows);
133
+ ${baseFromWhere}
134
+ ORDER BY COALESCE(n.updated_at, n.created_at) DESC, n.id DESC
135
+ ${buildPaginationSql(options.limit, options.offset)}
136
+ `, appendPaginationParams(params, options.limit, options.offset));
137
+ return { rows: await enrichSearchRows(db, rows), totalCount: totalCount ?? rows.length };
101
138
  }
102
- async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, starredFilter, zettelBoxFilter, sqlFilter) {
139
+ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, starredFilter, zettelBoxFilter, sqlFilter, limit, offset) {
103
140
  const whereClauses = ['(n.is_del IS NULL OR n.is_del = 0)', 'note_local_fts MATCH ?'];
104
141
  const params = [matchQuery];
105
142
  if (tagFilter) {
@@ -122,7 +159,17 @@ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, st
122
159
  whereClauses.push(sqlFilter.sql);
123
160
  params.push(...sqlFilter.params);
124
161
  }
125
- return db.getAll(`
162
+ const baseFromWhere = `
163
+ FROM note_local_fts f
164
+ JOIN c_note n ON n.id = f.note_id
165
+ WHERE ${whereClauses.join('\n AND ')}
166
+ `;
167
+ const paginated = typeof limit === 'number';
168
+ const totalCount = paginated ? await countSearchRows(db, baseFromWhere, params) : null;
169
+ if (totalCount === 0) {
170
+ return { rows: [], totalCount };
171
+ }
172
+ const rows = await db.getAll(`
126
173
  SELECT
127
174
  n.id,
128
175
  n.title,
@@ -132,11 +179,31 @@ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, st
132
179
  n.zettel_boxes,
133
180
  n.created_at,
134
181
  n.is_starred
135
- FROM note_local_fts f
136
- JOIN c_note n ON n.id = f.note_id
137
- WHERE ${whereClauses.join('\n AND ')}
138
- ORDER BY bm25(note_local_fts, 8.0, 4.0, 1.0) ASC, COALESCE(n.updated_at, n.created_at) DESC
182
+ ${baseFromWhere}
183
+ ORDER BY bm25(note_local_fts, 0.0, 8.0, 4.0, 1.0) ASC, COALESCE(n.updated_at, n.created_at) DESC, n.id DESC
184
+ ${buildPaginationSql(limit, offset)}
185
+ `, appendPaginationParams(params, limit, offset));
186
+ return { rows, totalCount: totalCount ?? rows.length };
187
+ }
188
+ function escapeLikePattern(value) {
189
+ return value.replace(/[\\%_]/g, (match) => `\\${match}`);
190
+ }
191
+ async function countSearchRows(db, fromWhereSql, params) {
192
+ const rows = await db.getAll(`
193
+ SELECT COUNT(*) AS total_count
194
+ ${fromWhereSql}
139
195
  `, params);
196
+ const value = Number(rows[0]?.total_count ?? 0);
197
+ return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
198
+ }
199
+ function buildPaginationSql(limit, offset) {
200
+ return typeof limit === 'number' ? 'LIMIT ? OFFSET ?' : '';
201
+ }
202
+ function appendPaginationParams(params, limit, offset) {
203
+ if (typeof limit !== 'number') {
204
+ return params;
205
+ }
206
+ return [...params, limit, offset ?? 0];
140
207
  }
141
208
  async function enrichSearchRows(db, rows) {
142
209
  const allBoxIds = Array.from(new Set(rows.flatMap((row) => parseZettelBoxIds(row.zettel_boxes))));
@@ -536,7 +603,7 @@ async function getLinkCount(db, direction, noteId) {
536
603
  FROM c_note_link l
537
604
  JOIN c_note n ON n.id = l.${column}
538
605
  WHERE l.${boundColumn} = ?
539
- AND l.is_del = 0
606
+ AND (l.is_del IS NULL OR l.is_del = 0)
540
607
  AND (n.is_del IS NULL OR n.is_del = 0)
541
608
  `, [noteId]);
542
609
  return Number(row?.count ?? 0);
@@ -636,13 +703,16 @@ function mapNoteDetailRow(note, boxNameMap) {
636
703
  return {
637
704
  id: note.id,
638
705
  created_at: note.created_at,
706
+ updated_at: note.updated_at,
639
707
  title: note.title,
640
708
  type: note.type,
641
709
  source: note.source,
642
- tags: note.tags,
710
+ tags: parseStoredTags(note.tags),
643
711
  is_del: note.is_del,
644
712
  is_starred: normalizeStoredStarred(note.is_starred),
645
713
  content_md: note.content_md,
714
+ content_text: note.content_text,
715
+ content_json: parseStoredJsonValue(note.content_json),
646
716
  zettel_boxes: zettelBoxes,
647
717
  };
648
718
  }
@@ -697,6 +767,7 @@ export async function getNoteDetails(db, ids) {
697
767
  SELECT
698
768
  id,
699
769
  created_at,
770
+ updated_at,
700
771
  title,
701
772
  type,
702
773
  source,
@@ -704,6 +775,8 @@ export async function getNoteDetails(db, ids) {
704
775
  is_del,
705
776
  is_starred,
706
777
  content_md,
778
+ content_text,
779
+ content_json,
707
780
  zettel_boxes AS zettel_boxes_raw
708
781
  FROM c_note
709
782
  WHERE id IN (${placeholders})
@@ -792,7 +865,9 @@ export async function createNote(db, input) {
792
865
  const contentJson = JSON.stringify(input.contentJson);
793
866
  const tags = JSON.stringify(input.tags);
794
867
  const zettelBoxes = JSON.stringify(input.zettelBoxIds);
795
- const imageDetail = JSON.stringify(Array.isArray(input.imageDetail) ? input.imageDetail : []);
868
+ const imageDetail = JSON.stringify(Array.isArray(input.imageDetail)
869
+ ? input.imageDetail
870
+ : deriveImageDetailFromContentJson(input.contentJson, input.id));
796
871
  const columns = await db.getAll('PRAGMA table_info(c_note)', []);
797
872
  const columnNames = new Set(columns
798
873
  .map((row) => row.name?.trim().toLowerCase() ?? '')
@@ -893,20 +968,24 @@ function buildZettelBoxRetryQuestion(intent, type) {
893
968
  }
894
969
  return `Do you want to add these missing boxes and retry \`${command}\`?`;
895
970
  }
896
- export async function validateTagsForCreate(db, requestedTags) {
897
- return validateTagsForWrite(db, requestedTags, 'dino note create');
971
+ export async function validateTagsForCreate(db, requestedTags, userId) {
972
+ return validateTagsForWrite(db, requestedTags, userId, 'dino note create');
898
973
  }
899
- export async function validateTagsForUpdate(db, requestedTags) {
900
- return validateTagsForWrite(db, requestedTags, 'dino note update');
974
+ export async function validateTagsForUpdate(db, requestedTags, userId) {
975
+ return validateTagsForWrite(db, requestedTags, userId, 'dino note update');
901
976
  }
902
- export async function validateTagsForBulk(db, requestedTags) {
903
- return validateTagsForWrite(db, requestedTags, 'dino note bulk');
977
+ export async function validateTagsForBulk(db, requestedTags, userId) {
978
+ return validateTagsForWrite(db, requestedTags, userId, 'dino note bulk');
904
979
  }
905
- async function validateTagsForWrite(db, requestedTags, retryCommand) {
980
+ async function validateTagsForWrite(db, requestedTags, userId, retryCommand) {
981
+ const normalizedUserId = userId.trim();
982
+ if (!normalizedUserId) {
983
+ throw new DinoxError('Tag validation requires the current userId. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
984
+ }
906
985
  if (requestedTags.length === 0) {
907
986
  return [];
908
987
  }
909
- const tags = await listTags(db);
988
+ const tags = await listTags(db, { userId: normalizedUserId });
910
989
  const existingTagMap = new Map();
911
990
  for (const tag of tags) {
912
991
  const key = normalizeLookupKey(tag);
@@ -1026,6 +1105,7 @@ export async function updateNote(db, input) {
1026
1105
  return {
1027
1106
  id,
1028
1107
  updated: false,
1108
+ changed: [],
1029
1109
  tags: currentTags,
1030
1110
  zettelBoxIds: currentZettelBoxIds,
1031
1111
  is_starred: currentStarred,
@@ -1057,6 +1137,11 @@ export async function updateNote(db, input) {
1057
1137
  return {
1058
1138
  id,
1059
1139
  updated: true,
1140
+ changed: [
1141
+ ...(tagsChanged ? ['tags'] : []),
1142
+ ...(boxesChanged ? ['boxes'] : []),
1143
+ ...(starredChanged ? ['is_starred'] : []),
1144
+ ],
1060
1145
  tags: nextTags,
1061
1146
  zettelBoxIds: nextZettelBoxIds,
1062
1147
  is_starred: nextStarred,
@@ -9,6 +9,7 @@ export declare function parseStarredOption(value: unknown, optionName: string):
9
9
  export declare function parseStringInput(value: string): Promise<string>;
10
10
  export declare function parseListInput(raw: string, optionName: string): string[];
11
11
  export declare function parseStringListInput(value: string, optionName: string): Promise<string[]>;
12
+ export declare function parseMutationListInput(value: string, optionName: string): Promise<string[]>;
12
13
  export type ParsedSearchListInput = {
13
14
  values: string[];
14
15
  isExplicitEmptyArray: boolean;
@@ -16,4 +17,4 @@ export type ParsedSearchListInput = {
16
17
  export declare function parseSearchListInput(value: string, optionName: string): Promise<ParsedSearchListInput>;
17
18
  export declare function parseTargetNoteIds(idArg: string | undefined, idsOption: string | undefined, commandName: 'detail' | 'update'): Promise<string[]>;
18
19
  export declare function syncBeforeNoteCommand(runtime: CommandRuntime): Promise<void>;
19
- export declare function withNoteDatabase(command: Command, run: (runtime: NoteCommandRuntime) => Promise<void>): Promise<void>;
20
+ export declare function withNoteDatabase<T = void>(command: Command, run: (runtime: NoteCommandRuntime) => Promise<T>): Promise<T>;
@@ -34,6 +34,14 @@ export function parseSearchOffset(value) {
34
34
  if (value === undefined || value === null || value === '') {
35
35
  return 0;
36
36
  }
37
+ // Commander supplies the option's numeric defaultValue (0) as a number, while an
38
+ // explicit `--offset N` arrives as a string; accept both.
39
+ if (typeof value === 'number') {
40
+ if (!Number.isInteger(value) || value < 0) {
41
+ throw new DinoxError('--offset must be a non-negative integer');
42
+ }
43
+ return value;
44
+ }
37
45
  if (typeof value !== 'string') {
38
46
  throw new DinoxError('--offset must be a non-negative integer');
39
47
  }
@@ -114,6 +122,14 @@ export async function parseStringListInput(value, optionName) {
114
122
  const raw = await readStringOrFile(value);
115
123
  return parseListInput(raw, optionName);
116
124
  }
125
+ export async function parseMutationListInput(value, optionName) {
126
+ const raw = await readStringOrFile(value);
127
+ const values = parseListInput(raw, optionName);
128
+ if (values.length === 0 && !isExplicitEmptyArrayLiteral(raw)) {
129
+ throw new DinoxError(`${optionName} requires at least one value. Use [] to explicitly clear the list.`);
130
+ }
131
+ return values;
132
+ }
117
133
  export async function parseSearchListInput(value, optionName) {
118
134
  const raw = await readStringOrFile(value);
119
135
  return {
@@ -155,5 +171,5 @@ export async function syncBeforeNoteCommand(runtime) {
155
171
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
156
172
  }
157
173
  export async function withNoteDatabase(command, run) {
158
- await withCommandRuntime(command, run);
174
+ return withCommandRuntime(command, run);
159
175
  }
@@ -430,13 +430,20 @@ async function buildZettelBoxIdLookup(ast, resolveZettelBoxNames) {
430
430
  seen.add(key);
431
431
  uniqueNames.push(raw);
432
432
  }
433
- const ids = await resolveZettelBoxNames(uniqueNames);
434
- if (ids.length !== uniqueNames.length) {
435
- throw new DinoxError('Failed to resolve zettel box paths or names in --sql expression');
436
- }
433
+ const batchIds = await resolveZettelBoxNames(uniqueNames);
437
434
  const lookup = new Map();
438
- for (let i = 0; i < uniqueNames.length; i += 1) {
439
- lookup.set(normalizeLookupKey(uniqueNames[i]), ids[i]);
435
+ if (batchIds.length === uniqueNames.length) {
436
+ for (let i = 0; i < uniqueNames.length; i += 1) {
437
+ lookup.set(normalizeLookupKey(uniqueNames[i]), batchIds[i]);
438
+ }
439
+ return lookup;
440
+ }
441
+ for (const name of uniqueNames) {
442
+ const ids = await resolveZettelBoxNames([name]);
443
+ if (ids.length !== 1) {
444
+ throw new DinoxError('Failed to resolve zettel box paths or names in --sql expression');
445
+ }
446
+ lookup.set(normalizeLookupKey(name), ids[0]);
440
447
  }
441
448
  return lookup;
442
449
  }
@@ -0,0 +1,20 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { MutationUploadReceipt } from '../runtime.js';
3
+ export type NoteRevisionReceipt = {
4
+ version: number | null;
5
+ updated_at: string | null;
6
+ content_hash: string;
7
+ changed: string[];
8
+ };
9
+ export type NoteWriteReceipt = MutationUploadReceipt & NoteRevisionReceipt & {
10
+ stale: boolean;
11
+ };
12
+ export declare function readNoteRevisionReceipts(db: AbstractPowerSyncDatabase, ids: readonly string[], changesById: ReadonlyMap<string, readonly string[]>): Promise<Map<string, NoteRevisionReceipt>>;
13
+ export declare function readNoteWriteReceipt(db: AbstractPowerSyncDatabase, id: string, input: {
14
+ changed: readonly string[];
15
+ upload: MutationUploadReceipt;
16
+ stale: boolean;
17
+ }): Promise<NoteWriteReceipt>;
18
+ export declare function buildBatchWriteReceiptFields(upload: MutationUploadReceipt, stale: boolean): MutationUploadReceipt & {
19
+ stale: boolean;
20
+ };
@@ -0,0 +1,82 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { getNoteContentHash, parseStoredNoteDocument, } from './contentEngine.js';
3
+ const NOTE_RECEIPT_BATCH_SIZE = 500;
4
+ function normalizeVersion(value) {
5
+ const parsed = Number(value);
6
+ return Number.isFinite(parsed) ? parsed : null;
7
+ }
8
+ function normalizeChangedFields(fields) {
9
+ const result = [];
10
+ const seen = new Set();
11
+ for (const field of fields) {
12
+ const normalized = field.trim();
13
+ if (!normalized || seen.has(normalized)) {
14
+ continue;
15
+ }
16
+ seen.add(normalized);
17
+ result.push(normalized);
18
+ }
19
+ return result;
20
+ }
21
+ function toRevisionReceipt(row, changed) {
22
+ const document = parseStoredNoteDocument({
23
+ contentJson: row.content_json,
24
+ contentMd: row.content_md,
25
+ });
26
+ return {
27
+ version: normalizeVersion(row.version),
28
+ updated_at: row.updated_at ?? null,
29
+ content_hash: `sha256:${getNoteContentHash(document)}`,
30
+ changed: normalizeChangedFields(changed),
31
+ };
32
+ }
33
+ async function loadNoteReceiptRows(db, ids) {
34
+ const rowsById = new Map();
35
+ const uniqueIds = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
36
+ for (let index = 0; index < uniqueIds.length; index += NOTE_RECEIPT_BATCH_SIZE) {
37
+ const batch = uniqueIds.slice(index, index + NOTE_RECEIPT_BATCH_SIZE);
38
+ const placeholders = batch.map(() => '?').join(', ');
39
+ const rows = await db.getAll(`
40
+ SELECT id, content_md, content_json, updated_at, version
41
+ FROM c_note
42
+ WHERE id IN (${placeholders})
43
+ `, batch);
44
+ for (const row of rows) {
45
+ const id = row.id?.trim() ?? '';
46
+ if (id) {
47
+ rowsById.set(id, row);
48
+ }
49
+ }
50
+ }
51
+ return rowsById;
52
+ }
53
+ export async function readNoteRevisionReceipts(db, ids, changesById) {
54
+ const uniqueIds = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
55
+ const rowsById = await loadNoteReceiptRows(db, uniqueIds);
56
+ const missing = uniqueIds.filter((id) => !rowsById.has(id));
57
+ if (missing.length > 0) {
58
+ throw new DinoxError(`Could not read note write receipt for: ${missing.join(', ')}`, {
59
+ code: 'PRECONDITION_FAILED',
60
+ suggestion: 'Run `dino doctor --format json` to inspect the local database, then retry the write.',
61
+ recoverable: true,
62
+ });
63
+ }
64
+ return new Map(uniqueIds.map((id) => [
65
+ id,
66
+ toRevisionReceipt(rowsById.get(id), changesById.get(id) ?? []),
67
+ ]));
68
+ }
69
+ export async function readNoteWriteReceipt(db, id, input) {
70
+ const revisions = await readNoteRevisionReceipts(db, [id], new Map([[id, input.changed]]));
71
+ return {
72
+ ...input.upload,
73
+ ...revisions.get(id),
74
+ stale: input.stale,
75
+ };
76
+ }
77
+ export function buildBatchWriteReceiptFields(upload, stale) {
78
+ return {
79
+ ...upload,
80
+ stale,
81
+ };
82
+ }
@@ -5,13 +5,14 @@ import { buildPromptAddDryRunPreview } from '../writeDryRun.js';
5
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
6
6
  import { DinoxError } from '../../utils/errors.js';
7
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
8
- import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
8
+ import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
9
9
  import { addPrompt, inspectAddPrompt, listPrompts } from './repo.js';
10
10
  const PROMPT_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
11
11
  async function runPromptList(context) {
12
12
  await withCommandRuntime(context, async (runtime) => {
13
+ const identity = await resolveAuthIdentity(runtime.config);
13
14
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
14
- printSuccess((await listPrompts(runtime.db)).map((prompt) => ({
15
+ printSuccess((await listPrompts(runtime.db, { userId: identity.userId })).map((prompt) => ({
15
16
  name: prompt.name,
16
17
  prompt: prompt.cmd,
17
18
  })));
@@ -47,7 +48,6 @@ async function runPromptAdd(context) {
47
48
  cmd,
48
49
  userId: identity.userId,
49
50
  });
50
- printUploadDisabledWarning(runtime);
51
51
  await waitForMutationUpload(runtime);
52
52
  const payload = {
53
53
  ok: true,
@@ -3,6 +3,9 @@ export type PromptListItem = {
3
3
  name: string;
4
4
  cmd: string;
5
5
  };
6
+ export type ListPromptsInput = {
7
+ userId?: string;
8
+ };
6
9
  export type AddPromptInput = {
7
10
  name: string;
8
11
  cmd: string;
@@ -20,6 +23,6 @@ export type InspectAddPromptResult = {
20
23
  restored: boolean;
21
24
  existingId?: string;
22
25
  };
23
- export declare function listPrompts(db: AbstractPowerSyncDatabase): Promise<PromptListItem[]>;
26
+ export declare function listPrompts(db: AbstractPowerSyncDatabase, input?: ListPromptsInput): Promise<PromptListItem[]>;
24
27
  export declare function addPrompt(db: AbstractPowerSyncDatabase, input: AddPromptInput): Promise<AddPromptResult>;
25
28
  export declare function inspectAddPrompt(db: AbstractPowerSyncDatabase, input: AddPromptInput): Promise<InspectAddPromptResult>;
@@ -10,7 +10,7 @@ async function getPromptColumns(db) {
10
10
  const rows = await db.getAll('PRAGMA table_info(c_cmd)', []);
11
11
  return new Set(rows.map((row) => row.name).filter(Boolean));
12
12
  }
13
- async function listPromptRowsForWrite(db, columnNames) {
13
+ async function listPromptRowsForWrite(db, columnNames, userId) {
14
14
  const isDelExpr = columnNames.has('is_del') ? 'is_del' : 'NULL';
15
15
  const userIdExpr = columnNames.has('user_id') ? `TRIM(COALESCE(user_id, ''))` : `''`;
16
16
  const activeFirstExpr = columnNames.has('is_del')
@@ -18,6 +18,7 @@ async function listPromptRowsForWrite(db, columnNames) {
18
18
  : '0';
19
19
  const updateOrderExpr = columnNames.has('update_at') ? 'update_at DESC,' : '';
20
20
  const insertOrderExpr = columnNames.has('insert_at') ? 'insert_at DESC,' : '';
21
+ const userFilter = columnNames.has('user_id') ? `AND TRIM(COALESCE(user_id, '')) = ?` : '';
21
22
  return db.getAll(`
22
23
  SELECT
23
24
  TRIM(COALESCE(id, '')) AS id,
@@ -27,14 +28,21 @@ async function listPromptRowsForWrite(db, columnNames) {
27
28
  ${userIdExpr} AS user_id
28
29
  FROM c_cmd
29
30
  WHERE TRIM(COALESCE(id, '')) <> ''
31
+ ${userFilter}
30
32
  ORDER BY
31
33
  ${activeFirstExpr} ASC,
32
34
  ${updateOrderExpr}
33
35
  ${insertOrderExpr}
34
36
  id DESC
35
- `, []);
37
+ `, columnNames.has('user_id') ? [userId] : []);
36
38
  }
37
- export async function listPrompts(db) {
39
+ export async function listPrompts(db, input = {}) {
40
+ const userId = normalizeText(input.userId ?? '');
41
+ const columnNames = await getPromptColumns(db);
42
+ if (columnNames.has('user_id') && !userId) {
43
+ throw new DinoxError('c_cmd query requires user_id. Missing resolved userId. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
44
+ }
45
+ const userFilter = columnNames.has('user_id') ? `AND TRIM(COALESCE(user_id, '')) = ?` : '';
38
46
  const rows = await db.getAll(`
39
47
  SELECT DISTINCT
40
48
  TRIM(COALESCE(name, '')) AS name,
@@ -43,8 +51,9 @@ export async function listPrompts(db) {
43
51
  WHERE (is_del IS NULL OR is_del = 0)
44
52
  AND TRIM(COALESCE(name, '')) <> ''
45
53
  AND TRIM(COALESCE(cmd, '')) <> ''
54
+ ${userFilter}
46
55
  ORDER BY COALESCE(priority, 0) DESC, name COLLATE NOCASE ASC, cmd COLLATE NOCASE ASC
47
- `, []);
56
+ `, columnNames.has('user_id') ? [userId] : []);
48
57
  return rows
49
58
  .map((row) => ({
50
59
  name: row.name?.trim() ?? '',
@@ -73,7 +82,7 @@ export async function addPrompt(db, input) {
73
82
  if (columnNames.has('user_id') && !userId) {
74
83
  throw new DinoxError('c_cmd insert requires user_id. Missing resolved userId. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
75
84
  }
76
- const rows = await listPromptRowsForWrite(db, columnNames);
85
+ const rows = await listPromptRowsForWrite(db, columnNames, userId);
77
86
  const samePromptRows = rows.filter((row) => {
78
87
  return normalizeLookup(row.name ?? '') === nameLookup && normalizeLookup(row.cmd ?? '') === cmdLookup;
79
88
  });
@@ -113,7 +122,8 @@ export async function addPrompt(db, input) {
113
122
  UPDATE c_cmd
114
123
  SET ${setParts.join(', ')}
115
124
  WHERE id = ?
116
- `, params);
125
+ ${columnNames.has('user_id') ? `AND TRIM(COALESCE(user_id, '')) = ?` : ''}
126
+ `, columnNames.has('user_id') ? [...params, userId] : params);
117
127
  }
118
128
  return {
119
129
  id: deletedRow.id,
@@ -183,7 +193,7 @@ export async function inspectAddPrompt(db, input) {
183
193
  if (columnNames.has('user_id') && !userId) {
184
194
  throw new DinoxError('c_cmd insert requires user_id. Missing resolved userId. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
185
195
  }
186
- const rows = await listPromptRowsForWrite(db, columnNames);
196
+ const rows = await listPromptRowsForWrite(db, columnNames, userId);
187
197
  const samePromptRows = rows.filter((row) => {
188
198
  return normalizeLookup(row.name ?? '') === nameLookup && normalizeLookup(row.cmd ?? '') === cmdLookup;
189
199
  });
@@ -4,10 +4,21 @@ import { loadConfig } from '../config/store.js';
4
4
  import type { ResolvedDinoxConfig } from '../config/types.js';
5
5
  import { type SyncGateResult } from '../powersync/syncGate.js';
6
6
  import type { CommandExecutionContext } from './descriptor/types.js';
7
+ import { type WriteDurability } from './writeDurability.js';
8
+ export type MutationUploadReceipt = {
9
+ durability: WriteDurability;
10
+ upload_queue_remaining: number | null;
11
+ upload_error?: {
12
+ name: string;
13
+ message: string;
14
+ };
15
+ };
7
16
  export type CommandSyncState = {
8
17
  firstSyncStale: boolean;
9
18
  preSyncIdle: boolean | null;
19
+ recentSync: boolean | null;
10
20
  gate: SyncGateResult | null;
21
+ startedAtIso: string;
11
22
  };
12
23
  export type CommandRuntime = {
13
24
  db: AbstractPowerSyncDatabase;
@@ -21,6 +32,7 @@ export type CommandRuntime = {
21
32
  };
22
33
  type RuntimeSource = CommandExecutionContext | Command | Record<string, unknown>;
23
34
  export declare function commandStale(runtime: Pick<CommandRuntime, 'offline' | 'stale'>): boolean;
35
+ export declare function resolveWriteDurabilityForRuntime(runtime: Pick<CommandRuntime, 'offline'>, value: unknown): WriteDurability;
24
36
  export declare function withCommandRuntime<T>(source: RuntimeSource, run: (runtime: CommandRuntime) => Promise<T>): Promise<T>;
25
37
  export declare function syncBeforeCommand(runtime: CommandRuntime, options?: {
26
38
  syncTokenIndex?: boolean;
@@ -28,7 +40,7 @@ export declare function syncBeforeCommand(runtime: CommandRuntime, options?: {
28
40
  }): Promise<void>;
29
41
  export declare function waitForMutationUpload(runtime: CommandRuntime, options?: {
30
42
  timeoutMs?: number;
31
- }): Promise<void>;
32
- export declare function printUploadDisabledWarning(runtime: CommandRuntime, message?: string): void;
43
+ durability?: unknown;
44
+ }): Promise<MutationUploadReceipt>;
33
45
  export declare function printStaleCacheWarning(runtime: CommandRuntime, message?: string): void;
34
46
  export {};