@dinoxx/dinox-cli 2.3.0 → 2.5.2
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 +41 -0
- package/dist/gateway/adapters/acp.d.ts +47 -0
- package/dist/gateway/adapters/acp.js +302 -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 +31 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +60 -0
- package/dist/gateway/config.js +291 -0
- package/dist/gateway/conversationContext.d.ts +44 -0
- package/dist/gateway/conversationContext.js +75 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +91 -0
- package/dist/gateway/deviceHost.js +641 -0
- package/dist/gateway/keepAwake.d.ts +24 -0
- package/dist/gateway/keepAwake.js +64 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +138 -0
- package/dist/gateway/protocol.d.ts +116 -0
- package/dist/gateway/protocol.js +182 -0
- package/dist/gateway/runExecutor.d.ts +91 -0
- package/dist/gateway/runExecutor.js +422 -0
- package/dist/gateway/service.d.ts +30 -0
- package/dist/gateway/service.js +112 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +222 -0
- package/dist/gateway/threadStore.js +597 -0
- package/dist/gateway/types.d.ts +136 -0
- package/dist/gateway/types.js +10 -0
- package/dist/gateway/workspaceFolders.d.ts +33 -0
- package/dist/gateway/workspaceFolders.js +129 -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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { Readable, Writable } from 'node:stream';
|
|
4
|
+
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
|
|
5
|
+
import { generateId } from '../../utils/id.js';
|
|
6
|
+
import { getPackageVersion } from '../../utils/version.js';
|
|
7
|
+
const TOOL_STATUS = {
|
|
8
|
+
pending: 'pending',
|
|
9
|
+
in_progress: 'running',
|
|
10
|
+
completed: 'completed',
|
|
11
|
+
failed: 'failed',
|
|
12
|
+
};
|
|
13
|
+
const STOP_REASON = {
|
|
14
|
+
end_turn: 'end_turn',
|
|
15
|
+
max_tokens: 'max_tokens',
|
|
16
|
+
max_turn_requests: 'max_tokens',
|
|
17
|
+
refusal: 'refusal',
|
|
18
|
+
cancelled: 'cancelled',
|
|
19
|
+
};
|
|
20
|
+
function toolContentText(content) {
|
|
21
|
+
const text = (content ?? [])
|
|
22
|
+
.map((item) => (item.type === 'content' && item.content.type === 'text' ? item.content.text : ''))
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join('\n');
|
|
25
|
+
return text || undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Select options only; boolean toggles have no use in the app yet. */
|
|
28
|
+
export function normalizeConfigOptions(options) {
|
|
29
|
+
const normalized = [];
|
|
30
|
+
for (const option of options ?? []) {
|
|
31
|
+
if (option.type !== 'select')
|
|
32
|
+
continue;
|
|
33
|
+
const values = [];
|
|
34
|
+
for (const entry of option.options) {
|
|
35
|
+
if ('group' in entry) {
|
|
36
|
+
for (const value of entry.options) {
|
|
37
|
+
values.push({ value: value.value, name: value.name, description: value.description ?? null, group: entry.name });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
values.push({ value: entry.value, name: entry.name, description: entry.description ?? null, group: null });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
normalized.push({
|
|
45
|
+
id: option.id,
|
|
46
|
+
name: option.name,
|
|
47
|
+
category: option.category ?? null,
|
|
48
|
+
description: option.description ?? null,
|
|
49
|
+
currentValue: option.currentValue,
|
|
50
|
+
values,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
/** Tool kinds that only look at things; `auto_read` lets them run without asking. */
|
|
56
|
+
const READ_ONLY_TOOL_KINDS = new Set(['read', 'search', 'think']);
|
|
57
|
+
export function isReadOnlyToolKind(kind) {
|
|
58
|
+
return kind != null && READ_ONLY_TOOL_KINDS.has(kind);
|
|
59
|
+
}
|
|
60
|
+
function pickOption(options, approved) {
|
|
61
|
+
const preferred = approved ? ['allow_once', 'allow_always'] : ['reject_once', 'reject_always'];
|
|
62
|
+
for (const kind of preferred) {
|
|
63
|
+
const option = options.find((item) => item.kind === kind);
|
|
64
|
+
if (option)
|
|
65
|
+
return option;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Drives any Agent Client Protocol agent over stdio (`hermes acp`, `openclaw acp`,
|
|
71
|
+
* `claude-code-acp`, `codex-acp`, `pi-acp`, ...). One long-lived process per
|
|
72
|
+
* gateway hosts every thread's session; the agent keeps the conversation, so
|
|
73
|
+
* each turn sends only the new prompt.
|
|
74
|
+
*/
|
|
75
|
+
export class AcpGatewayAdapter {
|
|
76
|
+
gateway;
|
|
77
|
+
log;
|
|
78
|
+
mcpServers;
|
|
79
|
+
kind = 'acp';
|
|
80
|
+
process = null;
|
|
81
|
+
starting = null;
|
|
82
|
+
turns = new Map();
|
|
83
|
+
/** The latest settings any session reported: the agent-wide choices the app lists. */
|
|
84
|
+
catalog = null;
|
|
85
|
+
constructor(
|
|
86
|
+
/** Live: settings changed from the app (approval, Dinox tools) apply to the next request. */
|
|
87
|
+
gateway, log = () => { },
|
|
88
|
+
/** MCP servers offered to sessions as they are created or loaded, e.g. the Dinox knowledge tools. */
|
|
89
|
+
mcpServers = () => []) {
|
|
90
|
+
this.gateway = gateway;
|
|
91
|
+
this.log = log;
|
|
92
|
+
this.mcpServers = mcpServers;
|
|
93
|
+
}
|
|
94
|
+
info() {
|
|
95
|
+
const agentInfo = this.process?.init.agentInfo;
|
|
96
|
+
return { kind: this.kind, agentName: agentInfo?.name ?? null, agentVersion: agentInfo?.version ?? null };
|
|
97
|
+
}
|
|
98
|
+
async runTurn(input) {
|
|
99
|
+
const agent = await this.ensureProcess();
|
|
100
|
+
const { sessionId, created } = await this.resolveSession(agent, input.sessionId, input.cwd);
|
|
101
|
+
await this.applyConfig(agent, sessionId, input.config);
|
|
102
|
+
this.turns.set(sessionId, { onEvent: input.onEvent, awaitApproval: input.awaitApproval });
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
void agent.connection.cancel({ sessionId }).catch(() => undefined);
|
|
105
|
+
};
|
|
106
|
+
input.signal.addEventListener('abort', onAbort, { once: true });
|
|
107
|
+
try {
|
|
108
|
+
const text = created && input.sessionPreamble ? `${input.sessionPreamble}\n\n${input.prompt}` : input.prompt;
|
|
109
|
+
const prompt = [{ type: 'text', text }];
|
|
110
|
+
for (const resource of input.resources) {
|
|
111
|
+
prompt.push({ type: 'resource_link', uri: resource.uri, name: resource.name, mimeType: resource.mediaType ?? null });
|
|
112
|
+
}
|
|
113
|
+
const response = await agent.connection.prompt({ sessionId, prompt });
|
|
114
|
+
return { sessionId, stopReason: STOP_REASON[response.stopReason] ?? 'end_turn' };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (input.signal.aborted)
|
|
118
|
+
return { sessionId, stopReason: 'cancelled' };
|
|
119
|
+
return { sessionId, stopReason: 'error', error: error instanceof Error ? error.message : String(error) };
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
input.signal.removeEventListener('abort', onAbort);
|
|
123
|
+
this.turns.delete(sessionId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async describeConfig() {
|
|
127
|
+
if (this.catalog)
|
|
128
|
+
return this.catalog;
|
|
129
|
+
const agent = await this.ensureProcess();
|
|
130
|
+
if (this.catalog)
|
|
131
|
+
return this.catalog;
|
|
132
|
+
// No session yet: open one only to read the agent's choices. It never runs a turn,
|
|
133
|
+
// so close it where the agent allows, or it shows up in the user's session list.
|
|
134
|
+
const created = await agent.connection.newSession({ cwd: this.gateway.cwd ?? os.homedir(), mcpServers: [] });
|
|
135
|
+
this.rememberConfig(agent, created.sessionId, created.configOptions);
|
|
136
|
+
if (agent.init.agentCapabilities?.sessionCapabilities?.close) {
|
|
137
|
+
await agent.connection.closeSession({ sessionId: created.sessionId }).catch((error) => this.log('closing the settings session failed', { error: String(error) }));
|
|
138
|
+
}
|
|
139
|
+
return this.catalog ?? [];
|
|
140
|
+
}
|
|
141
|
+
async dispose() {
|
|
142
|
+
const agent = this.process;
|
|
143
|
+
this.process = null;
|
|
144
|
+
this.starting = null;
|
|
145
|
+
if (!agent)
|
|
146
|
+
return;
|
|
147
|
+
agent.child.stdin.end();
|
|
148
|
+
agent.child.kill('SIGTERM');
|
|
149
|
+
}
|
|
150
|
+
/** Continues the session (loading it in `cwd` when this process has not seen it) or starts one there. */
|
|
151
|
+
async resolveSession(agent, sessionId, cwd) {
|
|
152
|
+
if (sessionId && agent.liveSessions.has(sessionId))
|
|
153
|
+
return { sessionId, created: false };
|
|
154
|
+
if (sessionId && agent.init.agentCapabilities?.loadSession) {
|
|
155
|
+
try {
|
|
156
|
+
// No turn is registered yet, so the history the agent replays is ignored.
|
|
157
|
+
const loaded = await agent.connection.loadSession({ sessionId, cwd, mcpServers: this.mcpServers() });
|
|
158
|
+
agent.liveSessions.add(sessionId);
|
|
159
|
+
this.rememberConfig(agent, sessionId, loaded.configOptions);
|
|
160
|
+
return { sessionId, created: false };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
this.log('session/load failed; starting a new session', { sessionId, error: String(error) });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const created = await agent.connection.newSession({ cwd, mcpServers: this.mcpServers() });
|
|
167
|
+
agent.liveSessions.add(created.sessionId);
|
|
168
|
+
this.rememberConfig(agent, created.sessionId, created.configOptions);
|
|
169
|
+
return { sessionId: created.sessionId, created: true };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Brings the session to the requested settings before its turn. Entries the
|
|
173
|
+
* agent does not offer are skipped, so a stale choice never fails the run.
|
|
174
|
+
*/
|
|
175
|
+
async applyConfig(agent, sessionId, config) {
|
|
176
|
+
for (const [configId, value] of Object.entries(config ?? {})) {
|
|
177
|
+
const option = agent.sessionConfig.get(sessionId)?.find((item) => item.id === configId);
|
|
178
|
+
if (!option || option.currentValue === value)
|
|
179
|
+
continue;
|
|
180
|
+
if (!option.values.some((item) => item.value === value)) {
|
|
181
|
+
this.log('agent setting not offered; skipped', { configId, value });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const response = await agent.connection.setSessionConfigOption({ sessionId, configId, value });
|
|
186
|
+
this.rememberConfig(agent, sessionId, response.configOptions);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
this.log('agent setting failed; continuing with the current one', { configId, error: String(error) });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
rememberConfig(agent, sessionId, options) {
|
|
194
|
+
if (!options)
|
|
195
|
+
return;
|
|
196
|
+
const normalized = normalizeConfigOptions(options);
|
|
197
|
+
agent.sessionConfig.set(sessionId, normalized);
|
|
198
|
+
this.catalog = normalized;
|
|
199
|
+
}
|
|
200
|
+
ensureProcess() {
|
|
201
|
+
if (this.process)
|
|
202
|
+
return Promise.resolve(this.process);
|
|
203
|
+
this.starting ??= this.startProcess().finally(() => {
|
|
204
|
+
this.starting = null;
|
|
205
|
+
});
|
|
206
|
+
return this.starting;
|
|
207
|
+
}
|
|
208
|
+
async startProcess() {
|
|
209
|
+
const command = this.gateway.command;
|
|
210
|
+
if (!command)
|
|
211
|
+
throw new Error(`Gateway ${this.gateway.name} has no ACP command`);
|
|
212
|
+
const child = spawn(command, this.gateway.args ?? [], {
|
|
213
|
+
cwd: this.gateway.cwd ?? os.homedir(),
|
|
214
|
+
env: { ...process.env, ...(this.gateway.env ?? {}) },
|
|
215
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
216
|
+
});
|
|
217
|
+
child.stderr.on('data', (chunk) => this.log('agent stderr', { text: chunk.toString('utf8').slice(0, 2000) }));
|
|
218
|
+
const exited = new Promise((_, reject) => {
|
|
219
|
+
child.once('error', reject);
|
|
220
|
+
child.once('exit', (code, signal) => reject(new Error(`agent exited (code ${code}, signal ${signal})`)));
|
|
221
|
+
});
|
|
222
|
+
child.once('exit', () => {
|
|
223
|
+
if (this.process?.child === child)
|
|
224
|
+
this.process = null;
|
|
225
|
+
});
|
|
226
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
227
|
+
const connection = new ClientSideConnection(() => ({
|
|
228
|
+
sessionUpdate: (params) => this.handleSessionUpdate(params),
|
|
229
|
+
requestPermission: (params) => this.handlePermission(params),
|
|
230
|
+
}), stream);
|
|
231
|
+
const init = await Promise.race([
|
|
232
|
+
connection.initialize({
|
|
233
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
234
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
235
|
+
clientInfo: { name: 'dinox-gateway', version: getPackageVersion() },
|
|
236
|
+
}),
|
|
237
|
+
exited,
|
|
238
|
+
]);
|
|
239
|
+
exited.catch(() => undefined);
|
|
240
|
+
const agent = { child, connection, init, liveSessions: new Set(), sessionConfig: new Map() };
|
|
241
|
+
this.process = agent;
|
|
242
|
+
this.log('agent connected', { agent: init.agentInfo?.name ?? command, protocol: init.protocolVersion });
|
|
243
|
+
return agent;
|
|
244
|
+
}
|
|
245
|
+
handleSessionUpdate(params) {
|
|
246
|
+
const update = params.update;
|
|
247
|
+
if (update.sessionUpdate === 'config_option_update' && this.process) {
|
|
248
|
+
this.rememberConfig(this.process, params.sessionId, update.configOptions);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const turn = this.turns.get(params.sessionId);
|
|
252
|
+
if (!turn)
|
|
253
|
+
return;
|
|
254
|
+
switch (update.sessionUpdate) {
|
|
255
|
+
case 'agent_message_chunk':
|
|
256
|
+
if (update.content.type === 'text')
|
|
257
|
+
turn.onEvent({ type: 'text-delta', text: update.content.text });
|
|
258
|
+
break;
|
|
259
|
+
case 'agent_thought_chunk':
|
|
260
|
+
if (update.content.type === 'text')
|
|
261
|
+
turn.onEvent({ type: 'reasoning-delta', text: update.content.text });
|
|
262
|
+
break;
|
|
263
|
+
case 'tool_call':
|
|
264
|
+
case 'tool_call_update':
|
|
265
|
+
turn.onEvent({
|
|
266
|
+
type: 'tool',
|
|
267
|
+
toolCallId: update.toolCallId,
|
|
268
|
+
...(update.title ? { title: update.title } : {}),
|
|
269
|
+
...(update.status ? { status: TOOL_STATUS[update.status] } : {}),
|
|
270
|
+
...(update.rawInput !== undefined ? { input: update.rawInput } : {}),
|
|
271
|
+
...(update.rawOutput !== undefined || update.content
|
|
272
|
+
? { output: update.rawOutput ?? toolContentText(update.content) }
|
|
273
|
+
: {}),
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async handlePermission(params) {
|
|
281
|
+
const turn = this.turns.get(params.sessionId);
|
|
282
|
+
if (!turn)
|
|
283
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
284
|
+
if (this.gateway.approval === 'auto_read' && isReadOnlyToolKind(params.toolCall.kind)) {
|
|
285
|
+
const option = pickOption(params.options, true);
|
|
286
|
+
if (option)
|
|
287
|
+
return { outcome: { outcome: 'selected', optionId: option.optionId } };
|
|
288
|
+
}
|
|
289
|
+
const approvalId = `acp-${generateId()}`;
|
|
290
|
+
turn.onEvent({
|
|
291
|
+
type: 'approval-request',
|
|
292
|
+
approvalId,
|
|
293
|
+
toolCallId: params.toolCall.toolCallId,
|
|
294
|
+
toolName: params.toolCall.title || params.toolCall.name || 'tool',
|
|
295
|
+
input: params.toolCall.rawInput,
|
|
296
|
+
});
|
|
297
|
+
const approved = await turn.awaitApproval(approvalId);
|
|
298
|
+
turn.onEvent({ type: 'approval-resolved', approvalId, approved });
|
|
299
|
+
const option = pickOption(params.options, approved);
|
|
300
|
+
return option ? { outcome: { outcome: 'selected', optionId: option.optionId } } : { outcome: { outcome: 'cancelled' } };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GatewayAdapter, GatewayAdapterInfo, GatewayTurnInput, GatewayTurnResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loopback agent for end-to-end checks of the gateway path (app ⇄ NATS ⇄ CLI ⇄
|
|
4
|
+
* PowerSync) without a real runtime. Echoes the prompt; a prompt containing
|
|
5
|
+
* `/approve` exercises the tool-approval round trip first.
|
|
6
|
+
*/
|
|
7
|
+
export declare class EchoGatewayAdapter implements GatewayAdapter {
|
|
8
|
+
readonly kind: "echo";
|
|
9
|
+
info(): GatewayAdapterInfo;
|
|
10
|
+
runTurn(input: GatewayTurnInput): Promise<GatewayTurnResult>;
|
|
11
|
+
dispose(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { generateId } from '../../utils/id.js';
|
|
2
|
+
const CHUNK_DELAY_MS = 40;
|
|
3
|
+
function delay(ms, signal) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
if (signal.aborted)
|
|
6
|
+
return reject(signal.reason);
|
|
7
|
+
const timer = setTimeout(resolve, ms);
|
|
8
|
+
signal.addEventListener('abort', () => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
reject(signal.reason);
|
|
11
|
+
}, { once: true });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Loopback agent for end-to-end checks of the gateway path (app ⇄ NATS ⇄ CLI ⇄
|
|
16
|
+
* PowerSync) without a real runtime. Echoes the prompt; a prompt containing
|
|
17
|
+
* `/approve` exercises the tool-approval round trip first.
|
|
18
|
+
*/
|
|
19
|
+
export class EchoGatewayAdapter {
|
|
20
|
+
kind = 'echo';
|
|
21
|
+
info() {
|
|
22
|
+
return { kind: this.kind, agentName: 'echo', agentVersion: null };
|
|
23
|
+
}
|
|
24
|
+
async runTurn(input) {
|
|
25
|
+
const sessionId = input.sessionId ?? `echo-${generateId()}`;
|
|
26
|
+
try {
|
|
27
|
+
if (input.prompt.includes('/approve')) {
|
|
28
|
+
const toolCallId = `tool-${generateId()}`;
|
|
29
|
+
const approvalId = `approval-${generateId()}`;
|
|
30
|
+
input.onEvent({ type: 'tool', toolCallId, title: 'echo.write', status: 'pending', input: { text: input.prompt } });
|
|
31
|
+
input.onEvent({ type: 'approval-request', approvalId, toolCallId, toolName: 'echo.write', input: { text: input.prompt } });
|
|
32
|
+
const approved = await input.awaitApproval(approvalId);
|
|
33
|
+
input.onEvent({ type: 'approval-resolved', approvalId, approved });
|
|
34
|
+
input.onEvent(approved
|
|
35
|
+
? { type: 'tool', toolCallId, status: 'completed', output: 'approved' }
|
|
36
|
+
: { type: 'tool', toolCallId, status: 'failed', output: 'denied by user' });
|
|
37
|
+
}
|
|
38
|
+
for (const word of `Echo: ${input.prompt}`.split(/(\s+)/)) {
|
|
39
|
+
await delay(CHUNK_DELAY_MS, input.signal);
|
|
40
|
+
input.onEvent({ type: 'text-delta', text: word });
|
|
41
|
+
}
|
|
42
|
+
return { sessionId, stopReason: 'end_turn' };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (input.signal.aborted)
|
|
46
|
+
return { sessionId, stopReason: 'cancelled' };
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async dispose() { }
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from '@agentclientprotocol/sdk';
|
|
2
|
+
import type { GatewayAdapter, GatewayDefinition } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The Dinox knowledge tools as a stdio MCP server: this same CLI, run as
|
|
5
|
+
* `dino gateway mcp`, reading through the daemon. DINOX_* variables are passed
|
|
6
|
+
* on so the child finds the same config, data dir and daemon socket.
|
|
7
|
+
*/
|
|
8
|
+
export declare function dinoxMcpServerConfig(): McpServer | null;
|
|
9
|
+
export declare function createGatewayAdapter(gateway: GatewayDefinition, log: (message: string, details?: Record<string, unknown>) => void): GatewayAdapter;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AcpGatewayAdapter } from './acp.js';
|
|
2
|
+
import { EchoGatewayAdapter } from './echo.js';
|
|
3
|
+
/**
|
|
4
|
+
* The Dinox knowledge tools as a stdio MCP server: this same CLI, run as
|
|
5
|
+
* `dino gateway mcp`, reading through the daemon. DINOX_* variables are passed
|
|
6
|
+
* on so the child finds the same config, data dir and daemon socket.
|
|
7
|
+
*/
|
|
8
|
+
export function dinoxMcpServerConfig() {
|
|
9
|
+
const entrypoint = process.argv[1];
|
|
10
|
+
if (!entrypoint)
|
|
11
|
+
return null;
|
|
12
|
+
return {
|
|
13
|
+
name: 'dinox',
|
|
14
|
+
command: process.execPath,
|
|
15
|
+
args: [...process.execArgv, entrypoint, 'gateway', 'mcp'],
|
|
16
|
+
env: Object.entries(process.env)
|
|
17
|
+
.filter((entry) => entry[0].startsWith('DINOX_') && typeof entry[1] === 'string')
|
|
18
|
+
.map(([name, value]) => ({ name, value })),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function createGatewayAdapter(gateway, log) {
|
|
22
|
+
switch (gateway.kind) {
|
|
23
|
+
case 'acp': {
|
|
24
|
+
const dinoxTools = dinoxMcpServerConfig();
|
|
25
|
+
// Read per session, so turning the Dinox tools off applies to the next new conversation.
|
|
26
|
+
return new AcpGatewayAdapter(gateway, log, () => (gateway.dinoxTools !== false && dinoxTools ? [dinoxTools] : []));
|
|
27
|
+
}
|
|
28
|
+
case 'echo':
|
|
29
|
+
return new EchoGatewayAdapter();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChatUIMessage } from '../vendor/electron/shared/external-types.js';
|
|
2
|
+
import type { RemoteToolApprovalPrompt } from '../vendor/electron/shared/remote-device-agent.js';
|
|
3
|
+
import type { GatewayEvent } from './types.js';
|
|
4
|
+
export declare class AssistantProjection {
|
|
5
|
+
private readonly base;
|
|
6
|
+
private readonly parts;
|
|
7
|
+
private readonly toolIndex;
|
|
8
|
+
private readonly approvals;
|
|
9
|
+
private revisionCounter;
|
|
10
|
+
constructor(base: ChatUIMessage);
|
|
11
|
+
get revision(): number;
|
|
12
|
+
apply(event: GatewayEvent): void;
|
|
13
|
+
/** Closes streaming parts; called once when the turn ends. */
|
|
14
|
+
finish(): void;
|
|
15
|
+
snapshot(): ChatUIMessage;
|
|
16
|
+
private appendText;
|
|
17
|
+
private ensureTool;
|
|
18
|
+
private findToolByApproval;
|
|
19
|
+
private applyTool;
|
|
20
|
+
}
|
|
21
|
+
/** Same projection the desktop host sends: open approval requests found in the snapshot. */
|
|
22
|
+
/**
|
|
23
|
+
* The reply as it is kept in the synced transcript. A computer agent's
|
|
24
|
+
* reasoning and tool calls are working state: they stream to the client while
|
|
25
|
+
* the turn runs and stay in the local run snapshot, but only the answer is
|
|
26
|
+
* stored in `c_chat_message`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toTranscriptMessage(message: ChatUIMessage): ChatUIMessage;
|
|
29
|
+
export declare function projectPendingApprovals(message: ChatUIMessage | null | undefined, ids: {
|
|
30
|
+
runId: string;
|
|
31
|
+
threadId: string;
|
|
32
|
+
}): RemoteToolApprovalPrompt[];
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folds normalized agent events into the AI SDK `UIMessage` the app renders.
|
|
3
|
+
* Agent tools become read-only `dynamic-tool` parts (they ran on the agent's
|
|
4
|
+
* machine); approvals use the same part/metadata shape as desktop so the app's
|
|
5
|
+
* approval bar and `run.status` recovery work unchanged.
|
|
6
|
+
*/
|
|
7
|
+
const APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
8
|
+
const MAX_PENDING_APPROVALS = 32;
|
|
9
|
+
const PREVIEW_MAX_CHARS = 1500;
|
|
10
|
+
const SENSITIVE_KEY = /password|token|secret|api[-_ ]?key|authorization|cookie|credential|private[-_ ]?key/i;
|
|
11
|
+
export class AssistantProjection {
|
|
12
|
+
base;
|
|
13
|
+
parts = [];
|
|
14
|
+
toolIndex = new Map();
|
|
15
|
+
approvals = {};
|
|
16
|
+
revisionCounter = 0;
|
|
17
|
+
constructor(base) {
|
|
18
|
+
this.base = base;
|
|
19
|
+
}
|
|
20
|
+
get revision() {
|
|
21
|
+
return this.revisionCounter;
|
|
22
|
+
}
|
|
23
|
+
apply(event) {
|
|
24
|
+
switch (event.type) {
|
|
25
|
+
case 'text-delta':
|
|
26
|
+
this.appendText('text', event.text);
|
|
27
|
+
break;
|
|
28
|
+
case 'reasoning-delta':
|
|
29
|
+
this.appendText('reasoning', event.text);
|
|
30
|
+
break;
|
|
31
|
+
case 'tool':
|
|
32
|
+
this.applyTool(event);
|
|
33
|
+
break;
|
|
34
|
+
case 'approval-request': {
|
|
35
|
+
const requestedAt = new Date().toISOString();
|
|
36
|
+
this.approvals[event.approvalId] = {
|
|
37
|
+
requestedAt,
|
|
38
|
+
expiresAt: new Date(Date.parse(requestedAt) + APPROVAL_TTL_MS).toISOString(),
|
|
39
|
+
toolName: event.toolName,
|
|
40
|
+
toolCallId: event.toolCallId,
|
|
41
|
+
};
|
|
42
|
+
const part = this.ensureTool(event.toolCallId, event.toolName);
|
|
43
|
+
part.state = 'approval-requested';
|
|
44
|
+
part.approval = { id: event.approvalId };
|
|
45
|
+
if (event.input !== undefined)
|
|
46
|
+
part.input = event.input;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case 'approval-resolved': {
|
|
50
|
+
const part = this.findToolByApproval(event.approvalId);
|
|
51
|
+
if (part) {
|
|
52
|
+
part.approval = { id: event.approvalId, approved: event.approved };
|
|
53
|
+
part.state = event.approved ? 'approval-responded' : 'output-denied';
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case 'usage':
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.revisionCounter += 1;
|
|
61
|
+
}
|
|
62
|
+
/** Closes streaming parts; called once when the turn ends. */
|
|
63
|
+
finish() {
|
|
64
|
+
for (const part of this.parts) {
|
|
65
|
+
if ((part.type === 'text' || part.type === 'reasoning') && part.state === 'streaming')
|
|
66
|
+
part.state = 'done';
|
|
67
|
+
}
|
|
68
|
+
this.revisionCounter += 1;
|
|
69
|
+
}
|
|
70
|
+
snapshot() {
|
|
71
|
+
return {
|
|
72
|
+
...this.base,
|
|
73
|
+
role: 'assistant',
|
|
74
|
+
parts: this.parts.map((part) => ({ ...part })),
|
|
75
|
+
metadata: {
|
|
76
|
+
...(this.base.metadata ?? {}),
|
|
77
|
+
...(Object.keys(this.approvals).length ? { toolApprovals: { ...this.approvals } } : {}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
appendText(type, text) {
|
|
82
|
+
if (!text)
|
|
83
|
+
return;
|
|
84
|
+
const last = this.parts.at(-1);
|
|
85
|
+
if (last?.type === type && last.state === 'streaming') {
|
|
86
|
+
last.text = `${String(last.text ?? '')}${text}`;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.parts.push({ type, text, state: 'streaming' });
|
|
90
|
+
}
|
|
91
|
+
ensureTool(toolCallId, toolName) {
|
|
92
|
+
const index = this.toolIndex.get(toolCallId);
|
|
93
|
+
if (index !== undefined)
|
|
94
|
+
return this.parts[index];
|
|
95
|
+
// A tool call closes the text segment before it.
|
|
96
|
+
const last = this.parts.at(-1);
|
|
97
|
+
if (last && (last.type === 'text' || last.type === 'reasoning'))
|
|
98
|
+
last.state = 'done';
|
|
99
|
+
const part = { type: 'dynamic-tool', toolCallId, toolName, state: 'input-streaming', input: undefined };
|
|
100
|
+
this.toolIndex.set(toolCallId, this.parts.push(part) - 1);
|
|
101
|
+
return part;
|
|
102
|
+
}
|
|
103
|
+
findToolByApproval(approvalId) {
|
|
104
|
+
return this.parts.find((part) => part.type === 'dynamic-tool' && part.approval?.id === approvalId);
|
|
105
|
+
}
|
|
106
|
+
applyTool(event) {
|
|
107
|
+
const part = this.ensureTool(event.toolCallId, event.title || 'tool');
|
|
108
|
+
if (event.title)
|
|
109
|
+
part.toolName = event.title;
|
|
110
|
+
if (event.input !== undefined)
|
|
111
|
+
part.input = event.input;
|
|
112
|
+
switch (event.status) {
|
|
113
|
+
case 'completed':
|
|
114
|
+
part.state = 'output-available';
|
|
115
|
+
part.output = event.output ?? part.output ?? null;
|
|
116
|
+
break;
|
|
117
|
+
case 'failed':
|
|
118
|
+
part.state = 'output-error';
|
|
119
|
+
part.errorText = typeof event.output === 'string' ? event.output : 'Tool failed';
|
|
120
|
+
break;
|
|
121
|
+
case 'pending':
|
|
122
|
+
case 'running':
|
|
123
|
+
if (part.state !== 'approval-requested')
|
|
124
|
+
part.state = 'input-available';
|
|
125
|
+
if (event.output !== undefined)
|
|
126
|
+
part.output = event.output;
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
if (event.output !== undefined)
|
|
130
|
+
part.output = event.output;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function redact(value, depth) {
|
|
135
|
+
if (depth > 4)
|
|
136
|
+
return '[…]';
|
|
137
|
+
if (Array.isArray(value))
|
|
138
|
+
return value.slice(0, 16).map((item) => redact(item, depth + 1));
|
|
139
|
+
if (value && typeof value === 'object') {
|
|
140
|
+
return Object.fromEntries(Object.entries(value)
|
|
141
|
+
.slice(0, 24)
|
|
142
|
+
.map(([key, item]) => [key, SENSITIVE_KEY.test(key) ? '[redacted]' : redact(item, depth + 1)]));
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
/** Same projection the desktop host sends: open approval requests found in the snapshot. */
|
|
147
|
+
/**
|
|
148
|
+
* The reply as it is kept in the synced transcript. A computer agent's
|
|
149
|
+
* reasoning and tool calls are working state: they stream to the client while
|
|
150
|
+
* the turn runs and stay in the local run snapshot, but only the answer is
|
|
151
|
+
* stored in `c_chat_message`.
|
|
152
|
+
*/
|
|
153
|
+
export function toTranscriptMessage(message) {
|
|
154
|
+
const { toolApprovals: _approvals, ...metadata } = (message.metadata ?? {});
|
|
155
|
+
return {
|
|
156
|
+
...message,
|
|
157
|
+
parts: message.parts.filter((part) => part.type === 'text' || part.type === 'file'),
|
|
158
|
+
metadata,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export function projectPendingApprovals(message, ids) {
|
|
162
|
+
if (!message)
|
|
163
|
+
return [];
|
|
164
|
+
const approvals = (message.metadata?.toolApprovals ?? {});
|
|
165
|
+
const prompts = [];
|
|
166
|
+
for (const part of message.parts) {
|
|
167
|
+
if (prompts.length >= MAX_PENDING_APPROVALS)
|
|
168
|
+
break;
|
|
169
|
+
const approvalId = part.approval?.id;
|
|
170
|
+
if (part.state !== 'approval-requested' || typeof approvalId !== 'string')
|
|
171
|
+
continue;
|
|
172
|
+
const meta = approvals[approvalId] ?? {};
|
|
173
|
+
const requestedAt = meta.requestedAt ?? new Date().toISOString();
|
|
174
|
+
let inputPreview = '';
|
|
175
|
+
try {
|
|
176
|
+
inputPreview = JSON.stringify(redact(part.input, 0)) ?? '';
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
inputPreview = '';
|
|
180
|
+
}
|
|
181
|
+
prompts.push({
|
|
182
|
+
approvalId,
|
|
183
|
+
runId: ids.runId,
|
|
184
|
+
threadId: ids.threadId,
|
|
185
|
+
toolCallId: String(part.toolCallId ?? meta.toolCallId ?? ''),
|
|
186
|
+
toolName: String(part.toolName ?? meta.toolName ?? 'tool'),
|
|
187
|
+
inputPreview: inputPreview.slice(0, PREVIEW_MAX_CHARS),
|
|
188
|
+
requestedAt,
|
|
189
|
+
expiresAt: meta.expiresAt ?? new Date(Date.parse(requestedAt) + APPROVAL_TTL_MS).toISOString(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return prompts;
|
|
193
|
+
}
|