@scotthuang/agent-knock-knock 0.2.46 → 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.
@@ -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