@dinoxx/dinox-cli 1.0.22 → 1.1.1

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 (147) hide show
  1. package/README.md +33 -10
  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 +89 -19
  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.d.ts +23 -1
  55. package/dist/commands/sync.js +105 -60
  56. package/dist/commands/tags/content.js +9 -0
  57. package/dist/commands/tags/index.js +2 -6
  58. package/dist/commands/tags/operations.js +15 -10
  59. package/dist/commands/tags/repo.d.ts +2 -0
  60. package/dist/commands/tags/repo.js +18 -5
  61. package/dist/commands/tags/store.js +1 -1
  62. package/dist/commands/todo/contentModel.d.ts +4 -3
  63. package/dist/commands/todo/contentModel.js +78 -28
  64. package/dist/commands/todo/dryRun.d.ts +1 -0
  65. package/dist/commands/todo/dryRun.js +17 -13
  66. package/dist/commands/todo/index.d.ts +6 -0
  67. package/dist/commands/todo/index.js +89 -123
  68. package/dist/commands/todo/repo.d.ts +36 -1
  69. package/dist/commands/todo/repo.js +146 -37
  70. package/dist/commands/todo/taskIdentity.d.ts +2 -0
  71. package/dist/commands/todo/taskIdentity.js +64 -0
  72. package/dist/commands/todo/taskModel.js +27 -17
  73. package/dist/commands/update.d.ts +13 -1
  74. package/dist/commands/update.js +135 -23
  75. package/dist/commands/writeDryRun.d.ts +3 -2
  76. package/dist/commands/writeDryRun.js +4 -3
  77. package/dist/commands/writeDurability.d.ts +5 -0
  78. package/dist/commands/writeDurability.js +22 -0
  79. package/dist/config/paths.js +6 -1
  80. package/dist/config/resolve.d.ts +2 -0
  81. package/dist/config/resolve.js +23 -9
  82. package/dist/config/store.d.ts +3 -0
  83. package/dist/config/store.js +61 -14
  84. package/dist/config/types.d.ts +4 -0
  85. package/dist/daemon/client.d.ts +26 -1
  86. package/dist/daemon/client.js +236 -12
  87. package/dist/daemon/ensure.js +74 -12
  88. package/dist/daemon/index.d.ts +42 -0
  89. package/dist/daemon/index.js +202 -52
  90. package/dist/daemon/lifecycle.d.ts +44 -0
  91. package/dist/daemon/lifecycle.js +118 -0
  92. package/dist/daemon/pid.d.ts +19 -6
  93. package/dist/daemon/pid.js +147 -13
  94. package/dist/daemon/route.d.ts +14 -0
  95. package/dist/daemon/route.js +40 -0
  96. package/dist/daemon/server.d.ts +11 -0
  97. package/dist/daemon/server.js +201 -50
  98. package/dist/daemon/types.d.ts +29 -1
  99. package/dist/daemon/types.js +1 -0
  100. package/dist/dinox.js +25 -1
  101. package/dist/powersync/connector.js +31 -16
  102. package/dist/powersync/exclusiveRuntime.d.ts +20 -0
  103. package/dist/powersync/exclusiveRuntime.js +48 -0
  104. package/dist/powersync/runtime.d.ts +25 -4
  105. package/dist/powersync/runtime.js +255 -35
  106. package/dist/powersync/syncGate.d.ts +4 -1
  107. package/dist/powersync/syncGate.js +11 -2
  108. package/dist/powersync/syncPass.d.ts +2 -0
  109. package/dist/powersync/syncPass.js +15 -4
  110. package/dist/powersync/tokenIndex.d.ts +13 -0
  111. package/dist/powersync/tokenIndex.js +216 -37
  112. package/dist/powersync/uploader.js +107 -52
  113. package/dist/utils/abort.js +0 -4
  114. package/dist/utils/asyncRelease.d.ts +2 -0
  115. package/dist/utils/asyncRelease.js +25 -0
  116. package/dist/utils/bidlinkCanonicalization.js +3 -0
  117. package/dist/utils/dryRun.d.ts +1 -1
  118. package/dist/utils/errors.d.ts +15 -1
  119. package/dist/utils/errors.js +223 -5
  120. package/dist/utils/output.d.ts +1 -0
  121. package/dist/utils/output.js +51 -15
  122. package/dist/utils/redact.js +1 -1
  123. package/dist/utils/sqliteLease.d.ts +8 -0
  124. package/dist/utils/sqliteLease.js +59 -0
  125. package/dist/utils/syncTimeout.d.ts +7 -1
  126. package/dist/utils/syncTimeout.js +8 -7
  127. package/dist/utils/terminalSanitization.js +6 -2
  128. package/dist/utils/tiptapMarkdown.js +49 -3
  129. package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
  130. package/dist/utils/tiptapMarkdownSyntax.js +249 -0
  131. package/dist/utils/tokenize.js +24 -2
  132. package/dist/utils/updateNotice.js +60 -12
  133. package/package.json +14 -10
  134. package/dist/commands/mcp/auth.d.ts +0 -4
  135. package/dist/commands/mcp/auth.js +0 -75
  136. package/dist/commands/mcp/index.d.ts +0 -2
  137. package/dist/commands/mcp/index.js +0 -86
  138. package/dist/commands/mcp/noteContentTools.d.ts +0 -222
  139. package/dist/commands/mcp/noteContentTools.js +0 -390
  140. package/dist/commands/mcp/server.d.ts +0 -34
  141. package/dist/commands/mcp/server.js +0 -424
  142. package/dist/commands/mcp/tools.d.ts +0 -2
  143. package/dist/commands/mcp/tools.js +0 -32
  144. package/dist/commands/mcp/types.d.ts +0 -39
  145. package/dist/commands/mcp/types.js +0 -1
  146. package/dist/utils/noteContentEngine.d.ts +0 -333
  147. package/dist/utils/noteContentEngine.js +0 -740
