@dinoxx/dinox-cli 1.0.18 → 1.0.20

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 (118) hide show
  1. package/README.md +807 -418
  2. package/dist/auth/userInfo.d.ts +2 -0
  3. package/dist/auth/userInfo.js +28 -11
  4. package/dist/cli.js +15 -1
  5. package/dist/commands/auth/index.d.ts +2 -0
  6. package/dist/commands/auth/index.js +217 -163
  7. package/dist/commands/boxes/index.d.ts +2 -0
  8. package/dist/commands/boxes/index.js +119 -75
  9. package/dist/commands/boxes/repo.d.ts +8 -0
  10. package/dist/commands/boxes/repo.js +41 -0
  11. package/dist/commands/config/index.d.ts +2 -0
  12. package/dist/commands/config/index.js +85 -36
  13. package/dist/commands/descriptor/register.d.ts +8 -0
  14. package/dist/commands/descriptor/register.js +113 -0
  15. package/dist/commands/descriptor/types.d.ts +37 -0
  16. package/dist/commands/descriptor/types.js +17 -0
  17. package/dist/commands/graph/index.d.ts +2 -0
  18. package/dist/commands/graph/index.js +143 -73
  19. package/dist/commands/info/index.d.ts +2 -0
  20. package/dist/commands/info/index.js +41 -14
  21. package/dist/commands/mcp/index.js +11 -10
  22. package/dist/commands/mcp/server.js +8 -4
  23. package/dist/commands/mcp/tools.js +30 -104
  24. package/dist/commands/mcp/types.d.ts +8 -1
  25. package/dist/commands/meta/index.d.ts +2 -0
  26. package/dist/commands/meta/index.js +64 -33
  27. package/dist/commands/meta/repo.d.ts +6 -0
  28. package/dist/commands/meta/repo.js +6 -0
  29. package/dist/commands/notes/actions.d.ts +10 -0
  30. package/dist/commands/notes/actions.js +403 -0
  31. package/dist/commands/notes/descriptors.d.ts +4 -0
  32. package/dist/commands/notes/descriptors.js +250 -0
  33. package/dist/commands/notes/dryRun.d.ts +31 -0
  34. package/dist/commands/notes/dryRun.js +130 -0
  35. package/dist/commands/notes/index.d.ts +2 -51
  36. package/dist/commands/notes/index.js +2 -581
  37. package/dist/commands/notes/repo.d.ts +9 -0
  38. package/dist/commands/notes/repo.js +56 -10
  39. package/dist/commands/notes/runtime.d.ts +28 -0
  40. package/dist/commands/notes/runtime.js +185 -0
  41. package/dist/commands/notes/searchOutput.d.ts +10 -2
  42. package/dist/commands/notes/searchOutput.js +14 -2
  43. package/dist/commands/notes/searchResponse.d.ts +51 -0
  44. package/dist/commands/notes/searchResponse.js +56 -0
  45. package/dist/commands/notes/searchSql.js +2 -0
  46. package/dist/commands/prompt/index.d.ts +2 -0
  47. package/dist/commands/prompt/index.js +122 -72
  48. package/dist/commands/prompt/repo.d.ts +7 -0
  49. package/dist/commands/prompt/repo.js +46 -0
  50. package/dist/commands/schema/commandMetadata.d.ts +2 -0
  51. package/dist/commands/schema/commandMetadata.js +18 -0
  52. package/dist/commands/schema/globalOptions.d.ts +3 -0
  53. package/dist/commands/schema/globalOptions.js +3 -0
  54. package/dist/commands/schema/index.d.ts +4 -0
  55. package/dist/commands/schema/index.js +109 -0
  56. package/dist/commands/schema/renderSkills.d.ts +6 -0
  57. package/dist/commands/schema/renderSkills.js +216 -0
  58. package/dist/commands/schema/spec.d.ts +6 -0
  59. package/dist/commands/schema/spec.js +40 -0
  60. package/dist/commands/schema/types.d.ts +47 -0
  61. package/dist/commands/schema/types.js +1 -0
  62. package/dist/commands/storage/index.d.ts +4 -0
  63. package/dist/commands/storage/index.js +292 -0
  64. package/dist/commands/storage/repo.d.ts +162 -0
  65. package/dist/commands/storage/repo.js +770 -0
  66. package/dist/commands/sync.d.ts +2 -0
  67. package/dist/commands/sync.js +41 -27
  68. package/dist/commands/tags/index.d.ts +2 -0
  69. package/dist/commands/tags/index.js +121 -79
  70. package/dist/commands/tags/repo.d.ts +7 -0
  71. package/dist/commands/tags/repo.js +52 -0
  72. package/dist/commands/todo/dryRun.d.ts +23 -0
  73. package/dist/commands/todo/dryRun.js +133 -0
  74. package/dist/commands/todo/index.d.ts +2 -0
  75. package/dist/commands/todo/index.js +285 -135
  76. package/dist/commands/todo/repo.d.ts +11 -0
  77. package/dist/commands/todo/repo.js +37 -9
  78. package/dist/commands/update.d.ts +3 -0
  79. package/dist/commands/update.js +35 -13
  80. package/dist/commands/writeDryRun.d.ts +62 -0
  81. package/dist/commands/writeDryRun.js +131 -0
  82. package/dist/config/paths.d.ts +2 -1
  83. package/dist/config/paths.js +11 -1
  84. package/dist/config/resolve.js +7 -5
  85. package/dist/constants/links.d.ts +1 -1
  86. package/dist/constants/links.js +1 -1
  87. package/dist/daemon/ensure.js +3 -2
  88. package/dist/daemon/index.js +17 -15
  89. package/dist/dinox.js +9 -13
  90. package/dist/powersync/runtime.d.ts +1 -1
  91. package/dist/powersync/runtime.js +5 -3
  92. package/dist/powersync/schema/index.d.ts +27 -0
  93. package/dist/powersync/schema/index.js +2 -0
  94. package/dist/powersync/schema/note.d.ts +1 -0
  95. package/dist/powersync/schema/note.js +1 -0
  96. package/dist/powersync/schema/projects.d.ts +3 -0
  97. package/dist/powersync/schema/projects.js +3 -0
  98. package/dist/powersync/schema/storage.d.ts +24 -0
  99. package/dist/powersync/schema/storage.js +24 -0
  100. package/dist/skills/check.d.ts +6 -0
  101. package/dist/skills/check.js +254 -0
  102. package/dist/utils/bidlinkCanonicalization.d.ts +2 -0
  103. package/dist/utils/bidlinkCanonicalization.js +161 -0
  104. package/dist/utils/dryRun.d.ts +13 -0
  105. package/dist/utils/dryRun.js +69 -0
  106. package/dist/utils/output.d.ts +19 -0
  107. package/dist/utils/output.js +155 -1
  108. package/dist/utils/terminalSanitization.d.ts +2 -0
  109. package/dist/utils/terminalSanitization.js +33 -0
  110. package/dist/utils/tiptapMarkdown.d.ts +4 -1
  111. package/dist/utils/tiptapMarkdown.js +285 -113
  112. package/dist/utils/tiptapMarkdownSchema.d.ts +37 -0
  113. package/dist/utils/tiptapMarkdownSchema.js +577 -0
  114. package/dist/utils/tiptapMarkdownSchemaUtils.d.ts +74 -0
  115. package/dist/utils/tiptapMarkdownSchemaUtils.js +394 -0
  116. package/dist/utils/updateNotice.d.ts +7 -0
  117. package/dist/utils/updateNotice.js +64 -0
  118. package/package.json +25 -3
