agent-theatre 0.0.0-stage → 0.1.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/dist/cli.js +480 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/hooks/agent-theatre-hook.mjs +19 -0
- package/package.json +57 -3
- package/web/assets/browserAll-CHKXHvgf.js +2 -0
- package/web/assets/browserAll-CHKXHvgf.js.map +1 -0
- package/web/assets/index-CiLhC8BO.js +956 -0
- package/web/assets/index-CiLhC8BO.js.map +1 -0
- package/web/assets/index-OtOd0IgG.css +1 -0
- package/web/assets/webworkerAll-kXIcM_xs.js +2 -0
- package/web/assets/webworkerAll-kXIcM_xs.js.map +1 -0
- package/web/demo/bash-heavy.jsonl +12 -0
- package/web/demo/demos.json +44 -0
- package/web/demo/deploy.files.json +65 -0
- package/web/demo/deploy.jsonl +18 -0
- package/web/demo/episode-1.files.json +8 -0
- package/web/demo/episode-1.jsonl +18 -0
- package/web/demo/feature.files.json +65 -0
- package/web/demo/feature.jsonl +16 -0
- package/web/demo/refactor.files.json +73 -0
- package/web/demo/refactor.jsonl +22 -0
- package/web/demo/research.files.json +69 -0
- package/web/demo/research.jsonl +22 -0
- package/web/demo/subagents.jsonl +10 -0
- package/web/favicon.svg +1 -0
- package/web/fonts/Fredoka-600.woff2 +0 -0
- package/web/fonts/Fredoka-700.woff2 +0 -0
- package/web/fonts/JetBrainsMono-500.woff2 +0 -0
- package/web/fonts/JetBrainsMono-600.woff2 +0 -0
- package/web/fonts/LICENSE.txt +4 -0
- package/web/fonts/NunitoSans-400.woff2 +0 -0
- package/web/fonts/NunitoSans-600.woff2 +0 -0
- package/web/fonts/NunitoSans-700.woff2 +0 -0
- package/web/index.html +15 -0
- package/README.md +0 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname, join as join5 } from "path";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/server.ts
|
|
11
|
+
import { createServer } from "http";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
13
|
+
import { homedir as homedir2 } from "os";
|
|
14
|
+
import { join as join2, basename } from "path";
|
|
15
|
+
import { Hono } from "hono";
|
|
16
|
+
import { getRequestListener } from "@hono/node-server";
|
|
17
|
+
import { WebSocketServer } from "ws";
|
|
18
|
+
import { Director, direct, filesFromEvents, generateWorld, hookPayloadToEvents, parseTranscript } from "@agent-theatre/core";
|
|
19
|
+
|
|
20
|
+
// src/sessions.ts
|
|
21
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
function projectSlug(cwd) {
|
|
25
|
+
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
26
|
+
}
|
|
27
|
+
function claudeConfigDir(env = process.env) {
|
|
28
|
+
return env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
29
|
+
}
|
|
30
|
+
function listSessions(cwd, env = process.env) {
|
|
31
|
+
const dir = join(claudeConfigDir(env), "projects", projectSlug(cwd));
|
|
32
|
+
let names = [];
|
|
33
|
+
try {
|
|
34
|
+
names = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const f of names) {
|
|
40
|
+
const path = join(dir, f);
|
|
41
|
+
try {
|
|
42
|
+
const st = statSync(path);
|
|
43
|
+
out.push({ id: f.replace(/\.jsonl$/, ""), path, mtime: st.mtimeMs, bytes: st.size, title: sessionTitle(path) });
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
48
|
+
}
|
|
49
|
+
function sessionTitle(path) {
|
|
50
|
+
let text = "";
|
|
51
|
+
try {
|
|
52
|
+
const st = statSync(path);
|
|
53
|
+
const fd = readFileSync(path);
|
|
54
|
+
text = fd.subarray(Math.max(0, st.size - 262144)).toString("utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
let ai = "", prompt = "";
|
|
59
|
+
for (const line of text.split("\n")) {
|
|
60
|
+
if (!line.includes('"aiTitle"') && !line.includes('"lastPrompt"')) continue;
|
|
61
|
+
try {
|
|
62
|
+
const row = JSON.parse(line);
|
|
63
|
+
if (typeof row.aiTitle === "string" && row.aiTitle.trim()) ai = row.aiTitle.trim();
|
|
64
|
+
if (typeof row.lastPrompt === "string" && row.lastPrompt.trim()) prompt = row.lastPrompt.trim();
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const t = ai || prompt;
|
|
69
|
+
return t.length > 80 ? t.slice(0, 77) + "\u2026" : t;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/server.ts
|
|
73
|
+
var Theatre = class {
|
|
74
|
+
constructor(opts) {
|
|
75
|
+
this.opts = opts;
|
|
76
|
+
const world = generateWorld({ repoId: opts.repoId, files: opts.files });
|
|
77
|
+
this.live = { world, director: new Director(world), events: [], beats: [], lastEventAt: 0, sessionOpen: false };
|
|
78
|
+
this.routes();
|
|
79
|
+
}
|
|
80
|
+
opts;
|
|
81
|
+
app = new Hono();
|
|
82
|
+
server = null;
|
|
83
|
+
wss = null;
|
|
84
|
+
live;
|
|
85
|
+
signal = "ok";
|
|
86
|
+
timer = null;
|
|
87
|
+
port = 0;
|
|
88
|
+
routes() {
|
|
89
|
+
const a = this.app;
|
|
90
|
+
a.post("/hook", async (c) => {
|
|
91
|
+
let payload;
|
|
92
|
+
try {
|
|
93
|
+
payload = await c.req.json();
|
|
94
|
+
} catch {
|
|
95
|
+
return c.body(null, 204);
|
|
96
|
+
}
|
|
97
|
+
const { events } = hookPayloadToEvents(payload, { home: homedir2() });
|
|
98
|
+
if (events.length) this.ingest(events);
|
|
99
|
+
return c.body(null, 204);
|
|
100
|
+
});
|
|
101
|
+
a.get("/api/scene", (c) => c.json({ mode: "live", repoId: this.opts.repoId, world: this.live.world, events: this.live.events, beats: this.live.beats, signal: this.signal }));
|
|
102
|
+
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 }))));
|
|
103
|
+
a.get("/api/replay/:id", (c) => {
|
|
104
|
+
const id = c.req.param("id");
|
|
105
|
+
const s = listSessions(this.opts.cwd).find((x) => x.id === id);
|
|
106
|
+
if (!s) return c.json({ error: "no such session" }, 404);
|
|
107
|
+
const { events, warnings } = parseTranscript(readFileSync2(s.path, "utf8"), { home: homedir2() });
|
|
108
|
+
const inferred = filesFromEvents(events);
|
|
109
|
+
const known = new Set(this.opts.files.map((f) => f.path));
|
|
110
|
+
const files = [...this.opts.files, ...inferred.files.filter((f) => !known.has(f.path))];
|
|
111
|
+
const world = generateWorld({ repoId: this.opts.repoId, files, touched: inferred.touched });
|
|
112
|
+
const beats = direct(events, world);
|
|
113
|
+
return c.json({ mode: "replay", repoId: this.opts.repoId, title: s.title, world, events, beats, warnings: warnings.length });
|
|
114
|
+
});
|
|
115
|
+
a.get("/healthz", (c) => c.json({ ok: true, port: this.port, events: this.live.events.length }));
|
|
116
|
+
if (this.opts.webDir) {
|
|
117
|
+
const dir = this.opts.webDir;
|
|
118
|
+
a.get("/*", (c) => {
|
|
119
|
+
const p = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
120
|
+
const file = join2(dir, p);
|
|
121
|
+
if (!file.startsWith(dir) || !existsSync(file)) return c.text("not found", 404);
|
|
122
|
+
const ext = p.slice(p.lastIndexOf(".") + 1);
|
|
123
|
+
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" };
|
|
124
|
+
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" });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Append events to the live session, direct them, and broadcast. */
|
|
129
|
+
ingest(events) {
|
|
130
|
+
const l = this.live;
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const e of events) {
|
|
133
|
+
if (e.kind === "session_start") l.sessionOpen = true;
|
|
134
|
+
if (e.kind === "session_end") l.sessionOpen = false;
|
|
135
|
+
l.events.push(e);
|
|
136
|
+
out.push(...l.director.feed(e));
|
|
137
|
+
}
|
|
138
|
+
l.beats.push(...out);
|
|
139
|
+
l.lastEventAt = Date.now();
|
|
140
|
+
if (this.signal !== "ok") {
|
|
141
|
+
this.signal = "ok";
|
|
142
|
+
this.broadcast({ type: "signal", state: "ok" });
|
|
143
|
+
}
|
|
144
|
+
this.broadcast({ type: "beats", beats: out, events });
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
broadcast(msg) {
|
|
148
|
+
const data = JSON.stringify(msg);
|
|
149
|
+
for (const ws of this.wss?.clients ?? []) if (ws.readyState === 1) ws.send(data);
|
|
150
|
+
}
|
|
151
|
+
async listen(port = 0) {
|
|
152
|
+
const listener = getRequestListener(this.app.fetch);
|
|
153
|
+
this.server = createServer(listener);
|
|
154
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
155
|
+
this.server.on("upgrade", (req, socket, head) => {
|
|
156
|
+
if (req.url?.startsWith("/live")) this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
157
|
+
this.wss.emit("connection", ws, req);
|
|
158
|
+
ws.send(JSON.stringify({ type: "hello", signal: this.signal }));
|
|
159
|
+
});
|
|
160
|
+
else socket.destroy();
|
|
161
|
+
});
|
|
162
|
+
await new Promise((res) => this.server.listen(port, "127.0.0.1", res));
|
|
163
|
+
const addr = this.server.address();
|
|
164
|
+
this.port = typeof addr === "object" && addr ? addr.port : port;
|
|
165
|
+
const home = this.opts.home ?? join2(homedir2(), ".agent-theatre");
|
|
166
|
+
mkdirSync(home, { recursive: true });
|
|
167
|
+
writeFileSync(join2(home, "port"), String(this.port));
|
|
168
|
+
const lostAfter = this.opts.lostAfterMs ?? 9e4;
|
|
169
|
+
this.timer = setInterval(() => {
|
|
170
|
+
const l = this.live;
|
|
171
|
+
if (l.sessionOpen && l.lastEventAt && Date.now() - l.lastEventAt > lostAfter && this.signal === "ok") {
|
|
172
|
+
this.signal = "lost";
|
|
173
|
+
this.broadcast({ type: "signal", state: "lost" });
|
|
174
|
+
}
|
|
175
|
+
}, 5e3);
|
|
176
|
+
this.timer.unref();
|
|
177
|
+
return this.port;
|
|
178
|
+
}
|
|
179
|
+
async close() {
|
|
180
|
+
if (this.timer) clearInterval(this.timer);
|
|
181
|
+
const home = this.opts.home ?? join2(homedir2(), ".agent-theatre");
|
|
182
|
+
try {
|
|
183
|
+
if (readFileSync2(join2(home, "port"), "utf8").trim() === String(this.port)) rmSync(join2(home, "port"));
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
for (const ws of this.wss?.clients ?? []) ws.close();
|
|
187
|
+
this.wss?.close();
|
|
188
|
+
await new Promise((res) => this.server ? this.server.close(() => res()) : res());
|
|
189
|
+
}
|
|
190
|
+
get url() {
|
|
191
|
+
return `http://127.0.0.1:${this.port}/`;
|
|
192
|
+
}
|
|
193
|
+
get name() {
|
|
194
|
+
return basename(this.opts.cwd);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/scan.ts
|
|
199
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
200
|
+
import { join as join3, relative } from "path";
|
|
201
|
+
import { isIgnoredPath } from "@agent-theatre/core";
|
|
202
|
+
function scanRepo(root, opts = {}) {
|
|
203
|
+
const maxFiles = opts.maxFiles ?? 2e4, maxBytes = opts.maxBytesForLoc ?? 512e3;
|
|
204
|
+
const files = [];
|
|
205
|
+
const walk = (dir) => {
|
|
206
|
+
if (files.length >= maxFiles) return;
|
|
207
|
+
let ents;
|
|
208
|
+
try {
|
|
209
|
+
ents = readdirSync2(dir, { withFileTypes: true });
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const ent of ents) {
|
|
214
|
+
if (files.length >= maxFiles) return;
|
|
215
|
+
const full = join3(dir, ent.name);
|
|
216
|
+
const rel = relative(root, full).replace(/\\/g, "/");
|
|
217
|
+
if (isIgnoredPath(rel + (ent.isDirectory() ? "/" : ""))) continue;
|
|
218
|
+
if (ent.isDirectory()) walk(full);
|
|
219
|
+
else if (ent.isFile()) {
|
|
220
|
+
let size = 0;
|
|
221
|
+
try {
|
|
222
|
+
size = statSync2(full).size;
|
|
223
|
+
} catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
let loc;
|
|
227
|
+
if (size < maxBytes) {
|
|
228
|
+
try {
|
|
229
|
+
const buf = readFileSync3(full);
|
|
230
|
+
if (!buf.subarray(0, 1024).includes(0)) loc = buf.toString("utf8").split("\n").length;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
files.push({ path: rel, size, ...loc !== void 0 ? { loc } : {} });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
walk(root);
|
|
239
|
+
return files;
|
|
240
|
+
}
|
|
241
|
+
function repoIdFor(root) {
|
|
242
|
+
try {
|
|
243
|
+
const cfg = readFileSync3(join3(root, ".git", "config"), "utf8");
|
|
244
|
+
const m = /\[remote "origin"\]\s*\n\s*url\s*=\s*(\S+)/.exec(cfg);
|
|
245
|
+
if (m) return m[1].replace(/\.git$/, "").replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "");
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
return root;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/hooks/install.ts
|
|
252
|
+
import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
253
|
+
import { homedir as homedir3 } from "os";
|
|
254
|
+
import { join as join4 } from "path";
|
|
255
|
+
|
|
256
|
+
// src/hooks/patch.ts
|
|
257
|
+
var HOOK_MARKER = "agent-theatre-hook.mjs";
|
|
258
|
+
var HOOK_EVENTS = [
|
|
259
|
+
{ event: "SessionStart" },
|
|
260
|
+
{ event: "PostToolUse" },
|
|
261
|
+
{ event: "PostToolUseFailure" },
|
|
262
|
+
{ event: "SubagentStart" },
|
|
263
|
+
{ event: "SubagentStop" },
|
|
264
|
+
{ event: "Notification" },
|
|
265
|
+
{ event: "Stop" },
|
|
266
|
+
{ event: "SessionEnd" }
|
|
267
|
+
];
|
|
268
|
+
var HOOK_TIMEOUT_S = 5;
|
|
269
|
+
function parse(existing) {
|
|
270
|
+
if (existing === null || existing.trim() === "") return {};
|
|
271
|
+
try {
|
|
272
|
+
const obj = JSON.parse(existing);
|
|
273
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("not an object");
|
|
274
|
+
return obj;
|
|
275
|
+
} catch {
|
|
276
|
+
throw new Error("existing settings.json is not valid JSON; refusing to overwrite it");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
var isOurs = (group) => {
|
|
280
|
+
const g = group;
|
|
281
|
+
return Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER));
|
|
282
|
+
};
|
|
283
|
+
function patchSettings(existing, scriptPath) {
|
|
284
|
+
const obj = parse(existing);
|
|
285
|
+
const hooks2 = obj.hooks ??= {};
|
|
286
|
+
const cmd = `node "${scriptPath}"`;
|
|
287
|
+
let changed = false;
|
|
288
|
+
for (const { event, matcher } of HOOK_EVENTS) {
|
|
289
|
+
const list = hooks2[event] ??= [];
|
|
290
|
+
const mine = list.find(isOurs);
|
|
291
|
+
if (mine) {
|
|
292
|
+
for (const h of mine.hooks) if (h.command.includes(HOOK_MARKER) && h.command !== cmd) {
|
|
293
|
+
h.command = cmd;
|
|
294
|
+
changed = true;
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
list.push({ ...matcher ? { matcher } : {}, hooks: [{ type: "command", command: cmd, timeout: HOOK_TIMEOUT_S }] });
|
|
299
|
+
changed = true;
|
|
300
|
+
}
|
|
301
|
+
return { next: JSON.stringify(obj, null, 2) + "\n", changed };
|
|
302
|
+
}
|
|
303
|
+
function unpatchSettings(existing) {
|
|
304
|
+
const obj = parse(existing);
|
|
305
|
+
let changed = false;
|
|
306
|
+
const hooks2 = obj.hooks;
|
|
307
|
+
if (hooks2) {
|
|
308
|
+
for (const event of Object.keys(hooks2)) {
|
|
309
|
+
const list = hooks2[event];
|
|
310
|
+
if (!Array.isArray(list)) continue;
|
|
311
|
+
const kept = list.filter((g) => !isOurs(g));
|
|
312
|
+
if (kept.length !== list.length) changed = true;
|
|
313
|
+
if (kept.length) hooks2[event] = kept;
|
|
314
|
+
else delete hooks2[event];
|
|
315
|
+
}
|
|
316
|
+
if (Object.keys(hooks2).length === 0) delete obj.hooks;
|
|
317
|
+
}
|
|
318
|
+
return { next: JSON.stringify(obj, null, 2) + "\n", changed };
|
|
319
|
+
}
|
|
320
|
+
function hasOurHooks(existing) {
|
|
321
|
+
try {
|
|
322
|
+
const obj = parse(existing);
|
|
323
|
+
return Object.values(obj.hooks ?? {}).some((l) => Array.isArray(l) && l.some(isOurs));
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/hooks/script.ts
|
|
330
|
+
var HOOK_SCRIPT = `#!/usr/bin/env node
|
|
331
|
+
// Agent Theatre hook \u2014 forwards Claude Code events to the local theatre. Never blocks the agent.
|
|
332
|
+
import { readFileSync } from "node:fs";
|
|
333
|
+
import { homedir } from "node:os";
|
|
334
|
+
import { join } from "node:path";
|
|
335
|
+
const portFile = join(process.env.AGENT_THEATRE_HOME || join(homedir(), ".agent-theatre"), "port");
|
|
336
|
+
let port = 0;
|
|
337
|
+
try { port = parseInt(readFileSync(portFile, "utf8").trim(), 10); } catch { process.exit(0); }
|
|
338
|
+
if (!port) process.exit(0);
|
|
339
|
+
let body = "";
|
|
340
|
+
process.stdin.setEncoding("utf8");
|
|
341
|
+
process.stdin.on("data", (c) => { body += c; if (body.length > 2_000_000) body = body.slice(0, 2_000_000); });
|
|
342
|
+
process.stdin.on("end", async () => {
|
|
343
|
+
try {
|
|
344
|
+
await fetch(\`http://127.0.0.1:\${port}/hook\`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(300) });
|
|
345
|
+
} catch { /* theatre not running or slow: that is fine */ }
|
|
346
|
+
process.exit(0);
|
|
347
|
+
});
|
|
348
|
+
setTimeout(() => process.exit(0), 600).unref();
|
|
349
|
+
`;
|
|
350
|
+
|
|
351
|
+
// src/hooks/install.ts
|
|
352
|
+
function installPaths(env = process.env) {
|
|
353
|
+
const claude = env.CLAUDE_CONFIG_DIR || join4(homedir3(), ".claude");
|
|
354
|
+
const home = env.AGENT_THEATRE_HOME || join4(homedir3(), ".agent-theatre");
|
|
355
|
+
const settings = join4(claude, "settings.json");
|
|
356
|
+
return { settings, home, script: join4(home, "hooks", "agent-theatre-hook.mjs"), backup: `${settings}.bak-agent-theatre` };
|
|
357
|
+
}
|
|
358
|
+
var read = (p) => existsSync2(p) ? readFileSync4(p, "utf8") : null;
|
|
359
|
+
function installHooks(paths = installPaths()) {
|
|
360
|
+
mkdirSync2(join4(paths.home, "hooks"), { recursive: true });
|
|
361
|
+
writeFileSync2(paths.script, HOOK_SCRIPT, { mode: 493 });
|
|
362
|
+
chmodSync(paths.script, 493);
|
|
363
|
+
const existing = read(paths.settings);
|
|
364
|
+
const { next, changed } = patchSettings(existing, paths.script);
|
|
365
|
+
let backup = null;
|
|
366
|
+
if (changed) {
|
|
367
|
+
if (existing !== null) {
|
|
368
|
+
copyFileSync(paths.settings, paths.backup);
|
|
369
|
+
backup = paths.backup;
|
|
370
|
+
}
|
|
371
|
+
mkdirSync2(join4(paths.settings, ".."), { recursive: true });
|
|
372
|
+
writeFileSync2(paths.settings, next);
|
|
373
|
+
}
|
|
374
|
+
return { changed, backup };
|
|
375
|
+
}
|
|
376
|
+
function uninstallHooks(paths = installPaths()) {
|
|
377
|
+
const existing = read(paths.settings);
|
|
378
|
+
if (existing === null) return { changed: false };
|
|
379
|
+
const { next, changed } = unpatchSettings(existing);
|
|
380
|
+
if (changed) {
|
|
381
|
+
copyFileSync(paths.settings, paths.backup);
|
|
382
|
+
writeFileSync2(paths.settings, next);
|
|
383
|
+
}
|
|
384
|
+
return { changed };
|
|
385
|
+
}
|
|
386
|
+
function hooksStatus(paths = installPaths()) {
|
|
387
|
+
return { installed: hasOurHooks(read(paths.settings)), scriptPresent: existsSync2(paths.script), settings: paths.settings };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/cli.ts
|
|
391
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
392
|
+
var pkg = JSON.parse(readFileSync5(join5(here, "..", "package.json"), "utf8"));
|
|
393
|
+
var webDir = [join5(here, "..", "web"), join5(here, "..", "..", "web", "dist")].find((d) => existsSync3(join5(d, "index.html")));
|
|
394
|
+
async function openBrowser(url) {
|
|
395
|
+
try {
|
|
396
|
+
const { default: open } = await import("open");
|
|
397
|
+
await open(url);
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
var program = new Command().name("agent-theatre").description("Watch your coding agent work \u2014 as a film, not a log.").version(pkg.version);
|
|
402
|
+
program.option("-p, --port <n>", "port to listen on (default: a free one)", "0").option("--no-open", "don't open the browser").option("--set <id>", "stage set: island | orbit | metropolis").action(async (o) => {
|
|
403
|
+
const cwd = process.cwd();
|
|
404
|
+
const st = hooksStatus();
|
|
405
|
+
if (!st.installed) console.log(pc.yellow("Hooks are not installed \u2014 the scene will stay empty until you run:"), pc.bold("agent-theatre hooks install"));
|
|
406
|
+
const files = scanRepo(cwd);
|
|
407
|
+
const theatre = new Theatre({ cwd, repoId: repoIdFor(cwd), files, ...webDir ? { webDir } : {} });
|
|
408
|
+
await theatre.listen(Number(o.port) || 0);
|
|
409
|
+
const url = `${theatre.url}?live=1${o.set ? `&set=${o.set}` : ""}`;
|
|
410
|
+
console.log(`${pc.bold("Agent Theatre")} \xB7 ${pc.dim(theatre.name)} \xB7 ${files.length} files`);
|
|
411
|
+
console.log(`${pc.green("\u25B6")} ${url}`);
|
|
412
|
+
if (!webDir) console.log(pc.yellow("web bundle not found \u2014 API only. Run `pnpm -F @agent-theatre/web build` or use the Vite dev server with a proxy."));
|
|
413
|
+
if (o.open) await openBrowser(url);
|
|
414
|
+
const bye = async () => {
|
|
415
|
+
await theatre.close();
|
|
416
|
+
process.exit(0);
|
|
417
|
+
};
|
|
418
|
+
process.on("SIGINT", bye);
|
|
419
|
+
process.on("SIGTERM", bye);
|
|
420
|
+
});
|
|
421
|
+
program.command("replay [session]").description("replay a past Claude Code session for this repo (default: pick from a list)").option("--latest", "replay the newest session without asking").option("--no-open", "don't open the browser").option("--set <id>", "stage set").action(async (session, o) => {
|
|
422
|
+
const cwd = process.cwd();
|
|
423
|
+
const sessions = listSessions(cwd);
|
|
424
|
+
if (!sessions.length) {
|
|
425
|
+
console.log(pc.yellow(`No Claude Code sessions found for ${cwd}.`));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
let id = session;
|
|
429
|
+
if (!id && (o.latest || !process.stdin.isTTY)) id = sessions[0].id;
|
|
430
|
+
if (!id) {
|
|
431
|
+
console.log(pc.bold("Sessions for this repo, newest first:\n"));
|
|
432
|
+
sessions.slice(0, 15).forEach((s, i) => console.log(` ${pc.dim(String(i + 1).padStart(2))} ${new Date(s.mtime).toLocaleString()} ${pc.dim((s.bytes / 1024).toFixed(0).padStart(6) + " KB")} ${s.title || pc.dim(s.id)}`));
|
|
433
|
+
const { createInterface } = await import("readline/promises");
|
|
434
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
435
|
+
const ans = (await rl.question(`
|
|
436
|
+
Which one? [1] `)).trim() || "1";
|
|
437
|
+
rl.close();
|
|
438
|
+
const pick = sessions[Math.max(0, Math.min(sessions.length - 1, (parseInt(ans, 10) || 1) - 1))];
|
|
439
|
+
id = pick.id;
|
|
440
|
+
}
|
|
441
|
+
const found = sessions.find((s) => s.id === id || s.id.startsWith(id));
|
|
442
|
+
if (!found) {
|
|
443
|
+
console.log(pc.red(`No session ${id}`));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
const files = scanRepo(cwd);
|
|
447
|
+
const theatre = new Theatre({ cwd, repoId: repoIdFor(cwd), files, ...webDir ? { webDir } : {} });
|
|
448
|
+
await theatre.listen(0);
|
|
449
|
+
const url = `${theatre.url}?session=${found.id}${o.set ? `&set=${o.set}` : ""}`;
|
|
450
|
+
console.log(`${pc.bold("Agent Theatre")} \xB7 replay \xB7 ${found.title || found.id}`);
|
|
451
|
+
console.log(`${pc.green("\u25B6")} ${url}`);
|
|
452
|
+
if (o.open) await openBrowser(url);
|
|
453
|
+
const bye = async () => {
|
|
454
|
+
await theatre.close();
|
|
455
|
+
process.exit(0);
|
|
456
|
+
};
|
|
457
|
+
process.on("SIGINT", bye);
|
|
458
|
+
process.on("SIGTERM", bye);
|
|
459
|
+
});
|
|
460
|
+
var hooks = program.command("hooks").description("install, remove or inspect the Claude Code hooks");
|
|
461
|
+
hooks.command("install").description("merge our hooks into ~/.claude/settings.json (backup kept, nothing else touched)").action(() => {
|
|
462
|
+
const r = installHooks();
|
|
463
|
+
console.log(r.changed ? pc.green("Hooks installed.") : pc.dim("Hooks were already installed."), r.backup ? pc.dim(`backup: ${r.backup}`) : "");
|
|
464
|
+
console.log(pc.dim("Claude Code picks up settings changes automatically; no restart needed."));
|
|
465
|
+
});
|
|
466
|
+
hooks.command("uninstall").description("remove only our hook entries").action(() => {
|
|
467
|
+
const r = uninstallHooks();
|
|
468
|
+
console.log(r.changed ? pc.green("Hooks removed.") : pc.dim("Nothing to remove."));
|
|
469
|
+
});
|
|
470
|
+
hooks.command("status").action(() => {
|
|
471
|
+
const s = hooksStatus();
|
|
472
|
+
console.log(`settings: ${s.settings}
|
|
473
|
+
installed: ${s.installed ? pc.green("yes") : pc.yellow("no")}
|
|
474
|
+
script present: ${s.scriptPresent ? "yes" : "no"}`);
|
|
475
|
+
});
|
|
476
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
477
|
+
console.error(pc.red(String(err?.message ?? err)));
|
|
478
|
+
process.exit(1);
|
|
479
|
+
});
|
|
480
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/sessions.ts","../src/scan.ts","../src/hooks/install.ts","../src/hooks/patch.ts","../src/hooks/script.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport pc from \"picocolors\";\nimport { Theatre } from \"./server.js\";\nimport { repoIdFor, scanRepo } from \"./scan.js\";\nimport { listSessions } from \"./sessions.js\";\nimport { hooksStatus, installHooks, uninstallHooks } from \"./hooks/install.js\";\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\")) as { version: string };\nconst webDir = [join(here, \"..\", \"web\"), join(here, \"..\", \"..\", \"web\", \"dist\")].find((d) => existsSync(join(d, \"index.html\")));\n\nasync function openBrowser(url: string): Promise<void> {\n try { const { default: open } = await import(\"open\"); await open(url); } catch { /* print the URL instead */ }\n}\n\nconst program = new Command().name(\"agent-theatre\").description(\"Watch your coding agent work — as a film, not a log.\").version(pkg.version);\n\nprogram\n .option(\"-p, --port <n>\", \"port to listen on (default: a free one)\", \"0\")\n .option(\"--no-open\", \"don't open the browser\")\n .option(\"--set <id>\", \"stage set: island | orbit | metropolis\")\n .action(async (o: { port: string; open: boolean; set?: string }) => {\n const cwd = process.cwd();\n const st = hooksStatus();\n if (!st.installed) console.log(pc.yellow(\"Hooks are not installed — the scene will stay empty until you run:\"), pc.bold(\"agent-theatre hooks install\"));\n const files = scanRepo(cwd);\n const theatre = new Theatre({ cwd, repoId: repoIdFor(cwd), files, ...(webDir ? { webDir } : {}) });\n await theatre.listen(Number(o.port) || 0);\n const url = `${theatre.url}?live=1${o.set ? `&set=${o.set}` : \"\"}`;\n console.log(`${pc.bold(\"Agent Theatre\")} · ${pc.dim(theatre.name)} · ${files.length} files`);\n console.log(`${pc.green(\"▶\")} ${url}`);\n if (!webDir) console.log(pc.yellow(\"web bundle not found — API only. Run `pnpm -F @agent-theatre/web build` or use the Vite dev server with a proxy.\"));\n if (o.open) await openBrowser(url);\n const bye = async () => { await theatre.close(); process.exit(0); };\n process.on(\"SIGINT\", bye); process.on(\"SIGTERM\", bye);\n });\n\nprogram\n .command(\"replay [session]\")\n .description(\"replay a past Claude Code session for this repo (default: pick from a list)\")\n .option(\"--latest\", \"replay the newest session without asking\")\n .option(\"--no-open\", \"don't open the browser\")\n .option(\"--set <id>\", \"stage set\")\n .action(async (session: string | undefined, o: { latest?: boolean; open: boolean; set?: string }) => {\n const cwd = process.cwd();\n const sessions = listSessions(cwd);\n if (!sessions.length) { console.log(pc.yellow(`No Claude Code sessions found for ${cwd}.`)); process.exit(1); }\n let id = session;\n if (!id && (o.latest || !process.stdin.isTTY)) id = sessions[0]!.id;\n if (!id) {\n console.log(pc.bold(\"Sessions for this repo, newest first:\\n\"));\n sessions.slice(0, 15).forEach((s, i) => console.log(` ${pc.dim(String(i + 1).padStart(2))} ${new Date(s.mtime).toLocaleString()} ${pc.dim((s.bytes / 1024).toFixed(0).padStart(6) + \" KB\")} ${s.title || pc.dim(s.id)}`));\n const { createInterface } = await import(\"node:readline/promises\");\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const ans = (await rl.question(`\\nWhich one? [1] `)).trim() || \"1\";\n rl.close();\n const pick = sessions[Math.max(0, Math.min(sessions.length - 1, (parseInt(ans, 10) || 1) - 1))]!;\n id = pick.id;\n }\n const found = sessions.find((s) => s.id === id || s.id.startsWith(id!));\n if (!found) { console.log(pc.red(`No session ${id}`)); process.exit(1); }\n const files = scanRepo(cwd);\n const theatre = new Theatre({ cwd, repoId: repoIdFor(cwd), files, ...(webDir ? { webDir } : {}) });\n await theatre.listen(0);\n const url = `${theatre.url}?session=${found.id}${o.set ? `&set=${o.set}` : \"\"}`;\n console.log(`${pc.bold(\"Agent Theatre\")} · replay · ${found.title || found.id}`);\n console.log(`${pc.green(\"▶\")} ${url}`);\n if (o.open) await openBrowser(url);\n const bye = async () => { await theatre.close(); process.exit(0); };\n process.on(\"SIGINT\", bye); process.on(\"SIGTERM\", bye);\n });\n\nconst hooks = program.command(\"hooks\").description(\"install, remove or inspect the Claude Code hooks\");\nhooks.command(\"install\").description(\"merge our hooks into ~/.claude/settings.json (backup kept, nothing else touched)\").action(() => {\n const r = installHooks();\n console.log(r.changed ? pc.green(\"Hooks installed.\") : pc.dim(\"Hooks were already installed.\"), r.backup ? pc.dim(`backup: ${r.backup}`) : \"\");\n console.log(pc.dim(\"Claude Code picks up settings changes automatically; no restart needed.\"));\n});\nhooks.command(\"uninstall\").description(\"remove only our hook entries\").action(() => {\n const r = uninstallHooks();\n console.log(r.changed ? pc.green(\"Hooks removed.\") : pc.dim(\"Nothing to remove.\"));\n});\nhooks.command(\"status\").action(() => {\n const s = hooksStatus();\n console.log(`settings: ${s.settings}\\ninstalled: ${s.installed ? pc.green(\"yes\") : pc.yellow(\"no\")}\\nscript present: ${s.scriptPresent ? \"yes\" : \"no\"}`);\n});\n\nprogram.parseAsync(process.argv).catch((err) => { console.error(pc.red(String(err?.message ?? err))); process.exit(1); });\n","import { createServer, type Server } from \"node:http\";\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, basename } from \"node:path\";\nimport { Hono } from \"hono\";\nimport { getRequestListener } from \"@hono/node-server\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport { Director, direct, filesFromEvents, generateWorld, hookPayloadToEvents, parseTranscript, type AgentEvent, type Beat, type FileNode, type World } from \"@agent-theatre/core\";\nimport { listSessions } from \"./sessions.js\";\n\nexport interface TheatreOptions {\n cwd: string;\n repoId: string;\n files: FileNode[];\n /** Directory of the built web bundle (index.html + assets). Optional in tests. */\n webDir?: string;\n /** Where to write the port file. Default ~/.agent-theatre */\n home?: string;\n /** No hook events for this long while a session is open → \"lost\" signal. */\n lostAfterMs?: number;\n}\n\ninterface Live { world: World; director: Director; events: AgentEvent[]; beats: Beat[]; lastEventAt: number; sessionOpen: boolean }\n\n/**\n * The local theatre. Loopback only. Receives hook payloads, directs them into\n * beats, and streams events + beats to the browser. Serves the web bundle and\n * replay payloads. Never logs or forwards tool input/output: the hook payload\n * is reduced to AgentEvents at the door and discarded.\n */\nexport class Theatre {\n readonly app = new Hono();\n private server: Server | null = null;\n private wss: WebSocketServer | null = null;\n private live: Live;\n private signal: \"ok\" | \"lost\" = \"ok\";\n private timer: NodeJS.Timeout | null = null;\n port = 0;\n\n constructor(private readonly opts: TheatreOptions) {\n const world = generateWorld({ repoId: opts.repoId, files: opts.files });\n this.live = { world, director: new Director(world), events: [], beats: [], lastEventAt: 0, sessionOpen: false };\n this.routes();\n }\n\n private routes(): void {\n const a = this.app;\n a.post(\"/hook\", async (c) => {\n let payload: unknown;\n try { payload = await c.req.json(); } catch { return c.body(null, 204); }\n const { events } = hookPayloadToEvents(payload, { home: homedir() });\n if (events.length) this.ingest(events);\n return c.body(null, 204);\n });\n a.get(\"/api/scene\", (c) => c.json({ mode: \"live\", repoId: this.opts.repoId, world: this.live.world, events: this.live.events, beats: this.live.beats, signal: this.signal }));\n 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 }))));\n a.get(\"/api/replay/:id\", (c) => {\n const id = c.req.param(\"id\");\n const s = listSessions(this.opts.cwd).find((x) => x.id === id);\n if (!s) return c.json({ error: \"no such session\" }, 404);\n const { events, warnings } = parseTranscript(readFileSync(s.path, \"utf8\"), { home: homedir() });\n const inferred = filesFromEvents(events);\n const known = new Set(this.opts.files.map((f) => f.path));\n const files = [...this.opts.files, ...inferred.files.filter((f) => !known.has(f.path))];\n const world = generateWorld({ repoId: this.opts.repoId, files, touched: inferred.touched });\n const beats = direct(events, world);\n return c.json({ mode: \"replay\", repoId: this.opts.repoId, title: s.title, world, events, beats, warnings: warnings.length });\n });\n a.get(\"/healthz\", (c) => c.json({ ok: true, port: this.port, events: this.live.events.length }));\n if (this.opts.webDir) {\n const dir = this.opts.webDir;\n a.get(\"/*\", (c) => {\n const p = c.req.path === \"/\" ? \"/index.html\" : c.req.path;\n const file = join(dir, p);\n if (!file.startsWith(dir) || !existsSync(file)) return c.text(\"not found\", 404);\n const ext = p.slice(p.lastIndexOf(\".\") + 1);\n const type: Record<string, string> = { 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\" };\n return c.body(readFileSync(file), 200, { \"content-type\": type[ext] ?? \"application/octet-stream\", \"cache-control\": p.includes(\"/assets/\") ? \"public, max-age=31536000, immutable\" : \"no-cache\" });\n });\n }\n }\n\n /** Append events to the live session, direct them, and broadcast. */\n ingest(events: AgentEvent[]): Beat[] {\n const l = this.live;\n const out: Beat[] = [];\n for (const e of events) {\n if (e.kind === \"session_start\") l.sessionOpen = true;\n if (e.kind === \"session_end\") l.sessionOpen = false;\n l.events.push(e);\n out.push(...l.director.feed(e));\n }\n l.beats.push(...out);\n l.lastEventAt = Date.now();\n if (this.signal !== \"ok\") { this.signal = \"ok\"; this.broadcast({ type: \"signal\", state: \"ok\" }); }\n this.broadcast({ type: \"beats\", beats: out, events });\n return out;\n }\n\n private broadcast(msg: unknown): void {\n const data = JSON.stringify(msg);\n for (const ws of this.wss?.clients ?? []) if (ws.readyState === 1) ws.send(data);\n }\n\n async listen(port = 0): Promise<number> {\n const listener = getRequestListener(this.app.fetch);\n this.server = createServer(listener);\n this.wss = new WebSocketServer({ noServer: true });\n this.server.on(\"upgrade\", (req, socket, head) => {\n if (req.url?.startsWith(\"/live\")) this.wss!.handleUpgrade(req, socket, head, (ws: WebSocket) => { this.wss!.emit(\"connection\", ws, req); ws.send(JSON.stringify({ type: \"hello\", signal: this.signal })); });\n else socket.destroy();\n });\n await new Promise<void>((res) => this.server!.listen(port, \"127.0.0.1\", res));\n const addr = this.server.address();\n this.port = typeof addr === \"object\" && addr ? addr.port : port;\n const home = this.opts.home ?? join(homedir(), \".agent-theatre\");\n mkdirSync(home, { recursive: true });\n writeFileSync(join(home, \"port\"), String(this.port));\n const lostAfter = this.opts.lostAfterMs ?? 90_000;\n this.timer = setInterval(() => {\n const l = this.live;\n if (l.sessionOpen && l.lastEventAt && Date.now() - l.lastEventAt > lostAfter && this.signal === \"ok\") { this.signal = \"lost\"; this.broadcast({ type: \"signal\", state: \"lost\" }); }\n }, 5_000);\n this.timer.unref();\n return this.port;\n }\n\n async close(): Promise<void> {\n if (this.timer) clearInterval(this.timer);\n const home = this.opts.home ?? join(homedir(), \".agent-theatre\");\n try { if (readFileSync(join(home, \"port\"), \"utf8\").trim() === String(this.port)) rmSync(join(home, \"port\")); } catch { /* already gone */ }\n for (const ws of this.wss?.clients ?? []) ws.close();\n this.wss?.close();\n await new Promise<void>((res) => (this.server ? this.server.close(() => res()) : res()));\n }\n\n get url(): string { return `http://127.0.0.1:${this.port}/`; }\n get name(): string { return basename(this.opts.cwd); }\n}\n","import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** ~/.claude/projects/<slug>/<session>.jsonl — slug = cwd with every non-alphanumeric → '-'. */\nexport function projectSlug(cwd: string): string {\n return cwd.replace(/[^A-Za-z0-9]/g, \"-\");\n}\nexport function claudeConfigDir(env: NodeJS.ProcessEnv = process.env): string {\n return env.CLAUDE_CONFIG_DIR || join(homedir(), \".claude\");\n}\nexport interface SessionInfo { id: string; path: string; mtime: number; bytes: number; title: string }\n\n/** Sessions for a repo, newest first, with a human title from the transcript's own title rows (local picker only; never into events). */\nexport function listSessions(cwd: string, env: NodeJS.ProcessEnv = process.env): SessionInfo[] {\n const dir = join(claudeConfigDir(env), \"projects\", projectSlug(cwd));\n let names: string[] = [];\n try { names = readdirSync(dir).filter((f) => f.endsWith(\".jsonl\")); } catch { return []; }\n const out: SessionInfo[] = [];\n for (const f of names) {\n const path = join(dir, f);\n try {\n const st = statSync(path);\n out.push({ id: f.replace(/\\.jsonl$/, \"\"), path, mtime: st.mtimeMs, bytes: st.size, title: sessionTitle(path) });\n } catch { /* unreadable: skip */ }\n }\n return out.sort((a, b) => b.mtime - a.mtime);\n}\n\n/** Title = latest `aiTitle` row, else the latest `lastPrompt` row (trimmed), else the id. Reads at most the last 256 KB. */\nexport function sessionTitle(path: string): string {\n let text = \"\";\n try {\n const st = statSync(path);\n const fd = readFileSync(path);\n text = fd.subarray(Math.max(0, st.size - 262_144)).toString(\"utf8\");\n } catch { return \"\"; }\n let ai = \"\", prompt = \"\";\n for (const line of text.split(\"\\n\")) {\n if (!line.includes('\"aiTitle\"') && !line.includes('\"lastPrompt\"')) continue;\n try {\n const row = JSON.parse(line) as { aiTitle?: string; lastPrompt?: string };\n if (typeof row.aiTitle === \"string\" && row.aiTitle.trim()) ai = row.aiTitle.trim();\n if (typeof row.lastPrompt === \"string\" && row.lastPrompt.trim()) prompt = row.lastPrompt.trim();\n } catch { /* partial line */ }\n }\n const t = ai || prompt;\n return t.length > 80 ? t.slice(0, 77) + \"…\" : t;\n}\n","import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { isIgnoredPath, type FileNode } from \"@agent-theatre/core\";\n\n/** The repo's file tree as FileNodes. Counts lines only; never keeps contents. Bounded for huge repos. */\nexport function scanRepo(root: string, opts: { maxFiles?: number; maxBytesForLoc?: number } = {}): FileNode[] {\n const maxFiles = opts.maxFiles ?? 20_000, maxBytes = opts.maxBytesForLoc ?? 512_000;\n const files: FileNode[] = [];\n const walk = (dir: string): void => {\n if (files.length >= maxFiles) return;\n let ents: import(\"node:fs\").Dirent[];\n try { ents = readdirSync(dir, { withFileTypes: true }); } catch { return; }\n for (const ent of ents) {\n if (files.length >= maxFiles) return;\n const full = join(dir, ent.name);\n const rel = relative(root, full).replace(/\\\\/g, \"/\");\n if (isIgnoredPath(rel + (ent.isDirectory() ? \"/\" : \"\"))) continue;\n if (ent.isDirectory()) walk(full);\n else if (ent.isFile()) {\n let size = 0;\n try { size = statSync(full).size; } catch { continue; }\n let loc: number | undefined;\n if (size < maxBytes) { try { const buf = readFileSync(full); if (!buf.subarray(0, 1024).includes(0)) loc = buf.toString(\"utf8\").split(\"\\n\").length; } catch { /* unreadable */ } }\n files.push({ path: rel, size, ...(loc !== undefined ? { loc } : {}) });\n }\n }\n };\n walk(root);\n return files;\n}\n\n/** Stable repo identity: git remote URL when available, else the absolute path. */\nexport function repoIdFor(root: string): string {\n try {\n const cfg = readFileSync(join(root, \".git\", \"config\"), \"utf8\");\n const m = /\\[remote \"origin\"\\]\\s*\\n\\s*url\\s*=\\s*(\\S+)/.exec(cfg);\n if (m) return m[1]!.replace(/\\.git$/, \"\").replace(/^git@([^:]+):/, \"$1/\").replace(/^https?:\\/\\//, \"\");\n } catch { /* not a git repo */ }\n return root;\n}\n","import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { hasOurHooks, patchSettings, unpatchSettings } from \"./patch.js\";\nimport { HOOK_SCRIPT } from \"./script.js\";\n\nexport interface InstallPaths { settings: string; home: string; script: string; backup: string }\nexport function installPaths(env: NodeJS.ProcessEnv = process.env): InstallPaths {\n const claude = env.CLAUDE_CONFIG_DIR || join(homedir(), \".claude\");\n const home = env.AGENT_THEATRE_HOME || join(homedir(), \".agent-theatre\");\n const settings = join(claude, \"settings.json\");\n return { settings, home, script: join(home, \"hooks\", \"agent-theatre-hook.mjs\"), backup: `${settings}.bak-agent-theatre` };\n}\nconst read = (p: string): string | null => (existsSync(p) ? readFileSync(p, \"utf8\") : null);\n\n/** Writes the hook script, then merges our entries into settings.json (backup first). Idempotent. */\nexport function installHooks(paths = installPaths()): { changed: boolean; backup: string | null } {\n mkdirSync(join(paths.home, \"hooks\"), { recursive: true });\n writeFileSync(paths.script, HOOK_SCRIPT, { mode: 0o755 });\n chmodSync(paths.script, 0o755);\n const existing = read(paths.settings);\n const { next, changed } = patchSettings(existing, paths.script);\n let backup: string | null = null;\n if (changed) {\n if (existing !== null) { copyFileSync(paths.settings, paths.backup); backup = paths.backup; }\n mkdirSync(join(paths.settings, \"..\"), { recursive: true });\n writeFileSync(paths.settings, next);\n }\n return { changed, backup };\n}\n\n/** Removes only our entries. Leaves the backup file in place for the user. */\nexport function uninstallHooks(paths = installPaths()): { changed: boolean } {\n const existing = read(paths.settings);\n if (existing === null) return { changed: false };\n const { next, changed } = unpatchSettings(existing);\n if (changed) { copyFileSync(paths.settings, paths.backup); writeFileSync(paths.settings, next); }\n return { changed };\n}\n\nexport function hooksStatus(paths = installPaths()): { installed: boolean; scriptPresent: boolean; settings: string } {\n return { installed: hasOurHooks(read(paths.settings)), scriptPresent: existsSync(paths.script), settings: paths.settings };\n}\n","/**\n * Non-destructive edits to ~/.claude/settings.json. Pure text → text so it is\n * trivially testable; the caller handles files and backups.\n *\n * // suite: candidate for shared package (Debug Anything has the same shape)\n */\nexport const HOOK_MARKER = \"agent-theatre-hook.mjs\";\n/** Events we observe. No matcher on PostToolUse: every tool is a beat. */\nexport const HOOK_EVENTS: ReadonlyArray<{ event: string; matcher?: string }> = [\n { event: \"SessionStart\" },\n { event: \"PostToolUse\" },\n { event: \"PostToolUseFailure\" },\n { event: \"SubagentStart\" },\n { event: \"SubagentStop\" },\n { event: \"Notification\" },\n { event: \"Stop\" },\n { event: \"SessionEnd\" },\n];\nexport const HOOK_TIMEOUT_S = 5;\n\ntype Settings = Record<string, unknown> & { hooks?: Record<string, unknown[]> };\n\nfunction parse(existing: string | null): Settings {\n if (existing === null || existing.trim() === \"\") return {};\n try {\n const obj = JSON.parse(existing);\n if (!obj || typeof obj !== \"object\" || Array.isArray(obj)) throw new Error(\"not an object\");\n return obj as Settings;\n } catch {\n throw new Error(\"existing settings.json is not valid JSON; refusing to overwrite it\");\n }\n}\nconst isOurs = (group: unknown): boolean => {\n const g = group as { hooks?: Array<{ command?: unknown }> };\n return Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === \"string\" && h.command.includes(HOOK_MARKER));\n};\n\nexport function patchSettings(existing: string | null, scriptPath: string): { next: string; changed: boolean } {\n const obj = parse(existing);\n const hooks = (obj.hooks ??= {});\n const cmd = `node \"${scriptPath}\"`;\n let changed = false;\n for (const { event, matcher } of HOOK_EVENTS) {\n const list = (hooks[event] ??= []);\n const mine = list.find(isOurs) as { hooks: Array<{ command: string; timeout?: number }> } | undefined;\n if (mine) {\n // keep the path current if the install location moved\n for (const h of mine.hooks) if (h.command.includes(HOOK_MARKER) && h.command !== cmd) { h.command = cmd; changed = true; }\n continue;\n }\n list.push({ ...(matcher ? { matcher } : {}), hooks: [{ type: \"command\", command: cmd, timeout: HOOK_TIMEOUT_S }] });\n changed = true;\n }\n return { next: JSON.stringify(obj, null, 2) + \"\\n\", changed };\n}\n\nexport function unpatchSettings(existing: string | null): { next: string; changed: boolean } {\n const obj = parse(existing);\n let changed = false;\n const hooks = obj.hooks;\n if (hooks) {\n for (const event of Object.keys(hooks)) {\n const list = hooks[event];\n if (!Array.isArray(list)) continue;\n const kept = list.filter((g) => !isOurs(g));\n if (kept.length !== list.length) changed = true;\n if (kept.length) hooks[event] = kept;\n else delete hooks[event];\n }\n if (Object.keys(hooks).length === 0) delete obj.hooks;\n }\n return { next: JSON.stringify(obj, null, 2) + \"\\n\", changed };\n}\n\nexport function hasOurHooks(existing: string | null): boolean {\n try {\n const obj = parse(existing);\n return Object.values(obj.hooks ?? {}).some((l) => Array.isArray(l) && l.some(isOurs));\n } catch {\n return false;\n }\n}\n","/**\n * The hook script Claude Code runs on every event. Dependency-free, starts in\n * ~40 ms, and can NEVER slow or block the agent: a dead server, a missing\n * port file, bad JSON — all exit 0 silently within 300 ms. It never writes to\n * stdout, so it can never inject context or block a tool.\n *\n * hooks/agent-theatre-hook.mjs must be byte-identical (tests/script.test.ts).\n */\nexport const HOOK_SCRIPT = `#!/usr/bin/env node\n// Agent Theatre hook — forwards Claude Code events to the local theatre. Never blocks the agent.\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nconst portFile = join(process.env.AGENT_THEATRE_HOME || join(homedir(), \".agent-theatre\"), \"port\");\nlet port = 0;\ntry { port = parseInt(readFileSync(portFile, \"utf8\").trim(), 10); } catch { process.exit(0); }\nif (!port) process.exit(0);\nlet body = \"\";\nprocess.stdin.setEncoding(\"utf8\");\nprocess.stdin.on(\"data\", (c) => { body += c; if (body.length > 2_000_000) body = body.slice(0, 2_000_000); });\nprocess.stdin.on(\"end\", async () => {\n try {\n await fetch(\\`http://127.0.0.1:\\${port}/hook\\`, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, signal: AbortSignal.timeout(300) });\n } catch { /* theatre not running or slow: that is fine */ }\n process.exit(0);\n});\nsetTimeout(() => process.exit(0), 600).unref();\n`;\n"],"mappings":"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","names":["existsSync","readFileSync","join","readFileSync","homedir","join","homedir","readFileSync","join","readdirSync","readFileSync","statSync","join","existsSync","mkdirSync","readFileSync","writeFileSync","homedir","join","hooks","join","homedir","existsSync","readFileSync","mkdirSync","writeFileSync","readFileSync","join","existsSync"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as hono_types from 'hono/types';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { FileNode, AgentEvent, Beat } from '@agent-theatre/core';
|
|
4
|
+
|
|
5
|
+
interface TheatreOptions {
|
|
6
|
+
cwd: string;
|
|
7
|
+
repoId: string;
|
|
8
|
+
files: FileNode[];
|
|
9
|
+
/** Directory of the built web bundle (index.html + assets). Optional in tests. */
|
|
10
|
+
webDir?: string;
|
|
11
|
+
/** Where to write the port file. Default ~/.agent-theatre */
|
|
12
|
+
home?: string;
|
|
13
|
+
/** No hook events for this long while a session is open → "lost" signal. */
|
|
14
|
+
lostAfterMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The local theatre. Loopback only. Receives hook payloads, directs them into
|
|
18
|
+
* beats, and streams events + beats to the browser. Serves the web bundle and
|
|
19
|
+
* replay payloads. Never logs or forwards tool input/output: the hook payload
|
|
20
|
+
* is reduced to AgentEvents at the door and discarded.
|
|
21
|
+
*/
|
|
22
|
+
declare class Theatre {
|
|
23
|
+
private readonly opts;
|
|
24
|
+
readonly app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
25
|
+
private server;
|
|
26
|
+
private wss;
|
|
27
|
+
private live;
|
|
28
|
+
private signal;
|
|
29
|
+
private timer;
|
|
30
|
+
port: number;
|
|
31
|
+
constructor(opts: TheatreOptions);
|
|
32
|
+
private routes;
|
|
33
|
+
/** Append events to the live session, direct them, and broadcast. */
|
|
34
|
+
ingest(events: AgentEvent[]): Beat[];
|
|
35
|
+
private broadcast;
|
|
36
|
+
listen(port?: number): Promise<number>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
get url(): string;
|
|
39
|
+
get name(): string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The repo's file tree as FileNodes. Counts lines only; never keeps contents. Bounded for huge repos. */
|
|
43
|
+
declare function scanRepo(root: string, opts?: {
|
|
44
|
+
maxFiles?: number;
|
|
45
|
+
maxBytesForLoc?: number;
|
|
46
|
+
}): FileNode[];
|
|
47
|
+
/** Stable repo identity: git remote URL when available, else the absolute path. */
|
|
48
|
+
declare function repoIdFor(root: string): string;
|
|
49
|
+
|
|
50
|
+
interface SessionInfo {
|
|
51
|
+
id: string;
|
|
52
|
+
path: string;
|
|
53
|
+
mtime: number;
|
|
54
|
+
bytes: number;
|
|
55
|
+
title: string;
|
|
56
|
+
}
|
|
57
|
+
/** Sessions for a repo, newest first, with a human title from the transcript's own title rows (local picker only; never into events). */
|
|
58
|
+
declare function listSessions(cwd: string, env?: NodeJS.ProcessEnv): SessionInfo[];
|
|
59
|
+
/** Title = latest `aiTitle` row, else the latest `lastPrompt` row (trimmed), else the id. Reads at most the last 256 KB. */
|
|
60
|
+
declare function sessionTitle(path: string): string;
|
|
61
|
+
|
|
62
|
+
interface InstallPaths {
|
|
63
|
+
settings: string;
|
|
64
|
+
home: string;
|
|
65
|
+
script: string;
|
|
66
|
+
backup: string;
|
|
67
|
+
}
|
|
68
|
+
declare function installPaths(env?: NodeJS.ProcessEnv): InstallPaths;
|
|
69
|
+
/** Writes the hook script, then merges our entries into settings.json (backup first). Idempotent. */
|
|
70
|
+
declare function installHooks(paths?: InstallPaths): {
|
|
71
|
+
changed: boolean;
|
|
72
|
+
backup: string | null;
|
|
73
|
+
};
|
|
74
|
+
/** Removes only our entries. Leaves the backup file in place for the user. */
|
|
75
|
+
declare function uninstallHooks(paths?: InstallPaths): {
|
|
76
|
+
changed: boolean;
|
|
77
|
+
};
|
|
78
|
+
declare function hooksStatus(paths?: InstallPaths): {
|
|
79
|
+
installed: boolean;
|
|
80
|
+
scriptPresent: boolean;
|
|
81
|
+
settings: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export { type InstallPaths, type SessionInfo, Theatre, type TheatreOptions, hooksStatus, installHooks, installPaths, listSessions, repoIdFor, scanRepo, sessionTitle, uninstallHooks };
|