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.
- package/dashboard/dist/assets/{AgentTab-DbFzaIyZ.js → AgentTab-Ds8D6yrl.js} +1 -1
- package/dashboard/dist/assets/{ConnectionModal-QNYOWHCU.js → ConnectionModal-Buj2d5L5.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-BGaKHjFT.js → DirBrowser-BTv7rH8E.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-D4Gv9b8z.js → ExtensionsTab-BnGwxS9U.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-DxxoZd84.js → IMAccessTab-DjQnpOmF.js} +1 -1
- package/dashboard/dist/assets/{Modal-Nm2e93s5.js → Modal-DpuinsE3.js} +1 -1
- package/dashboard/dist/assets/{Modals-BwxZmU0U.js → Modals-Bs842H3n.js} +1 -1
- package/dashboard/dist/assets/{Select-SGlII0yx.js → Select-Dvia29HZ.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-CVItEgcH.js +1 -0
- package/dashboard/dist/assets/{SystemTab-BF6lIAYM.js → SystemTab-BzPtwDxq.js} +1 -1
- package/dashboard/dist/assets/index-Bthwt6K_.css +1 -0
- package/dashboard/dist/assets/{index-DZiAiRNt.js → index-S0NmlDEH.js} +14 -14
- package/dashboard/dist/assets/{index-BP8R_bLT.js → index-yZ-iG1qk.js} +2 -2
- package/dashboard/dist/assets/{shared-S0kcs5yP.js → shared-p3kZpiD4.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/kernel-bridge.js +207 -0
- package/dist/agent/mcp/capabilities.js +39 -0
- package/dist/agent/stream.js +19 -1
- package/dist/bot/bot.js +4 -13
- package/dist/cli/kernel-app.js +115 -0
- package/dist/cli/main.js +7 -0
- package/package.json +4 -2
- package/packages/kernel/README.md +305 -0
- package/packages/kernel/dist/contracts/driver.d.ts +95 -0
- package/packages/kernel/dist/contracts/driver.js +1 -0
- package/packages/kernel/dist/contracts/ports.d.ts +84 -0
- package/packages/kernel/dist/contracts/ports.js +1 -0
- package/packages/kernel/dist/contracts/surface.d.ts +92 -0
- package/packages/kernel/dist/contracts/surface.js +1 -0
- package/packages/kernel/dist/drivers/claude.d.ts +34 -0
- package/packages/kernel/dist/drivers/claude.js +500 -0
- package/packages/kernel/dist/drivers/codex.d.ts +24 -0
- package/packages/kernel/dist/drivers/codex.js +415 -0
- package/packages/kernel/dist/drivers/echo.d.ts +20 -0
- package/packages/kernel/dist/drivers/echo.js +61 -0
- package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
- package/packages/kernel/dist/drivers/gemini.js +143 -0
- package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
- package/packages/kernel/dist/drivers/hermes.js +194 -0
- package/packages/kernel/dist/drivers/index.d.ts +5 -0
- package/packages/kernel/dist/drivers/index.js +5 -0
- package/packages/kernel/dist/index.d.ts +18 -0
- package/packages/kernel/dist/index.js +29 -0
- package/packages/kernel/dist/ports/defaults.d.ts +59 -0
- package/packages/kernel/dist/ports/defaults.js +137 -0
- package/packages/kernel/dist/protocol/index.d.ts +309 -0
- package/packages/kernel/dist/protocol/index.js +59 -0
- package/packages/kernel/dist/runtime/hub.d.ts +68 -0
- package/packages/kernel/dist/runtime/hub.js +334 -0
- package/packages/kernel/dist/runtime/loom.d.ts +45 -0
- package/packages/kernel/dist/runtime/loom.js +72 -0
- package/packages/kernel/dist/runtime/pty.d.ts +23 -0
- package/packages/kernel/dist/runtime/pty.js +69 -0
- package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
- package/packages/kernel/dist/runtime/session-runner.js +210 -0
- package/packages/kernel/dist/runtime/tui.d.ts +8 -0
- package/packages/kernel/dist/runtime/tui.js +35 -0
- package/packages/kernel/dist/runtime/turn.d.ts +17 -0
- package/packages/kernel/dist/runtime/turn.js +16 -0
- package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
- package/packages/kernel/dist/surfaces/cli.js +65 -0
- package/packages/kernel/dist/surfaces/index.d.ts +2 -0
- package/packages/kernel/dist/surfaces/index.js +2 -0
- package/packages/kernel/dist/surfaces/web.d.ts +39 -0
- package/packages/kernel/dist/surfaces/web.js +244 -0
- package/dashboard/dist/assets/SessionPanel-DYfSlreh.js +0 -1
- 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
|
+
}
|
package/dist/agent/stream.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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",
|