@@ -1,11 +1,15 @@
1
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
+ import { registerCommandGroup } from '../descriptor/register.js';
3
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
1
4
  import { loadConfig } from '../../config/store.js';
2
5
  import { resolveConfig } from '../../config/resolve.js';
3
- import { DinoxError } from '../../utils/errors.js';
6
+ import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
4
7
  import { readStringOrFile } from '../../utils/argValue.js';
5
- import { printYaml } from '../../utils/output.js';
8
+ import { printDryRunPreview } from '../../utils/dryRun.js';
9
+ import { DinoxError } from '../../utils/errors.js';
10
+ import { printHumanReadableLine, printSuccess } from '../../utils/output.js';
6
11
  import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
7
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
8
- import { resolveAuthIdentity } from '../../auth/userInfo.js';
12
+ import { buildTodoAppendDryRunPreview, buildTodoCreateDryRunPreview, buildTodoUpdateDryRunPreview, } from './dryRun.js';
9
13
  import { appendTodoTasks, createTodoNote, normalizeTaskTexts, searchTodoTasks, updateTodoTaskStatus, } from './repo.js';
10
14
  const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
11
15
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -13,6 +17,7 @@ const DEFAULT_TASK_LIMIT = 50;
13
17
  const DEFAULT_NOTE_SCAN_LIMIT = 200;
14
18
  const MAX_TASK_LIMIT = 500;
15
19
  const MAX_NOTE_SCAN_LIMIT = 5000;
