@dinoxx/dinox-cli 2.14.0 → 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 (76) hide show
  1. package/dist/ai/ragxClient.d.ts +15 -0
  2. package/dist/ai/ragxClient.js +71 -0
  3. package/dist/cli.js +27 -3
  4. package/dist/commands/batch.d.ts +35 -0
  5. package/dist/commands/batch.js +202 -0
  6. package/dist/commands/boxes/index.js +21 -21
  7. package/dist/commands/mcp.d.ts +6 -0
  8. package/dist/commands/mcp.js +47 -0
  9. package/dist/commands/notes/actions.js +18 -2
  10. package/dist/commands/notes/descriptors.js +111 -5
  11. package/dist/commands/notes/detailContent.d.ts +19 -0
  12. package/dist/commands/notes/detailContent.js +62 -0
  13. package/dist/commands/notes/export.js +4 -0
  14. package/dist/commands/notes/repo/search.d.ts +2 -1
  15. package/dist/commands/notes/repo/search.js +1 -1
  16. package/dist/commands/notes/repo/semantic.d.ts +14 -0
  17. package/dist/commands/notes/repo/semantic.js +31 -0
  18. package/dist/commands/notes/searchResponse.d.ts +3 -0
  19. package/dist/commands/notes/searchResponse.js +1 -0
  20. package/dist/commands/notes/semantic.d.ts +116 -0
  21. package/dist/commands/notes/semantic.js +332 -0
  22. package/dist/commands/prompt/index.js +7 -5
  23. package/dist/commands/savedViews/index.js +12 -3
  24. package/dist/commands/schema/globalOptions.d.ts +6 -0
  25. package/dist/commands/schema/globalOptions.js +6 -0
  26. package/dist/commands/schema/renderSkills.js +5 -2
  27. package/dist/commands/schema/spec.js +18 -7
  28. package/dist/commands/storage/index.js +7 -5
  29. package/dist/commands/sync.js +3 -3
  30. package/dist/commands/tags/index.js +21 -21
  31. package/dist/commands/todo/index.js +22 -2
  32. package/dist/commands/todo/repo.d.ts +5 -0
  33. package/dist/commands/todo/repo.js +7 -2
  34. package/dist/commands/writeReceipt.d.ts +13 -0
  35. package/dist/commands/writeReceipt.js +12 -0
  36. package/dist/config/serviceEndpoints.d.ts +2 -0
  37. package/dist/config/serviceEndpoints.js +2 -0
  38. package/dist/daemon/client.d.ts +2 -0
  39. package/dist/daemon/client.js +22 -2
  40. package/dist/daemon/forward.d.ts +4 -1
  41. package/dist/daemon/forward.js +13 -4
  42. package/dist/daemon/server.d.ts +1 -1
  43. package/dist/daemon/server.js +17 -1
  44. package/dist/daemon/types.d.ts +6 -0
  45. package/dist/dinox.js +12 -10
  46. package/dist/gateway/deviceHost.js +2 -1
  47. package/dist/gateway/mcpCommandTools.d.ts +15 -5
  48. package/dist/gateway/mcpCommandTools.js +30 -19
  49. package/dist/gateway/mcpEnv.d.ts +2 -0
  50. package/dist/gateway/mcpEnv.js +2 -0
  51. package/dist/gateway/mcpGroups.d.ts +3 -0
  52. package/dist/gateway/mcpGroups.js +4 -0
  53. package/dist/gateway/mcpServer.d.ts +10 -1
  54. package/dist/gateway/mcpServer.js +25 -5
  55. package/dist/powersync/syncPass.js +13 -1
  56. package/dist/powersync/tokenIndex.d.ts +2 -0
  57. package/dist/powersync/tokenIndex.js +1 -0
  58. package/dist/requests/execute.d.ts +11 -0
  59. package/dist/requests/execute.js +26 -0
  60. package/dist/skills/commandRefs.js +2 -10
  61. package/dist/utils/cursor.d.ts +9 -0
  62. package/dist/utils/cursor.js +61 -0
  63. package/dist/utils/defaultFormat.d.ts +8 -0
  64. package/dist/utils/defaultFormat.js +38 -0
  65. package/dist/utils/errorCodes.d.ts +4 -0
  66. package/dist/utils/errorCodes.js +1 -0
  67. package/dist/utils/output.d.ts +2 -0
  68. package/dist/utils/output.js +41 -3
  69. package/dist/utils/progress.d.ts +25 -0
  70. package/dist/utils/progress.js +59 -0
  71. package/dist/utils/requestContext.d.ts +8 -0
  72. package/dist/utils/select.d.ts +20 -0
  73. package/dist/utils/select.js +92 -0
  74. package/dist/utils/usageError.d.ts +37 -0
  75. package/dist/utils/usageError.js +127 -0
  76. package/package.json +1 -1
