@posthog/wizard 2.79.1 → 2.80.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.
Files changed (192) hide show
  1. package/dist/{PickerMenu-dG5DQCZA.js → PickerMenu-m4CCPX4f.js} +10 -10
  2. package/dist/PickerMenu-m4CCPX4f.js.map +1 -0
  3. package/dist/{agent-BWFhuLgo.js → agent-BkBbVuNq.js} +48 -37
  4. package/dist/agent-BkBbVuNq.js.map +1 -0
  5. package/dist/agent-QGK2YzlX.js +6 -0
  6. package/dist/{agent-interface-FkbaBJwz.js → agent-interface-CzyCmp7q.js} +68 -31
  7. package/dist/agent-interface-CzyCmp7q.js.map +1 -0
  8. package/dist/{agent-prompt-loader-D0weQ-7k.js → agent-prompt-loader-4wMxlYSX.js} +5 -5
  9. package/dist/{agent-prompt-loader-D0weQ-7k.js.map → agent-prompt-loader-4wMxlYSX.js.map} +1 -1
  10. package/dist/{analytics-DXK1gc12.js → analytics-B6q5PN7a.js} +5 -5
  11. package/dist/analytics-B6q5PN7a.js.map +1 -0
  12. package/dist/analytics-BXtfK9o7.js +2 -0
  13. package/dist/{api-CrbCIQOz.js → api-C3Dc4LFl.js} +3 -3
  14. package/dist/{api-CrbCIQOz.js.map → api-C3Dc4LFl.js.map} +1 -1
  15. package/dist/api-yNjrfZs2.js +2 -0
  16. package/dist/bin.js +1 -1
  17. package/dist/{bounded-fs-vAq-T-K7.js → bounded-fs-CC_NDtoP.js} +3 -3
  18. package/dist/{bounded-fs-vAq-T-K7.js.map → bounded-fs-CC_NDtoP.js.map} +1 -1
  19. package/dist/check-screens.tsx +5 -5
  20. package/dist/{ci-install-COcV8SY2.js → ci-install-DfcAMhlu.js} +7 -7
  21. package/dist/ci-install-DfcAMhlu.js.map +1 -0
  22. package/dist/{constants-DHCBSfnx.js → constants-DO4X70po.js} +3 -3
  23. package/dist/{constants-DHCBSfnx.js.map → constants-DO4X70po.js.map} +1 -1
  24. package/dist/{debug-BuYHGr6_.js → debug-BueK5mRR.js} +1 -1
  25. package/dist/{debug-kAHwWGg7.js → debug-DyARf5TF.js} +3 -3
  26. package/dist/debug-DyARf5TF.js.map +1 -0
  27. package/dist/{defaults-a9TelDhJ.js → defaults-DpfMnH2q.js} +2 -2
  28. package/dist/defaults-DpfMnH2q.js.map +1 -0
  29. package/dist/{endpoints-BVMYm9F4.js → endpoints-D-4KxZiB.js} +3 -3
  30. package/dist/{endpoints-BVMYm9F4.js.map → endpoints-D-4KxZiB.js.map} +1 -1
  31. package/dist/env-api-key-CHGLe0iz.js +2 -0
  32. package/dist/{env-api-key-BP-NFFXt.js → env-api-key-COf0U-Au.js} +2 -2
  33. package/dist/{env-api-key-BP-NFFXt.js.map → env-api-key-COf0U-Au.js.map} +1 -1
  34. package/dist/{environment-BATlQxcQ.js → environment-DrLaSsA8.js} +4 -4
  35. package/dist/{environment-BATlQxcQ.js.map → environment-DrLaSsA8.js.map} +1 -1
  36. package/dist/environment-hBj7ngmO.js +2 -0
  37. package/dist/{file-DcRKycV8.js → file-CVA9ocIC.js} +3 -3
  38. package/dist/file-CVA9ocIC.js.map +1 -0
  39. package/dist/file-CmoKK_Pp.js +2 -0
  40. package/dist/{file-watcher-Brq2nnHV.js → file-watcher-C3Su8vXm.js} +3 -3
  41. package/dist/file-watcher-C3Su8vXm.js.map +1 -0
  42. package/dist/{gateway-C9-GlLO9.js → gateway-BmjYUSua.js} +6 -6
  43. package/dist/{gateway-C9-GlLO9.js.map → gateway-BmjYUSua.js.map} +1 -1
  44. package/dist/{headless-ui-CqI2gXlD.js → headless-ui-CUbv12Yx.js} +3 -3
  45. package/dist/headless-ui-CUbv12Yx.js.map +1 -0
  46. package/dist/host-resolution-DVSb7kXb.js +114 -0
  47. package/dist/host-resolution-DVSb7kXb.js.map +1 -0
  48. package/dist/{add-mcp-server-to-clients-DIaTRWMY.js → install-Bk5TNk27.js} +15 -14
  49. package/dist/install-Bk5TNk27.js.map +1 -0
  50. package/dist/{add-mcp-server-to-clients-artmN7mz.js → install-F4wm3RSl.js} +1 -1
  51. package/dist/interactive-C5LNvDj6.js +11 -0
  52. package/dist/interactive-C5LNvDj6.js.map +1 -0
  53. package/dist/{telemetry-B7zJdRmA.js → links-9LWZPSCq.js} +4 -13
  54. package/dist/links-9LWZPSCq.js.map +1 -0
  55. package/dist/{logging-ui-Dza4D0MK.js → logging-ui-p6XKVmTx.js} +6 -6
  56. package/dist/logging-ui-p6XKVmTx.js.map +1 -0
  57. package/dist/{main-CgKxbwMP.js → main-DKPjFHEV.js} +149 -141
  58. package/dist/main-DKPjFHEV.js.map +1 -0
  59. package/dist/{mcp-DnAyW0-w.js → mcp-Co8VRF62.js} +4 -4
  60. package/dist/{mcp-DnAyW0-w.js.map → mcp-Co8VRF62.js.map} +1 -1
  61. package/dist/mcp-install-smoke-test.ts +1 -1
  62. package/dist/{mcp-prompt-streaming-DDWQ-FCA.js → mcp-prompt-streaming-ljCVoQ9I.js} +6 -6
  63. package/dist/{mcp-prompt-streaming-DDWQ-FCA.js.map → mcp-prompt-streaming-ljCVoQ9I.js.map} +1 -1
  64. package/dist/{mint-failure-u8xQKNOn.js → mint-failure-C0HyvEwO.js} +103 -92
  65. package/dist/mint-failure-C0HyvEwO.js.map +1 -0
  66. package/dist/{models-WcwUnSKb.js → models-Dba8ITeo.js} +2 -2
  67. package/dist/{models-WcwUnSKb.js.map → models-Dba8ITeo.js.map} +1 -1
  68. package/dist/{non-interactive-DyVC1AGc.js → non-interactive-CLMJrj_7.js} +3 -3
  69. package/dist/non-interactive-CLMJrj_7.js.map +1 -0
  70. package/dist/{oauth-session-DYM2Hrl2.js → oauth-session-D-RfcVmr.js} +2 -2
  71. package/dist/{oauth-session-DYM2Hrl2.js.map → oauth-session-D-RfcVmr.js.map} +1 -1
  72. package/dist/{orchestrator-tools-DbgNBFGA.js → orchestrator-tools-CVJ1F9Ub.js} +3 -3
  73. package/dist/{orchestrator-tools-DbgNBFGA.js.map → orchestrator-tools-CVJ1F9Ub.js.map} +1 -1
  74. package/dist/package-json--ppioh2F.js +2 -0
  75. package/dist/{package-json-Atg34fa4.js → package-json-Cowya9-3.js} +1 -1
  76. package/dist/{package-json-Atg34fa4.js.map → package-json-Cowya9-3.js.map} +1 -1
  77. package/dist/{pi-8KsYsPmM.js → pi-B5YTBRDv.js} +18 -17
  78. package/dist/pi-B5YTBRDv.js.map +1 -0
  79. package/dist/{playground-BlYRBMj7.js → playground-BLsCfHmQ.js} +33 -33
  80. package/dist/playground-BLsCfHmQ.js.map +1 -0
  81. package/dist/posthog-B2E0hpZt.js +2 -0
  82. package/dist/{posthog-Bt0Aplb8.js → posthog-B42rt_1S.js} +3 -3
  83. package/dist/posthog-B42rt_1S.js.map +1 -0
  84. package/dist/project-data--L-WZyvj.js +2 -0
  85. package/dist/{setup-utils-B26Lf2yF.js → project-data-BO9IXn9y.js} +566 -947
  86. package/dist/project-data-BO9IXn9y.js.map +1 -0
  87. package/dist/provisioning-C01REIU2.js +2 -0
  88. package/dist/{provisioning-Bo2NRxFz.js → provisioning-CLhuKqXH.js} +6 -67
  89. package/dist/provisioning-CLhuKqXH.js.map +1 -0
  90. package/dist/{queue-tools-C8mXOh7e.js → queue-tools-knnPQP0e.js} +4 -4
  91. package/dist/{queue-tools-C8mXOh7e.js.map → queue-tools-knnPQP0e.js.map} +1 -1
  92. package/dist/{registry-CD5RQt38.js → registry-B3rC5U4b.js} +6 -6
  93. package/dist/{registry-CD5RQt38.js.map → registry-B3rC5U4b.js.map} +1 -1
  94. package/dist/registry-BamsM7J9.js +2 -0
  95. package/dist/run-agent-legacy-CwUmPfQF.js +2 -0
  96. package/dist/{run-agent-legacy-DOlM6DQu.js → run-agent-legacy-DWBajsMh.js} +38 -111
  97. package/dist/run-agent-legacy-DWBajsMh.js.map +1 -0
  98. package/dist/{runners-ov3EXF1n.js → runners-D1PqqpjA.js} +38 -38
  99. package/dist/runners-D1PqqpjA.js.map +1 -0
  100. package/dist/{security-C71pkwqH.js → security-BvdSFDuM.js} +5 -5
  101. package/dist/{security-C71pkwqH.js.map → security-BvdSFDuM.js.map} +1 -1
  102. package/dist/setup-utils-ChXXTi1m.js +301 -0
  103. package/dist/setup-utils-ChXXTi1m.js.map +1 -0
  104. package/dist/{start-tui-Bo2L0bar.js → start-tui-DicLfgsj.js} +92 -90
  105. package/dist/start-tui-DicLfgsj.js.map +1 -0
  106. package/dist/{steps-BPcg3_3Q.js → steps-Hc28zVc_.js} +10 -10
  107. package/dist/steps-Hc28zVc_.js.map +1 -0
  108. package/dist/store-COCO6U0P.js +5 -0
  109. package/dist/{store-BTzcvRGT.js → store-D3509G87.js} +66 -28
  110. package/dist/store-D3509G87.js.map +1 -0
  111. package/dist/{subagent-D1TrTchE.js → subagent-D_Sqak94.js} +3 -3
  112. package/dist/{subagent-D1TrTchE.js.map → subagent-D_Sqak94.js.map} +1 -1
  113. package/dist/{task-DqRlQBfs.js → task-DiTI6Sak.js} +16 -16
  114. package/dist/task-DiTI6Sak.js.map +1 -0
  115. package/dist/{wizard-ui-DVqrYaOM.js → task-status-CmrrR7zo.js} +2 -2
  116. package/dist/task-status-CmrrR7zo.js.map +1 -0
  117. package/dist/{task-stream-Bw6JaLCX.js → task-stream-BckhG_cB.js} +10 -10
  118. package/dist/task-stream-BckhG_cB.js.map +1 -0
  119. package/dist/{tasks-ChWufsdO.js → tasks-ChZ_F1YP.js} +1 -1
  120. package/dist/{tasks-ChWufsdO.js.map → tasks-ChZ_F1YP.js.map} +1 -1
  121. package/dist/telemetry-BNKsDxNS.js +13 -0
  122. package/dist/telemetry-BNKsDxNS.js.map +1 -0
  123. package/dist/{terminal-zgkEfoCL.js → terminal-PK9bCLRi.js} +123 -97
  124. package/dist/terminal-PK9bCLRi.js.map +1 -0
  125. package/dist/{tools-CkJ2y9Wt.js → tools-C8LQ8QFz.js} +8 -8
  126. package/dist/{tools-CkJ2y9Wt.js.map → tools-C8LQ8QFz.js.map} +1 -1
  127. package/dist/{ui-Q6-RvERF.js → ui-tPNpM6vj.js} +3 -3
  128. package/dist/ui-tPNpM6vj.js.map +1 -0
  129. package/dist/urls-BI4kJ4qQ.js +67 -0
  130. package/dist/urls-BI4kJ4qQ.js.map +1 -0
  131. package/dist/{version-CHhJjz_H.js → version-5_ydHkM6.js} +2 -2
  132. package/dist/{version-CHhJjz_H.js.map → version-5_ydHkM6.js.map} +1 -1
  133. package/dist/{wizard-abort-Br1qMDNa.js → wizard-abort-BVjqLyeA.js} +1 -1
  134. package/dist/{wizard-abort-BPdv07np.js → wizard-abort-DBKtfnBR.js} +18 -16
  135. package/dist/wizard-abort-DBKtfnBR.js.map +1 -0
  136. package/dist/wizard-session-B7BHnay0.js +2 -0
  137. package/dist/{wizard-session-2w4Ru-Wz.js → wizard-session-BYw6LzpS.js} +30 -28
  138. package/dist/wizard-session-BYw6LzpS.js.map +1 -0
  139. package/dist/{yara-hooks-BnxejtXK.js → yara-hooks-C3L6lBij.js} +5 -5
  140. package/dist/{yara-hooks-BnxejtXK.js.map → yara-hooks-C3L6lBij.js.map} +1 -1
  141. package/dist/yara-hooks-CF_IsoaA.js +2 -0
  142. package/package.json +2 -3
  143. package/dist/PickerMenu-dG5DQCZA.js.map +0 -1
  144. package/dist/add-mcp-server-to-clients-DIaTRWMY.js.map +0 -1
  145. package/dist/agent-BEjARD2a.js +0 -6
  146. package/dist/agent-BWFhuLgo.js.map +0 -1
  147. package/dist/agent-interface-FkbaBJwz.js.map +0 -1
  148. package/dist/analytics-DFtbRjYh.js +0 -2
  149. package/dist/analytics-DXK1gc12.js.map +0 -1
  150. package/dist/api-Cm9UtKZh.js +0 -2
  151. package/dist/ci-install-COcV8SY2.js.map +0 -1
  152. package/dist/debug-kAHwWGg7.js.map +0 -1
  153. package/dist/defaults-a9TelDhJ.js.map +0 -1
  154. package/dist/env-api-key-CaWEDj_9.js +0 -2
  155. package/dist/environment-CwD8OPYj.js +0 -2
  156. package/dist/file-C5JMsyR1.js +0 -2
  157. package/dist/file-DcRKycV8.js.map +0 -1
  158. package/dist/file-watcher-Brq2nnHV.js.map +0 -1
  159. package/dist/headless-ui-CqI2gXlD.js.map +0 -1
  160. package/dist/interactive-B_Ti_CsS.js +0 -11
  161. package/dist/interactive-B_Ti_CsS.js.map +0 -1
  162. package/dist/logging-ui-Dza4D0MK.js.map +0 -1
  163. package/dist/main-CgKxbwMP.js.map +0 -1
  164. package/dist/mint-failure-u8xQKNOn.js.map +0 -1
  165. package/dist/non-interactive-DyVC1AGc.js.map +0 -1
  166. package/dist/package-json-D8Ga03tK.js +0 -2
  167. package/dist/pi-8KsYsPmM.js.map +0 -1
  168. package/dist/playground-BlYRBMj7.js.map +0 -1
  169. package/dist/posthog-Bt0Aplb8.js.map +0 -1
  170. package/dist/posthog-CUgCEhH0.js +0 -2
  171. package/dist/provisioning-Bo2NRxFz.js.map +0 -1
  172. package/dist/provisioning-OGFGlzf7.js +0 -2
  173. package/dist/registry-C06e_x-p.js +0 -2
  174. package/dist/run-agent-legacy-ChacZImU.js +0 -2
  175. package/dist/run-agent-legacy-DOlM6DQu.js.map +0 -1
  176. package/dist/runners-ov3EXF1n.js.map +0 -1
  177. package/dist/setup-utils-B26Lf2yF.js.map +0 -1
  178. package/dist/setup-utils-CVS86NYR.js +0 -2
  179. package/dist/start-tui-Bo2L0bar.js.map +0 -1
  180. package/dist/steps-BPcg3_3Q.js.map +0 -1
  181. package/dist/store-BTzcvRGT.js.map +0 -1
  182. package/dist/store-BpqySPXm.js +0 -5
  183. package/dist/task-DqRlQBfs.js.map +0 -1
  184. package/dist/task-stream-Bw6JaLCX.js.map +0 -1
  185. package/dist/telemetry-B7zJdRmA.js.map +0 -1
  186. package/dist/terminal-zgkEfoCL.js.map +0 -1
  187. package/dist/ui-Q6-RvERF.js.map +0 -1
  188. package/dist/wizard-abort-BPdv07np.js.map +0 -1
  189. package/dist/wizard-session-2w4Ru-Wz.js.map +0 -1
  190. package/dist/wizard-session-DGaUHOF3.js +0 -2
  191. package/dist/wizard-ui-DVqrYaOM.js.map +0 -1
  192. package/dist/yara-hooks-DSuExwb4.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"yara-hooks-BnxejtXK.js","names":[],"sources":["../src/shared/skill-install.ts","../src/agent/yara-policy.ts","../src/shared/audit-ledger.ts","../src/agent/yara-hooks.ts"],"sourcesContent":["/**\n * Check if command is a PostHog skill installation from MCP.\n * We control the MCP server, so we only need to verify:\n * 1. It installs to .claude/skills/\n * 2. It downloads from a context-mill release origin (GitHub Releases or the\n * AWS mirror) or localhost (dev)\n *\n * Extracted to its own module to avoid a circular dependency\n * between agent-interface.ts and yara-hooks.ts.\n */\nexport function isSkillInstallCommand(command: string): boolean {\n if (!command.startsWith('mkdir -p .claude/skills/')) return false;\n\n const urlMatch = command.match(/curl -sL ['\"]([^'\"]+)['\"]/);\n if (!urlMatch) return false;\n\n // Literal prefixes rather than the constants in `@lib/constants`: an\n // allow-list is easier to audit when it reads as the URLs themselves.\n const url = urlMatch[1];\n return (\n url.startsWith('https://github.com/PostHog/context-mill/releases/') ||\n url.startsWith('https://context-mill.posthog.com/') ||\n /^http:\\/\\/localhost:\\d+\\//.test(url)\n );\n}\n","/**\n * Shared YARA scan policy: the vocabulary every scan surface speaks, and the\n * one decision they all make — does a set of matches terminate the session, or\n * only warn. Both fences (the anthropic PostToolUse hooks in `yara-hooks.ts`\n * and the pi tool_result extension in `harness/pi/security.ts`) resolve a\n * verdict here rather than each re-deriving it from severity and action.\n */\n\nimport type { ScanMatch } from '@posthog/warlock';\n\n/** Which content surface a warlock rule applies to (from rule `scan_context`). */\nexport type ScanContext = 'command' | 'input' | 'output';\n\n/** What a fence did about a match, as recorded in the scan report. */\nexport type ScanAction = 'blocked' | 'reverted' | 'warned' | 'aborted';\n\n/** Severity order, highest first, for picking the match worth reporting. */\nconst SEVERITY_RANK: Record<string, number> = {\n critical: 4,\n high: 3,\n medium: 2,\n low: 1,\n};\n\n/** Severities that end the session. Critical only — nothing below it is worth killing a run over. */\nconst TERMINAL_SEVERITIES = new Set(['critical']);\n\n/**\n * The match that ends the session rather than just warning. Severity alone\n * decides: a rule's `action: 'block'` is a recommendation about the *operation*\n * (PreToolUse still refuses the command), not a licence to end the run. Medium\n * and high `block` rules fire on first-party content often enough that\n * terminating on them costs more than it prevents.\n */\nexport function isTerminalMatch(match: ScanMatch): boolean {\n return TERMINAL_SEVERITIES.has(match.metadata.severity ?? '');\n}\n\n/** Return the highest-severity match from a list of matches. */\nexport function highestSeverityMatch(matches: ScanMatch[]): ScanMatch {\n return matches.reduce((worst, m) =>\n (SEVERITY_RANK[m.metadata.severity ?? ''] ?? 0) >\n (SEVERITY_RANK[worst.metadata.severity ?? ''] ?? 0)\n ? m\n : worst,\n );\n}\n\n/**\n * The match that refuses a publish, or null to allow. Critical only — every\n * 'output' rule asks for `remediate`, and the handoff has no second channel.\n */\nexport function publishBlockingMatch(matches: ScanMatch[]): ScanMatch | null {\n if (matches.length === 0) return null;\n const match = highestSeverityMatch(matches);\n return isTerminalMatch(match) ? match : null;\n}\n\nexport interface ScanVerdict {\n /** The match worth reporting: the highest-severity one. */\n match: ScanMatch;\n /** Session ends now; anything else is advisory. */\n terminal: boolean;\n /** How the scan report records it. */\n action: ScanAction;\n}\n\n/** Resolve what a fence should do about a set of matches. Null when clean. */\nexport function scanVerdict(matches: ScanMatch[]): ScanVerdict | null {\n if (matches.length === 0) return null;\n const match = highestSeverityMatch(matches);\n const terminal = isTerminalMatch(match);\n return { match, terminal, action: terminal ? 'aborted' : 'warned' };\n}\n","/**\n * The audit ledger's contract: the file the audit tools write into the user's\n * project, the check shape inside it, and the read-side coercion. Shared by\n * the agent's ledger tools (both harnesses), the scanner's documentation\n * allowlist and the audit program that watches the file.\n */\n\nexport type AuditStatus =\n | 'pending'\n | 'pass'\n | 'error'\n | 'warning'\n | 'suggestion';\n\nexport interface AuditCheck {\n id: string;\n area: string;\n label: string;\n status: AuditStatus;\n file?: string;\n details?: string;\n}\n\nexport const AUDIT_CHECKS_FILE = '.posthog-audit-checks.json';\nexport const AUDIT_REPORT_FILE = 'posthog-audit-report.md';\n\n/**\n * Read the audit checks ledger off disk. Validation lives at write time —\n * every writer (`audit_seed_checks` / `audit_add_checks` / `audit_resolve_checks`\n * MCP tools, `seedAuditLedger`) zod-parses entries before the atomic write,\n * so by the time the file watcher fires we trust the shape and only guard\n * against the file not being a JSON array (corrupted / hand-edited / not yet\n * seeded).\n */\nexport function coerceAuditChecks(parsed: unknown): AuditCheck[] {\n return Array.isArray(parsed) ? (parsed as AuditCheck[]) : [];\n}\n","/**\n * YARA hook wiring for the Claude Agent SDK.\n *\n * Creates PreToolUse and PostToolUse hook callback arrays that integrate the\n * real @posthog/warlock security scanner (YARA-X via WASM) into the wizard's\n * agent loop. These hooks are registered in the SDK's query() options alongside\n * the existing Stop hook.\n *\n * PreToolUse hooks block dangerous commands before execution. PostToolUse hooks\n * detect violations in written code and prompt injection in read content, and\n * scan context-mill skill downloads.\n *\n * Warlock owns the rules; this file owns the *policy* — how a match maps to a\n * block / revert / terminate response, plus the optional LLM triage pass that\n * filters false positives before we act.\n *\n * Naming: \"yara\" is the technique — scanning content against YARA rules — so the\n * wizard-side wiring keeps that name. \"warlock\" is the engine package that runs\n * the rules. Both names showing up here is intentional.\n *\n * This is Layer 2 (L2) in the wizard's defense-in-depth model, complementing the\n * prompt-based commandments (L0) and the canUseTool() allowlist (L1).\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { createHash } from 'crypto';\nimport fg from 'fast-glob';\nimport type {\n ScanMatch,\n TriageMatch,\n LLMProvider,\n Category,\n} from '@posthog/warlock';\nimport { logToFile } from '@utils/debug';\nimport { readFileHead } from '@utils/bounded-fs';\nimport { analytics } from '@utils/analytics';\nimport { isSkillInstallCommand } from '@shared/skill-install';\nimport {\n highestSeverityMatch,\n publishBlockingMatch,\n scanVerdict,\n} from './yara-policy';\nimport type { ScanAction, ScanContext } from './yara-policy';\nimport { WIZARD_YARA_REPORT_FILE } from '@utils/paths';\n// TODO(wizard#594): invert this dependency. The document names are shared\n// constants now, so nothing here imports a program, but this file still knows\n// which programs write PII-shaped reports: every new one has to be added.\n// Proper fix is inverted — programs declare their own doc paths and the hooks\n// read a generic registry. Land that before adding a fourth entry.\nimport {\n EVENTS_AUDIT_REPORT_FILE,\n EVENT_INVENTORY_FILE,\n EVENT_INVENTORY_PART_PATTERN,\n EVENT_PLAN_FILE,\n} from '@shared/constants';\nimport { AUDIT_REPORT_FILE } from '@shared/audit-ledger';\n\n// ─── Warlock module accessor ─────────────────────────────────────\n// Warlock is ESM-only and lazily inits its WASM engine + compiles rules on the\n// first scan() call. We import it once and cache the module promise so the\n// warmed engine (a warlock-internal singleton) is shared across every hook.\n// Mirrors the lazy `await import(...)` pattern used for the agent SDK.\n\ntype WarlockModule = typeof import('@posthog/warlock');\n\nlet warlockModulePromise: Promise<WarlockModule> | null = null;\n\nfunction getWarlock(): Promise<WarlockModule> {\n if (!warlockModulePromise) {\n // Clear the cache on rejection so a transient failure (flaky disk, ESM\n // resolver hiccup) doesn't poison the cache for the rest of the process\n // lifetime. Without this, one rejected import permanently locks every\n // hook into fail-closed-terminate until restart.\n warlockModulePromise = import('@posthog/warlock').catch((err) => {\n warlockModulePromise = null;\n throw err;\n });\n }\n return warlockModulePromise;\n}\n\n/**\n * Pay the WASM-init + rule-compile cost up front, off the hook path, so the\n * first real tool-call scan doesn't eat cold-start under a hook timeout.\n * Best-effort: a failure here is non-fatal — hooks still fail closed per scan.\n */\nexport async function prewarmYaraScanner(): Promise<void> {\n try {\n const warlock = await getWarlock();\n await warlock.scan('');\n logToFile('[YARA] warlock pre-warmed');\n } catch (err) {\n logToFile('[YARA] warlock pre-warm failed:', err);\n }\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n// Loose hook types to avoid tight coupling to the SDK version.\n// The SDK hook types are: HookCallbackMatcher[], where each matcher\n// has { matcher?: string, hooks: HookCallback[], timeout?: number }\n\ntype HookInput = Record<string, unknown>;\ntype HookOutput = Record<string, unknown>;\ntype HookCallback = (\n input: HookInput,\n toolUseID: string | undefined,\n options: { signal: AbortSignal },\n) => Promise<HookOutput>;\n\nexport interface HookCallbackMatcher {\n matcher?: string;\n hooks: HookCallback[];\n timeout?: number;\n}\n\nexport type { ScanAction, ScanContext } from './yara-policy';\n\n// ─── Scan Report Accumulator ─────────────────────────────────────\n\ninterface ScanReportEntry {\n rule: string;\n severity: string;\n category: string;\n action: ScanAction;\n phase: string;\n tool: string;\n /** Rule description — the human-readable \"why\". Rule-static, safe to report. */\n description: string;\n /** Triage outcome, when triage ran. */\n triageVerdict?: string;\n /**\n * Free-text triage rationale. May quote scanned content, so it is written to\n * the local report file only — NEVER sent to PostHog (see captureScanReport).\n */\n triageReason?: string;\n}\n\nlet scanCount = 0;\nconst scanViolations: ScanReportEntry[] = [];\n\nfunction recordScan(): void {\n scanCount++;\n}\n\n// recordMatch is the single entry point for a violation (log + telemetry +\n// report). scanViolations is appended only there.\n\n/**\n * Log a match to the run log + PostHog, and record it in the run's scan report.\n * Single entry point so every violation is reported consistently.\n */\nfunction recordMatch(\n phase: string,\n tool: string,\n match: ScanMatch,\n action: ScanAction,\n): void {\n const triage = (match as TriageMatch).triage;\n logYaraMatch(phase, tool, match, action);\n scanViolations.push({\n rule: match.rule,\n severity: match.metadata.severity ?? 'unknown',\n category: match.metadata.category ?? 'unknown',\n action,\n phase,\n tool,\n description: match.metadata.description ?? '',\n triageVerdict: triage?.verdict,\n triageReason: triage?.reason,\n });\n}\n\n/** Reset counters (for testing) */\nexport function resetScanReport(): void {\n scanCount = 0;\n scanViolations.length = 0;\n}\n\n/**\n * Scan bookkeeping for harnesses that run a scanner outside these hooks (pi):\n * counts the scan and routes violations through `recordMatch`, so they reach\n * the run log, per-match telemetry, and the end-of-run scan report.\n */\nexport function recordExternalScan(\n phase: string,\n tool: string,\n violations: Array<{\n rule: string;\n severity: string;\n category: string;\n description: string;\n }>,\n action: ScanAction,\n): void {\n recordScan();\n for (const v of violations) {\n recordMatch(\n phase,\n tool,\n {\n rule: v.rule,\n metadata: {\n severity: v.severity as ScanMatch['metadata']['severity'],\n category: v.category as ScanMatch['metadata']['category'],\n description: v.description,\n },\n matchedStrings: [],\n },\n action,\n );\n }\n}\n\n/** Format the scan report summary. Returns null if no scans occurred */\nexport function formatScanReport(): string | null {\n if (scanCount === 0) return null;\n\n const lines: string[] = ['', '— YARA Scanner Summary —'];\n const violationCount = scanViolations.length;\n const cleanCount = scanCount - violationCount;\n\n lines.push(\n `✓ ${scanCount} tool calls scanned, ${violationCount} violation${\n violationCount !== 1 ? 's' : ''\n } detected`,\n );\n\n if (violationCount > 0) {\n lines.push('');\n for (const v of scanViolations) {\n const tag = v.action.toUpperCase();\n lines.push(\n ` [${tag}] ${v.rule} (${v.severity.toUpperCase()}) — ${v.phase}:${\n v.tool\n }`,\n );\n }\n }\n\n if (cleanCount > 0) {\n lines.push('');\n lines.push(\n `No violations: ✓ ${cleanCount} clean scan${cleanCount !== 1 ? 's' : ''}`,\n );\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\n/** Write the scan report to a JSON file. Returns the file path, or null if no scans occurred. */\nexport function writeScanReport(): string | null {\n if (scanCount === 0) return null;\n\n const report = {\n summary: {\n totalScans: scanCount,\n violations: scanViolations.length,\n clean: scanCount - scanViolations.length,\n },\n violations: scanViolations,\n };\n\n try {\n fs.writeFileSync(WIZARD_YARA_REPORT_FILE, JSON.stringify(report, null, 2));\n } catch (err) {\n logToFile('[YARA] Failed to write scan report:', err);\n return null;\n }\n return WIZARD_YARA_REPORT_FILE;\n}\n\n// ─── User-facing abort copy ──────────────────────────────────────\n//\n// The abort screen is the one security surface a user actually sees, so it must\n// say what was caught and that we protected them, in plain language, instead of\n// a bare \"Security violation detected.\" Warlock owns the rules and categories;\n// this map is the wizard's *presentation* of a category it already detected. We\n// deliberately do not surface warlock's own rule `description` here: it is\n// written for maintainers (it ships to telemetry) and reads as technical. The\n// `Category` type is a stable, append-only warlock API, so a new category just\n// falls through to the generic line below rather than breaking the build.\n\nconst CATEGORY_DESCRIPTIONS: Record<Category, string> = {\n prompt_injection:\n 'hit hidden instructions in a file it read, a possible attempt to hijack the setup',\n exfiltration: 'tried to send data to an outside location',\n destructive_operations: 'tried to run a destructive command',\n supply_chain: 'tried to pull in untrusted code',\n posthog_pii: 'tried to write personal data (PII) into your project',\n posthog_hardcoded_key: 'tried to hardcode a PostHog key into your project',\n hardcoded_secret: 'tried to hardcode a secret into your project',\n};\n\n/** Most recent violation that terminated the run, or null. */\nfunction lastTerminatingViolation(): ScanReportEntry | null {\n for (let i = scanViolations.length - 1; i >= 0; i--) {\n if (scanViolations[i].action === 'aborted') return scanViolations[i];\n }\n return null;\n}\n\n/**\n * Build the user-facing copy for the security abort screen: what the scanner\n * caught and what we did about it. This screen renders only because the run was\n * stopped, so the \"what we did\" half is constant. Falls back to a generic line\n * when no specific violation was recorded (e.g. the scanner itself errored and\n * failed closed before a match was logged).\n */\nexport function formatYaraAbortMessage(): string {\n const v = lastTerminatingViolation();\n const what =\n (v && CATEGORY_DESCRIPTIONS[v.category as Category]) ??\n 'did something our security scanner flagged';\n\n return (\n 'Security check stopped the setup.\\n\\n' +\n `The agent ${what}. We stopped the run to keep your project safe.\\n` +\n 'No unsafe changes were left in your project.\\n\\n' +\n 'If this looks wrong, let us know: wizard@posthog.com'\n );\n}\n\n// ─── Hook Timeouts (ms) ─────────────────────────────────────────\n// Generous because triage adds an LLM round-trip, and because a *fired* hook\n// timeout means the protective action is dropped (fail-open). The triage HTTP\n// call has its own shorter internal timeout (see agent-interface), so a hung\n// triage fails inside our catch (fail-closed) before these fire.\n\n/** Timeout for scan hooks (PreToolUse, PostToolUse Write/Edit/Read/Grep) */\nconst HOOK_TIMEOUT_MS = 30_000;\n/** Timeout for the skill install hook (filesystem I/O + multiple scans) */\nconst SKILL_SCAN_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Chunk size for scanning (100 KB). Oversized content is scanned in\n * overlapping chunks so coverage is complete — nothing is silently truncated.\n * The size also bounds what a single triage call pastes into the LLM prompt\n * (~25K tokens), so triage always sees the chunk its matches came from.\n */\nconst SCAN_CHUNK_SIZE = 100_000;\n\n// A skill file is read at most this far; the rest is head-scanned and logged.\nconst SKILL_FILE_SCAN_BYTES = 10 * 1024 * 1024;\n/**\n * Overlap between adjacent chunks so a pattern straddling a chunk boundary\n * still lands whole inside at least one chunk. YARA rule strings are at most\n * a few hundred bytes; 4 KB is generous.\n */\nconst SCAN_CHUNK_OVERLAP = 4_096;\n\n// ─── Logging ─────────────────────────────────────────────────────\n\nfunction logYaraMatch(\n phase: string,\n tool: string,\n match: ScanMatch,\n action: ScanAction,\n): void {\n const severity = match.metadata.severity ?? 'unknown';\n const category = match.metadata.category ?? 'unknown';\n const ruleAction = match.metadata.action ?? 'unknown';\n const description = match.metadata.description ?? '';\n logToFile(\n `[YARA] ${phase}:${tool} [${action.toUpperCase()}] rule \"${match.rule}\" ` +\n `(severity: ${severity}, category: ${category}, action: ${ruleAction})\\n` +\n ` Description: ${description}`,\n );\n analytics.wizardCapture('yara rule matched', {\n rule: match.rule,\n severity: match.metadata.severity,\n category: match.metadata.category,\n rule_action: match.metadata.action,\n action,\n phase,\n tool,\n // Rule-static description — the \"why\". The free-text triage reason is\n // deliberately omitted (it can quote scanned content; stays local-only).\n description: match.metadata.description,\n triage_verdict: (match as TriageMatch).triage?.verdict,\n });\n}\n\n/**\n * Send the run's full scan report to PostHog so maintainers can see why a run\n * was flagged, warned, or aborted. This is the maintainer-facing report — it is\n * NEVER shown to the user. No-op if nothing was scanned. The free-text triage\n * reason is omitted to avoid sending scanned content off the user's machine.\n */\nexport function captureScanReport(): void {\n if (scanCount === 0) return;\n analytics.wizardCapture('yara scan report', {\n total_scans: scanCount,\n violation_count: scanViolations.length,\n clean_count: scanCount - scanViolations.length,\n violations: scanViolations.map((v) => ({\n rule: v.rule,\n severity: v.severity,\n category: v.category,\n action: v.action,\n phase: v.phase,\n tool: v.tool,\n description: v.description,\n triage_verdict: v.triageVerdict,\n })),\n });\n // Reset after sending so a second captureScanReport on this same run\n // (e.g. success-path call followed by an abort during shutdown) can't\n // ship duplicate telemetry. Also protects future chained-program runs.\n scanCount = 0;\n scanViolations.length = 0;\n}\n\n/**\n * End-of-run flush for the scan report. Wired once at the runner seam so every\n * harness (linear, orchestrator, and any future shape) reports without having\n * to know warlock exists — scanning is already harness-agnostic (it runs from\n * SDK Pre/PostToolUse hooks), and this keeps reporting at a single shared seam.\n *\n * Order matters: write the local --yara-report file FIRST (it reads scanCount /\n * scanViolations), THEN send telemetry. captureScanReport() zeroes scan state,\n * which is what makes this whole function idempotent — a second call from\n * another termination path (e.g. finally after an abort already flushed) finds\n * scanCount === 0 and every step no-ops.\n *\n * Returns the user-facing report line when a report was written; the caller\n * decides where it goes (the runner emits it as progress).\n */\nexport function flushScanReport(options: {\n yaraReport: boolean;\n}): string | undefined {\n let line: string | undefined;\n if (options.yaraReport) {\n const reportPath = writeScanReport();\n if (reportPath) {\n const summary = formatScanReport();\n line = `YARA scan report: ${reportPath}${summary ?? ''}`;\n }\n }\n captureScanReport();\n return line;\n}\n\n// ─── Wizard-documentation allowlist ───────────────────────────────\n//\n// Files the wizard's own programs write to describe events the user's\n// codebase already captures, or events the integration program is\n// proposing to add. When the agent copies a literal\n// `posthog.capture('event', { email: ... })` snippet (or a property\n// list including PII-shaped keys) into one of these files, the\n// `pii_in_capture_call` rule (category: posthog_pii) fires even though\n// the wizard is documenting / planning, not introducing, the pattern.\n// Suppress posthog_pii matches on these paths only; every other rule\n// (secrets, prompt injection, supply chain, destructive ops) still\n// fires normally so the file cannot be used as a smuggling vector for\n// actual violations.\n\nconst WIZARD_DOC_BASENAMES = new Set([\n EVENT_INVENTORY_FILE,\n EVENTS_AUDIT_REPORT_FILE,\n AUDIT_REPORT_FILE,\n EVENT_PLAN_FILE,\n]);\n\nconst WIZARD_DOC_PATTERNS: RegExp[] = [EVENT_INVENTORY_PART_PATTERN];\n\nexport function isWizardDocumentationPath(\n filePath: string | undefined,\n): boolean {\n if (!filePath) return false;\n const basename = path.basename(filePath);\n if (WIZARD_DOC_BASENAMES.has(basename)) return true;\n return WIZARD_DOC_PATTERNS.some((re) => re.test(basename));\n}\n\n// ─── Repeat-block tracker ─────────────────────────────────────────\n//\n// Field data (switchboard YARA tile): an agent whose edit gets blocked often\n// retries the identical payload — the scan is deterministic, so every retry\n// blocks again and the run burns turns (~4.2 blocks per affected run). The\n// runtime notes already say \"don't retry\"; the data shows the prompt alone\n// doesn't hold. This is the code-side guard: remember the exact payloads we\n// blocked, and when the same content comes back, escalate the block reason —\n// first to \"change the code, not the retry\", then to \"report it and move on\".\n// Used by both fences: the anthropic PreToolUse Bash hook below, and the pi\n// security extension's pre-execution scan (bash/write/edit).\n\n/** Identical-payload attempt at which the agent is told to move on. */\nconst REPEAT_BLOCK_MOVE_ON_ATTEMPT = 3;\n\nexport interface RepeatBlockTracker {\n /**\n * Record a YARA block of `content` on `tool`; returns the attempt count\n * for that exact payload (1 = first time blocked).\n */\n attempt(tool: string, content: string): number;\n}\n\n/** Per-run tracker of YARA-blocked payloads (keyed by tool + content hash). */\nexport function createRepeatBlockTracker(): RepeatBlockTracker {\n const counts = new Map<string, number>();\n return {\n attempt(tool: string, content: string): number {\n const key = `${tool} ${createHash('sha256')\n .update(content)\n .digest('hex')}`;\n const next = (counts.get(key) ?? 0) + 1;\n counts.set(key, next);\n return next;\n },\n };\n}\n\n/**\n * The block reason for the given attempt: the plain reason the first time,\n * an explicit \"this exact retry can never work\" on the second, and a\n * \"report it and move on\" instruction from the third on.\n */\nexport function repeatBlockReason(\n attempt: number,\n tool: string,\n baseReason: string,\n): string {\n if (attempt <= 1) return baseReason;\n if (attempt < REPEAT_BLOCK_MOVE_ON_ATTEMPT) {\n return (\n `${baseReason} This exact ${tool} content was ALREADY blocked — the scan is ` +\n 'deterministic, so retrying identical content will always block again. ' +\n 'Change the code to remove the flagged pattern instead of retrying.'\n );\n }\n return (\n `${baseReason} This exact ${tool} content has now been blocked ${attempt} times. ` +\n 'STOP retrying it. Note the blocked change in the setup report and move on to the next task.'\n );\n}\n\n// ─── Severity helpers ────────────────────────────────────────────\n// Severity vocabulary and the terminate-or-warn decision live in yara-policy.\n\n// ─── Scan + triage core ──────────────────────────────────────────\n\n/**\n * Keep only matches whose rule targets this content surface. Rules carry a\n * `scan_context` of 'command' | 'input' | 'output'. An undefined context is\n * treated as \"applies everywhere\" (fail-safe — a rule that forgets the tag\n * still gets enforced rather than silently skipped).\n */\nfunction matchesForContext(\n matches: ScanMatch[],\n ctx: ScanContext,\n): ScanMatch[] {\n return matches.filter((m) => {\n const c = m.metadata.scan_context as string | undefined;\n return c === ctx || c === undefined;\n });\n}\n\n/**\n * Drop false positives via warlock's LLM triage. Fail-closed: if no provider is\n * available, or the triage call throws, every match is treated as real — we\n * never silently suppress a flagged match.\n *\n * Every overruled match is reported to PostHog (rule metadata only, never the\n * free-text reason) so maintainers can alert on triage-overrule patterns — the\n * signal that someone is either tripping a noisy rule or trying to talk the\n * triage model out of a real finding.\n */\nasync function triageFilter(\n content: string,\n matches: ScanMatch[],\n ctx: ScanContext,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n if (matches.length === 0) return [];\n if (!llmProvider) {\n logToFile(\n `[YARA] triage skipped (no provider) — treating ${matches.length} match(es) as real`,\n );\n return matches;\n }\n try {\n const warlock = await getWarlock();\n const triaged = await warlock.triageMatches(content, matches, llmProvider);\n const kept = triaged.filter((m) => m.triage.verdict === 'true_positive');\n for (const m of triaged) {\n if (m.triage.verdict === 'true_positive') continue;\n logToFile(\n `[YARA] triage overruled rule \"${m.rule}\" (${\n m.metadata.severity ?? 'unknown'\n }) — not acting on it`,\n );\n analytics.wizardCapture('yara triage overruled', {\n rule: m.rule,\n severity: m.metadata.severity,\n category: m.metadata.category,\n scan_context: ctx,\n // The free-text triage reason is deliberately omitted — it can quote\n // scanned content, which must never leave the user's machine.\n });\n }\n logToFile(\n `[YARA] triage: ${matches.length} flagged → ${kept.length} kept as real`,\n );\n return kept;\n } catch (err) {\n logToFile('[YARA] triage failed — treating all matches as real:', err);\n return matches;\n }\n}\n\n/** A chunk of scanned content together with the matches found inside it. */\ninterface FlaggedChunk {\n chunk: string;\n matches: ScanMatch[];\n}\n\n/**\n * Split oversized content into overlapping chunks so the scanner covers all\n * of it. Content that fits in one chunk is returned as-is.\n */\nfunction chunkContent(content: string): string[] {\n if (content.length <= SCAN_CHUNK_SIZE) return [content];\n const chunks: string[] = [];\n const step = SCAN_CHUNK_SIZE - SCAN_CHUNK_OVERLAP;\n for (let start = 0; start < content.length; start += step) {\n chunks.push(content.slice(start, start + SCAN_CHUNK_SIZE));\n if (start + SCAN_CHUNK_SIZE >= content.length) break;\n }\n return chunks;\n}\n\n/**\n * Scan content against warlock rules — chunked when oversized, so nothing is\n * skipped — and keep only matches for this content surface. Chunks scan\n * sequentially (the WASM engine is single-threaded; parallelism buys nothing).\n * Returns each flagged chunk with its matches so triage can later judge every\n * match against the exact content it came from.\n */\nasync function scanForContext(\n content: string,\n ctx: ScanContext,\n): Promise<FlaggedChunk[]> {\n const warlock = await getWarlock();\n const chunks = chunkContent(content);\n if (chunks.length > 1) {\n logToFile(\n `[YARA] content is ${content.length} chars — scanning ${chunks.length} overlapping chunks`,\n );\n // Alertable signal: oversized content should be rare; a spike could mean\n // someone is padding content to probe the scanner.\n analytics.wizardCapture('yara scan chunked', {\n content_length: content.length,\n chunk_count: chunks.length,\n scan_context: ctx,\n });\n }\n const flagged: FlaggedChunk[] = [];\n for (const chunk of chunks) {\n const result = await warlock.scan(chunk);\n if (!result.matched) continue;\n const matches = matchesForContext(result.matches, ctx);\n if (matches.length === 0) continue;\n flagged.push({ chunk, matches });\n }\n return flagged;\n}\n\n/** The overlap between chunks can surface the same rule twice; count it once. */\nfunction dedupeByRule(matches: ScanMatch[]): ScanMatch[] {\n const seen = new Set<string>();\n return matches.filter((m) => {\n if (seen.has(m.rule)) return false;\n seen.add(m.rule);\n return true;\n });\n}\n\n/**\n * Triage each flagged chunk against its own content — the evidence is always\n * inside the window the LLM sees. Triage calls run in parallel (each is just\n * an HTTP round trip) so N flagged chunks cost ~1× triage latency, not N×.\n */\nasync function triageFlagged(\n flagged: FlaggedChunk[],\n ctx: ScanContext,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n const triaged = await Promise.all(\n flagged.map(({ chunk, matches }) =>\n triageFilter(chunk, matches, ctx, llmProvider),\n ),\n );\n return dedupeByRule(triaged.flat());\n}\n\n/**\n * Scan content, filter to the relevant context, triage. Returns real matches.\n * Exported for harnesses that run their own hook loop (pi) so every harness\n * scans through the same warlock engine, chunking, and triage policy.\n */\nexport async function scanAndTriage(\n content: string,\n ctx: ScanContext,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n const flagged = await scanForContext(content, ctx);\n return triageFlagged(flagged, ctx, llmProvider);\n}\n\n// ─── PreToolUse Hooks ────────────────────────────────────────────\n\n/**\n * Create PreToolUse hook matchers for YARA scanning.\n * 1. Bash — scans commands before execution for exfiltration, destructive\n * operations, and supply chain violations ('command'-context rules).\n * 2. publish_handoff — scans the report content with the same 'output'-context\n * rules + triage as the PostToolUse Write/Edit hook, but BEFORE the MCP\n * tool runs: the report is agent-authored and is persisted to a\n * host-designated file (POSTHOG_HANDOFF_OUTPUT_PATH) as well as pushed to\n * the task stream. Only a CRITICAL match refuses the publish; below that\n * the report goes out with the match recorded.\n */\nexport function createPreToolUseYaraHooks(\n // Used only by the publish_handoff matcher below (output-context triage,\n // same fail-closed semantics as Write/Edit). The Bash matcher still passes\n // undefined — any flagged command blocks regardless of triage verdict, so\n // the LLM call would be wasted.\n llmProvider?: LLMProvider,\n // Ends the run when a handoff scan errors — without it that path just blocks,\n // which is the old behavior, so it stays optional.\n onTerminate?: (reason: string) => void,\n): HookCallbackMatcher[] {\n const repeatTracker = createRepeatBlockTracker();\n return [\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Bash') return {};\n\n const toolInput = input.tool_input as Record<string, unknown>;\n const command =\n typeof toolInput?.command === 'string' ? toolInput.command : '';\n\n if (!command) return {};\n\n recordScan();\n // Skip triage on PreToolUse Bash: any flagged command is blocked\n // regardless of triage verdict, so the LLM call would be wasted.\n const matches = await scanAndTriage(command, 'command', undefined);\n if (matches.length === 0) return {};\n\n const match = highestSeverityMatch(matches);\n recordMatch('PreToolUse', 'Bash', match, 'blocked');\n\n const attempt = repeatTracker.attempt('Bash', command);\n return {\n decision: 'block',\n reason: repeatBlockReason(\n attempt,\n 'Bash',\n `[YARA] ${match.rule}: ${\n match.metadata.description ?? 'security policy violation'\n }. Command blocked for security.`,\n ),\n };\n } catch (error) {\n logToFile('[YARA] PreToolUse hook error:', error);\n // Fail closed: block the command if scanning fails\n return {\n decision: 'block',\n reason: '[YARA] Scanner error — command blocked as a precaution.',\n };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n {\n // ── publish_handoff content scanning ──\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n // The wizard's publish_handoff MCP tool, fully qualified by the\n // SDK (e.g. mcp__wizard-tools__publish_handoff). Matched by\n // suffix so a server rename can't silently reopen the gap — the\n // exact FQN lives in WIZARD_TOOL_NAMES (wizard-tools/tools.ts),\n // which this module can't import without a cycle (tools.ts\n // imports scanInstalledSkill from here).\n if (\n typeof toolName !== 'string' ||\n !toolName.endsWith('__publish_handoff')\n ) {\n return {};\n }\n\n const toolInput = input.tool_input as Record<string, unknown>;\n const content =\n typeof toolInput?.content === 'string' ? toolInput.content : '';\n\n if (!content) return {};\n\n recordScan();\n // Same 'output'-context scan + triage as the Write/Edit hook:\n // the report is agent-authored, quotes the user's code, and now\n // reaches a second persisted consumer (the host file + the cloud\n // worker's PR body) — so it must clear the bar of any other\n // agent-driven write BEFORE the tool executes.\n const matches = await scanAndTriage(content, 'output', llmProvider);\n if (matches.length === 0) return {};\n\n // Below critical: record it, don't cost the run its only report.\n const match = publishBlockingMatch(matches);\n if (!match) {\n const worst = highestSeverityMatch(matches);\n recordMatch('PreToolUse', 'publish_handoff', worst, 'warned');\n logToFile(\n `[YARA] publish_handoff allowed with a ${\n worst.metadata.severity ?? 'unknown'\n } match (${worst.rule}) — below the critical publish bar`,\n );\n return {};\n }\n\n recordMatch('PreToolUse', 'publish_handoff', match, 'blocked');\n\n const attempt = repeatTracker.attempt('publish_handoff', content);\n return {\n decision: 'block',\n reason: repeatBlockReason(\n attempt,\n 'publish_handoff',\n `[YARA] ${match.rule}: ${\n match.metadata.description ?? 'security policy violation'\n }. Handoff blocked for security — remove the flagged content from the report.`,\n ),\n };\n } catch (error) {\n logToFile('[YARA] PreToolUse publish_handoff hook error:', error);\n // Fail closed, but end the run: a silent block would just leave\n // the agent rewording a report that can never publish.\n const reason =\n '[YARA] Scanner error while scanning the handoff report — session terminated as a precaution.';\n onTerminate?.(reason);\n return { decision: 'block', reason };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n ];\n}\n\n// ─── PostToolUse Hooks ───────────────────────────────────────────\n\n/**\n * Create PostToolUse hook matchers for YARA scanning.\n *\n * Three matchers:\n * 1. Write/Edit — scan written content ('output'-context rules: PII, secrets)\n * 2. Read/Grep — scan read content ('input'-context rules: prompt injection)\n * 3. Bash (skill install) — scan downloaded skill files for poisoned content\n *\n * `onTerminate` is invoked on the terminal paths (critical prompt injection in\n * read content, a poisoned skill, or a scanner error under fail-closed). It is\n * what ACTUALLY stops the run — returning `stopReason` from a PostToolUse hook\n * is not honored by the SDK, so the caller wires this to the run's\n * AbortController (see agent-interface).\n */\nexport function createPostToolUseYaraHooks(\n llmProvider: LLMProvider | undefined,\n // REQUIRED, not optional: the SDK ignores `stopReason` from PostToolUse\n // hooks, so terminal paths (critical prompt injection, poisoned skill,\n // fail-closed scanner error) depend entirely on this callback firing.\n // Making it required at compile time prevents a future caller from\n // silently regressing every terminal path to a no-op.\n onTerminate: (reason: string) => void,\n): HookCallbackMatcher[] {\n return [\n // ── Write/Edit content scanning ──\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Write' && toolName !== 'Edit') return {};\n\n const toolInput = input.tool_input as Record<string, unknown>;\n // For Write, scan the content being written.\n // For Edit, scan the new_string (replacement text).\n const content =\n toolName === 'Write'\n ? (toolInput?.content as string) ?? ''\n : (toolInput?.new_string as string) ?? '';\n\n if (!content) return {};\n\n recordScan();\n const flagged = await scanForContext(content, 'output');\n if (flagged.length === 0) return {};\n\n // Wizard-documentation paths: suppress posthog_pii matches that\n // come from the agent verbatim-copying the user's existing\n // capture calls into an inventory / report, or planning new\n // events with PII-shaped property keys. Every other category\n // still triggers the revert. Suppression runs BEFORE triage so\n // we never pay an LLM round trip for a match we're about to\n // discard anyway.\n // TODO(warlock#33): once warlock PR #33 lands with its\n // more-precise PII rules, the wizard-side suppression below\n // should become unnecessary — remove `WIZARD_DOC_BASENAMES`,\n // `WIZARD_DOC_PATTERNS`, and `isWizardDocumentationPath` and\n // verify the noisy-PII issue stays fixed.\n const filePath = toolInput?.file_path as string | undefined;\n const activeFlagged = isWizardDocumentationPath(filePath)\n ? flagged\n .map(({ chunk, matches }) => ({\n chunk,\n matches: matches.filter(\n (m) => m.metadata.category !== 'posthog_pii',\n ),\n }))\n .filter(({ matches }) => matches.length > 0)\n : flagged;\n if (activeFlagged.length === 0) {\n logToFile(\n `[YARA] posthog_pii match suppressed on wizard doc ${path.basename(\n filePath ?? '',\n )} (rule: ${flagged[0]?.matches[0]?.rule})`,\n );\n return {};\n }\n\n const matches = await triageFlagged(\n activeFlagged,\n 'output',\n llmProvider,\n );\n if (matches.length === 0) return {};\n\n const match = highestSeverityMatch(matches);\n recordMatch('PostToolUse', toolName, match, 'reverted');\n\n return {\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext:\n `[YARA VIOLATION] ${match.rule}: ${\n match.metadata.description ?? ''\n }. ` +\n `You MUST revert this change immediately. The content you just wrote violates security policy.`,\n },\n };\n } catch (error) {\n logToFile('[YARA] PostToolUse Write/Edit hook error:', error);\n // Fail closed: if scanning is broken on a Write/Edit, the next\n // Read/skill-install scan is also going to be broken — terminate\n // cleanly instead of looping the agent through \"please revert\"\n // until a Read finally aborts. Matches Read/Grep + skill-install\n // catch behavior; consistent fail-closed across all PostToolUse.\n const reason =\n '[YARA] Scanner error while scanning written content — session terminated as a precaution.';\n onTerminate(reason);\n return { stopReason: reason };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n\n // ── Read/Grep prompt injection scanning ──\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Read' && toolName !== 'Grep') return {};\n\n const toolResponse = input.tool_response;\n // Guard before stringify: `JSON.stringify(undefined ?? '')`\n // returns the 2-char string '\"\"', which is truthy and slips\n // past the empty-content check below. Exit early when the SDK\n // gave us no response payload.\n if (toolResponse == null) return {};\n const content =\n typeof toolResponse === 'string'\n ? toolResponse\n : JSON.stringify(toolResponse);\n\n if (!content) return {};\n\n recordScan();\n const matches = await scanAndTriage(content, 'input', llmProvider);\n const verdict = scanVerdict(matches);\n if (!verdict) return {};\n\n const { match } = verdict;\n recordMatch('PostToolUse', toolName, match, verdict.action);\n\n if (verdict.terminal) {\n const reason =\n `[YARA CRITICAL] ${match.rule}: Prompt injection detected in file content. ` +\n `Agent context is potentially poisoned. Session terminated for safety.`;\n onTerminate(reason);\n return { stopReason: reason };\n }\n\n return {\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext: `[YARA WARNING] ${match.rule}: ${\n match.metadata.description ?? ''\n }`,\n },\n };\n } catch (error) {\n logToFile('[YARA] PostToolUse Read/Grep hook error:', error);\n // Fail closed: terminate session if scanning fails on read content\n const reason =\n '[YARA] Scanner error while scanning read content — session terminated as a precaution.';\n onTerminate(reason);\n return { stopReason: reason };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n\n // ── Context-mill skill install scanning ──\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Bash') return {};\n\n const toolInput = input.tool_input as Record<string, unknown>;\n const command =\n typeof toolInput?.command === 'string' ? toolInput.command : '';\n\n // Only scan after skill install commands\n if (!isSkillInstallCommand(command)) return {};\n\n // Extract skill directory from command\n const dirMatch = command.match(\n /mkdir -p (.claude\\/skills\\/[^\\s&]+)/,\n );\n if (!dirMatch) return {};\n\n const skillDir = dirMatch[1];\n const cwd = (input.cwd as string) ?? process.cwd();\n recordScan();\n const matches = await scanSkillFiles(cwd, skillDir, llmProvider);\n\n const verdict = scanVerdict(matches);\n if (!verdict) return {};\n\n recordMatch(\n 'PostToolUse',\n 'Bash (skill install)',\n verdict.match,\n verdict.action,\n );\n // Same verdict as every other surface: critical terminates, the\n // rest warn. These rules fire on first-party skill prose often\n // enough that terminating costs more than it prevents.\n if (!verdict.terminal) return {};\n\n const reason =\n `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${verdict.match.rule}. ` +\n `The downloaded skill contains potential prompt injection. Session terminated for safety.`;\n onTerminate(reason);\n return { stopReason: reason };\n } catch (error) {\n logToFile('[YARA] PostToolUse skill install hook error:', error);\n // Fail closed: terminate if skill scanning fails\n const reason =\n '[YARA] Scanner error while scanning skill files — session terminated as a precaution.';\n onTerminate(reason);\n return { stopReason: reason };\n }\n },\n ],\n timeout: SKILL_SCAN_HOOK_TIMEOUT_MS,\n },\n ];\n}\n\n// ─── Skill File Scanner ──────────────────────────────────────────\n\n/**\n * Scan a freshly installed skill directory (any root — .claude/skills or the\n * orchestrator's run cache) and return a terminate reason when it is poisoned,\n * else null. The choke point for TS-path installs (downloadSkill); agent Bash\n * installs are covered by the PostToolUse matcher above. Runs the same LLM\n * triage as the tool-use scans; fail-closed to treating every match as real when\n * no provider is configured, so a missing key never weakens the check.\n * `llmProvider` is explicit — pi and the orchestrator never set the env fallback.\n *\n * Terminates on the same verdict as every other surface (critical only). A\n * non-terminal match is recorded and the skill is kept: these rules fire on\n * first-party skill prose, and deleting the skill leaves the agent working blind\n * on the very content it needed.\n */\nexport async function scanInstalledSkill(\n absoluteSkillDir: string,\n llmProvider: LLMProvider | undefined,\n): Promise<string | null> {\n recordScan();\n const matches = await scanSkillFiles(absoluteSkillDir, '.', llmProvider);\n const verdict = scanVerdict(matches);\n if (!verdict) return null;\n recordMatch(\n 'skill-install',\n 'installSkillById',\n verdict.match,\n verdict.action,\n );\n if (!verdict.terminal) {\n logToFile(\n `[YARA] ${verdict.match.rule} (${\n verdict.match.metadata.severity ?? 'unknown'\n }) in ${absoluteSkillDir} — non-terminal, keeping the skill`,\n );\n return null;\n }\n return `Poisoned skill detected: ${verdict.match.rule} (${\n verdict.match.metadata.severity ?? 'unknown'\n }) in ${absoluteSkillDir}`;\n}\n\n/**\n * Read and scan all text files in a skill directory for prompt injection.\n * Scans each file sequentially (single-threaded WASM — parallelism buys\n * nothing), unions the 'input'-context matches, then triages once across the\n * combined content. Returns the real (post-triage) matches.\n */\nasync function scanSkillFiles(\n cwd: string,\n skillDir: string,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n const absoluteDir = path.resolve(cwd, skillDir);\n\n if (!fs.existsSync(absoluteDir)) {\n logToFile(`[YARA] Skill directory does not exist: ${absoluteDir}`);\n return [];\n }\n\n const files = await fg('**/*.{md,txt,yaml,yml,json,js,ts,py,rb,sh}', {\n cwd: absoluteDir,\n absolute: true,\n });\n\n if (files.length === 0) {\n logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);\n return [];\n }\n\n logToFile(\n `[YARA] Scanning ${files.length} files in skill directory: ${skillDir}`,\n );\n\n // Pass 1 (sequential): read and scan ONE file at a time through the\n // chunk-aware path. Oversized files are scanned up to the cap and logged —\n // never silently skipped. Flagged chunks keep a reference to the exact\n // content their matches came from.\n const flagged: FlaggedChunk[] = [];\n for (const filePath of files) {\n let content: string | null;\n try {\n if (fs.statSync(filePath).size > SKILL_FILE_SCAN_BYTES) {\n logToFile(\n `[YARA] Skill file over ${SKILL_FILE_SCAN_BYTES} bytes; scanning its head only: ${filePath}`,\n );\n content = readFileHead(filePath, SKILL_FILE_SCAN_BYTES);\n } else {\n content = fs.readFileSync(filePath, 'utf-8');\n }\n } catch (err) {\n logToFile(`[YARA] Could not read skill file ${filePath}:`, err);\n continue;\n }\n if (content) {\n flagged.push(...(await scanForContext(content, 'input')));\n }\n }\n\n // Pass 2 (parallel, inside triageFlagged): triage each flagged chunk\n // against its own content concurrently. Triage is just an axios call —\n // running them in parallel cuts total wall-clock from N × triage-timeout\n // to ~1×. Important because the hook's HOOK_TIMEOUT_MS is 30s; serial\n // triage on 2+ matched files could blow the hook timeout under a slow\n // Haiku.\n return triageFlagged(flagged, 'input', llmProvider);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAUA,SAAgB,sBAAsB,SAA0B;AAC9D,KAAI,CAAC,QAAQ,WAAW,2BAA2B,CAAE,QAAO;CAE5D,MAAM,WAAW,QAAQ,MAAM,4BAA4B;AAC3D,KAAI,CAAC,SAAU,QAAO;CAItB,MAAM,MAAM,SAAS;AACrB,QACE,IAAI,WAAW,oDAAoD,IACnE,IAAI,WAAW,oCAAoC,IACnD,4BAA4B,KAAK,IAAI;;;;;ACLzC,MAAM,gBAAwC;CAC5C,UAAU;CACV,MAAM;CACN,QAAQ;CACR,KAAK;CACN;;AAGD,MAAM,sBAAsB,IAAI,IAAI,CAAC,WAAW,CAAC;;;;;;;;AASjD,SAAgB,gBAAgB,OAA2B;AACzD,QAAO,oBAAoB,IAAI,MAAM,SAAS,YAAY,GAAG;;;AAI/D,SAAgB,qBAAqB,SAAiC;AACpE,QAAO,QAAQ,QAAQ,OAAO,OAC3B,cAAc,EAAE,SAAS,YAAY,OAAO,MAC5C,cAAc,MAAM,SAAS,YAAY,OAAO,KAC7C,IACA,MACL;;;;;;AAOH,SAAgB,qBAAqB,SAAwC;AAC3E,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAQ,qBAAqB,QAAQ;AAC3C,QAAO,gBAAgB,MAAM,GAAG,QAAQ;;;AAa1C,SAAgB,YAAY,SAA0C;AACpE,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAQ,qBAAqB,QAAQ;CAC3C,MAAM,WAAW,gBAAgB,MAAM;AACvC,QAAO;EAAE;EAAO;EAAU,QAAQ,WAAW,YAAY;EAAU;;;;ACjDrE,MAAa,oBAAoB;AACjC,MAAa,oBAAoB;;;;;;;;;AAUjC,SAAgB,kBAAkB,QAA+B;AAC/D,QAAO,MAAM,QAAQ,OAAO,GAAI,SAA0B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;AC+B9D,IAAI,uBAAsD;AAE1D,SAAS,aAAqC;AAC5C,KAAI,CAAC,qBAKH,wBAAuB,OAAO,oBAAoB,OAAO,QAAQ;AAC/D,yBAAuB;AACvB,QAAM;GACN;AAEJ,QAAO;;;;;;;AAQT,eAAsB,qBAAoC;AACxD,KAAI;AAEF,SADgB,MAAM,YAAY,EACpB,KAAK,GAAG;AACtB,YAAU,4BAA4B;UAC/B,KAAK;AACZ,YAAU,mCAAmC,IAAI;;;AA6CrD,IAAI,YAAY;AAChB,MAAM,iBAAoC,EAAE;AAE5C,SAAS,aAAmB;AAC1B;;;;;;AAUF,SAAS,YACP,OACA,MACA,OACA,QACM;CACN,MAAM,SAAU,MAAsB;AACtC,cAAa,OAAO,MAAM,OAAO,OAAO;AACxC,gBAAe,KAAK;EAClB,MAAM,MAAM;EACZ,UAAU,MAAM,SAAS,YAAY;EACrC,UAAU,MAAM,SAAS,YAAY;EACrC;EACA;EACA;EACA,aAAa,MAAM,SAAS,eAAe;EAC3C,eAAe,QAAQ;EACvB,cAAc,QAAQ;EACvB,CAAC;;;;;;;AAcJ,SAAgB,mBACd,OACA,MACA,YAMA,QACM;AACN,aAAY;AACZ,MAAK,MAAM,KAAK,WACd,aACE,OACA,MACA;EACE,MAAM,EAAE;EACR,UAAU;GACR,UAAU,EAAE;GACZ,UAAU,EAAE;GACZ,aAAa,EAAE;GAChB;EACD,gBAAgB,EAAE;EACnB,EACD,OACD;;;AAKL,SAAgB,mBAAkC;AAChD,KAAI,cAAc,EAAG,QAAO;CAE5B,MAAM,QAAkB,CAAC,IAAI,2BAA2B;CACxD,MAAM,iBAAiB,eAAe;CACtC,MAAM,aAAa,YAAY;AAE/B,OAAM,KACJ,KAAK,UAAU,uBAAuB,eAAe,YACnD,mBAAmB,IAAI,MAAM,GAC9B,WACF;AAED,KAAI,iBAAiB,GAAG;AACtB,QAAM,KAAK,GAAG;AACd,OAAK,MAAM,KAAK,gBAAgB;GAC9B,MAAM,MAAM,EAAE,OAAO,aAAa;AAClC,SAAM,KACJ,MAAM,IAAI,IAAI,EAAE,KAAK,IAAI,EAAE,SAAS,aAAa,CAAC,MAAM,EAAE,MAAM,GAC9D,EAAE,OAEL;;;AAIL,KAAI,aAAa,GAAG;AAClB,QAAM,KAAK,GAAG;AACd,QAAM,KACJ,oBAAoB,WAAW,aAAa,eAAe,IAAI,MAAM,KACtE;;AAGH,OAAM,KAAK,GAAG;AACd,QAAO,MAAM,KAAK,KAAK;;;AAIzB,SAAgB,kBAAiC;AAC/C,KAAI,cAAc,EAAG,QAAO;CAE5B,MAAM,SAAS;EACb,SAAS;GACP,YAAY;GACZ,YAAY,eAAe;GAC3B,OAAO,YAAY,eAAe;GACnC;EACD,YAAY;EACb;AAED,KAAI;AACF,KAAG,cAAc,yBAAyB,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;UACnE,KAAK;AACZ,YAAU,uCAAuC,IAAI;AACrD,SAAO;;AAET,QAAO;;AAcT,MAAM,wBAAkD;CACtD,kBACE;CACF,cAAc;CACd,wBAAwB;CACxB,cAAc;CACd,aAAa;CACb,uBAAuB;CACvB,kBAAkB;CACnB;;AAGD,SAAS,2BAAmD;AAC1D,MAAK,IAAI,IAAI,eAAe,SAAS,GAAG,KAAK,GAAG,IAC9C,KAAI,eAAe,GAAG,WAAW,UAAW,QAAO,eAAe;AAEpE,QAAO;;;;;;;;;AAUT,SAAgB,yBAAiC;CAC/C,MAAM,IAAI,0BAA0B;AAKpC,QACE;;aAJC,KAAK,sBAAsB,EAAE,cAC9B,6CAIkB;;;;;AAatB,MAAM,kBAAkB;;AAExB,MAAM,6BAA6B;;;;;;;AAQnC,MAAM,kBAAkB;AAGxB,MAAM,wBAAwB,KAAK,OAAO;;;;;;AAM1C,MAAM,qBAAqB;AAI3B,SAAS,aACP,OACA,MACA,OACA,QACM;CACN,MAAM,WAAW,MAAM,SAAS,YAAY;CAC5C,MAAM,WAAW,MAAM,SAAS,YAAY;CAC5C,MAAM,aAAa,MAAM,SAAS,UAAU;CAC5C,MAAM,cAAc,MAAM,SAAS,eAAe;AAClD,WACE,UAAU,MAAM,GAAG,KAAK,IAAI,OAAO,aAAa,CAAC,UAAU,MAAM,KAAK,eACtD,SAAS,cAAc,SAAS,YAAY,WAAW,oBACnD,cACrB;AACD,WAAU,cAAc,qBAAqB;EAC3C,MAAM,MAAM;EACZ,UAAU,MAAM,SAAS;EACzB,UAAU,MAAM,SAAS;EACzB,aAAa,MAAM,SAAS;EAC5B;EACA;EACA;EAGA,aAAa,MAAM,SAAS;EAC5B,gBAAiB,MAAsB,QAAQ;EAChD,CAAC;;;;;;;;AASJ,SAAgB,oBAA0B;AACxC,KAAI,cAAc,EAAG;AACrB,WAAU,cAAc,oBAAoB;EAC1C,aAAa;EACb,iBAAiB,eAAe;EAChC,aAAa,YAAY,eAAe;EACxC,YAAY,eAAe,KAAK,OAAO;GACrC,MAAM,EAAE;GACR,UAAU,EAAE;GACZ,UAAU,EAAE;GACZ,QAAQ,EAAE;GACV,OAAO,EAAE;GACT,MAAM,EAAE;GACR,aAAa,EAAE;GACf,gBAAgB,EAAE;GACnB,EAAE;EACJ,CAAC;AAIF,aAAY;AACZ,gBAAe,SAAS;;;;;;;;;;;;;;;;;AAkB1B,SAAgB,gBAAgB,SAET;CACrB,IAAI;AACJ,KAAI,QAAQ,YAAY;EACtB,MAAM,aAAa,iBAAiB;AACpC,MAAI,WAEF,QAAO,qBAAqB,aADZ,kBAAkB,IACkB;;AAGxD,oBAAmB;AACnB,QAAO;;AAiBT,MAAM,uBAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,sBAAgC,CAAC,6BAA6B;AAEpE,SAAgB,0BACd,UACS;AACT,KAAI,CAAC,SAAU,QAAO;CACtB,MAAM,WAAW,KAAK,SAAS,SAAS;AACxC,KAAI,qBAAqB,IAAI,SAAS,CAAE,QAAO;AAC/C,QAAO,oBAAoB,MAAM,OAAO,GAAG,KAAK,SAAS,CAAC;;;AAgB5D,MAAM,+BAA+B;;AAWrC,SAAgB,2BAA+C;CAC7D,MAAM,yBAAS,IAAI,KAAqB;AACxC,QAAO,EACL,QAAQ,MAAc,SAAyB;EAC7C,MAAM,MAAM,GAAG,KAAK,GAAG,WAAW,SAAS,CACxC,OAAO,QAAQ,CACf,OAAO,MAAM;EAChB,MAAM,QAAQ,OAAO,IAAI,IAAI,IAAI,KAAK;AACtC,SAAO,IAAI,KAAK,KAAK;AACrB,SAAO;IAEV;;;;;;;AAQH,SAAgB,kBACd,SACA,MACA,YACQ;AACR,KAAI,WAAW,EAAG,QAAO;AACzB,KAAI,UAAU,6BACZ,QACE,GAAG,WAAW,cAAc,KAAK;AAKrC,QACE,GAAG,WAAW,cAAc,KAAK,gCAAgC,QAAQ;;;;;;;;AAgB7E,SAAS,kBACP,SACA,KACa;AACb,QAAO,QAAQ,QAAQ,MAAM;EAC3B,MAAM,IAAI,EAAE,SAAS;AACrB,SAAO,MAAM,OAAO,MAAM,KAAA;GAC1B;;;;;;;;;;;;AAaJ,eAAe,aACb,SACA,SACA,KACA,aACsB;AACtB,KAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;AACnC,KAAI,CAAC,aAAa;AAChB,YACE,kDAAkD,QAAQ,OAAO,oBAClE;AACD,SAAO;;AAET,KAAI;EAEF,MAAM,UAAU,OADA,MAAM,YAAY,EACJ,cAAc,SAAS,SAAS,YAAY;EAC1E,MAAM,OAAO,QAAQ,QAAQ,MAAM,EAAE,OAAO,YAAY,gBAAgB;AACxE,OAAK,MAAM,KAAK,SAAS;AACvB,OAAI,EAAE,OAAO,YAAY,gBAAiB;AAC1C,aACE,iCAAiC,EAAE,KAAK,KACtC,EAAE,SAAS,YAAY,UACxB,sBACF;AACD,aAAU,cAAc,yBAAyB;IAC/C,MAAM,EAAE;IACR,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,SAAS;IACrB,cAAc;IAGf,CAAC;;AAEJ,YACE,kBAAkB,QAAQ,OAAO,aAAa,KAAK,OAAO,eAC3D;AACD,SAAO;UACA,KAAK;AACZ,YAAU,wDAAwD,IAAI;AACtE,SAAO;;;;;;;AAcX,SAAS,aAAa,SAA2B;AAC/C,KAAI,QAAQ,UAAU,gBAAiB,QAAO,CAAC,QAAQ;CACvD,MAAM,SAAmB,EAAE;CAC3B,MAAM,OAAO,kBAAkB;AAC/B,MAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,MAAM;AACzD,SAAO,KAAK,QAAQ,MAAM,OAAO,QAAQ,gBAAgB,CAAC;AAC1D,MAAI,QAAQ,mBAAmB,QAAQ,OAAQ;;AAEjD,QAAO;;;;;;;;;AAUT,eAAe,eACb,SACA,KACyB;CACzB,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,SAAS,aAAa,QAAQ;AACpC,KAAI,OAAO,SAAS,GAAG;AACrB,YACE,qBAAqB,QAAQ,OAAO,oBAAoB,OAAO,OAAO,qBACvE;AAGD,YAAU,cAAc,qBAAqB;GAC3C,gBAAgB,QAAQ;GACxB,aAAa,OAAO;GACpB,cAAc;GACf,CAAC;;CAEJ,MAAM,UAA0B,EAAE;AAClC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,MAAM,QAAQ,KAAK,MAAM;AACxC,MAAI,CAAC,OAAO,QAAS;EACrB,MAAM,UAAU,kBAAkB,OAAO,SAAS,IAAI;AACtD,MAAI,QAAQ,WAAW,EAAG;AAC1B,UAAQ,KAAK;GAAE;GAAO;GAAS,CAAC;;AAElC,QAAO;;;AAIT,SAAS,aAAa,SAAmC;CACvD,MAAM,uBAAO,IAAI,KAAa;AAC9B,QAAO,QAAQ,QAAQ,MAAM;AAC3B,MAAI,KAAK,IAAI,EAAE,KAAK,CAAE,QAAO;AAC7B,OAAK,IAAI,EAAE,KAAK;AAChB,SAAO;GACP;;;;;;;AAQJ,eAAe,cACb,SACA,KACA,aACsB;AAMtB,QAAO,cALS,MAAM,QAAQ,IAC5B,QAAQ,KAAK,EAAE,OAAO,cACpB,aAAa,OAAO,SAAS,KAAK,YAAY,CAC/C,CACF,EAC2B,MAAM,CAAC;;;;;;;AAQrC,eAAsB,cACpB,SACA,KACA,aACsB;AAEtB,QAAO,cADS,MAAM,eAAe,SAAS,IAAI,EACpB,KAAK,YAAY;;;;;;;;;;;;;AAgBjD,SAAgB,0BAKd,aAGA,aACuB;CACvB,MAAM,gBAAgB,0BAA0B;AAChD,QAAO,CACL;EACE,OAAO,CACL,OAAO,UAA0C;AAC/C,OAAI;AAEF,QADiB,MAAM,cACN,OAAQ,QAAO,EAAE;IAElC,MAAM,YAAY,MAAM;IACxB,MAAM,UACJ,OAAO,WAAW,YAAY,WAAW,UAAU,UAAU;AAE/D,QAAI,CAAC,QAAS,QAAO,EAAE;AAEvB,gBAAY;IAGZ,MAAM,UAAU,MAAM,cAAc,SAAS,WAAW,KAAA,EAAU;AAClE,QAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;IAEnC,MAAM,QAAQ,qBAAqB,QAAQ;AAC3C,gBAAY,cAAc,QAAQ,OAAO,UAAU;AAGnD,WAAO;KACL,UAAU;KACV,QAAQ,kBAHM,cAAc,QAAQ,QAAQ,QAAQ,EAKlD,QACA,UAAU,MAAM,KAAK,IACnB,MAAM,SAAS,eAAe,4BAC/B,iCACF;KACF;YACM,OAAO;AACd,cAAU,iCAAiC,MAAM;AAEjD,WAAO;KACL,UAAU;KACV,QAAQ;KACT;;IAGN;EACD,SAAS;EACV,EACD;EAEE,OAAO,CACL,OAAO,UAA0C;AAC/C,OAAI;IACF,MAAM,WAAW,MAAM;AAOvB,QACE,OAAO,aAAa,YACpB,CAAC,SAAS,SAAS,oBAAoB,CAEvC,QAAO,EAAE;IAGX,MAAM,YAAY,MAAM;IACxB,MAAM,UACJ,OAAO,WAAW,YAAY,WAAW,UAAU,UAAU;AAE/D,QAAI,CAAC,QAAS,QAAO,EAAE;AAEvB,gBAAY;IAMZ,MAAM,UAAU,MAAM,cAAc,SAAS,UAAU,YAAY;AACnE,QAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;IAGnC,MAAM,QAAQ,qBAAqB,QAAQ;AAC3C,QAAI,CAAC,OAAO;KACV,MAAM,QAAQ,qBAAqB,QAAQ;AAC3C,iBAAY,cAAc,mBAAmB,OAAO,SAAS;AAC7D,eACE,yCACE,MAAM,SAAS,YAAY,UAC5B,UAAU,MAAM,KAAK,oCACvB;AACD,YAAO,EAAE;;AAGX,gBAAY,cAAc,mBAAmB,OAAO,UAAU;AAG9D,WAAO;KACL,UAAU;KACV,QAAQ,kBAHM,cAAc,QAAQ,mBAAmB,QAAQ,EAK7D,mBACA,UAAU,MAAM,KAAK,IACnB,MAAM,SAAS,eAAe,4BAC/B,8EACF;KACF;YACM,OAAO;AACd,cAAU,iDAAiD,MAAM;IAGjE,MAAM,SACJ;AACF,kBAAc,OAAO;AACrB,WAAO;KAAE,UAAU;KAAS;KAAQ;;IAGzC;EACD,SAAS;EACV,CACF;;;;;;;;;;;;;;;;AAmBH,SAAgB,2BACd,aAMA,aACuB;AACvB,QAAO;EAEL;GACE,OAAO,CACL,OAAO,UAA0C;AAC/C,QAAI;KACF,MAAM,WAAW,MAAM;AACvB,SAAI,aAAa,WAAW,aAAa,OAAQ,QAAO,EAAE;KAE1D,MAAM,YAAY,MAAM;KAGxB,MAAM,UACJ,aAAa,UACR,WAAW,WAAsB,KACjC,WAAW,cAAyB;AAE3C,SAAI,CAAC,QAAS,QAAO,EAAE;AAEvB,iBAAY;KACZ,MAAM,UAAU,MAAM,eAAe,SAAS,SAAS;AACvD,SAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;KAcnC,MAAM,WAAW,WAAW;KAC5B,MAAM,gBAAgB,0BAA0B,SAAS,GACrD,QACG,KAAK,EAAE,OAAO,eAAe;MAC5B;MACA,SAAS,QAAQ,QACd,MAAM,EAAE,SAAS,aAAa,cAChC;MACF,EAAE,CACF,QAAQ,EAAE,cAAc,QAAQ,SAAS,EAAE,GAC9C;AACJ,SAAI,cAAc,WAAW,GAAG;AAC9B,gBACE,qDAAqD,KAAK,SACxD,YAAY,GACb,CAAC,UAAU,QAAQ,IAAI,QAAQ,IAAI,KAAK,GAC1C;AACD,aAAO,EAAE;;KAGX,MAAM,UAAU,MAAM,cACpB,eACA,UACA,YACD;AACD,SAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;KAEnC,MAAM,QAAQ,qBAAqB,QAAQ;AAC3C,iBAAY,eAAe,UAAU,OAAO,WAAW;AAEvD,YAAO,EACL,oBAAoB;MAClB,eAAe;MACf,mBACE,oBAAoB,MAAM,KAAK,IAC7B,MAAM,SAAS,eAAe,GAC/B;MAEJ,EACF;aACM,OAAO;AACd,eAAU,6CAA6C,MAAM;KAM7D,MAAM,SACJ;AACF,iBAAY,OAAO;AACnB,YAAO,EAAE,YAAY,QAAQ;;KAGlC;GACD,SAAS;GACV;EAGD;GACE,OAAO,CACL,OAAO,UAA0C;AAC/C,QAAI;KACF,MAAM,WAAW,MAAM;AACvB,SAAI,aAAa,UAAU,aAAa,OAAQ,QAAO,EAAE;KAEzD,MAAM,eAAe,MAAM;AAK3B,SAAI,gBAAgB,KAAM,QAAO,EAAE;KACnC,MAAM,UACJ,OAAO,iBAAiB,WACpB,eACA,KAAK,UAAU,aAAa;AAElC,SAAI,CAAC,QAAS,QAAO,EAAE;AAEvB,iBAAY;KAEZ,MAAM,UAAU,YADA,MAAM,cAAc,SAAS,SAAS,YAAY,CAC9B;AACpC,SAAI,CAAC,QAAS,QAAO,EAAE;KAEvB,MAAM,EAAE,UAAU;AAClB,iBAAY,eAAe,UAAU,OAAO,QAAQ,OAAO;AAE3D,SAAI,QAAQ,UAAU;MACpB,MAAM,SACJ,mBAAmB,MAAM,KAAK;AAEhC,kBAAY,OAAO;AACnB,aAAO,EAAE,YAAY,QAAQ;;AAG/B,YAAO,EACL,oBAAoB;MAClB,eAAe;MACf,mBAAmB,kBAAkB,MAAM,KAAK,IAC9C,MAAM,SAAS,eAAe;MAEjC,EACF;aACM,OAAO;AACd,eAAU,4CAA4C,MAAM;KAE5D,MAAM,SACJ;AACF,iBAAY,OAAO;AACnB,YAAO,EAAE,YAAY,QAAQ;;KAGlC;GACD,SAAS;GACV;EAGD;GACE,OAAO,CACL,OAAO,UAA0C;AAC/C,QAAI;AAEF,SADiB,MAAM,cACN,OAAQ,QAAO,EAAE;KAElC,MAAM,YAAY,MAAM;KACxB,MAAM,UACJ,OAAO,WAAW,YAAY,WAAW,UAAU,UAAU;AAG/D,SAAI,CAAC,sBAAsB,QAAQ,CAAE,QAAO,EAAE;KAG9C,MAAM,WAAW,QAAQ,MACvB,sCACD;AACD,SAAI,CAAC,SAAU,QAAO,EAAE;KAExB,MAAM,WAAW,SAAS;KAC1B,MAAM,MAAO,MAAM,OAAkB,QAAQ,KAAK;AAClD,iBAAY;KAGZ,MAAM,UAAU,YAFA,MAAM,eAAe,KAAK,UAAU,YAAY,CAE5B;AACpC,SAAI,CAAC,QAAS,QAAO,EAAE;AAEvB,iBACE,eACA,wBACA,QAAQ,OACR,QAAQ,OACT;AAID,SAAI,CAAC,QAAQ,SAAU,QAAO,EAAE;KAEhC,MAAM,SACJ,8CAA8C,SAAS,IAAI,QAAQ,MAAM,KAAK;AAEhF,iBAAY,OAAO;AACnB,YAAO,EAAE,YAAY,QAAQ;aACtB,OAAO;AACd,eAAU,gDAAgD,MAAM;KAEhE,MAAM,SACJ;AACF,iBAAY,OAAO;AACnB,YAAO,EAAE,YAAY,QAAQ;;KAGlC;GACD,SAAS;GACV;EACF;;;;;;;;;;;;;;;;AAmBH,eAAsB,mBACpB,kBACA,aACwB;AACxB,aAAY;CAEZ,MAAM,UAAU,YADA,MAAM,eAAe,kBAAkB,KAAK,YAAY,CACpC;AACpC,KAAI,CAAC,QAAS,QAAO;AACrB,aACE,iBACA,oBACA,QAAQ,OACR,QAAQ,OACT;AACD,KAAI,CAAC,QAAQ,UAAU;AACrB,YACE,UAAU,QAAQ,MAAM,KAAK,IAC3B,QAAQ,MAAM,SAAS,YAAY,UACpC,OAAO,iBAAiB,oCAC1B;AACD,SAAO;;AAET,QAAO,4BAA4B,QAAQ,MAAM,KAAK,IACpD,QAAQ,MAAM,SAAS,YAAY,UACpC,OAAO;;;;;;;;AASV,eAAe,eACb,KACA,UACA,aACsB;CACtB,MAAM,cAAc,KAAK,QAAQ,KAAK,SAAS;AAE/C,KAAI,CAAC,GAAG,WAAW,YAAY,EAAE;AAC/B,YAAU,0CAA0C,cAAc;AAClE,SAAO,EAAE;;CAGX,MAAM,QAAQ,MAAM,GAAG,8CAA8C;EACnE,KAAK;EACL,UAAU;EACX,CAAC;AAEF,KAAI,MAAM,WAAW,GAAG;AACtB,YAAU,kDAAkD,cAAc;AAC1E,SAAO,EAAE;;AAGX,WACE,mBAAmB,MAAM,OAAO,6BAA6B,WAC9D;CAMD,MAAM,UAA0B,EAAE;AAClC,MAAK,MAAM,YAAY,OAAO;EAC5B,IAAI;AACJ,MAAI;AACF,OAAI,GAAG,SAAS,SAAS,CAAC,OAAO,uBAAuB;AACtD,cACE,0BAA0B,sBAAsB,kCAAkC,WACnF;AACD,cAAU,aAAa,UAAU,sBAAsB;SAEvD,WAAU,GAAG,aAAa,UAAU,QAAQ;WAEvC,KAAK;AACZ,aAAU,oCAAoC,SAAS,IAAI,IAAI;AAC/D;;AAEF,MAAI,QACF,SAAQ,KAAK,GAAI,MAAM,eAAe,SAAS,QAAQ,CAAE;;AAU7D,QAAO,cAAc,SAAS,SAAS,YAAY"}
