pikiloom 0.4.40 → 0.4.42
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/dashboard/dist/assets/AgentTab-B9z4W2mh.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-g_6IHAWE.js → ConnectionModal-2bL7sFKk.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-DVe6q12N.js → DirBrowser-CA4k8E_A.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-GqxD61Pp.js → ExtensionsTab-CRjTmFRl.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-DIqTzZJ9.js → IMAccessTab-DDb7rsHe.js} +1 -1
- package/dashboard/dist/assets/{Modal-CIYrMwm0.js → Modal-DDiBG4kb.js} +1 -1
- package/dashboard/dist/assets/{Modals-B-Afu2rD.js → Modals-CsUQjpxn.js} +1 -1
- package/dashboard/dist/assets/{Select-B1UL27gw.js → Select-CG_7h_Tz.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-B5964GUj.js +1 -0
- package/dashboard/dist/assets/{SystemTab-Bv1uT5-h.js → SystemTab-D7r1PsC9.js} +1 -1
- package/dashboard/dist/assets/index-BbplxgnQ.js +3 -0
- package/dashboard/dist/assets/index-CULTTvtY.js +23 -0
- package/dashboard/dist/assets/index-DwmXPtDd.css +1 -0
- package/dashboard/dist/assets/{shared-624PaUfm.js → shared-Bv8WvQSo.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/accounts.js +181 -0
- package/dist/agent/drivers/claude.js +108 -3
- package/dist/agent/kernel-bridge.js +45 -1
- package/dist/agent/session.js +6 -2
- package/dist/agent/stream.js +26 -0
- package/dist/bot/bot.js +45 -10
- package/dist/bot/command-ui.js +69 -4
- package/dist/bot/render-shared.js +2 -1
- package/dist/cli/kernel-app.js +4 -2
- package/dist/core/config/runtime-config.js +14 -22
- package/dist/dashboard/routes/accounts.js +113 -0
- package/dist/dashboard/server.js +2 -0
- package/dist/model/responses-bridge.js +37 -0
- package/package.json +1 -1
- package/packages/kernel/README.md +35 -0
- package/packages/kernel/dist/accounts.d.ts +6 -0
- package/packages/kernel/dist/accounts.js +29 -0
- package/packages/kernel/dist/contracts/driver.d.ts +15 -0
- package/packages/kernel/dist/contracts/ports.d.ts +2 -0
- package/packages/kernel/dist/drivers/acp.d.ts +24 -0
- package/packages/kernel/dist/drivers/acp.js +477 -0
- package/packages/kernel/dist/drivers/claude.d.ts +5 -1
- package/packages/kernel/dist/drivers/claude.js +4 -0
- package/packages/kernel/dist/drivers/codex.d.ts +5 -1
- package/packages/kernel/dist/drivers/codex.js +24 -12
- package/packages/kernel/dist/drivers/gemini.d.ts +5 -1
- package/packages/kernel/dist/drivers/gemini.js +4 -0
- package/packages/kernel/dist/drivers/hermes.d.ts +3 -12
- package/packages/kernel/dist/drivers/hermes.js +8 -191
- package/packages/kernel/dist/drivers/index.d.ts +1 -0
- package/packages/kernel/dist/drivers/index.js +1 -0
- package/packages/kernel/dist/index.d.ts +4 -1
- package/packages/kernel/dist/index.js +5 -0
- package/packages/kernel/dist/ports/defaults.js +7 -1
- package/packages/kernel/dist/runtime/loom.d.ts +10 -0
- package/packages/kernel/dist/runtime/loom.js +18 -2
- package/packages/kernel/dist/workspace/index.d.ts +5 -0
- package/packages/kernel/dist/workspace/index.js +7 -0
- package/packages/kernel/dist/workspace/mcp.d.ts +44 -0
- package/packages/kernel/dist/workspace/mcp.js +82 -0
- package/packages/kernel/dist/workspace/native.d.ts +14 -0
- package/packages/kernel/dist/workspace/native.js +340 -0
- package/packages/kernel/dist/workspace/paths.d.ts +33 -0
- package/packages/kernel/dist/workspace/paths.js +40 -0
- package/packages/kernel/dist/workspace/sessions.d.ts +49 -0
- package/packages/kernel/dist/workspace/sessions.js +130 -0
- package/packages/kernel/dist/workspace/skills.d.ts +44 -0
- package/packages/kernel/dist/workspace/skills.js +173 -0
- package/dashboard/dist/assets/AgentTab-Cj7sekcS.js +0 -1
- package/dashboard/dist/assets/SessionPanel-BHuG5hxj.js +0 -1
- package/dashboard/dist/assets/index-BOEW0RIx.js +0 -23
- package/dashboard/dist/assets/index-Bthwt6K_.css +0 -1
- package/dashboard/dist/assets/index-qKexKdJl.js +0 -3
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, extname } from 'node:path';
|
|
5
|
+
class AcpRpcError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// ── ACP JSON-RPC client over a child process' stdio (ndjson framing) ───────────
|
|
13
|
+
class AcpClient {
|
|
14
|
+
bin;
|
|
15
|
+
args;
|
|
16
|
+
env;
|
|
17
|
+
cwd;
|
|
18
|
+
proc = null;
|
|
19
|
+
nextId = 1;
|
|
20
|
+
pending = new Map();
|
|
21
|
+
notifyCb;
|
|
22
|
+
requestCb;
|
|
23
|
+
stderrTail = [];
|
|
24
|
+
constructor(bin, args, env, cwd) {
|
|
25
|
+
this.bin = bin;
|
|
26
|
+
this.args = args;
|
|
27
|
+
this.env = env;
|
|
28
|
+
this.cwd = cwd;
|
|
29
|
+
}
|
|
30
|
+
onNotification(cb) { this.notifyCb = cb; }
|
|
31
|
+
onRequest(cb) { this.requestCb = cb; }
|
|
32
|
+
stderrText() { return this.stderrTail.join('\n'); }
|
|
33
|
+
start() {
|
|
34
|
+
try {
|
|
35
|
+
this.proc = spawn(this.bin, this.args, {
|
|
36
|
+
cwd: this.cwd,
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
env: this.env ? { ...process.env, ...this.env } : process.env,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const rl = createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
|
|
45
|
+
rl.on('line', (line) => this.onLine(line));
|
|
46
|
+
this.proc.stderr.on('data', (chunk) => {
|
|
47
|
+
for (const ln of chunk.toString('utf8').split('\n')) {
|
|
48
|
+
const t = ln.trim();
|
|
49
|
+
if (!t)
|
|
50
|
+
continue;
|
|
51
|
+
this.stderrTail.push(t.slice(0, 240));
|
|
52
|
+
if (this.stderrTail.length > 20)
|
|
53
|
+
this.stderrTail.shift();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
this.proc.on('close', () => { for (const cb of this.pending.values())
|
|
57
|
+
cb({ error: { message: 'acp process exited' } }); this.pending.clear(); });
|
|
58
|
+
this.proc.on('error', () => { });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
onLine(line) {
|
|
62
|
+
const t = line.trim();
|
|
63
|
+
if (!t)
|
|
64
|
+
return;
|
|
65
|
+
let m;
|
|
66
|
+
try {
|
|
67
|
+
m = JSON.parse(t);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return;
|
|
71
|
+
} // non-JSON stdout noise
|
|
72
|
+
if (m.method && m.id != null) {
|
|
73
|
+
void this.handleRequest(m);
|
|
74
|
+
return;
|
|
75
|
+
} // agent -> client request
|
|
76
|
+
if (m.id != null) { // response to one of our requests
|
|
77
|
+
const cb = this.pending.get(m.id);
|
|
78
|
+
if (cb) {
|
|
79
|
+
this.pending.delete(m.id);
|
|
80
|
+
cb(m);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (m.method)
|
|
85
|
+
this.notifyCb?.(m.method, m.params ?? {}); // notification (session/update)
|
|
86
|
+
}
|
|
87
|
+
async handleRequest(m) {
|
|
88
|
+
const id = m.id;
|
|
89
|
+
if (!this.requestCb) {
|
|
90
|
+
this.respondError(id, -32601, `Method not implemented: ${m.method}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const result = await this.requestCb(m.method, m.params ?? {});
|
|
95
|
+
this.respond(id, result ?? null);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
const code = e instanceof AcpRpcError ? e.code : -32603;
|
|
99
|
+
this.respondError(id, code, e?.message || 'handler error');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
request(method, params, timeoutMs = 60_000) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
if (!this.proc || this.proc.killed) {
|
|
105
|
+
resolve({ error: { message: 'not connected' } });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const id = this.nextId++;
|
|
109
|
+
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `ACP '${method}' timed out` } }); }, timeoutMs);
|
|
110
|
+
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
111
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
notify(method, params) { this.write({ jsonrpc: '2.0', method, params }); }
|
|
115
|
+
respond(id, result) { this.write({ jsonrpc: '2.0', id, result }); }
|
|
116
|
+
respondError(id, code, message) { this.write({ jsonrpc: '2.0', id, error: { code, message } }); }
|
|
117
|
+
write(msg) {
|
|
118
|
+
if (!this.proc || this.proc.killed)
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
122
|
+
}
|
|
123
|
+
catch { /* stream closed */ }
|
|
124
|
+
}
|
|
125
|
+
kill() { try {
|
|
126
|
+
this.proc?.kill('SIGTERM');
|
|
127
|
+
}
|
|
128
|
+
catch { /* ignore */ } this.proc = null; }
|
|
129
|
+
}
|
|
130
|
+
// ── pikiloom McpServerSpec[] -> ACP mcpServers[] ───────────────────────────────
|
|
131
|
+
export function toAcpMcpServers(servers) {
|
|
132
|
+
if (!servers || !servers.length)
|
|
133
|
+
return [];
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const s of servers) {
|
|
136
|
+
if (!s || !s.name)
|
|
137
|
+
continue;
|
|
138
|
+
if (s.type === 'http' && s.url) {
|
|
139
|
+
out.push({ type: 'http', name: s.name, url: s.url, headers: Object.entries(s.headers || {}).map(([name, value]) => ({ name, value: String(value) })) });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (s.command) {
|
|
143
|
+
out.push({ name: s.name, command: s.command, args: Array.isArray(s.args) ? s.args.map(String) : [], env: Object.entries(s.env || {}).map(([name, value]) => ({ name, value: String(value) })) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
149
|
+
function mimeForExt(ext) {
|
|
150
|
+
if (ext === '.jpg' || ext === '.jpeg')
|
|
151
|
+
return 'image/jpeg';
|
|
152
|
+
if (ext === '.gif')
|
|
153
|
+
return 'image/gif';
|
|
154
|
+
if (ext === '.webp')
|
|
155
|
+
return 'image/webp';
|
|
156
|
+
return 'image/png';
|
|
157
|
+
}
|
|
158
|
+
// ACP prompt content blocks: text + inline base64 images; other attachments noted as text.
|
|
159
|
+
export function buildAcpPromptBlocks(prompt, attachments) {
|
|
160
|
+
const blocks = [];
|
|
161
|
+
for (const f of attachments) {
|
|
162
|
+
const ext = extname(f).toLowerCase();
|
|
163
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
164
|
+
try {
|
|
165
|
+
blocks.push({ type: 'image', mimeType: mimeForExt(ext), data: readFileSync(f).toString('base64') });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
catch { /* fall through to a text note */ }
|
|
169
|
+
}
|
|
170
|
+
blocks.push({ type: 'text', text: `[Attached file: ${f}]` });
|
|
171
|
+
}
|
|
172
|
+
blocks.push({ type: 'text', text: prompt });
|
|
173
|
+
return blocks;
|
|
174
|
+
}
|
|
175
|
+
function acpContentText(content) {
|
|
176
|
+
if (content == null)
|
|
177
|
+
return '';
|
|
178
|
+
if (typeof content === 'string')
|
|
179
|
+
return content;
|
|
180
|
+
if (Array.isArray(content))
|
|
181
|
+
return content.map(acpContentText).filter(Boolean).join('\n');
|
|
182
|
+
if (typeof content === 'object') {
|
|
183
|
+
if (typeof content.text === 'string')
|
|
184
|
+
return content.text;
|
|
185
|
+
if (content.type === 'content' && content.content)
|
|
186
|
+
return acpContentText(content.content);
|
|
187
|
+
if (content.type === 'diff' && typeof content.path === 'string')
|
|
188
|
+
return `[diff ${content.path}]`;
|
|
189
|
+
}
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
function acpToolStatus(status, dflt) {
|
|
193
|
+
if (status === 'completed')
|
|
194
|
+
return 'done';
|
|
195
|
+
if (status === 'failed')
|
|
196
|
+
return 'failed';
|
|
197
|
+
if (status === 'pending' || status === 'in_progress')
|
|
198
|
+
return 'running';
|
|
199
|
+
return dflt;
|
|
200
|
+
}
|
|
201
|
+
function safeJson(v) {
|
|
202
|
+
try {
|
|
203
|
+
return typeof v === 'string' ? v : JSON.stringify(v);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function truncate(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; }
|
|
210
|
+
// session/update -> normalized DriverEvents. Exported (and aliased as applyHermesUpdate)
|
|
211
|
+
// so it can be unit-tested against the raw ACP wire without spawning a process.
|
|
212
|
+
export function applyAcpUpdate(update, s, tools, emit) {
|
|
213
|
+
if (!update)
|
|
214
|
+
return;
|
|
215
|
+
switch (update.sessionUpdate) {
|
|
216
|
+
case 'agent_message_chunk': {
|
|
217
|
+
const t = acpContentText(update.content);
|
|
218
|
+
if (t) {
|
|
219
|
+
s.text = (s.text || '') + t;
|
|
220
|
+
emit({ type: 'text', delta: t });
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case 'agent_thought_chunk': {
|
|
225
|
+
const t = acpContentText(update.content);
|
|
226
|
+
if (t) {
|
|
227
|
+
s.reasoning = (s.reasoning || '') + t;
|
|
228
|
+
emit({ type: 'reasoning', delta: t });
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case 'tool_call': {
|
|
233
|
+
const id = typeof update.toolCallId === 'string' ? update.toolCallId : '';
|
|
234
|
+
if (!id)
|
|
235
|
+
return;
|
|
236
|
+
const name = (typeof update.title === 'string' && update.title.trim()) || (typeof update.kind === 'string' && update.kind) || 'tool';
|
|
237
|
+
tools.add(id);
|
|
238
|
+
emit({ type: 'tool', call: { id, name, summary: name, input: update.rawInput != null ? truncate(safeJson(update.rawInput) || '', 2000) : null, status: acpToolStatus(update.status, 'running') } });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
case 'tool_call_update': {
|
|
242
|
+
const id = typeof update.toolCallId === 'string' ? update.toolCallId : '';
|
|
243
|
+
if (!id)
|
|
244
|
+
return;
|
|
245
|
+
const name = (typeof update.title === 'string' && update.title.trim()) || 'tool';
|
|
246
|
+
const status = acpToolStatus(update.status, 'running');
|
|
247
|
+
const result = update.content != null ? truncate(acpContentText(update.content), 2000) : null;
|
|
248
|
+
emit({ type: 'tool', call: { id, name, summary: name, result: result || null, status } });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
case 'plan': {
|
|
252
|
+
const entries = Array.isArray(update.entries) ? update.entries : [];
|
|
253
|
+
const steps = entries
|
|
254
|
+
.map((e) => ({ text: String(e?.content ?? '').trim(), status: e?.status === 'in_progress' ? 'inProgress' : e?.status === 'completed' ? 'completed' : 'pending' }))
|
|
255
|
+
.filter((st) => st.text);
|
|
256
|
+
if (steps.length)
|
|
257
|
+
emit({ type: 'plan', plan: { explanation: null, steps } });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
case 'usage_update': {
|
|
261
|
+
if (typeof update.size === 'number')
|
|
262
|
+
s.contextWindow = update.size;
|
|
263
|
+
if (typeof update.used === 'number')
|
|
264
|
+
s.contextUsed = update.used;
|
|
265
|
+
if (typeof update.used === 'number') {
|
|
266
|
+
const window = s.contextWindow;
|
|
267
|
+
const contextPercent = window && update.used > 0 ? Math.min(99.9, Math.round((update.used / window) * 1000) / 10) : null;
|
|
268
|
+
emit({ type: 'usage', usage: { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: update.used, contextPercent } });
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// session/request_permission params -> a normalized UniversalInteraction (kind 'permission').
|
|
275
|
+
// Each choice's `value` IS the ACP optionId, so the answer maps straight back to an outcome.
|
|
276
|
+
function permissionToInteraction(params, promptId) {
|
|
277
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
278
|
+
if (!options.length)
|
|
279
|
+
return null;
|
|
280
|
+
const tc = params?.toolCall || {};
|
|
281
|
+
const what = (typeof tc.title === 'string' && tc.title.trim()) || (typeof tc.kind === 'string' && tc.kind) || 'this action';
|
|
282
|
+
return {
|
|
283
|
+
promptId,
|
|
284
|
+
kind: 'permission',
|
|
285
|
+
title: 'Permission required',
|
|
286
|
+
hint: `The agent wants to: ${what}`,
|
|
287
|
+
questions: [{
|
|
288
|
+
id: 'choice',
|
|
289
|
+
header: 'Permission',
|
|
290
|
+
text: `Allow the agent to ${what}?`,
|
|
291
|
+
type: 'select',
|
|
292
|
+
choices: options.map((o) => ({ label: String(o?.name || o?.optionId || 'option'), value: String(o?.optionId || ''), description: String(o?.kind || '') })),
|
|
293
|
+
allowFreeform: false,
|
|
294
|
+
allowEmpty: true,
|
|
295
|
+
}],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function pickOptionId(answers, options) {
|
|
299
|
+
const vals = Object.values(answers || {}).flat().map(String);
|
|
300
|
+
for (const v of vals) {
|
|
301
|
+
const byId = options.find((o) => String(o?.optionId) === v);
|
|
302
|
+
if (byId)
|
|
303
|
+
return String(byId.optionId);
|
|
304
|
+
const byName = options.find((o) => String(o?.name) === v);
|
|
305
|
+
if (byName)
|
|
306
|
+
return String(byName.optionId);
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function fallbackOptionId(options, fallback) {
|
|
311
|
+
if (fallback === 'cancel')
|
|
312
|
+
return null;
|
|
313
|
+
const order = fallback === 'allow' ? ['allow_once', 'allow_always', 'allow'] : ['reject_once', 'reject_always', 'reject'];
|
|
314
|
+
for (const k of order) {
|
|
315
|
+
const o = options.find((x) => String(x?.kind) === k);
|
|
316
|
+
if (o)
|
|
317
|
+
return String(o.optionId);
|
|
318
|
+
}
|
|
319
|
+
return fallback === 'allow' && options[0] ? String(options[0].optionId) : null;
|
|
320
|
+
}
|
|
321
|
+
function readAcpTextFile(params) {
|
|
322
|
+
const p = String(params?.path || '');
|
|
323
|
+
if (!p || !existsSync(p))
|
|
324
|
+
throw new AcpRpcError(-32602, `file not found: ${p}`);
|
|
325
|
+
let content = readFileSync(p, 'utf8');
|
|
326
|
+
const line = typeof params?.line === 'number' ? params.line : null;
|
|
327
|
+
const limit = typeof params?.limit === 'number' ? params.limit : null;
|
|
328
|
+
if (line != null || limit != null) {
|
|
329
|
+
const lines = content.split('\n');
|
|
330
|
+
const start = line != null ? Math.max(0, line - 1) : 0;
|
|
331
|
+
content = lines.slice(start, limit != null ? start + limit : undefined).join('\n');
|
|
332
|
+
}
|
|
333
|
+
return { content };
|
|
334
|
+
}
|
|
335
|
+
function writeAcpTextFile(params) {
|
|
336
|
+
const p = String(params?.path || '');
|
|
337
|
+
if (!p)
|
|
338
|
+
throw new AcpRpcError(-32602, 'path required');
|
|
339
|
+
try {
|
|
340
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
catch { /* parent may exist */ }
|
|
343
|
+
writeFileSync(p, String(params?.content ?? ''), 'utf8');
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
function acpUsage(s, raw) {
|
|
347
|
+
let input = null, output = null, cached = null;
|
|
348
|
+
if (raw && typeof raw === 'object') {
|
|
349
|
+
const n = (v) => (typeof v === 'number' ? v : null);
|
|
350
|
+
input = n(raw.inputTokens ?? raw.input_tokens);
|
|
351
|
+
output = n(raw.outputTokens ?? raw.output_tokens);
|
|
352
|
+
cached = n(raw.cachedReadTokens ?? raw.cached_read_tokens ?? raw.cachedInputTokens);
|
|
353
|
+
}
|
|
354
|
+
const window = s.contextWindow, used = s.contextUsed;
|
|
355
|
+
const contextPercent = window && used ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
356
|
+
return { inputTokens: input, outputTokens: output, cachedInputTokens: cached, contextUsedTokens: used, contextPercent, turnOutputTokens: output };
|
|
357
|
+
}
|
|
358
|
+
export class AcpDriver {
|
|
359
|
+
id;
|
|
360
|
+
capabilities;
|
|
361
|
+
cfg;
|
|
362
|
+
constructor(config) {
|
|
363
|
+
this.id = config.id;
|
|
364
|
+
this.cfg = {
|
|
365
|
+
id: config.id,
|
|
366
|
+
command: config.command,
|
|
367
|
+
args: config.args ?? ['acp'],
|
|
368
|
+
env: config.env,
|
|
369
|
+
fsAccess: config.fsAccess ?? true,
|
|
370
|
+
permissionFallback: config.permissionFallback ?? 'allow',
|
|
371
|
+
protocolVersion: config.protocolVersion ?? 1,
|
|
372
|
+
promptTimeoutMs: config.promptTimeoutMs ?? 7_200_000,
|
|
373
|
+
capabilities: config.capabilities,
|
|
374
|
+
};
|
|
375
|
+
this.capabilities = config.capabilities ?? { steer: false, interact: true, resume: true, tui: false };
|
|
376
|
+
}
|
|
377
|
+
async run(input, ctx) {
|
|
378
|
+
const env = { ...(this.cfg.env || {}), ...(input.env || {}) };
|
|
379
|
+
const client = new AcpClient(this.cfg.command, this.cfg.args, env, input.workdir);
|
|
380
|
+
const state = { text: '', reasoning: '', contextWindow: null, contextUsed: null };
|
|
381
|
+
const tools = new Set();
|
|
382
|
+
let sessionId = input.sessionId ?? null;
|
|
383
|
+
let permSeq = 0;
|
|
384
|
+
if (!client.start())
|
|
385
|
+
return { ok: false, text: '', error: `failed to start ${this.cfg.command}`, stopReason: 'error' };
|
|
386
|
+
const onAbort = () => { if (sessionId)
|
|
387
|
+
client.notify('session/cancel', { sessionId }); client.kill(); };
|
|
388
|
+
if (ctx.signal.aborted)
|
|
389
|
+
onAbort();
|
|
390
|
+
else
|
|
391
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
392
|
+
client.onNotification((method, params) => {
|
|
393
|
+
if (method === 'session/update')
|
|
394
|
+
applyAcpUpdate(params?.update ?? params, state, tools, ctx.emit);
|
|
395
|
+
});
|
|
396
|
+
client.onRequest(async (method, params) => {
|
|
397
|
+
switch (method) {
|
|
398
|
+
case 'session/request_permission': return this.resolvePermission(params, ctx, `${this.id}-perm-${++permSeq}`);
|
|
399
|
+
case 'fs/read_text_file':
|
|
400
|
+
if (!this.cfg.fsAccess)
|
|
401
|
+
throw new AcpRpcError(-32601, 'fs/read_text_file not supported');
|
|
402
|
+
return readAcpTextFile(params);
|
|
403
|
+
case 'fs/write_text_file':
|
|
404
|
+
if (!this.cfg.fsAccess)
|
|
405
|
+
throw new AcpRpcError(-32601, 'fs/write_text_file not supported');
|
|
406
|
+
return writeAcpTextFile(params);
|
|
407
|
+
default:
|
|
408
|
+
throw new AcpRpcError(-32601, `Method not implemented: ${method}`);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
const init = await client.request('initialize', {
|
|
413
|
+
protocolVersion: this.cfg.protocolVersion,
|
|
414
|
+
clientCapabilities: { fs: { readTextFile: !!this.cfg.fsAccess, writeTextFile: !!this.cfg.fsAccess }, terminal: false },
|
|
415
|
+
});
|
|
416
|
+
if (init.error)
|
|
417
|
+
return { ok: false, text: '', error: this.startupError(client, init.error.message || 'initialize failed'), stopReason: 'error' };
|
|
418
|
+
const mcpServers = toAcpMcpServers(input.extraMcpServers);
|
|
419
|
+
if (!sessionId) {
|
|
420
|
+
const ns = await client.request('session/new', { cwd: input.workdir, mcpServers });
|
|
421
|
+
if (ns.error)
|
|
422
|
+
return { ok: false, text: '', error: ns.error.message || 'session/new failed', stopReason: 'error' };
|
|
423
|
+
sessionId = ns.result?.sessionId ?? ns.result?.session_id ?? null;
|
|
424
|
+
if (sessionId)
|
|
425
|
+
ctx.emit({ type: 'session', sessionId });
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
await client.request('session/load', { sessionId, cwd: input.workdir, mcpServers }, 30_000).catch(() => ({}));
|
|
429
|
+
ctx.emit({ type: 'session', sessionId });
|
|
430
|
+
}
|
|
431
|
+
if (!sessionId)
|
|
432
|
+
return { ok: false, text: '', error: `${this.id} returned no session id`, stopReason: 'error' };
|
|
433
|
+
if (input.model)
|
|
434
|
+
await client.request('session/set_model', { sessionId, modelId: input.model }, 15_000).catch(() => ({}));
|
|
435
|
+
if (input.effort)
|
|
436
|
+
await client.request('session/set_mode', { sessionId, modeId: input.effort }, 15_000).catch(() => ({}));
|
|
437
|
+
const promptResp = await client.request('session/prompt', {
|
|
438
|
+
sessionId,
|
|
439
|
+
prompt: buildAcpPromptBlocks(input.prompt, input.attachments || []),
|
|
440
|
+
}, this.cfg.promptTimeoutMs);
|
|
441
|
+
const usage = acpUsage(state, promptResp.result?.usage);
|
|
442
|
+
if (ctx.signal.aborted)
|
|
443
|
+
return { ok: false, text: state.text, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId, usage };
|
|
444
|
+
if (promptResp.error)
|
|
445
|
+
return { ok: false, text: state.text, error: promptResp.error.message || 'session/prompt failed', stopReason: 'error', sessionId, usage };
|
|
446
|
+
const stopReason = promptResp.result?.stopReason ?? 'end_turn';
|
|
447
|
+
return { ok: true, text: state.text, reasoning: state.reasoning || undefined, error: null, stopReason, sessionId, usage };
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
451
|
+
client.kill();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async resolvePermission(params, ctx, promptId) {
|
|
455
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
456
|
+
if (!options.length)
|
|
457
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
458
|
+
const interaction = permissionToInteraction(params, promptId);
|
|
459
|
+
let answers = {};
|
|
460
|
+
if (interaction) {
|
|
461
|
+
try {
|
|
462
|
+
answers = (await ctx.askUser(interaction)) || {};
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
answers = {};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (ctx.signal.aborted)
|
|
469
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
470
|
+
const picked = pickOptionId(answers, options) ?? fallbackOptionId(options, this.cfg.permissionFallback);
|
|
471
|
+
return picked ? { outcome: { outcome: 'selected', optionId: picked } } : { outcome: { outcome: 'cancelled' } };
|
|
472
|
+
}
|
|
473
|
+
startupError(client, base) {
|
|
474
|
+
const tail = client.stderrText().trim();
|
|
475
|
+
return tail ? `${base} — ${truncate(tail, 300)}` : base;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec } from '../contracts/driver.js';
|
|
1
|
+
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec, NativeSessionInfo } from '../contracts/driver.js';
|
|
2
2
|
import type { UniversalUsage, UniversalPlan } from '../protocol/index.js';
|
|
3
3
|
export declare class ClaudeDriver implements AgentDriver {
|
|
4
4
|
private readonly bin;
|
|
@@ -11,6 +11,10 @@ export declare class ClaudeDriver implements AgentDriver {
|
|
|
11
11
|
};
|
|
12
12
|
constructor(bin?: string);
|
|
13
13
|
tui(input: TuiInput): TuiSpec;
|
|
14
|
+
listNativeSessions(opts: {
|
|
15
|
+
workdir: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
}): NativeSessionInfo[];
|
|
14
18
|
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
15
19
|
private usage;
|
|
16
20
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { discoverClaudeNativeSessions } from '../workspace/native.js';
|
|
2
3
|
// Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
|
|
3
4
|
// events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
|
|
4
5
|
// (system / stream_event{message_start,content_block_delta,message_delta} / assistant / result),
|
|
@@ -22,6 +23,9 @@ export class ClaudeDriver {
|
|
|
22
23
|
args.push(...input.extraArgs);
|
|
23
24
|
return { command: this.bin, args, cwd: input.workdir, env: input.env };
|
|
24
25
|
}
|
|
26
|
+
listNativeSessions(opts) {
|
|
27
|
+
return discoverClaudeNativeSessions(opts.workdir, { limit: opts.limit });
|
|
28
|
+
}
|
|
25
29
|
run(input, ctx) {
|
|
26
30
|
const steerable = !!input.steerable;
|
|
27
31
|
const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, TuiInput, TuiSpec } from '../contracts/driver.js';
|
|
1
|
+
import type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, TuiInput, TuiSpec, NativeSessionInfo } from '../contracts/driver.js';
|
|
2
2
|
import type { UniversalUsage } from '../protocol/index.js';
|
|
3
3
|
export declare function codexToolSummary(item: any): {
|
|
4
4
|
id: string;
|
|
@@ -29,6 +29,10 @@ export declare class CodexDriver implements AgentDriver {
|
|
|
29
29
|
constructor(bin?: string);
|
|
30
30
|
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
31
31
|
tui(input: TuiInput): TuiSpec;
|
|
32
|
+
listNativeSessions(opts: {
|
|
33
|
+
workdir: string;
|
|
34
|
+
limit?: number;
|
|
35
|
+
}): NativeSessionInfo[];
|
|
32
36
|
}
|
|
33
37
|
export interface CodexUsageState {
|
|
34
38
|
input: number | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { discoverCodexNativeSessions } from '../workspace/native.js';
|
|
2
3
|
// Minimal newline-delimited JSON-RPC client for `codex app-server` (ported faithfully
|
|
3
4
|
// from pikiloom's CodexAppServer; trimmed to what the kernel needs).
|
|
4
5
|
class AppServer {
|
|
@@ -184,13 +185,11 @@ export function codexReasoningItemText(item) {
|
|
|
184
185
|
];
|
|
185
186
|
return parts.map((p) => (typeof p === 'string' ? p : p?.text || '')).filter(Boolean).join('\n').trim();
|
|
186
187
|
}
|
|
187
|
-
// A completed final_answer
|
|
188
|
-
// deltaItems holds the ids already streamed, so a completed item echoing
|
|
189
|
-
// not double-counted (matches the legacy driver's deltaSeenForItem guard).
|
|
188
|
+
// A completed agentMessage (commentary preamble OR final_answer) that did NOT stream deltas:
|
|
189
|
+
// append + emit it live. deltaItems holds the ids already streamed, so a completed item echoing
|
|
190
|
+
// a streamed one is not double-counted (matches the legacy driver's deltaSeenForItem guard).
|
|
191
|
+
// Both phases are surfaced — codex's commentary preambles are part of the visible "中间过程".
|
|
190
192
|
export function captureCodexAgentMessage(item, s, deltaItems, phases, emit) {
|
|
191
|
-
const phase = item?.phase || (item?.id ? phases.get(item.id) : null) || 'final_answer';
|
|
192
|
-
if (phase !== 'final_answer')
|
|
193
|
-
return;
|
|
194
193
|
const text = typeof item?.text === 'string' ? item.text.trim() : '';
|
|
195
194
|
if (!text)
|
|
196
195
|
return;
|
|
@@ -237,6 +236,7 @@ export class CodexDriver {
|
|
|
237
236
|
const phases = new Map();
|
|
238
237
|
const toolSummaries = new Map();
|
|
239
238
|
const deltaItems = new Set();
|
|
239
|
+
let lastTextItemId = null;
|
|
240
240
|
let steerRegistered = false;
|
|
241
241
|
const ok = await srv.start();
|
|
242
242
|
if (!ok)
|
|
@@ -297,13 +297,22 @@ export class CodexDriver {
|
|
|
297
297
|
break;
|
|
298
298
|
}
|
|
299
299
|
case 'item/agentMessage/delta': {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
if (!params?.delta)
|
|
301
|
+
break;
|
|
302
|
+
// Surface BOTH commentary (preamble) and final_answer messages live. Codex narrates
|
|
303
|
+
// what it is about to do via phase=commentary agentMessages before tool calls;
|
|
304
|
+
// gating on final_answer dropped them, leaving the "中间过程" invisible. Separate
|
|
305
|
+
// distinct message items with a blank line so preamble and answer don't run together.
|
|
306
|
+
if (params.itemId && params.itemId !== lastTextItemId && state.text) {
|
|
307
|
+
state.text += '\n\n';
|
|
308
|
+
ctx.emit({ type: 'text', delta: '\n\n' });
|
|
309
|
+
}
|
|
310
|
+
if (params.itemId) {
|
|
311
|
+
lastTextItemId = params.itemId;
|
|
312
|
+
deltaItems.add(params.itemId);
|
|
306
313
|
}
|
|
314
|
+
state.text += params.delta;
|
|
315
|
+
ctx.emit({ type: 'text', delta: params.delta });
|
|
307
316
|
break;
|
|
308
317
|
}
|
|
309
318
|
case 'item/reasoning/textDelta':
|
|
@@ -405,6 +414,9 @@ export class CodexDriver {
|
|
|
405
414
|
args.push(...input.extraArgs);
|
|
406
415
|
return { command: this.bin, args, cwd: input.workdir, env: input.env };
|
|
407
416
|
}
|
|
417
|
+
listNativeSessions(opts) {
|
|
418
|
+
return discoverCodexNativeSessions(opts.workdir, { limit: opts.limit });
|
|
419
|
+
}
|
|
408
420
|
}
|
|
409
421
|
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
410
422
|
function buildTurnInput(prompt, attachments) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec } from '../contracts/driver.js';
|
|
1
|
+
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec, NativeSessionInfo } from '../contracts/driver.js';
|
|
2
2
|
export declare class GeminiDriver implements AgentDriver {
|
|
3
3
|
private readonly bin;
|
|
4
4
|
readonly id = "gemini";
|
|
@@ -11,6 +11,10 @@ export declare class GeminiDriver implements AgentDriver {
|
|
|
11
11
|
constructor(bin?: string);
|
|
12
12
|
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
13
13
|
tui(input: TuiInput): TuiSpec;
|
|
14
|
+
listNativeSessions(opts: {
|
|
15
|
+
workdir: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
}): NativeSessionInfo[];
|
|
14
18
|
}
|
|
15
19
|
export declare function parseGeminiEvent(ev: any, s: any, tools: Map<string, {
|
|
16
20
|
name: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { discoverGeminiNativeSessions } from '../workspace/native.js';
|
|
2
3
|
// Native kernel Gemini driver: `gemini --output-format stream-json ... -p <prompt>` and
|
|
3
4
|
// parse its stream-json events into kernel DriverEvents. Faithful to pikiloom's geminiParse.
|
|
4
5
|
export class GeminiDriver {
|
|
@@ -80,6 +81,9 @@ export class GeminiDriver {
|
|
|
80
81
|
args.push(...input.extraArgs);
|
|
81
82
|
return { command: this.bin, args, cwd: input.workdir, env: input.env };
|
|
82
83
|
}
|
|
84
|
+
listNativeSessions(opts) {
|
|
85
|
+
return discoverGeminiNativeSessions(opts.workdir, { limit: opts.limit });
|
|
86
|
+
}
|
|
83
87
|
}
|
|
84
88
|
export function parseGeminiEvent(ev, s, tools, emit) {
|
|
85
89
|
const t = ev.type || '';
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
export declare class HermesDriver
|
|
3
|
-
private readonly bin;
|
|
4
|
-
readonly id = "hermes";
|
|
5
|
-
readonly capabilities: {
|
|
6
|
-
steer: boolean;
|
|
7
|
-
interact: boolean;
|
|
8
|
-
resume: boolean;
|
|
9
|
-
tui: boolean;
|
|
10
|
-
};
|
|
1
|
+
import { AcpDriver, applyAcpUpdate } from './acp.js';
|
|
2
|
+
export declare class HermesDriver extends AcpDriver {
|
|
11
3
|
constructor(bin?: string);
|
|
12
|
-
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
13
4
|
}
|
|
14
|
-
export declare
|
|
5
|
+
export declare const applyHermesUpdate: typeof applyAcpUpdate;
|