@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.
Files changed (47) hide show
  1. package/README.md +17 -3
  2. package/dist/cli.js +2 -0
  3. package/dist/commands/gateway/index.d.ts +4 -0
  4. package/dist/commands/gateway/index.js +300 -0
  5. package/dist/commands/schema/renderSkills.js +4 -0
  6. package/dist/commands/schema/spec.js +2 -0
  7. package/dist/config/paths.d.ts +1 -0
  8. package/dist/config/paths.js +1 -1
  9. package/dist/daemon/server.d.ts +1 -0
  10. package/dist/daemon/server.js +31 -0
  11. package/dist/gateway/adapters/acp.d.ts +42 -0
  12. package/dist/gateway/adapters/acp.js +285 -0
  13. package/dist/gateway/adapters/echo.d.ts +12 -0
  14. package/dist/gateway/adapters/echo.js +51 -0
  15. package/dist/gateway/adapters/index.d.ts +9 -0
  16. package/dist/gateway/adapters/index.js +30 -0
  17. package/dist/gateway/assistantProjection.d.ts +32 -0
  18. package/dist/gateway/assistantProjection.js +193 -0
  19. package/dist/gateway/config.d.ts +50 -0
  20. package/dist/gateway/config.js +253 -0
  21. package/dist/gateway/detect.d.ts +40 -0
  22. package/dist/gateway/detect.js +145 -0
  23. package/dist/gateway/deviceHost.d.ts +82 -0
  24. package/dist/gateway/deviceHost.js +530 -0
  25. package/dist/gateway/mcpServer.d.ts +4 -0
  26. package/dist/gateway/mcpServer.js +118 -0
  27. package/dist/gateway/protocol.d.ts +29 -0
  28. package/dist/gateway/protocol.js +127 -0
  29. package/dist/gateway/runExecutor.d.ts +67 -0
  30. package/dist/gateway/runExecutor.js +316 -0
  31. package/dist/gateway/service.d.ts +26 -0
  32. package/dist/gateway/service.js +82 -0
  33. package/dist/gateway/streamPublisher.d.ts +33 -0
  34. package/dist/gateway/streamPublisher.js +93 -0
  35. package/dist/gateway/threadStore.d.ts +157 -0
  36. package/dist/gateway/threadStore.js +430 -0
  37. package/dist/gateway/types.d.ts +117 -0
  38. package/dist/gateway/types.js +9 -0
  39. package/dist/vendor/electron/domain/properties.js +12 -1
  40. package/dist/vendor/electron/domain/saved-views.js +4 -1
  41. package/dist/vendor/electron/shared/external-types.d.ts +18 -0
  42. package/dist/vendor/electron/shared/note-properties.d.ts +16 -0
  43. package/dist/vendor/electron/shared/note-properties.js +45 -0
  44. package/dist/vendor/electron/shared/powersync-read-models.d.ts +7 -1
  45. package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
  46. package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
  47. 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 20.18.1+、22、23、24、25(不支持 Node.js 21 或 26+)
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 `info`:查看版本与技能仓库地址
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.16 `update`:升级 CLI
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,4 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../descriptor/types.js';
3
+ export declare const gatewayCommandGroup: CommandGroupDescriptor;
4
+ export declare function registerGatewayCommands(program: Command): void;
@@ -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),
@@ -1,3 +1,4 @@
1
+ export declare function normalizeUserPathSegment(userId: string): string;
1
2
  export declare function getConfigDir(): string;
2
3
  export declare function getConfigFilePath(): string;
3
4
  export declare function getDataDir(): string;
@@ -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 === '..') {
@@ -12,6 +12,7 @@ export declare class DaemonServer {
12
12
  private ownsArtifacts;
13
13
  private releaseOwner;
14
14
  private stopPromise;
15
+ private gateways;
15
16
  initialize(): Promise<void>;
16
17
  handleRequest(req: DaemonRequest): Promise<DaemonResponse>;
17
18
  private isCacheStale;
@@ -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 {};