@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.
- package/dist/ai/ragxClient.d.ts +15 -0
- package/dist/ai/ragxClient.js +71 -0
- package/dist/cli.js +31 -4
- package/dist/commands/auth/index.d.ts +2 -1
- package/dist/commands/auth/index.js +18 -1
- 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/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +20 -1
- package/dist/commands/graph/index.js +2 -1
- 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/contentPatch.js +2 -1
- 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 +7 -2
- 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/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/savedViews/index.js +12 -3
- package/dist/commands/schema/globalOptions.d.ts +7 -0
- package/dist/commands/schema/globalOptions.js +7 -0
- package/dist/commands/schema/renderSkills.js +6 -2
- package/dist/commands/schema/spec.js +20 -1
- package/dist/commands/storage/index.js +7 -5
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +27 -8
- 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 +53 -3
- package/dist/daemon/connectivity.d.ts +20 -0
- package/dist/daemon/connectivity.js +42 -0
- package/dist/daemon/ensure.js +48 -12
- package/dist/daemon/forward.d.ts +37 -0
- package/dist/daemon/forward.js +155 -0
- package/dist/daemon/fresh.d.ts +4 -0
- package/dist/daemon/fresh.js +10 -0
- package/dist/daemon/index.d.ts +11 -6
- package/dist/daemon/index.js +90 -25
- package/dist/daemon/launch.d.ts +13 -1
- package/dist/daemon/launch.js +31 -2
- package/dist/daemon/lifecycle.d.ts +3 -1
- package/dist/daemon/lifecycle.js +27 -3
- package/dist/daemon/pid.d.ts +8 -0
- package/dist/daemon/pid.js +20 -0
- package/dist/daemon/route.js +2 -1
- package/dist/daemon/server.d.ts +45 -1
- package/dist/daemon/server.js +192 -4
- package/dist/daemon/supervise.d.ts +6 -6
- package/dist/daemon/supervise.js +12 -10
- package/dist/daemon/supervisor.d.ts +21 -5
- package/dist/daemon/supervisor.js +125 -6
- package/dist/daemon/types.d.ts +22 -0
- package/dist/dinox.js +43 -64
- package/dist/gateway/deviceHost.js +2 -1
- package/dist/gateway/mcpCommandTools.d.ts +24 -5
- package/dist/gateway/mcpCommandTools.js +70 -29
- 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/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- 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/requests/store.d.ts +10 -0
- package/dist/requests/store.js +190 -0
- package/dist/runCli.d.ts +2 -0
- package/dist/runCli.js +64 -0
- package/dist/skills/commandRefs.js +2 -10
- package/dist/utils/argValue.js +2 -1
- 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 +24 -0
- package/dist/utils/errorCodes.js +6 -0
- package/dist/utils/output.d.ts +4 -0
- package/dist/utils/output.js +72 -10
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +59 -0
- package/dist/utils/requestContext.d.ts +32 -0
- package/dist/utils/requestContext.js +13 -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 +2 -1
|
@@ -4,7 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { sendDaemonRequest } from '../daemon/client.js';
|
|
7
|
-
import { DINOX_MCP_SERVER_NAME, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from './mcpEnv.js';
|
|
7
|
+
import { DINOX_MCP_SERVER_NAME, DINOX_STANDALONE_MCP_SERVER_NAME, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from './mcpEnv.js';
|
|
8
8
|
import { READ_CONVERSATION_MAX_LIMIT } from './conversationContext.js';
|
|
9
9
|
import { getPackageVersion } from '../utils/version.js';
|
|
10
10
|
/**
|
|
@@ -183,11 +183,31 @@ function registerKnowledgeTools(server, call) {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
+
const STANDALONE_INSTRUCTIONS = "Tools for the user's Dinox knowledge base (notes, todos, tags, card boxes, views, properties, templates, prompts, " +
|
|
187
|
+
'the link graph and storage). Each dinox_* tool groups the CLI commands of one area: pick an `action` and pass its ' +
|
|
188
|
+
'camelCase `input`. Prefer note search for keywords and note semantic/context for meaning-based questions. Results ' +
|
|
189
|
+
'carry `_notice` (stale cache, pending upload) worth relaying. High-risk writes need dryRun: true first, then the ' +
|
|
190
|
+
'confirmation options the action lists.';
|
|
191
|
+
/**
|
|
192
|
+
* The Dinox MCP server for any MCP client (`dino mcp`). Only the generated
|
|
193
|
+
* command tools: they reach the daemon when it is available and otherwise run
|
|
194
|
+
* the CLI in a child process, so no daemon is required.
|
|
195
|
+
*/
|
|
196
|
+
export function createStandaloneMcpServer(options = {}) {
|
|
197
|
+
const server = new McpServer({ name: DINOX_STANDALONE_MCP_SERVER_NAME, version: getPackageVersion() }, { instructions: STANDALONE_INSTRUCTIONS + (options.readOnly ? ' This server is read-only.' : '') });
|
|
198
|
+
registerCommandTools(server, options.commandRunner, { ...options, knowledgeTools: false });
|
|
199
|
+
return server;
|
|
200
|
+
}
|
|
201
|
+
async function serveOverStdio(server) {
|
|
202
|
+
await server.connect(new StdioServerTransport());
|
|
203
|
+
await new Promise((resolve) => process.stdin.once('close', resolve));
|
|
204
|
+
}
|
|
186
205
|
export async function serveDinoxMcpOverStdio() {
|
|
187
|
-
|
|
206
|
+
await serveOverStdio(createDinoxMcpServer(undefined, {
|
|
188
207
|
knowledgeTools: process.env[GATEWAY_KNOWLEDGE_TOOLS_ENV] !== '0',
|
|
189
208
|
sessionToken: process.env[GATEWAY_SESSION_TOKEN_ENV]?.trim() || undefined,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
export async function serveStandaloneMcpOverStdio(options) {
|
|
212
|
+
await serveOverStdio(createStandaloneMcpServer(options));
|
|
193
213
|
}
|
|
@@ -7,6 +7,8 @@ export type ConnectResult = {
|
|
|
7
7
|
dbPath: string;
|
|
8
8
|
stale: boolean;
|
|
9
9
|
releaseOwner: () => Promise<void>;
|
|
10
|
+
/** Drops the sync stream and connects again (online connections only). */
|
|
11
|
+
reconnect?: () => Promise<void>;
|
|
10
12
|
};
|
|
11
13
|
type ClosablePowerSyncDatabase = {
|
|
12
14
|
close: () => Promise<void>;
|
|
@@ -215,9 +215,11 @@ export async function connectPowerSync(args) {
|
|
|
215
215
|
});
|
|
216
216
|
}, uploader);
|
|
217
217
|
await db.connect(connector);
|
|
218
|
+
// connect() disconnects an existing stream first.
|
|
219
|
+
const reconnect = () => db.connect(connector);
|
|
218
220
|
if (args.waitForInitialSync === false) {
|
|
219
221
|
const status = getStatusSnapshot(db);
|
|
220
|
-
return { db, dbPath, stale: !status.connected || !status.hasSynced, releaseOwner };
|
|
222
|
+
return { db, dbPath, stale: !status.connected || !status.hasSynced, releaseOwner, reconnect };
|
|
221
223
|
}
|
|
222
224
|
const { signal, cancel } = createTimeoutSignal(args.timeoutMs);
|
|
223
225
|
try {
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { createTimeoutSignal } from '../utils/abort.js';
|
|
2
|
+
import { reportProgress } from '../utils/progress.js';
|
|
2
3
|
import { getStatusSnapshot, waitForDownloadIdle } from './runtime.js';
|
|
3
4
|
import { syncNoteTokenIndex } from './tokenIndex.js';
|
|
4
5
|
export async function runSingleSyncPass(db, timeoutMs) {
|
|
5
6
|
const deadline = Date.now() + Math.max(1, timeoutMs);
|
|
6
7
|
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
7
|
-
|
|
8
|
+
// PowerSync reports operation counts while a checkpoint downloads.
|
|
9
|
+
const stopDownloadProgress = db.registerListener({
|
|
10
|
+
statusChanged: (status) => {
|
|
11
|
+
const progress = status.downloadProgress;
|
|
12
|
+
if (progress)
|
|
13
|
+
reportProgress({ phase: 'download', current: progress.downloadedOperations, total: progress.totalOperations });
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
reportProgress({ phase: 'download' });
|
|
17
|
+
const downloadIdle = await waitForDownloadIdle(db, Math.max(1, Math.min(remainingMs(), 60_000)))
|
|
18
|
+
.finally(stopDownloadProgress);
|
|
8
19
|
const indexTimeoutMs = remainingMs();
|
|
9
20
|
const timeout = indexTimeoutMs > 0 ? createTimeoutSignal(indexTimeoutMs) : null;
|
|
10
21
|
const tokenIndex = await syncNoteTokenIndex(db, {
|
|
11
22
|
signal: timeout?.signal ?? AbortSignal.abort(),
|
|
23
|
+
onBatch: (progress) => reportProgress({ phase: 'index', current: progress.scanned }),
|
|
12
24
|
}).finally(() => timeout?.cancel());
|
|
13
25
|
const status = getStatusSnapshot(db);
|
|
14
26
|
const uploadIdle = !status.dataFlowStatus.uploading;
|
|
@@ -25,6 +25,8 @@ export declare function syncNoteTokenIndex(db: AbstractPowerSyncDatabase, option
|
|
|
25
25
|
nowIso?: () => string;
|
|
26
26
|
tokenize?: (text: string, limit?: number) => string[];
|
|
27
27
|
signal?: AbortSignal;
|
|
28
|
+
/** Called after each batch with the running counters. */
|
|
29
|
+
onBatch?: (result: NoteTokenIndexResult) => void;
|
|
28
30
|
}): Promise<NoteTokenIndexResult>;
|
|
29
31
|
export declare function inspectNoteTokenIndex(db: AbstractPowerSyncDatabase): Promise<NoteTokenIndexInspection>;
|
|
30
32
|
export declare function rebuildNoteTokenIndex(db: AbstractPowerSyncDatabase, options?: Parameters<typeof syncNoteTokenIndex>[1]): Promise<NoteTokenIndexResult>;
|
|
@@ -67,6 +67,7 @@ export async function syncNoteTokenIndex(db, options) {
|
|
|
67
67
|
}
|
|
68
68
|
await updateIndexCursor(db, cursorUpdatedAt, cursorId, nowIso);
|
|
69
69
|
result.cursor = { updatedAt: cursorUpdatedAt, id: cursorId };
|
|
70
|
+
options?.onBatch?.(result);
|
|
70
71
|
}
|
|
71
72
|
result.complete = await reconcileSkippedRows(db, result, batchSize, nowIso, tokenize, signal);
|
|
72
73
|
result.cursor = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CommandExecuteResult } from '../daemon/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs a command line in this process with its stdout/stderr captured instead
|
|
4
|
+
* of written, the same way the daemon runs a forwarded command.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runCapturedInProcess(argv: string[], onFatalError?: (error: unknown) => void): Promise<CommandExecuteResult>;
|
|
7
|
+
/**
|
|
8
|
+
* Runs one data command and returns its captured output: inside the daemon
|
|
9
|
+
* when it is available, otherwise in this process (honoring `--request-id`).
|
|
10
|
+
*/
|
|
11
|
+
export declare function executeCommandLine(argv: string[]): Promise<CommandExecuteResult>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { tryForwardToDaemon } from '../daemon/forward.js';
|
|
2
|
+
import { localCommandExecution, requestContext } from '../utils/requestContext.js';
|
|
3
|
+
import { requestIdFromArgv, runWithRequestId } from './store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Runs a command line in this process with its stdout/stderr captured instead
|
|
6
|
+
* of written, the same way the daemon runs a forwarded command.
|
|
7
|
+
*/
|
|
8
|
+
export async function runCapturedInProcess(argv, onFatalError) {
|
|
9
|
+
const { runCli } = await import('../runCli.js');
|
|
10
|
+
const context = {
|
|
11
|
+
stdout: [], stderr: [], format: null, noticeProvider: null, cwd: process.cwd(),
|
|
12
|
+
};
|
|
13
|
+
const exitCode = await requestContext.run(context, () => localCommandExecution.run(true, () => runCli(argv, onFatalError)));
|
|
14
|
+
return { stdout: context.stdout.join(''), stderr: context.stderr.join(''), exitCode };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs one data command and returns its captured output: inside the daemon
|
|
18
|
+
* when it is available, otherwise in this process (honoring `--request-id`).
|
|
19
|
+
*/
|
|
20
|
+
export async function executeCommandLine(argv) {
|
|
21
|
+
const forwarded = await tryForwardToDaemon(argv);
|
|
22
|
+
if (forwarded)
|
|
23
|
+
return forwarded;
|
|
24
|
+
const key = requestIdFromArgv(argv);
|
|
25
|
+
return key ? runWithRequestId(key, argv, () => runCapturedInProcess(argv)) : runCapturedInProcess(argv);
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandExecuteResult } from '../daemon/types.js';
|
|
2
|
+
export declare const AUTO_REQUEST_ID_PREFIX = "auto:";
|
|
3
|
+
export declare function validateRequestId(key: string): string;
|
|
4
|
+
/** Reads only options before the argument terminator. */
|
|
5
|
+
export declare function requestIdFromArgv(argv: string[]): string | undefined;
|
|
6
|
+
/** Claims once across processes and preserves the complete output for replay. */
|
|
7
|
+
export declare function runWithRequestId(key: string, argv: string[], run: () => Promise<CommandExecuteResult>, opts?: {
|
|
8
|
+
waitTimeoutMs?: number;
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
}): Promise<CommandExecuteResult>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { getDataDir } from '../config/paths.js';
|
|
6
|
+
import { DinoxError } from '../utils/errors.js';
|
|
7
|
+
const RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
/** Ids the CLI generates only cover its own single retry; their output need not linger. */
|
|
9
|
+
const AUTO_RETENTION_MS = 10 * 60 * 1000;
|
|
10
|
+
export const AUTO_REQUEST_ID_PREFIX = 'auto:';
|
|
11
|
+
const instanceId = randomUUID();
|
|
12
|
+
const cleanedDirs = new Set();
|
|
13
|
+
export function validateRequestId(key) {
|
|
14
|
+
if (typeof key !== 'string' || key.length < 1 || key.length > 128 || /[^A-Za-z0-9._:-]/.test(key)) {
|
|
15
|
+
throw new DinoxError('Request id must contain 1–128 characters from A-Z, a-z, 0-9, ., _, :, -.', { code: 'INVALID_ARGUMENT' });
|
|
16
|
+
}
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
/** Reads only options before the argument terminator. */
|
|
20
|
+
export function requestIdFromArgv(argv) {
|
|
21
|
+
let key;
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
if (argv[i] === '--')
|
|
24
|
+
break;
|
|
25
|
+
if (argv[i] === '--request-id')
|
|
26
|
+
key = validateRequestId(argv[++i] ?? '');
|
|
27
|
+
else if (argv[i].startsWith('--request-id='))
|
|
28
|
+
key = validateRequestId(argv[i].slice(13));
|
|
29
|
+
}
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
function hash(value) {
|
|
33
|
+
return createHash('sha256').update(value).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
function argvHash(argv) {
|
|
36
|
+
const filtered = [];
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
if (argv[i] === '--') {
|
|
39
|
+
filtered.push(...argv.slice(i));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (argv[i] === '--request-id') {
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (argv[i].startsWith('--request-id='))
|
|
47
|
+
continue;
|
|
48
|
+
filtered.push(argv[i]);
|
|
49
|
+
}
|
|
50
|
+
return hash(JSON.stringify(filtered));
|
|
51
|
+
}
|
|
52
|
+
function ownerAlive(pid) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return error.code !== 'ESRCH';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function cleanup(dir) {
|
|
62
|
+
if (cleanedDirs.has(dir))
|
|
63
|
+
return;
|
|
64
|
+
cleanedDirs.add(dir);
|
|
65
|
+
const lockPath = path.join(dir, '.cleanup.lock');
|
|
66
|
+
let lock;
|
|
67
|
+
try {
|
|
68
|
+
lock = await fs.open(lockPath, 'wx', 0o600);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code === 'EEXIST')
|
|
72
|
+
return;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
for (const name of await fs.readdir(dir)) {
|
|
77
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name))
|
|
78
|
+
continue;
|
|
79
|
+
const file = path.join(dir, name);
|
|
80
|
+
try {
|
|
81
|
+
const age = Date.now() - (await fs.stat(file)).mtimeMs;
|
|
82
|
+
if (age <= AUTO_RETENTION_MS)
|
|
83
|
+
continue;
|
|
84
|
+
const record = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
85
|
+
if (!record.auto && age <= RETENTION_MS)
|
|
86
|
+
continue;
|
|
87
|
+
// Never expire a command that is still executing, even after 24 hours.
|
|
88
|
+
if (record.state === 'running' && ownerAlive(record.pid))
|
|
89
|
+
continue;
|
|
90
|
+
await fs.unlink(file);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Cleanup is best effort; another process may already have removed it.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await lock.close();
|
|
99
|
+
await fs.rm(lockPath, { force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Claims once across processes and preserves the complete output for replay. */
|
|
103
|
+
export async function runWithRequestId(key, argv, run, opts = {}) {
|
|
104
|
+
validateRequestId(key);
|
|
105
|
+
const dir = path.join(getDataDir(), 'requests');
|
|
106
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
107
|
+
await fs.chmod(dir, 0o700);
|
|
108
|
+
await cleanup(dir);
|
|
109
|
+
const file = path.join(dir, `${hash(key)}.json`);
|
|
110
|
+
const record = {
|
|
111
|
+
version: 1, argvHash: argvHash(argv), state: 'running', auto: key.startsWith(AUTO_REQUEST_ID_PREFIX), pid: process.pid,
|
|
112
|
+
instanceId, startedAt: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
const deadline = Date.now() + (opts.waitTimeoutMs ?? 300_000);
|
|
115
|
+
try {
|
|
116
|
+
const handle = await fs.open(file, 'wx', 0o600);
|
|
117
|
+
try {
|
|
118
|
+
await handle.writeFile(JSON.stringify(record));
|
|
119
|
+
await handle.sync();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await handle.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== 'EEXIST')
|
|
127
|
+
throw error;
|
|
128
|
+
while (true) {
|
|
129
|
+
let previous;
|
|
130
|
+
try {
|
|
131
|
+
previous = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// A claimant may still be writing, or may have rejected and removed its claim.
|
|
135
|
+
if (error.code === 'ENOENT') {
|
|
136
|
+
return runWithRequestId(key, argv, run, { ...opts, waitTimeoutMs: Math.max(0, deadline - Date.now()) });
|
|
137
|
+
}
|
|
138
|
+
if (!(error instanceof SyntaxError))
|
|
139
|
+
throw error;
|
|
140
|
+
if (Date.now() >= deadline) {
|
|
141
|
+
throw new DinoxError('Request claim is not readable yet.', { code: 'REQUEST_IN_PROGRESS', recoverable: true });
|
|
142
|
+
}
|
|
143
|
+
await delay(opts.pollIntervalMs ?? 100);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (previous.argvHash !== record.argvHash) {
|
|
147
|
+
throw new DinoxError('Request id was used with different arguments.', { code: 'REQUEST_ID_CONFLICT' });
|
|
148
|
+
}
|
|
149
|
+
if (previous.state === 'done' && previous.result)
|
|
150
|
+
return previous.result;
|
|
151
|
+
if (!ownerAlive(previous.pid)) {
|
|
152
|
+
throw new DinoxError('The earlier request stopped; its outcome is unknown.', {
|
|
153
|
+
code: 'REQUEST_INTERRUPTED',
|
|
154
|
+
suggestion: 'Verify the result, then retry with a new request id.',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (Date.now() >= deadline) {
|
|
158
|
+
throw new DinoxError('The earlier request is still running.', {
|
|
159
|
+
code: 'REQUEST_IN_PROGRESS', recoverable: true,
|
|
160
|
+
suggestion: 'Rerun the exact same command with the same request id.',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
await delay(opts.pollIntervalMs ?? 100);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let result;
|
|
167
|
+
try {
|
|
168
|
+
result = await run();
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
await fs.unlink(file);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
175
|
+
try {
|
|
176
|
+
const handle = await fs.open(temporary, 'wx', 0o600);
|
|
177
|
+
try {
|
|
178
|
+
await handle.writeFile(JSON.stringify({ ...record, state: 'done', finishedAt: Date.now(), result }));
|
|
179
|
+
await handle.sync();
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await handle.close();
|
|
183
|
+
}
|
|
184
|
+
await fs.rename(temporary, file);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await fs.rm(temporary, { force: true });
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
package/dist/runCli.d.ts
ADDED
package/dist/runCli.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createCli } from './cli.js';
|
|
2
|
+
import { buildStructuredErrorPayload, isDinoxError } from './utils/errors.js';
|
|
3
|
+
import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, printHumanReadableError, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
4
|
+
function normalizeArgv(argv) {
|
|
5
|
+
// pnpm/npm script forwarding can prepend a standalone "--".
|
|
6
|
+
if (argv[0] === '--') {
|
|
7
|
+
return argv.slice(1);
|
|
8
|
+
}
|
|
9
|
+
return argv;
|
|
10
|
+
}
|
|
11
|
+
function isVerbose(argv) {
|
|
12
|
+
return argv.includes('--verbose');
|
|
13
|
+
}
|
|
14
|
+
function isCommanderError(error) {
|
|
15
|
+
return Boolean(error)
|
|
16
|
+
&& typeof error === 'object'
|
|
17
|
+
&& typeof error.code === 'string'
|
|
18
|
+
&& error.code.startsWith('commander.')
|
|
19
|
+
&& typeof error.exitCode === 'number';
|
|
20
|
+
}
|
|
21
|
+
export async function runCli(input, onFatalError) {
|
|
22
|
+
const argv = normalizeArgv(input);
|
|
23
|
+
try {
|
|
24
|
+
await createCli().parseAsync(['node', process.argv[1] ?? 'dino', ...argv]);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
onFatalError?.(error);
|
|
29
|
+
return printCliError(error, argv);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function printCliError(error, argv) {
|
|
33
|
+
if (isCommanderError(error)) {
|
|
34
|
+
return error.exitCode === 0 ? 0 : 2;
|
|
35
|
+
}
|
|
36
|
+
let format = null;
|
|
37
|
+
try {
|
|
38
|
+
format = parseStructuredOutputFormatFromArgv(argv);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
format = null;
|
|
42
|
+
}
|
|
43
|
+
const verbose = isVerbose(argv);
|
|
44
|
+
const payload = buildStructuredErrorPayload(error, verbose);
|
|
45
|
+
const exitCode = isDinoxError(error) ? error.exitCode : 1;
|
|
46
|
+
if (format) {
|
|
47
|
+
setRequestedStructuredOutputFormat(format);
|
|
48
|
+
printError(payload, format);
|
|
49
|
+
clearRequestedStructuredOutputFormat();
|
|
50
|
+
return exitCode;
|
|
51
|
+
}
|
|
52
|
+
printHumanReadableError(payload.message);
|
|
53
|
+
if (payload.suggested_action) {
|
|
54
|
+
printHumanReadableError(`suggested action: ${payload.suggested_action.command}`);
|
|
55
|
+
printHumanReadableError(`reason: ${payload.suggested_action.reason}`);
|
|
56
|
+
}
|
|
57
|
+
else if (payload.suggestion) {
|
|
58
|
+
printHumanReadableError(`suggestion: ${payload.suggestion}`);
|
|
59
|
+
}
|
|
60
|
+
if (verbose && payload.stack) {
|
|
61
|
+
printHumanReadableError(payload.stack);
|
|
62
|
+
}
|
|
63
|
+
return exitCode;
|
|
64
|
+
}
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { GLOBAL_OPTION_NAMES } from '../commands/schema/globalOptions.js';
|
|
3
4
|
import { listCommandSchemas } from '../commands/schema/spec.js';
|
|
4
5
|
// Global flags accepted by every command (see src/cli.ts) plus Commander builtins.
|
|
5
|
-
const GLOBAL_FLAGS = new Set([
|
|
6
|
-
'--json',
|
|
7
|
-
'--format',
|
|
8
|
-
'--offline',
|
|
9
|
-
'--require-sync',
|
|
10
|
-
'--sync-timeout',
|
|
11
|
-
'--verbose',
|
|
12
|
-
'--help',
|
|
13
|
-
'--version',
|
|
14
|
-
]);
|
|
6
|
+
const GLOBAL_FLAGS = new Set([...GLOBAL_OPTION_NAMES, '--help', '--version']);
|
|
15
7
|
const SHELL_BOUNDARY = /\s(?:\||&&|\|\||;|>|2>|#)\s/;
|
|
16
8
|
function buildCommandIndex() {
|
|
17
9
|
const byCommand = new Map();
|
package/dist/utils/argValue.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import { DinoxError } from './errors.js';
|
|
3
|
+
import { resolveUserPath } from './requestContext.js';
|
|
3
4
|
export async function readStringOrFile(value) {
|
|
4
5
|
if (!value.startsWith('@')) {
|
|
5
6
|
return value;
|
|
@@ -9,7 +10,7 @@ export async function readStringOrFile(value) {
|
|
|
9
10
|
throw new DinoxError('Invalid @file syntax: expected @<path>');
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
12
|
-
return await fs.readFile(filePath, 'utf8');
|
|
13
|
+
return await fs.readFile(resolveUserPath(filePath), 'utf8');
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
16
|
throw new DinoxError(`Failed to read file: ${filePath}`, { cause: error });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function encodeCursor(command: string, offset: number, query: unknown): string;
|
|
2
|
+
/** Returns the offset a cursor points at; throws when it is malformed or belongs to another query. */
|
|
3
|
+
export declare function decodeCursor(token: string, command: string, query: unknown): number;
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the start offset from `--cursor` or `--offset`; the two are
|
|
6
|
+
* mutually exclusive (an offset of 0, the default, does not count).
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveCursorOffset(cursor: unknown, offset: number, command: string, query: unknown): number;
|
|
9
|
+
export declare function nextCursor(command: string, query: unknown, nextOffset: number, hasMore: boolean): string | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DinoxError } from './errors.js';
|
|
3
|
+
function stableJson(value) {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
6
|
+
if (value && typeof value === 'object') {
|
|
7
|
+
const entries = Object.entries(value)
|
|
8
|
+
.filter(([, item]) => item !== undefined)
|
|
9
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
10
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(',')}}`;
|
|
11
|
+
}
|
|
12
|
+
return JSON.stringify(value ?? null);
|
|
13
|
+
}
|
|
14
|
+
function queryHash(command, query) {
|
|
15
|
+
return createHash('sha256').update(`${command}\n${stableJson(query)}`).digest('base64url').slice(0, 16);
|
|
16
|
+
}
|
|
17
|
+
export function encodeCursor(command, offset, query) {
|
|
18
|
+
const payload = { v: 1, c: command, o: offset, q: queryHash(command, query) };
|
|
19
|
+
return Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
20
|
+
}
|
|
21
|
+
function invalidCursor(message) {
|
|
22
|
+
return new DinoxError(message, {
|
|
23
|
+
code: 'INVALID_ARGUMENT',
|
|
24
|
+
recoverable: true,
|
|
25
|
+
suggestion: 'Pass the nextCursor from the previous page of the same command and query, or drop --cursor to start over.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Returns the offset a cursor points at; throws when it is malformed or belongs to another query. */
|
|
29
|
+
export function decodeCursor(token, command, query) {
|
|
30
|
+
let payload;
|
|
31
|
+
try {
|
|
32
|
+
payload = JSON.parse(Buffer.from(token.trim(), 'base64url').toString('utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw invalidCursor('--cursor is not a valid pagination cursor');
|
|
36
|
+
}
|
|
37
|
+
if (!payload || payload.v !== 1 || typeof payload.o !== 'number' || !Number.isInteger(payload.o) || payload.o < 0) {
|
|
38
|
+
throw invalidCursor('--cursor is not a valid pagination cursor');
|
|
39
|
+
}
|
|
40
|
+
if (payload.c !== command || payload.q !== queryHash(command, query)) {
|
|
41
|
+
throw invalidCursor(`--cursor belongs to a different ${payload.c === command ? 'query' : 'command'}`);
|
|
42
|
+
}
|
|
43
|
+
return payload.o;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the start offset from `--cursor` or `--offset`; the two are
|
|
47
|
+
* mutually exclusive (an offset of 0, the default, does not count).
|
|
48
|
+
*/
|
|
49
|
+
export function resolveCursorOffset(cursor, offset, command, query) {
|
|
50
|
+
if (cursor === undefined || cursor === null || cursor === '')
|
|
51
|
+
return offset;
|
|
52
|
+
if (typeof cursor !== 'string')
|
|
53
|
+
throw invalidCursor('--cursor must be a string');
|
|
54
|
+
if (offset > 0) {
|
|
55
|
+
throw new DinoxError('--cursor and --offset cannot be used together', { code: 'INVALID_ARGUMENT', recoverable: true });
|
|
56
|
+
}
|
|
57
|
+
return decodeCursor(cursor, command, query);
|
|
58
|
+
}
|
|
59
|
+
export function nextCursor(command, query, nextOffset, hasMore) {
|
|
60
|
+
return hasMore ? encodeCursor(command, nextOffset, query) : null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const DEFAULT_FORMAT_ENV = "DINOX_FORMAT";
|
|
2
|
+
/**
|
|
3
|
+
* Applies DINOX_FORMAT (json, yaml, or auto: json when stdout is not a
|
|
4
|
+
* terminal) by adding `--format` when the command line has none. Agents set
|
|
5
|
+
* it once instead of passing `--format json` on every call. Help, version and
|
|
6
|
+
* protocol commands are left alone.
|
|
7
|
+
*/
|
|
8
|
+
export declare function applyDefaultFormat(argv: string[], env?: NodeJS.ProcessEnv, stdoutIsTTY?: boolean): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DinoxError } from './errors.js';
|
|
2
|
+
export const DEFAULT_FORMAT_ENV = 'DINOX_FORMAT';
|
|
3
|
+
/** Commands whose stdout is a protocol, never a command result. */
|
|
4
|
+
const PROTOCOL_COMMANDS = [['gateway', 'mcp'], ['mcp'], ['daemon', 'supervise']];
|
|
5
|
+
function commandWords(argv) {
|
|
6
|
+
return argv.filter((arg) => !arg.startsWith('-')).slice(0, 2);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Applies DINOX_FORMAT (json, yaml, or auto: json when stdout is not a
|
|
10
|
+
* terminal) by adding `--format` when the command line has none. Agents set
|
|
11
|
+
* it once instead of passing `--format json` on every call. Help, version and
|
|
12
|
+
* protocol commands are left alone.
|
|
13
|
+
*/
|
|
14
|
+
export function applyDefaultFormat(argv, env = process.env, stdoutIsTTY = Boolean(process.stdout.isTTY)) {
|
|
15
|
+
const raw = env[DEFAULT_FORMAT_ENV]?.trim().toLowerCase();
|
|
16
|
+
if (!raw)
|
|
17
|
+
return argv;
|
|
18
|
+
if (raw !== 'json' && raw !== 'yaml' && raw !== 'auto') {
|
|
19
|
+
throw new DinoxError(`${DEFAULT_FORMAT_ENV} must be one of: json, yaml, auto`, {
|
|
20
|
+
code: 'INVALID_ARGUMENT',
|
|
21
|
+
recoverable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const format = raw === 'auto' ? (stdoutIsTTY ? null : 'json') : raw;
|
|
25
|
+
if (!format || argv.length === 0)
|
|
26
|
+
return argv;
|
|
27
|
+
const terminator = argv.indexOf('--');
|
|
28
|
+
const options = terminator === -1 ? argv : argv.slice(0, terminator);
|
|
29
|
+
const explicit = options.some((arg) => arg === '--format' || arg.startsWith('--format=') || arg === '--json');
|
|
30
|
+
const informational = options.some((arg) => ['--help', '-h', '--version', '-V'].includes(arg)) || options[0] === 'help';
|
|
31
|
+
const words = commandWords(options);
|
|
32
|
+
const protocol = PROTOCOL_COMMANDS.some((command) => command.every((word, index) => words[index] === word));
|
|
33
|
+
if (explicit || informational || protocol)
|
|
34
|
+
return argv;
|
|
35
|
+
return terminator === -1
|
|
36
|
+
? [...argv, '--format', format]
|
|
37
|
+
: [...argv.slice(0, terminator), '--format', format, ...argv.slice(terminator)];
|
|
38
|
+
}
|
|
@@ -47,6 +47,10 @@ export declare const ERROR_CODES: {
|
|
|
47
47
|
readonly exitCode: 1;
|
|
48
48
|
readonly description: "Another process is writing the CLI config.";
|
|
49
49
|
};
|
|
50
|
+
readonly DAEMON_COMMAND_NOT_FORWARDABLE: {
|
|
51
|
+
readonly exitCode: 2;
|
|
52
|
+
readonly description: "The command must run in the CLI process.";
|
|
53
|
+
};
|
|
50
54
|
readonly DAEMON_ERROR: {
|
|
51
55
|
readonly exitCode: 5;
|
|
52
56
|
readonly description: "The background daemon reported an error.";
|
|
@@ -79,6 +83,10 @@ export declare const ERROR_CODES: {
|
|
|
79
83
|
readonly exitCode: 1;
|
|
80
84
|
readonly description: "The service manager (launchd / systemd) rejected a daemon service operation.";
|
|
81
85
|
};
|
|
86
|
+
readonly DAEMON_SERVICE_FOREIGN: {
|
|
87
|
+
readonly exitCode: 1;
|
|
88
|
+
readonly description: "The daemon service runs another dino install; it was left unchanged.";
|
|
89
|
+
};
|
|
82
90
|
readonly DAEMON_SERVICE_UNSUPPORTED: {
|
|
83
91
|
readonly exitCode: 5;
|
|
84
92
|
readonly description: "The daemon cannot run as a login service on this platform or with this auth setup.";
|
|
@@ -343,6 +351,18 @@ export declare const ERROR_CODES: {
|
|
|
343
351
|
readonly exitCode: 1;
|
|
344
352
|
readonly description: "A property value does not match its definition.";
|
|
345
353
|
};
|
|
354
|
+
readonly REQUEST_ID_CONFLICT: {
|
|
355
|
+
readonly exitCode: 2;
|
|
356
|
+
readonly description: "The request id was already used for different arguments.";
|
|
357
|
+
};
|
|
358
|
+
readonly REQUEST_INTERRUPTED: {
|
|
359
|
+
readonly exitCode: 1;
|
|
360
|
+
readonly description: "The earlier request owner exited; its outcome is unknown.";
|
|
361
|
+
};
|
|
362
|
+
readonly REQUEST_IN_PROGRESS: {
|
|
363
|
+
readonly exitCode: 1;
|
|
364
|
+
readonly description: "The earlier request is still running; retry with the same id.";
|
|
365
|
+
};
|
|
346
366
|
readonly SAVED_VIEW_FILTER_CORRUPT: {
|
|
347
367
|
readonly exitCode: 1;
|
|
348
368
|
readonly description: "A stored saved view filter is corrupt.";
|
|
@@ -451,6 +471,10 @@ export declare const ERROR_CODES: {
|
|
|
451
471
|
readonly exitCode: 5;
|
|
452
472
|
readonly description: "No command schema exists for the requested path.";
|
|
453
473
|
};
|
|
474
|
+
readonly SEMANTIC_SEARCH_UNAVAILABLE: {
|
|
475
|
+
readonly exitCode: 1;
|
|
476
|
+
readonly description: "The semantic search service could not be reached or failed; keyword search (note search) still works.";
|
|
477
|
+
};
|
|
454
478
|
readonly SKILLS_LOCK_INVALID: {
|
|
455
479
|
readonly exitCode: 1;
|
|
456
480
|
readonly description: "The skills installer lock file is malformed.";
|