@posthog/wizard 2.79.0 → 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.
- package/dist/PickerMenu-m4CCPX4f.js +758 -0
- package/dist/PickerMenu-m4CCPX4f.js.map +1 -0
- package/dist/{agent-CNrEC-9v.js → agent-BkBbVuNq.js} +50 -45
- package/dist/agent-BkBbVuNq.js.map +1 -0
- package/dist/agent-QGK2YzlX.js +6 -0
- package/dist/{agent-interface-BXvmAm4c.js → agent-interface-CzyCmp7q.js} +87 -38
- package/dist/agent-interface-CzyCmp7q.js.map +1 -0
- package/dist/{agent-prompt-loader-DiKUbhi_.js → agent-prompt-loader-4wMxlYSX.js} +5 -5
- package/dist/{agent-prompt-loader-DiKUbhi_.js.map → agent-prompt-loader-4wMxlYSX.js.map} +1 -1
- package/dist/{analytics-VRYdebXW.js → analytics-B6q5PN7a.js} +5 -5
- package/dist/analytics-B6q5PN7a.js.map +1 -0
- package/dist/analytics-BXtfK9o7.js +2 -0
- package/dist/{api-DGuJTzjr.js → api-C3Dc4LFl.js} +4 -16
- package/dist/{api-DGuJTzjr.js.map → api-C3Dc4LFl.js.map} +1 -1
- package/dist/api-yNjrfZs2.js +2 -0
- package/dist/bin.js +5 -2960
- package/dist/bin.js.map +1 -1
- package/dist/{bounded-fs-Cpn129A4.js → bounded-fs-CC_NDtoP.js} +3 -3
- package/dist/{bounded-fs-Cpn129A4.js.map → bounded-fs-CC_NDtoP.js.map} +1 -1
- package/dist/check-screens.tsx +5 -5
- package/dist/{ci-install-BPA6gzEq.js → ci-install-DfcAMhlu.js} +9 -8
- package/dist/ci-install-DfcAMhlu.js.map +1 -0
- package/dist/{constants-CuJfQEcC.js → constants-DO4X70po.js} +3 -3
- package/dist/{constants-CuJfQEcC.js.map → constants-DO4X70po.js.map} +1 -1
- package/dist/{debug-B9k4ideV.js → debug-BueK5mRR.js} +1 -1
- package/dist/{debug-Cj-8vfXY.js → debug-DyARf5TF.js} +3 -3
- package/dist/debug-DyARf5TF.js.map +1 -0
- package/dist/{defaults-C6J7yL4O.js → defaults-DpfMnH2q.js} +2 -2
- package/dist/defaults-DpfMnH2q.js.map +1 -0
- package/dist/emit-BSetXzDV.js +69 -0
- package/dist/emit-BSetXzDV.js.map +1 -0
- package/dist/{endpoints-DnIX0pZH.js → endpoints-D-4KxZiB.js} +3 -3
- package/dist/{endpoints-DnIX0pZH.js.map → endpoints-D-4KxZiB.js.map} +1 -1
- package/dist/env-api-key-CHGLe0iz.js +2 -0
- package/dist/{env-api-key-BWdKpGEr.js → env-api-key-COf0U-Au.js} +3 -5
- package/dist/{env-api-key-BWdKpGEr.js.map → env-api-key-COf0U-Au.js.map} +1 -1
- package/dist/{environment-DZYUSCpV.js → environment-DrLaSsA8.js} +5 -10
- package/dist/{environment-DZYUSCpV.js.map → environment-DrLaSsA8.js.map} +1 -1
- package/dist/environment-hBj7ngmO.js +2 -0
- package/dist/{errors-ddDE6MbA.js → errors-C2domSkK.js} +3 -67
- package/dist/errors-C2domSkK.js.map +1 -0
- package/dist/{file-dEhr-l3n.js → file-CVA9ocIC.js} +4 -4
- package/dist/file-CVA9ocIC.js.map +1 -0
- package/dist/file-CmoKK_Pp.js +2 -0
- package/dist/{file-watcher-CPM-Nn9L.js → file-watcher-C3Su8vXm.js} +3 -3
- package/dist/file-watcher-C3Su8vXm.js.map +1 -0
- package/dist/{gateway-CVU1G0H3.js → gateway-BmjYUSua.js} +8 -7
- package/dist/{gateway-CVU1G0H3.js.map → gateway-BmjYUSua.js.map} +1 -1
- package/dist/{headless-ui-pY2xB5ug.js → headless-ui-CUbv12Yx.js} +3 -3
- package/dist/headless-ui-CUbv12Yx.js.map +1 -0
- package/dist/host-resolution-DVSb7kXb.js +114 -0
- package/dist/host-resolution-DVSb7kXb.js.map +1 -0
- package/dist/{add-mcp-server-to-clients-amlJWW7W.js → install-Bk5TNk27.js} +16 -26
- package/dist/install-Bk5TNk27.js.map +1 -0
- package/dist/install-F4wm3RSl.js +2 -0
- package/dist/interactive-C5LNvDj6.js +11 -0
- package/dist/interactive-C5LNvDj6.js.map +1 -0
- package/dist/{telemetry-Bpv-Mh5B.js → links-9LWZPSCq.js} +4 -13
- package/dist/links-9LWZPSCq.js.map +1 -0
- package/dist/{logging-ui-CUxNYF2t.js → logging-ui-p6XKVmTx.js} +6 -6
- package/dist/logging-ui-p6XKVmTx.js.map +1 -0
- package/dist/main-DKPjFHEV.js +1404 -0
- package/dist/main-DKPjFHEV.js.map +1 -0
- package/dist/{mcp-JcsBBnWv.js → mcp-Co8VRF62.js} +4 -4
- package/dist/{mcp-JcsBBnWv.js.map → mcp-Co8VRF62.js.map} +1 -1
- package/dist/mcp-install-smoke-test.ts +1 -1
- package/dist/{mcp-prompt-streaming-B-p_Pvb2.js → mcp-prompt-streaming-ljCVoQ9I.js} +6 -6
- package/dist/{mcp-prompt-streaming-B-p_Pvb2.js.map → mcp-prompt-streaming-ljCVoQ9I.js.map} +1 -1
- package/dist/{mint-failure-CZFZpHpW.js → mint-failure-C0HyvEwO.js} +105 -93
- package/dist/mint-failure-C0HyvEwO.js.map +1 -0
- package/dist/{models-DnAY67Lg.js → models-Dba8ITeo.js} +2 -2
- package/dist/{models-DnAY67Lg.js.map → models-Dba8ITeo.js.map} +1 -1
- package/dist/{non-interactive-DR1CWXY1.js → non-interactive-CLMJrj_7.js} +5 -4
- package/dist/non-interactive-CLMJrj_7.js.map +1 -0
- package/dist/{oauth-session-CQTFOk-I.js → oauth-session-D-RfcVmr.js} +2 -2
- package/dist/{oauth-session-CQTFOk-I.js.map → oauth-session-D-RfcVmr.js.map} +1 -1
- package/dist/{orchestrator-tools-CBdt-SXO.js → orchestrator-tools-CVJ1F9Ub.js} +3 -3
- package/dist/{orchestrator-tools-CBdt-SXO.js.map → orchestrator-tools-CVJ1F9Ub.js.map} +1 -1
- package/dist/package-json--ppioh2F.js +2 -0
- package/dist/{package-json-DY70kkNp.js → package-json-Cowya9-3.js} +2 -8
- package/dist/{package-json-DY70kkNp.js.map → package-json-Cowya9-3.js.map} +1 -1
- package/dist/{pi-BtqcLX3y.js → pi-B5YTBRDv.js} +20 -18
- package/dist/pi-B5YTBRDv.js.map +1 -0
- package/dist/{playground-Dz-ZVjBk.js → playground-BLsCfHmQ.js} +33 -33
- package/dist/playground-BLsCfHmQ.js.map +1 -0
- package/dist/posthog-B2E0hpZt.js +2 -0
- package/dist/{posthog-6Uqw_Eux.js → posthog-B42rt_1S.js} +4 -9
- package/dist/posthog-B42rt_1S.js.map +1 -0
- package/dist/project-data--L-WZyvj.js +2 -0
- package/dist/{setup-utils-dCViCC6s.js → project-data-BO9IXn9y.js} +566 -957
- package/dist/project-data-BO9IXn9y.js.map +1 -0
- package/dist/provisioning-C01REIU2.js +2 -0
- package/dist/{provisioning-Zf7yPcEs.js → provisioning-CLhuKqXH.js} +6 -73
- package/dist/provisioning-CLhuKqXH.js.map +1 -0
- package/dist/{queue-tools-B7_aCyd-.js → queue-tools-knnPQP0e.js} +4 -4
- package/dist/{queue-tools-B7_aCyd-.js.map → queue-tools-knnPQP0e.js.map} +1 -1
- package/dist/{registry-B6iLGorH.js → registry-B3rC5U4b.js} +7 -9
- package/dist/{registry-B6iLGorH.js.map → registry-B3rC5U4b.js.map} +1 -1
- package/dist/registry-BamsM7J9.js +2 -0
- package/dist/run-agent-legacy-CwUmPfQF.js +2 -0
- package/dist/{run-agent-legacy-6WcYMaR9.js → run-agent-legacy-DWBajsMh.js} +40 -114
- package/dist/run-agent-legacy-DWBajsMh.js.map +1 -0
- package/dist/runners-D1PqqpjA.js +834 -0
- package/dist/runners-D1PqqpjA.js.map +1 -0
- package/dist/{security-lkBwWbrK.js → security-BvdSFDuM.js} +5 -5
- package/dist/{security-lkBwWbrK.js.map → security-BvdSFDuM.js.map} +1 -1
- package/dist/setup-utils-ChXXTi1m.js +301 -0
- package/dist/setup-utils-ChXXTi1m.js.map +1 -0
- package/dist/{start-tui-oCWMgsDP.js → start-tui-DicLfgsj.js} +95 -91
- package/dist/start-tui-DicLfgsj.js.map +1 -0
- package/dist/{steps-gx0Pq4I4.js → steps-Hc28zVc_.js} +10 -10
- package/dist/steps-Hc28zVc_.js.map +1 -0
- package/dist/store-COCO6U0P.js +5 -0
- package/dist/{store-xYy_6Ruv.js → store-D3509G87.js} +67 -35
- package/dist/store-D3509G87.js.map +1 -0
- package/dist/{subagent-Bfy0sfhU.js → subagent-D_Sqak94.js} +3 -3
- package/dist/{subagent-Bfy0sfhU.js.map → subagent-D_Sqak94.js.map} +1 -1
- package/dist/{task-BOfw7GPc.js → task-DiTI6Sak.js} +19 -17
- package/dist/task-DiTI6Sak.js.map +1 -0
- package/dist/{wizard-ui-DoyOZxK8.js → task-status-CmrrR7zo.js} +2 -2
- package/dist/task-status-CmrrR7zo.js.map +1 -0
- package/dist/{task-stream-kYlm9qd0.js → task-stream-BckhG_cB.js} +11 -11
- package/dist/task-stream-BckhG_cB.js.map +1 -0
- package/dist/{tasks-Bbv_lOBG.js → tasks-ChZ_F1YP.js} +1 -1
- package/dist/{tasks-Bbv_lOBG.js.map → tasks-ChZ_F1YP.js.map} +1 -1
- package/dist/telemetry-BNKsDxNS.js +13 -0
- package/dist/telemetry-BNKsDxNS.js.map +1 -0
- package/dist/{terminal-D07EQ2y0.js → terminal-PK9bCLRi.js} +123 -97
- package/dist/terminal-PK9bCLRi.js.map +1 -0
- package/dist/{tools-Cc41af5W.js → tools-C8LQ8QFz.js} +9 -9
- package/dist/tools-C8LQ8QFz.js.map +1 -0
- package/dist/{ui-CGXtC7Vn.js → ui-tPNpM6vj.js} +3 -3
- package/dist/ui-tPNpM6vj.js.map +1 -0
- package/dist/urls-BI4kJ4qQ.js +67 -0
- package/dist/urls-BI4kJ4qQ.js.map +1 -0
- package/dist/{version-Cadof0Z_.js → version-5_ydHkM6.js} +2 -2
- package/dist/{version-Cadof0Z_.js.map → version-5_ydHkM6.js.map} +1 -1
- package/dist/wizard-abort-BVjqLyeA.js +3 -0
- package/dist/{wizard-abort-HDbP3YcS.js → wizard-abort-DBKtfnBR.js} +20 -17
- package/dist/wizard-abort-DBKtfnBR.js.map +1 -0
- package/dist/wizard-session-B7BHnay0.js +2 -0
- package/dist/{wizard-session-2w4Ru-Wz.js → wizard-session-BYw6LzpS.js} +30 -28
- package/dist/wizard-session-BYw6LzpS.js.map +1 -0
- package/dist/{yara-hooks-DCfROpZt.js → yara-hooks-C3L6lBij.js} +5 -5
- package/dist/{yara-hooks-DCfROpZt.js.map → yara-hooks-C3L6lBij.js.map} +1 -1
- package/dist/yara-hooks-CF_IsoaA.js +2 -0
- package/package.json +2 -3
- package/dist/add-mcp-server-to-clients-amlJWW7W.js.map +0 -1
- package/dist/agent-CNrEC-9v.js.map +0 -1
- package/dist/agent-interface-BXvmAm4c.js.map +0 -1
- package/dist/analytics-DxwRi2Hf.js +0 -2
- package/dist/analytics-VRYdebXW.js.map +0 -1
- package/dist/ci-install-BPA6gzEq.js.map +0 -1
- package/dist/debug-Cj-8vfXY.js.map +0 -1
- package/dist/defaults-C6J7yL4O.js.map +0 -1
- package/dist/errors-ddDE6MbA.js.map +0 -1
- package/dist/file-dEhr-l3n.js.map +0 -1
- package/dist/file-watcher-CPM-Nn9L.js.map +0 -1
- package/dist/headless-ui-pY2xB5ug.js.map +0 -1
- package/dist/interactive-BOGlinZe.js +0 -11
- package/dist/interactive-BOGlinZe.js.map +0 -1
- package/dist/logging-ui-CUxNYF2t.js.map +0 -1
- package/dist/mint-failure-CZFZpHpW.js.map +0 -1
- package/dist/non-interactive-DR1CWXY1.js.map +0 -1
- package/dist/pi-BtqcLX3y.js.map +0 -1
- package/dist/playground-Dz-ZVjBk.js.map +0 -1
- package/dist/posthog-6Uqw_Eux.js.map +0 -1
- package/dist/provisioning-Zf7yPcEs.js.map +0 -1
- package/dist/rolldown-runtime-B_-DWIq7.js +0 -15
- package/dist/run-agent-legacy-6WcYMaR9.js.map +0 -1
- package/dist/setup-utils-dCViCC6s.js.map +0 -1
- package/dist/start-tui-oCWMgsDP.js.map +0 -1
- package/dist/steps-gx0Pq4I4.js.map +0 -1
- package/dist/store-xYy_6Ruv.js.map +0 -1
- package/dist/task-BOfw7GPc.js.map +0 -1
- package/dist/task-stream-kYlm9qd0.js.map +0 -1
- package/dist/telemetry-Bpv-Mh5B.js.map +0 -1
- package/dist/terminal-D07EQ2y0.js.map +0 -1
- package/dist/tools-Cc41af5W.js.map +0 -1
- package/dist/ui-CGXtC7Vn.js.map +0 -1
- package/dist/wizard-abort-HDbP3YcS.js.map +0 -1
- package/dist/wizard-abort-Y-Ip2K4z.js +0 -3
- package/dist/wizard-session-2w4Ru-Wz.js.map +0 -1
- package/dist/wizard-session-DGaUHOF3.js +0 -2
- package/dist/wizard-ui-DoyOZxK8.js.map +0 -1
- package/dist/yara-hooks-B49RIk5d.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yara-hooks-DCfROpZt.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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/wizard",
|
|
3
|
-
"version": "2.
|
|
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": "^
|
|
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":"add-mcp-server-to-clients-amlJWW7W.js","names":["fs","path","path","os","PLUGIN_MARKETPLACE","PLUGIN_MARKETPLACE_SOURCE","PLUGIN_REF","RUN_TIMEOUT_MS","MAX_OUTPUT_BYTES","path","os","fs","execSync","z","os","path","z","os","path","path","fs","z","path","fs","os"],"sources":["../src/steps/add-mcp-server-to-clients/MCPClient.ts","../src/steps/add-mcp-server-to-clients/clients/cursor.ts","../src/steps/add-mcp-server-to-clients/clients/claude-code.ts","../src/steps/add-mcp-server-to-clients/clients/claude-web.ts","../src/steps/add-mcp-server-to-clients/clients/visual-studio-code.ts","../src/steps/add-mcp-server-to-clients/clients/zed.ts","../src/steps/add-mcp-server-to-clients/clients/codex.ts","../src/steps/add-mcp-server-to-clients/clients/opencode.ts","../src/steps/add-mcp-server-to-clients/plugin-client.ts","../src/steps/add-mcp-server-to-clients/index.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as jsonc from 'jsonc-parser';\nimport { getDefaultServerConfig } from './defaults';\nimport type { InstallResult } from './results';\n\nexport type MCPServerConfig = Record<string, unknown>;\n\nexport abstract class MCPClient {\n name: string;\n abstract getConfigPath(): Promise<string>;\n abstract getServerPropertyName(): string;\n abstract isServerInstalled(local?: boolean): Promise<boolean>;\n abstract addServer(\n apiKey?: string,\n selectedFeatures?: string[],\n local?: boolean,\n ): Promise<InstallResult>;\n abstract removeServer(local?: boolean): Promise<InstallResult>;\n abstract isClientSupported(): Promise<boolean>;\n}\n\nexport abstract class DefaultMCPClient extends MCPClient {\n name = 'Default';\n\n constructor() {\n super();\n }\n\n getServerPropertyName(): string {\n return 'mcpServers';\n }\n\n getServerConfig(\n apiKey: string | undefined,\n selectedFeatures?: string[],\n local?: boolean,\n ): MCPServerConfig {\n return getDefaultServerConfig(apiKey, selectedFeatures, local);\n }\n\n async isServerInstalled(local?: boolean): Promise<boolean> {\n try {\n const configPath = await this.getConfigPath();\n\n if (!fs.existsSync(configPath)) {\n return false;\n }\n\n const configContent = await fs.promises.readFile(configPath, 'utf8');\n const config = jsonc.parse(configContent) as Record<string, any>;\n const serverPropertyName = this.getServerPropertyName();\n const serverName = local ? 'posthog-local' : 'posthog';\n\n return (\n serverPropertyName in config && serverName in config[serverPropertyName]\n );\n } catch {\n return false;\n }\n }\n\n async addServer(\n apiKey?: string,\n selectedFeatures?: string[],\n local?: boolean,\n ): Promise<InstallResult> {\n try {\n const configPath = await this.getConfigPath();\n const configDir = path.dirname(configPath);\n\n await fs.promises.mkdir(configDir, { recursive: true });\n\n const serverPropertyName = this.getServerPropertyName();\n let configContent = '';\n let existingConfig = {};\n\n if (fs.existsSync(configPath)) {\n configContent = await fs.promises.readFile(configPath, 'utf8');\n existingConfig = jsonc.parse(configContent) || {};\n }\n\n const newServerConfig = this.getServerConfig(\n apiKey,\n selectedFeatures,\n local,\n );\n const typedConfig = existingConfig as Record<string, any>;\n if (!typedConfig[serverPropertyName]) {\n typedConfig[serverPropertyName] = {};\n }\n const serverName = local ? 'posthog-local' : 'posthog';\n\n // An identical entry means this config is already set up — leave the file\n // untouched and report it, so a re-run says \"already installed\" instead of\n // claiming a write that changed nothing. A differing entry (new features,\n // new key) still gets overwritten and reported as installed.\n const existingServerConfig = typedConfig[serverPropertyName][serverName];\n if (\n existingServerConfig !== undefined &&\n JSON.stringify(existingServerConfig) === JSON.stringify(newServerConfig)\n ) {\n return { success: true, alreadyInstalled: true };\n }\n\n typedConfig[serverPropertyName][serverName] = newServerConfig;\n\n const edits = jsonc.modify(\n configContent,\n [serverPropertyName, serverName],\n newServerConfig,\n {\n formattingOptions: {\n tabSize: 2,\n insertSpaces: true,\n },\n },\n );\n\n const modifiedContent = jsonc.applyEdits(configContent, edits);\n\n await fs.promises.writeFile(configPath, modifiedContent, 'utf8');\n\n return { success: true };\n } catch (error) {\n return {\n success: false,\n reason: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n async removeServer(local?: boolean): Promise<InstallResult> {\n let configPath = '';\n try {\n configPath = await this.getConfigPath();\n\n if (!fs.existsSync(configPath)) {\n return { success: true, alreadyInstalled: true };\n }\n\n const configContent = await fs.promises.readFile(configPath, 'utf8');\n const config = jsonc.parse(configContent) as Record<string, any>;\n const serverPropertyName = this.getServerPropertyName();\n\n const serverName = local ? 'posthog-local' : 'posthog';\n\n if (\n serverPropertyName in config &&\n serverName in config[serverPropertyName]\n ) {\n const edits = jsonc.modify(\n configContent,\n [serverPropertyName, serverName],\n undefined,\n {\n formattingOptions: {\n tabSize: 2,\n insertSpaces: true,\n },\n },\n );\n\n const modifiedContent = jsonc.applyEdits(configContent, edits);\n\n await fs.promises.writeFile(configPath, modifiedContent, 'utf8');\n\n return { success: true };\n }\n // No PostHog entry to delete — the config changed under us between the\n // detection pass and now. Nothing failed, so don't report a failure.\n return { success: true, alreadyInstalled: true };\n } catch (error) {\n return {\n success: false,\n reason: `${configPath}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n };\n }\n }\n}\n","import {\n DefaultMCPClient,\n MCPServerConfig,\n} from '@steps/add-mcp-server-to-clients/MCPClient';\nimport * as path from 'path';\nimport * as os from 'os';\nimport {\n DefaultMCPClientConfig,\n getNativeHTTPServerConfig,\n} from '@steps/add-mcp-server-to-clients/defaults';\nimport { z } from 'zod';\n\nexport const CursorMCPConfig = DefaultMCPClientConfig;\n\nexport type CursorMCPConfig = z.infer<typeof DefaultMCPClientConfig>;\n\nexport class CursorMCPClient extends DefaultMCPClient {\n name = 'Cursor';\n\n constructor() {\n super();\n }\n\n async isClientSupported(): Promise<boolean> {\n return Promise.resolve(\n process.platform === 'darwin' || process.platform === 'win32',\n );\n }\n\n async getConfigPath(): Promise<string> {\n return Promise.resolve(path.join(os.homedir(), '.cursor', 'mcp.json'));\n }\n\n getServerConfig(\n apiKey: string | undefined,\n selectedFeatures?: string[],\n local?: boolean,\n ): MCPServerConfig {\n return getNativeHTTPServerConfig(apiKey, selectedFeatures, local);\n }\n}\n","import { DefaultMCPClient } from '@steps/add-mcp-server-to-clients/MCPClient';\nimport {\n DefaultMCPClientConfig,\n buildMCPUrl,\n} from '@steps/add-mcp-server-to-clients/defaults';\nimport {\n PluginCapable,\n PluginInstallResult,\n} from '@steps/add-mcp-server-to-clients/plugin-client';\nimport {\n redactSecrets,\n scrubHomePaths,\n type InstallResult,\n} from '@steps/add-mcp-server-to-clients/results';\nimport { LoginCapable } from '@steps/add-mcp-server-to-clients/login-client';\nimport { z } from 'zod';\nimport { execSync, execFile } from 'child_process';\nimport { analytics } from '@utils/analytics';\nimport { debug } from '@utils/debug';\nimport * as os from 'os';\nimport * as path from 'path';\nimport * as fs from 'fs';\n\nexport const ClaudeCodeMCPConfig = DefaultMCPClientConfig;\n\nexport type ClaudeCodeMCPConfig = z.infer<typeof DefaultMCPClientConfig>;\n\n// `plugin install <name>` only resolves against marketplaces the user already\n// registered, and the wizard never opens Claude Code interactively — so without\n// the add below the install dies with `not found in any configured marketplace`.\n// Our own marketplace, not Anthropic's: that one carries us as a pinned sha, so\n// installing through it would gate plugin releases on Anthropic bumping the pin.\nconst PLUGIN_NAME = 'posthog';\nconst PLUGIN_MARKETPLACE = 'posthog';\nconst PLUGIN_MARKETPLACE_SOURCE = 'PostHog/ai-plugin';\nconst PLUGIN_REF = `${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}`;\n\n/** The plugin is missing from every catalog the CLI can currently see. */\nconst NOT_IN_A_MARKETPLACE = /not found in/i;\n\n/** One `plugin list --json` row; the CLI returns more fields than we read. */\ninterface ListedPlugin {\n /** `<plugin>@<marketplace>`, e.g. `posthog@posthog`. */\n id: string;\n /**\n * A plugin disabled in `/plugin` stays listed with `enabled: false`. It is\n * still installed, but it contributes no MCP server, so treating it as\n * installed reports \"you are good to go\" with nothing serving posthog.\n */\n enabled?: boolean;\n}\n\ninterface ListedMarketplace {\n name: string;\n /**\n * `owner/repo`, present since the CLI started emitting `--json`. A\n * marketplace merely *named* `posthog` may be someone else's, and installing\n * from it would ship a different plugin under our name.\n */\n repo?: string;\n}\n\ntype ClaudeRun = { ok: boolean; output: string };\n\n/**\n * `plugin marketplace add` clones a git repository, the slowest thing we run.\n * Past this the command is not slow, it is stuck, and a clone waiting on a\n * credential prompt would otherwise hang the wizard with the spinner frozen.\n */\nconst RUN_TIMEOUT_MS = 120_000;\n\n/**\n * `execFile` kills the child once output passes this. The default is 1 MB,\n * which a verbose clone can reach, and the kill then looks like Claude Code\n * rejecting the install rather than us cutting it off.\n */\nconst MAX_OUTPUT_BYTES = 16 * 1024 * 1024;\n\n/**\n * Our catalog, by name *and* repository. A marketplace merely called `posthog`\n * may be someone else's, and neither installing from it nor removing it is\n * ours to do. `repo` is absent on CLIs predating it, where the name is all\n * there is.\n */\nconst isOurMarketplace = (m: ListedMarketplace | undefined): boolean =>\n m?.name === PLUGIN_MARKETPLACE &&\n (m.repo === undefined ||\n m.repo.toLowerCase() === PLUGIN_MARKETPLACE_SOURCE.toLowerCase());\n\nexport class ClaudeCodeMCPClient\n extends DefaultMCPClient\n implements PluginCapable, LoginCapable\n{\n name = 'Claude Code';\n private claudeBinaryPath: string | null = null;\n\n constructor() {\n super();\n }\n\n private findClaudeBinary(): string | null {\n if (this.claudeBinaryPath) {\n return this.claudeBinaryPath;\n }\n\n // Common installation paths for Claude Code CLI\n const possiblePaths = [\n path.join(os.homedir(), '.claude', 'local', 'claude'),\n '/usr/local/bin/claude',\n '/opt/homebrew/bin/claude',\n ];\n\n for (const claudePath of possiblePaths) {\n if (fs.existsSync(claudePath)) {\n debug(` Found claude binary at: ${claudePath}`);\n this.claudeBinaryPath = claudePath;\n return claudePath;\n }\n }\n\n // Try PATH as fallback\n try {\n execSync('command -v claude', { stdio: 'pipe' });\n debug(' Found claude in PATH');\n this.claudeBinaryPath = 'claude';\n return 'claude';\n } catch {\n // Not in PATH\n }\n\n return null;\n }\n\n isClientSupported(): Promise<boolean> {\n try {\n debug(' Checking for Claude Code...');\n const claudeBinary = this.findClaudeBinary();\n\n if (!claudeBinary) {\n debug(' Claude Code not found. Installation paths checked:');\n debug(` - ${path.join(os.homedir(), '.claude', 'local', 'claude')}`);\n debug(` - /usr/local/bin/claude`);\n debug(` - /opt/homebrew/bin/claude`);\n debug(` - PATH`);\n return Promise.resolve(false);\n }\n\n const output = execSync(`${claudeBinary} --version`, { stdio: 'pipe' });\n const version = output.toString().trim();\n debug(` Claude Code detected: ${version}`);\n return Promise.resolve(true);\n } catch (error) {\n debug(\n ` Claude Code check failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return Promise.resolve(false);\n }\n }\n\n isServerInstalled(local?: boolean): Promise<boolean> {\n const binary = this.findClaudeBinary();\n if (!binary) return Promise.resolve(false);\n const serverName = local ? 'posthog-local' : 'posthog';\n // `mcp get <name>` exits non-zero when the entry doesn't exist. A substring\n // scan of `mcp list` also matches the claude.ai \"PostHog\" connector and the\n // plugin's `plugin:posthog:posthog`, reporting installed forever.\n try {\n execSync(`${binary} mcp get ${serverName}`, { stdio: 'pipe' });\n return Promise.resolve(true);\n } catch {\n return Promise.resolve(false);\n }\n }\n\n getConfigPath(): Promise<string> {\n throw new Error('Not implemented');\n }\n\n addServer(\n apiKey?: string,\n selectedFeatures?: string[],\n local?: boolean,\n ): Promise<InstallResult> {\n const binary = this.findClaudeBinary();\n if (!binary)\n return Promise.resolve({\n success: false,\n reason: 'The claude CLI is no longer on your PATH.',\n });\n\n const serverName = local ? 'posthog-local' : 'posthog';\n const url = buildMCPUrl(selectedFeatures, local);\n const args = [\n 'mcp',\n 'add',\n '--transport',\n 'http',\n '--scope',\n 'user',\n serverName,\n url,\n ];\n if (apiKey) {\n args.push('--header', `Authorization: Bearer ${apiKey}`);\n }\n\n try {\n execSync(`${binary} ${args.map((a) => JSON.stringify(a)).join(' ')}`, {\n stdio: 'pipe',\n });\n return Promise.resolve({ success: true });\n } catch (error) {\n // The failing command echoes back the Authorization header we passed, so\n // redact before this reaches a log, the screen, or an exception report.\n const msg = redactSecrets(\n error instanceof Error ? error.message : String(error),\n );\n if (msg.includes('already exists')) {\n return Promise.resolve({ success: true, alreadyInstalled: true });\n }\n analytics.captureException(\n new Error(`Claude Code MCP add failed: ${msg}`),\n );\n return Promise.resolve({ success: false, reason: msg });\n }\n }\n\n /** Claude Code's own login runs its OAuth and owns the token; the wizard only surfaces the command. */\n loginCommand(local?: boolean): string {\n return `claude mcp login ${local ? 'posthog-local' : 'posthog'}`;\n }\n\n /** The plugin bundles its own `posthog` server, addressed as `plugin:<plugin>:<server>`. */\n pluginLoginCommand(): string {\n return 'claude mcp login plugin:posthog:posthog';\n }\n\n async removePlugin(): Promise<PluginInstallResult> {\n const binary = this.findClaudeBinary();\n if (!binary)\n return {\n success: false,\n reason: 'The claude CLI is no longer on your PATH.',\n };\n\n // `plugin uninstall posthog` removes exactly one match, so with both\n // `posthog@posthog` and `posthog@claude-plugins-official` installed the bare\n // call leaves the other copy enabled — and `mcp remove` would be lying.\n const ids = await this.installedPluginIds(binary);\n // `installPlugin` registers the marketplace, so removal owes the user the\n // other half: a machine left with our catalog and no plugin is a state the\n // wizard created. `codex.ts` clears both on the same call.\n const marketplace = await this.ourMarketplaceRegistered(binary);\n\n const nothingInstalled =\n ids?.length === 0 || (ids === null && !(await this.isPluginInstalled()));\n if (nothingInstalled && !marketplace) {\n return { success: true, alreadyInstalled: true };\n }\n\n const targets = nothingInstalled ? [] : ids?.length ? ids : [PLUGIN_NAME];\n const failures: string[] = [];\n for (const target of targets) {\n const run = await this.runClaude(binary, ['plugin', 'uninstall', target]);\n if (!run.ok) failures.push(`${target}: ${run.output}`);\n }\n\n // Only when we know it is there: `marketplace remove` exits non-zero on a\n // marketplace that is absent, which would report a failure for a no-op.\n if (marketplace) {\n const run = await this.runClaude(binary, [\n 'plugin',\n 'marketplace',\n 'remove',\n PLUGIN_MARKETPLACE,\n ]);\n if (!run.ok) failures.push(`${PLUGIN_MARKETPLACE}: ${run.output}`);\n }\n\n if (failures.length === 0) return { success: true };\n\n const reason = failures.join('; ');\n analytics.captureException(\n new Error(`Claude Code plugin uninstall failed: ${reason}`),\n );\n return { success: false, reason };\n }\n\n removeServer(local?: boolean): Promise<InstallResult> {\n const claudeBinary = this.findClaudeBinary();\n if (!claudeBinary) {\n return Promise.resolve({\n success: false,\n reason: 'The claude CLI is no longer on your PATH.',\n });\n }\n\n const serverName = local ? 'posthog-local' : 'posthog';\n const command = `${claudeBinary} mcp remove --scope user ${serverName}`;\n\n try {\n execSync(command, { stdio: 'pipe' });\n } catch (error) {\n const reason = redactSecrets(\n error instanceof Error ? error.message : String(error),\n );\n // Removing something that isn't there is the requested end state, not a\n // failure to report.\n if (/no( such)? mcp server|not found/i.test(reason)) {\n return Promise.resolve({ success: true, alreadyInstalled: true });\n }\n analytics.captureException(\n new Error(`Failed to remove server from Claude Code: ${reason}`),\n );\n return Promise.resolve({ success: false, reason });\n }\n\n return Promise.resolve({ success: true });\n }\n\n /** The plugin ships mcp.json with the posthog server — no direct entry needed. */\n pluginBundlesMcpServer(): boolean {\n return true;\n }\n\n supportsPlugin(): boolean {\n return this.findClaudeBinary() !== null;\n }\n\n // Async on purpose: these calls clone git repos and take seconds, and\n // execSync would block the event loop and freeze the TUI spinner. Never\n // throws; the caller decides what a failure means.\n private runClaude(binary: string, args: string[]): Promise<ClaudeRun> {\n return new Promise((resolve) => {\n execFile(\n binary,\n args,\n { timeout: RUN_TIMEOUT_MS, maxBuffer: MAX_OUTPUT_BYTES },\n (error, stdout) => {\n if (!error)\n return resolve({ ok: true, output: stdout?.toString() ?? '' });\n if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER')\n return resolve({\n ok: false,\n output:\n 'claude produced more output than the wizard can hold, so it was stopped',\n });\n if (error.killed || error.signal)\n return resolve({\n ok: false,\n output: `claude did not finish within ${\n RUN_TIMEOUT_MS / 1000\n }s, so it was stopped`,\n });\n // execFile puts `Command failed: <cmd>` and stderr in the message, the\n // same text execSync produced, so failure matching is unchanged.\n resolve({\n ok: false,\n output: scrubHomePaths(redactSecrets(error.message)),\n });\n },\n );\n });\n }\n\n /** Null when `--json` is unsupported, the command failed, or output didn't parse — never \"nothing installed\". */\n private async listJson<T>(\n binary: string,\n args: string[],\n ): Promise<T[] | null> {\n const result = await this.runClaude(binary, [...args, '--json']);\n if (!result.ok) return null;\n try {\n const parsed: unknown = JSON.parse(result.output);\n return Array.isArray(parsed) ? (parsed as T[]) : null;\n } catch {\n return null;\n }\n }\n\n // Every installed copy, from any marketplace: Claude Code permits\n // `posthog@posthog` and `posthog@claude-plugins-official` at the same time.\n // Null means the probe is unavailable, which is not the same as none.\n private async installedPlugins(\n binary: string,\n ): Promise<ListedPlugin[] | null> {\n const listed = await this.listJson<ListedPlugin>(binary, [\n 'plugin',\n 'list',\n ]);\n if (!listed) return null;\n return listed.filter(\n (p): p is ListedPlugin =>\n typeof p?.id === 'string' && p.id.split('@')[0] === PLUGIN_NAME,\n );\n }\n\n private async installedPluginIds(binary: string): Promise<string[] | null> {\n const listed = await this.installedPlugins(binary);\n return listed?.map((p) => p.id) ?? null;\n }\n\n async isPluginInstalled(): Promise<boolean> {\n const binary = this.findClaudeBinary();\n if (!binary) return false;\n\n // Presence, not health: `index.ts` asks this to decide whether there is\n // anything to remove, and a disabled plugin still has to be uninstalled.\n // Whether it actually serves is `installPlugin`'s question, not this one.\n const listed = await this.installedPlugins(binary);\n if (listed) return listed.length > 0;\n\n // Older CLI without `--json`. A substring scan also matches a plugin merely\n // named `posthog-something`, which is why it's the fallback and not the path.\n const plain = await this.runClaude(binary, ['plugin', 'list']);\n return plain.ok && plain.output.toLowerCase().includes(PLUGIN_NAME);\n }\n\n async installPlugin(): Promise<PluginInstallResult> {\n const binary = this.findClaudeBinary();\n if (!binary)\n return {\n success: false,\n reason: 'The claude CLI is no longer on your PATH.',\n };\n\n // Ask before installing so a re-run reports \"already installed\" rather than\n // relying on the CLI's error text to spot the no-op.\n //\n // A plugin disabled in `/plugin` is installed but serves nothing, and\n // Claude Code takes its MCP server from the plugin. Reporting it as\n // already installed leaves the user with no server and a screen saying\n // they are done, so fall through and install: the CLI re-enables it.\n const listed = await this.installedPlugins(binary);\n const serving = listed\n ? listed.some((p) => p.enabled !== false)\n : await this.isPluginInstalled();\n if (serving) {\n return { success: true, alreadyInstalled: true };\n }\n\n const marketplaceFailure = await this.ensurePluginMarketplace(binary);\n\n let result = await this.runClaude(binary, [\n 'plugin',\n 'install',\n PLUGIN_REF,\n ]);\n\n // A registered but stale catalog still reports the plugin as missing.\n if (!result.ok && NOT_IN_A_MARKETPLACE.test(result.output)) {\n await this.runClaude(binary, [\n 'plugin',\n 'marketplace',\n 'update',\n PLUGIN_MARKETPLACE,\n ]);\n result = await this.runClaude(binary, ['plugin', 'install', PLUGIN_REF]);\n }\n\n // Last resort: let Claude Code resolve the bare name against whatever the\n // user does have, the way the wizard always did. Never worse than before.\n if (!result.ok && NOT_IN_A_MARKETPLACE.test(result.output)) {\n result = await this.runClaude(binary, ['plugin', 'install', PLUGIN_NAME]);\n }\n\n if (result.ok) return { success: true };\n\n const msg = result.output;\n if (msg.includes('already installed') || msg.includes('already exists')) {\n return { success: true, alreadyInstalled: true };\n }\n // `not found in any configured marketplace` is also what the pre-PR bug\n // produced, so without the marketplace-add failure beside it the new root\n // cause is indistinguishable from the old one in error tracking.\n const failure = new Error(`Claude Code plugin install failed: ${msg}`);\n if (marketplaceFailure) {\n analytics.captureException(failure, { marketplaceFailure });\n } else {\n analytics.captureException(failure);\n }\n return { success: false, reason: msg };\n }\n\n // Best-effort: a failure here only matters if the install also fails, since\n // the user may hold the plugin in another catalog that the fallback finds.\n /** Registered, and ours: a catalog merely named `posthog` is someone else's. */\n private async ourMarketplaceRegistered(\n binary: string,\n ): Promise<boolean | null> {\n const listed = await this.listJson<ListedMarketplace>(binary, [\n 'plugin',\n 'marketplace',\n 'list',\n ]);\n return listed ? listed.some(isOurMarketplace) : null;\n }\n\n private async ensurePluginMarketplace(\n binary: string,\n ): Promise<string | undefined> {\n const listed = await this.listJson<ListedMarketplace>(binary, [\n 'plugin',\n 'marketplace',\n 'list',\n ]);\n if (listed?.some(isOurMarketplace)) {\n debug(` Marketplace ${PLUGIN_MARKETPLACE} already registered`);\n return undefined;\n }\n\n // `marketplace add` is idempotent on Claude Code and exits 0 when the\n // marketplace is already on disk, so there's no \"already added\" text to\n // special-case here — unlike the Codex CLI, where it exits non-zero.\n const added = await this.runClaude(binary, [\n 'plugin',\n 'marketplace',\n 'add',\n PLUGIN_MARKETPLACE_SOURCE,\n ]);\n if (!added.ok) {\n debug(` Marketplace add failed: ${added.output}`);\n return added.output;\n }\n return undefined;\n }\n}\n","import { MCPClient } from '@steps/add-mcp-server-to-clients/MCPClient';\nimport type { InstallResult } from '@steps/add-mcp-server-to-clients/results';\nimport { BrowserFinishable } from '@steps/add-mcp-server-to-clients/browser-client';\nimport { openTrackedLink } from '@utils/links';\n\n/**\n * Claude Desktop / Claude.ai (web). PostHog ships here as a hosted connector,\n * not a local config — so instead of writing files we open the connector\n * directory page and let the user sign in and click \"Connect\".\n */\nexport class ClaudeWebMCPClient extends MCPClient implements BrowserFinishable {\n name = 'Claude Desktop/Web';\n connectorUrl = 'https://claude.ai/directory/connectors/posthog';\n finishInstruction = 'Sign in and click \"Connect\" to finish.';\n\n isClientSupported(): Promise<boolean> {\n // Browser-based — available on every platform.\n return Promise.resolve(true);\n }\n\n isServerInstalled(): Promise<boolean> {\n // The connector lives in the user's Claude account; nothing local to\n // inspect. Returning false also keeps it out of `mcp remove`.\n return Promise.resolve(false);\n }\n\n addServer(): Promise<InstallResult> {\n // Not a PostHog property, so no UTMs — just the tracked open.\n openTrackedLink(this.connectorUrl, 'claude-web-connector', {\n auto: true,\n skipUtm: true,\n });\n return Promise.resolve({ success: true });\n }\n\n removeServer(): Promise<InstallResult> {\n // The connector lives in the user's Claude account, so `mcp remove` can't\n // touch it. It never reaches here today (isServerInstalled is always\n // false), but say where to go if it ever does.\n return Promise.resolve({\n success: false,\n reason:\n 'This connector is managed in your Claude account — remove it at claude.ai/settings/connectors.',\n });\n }\n\n getConfigPath(): Promise<string> {\n throw new Error('Not implemented');\n }\n\n getServerPropertyName(): string {\n throw new Error('Not implemented');\n }\n}\n\nexport default ClaudeWebMCPClient;\n","import z from 'zod';\nimport * as path from 'path';\nimport * as os from 'os';\nimport {\n DefaultMCPClient,\n MCPServerConfig,\n} from '@steps/add-mcp-server-to-clients/MCPClient';\nimport { getNativeHTTPServerConfig } from '@steps/add-mcp-server-to-clients/defaults';\nimport { runtimeEnv } from '@env';\n\nexport const VisualStudioCodeMCPConfig = z\n .object({\n servers: z.record(\n z.string(),\n z.union([\n z.object({\n command: z.string().optional(),\n args: z.array(z.string()).optional(),\n env: z.record(z.string(), z.string()).optional(),\n }),\n z.object({\n type: z.enum(['http', 'sse']),\n url: z.string(),\n headers: z.record(z.string(), z.string()).optional(),\n }),\n ]),\n ),\n })\n .passthrough();\n\nexport type VisualStudioCodeMCPConfig = z.infer<\n typeof VisualStudioCodeMCPConfig\n>;\n\nexport class VisualStudioCodeClient extends DefaultMCPClient {\n name = 'Visual Studio Code';\n\n getServerPropertyName(): string {\n return 'servers';\n }\n\n async isClientSupported(): Promise<boolean> {\n return Promise.resolve(\n process.platform === 'darwin' ||\n process.platform === 'win32' ||\n process.platform === 'linux',\n );\n }\n\n async getConfigPath(): Promise<string> {\n const homeDir = os.homedir();\n const isWindows = process.platform === 'win32';\n const isMac = process.platform === 'darwin';\n const isLinux = process.platform === 'linux';\n\n if (isMac) {\n return Promise.resolve(\n path.join(\n homeDir,\n 'Library',\n 'Application Support',\n 'Code',\n 'User',\n 'mcp.json',\n ),\n );\n }\n\n if (isWindows) {\n return Promise.resolve(\n path.join(runtimeEnv('APPDATA') || '', 'Code', 'User', 'mcp.json'),\n );\n }\n\n if (isLinux) {\n return Promise.resolve(\n path.join(homeDir, '.config', 'Code', 'User', 'mcp.json'),\n );\n }\n\n throw new Error(`Unsupported platform: ${process.platform}`);\n }\n\n getServerConfig(\n apiKey: string | undefined,\n selectedFeatures?: string[],\n local?: boolean,\n ): MCPServerConfig {\n return {\n type: 'http',\n ...getNativeHTTPServerConfig(apiKey, selectedFeatures, local),\n };\n }\n}\n","import z from 'zod';\nimport * as path from 'path';\nimport * as os from 'os';\nimport {\n DefaultMCPClient,\n MCPServerConfig,\n} from '@steps/add-mcp-server-to-clients/MCPClient';\nimport { getNativeHTTPServerConfig } from '@steps/add-mcp-server-to-clients/defaults';\nimport { runtimeEnv } from '@env';\n\nexport const ZedMCPConfig = z\n .object({\n context_servers: z.record(\n z.string(),\n z.union([\n z.object({\n enabled: z.boolean().optional(),\n source: z.string().optional(),\n command: z.string().optional(),\n args: z.array(z.string()).optional(),\n env: z.record(z.string(), z.string()).optional(),\n }),\n z.object({\n enabled: z.boolean().optional(),\n url: z.string(),\n headers: z.record(z.string(), z.string()).optional(),\n }),\n ]),\n ),\n })\n .passthrough();\n\nexport type ZedMCPConfig = z.infer<typeof ZedMCPConfig>;\n\nexport class ZedClient extends DefaultMCPClient {\n name = 'Zed';\n\n getServerPropertyName(): string {\n return 'context_servers';\n }\n\n async isClientSupported(): Promise<boolean> {\n return Promise.resolve(\n process.platform === 'darwin' || process.platform === 'linux',\n );\n }\n\n async getConfigPath(): Promise<string> {\n const homeDir = os.homedir();\n const isMac = process.platform === 'darwin';\n const isLinux = process.platform === 'linux';\n\n if (isMac) {\n return Promise.resolve(\n path.join(homeDir, '.config', 'zed', 'settings.json'),\n );\n }\n\n if (isLinux) {\n // https://zed.dev/docs/configuring-zed#settings-files\n const xdgConfigHome = runtimeEnv('XDG_CONFIG_HOME');\n if (xdgConfigHome) {\n return Promise.resolve(\n path.join(xdgConfigHome, 'zed', 'settings.json'),\n );\n }\n return Promise.resolve(\n path.join(homeDir, '.config', 'zed', 'settings.json'),\n );\n }\n\n throw new Error(`Unsupported platform: ${process.platform}`);\n }\n\n getServerConfig(\n apiKey: string | undefined,\n selectedFeatures?: string[],\n local?: boolean,\n ): MCPServerConfig {\n return {\n enabled: true,\n ...getNativeHTTPServerConfig(apiKey, selectedFeatures, local),\n };\n }\n}\n","import { z } from 'zod';\nimport { execSync, execFile, type ExecFileException } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport { LoginCapable } from '@steps/add-mcp-server-to-clients/login-client';\nimport { DefaultMCPClient } from '@steps/add-mcp-server-to-clients/MCPClient';\nimport {\n DefaultMCPClientConfig,\n buildMCPUrl,\n} from '@steps/add-mcp-server-to-clients/defaults';\nimport {\n PluginCapable,\n PluginInstallResult,\n} from '@steps/add-mcp-server-to-clients/plugin-client';\nimport {\n redactSecrets,\n scrubHomePaths,\n expectedFailureHint,\n type ExpectedFailure,\n type InstallResult,\n} from '@steps/add-mcp-server-to-clients/results';\n\nimport { analytics } from '@utils/analytics';\n\nconst PLUGIN_MARKETPLACE = 'posthog';\nconst PLUGIN_MARKETPLACE_SOURCE = 'PostHog/ai-plugin';\nconst PLUGIN_REF = `posthog@${PLUGIN_MARKETPLACE}`;\n\n/**\n * The plugin's row in `codex plugin list`, whose STATUS column reads\n * `installed, enabled` or `not installed`. Registering the marketplace only\n * publishes the catalog, so the marketplace section in config.toml says nothing\n * about whether the plugin itself is there.\n */\nconst listedAsInstalled = (stdout: string): boolean => {\n const row = new RegExp(`^${PLUGIN_REF}\\\\s+(.*)$`, 'm').exec(stdout)?.[1];\n return !!row && /\\binstalled\\b/.test(row) && !/\\bnot installed\\b/.test(row);\n};\n\n/**\n * What `reportSpawnFailure` was doing, for hint scoping and for the report.\n * A union rather than loose strings: a hint scoped to a stage nobody passes is\n * silently dead, which is how `marketplace add` lost its hint the first time.\n */\ntype CodexStage =\n | 'MCP add'\n | 'MCP remove'\n | 'plugin install'\n | 'plugin uninstall'\n | 'marketplace add';\n\nconst PLUGIN_STAGES: CodexStage[] = [\n 'plugin install',\n 'plugin uninstall',\n 'marketplace add',\n];\nconst MCP_STAGES: CodexStage[] = ['MCP add', 'MCP remove'];\nconst ALL_STAGES: CodexStage[] = [...PLUGIN_STAGES, ...MCP_STAGES];\n\n/**\n * Wording that proves the file codex choked on is the plugin's, not the user's.\n * Serde phrasing alone (\"invalid type\", \"is no longer supported\") says nothing\n * about whose file it came from, and blaming `~/.codex/config.toml` for our own\n * broken manifest sends the user to edit a file that is fine.\n */\nconst OUR_MANIFEST =\n /ai-plugin|plugin\\.toml|plugin\\.json|marketplace\\.toml|posthog@posthog/i;\n\n/**\n * Failures in the user's own environment. Reporting them files issues nobody\n * can action, so hand back a hint instead. Drawn from what the codex install\n * path actually produced in the field.\n *\n * Every entry is scoped: a hint that fires on the wrong stage tells the user to\n * run a command that has nothing to do with what just failed.\n */\nconst EXPECTED_FAILURES: ExpectedFailure[] = [\n {\n // `plugin`/`marketplace` subcommands a pre-plugin codex doesn't have.\n match:\n /unexpected argument|unknown (command|option|argument)|Missing option/i,\n stages: PLUGIN_STAGES,\n hint: 'your codex CLI is too old for plugins — update codex, then run `codex plugin marketplace add PostHog/ai-plugin`',\n },\n {\n // The same wording during `mcp add` is a flag we passed, not a plugin\n // subcommand, so the plugin advice above would send the user nowhere.\n match:\n /unexpected argument|unknown (command|option|argument)|Missing option/i,\n stages: MCP_STAGES,\n hint: 'your codex CLI is too old for this install — update codex, then run `npx @posthog/wizard mcp add` again',\n },\n {\n match: /ENOENT|not found in PATH|spawn .* ENOENT/i,\n stages: ALL_STAGES,\n hint: 'your codex install looks broken — reinstall codex, then run `codex plugin marketplace add PostHog/ai-plugin`',\n },\n {\n // Either codex says outright that it could not load its configuration, or\n // serde phrasing appears alongside `config.toml`. Bare serde wording cannot\n // tell the user's file from the plugin's, and `config.toml` on its own\n // silences any failure that merely mentions the path — both send someone to\n // edit a file that is fine, so the ambiguous case stays reportable.\n match:\n /failed to load (bootstrap )?configuration|OPENAI_API_KEY|Missing OpenAI API key|(?=[\\s\\S]*config\\.toml)[\\s\\S]*(invalid type|unknown field|is no longer supported|must contain at least one)/i,\n stages: ALL_STAGES,\n unless: OUR_MANIFEST,\n hint: 'codex could not read its own config — fix what it reports in ~/.codex/config.toml, then retry',\n },\n {\n match:\n /EACCES|EPERM|permission denied|read-only file system|not permitted/i,\n stages: ALL_STAGES,\n // git says `Permission denied (publickey)` when an SSH clone of the\n // marketplace cannot authenticate to GitHub. Nothing on ~/.codex is wrong\n // there, so the hint below would send the user to chmod a directory that\n // is already fine while the real cause goes unreported.\n unless: /permission denied \\((?:publickey|password|gssapi)/i,\n hint: 'codex could not write to its config — fix the permissions on ~/.codex, then retry',\n },\n {\n match: /ENOSPC|no space left/i,\n stages: ALL_STAGES,\n hint: 'the disk is full — free some space, then retry',\n },\n {\n // A clone that fails because the repo is missing is our publishing problem,\n // not the user's network, and telling them to check their connection buries\n // a renamed or deleted PostHog/ai-plugin where nobody will see it.\n match:\n /git clone .* failed|ENOTFOUND|ETIMEDOUT|ECONNREFUSED|could not resolve host/i,\n stages: ALL_STAGES,\n unless:\n /repository not found|not found|404|does not exist|authentication failed/i,\n hint: 'codex could not reach GitHub to download the plugin — check your network, then retry',\n },\n];\n\ninterface CodexRun {\n ok: boolean;\n /** Never blank on a failure: the cause, or the exit status when there is none. */\n output: string;\n}\n\n/**\n * A clone of the plugin marketplace is the slowest thing we run. Past this the\n * command is not slow, it is stuck, and the wizard should say so rather than\n * wait forever.\n */\nconst RUN_TIMEOUT_MS = 120_000;\n\n/**\n * `execFile` kills the child once output passes this. The default is 1 MB,\n * which a verbose clone can reach, and the kill then looks like codex rejecting\n * the install rather than us cutting it off.\n */\nconst MAX_OUTPUT_BYTES = 16 * 1024 * 1024;\n\n/**\n * What `execFile` puts in `error.message` when the command itself never spoke:\n * the full binary path and arguments. That path contains the user's home\n * directory, so reporting it fingerprints one root cause per machine — the\n * failure this reporting was built to stop. Never send it onward.\n */\nconst COMMAND_FAILED_PREAMBLE = /^Command failed:.*$/m;\n\n/**\n * The cause when neither stream carried one. `execFile` always sets a message,\n * so there is no \"empty error\" case to fall through — the message just isn't\n * usable as it stands.\n */\nconst describeExecFailure = (error: ExecFileException): string => {\n if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER')\n return 'codex produced more output than the wizard can hold, so it was stopped';\n if (error.killed)\n return `codex did not finish within ${\n RUN_TIMEOUT_MS / 1000\n }s, so it was stopped`;\n if (error.signal) return `codex was killed by ${error.signal}`;\n\n const said = error.message.replace(COMMAND_FAILED_PREAMBLE, '').trim();\n if (said) return said;\n if (typeof error.code === 'number')\n return `codex exited with code ${error.code}`;\n if (error.code) return `codex failed with ${error.code}`;\n return 'codex failed without reporting a reason';\n};\n\n/**\n * One codex invocation. Async on purpose: `plugin marketplace add` and\n * `plugin add` clone git repositories and take seconds, and a synchronous child\n * process blocks the event loop, which freezes the TUI spinner on its first\n * frame. Never throws; the caller decides what a failure means.\n *\n * A failure's cause is split across the error and the two streams, and none of\n * them carries it when the process merely exits non-zero — reading one alone\n * reported a blank reason and filed an exception carrying nothing.\n */\nconst runCodex = (\n binary: string,\n args: string[],\n env?: NodeJS.ProcessEnv,\n): Promise<CodexRun> =>\n new Promise((resolve) => {\n const child = execFile(\n binary,\n args,\n { env, timeout: RUN_TIMEOUT_MS, maxBuffer: MAX_OUTPUT_BYTES },\n (error, stdout, stderr) => {\n if (!error) return resolve({ ok: true, output: stdout ?? '' });\n // stderr first: it carries what codex actually said, and the TUI shows\n // the first line. `error.message` leads with our own invocation, so\n // preferring it would show the user the command instead of the cause.\n const said = [stderr, stdout]\n .map((p) => (p ? String(p).trim() : ''))\n .filter(Boolean)\n .join('\\n');\n // A kill is ours, not codex's, so it outranks whatever the streams got\n // out before we cut them off.\n const stopped =\n error.killed ||\n !!error.signal ||\n error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';\n resolve({\n ok: false,\n output: redactSecrets(\n stopped || !said ? describeExecFailure(error) : said,\n ),\n });\n },\n );\n // codex asks for nothing on stdin here. Leaving the pipe open means a\n // command that does ask waits on input that never comes, and the promise\n // never settles. `spawnSync` closed it for us.\n child.stdin?.end();\n });\n\n/**\n * Turn a failed spawn into a result: an expected local failure becomes a hint,\n * anything else is reported under a constant message so one root cause stays\n * one issue, with the varying detail in properties.\n */\nconst reportSpawnFailure = (\n stage: CodexStage,\n details: string,\n): InstallResult => {\n const hint = expectedFailureHint(details, EXPECTED_FAILURES, stage);\n if (hint) {\n // Hinting takes a failure out of error tracking, so without this the only\n // evidence a pattern has started over-matching is that our exception count\n // fell, which reads as the fix working. An event keeps the count.\n analytics.wizardCapture('mcp expected failure hinted', {\n client: 'Codex',\n stage,\n hint,\n details: scrubHomePaths(details),\n });\n return { success: false, reason: hint };\n }\n analytics.captureException(new Error(`Codex ${stage} failed`), {\n stage,\n details: scrubHomePaths(details),\n });\n return { success: false, reason: details };\n};\n\n/** Wording codex uses when the thing we're adding is already registered. */\nconst ALREADY_INSTALLED_PATTERN =\n /already (installed|exists|added|registered)/i;\n/** Wording that means the opposite: a cache entry exists but the plugin doesn't. */\nconst STALE_MARKETPLACE_CACHE = /already added from a different source/i;\n\n/**\n * Codex allows servers 10s to finish the MCP initialize handshake, and a remote\n * OAuth handshake can exceed that on a cold start — the \"servers were not\n * initialized\" warning.\n */\nconst STARTUP_TIMEOUT_SEC = 30;\n\n/**\n * The server's table header. TOML accepts a bare or quoted key for the same\n * table, and matching only the bare form means we append a second definition of\n * a table that already exists — a duplicate-key error that takes the user's\n * whole config down, not just PostHog's entry.\n */\nconst sectionHeader = (serverName: string): RegExp =>\n new RegExp(\n `^\\\\[mcp_servers\\\\.(?:${serverName}|\"${serverName}\")\\\\][ \\\\t]*$`,\n 'm',\n );\n\n/**\n * Set `key = value` inside a section body, inserting it when absent. Scans the\n * whole body rather than the line after the header: TOML does not care about\n * key order, and assuming it does means silently matching nothing.\n */\nconst setKey = (body: string, key: string, value: string): string => {\n const existing = new RegExp(`^[ \\\\t]*${key}[ \\\\t]*=.*$`, 'm');\n return existing.test(body)\n ? body.replace(existing, `${key} = ${value}`)\n : `\\n${key} = ${value}${body}`;\n};\n\nexport const CodexMCPConfig = DefaultMCPClientConfig;\n\nexport type CodexMCPConfig = z.infer<typeof DefaultMCPClientConfig>;\n\nexport class CodexMCPClient\n extends DefaultMCPClient\n implements PluginCapable, LoginCapable\n{\n name = 'Codex';\n private codexBinaryPath: string | null = null;\n\n constructor() {\n super();\n }\n\n private findCodexBinary(): string | null {\n if (this.codexBinaryPath) return this.codexBinaryPath;\n try {\n const resolved = execSync('command -v codex', { stdio: 'pipe' })\n .toString()\n .trim();\n if (resolved) {\n this.codexBinaryPath = resolved;\n return resolved;\n }\n } catch {\n // not in PATH\n }\n return null;\n }\n\n private configPath(): string {\n return path.join(os.homedir(), '.codex', 'config.toml');\n }\n\n isClientSupported(): Promise<boolean> {\n return Promise.resolve(this.findCodexBinary() !== null);\n }\n\n getConfigPath(): Promise<string> {\n throw new Error('Not implemented');\n }\n\n isServerInstalled(local?: boolean): Promise<boolean> {\n const serverName = local ? 'posthog-local' : 'posthog';\n // The `[mcp_servers.<name>]` section in config.toml — both the CLI and the\n // desktop app read (and `codex mcp add` writes) this file, and a substring\n // scan of `mcp list` matches unrelated posthog-ish servers.\n try {\n const contents = fs.readFileSync(this.configPath(), 'utf-8');\n return Promise.resolve(sectionHeader(serverName).test(contents));\n } catch {\n return Promise.resolve(false);\n }\n }\n\n async addServer(\n apiKey?: string,\n selectedFeatures?: string[],\n local?: boolean,\n ): Promise<InstallResult> {\n const serverName = local ? 'posthog-local' : 'posthog';\n const url = buildMCPUrl(selectedFeatures, local);\n\n // Api-key installs go through the CLI for its bearer-token env wiring.\n if (apiKey) {\n const binary = this.findCodexBinary();\n if (!binary)\n return {\n success: false,\n reason: 'An API-key install into Codex needs the codex CLI.',\n };\n const args = [\n 'mcp',\n 'add',\n serverName,\n '--url',\n url,\n '--bearer-token-env-var',\n 'POSTHOG_AUTH_HEADER',\n ];\n const env = { ...process.env, POSTHOG_AUTH_HEADER: `Bearer ${apiKey}` };\n const result = await runCodex(binary, args, env);\n if (!result.ok) {\n if (ALREADY_INSTALLED_PATTERN.test(result.output)) {\n return { success: true, alreadyInstalled: true };\n }\n return reportSpawnFailure('MCP add', result.output);\n }\n return { success: true };\n }\n\n // OAuth installs write config.toml directly: running `codex mcp add` here\n // would hang the wizard on its built-in OAuth browser wait. Codex nags\n // about the unauthenticated server until the surfaced `codex mcp login`\n // runs — the same interim state as Claude Code's \"needs authentication\".\n return this.writeServerSection(serverName, url);\n }\n\n /**\n * Create or update the `[mcp_servers.<name>]` section in config.toml, editing\n * in place so the user's other servers, key order, and comments survive.\n */\n private writeServerSection(serverName: string, url: string): InstallResult {\n const configPath = this.configPath();\n try {\n // A machine that has codex installed but has never run it has no\n // ~/.codex at all — `codex mcp add` used to create it for us.\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n const contents = fs.existsSync(configPath)\n ? fs.readFileSync(configPath, 'utf-8')\n : '';\n\n const header = sectionHeader(serverName).exec(contents);\n if (!header) {\n const gap = contents === '' || contents.endsWith('\\n\\n') ? '' : '\\n';\n const pad = contents === '' || contents.endsWith('\\n') ? '' : '\\n';\n this.write(\n configPath,\n `${contents}${pad}${gap}[mcp_servers.${serverName}]\\n` +\n `url = \"${url}\"\\nstartup_timeout_sec = ${STARTUP_TIMEOUT_SEC}\\n`,\n );\n return { success: true };\n }\n\n // Everything up to the next table header belongs to this server.\n const start = header.index + header[0].length;\n const rest = contents.slice(start);\n const next = /^\\[/m.exec(rest);\n const end = next ? start + next.index : contents.length;\n\n const body = contents.slice(start, end);\n const updated = setKey(\n setKey(body, 'url', `\"${url}\"`),\n 'startup_timeout_sec',\n String(STARTUP_TIMEOUT_SEC),\n );\n if (updated === body) return { success: true, alreadyInstalled: true };\n\n this.write(\n configPath,\n contents.slice(0, start) + updated + contents.slice(end),\n );\n return { success: true };\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n // Constant message, varying detail in properties: the reason carries the\n // config path, so interpolating it filed one issue per user.\n analytics.captureException(new Error('Codex config.toml write failed'), {\n details: scrubHomePaths(reason),\n });\n return { success: false, reason };\n }\n }\n\n /** Write via a sibling temp file: a crash mid-write must not truncate the config. */\n private write(configPath: string, contents: string): void {\n const tmp = `${configPath}.wizard-tmp`;\n fs.writeFileSync(tmp, contents);\n fs.renameSync(tmp, configPath);\n }\n\n /** Codex's own login runs its OAuth and owns the token; the wizard only surfaces the command. */\n loginCommand(local?: boolean): string | null {\n if (!this.findCodexBinary()) return null;\n return `codex mcp login ${local ? 'posthog-local' : 'posthog'}`;\n }\n\n async removeServer(local?: boolean): Promise<InstallResult> {\n const binary = this.findCodexBinary();\n if (!binary)\n return {\n success: false,\n reason: 'The codex CLI is no longer on your PATH.',\n };\n\n // `local` was ignored here, so `mcp remove --local` reported success while\n // leaving the posthog-local server in place.\n const serverName = local ? 'posthog-local' : 'posthog';\n const result = await runCodex(binary, ['mcp', 'remove', serverName]);\n\n if (!result.ok) {\n return reportSpawnFailure('MCP remove', result.output);\n }\n\n return { success: true };\n }\n\n /** The codex marketplace plugin ships skills only — the MCP server needs its own entry. */\n pluginBundlesMcpServer(): boolean {\n return false;\n }\n\n supportsPlugin(): boolean {\n return this.findCodexBinary() !== null;\n }\n\n /** The plugin itself, as the CLI reports it. The install decision's question. */\n private async isPluginPresent(): Promise<boolean> {\n const binary = this.findCodexBinary();\n if (!binary) return false;\n const result = await runCodex(binary, [\n 'plugin',\n 'list',\n '-m',\n PLUGIN_MARKETPLACE,\n ]);\n return result.ok && listedAsInstalled(result.output);\n }\n\n /**\n * Anything of ours left on the machine, which is what `index.ts` asks before\n * offering removal. A registered marketplace with no plugin is a state the\n * wizard created — and the state this branch exists to correct — so removal\n * has to see it, or `removePlugin` never runs and the catalog stays forever.\n */\n async isPluginInstalled(): Promise<boolean> {\n return (await this.isPluginPresent()) || this.isMarketplaceRegistered();\n }\n\n /** The catalog, which the plugin is installed *from* — not the plugin. */\n private isMarketplaceRegistered(): boolean {\n try {\n const contents = fs.readFileSync(this.configPath(), 'utf-8');\n return contents\n .toLowerCase()\n .includes(`[marketplaces.${PLUGIN_MARKETPLACE}]`);\n } catch {\n return false;\n }\n }\n\n async removePlugin(): Promise<PluginInstallResult> {\n const binary = this.findCodexBinary();\n if (!binary)\n return {\n success: false,\n reason: 'The codex CLI is no longer on your PATH.',\n };\n\n // Both halves are removed, and either alone is enough to act on: a user\n // left with a registered marketplace and no plugin still needs it cleared.\n const steps: string[][] = [];\n if (await this.isPluginPresent())\n steps.push(['plugin', 'remove', PLUGIN_REF]);\n if (this.isMarketplaceRegistered())\n steps.push(['plugin', 'marketplace', 'remove', PLUGIN_MARKETPLACE]);\n\n if (steps.length === 0) return { success: true, alreadyInstalled: true };\n\n for (const args of steps) {\n const result = await runCodex(binary, args);\n if (!result.ok) {\n return reportSpawnFailure('plugin uninstall', result.output);\n }\n }\n return { success: true };\n }\n\n async installPlugin(): Promise<PluginInstallResult> {\n const binary = this.findCodexBinary();\n if (!binary)\n return {\n success: false,\n reason: 'The codex CLI is no longer on your PATH.',\n };\n\n // The plugin, not the catalog: a registered marketplace with no plugin is\n // exactly the broken state this installs over, so asking the wider question\n // here would skip the install and leave it broken.\n if (await this.isPluginPresent()) {\n return { success: true, alreadyInstalled: true };\n }\n\n // `codex plugin marketplace add` exits non-zero once the marketplace is\n // registered, so ask config.toml first. Without this the second run of\n // `mcp add` looked like a failure and reported nothing at all.\n const marketplace = this.isMarketplaceRegistered()\n ? null\n : await this.registerMarketplace(binary);\n if (marketplace && !marketplace.success) return marketplace;\n\n // Registering the catalog does not install from it. Skipping this left the\n // user with a marketplace, no skills, and a wizard reporting success.\n const result = await runCodex(binary, ['plugin', 'add', PLUGIN_REF]);\n\n if (!result.ok) {\n // The listing above can report \"not installed\" when it merely failed to\n // run, and every other spawn here reads codex's own wording rather than\n // turning a no-op into a reported failure.\n if (ALREADY_INSTALLED_PATTERN.test(result.output)) {\n return { success: true, alreadyInstalled: true };\n }\n return reportSpawnFailure('plugin install', result.output);\n }\n\n return { success: true };\n }\n\n /**\n * Add the catalog the plugin is published in. A stale cache directory with no\n * config.toml entry reports the marketplace as added from a different source;\n * clear it and retry once.\n */\n private async registerMarketplace(\n binary: string,\n ): Promise<PluginInstallResult> {\n const run = () =>\n runCodex(binary, [\n 'plugin',\n 'marketplace',\n 'add',\n PLUGIN_MARKETPLACE_SOURCE,\n ]);\n\n let result = await run();\n\n if (!result.ok && STALE_MARKETPLACE_CACHE.test(result.output)) {\n const staleDir = path.join(\n os.homedir(),\n '.codex',\n '.tmp',\n 'marketplaces',\n PLUGIN_MARKETPLACE,\n );\n try {\n fs.rmSync(staleDir, { recursive: true, force: true });\n } catch {\n // ignore — retry anyway\n }\n result = await run();\n }\n\n if (!result.ok) {\n const details = result.output;\n // Registered by something other than us — the plugin add below can still\n // resolve against it. The stale-cache wording is excluded: that one means\n // the marketplace is NOT usable, and only reaches here if the retry failed.\n if (\n ALREADY_INSTALLED_PATTERN.test(details) &&\n !STALE_MARKETPLACE_CACHE.test(details)\n ) {\n return { success: true };\n }\n return reportSpawnFailure('marketplace add', details);\n }\n return { success: true };\n }\n}\n\nexport default CodexMCPClient;\n","import z from 'zod';\nimport * as path from 'path';\nimport * as os from 'os';\nimport * as fs from 'fs';\nimport { execSync } from 'node:child_process';\nimport {\n DefaultMCPClient,\n MCPServerConfig,\n} from '@steps/add-mcp-server-to-clients/MCPClient';\nimport { getNativeHTTPServerConfig } from '@steps/add-mcp-server-to-clients/defaults';\nimport { runtimeEnv } from '@env';\n\nexport const OpenCodeMCPConfig = z\n .object({\n mcp: z.record(\n z.string(),\n z.object({\n type: z.literal('remote'),\n url: z.string(),\n headers: z.record(z.string(), z.string()).optional(),\n }),\n ),\n })\n .passthrough();\n\nexport type OpenCodeMCPConfig = z.infer<typeof OpenCodeMCPConfig>;\n\nexport class OpenCodeMCPClient extends DefaultMCPClient {\n name = 'OpenCode';\n private opencodeBinaryPath: string | null = null;\n\n override getServerPropertyName(): string {\n return 'mcp';\n }\n\n private findOpenCodeBinary(): string | null {\n if (this.opencodeBinaryPath) return this.opencodeBinaryPath;\n try {\n const resolved = execSync('command -v opencode', { stdio: 'pipe' })\n .toString()\n .trim();\n if (resolved) {\n this.opencodeBinaryPath = resolved;\n return resolved;\n }\n } catch {\n // not in PATH\n }\n return null;\n }\n\n isClientSupported(): Promise<boolean> {\n return Promise.resolve(this.findOpenCodeBinary() !== null);\n }\n\n getConfigPath(): Promise<string> {\n const configDir = this.resolveConfigDir();\n const jsoncPath = path.join(configDir, 'opencode.jsonc');\n const jsonPath = path.join(configDir, 'opencode.json');\n if (fs.existsSync(jsoncPath)) return Promise.resolve(jsoncPath);\n return Promise.resolve(jsonPath);\n }\n\n private resolveConfigDir(): string {\n const envDir = runtimeEnv('OPENCODE_CONFIG_DIR');\n if (envDir) return envDir;\n\n const xdgHome = runtimeEnv('XDG_CONFIG_HOME');\n if (xdgHome) return path.join(xdgHome, 'opencode');\n\n return path.join(os.homedir(), '.config', 'opencode');\n }\n\n override getServerConfig(\n apiKey: string | undefined,\n selectedFeatures?: string[],\n local?: boolean,\n ): MCPServerConfig {\n return {\n type: 'remote',\n ...getNativeHTTPServerConfig(apiKey, selectedFeatures, local),\n };\n }\n}\n","import type { InstallResult } from './results';\n\nexport type PluginInstallResult = InstallResult;\n\nexport interface PluginCapable {\n supportsPlugin(): boolean;\n isPluginInstalled(): Promise<boolean>;\n installPlugin(): Promise<PluginInstallResult>;\n /** Uninstall the plugin. Absent when the client CLI has no uninstall surface. */\n removePlugin?(): Promise<PluginInstallResult>;\n /** True when the plugin ships its own posthog MCP server, making a direct entry redundant. */\n pluginBundlesMcpServer(): boolean;\n}\n\nexport function isPluginCapable<T>(client: T): client is T & PluginCapable {\n return (\n typeof client === 'object' &&\n client !== null &&\n 'supportsPlugin' in client &&\n 'installPlugin' in client\n );\n}\n","import type { Integration } from '@shared/constants';\nimport type { CloudRegion } from '@utils/types';\nimport { withProgress } from '@utils/telemetry';\nimport { analytics } from '@utils/analytics';\nimport { getUI } from '@ui';\nimport { MCPClient } from './MCPClient';\nimport { CursorMCPClient } from './clients/cursor';\nimport { ClaudeCodeMCPClient } from './clients/claude-code';\nimport { ClaudeWebMCPClient } from './clients/claude-web';\nimport { VisualStudioCodeClient } from './clients/visual-studio-code';\nimport { ZedClient } from './clients/zed';\nimport { CodexMCPClient } from './clients/codex';\nimport { OpenCodeMCPClient } from './clients/opencode';\nimport { ALL_FEATURE_VALUES } from './defaults';\nimport { debug } from '@utils/debug';\nimport { isPluginCapable, PluginCapable } from './plugin-client';\nimport {\n McpClientStatus,\n namesWithStatus,\n toClientResult,\n type McpClientResult,\n} from './results';\n\nexport const getSupportedClients = async (): Promise<MCPClient[]> => {\n const allClients = [\n new ClaudeCodeMCPClient(),\n new ClaudeWebMCPClient(),\n new CodexMCPClient(),\n new CursorMCPClient(),\n new VisualStudioCodeClient(),\n new ZedClient(),\n new OpenCodeMCPClient(),\n ];\n const supportedClients: MCPClient[] = [];\n\n debug('Checking for supported MCP clients...');\n for (const client of allClients) {\n const isSupported = await client.isClientSupported();\n debug(`${client.name}: ${isSupported ? '✓ supported' : '✗ not supported'}`);\n if (isSupported) {\n supportedClients.push(client);\n }\n }\n debug(\n `Found ${supportedClients.length} supported client(s): ${supportedClients\n .map((c) => c.name)\n .join(', ')}`,\n );\n\n return supportedClients;\n};\n\n/** Per-client outcome, so a scripted caller can turn a failure into an exit code. */\nexport interface McpStepOutcome {\n /** Clients that ended up with the server, whether we wrote it or it was already there. */\n installed: string[];\n failed: string[];\n}\n\n/**\n * Add MCP server to clients. No prompts — pure orchestration.\n * Prompts are handled by McpScreen (TUI) or auto-accepted (CI).\n */\nexport const addMCPServerToClientsStep = async ({\n integration,\n local = false,\n ci = false,\n cloudRegion: _cloudRegion,\n features,\n apiKey,\n}: {\n integration?: Integration;\n local?: boolean;\n ci?: boolean;\n cloudRegion?: CloudRegion;\n features?: string[];\n apiKey?: string;\n}): Promise<McpStepOutcome> => {\n const ui = getUI();\n\n // CI mode: skip MCP installation entirely\n if (ci) {\n ui.log.info('Skipping MCP installation (CI mode)');\n return { installed: [], failed: [] };\n }\n\n const supportedClients = await getSupportedClients();\n\n if (supportedClients.length === 0) {\n ui.log.info(\n 'No supported MCP clients detected. Skipping MCP installation.',\n );\n return { installed: [], failed: [] };\n }\n\n // Auto-install to all supported clients\n const results = await withProgress('adding mcp servers', () =>\n addMCPServer(\n supportedClients,\n apiKey,\n features ?? [...ALL_FEATURE_VALUES],\n local,\n ),\n );\n\n const installed = namesWithStatus(results, McpClientStatus.Changed);\n const already = namesWithStatus(results, McpClientStatus.Unchanged);\n const failed = results.filter((r) => r.status === McpClientStatus.Failed);\n\n // Report each outcome on its own — a blanket \"Added the MCP server to: ...\"\n // hid both the no-op re-runs and the outright failures.\n if (installed.length > 0) {\n ui.log.success(`Added the MCP server to:\\n${bulletList(installed)}`);\n }\n if (already.length > 0) {\n ui.log.info(\n `The PostHog MCP server was already installed, so nothing changed for:\\n${bulletList(\n already,\n )}`,\n );\n }\n if (failed.length > 0) {\n ui.log.warn(\n `Couldn't add the MCP server to:\\n${bulletList(\n failed.map((r) => (r.detail ? `${r.name} — ${r.detail}` : r.name)),\n )}`,\n );\n }\n\n const withServer = [...installed, ...already];\n\n analytics.wizardCapture('mcp servers added', {\n // `clients` stays \"every client that ended up with the MCP server\", which is\n // what it meant before — the new properties break that down.\n clients: withServer,\n already_installed_clients: already,\n failed_clients: failed.map((r) => r.name),\n attempted_clients: supportedClients.map((c) => c.name),\n integration,\n });\n\n return { installed: withServer, failed: failed.map((r) => r.name) };\n};\n\nconst bulletList = (items: string[]): string =>\n items.map((item) => ` - ${item}`).join('\\n');\n\nexport const removeMCPServerFromClientsStep = async ({\n integration,\n local = false,\n}: {\n integration?: Integration;\n local?: boolean;\n}): Promise<string[]> => {\n const ui = getUI();\n const installedClients = await getInstalledClients(local);\n if (installedClients.length === 0) {\n ui.log.info(\n 'The PostHog MCP server is not installed for any supported client. Nothing to remove.',\n );\n analytics.wizardCapture('mcp no servers to remove', {\n integration,\n });\n return [];\n }\n\n // Auto-remove from all installed clients\n const results = await withProgress('removing mcp servers', () =>\n removeMCPServer(installedClients, local),\n );\n\n const removed = namesWithStatus(results, McpClientStatus.Changed);\n const nothingToDo = namesWithStatus(results, McpClientStatus.Unchanged);\n const failed = results.filter((r) => r.status === McpClientStatus.Failed);\n\n // This step used to print nothing at all, so a non-TTY `mcp remove` gave no\n // hint whether anything happened — let alone whether a client failed.\n if (removed.length > 0) {\n ui.log.success(`Removed the MCP server from:\\n${bulletList(removed)}`);\n }\n if (nothingToDo.length > 0) {\n ui.log.info(\n `No PostHog MCP entry left to remove for:\\n${bulletList(nothingToDo)}`,\n );\n }\n if (failed.length > 0) {\n ui.log.warn(\n `Couldn't remove the MCP server from:\\n${bulletList(\n failed.map((r) => (r.detail ? `${r.name} — ${r.detail}` : r.name)),\n )}`,\n );\n }\n\n analytics.wizardCapture('mcp servers removed', {\n clients: removed,\n nothing_to_remove_clients: nothingToDo,\n failed_clients: failed.map((r) => r.name),\n attempted_clients: installedClients.map((c) => c.name),\n integration,\n });\n\n return removed;\n};\n\nexport const getInstalledClients = async (\n local?: boolean,\n): Promise<MCPClient[]> => {\n const clients = await getSupportedClients();\n const installedClients: MCPClient[] = [];\n\n for (const client of clients) {\n // The plugin bundles its own posthog MCP server, so for removal purposes a\n // plugin install counts as installed even with no config entry (`--local`\n // targets only the local-dev entry and leaves the plugin alone).\n const pluginInstalled =\n !local && isPluginCapable(client) && (await client.isPluginInstalled());\n if ((await client.isServerInstalled(local)) || pluginInstalled) {\n installedClients.push(client);\n }\n }\n\n return installedClients;\n};\n\nexport const addMCPServer = async (\n clients: MCPClient[],\n personalApiKey?: string,\n selectedFeatures?: string[],\n local?: boolean,\n): Promise<McpClientResult[]> => {\n const results: McpClientResult[] = [];\n for (const client of clients) {\n try {\n const result = await client.addServer(\n personalApiKey,\n selectedFeatures,\n local,\n );\n results.push(toClientResult(client.name, result));\n } catch (err) {\n debug(`[addMCPServer] addServer threw for ${client.name}: ${err}`);\n results.push(\n toClientResult(client.name, {\n success: false,\n reason: err instanceof Error ? err.message : String(err),\n }),\n );\n }\n }\n return results;\n};\n\nexport const getSupportedPluginClients = (\n clients: MCPClient[],\n): Array<MCPClient & PluginCapable> => {\n return clients.filter(isPluginCapable).filter((c) => c.supportsPlugin());\n};\n\nexport const installPlugins = async (\n clients: Array<MCPClient & PluginCapable>,\n): Promise<McpClientResult[]> => {\n const results: McpClientResult[] = [];\n for (const client of clients) {\n try {\n results.push(toClientResult(client.name, await client.installPlugin()));\n } catch (err) {\n debug(`[installPlugins] installPlugin threw for ${client.name}: ${err}`);\n results.push(\n toClientResult(client.name, {\n success: false,\n reason: err instanceof Error ? err.message : String(err),\n }),\n );\n }\n }\n return results;\n};\n\nexport const removeMCPServer = async (\n clients: MCPClient[],\n local?: boolean,\n): Promise<McpClientResult[]> => {\n const results: McpClientResult[] = [];\n for (const client of clients) {\n try {\n let result = await client.removeServer(local);\n // The plugin bundles its own posthog server — leaving it installed makes\n // the removal a lie (`--local` never touches the plugin).\n if (!local && isPluginCapable(client) && client.removePlugin) {\n const plugin = await client.removePlugin();\n result =\n !result.success || !plugin.success\n ? {\n success: false,\n reason: [result.reason, plugin.reason]\n .filter(Boolean)\n .join('; '),\n }\n : {\n success: true,\n ...(result.alreadyInstalled && plugin.alreadyInstalled\n ? { alreadyInstalled: true }\n : {}),\n };\n }\n results.push(toClientResult(client.name, result));\n } catch (err) {\n debug(`[removeMCPServer] removeServer threw for ${client.name}: ${err}`);\n results.push(\n toClientResult(client.name, {\n success: false,\n reason: err instanceof Error ? err.message : String(err),\n }),\n );\n }\n }\n return results;\n};\n"],"mappings":"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"}
|