20
+ const TODO_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
16
21
  function parseTaskStatus(value) {
17
22
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
18
23
  if (normalized === 'all' || normalized === 'completed' || normalized === 'uncompleted') {
@@ -170,8 +175,8 @@ async function parseTaskListInput(value) {
170
175
  const raw = await readStringOrFile(value);
171
176
  return parseListInput(raw, '--tasks');
172
177
  }
173
- function collectStringOptionValue(value, previous = []) {
174
- return [...previous, value];
178
+ function collectStringOptionValue(value, previous) {
179
+ return [...(Array.isArray(previous) ? previous : []), value];
175
180
  }
176
181
  async function parseTaskInput(args) {
177
182
  const merged = [];
@@ -193,148 +198,293 @@ async function syncBeforeTodoCommand(db, offline, timeoutMs) {
193
198
  }
194
199
  await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
195
200
  }
196
- export function registerTodoCommands(program) {
197
- const todo = program.command('todo').description('Manage todo tasks');
198
- todo
199
- .command('search')
200
- .description('Search tasks from note image_detail')
201
- .argument('[query]', 'task text query')
202
- .option('--status <status>', 'task status: all | completed | uncompleted', 'all')
203
- .option('--tags <string|@file>', 'task tags (JSON array or comma/newline-separated)')
204
- .option('--from <date>', 'task date range start (YYYY-MM-DD or ISO datetime)')
205
- .option('--to <date>', 'task date range end (YYYY-MM-DD or ISO datetime)')
206
- .option('--days <n>', 'recent N days for task date range (cannot be used with --from/--to)')
207
- .option('--limit <n>', `maximum returned tasks (default: ${DEFAULT_TASK_LIMIT}, max: ${MAX_TASK_LIMIT})`)
208
- .option('--scan-limit <n>', `maximum note rows scanned from c_note (default: ${DEFAULT_NOTE_SCAN_LIMIT}, max: ${MAX_NOTE_SCAN_LIMIT})`)
209
- .option('--include-deleted', 'include soft-deleted notes')
210
- .action(async (query, options, command) => {
211
- const globals = command.optsWithGlobals?.() ?? {};
212
- const offline = Boolean(globals.offline);
213
- const config = resolveConfig(await loadConfig());
214
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
215
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
216
- try {
217
- await syncBeforeTodoCommand(db, offline, timeoutMs);
218
- const keyword = query?.trim() ?? '';
219
- const taskStatus = parseTaskStatus(options.status);
220
- const taskTagFilter = typeof options.tags === 'string'
221
- ? await parseTagFilterInput(options.tags)
222
- : [];
223
- const taskDateRange = parseTaskDateRange({
224
- from: options.from,
225
- to: options.to,
226
- days: options.days,
227
- });
228
- const limit = parsePositiveIntegerOption(options.limit, '--limit', DEFAULT_TASK_LIMIT, MAX_TASK_LIMIT);
229
- const scanLimit = parsePositiveIntegerOption(options.scanLimit, '--scan-limit', DEFAULT_NOTE_SCAN_LIMIT, MAX_NOTE_SCAN_LIMIT);
230
- const result = await searchTodoTasks(db, keyword, {
231
- includeDeleted: Boolean(options.includeDeleted),
232
- taskStatus,
233
- taskTagFilter,
234
- taskDateRange,
235
- limit,
236
- scanLimit,
237
- });
238
- printYaml(result);
239
- }
240
- finally {
241
- await db.close().catch(() => undefined);
242
- }
201
+ async function withTodoDatabase(globals, run) {
202
+ const rawConfig = await loadConfig();
203
+ const config = resolveConfig(rawConfig);
204
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
205
+ const offline = Boolean(globals.offline);
206
+ const { db } = await connectPowerSync({ config, offline, timeoutMs });
207
+ try {
208
+ await run({
209
+ db,
210
+ rawConfig,
211
+ config,
212
+ offline,
213
+ timeoutMs,
214
+ });
215
+ }
216
+ finally {
217
+ await db.close().catch(() => undefined);
218
+ }
219
+ }
220
+ async function runTodoSearch({ args, globals, options }) {
221
+ const searchOptions = options;
222
+ await withTodoDatabase(globals, async ({ db, offline, timeoutMs }) => {
223
+ await syncBeforeTodoCommand(db, offline, timeoutMs);
224
+ const keyword = args.query?.trim() ?? '';
225
+ const taskStatus = parseTaskStatus(searchOptions.status);
226
+ const taskTagFilter = typeof searchOptions.tags === 'string'
227
+ ? await parseTagFilterInput(searchOptions.tags)
228
+ : [];
229
+ const taskDateRange = parseTaskDateRange({
230
+ from: searchOptions.from,
231
+ to: searchOptions.to,
232
+ days: searchOptions.days,
233
+ });
234
+ const limit = parsePositiveIntegerOption(searchOptions.limit, '--limit', DEFAULT_TASK_LIMIT, MAX_TASK_LIMIT);
235
+ const scanLimit = parsePositiveIntegerOption(searchOptions.scanLimit, '--scan-limit', DEFAULT_NOTE_SCAN_LIMIT, MAX_NOTE_SCAN_LIMIT);
236
+ const result = await searchTodoTasks(db, keyword, {
237
+ includeDeleted: Boolean(searchOptions.includeDeleted),
238
+ taskStatus,
239
+ taskTagFilter,
240
+ taskDateRange,
241
+ limit,
242
+ scanLimit,
243
+ });
244
+ printSuccess(result);
243
245
  });
244
- todo
245
- .command('append')
246
- .description('Append one or more todo items to a note')
247
- .argument('[task]', 'single task text')
248
- .option('--task <text>', 'task text (repeatable)', collectStringOptionValue, [])
249
- .option('--tasks <string|@file>', 'task list (JSON array or comma/newline-separated)')
250
- .option('--note-id <id>', 'target note id; defaults to latest note with non-empty image_detail')
251
- .action(async (taskArg, options, command) => {
252
- const globals = command.optsWithGlobals?.() ?? {};
253
- const offline = Boolean(globals.offline);
254
- const config = resolveConfig(await loadConfig());
255
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
256
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
257
- try {
258
- await syncBeforeTodoCommand(db, offline, timeoutMs);
259
- const taskTexts = await parseTaskInput({
260
- positionalTask: taskArg,
261
- tasksOption: typeof options.tasks === 'string' ? options.tasks : undefined,
262
- taskOptionList: Array.isArray(options.task) ? options.task : [],
263
- });
264
- const result = await appendTodoTasks(db, {
265
- noteId: typeof options.noteId === 'string' ? options.noteId : undefined,
246
+ }
247
+ async function runTodoAppend({ args, globals, options }) {
248
+ const appendOptions = options;
249
+ await withTodoDatabase(globals, async ({ db, config, offline, timeoutMs }) => {
250
+ await syncBeforeTodoCommand(db, offline, timeoutMs);
251
+ const taskTexts = await parseTaskInput({
252
+ positionalTask: args.task,
253
+ tasksOption: typeof appendOptions.tasks === 'string' ? appendOptions.tasks : undefined,
254
+ taskOptionList: Array.isArray(appendOptions.task) ? appendOptions.task : [],
255
+ });
256
+ if (Boolean(appendOptions.dryRun)) {
257
+ const preview = await buildTodoAppendDryRunPreview({
258
+ db,
259
+ config,
260
+ offline,
261
+ noteId: typeof appendOptions.noteId === 'string' ? appendOptions.noteId : undefined,
266
262
  taskTexts,
267
263
  });
268
- printYaml({ ok: true, ...result });
269
- }
270
- finally {
271
- await db.close().catch(() => undefined);
264
+ printDryRunPreview(preview);
265
+ return;
272
266
  }
267
+ const result = await appendTodoTasks(db, {
268
+ noteId: typeof appendOptions.noteId === 'string' ? appendOptions.noteId : undefined,
269
+ taskTexts,
270
+ });
271
+ printSuccess({ ok: true, ...result });
273
272
  });
274
- todo
275
- .command('create')
276
- .description('Create a new note containing one or more todo items')
277
- .argument('[task]', 'single task text')
278
- .option('--task <text>', 'task text (repeatable)', collectStringOptionValue, [])
279
- .option('--tasks <string|@file>', 'task list (JSON array or comma/newline-separated)')
280
- .option('--title <string>', 'optional note title; defaults to an auto-generated summary')
281
- .action(async (taskArg, options, command) => {
282
- const globals = command.optsWithGlobals?.() ?? {};
283
- const offline = Boolean(globals.offline);
284
- const rawConfig = await loadConfig();
285
- const config = resolveConfig(rawConfig);
286
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
287
- const identity = await resolveAuthIdentity(rawConfig, config);
288
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
289
- try {
290
- await syncBeforeTodoCommand(db, offline, timeoutMs);
291
- const taskTexts = await parseTaskInput({
292
- positionalTask: taskArg,
293
- tasksOption: typeof options.tasks === 'string' ? options.tasks : undefined,
294
- taskOptionList: Array.isArray(options.task) ? options.task : [],
295
- });
296
- const result = await createTodoNote(db, {
273
+ }
274
+ async function runTodoCreate({ args, globals, options }) {
275
+ const createOptions = options;
276
+ await withTodoDatabase(globals, async ({ db, rawConfig, config, offline, timeoutMs }) => {
277
+ await syncBeforeTodoCommand(db, offline, timeoutMs);
278
+ const taskTexts = await parseTaskInput({
279
+ positionalTask: args.task,
280
+ tasksOption: typeof createOptions.tasks === 'string' ? createOptions.tasks : undefined,
281
+ taskOptionList: Array.isArray(createOptions.task) ? createOptions.task : [],
282
+ });
283
+ if (Boolean(createOptions.dryRun)) {
284
+ const preview = await buildTodoCreateDryRunPreview({
285
+ config,
286
+ offline,
287
+ title: typeof createOptions.title === 'string' ? createOptions.title : undefined,
297
288
  taskTexts,
298
- title: typeof options.title === 'string' ? options.title : undefined,
299
- userId: identity.userId,
300
289
  });
301
- if (!offline && !config.powersync.uploadBaseUrl) {
302
- console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
303
- }
304
- if (!offline) {
305
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
306
- }
307
- printYaml({ ok: true, ...result });
290
+ printDryRunPreview(preview);
291
+ return;
308
292
  }
309
- finally {
310
- await db.close().catch(() => undefined);
293
+ const identity = await resolveAuthIdentity(rawConfig, config);
294
+ const result = await createTodoNote(db, {
295
+ taskTexts,
296
+ title: typeof createOptions.title === 'string' ? createOptions.title : undefined,
297
+ userId: identity.userId,
298
+ });
299
+ if (!offline && !config.powersync.uploadBaseUrl) {
300
+ printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
311
301
  }
302
+ if (!offline) {
303
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
304
+ }
305
+ printSuccess({ ok: true, ...result });
312
306
  });
313
- todo
314
- .command('update')
315
- .description('Update checked status for a todo task by taskId')
316
- .argument('<taskId>', 'task id')
317
- .requiredOption('--status <status>', 'target status: completed|uncompleted|done|undone|true|false|1|0')
318
- .action(async (taskId, options, command) => {
319
- const globals = command.optsWithGlobals?.() ?? {};
320
- const offline = Boolean(globals.offline);
321
- const config = resolveConfig(await loadConfig());
322
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
323
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
324
- try {
325
- await syncBeforeTodoCommand(db, offline, timeoutMs);
326
- const checked = parseTaskCheckedStatus(options.status);
327
- const result = await updateTodoTaskStatus(db, {
307
+ }
308
+ async function runTodoUpdate({ args, globals, options }) {
309
+ const updateOptions = options;
310
+ await withTodoDatabase(globals, async ({ db, config, offline, timeoutMs }) => {
311
+ await syncBeforeTodoCommand(db, offline, timeoutMs);
312
+ const taskId = args.taskId;
313
+ if (!taskId) {
314
+ throw new DinoxError('taskId is required');
315
+ }
316
+ const checked = parseTaskCheckedStatus(updateOptions.status);
317
+ if (Boolean(updateOptions.dryRun)) {
318
+ const preview = await buildTodoUpdateDryRunPreview({
319
+ db,
320
+ config,
321
+ offline,
328
322
  taskId,
329
323
  checked,
330
324
  });
331
- printYaml({ ok: true, ...result });
325
+ printDryRunPreview(preview);
326
+ return;
332
327
  }
333
- finally {
334
- await db.close().catch(() => undefined);
335
- }
336
- });
337
- todo.action((_options, command) => {
338
- command.outputHelp();
328
+ const result = await updateTodoTaskStatus(db, {
329
+ taskId,
330
+ checked,
331
+ });
332
+ printSuccess({ ok: true, ...result });
339
333
  });
340
334
  }
335
+ const todoSearchCommand = {
336
+ name: 'search',
337
+ description: 'Search todo tasks extracted from note content',
338
+ arguments: [
339
+ {
340
+ syntax: '[query]',
341
+ description: 'Task text query; may be omitted for filter-only search',
342
+ },
343
+ ],
344
+ options: [
345
+ { flags: '--status <status>', description: 'Task status: all | completed | uncompleted', defaultValue: 'all' },
346
+ { flags: '--tags <string|@file>', description: 'Task tags (JSON array or comma/newline-separated)' },
347
+ { flags: '--from <date>', description: 'Task date range start (YYYY-MM-DD or ISO datetime)' },
348
+ { flags: '--to <date>', description: 'Task date range end (YYYY-MM-DD or ISO datetime)' },
349
+ { flags: '--days <n>', description: 'Recent N days; mutually exclusive with --from/--to' },
350
+ { flags: '--limit <n>', description: 'Maximum returned tasks' },
351
+ { flags: '--scan-limit <n>', description: 'Maximum scanned note rows' },
352
+ { flags: '--include-deleted', description: 'Include soft-deleted notes' },
353
+ ],
354
+ schema: {
355
+ path: 'todo.search',
356
+ risk: 'read',
357
+ auth: 'required',
358
+ globalOptions: TODO_GLOBAL_OPTIONS,
359
+ output: {
360
+ type: 'structured',
361
+ summary: 'Todo search payload with metadata and task list',
362
+ fields: ['tasks.task_id', 'tasks.task_key', 'tasks.note_id', 'tasks.note_title', 'tasks.status', 'tasks.depth', 'tasks.parent_id', 'tasks.tags'],
363
+ metaFields: ['meta.query', 'meta.returned_count', 'meta.limit', 'meta.task_status', 'meta.task_tag_filter', 'meta.task_date_range', 'meta.scanned_note_count'],
364
+ },
365
+ mcp: {
366
+ toolName: 'dinox_search_todos',
367
+ description: 'Search todo tasks',
368
+ properties: [
369
+ { name: 'query', type: 'string', description: 'Task text query' },
370
+ { name: 'status', type: 'string', description: 'Task status', enum: ['all', 'completed', 'uncompleted'] },
371
+ { name: 'tags', type: 'string[]', description: 'Task tags' },
372
+ { name: 'limit', type: 'number', description: 'Max results (default: 50)' },
373
+ ],
374
+ },
375
+ examples: ['dino todo search "报销" --status uncompleted --format json', 'dino todo search --days 7 --format json'],
376
+ },
377
+ action: runTodoSearch,
378
+ };
379
+ const todoAppendCommand = {
380
+ name: 'append',
381
+ description: 'Append one or more tasks to an existing note',
382
+ arguments: [
383
+ {
384
+ syntax: '[task]',
385
+ description: 'Single task text',
386
+ },
387
+ ],
388
+ options: [
389
+ {
390
+ flags: '--task <text>',
391
+ description: 'Repeatable task text',
392
+ parser: collectStringOptionValue,
393
+ defaultValue: [],
394
+ },
395
+ { flags: '--tasks <string|@file>', description: 'Task list (JSON array or comma/newline-separated)' },
396
+ { flags: '--note-id <id>', description: 'Target note id; omitted means latest eligible note' },
397
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
398
+ ],
399
+ schema: {
400
+ path: 'todo.append',
401
+ risk: 'write',
402
+ auth: 'required',
403
+ globalOptions: TODO_GLOBAL_OPTIONS,
404
+ output: {
405
+ type: 'structured',
406
+ summary: 'Append result with target note and appended task ids',
407
+ fields: ['noteId', 'appendedCount', 'taskIds'],
408
+ },
409
+ examples: ['dino todo append --note-id <id> --task "补交报销单" --format json'],
410
+ },
411
+ action: runTodoAppend,
412
+ };
413
+ const todoCreateCommand = {
414
+ name: 'create',
415
+ description: 'Create a new note containing one or more todo items',
416
+ arguments: [
417
+ {
418
+ syntax: '[task]',
419
+ description: 'Single task text',
420
+ },
421
+ ],
422
+ options: [
423
+ {
424
+ flags: '--task <text>',
425
+ description: 'Repeatable task text',
426
+ parser: collectStringOptionValue,
427
+ defaultValue: [],
428
+ },
429
+ { flags: '--tasks <string|@file>', description: 'Task list (JSON array or comma/newline-separated)' },
430
+ { flags: '--title <string>', description: 'Optional note title' },
431
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
432
+ ],
433
+ schema: {
434
+ path: 'todo.create',
435
+ risk: 'write',
436
+ auth: 'required',
437
+ globalOptions: TODO_GLOBAL_OPTIONS,
438
+ output: {
439
+ type: 'structured',
440
+ summary: 'Creation result with new note id and task ids',
441
+ fields: ['noteId', 'taskIds', 'title'],
442
+ },
443
+ examples: ['dino todo create --task "补交报销单" --task "整理发票" --format json'],
444
+ },
445
+ action: runTodoCreate,
446
+ };
447
+ const todoUpdateCommand = {
448
+ name: 'update',
449
+ description: 'Update a todo task checked status by task id',
450
+ arguments: [
451
+ {
452
+ syntax: '<taskId>',
453
+ description: 'Task id',
454
+ },
455
+ ],
456
+ options: [
457
+ {
458
+ flags: '--status <status>',
459
+ description: 'Target status: completed|uncompleted|done|undone|true|false|1|0',
460
+ required: true,
461
+ },
462
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
463
+ ],
464
+ schema: {
465
+ path: 'todo.update',
466
+ risk: 'write',
467
+ auth: 'required',
468
+ globalOptions: TODO_GLOBAL_OPTIONS,
469
+ output: {
470
+ type: 'structured',
471
+ summary: 'Mutation result with updated status and target note id',
472
+ fields: ['taskId', 'checked', 'updated', 'noteId'],
473
+ },
474
+ examples: ['dino todo update <taskId> --status completed --format json'],
475
+ },
476
+ action: runTodoUpdate,
477
+ };
478
+ export const todoCommandGroup = {
479
+ name: 'todo',
480
+ description: 'Manage todo tasks',
481
+ commands: [
482
+ todoSearchCommand,
483
+ todoAppendCommand,
484
+ todoCreateCommand,
485
+ todoUpdateCommand,
486
+ ],
487
+ };
488
+ export function registerTodoCommands(program) {
489
+ registerCommandGroup(program, todoCommandGroup);
490
+ }
@@ -79,7 +79,18 @@ export type SearchedTodoResponse = {
79
79
  };
80
80
  export type { TaskDateRangeFilter, TaskStatusFilter } from './taskModel.js';
81
81
  export { normalizeTaskTexts } from './contentModel.js';
82
+ type TodoMutableNoteRow = {
83
+ id: string | null;
84
+ title: string | null;
85
+ content_json: string | null;
86
+ content_md: string | null;
87
+ is_del: number | null;
88
+ created_at?: string | null;
89
+ };
82
90
  export declare function searchTodoTasks(db: AbstractPowerSyncDatabase, query: string, options: SearchTodoOptions): Promise<SearchedTodoResponse>;
83
91
  export declare function appendTodoTasks(db: AbstractPowerSyncDatabase, options: AppendTodoTasksOptions): Promise<AppendTodoTasksResult>;
84
92
  export declare function createTodoNote(db: AbstractPowerSyncDatabase, options: CreateTodoNoteOptions): Promise<CreateTodoNoteResult>;
85
93
  export declare function updateTodoTaskStatus(db: AbstractPowerSyncDatabase, options: UpdateTodoTaskStatusOptions): Promise<UpdateTodoTaskStatusResult>;
94
+ export declare function getTodoAppendTargetById(db: AbstractPowerSyncDatabase, noteId: string): Promise<TodoMutableNoteRow | null>;
95
+ export declare function getLatestTodoAppendTarget(db: AbstractPowerSyncDatabase): Promise<TodoMutableNoteRow | null>;
96
+ export declare function loadTodoTaskCandidatesByTaskId(db: AbstractPowerSyncDatabase, taskId: string): Promise<TodoMutableNoteRow[]>;
@@ -133,20 +133,48 @@ export async function updateTodoTaskStatus(db, options) {
133
133
  throw new DinoxError('taskId is required');
134
134
  }
135
135
  const candidates = await loadTodoTaskCandidatesByTaskId(db, taskId);
136
- const matches = candidates.flatMap((candidate) => {
136
+ const matches = [];
137
+ const skippedCandidates = [];
138
+ for (const candidate of candidates) {
137
139
  try {
138
140
  const doc = parseTiptapDocFromDb(candidate.content_json);
139
141
  const applied = applyTaskCheckedStatus(doc, taskId, options.checked);
140
142
  if (applied.matchedIndices.length === 0) {
141
- return [];
143
+ continue;
142
144
  }
143
- return [{ candidate, applied }];
145
+ matches.push({ candidate, applied });
144
146
  }
145
- catch {
146
- return [];
147
+ catch (error) {
148
+ skippedCandidates.push({
149
+ noteId: candidate.id,
150
+ errorMessage: error instanceof Error ? error.message : String(error),
151
+ });
147
152
  }
148
- });
153
+ }
149
154
  if (matches.length === 0) {
155
+ if (skippedCandidates.length > 0) {
156
+ const failedNoteIds = Array.from(new Set(skippedCandidates
157
+ .map((entry) => (entry.noteId ?? '').trim())
158
+ .filter((value) => value.length > 0)));
159
+ const sampleFailedIds = failedNoteIds.slice(0, 3);
160
+ const suffix = sampleFailedIds.length > 0
161
+ ? ` Failed note ids: ${sampleFailedIds.join(', ')}${failedNoteIds.length > sampleFailedIds.length ? ', ...' : ''}.`
162
+ : '';
163
+ throw new DinoxError(`Task not found: ${taskId}. Skipped ${skippedCandidates.length} candidate note(s) with invalid content_json.${suffix}`, {
164
+ code: 'TODO_TASK_NOT_FOUND',
165
+ suggestion: sampleFailedIds.length > 0
166
+ ? `Try 'dino note detail ${sampleFailedIds[0]}' to inspect/repair note content.`
167
+ : undefined,
168
+ details: {
169
+ taskId,
170
+ candidatesCount: candidates.length,
171
+ skippedCandidatesCount: skippedCandidates.length,
172
+ failedNoteIds,
173
+ skippedCandidates: skippedCandidates.slice(0, 5),
174
+ },
175
+ recoverable: true,
176
+ });
177
+ }
150
178
  throw new DinoxError(`Task not found: ${taskId}`);
151
179
  }
152
180
  if (matches.length > 1) {
@@ -222,7 +250,7 @@ async function loadTodoNoteRows(db, includeDeleted, scanLimit) {
222
250
  LIMIT ?
223
251
  `, [scanLimit]);
224
252
  }
225
- async function getTodoAppendTargetById(db, noteId) {
253
+ export async function getTodoAppendTargetById(db, noteId) {
226
254
  return db.getOptional(`
227
255
  SELECT
228
256
  n.id,
@@ -234,7 +262,7 @@ async function getTodoAppendTargetById(db, noteId) {
234
262
  WHERE n.id = ?
235
263
  `, [noteId]);
236
264
  }
237
- async function getLatestTodoAppendTarget(db) {
265
+ export async function getLatestTodoAppendTarget(db) {
238
266
  return db.getOptional(`
239
267
  SELECT
240
268
  n.id,
@@ -251,7 +279,7 @@ async function getLatestTodoAppendTarget(db) {
251
279
  LIMIT 1
252
280
  `, []);
253
281
  }
254
- async function loadTodoTaskCandidatesByTaskId(db, taskId) {
282
+ export async function loadTodoTaskCandidatesByTaskId(db, taskId) {
255
283
  const escapedTaskId = escapeLikePattern(taskId);
256
284
  return db.getAll(`
257
285
  SELECT
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandDescriptor } from './descriptor/types.js';
2
3
  type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
3
4
  type LogFn = (...parts: unknown[]) => void;
4
5
  type UpdateCommandDeps = {
@@ -7,5 +8,7 @@ type UpdateCommandDeps = {
7
8
  getVersion: () => string;
8
9
  log: LogFn;
9
10
  };
11
+ export declare function createUpdateCommandDescriptor(overrides?: Partial<UpdateCommandDeps>): CommandDescriptor;
12
+ export declare const updateCommandDescriptor: CommandDescriptor;
10
13
  export declare function registerUpdateCommand(program: Command, overrides?: Partial<UpdateCommandDeps>): void;
11
14
  export {};
@@ -1,7 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { registerStandaloneCommand } from './descriptor/register.js';
2
3
  import { SKILLS_REPO_URL } from '../constants/links.js';
4
+ import { printHumanReadableLine } from '../utils/output.js';
3
5
  import { getPackageVersion } from '../utils/version.js';
4
6
  const PACKAGE_NAME = '@dinoxx/dinox-cli';
7
+ function logHumanReadable(...parts) {
8
+ printHumanReadableLine(parts.map((part) => String(part)).join(' '));
9
+ }
5
10
  function detectPackageManager() {
6
11
  const scriptPath = process.argv[1] ?? '';
7
12
  if (scriptPath.includes('/.bun/'))
@@ -38,7 +43,7 @@ function resolveDeps(overrides) {
38
43
  detectPackageManager,
39
44
  runUpdate,
40
45
  getVersion: getPackageVersion,
41
- log: console.log,
46
+ log: logHumanReadable,
42
47
  ...overrides,
43
48
  };
44
49
  }
@@ -46,17 +51,34 @@ function printSkillsReminder(log) {
46
51
  log(`skills repo: ${SKILLS_REPO_URL}`);
47
52
  log('ai reminder: review and update local Dinox skills after CLI updates (especially dino-dinox and dino-update-cli).');
48
53
  }
49
- export function registerUpdateCommand(program, overrides = {}) {
54
+ export function createUpdateCommandDescriptor(overrides = {}) {
50
55
  const deps = resolveDeps(overrides);
51
- program
52
- .command('update')
53
- .description(`Update ${PACKAGE_NAME} to the latest version`)
54
- .action(async () => {
55
- const pm = deps.detectPackageManager();
56
- deps.log(`current version: ${deps.getVersion()}`);
57
- deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
58
- await deps.runUpdate(pm);
59
- deps.log('done');
60
- printSkillsReminder(deps.log);
61
- });
56
+ return {
57
+ name: 'update',
58
+ description: `Update ${PACKAGE_NAME} to the latest version`,
59
+ schema: {
60
+ path: 'update',
61
+ risk: 'write',
62
+ auth: 'none',
63
+ globalOptions: [],
64
+ output: {
65
+ type: 'structured',
66
+ summary: 'Human-readable updater progress and bundled skills reminder',
67
+ fields: ['current version log', 'package manager log', 'skills repo reminder'],
68
+ },
69
+ examples: ['dino update'],
70
+ },
71
+ action: async () => {
72
+ const pm = deps.detectPackageManager();
73
+ deps.log(`current version: ${deps.getVersion()}`);
74
+ deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
75
+ await deps.runUpdate(pm);
76
+ deps.log('done');
77
+ printSkillsReminder(deps.log);
78
+ },
79
+ };
80
+ }
81
+ export const updateCommandDescriptor = createUpdateCommandDescriptor();
82
+ export function registerUpdateCommand(program, overrides = {}) {
83
+ registerStandaloneCommand(program, createUpdateCommandDescriptor(overrides));
62
84
  }