@dinoxx/dinox-cli 2.3.0 → 2.4.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 +17 -3
- package/dist/cli.js +2 -0
- package/dist/commands/gateway/index.d.ts +4 -0
- package/dist/commands/gateway/index.js +300 -0
- package/dist/commands/schema/renderSkills.js +4 -0
- package/dist/commands/schema/spec.js +2 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +31 -0
- package/dist/gateway/adapters/acp.d.ts +42 -0
- package/dist/gateway/adapters/acp.js +285 -0
- package/dist/gateway/adapters/echo.d.ts +12 -0
- package/dist/gateway/adapters/echo.js +51 -0
- package/dist/gateway/adapters/index.d.ts +9 -0
- package/dist/gateway/adapters/index.js +30 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +50 -0
- package/dist/gateway/config.js +253 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +82 -0
- package/dist/gateway/deviceHost.js +530 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +118 -0
- package/dist/gateway/protocol.d.ts +29 -0
- package/dist/gateway/protocol.js +127 -0
- package/dist/gateway/runExecutor.d.ts +67 -0
- package/dist/gateway/runExecutor.js +316 -0
- package/dist/gateway/service.d.ts +26 -0
- package/dist/gateway/service.js +82 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +157 -0
- package/dist/gateway/threadStore.js +430 -0
- package/dist/gateway/types.d.ts +117 -0
- package/dist/gateway/types.js +9 -0
- package/dist/vendor/electron/domain/properties.js +12 -1
- package/dist/vendor/electron/domain/saved-views.js +4 -1
- package/dist/vendor/electron/shared/external-types.d.ts +18 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +16 -0
- package/dist/vendor/electron/shared/note-properties.js +45 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +7 -1
- package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
- package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Dinox CLI 适合这几类场景:
|
|
|
52
52
|
|
|
53
53
|
安装前请先确认:
|
|
54
54
|
|
|
55
|
-
- 推荐使用 Node.js 24 LTS;支持 Node.js
|
|
55
|
+
- 推荐使用 Node.js 24 LTS;支持 Node.js 22、23、24、25(不支持 Node.js 21 及以下或 26+)
|
|
56
56
|
- 已安装 npm
|
|
57
57
|
- 你的系统可以正常执行全局 npm 命令
|
|
58
58
|
|
|
@@ -937,13 +937,27 @@ daemon 通过用户数据目录下的私有 Unix domain socket 通信,Windows
|
|
|
937
937
|
|
|
938
938
|
需要在当前进程打开 PowerSync SQLite 的命令(例如写命令、`sync`、在线 `auth status`、`--offline` 或 `--require-sync` 读取)会先取得跨进程生命周期锁。如果同一用户数据库正由健康 daemon 持有,CLI 会暂时停止该实例;只有在命令结束、数据库关闭且 owner lease 成功释放后,才会按原端口恢复它。使用其他用户数据库的 daemon 不会被停止;如果关闭或释放失败,CLI 会保留 daemon 为停止状态并在 stderr 报告警告,避免两个运行时同时持有同一 SQLite 数据库。
|
|
939
939
|
|
|
940
|
-
### 6.15 `
|
|
940
|
+
### 6.15 `gateway`:把本机 Agent 挂成远程设备
|
|
941
|
+
|
|
942
|
+
```bash
|
|
943
|
+
dino gateway detect
|
|
944
|
+
dino gateway detect --add
|
|
945
|
+
dino gateway add Hermes --command hermes --arg acp
|
|
946
|
+
dino gateway list
|
|
947
|
+
dino gateway status
|
|
948
|
+
dino gateway reload
|
|
949
|
+
dino gateway remove Hermes
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
`detect` 会在 `PATH` 上查找 ACP agent,并用真实握手确认能否启动。`--add` 把已确认的 agent 写入当前账号的 gateway 配置,并由 daemon 托管。`gateway list` 只显示环境变量名,不显示值。`dino gateway mcp` 由这些 agent 拉起,对外提供只读的 Dinox 知识工具。
|
|
953
|
+
|
|
954
|
+
### 6.16 `info`:查看版本与技能仓库地址
|
|
941
955
|
|
|
942
956
|
```bash
|
|
943
957
|
dino info
|
|
944
958
|
```
|
|
945
959
|
|
|
946
|
-
### 6.
|
|
960
|
+
### 6.17 `update`:升级 CLI
|
|
947
961
|
|
|
948
962
|
```bash
|
|
949
963
|
dino update
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerAuthCommands } from './commands/auth/index.js';
|
|
|
3
3
|
import { registerBoxesCommand } from './commands/boxes/index.js';
|
|
4
4
|
import { registerConfigCommands } from './commands/config/index.js';
|
|
5
5
|
import { registerDaemonCommands } from './daemon/index.js';
|
|
6
|
+
import { registerGatewayCommands } from './commands/gateway/index.js';
|
|
6
7
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
7
8
|
import { registerGraphCommands } from './commands/graph/index.js';
|
|
8
9
|
import { registerInfoCommand } from './commands/info/index.js';
|
|
@@ -79,6 +80,7 @@ export function createCli() {
|
|
|
79
80
|
registerBoxesCommand(program);
|
|
80
81
|
registerConfigCommands(program);
|
|
81
82
|
registerDaemonCommands(program);
|
|
83
|
+
registerGatewayCommands(program);
|
|
82
84
|
registerDoctorCommand(program);
|
|
83
85
|
registerGraphCommands(program);
|
|
84
86
|
registerInfoCommand(program);
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { isDaemonRunning, sendDaemonRequest } from '../../daemon/client.js';
|
|
2
|
+
import { ensureDaemonRunning } from '../../daemon/ensure.js';
|
|
3
|
+
import { loadConfig } from '../../config/store.js';
|
|
4
|
+
import { addGateway, adoptDetectedAgent, getGatewaysFilePath, loadGateways, removeGateway, } from '../../gateway/config.js';
|
|
5
|
+
import { detectAcpAgents, inferAgentId } from '../../gateway/detect.js';
|
|
6
|
+
import { serveDinoxMcpOverStdio } from '../../gateway/mcpServer.js';
|
|
7
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
8
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../../utils/output.js';
|
|
9
|
+
import { registerCommandGroup } from '../descriptor/register.js';
|
|
10
|
+
import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
|
|
11
|
+
const GATEWAY_KINDS = ['acp', 'echo'];
|
|
12
|
+
function collect(value, previous) {
|
|
13
|
+
return [...(Array.isArray(previous) ? previous : []), value];
|
|
14
|
+
}
|
|
15
|
+
function parseEnv(entries) {
|
|
16
|
+
const env = {};
|
|
17
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
18
|
+
const separator = entry.indexOf('=');
|
|
19
|
+
if (separator <= 0) {
|
|
20
|
+
throw new DinoxError(`--env expects KEY=VALUE, got "${entry}"`, { code: 'INVALID_ARGUMENT' });
|
|
21
|
+
}
|
|
22
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
/** Gateways belong to the logged-in account. */
|
|
27
|
+
async function currentUserId() {
|
|
28
|
+
return (await loadConfig()).auth?.userId ?? '';
|
|
29
|
+
}
|
|
30
|
+
/** Public view: never echoes env values (they may hold API keys). */
|
|
31
|
+
function describeGateway(gateway) {
|
|
32
|
+
return {
|
|
33
|
+
id: gateway.id,
|
|
34
|
+
name: gateway.name,
|
|
35
|
+
kind: gateway.kind,
|
|
36
|
+
agentId: gateway.agentId ?? null,
|
|
37
|
+
deviceId: gateway.deviceId,
|
|
38
|
+
command: gateway.command ?? null,
|
|
39
|
+
args: gateway.args ?? [],
|
|
40
|
+
cwd: gateway.cwd ?? null,
|
|
41
|
+
envKeys: Object.keys(gateway.env ?? {}),
|
|
42
|
+
enabled: gateway.enabled,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Applies the account's gateways to the running daemon, starting it when needed. */
|
|
46
|
+
async function applyToDaemon() {
|
|
47
|
+
if (!(await isDaemonRunning())) {
|
|
48
|
+
await ensureDaemonRunning();
|
|
49
|
+
return (await sendDaemonRequest('gateway.status')).gateways;
|
|
50
|
+
}
|
|
51
|
+
return (await sendDaemonRequest('gateway.reload', undefined, { timeoutMs: 30_000 })).gateways;
|
|
52
|
+
}
|
|
53
|
+
function printStatuses(statuses) {
|
|
54
|
+
if (statuses.length === 0) {
|
|
55
|
+
printHumanReadableLine('No gateways are running.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const status of statuses) {
|
|
59
|
+
const busy = status.busyThreadIds.length ? ` · ${status.busyThreadIds.length} running` : '';
|
|
60
|
+
const error = status.lastError ? ` · ${status.lastError}` : '';
|
|
61
|
+
printHumanReadableLine(`${status.name} (${status.kind}, ${status.deviceId}): ${status.state}${busy}${error}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function runAdd(context) {
|
|
65
|
+
const kind = context.options.kind ?? 'acp';
|
|
66
|
+
if (!GATEWAY_KINDS.includes(kind)) {
|
|
67
|
+
throw new DinoxError(`--kind must be one of ${GATEWAY_KINDS.join(', ')}`, { code: 'INVALID_ARGUMENT' });
|
|
68
|
+
}
|
|
69
|
+
const command = context.options.command;
|
|
70
|
+
const args = context.options.arg;
|
|
71
|
+
const gateway = await addGateway(await currentUserId(), {
|
|
72
|
+
name: context.args.name ?? '',
|
|
73
|
+
kind,
|
|
74
|
+
// A manual launch of a known agent counts as that agent, so detect never hosts it twice.
|
|
75
|
+
agentId: kind === 'acp' ? inferAgentId(command, args) : undefined,
|
|
76
|
+
command,
|
|
77
|
+
args,
|
|
78
|
+
cwd: context.options.cwd,
|
|
79
|
+
env: parseEnv(context.options.env),
|
|
80
|
+
});
|
|
81
|
+
const statuses = await applyToDaemon();
|
|
82
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
83
|
+
printSuccess({ gateway: describeGateway(gateway), status: statuses.find((item) => item.gatewayId === gateway.id) ?? null });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
printHumanReadableLine(`Added gateway "${gateway.name}" as device ${gateway.deviceId}.`);
|
|
87
|
+
printStatuses(statuses);
|
|
88
|
+
}
|
|
89
|
+
async function runDetect(context) {
|
|
90
|
+
const detected = await detectAcpAgents({ probe: true });
|
|
91
|
+
const added = [];
|
|
92
|
+
const updated = [];
|
|
93
|
+
if (context.options.add) {
|
|
94
|
+
const userId = await currentUserId();
|
|
95
|
+
for (const agent of detected) {
|
|
96
|
+
if (!agent.verified)
|
|
97
|
+
continue;
|
|
98
|
+
const result = await adoptDetectedAgent(userId, agent);
|
|
99
|
+
if (result.change === 'added')
|
|
100
|
+
added.push(result.gateway);
|
|
101
|
+
if (result.change === 'updated')
|
|
102
|
+
updated.push(result.gateway);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const statuses = added.length || updated.length ? await applyToDaemon() : [];
|
|
106
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
107
|
+
printSuccess({
|
|
108
|
+
agents: detected,
|
|
109
|
+
added: added.map(describeGateway),
|
|
110
|
+
updated: updated.map(describeGateway),
|
|
111
|
+
gateways: statuses,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (detected.length === 0) {
|
|
116
|
+
printHumanReadableLine('No ACP agents found on PATH.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const agent of detected) {
|
|
120
|
+
const launch = [agent.command, ...agent.args].join(' ');
|
|
121
|
+
const label = agent.verified
|
|
122
|
+
? `ready${agent.agentVersion ? ` (${agent.agentName ?? agent.name} ${agent.agentVersion})` : ''}`
|
|
123
|
+
: agent.installHint
|
|
124
|
+
? `installed, needs its ACP adapter: ${agent.installHint}`
|
|
125
|
+
: `not responding: ${agent.error ?? 'unknown'}`;
|
|
126
|
+
printHumanReadableLine(`${agent.name}: ${label} · ${launch}`);
|
|
127
|
+
}
|
|
128
|
+
for (const gateway of added)
|
|
129
|
+
printHumanReadableLine(`Added gateway "${gateway.name}" (${gateway.deviceId}).`);
|
|
130
|
+
for (const gateway of updated) {
|
|
131
|
+
printHumanReadableLine(`Updated gateway "${gateway.name}": ${[gateway.command, ...(gateway.args ?? [])].join(' ')}`);
|
|
132
|
+
}
|
|
133
|
+
if (!context.options.add && detected.some((agent) => agent.verified)) {
|
|
134
|
+
printHumanReadableLine('Run `dino gateway detect --add` to host the ready agents.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function runList() {
|
|
138
|
+
const userId = await currentUserId();
|
|
139
|
+
const gateways = (await loadGateways(userId)).map(describeGateway);
|
|
140
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
141
|
+
printSuccess({ gateways, file: getGatewaysFilePath(userId) });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (gateways.length === 0) {
|
|
145
|
+
printHumanReadableLine('No gateways configured. Add one with `dino gateway add <name> --command <agent>`.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const gateway of gateways) {
|
|
149
|
+
const command = gateway.command ? ` · ${[gateway.command, ...gateway.args].join(' ')}` : '';
|
|
150
|
+
printHumanReadableLine(`${gateway.name} (${gateway.kind}, ${gateway.deviceId})${command}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function runRemove(context) {
|
|
154
|
+
const gateway = await removeGateway(await currentUserId(), context.args.gateway ?? '');
|
|
155
|
+
const statuses = (await isDaemonRunning()) ? await applyToDaemon() : [];
|
|
156
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
157
|
+
printSuccess({ removed: describeGateway(gateway), gateways: statuses });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
printHumanReadableLine(`Removed gateway "${gateway.name}".`);
|
|
161
|
+
}
|
|
162
|
+
async function runStatus() {
|
|
163
|
+
await ensureDaemonRunning();
|
|
164
|
+
const { gateways } = (await sendDaemonRequest('gateway.status'));
|
|
165
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
166
|
+
printSuccess({ gateways });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
printStatuses(gateways);
|
|
170
|
+
}
|
|
171
|
+
async function runReload() {
|
|
172
|
+
const gateways = await applyToDaemon();
|
|
173
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
174
|
+
printSuccess({ gateways });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
printStatuses(gateways);
|
|
178
|
+
}
|
|
179
|
+
const STATUS_FIELDS = ['gatewayId', 'name', 'deviceId', 'kind', 'state', 'connectedAt', 'lastError', 'busyThreadIds', 'agentName'];
|
|
180
|
+
export const gatewayCommandGroup = {
|
|
181
|
+
name: 'gateway',
|
|
182
|
+
description: 'Host external agents (Hermes, OpenClaw, pi, Claude Code, ...) as Dinox remote devices',
|
|
183
|
+
commands: [
|
|
184
|
+
{
|
|
185
|
+
name: 'add',
|
|
186
|
+
description: 'Add an agent gateway and start hosting it in the daemon',
|
|
187
|
+
arguments: [{ syntax: '<name>', description: 'Display name shown in the Dinox app' }],
|
|
188
|
+
options: [
|
|
189
|
+
{ flags: '--kind <kind>', description: 'Agent runtime protocol', type: 'string', enum: GATEWAY_KINDS, defaultValue: 'acp' },
|
|
190
|
+
{ flags: '--command <command>', description: 'ACP agent executable (e.g. hermes, openclaw, claude-code-acp)', type: 'string' },
|
|
191
|
+
{ flags: '--arg <value>', description: 'Argument passed to the agent (repeatable, e.g. --arg acp)', type: 'string[]', parser: collect },
|
|
192
|
+
{ flags: '--cwd <dir>', description: 'Working directory for the agent sessions', type: 'string' },
|
|
193
|
+
{ flags: '--env <KEY=VALUE>', description: 'Environment for the agent process, stored locally (repeatable)', type: 'string[]', parser: collect },
|
|
194
|
+
],
|
|
195
|
+
schema: {
|
|
196
|
+
path: 'gateway.add',
|
|
197
|
+
risk: 'write',
|
|
198
|
+
auth: 'required',
|
|
199
|
+
globalOptions: FORMAT_OPTIONS,
|
|
200
|
+
output: {
|
|
201
|
+
type: 'structured',
|
|
202
|
+
summary: 'The added gateway and its live device status',
|
|
203
|
+
fields: ['gateway', 'status'],
|
|
204
|
+
},
|
|
205
|
+
examples: [
|
|
206
|
+
'dino gateway add Hermes --command hermes --arg acp --format json',
|
|
207
|
+
'dino gateway add Echo --kind echo --format json',
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
action: runAdd,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'detect',
|
|
214
|
+
description: 'Find ACP agents installed on this machine (verified with a real handshake)',
|
|
215
|
+
options: [{ flags: '--add', description: 'Add every verified agent that is not configured yet', type: 'boolean' }],
|
|
216
|
+
schema: {
|
|
217
|
+
path: 'gateway.detect',
|
|
218
|
+
risk: 'write',
|
|
219
|
+
auth: 'none',
|
|
220
|
+
globalOptions: FORMAT_OPTIONS,
|
|
221
|
+
output: {
|
|
222
|
+
type: 'structured',
|
|
223
|
+
summary: 'Detected agents, gateways added, and their live statuses',
|
|
224
|
+
fields: ['agents', 'added', 'gateways'],
|
|
225
|
+
},
|
|
226
|
+
examples: ['dino gateway detect --format json', 'dino gateway detect --add --format json'],
|
|
227
|
+
},
|
|
228
|
+
action: runDetect,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'list',
|
|
232
|
+
description: 'List configured agent gateways (environment values are never shown)',
|
|
233
|
+
schema: {
|
|
234
|
+
path: 'gateway.list',
|
|
235
|
+
risk: 'read',
|
|
236
|
+
auth: 'none',
|
|
237
|
+
globalOptions: FORMAT_OPTIONS,
|
|
238
|
+
output: { type: 'structured', summary: 'Configured gateways', fields: ['gateways', 'file'] },
|
|
239
|
+
examples: ['dino gateway list --format json'],
|
|
240
|
+
},
|
|
241
|
+
action: runList,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'remove',
|
|
245
|
+
description: 'Remove an agent gateway and stop hosting it',
|
|
246
|
+
arguments: [{ syntax: '<gateway>', description: 'Gateway name, id, or device id' }],
|
|
247
|
+
schema: {
|
|
248
|
+
path: 'gateway.remove',
|
|
249
|
+
risk: 'write',
|
|
250
|
+
auth: 'none',
|
|
251
|
+
globalOptions: FORMAT_OPTIONS,
|
|
252
|
+
output: { type: 'structured', summary: 'The removed gateway and remaining statuses', fields: ['removed', 'gateways'] },
|
|
253
|
+
examples: ['dino gateway remove Hermes --format json'],
|
|
254
|
+
},
|
|
255
|
+
action: runRemove,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'status',
|
|
259
|
+
description: 'Show live connection status of each hosted gateway',
|
|
260
|
+
schema: {
|
|
261
|
+
path: 'gateway.status',
|
|
262
|
+
risk: 'read',
|
|
263
|
+
auth: 'required',
|
|
264
|
+
globalOptions: FORMAT_OPTIONS,
|
|
265
|
+
output: { type: 'structured', summary: 'Live gateway device statuses', fields: STATUS_FIELDS },
|
|
266
|
+
examples: ['dino gateway status --format json'],
|
|
267
|
+
},
|
|
268
|
+
action: runStatus,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'mcp',
|
|
272
|
+
description: 'Serve read-only Dinox knowledge tools over stdio MCP (started by gateway agents)',
|
|
273
|
+
schema: {
|
|
274
|
+
path: 'gateway.mcp',
|
|
275
|
+
risk: 'read',
|
|
276
|
+
auth: 'required',
|
|
277
|
+
globalOptions: [],
|
|
278
|
+
output: { type: 'structured', summary: 'MCP JSON-RPC over stdio until stdin closes', fields: [] },
|
|
279
|
+
examples: ['dino gateway mcp'],
|
|
280
|
+
},
|
|
281
|
+
action: serveDinoxMcpOverStdio,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'reload',
|
|
285
|
+
description: 'Re-read gateway configuration in the running daemon',
|
|
286
|
+
schema: {
|
|
287
|
+
path: 'gateway.reload',
|
|
288
|
+
risk: 'write',
|
|
289
|
+
auth: 'required',
|
|
290
|
+
globalOptions: FORMAT_OPTIONS,
|
|
291
|
+
output: { type: 'structured', summary: 'Live gateway device statuses after reload', fields: STATUS_FIELDS },
|
|
292
|
+
examples: ['dino gateway reload --format json'],
|
|
293
|
+
},
|
|
294
|
+
action: runReload,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
export function registerGatewayCommands(program) {
|
|
299
|
+
registerCommandGroup(program, gatewayCommandGroup);
|
|
300
|
+
}
|
|
@@ -5,6 +5,10 @@ import { listCommandSchemas } from './spec.js';
|
|
|
5
5
|
const SECTION_ORDER = [
|
|
6
6
|
{ title: 'Auth', paths: ['auth.login', 'auth.logout', 'auth.status'] },
|
|
7
7
|
{ title: 'Daemon', paths: ['daemon.start', 'daemon.status', 'daemon.restart', 'daemon.stop'] },
|
|
8
|
+
{
|
|
9
|
+
title: 'Agent Gateways',
|
|
10
|
+
paths: ['gateway.detect', 'gateway.add', 'gateway.list', 'gateway.remove', 'gateway.status', 'gateway.reload', 'gateway.mcp'],
|
|
11
|
+
},
|
|
8
12
|
{ title: 'Doctor', paths: ['doctor'] },
|
|
9
13
|
{ title: 'Sync', paths: ['sync'] },
|
|
10
14
|
{ title: 'Schema', paths: ['schema'] },
|
|
@@ -3,6 +3,7 @@ import { boxCommandGroup } from '../boxes/index.js';
|
|
|
3
3
|
import { commandSchemasFromGroupDescriptor } from '../descriptor/register.js';
|
|
4
4
|
import { standaloneCommandSchemaFromDescriptor } from '../descriptor/register.js';
|
|
5
5
|
import { configCommandGroup } from '../config/index.js';
|
|
6
|
+
import { gatewayCommandGroup } from '../gateway/index.js';
|
|
6
7
|
import { daemonCommandGroup } from '../../daemon/index.js';
|
|
7
8
|
import { doctorCommandDescriptor } from '../doctor.js';
|
|
8
9
|
import { graphCommandGroup } from '../graph/index.js';
|
|
@@ -23,6 +24,7 @@ import { updateCommandDescriptor } from '../update.js';
|
|
|
23
24
|
export const COMMAND_SCHEMAS = [
|
|
24
25
|
...commandSchemasFromGroupDescriptor(authCommandGroup),
|
|
25
26
|
...commandSchemasFromGroupDescriptor(configCommandGroup),
|
|
27
|
+
...commandSchemasFromGroupDescriptor(gatewayCommandGroup),
|
|
26
28
|
...commandSchemasFromGroupDescriptor(daemonCommandGroup),
|
|
27
29
|
standaloneCommandSchemaFromDescriptor(doctorCommandDescriptor),
|
|
28
30
|
standaloneCommandSchemaFromDescriptor(syncCommandDescriptor),
|
package/dist/config/paths.d.ts
CHANGED
package/dist/config/paths.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const POWERSYNC_DB_FILENAME = 'dinox-cli.sqlite';
|
|
4
|
-
function normalizeUserPathSegment(userId) {
|
|
4
|
+
export function normalizeUserPathSegment(userId) {
|
|
5
5
|
const normalized = userId.trim();
|
|
6
6
|
const encoded = encodeURIComponent(normalized);
|
|
7
7
|
if (encoded === '.' || encoded === '..') {
|
package/dist/daemon/server.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve
|
|
|
6
6
|
import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
7
7
|
import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, } from '../powersync/runtime.js';
|
|
8
8
|
import { executeNoteSearch } from '../commands/notes/repo.js';
|
|
9
|
+
import { getNoteDetail } from '../commands/notes/repo/read.js';
|
|
9
10
|
import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from '../commands/graph/repo.js';
|
|
10
11
|
import { executeTodoSearch } from '../commands/todo/repo.js';
|
|
11
12
|
import { ensureDaemonDir, getDaemonSocketPath, isProcessRunning, readPid, removePid, savePid, } from './pid.js';
|
|
@@ -13,6 +14,7 @@ import { getPackageVersion } from '../utils/version.js';
|
|
|
13
14
|
import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
|
|
14
15
|
import { buildStructuredErrorPayload, DinoxError } from '../utils/errors.js';
|
|
15
16
|
import { getDaemonInfo } from './client.js';
|
|
17
|
+
import { GatewayService } from '../gateway/service.js';
|
|
16
18
|
const MAX_DAEMON_REQUEST_BYTES = 1 * 1024 * 1024;
|
|
17
19
|
const DAEMON_PROCESS_VERSION = getPackageVersion();
|
|
18
20
|
async function readJsonBody(req) {
|
|
@@ -68,6 +70,7 @@ export class DaemonServer {
|
|
|
68
70
|
ownsArtifacts = false;
|
|
69
71
|
releaseOwner = null;
|
|
70
72
|
stopPromise = null;
|
|
73
|
+
gateways = null;
|
|
71
74
|
async initialize() {
|
|
72
75
|
const baseConfig = resolveConfig(await loadConfig());
|
|
73
76
|
const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs: baseConfig.sync.timeoutMs });
|
|
@@ -83,6 +86,19 @@ export class DaemonServer {
|
|
|
83
86
|
});
|
|
84
87
|
this.db = db;
|
|
85
88
|
this.releaseOwner = releaseOwner;
|
|
89
|
+
if (config.auth.userId && config.auth.authorization) {
|
|
90
|
+
// Agent gateways share the daemon's PowerSync connection; a gateway
|
|
91
|
+
// failure must never take the daemon down.
|
|
92
|
+
this.gateways = new GatewayService({
|
|
93
|
+
db,
|
|
94
|
+
userId: config.auth.userId,
|
|
95
|
+
authorization: config.auth.authorization,
|
|
96
|
+
credentialsUrl: process.env.DINOX_NATS_CREDENTIALS_URL || undefined,
|
|
97
|
+
});
|
|
98
|
+
await this.gateways.start().catch((error) => {
|
|
99
|
+
process.stderr.write(`[dinox-gateway] start failed: ${String(error)}\n`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
86
102
|
// PowerSync keeps the local cache in sync continuously while connected,
|
|
87
103
|
// so no background polling loop is needed here.
|
|
88
104
|
}
|
|
@@ -147,6 +163,18 @@ export class DaemonServer {
|
|
|
147
163
|
case 'graph.stats':
|
|
148
164
|
const graphStats = await getGraphStats(this.db);
|
|
149
165
|
return { id: req.id, ok: true, result: { ...graphStats, stale: this.isCacheStale() } };
|
|
166
|
+
case 'note.detail': {
|
|
167
|
+
const note = await getNoteDetail(this.db, String(params?.id ?? ''));
|
|
168
|
+
return { id: req.id, ok: true, result: { note, stale: this.isCacheStale() } };
|
|
169
|
+
}
|
|
170
|
+
case 'gateway.status':
|
|
171
|
+
return { id: req.id, ok: true, result: { gateways: this.gateways?.status() ?? [] } };
|
|
172
|
+
case 'gateway.reload': {
|
|
173
|
+
if (!this.gateways) {
|
|
174
|
+
throw new DinoxError('Gateways need a signed-in daemon', { code: 'AUTH_REQUIRED' });
|
|
175
|
+
}
|
|
176
|
+
return { id: req.id, ok: true, result: { gateways: await this.gateways.reload() } };
|
|
177
|
+
}
|
|
150
178
|
case 'todo.search': {
|
|
151
179
|
// Same executor as the local `dino todo search` path — no option/result drift.
|
|
152
180
|
const result = await executeTodoSearch(this.db, params);
|
|
@@ -258,6 +286,9 @@ export class DaemonServer {
|
|
|
258
286
|
}
|
|
259
287
|
}
|
|
260
288
|
async stopInternal() {
|
|
289
|
+
const gateways = this.gateways;
|
|
290
|
+
this.gateways = null;
|
|
291
|
+
await gateways?.stop().catch(() => undefined);
|
|
261
292
|
const db = this.db;
|
|
262
293
|
const releaseOwner = this.releaseOwner;
|
|
263
294
|
if (db) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type McpServer, type SessionConfigOption } from '@agentclientprotocol/sdk';
|
|
2
|
+
import type { GatewayAdapter, GatewayAdapterInfo, GatewayConfigOption, GatewayDefinition, GatewayTurnInput, GatewayTurnResult } from '../types.js';
|
|
3
|
+
type Logger = (message: string, details?: Record<string, unknown>) => void;
|
|
4
|
+
/** Select options only; boolean toggles have no use in the app yet. */
|
|
5
|
+
export declare function normalizeConfigOptions(options: SessionConfigOption[] | null | undefined): GatewayConfigOption[];
|
|
6
|
+
/**
|
|
7
|
+
* Drives any Agent Client Protocol agent over stdio (`hermes acp`, `openclaw acp`,
|
|
8
|
+
* `claude-code-acp`, `codex-acp`, `pi-acp`, ...). One long-lived process per
|
|
9
|
+
* gateway hosts every thread's session; the agent keeps the conversation, so
|
|
10
|
+
* each turn sends only the new prompt.
|
|
11
|
+
*/
|
|
12
|
+
export declare class AcpGatewayAdapter implements GatewayAdapter {
|
|
13
|
+
private readonly gateway;
|
|
14
|
+
private readonly log;
|
|
15
|
+
/** MCP servers offered to every session, e.g. the Dinox knowledge tools. */
|
|
16
|
+
private readonly mcpServers;
|
|
17
|
+
readonly kind: "acp";
|
|
18
|
+
private process;
|
|
19
|
+
private starting;
|
|
20
|
+
private readonly turns;
|
|
21
|
+
/** The latest settings any session reported: the agent-wide choices the app lists. */
|
|
22
|
+
private catalog;
|
|
23
|
+
constructor(gateway: GatewayDefinition, log?: Logger,
|
|
24
|
+
/** MCP servers offered to every session, e.g. the Dinox knowledge tools. */
|
|
25
|
+
mcpServers?: McpServer[]);
|
|
26
|
+
info(): GatewayAdapterInfo;
|
|
27
|
+
runTurn(input: GatewayTurnInput): Promise<GatewayTurnResult>;
|
|
28
|
+
describeConfig(): Promise<GatewayConfigOption[]>;
|
|
29
|
+
dispose(): Promise<void>;
|
|
30
|
+
private resolveSession;
|
|
31
|
+
/**
|
|
32
|
+
* Brings the session to the requested settings before its turn. Entries the
|
|
33
|
+
* agent does not offer are skipped, so a stale choice never fails the run.
|
|
34
|
+
*/
|
|
35
|
+
private applyConfig;
|
|
36
|
+
private rememberConfig;
|
|
37
|
+
private ensureProcess;
|
|
38
|
+
private startProcess;
|
|
39
|
+
private handleSessionUpdate;
|
|
40
|
+
private handlePermission;
|
|
41
|
+
}
|
|
42
|
+
export {};
|