@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 +45 -0
- package/bin/kai-bridge.mjs +275 -0
- package/package.json +47 -0
- package/runner/acp-runner.mjs +671 -0
- package/runner/lib/acp/harnesses.mjs +342 -0
- package/runner/lib/acp/mapper.mjs +575 -0
- package/runner/lib/acp/transcripts.mjs +238 -0
- package/runner/lib/contract.mjs +1122 -0
- package/runner/lib/wire-proxy.mjs +200 -0
- package/runner/personas/claude/kai-asker.md +69 -0
- package/runner/personas/claude/kai-doc-explorer.md +130 -0
- package/runner/personas/claude/kai-documentarian.md +205 -0
- package/runner/personas/claude/kai-researcher.md +68 -0
- package/runner/personas/claude/kai-resolution-analyst.md +164 -0
- package/runner/personas/codex/kai-asker.md +68 -0
- package/runner/personas/codex/kai-doc-explorer.md +130 -0
- package/runner/personas/codex/kai-documentarian.md +211 -0
- package/runner/personas/codex/kai-researcher.md +67 -0
- package/runner/personas/codex/kai-resolution-analyst.md +164 -0
- package/runner/tools/ask-user-mcp.mjs +130 -0
- package/runner/tools/todo-mcp.mjs +116 -0
- package/scripts/postinstall.mjs +24 -0
- package/src/api.mjs +141 -0
- package/src/config.mjs +76 -0
- package/src/daemon.mjs +904 -0
- package/src/executor.mjs +156 -0
- package/src/harnesses.mjs +252 -0
- package/src/preview.mjs +337 -0
- package/src/profiles.mjs +250 -0
- package/src/repos.mjs +182 -0
- package/src/service.mjs +162 -0
- package/src/setup.mjs +261 -0
- package/src/workspace.mjs +175 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Where a session's changes land on the device.
|
|
2
|
+
//
|
|
3
|
+
// worktree (default) — `~/.kai/worktrees/<repo>/<session-slug>` created
|
|
4
|
+
// from a fresh `origin/<base>`; identical to the
|
|
5
|
+
// cloud flow, never touches the user's checkout.
|
|
6
|
+
// Optionally starts from the checkout's uncommitted
|
|
7
|
+
// changes (applied as a patch).
|
|
8
|
+
// local — run in place on the checkout's current branch; the
|
|
9
|
+
// dashboard shows the dirty-file count first.
|
|
10
|
+
//
|
|
11
|
+
// Multi-repo sessions bind each repo independently (one may be local,
|
|
12
|
+
// another a worktree). Git ops ported from the retired desktop runtime's
|
|
13
|
+
// git-checkout-ops.mjs in spirit: plain `git` CLI, no libraries.
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
function git(cwd, args, opts = {}) {
|
|
20
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...opts }).trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sessionSlug(sessionId, title) {
|
|
24
|
+
const t = String(title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
25
|
+
const id = String(sessionId || "").slice(-8);
|
|
26
|
+
return t ? `${t}-${id}` : `session-${id}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function worktreePath(kaiHome, repoName, slug) {
|
|
30
|
+
return join(kaiHome, "worktrees", repoName, slug);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Copy the primary checkout's local env files (.env, .env.local, …) into a
|
|
35
|
+
* worktree when the worktree doesn't have its own. They're gitignored, so a
|
|
36
|
+
* fresh worktree never carries them — and an env-dependent app (the Gleap
|
|
37
|
+
* dashboard, most real services) boots blank without them. Same machine,
|
|
38
|
+
* same user, never overwrites: safe by construction.
|
|
39
|
+
*/
|
|
40
|
+
export function copyPrimaryEnvFiles(primaryPath, cwd) {
|
|
41
|
+
if (!primaryPath || !cwd || primaryPath === cwd) return [];
|
|
42
|
+
const copied = [];
|
|
43
|
+
let names = [];
|
|
44
|
+
try {
|
|
45
|
+
names = readdirSync(primaryPath).filter((n) => n === ".env" || n.startsWith(".env."));
|
|
46
|
+
} catch {
|
|
47
|
+
return copied;
|
|
48
|
+
}
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
const src = join(primaryPath, name);
|
|
51
|
+
const dst = join(cwd, name);
|
|
52
|
+
try {
|
|
53
|
+
if (!statSync(src).isFile()) continue;
|
|
54
|
+
if (existsSync(dst)) continue;
|
|
55
|
+
copyFileSync(src, dst);
|
|
56
|
+
copied.push(name);
|
|
57
|
+
} catch {
|
|
58
|
+
/* best-effort */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return copied;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Materialise one repo binding. Returns `{ cwd, mode, branch, base }`.
|
|
66
|
+
* `repo` = `{ name, primaryPath, defaultBranch }`, `binding` = `{ mode, base?, carryUncommitted? }`.
|
|
67
|
+
*/
|
|
68
|
+
export function materializeBinding({ kaiHome, repo, binding, sessionId, title, branchPrefix = "kai" }) {
|
|
69
|
+
const mode = binding?.mode === "local" ? "local" : "worktree";
|
|
70
|
+
if (mode === "local") {
|
|
71
|
+
const branch = git(repo.primaryPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
72
|
+
return { cwd: repo.primaryPath, mode, branch, base: branch };
|
|
73
|
+
}
|
|
74
|
+
const base = binding?.base || repo.defaultBranch || "main";
|
|
75
|
+
const slug = sessionSlug(sessionId, title);
|
|
76
|
+
const dir = worktreePath(kaiHome, repo.name, slug);
|
|
77
|
+
const branch = `${branchPrefix}/${slug}`;
|
|
78
|
+
if (existsSync(dir)) {
|
|
79
|
+
// Resume: the worktree from the previous turn is the session state.
|
|
80
|
+
return { cwd: dir, mode, branch, base, resumed: true };
|
|
81
|
+
}
|
|
82
|
+
mkdirSync(join(kaiHome, "worktrees", repo.name), { recursive: true });
|
|
83
|
+
git(repo.primaryPath, ["fetch", "origin", base, "--quiet"]);
|
|
84
|
+
git(repo.primaryPath, ["worktree", "add", "-b", branch, dir, `origin/${base}`]);
|
|
85
|
+
if (binding?.carryUncommitted) {
|
|
86
|
+
// Tracked changes as a patch; untracked (non-ignored) files copied.
|
|
87
|
+
const patch = git(repo.primaryPath, ["diff", "HEAD"], { maxBuffer: 64 * 1024 * 1024 });
|
|
88
|
+
if (patch) {
|
|
89
|
+
const patchPath = join(dir, ".kai-carry.patch");
|
|
90
|
+
writeFileSync(patchPath, patch + "\n");
|
|
91
|
+
try {
|
|
92
|
+
git(dir, ["apply", "--3way", patchPath]);
|
|
93
|
+
} finally {
|
|
94
|
+
rmSync(patchPath, { force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const untracked = git(repo.primaryPath, ["ls-files", "--others", "--exclude-standard"]).split("\n").filter(Boolean);
|
|
98
|
+
for (const rel of untracked) {
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(dirname(join(dir, rel)), { recursive: true });
|
|
101
|
+
copyFileSync(join(repo.primaryPath, rel), join(dir, rel));
|
|
102
|
+
} catch {
|
|
103
|
+
/* best-effort */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { cwd: dir, mode, branch, base, resumed: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Diff of what the turn changed (for the dashboard's file-changes panel). */
|
|
111
|
+
export function collectChanges(cwd) {
|
|
112
|
+
const status = git(cwd, ["status", "--porcelain"]);
|
|
113
|
+
const files = status
|
|
114
|
+
.split("\n")
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((l) => ({ status: l.slice(0, 2).trim(), path: l.slice(3).trim() }));
|
|
117
|
+
const diff = git(cwd, ["diff", "HEAD"], { maxBuffer: 64 * 1024 * 1024 });
|
|
118
|
+
return { files, diff };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Drop a session's worktrees (after merge/close). */
|
|
122
|
+
export function removeWorktree({ kaiHome, repo, sessionId, title }) {
|
|
123
|
+
const dir = worktreePath(kaiHome, repo.name, sessionSlug(sessionId, title));
|
|
124
|
+
if (!existsSync(dir)) return false;
|
|
125
|
+
try {
|
|
126
|
+
git(repo.primaryPath, ["worktree", "remove", "--force", dir]);
|
|
127
|
+
} catch {
|
|
128
|
+
rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
try {
|
|
130
|
+
git(repo.primaryPath, ["worktree", "prune"]);
|
|
131
|
+
} catch {
|
|
132
|
+
/* best-effort */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* After a build turn in worktree mode: commit everything the agent left
|
|
140
|
+
* uncommitted (the runner forbids the agent from committing) under the
|
|
141
|
+
* user's own git identity and push the session branch with the user's
|
|
142
|
+
* own git auth. Local mode is left alone — the user owns that tree.
|
|
143
|
+
*
|
|
144
|
+
* Returns `{ committed, commitSha, pushed, branch, remote, error? }`.
|
|
145
|
+
* A failed push is NOT fatal: the dashboard still shows the diff, and the
|
|
146
|
+
* result carries the reason (no credentials, protected branch, …).
|
|
147
|
+
*/
|
|
148
|
+
export function commitAndPush(cwd, { branch, message, allowEmpty = false } = {}) {
|
|
149
|
+
const out = { committed: false, pushed: false, branch, commitSha: null, remote: null, error: null };
|
|
150
|
+
try {
|
|
151
|
+
// Agent state dirs must never enter the commit.
|
|
152
|
+
const excludes = [".codex-kai/", ".kai-acp-state/", ".kai-carry.patch"];
|
|
153
|
+
const excludePath = join(cwd, ".git", "info", "exclude");
|
|
154
|
+
try {
|
|
155
|
+
const current = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
156
|
+
const missing = excludes.filter((e) => !current.includes(e));
|
|
157
|
+
if (missing.length) writeFileSync(excludePath, `${current.trimEnd()}\n${missing.join("\n")}\n`);
|
|
158
|
+
} catch {
|
|
159
|
+
/* worktree .git is a file → exclude lives in the main repo; fine */
|
|
160
|
+
}
|
|
161
|
+
git(cwd, ["add", "-A"]);
|
|
162
|
+
const staged = git(cwd, ["diff", "--cached", "--name-only"]);
|
|
163
|
+
if (staged || allowEmpty) {
|
|
164
|
+
git(cwd, ["commit", "-q", "-m", message || "Kai Code changes", ...(allowEmpty ? ["--allow-empty"] : [])]);
|
|
165
|
+
out.committed = true;
|
|
166
|
+
}
|
|
167
|
+
out.commitSha = git(cwd, ["rev-parse", "HEAD"]);
|
|
168
|
+
out.remote = git(cwd, ["remote", "get-url", "origin"]);
|
|
169
|
+
git(cwd, ["push", "-u", "origin", `HEAD:${branch}`], { timeout: 120_000 });
|
|
170
|
+
out.pushed = true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
out.error = String(err?.stderr || err?.message || err).trim().slice(0, 500);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|