@pikiloom/kernel 0.1.5 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +29 -0
- package/dist/contracts/driver.d.ts +15 -0
- package/dist/contracts/ports.d.ts +2 -0
- package/dist/drivers/acp.d.ts +24 -0
- package/dist/drivers/acp.js +477 -0
- package/dist/drivers/claude.d.ts +5 -1
- package/dist/drivers/claude.js +4 -0
- package/dist/drivers/codex.d.ts +5 -1
- package/dist/drivers/codex.js +24 -12
- package/dist/drivers/gemini.d.ts +5 -1
- package/dist/drivers/gemini.js +4 -0
- package/dist/drivers/hermes.d.ts +3 -12
- package/dist/drivers/hermes.js +8 -191
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/index.js +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -0
- package/dist/ports/defaults.js +7 -1
- package/dist/runtime/loom.d.ts +10 -0
- package/dist/runtime/loom.js +18 -2
- package/dist/workspace/index.d.ts +5 -0
- package/dist/workspace/index.js +7 -0
- package/dist/workspace/mcp.d.ts +44 -0
- package/dist/workspace/mcp.js +82 -0
- package/dist/workspace/native.d.ts +14 -0
- package/dist/workspace/native.js +340 -0
- package/dist/workspace/paths.d.ts +33 -0
- package/dist/workspace/paths.js +40 -0
- package/dist/workspace/sessions.d.ts +49 -0
- package/dist/workspace/sessions.js +130 -0
- package/dist/workspace/skills.d.ts +44 -0
- package/dist/workspace/skills.js +173 -0
- package/llms.txt +23 -1
- package/package.json +1 -1
package/dist/drivers/codex.d.ts
CHANGED
|
@@ -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;
|
package/dist/drivers/codex.js
CHANGED
|
@@ -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) {
|
package/dist/drivers/gemini.d.ts
CHANGED
|
@@ -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;
|
package/dist/drivers/gemini.js
CHANGED
|
@@ -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 || '';
|
package/dist/drivers/hermes.d.ts
CHANGED
|
@@ -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;
|
package/dist/drivers/hermes.js
CHANGED
|
@@ -1,194 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
class
|
|
5
|
-
bin;
|
|
6
|
-
args;
|
|
7
|
-
env;
|
|
8
|
-
proc = null;
|
|
9
|
-
buf = '';
|
|
10
|
-
nextId = 1;
|
|
11
|
-
pending = new Map();
|
|
12
|
-
notify;
|
|
13
|
-
constructor(bin, args, env) {
|
|
14
|
-
this.bin = bin;
|
|
15
|
-
this.args = args;
|
|
16
|
-
this.env = env;
|
|
17
|
-
}
|
|
18
|
-
onNotification(cb) { this.notify = cb; }
|
|
19
|
-
start() {
|
|
20
|
-
try {
|
|
21
|
-
this.proc = spawn(this.bin, this.args, { stdio: ['pipe', 'pipe', 'pipe'], env: this.env ? { ...process.env, ...this.env } : process.env });
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
this.proc.stdout.on('data', (chunk) => this.onData(chunk));
|
|
27
|
-
this.proc.on('close', () => { for (const cb of this.pending.values())
|
|
28
|
-
cb({ error: { message: 'acp exited' } }); this.pending.clear(); });
|
|
29
|
-
this.proc.on('error', () => { });
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
onData(chunk) {
|
|
33
|
-
this.buf += chunk.toString('utf8');
|
|
34
|
-
const lines = this.buf.split('\n');
|
|
35
|
-
this.buf = lines.pop() || '';
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
if (!line.trim())
|
|
38
|
-
continue;
|
|
39
|
-
let m;
|
|
40
|
-
try {
|
|
41
|
-
m = JSON.parse(line);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (m.method && m.id != null) {
|
|
47
|
-
this.respond(m.id, {});
|
|
48
|
-
} // agent->client request: ack
|
|
49
|
-
else if (m.id != null) {
|
|
50
|
-
const cb = this.pending.get(m.id);
|
|
51
|
-
if (cb) {
|
|
52
|
-
this.pending.delete(m.id);
|
|
53
|
-
cb(m);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
else if (m.method)
|
|
57
|
-
this.notify?.(m.method, m.params ?? {});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
request(method, params, timeoutMs = 60_000) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
if (!this.proc || this.proc.killed) {
|
|
63
|
-
resolve({ error: { message: 'not connected' } });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const id = this.nextId++;
|
|
67
|
-
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `ACP '${method}' timed out` } }); }, timeoutMs);
|
|
68
|
-
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
69
|
-
try {
|
|
70
|
-
this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
this.pending.delete(id);
|
|
75
|
-
resolve({ error: { message: 'write failed' } });
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
respond(id, result) { try {
|
|
80
|
-
this.proc?.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
81
|
-
}
|
|
82
|
-
catch { /* closed */ } }
|
|
83
|
-
kill() { try {
|
|
84
|
-
this.proc?.kill('SIGTERM');
|
|
85
|
-
}
|
|
86
|
-
catch { /* ignore */ } this.proc = null; }
|
|
87
|
-
}
|
|
88
|
-
export class HermesDriver {
|
|
89
|
-
bin;
|
|
90
|
-
id = 'hermes';
|
|
91
|
-
capabilities = { steer: false, interact: false, resume: true, tui: false };
|
|
1
|
+
import { AcpDriver, applyAcpUpdate } from './acp.js';
|
|
2
|
+
// Hermes = the reference ACP agent, shipped as a thin preset over the generic AcpDriver.
|
|
3
|
+
// Any other ACP CLI (OpenCode, Gemini-ACP, …) is just `new AcpDriver({ id, command, args })`.
|
|
4
|
+
export class HermesDriver extends AcpDriver {
|
|
92
5
|
constructor(bin = 'hermes') {
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
async run(input, ctx) {
|
|
96
|
-
const client = new AcpClient(this.bin, ['acp'], input.env);
|
|
97
|
-
const s = { text: '', reasoning: '', sessionId: input.sessionId ?? null, contextWindow: null, contextUsed: null, error: null };
|
|
98
|
-
const tools = new Set();
|
|
99
|
-
if (!client.start())
|
|
100
|
-
return { ok: false, text: '', error: 'failed to start hermes acp', stopReason: 'error' };
|
|
101
|
-
const onAbort = () => { try {
|
|
102
|
-
client.request('session/cancel', { sessionId: s.sessionId });
|
|
103
|
-
}
|
|
104
|
-
catch { /* ignore */ } client.kill(); };
|
|
105
|
-
if (ctx.signal.aborted)
|
|
106
|
-
onAbort();
|
|
107
|
-
else
|
|
108
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
-
client.onNotification((method, params) => {
|
|
110
|
-
if (method !== 'session/update')
|
|
111
|
-
return;
|
|
112
|
-
applyHermesUpdate(params?.update ?? params, s, tools, ctx.emit);
|
|
113
|
-
});
|
|
114
|
-
try {
|
|
115
|
-
const init = await client.request('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } } });
|
|
116
|
-
if (init.error)
|
|
117
|
-
return { ok: false, text: '', error: init.error.message || 'initialize failed', stopReason: 'error' };
|
|
118
|
-
if (!s.sessionId) {
|
|
119
|
-
const ns = await client.request('session/new', { cwd: input.workdir, mcpServers: [] });
|
|
120
|
-
if (ns.error)
|
|
121
|
-
return { ok: false, text: '', error: ns.error.message || 'session/new failed', stopReason: 'error' };
|
|
122
|
-
s.sessionId = ns.result?.sessionId || ns.result?.session_id || null;
|
|
123
|
-
if (s.sessionId)
|
|
124
|
-
ctx.emit({ type: 'session', sessionId: s.sessionId });
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
await client.request('session/load', { sessionId: s.sessionId, cwd: input.workdir, mcpServers: [] }, 30_000).catch(() => ({}));
|
|
128
|
-
}
|
|
129
|
-
if (!s.sessionId)
|
|
130
|
-
return { ok: false, text: '', error: 'hermes returned no session id', stopReason: 'error' };
|
|
131
|
-
if (input.model)
|
|
132
|
-
await client.request('session/set_model', { sessionId: s.sessionId, modelId: input.model }, 15_000).catch(() => ({}));
|
|
133
|
-
const promptResp = await client.request('session/prompt', {
|
|
134
|
-
sessionId: s.sessionId,
|
|
135
|
-
prompt: [{ type: 'text', text: input.prompt }],
|
|
136
|
-
}, 7_200_000);
|
|
137
|
-
const usage = { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: s.contextUsed, contextPercent: null };
|
|
138
|
-
if (ctx.signal.aborted)
|
|
139
|
-
return { ok: false, text: s.text, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: s.sessionId, usage };
|
|
140
|
-
if (promptResp.error)
|
|
141
|
-
return { ok: false, text: s.text, error: promptResp.error.message || 'session/prompt failed', stopReason: 'error', sessionId: s.sessionId, usage };
|
|
142
|
-
const stopReason = promptResp.result?.stopReason ?? 'end_turn';
|
|
143
|
-
return { ok: !s.error, text: s.text, reasoning: s.reasoning || undefined, error: s.error, stopReason, sessionId: s.sessionId, usage };
|
|
144
|
-
}
|
|
145
|
-
finally {
|
|
146
|
-
client.kill();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
export function applyHermesUpdate(u, s, tools, emit) {
|
|
151
|
-
if (!u)
|
|
152
|
-
return;
|
|
153
|
-
switch (u.sessionUpdate) {
|
|
154
|
-
case 'agent_message_chunk': {
|
|
155
|
-
const t = u.content?.text;
|
|
156
|
-
if (typeof t === 'string' && t) {
|
|
157
|
-
s.text += t;
|
|
158
|
-
emit({ type: 'text', delta: t });
|
|
159
|
-
}
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
case 'agent_thought_chunk': {
|
|
163
|
-
const t = u.content?.text;
|
|
164
|
-
if (typeof t === 'string' && t) {
|
|
165
|
-
s.reasoning += t;
|
|
166
|
-
emit({ type: 'reasoning', delta: t });
|
|
167
|
-
}
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
case 'tool_call': {
|
|
171
|
-
const id = typeof u.toolCallId === 'string' ? u.toolCallId : '';
|
|
172
|
-
const title = (typeof u.title === 'string' && u.title.trim()) || 'tool';
|
|
173
|
-
if (id && !tools.has(id)) {
|
|
174
|
-
tools.add(id);
|
|
175
|
-
emit({ type: 'tool', call: { id, name: title, summary: title, status: 'running' } });
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
case 'tool_call_update': {
|
|
180
|
-
const id = typeof u.toolCallId === 'string' ? u.toolCallId : '';
|
|
181
|
-
const title = (typeof u.title === 'string' && u.title.trim()) || 'tool';
|
|
182
|
-
if (id && (u.status === 'completed' || u.status === 'failed'))
|
|
183
|
-
emit({ type: 'tool', call: { id, name: title, summary: title, status: u.status === 'failed' ? 'failed' : 'done' } });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
case 'usage_update': {
|
|
187
|
-
if (typeof u.used === 'number') {
|
|
188
|
-
s.contextUsed = u.used;
|
|
189
|
-
emit({ type: 'usage', usage: { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: u.used, contextPercent: null } });
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
6
|
+
super({ id: 'hermes', command: bin, args: ['acp'] });
|
|
193
7
|
}
|
|
194
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;
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { EchoDriver } from './echo.js';
|
|
|
2
2
|
export { ClaudeDriver, handleClaudeEvent, todoWriteToPlan } from './claude.js';
|
|
3
3
|
export { CodexDriver } from './codex.js';
|
|
4
4
|
export { GeminiDriver, parseGeminiEvent } from './gemini.js';
|
|
5
|
+
export { AcpDriver, applyAcpUpdate, toAcpMcpServers, buildAcpPromptBlocks, type AcpDriverConfig } from './acp.js';
|
|
5
6
|
export { HermesDriver, applyHermesUpdate } from './hermes.js';
|
package/dist/drivers/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { EchoDriver } from './echo.js';
|
|
|
2
2
|
export { ClaudeDriver, handleClaudeEvent, todoWriteToPlan } from './claude.js';
|
|
3
3
|
export { CodexDriver } from './codex.js';
|
|
4
4
|
export { GeminiDriver, parseGeminiEvent } from './gemini.js';
|
|
5
|
+
export { AcpDriver, applyAcpUpdate, toAcpMcpServers, buildAcpPromptBlocks } from './acp.js';
|
|
5
6
|
export { HermesDriver, applyHermesUpdate } from './hermes.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { SessionRunner } from './runtime/session-runner.js';
|
|
|
4
4
|
export { runTurn, type RunTurnOptions, type TurnOutcome } from './runtime/turn.js';
|
|
5
5
|
export { PtyBridge, ptyAvailable, type PtyExit, type PtyOpenOpts } from './runtime/pty.js';
|
|
6
6
|
export { attachTui, type AttachTuiOptions } from './runtime/tui.js';
|
|
7
|
-
export type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, SteerFn, McpServerSpec, TuiInput, TuiSpec, } from './contracts/driver.js';
|
|
7
|
+
export type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, SteerFn, McpServerSpec, TuiInput, TuiSpec, NativeSessionInfo, } from './contracts/driver.js';
|
|
8
8
|
export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, ToolProvider, SystemPromptBuilder, InteractionHandler, Catalog, } from './contracts/ports.js';
|
|
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';
|
|
@@ -12,7 +12,10 @@ export { EchoDriver } from './drivers/echo.js';
|
|
|
12
12
|
export { ClaudeDriver } from './drivers/claude.js';
|
|
13
13
|
export { CodexDriver } from './drivers/codex.js';
|
|
14
14
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
15
|
+
export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
|
|
15
16
|
export { HermesDriver } from './drivers/hermes.js';
|
|
16
17
|
export { WebSurface, type WebSurfaceOptions } from './surfaces/web.js';
|
|
17
18
|
export { CliSurface } from './surfaces/cli.js';
|
|
19
|
+
export * from './workspace/index.js';
|
|
20
|
+
export * from './accounts.js';
|
|
18
21
|
export * from './protocol/index.js';
|
package/dist/index.js
CHANGED
|
@@ -22,8 +22,13 @@ export { EchoDriver } from './drivers/echo.js';
|
|
|
22
22
|
export { ClaudeDriver } from './drivers/claude.js';
|
|
23
23
|
export { CodexDriver } from './drivers/codex.js';
|
|
24
24
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
|
+
export { AcpDriver } from './drivers/acp.js';
|
|
25
26
|
export { HermesDriver } from './drivers/hermes.js';
|
|
26
27
|
export { WebSurface } from './surfaces/web.js';
|
|
27
28
|
export { CliSurface } from './surfaces/cli.js';
|
|
29
|
+
// Workspace: unified top-level directory + session/skill/mcp management (loom.paths/sessions/skills/mcp)
|
|
30
|
+
export * from './workspace/index.js';
|
|
31
|
+
// Multi-account: per-account isolated config dirs (CLAUDE_CONFIG_DIR / CODEX_HOME)
|
|
32
|
+
export * from './accounts.js';
|
|
28
33
|
// Protocol (the wire vocabulary; shared with transports)
|
|
29
34
|
export * from './protocol/index.js';
|
package/dist/ports/defaults.js
CHANGED
|
@@ -25,11 +25,15 @@ export class FsSessionStore {
|
|
|
25
25
|
if (!existing) {
|
|
26
26
|
const now = new Date().toISOString();
|
|
27
27
|
await this.save({
|
|
28
|
-
agent, sessionId, workspacePath,
|
|
28
|
+
agent, sessionId, workspacePath, workdir: path.resolve(opts.workdir),
|
|
29
29
|
createdAt: now, updatedAt: now,
|
|
30
30
|
title: opts.title ?? null, runState: 'running', runDetail: null,
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
else if (!existing.workdir) {
|
|
34
|
+
existing.workdir = path.resolve(opts.workdir);
|
|
35
|
+
await this.save(existing);
|
|
36
|
+
}
|
|
33
37
|
return { sessionId, workspacePath };
|
|
34
38
|
}
|
|
35
39
|
async get(agent, sessionId) {
|
|
@@ -74,6 +78,8 @@ export class FsSessionStore {
|
|
|
74
78
|
rec.nativeSessionId = result.sessionId;
|
|
75
79
|
if (result.text && !rec.title)
|
|
76
80
|
rec.title = result.text.slice(0, 80);
|
|
81
|
+
if (result.text)
|
|
82
|
+
rec.preview = result.text.replace(/\s+/g, ' ').trim().slice(0, 200) || rec.preview || null;
|
|
77
83
|
await this.save(rec);
|
|
78
84
|
}
|
|
79
85
|
async appendTurn(agent, sessionId, turn) {
|
package/dist/runtime/loom.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { AgentDriver, TuiSpec } from '../contracts/driver.js';
|
|
|
2
2
|
import type { SessionStore, ModelResolver, ToolProvider, SystemPromptBuilder, Catalog, InteractionHandler } from '../contracts/ports.js';
|
|
3
3
|
import type { LoomIO, Surface, Plugin } from '../contracts/surface.js';
|
|
4
4
|
import { PtyBridge, type PtyOpenOpts, type PtyExit } from './pty.js';
|
|
5
|
+
import { type LoomPaths } from '../workspace/paths.js';
|
|
6
|
+
import { SessionsManager } from '../workspace/sessions.js';
|
|
7
|
+
import { SkillsManager } from '../workspace/skills.js';
|
|
8
|
+
import { McpRegistry } from '../workspace/mcp.js';
|
|
5
9
|
export interface TuiLaunchOptions {
|
|
6
10
|
agent?: string;
|
|
7
11
|
workdir?: string;
|
|
@@ -9,9 +13,11 @@ export interface TuiLaunchOptions {
|
|
|
9
13
|
sessionId?: string | null;
|
|
10
14
|
}
|
|
11
15
|
export interface LoomConfig {
|
|
16
|
+
stateDirName?: string;
|
|
12
17
|
appNamespace?: string;
|
|
13
18
|
workdir?: string;
|
|
14
19
|
defaultAgent?: string;
|
|
20
|
+
agentSkillDirs?: string[];
|
|
15
21
|
drivers?: AgentDriver[];
|
|
16
22
|
surfaces?: Surface[];
|
|
17
23
|
plugins?: Plugin[];
|
|
@@ -27,6 +33,10 @@ export interface LoomConfig {
|
|
|
27
33
|
}
|
|
28
34
|
export interface Loom {
|
|
29
35
|
readonly io: LoomIO;
|
|
36
|
+
readonly paths: LoomPaths;
|
|
37
|
+
readonly sessions: SessionsManager;
|
|
38
|
+
readonly skills: SkillsManager;
|
|
39
|
+
readonly mcp: McpRegistry;
|
|
30
40
|
registerDriver(driver: AgentDriver): void;
|
|
31
41
|
registerPlugin(plugin: Plugin): void;
|
|
32
42
|
start(): Promise<void>;
|
package/dist/runtime/loom.js
CHANGED
|
@@ -2,8 +2,15 @@ import { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemP
|
|
|
2
2
|
import { Hub } from './hub.js';
|
|
3
3
|
import { PtyBridge } from './pty.js';
|
|
4
4
|
import { attachTui } from './tui.js';
|
|
5
|
+
import { resolveLoomPaths } from '../workspace/paths.js';
|
|
6
|
+
import { SessionsManager } from '../workspace/sessions.js';
|
|
7
|
+
import { SkillsManager } from '../workspace/skills.js';
|
|
8
|
+
import { McpRegistry } from '../workspace/mcp.js';
|
|
5
9
|
export function createLoom(config = {}) {
|
|
6
|
-
|
|
10
|
+
// The top-level dir defaults to 'pikiloom'; appNamespace (the default session-store base)
|
|
11
|
+
// falls back to it so a single knob configures everything for a fresh consumer.
|
|
12
|
+
const stateDirName = config.stateDirName || config.appNamespace || 'pikiloom';
|
|
13
|
+
const appNamespace = config.appNamespace || stateDirName;
|
|
7
14
|
const workdir = config.workdir || process.cwd();
|
|
8
15
|
const log = config.log || (() => { });
|
|
9
16
|
const drivers = new Map();
|
|
@@ -12,11 +19,16 @@ export function createLoom(config = {}) {
|
|
|
12
19
|
const defaultAgent = config.defaultAgent || config.drivers?.[0]?.id || 'echo';
|
|
13
20
|
// Held as a live reference so registerPlugin() mutates the same array the Hub iterates.
|
|
14
21
|
const plugins = [...(config.plugins || [])];
|
|
22
|
+
const sessionStore = config.sessionStore || new FsSessionStore(defaultBaseDir(appNamespace));
|
|
23
|
+
const paths = resolveLoomPaths({ stateDirName });
|
|
24
|
+
const sessions = new SessionsManager({ store: sessionStore, drivers: () => drivers, defaultWorkdir: workdir, log });
|
|
25
|
+
const skills = new SkillsManager({ paths, agentSkillDirs: config.agentSkillDirs, log });
|
|
26
|
+
const mcp = new McpRegistry({ log });
|
|
15
27
|
const hub = new Hub({
|
|
16
28
|
drivers,
|
|
17
29
|
defaultAgent,
|
|
18
30
|
workdir,
|
|
19
|
-
sessionStore
|
|
31
|
+
sessionStore,
|
|
20
32
|
modelResolver: config.modelResolver || new NullModelResolver(),
|
|
21
33
|
toolProvider: config.toolProvider || new NoopToolProvider(),
|
|
22
34
|
systemPromptBuilder: config.systemPromptBuilder || new PassthroughSystemPromptBuilder(),
|
|
@@ -37,6 +49,10 @@ export function createLoom(config = {}) {
|
|
|
37
49
|
};
|
|
38
50
|
return {
|
|
39
51
|
io: hub,
|
|
52
|
+
paths,
|
|
53
|
+
sessions,
|
|
54
|
+
skills,
|
|
55
|
+
mcp,
|
|
40
56
|
registerDriver(driver) { drivers.set(driver.id, driver); },
|
|
41
57
|
registerPlugin(plugin) { plugins.push(plugin); },
|
|
42
58
|
async start() {
|
|
@@ -0,0 +1,5 @@
|
|
|
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
|
+
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';
|
|
5
|
+
export { McpRegistry, type McpCatalogEntry, type McpSearchResult, type McpRegistryOptions, } from './mcp.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Workspace subsystem: the unified top-level directory + session/skill/mcp management that
|
|
2
|
+
// a consuming app gets "for free" off createLoom() (exposed as loom.paths/sessions/skills/mcp).
|
|
3
|
+
export { resolveLoomPaths, normalizeStateDirName } from './paths.js';
|
|
4
|
+
export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, NATIVE_SESSION_RUNNING_THRESHOLD_MS, } from './native.js';
|
|
5
|
+
export { SessionsManager, } from './sessions.js';
|
|
6
|
+
export { SkillsManager, ensureDirSymlink, } from './skills.js';
|
|
7
|
+
export { McpRegistry, } from './mcp.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { McpServerSpec } from '../contracts/driver.js';
|
|
2
|
+
export interface McpCatalogEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
category?: string;
|
|
7
|
+
brand?: string;
|
|
8
|
+
transport: {
|
|
9
|
+
type: 'stdio';
|
|
10
|
+
command: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
} | {
|
|
13
|
+
type: 'http';
|
|
14
|
+
url: string;
|
|
15
|
+
};
|
|
16
|
+
/** Required credential env var names (so a UI can prompt for them). */
|
|
17
|
+
envKeys?: string[];
|
|
18
|
+
homepage?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface McpSearchResult {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string | null;
|
|
23
|
+
source: 'registry' | 'npm';
|
|
24
|
+
npmPackage?: string | null;
|
|
25
|
+
homepage?: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface McpRegistryOptions {
|
|
28
|
+
/** Replace/extend the built-in recommended catalog. */
|
|
29
|
+
recommended?: McpCatalogEntry[];
|
|
30
|
+
fetchImpl?: typeof fetch;
|
|
31
|
+
log?: (msg: string) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare class McpRegistry {
|
|
34
|
+
private readonly _recommended;
|
|
35
|
+
private readonly fetchImpl;
|
|
36
|
+
private readonly log?;
|
|
37
|
+
constructor(opts?: McpRegistryOptions);
|
|
38
|
+
/** The curated catalog of well-known servers. */
|
|
39
|
+
recommended(): McpCatalogEntry[];
|
|
40
|
+
/** Turn a catalog entry into a kernel McpServerSpec ready to hand to a driver/plugin. */
|
|
41
|
+
toServerSpec(entry: McpCatalogEntry, env?: Record<string, string>): McpServerSpec;
|
|
42
|
+
/** Search the public MCP registry, falling back to npm. Best-effort; [] on failure. */
|
|
43
|
+
search(query: string, limit?: number): Promise<McpSearchResult[]>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const BUILTIN_RECOMMENDED = [
|
|
2
|
+
{ 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
|
+
{ 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' },
|
|
4
|
+
{ id: 'fetch', name: 'Fetch', description: 'Fetch and convert web pages to markdown.', category: 'web', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-fetch'] }, homepage: 'https://github.com/modelcontextprotocol/servers' },
|
|
5
|
+
{ id: 'git', name: 'Git', description: 'Local git repository operations.', category: 'dev', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-git'] }, homepage: 'https://github.com/modelcontextprotocol/servers' },
|
|
6
|
+
{ id: 'memory', name: 'Memory', description: 'A knowledge-graph memory store.', category: 'core', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-memory'] }, homepage: 'https://github.com/modelcontextprotocol/servers' },
|
|
7
|
+
{ id: 'playwright', name: 'Playwright', description: 'Drive a real browser for automation.', category: 'web', brand: 'playwright', transport: { type: 'stdio', command: 'npx', args: ['-y', '@playwright/mcp@latest'] }, homepage: 'https://github.com/microsoft/playwright-mcp' },
|
|
8
|
+
{ id: 'context7', name: 'Context7', description: 'Up-to-date library docs & code examples.', category: 'docs', transport: { type: 'http', url: 'https://mcp.context7.com/mcp' }, homepage: 'https://context7.com' },
|
|
9
|
+
];
|
|
10
|
+
export class McpRegistry {
|
|
11
|
+
_recommended;
|
|
12
|
+
fetchImpl;
|
|
13
|
+
log;
|
|
14
|
+
constructor(opts = {}) {
|
|
15
|
+
this._recommended = opts.recommended?.length ? opts.recommended : BUILTIN_RECOMMENDED;
|
|
16
|
+
this.fetchImpl = opts.fetchImpl ?? (typeof fetch === 'function' ? fetch : undefined);
|
|
17
|
+
this.log = opts.log;
|
|
18
|
+
}
|
|
19
|
+
/** The curated catalog of well-known servers. */
|
|
20
|
+
recommended() {
|
|
21
|
+
return this._recommended.map(e => ({ ...e }));
|
|
22
|
+
}
|
|
23
|
+
/** Turn a catalog entry into a kernel McpServerSpec ready to hand to a driver/plugin. */
|
|
24
|
+
toServerSpec(entry, env) {
|
|
25
|
+
if (entry.transport.type === 'http') {
|
|
26
|
+
return { name: entry.id, type: 'http', url: entry.transport.url };
|
|
27
|
+
}
|
|
28
|
+
return { name: entry.id, type: 'stdio', command: entry.transport.command, args: entry.transport.args, env };
|
|
29
|
+
}
|
|
30
|
+
/** Search the public MCP registry, falling back to npm. Best-effort; [] on failure. */
|
|
31
|
+
async search(query, limit = 20) {
|
|
32
|
+
const q = (query || '').trim();
|
|
33
|
+
const n = Math.max(1, Math.min(50, limit));
|
|
34
|
+
const fetchImpl = this.fetchImpl;
|
|
35
|
+
if (!fetchImpl)
|
|
36
|
+
return [];
|
|
37
|
+
// 1) Official MCP registry.
|
|
38
|
+
try {
|
|
39
|
+
const url = `https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(q)}&limit=${n}`;
|
|
40
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
const servers = Array.isArray(data?.servers) ? data.servers : Array.isArray(data?.data) ? data.data : [];
|
|
44
|
+
const mapped = servers.map((s) => ({
|
|
45
|
+
name: String(s?.name ?? s?.id ?? ''),
|
|
46
|
+
description: s?.description ?? null,
|
|
47
|
+
source: 'registry',
|
|
48
|
+
npmPackage: s?.packages?.[0]?.identifier ?? s?.npm ?? null,
|
|
49
|
+
homepage: s?.repository?.url ?? s?.homepage ?? null,
|
|
50
|
+
})).filter(s => s.name);
|
|
51
|
+
if (mapped.length)
|
|
52
|
+
return mapped.slice(0, n);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
this.log?.(`[mcp] registry search failed: ${e?.message || e}`);
|
|
57
|
+
}
|
|
58
|
+
// 2) npm fallback.
|
|
59
|
+
try {
|
|
60
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(`mcp server ${q}`.trim())}&size=${n}`;
|
|
61
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
return [];
|
|
64
|
+
const data = await res.json();
|
|
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);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
this.log?.(`[mcp] npm search failed: ${e?.message || e}`);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|