pikiloom 0.4.37 → 0.4.38

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.
Files changed (48) hide show
  1. package/dist/agent/kernel-bridge.js +206 -0
  2. package/dist/agent/stream.js +19 -1
  3. package/dist/cli/kernel-app.js +115 -0
  4. package/dist/cli/main.js +7 -0
  5. package/package.json +4 -2
  6. package/packages/kernel/README.md +107 -0
  7. package/packages/kernel/dist/contracts/driver.d.ts +95 -0
  8. package/packages/kernel/dist/contracts/driver.js +1 -0
  9. package/packages/kernel/dist/contracts/ports.d.ts +84 -0
  10. package/packages/kernel/dist/contracts/ports.js +1 -0
  11. package/packages/kernel/dist/contracts/surface.d.ts +75 -0
  12. package/packages/kernel/dist/contracts/surface.js +1 -0
  13. package/packages/kernel/dist/drivers/claude.d.ts +23 -0
  14. package/packages/kernel/dist/drivers/claude.js +453 -0
  15. package/packages/kernel/dist/drivers/codex.d.ts +14 -0
  16. package/packages/kernel/dist/drivers/codex.js +346 -0
  17. package/packages/kernel/dist/drivers/echo.d.ts +20 -0
  18. package/packages/kernel/dist/drivers/echo.js +61 -0
  19. package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
  20. package/packages/kernel/dist/drivers/gemini.js +143 -0
  21. package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
  22. package/packages/kernel/dist/drivers/hermes.js +194 -0
  23. package/packages/kernel/dist/drivers/index.d.ts +5 -0
  24. package/packages/kernel/dist/drivers/index.js +5 -0
  25. package/packages/kernel/dist/index.d.ts +18 -0
  26. package/packages/kernel/dist/index.js +29 -0
  27. package/packages/kernel/dist/ports/defaults.d.ts +59 -0
  28. package/packages/kernel/dist/ports/defaults.js +137 -0
  29. package/packages/kernel/dist/protocol/index.d.ts +309 -0
  30. package/packages/kernel/dist/protocol/index.js +59 -0
  31. package/packages/kernel/dist/runtime/hub.d.ts +65 -0
  32. package/packages/kernel/dist/runtime/hub.js +260 -0
  33. package/packages/kernel/dist/runtime/loom.d.ts +44 -0
  34. package/packages/kernel/dist/runtime/loom.js +69 -0
  35. package/packages/kernel/dist/runtime/pty.d.ts +23 -0
  36. package/packages/kernel/dist/runtime/pty.js +69 -0
  37. package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
  38. package/packages/kernel/dist/runtime/session-runner.js +210 -0
  39. package/packages/kernel/dist/runtime/tui.d.ts +8 -0
  40. package/packages/kernel/dist/runtime/tui.js +35 -0
  41. package/packages/kernel/dist/runtime/turn.d.ts +17 -0
  42. package/packages/kernel/dist/runtime/turn.js +16 -0
  43. package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
  44. package/packages/kernel/dist/surfaces/cli.js +65 -0
  45. package/packages/kernel/dist/surfaces/index.d.ts +2 -0
  46. package/packages/kernel/dist/surfaces/index.js +2 -0
  47. package/packages/kernel/dist/surfaces/web.d.ts +39 -0
  48. package/packages/kernel/dist/surfaces/web.js +244 -0
