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.mjs
CHANGED
|
@@ -269,12 +269,14 @@ var SPECIAL_KEYS = {
|
|
|
269
269
|
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
270
270
|
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
271
271
|
var PTYSession = class extends EventEmitter {
|
|
272
|
-
constructor(adapter, config, logger) {
|
|
272
|
+
constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
|
|
273
273
|
super();
|
|
274
274
|
this.adapter = adapter;
|
|
275
275
|
this.id = config.id || generateId();
|
|
276
276
|
this.config = { ...config, id: this.id };
|
|
277
277
|
this.logger = logger || consoleLogger;
|
|
278
|
+
this._stallDetectionEnabled = stallDetectionEnabled ?? false;
|
|
279
|
+
this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
|
|
278
280
|
}
|
|
279
281
|
ptyProcess = null;
|
|
280
282
|
outputBuffer = "";
|
|
@@ -285,6 +287,12 @@ var PTYSession = class extends EventEmitter {
|
|
|
285
287
|
logger;
|
|
286
288
|
sessionRules = [];
|
|
287
289
|
_lastBlockingPromptHash = null;
|
|
290
|
+
// Stall detection
|
|
291
|
+
_stallTimer = null;
|
|
292
|
+
_stallTimeoutMs;
|
|
293
|
+
_stallDetectionEnabled;
|
|
294
|
+
_lastStallHash = null;
|
|
295
|
+
_stallStartedAt = null;
|
|
288
296
|
id;
|
|
289
297
|
config;
|
|
290
298
|
get status() {
|
|
@@ -366,6 +374,123 @@ var PTYSession = class extends EventEmitter {
|
|
|
366
374
|
this.logger.debug({ sessionId: this.id }, "Cleared auto-response rules");
|
|
367
375
|
}
|
|
368
376
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
377
|
+
// Stall Detection
|
|
378
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
/**
|
|
380
|
+
* Start or reset the stall detection timer.
|
|
381
|
+
* Only active when status is "busy" and stall detection is enabled.
|
|
382
|
+
*/
|
|
383
|
+
resetStallTimer() {
|
|
384
|
+
this.clearStallTimer();
|
|
385
|
+
if (!this._stallDetectionEnabled || this._status !== "busy") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this._stallStartedAt = Date.now();
|
|
389
|
+
this._lastStallHash = null;
|
|
390
|
+
this._stallTimer = setTimeout(() => {
|
|
391
|
+
this.onStallTimerFired();
|
|
392
|
+
}, this._stallTimeoutMs);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Clear the stall detection timer.
|
|
396
|
+
*/
|
|
397
|
+
clearStallTimer() {
|
|
398
|
+
if (this._stallTimer) {
|
|
399
|
+
clearTimeout(this._stallTimer);
|
|
400
|
+
this._stallTimer = null;
|
|
401
|
+
}
|
|
402
|
+
this._stallStartedAt = null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Called when the stall timer fires (no output for stallTimeoutMs).
|
|
406
|
+
*/
|
|
407
|
+
onStallTimerFired() {
|
|
408
|
+
if (this._status !== "busy") {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const tail = this.outputBuffer.slice(-500);
|
|
412
|
+
const hash = this.simpleHash(tail);
|
|
413
|
+
if (hash === this._lastStallHash) {
|
|
414
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this._lastStallHash = hash;
|
|
418
|
+
const recentRaw = this.outputBuffer.slice(-2e3);
|
|
419
|
+
const recentOutput = this.stripAnsiForStall(recentRaw);
|
|
420
|
+
const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
|
|
421
|
+
this.logger.debug(
|
|
422
|
+
{ sessionId: this.id, stallDurationMs, bufferTailLength: tail.length },
|
|
423
|
+
"Stall detected"
|
|
424
|
+
);
|
|
425
|
+
this.emit("stall_detected", recentOutput, stallDurationMs);
|
|
426
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Simple string hash for deduplication.
|
|
430
|
+
*/
|
|
431
|
+
simpleHash(str) {
|
|
432
|
+
let hash = 0;
|
|
433
|
+
for (let i = 0; i < str.length; i++) {
|
|
434
|
+
const char = str.charCodeAt(i);
|
|
435
|
+
hash = (hash << 5) - hash + char;
|
|
436
|
+
hash |= 0;
|
|
437
|
+
}
|
|
438
|
+
return hash.toString(36);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Strip ANSI codes for stall detection output.
|
|
442
|
+
* Replaces cursor-forward sequences with spaces first.
|
|
443
|
+
*/
|
|
444
|
+
stripAnsiForStall(str) {
|
|
445
|
+
const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
|
|
446
|
+
return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Handle external stall classification result.
|
|
450
|
+
* Called by the manager after onStallClassify resolves.
|
|
451
|
+
*/
|
|
452
|
+
handleStallClassification(classification) {
|
|
453
|
+
if (this._status !== "busy") {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!classification || classification.state === "still_working") {
|
|
457
|
+
this.resetStallTimer();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
switch (classification.state) {
|
|
461
|
+
case "waiting_for_input": {
|
|
462
|
+
const promptInfo = {
|
|
463
|
+
type: "stall_classified",
|
|
464
|
+
prompt: classification.prompt,
|
|
465
|
+
canAutoRespond: !!classification.suggestedResponse
|
|
466
|
+
};
|
|
467
|
+
if (classification.suggestedResponse) {
|
|
468
|
+
this.logger.info(
|
|
469
|
+
{ sessionId: this.id, response: classification.suggestedResponse },
|
|
470
|
+
"Auto-responding to stall-classified prompt"
|
|
471
|
+
);
|
|
472
|
+
this.writeRaw(classification.suggestedResponse + "\r");
|
|
473
|
+
this.emit("blocking_prompt", promptInfo, true);
|
|
474
|
+
} else {
|
|
475
|
+
this.emit("blocking_prompt", promptInfo, false);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case "task_complete":
|
|
480
|
+
this._status = "ready";
|
|
481
|
+
this._lastBlockingPromptHash = null;
|
|
482
|
+
this.outputBuffer = "";
|
|
483
|
+
this.clearStallTimer();
|
|
484
|
+
this.emit("ready");
|
|
485
|
+
this.logger.info({ sessionId: this.id }, "Stall classified as task_complete, transitioning to ready");
|
|
486
|
+
break;
|
|
487
|
+
case "error":
|
|
488
|
+
this.clearStallTimer();
|
|
489
|
+
this.emit("error", new Error(classification.prompt || "Stall classified as error"));
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
369
494
|
// Lifecycle
|
|
370
495
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
371
496
|
/**
|
|
@@ -423,11 +548,15 @@ var PTYSession = class extends EventEmitter {
|
|
|
423
548
|
this.ptyProcess.onData((data) => {
|
|
424
549
|
this._lastActivityAt = /* @__PURE__ */ new Date();
|
|
425
550
|
this.outputBuffer += data;
|
|
551
|
+
if (this._status === "busy") {
|
|
552
|
+
this.resetStallTimer();
|
|
553
|
+
}
|
|
426
554
|
this.emit("output", data);
|
|
427
555
|
if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
|
|
428
556
|
this._status = "ready";
|
|
429
557
|
this._lastBlockingPromptHash = null;
|
|
430
558
|
this.outputBuffer = "";
|
|
559
|
+
this.clearStallTimer();
|
|
431
560
|
this.emit("ready");
|
|
432
561
|
this.logger.info({ sessionId: this.id }, "Session ready");
|
|
433
562
|
return;
|
|
@@ -440,6 +569,7 @@ var PTYSession = class extends EventEmitter {
|
|
|
440
569
|
const loginDetection = this.adapter.detectLogin(this.outputBuffer);
|
|
441
570
|
if (loginDetection.required && this._status !== "authenticating") {
|
|
442
571
|
this._status = "authenticating";
|
|
572
|
+
this.clearStallTimer();
|
|
443
573
|
this.emit("login_required", loginDetection.instructions, loginDetection.url);
|
|
444
574
|
this.logger.warn(
|
|
445
575
|
{ sessionId: this.id, loginType: loginDetection.type },
|
|
@@ -451,6 +581,7 @@ var PTYSession = class extends EventEmitter {
|
|
|
451
581
|
const exitDetection = this.adapter.detectExit(this.outputBuffer);
|
|
452
582
|
if (exitDetection.exited) {
|
|
453
583
|
this._status = "stopped";
|
|
584
|
+
this.clearStallTimer();
|
|
454
585
|
this.emit("exit", exitDetection.code || 0);
|
|
455
586
|
}
|
|
456
587
|
if (this._status !== "starting" && this._status !== "authenticating") {
|
|
@@ -459,6 +590,7 @@ var PTYSession = class extends EventEmitter {
|
|
|
459
590
|
});
|
|
460
591
|
this.ptyProcess.onExit(({ exitCode, signal }) => {
|
|
461
592
|
this._status = "stopped";
|
|
593
|
+
this.clearStallTimer();
|
|
462
594
|
this.logger.info(
|
|
463
595
|
{ sessionId: this.id, exitCode, signal },
|
|
464
596
|
"PTY session exited"
|
|
@@ -534,8 +666,11 @@ var PTYSession = class extends EventEmitter {
|
|
|
534
666
|
if (allRules.length === 0) {
|
|
535
667
|
return false;
|
|
536
668
|
}
|
|
669
|
+
let stripped = this.stripAnsiForStall(this.outputBuffer);
|
|
670
|
+
stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
|
|
671
|
+
stripped = stripped.replace(/ {2,}/g, " ");
|
|
537
672
|
for (const rule of allRules) {
|
|
538
|
-
if (rule.pattern.test(
|
|
673
|
+
if (rule.pattern.test(stripped)) {
|
|
539
674
|
const safe = rule.safe !== false;
|
|
540
675
|
const isSessionRule = this.sessionRules.includes(rule);
|
|
541
676
|
if (safe) {
|
|
@@ -626,6 +761,7 @@ var PTYSession = class extends EventEmitter {
|
|
|
626
761
|
*/
|
|
627
762
|
send(message) {
|
|
628
763
|
this._status = "busy";
|
|
764
|
+
this.resetStallTimer();
|
|
629
765
|
const msg = {
|
|
630
766
|
id: `${this.id}-msg-${++this.messageCounter}`,
|
|
631
767
|
sessionId: this.id,
|
|
@@ -714,6 +850,7 @@ var PTYSession = class extends EventEmitter {
|
|
|
714
850
|
kill(signal) {
|
|
715
851
|
if (this.ptyProcess) {
|
|
716
852
|
this._status = "stopping";
|
|
853
|
+
this.clearStallTimer();
|
|
717
854
|
this.ptyProcess.kill(signal);
|
|
718
855
|
this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
|
|
719
856
|
}
|
|
@@ -783,11 +920,18 @@ var PTYManager = class extends EventEmitter2 {
|
|
|
783
920
|
maxLogLines;
|
|
784
921
|
logger;
|
|
785
922
|
adapters;
|
|
923
|
+
// Stall detection config
|
|
924
|
+
_stallDetectionEnabled;
|
|
925
|
+
_stallTimeoutMs;
|
|
926
|
+
_onStallClassify;
|
|
786
927
|
constructor(config = {}) {
|
|
787
928
|
super();
|
|
788
929
|
this.adapters = new AdapterRegistry();
|
|
789
930
|
this.logger = config.logger || consoleLogger2;
|
|
790
931
|
this.maxLogLines = config.maxLogLines || 1e3;
|
|
932
|
+
this._stallDetectionEnabled = config.stallDetectionEnabled ?? false;
|
|
933
|
+
this._stallTimeoutMs = config.stallTimeoutMs ?? 8e3;
|
|
934
|
+
this._onStallClassify = config.onStallClassify;
|
|
791
935
|
}
|
|
792
936
|
/**
|
|
793
937
|
* Register a CLI adapter
|
|
@@ -810,7 +954,13 @@ var PTYManager = class extends EventEmitter2 {
|
|
|
810
954
|
{ type: config.type, name: config.name },
|
|
811
955
|
"Spawning session"
|
|
812
956
|
);
|
|
813
|
-
const session = new PTYSession(
|
|
957
|
+
const session = new PTYSession(
|
|
958
|
+
adapter,
|
|
959
|
+
config,
|
|
960
|
+
this.logger,
|
|
961
|
+
this._stallDetectionEnabled,
|
|
962
|
+
this._stallTimeoutMs
|
|
963
|
+
);
|
|
814
964
|
this.setupSessionEvents(session);
|
|
815
965
|
this.sessions.set(session.id, session);
|
|
816
966
|
this.outputLogs.set(session.id, []);
|
|
@@ -854,6 +1004,21 @@ var PTYManager = class extends EventEmitter2 {
|
|
|
854
1004
|
session.on("error", (error) => {
|
|
855
1005
|
this.emit("session_error", session.toHandle(), error.message);
|
|
856
1006
|
});
|
|
1007
|
+
session.on("stall_detected", (recentOutput, stallDurationMs) => {
|
|
1008
|
+
const handle = session.toHandle();
|
|
1009
|
+
this.emit("stall_detected", handle, recentOutput, stallDurationMs);
|
|
1010
|
+
if (this._onStallClassify) {
|
|
1011
|
+
this._onStallClassify(session.id, recentOutput, stallDurationMs).then((classification) => {
|
|
1012
|
+
session.handleStallClassification(classification);
|
|
1013
|
+
}).catch((err) => {
|
|
1014
|
+
this.logger.error(
|
|
1015
|
+
{ sessionId: session.id, error: err },
|
|
1016
|
+
"Stall classification callback failed"
|
|
1017
|
+
);
|
|
1018
|
+
session.handleStallClassification(null);
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
857
1022
|
}
|
|
858
1023
|
/**
|
|
859
1024
|
* Stop a session
|
|
@@ -1025,6 +1190,22 @@ var PTYManager = class extends EventEmitter2 {
|
|
|
1025
1190
|
return this.sessions.get(sessionId);
|
|
1026
1191
|
}
|
|
1027
1192
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1193
|
+
// Stall Detection Configuration
|
|
1194
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1195
|
+
/**
|
|
1196
|
+
* Configure stall detection at runtime.
|
|
1197
|
+
* Affects newly spawned sessions only — existing sessions keep their config.
|
|
1198
|
+
*/
|
|
1199
|
+
configureStallDetection(enabled, timeoutMs, classify) {
|
|
1200
|
+
this._stallDetectionEnabled = enabled;
|
|
1201
|
+
if (timeoutMs !== void 0) {
|
|
1202
|
+
this._stallTimeoutMs = timeoutMs;
|
|
1203
|
+
}
|
|
1204
|
+
if (classify !== void 0) {
|
|
1205
|
+
this._onStallClassify = classify;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1028
1209
|
// Runtime Auto-Response Rules API
|
|
1029
1210
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1030
1211
|
/**
|
|
@@ -1114,7 +1295,9 @@ var BaseCLIAdapter = class {
|
|
|
1114
1295
|
* Subclasses should override for CLI-specific detection.
|
|
1115
1296
|
*/
|
|
1116
1297
|
detectBlockingPrompt(output) {
|
|
1117
|
-
|
|
1298
|
+
let stripped = this.stripAnsi(output);
|
|
1299
|
+
stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
|
|
1300
|
+
stripped = stripped.replace(/ {2,}/g, " ");
|
|
1118
1301
|
const loginDetection = this.detectLogin(output);
|
|
1119
1302
|
if (loginDetection.required) {
|
|
1120
1303
|
return {
|
|
@@ -1294,7 +1477,8 @@ var BaseCLIAdapter = class {
|
|
|
1294
1477
|
* Helper to strip ANSI escape codes from output
|
|
1295
1478
|
*/
|
|
1296
1479
|
stripAnsi(str) {
|
|
1297
|
-
|
|
1480
|
+
const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
|
|
1481
|
+
return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
1298
1482
|
}
|
|
1299
1483
|
};
|
|
1300
1484
|
|
|
@@ -1497,12 +1681,18 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
|
|
|
1497
1681
|
workerPath;
|
|
1498
1682
|
env;
|
|
1499
1683
|
adapterModules;
|
|
1684
|
+
_stallDetectionEnabled;
|
|
1685
|
+
_stallTimeoutMs;
|
|
1686
|
+
_onStallClassify;
|
|
1500
1687
|
constructor(options = {}) {
|
|
1501
1688
|
super();
|
|
1502
1689
|
this.nodePath = options.nodePath || "node";
|
|
1503
1690
|
this.workerPath = options.workerPath || this.findWorkerPath();
|
|
1504
1691
|
this.env = options.env || {};
|
|
1505
1692
|
this.adapterModules = options.adapterModules || [];
|
|
1693
|
+
this._stallDetectionEnabled = options.stallDetectionEnabled ?? false;
|
|
1694
|
+
this._stallTimeoutMs = options.stallTimeoutMs ?? 8e3;
|
|
1695
|
+
this._onStallClassify = options.onStallClassify;
|
|
1506
1696
|
this.readyPromise = new Promise((resolve) => {
|
|
1507
1697
|
this.readyResolve = resolve;
|
|
1508
1698
|
});
|
|
@@ -1564,6 +1754,13 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
|
|
|
1564
1754
|
if (this.adapterModules.length > 0) {
|
|
1565
1755
|
this.sendCommand({ cmd: "registerAdapters", modules: this.adapterModules });
|
|
1566
1756
|
}
|
|
1757
|
+
if (this._stallDetectionEnabled) {
|
|
1758
|
+
this.sendCommand({
|
|
1759
|
+
cmd: "configureStallDetection",
|
|
1760
|
+
enabled: true,
|
|
1761
|
+
timeoutMs: this._stallTimeoutMs
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1567
1764
|
this.ready = true;
|
|
1568
1765
|
this.readyResolve();
|
|
1569
1766
|
this.emit("ready");
|
|
@@ -1654,6 +1851,30 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
|
|
|
1654
1851
|
}
|
|
1655
1852
|
break;
|
|
1656
1853
|
}
|
|
1854
|
+
case "stall_detected": {
|
|
1855
|
+
const session = this.sessions.get(id);
|
|
1856
|
+
if (session) {
|
|
1857
|
+
const recentOutput = event.recentOutput;
|
|
1858
|
+
const stallDurationMs = event.stallDurationMs;
|
|
1859
|
+
this.emit("stall_detected", session, recentOutput, stallDurationMs);
|
|
1860
|
+
if (this._onStallClassify) {
|
|
1861
|
+
this._onStallClassify(id, recentOutput, stallDurationMs).then((classification) => {
|
|
1862
|
+
this.sendCommand({
|
|
1863
|
+
cmd: "classifyStallResult",
|
|
1864
|
+
id,
|
|
1865
|
+
classification
|
|
1866
|
+
});
|
|
1867
|
+
}).catch(() => {
|
|
1868
|
+
this.sendCommand({
|
|
1869
|
+
cmd: "classifyStallResult",
|
|
1870
|
+
id,
|
|
1871
|
+
classification: null
|
|
1872
|
+
});
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1657
1878
|
case "list": {
|
|
1658
1879
|
const sessions = event.sessions.map((s) => ({
|
|
1659
1880
|
...s,
|