agent-theatre 1.3.9 → 1.4.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 +4 -0
- package/dist/chunk-WAD5QXS2.js +1614 -0
- package/dist/chunk-WAD5QXS2.js.map +1 -0
- package/dist/cli.js +35 -1601
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-L3HXVFDZ.js +99 -0
- package/dist/mcp-L3HXVFDZ.js.map +1 -0
- package/dist/setup-IZE4A3CD.js +85 -0
- package/dist/setup-IZE4A3CD.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/sessions.ts
|
|
4
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
function projectSlug(cwd) {
|
|
8
|
+
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
9
|
+
}
|
|
10
|
+
function claudeConfigDir(env = process.env) {
|
|
11
|
+
return env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
12
|
+
}
|
|
13
|
+
function listSessions(cwd, env = process.env) {
|
|
14
|
+
const dir = join(claudeConfigDir(env), "projects", projectSlug(cwd));
|
|
15
|
+
let names = [];
|
|
16
|
+
try {
|
|
17
|
+
names = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const f of names) {
|
|
23
|
+
const path = join(dir, f);
|
|
24
|
+
try {
|
|
25
|
+
const st = statSync(path);
|
|
26
|
+
out.push({ id: f.replace(/\.jsonl$/, ""), path, mtime: st.mtimeMs, bytes: st.size, title: sessionTitle(path) });
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
31
|
+
}
|
|
32
|
+
function sessionTitle(path) {
|
|
33
|
+
let text = "";
|
|
34
|
+
try {
|
|
35
|
+
const st = statSync(path);
|
|
36
|
+
const fd = readFileSync(path);
|
|
37
|
+
text = fd.subarray(Math.max(0, st.size - 262144)).toString("utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
let ai = "", prompt = "";
|
|
42
|
+
for (const line of text.split("\n")) {
|
|
43
|
+
if (!line.includes('"aiTitle"') && !line.includes('"lastPrompt"')) continue;
|
|
44
|
+
try {
|
|
45
|
+
const row = JSON.parse(line);
|
|
46
|
+
if (typeof row.aiTitle === "string" && row.aiTitle.trim()) ai = row.aiTitle.trim();
|
|
47
|
+
if (typeof row.lastPrompt === "string" && row.lastPrompt.trim()) prompt = row.lastPrompt.trim();
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const t = ai || prompt;
|
|
52
|
+
return t.length > 80 ? t.slice(0, 77) + "\u2026" : t;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/server.ts
|
|
56
|
+
import { createServer } from "http";
|
|
57
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync, writeFileSync, realpathSync } from "fs";
|
|
58
|
+
import { homedir as homedir2 } from "os";
|
|
59
|
+
import { join as join2, basename as basename2, resolve, sep } from "path";
|
|
60
|
+
import { spawnSync } from "child_process";
|
|
61
|
+
import { Hono } from "hono";
|
|
62
|
+
import { getRequestListener } from "@hono/node-server";
|
|
63
|
+
import { WebSocketServer } from "ws";
|
|
64
|
+
|
|
65
|
+
// ../core/dist/index.js
|
|
66
|
+
var last = 0;
|
|
67
|
+
var counter = 0;
|
|
68
|
+
var ALPHA = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
69
|
+
function mintId(now = Date.now()) {
|
|
70
|
+
if (now === last) counter++;
|
|
71
|
+
else {
|
|
72
|
+
last = now;
|
|
73
|
+
counter = 0;
|
|
74
|
+
}
|
|
75
|
+
let t = now;
|
|
76
|
+
let s = "";
|
|
77
|
+
for (let i = 0; i < 10; i++) {
|
|
78
|
+
s = ALPHA[t % 32] + s;
|
|
79
|
+
t = Math.floor(t / 32);
|
|
80
|
+
}
|
|
81
|
+
return `ev_${s}${counter.toString(32).padStart(3, "0")}`;
|
|
82
|
+
}
|
|
83
|
+
function norm(p) {
|
|
84
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
85
|
+
}
|
|
86
|
+
function relativizePath(abs, ctx) {
|
|
87
|
+
const p = norm(abs);
|
|
88
|
+
const cwd = norm(ctx.cwd);
|
|
89
|
+
if (cwd && (p === cwd || p.startsWith(cwd + "/"))) return p.slice(cwd.length + 1) || ".";
|
|
90
|
+
if (ctx.home) {
|
|
91
|
+
const home = norm(ctx.home);
|
|
92
|
+
if (home && (p === home || p.startsWith(home + "/"))) return "~/" + p.slice(home.length + 1);
|
|
93
|
+
}
|
|
94
|
+
if (!p.startsWith("/")) return p;
|
|
95
|
+
const base2 = p.slice(p.lastIndexOf("/") + 1);
|
|
96
|
+
return `//ext/${base2}`;
|
|
97
|
+
}
|
|
98
|
+
function basename(p) {
|
|
99
|
+
const n = norm(p);
|
|
100
|
+
return n.slice(n.lastIndexOf("/") + 1);
|
|
101
|
+
}
|
|
102
|
+
function isTestPath(p) {
|
|
103
|
+
return /(^|\/)(tests?|__tests__|spec|specs)\//i.test(p) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(p) || /(^|\/)test_[^/]+\.py$/i.test(p) || /_test\.(go|rs|py)$/i.test(p);
|
|
104
|
+
}
|
|
105
|
+
var TEST_RUNNERS = /\b(vitest|jest|mocha|pytest|py\.test|go\s+test|cargo\s+test|npm\s+test|pnpm\s+test|yarn\s+test|bun\s+test|npx\s+(vitest|jest|playwright)|rspec|phpunit|dotnet\s+test|mvn\s+test|gradle\s+test|make\s+test)\b/;
|
|
106
|
+
var BUILD = /\b(tsc|tsup|vite\s+build|webpack|esbuild|rollup|next\s+build|cargo\s+build|go\s+build|make\b(?!\s+test)|npm\s+run\s+build|pnpm\s+(run\s+)?build|yarn\s+build|gradle\s+build|mvn\s+package|docker\s+build)\b/;
|
|
107
|
+
var INSTALL = /\b(npm\s+(i|install|ci)|pnpm\s+(i|install|add)|yarn(\s+add|\s+install)?|pip3?\s+install|poetry\s+(add|install)|cargo\s+add|go\s+get|brew\s+install|apt(-get)?\s+install|bun\s+(i|install|add))\b/;
|
|
108
|
+
var SEARCH = /\b(grep|rg|ag|ack|find|fd|locate)\b/;
|
|
109
|
+
var READ = /^\s*(cat|head|tail|less|more|bat|wc|stat|jq)\b/;
|
|
110
|
+
function argv0Of(command) {
|
|
111
|
+
const segments = command.split(/\s*(?:&&|\|\||;|\|)\s*/);
|
|
112
|
+
const first = (segments.find((s) => !/^\s*(\[|test\b|!)/.test(s)) ?? segments[0] ?? "").replace(/^\s*!\s*/, "");
|
|
113
|
+
const toks = first.trim().split(/\s+/).map((t) => t.replace(/^[({]+/, "")).filter(Boolean);
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < toks.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])) i++;
|
|
116
|
+
if (toks[i] === "cd" && toks.length > i + 1) {
|
|
117
|
+
const rest = command.split(/\s*&&\s*/)[1];
|
|
118
|
+
if (rest) return argv0Of(rest);
|
|
119
|
+
}
|
|
120
|
+
const a = toks[i] ?? "";
|
|
121
|
+
return a.replace(/^.*\//, "").slice(0, 40) || "sh";
|
|
122
|
+
}
|
|
123
|
+
function cleanTarget(t) {
|
|
124
|
+
const s = t.replace(/^["']|["']$/g, "").trim();
|
|
125
|
+
if (!s || s.startsWith("-") || s === "/dev/null" || s.includes("$(") || s.startsWith("$")) return null;
|
|
126
|
+
if (/^\/dev\//.test(s) || /^&\d/.test(s)) return null;
|
|
127
|
+
return s;
|
|
128
|
+
}
|
|
129
|
+
function withoutHeredocBodies(command) {
|
|
130
|
+
return command.replace(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[^\n]*\n[\s\S]*?\n\s*\2(?=\s*(?:\n|$))/g, (m) => m.split("\n")[0] ?? "");
|
|
131
|
+
}
|
|
132
|
+
var plausibleFile = (t) => /[A-Za-z/.]/.test(t) && !/^\d+$/.test(t) && !/[()]$/.test(t);
|
|
133
|
+
function extractWrites(commandRaw) {
|
|
134
|
+
const command = withoutHeredocBodies(commandRaw);
|
|
135
|
+
const out = /* @__PURE__ */ new Set();
|
|
136
|
+
for (const m of command.matchAll(/(?:^|[\s)])\d?>{1,2}\s*([^\s|&;<>]+)/g)) {
|
|
137
|
+
const t = cleanTarget(m[1] ?? "");
|
|
138
|
+
if (t && plausibleFile(t)) out.add(t);
|
|
139
|
+
}
|
|
140
|
+
for (const m of command.matchAll(/\btee\s+(?:-[a-z]+\s+)*((?:[^\s|&;<>]+\s*)+)/g)) {
|
|
141
|
+
for (const f of (m[1] ?? "").split(/\s+/)) {
|
|
142
|
+
const t = cleanTarget(f);
|
|
143
|
+
if (t) out.add(t);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const m of command.matchAll(/\bsed\s+-i\S*\s+(.+?)(?=\s*(?:&&|\|\||;|\||$))/g)) {
|
|
147
|
+
const toks = (m[1] ?? "").match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
148
|
+
const files = toks.filter((t) => !t.startsWith("-") && !t.startsWith("'") && !t.startsWith('"') && !/^s[/|#]/.test(t) && !/^-e$/.test(t));
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
const t = cleanTarget(f);
|
|
151
|
+
if (t) out.add(t);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const m of command.matchAll(/\b(?:cp|mv)\s+(?:-[a-zA-Z]+\s+)*((?:[^\s|&;<>]+\s+)+)([^\s|&;<>]+)/g)) {
|
|
155
|
+
const t = cleanTarget(m[2] ?? "");
|
|
156
|
+
if (t) out.add(t);
|
|
157
|
+
}
|
|
158
|
+
for (const m of command.matchAll(/\btouch\s+((?:[^\s|&;<>]+\s*)+)/g)) {
|
|
159
|
+
for (const f of (m[1] ?? "").split(/\s+/)) {
|
|
160
|
+
const t = cleanTarget(f);
|
|
161
|
+
if (t) out.add(t);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [...out];
|
|
165
|
+
}
|
|
166
|
+
function extractReads(command) {
|
|
167
|
+
const out = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const seg of command.split(/\s*(?:&&|\|\||;|\|)\s*/)) {
|
|
169
|
+
const m = seg.match(/^\s*(?:cat|head|tail|less|more|bat|wc)\s+(?:-[^\s]+\s+)*((?:[^\s|&;<>]+\s*)+)/);
|
|
170
|
+
if (!m) continue;
|
|
171
|
+
for (const f of (m[1] ?? "").split(/\s+/)) {
|
|
172
|
+
const t = cleanTarget(f);
|
|
173
|
+
if (t && !/^\d+$/.test(t)) out.add(t);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [...out];
|
|
177
|
+
}
|
|
178
|
+
function classifyBash(command) {
|
|
179
|
+
const argv0 = argv0Of(command);
|
|
180
|
+
const writes = extractWrites(command);
|
|
181
|
+
const reads = extractReads(command);
|
|
182
|
+
let intent = "other";
|
|
183
|
+
if (TEST_RUNNERS.test(command)) intent = "test";
|
|
184
|
+
else if (BUILD.test(command)) intent = "build";
|
|
185
|
+
else if (INSTALL.test(command)) intent = "install";
|
|
186
|
+
else if (/^\s*git\b/.test(command) || argv0 === "git" || argv0 === "gh") intent = "git";
|
|
187
|
+
else if (writes.length) intent = "write_file";
|
|
188
|
+
else if (SEARCH.test(command) && argv0 !== "cd") intent = "search";
|
|
189
|
+
else if (READ.test(command) && reads.length) intent = "read_file";
|
|
190
|
+
return { argv0, intent, writes, reads };
|
|
191
|
+
}
|
|
192
|
+
var num = (s) => s ? parseInt(s, 10) : 0;
|
|
193
|
+
function parseTestSummary(output) {
|
|
194
|
+
const tail = output.length > 2e4 ? output.slice(-2e4) : output;
|
|
195
|
+
let m = tail.match(/Tests\s+(?:(\d+)\s+failed\s*\|\s*)?(\d+)\s+passed(?:\s*\|\s*(\d+)\s+skipped)?/);
|
|
196
|
+
if (m) return { failed: num(m[1]), passed: num(m[2]), ...m[3] ? { skipped: num(m[3]) } : {}, framework: "vitest" };
|
|
197
|
+
m = tail.match(/Tests\s+(\d+)\s+failed\s*\((\d+)\)/);
|
|
198
|
+
if (m) return { failed: num(m[1]), passed: Math.max(0, num(m[2]) - num(m[1])), framework: "vitest" };
|
|
199
|
+
m = tail.match(/Tests:\s+(?:(\d+)\s+failed,\s*)?(?:(\d+)\s+skipped,\s*)?(\d+)\s+passed,\s*(?:(\d+)\s+skipped,\s*)?\d+\s+total/);
|
|
200
|
+
if (m) return { failed: num(m[1]), passed: num(m[3]), ...m[2] ?? m[4] ? { skipped: num(m[2] ?? m[4]) } : {}, framework: "jest" };
|
|
201
|
+
m = tail.match(/Tests:\s+(\d+)\s+failed,\s*\d+\s+total/);
|
|
202
|
+
if (m) return { failed: num(m[1]), passed: 0, framework: "jest" };
|
|
203
|
+
m = tail.match(/=+\s*(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+skipped,?\s*)?(?:\d+\s+(?:error|errors|warnings?|deselected|xfailed|xpassed),?\s*)*in\s+[\d.]+s/);
|
|
204
|
+
if (m && (m[1] || m[2])) return { failed: num(m[1]), passed: num(m[2]), ...m[3] ? { skipped: num(m[3]) } : {}, framework: "pytest" };
|
|
205
|
+
if (/^--- (?:PASS|FAIL):/m.test(tail) || /^(?:ok|FAIL)\s+\S+\s+(?:[\d.]+s|\(cached\))/m.test(tail)) {
|
|
206
|
+
const failed = (tail.match(/^--- FAIL:/gm) ?? []).length || (tail.match(/^FAIL\s+\S+\s+[\d.]+s/gm) ?? []).length;
|
|
207
|
+
const passed = (tail.match(/^--- PASS:/gm) ?? []).length || (tail.match(/^ok\s+\S+/gm) ?? []).length;
|
|
208
|
+
return { failed, passed, framework: "go" };
|
|
209
|
+
}
|
|
210
|
+
m = tail.match(/test result:\s*\w+\.\s*(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
|
|
211
|
+
if (m) return { passed: num(m[1]), failed: num(m[2]), skipped: num(m[3]), framework: "cargo" };
|
|
212
|
+
const mp = tail.match(/^\s*(\d+)\s+passing\b/m);
|
|
213
|
+
const mf = tail.match(/^\s*(\d+)\s+failing\b/m);
|
|
214
|
+
if (mp || mf) {
|
|
215
|
+
const ms = tail.match(/^\s*(\d+)\s+pending\b/m);
|
|
216
|
+
return { passed: num(mp?.[1]), failed: num(mf?.[1]), ...ms ? { skipped: num(ms[1]) } : {}, framework: "mocha" };
|
|
217
|
+
}
|
|
218
|
+
const pp = tail.match(/^\s*(\d+)\s+passed\s*\(/m);
|
|
219
|
+
const pf = tail.match(/^\s*(\d+)\s+failed\b/m);
|
|
220
|
+
if (pp || pf) return { passed: num(pp?.[1]), failed: num(pf?.[1]), framework: "playwright" };
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
var obj = (x) => x && typeof x === "object" ? x : {};
|
|
224
|
+
var str = (x) => typeof x === "string" ? x : void 0;
|
|
225
|
+
var lineCount = (s) => s ? (s.match(/\n/g)?.length ?? 0) + (s.endsWith("\n") ? 0 : 1) : 0;
|
|
226
|
+
function patchCounts(response) {
|
|
227
|
+
const patch = obj(response).structuredPatch;
|
|
228
|
+
if (!Array.isArray(patch)) return null;
|
|
229
|
+
let added = 0, removed = 0;
|
|
230
|
+
for (const h of patch) {
|
|
231
|
+
const lines = obj(h).lines;
|
|
232
|
+
if (!Array.isArray(lines)) continue;
|
|
233
|
+
for (const l of lines) {
|
|
234
|
+
if (typeof l !== "string") continue;
|
|
235
|
+
if (l.startsWith("+")) added++;
|
|
236
|
+
else if (l.startsWith("-")) removed++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { added, removed, hunks: patch.length };
|
|
240
|
+
}
|
|
241
|
+
function responseText(response) {
|
|
242
|
+
if (typeof response === "string") return response;
|
|
243
|
+
const r = obj(response);
|
|
244
|
+
const parts = [str(r.stdout), str(r.stderr), str(r.output)].filter((x) => !!x);
|
|
245
|
+
if (parts.length) return parts.join("\n");
|
|
246
|
+
if (Array.isArray(response)) return response.map((b) => str(obj(b).text) ?? "").join("\n");
|
|
247
|
+
return "";
|
|
248
|
+
}
|
|
249
|
+
function heredocLines(command) {
|
|
250
|
+
let n = 0;
|
|
251
|
+
for (const m of command.matchAll(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g)) n += lineCount(m[3]);
|
|
252
|
+
if (n === 0 && /(?:^|[\s)])\d?>{1,2}\s*[^\s|&;<>]+/.test(command) && !/>\s*\/dev\/null/.test(command)) n = 1;
|
|
253
|
+
return n;
|
|
254
|
+
}
|
|
255
|
+
var SEARCH_TOOLS_REPO = /* @__PURE__ */ new Set(["Grep", "Glob", "LS", "ToolSearch"]);
|
|
256
|
+
var SEARCH_TOOLS_WEB = /* @__PURE__ */ new Set(["WebSearch", "WebFetch"]);
|
|
257
|
+
var SILENT_TOOLS = /* @__PURE__ */ new Set(["TodoWrite", "TodoRead", "Skill", "EnterPlanMode", "ExitPlanMode", "EnterWorktree", "ExitWorktree", "NotebookRead", "ListAgents", "SendMessage", "TaskStop", "Monitor", "ScheduleWakeup", "ReportFindings"]);
|
|
258
|
+
function toolCallToOutcome(call, ctx) {
|
|
259
|
+
const input = obj(call.input);
|
|
260
|
+
const resp = call.response;
|
|
261
|
+
const ok = !call.isError && obj(resp).interrupted !== true;
|
|
262
|
+
const rel = (p) => relativizePath(p, ctx);
|
|
263
|
+
const dur = call.durationMs !== void 0 ? { durationMs: call.durationMs } : {};
|
|
264
|
+
switch (call.name) {
|
|
265
|
+
case "Read": {
|
|
266
|
+
const path = str(input.file_path) ?? str(input.path);
|
|
267
|
+
if (!path) return { bodies: [], warning: { code: "missing_field", detail: "Read without file_path" } };
|
|
268
|
+
const content = str(obj(obj(resp).file).content);
|
|
269
|
+
return { bodies: [{ kind: "read_file", path: rel(path), ...content !== void 0 ? { bytes: content.length } : {} }] };
|
|
270
|
+
}
|
|
271
|
+
case "Edit":
|
|
272
|
+
case "MultiEdit":
|
|
273
|
+
case "NotebookEdit": {
|
|
274
|
+
const path = str(input.file_path) ?? str(input.notebook_path);
|
|
275
|
+
if (!path) return { bodies: [], warning: { code: "missing_field", detail: `${call.name} without file_path` } };
|
|
276
|
+
const counts = patchCounts(resp) ?? {
|
|
277
|
+
added: lineCount(str(input.new_string) ?? str(input.new_source)),
|
|
278
|
+
removed: lineCount(str(input.old_string)),
|
|
279
|
+
hunks: 1
|
|
280
|
+
};
|
|
281
|
+
return { bodies: [{ kind: "edit_file", path: rel(path), ...counts }] };
|
|
282
|
+
}
|
|
283
|
+
case "Write": {
|
|
284
|
+
const path = str(input.file_path);
|
|
285
|
+
if (!path) return { bodies: [], warning: { code: "missing_field", detail: "Write without file_path" } };
|
|
286
|
+
const r = obj(resp);
|
|
287
|
+
const created = r.type === "create" || r.type === void 0 && r.originalFile === null;
|
|
288
|
+
return { bodies: [{ kind: "write_file", path: rel(path), added: lineCount(str(input.content)), created }] };
|
|
289
|
+
}
|
|
290
|
+
case "Bash": {
|
|
291
|
+
const command = str(input.command) ?? "";
|
|
292
|
+
const c = classifyBash(command);
|
|
293
|
+
const touched = [...new Set([...c.writes, ...c.reads].map(rel))];
|
|
294
|
+
const bodies = [{ kind: "run_command", argv0: c.argv0, intent: c.intent, touched, ok, ...dur }];
|
|
295
|
+
const heredoc = heredocLines(command);
|
|
296
|
+
c.writes.forEach((w, i) => bodies.push({ kind: "write_file", path: rel(w), added: i === 0 ? heredoc : 0, created: false }));
|
|
297
|
+
for (const r of c.reads) bodies.push({ kind: "read_file", path: rel(r) });
|
|
298
|
+
if (c.intent === "test") {
|
|
299
|
+
const sum = parseTestSummary(responseText(resp));
|
|
300
|
+
if (sum) bodies.push({ kind: "test_result", ok: sum.failed === 0 && ok, passed: sum.passed, failed: sum.failed, ...sum.skipped !== void 0 ? { skipped: sum.skipped } : {}, framework: sum.framework, ...dur });
|
|
301
|
+
else bodies.push({ kind: "test_result", ok, passed: 0, failed: 0, framework: "unknown", ...dur });
|
|
302
|
+
}
|
|
303
|
+
return { bodies };
|
|
304
|
+
}
|
|
305
|
+
case "Agent":
|
|
306
|
+
case "Task": {
|
|
307
|
+
const r = obj(resp);
|
|
308
|
+
const agentId = str(r.agentId) ?? str(r.agent_id) ?? "";
|
|
309
|
+
const label = str(input.description) ?? str(input.subagent_type);
|
|
310
|
+
const start = { kind: "subagent_start", childId: agentId, ...label ? { label } : {} };
|
|
311
|
+
if (r.status === "async_launched" || r.isAsync === true) return { bodies: [start], pendingSubagent: { childId: agentId } };
|
|
312
|
+
return { bodies: [start, { kind: "subagent_stop", childId: agentId, ok }] };
|
|
313
|
+
}
|
|
314
|
+
case "AskUserQuestion":
|
|
315
|
+
return { bodies: [{ kind: "waiting_for_user", reason: "question" }] };
|
|
316
|
+
}
|
|
317
|
+
if (SEARCH_TOOLS_REPO.has(call.name)) {
|
|
318
|
+
const n = obj(resp).numFiles ?? obj(resp).numLines ?? obj(resp).count;
|
|
319
|
+
return { bodies: [{ kind: "search", scope: "repo", ...typeof n === "number" ? { hits: n } : {} }] };
|
|
320
|
+
}
|
|
321
|
+
if (SEARCH_TOOLS_WEB.has(call.name)) return { bodies: [{ kind: "search", scope: "web" }] };
|
|
322
|
+
if (SILENT_TOOLS.has(call.name)) return { bodies: [] };
|
|
323
|
+
return {
|
|
324
|
+
bodies: [{ kind: "run_command", argv0: call.name.slice(0, 40), intent: "other", touched: [], ok, ...dur }],
|
|
325
|
+
warning: { code: "unknown_tool", detail: call.name.slice(0, 80) }
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
var SUCCESS_LEXICON = [
|
|
329
|
+
/\ball (?:\d+ )?tests? (?:are )?pass(?:ing|ed)?\b/i,
|
|
330
|
+
/\btests? (?:are |is )?(?:now )?(?:passing|green)\b/i,
|
|
331
|
+
/\b(?:is|are|it's|that's|everything(?:'s| is)) (?:now )?(?:fixed|working|done|complete|resolved)\b/i,
|
|
332
|
+
/\bshould (?:now )?work\b/i,
|
|
333
|
+
/\bworks now\b/i,
|
|
334
|
+
/\b(?:done|complete|completed|fixed|resolved|shipped|implemented)[.!]/i,
|
|
335
|
+
/✅/
|
|
336
|
+
];
|
|
337
|
+
var HEDGE_LEXICON = [
|
|
338
|
+
/\bnot (?:yet )?(?:done|fixed|working|tested|verified|complete)\b/i,
|
|
339
|
+
/\bstill (?:fail|broken|not)/i,
|
|
340
|
+
/\b(?:could ?n[’']?t|cannot|can ?not|unable to|wasn[’']?t able)\b/i,
|
|
341
|
+
/\bfail(?:ed|ing|s)\b/i,
|
|
342
|
+
/\bTODO\b/,
|
|
343
|
+
/\bhaven[’']?t (?:run|tested|verified|checked)/i,
|
|
344
|
+
/\buntested\b/i,
|
|
345
|
+
/\bI did not (?:run|test|verify)/i
|
|
346
|
+
];
|
|
347
|
+
var TAIL = 600;
|
|
348
|
+
function claimsSuccess(text) {
|
|
349
|
+
if (!text) return false;
|
|
350
|
+
const tail = text.length > TAIL ? text.slice(-TAIL) : text;
|
|
351
|
+
if (HEDGE_LEXICON.some((r) => r.test(tail))) return false;
|
|
352
|
+
return SUCCESS_LEXICON.some((r) => r.test(tail));
|
|
353
|
+
}
|
|
354
|
+
var obj2 = (x) => x && typeof x === "object" ? x : {};
|
|
355
|
+
var str2 = (x) => typeof x === "string" ? x : void 0;
|
|
356
|
+
var TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
|
|
357
|
+
function endReason(reason) {
|
|
358
|
+
switch (reason) {
|
|
359
|
+
case "clear":
|
|
360
|
+
case "logout":
|
|
361
|
+
case "prompt_input_exit":
|
|
362
|
+
return "user";
|
|
363
|
+
default:
|
|
364
|
+
return "stop";
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function hookPayloadToEvents(payload, opts = {}) {
|
|
368
|
+
const p = obj2(payload);
|
|
369
|
+
const name = str2(p.hook_event_name);
|
|
370
|
+
const sessionId = str2(p.session_id) ?? "";
|
|
371
|
+
const ts = opts.now ?? Date.now();
|
|
372
|
+
if (!name) return { events: [], warnings: [{ code: "missing_field", detail: "hook_event_name" }] };
|
|
373
|
+
const cwd = str2(p.cwd) ?? "";
|
|
374
|
+
const ctx = { cwd, ...opts.home ? { home: opts.home } : {} };
|
|
375
|
+
const agentId = TOOL_EVENTS.has(name) ? str2(p.agent_id) : void 0;
|
|
376
|
+
const extra = agentId ? { agentId } : {};
|
|
377
|
+
let seq = 0;
|
|
378
|
+
const mk = (body, id = mintId(ts)) => ({ id, seq: seq++, ts, sessionId, source: "hook", ...extra, ...body });
|
|
379
|
+
const one = (body) => ({ events: [mk(body)], warnings: [] });
|
|
380
|
+
switch (name) {
|
|
381
|
+
case "SessionStart":
|
|
382
|
+
return one({ kind: "session_start", repoId: basename(cwd) || "repo" });
|
|
383
|
+
case "SessionEnd":
|
|
384
|
+
return one({ kind: "session_end", reason: endReason(str2(p.reason)), claimedSuccess: false });
|
|
385
|
+
case "Stop":
|
|
386
|
+
return one({ kind: "turn_end", claimedSuccess: claimsSuccess(str2(p.last_assistant_message)), ...noteOf(str2(p.last_assistant_message)) });
|
|
387
|
+
case "SubagentStart": {
|
|
388
|
+
const id = str2(p.agent_id) ?? "";
|
|
389
|
+
const label = str2(p.agent_type);
|
|
390
|
+
return { events: [mk({ kind: "subagent_start", childId: id, ...label ? { label } : {} })], warnings: [] };
|
|
391
|
+
}
|
|
392
|
+
case "SubagentStop":
|
|
393
|
+
return one({ kind: "subagent_stop", childId: str2(p.agent_id) ?? "", ok: true });
|
|
394
|
+
case "Notification": {
|
|
395
|
+
const t = str2(p.notification_type);
|
|
396
|
+
if (t === "permission_prompt") return one({ kind: "waiting_for_user", reason: "permission" });
|
|
397
|
+
if (t === "idle_prompt") return one({ kind: "waiting_for_user", reason: "idle" });
|
|
398
|
+
if (t === "elicitation_dialog") return one({ kind: "waiting_for_user", reason: "question" });
|
|
399
|
+
return { events: [], warnings: [] };
|
|
400
|
+
}
|
|
401
|
+
case "PostToolUseFailure":
|
|
402
|
+
return one({ kind: "error", where: str2(p.tool_name) ?? "tool", retrying: false });
|
|
403
|
+
case "PostToolUse": {
|
|
404
|
+
const tool = str2(p.tool_name);
|
|
405
|
+
if (!tool) return { events: [], warnings: [{ code: "missing_field", detail: "tool_name" }] };
|
|
406
|
+
const dur = typeof p.duration_ms === "number" ? { durationMs: p.duration_ms } : {};
|
|
407
|
+
const out = toolCallToOutcome({ name: tool, input: p.tool_input, response: p.tool_response, ...dur }, ctx);
|
|
408
|
+
const baseId = str2(p.tool_use_id) ?? mintId(ts);
|
|
409
|
+
const events = out.bodies.map((body, i) => mk(body, i === 0 ? baseId : `${baseId}:${i}`));
|
|
410
|
+
return { events, warnings: out.warning ? [out.warning] : [] };
|
|
411
|
+
}
|
|
412
|
+
case "PreToolUse":
|
|
413
|
+
case "UserPromptSubmit":
|
|
414
|
+
case "PreCompact":
|
|
415
|
+
case "PostCompact":
|
|
416
|
+
return { events: [], warnings: [] };
|
|
417
|
+
}
|
|
418
|
+
return { events: [], warnings: [{ code: "unknown_row_type", detail: name }] };
|
|
419
|
+
}
|
|
420
|
+
function noteOf(text) {
|
|
421
|
+
if (!text) return {};
|
|
422
|
+
const clean = text.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]*)`/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/\*+|#+|>+/g, "").replace(/(^|\s)_+|_+(?=\s|$)/g, "$1").replace(/^\s*(?:[-•]|\d+[.)])\s+/gm, "").replace(/\s+/g, " ").trim();
|
|
423
|
+
if (!clean) return {};
|
|
424
|
+
const sentences = clean.split(/(?<=[.!?])\s+(?=["'(A-Z])/).map((x) => x.trim()).filter(Boolean);
|
|
425
|
+
if (!sentences.length) return {};
|
|
426
|
+
const questions = sentences.filter((x) => x.endsWith("?"));
|
|
427
|
+
let note = questions.length ? questions[questions.length - 1] : sentences[0];
|
|
428
|
+
if (!questions.length) for (const raw of sentences.slice(1)) {
|
|
429
|
+
const next = /[.!?]$/.test(raw) ? raw : raw.replace(/[^.!?]*$/, "").trim();
|
|
430
|
+
if (!next || note.length + next.length + 1 > 160) break;
|
|
431
|
+
note = `${note} ${next}`;
|
|
432
|
+
}
|
|
433
|
+
if (note.length > 200) note = note.slice(0, 197).replace(/\s+\S*$/, "") + "\u2026";
|
|
434
|
+
return { note };
|
|
435
|
+
}
|
|
436
|
+
var ROW_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system"]);
|
|
437
|
+
var obj3 = (x) => x && typeof x === "object" ? x : {};
|
|
438
|
+
var str3 = (x) => typeof x === "string" ? x : void 0;
|
|
439
|
+
function parseTranscript(text, opts = {}) {
|
|
440
|
+
const rows = [];
|
|
441
|
+
const warnings = [];
|
|
442
|
+
const lines = text.split(/\r?\n/);
|
|
443
|
+
for (let i = 0; i < lines.length; i++) {
|
|
444
|
+
const line = lines[i];
|
|
445
|
+
if (!line.trim()) continue;
|
|
446
|
+
try {
|
|
447
|
+
rows.push(JSON.parse(line));
|
|
448
|
+
} catch {
|
|
449
|
+
warnings.push({ code: "bad_json", detail: `line ${i + 1}`, line: i + 1 });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const res = parseTranscriptRows(rows, opts);
|
|
453
|
+
return { events: res.events, warnings: [...warnings, ...res.warnings] };
|
|
454
|
+
}
|
|
455
|
+
function parseTranscriptRows(rows, opts = {}) {
|
|
456
|
+
const events = [];
|
|
457
|
+
const warnings = [];
|
|
458
|
+
const unknownTypes = /* @__PURE__ */ new Set();
|
|
459
|
+
const pending = /* @__PURE__ */ new Map();
|
|
460
|
+
const openSubagents = /* @__PURE__ */ new Map();
|
|
461
|
+
let sessionId = "";
|
|
462
|
+
let cwd = "";
|
|
463
|
+
let branch;
|
|
464
|
+
let firstTs = Number.NaN;
|
|
465
|
+
let lastTs = Number.NaN;
|
|
466
|
+
let lastAssistantText = "";
|
|
467
|
+
let seq = 0;
|
|
468
|
+
let sawEnd = false;
|
|
469
|
+
const ctx = () => ({ cwd, ...opts.home ? { home: opts.home } : {} });
|
|
470
|
+
const push = (ts, id, body, extra = {}) => {
|
|
471
|
+
events.push({ id, seq: seq++, ts, sessionId, source: "transcript", ...extra, ...body });
|
|
472
|
+
};
|
|
473
|
+
const tsOf = (row) => {
|
|
474
|
+
const t = str3(row.timestamp);
|
|
475
|
+
const n = t ? Date.parse(t) : Number.NaN;
|
|
476
|
+
return Number.isFinite(n) ? n : Number.isFinite(lastTs) ? lastTs : 0;
|
|
477
|
+
};
|
|
478
|
+
const agentOf = (row) => str3(row.agentId) ?? (row.isSidechain === true ? opts.sidechainAgentId ?? "sidechain" : void 0);
|
|
479
|
+
for (const raw of rows) {
|
|
480
|
+
const row = obj3(raw);
|
|
481
|
+
const type = str3(row.type) ?? "";
|
|
482
|
+
if (!ROW_TYPES.has(type)) {
|
|
483
|
+
if (type) unknownTypes.add(type);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (!sessionId && str3(row.sessionId)) sessionId = str3(row.sessionId);
|
|
487
|
+
if (!cwd && str3(row.cwd)) cwd = str3(row.cwd);
|
|
488
|
+
if (!branch && str3(row.gitBranch)) branch = str3(row.gitBranch);
|
|
489
|
+
const ts = tsOf(row);
|
|
490
|
+
if (!Number.isFinite(firstTs)) {
|
|
491
|
+
firstTs = ts;
|
|
492
|
+
push(ts, `${sessionId || "session"}:start`, { kind: "session_start", repoId: basename(cwd) || "repo", ...branch ? { branch } : {} });
|
|
493
|
+
}
|
|
494
|
+
lastTs = ts;
|
|
495
|
+
const agentId = agentOf(row);
|
|
496
|
+
const message = obj3(row.message);
|
|
497
|
+
const content = message.content;
|
|
498
|
+
if (type === "system") {
|
|
499
|
+
if (row.subtype === "api_error") {
|
|
500
|
+
const attempt = typeof row.retryAttempt === "number" ? row.retryAttempt : 0;
|
|
501
|
+
const max = typeof row.maxRetries === "number" ? row.maxRetries : 0;
|
|
502
|
+
push(ts, str3(row.uuid) ?? `sys:${seq}`, { kind: "error", where: "api", retrying: attempt < max }, agentId ? { agentId } : {});
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (type === "assistant") {
|
|
507
|
+
const blocks2 = Array.isArray(content) ? content : [];
|
|
508
|
+
for (const b of blocks2) {
|
|
509
|
+
const block = obj3(b);
|
|
510
|
+
if (block.type === "tool_use" && str3(block.id) && str3(block.name)) {
|
|
511
|
+
pending.set(str3(block.id), { name: str3(block.name), input: block.input, ts, ...agentId ? { agentId } : {} });
|
|
512
|
+
} else if (block.type === "text" && !agentId) {
|
|
513
|
+
const t = str3(block.text);
|
|
514
|
+
if (t) lastAssistantText = t;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (typeof content === "string" && !agentId) lastAssistantText = content;
|
|
518
|
+
if (message.stop_reason === "end_turn" && !agentId) {
|
|
519
|
+
push(ts, `${str3(row.uuid) ?? seq}:turn`, { kind: "turn_end", claimedSuccess: claimsSuccess(lastAssistantText), ...noteOf(lastAssistantText) });
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (row.toolDenialKind !== void 0) push(ts, `${str3(row.uuid) ?? seq}:deny`, { kind: "waiting_for_user", reason: "permission" });
|
|
524
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
525
|
+
for (const b of blocks) {
|
|
526
|
+
const block = obj3(b);
|
|
527
|
+
if (block.type !== "tool_result") continue;
|
|
528
|
+
const useId = str3(block.tool_use_id);
|
|
529
|
+
if (!useId) continue;
|
|
530
|
+
const p = pending.get(useId);
|
|
531
|
+
if (!p) {
|
|
532
|
+
warnings.push({ code: "unpaired_result", detail: useId });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
pending.delete(useId);
|
|
536
|
+
const response = row.toolUseResult ?? block.content;
|
|
537
|
+
const out = toolCallToOutcome(
|
|
538
|
+
{ name: p.name, input: p.input, response, isError: block.is_error === true, durationMs: Math.max(0, ts - p.ts) },
|
|
539
|
+
ctx()
|
|
540
|
+
);
|
|
541
|
+
if (out.warning) warnings.push(out.warning);
|
|
542
|
+
const rowAgent = p.agentId ?? agentId;
|
|
543
|
+
const parentId = rowAgent ? openSubagents.get(rowAgent) : void 0;
|
|
544
|
+
const extra = { ...rowAgent ? { agentId: rowAgent } : {}, ...parentId ? { parentId } : {} };
|
|
545
|
+
out.bodies.forEach((body, i) => push(p.ts, i === 0 ? useId : `${useId}:${i}`, body, extra));
|
|
546
|
+
if (out.pendingSubagent) openSubagents.set(out.pendingSubagent.childId, useId);
|
|
547
|
+
}
|
|
548
|
+
if (blocks.length === 0 && typeof content === "string" && /\[Request interrupted by user/i.test(content)) {
|
|
549
|
+
push(ts, `${str3(row.uuid) ?? seq}:interrupt`, { kind: "waiting_for_user", reason: "idle" });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
for (const [useId, p] of pending) {
|
|
553
|
+
warnings.push({ code: "unpaired_result", detail: `tool_use ${useId} (${p.name}) has no result` });
|
|
554
|
+
const out = toolCallToOutcome({ name: p.name, input: p.input, isError: true }, ctx());
|
|
555
|
+
if (out.warning) warnings.push(out.warning);
|
|
556
|
+
out.bodies.forEach((body, i) => push(p.ts, i === 0 ? useId : `${useId}:${i}`, body, p.agentId ? { agentId: p.agentId } : {}));
|
|
557
|
+
}
|
|
558
|
+
for (const [childId, spawnId] of openSubagents) {
|
|
559
|
+
push(lastTs, `${spawnId}:stop`, { kind: "subagent_stop", childId, ok: true }, { parentId: spawnId });
|
|
560
|
+
}
|
|
561
|
+
for (const t of unknownTypes) warnings.push({ code: "unknown_row_type", detail: t });
|
|
562
|
+
if (Number.isFinite(firstTs) && !sawEnd) {
|
|
563
|
+
sawEnd = true;
|
|
564
|
+
push(lastTs, `${sessionId || "session"}:end`, { kind: "session_end", reason: "stop", claimedSuccess: claimsSuccess(lastAssistantText) });
|
|
565
|
+
}
|
|
566
|
+
return { events, warnings };
|
|
567
|
+
}
|
|
568
|
+
function hashString(s) {
|
|
569
|
+
let h = 2166136261;
|
|
570
|
+
for (let i = 0; i < s.length; i++) {
|
|
571
|
+
h ^= s.charCodeAt(i);
|
|
572
|
+
h = Math.imul(h, 16777619);
|
|
573
|
+
}
|
|
574
|
+
return h >>> 0;
|
|
575
|
+
}
|
|
576
|
+
function mulberry32(seed) {
|
|
577
|
+
let a = seed >>> 0;
|
|
578
|
+
return () => {
|
|
579
|
+
a = a + 1831565813 >>> 0;
|
|
580
|
+
let t = a;
|
|
581
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
582
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
583
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
var ARCHIVE_RE = /(^|\/)(schema\.(sql|prisma|graphql|gql)|.*\.(sql|prisma|db|sqlite)|migrations?\/|drizzle\/|models?\/.*\.(ts|js|py|rb)$)/i;
|
|
587
|
+
var IGNORE_RE = /(^|\/)(node_modules|\.git|dist|build|out|coverage|_site|\.next|\.turbo|target|vendor|__pycache__|\.venv|venv)(\/|$)|(^|\/)\.[^/]+$|\.(lock|map|min\.js|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|mp4|mp3|pdf|zip)$/i;
|
|
588
|
+
function isIgnoredPath(p) {
|
|
589
|
+
return IGNORE_RE.test(p);
|
|
590
|
+
}
|
|
591
|
+
function kindOf(path) {
|
|
592
|
+
if (isTestPath(path)) return "beacon";
|
|
593
|
+
if (ARCHIVE_RE.test(path)) return "archive";
|
|
594
|
+
return "building";
|
|
595
|
+
}
|
|
596
|
+
function locOf(f) {
|
|
597
|
+
if (typeof f.loc === "number") return f.loc;
|
|
598
|
+
if (typeof f.size === "number") return Math.max(1, Math.round(f.size / 40));
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
function scoreFiles(files, touched) {
|
|
602
|
+
const kept = files.filter((f) => !isIgnoredPath(f.path));
|
|
603
|
+
const maxLoc = Math.max(1, ...kept.map(locOf));
|
|
604
|
+
const maxChurn = Math.max(1, ...kept.map((f) => f.churn ?? 0));
|
|
605
|
+
return kept.map((file) => {
|
|
606
|
+
const loc = locOf(file);
|
|
607
|
+
const t = touched.has(file.path);
|
|
608
|
+
const score = 0.5 * Math.sqrt(loc / maxLoc) + 0.3 * ((file.churn ?? 0) / maxChurn) + (t ? 0.2 : 0);
|
|
609
|
+
return { file, loc, score, kind: kindOf(file.path), touched: t };
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function heightClass(loc, maxLoc) {
|
|
613
|
+
const r = Math.sqrt(loc / Math.max(1, maxLoc));
|
|
614
|
+
return Math.max(1, Math.min(5, Math.ceil(r * 5)));
|
|
615
|
+
}
|
|
616
|
+
function footprintOf(height) {
|
|
617
|
+
return height >= 5 ? 3 : height >= 3 ? 2 : 1;
|
|
618
|
+
}
|
|
619
|
+
var EXT_LANG = { ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", jsx: "JavaScript", mjs: "JavaScript", py: "Python", go: "Go", rs: "Rust", java: "Java", kt: "Kotlin", swift: "Swift", rb: "Ruby", php: "PHP", cs: "C#", cpp: "C++", cc: "C++", c: "C", h: "C", m: "Objective-C", scala: "Scala", ex: "Elixir", exs: "Elixir", dart: "Dart", lua: "Lua", zig: "Zig", sh: "Shell", sql: "SQL", md: "Markdown", mdx: "Markdown", html: "HTML", css: "CSS", scss: "CSS", vue: "Vue", svelte: "Svelte", tf: "Terraform", yaml: "YAML", yml: "YAML", ipynb: "Notebook" };
|
|
620
|
+
function profileRepo(files, manifests = {}) {
|
|
621
|
+
const paths = files.map((f) => f.path.toLowerCase());
|
|
622
|
+
const has = (re) => paths.some((p) => re.test(p));
|
|
623
|
+
const count = (re) => paths.filter((p) => re.test(p)).length;
|
|
624
|
+
const langCount = /* @__PURE__ */ new Map();
|
|
625
|
+
for (const p of paths) {
|
|
626
|
+
const ext = p.slice(p.lastIndexOf(".") + 1);
|
|
627
|
+
const l = EXT_LANG[ext];
|
|
628
|
+
if (l && !/^(markdown|yaml|html|css|shell|sql)$/i.test(l)) langCount.set(l, (langCount.get(l) ?? 0) + 1);
|
|
629
|
+
}
|
|
630
|
+
const languages = [...langCount.entries()].sort((a, b) => b[1] - a[1]).map(([l]) => l).slice(0, 4);
|
|
631
|
+
const pkg = (manifests["package.json"] ?? "").toLowerCase();
|
|
632
|
+
const py = ((manifests["pyproject.toml"] ?? "") + (manifests["requirements.txt"] ?? "")).toLowerCase();
|
|
633
|
+
const dep = (re) => re.test(pkg) || re.test(py);
|
|
634
|
+
const scores = /* @__PURE__ */ new Map();
|
|
635
|
+
const add = (k, n) => scores.set(k, (scores.get(k) ?? 0) + n);
|
|
636
|
+
if (dep(/"(react|react-dom|next|vue|nuxt|svelte|@sveltejs\/kit|@angular\/core|solid-js|astro|remix|vite)"/)) add("web-frontend", 3);
|
|
637
|
+
if (count(/\.(tsx|jsx|vue|svelte)$/) > 3) add("web-frontend", 2);
|
|
638
|
+
if (has(/(^|\/)(index|app)\.html$/) || count(/\.(css|scss)$/) > 3) add("web-frontend", 1);
|
|
639
|
+
if (dep(/"(express|fastify|hono|koa|@nestjs\/core|apollo-server|prisma|drizzle-orm|django|flask|fastapi|sqlalchemy|gin|fiber|actix|axum|rails|spring)"/)) add("backend-api", 3);
|
|
640
|
+
if (has(/(^|\/)(routes?|controllers?|api|handlers?|migrations?)\//) || has(/\.(sql|prisma)$/)) add("backend-api", 2);
|
|
641
|
+
if (pkg.includes('"bin"') || has(/(^|\/)(cli|cmd|bin)\//) || dep(/"(commander|yargs|clap|cobra|click|typer)"/)) add("cli-tool", 3);
|
|
642
|
+
if (dep(/"(torch|tensorflow|jax|pandas|numpy|scikit-learn|sklearn|transformers|xgboost|lightgbm|polars)"/) || count(/\.ipynb$/) > 0 || has(/(^|\/)(notebooks?|models?|datasets?|training)\//)) add("data-ml", 3);
|
|
643
|
+
if (has(/\.(swift|kt)$/) || has(/(^|\/)(ios|android)\//) || has(/package\.swift$/) || has(/build\.gradle(\.kts)?$/) || dep(/"(react-native|expo|flutter)"/) || has(/\.dart$/)) add("mobile", 3);
|
|
644
|
+
if (has(/\.(unity|unitypackage|godot|tscn|gd)$/) || has(/(^|\/)assets\/.*\.(prefab|fbx)$/) || dep(/"(phaser|pixi\.js|three|babylonjs|@react-three\/fiber|excalibur|kaplay|bevy|godot)"/)) add("game", 3);
|
|
645
|
+
if (has(/\.tf$/) || has(/(^|\/)(dockerfile|docker-compose\.ya?ml|helm|k8s|kubernetes|\.github\/workflows|ansible|pulumi)/)) add("infra-devops", has(/\.tf$/) || has(/(^|\/)(helm|k8s|kubernetes|ansible|pulumi)\//) ? 3 : 1);
|
|
646
|
+
const mdShare = count(/\.(md|mdx|rst)$/) / Math.max(1, paths.length);
|
|
647
|
+
if (mdShare > 0.4 || has(/(^|\/)(mkdocs|docusaurus)\.config|(^|\/)docs\/.*\.mdx?$/) && mdShare > 0.25) add("docs-content", 3);
|
|
648
|
+
if (has(/(^|\/)pnpm-workspace\.ya?ml$/) || has(/(^|\/)packages\/[^/]+\/package\.json$/) || has(/(^|\/)(lerna|nx|turbo)\.json$/) || has(/(^|\/)cargo\.toml$/) && count(/(^|\/)crates\/[^/]+\//) > 0) add("monorepo", 2);
|
|
649
|
+
if (count(/\.(rs|go|c|cpp|cc|h|zig)$/) > 5 && !scores.has("web-frontend")) add("systems", 2);
|
|
650
|
+
if (pkg.includes('"main"') && !pkg.includes('"bin"') && !scores.has("web-frontend") && !scores.has("backend-api")) add("library", 2);
|
|
651
|
+
if (has(/(^|\/)(lib|src\/lib)\//) && !scores.has("web-frontend")) add("library", 1);
|
|
652
|
+
const kinds = [...scores.entries()].filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).map(([k]) => k);
|
|
653
|
+
return { kinds: kinds.length ? kinds : ["unknown"], languages, files: files.length };
|
|
654
|
+
}
|
|
655
|
+
function generateWorld(opts) {
|
|
656
|
+
const cap = opts.cap ?? 60;
|
|
657
|
+
const beaconCap = opts.beaconCap ?? 8;
|
|
658
|
+
const touchedCap = opts.touchedCap ?? 48;
|
|
659
|
+
const touched = new Set((opts.touched ?? []).map(norm2).slice(0, touchedCap));
|
|
660
|
+
const seed = hashString(opts.repoId);
|
|
661
|
+
const rnd = mulberry32(seed);
|
|
662
|
+
const files = opts.files.map((f) => ({ ...f, path: norm2(f.path) }));
|
|
663
|
+
const scored = scoreFiles(files, touched);
|
|
664
|
+
const maxLoc = Math.max(1, ...scored.map((s) => s.loc));
|
|
665
|
+
const byScore = [...scored].sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path));
|
|
666
|
+
const beacons = pick(byScore.filter((s) => s.kind === "beacon"), beaconCap);
|
|
667
|
+
const others = pick(byScore.filter((s) => s.kind !== "beacon"), cap);
|
|
668
|
+
const selected = [...others, ...beacons];
|
|
669
|
+
const capped = selected.length < scored.length;
|
|
670
|
+
const groups = /* @__PURE__ */ new Map();
|
|
671
|
+
for (const s of others) {
|
|
672
|
+
const d = topDir(s.file.path);
|
|
673
|
+
(groups.get(d) ?? groups.set(d, []).get(d)).push(s);
|
|
674
|
+
}
|
|
675
|
+
const names = [...groups.keys()].sort();
|
|
676
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
677
|
+
const places = [];
|
|
678
|
+
const districts = [];
|
|
679
|
+
const forge = { id: "forge", kind: "forge", path: "", label: "forge", district: "", gx: -1, gy: -1, footprint: 2, height: 2, score: 0, touched: false };
|
|
680
|
+
places.push(forge);
|
|
681
|
+
const layout = opts.layout ?? LAYOUTS[seed % LAYOUTS.length];
|
|
682
|
+
const jitter = rnd() * Math.PI * 2;
|
|
683
|
+
const sized = names.map((name) => {
|
|
684
|
+
const items = groups.get(name).sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path));
|
|
685
|
+
const cells = items.reduce((n, s) => n + footprintOf(heightClass(s.loc, maxLoc)) ** 2, 0);
|
|
686
|
+
const side = Math.max(3, Math.ceil(Math.sqrt(cells * 1.45)));
|
|
687
|
+
return { name, items, side };
|
|
688
|
+
});
|
|
689
|
+
const placedRects = [];
|
|
690
|
+
const hubRect = { x: -2, y: -2, w: 4, h: 4 };
|
|
691
|
+
const rects = layoutRects(layout, sized.map((d) => d.side), hubRect, jitter, rnd);
|
|
692
|
+
placedRects.push(hubRect, ...rects);
|
|
693
|
+
const hub = hubFor(layout, rects);
|
|
694
|
+
forge.gx = hub[0];
|
|
695
|
+
forge.gy = hub[1];
|
|
696
|
+
occupied.clear();
|
|
697
|
+
claim(occupied, forge);
|
|
698
|
+
sized.forEach((d0, i) => {
|
|
699
|
+
const rect = rects[i];
|
|
700
|
+
const d = { id: `d:${d0.name}`, label: d0.name === "" ? "/" : `${d0.name}/`, bounds: rect, cx: rect.x + Math.floor(rect.w / 2), cy: rect.y + Math.floor(rect.h / 2), placeIds: [] };
|
|
701
|
+
for (const s of d0.items) {
|
|
702
|
+
const h = heightClass(s.loc, maxLoc);
|
|
703
|
+
const fp = footprintOf(h);
|
|
704
|
+
const spot = spiralSpot(occupied, d.cx, d.cy, fp, rect);
|
|
705
|
+
const p = { id: `p:${s.file.path}`, kind: s.kind, path: s.file.path, label: base(s.file.path), district: d.id, gx: spot[0], gy: spot[1], footprint: fp, height: h, score: round3(s.score), touched: s.touched };
|
|
706
|
+
claim(occupied, p);
|
|
707
|
+
places.push(p);
|
|
708
|
+
d.placeIds.push(p.id);
|
|
709
|
+
}
|
|
710
|
+
districts.push(d);
|
|
711
|
+
});
|
|
712
|
+
let ring = Math.ceil(maxExtent(placedRects));
|
|
713
|
+
void ring;
|
|
714
|
+
for (const r of placedRects) for (let x = r.x - 1; x < r.x + r.w + 1; x++) for (let y = r.y - 1; y < r.y + r.h + 1; y++) occupied.add(`${x},${y}`);
|
|
715
|
+
for (const s of beacons) {
|
|
716
|
+
const home = districts.find((d) => d.id === `d:${topDir(s.file.path)}`);
|
|
717
|
+
const spot = home ? edgeSpot(occupied, home, forge, 1) : nearSpot(occupied, forge.gx + 1, forge.gy + 1, rnd() * Math.PI * 2, 4, 1);
|
|
718
|
+
const p = { id: `p:${s.file.path}`, kind: "beacon", path: s.file.path, label: base(s.file.path), district: home?.id ?? "", gx: spot[0], gy: spot[1], footprint: 1, height: 4, score: round3(s.score), touched: s.touched };
|
|
719
|
+
claim(occupied, p);
|
|
720
|
+
places.push(p);
|
|
721
|
+
home?.placeIds.push(p.id);
|
|
722
|
+
}
|
|
723
|
+
const dockSpot = nearSpot(occupied, forge.gx + 1, forge.gy + 1, jitter + Math.PI, 5, 2);
|
|
724
|
+
const dock = { id: "dock", kind: "dock", path: "", label: "dock", district: "", gx: dockSpot[0], gy: dockSpot[1], footprint: 2, height: 1, score: 0, touched: false };
|
|
725
|
+
claim(occupied, dock);
|
|
726
|
+
places.push(dock);
|
|
727
|
+
const paths = layoutPaths(layout, districts, forge, dock);
|
|
728
|
+
if (capped) {
|
|
729
|
+
for (const d of districts) {
|
|
730
|
+
const top = d.id.slice(2);
|
|
731
|
+
const total = scored.filter((s) => s.kind !== "beacon" && topDir(s.file.path) === top).length;
|
|
732
|
+
if (total <= d.placeIds.length) continue;
|
|
733
|
+
const spot = spiralSpot(occupied, d.cx, d.cy, 1, d.bounds);
|
|
734
|
+
const p = { id: `p:${top ? top + "/" : ""}\u2026`, kind: "overflow", path: `${top ? top + "/" : ""}\u2026`, label: `+${total - d.placeIds.length} more`, district: d.id, gx: spot[0], gy: spot[1], footprint: 1, height: 2, score: 0, touched: false };
|
|
735
|
+
claim(occupied, p);
|
|
736
|
+
places.push(p);
|
|
737
|
+
d.placeIds.push(p.id);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const bounds = boundsOf(places, 2);
|
|
741
|
+
const profile = profileRepo(files, opts.manifests ?? {});
|
|
742
|
+
return { repoId: opts.repoId, seed, layout, profile, districts, places, paths, bounds, stats: { files: scored.length, placed: selected.length, capped } };
|
|
743
|
+
}
|
|
744
|
+
var norm2 = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
745
|
+
var base = (p) => p.slice(p.lastIndexOf("/") + 1);
|
|
746
|
+
var topDir = (p) => p.includes("/") ? p.slice(0, p.indexOf("/")) : "";
|
|
747
|
+
var round3 = (n) => Math.round(n * 1e3) / 1e3;
|
|
748
|
+
function pick(sorted, cap) {
|
|
749
|
+
const forced = sorted.filter((s) => s.touched);
|
|
750
|
+
const rest = sorted.filter((s) => !s.touched);
|
|
751
|
+
return [...forced, ...rest.slice(0, Math.max(0, cap - forced.length))];
|
|
752
|
+
}
|
|
753
|
+
function overlaps(a, b, gap) {
|
|
754
|
+
return a.x < b.x + b.w + gap && b.x < a.x + a.w + gap && a.y < b.y + b.h + gap && b.y < a.y + a.h + gap;
|
|
755
|
+
}
|
|
756
|
+
function cellsOf(gx, gy, fp) {
|
|
757
|
+
const out = [];
|
|
758
|
+
for (let dx = 0; dx < fp; dx++) for (let dy = 0; dy < fp; dy++) out.push(`${gx + dx},${gy + dy}`);
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
function free(occ, gx, gy, fp) {
|
|
762
|
+
return cellsOf(gx, gy, fp).every((c) => !occ.has(c));
|
|
763
|
+
}
|
|
764
|
+
function claim(occ, p) {
|
|
765
|
+
for (const c of cellsOf(p.gx, p.gy, p.footprint)) occ.add(c);
|
|
766
|
+
}
|
|
767
|
+
function spiralSpot(occ, cx, cy, fp, rect) {
|
|
768
|
+
let x = 0, y = 0, dx = 0, dy = -1;
|
|
769
|
+
let fallback = null;
|
|
770
|
+
for (let i = 0; i < 4096; i++) {
|
|
771
|
+
const gx = cx + x - Math.floor(fp / 2), gy = cy + y - Math.floor(fp / 2);
|
|
772
|
+
if (free(occ, gx, gy, fp)) {
|
|
773
|
+
const inside = gx >= rect.x - 1 && gy >= rect.y - 1 && gx + fp <= rect.x + rect.w + 1 && gy + fp <= rect.y + rect.h + 1;
|
|
774
|
+
if (inside) return [gx, gy];
|
|
775
|
+
fallback ??= [gx, gy];
|
|
776
|
+
if (i > 600) return fallback;
|
|
777
|
+
}
|
|
778
|
+
if (x === y || x < 0 && x === -y || x > 0 && x === 1 - y) [dx, dy] = [-dy, dx];
|
|
779
|
+
x += dx;
|
|
780
|
+
y += dy;
|
|
781
|
+
}
|
|
782
|
+
return fallback ?? [cx, cy];
|
|
783
|
+
}
|
|
784
|
+
function maxExtent(rects) {
|
|
785
|
+
return Math.max(...rects.map((r) => Math.max(Math.abs(r.x), Math.abs(r.x + r.w), Math.abs(r.y), Math.abs(r.y + r.h))));
|
|
786
|
+
}
|
|
787
|
+
function boundsOf(places, pad) {
|
|
788
|
+
const xs = places.flatMap((p) => [p.gx, p.gx + p.footprint]);
|
|
789
|
+
const ys = places.flatMap((p) => [p.gy, p.gy + p.footprint]);
|
|
790
|
+
const x = Math.min(...xs) - pad, y = Math.min(...ys) - pad;
|
|
791
|
+
return { x, y, w: Math.max(...xs) + pad - x, h: Math.max(...ys) + pad - y };
|
|
792
|
+
}
|
|
793
|
+
var LAYOUTS = ["ring", "grid", "strip", "scatter", "terrace", "spine"];
|
|
794
|
+
function layoutRects(style, sides, hub, jitter, rnd) {
|
|
795
|
+
const n = sides.length, out = [];
|
|
796
|
+
const maxSide = Math.max(3, ...sides);
|
|
797
|
+
const fits = (r) => !overlaps(hub, r, 1) && !out.some((o) => overlaps(o, r, 1));
|
|
798
|
+
const push = (x, y, side) => {
|
|
799
|
+
const r = { x, y, w: side, h: side };
|
|
800
|
+
out.push(r);
|
|
801
|
+
return r;
|
|
802
|
+
};
|
|
803
|
+
switch (style) {
|
|
804
|
+
case "grid": {
|
|
805
|
+
const cols = Math.max(2, Math.ceil(Math.sqrt(n + 1)));
|
|
806
|
+
const cells = [];
|
|
807
|
+
let y = 0, i = 0;
|
|
808
|
+
while (i < n) {
|
|
809
|
+
const row = sides.slice(i, i + cols), rowH = Math.max(...row) + 2;
|
|
810
|
+
let x = 0;
|
|
811
|
+
for (const w of row) {
|
|
812
|
+
cells.push({ x, y });
|
|
813
|
+
x += w + 2;
|
|
814
|
+
}
|
|
815
|
+
y += rowH;
|
|
816
|
+
i += cols;
|
|
817
|
+
}
|
|
818
|
+
const totalW = Math.max(...cells.map((c, j) => c.x + sides[j])), totalH = y;
|
|
819
|
+
for (let j = 0; j < n; j++) {
|
|
820
|
+
let rect = { x: cells[j].x - Math.round(totalW / 2), y: cells[j].y - Math.round(totalH / 2), w: sides[j], h: sides[j] };
|
|
821
|
+
while (!fits(rect)) rect = { ...rect, x: rect.x + 1 };
|
|
822
|
+
out.push(rect);
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
case "strip": {
|
|
827
|
+
const total = sides.reduce((a, b) => a + b + 2, 0);
|
|
828
|
+
let x = Math.round(-total / 2) + 6;
|
|
829
|
+
for (let i = 0; i < n; i++) {
|
|
830
|
+
const wobble = Math.round((rnd() - 0.5) * 3);
|
|
831
|
+
push(x, wobble - Math.floor(sides[i] / 2), sides[i]);
|
|
832
|
+
x += sides[i] + 2;
|
|
833
|
+
}
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
case "terrace": {
|
|
837
|
+
let x = -Math.round(n * (maxSide + 1) / 2), y = -Math.round(n * (maxSide + 1) * 0.3);
|
|
838
|
+
for (let i = 0; i < n; i++) {
|
|
839
|
+
push(x, y, sides[i]);
|
|
840
|
+
x += sides[i] + 1;
|
|
841
|
+
y += Math.round(sides[i] * 0.6) + 1;
|
|
842
|
+
}
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
case "spine": {
|
|
846
|
+
let y = -Math.round(n * (maxSide + 2) / 4);
|
|
847
|
+
for (let i = 0; i < n; i++) {
|
|
848
|
+
const left = i % 2 === 0;
|
|
849
|
+
push(left ? -sides[i] - 3 : 3, y, sides[i]);
|
|
850
|
+
if (!left) y += maxSide + 2;
|
|
851
|
+
}
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
case "scatter": {
|
|
855
|
+
for (let i = 0; i < n; i++) {
|
|
856
|
+
let R = 5 + maxSide, placed = false;
|
|
857
|
+
for (let tries = 0; tries < 400 && !placed; tries++) {
|
|
858
|
+
const a = jitter + rnd() * Math.PI * 2, r = R * (0.5 + rnd() * 0.7);
|
|
859
|
+
const rect = { x: Math.round(Math.cos(a) * r), y: Math.round(Math.sin(a) * r * 0.8), w: sides[i], h: sides[i] };
|
|
860
|
+
if (fits(rect)) {
|
|
861
|
+
out.push(rect);
|
|
862
|
+
placed = true;
|
|
863
|
+
}
|
|
864
|
+
if (tries % 40 === 39) R += 2;
|
|
865
|
+
}
|
|
866
|
+
if (!placed) push(R + i * 2, 0, sides[i]);
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
default: {
|
|
871
|
+
let ring = 3;
|
|
872
|
+
for (let i = 0; i < n; i++) {
|
|
873
|
+
const side = sides[i], angle = jitter + i / Math.max(1, n) * Math.PI * 2;
|
|
874
|
+
let r = ring + side / 2, rect;
|
|
875
|
+
for (; ; ) {
|
|
876
|
+
const cx = Math.round(Math.cos(angle) * r), cy = Math.round(Math.sin(angle) * r * 0.8);
|
|
877
|
+
rect = { x: cx - Math.floor(side / 2), y: cy - Math.floor(side / 2), w: side, h: side };
|
|
878
|
+
if (fits(rect)) break;
|
|
879
|
+
r += 1;
|
|
880
|
+
}
|
|
881
|
+
out.push(rect);
|
|
882
|
+
ring = Math.max(ring, r - side / 2);
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function hubFor(style, rects) {
|
|
889
|
+
if (!rects.length) return [-1, -1];
|
|
890
|
+
const minX = Math.min(...rects.map((r) => r.x)), minY = Math.min(...rects.map((r) => r.y));
|
|
891
|
+
switch (style) {
|
|
892
|
+
case "strip":
|
|
893
|
+
return [minX - 5, -1];
|
|
894
|
+
case "terrace":
|
|
895
|
+
return [minX - 5, minY - 1];
|
|
896
|
+
case "spine":
|
|
897
|
+
return [-1, minY - 5];
|
|
898
|
+
default:
|
|
899
|
+
return [-1, -1];
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function layoutPaths(style, districts, forge, dock) {
|
|
903
|
+
const hub = [forge.gx + 1, forge.gy + 1];
|
|
904
|
+
const paths = [];
|
|
905
|
+
if (style === "strip" || style === "terrace") {
|
|
906
|
+
const ordered = [...districts].sort((a, b) => a.cx - b.cx || a.cy - b.cy);
|
|
907
|
+
let prev = hub, prevId = "forge";
|
|
908
|
+
for (const d of ordered) {
|
|
909
|
+
paths.push({ from: prevId, to: d.id, points: [prev, [d.cx, d.cy]] });
|
|
910
|
+
prev = [d.cx, d.cy];
|
|
911
|
+
prevId = d.id;
|
|
912
|
+
}
|
|
913
|
+
} else if (style === "grid" || style === "spine") {
|
|
914
|
+
for (const d of districts) paths.push({ from: d.id, to: "forge", points: [[d.cx, d.cy], [d.cx, hub[1]], hub] });
|
|
915
|
+
} else {
|
|
916
|
+
for (const d of districts) paths.push({ from: d.id, to: "forge", points: [[d.cx, d.cy], [Math.round((d.cx + hub[0]) / 2), Math.round((d.cy + hub[1]) / 2)], hub] });
|
|
917
|
+
}
|
|
918
|
+
paths.push({ from: "dock", to: "forge", points: [[dock.gx, dock.gy], hub] });
|
|
919
|
+
return paths;
|
|
920
|
+
}
|
|
921
|
+
function nearSpot(occ, cx, cy, ang, radius, fp) {
|
|
922
|
+
for (let r = radius; r < radius + 40; r++) {
|
|
923
|
+
for (let k = 0; k < 64; k++) {
|
|
924
|
+
const a = ang + (k % 2 ? 1 : -1) * Math.ceil(k / 2) * (Math.PI / 32);
|
|
925
|
+
const gx = Math.round(cx + Math.cos(a) * r), gy = Math.round(cy + Math.sin(a) * r * 0.85);
|
|
926
|
+
if (free(occ, gx, gy, fp)) return [gx, gy];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return [cx + radius, cy];
|
|
930
|
+
}
|
|
931
|
+
function edgeSpot(occ, d, hub, fp) {
|
|
932
|
+
const ang = Math.atan2(d.cy - hub.gy, d.cx - hub.gx);
|
|
933
|
+
const r0 = Math.ceil(Math.max(d.bounds.w, d.bounds.h) / 2) + 2;
|
|
934
|
+
return nearSpot(occ, d.cx, d.cy, ang, r0, fp);
|
|
935
|
+
}
|
|
936
|
+
var MoodTracker = class {
|
|
937
|
+
constructor(windowMs) {
|
|
938
|
+
this.windowMs = windowMs;
|
|
939
|
+
}
|
|
940
|
+
windowMs;
|
|
941
|
+
mood = "curious";
|
|
942
|
+
trouble = [];
|
|
943
|
+
recordTrouble(ts) {
|
|
944
|
+
this.trouble = this.trouble.filter((t) => ts - t <= this.windowMs);
|
|
945
|
+
this.trouble.push(ts);
|
|
946
|
+
return this.trouble.length >= 2;
|
|
947
|
+
}
|
|
948
|
+
onRead() {
|
|
949
|
+
return this.mood = this.mood === "lost" ? "lost" : "curious";
|
|
950
|
+
}
|
|
951
|
+
onBuild() {
|
|
952
|
+
return this.mood = this.mood === "lost" ? "lost" : "focused";
|
|
953
|
+
}
|
|
954
|
+
onFail(ts) {
|
|
955
|
+
return this.mood = this.recordTrouble(ts) ? "lost" : "uneasy";
|
|
956
|
+
}
|
|
957
|
+
onError(ts) {
|
|
958
|
+
return this.mood = this.recordTrouble(ts) ? "lost" : "uneasy";
|
|
959
|
+
}
|
|
960
|
+
onPass() {
|
|
961
|
+
this.trouble = [];
|
|
962
|
+
return this.mood = "happy";
|
|
963
|
+
}
|
|
964
|
+
onClaimUnverified() {
|
|
965
|
+
return this.mood = "uneasy";
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
var Director = class {
|
|
969
|
+
constructor(world, opts = {}) {
|
|
970
|
+
this.world = world;
|
|
971
|
+
for (const p of world.places) {
|
|
972
|
+
this.places.set(p.id, p);
|
|
973
|
+
if (p.path) this.byPath.set(p.path, p);
|
|
974
|
+
}
|
|
975
|
+
this.beacons = world.places.filter((p) => p.kind === "beacon");
|
|
976
|
+
this.forge = world.places.find((p) => p.kind === "forge") ?? world.places[0];
|
|
977
|
+
this.dock = world.places.find((p) => p.kind === "dock") ?? this.forge;
|
|
978
|
+
this.maxGap = opts.maxGapMs ?? 2500;
|
|
979
|
+
this.msPerCell = opts.walkMsPerCell ?? 150;
|
|
980
|
+
this.mood = new MoodTracker(opts.lostWindowMs ?? 9e4);
|
|
981
|
+
this.actorAt.set("pip", this.forge.id);
|
|
982
|
+
}
|
|
983
|
+
world;
|
|
984
|
+
places = /* @__PURE__ */ new Map();
|
|
985
|
+
byPath = /* @__PURE__ */ new Map();
|
|
986
|
+
beacons;
|
|
987
|
+
forge;
|
|
988
|
+
dock;
|
|
989
|
+
maxGap;
|
|
990
|
+
msPerCell;
|
|
991
|
+
mood;
|
|
992
|
+
beats = [];
|
|
993
|
+
sceneMs = 0;
|
|
994
|
+
lastRealTs = null;
|
|
995
|
+
seq = 0;
|
|
996
|
+
actorAt = /* @__PURE__ */ new Map();
|
|
997
|
+
helpers = /* @__PURE__ */ new Map();
|
|
998
|
+
gremlins = /* @__PURE__ */ new Map();
|
|
999
|
+
// gremlinId -> place
|
|
1000
|
+
lastEdited = null;
|
|
1001
|
+
pendingBeacon = null;
|
|
1002
|
+
editedSinceTurn = false;
|
|
1003
|
+
verifiedSinceEdit = false;
|
|
1004
|
+
everVerified = false;
|
|
1005
|
+
claimGremlin = null;
|
|
1006
|
+
get timeline() {
|
|
1007
|
+
return this.beats;
|
|
1008
|
+
}
|
|
1009
|
+
feed(e) {
|
|
1010
|
+
const start = this.beats.length;
|
|
1011
|
+
this.advanceClock(e.ts, e.id);
|
|
1012
|
+
const actor = e.agentId && this.helpers.has(e.agentId) ? e.agentId : "pip";
|
|
1013
|
+
switch (e.kind) {
|
|
1014
|
+
case "session_start":
|
|
1015
|
+
this.emit(e, actor, { kind: "curtain_up", repoId: e.repoId }, 1200);
|
|
1016
|
+
break;
|
|
1017
|
+
case "read_file": {
|
|
1018
|
+
const p = this.placeFor(e.path, e, actor);
|
|
1019
|
+
this.walk(e, actor, p);
|
|
1020
|
+
this.emit(e, actor, { kind: "read", target: p.id }, clamp(700 + (e.bytes ?? 400) / 4, 900, 1800), this.mood.onRead());
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
case "edit_file":
|
|
1024
|
+
case "write_file": {
|
|
1025
|
+
const p = this.placeFor(e.path, e, actor);
|
|
1026
|
+
this.walk(e, actor, p);
|
|
1027
|
+
const lines = e.kind === "edit_file" ? e.added + e.removed : e.added;
|
|
1028
|
+
const intensity = clamp(Math.sqrt(lines / 50), 0.15, 1);
|
|
1029
|
+
const g = [...this.gremlins].find(([, at]) => at === p.id);
|
|
1030
|
+
if (g) this.emit(e, actor, { kind: "chase", gremlinId: g[0], target: p.id }, 900, this.mood.mood);
|
|
1031
|
+
if (e.kind === "write_file" && e.created) this.emit(e, actor, { kind: "raise", target: p.id }, 900, this.mood.onBuild());
|
|
1032
|
+
this.emit(e, actor, { kind: "build", target: p.id, intensity }, clamp(600 + intensity * 1900, 800, 2500), this.mood.onBuild());
|
|
1033
|
+
this.lastEdited = p;
|
|
1034
|
+
this.editedSinceTurn = true;
|
|
1035
|
+
this.verifiedSinceEdit = false;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
case "run_command": {
|
|
1039
|
+
if (e.intent === "test") {
|
|
1040
|
+
const beacon = this.beaconFor(e.touched);
|
|
1041
|
+
this.walk(e, actor, beacon);
|
|
1042
|
+
this.pendingBeacon = beacon;
|
|
1043
|
+
this.emit(e, actor, { kind: "run_tests", target: beacon.id }, clamp(e.durationMs ?? 1200, 900, 3e3), this.mood.mood);
|
|
1044
|
+
} else if (e.intent === "search") {
|
|
1045
|
+
this.emit(e, actor, { kind: "search", scope: "repo" }, 700, this.mood.onRead());
|
|
1046
|
+
} else if (e.touched.length === 0 || e.intent === "git" || e.intent === "install" || e.intent === "build") {
|
|
1047
|
+
this.walk(e, actor, this.forge);
|
|
1048
|
+
const intensity = e.intent === "build" || e.intent === "install" ? 0.8 : e.intent === "git" ? 0.4 : 0.5;
|
|
1049
|
+
this.emit(e, actor, { kind: "forge", intensity }, clamp(e.durationMs ?? 800, 700, 2e3), this.mood.mood);
|
|
1050
|
+
}
|
|
1051
|
+
if (!e.ok && e.intent !== "test") this.emit(e, actor, { kind: "stumble", retrying: false }, 600, this.mood.onError(e.ts));
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
case "test_result": {
|
|
1055
|
+
const beacon = this.pendingBeacon ?? this.beaconFor([]);
|
|
1056
|
+
this.pendingBeacon = null;
|
|
1057
|
+
const outcome = e.framework === "unknown" && e.passed === 0 && e.failed === 0 ? e.ok ? "pass" : "fail" : e.ok ? "pass" : "fail";
|
|
1058
|
+
if (outcome === "pass") {
|
|
1059
|
+
this.emit(e, actor, { kind: "test_outcome", target: beacon.id, outcome, passed: e.passed, failed: e.failed }, 800, this.mood.onPass());
|
|
1060
|
+
if (e.passed > 0 || e.framework === "unknown") {
|
|
1061
|
+
this.verifiedSinceEdit = true;
|
|
1062
|
+
this.everVerified = true;
|
|
1063
|
+
}
|
|
1064
|
+
for (const [gid] of this.gremlins) this.emit(e, actor, { kind: "banish", gremlinId: gid }, 700, "happy");
|
|
1065
|
+
this.gremlins.clear();
|
|
1066
|
+
if (this.claimGremlin) {
|
|
1067
|
+
this.emit(e, actor, { kind: "banish", gremlinId: this.claimGremlin }, 700, "happy");
|
|
1068
|
+
this.claimGremlin = null;
|
|
1069
|
+
}
|
|
1070
|
+
this.emit(e, actor, { kind: "celebrate", target: beacon.id }, 1600, "happy");
|
|
1071
|
+
} else {
|
|
1072
|
+
this.emit(e, actor, { kind: "test_outcome", target: beacon.id, outcome, passed: e.passed, failed: e.failed }, 800, this.mood.onFail(e.ts));
|
|
1073
|
+
const at = this.lastEdited ?? beacon;
|
|
1074
|
+
const gid = `g:${e.id}`;
|
|
1075
|
+
this.gremlins.set(gid, at.id);
|
|
1076
|
+
this.emit(e, actor, { kind: "spawn_gremlin", gremlinId: gid, target: at.id }, 900, this.mood.mood);
|
|
1077
|
+
}
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
case "search":
|
|
1081
|
+
this.emit(e, actor, { kind: "search", scope: e.scope }, 700, this.mood.onRead());
|
|
1082
|
+
break;
|
|
1083
|
+
case "subagent_start":
|
|
1084
|
+
this.helpers.set(e.childId, e.label ? { label: e.label } : {});
|
|
1085
|
+
this.actorAt.set(e.childId, this.dock.id);
|
|
1086
|
+
this.emit(e, "pip", { kind: "summon_helper", helperId: e.childId, ...e.label ? { label: e.label } : {} }, 1400, this.mood.mood);
|
|
1087
|
+
break;
|
|
1088
|
+
case "subagent_stop":
|
|
1089
|
+
if (this.helpers.has(e.childId)) {
|
|
1090
|
+
this.emit(e, e.childId, { kind: "dismiss_helper", helperId: e.childId, ok: e.ok }, 1e3, this.mood.mood);
|
|
1091
|
+
this.helpers.delete(e.childId);
|
|
1092
|
+
this.actorAt.delete(e.childId);
|
|
1093
|
+
}
|
|
1094
|
+
break;
|
|
1095
|
+
case "waiting_for_user":
|
|
1096
|
+
if (e.reason === "idle") this.emit(e, actor, { kind: "your_turn" }, 1e3, this.mood.mood);
|
|
1097
|
+
else this.emit(e, actor, { kind: "blocked_on_you", reason: e.reason }, 1200, this.mood.mood);
|
|
1098
|
+
break;
|
|
1099
|
+
case "error":
|
|
1100
|
+
this.emit(e, actor, { kind: "stumble", retrying: e.retrying }, 700, this.mood.onError(e.ts));
|
|
1101
|
+
break;
|
|
1102
|
+
case "turn_end": {
|
|
1103
|
+
if (e.claimedSuccess && this.editedSinceTurn && !this.verifiedSinceEdit) {
|
|
1104
|
+
const gid = `claim:${e.id}`;
|
|
1105
|
+
this.claimGremlin = gid;
|
|
1106
|
+
this.emit(e, actor, { kind: "unverified_claim", gremlinId: gid }, 1400, this.mood.onClaimUnverified());
|
|
1107
|
+
}
|
|
1108
|
+
this.editedSinceTurn = false;
|
|
1109
|
+
this.emit(e, actor, { kind: "your_turn", ...e.note ? { note: e.note } : {} }, 1e3, this.mood.mood);
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
case "session_end":
|
|
1113
|
+
this.emit(e, actor, { kind: "curtain_down", verified: this.everVerified && !this.claimGremlin }, 1500, this.mood.mood);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
return this.beats.slice(start);
|
|
1117
|
+
}
|
|
1118
|
+
// ---- internals ----
|
|
1119
|
+
advanceClock(ts, eventId) {
|
|
1120
|
+
if (this.lastRealTs !== null) {
|
|
1121
|
+
const lastEnd = this.beats.length ? Math.max(...this.beats.slice(-3).map((b) => b.atMs + b.durationMs)) : this.sceneMs;
|
|
1122
|
+
const gap = Math.min(Math.max(0, ts - this.lastRealTs), this.maxGap);
|
|
1123
|
+
const target = Math.max(lastEnd, this.sceneMs) + gap;
|
|
1124
|
+
if (target - lastEnd >= 1e3) {
|
|
1125
|
+
const idle = { id: `b${this.seq++}`, atMs: lastEnd, durationMs: target - lastEnd, realTs: this.lastRealTs, mood: this.mood.mood, actor: "pip", sourceEventIds: [eventId], kind: "idle" };
|
|
1126
|
+
this.beats.push(idle);
|
|
1127
|
+
}
|
|
1128
|
+
this.sceneMs = Math.max(this.sceneMs, target);
|
|
1129
|
+
}
|
|
1130
|
+
this.lastRealTs = ts;
|
|
1131
|
+
}
|
|
1132
|
+
emit(e, actor, body, durationMs, mood = this.mood.mood) {
|
|
1133
|
+
durationMs = Math.round(durationMs);
|
|
1134
|
+
if ("intensity" in body) body = { ...body, intensity: Math.round(body.intensity * 1e3) / 1e3 };
|
|
1135
|
+
const b = { id: `b${this.seq++}`, atMs: Math.round(this.sceneMs), durationMs, realTs: e.ts, mood, actor, sourceEventIds: [e.id], ...body };
|
|
1136
|
+
this.beats.push(b);
|
|
1137
|
+
this.sceneMs += durationMs;
|
|
1138
|
+
return b;
|
|
1139
|
+
}
|
|
1140
|
+
walk(e, actor, to) {
|
|
1141
|
+
const fromId = this.actorAt.get(actor) ?? this.forge.id;
|
|
1142
|
+
if (fromId === to.id) return;
|
|
1143
|
+
const from = this.places.get(fromId) ?? this.forge;
|
|
1144
|
+
const cells = Math.hypot(to.gx - from.gx, to.gy - from.gy);
|
|
1145
|
+
this.emit(e, actor, { kind: "walk_to", target: to.id }, clamp(Math.round(cells * this.msPerCell), 700, 3500));
|
|
1146
|
+
this.actorAt.set(actor, to.id);
|
|
1147
|
+
}
|
|
1148
|
+
raised = 0;
|
|
1149
|
+
/** Known place; else the district's overflow building; else (bounded) a new one raised for a file created mid-session. */
|
|
1150
|
+
placeFor(path, e, actor) {
|
|
1151
|
+
const known = this.byPath.get(path);
|
|
1152
|
+
if (known) return known;
|
|
1153
|
+
const top = path.includes("/") ? path.slice(0, path.indexOf("/")) : "";
|
|
1154
|
+
const overflow = this.byPath.get(`${top ? top + "/" : ""}\u2026`);
|
|
1155
|
+
if (overflow && (this.raised >= 24 || !this.world.stats.capped === false)) return overflow;
|
|
1156
|
+
if (this.raised >= 24) return overflow ?? this.forge;
|
|
1157
|
+
this.raised++;
|
|
1158
|
+
const district = this.world.districts.find((d) => d.id === `d:${top}`) ?? this.world.districts[0];
|
|
1159
|
+
const anchor = district ? { gx: district.cx, gy: district.cy } : { gx: this.forge.gx + 3, gy: this.forge.gy };
|
|
1160
|
+
const place = { id: `p:${path}`, kind: kindOf(path), path, label: path.slice(path.lastIndexOf("/") + 1), district: district?.id ?? "", gx: anchor.gx, gy: anchor.gy, footprint: 1, height: 1, score: 0, touched: true };
|
|
1161
|
+
this.places.set(place.id, place);
|
|
1162
|
+
this.byPath.set(path, place);
|
|
1163
|
+
if (place.kind === "beacon") this.beacons.push(place);
|
|
1164
|
+
this.emit(e, actor, { kind: "raise", target: place.id, newPlace: { path, label: place.label, kind: place.kind, district: place.district } }, 900);
|
|
1165
|
+
return place;
|
|
1166
|
+
}
|
|
1167
|
+
/** The beacon a test run belongs to: an explicitly touched test file, else the last-edited district's, else the first, else the forge. */
|
|
1168
|
+
beaconFor(touched) {
|
|
1169
|
+
const t = touched.find((p) => isTestPath(p));
|
|
1170
|
+
if (t) {
|
|
1171
|
+
const b = this.byPath.get(t);
|
|
1172
|
+
if (b) return b;
|
|
1173
|
+
}
|
|
1174
|
+
if (this.lastEdited) {
|
|
1175
|
+
const same = this.beacons.find((b) => b.district === this.lastEdited.district);
|
|
1176
|
+
if (same) return same;
|
|
1177
|
+
}
|
|
1178
|
+
return this.beacons[0] ?? this.forge;
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
function direct(events, world, opts) {
|
|
1182
|
+
const d = new Director(world, opts);
|
|
1183
|
+
for (const e of events) d.feed(e);
|
|
1184
|
+
return [...d.timeline];
|
|
1185
|
+
}
|
|
1186
|
+
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1187
|
+
function filesFromEvents(events) {
|
|
1188
|
+
const loc = /* @__PURE__ */ new Map();
|
|
1189
|
+
const bump = (p, n) => loc.set(p, (loc.get(p) ?? 0) + n);
|
|
1190
|
+
for (const e of events) {
|
|
1191
|
+
if (e.kind === "read_file") bump(e.path, Math.max(5, Math.round((e.bytes ?? 400) / 40)));
|
|
1192
|
+
else if (e.kind === "edit_file") bump(e.path, Math.max(5, (e.added + e.removed) * 3));
|
|
1193
|
+
else if (e.kind === "write_file") bump(e.path, Math.max(5, e.added * 2));
|
|
1194
|
+
else if (e.kind === "run_command") for (const p of e.touched) bump(p, 5);
|
|
1195
|
+
}
|
|
1196
|
+
const touched = [...loc.keys()].sort((a, b) => loc.get(b) - loc.get(a));
|
|
1197
|
+
return { files: touched.map((path) => ({ path, loc: loc.get(path) })), touched };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/server.ts
|
|
1201
|
+
var Theatre = class {
|
|
1202
|
+
constructor(opts) {
|
|
1203
|
+
this.opts = opts;
|
|
1204
|
+
const world = generateWorld({ repoId: opts.repoId, files: opts.files });
|
|
1205
|
+
this.live = { world, director: new Director(world), events: [], beats: [], lastEventAt: 0, sessionOpen: false };
|
|
1206
|
+
this.routes();
|
|
1207
|
+
}
|
|
1208
|
+
opts;
|
|
1209
|
+
app = new Hono();
|
|
1210
|
+
server = null;
|
|
1211
|
+
wss = null;
|
|
1212
|
+
live;
|
|
1213
|
+
signal = "ok";
|
|
1214
|
+
/** The most recent cwd of a Claude Code session that is NOT this repo (so an empty stage can point the person there). */
|
|
1215
|
+
elsewhere = null;
|
|
1216
|
+
openSessions = /* @__PURE__ */ new Set();
|
|
1217
|
+
seen = /* @__PURE__ */ new Set();
|
|
1218
|
+
timer = null;
|
|
1219
|
+
port = 0;
|
|
1220
|
+
routes() {
|
|
1221
|
+
const a = this.app;
|
|
1222
|
+
a.post("/hook", async (c) => {
|
|
1223
|
+
let payload;
|
|
1224
|
+
try {
|
|
1225
|
+
payload = await c.req.json();
|
|
1226
|
+
} catch {
|
|
1227
|
+
return c.body(null, 204);
|
|
1228
|
+
}
|
|
1229
|
+
const cwd = typeof payload?.cwd === "string" ? payload.cwd : "";
|
|
1230
|
+
if (cwd && !isWithin(cwd, this.opts.cwd)) {
|
|
1231
|
+
if (this.elsewhere !== cwd) {
|
|
1232
|
+
this.elsewhere = cwd;
|
|
1233
|
+
this.broadcast({ type: "elsewhere", cwd });
|
|
1234
|
+
}
|
|
1235
|
+
return c.body(null, 204);
|
|
1236
|
+
}
|
|
1237
|
+
const sid = payload.session_id;
|
|
1238
|
+
const hookName = payload.hook_event_name;
|
|
1239
|
+
if (typeof sid === "string" && sid) {
|
|
1240
|
+
if (hookName === "SessionEnd") this.openSessions.delete(sid);
|
|
1241
|
+
else this.openSessions.add(sid);
|
|
1242
|
+
}
|
|
1243
|
+
let { events } = hookPayloadToEvents(payload, { home: homedir2() });
|
|
1244
|
+
events = events.filter((e) => {
|
|
1245
|
+
if (this.seen.has(e.id)) return false;
|
|
1246
|
+
this.seen.add(e.id);
|
|
1247
|
+
if (this.seen.size > 5e3) {
|
|
1248
|
+
const first = this.seen.values().next().value;
|
|
1249
|
+
if (first !== void 0) this.seen.delete(first);
|
|
1250
|
+
}
|
|
1251
|
+
return true;
|
|
1252
|
+
});
|
|
1253
|
+
if (this.openSessions.size > 0) events = events.filter((e) => e.kind !== "session_end");
|
|
1254
|
+
if (events.length) this.ingest(events);
|
|
1255
|
+
return c.body(null, 204);
|
|
1256
|
+
});
|
|
1257
|
+
const cors = { "access-control-allow-origin": "*", "access-control-allow-headers": "content-type", "access-control-allow-methods": "GET, POST, OPTIONS", "access-control-allow-private-network": "true" };
|
|
1258
|
+
a.use("/api/*", async (c, next) => {
|
|
1259
|
+
await next();
|
|
1260
|
+
for (const [k, v] of Object.entries(cors)) c.res.headers.set(k, v);
|
|
1261
|
+
});
|
|
1262
|
+
a.options("/api/*", (c) => c.body(null, 204, cors));
|
|
1263
|
+
a.get("/api/scene", (c) => c.json(this.scene()));
|
|
1264
|
+
a.get("/api/sessions", (c) => c.json(listSessions(this.opts.cwd).map((s) => ({ id: s.id, title: s.title, mtime: s.mtime, bytes: s.bytes }))));
|
|
1265
|
+
a.get("/api/replay/:id", (c) => {
|
|
1266
|
+
const id = c.req.param("id");
|
|
1267
|
+
const s = listSessions(this.opts.cwd).find((x) => x.id === id);
|
|
1268
|
+
if (!s) return c.json({ error: "no such session" }, 404);
|
|
1269
|
+
const { events, warnings } = parseTranscript(readFileSync2(s.path, "utf8"), { home: homedir2() });
|
|
1270
|
+
const inferred = filesFromEvents(events);
|
|
1271
|
+
const known = new Set(this.opts.files.map((f) => f.path));
|
|
1272
|
+
const files = [...this.opts.files, ...inferred.files.filter((f) => !known.has(f.path))];
|
|
1273
|
+
const world = generateWorld({ repoId: this.opts.repoId, files, touched: inferred.touched });
|
|
1274
|
+
const beats = direct(events, world);
|
|
1275
|
+
return c.json({ mode: "replay", repoId: this.opts.repoId, title: s.title, world, events, beats, warnings: warnings.length });
|
|
1276
|
+
});
|
|
1277
|
+
a.get("/healthz", (c) => c.json({ ok: true, port: this.port, events: this.live.events.length }));
|
|
1278
|
+
if (this.opts.webDir) {
|
|
1279
|
+
const dir = this.opts.webDir;
|
|
1280
|
+
a.get("/*", (c) => {
|
|
1281
|
+
const p = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
1282
|
+
const file = join2(dir, p);
|
|
1283
|
+
if (!file.startsWith(dir) || !existsSync(file)) return c.text("not found", 404);
|
|
1284
|
+
const ext = p.slice(p.lastIndexOf(".") + 1);
|
|
1285
|
+
const type = { html: "text/html; charset=utf-8", js: "text/javascript", css: "text/css", json: "application/json", jsonl: "application/x-ndjson", svg: "image/svg+xml", png: "image/png", woff2: "font/woff2", webp: "image/webp" };
|
|
1286
|
+
return c.body(readFileSync2(file), 200, { "content-type": type[ext] ?? "application/octet-stream", "cache-control": p.includes("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/** Append events to the live session, direct them, and broadcast. */
|
|
1291
|
+
ingest(events) {
|
|
1292
|
+
const l = this.live;
|
|
1293
|
+
const out = [];
|
|
1294
|
+
for (const e of events) {
|
|
1295
|
+
if (e.kind === "session_start") l.sessionOpen = true;
|
|
1296
|
+
if (e.kind === "session_end") l.sessionOpen = false;
|
|
1297
|
+
l.events.push(e);
|
|
1298
|
+
out.push(...l.director.feed(e));
|
|
1299
|
+
}
|
|
1300
|
+
l.beats.push(...out);
|
|
1301
|
+
l.lastEventAt = Date.now();
|
|
1302
|
+
if (this.signal !== "ok") {
|
|
1303
|
+
this.signal = "ok";
|
|
1304
|
+
this.broadcast({ type: "signal", state: "ok" });
|
|
1305
|
+
}
|
|
1306
|
+
this.broadcast({ type: "beats", beats: out, events });
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
/** In-process subscribers (the VS Code extension relays these to its webview). */
|
|
1310
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1311
|
+
broadcast(msg) {
|
|
1312
|
+
for (const l of this.listeners) {
|
|
1313
|
+
try {
|
|
1314
|
+
l(msg);
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const data = JSON.stringify(msg);
|
|
1319
|
+
for (const ws of this.wss?.clients ?? []) if (ws.readyState === 1) ws.send(data);
|
|
1320
|
+
}
|
|
1321
|
+
async listen(port = 0) {
|
|
1322
|
+
const listener = getRequestListener(this.app.fetch);
|
|
1323
|
+
this.server = createServer(listener);
|
|
1324
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
1325
|
+
this.server.on("upgrade", (req, socket, head) => {
|
|
1326
|
+
if (req.url?.startsWith("/live")) this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1327
|
+
this.wss.emit("connection", ws, req);
|
|
1328
|
+
ws.send(JSON.stringify({ type: "hello", signal: this.signal }));
|
|
1329
|
+
});
|
|
1330
|
+
else socket.destroy();
|
|
1331
|
+
});
|
|
1332
|
+
await new Promise((res) => this.server.listen(port, "127.0.0.1", res));
|
|
1333
|
+
const addr = this.server.address();
|
|
1334
|
+
this.port = typeof addr === "object" && addr ? addr.port : port;
|
|
1335
|
+
const home = this.opts.home ?? join2(homedir2(), ".agent-theatre");
|
|
1336
|
+
mkdirSync(home, { recursive: true });
|
|
1337
|
+
try {
|
|
1338
|
+
const old = parseInt(readFileSync2(join2(home, "port"), "utf8").trim(), 10);
|
|
1339
|
+
if (old > 0 && old !== this.port && portAlive(old) && !existsSync(join2(home, "ports", String(old)))) {
|
|
1340
|
+
mkdirSync(join2(home, "ports"), { recursive: true });
|
|
1341
|
+
writeFileSync(join2(home, "ports", String(old)), "(started by an older build)");
|
|
1342
|
+
}
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
writeFileSync(join2(home, "port"), String(this.port));
|
|
1346
|
+
mkdirSync(join2(home, "ports"), { recursive: true });
|
|
1347
|
+
for (const f of readdirSync2(join2(home, "ports"))) {
|
|
1348
|
+
const n = parseInt(f, 10);
|
|
1349
|
+
if (n > 0 && n !== this.port && !portAlive(n)) {
|
|
1350
|
+
try {
|
|
1351
|
+
rmSync(join2(home, "ports", f));
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
writeFileSync(join2(home, "ports", String(this.port)), this.opts.cwd);
|
|
1357
|
+
const lostAfter = this.opts.lostAfterMs ?? 9e4;
|
|
1358
|
+
this.timer = setInterval(() => {
|
|
1359
|
+
const l = this.live;
|
|
1360
|
+
if (l.sessionOpen && l.lastEventAt && Date.now() - l.lastEventAt > lostAfter && this.signal === "ok") {
|
|
1361
|
+
this.signal = "lost";
|
|
1362
|
+
this.broadcast({ type: "signal", state: "lost" });
|
|
1363
|
+
}
|
|
1364
|
+
}, 5e3);
|
|
1365
|
+
this.timer.unref();
|
|
1366
|
+
return this.port;
|
|
1367
|
+
}
|
|
1368
|
+
async close() {
|
|
1369
|
+
if (this.timer) clearInterval(this.timer);
|
|
1370
|
+
const home = this.opts.home ?? join2(homedir2(), ".agent-theatre");
|
|
1371
|
+
try {
|
|
1372
|
+
if (readFileSync2(join2(home, "port"), "utf8").trim() === String(this.port)) rmSync(join2(home, "port"));
|
|
1373
|
+
} catch {
|
|
1374
|
+
}
|
|
1375
|
+
try {
|
|
1376
|
+
rmSync(join2(home, "ports", String(this.port)));
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
for (const ws of this.wss?.clients ?? []) ws.close();
|
|
1380
|
+
this.wss?.close();
|
|
1381
|
+
await new Promise((res) => this.server ? this.server.close(() => res()) : res());
|
|
1382
|
+
}
|
|
1383
|
+
/** The live scene as /api/scene returns it. */
|
|
1384
|
+
scene() {
|
|
1385
|
+
return { mode: "live", repoId: this.opts.repoId, cwd: this.opts.cwd, elsewhere: this.elsewhere, world: this.live.world, events: this.live.events, beats: this.live.beats, signal: this.signal };
|
|
1386
|
+
}
|
|
1387
|
+
get url() {
|
|
1388
|
+
return `http://127.0.0.1:${this.port}/`;
|
|
1389
|
+
}
|
|
1390
|
+
get name() {
|
|
1391
|
+
return basename2(this.opts.cwd);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
function isWithin(dir, root) {
|
|
1395
|
+
const norm3 = (p) => {
|
|
1396
|
+
try {
|
|
1397
|
+
return realpathSync(p);
|
|
1398
|
+
} catch {
|
|
1399
|
+
return resolve(p);
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
const d = norm3(dir), r = norm3(root);
|
|
1403
|
+
return d === r || d.startsWith(r.endsWith(sep) ? r : r + sep);
|
|
1404
|
+
}
|
|
1405
|
+
function portAlive(port) {
|
|
1406
|
+
const r = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8", timeout: 2e3 });
|
|
1407
|
+
if (r.status === 0 && r.stdout.trim()) return true;
|
|
1408
|
+
if (r.error) return true;
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/scan.ts
|
|
1413
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
1414
|
+
import { join as join3, relative } from "path";
|
|
1415
|
+
function scanRepo(root, opts = {}) {
|
|
1416
|
+
const maxFiles = opts.maxFiles ?? 2e4, maxBytes = opts.maxBytesForLoc ?? 512e3;
|
|
1417
|
+
const files = [];
|
|
1418
|
+
const walk = (dir) => {
|
|
1419
|
+
if (files.length >= maxFiles) return;
|
|
1420
|
+
let ents;
|
|
1421
|
+
try {
|
|
1422
|
+
ents = readdirSync3(dir, { withFileTypes: true });
|
|
1423
|
+
} catch {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
for (const ent of ents) {
|
|
1427
|
+
if (files.length >= maxFiles) return;
|
|
1428
|
+
const full = join3(dir, ent.name);
|
|
1429
|
+
const rel = relative(root, full).replace(/\\/g, "/");
|
|
1430
|
+
if (isIgnoredPath(rel + (ent.isDirectory() ? "/" : ""))) continue;
|
|
1431
|
+
if (ent.isDirectory()) walk(full);
|
|
1432
|
+
else if (ent.isFile()) {
|
|
1433
|
+
let size = 0;
|
|
1434
|
+
try {
|
|
1435
|
+
size = statSync2(full).size;
|
|
1436
|
+
} catch {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
let loc;
|
|
1440
|
+
if (size < maxBytes) {
|
|
1441
|
+
try {
|
|
1442
|
+
const buf = readFileSync3(full);
|
|
1443
|
+
if (!buf.subarray(0, 1024).includes(0)) loc = buf.toString("utf8").split("\n").length;
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
files.push({ path: rel, size, ...loc !== void 0 ? { loc } : {} });
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
walk(root);
|
|
1452
|
+
return files;
|
|
1453
|
+
}
|
|
1454
|
+
function repoIdFor(root) {
|
|
1455
|
+
try {
|
|
1456
|
+
const cfg = readFileSync3(join3(root, ".git", "config"), "utf8");
|
|
1457
|
+
const m = /\[remote "origin"\]\s*\n\s*url\s*=\s*(\S+)/.exec(cfg);
|
|
1458
|
+
if (m) return m[1].replace(/\.git$/, "").replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "");
|
|
1459
|
+
} catch {
|
|
1460
|
+
}
|
|
1461
|
+
return root;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/hooks/install.ts
|
|
1465
|
+
import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1466
|
+
import { homedir as homedir3 } from "os";
|
|
1467
|
+
import { join as join4 } from "path";
|
|
1468
|
+
|
|
1469
|
+
// src/hooks/patch.ts
|
|
1470
|
+
var HOOK_MARKER = "agent-theatre-hook.mjs";
|
|
1471
|
+
var HOOK_EVENTS = [
|
|
1472
|
+
{ event: "SessionStart" },
|
|
1473
|
+
{ event: "PostToolUse" },
|
|
1474
|
+
{ event: "PostToolUseFailure" },
|
|
1475
|
+
{ event: "SubagentStart" },
|
|
1476
|
+
{ event: "SubagentStop" },
|
|
1477
|
+
{ event: "Notification" },
|
|
1478
|
+
{ event: "Stop" },
|
|
1479
|
+
{ event: "SessionEnd" }
|
|
1480
|
+
];
|
|
1481
|
+
var HOOK_TIMEOUT_S = 5;
|
|
1482
|
+
function parse(existing) {
|
|
1483
|
+
if (existing === null || existing.trim() === "") return {};
|
|
1484
|
+
try {
|
|
1485
|
+
const obj4 = JSON.parse(existing);
|
|
1486
|
+
if (!obj4 || typeof obj4 !== "object" || Array.isArray(obj4)) throw new Error("not an object");
|
|
1487
|
+
return obj4;
|
|
1488
|
+
} catch {
|
|
1489
|
+
throw new Error("existing settings.json is not valid JSON; refusing to overwrite it");
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
var isOurs = (group) => {
|
|
1493
|
+
const g = group;
|
|
1494
|
+
return Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER));
|
|
1495
|
+
};
|
|
1496
|
+
function patchSettings(existing, scriptPath) {
|
|
1497
|
+
const obj4 = parse(existing);
|
|
1498
|
+
const hooks = obj4.hooks ??= {};
|
|
1499
|
+
const cmd = `node "${scriptPath}"`;
|
|
1500
|
+
let changed = false;
|
|
1501
|
+
for (const { event, matcher } of HOOK_EVENTS) {
|
|
1502
|
+
const list = hooks[event] ??= [];
|
|
1503
|
+
const mine = list.find(isOurs);
|
|
1504
|
+
if (mine) {
|
|
1505
|
+
for (const h of mine.hooks) if (h.command.includes(HOOK_MARKER) && h.command !== cmd) {
|
|
1506
|
+
h.command = cmd;
|
|
1507
|
+
changed = true;
|
|
1508
|
+
}
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
list.push({ ...matcher ? { matcher } : {}, hooks: [{ type: "command", command: cmd, timeout: HOOK_TIMEOUT_S }] });
|
|
1512
|
+
changed = true;
|
|
1513
|
+
}
|
|
1514
|
+
return { next: JSON.stringify(obj4, null, 2) + "\n", changed };
|
|
1515
|
+
}
|
|
1516
|
+
function unpatchSettings(existing) {
|
|
1517
|
+
const obj4 = parse(existing);
|
|
1518
|
+
let changed = false;
|
|
1519
|
+
const hooks = obj4.hooks;
|
|
1520
|
+
if (hooks) {
|
|
1521
|
+
for (const event of Object.keys(hooks)) {
|
|
1522
|
+
const list = hooks[event];
|
|
1523
|
+
if (!Array.isArray(list)) continue;
|
|
1524
|
+
const kept = list.filter((g) => !isOurs(g));
|
|
1525
|
+
if (kept.length !== list.length) changed = true;
|
|
1526
|
+
if (kept.length) hooks[event] = kept;
|
|
1527
|
+
else delete hooks[event];
|
|
1528
|
+
}
|
|
1529
|
+
if (Object.keys(hooks).length === 0) delete obj4.hooks;
|
|
1530
|
+
}
|
|
1531
|
+
return { next: JSON.stringify(obj4, null, 2) + "\n", changed };
|
|
1532
|
+
}
|
|
1533
|
+
function hasOurHooks(existing) {
|
|
1534
|
+
try {
|
|
1535
|
+
const obj4 = parse(existing);
|
|
1536
|
+
return Object.values(obj4.hooks ?? {}).some((l) => Array.isArray(l) && l.some(isOurs));
|
|
1537
|
+
} catch {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/hooks/script.ts
|
|
1543
|
+
var HOOK_SCRIPT = `#!/usr/bin/env node
|
|
1544
|
+
// Agent Theatre hook \u2014 forwards Claude Code events to the local theatre. Never blocks the agent.
|
|
1545
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
1546
|
+
import { homedir } from "node:os";
|
|
1547
|
+
import { join } from "node:path";
|
|
1548
|
+
// Every running stage registers itself in ports/<port>; the event goes to all of them (browser, VS Code, a second repo\u2026).
|
|
1549
|
+
const home = process.env.AGENT_THEATRE_HOME || join(homedir(), ".agent-theatre");
|
|
1550
|
+
const ports = new Set();
|
|
1551
|
+
try { for (const f of readdirSync(join(home, "ports"))) { const n = parseInt(f, 10); if (n > 0) ports.add(n); } } catch { /* no registry yet */ }
|
|
1552
|
+
try { const n = parseInt(readFileSync(join(home, "port"), "utf8").trim(), 10); if (n > 0) ports.add(n); } catch { /* no port file */ }
|
|
1553
|
+
if (!ports.size) process.exit(0);
|
|
1554
|
+
let body = "";
|
|
1555
|
+
process.stdin.setEncoding("utf8");
|
|
1556
|
+
process.stdin.on("data", (c) => { body += c; if (body.length > 2_000_000) body = body.slice(0, 2_000_000); });
|
|
1557
|
+
process.stdin.on("end", async () => {
|
|
1558
|
+
try {
|
|
1559
|
+
await Promise.allSettled([...ports].map((port) => fetch(\`http://127.0.0.1:\${port}/hook\`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(300) })));
|
|
1560
|
+
} catch { /* a theatre not running or slow: that is fine */ }
|
|
1561
|
+
process.exit(0);
|
|
1562
|
+
});
|
|
1563
|
+
setTimeout(() => process.exit(0), 600).unref();
|
|
1564
|
+
`;
|
|
1565
|
+
|
|
1566
|
+
// src/hooks/install.ts
|
|
1567
|
+
function installPaths(env = process.env) {
|
|
1568
|
+
const claude = env.CLAUDE_CONFIG_DIR || join4(homedir3(), ".claude");
|
|
1569
|
+
const home = env.AGENT_THEATRE_HOME || join4(homedir3(), ".agent-theatre");
|
|
1570
|
+
const settings = join4(claude, "settings.json");
|
|
1571
|
+
return { settings, home, script: join4(home, "hooks", "agent-theatre-hook.mjs"), backup: `${settings}.bak-agent-theatre` };
|
|
1572
|
+
}
|
|
1573
|
+
var read = (p) => existsSync2(p) ? readFileSync4(p, "utf8") : null;
|
|
1574
|
+
function installHooks(paths = installPaths()) {
|
|
1575
|
+
mkdirSync2(join4(paths.home, "hooks"), { recursive: true });
|
|
1576
|
+
writeFileSync2(paths.script, HOOK_SCRIPT, { mode: 493 });
|
|
1577
|
+
chmodSync(paths.script, 493);
|
|
1578
|
+
const existing = read(paths.settings);
|
|
1579
|
+
const { next, changed } = patchSettings(existing, paths.script);
|
|
1580
|
+
let backup = null;
|
|
1581
|
+
if (changed) {
|
|
1582
|
+
if (existing !== null) {
|
|
1583
|
+
copyFileSync(paths.settings, paths.backup);
|
|
1584
|
+
backup = paths.backup;
|
|
1585
|
+
}
|
|
1586
|
+
mkdirSync2(join4(paths.settings, ".."), { recursive: true });
|
|
1587
|
+
writeFileSync2(paths.settings, next);
|
|
1588
|
+
}
|
|
1589
|
+
return { changed, backup };
|
|
1590
|
+
}
|
|
1591
|
+
function uninstallHooks(paths = installPaths()) {
|
|
1592
|
+
const existing = read(paths.settings);
|
|
1593
|
+
if (existing === null) return { changed: false };
|
|
1594
|
+
const { next, changed } = unpatchSettings(existing);
|
|
1595
|
+
if (changed) {
|
|
1596
|
+
copyFileSync(paths.settings, paths.backup);
|
|
1597
|
+
writeFileSync2(paths.settings, next);
|
|
1598
|
+
}
|
|
1599
|
+
return { changed };
|
|
1600
|
+
}
|
|
1601
|
+
function hooksStatus(paths = installPaths()) {
|
|
1602
|
+
return { installed: hasOurHooks(read(paths.settings)), scriptPresent: existsSync2(paths.script), settings: paths.settings };
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
export {
|
|
1606
|
+
listSessions,
|
|
1607
|
+
Theatre,
|
|
1608
|
+
scanRepo,
|
|
1609
|
+
repoIdFor,
|
|
1610
|
+
installHooks,
|
|
1611
|
+
uninstallHooks,
|
|
1612
|
+
hooksStatus
|
|
1613
|
+
};
|
|
1614
|
+
//# sourceMappingURL=chunk-WAD5QXS2.js.map
|