@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/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 {
|
|
@@ -557,14 +417,15 @@ 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
|
},
|
|
566
424
|
});
|
|
567
425
|
for (const snapshot of this.storage.loadSessions()) {
|
|
426
|
+
if ((snapshot.sessionKind ?? "pty") !== "pty") {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
568
429
|
const isClaudeCmd = /^claude\b/.test(snapshot.command.trim());
|
|
569
430
|
const resumeCommandSessionId = getResumeCommandSessionId(snapshot.command);
|
|
570
431
|
// Sessions restored from storage have ptyProcess: null — the old server's PTY
|
|
@@ -599,7 +460,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
599
460
|
knownClaudeTaskIds: undefined,
|
|
600
461
|
claudeTaskDiscoveryTimer: null,
|
|
601
462
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
|
|
602
|
-
claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId
|
|
463
|
+
claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
|
|
464
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
603
465
|
});
|
|
604
466
|
this.lifecycleManager.register(snapshot.id, "idle");
|
|
605
467
|
console.error(`[ProcessManager] Restored session ${snapshot.id} marked as exited (PTY orphaned)`);
|
|
@@ -631,7 +493,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
631
493
|
knownClaudeTaskIds: undefined,
|
|
632
494
|
claudeTaskDiscoveryTimer: null,
|
|
633
495
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
|
|
634
|
-
claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId
|
|
496
|
+
claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
|
|
497
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
635
498
|
});
|
|
636
499
|
this.lifecycleManager.register(snapshot.id, "archived");
|
|
637
500
|
}
|
|
@@ -741,7 +604,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
741
604
|
lastEmittedTask: null,
|
|
742
605
|
knownClaudeTaskIds: knownClaudeTaskIds ?? undefined,
|
|
743
606
|
claudeTaskDiscoveryTimer: null,
|
|
744
|
-
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined
|
|
607
|
+
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
608
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
745
609
|
};
|
|
746
610
|
// Create PTY bridge for this session
|
|
747
611
|
record.ptyBridge = new ClaudePtyBridge({
|
|
@@ -844,7 +708,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
844
708
|
process.stderr.write(`[wand] Cannot send initial input: session not ready\n`);
|
|
845
709
|
return;
|
|
846
710
|
}
|
|
847
|
-
process.stderr.write(`[wand] Sending initial input
|
|
711
|
+
process.stderr.write(`[wand] Sending initial input (${initialInput.length} chars)\n`);
|
|
848
712
|
// Track initial input via bridge for Chat mode
|
|
849
713
|
if (current.ptyBridge) {
|
|
850
714
|
current.ptyBridge.onUserInput(initialInput);
|
|
@@ -1012,35 +876,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
1012
876
|
sendInput(id, input, view, shortcutKey) {
|
|
1013
877
|
const record = this.mustGet(id);
|
|
1014
878
|
if (record.status !== "running") {
|
|
1015
|
-
console.error(
|
|
1016
|
-
sessionId: id,
|
|
1017
|
-
status: record.status,
|
|
1018
|
-
hasPty: !!record.ptyProcess,
|
|
1019
|
-
inputLength: input.length,
|
|
1020
|
-
view: view ?? "chat"
|
|
1021
|
-
});
|
|
879
|
+
console.error(`[ProcessManager] Rejecting input: session ${id} not running (${record.status})`);
|
|
1022
880
|
throw new SessionInputError("Session is not running.", "SESSION_NOT_RUNNING", id, record.status);
|
|
1023
881
|
}
|
|
1024
882
|
// Update lifecycle
|
|
1025
883
|
this.lifecycleManager.touch(id);
|
|
1026
884
|
this.lifecycleManager.startThinking(id);
|
|
1027
885
|
if (!record.ptyProcess) {
|
|
1028
|
-
console.error(
|
|
1029
|
-
sessionId: id,
|
|
1030
|
-
status: record.status,
|
|
1031
|
-
hasPty: !!record.ptyProcess,
|
|
1032
|
-
inputLength: input.length,
|
|
1033
|
-
view: view ?? "chat"
|
|
1034
|
-
});
|
|
886
|
+
console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
|
|
1035
887
|
throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
|
|
1036
888
|
}
|
|
1037
|
-
console.error("[ProcessManager] Sending input to session", {
|
|
1038
|
-
sessionId: id,
|
|
1039
|
-
status: record.status,
|
|
1040
|
-
hasPty: !!record.ptyProcess,
|
|
1041
|
-
inputLength: input.length,
|
|
1042
|
-
view: view ?? "chat"
|
|
1043
|
-
});
|
|
1044
889
|
// Log shortcut key interactions for auto-confirm and mode analysis
|
|
1045
890
|
if (shortcutKey) {
|
|
1046
891
|
const outputLines = record.output.split("\n");
|
|
@@ -1233,6 +1078,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1233
1078
|
const messages = record.ptyBridge?.getMessages() ?? record.messages;
|
|
1234
1079
|
return {
|
|
1235
1080
|
id: record.id,
|
|
1081
|
+
sessionKind: "pty",
|
|
1082
|
+
runner: "pty",
|
|
1236
1083
|
command: record.command,
|
|
1237
1084
|
cwd: record.cwd,
|
|
1238
1085
|
mode: record.mode,
|
|
@@ -1253,7 +1100,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
1253
1100
|
messages: messages.length > 0 ? messages : undefined,
|
|
1254
1101
|
resumedFromSessionId: record.resumedFromSessionId ?? undefined,
|
|
1255
1102
|
resumedToSessionId: record.resumedToSessionId ?? undefined,
|
|
1256
|
-
autoRecovered: record.autoRecovered ?? false
|
|
1103
|
+
autoRecovered: record.autoRecovered ?? false,
|
|
1104
|
+
autoApprovePermissions: record.autoApprovePermissions || undefined,
|
|
1105
|
+
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined
|
|
1257
1106
|
};
|
|
1258
1107
|
}
|
|
1259
1108
|
isPermissionBlocked(record) {
|
|
@@ -1274,6 +1123,15 @@ export class ProcessManager extends EventEmitter {
|
|
|
1274
1123
|
denyPermission(id) {
|
|
1275
1124
|
return this.resolvePermission(id, "deny");
|
|
1276
1125
|
}
|
|
1126
|
+
toggleAutoApprove(id) {
|
|
1127
|
+
const record = this.mustGet(id);
|
|
1128
|
+
record.autoApprovePermissions = !record.autoApprovePermissions;
|
|
1129
|
+
if (record.ptyBridge) {
|
|
1130
|
+
record.ptyBridge.setAutoApprove(record.autoApprovePermissions);
|
|
1131
|
+
}
|
|
1132
|
+
this.persist(record);
|
|
1133
|
+
return this.snapshot(record);
|
|
1134
|
+
}
|
|
1277
1135
|
/**
|
|
1278
1136
|
* Canonical permission resolution method.
|
|
1279
1137
|
* All other permission methods delegate to this.
|
|
@@ -1493,11 +1351,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
1493
1351
|
|| claudeConfirmPrompt
|
|
1494
1352
|
|| toolPermissionPrompt
|
|
1495
1353
|
|| (hasExplicitConfirmSyntax(normalized)
|
|
1496
|
-
&& hasPermissionActionContext(normalized)
|
|
1497
|
-
&& PROMPT_PATTERNS.some((pattern) => pattern.test(normalized)));
|
|
1354
|
+
&& hasPermissionActionContext(normalized));
|
|
1498
1355
|
if (shouldConfirm) {
|
|
1499
1356
|
record.lastAutoConfirmAt = now;
|
|
1500
|
-
process.stderr.write(`[wand] Auto-confirming prompt for ${record.mode} mode\n`);
|
|
1501
1357
|
// Always auto-confirm by sending Enter directly
|
|
1502
1358
|
ptyProcess.write("\r");
|
|
1503
1359
|
}
|
|
@@ -1562,15 +1418,50 @@ export class ProcessManager extends EventEmitter {
|
|
|
1562
1418
|
});
|
|
1563
1419
|
break;
|
|
1564
1420
|
}
|
|
1565
|
-
case "permission.resolved":
|
|
1421
|
+
case "permission.resolved": {
|
|
1422
|
+
// Increment approval stats before clearing pendingEscalation
|
|
1423
|
+
const resolvedScope = record.pendingEscalation?.scope;
|
|
1424
|
+
if (resolvedScope) {
|
|
1425
|
+
if (resolvedScope === "run_command" || resolvedScope === "dangerous_shell") {
|
|
1426
|
+
record.approvalStats.command++;
|
|
1427
|
+
}
|
|
1428
|
+
else if (resolvedScope === "write_file") {
|
|
1429
|
+
record.approvalStats.file++;
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
record.approvalStats.tool++;
|
|
1433
|
+
}
|
|
1434
|
+
record.approvalStats.total++;
|
|
1435
|
+
}
|
|
1566
1436
|
record.pendingEscalation = null;
|
|
1567
1437
|
record.ptyPermissionBlocked = false;
|
|
1568
1438
|
this.emitEvent({
|
|
1569
1439
|
type: "status",
|
|
1570
1440
|
sessionId: event.sessionId,
|
|
1571
|
-
data: {
|
|
1441
|
+
data: {
|
|
1442
|
+
permissionBlocked: false,
|
|
1443
|
+
approvalStats: record.approvalStats,
|
|
1444
|
+
},
|
|
1572
1445
|
});
|
|
1446
|
+
// Log auto-approve events to shortcut-interactions.jsonl for analysis
|
|
1447
|
+
const resolvedData = event.data;
|
|
1448
|
+
if (resolvedData?.autoApproved) {
|
|
1449
|
+
const outputLines = record.output.split("\n");
|
|
1450
|
+
const tailLines = outputLines.slice(-8).join("\n");
|
|
1451
|
+
this.logger.appendShortcutLog(record.id, "auto_approve", tailLines, {
|
|
1452
|
+
mode: record.mode,
|
|
1453
|
+
scope: resolvedScope ?? "unknown",
|
|
1454
|
+
autoApprove: record.autoApprovePermissions,
|
|
1455
|
+
permissionBlocked: true,
|
|
1456
|
+
input: "\r",
|
|
1457
|
+
approveType: resolvedData.approveType,
|
|
1458
|
+
score: resolvedData.score,
|
|
1459
|
+
matched: resolvedData.matched,
|
|
1460
|
+
falsePositive: resolvedData.falsePositive,
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1573
1463
|
break;
|
|
1464
|
+
}
|
|
1574
1465
|
case "session.id":
|
|
1575
1466
|
// Claude session ID captured - already handled in onData
|
|
1576
1467
|
break;
|
|
@@ -1633,15 +1524,42 @@ export class ProcessManager extends EventEmitter {
|
|
|
1633
1524
|
return false;
|
|
1634
1525
|
}
|
|
1635
1526
|
processCommandForMode(command, mode) {
|
|
1527
|
+
const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
|
|
1528
|
+
if (!isClaudeCmd)
|
|
1529
|
+
return command;
|
|
1530
|
+
let result = command;
|
|
1531
|
+
// Skip if command already contains --permission-mode
|
|
1532
|
+
const hasPermFlag = /--permission-mode\s/.test(command);
|
|
1533
|
+
if (!hasPermFlag) {
|
|
1534
|
+
if (isRunningAsRoot()) {
|
|
1535
|
+
// Root: Claude CLI refuses --permission-mode bypassPermissions.
|
|
1536
|
+
// Use acceptEdits + --allowedTools to auto-approve all tool calls
|
|
1537
|
+
// regardless of whether the target path is inside or outside the CWD.
|
|
1538
|
+
if (mode === "managed" || mode === "full-access" || mode === "auto-edit") {
|
|
1539
|
+
result += " --permission-mode acceptEdits";
|
|
1540
|
+
result += " --allowedTools Bash Edit Write Read Glob Grep NotebookEdit WebFetch WebSearch";
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
// Non-root: use bypassPermissions for full-access (skips all prompts),
|
|
1545
|
+
// acceptEdits for auto-edit (auto-accepts file writes, prompts for bash).
|
|
1546
|
+
if (mode === "full-access" || mode === "managed") {
|
|
1547
|
+
result += " --permission-mode bypassPermissions";
|
|
1548
|
+
}
|
|
1549
|
+
else if (mode === "auto-edit") {
|
|
1550
|
+
result += " --permission-mode acceptEdits";
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1636
1554
|
// In managed mode, append a system prompt instructing Claude to act autonomously
|
|
1637
1555
|
// without asking the user for confirmation, since the user may not be monitoring.
|
|
1638
|
-
if (mode === "managed"
|
|
1556
|
+
if (mode === "managed") {
|
|
1639
1557
|
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.";
|
|
1640
1558
|
// Escape single quotes for shell safety
|
|
1641
1559
|
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1642
|
-
|
|
1560
|
+
result += ` --append-system-prompt '${escaped}'`;
|
|
1643
1561
|
}
|
|
1644
|
-
return
|
|
1562
|
+
return result;
|
|
1645
1563
|
}
|
|
1646
1564
|
}
|
|
1647
1565
|
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
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Express } from "express";
|
|
2
2
|
import { ProcessManager } from "./process-manager.js";
|
|
3
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
3
4
|
import { WandStorage } from "./storage.js";
|
|
4
5
|
import { ExecutionMode } from "./types.js";
|
|
5
|
-
export declare function
|
|
6
|
+
export declare function getErrorMessage(error: unknown, fallback: string): string;
|
|
7
|
+
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode): void;
|
|
6
8
|
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
|