pikiloom 0.4.26 → 0.4.27
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-BNJ73P0m.js → AgentTab-DIP-y4dl.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
- package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
- package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
- package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
- package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
package/dist/agent/mcp/bridge.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp-bridge.ts — MCP session bridge orchestrator.
|
|
3
|
-
*
|
|
4
|
-
* Runs inside the main pikiloom process. For each agent stream:
|
|
5
|
-
* 1. Starts a tiny HTTP callback server on localhost (random port).
|
|
6
|
-
* 2. Writes an MCP config JSON pointing to `pikiloom --mcp-serve`.
|
|
7
|
-
* 3. The agent CLI loads that config via its MCP registration mechanism.
|
|
8
|
-
* 4. When the agent calls `send_file`, the MCP server POSTs to our callback.
|
|
9
|
-
* 5. We forward the request to the IM channel and respond with success/failure.
|
|
10
|
-
*
|
|
11
|
-
* Lifecycle: one bridge per stream, created before spawn, stopped after stream ends.
|
|
12
|
-
*/
|
|
13
1
|
import http from 'node:http';
|
|
14
2
|
import fs from 'node:fs';
|
|
15
3
|
import os from 'node:os';
|
|
@@ -45,18 +33,15 @@ export function resolveMcpServerCommand(runtime = {
|
|
|
45
33
|
const currentProcess = resolveCurrentCliCommand(runtime, ['--mcp-serve']);
|
|
46
34
|
if (currentProcess)
|
|
47
35
|
return currentProcess;
|
|
48
|
-
// Try to find the compiled JS file in the same directory as this module
|
|
49
36
|
const thisDir = path.dirname(fileURLToPath(runtime.moduleUrl));
|
|
50
37
|
const serverScript = path.join(thisDir, 'session-server.js');
|
|
51
38
|
if (fs.existsSync(serverScript)) {
|
|
52
39
|
return { command: 'node', args: [serverScript] };
|
|
53
40
|
}
|
|
54
|
-
// Fallback: use pikiloom CLI with --mcp-serve flag
|
|
55
41
|
const cliScript = path.resolve(thisDir, '../../cli/main.js');
|
|
56
42
|
if (fs.existsSync(cliScript)) {
|
|
57
43
|
return { command: 'node', args: [cliScript, '--mcp-serve'] };
|
|
58
44
|
}
|
|
59
|
-
// Last resort: assume pikiloom is in PATH
|
|
60
45
|
return { command: 'pikiloom', args: ['--mcp-serve'] };
|
|
61
46
|
}
|
|
62
47
|
function parseOptionalBool(value) {
|
|
@@ -81,11 +66,6 @@ function boolFromConfigEnv(configValue, envValue, fallback) {
|
|
|
81
66
|
return fallback;
|
|
82
67
|
}
|
|
83
68
|
export function resolveGuiIntegrationConfig(config = loadUserConfig(), env = process.env) {
|
|
84
|
-
// A configured remote CDP endpoint implies the user wants browser automation,
|
|
85
|
-
// so it flips the *default* on. An explicit PIKILOOM_BROWSER_ENABLED / config
|
|
86
|
-
// value still wins (so `=false` can disable even with a CDP URL set). This
|
|
87
|
-
// removes the footgun where setting only PIKILOOM_BROWSER_CDP_URL silently
|
|
88
|
-
// injected no browser server at all.
|
|
89
69
|
const browserEnabled = boolFromConfigEnv(typeof config.browserEnabled === 'boolean' ? config.browserEnabled : config.browserUseProfile, env.PIKILOOM_BROWSER_ENABLED ?? env.PIKILOOM_BROWSER_USE_PROFILE, !!getConfiguredRemoteCdpUrl(env));
|
|
90
70
|
const peekabooEnabled = boolFromConfigEnv(config.peekabooEnabled, env.PIKILOOM_PEEKABOO_ENABLED, false);
|
|
91
71
|
return {
|
|
@@ -95,19 +75,8 @@ export function resolveGuiIntegrationConfig(config = loadUserConfig(), env = pro
|
|
|
95
75
|
peekabooEnabled,
|
|
96
76
|
};
|
|
97
77
|
}
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Peekaboo (native macOS GUI control) — npx package warming
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
/** npx package spec for the multi-bin Peekaboo package. */
|
|
102
78
|
export const PEEKABOO_NPX_PACKAGE = '@steipete/peekaboo';
|
|
103
|
-
/** argv for the long-running MCP server written into the agent's mcp-config. */
|
|
104
79
|
export const PEEKABOO_MCP_ARGV = ['-y', '-p', PEEKABOO_NPX_PACKAGE, 'peekaboo-mcp'];
|
|
105
|
-
/**
|
|
106
|
-
* argv for the cache warm: the quick `peekaboo` bin (prints its version and
|
|
107
|
-
* exits) under the SAME `-p` package spec the server uses, so it populates the
|
|
108
|
-
* exact npx cache entry `peekaboo-mcp` later reuses. Deliberately NOT the
|
|
109
|
-
* `peekaboo-mcp` bin — that's a server that never exits.
|
|
110
|
-
*/
|
|
111
80
|
export const PEEKABOO_WARM_ARGV = ['-y', '-p', PEEKABOO_NPX_PACKAGE, 'peekaboo', '--version'];
|
|
112
81
|
let peekabooWarmStarted = false;
|
|
113
82
|
const PEEKABOO_ENV_ALLOWLIST = [
|
|
@@ -132,12 +101,6 @@ function cleanEnvString(value) {
|
|
|
132
101
|
return null;
|
|
133
102
|
return trimmed;
|
|
134
103
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Peekaboo only needs a normal user shell environment so npx can resolve/cache
|
|
137
|
-
* the package. Do not inherit the full pikiloom/agent environment here: it may
|
|
138
|
-
* contain provider API keys, channel tokens, OAuth bearer values, or database
|
|
139
|
-
* URLs that a GUI MCP server has no reason to see.
|
|
140
|
-
*/
|
|
141
104
|
export function buildPeekabooChildEnv(env = process.env) {
|
|
142
105
|
const safe = {};
|
|
143
106
|
for (const key of PEEKABOO_ENV_ALLOWLIST) {
|
|
@@ -162,23 +125,6 @@ function buildPeekabooMcpServer() {
|
|
|
162
125
|
args: ['-i', ...peekabooEnvArgs(safeEnv), 'npx', ...PEEKABOO_MCP_ARGV],
|
|
163
126
|
};
|
|
164
127
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Pre-warm the Peekaboo npx package so the agent's MCP server connects instantly.
|
|
167
|
-
*
|
|
168
|
-
* `peekaboo-mcp` ships a native Swift binary, so a cold `npx -y -p
|
|
169
|
-
* @steipete/peekaboo …` pays a large one-time download. The agent CLI launches
|
|
170
|
-
* that server itself and abandons it when the MCP handshake doesn't arrive
|
|
171
|
-
* within its startup window — killing the npx child mid-download. The cache
|
|
172
|
-
* never finishes, the next session is cold too, and Peekaboo stays stuck at
|
|
173
|
-
* "Still connecting" indefinitely.
|
|
174
|
-
*
|
|
175
|
-
* Warming from a detached process that outlives any single session breaks the
|
|
176
|
-
* loop: the download completes once, independent of the agent's timeout, and
|
|
177
|
-
* every later session finds the package cached. Idempotent + process-singleton
|
|
178
|
-
* (repeated stream-start calls are no-ops; npx itself short-circuits once the
|
|
179
|
-
* package is present) and strictly fire-and-forget — it never blocks or fails
|
|
180
|
-
* a session.
|
|
181
|
-
*/
|
|
182
128
|
export function ensurePeekabooWarm() {
|
|
183
129
|
if (process.platform !== 'darwin' || peekabooWarmStarted)
|
|
184
130
|
return;
|
|
@@ -189,7 +135,6 @@ export function ensurePeekabooWarm() {
|
|
|
189
135
|
detached: true,
|
|
190
136
|
env: buildPeekabooChildEnv(),
|
|
191
137
|
});
|
|
192
|
-
// npx missing / spawn failure: clear the latch so a later call can retry.
|
|
193
138
|
child.on('error', () => { peekabooWarmStarted = false; });
|
|
194
139
|
child.unref();
|
|
195
140
|
}
|
|
@@ -213,8 +158,6 @@ export function buildSupplementalMcpServers(gui = resolveGuiIntegrationConfig(),
|
|
|
213
158
|
});
|
|
214
159
|
}
|
|
215
160
|
if (gui.peekabooEnabled && process.platform === 'darwin') {
|
|
216
|
-
// Peekaboo — native macOS GUI automation via Accessibility + ScreenCaptureKit.
|
|
217
|
-
// Run the dedicated MCP bin from the multi-bin @steipete/peekaboo package.
|
|
218
161
|
servers.push(buildPeekabooMcpServer());
|
|
219
162
|
}
|
|
220
163
|
return servers;
|
|
@@ -237,19 +180,6 @@ function buildClaudeMcpConfig(servers) {
|
|
|
237
180
|
])),
|
|
238
181
|
};
|
|
239
182
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Build the `codex mcp add` argv for a single registered server. Returns
|
|
242
|
-
* `null` when the descriptor lacks the fields needed for its transport
|
|
243
|
-
* (treated as a no-op rather than throwing — keeps a malformed entry from
|
|
244
|
-
* breaking the whole session).
|
|
245
|
-
*
|
|
246
|
-
* HTTP servers can't pass a literal bearer to codex; the CLI only accepts
|
|
247
|
-
* `--bearer-token-env-var <NAME>`. We synthesize a deterministic env-var
|
|
248
|
-
* name per server, stash the token in the supplied `tokenEnv` map, and the
|
|
249
|
-
* caller threads that map into the codex child process via extraEnv.
|
|
250
|
-
*
|
|
251
|
-
* Exported for unit tests; not re-exported from the package surface.
|
|
252
|
-
*/
|
|
253
183
|
export function buildCodexMcpAddArgs(server, tokenEnv) {
|
|
254
184
|
if (server.type === 'http') {
|
|
255
185
|
if (!server.url)
|
|
@@ -316,8 +246,6 @@ export function redactMcpConfigForLog(configPath) {
|
|
|
316
246
|
}
|
|
317
247
|
export function buildGeminiMcpConfig(servers) {
|
|
318
248
|
return {
|
|
319
|
-
// Session attachments live under .pikiloom/... and should remain readable to
|
|
320
|
-
// Gemini's built-in file tools even when the project ignores that directory.
|
|
321
249
|
fileFiltering: {
|
|
322
250
|
respectGitIgnore: false,
|
|
323
251
|
respectGeminiIgnore: false,
|
|
@@ -346,43 +274,6 @@ export function buildGeminiMcpConfig(servers) {
|
|
|
346
274
|
})),
|
|
347
275
|
};
|
|
348
276
|
}
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
// Stale playwright-mcp reaper
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
/**
|
|
353
|
-
* Find and SIGTERM playwright-mcp processes that attach to the same managed
|
|
354
|
-
* Chrome CDP endpoint but are NOT descendants of the current pikiloom process.
|
|
355
|
-
*
|
|
356
|
-
* Background: playwright-mcp is spawned by the agent CLI (e.g. claude) as a
|
|
357
|
-
* child via the mcp-config we write. When the agent CLI is killed ungracefully
|
|
358
|
-
* — or worse, gets reparented to launchd/init and survives across pikiloom
|
|
359
|
-
* restarts — its playwright-mcp child stays alive too. Multiple playwright-mcp
|
|
360
|
-
* instances attached to the same `--cdp-endpoint` cause backend state
|
|
361
|
-
* confusion (microsoft/playwright-mcp#1299, #893) and manifest as
|
|
362
|
-
* `Connection closed` errors or 2+ minute hangs the next time a tool is
|
|
363
|
-
* called. The community's recommended hygiene is one playwright-mcp per
|
|
364
|
-
* agent instance; this sweeper enforces that by reaping orphans from prior
|
|
365
|
-
* runs at the start of every new bridge.
|
|
366
|
-
*
|
|
367
|
-
* Safety: a candidate is only reaped if its `ppid` chain — walked entirely in
|
|
368
|
-
* memory from a single `ps` snapshot — does NOT include the current pikiloom
|
|
369
|
-
* process. In-flight playwright-mcp children of THIS pikiloom (sibling
|
|
370
|
-
* streams) are always spared.
|
|
371
|
-
*/
|
|
372
|
-
/**
|
|
373
|
-
* Pure matcher for the reaper. Returns true when `command` looks like a
|
|
374
|
-
* playwright-mcp process attached to the same CDP endpoint as ours.
|
|
375
|
-
*
|
|
376
|
-
* Accepts both invocation forms we have seen in the wild:
|
|
377
|
-
* - `node <path>/@playwright/mcp/cli.js …` (direct, pikiloom's preferred)
|
|
378
|
-
* - `node <path>/node_modules/.bin/playwright-mcp …` (npm bin symlink,
|
|
379
|
-
* used by `npx @playwright/mcp` and any agent CLI that resolves via PATH)
|
|
380
|
-
*
|
|
381
|
-
* The CDP endpoint must also appear literally in the argv — without that
|
|
382
|
-
* guard a stray `npm exec @playwright/mcp` with its own browser would be
|
|
383
|
-
* killed, and unrelated `node -e <src>` processes whose inline source happens
|
|
384
|
-
* to mention `@playwright/mcp` would also be misidentified.
|
|
385
|
-
*/
|
|
386
277
|
export function _matchPlaywrightMcpProcessCommand(command, normalizedCdpEndpoint) {
|
|
387
278
|
if (!command || !normalizedCdpEndpoint)
|
|
388
279
|
return false;
|
|
@@ -399,12 +290,7 @@ export function _matchPlaywrightMcpProcessCommand(command, normalizedCdpEndpoint
|
|
|
399
290
|
return false;
|
|
400
291
|
return true;
|
|
401
292
|
}
|
|
402
|
-
// Promisified spawn for codex MCP registration — keeps the per-server add/remove
|
|
403
|
-
// off the event loop (was execFileSync, which blocked per spawn at stream start).
|
|
404
293
|
const execFileAsync = promisify(execFile);
|
|
405
|
-
// Reaping shells out to a full `ps` table scan + per-line regex and only guards
|
|
406
|
-
// against playwright-mcp orphans left by a previous run, so it need not run on
|
|
407
|
-
// every browser-enabled stream start. Throttle it per CDP endpoint.
|
|
408
294
|
const REAP_THROTTLE_MS = 30_000;
|
|
409
295
|
const lastReapAt = new Map();
|
|
410
296
|
function reapStalePlaywrightMcpProcesses(cdpEndpoint) {
|
|
@@ -457,21 +343,13 @@ function reapStalePlaywrightMcpProcesses(cdpEndpoint) {
|
|
|
457
343
|
reaped.push(pid);
|
|
458
344
|
}
|
|
459
345
|
catch {
|
|
460
|
-
// Already dead — no-op.
|
|
461
346
|
}
|
|
462
347
|
}
|
|
463
348
|
return { reaped, spared };
|
|
464
349
|
}
|
|
465
|
-
// ---------------------------------------------------------------------------
|
|
466
|
-
// Stale peekaboo-mcp reaper
|
|
467
|
-
// ---------------------------------------------------------------------------
|
|
468
350
|
function commandTokenBase(token) {
|
|
469
351
|
return path.basename(token.replace(/^"+|"+$/g, '')).replace(/\.(?:cmd|exe)$/i, '').toLowerCase();
|
|
470
352
|
}
|
|
471
|
-
/**
|
|
472
|
-
* Pure matcher for stale-process hygiene. It intentionally targets the
|
|
473
|
-
* long-running MCP server only, not the quick cache warm (`peekaboo --version`).
|
|
474
|
-
*/
|
|
475
353
|
export function _matchPeekabooMcpProcessCommand(command) {
|
|
476
354
|
if (!command || !command.includes('peekaboo-mcp'))
|
|
477
355
|
return false;
|
|
@@ -560,51 +438,28 @@ function reapStalePeekabooMcpProcesses() {
|
|
|
560
438
|
process.kill(pid, 'SIGKILL');
|
|
561
439
|
}
|
|
562
440
|
catch {
|
|
563
|
-
// Process already exited or cannot be signalled — no-op.
|
|
564
441
|
}
|
|
565
442
|
}, PEEKABOO_REAP_FORCE_AFTER_MS);
|
|
566
443
|
forceTimer.unref?.();
|
|
567
444
|
reaped.push(pid);
|
|
568
445
|
}
|
|
569
446
|
catch {
|
|
570
|
-
// Already dead — no-op.
|
|
571
447
|
}
|
|
572
448
|
}
|
|
573
449
|
return { reaped, spared };
|
|
574
450
|
}
|
|
575
|
-
/**
|
|
576
|
-
* Decide which CDP endpoint the per-session playwright/mcp should attach to.
|
|
577
|
-
*
|
|
578
|
-
* When `PIKILOOM_BROWSER_CDP_URL` is set we return it UNCONDITIONALLY (mode
|
|
579
|
-
* `remote`) — without probing it for reachability. This is deliberate: the
|
|
580
|
-
* documented contract is that pikiloom never launches, probes, or kills a local
|
|
581
|
-
* Chrome in remote mode (e.g. inside a headless container that has no Chrome at
|
|
582
|
-
* all). Gating on a reachability ping would let a momentarily-unreachable
|
|
583
|
-
* sidecar fall through to the local-launch branch and silently spawn a browser
|
|
584
|
-
* — exactly the bug reported in #16. Handing `--cdp-endpoint <url>` to
|
|
585
|
-
* playwright/mcp instead surfaces an honest connection error on the first
|
|
586
|
-
* `browser_*` call if the sidecar is down.
|
|
587
|
-
*
|
|
588
|
-
* Without the override, fall back to probing the local managed Chrome via its
|
|
589
|
-
* DevToolsActivePort file (cross-process attach); `none` means leave Chrome
|
|
590
|
-
* unlaunched and let playwright/mcp cold-start one with `--user-data-dir`.
|
|
591
|
-
*/
|
|
592
451
|
export async function resolveBridgeBrowserEndpoint(profileDir = getManagedBrowserProfileDir(), remoteCdpUrl = getConfiguredRemoteCdpUrl()) {
|
|
593
452
|
if (remoteCdpUrl)
|
|
594
453
|
return { endpoint: remoteCdpUrl, mode: 'remote' };
|
|
595
454
|
const local = await resolveManagedBrowserCdpEndpoint(profileDir).catch(() => null);
|
|
596
455
|
return { endpoint: local, mode: local ? 'local-attach' : 'none' };
|
|
597
456
|
}
|
|
598
|
-
// ---------------------------------------------------------------------------
|
|
599
|
-
// Bridge implementation
|
|
600
|
-
// ---------------------------------------------------------------------------
|
|
601
457
|
const ARTIFACT_MAX_BYTES = MCP_ARTIFACT_MAX_BYTES;
|
|
602
458
|
const SEND_FILE_TIMEOUT_MS = MCP_TIMEOUTS.sendFile;
|
|
603
459
|
const PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
604
460
|
function isPhotoFile(filePath) {
|
|
605
461
|
return PHOTO_EXTS.has(path.extname(filePath).toLowerCase());
|
|
606
462
|
}
|
|
607
|
-
/** Check if realFile is inside any of the allowed root directories. */
|
|
608
463
|
function isInsideAllowedRoot(realFile, allowedRoots) {
|
|
609
464
|
for (const root of allowedRoots) {
|
|
610
465
|
try {
|
|
@@ -613,7 +468,7 @@ function isInsideAllowedRoot(realFile, allowedRoots) {
|
|
|
613
468
|
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel))
|
|
614
469
|
return true;
|
|
615
470
|
}
|
|
616
|
-
catch {
|
|
471
|
+
catch { }
|
|
617
472
|
}
|
|
618
473
|
return false;
|
|
619
474
|
}
|
|
@@ -656,7 +511,6 @@ export function resolveSendFilePath(inputPath, workspacePath, stagedFiles = [],
|
|
|
656
511
|
return { path: candidate };
|
|
657
512
|
}
|
|
658
513
|
catch {
|
|
659
|
-
// Try next candidate.
|
|
660
514
|
}
|
|
661
515
|
}
|
|
662
516
|
if (!requested.includes('/') && !requested.includes(path.sep)) {
|
|
@@ -700,9 +554,6 @@ export async function startMcpBridge(opts) {
|
|
|
700
554
|
const gui = resolveGuiIntegrationConfig();
|
|
701
555
|
for (const hint of buildGuiSetupHints(gui))
|
|
702
556
|
opts.onLog?.(hint);
|
|
703
|
-
// Peekaboo ships a native binary behind npx; warm its cache out-of-band so the
|
|
704
|
-
// agent's MCP server connects instantly instead of hanging at "Still
|
|
705
|
-
// connecting" on a cold first-run download (see ensurePeekabooWarm).
|
|
706
557
|
if (gui.peekabooEnabled) {
|
|
707
558
|
ensurePeekabooWarm();
|
|
708
559
|
const { reaped, spared } = reapStalePeekabooMcpProcesses();
|
|
@@ -710,16 +561,8 @@ export async function startMcpBridge(opts) {
|
|
|
710
561
|
opts.onLog?.(`reaped ${reaped.length} stale peekaboo-mcp process(es): pid=${reaped.join(',')}${spared.length ? ` (spared active: ${spared.join(',')})` : ''}`);
|
|
711
562
|
}
|
|
712
563
|
}
|
|
713
|
-
// Lazy browser lifecycle: probe an already-running managed Chrome via
|
|
714
|
-
// <profileDir>/DevToolsActivePort and attach if reachable; otherwise leave
|
|
715
|
-
// Chrome unlaunched and let playwright/mcp launch it with `--user-data-dir`
|
|
716
|
-
// on the first browser_* tool call. Previously the bridge eagerly called
|
|
717
|
-
// `ensureManagedBrowser`, which forced a Chrome window to open at every
|
|
718
|
-
// stream start even when the agent never touched the browser.
|
|
719
564
|
let browserCdpEndpoint = null;
|
|
720
565
|
if (gui.browserEnabled) {
|
|
721
|
-
// Write the playwright/mcp config file (referenced by --config in
|
|
722
|
-
// getManagedBrowserMcpArgs) before the agent CLI spawns playwright/mcp.
|
|
723
566
|
ensurePlaywrightMcpConfigFile();
|
|
724
567
|
const { endpoint, mode } = await resolveBridgeBrowserEndpoint(gui.browserProfileDir);
|
|
725
568
|
browserCdpEndpoint = endpoint;
|
|
@@ -727,10 +570,6 @@ export async function startMcpBridge(opts) {
|
|
|
727
570
|
opts.onLog?.(mode === 'remote'
|
|
728
571
|
? `attaching to remote CDP endpoint ${endpoint} (PIKILOOM_BROWSER_CDP_URL); local Chrome launch disabled.`
|
|
729
572
|
: `attaching to existing managed browser at ${endpoint}.`);
|
|
730
|
-
// Clear stale playwright-mcp children still bound to this endpoint (one
|
|
731
|
-
// playwright-mcp per browser, per microsoft/playwright-mcp#1299). Safe for
|
|
732
|
-
// the remote sidecar too — it only ever SIGTERMs local playwright-mcp
|
|
733
|
-
// processes, never the Chrome itself.
|
|
734
573
|
const { reaped, spared } = reapStalePlaywrightMcpProcesses(endpoint);
|
|
735
574
|
if (reaped.length) {
|
|
736
575
|
opts.onLog?.(`reaped ${reaped.length} stale playwright-mcp process(es) attached to ${endpoint}: pid=${reaped.join(',')}${spared.length ? ` (spared in-tree: ${spared.join(',')})` : ''}`);
|
|
@@ -740,16 +579,10 @@ export async function startMcpBridge(opts) {
|
|
|
740
579
|
opts.onLog?.('no managed browser running; playwright/mcp will launch one on first browser_* tool call.');
|
|
741
580
|
}
|
|
742
581
|
}
|
|
743
|
-
// Build allowed roots: workspace + workdir + /tmp
|
|
744
582
|
const allowedRoots = [workspacePath];
|
|
745
583
|
if (opts.workdir)
|
|
746
584
|
allowedRoots.push(opts.workdir);
|
|
747
585
|
allowedRoots.push('/tmp', os.tmpdir());
|
|
748
|
-
// ── HTTP callback server ──
|
|
749
|
-
// Started only when an IM-side callback is wired up, to serve:
|
|
750
|
-
// - `im_send_file` → /send-file
|
|
751
|
-
// - `im_ask_user` → /ask-user
|
|
752
|
-
// - structured tool-activity logging from the in-process MCP server → /log
|
|
753
586
|
let callbackServer = null;
|
|
754
587
|
let port = 0;
|
|
755
588
|
const needsCallbackServer = !!sendFile || !!onInteraction;
|
|
@@ -762,14 +595,12 @@ export async function startMcpBridge(opts) {
|
|
|
762
595
|
res.end();
|
|
763
596
|
return;
|
|
764
597
|
}
|
|
765
|
-
// /ask-user blocks until the user replies; disable timeouts for it.
|
|
766
598
|
if (endpoint === '/ask-user') {
|
|
767
599
|
req.setTimeout(0);
|
|
768
600
|
res.setTimeout(0);
|
|
769
601
|
}
|
|
770
602
|
let body = '';
|
|
771
603
|
req.on('data', (chunk) => { body += chunk; });
|
|
772
|
-
// Timeout for receiving the request body
|
|
773
604
|
const bodyTimer = setTimeout(() => {
|
|
774
605
|
req.destroy(new Error('request body timeout'));
|
|
775
606
|
}, MCP_TIMEOUTS.requestBody);
|
|
@@ -845,7 +676,6 @@ export async function startMcpBridge(opts) {
|
|
|
845
676
|
}
|
|
846
677
|
return;
|
|
847
678
|
}
|
|
848
|
-
// endpoint === '/send-file'
|
|
849
679
|
if (!sendFile) {
|
|
850
680
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
851
681
|
res.end(JSON.stringify({ ok: false, error: 'send-file is not available for this session' }));
|
|
@@ -858,7 +688,6 @@ export async function startMcpBridge(opts) {
|
|
|
858
688
|
res.end(JSON.stringify({ ok: false, error: 'path is required' }));
|
|
859
689
|
return;
|
|
860
690
|
}
|
|
861
|
-
// Resolve and validate path
|
|
862
691
|
const resolved = resolveSendFilePath(relPath, workspacePath, stagedFiles, opts.workdir);
|
|
863
692
|
const absPath = resolved.path;
|
|
864
693
|
let realFile;
|
|
@@ -875,7 +704,6 @@ export async function startMcpBridge(opts) {
|
|
|
875
704
|
res.end(JSON.stringify({ ok: false, error: 'file must be inside the workspace, workdir, or /tmp' }));
|
|
876
705
|
return;
|
|
877
706
|
}
|
|
878
|
-
// Size check
|
|
879
707
|
const stat = fs.statSync(realFile);
|
|
880
708
|
if (!stat.isFile()) {
|
|
881
709
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -887,7 +715,6 @@ export async function startMcpBridge(opts) {
|
|
|
887
715
|
res.end(JSON.stringify({ ok: false, error: `file too large (${stat.size} bytes, max ${ARTIFACT_MAX_BYTES})` }));
|
|
888
716
|
return;
|
|
889
717
|
}
|
|
890
|
-
// Auto-detect kind
|
|
891
718
|
const kind = data.kind === 'photo' ? 'photo'
|
|
892
719
|
: data.kind === 'document' ? 'document'
|
|
893
720
|
: isPhotoFile(realFile) ? 'photo'
|
|
@@ -907,10 +734,7 @@ export async function startMcpBridge(opts) {
|
|
|
907
734
|
}
|
|
908
735
|
});
|
|
909
736
|
});
|
|
910
|
-
// Per-request body timers above guard against partial uploads.
|
|
911
737
|
callbackServer.headersTimeout = MCP_TIMEOUTS.serverHeaders;
|
|
912
|
-
// /ask-user can block indefinitely; drop the server-wide request timeout
|
|
913
|
-
// when that endpoint is wired up.
|
|
914
738
|
if (onInteraction)
|
|
915
739
|
callbackServer.requestTimeout = 0;
|
|
916
740
|
await new Promise((resolve, reject) => {
|
|
@@ -919,18 +743,13 @@ export async function startMcpBridge(opts) {
|
|
|
919
743
|
});
|
|
920
744
|
port = callbackServer.address().port;
|
|
921
745
|
}
|
|
922
|
-
// ── Register MCP server with the agent ──
|
|
923
746
|
const supplementalServers = buildSupplementalMcpServers(gui, { cdpEndpoint: browserCdpEndpoint });
|
|
924
747
|
const servers = [...supplementalServers];
|
|
925
|
-
// Register the pikiloom stdio MCP server when any in-process tool needs the
|
|
926
|
-
// callback channel. `MCP_TOOLS_AVAILABLE` tells the server which tool
|
|
927
|
-
// families to advertise.
|
|
928
748
|
if (port && (sendFile || onInteraction)) {
|
|
929
749
|
const { command, args } = resolveMcpServerCommand();
|
|
930
750
|
const enabledTools = [];
|
|
931
751
|
if (sendFile)
|
|
932
752
|
enabledTools.push('workspace');
|
|
933
|
-
// Codex has native user-input via JSON-RPC; don't expose `im_ask_user`.
|
|
934
753
|
if (onInteraction && opts.agent !== 'codex')
|
|
935
754
|
enabledTools.push('ask-user');
|
|
936
755
|
const envVars = {
|
|
@@ -944,7 +763,6 @@ export async function startMcpBridge(opts) {
|
|
|
944
763
|
};
|
|
945
764
|
servers.unshift({ name: 'pikiloom', command, args, env: envVars });
|
|
946
765
|
}
|
|
947
|
-
// Nothing to register — skip bridge entirely
|
|
948
766
|
if (!servers.length) {
|
|
949
767
|
if (callbackServer)
|
|
950
768
|
await new Promise(resolve => callbackServer.close(() => resolve()));
|
|
@@ -955,16 +773,9 @@ export async function startMcpBridge(opts) {
|
|
|
955
773
|
let mcpServers;
|
|
956
774
|
const codexRegisteredNames = [];
|
|
957
775
|
if (opts.agent === 'codex') {
|
|
958
|
-
// Codex: register MCP servers via `codex mcp add/remove`
|
|
959
|
-
// Include global + workspace extensions alongside built-in servers
|
|
960
776
|
const extServers = getGlobalExtensionsAsServers(opts.workdir);
|
|
961
777
|
const allServers = [...extServers, ...servers];
|
|
962
|
-
// Bearer tokens for HTTP MCP servers are injected into codex's process env
|
|
963
|
-
// via extraEnv — codex's `--bearer-token-env-var` only accepts an env name,
|
|
964
|
-
// never a literal token, so the value MUST land in the child env.
|
|
965
778
|
const codexBearerEnv = {};
|
|
966
|
-
// Sequential (codex serializes its own config writes) but async, so the
|
|
967
|
-
// per-server spawns don't block the event loop at stream start.
|
|
968
779
|
for (const server of allServers) {
|
|
969
780
|
const codexArgs = buildCodexMcpAddArgs(server, codexBearerEnv);
|
|
970
781
|
if (!codexArgs)
|
|
@@ -987,8 +798,6 @@ export async function startMcpBridge(opts) {
|
|
|
987
798
|
}
|
|
988
799
|
}
|
|
989
800
|
else if (opts.agent === 'gemini') {
|
|
990
|
-
// Gemini CLI 0.32+ loads MCP servers from settings.json rather than --mcp-config.
|
|
991
|
-
// Include global + workspace extensions alongside built-in servers
|
|
992
801
|
const extServers = getGlobalExtensionsAsServers(opts.workdir);
|
|
993
802
|
const allServers = [...extServers, ...servers];
|
|
994
803
|
configPath = path.join(sessionDir, 'gemini-system-settings.json');
|
|
@@ -998,14 +807,9 @@ export async function startMcpBridge(opts) {
|
|
|
998
807
|
extraEnv = { GEMINI_CLI_SYSTEM_SETTINGS_PATH: configPath };
|
|
999
808
|
}
|
|
1000
809
|
else if (opts.agent === 'hermes') {
|
|
1001
|
-
// Hermes consumes structured MCP server objects via ACP `session/new`,
|
|
1002
|
-
// not a config file path. Resolve the merged server list and expose it
|
|
1003
|
-
// on the bridge handle so the driver can translate to ACP's wire format.
|
|
1004
810
|
mcpServers = mergeExtensionsForSession(servers, opts.workdir);
|
|
1005
811
|
}
|
|
1006
812
|
else {
|
|
1007
|
-
// Claude: write MCP config JSON for --mcp-config
|
|
1008
|
-
// Uses centralized merge: global extensions → .mcp.json files → built-in servers
|
|
1009
813
|
configPath = path.join(sessionDir, 'mcp-config.json');
|
|
1010
814
|
mcpServers = mergeExtensionsForSession(servers, opts.workdir);
|
|
1011
815
|
fs.mkdirSync(sessionDir, { recursive: true });
|