@scotthuang/agent-knock-knock 0.2.39 → 0.2.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -145,12 +145,11 @@ function runInstallOpenClaw(options) {
145
145
  const skillDest = expandHome(options.skillPath ?? "~/.openclaw/skills/agent-knock-knock/SKILL.md");
146
146
  const steps = [];
147
147
  if (!skillOnly) {
148
- runCheckedCommand(openclawBin, ["plugins", "install", "--link", "--force", root], {
149
- label: "openclaw plugins install"
150
- });
148
+ const pluginInstall = installOpenClawPlugin(openclawBin, root);
151
149
  steps.push({
152
150
  name: "plugin_installed",
153
- path: root
151
+ path: root,
152
+ mode: pluginInstall.mode
154
153
  });
155
154
  runCheckedCommand(openclawBin, ["plugins", "enable", "agent-knock-knock"], {
156
155
  label: "openclaw plugins enable"
@@ -185,6 +184,27 @@ function runInstallOpenClaw(options) {
185
184
  : "Agent Knock Knock is installed. Try: AKK list"
186
185
  });
187
186
  }
187
+ function installOpenClawPlugin(openclawBin, root) {
188
+ const linked = spawnSync(openclawBin, ["plugins", "install", "--link", root], {
189
+ encoding: "utf8",
190
+ maxBuffer: 1024 * 1024 * 10
191
+ });
192
+ if (linked.error) {
193
+ throw new Error(`openclaw plugins install failed to start: ${linked.error.message}`);
194
+ }
195
+ if (linked.status === 0) {
196
+ return { mode: "linked" };
197
+ }
198
+ const failure = cleanProcessText(linked.stderr || linked.stdout)
199
+ ?? `openclaw plugins install exited with status ${linked.status}`;
200
+ if (!/plugin already exists:/i.test(failure)) {
201
+ throw new Error(failure);
202
+ }
203
+ runCheckedCommand(openclawBin, ["plugins", "install", "--force", root], {
204
+ label: "openclaw plugins replace"
205
+ });
206
+ return { mode: "replaced" };
207
+ }
188
208
  function runDoctor(options) {
189
209
  const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
190
210
  const checks = commands.map((commandName) => executableCheck(commandName));
@@ -613,18 +633,21 @@ function detectCodexApprovalPrompt(screen) {
613
633
  if (key !== "y") {
614
634
  return {
615
635
  approvable: false,
616
- reason: `primary approval shortcut is ${key}, not y`
636
+ reason: `primary approval shortcut is ${key}, not y`,
637
+ ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
617
638
  };
618
639
  }
619
640
  return {
620
641
  approvable: true,
621
642
  key,
622
- label: match[1].trim()
643
+ label: match[1].trim(),
644
+ ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
623
645
  };
624
646
  }
625
647
  return {
626
648
  approvable: false,
627
- reason: "no primary approve option with a shortcut was detected"
649
+ reason: "no primary approve option with a shortcut was detected",
650
+ ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
628
651
  };
629
652
  }
630
653
  function isCodexApprovalPromptVisible(screen) {
@@ -639,9 +662,12 @@ function codexApprovalPromptRegion(screen) {
639
662
  ];
640
663
  const lines = screen.split(/\r?\n/);
641
664
  let markerIndex = -1;
665
+ let matchedMarker = "";
642
666
  for (let index = lines.length - 1; index >= 0; index -= 1) {
643
- if (approvalMarkers.some((marker) => lines[index].includes(marker))) {
667
+ const marker = approvalMarkers.find((candidate) => lines[index].includes(candidate));
668
+ if (marker) {
644
669
  markerIndex = index;
670
+ matchedMarker = marker;
645
671
  break;
646
672
  }
647
673
  }
@@ -661,9 +687,40 @@ function codexApprovalPromptRegion(screen) {
661
687
  }
662
688
  return {
663
689
  visible: true,
664
- region: regionLines.join("\n")
690
+ region: regionLines.join("\n"),
691
+ marker: matchedMarker
692
+ };
693
+ }
694
+ function approvalCandidateFromPrompt(marker, region) {
695
+ const promptKind = marker === "Would you like to run the following command?"
696
+ ? "run_command"
697
+ : marker === "Would you like to make the following edits?"
698
+ ? "file_edit"
699
+ : marker === "Would you like to grant these permissions?"
700
+ ? "grant_permissions"
701
+ : "unknown";
702
+ return {
703
+ promptKind,
704
+ command: promptKind === "run_command" ? commandFromApprovalRegion(region) : undefined
665
705
  };
666
706
  }
707
+ function commandFromApprovalRegion(region) {
708
+ const lines = region.split(/\r?\n/);
709
+ const commandStart = lines.findIndex((line) => /^\s*\$\s+/u.test(line));
710
+ if (commandStart < 0) {
711
+ return undefined;
712
+ }
713
+ const parts = [];
714
+ for (let index = commandStart; index < lines.length; index += 1) {
715
+ const line = lines[index];
716
+ if (index > commandStart && (!line.trim() || /^[\s›]*\d+\.\s+/u.test(line) || /Press enter to confirm/u.test(line))) {
717
+ break;
718
+ }
719
+ parts.push(index === commandStart ? line.replace(/^\s*\$\s+/u, "").trim() : line.trim());
720
+ }
721
+ const command = parts.filter(Boolean).join(" ").trim();
722
+ return command ? redactString(command) : undefined;
723
+ }
667
724
  function isPostApprovalActivityLine(line) {
668
725
  const trimmed = line.trim();
669
726
  if (!trimmed) {
@@ -1616,6 +1673,8 @@ async function listStateForTerminal(terminalControl, options) {
1616
1673
  approvable: approval.approvable,
1617
1674
  key: approval.approvable ? approval.key : undefined,
1618
1675
  label: approval.approvable ? approval.label : undefined,
1676
+ prompt_kind: approval.promptKind,
1677
+ command: approval.command,
1619
1678
  reason: approval.approvable ? undefined : approval.reason,
1620
1679
  screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
1621
1680
  },
@@ -1832,6 +1891,8 @@ async function terminalStatusForControl(terminalControl, options) {
1832
1891
  approvable: approval.approvable,
1833
1892
  key: approval.approvable ? approval.key : undefined,
1834
1893
  label: approval.approvable ? approval.label : undefined,
1894
+ prompt_kind: approval.promptKind,
1895
+ command: approval.command,
1835
1896
  reason: approval.approvable ? undefined : approval.reason
1836
1897
  },
1837
1898
  screen: {
@@ -1866,11 +1927,29 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
1866
1927
  target: terminalControl.target,
1867
1928
  key: approval.key,
1868
1929
  label: approval.label,
1930
+ prompt_kind: approval.prompt_kind,
1931
+ command: approval.command,
1869
1932
  excerpt: screen.excerpt
1870
1933
  }))
1871
1934
  .digest("hex")
1872
1935
  .slice(0, 16);
1873
1936
  }
1937
+ function terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval }) {
1938
+ return terminalBridgeApprovalFingerprint({
1939
+ terminalControl,
1940
+ terminalStatus: {
1941
+ approval_state: {
1942
+ key: approval.key,
1943
+ label: approval.label,
1944
+ prompt_kind: approval.promptKind,
1945
+ command: approval.command
1946
+ },
1947
+ screen: {
1948
+ excerpt: screenExcerpt(screen)
1949
+ }
1950
+ }
1951
+ });
1952
+ }
1874
1953
  function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
