@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.
- package/dist/ai/ragxClient.d.ts +15 -0
- package/dist/ai/ragxClient.js +71 -0
- package/dist/cli.js +27 -3
- package/dist/commands/batch.d.ts +35 -0
- package/dist/commands/batch.js +202 -0
- package/dist/commands/boxes/index.js +21 -21
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +47 -0
- package/dist/commands/notes/actions.js +18 -2
- package/dist/commands/notes/descriptors.js +111 -5
- package/dist/commands/notes/detailContent.d.ts +19 -0
- package/dist/commands/notes/detailContent.js +62 -0
- package/dist/commands/notes/export.js +4 -0
- package/dist/commands/notes/repo/search.d.ts +2 -1
- package/dist/commands/notes/repo/search.js +1 -1
- package/dist/commands/notes/repo/semantic.d.ts +14 -0
- package/dist/commands/notes/repo/semantic.js +31 -0
- package/dist/commands/notes/searchResponse.d.ts +3 -0
- package/dist/commands/notes/searchResponse.js +1 -0
- package/dist/commands/notes/semantic.d.ts +116 -0
- package/dist/commands/notes/semantic.js +332 -0
- package/dist/commands/prompt/index.js +7 -5
- package/dist/commands/savedViews/index.js +12 -3
- package/dist/commands/schema/globalOptions.d.ts +6 -0
- package/dist/commands/schema/globalOptions.js +6 -0
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +18 -7
- package/dist/commands/storage/index.js +7 -5
- package/dist/commands/sync.js +3 -3
- package/dist/commands/tags/index.js +21 -21
- package/dist/commands/todo/index.js +22 -2
- package/dist/commands/todo/repo.d.ts +5 -0
- package/dist/commands/todo/repo.js +7 -2
- package/dist/commands/writeReceipt.d.ts +13 -0
- package/dist/commands/writeReceipt.js +12 -0
- package/dist/config/serviceEndpoints.d.ts +2 -0
- package/dist/config/serviceEndpoints.js +2 -0
- package/dist/daemon/client.d.ts +2 -0
- package/dist/daemon/client.js +22 -2
- package/dist/daemon/forward.d.ts +4 -1
- package/dist/daemon/forward.js +13 -4
- package/dist/daemon/server.d.ts +1 -1
- package/dist/daemon/server.js +17 -1
- package/dist/daemon/types.d.ts +6 -0
- package/dist/dinox.js +12 -10
- package/dist/gateway/deviceHost.js +2 -1
- package/dist/gateway/mcpCommandTools.d.ts +15 -5
- package/dist/gateway/mcpCommandTools.js +30 -19
- package/dist/gateway/mcpEnv.d.ts +2 -0
- package/dist/gateway/mcpEnv.js +2 -0
- package/dist/gateway/mcpGroups.d.ts +3 -0
- package/dist/gateway/mcpGroups.js +4 -0
- package/dist/gateway/mcpServer.d.ts +10 -1
- package/dist/gateway/mcpServer.js +25 -5
- package/dist/powersync/syncPass.js +13 -1
- package/dist/powersync/tokenIndex.d.ts +2 -0
- package/dist/powersync/tokenIndex.js +1 -0
- package/dist/requests/execute.d.ts +11 -0
- package/dist/requests/execute.js +26 -0
- package/dist/skills/commandRefs.js +2 -10
- package/dist/utils/cursor.d.ts +9 -0
- package/dist/utils/cursor.js +61 -0
- package/dist/utils/defaultFormat.d.ts +8 -0
- package/dist/utils/defaultFormat.js +38 -0
- package/dist/utils/errorCodes.d.ts +4 -0
- package/dist/utils/errorCodes.js +1 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +41 -3
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +59 -0
- package/dist/utils/requestContext.d.ts +8 -0
- package/dist/utils/select.d.ts +20 -0
- package/dist/utils/select.js +92 -0
- package/dist/utils/usageError.d.ts +37 -0
- package/dist/utils/usageError.js +127 -0
- 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
|
|
59
|
-
const
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
+
}
|