@dinoxx/dinox-cli 1.0.20 → 1.0.21

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 (84) hide show
  1. package/README.md +72 -139
  2. package/dist/auth/userInfo.d.ts +8 -1
  3. package/dist/auth/userInfo.js +38 -6
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +11 -4
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +43 -15
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/sync.js +4 -2
  55. package/dist/commands/tags/content.d.ts +32 -0
  56. package/dist/commands/tags/content.js +243 -0
  57. package/dist/commands/tags/index.js +285 -49
  58. package/dist/commands/tags/model.d.ts +38 -0
  59. package/dist/commands/tags/model.js +100 -0
  60. package/dist/commands/tags/operations.d.ts +105 -0
  61. package/dist/commands/tags/operations.js +438 -0
  62. package/dist/commands/tags/repo.d.ts +5 -1
  63. package/dist/commands/tags/repo.js +142 -218
  64. package/dist/commands/tags/store.d.ts +28 -0
  65. package/dist/commands/tags/store.js +166 -0
  66. package/dist/commands/todo/index.js +30 -61
  67. package/dist/commands/writeDryRun.d.ts +25 -0
  68. package/dist/commands/writeDryRun.js +31 -1
  69. package/dist/config/resolve.js +2 -2
  70. package/dist/daemon/index.d.ts +2 -0
  71. package/dist/daemon/index.js +176 -58
  72. package/dist/powersync/runtime.d.ts +1 -0
  73. package/dist/powersync/runtime.js +10 -0
  74. package/dist/powersync/schema/content.d.ts +2 -0
  75. package/dist/powersync/schema/content.js +2 -0
  76. package/dist/powersync/schema/index.d.ts +2 -0
  77. package/dist/skills/check.d.ts +1 -1
  78. package/dist/skills/check.js +27 -2
  79. package/dist/utils/authToken.d.ts +2 -0
  80. package/dist/utils/authToken.js +7 -2
  81. package/dist/utils/noteContentEngine.d.ts +333 -0
  82. package/dist/utils/noteContentEngine.js +740 -0
  83. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  84. package/package.json +4 -8
