pikiloom 0.4.61 → 0.4.62
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/package.json +2 -1
- package/packages/kernel/README.md +20 -7
- package/packages/kernel/dist/drivers/acp.js +30 -163
- package/packages/kernel/dist/drivers/claude.d.ts +1 -21
- package/packages/kernel/dist/drivers/claude.js +29 -57
- package/packages/kernel/dist/drivers/codex.d.ts +0 -10
- package/packages/kernel/dist/drivers/codex.js +47 -144
- package/packages/kernel/dist/drivers/gemini.js +9 -27
- package/packages/kernel/dist/drivers/hermes.d.ts +1 -2
- package/packages/kernel/dist/drivers/hermes.js +1 -4
- package/packages/kernel/dist/drivers/index.d.ts +5 -4
- package/packages/kernel/dist/drivers/index.js +8 -4
- package/packages/kernel/dist/{workspace → drivers}/native.d.ts +0 -1
- package/packages/kernel/dist/{workspace → drivers}/native.js +1 -1
- package/packages/kernel/dist/drivers/rpc.d.ts +45 -0
- package/packages/kernel/dist/drivers/rpc.js +130 -0
- package/packages/kernel/dist/drivers/shared.d.ts +21 -0
- package/packages/kernel/dist/drivers/shared.js +66 -0
- package/packages/kernel/dist/index.d.ts +2 -1
- package/packages/kernel/dist/index.js +10 -2
- package/packages/kernel/dist/protocol/index.d.ts +5 -0
- package/packages/kernel/dist/protocol/index.js +10 -0
- package/packages/kernel/dist/runtime/hub.js +4 -8
- package/packages/kernel/dist/workspace/index.d.ts +1 -2
- package/packages/kernel/dist/workspace/index.js +3 -2
- package/packages/kernel/dist/workspace/mcp.js +6 -16
- package/packages/kernel/dist/workspace/npm-search.d.ts +9 -0
- package/packages/kernel/dist/workspace/npm-search.js +21 -0
- package/packages/kernel/dist/workspace/sessions.js +6 -9
- package/packages/kernel/dist/workspace/skills.d.ts +11 -0
- package/packages/kernel/dist/workspace/skills.js +14 -20
|
@@ -1,112 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
// from pikiloom's CodexAppServer; trimmed to what the kernel needs).
|
|
5
|
-
class AppServer {
|
|
6
|
-
bin;
|
|
7
|
-
configOverrides;
|
|
8
|
-
env;
|
|
9
|
-
proc = null;
|
|
10
|
-
buf = '';
|
|
11
|
-
nextId = 1;
|
|
12
|
-
pending = new Map();
|
|
13
|
-
notify;
|
|
14
|
-
requestResponder;
|
|
15
|
-
constructor(bin, configOverrides, env) {
|
|
16
|
-
this.bin = bin;
|
|
17
|
-
this.configOverrides = configOverrides;
|
|
18
|
-
this.env = env;
|
|
19
|
-
}
|
|
20
|
-
onNotification(cb) { this.notify = cb; }
|
|
21
|
-
onServerRequest(cb) { this.requestResponder = cb; }
|
|
22
|
-
async start() {
|
|
23
|
-
const args = ['app-server'];
|
|
24
|
-
// Do NOT force model_reasoning_summary — codex reasoning summaries stay OFF by default
|
|
25
|
-
// (respecting ~/.codex/config.toml, which is the original behavior). A caller that wants
|
|
26
|
-
// thinking can pass `model_reasoning_summary=...` via configOverrides; we never inject it.
|
|
27
|
-
const overrides = [...this.configOverrides];
|
|
28
|
-
if (!overrides.some(c => /^features\.goals\s*=/.test(c)))
|
|
29
|
-
overrides.push('features.goals=true');
|
|
30
|
-
for (const c of overrides)
|
|
31
|
-
args.push('-c', c);
|
|
32
|
-
try {
|
|
33
|
-
this.proc = spawn(this.bin, args, { stdio: ['pipe', 'pipe', 'pipe'], env: this.env ? { ...process.env, ...this.env } : process.env });
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
this.proc.stdout.on('data', (chunk) => this.onData(chunk));
|
|
39
|
-
this.proc.on('close', () => { for (const cb of this.pending.values())
|
|
40
|
-
cb({ error: { message: 'app-server exited' } }); this.pending.clear(); });
|
|
41
|
-
this.proc.on('error', () => { });
|
|
42
|
-
const init = await this.call('initialize', { clientInfo: { name: '@pikiloom/kernel', version: '0.1.0' }, capabilities: { experimentalApi: true } }, 15_000);
|
|
43
|
-
return !init.error;
|
|
44
|
-
}
|
|
45
|
-
onData(chunk) {
|
|
46
|
-
this.buf += chunk.toString('utf8');
|
|
47
|
-
const lines = this.buf.split('\n');
|
|
48
|
-
this.buf = lines.pop() || '';
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
if (!line.trim())
|
|
51
|
-
continue;
|
|
52
|
-
let msg;
|
|
53
|
-
try {
|
|
54
|
-
msg = JSON.parse(line);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (msg.method && msg.id != null) { // server -> client request
|
|
60
|
-
const id = msg.id;
|
|
61
|
-
Promise.resolve(this.requestResponder ? this.requestResponder(msg.method, msg.params ?? {}, id) : {})
|
|
62
|
-
.then((result) => this.respond(id, result ?? {}))
|
|
63
|
-
.catch(() => this.respond(id, {}));
|
|
64
|
-
}
|
|
65
|
-
else if (msg.id != null) { // response to our call
|
|
66
|
-
const cb = this.pending.get(msg.id);
|
|
67
|
-
if (cb) {
|
|
68
|
-
this.pending.delete(msg.id);
|
|
69
|
-
cb(msg);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else if (msg.method) { // notification
|
|
73
|
-
this.notify?.(msg.method, msg.params ?? {});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
call(method, params, timeoutMs = 60_000) {
|
|
78
|
-
return new Promise((resolve) => {
|
|
79
|
-
if (!this.proc || this.proc.killed) {
|
|
80
|
-
resolve({ error: { message: 'not connected' } });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const id = this.nextId++;
|
|
84
|
-
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `RPC '${method}' timed out` } }); }, timeoutMs);
|
|
85
|
-
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
86
|
-
const msg = { jsonrpc: '2.0', id, method };
|
|
87
|
-
if (params !== undefined)
|
|
88
|
-
msg.params = params;
|
|
89
|
-
try {
|
|
90
|
-
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
clearTimeout(timer);
|
|
94
|
-
this.pending.delete(id);
|
|
95
|
-
resolve({ error: { message: 'write failed' } });
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
respond(id, result) {
|
|
100
|
-
try {
|
|
101
|
-
this.proc?.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
102
|
-
}
|
|
103
|
-
catch { /* closed */ }
|
|
104
|
-
}
|
|
105
|
-
kill() { try {
|
|
106
|
-
this.proc?.kill('SIGTERM');
|
|
107
|
-
}
|
|
108
|
-
catch { /* ignore */ } this.proc = null; }
|
|
109
|
-
}
|
|
1
|
+
import { discoverCodexNativeSessions } from './native.js';
|
|
2
|
+
import { StdioRpcClient } from './rpc.js';
|
|
3
|
+
import { attachedFileNote, contextPercent, imageMimeForFile, wireAbort } from './shared.js';
|
|
110
4
|
function planFromUpdate(params) {
|
|
111
5
|
const rawSteps = Array.isArray(params?.plan?.steps) ? params.plan.steps : Array.isArray(params?.steps) ? params.steps : Array.isArray(params?.plan) ? params.plan : [];
|
|
112
6
|
const steps = rawSteps.map((st) => {
|
|
@@ -247,16 +141,23 @@ export class CodexDriver {
|
|
|
247
141
|
// BYOK provider routing arrives as `-c key=value` overrides; pass them through so the
|
|
248
142
|
// kernel path keeps third-party models (e.g. glm via OpenRouter) instead of falling back
|
|
249
143
|
// to the native account.
|
|
250
|
-
|
|
251
|
-
|
|
144
|
+
// Do NOT force model_reasoning_summary — codex reasoning summaries stay OFF by default
|
|
145
|
+
// (respecting ~/.codex/config.toml, which is the original behavior). A caller that wants
|
|
146
|
+
// thinking can pass `model_reasoning_summary=...` via configOverrides; we never inject it.
|
|
147
|
+
const overrides = [...(input.configOverrides || [])];
|
|
148
|
+
if (!overrides.some(c => /^features\.goals\s*=/.test(c)))
|
|
149
|
+
overrides.push('features.goals=true');
|
|
150
|
+
const args = ['app-server'];
|
|
151
|
+
for (const c of overrides)
|
|
152
|
+
args.push('-c', c);
|
|
153
|
+
const srv = new StdioRpcClient({ command: this.bin, args, env: input.env, label: 'codex app-server' });
|
|
252
154
|
const state = { text: '', reasoning: '', streamedReasoning: false, msgs: [], thinkParts: [], sessionId: input.sessionId ?? null, input: null, output: null, cached: null, contextUsed: null, contextWindow: null, status: null, error: null, turnId: null };
|
|
253
155
|
const phases = new Map();
|
|
254
156
|
const toolSummaries = new Map();
|
|
255
157
|
const deltaItems = new Set();
|
|
256
158
|
let lastTextItemId = null;
|
|
257
159
|
let steerRegistered = false;
|
|
258
|
-
|
|
259
|
-
if (!ok)
|
|
160
|
+
if (!srv.start())
|
|
260
161
|
return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
|
|
261
162
|
let settle = () => { };
|
|
262
163
|
const turnDone = new Promise((res) => { settle = res; });
|
|
@@ -264,20 +165,19 @@ export class CodexDriver {
|
|
|
264
165
|
// srv.kill() (SIGTERM) never produces a turn/completed notification, so without this explicit
|
|
265
166
|
// settle() the `await turnDone` below hangs forever — run() never resolves and the task stays
|
|
266
167
|
// "running" in the orchestrator even though the codex process is already dead ("停止不掉,但实际上已经停了").
|
|
267
|
-
|
|
168
|
+
wireAbort(ctx.signal, () => {
|
|
268
169
|
if (state.sessionId && state.turnId) {
|
|
269
|
-
srv.
|
|
170
|
+
srv.request('turn/interrupt', { threadId: state.sessionId, turnId: state.turnId }, 5_000).finally(() => settle());
|
|
270
171
|
}
|
|
271
172
|
else {
|
|
272
173
|
srv.kill();
|
|
273
174
|
settle();
|
|
274
175
|
}
|
|
275
|
-
};
|
|
276
|
-
if (ctx.signal.aborted)
|
|
277
|
-
onAbort();
|
|
278
|
-
else
|
|
279
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
176
|
+
});
|
|
280
177
|
try {
|
|
178
|
+
const init = await srv.request('initialize', { clientInfo: { name: '@pikiloom/kernel', version: '0.1.0' }, capabilities: { experimentalApi: true } }, 15_000);
|
|
179
|
+
if (init.error)
|
|
180
|
+
return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
|
|
281
181
|
const threadParams = { cwd: input.workdir, model: input.model || null };
|
|
282
182
|
if (input.systemPrompt)
|
|
283
183
|
threadParams.developerInstructions = input.systemPrompt;
|
|
@@ -286,8 +186,8 @@ export class CodexDriver {
|
|
|
286
186
|
threadParams.sandbox = 'danger-full-access';
|
|
287
187
|
}
|
|
288
188
|
const threadResp = input.sessionId
|
|
289
|
-
? await srv.
|
|
290
|
-
: await srv.
|
|
189
|
+
? await srv.request('thread/resume', { threadId: input.sessionId, ...threadParams })
|
|
190
|
+
: await srv.request('thread/start', threadParams);
|
|
291
191
|
if (threadResp.error)
|
|
292
192
|
return { ok: false, text: '', error: threadResp.error.message || 'thread/start failed', stopReason: 'error' };
|
|
293
193
|
const threadId = threadResp.result?.thread?.id ?? input.sessionId ?? null;
|
|
@@ -306,7 +206,7 @@ export class CodexDriver {
|
|
|
306
206
|
ctx.registerSteer(async (prompt, attachments = []) => {
|
|
307
207
|
if (!state.sessionId || !state.turnId)
|
|
308
208
|
return false;
|
|
309
|
-
const r = await srv.
|
|
209
|
+
const r = await srv.request('turn/steer', { threadId: state.sessionId, expectedTurnId: state.turnId, input: buildTurnInput(prompt, attachments) }, 30_000);
|
|
310
210
|
if (r.error)
|
|
311
211
|
return false;
|
|
312
212
|
state.turnId = r.result?.turnId ?? state.turnId;
|
|
@@ -400,22 +300,28 @@ export class CodexDriver {
|
|
|
400
300
|
}
|
|
401
301
|
});
|
|
402
302
|
// Codex server->client requests: route user-input to the HITL seam (ctx.askUser),
|
|
403
|
-
// accept approvals by default (parity with the legacy codex driver).
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
303
|
+
// accept approvals by default (parity with the legacy codex driver). Never throw —
|
|
304
|
+
// an unanswerable request degrades to an empty response, not a JSON-RPC error.
|
|
305
|
+
srv.onRequest(async (method, params, id) => {
|
|
306
|
+
try {
|
|
307
|
+
if (method === 'item/tool/requestUserInput') {
|
|
308
|
+
const interaction = codexUserInputToInteraction(params, `codex-input-${id}`);
|
|
309
|
+
if (!interaction)
|
|
310
|
+
return { answers: {} };
|
|
311
|
+
const answers = await ctx.askUser(interaction);
|
|
312
|
+
return { answers: Object.fromEntries(Object.entries(answers).map(([qid, vals]) => [qid, { answers: vals }])) };
|
|
313
|
+
}
|
|
314
|
+
if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval')
|
|
315
|
+
return { decision: 'accept' };
|
|
316
|
+
if (method === 'item/permissions/requestApproval')
|
|
317
|
+
return { permissions: {}, scope: 'turn' };
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return {};
|
|
411
322
|
}
|
|
412
|
-
if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval')
|
|
413
|
-
return { decision: 'accept' };
|
|
414
|
-
if (method === 'item/permissions/requestApproval')
|
|
415
|
-
return { permissions: {}, scope: 'turn' };
|
|
416
|
-
return {};
|
|
417
323
|
});
|
|
418
|
-
const turnResp = await srv.
|
|
324
|
+
const turnResp = await srv.request('turn/start', {
|
|
419
325
|
threadId: state.sessionId,
|
|
420
326
|
input: buildTurnInput(input.prompt, input.attachments || []),
|
|
421
327
|
model: input.model || undefined,
|
|
@@ -453,12 +359,10 @@ export class CodexDriver {
|
|
|
453
359
|
return discoverCodexNativeSessions(opts.workdir, { limit: opts.limit });
|
|
454
360
|
}
|
|
455
361
|
}
|
|
456
|
-
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
457
362
|
function buildTurnInput(prompt, attachments) {
|
|
458
363
|
const input = [];
|
|
459
364
|
for (const f of attachments) {
|
|
460
|
-
|
|
461
|
-
input.push(IMAGE_EXTS.has(ext) ? { type: 'localImage', path: f } : { type: 'text', text: `[Attached file: ${f}]` });
|
|
365
|
+
input.push(imageMimeForFile(f) ? { type: 'localImage', path: f } : { type: 'text', text: attachedFileNote(f) });
|
|
462
366
|
}
|
|
463
367
|
input.push({ type: 'text', text: prompt });
|
|
464
368
|
return input;
|
|
@@ -486,7 +390,7 @@ function codexContextUsed(raw) {
|
|
|
486
390
|
}
|
|
487
391
|
// Fold a codex tokenUsage payload into driver state: last-turn counts, context
|
|
488
392
|
// occupancy, and the model window. Tolerant of nested {info:{…}} and flat shapes.
|
|
489
|
-
|
|
393
|
+
function applyCodexTokenUsage(s, rawUsage) {
|
|
490
394
|
if (!rawUsage || typeof rawUsage !== 'object')
|
|
491
395
|
return;
|
|
492
396
|
const info = rawUsage.info && typeof rawUsage.info === 'object' ? rawUsage.info : rawUsage;
|
|
@@ -521,18 +425,17 @@ export function applyCodexTokenUsage(s, rawUsage) {
|
|
|
521
425
|
if (cw != null && cw > 0)
|
|
522
426
|
s.contextWindow = cw;
|
|
523
427
|
}
|
|
524
|
-
|
|
428
|
+
function codexUsageOf(s) {
|
|
525
429
|
const fallback = (s.input ?? 0) + (s.cached ?? 0);
|
|
526
430
|
const used = s.contextUsed ?? (fallback > 0 ? fallback : null);
|
|
527
431
|
const window = s.contextWindow ?? null;
|
|
528
|
-
const contextPercent = used != null && window ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
529
432
|
const turnOutput = s.output ?? 0;
|
|
530
433
|
return {
|
|
531
434
|
inputTokens: s.input,
|
|
532
435
|
outputTokens: s.output,
|
|
533
436
|
cachedInputTokens: s.cached,
|
|
534
437
|
contextUsedTokens: used,
|
|
535
|
-
contextPercent,
|
|
438
|
+
contextPercent: contextPercent(used, window),
|
|
536
439
|
turnOutputTokens: turnOutput > 0 ? turnOutput : null,
|
|
537
440
|
};
|
|
538
441
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { discoverGeminiNativeSessions } from '
|
|
2
|
+
import { discoverGeminiNativeSessions } from './native.js';
|
|
3
|
+
import { createLineBuffer, parseJsonLine, sigterm, wireAbort } from './shared.js';
|
|
3
4
|
// Native kernel Gemini driver: `gemini --output-format stream-json ... -p <prompt>` and
|
|
4
5
|
// parse its stream-json events into kernel DriverEvents. Faithful to pikiloom's geminiParse.
|
|
5
6
|
export class GeminiDriver {
|
|
@@ -21,7 +22,7 @@ export class GeminiDriver {
|
|
|
21
22
|
if (extra.length)
|
|
22
23
|
args.push(...extra);
|
|
23
24
|
args.push('-p', input.prompt);
|
|
24
|
-
const s = { text: '', sessionId: input.sessionId ?? null,
|
|
25
|
+
const s = { text: '', sessionId: input.sessionId ?? null, input: null, output: null, cached: null, stopReason: null, error: null };
|
|
25
26
|
const tools = new Map();
|
|
26
27
|
return new Promise((resolve) => {
|
|
27
28
|
let child;
|
|
@@ -32,32 +33,14 @@ export class GeminiDriver {
|
|
|
32
33
|
resolve({ ok: false, text: '', error: `spawn failed: ${err?.message || err}`, stopReason: 'error' });
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
catch { /* ignore */ } };
|
|
39
|
-
if (ctx.signal.aborted)
|
|
40
|
-
onAbort();
|
|
41
|
-
else
|
|
42
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
43
|
-
let buf = '';
|
|
36
|
+
wireAbort(ctx.signal, () => sigterm(child));
|
|
37
|
+
const nextLines = createLineBuffer();
|
|
44
38
|
let stderr = '';
|
|
45
39
|
child.stdout.on('data', (chunk) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const trimmed = line.trim();
|
|
51
|
-
if (!trimmed)
|
|
52
|
-
continue;
|
|
53
|
-
let ev;
|
|
54
|
-
try {
|
|
55
|
-
ev = JSON.parse(trimmed);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
parseGeminiEvent(ev, s, tools, ctx.emit);
|
|
40
|
+
for (const line of nextLines(chunk)) {
|
|
41
|
+
const ev = parseJsonLine(line);
|
|
42
|
+
if (ev !== undefined)
|
|
43
|
+
parseGeminiEvent(ev, s, tools, ctx.emit);
|
|
61
44
|
}
|
|
62
45
|
});
|
|
63
46
|
child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
|
|
@@ -92,7 +75,6 @@ export function parseGeminiEvent(ev, s, tools, emit) {
|
|
|
92
75
|
s.sessionId = ev.session_id;
|
|
93
76
|
emit({ type: 'session', sessionId: ev.session_id });
|
|
94
77
|
}
|
|
95
|
-
s.model = ev.model ?? s.model;
|
|
96
78
|
return;
|
|
97
79
|
}
|
|
98
80
|
if (t === 'message' && ev.role === 'assistant') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AcpDriver
|
|
1
|
+
import { AcpDriver } from './acp.js';
|
|
2
2
|
// Hermes = the reference ACP agent, shipped as a thin preset over the generic AcpDriver.
|
|
3
3
|
// Any other ACP CLI (OpenCode, Gemini-ACP, …) is just `new AcpDriver({ id, command, args })`.
|
|
4
4
|
export class HermesDriver extends AcpDriver {
|
|
@@ -6,6 +6,3 @@ export class HermesDriver extends AcpDriver {
|
|
|
6
6
|
super({ id: 'hermes', command: bin, args: ['acp'] });
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
// Back-compat alias: the generic ACP session/update parser handles the identical wire that
|
|
10
|
-
// the original hermes-specific parser did. Kept so existing imports/tests resolve.
|
|
11
|
-
export const applyHermesUpdate = applyAcpUpdate;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { EchoDriver } from './echo.js';
|
|
2
|
-
export { ClaudeDriver
|
|
2
|
+
export { ClaudeDriver } from './claude.js';
|
|
3
3
|
export { CodexDriver } from './codex.js';
|
|
4
|
-
export { GeminiDriver
|
|
5
|
-
export { AcpDriver,
|
|
6
|
-
export { HermesDriver
|
|
4
|
+
export { GeminiDriver } from './gemini.js';
|
|
5
|
+
export { AcpDriver, type AcpDriverConfig } from './acp.js';
|
|
6
|
+
export { HermesDriver } from './hermes.js';
|
|
7
|
+
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, type DiscoverOptions, type NativeSessionInfo, } from './native.js';
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
// The agent axis: one driver per coding-agent CLI, plus native-session discovery (pure
|
|
2
|
+
// readers of each CLI's own on-disk transcript store). White-box parser/settle helpers are
|
|
3
|
+
// deliberately NOT re-exported here — tests import them from their defining module.
|
|
1
4
|
export { EchoDriver } from './echo.js';
|
|
2
|
-
export { ClaudeDriver
|
|
5
|
+
export { ClaudeDriver } from './claude.js';
|
|
3
6
|
export { CodexDriver } from './codex.js';
|
|
4
|
-
export { GeminiDriver
|
|
5
|
-
export { AcpDriver
|
|
6
|
-
export { HermesDriver
|
|
7
|
+
export { GeminiDriver } from './gemini.js';
|
|
8
|
+
export { AcpDriver } from './acp.js';
|
|
9
|
+
export { HermesDriver } from './hermes.js';
|
|
10
|
+
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, } from './native.js';
|
|
@@ -6,7 +6,6 @@ export interface DiscoverOptions {
|
|
|
6
6
|
/** A session whose file changed within this window is treated as "running". */
|
|
7
7
|
runningThresholdMs?: number;
|
|
8
8
|
}
|
|
9
|
-
export declare const NATIVE_SESSION_RUNNING_THRESHOLD_MS = 10000;
|
|
10
9
|
/** Claude encodes a workdir into a project dir name by replacing non-alphanumerics with '-'. */
|
|
11
10
|
export declare function encodeClaudeProjectDir(workdir: string): string;
|
|
12
11
|
export declare function discoverClaudeNativeSessions(workdir: string, opts?: DiscoverOptions): NativeSessionInfo[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
|
|
4
|
+
const NATIVE_SESSION_RUNNING_THRESHOLD_MS = 10_000;
|
|
5
5
|
function homeOf(opts) {
|
|
6
6
|
return opts?.home || os.homedir();
|
|
7
7
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type RpcMsg = {
|
|
2
|
+
jsonrpc?: string;
|
|
3
|
+
id?: number | string;
|
|
4
|
+
method?: string;
|
|
5
|
+
params?: any;
|
|
6
|
+
result?: any;
|
|
7
|
+
error?: any;
|
|
8
|
+
};
|
|
9
|
+
/** Throw from an onRequest handler to answer with a specific JSON-RPC error code. */
|
|
10
|
+
export declare class RpcError extends Error {
|
|
11
|
+
readonly code: number;
|
|
12
|
+
constructor(code: number, message: string);
|
|
13
|
+
}
|
|
14
|
+
export interface StdioRpcOptions {
|
|
15
|
+
command: string;
|
|
16
|
+
args: string[];
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class StdioRpcClient {
|
|
22
|
+
private readonly opts;
|
|
23
|
+
private proc;
|
|
24
|
+
private nextId;
|
|
25
|
+
private readonly pending;
|
|
26
|
+
private notifyCb?;
|
|
27
|
+
private requestCb?;
|
|
28
|
+
private readonly stderrTail;
|
|
29
|
+
private readonly label;
|
|
30
|
+
constructor(opts: StdioRpcOptions);
|
|
31
|
+
onNotification(cb: (method: string, params: any) => void): void;
|
|
32
|
+
/** Serve peer->client requests. Throw RpcError for a coded error; anything else answers -32603. */
|
|
33
|
+
onRequest(cb: (method: string, params: any, id: number | string) => any | Promise<any>): void;
|
|
34
|
+
/** Rolling tail of the process' stderr — startup/crash diagnostics. */
|
|
35
|
+
stderrText(): string;
|
|
36
|
+
start(): boolean;
|
|
37
|
+
private onLine;
|
|
38
|
+
private handleRequest;
|
|
39
|
+
request(method: string, params?: any, timeoutMs?: number): Promise<RpcMsg>;
|
|
40
|
+
notify(method: string, params?: any): void;
|
|
41
|
+
private respond;
|
|
42
|
+
private respondError;
|
|
43
|
+
private write;
|
|
44
|
+
kill(): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
/** Throw from an onRequest handler to answer with a specific JSON-RPC error code. */
|
|
4
|
+
export class RpcError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const STDERR_TAIL_LINES = 20;
|
|
12
|
+
export class StdioRpcClient {
|
|
13
|
+
opts;
|
|
14
|
+
proc = null;
|
|
15
|
+
nextId = 1;
|
|
16
|
+
pending = new Map();
|
|
17
|
+
notifyCb;
|
|
18
|
+
requestCb;
|
|
19
|
+
stderrTail = [];
|
|
20
|
+
label;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
this.label = opts.label || opts.command;
|
|
24
|
+
}
|
|
25
|
+
onNotification(cb) { this.notifyCb = cb; }
|
|
26
|
+
/** Serve peer->client requests. Throw RpcError for a coded error; anything else answers -32603. */
|
|
27
|
+
onRequest(cb) { this.requestCb = cb; }
|
|
28
|
+
/** Rolling tail of the process' stderr — startup/crash diagnostics. */
|
|
29
|
+
stderrText() { return this.stderrTail.join('\n'); }
|
|
30
|
+
start() {
|
|
31
|
+
try {
|
|
32
|
+
this.proc = spawn(this.opts.command, this.opts.args, {
|
|
33
|
+
cwd: this.opts.cwd,
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
env: this.opts.env ? { ...process.env, ...this.opts.env } : process.env,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const rl = createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
|
|
42
|
+
rl.on('line', (line) => this.onLine(line));
|
|
43
|
+
// Always drain stderr (an unread pipe backpressures the child) and keep a tail for errors.
|
|
44
|
+
this.proc.stderr.on('data', (chunk) => {
|
|
45
|
+
for (const ln of chunk.toString('utf8').split('\n')) {
|
|
46
|
+
const t = ln.trim();
|
|
47
|
+
if (!t)
|
|
48
|
+
continue;
|
|
49
|
+
this.stderrTail.push(t.slice(0, 240));
|
|
50
|
+
if (this.stderrTail.length > STDERR_TAIL_LINES)
|
|
51
|
+
this.stderrTail.shift();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.proc.on('close', () => {
|
|
55
|
+
for (const cb of this.pending.values())
|
|
56
|
+
cb({ error: { message: `${this.label} exited` } });
|
|
57
|
+
this.pending.clear();
|
|
58
|
+
});
|
|
59
|
+
this.proc.on('error', () => { });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
onLine(line) {
|
|
63
|
+
const t = line.trim();
|
|
64
|
+
if (!t)
|
|
65
|
+
return;
|
|
66
|
+
let m;
|
|
67
|
+
try {
|
|
68
|
+
m = JSON.parse(t);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return;
|
|
72
|
+
} // non-JSON stdout noise
|
|
73
|
+
if (m.method && m.id != null) {
|
|
74
|
+
void this.handleRequest(m);
|
|
75
|
+
return;
|
|
76
|
+
} // peer -> client request
|
|
77
|
+
if (m.id != null) { // response to one of our requests
|
|
78
|
+
const cb = this.pending.get(m.id);
|
|
79
|
+
if (cb) {
|
|
80
|
+
this.pending.delete(m.id);
|
|
81
|
+
cb(m);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (m.method)
|
|
86
|
+
this.notifyCb?.(m.method, m.params ?? {}); // notification
|
|
87
|
+
}
|
|
88
|
+
async handleRequest(m) {
|
|
89
|
+
const id = m.id;
|
|
90
|
+
if (!this.requestCb) {
|
|
91
|
+
this.respondError(id, -32601, `Method not implemented: ${m.method}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const result = await this.requestCb(m.method, m.params ?? {}, id);
|
|
96
|
+
this.respond(id, result ?? null);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
const code = e instanceof RpcError ? e.code : -32603;
|
|
100
|
+
this.respondError(id, code, e?.message || 'handler error');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
request(method, params, timeoutMs = 60_000) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
if (!this.proc || this.proc.killed) {
|
|
106
|
+
resolve({ error: { message: 'not connected' } });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const id = this.nextId++;
|
|
110
|
+
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `${this.label} '${method}' timed out` } }); }, timeoutMs);
|
|
111
|
+
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
112
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
notify(method, params) { this.write({ jsonrpc: '2.0', method, params }); }
|
|
116
|
+
respond(id, result) { this.write({ jsonrpc: '2.0', id, result }); }
|
|
117
|
+
respondError(id, code, message) { this.write({ jsonrpc: '2.0', id, error: { code, message } }); }
|
|
118
|
+
write(msg) {
|
|
119
|
+
if (!this.proc || this.proc.killed)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
123
|
+
}
|
|
124
|
+
catch { /* stream closed */ }
|
|
125
|
+
}
|
|
126
|
+
kill() { try {
|
|
127
|
+
this.proc?.kill('SIGTERM');
|
|
128
|
+
}
|
|
129
|
+
catch { /* ignore */ } this.proc = null; }
|
|
130
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
/** Stateful newline splitter for a child process' stdout: feed chunks, get complete lines. */
|
|
3
|
+
export declare function createLineBuffer(): (chunk: Buffer | string) => string[];
|
|
4
|
+
/** Parse one ndjson line; undefined for blank lines / non-JSON noise. */
|
|
5
|
+
export declare function parseJsonLine(line: string): any | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Run `fn` when the signal aborts (immediately if it already has). Returns an
|
|
8
|
+
* unsubscribe for drivers that must detach the handler when the turn settles.
|
|
9
|
+
*/
|
|
10
|
+
export declare function wireAbort(signal: AbortSignal, fn: () => void): () => void;
|
|
11
|
+
/** SIGTERM a child, swallowing the already-dead race. */
|
|
12
|
+
export declare function sigterm(proc: ChildProcess | null | undefined): void;
|
|
13
|
+
/** Mime type when the file is an inlineable image, else null. */
|
|
14
|
+
export declare function imageMimeForFile(filePath: string): string | null;
|
|
15
|
+
/** The text note substituted for a non-image attachment. */
|
|
16
|
+
export declare function attachedFileNote(filePath: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Context-window occupancy as a display percent (one decimal, capped at 99.9).
|
|
19
|
+
* Pass `used` as null when the caller wants "no data" rather than 0%.
|
|
20
|
+
*/
|
|
21
|
+
export declare function contextPercent(used: number | null | undefined, window: number | null | undefined): number | null;
|