@@ -0,0 +1,206 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { agentLog, agentWarn } from './utils.js';
6
+ import { normalizeClaudeSessionEntrypoint } from './drivers/claude.js';
7
+ import { humanizeCodexError } from './drivers/codex.js';
8
+ // ── The cutover seam: route an agent turn through @pikiloom/kernel ──────────────
9
+ //
10
+ // DEFAULT: ON — claude/codex/gemini/hermes turns run on the kernel drivers (via runTurn).
11
+ // Escape hatches to the legacy driver path:
12
+ // LOOM_KERNEL_PIPELINE=0 (env; force legacy at startup; survives dev.sh scrub)
13
+ // ~/.pikiloom/dev/kernel-legacy.on (file; hot-toggle legacy without restart)
14
+ // LOOM_KERNEL_PIPELINE=1 (env; force kernel, overriding the file)
15
+ // Tests always run legacy (the unit suite asserts legacy driver behavior). The bridge
16
+ // re-applies app-level parity the pure kernel must not own (claude jsonl entrypoint, codex humanize).
17
+ const KERNEL_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
18
+ export function shouldUseKernelPipeline(agent) {
19
+ if (!KERNEL_AGENTS.has(agent))
20
+ return false;
21
+ if (process.env.VITEST || process.env.NODE_ENV === 'test')
22
+ return false; // tests assert legacy
23
+ if (process.env.LOOM_KERNEL_PIPELINE === '0')
24
+ return false; // explicit legacy
25
+ if (process.env.LOOM_KERNEL_PIPELINE === '1')
26
+ return true; // explicit kernel
27
+ try {
28
+ if (fs.existsSync(path.join(os.homedir(), '.pikiloom', 'dev', 'kernel-legacy.on')))
29
+ return false;
30
+ }
31
+ catch { /* ignore */ }
32
+ return true; // default: kernel
33
+ }
34
+ // Build the kernel driver instance + the per-agent AgentTurnInput from pikiloom StreamOpts.
35
+ function buildKernelDriver(kernel, opts) {
36
+ const common = {
37
+ prompt: opts.prompt,
38
+ workdir: opts.workdir,
39
+ sessionId: opts.sessionId ?? null,
40
+ attachments: opts.attachments,
41
+ effort: opts.thinkingEffort,
42
+ env: opts.extraEnv,
43
+ mcpConfigPath: opts.mcpConfigPath ?? null,
44
+ };
45
+ switch (opts.agent) {
46
+ case 'codex': {
47
+ // codexExtraArgs is a flattened ['-c','k=v','-c','k=v',...]; extract the k=v values
48
+ // so the kernel keeps BYOK provider routing.
49
+ const ce = opts.codexExtraArgs || [];
50
+ const configOverrides = [];
51
+ for (let i = 0; i < ce.length; i++)
52
+ if (ce[i] === '-c' && ce[i + 1])
53
+ configOverrides.push(ce[++i]);
54
+ return { driver: new kernel.CodexDriver(), input: { ...common, model: opts.codexModel ?? opts.model ?? null, systemPrompt: opts.codexDeveloperInstructions, configOverrides, fullAccess: opts.codexFullAccess } };
55
+ }
56
+ case 'gemini':
57
+ return { driver: new kernel.GeminiDriver(), input: { ...common, model: opts.geminiModel ?? opts.model ?? null, systemPrompt: opts.geminiSystemInstruction, extraArgs: opts.geminiExtraArgs } };
58
+ case 'hermes':
59
+ return { driver: new kernel.HermesDriver(), input: { ...common, model: opts.hermesModel ?? opts.model ?? null } };
60
+ case 'claude':
61
+ default:
62
+ return {
63
+ driver: new kernel.ClaudeDriver(),
64
+ input: { ...common, model: opts.claudeModel ?? opts.model ?? null, systemPrompt: opts.claudeAppendSystemPrompt, permissionMode: opts.claudePermissionMode ?? null, extraArgs: opts.claudeExtraArgs, steerable: !!opts.onSteerReady },
65
+ };
66
+ }
67
+ }
68
+ let _kernel = null;
69
+ export async function loadKernel() {
70
+ if (_kernel)
71
+ return _kernel;
72
+ const here = path.dirname(fileURLToPath(import.meta.url));
73
+ const candidates = [
74
+ path.resolve(here, '../../packages/kernel/dist/index.js'), // src/agent (dev) & dist/agent (prod) -> <repo>/packages/kernel
75
+ path.resolve(process.cwd(), 'packages/kernel/dist/index.js'),
76
+ ];
77
+ // Prefer an installed package if present, else the in-repo built dist.
78
+ // (specifier kept in a variable so tsc treats it as a dynamic any, not a static resolve)
79
+ const pkgName = '@pikiloom/kernel';
80
+ try {
81
+ _kernel = await import(pkgName);
82
+ return _kernel;
83
+ }
84
+ catch { /* not installed */ }
85
+ for (const c of candidates) {
86
+ if (fs.existsSync(c)) {
87
+ _kernel = await import(pathToFileURL(c).href);
88
+ return _kernel;
89
+ }
90
+ }
91
+ throw new Error(`@pikiloom/kernel not found (build it: npm run build in packages/kernel). Looked in: ${candidates.join(', ')}`);
92
+ }
93
+ // A kernel 'artifact' (e.g. a generated image as a data URL) -> write to a temp file and
94
+ // deliver through pikiloom's normal file-delivery seam (same path as im_send_file).
95
+ async function deliverKernelArtifact(artifact, opts) {
96
+ try {
97
+ const send = opts.mcpSendFile;
98
+ if (typeof send !== 'function' || !artifact?.url)
99
+ return;
100
+ const m = String(artifact.url).match(/^data:([^;]+);base64,(.*)$/s);
101
+ if (!m)
102
+ return;
103
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kernel-art-'));
104
+ const file = path.join(dir, artifact.fileName || 'artifact.bin');
105
+ fs.writeFileSync(file, Buffer.from(m[2], 'base64'));
106
+ await send(file, { kind: artifact.kind || 'document', caption: artifact.caption });
107
+ }
108
+ catch { /* best-effort delivery */ }
109
+ }
110
+ export async function kernelStream(opts) {
111
+ const start = Date.now();
112
+ const kernel = await loadKernel();
113
+ const { driver, input } = buildKernelDriver(kernel, opts);
114
+ agentLog(`[kernel-bridge] routing ${opts.agent} turn via @pikiloom/kernel ${driver?.constructor?.name || 'Driver'}`);
115
+ let delivered = 0; // artifacts already sent out-of-band
116
+ let seenSid = null;
117
+ // SessionRunner (via runTurn) owns the event accumulation; the bridge only translates
118
+ // the kernel's UniversalSnapshot onto pikiloom's preview (onText) and delivers any new
119
+ // artifacts through the normal file seam. This is the "product bridge" pattern: map your
120
+ // request -> AgentTurnInput, map snapshot -> your UI, map result -> your result shape.
121
+ const onSnapshot = (s) => {
122
+ const arts = s.artifacts || [];
123
+ for (; delivered < arts.length; delivered++)
124
+ void deliverKernelArtifact(arts[delivered], opts);
125
+ if (s.sessionId && s.sessionId !== seenSid) {
126
+ seenSid = s.sessionId;
127
+ try {
128
+ opts.onSessionId?.(s.sessionId);
129
+ }
130
+ catch { /* ignore */ }
131
+ }
132
+ const m = {
133
+ inputTokens: s.usage?.inputTokens ?? null,
134
+ outputTokens: s.usage?.outputTokens ?? null,
135
+ cachedInputTokens: s.usage?.cachedInputTokens ?? null,
136
+ contextUsedTokens: s.usage?.contextUsedTokens ?? null,
137
+ contextPercent: s.usage?.contextPercent ?? null,
138
+ toolCalls: s.toolCalls?.length ? s.toolCalls : undefined,
139
+ subAgents: s.subAgents?.length ? s.subAgents : undefined,
140
+ providerName: opts.byokProviderName ?? null,
141
+ };
142
+ try {
143
+ opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, s.plan ?? null);
144
+ }
145
+ catch { /* isolate */ }
146
+ };
147
+ let result;
148
+ let snapshot = {};
149
+ try {
150
+ ({ result, snapshot } = await kernel.runTurn(driver, input, {
151
+ onSnapshot,
152
+ onSteer: (fn) => { try {
153
+ opts.onSteerReady?.(fn);
154
+ }
155
+ catch { /* ignore */ } },
156
+ signal: opts.abortSignal,
157
+ }));
158
+ }
159
+ catch (err) {
160
+ agentWarn(`[kernel-bridge] kernel run failed: ${err?.message || err}`);
161
+ result = { ok: false, text: '', error: err?.message || String(err), stopReason: 'error', sessionId: opts.sessionId ?? null };
162
+ }
163
+ const finalSessionId = result.sessionId || snapshot.sessionId || opts.sessionId || null;
164
+ if (finalSessionId && finalSessionId !== seenSid) {
165
+ try {
166
+ opts.onSessionId?.(finalSessionId);
167
+ }
168
+ catch { /* ignore */ }
169
+ }
170
+ // App-level parity the pure kernel must not own (mirrors legacy *.doStream):
171
+ // - claude: rewrite the native jsonl entrypoint sdk-cli->cli (VSCode session visibility).
172
+ // - codex: translate raw provider error JSON (ChatGPT-account / third-party) to prose.
173
+ if (opts.agent === 'claude') {
174
+ try {
175
+ normalizeClaudeSessionEntrypoint(opts.workdir, finalSessionId);
176
+ }
177
+ catch { /* best-effort */ }
178
+ }
179
+ const finalError = (opts.agent === 'codex' && result.error)
180
+ ? (humanizeCodexError(result.error) ?? result.error)
181
+ : (result.error ?? null);
182
+ const u = result.usage || snapshot.usage || {};
183
+ return {
184
+ ok: !!result.ok,
185
+ message: (result.text || snapshot.text || '').trim() || (finalError ?? '(no output)'),
186
+ thinking: (result.reasoning || snapshot.reasoning || '').trim() || null,
187
+ plan: snapshot.plan ?? null,
188
+ sessionId: finalSessionId,
189
+ workspacePath: null,
190
+ model: input.model ?? null,
191
+ thinkingEffort: opts.thinkingEffort,
192
+ elapsedS: (Date.now() - start) / 1000,
193
+ inputTokens: u.inputTokens ?? null,
194
+ outputTokens: u.outputTokens ?? null,
195
+ cachedInputTokens: u.cachedInputTokens ?? null,
196
+ cacheCreationInputTokens: null,
197
+ contextWindow: null,
198
+ contextUsedTokens: u.contextUsedTokens ?? null,
199
+ contextPercent: null,
200
+ codexCumulative: null,
201
+ error: finalError,
202
+ stopReason: result.stopReason ?? null,
203
+ incomplete: !result.ok,
204
+ activity: snapshot.activity || null,
205
+ };
206
+ }
@@ -12,6 +12,7 @@ import { Q, agentLog, agentWarn, agentError, joinErrorMessages, normalizeErrorMe
12
12
  import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSessionWorkspace, importFilesIntoWorkspace, syncManagedSessionIdentity, summarizePromptTitle, recordFork, resolveCanonicalSessionId, } from './session.js';