@@ -0,0 +1,127 @@
1
+ import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
2
+ import { DinoxError } from '../../utils/errors.js';
3
+ import { nowIsoUtc } from '../../utils/time.js';
4
+ import { parseStoredTags, parseZettelBoxIds } from './repo.js';
5
+ function normalizeDistinct(values) {
6
+ const result = [];
7
+ const seen = new Set();
8
+ for (const value of values) {
9
+ const normalized = value.trim();
10
+ const key = normalized.toLowerCase();
11
+ if (!normalized || seen.has(key)) {
12
+ continue;
13
+ }
14
+ seen.add(key);
15
+ result.push(normalized);
16
+ }
17
+ return result;
18
+ }
19
+ function sameOrdered(left, right) {
20
+ if (left.length !== right.length) {
21
+ return false;
22
+ }
23
+ return left.every((value, index) => value === right[index]);
24
+ }
25
+ function keySet(values) {
26
+ return new Set(values.map((value) => value.trim().toLowerCase()).filter(Boolean));
27
+ }
28
+ function applyListOperation(current, values, operation) {
29
+ const normalizedValues = normalizeDistinct(values);
30
+ if (operation === 'replace') {
31
+ return normalizedValues;
32
+ }
33
+ if (operation === 'add') {
34
+ return normalizeDistinct([...current, ...normalizedValues]);
35
+ }
36
+ const removeKeys = keySet(normalizedValues);
37
+ return current.filter((value) => !removeKeys.has(value.trim().toLowerCase()));
38
+ }
39
+ function assertMutationInput(input) {
40
+ const ids = normalizeDistinct(input.ids);
41
+ if (ids.length === 0) {
42
+ throw new DinoxError('At least one note id is required');
43
+ }
44
+ const values = normalizeDistinct(input.values);
45
+ if (values.length === 0 && input.operation !== 'replace') {
46
+ const option = input.operation === 'add' ? '--add' : '--remove';
47
+ throw new DinoxError(`${option} requires at least one ${input.field === 'tags' ? 'tag' : 'box'}`);
48
+ }
49
+ return {
50
+ ids,
51
+ values,
52
+ displayValues: normalizeDistinct(input.displayValues ?? input.values),
53
+ };
54
+ }
55
+ async function loadNoteMetadataState(db, ids) {
56
+ const rowsById = new Map();
57
+ const chunkSize = 500;
58
+ for (let index = 0; index < ids.length; index += chunkSize) {
59
+ const batch = ids.slice(index, index + chunkSize);
60
+ const placeholders = batch.map(() => '?').join(', ');
61
+ const rows = await db.getAll(`
62
+ SELECT id, title, tags, zettel_boxes, is_del
63
+ FROM c_note
64
+ WHERE id IN (${placeholders})
65
+ `, batch);
66
+ for (const row of rows) {
67
+ const id = row.id?.trim() ?? '';
68
+ if (!id || rowsById.has(id)) {
69
+ continue;
70
+ }
71
+ rowsById.set(id, row);
72
+ }
73
+ }
74
+ return rowsById;
75
+ }
76
+ export async function planNoteMetadataMutation(db, input) {
77
+ const { ids, values, displayValues } = assertMutationInput(input);
78
+ const stateById = await loadNoteMetadataState(db, ids);
79
+ const missing = ids.filter((id) => !stateById.has(id));
80
+ if (missing.length > 0) {
81
+ throw new DinoxError(`Notes not found: ${missing.join(', ')}`);
82
+ }
83
+ const results = ids.map((id) => {
84
+ const note = stateById.get(id);
85
+ if (typeof note.is_del === 'number' && note.is_del !== 0) {
86
+ throw new DinoxError(`Note has been deleted: ${id}`);
87
+ }
88
+ const before = input.field === 'tags'
89
+ ? parseStoredTags(note.tags)
90
+ : parseZettelBoxIds(note.zettel_boxes);
91
+ const after = applyListOperation(before, values, input.operation);
92
+ return {
93
+ id,
94
+ title: note.title ?? '',
95
+ before,
96
+ after,
97
+ changed: !sameOrdered(before, after),
98
+ };
99
+ });
100
+ return {
101
+ field: input.field,
102
+ operation: input.operation,
103
+ values,
104
+ displayValues,
105
+ total: results.length,
106
+ changedCount: results.filter((item) => item.changed).length,
107
+ results,
108
+ };
109
+ }
110
+ export async function applyNoteMetadataMutation(db, plan) {
111
+ const changed = plan.results.filter((item) => item.changed);
112
+ if (changed.length === 0) {
113
+ return plan;
114
+ }
115
+ await withPowerSyncWriteTransaction(db, async (tx) => {
116
+ const timestamp = nowIsoUtc();
117
+ for (const item of changed) {
118
+ const column = plan.field === 'tags' ? 'tags' : 'zettel_boxes';
119
+ await tx.execute(`
120
+ UPDATE c_note
121
+ SET ${column} = ?, updated_at = ?, version = COALESCE(version, 0) + 1
122
+ WHERE id = ?
123
+ `, [JSON.stringify(item.after), timestamp, item.id]);
124
+ }
125
+ });
126
+ return plan;
127
+ }
@@ -45,6 +45,23 @@ export type NoteDetail = {
45
45
  content_md: string | null;
46
46
  zettel_boxes: string[];
47
47
  };