1
+ {"version":3,"file":"yara-hooks-C3L6lBij.js","names":[],"sources":["../src/shared/skill-install.ts","../src/agent/yara-policy.ts","../src/shared/audit-ledger.ts","../src/agent/yara-hooks.ts"],"sourcesContent":["/**\n * Check if command is a PostHog skill installation from MCP.\n * We control the MCP server, so we only need to verify:\n * 1. It installs to .claude/skills/\n * 2. It downloads from a context-mill release origin (GitHub Releases or the\n * AWS mirror) or localhost (dev)\n *\n * Extracted to its own module to avoid a circular dependency\n * between agent-interface.ts and yara-hooks.ts.\n */\nexport function isSkillInstallCommand(command: string): boolean {\n if (!command.startsWith('mkdir -p .claude/skills/')) return false;\n\n const urlMatch = command.match(/curl -sL ['\"]([^'\"]+)['\"]/);\n if (!urlMatch) return false;\n\n // Literal prefixes rather than the constants in `@lib/constants`: an\n // allow-list is easier to audit when it reads as the URLs themselves.\n const url = urlMatch[1];\n return (\n url.startsWith('https://github.com/PostHog/context-mill/releases/') ||\n url.startsWith('https://context-mill.posthog.com/') ||\n /^http:\\/\\/localhost:\\d+\\//.test(url)\n );\n}\n","/**\n * Shared YARA scan policy: the vocabulary every scan surface speaks, and the\n * one decision they all make — does a set of matches terminate the session, or\n * only warn. Both fences (the anthropic PostToolUse hooks in `yara-hooks.ts`\n * and the pi tool_result extension in `harness/pi/security.ts`) resolve a\n * verdict here rather than each re-deriving it from severity and action.\n */\n\nimport type { ScanMatch } from '@posthog/warlock';\n\n/** Which content surface a warlock rule applies to (from rule `scan_context`). */\nexport type ScanContext = 'command' | 'input' | 'output';\n\n/** What a fence did about a match, as recorded in the scan report. */\nexport type ScanAction = 'blocked' | 'reverted' | 'warned' | 'aborted';\n\n/** Severity order, highest first, for picking the match worth reporting. */\nconst SEVERITY_RANK: Record<string, number> = {\n critical: 4,\n high: 3,\n medium: 2,\n low: 1,\n};\n\n/** Severities that end the session. Critical only — nothing below it is worth killing a run over. */\nconst TERMINAL_SEVERITIES = new Set(['critical']);\n\n/**\n * The match that ends the session rather than just warning. Severity alone\n * decides: a rule's `action: 'block'` is a recommendation about the *operation*\n * (PreToolUse still refuses the command), not a licence to end the run. Medium\n * and high `block` rules fire on first-party content often enough that\n * terminating on them costs more than it prevents.\n */\nexport function isTerminalMatch(match: ScanMatch): boolean {\n return TERMINAL_SEVERITIES.has(match.metadata.severity ?? '');\n}\n\n/** Return the highest-severity match from a list of matches. */\nexport function highestSeverityMatch(matches: ScanMatch[]): ScanMatch {\n return matches.reduce((worst, m) =>\n (SEVERITY_RANK[m.metadata.severity ?? ''] ?? 0) >\n (SEVERITY_RANK[worst.metadata.severity ?? ''] ?? 0)\n ? m\n : worst,\n );\n}\n\n/**\n * The match that refuses a publish, or null to allow. Critical only — every\n * 'output' rule asks for `remediate`, and the handoff has no second channel.\n */\nexport function publishBlockingMatch(matches: ScanMatch[]): ScanMatch | null {\n if (matches.length === 0) return null;\n const match = highestSeverityMatch(matches);\n return isTerminalMatch(match) ? match : null;\n}\n\nexport interface ScanVerdict {\n /** The match worth reporting: the highest-severity one. */\n match: ScanMatch;\n /** Session ends now; anything else is advisory. */\n terminal: boolean;\n /** How the scan report records it. */\n action: ScanAction;\n}\n\n/** Resolve what a fence should do about a set of matches. Null when clean. */\nexport function scanVerdict(matches: ScanMatch[]): ScanVerdict | null {\n if (matches.length === 0) return null;\n const match = highestSeverityMatch(matches);\n const terminal = isTerminalMatch(match);\n return { match, terminal, action: terminal ? 'aborted' : 'warned' };\n}\n","/**\n * The audit ledger's contract: the file the audit tools write into the user's\n * project, the check shape inside it, and the read-side coercion. Shared by\n * the agent's ledger tools (both harnesses), the scanner's documentation\n * allowlist and the audit program that watches the file.\n */\n\nexport type AuditStatus =\n | 'pending'\n | 'pass'\n | 'error'\n | 'warning'\n | 'suggestion';\n\nexport interface AuditCheck {\n id: string;\n area: string;\n label: string;\n status: AuditStatus;\n file?: string;\n details?: string;\n}\n\nexport const AUDIT_CHECKS_FILE = '.posthog-audit-checks.json';\nexport const AUDIT_REPORT_FILE = 'posthog-audit-report.md';\n\n/**\n * Read the audit checks ledger off disk. Validation lives at write time —\n * every writer (`audit_seed_checks` / `audit_add_checks` / `audit_resolve_checks`\n * MCP tools, `seedAuditLedger`) zod-parses entries before the atomic write,\n * so by the time the file watcher fires we trust the shape and only guard\n * against the file not being a JSON array (corrupted / hand-edited / not yet\n * seeded).\n */\nexport function coerceAuditChecks(parsed: unknown): AuditCheck[] {\n return Array.isArray(parsed) ? (parsed as AuditCheck[]) : [];\n}\n","/**\n * YARA hook wiring for the Claude Agent SDK.\n *\n * Creates PreToolUse and PostToolUse hook callback arrays that integrate the\n * real @posthog/warlock security scanner (YARA-X via WASM) into the wizard's\n * agent loop. These hooks are registered in the SDK's query() options alongside\n * the existing Stop hook.\n *\n * PreToolUse hooks block dangerous commands before execution. PostToolUse hooks\n * detect violations in written code and prompt injection in read content, and\n * scan context-mill skill downloads.\n *\n * Warlock owns the rules; this file owns the *policy* — how a match maps to a\n * block / revert / terminate response, plus the optional LLM triage pass that\n * filters false positives before we act.\n *\n * Naming: \"yara\" is the technique — scanning content against YARA rules — so the\n * wizard-side wiring keeps that name. \"warlock\" is the engine package that runs\n * the rules. Both names showing up here is intentional.\n *\n * This is Layer 2 (L2) in the wizard's defense-in-depth model, complementing the\n * prompt-based commandments (L0) and the canUseTool() allowlist (L1).\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { createHash } from 'crypto';\nimport fg from 'fast-glob';\nimport type {\n ScanMatch,\n TriageMatch,\n LLMProvider,\n Category,\n} from '@posthog/warlock';\nimport { logToFile } from '@utils/debug';\nimport { readFileHead } from '@utils/bounded-fs';\nimport { analytics } from '@utils/analytics';\nimport { isSkillInstallCommand } from '@shared/skill-install';\nimport {\n highestSeverityMatch,\n publishBlockingMatch,\n scanVerdict,\n} from './yara-policy';\nimport type { ScanAction, ScanContext } from './yara-policy';\nimport { WIZARD_YARA_REPORT_FILE } from '@utils/paths';\n// TODO(wizard#594): invert this dependency. The document names are shared\n// constants now, so nothing here imports a program, but this file still knows\n// which programs write PII-shaped reports: every new one has to be added.\n// Proper fix is inverted — programs declare their own doc paths and the hooks\n// read a generic registry. Land that before adding a fourth entry.\nimport {\n EVENTS_AUDIT_REPORT_FILE,\n EVENT_INVENTORY_FILE,\n EVENT_INVENTORY_PART_PATTERN,\n EVENT_PLAN_FILE,\n} from '@shared/constants';\nimport { AUDIT_REPORT_FILE } from '@shared/audit-ledger';\n\n// ─── Warlock module accessor ─────────────────────────────────────\n// Warlock is ESM-only and lazily inits its WASM engine + compiles rules on the\n// first scan() call. We import it once and cache the module promise so the\n// warmed engine (a warlock-internal singleton) is shared across every hook.\n// Mirrors the lazy `await import(...)` pattern used for the agent SDK.\n\ntype WarlockModule = typeof import('@posthog/warlock');\n\nlet warlockModulePromise: Promise<WarlockModule> | null = null;\n\nfunction getWarlock(): Promise<WarlockModule> {\n if (!warlockModulePromise) {\n // Clear the cache on rejection so a transient failure (flaky disk, ESM\n // resolver hiccup) doesn't poison the cache for the rest of the process\n // lifetime. Without this, one rejected import permanently locks every\n // hook into fail-closed-terminate until restart.\n warlockModulePromise = import('@posthog/warlock').catch((err) => {\n warlockModulePromise = null;\n throw err;\n });\n }\n return warlockModulePromise;\n}\n\n/**\n * Pay the WASM-init + rule-compile cost up front, off the hook path, so the\n * first real tool-call scan doesn't eat cold-start under a hook timeout.\n * Best-effort: a failure here is non-fatal — hooks still fail closed per scan.\n */\nexport async function prewarmYaraScanner(): Promise<void> {\n try {\n const warlock = await getWarlock();\n await warlock.scan('');\n logToFile('[YARA] warlock pre-warmed');\n } catch (err) {\n logToFile('[YARA] warlock pre-warm failed:', err);\n }\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n// Loose hook types to avoid tight coupling to the SDK version.\n// The SDK hook types are: HookCallbackMatcher[], where each matcher\n// has { matcher?: string, hooks: HookCallback[], timeout?: number }\n\ntype HookInput = Record<string, unknown>;\ntype HookOutput = Record<string, unknown>;\ntype HookCallback = (\n input: HookInput,\n toolUseID: string | undefined,\n options: { signal: AbortSignal },\n) => Promise<HookOutput>;\n\nexport interface HookCallbackMatcher {\n matcher?: string;\n hooks: HookCallback[];\n timeout?: number;\n}\n\nexport type { ScanAction, ScanContext } from './yara-policy';\n\n// ─── Scan Report Accumulator ─────────────────────────────────────\n\ninterface ScanReportEntry {\n rule: string;\n severity: string;\n category: string;\n action: ScanAction;\n phase: string;\n tool: string;\n /** Rule description — the human-readable \"why\". Rule-static, safe to report. */\n description: string;\n /** Triage outcome, when triage ran. */\n triageVerdict?: string;\n /**\n * Free-text triage rationale. May quote scanned content, so it is written to\n * the local report file only — NEVER sent to PostHog (see captureScanReport).\n */\n triageReason?: string;\n}\n\nlet scanCount = 0;\nconst scanViolations: ScanReportEntry[] = [];\n\nfunction recordScan(): void {\n scanCount++;\n}\n\n// recordMatch is the single entry point for a violation (log + telemetry +\n// report). scanViolations is appended only there.\n\n/**\n * Log a match to the run log + PostHog, and record it in the run's scan report.\n * Single entry point so every violation is reported consistently.\n */\nfunction recordMatch(\n phase: string,\n tool: string,\n match: ScanMatch,\n action: ScanAction,\n): void {\n const triage = (match as TriageMatch).triage;\n logYaraMatch(phase, tool, match, action);\n scanViolations.push({\n rule: match.rule,\n severity: match.metadata.severity ?? 'unknown',\n category: match.metadata.category ?? 'unknown',\n action,\n phase,\n tool,\n description: match.metadata.description ?? '',\n triageVerdict: triage?.verdict,\n triageReason: triage?.reason,\n });\n}\n\n/** Reset counters (for testing) */\nexport function resetScanReport(): void {\n scanCount = 0;\n scanViolations.length = 0;\n}\n\n/**\n * Scan bookkeeping for harnesses that run a scanner outside these hooks (pi):\n * counts the scan and routes violations through `recordMatch`, so they reach\n * the run log, per-match telemetry, and the end-of-run scan report.\n */\nexport function recordExternalScan(\n phase: string,\n tool: string,\n violations: Array<{\n rule: string;\n severity: string;\n category: string;\n description: string;\n }>,\n action: ScanAction,\n): void {\n recordScan();\n for (const v of violations) {\n recordMatch(\n phase,\n tool,\n {\n rule: v.rule,\n metadata: {\n severity: v.severity as ScanMatch['metadata']['severity'],\n category: v.category as ScanMatch['metadata']['category'],\n description: v.description,\n },\n matchedStrings: [],\n },\n action,\n );\n }\n}\n\n/** Format the scan report summary. Returns null if no scans occurred */\nexport function formatScanReport(): string | null {\n if (scanCount === 0) return null;\n\n const lines: string[] = ['', '— YARA Scanner Summary —'];\n const violationCount = scanViolations.length;\n const cleanCount = scanCount - violationCount;\n\n lines.push(\n `✓ ${scanCount} tool calls scanned, ${violationCount} violation${\n violationCount !== 1 ? 's' : ''\n } detected`,\n );\n\n if (violationCount > 0) {\n lines.push('');\n for (const v of scanViolations) {\n const tag = v.action.toUpperCase();\n lines.push(\n ` [${tag}] ${v.rule} (${v.severity.toUpperCase()}) — ${v.phase}:${\n v.tool\n }`,\n );\n }\n }\n\n if (cleanCount > 0) {\n lines.push('');\n lines.push(\n `No violations: ✓ ${cleanCount} clean scan${cleanCount !== 1 ? 's' : ''}`,\n );\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\n/** Write the scan report to a JSON file. Returns the file path, or null if no scans occurred. */\nexport function writeScanReport(): string | null {\n if (scanCount === 0) return null;\n\n const report = {\n summary: {\n totalScans: scanCount,\n violations: scanViolations.length,\n clean: scanCount - scanViolations.length,\n },\n violations: scanViolations,\n };\n\n try {\n fs.writeFileSync(WIZARD_YARA_REPORT_FILE, JSON.stringify(report, null, 2));\n } catch (err) {\n logToFile('[YARA] Failed to write scan report:', err);\n return null;\n }\n return WIZARD_YARA_REPORT_FILE;\n}\n\n// ─── User-facing abort copy ──────────────────────────────────────\n//\n// The abort screen is the one security surface a user actually sees, so it must\n// say what was caught and that we protected them, in plain language, instead of\n// a bare \"Security violation detected.\" Warlock owns the rules and categories;\n// this map is the wizard's *presentation* of a category it already detected. We\n// deliberately do not surface warlock's own rule `description` here: it is\n// written for maintainers (it ships to telemetry) and reads as technical. The\n// `Category` type is a stable, append-only warlock API, so a new category just\n// falls through to the generic line below rather than breaking the build.\n\nconst CATEGORY_DESCRIPTIONS: Record<Category, string> = {\n prompt_injection:\n 'hit hidden instructions in a file it read, a possible attempt to hijack the setup',\n exfiltration: 'tried to send data to an outside location',\n destructive_operations: 'tried to run a destructive command',\n supply_chain: 'tried to pull in untrusted code',\n posthog_pii: 'tried to write personal data (PII) into your project',\n posthog_hardcoded_key: 'tried to hardcode a PostHog key into your project',\n hardcoded_secret: 'tried to hardcode a secret into your project',\n};\n\n/** Most recent violation that terminated the run, or null. */\nfunction lastTerminatingViolation(): ScanReportEntry | null {\n for (let i = scanViolations.length - 1; i >= 0; i--) {\n if (scanViolations[i].action === 'aborted') return scanViolations[i];\n }\n return null;\n}\n\n/**\n * Build the user-facing copy for the security abort screen: what the scanner\n * caught and what we did about it. This screen renders only because the run was\n * stopped, so the \"what we did\" half is constant. Falls back to a generic line\n * when no specific violation was recorded (e.g. the scanner itself errored and\n * failed closed before a match was logged).\n */\nexport function formatYaraAbortMessage(): string {\n const v = lastTerminatingViolation();\n const what =\n (v && CATEGORY_DESCRIPTIONS[v.category as Category]) ??\n 'did something our security scanner flagged';\n\n return (\n 'Security check stopped the setup.\\n\\n' +\n `The agent ${what}. We stopped the run to keep your project safe.\\n` +\n 'No unsafe changes were left in your project.\\n\\n' +\n 'If this looks wrong, let us know: wizard@posthog.com'\n );\n}\n\n// ─── Hook Timeouts (ms) ─────────────────────────────────────────\n// Generous because triage adds an LLM round-trip, and because a *fired* hook\n// timeout means the protective action is dropped (fail-open). The triage HTTP\n// call has its own shorter internal timeout (see agent-interface), so a hung\n// triage fails inside our catch (fail-closed) before these fire.\n\n/** Timeout for scan hooks (PreToolUse, PostToolUse Write/Edit/Read/Grep) */\nconst HOOK_TIMEOUT_MS = 30_000;\n/** Timeout for the skill install hook (filesystem I/O + multiple scans) */\nconst SKILL_SCAN_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Chunk size for scanning (100 KB). Oversized content is scanned in\n * overlapping chunks so coverage is complete — nothing is silently truncated.\n * The size also bounds what a single triage call pastes into the LLM prompt\n * (~25K tokens), so triage always sees the chunk its matches came from.\n */\nconst SCAN_CHUNK_SIZE = 100_000;\n\n// A skill file is read at most this far; the rest is head-scanned and logged.\nconst SKILL_FILE_SCAN_BYTES = 10 * 1024 * 1024;\n/**\n * Overlap between adjacent chunks so a pattern straddling a chunk boundary\n * still lands whole inside at least one chunk. YARA rule strings are at most\n * a few hundred bytes; 4 KB is generous.\n */\nconst SCAN_CHUNK_OVERLAP = 4_096;\n\n// ─── Logging ─────────────────────────────────────────────────────\n\nfunction logYaraMatch(\n phase: string,\n tool: string,\n match: ScanMatch,\n action: ScanAction,\n): void {\n const severity = match.metadata.severity ?? 'unknown';\n const category = match.metadata.category ?? 'unknown';\n const ruleAction = match.metadata.action ?? 'unknown';\n const description = match.metadata.description ?? '';\n logToFile(\n `[YARA] ${phase}:${tool} [${action.toUpperCase()}] rule \"${match.rule}\" ` +\n `(severity: ${severity}, category: ${category}, action: ${ruleAction})\\n` +\n ` Description: ${description}`,\n );\n analytics.wizardCapture('yara rule matched', {\n rule: match.rule,\n severity: match.metadata.severity,\n category: match.metadata.category,\n rule_action: match.metadata.action,\n action,\n phase,\n tool,\n // Rule-static description — the \"why\". The free-text triage reason is\n // deliberately omitted (it can quote scanned content; stays local-only).\n description: match.metadata.description,\n triage_verdict: (match as TriageMatch).triage?.verdict,\n });\n}\n\n/**\n * Send the run's full scan report to PostHog so maintainers can see why a run\n * was flagged, warned, or aborted. This is the maintainer-facing report — it is\n * NEVER shown to the user. No-op if nothing was scanned. The free-text triage\n * reason is omitted to avoid sending scanned content off the user's machine.\n */\nexport function captureScanReport(): void {\n if (scanCount === 0) return;\n analytics.wizardCapture('yara scan report', {\n total_scans: scanCount,\n violation_count: scanViolations.length,\n clean_count: scanCount - scanViolations.length,\n violations: scanViolations.map((v) => ({\n rule: v.rule,\n severity: v.severity,\n category: v.category,\n action: v.action,\n phase: v.phase,\n tool: v.tool,\n description: v.description,\n triage_verdict: v.triageVerdict,\n })),\n });\n // Reset after sending so a second captureScanReport on this same run\n // (e.g. success-path call followed by an abort during shutdown) can't\n // ship duplicate telemetry. Also protects future chained-program runs.\n scanCount = 0;\n scanViolations.length = 0;\n}\n\n/**\n * End-of-run flush for the scan report. Wired once at the runner seam so every\n * harness (linear, orchestrator, and any future shape) reports without having\n * to know warlock exists — scanning is already harness-agnostic (it runs from\n * SDK Pre/PostToolUse hooks), and this keeps reporting at a single shared seam.\n *\n * Order matters: write the local --yara-report file FIRST (it reads scanCount /\n * scanViolations), THEN send telemetry. captureScanReport() zeroes scan state,\n * which is what makes this whole function idempotent — a second call from\n * another termination path (e.g. finally after an abort already flushed) finds\n * scanCount === 0 and every step no-ops.\n *\n * Returns the user-facing report line when a report was written; the caller\n * decides where it goes (the runner emits it as progress).\n */\nexport function flushScanReport(options: {\n yaraReport: boolean;\n}): string | undefined {\n let line: string | undefined;\n if (options.yaraReport) {\n const reportPath = writeScanReport();\n if (reportPath) {\n const summary = formatScanReport();\n line = `YARA scan report: ${reportPath}${summary ?? ''}`;\n }\n }\n captureScanReport();\n return line;\n}\n\n// ─── Wizard-documentation allowlist ───────────────────────────────\n//\n// Files the wizard's own programs write to describe events the user's\n// codebase already captures, or events the integration program is\n// proposing to add. When the agent copies a literal\n// `posthog.capture('event', { email: ... })` snippet (or a property\n// list including PII-shaped keys) into one of these files, the\n// `pii_in_capture_call` rule (category: posthog_pii) fires even though\n// the wizard is documenting / planning, not introducing, the pattern.\n// Suppress posthog_pii matches on these paths only; every other rule\n// (secrets, prompt injection, supply chain, destructive ops) still\n// fires normally so the file cannot be used as a smuggling vector for\n// actual violations.\n\nconst WIZARD_DOC_BASENAMES = new Set([\n EVENT_INVENTORY_FILE,\n EVENTS_AUDIT_REPORT_FILE,\n AUDIT_REPORT_FILE,\n EVENT_PLAN_FILE,\n]);\n\nconst WIZARD_DOC_PATTERNS: RegExp[] = [EVENT_INVENTORY_PART_PATTERN];\n\nexport function isWizardDocumentationPath(\n filePath: string | undefined,\n): boolean {\n if (!filePath) return false;\n const basename = path.basename(filePath);\n if (WIZARD_DOC_BASENAMES.has(basename)) return true;\n return WIZARD_DOC_PATTERNS.some((re) => re.test(basename));\n}\n\n// ─── Repeat-block tracker ─────────────────────────────────────────\n//\n// Field data (switchboard YARA tile): an agent whose edit gets blocked often\n// retries the identical payload — the scan is deterministic, so every retry\n// blocks again and the run burns turns (~4.2 blocks per affected run). The\n// runtime notes already say \"don't retry\"; the data shows the prompt alone\n// doesn't hold. This is the code-side guard: remember the exact payloads we\n// blocked, and when the same content comes back, escalate the block reason —\n// first to \"change the code, not the retry\", then to \"report it and move on\".\n// Used by both fences: the anthropic PreToolUse Bash hook below, and the pi\n// security extension's pre-execution scan (bash/write/edit).\n\n/** Identical-payload attempt at which the agent is told to move on. */\nconst REPEAT_BLOCK_MOVE_ON_ATTEMPT = 3;\n\nexport interface RepeatBlockTracker {\n /**\n * Record a YARA block of `content` on `tool`; returns the attempt count\n * for that exact payload (1 = first time blocked).\n */\n attempt(tool: string, content: string): number;\n}\n\n/** Per-run tracker of YARA-blocked payloads (keyed by tool + content hash). */\nexport function createRepeatBlockTracker(): RepeatBlockTracker {\n const counts = new Map<string, number>();\n return {\n attempt(tool: string, content: string): number {\n const key = `${tool} ${createHash('sha256')\n .update(content)\n .digest('hex')}`;\n const next = (counts.get(key) ?? 0) + 1;\n counts.set(key, next);\n return next;\n },\n };\n}\n\n/**\n * The block reason for the given attempt: the plain reason the first time,\n * an explicit \"this exact retry can never work\" on the second, and a\n * \"report it and move on\" instruction from the third on.\n */\nexport function repeatBlockReason(\n attempt: number,\n tool: string,\n baseReason: string,\n): string {\n if (attempt <= 1) return baseReason;\n if (attempt < REPEAT_BLOCK_MOVE_ON_ATTEMPT) {\n return (\n `${baseReason} This exact ${tool} content was ALREADY blocked — the scan is ` +\n 'deterministic, so retrying identical content will always block again. ' +\n 'Change the code to remove the flagged pattern instead of retrying.'\n );\n }\n return (\n `${baseReason} This exact ${tool} content has now been blocked ${attempt} times. ` +\n 'STOP retrying it. Note the blocked change in the setup report and move on to the next task.'\n );\n}\n\n// ─── Severity helpers ────────────────────────────────────────────\n// Severity vocabulary and the terminate-or-warn decision live in yara-policy.\n\n// ─── Scan + triage core ──────────────────────────────────────────\n\n/**\n * Keep only matches whose rule targets this content surface. Rules carry a\n * `scan_context` of 'command' | 'input' | 'output'. An undefined context is\n * treated as \"applies everywhere\" (fail-safe — a rule that forgets the tag\n * still gets enforced rather than silently skipped).\n */\nfunction matchesForContext(\n matches: ScanMatch[],\n ctx: ScanContext,\n): ScanMatch[] {\n return matches.filter((m) => {\n const c = m.metadata.scan_context as string | undefined;\n return c === ctx || c === undefined;\n });\n}\n\n/**\n * Drop false positives via warlock's LLM triage. Fail-closed: if no provider is\n * available, or the triage call throws, every match is treated as real — we\n * never silently suppress a flagged match.\n *\n * Every overruled match is reported to PostHog (rule metadata only, never the\n * free-text reason) so maintainers can alert on triage-overrule patterns — the\n * signal that someone is either tripping a noisy rule or trying to talk the\n * triage model out of a real finding.\n */\nasync function triageFilter(\n content: string,\n matches: ScanMatch[],\n ctx: ScanContext,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n if (matches.length === 0) return [];\n if (!llmProvider) {\n logToFile(\n `[YARA] triage skipped (no provider) — treating ${matches.length} match(es) as real`,\n );\n return matches;\n }\n try {\n const warlock = await getWarlock();\n const triaged = await warlock.triageMatches(content, matches, llmProvider);\n const kept = triaged.filter((m) => m.triage.verdict === 'true_positive');\n for (const m of triaged) {\n if (m.triage.verdict === 'true_positive') continue;\n logToFile(\n `[YARA] triage overruled rule \"${m.rule}\" (${\n m.metadata.severity ?? 'unknown'\n }) — not acting on it`,\n );\n analytics.wizardCapture('yara triage overruled', {\n rule: m.rule,\n severity: m.metadata.severity,\n category: m.metadata.category,\n scan_context: ctx,\n // The free-text triage reason is deliberately omitted — it can quote\n // scanned content, which must never leave the user's machine.\n });\n }\n logToFile(\n `[YARA] triage: ${matches.length} flagged → ${kept.length} kept as real`,\n );\n return kept;\n } catch (err) {\n logToFile('[YARA] triage failed — treating all matches as real:', err);\n return matches;\n }\n}\n\n/** A chunk of scanned content together with the matches found inside it. */\ninterface FlaggedChunk {\n chunk: string;\n matches: ScanMatch[];\n}\n\n/**\n * Split oversized content into overlapping chunks so the scanner covers all\n * of it. Content that fits in one chunk is returned as-is.\n */\nfunction chunkContent(content: string): string[] {\n if (content.length <= SCAN_CHUNK_SIZE) return [content];\n const chunks: string[] = [];\n const step = SCAN_CHUNK_SIZE - SCAN_CHUNK_OVERLAP;\n for (let start = 0; start < content.length; start += step) {\n chunks.push(content.slice(start, start + SCAN_CHUNK_SIZE));\n if (start + SCAN_CHUNK_SIZE >= content.length) break;\n }\n return chunks;\n}\n\n/**\n * Scan content against warlock rules — chunked when oversized, so nothing is\n * skipped — and keep only matches for this content surface. Chunks scan\n * sequentially (the WASM engine is single-threaded; parallelism buys nothing).\n * Returns each flagged chunk with its matches so triage can later judge every\n * match against the exact content it came from.\n */\nasync function scanForContext(\n content: string,\n ctx: ScanContext,\n): Promise<FlaggedChunk[]> {\n const warlock = await getWarlock();\n const chunks = chunkContent(content);\n if (chunks.length > 1) {\n logToFile(\n `[YARA] content is ${content.length} chars — scanning ${chunks.length} overlapping chunks`,\n );\n // Alertable signal: oversized content should be rare; a spike could mean\n // someone is padding content to probe the scanner.\n analytics.wizardCapture('yara scan chunked', {\n content_length: content.length,\n chunk_count: chunks.length,\n scan_context: ctx,\n });\n }\n const flagged: FlaggedChunk[] = [];\n for (const chunk of chunks) {\n const result = await warlock.scan(chunk);\n if (!result.matched) continue;\n const matches = matchesForContext(result.matches, ctx);\n if (matches.length === 0) continue;\n flagged.push({ chunk, matches });\n }\n return flagged;\n}\n\n/** The overlap between chunks can surface the same rule twice; count it once. */\nfunction dedupeByRule(matches: ScanMatch[]): ScanMatch[] {\n const seen = new Set<string>();\n return matches.filter((m) => {\n if (seen.has(m.rule)) return false;\n seen.add(m.rule);\n return true;\n });\n}\n\n/**\n * Triage each flagged chunk against its own content — the evidence is always\n * inside the window the LLM sees. Triage calls run in parallel (each is just\n * an HTTP round trip) so N flagged chunks cost ~1× triage latency, not N×.\n */\nasync function triageFlagged(\n flagged: FlaggedChunk[],\n ctx: ScanContext,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n const triaged = await Promise.all(\n flagged.map(({ chunk, matches }) =>\n triageFilter(chunk, matches, ctx, llmProvider),\n ),\n );\n return dedupeByRule(triaged.flat());\n}\n\n/**\n * Scan content, filter to the relevant context, triage. Returns real matches.\n * Exported for harnesses that run their own hook loop (pi) so every harness\n * scans through the same warlock engine, chunking, and triage policy.\n */\nexport async function scanAndTriage(\n content: string,\n ctx: ScanContext,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n const flagged = await scanForContext(content, ctx);\n return triageFlagged(flagged, ctx, llmProvider);\n}\n\n// ─── PreToolUse Hooks ────────────────────────────────────────────\n\n/**\n * Create PreToolUse hook matchers for YARA scanning.\n * 1. Bash — scans commands before execution for exfiltration, destructive\n * operations, and supply chain violations ('command'-context rules).\n * 2. publish_handoff — scans the report content with the same 'output'-context\n * rules + triage as the PostToolUse Write/Edit hook, but BEFORE the MCP\n * tool runs: the report is agent-authored and is persisted to a\n * host-designated file (POSTHOG_HANDOFF_OUTPUT_PATH) as well as pushed to\n * the task stream. Only a CRITICAL match refuses the publish; below that\n * the report goes out with the match recorded.\n */\nexport function createPreToolUseYaraHooks(\n // Used only by the publish_handoff matcher below (output-context triage,\n // same fail-closed semantics as Write/Edit). The Bash matcher still passes\n // undefined — any flagged command blocks regardless of triage verdict, so\n // the LLM call would be wasted.\n llmProvider?: LLMProvider,\n // Ends the run when a handoff scan errors — without it that path just blocks,\n // which is the old behavior, so it stays optional.\n onTerminate?: (reason: string) => void,\n): HookCallbackMatcher[] {\n const repeatTracker = createRepeatBlockTracker();\n return [\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Bash') return {};\n\n const toolInput = input.tool_input as Record<string, unknown>;\n const command =\n typeof toolInput?.command === 'string' ? toolInput.command : '';\n\n if (!command) return {};\n\n recordScan();\n // Skip triage on PreToolUse Bash: any flagged command is blocked\n // regardless of triage verdict, so the LLM call would be wasted.\n const matches = await scanAndTriage(command, 'command', undefined);\n if (matches.length === 0) return {};\n\n const match = highestSeverityMatch(matches);\n recordMatch('PreToolUse', 'Bash', match, 'blocked');\n\n const attempt = repeatTracker.attempt('Bash', command);\n return {\n decision: 'block',\n reason: repeatBlockReason(\n attempt,\n 'Bash',\n `[YARA] ${match.rule}: ${\n match.metadata.description ?? 'security policy violation'\n }. Command blocked for security.`,\n ),\n };\n } catch (error) {\n logToFile('[YARA] PreToolUse hook error:', error);\n // Fail closed: block the command if scanning fails\n return {\n decision: 'block',\n reason: '[YARA] Scanner error — command blocked as a precaution.',\n };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n {\n // ── publish_handoff content scanning ──\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n // The wizard's publish_handoff MCP tool, fully qualified by the\n // SDK (e.g. mcp__wizard-tools__publish_handoff). Matched by\n // suffix so a server rename can't silently reopen the gap — the\n // exact FQN lives in WIZARD_TOOL_NAMES (wizard-tools/tools.ts),\n // which this module can't import without a cycle (tools.ts\n // imports scanInstalledSkill from here).\n if (\n typeof toolName !== 'string' ||\n !toolName.endsWith('__publish_handoff')\n ) {\n return {};\n }\n\n const toolInput = input.tool_input as Record<string, unknown>;\n const content =\n typeof toolInput?.content === 'string' ? toolInput.content : '';\n\n if (!content) return {};\n\n recordScan();\n // Same 'output'-context scan + triage as the Write/Edit hook:\n // the report is agent-authored, quotes the user's code, and now\n // reaches a second persisted consumer (the host file + the cloud\n // worker's PR body) — so it must clear the bar of any other\n // agent-driven write BEFORE the tool executes.\n const matches = await scanAndTriage(content, 'output', llmProvider);\n if (matches.length === 0) return {};\n\n // Below critical: record it, don't cost the run its only report.\n const match = publishBlockingMatch(matches);\n if (!match) {\n const worst = highestSeverityMatch(matches);\n recordMatch('PreToolUse', 'publish_handoff', worst, 'warned');\n logToFile(\n `[YARA] publish_handoff allowed with a ${\n worst.metadata.severity ?? 'unknown'\n } match (${worst.rule}) — below the critical publish bar`,\n );\n return {};\n }\n\n recordMatch('PreToolUse', 'publish_handoff', match, 'blocked');\n\n const attempt = repeatTracker.attempt('publish_handoff', content);\n return {\n decision: 'block',\n reason: repeatBlockReason(\n attempt,\n 'publish_handoff',\n `[YARA] ${match.rule}: ${\n match.metadata.description ?? 'security policy violation'\n }. Handoff blocked for security — remove the flagged content from the report.`,\n ),\n };\n } catch (error) {\n logToFile('[YARA] PreToolUse publish_handoff hook error:', error);\n // Fail closed, but end the run: a silent block would just leave\n // the agent rewording a report that can never publish.\n const reason =\n '[YARA] Scanner error while scanning the handoff report — session terminated as a precaution.';\n onTerminate?.(reason);\n return { decision: 'block', reason };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n ];\n}\n\n// ─── PostToolUse Hooks ───────────────────────────────────────────\n\n/**\n * Create PostToolUse hook matchers for YARA scanning.\n *\n * Three matchers:\n * 1. Write/Edit — scan written content ('output'-context rules: PII, secrets)\n * 2. Read/Grep — scan read content ('input'-context rules: prompt injection)\n * 3. Bash (skill install) — scan downloaded skill files for poisoned content\n *\n * `onTerminate` is invoked on the terminal paths (critical prompt injection in\n * read content, a poisoned skill, or a scanner error under fail-closed). It is\n * what ACTUALLY stops the run — returning `stopReason` from a PostToolUse hook\n * is not honored by the SDK, so the caller wires this to the run's\n * AbortController (see agent-interface).\n */\nexport function createPostToolUseYaraHooks(\n llmProvider: LLMProvider | undefined,\n // REQUIRED, not optional: the SDK ignores `stopReason` from PostToolUse\n // hooks, so terminal paths (critical prompt injection, poisoned skill,\n // fail-closed scanner error) depend entirely on this callback firing.\n // Making it required at compile time prevents a future caller from\n // silently regressing every terminal path to a no-op.\n onTerminate: (reason: string) => void,\n): HookCallbackMatcher[] {\n return [\n // ── Write/Edit content scanning ──\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Write' && toolName !== 'Edit') return {};\n\n const toolInput = input.tool_input as Record<string, unknown>;\n // For Write, scan the content being written.\n // For Edit, scan the new_string (replacement text).\n const content =\n toolName === 'Write'\n ? (toolInput?.content as string) ?? ''\n : (toolInput?.new_string as string) ?? '';\n\n if (!content) return {};\n\n recordScan();\n const flagged = await scanForContext(content, 'output');\n if (flagged.length === 0) return {};\n\n // Wizard-documentation paths: suppress posthog_pii matches that\n // come from the agent verbatim-copying the user's existing\n // capture calls into an inventory / report, or planning new\n // events with PII-shaped property keys. Every other category\n // still triggers the revert. Suppression runs BEFORE triage so\n // we never pay an LLM round trip for a match we're about to\n // discard anyway.\n // TODO(warlock#33): once warlock PR #33 lands with its\n // more-precise PII rules, the wizard-side suppression below\n // should become unnecessary — remove `WIZARD_DOC_BASENAMES`,\n // `WIZARD_DOC_PATTERNS`, and `isWizardDocumentationPath` and\n // verify the noisy-PII issue stays fixed.\n const filePath = toolInput?.file_path as string | undefined;\n const activeFlagged = isWizardDocumentationPath(filePath)\n ? flagged\n .map(({ chunk, matches }) => ({\n chunk,\n matches: matches.filter(\n (m) => m.metadata.category !== 'posthog_pii',\n ),\n }))\n .filter(({ matches }) => matches.length > 0)\n : flagged;\n if (activeFlagged.length === 0) {\n logToFile(\n `[YARA] posthog_pii match suppressed on wizard doc ${path.basename(\n filePath ?? '',\n )} (rule: ${flagged[0]?.matches[0]?.rule})`,\n );\n return {};\n }\n\n const matches = await triageFlagged(\n activeFlagged,\n 'output',\n llmProvider,\n );\n if (matches.length === 0) return {};\n\n const match = highestSeverityMatch(matches);\n recordMatch('PostToolUse', toolName, match, 'reverted');\n\n return {\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext:\n `[YARA VIOLATION] ${match.rule}: ${\n match.metadata.description ?? ''\n }. ` +\n `You MUST revert this change immediately. The content you just wrote violates security policy.`,\n },\n };\n } catch (error) {\n logToFile('[YARA] PostToolUse Write/Edit hook error:', error);\n // Fail closed: if scanning is broken on a Write/Edit, the next\n // Read/skill-install scan is also going to be broken — terminate\n // cleanly instead of looping the agent through \"please revert\"\n // until a Read finally aborts. Matches Read/Grep + skill-install\n // catch behavior; consistent fail-closed across all PostToolUse.\n const reason =\n '[YARA] Scanner error while scanning written content — session terminated as a precaution.';\n onTerminate(reason);\n return { stopReason: reason };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n\n // ── Read/Grep prompt injection scanning ──\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Read' && toolName !== 'Grep') return {};\n\n const toolResponse = input.tool_response;\n // Guard before stringify: `JSON.stringify(undefined ?? '')`\n // returns the 2-char string '\"\"', which is truthy and slips\n // past the empty-content check below. Exit early when the SDK\n // gave us no response payload.\n if (toolResponse == null) return {};\n const content =\n typeof toolResponse === 'string'\n ? toolResponse\n : JSON.stringify(toolResponse);\n\n if (!content) return {};\n\n recordScan();\n const matches = await scanAndTriage(content, 'input', llmProvider);\n const verdict = scanVerdict(matches);\n if (!verdict) return {};\n\n const { match } = verdict;\n recordMatch('PostToolUse', toolName, match, verdict.action);\n\n if (verdict.terminal) {\n const reason =\n `[YARA CRITICAL] ${match.rule}: Prompt injection detected in file content. ` +\n `Agent context is potentially poisoned. Session terminated for safety.`;\n onTerminate(reason);\n return { stopReason: reason };\n }\n\n return {\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext: `[YARA WARNING] ${match.rule}: ${\n match.metadata.description ?? ''\n }`,\n },\n };\n } catch (error) {\n logToFile('[YARA] PostToolUse Read/Grep hook error:', error);\n // Fail closed: terminate session if scanning fails on read content\n const reason =\n '[YARA] Scanner error while scanning read content — session terminated as a precaution.';\n onTerminate(reason);\n return { stopReason: reason };\n }\n },\n ],\n timeout: HOOK_TIMEOUT_MS,\n },\n\n // ── Context-mill skill install scanning ──\n {\n hooks: [\n async (input: HookInput): Promise<HookOutput> => {\n try {\n const toolName = input.tool_name as string;\n if (toolName !== 'Bash') return {};\n\n const toolInput = input.tool_input as Record<string, unknown>;\n const command =\n typeof toolInput?.command === 'string' ? toolInput.command : '';\n\n // Only scan after skill install commands\n if (!isSkillInstallCommand(command)) return {};\n\n // Extract skill directory from command\n const dirMatch = command.match(\n /mkdir -p (.claude\\/skills\\/[^\\s&]+)/,\n );\n if (!dirMatch) return {};\n\n const skillDir = dirMatch[1];\n const cwd = (input.cwd as string) ?? process.cwd();\n recordScan();\n const matches = await scanSkillFiles(cwd, skillDir, llmProvider);\n\n const verdict = scanVerdict(matches);\n if (!verdict) return {};\n\n recordMatch(\n 'PostToolUse',\n 'Bash (skill install)',\n verdict.match,\n verdict.action,\n );\n // Same verdict as every other surface: critical terminates, the\n // rest warn. These rules fire on first-party skill prose often\n // enough that terminating costs more than it prevents.\n if (!verdict.terminal) return {};\n\n const reason =\n `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${verdict.match.rule}. ` +\n `The downloaded skill contains potential prompt injection. Session terminated for safety.`;\n onTerminate(reason);\n return { stopReason: reason };\n } catch (error) {\n logToFile('[YARA] PostToolUse skill install hook error:', error);\n // Fail closed: terminate if skill scanning fails\n const reason =\n '[YARA] Scanner error while scanning skill files — session terminated as a precaution.';\n onTerminate(reason);\n return { stopReason: reason };\n }\n },\n ],\n timeout: SKILL_SCAN_HOOK_TIMEOUT_MS,\n },\n ];\n}\n\n// ─── Skill File Scanner ──────────────────────────────────────────\n\n/**\n * Scan a freshly installed skill directory (any root — .claude/skills or the\n * orchestrator's run cache) and return a terminate reason when it is poisoned,\n * else null. The choke point for TS-path installs (downloadSkill); agent Bash\n * installs are covered by the PostToolUse matcher above. Runs the same LLM\n * triage as the tool-use scans; fail-closed to treating every match as real when\n * no provider is configured, so a missing key never weakens the check.\n * `llmProvider` is explicit — pi and the orchestrator never set the env fallback.\n *\n * Terminates on the same verdict as every other surface (critical only). A\n * non-terminal match is recorded and the skill is kept: these rules fire on\n * first-party skill prose, and deleting the skill leaves the agent working blind\n * on the very content it needed.\n */\nexport async function scanInstalledSkill(\n absoluteSkillDir: string,\n llmProvider: LLMProvider | undefined,\n): Promise<string | null> {\n recordScan();\n const matches = await scanSkillFiles(absoluteSkillDir, '.', llmProvider);\n const verdict = scanVerdict(matches);\n if (!verdict) return null;\n recordMatch(\n 'skill-install',\n 'installSkillById',\n verdict.match,\n verdict.action,\n );\n if (!verdict.terminal) {\n logToFile(\n `[YARA] ${verdict.match.rule} (${\n verdict.match.metadata.severity ?? 'unknown'\n }) in ${absoluteSkillDir} — non-terminal, keeping the skill`,\n );\n return null;\n }\n return `Poisoned skill detected: ${verdict.match.rule} (${\n verdict.match.metadata.severity ?? 'unknown'\n }) in ${absoluteSkillDir}`;\n}\n\n/**\n * Read and scan all text files in a skill directory for prompt injection.\n * Scans each file sequentially (single-threaded WASM — parallelism buys\n * nothing), unions the 'input'-context matches, then triages once across the\n * combined content. Returns the real (post-triage) matches.\n */\nasync function scanSkillFiles(\n cwd: string,\n skillDir: string,\n llmProvider: LLMProvider | undefined,\n): Promise<ScanMatch[]> {\n const absoluteDir = path.resolve(cwd, skillDir);\n\n if (!fs.existsSync(absoluteDir)) {\n logToFile(`[YARA] Skill directory does not exist: ${absoluteDir}`);\n return [];\n }\n\n const files = await fg('**/*.{md,txt,yaml,yml,json,js,ts,py,rb,sh}', {\n cwd: absoluteDir,\n absolute: true,\n });\n\n if (files.length === 0) {\n logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);\n return [];\n }\n\n logToFile(\n `[YARA] Scanning ${files.length} files in skill directory: ${skillDir}`,\n );\n\n // Pass 1 (sequential): read and scan ONE file at a time through the\n // chunk-aware path. Oversized files are scanned up to the cap and logged —\n // never silently skipped. Flagged chunks keep a reference to the exact\n // content their matches came from.\n const flagged: FlaggedChunk[] = [];\n for (const filePath of files) {\n let content: string | null;\n try {\n if (fs.statSync(filePath).size > SKILL_FILE_SCAN_BYTES) {\n logToFile(\n `[YARA] Skill file over ${SKILL_FILE_SCAN_BYTES} bytes; scanning its head only: ${filePath}`,\n );\n content = readFileHead(filePath, SKILL_FILE_SCAN_BYTES);\n } else {\n content = fs.readFileSync(filePath, 'utf-8');\n }\n } catch (err) {\n logToFile(`[YARA] Could not read skill file ${filePath}:`, err);\n continue;\n }\n if (content) {\n flagged.push(...(await scanForContext(content, 'input')));\n }\n }\n\n // Pass 2 (parallel, inside triageFlagged): triage each flagged chunk\n // against its own content concurrently. Triage is just an axios call —\n // running them in parallel cuts total wall-clock from N × triage-timeout\n // to ~1×. Important because the hook's HOOK_TIMEOUT_MS is 30s; serial\n // triage on 2+ matched files could blow the hook timeout under a slow\n // Haiku.\n return triageFlagged(flagged, 'input', llmProvider);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { l as prewarmYaraScanner } from "./yara-hooks-C3L6lBij.js";
2
+ export { prewarmYaraScanner };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/wizard",
3
- "version": "2.79.1",
3
+ "version": "2.80.0",
4
4
  "homepage": "https://github.com/PostHog/wizard",
5
5
  "repository": "https://github.com/PostHog/wizard",
6
6
  "description": "The PostHog wizard helps you to configure your project",
@@ -45,7 +45,7 @@
45
45
  "fflate": "^0.8.3",
46
46
  "fuse.js": "^7.5.0",
47
47
  "glob": "9.3.5",
48
- "ink": "^6.8.0",
48
+ "ink": "^7.1.1",
49
49
  "inquirer": "^6.2.0",
50
50
  "jiti": "^2.7.0",
51
51
  "jsonc-parser": "^3.3.1",
@@ -150,7 +150,6 @@
150
150
  "test:tui": "vitest run --project tui",
151
151
  "test:cli": "vitest run --project cli",
152
152
  "test:harness": "vitest run --project harness",
153
- "test:arch": "vitest run --project architecture",
154
153
  "test:goldens:update": "vitest run -u",
155
154
  "test:coverage": "pnpm build && vitest run --coverage",
156
155
  "test:e2e": "pnpm build && ./e2e-tests/run.sh",
@@ -1 +0,0 @@
1
- {"version":3,"file":"PickerMenu-dG5DQCZA.js","names":[],"sources":["../src/ui/tui/primitives/picker-filter.ts","../src/ui/tui/primitives/PromptLabel.tsx","../src/ui/tui/primitives/ConfirmButton.tsx","../src/ui/tui/hooks/useStdoutDimensions.ts","../src/ui/tui/hooks/keyboard-hints-utils.ts","../src/ui/tui/hooks/useKeyboardHints.tsx","../src/ui/tui/hooks/useKeyBindings.ts","../src/ui/tui/primitives/PickerMenu.tsx"],"sourcesContent":["/**\n * Filter-as-you-type matching for PickerMenu. Split out from the component so the\n * ranking is testable on its own — the component only decides when to call it.\n */\n\nimport Fuse, { type IFuseOptions } from 'fuse.js';\n\n/** The searchable face of a picker option. PickerMenu's `PickerOption<T>` satisfies it. */\nexport interface Searchable {\n label: string;\n hint?: string;\n description?: string;\n}\n\n/**\n * Fuzzy fallback config. `ignoreLocation` because a term matching at the end of a\n * description is as good as one at the start, and Fuse's default distance window\n * would score the late match away. 0.5 is the loosest threshold that still bottoms\n * out: below it `gthb` misses `GitHub` entirely, which is the whole point of having\n * a fuzzy pass; above it the tail grows without surfacing anything new at the top.\n */\nexport const FUSE_OPTIONS: IFuseOptions<Searchable> = {\n keys: [\n { name: 'label', weight: 3 },\n { name: 'hint', weight: 1 },\n { name: 'description', weight: 1 },\n ],\n ignoreLocation: true,\n threshold: 0.5,\n includeScore: true,\n};\n\nexport function buildPickerIndex<T extends Searchable>(options: T[]): Fuse<T> {\n return new Fuse(options, FUSE_OPTIONS as IFuseOptions<T>);\n}\n\nfunction haystack(option: Searchable): string {\n return `${option.label} ${option.hint ?? ''} ${\n option.description ?? ''\n }`.toLowerCase();\n}\n\n/**\n * Rank options against a query, best first, or `null` for an empty query.\n *\n * Two passes, literal before fuzzy. Anything that literally contains every\n * whitespace-separated term wins outright, in the list's own order — that covers how\n * people actually type (`git iss` → `GitHub issues`) and keeps the common case as\n * tight as a plain substring filter, which fuzzy matching on its own is not: Fuse at\n * a threshold loose enough to forgive `gthb` also lets a third of the list through\n * on `replay`.\n *\n * Only when nothing matches literally does Fuse get a turn, so a typo degrades to a\n * ranked guess instead of an empty list. Terms are AND-ed there too, each matched\n * fuzzily on its own — Fuse's bitap pass over the whole query would otherwise need\n * `git iss` to be a near-edit of the text it's matching. Scores are distances (0 is\n * exact) and sum across terms, hence the ascending sort; ties keep the order the\n * first term put them in, since the sort is stable.\n */\nexport function rankOptions<T extends Searchable>(\n fuse: Fuse<T>,\n options: T[],\n query: string,\n): T[] | null {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!terms.length) {\n return null;\n }\n\n const literal = options.filter((option) =>\n terms.every((term) => haystack(option).includes(term)),\n );\n if (literal.length) {\n return literal;\n }\n\n let surviving: Map<T, number> | null = null;\n for (const term of terms) {\n const hits = new Map<T, number>(\n fuse.search(term).map(({ item, score }) => [item, score ?? 0]),\n );\n if (surviving === null) {\n surviving = hits;\n continue;\n }\n // Deleting the current key mid-iteration is well-defined for a Map.\n for (const [option, total] of surviving) {\n const score = hits.get(option);\n if (score === undefined) {\n surviving.delete(option);\n } else {\n surviving.set(option, total + score);\n }\n }\n }\n\n return [...(surviving ?? [])]\n .sort(([, a], [, b]) => a - b)\n .map(([option]) => option);\n}\n","/**\n * PromptLabel — Compact inline label for input prompts.\n *\n * Renders: [!] message\n * where [!] is black text on accent background.\n */\n\nimport { Box, Text } from 'ink';\nimport { Colors } from '@ui/tui/styles';\n\ninterface PromptLabelProps {\n message?: string;\n}\n\nexport const PromptLabel = ({ message }: PromptLabelProps) => {\n // No message → no row, so label-less pickers don't get a stray blank line.\n if (!message) return null;\n return (\n <Box>\n <Text bold color={Colors.accent}>\n {' '}\n {message}\n </Text>\n </Box>\n );\n};\n","/**\n * ConfirmButton — the confirm row used to submit a selection.\n *\n * Pure render. Multi-select menus (PickerMenu mode=\"multi\", GroupedPickerMenu)\n * append this below their options as the final focusable row: the user toggles\n * options with enter, then arrows down onto this row and presses enter to\n * submit. This replaces the older \"enter anywhere submits\" pattern, which\n * confused people who expected enter to toggle the focused item.\n *\n * Renders flat, mirroring the option rows — a focus triangle and the label,\n * accent and bold when focused, dimmed otherwise — so it lines up under the\n * options instead of sitting in a separate boxed target.\n */\n\nimport { Text } from 'ink';\nimport { Icons, Colors } from '@ui/tui/styles';\n\ninterface ConfirmButtonProps {\n /** Button text. Defaults to \"Confirm\". */\n label?: string;\n /** Whether the cursor is currently on the button. */\n focused: boolean;\n /** Optional selected-item count, rendered as \"Label (n)\" when > 0. */\n count?: number;\n}\n\nexport const ConfirmButton = ({\n label = 'Confirm',\n focused,\n count,\n}: ConfirmButtonProps) => {\n const text = count && count > 0 ? `${label} (${count})` : label;\n return (\n <Text\n color={focused ? Colors.accent : undefined}\n bold={focused}\n dimColor={!focused}\n >\n {focused ? Icons.triangleSmallRight : ' '} {text}\n </Text>\n );\n};\n","/**\n * useStdoutDimensions — Returns [columns, rows] and re-renders on terminal resize.\n *\n * Ink's useStdout() does not subscribe to resize events, so layout only updates\n * when something else causes a re-render. This hook listens to the stream's\n * 'resize' event (Node TTY) and updates state so the component re-renders\n * with the new dimensions.\n */\n\nimport { useStdout } from 'ink';\nimport { useState, useEffect } from 'react';\n\nexport function useStdoutDimensions(): [number, number] {\n const { stdout } = useStdout();\n const [size, setSize] = useState<[number, number]>(() => [\n stdout.columns || 80,\n stdout.rows || 24,\n ]);\n\n useEffect(() => {\n const cols = stdout.columns || 80;\n const rows = stdout.rows || 24;\n setSize([cols, rows]);\n\n const stream = stdout as NodeJS.WriteStream & {\n on?(event: string, fn: () => void): void;\n };\n if (typeof stream.on !== 'function') return;\n\n const onResize = () => {\n const c = stdout.columns || 80;\n const r = stdout.rows || 24;\n if (c > 0 && r > 0) setSize([c, r]);\n };\n stream.on('resize', onResize);\n return () => {\n stream.off?.('resize', onResize);\n };\n }, [stdout]);\n\n return size;\n}\n","/**\n * Pure utility functions for keyboard hints — no React or Ink dependencies.\n * Extracted for testability in a node Jest environment.\n */\n\nexport interface KeyboardHint {\n label: string;\n action: string;\n priority: number;\n}\n\n/** Well-known key matches corresponding to Ink's key.* properties. */\nexport enum KeyMatch {\n UpArrow = 'upArrow',\n DownArrow = 'downArrow',\n LeftArrow = 'leftArrow',\n RightArrow = 'rightArrow',\n Return = 'return',\n Escape = 'escape',\n Space = 'space',\n Backspace = 'backspace',\n /**\n * Any single printable character typed without a modifier. Broad by design,\n * so a binding using it must sit last — the first match wins, which is what\n * lets a specific char binding still take precedence over free typing.\n */\n Printable = 'printable',\n}\n\n/** A key match: either a KeyMatch enum value or a literal character string (e.g. 'a', 's'). */\nexport type KeyMatchOrChar = KeyMatch | (string & NonNullable<unknown>);\n\n/** Default priorities by key type. */\nconst DEFAULT_PRIORITY: Record<string, number> = {\n [KeyMatch.UpArrow]: 0,\n [KeyMatch.DownArrow]: 0,\n [KeyMatch.LeftArrow]: 1,\n [KeyMatch.RightArrow]: 1,\n [KeyMatch.Space]: 10,\n [KeyMatch.Printable]: 12,\n [KeyMatch.Backspace]: 13,\n [KeyMatch.Escape]: 20,\n [KeyMatch.Return]: 21,\n};\n\n/** Get the default display priority for a key match. */\nexport function getDefaultPriority(\n match: KeyMatchOrChar | KeyMatchOrChar[],\n): number {\n const first = Array.isArray(match) ? match[0] : match;\n return DEFAULT_PRIORITY[first] ?? 15;\n}\n\n/** Check if a KeyMatchOrChar matches the given input string and key flags. */\nexport function matchesKey(\n m: KeyMatchOrChar,\n input: string,\n key: { [k: string]: unknown },\n): boolean {\n switch (m) {\n case KeyMatch.UpArrow:\n return !!key.upArrow;\n case KeyMatch.DownArrow:\n return !!key.downArrow;\n case KeyMatch.LeftArrow:\n return !!key.leftArrow;\n case KeyMatch.RightArrow:\n return !!key.rightArrow;\n case KeyMatch.Return:\n return !!key.return;\n case KeyMatch.Escape:\n return !!key.escape;\n case KeyMatch.Space:\n return input === ' ';\n case KeyMatch.Backspace:\n return !!key.backspace || !!key.delete;\n case KeyMatch.Printable:\n // Ink sets a flag for every non-text key while still passing a non-empty\n // `input`, so any flag but shift rules out typing.\n // Any single non-control character, not just ASCII — a filter query can\n // hold an accented or non-Latin letter.\n return (\n input.length === 1 &&\n input >= ' ' &&\n input !== '\\x7f' &&\n Object.entries(key).every(([flag, on]) => !on || flag === 'shift')\n );\n default:\n return input === m;\n }\n}\n\n/** Serialize hints for comparison. */\nexport function hintsKey(hints: KeyboardHint[]): string {\n return hints.map((h) => `${h.label}:${h.action}`).join('|');\n}\n\n/** Deduplicate hints by label+action and sort by priority. */\nexport function deduplicateAndSort(hints: KeyboardHint[]): KeyboardHint[] {\n const seen = new Set<string>();\n const deduped: KeyboardHint[] = [];\n for (const hint of hints) {\n const k = `${hint.label}:${hint.action}`;\n if (!seen.has(k)) {\n seen.add(k);\n deduped.push(hint);\n }\n }\n deduped.sort((a, b) => a.priority - b.priority);\n return deduped;\n}\n","/**\n * KeyboardHintsProvider — Context for collecting and displaying keyboard hints.\n *\n * Input components register their hints via useKeyBindings. The provider\n * flattens, deduplicates, and sorts them. The hints bar stays visible for as\n * long as a screen has registered hints — it never auto-dismisses.\n */\n\nimport {\n createContext,\n useCallback,\n useContext,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n hintsKey,\n deduplicateAndSort,\n type KeyboardHint,\n} from './keyboard-hints-utils.js';\n\nexport type { KeyboardHint } from './keyboard-hints-utils.js';\n\ninterface KeyboardHintsContextValue {\n register(id: string, hints: KeyboardHint[]): void;\n unregister(id: string): void;\n hints: KeyboardHint[];\n}\n\nconst KeyboardHintsContext = createContext<KeyboardHintsContextValue>({\n register: () => undefined,\n unregister: () => undefined,\n hints: [],\n});\n\nexport const useKeyboardHintsContext = () => useContext(KeyboardHintsContext);\n\nexport const KeyboardHintsProvider = ({\n children,\n}: {\n children: ReactNode;\n}) => {\n const registrationsRef = useRef(new Map<string, KeyboardHint[]>());\n const [hints, setHints] = useState<KeyboardHint[]>([]);\n const prevHintsKeyRef = useRef('');\n\n const recompute = useCallback(() => {\n const all: KeyboardHint[] = [];\n for (const h of registrationsRef.current.values()) {\n all.push(...h);\n }\n const deduped = deduplicateAndSort(all);\n\n const newKey = hintsKey(deduped);\n if (newKey !== prevHintsKeyRef.current) {\n prevHintsKeyRef.current = newKey;\n setHints(deduped);\n }\n }, []);\n\n const register = useCallback(\n (id: string, h: KeyboardHint[]) => {\n registrationsRef.current.set(id, h);\n recompute();\n },\n [recompute],\n );\n\n const unregister = useCallback(\n (id: string) => {\n registrationsRef.current.delete(id);\n recompute();\n },\n [recompute],\n );\n\n return (\n <KeyboardHintsContext.Provider value={{ register, unregister, hints }}>\n {children}\n </KeyboardHintsContext.Provider>\n );\n};\n","/**\n * useKeyBindings — Declarative keyboard input + automatic hint registration.\n *\n * Replaces raw `useInput` in input components. Define bindings as data;\n * the hook wires up the Ink input handler AND registers hints in the\n * KeyboardHintsProvider. One source of truth for keys and their labels.\n */\n\nimport { useInput, type Key } from 'ink';\nimport { useEffect, useRef } from 'react';\nimport { useKeyboardHintsContext } from './useKeyboardHints.js';\nimport {\n matchesKey,\n getDefaultPriority,\n KeyMatch,\n type KeyboardHint,\n type KeyMatchOrChar,\n} from './keyboard-hints-utils.js';\n\nexport { KeyMatch };\nexport type { KeyMatchOrChar } from './keyboard-hints-utils.js';\n\nexport interface KeyBinding {\n /** Which key(s) trigger this binding. Array = multiple keys, one hint. */\n match: KeyMatchOrChar | KeyMatchOrChar[];\n /** Display label in hints bar (e.g. \"↑↓\", \"space\", \"enter\") */\n label: string;\n /** Description in hints bar (e.g. \"navigate\", \"toggle\") */\n action: string;\n /** Ordering priority (lower = further left). Defaults based on key type. */\n priority?: number;\n /** Handler called when the key matches. */\n handler: (input: string, key: Key) => void;\n}\n\n/**\n * Declarative key bindings hook. Replaces `useInput` in input components.\n * Registers hints automatically with the KeyboardHintsProvider.\n *\n * @param id Unique identifier for this component's hints registration\n * @param bindings Array of key binding definitions\n */\nexport function useKeyBindings(id: string, bindings: KeyBinding[]): void {\n const ctx = useKeyboardHintsContext();\n\n // Extract hints and register. Use a serialized key to avoid unnecessary updates.\n const hintsRef = useRef<string>('');\n const hints: KeyboardHint[] = bindings.map((b) => ({\n label: b.label,\n action: b.action,\n priority: b.priority ?? getDefaultPriority(b.match),\n }));\n const serialized = hints\n .map((h) => `${h.label}:${h.action}:${h.priority}`)\n .join('|');\n\n useEffect(() => {\n if (serialized !== hintsRef.current) {\n hintsRef.current = serialized;\n ctx.register(id, hints);\n }\n return () => ctx.unregister(id);\n // eslint-disable-next-line\n }, [id, serialized]);\n\n // Wire up input handling\n useInput((input, key) => {\n for (const binding of bindings) {\n const matches = Array.isArray(binding.match)\n ? binding.match\n : [binding.match];\n if (matches.some((m) => matchesKey(m, input, key))) {\n binding.handler(input, key);\n return;\n }\n }\n });\n}\n","/**\n * PickerMenu — Single and multi select.\n * Single mode: custom renderer with small triangle indicator; enter selects.\n * Multi mode: checkbox glyphs toggled with enter, plus a focusable\n * Confirm button below the options. The cursor moves onto the button and\n * enter submits — see MultiPickerMenu for the rationale.\n *\n * Key bindings are declared via useKeyBindings, which auto-registers\n * hints in the KeyboardHintsBar.\n */\n\nimport { Box, Text } from 'ink';\nimport { useEffect, useMemo, useState } from 'react';\nimport { buildPickerIndex, rankOptions } from './picker-filter.js';\nimport { Icons, Colors } from '@ui/tui/styles';\nimport { PromptLabel } from './PromptLabel.js';\nimport { ConfirmButton } from './ConfirmButton.js';\nimport { wordWrap } from './layout-helpers.js';\nimport { useStdoutDimensions } from '@ui/tui/hooks/useStdoutDimensions';\nimport {\n useKeyBindings,\n KeyMatch,\n type KeyBinding,\n type KeyMatchOrChar,\n} from '@ui/tui/hooks/useKeyBindings';\n\nexport interface PickerOption<T> {\n label: string;\n value: T;\n hint?: string;\n /**\n * Multi-select only: a secondary explanation rendered dimmed and wrapped on\n * its own line(s) beneath the label, for choices that need more than a title.\n * When unset, the row renders exactly as a label-only row.\n */\n description?: string;\n /** Glyph rendered before the label, in its own color — unaffected by\n * focus and disabled styling. */\n icon?: { glyph: string; color?: string };\n /** Dimmed and unselectable; navigation skips over it. */\n disabled?: boolean;\n /**\n * Section heading: unselectable (navigation skips it, like `disabled`) but\n * rendered as a bold header rather than a muted row. Set `disabled` too so\n * the skip logic treats it as non-selectable.\n */\n header?: boolean;\n /** Indent the label one level, e.g. items nested under a `header` row. */\n indent?: boolean;\n /**\n * Multi-select only: marks this option mutually exclusive with every other\n * option. Selecting it clears all other picks; selecting any non-exclusive\n * option clears it. Used e.g. for a browser connector that can't be\n * installed alongside local editors.\n */\n exclusive?: boolean;\n}\n\n/**\n * Step through a column's options in `dir`, wrapping, until an enabled\n * option is found. Returns `from` unchanged if the column is entirely\n * disabled.\n */\nfunction stepEnabled<T>(\n options: PickerOption<T>[],\n rows: number,\n from: number,\n dir: 1 | -1,\n): number {\n const col = Math.floor(from / rows);\n const colStart = col * rows;\n const colLen = Math.min(rows, options.length - colStart);\n let row = from % rows;\n for (let i = 0; i < colLen; i++) {\n row = (row + dir + colLen) % colLen;\n const idx = colStart + row;\n if (!options[idx]?.disabled) return idx;\n }\n return from;\n}\n\n/** First enabled option in [start, end), for initial focus and for entering\n * a page from the Confirm button. */\nfunction firstEnabled<T>(\n options: PickerOption<T>[],\n start = 0,\n end = options.length,\n): number {\n for (let i = start; i < end; i++) {\n if (!options[i]?.disabled) return i;\n }\n return start;\n}\n\n/** Last enabled option in [start, end), for wrapping from the button onto\n * the bottom of the page. */\nfunction lastEnabled<T>(\n options: PickerOption<T>[],\n start = 0,\n end = options.length,\n): number {\n for (let i = end - 1; i >= start; i--) {\n if (!options[i]?.disabled) return i;\n }\n return end - 1;\n}\n\n/**\n * Rows the surrounding screen or overlay consumes above and below a picker —\n * border + padding, title, prompt text, and the keyboard-hints bar. A\n * deliberately generous estimate: overshooting just shows a few fewer rows,\n * whereas undershooting lets a long list overflow the viewport (the bug this\n * windowing guards against). Mirrors GroupedPickerMenu's budgeting.\n */\nconst CHROME_OVERHEAD = 13;\n/**\n * Max visual rows a picker renders regardless of terminal height. Without a\n * ceiling a tall terminal lets a long list fill the whole viewport, which\n * reads as a wall of options; ~12 rows keeps the menu scannable and leaves\n * known breathing room above and below.\n */\nconst MAX_LIST_ROWS = 12;\n/** Extra rows a multi-select adds below its options: marginTop + Confirm button. */\nconst CONFIRM_CHROME = 3;\n/** Lists shorter than this never page — tall rows would split 2-3 options into pages of one. */\nconst MIN_COUNT_TO_PAGE = 5;\n/** Width the multi-select wraps option descriptions to (matches the render). */\nconst DESCRIPTION_WIDTH = 56;\n\ninterface PickerViewport {\n needsScroll: boolean;\n /** First option index on the current page. */\n start: number;\n /** One past the last option index on the current page. */\n end: number;\n hiddenAbove: number;\n hiddenBelow: number;\n /** Focus target one page over (wrapping), for the n/p keys. */\n pageStep: (focused: number, dir: 1 | -1) => number;\n}\n\n/**\n * Pages a single-column option list to the terminal height. The visible page\n * is derived from the focused index — no scroll state — so ↑/↓ flip pages as\n * focus crosses a page edge and n/p jump a whole page. Pages hold a fixed\n * option count sized to the tallest row (`rowCost`), trading a sparser page\n * on mixed-height lists for arithmetic-only paging. Engages only for\n * single-column pickers — multi-column grids already compress vertically.\n */\nfunction usePickerViewport(\n count: number,\n rowCost: number,\n chromeBelow: number,\n enabled: boolean,\n focused: number,\n): PickerViewport {\n const [, termRows] = useStdoutDimensions();\n const budget = Math.max(\n 5,\n Math.min(termRows - CHROME_OVERHEAD - chromeBelow, MAX_LIST_ROWS),\n );\n const needsScroll =\n enabled && count >= MIN_COUNT_TO_PAGE && count * rowCost > budget;\n // Reserve two rows for the \"↑/↓ N more\" indicators.\n const perPage = needsScroll\n ? Math.max(1, Math.floor((budget - 2) / rowCost))\n : count;\n const pageCount = Math.max(1, Math.ceil(count / perPage));\n const start = Math.floor(focused / perPage) * perPage;\n const end = Math.min(start + perPage, count);\n return {\n needsScroll,\n start,\n end,\n hiddenAbove: start,\n hiddenBelow: count - end,\n pageStep: (f, dir) =>\n ((Math.floor(f / perPage) + dir + pageCount) % pageCount) * perPage,\n };\n}\n\ninterface PickerMenuProps<T> {\n message?: string;\n options: PickerOption<T>[];\n mode?: 'single' | 'multi';\n centered?: boolean;\n columns?: 1 | 2 | 3 | 4;\n /**\n * Vertical space between options, in TUI rows. Defaults to 0 — i.e.\n * options stack tightly. Set to 1+ when the option labels are long\n * (wrap across multiple lines) or for visual breathing room.\n */\n optionMarginBottom?: number;\n /**\n * Filter-as-you-type. Defaults on once the list is long enough to page, where\n * finding one option means walking pages. Pass `false` to force it off.\n */\n filterable?: boolean;\n onSelect: (value: T | T[]) => void;\n}\n\n/** Lists at least this long get filtering. Shorter ones fit on a page or two,\n * where a filter row costs more rows than it saves. */\nconst FILTER_MIN_OPTIONS = 10;\n\nexport const PickerMenu = <T,>({\n message,\n options,\n mode = 'single',\n centered = false,\n columns = 1,\n optionMarginBottom = 0,\n filterable,\n onSelect,\n}: PickerMenuProps<T>) => {\n const canFilter = filterable ?? options.length >= FILTER_MIN_OPTIONS;\n const [filter, setFilter] = useState('');\n const fuse = useMemo(\n () => (canFilter ? buildPickerIndex(options) : null),\n [options, canFilter],\n );\n const visible = useMemo(\n () => (fuse ? rankOptions(fuse, options, filter) ?? options : options),\n [fuse, options, filter],\n );\n\n // Keyed by label, and owned here rather than by MultiPickerMenu: filtering\n // rewrites the list under the ticks, and the remount below would drop them.\n const [selected, setSelected] = useState<Set<string>>(new Set());\n\n const shared = {\n message,\n options: visible,\n centered,\n optionMarginBottom,\n onSelect,\n filter: canFilter ? filter : null,\n setFilter,\n totalCount: options.length,\n };\n\n // A filtered list is a fresh list with its own focus and paging, so remount on\n // each query rather than leaving stale indices to be re-validated.\n const key = String(filter);\n\n if (mode === 'multi') {\n return (\n <MultiPickerMenu\n key={key}\n {...shared}\n columns={columns}\n allOptions={options}\n selected={selected}\n setSelected={setSelected}\n />\n );\n }\n\n return <SinglePickerMenu key={key} {...shared} columns={columns} />;\n};\n\n/**\n * Backspace and free typing. Append these last: `Printable` matches almost any\n * key, and bindings resolve in order, so anything specific has to be registered\n * first to keep working. Both share a label/action so the hints bar shows one.\n */\nfunction filterBindings(\n filter: string,\n setFilter: (query: string) => void,\n): KeyBinding[] {\n return [\n {\n match: KeyMatch.Backspace,\n label: 'type',\n action: 'filter',\n handler: () => setFilter(filter.slice(0, -1)),\n },\n {\n match: KeyMatch.Printable,\n label: 'type',\n action: 'filter',\n handler: (input: string) => setFilter(filter + input),\n },\n ];\n}\n\n/** The filter row above a filterable list: what's typed, and how much it left. */\nconst FilterRow = ({\n filter,\n shown,\n total,\n}: {\n filter: string;\n shown: number;\n total: number;\n}) => (\n // Indented to sit under PromptLabel, which carries a leading space.\n <Box paddingLeft={1}>\n <Text dimColor>\n {filter\n ? `Filter: ${filter} (${shown} of ${total})`\n : `Type to filter ${total} options`}\n </Text>\n </Box>\n);\n\n/** Custom single-select with triangle indicator and accent highlight. */\nconst SinglePickerMenu = <T,>({\n message,\n options,\n centered = false,\n columns = 1,\n optionMarginBottom = 0,\n filter,\n setFilter,\n totalCount,\n onSelect,\n}: {\n message?: string;\n options: PickerOption<T>[];\n centered?: boolean;\n columns?: number;\n optionMarginBottom?: number;\n /** Current filter query, or null when this picker isn't filterable. */\n filter: string | null;\n setFilter: (query: string) => void;\n totalCount: number;\n onSelect: (value: T | T[]) => void;\n}) => {\n const [focused, setFocused] = useState(() => firstEnabled(options));\n const rows = Math.ceil(options.length / columns);\n // Single-select rows are label-only (no descriptions): one line plus margin.\n const viewport = usePickerViewport(\n options.length,\n 1 + optionMarginBottom,\n filter !== null ? 1 : 0,\n columns === 1,\n focused,\n );\n\n // Re-validate focus when the options change while mounted \\u2014 a list\n // that shrinks or disables entries can leave `focused` pointing at a\n // missing or disabled option, which would make enter a no-op.\n useEffect(() => {\n if (focused >= options.length || options[focused]?.disabled) {\n setFocused(firstEnabled(options));\n }\n }, [options, focused]);\n\n const bindings: KeyBinding[] = [\n {\n match: [KeyMatch.UpArrow, KeyMatch.DownArrow],\n label: '\\u2191\\u2193',\n action: 'navigate',\n handler: (_input, key) => {\n const dir = key.upArrow ? -1 : 1;\n // `start`/`end` derive from `focused`, so stepping past a page edge\n // flips the page rather than needing a bound here.\n setFocused(stepEnabled(options, rows, focused, dir));\n },\n },\n // Only on a non-filterable list. Once a filter row exists `n` and `p` are\n // characters, not commands — so they go for good, not just once a query is\n // typed, and the arrows carry paging instead by walking the whole list.\n ...(viewport.needsScroll && filter === null\n ? [\n {\n match: ['n', 'p'] as KeyMatchOrChar[],\n label: 'n/p',\n action: 'page',\n handler: (input: string) => {\n const target = viewport.pageStep(focused, input === 'n' ? 1 : -1);\n setFocused(\n options[target]?.disabled\n ? stepEnabled(options, rows, target, 1)\n : target,\n );\n },\n },\n ]\n : []),\n {\n match: KeyMatch.Return,\n label: 'enter',\n action: 'select',\n handler: () => {\n const selected = options[focused];\n if (selected && !selected.disabled) {\n onSelect(selected.value);\n }\n },\n },\n ];\n\n if (columns > 1) {\n bindings.splice(1, 0, {\n match: [KeyMatch.LeftArrow, KeyMatch.RightArrow],\n label: '\\u2190\\u2192',\n action: 'navigate',\n handler: (_input, key) => {\n const col = Math.floor(focused / rows);\n const row = focused % rows;\n\n let next = focused;\n if (key.leftArrow) {\n const prevCol = col > 0 ? col - 1 : columns - 1;\n next = Math.min(prevCol * rows + row, options.length - 1);\n }\n if (key.rightArrow) {\n const nextCol = col < columns - 1 ? col + 1 : 0;\n next = Math.min(nextCol * rows + row, options.length - 1);\n }\n // Landing on a disabled option slides to the column's nearest\n // enabled one.\n if (options[next]?.disabled) {\n next = stepEnabled(options, rows, next, 1);\n }\n setFocused(next);\n },\n });\n }\n\n if (filter !== null) {\n bindings.push(...filterBindings(filter, setFilter));\n }\n\n useKeyBindings('single-picker', bindings);\n\n // Chunk options into columns (column-first ordering)\n const columnArrays: PickerOption<T>[][] = [];\n for (let c = 0; c < columns; c++) {\n columnArrays.push(options.slice(c * rows, c * rows + rows));\n }\n\n const align = centered ? 'center' : undefined;\n\n const renderOption = (opt: PickerOption<T>, flatIdx: number) => {\n const isFocused = flatIdx === focused;\n const base = opt.hint ? `${opt.label} (${opt.hint})` : opt.label;\n const label = opt.indent ? ` ${base}` : base;\n return (\n <Box key={flatIdx} gap={1} marginBottom={optionMarginBottom}>\n <Text\n color={isFocused ? Colors.accent : undefined}\n dimColor={!isFocused}\n >\n {isFocused && !opt.header ? Icons.triangleSmallRight : ' '}\n </Text>\n {opt.icon && <Text color={opt.icon.color}>{opt.icon.glyph}</Text>}\n <Text\n color={\n opt.header\n ? undefined\n : opt.disabled\n ? Colors.muted\n : isFocused\n ? Colors.accent\n : undefined\n }\n bold={opt.header || (isFocused && !opt.disabled)}\n dimColor={!opt.header && (!isFocused || opt.disabled)}\n >\n {label}\n </Text>\n </Box>\n );\n };\n\n return (\n <Box flexDirection=\"column\" alignItems={align}>\n <PromptLabel message={message} />\n {filter !== null && (\n <FilterRow filter={filter} shown={options.length} total={totalCount} />\n )}\n {filter && options.length === 0 ? (\n <Text dimColor>No options match. Backspace to widen the filter.</Text>\n ) : viewport.needsScroll ? (\n <Box flexDirection=\"column\">\n <Text dimColor>\n {viewport.hiddenAbove > 0\n ? `↑ ${viewport.hiddenAbove} more${\n filter === null ? ' [P] for previous page' : ''\n }`\n : ' '}\n </Text>\n {options\n .slice(viewport.start, viewport.end)\n .map((opt, relIdx) => renderOption(opt, viewport.start + relIdx))}\n <Text dimColor>\n {viewport.hiddenBelow > 0\n ? `↓ ${viewport.hiddenBelow} more${\n filter === null ? ' [N] for next page' : ''\n }`\n : ' '}\n </Text>\n </Box>\n ) : (\n <Box flexDirection=\"row\" gap={4}>\n {columnArrays.map((colOpts, colIdx) => (\n <Box key={colIdx} flexDirection=\"column\">\n {colOpts.map((opt, rowIdx) =>\n renderOption(opt, colIdx * rows + rowIdx),\n )}\n </Box>\n ))}\n </Box>\n )}\n </Box>\n );\n};\n\n/**\n * Custom multi-select with checkbox glyphs and accent highlight.\n *\n * Interaction model (shared with GroupedPickerMenu):\n * - \\u2191\\u2193 move the cursor through the options AND onto the Confirm button,\n * which lives just past the last option.\n * - enter toggles the focused option (no more \"space toggles but enter\n * advances\" split that tripped people up). Space is kept as an\n * undocumented alias, but the hints bar advertises only enter.\n * - moving onto the Confirm button and pressing enter submits the\n * current selection.\n */\nconst MultiPickerMenu = <T,>({\n message,\n options,\n centered = false,\n columns = 1,\n optionMarginBottom = 0,\n filter,\n setFilter,\n totalCount,\n allOptions,\n selected,\n setSelected,\n onSelect,\n}: {\n message?: string;\n options: PickerOption<T>[];\n centered?: boolean;\n columns?: number;\n optionMarginBottom?: number;\n /** Current filter query, or null when this picker isn't filterable. */\n filter: string | null;\n setFilter: (query: string) => void;\n totalCount: number;\n /** Every option, filtered or not. Ticks outlive the query, so resolving and\n * submitting them has to happen against the full list. */\n allOptions: PickerOption<T>[];\n selected: Set<string>;\n setSelected: (update: (prev: Set<string>) => Set<string>) => void;\n onSelect: (value: T | T[]) => void;\n}) => {\n const [focused, setFocused] = useState(() => firstEnabled(options));\n // When true, the cursor is on the Confirm button rather than an option.\n const [onButton, setOnButton] = useState(false);\n const rows = Math.ceil(options.length / columns);\n // A row is its label line plus any margin; a description adds one line per\n // wrapped line beneath the label. Pages size to the tallest row.\n const rowCost = options.reduce(\n (max, opt) =>\n Math.max(\n max,\n 1 +\n optionMarginBottom +\n (opt.description\n ? wordWrap(opt.description, DESCRIPTION_WIDTH).length\n : 0),\n ),\n 1,\n );\n const viewport = usePickerViewport(\n options.length,\n rowCost,\n CONFIRM_CHROME + (filter !== null ? 1 : 0),\n columns === 1,\n focused,\n );\n\n // Re-validate focus when the options change while mounted — a list\n // that shrinks or disables entries can leave `focused` pointing at a\n // missing or disabled option, which would make enter a no-op.\n useEffect(() => {\n if (focused >= options.length || options[focused]?.disabled) {\n setFocused(firstEnabled(options));\n }\n }, [options, focused]);\n\n const confirm = () => {\n // In `allOptions` order, so a pick made under one filter still submits\n // after the query moved on.\n const values = allOptions\n .filter((option) => selected.has(option.label))\n .map((option) => option.value);\n onSelect(values);\n };\n\n const bindings: KeyBinding[] = [\n {\n match: [KeyMatch.UpArrow, KeyMatch.DownArrow],\n label: '\\u2191\\u2193',\n action: 'navigate',\n handler: (_input, key) => {\n const dir = key.upArrow ? -1 : 1;\n if (onButton) {\n // Button \\u2192 back onto the current page (bottom for \\u2191, top for \\u2193).\n setOnButton(false);\n setFocused(\n dir === -1\n ? lastEnabled(options, viewport.start, viewport.end)\n : firstEnabled(options, viewport.start, viewport.end),\n );\n return;\n }\n const col = Math.floor(focused / rows);\n const row = focused % rows;\n const colLen = Math.min(rows, options.length - col * rows);\n // Nearest enabled option above/below in this column; leaving the\n // column lands on the button.\n let r = row + dir;\n while (r >= 0 && r < colLen && options[col * rows + r]?.disabled) {\n r += dir;\n }\n if (r >= 0 && r < colLen) {\n setFocused(col * rows + r);\n } else {\n setOnButton(true);\n }\n },\n },\n // Only on a non-filterable list. Once a filter row exists `n` and `p` are\n // characters, not commands — so they go for good, not just once a query is\n // typed, and the arrows carry paging instead by walking the whole list.\n ...(viewport.needsScroll && filter === null\n ? [\n {\n match: ['n', 'p'] as KeyMatchOrChar[],\n label: 'n/p',\n action: 'page',\n handler: (input: string) => {\n const target = viewport.pageStep(focused, input === 'n' ? 1 : -1);\n setOnButton(false);\n setFocused(\n options[target]?.disabled\n ? stepEnabled(options, rows, target, 1)\n : target,\n );\n },\n },\n ]\n : []),\n {\n // Space is only an alias for enter when there's no filter to type into.\n match:\n filter !== null ? KeyMatch.Return : [KeyMatch.Space, KeyMatch.Return],\n label: 'enter',\n action: 'select',\n handler: () => {\n if (onButton) {\n confirm();\n return;\n }\n // `focused` can point past the end: filtering to zero matches leaves\n // nothing under the cursor, and enter is the natural thing to try there.\n const option = options[focused];\n if (!option || option.disabled) return;\n const label = option.label;\n setSelected((prev) => {\n const next = new Set(prev);\n if (next.has(label)) {\n next.delete(label);\n return next;\n }\n // Enforce mutual exclusivity: an exclusive option clears every other\n // pick; any other option clears previously-picked exclusive ones.\n if (option.exclusive) {\n return new Set([label]);\n }\n // Against `allOptions`: the pick being cleared may be off-screen\n // under the current query.\n for (const picked of next) {\n if (allOptions.find((o) => o.label === picked)?.exclusive) {\n next.delete(picked);\n }\n }\n next.add(label);\n return next;\n });\n },\n },\n ];\n\n if (columns > 1) {\n bindings.splice(1, 0, {\n match: [KeyMatch.LeftArrow, KeyMatch.RightArrow],\n label: '\\u2190\\u2192',\n action: 'navigate',\n handler: (_input, key) => {\n if (onButton) return;\n const col = Math.floor(focused / rows);\n const row = focused % rows;\n\n let next = focused;\n if (key.leftArrow) {\n const prevCol = col > 0 ? col - 1 : columns - 1;\n next = Math.min(prevCol * rows + row, options.length - 1);\n }\n if (key.rightArrow) {\n const nextCol = col < columns - 1 ? col + 1 : 0;\n next = Math.min(nextCol * rows + row, options.length - 1);\n }\n // Landing on a disabled option slides to the column's nearest\n // enabled one.\n if (options[next]?.disabled) {\n next = stepEnabled(options, rows, next, 1);\n }\n setFocused(next);\n },\n });\n }\n\n if (filter !== null) {\n bindings.push(...filterBindings(filter, setFilter));\n }\n\n useKeyBindings('multi-picker', bindings);\n\n const columnArrays: PickerOption<T>[][] = [];\n for (let c = 0; c < columns; c++) {\n columnArrays.push(options.slice(c * rows, c * rows + rows));\n }\n\n const renderOption = (opt: PickerOption<T>, flatIdx: number) => {\n const isFocused = !onButton && flatIdx === focused;\n const isSelected = selected.has(opt.label);\n const label = opt.hint ? `${opt.label} (${opt.hint})` : opt.label;\n const checkbox = isSelected ? Icons.squareFilled : Icons.squareOpen;\n return (\n <Box\n key={flatIdx}\n flexDirection=\"column\"\n marginBottom={optionMarginBottom}\n >\n <Box gap={1}>\n <Text\n color={isSelected ? 'white' : Colors.muted}\n dimColor={!isFocused && !isSelected}\n >\n {checkbox}\n </Text>\n {opt.icon && <Text color={opt.icon.color}>{opt.icon.glyph}</Text>}\n <Text\n color={\n opt.disabled\n ? Colors.muted\n : isFocused\n ? Colors.accent\n : undefined\n }\n bold={isFocused && !opt.disabled}\n dimColor={!isFocused || opt.disabled}\n >\n {label}\n </Text>\n </Box>\n {/* Optional dimmed, wrapped explanation under the label. The explicit\n width forces Ink to wrap (an unconstrained Box shrinks to its\n content and never wraps). Renders only when set, so label-only rows\n are byte-for-byte unchanged. */}\n {opt.description && (\n <Box marginLeft={4} width={DESCRIPTION_WIDTH}>\n <Text dimColor wrap=\"wrap\">\n {opt.description}\n </Text>\n </Box>\n )}\n </Box>\n );\n };\n\n return (\n <Box flexDirection=\"column\" alignItems={centered ? 'center' : undefined}>\n <PromptLabel message={message} />\n {filter !== null && (\n <FilterRow filter={filter} shown={options.length} total={totalCount} />\n )}\n {filter && options.length === 0 && (\n <Text dimColor>No options match. Backspace to widen the filter.</Text>\n )}\n {viewport.needsScroll ? (\n <Box\n flexDirection=\"column\"\n marginLeft={centered ? 0 : 2}\n marginTop={message ? 1 : 0}\n >\n <Text dimColor>\n {viewport.hiddenAbove > 0\n ? `↑ ${viewport.hiddenAbove} more${\n filter === null ? ' [P] for previous page' : ''\n }`\n : ' '}\n </Text>\n {options\n .slice(viewport.start, viewport.end)\n .map((opt, relIdx) => renderOption(opt, viewport.start + relIdx))}\n <Text dimColor>\n {viewport.hiddenBelow > 0\n ? `↓ ${viewport.hiddenBelow} more${\n filter === null ? ' [N] for next page' : ''\n }`\n : ' '}\n </Text>\n </Box>\n ) : (\n <Box\n flexDirection=\"row\"\n gap={4}\n marginLeft={centered ? 0 : 2}\n marginTop={message ? 1 : 0}\n >\n {columnArrays.map((colOpts, colIdx) => (\n <Box key={colIdx} flexDirection=\"column\">\n {colOpts.map((opt, rowIdx) =>\n renderOption(opt, colIdx * rows + rowIdx),\n )}\n </Box>\n ))}\n </Box>\n )}\n <Box marginTop={1} marginLeft={centered ? 0 : 2}>\n <ConfirmButton focused={onButton} count={selected.size} />\n </Box>\n </Box>\n );\n};\n"],"mappings":"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"}