@posthog/wizard 2.44.0 → 2.45.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/{add-mcp-server-to-clients-D5qsqCkj.js → add-mcp-server-to-clients-CNzZMSat.js} +5 -5
- package/dist/{add-mcp-server-to-clients-D5qsqCkj.js.map → add-mcp-server-to-clients-CNzZMSat.js.map} +1 -1
- package/dist/{agent-interface-BvTxWiol.js → agent-interface-CF7GsdFf.js} +297 -157
- package/dist/agent-interface-CF7GsdFf.js.map +1 -0
- package/dist/{agent-runner-BF6_f8Qh.js → agent-runner-B0LTVFfw.js} +130 -598
- package/dist/agent-runner-B0LTVFfw.js.map +1 -0
- package/dist/{analytics-DSlQuwOT.js → analytics-DQEuL6qS.js} +2 -2
- package/dist/{analytics-DSlQuwOT.js.map → analytics-DQEuL6qS.js.map} +1 -1
- package/dist/analytics-YD3D38z2.js +2 -0
- package/dist/{api-BXduJZrw.js → api-uCMvDII5.js} +3 -3
- package/dist/{api-BXduJZrw.js.map → api-uCMvDII5.js.map} +1 -1
- package/dist/bin.js +57 -101
- package/dist/bin.js.map +1 -1
- package/dist/{ci-install-GirEJqu2.js → ci-install-Bow8-f0j.js} +4 -4
- package/dist/{ci-install-GirEJqu2.js.map → ci-install-Bow8-f0j.js.map} +1 -1
- package/dist/commandments-CyB7Icr1.js +42 -0
- package/dist/commandments-CyB7Icr1.js.map +1 -0
- package/dist/commandments-DguPfCX8.js +2 -0
- package/dist/{debug-BOFxiVVs.js → debug-BixHZmHv.js} +1 -1
- package/dist/{debug-DXfQFmcM.js → debug-DBmxx59Q.js} +21 -5
- package/dist/debug-DBmxx59Q.js.map +1 -0
- package/dist/{defaults-BWLIffh5.js → defaults-8zjNjGGi.js} +1 -1
- package/dist/{defaults-BWLIffh5.js.map → defaults-8zjNjGGi.js.map} +1 -1
- package/dist/{env-api-key-cjD4yfGT.js → env-api-key-AcC_tz8G.js} +1 -1
- package/dist/{env-api-key-cjD4yfGT.js.map → env-api-key-AcC_tz8G.js.map} +1 -1
- package/dist/{environment-F1yw21dG.js → environment-kZtkp7eG.js} +3 -3
- package/dist/{environment-F1yw21dG.js.map → environment-kZtkp7eG.js.map} +1 -1
- package/dist/{file-utils-D-7YfJ7A.js → file-utils-BAhYy5vC.js} +2 -2
- package/dist/{file-utils-D-7YfJ7A.js.map → file-utils-BAhYy5vC.js.map} +1 -1
- package/dist/file-watcher-BL-IRbR7.js +123 -0
- package/dist/file-watcher-BL-IRbR7.js.map +1 -0
- package/dist/{headless-ui-Dni0gPND.js → headless-ui-DWmMyhaP.js} +5 -4
- package/dist/headless-ui-DWmMyhaP.js.map +1 -0
- package/dist/{interactive-Bmi82rsO.js → interactive-Cyic0bGL.js} +2 -2
- package/dist/{interactive-Bmi82rsO.js.map → interactive-Cyic0bGL.js.map} +1 -1
- package/dist/{mcp-xKRDnrcq.js → mcp-BSGmEWjt.js} +3 -3
- package/dist/mcp-BSGmEWjt.js.map +1 -0
- package/dist/{mcp-prompt-streaming-AtntLsJ8.js → mcp-prompt-streaming-C227vG1X.js} +3 -3
- package/dist/{mcp-prompt-streaming-AtntLsJ8.js.map → mcp-prompt-streaming-C227vG1X.js.map} +1 -1
- package/dist/{non-interactive-BIHqcW8g.js → non-interactive-DQA9vn9f.js} +2 -2
- package/dist/{non-interactive-BIHqcW8g.js.map → non-interactive-DQA9vn9f.js.map} +1 -1
- package/dist/orchestrator-tools-NuWONNkF.js +99 -0
- package/dist/orchestrator-tools-NuWONNkF.js.map +1 -0
- package/dist/{package-json-CDs1ZVKu.js → package-json-DCuoye-H.js} +1 -1
- package/dist/{package-json-CDs1ZVKu.js.map → package-json-DCuoye-H.js.map} +1 -1
- package/dist/{package-manager-C6R_zpML.js → package-manager-DKtR3xzM.js} +2 -2
- package/dist/{package-manager-C6R_zpML.js.map → package-manager-DKtR3xzM.js.map} +1 -1
- package/dist/pi-DJCln9jJ.js +603 -0
- package/dist/pi-DJCln9jJ.js.map +1 -0
- package/dist/{playground-DwL36I2u.js → playground-CbEndzc8.js} +7 -8
- package/dist/{playground-DwL36I2u.js.map → playground-CbEndzc8.js.map} +1 -1
- package/dist/{posthog-DwKlq0HM.js → posthog-DFyux4Vw.js} +1 -1
- package/dist/{posthog-DwKlq0HM.js.map → posthog-DFyux4Vw.js.map} +1 -1
- package/dist/{posthog-integration-BhLKCFsI.js → posthog-integration-dmWJZ8mj.js} +131 -26
- package/dist/posthog-integration-dmWJZ8mj.js.map +1 -0
- package/dist/{provisioning-jVZm2E-A.js → provisioning-CcxCry5p.js} +3 -3
- package/dist/{provisioning-jVZm2E-A.js.map → provisioning-CcxCry5p.js.map} +1 -1
- package/dist/queue-tools-Bw-AANLM.js +366 -0
- package/dist/queue-tools-Bw-AANLM.js.map +1 -0
- package/dist/{registry-DcoBCMQR.js → registry-D2V88NoC.js} +96 -18
- package/dist/registry-D2V88NoC.js.map +1 -0
- package/dist/{security-jxjVsOpd.js → security-DOtBCBvx.js} +4 -4
- package/dist/{security-jxjVsOpd.js.map → security-DOtBCBvx.js.map} +1 -1
- package/dist/{setup-utils-BQCl28D8.js → setup-utils-Qp-u6BZk.js} +7 -7
- package/dist/{setup-utils-BQCl28D8.js.map → setup-utils-Qp-u6BZk.js.map} +1 -1
- package/dist/{start-tui-DgSKFMVh.js → start-tui-DHa_9aiB.js} +19 -80
- package/dist/start-tui-DHa_9aiB.js.map +1 -0
- package/dist/{steps-erepgD0V.js → steps-D07HUQyE.js} +7 -7
- package/dist/{steps-erepgD0V.js.map → steps-D07HUQyE.js.map} +1 -1
- package/dist/{store-C79GoHCQ.js → store-DSwsITKX.js} +5 -6
- package/dist/store-DSwsITKX.js.map +1 -0
- package/dist/{subagent-aDlD_Ltz.js → subagent-Dk4K5MDa.js} +2 -2
- package/dist/{subagent-aDlD_Ltz.js.map → subagent-Dk4K5MDa.js.map} +1 -1
- package/dist/task-CQnV9Qd2.js +305 -0
- package/dist/task-CQnV9Qd2.js.map +1 -0
- package/dist/{task-stream-0Pk0tPBN.js → task-stream-B7qia9eW.js} +72 -7
- package/dist/task-stream-B7qia9eW.js.map +1 -0
- package/dist/{tasks-ZkrluXCa.js → tasks-C7BQp659.js} +2 -2
- package/dist/{tasks-ZkrluXCa.js.map → tasks-C7BQp659.js.map} +1 -1
- package/dist/{telemetry-DZShaH-y.js → telemetry-DgahJGSx.js} +3 -3
- package/dist/{telemetry-DZShaH-y.js.map → telemetry-DgahJGSx.js.map} +1 -1
- package/dist/{terminal-CEhufbJ4.js → terminal-Bb6tZkf-.js} +13 -13
- package/dist/{terminal-CEhufbJ4.js.map → terminal-Bb6tZkf-.js.map} +1 -1
- package/dist/{tools-BIPMan6l.js → tools-BJEvSUWN.js} +4 -4
- package/dist/{tools-BIPMan6l.js.map → tools-BJEvSUWN.js.map} +1 -1
- package/dist/{types-y3HkjtSq.js → types-BRe_EzTW.js} +1 -1
- package/dist/{types-y3HkjtSq.js.map → types-BRe_EzTW.js.map} +1 -1
- package/dist/{wizard-abort-C14PzXZ8.js → wizard-abort-BxtdkgI-.js} +4 -4
- package/dist/{wizard-abort-C14PzXZ8.js.map → wizard-abort-BxtdkgI-.js.map} +1 -1
- package/dist/{wizard-abort-UCm4Pd5m.js → wizard-abort-DpDmuCfA.js} +1 -1
- package/dist/{wizard-session-C5lZPPEc.js → wizard-session-DWHe83uk.js} +1 -1
- package/dist/{wizard-session-BAZql11N.js → wizard-session-tp4Zdbwl.js} +1 -1
- package/dist/{wizard-session-BAZql11N.js.map → wizard-session-tp4Zdbwl.js.map} +1 -1
- package/dist/{wizard-tools-B7SLjCic.js → wizard-tools-Dg8o2lcH.js} +64 -387
- package/dist/wizard-tools-Dg8o2lcH.js.map +1 -0
- package/dist/yara-hooks-CncaWRuC.js +2 -0
- package/dist/{yara-hooks-CPH8N4yz.js → yara-hooks-pw86rZ8y.js} +4 -4
- package/dist/{yara-hooks-CPH8N4yz.js.map → yara-hooks-pw86rZ8y.js.map} +1 -1
- package/package.json +1 -1
- package/dist/agent-interface-BvTxWiol.js.map +0 -1
- package/dist/agent-runner-BF6_f8Qh.js.map +0 -1
- package/dist/analytics-C7Xx6csw.js +0 -2
- package/dist/debug-DXfQFmcM.js.map +0 -1
- package/dist/headless-ui-Dni0gPND.js.map +0 -1
- package/dist/mcp-xKRDnrcq.js.map +0 -1
- package/dist/posthog-integration-BhLKCFsI.js.map +0 -1
- package/dist/registry-DcoBCMQR.js.map +0 -1
- package/dist/start-tui-DgSKFMVh.js.map +0 -1
- package/dist/store-C79GoHCQ.js.map +0 -1
- package/dist/task-stream-0Pk0tPBN.js.map +0 -1
- package/dist/wizard-tools-B7SLjCic.js.map +0 -1
- package/dist/wizard-ui-WZ48rUgr.js +0 -15
- package/dist/wizard-ui-WZ48rUgr.js.map +0 -1
- package/dist/yara-hooks-VZlj4Lib.js +0 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard-tools-Dg8o2lcH.js","names":[],"sources":["../src/lib/wizard-ask-bridge.ts","../src/lib/secret-vault.ts","../src/lib/fetch-retry.ts","../src/lib/wizard-tools.ts"],"sourcesContent":["/**\n * WizardAskBridge — host-side promise broker for the `wizard_ask` MCP tool.\n *\n * The `wizard_ask` tool needs to (a) read information from the wizard\n * session (the active skill id, used as the analytics `source`) and\n * (b) drive the TUI overlay. Wiring `wizard-tools.ts` directly to either\n * would couple our pure-data MCP server to the runtime UI layer.\n *\n * The bridge is the seam: `wizard-tools.ts` depends on this interface,\n * and `agent-runner.ts` constructs an implementation that knows about\n * both the session and `getUI()`.\n */\nimport { randomUUID } from 'crypto';\n\nimport { analytics } from '@utils/analytics';\nimport type {\n AskAnswers,\n AskQuestion,\n PendingQuestion,\n} from './wizard-session';\n\nexport interface WizardAskRequest {\n questions: AskQuestion[];\n}\n\nexport interface WizardAskBridge {\n /**\n * Open the WizardAsk overlay and resolve with the user's answers.\n * One answer per question id (string for `single`/`text`, string[] for\n * `multi`). Cancelled fields come back as the literal `\"__cancelled__\"`.\n */\n request(req: WizardAskRequest): Promise<AskAnswers>;\n}\n\nexport interface WizardAskBridgeOptions {\n /** Returns the active skill id, used as the analytics `source` on the request. */\n getSource: () => string;\n /** Opens the overlay and resolves once the user submits or cancels. */\n showQuestion: (question: PendingQuestion) => Promise<AskAnswers>;\n /**\n * Per-question timeout in milliseconds. When the user takes longer than\n * this to answer, every unanswered field resolves with the\n * {@link CANCELLED_SENTINEL} value. Defaults to {@link DEFAULT_ASK_TIMEOUT_MS}.\n */\n timeoutMs?: number;\n /**\n * Opt the rendered overlay into rich link handling (OSC 8 hyperlinks +\n * clipboard copy for prompt URLs). Set per program; defaults to false.\n * Propagated onto every {@link PendingQuestion} this bridge creates.\n */\n richLinks?: boolean;\n /**\n * Dismiss the host's in-flight question overlay. Called when the timeout\n * wins the race: without it the host keeps its pending-question state, and\n * every later `wizard_ask` in the run fails with \"another request is\n * pending\" — one unanswered prompt would block credential collection for\n * all remaining sources.\n */\n cancelQuestion?: () => void;\n}\n\n/** Sentinel returned for unanswered fields on cancellation or timeout. */\nexport const CANCELLED_SENTINEL = '__cancelled__';\n\n/** Default per-question timeout (5 minutes). */\nexport const DEFAULT_ASK_TIMEOUT_MS = 5 * 60 * 1000;\n\nfunction buildCancelledAnswers(questions: AskQuestion[]): AskAnswers {\n const out: AskAnswers = {};\n for (const q of questions) {\n out[q.id] = CANCELLED_SENTINEL;\n }\n return out;\n}\n\nexport function isFullyCancelled(answers: AskAnswers): boolean {\n const values = Object.values(answers);\n if (values.length === 0) return false;\n return values.every((v) => v === CANCELLED_SENTINEL);\n}\n\nexport function createWizardAskBridge(\n opts: WizardAskBridgeOptions,\n): WizardAskBridge {\n const timeoutMs = opts.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS;\n\n return {\n async request({ questions }) {\n const pending: PendingQuestion = {\n id: randomUUID(),\n questions,\n source: opts.getSource(),\n richLinks: opts.richLinks ?? false,\n };\n\n const startedAt = Date.now();\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n // Race the user against the timeout. Whichever fires first wins. On\n // timeout we also cancel the host's overlay: resolving our side alone\n // would leave the host's pending-question state set, and the next\n // wizard_ask would be rejected as a duplicate request.\n const timeoutPromise = new Promise<AskAnswers>((resolve) => {\n timer = setTimeout(() => {\n opts.cancelQuestion?.();\n resolve(buildCancelledAnswers(questions));\n }, timeoutMs);\n });\n\n try {\n const answers = await Promise.race([\n opts.showQuestion(pending),\n timeoutPromise,\n ]);\n const durationMs = Date.now() - startedAt;\n\n if (isFullyCancelled(answers)) {\n analytics.wizardCapture('wizard_ask cancelled', {\n source: pending.source,\n question_count: questions.length,\n duration_ms: durationMs,\n timed_out: durationMs >= timeoutMs,\n });\n } else {\n analytics.wizardCapture('wizard_ask answered', {\n source: pending.source,\n question_count: questions.length,\n duration_ms: durationMs,\n });\n }\n\n return answers;\n } finally {\n if (timer) clearTimeout(timer);\n }\n },\n };\n}\n","/**\n * Session-scoped secret vault.\n *\n * Tools that handle sensitive values (personal API keys pasted by the user\n * or minted by the wizard, OAuth tokens, etc.) store them here and return\n * an opaque `secret:<uuid>` ref to the agent. The agent passes the ref\n * around to subsequent tools — `set_env_values` resolves it host-side\n * before writing — but the raw value never enters the LLM conversation.\n *\n * The vault is created once per `createWizardToolsServer` call and lives\n * for the duration of a single wizard run. There is no persistence and\n * no cross-session sharing; refs minted in one run cannot be resolved in\n * another.\n */\n\nimport { randomUUID } from 'crypto';\n\nconst REF_PREFIX = 'secret:';\n\nexport interface SecretMeta {\n /** Opaque reference handed to the agent. */\n ref: string;\n /** Human-readable label shown to the user (e.g. \"Personal API key\"). */\n label: string;\n /** Where the secret came from (e.g. \"wizard_ask\"). */\n source: string;\n /** ms epoch when the secret was stored. */\n createdAt: number;\n}\n\nexport interface SecretVault {\n /** Store a value and return its ref. */\n put(value: string, meta: Omit<SecretMeta, 'ref' | 'createdAt'>): string;\n /** Resolve a ref to its value, or undefined if unknown. */\n get(ref: string): string | undefined;\n /** Whether the vault knows about this ref. */\n has(ref: string): boolean;\n /** Metadata for every stored secret (never the values). */\n list(): SecretMeta[];\n /** Drop every secret. Call at session teardown. */\n clear(): void;\n}\n\n/** True when the value looks like a secret ref. Does not assert resolvability. */\nexport function isSecretRef(value: unknown): value is string {\n return typeof value === 'string' && value.startsWith(REF_PREFIX);\n}\n\nexport function createSecretVault(): SecretVault {\n const store = new Map<string, { value: string; meta: SecretMeta }>();\n\n return {\n put(value, meta) {\n const ref = `${REF_PREFIX}${randomUUID()}`;\n store.set(ref, {\n value,\n meta: {\n ref,\n label: meta.label,\n source: meta.source,\n createdAt: Date.now(),\n },\n });\n return ref;\n },\n get(ref) {\n return store.get(ref)?.value;\n },\n has(ref) {\n return store.has(ref);\n },\n list() {\n return [...store.values()].map((s) => s.meta);\n },\n clear() {\n store.clear();\n },\n };\n}\n","/**\n * Retry wrapper for fetching from the skills/agents server (GitHub releases in\n * production). GitHub releases blips transiently, so every fetch on the run's\n * critical path — skill menu, skill zips, agent menu, agent prompt bodies —\n * goes through here rather than a bare `fetch`.\n */\n\nconst DEFAULT_TIMEOUT_MS = 60000; // per attempt\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_BACKOFF_MS = 500; // doubles each retry\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport interface RetryOpts {\n fetchImpl?: typeof fetch;\n sleepImpl?: (ms: number) => Promise<void>;\n timeoutMs?: number;\n maxAttempts?: number;\n backoffMs?: number;\n}\n\n/** Fetch a URL, retrying transient failures (network error or non-ok HTTP) with backoff. */\nexport async function fetchWithRetry(\n url: string,\n opts: RetryOpts = {},\n): Promise<Response> {\n const {\n fetchImpl = fetch,\n sleepImpl = sleep,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n maxAttempts = DEFAULT_MAX_ATTEMPTS,\n backoffMs = DEFAULT_BACKOFF_MS,\n } = opts;\n\n const failures: string[] = [];\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const resp = await fetchImpl(url, {\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);\n return resp;\n } catch (err: any) {\n failures.push(`attempt ${attempt}: ${err.message}`);\n if (attempt < maxAttempts) {\n await sleepImpl(backoffMs * 2 ** (attempt - 1));\n }\n }\n }\n throw new Error(`fetch ${url} failed — ${failures.join('; ')}`);\n}\n","/**\n * Unified in-process MCP server for the PostHog wizard.\n *\n * Provides tools that run locally (secret values never leave the machine):\n * - check_env_keys: Check which env var keys exist in a .env file\n * - set_env_values: Create/update env vars in a .env file\n * - detect_package_manager: Detect the project's package manager(s)\n * - load_skill_menu / install_skill: Skill installation\n * - audit_seed_checks / audit_add_checks / audit_resolve_checks: Audit ledger ownership\n */\n\nimport path from 'path';\nimport fs from 'fs';\nimport { unzipSync } from 'fflate';\nimport { z } from 'zod';\nimport { logToFile } from '@utils/debug';\nimport { analytics } from '@utils/analytics';\nimport { writeJsonAtomic, makeMutex } from '@utils/atomic-ledger';\nimport type { PackageManagerDetector } from './detection/package-manager';\nimport {\n AUDIT_CHECKS_FILE,\n coerceAuditChecks,\n type AuditCheck,\n type AuditStatus,\n} from './programs/audit/types';\nimport { type WizardAskBridge, isFullyCancelled } from './wizard-ask-bridge';\nimport { createSecretVault, type SecretVault } from './secret-vault';\nimport { fetchWithRetry, type RetryOpts } from './fetch-retry';\nimport {\n buildOrchestratorTools,\n type OrchestratorToolsContext,\n} from './agent/runner/sequence/orchestrator/queue-tools';\n\n// ---------------------------------------------------------------------------\n// SDK dynamic import (ESM module loaded once, cached)\n// ---------------------------------------------------------------------------\n\nlet _sdkModule: any = null;\nasync function getSDKModule(): Promise<any> {\n if (!_sdkModule) {\n _sdkModule = await import('@anthropic-ai/claude-agent-sdk');\n }\n return _sdkModule;\n}\n\n// ---------------------------------------------------------------------------\n// Skill types\n// ---------------------------------------------------------------------------\n\nexport type SkillEntry = {\n id: string;\n name: string;\n downloadUrl: string;\n /** The hyphenated skill-group prefix of `id` (e.g. `posthog-integration-install`). */\n group?: string;\n /** The detection id this variant serves (e.g. `rails`, `react-router`). */\n framework?: string;\n /** The variant a bare framework id resolves to when its family has several. */\n default?: boolean;\n /** This entry's download is a bundle JSON of every variant, not a single skill's zip. */\n bundle?: boolean;\n /** Menu-only: the variants inside a bundle, expanded into entries of their own on fetch. */\n variants?: { id: string; framework?: string; default?: boolean }[];\n};\n\n/** A bundle's files, keyed by variant short id then path. */\nexport type SkillBundle = {\n id: string;\n variants: Record<string, Record<string, string>>;\n};\n\n/**\n * Entry in the wizard's runtime CLI registry. Mirrors the shape context-mill\n * publishes under `cliEntries` inside `skill-menu.json`. The wizard uses these\n * to register skill-backed subcommands at runtime instead of from a baked\n * build-time snapshot.\n */\nexport type CliEntry = {\n skillId: string;\n role: 'command' | 'skill' | 'internal';\n command?: string;\n parentCommand?: string;\n default?: boolean;\n displayName: string;\n description: string;\n};\n\nexport interface SkillMenu {\n categories: Record<string, SkillEntry[]>;\n /**\n * Skills exposed as CLI commands. Optional because context-mill releases\n * older than the runtime-resolver cutover don't emit this field.\n */\n cliEntries?: CliEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Standalone skill helpers (usable before the MCP server is created)\n// ---------------------------------------------------------------------------\n\n/** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */\nexport function expandBundleEntry(entry: SkillEntry): SkillEntry[] {\n if (!entry.bundle || !entry.variants) return [entry];\n return entry.variants.map((variant) => ({\n ...variant,\n name: entry.name,\n group: entry.group,\n bundle: true,\n downloadUrl: entry.downloadUrl,\n }));\n}\n\n/**\n * Fetch the skill menu from the skills server.\n * Returns parsed data on success, `null` on failure.\n */\nexport async function fetchSkillMenu(\n skillsBaseUrl: string,\n opts: RetryOpts = {},\n): Promise<SkillMenu | null> {\n const menuUrl = `${skillsBaseUrl}/skill-menu.json`;\n try {\n logToFile(`fetchSkillMenu: fetching from ${menuUrl}`);\n const resp = await fetchWithRetry(menuUrl, opts);\n const data = (await resp.json()) as SkillMenu;\n for (const [category, entries] of Object.entries(data.categories)) {\n data.categories[category] = entries.flatMap(expandBundleEntry);\n }\n logToFile(\n `fetchSkillMenu: loaded (${\n Object.keys(data.categories).length\n } categories)`,\n );\n return data;\n } catch (err: any) {\n logToFile(`fetchSkillMenu: error: ${err.message}`);\n return null;\n }\n}\n\n/** Extract a zip buffer, refusing entries that escape destDir (zip-slip). */\nfunction extractZipArchive(zip: Uint8Array, destDir: string): number {\n const root = path.resolve(destDir);\n let written = 0;\n for (const [entryPath, data] of Object.entries(unzipSync(zip))) {\n const target = path.resolve(root, entryPath);\n if (target !== root && !target.startsWith(root + path.sep)) {\n throw new Error(`zip entry escapes destination: ${entryPath}`);\n }\n if (entryPath.endsWith('/')) {\n fs.mkdirSync(target, { recursive: true });\n continue;\n }\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, data);\n written++;\n }\n return written;\n}\n\n/** Unpack the one variant this entry names out of a bundle; the rest is noise and never hits disk. */\nfunction extractBundle(\n bundle: SkillBundle,\n destDir: string,\n entryId: string,\n): number {\n if (\n typeof bundle?.id !== 'string' ||\n typeof bundle?.variants !== 'object' ||\n bundle.variants === null\n ) {\n throw new Error('malformed bundle: expected { id, variants }');\n }\n const files = bundle.variants[entryId.slice(bundle.id.length + 1)];\n if (!files) {\n throw new Error(`bundle ${bundle.id} has no variant \"${entryId}\"`);\n }\n const root = path.resolve(destDir);\n let written = 0;\n for (const [entryPath, contents] of Object.entries(files)) {\n const target = path.resolve(root, entryPath);\n if (target !== root && !target.startsWith(root + path.sep)) {\n throw new Error(`bundle entry escapes destination: ${entryPath}`);\n }\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, contents);\n written++;\n }\n return written;\n}\n\n/** Download a URL to a buffer, retrying transient failures with backoff. */\nasync function downloadWithRetry(\n url: string,\n opts: RetryOpts = {},\n): Promise<Uint8Array> {\n const resp = await fetchWithRetry(url, opts);\n return new Uint8Array(await resp.arrayBuffer());\n}\n\n/**\n * Download and extract a skill.\n * By default installs to `<installDir>/.claude/skills/<id>/`.\n * Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`).\n */\nexport async function downloadSkill(\n skillEntry: SkillEntry,\n installDir: string,\n skillsRoot?: string,\n): Promise<{ success: boolean; error?: string }> {\n const skillDir = skillsRoot\n ? path.join(installDir, skillsRoot, skillEntry.id)\n : path.join(installDir, '.claude', 'skills', skillEntry.id);\n let step: 'download' | 'extract' = 'download';\n\n try {\n fs.mkdirSync(skillDir, { recursive: true });\n const data = await downloadWithRetry(skillEntry.downloadUrl);\n step = 'extract';\n const fileCount = skillEntry.bundle\n ? extractBundle(\n JSON.parse(Buffer.from(data).toString('utf8')) as SkillBundle,\n skillDir,\n skillEntry.id,\n )\n : extractZipArchive(data, skillDir);\n fs.writeFileSync(path.join(skillDir, '.posthog-wizard'), '');\n\n logToFile(\n `downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl} (${fileCount} files)`,\n );\n return { success: true };\n } catch (err: any) {\n logToFile(`downloadSkill: error: ${err.message}`);\n // A skill-less run still reports success — keep the failure visible.\n analytics.wizardCapture('skill install failed', {\n skill_id: skillEntry.id,\n step,\n platform: process.platform,\n error: String(err.message).slice(0, 500),\n });\n return { success: false, error: err.message };\n }\n}\n\n/**\n * Structured result for installSkillById.\n * - `ok`: the skill was fetched and extracted; `path` is where it lives\n * relative to installDir.\n * - `menu-fetch-failed`: couldn't fetch or parse the skill menu.\n * - `skill-not-found`: the menu didn't contain a skill with this id.\n * - `download-failed`: found the skill but download/extract failed;\n * `message` has the underlying error.\n */\nexport type InstallSkillResult =\n | { kind: 'ok'; path: string }\n | { kind: 'menu-fetch-failed' }\n | { kind: 'skill-not-found'; skillId: string }\n | { kind: 'download-failed'; message: string };\n\n/**\n * High-level \"install a skill by ID\" helper. Fetches the skill menu,\n * finds the skill, downloads and extracts it. Programs should use this\n * instead of composing fetchSkillMenu + downloadSkill themselves.\n */\nexport async function installSkillById(\n skillId: string,\n installDir: string,\n skillsBaseUrl: string,\n skillsRoot?: string,\n): Promise<InstallSkillResult> {\n const menu = await fetchSkillMenu(skillsBaseUrl);\n if (!menu) return { kind: 'menu-fetch-failed' };\n\n const skill = Object.values(menu.categories)\n .flat()\n .find((s) => s.id === skillId);\n if (!skill) return { kind: 'skill-not-found', skillId };\n\n const result = await downloadSkill(skill, installDir, skillsRoot);\n if (!result.success) {\n return { kind: 'download-failed', message: result.error ?? 'unknown' };\n }\n\n const relPath = skillsRoot\n ? `${skillsRoot}/${skillId}`\n : `.claude/skills/${skillId}`;\n return { kind: 'ok', path: relPath };\n}\n\n// ---------------------------------------------------------------------------\n// Options for creating the wizard tools server\n// ---------------------------------------------------------------------------\n\nexport interface WizardToolsOptions {\n /** Root directory of the project being analyzed */\n workingDirectory: string;\n\n /** Framework-specific package manager detector */\n detectPackageManager: PackageManagerDetector;\n\n /** Base URL for the skills server (e.g. http://localhost:8765 or GitHub releases URL) */\n skillsBaseUrl: string;\n\n /**\n * Bridge that drives the `wizard_ask` overlay. When omitted, the\n * `wizard_ask` tool is still registered but returns an error explaining\n * the host is non-interactive — keeps the tool surface stable across\n * CI/dev environments.\n */\n askBridge?: WizardAskBridge;\n\n /**\n * Per-run cap on `wizard_ask` invocations. Defaults to {@link DEFAULT_ASK_MAX_QUESTIONS}.\n * The 4th call always returns a \"batch your questions\" error regardless\n * of this cap — see {@link ASK_BATCH_THRESHOLD}.\n */\n askMaxQuestions?: number;\n\n /**\n * Optional secret vault. When provided, tools that handle sensitive\n * values (wizard_ask with `sensitive: true`, set_env_values) route\n * those values through the vault and return opaque refs to the agent\n * instead of raw strings — so the LLM never sees them. When omitted\n * (e.g. in unit tests), a fresh vault is created internally.\n */\n secretVault?: SecretVault;\n\n /**\n * Orchestrator queue context. Present only when the `wizard-orchestrator`\n * flag routes the run to the orchestrator; when set, the orchestrator tools\n * (enqueue_task, complete_task, read_handoffs) are registered. Absent on the\n * linear path.\n */\n orchestrator?: OrchestratorToolsContext;\n}\n\n/** Default per-run cap on wizard_ask calls when no override is provided. */\nexport const DEFAULT_ASK_MAX_QUESTIONS = 10;\n/** The call after this many returns a one-time batch-your-questions nudge. */\nexport const ASK_BATCH_THRESHOLD = 3;\n\nexport type AskCapDecision =\n | { kind: 'ok' }\n | {\n kind: 'capped';\n reason: 'max_questions' | 'adjacency';\n message: string;\n };\n\n/**\n * Pure decision function for the wizard_ask caps. Returns whether the\n * upcoming call should proceed and, if not, the error message to surface\n * to the agent. Extracted so the policy can be unit-tested without\n * spinning up an MCP server.\n *\n * The adjacency nudge fires exactly once per run (the caller records it\n * via `adjacencyNudged`) — flows that legitimately need several\n * sequential, answer-dependent asks then proceed up to `maxQuestions`.\n * Without the flag the rejected call would never advance the counter and\n * every later call would be rejected, making caps above the threshold\n * unreachable.\n */\nexport function evaluateAskCap(\n callCount: number,\n maxQuestions: number,\n adjacencyNudged = false,\n): AskCapDecision {\n if (callCount >= maxQuestions) {\n return {\n kind: 'capped',\n reason: 'max_questions',\n message: `Error: wizard_ask cap reached (${maxQuestions} calls in this run). Proceed with sensible defaults using the answers you already have, or emit [ABORT] requirements-incomplete.`,\n };\n }\n if (!adjacencyNudged && callCount >= ASK_BATCH_THRESHOLD) {\n return {\n kind: 'capped',\n reason: 'adjacency',\n message: `Error: too many wizard_ask calls in a row (${callCount} so far). Batch the remaining questions into a single call — the schema accepts up to 8 questions per invocation. If the remaining questions truly depend on earlier answers, ask again and they will go through.`,\n };\n }\n return { kind: 'ok' };\n}\n\n// ---------------------------------------------------------------------------\n// Env file helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve filePath relative to workingDirectory, rejecting path traversal.\n */\nexport function resolveEnvPath(\n workingDirectory: string,\n filePath: string,\n): string {\n const resolved = path.resolve(workingDirectory, filePath);\n if (\n !resolved.startsWith(workingDirectory + path.sep) &&\n resolved !== workingDirectory\n ) {\n throw new Error(\n `Path traversal rejected: \"${filePath}\" resolves outside working directory`,\n );\n }\n return resolved;\n}\n\n/**\n * Ensure the given env file basename is covered by .gitignore in the working directory.\n * Creates .gitignore if it doesn't exist; appends the entry if missing.\n */\nexport function ensureGitignoreCoverage(\n workingDirectory: string,\n envFileName: string,\n): void {\n const gitignorePath = path.join(workingDirectory, '.gitignore');\n\n if (fs.existsSync(gitignorePath)) {\n const content = fs.readFileSync(gitignorePath, 'utf8');\n // Check if the file (or a glob covering it) is already listed\n if (content.split('\\n').some((line) => line.trim() === envFileName)) {\n return;\n }\n const newContent = content.endsWith('\\n')\n ? `${content}${envFileName}\\n`\n : `${content}\\n${envFileName}\\n`;\n fs.writeFileSync(gitignorePath, newContent, 'utf8');\n } else {\n fs.writeFileSync(gitignorePath, `${envFileName}\\n`, 'utf8');\n }\n}\n\n/**\n * Parse a .env file's content and return the set of defined key names.\n */\nexport function parseEnvKeys(content: string): Set<string> {\n const keys = new Set<string>();\n for (const line of content.split('\\n')) {\n const match = line.match(/^\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*=/);\n if (match) {\n keys.add(match[1]);\n }\n }\n return keys;\n}\n\n/**\n * Merge key-value pairs into existing .env content.\n * Updates existing keys in-place, appends new keys at the end.\n */\nexport function mergeEnvValues(\n content: string,\n values: Record<string, string>,\n): string {\n let result = content;\n const updatedKeys = new Set<string>();\n\n for (const [key, value] of Object.entries(values)) {\n const regex = new RegExp(`^(\\\\s*${key}\\\\s*=).*$`, 'm');\n if (regex.test(result)) {\n result = result.replace(regex, `$1${value}`);\n updatedKeys.add(key);\n }\n }\n\n const newKeys = Object.entries(values).filter(\n ([key]) => !updatedKeys.has(key),\n );\n if (newKeys.length > 0) {\n if (result.length > 0 && !result.endsWith('\\n')) {\n result += '\\n';\n }\n for (const [key, value] of newKeys) {\n result += `${key}=${value}\\n`;\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Audit ledger helpers\n// ---------------------------------------------------------------------------\n\nconst AUDIT_STATUSES: readonly AuditStatus[] = [\n 'pending',\n 'pass',\n 'error',\n 'warning',\n 'suggestion',\n];\n\nconst auditCheckSchema = z.object({\n id: z.string().min(1),\n area: z.string().min(1),\n label: z.string().min(1),\n status: z.enum(AUDIT_STATUSES as [AuditStatus, ...AuditStatus[]]),\n file: z.string().optional(),\n details: z.string().optional(),\n});\n\nconst auditUpdateSchema = z.object({\n id: z.string().min(1),\n status: z.enum(AUDIT_STATUSES as [AuditStatus, ...AuditStatus[]]),\n file: z.string().optional(),\n details: z.string().optional(),\n});\n\n/** Atomically write the audit ledger. Thin typed wrapper over writeJsonAtomic. */\nfunction writeLedgerAtomic(targetPath: string, checks: AuditCheck[]): void {\n writeJsonAtomic(targetPath, checks);\n}\n\n/**\n * Apply a batch of patches to the ledger by id. Returns the new array and the\n * list of update ids that didn't match any existing check.\n */\nfunction applyAuditUpdates(\n current: AuditCheck[],\n updates: Array<{\n id: string;\n status: AuditStatus;\n file?: string;\n details?: string;\n }>,\n): { next: AuditCheck[]; unknown: string[] } {\n const byId = new Map(current.map((c) => [c.id, c]));\n const unknown: string[] = [];\n\n for (const u of updates) {\n const existing = byId.get(u.id);\n if (!existing) {\n unknown.push(u.id);\n continue;\n }\n byId.set(u.id, {\n ...existing,\n status: u.status,\n ...(u.file !== undefined ? { file: u.file } : {}),\n ...(u.details !== undefined ? { details: u.details } : {}),\n });\n }\n\n return {\n next: current.map((c) => byId.get(c.id) ?? c),\n unknown,\n };\n}\n\n/**\n * Append new checks to a seeded ledger. Duplicate ids are reported without\n * mutating the current ledger, including duplicates inside the additions.\n */\nfunction applyAuditAdditions(\n current: AuditCheck[],\n additions: AuditCheck[],\n): { next: AuditCheck[]; duplicates: string[] } {\n const existingIds = new Set(current.map((c) => c.id));\n const additionIds = new Set<string>();\n const duplicates: string[] = [];\n\n for (const check of additions) {\n if (existingIds.has(check.id) || additionIds.has(check.id)) {\n duplicates.push(check.id);\n continue;\n }\n additionIds.add(check.id);\n }\n\n if (duplicates.length > 0) {\n return { next: current, duplicates };\n }\n\n return { next: [...current, ...additions], duplicates: [] };\n}\n\nfunction readLedger(targetPath: string): AuditCheck[] {\n if (!fs.existsSync(targetPath)) return [];\n try {\n const parsed = JSON.parse(fs.readFileSync(targetPath, 'utf8'));\n return coerceAuditChecks(parsed);\n } catch {\n return [];\n }\n}\n\ntype AppendAuditChecksResult =\n | { ok: true; added: number }\n | { ok: false; reason: 'missing-ledger' }\n | { ok: false; reason: 'duplicate-ids'; ids: string[] };\n\nfunction appendAuditChecksToLedger(\n targetPath: string,\n additions: AuditCheck[],\n): AppendAuditChecksResult {\n if (!fs.existsSync(targetPath)) {\n return { ok: false, reason: 'missing-ledger' };\n }\n\n const current = readLedger(targetPath);\n const { next, duplicates } = applyAuditAdditions(current, additions);\n if (duplicates.length > 0) {\n return { ok: false, reason: 'duplicate-ids', ids: duplicates };\n }\n\n writeLedgerAtomic(targetPath, next);\n return { ok: true, added: additions.length };\n}\n\n// ---------------------------------------------------------------------------\n// Server factory\n// ---------------------------------------------------------------------------\n\nconst SERVER_NAME = 'wizard-tools';\n\n/**\n * Create the unified in-process MCP server with all wizard tools.\n * Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.\n */\nexport async function createWizardToolsServer(options: WizardToolsOptions) {\n const {\n workingDirectory,\n detectPackageManager,\n skillsBaseUrl,\n askBridge,\n askMaxQuestions = DEFAULT_ASK_MAX_QUESTIONS,\n secretVault = createSecretVault(),\n orchestrator,\n } = options;\n const sdk = await getSDKModule();\n const { tool, createSdkMcpServer } = sdk;\n\n // Per-server counter for wizard_ask call accounting (adjacency + total cap).\n let askCallCount = 0;\n // The adjacency nudge fires once per run; after that only the total cap applies.\n let askAdjacencyNudged = false;\n\n // Pre-fetch skill menu so category names are available in the tool schema\n let cachedSkillMenu: Record<string, SkillEntry[]> = {};\n let categoryNames: [string, ...string[]] = ['integration'];\n\n const menu = await fetchSkillMenu(skillsBaseUrl);\n if (menu) {\n cachedSkillMenu = menu.categories;\n }\n\n const keys = Object.keys(cachedSkillMenu);\n if (keys.length > 0) {\n categoryNames = keys as [string, ...string[]];\n }\n\n // -- check_env_keys -------------------------------------------------------\n\n const checkEnvKeys = tool(\n 'check_env_keys',\n 'Check which environment variable keys are present or missing in a .env file. Never reveals values.',\n {\n filePath: z\n .string()\n .describe('Path to the .env file, relative to the project root'),\n keys: z\n .array(z.string())\n .describe('Environment variable key names to check'),\n },\n (args: { filePath: string; keys: string[] }) => {\n const resolved = resolveEnvPath(workingDirectory, args.filePath);\n logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(', ')}`);\n\n const existingKeys: Set<string> = fs.existsSync(resolved)\n ? parseEnvKeys(fs.readFileSync(resolved, 'utf8'))\n : new Set<string>();\n\n const results: Record<string, 'present' | 'missing'> = {};\n for (const key of args.keys) {\n results[key] = existingKeys.has(key) ? 'present' : 'missing';\n }\n\n return {\n content: [\n { type: 'text' as const, text: JSON.stringify(results, null, 2) },\n ],\n };\n },\n );\n\n // -- set_env_values -------------------------------------------------------\n\n const setEnvValues = tool(\n 'set_env_values',\n 'Create or update environment variable keys in a .env file. Creates the file if it does not exist. Ensures .gitignore coverage. Each value can be either a literal string or a secret reference of the form `{ \"secretRef\": \"secret:...\" }` returned by another tool (e.g. wizard_ask). Secret references are resolved locally — the actual value is written to the file but never returned to the agent.',\n {\n filePath: z\n .string()\n .describe('Path to the .env file, relative to the project root'),\n values: z\n .record(\n z.string(),\n z.union([z.string(), z.object({ secretRef: z.string() })]),\n )\n .describe(\n 'Key → (literal string OR { secretRef } pointing to a vaulted secret)',\n ),\n },\n (args: {\n filePath: string;\n values: Record<string, string | { secretRef: string }>;\n }) => {\n // Block the wrong key name — the correct key is NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN or similar\n const forbidden = Object.keys(args.values).find(\n (k) => k.toUpperCase() === 'POSTHOG_KEY',\n );\n if (forbidden) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: \"${forbidden}\" is not a valid PostHog env var name. Use the project-specific key name from your framework's integration guide (e.g. NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN).`,\n },\n ],\n isError: true,\n };\n }\n\n // Resolve any secret refs from the vault before writing.\n const resolvedValues: Record<string, string> = {};\n const resolvedRefKeys: string[] = [];\n for (const [key, val] of Object.entries(args.values)) {\n if (typeof val === 'string') {\n resolvedValues[key] = val;\n } else {\n const secret = secretVault.get(val.secretRef);\n if (secret === undefined) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: secret reference \"${val.secretRef}\" for key \"${key}\" is not known to the vault. The ref may have expired, been minted in a different run, or been mistyped.`,\n },\n ],\n isError: true,\n };\n }\n resolvedValues[key] = secret;\n resolvedRefKeys.push(key);\n }\n }\n\n const resolved = resolveEnvPath(workingDirectory, args.filePath);\n logToFile(\n `set_env_values: ${resolved}, keys: ${Object.keys(resolvedValues).join(\n ', ',\n )}${\n resolvedRefKeys.length > 0\n ? ` (secret refs: ${resolvedRefKeys.join(', ')})`\n : ''\n }`,\n );\n\n const existing = fs.existsSync(resolved)\n ? fs.readFileSync(resolved, 'utf8')\n : '';\n const content = mergeEnvValues(existing, resolvedValues);\n\n // Ensure parent directory exists\n const dir = path.dirname(resolved);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n fs.writeFileSync(resolved, content, 'utf8');\n\n // Ensure .gitignore coverage for this env file\n const envFileName = path.basename(resolved);\n ensureGitignoreCoverage(workingDirectory, envFileName);\n\n return {\n content: [\n {\n type: 'text' as const,\n text: `Updated ${Object.keys(args.values).length} key(s) in ${\n args.filePath\n }`,\n },\n ],\n };\n },\n );\n\n // -- detect_package_manager -----------------------------------------------\n\n const detectPM = tool(\n 'detect_package_manager',\n 'Detect which package manager(s) the project uses. Returns the name, install command, and run command for each detected package manager. Call this before running any install commands.',\n {},\n async () => {\n logToFile(`detect_package_manager: scanning ${workingDirectory}`);\n\n const result = await detectPackageManager(workingDirectory);\n\n logToFile(\n `detect_package_manager: detected ${result.detected.length} package manager(s)`,\n );\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result, null, 2),\n },\n ],\n };\n },\n );\n\n // -- load_skill_menu ------------------------------------------------------\n\n const loadSkillMenu = tool(\n 'load_skill_menu',\n 'Load available PostHog skills for a category. Returns skill IDs and names. Call this first, then use install_skill with the chosen ID.',\n {\n category: z.enum(categoryNames).describe('Skill category'),\n },\n (args: { category: string }) => {\n const skills = cachedSkillMenu[args.category];\n if (!skills || skills.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `No skills found for category \"${args.category}\".`,\n },\n ],\n isError: true,\n };\n }\n\n const menuText = skills.map((s) => `- ${s.id}: ${s.name}`).join('\\n');\n\n logToFile(\n `load_skill_menu: returning ${skills.length} skills for \"${args.category}\"`,\n );\n\n return {\n content: [{ type: 'text' as const, text: menuText }],\n };\n },\n );\n\n // -- install_skill --------------------------------------------------------\n\n const installSkill = tool(\n 'install_skill',\n 'Download and install a PostHog skill by ID. Call load_skill_menu first to see available skills. Extracts the skill to .claude/skills/<skillId>/.',\n {\n skillId: z\n .string()\n .describe(\n 'Skill ID from the skill menu (e.g., \"integration-nextjs-app-router\")',\n ),\n },\n async (args: { skillId: string }) => {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(args.skillId)) {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Error: skillId must be lowercase alphanumeric with hyphens.',\n },\n ],\n isError: true,\n };\n }\n\n // Look up download URL from cached menu\n const allSkills: SkillEntry[] = Object.values(cachedSkillMenu).flat();\n const skill = allSkills.find((s) => s.id === args.skillId);\n if (!skill) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: skill \"${args.skillId}\" not found. Use load_skill_menu to see available skills.`,\n },\n ],\n isError: true,\n };\n }\n\n const result = await downloadSkill(skill, workingDirectory);\n if (result.success) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Skill installed to .claude/skills/${args.skillId}/`,\n },\n ],\n };\n } else {\n // The agent only sees a tool-result string — report the failure too.\n analytics.captureException(\n new Error('Skill install failed: download-failed'),\n {\n source: 'install_skill_tool',\n skill_id: args.skillId,\n error_detail: String(result.error).slice(0, 500),\n platform: process.platform,\n },\n );\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error installing skill: ${result.error}`,\n },\n ],\n isError: true,\n };\n }\n },\n );\n\n // -- audit_seed_checks ----------------------------------------------------\n\n const auditLedgerPath = path.join(workingDirectory, AUDIT_CHECKS_FILE);\n const auditMutex = makeMutex();\n\n const auditSeedChecks = tool(\n 'audit_seed_checks',\n 'Seed the audit ledger at .posthog-audit-checks.json with the full set of pending checks. Call this once at the start of the audit. Atomically replaces any existing ledger.',\n {\n checks: z\n .array(auditCheckSchema)\n .describe('Full pending checklist to write to the ledger'),\n },\n async (args: { checks: AuditCheck[] }) => {\n return auditMutex(() => {\n writeLedgerAtomic(auditLedgerPath, args.checks);\n logToFile(`audit_seed_checks: wrote ${args.checks.length} entries`);\n return {\n content: [\n {\n type: 'text' as const,\n text: `Seeded ${args.checks.length} audit checks.`,\n },\n ],\n };\n });\n },\n );\n\n // -- audit_add_checks -----------------------------------------------------\n\n const auditAddChecks = tool(\n 'audit_add_checks',\n 'Append one or more pending checks to the existing audit ledger at .posthog-audit-checks.json. Call audit_seed_checks first. Atomically rejects duplicate ids without changing the ledger.',\n {\n checks: z\n .array(auditCheckSchema)\n .min(1)\n .describe('Additional checks to append to the existing ledger'),\n },\n async (args: { checks: AuditCheck[] }) => {\n return auditMutex(() => {\n const result = appendAuditChecksToLedger(auditLedgerPath, args.checks);\n\n if (!result.ok) {\n if (result.reason === 'missing-ledger') {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Error: audit ledger does not exist. Run audit_seed_checks first.',\n },\n ],\n isError: true,\n };\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: duplicate check id(s): ${result.ids.join(\n ', ',\n )}. Check ids must be unique.`,\n },\n ],\n isError: true,\n };\n }\n\n logToFile(`audit_add_checks: added ${result.added} entries`);\n return {\n content: [\n {\n type: 'text' as const,\n text: `Added ${result.added} audit check(s).`,\n },\n ],\n };\n });\n },\n );\n\n // -- audit_resolve_checks -------------------------------------------------\n\n const auditResolveChecks = tool(\n 'audit_resolve_checks',\n \"Resolve one or more audit checks by id. Patches each entry's status (and optional file/details) and writes the ledger back atomically. Concurrent calls serialize.\",\n {\n updates: z\n .array(auditUpdateSchema)\n .min(1)\n .describe('Patches to apply, keyed by check id'),\n },\n async (args: {\n updates: Array<{\n id: string;\n status: AuditStatus;\n file?: string;\n details?: string;\n }>;\n }) => {\n return auditMutex(() => {\n const current = readLedger(auditLedgerPath);\n const { next, unknown } = applyAuditUpdates(current, args.updates);\n\n if (unknown.length > 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: unknown check id(s): ${unknown.join(\n ', ',\n )}. Run audit_seed_checks first or check the id.`,\n },\n ],\n isError: true,\n };\n }\n\n writeLedgerAtomic(auditLedgerPath, next);\n logToFile(\n `audit_resolve_checks: applied ${args.updates.length} update(s)`,\n );\n\n return {\n content: [\n {\n type: 'text' as const,\n text: `Resolved ${args.updates.length} check(s).`,\n },\n ],\n };\n });\n },\n );\n\n // -- wizard_ask -----------------------------------------------------------\n\n const askQuestionSchema = z.object({\n id: z\n .string()\n .min(1)\n .describe('Stable key for the answer in the response map'),\n prompt: z.string().min(1).describe('Question text shown to the user'),\n kind: z\n .enum(['single', 'multi', 'text'])\n .describe(\n \"'single' = pick one option, 'multi' = pick any, 'text' = free-form single-line answer\",\n ),\n options: z\n .array(\n z.object({\n label: z.string(),\n value: z.string(),\n description: z\n .string()\n .optional()\n .describe(\n 'Optional secondary line shown dimmed and wrapped beneath the ' +\n 'label (multi-select only). Use when a choice needs more than a ' +\n 'title — e.g. what a custom scout watches and what makes it speak up.',\n ),\n }),\n )\n .optional()\n .describe('Required for kind=single|multi; ignored for kind=text'),\n required: z.boolean().optional().describe('Defaults to true'),\n sensitive: z\n .boolean()\n .optional()\n .describe(\n \"Only valid for kind='text'. When true, the user's answer is stored in the wizard's secret vault and returned to you as { secretRef: 'secret:...' } instead of the raw string. Use for API keys, tokens, and any other secret the user types in. The secretRef is only resolved by wizard-tools that accept it (e.g. set_env_values) — it is NOT resolved when passed to other MCP tools (e.g. PostHog data-warehouse tools), which will reject it. For a secret that must reach another tool, write it to the env with set_env_values first, or use that tool's own credential-reference flow.\",\n ),\n });\n\n const wizardAsk = tool(\n 'wizard_ask',\n 'Ask the user one or more structured questions and wait for their answers. ' +\n 'Use this whenever you would otherwise inline a question in your text output. ' +\n 'Batch related questions into a single call (up to 8) rather than asking one at a ' +\n 'time; sequential calls are fine when later questions genuinely depend on earlier ' +\n 'answers. A fully cancelled or timed-out response does NOT count against the per-run ' +\n 'cap — treat it as \"the user declined\" and fall back gracefully (e.g. hand over a ' +\n 'deep link) without worrying about a wasted call.',\n {\n questions: z.array(askQuestionSchema).min(1).max(8),\n },\n async (args: {\n questions: Array<{\n id: string;\n prompt: string;\n kind: 'single' | 'multi' | 'text';\n options?: { label: string; value: string }[];\n required?: boolean;\n sensitive?: boolean;\n }>;\n }) => {\n if (!askBridge) {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Error: wizard_ask is not available in this environment (CI / non-interactive). Proceed with sensible defaults or emit [ABORT] requirements-incomplete.',\n },\n ],\n isError: true,\n };\n }\n\n const capDecision = evaluateAskCap(\n askCallCount,\n askMaxQuestions,\n askAdjacencyNudged,\n );\n if (capDecision.kind === 'capped') {\n if (capDecision.reason === 'adjacency') {\n askAdjacencyNudged = true;\n }\n analytics.wizardCapture('wizard_ask capped', {\n reason: capDecision.reason,\n call_count: askCallCount,\n max_questions: askMaxQuestions,\n });\n return {\n content: [{ type: 'text' as const, text: capDecision.message }],\n isError: true,\n };\n }\n\n // Validate that single/multi questions include options. The schema\n // alone can't enforce a per-kind requirement.\n for (const q of args.questions) {\n if (\n (q.kind === 'single' || q.kind === 'multi') &&\n (!q.options || q.options.length === 0)\n ) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: question \"${q.id}\" has kind=\"${q.kind}\" but no options. Provide at least one { label, value } option, or change kind to \"text\".`,\n },\n ],\n isError: true,\n };\n }\n if (q.sensitive && q.kind !== 'text') {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: question \"${q.id}\" sets sensitive=true but kind=\"${q.kind}\". Only kind=\"text\" answers can be vaulted as secrets.`,\n },\n ],\n isError: true,\n };\n }\n }\n\n const ids = new Set<string>();\n for (const q of args.questions) {\n if (ids.has(q.id)) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: duplicate question id \"${q.id}\". Each question must have a unique id.`,\n },\n ],\n isError: true,\n };\n }\n ids.add(q.id);\n }\n\n askCallCount += 1;\n\n try {\n const answers = await askBridge.request({ questions: args.questions });\n\n // A fully cancelled/timed-out ask (the user dismissed the overlay or let\n // it time out) shouldn't burn the per-run cap. Otherwise one cancellation\n // exhausts the budget for every remaining source and forces a deep-link\n // fallback even when the user was willing to answer. Refund the slot we\n // optimistically took so cancellation is free.\n if (isFullyCancelled(answers)) {\n askCallCount -= 1;\n }\n\n // For any question marked sensitive, move the raw answer into the\n // vault and replace it with an opaque ref before returning to the\n // agent — so the secret never enters the LLM conversation.\n const sensitiveById = new Map(\n args.questions\n .filter((q) => q.sensitive)\n .map((q) => [q.id, q.prompt]),\n );\n const sanitised: Record<\n string,\n string | string[] | { secretRef: string }\n > = {};\n for (const [id, answer] of Object.entries(answers)) {\n const label = sensitiveById.get(id);\n if (\n label !== undefined &&\n typeof answer === 'string' &&\n answer !== '__cancelled__'\n ) {\n const ref = secretVault.put(answer, {\n label,\n source: 'wizard_ask',\n });\n sanitised[id] = { secretRef: ref };\n logToFile(`wizard_ask: vaulted answer for \"${id}\" as ${ref}`);\n } else {\n sanitised[id] = answer;\n }\n }\n\n logToFile(\n `wizard_ask: resolved ${Object.keys(answers).length} answer(s) for ${\n args.questions.length\n } question(s)`,\n );\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ answers: sanitised }, null, 2),\n },\n ],\n };\n } catch (err: any) {\n // A failed ask never reached the user, so it shouldn't burn the\n // per-run cap either — otherwise a transient bridge error eats the\n // budget for every remaining source.\n askCallCount -= 1;\n logToFile(`wizard_ask: error: ${err?.message ?? err}`);\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: wizard_ask failed: ${err?.message ?? String(err)}`,\n },\n ],\n isError: true,\n };\n }\n },\n );\n\n // -- Assemble server ------------------------------------------------------\n\n const orchestratorTools = orchestrator\n ? buildOrchestratorTools(tool, orchestrator)\n : [];\n\n return createSdkMcpServer({\n name: SERVER_NAME,\n version: '1.0.0',\n tools: [\n checkEnvKeys,\n setEnvValues,\n detectPM,\n loadSkillMenu,\n installSkill,\n auditSeedChecks,\n auditAddChecks,\n auditResolveChecks,\n wizardAsk,\n ...orchestratorTools,\n ],\n });\n}\n\n/** Tool names exposed by the wizard-tools server, keyed for selective use. */\n// SDK expects MCP tool names in allowedTools/disallowedTools to be the\n// fully-qualified `mcp__<server>__<tool>` form (sdk.d.ts: \"Fully-qualified\n// MCP tool name, e.g. mcp__server__tool_name.\"). The colon form silently\n// fails to match, which made every program's `disallowedTools` entry a no-op.\nexport const WIZARD_TOOL_NAMES = {\n checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`,\n setEnvValues: `mcp__${SERVER_NAME}__set_env_values`,\n detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`,\n loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`,\n installSkill: `mcp__${SERVER_NAME}__install_skill`,\n auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`,\n auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`,\n auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`,\n wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`,\n enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`,\n completeTask: `mcp__${SERVER_NAME}__complete_task`,\n readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Test-only exports\n// ---------------------------------------------------------------------------\n\nexport const __test = {\n extractZipArchive,\n extractBundle,\n fetchWithRetry,\n downloadWithRetry,\n writeLedgerAtomic,\n readLedger,\n applyAuditAdditions,\n appendAuditChecksToLedger,\n applyAuditUpdates,\n makeMutex,\n AUDIT_CHECKS_FILE,\n};\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { f as getUI, s as logToFile, u as WIZARD_YARA_REPORT_FILE } from "./debug-
|
|
2
|
-
import { n as analytics } from "./analytics-
|
|
3
|
-
import { r as AUDIT_REPORT_FILE } from "./types-
|
|
1
|
+
import { f as getUI, s as logToFile, u as WIZARD_YARA_REPORT_FILE } from "./debug-DBmxx59Q.js";
|
|
2
|
+
import { n as analytics } from "./analytics-DQEuL6qS.js";
|
|
3
|
+
import { r as AUDIT_REPORT_FILE } from "./types-BRe_EzTW.js";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import fg from "fast-glob";
|
|
@@ -632,4 +632,4 @@ async function scanSkillFiles(cwd, skillDir, llmProvider) {
|
|
|
632
632
|
//#endregion
|
|
633
633
|
export { flushScanReport as a, isWizardDocumentationPath as c, repeatBlockReason as d, scanAndTriage as f, SETUP_REPORT_FILE as h, createRepeatBlockTracker as i, prewarmYaraScanner as l, EVENT_PLAN_FILE as m, createPostToolUseYaraHooks as n, formatScanReport as o, writeScanReport as p, createPreToolUseYaraHooks as r, formatYaraAbortMessage as s, captureScanReport as t, recordExternalScan as u };
|
|
634
634
|
|
|
635
|
-
//# sourceMappingURL=yara-hooks-
|
|
635
|
+
//# sourceMappingURL=yara-hooks-pw86rZ8y.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yara-hooks-CPH8N4yz.js","names":["EVENTS_AUDIT_REPORT_FILE"],"sources":["../src/lib/skill-install.ts","../src/lib/programs/events-audit/constants.ts","../src/lib/programs/posthog-integration/constants.ts","../src/lib/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 our GitHub releases 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 const url = urlMatch[1];\n return (\n url.startsWith('https://github.com/PostHog/context-mill/releases/') ||\n /^http:\\/\\/localhost:\\d+\\//.test(url)\n );\n}\n","/**\n * Leaf-level constants for the events-audit program.\n *\n * Kept separate from `index.ts` so files like `yara-hooks.ts` can import\n * the filename constants without dragging in `index.ts`'s heavier imports\n * (agent-runner, audit/seed, etc.) — which can create import cycles.\n */\n\nexport const SETUP_REPORT_FILE = 'posthog-events-audit-report.md';\nexport const EVENT_INVENTORY_FILE = '.posthog-events-inventory.json';\n/** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */\nexport const EVENT_INVENTORY_PART_PATTERN =\n /^\\.posthog-events-inventory\\.part-\\d+\\.json$/;\n","/**\n * Leaf-level constants for the posthog-integration program.\n *\n * Kept separate from `index.ts` so files like `yara-hooks.ts` can import\n * the filename constants without dragging in `index.ts`'s heavier imports\n * (agent-interface, framework-config, etc.) — which would create an import\n * cycle through agent-interface → yara-hooks.\n */\n\nexport const EVENT_PLAN_FILE = '.posthog-events.json';\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 { analytics } from '@utils/analytics';\nimport { getUI } from '@ui';\nimport type { WizardSession } from '@lib/wizard-session';\nimport { isSkillInstallCommand } from './skill-install';\nimport { WIZARD_YARA_REPORT_FILE } from '@utils/paths';\n// TODO(wizard#594): invert this dependency.\n// L2 infra (yara-hooks) imports product-specific filename constants from\n// individual programs. The leaf `constants.ts` modules break the *import*\n// cycle but don't fix the *layering* concern: this file knowing about\n// `events-audit`, `posthog-integration`, and `audit` violates \"product\n// knowledge never enters infrastructure code.\" Proper fix is inverted —\n// programs declare their own doc paths, the hooks read from a generic\n// registry. For now: every new program emitting a PII-shaped report has\n// to be added here. Land that cleanup before adding a fourth entry.\nimport {\n SETUP_REPORT_FILE as EVENTS_AUDIT_REPORT_FILE,\n EVENT_INVENTORY_FILE,\n EVENT_INVENTORY_PART_PATTERN,\n} from '@lib/programs/events-audit/constants';\nimport { AUDIT_REPORT_FILE } from '@lib/programs/audit/types';\nimport { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/constants';\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\n/** Which content surface a warlock rule applies to (from rule `scan_context`). */\nexport type ScanContext = 'command' | 'input' | 'output';\n\n// ─── Scan Report Accumulator ─────────────────────────────────────\n\nexport type ScanAction = 'blocked' | 'reverted' | 'warned' | 'aborted';\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 * 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 */\nexport function flushScanReport(\n session: Pick<WizardSession, 'yaraReport'>,\n): void {\n if (session.yaraReport) {\n const reportPath = writeScanReport();\n if (reportPath) {\n const summary = formatScanReport();\n getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ''}`);\n }\n }\n captureScanReport();\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\nconst SEVERITY_RANK: Record<string, number> = {\n critical: 4,\n high: 3,\n medium: 2,\n low: 1,\n};\n\n/** Return the highest-severity match from a list of matches. */\nfunction 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// ─── 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 * Scans Bash commands before execution for exfiltration, destructive\n * operations, and supply chain violations ('command'-context rules).\n */\nexport function createPreToolUseYaraHooks(\n // Accepted for API symmetry with createPostToolUseYaraHooks, but\n // intentionally unused: PreToolUse Bash always blocks on any flagged\n // command regardless of triage verdict, so the LLM call would be wasted.\n // Future PreToolUse hooks that need triage can wire this through.\n _llmProvider?: LLMProvider,\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}\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 if (matches.length === 0) return {};\n\n const match = highestSeverityMatch(matches);\n\n // Critical severity or a rule asking us to block => terminate; the\n // agent's context may be poisoned by injected instructions.\n const isTerminal =\n match.metadata.severity === 'critical' ||\n match.metadata.action === 'block';\n\n if (isTerminal) {\n recordMatch('PostToolUse', toolName, match, 'aborted');\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 recordMatch('PostToolUse', toolName, match, 'warned');\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 if (matches.length === 0) return {};\n\n // INTENTIONAL ASYMMETRY: skill-install terminates on ANY\n // post-triage match, while Read/Grep only terminates on\n // critical-or-block. Skills are downloaded code from an\n // external source; any flagged content in them is treated as\n // poisoned. Read/Grep, by contrast, may scan project files\n // the user wrote themselves where a medium-severity match\n // can warrant a warning instead of a terminate.\n // TODO(wizard#593): document / lock in this contract with a test.\n const match = highestSeverityMatch(matches);\n recordMatch(\n 'PostToolUse',\n 'Bash (skill install)',\n match,\n 'aborted',\n );\n\n const reason =\n `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${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 * 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 const fileContents: string[] = [];\n for (const filePath of files) {\n try {\n fileContents.push(fs.readFileSync(filePath, 'utf-8'));\n } catch (err) {\n logToFile(`[YARA] Could not read skill file ${filePath}:`, err);\n }\n }\n\n if (fileContents.length === 0) {\n logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);\n return [];\n }\n\n logToFile(\n `[YARA] Scanning ${fileContents.length} files in skill directory: ${skillDir}`,\n );\n\n // Pass 1 (sequential): scan each file through the chunk-aware path —\n // full coverage even for oversized files, and every flagged chunk keeps a\n // reference to the exact content its matches came from.\n const flagged: FlaggedChunk[] = [];\n for (const content of fileContents) {\n flagged.push(...(await scanForContext(content, 'input')));\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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"}
|
|
1
|
+
{"version":3,"file":"yara-hooks-pw86rZ8y.js","names":["EVENTS_AUDIT_REPORT_FILE"],"sources":["../src/lib/skill-install.ts","../src/lib/programs/events-audit/constants.ts","../src/lib/programs/posthog-integration/constants.ts","../src/lib/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 our GitHub releases 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 const url = urlMatch[1];\n return (\n url.startsWith('https://github.com/PostHog/context-mill/releases/') ||\n /^http:\\/\\/localhost:\\d+\\//.test(url)\n );\n}\n","/**\n * Leaf-level constants for the events-audit program.\n *\n * Kept separate from `index.ts` so files like `yara-hooks.ts` can import\n * the filename constants without dragging in `index.ts`'s heavier imports\n * (agent-runner, audit/seed, etc.) — which can create import cycles.\n */\n\nexport const SETUP_REPORT_FILE = 'posthog-events-audit-report.md';\nexport const EVENT_INVENTORY_FILE = '.posthog-events-inventory.json';\n/** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */\nexport const EVENT_INVENTORY_PART_PATTERN =\n /^\\.posthog-events-inventory\\.part-\\d+\\.json$/;\n","/**\n * Leaf-level constants for the posthog-integration program.\n *\n * Kept separate from `index.ts` so files like `yara-hooks.ts` can import\n * the filename constants without dragging in `index.ts`'s heavier imports\n * (agent-interface, framework-config, etc.) — which would create an import\n * cycle through agent-interface → yara-hooks.\n */\n\nexport const EVENT_PLAN_FILE = '.posthog-events.json';\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 { analytics } from '@utils/analytics';\nimport { getUI } from '@ui';\nimport type { WizardSession } from '@lib/wizard-session';\nimport { isSkillInstallCommand } from './skill-install';\nimport { WIZARD_YARA_REPORT_FILE } from '@utils/paths';\n// TODO(wizard#594): invert this dependency.\n// L2 infra (yara-hooks) imports product-specific filename constants from\n// individual programs. The leaf `constants.ts` modules break the *import*\n// cycle but don't fix the *layering* concern: this file knowing about\n// `events-audit`, `posthog-integration`, and `audit` violates \"product\n// knowledge never enters infrastructure code.\" Proper fix is inverted —\n// programs declare their own doc paths, the hooks read from a generic\n// registry. For now: every new program emitting a PII-shaped report has\n// to be added here. Land that cleanup before adding a fourth entry.\nimport {\n SETUP_REPORT_FILE as EVENTS_AUDIT_REPORT_FILE,\n EVENT_INVENTORY_FILE,\n EVENT_INVENTORY_PART_PATTERN,\n} from '@lib/programs/events-audit/constants';\nimport { AUDIT_REPORT_FILE } from '@lib/programs/audit/types';\nimport { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/constants';\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\n/** Which content surface a warlock rule applies to (from rule `scan_context`). */\nexport type ScanContext = 'command' | 'input' | 'output';\n\n// ─── Scan Report Accumulator ─────────────────────────────────────\n\nexport type ScanAction = 'blocked' | 'reverted' | 'warned' | 'aborted';\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 * 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 */\nexport function flushScanReport(\n session: Pick<WizardSession, 'yaraReport'>,\n): void {\n if (session.yaraReport) {\n const reportPath = writeScanReport();\n if (reportPath) {\n const summary = formatScanReport();\n getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ''}`);\n }\n }\n captureScanReport();\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\nconst SEVERITY_RANK: Record<string, number> = {\n critical: 4,\n high: 3,\n medium: 2,\n low: 1,\n};\n\n/** Return the highest-severity match from a list of matches. */\nfunction 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// ─── 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 * Scans Bash commands before execution for exfiltration, destructive\n * operations, and supply chain violations ('command'-context rules).\n */\nexport function createPreToolUseYaraHooks(\n // Accepted for API symmetry with createPostToolUseYaraHooks, but\n // intentionally unused: PreToolUse Bash always blocks on any flagged\n // command regardless of triage verdict, so the LLM call would be wasted.\n // Future PreToolUse hooks that need triage can wire this through.\n _llmProvider?: LLMProvider,\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}\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 if (matches.length === 0) return {};\n\n const match = highestSeverityMatch(matches);\n\n // Critical severity or a rule asking us to block => terminate; the\n // agent's context may be poisoned by injected instructions.\n const isTerminal =\n match.metadata.severity === 'critical' ||\n match.metadata.action === 'block';\n\n if (isTerminal) {\n recordMatch('PostToolUse', toolName, match, 'aborted');\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 recordMatch('PostToolUse', toolName, match, 'warned');\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 if (matches.length === 0) return {};\n\n // INTENTIONAL ASYMMETRY: skill-install terminates on ANY\n // post-triage match, while Read/Grep only terminates on\n // critical-or-block. Skills are downloaded code from an\n // external source; any flagged content in them is treated as\n // poisoned. Read/Grep, by contrast, may scan project files\n // the user wrote themselves where a medium-severity match\n // can warrant a warning instead of a terminate.\n // TODO(wizard#593): document / lock in this contract with a test.\n const match = highestSeverityMatch(matches);\n recordMatch(\n 'PostToolUse',\n 'Bash (skill install)',\n match,\n 'aborted',\n );\n\n const reason =\n `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${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 * 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 const fileContents: string[] = [];\n for (const filePath of files) {\n try {\n fileContents.push(fs.readFileSync(filePath, 'utf-8'));\n } catch (err) {\n logToFile(`[YARA] Could not read skill file ${filePath}:`, err);\n }\n }\n\n if (fileContents.length === 0) {\n logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);\n return [];\n }\n\n logToFile(\n `[YARA] Scanning ${fileContents.length} files in skill directory: ${skillDir}`,\n );\n\n // Pass 1 (sequential): scan each file through the chunk-aware path —\n // full coverage even for oversized files, and every flagged chunk keeps a\n // reference to the exact content its matches came from.\n const flagged: FlaggedChunk[] = [];\n for (const content of fileContents) {\n flagged.push(...(await scanForContext(content, 'input')));\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":";;;;;;;;;;;;;;;;;AASA,SAAgB,sBAAsB,SAA0B;AAC9D,KAAI,CAAC,QAAQ,WAAW,2BAA2B,CAAE,QAAO;CAE5D,MAAM,WAAW,QAAQ,MAAM,4BAA4B;AAC3D,KAAI,CAAC,SAAU,QAAO;CAEtB,MAAM,MAAM,SAAS;AACrB,QACE,IAAI,WAAW,oDAAoD,IACnE,4BAA4B,KAAK,IAAI;;;;;;;;;;;ACVzC,MAAa,oBAAoB;AACjC,MAAa,uBAAuB;;AAEpC,MAAa,+BACX;;;;;;;;;;;ACHF,MAAa,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;ACwD/B,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;;;AAgDrD,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;;;;;;AAMxB,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;;;;;;;;;;;;;;AAe1B,SAAgB,gBACd,SACM;AACN,KAAI,QAAQ,YAAY;EACtB,MAAM,aAAa,iBAAiB;AACpC,MAAI,YAAY;GACd,MAAM,UAAU,kBAAkB;AAClC,UAAO,CAAC,IAAI,KAAK,qBAAqB,aAAa,WAAW,KAAK;;;AAGvE,oBAAmB;;AAiBrB,MAAM,uBAAuB,IAAI,IAAI;CACnC;CACAA;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;;AAO7E,MAAM,gBAAwC;CAC5C,UAAU;CACV,MAAM;CACN,QAAQ;CACR,KAAK;CACN;;AAGD,SAAS,qBAAqB,SAAiC;AAC7D,QAAO,QAAQ,QAAQ,OAAO,OAC3B,cAAc,EAAE,SAAS,YAAY,OAAO,MAC5C,cAAc,MAAM,SAAS,YAAY,OAAO,KAC7C,IACA,MACL;;;;;;;;AAWH,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;;;;;;;AAUjD,SAAgB,0BAKd,cACuB;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,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;KACZ,MAAM,UAAU,MAAM,cAAc,SAAS,SAAS,YAAY;AAClE,SAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;KAEnC,MAAM,QAAQ,qBAAqB,QAAQ;AAQ3C,SAHE,MAAM,SAAS,aAAa,cAC5B,MAAM,SAAS,WAAW,SAEZ;AACd,kBAAY,eAAe,UAAU,OAAO,UAAU;MACtD,MAAM,SACJ,mBAAmB,MAAM,KAAK;AAEhC,kBAAY,OAAO;AACnB,aAAO,EAAE,YAAY,QAAQ;;AAG/B,iBAAY,eAAe,UAAU,OAAO,SAAS;AACrD,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;KACZ,MAAM,UAAU,MAAM,eAAe,KAAK,UAAU,YAAY;AAEhE,SAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;KAUnC,MAAM,QAAQ,qBAAqB,QAAQ;AAC3C,iBACE,eACA,wBACA,OACA,UACD;KAED,MAAM,SACJ,8CAA8C,SAAS,IAAI,MAAM,KAAK;AAExE,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;;;;;;;;AAWH,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;CAEF,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,YAAY,MACrB,KAAI;AACF,eAAa,KAAK,GAAG,aAAa,UAAU,QAAQ,CAAC;UAC9C,KAAK;AACZ,YAAU,oCAAoC,SAAS,IAAI,IAAI;;AAInE,KAAI,aAAa,WAAW,GAAG;AAC7B,YAAU,kDAAkD,cAAc;AAC1E,SAAO,EAAE;;AAGX,WACE,mBAAmB,aAAa,OAAO,6BAA6B,WACrE;CAKD,MAAM,UAA0B,EAAE;AAClC,MAAK,MAAM,WAAW,aACpB,SAAQ,KAAK,GAAI,MAAM,eAAe,SAAS,QAAQ,CAAE;AAS3D,QAAO,cAAc,SAAS,SAAS,YAAY"}
|
package/package.json
CHANGED