48
+ export type NoteExportItem = {
49
+ id: string;
50
+ created_at: string | null;
51
+ updated_at: string | null;
52
+ title: string | null;
53
+ type: string | null;
54
+ source: string | null;
55
+ summary: string | null;
56
+ tags: string[];
57
+ is_del: number | null;
58
+ is_starred: boolean;
59
+ content_md: string;
60
+ content_text: string | null;
61
+ content_json: unknown;
62
+ box_ids: string[];
63
+ boxes: string[];
64
+ };
48
65
  export type NoteContext = {
49
66
  id: string;
50
67
  title: string;
@@ -84,6 +101,7 @@ export declare function getNoteContext(db: AbstractPowerSyncDatabase, id: string
84
101
  export declare function getNotePreview(db: AbstractPowerSyncDatabase, id: string, requestedLines: number): Promise<NotePreview | null>;
85
102
  export declare function getNoteDetails(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteDetail[]>;
86
103
  export declare function getNoteDetail(db: AbstractPowerSyncDatabase, id: string): Promise<NoteDetail | null>;
104
+ export declare function getNoteExportItems(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteExportItem[]>;
87
105
  export declare function createNote(db: AbstractPowerSyncDatabase, input: {
88
106
  id: string;
89
107
  title: string;
@@ -100,6 +118,7 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
100
118
  }>;
101
119
  export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
102
120
  export declare function validateTagsForUpdate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
121
+ export declare function validateTagsForBulk(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
103
122
  export declare function parseStoredTags(raw: string | null): string[];
104
123
  export declare function updateNote(db: AbstractPowerSyncDatabase, input: {
105
124
  id: string;
@@ -111,5 +130,6 @@ export declare function assertNotesUpdatable(db: AbstractPowerSyncDatabase, note
111
130
  export declare function resolveZettelBoxIdsForCreate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
112
131
  export declare function resolveZettelBoxIdsForSearch(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
113
132
  export declare function resolveZettelBoxIdsForUpdate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
133
+ export declare function resolveZettelBoxIdsForBulk(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
114
134
  export declare function softDeleteNote(db: AbstractPowerSyncDatabase, id: string): Promise<void>;
115
135
  export {};
@@ -1,7 +1,7 @@
1
1
  import { DinoxError } from '../../utils/errors.js';
2
2
  import { nowIsoUtc } from '../../utils/time.js';
3
3
  import { tokenizeWithJieba } from '../../utils/tokenize.js';
4
- import { listBoxes } from '../boxes/repo.js';
4
+ import { canonicalizeBoxPath, listBoxes } from '../boxes/repo.js';
5
5
  import { listTags } from '../tags/repo.js';
6
6
  import { buildNoteSearchSqlFilter } from './searchSql.js';
7
7
  const MAX_SEARCH_SUMMARY_CHARS = 200;
@@ -199,7 +199,7 @@ async function loadZettelBoxNameMap(db, ids) {
199
199
  const batch = ids.slice(i, i + chunkSize);
200
200
  const placeholders = batch.map(() => '?').join(', ');
201
201
  const rows = await db.getAll(`
202
- SELECT id, name
202
+ SELECT id, name, path
203
203
  FROM c_zettel_box
204
204
  WHERE (is_del IS NULL OR is_del = 0)
205
205
  AND id IN (${placeholders})
@@ -209,7 +209,8 @@ async function loadZettelBoxNameMap(db, ids) {
209
209
  if (!id) {
210
210
  continue;
211
211
  }
212
- map.set(id, row.name?.trim() ?? '');
212
+ const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
213
+ map.set(id, path || row.name?.trim() || '');
213
214
  }
214
215
  }
215
216
  return map;
@@ -645,6 +646,41 @@ function mapNoteDetailRow(note, boxNameMap) {
645
646
  zettel_boxes: zettelBoxes,
646
647
  };
647
648
  }
649
+ function parseStoredJsonValue(raw) {
650
+ const value = raw?.trim() ?? '';
651
+ if (!value) {
652
+ return null;
653
+ }
654
+ try {
655
+ return JSON.parse(value);
656
+ }
657
+ catch {
658
+ return raw;
659
+ }
660
+ }
661
+ function mapNoteExportRow(note, boxNameMap) {
662
+ const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
663
+ const boxes = boxIds
664
+ .map((boxId) => boxNameMap.get(boxId))
665
+ .filter((name) => typeof name === 'string' && name.length > 0);
666
+ return {
667
+ id: note.id,
668
+ created_at: note.created_at,
669
+ updated_at: note.updated_at,
670
+ title: note.title,
671
+ type: note.type,
672
+ source: note.source,
673
+ summary: note.summary,
674
+ tags: parseStoredTags(note.tags),
675
+ is_del: note.is_del,
676
+ is_starred: normalizeStoredStarred(note.is_starred),
677
+ content_md: note.content_md ?? '',
678
+ content_text: note.content_text,
679
+ content_json: parseStoredJsonValue(note.content_json),
680
+ box_ids: boxIds,
681
+ boxes,
682
+ };
683
+ }
648
684
  export async function getNoteDetails(db, ids) {
649
685
  const targetIds = Array.from(new Set(ids
650
686
  .map((id) => id.trim())
@@ -696,6 +732,57 @@ export async function getNoteDetail(db, id) {
696
732
  const details = await getNoteDetails(db, [id]);
697
733
  return details[0] ?? null;
698
734
  }
735
+ export async function getNoteExportItems(db, ids) {
736
+ const targetIds = Array.from(new Set(ids
737
+ .map((id) => id.trim())
738
+ .filter((id) => id.length > 0)));
739
+ if (targetIds.length === 0) {
740
+ return [];
741
+ }
742
+ const noteRowsById = new Map();
743
+ const chunkSize = 500;
744
+ for (let index = 0; index < targetIds.length; index += chunkSize) {
745
+ const batch = targetIds.slice(index, index + chunkSize);
746
+ const placeholders = batch.map(() => '?').join(', ');
747
+ const rows = await db.getAll(`
748
+ SELECT
749
+ id,
750
+ created_at,
751
+ updated_at,
752
+ title,
753
+ type,
754
+ source,
755
+ tags,
756
+ summary,
757
+ is_del,
758
+ is_starred,
759
+ content_md,
760
+ content_text,
761
+ content_json,
762
+ zettel_boxes AS zettel_boxes_raw
763
+ FROM c_note
764
+ WHERE id IN (${placeholders})
765
+ `, batch);
766
+ for (const row of rows) {
767
+ const id = row.id?.trim() ?? '';
768
+ if (!id || noteRowsById.has(id)) {
769
+ continue;
770
+ }
771
+ noteRowsById.set(id, row);
772
+ }
773
+ }
774
+ const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
775
+ const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
776
+ const items = [];
777
+ for (const id of targetIds) {
778
+ const row = noteRowsById.get(id);
779
+ if (!row) {
780
+ continue;
781
+ }
782
+ items.push(mapNoteExportRow(row, boxNameMap));
783
+ }
784
+ return items;
785
+ }
699
786
  export async function createNote(db, input) {
700
787
  const userId = input.userId.trim();
701
788
  if (!userId) {
@@ -790,14 +877,19 @@ export async function createNote(db, input) {
790
877
  function normalizeLookupKey(value) {
791
878
  return value.trim().toLowerCase();
792
879
  }
880
+ function normalizeZettelBoxLookupKey(value) {
881
+ return canonicalizeBoxPath(value).toLowerCase();
882
+ }
793
883
  function buildZettelBoxRetryQuestion(intent, type) {
794
884
  const command = intent === 'search'
795
885
  ? 'dino note search'
796
886
  : intent === 'update'
797
887
  ? 'dino note update'
798
- : 'dino note create';
888
+ : intent === 'bulk'
889
+ ? 'dino note bulk'
890
+ : 'dino note create';
799
891
  if (type === 'ambiguous') {
800
- return `Do you want to specify unique box names (or IDs) and retry \`${command}\`?`;
892
+ return `Do you want to specify box paths and retry \`${command}\`?`;
801
893
  }
802
894
  return `Do you want to add these missing boxes and retry \`${command}\`?`;
803
895
  }
@@ -807,6 +899,9 @@ export async function validateTagsForCreate(db, requestedTags) {
807
899
  export async function validateTagsForUpdate(db, requestedTags) {
808
900
  return validateTagsForWrite(db, requestedTags, 'dino note update');
809
901
  }
902
+ export async function validateTagsForBulk(db, requestedTags) {
903
+ return validateTagsForWrite(db, requestedTags, 'dino note bulk');
904
+ }
810
905
  async function validateTagsForWrite(db, requestedTags, retryCommand) {
811
906
  if (requestedTags.length === 0) {
812
907
  return [];
@@ -1013,30 +1108,43 @@ export async function resolveZettelBoxIdsForSearch(db, requestedNames) {
1013
1108
  export async function resolveZettelBoxIdsForUpdate(db, requestedNames) {
1014
1109
  return resolveZettelBoxIdsByName(db, requestedNames, 'update');
1015
1110
  }
1111
+ export async function resolveZettelBoxIdsForBulk(db, requestedNames) {
1112
+ return resolveZettelBoxIdsByName(db, requestedNames, 'bulk');
1113
+ }
1016
1114
  async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
1017
1115
  if (requestedNames.length === 0) {
1018
1116
  return [];
1019
1117
  }
1020
1118
  const boxes = await listBoxes(db);
1021
- const availableNames = Array.from(new Set(boxes.map((box) => box.name.trim()).filter(Boolean)));
1119
+ const availablePaths = Array.from(new Set(boxes.map((box) => box.path.trim()).filter(Boolean)));
1120
+ const boxByPath = new Map();
1022
1121
  const boxByName = new Map();
1023
1122
  for (const box of boxes) {
1024
1123
  const name = box.name.trim();
1025
- const key = normalizeLookupKey(name);
1026
- if (!key) {
1124
+ const path = canonicalizeBoxPath(box.path || name);
1125
+ const pathKey = normalizeZettelBoxLookupKey(path);
1126
+ if (pathKey && !boxByPath.has(pathKey)) {
1127
+ boxByPath.set(pathKey, box.id);
1128
+ }
1129
+ const nameKey = normalizeLookupKey(name);
1130
+ if (!nameKey) {
1027
1131
  continue;
1028
1132
  }
1029
- const current = boxByName.get(key);
1133
+ const current = boxByName.get(nameKey);
1030
1134
  if (!current) {
1031
- boxByName.set(key, {
1135
+ boxByName.set(nameKey, {
1032
1136
  name,
1033
1137
  ids: [box.id],
1138
+ paths: path ? [path] : [],
1034
1139
  });
1035
1140
  continue;
1036
1141
  }
1037
1142
  if (!current.ids.includes(box.id)) {
1038
1143
  current.ids.push(box.id);
1039
1144
  }
1145
+ if (path && !current.paths.includes(path)) {
1146
+ current.paths.push(path);
1147
+ }
1040
1148
  }
1041
1149
  const resolvedIds = [];
1042
1150
  const seenIds = new Set();
@@ -1044,11 +1152,19 @@ async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
1044
1152
  const ambiguous = [];
1045
1153
  const seenAmbiguous = new Set();
1046
1154
  for (const requested of requestedNames) {
1047
- const key = normalizeLookupKey(requested);
1048
- if (!key) {
1155
+ const pathKey = normalizeZettelBoxLookupKey(requested);
1156
+ if (!pathKey) {
1157
+ continue;
1158
+ }
1159
+ const pathMatchId = boxByPath.get(pathKey);
1160
+ if (pathMatchId) {
1161
+ if (!seenIds.has(pathMatchId)) {
1162
+ seenIds.add(pathMatchId);
1163
+ resolvedIds.push(pathMatchId);
1164
+ }
1049
1165
  continue;
1050
1166
  }
1051
- const lookup = boxByName.get(key);
1167
+ const lookup = boxByName.get(normalizeLookupKey(requested));
1052
1168
  if (!lookup) {
1053
1169
  missing.push(requested);
1054
1170
  continue;
@@ -1060,6 +1176,7 @@ async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
1060
1176
  ambiguous.push({
1061
1177
  name: requested,
1062
1178
  ids: lookup.ids,
1179
+ paths: lookup.paths,
1063
1180
  });
1064
1181
  }
1065
1182
  continue;
@@ -1071,22 +1188,22 @@ async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
1071
1188
  }
1072
1189
  }
1073
1190
  if (ambiguous.length > 0) {
1074
- throw new DinoxError(`Ambiguous zettel box names: ${ambiguous.map((item) => `${item.name} -> [${item.ids.join(', ')}]`).join('; ')}`, {
1191
+ throw new DinoxError(`Ambiguous zettel box names: ${ambiguous.map((item) => `${item.name} -> [${item.paths.join(', ')}]`).join('; ')}`, {
1075
1192
  details: {
1076
1193
  type: 'ambiguous_zettel_boxes',
1077
1194
  ambiguous,
1078
- availableNamesSample: availableNames.slice(0, 20),
1195
+ availablePathsSample: availablePaths.slice(0, 20),
1079
1196
  question: buildZettelBoxRetryQuestion(intent, 'ambiguous'),
1080
1197
  },
1081
1198
  });
1082
1199
  }
1083
1200
  if (missing.length > 0) {
1084
- throw new DinoxError(`Unknown zettel box names: ${missing.join(', ')}`, {
1201
+ throw new DinoxError(`Unknown zettel box paths or names: ${missing.join(', ')}`, {
1085
1202
  details: {
1086
1203
  type: 'missing_zettel_boxes',
1087
1204
  missing,
1088
- availableCount: availableNames.length,
1089
- availableNamesSample: availableNames.slice(0, 20),
1205
+ availableCount: availablePaths.length,
1206
+ availablePathsSample: availablePaths.slice(0, 20),
1090
1207
  question: buildZettelBoxRetryQuestion(intent, 'missing'),
1091
1208
  },
1092
1209
  });
@@ -1,15 +1,6 @@
1
1
  import type { Command } from 'commander';
2
- import type { AbstractPowerSyncDatabase } from '@powersync/common';
3
- import { loadConfig } from '../../config/store.js';
4
- import { resolveConfig } from '../../config/resolve.js';
5
- export type NoteCommandRuntime = {
6
- db: AbstractPowerSyncDatabase;
7
- config: ReturnType<typeof resolveConfig>;
8
- rawConfig: Awaited<ReturnType<typeof loadConfig>>;
9
- offline: boolean;
10
- timeoutMs: number;
11
- stale: boolean;
12
- };
2
+ import { type CommandRuntime } from '../runtime.js';
3
+ export type NoteCommandRuntime = CommandRuntime;
13
4
  export declare function parseNoteType(value: unknown): 'note' | 'crawl';
14
5
  export declare function parseSearchLimit(value: unknown): number;
15
6
  export declare function parseSearchOffset(value: unknown): number;
@@ -24,5 +15,5 @@ export type ParsedSearchListInput = {
24
15
  };
25
16
  export declare function parseSearchListInput(value: string, optionName: string): Promise<ParsedSearchListInput>;
26
17
  export declare function parseTargetNoteIds(idArg: string | undefined, idsOption: string | undefined, commandName: 'detail' | 'update'): Promise<string[]>;
27
- export declare function syncBeforeNoteCommand(db: AbstractPowerSyncDatabase, offline: boolean, timeoutMs: number): Promise<void>;
18
+ export declare function syncBeforeNoteCommand(runtime: CommandRuntime): Promise<void>;
28
19
  export declare function withNoteDatabase(command: Command, run: (runtime: NoteCommandRuntime) => Promise<void>): Promise<void>;
@@ -1,10 +1,6 @@
1
- import { loadConfig } from '../../config/store.js';
2
- import { resolveConfig } from '../../config/resolve.js';
3
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
4
- import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
5
1
  import { readStringOrFile } from '../../utils/argValue.js';
6
2
  import { DinoxError } from '../../utils/errors.js';
7
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
3
+ import { syncBeforeCommand, withCommandRuntime, } from '../runtime.js';
8
4
  const DEFAULT_NOTE_SEARCH_LIMIT = 50;
9
5
  const MAX_NOTE_SEARCH_LIMIT = 500;
10
6
  export function parseNoteType(value) {
@@ -155,31 +151,9 @@ export async function parseTargetNoteIds(idArg, idsOption, commandName) {
155
151
  }
156
152
  return ids;
157
153
  }
158
- export async function syncBeforeNoteCommand(db, offline, timeoutMs) {
159
- if (offline) {
160
- return;
161
- }
162
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
163
- await syncNoteTokenIndex(db);
154
+ export async function syncBeforeNoteCommand(runtime) {
155
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
164
156
  }
165
157
  export async function withNoteDatabase(command, run) {
166
- const globals = command.optsWithGlobals?.() ?? {};
167
- const rawConfig = await loadConfig();
168
- const config = resolveConfig(rawConfig);
169
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
170
- const offline = Boolean(globals.offline);
171
- const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
172
- try {
173
- await run({
174
- db,
175
- config,
176
- rawConfig,
177
- offline,
178
- timeoutMs,
179
- stale,
180
- });
181
- }
182
- finally {
183
- await db.close().catch(() => undefined);
184
- }
158
+ await withCommandRuntime(command, run);
185
159
  }
@@ -432,7 +432,7 @@ async function buildZettelBoxIdLookup(ast, resolveZettelBoxNames) {
432
432
  }
433
433
  const ids = await resolveZettelBoxNames(uniqueNames);
434
434
  if (ids.length !== uniqueNames.length) {
435
- throw new DinoxError('Failed to resolve zettel box names in --sql expression');
435
+ throw new DinoxError('Failed to resolve zettel box paths or names in --sql expression');
436
436
  }
437
437
  const lookup = new Map();
438
438
  for (let i = 0; i < uniqueNames.length; i += 1) {
@@ -1,41 +1,17 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
- import { loadConfig } from '../../config/store.js';
3
- import { resolveConfig } from '../../config/resolve.js';
4
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
5
- import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
6
2
  import { registerCommandGroup } from '../descriptor/register.js';
7
3
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
8
4
  import { buildPromptAddDryRunPreview } from '../writeDryRun.js';
9
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
10
6
  import { DinoxError } from '../../utils/errors.js';
11
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
12
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
8
+ import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
13
9
  import { addPrompt, inspectAddPrompt, listPrompts } from './repo.js';
14
10
  const PROMPT_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
15
- async function syncBeforePromptCommand(db, offline, timeoutMs) {
16
- if (offline) {
17
- return;
18
- }
19
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
20
- await syncNoteTokenIndex(db);
21
- }
22
- async function withPromptDb(context, run) {
23
- const rawConfig = await loadConfig();
24
- const config = resolveConfig(rawConfig);
25
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
26
- const offline = Boolean(context.globals.offline);
27
- const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
28
- try {
29
- await run({ db, config, rawConfig, offline, timeoutMs, stale });
30
- }
31
- finally {
32
- await db.close().catch(() => undefined);
33
- }
34
- }
35
11
  async function runPromptList(context) {
36
- await withPromptDb(context, async ({ db, offline, timeoutMs }) => {
37
- await syncBeforePromptCommand(db, offline, timeoutMs);
38
- printSuccess((await listPrompts(db)).map((prompt) => ({
12
+ await withCommandRuntime(context, async (runtime) => {
13
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
14
+ printSuccess((await listPrompts(runtime.db)).map((prompt) => ({
39
15
  name: prompt.name,
40
16
  prompt: prompt.cmd,
41
17
  })));
@@ -51,14 +27,14 @@ async function runPromptAdd(context) {
51
27
  throw new DinoxError('Prompt text is required. Use `dino prompt add --name <name> --prompt <text>`.');
52
28
  }
53
29
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
54
- await withPromptDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
55
- const identity = await resolveAuthIdentity(rawConfig, config);
56
- await syncBeforePromptCommand(db, offline, timeoutMs);
30
+ await withCommandRuntime(context, async (runtime) => {
31
+ const identity = await resolveAuthIdentity(runtime.config);
32
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
57
33
  if (Boolean(context.options.dryRun)) {
58
34
  printDryRunPreview(buildPromptAddDryRunPreview({
59
- config,
60
- offline,
61
- ...(await inspectAddPrompt(db, {
35
+ config: runtime.config,
36
+ offline: runtime.offline,
37
+ ...(await inspectAddPrompt(runtime.db, {
62
38
  name,
63
39
  cmd,
64
40
  userId: identity.userId,
@@ -66,20 +42,16 @@ async function runPromptAdd(context) {
66
42
  }));
67
43
  return;
68
44
  }
69
- const result = await addPrompt(db, {
45
+ const result = await addPrompt(runtime.db, {
70
46
  name,
71
47
  cmd,
72
48
  userId: identity.userId,
73
49
  });
74
- if (!offline && !config.powersync.uploadBaseUrl) {
75
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
76
- }
77
- if (!offline) {
78
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
79
- }
50
+ printUploadDisabledWarning(runtime);
51
+ await waitForMutationUpload(runtime);
80
52
  const payload = {
81
53
  ok: true,
82
- stale: offline ? true : stale,
54
+ stale: commandStale(runtime),
83
55
  id: result.id,
84
56
  name: result.name,
85
57
  prompt: result.cmd,
@@ -90,9 +62,7 @@ async function runPromptAdd(context) {
90
62
  return;
91
63
  }
92
64
  printHumanReadableLine(result.id);
93
- if (stale && !offline) {
94
- printHumanReadableLine('warning: sync timed out; results may be stale');
95
- }
65
+ printStaleCacheWarning(runtime, 'warning: sync timed out; results may be stale');
96
66
  });
97
67
  }
98
68
  export const promptCommandGroup = {
@@ -71,7 +71,7 @@ export async function addPrompt(db, input) {
71
71
  throw new DinoxError(`c_cmd table is missing required columns: ${missingRequired.join(', ')}`);
72
72
  }
73
73
  if (columnNames.has('user_id') && !userId) {
74
- throw new DinoxError('c_cmd insert requires user_id. Missing persisted userId. Run `dino auth login` first.');
74
+ 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
75
  }
76
76
  const rows = await listPromptRowsForWrite(db, columnNames);
77
77
  const samePromptRows = rows.filter((row) => {
@@ -181,7 +181,7 @@ export async function inspectAddPrompt(db, input) {
181
181
  throw new DinoxError(`c_cmd table is missing required columns: ${missingRequired.join(', ')}`);
182
182
  }
183
183
  if (columnNames.has('user_id') && !userId) {
184
- throw new DinoxError('c_cmd insert requires user_id. Missing persisted userId. Run `dino auth login` first.');
184
+ 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
185
  }
186
186
  const rows = await listPromptRowsForWrite(db, columnNames);
187
187
  const samePromptRows = rows.filter((row) => {
@@ -0,0 +1,26 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { Command } from 'commander';
3
+ import { loadConfig } from '../config/store.js';
4
+ import type { ResolvedDinoxConfig } from '../config/types.js';
5
+ import type { CommandExecutionContext } from './descriptor/types.js';
6
+ export type CommandRuntime = {
7
+ db: AbstractPowerSyncDatabase;
8
+ config: ResolvedDinoxConfig;
9
+ rawConfig: Awaited<ReturnType<typeof loadConfig>>;
10
+ offline: boolean;
11
+ timeoutMs: number;
12
+ stale: boolean;
13
+ };
14
+ type RuntimeSource = CommandExecutionContext | Command | Record<string, unknown>;
15
+ export declare function commandStale(runtime: Pick<CommandRuntime, 'offline' | 'stale'>): boolean;
16
+ export declare function withCommandRuntime<T>(source: RuntimeSource, run: (runtime: CommandRuntime) => Promise<T>): Promise<T>;
17
+ export declare function syncBeforeCommand(runtime: CommandRuntime, options?: {
18
+ syncTokenIndex?: boolean;
19
+ timeoutMs?: number;
20
+ }): Promise<void>;
21
+ export declare function waitForMutationUpload(runtime: CommandRuntime, options?: {
22
+ timeoutMs?: number;
23
+ }): Promise<void>;
24
+ export declare function printUploadDisabledWarning(runtime: CommandRuntime, message?: string): void;
25
+ export declare function printStaleCacheWarning(runtime: CommandRuntime, message?: string): void;
26
+ export {};