piu-piu-cli 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.
@@ -0,0 +1,1975 @@
1
+ // ../../apps/daemon/src/config.ts
2
+ import { homedir } from "node:os";
3
+ import { randomBytes } from "node:crypto";
4
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ var home = process.env.PIU_PIU_HOME ?? join(homedir(), ".piu-piu");
7
+ var env = (name) => process.env[`PIU_PIU_${name}`] ?? process.env[`MAZE_OFFICE_${name}`];
8
+ var officeDist = [process.env.PIU_PIU_OFFICE, join(import.meta.dirname, "office"), resolve(import.meta.dirname, "../../office/dist")].find(
9
+ (p) => Boolean(p && existsSync(join(p, "index.html")))
10
+ );
11
+ var config = {
12
+ host: env("HOST") ?? "127.0.0.1",
13
+ port: Number(env("PORT") ?? 7777),
14
+ home,
15
+ officeDist,
16
+ rosterPath: join(home, "agents.yaml"),
17
+ dataDir: home,
18
+ projectsDir: join(homedir(), ".claude", "projects"),
19
+ // Transcripts untouched for longer than this are not replayed at startup.
20
+ replayWindowMs: 24 * 60 * 60 * 1e3,
21
+ // How much of the end of a transcript to replay to rebuild a session's state.
22
+ replayBytes: 512 * 1024,
23
+ // A session with no activity for this long is considered closed.
24
+ staleMs: 30 * 60 * 1e3,
25
+ // "Working" with no activity for this long means the session stalled or was
26
+ // killed without telling us; show it as idle.
27
+ stalledMs: 10 * 60 * 1e3,
28
+ maxEvents: 2e3
29
+ };
30
+ var ROSTER_TEMPLATE = `# piu-piu roster. Sessions whose folder is inside an agent's cwd sit at that agent's desk;
31
+ # anything else gets a "freelancer" desk named after its folder.
32
+ # Agents can also be created in the office (Team > New agent); those are saved in agents.json.
33
+ # Rooms with desks: dev-floor | server-room | library | studio-b
34
+ #
35
+ # agents:
36
+ # - id: budget
37
+ # name: L\xE9a
38
+ # role: dev # dev | lead | devops | web | design | docs | qa | product
39
+ # room: dev-floor
40
+ # cwd: ~/code/budget
41
+ #
42
+ # Seat specialists (Claude Code agents) in a room of your choice:
43
+ # specialists:
44
+ # my-docs-agent: library
45
+ agents: []
46
+ `;
47
+ function prepareHome() {
48
+ const fresh = !existsSync(join(home, "office.db"));
49
+ mkdirSync(home, { recursive: true, mode: 448 });
50
+ const legacy = resolve(import.meta.dirname, "../../../data");
51
+ if (fresh && existsSync(join(legacy, "office.db"))) {
52
+ for (const f of ["office.db", "office.db-wal", "office.db-shm", "agents.json", "channel-token"]) {
53
+ if (existsSync(join(legacy, f))) copyFileSync(join(legacy, f), join(home, f));
54
+ }
55
+ const legacyRoster = resolve(legacy, "../agents.yaml");
56
+ if (existsSync(legacyRoster) && !existsSync(config.rosterPath)) copyFileSync(legacyRoster, config.rosterPath);
57
+ console.log(`[piu-piu] moved your data from ${legacy} to ${home}`);
58
+ }
59
+ if (!existsSync(config.rosterPath)) writeFileSync(config.rosterPath, ROSTER_TEMPLATE);
60
+ }
61
+ function channelToken() {
62
+ const path = join(config.dataDir, "channel-token");
63
+ try {
64
+ return readFileSync(path, "utf8").trim();
65
+ } catch {
66
+ mkdirSync(config.dataDir, { recursive: true });
67
+ const token = randomBytes(24).toString("hex");
68
+ writeFileSync(path, token, { mode: 384 });
69
+ return token;
70
+ }
71
+ }
72
+ function expandHome(p) {
73
+ if (p === "~") return homedir();
74
+ return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
75
+ }
76
+
77
+ // ../../apps/daemon/src/roster.ts
78
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
79
+ import { basename, join as join2 } from "node:path";
80
+ import { parse, parseDocument } from "yaml";
81
+ var appAgentsPath = () => join2(config.dataDir, "agents.json");
82
+ function loadAppAgents() {
83
+ try {
84
+ return JSON.parse(readFileSync2(appAgentsPath(), "utf8")).map((a) => ({ ...a, managed: true, exact: true }));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ function saveAppAgents(agents) {
90
+ writeFileSync2(appAgentsPath(), JSON.stringify(agents.map(({ managed: _m, ...a }) => a), null, 2) + "\n");
91
+ }
92
+ function renameYamlAgent(id, name, path = config.rosterPath) {
93
+ const doc = parseDocument(readFileSync2(path, "utf8"));
94
+ const list = doc.get("agents");
95
+ const item = list?.items?.find((it) => it.get("id") === id);
96
+ if (!item) return false;
97
+ item.set("name", name);
98
+ writeFileSync2(path, doc.toString());
99
+ return true;
100
+ }
101
+ function appAgents() {
102
+ return loadAppAgents();
103
+ }
104
+ function loadRoster(path = config.rosterPath) {
105
+ const doc = parse(readFileSync2(path, "utf8"));
106
+ const fromYaml = (doc.agents ?? []).map((a) => ({ ...a, cwd: expandHome(a.cwd) }));
107
+ const ids = new Set(fromYaml.map((a) => a.id));
108
+ return [...fromYaml, ...loadAppAgents().filter((a) => !ids.has(a.id))];
109
+ }
110
+ function specialistRooms(path = config.rosterPath) {
111
+ try {
112
+ return parse(readFileSync2(path, "utf8")).specialists ?? {};
113
+ } catch {
114
+ return {};
115
+ }
116
+ }
117
+ function assignDesk(roster, cwd) {
118
+ let best;
119
+ for (const agent of roster) {
120
+ const inside = cwd === agent.cwd || !agent.exact && cwd.startsWith(agent.cwd + "/");
121
+ if (inside && (!best || agent.cwd.length > best.cwd.length || agent.cwd.length === best.cwd.length && agent.exact)) best = agent;
122
+ }
123
+ if (best) return { agentId: best.id, agentName: best.name, role: best.role, room: best.room };
124
+ const project = basename(cwd) || "home";
125
+ return { agentId: `freelancer:${project}`, agentName: project, role: "freelancer", room: "dev-floor" };
126
+ }
127
+
128
+ // ../../apps/daemon/src/observe/specialists.ts
129
+ import { EventEmitter } from "node:events";
130
+ import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
131
+ import { homedir as homedir2 } from "node:os";
132
+ import { basename as basename2, join as join3 } from "node:path";
133
+ import chokidar from "chokidar";
134
+ import { parse as parse2 } from "yaml";
135
+ var BUILTINS = [
136
+ { type: "Explore", name: "Explore", room: "library", description: "Read-only search across a codebase: finds files, code and conventions." },
137
+ { type: "Plan", name: "Plan", room: "library", description: "Software architect: designs implementation plans." },
138
+ { type: "general-purpose", name: "General purpose", room: "dev-floor", description: "Researches questions and runs multi-step tasks." },
139
+ { type: "claude-code-guide", name: "Claude Code guide", room: "library", description: "Answers questions about Claude Code, the Agent SDK and the API." },
140
+ { type: "statusline-setup", name: "Status line setup", room: "dev-floor", description: "Configures Claude Code's status line." },
141
+ { type: "fork", name: "Fork", room: "dev-floor", description: "A copy of the calling agent, working on a side task with its context." }
142
+ ];
143
+ var ROOM_RULES = [
144
+ [/doc|wiki|librar|writer|explore|plan|guide|research/i, "library"],
145
+ [/devops|infra|deploy|\bops\b|k8s|kube|cloud|sre|security/i, "server-room"],
146
+ [/seo|content|design|film|motion|video|image|market|brand|copy/i, "studio-b"]
147
+ ];
148
+ function homeRoom(type, description, overrides) {
149
+ if (overrides[type]) return overrides[type];
150
+ for (const text of [type, description.slice(0, 160)]) for (const [re, room] of ROOM_RULES) if (re.test(text)) return room;
151
+ return "dev-floor";
152
+ }
153
+ var titleCase = (s) => s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
154
+ function frontmatter(path) {
155
+ let block;
156
+ try {
157
+ block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(readFileSync3(path, "utf8"))?.[1];
158
+ } catch {
159
+ return void 0;
160
+ }
161
+ if (block === void 0) return void 0;
162
+ try {
163
+ const doc = parse2(block);
164
+ if (doc && typeof doc === "object") return doc;
165
+ } catch {
166
+ }
167
+ const out = {};
168
+ for (const line of block.split(/\r?\n/)) {
169
+ const m = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
170
+ if (m) out[m[1]] = m[2].replace(/^(["'])(.*)\1$/, "$2");
171
+ }
172
+ return out;
173
+ }
174
+ function readAgentsDir(dir, source, prefix = "") {
175
+ let files;
176
+ try {
177
+ files = readdirSync(dir).filter((f) => f.endsWith(".md"));
178
+ } catch {
179
+ return [];
180
+ }
181
+ return files.flatMap((f) => {
182
+ const fm = frontmatter(join3(dir, f));
183
+ if (!fm) return [];
184
+ const name = typeof fm.name === "string" ? fm.name : basename2(f, ".md");
185
+ const description = typeof fm.description === "string" ? fm.description.split(/<example>|\n/)[0].trim() : "";
186
+ return [{
187
+ type: prefix + name,
188
+ name: titleCase(name),
189
+ description,
190
+ model: typeof fm.model === "string" ? fm.model : void 0,
191
+ color: typeof fm.color === "string" ? fm.color : void 0,
192
+ source
193
+ }];
194
+ });
195
+ }
196
+ function pluginAgentDirs() {
197
+ try {
198
+ const installed = JSON.parse(readFileSync3(join3(homedir2(), ".claude", "plugins", "installed_plugins.json"), "utf8"));
199
+ let enabled = {};
200
+ try {
201
+ enabled = JSON.parse(readFileSync3(join3(homedir2(), ".claude", "settings.json"), "utf8")).enabledPlugins ?? {};
202
+ } catch {
203
+ }
204
+ return Object.entries(installed.plugins ?? {}).flatMap(
205
+ ([key, installs]) => enabled[key] === false ? [] : installs.map((i) => ({ dir: join3(i.installPath, "agents"), prefix: `${key.split("@")[0]}:` }))
206
+ );
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+ var Specialists = class extends EventEmitter {
212
+ constructor(overrides) {
213
+ super();
214
+ this.overrides = overrides;
215
+ this.reload();
216
+ const dirs = [join3(homedir2(), ".claude", "agents"), join3(homedir2(), ".claude", "plugins", "installed_plugins.json")];
217
+ chokidar.watch(dirs, { ignoreInitial: true, depth: 0 }).on("all", () => this.refresh());
218
+ }
219
+ list = [];
220
+ lastUsed = /* @__PURE__ */ new Map();
221
+ folders = /* @__PURE__ */ new Set();
222
+ timer;
223
+ get all() {
224
+ return this.list;
225
+ }
226
+ // A folder sessions run in: its .claude/agents may define project specialists.
227
+ addFolder(cwd) {
228
+ if (this.folders.has(cwd)) return;
229
+ this.folders.add(cwd);
230
+ this.refresh();
231
+ }
232
+ // Someone called this type: remember it, and learn types we haven't seen defined.
233
+ used(type, at) {
234
+ if ((this.lastUsed.get(type) ?? 0) >= at) return;
235
+ this.lastUsed.set(type, at);
236
+ const known = this.list.find((s) => s.type === type);
237
+ if (known) {
238
+ known.lastUsed = at;
239
+ this.schedule();
240
+ } else this.refresh();
241
+ }
242
+ refresh() {
243
+ this.schedule(() => this.reload());
244
+ }
245
+ schedule(work) {
246
+ clearTimeout(this.timer);
247
+ this.timer = setTimeout(() => {
248
+ work?.();
249
+ this.emit("changed", this.list);
250
+ }, 400);
251
+ }
252
+ reload() {
253
+ const found = /* @__PURE__ */ new Map();
254
+ const add = (items) => items.forEach((s) => !found.has(s.type) && found.set(s.type, s));
255
+ for (const cwd of this.folders) add(readAgentsDir(join3(cwd, ".claude", "agents"), "project"));
256
+ add(readAgentsDir(join3(homedir2(), ".claude", "agents"), "user"));
257
+ for (const { dir, prefix } of pluginAgentDirs()) add(readAgentsDir(dir, "plugin", prefix));
258
+ add(BUILTINS.map(({ room: _r, ...b }) => ({ ...b, source: "builtin" })));
259
+ for (const type of this.lastUsed.keys()) if (!found.has(type)) found.set(type, { type, name: titleCase(type), description: "", source: "builtin" });
260
+ const overrides = this.overrides();
261
+ const builtinRoom = (type) => BUILTINS.find((b) => b.type === type)?.room;
262
+ this.list = [...found.values()].map((s) => ({
263
+ ...s,
264
+ room: overrides[s.type] ?? (s.source === "builtin" ? builtinRoom(s.type) : void 0) ?? homeRoom(s.type, s.description, overrides),
265
+ lastUsed: this.lastUsed.get(s.type)
266
+ }));
267
+ }
268
+ };
269
+
270
+ // ../../apps/daemon/src/observe/subagents.ts
271
+ import { EventEmitter as EventEmitter3 } from "node:events";
272
+ import { open, readFile, stat } from "node:fs/promises";
273
+ import { basename as basename4, dirname, relative, sep } from "node:path";
274
+ import chokidar2 from "chokidar";
275
+
276
+ // ../../apps/daemon/src/state.ts
277
+ import { EventEmitter as EventEmitter2 } from "node:events";
278
+ import { basename as basename3, resolve as resolve2 } from "node:path";
279
+ var DEDUPE_WINDOW_MS = 1e4;
280
+ var Office = class extends EventEmitter2 {
281
+ constructor(roster, store2) {
282
+ super();
283
+ this.roster = roster;
284
+ this.store = store2;
285
+ for (const s of store2.loadSessions(Date.now() - config.staleMs)) this.sessions.set(s.sessionId, s);
286
+ setInterval(() => this.sweep(), 3e4).unref();
287
+ }
288
+ sessions = /* @__PURE__ */ new Map();
289
+ // Claude Code process id -> the session it is currently running (changes on /clear).
290
+ sessionByPid = /* @__PURE__ */ new Map();
291
+ // session id -> its transcript file, filled in by the transcript watcher
292
+ transcriptPaths = /* @__PURE__ */ new Map();
293
+ recent = /* @__PURE__ */ new Map();
294
+ // Create the session if needed and merge the patch. Returns undefined when the
295
+ // session is unknown and no cwd is available to place it in the office.
296
+ update(sessionId, cwd, patch, opts = {}) {
297
+ if (cwd) cwd = resolve2(cwd);
298
+ let s = this.sessions.get(sessionId);
299
+ if (!s) {
300
+ if (!cwd) return void 0;
301
+ const now = patch.lastActivity ?? Date.now();
302
+ s = {
303
+ sessionId,
304
+ cwd,
305
+ project: basename3(cwd),
306
+ ...assignDesk(this.roster, cwd),
307
+ source: "terminal",
308
+ status: "idle",
309
+ contextTokens: 0,
310
+ costUSD: 0,
311
+ activeSubagents: 0,
312
+ startedAt: now,
313
+ lastActivity: now
314
+ };
315
+ this.sessions.set(sessionId, s);
316
+ }
317
+ Object.assign(s, patch);
318
+ if (!opts.silent) this.publish(s);
319
+ return s;
320
+ }
321
+ // The roster changed (an agent was added or removed): re-seat every session.
322
+ setRoster(roster) {
323
+ this.roster = roster;
324
+ for (const s of this.sessions.values()) {
325
+ const desk = assignDesk(roster, s.cwd);
326
+ if (desk.agentId !== s.agentId || desk.agentName !== s.agentName) {
327
+ Object.assign(s, desk);
328
+ this.publish(s);
329
+ }
330
+ }
331
+ }
332
+ publish(s) {
333
+ this.store.saveSession(s);
334
+ this.emit("session", s);
335
+ }
336
+ // Transcripts and hooks both report tool calls; the first one to arrive wins.
337
+ event(e, dedupeKey) {
338
+ if (dedupeKey) {
339
+ const key = `${e.sessionId}:${e.kind}:${dedupeKey}`;
340
+ const seen = this.recent.get(key);
341
+ if (seen && e.ts - seen < DEDUPE_WINDOW_MS) return;
342
+ this.recent.set(key, e.ts);
343
+ }
344
+ this.store.addEvent(e);
345
+ this.emit("event", e);
346
+ }
347
+ snapshot() {
348
+ return {
349
+ roster: this.roster,
350
+ sessions: [...this.sessions.values()].filter((s) => s.status !== "ended"),
351
+ events: this.store.recentEvents(200)
352
+ };
353
+ }
354
+ sweep() {
355
+ const now = Date.now();
356
+ for (const s of this.sessions.values()) {
357
+ if (s.status !== "ended" && now - s.lastActivity > config.staleMs) {
358
+ s.status = "ended";
359
+ s.currentTool = void 0;
360
+ this.publish(s);
361
+ } else if (s.status === "working" && now - s.lastActivity > config.stalledMs) {
362
+ s.status = "idle";
363
+ s.currentTool = void 0;
364
+ s.currentToolDetail = void 0;
365
+ this.publish(s);
366
+ }
367
+ }
368
+ for (const [key, ts] of this.recent) if (now - ts > DEDUPE_WINDOW_MS) this.recent.delete(key);
369
+ this.store.prune();
370
+ }
371
+ };
372
+ function describeTool(input) {
373
+ if (!input || typeof input !== "object") return void 0;
374
+ const i = input;
375
+ const raw = i.description ?? i.command ?? i.file_path ?? i.pattern ?? i.url ?? i.query ?? i.prompt;
376
+ if (typeof raw !== "string") return void 0;
377
+ const text = raw.replace(/\s+/g, " ").trim();
378
+ return text.length > 120 ? text.slice(0, 117) + "\u2026" : text;
379
+ }
380
+
381
+ // ../../apps/daemon/src/observe/subagents.ts
382
+ var INACTIVE_MS = 10 * 60 * 1e3;
383
+ var RECENT_MS = 24 * 60 * 60 * 1e3;
384
+ var HISTORY_MS = 30 * 24 * 60 * 60 * 1e3;
385
+ var MAX_RUNS = 1e3;
386
+ var HANDBACK_TOOLS = /* @__PURE__ */ new Set(["SubagentHandback"]);
387
+ var Subagents = class extends EventEmitter3 {
388
+ constructor(office2) {
389
+ super();
390
+ this.office = office2;
391
+ const root = config.projectsDir;
392
+ const watcher = chokidar2.watch(root, {
393
+ depth: 3,
394
+ ignoreInitial: false,
395
+ ignored: (path, stats) => {
396
+ const parts = relative(root, path).split(sep);
397
+ if (!parts[0]) return false;
398
+ if (parts[0] === "memory" || parts[1] === "memory") return true;
399
+ if (parts.length <= 2) return Boolean(stats?.isFile());
400
+ if (parts.length === 3) return parts[2] !== "subagents";
401
+ return !/^agent-[\w-]+\.(jsonl|meta\.json)$/.test(parts[3]);
402
+ }
403
+ });
404
+ const onFile = (path) => this.schedule(path);
405
+ watcher.on("add", onFile).on("change", onFile);
406
+ watcher.on("ready", () => {
407
+ this.ready = true;
408
+ console.log("[subagents] watching specialist runs");
409
+ });
410
+ setInterval(() => this.sweep(), 3e4).unref();
411
+ }
412
+ runs = /* @__PURE__ */ new Map();
413
+ paths = /* @__PURE__ */ new Map();
414
+ // run id -> transcript
415
+ tails = /* @__PURE__ */ new Map();
416
+ queue = /* @__PURE__ */ new Map();
417
+ ready = false;
418
+ // SubagentStop time per run: transcript lines written up to then can't reopen it
419
+ stoppedAt = /* @__PURE__ */ new Map();
420
+ // Live runs and those that ended in the last day, newest first.
421
+ recent() {
422
+ const since = Date.now() - RECENT_MS;
423
+ return [...this.runs.values()].filter((r) => r.status === "working" || r.lastActivity > since).sort((a, b) => b.lastActivity - a.lastActivity).slice(0, 200);
424
+ }
425
+ history(type, limit = 50) {
426
+ return [...this.runs.values()].filter((r) => r.type === type).sort((a, b) => b.startedAt - a.startedAt).slice(0, limit);
427
+ }
428
+ // SubagentStart / SubagentStop hooks: instant signals, also for background runs.
429
+ hook(event, p) {
430
+ if (!p.agent_id || !p.session_id) return;
431
+ const now = Date.now();
432
+ let run2 = this.runs.get(p.agent_id);
433
+ if (event === "SubagentStart") {
434
+ if (!run2) {
435
+ run2 = { id: p.agent_id, type: p.agent_type || "general-purpose", parentSessionId: p.session_id, status: "working", startedAt: now, lastActivity: now };
436
+ this.runs.set(run2.id, run2);
437
+ this.announce(run2, true);
438
+ }
439
+ return;
440
+ }
441
+ if (event === "SubagentStop") {
442
+ this.stoppedAt.set(p.agent_id, now);
443
+ if (run2 && run2.status === "working") this.finish(run2, now);
444
+ }
445
+ }
446
+ schedule(path) {
447
+ const id = /agent-([\w-]+)\.(?:jsonl|meta\.json)$/.exec(basename4(path))?.[1];
448
+ if (!id) return;
449
+ const prev = this.queue.get(id) ?? Promise.resolve();
450
+ const next = prev.then(() => this.read(id, path)).catch((err) => console.error(`[subagents] ${path}:`, err.message));
451
+ this.queue.set(id, next);
452
+ }
453
+ async read(id, path) {
454
+ const dir = dirname(path);
455
+ const parentSessionId = basename4(dirname(dir));
456
+ const transcript = `${dir}/agent-${id}.jsonl`;
457
+ let run2 = this.runs.get(id);
458
+ if (!run2) {
459
+ const meta = await readMeta(`${dir}/agent-${id}.meta.json`);
460
+ const info = await stat(transcript).catch(() => stat(path));
461
+ const startedAt = info.birthtimeMs || info.mtimeMs;
462
+ if (Date.now() - info.mtimeMs > HISTORY_MS) return;
463
+ run2 = {
464
+ id,
465
+ type: meta.agentType || "general-purpose",
466
+ parentSessionId,
467
+ description: meta.description,
468
+ background: meta.requestShape === "background" || void 0,
469
+ status: Date.now() - info.mtimeMs > INACTIVE_MS ? "done" : "working",
470
+ startedAt,
471
+ lastActivity: info.mtimeMs,
472
+ endedAt: Date.now() - info.mtimeMs > INACTIVE_MS ? info.mtimeMs : void 0
473
+ };
474
+ this.runs.set(id, run2);
475
+ this.paths.set(id, transcript);
476
+ this.emit("used", run2.type, run2.startedAt);
477
+ if (run2.status === "done" && Date.now() - info.mtimeMs > RECENT_MS) {
478
+ this.trim();
479
+ return;
480
+ }
481
+ } else if (path.endsWith(".meta.json")) {
482
+ const meta = await readMeta(path);
483
+ const changed = meta.agentType && meta.agentType !== run2.type || meta.description && !run2.description;
484
+ if (meta.agentType) run2.type = meta.agentType;
485
+ run2.description ??= meta.description;
486
+ run2.background ??= meta.requestShape === "background" || void 0;
487
+ this.paths.set(id, transcript);
488
+ if (changed) this.publish(run2);
489
+ return;
490
+ }
491
+ this.paths.set(id, transcript);
492
+ if (path.endsWith(".jsonl")) await this.tail(run2, transcript);
493
+ else if (!this.tails.has(id)) await this.tail(run2, transcript).catch(() => void 0);
494
+ }
495
+ async tail(run2, path) {
496
+ const { size } = await stat(path);
497
+ let t = this.tails.get(run2.id);
498
+ const first = !t;
499
+ if (!t) {
500
+ t = { offset: Math.max(0, size - 256 * 1024), partial: "" };
501
+ this.tails.set(run2.id, t);
502
+ }
503
+ if (size < t.offset) Object.assign(t, { offset: 0, partial: "" });
504
+ if (size === t.offset) {
505
+ if (first) this.announce(run2, false);
506
+ return;
507
+ }
508
+ const fh = await open(path, "r");
509
+ const cutStart = first && t.offset > 0;
510
+ try {
511
+ const buf = Buffer.alloc(size - t.offset);
512
+ await fh.read(buf, 0, buf.length, t.offset);
513
+ t.offset = size;
514
+ const lines = (t.partial + buf.toString("utf8")).split("\n");
515
+ t.partial = lines.pop() ?? "";
516
+ lines.forEach((line, i) => {
517
+ if (!line || i === 0 && cutStart) return;
518
+ try {
519
+ this.line(run2, JSON.parse(line));
520
+ } catch {
521
+ }
522
+ });
523
+ } finally {
524
+ await fh.close();
525
+ }
526
+ if (first) {
527
+ if (run2.status === "working" && Date.now() - run2.lastActivity > INACTIVE_MS) {
528
+ run2.status = "done";
529
+ run2.endedAt = run2.lastActivity;
530
+ run2.currentTool = void 0;
531
+ run2.currentToolDetail = void 0;
532
+ }
533
+ this.announce(run2, this.ready && run2.status === "working");
534
+ } else this.publish(run2);
535
+ }
536
+ // One line of the run's own transcript.
537
+ line(run2, o) {
538
+ const ts = o.timestamp ? Date.parse(o.timestamp) : Date.now();
539
+ run2.lastActivity = Math.max(run2.lastActivity, ts);
540
+ const m = o.message ?? {};
541
+ const stopped = this.stoppedAt.get(run2.id);
542
+ if (stopped !== void 0 && ts <= stopped + 2e3) {
543
+ if (o.type === "assistant") {
544
+ for (const b of Array.isArray(m.content) ? m.content : []) if (b.type === "text" && b.text?.trim()) run2.lastText = b.text.trim().slice(0, 280);
545
+ }
546
+ return;
547
+ }
548
+ if (o.type === "assistant") {
549
+ let usedTool = false;
550
+ let handedBack = false;
551
+ for (const b of Array.isArray(m.content) ? m.content : []) {
552
+ if (b.type === "tool_use" && HANDBACK_TOOLS.has(b.name)) {
553
+ handedBack = true;
554
+ if (typeof b.input?.message === "string") run2.lastText = b.input.message.trim().slice(0, 280);
555
+ } else if (b.type === "tool_use") {
556
+ usedTool = true;
557
+ run2.currentTool = b.name;
558
+ run2.currentToolDetail = describeTool(b.input);
559
+ } else if (b.type === "text" && b.text?.trim()) {
560
+ const text = b.text.trim();
561
+ run2.lastText = text.length > 280 ? text.slice(0, 277) + "\u2026" : text;
562
+ }
563
+ }
564
+ if (handedBack || !usedTool && (m.stop_reason === "end_turn" || m.stop_reason === "stop_sequence")) {
565
+ run2.status = "done";
566
+ run2.endedAt = ts;
567
+ run2.currentTool = void 0;
568
+ run2.currentToolDetail = void 0;
569
+ } else {
570
+ run2.status = "working";
571
+ run2.endedAt = void 0;
572
+ }
573
+ } else if (o.type === "user") {
574
+ const result = Array.isArray(m.content) && m.content.some((b) => b.type === "tool_result");
575
+ if (result) {
576
+ run2.currentTool = void 0;
577
+ run2.currentToolDetail = void 0;
578
+ }
579
+ if (Date.now() - ts < INACTIVE_MS && !(result && run2.status === "done")) {
580
+ run2.status = "working";
581
+ run2.endedAt = void 0;
582
+ }
583
+ }
584
+ }
585
+ // First sight of a run: tell the office (and the parent's activity feed if it is live news).
586
+ announce(run2, live) {
587
+ this.publish(run2);
588
+ if (live) this.office.event({ ts: run2.startedAt, sessionId: run2.parentSessionId, kind: "subagent_start", tool: run2.type, detail: run2.description }, run2.id);
589
+ }
590
+ finish(run2, at) {
591
+ run2.status = "done";
592
+ run2.endedAt = at;
593
+ run2.currentTool = void 0;
594
+ run2.currentToolDetail = void 0;
595
+ this.publish(run2);
596
+ }
597
+ wasWorking = /* @__PURE__ */ new Map();
598
+ publish(run2) {
599
+ const parent = this.office.sessions.get(run2.parentSessionId);
600
+ if (parent) {
601
+ run2.parentAgentId = parent.agentId;
602
+ run2.parentName = parent.agentName;
603
+ }
604
+ const was = this.wasWorking.get(run2.id);
605
+ this.wasWorking.set(run2.id, run2.status === "working");
606
+ if (was && run2.status === "done" && this.ready) this.office.event({ ts: run2.endedAt ?? Date.now(), sessionId: run2.parentSessionId, kind: "subagent_stop", tool: run2.type, detail: run2.description }, run2.id);
607
+ if (was !== (run2.status === "working")) this.countFor(run2.parentSessionId);
608
+ this.emit("run", run2);
609
+ }
610
+ // The parent session's helper count is the number of its runs still working.
611
+ countFor(sessionId) {
612
+ const s = this.office.sessions.get(sessionId);
613
+ if (!s) return;
614
+ let n = 0;
615
+ for (const r of this.runs.values()) if (r.parentSessionId === sessionId && r.status === "working") n++;
616
+ if (s.activeSubagents !== n) this.office.update(sessionId, void 0, { activeSubagents: n });
617
+ }
618
+ sweep() {
619
+ const now = Date.now();
620
+ for (const r of this.runs.values()) if (r.status === "working" && now - r.lastActivity > INACTIVE_MS) this.finish(r, r.lastActivity);
621
+ this.trim();
622
+ }
623
+ trim() {
624
+ if (this.runs.size <= MAX_RUNS) return;
625
+ const oldest = [...this.runs.values()].filter((r) => r.status === "done").sort((a, b) => a.lastActivity - b.lastActivity);
626
+ for (const r of oldest.slice(0, this.runs.size - MAX_RUNS)) {
627
+ this.runs.delete(r.id);
628
+ this.paths.delete(r.id);
629
+ this.tails.delete(r.id);
630
+ this.wasWorking.delete(r.id);
631
+ this.stoppedAt.delete(r.id);
632
+ }
633
+ }
634
+ };
635
+ async function readMeta(path) {
636
+ try {
637
+ return JSON.parse(await readFile(path, "utf8"));
638
+ } catch {
639
+ return {};
640
+ }
641
+ }
642
+
643
+ // ../../apps/daemon/src/runtime/agents.ts
644
+ import { randomUUID, timingSafeEqual } from "node:crypto";
645
+ import { EventEmitter as EventEmitter4 } from "node:events";
646
+ import { query } from "@anthropic-ai/claude-agent-sdk";
647
+
648
+ // ../../apps/daemon/src/runtime/peer.ts
649
+ import { execFile } from "node:child_process";
650
+ import { promisify } from "node:util";
651
+ var run = promisify(execFile);
652
+ async function peerAncestry(remotePort, depth = 4) {
653
+ try {
654
+ const { stdout } = await run("lsof", ["-n", "-P", `-iTCP@127.0.0.1:${remotePort}`, "-sTCP:ESTABLISHED", "-Fp"]);
655
+ const pids = [...stdout.matchAll(/^p(\d+)$/gm)].map((m) => Number(m[1])).filter((p) => p !== process.pid);
656
+ if (pids.length !== 1) return void 0;
657
+ const chain = [pids[0]];
658
+ for (let i = 0; i < depth; i++) {
659
+ const { stdout: ppid } = await run("ps", ["-o", "ppid=", "-p", String(chain[chain.length - 1])]);
660
+ const p = Number(ppid.trim());
661
+ if (!p || p === 1) break;
662
+ chain.push(p);
663
+ }
664
+ return chain;
665
+ } catch {
666
+ return void 0;
667
+ }
668
+ }
669
+
670
+ // ../../apps/daemon/src/runtime/agents.ts
671
+ function isAlive(pid) {
672
+ try {
673
+ process.kill(pid, 0);
674
+ return true;
675
+ } catch {
676
+ return false;
677
+ }
678
+ }
679
+ function sameSecret(a, b) {
680
+ const x = Buffer.from(a);
681
+ const y = Buffer.from(b);
682
+ return x.length === y.length && timingSafeEqual(x, y);
683
+ }
684
+ var READ_ONLY_TOOLS = ["Read", "Glob", "Grep", "LS", "TodoWrite", "Task", "Agent", "ToolSearch"];
685
+ var PROMPT_SETTLED = /* @__PURE__ */ new Set(["PostToolUse", "PostToolUseFailure", "Stop", "UserPromptSubmit", "SessionEnd"]);
686
+ var IDLE_CLOSE_MS = 20 * 60 * 1e3;
687
+ function inputStream() {
688
+ const items = [];
689
+ let wake;
690
+ let closed = false;
691
+ return {
692
+ push(text) {
693
+ items.push({ type: "user", message: { role: "user", content: text }, parent_tool_use_id: null });
694
+ wake?.();
695
+ wake = void 0;
696
+ },
697
+ close() {
698
+ closed = true;
699
+ wake?.();
700
+ },
701
+ async *messages() {
702
+ while (!closed) {
703
+ if (items.length) yield items.shift();
704
+ else await new Promise((r) => wake = r);
705
+ }
706
+ }
707
+ };
708
+ }
709
+ var AgentRuntime = class extends EventEmitter4 {
710
+ // a taken-over session runs in its own folder
711
+ constructor(office2, store2, terminals2) {
712
+ super();
713
+ this.office = office2;
714
+ this.store = store2;
715
+ this.terminals = terminals2;
716
+ }
717
+ live = /* @__PURE__ */ new Map();
718
+ pending = /* @__PURE__ */ new Map();
719
+ channels = /* @__PURE__ */ new Map();
720
+ // by Claude Code pid
721
+ modes = /* @__PURE__ */ new Map();
722
+ commandCache = /* @__PURE__ */ new Map();
723
+ terminalOnly = /* @__PURE__ */ new Set(["exit", "quit", "statusline", "terminal-setup", "vim", "ide", "keybindings", "login", "logout", "doctor", "resume", "theme", "config", "install-github-app", "upgrade", "migrate-installer"]);
724
+ cwdOverride = /* @__PURE__ */ new Map();
725
+ // Hand an agent's conversation to the real Claude Code in a terminal. Office
726
+ // chat pauses for that agent until the terminal exits; then the next chat
727
+ // message takes the session back over.
728
+ // how: continue the agent's conversation, pick one in Claude Code's own
729
+ // /resume picker, or start a new one.
730
+ openTerminal(agentId, cols, rows, how = "continue") {
731
+ if (this.terminals.has(agentId)) return;
732
+ const a = this.agent(agentId);
733
+ if (this.isBusy(agentId)) throw new Error(`${a.name} is working in chat mode; stop that task first.`);
734
+ this.closeLive(agentId);
735
+ const closed = this.closedTerminalSession(agentId);
736
+ const cwd = (how === "continue" ? closed?.cwd : void 0) ?? this.cwdOverride.get(agentId) ?? a.cwd;
737
+ let resume;
738
+ if (how === "pick") resume = true;
739
+ else if (how === "continue") {
740
+ resume = closed?.sessionId ?? this.store.agentSession(agentId);
741
+ if (resume) this.store.setAgentSession(agentId, resume);
742
+ }
743
+ this.terminals.start(agentId, { cwd, resume, mode: this.mode(agentId) }, cols, rows);
744
+ this.say(
745
+ agentId,
746
+ "status",
747
+ how === "pick" ? "Opened Claude Code's resume picker in the terminal." : how === "new" ? "Started a new conversation in the terminal." : "Opened in the terminal: the real Claude Code is running this conversation."
748
+ );
749
+ }
750
+ get approvals() {
751
+ return [...this.pending.values()].map((p) => p.approval);
752
+ }
753
+ get running() {
754
+ return [...this.live].filter(([, l]) => l.busy > 0).map(([id]) => id);
755
+ }
756
+ isBusy(agentId) {
757
+ return (this.live.get(agentId)?.busy ?? 0) > 0;
758
+ }
759
+ // End an agent's live process (e.g. to switch to another conversation).
760
+ closeLive(agentId) {
761
+ const l = this.live.get(agentId);
762
+ if (!l) return;
763
+ this.live.delete(agentId);
764
+ clearTimeout(l.idleTimer);
765
+ l.close();
766
+ l.query.close();
767
+ }
768
+ history(agentId, limit = 200) {
769
+ return this.store.chatHistory(agentId, limit);
770
+ }
771
+ // Roster agents come from agents.yaml. Freelancers (sessions in folders the
772
+ // roster doesn't cover) work in the folder their latest session used, and ask
773
+ // before every edit since that folder can be as broad as the home directory.
774
+ agent(agentId) {
775
+ const a = this.office.roster.find((r) => r.id === agentId);
776
+ if (a) return a;
777
+ if (agentId.startsWith("freelancer:")) {
778
+ const latest = [...this.office.sessions.values()].filter((s) => s.agentId === agentId).sort((x, y) => y.lastActivity - x.lastActivity)[0];
779
+ if (latest) return { id: agentId, name: latest.agentName, role: "freelancer", room: "dev-floor", cwd: latest.cwd, permissionMode: "default" };
780
+ }
781
+ throw new Error(`No agent "${agentId}" in agents.yaml`);
782
+ }
783
+ say(agentId, role, text) {
784
+ this.emit("chat", this.store.addChat({ agentId, ts: Date.now(), role, text }));
785
+ }
786
+ // Messages sent while the agent is busy are queued and sent when it finishes.
787
+ send(agentId, text) {
788
+ this.agent(agentId);
789
+ if (this.terminals.has(agentId) && !this.channelFor(agentId)) {
790
+ throw new Error(`${this.agent(agentId).name} is running in the terminal: type there, or close the terminal to chat.`);
791
+ }
792
+ this.say(agentId, "you", text);
793
+ const channel = this.channelFor(agentId);
794
+ console.log(`[runtime] message for ${agentId} -> ${channel ? "terminal session (channel)" : "office agent"}`);
795
+ if (channel) {
796
+ channel.send(JSON.stringify({ type: "message", chatId: agentId, text }));
797
+ return;
798
+ }
799
+ const closed = this.closedTerminalSession(agentId);
800
+ if (closed && !this.isBusy(agentId)) {
801
+ this.closeLive(agentId);
802
+ this.store.setAgentSession(agentId, closed.sessionId);
803
+ this.cwdOverride.set(agentId, closed.cwd);
804
+ this.say(agentId, "status", "The terminal is closed, so the office has taken over this session.");
805
+ }
806
+ const l = this.live.get(agentId) ?? this.start(agentId);
807
+ clearTimeout(l.idleTimer);
808
+ l.busy++;
809
+ if (l.busy === 1) this.emit("running", agentId, true);
810
+ this.markSession(agentId, { status: "working" });
811
+ l.push(text);
812
+ }
813
+ async stop(agentId) {
814
+ const l = this.live.get(agentId);
815
+ if (!l || l.busy === 0) return;
816
+ l.stopping = true;
817
+ for (const [id, p] of this.pending) if (p.approval.agentId === agentId) this.resolve(id, { behavior: "deny", message: "Stopped by the user." });
818
+ await l.query.interrupt();
819
+ }
820
+ // Continue an earlier conversation from its folder: the next message resumes it.
821
+ resume(agentId, session) {
822
+ const a = this.agent(agentId);
823
+ if (this.isBusy(agentId)) throw new Error(`${a.name} is busy; stop the current task first.`);
824
+ this.closeLive(agentId);
825
+ this.store.setAgentSession(agentId, session.sessionId);
826
+ this.cwdOverride.set(agentId, a.cwd);
827
+ this.office.transcriptPaths.set(session.sessionId, session.path);
828
+ this.office.update(session.sessionId, a.cwd, { source: "sdk", status: "idle", title: session.title, lastActivity: Date.now() });
829
+ this.say(agentId, "status", `Resumed \u201C${session.title}\u201D. Your next message continues it.`);
830
+ }
831
+ // Forget in-memory state for an agent id (removed, or about to be reused).
832
+ forget(agentId) {
833
+ this.closeLive(agentId);
834
+ this.modes.delete(agentId);
835
+ this.cwdOverride.delete(agentId);
836
+ this.commandCache.delete(agentId);
837
+ this.store.forgetAgent(agentId);
838
+ }
839
+ mode(agentId) {
840
+ return this.modes.get(agentId) ?? this.store.agentMode(agentId) ?? this.agent(agentId).permissionMode ?? "acceptEdits";
841
+ }
842
+ // Like Shift+Tab in the terminal; applies to the running task too.
843
+ async setMode(agentId, mode) {
844
+ const before = this.mode(agentId);
845
+ this.modes.set(agentId, mode);
846
+ this.store.setAgentMode(agentId, mode);
847
+ const l = this.live.get(agentId);
848
+ if (!l) return;
849
+ if (mode === "bypassPermissions") {
850
+ for (const [id, p] of this.pending) if (p.approval.agentId === agentId && p.approval.kind === "tool" && !p.relayedFromPid) this.resolve(id, { behavior: "allow" });
851
+ if (before !== "bypassPermissions") this.say(agentId, "status", "Allow all is on: nothing will ask for permission.");
852
+ return;
853
+ }
854
+ await l.query.setPermissionMode(mode);
855
+ }
856
+ // The slash commands this agent's sessions support (built-ins, skills,
857
+ // custom commands), minus the ones that only make sense in a terminal.
858
+ // Loading them starts Claude Code without sending any message.
859
+ async commands(agentId) {
860
+ const cached = this.commandCache.get(agentId);
861
+ if (cached && Date.now() - cached.at < 10 * 60 * 1e3) return cached.list;
862
+ const a = this.agent(agentId);
863
+ let release;
864
+ const hold = new Promise((r) => release = r);
865
+ const q = query({
866
+ prompt: (async function* () {
867
+ await hold;
868
+ })(),
869
+ options: { cwd: this.cwdOverride.get(agentId) ?? a.cwd, settingSources: ["user", "project", "local"] }
870
+ });
871
+ try {
872
+ const list = (await q.supportedCommands()).filter((c) => !this.terminalOnly.has(c.name) && !c.name.startsWith("_")).map((c) => ({ name: c.name, description: c.description, argumentHint: c.argumentHint || void 0 })).sort((x, y) => x.name.localeCompare(y.name));
873
+ this.commandCache.set(agentId, { at: Date.now(), list });
874
+ return list;
875
+ } finally {
876
+ release();
877
+ q.close();
878
+ }
879
+ }
880
+ // Forget the conversation; the next message starts a fresh session.
881
+ reset(agentId) {
882
+ this.closeLive(agentId);
883
+ this.store.setAgentSession(agentId, void 0);
884
+ this.say(agentId, "status", "Started a new conversation.");
885
+ }
886
+ // The agent's most recent terminal session, if its terminal has exited and it's
887
+ // newer than the office's own conversation with that agent.
888
+ closedTerminalSession(agentId) {
889
+ const officeSession = this.office.sessions.get(this.store.agentSession(agentId) ?? "");
890
+ const candidates = [...this.office.sessions.values()].filter((s) => s.agentId === agentId && s.source === "terminal" && !s.background && this.office.transcriptPaths.has(s.sessionId)).sort((x, y) => y.lastActivity - x.lastActivity);
891
+ const latest = candidates[0];
892
+ if (!latest || officeSession && officeSession.lastActivity >= latest.lastActivity) return void 0;
893
+ const pid = [...this.office.sessionByPid].find(([, id]) => id === latest.sessionId)?.[0];
894
+ if (pid && isAlive(pid)) return void 0;
895
+ return latest;
896
+ }
897
+ // --- channels: terminal sessions linked to the office ---------------------
898
+ // remotePort identifies the connecting process, so a channel can only speak
899
+ // for the Claude Code session that actually started it.
900
+ attachChannel(socket, remotePort) {
901
+ let pid;
902
+ socket.on("message", async (raw) => {
903
+ let msg;
904
+ try {
905
+ msg = JSON.parse(String(raw));
906
+ } catch {
907
+ return;
908
+ }
909
+ if (msg.type === "hello" && Number.isInteger(msg.pid)) {
910
+ if (pid !== void 0) return;
911
+ const chain = await peerAncestry(remotePort, 1);
912
+ if (typeof msg.token !== "string" || !sameSecret(msg.token, channelToken()) || !chain || chain[1] !== msg.pid) {
913
+ console.warn(`[channel] refused a channel claiming pid ${msg.pid} (peer chain ${chain?.join(" <- ") ?? "unknown"})`);
914
+ socket.close(4003, "not verified");
915
+ return;
916
+ }
917
+ pid = msg.pid;
918
+ this.channels.set(pid, socket);
919
+ this.markChannel(pid, true);
920
+ return;
921
+ }
922
+ if (!pid) return;
923
+ const session = this.sessionForPid(pid);
924
+ if (msg.type === "reply" && typeof msg.text === "string") {
925
+ if (session) this.say(session.agentId, "agent", msg.text);
926
+ } else if (msg.type === "permission" && session && /^[a-km-z]{5}$/.test(msg.requestId)) {
927
+ this.relayPermission(pid, socket, session.agentId, msg);
928
+ }
929
+ });
930
+ socket.on("close", () => {
931
+ if (pid === void 0 || this.channels.get(pid) !== socket) return;
932
+ this.channels.delete(pid);
933
+ this.markChannel(pid, false);
934
+ for (const [id, p] of this.pending) if (p.relayedFromPid === pid) this.drop(id);
935
+ });
936
+ }
937
+ // Called for every hook event: links a pid to its (possibly new) session, and
938
+ // clears relayed prompts that were settled in the terminal.
939
+ hookEvent(pid, eventName) {
940
+ if (this.channels.has(pid)) this.markChannel(pid, true);
941
+ if (PROMPT_SETTLED.has(eventName)) {
942
+ for (const [id, p] of this.pending) if (p.relayedFromPid === pid) this.drop(id);
943
+ }
944
+ }
945
+ routesToChannel(agentId) {
946
+ return Boolean(this.channelFor(agentId));
947
+ }
948
+ // What the daemon knows about linked terminal sessions (for /api/channels).
949
+ channelInfo() {
950
+ return [...this.channels.keys()].map((pid) => {
951
+ const s = this.sessionForPid(pid);
952
+ return { pid, sessionId: this.office.sessionByPid.get(pid), agentId: s?.agentId, status: s?.status, channel: s?.channel };
953
+ });
954
+ }
955
+ sessionForPid(pid) {
956
+ const id = this.office.sessionByPid.get(pid);
957
+ return id ? this.office.sessions.get(id) : void 0;
958
+ }
959
+ markChannel(pid, connected) {
960
+ const s = this.sessionForPid(pid);
961
+ if (s && Boolean(s.channel) !== connected) this.office.update(s.sessionId, s.cwd, { channel: connected });
962
+ }
963
+ channelFor(agentId) {
964
+ let best;
965
+ for (const [pid, ws] of this.channels) {
966
+ const s = this.sessionForPid(pid);
967
+ if (s && s.agentId === agentId && s.status !== "ended" && (!best || s.lastActivity > best.at)) best = { ws, at: s.lastActivity };
968
+ }
969
+ return best?.ws;
970
+ }
971
+ relayPermission(pid, socket, agentId, msg) {
972
+ const approval = {
973
+ id: randomUUID(),
974
+ agentId,
975
+ ts: Date.now(),
976
+ kind: "tool",
977
+ tool: msg.tool,
978
+ detail: msg.preview || msg.description
979
+ };
980
+ const verdict = (behavior) => socket.send(JSON.stringify({ type: "verdict", requestId: msg.requestId, behavior }));
981
+ this.pending.set(approval.id, { approval, input: {}, relayedFromPid: pid, resolve: (r) => verdict(r.behavior === "allow" ? "allow" : "deny") });
982
+ this.emit("approval", approval);
983
+ }
984
+ drop(approvalId) {
985
+ if (this.pending.delete(approvalId)) this.emit("approvalResolved", approvalId);
986
+ }
987
+ resolve(approvalId, decision) {
988
+ const p = this.pending.get(approvalId);
989
+ if (!p) return false;
990
+ this.pending.delete(approvalId);
991
+ const { approval } = p;
992
+ if (decision.behavior === "allow") {
993
+ const updatedInput = approval.kind === "question" ? { ...p.input, answers: decision.answers ?? {} } : p.input;
994
+ p.resolve({ behavior: "allow", updatedInput });
995
+ this.say(approval.agentId, "status", approval.kind === "question" ? "You answered." : `You allowed ${approval.tool}.`);
996
+ } else {
997
+ p.resolve({ behavior: "deny", message: decision.message || "The user declined this." });
998
+ this.say(approval.agentId, "status", `You declined ${approval.tool}${decision.message ? `: ${decision.message}` : "."}`);
999
+ }
1000
+ this.emit("approvalResolved", approvalId);
1001
+ this.markSession(approval.agentId, { status: "working" });
1002
+ return true;
1003
+ }
1004
+ markSession(agentId, patch) {
1005
+ const sessionId = this.live.get(agentId)?.sessionId;
1006
+ if (sessionId) this.office.update(sessionId, this.agent(agentId).cwd, { ...patch, source: "sdk", lastActivity: Date.now() });
1007
+ }
1008
+ start(agentId) {
1009
+ const a = this.agent(agentId);
1010
+ const resume = this.store.agentSession(agentId);
1011
+ const instructions = [
1012
+ `You are ${a.name}, the ${a.role} agent at Mazestudio, working in ${a.cwd}.`,
1013
+ "You are being messaged from the piu-piu office dashboard, not a terminal: keep replies short and plain, and say what you did.",
1014
+ a.skills?.length ? `Use these skills when relevant: ${a.skills.join(", ")}.` : "",
1015
+ a.instructions ?? ""
1016
+ ].filter(Boolean).join("\n");
1017
+ const input = inputStream();
1018
+ const q = query({
1019
+ prompt: input.messages(),
1020
+ options: {
1021
+ cwd: this.cwdOverride.get(agentId) ?? a.cwd,
1022
+ resume,
1023
+ systemPrompt: { type: "preset", preset: "claude_code", append: instructions },
1024
+ settingSources: ["user", "project", "local"],
1025
+ permissionMode: this.mode(agentId),
1026
+ // only processes started in "Allow all" may bypass permissions in the CLI;
1027
+ // switching to it later is honoured by ask()
1028
+ allowDangerouslySkipPermissions: this.mode(agentId) === "bypassPermissions",
1029
+ allowedTools: READ_ONLY_TOOLS,
1030
+ canUseTool: (toolName, toolInput, { signal }) => this.ask(agentId, toolName, toolInput, signal)
1031
+ }
1032
+ });
1033
+ const l = { query: q, push: input.push, close: input.close, busy: 0 };
1034
+ this.live.set(agentId, l);
1035
+ void this.consume(agentId, l, Boolean(resume));
1036
+ return l;
1037
+ }
1038
+ async consume(agentId, l, resumed) {
1039
+ try {
1040
+ for await (const msg of l.query) this.handle(agentId, l, msg);
1041
+ } catch (err) {
1042
+ if (!l.stopping && this.live.get(agentId) === l) this.say(agentId, "error", `Something went wrong: ${err.message}`);
1043
+ if (resumed && /session|resume/i.test(err.message)) this.store.setAgentSession(agentId, void 0);
1044
+ } finally {
1045
+ if (this.live.get(agentId) === l) this.live.delete(agentId);
1046
+ clearTimeout(l.idleTimer);
1047
+ if (l.busy > 0) {
1048
+ l.busy = 0;
1049
+ this.emit("running", agentId, false);
1050
+ this.markSession(agentId, { status: "idle", currentTool: void 0, currentToolDetail: void 0 });
1051
+ }
1052
+ }
1053
+ }
1054
+ // A turn finished (done, failed or stopped).
1055
+ turnEnded(agentId, l) {
1056
+ l.busy = Math.max(0, l.busy - 1);
1057
+ if (l.stopping) {
1058
+ this.say(agentId, "status", "Stopped.");
1059
+ l.stopping = false;
1060
+ l.busy = 0;
1061
+ }
1062
+ if (l.busy > 0) return;
1063
+ this.emit("running", agentId, false);
1064
+ this.markSession(agentId, { status: "idle", currentTool: void 0, currentToolDetail: void 0 });
1065
+ l.idleTimer = setTimeout(() => this.live.get(agentId) === l && this.closeLive(agentId), IDLE_CLOSE_MS);
1066
+ }
1067
+ handle(agentId, l, msg) {
1068
+ switch (msg.type) {
1069
+ case "system":
1070
+ if (msg.subtype === "init") {
1071
+ l.sessionId = msg.session_id;
1072
+ for (const c of msg.terminal_slash_commands ?? []) this.terminalOnly.add(c.replace(/^\//, ""));
1073
+ this.store.setAgentSession(agentId, msg.session_id);
1074
+ this.markSession(agentId, { status: "working" });
1075
+ }
1076
+ return;
1077
+ case "assistant":
1078
+ if (msg.parent_tool_use_id) return;
1079
+ for (const block of msg.message.content) {
1080
+ if (block.type === "text" && block.text.trim()) this.say(agentId, "agent", block.text.trim());
1081
+ else if (block.type === "tool_use") {
1082
+ const detail = describeTool(block.input);
1083
+ this.say(agentId, "tool", `${block.name}${detail ? `: ${detail}` : ""}`);
1084
+ }
1085
+ }
1086
+ return;
1087
+ case "result":
1088
+ if (msg.subtype === "success") {
1089
+ if (msg.num_turns > 0) this.say(agentId, "status", `Done in ${msg.num_turns} turns, $${msg.total_cost_usd.toFixed(2)}.`);
1090
+ } else if (!l.stopping) this.say(agentId, "error", `Stopped: ${msg.subtype.replace(/_/g, " ")}${"errors" in msg && msg.errors.length ? ` (${msg.errors.join("; ")})` : ""}`);
1091
+ this.turnEnded(agentId, l);
1092
+ return;
1093
+ }
1094
+ }
1095
+ // Called by the SDK whenever a tool needs permission or Claude asks a question.
1096
+ // The promise stays pending until you decide in the office.
1097
+ ask(agentId, toolName, input, signal) {
1098
+ const isQuestion = toolName === "AskUserQuestion";
1099
+ if (!isQuestion && this.mode(agentId) === "bypassPermissions") return Promise.resolve({ behavior: "allow", updatedInput: input });
1100
+ const approval = {
1101
+ id: randomUUID(),
1102
+ agentId,
1103
+ ts: Date.now(),
1104
+ kind: isQuestion ? "question" : "tool",
1105
+ tool: toolName,
1106
+ detail: isQuestion ? void 0 : String(input.plan ?? input.command ?? input.url ?? input.query ?? input.file_path ?? describeTool(input) ?? ""),
1107
+ questions: isQuestion ? input.questions : void 0
1108
+ };
1109
+ return new Promise((resolve4) => {
1110
+ this.pending.set(approval.id, { approval, input, resolve: resolve4 });
1111
+ signal.addEventListener("abort", () => {
1112
+ if (this.pending.delete(approval.id)) {
1113
+ this.emit("approvalResolved", approval.id);
1114
+ resolve4({ behavior: "deny", message: "Cancelled." });
1115
+ }
1116
+ });
1117
+ this.emit("approval", approval);
1118
+ this.markSession(agentId, { status: "waiting" });
1119
+ });
1120
+ }
1121
+ };
1122
+
1123
+ // ../../apps/daemon/src/runtime/terminals.ts
1124
+ import { accessSync, chmodSync, constants, existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
1125
+ import { createRequire } from "node:module";
1126
+ import { homedir as homedir3 } from "node:os";
1127
+ import { dirname as dirname2, join as join4 } from "node:path";
1128
+ import { EventEmitter as EventEmitter5 } from "node:events";
1129
+ import * as pty from "node-pty";
1130
+ var SCROLLBACK_BYTES = 2 * 1024 * 1024;
1131
+ var helperChecked = false;
1132
+ function ensureSpawnHelper() {
1133
+ if (helperChecked) return;
1134
+ helperChecked = true;
1135
+ try {
1136
+ const prebuilds = join4(dirname2(createRequire(import.meta.url).resolve("node-pty/package.json")), "prebuilds");
1137
+ for (const arch of existsSync2(prebuilds) ? readdirSync2(prebuilds) : []) {
1138
+ const helper = join4(prebuilds, arch, "spawn-helper");
1139
+ if (!existsSync2(helper)) continue;
1140
+ try {
1141
+ accessSync(helper, constants.X_OK);
1142
+ } catch {
1143
+ chmodSync(helper, 493);
1144
+ }
1145
+ }
1146
+ } catch (err) {
1147
+ console.warn("[terminals] could not check node-pty:", err.message);
1148
+ }
1149
+ }
1150
+ function claudeBinary() {
1151
+ for (const p of [join4(homedir3(), ".local", "bin", "claude"), "/opt/homebrew/bin/claude", "/usr/local/bin/claude"]) if (existsSync2(p)) return p;
1152
+ return "claude";
1153
+ }
1154
+ var Terminals = class extends EventEmitter5 {
1155
+ terms = /* @__PURE__ */ new Map();
1156
+ get running() {
1157
+ return [...this.terms.keys()];
1158
+ }
1159
+ has(agentId) {
1160
+ return this.terms.has(agentId);
1161
+ }
1162
+ pid(agentId) {
1163
+ return this.terms.get(agentId)?.proc.pid;
1164
+ }
1165
+ start(agentId, opts, cols = 100, rows = 30) {
1166
+ if (this.terms.has(agentId)) return;
1167
+ const args = [];
1168
+ if (opts.resume === true) args.push("--resume");
1169
+ else if (opts.resume) args.push("--resume", opts.resume);
1170
+ if (opts.mode === "bypassPermissions") args.push("--dangerously-skip-permissions");
1171
+ else if (opts.mode === "plan" || opts.mode === "acceptEdits") args.push("--permission-mode", opts.mode);
1172
+ ensureSpawnHelper();
1173
+ const proc = pty.spawn(claudeBinary(), args, {
1174
+ name: "xterm-256color",
1175
+ cols,
1176
+ rows,
1177
+ cwd: opts.cwd,
1178
+ env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor", FORCE_COLOR: "1" }
1179
+ });
1180
+ const term = { proc, buffer: "", clients: /* @__PURE__ */ new Set(), cols, rows };
1181
+ this.terms.set(agentId, term);
1182
+ proc.onData((data) => {
1183
+ term.buffer += data;
1184
+ if (term.buffer.length > SCROLLBACK_BYTES) term.buffer = term.buffer.slice(-SCROLLBACK_BYTES);
1185
+ const frame = JSON.stringify({ type: "data", data });
1186
+ for (const c of term.clients) if (c.readyState === c.OPEN) c.send(frame);
1187
+ });
1188
+ proc.onExit(({ exitCode }) => {
1189
+ if (this.terms.get(agentId) !== term) return;
1190
+ this.terms.delete(agentId);
1191
+ const frame = JSON.stringify({ type: "exit", code: exitCode });
1192
+ for (const c of term.clients) if (c.readyState === c.OPEN) c.send(frame);
1193
+ this.emit("changed", this.running);
1194
+ });
1195
+ this.emit("changed", this.running);
1196
+ }
1197
+ // A browser view attaches: replay the scrollback, then stream. Keystrokes
1198
+ // and resizes come back over the same socket.
1199
+ attach(agentId, socket) {
1200
+ const term = this.terms.get(agentId);
1201
+ if (!term) return false;
1202
+ term.clients.add(socket);
1203
+ socket.send(JSON.stringify({ type: "data", data: term.buffer }));
1204
+ socket.on("message", (raw) => {
1205
+ let msg;
1206
+ try {
1207
+ msg = JSON.parse(String(raw));
1208
+ } catch {
1209
+ return;
1210
+ }
1211
+ if (msg.type === "input" && typeof msg.data === "string") term.proc.write(msg.data);
1212
+ else if (msg.type === "resize" && Number.isInteger(msg.cols) && Number.isInteger(msg.rows) && msg.cols > 10 && msg.rows > 3) {
1213
+ term.cols = msg.cols;
1214
+ term.rows = msg.rows;
1215
+ term.proc.resize(msg.cols, msg.rows);
1216
+ }
1217
+ });
1218
+ socket.on("close", () => term.clients.delete(socket));
1219
+ return true;
1220
+ }
1221
+ stop(agentId) {
1222
+ this.terms.get(agentId)?.proc.kill();
1223
+ }
1224
+ stopAll() {
1225
+ for (const t of this.terms.values()) t.proc.kill();
1226
+ }
1227
+ };
1228
+
1229
+ // ../../apps/daemon/src/server.ts
1230
+ import { existsSync as existsSync4 } from "node:fs";
1231
+ import { join as join7 } from "node:path";
1232
+ import fastifyStatic from "@fastify/static";
1233
+ import fastifyWebsocket from "@fastify/websocket";
1234
+ import Fastify from "fastify";
1235
+
1236
+ // ../../apps/daemon/src/observe/skills.ts
1237
+ import { existsSync as existsSync3 } from "node:fs";
1238
+ import { homedir as homedir4 } from "node:os";
1239
+ import { join as join5 } from "node:path";
1240
+ function isSkill(name, cwd) {
1241
+ if (!name || name.includes("/")) return false;
1242
+ if (name.includes(":")) return true;
1243
+ return existsSync3(join5(homedir4(), ".claude", "skills", name)) || Boolean(cwd && existsSync3(join5(cwd, ".claude", "skills", name)));
1244
+ }
1245
+ function skillOf(input) {
1246
+ const skill = input?.skill;
1247
+ return typeof skill === "string" && skill ? skill : void 0;
1248
+ }
1249
+
1250
+ // ../../apps/daemon/src/observe/hooks.ts
1251
+ function handleHook(office2, eventName, p) {
1252
+ const sessionId = p.session_id;
1253
+ if (!sessionId) return;
1254
+ const ts = Date.now();
1255
+ const update = (patch) => office2.update(sessionId, p.cwd, { lastActivity: ts, ...patch });
1256
+ const emit = (kind, extra = {}, key) => office2.event({ ts, sessionId, kind, ...extra }, key);
1257
+ switch (eventName) {
1258
+ case "SessionStart":
1259
+ if (update({ status: "idle" })) emit("session_start");
1260
+ return;
1261
+ case "UserPromptSubmit":
1262
+ if (update({ status: "working" })) emit("prompt", { detail: p.prompt?.slice(0, 200) });
1263
+ return;
1264
+ case "PreToolUse": {
1265
+ const detail = describeTool(p.tool_input);
1266
+ const skill = p.tool_name === "Skill" ? skillOf(p.tool_input) : void 0;
1267
+ const waiting = office2.sessions.get(sessionId)?.status === "waiting";
1268
+ if (update({ ...waiting ? {} : { status: "working" }, currentTool: p.tool_name, currentToolDetail: detail, ...skill ? { activeSkill: skill } : {} })) emit("tool_start", { tool: p.tool_name, detail }, p.tool_use_id);
1269
+ return;
1270
+ }
1271
+ case "PostToolUse":
1272
+ if (update({ status: "working", currentTool: void 0, currentToolDetail: void 0 })) emit("tool_end", {}, p.tool_use_id);
1273
+ return;
1274
+ case "Notification":
1275
+ if (update({ status: "waiting" })) emit("waiting", { detail: p.message });
1276
+ return;
1277
+ case "Stop":
1278
+ if (update({ status: "idle", currentTool: void 0, currentToolDetail: void 0, activeSkill: void 0 })) emit("stop");
1279
+ return;
1280
+ case "SessionEnd": {
1281
+ const chat = office2.sessions.get(sessionId)?.source === "sdk";
1282
+ if (update({ status: chat ? "idle" : "ended", currentTool: void 0, currentToolDetail: void 0 })) emit(chat ? "stop" : "session_end");
1283
+ return;
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ // ../../apps/daemon/src/folders.ts
1289
+ import { open as open2, readdir, stat as stat2 } from "node:fs/promises";
1290
+ import { homedir as homedir5 } from "node:os";
1291
+ import { join as join6, resolve as resolve3 } from "node:path";
1292
+ var projectDir = (cwd) => join6(config.projectsDir, resolve3(cwd).replace(/[^a-zA-Z0-9]/g, "-"));
1293
+ async function suggestFolders(query2, known) {
1294
+ const home2 = homedir5();
1295
+ const out = new Set(known);
1296
+ const roots = [join6(home2, "workspace"), join6(home2, "Documents", "DEV"), join6(home2, "projects"), join6(home2, "code")];
1297
+ async function walk(dir, depth) {
1298
+ if (depth < 0) return;
1299
+ let entries;
1300
+ try {
1301
+ entries = await readdir(dir, { withFileTypes: true });
1302
+ } catch {
1303
+ return;
1304
+ }
1305
+ for (const e of entries) {
1306
+ if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules") continue;
1307
+ const p = join6(dir, e.name);
1308
+ out.add(p);
1309
+ await walk(p, depth - 1);
1310
+ }
1311
+ }
1312
+ for (const r of roots) await walk(r, 2);
1313
+ const typed = query2.trim();
1314
+ if (typed.startsWith("/") || typed.startsWith("~")) {
1315
+ const abs = expandHome(typed);
1316
+ const parent = abs.endsWith("/") ? abs : abs.slice(0, abs.lastIndexOf("/") + 1);
1317
+ try {
1318
+ for (const e of await readdir(parent, { withFileTypes: true })) if (e.isDirectory() && !e.name.startsWith(".")) out.add(join6(parent, e.name));
1319
+ } catch {
1320
+ }
1321
+ }
1322
+ out.add(home2);
1323
+ const q = typed.replace(/^~/, home2).toLowerCase();
1324
+ const list = [...out].filter((p) => !q || p.toLowerCase().includes(q.replace(/^~/, home2)));
1325
+ return list.sort((a, b) => a.length - b.length || a.localeCompare(b)).slice(0, 40);
1326
+ }
1327
+ async function isFolder(path) {
1328
+ try {
1329
+ return (await stat2(expandHome(path))).isDirectory();
1330
+ } catch {
1331
+ return false;
1332
+ }
1333
+ }
1334
+ async function slice(path, start, length) {
1335
+ const fh = await open2(path, "r");
1336
+ try {
1337
+ const buf = Buffer.alloc(length);
1338
+ const { bytesRead } = await fh.read(buf, 0, length, start);
1339
+ return buf.subarray(0, bytesRead).toString("utf8");
1340
+ } finally {
1341
+ await fh.close();
1342
+ }
1343
+ }
1344
+ function lastField(text, key) {
1345
+ const all = [...text.matchAll(new RegExp(`"${key}":"((?:[^"\\\\]|\\\\.)*)"`, "g"))];
1346
+ if (!all.length) return void 0;
1347
+ try {
1348
+ return JSON.parse(`"${all[all.length - 1][1]}"`) || void 0;
1349
+ } catch {
1350
+ return void 0;
1351
+ }
1352
+ }
1353
+ async function titleOf(path, size) {
1354
+ const tail = await slice(path, Math.max(0, size - 1024 * 1024), Math.min(size, 1024 * 1024));
1355
+ const head = size > 1024 * 1024 ? await slice(path, 0, 256 * 1024) : "";
1356
+ for (const key of ["customTitle", "agentName", "aiTitle"]) {
1357
+ const t = lastField(tail, key) ?? lastField(head, key);
1358
+ if (t) return t;
1359
+ }
1360
+ const headText = head || await slice(path, 0, Math.min(size, 256 * 1024));
1361
+ for (const line of headText.split("\n")) {
1362
+ try {
1363
+ const o = JSON.parse(line);
1364
+ if (o.type !== "user" || o.isMeta) continue;
1365
+ const c = o.message?.content;
1366
+ const text = typeof c === "string" ? c : Array.isArray(c) ? c.find((b) => b.type === "text")?.text : void 0;
1367
+ if (text && !text.trim().startsWith("<")) return text.trim().slice(0, 90);
1368
+ } catch {
1369
+ }
1370
+ }
1371
+ return void 0;
1372
+ }
1373
+ async function pastSessions(cwd, limit = 25) {
1374
+ const dir = projectDir(expandHome(cwd));
1375
+ let files;
1376
+ try {
1377
+ files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl"));
1378
+ } catch {
1379
+ return [];
1380
+ }
1381
+ const stats = await Promise.all(files.map(async (f) => ({ f, s: await stat2(join6(dir, f)) })));
1382
+ const newest = [];
1383
+ for (const entry of stats.sort((a, b) => b.s.mtimeMs - a.s.mtimeMs).slice(0, 400)) {
1384
+ if (newest.length >= limit) break;
1385
+ const entrypoint = /"entrypoint":"([^"]+)"/.exec(await slice(join6(dir, entry.f), 0, Math.min(entry.s.size, 1024 * 1024)))?.[1];
1386
+ if (!entrypoint || entrypoint === "cli" || entrypoint === "sdk-ts") newest.push(entry);
1387
+ }
1388
+ return Promise.all(
1389
+ newest.map(async ({ f, s }) => {
1390
+ const path = join6(dir, f);
1391
+ return {
1392
+ sessionId: f.replace(/\.jsonl$/, ""),
1393
+ title: await titleOf(path, s.size) ?? "Untitled conversation",
1394
+ lastActivity: s.mtimeMs,
1395
+ sizeKB: Math.round(s.size / 1024),
1396
+ path
1397
+ };
1398
+ })
1399
+ );
1400
+ }
1401
+
1402
+ // ../../apps/daemon/src/observe/mirror.ts
1403
+ import { open as open3, stat as stat3 } from "node:fs/promises";
1404
+ var OFFICE_REPLY_TOOL = /^mcp__(piu-piu|maze-office)__reply$/;
1405
+ function commandText(text) {
1406
+ const name = /<command-name>([\s\S]*?)<\/command-name>/.exec(text)?.[1]?.trim();
1407
+ if (!name) return void 0;
1408
+ const args = /<command-args>([\s\S]*?)<\/command-args>/.exec(text)?.[1]?.trim();
1409
+ return `${name.startsWith("/") ? name : `/${name}`}${args ? ` ${args}` : ""}`;
1410
+ }
1411
+ function commandOutput(text) {
1412
+ const out = [...text.matchAll(/<local-command-(?:stdout|stderr)>([\s\S]*?)<\/local-command-(?:stdout|stderr)>/g)].map((m) => m[1].trim()).filter(Boolean);
1413
+ return out.length ? out.join("\n") : void 0;
1414
+ }
1415
+ function cleanUserText(text) {
1416
+ const t = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
1417
+ if (!t || t.startsWith("<")) return void 0;
1418
+ return t;
1419
+ }
1420
+ function mirrorMessages(o, offset, sidechain = false) {
1421
+ if (!o || Boolean(o.isSidechain) !== sidechain || o.isMeta || !o.sessionId) return [];
1422
+ const ts = o.timestamp ? Date.parse(o.timestamp) : Date.now();
1423
+ const base = { sessionId: o.sessionId, ts };
1424
+ const content = o.message?.content;
1425
+ const out = [];
1426
+ if (o.type === "system" && o.subtype === "local_command" && typeof o.content === "string") {
1427
+ const text = commandOutput(o.content);
1428
+ return text ? [{ ...base, id: -(offset + 1), role: "agent", text }] : [];
1429
+ }
1430
+ if (o.type === "user") {
1431
+ if (Array.isArray(content) && content.some((b) => b.type === "tool_result")) return [];
1432
+ const raw = typeof content === "string" ? content : Array.isArray(content) ? content.filter((b) => b.type === "text").map((b) => b.text).join("\n") : "";
1433
+ const command = commandText(raw);
1434
+ if (command) return [{ ...base, id: -(offset + 1), role: "you", text: command }];
1435
+ const printed = commandOutput(raw);
1436
+ if (printed) return [{ ...base, id: -(offset + 1), role: "agent", text: printed }];
1437
+ const texts = typeof content === "string" ? [content] : Array.isArray(content) ? content.filter((b) => b.type === "text").map((b) => b.text) : [];
1438
+ const text = texts.map(cleanUserText).filter(Boolean).join("\n\n");
1439
+ if (text) out.push({ ...base, id: -(offset + 1), role: "you", text });
1440
+ } else if (o.type === "assistant" && Array.isArray(content)) {
1441
+ content.forEach((b, i) => {
1442
+ const id = -(offset + 1) - i / 100;
1443
+ if (b.type === "text" && b.text?.trim()) out.push({ ...base, id, role: "agent", text: b.text.trim() });
1444
+ else if (b.type === "tool_use" && b.name === "SubagentHandback" && typeof b.input?.message === "string") out.push({ ...base, id, role: "agent", text: b.input.message.trim() });
1445
+ else if (b.type === "tool_use" && !OFFICE_REPLY_TOOL.test(b.name)) {
1446
+ const detail = describeTool(b.input);
1447
+ out.push({ ...base, id, role: "tool", text: `${b.name}${detail ? `: ${detail}` : ""}` });
1448
+ }
1449
+ });
1450
+ }
1451
+ return out;
1452
+ }
1453
+ async function readMirror(path, limit = 300, before, sidechain = false) {
1454
+ const size = before !== void 0 ? Math.max(0, Math.min(before, (await stat3(path)).size)) : (await stat3(path)).size;
1455
+ if (size === 0) return [];
1456
+ let tail = 4 * 1024 * 1024;
1457
+ for (; ; ) {
1458
+ const messages = await readTail(path, size, tail, sidechain);
1459
+ if (messages.length >= limit || tail >= size || tail >= 128 * 1024 * 1024) return messages.slice(-limit);
1460
+ tail *= 4;
1461
+ }
1462
+ }
1463
+ async function readTail(path, size, tailBytes, sidechain) {
1464
+ const start = Math.max(0, size - tailBytes);
1465
+ const fh = await open3(path, "r");
1466
+ try {
1467
+ const buf = Buffer.alloc(size - start);
1468
+ await fh.read(buf, 0, buf.length, start);
1469
+ const text = buf.toString("utf8");
1470
+ const out = [];
1471
+ let offset = start;
1472
+ const lines = text.split("\n");
1473
+ lines.forEach((line, i) => {
1474
+ const lineOffset = offset;
1475
+ offset += Buffer.byteLength(line) + 1;
1476
+ if (!line || i === 0 && start > 0) return;
1477
+ try {
1478
+ out.push(...mirrorMessages(JSON.parse(line), lineOffset, sidechain));
1479
+ } catch {
1480
+ }
1481
+ });
1482
+ return out;
1483
+ } finally {
1484
+ await fh.close();
1485
+ }
1486
+ }
1487
+
1488
+ // ../../apps/daemon/src/server.ts
1489
+ function trustedOrigin(origin) {
1490
+ if (!origin) return true;
1491
+ try {
1492
+ const u = new URL(origin);
1493
+ return ["127.0.0.1", "localhost"].includes(u.hostname) && [String(config.port), "5173"].includes(u.port);
1494
+ } catch {
1495
+ return false;
1496
+ }
1497
+ }
1498
+ async function startServer(office2, runtime2, store2, terminals2, specialists2, subagents2) {
1499
+ const app2 = Fastify({ logger: false, bodyLimit: 5 * 1024 * 1024 });
1500
+ await app2.register(fastifyWebsocket);
1501
+ const roots = [config.officeDist, join7(import.meta.dirname, "../public"), join7(import.meta.dirname, "public")].filter((p) => Boolean(p && existsSync4(p)));
1502
+ if (!config.officeDist) console.warn("[server] the office is not built: run pnpm build");
1503
+ await app2.register(fastifyStatic, { root: roots });
1504
+ app2.post("/hooks/:event", async (req, reply) => {
1505
+ try {
1506
+ const body = req.body ?? {};
1507
+ handleHook(office2, req.params.event, body);
1508
+ if (req.params.event === "SubagentStart" || req.params.event === "SubagentStop") subagents2.hook(req.params.event, body);
1509
+ const pid = Number(req.headers["x-claude-pid"]) || void 0;
1510
+ if (pid && body.session_id) {
1511
+ if (office2.sessionByPid.get(pid) !== body.session_id) {
1512
+ const chain = await peerAncestry(req.socket.remotePort ?? 0, 3);
1513
+ if (chain?.includes(pid)) office2.sessionByPid.set(pid, body.session_id);
1514
+ else console.warn(`[hooks] ignored a session link for pid ${pid} from an unrelated process`);
1515
+ }
1516
+ if (office2.sessionByPid.get(pid) === body.session_id) runtime2.hookEvent(pid, req.params.event);
1517
+ }
1518
+ } catch (err) {
1519
+ console.error("[hooks]", err);
1520
+ }
1521
+ return reply.code(204).send();
1522
+ });
1523
+ app2.addHook("onRequest", async (req, reply) => {
1524
+ if (req.url.startsWith("/api/") || req.url.startsWith("/ws") || req.url.startsWith("/channel") || req.url.startsWith("/terminal")) {
1525
+ if (!trustedOrigin(req.headers.origin)) return reply.code(403).send({ error: "Forbidden origin" });
1526
+ }
1527
+ });
1528
+ const snapshot = () => ({
1529
+ ...office2.snapshot(),
1530
+ approvals: runtime2.approvals,
1531
+ running: runtime2.running,
1532
+ terminals: terminals2.running,
1533
+ specialists: specialists2.all,
1534
+ subagents: subagents2.recent()
1535
+ });
1536
+ app2.get("/api/state", async () => snapshot());
1537
+ app2.get("/api/sessions/:id/transcript", async (req, reply) => {
1538
+ const path = office2.transcriptPaths.get(req.params.id);
1539
+ if (!path) return reply.code(404).send({ error: "No transcript for that session yet." });
1540
+ const before = req.query.before !== void 0 ? Number(req.query.before) : void 0;
1541
+ return (await readMirror(path, 300, before)).map(({ sessionId: _s, ...m }) => m);
1542
+ });
1543
+ app2.get("/api/subagents/:id/transcript", async (req, reply) => {
1544
+ const path = subagents2.paths.get(req.params.id);
1545
+ if (!path) return reply.code(404).send({ error: "No transcript for that run." });
1546
+ const before = req.query.before !== void 0 ? Number(req.query.before) : void 0;
1547
+ try {
1548
+ return (await readMirror(path, 300, before, true)).map(({ sessionId: _s, ...m }) => ({ ...m, agentId: `run:${req.params.id}` }));
1549
+ } catch {
1550
+ return reply.code(404).send({ error: "That run left no transcript." });
1551
+ }
1552
+ });
1553
+ app2.get("/api/specialists/:type/runs", async (req) => subagents2.history(req.params.type));
1554
+ app2.get("/api/agents/:id/chat", async (req) => runtime2.history(req.params.id, 2e3));
1555
+ app2.get("/api/channels", async (req) => ({
1556
+ channels: runtime2.channelInfo(),
1557
+ links: Object.fromEntries(office2.sessionByPid),
1558
+ routesTo: req.query.agent ? runtime2.routesToChannel(req.query.agent) : void 0
1559
+ }));
1560
+ app2.post("/api/agents/:id/messages", async (req, reply) => {
1561
+ const text = req.body?.text?.trim();
1562
+ if (!text) return reply.code(400).send({ error: "Write a message first." });
1563
+ try {
1564
+ runtime2.send(req.params.id, text);
1565
+ } catch (err) {
1566
+ return reply.code(404).send({ error: err.message });
1567
+ }
1568
+ return reply.code(202).send({ ok: true });
1569
+ });
1570
+ const roomsWithDesks = ["dev-floor", "server-room", "library", "studio-b"];
1571
+ const clients = /* @__PURE__ */ new Set();
1572
+ const reloadRoster = () => {
1573
+ office2.setRoster(loadRoster());
1574
+ for (const send of clients) send({ type: "roster", roster: office2.roster });
1575
+ };
1576
+ app2.post("/api/agents", async (req, reply) => {
1577
+ const name = req.body?.name?.trim();
1578
+ const cwd = req.body?.cwd?.trim();
1579
+ const room = req.body?.room ?? "dev-floor";
1580
+ if (!name) return reply.code(400).send({ error: "Give the agent a name." });
1581
+ if (!cwd || !await isFolder(cwd)) return reply.code(400).send({ error: "Pick a folder that exists on this Mac." });
1582
+ if (!roomsWithDesks.includes(room)) return reply.code(400).send({ error: "Pick a room with desks." });
1583
+ const base = name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent";
1584
+ let id = base;
1585
+ for (let i = 2; office2.roster.some((a) => a.id === id); i++) id = `${base}-${i}`;
1586
+ const agent = { id, name, role: req.body?.role?.trim() || "dev", room, cwd: expandHome(cwd) };
1587
+ runtime2.forget(agent.id);
1588
+ saveAppAgents([...appAgents(), agent]);
1589
+ if (req.body?.adopt?.startsWith("freelancer:")) store2.moveAgent(req.body.adopt, agent.id);
1590
+ reloadRoster();
1591
+ return reply.code(201).send(agent);
1592
+ });
1593
+ app2.patch("/api/agents/:id", async (req, reply) => {
1594
+ const name = req.body?.name?.trim();
1595
+ if (!name) return reply.code(400).send({ error: "The name can\u2019t be empty." });
1596
+ const mine = appAgents();
1597
+ if (mine.some((a) => a.id === req.params.id)) saveAppAgents(mine.map((a) => a.id === req.params.id ? { ...a, name } : a));
1598
+ else if (!renameYamlAgent(req.params.id, name)) return reply.code(404).send({ error: "Unknown agent." });
1599
+ reloadRoster();
1600
+ return { ok: true, name };
1601
+ });
1602
+ app2.delete("/api/agents/:id", async (req, reply) => {
1603
+ const mine = appAgents();
1604
+ if (!mine.some((a) => a.id === req.params.id)) return reply.code(404).send({ error: "Only agents created in the office can be removed here." });
1605
+ saveAppAgents(mine.filter((a) => a.id !== req.params.id));
1606
+ runtime2.forget(req.params.id);
1607
+ reloadRoster();
1608
+ return { ok: true };
1609
+ });
1610
+ app2.get("/api/folders", async (req) => {
1611
+ const known = [.../* @__PURE__ */ new Set([...office2.roster.map((a) => a.cwd), ...[...office2.sessions.values()].map((s) => s.cwd)])];
1612
+ return suggestFolders(req.query.q ?? "", known);
1613
+ });
1614
+ app2.get("/api/folders/sessions", async (req, reply) => {
1615
+ if (!req.query.cwd) return reply.code(400).send({ error: "cwd is required" });
1616
+ return (await pastSessions(req.query.cwd)).map(({ path: _p, ...s }) => s);
1617
+ });
1618
+ app2.post("/api/agents/:id/resume", async (req, reply) => {
1619
+ const agent = office2.roster.find((a) => a.id === req.params.id);
1620
+ if (!agent) return reply.code(404).send({ error: "Unknown agent." });
1621
+ const session = (await pastSessions(agent.cwd, 200)).find((s) => s.sessionId === req.body?.sessionId);
1622
+ if (!session) return reply.code(404).send({ error: "That conversation isn't in this agent's folder." });
1623
+ try {
1624
+ runtime2.resume(agent.id, session);
1625
+ } catch (err) {
1626
+ return reply.code(409).send({ error: err.message });
1627
+ }
1628
+ return { ok: true };
1629
+ });
1630
+ app2.get("/api/agents/:id/commands", async (req, reply) => {
1631
+ try {
1632
+ return { commands: await runtime2.commands(req.params.id), mode: runtime2.mode(req.params.id) };
1633
+ } catch (err) {
1634
+ return reply.code(404).send({ error: err.message });
1635
+ }
1636
+ });
1637
+ app2.post("/api/agents/:id/mode", async (req, reply) => {
1638
+ const mode = req.body?.mode;
1639
+ if (mode !== "default" && mode !== "acceptEdits" && mode !== "plan" && mode !== "bypassPermissions") return reply.code(400).send({ error: "mode must be default, acceptEdits, plan or bypassPermissions" });
1640
+ await runtime2.setMode(req.params.id, mode);
1641
+ return { mode };
1642
+ });
1643
+ app2.post("/api/agents/:id/stop", async (req) => {
1644
+ await runtime2.stop(req.params.id);
1645
+ return { ok: true };
1646
+ });
1647
+ app2.post("/api/agents/:id/reset", async (req) => {
1648
+ runtime2.reset(req.params.id);
1649
+ return { ok: true };
1650
+ });
1651
+ app2.post("/api/approvals/:id", async (req, reply) => {
1652
+ const body = req.body;
1653
+ if (!body || body.behavior !== "allow" && body.behavior !== "deny") return reply.code(400).send({ error: "behavior must be allow or deny" });
1654
+ return runtime2.resolve(req.params.id, body) ? { ok: true } : reply.code(404).send({ error: "That request is no longer waiting." });
1655
+ });
1656
+ app2.get("/ws", { websocket: true }, (socket) => {
1657
+ const send = (msg) => socket.readyState === socket.OPEN && socket.send(JSON.stringify(msg));
1658
+ send({ type: "snapshot", ...snapshot() });
1659
+ clients.add(send);
1660
+ const onSession = (session) => send({ type: "session", session });
1661
+ const onEvent = (event) => send({ type: "event", event });
1662
+ const onChat = (message) => send({ type: "chat", message });
1663
+ const onApproval = (approval) => send({ type: "approval", approval });
1664
+ const onResolved = (id) => send({ type: "approval_resolved", id });
1665
+ const onRunning = (agentId, running) => send({ type: "running", agentId, running });
1666
+ const onTranscript = ({ sessionId, ...m }) => send({ type: "transcript", sessionId, message: { ...m, agentId: office2.sessions.get(sessionId)?.agentId ?? "" } });
1667
+ office2.on("session", onSession).on("event", onEvent).on("transcript", onTranscript);
1668
+ runtime2.on("chat", onChat).on("approval", onApproval).on("approvalResolved", onResolved).on("running", onRunning);
1669
+ socket.on("close", () => {
1670
+ clients.delete(send);
1671
+ office2.off("session", onSession).off("event", onEvent).off("transcript", onTranscript);
1672
+ runtime2.off("chat", onChat).off("approval", onApproval).off("approvalResolved", onResolved).off("running", onRunning);
1673
+ });
1674
+ });
1675
+ app2.post("/api/agents/:id/terminal", async (req, reply) => {
1676
+ const how = req.body?.how === "pick" || req.body?.how === "new" ? req.body.how : "continue";
1677
+ try {
1678
+ runtime2.openTerminal(req.params.id, req.body?.cols, req.body?.rows, how);
1679
+ } catch (err) {
1680
+ return reply.code(409).send({ error: err.message });
1681
+ }
1682
+ return { ok: true };
1683
+ });
1684
+ app2.delete("/api/agents/:id/terminal", async (req) => {
1685
+ terminals2.stop(req.params.id);
1686
+ return { ok: true };
1687
+ });
1688
+ app2.get("/terminal/:id", { websocket: true }, (socket, req) => {
1689
+ if (!terminals2.attach(req.params.id, socket)) {
1690
+ socket.send(JSON.stringify({ type: "exit", code: null }));
1691
+ socket.close();
1692
+ }
1693
+ });
1694
+ terminals2.on("changed", (ids) => {
1695
+ for (const send of clients) send({ type: "terminals", ids });
1696
+ });
1697
+ specialists2.on("changed", (list) => {
1698
+ for (const send of clients) send({ type: "specialists", list });
1699
+ });
1700
+ subagents2.on("run", (run2) => {
1701
+ for (const send of clients) send({ type: "subagent", run: run2 });
1702
+ });
1703
+ app2.get("/channel", { websocket: true }, (socket, req) => runtime2.attachChannel(socket, req.socket.remotePort ?? 0));
1704
+ await app2.listen({ host: config.host, port: config.port });
1705
+ console.log(`[server] http://${config.host}:${config.port}`);
1706
+ return app2;
1707
+ }
1708
+
1709
+ // ../../apps/daemon/src/store.ts
1710
+ import { mkdirSync as mkdirSync2 } from "node:fs";
1711
+ import { join as join8 } from "node:path";
1712
+ import { DatabaseSync } from "node:sqlite";
1713
+ var Store = class {
1714
+ db;
1715
+ constructor(dir = config.dataDir) {
1716
+ mkdirSync2(dir, { recursive: true });
1717
+ this.db = new DatabaseSync(join8(dir, "office.db"));
1718
+ this.db.exec(`
1719
+ PRAGMA journal_mode = WAL;
1720
+ CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, last_activity INTEGER, json TEXT NOT NULL);
1721
+ CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, session_id TEXT, json TEXT NOT NULL);
1722
+ CREATE TABLE IF NOT EXISTS chat (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, ts INTEGER NOT NULL, role TEXT NOT NULL, text TEXT NOT NULL);
1723
+ CREATE INDEX IF NOT EXISTS chat_agent ON chat (agent_id, id);
1724
+ CREATE TABLE IF NOT EXISTS agent_sessions (agent_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, updated INTEGER NOT NULL);
1725
+ CREATE TABLE IF NOT EXISTS agent_modes (agent_id TEXT PRIMARY KEY, mode TEXT NOT NULL);
1726
+ `);
1727
+ }
1728
+ saveSession(s) {
1729
+ this.db.prepare("INSERT INTO sessions (id, last_activity, json) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET last_activity = excluded.last_activity, json = excluded.json").run(s.sessionId, s.lastActivity, JSON.stringify(s));
1730
+ }
1731
+ loadSessions(since) {
1732
+ const rows = this.db.prepare("SELECT json FROM sessions WHERE last_activity >= ?").all(since);
1733
+ return rows.map((r) => JSON.parse(r.json));
1734
+ }
1735
+ addEvent(e) {
1736
+ this.db.prepare("INSERT INTO events (ts, session_id, json) VALUES (?, ?, ?)").run(e.ts, e.sessionId, JSON.stringify(e));
1737
+ }
1738
+ recentEvents(limit) {
1739
+ const rows = this.db.prepare("SELECT json FROM events ORDER BY id DESC LIMIT ?").all(limit);
1740
+ return rows.map((r) => JSON.parse(r.json)).reverse();
1741
+ }
1742
+ addChat(m) {
1743
+ const r = this.db.prepare("INSERT INTO chat (agent_id, ts, role, text) VALUES (?, ?, ?, ?)").run(m.agentId, m.ts, m.role, m.text);
1744
+ return { ...m, id: Number(r.lastInsertRowid) };
1745
+ }
1746
+ chatHistory(agentId, limit = 200) {
1747
+ const rows = this.db.prepare("SELECT id, agent_id AS agentId, ts, role, text FROM chat WHERE agent_id = ? ORDER BY id DESC LIMIT ?").all(agentId, limit);
1748
+ return rows.reverse();
1749
+ }
1750
+ // Each agent's mode (Ask first / Auto-edit / Plan / Allow all) survives restarts.
1751
+ agentMode(agentId) {
1752
+ return this.db.prepare("SELECT mode FROM agent_modes WHERE agent_id = ?").get(agentId)?.mode;
1753
+ }
1754
+ setAgentMode(agentId, mode) {
1755
+ this.db.prepare("INSERT INTO agent_modes (agent_id, mode) VALUES (?, ?) ON CONFLICT(agent_id) DO UPDATE SET mode = excluded.mode").run(agentId, mode);
1756
+ }
1757
+ // An agent was removed (or an id is being reused): drop its saved mode and
1758
+ // conversation link, so a later agent with the same id starts clean rather
1759
+ // than inheriting, say, Allow all.
1760
+ forgetAgent(agentId) {
1761
+ this.db.prepare("DELETE FROM agent_modes WHERE agent_id = ?").run(agentId);
1762
+ this.db.prepare("DELETE FROM agent_sessions WHERE agent_id = ?").run(agentId);
1763
+ }
1764
+ // A freelancer became a named agent: their office chat and conversation follow.
1765
+ moveAgent(from, to) {
1766
+ this.db.prepare("UPDATE chat SET agent_id = ? WHERE agent_id = ?").run(to, from);
1767
+ this.db.prepare("UPDATE OR REPLACE agent_sessions SET agent_id = ? WHERE agent_id = ?").run(to, from);
1768
+ this.db.prepare("UPDATE OR REPLACE agent_modes SET agent_id = ? WHERE agent_id = ?").run(to, from);
1769
+ }
1770
+ // The SDK session each roster agent continues when you message them.
1771
+ agentSession(agentId) {
1772
+ const row = this.db.prepare("SELECT session_id FROM agent_sessions WHERE agent_id = ?").get(agentId);
1773
+ return row?.session_id;
1774
+ }
1775
+ setAgentSession(agentId, sessionId) {
1776
+ if (sessionId) this.db.prepare("INSERT INTO agent_sessions (agent_id, session_id, updated) VALUES (?, ?, ?) ON CONFLICT(agent_id) DO UPDATE SET session_id = excluded.session_id, updated = excluded.updated").run(agentId, sessionId, Date.now());
1777
+ else this.db.prepare("DELETE FROM agent_sessions WHERE agent_id = ?").run(agentId);
1778
+ }
1779
+ prune() {
1780
+ this.db.prepare("DELETE FROM events WHERE id <= (SELECT MAX(id) FROM events) - ?").run(config.maxEvents);
1781
+ this.db.prepare("DELETE FROM sessions WHERE last_activity < ?").run(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1782
+ }
1783
+ };
1784
+
1785
+ // ../../apps/daemon/src/observe/transcripts.ts
1786
+ import { open as open4, stat as stat4 } from "node:fs/promises";
1787
+ import { basename as basename5 } from "node:path";
1788
+ import chokidar3 from "chokidar";
1789
+ function watchTranscripts(office2) {
1790
+ const cursors = /* @__PURE__ */ new Map();
1791
+ const queue = /* @__PURE__ */ new Map();
1792
+ const schedule = (path) => {
1793
+ const prev = queue.get(path) ?? Promise.resolve();
1794
+ const next = prev.then(() => readNew(path)).catch((err) => console.error(`[transcripts] ${path}:`, err.message));
1795
+ queue.set(path, next);
1796
+ };
1797
+ async function readNew(path) {
1798
+ const { size, mtimeMs } = await stat4(path);
1799
+ let cur = cursors.get(path);
1800
+ if (!cur) {
1801
+ cur = { offset: 0, partial: "", pending: {}, replaying: true };
1802
+ cursors.set(path, cur);
1803
+ if (Date.now() - mtimeMs > config.replayWindowMs) {
1804
+ cur.offset = size;
1805
+ cur.replaying = false;
1806
+ return;
1807
+ }
1808
+ cur.offset = Math.max(0, size - config.replayBytes);
1809
+ }
1810
+ if (size < cur.offset) Object.assign(cur, { offset: 0, partial: "" });
1811
+ if (size === cur.offset) return;
1812
+ const fh = await open4(path, "r");
1813
+ try {
1814
+ const buf = Buffer.alloc(size - cur.offset);
1815
+ await fh.read(buf, 0, buf.length, cur.offset);
1816
+ const startedMidFile = cur.replaying && cur.offset > 0 && !cur.partial;
1817
+ let lineOffset = cur.offset - Buffer.byteLength(cur.partial);
1818
+ cur.offset = size;
1819
+ const lines = (cur.partial + buf.toString("utf8")).split("\n");
1820
+ cur.partial = lines.pop() ?? "";
1821
+ lines.forEach((line, i) => {
1822
+ const at = lineOffset;
1823
+ lineOffset += Buffer.byteLength(line) + 1;
1824
+ if (!line || i === 0 && startedMidFile) return;
1825
+ handleLine(cur, line, at, path);
1826
+ });
1827
+ } finally {
1828
+ await fh.close();
1829
+ }
1830
+ if (cur.replaying) {
1831
+ cur.replaying = false;
1832
+ const s = cur.sessionId && office2.sessions.get(cur.sessionId);
1833
+ if (s) {
1834
+ if (Date.now() - s.lastActivity > config.staleMs) s.status = "ended";
1835
+ office2.publish(s);
1836
+ }
1837
+ }
1838
+ }
1839
+ function handleLine(cur, line, offset, path) {
1840
+ let o;
1841
+ try {
1842
+ o = JSON.parse(line);
1843
+ } catch {
1844
+ return;
1845
+ }
1846
+ if (o.isSidechain) return;
1847
+ cur.sessionId ??= o.sessionId;
1848
+ if (cur.sessionId) office2.transcriptPaths.set(cur.sessionId, path);
1849
+ if (typeof o.entrypoint === "string" && o.entrypoint !== "cli" && !cur.pending.background && !office2.sessions.get(cur.sessionId)?.background) {
1850
+ const known = office2.sessions.get(cur.sessionId);
1851
+ if (known) office2.update(cur.sessionId, cur.cwd, { background: true }, { silent: cur.replaying });
1852
+ else cur.pending.background = true;
1853
+ }
1854
+ if (typeof o.gitBranch === "string" && o.gitBranch && cur.sessionId) {
1855
+ const known = office2.sessions.get(cur.sessionId);
1856
+ if (known && known.gitBranch !== o.gitBranch) office2.update(cur.sessionId, cur.cwd, { gitBranch: o.gitBranch }, { silent: cur.replaying });
1857
+ else if (!known) cur.pending.gitBranch = o.gitBranch;
1858
+ }
1859
+ if (!cur.replaying) for (const m of mirrorMessages(o, offset)) office2.emit("transcript", m);
1860
+ if (typeof o.cwd === "string") cur.cwd = o.cwd;
1861
+ if (!cur.sessionId) return;
1862
+ const ts = o.timestamp ? Date.parse(o.timestamp) : Date.now();
1863
+ const silent = cur.replaying;
1864
+ const apply = (patch) => {
1865
+ const merged = { ...cur.pending, ...patch };
1866
+ const s = office2.update(cur.sessionId, cur.cwd, merged, { silent });
1867
+ cur.pending = s ? {} : merged;
1868
+ };
1869
+ const emit = (kind, extra = {}, key) => {
1870
+ if (!silent && office2.sessions.has(cur.sessionId)) office2.event({ ts, sessionId: cur.sessionId, kind, ...extra }, key);
1871
+ };
1872
+ switch (o.type) {
1873
+ case "custom-title":
1874
+ if (o.customTitle) {
1875
+ cur.named = true;
1876
+ apply({ title: o.customTitle });
1877
+ }
1878
+ return;
1879
+ case "ai-title":
1880
+ if (!cur.named) apply({ title: o.aiTitle });
1881
+ return;
1882
+ case "cost-state":
1883
+ apply({ costUSD: o.totalCostUSD ?? 0 });
1884
+ return;
1885
+ case "system":
1886
+ if (o.subtype === "turn_duration") apply({ status: "idle", currentTool: void 0, currentToolDetail: void 0, activeSkill: void 0, lastActivity: ts });
1887
+ return;
1888
+ case "assistant":
1889
+ return handleAssistant(cur, o, ts, apply, emit);
1890
+ case "user":
1891
+ return handleUser(cur, o, ts, apply, emit);
1892
+ }
1893
+ }
1894
+ function handleAssistant(cur, o, ts, apply, emit) {
1895
+ const m = o.message ?? {};
1896
+ const u = m.usage;
1897
+ const waiting = office2.sessions.get(cur.sessionId)?.status === "waiting";
1898
+ const patch = { lastActivity: ts, ...waiting ? {} : { status: "working" } };
1899
+ if (m.model) patch.model = m.model;
1900
+ if (u) patch.contextTokens = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
1901
+ for (const block of Array.isArray(m.content) ? m.content : []) {
1902
+ if (block.type === "tool_use") {
1903
+ const detail = describeTool(block.input);
1904
+ patch.currentTool = block.name;
1905
+ patch.currentToolDetail = detail;
1906
+ if (block.name === "Skill" && skillOf(block.input)) patch.activeSkill = skillOf(block.input);
1907
+ emit("tool_start", { tool: block.name, detail }, block.id);
1908
+ } else if (block.type === "text" && block.text?.trim()) {
1909
+ const text = block.text.trim();
1910
+ patch.lastText = text.length > 280 ? text.slice(0, 277) + "\u2026" : text;
1911
+ emit("text", { detail: patch.lastText });
1912
+ }
1913
+ }
1914
+ if (m.stop_reason === "end_turn" || m.stop_reason === "stop_sequence") {
1915
+ patch.status = "idle";
1916
+ patch.currentTool = void 0;
1917
+ patch.currentToolDetail = void 0;
1918
+ patch.activeSkill = void 0;
1919
+ }
1920
+ apply(patch);
1921
+ }
1922
+ function handleUser(cur, o, ts, apply, emit) {
1923
+ if (o.isMeta) return;
1924
+ const content = o.message?.content;
1925
+ if (Array.isArray(content) && content.some((b) => b.type === "tool_result")) {
1926
+ for (const b of content) {
1927
+ if (b.type !== "tool_result") continue;
1928
+ emit("tool_end", { detail: b.is_error ? "error" : void 0 }, b.tool_use_id);
1929
+ }
1930
+ apply({ lastActivity: ts, status: "working", currentTool: void 0, currentToolDetail: void 0 });
1931
+ return;
1932
+ }
1933
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : void 0;
1934
+ const command = text && /<command-name>\/?([^<\s]+)<\/command-name>/.exec(text)?.[1];
1935
+ if (command && isSkill(command, cur.cwd)) {
1936
+ apply({ lastActivity: ts, status: "working", activeSkill: command });
1937
+ return;
1938
+ }
1939
+ if (!text || text.startsWith("<")) return;
1940
+ apply({ lastActivity: ts, status: "working" });
1941
+ emit("prompt", { detail: text.length > 200 ? text.slice(0, 197) + "\u2026" : text });
1942
+ }
1943
+ const watcher = chokidar3.watch(config.projectsDir, {
1944
+ depth: 1,
1945
+ ignoreInitial: false,
1946
+ ignored: (path, stats) => Boolean(stats?.isFile() && !path.endsWith(".jsonl")) || basename5(path) === "memory"
1947
+ });
1948
+ watcher.on("add", schedule).on("change", schedule);
1949
+ watcher.on("ready", () => console.log(`[transcripts] watching ${config.projectsDir}`));
1950
+ return () => watcher.close();
1951
+ }
1952
+
1953
+ // ../../apps/daemon/src/index.ts
1954
+ prepareHome();
1955
+ channelToken();
1956
+ console.log(`[piu-piu] data in ${config.home}`);
1957
+ var store = new Store();
1958
+ var office = new Office(loadRoster(), store);
1959
+ var terminals = new Terminals();
1960
+ var runtime = new AgentRuntime(office, store, terminals);
1961
+ var stopWatching = watchTranscripts(office);
1962
+ var specialists = new Specialists(specialistRooms);
1963
+ var subagents = new Subagents(office);
1964
+ subagents.on("used", (type, at) => specialists.used(type, at));
1965
+ for (const a of office.roster) specialists.addFolder(a.cwd);
1966
+ office.on("session", (s) => specialists.addFolder(s.cwd));
1967
+ var app = await startServer(office, runtime, store, terminals, specialists, subagents);
1968
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1969
+ process.on(signal, async () => {
1970
+ terminals.stopAll();
1971
+ await stopWatching();
1972
+ await app.close();
1973
+ process.exit(0);
1974
+ });
1975
+ }