@scotthuang/agent-knock-knock 0.2.44 → 0.2.47
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/CHANGELOG.md +33 -0
- package/README.md +148 -242
- package/dist/src/approval-policy.d.ts +1 -0
- package/dist/src/approval-policy.js +6 -0
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-installer.d.ts +65 -0
- package/dist/src/claude-hook-installer.js +410 -0
- package/dist/src/claude-hook-installer.js.map +1 -0
- package/dist/src/claude-hook-protocol.d.ts +89 -0
- package/dist/src/claude-hook-protocol.js +243 -0
- package/dist/src/claude-hook-protocol.js.map +1 -0
- package/dist/src/claude-hook-store.d.ts +247 -0
- package/dist/src/claude-hook-store.js +1064 -0
- package/dist/src/claude-hook-store.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +47 -0
- package/dist/src/claude-terminal-agent-adapter.js +920 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -0
- package/dist/src/cli.js +1565 -772
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.d.ts +6 -32
- package/dist/src/codex-session-provider.js +2 -1
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +3 -9
- package/dist/src/codex-store-adapter.js +5 -55
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +34 -0
- package/dist/src/codex-terminal-agent-adapter.js +386 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin.js +60 -11
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +158 -0
- package/dist/src/terminal-agent-adapter.js +109 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -0
- package/dist/src/terminal-agent-bridge.d.ts +114 -0
- package/dist/src/terminal-agent-bridge.js +487 -0
- package/dist/src/terminal-agent-bridge.js.map +1 -0
- package/dist/src/terminal-agent-registry.d.ts +7 -0
- package/dist/src/terminal-agent-registry.js +24 -0
- package/dist/src/terminal-agent-registry.js.map +1 -0
- package/dist/src/terminal-control-provider.d.ts +8 -6
- package/dist/src/terminal-control-provider.js +5 -5
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-process-source.d.ts +25 -0
- package/dist/src/terminal-process-source.js +77 -0
- package/dist/src/terminal-process-source.js.map +1 -0
- package/openclaw.plugin.json +7 -3
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +19 -16
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { redactString } from "./runtime-log.js";
|
|
4
|
+
const CLAUDE_SUBCOMMANDS = new Set([
|
|
5
|
+
"agents",
|
|
6
|
+
"auth",
|
|
7
|
+
"auto-mode",
|
|
8
|
+
"doctor",
|
|
9
|
+
"gateway",
|
|
10
|
+
"install",
|
|
11
|
+
"mcp",
|
|
12
|
+
"plugin",
|
|
13
|
+
"plugins",
|
|
14
|
+
"project",
|
|
15
|
+
"setup-token",
|
|
16
|
+
"ultrareview",
|
|
17
|
+
"update",
|
|
18
|
+
"upgrade"
|
|
19
|
+
]);
|
|
20
|
+
const NON_INTERACTIVE_FLAGS = new Set([
|
|
21
|
+
"-h",
|
|
22
|
+
"--help",
|
|
23
|
+
"-p",
|
|
24
|
+
"--print",
|
|
25
|
+
"-v",
|
|
26
|
+
"--version",
|
|
27
|
+
"--bg",
|
|
28
|
+
"--background"
|
|
29
|
+
]);
|
|
30
|
+
const OPTIONS_WITH_VALUES = new Set([
|
|
31
|
+
"--add-dir",
|
|
32
|
+
"--agent",
|
|
33
|
+
"--agents",
|
|
34
|
+
"--allowedTools",
|
|
35
|
+
"--allowed-tools",
|
|
36
|
+
"--append-system-prompt",
|
|
37
|
+
"--betas",
|
|
38
|
+
"--debug-file",
|
|
39
|
+
"--disallowedTools",
|
|
40
|
+
"--disallowed-tools",
|
|
41
|
+
"--effort",
|
|
42
|
+
"--fallback-model",
|
|
43
|
+
"--file",
|
|
44
|
+
"--from-pr",
|
|
45
|
+
"--input-format",
|
|
46
|
+
"--json-schema",
|
|
47
|
+
"--max-budget-usd",
|
|
48
|
+
"--mcp-config",
|
|
49
|
+
"--model",
|
|
50
|
+
"-n",
|
|
51
|
+
"--name",
|
|
52
|
+
"--output-format",
|
|
53
|
+
"--permission-mode",
|
|
54
|
+
"--plugin-dir",
|
|
55
|
+
"--plugin-url",
|
|
56
|
+
"--remote-control-session-name-prefix",
|
|
57
|
+
"--setting-sources",
|
|
58
|
+
"--settings",
|
|
59
|
+
"--system-prompt",
|
|
60
|
+
"--tools"
|
|
61
|
+
]);
|
|
62
|
+
const OPTIONAL_VALUE_OPTIONS = new Set([
|
|
63
|
+
"-d",
|
|
64
|
+
"--debug",
|
|
65
|
+
"--prompt-suggestions",
|
|
66
|
+
"--remote-control",
|
|
67
|
+
"-w",
|
|
68
|
+
"--worktree"
|
|
69
|
+
]);
|
|
70
|
+
const CLAUDE_SCREEN_TAIL_LINES = 48;
|
|
71
|
+
const CLAUDE_EXCERPT_LINES = 80;
|
|
72
|
+
const CLAUDE_PERMISSION_DETAIL_LENGTH = 600;
|
|
73
|
+
const CLAUDE_AUTO_APPROVAL_COMMAND_LENGTH = 2000;
|
|
74
|
+
const TRUSTED_CLAUDE_SHELL_PATHS = new Set([
|
|
75
|
+
"/bin/bash",
|
|
76
|
+
"/bin/dash",
|
|
77
|
+
"/bin/sh",
|
|
78
|
+
"/bin/zsh",
|
|
79
|
+
"/usr/bin/bash",
|
|
80
|
+
"/usr/bin/dash",
|
|
81
|
+
"/usr/bin/sh",
|
|
82
|
+
"/usr/bin/zsh"
|
|
83
|
+
]);
|
|
84
|
+
const TRUSTED_CLAUDE_SHELL_TARGETS = new Set([...TRUSTED_CLAUDE_SHELL_PATHS].flatMap((shell) => {
|
|
85
|
+
try {
|
|
86
|
+
return [fs.realpathSync(shell)];
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
const ANSI_ESCAPE_PATTERN = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu;
|
|
93
|
+
export function createClaudeTerminalAgentAdapter(options = {}) {
|
|
94
|
+
const agentRows = options.agentRows ?? [];
|
|
95
|
+
const hookStore = options.hookStore;
|
|
96
|
+
const trustedTokenjuiceLaunchers = options.trustedTokenjuiceLaunchers ?? [];
|
|
97
|
+
return {
|
|
98
|
+
agent: "claude",
|
|
99
|
+
displayName: "Claude Code",
|
|
100
|
+
capabilities: {
|
|
101
|
+
processDiscovery: true,
|
|
102
|
+
screenStatus: true,
|
|
103
|
+
terminalApproval: true,
|
|
104
|
+
// A visible idle prompt is not durable proof that the requested turn completed.
|
|
105
|
+
screenCompletion: false,
|
|
106
|
+
durableCompletion: hookStore !== undefined,
|
|
107
|
+
cancellation: true
|
|
108
|
+
},
|
|
109
|
+
cancelKeys: ["Escape"],
|
|
110
|
+
classifyProcess(snapshot) {
|
|
111
|
+
return classifyClaudeProcess(snapshot, agentRows);
|
|
112
|
+
},
|
|
113
|
+
inspectScreen(screenOptions) {
|
|
114
|
+
return inspectClaudeScreenWithHooks(screenOptions, hookStore, trustedTokenjuiceLaunchers);
|
|
115
|
+
},
|
|
116
|
+
...(hookStore
|
|
117
|
+
? {
|
|
118
|
+
async resolveApproval(request) {
|
|
119
|
+
return resolveClaudeHookApproval(hookStore, request);
|
|
120
|
+
},
|
|
121
|
+
async detectDurableCompletion(request) {
|
|
122
|
+
return detectClaudeHookCompletion(hookStore, request);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
: {})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export const claudeTerminalAgentAdapter = createClaudeTerminalAgentAdapter();
|
|
129
|
+
export function classifyClaudeProcess(snapshot, agentRows = []) {
|
|
130
|
+
const tokens = tokenizeCommand(snapshot.command);
|
|
131
|
+
if (tokens.length === 0 || path.basename(tokens[0]) !== "claude") {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const args = tokens.slice(1);
|
|
135
|
+
if (args.some((token) => isNonInteractiveFlag(token))) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (findClaudeSubcommand(args)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const agentRow = exactInteractiveAgentRow(snapshot.pid, agentRows);
|
|
142
|
+
if (agentRow === null) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const commandSessionId = extractClaudeSessionId(snapshot.command);
|
|
146
|
+
// `--resume` can retain an old argv value after Claude changes sessions. Exact-PID agent
|
|
147
|
+
// metadata is the current runtime identity and therefore wins on conflict.
|
|
148
|
+
const sessionId = nonEmptyString(agentRow?.sessionId) ?? commandSessionId;
|
|
149
|
+
const cwd = nonEmptyString(snapshot.cwd) ?? nonEmptyString(agentRow?.cwd);
|
|
150
|
+
const confidence = agentRow
|
|
151
|
+
? "high"
|
|
152
|
+
: sessionId
|
|
153
|
+
? "high"
|
|
154
|
+
: cwd
|
|
155
|
+
? "medium"
|
|
156
|
+
: "low";
|
|
157
|
+
const reason = agentRow
|
|
158
|
+
? "interactive Claude CLI matched an exact PID from claude agents --json --all"
|
|
159
|
+
: sessionId
|
|
160
|
+
? "interactive Claude CLI command includes a session id"
|
|
161
|
+
: "interactive Claude CLI process without an exact agent metadata row";
|
|
162
|
+
return {
|
|
163
|
+
...snapshot,
|
|
164
|
+
cwd,
|
|
165
|
+
agent: "claude",
|
|
166
|
+
kind: "claude_cli",
|
|
167
|
+
sessionId,
|
|
168
|
+
confidence,
|
|
169
|
+
reason
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function extractClaudeSessionId(command) {
|
|
173
|
+
const args = tokenizeCommand(command).slice(1);
|
|
174
|
+
return optionValue(args, "--session-id") ??
|
|
175
|
+
optionValue(args, "--resume") ??
|
|
176
|
+
shortOptionValue(args, "-r");
|
|
177
|
+
}
|
|
178
|
+
export function inspectClaudeScreen(options) {
|
|
179
|
+
const approval = detectClaudeApprovalPrompt(options.screen);
|
|
180
|
+
return {
|
|
181
|
+
activity: detectClaudeActivityState(options.screen, approval),
|
|
182
|
+
approval,
|
|
183
|
+
screenExcerpt: claudeScreenExcerpt(options.screen, options.maxExcerptLength ?? 4000)
|
|
184
|
+
// Deliberately no screen completion: idle alone can follow cancellation, errors, or old output.
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function inspectClaudeScreenWithHooks(options, hookStore, trustedTokenjuiceLaunchers) {
|
|
188
|
+
const screenInspection = inspectClaudeScreen(options);
|
|
189
|
+
if (!hookStore) {
|
|
190
|
+
return screenInspection;
|
|
191
|
+
}
|
|
192
|
+
const identity = exactClaudeHookIdentity(options.runtime);
|
|
193
|
+
if (!identity) {
|
|
194
|
+
return screenInspection.approval.blocked
|
|
195
|
+
? unverifiedHookApproval(screenInspection, "Claude hook permission could not be verified without an exact session id or pid")
|
|
196
|
+
: screenInspection;
|
|
197
|
+
}
|
|
198
|
+
let hookInspection;
|
|
199
|
+
try {
|
|
200
|
+
hookInspection = hookStore.inspectPermission(identity);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return screenInspection.approval.blocked
|
|
204
|
+
? unverifiedHookApproval(screenInspection, `Claude hook permission identity could not be verified: ${errorMessage(error)}`)
|
|
205
|
+
: screenInspection;
|
|
206
|
+
}
|
|
207
|
+
if (hookInspection.blocked) {
|
|
208
|
+
return hookPermissionScreenInspection(screenInspection, hookInspection, trustedTokenjuiceLaunchers);
|
|
209
|
+
}
|
|
210
|
+
if (screenInspection.approval.blocked) {
|
|
211
|
+
return unverifiedHookApproval(screenInspection, `Claude terminal permission is visible but no current structured hook request matched: ${hookInspection.reason}`);
|
|
212
|
+
}
|
|
213
|
+
return screenInspection;
|
|
214
|
+
}
|
|
215
|
+
function hookPermissionScreenInspection(screenInspection, hookInspection, trustedTokenjuiceLaunchers) {
|
|
216
|
+
if (!hookInspection.approvable) {
|
|
217
|
+
return {
|
|
218
|
+
...screenInspection,
|
|
219
|
+
activity: {
|
|
220
|
+
state: "awaiting_approval",
|
|
221
|
+
reason: hookInspection.reason
|
|
222
|
+
},
|
|
223
|
+
approval: {
|
|
224
|
+
blocked: true,
|
|
225
|
+
approvable: false,
|
|
226
|
+
reason: hookInspection.reason,
|
|
227
|
+
promptKind: "claude_permission"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const display = claudePermissionDisplay(hookInspection.toolName, hookInspection.toolInput);
|
|
232
|
+
const command = hookInspection.command === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: claudePermissionCommandForApproval(hookInspection.command, trustedTokenjuiceLaunchers);
|
|
235
|
+
const requestDetail = command && command.command === undefined
|
|
236
|
+
? [
|
|
237
|
+
command.display ? `Command: ${command.display}` : undefined,
|
|
238
|
+
display.requestDetail
|
|
239
|
+
].filter((value) => Boolean(value))
|
|
240
|
+
.join("; ")
|
|
241
|
+
.slice(0, CLAUDE_PERMISSION_DETAIL_LENGTH)
|
|
242
|
+
: display.requestDetail;
|
|
243
|
+
return {
|
|
244
|
+
...screenInspection,
|
|
245
|
+
activity: {
|
|
246
|
+
state: "awaiting_approval",
|
|
247
|
+
reason: hookInspection.reason
|
|
248
|
+
},
|
|
249
|
+
approval: {
|
|
250
|
+
blocked: true,
|
|
251
|
+
approvable: true,
|
|
252
|
+
promptKind: hookInspection.promptKind,
|
|
253
|
+
toolName: display.toolName,
|
|
254
|
+
...(requestDetail ? { requestDetail } : {}),
|
|
255
|
+
...(command?.command === undefined ? {} : { command: command.command }),
|
|
256
|
+
action: {
|
|
257
|
+
mode: "structured",
|
|
258
|
+
keys: [],
|
|
259
|
+
label: "Allow once",
|
|
260
|
+
requestId: hookInspection.requestId
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Tokenjuice's official Claude PreToolUse hook wraps Bash through a login shell before
|
|
267
|
+
* PermissionRequest runs. Recover only that exact, source-tagged shape so policy matching
|
|
268
|
+
* sees Claude's original command; every other wrapper remains visible and fails closed.
|
|
269
|
+
*/
|
|
270
|
+
export function normalizeClaudePermissionCommand(command, trustedTokenjuiceLaunchers = []) {
|
|
271
|
+
const tokens = tokenizeTrustedWrapperCommand(command);
|
|
272
|
+
let normalized = command;
|
|
273
|
+
const launcher = tokens?.[0];
|
|
274
|
+
const canonicalLauncher = launcher && path.isAbsolute(launcher)
|
|
275
|
+
? realpathOrUndefined(launcher)
|
|
276
|
+
: undefined;
|
|
277
|
+
const trustedLauncher = launcher
|
|
278
|
+
? trustedTokenjuiceLaunchers.find((candidate) => candidate.configuredPath === launcher)
|
|
279
|
+
: undefined;
|
|
280
|
+
if (tokens &&
|
|
281
|
+
canonicalLauncher &&
|
|
282
|
+
trustedLauncher?.canonicalPath === canonicalLauncher &&
|
|
283
|
+
tokens[1] === "wrap") {
|
|
284
|
+
let index = 2;
|
|
285
|
+
if (tokens[index] === "--source" && tokens[index + 1] === "claude-code") {
|
|
286
|
+
index += 2;
|
|
287
|
+
}
|
|
288
|
+
else if (tokens[index] === "--source=claude-code") {
|
|
289
|
+
index += 1;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
return command;
|
|
293
|
+
}
|
|
294
|
+
const shell = tokens[index + 1];
|
|
295
|
+
const canonicalShell = shell && path.isAbsolute(shell)
|
|
296
|
+
? realpathOrUndefined(shell)
|
|
297
|
+
: undefined;
|
|
298
|
+
if (tokens[index] === "--" &&
|
|
299
|
+
shell !== undefined &&
|
|
300
|
+
TRUSTED_CLAUDE_SHELL_PATHS.has(shell) &&
|
|
301
|
+
canonicalShell !== undefined &&
|
|
302
|
+
TRUSTED_CLAUDE_SHELL_TARGETS.has(canonicalShell) &&
|
|
303
|
+
tokens[index + 2] === "-lc" &&
|
|
304
|
+
tokens.length === index + 4 &&
|
|
305
|
+
tokens[index + 3]) {
|
|
306
|
+
normalized = tokens[index + 3];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return normalized;
|
|
310
|
+
}
|
|
311
|
+
export function claudePermissionCommandForApproval(command, trustedTokenjuiceLaunchers = []) {
|
|
312
|
+
const normalized = normalizeClaudePermissionCommand(command, trustedTokenjuiceLaunchers);
|
|
313
|
+
const redacted = redactString(normalized);
|
|
314
|
+
const display = singleLineClaudePermissionValue(redacted, CLAUDE_PERMISSION_DETAIL_LENGTH - 20);
|
|
315
|
+
const policySafe = normalized.length <= CLAUDE_AUTO_APPROVAL_COMMAND_LENGTH && redacted === normalized;
|
|
316
|
+
return {
|
|
317
|
+
...(policySafe && normalized.trim() ? { command: normalized } : {}),
|
|
318
|
+
display
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function claudePermissionDisplay(toolName, toolInput) {
|
|
322
|
+
const safeToolName = singleLineClaudePermissionValue(toolName, 120) || "Unknown Claude tool";
|
|
323
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
324
|
+
return { toolName: safeToolName };
|
|
325
|
+
}
|
|
326
|
+
const input = toolInput;
|
|
327
|
+
const fields = [
|
|
328
|
+
["file_path", "File"],
|
|
329
|
+
["path", "Path"],
|
|
330
|
+
["notebook_path", "Notebook"],
|
|
331
|
+
["url", "URL"],
|
|
332
|
+
["query", "Query"],
|
|
333
|
+
["pattern", "Pattern"],
|
|
334
|
+
["description", "Description"],
|
|
335
|
+
["domain", "Domain"]
|
|
336
|
+
];
|
|
337
|
+
const details = [];
|
|
338
|
+
for (const [key, label] of fields) {
|
|
339
|
+
const value = input[key];
|
|
340
|
+
if (typeof value !== "string") {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const safeValue = singleLineClaudePermissionValue(value, 300);
|
|
344
|
+
if (safeValue) {
|
|
345
|
+
details.push(`${label}: ${safeValue}`);
|
|
346
|
+
}
|
|
347
|
+
if (details.length === 3) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const requestDetail = details.join("; ").slice(0, CLAUDE_PERMISSION_DETAIL_LENGTH);
|
|
352
|
+
return requestDetail ? { toolName: safeToolName, requestDetail } : { toolName: safeToolName };
|
|
353
|
+
}
|
|
354
|
+
function singleLineClaudePermissionValue(value, maxLength) {
|
|
355
|
+
return redactString(value)
|
|
356
|
+
.replace(/[\u0000-\u001F\u007F]+/gu, " ")
|
|
357
|
+
.replace(/\s+/gu, " ")
|
|
358
|
+
.trim()
|
|
359
|
+
.slice(0, maxLength);
|
|
360
|
+
}
|
|
361
|
+
function unverifiedHookApproval(screenInspection, reason) {
|
|
362
|
+
return {
|
|
363
|
+
...screenInspection,
|
|
364
|
+
activity: {
|
|
365
|
+
state: "awaiting_approval",
|
|
366
|
+
reason
|
|
367
|
+
},
|
|
368
|
+
approval: {
|
|
369
|
+
blocked: true,
|
|
370
|
+
approvable: false,
|
|
371
|
+
reason,
|
|
372
|
+
promptKind: screenInspection.approval.promptKind ?? "claude_permission",
|
|
373
|
+
...(screenInspection.approval.command === undefined
|
|
374
|
+
? {}
|
|
375
|
+
: { command: screenInspection.approval.command })
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async function resolveClaudeHookApproval(hookStore, request) {
|
|
380
|
+
if (request.expectedFingerprint !== request.actualFingerprint) {
|
|
381
|
+
return {
|
|
382
|
+
resolved: false,
|
|
383
|
+
reason: "Claude approval fingerprint changed before the structured decision"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (!request.inspection.approval.approvable ||
|
|
387
|
+
request.inspection.approval.action.mode !== "structured" ||
|
|
388
|
+
!request.inspection.approval.action.requestId) {
|
|
389
|
+
return {
|
|
390
|
+
resolved: false,
|
|
391
|
+
reason: "Claude approval is not a current structured hook request"
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const runtime = request.runtime;
|
|
395
|
+
const identity = exactClaudeHookIdentity(runtime);
|
|
396
|
+
if (!identity || !runtime?.conversationId || !runtime.messageId) {
|
|
397
|
+
return {
|
|
398
|
+
resolved: false,
|
|
399
|
+
requestId: request.inspection.approval.action.requestId,
|
|
400
|
+
reason: "Claude structured approval requires exact session or pid plus conversation and message identity"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const current = hookStore.inspectPermission(identity);
|
|
405
|
+
if (!current.blocked || !current.approvable) {
|
|
406
|
+
return {
|
|
407
|
+
resolved: false,
|
|
408
|
+
requestId: request.inspection.approval.action.requestId,
|
|
409
|
+
reason: current.reason
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (current.requestId !== request.inspection.approval.action.requestId) {
|
|
413
|
+
return {
|
|
414
|
+
resolved: false,
|
|
415
|
+
requestId: current.requestId,
|
|
416
|
+
reason: "Claude permission request changed before the structured decision"
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (current.conversationId !== runtime.conversationId || current.messageId !== runtime.messageId) {
|
|
420
|
+
return {
|
|
421
|
+
resolved: false,
|
|
422
|
+
requestId: current.requestId,
|
|
423
|
+
reason: "Claude permission request belongs to a different conversation message"
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (runtime.sessionId !== undefined && current.sessionId !== runtime.sessionId) {
|
|
427
|
+
return {
|
|
428
|
+
resolved: false,
|
|
429
|
+
requestId: current.requestId,
|
|
430
|
+
reason: "Claude permission request belongs to a different session"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const lease = hookStore.resolveLease(identity);
|
|
434
|
+
if (!lease?.authorizationEligible ||
|
|
435
|
+
lease.lease.conversationId !== runtime.conversationId ||
|
|
436
|
+
lease.lease.messageId !== runtime.messageId ||
|
|
437
|
+
(runtime.terminalTarget !== undefined && lease.lease.terminalTarget !== runtime.terminalTarget)) {
|
|
438
|
+
return {
|
|
439
|
+
resolved: false,
|
|
440
|
+
requestId: current.requestId,
|
|
441
|
+
reason: "Claude managed lease no longer matches the terminal conversation"
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
hookStore.decidePermission({
|
|
445
|
+
sessionId: current.sessionId,
|
|
446
|
+
requestId: current.requestId,
|
|
447
|
+
fingerprint: current.fingerprint,
|
|
448
|
+
conversationId: runtime.conversationId,
|
|
449
|
+
messageId: runtime.messageId,
|
|
450
|
+
decision: request.decision,
|
|
451
|
+
...(request.decision === "deny" && request.interrupt !== undefined
|
|
452
|
+
? { interrupt: request.interrupt }
|
|
453
|
+
: {})
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
resolved: true,
|
|
457
|
+
requestId: current.requestId,
|
|
458
|
+
reason: request.decision === "allow"
|
|
459
|
+
? "Claude one-time permission was allowed through the structured hook"
|
|
460
|
+
: "Claude permission was denied through the structured hook"
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return {
|
|
465
|
+
resolved: false,
|
|
466
|
+
requestId: request.inspection.approval.action.requestId,
|
|
467
|
+
reason: `Claude structured permission could not be resolved: ${errorMessage(error)}`
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function detectClaudeHookCompletion(hookStore, request) {
|
|
472
|
+
if (!request.startedAt) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
const runtime = completionRuntime(request.context);
|
|
476
|
+
if (!runtime.conversationId || !runtime.messageId) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
const identity = {
|
|
480
|
+
...(runtime.sessionId ?? request.sessionId
|
|
481
|
+
? { sessionId: runtime.sessionId ?? request.sessionId }
|
|
482
|
+
: {}),
|
|
483
|
+
...(runtime.pid === undefined ? {} : { pid: runtime.pid }),
|
|
484
|
+
...(runtime.cwd ?? request.cwd ? { cwd: runtime.cwd ?? request.cwd } : {}),
|
|
485
|
+
requireUnique: true
|
|
486
|
+
};
|
|
487
|
+
if (!identity.sessionId && identity.pid === undefined && !identity.cwd) {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
let completion;
|
|
491
|
+
try {
|
|
492
|
+
completion = hookStore.detectCompletion({
|
|
493
|
+
...identity,
|
|
494
|
+
startedAt: request.startedAt,
|
|
495
|
+
...(runtime.promptId === undefined ? {} : { promptId: runtime.promptId }),
|
|
496
|
+
conversationId: runtime.conversationId,
|
|
497
|
+
messageId: runtime.messageId
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
if (completion.status === "done") {
|
|
504
|
+
return {
|
|
505
|
+
source: "durable",
|
|
506
|
+
outcome: "success",
|
|
507
|
+
text: completion.text,
|
|
508
|
+
timestamp: completion.timestamp,
|
|
509
|
+
id: completion.eventId,
|
|
510
|
+
confidence: "high",
|
|
511
|
+
metadata: {
|
|
512
|
+
match: "claude_stop_hook",
|
|
513
|
+
session_id: completion.sessionId,
|
|
514
|
+
prompt_id: completion.promptId,
|
|
515
|
+
cwd: completion.cwd
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (completion.status === "failed") {
|
|
520
|
+
return {
|
|
521
|
+
source: "durable",
|
|
522
|
+
outcome: "failure",
|
|
523
|
+
text: claudeFailureText(completion),
|
|
524
|
+
timestamp: completion.failure.receivedAt,
|
|
525
|
+
id: completion.failure.eventId,
|
|
526
|
+
confidence: "high",
|
|
527
|
+
metadata: {
|
|
528
|
+
match: "claude_stop_failure_hook",
|
|
529
|
+
session_id: completion.sessionId,
|
|
530
|
+
prompt_id: completion.promptId,
|
|
531
|
+
error: completion.failure.code,
|
|
532
|
+
...(completion.failure.details === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { error_details: completion.failure.details })
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
// Pending background work and unknown/incomplete hook payloads are not completion evidence.
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
function exactClaudeHookIdentity(runtime) {
|
|
542
|
+
if (!runtime || (runtime.sessionId === undefined && runtime.pid === undefined)) {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
...(runtime.sessionId === undefined ? {} : { sessionId: runtime.sessionId }),
|
|
547
|
+
...(runtime.pid === undefined ? {} : { pid: runtime.pid }),
|
|
548
|
+
...(runtime.cwd === undefined ? {} : { cwd: runtime.cwd }),
|
|
549
|
+
requireUnique: true
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function completionRuntime(context) {
|
|
553
|
+
const direct = recordValue(context);
|
|
554
|
+
const nativeTakeover = recordValue(direct?.nativeTakeover);
|
|
555
|
+
const conversation = recordValue(direct?.conversation);
|
|
556
|
+
return {
|
|
557
|
+
sessionId: nonEmptyString(nativeTakeover?.terminal_agent_session_id) ??
|
|
558
|
+
nonEmptyString(direct?.sessionId),
|
|
559
|
+
pid: positiveInteger(nativeTakeover?.terminal_agent_pid) ?? positiveInteger(direct?.pid),
|
|
560
|
+
cwd: nonEmptyString(nativeTakeover?.source_cwd) ?? nonEmptyString(direct?.cwd),
|
|
561
|
+
promptId: nonEmptyString(nativeTakeover?.terminal_agent_prompt_id) ??
|
|
562
|
+
nonEmptyString(direct?.promptId) ??
|
|
563
|
+
nonEmptyString(direct?.prompt_id),
|
|
564
|
+
conversationId: nonEmptyString(direct?.conversationId) ??
|
|
565
|
+
nonEmptyString(conversation?.conversation_id),
|
|
566
|
+
messageId: nonEmptyString(direct?.messageId) ??
|
|
567
|
+
nonEmptyString(nativeTakeover?.terminal_bridge_message_id)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function claudeFailureText(completion) {
|
|
571
|
+
const details = completion.failure.details === undefined
|
|
572
|
+
? undefined
|
|
573
|
+
: typeof completion.failure.details === "string"
|
|
574
|
+
? completion.failure.details
|
|
575
|
+
: JSON.stringify(completion.failure.details);
|
|
576
|
+
return [
|
|
577
|
+
`Claude Code turn failed (${completion.failure.code}).`,
|
|
578
|
+
completion.failure.message,
|
|
579
|
+
details === undefined ? undefined : `Details: ${details}`
|
|
580
|
+
].filter((part) => Boolean(part)).join(" ");
|
|
581
|
+
}
|
|
582
|
+
function recordValue(value) {
|
|
583
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
584
|
+
? value
|
|
585
|
+
: undefined;
|
|
586
|
+
}
|
|
587
|
+
function positiveInteger(value) {
|
|
588
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
589
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
590
|
+
}
|
|
591
|
+
function errorMessage(error) {
|
|
592
|
+
return error instanceof Error ? error.message : String(error);
|
|
593
|
+
}
|
|
594
|
+
export function detectClaudeApprovalPrompt(screen) {
|
|
595
|
+
const lines = claudeDetectionTail(screen);
|
|
596
|
+
const markerIndex = findLastIndex(lines, (line) => /\bDo you want to proceed\?\s*$/iu.test(line));
|
|
597
|
+
if (markerIndex < 0) {
|
|
598
|
+
return {
|
|
599
|
+
blocked: false,
|
|
600
|
+
approvable: false,
|
|
601
|
+
reason: "no current Claude Code permission dialog was detected in the terminal tail"
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const region = lines.slice(markerIndex);
|
|
605
|
+
const highlighted = findHighlightedChoice(region);
|
|
606
|
+
const newerStateIndex = findLastIndex(region, (line, index) => index > (highlighted?.index ?? -1) && (isClaudeIdlePromptLine(line) || isClaudeWorkingLine(line)));
|
|
607
|
+
const hasPermissionChoices = region.some((line) => isPermissionChoice(line));
|
|
608
|
+
const hasUnexpectedTrailingContent = highlighted
|
|
609
|
+
? region.slice(highlighted.index + 1).some((line) => !isPermissionDialogTrailingLine(line))
|
|
610
|
+
: false;
|
|
611
|
+
if (newerStateIndex >= 0 || hasUnexpectedTrailingContent || !hasPermissionChoices) {
|
|
612
|
+
return {
|
|
613
|
+
blocked: false,
|
|
614
|
+
approvable: false,
|
|
615
|
+
reason: newerStateIndex >= 0 || hasUnexpectedTrailingContent
|
|
616
|
+
? "the Claude Code permission dialog appears stale"
|
|
617
|
+
: "the matching text is not a recognized Claude Code permission dialog"
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (!highlighted) {
|
|
621
|
+
return {
|
|
622
|
+
blocked: true,
|
|
623
|
+
approvable: false,
|
|
624
|
+
reason: "the current Claude Code permission dialog has no recognized highlighted choice",
|
|
625
|
+
promptKind: "claude_permission"
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (!isOneTimeYesChoice(highlighted.label)) {
|
|
629
|
+
return {
|
|
630
|
+
blocked: true,
|
|
631
|
+
approvable: false,
|
|
632
|
+
reason: isPersistentPermissionChoice(highlighted.label)
|
|
633
|
+
? "the highlighted Claude Code choice would persist permission"
|
|
634
|
+
: "the highlighted Claude Code choice is not the one-time Yes option",
|
|
635
|
+
promptKind: "claude_permission"
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
blocked: true,
|
|
640
|
+
approvable: true,
|
|
641
|
+
promptKind: "claude_permission",
|
|
642
|
+
action: {
|
|
643
|
+
mode: "keys",
|
|
644
|
+
keys: ["C-m"],
|
|
645
|
+
label: "Yes"
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
export function detectClaudeActivityState(screen, approval = detectClaudeApprovalPrompt(screen)) {
|
|
650
|
+
if (approval.blocked) {
|
|
651
|
+
return {
|
|
652
|
+
state: "awaiting_approval",
|
|
653
|
+
reason: approval.approvable
|
|
654
|
+
? "current Claude Code one-time permission prompt is highlighted"
|
|
655
|
+
: "current Claude Code permission dialog requires manual review"
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const lines = claudeDetectionTail(screen);
|
|
659
|
+
const idleIndex = findLastIndex(lines, (line) => isClaudeIdlePromptLine(line));
|
|
660
|
+
const workingIndex = findLastIndex(lines, (line) => isClaudeWorkingLine(line));
|
|
661
|
+
if (idleIndex > workingIndex && idleIndex >= Math.max(0, lines.length - 10)) {
|
|
662
|
+
return {
|
|
663
|
+
state: "idle",
|
|
664
|
+
reason: "current Claude Code input prompt is visible near the end of the terminal tail"
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (workingIndex >= 0 && workingIndex >= idleIndex) {
|
|
668
|
+
return {
|
|
669
|
+
state: "working",
|
|
670
|
+
reason: "current Claude Code interruptible working marker is visible in the terminal tail"
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
state: "unknown",
|
|
675
|
+
reason: "no current Claude Code idle, working, or permission marker was detected in the terminal tail"
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
export function claudeScreenExcerpt(screen, maxLength = 4000) {
|
|
679
|
+
const lines = normalizedScreenLines(screen);
|
|
680
|
+
const excerpt = lines.slice(Math.max(0, lines.length - CLAUDE_EXCERPT_LINES)).join("\n");
|
|
681
|
+
return redactString(excerpt).slice(-Math.max(0, maxLength));
|
|
682
|
+
}
|
|
683
|
+
function exactInteractiveAgentRow(pid, rows) {
|
|
684
|
+
const row = rows.find((candidate) => Number.isInteger(candidate.pid) && candidate.pid === pid);
|
|
685
|
+
if (!row) {
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
return row.kind && row.kind !== "interactive" ? null : row;
|
|
689
|
+
}
|
|
690
|
+
function isNonInteractiveFlag(token) {
|
|
691
|
+
const flag = token.split("=", 1)[0];
|
|
692
|
+
return NON_INTERACTIVE_FLAGS.has(flag) || /^-p.+/u.test(token);
|
|
693
|
+
}
|
|
694
|
+
function findClaudeSubcommand(args) {
|
|
695
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
696
|
+
const token = args[index];
|
|
697
|
+
if (token === "--") {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
if (!token.startsWith("-")) {
|
|
701
|
+
return CLAUDE_SUBCOMMANDS.has(token) ? token : undefined;
|
|
702
|
+
}
|
|
703
|
+
const flag = token.split("=", 1)[0];
|
|
704
|
+
if (!token.includes("=") && OPTIONS_WITH_VALUES.has(flag)) {
|
|
705
|
+
index += 1;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (!token.includes("=") && OPTIONAL_VALUE_OPTIONS.has(flag)) {
|
|
709
|
+
const possibleValue = args[index + 1];
|
|
710
|
+
if (possibleValue && !possibleValue.startsWith("-") && !CLAUDE_SUBCOMMANDS.has(possibleValue)) {
|
|
711
|
+
index += 1;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
function optionValue(args, option) {
|
|
718
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
719
|
+
const token = args[index];
|
|
720
|
+
if (token === option) {
|
|
721
|
+
const value = args[index + 1];
|
|
722
|
+
return value && !value.startsWith("-") ? nonEmptyString(value) : undefined;
|
|
723
|
+
}
|
|
724
|
+
if (token.startsWith(`${option}=`)) {
|
|
725
|
+
return nonEmptyString(token.slice(option.length + 1));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
function shortOptionValue(args, option) {
|
|
731
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
732
|
+
const token = args[index];
|
|
733
|
+
if (token === option) {
|
|
734
|
+
const value = args[index + 1];
|
|
735
|
+
return value && !value.startsWith("-") ? nonEmptyString(value) : undefined;
|
|
736
|
+
}
|
|
737
|
+
if (token.startsWith(option) && token.length > option.length) {
|
|
738
|
+
return nonEmptyString(token.slice(option.length));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
function realpathOrUndefined(value) {
|
|
744
|
+
try {
|
|
745
|
+
return fs.realpathSync(value);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function tokenizeTrustedWrapperCommand(command) {
|
|
752
|
+
const tokens = [];
|
|
753
|
+
let token = "";
|
|
754
|
+
let quote;
|
|
755
|
+
let escaped = false;
|
|
756
|
+
let started = false;
|
|
757
|
+
for (const character of command.trim()) {
|
|
758
|
+
if (escaped) {
|
|
759
|
+
token += character;
|
|
760
|
+
escaped = false;
|
|
761
|
+
started = true;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (character === "\\" && quote !== "'") {
|
|
765
|
+
escaped = true;
|
|
766
|
+
started = true;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (quote) {
|
|
770
|
+
if (character === quote) {
|
|
771
|
+
quote = undefined;
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
token += character;
|
|
775
|
+
}
|
|
776
|
+
started = true;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (character === "'" || character === '"') {
|
|
780
|
+
quote = character;
|
|
781
|
+
started = true;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (/\s/u.test(character)) {
|
|
785
|
+
if (started) {
|
|
786
|
+
tokens.push(token);
|
|
787
|
+
token = "";
|
|
788
|
+
started = false;
|
|
789
|
+
}
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
token += character;
|
|
793
|
+
started = true;
|
|
794
|
+
}
|
|
795
|
+
if (escaped || quote) {
|
|
796
|
+
return undefined;
|
|
797
|
+
}
|
|
798
|
+
if (started) {
|
|
799
|
+
tokens.push(token);
|
|
800
|
+
}
|
|
801
|
+
return tokens;
|
|
802
|
+
}
|
|
803
|
+
function tokenizeCommand(command) {
|
|
804
|
+
const tokens = [];
|
|
805
|
+
let token = "";
|
|
806
|
+
let quote;
|
|
807
|
+
let escaped = false;
|
|
808
|
+
let started = false;
|
|
809
|
+
for (const character of command.trim()) {
|
|
810
|
+
if (escaped) {
|
|
811
|
+
token += character;
|
|
812
|
+
escaped = false;
|
|
813
|
+
started = true;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (character === "\\" && quote !== "'") {
|
|
817
|
+
escaped = true;
|
|
818
|
+
started = true;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (quote) {
|
|
822
|
+
if (character === quote) {
|
|
823
|
+
quote = undefined;
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
token += character;
|
|
827
|
+
}
|
|
828
|
+
started = true;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (character === "'" || character === '"') {
|
|
832
|
+
quote = character;
|
|
833
|
+
started = true;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (/\s/u.test(character)) {
|
|
837
|
+
if (started) {
|
|
838
|
+
tokens.push(token);
|
|
839
|
+
token = "";
|
|
840
|
+
started = false;
|
|
841
|
+
}
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
token += character;
|
|
845
|
+
started = true;
|
|
846
|
+
}
|
|
847
|
+
if (escaped) {
|
|
848
|
+
token += "\\";
|
|
849
|
+
}
|
|
850
|
+
if (started) {
|
|
851
|
+
tokens.push(token);
|
|
852
|
+
}
|
|
853
|
+
return tokens;
|
|
854
|
+
}
|
|
855
|
+
function claudeDetectionTail(screen) {
|
|
856
|
+
const lines = normalizedScreenLines(screen);
|
|
857
|
+
return lines.slice(Math.max(0, lines.length - CLAUDE_SCREEN_TAIL_LINES));
|
|
858
|
+
}
|
|
859
|
+
function normalizedScreenLines(screen) {
|
|
860
|
+
return stripAnsi(String(screen || ""))
|
|
861
|
+
.replace(/\r/g, "")
|
|
862
|
+
.trimEnd()
|
|
863
|
+
.split("\n");
|
|
864
|
+
}
|
|
865
|
+
function stripAnsi(value) {
|
|
866
|
+
return value.replace(ANSI_ESCAPE_PATTERN, "");
|
|
867
|
+
}
|
|
868
|
+
function isClaudeIdlePromptLine(line) {
|
|
869
|
+
return /^\s*❯[\s\u00a0]*$/u.test(line);
|
|
870
|
+
}
|
|
871
|
+
function isClaudeWorkingLine(line) {
|
|
872
|
+
// Codex also renders the words "esc to interrupt". Requiring Claude's spinner prevents
|
|
873
|
+
// a recently reused tmux pane from inheriting a stale Codex working state.
|
|
874
|
+
return /^\s*[✻✽✢✣✤✥✦✧]\s+.+(?:…|\.\.\.|\([^)]*(?:tokens?|\d+s|esc to interrupt)[^)]*\))\s*$/iu.test(line);
|
|
875
|
+
}
|
|
876
|
+
function isPermissionChoice(line) {
|
|
877
|
+
const choice = choiceLabel(line);
|
|
878
|
+
return choice !== undefined && (isOneTimeYesChoice(choice) ||
|
|
879
|
+
isPersistentPermissionChoice(choice) ||
|
|
880
|
+
/^No(?:\b|,)/iu.test(choice));
|
|
881
|
+
}
|
|
882
|
+
function isPermissionDialogTrailingLine(line) {
|
|
883
|
+
const trimmed = line.trim();
|
|
884
|
+
return !trimmed ||
|
|
885
|
+
isPermissionChoice(line) ||
|
|
886
|
+
/^(?:Esc|Enter|Tab|Shift\+Tab)\b.*(?:cancel|confirm|amend|select|cycle)/iu.test(trimmed) ||
|
|
887
|
+
/^[─━═╌╍┄┅┈┉\s]+$/u.test(trimmed);
|
|
888
|
+
}
|
|
889
|
+
function findHighlightedChoice(lines) {
|
|
890
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
891
|
+
const match = /^\s*❯\s*(?:\d+\.\s*)?(.+?)\s*$/u.exec(lines[index]);
|
|
892
|
+
const label = nonEmptyString(match?.[1]);
|
|
893
|
+
if (label) {
|
|
894
|
+
return { index, label };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return undefined;
|
|
898
|
+
}
|
|
899
|
+
function choiceLabel(line) {
|
|
900
|
+
const match = /^\s*(?:❯\s*)?(?:\d+\.\s*)?(.+?)\s*$/u.exec(line);
|
|
901
|
+
return nonEmptyString(match?.[1]);
|
|
902
|
+
}
|
|
903
|
+
function isOneTimeYesChoice(label) {
|
|
904
|
+
return /^Yes\s*$/iu.test(label);
|
|
905
|
+
}
|
|
906
|
+
function isPersistentPermissionChoice(label) {
|
|
907
|
+
return /(?:don['’]t ask again|always allow|allow (?:this|the).*(?:session|project|directory)|yes,\s*and)/iu.test(label);
|
|
908
|
+
}
|
|
909
|
+
function findLastIndex(values, predicate) {
|
|
910
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
911
|
+
if (predicate(values[index], index)) {
|
|
912
|
+
return index;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return -1;
|
|
916
|
+
}
|
|
917
|
+
function nonEmptyString(value) {
|
|
918
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
919
|
+
}
|
|
920
|
+
//# sourceMappingURL=claude-terminal-agent-adapter.js.map
|