@dinoxx/dinox-cli 2.13.1 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/ai/ragxClient.d.ts +15 -0
  2. package/dist/ai/ragxClient.js +71 -0
  3. package/dist/cli.js +31 -4
  4. package/dist/commands/auth/index.d.ts +2 -1
  5. package/dist/commands/auth/index.js +18 -1
  6. package/dist/commands/batch.d.ts +35 -0
  7. package/dist/commands/batch.js +202 -0
  8. package/dist/commands/boxes/index.js +21 -21
  9. package/dist/commands/doctor.d.ts +4 -1
  10. package/dist/commands/doctor.js +20 -1
  11. package/dist/commands/graph/index.js +2 -1
  12. package/dist/commands/mcp.d.ts +6 -0
  13. package/dist/commands/mcp.js +47 -0
  14. package/dist/commands/notes/actions.js +18 -2
  15. package/dist/commands/notes/contentPatch.js +2 -1
  16. package/dist/commands/notes/descriptors.js +111 -5
  17. package/dist/commands/notes/detailContent.d.ts +19 -0
  18. package/dist/commands/notes/detailContent.js +62 -0
  19. package/dist/commands/notes/export.js +7 -2
  20. package/dist/commands/notes/repo/search.d.ts +2 -1
  21. package/dist/commands/notes/repo/search.js +1 -1
  22. package/dist/commands/notes/repo/semantic.d.ts +14 -0
  23. package/dist/commands/notes/repo/semantic.js +31 -0
  24. package/dist/commands/notes/searchResponse.d.ts +3 -0
  25. package/dist/commands/notes/searchResponse.js +1 -0
  26. package/dist/commands/notes/semantic.d.ts +116 -0
  27. package/dist/commands/notes/semantic.js +332 -0
  28. package/dist/commands/prompt/index.js +7 -5
  29. package/dist/commands/runtime.d.ts +8 -0
  30. package/dist/commands/runtime.js +49 -1
  31. package/dist/commands/savedViews/index.js +12 -3
  32. package/dist/commands/schema/globalOptions.d.ts +7 -0
  33. package/dist/commands/schema/globalOptions.js +7 -0
  34. package/dist/commands/schema/renderSkills.js +6 -2
  35. package/dist/commands/schema/spec.js +20 -1
  36. package/dist/commands/storage/index.js +7 -5
  37. package/dist/commands/storage/repo/operations.js +3 -1
  38. package/dist/commands/sync.d.ts +2 -1
  39. package/dist/commands/sync.js +27 -8
  40. package/dist/commands/tags/index.js +21 -21
  41. package/dist/commands/todo/index.js +22 -2
  42. package/dist/commands/todo/repo.d.ts +5 -0
  43. package/dist/commands/todo/repo.js +7 -2
  44. package/dist/commands/writeReceipt.d.ts +13 -0
  45. package/dist/commands/writeReceipt.js +12 -0
  46. package/dist/config/serviceEndpoints.d.ts +2 -0
  47. package/dist/config/serviceEndpoints.js +2 -0
  48. package/dist/daemon/client.d.ts +2 -0
  49. package/dist/daemon/client.js +53 -3
  50. package/dist/daemon/connectivity.d.ts +20 -0
  51. package/dist/daemon/connectivity.js +42 -0
  52. package/dist/daemon/ensure.js +48 -12
  53. package/dist/daemon/forward.d.ts +37 -0
  54. package/dist/daemon/forward.js +155 -0
  55. package/dist/daemon/fresh.d.ts +4 -0
  56. package/dist/daemon/fresh.js +10 -0
  57. package/dist/daemon/index.d.ts +11 -6
  58. package/dist/daemon/index.js +90 -25
  59. package/dist/daemon/launch.d.ts +13 -1
  60. package/dist/daemon/launch.js +31 -2
  61. package/dist/daemon/lifecycle.d.ts +3 -1
  62. package/dist/daemon/lifecycle.js +27 -3
  63. package/dist/daemon/pid.d.ts +8 -0
  64. package/dist/daemon/pid.js +20 -0
  65. package/dist/daemon/route.js +2 -1
  66. package/dist/daemon/server.d.ts +45 -1
  67. package/dist/daemon/server.js +192 -4
  68. package/dist/daemon/supervise.d.ts +6 -6
  69. package/dist/daemon/supervise.js +12 -10
  70. package/dist/daemon/supervisor.d.ts +21 -5
  71. package/dist/daemon/supervisor.js +125 -6
  72. package/dist/daemon/types.d.ts +22 -0
  73. package/dist/dinox.js +43 -64
  74. package/dist/gateway/deviceHost.js +2 -1
  75. package/dist/gateway/mcpCommandTools.d.ts +24 -5
  76. package/dist/gateway/mcpCommandTools.js +70 -29
  77. package/dist/gateway/mcpEnv.d.ts +2 -0
  78. package/dist/gateway/mcpEnv.js +2 -0
  79. package/dist/gateway/mcpGroups.d.ts +3 -0
  80. package/dist/gateway/mcpGroups.js +4 -0
  81. package/dist/gateway/mcpServer.d.ts +10 -1
  82. package/dist/gateway/mcpServer.js +25 -5
  83. package/dist/powersync/runtime.d.ts +2 -0
  84. package/dist/powersync/runtime.js +3 -1
  85. package/dist/powersync/syncPass.js +13 -1
  86. package/dist/powersync/tokenIndex.d.ts +2 -0
  87. package/dist/powersync/tokenIndex.js +1 -0
  88. package/dist/requests/execute.d.ts +11 -0
  89. package/dist/requests/execute.js +26 -0
  90. package/dist/requests/store.d.ts +10 -0
  91. package/dist/requests/store.js +190 -0
  92. package/dist/runCli.d.ts +2 -0
  93. package/dist/runCli.js +64 -0
  94. package/dist/skills/commandRefs.js +2 -10
  95. package/dist/utils/argValue.js +2 -1
  96. package/dist/utils/cursor.d.ts +9 -0
  97. package/dist/utils/cursor.js +61 -0
  98. package/dist/utils/defaultFormat.d.ts +8 -0
  99. package/dist/utils/defaultFormat.js +38 -0
  100. package/dist/utils/errorCodes.d.ts +24 -0
  101. package/dist/utils/errorCodes.js +6 -0
  102. package/dist/utils/output.d.ts +4 -0
  103. package/dist/utils/output.js +72 -10
  104. package/dist/utils/progress.d.ts +25 -0
  105. package/dist/utils/progress.js +59 -0
  106. package/dist/utils/requestContext.d.ts +32 -0
  107. package/dist/utils/requestContext.js +13 -0
  108. package/dist/utils/select.d.ts +20 -0
  109. package/dist/utils/select.js +92 -0
  110. package/dist/utils/usageError.d.ts +37 -0
  111. package/dist/utils/usageError.js +127 -0
  112. package/package.json +2 -1
