pikiloom 0.4.37 → 0.4.39

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 (67) hide show
  1. package/dashboard/dist/assets/{AgentTab-DbFzaIyZ.js → AgentTab-Ds8D6yrl.js} +1 -1
  2. package/dashboard/dist/assets/{ConnectionModal-QNYOWHCU.js → ConnectionModal-Buj2d5L5.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-BGaKHjFT.js → DirBrowser-BTv7rH8E.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-D4Gv9b8z.js → ExtensionsTab-BnGwxS9U.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-DxxoZd84.js → IMAccessTab-DjQnpOmF.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-Nm2e93s5.js → Modal-DpuinsE3.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-BwxZmU0U.js → Modals-Bs842H3n.js} +1 -1
  8. package/dashboard/dist/assets/{Select-SGlII0yx.js → Select-Dvia29HZ.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-CVItEgcH.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-BF6lIAYM.js → SystemTab-BzPtwDxq.js} +1 -1
  11. package/dashboard/dist/assets/index-Bthwt6K_.css +1 -0
  12. package/dashboard/dist/assets/{index-DZiAiRNt.js → index-S0NmlDEH.js} +14 -14
  13. package/dashboard/dist/assets/{index-BP8R_bLT.js → index-yZ-iG1qk.js} +2 -2
  14. package/dashboard/dist/assets/{shared-S0kcs5yP.js → shared-p3kZpiD4.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/kernel-bridge.js +207 -0
  17. package/dist/agent/mcp/capabilities.js +39 -0
  18. package/dist/agent/stream.js +19 -1
  19. package/dist/bot/bot.js +4 -13
  20. package/dist/cli/kernel-app.js +115 -0
  21. package/dist/cli/main.js +7 -0
  22. package/package.json +4 -2
  23. package/packages/kernel/README.md +305 -0
  24. package/packages/kernel/dist/contracts/driver.d.ts +95 -0
  25. package/packages/kernel/dist/contracts/driver.js +1 -0
  26. package/packages/kernel/dist/contracts/ports.d.ts +84 -0
  27. package/packages/kernel/dist/contracts/ports.js +1 -0
  28. package/packages/kernel/dist/contracts/surface.d.ts +92 -0
  29. package/packages/kernel/dist/contracts/surface.js +1 -0
  30. package/packages/kernel/dist/drivers/claude.d.ts +34 -0
  31. package/packages/kernel/dist/drivers/claude.js +500 -0
  32. package/packages/kernel/dist/drivers/codex.d.ts +24 -0
  33. package/packages/kernel/dist/drivers/codex.js +415 -0
  34. package/packages/kernel/dist/drivers/echo.d.ts +20 -0
  35. package/packages/kernel/dist/drivers/echo.js +61 -0
  36. package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
  37. package/packages/kernel/dist/drivers/gemini.js +143 -0
  38. package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
  39. package/packages/kernel/dist/drivers/hermes.js +194 -0
  40. package/packages/kernel/dist/drivers/index.d.ts +5 -0
  41. package/packages/kernel/dist/drivers/index.js +5 -0
  42. package/packages/kernel/dist/index.d.ts +18 -0
  43. package/packages/kernel/dist/index.js +29 -0
  44. package/packages/kernel/dist/ports/defaults.d.ts +59 -0
  45. package/packages/kernel/dist/ports/defaults.js +137 -0
  46. package/packages/kernel/dist/protocol/index.d.ts +309 -0
  47. package/packages/kernel/dist/protocol/index.js +59 -0
  48. package/packages/kernel/dist/runtime/hub.d.ts +68 -0
  49. package/packages/kernel/dist/runtime/hub.js +334 -0
  50. package/packages/kernel/dist/runtime/loom.d.ts +45 -0
  51. package/packages/kernel/dist/runtime/loom.js +72 -0
  52. package/packages/kernel/dist/runtime/pty.d.ts +23 -0
  53. package/packages/kernel/dist/runtime/pty.js +69 -0
  54. package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
  55. package/packages/kernel/dist/runtime/session-runner.js +210 -0
  56. package/packages/kernel/dist/runtime/tui.d.ts +8 -0
  57. package/packages/kernel/dist/runtime/tui.js +35 -0
  58. package/packages/kernel/dist/runtime/turn.d.ts +17 -0
  59. package/packages/kernel/dist/runtime/turn.js +16 -0
  60. package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
  61. package/packages/kernel/dist/surfaces/cli.js +65 -0
  62. package/packages/kernel/dist/surfaces/index.d.ts +2 -0
  63. package/packages/kernel/dist/surfaces/index.js +2 -0
  64. package/packages/kernel/dist/surfaces/web.d.ts +39 -0
  65. package/packages/kernel/dist/surfaces/web.js +244 -0
  66. package/dashboard/dist/assets/SessionPanel-DYfSlreh.js +0 -1
  67. package/dashboard/dist/assets/index-CtS48Jn-.css +0 -1
@@ -0,0 +1,207 @@
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
+ turnOutputTokens: s.usage?.turnOutputTokens ?? null,
139
+ toolCalls: s.toolCalls?.length ? s.toolCalls : undefined,
140
+ subAgents: s.subAgents?.length ? s.subAgents : undefined,
141
+ providerName: opts.byokProviderName ?? null,
142
+ };
143
+ try {
144
+ opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, s.plan ?? null);
145
+ }
146
+ catch { /* isolate */ }
147
+ };
148
+ let result;
149
+ let snapshot = {};
150
+ try {
151
+ ({ result, snapshot } = await kernel.runTurn(driver, input, {
152
+ onSnapshot,
153
+ onSteer: (fn) => { try {
154
+ opts.onSteerReady?.(fn);
155
+ }
156
+ catch { /* ignore */ } },
157
+ signal: opts.abortSignal,
158
+ }));
159
+ }
160
+ catch (err) {
161
+ agentWarn(`[kernel-bridge] kernel run failed: ${err?.message || err}`);
162
+ result = { ok: false, text: '', error: err?.message || String(err), stopReason: 'error', sessionId: opts.sessionId ?? null };
163
+ }
164
+ const finalSessionId = result.sessionId || snapshot.sessionId || opts.sessionId || null;
165
+ if (finalSessionId && finalSessionId !== seenSid) {
166
+ try {
167
+ opts.onSessionId?.(finalSessionId);
168
+ }
169
+ catch { /* ignore */ }
170
+ }
171
+ // App-level parity the pure kernel must not own (mirrors legacy *.doStream):
172
+ // - claude: rewrite the native jsonl entrypoint sdk-cli->cli (VSCode session visibility).
173
+ // - codex: translate raw provider error JSON (ChatGPT-account / third-party) to prose.
174
+ if (opts.agent === 'claude') {
175
+ try {
176
+ normalizeClaudeSessionEntrypoint(opts.workdir, finalSessionId);
177
+ }
178
+ catch { /* best-effort */ }
179
+ }
180
+ const finalError = (opts.agent === 'codex' && result.error)
181
+ ? (humanizeCodexError(result.error) ?? result.error)
182
+ : (result.error ?? null);
183
+ const u = result.usage || snapshot.usage || {};
184
+ return {
185
+ ok: !!result.ok,
186
+ message: (result.text || snapshot.text || '').trim() || (finalError ?? '(no output)'),
187
+ thinking: (result.reasoning || snapshot.reasoning || '').trim() || null,
188
+ plan: snapshot.plan ?? null,
189
+ sessionId: finalSessionId,
190
+ workspacePath: null,
191
+ model: input.model ?? null,
192
+ thinkingEffort: opts.thinkingEffort,
193
+ elapsedS: (Date.now() - start) / 1000,
194
+ inputTokens: u.inputTokens ?? null,
195
+ outputTokens: u.outputTokens ?? null,
196
+ cachedInputTokens: u.cachedInputTokens ?? null,
197
+ cacheCreationInputTokens: null,
198
+ contextWindow: null,
199
+ contextUsedTokens: u.contextUsedTokens ?? null,
200
+ contextPercent: null,
201
+ codexCumulative: null,
202
+ error: finalError,
203
+ stopReason: result.stopReason ?? null,
204
+ incomplete: !result.ok,
205
+ activity: snapshot.activity || null,
206
+ };
207
+ }
@@ -0,0 +1,39 @@
1
+ // Session tool capabilities: each co-locates a session MCP tool group (defined under
2
+ // ./tools/) with the prompt fragment that teaches the agent to use it. This is the local,
3
+ // decoupled analog of a @pikiloom/kernel Plugin (a capability = its tools + its usage
4
+ // prompt, as one unit). pikiloom composes its own system prompt app-side and feeds the
5
+ // kernel a finished string, so this module intentionally does NOT import the kernel — that
6
+ // is what keeps pikiloom fully decoupled from the kernel's Hub/plugin runtime.
7
+ const ARTIFACT_RETURN = [
8
+ '[Artifact Return]',
9
+ 'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
10
+ ].join('\n');
11
+ const ASK_USER = [
12
+ '[Asking the user]',
13
+ 'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
14
+ ].join('\n');
15
+ export const SESSION_TOOL_CAPABILITIES = [
16
+ {
17
+ id: 'artifact-delivery',
18
+ tools: ['im_send_file', 'im_list_files'],
19
+ promptFragment: () => ARTIFACT_RETURN,
20
+ },
21
+ {
22
+ id: 'ask-user',
23
+ tools: ['im_ask_user'],
24
+ // Only when the HITL path is wired and the agent is claude (codex/gemini handle asks natively).
25
+ promptFragment: ({ agent, onInteraction }) => (onInteraction && agent === 'claude') ? ASK_USER : null,
26
+ },
27
+ ];
28
+ // Compose the session tool prompt = each capability's applicable fragment, in registration
29
+ // order, joined by a blank line. Byte-identical to the prior inline assembly in bot.ts.
30
+ export function composeSessionToolPrompt(ctx) {
31
+ const parts = [];
32
+ for (const cap of SESSION_TOOL_CAPABILITIES) {
33
+ const fragment = cap.promptFragment(ctx);
34
+ const trimmed = String(fragment || '').trim();
35
+ if (trimmed)
36
+ parts.push(trimmed);
37
+ }
38
+ return parts.join('\n\n');
39
+ }
@@ -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;
package/dist/bot/bot.js CHANGED
@@ -8,6 +8,7 @@ import { getActiveProfileId, getActiveProfile, setActiveProfile } from '../model
8
8
  import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
