@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.
@@ -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, hasLiveProjectConversation, hasRealConversationMessages, hasStoredProjectConversation, shouldAllowResume, shouldDisplayResumeAction, } from "./resume-policy.js";
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: (sessionId) => {
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: ${initialInput}\n`);
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("[ProcessManager] Rejecting input for non-running session", {
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("[ProcessManager] Rejecting input because PTY is missing", {
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: { permissionBlocked: false },
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" && /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command)) {
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
- return `${command} --append-system-prompt '${escaped}'`;
1560
+ result += ` --append-system-prompt '${escaped}'`;
1643
1561
  }
1644
- return command;
1562
+ return result;
1645
1563
  }
1646
1564
  }
1647
1565
  function clampDimension(value, min, max) {
@@ -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 {};
@@ -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.*\bno\b/i,
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 registerSessionRoutes(app: Express, processes: ProcessManager, storage: WandStorage, defaultMode: ExecutionMode): void;
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;