1875
1954
  const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
1876
1955
  const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
@@ -2372,6 +2451,38 @@ async function runApprove(options) {
2372
2451
  });
2373
2452
  return;
2374
2453
  }
2454
+ const expectedFingerprint = stringValue(options.expectedApprovalFingerprint);
2455
+ const actualFingerprint = terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval });
2456
+ const autoApproved = options.autoApproved === true;
2457
+ const policyRuleId = stringValue(options.policyRuleId);
2458
+ const policyFingerprint = stringValue(options.policyFingerprint);
2459
+ if (expectedFingerprint && actualFingerprint !== expectedFingerprint) {
2460
+ if (autoApproved) {
2461
+ appendEvent(logPath, {
2462
+ ts: new Date().toISOString(),
2463
+ conversation_id: conversation.conversation_id,
2464
+ event: "terminal_auto_approval_decision",
2465
+ action: "rejected",
2466
+ reason: "approval fingerprint changed before execution",
2467
+ terminal_control: terminalControl,
2468
+ expected_fingerprint: expectedFingerprint,
2469
+ actual_fingerprint: actualFingerprint,
2470
+ policy_rule_id: policyRuleId,
2471
+ policy_fingerprint: policyFingerprint
2472
+ });
2473
+ }
2474
+ printJson({
2475
+ conversation,
2476
+ approved: false,
2477
+ blocked: true,
2478
+ reason: "approval fingerprint changed before execution",
2479
+ terminal_control: terminalControl,
2480
+ expected_approval_fingerprint: expectedFingerprint,
2481
+ actual_approval_fingerprint: actualFingerprint,
2482
+ screen_excerpt: screenExcerpt(screen)
2483
+ });
2484
+ return;
2485
+ }
2375
2486
  await provider.sendKeys(terminalControl.target, [approval.key], {
2376
2487
  socketPath: terminalControl.socketPath
2377
2488
  });
