@dinoxx/dinox-cli 2.11.0 → 2.13.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/README.md +2 -2
- package/dist/commands/gateway/index.js +5 -3
- package/dist/gateway/adapters/index.js +2 -2
- package/dist/gateway/detect.js +20 -1
- 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
|
@@ -103,7 +103,7 @@ winget install OpenJS.NodeJS.LTS
|
|
|
103
103
|
dino update
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
它会自动根据你当前的安装方式选择合适的包管理器执行更新。升级完成后会检查本机已安装的 Agent:有 `claude` 但没有 ACP 命令时安装 `@agentclientprotocol/claude-agent-acp`,有 `codex` 时安装 `@agentclientprotocol/codex-acp`,有 `zcode` 时安装 `zcode-acp`。已经装过的适配器会跳过;某个适配器安装失败只给出警告,不影响 CLI 升级本身。
|
|
106
|
+
它会自动根据你当前的安装方式选择合适的包管理器执行更新。升级完成后会检查本机已安装的 Agent:有 `claude` 但没有 ACP 命令时安装 `@agentclientprotocol/claude-agent-acp`,有 `codex` 时安装 `@agentclientprotocol/codex-acp`,有 `zcode` 时安装 `zcode-acp`,有 `pi` 时安装 `pi-acp`,有 `agy` 时安装 `agy-acp`,有 `muse` 时安装 `@bex-co/muse-code-acp`。已经装过的适配器会跳过;某个适配器安装失败只给出警告,不影响 CLI 升级本身。
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
@@ -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)
|
|
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,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: [
|
package/dist/gateway/detect.js
CHANGED
|
@@ -29,7 +29,12 @@ export const KNOWN_ACP_AGENTS = [
|
|
|
29
29
|
{ id: 'copilot', name: 'GitHub Copilot', candidates: [['copilot', '--acp']] },
|
|
30
30
|
{ id: 'cursor', name: 'Cursor Agent', candidates: [['cursor-agent', 'acp']] },
|
|
31
31
|
{ id: 'opencode', name: 'OpenCode', candidates: [['opencode', 'acp']] },
|
|
32
|
-
{
|
|
32
|
+
{
|
|
33
|
+
id: 'pi',
|
|
34
|
+
name: 'pi',
|
|
35
|
+
candidates: [['pi-acp']],
|
|
36
|
+
adapter: { baseBinary: 'pi', packageName: 'pi-acp' },
|
|
37
|
+
},
|
|
33
38
|
{ id: 'goose', name: 'Goose', candidates: [['goose', 'acp']] },
|
|
34
39
|
{ id: 'qwen', name: 'Qwen Code', candidates: [['qwen', '--acp'], ['qwen', '--acp', '--experimental-skills'], ['qwen', '--experimental-acp']] },
|
|
35
40
|
{ id: 'kimi', name: 'Kimi Code CLI', candidates: [['kimi', 'acp'], ['kimi', '--acp']] },
|
|
@@ -43,7 +48,21 @@ export const KNOWN_ACP_AGENTS = [
|
|
|
43
48
|
adapter: { baseBinary: 'zcode', packageName: 'zcode-acp' },
|
|
44
49
|
},
|
|
45
50
|
// Launch lines follow Paseo's ACP catalog (packages/app/src/data/acp-provider-catalog.ts).
|
|
51
|
+
// Pi, Antigravity, Muse, and OMP are native in Paseo; here they run through ACP adapters.
|
|
46
52
|
// Local binaries only: detect must not run `npx -y`, which would download a package just to probe it.
|
|
53
|
+
{
|
|
54
|
+
id: 'antigravity',
|
|
55
|
+
name: 'Antigravity',
|
|
56
|
+
candidates: [['agy-acp']],
|
|
57
|
+
adapter: { baseBinary: 'agy', packageName: 'agy-acp' },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'muse',
|
|
61
|
+
name: 'Muse Code',
|
|
62
|
+
candidates: [['muse-code-acp'], ['muse-acp']],
|
|
63
|
+
adapter: { baseBinary: 'muse', packageName: '@bex-co/muse-code-acp' },
|
|
64
|
+
},
|
|
65
|
+
{ id: 'omp', name: 'OMP', candidates: [['omp', 'acp'], ['omp-acp']] },
|
|
47
66
|
{ id: 'amp', name: 'Amp', candidates: [['amp-acp']] },
|
|
48
67
|
{
|
|
49
68
|
id: 'auggie',
|
|
@@ -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;
|