@gleapai/kai-bridge 0.2.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 ADDED
@@ -0,0 +1,45 @@
1
+ # kai-bridge
2
+
3
+ Run [Gleap Kai Code](https://gleap.io) sessions on your own machine — with your own
4
+ Claude Code / Codex login — and preview your real dev servers from the dashboard or the
5
+ Gleap app on your phone.
6
+
7
+ ```bash
8
+ npm i -g @gleapai/kai-bridge # one install, so the background service has a stable path
9
+ kai-bridge login # pair this machine with your Gleap account
10
+ kai-bridge install # run at login (launchd / systemd / Task Scheduler)
11
+ kai-bridge status
12
+ ```
13
+
14
+ (`npx @gleapai/kai-bridge login` works for a quick look, but `install` needs the
15
+ global install — npm garbage-collects the npx cache, which would leave the
16
+ background service pointing at a deleted file.)
17
+
18
+ Then pick **Run on → your machine** in any Kai Code session. Repos are discovered
19
+ automatically under `~/Documents`, `~/code`, `~/dev`, `~/Projects`, `~/src` (add more with
20
+ `kai-bridge repo roots add <dir>`); each session runs in an isolated worktree under
21
+ `~/.kai/worktrees` unless you choose "Work in my checkout".
22
+
23
+ ## Accounts
24
+
25
+ The bridge runs the official `claude` / `codex` binaries installed on your machine under
26
+ **your** login (`ambient` profile). Add a second account with a managed profile:
27
+
28
+ ```bash
29
+ kai-bridge profile add personal --harness claude
30
+ kai-bridge profile login personal
31
+ ```
32
+
33
+ Your harness logins never leave the machine; the bridge stores only its pairing token
34
+ (`~/.kai/config.json`).
35
+
36
+ ## Local dry run
37
+
38
+ ```bash
39
+ kai-bridge run --task "Explain the auth flow in this repo" --cwd ~/code/my-app
40
+ ```
41
+
42
+ ## Development
43
+
44
+ `npm run sync-runner` copies the ACP runner from a sibling `gleap_code_analyzer` checkout;
45
+ `npm test` runs the unit suite (uses real `git`).
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ // kai-bridge — run Gleap Kai Code on this machine.
3
+ //
4
+ // kai-bridge setup guided onboarding: pair · background service · harness sign-ins
5
+ // (a bare `kai-bridge` on an unpaired machine runs it too)
6
+ // kai-bridge login | logout pair this device with your Gleap account / disconnect it
7
+ // kai-bridge install | uninstall run at login (launchd / systemd / Task Scheduler)
8
+ // kai-bridge start run in the foreground (what the service runs)
9
+ // kai-bridge status device, service, profiles, repos
10
+ // kai-bridge harness list|install <id>|login <id> Claude Code + Codex are bundled; Cursor is downloaded
11
+ // kai-bridge profile list|add <id> --harness claude|codex|cursor [--label …]|login <id>|remove <id>
12
+ // kai-bridge repo scan|roots [add <dir>|remove <dir>]|primary <repoKey> <path>
13
+ // kai-bridge run --task "…" [--model …] [--profile …] [--cwd …] (local turn, no server)
14
+ // kai-bridge doctor
15
+
16
+ import { spawn } from "node:child_process";
17
+ import { mkdtempSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ import { KAI_HOME, loadConfig, saveConfig } from "../src/config.mjs";
23
+ import { logout, pairDevice, runSetup } from "../src/setup.mjs";
24
+ import { BridgeDaemon, createLogger, resolveProfiles } from "../src/daemon.mjs";
25
+ import { runTurn } from "../src/executor.mjs";
26
+ import { HARNESSES, createManagedProfile, describeProfiles, findBinary, loginCommand } from "../src/profiles.mjs";
27
+ import { defaultRoots, groupByRepo, scanRoots, toDeviceRepoReport } from "../src/repos.mjs";
28
+ import { install, isInstalled, uninstall, isEphemeralBinPath } from "../src/service.mjs";
29
+ import { HARNESS_INFO, describeHarnesses, installHarness } from "../src/harnesses.mjs";
30
+
31
+ const BIN = fileURLToPath(import.meta.url);
32
+ const argv = process.argv.slice(2);
33
+ const cmd = argv[0];
34
+ const flags = {};
35
+ const positional = [];
36
+ for (let i = 1; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a.startsWith("--")) {
39
+ const [k, v] = a.slice(2).split("=");
40
+ flags[k] = v ?? (argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true);
41
+ } else positional.push(a);
42
+ }
43
+ const out = (s) => process.stdout.write(s + "\n");
44
+ const fail = (s) => {
45
+ process.stderr.write(`error: ${s}\n`);
46
+ process.exit(1);
47
+ };
48
+
49
+ async function login() {
50
+ const config = loadConfig();
51
+ await pairDevice({ config, name: flags.name, print: out });
52
+ out(`Next: \`kai-bridge install\` keeps it running at login. \`kai-bridge start\` runs it right now.`);
53
+ }
54
+
55
+ async function status() {
56
+ const config = loadConfig();
57
+ out(`home: ${KAI_HOME}`);
58
+ out(`device: ${config.device ? `${config.device.name} (${config.device.id})` : "not paired — run `kai-bridge login`"}`);
59
+ out(`service: ${isInstalled() ? "installed" : "not installed — run `kai-bridge install`"}`);
60
+ out("harnesses:");
61
+ for (const h of describeHarnesses(KAI_HOME)) out(` ${h.id.padEnd(8)} ${h.installed ? `installed ${h.version ?? ""}` : "not installed — kai-bridge harness install " + h.id}${h.bundled ? " (bundled)" : ""}`);
62
+ const profiles = await describeProfiles(resolveProfiles(config));
63
+ out("profiles:");
64
+ for (const p of profiles) out(` ${p.id.padEnd(18)} ${p.harness.padEnd(7)} ${p.authState.padEnd(14)} ${p.account ?? ""} ${p.version ? `(${p.version})` : ""}`);
65
+ const groups = groupByRepo(scanRoots([...defaultRoots(), ...(config.roots || [])]), config.primaryOverrides);
66
+ out(`repos (${groups.length}):`);
67
+ for (const r of toDeviceRepoReport(groups)) out(` ${r.key.padEnd(48)} ${r.primaryPath}${r.checkouts > 1 ? ` (+${r.checkouts - 1})` : ""} ${r.dirtyCount ? `· ${r.dirtyCount} dirty` : ""}`);
68
+ }
69
+
70
+ async function profile() {
71
+ const config = loadConfig();
72
+ const sub = positional[0];
73
+ if (sub === "list" || !sub) {
74
+ for (const p of await describeProfiles(resolveProfiles(config))) out(`${p.id}\t${p.harness}\t${p.kind}\t${p.authState}\t${p.account ?? ""}`);
75
+ return;
76
+ }
77
+ if (sub === "add") {
78
+ const id = positional[1];
79
+ const harness = flags.harness;
80
+ if (!id || !HARNESSES.includes(harness)) fail("usage: profile add <id> --harness claude|codex [--label …]");
81
+ if (config.profiles.some((p) => p.id === id)) fail(`profile ${id} exists`);
82
+ createManagedProfile(harness, id, KAI_HOME);
83
+ config.profiles.push({ id, harness, kind: "managed", label: flags.label || `${harness} (${id})` });
84
+ saveConfig(config);
85
+ out(`Added managed profile ${id}. Sign in with: kai-bridge profile login ${id}`);
86
+ return;
87
+ }
88
+ if (sub === "login") {
89
+ const p = resolveProfiles(config).find((x) => x.id === positional[1]);
90
+ if (!p) fail("unknown profile");
91
+ const c = loginCommand(p.harness, p.configDir);
92
+ if (!c) fail(`${p.harness} is not installed`);
93
+ const child = spawn(c.cmd, c.args, { env: c.env, stdio: "inherit" });
94
+ child.on("close", (code) => process.exit(code ?? 0));
95
+ return;
96
+ }
97
+ if (sub === "remove") {
98
+ config.profiles = config.profiles.filter((p) => p.id !== positional[1]);
99
+ saveConfig(config);
100
+ out("removed (the login dir is kept under ~/.kai/accounts)");
101
+ return;
102
+ }
103
+ fail("usage: profile list|add|login|remove");
104
+ }
105
+
106
+ async function harness() {
107
+ const sub = positional[0] || "list";
108
+ if (sub === "list") {
109
+ for (const h of describeHarnesses(KAI_HOME)) out(`${h.id.padEnd(8)} ${h.label.padEnd(12)} ${h.installed ? "installed" : "not installed"} ${h.version ?? ""}${h.bundled ? " (bundled)" : ""}${h.localOnly ? " · local only" : ""}`);
110
+ return;
111
+ }
112
+ const id = positional[1];
113
+ if (!HARNESS_INFO[id]) fail("usage: harness list|install <claude|codex|cursor>|login <id>");
114
+ if (sub === "install" || sub === "update") {
115
+ const res = await installHarness(id, { kaiHome: KAI_HOME, onLog: out });
116
+ if (!res.ok) fail(`${id} is not usable`);
117
+ out(`${HARNESS_INFO[id].label} ${res.version} → ${res.binary}`);
118
+ return;
119
+ }
120
+ if (sub === "login") {
121
+ positional[1] = `${id}-ambient`;
122
+ positional[0] = "login";
123
+ return profile();
124
+ }
125
+ fail("usage: harness list|install <id>|login <id>");
126
+ }
127
+
128
+ async function repo() {
129
+ const config = loadConfig();
130
+ const sub = positional[0] || "scan";
131
+ if (sub === "scan") {
132
+ const groups = groupByRepo(scanRoots([...defaultRoots(), ...(config.roots || [])]), config.primaryOverrides);
133
+ for (const r of toDeviceRepoReport(groups)) out(`${r.key}\t${r.primaryPath}\t${r.branch}`);
134
+ return;
135
+ }
136
+ if (sub === "roots") {
137
+ if (positional[1] === "add") {
138
+ config.roots = [...new Set([...(config.roots || []), resolve(positional[2])])];
139
+ saveConfig(config);
140
+ } else if (positional[1] === "remove") {
141
+ config.roots = (config.roots || []).filter((r) => r !== resolve(positional[2]));
142
+ saveConfig(config);
143
+ }
144
+ for (const r of [...defaultRoots(), ...(config.roots || [])]) out(r);
145
+ return;
146
+ }
147
+ if (sub === "primary") {
148
+ config.primaryOverrides = { ...(config.primaryOverrides || {}), [positional[1]]: resolve(positional[2]) };
149
+ saveConfig(config);
150
+ out("ok");
151
+ return;
152
+ }
153
+ fail("usage: repo scan|roots [add|remove <dir>]|primary <repoKey> <path>");
154
+ }
155
+
156
+ async function runLocal() {
157
+ if (!flags.task) fail("usage: run --task \"…\" [--model anthropic/claude-sonnet-4-6] [--profile claude-ambient] [--cwd .]");
158
+ const config = loadConfig();
159
+ const profile = resolveProfiles(config).find((p) => p.id === (flags.profile || "claude-ambient"));
160
+ if (!profile) fail("unknown profile");
161
+ const model = flags.model || (profile.harness === "codex" ? "openai/gpt-5.5" : "anthropic/claude-sonnet-4-6");
162
+ const cwd = resolve(flags.cwd || process.cwd());
163
+ const res = await runTurn({
164
+ turn: { task: flags.task, model, engineModelSlug: model.split("/").slice(1).join("/"), effort: flags.effort, agent: flags.agent, harness: profile.harness },
165
+ profile,
166
+ workDir: cwd,
167
+ onEvent: (ev) => {
168
+ if (ev.type === "text" || ev.type === "plan") out(`\n${ev.message}`);
169
+ else if (ev.type === "tool_status" && ev.toolStatus === "running") out(`▸ ${ev.message}`);
170
+ else if (ev.type === "error") process.stderr.write(`✖ ${ev.message}\n`);
171
+ else if (ev.type === "result") out(`\n— ${ev.inputTokens ?? 0} in / ${ev.outputTokens ?? 0} out${ev.costUsd != null ? ` · $${ev.costUsd.toFixed(4)}` : ""} · session ${ev.sessionId}`);
172
+ },
173
+ onLog: (l) => {
174
+ if (process.env.KAI_RUNNER_DEBUG === "1") process.stderr.write(l);
175
+ },
176
+ });
177
+ process.exit(res.code ?? 0);
178
+ }
179
+
180
+ async function doctor() {
181
+ out(`node: ${process.version}`);
182
+ out(`claude: ${findBinary("claude") ?? "not found (npm i -g @anthropic-ai/claude-code)"}`);
183
+ out(`codex: ${findBinary("codex") ?? "not found (npm i -g @openai/codex)"}`);
184
+ out(`git: ${findBinary("git") ?? "not found"}`);
185
+ out(`home: ${KAI_HOME}`);
186
+ await status();
187
+ }
188
+
189
+ try {
190
+ switch (cmd) {
191
+ case "login":
192
+ await login();
193
+ break;
194
+ case "setup":
195
+ await runSetup({ binPath: BIN });
196
+ break;
197
+ case "logout":
198
+ await logout();
199
+ break;
200
+ case "install": {
201
+ if (isEphemeralBinPath(BIN)) {
202
+ fail(
203
+ "Running from npx, so there's no stable path to install from.\n" +
204
+ "Install it once, then this works:\n\n" +
205
+ " npm i -g @gleapai/kai-bridge\n kai-bridge install\n",
206
+ );
207
+ }
208
+ const res = install({ binPath: BIN, logDir: join(KAI_HOME, "logs") });
209
+ out(`Installed (${res.kind}): ${res.path}. It runs at login and restarts if it crashes.`);
210
+ break;
211
+ }
212
+ case "uninstall":
213
+ out(uninstall() ? "Removed." : "Nothing to remove.");
214
+ break;
215
+ case "start": {
216
+ process.env.KAI_BRIDGE_FOREGROUND = process.stdout.isTTY ? "1" : "0";
217
+ const daemon = new BridgeDaemon({ log: createLogger() });
218
+ await daemon.start();
219
+ const stop = () => {
220
+ // Give in-flight turns a moment to post their result before the
221
+ // process dies; a hard exit here is what left sessions spinning.
222
+ daemon.stop();
223
+ setTimeout(() => process.exit(0), 3_000).unref?.();
224
+ };
225
+ process.on("SIGINT", stop);
226
+ process.on("SIGTERM", stop);
227
+ break;
228
+ }
229
+ case "status":
230
+ await status();
231
+ break;
232
+ case "profile":
233
+ await profile();
234
+ break;
235
+ case "harness":
236
+ await harness();
237
+ break;
238
+ case "repo":
239
+ await repo();
240
+ break;
241
+ case "run":
242
+ await runLocal();
243
+ break;
244
+ case "doctor":
245
+ await doctor();
246
+ break;
247
+ default: {
248
+ // A bare `kai-bridge` in a terminal IS the setup — first run pairs,
249
+ // later runs open with keep / reconnect / log out, so it doubles as
250
+ // "manage this machine". Help lives on `kai-bridge help` (and on any
251
+ // non-TTY invocation, where a wizard could never prompt).
252
+ if (!cmd && process.stdin.isTTY) {
253
+ await runSetup({ binPath: BIN });
254
+ break;
255
+ }
256
+ out(`kai-bridge — run Gleap Kai Code on this machine
257
+
258
+ setup guided onboarding (pair · service · sign-ins)
259
+ login | logout | install | uninstall | start | status | doctor
260
+ harness list|install <id>|login <id>
261
+ profile list|add <id> --harness claude|codex|cursor|login <id>|remove <id>
262
+ repo scan|roots [add|remove <dir>]|primary <repoKey> <path>
263
+ run --task "…" [--model …] [--profile …] [--cwd …]`);
264
+ }
265
+ }
266
+ } catch (err) {
267
+ // Revoked pairing under the background service: exit 0 so launchd
268
+ // records a deliberate stop instead of crash-restarting into an
269
+ // endless loop of 403s. Interactively it stays a visible failure.
270
+ if (err?.code === "REVOKED" && !process.stdout.isTTY) {
271
+ process.stderr.write(`${err.message}\n`);
272
+ process.exit(0);
273
+ }
274
+ fail(err?.message ?? String(err));
275
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@gleapai/kai-bridge",
3
+ "version": "0.2.0",
4
+ "description": "Run Gleap Kai Code sessions on your own machine with your own Claude Code / Codex login — and preview your real dev servers from the dashboard or the phone.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "kai-bridge": "bin/kai-bridge.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "runner",
14
+ "scripts/postinstall.mjs",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test 'test/**/*.test.mjs'",
22
+ "sync-runner": "node scripts/sync-runner.mjs",
23
+ "postinstall": "node scripts/postinstall.mjs"
24
+ },
25
+ "dependencies": {
26
+ "@agentclientprotocol/claude-agent-acp": "0.70.0",
27
+ "@agentclientprotocol/codex-acp": "1.6.2",
28
+ "@agentclientprotocol/sdk": "1.4.0",
29
+ "@playwright/mcp": "^0.0.79",
30
+ "@sockudo/client": "^2.0.0",
31
+ "chokidar": "^4.0.1",
32
+ "yaml": "^2.9.0"
33
+ },
34
+ "overrides": {
35
+ "@anthropic-ai/claude-agent-sdk": "0.3.245"
36
+ },
37
+ "exports": {
38
+ ".": "./src/daemon.mjs",
39
+ "./src/*": "./src/*",
40
+ "./runner/*": "./runner/*"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/GleapSDK/kai-bridge.git"
45
+ },
46
+ "homepage": "https://gleap.io"
47
+ }