@@ -2381,13 +2492,33 @@ async function runApprove(options) {
2381
2492
  event: "terminal_approval_send",
2382
2493
  terminal_control: terminalControl,
2383
2494
  key: approval.key,
2384
- label: approval.label
2495
+ label: approval.label,
2496
+ approval_fingerprint: actualFingerprint,
2497
+ auto_approved: autoApproved,
2498
+ policy_rule_id: policyRuleId,
2499
+ policy_fingerprint: policyFingerprint
2385
2500
  });
2501
+ if (autoApproved) {
2502
+ appendEvent(logPath, {
2503
+ ts: new Date().toISOString(),
2504
+ conversation_id: conversation.conversation_id,
2505
+ event: "terminal_auto_approval_decision",
2506
+ action: "approved",
2507
+ terminal_control: terminalControl,
2508
+ approval_fingerprint: actualFingerprint,
2509
+ policy_rule_id: policyRuleId,
2510
+ policy_fingerprint: policyFingerprint
2511
+ });
2512
+ }
2386
2513
  runtimeLog("info", "terminal_approval_send", {
2387
2514
  conversation_id: conversation.conversation_id,
2388
2515
  terminal_target: terminalControl.target,
2389
2516
  key: approval.key,
2390
- label: approval.label
2517
+ label: approval.label,
2518
+ approval_fingerprint: actualFingerprint,
2519
+ auto_approved: autoApproved,
2520
+ policy_rule_id: policyRuleId,
2521
+ policy_fingerprint: policyFingerprint
2391
2522
  });
2392
2523
  const nativeTakeoverForUpdate = isRecord(conversation.native_session_takeover)
2393
2524
  ? { ...conversation.native_session_takeover }
@@ -2434,6 +2565,9 @@ async function runApprove(options) {
2434
2565
  terminal_control: terminalControl,
2435
2566
  key: approval.key,
2436
2567
  label: approval.label,
2568
+ approval_fingerprint: actualFingerprint,
2569
+ auto_approved: autoApproved,
2570
+ policy_rule_id: policyRuleId,
2437
2571
  monitor_pid: bridgeMonitor?.pid ?? null
2438
2572
  });
2439
2573
  }
@@ -3667,6 +3801,12 @@ async function runTerminalBridgeMonitor(options) {
3667
3801
  terminal_control: terminalControl,
3668
3802
  terminal_status: terminalStatus,
3669
3803
  approval_fingerprint: fingerprint,
3804
+ approval_candidate: terminalBridgeApprovalCandidate({
3805
+ executor,
3806
+ terminalControl,
3807
+ terminalStatus,
3808
+ fingerprint
3809
+ }),
3670
3810
  approve_command: `AKK approve ${notification.conversation.conversation_id}`,
3671
3811
  deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
3672
3812
  approve_tool: "agent_knock_knock_approve",
@@ -3794,6 +3934,20 @@ async function runTerminalBridgeMonitor(options) {
3794
3934
  sleepSync(pollIntervalMs);
3795
3935
  }
3796
3936
  }
3937
+ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalStatus, fingerprint }) {
3938
+ const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
3939
+ if (approval.approvable !== true) {
3940
+ return undefined;
3941
+ }
3942
+ return {
3943
+ agent: executor.kind,
3944
+ kind: stringValue(approval.prompt_kind) ?? "unknown",
3945
+ command: stringValue(approval.command),
3946
+ cwd: terminalControl.currentPath,
3947
+ fingerprint,
3948
+ terminal_target: terminalControl.target
3949
+ };
3950
+ }
3797
3951
  async function terminalBridgeCodexContext({ conversation, nativeTakeover, options }) {
3798
3952
  const provider = createAgentSessionProvider("codex", options);
3799
3953
  const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);