@@ -0,0 +1,15 @@
1
+ export type RagxRequest = {
2
+ baseUrl: string;
3
+ authorization: string | undefined;
4
+ path: '/api/ragx/search' | '/api/ragx/similar' | '/api/ragx/context';
5
+ body: unknown;
6
+ timeoutMs: number;
7
+ fetch?: typeof fetch;
8
+ };
9
+ /**
10
+ * Calls a dinox-tool RAG endpoint and returns its `data`. Business codes map
11
+ * onto CLI codes: 000001 is a rejected token or request, 000006 a missing
12
+ * target, anything else (and every transport failure) is the service being
13
+ * unavailable.
14
+ */
15
+ export declare function postRagx<T>(request: RagxRequest): Promise<T>;
@@ -0,0 +1,71 @@
1
+ import { toAuthorizationHeaderValue } from '../utils/authToken.js';
2
+ import { DinoxError } from '../utils/errors.js';
3
+ const RAGX_OK = '000000';
4
+ const RAGX_INVALID = '000001';
5
+ const RAGX_NOT_FOUND = '000006';
6
+ function unavailable(message, cause) {
7
+ return new DinoxError(message, {
8
+ code: 'SEMANTIC_SEARCH_UNAVAILABLE',
9
+ cause,
10
+ recoverable: true,
11
+ suggestion: 'Retry shortly, or fall back to keyword search with `dino note search <query> --format json`.',
12
+ });
13
+ }
14
+ /**
15
+ * Calls a dinox-tool RAG endpoint and returns its `data`. Business codes map
16
+ * onto CLI codes: 000001 is a rejected token or request, 000006 a missing
17
+ * target, anything else (and every transport failure) is the service being
18
+ * unavailable.
19
+ */
20
+ export async function postRagx(request) {
21
+ if (!request.authorization) {
22
+ throw new DinoxError('Semantic search needs a logged-in account.', { code: 'AUTH_REQUIRED', recoverable: true });
23
+ }
24
+ let response;
25
+ try {
26
+ response = await (request.fetch ?? fetch)(`${request.baseUrl}${request.path}`, {
27
+ method: 'POST',
28
+ headers: {
29
+ Accept: 'application/json',
30
+ 'Content-Type': 'application/json',
31
+ Authorization: toAuthorizationHeaderValue(request.authorization),
32
+ },
33
+ body: JSON.stringify(request.body),
34
+ signal: AbortSignal.timeout(request.timeoutMs),
35
+ });
36
+ }
37
+ catch (error) {
38
+ const timedOut = error instanceof Error && error.name === 'TimeoutError';
39
+ throw unavailable(timedOut
40
+ ? `Semantic search did not answer within ${request.timeoutMs} ms`
41
+ : `Semantic search service unreachable: ${error instanceof Error ? error.message : String(error)}`, error);
42
+ }
43
+ let payload;
44
+ try {
45
+ payload = JSON.parse(await response.text());
46
+ }
47
+ catch {
48
+ payload = null;
49
+ }
50
+ const code = typeof payload?.code === 'string' ? payload.code : null;
51
+ const message = typeof payload?.msg === 'string' ? payload.msg.trim() : '';
52
+ if (code === RAGX_OK && response.ok)
53
+ return (payload?.data ?? null);
54
+ if (response.status === 401 || response.status === 403 || (code === RAGX_INVALID && /authorization|userNo|token/i.test(message))) {
55
+ throw new DinoxError('The semantic search service rejected the login token.', {
56
+ code: 'AUTH_INVALID',
57
+ recoverable: true,
58
+ suggestion: 'Run `dino auth status --format json`; if the token expired, log in again with `dino auth login`.',
59
+ });
60
+ }
61
+ if (code === RAGX_INVALID) {
62
+ throw new DinoxError(`Semantic search rejected the request: ${message || 'invalid request'}`, {
63
+ code: 'INVALID_ARGUMENT',
64
+ recoverable: true,
65
+ });
66
+ }
67
+ if (code === RAGX_NOT_FOUND) {
68
+ throw new DinoxError(message || 'The semantic search target was not found.', { code: 'NOT_FOUND', recoverable: true });
69
+ }
70
+ throw unavailable(`Semantic search failed (HTTP ${response.status}${code ? `, code ${code}` : ''})${message ? `: ${message}` : ''}`);
71
+ }
package/dist/cli.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import { registerAuthCommands } from './commands/auth/index.js';
3
+ import { parseCommanderUsageError } from './utils/usageError.js';
4
+ import { registerBatchCommand } from './commands/batch.js';
5
+ import { registerMcpCommand } from './commands/mcp.js';
3
6
  import { registerBoxesCommand } from './commands/boxes/index.js';
