pikiloom 0.4.60 → 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 +4 -21
- package/packages/kernel/dist/drivers/claude.js +88 -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,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;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
// Driver-internal helpers shared by the concrete drivers (claude/codex/gemini/acp).
|
|
3
|
+
// NOT part of the public API — nothing here is re-exported by any barrel. Each helper
|
|
4
|
+
// exists because the same code appeared verbatim in 3+ drivers.
|
|
5
|
+
/** Stateful newline splitter for a child process' stdout: feed chunks, get complete lines. */
|
|
6
|
+
export function createLineBuffer() {
|
|
7
|
+
let buf = '';
|
|
8
|
+
return (chunk) => {
|
|
9
|
+
buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
10
|
+
const lines = buf.split('\n');
|
|
11
|
+
buf = lines.pop() || '';
|
|
12
|
+
return lines;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Parse one ndjson line; undefined for blank lines / non-JSON noise. */
|
|
16
|
+
export function parseJsonLine(line) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(trimmed);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run `fn` when the signal aborts (immediately if it already has). Returns an
|
|
29
|
+
* unsubscribe for drivers that must detach the handler when the turn settles.
|
|
30
|
+
*/
|
|
31
|
+
export function wireAbort(signal, fn) {
|
|
32
|
+
if (signal.aborted) {
|
|
33
|
+
fn();
|
|
34
|
+
return () => { };
|
|
35
|
+
}
|
|
36
|
+
signal.addEventListener('abort', fn, { once: true });
|
|
37
|
+
return () => signal.removeEventListener('abort', fn);
|
|
38
|
+
}
|
|
39
|
+
/** SIGTERM a child, swallowing the already-dead race. */
|
|
40
|
+
export function sigterm(proc) {
|
|
41
|
+
try {
|
|
42
|
+
proc?.kill('SIGTERM');
|
|
43
|
+
}
|
|
44
|
+
catch { /* ignore */ }
|
|
45
|
+
}
|
|
46
|
+
// Attachment vocabulary: every driver inlines the same image formats (the Anthropic
|
|
47
|
+
// vision set, which the others accept too) and notes non-image files the same way.
|
|
48
|
+
const IMAGE_MIME_BY_EXT = {
|
|
49
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
50
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
51
|
+
};
|
|
52
|
+
/** Mime type when the file is an inlineable image, else null. */
|
|
53
|
+
export function imageMimeForFile(filePath) {
|
|
54
|
+
return IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()] ?? null;
|
|
55
|
+
}
|
|
56
|
+
/** The text note substituted for a non-image attachment. */
|
|
57
|
+
export function attachedFileNote(filePath) {
|
|
58
|
+
return `[Attached file: ${filePath}]`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Context-window occupancy as a display percent (one decimal, capped at 99.9).
|
|
62
|
+
* Pass `used` as null when the caller wants "no data" rather than 0%.
|
|
63
|
+
*/
|
|
64
|
+
export function contextPercent(used, window) {
|
|
65
|
+
return window && used != null ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
66
|
+
}
|
|
@@ -9,13 +9,14 @@ export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, To
|
|
|
9
9
|
export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, SpawnContribution, } from './contracts/surface.js';
|
|
10
10
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
11
11
|
export { EchoDriver } from './drivers/echo.js';
|
|
12
|
-
export { ClaudeDriver
|
|
12
|
+
export { ClaudeDriver } from './drivers/claude.js';
|
|
13
13
|
export { CodexDriver } from './drivers/codex.js';
|
|
14
14
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
15
15
|
export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
|
|
16
16
|
export { HermesDriver } from './drivers/hermes.js';
|
|
17
17
|
export { WebSurface, type WebSurfaceOptions } from './surfaces/web.js';
|
|
18
18
|
export { CliSurface } from './surfaces/cli.js';
|
|
19
|
+
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, type DiscoverOptions, } from './drivers/native.js';
|
|
19
20
|
export * from './workspace/index.js';
|
|
20
21
|
export * from './accounts.js';
|
|
21
22
|
export * from './protocol/index.js';
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
// await loom.start()
|
|
9
9
|
//
|
|
10
10
|
// pikiloom itself is just a consumer of this package.
|
|
11
|
+
//
|
|
12
|
+
// This barrel IS the public API (pinned by test/api-surface.test.ts). Driver-internal
|
|
13
|
+
// parser/settle helpers are exported by their modules for white-box tests but are
|
|
14
|
+
// deliberately NOT re-exported here.
|
|
11
15
|
export { createLoom } from './runtime/loom.js';
|
|
12
16
|
export { Hub } from './runtime/hub.js';
|
|
13
17
|
export { SessionRunner } from './runtime/session-runner.js';
|
|
@@ -19,16 +23,20 @@ export { attachTui } from './runtime/tui.js';
|
|
|
19
23
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
20
24
|
// Drivers & surfaces (also available via subpath exports)
|
|
21
25
|
export { EchoDriver } from './drivers/echo.js';
|
|
22
|
-
export { ClaudeDriver
|
|
26
|
+
export { ClaudeDriver } from './drivers/claude.js';
|
|
23
27
|
export { CodexDriver } from './drivers/codex.js';
|
|
24
28
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
29
|
export { AcpDriver } from './drivers/acp.js';
|
|
26
30
|
export { HermesDriver } from './drivers/hermes.js';
|
|
27
31
|
export { WebSurface } from './surfaces/web.js';
|
|
28
32
|
export { CliSurface } from './surfaces/cli.js';
|
|
33
|
+
// Native-session discovery: pure readers of each agent CLI's own transcript store
|
|
34
|
+
// (driver-axis knowledge; drivers implement listNativeSessions with these).
|
|
35
|
+
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, } from './drivers/native.js';
|
|
29
36
|
// Workspace: unified top-level directory + session/skill/mcp management (loom.paths/sessions/skills/mcp)
|
|
30
37
|
export * from './workspace/index.js';
|
|
31
|
-
// Multi-account:
|
|
38
|
+
// Multi-account: which env var carries an agent's auth token, so an app can inject a
|
|
39
|
+
// selected account's token per spawn (see accounts.ts for the design notes).
|
|
32
40
|
export * from './accounts.js';
|
|
33
41
|
// Protocol (the wire vocabulary; shared with transports)
|
|
34
42
|
export * from './protocol/index.js';
|
|
@@ -101,6 +101,11 @@ export interface SessionMeta {
|
|
|
101
101
|
phase?: SessionPhase;
|
|
102
102
|
updatedAt?: number;
|
|
103
103
|
}
|
|
104
|
+
export declare function makeSessionKey(agent: string, sessionId: string): string;
|
|
105
|
+
export declare function splitSessionKey(sessionKey: string): {
|
|
106
|
+
agent: string;
|
|
107
|
+
sessionId: string;
|
|
108
|
+
};
|
|
104
109
|
export interface SnapshotPatch {
|
|
105
110
|
full?: UniversalSnapshot;
|
|
106
111
|
appendText?: string;
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
// This is the SSOT shape: a driver-agnostic, accumulating snapshot of one session,
|
|
3
3
|
// plus a small set of control verbs. Ported from pikiloom's pikichannel protocol.
|
|
4
4
|
export const PROTOCOL_VERSION = 1;
|
|
5
|
+
// ---- Session keys ----
|
|
6
|
+
// A sessionKey is `${agent}:${sessionId}` — the composite id shared by the runtime, every
|
|
7
|
+
// store, and every terminal. These two helpers ARE that contract; nothing else parses it.
|
|
8
|
+
export function makeSessionKey(agent, sessionId) {
|
|
9
|
+
return `${agent}:${sessionId}`;
|
|
10
|
+
}
|
|
11
|
+
export function splitSessionKey(sessionKey) {
|
|
12
|
+
const i = sessionKey.indexOf(':');
|
|
13
|
+
return i < 0 ? { agent: '', sessionId: sessionKey } : { agent: sessionKey.slice(0, i), sessionId: sessionKey.slice(i + 1) };
|
|
14
|
+
}
|
|
5
15
|
export function emptySnapshot() {
|
|
6
16
|
return { phase: 'idle', updatedAt: 0 };
|
|
7
17
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { diffSnapshot, emptySnapshot, } from '../protocol/index.js';
|
|
2
|
+
import { diffSnapshot, emptySnapshot, makeSessionKey, splitSessionKey, } from '../protocol/index.js';
|
|
3
3
|
import { SessionRunner } from './session-runner.js';
|
|
4
|
-
function splitKey(sessionKey) {
|
|
5
|
-
const i = sessionKey.indexOf(':');
|
|
6
|
-
return i < 0 ? { agent: '', sessionId: sessionKey } : { agent: sessionKey.slice(0, i), sessionId: sessionKey.slice(i + 1) };
|
|
7
|
-
}
|
|
8
4
|
export class Hub {
|
|
9
5
|
deps;
|
|
10
6
|
sessions = new Map();
|
|
@@ -57,12 +53,12 @@ export class Hub {
|
|
|
57
53
|
if (!driver)
|
|
58
54
|
throw new Error(`No driver registered for agent "${agent}"`);
|
|
59
55
|
const workdir = input.workdir || this.deps.workdir;
|
|
60
|
-
const resumeId = input.sessionKey ?
|
|
56
|
+
const resumeId = input.sessionKey ? splitSessionKey(input.sessionKey).sessionId : null;
|
|
61
57
|
const preExisted = resumeId ? !!(await this.deps.sessionStore.get(agent, resumeId)) : false;
|
|
62
58
|
const { sessionId, workspacePath } = await this.deps.sessionStore.ensure(agent, {
|
|
63
59
|
sessionId: resumeId, workdir, title: input.prompt.slice(0, 80),
|
|
64
60
|
});
|
|
65
|
-
const sessionKey =
|
|
61
|
+
const sessionKey = makeSessionKey(agent, sessionId);
|
|
66
62
|
const taskId = randomUUID();
|
|
67
63
|
const runner = new SessionRunner(sessionKey, agent, taskId, (snap, seq) => this.onRunnerUpdate(sessionKey, snap, seq), this.deps.interactionHandler);
|
|
68
64
|
this.runnersByTask.set(taskId, runner);
|
|
@@ -320,7 +316,7 @@ export class Hub {
|
|
|
320
316
|
return e ? { snapshot: e.snapshot, seq: e.seq } : null;
|
|
321
317
|
}
|
|
322
318
|
async getHistory(sessionKey) {
|
|
323
|
-
const { agent, sessionId } =
|
|
319
|
+
const { agent, sessionId } = splitSessionKey(sessionKey);
|
|
324
320
|
if (!agent || !this.deps.sessionStore.history)
|
|
325
321
|
return [];
|
|
326
322
|
try {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { resolveLoomPaths, normalizeStateDirName, type LoomPaths, type LoomScope } from './paths.js';
|
|
2
|
-
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, NATIVE_SESSION_RUNNING_THRESHOLD_MS, type DiscoverOptions, type NativeSessionInfo, } from './native.js';
|
|
3
2
|
export { SessionsManager, type ManagedSessionInfo, type SessionSource, type ListSessionsOptions, type SearchSessionsOptions, type SessionsManagerDeps, } from './sessions.js';
|
|
4
|
-
export { SkillsManager, ensureDirSymlink, type SkillInfo, type SkillSearchResult, type SkillsManagerOptions, } from './skills.js';
|
|
3
|
+
export { SkillsManager, ensureDirSymlink, parseSkillMeta, type SkillInfo, type SkillMeta, type SkillSearchResult, type SkillsManagerOptions, } from './skills.js';
|
|
5
4
|
export { McpRegistry, type McpCatalogEntry, type McpSearchResult, type McpRegistryOptions, } from './mcp.js';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Workspace subsystem: the unified top-level directory + session/skill/mcp management that
|
|
2
2
|
// a consuming app gets "for free" off createLoom() (exposed as loom.paths/sessions/skills/mcp).
|
|
3
|
+
// (Native-session discovery lives with the drivers — drivers/native.ts — because knowing each
|
|
4
|
+
// agent CLI's on-disk transcript format is driver-axis knowledge.)
|
|
3
5
|
export { resolveLoomPaths, normalizeStateDirName } from './paths.js';
|
|
4
|
-
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, NATIVE_SESSION_RUNNING_THRESHOLD_MS, } from './native.js';
|
|
5
6
|
export { SessionsManager, } from './sessions.js';
|
|
6
|
-
export { SkillsManager, ensureDirSymlink, } from './skills.js';
|
|
7
|
+
export { SkillsManager, ensureDirSymlink, parseSkillMeta, } from './skills.js';
|
|
7
8
|
export { McpRegistry, } from './mcp.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { searchNpmPackages } from './npm-search.js';
|
|
1
2
|
const BUILTIN_RECOMMENDED = [
|
|
2
3
|
{ id: 'filesystem', name: 'Filesystem', description: 'Read/write files under allowed directories.', category: 'core', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'] }, homepage: 'https://github.com/modelcontextprotocol/servers' },
|
|
3
4
|
{ id: 'github', name: 'GitHub', description: 'GitHub repos, issues, and PRs.', category: 'dev', brand: 'github', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] }, envKeys: ['GITHUB_PERSONAL_ACCESS_TOKEN'], homepage: 'https://github.com/modelcontextprotocol/servers' },
|
|
@@ -57,22 +58,11 @@ export class McpRegistry {
|
|
|
57
58
|
}
|
|
58
59
|
// 2) npm fallback.
|
|
59
60
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const objects = Array.isArray(data?.objects) ? data.objects : [];
|
|
66
|
-
return objects.map((o) => {
|
|
67
|
-
const pkg = o?.package ?? {};
|
|
68
|
-
return {
|
|
69
|
-
name: String(pkg.name ?? ''),
|
|
70
|
-
description: pkg.description ?? null,
|
|
71
|
-
source: 'npm',
|
|
72
|
-
npmPackage: pkg.name ?? null,
|
|
73
|
-
homepage: pkg.links?.homepage ?? pkg.links?.npm ?? null,
|
|
74
|
-
};
|
|
75
|
-
}).filter(s => s.name).slice(0, n);
|
|
61
|
+
const hits = await searchNpmPackages(`mcp server ${q}`.trim(), n, fetchImpl);
|
|
62
|
+
return hits.map(h => ({
|
|
63
|
+
name: h.name, description: h.description, source: 'npm',
|
|
64
|
+
npmPackage: h.name, homepage: h.homepage,
|
|
65
|
+
})).slice(0, n);
|
|
76
66
|
}
|
|
77
67
|
catch (e) {
|
|
78
68
|
this.log?.(`[mcp] npm search failed: ${e?.message || e}`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface NpmPackageHit {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string | null;
|
|
4
|
+
homepage: string | null;
|
|
5
|
+
author: string | null;
|
|
6
|
+
version: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** Query registry.npmjs.org's search endpoint. [] on non-OK; throws on network failure. */
|
|
9
|
+
export declare function searchNpmPackages(text: string, size: number, fetchImpl: typeof fetch): Promise<NpmPackageHit[]>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shared npm-registry search used by SkillsManager.search and McpRegistry's npm fallback.
|
|
2
|
+
// Workspace-internal — not exported by any barrel.
|
|
3
|
+
/** Query registry.npmjs.org's search endpoint. [] on non-OK; throws on network failure. */
|
|
4
|
+
export async function searchNpmPackages(text, size, fetchImpl) {
|
|
5
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=${size}`;
|
|
6
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
7
|
+
if (!res.ok)
|
|
8
|
+
return [];
|
|
9
|
+
const data = await res.json();
|
|
10
|
+
const objects = Array.isArray(data?.objects) ? data.objects : [];
|
|
11
|
+
return objects.map((o) => {
|
|
12
|
+
const pkg = o?.package ?? {};
|
|
13
|
+
return {
|
|
14
|
+
name: String(pkg.name ?? ''),
|
|
15
|
+
description: pkg.description ?? null,
|
|
16
|
+
homepage: pkg.links?.homepage ?? pkg.links?.npm ?? null,
|
|
17
|
+
author: pkg.publisher?.username ?? pkg.author?.name ?? null,
|
|
18
|
+
version: pkg.version ?? null,
|
|
19
|
+
};
|
|
20
|
+
}).filter(h => h.name);
|
|
21
|
+
}
|