@gleapai/kai-bridge 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/runner/acp-runner.mjs +7 -51
- package/runner/lib/acp/mapper.mjs +1 -31
- package/runner/lib/contract.mjs +1 -16
- package/scripts/postinstall.mjs +7 -0
- package/src/api.mjs +16 -102
- package/src/companions.mjs +3 -2
- package/src/daemon.mjs +607 -1054
- package/src/executor.mjs +2 -2
- package/src/gateway.mjs +217 -0
- package/src/harnesses.mjs +15 -2
- package/src/playwright-patch.mjs +84 -0
- package/src/preview-errors.mjs +0 -29
- package/src/preview.mjs +291 -101
- package/src/service.mjs +0 -11
- package/src/setup.mjs +5 -5
- package/src/tunnel-binary.mjs +109 -0
- package/src/tunnel.mjs +227 -0
- package/src/workspace.mjs +1 -1
- package/runner/personas/claude/kai-verifier.md +0 -84
- package/runner/personas/codex/kai-verifier.md +0 -84
- package/runner/tools/verify-mcp.mjs +0 -442
- package/src/preview-login.mjs +0 -610
- package/src/verify.mjs +0 -387
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Kai Code Bridge
|
|
2
2
|
|
|
3
3
|
Run [Gleap Kai Code](https://gleap.io) on your computer or server with your own
|
|
4
|
-
Claude Code / Codex login, local dev-server previews
|
|
4
|
+
Claude Code / Codex login, and local dev-server previews.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
npm i -g @gleapai/kai-bridge # one install, so the background service has a stable path
|
|
@@ -67,4 +67,4 @@ Updates install into `~/.kai/harnesses`, verify the new executable, then select
|
|
|
67
67
|
|
|
68
68
|
Codex automatically uses device-code sign-in over SSH or on headless Linux. Enable device-code sign-in in the account settings if Codex asks. Claude uses its native `auth login` flow: follow the URL and prompts in the SSH terminal. Dashboard **Sign in** opens a terminal on desktop devices; for headless servers it shows the exact command to run over SSH. The login is picked up automatically.
|
|
69
69
|
|
|
70
|
-
**Previews
|
|
70
|
+
**Previews boot local to the Kai Code Bridge device.** Open them on that computer, or click **Make public** in the dashboard's Preview popover to get a stable HTTPS link (`https://web-<repo>-<id>.gleap-preview.dev`) that works from a phone or a teammate's laptop. Public links run through a Cloudflare Tunnel: the daemon starts `cloudflared` on the device (it uses one from your PATH, else downloads a pinned release into `~/.kai/bin`; `brew install cloudflared`, `apt install cloudflared` or `winget install Cloudflare.cloudflared` also work). The address is the same for every ticket of a repo on this device, so a saved login survives from one ticket to the next, and only one ticket per repo can be public on a device at a time: making another one public points the same link at it and stops the previous preview (its owner sees a notice). Nothing leaves the device until someone clicks Make public. `kai-bridge ps` shows what is running.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gleapai/kai-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@gleapai/kai-bridge",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.10.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gleapai/kai-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kai Code Bridge runs Kai Code on your computer or server with your own coding subscriptions
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Kai Code Bridge runs Kai Code on your computer or server with your own coding subscriptions and local previews.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
package/runner/acp-runner.mjs
CHANGED
|
@@ -28,14 +28,12 @@ import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
|
28
28
|
|
|
29
29
|
import {
|
|
30
30
|
KAI_RESOLUTION_ANALYST_AGENT_NAME,
|
|
31
|
-
KAI_VERIFIER_AGENT_NAME,
|
|
32
31
|
captureRepoBaselines,
|
|
33
32
|
createUsageTracker,
|
|
34
33
|
debugLog,
|
|
35
34
|
emit,
|
|
36
35
|
emitSync,
|
|
37
36
|
isArtifactWriterAgent,
|
|
38
|
-
isReadOnlyAgent,
|
|
39
37
|
parseRunnerArgs,
|
|
40
38
|
revertRepoMutations,
|
|
41
39
|
setTracePrefix,
|
|
@@ -77,10 +75,6 @@ const ENGINE_MODEL = ARGS.engineModel;
|
|
|
77
75
|
const HARNESS_ID = resolveHarnessId(ARGS.argv.harness, MODEL);
|
|
78
76
|
const HARNESS = getHarness(HARNESS_ID);
|
|
79
77
|
const IS_ARTIFACT_WRITER = isArtifactWriterAgent(AGENT);
|
|
80
|
-
// Read-only agents (the verifier): build-mode tool access minus the file
|
|
81
|
-
// write tools, repos reverted after the turn — see contract.mjs.
|
|
82
|
-
const IS_READ_ONLY = isReadOnlyAgent(AGENT);
|
|
83
|
-
const IS_VERIFIER = AGENT === KAI_VERIFIER_AGENT_NAME;
|
|
84
78
|
|
|
85
79
|
if (!TASK) {
|
|
86
80
|
emitSync({ type: "error", message: "runner: missing --task-b64 argument" });
|
|
@@ -89,7 +83,7 @@ if (!TASK) {
|
|
|
89
83
|
|
|
90
84
|
const RUNNER_DIR = dirname(fileURLToPath(import.meta.url));
|
|
91
85
|
const PERSONA_DIR = process.env.KAI_PERSONA_DIR || "/opt/gleap-runners/personas";
|
|
92
|
-
const PERSONA_AGENTS = new Set(["kai-documentarian", "kai-asker", "kai-researcher", KAI_RESOLUTION_ANALYST_AGENT_NAME
|
|
86
|
+
const PERSONA_AGENTS = new Set(["kai-documentarian", "kai-asker", "kai-researcher", KAI_RESOLUTION_ANALYST_AGENT_NAME]);
|
|
93
87
|
const SCRATCH_DIR = join(tmpdir(), `kai-acp-${process.pid}`);
|
|
94
88
|
mkdirSync(SCRATCH_DIR, { recursive: true });
|
|
95
89
|
// Harness config dir lives OUTSIDE the repo tree and survives across
|
|
@@ -132,32 +126,6 @@ const PROMPT_SUGGESTION_WAIT_MS = Math.max(0, Number(process.env.KAI_PROMPT_SUGG
|
|
|
132
126
|
const PROMPT_SUGGESTIONS_ENABLED = process.env.KAI_PROMPT_SUGGESTIONS !== "0" && PROMPT_SUGGESTION_WAIT_MS > 0;
|
|
133
127
|
const TODO_SERVER_KEY = "kai_todos";
|
|
134
128
|
const TODO_MCP_PATH = process.env.KAI_TODO_MCP_PATH || join(RUNNER_DIR, "tools", "todo-mcp.mjs");
|
|
135
|
-
// Verification report bridge (tools/verify-mcp.mjs) — the verifier's
|
|
136
|
-
// verdict channel. Attached ONLY to kai-verifier turns; the mapper turns
|
|
137
|
-
// its `report_verification` calls into `verify_report` contract events.
|
|
138
|
-
const VERIFY_SERVER_KEY = "kai_verify";
|
|
139
|
-
const VERIFY_MCP_PATH = process.env.KAI_VERIFY_MCP_PATH || join(RUNNER_DIR, "tools", "verify-mcp.mjs");
|
|
140
|
-
/**
|
|
141
|
-
* `KAI_VERIFY_*` — the host's request policy for the verify MCP's
|
|
142
|
-
* `http_request` (read-only flag, allowed origins, the app's auth header,
|
|
143
|
-
* the evidence dir for `requests.jsonl`). Passed EXPLICITLY on the server
|
|
144
|
-
* entry: Codex spawns MCP servers with a scrubbed env, and the values must
|
|
145
|
-
* never ride in the prompt (the auth header is a live credential).
|
|
146
|
-
*/
|
|
147
|
-
function verifyMcpEnv() {
|
|
148
|
-
const out = {};
|
|
149
|
-
for (const k of Object.keys(process.env).sort()) {
|
|
150
|
-
if (k.startsWith("KAI_VERIFY_") && k !== "KAI_VERIFY_MCP_PATH" && process.env[k]) out[k] = String(process.env[k]);
|
|
151
|
-
}
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
// The verifier must not write files. Claude's bypassPermissions mode still
|
|
155
|
-
// honours disallowedTools, so the write tools are removed outright (Bash
|
|
156
|
-
// redirections are caught by the post-turn revert).
|
|
157
|
-
const READ_ONLY_DISALLOWED = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
158
|
-
// The verifier tests the app, it never needs the app's secrets: env files
|
|
159
|
-
// and private keys stay unreadable (gitignore-style Read rules).
|
|
160
|
-
const VERIFIER_DISALLOWED = ["Read(**/.env*)", "Read(**/*.pem)"];
|
|
161
129
|
const TODO_NOTE =
|
|
162
130
|
"Track multi-step work as a live todo list with the `todo_write` tool " +
|
|
163
131
|
`from the \`${TODO_SERVER_KEY}\` MCP server: pass the FULL updated list ` +
|
|
@@ -311,9 +279,7 @@ function buildAppendSystemPrompt() {
|
|
|
311
279
|
if (IS_PLAN_MODE) sections.push(NEEDS_ASK_USER_MCP ? GATEWAY_PLAN_GUARD : PLAN_QUESTION_GUARD);
|
|
312
280
|
if (IS_PLAN_MODE) sections.push(PLAN_SCOPE_NOTE);
|
|
313
281
|
if (IS_PLAN_MODE && HARNESS_ID !== "claude") sections.push(READ_ONLY_PLAN_NOTE);
|
|
314
|
-
|
|
315
|
-
// the hand-off note ("the host commits your changes") would contradict it.
|
|
316
|
-
if (!IS_PLAN_MODE && !IS_READ_ONLY) sections.push(GIT_HANDOFF_PROMPT);
|
|
282
|
+
if (!IS_PLAN_MODE) sections.push(GIT_HANDOFF_PROMPT);
|
|
317
283
|
// After the safety guards (their leading position is load-bearing for
|
|
318
284
|
// cursor's prompt-prefix mode) but before project instructions.
|
|
319
285
|
if (!IS_ARTIFACT_WRITER) sections.push(TODO_NOTE);
|
|
@@ -331,9 +297,6 @@ function buildAcpMcpServers() {
|
|
|
331
297
|
if (!IS_ARTIFACT_WRITER && existsSync(TODO_MCP_PATH)) {
|
|
332
298
|
out.push({ name: TODO_SERVER_KEY, command: process.execPath, args: [TODO_MCP_PATH], env: [] });
|
|
333
299
|
}
|
|
334
|
-
if (IS_VERIFIER && existsSync(VERIFY_MCP_PATH)) {
|
|
335
|
-
out.push({ name: VERIFY_SERVER_KEY, command: process.execPath, args: [VERIFY_MCP_PATH], env: Object.entries(verifyMcpEnv()).map(([name, value]) => ({ name, value })) });
|
|
336
|
-
}
|
|
337
300
|
for (const server of MCP_SERVERS || []) {
|
|
338
301
|
if (!server || typeof server !== "object") continue;
|
|
339
302
|
const name = String(server.name || server.id || "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -384,8 +347,6 @@ function buildAgents() {
|
|
|
384
347
|
|
|
385
348
|
function buildDisallowedTools() {
|
|
386
349
|
const list = IS_PLAN_MODE || IS_ARTIFACT_WRITER ? [] : [...GIT_HANDOFF_DISALLOWED];
|
|
387
|
-
if (IS_READ_ONLY) list.push(...READ_ONLY_DISALLOWED);
|
|
388
|
-
if (IS_VERIFIER) list.push(...VERIFIER_DISALLOWED);
|
|
389
350
|
for (const server of MCP_SERVERS || []) {
|
|
390
351
|
const key = String(server?.name || server?.id || "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
391
352
|
for (const tool of [...(server?.disabledTools ?? []), ...(server?.gatedTools ?? [])]) {
|
|
@@ -503,14 +464,12 @@ async function main() {
|
|
|
503
464
|
maxSteps: MAX_STEPS,
|
|
504
465
|
isPlanMode: IS_PLAN_MODE,
|
|
505
466
|
isArtifactWriter: IS_ARTIFACT_WRITER,
|
|
506
|
-
isReadOnly: IS_READ_ONLY,
|
|
507
467
|
appendSystemPrompt,
|
|
508
468
|
instructionsPath,
|
|
509
469
|
disallowedTools: buildDisallowedTools(),
|
|
510
470
|
allowedTools: buildAllowedTools([
|
|
511
471
|
// The injected todo bridge must survive plan mode's allow-list.
|
|
512
472
|
`mcp__${TODO_SERVER_KEY}`,
|
|
513
|
-
...(IS_VERIFIER ? [`mcp__${VERIFY_SERVER_KEY}`] : []),
|
|
514
473
|
...(MCP_SERVERS || []).map((srv) => `mcp__${String(srv?.name || srv?.id || "").replace(/[^a-zA-Z0-9_-]/g, "_")}`).filter((k) => k !== "mcp__"),
|
|
515
474
|
]),
|
|
516
475
|
agents: buildAgents(),
|
|
@@ -527,9 +486,6 @@ async function main() {
|
|
|
527
486
|
...(!IS_ARTIFACT_WRITER && existsSync(TODO_MCP_PATH)
|
|
528
487
|
? [{ name: TODO_SERVER_KEY, command: process.execPath, args: [TODO_MCP_PATH] }]
|
|
529
488
|
: []),
|
|
530
|
-
...(IS_VERIFIER && existsSync(VERIFY_MCP_PATH)
|
|
531
|
-
? [{ name: VERIFY_SERVER_KEY, command: process.execPath, args: [VERIFY_MCP_PATH], env: verifyMcpEnv() }]
|
|
532
|
-
: []),
|
|
533
489
|
...(MCP_SERVERS || []),
|
|
534
490
|
]
|
|
535
491
|
: MCP_SERVERS || [],
|
|
@@ -618,7 +574,7 @@ async function main() {
|
|
|
618
574
|
isPlanMode: IS_PLAN_MODE,
|
|
619
575
|
mcpServerIds,
|
|
620
576
|
readPlanFile: readNewestPlan,
|
|
621
|
-
allowTool: permissionPolicy({ isPlanMode: IS_PLAN_MODE, isArtifactWriter: IS_ARTIFACT_WRITER,
|
|
577
|
+
allowTool: permissionPolicy({ isPlanMode: IS_PLAN_MODE, isArtifactWriter: IS_ARTIFACT_WRITER, workDir: WORK_DIR }),
|
|
622
578
|
onTurnShouldEnd: (reason) => {
|
|
623
579
|
cancelRequested = reason;
|
|
624
580
|
if (conn && acpSessionId) conn.cancel({ sessionId: acpSessionId }).catch(() => {});
|
|
@@ -749,10 +705,10 @@ async function main() {
|
|
|
749
705
|
}
|
|
750
706
|
}
|
|
751
707
|
|
|
752
|
-
// Artifact writers
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
const REVERTS_REPO = IS_ARTIFACT_WRITER
|
|
708
|
+
// Artifact writers must leave the repo untouched: snapshot before, revert
|
|
709
|
+
// anything outside `.kai/` after (belt-and-braces under the permission
|
|
710
|
+
// rules — shell redirections can still write).
|
|
711
|
+
const REVERTS_REPO = IS_ARTIFACT_WRITER;
|
|
756
712
|
const baselines = REVERTS_REPO ? captureRepoBaselines(WORK_DIR) : null;
|
|
757
713
|
// Harnesses without a system-prompt channel (Cursor) get the persona as
|
|
758
714
|
// a prefix of the first prompt.
|
|
@@ -24,19 +24,6 @@ import {
|
|
|
24
24
|
const QUESTION_TOOL = "Question";
|
|
25
25
|
const EXIT_PLAN_TOOL = "ExitPlanMode";
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* The `kai_verify` bridge's `report_verification` (tools/verify-mcp.mjs)
|
|
29
|
-
* in every harness spelling: Claude `mcp__kai_verify__report_verification`,
|
|
30
|
-
* Codex's server-scoped form (rebuilt from `rawInput.server/tool` by
|
|
31
|
-
* toolNameFromUpdate), a bare title. Its call becomes the `verify_report`
|
|
32
|
-
* contract event — never a tool row, and NOT turn-ending (the persona
|
|
33
|
-
* wraps up after reporting).
|
|
34
|
-
*/
|
|
35
|
-
export function isVerifyReportTool(name) {
|
|
36
|
-
return /(^|__|\.)report_verification$/.test(String(name || ""));
|
|
37
|
-
}
|
|
38
|
-
const VERIFY_REPORT_TOOL_NAME = "mcp__kai_verify__report_verification";
|
|
39
|
-
|
|
40
27
|
/** Generic ACP `kind` → a readable tool label when no meta name exists. */
|
|
41
28
|
const KIND_LABEL = {
|
|
42
29
|
read: "Read",
|
|
@@ -75,8 +62,6 @@ export function toolNameFromUpdate(update) {
|
|
|
75
62
|
// the announce leaks a bare "Tool (running)" row that the suppressed
|
|
76
63
|
// completion never clears.
|
|
77
64
|
if (typeof update?.title === "string" && /todo_write/.test(update.title)) return "TodoWrite";
|
|
78
|
-
// Same for the verifier's report bridge.
|
|
79
|
-
if (typeof update?.title === "string" && /report_verification/.test(update.title)) return VERIFY_REPORT_TOOL_NAME;
|
|
80
65
|
if (update?.kind && KIND_LABEL[update.kind]) return KIND_LABEL[update.kind];
|
|
81
66
|
return "Tool";
|
|
82
67
|
}
|
|
@@ -248,16 +233,11 @@ function contentToValue(content) {
|
|
|
248
233
|
* the agent: a mode the adapter fails to apply must not become a free
|
|
249
234
|
* pass for edits.
|
|
250
235
|
*/
|
|
251
|
-
export function permissionPolicy({ isPlanMode = false, isArtifactWriter = false,
|
|
236
|
+
export function permissionPolicy({ isPlanMode = false, isArtifactWriter = false, workDir = "" } = {}) {
|
|
252
237
|
const WRITE_TOOLS = /^(Write|Edit|MultiEdit|NotebookEdit)$/;
|
|
253
238
|
const READONLY_BASH = /^\s*(cat|head|tail|wc|stat|ls|find|grep|rg|git (log|diff|show|status|ls-files|grep|rev-parse|branch)|sed -n|awk|jq|sort|uniq|cut|tr|diff|nl|basename|dirname|realpath|file|echo|pwd|which)\b/;
|
|
254
239
|
const kaiDir = workDir ? `${workDir.replace(/\/+$/, "")}/.kai/` : "/.kai/";
|
|
255
240
|
return (name, input) => {
|
|
256
|
-
// Read-only agents (kai-verifier) keep build-mode tool access — the
|
|
257
|
-
// browser MCP, shell probes and questions must run unprompted — but a
|
|
258
|
-
// file-write tool is never theirs. Shell leaks are caught by the
|
|
259
|
-
// post-turn revert.
|
|
260
|
-
if (isReadOnly && !isPlanMode && !isArtifactWriter) return !WRITE_TOOLS.test(canonicalToolName(name));
|
|
261
241
|
if (!isPlanMode && !isArtifactWriter) return true;
|
|
262
242
|
const canonical = canonicalToolName(name);
|
|
263
243
|
if (WRITE_TOOLS.test(canonical)) {
|
|
@@ -342,16 +322,6 @@ export function createAcpMapper({ emit, isPlanMode = false, onTurnShouldEnd, onC
|
|
|
342
322
|
}
|
|
343
323
|
return;
|
|
344
324
|
}
|
|
345
|
-
if (isVerifyReportTool(meta.name)) {
|
|
346
|
-
// One `verify_report` per call, on completion only — the announce
|
|
347
|
-
// (input still streaming) and the completed update would otherwise
|
|
348
|
-
// hand the host two reports for one filing. Local artifact paths
|
|
349
|
-
// ride in `report`; the host uploads them and rewrites to URLs.
|
|
350
|
-
if (status !== "running" && meta.input && typeof meta.input === "object" && !Array.isArray(meta.input)) {
|
|
351
|
-
emit({ type: "verify_report", message: "Verification report filed", report: sanitizeToolValue(meta.input, 4000) });
|
|
352
|
-
}
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
325
|
// Adapter-provided titles ("List files in 'src'") stand in when the
|
|
356
326
|
// input carries nothing summarizeTool understands.
|
|
357
327
|
const summary = summarizeTool(name, meta.input || {}) || meta.title || "";
|
package/runner/lib/contract.mjs
CHANGED
|
@@ -64,9 +64,6 @@ export function decodeB64Json(value, fallback) {
|
|
|
64
64
|
export const KAI_PLANNER_AGENT_NAME = "kai-planner";
|
|
65
65
|
export const KAI_BUILDER_AGENT_NAME = "kai";
|
|
66
66
|
export const KAI_RESOLUTION_ANALYST_AGENT_NAME = "kai-resolution-analyst";
|
|
67
|
-
// Kai Code "Verify": drives the running preview with the browser tools,
|
|
68
|
-
// records evidence, files a `report_verification`. Read-only by contract.
|
|
69
|
-
export const KAI_VERIFIER_AGENT_NAME = "kai-verifier";
|
|
70
67
|
|
|
71
68
|
export function normalizeAgentName(raw) {
|
|
72
69
|
const value = typeof raw === "string" ? raw : "";
|
|
@@ -121,18 +118,6 @@ export function isArtifactWriterAgent(agentName) {
|
|
|
121
118
|
return ARTIFACT_WRITER_AGENT_NAMES.includes(agentName);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
// Read-only agents produce NO files at all — not even `.kai/` artifacts.
|
|
125
|
-
// The verifier's output is the `report_verification` tool call plus the
|
|
126
|
-
// browser recordings the Playwright MCP writes OUTSIDE the workspace
|
|
127
|
-
// (the host's artifacts dir). They run with build-mode tool access (the
|
|
128
|
-
// browser MCP + questions must work unprompted), so `revertRepoMutations`
|
|
129
|
-
// is what guarantees the repos come out exactly as they went in.
|
|
130
|
-
export const READ_ONLY_AGENT_NAMES = [KAI_VERIFIER_AGENT_NAME];
|
|
131
|
-
|
|
132
|
-
export function isReadOnlyAgent(agentName) {
|
|
133
|
-
return READ_ONLY_AGENT_NAMES.includes(agentName);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
121
|
function findWorkspaceRepos(workDir) {
|
|
137
122
|
const repos = [];
|
|
138
123
|
if (existsSync(join(workDir, ".git"))) {
|
|
@@ -356,7 +341,7 @@ const SUPPORTED_FILE_PART_MIMES = new Set(["application/pdf"]);
|
|
|
356
341
|
* flags are optional except `--task-b64`; callers fail fast on an
|
|
357
342
|
* empty `task` themselves (the error message differs per runner).
|
|
358
343
|
*
|
|
359
|
-
* --agent <name> kai | kai-planner | kai-
|
|
344
|
+
* --agent <name> kai | kai-planner | kai-documentarian | …
|
|
360
345
|
* --model <provider/modelID> canonical registry id
|
|
361
346
|
* --engine-model <slug> engine-native model slug (claude/codex wire form)
|
|
362
347
|
* --subagent-model <provider/modelID> cheaper sibling for explorer subagents
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -16,6 +16,13 @@ try {
|
|
|
16
16
|
} catch {
|
|
17
17
|
// The daemon retries on start; a missing patch only means no suggestions.
|
|
18
18
|
}
|
|
19
|
+
// Raise the browser MCP's 30 s per-call default (see src/playwright-patch.mjs).
|
|
20
|
+
try {
|
|
21
|
+
const { ensurePlaywrightTimeoutPatched } = await import("../src/playwright-patch.mjs");
|
|
22
|
+
ensurePlaywrightTimeoutPatched();
|
|
23
|
+
} catch {
|
|
24
|
+
// The daemon retries on start.
|
|
25
|
+
}
|
|
19
26
|
try {
|
|
20
27
|
const interactive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
|
|
21
28
|
const isGlobal = process.env.npm_config_global === "true";
|
package/src/api.mjs
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
// POST /gleapcode/bridge/devices/me/heartbeat { running: [turnIds] }
|
|
7
7
|
// POST /gleapcode/bridge/turns/:id/events { events: [contract lines] }
|
|
8
8
|
// POST /gleapcode/bridge/turns/:id/result { result, changes, status }
|
|
9
|
-
// POST /gleapcode/bridge/
|
|
10
|
-
// POST /gleapcode/bridge/turns/:id/verification{ status, scope, reason, checks, untested, artifacts, evidence?, revisions? }
|
|
11
|
-
// PUT /gleapcode/bridge/turns/:id/verification/artifacts { artifacts, evidence } (evidence retry — union by URL, clears evidenceMissing)
|
|
12
|
-
// POST /gleapcode/bridge/turns/:id/verification/stage { stage, note? } (setup|booting|login|verifying|fixing|saving)
|
|
13
|
-
// GET /gleapcode/bridge/turns/:id/preview-login → the saved sign-in record for this verify turn (one GET per turn)
|
|
14
|
-
// POST /gleapcode/bridge/preview-logins/:requestId { storageState, origins, services, landingUrl, loginPaths } (capture upload)
|
|
15
|
-
// POST /gleapcode/bridge/preview-logins/:requestId/status { status: opened|waiting_signin|detected|failed|cancelled, error? }
|
|
16
|
-
// PUT /gleapcode/bridge/preview-logins/:id { storageState, basedOnVersion } → 409 on a stale version
|
|
9
|
+
// POST /gleapcode/bridge/devices/me/public-hosts { sessionId, services } → { domain, hosts, tunnel, displaced }
|
|
17
10
|
// POST /users/me/pusher { socket_id, channel_name } (channel auth)
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The human-readable part of a Server error body. `statusOr500` answers
|
|
14
|
+
* `{ ok: false, message }`; the global handler answers `{ error: { message,
|
|
15
|
+
* details } }` — reading `error` itself printed "[object Object]".
|
|
16
|
+
*/
|
|
17
|
+
export function errorMessage(data, text = "") {
|
|
18
|
+
const pick = (v) => (typeof v === "string" && v ? v : null);
|
|
19
|
+
return pick(data?.message) ?? pick(data?.error?.message) ?? pick(data?.error) ?? pick(text.slice(0, 200)) ?? "no error body";
|
|
20
|
+
}
|
|
21
21
|
|
|
22
22
|
export class BridgeApi {
|
|
23
23
|
constructor({ apiBase, token, fetchImpl = fetch }) {
|
|
@@ -51,7 +51,7 @@ export class BridgeApi {
|
|
|
51
51
|
data = { raw: text };
|
|
52
52
|
}
|
|
53
53
|
if (!res.ok) {
|
|
54
|
-
const err = new Error(`${method} ${path} → ${res.status}: ${data
|
|
54
|
+
const err = new Error(`${method} ${path} → ${res.status}: ${errorMessage(data, text)}`);
|
|
55
55
|
err.status = res.status;
|
|
56
56
|
err.data = data;
|
|
57
57
|
throw err;
|
|
@@ -116,97 +116,7 @@ export class BridgeApi {
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/**
|
|
120
|
-
* Upload one verify artifact (recording / screenshot / trace) as
|
|
121
|
-
* multipart `file`; resolves the Server's `{ url }`. Separate from the
|
|
122
|
-
* JSON-only `request()`: the body is a FormData (fetch sets the boundary
|
|
123
|
-
* header itself — never set content-type here), a video can take a
|
|
124
|
-
* while (5-min cap), and it retries transient failures 3× — a dropped
|
|
125
|
-
* screenshot is missing evidence, not a broken turn.
|
|
126
|
-
*/
|
|
127
|
-
async uploadArtifact(turnId, filePath, contentType, { tries = 3, timeoutMs = 5 * 60_000, onRetry } = {}) {
|
|
128
|
-
const bytes = await readFile(filePath);
|
|
129
|
-
const path = `/gleapcode/bridge/turns/${encodeURIComponent(turnId)}/artifacts`;
|
|
130
|
-
let delay = 1_000;
|
|
131
|
-
for (let attempt = 1; ; attempt += 1) {
|
|
132
|
-
try {
|
|
133
|
-
const form = new FormData();
|
|
134
|
-
form.append("file", new Blob([bytes], { type: contentType }), basename(filePath));
|
|
135
|
-
const res = await this.fetch(`${this.apiBase}${path}`, {
|
|
136
|
-
method: "POST",
|
|
137
|
-
headers: { ...(this.token ? { authorization: `Bearer ${this.token}` } : {}) },
|
|
138
|
-
body: form,
|
|
139
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
140
|
-
});
|
|
141
|
-
const text = await res.text();
|
|
142
|
-
let data = null;
|
|
143
|
-
try {
|
|
144
|
-
data = text ? JSON.parse(text) : null;
|
|
145
|
-
} catch {
|
|
146
|
-
data = { raw: text };
|
|
147
|
-
}
|
|
148
|
-
if (!res.ok) {
|
|
149
|
-
const err = new Error(`POST ${path} → ${res.status}: ${data?.message ?? data?.error ?? text.slice(0, 200)}`);
|
|
150
|
-
err.status = res.status;
|
|
151
|
-
err.data = data;
|
|
152
|
-
throw err;
|
|
153
|
-
}
|
|
154
|
-
if (typeof data?.url !== "string" || !data.url) throw new Error(`POST ${path} → no url in response`);
|
|
155
|
-
return data;
|
|
156
|
-
} catch (err) {
|
|
157
|
-
const permanent = err.status >= 400 && err.status < 500 && err.status !== 429;
|
|
158
|
-
if (permanent || attempt >= tries) throw err;
|
|
159
|
-
onRetry?.(err, attempt, delay);
|
|
160
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
161
|
-
delay = Math.min(delay * 2, 30_000);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** The verification report for a verify turn (artifacts already uploaded → URLs). */
|
|
167
|
-
turnVerification(turnId, payload, opts) {
|
|
168
|
-
return this.requestWithRetry("POST", `/gleapcode/bridge/turns/${encodeURIComponent(turnId)}/verification`, payload, opts);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Late evidence (`bridge.verify.evidence.retry`): the Server unions by URL and clears `evidenceMissing`. */
|
|
172
|
-
putVerificationArtifacts(turnId, payload, opts) {
|
|
173
|
-
return this.requestWithRetry("PUT", `/gleapcode/bridge/turns/${encodeURIComponent(turnId)}/verification/artifacts`, payload, opts);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Stage transition of a running verify turn (best-effort UI state — callers swallow errors). */
|
|
177
|
-
turnVerificationStage(turnId, payload) {
|
|
178
|
-
return this.request("POST", `/gleapcode/bridge/turns/${encodeURIComponent(turnId)}/verification/stage`, payload);
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* The saved sign-in for this verify turn: `{ id, version, storageState,
|
|
182
|
-
* origins, services, landingUrl, loginPaths, optional }` (or `null` —
|
|
183
|
-
* a 404 is "no record", not an error). The Server allows ONE successful
|
|
184
|
-
* GET per turn and never caches it.
|
|
185
|
-
*/
|
|
186
|
-
async turnPreviewLogin(turnId) {
|
|
187
|
-
try {
|
|
188
|
-
const data = await this.request("GET", `/gleapcode/bridge/turns/${encodeURIComponent(turnId)}/preview-login`);
|
|
189
|
-
const record = data && typeof data === "object" && "record" in data ? data.record : data;
|
|
190
|
-
return record && typeof record === "object" && record.storageState ? record : null;
|
|
191
|
-
} catch (err) {
|
|
192
|
-
if (err?.status === 404) return null;
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/** Capture upload against a pending login request (the ONLY thing a device may upload a sign-in against). */
|
|
197
|
-
uploadPreviewLogin(requestId, payload, opts) {
|
|
198
|
-
return this.requestWithRetry("POST", `/gleapcode/bridge/preview-logins/${encodeURIComponent(requestId)}`, payload, opts);
|
|
199
|
-
}
|
|
200
|
-
/** Capture progress for the dashboard's sign-in card. */
|
|
201
|
-
previewLoginStatus(requestId, payload) {
|
|
202
|
-
return this.request("POST", `/gleapcode/bridge/preview-logins/${encodeURIComponent(requestId)}/status`, payload);
|
|
203
|
-
}
|
|
204
|
-
/** Refresh a record after a verify run (CAS on `basedOnVersion`; 409 = someone else refreshed first → discard). */
|
|
205
|
-
refreshPreviewLogin(recordId, payload) {
|
|
206
|
-
return this.request("PUT", `/gleapcode/bridge/preview-logins/${encodeURIComponent(recordId)}`, payload);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Turns (and pending sign-in requests) the server still believes this device is running. */
|
|
119
|
+
/** Turns the server still believes this device is running. */
|
|
210
120
|
pendingTurns() {
|
|
211
121
|
return this.request("GET", "/gleapcode/bridge/devices/me/pending");
|
|
212
122
|
}
|
|
@@ -217,6 +127,10 @@ export class BridgeApi {
|
|
|
217
127
|
gitCredentials(repoKey) {
|
|
218
128
|
return this.request("POST", "/gleapcode/bridge/devices/me/git-credentials", { repoKey });
|
|
219
129
|
}
|
|
130
|
+
/** Public hostnames + tunnel credentials for a session being published (the Server decides displacement). */
|
|
131
|
+
publicHosts(payload) {
|
|
132
|
+
return this.request("POST", "/gleapcode/bridge/devices/me/public-hosts", payload);
|
|
133
|
+
}
|
|
220
134
|
|
|
221
135
|
commandAck(commandId, payload) {
|
|
222
136
|
return this.request("POST", `/gleapcode/bridge/commands/${encodeURIComponent(commandId)}/ack`, payload);
|
package/src/companions.mjs
CHANGED
|
@@ -69,7 +69,7 @@ export function orderCompanions(entries) {
|
|
|
69
69
|
* repo required by any config is required overall; depth is capped at
|
|
70
70
|
* `maxDepth`; a visited set keeps cycles harmless.
|
|
71
71
|
*/
|
|
72
|
-
export async function collectCompanions({ roots, loadConfig, maxDepth = 3 } = {}) {
|
|
72
|
+
export async function collectCompanions({ roots, loadConfig, maxDepth = 3, extra = [] } = {}) {
|
|
73
73
|
const sessionKeys = new Set((roots || []).map((r) => String(r.key).toLowerCase()));
|
|
74
74
|
const seen = new Map(); // key → entry
|
|
75
75
|
let queue = (roots || []).map((r) => ({ key: String(r.key).toLowerCase(), config: r.config, depth: 0 }));
|
|
@@ -77,7 +77,8 @@ export async function collectCompanions({ roots, loadConfig, maxDepth = 3 } = {}
|
|
|
77
77
|
for (let depth = 1; depth <= maxDepth && queue.length > 0; depth += 1) {
|
|
78
78
|
const next = [];
|
|
79
79
|
for (const node of queue) {
|
|
80
|
-
|
|
80
|
+
// `extra`: companions the daemon inferred (reverse companions) count as declared by the session repos.
|
|
81
|
+
for (const c of [...(node.config?.companions || []), ...(node.depth === 0 ? extra : [])]) {
|
|
81
82
|
const key = String(c.repo).toLowerCase();
|
|
82
83
|
if (sessionKeys.has(key)) continue; // already part of the session
|
|
83
84
|
const prev = seen.get(key);
|