4
7
  import { registerConfigCommands } from './commands/config/index.js';
5
8
  import { registerDaemonCommands } from './daemon/index.js';
@@ -22,8 +25,17 @@ import { registerTemplateCommands } from './commands/templates/index.js';
22
25
  import { registerTodoCommands } from './commands/todo/index.js';
23
26
  import { getPackageVersion } from './utils/version.js';
24
27
  import { validateRequestId } from './requests/store.js';
28
+ import { parseFieldSelection } from './utils/select.js';
29
+ import { setProgressReporting } from './utils/progress.js';
25
30
  import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
26
- import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
31
+ import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedFieldSelection, setRequestedStructuredOutputFormat, } from './utils/output.js';
32
+ /** `note.export` for `dino note export`. */
33
+ function commandPath(command) {
34
+ const names = [];
35
+ for (let current = command; current?.parent; current = current.parent)
36
+ names.unshift(current.name());
37
+ return names.join('.');
38
+ }
27
39
  export function createCli() {
28
40
  clearRequestedStructuredOutputFormat();
29
41
  const program = new Command();
@@ -55,10 +67,14 @@ export function createCli() {
55
67
  return;
56
68
  }
57
69
  structuredCommanderErrorWritten = true;
58
- const message = text.trim().replace(/^error:\s*/i, '');
59
- const error = new DinoxError(message, {
70
+ const args = program.rawArgs?.slice(2) ?? [];
71
+ const usage = parseCommanderUsageError(text, args, program);
72
+ const error = new DinoxError(usage.message, {
60
73
  code: 'USAGE_ERROR',
61
74
  recoverable: true,
75
+ details: usage.details,
76
+ suggestion: usage.suggestion,
77
+ suggestedAction: usage.suggestedAction,
62
78
  });
63
79
  printError(buildStructuredErrorPayload(error, false), format);
64
80
  },
@@ -71,15 +87,22 @@ export function createCli() {
71
87
  .option('--require-sync', 'fail if the local PowerSync cache cannot be confirmed fresh before reading')
72
88
  .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
73
89
  .option('--request-id <id>', 'idempotency key: replays the stored result instead of running the command again (24h)', validateRequestId)
90
+ .option('--select <fields>', 'keep only these comma-separated output fields (dot paths, e.g. notes.id,notes.title)')
91
+ .option('--progress', 'report progress of long-running commands (sync, note export) on stderr')
74
92
  .option('--verbose', 'enable verbose logging');
75
93
  program.hook('preAction', (_thisCommand, actionCommand) => {
76
94
  const options = actionCommand.optsWithGlobals?.() ?? {};
77
95
  setRequestedStructuredOutputFormat(resolveStructuredOutputFormat(options));
96
+ setRequestedFieldSelection(options.select === undefined ? null : parseFieldSelection(options.select));
97
+ setProgressReporting(options.progress === true ? commandPath(actionCommand) : null);
78
98
  });
79
99
  program.hook('postAction', () => {
80
100
  clearRequestedStructuredOutputFormat();
101
+ setRequestedFieldSelection(null);
102
+ setProgressReporting(null);
81
103
  });
82
104
  registerAuthCommands(program);
105
+ registerBatchCommand(program);
83
106
  registerBoxesCommand(program);
84
107
  registerConfigCommands(program);
85
108
  registerDaemonCommands(program);
@@ -87,6 +110,7 @@ export function createCli() {
87
110
  registerDoctorCommand(program);
88
111
  registerGraphCommands(program);
89
112
  registerInfoCommand(program);
113
+ registerMcpCommand(program);
90
114
  registerMetaCommands(program);
91
115
  registerSyncCommand(program);
92
116
  registerUpdateCommand(program);
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandExecuteResult } from '../daemon/types.js';
3
+ import { executeCommandLine } from '../requests/execute.js';
4
+ import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
5
+ export declare const MAX_BATCH_ITEMS = 100;
6
+ export type BatchItem = {
7
+ index: number;
8
+ id: string | null;
9
+ args: string[];
10
+ };
11
+ export type BatchItemResult = {
12
+ index: number;
13
+ id: string | null;
14
+ args: string[];
15
+ ok: boolean;
16
+ exit_code: number;
17
+ data?: unknown;
18
+ notice?: unknown;
19
+ error?: unknown;
20
+ };
21
+ /** Validates the batch document: argv arrays, or `{ id?, args, request_id? }` objects. */
22
+ export declare function parseBatchItems(text: string): BatchItem[];
23
+ /** Adds the batch's own sync options to an item that does not set them itself. */
24
+ export declare function itemArgv(args: string[], globals: Record<string, unknown>): string[];
25
+ export declare function toBatchItemResult(item: BatchItem, result: CommandExecuteResult): BatchItemResult;
26
+ declare function readStdin(): Promise<string>;
27
+ declare const batchDependencies: {
28
+ readStdin: typeof readStdin;
29
+ readFile: (file: string) => Promise<string>;
30
+ execute: typeof executeCommandLine;
31
+ };
32
+ export declare function runBatch(context: CommandExecutionContext, overrides?: Partial<typeof batchDependencies>): Promise<void>;
33
+ export declare const batchCommandDescriptor: CommandDescriptor;
34
+ export declare function registerBatchCommand(program: Command): void;
35
+ export {};
@@ -0,0 +1,202 @@
1
+ import fs from 'node:fs/promises';
2
+ import { commandExecutionTarget } from '../daemon/forward.js';
3
+ import { executeCommandLine } from '../requests/execute.js';
4
+ import { validateRequestId } from '../requests/store.js';
5
+ import { DinoxError } from '../utils/errors.js';
6
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
7
+ import { assertStdinAvailable, resolveUserPath } from '../utils/requestContext.js';
8
+ import { registerStandaloneCommand } from './descriptor/register.js';
9
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
10
+ export const MAX_BATCH_ITEMS = 100;
11
+ /** Options each item may not set: the batch owns output format and help. */
12
+ const RESERVED_ITEM_OPTIONS = ['--format', '--json', '--help', '-h', '--version', '-V'];
13
+ function invalidInput(message, details) {
14
+ return new DinoxError(message, {
15
+ code: 'INVALID_ARGUMENT',
16
+ recoverable: true,
17
+ details,
18
+ suggestion: 'Pass a JSON array whose items are argument arrays, e.g. [["note","search","AI"],{"id":"t1","args":["todo","search"]}].',
19
+ });
20
+ }
21
+ /** Validates the batch document: argv arrays, or `{ id?, args, request_id? }` objects. */
22
+ export function parseBatchItems(text) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(text);
26
+ }
27
+ catch (error) {
28
+ throw invalidInput(`Batch input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ if (!Array.isArray(parsed) || parsed.length === 0)
31
+ throw invalidInput('Batch input must be a non-empty JSON array');
32
+ if (parsed.length > MAX_BATCH_ITEMS)
33
+ throw invalidInput(`Batch input has ${parsed.length} items; the limit is ${MAX_BATCH_ITEMS}`);
34
+ return parsed.map((entry, index) => {
35
+ const record = Array.isArray(entry) ? { args: entry } : entry;
36
+ const args = record && typeof record === 'object' ? record.args : undefined;
37
+ if (!Array.isArray(args) || args.length === 0 || !args.every((arg) => typeof arg === 'string')) {
38
+ throw invalidInput(`Batch item ${index} must be a non-empty array of strings or an object with "args"`, { index });
39
+ }
40
+ const id = record.id;
41
+ if (id !== undefined && typeof id !== 'string')
42
+ throw invalidInput(`Batch item ${index} has a non-string "id"`, { index });
43
+ const reserved = args.find((arg) => RESERVED_ITEM_OPTIONS.includes(arg) || arg.startsWith('--format='));
44
+ if (reserved)
45
+ throw invalidInput(`Batch item ${index} may not set ${reserved}; the batch returns JSON for every item`, { index });
46
+ if (commandExecutionTarget(args) !== 'daemon') {
47
+ throw invalidInput(`Batch item ${index} (${args.join(' ')}) is not a data command; only note, todo, tag, box, view, prop, template, prompt, graph, storage and meta commands can be batched`, { index });
48
+ }
49
+ const requestId = record.request_id;
50
+ if (requestId !== undefined) {
51
+ if (typeof requestId !== 'string')
52
+ throw invalidInput(`Batch item ${index} has a non-string "request_id"`, { index });
53
+ validateRequestId(requestId);
54
+ if (args.includes('--request-id'))
55
+ throw invalidInput(`Batch item ${index} sets both "request_id" and --request-id`, { index });
56
+ }
57
+ return {
58
+ index,
59
+ id: typeof id === 'string' ? id : null,
60
+ args: requestId === undefined ? [...args] : [...args, '--request-id', requestId],
61
+ };
62
+ });
63
+ }
64
+ /** Adds the batch's own sync options to an item that does not set them itself. */
65
+ export function itemArgv(args, globals) {
66
+ const argv = [...args];
67
+ if (globals.offline && !argv.includes('--offline'))
68
+ argv.push('--offline');
69
+ if (globals.requireSync && !argv.includes('--require-sync'))
70
+ argv.push('--require-sync');
71
+ if (typeof globals.syncTimeout === 'string' && !argv.some((arg) => arg === '--sync-timeout' || arg.startsWith('--sync-timeout='))) {
72
+ argv.push('--sync-timeout', globals.syncTimeout);
73
+ }
74
+ return [...argv, '--format', 'json'];
75
+ }
76
+ /** Reads the JSON document a command printed, skipping any plain-text lines before it. */
77
+ function parseJsonOutput(text) {
78
+ const start = text.search(/^[{[]/m);
79
+ if (start === -1)
80
+ return null;
81
+ try {
82
+ const value = JSON.parse(text.slice(start));
83
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ export function toBatchItemResult(item, result) {
90
+ const base = { index: item.index, id: item.id, args: item.args, exit_code: result.exitCode };
91
+ if (result.exitCode === 0) {
92
+ const envelope = parseJsonOutput(result.stdout);
93
+ return {
94
+ ...base,
95
+ ok: true,
96
+ data: envelope && 'data' in envelope ? envelope.data : (envelope ?? result.stdout),
97
+ ...(envelope?._notice ? { notice: envelope._notice } : {}),
98
+ };
99
+ }
100
+ const payload = parseJsonOutput(result.stderr);
101
+ return {
102
+ ...base,
103
+ ok: false,
104
+ error: payload?.error ?? payload ?? { code: 'COMMAND_FAILED', message: result.stderr.trim() || `exit code ${result.exitCode}` },
105
+ };
106
+ }
107
+ function errorResult(item, error) {
108
+ const dinox = error instanceof DinoxError ? error : null;
109
+ return {
110
+ index: item.index,
111
+ id: item.id,
112
+ args: item.args,
113
+ ok: false,
114
+ exit_code: dinox?.exitCode ?? 1,
115
+ error: {
116
+ code: dinox?.code ?? 'INTERNAL_ERROR',
117
+ message: error instanceof Error ? error.message : String(error),
118
+ ...(dinox?.suggestion ? { suggestion: dinox.suggestion } : {}),
119
+ ...(dinox?.details !== undefined ? { details: dinox.details } : {}),
120
+ },
121
+ };
122
+ }
123
+ async function readStdin() {
124
+ assertStdinAvailable();
125
+ if (process.stdin.isTTY) {
126
+ throw invalidInput('dino batch reads a JSON array from stdin; pipe it in or pass --input <file>');
127
+ }
128
+ const chunks = [];
129
+ for await (const chunk of process.stdin)
130
+ chunks.push(Buffer.from(chunk));
131
+ return Buffer.concat(chunks).toString('utf8');
132
+ }
133
+ const batchDependencies = {
134
+ readStdin,
135
+ readFile: (file) => fs.readFile(resolveUserPath(file), 'utf8'),
136
+ execute: executeCommandLine,
137
+ };
138
+ export async function runBatch(context, overrides = {}) {
139
+ const deps = { ...batchDependencies, ...overrides };
140
+ const input = typeof context.options.input === 'string' && context.options.input !== '-'
141
+ ? await deps.readFile(context.options.input)
142
+ : await deps.readStdin();
143
+ const items = parseBatchItems(input);
144
+ const stopOnError = Boolean(context.options.stopOnError);
145
+ const results = [];
146
+ for (const item of items) {
147
+ let result;
148
+ try {
149
+ result = toBatchItemResult(item, await deps.execute(itemArgv(item.args, context.globals)));
150
+ }
151
+ catch (error) {
152
+ result = errorResult(item, error);
153
+ }
154
+ results.push(result);
155
+ if (!result.ok && stopOnError)
156
+ break;
157
+ }
158
+ const failed = results.filter((result) => !result.ok).length;
159
+ const summary = {
160
+ total: items.length,
161
+ succeeded: results.length - failed,
162
+ failed,
163
+ skipped: items.length - results.length,
164
+ };
165
+ if (getRequestedStructuredOutputFormat()) {
166
+ printSuccess({ ok: failed === 0, summary, results });
167
+ return;
168
+ }
169
+ for (const result of results) {
170
+ const label = result.id ? `${result.id} ` : '';
171
+ const error = result.error;
172
+ printHumanReadableLine(`#${result.index} ${label}${result.ok ? 'ok' : `failed (${error?.code ?? 'ERROR'}: ${error?.message ?? ''})`}\t${result.args.join(' ')}`);
173
+ }
174
+ printHumanReadableLine(`${summary.succeeded} succeeded, ${summary.failed} failed, ${summary.skipped} skipped of ${summary.total}.`);
175
+ }
176
+ export const batchCommandDescriptor = {
177
+ name: 'batch',
178
+ description: 'Run several data commands from one JSON array on stdin or a file and return every result',
179
+ options: [
180
+ { flags: '--input <file>', description: 'Read the JSON array from this file instead of stdin (- for stdin)' },
181
+ { flags: '--stop-on-error', description: 'Stop at the first failed item; later items are reported as skipped', type: 'boolean' },
182
+ ],
183
+ schema: {
184
+ path: 'batch',
185
+ risk: 'write',
186
+ auth: 'required',
187
+ globalOptions: [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS],
188
+ output: {
189
+ type: 'structured',
190
+ summary: 'Per-item results in input order with a success summary; each item carries the command\'s own data or structured error',
191
+ fields: ['ok', 'summary', 'results'],
192
+ },
193
+ examples: [
194
+ 'echo \'[["note","search","AI","--limit","5"],["todo","search","--status","uncompleted"]]\' | dino batch --format json',
195
+ 'dino batch --input ops.json --stop-on-error --format json',
196
+ ],
197
+ },
198
+ action: (context) => runBatch(context),
199
+ };
200
+ export function registerBatchCommand(program) {
201
+ registerStandaloneCommand(program, batchCommandDescriptor);
202
+ }
@@ -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',
@@ -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
+ }