9
9
  import { getDriver, hasDriver, allDriverIds, getDriverCapabilities } from '../agent/driver.js';
10
10
  import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
11
+ import { composeSessionToolPrompt } from '../agent/mcp/capabilities.js';
11
12
  import { terminateProcessTree } from '../core/process-control.js';
12
13
  import { expandTilde } from '../core/platform.js';
13
14
  import { VERSION } from '../core/version.js';
@@ -48,18 +49,6 @@ function appendExtraPrompt(base, extra) {
48
49
  return lhs;
49
50
  return `${lhs}\n\n${rhs}`;
50
51
  }
51
- function buildMcpDeliveryPrompt() {
52
- return [
53
- '[Artifact Return]',
54
- 'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
55
- ].join('\n');
56
- }
57
- function buildClaudeAskUserPrompt() {
58
- return [
59
- '[Asking the user]',
60
- 'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
61
- ].join('\n');
62
- }
63
52
  function buildBrowserAutomationPrompt(browserEnabled) {
64
53
  if (!browserEnabled) {
65
54
  return [
@@ -2022,7 +2011,9 @@ export class Bot {
2022
2011
  const workflowEnabled = cs.agent === 'claude' && (extras?.workflowEnabled ?? this.claudeWorkflowEnabled);
2023
2012
  const deliverySessionKey = ('key' in cs && typeof cs.key === 'string') ? cs.key : null;
2024
2013
  const wrappedSendFile = this.buildArtifactSendFile(cs.agent, deliverySessionKey, cs, mcpSendFile);
2025
- const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(appendExtraPrompt(buildMcpDeliveryPrompt(), onInteraction && cs.agent === 'claude' ? buildClaudeAskUserPrompt() : ''), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
2014
+ // Session tool capabilities (im_send_file / im_ask_user) bundle their usage prompt with
2015
+ // the tool itself — see agent/mcp/capabilities.ts. Browser/workflow remain session policy.
2016
+ const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(composeSessionToolPrompt({ agent: cs.agent, onInteraction: !!onInteraction }), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
2026
2017
  const effectiveSystemPrompt = isFirstTurnOfSession
2027
2018
  ? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
2028
2019
  : (mcpSystemPrompt || undefined);
@@ -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.39",
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",