@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
|
@@ -3,6 +3,7 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { listCommandSchemas } from '../commands/schema/spec.js';
|
|
5
5
|
import { tryForwardToDaemon } from '../daemon/forward.js';
|
|
6
|
+
import { MCP_COMMAND_GROUPS } from './mcpGroups.js';
|
|
6
7
|
import { requestContext } from '../utils/requestContext.js';
|
|
7
8
|
/**
|
|
8
9
|
* One MCP tool per command group, generated from the command schemas. Each runs
|
|
@@ -10,14 +11,19 @@ import { requestContext } from '../utils/requestContext.js';
|
|
|
10
11
|
* validation, dry-run and confirmation guards. Approval belongs to the agent's
|
|
11
12
|
* permission flow, guided by the tool annotations.
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
-
export const MCP_COMMAND_GROUPS = [
|
|
15
|
-
'note', 'tag', 'box', 'todo', 'view', 'prop', 'template', 'prompt', 'graph', 'storage', 'meta.stats',
|
|
16
|
-
];
|
|
14
|
+
export { MCP_COMMAND_GROUPS };
|
|
17
15
|
/** Commands deliberately excluded from the MCP surface. */
|
|
18
16
|
export const MCP_COMMAND_EXCLUSIONS = [
|
|
19
17
|
'auth.*', 'config.*', 'daemon.*', 'gateway.*', 'doctor', 'skills.*', 'update', 'sync', 'info', 'schema', 'meta.schema',
|
|
18
|
+
// MCP clients issue several tool calls themselves; batch is the shell's equivalent.
|
|
19
|
+
'batch',
|
|
20
|
+
// The MCP server itself.
|
|
21
|
+
'mcp',
|
|
20
22
|
];
|
|
23
|
+
/** The tool name a command group is published under. */
|
|
24
|
+
export function commandToolName(group) {
|
|
25
|
+
return group === 'meta.stats' ? 'dinox_stats' : `dinox_${group}`;
|
|
26
|
+
}
|
|
21
27
|
const execute = promisify(execFile);
|
|
22
28
|
/** Runs the current CLI entry point without a shell. */
|
|
23
29
|
export function createCliCommandRunner(overrides = {}) {
|
|
@@ -169,28 +175,33 @@ export function commandArgv(schema, input) {
|
|
|
169
175
|
return [...schema.path.split('.'), ...flags, '--format', 'json', '--', ...positionals];
|
|
170
176
|
}
|
|
171
177
|
/** Builds grouped MCP tool definitions from the command schemas. */
|
|
172
|
-
export function commandToolDefinitions() {
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
export function commandToolDefinitions(options = {}) {
|
|
179
|
+
const { readOnly = false, groups = MCP_COMMAND_GROUPS, knowledgeTools = true } = options;
|
|
180
|
+
return MCP_COMMAND_GROUPS.filter((group) => groups.includes(group)).flatMap((group) => {
|
|
181
|
+
const commands = listCommandSchemas().filter((schema) => (group === 'meta.stats' ? schema.path === group : schema.path.startsWith(`${group}.`))
|
|
182
|
+
&& (!readOnly || schema.risk === 'read'));
|
|
183
|
+
if (commands.length === 0)
|
|
184
|
+
return [];
|
|
175
185
|
const actions = commands.map(actionName);
|
|
176
|
-
const description = 'Dinox CLI commands.
|
|
186
|
+
const description = 'Dinox CLI commands. ' +
|
|
187
|
+
(knowledgeTools ? 'search_notes/read_note/search_todos/related_notes are the fast daemon-backed path for plain search/read. ' : '') +
|
|
177
188
|
'High-risk writes need dryRun: true first, then the CLI confirmation options listed below (e.g. confirm/expectedCount); ' +
|
|
178
189
|
'CLI guards enforce them. Input keys are camelCase.\n' +
|
|
179
190
|
commands.map(describeAction).join('\n');
|
|
180
|
-
return {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
return [{
|
|
192
|
+
name: commandToolName(group),
|
|
193
|
+
commands,
|
|
194
|
+
config: {
|
|
195
|
+
description,
|
|
196
|
+
annotations: { ...toolAnnotations(commands), openWorldHint: group === 'storage' },
|
|
197
|
+
inputSchema: { action: z.enum(actions), input: z.record(z.string(), z.unknown()).optional() },
|
|
198
|
+
},
|
|
199
|
+
}];
|
|
189
200
|
});
|
|
190
201
|
}
|
|
191
202
|
/** Registers command tools using the real CLI or an injected runner. */
|
|
192
|
-
export function registerCommandTools(server, runner = runCliCommand) {
|
|
193
|
-
for (const tool of commandToolDefinitions()) {
|
|
203
|
+
export function registerCommandTools(server, runner = runCliCommand, options = {}) {
|
|
204
|
+
for (const tool of commandToolDefinitions(options)) {
|
|
194
205
|
server.registerTool(tool.name, tool.config, async ({ action, input }) => {
|
|
195
206
|
try {
|
|
196
207
|
const schema = tool.commands.find((command) => actionName(command) === action);
|
package/dist/gateway/mcpEnv.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export declare const GATEWAY_SESSION_TOKEN_ENV = "DINOX_GATEWAY_SESSION_TOKEN";
|
|
|
5
5
|
export declare const GATEWAY_KNOWLEDGE_TOOLS_ENV = "DINOX_GATEWAY_KNOWLEDGE_TOOLS";
|
|
6
6
|
/** Must not collide with MCP servers the user already registered with the agent. */
|
|
7
7
|
export declare const DINOX_MCP_SERVER_NAME = "dinox-gateway";
|
|
8
|
+
/** `dino mcp`, the server users register with their own MCP clients. */
|
|
9
|
+
export declare const DINOX_STANDALONE_MCP_SERVER_NAME = "dinox";
|
package/dist/gateway/mcpEnv.js
CHANGED
|
@@ -5,3 +5,5 @@ export const GATEWAY_SESSION_TOKEN_ENV = 'DINOX_GATEWAY_SESSION_TOKEN';
|
|
|
5
5
|
export const GATEWAY_KNOWLEDGE_TOOLS_ENV = 'DINOX_GATEWAY_KNOWLEDGE_TOOLS';
|
|
6
6
|
/** Must not collide with MCP servers the user already registered with the agent. */
|
|
7
7
|
export const DINOX_MCP_SERVER_NAME = 'dinox-gateway';
|
|
8
|
+
/** `dino mcp`, the server users register with their own MCP clients. */
|
|
9
|
+
export const DINOX_STANDALONE_MCP_SERVER_NAME = 'dinox';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Command groups exposed as MCP tools. Kept free of imports: command descriptors read it at load time. */
|
|
2
|
+
export declare const MCP_COMMAND_GROUPS: readonly ["note", "tag", "box", "todo", "view", "prop", "template", "prompt", "graph", "storage", "meta.stats"];
|
|
3
|
+
export type McpCommandGroup = (typeof MCP_COMMAND_GROUPS)[number];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandRunner } from './mcpCommandTools.js';
|
|
1
|
+
import { type CommandRunner, type CommandToolOptions } from './mcpCommandTools.js';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
export type DaemonCall = (method: string, params?: unknown, options?: {
|
|
4
4
|
timeoutMs?: number;
|
|
@@ -13,4 +13,13 @@ export type DinoxMcpOptions = {
|
|
|
13
13
|
cwd?: string;
|
|
14
14
|
};
|
|
15
15
|
export declare function createDinoxMcpServer(call?: DaemonCall, options?: DinoxMcpOptions): McpServer;
|
|
16
|
+
/**
|
|
17
|
+
* The Dinox MCP server for any MCP client (`dino mcp`). Only the generated
|
|
18
|
+
* command tools: they reach the daemon when it is available and otherwise run
|
|
19
|
+
* the CLI in a child process, so no daemon is required.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createStandaloneMcpServer(options?: Omit<CommandToolOptions, 'knowledgeTools'> & {
|
|
22
|
+
commandRunner?: CommandRunner;
|
|
23
|
+
}): McpServer;
|
|
16
24
|
export declare function serveDinoxMcpOverStdio(): Promise<void>;
|
|
25
|
+
export declare function serveStandaloneMcpOverStdio(options: Omit<CommandToolOptions, 'knowledgeTools'>): Promise<void>;
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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();
|
|
@@ -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
|
+
}
|
|
@@ -471,6 +471,10 @@ export declare const ERROR_CODES: {
|
|
|
471
471
|
readonly exitCode: 5;
|
|
472
472
|
readonly description: "No command schema exists for the requested path.";
|
|
473
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
|
+
};
|
|
474
478
|
readonly SKILLS_LOCK_INVALID: {
|
|
475
479
|
readonly exitCode: 1;
|
|
476
480
|
readonly description: "The skills installer lock file is malformed.";
|
package/dist/utils/errorCodes.js
CHANGED
|
@@ -123,6 +123,7 @@ export const ERROR_CODES = {
|
|
|
123
123
|
SAVED_VIEW_UNSUPPORTED_LAYOUT: { exitCode: 1, description: 'The saved view layout is unsupported.' },
|
|
124
124
|
SAVED_VIEW_VALUE_TYPE_MISMATCH: { exitCode: 1, description: 'A filter value does not match the field type.' },
|
|
125
125
|
SCHEMA_PATH_NOT_FOUND: { exitCode: 5, description: 'No command schema exists for the requested path.' },
|
|
126
|
+
SEMANTIC_SEARCH_UNAVAILABLE: { exitCode: 1, description: 'The semantic search service could not be reached or failed; keyword search (note search) still works.' },
|
|
126
127
|
SKILLS_LOCK_INVALID: { exitCode: 1, description: 'The skills installer lock file is malformed.' },
|
|
127
128
|
SKILLS_LOCK_READ_FAILED: { exitCode: 1, description: 'The skills installer lock file could not be read.' },
|
|
128
129
|
SKILLS_MIGRATION_FAILED: { exitCode: 1, description: 'The skills installer failed during migration.' },
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FieldSelection } from './select.js';
|
|
1
2
|
export type StructuredOutputFormat = 'yaml' | 'json';
|
|
2
3
|
export type StructuredNoticeMap = Record<string, unknown>;
|
|
3
4
|
export declare function toYaml(value: unknown): string;
|
|
@@ -16,6 +17,7 @@ export declare function parseStructuredOutputFormatFromArgv(argv: string[]): Str
|
|
|
16
17
|
export declare function setRequestedStructuredOutputFormat(format: StructuredOutputFormat | null): void;
|
|
17
18
|
export declare function clearRequestedStructuredOutputFormat(): void;
|
|
18
19
|
export declare function getRequestedStructuredOutputFormat(): StructuredOutputFormat | null;
|
|
20
|
+
export declare function setRequestedFieldSelection(selection: FieldSelection | null): void;
|
|
19
21
|
export declare function setPendingStructuredNoticeProvider(provider: (() => StructuredNoticeMap | null) | null): void;
|
|
20
22
|
export declare function clearPendingStructuredNoticeProvider(): void;
|
|
21
23
|
export declare function printStructuredData(value: unknown, format?: StructuredOutputFormat | null): void;
|
package/dist/utils/output.js
CHANGED
|
@@ -2,8 +2,10 @@ import { stringify as stringifyYaml } from 'yaml';
|
|
|
2
2
|
import { DinoxError } from './errors.js';
|
|
3
3
|
import { sanitizeForTerminal } from './terminalSanitization.js';
|
|
4
4
|
import { requestContext } from './requestContext.js';
|
|
5
|
+
import { projectFields } from './select.js';
|
|
5
6
|
let requestedStructuredOutputFormat = null;
|
|
6
7
|
let pendingStructuredNoticeProvider = null;
|
|
8
|
+
let requestedFieldSelection = null;
|
|
7
9
|
export function toYaml(value) {
|
|
8
10
|
return stringifyYaml(value, {
|
|
9
11
|
lineWidth: 0,
|
|
@@ -117,6 +119,36 @@ export function getRequestedStructuredOutputFormat() {
|
|
|
117
119
|
const context = requestContext.getStore();
|
|
118
120
|
return context ? context.format : requestedStructuredOutputFormat;
|
|
119
121
|
}
|
|
122
|
+
export function setRequestedFieldSelection(selection) {
|
|
123
|
+
const context = requestContext.getStore();
|
|
124
|
+
if (context) {
|
|
125
|
+
context.select = selection;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
requestedFieldSelection = selection;
|
|
129
|
+
}
|
|
130
|
+
function getRequestedFieldSelection() {
|
|
131
|
+
const context = requestContext.getStore();
|
|
132
|
+
return context ? context.select ?? null : requestedFieldSelection;
|
|
133
|
+
}
|
|
134
|
+
/** Applies `--select` to a command result; unmatched paths become a notice. */
|
|
135
|
+
function selectRequestedFields(value) {
|
|
136
|
+
const selection = getRequestedFieldSelection();
|
|
137
|
+
if (!selection)
|
|
138
|
+
return { value, notice: null };
|
|
139
|
+
const { value: projected, missing } = projectFields(value, selection);
|
|
140
|
+
if (missing.length === 0)
|
|
141
|
+
return { value: projected, notice: null };
|
|
142
|
+
return {
|
|
143
|
+
value: projected,
|
|
144
|
+
notice: {
|
|
145
|
+
select_missing: {
|
|
146
|
+
message: 'These --select fields are not in the result; check `dino schema <command>` output fields.',
|
|
147
|
+
fields: missing,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
120
152
|
export function setPendingStructuredNoticeProvider(provider) {
|
|
121
153
|
const context = requestContext.getStore();
|
|
122
154
|
if (context) {
|
|
@@ -190,15 +222,21 @@ function summarizeStructuredError(value) {
|
|
|
190
222
|
export function printSuccess(value, format = getRequestedStructuredOutputFormat()) {
|
|
191
223
|
if (format === 'json') {
|
|
192
224
|
const data = normalizeSuccessEnvelopeData(value);
|
|
193
|
-
const
|
|
225
|
+
const selected = selectRequestedFields(data);
|
|
226
|
+
const notice = mergeStructuredNotices(mergeStructuredNotices(getPendingStructuredNotice(), deriveStructuredNotice(data)) ?? null, selected.notice);
|
|
194
227
|
printStructuredData({
|
|
195
228
|
ok: true,
|
|
196
|
-
data,
|
|
229
|
+
data: selected.value,
|
|
197
230
|
...(notice ? { _notice: notice } : {}),
|
|
198
231
|
}, 'json');
|
|
199
232
|
return;
|
|
200
233
|
}
|
|
201
|
-
|
|
234
|
+
const selected = selectRequestedFields(value);
|
|
235
|
+
if (selected.notice) {
|
|
236
|
+
const fields = selected.notice.select_missing.fields;
|
|
237
|
+
printHumanReadableWarning(`warning: --select fields not in the result: ${fields.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
printStructuredData(selected.value, format);
|
|
202
240
|
}
|
|
203
241
|
export function printError(value, format = getRequestedStructuredOutputFormat()) {
|
|
204
242
|
if (format === 'json') {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** One step of a long-running command, reported on stderr under `--progress`. */
|
|
2
|
+
export type ProgressEvent = {
|
|
3
|
+
/** What the command is doing: `download`, `index`, `write`, ... */
|
|
4
|
+
phase: string;
|
|
5
|
+
current?: number;
|
|
6
|
+
total?: number;
|
|
7
|
+
message?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ProgressReporting = {
|
|
10
|
+
/** Command path (`sync`, `note.export`), carried by every event. */
|
|
11
|
+
command: string;
|
|
12
|
+
startedAtMs: number;
|
|
13
|
+
lastPhase?: string;
|
|
14
|
+
lastAtMs?: number;
|
|
15
|
+
};
|
|
16
|
+
/** Enables (`command`) or disables (`null`) progress events for the running command. */
|
|
17
|
+
export declare function setProgressReporting(command: string | null, now?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Structured output gets one JSON object per line (`"type":"progress"`), so
|
|
20
|
+
* stdout stays a single result while stderr streams events; human output gets
|
|
21
|
+
* a short line.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderProgressEvent(reporting: ProgressReporting, event: ProgressEvent, structured: boolean, now: number): string;
|
|
24
|
+
/** Reports a progress event when `--progress` is on; otherwise does nothing. */
|
|
25
|
+
export declare function reportProgress(event: ProgressEvent, now?: number): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getRequestedStructuredOutputFormat, writeToStderr } from './output.js';
|
|
2
|
+
import { requestContext } from './requestContext.js';
|
|
3
|
+
/** Repeated events of one phase closer together than this are dropped, except the last of a known total. */
|
|
4
|
+
const MIN_INTERVAL_MS = 200;
|
|
5
|
+
let localReporting = null;
|
|
6
|
+
/** Enables (`command`) or disables (`null`) progress events for the running command. */
|
|
7
|
+
export function setProgressReporting(command, now = Date.now()) {
|
|
8
|
+
const reporting = command ? { command, startedAtMs: now } : null;
|
|
9
|
+
const context = requestContext.getStore();
|
|
10
|
+
if (context) {
|
|
11
|
+
context.progress = reporting;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
localReporting = reporting;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Structured output gets one JSON object per line (`"type":"progress"`), so
|
|
18
|
+
* stdout stays a single result while stderr streams events; human output gets
|
|
19
|
+
* a short line.
|
|
20
|
+
*/
|
|
21
|
+
export function renderProgressEvent(reporting, event, structured, now) {
|
|
22
|
+
if (structured) {
|
|
23
|
+
return `${JSON.stringify({
|
|
24
|
+
type: 'progress',
|
|
25
|
+
command: reporting.command,
|
|
26
|
+
phase: event.phase,
|
|
27
|
+
...(event.current !== undefined ? { current: event.current } : {}),
|
|
28
|
+
...(event.total !== undefined ? { total: event.total } : {}),
|
|
29
|
+
...(event.message !== undefined ? { message: event.message } : {}),
|
|
30
|
+
elapsed_ms: Math.max(0, now - reporting.startedAtMs),
|
|
31
|
+
})}\n`;
|
|
32
|
+
}
|
|
33
|
+
const count = event.current === undefined
|
|
34
|
+
? ''
|
|
35
|
+
: event.total === undefined
|
|
36
|
+
? ` ${event.current}`
|
|
37
|
+
: ` ${event.current}/${event.total}${event.total > 0 ? ` (${Math.floor((event.current / event.total) * 100)}%)` : ''}`;
|
|
38
|
+
return `[${reporting.command}] ${event.phase}${count}${event.message ? ` ${event.message}` : ''}\n`;
|
|
39
|
+
}
|
|
40
|
+
/** Reports a progress event when `--progress` is on; otherwise does nothing. */
|
|
41
|
+
export function reportProgress(event, now = Date.now()) {
|
|
42
|
+
const context = requestContext.getStore();
|
|
43
|
+
const reporting = context ? context.progress ?? null : localReporting;
|
|
44
|
+
if (!reporting)
|
|
45
|
+
return;
|
|
46
|
+
const finished = event.total !== undefined && event.current === event.total;
|
|
47
|
+
if (reporting.lastPhase === event.phase
|
|
48
|
+
&& reporting.lastAtMs !== undefined
|
|
49
|
+
&& now - reporting.lastAtMs < MIN_INTERVAL_MS
|
|
50
|
+
&& !finished)
|
|
51
|
+
return;
|
|
52
|
+
reporting.lastPhase = event.phase;
|
|
53
|
+
reporting.lastAtMs = now;
|
|
54
|
+
const text = renderProgressEvent(reporting, event, getRequestedStructuredOutputFormat() !== null, now);
|
|
55
|
+
if (context?.onProgress)
|
|
56
|
+
context.onProgress(text);
|
|
57
|
+
else
|
|
58
|
+
writeToStderr(text);
|
|
59
|
+
}
|
|
@@ -2,12 +2,20 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
2
2
|
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
3
3
|
import type { ResolvedDinoxConfig } from '../config/types.js';
|
|
4
4
|
import type { StructuredNoticeMap, StructuredOutputFormat } from './output.js';
|
|
5
|
+
import type { FieldSelection } from './select.js';
|
|
6
|
+
import type { ProgressReporting } from './progress.js';
|
|
5
7
|
export type RequestContext = {
|
|
6
8
|
stdout: string[];
|
|
7
9
|
stderr: string[];
|
|
8
10
|
format: StructuredOutputFormat | null;
|
|
11
|
+
/** `--select` of the running command; absent means the whole result. */
|
|
12
|
+
select?: FieldSelection | null;
|
|
9
13
|
noticeProvider: (() => StructuredNoticeMap | null) | null;
|
|
10
14
|
cwd: string;
|
|
15
|
+
/** `--progress` of the running command; absent means off. */
|
|
16
|
+
progress?: ProgressReporting | null;
|
|
17
|
+
/** Delivers rendered progress events live (the daemon streams them to the client); default stderr. */
|
|
18
|
+
onProgress?: (text: string) => void;
|
|
11
19
|
daemon?: {
|
|
12
20
|
db: AbstractPowerSyncDatabase;
|
|
13
21
|
config: ResolvedDinoxConfig;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A parsed `--select` list: each key keeps the whole value (true) or a nested selection. */
|
|
2
|
+
export type FieldSelection = {
|
|
3
|
+
[key: string]: FieldSelection | true;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Parses `--select notes.id,notes.title,count` into a selection tree. A path
|
|
7
|
+
* that names a parent also keeps everything below it (`notes,notes.id` keeps
|
|
8
|
+
* whole notes).
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseFieldSelection(value: unknown): FieldSelection;
|
|
11
|
+
/**
|
|
12
|
+
* Keeps only the selected fields. Arrays are projected item by item, so
|
|
13
|
+
* `notes.id` selects the id of every note. Returns the projected value and the
|
|
14
|
+
* paths that matched nothing anywhere, so a typo is reported instead of
|
|
15
|
+
* silently producing empty output.
|
|
16
|
+
*/
|
|
17
|
+
export declare function projectFields(value: unknown, selection: FieldSelection): {
|
|
18
|
+
value: unknown;
|
|
19
|
+
missing: string[];
|
|
20
|
+
};
|