@co0ontty/wand 1.3.4 → 1.4.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/auth.js +2 -1
- package/dist/claude-pty-bridge.d.ts +38 -0
- package/dist/claude-pty-bridge.js +224 -9
- package/dist/cli.js +2 -2
- package/dist/middleware/rate-limit.js +2 -1
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +96 -178
- package/dist/pty-text-utils.d.ts +12 -0
- package/dist/pty-text-utils.js +37 -1
- package/dist/server-session-routes.d.ts +3 -1
- package/dist/server-session-routes.js +114 -10
- package/dist/server.js +14 -34
- package/dist/session-lifecycle.js +0 -5
- package/dist/session-logger.d.ts +10 -0
- package/dist/storage.js +42 -8
- package/dist/structured-session-manager.d.ts +55 -0
- package/dist/structured-session-manager.js +723 -0
- package/dist/types.d.ts +22 -0
- package/dist/web-ui/content/scripts.js +746 -102
- package/dist/web-ui/content/styles.css +275 -9
- package/package.json +2 -1
package/dist/auth.js
CHANGED
|
@@ -3,9 +3,10 @@ const sessions = new Map();
|
|
|
3
3
|
const SESSION_TTL_MS = 1000 * 60 * 60 * 12;
|
|
4
4
|
let storage = null;
|
|
5
5
|
// Periodic cleanup every 10 minutes
|
|
6
|
-
setInterval(() => {
|
|
6
|
+
const sessionCleanupTimer = setInterval(() => {
|
|
7
7
|
cleanupExpiredSessions();
|
|
8
8
|
}, 1000 * 60 * 10);
|
|
9
|
+
sessionCleanupTimer.unref();
|
|
9
10
|
export function createSession() {
|
|
10
11
|
const token = crypto.randomBytes(24).toString("hex");
|
|
11
12
|
const expiresAt = Date.now() + SESSION_TTL_MS;
|
|
@@ -23,6 +23,18 @@ interface PermissionState {
|
|
|
23
23
|
lastAutoConfirmAt: number;
|
|
24
24
|
/** Timer for delayed auto-approve (gives CLI time to be ready) */
|
|
25
25
|
pendingAutoApproveTimer: ReturnType<typeof setTimeout> | null;
|
|
26
|
+
/** Fallback auto-approve: verification deadline timestamp */
|
|
27
|
+
fallbackVerifyUntil: number;
|
|
28
|
+
/** Fallback auto-approve: context for logging */
|
|
29
|
+
fallbackContext: {
|
|
30
|
+
score: number;
|
|
31
|
+
matched: string[];
|
|
32
|
+
text: string;
|
|
33
|
+
} | null;
|
|
34
|
+
/** Output length snapshot taken right before fallback auto-approve fires */
|
|
35
|
+
fallbackOutputLenAtApprove: number;
|
|
36
|
+
/** Consecutive auto-approve attempts for the same prompt without resolution */
|
|
37
|
+
retryCount: number;
|
|
26
38
|
}
|
|
27
39
|
/** Permission resolution result */
|
|
28
40
|
export type PermissionResolution = "approve_once" | "approve_turn" | "deny";
|
|
@@ -73,6 +85,10 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
73
85
|
private _exited;
|
|
74
86
|
private rememberedScopes;
|
|
75
87
|
private rememberedTargets;
|
|
88
|
+
private lastOutputAt;
|
|
89
|
+
private lastUserInputAt;
|
|
90
|
+
private idleProbeTimer;
|
|
91
|
+
private lastIdleProbeAt;
|
|
76
92
|
constructor(options: ClaudePtyBridgeOptions);
|
|
77
93
|
/**
|
|
78
94
|
* Process a raw PTY chunk.
|
|
@@ -99,6 +115,10 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
99
115
|
* Set the PTY write function for sending approval input.
|
|
100
116
|
*/
|
|
101
117
|
setPtyWrite(fn: (input: string) => void): void;
|
|
118
|
+
/**
|
|
119
|
+
* Toggle auto-approve at runtime.
|
|
120
|
+
*/
|
|
121
|
+
setAutoApprove(enabled: boolean): void;
|
|
102
122
|
/**
|
|
103
123
|
* Resolve the current permission prompt.
|
|
104
124
|
* @param resolution - How to resolve the permission
|
|
@@ -125,7 +145,25 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
125
145
|
* selection prompt time to fully render and enter its input loop before we send \r.
|
|
126
146
|
*/
|
|
127
147
|
private scheduleAutoApprove;
|
|
148
|
+
/**
|
|
149
|
+
* Schedule a fallback auto-approve with false-positive verification.
|
|
150
|
+
* Similar to scheduleAutoApprove but sets up post-approve monitoring.
|
|
151
|
+
*/
|
|
152
|
+
private scheduleFallbackAutoApprove;
|
|
128
153
|
private cancelPendingAutoApprove;
|
|
154
|
+
/**
|
|
155
|
+
* Reset the idle probe timer. Called on every new output chunk.
|
|
156
|
+
* If the terminal goes idle (no output for IDLE_PROBE_DELAY_MS) while
|
|
157
|
+
* auto-approve is enabled and no permission is currently detected,
|
|
158
|
+
* we send a speculative \r to catch prompts that all detection layers missed.
|
|
159
|
+
*/
|
|
160
|
+
private resetIdleProbeTimer;
|
|
161
|
+
private clearIdleProbeTimer;
|
|
162
|
+
/**
|
|
163
|
+
* Idle probe: the terminal has been quiet for a while. If conditions suggest
|
|
164
|
+
* a stuck permission prompt, send a speculative \r and monitor the result.
|
|
165
|
+
*/
|
|
166
|
+
private maybeIdleProbe;
|
|
129
167
|
private isPermissionPromptDetected;
|
|
130
168
|
private extractPromptText;
|
|
131
169
|
private extractPermissionTarget;
|
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
* 2. Structured messages for chat view (parsed)
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
|
-
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext } from "./pty-text-utils.js";
|
|
10
|
+
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD } from "./pty-text-utils.js";
|
|
11
11
|
// ── Constants ──
|
|
12
12
|
const OUTPUT_MAX_SIZE = 120000;
|
|
13
13
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
14
14
|
const PERMISSION_WINDOW_SIZE = 2000;
|
|
15
|
-
const AUTO_APPROVE_DELAY_MS =
|
|
15
|
+
const AUTO_APPROVE_DELAY_MS = 350;
|
|
16
|
+
/** How long to monitor output after fallback auto-approve for false-positive detection */
|
|
17
|
+
const FALLBACK_VERIFY_WINDOW_MS = 600;
|
|
18
|
+
/** How long a session must be idle (no user input, no new output) before sending a probe */
|
|
19
|
+
const IDLE_PROBE_DELAY_MS = 3000;
|
|
20
|
+
/** Minimum time between idle probes */
|
|
21
|
+
const IDLE_PROBE_COOLDOWN_MS = 5000;
|
|
16
22
|
const UUID_PATTERN = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
|
|
17
23
|
const CLAUDE_SESSION_ID_PATTERNS = [
|
|
18
24
|
new RegExp(`"session_id"\\s*:\\s*"${UUID_PATTERN}"`, "i"),
|
|
@@ -63,6 +69,11 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
63
69
|
// Permission memory for "approve_turn" policy
|
|
64
70
|
rememberedScopes = new Set();
|
|
65
71
|
rememberedTargets = new Set();
|
|
72
|
+
// Idle probe state (last-resort robustness)
|
|
73
|
+
lastOutputAt;
|
|
74
|
+
lastUserInputAt;
|
|
75
|
+
idleProbeTimer;
|
|
76
|
+
lastIdleProbeAt;
|
|
66
77
|
constructor(options) {
|
|
67
78
|
super();
|
|
68
79
|
this.sessionId = options.sessionId;
|
|
@@ -87,8 +98,16 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
87
98
|
lastTarget: null,
|
|
88
99
|
lastAutoConfirmAt: 0,
|
|
89
100
|
pendingAutoApproveTimer: null,
|
|
101
|
+
fallbackVerifyUntil: 0,
|
|
102
|
+
fallbackContext: null,
|
|
103
|
+
fallbackOutputLenAtApprove: 0,
|
|
104
|
+
retryCount: 0,
|
|
90
105
|
};
|
|
91
106
|
this.sessionIdWindow = "";
|
|
107
|
+
this.lastOutputAt = 0;
|
|
108
|
+
this.lastUserInputAt = 0;
|
|
109
|
+
this.idleProbeTimer = null;
|
|
110
|
+
this.lastIdleProbeAt = 0;
|
|
92
111
|
}
|
|
93
112
|
// ── Core API ──
|
|
94
113
|
/**
|
|
@@ -101,6 +120,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
101
120
|
return;
|
|
102
121
|
// 1. Append to raw output
|
|
103
122
|
this.rawOutput = appendWindow(this.rawOutput, normalizePtyOutput(chunk), OUTPUT_MAX_SIZE);
|
|
123
|
+
this.lastOutputAt = Date.now();
|
|
104
124
|
// 2. Emit raw output event (for terminal view)
|
|
105
125
|
this.emitEvent({
|
|
106
126
|
type: "output.raw",
|
|
@@ -117,6 +137,10 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
117
137
|
this.chatState.buffer += chunk;
|
|
118
138
|
this.parseChatResponse();
|
|
119
139
|
}
|
|
140
|
+
// 6. Reset idle probe timer on new output
|
|
141
|
+
if (this.autoApprove && this.isClaudeCommand) {
|
|
142
|
+
this.resetIdleProbeTimer();
|
|
143
|
+
}
|
|
120
144
|
}
|
|
121
145
|
/**
|
|
122
146
|
* Called when user sends input.
|
|
@@ -126,13 +150,12 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
126
150
|
// Guard against post-exit calls
|
|
127
151
|
if (this._exited)
|
|
128
152
|
return;
|
|
153
|
+
this.lastUserInputAt = Date.now();
|
|
129
154
|
// Filter out non-chat input (control chars, etc.)
|
|
130
155
|
if (!this.isRealChatInput(input)) {
|
|
131
|
-
process.stderr.write(`[Bridge] Input filtered as non-chat: ${input.substring(0, 50)}\n`);
|
|
132
156
|
return;
|
|
133
157
|
}
|
|
134
158
|
const cleanInput = input.replace(/[\r\n]+$/, "").trim();
|
|
135
|
-
process.stderr.write(`[Bridge] Starting chat tracking for: ${cleanInput.substring(0, 50)}\n`);
|
|
136
159
|
// Add user message
|
|
137
160
|
this.messages.push({
|
|
138
161
|
role: "user",
|
|
@@ -181,6 +204,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
181
204
|
}
|
|
182
205
|
// Clear permission state — prevents stale blocked state after exit
|
|
183
206
|
this.cancelPendingAutoApprove();
|
|
207
|
+
this.clearIdleProbeTimer();
|
|
184
208
|
this.permissionState.isBlocked = false;
|
|
185
209
|
this.permissionState.lastPrompt = null;
|
|
186
210
|
this.permissionState.lastScope = null;
|
|
@@ -217,6 +241,24 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
217
241
|
setPtyWrite(fn) {
|
|
218
242
|
this.ptyWrite = fn;
|
|
219
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Toggle auto-approve at runtime.
|
|
246
|
+
*/
|
|
247
|
+
setAutoApprove(enabled) {
|
|
248
|
+
this.autoApprove = enabled;
|
|
249
|
+
if (!enabled) {
|
|
250
|
+
// Cancel any pending auto-approve timer
|
|
251
|
+
if (this.permissionState.pendingAutoApproveTimer) {
|
|
252
|
+
clearTimeout(this.permissionState.pendingAutoApproveTimer);
|
|
253
|
+
this.permissionState.pendingAutoApproveTimer = null;
|
|
254
|
+
}
|
|
255
|
+
// Cancel idle probe
|
|
256
|
+
if (this.idleProbeTimer) {
|
|
257
|
+
clearTimeout(this.idleProbeTimer);
|
|
258
|
+
this.idleProbeTimer = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
220
262
|
// ── Permission Resolution ──
|
|
221
263
|
/**
|
|
222
264
|
* Resolve the current permission prompt.
|
|
@@ -334,7 +376,75 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
334
376
|
return;
|
|
335
377
|
this.permissionState.window = appendWindow(this.permissionState.window, chunk, PERMISSION_WINDOW_SIZE);
|
|
336
378
|
const normalized = normalizePromptText(this.permissionState.window);
|
|
379
|
+
// ── Fallback false-positive detection ──
|
|
380
|
+
// After a fallback auto-approve, monitor output for signs it was wrong
|
|
381
|
+
if (this.permissionState.fallbackContext) {
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
if (now < this.permissionState.fallbackVerifyUntil) {
|
|
384
|
+
// Check if the output grew with a prompt echo (false positive: \r was consumed as input)
|
|
385
|
+
const outputGrew = this.rawOutput.length > this.permissionState.fallbackOutputLenAtApprove;
|
|
386
|
+
if (outputGrew) {
|
|
387
|
+
// Check if the new output looks like Claude echoed our empty input (prompt re-appeared)
|
|
388
|
+
const newOutput = this.rawOutput.slice(this.permissionState.fallbackOutputLenAtApprove);
|
|
389
|
+
const stripped = stripAnsi(newOutput).trim();
|
|
390
|
+
// If we see a bare prompt symbol and no permission keywords → likely false positive
|
|
391
|
+
const looksLikeFalsePositive = /^❯\s*$/.test(stripped)
|
|
392
|
+
|| (stripped.includes("❯") && !this.isPermissionPromptDetected(normalized));
|
|
393
|
+
if (looksLikeFalsePositive) {
|
|
394
|
+
const ctx = this.permissionState.fallbackContext;
|
|
395
|
+
this.emitEvent({
|
|
396
|
+
type: "permission.resolved",
|
|
397
|
+
sessionId: this.sessionId,
|
|
398
|
+
timestamp: now,
|
|
399
|
+
data: {
|
|
400
|
+
resolution: "approve_once",
|
|
401
|
+
autoApproved: true,
|
|
402
|
+
approveType: "fallback",
|
|
403
|
+
falsePositive: true,
|
|
404
|
+
score: ctx.score,
|
|
405
|
+
matched: ctx.matched,
|
|
406
|
+
tail: ctx.text,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
this.permissionState.fallbackContext = null;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// Verification window expired — assume it was correct
|
|
416
|
+
const ctx = this.permissionState.fallbackContext;
|
|
417
|
+
this.emitEvent({
|
|
418
|
+
type: "permission.resolved",
|
|
419
|
+
sessionId: this.sessionId,
|
|
420
|
+
timestamp: now,
|
|
421
|
+
data: {
|
|
422
|
+
resolution: "approve_once",
|
|
423
|
+
autoApproved: true,
|
|
424
|
+
approveType: "fallback",
|
|
425
|
+
falsePositive: false,
|
|
426
|
+
score: ctx.score,
|
|
427
|
+
matched: ctx.matched,
|
|
428
|
+
tail: ctx.text,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
this.permissionState.fallbackContext = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
337
434
|
const blocked = this.isPermissionPromptDetected(normalized);
|
|
435
|
+
// ── Fallback: weighted keyword scoring ──
|
|
436
|
+
// If strict detection missed but auto-approve is on, try scoring
|
|
437
|
+
if (!blocked && !this.permissionState.isBlocked && this.autoApprove) {
|
|
438
|
+
const { score, matched } = scorePermissionLikelihood(normalized);
|
|
439
|
+
if (score >= FALLBACK_SCORE_THRESHOLD) {
|
|
440
|
+
const target = this.extractPermissionTarget(normalized);
|
|
441
|
+
const scope = this.inferScope(normalized, target);
|
|
442
|
+
const lines = normalized.split("\n");
|
|
443
|
+
const tailText = lines.slice(-8).join("\n");
|
|
444
|
+
this.scheduleFallbackAutoApprove(scope, target, { score, matched, text: tailText });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
338
448
|
// If state hasn't changed, check if a new distinct prompt appeared while already blocked
|
|
339
449
|
if (this.permissionState.isBlocked === blocked) {
|
|
340
450
|
if (blocked) {
|
|
@@ -409,12 +519,10 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
409
519
|
if (this.permissionState.pendingAutoApproveTimer)
|
|
410
520
|
return;
|
|
411
521
|
this.permissionState.lastAutoConfirmAt = now;
|
|
412
|
-
process.stderr.write(`[wand] Scheduling auto-confirm for ${scope}${target ? `: ${target}` : ""} (${AUTO_APPROVE_DELAY_MS}ms)\n`);
|
|
413
522
|
this.permissionState.pendingAutoApproveTimer = setTimeout(() => {
|
|
414
523
|
this.permissionState.pendingAutoApproveTimer = null;
|
|
415
524
|
if (this._exited)
|
|
416
525
|
return;
|
|
417
|
-
process.stderr.write(`[wand] Auto-confirming permission for ${scope}${target ? `: ${target}` : ""}\n`);
|
|
418
526
|
if (this.ptyWrite) {
|
|
419
527
|
this.ptyWrite("\r");
|
|
420
528
|
}
|
|
@@ -427,8 +535,56 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
427
535
|
type: "permission.resolved",
|
|
428
536
|
sessionId: this.sessionId,
|
|
429
537
|
timestamp: Date.now(),
|
|
430
|
-
data: { resolution: "approve_once", autoApproved: true },
|
|
538
|
+
data: { resolution: "approve_once", autoApproved: true, approveType: "strict" },
|
|
431
539
|
});
|
|
540
|
+
// Schedule a retry check: if the prompt re-appears shortly after,
|
|
541
|
+
// the \r may have arrived before the CLI was ready. Retry with a
|
|
542
|
+
// longer delay to handle slow-rendering selection menus.
|
|
543
|
+
if (this.permissionState.retryCount < 3) {
|
|
544
|
+
const retryDelay = 800 + this.permissionState.retryCount * 400;
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
if (this._exited)
|
|
547
|
+
return;
|
|
548
|
+
if (this.permissionState.isBlocked && this.autoApprove) {
|
|
549
|
+
this.permissionState.retryCount++;
|
|
550
|
+
this.permissionState.lastAutoConfirmAt = 0; // allow immediate retry
|
|
551
|
+
this.scheduleAutoApprove(scope, target);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
this.permissionState.retryCount = 0;
|
|
555
|
+
}
|
|
556
|
+
}, retryDelay);
|
|
557
|
+
}
|
|
558
|
+
}, AUTO_APPROVE_DELAY_MS);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Schedule a fallback auto-approve with false-positive verification.
|
|
562
|
+
* Similar to scheduleAutoApprove but sets up post-approve monitoring.
|
|
563
|
+
*/
|
|
564
|
+
scheduleFallbackAutoApprove(scope, target, context) {
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
if (now - this.permissionState.lastAutoConfirmAt < 500)
|
|
567
|
+
return;
|
|
568
|
+
if (this.permissionState.pendingAutoApproveTimer)
|
|
569
|
+
return;
|
|
570
|
+
this.permissionState.lastAutoConfirmAt = now;
|
|
571
|
+
this.permissionState.pendingAutoApproveTimer = setTimeout(() => {
|
|
572
|
+
this.permissionState.pendingAutoApproveTimer = null;
|
|
573
|
+
if (this._exited)
|
|
574
|
+
return;
|
|
575
|
+
// Snapshot output length before sending \r for false-positive detection
|
|
576
|
+
this.permissionState.fallbackOutputLenAtApprove = this.rawOutput.length;
|
|
577
|
+
this.permissionState.fallbackVerifyUntil = Date.now() + FALLBACK_VERIFY_WINDOW_MS;
|
|
578
|
+
this.permissionState.fallbackContext = context;
|
|
579
|
+
if (this.ptyWrite) {
|
|
580
|
+
this.ptyWrite("\r");
|
|
581
|
+
}
|
|
582
|
+
// Don't clear isBlocked/window here — let the verification logic in detectPermission handle it
|
|
583
|
+
this.permissionState.isBlocked = false;
|
|
584
|
+
this.permissionState.window = "";
|
|
585
|
+
this.permissionState.lastPrompt = null;
|
|
586
|
+
this.permissionState.lastScope = null;
|
|
587
|
+
this.permissionState.lastTarget = null;
|
|
432
588
|
}, AUTO_APPROVE_DELAY_MS);
|
|
433
589
|
}
|
|
434
590
|
cancelPendingAutoApprove() {
|
|
@@ -437,6 +593,67 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
437
593
|
this.permissionState.pendingAutoApproveTimer = null;
|
|
438
594
|
}
|
|
439
595
|
}
|
|
596
|
+
// ── Idle Probe (last-resort robustness) ──
|
|
597
|
+
/**
|
|
598
|
+
* Reset the idle probe timer. Called on every new output chunk.
|
|
599
|
+
* If the terminal goes idle (no output for IDLE_PROBE_DELAY_MS) while
|
|
600
|
+
* auto-approve is enabled and no permission is currently detected,
|
|
601
|
+
* we send a speculative \r to catch prompts that all detection layers missed.
|
|
602
|
+
*/
|
|
603
|
+
resetIdleProbeTimer() {
|
|
604
|
+
this.clearIdleProbeTimer();
|
|
605
|
+
this.idleProbeTimer = setTimeout(() => {
|
|
606
|
+
this.idleProbeTimer = null;
|
|
607
|
+
this.maybeIdleProbe();
|
|
608
|
+
}, IDLE_PROBE_DELAY_MS);
|
|
609
|
+
}
|
|
610
|
+
clearIdleProbeTimer() {
|
|
611
|
+
if (this.idleProbeTimer) {
|
|
612
|
+
clearTimeout(this.idleProbeTimer);
|
|
613
|
+
this.idleProbeTimer = null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Idle probe: the terminal has been quiet for a while. If conditions suggest
|
|
618
|
+
* a stuck permission prompt, send a speculative \r and monitor the result.
|
|
619
|
+
*/
|
|
620
|
+
maybeIdleProbe() {
|
|
621
|
+
if (this._exited || !this.autoApprove || !this.ptyWrite)
|
|
622
|
+
return;
|
|
623
|
+
// Don't probe if permission is already detected or fallback is pending verification
|
|
624
|
+
if (this.permissionState.isBlocked)
|
|
625
|
+
return;
|
|
626
|
+
if (this.permissionState.fallbackContext)
|
|
627
|
+
return;
|
|
628
|
+
if (this.permissionState.pendingAutoApproveTimer)
|
|
629
|
+
return;
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
// Cooldown between probes
|
|
632
|
+
if (now - this.lastIdleProbeAt < IDLE_PROBE_COOLDOWN_MS)
|
|
633
|
+
return;
|
|
634
|
+
// Don't probe if user recently sent input (they're actively interacting)
|
|
635
|
+
if (now - this.lastUserInputAt < IDLE_PROBE_DELAY_MS)
|
|
636
|
+
return;
|
|
637
|
+
// Only probe if output stopped recently (not ancient idle sessions)
|
|
638
|
+
if (now - this.lastOutputAt > 15000)
|
|
639
|
+
return;
|
|
640
|
+
// Quick heuristic: check if recent output has ANY permission-like keywords
|
|
641
|
+
const normalized = normalizePromptText(this.permissionState.window);
|
|
642
|
+
const { score, matched } = scorePermissionLikelihood(normalized);
|
|
643
|
+
// Lower threshold than fallback — we're being speculative
|
|
644
|
+
if (score < 4)
|
|
645
|
+
return;
|
|
646
|
+
this.lastIdleProbeAt = now;
|
|
647
|
+
// Snapshot output before probe
|
|
648
|
+
this.permissionState.fallbackOutputLenAtApprove = this.rawOutput.length;
|
|
649
|
+
this.permissionState.fallbackVerifyUntil = now + FALLBACK_VERIFY_WINDOW_MS;
|
|
650
|
+
this.permissionState.fallbackContext = {
|
|
651
|
+
score,
|
|
652
|
+
matched,
|
|
653
|
+
text: normalized.split("\n").slice(-8).join("\n"),
|
|
654
|
+
};
|
|
655
|
+
this.ptyWrite("\r");
|
|
656
|
+
}
|
|
440
657
|
isPermissionPromptDetected(normalized) {
|
|
441
658
|
const hasIntent = /\bdo you want to\b/i.test(normalized)
|
|
442
659
|
|| /\bwould you like to\b/i.test(normalized)
|
|
@@ -495,10 +712,8 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
495
712
|
}
|
|
496
713
|
this.chatState.echoSkipped = true;
|
|
497
714
|
this.chatState.buffer = clean.slice(echoEndIndex);
|
|
498
|
-
process.stderr.write(`[Bridge] Echo skipped, remaining length: ${this.chatState.buffer.length}\n`);
|
|
499
715
|
}
|
|
500
716
|
if (this.detectCompletion(this.chatState.buffer)) {
|
|
501
|
-
process.stderr.write("[Bridge] Completion detected, finalizing\n");
|
|
502
717
|
this.finalizeResponse();
|
|
503
718
|
return;
|
|
504
719
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { ensureConfig, hasConfigFile, isExecutionMode, resolveConfigPath, saveConfig } from "./config.js";
|
|
4
|
-
import { startServer } from "./server.js";
|
|
5
|
-
import { ensureDatabaseFile, resolveDatabasePath } from "./storage.js";
|
|
6
4
|
async function main() {
|
|
7
5
|
const args = process.argv.slice(2);
|
|
8
6
|
const command = args[0] || "help";
|
|
@@ -14,6 +12,7 @@ async function main() {
|
|
|
14
12
|
}
|
|
15
13
|
case "web": {
|
|
16
14
|
const config = await ensureRequiredFiles(configPath);
|
|
15
|
+
const { startServer } = await import("./server.js");
|
|
17
16
|
await startServer(config, configPath);
|
|
18
17
|
break;
|
|
19
18
|
}
|
|
@@ -67,6 +66,7 @@ Options:
|
|
|
67
66
|
`);
|
|
68
67
|
}
|
|
69
68
|
async function ensureRequiredFiles(configPath) {
|
|
69
|
+
const { ensureDatabaseFile, resolveDatabasePath } = await import("./storage.js");
|
|
70
70
|
const dbPath = resolveDatabasePath(configPath);
|
|
71
71
|
const hadConfig = hasConfigFile(configPath);
|
|
72
72
|
const config = await ensureConfig(configPath);
|
|
@@ -71,6 +71,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
71
71
|
resolveEscalation(id: string, requestId: string, resolution?: "approve_once" | "approve_turn" | "deny"): SessionSnapshot;
|
|
72
72
|
approvePermission(id: string): SessionSnapshot;
|
|
73
73
|
denyPermission(id: string): SessionSnapshot;
|
|
74
|
+
toggleAutoApprove(id: string): SessionSnapshot;
|
|
74
75
|
/**
|
|
75
76
|
* Canonical permission resolution method.
|
|
76
77
|
* All other permission methods delegate to this.
|