pty-manager 1.2.20 → 1.2.22

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.d.mts CHANGED
@@ -37,6 +37,11 @@ interface SpawnConfig {
37
37
  adapterConfig?: Record<string, unknown>;
38
38
  /** Per-session stall timeout in ms. Overrides PTYManagerConfig.stallTimeoutMs. */
39
39
  stallTimeoutMs?: number;
40
+ /** Override or disable specific adapter auto-response rules for this session.
41
+ * Keys are regex source strings (from rule.pattern.source).
42
+ * - null value disables that rule entirely
43
+ * - Object value merges fields into the matching adapter rule */
44
+ ruleOverrides?: Record<string, Partial<Omit<AutoResponseRule, 'pattern'>> | null>;
40
45
  }
41
46
  /**
42
47
  * Handle to a running session
@@ -400,6 +405,8 @@ interface PTYSessionEvents {
400
405
  exit: (code: number) => void;
401
406
  error: (error: Error) => void;
402
407
  stall_detected: (recentOutput: string, stallDurationMs: number) => void;
408
+ status_changed: (status: SessionStatus) => void;
409
+ task_complete: () => void;
403
410
  }
404
411
  /**
405
412
  * Special key mappings to escape sequences
@@ -420,12 +427,16 @@ declare class PTYSession extends EventEmitter {
420
427
  private sessionRules;
421
428
  private _firedOnceRules;
422
429
  private _lastBlockingPromptHash;
430
+ private _ruleOverrides;
431
+ private _disabledRulePatterns;
423
432
  private _stallTimer;
424
433
  private _stallTimeoutMs;
425
434
  private _stallDetectionEnabled;
426
435
  private _lastStallHash;
427
436
  private _stallStartedAt;
428
437
  private _lastContentHash;
438
+ private _taskCompleteTimer;
439
+ private static readonly TASK_COMPLETE_DEBOUNCE_MS;
429
440
  readonly id: string;
430
441
  readonly config: SpawnConfig;
431
442
  constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
@@ -494,6 +505,17 @@ declare class PTYSession extends EventEmitter {
494
505
  * Called by the manager after onStallClassify resolves.
495
506
  */
496
507
  handleStallClassification(classification: StallClassification | null): void;
508
+ /**
509
+ * Schedule a task_complete transition after a debounce period.
510
+ * If new non-whitespace output arrives before the timer fires,
511
+ * the timer is cancelled (by cancelTaskComplete in onData).
512
+ */
513
+ private scheduleTaskComplete;
514
+ /**
515
+ * Cancel a pending task_complete timer (new output arrived that
516
+ * doesn't match the idle prompt, so the agent is still working).
517
+ */
518
+ private cancelTaskComplete;
497
519
  /**
498
520
  * Start the PTY session
499
521
  */
@@ -605,6 +627,8 @@ interface PTYManagerEvents {
605
627
  message: (message: SessionMessage) => void;
606
628
  question: (session: SessionHandle, question: string) => void;
607
629
  stall_detected: (session: SessionHandle, recentOutput: string, stallDurationMs: number) => void;
630
+ session_status_changed: (session: SessionHandle) => void;
631
+ task_complete: (session: SessionHandle) => void;
608
632
  }
