@co0ontty/wand 1.3.3 → 1.3.6
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/claude-pty-bridge.d.ts +32 -0
- package/dist/claude-pty-bridge.js +186 -8
- package/dist/process-manager.js +87 -190
- package/dist/pty-text-utils.d.ts +12 -0
- package/dist/pty-text-utils.js +37 -1
- package/dist/server-session-routes.d.ts +1 -0
- package/dist/server-session-routes.js +1 -3
- package/dist/server.js +6 -31
- package/dist/session-lifecycle.js +0 -5
- package/dist/session-logger.d.ts +10 -0
- package/dist/types.d.ts +7 -0
- package/dist/web-ui/content/scripts.js +134 -30
- package/dist/web-ui/content/styles.css +111 -2
- package/package.json +1 -1
package/dist/process-manager.js
CHANGED
|
@@ -9,7 +9,7 @@ import { SessionLogger } from "./session-logger.js";
|
|
|
9
9
|
import { SessionLifecycleManager } from "./session-lifecycle.js";
|
|
10
10
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
11
11
|
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText } from "./pty-text-utils.js";
|
|
12
|
-
import { getResumeCommandSessionId,
|
|
12
|
+
import { getResumeCommandSessionId, hasRealConversationMessages, } from "./resume-policy.js";
|
|
13
13
|
/** Check if the current process is running as root (UID 0). */
|
|
14
14
|
function isRunningAsRoot() {
|
|
15
15
|
return process.getuid?.() === 0 || process.geteuid?.() === 0;
|
|
@@ -26,20 +26,6 @@ export class SessionInputError extends Error {
|
|
|
26
26
|
this.name = "SessionInputError";
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
const PROMPT_PATTERNS = [
|
|
30
|
-
/(?:^|\b)(?:press\s+)?(?:y|yes)\s*(?:\/|\bor\b)\s*(?:n|no)(?:\b|$)/i,
|
|
31
|
-
/\[(?:y|yes)\s*\/\s*(?:n|no)\]/i,
|
|
32
|
-
/\((?:y|yes)\s*\/\s*(?:n|no)\)/i,
|
|
33
|
-
/\((?:y|yes)\s*\/\s*(?:n|no)\s*\/\s*always\)/i,
|
|
34
|
-
/\bcontinue\?\s*(?:\((?:y|yes)\s*\/\s*(?:n|no)\))?/i,
|
|
35
|
-
/\bare you sure\??/i,
|
|
36
|
-
/\bdo you want to continue\??/i,
|
|
37
|
-
/\bdo you want to (?:create|write|delete|modify|execute)\b/i,
|
|
38
|
-
/\bconfirm(?:\s+execution|\s+changes|\s+action)?\??/i,
|
|
39
|
-
/\bproceed\?\s*(?:\((?:y|yes)\s*\/\s*(?:n|no)\))?/i,
|
|
40
|
-
/\benter to confirm\b/i,
|
|
41
|
-
/\bgrant\b.*\bpermission\b/i,
|
|
42
|
-
];
|
|
43
29
|
const REAL_CONVERSATION_MIN_LINES = 2;
|
|
44
30
|
const DISCOVERY_RECENT_WINDOW_MS = 10 * 60 * 1000;
|
|
45
31
|
const START_TIME_SKEW_MS = 30 * 1000;
|
|
@@ -89,132 +75,6 @@ function readClaudeProjectSessionDetails(filePath, id) {
|
|
|
89
75
|
return null;
|
|
90
76
|
}
|
|
91
77
|
}
|
|
92
|
-
function hasVisibleProjectConversation(messages) {
|
|
93
|
-
return shouldDisplayResumeAction(messages);
|
|
94
|
-
}
|
|
95
|
-
function hasRecoverableProjectConversation(messages) {
|
|
96
|
-
return shouldAllowResume({ claudeSessionId: "resume-candidate", messages });
|
|
97
|
-
}
|
|
98
|
-
function shouldBindLiveProjectSessionId(messages) {
|
|
99
|
-
return hasLiveProjectConversation(messages);
|
|
100
|
-
}
|
|
101
|
-
function shouldBackfillStoredProjectSessionId(messages) {
|
|
102
|
-
return hasStoredProjectConversation(messages);
|
|
103
|
-
}
|
|
104
|
-
function shouldDisplayVisibleProjectSessionId(messages) {
|
|
105
|
-
return hasVisibleProjectConversation(messages);
|
|
106
|
-
}
|
|
107
|
-
function shouldResumeRecoverableProjectSessionId(messages) {
|
|
108
|
-
return hasRecoverableProjectConversation(messages);
|
|
109
|
-
}
|
|
110
|
-
function canBindLiveProjectSession(record) {
|
|
111
|
-
return shouldBindLiveProjectSessionId(record.messages);
|
|
112
|
-
}
|
|
113
|
-
function canBackfillStoredProjectSession(record) {
|
|
114
|
-
return shouldBackfillStoredProjectSessionId(record.messages);
|
|
115
|
-
}
|
|
116
|
-
function canDisplayVisibleProjectSession(messages) {
|
|
117
|
-
return shouldDisplayVisibleProjectSessionId(messages);
|
|
118
|
-
}
|
|
119
|
-
function canResumeRecoverableProjectSession(messages) {
|
|
120
|
-
return shouldResumeRecoverableProjectSessionId(messages);
|
|
121
|
-
}
|
|
122
|
-
function shouldAdoptProjectSessionDuringRuntime(record) {
|
|
123
|
-
return canBindLiveProjectSession(record);
|
|
124
|
-
}
|
|
125
|
-
function shouldAdoptProjectSessionDuringBackfill(record) {
|
|
126
|
-
return canBackfillStoredProjectSession(record);
|
|
127
|
-
}
|
|
128
|
-
function shouldAdoptProjectSessionForUi(messages) {
|
|
129
|
-
return canDisplayVisibleProjectSession(messages);
|
|
130
|
-
}
|
|
131
|
-
function shouldAdoptProjectSessionForResume(messages) {
|
|
132
|
-
return canResumeRecoverableProjectSession(messages);
|
|
133
|
-
}
|
|
134
|
-
function hasRuntimeProjectAdoption(messages) {
|
|
135
|
-
return shouldAdoptProjectSessionForUi(messages);
|
|
136
|
-
}
|
|
137
|
-
function hasBackfillProjectAdoption(messages) {
|
|
138
|
-
return shouldBackfillStoredProjectSessionId(messages);
|
|
139
|
-
}
|
|
140
|
-
function hasUiProjectAdoption(messages) {
|
|
141
|
-
return shouldAdoptProjectSessionForUi(messages);
|
|
142
|
-
}
|
|
143
|
-
function hasResumeProjectAdoption(messages) {
|
|
144
|
-
return shouldAdoptProjectSessionForResume(messages);
|
|
145
|
-
}
|
|
146
|
-
function shouldAdoptProjectSession(record) {
|
|
147
|
-
return shouldAdoptProjectSessionDuringRuntime(record);
|
|
148
|
-
}
|
|
149
|
-
function shouldAdoptStoredProjectSession(record) {
|
|
150
|
-
return shouldAdoptProjectSessionDuringBackfill(record);
|
|
151
|
-
}
|
|
152
|
-
function shouldAdoptUiProjectSession(messages) {
|
|
153
|
-
return hasUiProjectAdoption(messages);
|
|
154
|
-
}
|
|
155
|
-
function shouldAdoptResumeProjectSession(messages) {
|
|
156
|
-
return hasResumeProjectAdoption(messages);
|
|
157
|
-
}
|
|
158
|
-
function canUseProjectSessionAtRuntime(record) {
|
|
159
|
-
return shouldAdoptProjectSession(record);
|
|
160
|
-
}
|
|
161
|
-
function canUseProjectSessionAtBackfill(record) {
|
|
162
|
-
return shouldAdoptStoredProjectSession(record);
|
|
163
|
-
}
|
|
164
|
-
function canUseProjectSessionAtUi(messages) {
|
|
165
|
-
return shouldAdoptUiProjectSession(messages);
|
|
166
|
-
}
|
|
167
|
-
function canUseProjectSessionAtResume(messages) {
|
|
168
|
-
return shouldAdoptResumeProjectSession(messages);
|
|
169
|
-
}
|
|
170
|
-
function hasProjectSessionRuntimeEligibility(messages) {
|
|
171
|
-
return shouldAdoptProjectSessionDuringRuntime({ messages: messages ?? [] });
|
|
172
|
-
}
|
|
173
|
-
function hasProjectSessionBackfillEligibility(messages) {
|
|
174
|
-
return shouldAdoptProjectSessionDuringBackfill({ messages: messages ?? [] });
|
|
175
|
-
}
|
|
176
|
-
function hasProjectSessionUiEligibility(messages) {
|
|
177
|
-
return canUseProjectSessionAtUi(messages);
|
|
178
|
-
}
|
|
179
|
-
function hasProjectSessionResumeEligibility(messages) {
|
|
180
|
-
return canUseProjectSessionAtResume(messages);
|
|
181
|
-
}
|
|
182
|
-
function shouldClaimProjectSessionDuringRuntime(messages) {
|
|
183
|
-
return hasProjectSessionRuntimeEligibility(messages);
|
|
184
|
-
}
|
|
185
|
-
function shouldClaimProjectSessionDuringBackfill(messages) {
|
|
186
|
-
return hasProjectSessionBackfillEligibility(messages);
|
|
187
|
-
}
|
|
188
|
-
function shouldClaimProjectSessionForUi(messages) {
|
|
189
|
-
return hasProjectSessionUiEligibility(messages);
|
|
190
|
-
}
|
|
191
|
-
function shouldClaimProjectSessionForResume(messages) {
|
|
192
|
-
return hasProjectSessionResumeEligibility(messages);
|
|
193
|
-
}
|
|
194
|
-
function hasClaimableProjectSessionRuntime(messages) {
|
|
195
|
-
return shouldClaimProjectSessionDuringRuntime(messages);
|
|
196
|
-
}
|
|
197
|
-
function hasClaimableProjectSessionBackfill(messages) {
|
|
198
|
-
return shouldClaimProjectSessionDuringBackfill(messages);
|
|
199
|
-
}
|
|
200
|
-
function hasClaimableProjectSessionUi(messages) {
|
|
201
|
-
return shouldClaimProjectSessionForUi(messages);
|
|
202
|
-
}
|
|
203
|
-
function hasClaimableProjectSessionResume(messages) {
|
|
204
|
-
return shouldClaimProjectSessionForResume(messages);
|
|
205
|
-
}
|
|
206
|
-
function isClaimableProjectSessionRuntime(messages) {
|
|
207
|
-
return hasClaimableProjectSessionRuntime(messages);
|
|
208
|
-
}
|
|
209
|
-
function isClaimableProjectSessionBackfill(messages) {
|
|
210
|
-
return hasClaimableProjectSessionBackfill(messages);
|
|
211
|
-
}
|
|
212
|
-
function isClaimableProjectSessionUi(messages) {
|
|
213
|
-
return hasClaimableProjectSessionUi(messages);
|
|
214
|
-
}
|
|
215
|
-
function isClaimableProjectSessionResume(messages) {
|
|
216
|
-
return hasClaimableProjectSessionResume(messages);
|
|
217
|
-
}
|
|
218
78
|
function listClaudeProjectSessionCandidates(cwd) {
|
|
219
79
|
const projectDir = getClaudeProjectDir(cwd);
|
|
220
80
|
try {
|
|
@@ -499,7 +359,7 @@ function shouldBackfillClaudeSessionId(record) {
|
|
|
499
359
|
function snapshotMessages(record) {
|
|
500
360
|
return record.ptyBridge?.getMessages() ?? record.messages;
|
|
501
361
|
}
|
|
502
|
-
const MAX_SESSIONS =
|
|
362
|
+
const MAX_SESSIONS = 200;
|
|
503
363
|
const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
|
|
504
364
|
const CONFIRM_WINDOW_SIZE = 800;
|
|
505
365
|
// Claude 会话 ID 格式:UUID v4
|
|
@@ -557,9 +417,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
557
417
|
onStateChange: (sessionId, oldState, newState) => {
|
|
558
418
|
this.emitEvent({ type: "status", sessionId, data: { oldState, newState } });
|
|
559
419
|
},
|
|
560
|
-
onIdle: (
|
|
561
|
-
console.error(`[ProcessManager] Session ${sessionId} is now idle`);
|
|
562
|
-
},
|
|
420
|
+
onIdle: (_sessionId) => { },
|
|
563
421
|
onArchived: (sessionId, reason) => {
|
|
564
422
|
console.error(`[ProcessManager] Session ${sessionId} archived: ${reason}`);
|
|
565
423
|
},
|
|
@@ -599,7 +457,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
599
457
|
knownClaudeTaskIds: undefined,
|
|
600
458
|
claudeTaskDiscoveryTimer: null,
|
|
601
459
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
|
|
602
|
-
claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId
|
|
460
|
+
claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
|
|
461
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
603
462
|
});
|
|
604
463
|
this.lifecycleManager.register(snapshot.id, "idle");
|
|
605
464
|
console.error(`[ProcessManager] Restored session ${snapshot.id} marked as exited (PTY orphaned)`);
|
|
@@ -631,7 +490,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
631
490
|
knownClaudeTaskIds: undefined,
|
|
632
491
|
claudeTaskDiscoveryTimer: null,
|
|
633
492
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
|
|
634
|
-
claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId
|
|
493
|
+
claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
|
|
494
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
635
495
|
});
|
|
636
496
|
this.lifecycleManager.register(snapshot.id, "archived");
|
|
637
497
|
}
|
|
@@ -651,24 +511,33 @@ export class ProcessManager extends EventEmitter {
|
|
|
651
511
|
this.emit("process", event);
|
|
652
512
|
}
|
|
653
513
|
cleanupOldSessions() {
|
|
654
|
-
//
|
|
514
|
+
// Only clean up when well over the limit
|
|
655
515
|
if (this.sessions.size < MAX_SESSIONS)
|
|
656
516
|
return;
|
|
657
|
-
const
|
|
517
|
+
const now = Date.now();
|
|
518
|
+
const STALE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
519
|
+
const removable = [];
|
|
658
520
|
for (const [id, record] of this.sessions) {
|
|
659
|
-
|
|
660
|
-
|
|
521
|
+
// Only remove archived, non-running sessions older than 7 days
|
|
522
|
+
if (record.status === "running")
|
|
523
|
+
continue;
|
|
524
|
+
if (!record.archived)
|
|
525
|
+
continue;
|
|
526
|
+
const ref = record.endedAt ?? record.startedAt;
|
|
527
|
+
const refMs = Date.parse(ref);
|
|
528
|
+
if (Number.isFinite(refMs) && now - refMs > STALE_MS) {
|
|
529
|
+
removable.push(id);
|
|
661
530
|
}
|
|
662
531
|
}
|
|
663
|
-
//
|
|
664
|
-
|
|
532
|
+
// Sort oldest first and remove enough to get back under the limit
|
|
533
|
+
const toRemove = removable
|
|
665
534
|
.sort((a, b) => {
|
|
666
535
|
const ra = this.sessions.get(a);
|
|
667
536
|
const rb = this.sessions.get(b);
|
|
668
537
|
return (ra?.endedAt || "").localeCompare(rb?.endedAt || "");
|
|
669
538
|
})
|
|
670
|
-
.slice(0, this.sessions.size - MAX_SESSIONS + 1)
|
|
671
|
-
|
|
539
|
+
.slice(0, this.sessions.size - MAX_SESSIONS + 1);
|
|
540
|
+
for (const id of toRemove) {
|
|
672
541
|
const record = this.sessions.get(id);
|
|
673
542
|
if (record) {
|
|
674
543
|
this.logger.deleteSession(id);
|
|
@@ -677,7 +546,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
677
546
|
this.sessions.delete(id);
|
|
678
547
|
this.lastPersistedMessageCount.delete(id);
|
|
679
548
|
this.storage.deleteSession(id);
|
|
680
|
-
}
|
|
549
|
+
}
|
|
681
550
|
}
|
|
682
551
|
start(command, cwd, mode, initialInput, opts) {
|
|
683
552
|
this.assertCommandAllowed(command);
|
|
@@ -732,7 +601,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
732
601
|
lastEmittedTask: null,
|
|
733
602
|
knownClaudeTaskIds: knownClaudeTaskIds ?? undefined,
|
|
734
603
|
claudeTaskDiscoveryTimer: null,
|
|
735
|
-
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined
|
|
604
|
+
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
605
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
736
606
|
};
|
|
737
607
|
// Create PTY bridge for this session
|
|
738
608
|
record.ptyBridge = new ClaudePtyBridge({
|
|
@@ -835,7 +705,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
835
705
|
process.stderr.write(`[wand] Cannot send initial input: session not ready\n`);
|
|
836
706
|
return;
|
|
837
707
|
}
|
|
838
|
-
process.stderr.write(`[wand] Sending initial input
|
|
708
|
+
process.stderr.write(`[wand] Sending initial input (${initialInput.length} chars)\n`);
|
|
839
709
|
// Track initial input via bridge for Chat mode
|
|
840
710
|
if (current.ptyBridge) {
|
|
841
711
|
current.ptyBridge.onUserInput(initialInput);
|
|
@@ -989,8 +859,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
989
859
|
get(id) {
|
|
990
860
|
this.archiveExpiredSessions();
|
|
991
861
|
const record = this.sessions.get(id);
|
|
992
|
-
if (!record)
|
|
993
|
-
|
|
862
|
+
if (!record) {
|
|
863
|
+
// Fallback: check SQLite for sessions that were evicted from memory
|
|
864
|
+
return this.storage.getSession(id) ?? null;
|
|
865
|
+
}
|
|
994
866
|
// For sessions loaded from storage on startup, in-memory output starts empty.
|
|
995
867
|
// Prefer in-memory output (live PTY data), fall back to stored output.
|
|
996
868
|
if (!record.output && record.storedOutput) {
|
|
@@ -1001,35 +873,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
1001
873
|
sendInput(id, input, view, shortcutKey) {
|
|
1002
874
|
const record = this.mustGet(id);
|
|
1003
875
|
if (record.status !== "running") {
|
|
1004
|
-
console.error(
|
|
1005
|
-
sessionId: id,
|
|
1006
|
-
status: record.status,
|
|
1007
|
-
hasPty: !!record.ptyProcess,
|
|
1008
|
-
inputLength: input.length,
|
|
1009
|
-
view: view ?? "chat"
|
|
1010
|
-
});
|
|
876
|
+
console.error(`[ProcessManager] Rejecting input: session ${id} not running (${record.status})`);
|
|
1011
877
|
throw new SessionInputError("Session is not running.", "SESSION_NOT_RUNNING", id, record.status);
|
|
1012
878
|
}
|
|
1013
879
|
// Update lifecycle
|
|
1014
880
|
this.lifecycleManager.touch(id);
|
|
1015
881
|
this.lifecycleManager.startThinking(id);
|
|
1016
882
|
if (!record.ptyProcess) {
|
|
1017
|
-
console.error(
|
|
1018
|
-
sessionId: id,
|
|
1019
|
-
status: record.status,
|
|
1020
|
-
hasPty: !!record.ptyProcess,
|
|
1021
|
-
inputLength: input.length,
|
|
1022
|
-
view: view ?? "chat"
|
|
1023
|
-
});
|
|
883
|
+
console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
|
|
1024
884
|
throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
|
|
1025
885
|
}
|
|
1026
|
-
console.error("[ProcessManager] Sending input to session", {
|
|
1027
|
-
sessionId: id,
|
|
1028
|
-
status: record.status,
|
|
1029
|
-
hasPty: !!record.ptyProcess,
|
|
1030
|
-
inputLength: input.length,
|
|
1031
|
-
view: view ?? "chat"
|
|
1032
|
-
});
|
|
1033
886
|
// Log shortcut key interactions for auto-confirm and mode analysis
|
|
1034
887
|
if (shortcutKey) {
|
|
1035
888
|
const outputLines = record.output.split("\n");
|
|
@@ -1242,7 +1095,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1242
1095
|
messages: messages.length > 0 ? messages : undefined,
|
|
1243
1096
|
resumedFromSessionId: record.resumedFromSessionId ?? undefined,
|
|
1244
1097
|
resumedToSessionId: record.resumedToSessionId ?? undefined,
|
|
1245
|
-
autoRecovered: record.autoRecovered ?? false
|
|
1098
|
+
autoRecovered: record.autoRecovered ?? false,
|
|
1099
|
+
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined
|
|
1246
1100
|
};
|
|
1247
1101
|
}
|
|
1248
1102
|
isPermissionBlocked(record) {
|
|
@@ -1482,11 +1336,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
1482
1336
|
|| claudeConfirmPrompt
|
|
1483
1337
|
|| toolPermissionPrompt
|
|
1484
1338
|
|| (hasExplicitConfirmSyntax(normalized)
|
|
1485
|
-
&& hasPermissionActionContext(normalized)
|
|
1486
|
-
&& PROMPT_PATTERNS.some((pattern) => pattern.test(normalized)));
|
|
1339
|
+
&& hasPermissionActionContext(normalized));
|
|
1487
1340
|
if (shouldConfirm) {
|
|
1488
1341
|
record.lastAutoConfirmAt = now;
|
|
1489
|
-
process.stderr.write(`[wand] Auto-confirming prompt for ${record.mode} mode\n`);
|
|
1490
1342
|
// Always auto-confirm by sending Enter directly
|
|
1491
1343
|
ptyProcess.write("\r");
|
|
1492
1344
|
}
|
|
@@ -1551,15 +1403,50 @@ export class ProcessManager extends EventEmitter {
|
|
|
1551
1403
|
});
|
|
1552
1404
|
break;
|
|
1553
1405
|
}
|
|
1554
|
-
case "permission.resolved":
|
|
1406
|
+
case "permission.resolved": {
|
|
1407
|
+
// Increment approval stats before clearing pendingEscalation
|
|
1408
|
+
const resolvedScope = record.pendingEscalation?.scope;
|
|
1409
|
+
if (resolvedScope) {
|
|
1410
|
+
if (resolvedScope === "run_command" || resolvedScope === "dangerous_shell") {
|
|
1411
|
+
record.approvalStats.command++;
|
|
1412
|
+
}
|
|
1413
|
+
else if (resolvedScope === "write_file") {
|
|
1414
|
+
record.approvalStats.file++;
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
record.approvalStats.tool++;
|
|
1418
|
+
}
|
|
1419
|
+
record.approvalStats.total++;
|
|
1420
|
+
}
|
|
1555
1421
|
record.pendingEscalation = null;
|
|
1556
1422
|
record.ptyPermissionBlocked = false;
|
|
1557
1423
|
this.emitEvent({
|
|
1558
1424
|
type: "status",
|
|
1559
1425
|
sessionId: event.sessionId,
|
|
1560
|
-
data: {
|
|
1426
|
+
data: {
|
|
1427
|
+
permissionBlocked: false,
|
|
1428
|
+
approvalStats: record.approvalStats,
|
|
1429
|
+
},
|
|
1561
1430
|
});
|
|
1431
|
+
// Log auto-approve events to shortcut-interactions.jsonl for analysis
|
|
1432
|
+
const resolvedData = event.data;
|
|
1433
|
+
if (resolvedData?.autoApproved) {
|
|
1434
|
+
const outputLines = record.output.split("\n");
|
|
1435
|
+
const tailLines = outputLines.slice(-8).join("\n");
|
|
1436
|
+
this.logger.appendShortcutLog(record.id, "auto_approve", tailLines, {
|
|
1437
|
+
mode: record.mode,
|
|
1438
|
+
scope: resolvedScope ?? "unknown",
|
|
1439
|
+
autoApprove: record.autoApprovePermissions,
|
|
1440
|
+
permissionBlocked: true,
|
|
1441
|
+
input: "\r",
|
|
1442
|
+
approveType: resolvedData.approveType,
|
|
1443
|
+
score: resolvedData.score,
|
|
1444
|
+
matched: resolvedData.matched,
|
|
1445
|
+
falsePositive: resolvedData.falsePositive,
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1562
1448
|
break;
|
|
1449
|
+
}
|
|
1563
1450
|
case "session.id":
|
|
1564
1451
|
// Claude session ID captured - already handled in onData
|
|
1565
1452
|
break;
|
|
@@ -1622,15 +1509,25 @@ export class ProcessManager extends EventEmitter {
|
|
|
1622
1509
|
return false;
|
|
1623
1510
|
}
|
|
1624
1511
|
processCommandForMode(command, mode) {
|
|
1512
|
+
const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
|
|
1513
|
+
if (!isClaudeCmd)
|
|
1514
|
+
return command;
|
|
1515
|
+
let result = command;
|
|
1516
|
+
// When running as root, Claude CLI refuses --permission-mode bypassPermissions.
|
|
1517
|
+
// Use acceptEdits to let Claude auto-accept edits natively, reducing the number
|
|
1518
|
+
// of permission prompts wand has to auto-approve.
|
|
1519
|
+
if (isRunningAsRoot() && (mode === "managed" || mode === "full-access" || mode === "auto-edit")) {
|
|
1520
|
+
result += " --permission-mode acceptEdits";
|
|
1521
|
+
}
|
|
1625
1522
|
// In managed mode, append a system prompt instructing Claude to act autonomously
|
|
1626
1523
|
// without asking the user for confirmation, since the user may not be monitoring.
|
|
1627
|
-
if (mode === "managed"
|
|
1524
|
+
if (mode === "managed") {
|
|
1628
1525
|
const autonomousPrompt = "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.";
|
|
1629
1526
|
// Escape single quotes for shell safety
|
|
1630
1527
|
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1631
|
-
|
|
1528
|
+
result += ` --append-system-prompt '${escaped}'`;
|
|
1632
1529
|
}
|
|
1633
|
-
return
|
|
1530
|
+
return result;
|
|
1634
1531
|
}
|
|
1635
1532
|
}
|
|
1636
1533
|
function clampDimension(value, min, max) {
|
package/dist/pty-text-utils.d.ts
CHANGED
|
@@ -11,5 +11,17 @@ export declare function isNoiseLine(line: string): boolean;
|
|
|
11
11
|
export declare function appendWindow(buffer: string, chunk: string, maxSize: number): string;
|
|
12
12
|
export declare function hasExplicitConfirmSyntax(normalized: string): boolean;
|
|
13
13
|
export declare function hasPermissionActionContext(normalized: string): boolean;
|
|
14
|
+
interface PermissionScore {
|
|
15
|
+
score: number;
|
|
16
|
+
matched: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Minimum score threshold for fallback permission detection */
|
|
19
|
+
export declare const FALLBACK_SCORE_THRESHOLD = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Score how likely the recent output contains a permission prompt.
|
|
22
|
+
* Evaluates the last few lines of normalized text against weighted keywords.
|
|
23
|
+
*/
|
|
24
|
+
export declare function scorePermissionLikelihood(normalized: string): PermissionScore;
|
|
14
25
|
/** Normalize prompt text for permission detection (strip ANSI, collapse whitespace). */
|
|
15
26
|
export declare function normalizePromptText(value: string): string;
|
|
27
|
+
export {};
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -77,8 +77,10 @@ const EXPLICIT_CONFIRM_PATTERNS = [
|
|
|
77
77
|
/\[(?:y|yes)\s*\/\s*(?:n|no)\]/i,
|
|
78
78
|
/\((?:y|yes)\s*\/\s*(?:n|no)\)/i,
|
|
79
79
|
/\((?:y|yes)\s*\/\s*(?:n|no)\s*\/\s*always\)/i,
|
|
80
|
-
/\byes\b
|
|
80
|
+
/\byes\b[\s\S]*\bno\b/i,
|
|
81
81
|
/\benter to confirm\b/i,
|
|
82
|
+
// Claude CLI numbered selection menus: "❯ 1. Yes" + "N. No"
|
|
83
|
+
/❯\s*1\.\s*yes\b/i,
|
|
82
84
|
];
|
|
83
85
|
const PERMISSION_ACTION_PATTERNS = [
|
|
84
86
|
/\bgrant\b.*\bpermission\b/i,
|
|
@@ -92,6 +94,40 @@ export function hasExplicitConfirmSyntax(normalized) {
|
|
|
92
94
|
export function hasPermissionActionContext(normalized) {
|
|
93
95
|
return PERMISSION_ACTION_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
94
96
|
}
|
|
97
|
+
const PERMISSION_KEYWORD_WEIGHTS = [
|
|
98
|
+
{ pattern: /\bdo you want to proceed\b/i, weight: 5, label: "do you want to proceed" },
|
|
99
|
+
{ pattern: /\bwould you like to proceed\b/i, weight: 5, label: "would you like to proceed" },
|
|
100
|
+
{ pattern: /\bdo you want to\b/i, weight: 4, label: "do you want to" },
|
|
101
|
+
{ pattern: /\bwould you like to\b/i, weight: 4, label: "would you like to" },
|
|
102
|
+
{ pattern: /\benter to confirm\b/i, weight: 4, label: "enter to confirm" },
|
|
103
|
+
{ pattern: /\bgrant\b[\s\S]*\bpermission\b/i, weight: 4, label: "grant permission" },
|
|
104
|
+
{ pattern: /❯\s*1\./i, weight: 3, label: "❯ 1." },
|
|
105
|
+
{ pattern: /\b1\.\s*yes\b/i, weight: 3, label: "1. yes" },
|
|
106
|
+
{ pattern: /\bdon'?t ask again\b/i, weight: 2, label: "don't ask again" },
|
|
107
|
+
{ pattern: /\b(?:bash|shell)\s*command\b/i, weight: 2, label: "bash/shell command" },
|
|
108
|
+
{ pattern: /\b(?:run|read|write|edit)\s+(?:file|command|shell)\b/i, weight: 2, label: "run/read/write action" },
|
|
109
|
+
{ pattern: /\ballow\b.*\breading\b/i, weight: 2, label: "allow reading" },
|
|
110
|
+
];
|
|
111
|
+
/** Minimum score threshold for fallback permission detection */
|
|
112
|
+
export const FALLBACK_SCORE_THRESHOLD = 8;
|
|
113
|
+
/**
|
|
114
|
+
* Score how likely the recent output contains a permission prompt.
|
|
115
|
+
* Evaluates the last few lines of normalized text against weighted keywords.
|
|
116
|
+
*/
|
|
117
|
+
export function scorePermissionLikelihood(normalized) {
|
|
118
|
+
// Take the last ~5 lines
|
|
119
|
+
const lines = normalized.split("\n");
|
|
120
|
+
const tail = lines.slice(-8).join("\n");
|
|
121
|
+
let score = 0;
|
|
122
|
+
const matched = [];
|
|
123
|
+
for (const { pattern, weight, label } of PERMISSION_KEYWORD_WEIGHTS) {
|
|
124
|
+
if (pattern.test(tail)) {
|
|
125
|
+
score += weight;
|
|
126
|
+
matched.push(label);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { score, matched };
|
|
130
|
+
}
|
|
95
131
|
/** Normalize prompt text for permission detection (strip ANSI, collapse whitespace). */
|
|
96
132
|
export function normalizePromptText(value) {
|
|
97
133
|
return value
|
|
@@ -2,5 +2,6 @@ import { Express } from "express";
|
|
|
2
2
|
import { ProcessManager } from "./process-manager.js";
|
|
3
3
|
import { WandStorage } from "./storage.js";
|
|
4
4
|
import { ExecutionMode } from "./types.js";
|
|
5
|
+
export declare function getErrorMessage(error: unknown, fallback: string): string;
|
|
5
6
|
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, storage: WandStorage, defaultMode: ExecutionMode): void;
|
|
6
7
|
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { parseMessages } from "./message-parser.js";
|
|
3
3
|
import { SessionInputError } from "./process-manager.js";
|
|
4
|
-
function getErrorMessage(error, fallback) {
|
|
4
|
+
export function getErrorMessage(error, fallback) {
|
|
5
5
|
return error instanceof Error ? error.message : fallback;
|
|
6
6
|
}
|
|
7
7
|
function getInputErrorResponse(error, sessionId) {
|
|
@@ -184,10 +184,8 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
184
184
|
const input = body.input ?? "";
|
|
185
185
|
const view = body.view;
|
|
186
186
|
const shortcutKey = body.shortcutKey;
|
|
187
|
-
console.error("[wand] Input request received", { sessionId, inputLength: input.length, view: view ?? "chat" });
|
|
188
187
|
try {
|
|
189
188
|
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
190
|
-
console.error("[wand] Input request succeeded", { sessionId, status: snapshot.status, inputLength: input.length, view: view ?? "chat" });
|
|
191
189
|
res.json(snapshot);
|
|
192
190
|
}
|
|
193
191
|
catch (error) {
|
package/dist/server.js
CHANGED
|
@@ -58,16 +58,12 @@ import { ensureCertificates } from "./cert.js";
|
|
|
58
58
|
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
59
59
|
import { ProcessManager } from "./process-manager.js";
|
|
60
60
|
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
61
|
-
import { registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
61
|
+
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
62
62
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
63
63
|
import { renderApp } from "./web-ui/index.js";
|
|
64
64
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
65
65
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
66
66
|
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
67
|
-
// ── Error helpers ──
|
|
68
|
-
function getErrorMessage(error, fallback) {
|
|
69
|
-
return error instanceof Error ? error.message : fallback;
|
|
70
|
-
}
|
|
71
67
|
// ── Git helpers ──
|
|
72
68
|
async function isGitRepo(dirPath) {
|
|
73
69
|
try {
|
|
@@ -221,23 +217,6 @@ function parseStoredPathList(raw) {
|
|
|
221
217
|
return [];
|
|
222
218
|
}
|
|
223
219
|
}
|
|
224
|
-
const HIDDEN_CLAUDE_SESSIONS_KEY = "hidden_claude_sessions";
|
|
225
|
-
function getHiddenClaudeSessionIds(storage) {
|
|
226
|
-
return new Set(parseStoredPathList(storage.getConfigValue(HIDDEN_CLAUDE_SESSIONS_KEY)));
|
|
227
|
-
}
|
|
228
|
-
function saveHiddenClaudeSessionIds(storage, hidden) {
|
|
229
|
-
storage.setConfigValue(HIDDEN_CLAUDE_SESSIONS_KEY, JSON.stringify(Array.from(hidden)));
|
|
230
|
-
}
|
|
231
|
-
function removeFromHiddenClaudeSessionIds(storage, idsToRemove) {
|
|
232
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
233
|
-
let changed = false;
|
|
234
|
-
for (const id of idsToRemove) {
|
|
235
|
-
if (hidden.delete(id))
|
|
236
|
-
changed = true;
|
|
237
|
-
}
|
|
238
|
-
if (changed)
|
|
239
|
-
saveHiddenClaudeSessionIds(storage, hidden);
|
|
240
|
-
}
|
|
241
220
|
const MAX_RECENT_PATHS = 10;
|
|
242
221
|
// ── File language detection ──
|
|
243
222
|
function getLanguageFromExt(ext, filePath) {
|
|
@@ -289,18 +268,14 @@ export async function startServer(config, configPath) {
|
|
|
289
268
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
290
269
|
res.send(generatePwaManifest());
|
|
291
270
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
271
|
+
for (const [route, size] of [["/icon.svg", 192], ["/icon-192.png", 192], ["/icon-512.png", 512]]) {
|
|
272
|
+
app.get(route, (_req, res) => {
|
|
273
|
+
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, size));
|
|
274
|
+
});
|
|
275
|
+
}
|
|
295
276
|
const iconsDir = path.resolve(existsSync(path.join(SERVER_MODULE_DIR, "web-ui", "content"))
|
|
296
277
|
? path.join(SERVER_MODULE_DIR, "web-ui", "content")
|
|
297
278
|
: path.join(RUNTIME_ROOT_DIR, "src", "web-ui", "content"));
|
|
298
|
-
app.get("/icon-192.png", (_req, res) => {
|
|
299
|
-
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, 192));
|
|
300
|
-
});
|
|
301
|
-
app.get("/icon-512.png", (_req, res) => {
|
|
302
|
-
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, 512));
|
|
303
|
-
});
|
|
304
279
|
app.get("/sw.js", (_req, res) => {
|
|
305
280
|
res.setHeader("Content-Type", "application/javascript");
|
|
306
281
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -25,7 +25,6 @@ export class SessionLifecycleManager {
|
|
|
25
25
|
lastActivityAt: Date.now(),
|
|
26
26
|
};
|
|
27
27
|
this.sessions.set(sessionId, lifecycle);
|
|
28
|
-
console.error(`[Lifecycle] Session ${sessionId} registered with state: ${initialState}`);
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
31
30
|
* Update session state
|
|
@@ -33,7 +32,6 @@ export class SessionLifecycleManager {
|
|
|
33
32
|
setState(sessionId, newState) {
|
|
34
33
|
const lifecycle = this.sessions.get(sessionId);
|
|
35
34
|
if (!lifecycle) {
|
|
36
|
-
console.error(`[Lifecycle] Session ${sessionId} not found`);
|
|
37
35
|
return;
|
|
38
36
|
}
|
|
39
37
|
const oldState = lifecycle.state;
|
|
@@ -43,7 +41,6 @@ export class SessionLifecycleManager {
|
|
|
43
41
|
lifecycle.state = newState;
|
|
44
42
|
lifecycle.stateSince = Date.now();
|
|
45
43
|
lifecycle.lastActivityAt = Date.now();
|
|
46
|
-
console.error(`[Lifecycle] Session ${sessionId} state changed: ${oldState} -> ${newState}`);
|
|
47
44
|
// Emit state change event
|
|
48
45
|
this.events.onStateChange?.(sessionId, oldState, newState);
|
|
49
46
|
}
|
|
@@ -89,7 +86,6 @@ export class SessionLifecycleManager {
|
|
|
89
86
|
lifecycle.stateSince = Date.now();
|
|
90
87
|
lifecycle.archivedBy = by;
|
|
91
88
|
lifecycle.archiveReason = reason;
|
|
92
|
-
console.error(`[Lifecycle] Session ${sessionId} archived: ${reason} (by: ${by})`);
|
|
93
89
|
// Emit archived event
|
|
94
90
|
this.events.onArchived?.(sessionId, reason);
|
|
95
91
|
}
|
|
@@ -98,7 +94,6 @@ export class SessionLifecycleManager {
|
|
|
98
94
|
*/
|
|
99
95
|
unregister(sessionId) {
|
|
100
96
|
this.sessions.delete(sessionId);
|
|
101
|
-
console.error(`[Lifecycle] Session ${sessionId} unregistered`);
|
|
102
97
|
}
|
|
103
98
|
/**
|
|
104
99
|
* Get session lifecycle
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -3,12 +3,22 @@ import type { ConversationTurn, ExecutionMode } from "./types.js";
|
|
|
3
3
|
export interface ShortcutLogContext {
|
|
4
4
|
/** Execution mode the session is running in (e.g. "managed", "full-access") */
|
|
5
5
|
mode: ExecutionMode;
|
|
6
|
+
/** Permission scope that was approved (e.g. "run_command", "write_file") */
|
|
7
|
+
scope?: string;
|
|
6
8
|
/** Whether auto-approve is active for this session */
|
|
7
9
|
autoApprove: boolean;
|
|
8
10
|
/** Whether a permission prompt was blocking at the time of the keypress */
|
|
9
11
|
permissionBlocked: boolean;
|
|
10
12
|
/** The actual input string sent to PTY */
|
|
11
13
|
input: string;
|
|
14
|
+
/** Auto-approve detection type: "strict" | "fallback" | "idle_probe" */
|
|
15
|
+
approveType?: string;
|
|
16
|
+
/** Fallback detection score */
|
|
17
|
+
score?: number;
|
|
18
|
+
/** Fallback detection matched keywords */
|
|
19
|
+
matched?: string[];
|
|
20
|
+
/** Whether the auto-approve was a false positive */
|
|
21
|
+
falsePositive?: boolean;
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
package/dist/types.d.ts
CHANGED
|
@@ -160,6 +160,13 @@ export interface SessionSnapshot {
|
|
|
160
160
|
resumedToSessionId?: string | null;
|
|
161
161
|
/** 服务器重启时是否自动恢复 */
|
|
162
162
|
autoRecovered?: boolean;
|
|
163
|
+
/** 自动批准统计(按类别分) */
|
|
164
|
+
approvalStats?: {
|
|
165
|
+
tool: number;
|
|
166
|
+
command: number;
|
|
167
|
+
file: number;
|
|
168
|
+
total: number;
|
|
169
|
+
};
|
|
163
170
|
}
|
|
164
171
|
export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
|
|
165
172
|
export interface SessionLifecycle {
|