pty-manager 1.2.12 → 1.2.14
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 +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +226 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +226 -5
- package/dist/index.mjs.map +1 -1
- package/dist/pty-worker.js +223 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -307,12 +307,14 @@ var SPECIAL_KEYS = {
|
|
|
307
307
|
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
308
308
|
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
309
309
|
var PTYSession = class extends import_events.EventEmitter {
|
|
310
|
-
constructor(adapter, config, logger) {
|
|
310
|
+
constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
|
|
311
311
|
super();
|
|
312
312
|
this.adapter = adapter;
|
|
313
313
|
this.id = config.id || generateId();
|
|
314
314
|
this.config = { ...config, id: this.id };
|
|
315
315
|
this.logger = logger || consoleLogger;
|
|
316
|
+
this._stallDetectionEnabled = stallDetectionEnabled ?? false;
|
|
317
|
+
this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
|
|
316
318
|
}
|
|
317
319
|
ptyProcess = null;
|
|
318
320
|
outputBuffer = "";
|
|
@@ -323,6 +325,12 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
323
325
|
logger;
|
|
324
326
|
sessionRules = [];
|
|
325
327
|
_lastBlockingPromptHash = null;
|
|
328
|
+
// Stall detection
|
|
329
|
+
_stallTimer = null;
|
|
330
|
+
_stallTimeoutMs;
|
|
331
|
+
_stallDetectionEnabled;
|
|
332
|
+
_lastStallHash = null;
|
|
333
|
+
_stallStartedAt = null;
|
|
326
334
|
id;
|
|
327
335
|
config;
|
|
328
336
|
get status() {
|
|
@@ -404,6 +412,123 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
404
412
|
this.logger.debug({ sessionId: this.id }, "Cleared auto-response rules");
|
|
405
413
|
}
|
|
406
414
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
415
|
+
// Stall Detection
|
|
416
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
417
|
+
/**
|
|
418
|
+
* Start or reset the stall detection timer.
|
|
419
|
+
* Only active when status is "busy" and stall detection is enabled.
|
|
420
|
+
*/
|
|
421
|
+
resetStallTimer() {
|
|
422
|
+
this.clearStallTimer();
|
|
423
|
+
if (!this._stallDetectionEnabled || this._status !== "busy") {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
this._stallStartedAt = Date.now();
|
|
427
|
+
this._lastStallHash = null;
|
|
428
|
+
this._stallTimer = setTimeout(() => {
|
|
429
|
+
this.onStallTimerFired();
|
|
430
|
+
}, this._stallTimeoutMs);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Clear the stall detection timer.
|
|
434
|
+
*/
|
|
435
|
+
clearStallTimer() {
|
|
436
|
+
if (this._stallTimer) {
|
|
437
|
+
clearTimeout(this._stallTimer);
|
|
438
|
+
this._stallTimer = null;
|
|
439
|
+
}
|
|
440
|
+
this._stallStartedAt = null;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Called when the stall timer fires (no output for stallTimeoutMs).
|
|
444
|
+
*/
|
|
445
|
+
onStallTimerFired() {
|
|
446
|
+
if (this._status !== "busy") {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const tail = this.outputBuffer.slice(-500);
|
|
450
|
+
const hash = this.simpleHash(tail);
|
|
451
|
+
if (hash === this._lastStallHash) {
|
|
452
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this._lastStallHash = hash;
|
|
456
|
+
const recentRaw = this.outputBuffer.slice(-2e3);
|
|
457
|
+
const recentOutput = this.stripAnsiForStall(recentRaw);
|
|
458
|
+
const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
|
|
459
|
+
this.logger.debug(
|
|
460
|
+
{ sessionId: this.id, stallDurationMs, bufferTailLength: tail.length },
|
|
461
|
+
"Stall detected"
|
|
462
|
+
);
|
|
463
|
+
this.emit("stall_detected", recentOutput, stallDurationMs);
|
|
464
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Simple string hash for deduplication.
|
|
468
|
+
*/
|
|
469
|
+
simpleHash(str) {
|
|
470
|
+
let hash = 0;
|
|
471
|
+
for (let i = 0; i < str.length; i++) {
|
|
472
|
+
const char = str.charCodeAt(i);
|
|
473
|
+
hash = (hash << 5) - hash + char;
|
|
474
|
+
hash |= 0;
|
|
475
|
+
}
|
|
476
|
+
return hash.toString(36);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Strip ANSI codes for stall detection output.
|
|
480
|
+
* Replaces cursor-forward sequences with spaces first.
|
|
481
|
+
*/
|
|
482
|
+
stripAnsiForStall(str) {
|
|
483
|
+
const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
|
|
484
|
+
return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Handle external stall classification result.
|
|
488
|
+
* Called by the manager after onStallClassify resolves.
|
|
489
|
+
*/
|
|
490
|
+
handleStallClassification(classification) {
|
|
491
|
+
if (this._status !== "busy") {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (!classification || classification.state === "still_working") {
|
|
495
|
+
this.resetStallTimer();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
switch (classification.state) {
|
|
499
|
+
case "waiting_for_input": {
|
|
500
|
+
const promptInfo = {
|
|
501
|
+
type: "stall_classified",
|
|
502
|
+
prompt: classification.prompt,
|
|
503
|
+
canAutoRespond: !!classification.suggestedResponse
|
|
504
|
+
};
|
|
505
|
+
if (classification.suggestedResponse) {
|
|
506
|
+
this.logger.info(
|
|
507
|
+
{ sessionId: this.id, response: classification.suggestedResponse },
|
|
508
|
+
"Auto-responding to stall-classified prompt"
|
|
509
|
+
);
|
|
510
|
+
this.writeRaw(classification.suggestedResponse + "\r");
|
|
511
|
+
this.emit("blocking_prompt", promptInfo, true);
|
|
512
|
+
} else {
|
|
513
|
+
this.emit("blocking_prompt", promptInfo, false);
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "task_complete":
|
|
518
|
+
this._status = "ready";
|
|
519
|
+
this._lastBlockingPromptHash = null;
|
|
520
|
+
this.outputBuffer = "";
|
|
521
|
+
this.clearStallTimer();
|
|
522
|
+
this.emit("ready");
|
|
523
|
+
this.logger.info({ sessionId: this.id }, "Stall classified as task_complete, transitioning to ready");
|
|
524
|
+
break;
|
|
525
|
+
case "error":
|
|
526
|
+
this.clearStallTimer();
|
|
527
|
+
this.emit("error", new Error(classification.prompt || "Stall classified as error"));
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
407
532
|
// Lifecycle
|
|
408
533
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
409
534
|
/**
|
|
@@ -461,11 +586,15 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
461
586
|
this.ptyProcess.onData((data) => {
|
|
462
587
|
this._lastActivityAt = /* @__PURE__ */ new Date();
|
|
463
588
|
this.outputBuffer += data;
|
|
589
|
+
if (this._status === "busy") {
|
|
590
|
+
this.resetStallTimer();
|
|
591
|
+
}
|
|
464
592
|
this.emit("output", data);
|
|
465
593
|
if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
|
|
466
594
|
this._status = "ready";
|
|
467
595
|
this._lastBlockingPromptHash = null;
|
|
468
596
|
this.outputBuffer = "";
|
|
597
|
+
this.clearStallTimer();
|
|
469
598
|
this.emit("ready");
|
|
470
599
|
this.logger.info({ sessionId: this.id }, "Session ready");
|
|
471
600
|
return;
|
|
@@ -478,6 +607,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
478
607
|
const loginDetection = this.adapter.detectLogin(this.outputBuffer);
|
|
479
608
|
if (loginDetection.required && this._status !== "authenticating") {
|
|
480
609
|
this._status = "authenticating";
|
|
610
|
+
this.clearStallTimer();
|
|
481
611
|
this.emit("login_required", loginDetection.instructions, loginDetection.url);
|
|
482
612
|
this.logger.warn(
|
|
483
613
|
{ sessionId: this.id, loginType: loginDetection.type },
|
|
@@ -489,6 +619,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
489
619
|
const exitDetection = this.adapter.detectExit(this.outputBuffer);
|
|
490
620
|
if (exitDetection.exited) {
|
|
491
621
|
this._status = "stopped";
|
|
622
|
+
this.clearStallTimer();
|
|
492
623
|
this.emit("exit", exitDetection.code || 0);
|
|
493
624
|
}
|
|
494
625
|
if (this._status !== "starting" && this._status !== "authenticating") {
|
|
@@ -497,6 +628,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
497
628
|
});
|
|
498
629
|
this.ptyProcess.onExit(({ exitCode, signal }) => {
|
|
499
630
|
this._status = "stopped";
|
|
631
|
+
this.clearStallTimer();
|
|
500
632
|
this.logger.info(
|
|
501
633
|
{ sessionId: this.id, exitCode, signal },
|
|
502
634
|
"PTY session exited"
|
|
@@ -572,8 +704,11 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
572
704
|
if (allRules.length === 0) {
|
|
573
705
|
return false;
|
|
574
706
|
}
|
|
707
|
+
let stripped = this.stripAnsiForStall(this.outputBuffer);
|
|
708
|
+
stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
|
|
709
|
+
stripped = stripped.replace(/ {2,}/g, " ");
|
|
575
710
|
for (const rule of allRules) {
|
|
576
|
-
if (rule.pattern.test(
|
|
711
|
+
if (rule.pattern.test(stripped)) {
|
|
577
712
|
const safe = rule.safe !== false;
|
|
578
713
|
const isSessionRule = this.sessionRules.includes(rule);
|
|
579
714
|
if (safe) {
|
|
@@ -664,6 +799,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
664
799
|
*/
|
|
665
800
|
send(message) {
|
|
666
801
|
this._status = "busy";
|
|
802
|
+
this.resetStallTimer();
|
|
667
803
|
const msg = {
|
|
668
804
|
id: `${this.id}-msg-${++this.messageCounter}`,
|
|
669
805
|
sessionId: this.id,
|
|
@@ -752,6 +888,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
752
888
|
kill(signal) {
|
|
753
889
|
if (this.ptyProcess) {
|
|
754
890
|
this._status = "stopping";
|
|
891
|
+
this.clearStallTimer();
|
|
755
892
|
this.ptyProcess.kill(signal);
|
|
756
893
|
this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
|
|
757
894
|
}
|
|
@@ -821,11 +958,18 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
821
958
|
maxLogLines;
|
|
822
959
|
logger;
|
|
823
960
|
adapters;
|
|
961
|
+
// Stall detection config
|
|
962
|
+
_stallDetectionEnabled;
|
|
963
|
+
_stallTimeoutMs;
|
|
964
|
+
_onStallClassify;
|
|
824
965
|
constructor(config = {}) {
|
|
825
966
|
super();
|
|
826
967
|
this.adapters = new AdapterRegistry();
|
|
827
968
|
this.logger = config.logger || consoleLogger2;
|
|
828
969
|
this.maxLogLines = config.maxLogLines || 1e3;
|
|
970
|
+
this._stallDetectionEnabled = config.stallDetectionEnabled ?? false;
|
|
971
|
+
this._stallTimeoutMs = config.stallTimeoutMs ?? 8e3;
|
|
972
|
+
this._onStallClassify = config.onStallClassify;
|
|
829
973
|
}
|
|
830
974
|
/**
|
|
831
975
|
* Register a CLI adapter
|
|
@@ -848,7 +992,13 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
848
992
|
{ type: config.type, name: config.name },
|
|
849
993
|
"Spawning session"
|
|
850
994
|
);
|
|
851
|
-
const session = new PTYSession(
|
|
995
|
+
const session = new PTYSession(
|
|
996
|
+
adapter,
|
|
997
|
+
config,
|
|
998
|
+
this.logger,
|
|
999
|
+
this._stallDetectionEnabled,
|
|
1000
|
+
this._stallTimeoutMs
|
|
1001
|
+
);
|
|
852
1002
|
this.setupSessionEvents(session);
|
|
853
1003
|
this.sessions.set(session.id, session);
|
|
854
1004
|
this.outputLogs.set(session.id, []);
|
|
@@ -892,6 +1042,21 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
892
1042
|
session.on("error", (error) => {
|
|
893
1043
|
this.emit("session_error", session.toHandle(), error.message);
|
|
894
1044
|
});
|
|
1045
|
+
session.on("stall_detected", (recentOutput, stallDurationMs) => {
|
|
1046
|
+
const handle = session.toHandle();
|
|
1047
|
+
this.emit("stall_detected", handle, recentOutput, stallDurationMs);
|
|
1048
|
+
if (this._onStallClassify) {
|
|
1049
|
+
this._onStallClassify(session.id, recentOutput, stallDurationMs).then((classification) => {
|
|
1050
|
+
session.handleStallClassification(classification);
|
|
1051
|
+
}).catch((err) => {
|
|
1052
|
+
this.logger.error(
|
|
1053
|
+
{ sessionId: session.id, error: err },
|
|
1054
|
+
"Stall classification callback failed"
|
|
1055
|
+
);
|
|
1056
|
+
session.handleStallClassification(null);
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
895
1060
|
}
|
|
896
1061
|
/**
|
|
897
1062
|
* Stop a session
|
|
@@ -1063,6 +1228,22 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
1063
1228
|
return this.sessions.get(sessionId);
|
|
1064
1229
|
}
|
|
1065
1230
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1231
|
+
// Stall Detection Configuration
|
|
1232
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1233
|
+
/**
|
|
1234
|
+
* Configure stall detection at runtime.
|
|
1235
|
+
* Affects newly spawned sessions only — existing sessions keep their config.
|
|
1236
|
+
*/
|
|
1237
|
+
configureStallDetection(enabled, timeoutMs, classify) {
|
|
1238
|
+
this._stallDetectionEnabled = enabled;
|
|
1239
|
+
if (timeoutMs !== void 0) {
|
|
1240
|
+
this._stallTimeoutMs = timeoutMs;
|
|
1241
|
+
}
|
|
1242
|
+
if (classify !== void 0) {
|
|
1243
|
+
this._onStallClassify = classify;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1066
1247
|
// Runtime Auto-Response Rules API
|
|
1067
1248
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1068
1249
|
/**
|
|
@@ -1152,7 +1333,9 @@ var BaseCLIAdapter = class {
|
|
|
1152
1333
|
* Subclasses should override for CLI-specific detection.
|
|
1153
1334
|
*/
|
|
1154
1335
|
detectBlockingPrompt(output) {
|
|
1155
|
-
|
|
1336
|
+
let stripped = this.stripAnsi(output);
|
|
1337
|
+
stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
|
|
1338
|
+
stripped = stripped.replace(/ {2,}/g, " ");
|
|
1156
1339
|
const loginDetection = this.detectLogin(output);
|
|
1157
1340
|
if (loginDetection.required) {
|
|
1158
1341
|
return {
|
|
@@ -1332,7 +1515,8 @@ var BaseCLIAdapter = class {
|
|
|
1332
1515
|
* Helper to strip ANSI escape codes from output
|
|
1333
1516
|
*/
|
|
1334
1517
|
stripAnsi(str) {
|
|
1335
|
-
|
|
1518
|
+
const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
|
|
1519
|
+
return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
1336
1520
|
}
|
|
1337
1521
|
};
|
|
1338
1522
|
|
|
@@ -1535,12 +1719,18 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
|
|
|
1535
1719
|
workerPath;
|
|
1536
1720
|
env;
|
|
1537
1721
|
adapterModules;
|
|
1722
|
+
_stallDetectionEnabled;
|
|
1723
|
+
_stallTimeoutMs;
|
|
1724
|
+
_onStallClassify;
|
|
1538
1725
|
constructor(options = {}) {
|
|
1539
1726
|
super();
|
|
1540
1727
|
this.nodePath = options.nodePath || "node";
|
|
1541
1728
|
this.workerPath = options.workerPath || this.findWorkerPath();
|
|
1542
1729
|
this.env = options.env || {};
|
|
1543
1730
|
this.adapterModules = options.adapterModules || [];
|
|
1731
|
+
this._stallDetectionEnabled = options.stallDetectionEnabled ?? false;
|
|
1732
|
+
this._stallTimeoutMs = options.stallTimeoutMs ?? 8e3;
|
|
1733
|
+
this._onStallClassify = options.onStallClassify;
|
|
1544
1734
|
this.readyPromise = new Promise((resolve) => {
|
|
1545
1735
|
this.readyResolve = resolve;
|
|
1546
1736
|
});
|
|
@@ -1602,6 +1792,13 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
|
|
|
1602
1792
|
if (this.adapterModules.length > 0) {
|
|
1603
1793
|
this.sendCommand({ cmd: "registerAdapters", modules: this.adapterModules });
|
|
1604
1794
|
}
|
|
1795
|
+
if (this._stallDetectionEnabled) {
|
|
1796
|
+
this.sendCommand({
|
|
1797
|
+
cmd: "configureStallDetection",
|
|
1798
|
+
enabled: true,
|
|
1799
|
+
timeoutMs: this._stallTimeoutMs
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1605
1802
|
this.ready = true;
|
|
1606
1803
|
this.readyResolve();
|
|
1607
1804
|
this.emit("ready");
|
|
@@ -1692,6 +1889,30 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
|
|
|
1692
1889
|
}
|
|
1693
1890
|
break;
|
|
1694
1891
|
}
|
|
1892
|
+
case "stall_detected": {
|
|
1893
|
+
const session = this.sessions.get(id);
|
|
1894
|
+
if (session) {
|
|
1895
|
+
const recentOutput = event.recentOutput;
|
|
1896
|
+
const stallDurationMs = event.stallDurationMs;
|
|
1897
|
+
this.emit("stall_detected", session, recentOutput, stallDurationMs);
|
|
1898
|
+
if (this._onStallClassify) {
|
|
1899
|
+
this._onStallClassify(id, recentOutput, stallDurationMs).then((classification) => {
|
|
1900
|
+
this.sendCommand({
|
|
1901
|
+
cmd: "classifyStallResult",
|
|
1902
|
+
id,
|
|
1903
|
+
classification
|
|
1904
|
+
});
|
|
1905
|
+
}).catch(() => {
|
|
1906
|
+
this.sendCommand({
|
|
1907
|
+
cmd: "classifyStallResult",
|
|
1908
|
+
id,
|
|
1909
|
+
classification: null
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1695
1916
|
case "list": {
|
|
1696
1917
|
const sessions = event.sessions.map((s) => ({
|
|
1697
1918
|
...s,
|