@@ -1,3 +1,5 @@
1
+ import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
2
+ import { completeWrite, WRITE_RECEIPT_FIELDS } from '../writeReceipt.js';
1
3
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
4
  import { registerCommandGroup } from '../descriptor/register.js';
3
5
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
@@ -5,7 +7,7 @@ import { buildTagAddDryRunPreview, buildTagMutationDryRunPreview } from '../writ
5
7
  import { printDryRunPreview } from '../../utils/dryRun.js';
6
8
  import { DinoxError } from '../../utils/errors.js';
7
9
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
8
- import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
10
+ import { commandStale, printStaleCacheWarning, syncBeforeCommand, resolveWriteDurabilityForRuntime, withCommandRuntime, } from '../runtime.js';
9
11
  import { assertWriteConfirmationProvided, parseExpectedCount } from '../writeConfirmation.js';
10
12
  import { addTag, inspectAddTag, listTags } from './repo.js';
11
13
  import { getTagStats, getTagTree, inspectTagCleanup, mergeTag, moveTag, renameTag, suggestTagMerges, } from './operations.js';
@@ -56,6 +58,7 @@ async function runTagAdd(context) {
56
58
  }
57
59
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
58
60
  await withCommandRuntime(context, async (runtime) => {
61
+ const durability = resolveWriteDurabilityForRuntime(runtime, context.options.durability);
59
62
  const identity = await resolveAuthIdentity(runtime.config);
60
63
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
61
64
  const emoji = typeof context.options.emoji === 'string' ? context.options.emoji : undefined;
@@ -76,8 +79,7 @@ async function runTagAdd(context) {
76
79
  userId: identity.userId,
77
80
  emoji,
78
81
  });
79
- await waitForMutationUpload(runtime);
80
- const payload = { ok: true, stale: commandStale(runtime), ...result };
82
+ const payload = { ok: true, ...(await completeWrite(runtime, durability)), ...result };
81
83
  if (structuredOutput) {
82
84
  printSuccess(payload);
83
85
  return;
@@ -88,9 +90,10 @@ async function runTagAdd(context) {
88
90
  }
89
91
  async function withTagWriteRuntime(context, run) {
90
92
  return withCommandRuntime(context, async (runtime) => {
93
+ const durability = resolveWriteDurabilityForRuntime(runtime, context.options.durability);
91
94
  const identity = await resolveAuthIdentity(runtime.config);
92
95
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
93
- return run({ runtime, userId: identity.userId });
96
+ return run({ runtime, userId: identity.userId, durability });
94
97
  });
95
98
  }
96
99
  function requireArg(value, message) {
@@ -103,7 +106,7 @@ function requireArg(value, message) {
103
106
  async function runTagRename(context) {
104
107
  const path = requireArg(context.args.path, 'Tag path is required');
105
108
  const newName = requireArg(context.args.newName, 'New tag name is required');
106
- await withTagWriteRuntime(context, async ({ runtime, userId }) => {
109
+ await withTagWriteRuntime(context, async ({ runtime, userId, durability }) => {
107
110
  const result = await renameTag(runtime.db, { path, newName, userId }, { dryRun: Boolean(context.options.dryRun) });
108
111
  if (Boolean(context.options.dryRun)) {
109
112
  printDryRunPreview(buildTagMutationDryRunPreview({
@@ -118,8 +121,7 @@ async function runTagRename(context) {
118
121
  }), runtime);
119
122
  return;
120
123
  }
121
- await waitForMutationUpload(runtime);
122
- printSuccess({ ok: true, stale: commandStale(runtime), ...result });
124
+ printSuccess({ ok: true, ...(await completeWrite(runtime, durability)), ...result });
123
125
  });
124
126
  }
125
127
  async function runTagMove(context) {
@@ -128,7 +130,7 @@ async function runTagMove(context) {
128
130
  if (!to) {
129
131
  throw new DinoxError('--to is required');
130
132
  }
131
- await withTagWriteRuntime(context, async ({ runtime, userId }) => {
133
+ await withTagWriteRuntime(context, async ({ runtime, userId, durability }) => {
132
134
  const result = await moveTag(runtime.db, { path, to, userId }, { dryRun: Boolean(context.options.dryRun) });
133
135
  if (Boolean(context.options.dryRun)) {
134
136
  printDryRunPreview(buildTagMutationDryRunPreview({
@@ -143,8 +145,7 @@ async function runTagMove(context) {
143
145
  }), runtime);
144
146
  return;
145
147
  }
146
- await waitForMutationUpload(runtime);
147
- printSuccess({ ok: true, stale: commandStale(runtime), ...result });
148
+ printSuccess({ ok: true, ...(await completeWrite(runtime, durability)), ...result });
148
149
  });
149
150
  }
150
151
  async function runTagMerge(context) {
@@ -158,7 +159,7 @@ async function runTagMerge(context) {
158
159
  expectedCount,
159
160
  };
160
161
  assertWriteConfirmationProvided('Tag merge', writeOptions);
161
- await withTagWriteRuntime(context, async ({ runtime, userId }) => {
162
+ await withTagWriteRuntime(context, async ({ runtime, userId, durability }) => {
162
163
  const result = await mergeTag(runtime.db, { from, to, userId }, writeOptions);
163
164
  if (dryRun) {
164
165
  printDryRunPreview(buildTagMutationDryRunPreview({
@@ -179,8 +180,7 @@ async function runTagMerge(context) {
179
180
  }), runtime);
180
181
  return;
181
182
  }
182
- await waitForMutationUpload(runtime);
183
- printSuccess({ ok: true, stale: commandStale(runtime), ...result });
183
+ printSuccess({ ok: true, ...(await completeWrite(runtime, durability)), ...result });
184
184
  });
185
185
  }
186
186
  async function runTagSuggest(context) {
@@ -273,7 +273,7 @@ export const tagCommandGroup = {
273
273
  options: [
274
274
  { flags: '--name <string>', description: 'Tag name/path (alternative to positional name)' },
275
275
  { flags: '--emoji <string>', description: 'Tag emoji' },
276
- { flags: '--dry-run', description: 'Preview the write without executing it' },
276
+ WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it' },
277
277
  ],
278
278
  schema: {
279
279
  path: 'tag.add',
@@ -283,7 +283,7 @@ export const tagCommandGroup = {
283
283
  output: {
284
284
  type: 'structured',
285
285
  summary: 'Tag creation acknowledgement',
286
- fields: ['id', 'tag', 'createdNodes', 'restoredNodes', 'stale'],
286
+ fields: ['id', 'tag', 'createdNodes', 'restoredNodes', ...WRITE_RECEIPT_FIELDS],
287
287
  },
288
288
  examples: ['dino tag add "work/projects" --format json --dry-run', 'dino tag add "work/projects" --emoji "🏷️" --format json'],
289
289
  },
@@ -296,7 +296,7 @@ export const tagCommandGroup = {
296
296
  { syntax: '<path>', description: 'Existing tag path' },
297
297
  { syntax: '<new-name>', description: 'New leaf name' },
298
298
  ],
299
- options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
299
+ options: [WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it' }],
300
300
  schema: {
301
301
  path: 'tag.rename',
302
302
  risk: 'write',
@@ -305,7 +305,7 @@ export const tagCommandGroup = {
305
305
  output: {
306
306
  type: 'structured',
307
307
  summary: 'Tag rename result',
308
- fields: ['id', 'fromPath', 'toPath', 'updatedTags', 'updatedNotes', 'stale'],
308
+ fields: ['id', 'fromPath', 'toPath', 'updatedTags', 'updatedNotes', ...WRITE_RECEIPT_FIELDS],
309
309
  },
310
310
  examples: ['dino tag rename "AI/old" "new" --format json --dry-run'],
311
311
  },
@@ -317,7 +317,7 @@ export const tagCommandGroup = {
317
317
  arguments: [{ syntax: '<path>', description: 'Existing tag path' }],
318
318
  options: [
319
319
  { flags: '--to <parent-path>', description: 'Target parent tag path', required: true, type: 'string' },
320
- { flags: '--dry-run', description: 'Preview the write without executing it' },
320
+ WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it' },
321
321
  ],
322
322
  schema: {
323
323
  path: 'tag.move',
@@ -327,7 +327,7 @@ export const tagCommandGroup = {
327
327
  output: {
328
328
  type: 'structured',
329
329
  summary: 'Tag move result',
330
- fields: ['id', 'fromPath', 'toPath', 'updatedTags', 'updatedNotes', 'stale'],
330
+ fields: ['id', 'fromPath', 'toPath', 'updatedTags', 'updatedNotes', ...WRITE_RECEIPT_FIELDS],
331
331
  },
332
332
  examples: ['dino tag move "AI" --to "Technology" --format json --dry-run'],
333
333
  },
@@ -343,7 +343,7 @@ export const tagCommandGroup = {
343
343
  options: [
344
344
  { flags: '--expected-count <n>', description: 'Required for real writes; must equal source tags plus notes affected', type: 'number' },
345
345
  { flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
346
- { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
346
+ WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
347
347
  ],
348
348
  schema: {
349
349
  path: 'tag.merge',
@@ -353,7 +353,7 @@ export const tagCommandGroup = {
353
353
  output: {
354
354
  type: 'structured',
355
355
  summary: 'Tag merge result',
356
- fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'affectedCount', 'stale'],
356
+ fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'affectedCount', ...WRITE_RECEIPT_FIELDS],
357
357
  },
358
358
  examples: [
359
359
  'dino tag merge "ai" "AI" --format json --dry-run',
@@ -1,4 +1,5 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
+ import { nextCursor, resolveCursorOffset } from '../../utils/cursor.js';
2
3
  import { registerCommandGroup } from '../descriptor/register.js';
3
4
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
4
5
  import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
@@ -147,12 +148,25 @@ async function runTodoSearch({ args, globals, options }) {
147
148
  });
148
149
  const limit = parsePositiveIntegerOption(searchOptions.limit, '--limit', DEFAULT_TASK_LIMIT, MAX_TASK_LIMIT);
149
150
  const scanLimit = parsePositiveIntegerOption(searchOptions.scanLimit, '--scan-limit', DEFAULT_NOTE_SCAN_LIMIT, MAX_NOTE_SCAN_LIMIT);
151
+ // Raw option values, not the resolved range: `--days` must not change the hash between pages.
152
+ const cursorQuery = {
153
+ keyword,
154
+ includeDeleted: Boolean(searchOptions.includeDeleted),
155
+ taskStatus,
156
+ taskTagFilter,
157
+ from: searchOptions.from,
158
+ to: searchOptions.to,
159
+ days: searchOptions.days,
160
+ scanLimit,
161
+ };
162
+ const offset = resolveCursorOffset(searchOptions.cursor, 0, 'todo.search', cursorQuery);
150
163
  const wire = toTodoSearchWire(keyword, {
151
164
  includeDeleted: Boolean(searchOptions.includeDeleted),
152
165
  taskStatus,
153
166
  taskTagFilter,
154
167
  taskDateRange,
155
168
  limit,
169
+ offset,
156
170
  scanLimit,
157
171
  });
158
172
  const result = await routeRead(globals, 'todo.search', wire, () => withTodoDatabase(globals, async (runtime) => {
@@ -161,7 +175,12 @@ async function runTodoSearch({ args, globals, options }) {
161
175
  return { ...local, stale: commandStale(runtime) };
162
176
  }));
163
177
  const { stale, ...response } = result;
164
- printSuccess({ stale: stale ?? false, ...response });
178
+ const nextOffset = response.meta.offset + response.meta.returned_count;
179
+ printSuccess({
180
+ stale: stale ?? false,
181
+ ...response,
182
+ meta: { ...response.meta, next_cursor: nextCursor('todo.search', cursorQuery, nextOffset, response.meta.has_more) },
183
+ });
165
184
  }
166
185
  async function runTodoAppend({ args, globals, options }) {
167
186
  const appendOptions = options;
@@ -290,6 +309,7 @@ const todoSearchCommand = {
290
309
  { flags: '--days <n>', description: 'Recent N days; mutually exclusive with --from/--to' },
291
310
  { flags: '--limit <n>', description: 'Maximum returned tasks' },
292
311
  { flags: '--scan-limit <n>', description: 'Maximum scanned note rows' },
312
+ { flags: '--cursor <token>', description: 'Continue from meta.next_cursor of the previous page (same query)' },
293
313
  { flags: '--include-deleted', description: 'Include soft-deleted notes' },
294
314
  ],
295
315
  schema: {
@@ -301,7 +321,7 @@ const todoSearchCommand = {
301
321
  type: 'structured',
302
322
  summary: 'Todo search payload with metadata and task list',
303
323
  fields: ['tasks.task_id', 'tasks.task_key', 'tasks.note_id', 'tasks.note_title', 'tasks.status', 'tasks.depth', 'tasks.parent_id', 'tasks.tags'],
304
- metaFields: ['stale', 'meta.query', 'meta.returned_count', 'meta.total_count', 'meta.total_count_is_lower_bound', 'meta.matched_note_count', 'meta.limit', 'meta.scan_limit', 'meta.scanned_note_count', 'meta.scan_truncated', 'meta.truncated', 'meta.applied_filters.status', 'meta.applied_filters.tags', 'meta.applied_filters.include_deleted', 'meta.applied_filters.date_range'],
324
+ metaFields: ['stale', 'meta.query', 'meta.returned_count', 'meta.total_count', 'meta.total_count_is_lower_bound', 'meta.matched_note_count', 'meta.limit', 'meta.offset', 'meta.has_more', 'meta.next_cursor', 'meta.scan_limit', 'meta.scanned_note_count', 'meta.scan_truncated', 'meta.truncated', 'meta.applied_filters.status', 'meta.applied_filters.tags', 'meta.applied_filters.include_deleted', 'meta.applied_filters.date_range'],
305
325
  },
306
326
  examples: ['dino todo search "报销" --status uncompleted --format json', 'dino todo search --days 7 --format json'],
307
327
  },
@@ -7,6 +7,7 @@ export type SearchTodoOptions = {
7
7
  taskTagFilter: string[];
8
8
  taskDateRange?: TaskDateRangeFilter;
9
9
  limit: number;
10
+ offset?: number;
10
11
  scanLimit: number;
11
12
  };
12
13
  export type AppendTodoTasksOptions = {
@@ -65,6 +66,8 @@ export type SearchedTodoResponse = {
65
66
  total_count: number;
66
67
  matched_note_count: number;
67
68
  limit: number;
69
+ offset: number;
70
+ has_more: boolean;
68
71
  scan_limit: number;
69
72
  scanned_note_count: number;
70
73
  scan_truncated: boolean;
@@ -109,6 +112,8 @@ export type TodoSearchWire = {
109
112
  taskStatus: TaskStatusFilter;
110
113
  taskTagFilter: string[];
111
114
  limit: number;
115
+ /** Index of the first returned task among the matches (from `--cursor`). */
116
+ offset?: number;
112
117
  scanLimit: number;
113
118
  startDate?: string;
114
119
  endDate?: string;
@@ -40,6 +40,7 @@ export function toTodoSearchWire(query, options) {
40
40
  taskStatus: options.taskStatus,
41
41
  taskTagFilter: options.taskTagFilter,
42
42
  limit: options.limit,
43
+ ...(options.offset ? { offset: options.offset } : {}),
43
44
  scanLimit: options.scanLimit,
44
45
  startDate: options.taskDateRange?.startDate?.toISOString(),
45
46
  endDate: options.taskDateRange?.endDate?.toISOString(),
@@ -62,6 +63,7 @@ export async function executeTodoSearch(db, wire) {
62
63
  taskTagFilter: wire.taskTagFilter,
63
64
  taskDateRange,
64
65
  limit: wire.limit,
66
+ offset: wire.offset,
65
67
  scanLimit: wire.scanLimit,
66
68
  });
67
69
  }
@@ -71,7 +73,8 @@ export async function searchTodoTasks(db, query, options) {
71
73
  const noteRows = scannedRows.slice(0, options.scanLimit);
72
74
  const allTasks = extractFlattenedTasks(noteRows);
73
75
  const filteredTasks = filterTasksForTodoList(allTasks, query, options.taskStatus, options.taskTagFilter, options.taskDateRange);
74
- const visibleTasks = filteredTasks.slice(0, options.limit);
76
+ const offset = options.offset ?? 0;
77
+ const visibleTasks = filteredTasks.slice(offset, offset + options.limit);
75
78
  const tasks = visibleTasks.map((task) => ({
76
79
  task_key: buildTaskKey(task.noteId, task.taskId),
77
80
  note_id: task.noteId,
@@ -96,11 +99,13 @@ export async function searchTodoTasks(db, query, options) {
96
99
  total_count: filteredTasks.length,
97
100
  matched_note_count: new Set(filteredTasks.map((task) => task.noteId)).size,
98
101
  limit: options.limit,
102
+ offset,
103
+ has_more: filteredTasks.length > offset + tasks.length,
99
104
  scan_limit: options.scanLimit,
100
105
  scanned_note_count: noteRows.length,
101
106
  scan_truncated: scanTruncated,
102
107
  total_count_is_lower_bound: scanTruncated,
103
- truncated: scanTruncated || filteredTasks.length > tasks.length,
108
+ truncated: scanTruncated || filteredTasks.length > offset + tasks.length,
104
109
  applied_filters: {
105
110
  status: options.taskStatus,
106
111
  tags: [...options.taskTagFilter],
@@ -0,0 +1,13 @@
1
+ import type { WriteDurability } from './writeDurability.js';
2
+ import { type CommandRuntime, type MutationUploadReceipt } from './runtime.js';
3
+ /** Output fields every local-database write reports, as note and todo writes do. */
4
+ export declare const WRITE_RECEIPT_FIELDS: readonly ["durability", "upload_queue_remaining", "stale"];
5
+ export type WriteReceipt = MutationUploadReceipt & {
6
+ stale: boolean;
7
+ };
8
+ /**
9
+ * Waits for the upload `--durability` asks for and describes where the write
10
+ * stands: `durability` local or uploaded, the remaining upload queue, any
11
+ * upload error, and whether the pre-write sync was stale.
12
+ */
13
+ export declare function completeWrite(runtime: CommandRuntime, durability: WriteDurability): Promise<WriteReceipt>;
@@ -0,0 +1,12 @@
1
+ import { commandStale, waitForMutationUpload } from './runtime.js';
2
+ /** Output fields every local-database write reports, as note and todo writes do. */
3
+ export const WRITE_RECEIPT_FIELDS = ['durability', 'upload_queue_remaining', 'stale'];
4
+ /**
5
+ * Waits for the upload `--durability` asks for and describes where the write
6
+ * stands: `durability` local or uploaded, the remaining upload queue, any
7
+ * upload error, and whether the pre-write sync was stale.
8
+ */
9
+ export async function completeWrite(runtime, durability) {
10
+ const upload = await waitForMutationUpload(runtime, { durability });
11
+ return { ...upload, stale: commandStale(runtime) };
12
+ }
@@ -1,3 +1,5 @@
1
1
  export declare const POWERSYNC_DEFAULT_ENDPOINT = "https://powersync.dinoai.fun";
2
2
  export declare const POWERSYNC_DEFAULT_TOKEN_ENDPOINT = "https://dinoai.chatgo.pro/auth/v2/token";
3
3
  export declare const POWERSYNC_DEFAULT_UPLOAD_BASE_URL = "https://dinoai.chatgo.pro";
4
+ /** dinox-tool business API (semantic search, gateway credentials). */
5
+ export declare const AI_SERVICE_URL = "https://aisdk.chatgo.pro";
@@ -1,3 +1,5 @@
1
1
  export const POWERSYNC_DEFAULT_ENDPOINT = 'https://powersync.dinoai.fun';
2
2
  export const POWERSYNC_DEFAULT_TOKEN_ENDPOINT = 'https://dinoai.chatgo.pro/auth/v2/token';
3
3
  export const POWERSYNC_DEFAULT_UPLOAD_BASE_URL = 'https://dinoai.chatgo.pro';
4
+ /** dinox-tool business API (semantic search, gateway credentials). */
5
+ export const AI_SERVICE_URL = 'https://aisdk.chatgo.pro';
@@ -1,6 +1,8 @@
1
1
  import type { DaemonPingResult, DaemonShutdownResult } from './types.js';
2
2
  export declare function sendDaemonRequest(method: string, params?: unknown, requestOptions?: {
3
3
  timeoutMs?: number;
4
+ /** Streamed responses: called with each NDJSON frame ahead of the final response line. */
5
+ onFrame?: (frame: unknown) => void;
4
6
  }): Promise<unknown>;
5
7
  export declare function isDaemonRunning(timeoutMs?: number): Promise<boolean>;
6
8
  /**
@@ -1,6 +1,7 @@
1
1
  import { request as httpRequest } from 'node:http';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
4
+ import { getPackageVersion } from '../utils/version.js';
4
5
  import { remainingDeadlineMs } from '../utils/deadline.js';
5
6
  import { DinoxError, wireErrorCode } from '../utils/errors.js';
6
7
  const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
@@ -34,6 +35,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
34
35
  };
35
36
  return new Promise((resolve, reject) => {
36
37
  let settled = false;
38
+ let connected = false;
37
39
  let deadlineTimer;
38
40
  const finalize = (error, value) => {
39
41
  if (settled)
@@ -43,6 +45,8 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
43
45
  clearTimeout(deadlineTimer);
44
46
  }
45
47
  if (error) {
48
+ if (error instanceof Error)
49
+ Object.assign(error, { requestWritten: connected });
46
50
  reject(error);
47
51
  return;
48
52
  }
@@ -62,15 +66,31 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
62
66
  };
63
67
  const req = httpRequest(options, (res) => {
64
68
  const statusCode = res.statusCode ?? 0;
65
- const chunks = [];
69
+ let chunks = [];
66
70
  let totalBytes = 0;
71
+ const { onFrame } = requestOptions;
67
72
  res.on('data', (chunk) => {
68
73
  totalBytes += chunk.length;
69
- if (totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
74
+ if (method !== 'command.execute' && totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
70
75
  req.destroy(new Error('Daemon response too large'));
71
76
  return;
72
77
  }
73
78
  chunks.push(chunk);
79
+ if (!onFrame || !chunk.includes(0x0a))
80
+ return;
81
+ // Every complete line is a frame; the response itself is the unterminated last line.
82
+ let pending = Buffer.concat(chunks);
83
+ for (let newline = pending.indexOf(0x0a); newline !== -1; newline = pending.indexOf(0x0a)) {
84
+ const line = pending.subarray(0, newline).toString('utf-8');
85
+ pending = pending.subarray(newline + 1);
86
+ try {
87
+ onFrame(JSON.parse(line));
88
+ }
89
+ catch {
90
+ // A malformed frame only loses that progress line.
91
+ }
92
+ }
93
+ chunks = [pending];
74
94
  });
75
95
  res.on('end', () => {
76
96
  try {
@@ -92,10 +112,23 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
92
112
  }
93
113
  });
94
114
  });
115
+ req.on('socket', socket => {
116
+ // A reused keep-alive socket is already connected and never emits
117
+ // 'connect' again; listening anyway leaks one listener per request.
118
+ if (!socket.connecting) {
119
+ connected = true;
120
+ return;
121
+ }
122
+ socket.once('connect', () => { connected = true; });
123
+ });
95
124
  req.on('timeout', () => {
96
125
  req.destroy(new Error('Daemon request timed out'));
97
126
  });
98
127
  req.on('error', (error) => finalize(error));
128
+ req.on('response', res => {
129
+ res.on('aborted', () => finalize(new Error('Daemon response interrupted')));
130
+ res.on('error', error => finalize(error));
131
+ });
99
132
  deadlineTimer = setTimeout(() => {
100
133
  req.destroy(new Error('Daemon request timed out'));
101
134
  }, timeoutMs);
@@ -282,6 +315,16 @@ export async function waitForFreshDaemon(expectedVersion, expectedUserId, expect
282
315
  }
283
316
  }
284
317
  }
318
+ /** Daemon log context for a stop request; never carries credentials. */
319
+ function redactRequesterCommand(argv) {
320
+ return argv.join(' ')
321
+ .replace(/(auth\s+login)\b[\s\S]*/i, '$1 [REDACTED]')
322
+ .replace(/((?:--)?[\w-]*(?:token|authorization|secret|password|api[-_]?key)[\w-]*[=\s]+)(?:Bearer\s+)?[^\s]+/gi, '$1[REDACTED]')
323
+ .replace(/\bBearer\s+\S+/gi, 'Bearer [REDACTED]')
324
+ .replace(/\beyJ[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){1,2}\b/g, '[REDACTED]')
325
+ .replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[REDACTED]')
326
+ .slice(0, 200);
327
+ }
285
328
  /** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
286
329
  export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_000) {
287
330
  if (!expectedInstanceId) {
@@ -290,7 +333,14 @@ export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_00
290
333
  recoverable: true,
291
334
  });
292
335
  }
293
- const result = await sendDaemonRequest('shutdown', { expectedInstanceId }, { timeoutMs });
336
+ const result = await sendDaemonRequest('shutdown', {
337
+ expectedInstanceId,
338
+ requester: {
339
+ pid: process.pid,
340
+ version: getPackageVersion(),
341
+ command: redactRequesterCommand(process.argv.slice(2)),
342
+ },
343
+ }, { timeoutMs });
294
344
  if (!result
295
345
  || typeof result !== 'object'
296
346
  || result.stopping !== true
@@ -0,0 +1,20 @@
1
+ /** How often the daemon checks for a wake from sleep or a network change. */
2
+ export declare const CONNECTIVITY_CHECK_INTERVAL_MS = 10000;
3
+ /** A tick this late means the machine was asleep (timers do not run during sleep). */
4
+ export declare const WAKE_DRIFT_THRESHOLD_MS = 30000;
5
+ export type ConnectivityChange = 'wake' | 'network-change';
6
+ /** Addresses of the non-internal interfaces; changes on Wi-Fi switch, VPN, or link loss. */
7
+ export declare function networkSignature(): string;
8
+ /**
9
+ * Detects the events after which a long-lived sync stream may be a dead
10
+ * connection that still reports "connected": a wake from sleep and a change
11
+ * of network. Call `tick()` every CONNECTIVITY_CHECK_INTERVAL_MS.
12
+ */
13
+ export declare function createConnectivityWatcher(options: {
14
+ onChange: (change: ConnectivityChange) => void;
15
+ now?: () => number;
16
+ signature?: () => string;
17
+ intervalMs?: number;
18
+ }): {
19
+ tick: () => void;
20
+ };
@@ -0,0 +1,42 @@
1
+ import os from 'node:os';
2
+ /** How often the daemon checks for a wake from sleep or a network change. */
3
+ export const CONNECTIVITY_CHECK_INTERVAL_MS = 10_000;
4
+ /** A tick this late means the machine was asleep (timers do not run during sleep). */
5
+ export const WAKE_DRIFT_THRESHOLD_MS = 30_000;
6
+ /** Addresses of the non-internal interfaces; changes on Wi-Fi switch, VPN, or link loss. */
7
+ export function networkSignature() {
8
+ const addresses = [];
9
+ for (const [name, entries] of Object.entries(os.networkInterfaces())) {
10
+ for (const entry of entries ?? []) {
11
+ if (!entry.internal)
12
+ addresses.push(`${name}:${entry.address}`);
13
+ }
14
+ }
15
+ return addresses.sort().join(',');
16
+ }
17
+ /**
18
+ * Detects the events after which a long-lived sync stream may be a dead
19
+ * connection that still reports "connected": a wake from sleep and a change
20
+ * of network. Call `tick()` every CONNECTIVITY_CHECK_INTERVAL_MS.
21
+ */
22
+ export function createConnectivityWatcher(options) {
23
+ const now = options.now ?? Date.now;
24
+ const signature = options.signature ?? networkSignature;
25
+ const intervalMs = options.intervalMs ?? CONNECTIVITY_CHECK_INTERVAL_MS;
26
+ let lastTickAt = now();
27
+ let lastSignature = signature();
28
+ return {
29
+ tick() {
30
+ const tickAt = now();
31
+ const drift = tickAt - lastTickAt - intervalMs;
32
+ lastTickAt = tickAt;
33
+ const currentSignature = signature();
34
+ const networkChanged = currentSignature !== lastSignature;
35
+ lastSignature = currentSignature;
36
+ if (drift >= WAKE_DRIFT_THRESHOLD_MS)
37
+ options.onChange('wake');
38
+ else if (networkChanged)
39
+ options.onChange('network-change');
40
+ },
41
+ };
42
+ }
@@ -1,9 +1,12 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { getDaemonInfo, isDaemonFresh, waitForFreshDaemon } from './client.js';
2
- import { stopDaemonLocked } from './index.js';
3
+ import { matchesRuntimeTokenBinding } from './fresh.js';
4
+ import { restartDaemonLocked, stopDaemonLocked } from './index.js';
3
5
  import { DEFAULT_DAEMON_PORT } from './types.js';
4
- import { launchDaemon } from './launch.js';
6
+ import { daemonServiceSpec, foreignServiceOwner, launchDaemon, usableDaemonService } from './launch.js';
5
7
  import { getDaemonLogPath } from './log.js';
6
- import { acquireDaemonStartLock, } from './pid.js';
8
+ import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from './pid.js';
9
+ import { getDaemonService } from './supervisor.js';
7
10
  import { loadConfig } from '../config/store.js';
8
11
  import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
9
12
  import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
@@ -20,13 +23,22 @@ async function hydrateDaemonIdentity(baseConfig) {
20
23
  runtimeConfigFingerprint: fingerprintDaemonRuntimeConfig(config),
21
24
  };
22
25
  }
23
- function matchesRuntimeTokenBinding(info, version, config) {
24
- return Boolean(config.auth.authorization
25
- && info.authenticated
26
- && info.version === version
27
- && info.tokenFingerprint === (config.auth.tokenFingerprint ?? null)
28
- && info.runtimeConfigFingerprint === fingerprintDaemonRuntimeConfig(config)
29
- && (!config.auth.userId || info.userId === config.auth.userId));
26
+ /**
27
+ * Refuses to restart or rewrite a service another dino install owns. The data
28
+ * command then runs in-process; `dino daemon install` (or start/restart)
29
+ * switches the service to this CLI.
30
+ */
31
+ async function assertServiceNotForeign() {
32
+ const service = await usableDaemonService();
33
+ const owner = service ? foreignServiceOwner(service) : null;
34
+ if (!service || !owner)
35
+ return;
36
+ throw new DinoxError(`The ${service.kind} daemon service belongs to another dino install (${owner}).`, {
37
+ code: 'DAEMON_SERVICE_FOREIGN',
38
+ recoverable: true,
39
+ details: { service: service.kind, owner, definitionPath: service.definitionPath },
40
+ suggestion: 'Run `dino daemon install` with the dino you want the service to use.',
41
+ });
30
42
  }
31
43
  async function ensureDaemonRunningUnlocked() {
32
44
  const expectedVersion = getPackageVersion();
@@ -43,15 +55,22 @@ async function ensureDaemonRunningUnlocked() {
43
55
  return;
44
56
  }
45
57
  const expectedIdentity = await hydrateDaemonIdentity(baseConfig);
58
+ if (info && isDaemonFresh(info, expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint)) {
59
+ return;
60
+ }
61
+ await assertServiceNotForeign();
46
62
  if (info) {
47
63
  // A daemon is reachable — make sure it is the *right* one. It holds a long-lived
48
64
  // PowerSync connection bound to whatever account/version it started with, so after
49
65
  // `auth login` (new userId) or `dino update` (new version) the running daemon would
50
66
  // otherwise keep serving stale or wrong-account data. Restart it when it drifts.
51
- if (isDaemonFresh(info, expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint)) {
67
+ printHumanReadableWarning('Restarting daemon (version or account changed)...');
68
+ const service = getDaemonService();
69
+ if (service?.isInstalled() && info.supervisor === service.kind
70
+ && service.isCurrent(daemonServiceSpec(service, DEFAULT_DAEMON_PORT))) {
71
+ await restartDaemonLocked(DEFAULT_DAEMON_PORT, { quiet: true });
52
72
  return;
53
73
  }
54
- printHumanReadableWarning('Restarting daemon (version or account changed)...');
55
74
  await stopDaemonLocked().catch((error) => {
56
75
  const message = error instanceof Error ? error.message : String(error);
57
76
  printHumanReadableWarning(`warning: failed to stop stale daemon: ${message}`);
@@ -65,6 +84,23 @@ async function ensureDaemonRunningUnlocked() {
65
84
  }
66
85
  }
67
86
  else {
87
+ // A live process that owns the socket but does not answer is hung; only
88
+ // launchd / systemd can replace it safely (they know the exact process).
89
+ const pid = readPid();
90
+ const service = getDaemonService();
91
+ if (pid && isProcessRunning(pid.pid) && existsSync(getDaemonSocketPath())
92
+ && service?.isInstalled() && service.kind !== 'schtasks') {
93
+ printHumanReadableWarning('warning: daemon is not responding; restarting the service');
94
+ await service.restart();
95
+ if (await waitForFreshDaemon(expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint, DAEMON_READY_TIMEOUT_MS)) {
96
+ return;
97
+ }
98
+ throw new DinoxError('The daemon service is still not responding after a restart.', {
99
+ code: 'DAEMON_START_FAILED',
100
+ details: { service: service.kind, logPath: getDaemonLogPath() },
101
+ suggestion: `Inspect ${getDaemonLogPath()} for the daemon's startup error.`,
102
+ });
103
+ }
68
104
  printHumanReadableWarning('Starting daemon...');
69
105
  }
70
106
  const launch = await launchDaemon(DEFAULT_DAEMON_PORT);