tmux-manager 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.mjs ADDED
@@ -0,0 +1,1951 @@
1
+ // src/ensure-tmux.ts
2
+ import { execSync } from "child_process";
3
+ var _checked = false;
4
+ var _version;
5
+ function ensureTmux(log) {
6
+ if (_checked && _version) {
7
+ return _version;
8
+ }
9
+ try {
10
+ const output = execSync("tmux -V", {
11
+ encoding: "utf-8",
12
+ timeout: 5e3,
13
+ stdio: ["pipe", "pipe", "pipe"]
14
+ }).trim();
15
+ const match = output.match(/tmux\s+([\w.\-]+)/);
16
+ _version = match?.[1] ?? output;
17
+ _checked = true;
18
+ log?.(`tmux ${_version} found`);
19
+ return _version;
20
+ } catch {
21
+ throw new Error(
22
+ "tmux is required but not found on PATH. Install it with: brew install tmux (macOS) or apt install tmux (Linux)"
23
+ );
24
+ }
25
+ }
26
+ function resetTmuxCheck() {
27
+ _checked = false;
28
+ _version = void 0;
29
+ }
30
+
31
+ // src/tmux-manager.ts
32
+ import { EventEmitter as EventEmitter2 } from "events";
33
+
34
+ // src/adapters/adapter-registry.ts
35
+ import { AdapterRegistry } from "adapter-types";
36
+
37
+ // src/tmux-session.ts
38
+ import { EventEmitter } from "events";
39
+ import { randomUUID } from "crypto";
40
+
41
+ // src/logger.ts
42
+ var consoleLogger = {
43
+ debug: (...args) => {
44
+ if (typeof args[0] === "string") {
45
+ console.debug(args[0], args[1]);
46
+ } else {
47
+ console.debug(args[1], args[0]);
48
+ }
49
+ },
50
+ info: (...args) => {
51
+ if (typeof args[0] === "string") {
52
+ console.info(args[0], args[1]);
53
+ } else {
54
+ console.info(args[1], args[0]);
55
+ }
56
+ },
57
+ warn: (...args) => {
58
+ if (typeof args[0] === "string") {
59
+ console.warn(args[0], args[1]);
60
+ } else {
61
+ console.warn(args[1], args[0]);
62
+ }
63
+ },
64
+ error: (...args) => {
65
+ if (typeof args[0] === "string") {
66
+ console.error(args[0], args[1]);
67
+ } else {
68
+ console.error(args[1], args[0]);
69
+ }
70
+ }
71
+ };
72
+
73
+ // src/tmux-transport.ts
74
+ import { execSync as execSync2 } from "child_process";
75
+ var TMUX_KEY_MAP = {
76
+ "enter": "Enter",
77
+ "return": "Enter",
78
+ "tab": "Tab",
79
+ "escape": "Escape",
80
+ "esc": "Escape",
81
+ "space": "Space",
82
+ "backspace": "BSpace",
83
+ "delete": "DC",
84
+ "insert": "IC",
85
+ "up": "Up",
86
+ "down": "Down",
87
+ "left": "Left",
88
+ "right": "Right",
89
+ "home": "Home",
90
+ "end": "End",
91
+ "pageup": "PageUp",
92
+ "pagedown": "PageDown",
93
+ "f1": "F1",
94
+ "f2": "F2",
95
+ "f3": "F3",
96
+ "f4": "F4",
97
+ "f5": "F5",
98
+ "f6": "F6",
99
+ "f7": "F7",
100
+ "f8": "F8",
101
+ "f9": "F9",
102
+ "f10": "F10",
103
+ "f11": "F11",
104
+ "f12": "F12",
105
+ // Ctrl keys map to tmux C- prefix
106
+ "ctrl+a": "C-a",
107
+ "ctrl+b": "C-b",
108
+ "ctrl+c": "C-c",
109
+ "ctrl+d": "C-d",
110
+ "ctrl+e": "C-e",
111
+ "ctrl+f": "C-f",
112
+ "ctrl+g": "C-g",
113
+ "ctrl+h": "C-h",
114
+ "ctrl+i": "C-i",
115
+ "ctrl+j": "C-j",
116
+ "ctrl+k": "C-k",
117
+ "ctrl+l": "C-l",
118
+ "ctrl+m": "C-m",
119
+ "ctrl+n": "C-n",
120
+ "ctrl+o": "C-o",
121
+ "ctrl+p": "C-p",
122
+ "ctrl+q": "C-q",
123
+ "ctrl+r": "C-r",
124
+ "ctrl+s": "C-s",
125
+ "ctrl+t": "C-t",
126
+ "ctrl+u": "C-u",
127
+ "ctrl+v": "C-v",
128
+ "ctrl+w": "C-w",
129
+ "ctrl+x": "C-x",
130
+ "ctrl+y": "C-y",
131
+ "ctrl+z": "C-z",
132
+ // Shift combinations
133
+ "shift+up": "S-Up",
134
+ "shift+down": "S-Down",
135
+ "shift+left": "S-Left",
136
+ "shift+right": "S-Right",
137
+ "shift+tab": "BTab"
138
+ };
139
+ var TmuxTransport = class {
140
+ pollingTimers = /* @__PURE__ */ new Map();
141
+ lastCapture = /* @__PURE__ */ new Map();
142
+ constructor() {
143
+ }
144
+ /**
145
+ * Spawn a new tmux session running the given command.
146
+ */
147
+ spawn(sessionName, options) {
148
+ ensureTmux();
149
+ const fullCommand = [options.command, ...options.args].join(" ");
150
+ const envExports = Object.entries(options.env).map(([k, v]) => `export ${k}=${this.shellEscape(v)}`).join("; ");
151
+ const shellCommand = envExports ? `${envExports}; exec ${fullCommand}` : `exec ${fullCommand}`;
152
+ execSync2(
153
+ `tmux new-session -d -s ${this.shellEscape(sessionName)} -x ${options.cols} -y ${options.rows} -c ${this.shellEscape(options.cwd)} ${this.shellEscape(shellCommand)}`,
154
+ { stdio: "pipe", timeout: 1e4 }
155
+ );
156
+ this.tmuxExec(`set-option -t ${this.shellEscape(sessionName)} history-limit ${options.historyLimit}`);
157
+ this.tmuxExec(`set-option -t ${this.shellEscape(sessionName)} remain-on-exit on`);
158
+ }
159
+ /**
160
+ * Check if a tmux session exists and is alive.
161
+ */
162
+ isAlive(sessionName) {
163
+ try {
164
+ execSync2(`tmux has-session -t ${this.shellEscape(sessionName)}`, {
165
+ stdio: "pipe",
166
+ timeout: 3e3
167
+ });
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ /**
174
+ * Kill a tmux session.
175
+ */
176
+ kill(sessionName) {
177
+ this.stopOutputStreaming(sessionName);
178
+ try {
179
+ this.tmuxExec(`kill-session -t ${this.shellEscape(sessionName)}`);
180
+ } catch {
181
+ }
182
+ }
183
+ /**
184
+ * Send a signal to the process in the tmux pane.
185
+ */
186
+ signal(sessionName, sig) {
187
+ try {
188
+ const pid = this.getPanePid(sessionName);
189
+ if (pid) {
190
+ const nodeSignal = sig === "SIGKILL" ? "-9" : sig === "SIGTERM" ? "-15" : `-${sig}`;
191
+ execSync2(`kill ${nodeSignal} ${pid}`, { stdio: "pipe", timeout: 3e3 });
192
+ }
193
+ } catch {
194
+ }
195
+ }
196
+ /**
197
+ * Send literal text to the tmux session (no key interpretation).
198
+ */
199
+ sendText(sessionName, text) {
200
+ this.tmuxExec(`send-keys -t ${this.shellEscape(sessionName)} -l ${this.shellEscape(text)}`);
201
+ }
202
+ /**
203
+ * Send a named key to the tmux session.
204
+ * Uses TMUX_KEY_MAP for translation from our key names.
205
+ */
206
+ sendKey(sessionName, key) {
207
+ const tmuxKey = TMUX_KEY_MAP[key.toLowerCase()];
208
+ if (tmuxKey) {
209
+ this.tmuxExec(`send-keys -t ${this.shellEscape(sessionName)} ${tmuxKey}`);
210
+ } else {
211
+ this.tmuxExec(`send-keys -t ${this.shellEscape(sessionName)} -l ${this.shellEscape(key)}`);
212
+ }
213
+ }
214
+ /**
215
+ * Capture the current pane content.
216
+ */
217
+ capturePane(sessionName, options = {}) {
218
+ const flags = ["-p"];
219
+ if (options.ansi) {
220
+ flags.push("-e");
221
+ }
222
+ if (options.scrollback !== void 0) {
223
+ flags.push(`-S -${options.scrollback}`);
224
+ }
225
+ try {
226
+ return execSync2(
227
+ `tmux capture-pane -t ${this.shellEscape(sessionName)} ${flags.join(" ")}`,
228
+ { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
229
+ );
230
+ } catch {
231
+ return "";
232
+ }
233
+ }
234
+ /**
235
+ * Start polling output from a tmux session via capture-pane.
236
+ * Polls at the given interval and calls the callback with new/changed content.
237
+ *
238
+ * Uses capture-pane with ANSI codes (-e) and scrollback for full fidelity.
239
+ * Compares against last capture to detect changes and emit only new data.
240
+ */
241
+ startOutputStreaming(sessionName, callback, pollIntervalMs = 100) {
242
+ this.lastCapture.set(sessionName, "");
243
+ const timer = setInterval(() => {
244
+ try {
245
+ const current = this.capturePane(sessionName, { ansi: true, scrollback: 500 });
246
+ const last = this.lastCapture.get(sessionName) || "";
247
+ if (current !== last) {
248
+ this.lastCapture.set(sessionName, current);
249
+ if (current.startsWith(last)) {
250
+ const newData = current.slice(last.length);
251
+ if (newData) callback(newData);
252
+ } else {
253
+ callback(current);
254
+ }
255
+ }
256
+ } catch {
257
+ }
258
+ }, pollIntervalMs);
259
+ this.pollingTimers.set(sessionName, timer);
260
+ }
261
+ /**
262
+ * Stop polling output from a tmux session.
263
+ */
264
+ stopOutputStreaming(sessionName) {
265
+ const timer = this.pollingTimers.get(sessionName);
266
+ if (timer) {
267
+ clearInterval(timer);
268
+ this.pollingTimers.delete(sessionName);
269
+ }
270
+ this.lastCapture.delete(sessionName);
271
+ }
272
+ /**
273
+ * Get the PID of the process running in the tmux pane.
274
+ */
275
+ getPanePid(sessionName) {
276
+ try {
277
+ const output = execSync2(
278
+ `tmux display-message -t ${this.shellEscape(sessionName)} -p '#{pane_pid}'`,
279
+ { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
280
+ ).trim();
281
+ const pid = parseInt(output, 10);
282
+ return isNaN(pid) ? void 0 : pid;
283
+ } catch {
284
+ return void 0;
285
+ }
286
+ }
287
+ /**
288
+ * Get the pane dimensions.
289
+ */
290
+ getPaneDimensions(sessionName) {
291
+ try {
292
+ const output = execSync2(
293
+ `tmux display-message -t ${this.shellEscape(sessionName)} -p '#{pane_width}x#{pane_height}'`,
294
+ { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
295
+ ).trim();
296
+ const [cols, rows] = output.split("x").map(Number);
297
+ return { cols: cols || 120, rows: rows || 40 };
298
+ } catch {
299
+ return { cols: 120, rows: 40 };
300
+ }
301
+ }
302
+ /**
303
+ * Resize a tmux session window.
304
+ */
305
+ resize(sessionName, cols, rows) {
306
+ try {
307
+ this.tmuxExec(`resize-window -t ${this.shellEscape(sessionName)} -x ${cols} -y ${rows}`);
308
+ } catch {
309
+ }
310
+ }
311
+ /**
312
+ * Check if the pane's process has exited.
313
+ * Uses remain-on-exit to detect dead panes.
314
+ */
315
+ isPaneAlive(sessionName) {
316
+ try {
317
+ const output = execSync2(
318
+ `tmux display-message -t ${this.shellEscape(sessionName)} -p '#{pane_dead}'`,
319
+ { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
320
+ ).trim();
321
+ return output !== "1";
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+ /**
327
+ * Get the exit status of a dead pane.
328
+ */
329
+ getPaneExitStatus(sessionName) {
330
+ try {
331
+ const output = execSync2(
332
+ `tmux display-message -t ${this.shellEscape(sessionName)} -p '#{pane_dead_status}'`,
333
+ { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
334
+ ).trim();
335
+ const code = parseInt(output, 10);
336
+ return isNaN(code) ? void 0 : code;
337
+ } catch {
338
+ return void 0;
339
+ }
340
+ }
341
+ /**
342
+ * List all tmux sessions matching a prefix.
343
+ */
344
+ static listSessions(prefix) {
345
+ try {
346
+ const output = execSync2(
347
+ `tmux list-sessions -F '#{session_name}|#{session_created}|#{session_attached}'`,
348
+ { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
349
+ ).trim();
350
+ if (!output) return [];
351
+ return output.split("\n").map((line) => {
352
+ const [name, created, attached] = line.split("|");
353
+ return { name, created, attached: attached === "1" };
354
+ }).filter((s) => !prefix || s.name.startsWith(prefix));
355
+ } catch {
356
+ return [];
357
+ }
358
+ }
359
+ /**
360
+ * Clean up resources.
361
+ */
362
+ destroy() {
363
+ for (const [, timer] of this.pollingTimers) {
364
+ clearInterval(timer);
365
+ }
366
+ this.pollingTimers.clear();
367
+ this.lastCapture.clear();
368
+ }
369
+ // ─────────────────────────────────────────────────────────────────────────────
370
+ // Private helpers
371
+ // ─────────────────────────────────────────────────────────────────────────────
372
+ tmuxExec(args) {
373
+ execSync2(`tmux ${args}`, { stdio: "pipe", timeout: 5e3 });
374
+ }
375
+ shellEscape(str) {
376
+ return `'${str.replace(/'/g, "'\\''")}'`;
377
+ }
378
+ };
379
+
380
+ // src/tmux-session.ts
381
+ var SPECIAL_KEYS = {
382
+ // Control keys
383
+ "ctrl+a": "",
384
+ "ctrl+b": "",
385
+ "ctrl+c": "",
386
+ "ctrl+d": "",
387
+ "ctrl+e": "",
388
+ "ctrl+f": "",
389
+ "ctrl+g": "\x07",
390
+ "ctrl+h": "\b",
391
+ "ctrl+i": " ",
392
+ "ctrl+j": "\n",
393
+ "ctrl+k": "\v",
394
+ "ctrl+l": "\f",
395
+ "ctrl+m": "\r",
396
+ "ctrl+n": "",
397
+ "ctrl+o": "",
398
+ "ctrl+p": "",
399
+ "ctrl+q": "",
400
+ "ctrl+r": "",
401
+ "ctrl+s": "",
402
+ "ctrl+t": "",
403
+ "ctrl+u": "",
404
+ "ctrl+v": "",
405
+ "ctrl+w": "",
406
+ "ctrl+x": "",
407
+ "ctrl+y": "",
408
+ "ctrl+z": "",
409
+ // Navigation
410
+ "up": "\x1B[A",
411
+ "down": "\x1B[B",
412
+ "right": "\x1B[C",
413
+ "left": "\x1B[D",
414
+ "home": "\x1B[H",
415
+ "end": "\x1B[F",
416
+ "pageup": "\x1B[5~",
417
+ "pagedown": "\x1B[6~",
418
+ // Editing
419
+ "enter": "\r",
420
+ "return": "\r",
421
+ "tab": " ",
422
+ "backspace": "\x7F",
423
+ "delete": "\x1B[3~",
424
+ "insert": "\x1B[2~",
425
+ "escape": "\x1B",
426
+ "esc": "\x1B",
427
+ "space": " ",
428
+ // Function keys
429
+ "f1": "\x1BOP",
430
+ "f2": "\x1BOQ",
431
+ "f3": "\x1BOR",
432
+ "f4": "\x1BOS",
433
+ "f5": "\x1B[15~",
434
+ "f6": "\x1B[17~",
435
+ "f7": "\x1B[18~",
436
+ "f8": "\x1B[19~",
437
+ "f9": "\x1B[20~",
438
+ "f10": "\x1B[21~",
439
+ "f11": "\x1B[23~",
440
+ "f12": "\x1B[24~"
441
+ };
442
+ function generateId() {
443
+ return `tmux-${Date.now()}-${randomUUID().slice(0, 8)}`;
444
+ }
445
+ var TmuxSession = class _TmuxSession extends EventEmitter {
446
+ constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs, transport, sessionPrefix, historyLimit) {
447
+ super();
448
+ this.adapter = adapter;
449
+ this.id = config.id || generateId();
450
+ this.config = { ...config, id: this.id };
451
+ this.logger = logger || consoleLogger;
452
+ this._stallDetectionEnabled = stallDetectionEnabled ?? false;
453
+ this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
454
+ this._stallBackoffMs = this._stallTimeoutMs;
455
+ this.transport = transport || new TmuxTransport();
456
+ this.sessionPrefix = sessionPrefix || "parallax";
457
+ this.historyLimit = historyLimit || 5e4;
458
+ this.tmuxSessionName = `${this.sessionPrefix}-${this.id}`;
459
+ if (config.ruleOverrides) {
460
+ for (const [key, value] of Object.entries(config.ruleOverrides)) {
461
+ if (value === null) {
462
+ this._disabledRulePatterns.add(key);
463
+ } else {
464
+ this._ruleOverrides.set(key, value);
465
+ }
466
+ }
467
+ }
468
+ }
469
+ transport;
470
+ tmuxSessionName;
471
+ outputBuffer = "";
472
+ _status = "pending";
473
+ _startedAt = null;
474
+ _lastActivityAt = null;
475
+ messageCounter = 0;
476
+ logger;
477
+ sessionRules = [];
478
+ _firedOnceRules = /* @__PURE__ */ new Set();
479
+ _lastBlockingPromptHash = null;
480
+ _ruleOverrides = /* @__PURE__ */ new Map();
481
+ _disabledRulePatterns = /* @__PURE__ */ new Set();
482
+ // Stall detection
483
+ _stallTimer = null;
484
+ _stallTimeoutMs;
485
+ _stallDetectionEnabled;
486
+ _lastStallHash = null;
487
+ _stallStartedAt = null;
488
+ _lastContentHash = null;
489
+ _stallBackoffMs = 0;
490
+ static MAX_STALL_BACKOFF_MS = 3e4;
491
+ _stallEmissionCount = 0;
492
+ static MAX_STALL_EMISSIONS = 5;
493
+ // Task completion detection
494
+ _taskCompleteTimer = null;
495
+ _taskCompletePending = false;
496
+ static TASK_COMPLETE_DEBOUNCE_MS = 1500;
497
+ // Ready detection settle delay
498
+ _readySettleTimer = null;
499
+ _readySettlePending = false;
500
+ // Tool running deduplication
501
+ _lastToolRunningName = null;
502
+ // Output buffer cap
503
+ static MAX_OUTPUT_BUFFER = 1e5;
504
+ // Poll-based exit detection
505
+ _exitPollTimer = null;
506
+ // Session prefix for tmux session naming
507
+ sessionPrefix;
508
+ // History limit for tmux scrollback
509
+ historyLimit;
510
+ id;
511
+ config;
512
+ get status() {
513
+ return this._status;
514
+ }
515
+ get pid() {
516
+ return this.transport.getPanePid(this.tmuxSessionName);
517
+ }
518
+ get startedAt() {
519
+ return this._startedAt ?? void 0;
520
+ }
521
+ get lastActivityAt() {
522
+ return this._lastActivityAt ?? void 0;
523
+ }
524
+ /**
525
+ * Get the tmux session name (for reconnection/debugging).
526
+ */
527
+ get tmuxName() {
528
+ return this.tmuxSessionName;
529
+ }
530
+ // ─────────────────────────────────────────────────────────────────────────────
531
+ // Runtime Auto-Response Rules API
532
+ // ─────────────────────────────────────────────────────────────────────────────
533
+ addAutoResponseRule(rule) {
534
+ const existingIndex = this.sessionRules.findIndex(
535
+ (r) => r.pattern.source === rule.pattern.source && r.pattern.flags === rule.pattern.flags
536
+ );
537
+ if (existingIndex >= 0) {
538
+ this.sessionRules[existingIndex] = rule;
539
+ } else {
540
+ this.sessionRules.push(rule);
541
+ }
542
+ }
543
+ removeAutoResponseRule(pattern) {
544
+ const initialLength = this.sessionRules.length;
545
+ this.sessionRules = this.sessionRules.filter(
546
+ (r) => !(r.pattern.source === pattern.source && r.pattern.flags === pattern.flags)
547
+ );
548
+ return this.sessionRules.length < initialLength;
549
+ }
550
+ setAutoResponseRules(rules) {
551
+ this.sessionRules = [...rules];
552
+ }
553
+ getAutoResponseRules() {
554
+ return [...this.sessionRules];
555
+ }
556
+ clearAutoResponseRules() {
557
+ this.sessionRules = [];
558
+ }
559
+ // ─────────────────────────────────────────────────────────────────────────────
560
+ // Stall Detection
561
+ // ─────────────────────────────────────────────────────────────────────────────
562
+ resetStallTimer() {
563
+ if (!this._stallDetectionEnabled || this._status !== "busy" && this._status !== "authenticating") {
564
+ this.clearStallTimer();
565
+ return;
566
+ }
567
+ const stripped = this.stripAnsiForStall(this.outputBuffer).trim();
568
+ const tail = stripped.slice(-500);
569
+ const hash = this.simpleHash(tail);
570
+ if (hash === this._lastContentHash) {
571
+ return;
572
+ }
573
+ this._lastContentHash = hash;
574
+ this._stallEmissionCount = 0;
575
+ if (this._stallTimer) {
576
+ clearTimeout(this._stallTimer);
577
+ this._stallTimer = null;
578
+ }
579
+ this._stallStartedAt = Date.now();
580
+ this._lastStallHash = null;
581
+ this._stallBackoffMs = this._stallTimeoutMs;
582
+ this._stallTimer = setTimeout(() => {
583
+ this.onStallTimerFired();
584
+ }, this._stallTimeoutMs);
585
+ }
586
+ clearStallTimer() {
587
+ if (this._stallTimer) {
588
+ clearTimeout(this._stallTimer);
589
+ this._stallTimer = null;
590
+ }
591
+ this._stallStartedAt = null;
592
+ this._lastContentHash = null;
593
+ this._stallBackoffMs = this._stallTimeoutMs;
594
+ this._stallEmissionCount = 0;
595
+ }
596
+ onStallTimerFired() {
597
+ if (this._status !== "busy" && this._status !== "authenticating") {
598
+ return;
599
+ }
600
+ if (this._status === "busy" && this.adapter.detectTaskComplete?.(this.outputBuffer)) {
601
+ this._status = "ready";
602
+ this._lastBlockingPromptHash = null;
603
+ this.outputBuffer = "";
604
+ this.clearStallTimer();
605
+ this.emit("status_changed", "ready");
606
+ this.emit("task_complete");
607
+ this.logger.info({ sessionId: this.id }, "Task complete (adapter fast-path)");
608
+ return;
609
+ }
610
+ if (this.adapter.detectLoading?.(this.outputBuffer)) {
611
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
612
+ return;
613
+ }
614
+ const toolInfo = this.adapter.detectToolRunning?.(this.outputBuffer);
615
+ if (toolInfo) {
616
+ if (toolInfo.toolName !== this._lastToolRunningName) {
617
+ this._lastToolRunningName = toolInfo.toolName;
618
+ this.emit("tool_running", toolInfo);
619
+ }
620
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
621
+ return;
622
+ }
623
+ if (this._lastToolRunningName) {
624
+ this._lastToolRunningName = null;
625
+ }
626
+ const tail = this.outputBuffer.slice(-500);
627
+ const hash = this.simpleHash(tail);
628
+ if (hash === this._lastStallHash) {
629
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
630
+ return;
631
+ }
632
+ this._lastStallHash = hash;
633
+ this._stallEmissionCount++;
634
+ if (this._stallEmissionCount > _TmuxSession.MAX_STALL_EMISSIONS) {
635
+ this.logger.warn({ sessionId: this.id }, "Max stall emissions reached");
636
+ this.clearStallTimer();
637
+ return;
638
+ }
639
+ const recentRaw = this.outputBuffer.slice(-2e3);
640
+ const recentOutput = this.stripAnsiForClassifier(recentRaw).trim();
641
+ const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
642
+ this.emit("stall_detected", recentOutput, stallDurationMs);
643
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
644
+ }
645
+ handleStallClassification(classification) {
646
+ if (this._status !== "busy" && this._status !== "authenticating") {
647
+ return;
648
+ }
649
+ if (!classification || classification.state === "still_working") {
650
+ this._stallBackoffMs = Math.min(
651
+ this._stallBackoffMs * 2,
652
+ _TmuxSession.MAX_STALL_BACKOFF_MS
653
+ );
654
+ this._lastContentHash = null;
655
+ this._lastStallHash = null;
656
+ if (this._stallTimer) {
657
+ clearTimeout(this._stallTimer);
658
+ this._stallTimer = null;
659
+ }
660
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
661
+ return;
662
+ }
663
+ switch (classification.state) {
664
+ case "waiting_for_input": {
665
+ const promptInfo = {
666
+ type: "stall_classified",
667
+ prompt: classification.prompt,
668
+ canAutoRespond: !!classification.suggestedResponse
669
+ };
670
+ if (classification.suggestedResponse) {
671
+ const resp = classification.suggestedResponse;
672
+ if (resp.startsWith("keys:")) {
673
+ const keys = resp.slice(5).split(",").map((k) => k.trim());
674
+ this.sendKeySequence(keys);
675
+ } else {
676
+ this.transport.sendText(this.tmuxSessionName, resp);
677
+ this.transport.sendKey(this.tmuxSessionName, "enter");
678
+ }
679
+ this.outputBuffer = "";
680
+ this.emit("blocking_prompt", promptInfo, true);
681
+ } else {
682
+ this.emit("blocking_prompt", promptInfo, false);
683
+ }
684
+ break;
685
+ }
686
+ case "task_complete":
687
+ this._status = "ready";
688
+ this._lastBlockingPromptHash = null;
689
+ this.outputBuffer = "";
690
+ this.clearStallTimer();
691
+ this.emit("ready");
692
+ break;
693
+ case "error":
694
+ this.clearStallTimer();
695
+ this.emit("error", new Error(classification.prompt || "Stall classified as error"));
696
+ break;
697
+ }
698
+ }
699
+ // ─────────────────────────────────────────────────────────────────────────────
700
+ // Task Completion Detection
701
+ // ─────────────────────────────────────────────────────────────────────────────
702
+ scheduleTaskComplete() {
703
+ if (this._taskCompleteTimer) {
704
+ clearTimeout(this._taskCompleteTimer);
705
+ }
706
+ this._taskCompletePending = true;
707
+ this._taskCompleteTimer = setTimeout(() => {
708
+ this._taskCompleteTimer = null;
709
+ this._taskCompletePending = false;
710
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
711
+ if (this._status !== "busy") return;
712
+ if (!signal) return;
713
+ this._status = "ready";
714
+ this._lastBlockingPromptHash = null;
715
+ this.outputBuffer = "";
716
+ this.clearStallTimer();
717
+ this.emit("status_changed", "ready");
718
+ this.emit("task_complete");
719
+ this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
720
+ }, _TmuxSession.TASK_COMPLETE_DEBOUNCE_MS);
721
+ }
722
+ isTaskCompleteSignal(output) {
723
+ if (this.adapter.detectTaskComplete) {
724
+ return this.adapter.detectTaskComplete(output);
725
+ }
726
+ return this.adapter.detectReady(output);
727
+ }
728
+ cancelTaskComplete() {
729
+ if (this._taskCompleteTimer) {
730
+ clearTimeout(this._taskCompleteTimer);
731
+ this._taskCompleteTimer = null;
732
+ }
733
+ this._taskCompletePending = false;
734
+ }
735
+ // ─────────────────────────────────────────────────────────────────────────────
736
+ // Ready Detection Settle Delay
737
+ // ─────────────────────────────────────────────────────────────────────────────
738
+ scheduleReadySettle() {
739
+ this._readySettlePending = true;
740
+ if (this._readySettleTimer) {
741
+ clearTimeout(this._readySettleTimer);
742
+ }
743
+ const settleMs = this.config.readySettleMs ?? this.adapter.readySettleMs ?? 100;
744
+ this._readySettleTimer = setTimeout(() => {
745
+ this._readySettleTimer = null;
746
+ this._readySettlePending = false;
747
+ if (this._status !== "starting" && this._status !== "authenticating") return;
748
+ if (!this.adapter.detectReady(this.outputBuffer)) return;
749
+ this._status = "ready";
750
+ this._lastBlockingPromptHash = null;
751
+ this.outputBuffer = "";
752
+ this.clearStallTimer();
753
+ this.emit("ready");
754
+ this.logger.info({ sessionId: this.id }, "Session ready (after settle)");
755
+ }, settleMs);
756
+ }
757
+ cancelReadySettle() {
758
+ if (this._readySettleTimer) {
759
+ clearTimeout(this._readySettleTimer);
760
+ this._readySettleTimer = null;
761
+ }
762
+ this._readySettlePending = false;
763
+ }
764
+ // ─────────────────────────────────────────────────────────────────────────────
765
+ // Lifecycle
766
+ // ─────────────────────────────────────────────────────────────────────────────
767
+ async start() {
768
+ this._status = "starting";
769
+ this._startedAt = /* @__PURE__ */ new Date();
770
+ const command = this.adapter.getCommand();
771
+ const args = this.adapter.getArgs(this.config);
772
+ const adapterEnv = this.adapter.getEnv(this.config);
773
+ const env = _TmuxSession.buildSpawnEnv(this.config, adapterEnv);
774
+ this.logger.info(
775
+ { sessionId: this.id, command, args: args.join(" "), tmuxSession: this.tmuxSessionName },
776
+ "Starting tmux session"
777
+ );
778
+ try {
779
+ this.transport.spawn(this.tmuxSessionName, {
780
+ command,
781
+ args,
782
+ cwd: this.config.workdir || process.cwd(),
783
+ env,
784
+ cols: this.config.cols || 120,
785
+ rows: this.config.rows || 40,
786
+ historyLimit: this.historyLimit
787
+ });
788
+ this.transport.startOutputStreaming(this.tmuxSessionName, (data) => {
789
+ this._lastActivityAt = /* @__PURE__ */ new Date();
790
+ this.outputBuffer += data;
791
+ if (this.outputBuffer.length > _TmuxSession.MAX_OUTPUT_BUFFER) {
792
+ this.outputBuffer = this.outputBuffer.slice(-_TmuxSession.MAX_OUTPUT_BUFFER);
793
+ }
794
+ this.emit("output", data);
795
+ this.processOutputBuffer();
796
+ });
797
+ this._exitPollTimer = setInterval(() => {
798
+ if (!this.transport.isPaneAlive(this.tmuxSessionName)) {
799
+ const exitCode = this.transport.getPaneExitStatus(this.tmuxSessionName) ?? 0;
800
+ this.handleExit(exitCode);
801
+ }
802
+ }, 1e3);
803
+ this.logger.info(
804
+ { sessionId: this.id, pid: this.pid, tmuxSession: this.tmuxSessionName },
805
+ "Tmux session started"
806
+ );
807
+ } catch (error) {
808
+ this._status = "error";
809
+ this.logger.error({ sessionId: this.id, error }, "Failed to start tmux session");
810
+ throw error;
811
+ }
812
+ }
813
+ /**
814
+ * Reconnect to an existing tmux session.
815
+ * Used for crash recovery.
816
+ */
817
+ async reconnect(existingTmuxName) {
818
+ if (!this.transport.isAlive(existingTmuxName)) {
819
+ throw new Error(`Tmux session ${existingTmuxName} does not exist`);
820
+ }
821
+ this.tmuxSessionName = existingTmuxName;
822
+ this._status = "starting";
823
+ this._startedAt = /* @__PURE__ */ new Date();
824
+ this.transport.startOutputStreaming(this.tmuxSessionName, (data) => {
825
+ this._lastActivityAt = /* @__PURE__ */ new Date();
826
+ this.outputBuffer += data;
827
+ if (this.outputBuffer.length > _TmuxSession.MAX_OUTPUT_BUFFER) {
828
+ this.outputBuffer = this.outputBuffer.slice(-_TmuxSession.MAX_OUTPUT_BUFFER);
829
+ }
830
+ this.emit("output", data);
831
+ this.processOutputBuffer();
832
+ });
833
+ const currentContent = this.transport.capturePane(this.tmuxSessionName, { ansi: true });
834
+ if (currentContent) {
835
+ this.outputBuffer = currentContent;
836
+ this.processOutputBuffer();
837
+ }
838
+ this._exitPollTimer = setInterval(() => {
839
+ if (!this.transport.isPaneAlive(this.tmuxSessionName)) {
840
+ const exitCode = this.transport.getPaneExitStatus(this.tmuxSessionName) ?? 0;
841
+ this.handleExit(exitCode);
842
+ }
843
+ }, 1e3);
844
+ this.logger.info(
845
+ { sessionId: this.id, tmuxSession: this.tmuxSessionName },
846
+ "Reconnected to tmux session"
847
+ );
848
+ }
849
+ handleExit(exitCode) {
850
+ if (this._status === "stopped" || this._status === "stopping") return;
851
+ this._status = "stopped";
852
+ this.clearStallTimer();
853
+ this.cancelTaskComplete();
854
+ this.cancelReadySettle();
855
+ this.stopExitPolling();
856
+ this.transport.stopOutputStreaming(this.tmuxSessionName);
857
+ this.logger.info({ sessionId: this.id, exitCode }, "Tmux session exited");
858
+ this.emit("exit", exitCode);
859
+ }
860
+ stopExitPolling() {
861
+ if (this._exitPollTimer) {
862
+ clearInterval(this._exitPollTimer);
863
+ this._exitPollTimer = null;
864
+ }
865
+ }
866
+ // ─────────────────────────────────────────────────────────────────────────────
867
+ // Output Processing
868
+ // ─────────────────────────────────────────────────────────────────────────────
869
+ processOutputBuffer() {
870
+ if (this._status === "busy" || this._status === "authenticating") {
871
+ this.resetStallTimer();
872
+ }
873
+ if (this._readySettlePending) {
874
+ if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
875
+ this.scheduleReadySettle();
876
+ } else {
877
+ this.cancelReadySettle();
878
+ }
879
+ return;
880
+ }
881
+ if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
882
+ this.scheduleReadySettle();
883
+ return;
884
+ }
885
+ if (this._status === "busy") {
886
+ const toolInfo = this.adapter.detectToolRunning?.(this.outputBuffer);
887
+ if (toolInfo) {
888
+ if (toolInfo.toolName !== this._lastToolRunningName) {
889
+ this._lastToolRunningName = toolInfo.toolName;
890
+ this.emit("tool_running", toolInfo);
891
+ }
892
+ } else if (this._lastToolRunningName) {
893
+ this._lastToolRunningName = null;
894
+ }
895
+ }
896
+ if (this._status === "busy") {
897
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
898
+ if (this._taskCompletePending || signal) {
899
+ this.scheduleTaskComplete();
900
+ }
901
+ }
902
+ if (this._status !== "stopping" && this._status !== "stopped") {
903
+ const blockingPrompt = this.detectAndHandleBlockingPrompt();
904
+ if (blockingPrompt) return;
905
+ }
906
+ if (this._status !== "ready" && this._status !== "busy") {
907
+ const loginDetection = this.adapter.detectLogin(this.outputBuffer);
908
+ if (loginDetection.required && this._status !== "authenticating") {
909
+ this._status = "authenticating";
910
+ this.clearStallTimer();
911
+ this.emitAuthRequired({
912
+ type: loginDetection.type,
913
+ url: loginDetection.url,
914
+ deviceCode: loginDetection.deviceCode,
915
+ instructions: loginDetection.instructions
916
+ });
917
+ return;
918
+ }
919
+ }
920
+ const exitDetection = this.adapter.detectExit(this.outputBuffer);
921
+ if (exitDetection.exited) {
922
+ this._status = "stopped";
923
+ this.clearStallTimer();
924
+ this.emit("exit", exitDetection.code || 0);
925
+ }
926
+ if (this._status === "ready") {
927
+ this.tryParseOutput();
928
+ }
929
+ }
930
+ // ─────────────────────────────────────────────────────────────────────────────
931
+ // Blocking Prompt Detection & Auto-Response
932
+ // ─────────────────────────────────────────────────────────────────────────────
933
+ detectAndHandleBlockingPrompt() {
934
+ const autoHandled = this.tryAutoResponse();
935
+ if (autoHandled) return true;
936
+ if (this.adapter.detectBlockingPrompt) {
937
+ const detection = this.adapter.detectBlockingPrompt(this.outputBuffer);
938
+ if (detection.detected) {
939
+ const normalizedPrompt = (detection.prompt || "").replace(/\s+/g, " ").replace(/\d+/g, "#").trim().slice(0, 100);
940
+ const promptHash = `${detection.type}:${normalizedPrompt}`;
941
+ if (promptHash === this._lastBlockingPromptHash) return true;
942
+ this._lastBlockingPromptHash = promptHash;
943
+ const promptInfo = {
944
+ type: detection.type || "unknown",
945
+ prompt: detection.prompt,
946
+ options: detection.options,
947
+ canAutoRespond: detection.canAutoRespond || false,
948
+ instructions: detection.instructions,
949
+ url: detection.url
950
+ };
951
+ if (detection.canAutoRespond && detection.suggestedResponse && !this.config.skipAdapterAutoResponse) {
952
+ const resp = detection.suggestedResponse;
953
+ if (resp.startsWith("keys:")) {
954
+ const keys = resp.slice(5).split(",").map((k) => k.trim());
955
+ this.sendKeySequence(keys);
956
+ } else {
957
+ this.transport.sendText(this.tmuxSessionName, resp);
958
+ this.transport.sendKey(this.tmuxSessionName, "enter");
959
+ }
960
+ this.outputBuffer = "";
961
+ this.emit("blocking_prompt", promptInfo, true);
962
+ return true;
963
+ }
964
+ if (detection.type === "login") {
965
+ this._status = "authenticating";
966
+ const inferred = this.adapter.detectLogin(this.outputBuffer);
967
+ this.emitAuthRequired({
968
+ type: inferred.required ? inferred.type : void 0,
969
+ url: detection.url ?? inferred.url,
970
+ deviceCode: inferred.required ? inferred.deviceCode : void 0,
971
+ instructions: detection.instructions ?? inferred.instructions
972
+ });
973
+ }
974
+ this.emit("blocking_prompt", promptInfo, false);
975
+ return true;
976
+ } else {
977
+ this._lastBlockingPromptHash = null;
978
+ }
979
+ }
980
+ return false;
981
+ }
982
+ tryAutoResponse() {
983
+ const adapterRules = (this.adapter.autoResponseRules || []).filter((r) => !this._disabledRulePatterns.has(r.pattern.source)).map((r) => {
984
+ const override = this._ruleOverrides.get(r.pattern.source);
985
+ return override ? { ...r, ...override } : r;
986
+ });
987
+ const allRules = [...this.sessionRules, ...adapterRules];
988
+ if (allRules.length === 0) return false;
989
+ const stripped = this.stripAnsiForStall(this.outputBuffer);
990
+ for (const rule of allRules) {
991
+ if (rule.once) {
992
+ const ruleKey = `${rule.pattern.source}:${rule.pattern.flags}`;
993
+ if (this._firedOnceRules.has(ruleKey)) continue;
994
+ }
995
+ if (rule.pattern.test(stripped)) {
996
+ const safe = rule.safe !== false;
997
+ if (safe) {
998
+ const useKeys = rule.keys && rule.keys.length > 0;
999
+ const isTuiDefault = !rule.responseType && !rule.keys && this.adapter.usesTuiMenus;
1000
+ if (useKeys) {
1001
+ this.sendKeySequence(rule.keys);
1002
+ } else if (isTuiDefault) {
1003
+ this.sendKeys("enter");
1004
+ } else {
1005
+ this.transport.sendText(this.tmuxSessionName, rule.response);
1006
+ this.transport.sendKey(this.tmuxSessionName, "enter");
1007
+ }
1008
+ if (rule.once) {
1009
+ const ruleKey = `${rule.pattern.source}:${rule.pattern.flags}`;
1010
+ this._firedOnceRules.add(ruleKey);
1011
+ }
1012
+ this.outputBuffer = "";
1013
+ const promptInfo = {
1014
+ type: rule.type,
1015
+ prompt: rule.description,
1016
+ canAutoRespond: true
1017
+ };
1018
+ this.emit("blocking_prompt", promptInfo, true);
1019
+ return true;
1020
+ } else {
1021
+ const promptInfo = {
1022
+ type: rule.type,
1023
+ prompt: rule.description,
1024
+ canAutoRespond: false,
1025
+ instructions: `Prompt matched but requires user confirmation: ${rule.description}`
1026
+ };
1027
+ this.emit("blocking_prompt", promptInfo, false);
1028
+ return true;
1029
+ }
1030
+ }
1031
+ }
1032
+ return false;
1033
+ }
1034
+ tryParseOutput() {
1035
+ const parsed = this.adapter.parseOutput(this.outputBuffer);
1036
+ if (parsed && parsed.isComplete) {
1037
+ this.outputBuffer = "";
1038
+ const message = {
1039
+ id: `${this.id}-msg-${++this.messageCounter}`,
1040
+ sessionId: this.id,
1041
+ direction: "outbound",
1042
+ type: parsed.type,
1043
+ content: parsed.content,
1044
+ metadata: parsed.metadata,
1045
+ timestamp: /* @__PURE__ */ new Date()
1046
+ };
1047
+ this.emit("message", message);
1048
+ if (parsed.isQuestion) {
1049
+ this.emit("question", parsed.content);
1050
+ }
1051
+ }
1052
+ }
1053
+ // ─────────────────────────────────────────────────────────────────────────────
1054
+ // I/O Methods
1055
+ // ─────────────────────────────────────────────────────────────────────────────
1056
+ /**
1057
+ * Write data to the session (formatted by adapter, with Enter)
1058
+ */
1059
+ write(data) {
1060
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1061
+ const formatted = this.adapter.formatInput(data);
1062
+ this.transport.sendText(this.tmuxSessionName, formatted);
1063
+ this.transport.sendKey(this.tmuxSessionName, "enter");
1064
+ }
1065
+ /**
1066
+ * Write raw data directly (no formatting, no Enter)
1067
+ */
1068
+ writeRaw(data) {
1069
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1070
+ this.transport.sendText(this.tmuxSessionName, data);
1071
+ }
1072
+ /**
1073
+ * Send a task/message to the session
1074
+ */
1075
+ send(message) {
1076
+ this._status = "busy";
1077
+ this.outputBuffer = "";
1078
+ this.emit("status_changed", "busy");
1079
+ this._stallEmissionCount = 0;
1080
+ this.resetStallTimer();
1081
+ const msg = {
1082
+ id: `${this.id}-msg-${++this.messageCounter}`,
1083
+ sessionId: this.id,
1084
+ direction: "inbound",
1085
+ type: "task",
1086
+ content: message,
1087
+ timestamp: /* @__PURE__ */ new Date()
1088
+ };
1089
+ const formatted = this.adapter.formatInput(message);
1090
+ this.transport.sendText(this.tmuxSessionName, formatted);
1091
+ setTimeout(() => {
1092
+ this.transport.sendKey(this.tmuxSessionName, "enter");
1093
+ }, 50);
1094
+ return msg;
1095
+ }
1096
+ /**
1097
+ * Resize the terminal
1098
+ */
1099
+ resize(cols, rows) {
1100
+ this.transport.resize(this.tmuxSessionName, cols, rows);
1101
+ }
1102
+ /**
1103
+ * Send special keys to the session.
1104
+ * Uses tmux send-keys with named keys.
1105
+ */
1106
+ sendKeys(keys) {
1107
+ const keyList = Array.isArray(keys) ? keys : [keys];
1108
+ const normalized = _TmuxSession.normalizeKeyList(keyList);
1109
+ this._stallEmissionCount = 0;
1110
+ this._lastBlockingPromptHash = null;
1111
+ this.outputBuffer = "";
1112
+ this.resetStallTimer();
1113
+ for (const key of normalized) {
1114
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1115
+ this.transport.sendKey(this.tmuxSessionName, key);
1116
+ }
1117
+ }
1118
+ /**
1119
+ * Select a TUI menu option by index (0-based).
1120
+ */
1121
+ async selectMenuOption(optionIndex) {
1122
+ for (let i = 0; i < optionIndex; i++) {
1123
+ this.sendKeys("down");
1124
+ await this.delay(50);
1125
+ }
1126
+ this.sendKeys("enter");
1127
+ }
1128
+ /**
1129
+ * Send a sequence of keys with staggered timing.
1130
+ */
1131
+ sendKeySequence(keys) {
1132
+ keys.forEach((key, i) => {
1133
+ setTimeout(() => this.sendKeys(key), i * 50);
1134
+ });
1135
+ }
1136
+ /**
1137
+ * Paste text using bracketed paste mode
1138
+ */
1139
+ paste(text, useBracketedPaste = true) {
1140
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1141
+ if (useBracketedPaste) {
1142
+ this.transport.sendText(this.tmuxSessionName, "\x1B[200~" + text + "\x1B[201~");
1143
+ } else {
1144
+ this.transport.sendText(this.tmuxSessionName, text);
1145
+ }
1146
+ }
1147
+ /**
1148
+ * Notify the session of an external hook event.
1149
+ */
1150
+ notifyHookEvent(event) {
1151
+ switch (event) {
1152
+ case "tool_running":
1153
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1154
+ this.resetStallTimer();
1155
+ break;
1156
+ case "task_complete":
1157
+ this._status = "ready";
1158
+ this._lastBlockingPromptHash = null;
1159
+ this.outputBuffer = "";
1160
+ this.clearStallTimer();
1161
+ this.emit("status_changed", "ready");
1162
+ this.emit("task_complete");
1163
+ break;
1164
+ case "permission_approved":
1165
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1166
+ this.outputBuffer = "";
1167
+ this.resetStallTimer();
1168
+ break;
1169
+ default:
1170
+ this._lastActivityAt = /* @__PURE__ */ new Date();
1171
+ this.resetStallTimer();
1172
+ break;
1173
+ }
1174
+ }
1175
+ /**
1176
+ * Kill the session.
1177
+ */
1178
+ kill(signal) {
1179
+ this._status = "stopping";
1180
+ this.clearStallTimer();
1181
+ this.cancelTaskComplete();
1182
+ this.cancelReadySettle();
1183
+ this.stopExitPolling();
1184
+ if (signal === "SIGKILL") {
1185
+ this.transport.signal(this.tmuxSessionName, "SIGKILL");
1186
+ setTimeout(() => {
1187
+ this.transport.kill(this.tmuxSessionName);
1188
+ this._status = "stopped";
1189
+ this.emit("exit", 137);
1190
+ }, 200);
1191
+ } else {
1192
+ this.transport.signal(this.tmuxSessionName, signal || "SIGTERM");
1193
+ }
1194
+ this.logger.info({ sessionId: this.id, signal }, "Killing tmux session");
1195
+ }
1196
+ /**
1197
+ * Get current output buffer
1198
+ */
1199
+ getOutputBuffer() {
1200
+ return this.outputBuffer;
1201
+ }
1202
+ /**
1203
+ * Clear output buffer
1204
+ */
1205
+ clearOutputBuffer() {
1206
+ this.outputBuffer = "";
1207
+ }
1208
+ /**
1209
+ * Convert to SessionHandle
1210
+ */
1211
+ toHandle() {
1212
+ return {
1213
+ id: this.id,
1214
+ name: this.config.name,
1215
+ type: this.config.type,
1216
+ status: this._status,
1217
+ pid: this.pid,
1218
+ startedAt: this._startedAt ?? void 0,
1219
+ lastActivityAt: this._lastActivityAt ?? void 0,
1220
+ tmuxSessionName: this.tmuxSessionName
1221
+ };
1222
+ }
1223
+ // ─────────────────────────────────────────────────────────────────────────────
1224
+ // Static Utilities
1225
+ // ─────────────────────────────────────────────────────────────────────────────
1226
+ static buildSpawnEnv(config, adapterEnv) {
1227
+ const baseEnv = config.inheritProcessEnv !== false ? process.env : {};
1228
+ return {
1229
+ ...baseEnv,
1230
+ ...adapterEnv,
1231
+ ...config.env,
1232
+ TERM: "xterm-256color",
1233
+ COLORTERM: "truecolor"
1234
+ };
1235
+ }
1236
+ static normalizeKeyList(keys) {
1237
+ const MODIFIER_MAP = {
1238
+ control: "ctrl",
1239
+ command: "meta",
1240
+ cmd: "meta",
1241
+ option: "alt",
1242
+ opt: "alt"
1243
+ };
1244
+ const MODIFIER_NAMES = /* @__PURE__ */ new Set([
1245
+ "ctrl",
1246
+ "alt",
1247
+ "shift",
1248
+ "meta",
1249
+ ...Object.keys(MODIFIER_MAP)
1250
+ ]);
1251
+ const result = [];
1252
+ let i = 0;
1253
+ while (i < keys.length) {
1254
+ let key = keys[i].toLowerCase().trim();
1255
+ if (MODIFIER_MAP[key]) {
1256
+ key = MODIFIER_MAP[key];
1257
+ }
1258
+ if (MODIFIER_NAMES.has(key) && i + 1 < keys.length) {
1259
+ let nextKey = keys[i + 1].toLowerCase().trim();
1260
+ if (MODIFIER_MAP[nextKey]) {
1261
+ nextKey = MODIFIER_MAP[nextKey];
1262
+ }
1263
+ if (!MODIFIER_NAMES.has(nextKey)) {
1264
+ result.push(`${key}+${nextKey}`);
1265
+ i += 2;
1266
+ continue;
1267
+ }
1268
+ }
1269
+ result.push(key);
1270
+ i++;
1271
+ }
1272
+ return result;
1273
+ }
1274
+ // ─────────────────────────────────────────────────────────────────────────────
1275
+ // Private Helpers
1276
+ // ─────────────────────────────────────────────────────────────────────────────
1277
+ delay(ms) {
1278
+ return new Promise((resolve) => setTimeout(resolve, ms));
1279
+ }
1280
+ simpleHash(str) {
1281
+ let hash = 0;
1282
+ for (let i = 0; i < str.length; i++) {
1283
+ const char = str.charCodeAt(i);
1284
+ hash = (hash << 5) - hash + char;
1285
+ hash |= 0;
1286
+ }
1287
+ return hash.toString(36);
1288
+ }
1289
+ mapLoginTypeToAuthMethod(type) {
1290
+ switch (type) {
1291
+ case "api_key":
1292
+ return "api_key";
1293
+ case "cli_auth":
1294
+ return "cli_auth";
1295
+ case "device_code":
1296
+ return "device_code";
1297
+ case "oauth":
1298
+ case "browser":
1299
+ return "oauth_browser";
1300
+ default:
1301
+ return "unknown";
1302
+ }
1303
+ }
1304
+ emitAuthRequired(details) {
1305
+ const info = {
1306
+ method: this.mapLoginTypeToAuthMethod(details.type),
1307
+ url: details.url,
1308
+ deviceCode: details.deviceCode,
1309
+ instructions: details.instructions
1310
+ };
1311
+ this.emit("auth_required", info);
1312
+ this.emit("login_required", info.instructions, info.url);
1313
+ }
1314
+ /**
1315
+ * Strip ANSI codes for stall detection hashing.
1316
+ * Simplified compared to pty-manager since tmux capture-pane can give clean text.
1317
+ */
1318
+ stripAnsiForStall(str) {
1319
+ let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
1320
+ result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
1321
+ result = result.replace(/\x1b\[\d*[JK]/g, " ");
1322
+ result = result.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
1323
+ result = result.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
1324
+ result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1325
+ result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
1326
+ result = result.replace(/\xa0/g, " ");
1327
+ result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
1328
+ result = result.replace(/\d+[hms](?:\s+\d+[hms])*/g, "0s");
1329
+ result = result.replace(/ {2,}/g, " ");
1330
+ return result;
1331
+ }
1332
+ /**
1333
+ * Less-aggressive ANSI stripping for classifier context.
1334
+ */
1335
+ stripAnsiForClassifier(str) {
1336
+ let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
1337
+ result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
1338
+ result = result.replace(/\x1b\[\d*[JK]/g, " ");
1339
+ result = result.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
1340
+ result = result.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
1341
+ result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1342
+ result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
1343
+ result = result.replace(/\xa0/g, " ");
1344
+ result = result.replace(/ {2,}/g, " ");
1345
+ return result;
1346
+ }
1347
+ };
1348
+
1349
+ // src/tmux-manager.ts
1350
+ var TmuxManager = class extends EventEmitter2 {
1351
+ sessions = /* @__PURE__ */ new Map();
1352
+ outputLogs = /* @__PURE__ */ new Map();
1353
+ maxLogLines;
1354
+ logger;
1355
+ transport;
1356
+ adapters;
1357
+ // Stall detection config
1358
+ _stallDetectionEnabled;
1359
+ _stallTimeoutMs;
1360
+ _onStallClassify;
1361
+ // Tmux-specific config
1362
+ _historyLimit;
1363
+ _sessionPrefix;
1364
+ constructor(config = {}) {
1365
+ super();
1366
+ this.adapters = new AdapterRegistry();
1367
+ this.logger = config.logger || consoleLogger;
1368
+ this.maxLogLines = config.maxLogLines || 1e3;
1369
+ this._stallDetectionEnabled = config.stallDetectionEnabled ?? false;
1370
+ this._stallTimeoutMs = config.stallTimeoutMs ?? 8e3;
1371
+ this._onStallClassify = config.onStallClassify;
1372
+ this._historyLimit = config.historyLimit ?? 5e4;
1373
+ this._sessionPrefix = config.sessionPrefix ?? "parallax";
1374
+ this.transport = new TmuxTransport();
1375
+ }
1376
+ /**
1377
+ * Register a CLI adapter
1378
+ */
1379
+ registerAdapter(adapter) {
1380
+ this.adapters.register(adapter);
1381
+ }
1382
+ /**
1383
+ * Spawn a new tmux session
1384
+ */
1385
+ async spawn(config) {
1386
+ const adapter = this.adapters.get(config.type);
1387
+ if (!adapter) {
1388
+ throw new Error(
1389
+ `No adapter found for type: ${config.type}. Registered adapters: ${this.adapters.list().join(", ") || "none"}`
1390
+ );
1391
+ }
1392
+ if (config.id && this.sessions.has(config.id)) {
1393
+ throw new Error(`Session with ID ${config.id} already exists`);
1394
+ }
1395
+ this.logger.info(
1396
+ { type: config.type, name: config.name },
1397
+ "Spawning tmux session"
1398
+ );
1399
+ const session = new TmuxSession(
1400
+ adapter,
1401
+ config,
1402
+ this.logger,
1403
+ this._stallDetectionEnabled,
1404
+ this._stallTimeoutMs,
1405
+ this.transport,
1406
+ this._sessionPrefix,
1407
+ this._historyLimit
1408
+ );
1409
+ this.setupSessionEvents(session);
1410
+ this.sessions.set(session.id, session);
1411
+ this.outputLogs.set(session.id, []);
1412
+ await session.start();
1413
+ const handle = session.toHandle();
1414
+ this.emit("session_started", handle);
1415
+ return handle;
1416
+ }
1417
+ /**
1418
+ * Set up event handlers for a session
1419
+ */
1420
+ setupSessionEvents(session) {
1421
+ session.on("output", (data) => {
1422
+ const logs = this.outputLogs.get(session.id) || [];
1423
+ const lines = data.split("\n");
1424
+ logs.push(...lines);
1425
+ while (logs.length > this.maxLogLines) {
1426
+ logs.shift();
1427
+ }
1428
+ this.outputLogs.set(session.id, logs);
1429
+ });
1430
+ session.on("ready", () => {
1431
+ this.emit("session_ready", session.toHandle());
1432
+ });
1433
+ session.on("login_required", (instructions, url) => {
1434
+ this.emit("login_required", session.toHandle(), instructions, url);
1435
+ });
1436
+ session.on("auth_required", (info) => {
1437
+ this.emit("auth_required", session.toHandle(), info);
1438
+ });
1439
+ session.on("blocking_prompt", (promptInfo, autoResponded) => {
1440
+ this.emit("blocking_prompt", session.toHandle(), promptInfo, autoResponded);
1441
+ });
1442
+ session.on("message", (message) => {
1443
+ this.emit("message", message);
1444
+ });
1445
+ session.on("question", (question) => {
1446
+ this.emit("question", session.toHandle(), question);
1447
+ });
1448
+ session.on("exit", (code) => {
1449
+ const reason = code === 0 ? "normal exit" : `exit code ${code}`;
1450
+ this.emit("session_stopped", session.toHandle(), reason);
1451
+ });
1452
+ session.on("error", (error) => {
1453
+ this.emit("session_error", session.toHandle(), error.message);
1454
+ });
1455
+ session.on("status_changed", () => {
1456
+ this.emit("session_status_changed", session.toHandle());
1457
+ });
1458
+ session.on("task_complete", () => {
1459
+ this.emit("task_complete", session.toHandle());
1460
+ });
1461
+ session.on("tool_running", (info) => {
1462
+ this.emit("tool_running", session.toHandle(), info);
1463
+ });
1464
+ session.on("stall_detected", (recentOutput, stallDurationMs) => {
1465
+ const handle = session.toHandle();
1466
+ this.emit("stall_detected", handle, recentOutput, stallDurationMs);
1467
+ if (this._onStallClassify) {
1468
+ const sanitized = recentOutput.slice(-1500).replace(/\b(ignore|disregard|forget)\s+(all\s+)?(previous|above|prior)\s+(instructions?|prompts?|rules?)\b/gi, "[REDACTED]").replace(/\b(you\s+are|act\s+as|pretend\s+to\s+be|you\s+must|system\s*:)\b/gi, "[REDACTED]");
1469
+ this._onStallClassify(session.id, sanitized, stallDurationMs).then((classification) => {
1470
+ session.handleStallClassification(classification);
1471
+ }).catch((err) => {
1472
+ this.logger.error(
1473
+ { sessionId: session.id, error: err },
1474
+ "Stall classification callback failed"
1475
+ );
1476
+ session.handleStallClassification(null);
1477
+ });
1478
+ }
1479
+ });
1480
+ }
1481
+ /**
1482
+ * Stop a session
1483
+ */
1484
+ async stop(sessionId, options) {
1485
+ const session = this.sessions.get(sessionId);
1486
+ if (!session) {
1487
+ throw new Error(`Session not found: ${sessionId}`);
1488
+ }
1489
+ this.logger.info({ sessionId, force: options?.force }, "Stopping session");
1490
+ const timeout = options?.timeout || 5e3;
1491
+ return new Promise((resolve) => {
1492
+ const timer = setTimeout(() => {
1493
+ session.kill("SIGKILL");
1494
+ setTimeout(() => {
1495
+ session.removeAllListeners();
1496
+ this.sessions.delete(sessionId);
1497
+ this.outputLogs.delete(sessionId);
1498
+ resolve();
1499
+ }, 500);
1500
+ }, timeout);
1501
+ session.once("exit", () => {
1502
+ clearTimeout(timer);
1503
+ session.removeAllListeners();
1504
+ this.sessions.delete(sessionId);
1505
+ this.outputLogs.delete(sessionId);
1506
+ resolve();
1507
+ });
1508
+ session.kill(options?.force ? "SIGKILL" : "SIGTERM");
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Stop all sessions
1513
+ */
1514
+ async stopAll(options) {
1515
+ const stopPromises = Array.from(this.sessions.keys()).map(
1516
+ (id) => this.stop(id, options).catch((err) => {
1517
+ this.logger.warn({ sessionId: id, error: err }, "Error stopping session");
1518
+ })
1519
+ );
1520
+ await Promise.all(stopPromises);
1521
+ }
1522
+ /**
1523
+ * Get a session by ID
1524
+ */
1525
+ get(sessionId) {
1526
+ const session = this.sessions.get(sessionId);
1527
+ return session ? session.toHandle() : null;
1528
+ }
1529
+ /**
1530
+ * List all sessions
1531
+ */
1532
+ list(filter) {
1533
+ const handles = [];
1534
+ for (const session of this.sessions.values()) {
1535
+ const handle = session.toHandle();
1536
+ if (filter) {
1537
+ if (filter.status) {
1538
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
1539
+ if (!statuses.includes(handle.status)) continue;
1540
+ }
1541
+ if (filter.type) {
1542
+ const types = Array.isArray(filter.type) ? filter.type : [filter.type];
1543
+ if (!types.includes(handle.type)) continue;
1544
+ }
1545
+ }
1546
+ handles.push(handle);
1547
+ }
1548
+ return handles;
1549
+ }
1550
+ /**
1551
+ * Send a message to a session
1552
+ */
1553
+ send(sessionId, message) {
1554
+ const session = this.sessions.get(sessionId);
1555
+ if (!session) {
1556
+ throw new Error(`Session not found: ${sessionId}`);
1557
+ }
1558
+ return session.send(message);
1559
+ }
1560
+ /**
1561
+ * Get logs for a session
1562
+ */
1563
+ async *logs(sessionId, options) {
1564
+ const logBuffer = this.outputLogs.get(sessionId);
1565
+ if (!logBuffer) {
1566
+ throw new Error(`Session not found: ${sessionId}`);
1567
+ }
1568
+ const lines = options?.tail ? logBuffer.slice(-options.tail) : logBuffer;
1569
+ for (const line of lines) {
1570
+ yield line;
1571
+ }
1572
+ }
1573
+ /**
1574
+ * Get metrics for a session
1575
+ */
1576
+ metrics(sessionId) {
1577
+ const session = this.sessions.get(sessionId);
1578
+ if (!session) return null;
1579
+ const handle = session.toHandle();
1580
+ const uptime = handle.startedAt ? Math.floor((Date.now() - handle.startedAt.getTime()) / 1e3) : void 0;
1581
+ return { uptime };
1582
+ }
1583
+ /**
1584
+ * Shutdown manager and stop all sessions
1585
+ */
1586
+ async shutdown() {
1587
+ this.logger.info({ count: this.sessions.size }, "Shutting down all tmux sessions");
1588
+ await this.stopAll({ timeout: 3e3 });
1589
+ this.sessions.clear();
1590
+ this.outputLogs.clear();
1591
+ this.transport.destroy();
1592
+ }
1593
+ /**
1594
+ * Get count of sessions by status
1595
+ */
1596
+ getStatusCounts() {
1597
+ const counts = {
1598
+ pending: 0,
1599
+ starting: 0,
1600
+ authenticating: 0,
1601
+ ready: 0,
1602
+ busy: 0,
1603
+ stopping: 0,
1604
+ stopped: 0,
1605
+ error: 0
1606
+ };
1607
+ for (const session of this.sessions.values()) {
1608
+ counts[session.status]++;
1609
+ }
1610
+ return counts;
1611
+ }
1612
+ /**
1613
+ * Attach to a session's terminal for raw I/O streaming
1614
+ */
1615
+ attachTerminal(sessionId) {
1616
+ const session = this.sessions.get(sessionId);
1617
+ if (!session) return null;
1618
+ return {
1619
+ onData: (callback) => {
1620
+ session.on("output", callback);
1621
+ return () => session.off("output", callback);
1622
+ },
1623
+ write: (data) => {
1624
+ session.writeRaw(data);
1625
+ },
1626
+ resize: (cols, rows) => {
1627
+ session.resize(cols, rows);
1628
+ }
1629
+ };
1630
+ }
1631
+ /**
1632
+ * Check if a session exists
1633
+ */
1634
+ has(sessionId) {
1635
+ return this.sessions.has(sessionId);
1636
+ }
1637
+ /**
1638
+ * Get the underlying TmuxSession (for advanced use)
1639
+ */
1640
+ getSession(sessionId) {
1641
+ return this.sessions.get(sessionId);
1642
+ }
1643
+ // ─────────────────────────────────────────────────────────────────────────────
1644
+ // Tmux-Specific Features
1645
+ // ─────────────────────────────────────────────────────────────────────────────
1646
+ /**
1647
+ * List orphaned tmux sessions from previous runs.
1648
+ * These are tmux sessions with the configured prefix that aren't tracked by this manager.
1649
+ */
1650
+ listOrphanedSessions() {
1651
+ const tmuxSessions = TmuxTransport.listSessions(this._sessionPrefix);
1652
+ const managedNames = new Set(
1653
+ Array.from(this.sessions.values()).map((s) => s.tmuxName)
1654
+ );
1655
+ return tmuxSessions.filter((s) => !managedNames.has(s.name));
1656
+ }
1657
+ /**
1658
+ * Clean up orphaned tmux sessions from previous runs.
1659
+ */
1660
+ cleanupOrphanedSessions() {
1661
+ const orphans = this.listOrphanedSessions();
1662
+ for (const orphan of orphans) {
1663
+ try {
1664
+ this.transport.kill(orphan.name);
1665
+ this.logger.info({ tmuxSession: orphan.name }, "Cleaned up orphaned tmux session");
1666
+ } catch {
1667
+ this.logger.warn({ tmuxSession: orphan.name }, "Failed to clean up orphaned tmux session");
1668
+ }
1669
+ }
1670
+ return orphans.length;
1671
+ }
1672
+ // ─────────────────────────────────────────────────────────────────────────────
1673
+ // Stall Detection Configuration
1674
+ // ─────────────────────────────────────────────────────────────────────────────
1675
+ configureStallDetection(enabled, timeoutMs, classify) {
1676
+ this._stallDetectionEnabled = enabled;
1677
+ if (timeoutMs !== void 0) {
1678
+ this._stallTimeoutMs = timeoutMs;
1679
+ }
1680
+ if (classify !== void 0) {
1681
+ this._onStallClassify = classify;
1682
+ }
1683
+ }
1684
+ // ─────────────────────────────────────────────────────────────────────────────
1685
+ // Runtime Auto-Response Rules API
1686
+ // ─────────────────────────────────────────────────────────────────────────────
1687
+ addAutoResponseRule(sessionId, rule) {
1688
+ const session = this.sessions.get(sessionId);
1689
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
1690
+ session.addAutoResponseRule(rule);
1691
+ }
1692
+ removeAutoResponseRule(sessionId, pattern) {
1693
+ const session = this.sessions.get(sessionId);
1694
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
1695
+ return session.removeAutoResponseRule(pattern);
1696
+ }
1697
+ setAutoResponseRules(sessionId, rules) {
1698
+ const session = this.sessions.get(sessionId);
1699
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
1700
+ session.setAutoResponseRules(rules);
1701
+ }
1702
+ getAutoResponseRules(sessionId) {
1703
+ const session = this.sessions.get(sessionId);
1704
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
1705
+ return session.getAutoResponseRules();
1706
+ }
1707
+ clearAutoResponseRules(sessionId) {
1708
+ const session = this.sessions.get(sessionId);
1709
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
1710
+ session.clearAutoResponseRules();
1711
+ }
1712
+ };
1713
+
1714
+ // src/task-completion-trace.ts
1715
+ function extractTaskCompletionTraceRecords(entries) {
1716
+ const out = [];
1717
+ for (const entry of entries) {
1718
+ let obj = null;
1719
+ if (typeof entry === "string") {
1720
+ const line = entry.trim();
1721
+ if (!line.startsWith("{") || !line.endsWith("}")) continue;
1722
+ try {
1723
+ obj = JSON.parse(line);
1724
+ } catch {
1725
+ continue;
1726
+ }
1727
+ } else if (entry && typeof entry === "object") {
1728
+ obj = entry;
1729
+ }
1730
+ if (!obj) continue;
1731
+ if (obj.msg !== "Task completion trace") continue;
1732
+ if (typeof obj.event !== "string") continue;
1733
+ out.push({
1734
+ sessionId: asString(obj.sessionId),
1735
+ adapterType: asString(obj.adapterType),
1736
+ event: obj.event,
1737
+ status: asString(obj.status),
1738
+ taskCompletePending: asBool(obj.taskCompletePending),
1739
+ signal: asBool(obj.signal),
1740
+ wasPending: asBool(obj.wasPending),
1741
+ debounceMs: asNumber(obj.debounceMs),
1742
+ detectTaskComplete: asBool(obj.detectTaskComplete),
1743
+ detectReady: asBool(obj.detectReady),
1744
+ detectLoading: asBool(obj.detectLoading),
1745
+ tailHash: asString(obj.tailHash),
1746
+ tailSnippet: asString(obj.tailSnippet),
1747
+ timestamp: asTimestamp(obj.time) ?? asTimestamp(obj.timestamp)
1748
+ });
1749
+ }
1750
+ return out;
1751
+ }
1752
+ function buildTaskCompletionTimeline(records, options = {}) {
1753
+ const filtered = records.filter((r) => {
1754
+ if (!options.adapterType) return true;
1755
+ return r.adapterType === options.adapterType;
1756
+ });
1757
+ const turns = [];
1758
+ let current = null;
1759
+ let ignored = 0;
1760
+ filtered.forEach((record, index) => {
1761
+ if (record.event === "busy_signal" && current && current.completed) {
1762
+ current = null;
1763
+ }
1764
+ if (!current) {
1765
+ current = {
1766
+ turn: turns.length + 1,
1767
+ startIndex: index,
1768
+ endIndex: index,
1769
+ completed: false,
1770
+ maxConfidence: 0,
1771
+ finalConfidence: 0,
1772
+ events: []
1773
+ };
1774
+ turns.push(current);
1775
+ }
1776
+ const step = toStep(record, index);
1777
+ if (!step) {
1778
+ ignored++;
1779
+ return;
1780
+ }
1781
+ current.events.push(step);
1782
+ current.endIndex = index;
1783
+ current.maxConfidence = Math.max(current.maxConfidence, step.confidence);
1784
+ current.finalConfidence = step.confidence;
1785
+ if (step.status === "completed") {
1786
+ current.completed = true;
1787
+ }
1788
+ });
1789
+ return {
1790
+ turns,
1791
+ totalRecords: filtered.length,
1792
+ ignoredRecords: ignored
1793
+ };
1794
+ }
1795
+ function toStep(record, atIndex) {
1796
+ const event = record.event;
1797
+ const confidence = scoreConfidence(record);
1798
+ if (event === "transition_ready") {
1799
+ return withCommon(record, { event, atIndex, status: "completed", confidence: 100 });
1800
+ }
1801
+ if (event === "debounce_reject_signal" || event === "debounce_reject_status") {
1802
+ return withCommon(record, { event, atIndex, status: "rejected", confidence });
1803
+ }
1804
+ if (record.detectLoading) {
1805
+ return withCommon(record, { event, atIndex, status: "active_loading", confidence });
1806
+ }
1807
+ if (event === "debounce_fire" && record.signal) {
1808
+ return withCommon(record, { event, atIndex, status: "likely_complete", confidence });
1809
+ }
1810
+ if (event === "busy_signal" || event === "debounce_schedule" || event === "debounce_fire") {
1811
+ return withCommon(record, { event, atIndex, status: "active", confidence });
1812
+ }
1813
+ return null;
1814
+ }
1815
+ function scoreConfidence(record) {
1816
+ let score = 10;
1817
+ if (record.detectLoading) score -= 40;
1818
+ if (record.detectReady) score += 20;
1819
+ if (record.detectTaskComplete) score += 45;
1820
+ if (record.signal) score += 20;
1821
+ if (record.event === "debounce_reject_signal" || record.event === "debounce_reject_status") {
1822
+ score -= 30;
1823
+ }
1824
+ if (record.event === "transition_ready") score = 100;
1825
+ if (score < 0) return 0;
1826
+ if (score > 100) return 100;
1827
+ return score;
1828
+ }
1829
+ function withCommon(record, step) {
1830
+ return {
1831
+ ...step,
1832
+ signal: record.signal,
1833
+ detectTaskComplete: record.detectTaskComplete,
1834
+ detectReady: record.detectReady,
1835
+ detectLoading: record.detectLoading
1836
+ };
1837
+ }
1838
+ function asString(value) {
1839
+ return typeof value === "string" ? value : void 0;
1840
+ }
1841
+ function asBool(value) {
1842
+ return typeof value === "boolean" ? value : void 0;
1843
+ }
1844
+ function asNumber(value) {
1845
+ return typeof value === "number" ? value : void 0;
1846
+ }
1847
+ function asTimestamp(value) {
1848
+ if (typeof value === "string" || typeof value === "number" || value instanceof Date) {
1849
+ return value;
1850
+ }
1851
+ return void 0;
1852
+ }
1853
+
1854
+ // src/adapters/base-adapter.ts
1855
+ import { BaseCLIAdapter } from "adapter-types";
1856
+
1857
+ // src/adapters/adapter-factory.ts
1858
+ import { createAdapter } from "adapter-types";
1859
+
1860
+ // src/adapters/shell-adapter.ts
1861
+ var ShellAdapter = class {
1862
+ adapterType = "shell";
1863
+ displayName = "Shell";
1864
+ autoResponseRules = [];
1865
+ shell;
1866
+ promptStr;
1867
+ constructor(options = {}) {
1868
+ this.shell = options.shell || process.env.SHELL || "/bin/bash";
1869
+ this.promptStr = options.prompt || "pty> ";
1870
+ }
1871
+ getCommand() {
1872
+ return this.shell;
1873
+ }
1874
+ getArgs(_config) {
1875
+ if (this.shell.endsWith("/zsh") || this.shell === "zsh") {
1876
+ return ["-f"];
1877
+ }
1878
+ if (this.shell.endsWith("/bash") || this.shell === "bash") {
1879
+ return ["--norc", "--noprofile"];
1880
+ }
1881
+ return [];
1882
+ }
1883
+ getEnv(_config) {
1884
+ return {
1885
+ PS1: this.promptStr,
1886
+ PROMPT: this.promptStr
1887
+ // zsh uses PROMPT instead of PS1
1888
+ };
1889
+ }
1890
+ detectLogin(_output) {
1891
+ return { required: false };
1892
+ }
1893
+ detectBlockingPrompt(_output) {
1894
+ return { detected: false };
1895
+ }
1896
+ detectReady(output) {
1897
+ if (this.isContinuationPrompt(output)) {
1898
+ return false;
1899
+ }
1900
+ return this.getPromptPattern().test(this.stripAnsi(output));
1901
+ }
1902
+ isContinuationPrompt(output) {
1903
+ const stripped = this.stripAnsi(output);
1904
+ return /(?:quote|dquote|heredoc|bquote|cmdsubst|pipe|then|else|do|loop)>\s*$/.test(stripped) || /(?:quote|dquote|heredoc|bquote)>\s*$/m.test(stripped);
1905
+ }
1906
+ detectExit(output) {
1907
+ if (output.includes("exit")) {
1908
+ return { exited: true, code: 0 };
1909
+ }
1910
+ return { exited: false };
1911
+ }
1912
+ parseOutput(output) {
1913
+ const cleaned = this.stripAnsi(output).trim();
1914
+ if (!cleaned) return null;
1915
+ return {
1916
+ type: "response",
1917
+ content: cleaned,
1918
+ isComplete: true,
1919
+ isQuestion: false
1920
+ };
1921
+ }
1922
+ formatInput(message) {
1923
+ return message;
1924
+ }
1925
+ getPromptPattern() {
1926
+ const escaped = this.promptStr.trimEnd().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1927
+ return new RegExp(`(?:${escaped}|\\$|#)\\s*$`, "m");
1928
+ }
1929
+ async validateInstallation() {
1930
+ return { installed: true };
1931
+ }
1932
+ stripAnsi(str) {
1933
+ return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1934
+ }
1935
+ };
1936
+ export {
1937
+ AdapterRegistry,
1938
+ BaseCLIAdapter,
1939
+ SPECIAL_KEYS,
1940
+ ShellAdapter,
1941
+ TMUX_KEY_MAP,
1942
+ TmuxManager,
1943
+ TmuxSession,
1944
+ TmuxTransport,
1945
+ buildTaskCompletionTimeline,
1946
+ createAdapter,
1947
+ ensureTmux,
1948
+ extractTaskCompletionTraceRecords,
1949
+ resetTmuxCheck
1950
+ };
1951
+ //# sourceMappingURL=index.mjs.map