@@ -4,9 +4,71 @@ import { generateId } from '../../utils/id.js';
4
4
  import { nowIsoUtc } from '../../utils/time.js';
5
5
  import { applyTaskCheckedStatus, appendTasksToDoc, buildAppendMarkdown, buildContentTextPreview, buildCreateMarkdown, buildSummaryTitle, createTodoDocFromTasks, deriveImageDetailFromContentJson, normalizeTaskTexts, patchTaskMarkdownByOrder, parseTiptapDocFromDb, } from './contentModel.js';
6
6
  import { buildTaskKey, extractFlattenedTasks, filterTasksForTodoList, } from './taskModel.js';
7
+ import { isLegacyTaskId } from './taskIdentity.js';
7
8
  export { normalizeTaskTexts } from './contentModel.js';
9
+ const TODO_TASK_CANDIDATE_SCAN_LIMIT = 500;
10
+ export function validateTodoAppendTarget(target) {
11
+ if (typeof target.is_del === 'number' && target.is_del !== 0) {
12
+ throw new DinoxError(`Note has been deleted: ${target.id}`);
13
+ }
14
+ const noteId = target.id?.trim() ?? '';
15
+ if (!noteId) {
16
+ throw new DinoxError('Note id is required');
17
+ }
18
+ const currentDoc = parseTiptapDocFromDb(target.content_json);
19
+ const hasMarkdownSnapshot = Boolean(target.content_md?.trim());
20
+ const hasDocContent = Array.isArray(currentDoc.content) && currentDoc.content.length > 0;
21
+ if (!hasMarkdownSnapshot && hasDocContent) {
22
+ throw new DinoxError('Cannot append todo: current note has non-empty content_json but empty content_md. Please repair note markdown before append.');
23
+ }
24
+ return { currentDoc, noteId, expectedVersion: normalizeVersion(target.version) };
25
+ }
26
+ function normalizeVersion(value) {
27
+ const parsed = Number(value ?? 0);
28
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
29
+ }
30
+ function assertSingleUpdatedRow(result, message) {
31
+ if (result.rows?.length !== 1) {
32
+ throw new DinoxError(message);
33
+ }
34
+ }
35
+ /** Serialize parsed todo-search options into a JSON-safe wire payload. */
36
+ export function toTodoSearchWire(query, options) {
37
+ return {
38
+ query,
39
+ includeDeleted: options.includeDeleted,
40
+ taskStatus: options.taskStatus,
41
+ taskTagFilter: options.taskTagFilter,
42
+ limit: options.limit,
43
+ scanLimit: options.scanLimit,
44
+ startDate: options.taskDateRange?.startDate?.toISOString(),
45
+ endDate: options.taskDateRange?.endDate?.toISOString(),
46
+ };
47
+ }
48
+ /**
49
+ * Run a todo search from a wire payload. Shared by the local command path and the daemon
50
+ * RPC handler so both produce identical results from one implementation.
51
+ */
52
+ export async function executeTodoSearch(db, wire) {
53
+ const taskDateRange = wire.startDate || wire.endDate
54
+ ? {
55
+ startDate: wire.startDate ? new Date(wire.startDate) : undefined,
56
+ endDate: wire.endDate ? new Date(wire.endDate) : undefined,
57
+ }
58
+ : undefined;
59
+ return searchTodoTasks(db, wire.query, {
60
+ includeDeleted: wire.includeDeleted,
61
+ taskStatus: wire.taskStatus,
62
+ taskTagFilter: wire.taskTagFilter,
63
+ taskDateRange,
64
+ limit: wire.limit,
65
+ scanLimit: wire.scanLimit,
66
+ });
67
+ }
8
68
  export async function searchTodoTasks(db, query, options) {
9
- const noteRows = await loadTodoNoteRows(db, options.includeDeleted, options.scanLimit);
69
+ const scannedRows = await loadTodoNoteRows(db, options.includeDeleted, options.scanLimit + 1);
70
+ const scanTruncated = scannedRows.length > options.scanLimit;
71
+ const noteRows = scannedRows.slice(0, options.scanLimit);
10
72
  const allTasks = extractFlattenedTasks(noteRows);
11
73
  const filteredTasks = filterTasksForTodoList(allTasks, query, options.taskStatus, options.taskTagFilter, options.taskDateRange);
12
74
  const visibleTasks = filteredTasks.slice(0, options.limit);
@@ -32,10 +94,13 @@ export async function searchTodoTasks(db, query, options) {
32
94
  query: query.trim(),
33
95
  returned_count: tasks.length,
34
96
  total_count: filteredTasks.length,
35
- matched_note_count: new Set(tasks.map((task) => task.note_id)).size,
97
+ matched_note_count: new Set(filteredTasks.map((task) => task.noteId)).size,
36
98
  limit: options.limit,
37
99
  scan_limit: options.scanLimit,
38
- truncated: filteredTasks.length > tasks.length,
100
+ scanned_note_count: noteRows.length,
101
+ scan_truncated: scanTruncated,
102
+ total_count_is_lower_bound: scanTruncated,
103
+ truncated: scanTruncated || filteredTasks.length > tasks.length,
39
104
  applied_filters: {
40
105
  status: options.taskStatus,
41
106
  tags: [...options.taskTagFilter],
@@ -59,24 +124,13 @@ export async function appendTodoTasks(db, options) {
59
124
  }
60
125
  throw new DinoxError('No eligible note found for append. Try `dino todo create ...` first.');
61
126
  }
62
- if (typeof target.is_del === 'number' && target.is_del !== 0) {
63
- throw new DinoxError(`Note has been deleted: ${target.id}`);
64
- }
65
- if (!target.id?.trim()) {
66
- throw new DinoxError('Note id is required');
67
- }
68
- const currentDoc = parseTiptapDocFromDb(target.content_json);
69
- const hasMarkdownSnapshot = Boolean(target.content_md?.trim());
70
- const hasDocContent = Array.isArray(currentDoc.content) && currentDoc.content.length > 0;
71
- if (!hasMarkdownSnapshot && hasDocContent) {
72
- throw new DinoxError('Cannot append todo: current note has non-empty content_json but empty content_md. Please repair note markdown before append.');
73
- }
127
+ const { currentDoc, noteId, expectedVersion } = validateTodoAppendTarget(target);
74
128
  const nextDoc = appendTasksToDoc(currentDoc, taskTexts);
75
- const imageDetail = deriveImageDetailFromContentJson(nextDoc);
129
+ const imageDetail = deriveImageDetailFromContentJson(nextDoc, noteId);
76
130
  const contentMd = buildAppendMarkdown(target.content_md, taskTexts);
77
131
  const contentText = buildContentTextPreview(contentMd);
78
132
  const now = nowIsoUtc();
79
- await db.execute(`
133
+ const updateResult = await db.execute(`
80
134
  UPDATE c_note
81
135
  SET
82
136
  content_json = ?,
@@ -87,16 +141,20 @@ export async function appendTodoTasks(db, options) {
87
141
  version = COALESCE(version, 0) + 1
88
142
  WHERE id = ?
89
143
  AND (is_del IS NULL OR is_del = 0)
144
+ AND COALESCE(version, 0) = ?
145
+ RETURNING id
90
146
  `, [
91
147
  JSON.stringify(nextDoc),
92
148
  JSON.stringify(imageDetail),
93
149
  contentMd,
94
150
  contentText,
95
151
  now,
96
- target.id,
152
+ noteId,
153
+ expectedVersion,
97
154
  ]);
155
+ assertSingleUpdatedRow(updateResult, `Note changed concurrently: ${noteId}. Re-run the todo append command.`);
98
156
  return {
99
- note_id: target.id,
157
+ note_id: noteId,
100
158
  title: (target.title ?? '').trim(),
101
159
  appended_count: taskTexts.length,
102
160
  source,
@@ -105,12 +163,13 @@ export async function appendTodoTasks(db, options) {
105
163
  export async function createTodoNote(db, options) {
106
164
  const taskTexts = normalizeTaskTexts(options.taskTexts);
107
165
  const title = options.title?.trim() || buildSummaryTitle(taskTexts);
166
+ const noteId = generateId();
108
167
  const contentJson = createTodoDocFromTasks(taskTexts);
109
- const imageDetail = deriveImageDetailFromContentJson(contentJson);
168
+ const imageDetail = deriveImageDetailFromContentJson(contentJson, noteId);
110
169
  const contentMd = buildCreateMarkdown(taskTexts);
111
170
  const contentText = buildContentTextPreview(contentMd);
112
171
  const created = await createNote(db, {
113
- id: generateId(),
172
+ id: noteId,
114
173
  title,
115
174
  noteType: 'note',
116
175
  contentMd,
@@ -132,13 +191,17 @@ export async function updateTodoTaskStatus(db, options) {
132
191
  if (!taskId) {
133
192
  throw new DinoxError('taskId is required');
134
193
  }
135
- const candidates = await loadTodoTaskCandidatesByTaskId(db, taskId);
194
+ const candidates = await loadTodoTaskCandidatesByTaskId(db, taskId, { noteId: options.noteId });
136
195
  const matches = [];
137
196
  const skippedCandidates = [];
138
197
  for (const candidate of candidates) {
139
198
  try {
199
+ const noteId = (candidate.id ?? '').trim();
200
+ if (!noteId) {
201
+ throw new DinoxError('Note id is required');
202
+ }
140
203
  const doc = parseTiptapDocFromDb(candidate.content_json);
141
- const applied = applyTaskCheckedStatus(doc, taskId, options.checked);
204
+ const applied = applyTaskCheckedStatus(doc, noteId, taskId, options.checked);
142
205
  if (applied.matchedIndices.length === 0) {
143
206
  continue;
144
207
  }
@@ -190,10 +253,10 @@ export async function updateTodoTaskStatus(db, options) {
190
253
  }
191
254
  const nextContentMd = patchTaskMarkdownByOrder(match.candidate.content_md, match.applied.taskOrder, match.applied.matchedIndices, options.checked);
192
255
  const nextContentText = buildContentTextPreview(nextContentMd);
193
- const nextImageDetail = deriveImageDetailFromContentJson(match.applied.nextDoc);
256
+ const nextImageDetail = deriveImageDetailFromContentJson(match.applied.nextDoc, noteId);
194
257
  const hasMarkdownChanged = nextContentMd !== (match.candidate.content_md ?? '');
195
- const hasContentJsonChanged = match.applied.changedCount > 0;
196
- if (!hasContentJsonChanged && !hasMarkdownChanged) {
258
+ const hasStatusChanged = match.applied.statusChangedCount > 0;
259
+ if (!hasStatusChanged && !hasMarkdownChanged) {
197
260
  return {
198
261
  note_id: noteId,
199
262
  task_id: taskId,
@@ -202,7 +265,7 @@ export async function updateTodoTaskStatus(db, options) {
202
265
  title: (match.candidate.title ?? '').trim(),
203
266
  };
204
267
  }
205
- await db.execute(`
268
+ const updateResult = await db.execute(`
206
269
  UPDATE c_note
207
270
  SET
208
271
  content_json = ?,
@@ -213,6 +276,8 @@ export async function updateTodoTaskStatus(db, options) {
213
276
  version = COALESCE(version, 0) + 1
214
277
  WHERE id = ?
215
278
  AND (is_del IS NULL OR is_del = 0)
279
+ AND COALESCE(version, 0) = ?
280
+ RETURNING id
216
281
  `, [
217
282
  JSON.stringify(match.applied.nextDoc),
218
283
  JSON.stringify(nextImageDetail),
@@ -220,7 +285,9 @@ export async function updateTodoTaskStatus(db, options) {
220
285
  nextContentText,
221
286
  nowIsoUtc(),
222
287
  noteId,
288
+ normalizeVersion(match.candidate.version),
223
289
  ]);
290
+ assertSingleUpdatedRow(updateResult, `Note changed concurrently: ${noteId}. Re-run the todo update command.`);
224
291
  return {
225
292
  note_id: noteId,
226
293
  task_id: taskId,
@@ -257,7 +324,8 @@ export async function getTodoAppendTargetById(db, noteId) {
257
324
  n.title,
258
325
  n.content_json,
259
326
  n.content_md,
260
- n.is_del
327
+ n.is_del,
328
+ n.version
261
329
  FROM c_note n
262
330
  WHERE n.id = ?
263
331
  `, [noteId]);
@@ -269,7 +337,8 @@ export async function getLatestTodoAppendTarget(db) {
269
337
  n.title,
270
338
  n.content_json,
271
339
  n.content_md,
272
- n.is_del
340
+ n.is_del,
341
+ n.version
273
342
  FROM c_note n
274
343
  WHERE (n.is_del IS NULL OR n.is_del = 0)
275
344
  AND n.image_detail IS NOT NULL
@@ -279,24 +348,64 @@ export async function getLatestTodoAppendTarget(db) {
279
348
  LIMIT 1
280
349
  `, []);
281
350
  }
282
- export async function loadTodoTaskCandidatesByTaskId(db, taskId) {
283
- const escapedTaskId = escapeLikePattern(taskId);
284
- return db.getAll(`
351
+ export async function loadTodoTaskCandidatesByTaskId(db, taskId, options = {}) {
352
+ const exactNoteId = options.noteId?.trim();
353
+ if (options.noteId !== undefined && !exactNoteId) {
354
+ throw new DinoxError('--note-id cannot be empty');
355
+ }
356
+ if (exactNoteId) {
357
+ return db.getAll(`
358
+ SELECT
359
+ n.id,
360
+ n.title,
361
+ n.content_json,
362
+ n.content_md,
363
+ n.is_del,
364
+ n.created_at,
365
+ n.version
366
+ FROM c_note n
367
+ WHERE n.id = ?
368
+ AND (n.is_del IS NULL OR n.is_del = 0)
369
+ AND n.content_json IS NOT NULL
370
+ AND LENGTH(TRIM(n.content_json)) > 0
371
+ LIMIT 1
372
+ `, [exactNoteId]);
373
+ }
374
+ const pattern = isLegacyTaskId(taskId)
375
+ ? '%taskItem%'
376
+ : `%${escapeLikePattern(taskId)}%`;
377
+ const escapeClause = isLegacyTaskId(taskId) ? '' : " ESCAPE '\\'";
378
+ const candidates = await db.getAll(`
285
379
  SELECT
286
380
  n.id,
287
381
  n.title,
288
382
  n.content_json,
289
383
  n.content_md,
290
384
  n.is_del,
291
- n.created_at
385
+ n.created_at,
386
+ n.version
292
387
  FROM c_note n
293
388
  WHERE (n.is_del IS NULL OR n.is_del = 0)
294
389
  AND n.content_json IS NOT NULL
295
390
  AND LENGTH(TRIM(n.content_json)) > 0
296
- AND n.content_json LIKE ? ESCAPE '\\'
297
- ORDER BY n.created_at DESC
298
- LIMIT 500
299
- `, [`%${escapedTaskId}%`]);
391
+ AND n.content_json LIKE ?${escapeClause}
392
+ LIMIT ?
393
+ `, [pattern, TODO_TASK_CANDIDATE_SCAN_LIMIT + 1]);
394
+ if (candidates.length > TODO_TASK_CANDIDATE_SCAN_LIMIT) {
395
+ throw new DinoxError(`Task lookup reached the ${TODO_TASK_CANDIDATE_SCAN_LIMIT}-note safety limit; refusing to update because task id uniqueness could not be verified.`, {
396
+ code: 'TODO_TASK_LOOKUP_TRUNCATED',
397
+ exitCode: 5,
398
+ suggestion: 'Run `dino todo search <task-text> --scan-limit 5000 --format json` to get the task note_id, then retry with `--note-id <note_id>`.',
399
+ details: {
400
+ taskId,
401
+ candidateLimit: TODO_TASK_CANDIDATE_SCAN_LIMIT,
402
+ scannedCandidateCount: candidates.length,
403
+ scanTruncated: true,
404
+ },
405
+ recoverable: true,
406
+ });
407
+ }
408
+ return candidates;
300
409
  }
301
410
  function escapeLikePattern(value) {
302
411
  return value.replace(/[\\%_]/g, (token) => `\\${token}`);
@@ -0,0 +1,2 @@
1
+ export declare function buildLegacyTaskId(noteId: string, preorderIndex: number, taskNode: unknown): string;
2
+ export declare function isLegacyTaskId(value: string): boolean;
@@ -0,0 +1,64 @@
1
+ import { createHash } from 'node:crypto';
2
+ const LEGACY_TASK_ID_PREFIX = 'legacy-task-';
3
+ const LEGACY_TASK_ID_RE = /^legacy-task-[a-f0-9]{24}$/;
4
+ function normalizeChecked(value) {
5
+ if (value === true || value === 'true')
6
+ return true;
7
+ if (value === false || value === 'false')
8
+ return false;
9
+ return undefined;
10
+ }
11
+ function canonicalizeTaskIdentityValue(value) {
12
+ if (Array.isArray(value)) {
13
+ return value
14
+ .map((item) => canonicalizeTaskIdentityValue(item))
15
+ .filter((item) => item !== undefined);
16
+ }
17
+ if (!value || typeof value !== 'object') {
18
+ return value;
19
+ }
20
+ const record = value;
21
+ if (record.type === 'taskList') {
22
+ return undefined;
23
+ }
24
+ const normalized = {};
25
+ for (const key of Object.keys(record).sort()) {
26
+ if (key === 'id' || key === 'checked') {
27
+ continue;
28
+ }
29
+ const next = canonicalizeTaskIdentityValue(record[key]);
30
+ if (next === undefined) {
31
+ continue;
32
+ }
33
+ if (key === 'attrs'
34
+ && next
35
+ && typeof next === 'object'
36
+ && !Array.isArray(next)
37
+ && Object.keys(next).length === 0) {
38
+ continue;
39
+ }
40
+ normalized[key] = next;
41
+ }
42
+ return normalized;
43
+ }
44
+ export function buildLegacyTaskId(noteId, preorderIndex, taskNode) {
45
+ const node = taskNode && typeof taskNode === 'object'
46
+ ? taskNode
47
+ : {};
48
+ const signature = JSON.stringify({
49
+ checked: normalizeChecked(node.checked) ?? normalizeChecked(node.attrs?.checked) ?? false,
50
+ node: canonicalizeTaskIdentityValue(taskNode),
51
+ });
52
+ const digest = createHash('sha256')
53
+ .update(noteId.trim())
54
+ .update('\0')
55
+ .update(String(preorderIndex))
56
+ .update('\0')
57
+ .update(signature)
58
+ .digest('hex')
59
+ .slice(0, 24);
60
+ return `${LEGACY_TASK_ID_PREFIX}${digest}`;
61
+ }
62
+ export function isLegacyTaskId(value) {
63
+ return LEGACY_TASK_ID_RE.test(value.trim());
64
+ }
@@ -1,3 +1,4 @@
1
+ import { buildLegacyTaskId } from './taskIdentity.js';
1
2
  const DEFAULT_TASK_TIME_DISPLAY_FORMAT = 'datetime';
2
3
  export function buildTaskKey(noteId, taskId) {
3
4
  return `${noteId}:${taskId}`;
@@ -14,6 +15,7 @@ export function extractFlattenedTasks(rows) {
14
15
  continue;
15
16
  }
16
17
  const { createdAt, createdAtRaw } = parseCreatedAt(row.created_at, row.updated_at);
18
+ const preorderState = { nextIndex: 0 };
17
19
  extracted.push(...extractTasksFromDetailWithHierarchy({
18
20
  noteId,
19
21
  noteTitle: normalizeString(row.title),
@@ -21,6 +23,7 @@ export function extractFlattenedTasks(rows) {
21
23
  createdAtRaw,
22
24
  noteTags: parseNoteTags(row.tags),
23
25
  taskItems,
26
+ preorderState,
24
27
  }));
25
28
  }
26
29
  extracted.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime());
@@ -233,16 +236,15 @@ function extractNestedTasks(content) {
233
236
  return nested;
234
237
  }
235
238
  function extractTasksFromDetailWithHierarchy(input) {
236
- const { noteId, noteTitle, createdAt, createdAtRaw, noteTags, taskItems, depth = 0 } = input;
239
+ const { noteId, noteTitle, createdAt, createdAtRaw, noteTags, taskItems, preorderState, depth = 0, } = input;
237
240
  const result = [];
238
241
  for (const item of taskItems) {
242
+ const preorderIndex = preorderState.nextIndex;
243
+ preorderState.nextIndex += 1;
239
244
  const taskId = normalizeString(item.id) ||
240
245
  normalizeString(item.attrs?.id) ||
241
- '';
242
- if (!taskId) {
243
- continue;
244
- }
245
- const checked = item.checked === true || item.checked === 'true' || item.attrs?.checked === true;
246
+ buildLegacyTaskId(noteId, preorderIndex, item);
247
+ const checked = normalizeTaskChecked(item.checked) ?? normalizeTaskChecked(item.attrs?.checked) ?? false;
246
248
  const text = getTaskText(item).trim();
247
249
  const tags = getTaskTags(item);
248
250
  const { startTime, dueTime } = getTaskTimeRange(item);
@@ -253,6 +255,7 @@ function extractTasksFromDetailWithHierarchy(input) {
253
255
  createdAtRaw,
254
256
  noteTags,
255
257
  taskItems: extractNestedTasks(item.content),
258
+ preorderState,
256
259
  depth: depth + 1,
257
260
  });
258
261
  result.push({
@@ -302,18 +305,18 @@ function resolveTaskDateRange(taskDateRange) {
302
305
  if (!taskDateRange) {
303
306
  return null;
304
307
  }
305
- const startSource = taskDateRange.startDate ?? taskDateRange.endDate;
306
- const endSource = taskDateRange.endDate ?? taskDateRange.startDate;
307
- if (!startSource || !endSource) {
308
+ if (!taskDateRange.startDate && !taskDateRange.endDate) {
308
309
  return null;
309
310
  }
310
- const start = new Date(startSource);
311
- const end = new Date(endSource);
311
+ const start = taskDateRange.startDate
312
+ ? new Date(taskDateRange.startDate)
313
+ : new Date(-8_640_000_000_000_000);
314
+ const end = taskDateRange.endDate
315
+ ? new Date(taskDateRange.endDate)
316
+ : new Date(8_640_000_000_000_000);
312
317
  if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
313
318
  return null;
314
319
  }
315
- start.setHours(0, 0, 0, 0);
316
- end.setHours(23, 59, 59, 999);
317
320
  return {
318
321
  startMs: Math.min(start.getTime(), end.getTime()),
319
322
  endMs: Math.max(start.getTime(), end.getTime()),
@@ -321,12 +324,12 @@ function resolveTaskDateRange(taskDateRange) {
321
324
  }
322
325
  function isTaskInDateRange(task, range) {
323
326
  const dueMs = parseTaskPointMs(task.dueTime);
324
- if (dueMs !== null && dueMs >= range.startMs && dueMs <= range.endMs) {
325
- return true;
327
+ if (dueMs !== null) {
328
+ return dueMs >= range.startMs && dueMs <= range.endMs;
326
329
  }
327
330
  const startMs = parseTaskPointMs(task.startTime);
328
- if (startMs !== null && startMs >= range.startMs && startMs <= range.endMs) {
329
- return true;
331
+ if (startMs !== null) {
332
+ return startMs >= range.startMs && startMs <= range.endMs;
330
333
  }
331
334
  const createdAtMs = task.createdAt.getTime();
332
335
  if (!Number.isNaN(createdAtMs) && createdAtMs >= range.startMs && createdAtMs <= range.endMs) {
@@ -347,3 +350,10 @@ function normalizeString(value) {
347
350
  }
348
351
  return value.trim();
349
352
  }
353
+ function normalizeTaskChecked(value) {
354
+ if (value === true || value === 'true')
355
+ return true;
356
+ if (value === false || value === 'false')
357
+ return false;
358
+ return undefined;
359
+ }
@@ -4,10 +4,22 @@ type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
4
4
  type LogFn = (...parts: unknown[]) => void;
5
5
  type UpdateCommandDeps = {
6
6
  detectPackageManager: () => PackageManager;
7
- runUpdate: (pm: PackageManager) => Promise<void>;
7
+ runUpdate: (pm: PackageManager, options: {
8
+ structuredOutput: boolean;
9
+ }) => Promise<void>;
8
10
  getVersion: () => string;
9
11
  log: LogFn;
10
12
  };
13
+ export declare function detectPackageManager(input?: {
14
+ env?: NodeJS.ProcessEnv;
15
+ scriptPath?: string;
16
+ }): PackageManager;
17
+ type UpdateSpawnSpec = {
18
+ command: string;
19
+ args: string[];
20
+ shell: boolean;
21
+ };
22
+ export declare function resolveUpdateSpawnSpec(pm: PackageManager, platform?: NodeJS.Platform): UpdateSpawnSpec;
11
23
  export declare function createUpdateCommandDescriptor(overrides?: Partial<UpdateCommandDeps>): CommandDescriptor;
12
24
  export declare const updateCommandDescriptor: CommandDescriptor;
13
25
  export declare function registerUpdateCommand(program: Command, overrides?: Partial<UpdateCommandDeps>): void;