@dinoxx/dinox-cli 2.12.0 → 2.13.1
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/README.md +1 -1
- package/dist/commands/auth/index.d.ts +0 -6
- package/dist/commands/auth/index.js +8 -50
- package/dist/commands/gateway/index.js +5 -3
- package/dist/gateway/adapters/index.js +2 -2
- package/dist/gateway/mcpCommandTools.d.ts +43 -0
- package/dist/gateway/mcpCommandTools.js +179 -0
- package/dist/gateway/mcpEnv.d.ts +2 -0
- package/dist/gateway/mcpEnv.js +2 -0
- package/dist/gateway/mcpServer.d.ts +2 -0
- package/dist/gateway/mcpServer.js +38 -17
- package/dist/gateway/runExecutor.d.ts +7 -2
- package/dist/gateway/runExecutor.js +10 -4
- package/dist/gateway/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -947,7 +947,7 @@ dino gateway reload
|
|
|
947
947
|
dino gateway remove Hermes
|
|
948
948
|
```
|
|
949
949
|
|
|
950
|
-
`detect` 会在 `PATH` 上查找 ACP agent,并用真实握手确认能否启动。覆盖范围与 [Paseo 的 ACP catalog](https://github.com/getpaseo/paseo/blob/main/packages/app/src/data/acp-provider-catalog.ts) 对齐,另外包含 Pi(`pi-acp`)、Antigravity(`agy-acp`)、Muse Code(`muse-code-acp`)和 OMP(`omp acp`)。只认本机已经装好的命令,不会为了探测去 `npx -y` 下载。每个 agent 一握手完就打印,不用等最慢的那个。某个启动方式一直不应答时,不再换参数重试。`--add` 把已确认的 agent 写入当前账号的 gateway 配置,并由 daemon 托管。`gateway list` 只显示环境变量名,不显示值。`dino gateway mcp` 由这些 agent
|
|
950
|
+
`detect` 会在 `PATH` 上查找 ACP agent,并用真实握手确认能否启动。覆盖范围与 [Paseo 的 ACP catalog](https://github.com/getpaseo/paseo/blob/main/packages/app/src/data/acp-provider-catalog.ts) 对齐,另外包含 Pi(`pi-acp`)、Antigravity(`agy-acp`)、Muse Code(`muse-code-acp`)和 OMP(`omp acp`)。只认本机已经装好的命令,不会为了探测去 `npx -y` 下载。每个 agent 一握手完就打印,不用等最慢的那个。某个启动方式一直不应答时,不再换参数重试。`--add` 把已确认的 agent 写入当前账号的 gateway 配置,并由 daemon 托管。`gateway list` 只显示环境变量名,不显示值。`dino gateway mcp` 由这些 agent 拉起,对外提供笔记、待办、标签、卡片盒等读写工具,以及把本机文件放进回复的 `send_file`。账号、同步、守护进程和 gateway 自身的命令不在这套工具里。
|
|
951
951
|
|
|
952
952
|
### 6.16 `info`:查看版本与技能仓库地址
|
|
953
953
|
|
|
@@ -5,8 +5,6 @@ import { resolveConfig } from '../../config/resolve.js';
|
|
|
5
5
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked } from '../../daemon/lifecycle.js';
|
|
6
6
|
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
7
7
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
8
|
-
import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js';
|
|
9
|
-
import { runSingleSyncPass } from '../../powersync/syncPass.js';
|
|
10
8
|
import type { CommandGroupDescriptor } from '../descriptor/types.js';
|
|
11
9
|
import { printHumanReadableWarning } from '../../utils/output.js';
|
|
12
10
|
export declare function releaseOwnerLeases(releases: ReadonlyArray<() => Promise<void>>): Promise<unknown[]>;
|
|
@@ -17,10 +15,6 @@ type AuthLoginDependencies = {
|
|
|
17
15
|
fetchCredentials: typeof fetchCredentialsWithAuthorization;
|
|
18
16
|
fetchUserInfo: typeof fetchUserInfoWithAuthorization;
|
|
19
17
|
suspendDaemon: typeof suspendDaemonForExclusiveAccessLocked;
|
|
20
|
-
connectPowerSync: typeof connectPowerSync;
|
|
21
|
-
runSingleSyncPass: typeof runSingleSyncPass;
|
|
22
|
-
getStatusSnapshot: typeof getStatusSnapshot;
|
|
23
|
-
closePowerSyncAndReleaseOwner: typeof closePowerSyncAndReleaseOwner;
|
|
24
18
|
updateConfig: typeof updateConfig;
|
|
25
19
|
restoreDaemon: typeof restoreSuspendedDaemonLocked;
|
|
26
20
|
printWarning: typeof printHumanReadableWarning;
|
|
@@ -13,8 +13,7 @@ import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../.
|
|
|
13
13
|
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
14
14
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
15
15
|
import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
|
|
16
|
-
import { acquirePowerSyncOwnerLease,
|
|
17
|
-
import { runSingleSyncPass } from '../../powersync/syncPass.js';
|
|
16
|
+
import { acquirePowerSyncOwnerLease, getStatusSnapshot, } from '../../powersync/runtime.js';
|
|
18
17
|
import { registerCommandGroup } from '../descriptor/register.js';
|
|
19
18
|
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
20
19
|
import { normalizeAuthToken, readEnvAuthToken } from '../../utils/authToken.js';
|
|
@@ -106,10 +105,6 @@ const authLoginDependencies = {
|
|
|
106
105
|
fetchCredentials: fetchCredentialsWithAuthorization,
|
|
107
106
|
fetchUserInfo: fetchUserInfoWithAuthorization,
|
|
108
107
|
suspendDaemon: suspendDaemonForExclusiveAccessLocked,
|
|
109
|
-
connectPowerSync,
|
|
110
|
-
runSingleSyncPass,
|
|
111
|
-
getStatusSnapshot,
|
|
112
|
-
closePowerSyncAndReleaseOwner,
|
|
113
108
|
updateConfig,
|
|
114
109
|
restoreDaemon: restoreSuspendedDaemonLocked,
|
|
115
110
|
printWarning: printHumanReadableWarning,
|
|
@@ -122,7 +117,6 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
122
117
|
const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
|
|
123
118
|
let previousConfig = null;
|
|
124
119
|
let suspended = null;
|
|
125
|
-
let restoreAllowed = true;
|
|
126
120
|
let lifecycleLockReleaseError;
|
|
127
121
|
let outcome;
|
|
128
122
|
try {
|
|
@@ -145,37 +139,6 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
145
139
|
const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
|
|
146
140
|
const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
|
|
147
141
|
suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
|
|
148
|
-
const { db, dbPath, stale, releaseOwner } = await dependencies.connectPowerSync({
|
|
149
|
-
config: verifiedResolved,
|
|
150
|
-
offline: false,
|
|
151
|
-
timeoutMs,
|
|
152
|
-
});
|
|
153
|
-
let syncOutcome;
|
|
154
|
-
try {
|
|
155
|
-
await dependencies.runSingleSyncPass(db, timeoutMs);
|
|
156
|
-
syncOutcome = {
|
|
157
|
-
ok: true,
|
|
158
|
-
lastSyncedAt: dependencies.getStatusSnapshot(db).lastSyncedAt,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
syncOutcome = { ok: false, error };
|
|
163
|
-
}
|
|
164
|
-
let cleanupError;
|
|
165
|
-
try {
|
|
166
|
-
await dependencies.closePowerSyncAndReleaseOwner(db, releaseOwner);
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
cleanupError = error;
|
|
170
|
-
restoreAllowed = false;
|
|
171
|
-
dependencies.printWarning(`warning: failed to close the login verification runtime safely: ${authLoginErrorMessage(error)}`);
|
|
172
|
-
}
|
|
173
|
-
if (!syncOutcome.ok) {
|
|
174
|
-
throw syncOutcome.error;
|
|
175
|
-
}
|
|
176
|
-
if (cleanupError) {
|
|
177
|
-
throw cleanupError;
|
|
178
|
-
}
|
|
179
142
|
await dependencies.updateConfig((current) => {
|
|
180
143
|
if (!persistedAuthStateMatches(current, previousConfig)) {
|
|
181
144
|
throw new DinoxError('Authentication changed while login was being verified.', {
|
|
@@ -192,16 +155,16 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
192
155
|
ok: true,
|
|
193
156
|
verified: true,
|
|
194
157
|
endpoint: verifiedResolved.powersync.endpoint,
|
|
195
|
-
dbPath,
|
|
196
|
-
stale,
|
|
197
|
-
lastSyncedAt:
|
|
158
|
+
dbPath: getPowerSyncDbPath(identity.userId),
|
|
159
|
+
stale: true,
|
|
160
|
+
lastSyncedAt: null,
|
|
198
161
|
userId: identity.userId,
|
|
199
162
|
},
|
|
200
163
|
};
|
|
201
164
|
}
|
|
202
165
|
catch (error) {
|
|
203
166
|
outcome = { ok: false, error };
|
|
204
|
-
if (previousConfig && suspended
|
|
167
|
+
if (previousConfig && suspended) {
|
|
205
168
|
try {
|
|
206
169
|
const current = await dependencies.loadConfig();
|
|
207
170
|
if (persistedAuthStateMatches(current, previousConfig)) {
|
|
@@ -315,12 +278,7 @@ async function runAuthLogin(context) {
|
|
|
315
278
|
printHumanReadableLine(`- endpoint: ${result.endpoint}`);
|
|
316
279
|
printHumanReadableLine(`- db: ${result.dbPath}`);
|
|
317
280
|
printHumanReadableLine(`- userId: ${result.userId}`);
|
|
318
|
-
|
|
319
|
-
printHumanReadableLine(`- lastSyncedAt: ${result.lastSyncedAt}`);
|
|
320
|
-
}
|
|
321
|
-
if (result.stale) {
|
|
322
|
-
printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
|
|
323
|
-
}
|
|
281
|
+
printHumanReadableLine('- initial sync continues in the background; run `dino sync` to wait for it');
|
|
324
282
|
const serviceNote = describeDaemonServiceAutoInstall(daemonService);
|
|
325
283
|
if (serviceNote)
|
|
326
284
|
printHumanReadableWarning(serviceNote);
|
|
@@ -567,7 +525,7 @@ export const authCommandGroup = {
|
|
|
567
525
|
commands: [
|
|
568
526
|
{
|
|
569
527
|
name: 'login',
|
|
570
|
-
description: 'Log in by scanning a QR code
|
|
528
|
+
description: 'Log in by scanning a QR code or with a token; the initial sync continues in the background',
|
|
571
529
|
arguments: [{ syntax: '[token]', description: 'Login token (omit to scan a QR code, or when using --token-stdin)' }],
|
|
572
530
|
options: [{ flags: '--token-stdin', description: 'Read the login token from piped stdin instead of argv', type: 'boolean' }],
|
|
573
531
|
schema: {
|
|
@@ -577,7 +535,7 @@ export const authCommandGroup = {
|
|
|
577
535
|
globalOptions: AUTH_LOGIN_GLOBAL_OPTIONS,
|
|
578
536
|
output: {
|
|
579
537
|
type: 'structured',
|
|
580
|
-
summary: 'Login verification result',
|
|
538
|
+
summary: 'Login verification result (initial sync is not awaited)',
|
|
581
539
|
fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId', 'daemonService'],
|
|
582
540
|
},
|
|
583
541
|
examples: [
|
|
@@ -5,7 +5,6 @@ import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../.
|
|
|
5
5
|
import { loadConfig } from '../../config/store.js';
|
|
6
6
|
import { addGateway, adoptDetectedAgent, getGatewaysFilePath, loadGateways, removeGateway, } from '../../gateway/config.js';
|
|
7
7
|
import { detectAcpAgents, inferAgentId } from '../../gateway/detect.js';
|
|
8
|
-
import { serveDinoxMcpOverStdio } from '../../gateway/mcpServer.js';
|
|
9
8
|
import { DinoxError } from '../../utils/errors.js';
|
|
10
9
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanReadableWarning, printSuccess, } from '../../utils/output.js';
|
|
11
10
|
import { registerCommandGroup } from '../descriptor/register.js';
|
|
@@ -307,7 +306,7 @@ export const gatewayCommandGroup = {
|
|
|
307
306
|
},
|
|
308
307
|
{
|
|
309
308
|
name: 'mcp',
|
|
310
|
-
description: 'Serve Dinox tools over stdio MCP: read
|
|
309
|
+
description: 'Serve Dinox tools over stdio MCP: full read/write knowledge tools and send_file (started by gateway agents)',
|
|
311
310
|
schema: {
|
|
312
311
|
path: 'gateway.mcp',
|
|
313
312
|
risk: 'read',
|
|
@@ -316,7 +315,10 @@ export const gatewayCommandGroup = {
|
|
|
316
315
|
output: { type: 'structured', summary: 'MCP JSON-RPC over stdio until stdin closes', fields: [] },
|
|
317
316
|
examples: ['dino gateway mcp'],
|
|
318
317
|
},
|
|
319
|
-
action:
|
|
318
|
+
action: async () => {
|
|
319
|
+
const { serveDinoxMcpOverStdio } = await import('../../gateway/mcpServer.js');
|
|
320
|
+
await serveDinoxMcpOverStdio();
|
|
321
|
+
},
|
|
320
322
|
},
|
|
321
323
|
{
|
|
322
324
|
name: 'reload',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from '../mcpEnv.js';
|
|
1
|
+
import { DINOX_MCP_SERVER_NAME, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from '../mcpEnv.js';
|
|
2
2
|
import { AcpGatewayAdapter } from './acp.js';
|
|
3
3
|
import { EchoGatewayAdapter } from './echo.js';
|
|
4
4
|
/**
|
|
@@ -15,7 +15,7 @@ export function dinoxMcpServerConfig(options) {
|
|
|
15
15
|
entry[0] !== GATEWAY_KNOWLEDGE_TOOLS_ENV &&
|
|
16
16
|
typeof entry[1] === 'string');
|
|
17
17
|
return {
|
|
18
|
-
name:
|
|
18
|
+
name: DINOX_MCP_SERVER_NAME,
|
|
19
19
|
command: process.execPath,
|
|
20
20
|
args: [...process.execArgv, entrypoint, 'gateway', 'mcp'],
|
|
21
21
|
env: [
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { CommandSchema } from '../commands/schema/types.js';
|
|
4
|
+
/**
|
|
5
|
+
* One MCP tool per command group, generated from the command schemas. Each runs
|
|
6
|
+
* the real CLI as a child process without a shell, preserving the CLI's own
|
|
7
|
+
* validation, dry-run and confirmation guards. Approval belongs to the agent's
|
|
8
|
+
* permission flow, guided by the tool annotations.
|
|
9
|
+
*/
|
|
10
|
+
/** Command groups exposed as MCP tools. */
|
|
11
|
+
export declare const MCP_COMMAND_GROUPS: readonly ["note", "tag", "box", "todo", "view", "prop", "template", "prompt", "graph", "storage", "meta.stats"];
|
|
12
|
+
/** Commands deliberately excluded from the MCP surface. */
|
|
13
|
+
export declare const MCP_COMMAND_EXCLUSIONS: readonly ["auth.*", "config.*", "daemon.*", "gateway.*", "doctor", "skills.*", "update", "sync", "info", "schema", "meta.schema"];
|
|
14
|
+
/** Runs a CLI invocation and captures its output. */
|
|
15
|
+
export type CommandRunner = (argv: string[]) => Promise<{
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** Runs the current CLI entry point without a shell. */
|
|
20
|
+
export declare const runCliCommand: CommandRunner;
|
|
21
|
+
/** Validates tool inputs and builds CLI arguments in schema order. */
|
|
22
|
+
export declare function commandArgv(schema: CommandSchema, input: Record<string, unknown>): string[];
|
|
23
|
+
/** Builds grouped MCP tool definitions from the command schemas. */
|
|
24
|
+
export declare function commandToolDefinitions(): {
|
|
25
|
+
name: string;
|
|
26
|
+
commands: CommandSchema[];
|
|
27
|
+
config: {
|
|
28
|
+
description: string;
|
|
29
|
+
annotations: {
|
|
30
|
+
openWorldHint: boolean;
|
|
31
|
+
destructiveHint?: boolean | undefined;
|
|
32
|
+
readOnlyHint: boolean;
|
|
33
|
+
};
|
|
34
|
+
inputSchema: {
|
|
35
|
+
action: z.ZodEnum<{
|
|
36
|
+
[x: string]: string;
|
|
37
|
+
}>;
|
|
38
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}[];
|
|
42
|
+
/** Registers command tools using the real CLI or an injected runner. */
|
|
43
|
+
export declare function registerCommandTools(server: McpServer, runner?: CommandRunner): void;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { listCommandSchemas } from '../commands/schema/spec.js';
|
|
5
|
+
/**
|
|
6
|
+
* One MCP tool per command group, generated from the command schemas. Each runs
|
|
7
|
+
* the real CLI as a child process without a shell, preserving the CLI's own
|
|
8
|
+
* validation, dry-run and confirmation guards. Approval belongs to the agent's
|
|
9
|
+
* permission flow, guided by the tool annotations.
|
|
10
|
+
*/
|
|
11
|
+
/** Command groups exposed as MCP tools. */
|
|
12
|
+
export const MCP_COMMAND_GROUPS = [
|
|
13
|
+
'note', 'tag', 'box', 'todo', 'view', 'prop', 'template', 'prompt', 'graph', 'storage', 'meta.stats',
|
|
14
|
+
];
|
|
15
|
+
/** Commands deliberately excluded from the MCP surface. */
|
|
16
|
+
export const MCP_COMMAND_EXCLUSIONS = [
|
|
17
|
+
'auth.*', 'config.*', 'daemon.*', 'gateway.*', 'doctor', 'skills.*', 'update', 'sync', 'info', 'schema', 'meta.schema',
|
|
18
|
+
];
|
|
19
|
+
const execute = promisify(execFile);
|
|
20
|
+
/** Runs the current CLI entry point without a shell. */
|
|
21
|
+
export const runCliCommand = (argv) => {
|
|
22
|
+
const entryPoint = process.argv[1];
|
|
23
|
+
if (entryPoint === undefined)
|
|
24
|
+
throw new Error('Cannot run CLI command: process.argv[1] is undefined');
|
|
25
|
+
return execute(process.execPath, [...process.execArgv, entryPoint, ...argv], {
|
|
26
|
+
env: process.env,
|
|
27
|
+
timeout: 120_000,
|
|
28
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
function key(name) {
|
|
32
|
+
return name
|
|
33
|
+
.replace(/^[<[.]+|[>\].]+$/g, '')
|
|
34
|
+
.replace(/[-_]+([a-zA-Z0-9])/g, (_match, letter) => letter.toUpperCase());
|
|
35
|
+
}
|
|
36
|
+
function flag(name) {
|
|
37
|
+
return name.match(/--[\w-]+/)[0];
|
|
38
|
+
}
|
|
39
|
+
function optionKey(name) {
|
|
40
|
+
return key(flag(name).replace(/^--(?:no-)?/, ''));
|
|
41
|
+
}
|
|
42
|
+
function text(value) {
|
|
43
|
+
return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] };
|
|
44
|
+
}
|
|
45
|
+
function parse(value) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(value);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function actionName(schema) {
|
|
54
|
+
return schema.path.split('.').slice(1).join('.');
|
|
55
|
+
}
|
|
56
|
+
function optionType(option) {
|
|
57
|
+
return option.type ?? (/[<[]/.test(option.name) ? 'string' : 'boolean');
|
|
58
|
+
}
|
|
59
|
+
function describeAction(schema) {
|
|
60
|
+
const args = (schema.arguments ?? []).map((arg) => `${key(arg.name)}:string${arg.required ? '!' : '?'}`);
|
|
61
|
+
const options = (schema.options ?? []).map((option) => {
|
|
62
|
+
const required = option.required ? '!' : '?';
|
|
63
|
+
const choices = option.enum ? `(${option.enum.join('|')})` : '';
|
|
64
|
+
return `${optionKey(option.name)}:${optionType(option)}${required}${choices}`;
|
|
65
|
+
});
|
|
66
|
+
const inputs = [...args, ...options].join(' ') || '(no input)';
|
|
67
|
+
return `${actionName(schema)} [${schema.risk}]: ${schema.summary}\n${inputs}`;
|
|
68
|
+
}
|
|
69
|
+
function toolAnnotations(commands) {
|
|
70
|
+
const readOnly = commands.every((schema) => schema.risk === 'read');
|
|
71
|
+
return {
|
|
72
|
+
readOnlyHint: readOnly,
|
|
73
|
+
...(!readOnly ? { destructiveHint: commands.some((schema) => schema.risk === 'high-risk-write') } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Validates tool inputs and builds CLI arguments in schema order. */
|
|
77
|
+
export function commandArgv(schema, input) {
|
|
78
|
+
const args = schema.arguments ?? [];
|
|
79
|
+
const options = schema.options ?? [];
|
|
80
|
+
const allowed = new Set([...args.map((arg) => key(arg.name)), ...options.map((option) => optionKey(option.name))]);
|
|
81
|
+
for (const name of Object.keys(input)) {
|
|
82
|
+
if (!allowed.has(name))
|
|
83
|
+
throw new Error(`Unknown input key: ${name}`);
|
|
84
|
+
}
|
|
85
|
+
const positionals = [];
|
|
86
|
+
let omitted = false;
|
|
87
|
+
for (const arg of args) {
|
|
88
|
+
const value = input[key(arg.name)];
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
if (arg.required)
|
|
91
|
+
throw new Error(`Missing required argument: ${key(arg.name)}`);
|
|
92
|
+
omitted = true;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (typeof value !== 'string' || omitted)
|
|
96
|
+
throw new Error(`Invalid positional argument: ${key(arg.name)}`);
|
|
97
|
+
positionals.push(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const flags = [];
|
|
101
|
+
for (const option of options) {
|
|
102
|
+
const name = optionKey(option.name);
|
|
103
|
+
const value = input[name];
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
if (option.required)
|
|
106
|
+
throw new Error(`Missing required option: ${name}`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const type = optionType(option);
|
|
110
|
+
const valid = type === 'string[]'
|
|
111
|
+
? Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
112
|
+
: typeof value === type && (type !== 'number' || Number.isFinite(value));
|
|
113
|
+
if (!valid || (option.enum && !option.enum.includes(String(value))))
|
|
114
|
+
throw new Error(`Invalid value for ${name} (${type})`);
|
|
115
|
+
const long = flag(option.name);
|
|
116
|
+
if (type === 'boolean') {
|
|
117
|
+
if (long.startsWith('--no-')) {
|
|
118
|
+
if (value === false)
|
|
119
|
+
flags.push(long);
|
|
120
|
+
}
|
|
121
|
+
else if (value === true) {
|
|
122
|
+
flags.push(long);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (Array.isArray(value)) {
|
|
126
|
+
// --fields is CSV; the other array descriptors use repeatable collectors.
|
|
127
|
+
if (long === '--fields') {
|
|
128
|
+
flags.push(`${long}=${value.join(',')}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
for (const item of value)
|
|
132
|
+
flags.push(`${long}=${String(item)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
flags.push(`${long}=${String(value)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [...schema.path.split('.'), ...flags, '--format', 'json', '--', ...positionals];
|
|
140
|
+
}
|
|
141
|
+
/** Builds grouped MCP tool definitions from the command schemas. */
|
|
142
|
+
export function commandToolDefinitions() {
|
|
143
|
+
return MCP_COMMAND_GROUPS.map((group) => {
|
|
144
|
+
const commands = listCommandSchemas().filter((schema) => group === 'meta.stats' ? schema.path === group : schema.path.startsWith(`${group}.`));
|
|
145
|
+
const actions = commands.map(actionName);
|
|
146
|
+
const description = 'Dinox CLI commands. search_notes/read_note/search_todos/related_notes are the fast daemon-backed path for plain search/read. ' +
|
|
147
|
+
'High-risk writes need dryRun: true first, then the CLI confirmation options listed below (e.g. confirm/expectedCount); ' +
|
|
148
|
+
'CLI guards enforce them. Input keys are camelCase.\n' +
|
|
149
|
+
commands.map(describeAction).join('\n');
|
|
150
|
+
return {
|
|
151
|
+
name: group === 'meta.stats' ? 'dinox_stats' : `dinox_${group}`,
|
|
152
|
+
commands,
|
|
153
|
+
config: {
|
|
154
|
+
description,
|
|
155
|
+
annotations: { ...toolAnnotations(commands), openWorldHint: group === 'storage' },
|
|
156
|
+
inputSchema: { action: z.enum(actions), input: z.record(z.string(), z.unknown()).optional() },
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Registers command tools using the real CLI or an injected runner. */
|
|
162
|
+
export function registerCommandTools(server, runner = runCliCommand) {
|
|
163
|
+
for (const tool of commandToolDefinitions()) {
|
|
164
|
+
server.registerTool(tool.name, tool.config, async ({ action, input }) => {
|
|
165
|
+
try {
|
|
166
|
+
const schema = tool.commands.find((command) => actionName(command) === action);
|
|
167
|
+
if (!schema)
|
|
168
|
+
throw new Error(`Unknown action: ${action}`);
|
|
169
|
+
const result = await runner(commandArgv(schema, input ?? {}));
|
|
170
|
+
return text(parse(result.stdout));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const failure = error;
|
|
174
|
+
const message = failure.stdout?.trim() || failure.stderr?.trim() || failure.message || String(error);
|
|
175
|
+
return { ...text(parse(message)), isError: true };
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/gateway/mcpEnv.d.ts
CHANGED
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
export declare const GATEWAY_SESSION_TOKEN_ENV = "DINOX_GATEWAY_SESSION_TOKEN";
|
|
4
4
|
/** `0` offers only `send_file`: the owner turned the knowledge tools off. */
|
|
5
5
|
export declare const GATEWAY_KNOWLEDGE_TOOLS_ENV = "DINOX_GATEWAY_KNOWLEDGE_TOOLS";
|
|
6
|
+
/** Must not collide with MCP servers the user already registered with the agent. */
|
|
7
|
+
export declare const DINOX_MCP_SERVER_NAME = "dinox-gateway";
|
package/dist/gateway/mcpEnv.js
CHANGED
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
export const GATEWAY_SESSION_TOKEN_ENV = 'DINOX_GATEWAY_SESSION_TOKEN';
|
|
4
4
|
/** `0` offers only `send_file`: the owner turned the knowledge tools off. */
|
|
5
5
|
export const GATEWAY_KNOWLEDGE_TOOLS_ENV = 'DINOX_GATEWAY_KNOWLEDGE_TOOLS';
|
|
6
|
+
/** Must not collide with MCP servers the user already registered with the agent. */
|
|
7
|
+
export const DINOX_MCP_SERVER_NAME = 'dinox-gateway';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CommandRunner } from './mcpCommandTools.js';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
export type DaemonCall = (method: string, params?: unknown, options?: {
|
|
3
4
|
timeoutMs?: number;
|
|
@@ -5,6 +6,7 @@ export type DaemonCall = (method: string, params?: unknown, options?: {
|
|
|
5
6
|
export type DinoxMcpOptions = {
|
|
6
7
|
/** Offer the knowledge tools (notes, todos, conversations); default true. */
|
|
7
8
|
knowledgeTools?: boolean;
|
|
9
|
+
commandRunner?: CommandRunner;
|
|
8
10
|
/** The session `send_file` belongs to; absent, `send_file` is not offered. */
|
|
9
11
|
sessionToken?: string;
|
|
10
12
|
/** Resolves relative `send_file` paths; default the process directory. */
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { registerCommandTools } from './mcpCommandTools.js';
|
|
2
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { sendDaemonRequest } from '../daemon/client.js';
|
|
6
|
-
import { GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from './mcpEnv.js';
|
|
7
|
+
import { DINOX_MCP_SERVER_NAME, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from './mcpEnv.js';
|
|
7
8
|
import { READ_CONVERSATION_MAX_LIMIT } from './conversationContext.js';
|
|
8
9
|
import { getPackageVersion } from '../utils/version.js';
|
|
9
10
|
/**
|
|
10
11
|
* Dinox tools for gateway agents, served over stdio MCP. The agent launches
|
|
11
|
-
* this as a child process (`dino gateway mcp`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* this as a child process (`dino gateway mcp`). Curated reads use the daemon;
|
|
13
|
+
* command-group tools run the CLI and allow writes. Approval belongs to the
|
|
14
|
+
* agent's permission flow. Destructive actions are flagged by annotations and
|
|
15
|
+
* retain the CLI's dry-run/confirmation guards. `send_file` adds reply files.
|
|
15
16
|
*/
|
|
16
17
|
const MAX_NOTE_CHARS = 20_000;
|
|
17
18
|
const MAX_RESULTS = 50;
|
|
@@ -24,15 +25,18 @@ function failure(error) {
|
|
|
24
25
|
return { ...text(`Dinox is unavailable: ${error instanceof Error ? error.message : String(error)}`), isError: true };
|
|
25
26
|
}
|
|
26
27
|
export function createDinoxMcpServer(call = (method, params, options) => sendDaemonRequest(method, params, options), options = {}) {
|
|
27
|
-
const server = new McpServer({ name:
|
|
28
|
+
const server = new McpServer({ name: DINOX_MCP_SERVER_NAME, version: getPackageVersion() });
|
|
28
29
|
if (options.sessionToken)
|
|
29
30
|
registerSendFile(server, call, options.sessionToken, options.cwd ?? process.cwd());
|
|
30
|
-
if (options.knowledgeTools !== false)
|
|
31
|
+
if (options.knowledgeTools !== false) {
|
|
31
32
|
registerKnowledgeTools(server, call);
|
|
33
|
+
registerCommandTools(server, options.commandRunner);
|
|
34
|
+
}
|
|
32
35
|
return server;
|
|
33
36
|
}
|
|
34
37
|
function registerSendFile(server, call, sessionToken, cwd) {
|
|
35
38
|
server.registerTool('send_file', {
|
|
39
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
36
40
|
description: 'Show the user a file from this computer (an image you generated, a document, a chart, ...). The user talks to ' +
|
|
37
41
|
'you from the Dinox app and cannot open files on this computer, so a path alone is useless to them: call this ' +
|
|
38
42
|
'for every file they should see, then answer as usual. Images appear in the reply; other files as attachments. ' +
|
|
@@ -53,15 +57,18 @@ function registerSendFile(server, call, sessionToken, cwd) {
|
|
|
53
57
|
}
|
|
54
58
|
function registerKnowledgeTools(server, call) {
|
|
55
59
|
server.registerTool('search_notes', {
|
|
60
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
56
61
|
description: "Search the user's Dinox notes by keyword. Returns id, title, summary, tags and folders; use read_note for full content.",
|
|
57
62
|
inputSchema: {
|
|
58
63
|
query: z.string().describe('Keywords; empty string lists recent notes'),
|
|
64
|
+
offset: z.number().int().min(0).optional().describe('Results to skip; use nextOffset from the previous page'),
|
|
65
|
+
boxes: z.array(z.string()).optional().describe('Card box (folder) names or ids; notes in any of them match'),
|
|
59
66
|
tags: z.string().optional().describe('Tag filter expression, e.g. "work AND !draft"'),
|
|
60
67
|
createdFrom: z.string().optional().describe('ISO date lower bound'),
|
|
61
68
|
createdTo: z.string().optional().describe('ISO date upper bound'),
|
|
62
69
|
limit: z.number().int().min(1).max(MAX_RESULTS).optional(),
|
|
63
70
|
},
|
|
64
|
-
}, async ({ query, tags, createdFrom, createdTo, limit }) => {
|
|
71
|
+
}, async ({ query, tags, createdFrom, createdTo, limit, offset = 0, boxes }) => {
|
|
65
72
|
try {
|
|
66
73
|
const result = (await call('note.search', {
|
|
67
74
|
keyword: query,
|
|
@@ -69,26 +76,34 @@ function registerKnowledgeTools(server, call) {
|
|
|
69
76
|
tagsExpression: tags || undefined,
|
|
70
77
|
createdAtFrom: createdFrom,
|
|
71
78
|
createdAtTo: createdTo,
|
|
72
|
-
boxValues: [],
|
|
79
|
+
boxValues: boxes ?? [],
|
|
73
80
|
matchEmptyZettelBoxes: false,
|
|
74
81
|
limit: limit ?? 10,
|
|
75
|
-
offset
|
|
82
|
+
offset,
|
|
76
83
|
}));
|
|
77
|
-
|
|
84
|
+
const nextOffset = offset + result.rows.length;
|
|
85
|
+
const hasMore = nextOffset < result.totalCount;
|
|
86
|
+
return text({ totalCount: result.totalCount, notes: result.rows, hasMore, ...(hasMore ? { nextOffset } : {}), stale: result.stale === true });
|
|
78
87
|
}
|
|
79
88
|
catch (error) {
|
|
80
89
|
return failure(error);
|
|
81
90
|
}
|
|
82
91
|
});
|
|
83
92
|
server.registerTool('read_note', {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
94
|
+
description: 'Read one Dinox note as Markdown, with title, tags, folders and properties. Use nextOffset to page through truncated content.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
id: z.string().describe('Note id from search_notes'),
|
|
97
|
+
offset: z.number().int().min(0).optional().describe('Character offset into the Markdown content; use nextOffset from a truncated result'),
|
|
98
|
+
},
|
|
99
|
+
}, async ({ id, offset = 0 }) => {
|
|
87
100
|
try {
|
|
88
|
-
const
|
|
101
|
+
const result = (await call('note.detail', { id }));
|
|
102
|
+
const { note } = result;
|
|
89
103
|
if (!note)
|
|
90
104
|
return { ...text(`Note not found: ${id}`), isError: true };
|
|
91
105
|
const markdown = String(note.content_md ?? note.content_text ?? '');
|
|
106
|
+
const truncated = offset + MAX_NOTE_CHARS < markdown.length;
|
|
92
107
|
return text({
|
|
93
108
|
id: note.id,
|
|
94
109
|
title: note.title,
|
|
@@ -97,8 +112,11 @@ function registerKnowledgeTools(server, call) {
|
|
|
97
112
|
properties: note.properties,
|
|
98
113
|
createdAt: note.created_at,
|
|
99
114
|
updatedAt: note.updated_at,
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
totalChars: markdown.length,
|
|
116
|
+
truncated,
|
|
117
|
+
...(truncated ? { nextOffset: offset + MAX_NOTE_CHARS } : {}),
|
|
118
|
+
content: markdown.slice(offset, offset + MAX_NOTE_CHARS),
|
|
119
|
+
stale: result.stale === true,
|
|
102
120
|
});
|
|
103
121
|
}
|
|
104
122
|
catch (error) {
|
|
@@ -106,6 +124,7 @@ function registerKnowledgeTools(server, call) {
|
|
|
106
124
|
}
|
|
107
125
|
});
|
|
108
126
|
server.registerTool('search_todos', {
|
|
127
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
109
128
|
description: "Search the user's Dinox todo items.",
|
|
110
129
|
inputSchema: {
|
|
111
130
|
query: z.string().describe('Keywords; empty string lists all'),
|
|
@@ -129,6 +148,7 @@ function registerKnowledgeTools(server, call) {
|
|
|
129
148
|
}
|
|
130
149
|
});
|
|
131
150
|
server.registerTool('related_notes', {
|
|
151
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
132
152
|
description: 'Notes linked to a note (backlinks and outlinks, up to the given depth).',
|
|
133
153
|
inputSchema: {
|
|
134
154
|
id: z.string().describe('Note id'),
|
|
@@ -143,6 +163,7 @@ function registerKnowledgeTools(server, call) {
|
|
|
143
163
|
}
|
|
144
164
|
});
|
|
145
165
|
server.registerTool('read_conversation', {
|
|
166
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
146
167
|
description: 'Read messages of a Dinox conversation, oldest first: who said what (the user, or which agent answered). ' +
|
|
147
168
|
'Use it when the conversation context you were given says earlier messages are not shown. Returns the newest ' +
|
|
148
169
|
'messages before `before`; pass the first returned id as `before` to page further back.',
|
|
@@ -30,8 +30,13 @@ export type RunExecutorDeps = {
|
|
|
30
30
|
export declare function agentInstructionsPreamble(instructions: string): string | undefined;
|
|
31
31
|
/** How the user sees this conversation, so the agent hands over files instead of printing their paths. */
|
|
32
32
|
export declare const SESSION_DELIVERY_NOTE: string;
|
|
33
|
-
/**
|
|
34
|
-
export declare
|
|
33
|
+
/** The knowledge tools available to the agent in Dinox. */
|
|
34
|
+
export declare const DINOX_KNOWLEDGE_NOTE: string;
|
|
35
|
+
/** What a new session starts with: available tools, then the user's instructions. */
|
|
36
|
+
export declare function sessionPreamble(instructions: string, options: {
|
|
37
|
+
delivery: boolean;
|
|
38
|
+
knowledge: boolean;
|
|
39
|
+
}): string | undefined;
|
|
35
40
|
/**
|
|
36
41
|
* Runs accepted turns on one gateway. A thread has at most one active run;
|
|
37
42
|
* turns sent meanwhile wait in a per-thread FIFO and, when it ends, run
|
|
@@ -49,9 +49,15 @@ export const SESSION_DELIVERY_NOTE = [
|
|
|
49
49
|
'To show them a file you made or found (an image, a document, a chart, ...), call the Dinox send_file tool with its path.',
|
|
50
50
|
'</dinox_delivery>',
|
|
51
51
|
].join('\n');
|
|
52
|
-
/**
|
|
53
|
-
export
|
|
54
|
-
|
|
52
|
+
/** The knowledge tools available to the agent in Dinox. */
|
|
53
|
+
export const DINOX_KNOWLEDGE_NOTE = [
|
|
54
|
+
'<dinox_knowledge>',
|
|
55
|
+
"The Dinox tools can search and read the user's Dinox notes, todos and conversations (search_notes, read_note, search_todos, related_notes, read_conversation). The dinox_* tools can also create, update and organize Dinox data. Confirm with the user before writes they did not request, and dry-run high-risk actions first. Use them when the request may involve what the user has written down in Dinox.",
|
|
56
|
+
'</dinox_knowledge>',
|
|
57
|
+
].join('\n');
|
|
58
|
+
/** What a new session starts with: available tools, then the user's instructions. */
|
|
59
|
+
export function sessionPreamble(instructions, options) {
|
|
60
|
+
return [options.delivery && SESSION_DELIVERY_NOTE, options.knowledge && DINOX_KNOWLEDGE_NOTE, agentInstructionsPreamble(instructions)].filter(Boolean).join('\n\n') || undefined;
|
|
55
61
|
}
|
|
56
62
|
function withErrorText(message, error) {
|
|
57
63
|
return { ...message, metadata: { ...(message.metadata ?? {}), error } };
|
|
@@ -323,7 +329,7 @@ export class GatewayRunExecutor {
|
|
|
323
329
|
const result = await adapter.runTurn({
|
|
324
330
|
sessionId: continued?.sessionId ?? null,
|
|
325
331
|
cwd,
|
|
326
|
-
sessionPreamble: this.deps.uploadFile
|
|
332
|
+
sessionPreamble: sessionPreamble(instructions, { delivery: !!this.deps.uploadFile, knowledge: gateway.dinoxTools !== false }),
|
|
327
333
|
prompt: withConversationContext(prompt, context, { canReadConversation: gateway.dinoxTools !== false }),
|
|
328
334
|
resources,
|
|
329
335
|
signal: active.controller.signal,
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type GatewayDefinition = {
|
|
|
36
36
|
/** Extra environment for the agent process (secrets stay on this machine). */
|
|
37
37
|
env?: Record<string, string>;
|
|
38
38
|
enabled: boolean;
|
|
39
|
-
/** Offer the
|
|
39
|
+
/** Offer the Dinox knowledge tools (MCP) to the agent; default true. */
|
|
40
40
|
dinoxTools?: boolean;
|
|
41
41
|
/** Default `ask`. */
|
|
42
42
|
approval?: GatewayApprovalMode;
|