@halfwhey/claudraband-core 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/index.js ADDED
@@ -0,0 +1,2491 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
+
19
+ // src/terminal/activity.ts
20
+ async function awaitPaneIdle(capture, options) {
21
+ const intervalMs = options?.intervalMs ?? 250;
22
+ const stableCount = options?.stableCount ?? 3;
23
+ const timeoutMs = options?.timeoutMs ?? 60000;
24
+ const signal = options?.signal;
25
+ if (signal?.aborted)
26
+ return "aborted";
27
+ const deadline = Date.now() + timeoutMs;
28
+ let previous = await capture();
29
+ let consecutiveStable = 0;
30
+ while (Date.now() < deadline) {
31
+ if (signal?.aborted)
32
+ return "aborted";
33
+ await new Promise((resolve, reject) => {
34
+ const timer = setTimeout(resolve, intervalMs);
35
+ if (signal) {
36
+ const onAbort = () => {
37
+ clearTimeout(timer);
38
+ reject(new DOMException("Aborted", "AbortError"));
39
+ };
40
+ signal.addEventListener("abort", onAbort, { once: true });
41
+ const origResolve = resolve;
42
+ resolve = () => {
43
+ signal.removeEventListener("abort", onAbort);
44
+ origResolve();
45
+ };
46
+ }
47
+ }).catch((err) => {
48
+ if (err instanceof DOMException && err.name === "AbortError")
49
+ return;
50
+ throw err;
51
+ });
52
+ if (signal?.aborted)
53
+ return "aborted";
54
+ const current = await capture();
55
+ if (current === previous) {
56
+ consecutiveStable++;
57
+ if (consecutiveStable >= stableCount) {
58
+ return "idle";
59
+ }
60
+ } else {
61
+ consecutiveStable = 0;
62
+ previous = current;
63
+ }
64
+ }
65
+ return "timeout";
66
+ }
67
+
68
+ // src/tmuxctl/tmux.ts
69
+ import { spawn, spawnSync } from "child_process";
70
+ async function tmux(...args) {
71
+ return new Promise((resolve, reject) => {
72
+ const proc = spawn("bash", ["-lc", shellCommand("tmux", ...args)]);
73
+ const out = [];
74
+ const err = [];
75
+ proc.stdout.on("data", (d) => out.push(d.toString()));
76
+ proc.stderr.on("data", (d) => err.push(d.toString()));
77
+ proc.on("close", (code) => {
78
+ if (code === 0) {
79
+ resolve({ stdout: out.join(""), stderr: err.join("") });
80
+ } else {
81
+ reject(new Error(`tmux ${args.join(" ")}: exit ${code} (stderr=${err.join("")})`));
82
+ }
83
+ });
84
+ proc.on("error", reject);
85
+ });
86
+ }
87
+ function shellCommand(...args) {
88
+ return args.map(shellQuote).join(" ");
89
+ }
90
+ function shellQuote(arg) {
91
+ return `'${arg.replace(/'/g, "'\\''")}'`;
92
+ }
93
+ function hasSession(name) {
94
+ return spawnSync("bash", ["-lc", shellCommand("tmux", "has-session", "-t", name)], {
95
+ stdio: "pipe"
96
+ }).status === 0;
97
+ }
98
+ function hasPane(id) {
99
+ return spawnSync("bash", ["-lc", shellCommand("tmux", "display-message", "-p", "-t", id, "#{pane_id}")], { stdio: "pipe" }).status === 0;
100
+ }
101
+ async function killSession(name) {
102
+ if (!hasSession(name))
103
+ return;
104
+ await tmux("kill-session", "-t", name);
105
+ }
106
+ async function listWindows(name) {
107
+ if (!hasSession(name))
108
+ return [];
109
+ const result = await tmux("list-windows", "-t", name, "-F", "#{window_id}\t#{pane_id}\t#{window_name}\t#{pane_current_path}\t#{window_activity}\t#{pane_pid}");
110
+ return result.stdout.trim().split(`
111
+ `).filter(Boolean).map((line) => {
112
+ const [
113
+ windowId,
114
+ paneId,
115
+ windowName,
116
+ paneCurrentPath,
117
+ windowActivity,
118
+ panePid
119
+ ] = line.split("\t", 6);
120
+ return {
121
+ windowId,
122
+ paneId,
123
+ windowName,
124
+ paneCurrentPath: paneCurrentPath || undefined,
125
+ windowActivity: windowActivity || undefined,
126
+ panePid: panePid ? parseInt(panePid, 10) : undefined
127
+ };
128
+ }).filter((window) => window.windowId && window.paneId && window.windowName);
129
+ }
130
+
131
+ class Session {
132
+ name;
133
+ command;
134
+ windowId;
135
+ paneId;
136
+ constructor(name, command, windowId, paneId) {
137
+ this.name = name;
138
+ this.command = command;
139
+ this.windowId = windowId;
140
+ this.paneId = paneId;
141
+ }
142
+ static async newSession(name, width, height, workingDir, command, windowName) {
143
+ if (command.length === 0) {
144
+ throw new Error("tmuxctl: command is required");
145
+ }
146
+ const format = "#{window_id}\t#{pane_id}";
147
+ const result = hasSession(name) ? await tmux("new-window", "-P", "-F", format, "-t", name, "-n", windowName, ...workingDir ? ["-c", workingDir] : [], ...command) : await tmux("new-session", "-d", "-P", "-F", format, "-s", name, "-n", windowName, "-x", String(width), "-y", String(height), ...workingDir ? ["-c", workingDir] : [], ...command);
148
+ const [windowId, paneId] = result.stdout.trim().split(/\s+/, 2);
149
+ if (!windowId || !paneId) {
150
+ throw new Error(`tmuxctl: failed to parse tmux target ids: ${result.stdout}`);
151
+ }
152
+ try {
153
+ await tmux("set-option", "-t", name, "status", "off");
154
+ } catch {}
155
+ await tmux("resize-window", "-t", windowId, "-x", String(width), "-y", String(height));
156
+ return new Session(name, command, windowId, paneId);
157
+ }
158
+ static async find(sessionName, windowName) {
159
+ if (!hasSession(sessionName))
160
+ return null;
161
+ try {
162
+ const result = await tmux("list-windows", "-t", sessionName, "-F", "#{window_id}\t#{pane_id}\t#{window_name}");
163
+ for (const line of result.stdout.trim().split(`
164
+ `)) {
165
+ const [windowId, paneId, name] = line.split("\t", 3);
166
+ if (name === windowName && windowId && paneId) {
167
+ const session = new Session(sessionName, [], windowId, paneId);
168
+ if (session.isAlive)
169
+ return session;
170
+ }
171
+ }
172
+ } catch {}
173
+ return null;
174
+ }
175
+ async kill() {
176
+ if (!this.isAlive)
177
+ return;
178
+ await tmux("kill-window", "-t", this.windowId);
179
+ }
180
+ get isAlive() {
181
+ return hasPane(this.paneId);
182
+ }
183
+ async resize(width, height) {
184
+ await tmux("resize-window", "-t", this.windowId, "-x", String(width), "-y", String(height));
185
+ }
186
+ async sendKeys(input) {
187
+ if (!input)
188
+ return;
189
+ await tmux("send-keys", "-t", this.paneId, "-l", "--", input);
190
+ }
191
+ async sendLine(input) {
192
+ if (input) {
193
+ await this.sendKeys(input);
194
+ }
195
+ await this.sendSpecial("Enter");
196
+ }
197
+ async sendSpecial(...keys) {
198
+ if (keys.length === 0)
199
+ return;
200
+ await tmux("send-keys", "-t", this.paneId, ...keys);
201
+ }
202
+ async interrupt() {
203
+ await this.sendSpecial("C-c");
204
+ }
205
+ async capturePane(opts = {}) {
206
+ const args = ["capture-pane", "-p", "-t", this.paneId];
207
+ if (opts.withEscapes) {
208
+ args.push("-e");
209
+ }
210
+ if (opts.includeScrollback) {
211
+ args.push("-S", "-");
212
+ }
213
+ args.push("-J");
214
+ const result = await tmux(...args);
215
+ return result.stdout;
216
+ }
217
+ async awaitIdle(options) {
218
+ return awaitPaneIdle(() => this.capturePane(), options);
219
+ }
220
+ async panePID() {
221
+ const result = await tmux("display-message", "-p", "-t", this.paneId, "#{pane_pid}");
222
+ return parseInt(result.stdout.trim(), 10);
223
+ }
224
+ }
225
+ var init_tmux = () => {};
226
+
227
+ // src/tmuxctl/index.ts
228
+ var exports_tmuxctl = {};
229
+ __export(exports_tmuxctl, {
230
+ listWindows: () => listWindows,
231
+ killSession: () => killSession,
232
+ hasSession: () => hasSession,
233
+ Session: () => Session
234
+ });
235
+ var init_tmuxctl = __esm(() => {
236
+ init_tmux();
237
+ });
238
+
239
+ // src/index.ts
240
+ import { open as open2, readFile as readFile3, stat as stat2 } from "node:fs/promises";
241
+ import { request as httpRequest } from "node:http";
242
+
243
+ // src/claude/claude.ts
244
+ import { homedir } from "node:os";
245
+ import { join as join2 } from "node:path";
246
+ import { randomUUID } from "node:crypto";
247
+ import { existsSync as existsSync2, statSync } from "node:fs";
248
+
249
+ // src/claude/parser.ts
250
+ import { stat, open } from "node:fs/promises";
251
+
252
+ // src/wrap/event.ts
253
+ var EventKind;
254
+ ((EventKind2) => {
255
+ EventKind2["UserMessage"] = "user_message";
256
+ EventKind2["AssistantText"] = "assistant_text";
257
+ EventKind2["AssistantThinking"] = "assistant_thinking";
258
+ EventKind2["ToolCall"] = "tool_call";
259
+ EventKind2["ToolResult"] = "tool_result";
260
+ EventKind2["System"] = "system";
261
+ EventKind2["Error"] = "error";
262
+ EventKind2["SessionStart"] = "session_start";
263
+ EventKind2["TurnStart"] = "turn_start";
264
+ EventKind2["TurnEnd"] = "turn_end";
265
+ })(EventKind ||= {});
266
+ function makeEvent(partial) {
267
+ return {
268
+ time: new Date,
269
+ text: "",
270
+ toolName: "",
271
+ toolID: "",
272
+ toolInput: "",
273
+ role: "",
274
+ ...partial
275
+ };
276
+ }
277
+
278
+ // src/claude/parser.ts
279
+ function sleep(ms) {
280
+ return new Promise((resolve) => {
281
+ setTimeout(resolve, ms);
282
+ });
283
+ }
284
+ function truncate(s, n) {
285
+ if (s.length <= n)
286
+ return s;
287
+ return s.slice(0, n) + "...";
288
+ }
289
+ function extractToolResultText(b) {
290
+ const raw = b.content;
291
+ if (raw != null) {
292
+ if (typeof raw === "string")
293
+ return raw;
294
+ if (Array.isArray(raw)) {
295
+ const parts = [];
296
+ for (const bb of raw) {
297
+ if (bb.text)
298
+ parts.push(bb.text);
299
+ }
300
+ if (parts.length > 0)
301
+ return parts.join(`
302
+ `);
303
+ }
304
+ return JSON.stringify(raw);
305
+ }
306
+ return "(no output)";
307
+ }
308
+ function parseProgress(env, ts) {
309
+ if (!env.data || typeof env.data !== "object")
310
+ return null;
311
+ const d = env.data;
312
+ let text = d.type ?? "";
313
+ if (d.query)
314
+ text += ": " + d.query;
315
+ return makeEvent({ kind: "system" /* System */, time: ts, text, role: "system" });
316
+ }
317
+ function parseMessageLineEvents(env, ts) {
318
+ if (!env.message)
319
+ return [];
320
+ const msg = env.message;
321
+ if (!msg.content) {
322
+ return msg.role === "assistant" && msg.stop_reason === "end_turn" ? [makeEvent({ kind: "turn_end" /* TurnEnd */, time: ts, role: "assistant" })] : [];
323
+ }
324
+ if (typeof msg.content === "string") {
325
+ const kind = msg.role === "assistant" ? "assistant_text" /* AssistantText */ : "user_message" /* UserMessage */;
326
+ return [
327
+ makeEvent({ kind, time: ts, text: msg.content, role: msg.role }),
328
+ ...msg.role === "assistant" && msg.stop_reason === "end_turn" ? [makeEvent({ kind: "turn_end" /* TurnEnd */, time: ts, role: "assistant" })] : []
329
+ ];
330
+ }
331
+ if (!Array.isArray(msg.content))
332
+ return [];
333
+ const blocks = msg.content;
334
+ const events = [];
335
+ for (const b of blocks) {
336
+ switch (b.type) {
337
+ case "text": {
338
+ const kind = msg.role === "user" ? "user_message" /* UserMessage */ : "assistant_text" /* AssistantText */;
339
+ events.push(makeEvent({ kind, time: ts, text: b.text ?? "", role: msg.role }));
340
+ break;
341
+ }
342
+ case "thinking":
343
+ if (b.thinking) {
344
+ events.push(makeEvent({
345
+ kind: "assistant_thinking" /* AssistantThinking */,
346
+ time: ts,
347
+ text: b.thinking,
348
+ role: "assistant"
349
+ }));
350
+ }
351
+ break;
352
+ case "tool_use": {
353
+ const inp = b.input ? JSON.stringify(b.input) : "{}";
354
+ events.push(makeEvent({
355
+ kind: "tool_call" /* ToolCall */,
356
+ time: ts,
357
+ text: `${b.name}(${truncate(inp, 80)})`,
358
+ toolName: b.name ?? "",
359
+ toolID: b.id ?? "",
360
+ toolInput: inp,
361
+ role: "assistant"
362
+ }));
363
+ break;
364
+ }
365
+ case "tool_result": {
366
+ const text = extractToolResultText(b);
367
+ events.push(makeEvent({
368
+ kind: "tool_result" /* ToolResult */,
369
+ time: ts,
370
+ text: truncate(text, 200),
371
+ toolID: b.tool_use_id ?? "",
372
+ role: "user"
373
+ }));
374
+ break;
375
+ }
376
+ }
377
+ }
378
+ if (msg.role === "assistant" && msg.stop_reason === "end_turn") {
379
+ events.push(makeEvent({ kind: "turn_end" /* TurnEnd */, time: ts, role: "assistant" }));
380
+ }
381
+ return events;
382
+ }
383
+ function parseLineEvents(line) {
384
+ let env;
385
+ try {
386
+ env = JSON.parse(line);
387
+ } catch {
388
+ return [];
389
+ }
390
+ const ts = env.timestamp ? new Date(env.timestamp) : new Date;
391
+ switch (env.type) {
392
+ case "user":
393
+ return parseMessageLineEvents(env, ts);
394
+ case "system":
395
+ if (env.content) {
396
+ return [
397
+ makeEvent({
398
+ kind: "system" /* System */,
399
+ time: ts,
400
+ text: env.content,
401
+ role: "system"
402
+ })
403
+ ];
404
+ }
405
+ return [];
406
+ case "progress": {
407
+ const ev = parseProgress(env, ts);
408
+ return ev ? [ev] : [];
409
+ }
410
+ default:
411
+ if (env.message) {
412
+ return parseMessageLineEvents(env, ts);
413
+ }
414
+ return [];
415
+ }
416
+ }
417
+ async function readAppended(path, offset) {
418
+ try {
419
+ const fi = await stat(path);
420
+ if (fi.size < offset)
421
+ offset = 0;
422
+ if (fi.size === offset)
423
+ return { data: "", offset };
424
+ const f = await open(path, "r");
425
+ try {
426
+ const buf = Buffer.alloc(fi.size - offset);
427
+ await f.read(buf, 0, buf.length, offset);
428
+ return { data: buf.toString(), offset: fi.size };
429
+ } finally {
430
+ await f.close();
431
+ }
432
+ } catch {
433
+ return { data: "", offset };
434
+ }
435
+ }
436
+
437
+ class Tailer {
438
+ path;
439
+ abortController;
440
+ eventQueue = [];
441
+ resolvers = [];
442
+ done = false;
443
+ startOffset;
444
+ constructor(path, startOffset = 0) {
445
+ this.path = path;
446
+ this.startOffset = startOffset;
447
+ this.abortController = new AbortController;
448
+ this.run();
449
+ }
450
+ close() {
451
+ this.abortController.abort();
452
+ }
453
+ async* events() {
454
+ while (true) {
455
+ if (this.eventQueue.length > 0) {
456
+ yield this.eventQueue.shift();
457
+ continue;
458
+ }
459
+ if (this.done)
460
+ return;
461
+ const result = await new Promise((resolve) => {
462
+ this.resolvers.push(resolve);
463
+ });
464
+ if (result.done)
465
+ return;
466
+ yield result.value;
467
+ }
468
+ }
469
+ pushEvent(ev) {
470
+ const resolver = this.resolvers.shift();
471
+ if (resolver) {
472
+ resolver({ value: ev, done: false });
473
+ } else {
474
+ this.eventQueue.push(ev);
475
+ }
476
+ }
477
+ finish() {
478
+ this.done = true;
479
+ for (const resolver of this.resolvers) {
480
+ resolver({ value: undefined, done: true });
481
+ }
482
+ this.resolvers = [];
483
+ }
484
+ emitCompleteLines(pending) {
485
+ while (true) {
486
+ const idx = pending.indexOf(`
487
+ `);
488
+ if (idx < 0)
489
+ return pending;
490
+ const line = pending.slice(0, idx).replace(/\r$/, "");
491
+ pending = pending.slice(idx + 1);
492
+ if (!line)
493
+ continue;
494
+ for (const ev of parseLineEvents(line)) {
495
+ this.pushEvent(ev);
496
+ }
497
+ }
498
+ }
499
+ async run() {
500
+ const signal = this.abortController.signal;
501
+ while (!signal.aborted) {
502
+ try {
503
+ await stat(this.path);
504
+ break;
505
+ } catch {
506
+ await sleep(250);
507
+ if (signal.aborted) {
508
+ this.finish();
509
+ return;
510
+ }
511
+ }
512
+ }
513
+ let offset = this.startOffset;
514
+ let pending = "";
515
+ while (!signal.aborted) {
516
+ const { data, offset: newOffset } = await readAppended(this.path, offset);
517
+ offset = newOffset;
518
+ if (data) {
519
+ pending += data;
520
+ pending = this.emitCompleteLines(pending);
521
+ }
522
+ await sleep(200);
523
+ }
524
+ this.finish();
525
+ }
526
+ }
527
+
528
+ // src/terminal/index.ts
529
+ init_tmuxctl();
530
+ import { spawnSync as spawnSync2 } from "node:child_process";
531
+ function getModuleExports(mod) {
532
+ if (typeof mod === "object" && mod !== null && "default" in mod && mod.default) {
533
+ return mod.default;
534
+ }
535
+ return mod;
536
+ }
537
+ function hasTmuxBinary() {
538
+ const result = spawnSync2("tmux", ["-V"], {
539
+ stdio: "ignore"
540
+ });
541
+ return result.status === 0;
542
+ }
543
+ function hasBunTerminalRuntime() {
544
+ return typeof Bun !== "undefined" && typeof Bun.Terminal === "function";
545
+ }
546
+ function resolveXtermTransportKind(detectBunRuntime = hasBunTerminalRuntime) {
547
+ return detectBunRuntime() ? "bun-terminal" : "node-pty";
548
+ }
549
+ function resolveTerminalBackend(backend, detectTmux = hasTmuxBinary) {
550
+ if (backend === "auto") {
551
+ return detectTmux() ? "tmux" : "xterm";
552
+ }
553
+ if (backend === "tmux" && !detectTmux()) {
554
+ throw new Error("terminal backend 'tmux' was requested, but tmux is not available on PATH");
555
+ }
556
+ return backend;
557
+ }
558
+ function createTerminalHost(options) {
559
+ return createTerminalBackendDriver({
560
+ backend: options.backend,
561
+ tmuxSessionName: options.tmuxSessionName
562
+ }).createHost(options.tmuxWindowName);
563
+ }
564
+ function createTerminalBackendDriver(options) {
565
+ const backend = resolveTerminalBackend(options.backend);
566
+ if (backend === "tmux") {
567
+ return new TmuxTerminalBackendDriver(options.tmuxSessionName);
568
+ }
569
+ return new XtermTerminalBackendDriver;
570
+ }
571
+
572
+ class TmuxTerminalBackendDriver {
573
+ sessionName;
574
+ backend = "tmux";
575
+ constructor(sessionName) {
576
+ this.sessionName = sessionName;
577
+ }
578
+ createHost(windowName) {
579
+ return new TmuxTerminalHost(this.sessionName, windowName);
580
+ }
581
+ async listLiveSessions() {
582
+ const windows = await listWindows(this.sessionName);
583
+ return windows.map((window) => ({
584
+ sessionId: window.windowName,
585
+ cwd: window.paneCurrentPath,
586
+ updatedAt: parseTmuxActivity(window.windowActivity),
587
+ pid: window.panePid
588
+ }));
589
+ }
590
+ async hasLiveSession(sessionId) {
591
+ return await Session.find(this.sessionName, sessionId) !== null;
592
+ }
593
+ async closeLiveSession(sessionId) {
594
+ const found = await Session.find(this.sessionName, sessionId);
595
+ if (!found)
596
+ return false;
597
+ await found.kill();
598
+ return true;
599
+ }
600
+ supportsLiveReconnect() {
601
+ return true;
602
+ }
603
+ }
604
+
605
+ class XtermTerminalBackendDriver {
606
+ backend = "xterm";
607
+ createHost(_windowName) {
608
+ return new XtermTerminalHost;
609
+ }
610
+ async listLiveSessions() {
611
+ return [];
612
+ }
613
+ async hasLiveSession(_sessionId) {
614
+ return false;
615
+ }
616
+ async closeLiveSession(_sessionId) {
617
+ return false;
618
+ }
619
+ supportsLiveReconnect() {
620
+ return false;
621
+ }
622
+ }
623
+ function parseTmuxActivity(activity) {
624
+ if (!activity)
625
+ return;
626
+ const epochSeconds = Number.parseInt(activity, 10);
627
+ if (!Number.isFinite(epochSeconds) || epochSeconds <= 0) {
628
+ return;
629
+ }
630
+ return new Date(epochSeconds * 1000).toISOString();
631
+ }
632
+
633
+ class TmuxTerminalHost {
634
+ sessionName;
635
+ windowName;
636
+ backend = "tmux";
637
+ session = null;
638
+ constructor(sessionName, windowName) {
639
+ this.sessionName = sessionName;
640
+ this.windowName = windowName;
641
+ }
642
+ async start(command, options) {
643
+ this.session = await Session.newSession(this.sessionName, options.cols, options.rows, options.cwd, command, this.windowName);
644
+ }
645
+ async stop() {
646
+ if (!this.session)
647
+ return;
648
+ await this.session.kill().catch(() => {});
649
+ this.session = null;
650
+ }
651
+ async detach() {
652
+ this.session = null;
653
+ }
654
+ async reattach(windowName) {
655
+ const existing = await Session.find(this.sessionName, windowName);
656
+ if (!existing)
657
+ return false;
658
+ this.session = existing;
659
+ return true;
660
+ }
661
+ async send(input) {
662
+ if (!this.session)
663
+ throw new Error("tmux terminal is not started");
664
+ await this.session.sendLine(input);
665
+ }
666
+ async interrupt() {
667
+ if (!this.session)
668
+ throw new Error("tmux terminal is not started");
669
+ await this.session.interrupt();
670
+ }
671
+ async capture() {
672
+ if (!this.session)
673
+ throw new Error("tmux terminal is not started");
674
+ return this.session.capturePane();
675
+ }
676
+ async awaitIdle(options) {
677
+ if (!this.session)
678
+ throw new Error("tmux terminal is not started");
679
+ return this.session.awaitIdle(options);
680
+ }
681
+ alive() {
682
+ return this.session !== null && this.session.isAlive;
683
+ }
684
+ async processId() {
685
+ if (!this.session)
686
+ return;
687
+ return this.session.panePID().catch(() => {
688
+ return;
689
+ });
690
+ }
691
+ }
692
+
693
+ class XtermTerminalHost {
694
+ backend = "xterm";
695
+ transport = null;
696
+ terminal = null;
697
+ serializeAddon = null;
698
+ outputDrain = Promise.resolve();
699
+ exited = false;
700
+ async start(command, options) {
701
+ if (command.length === 0) {
702
+ throw new Error("xterm terminal requires a command");
703
+ }
704
+ const [headlessModule, serializeModule] = await Promise.all([
705
+ import("@xterm/headless"),
706
+ import("@xterm/addon-serialize")
707
+ ]);
708
+ const { Terminal } = getModuleExports(headlessModule);
709
+ const { SerializeAddon } = getModuleExports(serializeModule);
710
+ this.terminal = new Terminal({
711
+ allowProposedApi: true,
712
+ cols: options.cols,
713
+ rows: options.rows,
714
+ scrollback: 5000
715
+ });
716
+ this.serializeAddon = new SerializeAddon;
717
+ this.terminal.loadAddon(this.serializeAddon);
718
+ this.exited = false;
719
+ this.transport = await createPtyTransport();
720
+ this.terminal.onData((data) => {
721
+ this.transport?.write(data);
722
+ });
723
+ this.terminal.onBinary((data) => {
724
+ this.transport?.write(data);
725
+ });
726
+ await this.transport.start(command, options, (data) => {
727
+ this.outputDrain = this.outputDrain.then(() => new Promise((resolve) => {
728
+ this.terminal?.write(data, () => resolve());
729
+ })).catch(() => {});
730
+ }, () => {
731
+ this.exited = true;
732
+ });
733
+ options.signal.addEventListener("abort", () => {
734
+ this.stop();
735
+ }, { once: true });
736
+ }
737
+ async stop() {
738
+ await this.transport?.stop().catch(() => {});
739
+ this.transport = null;
740
+ this.serializeAddon = null;
741
+ this.exited = true;
742
+ await this.outputDrain.catch(() => {});
743
+ }
744
+ async detach() {
745
+ await this.stop();
746
+ }
747
+ async reattach(_windowName) {
748
+ return false;
749
+ }
750
+ async send(input) {
751
+ if (!this.transport)
752
+ throw new Error("xterm terminal is not started");
753
+ if (!this.terminal)
754
+ throw new Error("xterm terminal is not started");
755
+ await this.outputDrain.catch(() => {});
756
+ if (input) {
757
+ for (const char of input) {
758
+ this.terminal.input(char);
759
+ }
760
+ await new Promise((resolve) => setTimeout(resolve, 10));
761
+ }
762
+ this.terminal.input("\r");
763
+ }
764
+ async interrupt() {
765
+ if (!this.transport)
766
+ throw new Error("xterm terminal is not started");
767
+ if (!this.terminal)
768
+ throw new Error("xterm terminal is not started");
769
+ await this.outputDrain.catch(() => {});
770
+ this.terminal.input("\x03");
771
+ }
772
+ async capture() {
773
+ if (!this.serializeAddon)
774
+ throw new Error("xterm terminal is not started");
775
+ await this.outputDrain;
776
+ return this.serializeAddon.serialize();
777
+ }
778
+ async awaitIdle(options) {
779
+ return awaitPaneIdle(() => this.capture(), options);
780
+ }
781
+ alive() {
782
+ return this.transport !== null && this.transport.alive() && !this.exited;
783
+ }
784
+ async processId() {
785
+ return this.transport?.pid();
786
+ }
787
+ }
788
+ async function createPtyTransport() {
789
+ if (resolveXtermTransportKind() === "bun-terminal") {
790
+ return new BunTerminalTransport;
791
+ }
792
+ return new NodePtyTransport;
793
+ }
794
+
795
+ class NodePtyTransport {
796
+ pty = null;
797
+ aliveFlag = false;
798
+ async start(command, options, onOutput, onExit) {
799
+ if (command.length === 0) {
800
+ throw new Error("xterm terminal requires a command");
801
+ }
802
+ let ptyModule;
803
+ try {
804
+ ptyModule = await import("node-pty");
805
+ } catch (error) {
806
+ throw new Error("xterm backend under Node requires the optional dependency 'node-pty'. Install it or run with Bun so Bun.Terminal can be used instead.", { cause: error });
807
+ }
808
+ const pty = getModuleExports(ptyModule);
809
+ const [file, ...args] = command;
810
+ this.pty = pty.spawn(file, args, {
811
+ name: "xterm-256color",
812
+ cols: options.cols,
813
+ rows: options.rows,
814
+ cwd: options.cwd,
815
+ env: {
816
+ ...process.env,
817
+ TERM: "xterm-256color"
818
+ }
819
+ });
820
+ this.aliveFlag = true;
821
+ this.pty.onData((data) => {
822
+ onOutput(data);
823
+ });
824
+ this.pty.onExit(() => {
825
+ this.aliveFlag = false;
826
+ onExit();
827
+ });
828
+ }
829
+ async stop() {
830
+ if (!this.pty)
831
+ return;
832
+ try {
833
+ this.pty.kill();
834
+ } catch {}
835
+ this.pty = null;
836
+ this.aliveFlag = false;
837
+ }
838
+ write(data) {
839
+ if (!this.pty)
840
+ throw new Error("node-pty transport is not started");
841
+ this.pty.write(data);
842
+ }
843
+ alive() {
844
+ return this.pty !== null && this.aliveFlag;
845
+ }
846
+ pid() {
847
+ return this.pty?.pid;
848
+ }
849
+ }
850
+
851
+ class BunTerminalTransport {
852
+ terminal = null;
853
+ process = null;
854
+ decoder = new TextDecoder;
855
+ aliveFlag = false;
856
+ async start(command, options, onOutput, onExit) {
857
+ if (command.length === 0) {
858
+ throw new Error("xterm terminal requires a command");
859
+ }
860
+ if (!hasBunTerminalRuntime()) {
861
+ throw new Error("bun terminal transport requires Bun runtime");
862
+ }
863
+ this.terminal = new Bun.Terminal({
864
+ cols: options.cols,
865
+ rows: options.rows,
866
+ name: "xterm-256color",
867
+ data: (_terminal, data) => {
868
+ const text = this.decoder.decode(data, { stream: true });
869
+ if (text) {
870
+ onOutput(text);
871
+ }
872
+ },
873
+ exit: () => {
874
+ this.aliveFlag = false;
875
+ const rest = this.decoder.decode();
876
+ if (rest) {
877
+ onOutput(rest);
878
+ }
879
+ onExit();
880
+ }
881
+ });
882
+ this.process = Bun.spawn(command, {
883
+ cwd: options.cwd,
884
+ env: {
885
+ ...process.env,
886
+ TERM: "xterm-256color"
887
+ },
888
+ terminal: this.terminal
889
+ });
890
+ this.aliveFlag = true;
891
+ }
892
+ async stop() {
893
+ if (!this.terminal && !this.process)
894
+ return;
895
+ try {
896
+ this.process?.kill();
897
+ } catch {}
898
+ try {
899
+ this.terminal?.close();
900
+ } catch {}
901
+ this.process = null;
902
+ this.terminal = null;
903
+ this.aliveFlag = false;
904
+ }
905
+ write(data) {
906
+ if (!this.terminal)
907
+ throw new Error("bun terminal transport is not started");
908
+ this.terminal.write(data);
909
+ }
910
+ alive() {
911
+ return this.terminal !== null && !this.terminal.closed && this.aliveFlag;
912
+ }
913
+ pid() {
914
+ return this.process?.pid;
915
+ }
916
+ }
917
+
918
+ // src/claude/resolve.ts
919
+ import { dirname, join } from "node:path";
920
+ import { existsSync } from "node:fs";
921
+ import { createRequire as createRequire2 } from "node:module";
922
+ var require2 = createRequire2(import.meta.url);
923
+ var ENV_OVERRIDE = "CLAUDRABAND_CLAUDE_PATH";
924
+ function resolveClaudeExecutable(explicitPath) {
925
+ if (explicitPath) {
926
+ return explicitPath;
927
+ }
928
+ const envOverride = process.env[ENV_OVERRIDE];
929
+ if (envOverride) {
930
+ return envOverride;
931
+ }
932
+ try {
933
+ const packageJsonPath = require2.resolve("@anthropic-ai/claude-code/package.json");
934
+ const packageJson = require2(packageJsonPath);
935
+ const binPath = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.claude;
936
+ if (!binPath) {
937
+ throw new Error("missing bin.claude entry");
938
+ }
939
+ const executable = join(dirname(packageJsonPath), binPath);
940
+ if (!existsSync(executable)) {
941
+ throw new Error(`resolved bin not found at ${executable}`);
942
+ }
943
+ return executable;
944
+ } catch (error) {
945
+ const reason = error instanceof Error ? error.message : String(error);
946
+ throw new Error(`claudraband could not resolve bundled Claude Code @anthropic-ai/claude-code@2.1.96 (${reason}). ` + `Install dependencies or set ${ENV_OVERRIDE} or ClaudrabandOptions.claudeExecutable.`);
947
+ }
948
+ }
949
+
950
+ // src/claude/claude.ts
951
+ function sessionPath(cwd, sessionID) {
952
+ const home = homedir();
953
+ const escaped = cwd.replace(/\//g, "-");
954
+ return join2(home, ".claude", "projects", escaped, `${sessionID}.jsonl`);
955
+ }
956
+
957
+ class ClaudeWrapper {
958
+ cfg;
959
+ terminal = null;
960
+ tailer = null;
961
+ _claudeSessionId = "";
962
+ _signal = null;
963
+ abortController = null;
964
+ eventIterable = null;
965
+ constructor(cfg) {
966
+ this.cfg = cfg;
967
+ }
968
+ name() {
969
+ return "claude";
970
+ }
971
+ model() {
972
+ return this.cfg.model;
973
+ }
974
+ setModel(model) {
975
+ this.cfg.model = model;
976
+ }
977
+ setPermissionMode(mode) {
978
+ this.cfg.permissionMode = mode;
979
+ }
980
+ get claudeSessionId() {
981
+ return this._claudeSessionId;
982
+ }
983
+ async start(signal) {
984
+ this._claudeSessionId = randomUUID();
985
+ this._signal = signal;
986
+ const cmd = this.buildCmd("--session-id", this._claudeSessionId);
987
+ await this.spawnAndTail(signal, cmd);
988
+ }
989
+ async startResume(claudeSessionId, signal) {
990
+ this._claudeSessionId = claudeSessionId;
991
+ this._signal = signal;
992
+ const terminal = createTerminalHost({
993
+ backend: this.cfg.terminalBackend,
994
+ tmuxSessionName: this.cfg.tmuxSession,
995
+ tmuxWindowName: claudeSessionId
996
+ });
997
+ const reattached = await terminal.reattach(claudeSessionId);
998
+ if (reattached) {
999
+ this.terminal = terminal;
1000
+ this.abortController = new AbortController;
1001
+ signal.addEventListener("abort", () => {
1002
+ this.abortController?.abort();
1003
+ });
1004
+ const jsonlPath2 = sessionPath(this.cfg.workingDir, claudeSessionId);
1005
+ let tailOffset2 = 0;
1006
+ if (existsSync2(jsonlPath2)) {
1007
+ tailOffset2 = statSync(jsonlPath2).size;
1008
+ }
1009
+ this.tailer = new Tailer(jsonlPath2, tailOffset2);
1010
+ this.eventIterable = this.tailer.events();
1011
+ const ac = this.abortController;
1012
+ const tailer = this.tailer;
1013
+ const term = this.terminal;
1014
+ ac.signal.addEventListener("abort", async () => {
1015
+ tailer.close();
1016
+ await term?.stop().catch(() => {});
1017
+ }, { once: true });
1018
+ return;
1019
+ }
1020
+ const jsonlPath = sessionPath(this.cfg.workingDir, claudeSessionId);
1021
+ let tailOffset = 0;
1022
+ if (existsSync2(jsonlPath)) {
1023
+ tailOffset = statSync(jsonlPath).size;
1024
+ }
1025
+ const cmd = existsSync2(jsonlPath) ? this.buildCmd("--resume", claudeSessionId) : this.buildCmd("--session-id", claudeSessionId);
1026
+ await this.spawnAndTail(signal, cmd, tailOffset);
1027
+ }
1028
+ async restart() {
1029
+ if (!this._signal)
1030
+ throw new Error("claude: not started");
1031
+ this.tailer?.close();
1032
+ if (this.terminal) {
1033
+ await this.terminal.stop().catch(() => {});
1034
+ }
1035
+ this.terminal = null;
1036
+ this.tailer = null;
1037
+ this.eventIterable = null;
1038
+ const jsonlPath = sessionPath(this.cfg.workingDir, this._claudeSessionId);
1039
+ let tailOffset = 0;
1040
+ if (existsSync2(jsonlPath)) {
1041
+ tailOffset = statSync(jsonlPath).size;
1042
+ }
1043
+ const cmd = existsSync2(jsonlPath) ? this.buildCmd("--resume", this._claudeSessionId) : this.buildCmd("--session-id", this._claudeSessionId);
1044
+ await this.spawnAndTail(this._signal, cmd, tailOffset);
1045
+ }
1046
+ buildCmd(...extra) {
1047
+ const cmd = [
1048
+ resolveClaudeExecutable(this.cfg.claudeExecutable),
1049
+ "--model",
1050
+ this.cfg.model,
1051
+ ...this.cfg.claudeArgs
1052
+ ];
1053
+ if (this.cfg.permissionMode && this.cfg.permissionMode !== "default") {
1054
+ cmd.push("--permission-mode", this.cfg.permissionMode);
1055
+ }
1056
+ cmd.push(...extra);
1057
+ return cmd;
1058
+ }
1059
+ async spawnAndTail(signal, cmd, tailOffset = 0) {
1060
+ this.abortController = new AbortController;
1061
+ this.terminal = createTerminalHost({
1062
+ backend: this.cfg.terminalBackend,
1063
+ tmuxSessionName: this.cfg.tmuxSession,
1064
+ tmuxWindowName: this._claudeSessionId
1065
+ });
1066
+ signal.addEventListener("abort", () => {
1067
+ this.abortController?.abort();
1068
+ });
1069
+ await this.terminal.start(cmd, {
1070
+ cwd: this.cfg.workingDir,
1071
+ cols: this.cfg.paneWidth,
1072
+ rows: this.cfg.paneHeight,
1073
+ signal
1074
+ });
1075
+ await this.waitForReady(signal);
1076
+ const jsonlPath = sessionPath(this.cfg.workingDir, this._claudeSessionId);
1077
+ this.tailer = new Tailer(jsonlPath, tailOffset);
1078
+ this.eventIterable = this.tailer.events();
1079
+ const ac = this.abortController;
1080
+ const terminal = this.terminal;
1081
+ const tailer = this.tailer;
1082
+ ac.signal.addEventListener("abort", async () => {
1083
+ tailer.close();
1084
+ await terminal?.stop().catch(() => {});
1085
+ }, { once: true });
1086
+ }
1087
+ async waitForReady(signal) {
1088
+ const MAX_WAIT_MS = 15000;
1089
+ const POLL_MS = 300;
1090
+ const start = Date.now();
1091
+ while (Date.now() - start < MAX_WAIT_MS) {
1092
+ if (signal.aborted)
1093
+ return;
1094
+ try {
1095
+ const pane = await this.terminal.capture();
1096
+ if (pane.includes("INSERT") || pane.includes("NORMAL")) {
1097
+ return;
1098
+ }
1099
+ } catch {}
1100
+ await new Promise((r) => setTimeout(r, POLL_MS));
1101
+ }
1102
+ }
1103
+ async stop() {
1104
+ this.abortController?.abort();
1105
+ if (this.terminal) {
1106
+ await this.terminal.stop();
1107
+ }
1108
+ }
1109
+ async detach() {
1110
+ this.tailer?.close();
1111
+ this.tailer = null;
1112
+ this.eventIterable = null;
1113
+ if (this.terminal) {
1114
+ await this.terminal.detach();
1115
+ }
1116
+ this.terminal = null;
1117
+ }
1118
+ isProcessAlive() {
1119
+ return this.terminal !== null && this.terminal.alive();
1120
+ }
1121
+ static async hasLiveProcess(tmuxSessionName, claudeSessionId) {
1122
+ const { Session: Session2 } = await Promise.resolve().then(() => (init_tmuxctl(), exports_tmuxctl));
1123
+ const found = await Session2.find(tmuxSessionName, claudeSessionId);
1124
+ return found !== null;
1125
+ }
1126
+ static async stopLiveProcess(tmuxSessionName, claudeSessionId) {
1127
+ const { Session: Session2 } = await Promise.resolve().then(() => (init_tmuxctl(), exports_tmuxctl));
1128
+ const found = await Session2.find(tmuxSessionName, claudeSessionId);
1129
+ if (!found)
1130
+ return false;
1131
+ await found.kill();
1132
+ return true;
1133
+ }
1134
+ async send(input) {
1135
+ if (!this.terminal)
1136
+ throw new Error("claude: not started");
1137
+ await this.terminal.send(input);
1138
+ }
1139
+ async interrupt() {
1140
+ if (!this.terminal)
1141
+ throw new Error("claude: not started");
1142
+ await this.terminal.interrupt();
1143
+ }
1144
+ async capturePane() {
1145
+ if (!this.terminal)
1146
+ throw new Error("claude: not started");
1147
+ return this.terminal.capture();
1148
+ }
1149
+ async processId() {
1150
+ return this.terminal?.processId();
1151
+ }
1152
+ alive() {
1153
+ return this.terminal !== null && this.terminal.alive();
1154
+ }
1155
+ async* events() {
1156
+ if (this.eventIterable) {
1157
+ yield* this.eventIterable;
1158
+ }
1159
+ }
1160
+ }
1161
+ function parseClaudeArgs(args) {
1162
+ const passthroughArgs = [];
1163
+ let model;
1164
+ let permissionMode;
1165
+ for (let i = 0;i < args.length; i++) {
1166
+ const arg = args[i];
1167
+ if (arg === "--model" && i + 1 < args.length) {
1168
+ model = args[++i];
1169
+ continue;
1170
+ }
1171
+ if (arg === "--permission-mode" && i + 1 < args.length) {
1172
+ permissionMode = args[++i];
1173
+ continue;
1174
+ }
1175
+ passthroughArgs.push(arg);
1176
+ }
1177
+ return { passthroughArgs, model, permissionMode };
1178
+ }
1179
+ // src/claude/inspect.ts
1180
+ import { readFile } from "node:fs/promises";
1181
+ async function hasPendingQuestion(jsonlPath) {
1182
+ let data;
1183
+ try {
1184
+ data = await readFile(jsonlPath, "utf-8");
1185
+ } catch {
1186
+ return false;
1187
+ }
1188
+ const pendingIds = new Set;
1189
+ for (const line of data.split(`
1190
+ `)) {
1191
+ if (!line.trim())
1192
+ continue;
1193
+ let entry;
1194
+ try {
1195
+ entry = JSON.parse(line);
1196
+ } catch {
1197
+ continue;
1198
+ }
1199
+ if (!entry.message || !Array.isArray(entry.message.content))
1200
+ continue;
1201
+ const blocks = entry.message.content;
1202
+ for (const block of blocks) {
1203
+ if (block.type === "tool_use" && block.name === "AskUserQuestion" && block.id) {
1204
+ pendingIds.add(block.id);
1205
+ }
1206
+ if (block.type === "tool_result" && block.tool_use_id) {
1207
+ pendingIds.delete(block.tool_use_id);
1208
+ }
1209
+ }
1210
+ }
1211
+ return pendingIds.size > 0;
1212
+ }
1213
+ function parseNativePermissionPrompt(paneText) {
1214
+ const questionMatch = paneText.match(/(?:^|\n)\s*(Do you want to [^\n]+\?)/);
1215
+ if (!questionMatch)
1216
+ return null;
1217
+ const afterQuestion = paneText.slice(paneText.indexOf(questionMatch[1]) + questionMatch[1].length);
1218
+ const optionRegex = /(?:❯\s*)?(\d+)\.\s+(.+)/g;
1219
+ const options = [];
1220
+ let match;
1221
+ while ((match = optionRegex.exec(afterQuestion)) !== null) {
1222
+ options.push({ number: match[1], label: match[2].trim() });
1223
+ }
1224
+ if (options.length === 0)
1225
+ return null;
1226
+ return { question: questionMatch[1], options };
1227
+ }
1228
+ function hasPendingNativePrompt(paneText) {
1229
+ return parseNativePermissionPrompt(paneText) !== null;
1230
+ }
1231
+ // src/session-registry.ts
1232
+ import { homedir as homedir2 } from "node:os";
1233
+ import { join as join3 } from "node:path";
1234
+ import {
1235
+ mkdir,
1236
+ readdir,
1237
+ readFile as readFile2,
1238
+ rename,
1239
+ rm,
1240
+ writeFile
1241
+ } from "node:fs/promises";
1242
+ function registryRoot() {
1243
+ return process.env.CLAUDRABAND_HOME || join3(homedir2(), ".claudraband");
1244
+ }
1245
+ function sessionsDir() {
1246
+ return join3(registryRoot(), "sessions");
1247
+ }
1248
+ function knownSessionsDir() {
1249
+ return join3(registryRoot(), "known-sessions");
1250
+ }
1251
+ function sessionFilePath(sessionId) {
1252
+ return join3(sessionsDir(), `${sessionId}.json`);
1253
+ }
1254
+ function knownSessionFilePath(sessionId) {
1255
+ return join3(knownSessionsDir(), `${sessionId}.json`);
1256
+ }
1257
+ async function ensureSessionRegistry() {
1258
+ await mkdir(sessionsDir(), { recursive: true });
1259
+ await mkdir(knownSessionsDir(), { recursive: true });
1260
+ }
1261
+ async function readSessionRecord(sessionId) {
1262
+ try {
1263
+ const text = await readFile2(sessionFilePath(sessionId), "utf8");
1264
+ return JSON.parse(text);
1265
+ } catch {
1266
+ return null;
1267
+ }
1268
+ }
1269
+ async function listSessionRecords() {
1270
+ return listRecordDir(sessionsDir());
1271
+ }
1272
+ async function readKnownSessionRecord(sessionId) {
1273
+ try {
1274
+ const text = await readFile2(knownSessionFilePath(sessionId), "utf8");
1275
+ return JSON.parse(text);
1276
+ } catch {
1277
+ return null;
1278
+ }
1279
+ }
1280
+ async function listKnownSessionRecords() {
1281
+ return listRecordDir(knownSessionsDir());
1282
+ }
1283
+ async function listRecordDir(dir) {
1284
+ try {
1285
+ const files = await readdir(dir);
1286
+ const records = await Promise.all(files.filter((file) => file.endsWith(".json")).map(async (file) => {
1287
+ try {
1288
+ const text = await readFile2(join3(dir, file), "utf8");
1289
+ return JSON.parse(text);
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ }));
1294
+ return records.filter((record) => record !== null);
1295
+ } catch {
1296
+ return [];
1297
+ }
1298
+ }
1299
+ async function writeSessionRecord(record) {
1300
+ await ensureSessionRegistry();
1301
+ const targetPath = sessionFilePath(record.sessionId);
1302
+ await writeRecordFile(targetPath, record);
1303
+ }
1304
+ async function writeKnownSessionRecord(record) {
1305
+ await ensureSessionRegistry();
1306
+ const targetPath = knownSessionFilePath(record.sessionId);
1307
+ await writeRecordFile(targetPath, record);
1308
+ }
1309
+ async function writeRecordFile(targetPath, record) {
1310
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
1311
+ await writeFile(tempPath, JSON.stringify(record, null, 2));
1312
+ await rename(tempPath, targetPath);
1313
+ }
1314
+ async function deleteSessionRecord(sessionId) {
1315
+ await rm(sessionFilePath(sessionId), { force: true }).catch(() => {});
1316
+ }
1317
+ function normalizeServerUrl(server) {
1318
+ return server.startsWith("http://") || server.startsWith("https://") ? server.replace(/\/+$/, "") : `http://${server}`.replace(/\/+$/, "");
1319
+ }
1320
+ function isPidAlive(pid) {
1321
+ if (!pid || !Number.isFinite(pid) || pid <= 0) {
1322
+ return false;
1323
+ }
1324
+ try {
1325
+ process.kill(pid, 0);
1326
+ return true;
1327
+ } catch {
1328
+ return false;
1329
+ }
1330
+ }
1331
+
1332
+ // src/index.ts
1333
+ var DEFAULT_PANE_WIDTH = 120;
1334
+ var DEFAULT_PANE_HEIGHT = 40;
1335
+ var IDLE_TIMEOUT_MS = 3000;
1336
+ var SHARED_TMUX_SESSION = "claudraband-working-session";
1337
+ var SHARED_TMUX_SESSION_NAME = "claudraband-working-session";
1338
+ async function hasLiveProcess(sessionId) {
1339
+ return createTerminalBackendDriver({
1340
+ backend: "tmux",
1341
+ tmuxSessionName: SHARED_TMUX_SESSION_NAME
1342
+ }).hasLiveSession(sessionId);
1343
+ }
1344
+ async function closeLiveProcess(sessionId) {
1345
+ return createTerminalBackendDriver({
1346
+ backend: "tmux",
1347
+ tmuxSessionName: SHARED_TMUX_SESSION_NAME
1348
+ }).closeLiveSession(sessionId);
1349
+ }
1350
+ var MODEL_OPTIONS = [
1351
+ { value: "haiku", name: "Haiku", description: "Fast and lightweight" },
1352
+ {
1353
+ value: "sonnet",
1354
+ name: "Sonnet",
1355
+ description: "Balanced speed and intelligence"
1356
+ },
1357
+ { value: "opus", name: "Opus", description: "Most capable" }
1358
+ ];
1359
+ var PERMISSION_MODES = [
1360
+ { id: "default", name: "Default", description: "Ask before tool use" },
1361
+ { id: "plan", name: "Plan", description: "Plan-only mode, no edits" },
1362
+ { id: "auto", name: "Auto", description: "Bypass permission checks" },
1363
+ {
1364
+ id: "acceptEdits",
1365
+ name: "Accept Edits",
1366
+ description: "Auto-accept file edits"
1367
+ },
1368
+ { id: "dontAsk", name: "Don't Ask", description: "Skip all confirmations" },
1369
+ {
1370
+ id: "bypassPermissions",
1371
+ name: "Bypass Permissions",
1372
+ description: "Dangerously Skip Permissions"
1373
+ }
1374
+ ];
1375
+ var TERMINAL_BACKENDS = [
1376
+ {
1377
+ id: "auto",
1378
+ name: "Auto",
1379
+ description: "Prefer tmux, then fall back to headless xterm"
1380
+ },
1381
+ {
1382
+ id: "tmux",
1383
+ name: "tmux",
1384
+ description: "Run Claude Code inside a tmux session"
1385
+ },
1386
+ {
1387
+ id: "xterm",
1388
+ name: "xterm",
1389
+ description: "Run Claude Code in a headless xterm-backed PTY"
1390
+ }
1391
+ ];
1392
+ function makeDefaultLogger() {
1393
+ const noop = () => {};
1394
+ return {
1395
+ info: noop,
1396
+ debug: noop,
1397
+ warn: noop,
1398
+ error: noop
1399
+ };
1400
+ }
1401
+ function normalizeOptions(defaults, options) {
1402
+ const parsedClaudeArgs = parseClaudeArgs(options?.claudeArgs ?? defaults.claudeArgs ?? []);
1403
+ return {
1404
+ cwd: options?.cwd ?? defaults.cwd ?? process.cwd(),
1405
+ claudeArgs: parsedClaudeArgs.passthroughArgs,
1406
+ claudeExecutable: options?.claudeExecutable ?? defaults.claudeExecutable ?? "",
1407
+ model: options?.model ?? parsedClaudeArgs.model ?? defaults.model ?? "sonnet",
1408
+ permissionMode: options?.permissionMode ?? parsedClaudeArgs.permissionMode ?? defaults.permissionMode ?? "default",
1409
+ allowTextResponses: options?.allowTextResponses ?? defaults.allowTextResponses ?? false,
1410
+ terminalBackend: options?.terminalBackend ?? defaults.terminalBackend ?? "auto",
1411
+ paneWidth: options?.paneWidth ?? defaults.paneWidth ?? DEFAULT_PANE_WIDTH,
1412
+ paneHeight: options?.paneHeight ?? defaults.paneHeight ?? DEFAULT_PANE_HEIGHT,
1413
+ logger: options?.logger ?? defaults.logger ?? makeDefaultLogger(),
1414
+ onPermissionRequest: options?.onPermissionRequest ?? defaults.onPermissionRequest,
1415
+ ...options?.sessionOwner ?? defaults.sessionOwner ? { sessionOwner: options?.sessionOwner ?? defaults.sessionOwner } : {}
1416
+ };
1417
+ }
1418
+ function createClaudraband(defaults = {}) {
1419
+ return new ClaudrabandRuntime(defaults);
1420
+ }
1421
+
1422
+ class ClaudrabandRuntime {
1423
+ defaults;
1424
+ constructor(defaults) {
1425
+ this.defaults = defaults;
1426
+ }
1427
+ async startSession(options) {
1428
+ const cfg = normalizeOptions(this.defaults, options);
1429
+ const backend = resolveTerminalBackend(cfg.terminalBackend);
1430
+ const wrapper = new ClaudeWrapper({
1431
+ model: cfg.model,
1432
+ claudeArgs: cfg.claudeArgs,
1433
+ claudeExecutable: cfg.claudeExecutable || undefined,
1434
+ permissionMode: cfg.permissionMode,
1435
+ terminalBackend: cfg.terminalBackend,
1436
+ workingDir: cfg.cwd,
1437
+ tmuxSession: SHARED_TMUX_SESSION,
1438
+ paneWidth: cfg.paneWidth,
1439
+ paneHeight: cfg.paneHeight
1440
+ });
1441
+ const lifetime = new AbortController;
1442
+ await wrapper.start(lifetime.signal);
1443
+ const session = new ClaudrabandSessionImpl(wrapper, wrapper.claudeSessionId, cfg.cwd, backend, cfg.model, cfg.permissionMode, cfg.allowTextResponses, cfg.logger, cfg.onPermissionRequest, lifetime, cfg.sessionOwner);
1444
+ await session.syncSessionRecord();
1445
+ return session;
1446
+ }
1447
+ async resumeSession(sessionId, options) {
1448
+ const cfg = normalizeOptions(this.defaults, options);
1449
+ const backend = resolveTerminalBackend(cfg.terminalBackend);
1450
+ const wrapper = new ClaudeWrapper({
1451
+ model: cfg.model,
1452
+ claudeArgs: cfg.claudeArgs,
1453
+ claudeExecutable: cfg.claudeExecutable || undefined,
1454
+ permissionMode: cfg.permissionMode,
1455
+ terminalBackend: cfg.terminalBackend,
1456
+ workingDir: cfg.cwd,
1457
+ tmuxSession: SHARED_TMUX_SESSION,
1458
+ paneWidth: cfg.paneWidth,
1459
+ paneHeight: cfg.paneHeight
1460
+ });
1461
+ const lifetime = new AbortController;
1462
+ await wrapper.startResume(sessionId, lifetime.signal);
1463
+ const session = new ClaudrabandSessionImpl(wrapper, sessionId, cfg.cwd, backend, cfg.model, cfg.permissionMode, cfg.allowTextResponses, cfg.logger, cfg.onPermissionRequest, lifetime, cfg.sessionOwner);
1464
+ await session.syncSessionRecord();
1465
+ return session;
1466
+ }
1467
+ listSessions(cwd) {
1468
+ return discoverSessions(cwd);
1469
+ }
1470
+ inspectSession(sessionId, cwd) {
1471
+ return inspectSession(sessionId, cwd);
1472
+ }
1473
+ closeSession(sessionId) {
1474
+ return closeSessionByRecord(sessionId);
1475
+ }
1476
+ async replaySession(sessionId, cwd) {
1477
+ let data;
1478
+ try {
1479
+ data = await readFile3(sessionPath(cwd, sessionId), "utf-8");
1480
+ } catch {
1481
+ return [];
1482
+ }
1483
+ const events = [];
1484
+ for (const line of data.split(`
1485
+ `)) {
1486
+ if (!line.trim())
1487
+ continue;
1488
+ events.push(...parseLineEvents(line));
1489
+ }
1490
+ return events;
1491
+ }
1492
+ }
1493
+
1494
+ class ClaudrabandSessionImpl {
1495
+ sessionId;
1496
+ cwd;
1497
+ backend;
1498
+ wrapper;
1499
+ logger;
1500
+ onPermissionRequest;
1501
+ lifetime;
1502
+ promptAbortController = null;
1503
+ subscribers = new Set;
1504
+ stopped = false;
1505
+ pumpDone = false;
1506
+ pumpError = null;
1507
+ activePrompt = null;
1508
+ pumpPromise;
1509
+ _model;
1510
+ _permissionMode;
1511
+ allowTextResponses;
1512
+ sessionOwner;
1513
+ constructor(wrapper, sessionId, cwd, backend, model, permissionMode, allowTextResponses, logger, onPermissionRequest, lifetime, sessionOwner) {
1514
+ this.sessionId = sessionId;
1515
+ this.cwd = cwd;
1516
+ this.backend = backend;
1517
+ this.wrapper = wrapper;
1518
+ this._model = model;
1519
+ this._permissionMode = permissionMode;
1520
+ this.allowTextResponses = allowTextResponses;
1521
+ this.logger = logger;
1522
+ this.onPermissionRequest = onPermissionRequest;
1523
+ this.lifetime = lifetime;
1524
+ this.sessionOwner = sessionOwner;
1525
+ this.pumpPromise = this.pumpEvents();
1526
+ }
1527
+ get model() {
1528
+ return this._model;
1529
+ }
1530
+ get permissionMode() {
1531
+ return this._permissionMode;
1532
+ }
1533
+ async prompt(text) {
1534
+ if (this.promptAbortController) {
1535
+ this.promptAbortController.abort();
1536
+ }
1537
+ const controller = new AbortController;
1538
+ this.promptAbortController = controller;
1539
+ if (!text) {
1540
+ controller.abort();
1541
+ this.promptAbortController = null;
1542
+ return { stopReason: "end_turn" };
1543
+ }
1544
+ const prompt = this.newPromptWaiter(text);
1545
+ this.activePrompt = prompt;
1546
+ this.logger.info("prompt received", "sid", this.sessionId, "length", text.length);
1547
+ await this.wrapper.send(text);
1548
+ try {
1549
+ const stopReason = await this.waitForPromptCompletion(prompt, controller.signal);
1550
+ this.logger.info("prompt completed", "sid", this.sessionId, "stop_reason", stopReason);
1551
+ return { stopReason };
1552
+ } finally {
1553
+ if (this.activePrompt === prompt) {
1554
+ this.activePrompt = null;
1555
+ }
1556
+ if (this.promptAbortController === controller) {
1557
+ this.promptAbortController = null;
1558
+ }
1559
+ }
1560
+ }
1561
+ async awaitTurn() {
1562
+ if (this.promptAbortController) {
1563
+ this.promptAbortController.abort();
1564
+ }
1565
+ const controller = new AbortController;
1566
+ this.promptAbortController = controller;
1567
+ const prompt = this.newPromptWaiter("");
1568
+ prompt.matchedUserEcho = true;
1569
+ this.activePrompt = prompt;
1570
+ this.logger.info("awaitTurn", "sid", this.sessionId);
1571
+ try {
1572
+ const stopReason = await this.waitForPromptCompletion(prompt, controller.signal);
1573
+ this.logger.info("awaitTurn completed", "sid", this.sessionId, "stop_reason", stopReason);
1574
+ return { stopReason };
1575
+ } finally {
1576
+ if (this.activePrompt === prompt) {
1577
+ this.activePrompt = null;
1578
+ }
1579
+ if (this.promptAbortController === controller) {
1580
+ this.promptAbortController = null;
1581
+ }
1582
+ }
1583
+ }
1584
+ async sendAndAwaitTurn(text) {
1585
+ if (this.promptAbortController) {
1586
+ this.promptAbortController.abort();
1587
+ }
1588
+ const controller = new AbortController;
1589
+ this.promptAbortController = controller;
1590
+ const prompt = this.newPromptWaiter("");
1591
+ prompt.matchedUserEcho = true;
1592
+ this.activePrompt = prompt;
1593
+ this.logger.info("sendAndAwaitTurn", "sid", this.sessionId, "length", text.length);
1594
+ await this.wrapper.send(text);
1595
+ try {
1596
+ const stopReason = await this.waitForPromptCompletion(prompt, controller.signal);
1597
+ this.logger.info("sendAndAwaitTurn completed", "sid", this.sessionId, "stop_reason", stopReason);
1598
+ return { stopReason };
1599
+ } finally {
1600
+ if (this.activePrompt === prompt) {
1601
+ this.activePrompt = null;
1602
+ }
1603
+ if (this.promptAbortController === controller) {
1604
+ this.promptAbortController = null;
1605
+ }
1606
+ }
1607
+ }
1608
+ send(text) {
1609
+ return this.wrapper.send(text);
1610
+ }
1611
+ async interrupt() {
1612
+ this.promptAbortController?.abort();
1613
+ await this.wrapper.interrupt();
1614
+ }
1615
+ async stop() {
1616
+ this.stopped = true;
1617
+ this.promptAbortController?.abort();
1618
+ this.lifetime.abort();
1619
+ await this.wrapper.stop().catch(() => {});
1620
+ await this.pumpPromise.catch(() => {});
1621
+ this.closeSubscribers();
1622
+ await this.syncSessionRecord(false);
1623
+ }
1624
+ async detach() {
1625
+ this.stopped = true;
1626
+ this.promptAbortController?.abort();
1627
+ await this.wrapper.detach();
1628
+ await this.pumpPromise.catch(() => {});
1629
+ this.closeSubscribers();
1630
+ await this.syncSessionRecord();
1631
+ }
1632
+ isProcessAlive() {
1633
+ return this.wrapper.isProcessAlive();
1634
+ }
1635
+ capturePane() {
1636
+ return this.wrapper.capturePane();
1637
+ }
1638
+ async syncSessionRecord(alive = this.isProcessAlive()) {
1639
+ const existing = await readSessionRecord(this.sessionId);
1640
+ const known = await readKnownSessionRecord(this.sessionId);
1641
+ const transcriptPath = sessionPath(this.cwd, this.sessionId);
1642
+ const now = new Date().toISOString();
1643
+ const title = existing?.title ?? known?.title ?? await extractSessionTitle(transcriptPath) ?? undefined;
1644
+ await writeKnownSessionRecord({
1645
+ version: 1,
1646
+ sessionId: this.sessionId,
1647
+ cwd: this.cwd,
1648
+ backend: this.backend,
1649
+ title,
1650
+ createdAt: existing?.createdAt ?? known?.createdAt ?? now,
1651
+ updatedAt: now,
1652
+ transcriptPath
1653
+ });
1654
+ if (!alive) {
1655
+ await deleteSessionRecord(this.sessionId);
1656
+ return;
1657
+ }
1658
+ const owner = await this.resolveSessionOwner();
1659
+ await writeSessionRecord({
1660
+ version: 1,
1661
+ sessionId: this.sessionId,
1662
+ cwd: this.cwd,
1663
+ backend: this.backend,
1664
+ title,
1665
+ createdAt: existing?.createdAt ?? now,
1666
+ updatedAt: now,
1667
+ lastKnownAlive: alive,
1668
+ reattachable: alive && this.supportsReconnect(owner),
1669
+ transcriptPath,
1670
+ owner
1671
+ });
1672
+ }
1673
+ async resolveSessionOwner() {
1674
+ if (this.sessionOwner?.kind === "daemon") {
1675
+ return this.sessionOwner;
1676
+ }
1677
+ return {
1678
+ kind: "local",
1679
+ pid: await this.wrapper.processId().catch(() => {
1680
+ return;
1681
+ })
1682
+ };
1683
+ }
1684
+ supportsReconnect(owner) {
1685
+ return owner.kind === "daemon" || this.backend === "tmux";
1686
+ }
1687
+ async hasPendingInput() {
1688
+ const jsonlPath = sessionPath(this.cwd, this.sessionId);
1689
+ const pendingQuestion = await hasPendingQuestion(jsonlPath);
1690
+ const pendingNativePrompt = hasPendingNativePrompt(await this.wrapper.capturePane().catch(() => ""));
1691
+ return {
1692
+ pending: pendingQuestion || pendingNativePrompt,
1693
+ source: pendingQuestion || pendingNativePrompt ? "terminal" : "none"
1694
+ };
1695
+ }
1696
+ async setModel(model) {
1697
+ this._model = model;
1698
+ this.wrapper.setModel(model);
1699
+ await this.wrapper.send(`/model ${model}`);
1700
+ }
1701
+ async setPermissionMode(mode) {
1702
+ this._permissionMode = mode;
1703
+ this.wrapper.setPermissionMode(mode);
1704
+ await this.wrapper.restart();
1705
+ await this.syncSessionRecord();
1706
+ }
1707
+ async flushEvents() {
1708
+ while (true) {
1709
+ let pending = false;
1710
+ for (const subscriber of this.subscribers) {
1711
+ if (subscriber.queue.length > 0 || subscriber.inFlight > 0) {
1712
+ pending = true;
1713
+ break;
1714
+ }
1715
+ }
1716
+ if (!pending) {
1717
+ return;
1718
+ }
1719
+ await new Promise((resolve) => setTimeout(resolve, 0));
1720
+ }
1721
+ }
1722
+ async* events() {
1723
+ const subscriber = {
1724
+ queue: [],
1725
+ resolvers: [],
1726
+ closed: false,
1727
+ inFlight: 0
1728
+ };
1729
+ this.subscribers.add(subscriber);
1730
+ try {
1731
+ while (true) {
1732
+ if (subscriber.queue.length > 0) {
1733
+ const event = subscriber.queue.shift();
1734
+ subscriber.inFlight++;
1735
+ yield event;
1736
+ subscriber.inFlight = Math.max(0, subscriber.inFlight - 1);
1737
+ continue;
1738
+ }
1739
+ if (subscriber.closed)
1740
+ return;
1741
+ const result = await new Promise((resolve) => {
1742
+ subscriber.resolvers.push(resolve);
1743
+ });
1744
+ if (result.done)
1745
+ return;
1746
+ yield result.value;
1747
+ subscriber.inFlight = Math.max(0, subscriber.inFlight - 1);
1748
+ }
1749
+ } finally {
1750
+ subscriber.closed = true;
1751
+ this.subscribers.delete(subscriber);
1752
+ }
1753
+ }
1754
+ emit(ev) {
1755
+ for (const subscriber of this.subscribers) {
1756
+ if (subscriber.closed)
1757
+ continue;
1758
+ const resolver = subscriber.resolvers.shift();
1759
+ if (resolver) {
1760
+ subscriber.inFlight++;
1761
+ resolver({ value: ev, done: false });
1762
+ } else {
1763
+ subscriber.queue.push(ev);
1764
+ }
1765
+ }
1766
+ }
1767
+ newPromptWaiter(text) {
1768
+ return {
1769
+ text,
1770
+ matchedUserEcho: false,
1771
+ pendingTools: 0,
1772
+ gotResponse: false,
1773
+ turnEnded: false,
1774
+ inputDeferred: false,
1775
+ lastPendingTool: null,
1776
+ consecutiveIdles: 0,
1777
+ waiters: new Set
1778
+ };
1779
+ }
1780
+ notifyPrompt(prompt) {
1781
+ if (!prompt)
1782
+ return;
1783
+ for (const wake of prompt.waiters) {
1784
+ wake();
1785
+ }
1786
+ prompt.waiters.clear();
1787
+ }
1788
+ async pumpEvents() {
1789
+ try {
1790
+ for await (const ev of this.wrapper.events()) {
1791
+ this.emit(ev);
1792
+ await this.handlePromptEvent(ev);
1793
+ }
1794
+ } catch (err) {
1795
+ this.pumpError = err;
1796
+ this.logger.error("session event pump failed", "sid", this.sessionId, err);
1797
+ } finally {
1798
+ this.pumpDone = true;
1799
+ this.notifyPrompt(this.activePrompt);
1800
+ this.closeSubscribers();
1801
+ }
1802
+ }
1803
+ closeSubscribers() {
1804
+ for (const subscriber of this.subscribers) {
1805
+ subscriber.closed = true;
1806
+ for (const resolver of subscriber.resolvers) {
1807
+ resolver({ value: undefined, done: true });
1808
+ }
1809
+ subscriber.resolvers = [];
1810
+ }
1811
+ this.subscribers.clear();
1812
+ }
1813
+ async handlePromptEvent(ev) {
1814
+ const prompt = this.activePrompt;
1815
+ if (!prompt)
1816
+ return;
1817
+ if (!prompt.matchedUserEcho) {
1818
+ if (ev.kind === "user_message" /* UserMessage */ && ev.text === prompt.text) {
1819
+ prompt.matchedUserEcho = true;
1820
+ this.notifyPrompt(prompt);
1821
+ }
1822
+ return;
1823
+ }
1824
+ switch (ev.kind) {
1825
+ case "assistant_text" /* AssistantText */:
1826
+ prompt.gotResponse = true;
1827
+ break;
1828
+ case "tool_call" /* ToolCall */:
1829
+ prompt.pendingTools++;
1830
+ if (ev.toolName === "AskUserQuestion") {
1831
+ prompt.inputDeferred = await this.handleUserQuestion(ev);
1832
+ prompt.pendingTools = Math.max(0, prompt.pendingTools - 1);
1833
+ prompt.gotResponse = true;
1834
+ } else {
1835
+ prompt.lastPendingTool = {
1836
+ name: ev.toolName,
1837
+ id: ev.toolID,
1838
+ input: ev.toolInput
1839
+ };
1840
+ }
1841
+ break;
1842
+ case "tool_result" /* ToolResult */:
1843
+ prompt.pendingTools = Math.max(0, prompt.pendingTools - 1);
1844
+ prompt.lastPendingTool = null;
1845
+ prompt.gotResponse = true;
1846
+ break;
1847
+ case "turn_end" /* TurnEnd */:
1848
+ prompt.gotResponse = true;
1849
+ prompt.turnEnded = true;
1850
+ break;
1851
+ default:
1852
+ break;
1853
+ }
1854
+ this.notifyPrompt(prompt);
1855
+ }
1856
+ waitForPromptUpdate(prompt, signal, timeoutMs) {
1857
+ return new Promise((resolve) => {
1858
+ if (signal.aborted) {
1859
+ resolve("abort");
1860
+ return;
1861
+ }
1862
+ if (this.pumpDone) {
1863
+ resolve("done");
1864
+ return;
1865
+ }
1866
+ let settled = false;
1867
+ let timer;
1868
+ const finish = (result) => {
1869
+ if (settled)
1870
+ return;
1871
+ settled = true;
1872
+ prompt.waiters.delete(onChange);
1873
+ signal.removeEventListener("abort", onAbort);
1874
+ if (timer)
1875
+ clearTimeout(timer);
1876
+ resolve(result);
1877
+ };
1878
+ const onChange = () => finish("changed");
1879
+ const onAbort = () => finish("abort");
1880
+ prompt.waiters.add(onChange);
1881
+ signal.addEventListener("abort", onAbort, { once: true });
1882
+ if (timeoutMs != null) {
1883
+ timer = setTimeout(() => finish("timeout"), timeoutMs);
1884
+ }
1885
+ if (this.pumpDone) {
1886
+ finish("done");
1887
+ }
1888
+ });
1889
+ }
1890
+ async waitForPromptCompletion(prompt, signal) {
1891
+ while (true) {
1892
+ if (signal.aborted)
1893
+ return "cancelled";
1894
+ if (this.stopped)
1895
+ return "cancelled";
1896
+ if (this.pumpDone) {
1897
+ if (this.pumpError) {
1898
+ this.logger.warn("prompt ending after pump shutdown", "sid", this.sessionId);
1899
+ }
1900
+ return "end_turn";
1901
+ }
1902
+ if (prompt.inputDeferred) {
1903
+ return "end_turn";
1904
+ }
1905
+ if (prompt.turnEnded && prompt.pendingTools <= 0) {
1906
+ return "end_turn";
1907
+ }
1908
+ const result = await this.waitForPromptUpdate(prompt, signal, prompt.matchedUserEcho ? IDLE_TIMEOUT_MS : undefined);
1909
+ if (result === "abort")
1910
+ return "cancelled";
1911
+ if (result === "done")
1912
+ return "end_turn";
1913
+ if (result === "changed") {
1914
+ prompt.consecutiveIdles = 0;
1915
+ continue;
1916
+ }
1917
+ if (result === "timeout") {
1918
+ prompt.consecutiveIdles++;
1919
+ if (!prompt.matchedUserEcho) {
1920
+ continue;
1921
+ }
1922
+ if (prompt.inputDeferred) {
1923
+ return "end_turn";
1924
+ }
1925
+ if (prompt.turnEnded && prompt.pendingTools <= 0) {
1926
+ return "end_turn";
1927
+ }
1928
+ if (prompt.pendingTools > 0) {
1929
+ const handled = await this.pollNativePermission(prompt.lastPendingTool);
1930
+ if (handled) {
1931
+ prompt.pendingTools = Math.max(0, prompt.pendingTools - 1);
1932
+ prompt.gotResponse = true;
1933
+ prompt.lastPendingTool = null;
1934
+ this.notifyPrompt(prompt);
1935
+ }
1936
+ continue;
1937
+ }
1938
+ if (prompt.gotResponse && prompt.consecutiveIdles >= 2) {
1939
+ return "end_turn";
1940
+ }
1941
+ continue;
1942
+ }
1943
+ }
1944
+ }
1945
+ async pollNativePermission(pendingTool) {
1946
+ let paneText;
1947
+ try {
1948
+ paneText = await this.wrapper.capturePane();
1949
+ } catch {
1950
+ return false;
1951
+ }
1952
+ const prompt = parseNativePermissionPrompt(paneText);
1953
+ if (!prompt)
1954
+ return false;
1955
+ const decision = await this.resolvePermission({
1956
+ source: "native_prompt",
1957
+ sessionId: this.sessionId,
1958
+ toolCallId: pendingTool?.id ?? `native-perm-${Date.now()}`,
1959
+ title: pendingTool ? `${pendingTool.name}: ${prompt.question}` : prompt.question,
1960
+ kind: pendingTool ? mapToolKind(pendingTool.name) : "other",
1961
+ content: [
1962
+ ...pendingTool ? (() => {
1963
+ const detail = formatToolDetail(pendingTool.name, pendingTool.input);
1964
+ return detail ? [{ type: "text", text: detail }] : [];
1965
+ })() : [],
1966
+ { type: "text", text: prompt.question }
1967
+ ],
1968
+ options: prompt.options.map((opt) => ({
1969
+ kind: opt.label.toLowerCase().startsWith("no") || opt.label.toLowerCase().startsWith("reject") ? "reject_once" : "allow_once",
1970
+ optionId: opt.number,
1971
+ name: opt.label
1972
+ }))
1973
+ });
1974
+ if (decision.outcome === "deferred") {
1975
+ return false;
1976
+ }
1977
+ if (decision.outcome === "cancelled") {
1978
+ await this.wrapper.interrupt();
1979
+ return true;
1980
+ }
1981
+ if (decision.outcome === "text") {
1982
+ await this.wrapper.send(decision.text);
1983
+ return true;
1984
+ }
1985
+ await this.wrapper.send(decision.optionId);
1986
+ return true;
1987
+ }
1988
+ async handleUserQuestion(ev) {
1989
+ const parsed = parseAskUserQuestion(ev.toolInput);
1990
+ if (!parsed) {
1991
+ this.logger.warn("AskUserQuestion parse failed", "sid", this.sessionId);
1992
+ await this.wrapper.send("1");
1993
+ return false;
1994
+ }
1995
+ for (const question of parsed.questions) {
1996
+ const decision = await this.resolvePermission({
1997
+ source: "ask_user_question",
1998
+ sessionId: this.sessionId,
1999
+ toolCallId: ev.toolID,
2000
+ title: question.header || "Claude has a question",
2001
+ kind: "other",
2002
+ content: [{ type: "text", text: question.question }],
2003
+ options: buildAskUserQuestionOptions(question, this.allowTextResponses)
2004
+ });
2005
+ if (decision.outcome === "deferred") {
2006
+ return true;
2007
+ }
2008
+ if (decision.outcome === "cancelled") {
2009
+ await this.wrapper.interrupt();
2010
+ return false;
2011
+ }
2012
+ if (decision.outcome === "text") {
2013
+ await this.wrapper.send(decision.text);
2014
+ return false;
2015
+ }
2016
+ if (decision.optionId === "0") {
2017
+ await this.wrapper.interrupt();
2018
+ return false;
2019
+ }
2020
+ await this.wrapper.send(decision.optionId);
2021
+ }
2022
+ return false;
2023
+ }
2024
+ async resolvePermission(request) {
2025
+ if (!this.onPermissionRequest) {
2026
+ return { outcome: "cancelled" };
2027
+ }
2028
+ try {
2029
+ return await this.onPermissionRequest(request);
2030
+ } catch (err) {
2031
+ this.logger.error("permission handler failed", err);
2032
+ return { outcome: "cancelled" };
2033
+ }
2034
+ }
2035
+ }
2036
+ var __test = {
2037
+ buildAskUserQuestionOptions,
2038
+ createSession(wrapper, options = {}) {
2039
+ return new ClaudrabandSessionImpl(wrapper, options.sessionId ?? "test-session", options.cwd ?? "/tmp", options.backend ?? "tmux", options.model ?? "sonnet", options.permissionMode ?? "default", options.allowTextResponses ?? false, options.logger ?? makeDefaultLogger(), options.onPermissionRequest, options.lifetime ?? new AbortController);
2040
+ }
2041
+ };
2042
+ function buildAskUserQuestionOptions(question, allowTextResponses = false) {
2043
+ return [
2044
+ ...question.options.map((opt, i) => ({
2045
+ kind: "allow_once",
2046
+ optionId: String(i + 1),
2047
+ name: opt.label + (opt.description ? ` — ${opt.description}` : "")
2048
+ })),
2049
+ ...allowTextResponses ? [{
2050
+ kind: "allow_once",
2051
+ optionId: String(question.options.length + 1),
2052
+ name: "Type a response",
2053
+ textInput: true
2054
+ }] : [],
2055
+ {
2056
+ kind: "reject_once",
2057
+ optionId: "0",
2058
+ name: "Cancel"
2059
+ }
2060
+ ];
2061
+ }
2062
+ async function discoverSessions(cwdFilter) {
2063
+ await reconcileLiveLocalSessions(cwdFilter);
2064
+ const live = (await refreshSessionRecords(await listSessionRecords())).filter((record) => !cwdFilter || record.cwd === cwdFilter).map(sessionRecordToSummary);
2065
+ const liveKeys = new Set(live.map((session) => `${session.sessionId} ${session.cwd}`));
2066
+ const history = (await discoverHistoricalSessions(cwdFilter)).filter((session) => !liveKeys.has(`${session.sessionId} ${session.cwd}`));
2067
+ return [...live, ...history].sort((left, right) => Number(right.alive) - Number(left.alive) || (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "") || left.sessionId.localeCompare(right.sessionId));
2068
+ }
2069
+ async function inspectSession(sessionId, cwd) {
2070
+ await reconcileLiveLocalSessions(cwd);
2071
+ const record = await readSessionRecord(sessionId);
2072
+ if (record && (!cwd || record.cwd === cwd)) {
2073
+ const refreshed = await refreshSessionRecords([record]);
2074
+ if (refreshed[0]) {
2075
+ return sessionRecordToSummary(refreshed[0]);
2076
+ }
2077
+ }
2078
+ const history = await discoverHistoricalSessions(cwd);
2079
+ const matches = history.filter((session) => session.sessionId === sessionId);
2080
+ return matches[0] ?? null;
2081
+ }
2082
+ async function closeSessionByRecord(sessionId) {
2083
+ await reconcileLiveLocalSessions();
2084
+ const record = await readSessionRecord(sessionId);
2085
+ if (!record)
2086
+ return false;
2087
+ const [refreshed] = await refreshSessionRecords([record]);
2088
+ if (!refreshed?.lastKnownAlive) {
2089
+ return false;
2090
+ }
2091
+ let closed = false;
2092
+ if (refreshed.owner.kind === "daemon") {
2093
+ closed = await closeDaemonSession(refreshed.owner.serverUrl, refreshed.sessionId);
2094
+ } else if (refreshed.backend === "tmux") {
2095
+ closed = await createTerminalBackendDriver({
2096
+ backend: "tmux",
2097
+ tmuxSessionName: SHARED_TMUX_SESSION
2098
+ }).closeLiveSession(refreshed.sessionId);
2099
+ } else if (isPidAlive(refreshed.owner.pid)) {
2100
+ try {
2101
+ process.kill(refreshed.owner.pid);
2102
+ closed = true;
2103
+ } catch {
2104
+ closed = false;
2105
+ }
2106
+ }
2107
+ if (closed) {
2108
+ await deleteSessionRecord(sessionId);
2109
+ }
2110
+ return closed;
2111
+ }
2112
+ async function reconcileLiveLocalSessions(cwdFilter) {
2113
+ const discovered = await discoverLegacyLiveSessions(createTerminalBackendDriver({
2114
+ backend: "tmux",
2115
+ tmuxSessionName: SHARED_TMUX_SESSION
2116
+ }), cwdFilter);
2117
+ for (const session of discovered) {
2118
+ const existing = await readSessionRecord(session.sessionId);
2119
+ const known = await readKnownSessionRecord(session.sessionId);
2120
+ const transcriptPath = sessionPath(session.cwd, session.sessionId);
2121
+ const title = existing?.title ?? known?.title ?? session.title;
2122
+ const createdAt = existing?.createdAt ?? known?.createdAt ?? session.createdAt;
2123
+ const updatedAt = session.updatedAt ?? existing?.updatedAt ?? known?.updatedAt ?? session.createdAt;
2124
+ await writeKnownSessionRecord({
2125
+ version: 1,
2126
+ sessionId: session.sessionId,
2127
+ cwd: session.cwd,
2128
+ backend: session.backend,
2129
+ title,
2130
+ createdAt,
2131
+ updatedAt,
2132
+ transcriptPath
2133
+ });
2134
+ await writeSessionRecord({
2135
+ version: 1,
2136
+ sessionId: session.sessionId,
2137
+ cwd: session.cwd,
2138
+ backend: session.backend,
2139
+ title,
2140
+ createdAt,
2141
+ updatedAt,
2142
+ lastKnownAlive: true,
2143
+ reattachable: session.reattachable,
2144
+ transcriptPath,
2145
+ owner: {
2146
+ kind: "local",
2147
+ pid: session.owner.kind === "local" ? session.owner.pid : undefined
2148
+ }
2149
+ });
2150
+ }
2151
+ }
2152
+ async function discoverHistoricalSessions(cwdFilter) {
2153
+ const records = (await listKnownSessionRecords()).filter((record) => !cwdFilter || record.cwd === cwdFilter);
2154
+ const results = [];
2155
+ for (const record of records) {
2156
+ const transcriptPath = record.transcriptPath ?? sessionPath(record.cwd, record.sessionId);
2157
+ try {
2158
+ const fileStat = await stat2(transcriptPath);
2159
+ if (fileStat.size < 100)
2160
+ continue;
2161
+ results.push({
2162
+ sessionId: record.sessionId,
2163
+ cwd: record.cwd,
2164
+ title: record.title ?? await extractSessionTitle(transcriptPath) ?? undefined,
2165
+ createdAt: record.createdAt || fileStat.birthtime.toISOString(),
2166
+ updatedAt: fileStat.mtime.toISOString(),
2167
+ backend: record.backend,
2168
+ source: "history",
2169
+ alive: false,
2170
+ reattachable: false,
2171
+ owner: { kind: "local" }
2172
+ });
2173
+ } catch {
2174
+ continue;
2175
+ }
2176
+ }
2177
+ return results;
2178
+ }
2179
+ async function discoverLegacyLiveSessions(backendDriver, cwdFilter) {
2180
+ try {
2181
+ const windows = await backendDriver.listLiveSessions();
2182
+ const results = [];
2183
+ for (const window of windows) {
2184
+ if (!isSessionId(window.sessionId))
2185
+ continue;
2186
+ if (!window.cwd)
2187
+ continue;
2188
+ if (cwdFilter && window.cwd !== cwdFilter)
2189
+ continue;
2190
+ results.push({
2191
+ sessionId: window.sessionId,
2192
+ cwd: window.cwd,
2193
+ createdAt: window.updatedAt ?? new Date().toISOString(),
2194
+ updatedAt: window.updatedAt,
2195
+ backend: backendDriver.backend,
2196
+ source: "live",
2197
+ alive: true,
2198
+ reattachable: backendDriver.supportsLiveReconnect(),
2199
+ owner: {
2200
+ kind: "local",
2201
+ pid: window.pid
2202
+ }
2203
+ });
2204
+ }
2205
+ return results;
2206
+ } catch {
2207
+ return [];
2208
+ }
2209
+ }
2210
+ async function refreshSessionRecords(records) {
2211
+ const tmuxDriver = createTerminalBackendDriver({
2212
+ backend: "tmux",
2213
+ tmuxSessionName: SHARED_TMUX_SESSION
2214
+ });
2215
+ const tmuxWindows = await tmuxDriver.listLiveSessions().catch(() => []);
2216
+ const tmuxById = new Map(tmuxWindows.map((window) => [window.sessionId, window]));
2217
+ const daemonByUrl = new Map;
2218
+ const refreshed = [];
2219
+ for (const record of records) {
2220
+ const metadata = await readRecordMetadata(record);
2221
+ let next = {
2222
+ ...record,
2223
+ transcriptPath: metadata.transcriptPath,
2224
+ title: metadata.title,
2225
+ updatedAt: metadata.updatedAt ?? record.updatedAt
2226
+ };
2227
+ if (record.owner.kind === "daemon") {
2228
+ const serverUrl = normalizeServerUrl(record.owner.serverUrl);
2229
+ if (!daemonByUrl.has(serverUrl)) {
2230
+ daemonByUrl.set(serverUrl, isPidAlive(record.owner.serverPid) ? await fetchDaemonSessionStates(serverUrl) : null);
2231
+ }
2232
+ const daemonSessions = daemonByUrl.get(serverUrl);
2233
+ const alive = daemonSessions?.get(record.sessionId) ?? false;
2234
+ next = {
2235
+ ...next,
2236
+ owner: {
2237
+ ...record.owner,
2238
+ serverUrl
2239
+ },
2240
+ lastKnownAlive: alive,
2241
+ reattachable: alive
2242
+ };
2243
+ } else if (record.backend === "tmux") {
2244
+ const liveWindow = tmuxById.get(record.sessionId);
2245
+ next = {
2246
+ ...next,
2247
+ owner: {
2248
+ kind: "local",
2249
+ pid: liveWindow?.pid ?? record.owner.pid
2250
+ },
2251
+ lastKnownAlive: liveWindow !== undefined,
2252
+ reattachable: liveWindow !== undefined
2253
+ };
2254
+ } else {
2255
+ const alive = isPidAlive(record.owner.pid);
2256
+ next = {
2257
+ ...next,
2258
+ lastKnownAlive: alive,
2259
+ reattachable: false
2260
+ };
2261
+ }
2262
+ if (!next.lastKnownAlive) {
2263
+ await deleteSessionRecord(record.sessionId);
2264
+ continue;
2265
+ }
2266
+ if (!sessionRecordsEqual(record, next)) {
2267
+ await writeSessionRecord(next);
2268
+ }
2269
+ refreshed.push(next);
2270
+ }
2271
+ return refreshed;
2272
+ }
2273
+ async function readRecordMetadata(record) {
2274
+ const transcriptPath = record.transcriptPath ?? sessionPath(record.cwd, record.sessionId);
2275
+ try {
2276
+ const transcriptStat = await stat2(transcriptPath);
2277
+ return {
2278
+ transcriptPath,
2279
+ title: record.title ?? await extractSessionTitle(transcriptPath) ?? undefined,
2280
+ updatedAt: transcriptStat.mtime.toISOString()
2281
+ };
2282
+ } catch {
2283
+ return {
2284
+ transcriptPath,
2285
+ title: record.title,
2286
+ updatedAt: record.updatedAt
2287
+ };
2288
+ }
2289
+ }
2290
+ function sessionRecordToSummary(record) {
2291
+ return {
2292
+ sessionId: record.sessionId,
2293
+ cwd: record.cwd,
2294
+ title: record.title,
2295
+ createdAt: record.createdAt,
2296
+ updatedAt: record.updatedAt,
2297
+ backend: record.backend,
2298
+ source: "live",
2299
+ alive: record.lastKnownAlive,
2300
+ reattachable: record.reattachable,
2301
+ owner: record.owner
2302
+ };
2303
+ }
2304
+ function sessionRecordsEqual(left, right) {
2305
+ return JSON.stringify(left) === JSON.stringify(right);
2306
+ }
2307
+ function isSessionId(value) {
2308
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value);
2309
+ }
2310
+ async function fetchDaemonSessionStates(serverUrl) {
2311
+ try {
2312
+ const result = await daemonRequest(serverUrl, "GET", "/sessions");
2313
+ const sessions = new Map;
2314
+ for (const session of result.sessions ?? []) {
2315
+ sessions.set(session.sessionId, session.alive);
2316
+ }
2317
+ return sessions;
2318
+ } catch {
2319
+ return null;
2320
+ }
2321
+ }
2322
+ async function closeDaemonSession(serverUrl, sessionId) {
2323
+ try {
2324
+ await daemonRequest(serverUrl, "DELETE", `/sessions/${sessionId}`);
2325
+ return true;
2326
+ } catch {
2327
+ return false;
2328
+ }
2329
+ }
2330
+ async function daemonRequest(serverUrl, method, path) {
2331
+ const url = new URL(path, normalizeServerUrl(serverUrl));
2332
+ return new Promise((resolve, reject) => {
2333
+ const req = httpRequest({
2334
+ hostname: url.hostname,
2335
+ port: url.port,
2336
+ path: url.pathname,
2337
+ method
2338
+ }, (res) => {
2339
+ const chunks = [];
2340
+ res.on("data", (chunk) => chunks.push(chunk));
2341
+ res.on("end", () => {
2342
+ const text = Buffer.concat(chunks).toString();
2343
+ if ((res.statusCode ?? 500) >= 400) {
2344
+ reject(new Error(text || `request failed with status ${res.statusCode}`));
2345
+ return;
2346
+ }
2347
+ if (!text) {
2348
+ resolve({});
2349
+ return;
2350
+ }
2351
+ try {
2352
+ resolve(JSON.parse(text));
2353
+ } catch {
2354
+ resolve(text);
2355
+ }
2356
+ });
2357
+ });
2358
+ req.on("error", reject);
2359
+ req.end();
2360
+ });
2361
+ }
2362
+ async function extractSessionTitle(filePath) {
2363
+ try {
2364
+ const buf = Buffer.alloc(4096);
2365
+ const fh = await open2(filePath, "r");
2366
+ const { bytesRead } = await fh.read(buf, 0, 4096, 0);
2367
+ await fh.close();
2368
+ for (const line of buf.toString("utf-8", 0, bytesRead).split(`
2369
+ `)) {
2370
+ if (!line.trim())
2371
+ continue;
2372
+ try {
2373
+ const obj = JSON.parse(line);
2374
+ if (obj.type !== "user" || !obj.message?.content)
2375
+ continue;
2376
+ const content = typeof obj.message.content === "string" ? obj.message.content : obj.message.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join(" ");
2377
+ return content.length > 80 ? `${content.slice(0, 77)}...` : content;
2378
+ } catch {
2379
+ continue;
2380
+ }
2381
+ }
2382
+ } catch {
2383
+ return null;
2384
+ }
2385
+ return null;
2386
+ }
2387
+ function mapToolKind(toolName) {
2388
+ switch (toolName) {
2389
+ case "Read":
2390
+ case "ReadFile":
2391
+ case "read_file":
2392
+ return "read";
2393
+ case "Write":
2394
+ case "WriteFile":
2395
+ case "write_to_file":
2396
+ case "Edit":
2397
+ case "EditFile":
2398
+ case "str_replace_editor":
2399
+ case "MultiEdit":
2400
+ case "NotebookEdit":
2401
+ return "edit";
2402
+ case "Bash":
2403
+ case "bash":
2404
+ case "execute_command":
2405
+ return "execute";
2406
+ case "Grep":
2407
+ case "Glob":
2408
+ case "Search":
2409
+ case "grep":
2410
+ case "search":
2411
+ case "find_file":
2412
+ case "list_files":
2413
+ return "search";
2414
+ case "WebFetch":
2415
+ case "WebSearch":
2416
+ case "web_fetch":
2417
+ case "fetch":
2418
+ return "fetch";
2419
+ case "Think":
2420
+ case "think":
2421
+ return "think";
2422
+ default:
2423
+ return "other";
2424
+ }
2425
+ }
2426
+ function formatToolDetail(toolName, toolInput) {
2427
+ let parsed;
2428
+ try {
2429
+ parsed = JSON.parse(toolInput);
2430
+ } catch {
2431
+ return null;
2432
+ }
2433
+ switch (toolName) {
2434
+ case "Write": {
2435
+ const filePath = String(parsed.file_path ?? parsed.path ?? "");
2436
+ const content = String(parsed.content ?? "");
2437
+ return `**${filePath}**
2438
+ \`\`\`
2439
+ ${content}
2440
+ \`\`\``;
2441
+ }
2442
+ case "Edit": {
2443
+ const filePath = String(parsed.file_path ?? parsed.path ?? "");
2444
+ const oldStr = String(parsed.old_string ?? "");
2445
+ const newStr = String(parsed.new_string ?? "");
2446
+ return `**${filePath}**
2447
+ \`\`\`diff
2448
+ - ${oldStr.split(`
2449
+ `).join(`
2450
+ - `)}
2451
+ + ${newStr.split(`
2452
+ `).join(`
2453
+ + `)}
2454
+ \`\`\``;
2455
+ }
2456
+ case "Bash":
2457
+ return `\`\`\`bash
2458
+ ${String(parsed.command ?? "")}
2459
+ \`\`\``;
2460
+ default:
2461
+ return `\`\`\`json
2462
+ ${JSON.stringify(parsed, null, 2)}
2463
+ \`\`\``;
2464
+ }
2465
+ }
2466
+ function parseAskUserQuestion(rawInput) {
2467
+ try {
2468
+ const parsed = JSON.parse(rawInput);
2469
+ if (!parsed.questions || parsed.questions.length === 0)
2470
+ return null;
2471
+ return parsed;
2472
+ } catch {
2473
+ return null;
2474
+ }
2475
+ }
2476
+ export {
2477
+ sessionPath,
2478
+ resolveTerminalBackend,
2479
+ parseClaudeArgs,
2480
+ hasPendingQuestion,
2481
+ hasPendingNativePrompt,
2482
+ hasLiveProcess,
2483
+ createClaudraband,
2484
+ closeLiveProcess,
2485
+ awaitPaneIdle,
2486
+ __test,
2487
+ TERMINAL_BACKENDS,
2488
+ PERMISSION_MODES,
2489
+ MODEL_OPTIONS,
2490
+ EventKind
2491
+ };