609
633
  declare class PTYManager extends EventEmitter {
610
634
  private sessions;
package/dist/index.d.ts CHANGED
@@ -37,6 +37,11 @@ interface SpawnConfig {
37
37
  adapterConfig?: Record<string, unknown>;
38
38
  /** Per-session stall timeout in ms. Overrides PTYManagerConfig.stallTimeoutMs. */
39
39
  stallTimeoutMs?: number;
40
+ /** Override or disable specific adapter auto-response rules for this session.
41
+ * Keys are regex source strings (from rule.pattern.source).
42
+ * - null value disables that rule entirely
43
+ * - Object value merges fields into the matching adapter rule */
44
+ ruleOverrides?: Record<string, Partial<Omit<AutoResponseRule, 'pattern'>> | null>;
40
45
  }
41
46
  /**
42
47
  * Handle to a running session
@@ -400,6 +405,8 @@ interface PTYSessionEvents {
400
405
  exit: (code: number) => void;
401
406
  error: (error: Error) => void;
402
407
  stall_detected: (recentOutput: string, stallDurationMs: number) => void;
408
+ status_changed: (status: SessionStatus) => void;
409
+ task_complete: () => void;
403
410
  }
404
411
  /**
405
412
  * Special key mappings to escape sequences
@@ -420,12 +427,16 @@ declare class PTYSession extends EventEmitter {
420
427
  private sessionRules;
421
428
  private _firedOnceRules;
422
429
  private _lastBlockingPromptHash;
430
+ private _ruleOverrides;
431
+ private _disabledRulePatterns;
423
432
  private _stallTimer;
424
433
  private _stallTimeoutMs;
425
434
  private _stallDetectionEnabled;
426
435
  private _lastStallHash;
427
436
  private _stallStartedAt;
428
437
  private _lastContentHash;
438
+ private _taskCompleteTimer;
439
+ private static readonly TASK_COMPLETE_DEBOUNCE_MS;
429
440
  readonly id: string;
430
441
  readonly config: SpawnConfig;
431
442
  constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
@@ -494,6 +505,17 @@ declare class PTYSession extends EventEmitter {
494
505
  * Called by the manager after onStallClassify resolves.
495
506
  */
496
507
  handleStallClassification(classification: StallClassification | null): void;
508
+ /**
509
+ * Schedule a task_complete transition after a debounce period.
510
+ * If new non-whitespace output arrives before the timer fires,
511
+ * the timer is cancelled (by cancelTaskComplete in onData).
512
+ */
513
+ private scheduleTaskComplete;
514
+ /**
515
+ * Cancel a pending task_complete timer (new output arrived that
516
+ * doesn't match the idle prompt, so the agent is still working).
517
+ */
518
+ private cancelTaskComplete;
497
519
  /**
498
520
  * Start the PTY session
499
521
  */
@@ -605,6 +627,8 @@ interface PTYManagerEvents {
605
627
  message: (message: SessionMessage) => void;
606
628
  question: (session: SessionHandle, question: string) => void;
607
629
  stall_detected: (session: SessionHandle, recentOutput: string, stallDurationMs: number) => void;
630
+ session_status_changed: (session: SessionHandle) => void;
631
+ task_complete: (session: SessionHandle) => void;
608
632
  }
609
633
  declare class PTYManager extends EventEmitter {
610
634
  private sessions;
package/dist/index.js CHANGED
@@ -306,7 +306,7 @@ var SPECIAL_KEYS = {
306
306
  };
307
307
  var BRACKETED_PASTE_START = "\x1B[200~";
308
308
  var BRACKETED_PASTE_END = "\x1B[201~";
309
- var PTYSession = class extends import_events.EventEmitter {
309
+ var PTYSession = class _PTYSession extends import_events.EventEmitter {
310
310
  constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
311
311
  super();
312
312
  this.adapter = adapter;
@@ -315,6 +315,15 @@ var PTYSession = class extends import_events.EventEmitter {
315
315
  this.logger = logger || consoleLogger;
316
316
  this._stallDetectionEnabled = stallDetectionEnabled ?? false;
317
317
  this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
318
+ if (config.ruleOverrides) {
319
+ for (const [key, value] of Object.entries(config.ruleOverrides)) {
320
+ if (value === null) {
321
+ this._disabledRulePatterns.add(key);
322
+ } else {
323
+ this._ruleOverrides.set(key, value);
324
+ }
325
+ }
326
+ }
318
327
  }
319
328
  ptyProcess = null;
320
329
  outputBuffer = "";
@@ -326,6 +335,8 @@ var PTYSession = class extends import_events.EventEmitter {
326
335
  sessionRules = [];
327
336
  _firedOnceRules = /* @__PURE__ */ new Set();
328
337
  _lastBlockingPromptHash = null;
338
+ _ruleOverrides = /* @__PURE__ */ new Map();
339
+ _disabledRulePatterns = /* @__PURE__ */ new Set();
329
340
  // Stall detection
330
341
  _stallTimer = null;
331
342
  _stallTimeoutMs;
@@ -333,6 +344,9 @@ var PTYSession = class extends import_events.EventEmitter {
333
344
  _lastStallHash = null;
334
345
  _stallStartedAt = null;
335
346
  _lastContentHash = null;
347
+ // Task completion detection (idle detection when busy)
348
+ _taskCompleteTimer = null;
349
+ static TASK_COMPLETE_DEBOUNCE_MS = 1500;
336
350
  id;
337
351
  config;
338
352
  get status() {
@@ -508,10 +522,12 @@ var PTYSession = class extends import_events.EventEmitter {
508
522
  * word boundaries — e.g. "Do\x1b[5Cyou" becomes "Do you", not "Doyou".
509
523
  */
510
524
  stripAnsiForStall(str) {
511
- let result = str.replace(/\x1b\[\d*[CDABG]/g, " ");
525
+ let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
512
526
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
527
+ result = result.replace(/\x1b\[\d*[JK]/g, " ");
513
528
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
514
- result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓]/g, " ");
529
+ result = result.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
530
+ result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
515
531
  result = result.replace(/ {2,}/g, " ");
516
532
  return result;
517
533
  }
@@ -548,6 +564,7 @@ var PTYSession = class extends import_events.EventEmitter {
548
564
  this.writeRaw(resp + "\r");
549
565
  }
550
566
  this.emit("blocking_prompt", promptInfo, true);
567
+ this.outputBuffer = "";
551
568
  } else {
552
569
  this.emit("blocking_prompt", promptInfo, false);
553
570
  }
@@ -568,6 +585,39 @@ var PTYSession = class extends import_events.EventEmitter {
568
585
  }
569
586
  }
570
587
  // ─────────────────────────────────────────────────────────────────────────────
588
+ // Task Completion Detection
589
+ // ─────────────────────────────────────────────────────────────────────────────
590
+ /**
591
+ * Schedule a task_complete transition after a debounce period.
592
+ * If new non-whitespace output arrives before the timer fires,
593
+ * the timer is cancelled (by cancelTaskComplete in onData).
594
+ */
595
+ scheduleTaskComplete() {
596
+ if (this._taskCompleteTimer) return;
597
+ this._taskCompleteTimer = setTimeout(() => {
598
+ this._taskCompleteTimer = null;
599
+ if (this._status !== "busy") return;
600
+ if (!this.adapter.detectReady(this.outputBuffer)) return;
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 \u2014 agent returned to idle prompt");
608
+ }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
609
+ }
610
+ /**
611
+ * Cancel a pending task_complete timer (new output arrived that
612
+ * doesn't match the idle prompt, so the agent is still working).
613
+ */
614
+ cancelTaskComplete() {
615
+ if (this._taskCompleteTimer) {
616
+ clearTimeout(this._taskCompleteTimer);
617
+ this._taskCompleteTimer = null;
618
+ }
619
+ }
620
+ // ─────────────────────────────────────────────────────────────────────────────
571
621
  // Lifecycle
572
622
  // ─────────────────────────────────────────────────────────────────────────────
573
623
  /**
@@ -629,10 +679,6 @@ var PTYSession = class extends import_events.EventEmitter {
629
679
  this.resetStallTimer();
630
680
  }
631
681
  this.emit("output", data);
632
- const blockingPrompt = this.detectAndHandleBlockingPrompt();
633
- if (blockingPrompt) {
634
- return;
635
- }
636
682
  if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
637
683
  this._status = "ready";
638
684
  this._lastBlockingPromptHash = null;
@@ -642,6 +688,15 @@ var PTYSession = class extends import_events.EventEmitter {
642
688
  this.logger.info({ sessionId: this.id }, "Session ready");
643
689
  return;
644
690
  }
691
+ if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
692
+ this.scheduleTaskComplete();
693
+ } else {
694
+ this.cancelTaskComplete();
695
+ }
696
+ const blockingPrompt = this.detectAndHandleBlockingPrompt();
697
+ if (blockingPrompt) {
698
+ return;
699
+ }
645
700
  if (this._status !== "ready" && this._status !== "busy") {
646
701
  const loginDetection = this.adapter.detectLogin(this.outputBuffer);
647
702
  if (loginDetection.required && this._status !== "authenticating") {
@@ -717,6 +772,7 @@ var PTYSession = class extends import_events.EventEmitter {
717
772
  this.writeRaw(resp + "\r");
718
773
  }
719
774
  this._lastBlockingPromptHash = null;
775
+ this.outputBuffer = "";
720
776
  this.emit("blocking_prompt", promptInfo, true);
721
777
  return true;
722
778
  }
@@ -744,7 +800,10 @@ var PTYSession = class extends import_events.EventEmitter {
744
800
  * Session rules are checked first, then adapter rules.
745
801
  */
746
802
  tryAutoResponse() {
747
- const adapterRules = this.adapter.autoResponseRules || [];
803
+ const adapterRules = (this.adapter.autoResponseRules || []).filter((r) => !this._disabledRulePatterns.has(r.pattern.source)).map((r) => {
804
+ const override = this._ruleOverrides.get(r.pattern.source);
805
+ return override ? { ...r, ...override } : r;
806
+ });
748
807
  const allRules = [...this.sessionRules, ...adapterRules];
749
808
  if (allRules.length === 0) {
750
809
  return false;
@@ -860,6 +919,7 @@ var PTYSession = class extends import_events.EventEmitter {
860
919
  */
861
920
  send(message) {
862
921
  this._status = "busy";
922
+ this.emit("status_changed", "busy");
863
923
  this.resetStallTimer();
864
924
  const msg = {
865
925
  id: `${this.id}-msg-${++this.messageCounter}`,
@@ -970,6 +1030,7 @@ var PTYSession = class extends import_events.EventEmitter {
970
1030
  if (this.ptyProcess) {
971
1031
  this._status = "stopping";
972
1032
  this.clearStallTimer();
1033
+ this.cancelTaskComplete();
973
1034
  this.ptyProcess.kill(signal);
974
1035
  this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
975
1036
  }
@@ -1123,6 +1184,12 @@ var PTYManager = class extends import_events2.EventEmitter {
1123
1184
  session.on("error", (error) => {
1124
1185
  this.emit("session_error", session.toHandle(), error.message);
1125
1186
  });
1187
+ session.on("status_changed", () => {
1188
+ this.emit("session_status_changed", session.toHandle());
1189
+ });
1190
+ session.on("task_complete", () => {
1191
+ this.emit("task_complete", session.toHandle());
1192
+ });
1126
1193
  session.on("stall_detected", (recentOutput, stallDurationMs) => {
1127
1194
  const handle = session.toHandle();
1128
1195
  this.emit("stall_detected", handle, recentOutput, stallDurationMs);
@@ -1976,6 +2043,24 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
1976
2043
  }
1977
2044
  break;
1978
2045
  }
2046
+ case "status_changed": {
2047
+ const session = this.sessions.get(id);
2048
+ if (session) {
2049
+ session.status = event.status;
2050
+ session.lastActivityAt = /* @__PURE__ */ new Date();
2051
+ this.emit("session_status_changed", session);
2052
+ }
2053
+ break;
2054
+ }
2055
+ case "task_complete": {
2056
+ const session = this.sessions.get(id);
2057
+ if (session) {
2058
+ session.status = "ready";
2059
+ session.lastActivityAt = /* @__PURE__ */ new Date();
2060
+ this.emit("task_complete", session);
2061
+ }
2062
+ break;
2063
+ }
1979
2064
  case "stall_detected": {
1980
2065
  const session = this.sessions.get(id);
1981
2066
  if (session) {