13
13
  import { clearAwaitResume } from './await-resume.js';
14
14
  import { collapseSkillPrompt } from './skills.js';
15
+ import { shouldUseKernelPipeline, kernelStream } from './kernel-bridge.js';
15
16
  function trimSessionText(value, max = 24_000) {
16
17
  const text = typeof value === 'string' ? value.trim() : '';
17
18
  if (!text)
@@ -558,7 +559,24 @@ export async function doStream(opts) {
558
559
  throw new Error(`Agent ${prepared.agent} does not support fork`);
559
560
  }
560
561
  await awaitAgentUpdateIdle(prepared.agent, AGENT_UPDATE_TIMEOUTS.spawnWait);
561
- const result = await driver.doStream(prepared);
562
+ // Cutover seam: when enabled (default ON for claude/codex/gemini/hermes), route the
563
+ // agent turn through @pikiloom/kernel. kernelStream throws ONLY if the kernel package
564
+ // can't be loaded (missing dist) — every in-turn error is returned as a result. So a
565
+ // throw here means "kernel unavailable": fall back to the legacy driver rather than
566
+ // failing the turn. See agent/kernel-bridge.ts.
567
+ let result;
568
+ if (shouldUseKernelPipeline(prepared.agent)) {
569
+ try {
570
+ result = await kernelStream(prepared);
571
+ }
572
+ catch (kernelErr) {
573
+ agentWarn(`[kernel-bridge] kernel unavailable, falling back to legacy driver: ${kernelErr?.message || kernelErr}`);
574
+ result = await driver.doStream(prepared);
575
+ }
576
+ }
577
+ else {
578
+ result = await driver.doStream(prepared);
579
+ }
562
580
  const finalized = finalizeStreamResult(result, opts.workdir, opts.prompt, session, opts.claudeWorkflowEnabled, opts.profileId);
563
581
  finalized.byokProviderName = prepared.byokProviderName ?? null;
564
582
  finalized.byokProfileName = prepared.byokProfileName ?? null;
@@ -0,0 +1,115 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadKernel } from '../agent/kernel-bridge.js';
6
+ import { loadUserConfig, applyUserConfig } from '../core/config/user-config.js';
7
+ import { resolveAgentInjection } from '../model/injector.js';
8
+ // ── The "new version": pikiloom's backend booted ON @pikiloom/kernel ────────────
9
+ //
10
+ // Gated by LOOM_KERNEL_APP=1 (non-PIKILOOM_ prefix so it survives dev.sh's env scrub).
11
+ // LOOM_KERNEL_APP=1 npm run dev -> this kernel runtime on the dashboard port
12
+ // npm run dev -> the legacy app (old version), untouched
13
+ //
14
+ // One createLoom() drives every agent through the kernel's Driver registry and exposes it
15
+ // over a WebSurface carrying both lanes on one ws host: Lane S (web/structured snapshot) and
16
+ // Lane R (raw PTY / terminal). The whole C1-C5 SDK surface (history, catalog, HITL, per-session
17
+ // queue, wire) is live here.
18
+ const DEMO_MODELS = {
19
+ claude: [{ id: 'claude-opus-4-8', label: 'Opus 4.8', providerName: 'anthropic', contextWindow: 200000 }],
20
+ codex: [{ id: 'gpt-5.5-codex', label: 'GPT-5.5 Codex', providerName: 'openai', contextWindow: 400000 }],
21
+ gemini: [{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', providerName: 'google', contextWindow: 1000000 }],
22
+ echo: [{ id: 'echo-1', label: 'Echo (hermetic)', providerName: 'local', contextWindow: 8192 }],
23
+ };
24
+ export async function runKernelApp(argv) {
25
+ const i = argv.indexOf('--dashboard-port');
26
+ const port = (i >= 0 && parseInt(argv[i + 1], 10)) || 3940;
27
+ const k = await loadKernel();
28
+ const { createLoom, ClaudeDriver, CodexDriver, GeminiDriver, HermesDriver, EchoDriver, WebSurface } = k;
29
+ // Load providers/profiles so the active BYOK/豆包 bindings are visible to the resolver.
30
+ try {
31
+ applyUserConfig(loadUserConfig(), undefined, { overwrite: true, clearMissing: true });
32
+ }
33
+ catch (e) {
34
+ console.log(`[kernel-app] config load: ${e?.message || e}`);
35
+ }
36
+ // REAL ModelResolver: delegate to pikiloom's resolveAgentInjection (BYOK / 豆包 / native).
37
+ // null => native CLI login; else map the InjectedSpawnConfig onto the kernel's ModelInjection.
38
+ const modelResolver = {
39
+ async resolve(agent, opts) {
40
+ let inj = null;
41
+ try {
42
+ inj = await resolveAgentInjection(agent, opts.profileId ?? undefined);
43
+ }
44
+ catch (e) {
45
+ console.log(`[kernel-app] inject ${agent} failed: ${e?.message || e}`);
46
+ return null;
47
+ }
48
+ if (!inj)
49
+ return null; // no active profile → native login
50
+ const env = { ...(inj.env || {}) };
51
+ if (inj.homeOverride)
52
+ env.HOME = inj.homeOverride;
53
+ if (inj.configFiles && Object.keys(inj.configFiles).length)
54
+ console.log(`[kernel-app] note: ${agent} injection has configFiles (not yet threaded through kernel)`);
55
+ return {
56
+ model: inj.modelOverride ?? opts.model ?? null,
57
+ env,
58
+ extraArgs: inj.argvAppend?.length ? inj.argvAppend : undefined,
59
+ configOverrides: inj.codexConfigOverrides?.length ? inj.codexConfigOverrides : undefined,
60
+ providerName: inj.providerName ?? null,
61
+ contextWindow: inj.contextWindow ?? null,
62
+ };
63
+ },
64
+ };
65
+ // Discovery (C2). A full cutover wires pikiloom's runtime-config / catalog / mcp SSOT
66
+ // through this port; here a representative catalog proves discovery end to end.
67
+ const catalog = {
68
+ async listModels({ agent }) { return DEMO_MODELS[agent] || []; },
69
+ async listEffort() { return [{ id: 'low' }, { id: 'medium' }, { id: 'high' }]; },
70
+ async listTools() { return []; },
71
+ async listSkills() { return []; },
72
+ };
73
+ const here = path.dirname(fileURLToPath(import.meta.url));
74
+ let html = '<!doctype html><meta charset=utf-8><title>pikiloom · kernel</title>'
75
+ + '<body style="font:14px system-ui;padding:2rem;max-width:42rem">'
76
+ + '<h1>pikiloom — kernel runtime (new version)</h1>'
77
+ + '<p>The WebSurface ws host is live on this port. Connect any pikichannel/ws client and speak the UniversalSnapshot wire protocol (hello → subscribe → prompt; getHistory / getCatalog).</p>';
78
+ for (const c of [
79
+ path.resolve(here, '../../packages/kernel/examples/console.html'),
80
+ path.resolve(process.cwd(), 'packages/kernel/examples/console.html'),
81
+ ]) {
82
+ try {
83
+ html = fs.readFileSync(c, 'utf8');
84
+ break;
85
+ }
86
+ catch { /* fallback inline */ }
87
+ }
88
+ const server = http.createServer((req, res) => {
89
+ if (req.method === 'GET' && (req.url === '/' || req.url?.startsWith('/?'))) {
90
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
91
+ res.end(html);
92
+ return;
93
+ }
94
+ res.writeHead(404);
95
+ res.end('not found');
96
+ });
97
+ // WebSurface serves BOTH lanes: Lane S (Web/structured snapshot) and Lane R (Surface/
98
+ // raw PTY, via the TuiHost the kernel hands it at start). One ws host, two lanes.
99
+ const web = new WebSurface({ server, name: 'pikiloom-kernel' });
100
+ const surfaces = [web];
101
+ const loom = createLoom({
102
+ appNamespace: 'pikiloom-kernel',
103
+ drivers: [new EchoDriver(), new ClaudeDriver(), new CodexDriver(), new GeminiDriver(), new HermesDriver()],
104
+ defaultAgent: 'claude',
105
+ surfaces,
106
+ catalog,
107
+ modelResolver,
108
+ log: (m) => console.log(`[kernel-app] ${m}`),
109
+ });
110
+ await loom.start();
111
+ await new Promise((resolve) => server.listen(port, () => resolve()));
112
+ const st = loom.status();
113
+ console.log(`[kernel-app] NEW VERSION up — http+ws http://localhost:${port} agents=${st.agents.join(',')} surfaces=${st.surfaces.join(',')}`);
114
+ // The http+ws server keeps the process alive.
115
+ }
package/dist/cli/main.js CHANGED
@@ -590,6 +590,13 @@ async function launchChannels(channels, dashboard) {
590
590
  export async function main() {
591
591
  if (await handleMcpServeMode())
592
592
  return;
593
+ // Cutover gate: LOOM_KERNEL_APP=1 boots the backend on @pikiloom/kernel (new version)
594
+ // instead of the legacy app. Non-PIKILOOM_ prefix so it survives dev.sh's env scrub.
595
+ if (process.env.LOOM_KERNEL_APP === '1') {
596
+ const { runKernelApp } = await import('./kernel-app.js');
597
+ await runKernelApp(process.argv.slice(2));
598
+ return;
599
+ }
593
600
  const args = parseArgs(process.argv.slice(2));
594
601
  let userConfig = loadUserConfig();
595
602
  if (args.version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist/",
11
11
  "dashboard/dist/",
12
+ "packages/kernel/dist/",
12
13
  "LICENSE",
13
14
  "README.md"
14
15
  ],
@@ -33,7 +34,8 @@
33
34
  "build:dashboard": "vite build --config dashboard/vite.config.ts",
34
35
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
35
36
  "copy:pikichannel-web": "node -e \"require('fs').cpSync('src/pikichannel/web','dist/pikichannel/web',{recursive:true})\"",
36
- "build": "npm run clean && npm run build:dashboard && tsc && npm run copy:pikichannel-web",
37
+ "build:kernel": "tsc -p packages/kernel/tsconfig.json",
38
+ "build": "npm run clean && npm run build:dashboard && tsc && npm run build:kernel && npm run copy:pikichannel-web",
37
39
  "postbuild": "node -e \"require('fs').chmodSync('dist/cli/main.js',0o755)\"",
38
40
  "prepack": "npm run build",
39
41
  "test": "vitest run",
@@ -0,0 +1,107 @@
1
+ # @pikiloom/kernel
2
+
3
+ Heterogeneous coding agents (Claude / Codex / Gemini / ACP) → an **interaction-friendly,
4
+ accumulating session snapshot + control handle**, exposed over **pluggable surfaces**
5
+ (IM, Web, tunnel). Environmental concerns are **injected ports** with working defaults.
6
+
7
+ This is the reusable core **pikiloom itself is meant to be built on**. Drop it into any
8
+ project and stand up a "pikiloom-like" backend in a few lines.
9
+
10
+ ## The model (one package, three rings)
11
+
12
+ ```
13
+ 上层 (you write): IM bindings · Web UI/skin · Plugins ── implement Surface / Plugin
14
+ ▲ createLoom({ surfaces, plugins, ...ports })
15
+ @pikiloom/kernel: terminal/ Surface contract + LoomIO + built-in WebSurface
16
+ (one import, runtime/ SessionRunner · Hub (multi-session) · snapshot accumulation · control verbs
17
+ internally agent/ driver registry + Claude/Echo drivers + spawn/parse ← Driver
18
+ modular) protocol/ UniversalSnapshot + diff (the wire vocabulary) ← Channel
19
+ ports/ SessionStore · ModelResolver · ToolProvider · ... (+ defaults)
20
+ ▲ spawns external CLIs (unchanged)
21
+ 下层 (unchanged): the `claude` / `codex` / … binaries, native protocols
22
+ ```
23
+
24
+ **IM and Web are not two systems** — they are both just `Surface`s over the same `LoomIO`.
25
+
26
+ ## Quickstart
27
+
28
+ ```ts
29
+ import { createLoom, ClaudeDriver, WebSurface } from '@pikiloom/kernel';
30
+
31
+ const loom = createLoom({
32
+ drivers: [new ClaudeDriver()], // 下层 (unchanged)
33
+ surfaces: [new WebSurface({ port: 8787 })], // 上层 (IM / Web / tunnel)
34
+ });
35
+ await loom.start();
36
+ ```
37
+
38
+ Add an IM terminal by implementing `Surface.start(io)`: route inbound messages to
39
+ `io.prompt(...)` and render `io.subscribe(...)` snapshots back. Everything else (storage,
40
+ credentials, tools, prompts) has a default and is overridable via ports.
41
+
42
+ ### TUI passthrough (`pikiloom code` → Claude/Codex TUI)
43
+
44
+ Two orthogonal driver outputs off the same registry: `run()` yields a structured
45
+ `UniversalSnapshot` (Web/IM); `tui()` yields a raw, full-screen interactive process that
46
+ the kernel spawns in a **PTY** and passes through transparently (with optional tee/mirror).
47
+
48
+ ```ts
49
+ import { createLoom, ClaudeDriver, CodexDriver } from '@pikiloom/kernel';
50
+ const loom = createLoom({ drivers: [new ClaudeDriver(), new CodexDriver()], defaultAgent: 'claude' });
51
+ await loom.runTui({ agent: 'claude', workdir: process.cwd() }); // you're now in the real Claude TUI, launched by the kernel
52
+ ```
53
+
54
+ `runTui` does full stdin/stdout raw passthrough on the current terminal; `openTui` returns
55
+ a `PtyBridge` (`onData`/`write`/`resize`/`onExit`) so you can drive or tee it yourself.
56
+ Requires the optional `node-pty` dependency.
57
+
58
+ ## Contracts
59
+
60
+ | Seam | Interface | Direction |
61
+ |------|-----------|-----------|
62
+ | Agent (下层) | `AgentDriver.run(input, ctx)` → emits `DriverEvent`s | down, bundled (+ `registerDriver` escape hatch) |
63
+ | Surface (上层) | `Surface.start(io: LoomIO)` | up, app-provided (WebSurface built-in) |
64
+ | Session API | `LoomIO` = prompt/stop/steer/interact + subscribe/getSnapshot/listSessions | the meeting point |
65
+ | Plugin | `Plugin.tools()` / `decorateSnapshot()` | up, app-provided |
66
+ | Ports | `SessionStore` · `ModelResolver` · `ToolProvider` · `SystemPromptBuilder` · `InteractionHandler` | side, defaults included |
67
+
68
+ Wire shape: `UniversalSnapshot` (phase/text/reasoning/plan/toolCalls/usage/interactions/artifacts)
69
+ + delta `diffSnapshot`/`applySnapshotPatch` (prefix-append text, `undefined→null` field-clears
70
+ that survive JSON).
71
+
72
+ ## What's included
73
+
74
+ - Drivers: `EchoDriver` (hermetic), `ClaudeDriver` (real `claude` CLI, stream-json + TUI), `CodexDriver` (app-server JSON-RPC + TUI), `GeminiDriver` (stream-json), `HermesDriver` (ACP).
75
+ - Terminals: `WebSurface` (ws host speaking the wire protocol — any pikichannel client connects), `CliSurface`.
76
+ - TUI: `PtyBridge` + `attachTui` + `Loom.runTui/openTui` (raw PTY passthrough with tee).
77
+ - Ports: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`.
78
+
79
+ ## Publishing / CI
80
+
81
+ `npm publish ./packages/kernel --access public` (scoped-public). Each pikiloom release bumps
82
+ + builds the kernel (`scripts/release.sh`) and the Release workflow publishes it alongside
83
+ pikiloom — needs the `@pikiloom` npm scope to exist and `NPM_TOKEN` to have publish access.
84
+
85
+ > Known rough edge: node-pty's prebuilt `spawn-helper` can lose its executable bit on some
86
+ > npm installs (`posix_spawnp failed`). Fix: `chmod +x node_modules/node-pty/prebuilds/*/spawn-helper`.
87
+
88
+ ## Verification
89
+
90
+ ```bash
91
+ npm run typecheck # tsc, clean
92
+ npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow
93
+ KERNEL_E2E_REAL=1 npm test # also drives the real `claude` CLI end-to-end
94
+ node examples/smoke.mjs # smoke against the compiled dist
95
+ ```
96
+
97
+ Status: contracts + runtime + Echo/Claude drivers + Web terminal + default ports are
98
+ implemented and **E2E-verified** (hermetic + real-claude + compiled-artifact). The kernel
99
+ lives beside `src/` and does **not** touch the existing pikiloom app.
100
+
101
+ ## Roadmap (gated cutover)
102
+
103
+ 1. Port the remaining drivers (Codex/Gemini/Hermes-ACP) behind the same `AgentDriver` contract.
104
+ 2. Promote pikiloom's concrete IM channels to reference `Surface` adapters (`@pikiloom/kernel/surfaces/*`).
105
+ 3. Add the full pikichannel transport (WebRTC + rendezvous + TURN + `/api` tunnel) as a `WebSurface` option.
106
+ 4. Re-point the pikiloom app's `main.ts` at `createLoom({...})`; keep multi-session queue / promotion / goal
107
+ auto-continuation in the app (not the kernel). Switch over only after parity is proven on DEV.
@@ -0,0 +1,95 @@
1
+ import type { UniversalToolCall, UniversalPlan, UniversalUsage, UniversalInteraction, UniversalArtifact, UniversalSubAgent } from '../protocol/index.js';
2
+ export interface AgentTurnInput {
3
+ prompt: string;
4
+ attachments?: string[];
5
+ sessionId?: string | null;
6
+ workdir: string;
7
+ model?: string | null;
8
+ effort?: string | null;
9
+ systemPrompt?: string;
10
+ env?: Record<string, string>;
11
+ extraMcpServers?: McpServerSpec[];
12
+ mcpConfigPath?: string | null;
13
+ permissionMode?: string | null;
14
+ extraArgs?: string[];
15
+ configOverrides?: string[];
16
+ fullAccess?: boolean;
17
+ steerable?: boolean;
18
+ }
19
+ export interface McpServerSpec {
20
+ name: string;
21
+ type?: 'stdio' | 'http';
22
+ command?: string;
23
+ args?: string[];
24
+ env?: Record<string, string>;
25
+ url?: string;
26
+ headers?: Record<string, string>;
27
+ }
28
+ export type DriverEvent = {
29
+ type: 'session';
30
+ sessionId: string;
31
+ } | {
32
+ type: 'text';
33
+ delta: string;
34
+ } | {
35
+ type: 'reasoning';
36
+ delta: string;
37
+ } | {
38
+ type: 'tool';
39
+ call: UniversalToolCall;
40
+ } | {
41
+ type: 'plan';
42
+ plan: UniversalPlan;
43
+ } | {
44
+ type: 'subagent';
45
+ subagent: UniversalSubAgent;
46
+ } | {
47
+ type: 'usage';
48
+ usage: Partial<UniversalUsage>;
49
+ } | {
50
+ type: 'artifact';
51
+ artifact: UniversalArtifact;
52
+ } | {
53
+ type: 'activity';
54
+ line: string;
55
+ };
56
+ export type SteerFn = (prompt: string, attachments?: string[]) => Promise<boolean>;
57
+ export interface DriverContext {
58
+ signal: AbortSignal;
59
+ emit(event: DriverEvent): void;
60
+ askUser(interaction: UniversalInteraction): Promise<Record<string, string[]>>;
61
+ registerSteer(fn: SteerFn): void;
62
+ }
63
+ export interface DriverResult {
64
+ ok: boolean;
65
+ text: string;
66
+ reasoning?: string;
67
+ error?: string | null;
68
+ stopReason?: string | null;
69
+ sessionId?: string | null;
70
+ usage?: UniversalUsage | null;
71
+ }
72
+ export interface TuiInput {
73
+ workdir: string;
74
+ model?: string | null;
75
+ sessionId?: string | null;
76
+ env?: Record<string, string>;
77
+ extraArgs?: string[];
78
+ }
79
+ export interface TuiSpec {
80
+ command: string;
81
+ args: string[];
82
+ cwd: string;
83
+ env?: Record<string, string>;
84
+ }
85
+ export interface AgentDriver {
86
+ readonly id: string;
87
+ readonly capabilities?: {
88
+ steer?: boolean;
89
+ interact?: boolean;
90
+ resume?: boolean;
91
+ tui?: boolean;
92
+ };
93
+ run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
94
+ tui?(input: TuiInput): TuiSpec;
95
+ }
@@ -0,0 +1 @@
1
+ export {};