@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 { buildBoxAddDryRunPreview, buildBoxMutationDryRunPreview } 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 { addBox, inspectAddBox, listBoxes } from './repo.js';
11
13
  import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
@@ -54,6 +56,7 @@ async function runBoxAdd(context) {
54
56
  }
55
57
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
56
58
  await withCommandRuntime(context, async (runtime) => {
59
+ const durability = resolveWriteDurabilityForRuntime(runtime, context.options.durability);
57
60
  const identity = await resolveAuthIdentity(runtime.config);
58
61
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
59
62
  const description = typeof context.options.description === 'string' ? context.options.description : undefined;
@@ -77,8 +80,7 @@ async function runBoxAdd(context) {
77
80
  description,
78
81
  color,
79
82
  });
80
- await waitForMutationUpload(runtime);
81
- const payload = { ok: true, stale: commandStale(runtime), ...result };
83
+ const payload = { ok: true, ...(await completeWrite(runtime, durability)), ...result };
82
84
  if (structuredOutput) {
83
85
  printSuccess(payload);
84
86
  return;
@@ -89,9 +91,10 @@ async function runBoxAdd(context) {
89
91
  }
90
92
  async function withBoxWriteRuntime(context, run) {
91
93
  return withCommandRuntime(context, async (runtime) => {
94
+ const durability = resolveWriteDurabilityForRuntime(runtime, context.options.durability);
92
95
  const identity = await resolveAuthIdentity(runtime.config);
93
96
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
94
- return run({ runtime, userId: identity.userId });
97
+ return run({ runtime, userId: identity.userId, durability });
95
98
  });
96
99
  }
97
100
  function requireArg(value, message) {
@@ -104,7 +107,7 @@ function requireArg(value, message) {
104
107
  async function runBoxRename(context) {
105
108
  const path = requireArg(context.args.path, 'Box path is required');
106
109
  const newName = requireArg(context.args.newName, 'New box name is required');
107
- await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
110
+ await withBoxWriteRuntime(context, async ({ runtime, userId, durability }) => {
108
111
  const result = await renameBox(runtime.db, { path, newName, userId }, { dryRun: Boolean(context.options.dryRun) });
109
112
  if (Boolean(context.options.dryRun)) {
110
113
  printDryRunPreview(buildBoxMutationDryRunPreview({
@@ -119,8 +122,7 @@ async function runBoxRename(context) {
119
122
  }), runtime);
120
123
  return;
121
124
  }
122
- await waitForMutationUpload(runtime);
123
- printSuccess({ ok: true, stale: commandStale(runtime), ...result });
125
+ printSuccess({ ok: true, ...(await completeWrite(runtime, durability)), ...result });
124
126
  });
125
127
  }
126
128
  async function runBoxMove(context) {
@@ -129,7 +131,7 @@ async function runBoxMove(context) {
129
131
  if (!to) {
130
132
  throw new DinoxError('--to is required');
131
133
  }
132
- await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
134
+ await withBoxWriteRuntime(context, async ({ runtime, userId, durability }) => {
133
135
  const result = await moveBox(runtime.db, { path, to, userId }, { dryRun: Boolean(context.options.dryRun) });
134
136
  if (Boolean(context.options.dryRun)) {
135
137
  printDryRunPreview(buildBoxMutationDryRunPreview({
@@ -144,8 +146,7 @@ async function runBoxMove(context) {
144
146
  }), runtime);
145
147
  return;
146
148
  }
147
- await waitForMutationUpload(runtime);
148
- printSuccess({ ok: true, stale: commandStale(runtime), ...result });
149
+ printSuccess({ ok: true, ...(await completeWrite(runtime, durability)), ...result });
149
150
  });
150
151
  }
151
152
  async function runBoxMerge(context) {
@@ -159,7 +160,7 @@ async function runBoxMerge(context) {
159
160
  expectedCount,
160
161
  };
161
162
  assertWriteConfirmationProvided('Box merge', writeOptions);
162
- await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
163
+ await withBoxWriteRuntime(context, async ({ runtime, userId, durability }) => {
163
164
  const result = await mergeBox(runtime.db, { from, to, userId }, writeOptions);
164
165
  if (dryRun) {
165
166
  printDryRunPreview(buildBoxMutationDryRunPreview({
@@ -180,8 +181,7 @@ async function runBoxMerge(context) {
180
181
  }), runtime);
181
182
  return;
182
183
  }
183
- await waitForMutationUpload(runtime);
184
- printSuccess({ ok: true, stale: commandStale(runtime), ...result });
184
+ printSuccess({ ok: true, ...(await completeWrite(runtime, durability)), ...result });
185
185
  });
186
186
  }
187
187
  async function runBoxCleanup(context) {
@@ -234,7 +234,7 @@ export const boxCommandGroup = {
234
234
  { flags: '--name <string>', description: 'Box path (alternative to positional path)' },
235
235
  { flags: '--description <string>', description: 'Box purpose/usage description' },
236
236
  { flags: '--color <string>', description: 'Box color' },
237
- { flags: '--dry-run', description: 'Preview the write without executing it' },
237
+ WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it' },
238
238
  ],
239
239
  schema: {
240
240
  path: 'box.add',
@@ -244,7 +244,7 @@ export const boxCommandGroup = {
244
244
  output: {
245
245
  type: 'structured',
246
246
  summary: 'Box creation acknowledgement',
247
- fields: ['id', 'name', 'path', 'parentId', 'description', 'color', 'restored', 'createdNodes', 'restoredNodes', 'stale'],
247
+ fields: ['id', 'name', 'path', 'parentId', 'description', 'color', 'restored', 'createdNodes', 'restoredNodes', ...WRITE_RECEIPT_FIELDS],
248
248
  },
249
249
  examples: ['dino box add "Work/Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
250
250
  },
@@ -291,7 +291,7 @@ export const boxCommandGroup = {
291
291
  { syntax: '<path>', description: 'Existing box path' },
292
292
  { syntax: '<new-name>', description: 'New leaf name' },
293
293
  ],
294
- options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
294
+ options: [WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it' }],
295
295
  schema: {
296
296
  path: 'box.rename',
297
297
  risk: 'write',
@@ -300,7 +300,7 @@ export const boxCommandGroup = {
300
300
  output: {
301
301
  type: 'structured',
302
302
  summary: 'Zettel box rename result',
303
- fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
303
+ fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', ...WRITE_RECEIPT_FIELDS],
304
304
  },
305
305
  examples: ['dino box rename "Work/Inbox" "Ideas" --format json --dry-run'],
306
306
  },
@@ -312,7 +312,7 @@ export const boxCommandGroup = {
312
312
  arguments: [{ syntax: '<path>', description: 'Existing box path' }],
313
313
  options: [
314
314
  { flags: '--to <parent-path>', description: 'Target parent box path', required: true, type: 'string' },
315
- { flags: '--dry-run', description: 'Preview the write without executing it' },
315
+ WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it' },
316
316
  ],
317
317
  schema: {
318
318
  path: 'box.move',
@@ -322,7 +322,7 @@ export const boxCommandGroup = {
322
322
  output: {
323
323
  type: 'structured',
324
324
  summary: 'Zettel box move result',
325
- fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
325
+ fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', ...WRITE_RECEIPT_FIELDS],
326
326
  },
327
327
  examples: ['dino box move "Inbox" --to "Work" --format json --dry-run'],
328
328
  },
@@ -338,7 +338,7 @@ export const boxCommandGroup = {
338
338
  options: [
339
339
  { flags: '--expected-count <n>', description: 'Required for real writes; must equal source boxes plus notes affected', type: 'number' },
340
340
  { flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
341
- { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
341
+ WRITE_DURABILITY_OPTION, { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
342
342
  ],
343
343
  schema: {
344
344
  path: 'box.merge',
@@ -348,7 +348,7 @@ export const boxCommandGroup = {
348
348
  output: {
349
349
  type: 'structured',
350
350
  summary: 'Zettel box merge result',
351
- fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'affectedCount', 'stale'],
351
+ fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'affectedCount', ...WRITE_RECEIPT_FIELDS],
352
352
  },
353
353
  examples: [
354
354
  'dino box merge "Work/Inbox" "Work/Archive" --format json --dry-run',
@@ -5,7 +5,7 @@ import { resolveConfig } from '../config/resolve.js';
5
5
  import type { ResolvedDinoxConfig } from '../config/types.js';
6
6
  import { shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
7
7
  import { restartDaemonLocked } from '../daemon/index.js';
8
- import { acquireDaemonStartLock } from '../daemon/pid.js';
8
+ import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../daemon/pid.js';
9
9
  import type { DaemonPidInfo } from '../daemon/pid.js';
10
10
  import { type DaemonPingResult } from '../daemon/types.js';
11
11
  import { connectPowerSync } from '../powersync/runtime.js';
@@ -89,6 +89,8 @@ type DoctorRuntime = {
89
89
  daemonBefore: DoctorDaemonSnapshot;
90
90
  daemonWasStopped: boolean;
91
91
  releaseDaemonStartLock: () => Promise<void>;
92
+ /** Held while doctor owns the database; see acquireDaemonExclusiveIntentLock. */
93
+ releaseIntentLock?: () => Promise<void>;
92
94
  daemonStartLockReleased: boolean;
93
95
  released: boolean;
94
96
  };
@@ -102,6 +104,7 @@ type DoctorPreparationDependencies = {
102
104
  resolveConfig: typeof resolveConfig;
103
105
  hydrateAuthIdentity: typeof hydrateResolvedAuthIdentity;
104
106
  acquireDaemonStartLock: typeof acquireDaemonStartLock;
107
+ acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
105
108
  captureDaemonSnapshot: typeof captureDaemonSnapshot;
106
109
  shutdownDaemon: typeof shutdownDaemonForExclusiveAccess;
107
110
  connectPowerSync: typeof connectPowerSync;
@@ -5,7 +5,7 @@ import { loadConfig } from '../config/store.js';
5
5
  import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
6
6
  import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
7
7
  import { restartDaemonLocked } from '../daemon/index.js';
8
- import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
8
+ import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
9
9
  import { DEFAULT_DAEMON_PORT } from '../daemon/types.js';
10
10
  import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
11
11
  import { inspectNoteTokenIndex, rebuildNoteTokenIndex, } from '../powersync/tokenIndex.js';
@@ -20,6 +20,7 @@ const doctorPreparationDependencies = {
20
20
  resolveConfig,
21
21
  hydrateAuthIdentity: hydrateResolvedAuthIdentity,
22
22
  acquireDaemonStartLock,
23
+ acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
23
24
  captureDaemonSnapshot,
24
25
  shutdownDaemon: shutdownDaemonForExclusiveAccess,
25
26
  connectPowerSync,
@@ -39,6 +40,14 @@ function doctorTimeoutMs(globals, config) {
39
40
  export async function prepareDoctorRuntime(context, overrides = {}) {
40
41
  const dependencies = { ...doctorPreparationDependencies, ...overrides };
41
42
  const releaseDaemonStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(context.globals.syncTimeout) ?? DEFAULT_DOCTOR_TIMEOUT_MS);
43
+ let releaseIntentLock;
44
+ const releaseIntent = async () => {
45
+ const release = releaseIntentLock;
46
+ releaseIntentLock = undefined;
47
+ await release?.().catch((error) => {
48
+ printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
49
+ });
50
+ };
42
51
  try {
43
52
  const rawConfig = await dependencies.loadConfig();
44
53
  const baseConfig = dependencies.resolveConfig(rawConfig);
@@ -56,6 +65,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
56
65
  authError = errorMessage(error);
57
66
  }
58
67
  }
68
+ releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
59
69
  const daemonBefore = await dependencies.captureDaemonSnapshot();
60
70
  const daemonDbPath = daemonBefore.info ? getPowerSyncDbPath(daemonBefore.info.userId) : null;
61
71
  const targetDbPath = getPowerSyncDbPath(config.auth.userId);
@@ -83,6 +93,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
83
93
  daemonBefore,
84
94
  daemonWasStopped,
85
95
  releaseDaemonStartLock,
96
+ releaseIntentLock,
86
97
  daemonStartLockReleased: false,
87
98
  released: false,
88
99
  };
@@ -109,16 +120,19 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
109
120
  daemonBefore,
110
121
  daemonWasStopped,
111
122
  releaseDaemonStartLock,
123
+ releaseIntentLock,
112
124
  daemonStartLockReleased: false,
113
125
  released: false,
114
126
  };
115
127
  }
116
128
  catch (error) {
129
+ await releaseIntent();
117
130
  await restoreStoppedDoctorDaemon(config, daemonBefore, daemonWasStopped, dependencies.restartDaemon);
118
131
  throw error;
119
132
  }
120
133
  }
121
134
  catch (error) {
135
+ await releaseIntent();
122
136
  await releaseDaemonStartLock().catch((releaseError) => {
123
137
  printHumanReadableWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(releaseError)}`);
124
138
  });
@@ -357,6 +371,11 @@ export async function releaseDoctorRuntime(runtime) {
357
371
  }
358
372
  await closePowerSyncAndReleaseOwner(runtime.db, runtime.releaseOwner);
359
373
  runtime.released = true;
374
+ const releaseIntentLock = runtime.releaseIntentLock;
375
+ runtime.releaseIntentLock = undefined;
376
+ await releaseIntentLock?.().catch((error) => {
377
+ printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
378
+ });
360
379
  }
361
380
  export async function releaseDoctorDaemonStartLock(runtime) {
362
381
  if (runtime.daemonStartLockReleased) {
@@ -4,6 +4,7 @@ import { registerCommandGroup } from '../descriptor/register.js';
4
4
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
5
5
  import { DinoxError } from '../../utils/errors.js';
6
6
  import { printSuccess } from '../../utils/output.js';
7
+ import { localCommandExecution, requestContext } from '../../utils/requestContext.js';
7
8
  import { commandStale, syncBeforeCommand, withCommandRuntime } from '../runtime.js';
8
9
  import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from './repo.js';
9
10
  const GRAPH_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
@@ -26,7 +27,7 @@ function parseDepth(value) {
26
27
  }
27
28
  async function runGraphCommand(context, onlineAction, offlineAction) {
28
29
  const globals = context.globals;
29
- if (Boolean(globals.offline) || Boolean(globals.requireSync)) {
30
+ if (requestContext.getStore()?.daemon || localCommandExecution.getStore() || Boolean(globals.offline) || Boolean(globals.requireSync)) {
30
31
  await withCommandRuntime(context, async (runtime) => {
31
32
  await syncBeforeCommand(runtime);
32
33
  printSuccess({
@@ -0,0 +1,6 @@
1
+ import { Command } from 'commander';
2
+ import { type McpCommandGroup } from '../gateway/mcpGroups.js';
3
+ import type { CommandDescriptor } from './descriptor/types.js';
4
+ export declare function parseMcpGroups(raw: unknown): McpCommandGroup[] | undefined;
5
+ export declare const mcpCommandDescriptor: CommandDescriptor;
6
+ export declare function registerMcpCommand(program: Command): void;
@@ -0,0 +1,47 @@
1
+ import { MCP_COMMAND_GROUPS } from '../gateway/mcpGroups.js';
2
+ import { DinoxError } from '../utils/errors.js';
3
+ import { registerStandaloneCommand } from './descriptor/register.js';
4
+ /** `--groups` names: the command group, with `stats` for meta.stats. */
5
+ const GROUP_NAMES = new Map(MCP_COMMAND_GROUPS.map((group) => [group === 'meta.stats' ? 'stats' : group, group]));
6
+ export function parseMcpGroups(raw) {
7
+ if (raw === undefined)
8
+ return undefined;
9
+ const names = String(raw).split(',').map((name) => name.trim().toLowerCase()).filter(Boolean);
10
+ const unknown = names.filter((name) => !GROUP_NAMES.has(name));
11
+ if (names.length === 0 || unknown.length > 0) {
12
+ throw new DinoxError(`--groups must list some of: ${[...GROUP_NAMES.keys()].join(', ')}`, {
13
+ code: 'INVALID_ARGUMENT',
14
+ recoverable: true,
15
+ details: { invalid: unknown, allowed: [...GROUP_NAMES.keys()] },
16
+ });
17
+ }
18
+ return [...new Set(names.map((name) => GROUP_NAMES.get(name)))];
19
+ }
20
+ async function runMcp(context) {
21
+ const groups = parseMcpGroups(context.options.groups);
22
+ const { serveStandaloneMcpOverStdio } = await import('../gateway/mcpServer.js');
23
+ await serveStandaloneMcpOverStdio({ readOnly: context.options.readOnly === true, groups });
24
+ }
25
+ export const mcpCommandDescriptor = {
26
+ name: 'mcp',
27
+ description: 'Serve Dinox commands as MCP tools over stdio for MCP clients (Claude Desktop, Cursor, ...)',
28
+ options: [
29
+ { flags: '--read-only', description: 'Expose only read actions', type: 'boolean' },
30
+ {
31
+ flags: '--groups <names>',
32
+ description: `Comma-separated tool groups to expose (${[...GROUP_NAMES.keys()].join(', ')}); default all`,
33
+ },
34
+ ],
35
+ schema: {
36
+ path: 'mcp',
37
+ risk: 'write',
38
+ auth: 'required',
39
+ globalOptions: [],
40
+ output: { type: 'structured', summary: 'MCP JSON-RPC over stdio until stdin closes', fields: [] },
41
+ examples: ['dino mcp', 'dino mcp --read-only --groups note,todo,graph'],
42
+ },
43
+ action: (context) => runMcp(context),
44
+ };
45
+ export function registerMcpCommand(program) {
46
+ registerStandaloneCommand(program, mcpCommandDescriptor);
47
+ }
@@ -1,4 +1,5 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
+ import { encodeCursor, resolveCursorOffset } from '../../utils/cursor.js';
2
3
  import { canonicalizeBidlinksInMarkdown } from '../../utils/bidlinkCanonicalization.js';
3
4
  import { printDryRunPreview } from '../../utils/dryRun.js';
4
5
  import { generateId } from '../../utils/id.js';
@@ -17,6 +18,7 @@ import { buildNoteSearchResponse, normalizeDaysForMeta } from './searchResponse.
17
18
  import { parseMutationListInput, parseNoteType, parsePreviewLineCount, parseSearchLimit, parseSearchListInput, parseSearchOffset, parseStarredOption, parseStringInput, parseStringListInput, parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
18
19
  import { readNoteProperties } from '../properties/repo.js';
19
20
  import { createNoteWithSeed, previewNoteCreateProperties, resolveNoteCreateSeed } from './createFromTemplate.js';
21
+ import { parseNoteContentShape, shapeNoteContent } from './detailContent.js';
20
22
  function toPublicNoteContext(note) {
21
23
  if (!note) {
22
24
  return null;
@@ -249,8 +251,20 @@ export async function runNoteSearch({ command, options, args, globals }) {
249
251
  : { values: [], isExplicitEmptyArray: false };
250
252
  const sqlExpression = typeof searchOptions.sql === 'string' ? searchOptions.sql.trim() : '';
251
253
  const limit = parseSearchLimit(searchOptions.limit);
252
- const offset = parseSearchOffset(searchOptions.offset);
253
254
  const fields = parseNoteSearchFields(searchOptions.fields, '--fields');
255
+ // Raw option values, not the resolved range: `--days` must not change the hash between pages.
256
+ const cursorQuery = {
257
+ keyword,
258
+ includeDeleted: Boolean(searchOptions.includeDeleted),
259
+ tags: tagsExpression,
260
+ from: searchOptions.from,
261
+ to: searchOptions.to,
262
+ days: searchOptions.days,
263
+ starred,
264
+ boxes: boxInput,
265
+ sql: sqlExpression,
266
+ };
267
+ const offset = resolveCursorOffset(searchOptions.cursor, parseSearchOffset(searchOptions.offset), 'note.search', cursorQuery);
254
268
  const execParams = {
255
269
  keyword,
256
270
  includeDeleted: Boolean(searchOptions.includeDeleted),
@@ -278,6 +292,7 @@ export async function runNoteSearch({ command, options, args, globals }) {
278
292
  includeDeleted: Boolean(searchOptions.includeDeleted),
279
293
  limit,
280
294
  offset,
295
+ nextCursor: (nextOffset) => encodeCursor('note.search', nextOffset, cursorQuery),
281
296
  fields,
282
297
  tagsExpression: tagsExpression || undefined,
283
298
  createdAtFrom,
@@ -349,6 +364,7 @@ export async function runNotePreview({ command, options, args }) {
349
364
  }
350
365
  export async function runNoteDetail({ command, options, args }) {
351
366
  const detailOptions = options;
367
+ const contentShape = parseNoteContentShape(detailOptions.content, detailOptions.maxChars);
352
368
  await withNoteDatabase(command, async (runtime) => {
353
369
  const { db } = runtime;
354
370
  await syncBeforeNoteCommand(runtime);
@@ -359,7 +375,7 @@ export async function runNoteDetail({ command, options, args }) {
359
375
  if (missing.length > 0) {
360
376
  throw new DinoxError(ids.length === 1 ? `Note not found: ${missing[0]}` : `Notes not found: ${missing.join(', ')}`);
361
377
  }
362
- const publicNotes = notes.map((note) => toPublicNoteDetail(note));
378
+ const publicNotes = notes.map((note) => shapeNoteContent(toPublicNoteDetail(note), contentShape));
363
379
  printSuccess({
364
380
  stale: commandStale(runtime),
365
381
  count: publicNotes.length,
@@ -11,6 +11,7 @@ import { nowIsoUtc } from '../../utils/time.js';
11
11
  import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
12
12
  import { applyJsonPatchToDocument, buildNoteContentBlocks, buildNoteContentOutline, getNoteContentHash, parseStoredNoteDocument, projectNoteContent, } from './contentEngine.js';
13
13
  import { parseStoredTags } from './repo.js';
14
+ import { resolveUserPath } from '../../utils/requestContext.js';
14
15
  const READ_TOKEN_TTL_MS = 30 * 60 * 1000;
15
16
  const MAX_STORED_READ_TOKENS = 500;
16
17
  const TOKEN_STORE_FILE = 'note-content-read-tokens.json';
@@ -111,7 +112,7 @@ async function loadTokenStore(nowMs = Date.now()) {
111
112
  const filePath = tokenStorePath();
112
113
  let tokens = [];
113
114
  try {
114
- const raw = await fs.readFile(filePath, 'utf8');
115
+ const raw = await fs.readFile(resolveUserPath(filePath), 'utf8');
115
116
  const parsed = JSON.parse(raw);
116
117
  if (Array.isArray(parsed.tokens)) {
117
118
  tokens = parsed.tokens.filter((token) => typeof token?.token === 'string' &&
@@ -1,11 +1,28 @@
1
1
  import { registerCommandGroup } from '../descriptor/register.js';
2
- import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
2
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, PROGRESS_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
3
3
  import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
4
4
  import { runNoteCreate, runNoteDelete, runNoteDetail, runNoteGet, runNoteMove, runNotePreview, runNoteSearch, runNoteStar, runNoteTag, runNoteUnstar, runNoteUpdate, } from './actions.js';
5
5
  import { runNoteBulk as runNoteBulkAction } from './bulk.js';
6
6
  import { runNoteContentRead, runNotePatch } from './contentPatchCommand.js';
7
7
  import { runNoteExport } from './export.js';
8
+ import { runNoteContext, runNoteSemantic, runNoteSimilar, SEMANTIC_LIMITS } from './semantic.js';
8
9
  const NOTE_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
10
+ // Semantic commands query the online index; they refuse --offline.
11
+ const NOTE_SEMANTIC_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
12
+ const SEMANTIC_FILTER_OPTIONS = [
13
+ { flags: '--tags <string|@file>', description: 'Only notes with all these tags (JSON array or comma list; child tags included)', type: 'string' },
14
+ { flags: '--boxes <string|@file>', description: 'Only notes in any of these box paths or unique names (JSON array or comma list)', type: 'string' },
15
+ { flags: '--from <date>', description: 'created_at start (YYYY-MM-DD or ISO datetime)', type: 'string' },
16
+ { flags: '--to <date>', description: 'created_at end (YYYY-MM-DD or ISO datetime)', type: 'string' },
17
+ { flags: '--days <n>', description: 'Recent N days by created_at; mutually exclusive with --from/--to', type: 'number' },
18
+ ];
19
+ const SEMANTIC_MODE_OPTION = {
20
+ flags: '--mode <mode>',
21
+ description: 'hybrid (meaning + keywords, default), semantic (meaning only), or keyword',
22
+ type: 'string',
23
+ enum: ['hybrid', 'semantic', 'keyword'],
24
+ };
25
+ const SEMANTIC_NOTE_FIELDS = ['id', 'title', 'tags', 'boxes', 'created_at', 'is_starred', 'score', 'section', 'heading_path', 'snippet', 'matched_source', 'matches', 'in_local_cache'];
9
26
  const NOTE_WRITE_RECEIPT_FIELDS = [
10
27
  'durability',
11
28
  'upload_queue_remaining',
@@ -42,6 +59,7 @@ export const noteCommandGroup = {
42
59
  { flags: '--sql <expr>', description: 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type/is_starred', type: 'string' },
43
60
  { flags: '--limit <n>', description: 'Maximum returned notes', type: 'number' },
44
61
  { flags: '--offset <n>', description: 'Result offset for pagination', type: 'number', defaultValue: 0 },
62
+ { flags: '--cursor <token>', description: 'Continue from meta.pagination.next_cursor of the previous page (same query)' },
45
63
  { flags: '--fields <list>', description: 'Comma-separated fields: id,title,summary,tags,created_at,starred_at,boxes,is_starred', type: 'string[]' },
46
64
  { flags: '--include-deleted', description: 'Include soft-deleted notes', type: 'boolean' },
47
65
  ],
@@ -60,10 +78,90 @@ export const noteCommandGroup = {
60
78
  examples: [
61
79
  'dino note search "AI" --days 7 --format json',
62
80
  'dino note search --tags "(work OR life) AND NOT archived" --format json',
81
+ 'dino note search "AI" --cursor <meta.pagination.next_cursor> --format json',
63
82
  ],
64
83
  },
65
84
  action: runNoteSearch,
66
85
  },
86
+ {
87
+ name: 'semantic',
88
+ description: 'Find notes by meaning through the Dinox semantic index (online)',
89
+ arguments: [{ syntax: '<query>', description: 'Natural-language query' }],
90
+ options: [
91
+ SEMANTIC_MODE_OPTION,
92
+ { flags: '--limit <n>', description: `Maximum returned notes (1-${SEMANTIC_LIMITS.search.max}, default ${SEMANTIC_LIMITS.search.default})`, type: 'number' },
93
+ ...SEMANTIC_FILTER_OPTIONS,
94
+ { flags: '--include-resources', description: 'Also match attachments (PDFs, images, web clips); hits report their parent note', type: 'boolean' },
95
+ ],
96
+ schema: {
97
+ path: 'note.semantic',
98
+ risk: 'read',
99
+ auth: 'required',
100
+ globalOptions: NOTE_SEMANTIC_GLOBAL_OPTIONS,
101
+ output: {
102
+ type: 'structured',
103
+ summary: 'Notes ranked by semantic relevance, one entry per note with its best-matching snippet',
104
+ entity: 'semantic_note',
105
+ fields: SEMANTIC_NOTE_FIELDS,
106
+ metaFields: ['query', 'mode', 'returned_count', 'limit', 'include_resources', 'dropped_deleted_count', 'applied_filters'],
107
+ },
108
+ examples: [
109
+ 'dino note semantic "how I planned the Q3 launch" --format json',
110
+ 'dino note semantic "sleep and productivity" --tags health --days 90 --limit 20 --format json',
111
+ ],
112
+ },
113
+ action: runNoteSemantic,
114
+ },
115
+ {
116
+ name: 'similar',
117
+ description: 'Find notes semantically similar to a note (online)',
118
+ arguments: [{ syntax: '<id>', description: 'Note id' }],
119
+ options: [
120
+ { flags: '--limit <n>', description: `Maximum returned notes (1-${SEMANTIC_LIMITS.similar.max}, default ${SEMANTIC_LIMITS.similar.default})`, type: 'number' },
121
+ ],
122
+ schema: {
123
+ path: 'note.similar',
124
+ risk: 'read',
125
+ auth: 'required',
126
+ globalOptions: NOTE_SEMANTIC_GLOBAL_OPTIONS,
127
+ output: {
128
+ type: 'structured',
129
+ summary: 'Semantically similar notes, excluding the note itself',
130
+ entity: 'semantic_note',
131
+ fields: SEMANTIC_NOTE_FIELDS,
132
+ metaFields: ['note_id', 'returned_count', 'limit', 'dropped_deleted_count'],
133
+ },
134
+ examples: ['dino note similar <id> --limit 10 --format json'],
135
+ },
136
+ action: runNoteSimilar,
137
+ },
138
+ {
139
+ name: 'context',
140
+ description: 'Assemble cited, token-budgeted context from notes to answer a question (online)',
141
+ arguments: [{ syntax: '<query>', description: 'Question to gather context for' }],
142
+ options: [
143
+ SEMANTIC_MODE_OPTION,
144
+ { flags: '--max-tokens <n>', description: `Context token budget (1-${SEMANTIC_LIMITS.contextTokens.max}, default ${SEMANTIC_LIMITS.contextTokens.default})`, type: 'number' },
145
+ { flags: '--max-notes <n>', description: `Maximum notes contributing blocks (1-${SEMANTIC_LIMITS.contextGroups.max}, default ${SEMANTIC_LIMITS.contextGroups.default})`, type: 'number' },
146
+ { flags: '--limit <n>', description: `Retrieved chunks before assembly (1-${SEMANTIC_LIMITS.contextTopK.max}, default ${SEMANTIC_LIMITS.contextTopK.default})`, type: 'number' },
147
+ ...SEMANTIC_FILTER_OPTIONS,
148
+ { flags: '--notes-only', description: 'Leave attachments out of the context', type: 'boolean' },
149
+ ],
150
+ schema: {
151
+ path: 'note.context',
152
+ risk: 'read',
153
+ auth: 'required',
154
+ globalOptions: NOTE_SEMANTIC_GLOBAL_OPTIONS,
155
+ output: {
156
+ type: 'structured',
157
+ summary: 'Context blocks (content with note id and citations) ready to ground an answer',
158
+ fields: ['blocks.ref', 'blocks.note_id', 'blocks.title', 'blocks.role', 'blocks.score', 'blocks.token_count', 'blocks.content', 'blocks.citations'],
159
+ metaFields: ['query', 'mode', 'returned_count', 'context_tokens', 'max_tokens', 'dropped_deleted_count'],
160
+ },
161
+ examples: ['dino note context "what did I decide about pricing?" --max-tokens 4000 --format json'],
162
+ },
163
+ action: runNoteContext,
164
+ },
67
165
  {
68
166
  name: 'get',
69
167
  description: 'Get lightweight note context by id without exposing full note content',
@@ -107,7 +205,11 @@ export const noteCommandGroup = {
107
205
  name: 'detail',
108
206
  description: 'Get full note details for one or more ids',
109
207
  arguments: [{ syntax: '[id]', description: 'Optional note id when reading a single note' }],
110
- options: [{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' }],
208
+ options: [
209
+ { flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
210
+ { flags: '--content <formats>', description: 'Content fields to return: comma-separated md,text,json, or none (default: all three)', type: 'string' },
211
+ { flags: '--max-chars <n>', description: 'Shorten content_md and content_text to n characters; sets content_truncated', type: 'number' },
212
+ ],
111
213
  schema: {
112
214
  path: 'note.detail',
113
215
  risk: 'read',
@@ -116,9 +218,13 @@ export const noteCommandGroup = {
116
218
  output: {
117
219
  type: 'structured',
118
220
  summary: 'Ordered full-note collection with freshness metadata',
119
- fields: ['stale', 'count', 'notes.id', 'notes.title', 'notes.content_md', 'notes.content_text', 'notes.content_json', 'notes.properties', 'notes.is_pub', 'notes.starred_at', 'notes.is_starred', 'notes.tags', 'notes.boxes', 'notes.created_at', 'notes.updated_at', 'notes.is_del'],
221
+ fields: ['stale', 'count', 'notes.id', 'notes.title', 'notes.content_md', 'notes.content_text', 'notes.content_json', 'notes.content_truncated', 'notes.properties', 'notes.is_pub', 'notes.starred_at', 'notes.is_starred', 'notes.tags', 'notes.boxes', 'notes.created_at', 'notes.updated_at', 'notes.is_del'],
120
222
  },
121
- examples: ['dino note detail <id> --format json', 'dino note detail --ids @/tmp/note-ids.txt --format json'],
223
+ examples: [
224
+ 'dino note detail <id> --format json',
225
+ 'dino note detail <id> --content md --max-chars 4000 --format json',
226
+ 'dino note detail --ids @/tmp/note-ids.txt --format json',
227
+ ],
122
228
  },
123
229
  action: runNoteDetail,
124
230
  },
@@ -137,7 +243,7 @@ export const noteCommandGroup = {
137
243
  path: 'note.export',
138
244
  risk: 'read',
139
245
  auth: 'required',
140
- globalOptions: NOTE_GLOBAL_OPTIONS,
246
+ globalOptions: [...NOTE_GLOBAL_OPTIONS, ...PROGRESS_OPTIONS],
141
247
  output: {
142
248
  type: 'structured',
143
249
  summary: 'Export payload or file write acknowledgement for note backup/migration',
@@ -0,0 +1,19 @@
1
+ declare const CONTENT_FIELDS: {
2
+ readonly md: "content_md";
3
+ readonly text: "content_text";
4
+ readonly json: "content_json";
5
+ };
6
+ type NoteContentFormat = keyof typeof CONTENT_FIELDS;
7
+ export type NoteContentShape = {
8
+ formats: ReadonlySet<NoteContentFormat>;
9
+ maxChars: number | null;
10
+ };
11
+ /** Parses `--content md,text,json|none` (default: all three, as before). */
12
+ export declare function parseNoteContentShape(content: unknown, maxChars: unknown): NoteContentShape;
13
+ /**
14
+ * Keeps the requested content fields and shortens Markdown / plain text to
15
+ * `maxChars` characters (whole code points). `content_truncated` reports
16
+ * whether anything was cut, so an agent knows to fetch more.
17
+ */
18
+ export declare function shapeNoteContent(note: Record<string, unknown>, shape: NoteContentShape): Record<string, unknown>;
19
+ export {};