@3030-labs/wotw 0.8.4 → 0.9.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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/daemon/config.ts","../src/utils/actionable-error.ts","../src/utils/fs.ts","../src/daemon/lifecycle.ts","../src/utils/logger.ts","../src/utils/version.ts","../src/ingestion/execution-mode.ts","../src/daemon/index.ts","../src/provenance/hash.ts","../src/utils/sanitize.ts"],"sourcesContent":["/**\n * Configuration loader. Uses cosmiconfig to discover a user config file, validates\n * it, and merges it with sensible defaults into a {@link WotwConfig}.\n */\nimport { cosmiconfig, type CosmiconfigResult } from \"cosmiconfig\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\nimport { configParseError } from \"../utils/actionable-error.js\";\nimport { resolvePath } from \"../utils/fs.js\";\nimport type { WotwConfig } from \"../utils/types.js\";\n\nconst MODULE_NAME = \"wotw\";\n\n/** Default plan limits. Active only when `hosted.enabled: true`. */\nexport const PLAN_DEFAULTS = {\n founding: {\n storage_bytes: 2 * 1024 ** 3, // 2 GB\n max_files_per_day: 50,\n max_file_size_bytes: 25 * 1024 ** 2, // 25 MB\n max_ingest_bytes_per_day: 250 * 1024 ** 2, // 250 MB\n heal_cooldown_seconds: 3600, // 1 hour\n query_rate_limit_per_hour: 60,\n },\n pro: {\n storage_bytes: 10 * 1024 ** 3, // 10 GB\n max_files_per_day: 200,\n max_file_size_bytes: 100 * 1024 ** 2, // 100 MB\n max_ingest_bytes_per_day: 1024 ** 3, // 1 GB\n heal_cooldown_seconds: 900, // 15 min\n query_rate_limit_per_hour: 300,\n },\n} as const;\n\n/**\n * Default configuration applied when no file is found (or when fields are missing).\n */\nexport function defaultConfig(): WotwConfig {\n return {\n wiki_root: \"./wiki-store\",\n raw_path: \"./wiki-store/raw\",\n llm: {\n provider: \"anthropic\",\n model: \"claude-sonnet-4-5\",\n },\n execution: {\n mode: \"auto\",\n cli_path: \"claude\",\n cli_model: \"claude-sonnet-4-5\",\n api_key_env: \"ANTHROPIC_API_KEY\",\n },\n models: {\n ingest: \"claude-haiku-4-5\",\n query: \"claude-sonnet-4-5\",\n lint: \"claude-sonnet-4-5\",\n compound_eval: \"claude-haiku-4-5\",\n },\n watcher: {\n debounce_initial_ms: 5000,\n debounce_max_ms: 60000,\n debounce_growth_factor: 1.5,\n burst_threshold: 5,\n max_batch_size: 20,\n ignore_patterns: [\"**/.git/**\", \"**/node_modules/**\", \"**/.DS_Store\", \"**/Thumbs.db\"],\n },\n ingestion: {\n max_turns: 50,\n max_budget_per_batch_usd: 1.0,\n resume_session: true,\n dead_letter_file: \".wotw/failed-batches.jsonl\",\n staging: true,\n },\n cost: {\n max_daily_usd: 10.0,\n max_per_query_usd: 0.5,\n max_per_ingest_usd: 2.0,\n track_file: \"~/.wotw/cost-log.jsonl\",\n },\n server: {\n port: 8787,\n host: \"127.0.0.1\",\n auth_token: null,\n rate_limit_rpm: 60,\n trust_proxy: false,\n },\n daemon: {\n pid_file: \"~/.wotw/daemon.pid\",\n lock_file: \"~/.wotw/daemon.lock\",\n log_file: \"~/.wotw/daemon.log\",\n log_level: \"info\",\n },\n compounding: {\n enabled: true,\n min_source_pages: 3,\n confidence_threshold: 70,\n },\n provenance: {\n enabled: true,\n chain_file: \"provenance-chain.jsonl\",\n // Review item 37: default ON so partial-corruption is detected at\n // boot. Pre-fix default false let a chain with one bad line boot\n // silently and advance on top of a verifiably-broken tail forever.\n verify_on_startup: true,\n },\n multi_user: {\n enabled: false,\n workspaces_dir: \"~/.wotw/workspaces\",\n },\n query: {\n expand: true,\n },\n fact_extraction: {\n enabled: \"auto\",\n force_enabled: false,\n questions_per_fact: 3,\n },\n lint: {\n schedule_enabled: false,\n interval_hours: 24,\n auto_fix: false,\n },\n health: {\n staleness_thresholds: [7, 30, 90, 180, 365],\n staleness_scores: [100, 80, 60, 40, 20, 0],\n weights: {\n staleness: 0.25,\n source_availability: 0.25,\n link_health: 0.2,\n duplicate_risk: 0.15,\n contradiction_risk: 0.15,\n },\n duplicate_threshold: 60,\n auto_fix_staleness_below: 40,\n max_fixes_per_run: 10,\n detect_contradictions: false,\n consolidation_threshold: 5,\n consolidation_enabled: true,\n zero_hit_threshold: 0.2,\n enrichment_enabled: true,\n query_log_file: \".wotw/query-log.jsonl\",\n },\n hosted: {\n enabled: false,\n tenant_id: null,\n concurrency_cap: 1,\n paused: false,\n plan: \"pro\",\n limits: {\n storage_bytes: PLAN_DEFAULTS.pro.storage_bytes,\n max_files_per_day: PLAN_DEFAULTS.pro.max_files_per_day,\n max_file_size_bytes: PLAN_DEFAULTS.pro.max_file_size_bytes,\n max_ingest_bytes_per_day: PLAN_DEFAULTS.pro.max_ingest_bytes_per_day,\n heal_cooldown_seconds: PLAN_DEFAULTS.pro.heal_cooldown_seconds,\n query_rate_limit_per_hour: PLAN_DEFAULTS.pro.query_rate_limit_per_hour,\n onboarding_burst_multiplier: 3,\n onboarding_burst_hours: 48,\n },\n timezone: \"America/New_York\",\n created_at: null,\n },\n };\n}\n\n/** Result of loading config: resolved value and origin path (if any). */\nexport interface LoadConfigResult {\n config: WotwConfig;\n path: string | null;\n}\n\n/**\n * Load configuration from cosmiconfig's discovery of `wotw.config.*`, `.wotwrc`, or\n * a `wotw` key in package.json. If no file is found, the default config is returned.\n *\n * Resolution order (highest to lowest priority):\n * 1. Environment variables (see {@link applyEnvOverrides})\n * 2. User config file\n * 3. Defaults\n *\n * In hosted mode (`WOTW_HOSTED=true` or `hosted.enabled` in the file), the\n * resulting config is additionally checked by {@link validateHostedConfig}\n * which throws when `tenant_id` is missing, malformed, or `wiki_root` is\n * unset.\n *\n * @param searchFrom optional directory to search from (defaults to process.cwd())\n */\nexport async function loadConfig(searchFrom?: string): Promise<LoadConfigResult> {\n const explorer = cosmiconfig(MODULE_NAME, {\n searchPlaces: [\n \"package.json\",\n `.${MODULE_NAME}rc`,\n `.${MODULE_NAME}rc.json`,\n `.${MODULE_NAME}rc.yaml`,\n `.${MODULE_NAME}rc.yml`,\n // `wotw.yaml` / `wotw.yml` are what the `wotw init` wizard writes\n // (see src/cli/commands/init.ts:CONFIG_CANDIDATES). They must be in\n // searchPlaces or every fresh-init vault runs with all-defaults.\n // Finding #12 from PASS-023 dogfood pass.\n `${MODULE_NAME}.yaml`,\n `${MODULE_NAME}.yml`,\n `${MODULE_NAME}.config.json`,\n `${MODULE_NAME}.config.yaml`,\n `${MODULE_NAME}.config.yml`,\n ],\n loaders: {\n \".yaml\": (_filepath, content) => parseYaml(content) as unknown,\n \".yml\": (_filepath, content) => parseYaml(content) as unknown,\n },\n });\n\n let result: CosmiconfigResult;\n try {\n result = await explorer.search(searchFrom ?? process.cwd());\n } catch (err) {\n // cosmiconfig parser errors (malformed YAML/JSON in a discovered config\n // file) surface here. Wrap as ActionableError so the CLI handler\n // renders the path + next-step suggestions instead of a stack trace.\n const hintPath =\n err instanceof Error && (err as { filepath?: string }).filepath\n ? ((err as { filepath?: string }).filepath as string)\n : (searchFrom ?? process.cwd());\n throw configParseError(hintPath, err);\n }\n const defaults = defaultConfig();\n let merged: WotwConfig;\n let path: string | null = null;\n if (!result || !result.config) {\n // eslint-disable-next-line no-console -- runs before pino logger is initialized\n console.warn(\n \"[wotw] no wotw.yaml found — using all defaults (auth disabled, max_daily_usd: 10.0)\",\n );\n merged = defaults;\n } else {\n merged = mergeConfig(defaults, result.config as Partial<WotwConfig>);\n path = result.filepath;\n }\n // Env overrides take precedence over the file. Applied here so the\n // hosted-mode validation below sees the final, fully-resolved values.\n const withEnv = applyEnvOverrides(merged);\n const validated = validateConfig(withEnv);\n validateHostedConfig(validated);\n return { config: validated, path };\n}\n\n/**\n * Apply runtime environment-variable overrides to a parsed config. Returns\n * a new object; the input is not mutated. Each override is read from the\n * corresponding env var and only set if the value is non-empty.\n *\n * Variables consumed (each optional):\n * WOTW_HOSTED \"true\" / \"false\" — `hosted.enabled`\n * TENANT_ID UUID — `hosted.tenant_id` (validated downstream)\n * WIKI_ROOT absolute path — `wiki_root`\n * WOTW_PLAN \"founding\" | \"pro\" — `hosted.plan`\n * WOTW_TIMEZONE IANA tz — `hosted.timezone`\n * WOTW_PORT integer — `server.port`\n * WOTW_HOST host string — `server.host`\n * WOTW_LOG_LEVEL pino level — `daemon.log_level`\n * WOTW_RUNTIME_MODE \"auto\" | \"cli\" | \"api\" — `execution.mode`\n * ADMIN_SERVICE_KEY bearer token — `server.auth_token`\n */\nexport function applyEnvOverrides(config: WotwConfig): WotwConfig {\n const out = structuredClone(config);\n const env = process.env;\n\n if (env.WOTW_HOSTED !== undefined) {\n // Review item 61: accept the conventional truthy spellings rather\n // than only \"true\". Pre-fix, \"1\" / \"True\" / \"yes\" / \"on\" all\n // silently set hosted.enabled = false; container operators expect\n // any of these to enable hosted-mode defaults.\n const v = env.WOTW_HOSTED.trim().toLowerCase();\n out.hosted.enabled = v === \"true\" || v === \"1\" || v === \"yes\" || v === \"on\";\n }\n if (env.TENANT_ID && env.TENANT_ID.length > 0) {\n out.hosted.tenant_id = env.TENANT_ID;\n }\n if (env.WIKI_ROOT && env.WIKI_ROOT.length > 0) {\n out.wiki_root = env.WIKI_ROOT;\n // When WIKI_ROOT is explicitly set, the wiki layout is flat — the env\n // value IS the wiki root, no wiki-store/ wrapper. Flatten default paths\n // that assume the wrapper. (User-overridden values in wotw.yaml stay\n // intact; we only touch defaults.)\n if (out.raw_path === \"./wiki-store/raw\") {\n out.raw_path = \"raw\";\n }\n }\n if (env.WOTW_PLAN === \"founding\" || env.WOTW_PLAN === \"pro\") {\n out.hosted.plan = env.WOTW_PLAN;\n }\n if (env.WOTW_TIMEZONE && env.WOTW_TIMEZONE.length > 0) {\n out.hosted.timezone = env.WOTW_TIMEZONE;\n }\n if (env.WOTW_PORT) {\n const n = Number.parseInt(env.WOTW_PORT, 10);\n if (Number.isFinite(n) && n > 0 && n < 65536) {\n out.server.port = n;\n }\n }\n if (env.WOTW_HOST && env.WOTW_HOST.length > 0) {\n out.server.host = env.WOTW_HOST;\n }\n if (env.WOTW_LOG_LEVEL) {\n const lvl = env.WOTW_LOG_LEVEL;\n if (\n lvl === \"trace\" ||\n lvl === \"debug\" ||\n lvl === \"info\" ||\n lvl === \"warn\" ||\n lvl === \"error\" ||\n lvl === \"fatal\"\n ) {\n out.daemon.log_level = lvl;\n }\n }\n if (\n env.WOTW_RUNTIME_MODE === \"auto\" ||\n env.WOTW_RUNTIME_MODE === \"cli\" ||\n env.WOTW_RUNTIME_MODE === \"api\"\n ) {\n out.execution.mode = env.WOTW_RUNTIME_MODE;\n }\n // Multi-LLM Phase 10: per-tenant provider selection via hosted-mode\n // env vars. The wotw-cloud orchestrator sets these at spawn time\n // based on `wiki.llm_provider`. Defaults to anthropic when unset.\n if (\n env.WOTW_LLM_PROVIDER === \"anthropic\" ||\n env.WOTW_LLM_PROVIDER === \"openai\" ||\n env.WOTW_LLM_PROVIDER === \"gemini\" ||\n env.WOTW_LLM_PROVIDER === \"ollama\"\n ) {\n out.llm.provider = env.WOTW_LLM_PROVIDER;\n // Update execution.api_key_env to match the chosen provider's\n // canonical key var. Each provider's SDK reads from its own env var;\n // the daemon's runtime-aware dispatch consults this field when\n // selecting which key to inject.\n switch (env.WOTW_LLM_PROVIDER) {\n case \"anthropic\":\n out.execution.api_key_env = \"ANTHROPIC_API_KEY\";\n break;\n case \"openai\":\n out.execution.api_key_env = \"OPENAI_API_KEY\";\n break;\n case \"gemini\":\n out.execution.api_key_env = \"GOOGLE_API_KEY\";\n break;\n case \"ollama\":\n // Ollama uses no API key. Leave api_key_env at whatever default\n // the config has; the dispatcher ignores it for Ollama.\n break;\n }\n }\n if (env.WOTW_LLM_MODEL && env.WOTW_LLM_MODEL.length > 0) {\n out.llm.model = env.WOTW_LLM_MODEL;\n }\n if (env.WOTW_OLLAMA_URL && env.WOTW_OLLAMA_URL.length > 0) {\n out.llm.ollama_url = env.WOTW_OLLAMA_URL;\n }\n // Review item 50: prefer the dedicated MCP bearer secret; fall back to\n // ADMIN_SERVICE_KEY only while wotw-cloud is being migrated to the\n // split-secret scheme (G4 — viable rip-and-replace with tenant_count=0).\n if (env.WOTW_MCP_BEARER && env.WOTW_MCP_BEARER.length > 0) {\n out.server.auth_token = env.WOTW_MCP_BEARER;\n } else if (env.ADMIN_SERVICE_KEY && env.ADMIN_SERVICE_KEY.length > 0) {\n out.server.auth_token = env.ADMIN_SERVICE_KEY;\n }\n // In hosted mode, default to stdout logging so container log streams\n // (Fly, Kubernetes, Docker) capture daemon output without ssh+cat. The\n // file-default ~/.wotw/daemon.log is invisible to Fly's log stream which\n // is a critical observability blind spot for production support. Users\n // who explicitly want file logging in hosted mode can set WOTW_LOG_FILE.\n if (env.WOTW_LOG_FILE !== undefined) {\n out.daemon.log_file = env.WOTW_LOG_FILE;\n } else if (out.hosted.enabled) {\n out.daemon.log_file = \"\";\n }\n\n // In hosted mode, disable candidates staging. The interactive `wotw start\n // --auto-approve` flag sets `ingestion.staging = false` to bypass the\n // human-review queue; the hosted entry point (dist/daemon/entry.js)\n // bypasses the CLI entirely, so the flag never fires and the\n // interactive-mode default `staging: true` leaks into hosted operation.\n // Without this override, every ingestion writes to candidates/ and waits\n // forever for a human reviewer who doesn't exist in the hosted topology\n // (per-tenant Fly Machine, BYOK, autonomous operation). BM25 indexes\n // wiki/{plural-category}/ and never sees candidates/, so MCP query returns\n // \"no relevant wiki pages found\" despite successful ingestion. Validation-\n // gap instance #12 closure, 2026-05-12.\n if (out.hosted.enabled) {\n out.ingestion.staging = false;\n // Pass 012-completion (2026-05-19): two additional hosted-mode\n // defaults ratified from Pass 012's audit. See\n // feedback_hosted_mode_default_audit.md for the inversion-shape framing.\n //\n // A third candidate (health.detect_contradictions) was originally in\n // Pass 012's audit but surfaced during Pass 012-completion verification\n // as having no runtime consumer at v0.2.12 — the LLM contradiction\n // detection pass that would check this flag is not yet implemented.\n // Hosted-mode override deferred to a future consumer-implementation\n // pass. See TODO at the field's declaration in src/utils/types.ts.\n out.lint.schedule_enabled = true;\n out.lint.auto_fix = true;\n }\n return out;\n}\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Hosted-mode runtime invariants. Throws with a precise message when:\n * - `hosted.enabled` is true but `tenant_id` is missing\n * - `tenant_id` is set but is not a valid UUID\n * - `wiki_root` is an empty/relative path that won't resolve to a stable\n * mount point under `/data`\n *\n * Called from {@link loadConfig}. Community-mode configs (where\n * `hosted.enabled` is false) are unaffected.\n */\nexport function validateHostedConfig(config: WotwConfig): void {\n if (!config.hosted.enabled) return;\n if (!config.hosted.tenant_id || config.hosted.tenant_id.length === 0) {\n throw new Error(\n \"Config error: hosted.enabled is true but TENANT_ID / hosted.tenant_id is unset.\",\n );\n }\n if (!UUID_REGEX.test(config.hosted.tenant_id)) {\n throw new Error(\n `Config error: hosted.tenant_id \"${config.hosted.tenant_id}\" is not a valid UUID.`,\n );\n }\n if (!config.wiki_root || config.wiki_root.length === 0) {\n throw new Error(\"Config error: hosted.enabled is true but wiki_root / WIKI_ROOT is unset.\");\n }\n // Review item 60: a relative wiki_root in hosted mode resolves against\n // process.cwd(), which is the ephemeral container filesystem on Fly\n // Machines. Tenant data would be wiped on every restart. Require\n // absolute path in hosted mode.\n if (!config.wiki_root.startsWith(\"/\")) {\n throw new Error(\n `Config error: hosted.enabled is true but wiki_root \"${config.wiki_root}\" is not absolute. ` +\n `In hosted mode the daemon must persist tenant data on a mounted volume; ` +\n `a relative path resolves against process.cwd() which is the ephemeral container fs.`,\n );\n }\n}\n\n/**\n * Deep-merge user config on top of defaults. Unknown keys in user config are dropped\n * to prevent typos from leaking into runtime behavior.\n */\nexport function mergeConfig(base: WotwConfig, override: Partial<WotwConfig>): WotwConfig {\n const out: WotwConfig = structuredClone(base);\n const assign = <K extends keyof WotwConfig>(key: K, value: Partial<WotwConfig[K]>): void => {\n out[key] = { ...(out[key] as object), ...(value as object) } as WotwConfig[K];\n };\n\n if (override.wiki_root !== undefined) out.wiki_root = override.wiki_root;\n if (override.raw_path !== undefined) out.raw_path = override.raw_path;\n if (override.llm) assign(\"llm\", override.llm);\n if (override.execution) assign(\"execution\", override.execution);\n if (override.models) assign(\"models\", override.models);\n if (override.watcher) assign(\"watcher\", override.watcher);\n if (override.ingestion) assign(\"ingestion\", override.ingestion);\n if (override.cost) assign(\"cost\", override.cost);\n if (override.server) assign(\"server\", override.server);\n if (override.daemon) assign(\"daemon\", override.daemon);\n if (override.compounding) assign(\"compounding\", override.compounding);\n if (override.provenance) assign(\"provenance\", override.provenance);\n if (override.multi_user) assign(\"multi_user\", override.multi_user);\n if (override.query) assign(\"query\", override.query);\n if (override.fact_extraction) assign(\"fact_extraction\", override.fact_extraction);\n if (override.lint) assign(\"lint\", override.lint);\n if (override.health) {\n // Deep-merge the weights sub-object separately.\n const healthBase = out.health;\n const healthOverride = override.health as Partial<WotwConfig[\"health\"]>;\n out.health = { ...healthBase, ...healthOverride };\n if (healthOverride.weights) {\n out.health.weights = { ...healthBase.weights, ...healthOverride.weights };\n }\n }\n if (override.hosted) assign(\"hosted\", override.hosted);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Zod validation schema\n// ---------------------------------------------------------------------------\n\nconst positiveNumber = z.number().positive();\nconst nonNegativeNumber = z.number().min(0);\nconst logLevelSchema = z.enum([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]);\nconst llmProviderSchema = z.enum([\"anthropic\", \"openai\", \"gemini\", \"ollama\"]);\n\n/**\n * Zod schema that validates a fully-merged WotwConfig object. Every field\n * has a default matching {@link defaultConfig} so a bare `{}` passes.\n */\nconst WotwConfigSchema = z.object({\n wiki_root: z.string().min(1),\n raw_path: z.string().min(1),\n llm: z.object({\n provider: llmProviderSchema,\n model: z.string().min(1),\n ollama_url: z.string().min(1).optional(),\n }),\n execution: z.object({\n mode: z.enum([\"auto\", \"cli\", \"api\"]),\n cli_path: z.string().min(1),\n cli_model: z.string().min(1),\n api_key_env: z.string().min(1),\n }),\n models: z.object({\n ingest: z.string().min(1),\n query: z.string().min(1),\n lint: z.string().min(1),\n compound_eval: z.string().min(1),\n }),\n watcher: z.object({\n debounce_initial_ms: positiveNumber,\n debounce_max_ms: positiveNumber,\n debounce_growth_factor: positiveNumber,\n burst_threshold: z.number().int().positive(),\n max_batch_size: z.number().int().positive(),\n ignore_patterns: z.array(z.string()),\n }),\n ingestion: z.object({\n max_turns: z.number().int().positive(),\n max_budget_per_batch_usd: positiveNumber,\n resume_session: z.boolean(),\n dead_letter_file: z.string(),\n staging: z.boolean(),\n }),\n cost: z.object({\n max_daily_usd: positiveNumber,\n max_per_query_usd: positiveNumber,\n max_per_ingest_usd: positiveNumber,\n track_file: z.string().min(1),\n }),\n server: z.object({\n port: z.number().int().min(1).max(65535),\n host: z.string().min(1),\n auth_token: z.string().nullable(),\n rate_limit_rpm: z.number().int().positive(),\n trust_proxy: z.boolean(),\n }),\n daemon: z.object({\n pid_file: z.string().min(1),\n lock_file: z.string().min(1),\n // Empty string is meaningful: \"log to stdout\" (used by hosted mode for\n // container log capture). initLogger treats empty as no destination\n // and defaults to pino's stdout output.\n log_file: z.string(),\n log_level: logLevelSchema,\n }),\n compounding: z.object({\n enabled: z.boolean(),\n min_source_pages: z.number().int().min(0),\n confidence_threshold: z.number().min(0).max(100),\n }),\n provenance: z.object({\n enabled: z.boolean(),\n chain_file: z.string().min(1),\n verify_on_startup: z.boolean(),\n }),\n multi_user: z.object({\n enabled: z.boolean(),\n workspaces_dir: z.string().min(1),\n }),\n query: z.object({\n expand: z.boolean(),\n }),\n fact_extraction: z.object({\n enabled: z.union([z.boolean(), z.literal(\"auto\")]),\n force_enabled: z.boolean(),\n questions_per_fact: z.number().int().min(1).max(5),\n model: z.string().min(1).optional(),\n }),\n lint: z.object({\n schedule_enabled: z.boolean(),\n interval_hours: positiveNumber,\n auto_fix: z.boolean(),\n }),\n hosted: z.object({\n enabled: z.boolean(),\n tenant_id: z.string().nullable(),\n concurrency_cap: z.number().int().positive(),\n paused: z.boolean(),\n plan: z.enum([\"founding\", \"pro\"]),\n limits: z.object({\n storage_bytes: positiveNumber,\n max_files_per_day: z.number().int().positive(),\n max_file_size_bytes: positiveNumber,\n max_ingest_bytes_per_day: positiveNumber,\n heal_cooldown_seconds: nonNegativeNumber,\n query_rate_limit_per_hour: z.number().int().positive(),\n onboarding_burst_multiplier: positiveNumber,\n onboarding_burst_hours: positiveNumber,\n }),\n timezone: z.string().min(1),\n created_at: z.string().nullable(),\n }),\n health: z.object({\n staleness_thresholds: z.array(z.number().int().min(0)),\n staleness_scores: z.array(z.number().min(0).max(100)),\n weights: z.object({\n staleness: nonNegativeNumber,\n source_availability: nonNegativeNumber,\n link_health: nonNegativeNumber,\n duplicate_risk: nonNegativeNumber,\n contradiction_risk: nonNegativeNumber,\n }),\n duplicate_threshold: z.number().min(0).max(100),\n auto_fix_staleness_below: z.number().min(0).max(100),\n max_fixes_per_run: z.number().int().min(0),\n detect_contradictions: z.boolean(),\n consolidation_threshold: z.number().int().min(2),\n consolidation_enabled: z.boolean(),\n zero_hit_threshold: z.number().min(0).max(1),\n enrichment_enabled: z.boolean(),\n query_log_file: z.string(),\n }),\n});\n\n/**\n * Validate a merged config against the Zod schema. Throws a descriptive\n * error on failure, naming the invalid field, expected type, and value.\n */\nexport function validateConfig(config: WotwConfig): WotwConfig {\n const result = WotwConfigSchema.safeParse(config);\n if (!result.success) {\n const issue = result.error.issues[0]!;\n const path = issue.path.join(\".\");\n throw new Error(`Config error: \"${path}\" ${issue.message}`);\n }\n return result.data as WotwConfig;\n}\n\n/**\n * Expand all path-like fields in a config using {@link resolvePath}.\n * Returns a new config; the input is not mutated.\n */\nexport function resolveConfigPaths(config: WotwConfig, baseDir?: string): WotwConfig {\n const out = structuredClone(config);\n out.wiki_root = resolvePath(out.wiki_root, baseDir);\n out.raw_path = resolvePath(out.raw_path, baseDir);\n out.cost.track_file = resolvePath(out.cost.track_file, baseDir);\n out.daemon.pid_file = resolvePath(out.daemon.pid_file, baseDir);\n out.daemon.lock_file = resolvePath(out.daemon.lock_file, baseDir);\n // Empty log_file is meaningful (means \"log to stdout\" in hosted mode);\n // resolvePath would prepend baseDir to it, producing a path that points\n // at the baseDir itself. Skip resolution when empty.\n if (out.daemon.log_file.length > 0) {\n out.daemon.log_file = resolvePath(out.daemon.log_file, baseDir);\n }\n out.multi_user.workspaces_dir = resolvePath(out.multi_user.workspaces_dir, baseDir);\n // Provenance chain lives inside the wiki root by default — resolve it\n // against the (already resolved) wiki_root so a relative default like\n // `provenance-chain.jsonl` lands in the right place.\n out.provenance.chain_file = resolvePath(out.provenance.chain_file, out.wiki_root);\n // Dead-letter file is likewise wiki-relative (so each wiki has its own\n // failure ledger). Empty string disables; leave it alone in that case.\n if (out.ingestion.dead_letter_file.length > 0) {\n out.ingestion.dead_letter_file = resolvePath(out.ingestion.dead_letter_file, out.wiki_root);\n }\n // Query log file is wiki-relative, like the dead-letter file.\n if (out.health.query_log_file.length > 0) {\n out.health.query_log_file = resolvePath(out.health.query_log_file, out.wiki_root);\n }\n return out;\n}\n","/**\n * Actionable error class for the 10 user-facing unhappy paths surfaced\n * by PASS-023 (public-launch readiness). Each instance carries:\n *\n * - a stable `code` for programmatic handling + log indexing\n * - a one-line `summary` for the top of the rendered error\n * - a `cause` chain for diagnostics\n * - a `suggestions` array of concrete next steps the user can take\n *\n * The CLI's top-level error handler renders these as a structured block\n * to stderr; the daemon's internal logger captures the same fields as\n * structured JSON. Stack traces are suppressed unless `WOTW_DEBUG=1`.\n */\nexport type ActionableErrorCode =\n | \"MISSING_VAULT_PATH\"\n | \"CONFIG_PARSE_ERROR\"\n | \"NATIVE_BINDING_LOAD_FAILURE\"\n | \"INVALID_API_KEY\"\n | \"RATE_LIMITED\"\n | \"WIKI_DIR_PERMISSION_DENIED\"\n | \"VAULT_FILE_LOCKED\"\n | \"PORT_IN_USE\"\n | \"DAEMON_ALREADY_RUNNING\"\n | \"INIT_TARGET_NOT_EMPTY\";\n\nexport interface ActionableErrorOptions {\n code: ActionableErrorCode;\n summary: string;\n suggestions: readonly string[];\n cause?: unknown;\n docs?: string;\n}\n\nexport class ActionableError extends Error {\n readonly code: ActionableErrorCode;\n readonly summary: string;\n readonly suggestions: readonly string[];\n readonly docs?: string;\n\n constructor(opts: ActionableErrorOptions) {\n const lines: string[] = [opts.summary];\n if (opts.suggestions.length > 0) {\n lines.push(\"\", \"What to try:\");\n for (const suggestion of opts.suggestions) {\n lines.push(` - ${suggestion}`);\n }\n }\n if (opts.docs) {\n lines.push(\"\", `Docs: ${opts.docs}`);\n }\n super(lines.join(\"\\n\"));\n this.name = \"ActionableError\";\n this.code = opts.code;\n this.summary = opts.summary;\n this.suggestions = opts.suggestions;\n this.docs = opts.docs;\n if (opts.cause !== undefined) {\n (this as { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n\nexport function isActionableError(e: unknown): e is ActionableError {\n return e instanceof ActionableError;\n}\n\n/**\n * Heuristic detection of native-binding load failures. better-sqlite3\n * (and other N-API addons) surface very platform-specific error\n * messages; this matcher captures the common shapes.\n */\nexport function looksLikeNativeBindingFailure(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n return (\n /Cannot find module .*\\.node/i.test(msg) ||\n /could not load the bindings file/i.test(msg) ||\n /better.sqlite3.*\\.node.*was compiled against/i.test(msg) ||\n /NODE_MODULE_VERSION/i.test(msg) ||\n /ERR_DLOPEN_FAILED/i.test(msg) ||\n /libstdc\\+\\+.*GLIBCXX/i.test(msg) ||\n /Symbol not found.*sqlite3/i.test(msg) ||\n /image not found/i.test(msg)\n );\n}\n\n/**\n * Heuristic detection of EACCES / EPERM errors from filesystem APIs.\n */\nexport function looksLikePermissionDenied(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n const code = e instanceof Error ? (e as { code?: string }).code : undefined;\n return code === \"EACCES\" || code === \"EPERM\" || /EACCES|EPERM/i.test(msg);\n}\n\n/**\n * Heuristic detection of file-lock contention errors. Obsidian holds\n * EBUSY/ETXTBSY on macOS, an undocumented lock state on Windows, and\n * EAGAIN on Linux when another process holds an exclusive lock.\n */\nexport function looksLikeFileLock(e: unknown): boolean {\n const code = e instanceof Error ? (e as { code?: string }).code : undefined;\n const msg = e instanceof Error ? e.message : String(e);\n return (\n code === \"EBUSY\" ||\n code === \"ETXTBSY\" ||\n code === \"EAGAIN\" ||\n /EBUSY|ETXTBSY|EAGAIN|file is locked/i.test(msg)\n );\n}\n\n/**\n * Heuristic detection of port-in-use binding failures. listen() rejects\n * with EADDRINUSE on every supported platform.\n */\nexport function looksLikePortInUse(e: unknown): boolean {\n const code = e instanceof Error ? (e as { code?: string }).code : undefined;\n const msg = e instanceof Error ? e.message : String(e);\n return code === \"EADDRINUSE\" || /EADDRINUSE/i.test(msg);\n}\n\n/**\n * Build an ActionableError for the OBSIDIAN_VAULT_PATH-missing path\n * (item 1 of the PASS-023 error audit).\n */\nexport function missingVaultPathError(): ActionableError {\n return new ActionableError({\n code: \"MISSING_VAULT_PATH\",\n summary: \"No Obsidian vault path was provided and none could be auto-detected.\",\n suggestions: [\n \"Run `wotw init` and pick a vault interactively.\",\n \"Pass `--path /path/to/vault` to point at a specific directory.\",\n \"Set OBSIDIAN_VAULT_PATH in your shell environment to a vault root.\",\n \"Install Obsidian and open at least one vault to register a default.\",\n ],\n docs: \"docs/init-walkthrough.md\",\n });\n}\n\n/**\n * Build an ActionableError for the malformed-config path (item 2).\n */\nexport function configParseError(configPath: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"CONFIG_PARSE_ERROR\",\n summary: `Could not parse wotw config at ${configPath}: ${cause instanceof Error ? cause.message : String(cause)}`,\n suggestions: [\n 'Validate the file with `node -e \\'console.log(JSON.parse(require(\"fs\").readFileSync(process.argv[1], \"utf8\")))\\' ' +\n configPath +\n \"` (for JSON) or `yamllint \" +\n configPath +\n \"` (for YAML).\",\n \"Check for unmatched braces, missing colons, or trailing commas.\",\n \"Compare against the example at docs/configuration.md.\",\n \"Delete the file and re-run `wotw init` to regenerate a fresh config.\",\n ],\n docs: \"docs/configuration.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the better-sqlite3 native-binding-load\n * path (item 3).\n */\nexport function nativeBindingLoadError(module: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"NATIVE_BINDING_LOAD_FAILURE\",\n summary: `Could not load the native binding for ${module} on this platform.`,\n suggestions: [\n \"Run `pnpm rebuild \" +\n module +\n \"` (or `npm rebuild \" +\n module +\n \"`) in the install directory.\",\n \"Verify Node.js >= 20 is installed (`node --version`).\",\n \"Confirm your platform matches one of: macOS arm64, macOS amd64, Linux amd64, Windows amd64.\",\n \"If running under Docker, ensure the image was built for the runtime arch (not host arch).\",\n \"Reinstall: `npm uninstall -g @3030-labs/wotw && npm install -g @3030-labs/wotw`.\",\n ],\n docs: \"docs/install-evidence/\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the invalid-API-key path (item 4).\n */\nexport function invalidApiKeyError(\n provider: \"anthropic\" | \"openai\" | \"gemini\" | \"other\",\n envVar: string,\n cause?: unknown,\n): ActionableError {\n return new ActionableError({\n code: \"INVALID_API_KEY\",\n summary: `Provider ${provider} returned 401 Unauthorized — the API key in ${envVar} is invalid or revoked.`,\n suggestions: [\n `Verify the key works: \\`curl -H \"x-api-key: $${envVar}\" https://api.${provider === \"anthropic\" ? \"anthropic.com\" : provider + \".com\"}/v1/models\\` (provider URL may vary).`,\n \"Regenerate the key at the provider's console if it's revoked.\",\n `Re-export the new key (\\`export ${envVar}=\"...\"\\`) and restart the daemon (\\`wotw stop && wotw start\\`).`,\n \"Daemon does NOT pick up env-var changes while running — restart is required.\",\n ],\n docs: \"docs/self-hosted-byok.md\",\n cause,\n });\n}\n\n/**\n * Detect a Claude Code CLI authentication failure in the CLI's output.\n * In CLI (subscription) runtime, a 401 surfaces in the agent's stdout as\n * an \"API Error: 401 ... authentication_error ... Please run /login\"\n * string rather than as an HTTP status the daemon can branch on\n * (PASS-023 dogfood finding #21).\n */\nexport function looksLikeCliAuthFailure(text: string): boolean {\n return (\n /API Error:\\s*401/i.test(text) ||\n /authentication_error/i.test(text) ||\n /invalid authentication credentials/i.test(text) ||\n /please run \\/login/i.test(text) ||\n /not (logged in|authenticated)/i.test(text)\n );\n}\n\n/**\n * Build an ActionableError for a Claude Code CLI auth failure (item 4,\n * CLI-runtime variant). Distinct remediation from the env-var API-key\n * path: the fix is `claude /login`, not rotating an env var.\n */\nexport function cliAuthError(cause?: unknown): ActionableError {\n return new ActionableError({\n code: \"INVALID_API_KEY\",\n summary:\n \"The Claude Code CLI is not authenticated (got 401 from the model API). \" +\n \"wotw is in CLI runtime mode and the `claude` binary has no valid session.\",\n suggestions: [\n \"Run `claude` to open the interactive shell, then type `/login` and complete the browser auth flow.\",\n 'Verify auth worked: `echo \"hi\" | claude -p` should print a reply, not a 401.',\n \"Then restart the daemon: `wotw stop && wotw start`.\",\n \"If you intended to use an API key instead of the subscription CLI, set `execution.mode: api` + `ANTHROPIC_API_KEY` in your config — see docs/self-hosted-byok.md.\",\n ],\n docs: \"docs/self-hosted-byok.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the rate-limited path (item 5).\n */\nexport function rateLimitedError(\n provider: \"anthropic\" | \"openai\" | \"gemini\" | \"other\",\n retryAfterSec?: number,\n cause?: unknown,\n): ActionableError {\n return new ActionableError({\n code: \"RATE_LIMITED\",\n summary: `Provider ${provider} returned 429 — rate limit exceeded${\n retryAfterSec !== undefined ? ` (retry after ${retryAfterSec}s)` : \"\"\n }.`,\n suggestions: [\n \"Lower `ingestion.concurrency` in wotw.config.yaml to spread requests further.\",\n `Wait ${retryAfterSec ?? \"the indicated\"} seconds and retry.`,\n \"Upgrade your provider tier if 429s are sustained over multiple ingest cycles.\",\n \"Check `wotw status` for dead-letter queue growth; clear with `wotw dlq retry` once the window recovers.\",\n ],\n docs: \"docs/self-hosted-byok.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the wiki-dir EACCES path (item 6).\n */\nexport function wikiDirPermissionError(path: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"WIKI_DIR_PERMISSION_DENIED\",\n summary: `Cannot create or write to wiki directory at ${path}: permission denied.`,\n suggestions: [\n `Check the directory's owner and permissions: \\`ls -la \"${path}\"\\`.`,\n `Ensure the daemon's user can write: \\`chmod u+w \"${path}\"\\` (or relocate to a writable parent).`,\n \"If running under Docker, confirm the volume mount has the right ownership.\",\n \"Choose a different `wiki_root` in wotw.config.yaml.\",\n ],\n docs: \"docs/configuration.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the locked-vault-file path (item 7).\n */\nexport function vaultFileLockedError(path: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"VAULT_FILE_LOCKED\",\n summary: `Cannot write to ${path}: another process is holding an exclusive lock (usually Obsidian).`,\n suggestions: [\n \"Close the file in Obsidian (or close Obsidian entirely) and retry the operation.\",\n \"On Windows, check for indexing services or backup tools that may briefly lock files.\",\n \"Run `wotw status` to confirm the lock cleared after closing the holder.\",\n \"If the lock persists with no holder visible, restart your machine.\",\n ],\n docs: \"docs/obsidian-setup.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the port-in-use path (item 8).\n */\nexport function portInUseError(port: number, cause?: unknown): ActionableError {\n return new ActionableError({\n code: \"PORT_IN_USE\",\n summary: `Port ${port} is already in use; the MCP server cannot bind.`,\n suggestions: [\n `Find the process holding the port: \\`lsof -iTCP:${port} -sTCP:LISTEN\\` (macOS/Linux) or \\`netstat -ano | findstr :${port}\\` (Windows).`,\n \"Stop that process, or change `server.port` in wotw.config.yaml to a free port.\",\n `If it's an orphaned wotw daemon, \\`wotw stop\\` (or kill the PID) and retry.`,\n ],\n docs: \"docs/configuration.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the daemon-already-running path (item 9).\n */\nexport function daemonAlreadyRunningError(lockPath: string, cause?: unknown): ActionableError {\n return new ActionableError({\n code: \"DAEMON_ALREADY_RUNNING\",\n summary: `Another wotw daemon already holds the lock at ${lockPath}.`,\n suggestions: [\n \"Run `wotw status` to see the live daemon's PID and health.\",\n \"If you intend to replace it, `wotw stop` first, then `wotw start`.\",\n \"If the lock is stale (e.g. after a crash), `wotw stop --force` clears it.\",\n ],\n docs: \"docs/cli-reference.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the non-empty-init-target path (item 10).\n */\nexport function initTargetNotEmptyError(\n path: string,\n conflictingEntries: readonly string[],\n): ActionableError {\n return new ActionableError({\n code: \"INIT_TARGET_NOT_EMPTY\",\n summary: `Cannot scaffold a new vault at ${path}: target directory is not empty.`,\n suggestions: [\n `Conflicting entries: ${conflictingEntries.slice(0, 5).join(\", \")}${conflictingEntries.length > 5 ? ` … (+${conflictingEntries.length - 5} more)` : \"\"}.`,\n \"Pick a different `--path` (or an empty subdirectory of this one).\",\n \"If you meant to overlay onto an existing vault, this is the wrong code path — the wizard would have offered an overlay prompt. Confirm the target has `.obsidian/` and re-run.\",\n \"If you're sure the existing files are safe to overwrite, re-run with `--force`.\",\n ],\n docs: \"docs/init-walkthrough.md\",\n });\n}\n","/**\n * File system utilities: atomic writes, directory scaffolding, path resolution.\n */\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, resolve } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Expand a leading `~` to the current user's home directory.\n */\nexport function expandHome(path: string): string {\n if (path.startsWith(\"~\")) {\n return resolve(homedir(), path.slice(path.startsWith(\"~/\") ? 2 : 1));\n }\n return path;\n}\n\n/**\n * Resolve a path against an optional base directory, expanding `~`.\n */\nexport function resolvePath(path: string, base?: string): string {\n const expanded = expandHome(path);\n if (isAbsolute(expanded)) return expanded;\n return resolve(base ?? process.cwd(), expanded);\n}\n\n/**\n * Ensure the directory at `dir` exists, creating it and all parents if needed.\n */\nexport function ensureDirSync(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\n/**\n * Async version of {@link ensureDirSync}.\n */\nexport async function ensureDir(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n}\n\n/**\n * Atomic write: write to a temp file in the same directory and rename.\n * This guarantees readers never see a partial file on POSIX systems.\n */\nexport function atomicWriteSync(filePath: string, contents: string | Buffer): void {\n ensureDirSync(dirname(filePath));\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n let renamed = false;\n try {\n writeFileSync(tmp, contents);\n renameSync(tmp, filePath);\n renamed = true;\n } finally {\n if (!renamed) {\n try {\n rmSync(tmp, { force: true });\n } catch {\n /* best effort */\n }\n }\n }\n}\n\n/**\n * Async version of {@link atomicWriteSync}.\n */\nexport async function atomicWrite(filePath: string, contents: string | Buffer): Promise<void> {\n await ensureDir(dirname(filePath));\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n let renamed = false;\n try {\n await writeFile(tmp, contents);\n await rename(tmp, filePath);\n renamed = true;\n } finally {\n if (!renamed) {\n try {\n rmSync(tmp, { force: true });\n } catch {\n /* best effort */\n }\n }\n }\n}\n\n/**\n * Read a UTF-8 file, returning null if it does not exist.\n */\nexport function readTextOrNull(filePath: string): string | null {\n try {\n return readFileSync(filePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/**\n * Async version of {@link readTextOrNull}.\n */\nexport async function readTextOrNullAsync(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/**\n * Delete a file if it exists, otherwise no-op.\n */\nexport function removeIfExistsSync(filePath: string): void {\n if (existsSync(filePath)) {\n rmSync(filePath, { force: true });\n }\n}\n\n/**\n * Check if a path points to an existing file.\n */\nexport function fileExists(filePath: string): boolean {\n try {\n return statSync(filePath).isFile();\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw err;\n }\n}\n\n/**\n * Check if a path points to an existing directory.\n */\nexport function dirExists(dirPath: string): boolean {\n try {\n return statSync(dirPath).isDirectory();\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw err;\n }\n}\n","/**\n * Daemon lifecycle: PID file management, file locking, graceful shutdown.\n *\n * A running daemon has three facts associated with it:\n * 1. A PID file containing its numeric PID and a timestamp.\n * 2. A lock file that prevents two daemons from starting simultaneously.\n * 3. A log file where pino writes structured events.\n */\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { atomicWriteSync, ensureDirSync, removeIfExistsSync } from \"../utils/fs.js\";\n\nexport interface PidFileContents {\n pid: number;\n started_at: string;\n version: string;\n}\n\n/**\n * Write the PID file atomically.\n */\nexport function writePidFile(pidFilePath: string, contents: PidFileContents): void {\n ensureDirSync(dirname(pidFilePath));\n atomicWriteSync(pidFilePath, JSON.stringify(contents));\n}\n\n/**\n * Read a PID file, returning null if missing or malformed.\n */\nexport function readPidFile(pidFilePath: string): PidFileContents | null {\n if (!existsSync(pidFilePath)) return null;\n try {\n const raw = readFileSync(pidFilePath, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed &&\n typeof parsed === \"object\" &&\n \"pid\" in parsed &&\n typeof (parsed as { pid: unknown }).pid === \"number\"\n ) {\n return parsed as PidFileContents;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Remove the PID file, ignoring missing files.\n */\nexport function removePidFile(pidFilePath: string): void {\n removeIfExistsSync(pidFilePath);\n}\n\n/**\n * Check whether a process is alive by sending signal 0. Returns true if it is\n * still alive, false otherwise. Handles EPERM (process owned by another user)\n * by returning true, since the process exists even if we cannot signal it.\n */\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ESRCH\") return false;\n if (code === \"EPERM\") return true;\n return false;\n }\n}\n\n/**\n * Check liveness based on the PID file: returns {alive, pid, contents}.\n * If the PID file references a dead process, the file is considered stale.\n */\nexport function checkDaemonAlive(pidFilePath: string): {\n alive: boolean;\n pid: number | null;\n stale: boolean;\n contents: PidFileContents | null;\n} {\n const contents = readPidFile(pidFilePath);\n if (!contents) return { alive: false, pid: null, stale: false, contents: null };\n const alive = isProcessAlive(contents.pid);\n return { alive, pid: contents.pid, stale: !alive, contents };\n}\n\n/**\n * Acquire the daemon start-lock. Returns a release function on success.\n * Throws if another process holds the lock.\n */\nexport async function acquireStartLock(lockPath: string): Promise<() => Promise<void>> {\n ensureDirSync(dirname(lockPath));\n // proper-lockfile requires the target file to exist; create a zero-byte stub if missing.\n if (!existsSync(lockPath)) writeFileSync(lockPath, \"\");\n const release = await lockfile.lock(lockPath, {\n stale: 10_000,\n retries: { retries: 0 },\n });\n return release;\n}\n\n/**\n * Send SIGTERM to a PID and wait up to `timeoutMs` for it to exit. Returns true\n * if the process exited, false if it is still alive after the timeout.\n */\nexport async function terminateAndWait(pid: number, timeoutMs = 10_000): Promise<boolean> {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ESRCH\") return true;\n throw err;\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isProcessAlive(pid)) return true;\n await new Promise((r) => setTimeout(r, 100));\n }\n return !isProcessAlive(pid);\n}\n","/**\n * Centralized pino logger factory. All modules should import `getLogger()` rather\n * than constructing their own logger so we have one configuration surface.\n */\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport pino, { type Logger, type LoggerOptions } from \"pino\";\n\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\" | \"fatal\";\n\nlet rootLogger: Logger | null = null;\n\n/**\n * Initialize the root logger. Idempotent — calling again replaces the existing logger.\n *\n * @param level minimum log level to emit\n * @param logFile optional path to a log file. If provided, JSON lines are written there.\n * If omitted, logs go to stdout in pretty format for humans.\n */\n/**\n * Pino redact paths — review item 1 closure.\n *\n * Pino's `redact.paths` walks the structured log object before serialization\n * and replaces matching values with `[Redacted]`. We list the common shapes\n * the daemon's log sites produce when error / response objects flow\n * through: headers.authorization, ANTHROPIC_API_KEY-shaped env values,\n * SDK error payloads with embedded keys, etc.\n *\n * This is BELT to the SUSPENDERS of src/utils/sanitize.ts's string-level\n * regex redaction. Sanitize covers free-form text in prompts; redact\n * covers structured fields the call site might have forgotten to pass\n * through sanitize.\n */\nconst REDACT_PATHS = [\n \"headers.authorization\",\n \"headers.Authorization\",\n \"*.headers.authorization\",\n \"*.headers.Authorization\",\n \"req.headers.authorization\",\n \"req.headers.Authorization\",\n \"request.headers.authorization\",\n \"request.headers.Authorization\",\n \"response.headers.authorization\",\n \"response.headers.Authorization\",\n \"headers['x-admin-key']\",\n \"headers['x-api-key']\",\n \"headers.cookie\",\n \"*.headers.cookie\",\n \"config.api_key\",\n \"config.headers.authorization\",\n \"err.config.headers.authorization\",\n \"err.request.headers.authorization\",\n \"err.response.headers.authorization\",\n // Provider SDK error shapes — observed leaking via err.headers.* on Axios-based clients.\n \"err.headers.authorization\",\n \"err.headers.Authorization\",\n // Env-bag dumps (occasionally added by ad-hoc debug logs).\n \"env.ANTHROPIC_API_KEY\",\n \"env.OPENAI_API_KEY\",\n \"env.GOOGLE_API_KEY\",\n \"env.ADMIN_SERVICE_KEY\",\n \"env.WOTW_MCP_BEARER\",\n \"env.WOTW_INTERNAL_ADMIN_KEY\",\n \"env.WOTW_CLOUD_SINK_SECRET\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GOOGLE_API_KEY\",\n \"ADMIN_SERVICE_KEY\",\n \"WOTW_MCP_BEARER\",\n \"WOTW_INTERNAL_ADMIN_KEY\",\n \"WOTW_CLOUD_SINK_SECRET\",\n \"apiKey\",\n \"api_key\",\n \"*.apiKey\",\n \"*.api_key\",\n \"secret\",\n \"*.secret\",\n];\n\n/**\n * Review item 6: Pino's default err serializer dumps arbitrary error\n * properties via `pino.stdSerializers.err`. Empirically verified that\n * SDK errors carry `err.headers.authorization` / `err.response.headers.*`\n * verbatim. The redact-paths layer (item 1) catches the common shapes,\n * but a custom serializer makes the safety guarantee load-bearing:\n * only the small allowlist of fields below is ever emitted; everything\n * else is dropped, regardless of where it sits on the error object.\n */\nfunction safeErrSerializer(err: unknown): Record<string, unknown> {\n if (!(err instanceof Error)) {\n return { message: String(err) };\n }\n const out: Record<string, unknown> = {\n type: err.constructor.name,\n message: err.message,\n };\n if (err.stack) out.stack = err.stack;\n // Some custom errors (SafeFetchError, OrchestratorError) carry a\n // `code` field that's safe to surface — it's an enum, never user\n // content.\n const maybeCode = (err as unknown as { code?: unknown }).code;\n if (typeof maybeCode === \"string\") out.code = maybeCode;\n // `cause` is usually another Error — recurse one level (cap depth\n // to avoid stack overflow on circular chains).\n const maybeCause = (err as unknown as { cause?: unknown }).cause;\n if (maybeCause && typeof maybeCause === \"object\" && \"message\" in maybeCause) {\n out.cause = {\n type: (maybeCause as { constructor?: { name: string } }).constructor?.name ?? \"Error\",\n message: (maybeCause as { message: unknown }).message,\n };\n }\n // INTENTIONALLY DROPPED (item 6 fix surface):\n // err.headers — Axios-shape; carries authorization\n // err.config — Axios-shape; carries authorization header\n // err.request — Axios-shape; carries headers\n // err.response — Axios-shape; carries headers + body\n // any other property — unknown SDK shapes default to \"drop\"\n return out;\n}\n\nexport function initLogger(level: LogLevel = \"info\", logFile?: string): Logger {\n const options: LoggerOptions = {\n level,\n base: { pid: process.pid, hostname: undefined },\n timestamp: pino.stdTimeFunctions.isoTime,\n redact: {\n paths: REDACT_PATHS,\n censor: \"[Redacted]\",\n remove: false,\n },\n serializers: {\n err: safeErrSerializer,\n error: safeErrSerializer,\n },\n };\n\n if (logFile) {\n mkdirSync(dirname(logFile), { recursive: true });\n rootLogger = pino(options, pino.destination({ dest: logFile, sync: false, mkdir: true }));\n } else {\n rootLogger = pino({\n ...options,\n transport: {\n target: \"pino-pretty\",\n options: { colorize: true, translateTime: \"HH:MM:ss.l\", ignore: \"pid,hostname\" },\n },\n });\n }\n return rootLogger;\n}\n\n/** Default context fields merged into every child logger (e.g. tenantId in hosted mode). */\nlet defaultContext: Record<string, unknown> = {};\n\n/**\n * Set default context fields that are merged into every child logger.\n * Call once at startup when hosted.enabled is true.\n */\nexport function setLoggerContext(ctx: Record<string, unknown>): void {\n defaultContext = ctx;\n}\n\n/**\n * Return the root logger, initializing with defaults if none exists.\n * When defaultContext has been set (hosted mode), every child logger\n * automatically includes those fields.\n */\nexport function getLogger(module?: string, extra?: Record<string, unknown>): Logger {\n if (!rootLogger) {\n rootLogger = initLogger(\"info\");\n }\n const ctx = { ...defaultContext, ...extra, ...(module ? { module } : {}) };\n return Object.keys(ctx).length > 0 ? rootLogger.child(ctx) : rootLogger;\n}\n","/**\n * Single source of truth for the package version.\n *\n * Uses `createRequire` to read `package.json` at runtime so the version\n * can never drift from the declared value. This is safe in ESM because\n * `createRequire` resolves relative to the compiled output directory and\n * `package.json` sits at the repo root (two levels up from `dist/utils/`).\n */\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../../package.json\") as { version: string };\n\nexport const VERSION: string = pkg.version;\n","/**\n * Execution mode detection. Resolves the configured {@link ExecutionMode}\n * (\"auto\" | \"cli\" | \"api\") into a concrete {@link RuntimeMode} (\"cli\" | \"api\")\n * at daemon startup.\n *\n * Rules:\n * - mode=auto: prefer the `claude` CLI binary on PATH. If absent, fall back\n * to the Agent SDK iff `ANTHROPIC_API_KEY` (or the configured env var) is\n * set. If neither is available, refuse to start.\n * - mode=cli: require the CLI binary on PATH; refuse to start if missing.\n * - mode=api: require the API key env var to be set; refuse to start if\n * missing.\n *\n * The detection is synchronous and cheap: one `which`/`where` invocation and\n * one env lookup. It must be called exactly once, during daemon init, so the\n * resolved mode can be logged prominently.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { platform } from \"node:os\";\nimport type { ExecutionMode, RuntimeMode, WotwConfig } from \"../utils/types.js\";\n\n/** Summary of a successful resolution. */\nexport interface ResolvedExecutionMode {\n /** Concrete runtime to use. */\n mode: RuntimeMode;\n /** Configured mode that produced this result. */\n configuredMode: ExecutionMode;\n /** Absolute path to the `claude` binary, if CLI mode was resolved. */\n cliPath: string | null;\n /** Name of the env var that supplied the API key, if API mode was resolved. */\n apiKeyEnv: string | null;\n /** Model identifier that will be used (cli_model in CLI mode, router in API mode). */\n effectiveModelHint: string;\n /** One-line human-readable summary, suitable for logging or CLI output. */\n description: string;\n}\n\n/**\n * Error thrown when the daemon cannot resolve a usable runtime mode.\n * Intentionally a subclass of Error with a `.code` field so callers can\n * surface a clean exit.\n */\nexport class ExecutionModeError extends Error {\n readonly code: string;\n constructor(message: string, code: string) {\n super(message);\n this.name = \"ExecutionModeError\";\n this.code = code;\n }\n}\n\n/**\n * Look up the absolute path of a binary on PATH. Returns null if not found.\n * Uses `which` on Unix and `where` on Windows. Safe to call on WSL — the\n * Linux `which` is used there since WSL reports `platform() === 'linux'`.\n */\nexport function findOnPath(binary: string): string | null {\n // `command -v` would be slightly more portable on Unix, but it is a shell\n // builtin and spawnSync won't invoke a shell. `which` is universally\n // available on macOS, Linux, and WSL; `where` is the Windows equivalent.\n const prog = platform() === \"win32\" ? \"where\" : \"which\";\n try {\n const result = spawnSync(prog, [binary], { encoding: \"utf8\" });\n if (result.status !== 0) return null;\n const stdout = result.stdout.trim();\n if (!stdout) return null;\n // `where` may list multiple matches; take the first line.\n const first = stdout.split(/\\r?\\n/)[0]?.trim();\n return first && first.length > 0 ? first : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Inspect the environment for an API key under the configured env var name.\n * Returns the var name if set (non-empty), null otherwise.\n */\nexport function findApiKey(envVarName: string): string | null {\n const value = process.env[envVarName];\n if (value && value.trim().length > 0) return envVarName;\n return null;\n}\n\n/**\n * Resolve {@link WotwConfig.execution} into a concrete {@link RuntimeMode}.\n * Throws {@link ExecutionModeError} with a precise reason if neither runtime\n * is available.\n */\nexport function resolveExecutionMode(config: WotwConfig): ResolvedExecutionMode {\n const { mode, cli_path: cliPath, cli_model: cliModel, api_key_env: apiKeyEnv } = config.execution;\n\n const detectCli = (): string | null => findOnPath(cliPath);\n const detectKey = (): string | null => findApiKey(apiKeyEnv);\n\n if (mode === \"cli\") {\n const path = detectCli();\n if (!path) {\n throw new ExecutionModeError(\n `execution.mode is 'cli' but the '${cliPath}' binary was not found on PATH. ` +\n \"Install Claude Code CLI (https://docs.claude.com/claude-code) or set execution.mode to 'api'.\",\n \"CLI_BINARY_NOT_FOUND\",\n );\n }\n return {\n mode: \"cli\",\n configuredMode: \"cli\",\n cliPath: path,\n apiKeyEnv: null,\n effectiveModelHint: cliModel,\n description: `CLI mode: using claude binary at ${path}, model ${cliModel}, zero marginal cost (subscription-covered)`,\n };\n }\n\n if (mode === \"api\") {\n const keyEnv = detectKey();\n if (!keyEnv) {\n throw new ExecutionModeError(\n `execution.mode is 'api' but ${apiKeyEnv} is not set. ` +\n \"Set the env var or change execution.mode to 'cli'/'auto'.\",\n \"API_KEY_NOT_SET\",\n );\n }\n return {\n mode: \"api\",\n configuredMode: \"api\",\n cliPath: null,\n apiKeyEnv: keyEnv,\n effectiveModelHint: `model-router (ingest=${config.models.ingest}, query=${config.models.query})`,\n description: `API mode: using Agent SDK with ${keyEnv}, model routing enabled (per-token billing)`,\n };\n }\n\n // mode === \"auto\"\n // Review item 62: when WOTW_LLM_PROVIDER (config.llm.provider) is\n // set to anything other than \"anthropic\", we MUST NOT silently auto-\n // detect into CLI mode. Pre-fix, an OpenAI / Gemini / Ollama tenant\n // with the claude binary on PATH (always true in the container)\n // would silently dispatch through CLI mode — billing the wrong key\n // for the wrong provider. Honor the explicit llm.provider.\n if (config.llm.provider !== \"anthropic\") {\n const keyEnv2 = detectKey();\n if (!keyEnv2 && config.llm.provider !== \"ollama\") {\n throw new ExecutionModeError(\n `auto-detect: llm.provider='${config.llm.provider}' but ${apiKeyEnv} is not set. ` +\n \"Refusing to fall back to CLI mode for non-anthropic provider.\",\n \"API_KEY_NOT_SET\",\n );\n }\n return {\n mode: \"api\",\n configuredMode: \"auto\",\n cliPath: null,\n apiKeyEnv: keyEnv2,\n effectiveModelHint: `model-router (ingest=${config.models.ingest}, query=${config.models.query})`,\n description: `API mode (auto-detected, provider=${config.llm.provider}): using ${keyEnv2 ?? \"no-key\"}, model routing enabled`,\n };\n }\n const cli = detectCli();\n if (cli) {\n return {\n mode: \"cli\",\n configuredMode: \"auto\",\n cliPath: cli,\n apiKeyEnv: null,\n effectiveModelHint: cliModel,\n description: `CLI mode (auto-detected): using claude binary at ${cli}, model ${cliModel}, zero marginal cost (subscription-covered)`,\n };\n }\n const keyEnv = detectKey();\n if (keyEnv) {\n return {\n mode: \"api\",\n configuredMode: \"auto\",\n cliPath: null,\n apiKeyEnv: keyEnv,\n effectiveModelHint: `model-router (ingest=${config.models.ingest}, query=${config.models.query})`,\n description: `API mode (auto-detected): using Agent SDK with ${keyEnv}, model routing enabled (per-token billing)`,\n };\n }\n throw new ExecutionModeError(\n `No '${cliPath}' binary on PATH and no ${apiKeyEnv} env var set. ` +\n \"Install Claude Code CLI (https://docs.claude.com/claude-code) or set an API key to run watcher-on-the-wall.\",\n \"NO_RUNTIME_AVAILABLE\",\n );\n}\n","/**\n * Daemon main loop. Initializes subsystems, wires up signal handlers, and keeps\n * the event loop alive until a graceful shutdown is requested.\n *\n * In Phase 1 the daemon owns the PID file, logger, and signal handlers. Phases 2-4\n * progressively attach watcher, ingestion, MCP server, and provenance subsystems\n * by calling {@link Daemon.attachSubsystem}.\n */\nimport { loadConfig, resolveConfigPaths } from \"./config.js\";\nimport { acquireStartLock, checkDaemonAlive, removePidFile, writePidFile } from \"./lifecycle.js\";\nimport { getLogger, initLogger } from \"../utils/logger.js\";\nimport type { WotwConfig } from \"../utils/types.js\";\nimport {\n daemonAlreadyRunningError,\n looksLikePermissionDenied,\n wikiDirPermissionError,\n} from \"../utils/actionable-error.js\";\nimport { ensureDirSync } from \"../utils/fs.js\";\n\nfunction ensureDirSyncOrActionable(path: string): void {\n try {\n ensureDirSync(path);\n } catch (err) {\n if (looksLikePermissionDenied(err)) {\n throw wikiDirPermissionError(path, err);\n }\n throw err;\n }\n}\nimport { VERSION } from \"../utils/version.js\";\nimport {\n resolveExecutionMode,\n type ResolvedExecutionMode,\n ExecutionModeError,\n} from \"../ingestion/execution-mode.js\";\n\n/** A daemon subsystem that can start and stop cleanly. */\nexport interface DaemonSubsystem {\n name: string;\n start(): Promise<void>;\n stop(): Promise<void>;\n}\n\nexport interface DaemonOptions {\n configPath: string | null;\n workingDir: string;\n}\n\n/**\n * The Daemon class holds runtime state and coordinates subsystem lifecycle.\n */\nexport class Daemon {\n private readonly subsystems: DaemonSubsystem[] = [];\n private shuttingDown = false;\n private readonly opts: DaemonOptions;\n private config: WotwConfig | null = null;\n private executionMode: ResolvedExecutionMode | null = null;\n private releaseLock: (() => Promise<void>) | null = null;\n\n constructor(opts: DaemonOptions) {\n this.opts = opts;\n }\n\n /** Resolve config and return it. */\n async init(): Promise<WotwConfig> {\n const loaded = await loadConfig(this.opts.workingDir);\n this.config = resolveConfigPaths(loaded.config, this.opts.workingDir);\n\n // Initialize logger against the resolved log file. Foreground runs set\n // WOTW_LOG_STDOUT=1 so logs stream (pretty) to the inherited terminal\n // instead of disappearing into the log file — otherwise `wotw start\n // --foreground` looks silent (PASS-023 dogfood finding #18).\n const logToStdout =\n process.env.WOTW_LOG_STDOUT === \"1\" || process.env.WOTW_LOG_STDOUT === \"true\";\n initLogger(this.config.daemon.log_level, logToStdout ? undefined : this.config.daemon.log_file);\n const log = getLogger(\"daemon\");\n log.info(\n {\n pid: process.pid,\n cwd: this.opts.workingDir,\n configPath: loaded.path,\n },\n \"daemon initializing\",\n );\n\n ensureDirSyncOrActionable(this.config.wiki_root);\n ensureDirSyncOrActionable(this.config.raw_path);\n\n // Resolve execution mode BEFORE starting any subsystem. If neither a\n // claude CLI binary nor an API key is available, refuse to start with a\n // precise error message — downstream code would crash in confusing ways\n // without this guard.\n try {\n this.executionMode = resolveExecutionMode(this.config);\n } catch (err) {\n if (err instanceof ExecutionModeError) {\n log.fatal({ code: err.code }, err.message);\n } else {\n log.fatal({ err }, \"failed to resolve execution mode\");\n }\n throw err;\n }\n // Log the resolved mode prominently so the user always knows which\n // runtime is active and what it costs.\n log.info(\n {\n mode: this.executionMode.mode,\n configured: this.executionMode.configuredMode,\n cliPath: this.executionMode.cliPath,\n apiKeyEnv: this.executionMode.apiKeyEnv,\n model: this.executionMode.effectiveModelHint,\n },\n this.executionMode.description,\n );\n\n return this.config;\n }\n\n /** Return the resolved execution mode, or null if init() hasn't run yet. */\n getExecutionMode(): ResolvedExecutionMode | null {\n return this.executionMode;\n }\n\n /** Attach a subsystem for start/stop management. */\n attachSubsystem(sub: DaemonSubsystem): void {\n this.subsystems.push(sub);\n }\n\n /**\n * Main run loop. Acquires the start lock, writes the PID file, starts all\n * subsystems, installs signal handlers, and blocks until shutdown.\n */\n async run(): Promise<void> {\n if (!this.config) throw new Error(\"Daemon.init() must be called before run()\");\n const log = getLogger(\"daemon\");\n\n // Guard against double-start\n const alive = checkDaemonAlive(this.config.daemon.pid_file);\n if (alive.alive) {\n log.error({ pid: alive.pid }, \"another daemon instance is already running\");\n throw daemonAlreadyRunningError(this.config.daemon.pid_file);\n }\n\n // Acquire lock to prevent simultaneous starts\n try {\n this.releaseLock = await acquireStartLock(this.config.daemon.lock_file);\n } catch (err) {\n log.error({ err }, \"failed to acquire start lock\");\n throw daemonAlreadyRunningError(this.config.daemon.lock_file, err);\n }\n\n // Write PID file\n writePidFile(this.config.daemon.pid_file, {\n pid: process.pid,\n started_at: new Date().toISOString(),\n version: VERSION,\n });\n log.info({ pidFile: this.config.daemon.pid_file }, \"PID file written\");\n\n // Install signal handlers BEFORE starting subsystems so any crash during\n // startup is handled cleanly.\n this.installSignalHandlers();\n\n // Start subsystems in order\n for (const sub of this.subsystems) {\n try {\n log.info({ subsystem: sub.name }, \"starting subsystem\");\n await sub.start();\n } catch (err) {\n log.error({ err, subsystem: sub.name }, \"subsystem failed to start\");\n await this.shutdown(1);\n return;\n }\n }\n\n log.info({ subsystems: this.subsystems.map((s) => s.name) }, \"daemon running\");\n\n // Block until shutdown. The check interval intentionally does NOT unref,\n // so it keeps the event loop alive even when no subsystems are attached.\n // Once this.shuttingDown flips true we clear it and resolve.\n await new Promise<void>((resolve) => {\n const check = setInterval(() => {\n if (this.shuttingDown) {\n clearInterval(check);\n resolve();\n }\n }, 250);\n });\n }\n\n /** Install SIGTERM / SIGINT handlers for graceful shutdown. */\n private installSignalHandlers(): void {\n const handle = (signal: NodeJS.Signals): void => {\n const log = getLogger(\"daemon\");\n log.info({ signal }, \"received shutdown signal\");\n void this.shutdown(0);\n };\n process.on(\"SIGTERM\", handle);\n process.on(\"SIGINT\", handle);\n process.on(\"uncaughtException\", (err) => {\n const log = getLogger(\"daemon\");\n log.fatal({ err }, \"uncaught exception\");\n void this.shutdown(1);\n });\n process.on(\"unhandledRejection\", (reason) => {\n const log = getLogger(\"daemon\");\n log.fatal(\n { reason: reason instanceof Error ? reason.message : String(reason) },\n \"unhandled rejection — shutting down\",\n );\n void this.shutdown(1);\n });\n }\n\n /** Stop all subsystems, release the lock, remove the PID file, and exit. */\n async shutdown(exitCode: number): Promise<void> {\n if (this.shuttingDown) return;\n this.shuttingDown = true;\n const log = getLogger(\"daemon\");\n log.info(\"daemon shutting down\");\n\n // Stop subsystems in reverse order\n for (const sub of [...this.subsystems].reverse()) {\n try {\n await sub.stop();\n log.info({ subsystem: sub.name }, \"subsystem stopped\");\n } catch (err) {\n log.error({ err, subsystem: sub.name }, \"subsystem stop failed\");\n }\n }\n\n // Remove PID file\n if (this.config) {\n removePidFile(this.config.daemon.pid_file);\n }\n\n // Release lock\n if (this.releaseLock) {\n try {\n await this.releaseLock();\n } catch {\n /* ignore */\n }\n }\n\n log.info(\"daemon shutdown complete\");\n // Give pino a tick to flush\n await new Promise((r) => setTimeout(r, 50));\n process.exit(exitCode);\n }\n}\n","/**\n * Hashing utilities used by the provenance chain and other subsystems.\n * Hashes are SHA-256 over canonical JSON (recursively sorted keys, no\n * whitespace, UTF-8). The canonical form is deterministic across machines\n * and language runtimes so verification does not depend on JSON.stringify\n * key-order quirks.\n *\n * This module is the single source of truth for hashing in `wotw` —\n * `src/utils/hash.ts` was previously a second copy with overlapping and\n * subtly-different sync/async signatures for `sha256File`. That duplication\n * has been removed (L-DUP-1). Prefer named exports from this module\n * (`sha256`/`sha256Hex`, `sha256File`, `sha256FileSync`, `canonicalJson`,\n * `sha256Canonical`) throughout the codebase.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/** Special value used as `previous_chain_hash` for the first record in a chain. */\nexport const GENESIS_HASH = \"0\".repeat(64);\n\n/**\n * Produce a canonical JSON string for any JSON-serializable input.\n * Keys in every object are sorted lexicographically, recursively.\n */\nexport function canonicalJson(value: unknown): string {\n const normalize = (v: unknown): unknown => {\n if (v === null || typeof v !== \"object\") return v;\n if (Array.isArray(v)) return v.map(normalize);\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(v as Record<string, unknown>).sort()) {\n sorted[key] = normalize((v as Record<string, unknown>)[key]);\n }\n return sorted;\n };\n return JSON.stringify(normalize(value));\n}\n\n/** SHA-256 of a string or buffer, returned as hex. */\nexport function sha256Hex(input: string | Buffer): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Alias for {@link sha256Hex}. Kept for ergonomic call sites\n * (`sha256(contents)` reads more naturally than `sha256Hex(contents)` in\n * non-provenance code) and for the stable public API in `src/index.ts`.\n */\nexport const sha256 = sha256Hex;\n\n/** SHA-256 of the canonical JSON form of a value. */\nexport function sha256Canonical(value: unknown): string {\n return sha256Hex(canonicalJson(value));\n}\n\n/** Alias for {@link sha256Canonical} — hashes the canonical JSON of a value. */\nexport const sha256Json = sha256Canonical;\n\n/** Alias for {@link canonicalJson} — kept for the stable public API. */\nexport const stableStringify = canonicalJson;\n\n/**\n * Synchronous SHA-256 of a file on disk. Reads the whole file into memory,\n * which is fine for wiki pages. Throws if the file does not exist — callers\n * that need ENOENT tolerance should use the async {@link sha256File} which\n * returns null. Used by the watcher's in-memory event classifier where\n * synchronous semantics simplify the seed flow.\n */\nexport function sha256FileSync(filePath: string): string {\n return sha256Hex(readFileSync(filePath));\n}\n\n/**\n * SHA-256 of a file on disk. Returns null if the file does not exist.\n * Reads the whole file into memory — fine for wiki pages which are small.\n */\nexport async function sha256File(path: string): Promise<string | null> {\n try {\n const buf = await readFile(path);\n return sha256Hex(buf);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/**\n * Hash many files in parallel. Missing files map to null entries.\n * Used when recording the post-ingestion state of the wiki.\n */\nexport async function sha256Files(paths: string[]): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n await Promise.all(\n paths.map(async (p) => {\n const h = await sha256File(p);\n if (h !== null) out[p] = h;\n }),\n );\n return out;\n}\n","/**\n * Content sanitization: strip credentials and PII patterns from text before LLM ingestion.\n *\n * This is a best-effort redaction layer. Users can extend the patterns list via\n * configuration. The goal is to keep secrets out of logs, prompts, and wiki pages.\n */\n\nexport interface RedactionRule {\n name: string;\n pattern: RegExp;\n replacement: string;\n}\n\n/**\n * Default redaction rules. Ordered by likely-to-match first for efficiency.\n */\nexport const DEFAULT_REDACTIONS: readonly RedactionRule[] = [\n {\n name: \"aws-access-key\",\n pattern: /\\bAKIA[0-9A-Z]{16}\\b/g,\n replacement: \"[REDACTED:AWS_ACCESS_KEY]\",\n },\n {\n name: \"aws-secret-key\",\n pattern: /\\b[A-Za-z0-9/+=]{40}\\b(?=.*(?:secret|aws))/gi,\n replacement: \"[REDACTED:AWS_SECRET_KEY]\",\n },\n {\n name: \"github-token\",\n // Review item 2: also catch GitHub fine-grained personal access\n // tokens (`github_pat_*`, 82+ chars per docs).\n pattern: /\\bgh[pousr]_[A-Za-z0-9]{36,255}\\b|\\bgithub_pat_[A-Za-z0-9_]{50,}\\b/g,\n replacement: \"[REDACTED:GITHUB_TOKEN]\",\n },\n {\n name: \"anthropic-api-key\",\n // Anthropic keys span ~95-115 chars after `sk-ant-`. The 80,120\n // window stays generous enough to catch both legacy and current\n // formats including api03- prefix.\n pattern: /\\bsk-ant-[A-Za-z0-9-_]{80,120}\\b/g,\n replacement: \"[REDACTED:ANTHROPIC_API_KEY]\",\n },\n {\n name: \"openai-api-key\",\n // Review item 2: original `\\bsk-[A-Za-z0-9]{20,}\\b` missed modern\n // formats with `-` and `_` in the body — `sk-proj-*`,\n // `sk-svcacct-*`, `sk-admin-*` all use `-` separators after the\n // prefix and longer character set. Updated character class.\n pattern: /\\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,200}\\b|\\bsk-[A-Za-z0-9]{20,200}\\b/g,\n replacement: \"[REDACTED:OPENAI_API_KEY]\",\n },\n {\n name: \"gemini-api-key\",\n // Review item 2: Google AI Studio API keys are `AIza` + 35 chars.\n // No rule existed pre-fix.\n pattern: /\\bAIza[A-Za-z0-9_-]{35}\\b/g,\n replacement: \"[REDACTED:GEMINI_API_KEY]\",\n },\n {\n name: \"wotw-daemon-token\",\n // Review item 2: daemon tokens emitted by `wotw user add` are\n // `wotw_` + base64url chars. Pre-fix these went unredacted.\n pattern: /\\bwotw_[A-Za-z0-9_-]{30,200}\\b/g,\n replacement: \"[REDACTED:WOTW_TOKEN]\",\n },\n {\n name: \"private-key-block\",\n pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,\n replacement: \"[REDACTED:PRIVATE_KEY_BLOCK]\",\n },\n {\n name: \"jwt\",\n pattern: /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\b/g,\n replacement: \"[REDACTED:JWT]\",\n },\n {\n // L-SEC-3: This pattern is deliberately scoped to full `scheme://`\n // URIs with a `user:password@` userinfo component. The `\\w+:\\/\\/`\n // prefix is load-bearing — without it the pattern would also match\n // bare `user@host` email addresses and `mailto:user@host` URIs,\n // both of which carry no password and must not be over-redacted.\n // Verified by unit tests in test/unit/sanitize.test.ts.\n name: \"password-in-url\",\n pattern: /(\\w+:\\/\\/[^:/\\s]+:)[^@\\s]+(@)/g,\n replacement: \"$1[REDACTED]$2\",\n },\n {\n name: \"credit-card\",\n pattern: /\\b(?:\\d[ -]*?){13,16}\\b/g,\n replacement: \"[REDACTED:PAN]\",\n },\n {\n name: \"us-ssn\",\n pattern: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\n replacement: \"[REDACTED:SSN]\",\n },\n];\n\n/**\n * Redact sensitive content from a text blob using the supplied rules (or defaults).\n */\nexport function sanitize(\n input: string,\n rules: readonly RedactionRule[] = DEFAULT_REDACTIONS,\n): string {\n let out = input;\n for (const rule of rules) {\n out = out.replace(rule.pattern, rule.replacement);\n }\n return out;\n}\n\n/**\n * Redact and return the list of rule names that triggered.\n */\nexport function sanitizeWithReport(\n input: string,\n rules: readonly RedactionRule[] = DEFAULT_REDACTIONS,\n): { output: string; triggered: string[] } {\n const triggered: string[] = [];\n let out = input;\n for (const rule of rules) {\n if (rule.pattern.test(out)) {\n triggered.push(rule.name);\n // Reset lastIndex for global regexes before replacing\n rule.pattern.lastIndex = 0;\n out = out.replace(rule.pattern, rule.replacement);\n }\n }\n return { output: out, triggered };\n}\n"],"mappings":"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","names":["existsSync","readFileSync","writeFileSync","dirname","dirname","existsSync","readFileSync","writeFileSync","mkdirSync","dirname","mkdirSync","dirname","require","keyEnv","resolve","readFileSync","readFile","readFileSync","readFile"]}
1
+ {"version":3,"sources":["../src/daemon/config.ts","../src/utils/actionable-error.ts","../src/utils/fs.ts","../src/daemon/lifecycle.ts","../src/utils/logger.ts","../src/utils/version.ts","../src/ingestion/execution-mode.ts","../src/daemon/index.ts","../src/provenance/hash.ts","../src/utils/sanitize.ts"],"sourcesContent":["/**\n * Configuration loader. Uses cosmiconfig to discover a user config file, validates\n * it, and merges it with sensible defaults into a {@link WotwConfig}.\n */\nimport { cosmiconfig, type CosmiconfigResult } from \"cosmiconfig\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\nimport { configParseError } from \"../utils/actionable-error.js\";\nimport { resolvePath } from \"../utils/fs.js\";\nimport type { WotwConfig } from \"../utils/types.js\";\n\nconst MODULE_NAME = \"wotw\";\n\n/** Default plan limits. Active only when `hosted.enabled: true`. */\nexport const PLAN_DEFAULTS = {\n founding: {\n storage_bytes: 2 * 1024 ** 3, // 2 GB\n max_files_per_day: 50,\n max_file_size_bytes: 25 * 1024 ** 2, // 25 MB\n max_ingest_bytes_per_day: 250 * 1024 ** 2, // 250 MB\n heal_cooldown_seconds: 3600, // 1 hour\n query_rate_limit_per_hour: 60,\n },\n pro: {\n storage_bytes: 10 * 1024 ** 3, // 10 GB\n max_files_per_day: 200,\n max_file_size_bytes: 100 * 1024 ** 2, // 100 MB\n max_ingest_bytes_per_day: 1024 ** 3, // 1 GB\n heal_cooldown_seconds: 900, // 15 min\n query_rate_limit_per_hour: 300,\n },\n} as const;\n\n/**\n * Default configuration applied when no file is found (or when fields are missing).\n */\nexport function defaultConfig(): WotwConfig {\n return {\n wiki_root: \"./wiki-store\",\n raw_path: \"./wiki-store/raw\",\n llm: {\n provider: \"anthropic\",\n model: \"claude-sonnet-4-5\",\n },\n execution: {\n mode: \"auto\",\n cli_path: \"claude\",\n cli_model: \"claude-sonnet-4-5\",\n api_key_env: \"ANTHROPIC_API_KEY\",\n },\n models: {\n ingest: \"claude-haiku-4-5\",\n query: \"claude-sonnet-4-5\",\n lint: \"claude-sonnet-4-5\",\n compound_eval: \"claude-haiku-4-5\",\n },\n watcher: {\n debounce_initial_ms: 5000,\n debounce_max_ms: 60000,\n debounce_growth_factor: 1.5,\n burst_threshold: 5,\n max_batch_size: 20,\n ignore_patterns: [\"**/.git/**\", \"**/node_modules/**\", \"**/.DS_Store\", \"**/Thumbs.db\"],\n },\n ingestion: {\n max_turns: 50,\n max_budget_per_batch_usd: 1.0,\n resume_session: true,\n dead_letter_file: \".wotw/failed-batches.jsonl\",\n staging: true,\n },\n cost: {\n max_daily_usd: 10.0,\n max_per_query_usd: 0.5,\n max_per_ingest_usd: 2.0,\n track_file: \"~/.wotw/cost-log.jsonl\",\n },\n server: {\n port: 8787,\n host: \"127.0.0.1\",\n auth_token: null,\n rate_limit_rpm: 60,\n trust_proxy: false,\n },\n daemon: {\n pid_file: \"~/.wotw/daemon.pid\",\n lock_file: \"~/.wotw/daemon.lock\",\n log_file: \"~/.wotw/daemon.log\",\n log_level: \"info\",\n },\n compounding: {\n enabled: true,\n min_source_pages: 3,\n confidence_threshold: 70,\n },\n provenance: {\n enabled: true,\n chain_file: \"provenance-chain.jsonl\",\n // Review item 37: default ON so partial-corruption is detected at\n // boot. Pre-fix default false let a chain with one bad line boot\n // silently and advance on top of a verifiably-broken tail forever.\n verify_on_startup: true,\n },\n multi_user: {\n enabled: false,\n workspaces_dir: \"~/.wotw/workspaces\",\n },\n query: {\n expand: true,\n },\n fact_extraction: {\n enabled: \"auto\",\n force_enabled: false,\n questions_per_fact: 3,\n },\n lint: {\n schedule_enabled: false,\n interval_hours: 24,\n auto_fix: false,\n },\n health: {\n staleness_thresholds: [7, 30, 90, 180, 365],\n staleness_scores: [100, 80, 60, 40, 20, 0],\n weights: {\n staleness: 0.25,\n source_availability: 0.25,\n link_health: 0.2,\n duplicate_risk: 0.15,\n contradiction_risk: 0.15,\n },\n duplicate_threshold: 60,\n auto_fix_staleness_below: 40,\n max_fixes_per_run: 10,\n detect_contradictions: false,\n consolidation_threshold: 5,\n consolidation_enabled: true,\n zero_hit_threshold: 0.2,\n enrichment_enabled: true,\n query_log_file: \".wotw/query-log.jsonl\",\n },\n hosted: {\n enabled: false,\n tenant_id: null,\n concurrency_cap: 1,\n paused: false,\n plan: \"pro\",\n limits: {\n storage_bytes: PLAN_DEFAULTS.pro.storage_bytes,\n max_files_per_day: PLAN_DEFAULTS.pro.max_files_per_day,\n max_file_size_bytes: PLAN_DEFAULTS.pro.max_file_size_bytes,\n max_ingest_bytes_per_day: PLAN_DEFAULTS.pro.max_ingest_bytes_per_day,\n heal_cooldown_seconds: PLAN_DEFAULTS.pro.heal_cooldown_seconds,\n query_rate_limit_per_hour: PLAN_DEFAULTS.pro.query_rate_limit_per_hour,\n onboarding_burst_multiplier: 3,\n onboarding_burst_hours: 48,\n },\n timezone: \"America/New_York\",\n created_at: null,\n },\n };\n}\n\n/** Result of loading config: resolved value and origin path (if any). */\nexport interface LoadConfigResult {\n config: WotwConfig;\n path: string | null;\n}\n\n/**\n * Load configuration from cosmiconfig's discovery of `wotw.config.*`, `.wotwrc`, or\n * a `wotw` key in package.json. If no file is found, the default config is returned.\n *\n * Resolution order (highest to lowest priority):\n * 1. Environment variables (see {@link applyEnvOverrides})\n * 2. User config file\n * 3. Defaults\n *\n * In hosted mode (`WOTW_HOSTED=true` or `hosted.enabled` in the file), the\n * resulting config is additionally checked by {@link validateHostedConfig}\n * which throws when `tenant_id` is missing, malformed, or `wiki_root` is\n * unset.\n *\n * @param searchFrom optional directory to search from (defaults to process.cwd())\n */\nexport async function loadConfig(searchFrom?: string): Promise<LoadConfigResult> {\n const explorer = cosmiconfig(MODULE_NAME, {\n searchPlaces: [\n \"package.json\",\n `.${MODULE_NAME}rc`,\n `.${MODULE_NAME}rc.json`,\n `.${MODULE_NAME}rc.yaml`,\n `.${MODULE_NAME}rc.yml`,\n // `wotw.yaml` / `wotw.yml` are what the `wotw init` wizard writes\n // (see src/cli/commands/init.ts:CONFIG_CANDIDATES). They must be in\n // searchPlaces or every fresh-init vault runs with all-defaults.\n // Finding #12 from PASS-023 dogfood pass.\n `${MODULE_NAME}.yaml`,\n `${MODULE_NAME}.yml`,\n `${MODULE_NAME}.config.json`,\n `${MODULE_NAME}.config.yaml`,\n `${MODULE_NAME}.config.yml`,\n ],\n loaders: {\n \".yaml\": (_filepath, content) => parseYaml(content) as unknown,\n \".yml\": (_filepath, content) => parseYaml(content) as unknown,\n },\n });\n\n let result: CosmiconfigResult;\n try {\n result = await explorer.search(searchFrom ?? process.cwd());\n } catch (err) {\n // cosmiconfig parser errors (malformed YAML/JSON in a discovered config\n // file) surface here. Wrap as ActionableError so the CLI handler\n // renders the path + next-step suggestions instead of a stack trace.\n const hintPath =\n err instanceof Error && (err as { filepath?: string }).filepath\n ? ((err as { filepath?: string }).filepath as string)\n : (searchFrom ?? process.cwd());\n throw configParseError(hintPath, err);\n }\n const defaults = defaultConfig();\n let merged: WotwConfig;\n let path: string | null = null;\n if (!result || !result.config) {\n // eslint-disable-next-line no-console -- runs before pino logger is initialized\n console.warn(\n \"[wotw] no wotw.yaml found — using all defaults (auth disabled, max_daily_usd: 10.0)\",\n );\n merged = defaults;\n } else {\n merged = mergeConfig(defaults, result.config as Partial<WotwConfig>);\n path = result.filepath;\n }\n // Env overrides take precedence over the file. Applied here so the\n // hosted-mode validation below sees the final, fully-resolved values.\n const withEnv = applyEnvOverrides(merged);\n const validated = validateConfig(withEnv);\n validateHostedConfig(validated);\n validateHostedRedactionSink(validated);\n return { config: validated, path };\n}\n\n/**\n * Apply runtime environment-variable overrides to a parsed config. Returns\n * a new object; the input is not mutated. Each override is read from the\n * corresponding env var and only set if the value is non-empty.\n *\n * Variables consumed (each optional):\n * WOTW_HOSTED \"true\" / \"false\" — `hosted.enabled`\n * TENANT_ID UUID — `hosted.tenant_id` (validated downstream)\n * WIKI_ROOT absolute path — `wiki_root`\n * WOTW_PLAN \"founding\" | \"pro\" — `hosted.plan`\n * WOTW_TIMEZONE IANA tz — `hosted.timezone`\n * WOTW_PORT integer — `server.port`\n * WOTW_HOST host string — `server.host`\n * WOTW_LOG_LEVEL pino level — `daemon.log_level`\n * WOTW_RUNTIME_MODE \"auto\" | \"cli\" | \"api\" — `execution.mode`\n * ADMIN_SERVICE_KEY bearer token — `server.auth_token`\n */\nexport function applyEnvOverrides(config: WotwConfig): WotwConfig {\n const out = structuredClone(config);\n const env = process.env;\n\n if (env.WOTW_HOSTED !== undefined) {\n // Review item 61: accept the conventional truthy spellings rather\n // than only \"true\". Pre-fix, \"1\" / \"True\" / \"yes\" / \"on\" all\n // silently set hosted.enabled = false; container operators expect\n // any of these to enable hosted-mode defaults.\n const v = env.WOTW_HOSTED.trim().toLowerCase();\n out.hosted.enabled = v === \"true\" || v === \"1\" || v === \"yes\" || v === \"on\";\n }\n if (env.TENANT_ID && env.TENANT_ID.length > 0) {\n out.hosted.tenant_id = env.TENANT_ID;\n }\n if (env.WIKI_ROOT && env.WIKI_ROOT.length > 0) {\n out.wiki_root = env.WIKI_ROOT;\n // When WIKI_ROOT is explicitly set, the wiki layout is flat — the env\n // value IS the wiki root, no wiki-store/ wrapper. Flatten default paths\n // that assume the wrapper. (User-overridden values in wotw.yaml stay\n // intact; we only touch defaults.)\n if (out.raw_path === \"./wiki-store/raw\") {\n out.raw_path = \"raw\";\n }\n }\n if (env.WOTW_PLAN === \"founding\" || env.WOTW_PLAN === \"pro\") {\n out.hosted.plan = env.WOTW_PLAN;\n }\n if (env.WOTW_TIMEZONE && env.WOTW_TIMEZONE.length > 0) {\n out.hosted.timezone = env.WOTW_TIMEZONE;\n }\n if (env.WOTW_PORT) {\n const n = Number.parseInt(env.WOTW_PORT, 10);\n if (Number.isFinite(n) && n > 0 && n < 65536) {\n out.server.port = n;\n }\n }\n if (env.WOTW_HOST && env.WOTW_HOST.length > 0) {\n out.server.host = env.WOTW_HOST;\n }\n if (env.WOTW_LOG_LEVEL) {\n const lvl = env.WOTW_LOG_LEVEL;\n if (\n lvl === \"trace\" ||\n lvl === \"debug\" ||\n lvl === \"info\" ||\n lvl === \"warn\" ||\n lvl === \"error\" ||\n lvl === \"fatal\"\n ) {\n out.daemon.log_level = lvl;\n }\n }\n if (\n env.WOTW_RUNTIME_MODE === \"auto\" ||\n env.WOTW_RUNTIME_MODE === \"cli\" ||\n env.WOTW_RUNTIME_MODE === \"api\"\n ) {\n out.execution.mode = env.WOTW_RUNTIME_MODE;\n }\n // Multi-LLM Phase 10: per-tenant provider selection via hosted-mode\n // env vars. The wotw-cloud orchestrator sets these at spawn time\n // based on `wiki.llm_provider`. Defaults to anthropic when unset.\n if (\n env.WOTW_LLM_PROVIDER === \"anthropic\" ||\n env.WOTW_LLM_PROVIDER === \"openai\" ||\n env.WOTW_LLM_PROVIDER === \"gemini\" ||\n env.WOTW_LLM_PROVIDER === \"ollama\"\n ) {\n out.llm.provider = env.WOTW_LLM_PROVIDER;\n // Update execution.api_key_env to match the chosen provider's\n // canonical key var. Each provider's SDK reads from its own env var;\n // the daemon's runtime-aware dispatch consults this field when\n // selecting which key to inject.\n switch (env.WOTW_LLM_PROVIDER) {\n case \"anthropic\":\n out.execution.api_key_env = \"ANTHROPIC_API_KEY\";\n break;\n case \"openai\":\n out.execution.api_key_env = \"OPENAI_API_KEY\";\n break;\n case \"gemini\":\n out.execution.api_key_env = \"GOOGLE_API_KEY\";\n break;\n case \"ollama\":\n // Ollama uses no API key. Leave api_key_env at whatever default\n // the config has; the dispatcher ignores it for Ollama.\n break;\n }\n }\n if (env.WOTW_LLM_MODEL && env.WOTW_LLM_MODEL.length > 0) {\n out.llm.model = env.WOTW_LLM_MODEL;\n }\n if (env.WOTW_OLLAMA_URL && env.WOTW_OLLAMA_URL.length > 0) {\n out.llm.ollama_url = env.WOTW_OLLAMA_URL;\n }\n // Review item 50: prefer the dedicated MCP bearer secret; fall back to\n // ADMIN_SERVICE_KEY only while wotw-cloud is being migrated to the\n // split-secret scheme (G4 — viable rip-and-replace with tenant_count=0).\n if (env.WOTW_MCP_BEARER && env.WOTW_MCP_BEARER.length > 0) {\n out.server.auth_token = env.WOTW_MCP_BEARER;\n } else if (env.ADMIN_SERVICE_KEY && env.ADMIN_SERVICE_KEY.length > 0) {\n out.server.auth_token = env.ADMIN_SERVICE_KEY;\n }\n // In hosted mode, default to stdout logging so container log streams\n // (Fly, Kubernetes, Docker) capture daemon output without ssh+cat. The\n // file-default ~/.wotw/daemon.log is invisible to Fly's log stream which\n // is a critical observability blind spot for production support. Users\n // who explicitly want file logging in hosted mode can set WOTW_LOG_FILE.\n if (env.WOTW_LOG_FILE !== undefined) {\n out.daemon.log_file = env.WOTW_LOG_FILE;\n } else if (out.hosted.enabled) {\n out.daemon.log_file = \"\";\n }\n\n // In hosted mode, disable candidates staging. The interactive `wotw start\n // --auto-approve` flag sets `ingestion.staging = false` to bypass the\n // human-review queue; the hosted entry point (dist/daemon/entry.js)\n // bypasses the CLI entirely, so the flag never fires and the\n // interactive-mode default `staging: true` leaks into hosted operation.\n // Without this override, every ingestion writes to candidates/ and waits\n // forever for a human reviewer who doesn't exist in the hosted topology\n // (per-tenant Fly Machine, BYOK, autonomous operation). BM25 indexes\n // wiki/{plural-category}/ and never sees candidates/, so MCP query returns\n // \"no relevant wiki pages found\" despite successful ingestion. Validation-\n // gap instance #12 closure, 2026-05-12.\n if (out.hosted.enabled) {\n out.ingestion.staging = false;\n // Pass 012-completion (2026-05-19): two additional hosted-mode\n // defaults ratified from Pass 012's audit. See\n // feedback_hosted_mode_default_audit.md for the inversion-shape framing.\n //\n // A third candidate (health.detect_contradictions) was originally in\n // Pass 012's audit but surfaced during Pass 012-completion verification\n // as having no runtime consumer at v0.2.12 — the LLM contradiction\n // detection pass that would check this flag is not yet implemented.\n // Hosted-mode override deferred to a future consumer-implementation\n // pass. See TODO at the field's declaration in src/utils/types.ts.\n out.lint.schedule_enabled = true;\n out.lint.auto_fix = true;\n }\n return out;\n}\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * FEATURE-PASS-011: hosted-mode invariant for the daemon→cloud\n * redaction-emit wire (PASS-024 / CT3). When hosted.enabled is true the\n * daemon MUST have a configured cloud sink secret — otherwise every\n * credential redaction silently fails to reach the compliance ledger\n * and the audit trail has gaps. This is the goal's \"Zod schema with a\n * clear error if missing in hosted mode\" requirement: WOTW_CLOUD_SINK_SECRET\n * is an env var (not a config-file field) so the check happens here,\n * alongside the other hosted-mode invariants.\n *\n * Local/offline operation (hosted.enabled false) is intentionally\n * lenient: the SQLite queue captures rows for forensic inspection even\n * when emission is disabled.\n */\nexport function validateHostedRedactionSink(\n config: WotwConfig,\n env: NodeJS.ProcessEnv = process.env,\n): void {\n if (!config.hosted.enabled) return;\n const secret = env.WOTW_CLOUD_SINK_SECRET;\n if (!secret || secret.length === 0) {\n throw new Error(\n \"Config error: hosted.enabled is true but WOTW_CLOUD_SINK_SECRET is unset. \" +\n \"The redaction-emit wire requires this secret to authenticate with \" +\n \"/api/internal/redaction-log; running hosted-mode without it would \" +\n \"silently drop every compliance redaction event.\",\n );\n }\n}\n\n/**\n * Hosted-mode runtime invariants. Throws with a precise message when:\n * - `hosted.enabled` is true but `tenant_id` is missing\n * - `tenant_id` is set but is not a valid UUID\n * - `wiki_root` is an empty/relative path that won't resolve to a stable\n * mount point under `/data`\n *\n * Called from {@link loadConfig}. Community-mode configs (where\n * `hosted.enabled` is false) are unaffected.\n */\nexport function validateHostedConfig(config: WotwConfig): void {\n if (!config.hosted.enabled) return;\n if (!config.hosted.tenant_id || config.hosted.tenant_id.length === 0) {\n throw new Error(\n \"Config error: hosted.enabled is true but TENANT_ID / hosted.tenant_id is unset.\",\n );\n }\n if (!UUID_REGEX.test(config.hosted.tenant_id)) {\n throw new Error(\n `Config error: hosted.tenant_id \"${config.hosted.tenant_id}\" is not a valid UUID.`,\n );\n }\n if (!config.wiki_root || config.wiki_root.length === 0) {\n throw new Error(\"Config error: hosted.enabled is true but wiki_root / WIKI_ROOT is unset.\");\n }\n // Review item 60: a relative wiki_root in hosted mode resolves against\n // process.cwd(), which is the ephemeral container filesystem on Fly\n // Machines. Tenant data would be wiped on every restart. Require\n // absolute path in hosted mode.\n if (!config.wiki_root.startsWith(\"/\")) {\n throw new Error(\n `Config error: hosted.enabled is true but wiki_root \"${config.wiki_root}\" is not absolute. ` +\n `In hosted mode the daemon must persist tenant data on a mounted volume; ` +\n `a relative path resolves against process.cwd() which is the ephemeral container fs.`,\n );\n }\n}\n\n/**\n * Deep-merge user config on top of defaults. Unknown keys in user config are dropped\n * to prevent typos from leaking into runtime behavior.\n */\nexport function mergeConfig(base: WotwConfig, override: Partial<WotwConfig>): WotwConfig {\n const out: WotwConfig = structuredClone(base);\n const assign = <K extends keyof WotwConfig>(key: K, value: Partial<WotwConfig[K]>): void => {\n out[key] = { ...(out[key] as object), ...(value as object) } as WotwConfig[K];\n };\n\n if (override.wiki_root !== undefined) out.wiki_root = override.wiki_root;\n if (override.raw_path !== undefined) out.raw_path = override.raw_path;\n if (override.llm) assign(\"llm\", override.llm);\n if (override.execution) assign(\"execution\", override.execution);\n if (override.models) assign(\"models\", override.models);\n if (override.watcher) assign(\"watcher\", override.watcher);\n if (override.ingestion) assign(\"ingestion\", override.ingestion);\n if (override.cost) assign(\"cost\", override.cost);\n if (override.server) assign(\"server\", override.server);\n if (override.daemon) assign(\"daemon\", override.daemon);\n if (override.compounding) assign(\"compounding\", override.compounding);\n if (override.provenance) assign(\"provenance\", override.provenance);\n if (override.multi_user) assign(\"multi_user\", override.multi_user);\n if (override.query) assign(\"query\", override.query);\n if (override.fact_extraction) assign(\"fact_extraction\", override.fact_extraction);\n if (override.lint) assign(\"lint\", override.lint);\n if (override.health) {\n // Deep-merge the weights sub-object separately.\n const healthBase = out.health;\n const healthOverride = override.health as Partial<WotwConfig[\"health\"]>;\n out.health = { ...healthBase, ...healthOverride };\n if (healthOverride.weights) {\n out.health.weights = { ...healthBase.weights, ...healthOverride.weights };\n }\n }\n if (override.hosted) assign(\"hosted\", override.hosted);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Zod validation schema\n// ---------------------------------------------------------------------------\n\nconst positiveNumber = z.number().positive();\nconst nonNegativeNumber = z.number().min(0);\nconst logLevelSchema = z.enum([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]);\nconst llmProviderSchema = z.enum([\"anthropic\", \"openai\", \"gemini\", \"ollama\"]);\n\n/**\n * Zod schema that validates a fully-merged WotwConfig object. Every field\n * has a default matching {@link defaultConfig} so a bare `{}` passes.\n */\nconst WotwConfigSchema = z.object({\n wiki_root: z.string().min(1),\n raw_path: z.string().min(1),\n llm: z.object({\n provider: llmProviderSchema,\n model: z.string().min(1),\n ollama_url: z.string().min(1).optional(),\n }),\n execution: z.object({\n mode: z.enum([\"auto\", \"cli\", \"api\"]),\n cli_path: z.string().min(1),\n cli_model: z.string().min(1),\n api_key_env: z.string().min(1),\n }),\n models: z.object({\n ingest: z.string().min(1),\n query: z.string().min(1),\n lint: z.string().min(1),\n compound_eval: z.string().min(1),\n }),\n watcher: z.object({\n debounce_initial_ms: positiveNumber,\n debounce_max_ms: positiveNumber,\n debounce_growth_factor: positiveNumber,\n burst_threshold: z.number().int().positive(),\n max_batch_size: z.number().int().positive(),\n ignore_patterns: z.array(z.string()),\n }),\n ingestion: z.object({\n max_turns: z.number().int().positive(),\n max_budget_per_batch_usd: positiveNumber,\n resume_session: z.boolean(),\n dead_letter_file: z.string(),\n staging: z.boolean(),\n }),\n cost: z.object({\n max_daily_usd: positiveNumber,\n max_per_query_usd: positiveNumber,\n max_per_ingest_usd: positiveNumber,\n track_file: z.string().min(1),\n }),\n server: z.object({\n port: z.number().int().min(1).max(65535),\n host: z.string().min(1),\n auth_token: z.string().nullable(),\n rate_limit_rpm: z.number().int().positive(),\n trust_proxy: z.boolean(),\n }),\n daemon: z.object({\n pid_file: z.string().min(1),\n lock_file: z.string().min(1),\n // Empty string is meaningful: \"log to stdout\" (used by hosted mode for\n // container log capture). initLogger treats empty as no destination\n // and defaults to pino's stdout output.\n log_file: z.string(),\n log_level: logLevelSchema,\n }),\n compounding: z.object({\n enabled: z.boolean(),\n min_source_pages: z.number().int().min(0),\n confidence_threshold: z.number().min(0).max(100),\n }),\n provenance: z.object({\n enabled: z.boolean(),\n chain_file: z.string().min(1),\n verify_on_startup: z.boolean(),\n }),\n multi_user: z.object({\n enabled: z.boolean(),\n workspaces_dir: z.string().min(1),\n }),\n query: z.object({\n expand: z.boolean(),\n }),\n fact_extraction: z.object({\n enabled: z.union([z.boolean(), z.literal(\"auto\")]),\n force_enabled: z.boolean(),\n questions_per_fact: z.number().int().min(1).max(5),\n model: z.string().min(1).optional(),\n }),\n lint: z.object({\n schedule_enabled: z.boolean(),\n interval_hours: positiveNumber,\n auto_fix: z.boolean(),\n }),\n hosted: z.object({\n enabled: z.boolean(),\n tenant_id: z.string().nullable(),\n concurrency_cap: z.number().int().positive(),\n paused: z.boolean(),\n plan: z.enum([\"founding\", \"pro\"]),\n limits: z.object({\n storage_bytes: positiveNumber,\n max_files_per_day: z.number().int().positive(),\n max_file_size_bytes: positiveNumber,\n max_ingest_bytes_per_day: positiveNumber,\n heal_cooldown_seconds: nonNegativeNumber,\n query_rate_limit_per_hour: z.number().int().positive(),\n onboarding_burst_multiplier: positiveNumber,\n onboarding_burst_hours: positiveNumber,\n }),\n timezone: z.string().min(1),\n created_at: z.string().nullable(),\n }),\n health: z.object({\n staleness_thresholds: z.array(z.number().int().min(0)),\n staleness_scores: z.array(z.number().min(0).max(100)),\n weights: z.object({\n staleness: nonNegativeNumber,\n source_availability: nonNegativeNumber,\n link_health: nonNegativeNumber,\n duplicate_risk: nonNegativeNumber,\n contradiction_risk: nonNegativeNumber,\n }),\n duplicate_threshold: z.number().min(0).max(100),\n auto_fix_staleness_below: z.number().min(0).max(100),\n max_fixes_per_run: z.number().int().min(0),\n detect_contradictions: z.boolean(),\n consolidation_threshold: z.number().int().min(2),\n consolidation_enabled: z.boolean(),\n zero_hit_threshold: z.number().min(0).max(1),\n enrichment_enabled: z.boolean(),\n query_log_file: z.string(),\n }),\n});\n\n/**\n * Validate a merged config against the Zod schema. Throws a descriptive\n * error on failure, naming the invalid field, expected type, and value.\n */\nexport function validateConfig(config: WotwConfig): WotwConfig {\n const result = WotwConfigSchema.safeParse(config);\n if (!result.success) {\n const issue = result.error.issues[0]!;\n const path = issue.path.join(\".\");\n throw new Error(`Config error: \"${path}\" ${issue.message}`);\n }\n return result.data as WotwConfig;\n}\n\n/**\n * Expand all path-like fields in a config using {@link resolvePath}.\n * Returns a new config; the input is not mutated.\n */\nexport function resolveConfigPaths(config: WotwConfig, baseDir?: string): WotwConfig {\n const out = structuredClone(config);\n out.wiki_root = resolvePath(out.wiki_root, baseDir);\n out.raw_path = resolvePath(out.raw_path, baseDir);\n out.cost.track_file = resolvePath(out.cost.track_file, baseDir);\n out.daemon.pid_file = resolvePath(out.daemon.pid_file, baseDir);\n out.daemon.lock_file = resolvePath(out.daemon.lock_file, baseDir);\n // Empty log_file is meaningful (means \"log to stdout\" in hosted mode);\n // resolvePath would prepend baseDir to it, producing a path that points\n // at the baseDir itself. Skip resolution when empty.\n if (out.daemon.log_file.length > 0) {\n out.daemon.log_file = resolvePath(out.daemon.log_file, baseDir);\n }\n out.multi_user.workspaces_dir = resolvePath(out.multi_user.workspaces_dir, baseDir);\n // Provenance chain lives inside the wiki root by default — resolve it\n // against the (already resolved) wiki_root so a relative default like\n // `provenance-chain.jsonl` lands in the right place.\n out.provenance.chain_file = resolvePath(out.provenance.chain_file, out.wiki_root);\n // Dead-letter file is likewise wiki-relative (so each wiki has its own\n // failure ledger). Empty string disables; leave it alone in that case.\n if (out.ingestion.dead_letter_file.length > 0) {\n out.ingestion.dead_letter_file = resolvePath(out.ingestion.dead_letter_file, out.wiki_root);\n }\n // Query log file is wiki-relative, like the dead-letter file.\n if (out.health.query_log_file.length > 0) {\n out.health.query_log_file = resolvePath(out.health.query_log_file, out.wiki_root);\n }\n return out;\n}\n","/**\n * Actionable error class for the 10 user-facing unhappy paths surfaced\n * by PASS-023 (public-launch readiness). Each instance carries:\n *\n * - a stable `code` for programmatic handling + log indexing\n * - a one-line `summary` for the top of the rendered error\n * - a `cause` chain for diagnostics\n * - a `suggestions` array of concrete next steps the user can take\n *\n * The CLI's top-level error handler renders these as a structured block\n * to stderr; the daemon's internal logger captures the same fields as\n * structured JSON. Stack traces are suppressed unless `WOTW_DEBUG=1`.\n */\nexport type ActionableErrorCode =\n | \"MISSING_VAULT_PATH\"\n | \"CONFIG_PARSE_ERROR\"\n | \"NATIVE_BINDING_LOAD_FAILURE\"\n | \"INVALID_API_KEY\"\n | \"RATE_LIMITED\"\n | \"WIKI_DIR_PERMISSION_DENIED\"\n | \"VAULT_FILE_LOCKED\"\n | \"PORT_IN_USE\"\n | \"DAEMON_ALREADY_RUNNING\"\n | \"INIT_TARGET_NOT_EMPTY\";\n\nexport interface ActionableErrorOptions {\n code: ActionableErrorCode;\n summary: string;\n suggestions: readonly string[];\n cause?: unknown;\n docs?: string;\n}\n\nexport class ActionableError extends Error {\n readonly code: ActionableErrorCode;\n readonly summary: string;\n readonly suggestions: readonly string[];\n readonly docs?: string;\n\n constructor(opts: ActionableErrorOptions) {\n const lines: string[] = [opts.summary];\n if (opts.suggestions.length > 0) {\n lines.push(\"\", \"What to try:\");\n for (const suggestion of opts.suggestions) {\n lines.push(` - ${suggestion}`);\n }\n }\n if (opts.docs) {\n lines.push(\"\", `Docs: ${opts.docs}`);\n }\n super(lines.join(\"\\n\"));\n this.name = \"ActionableError\";\n this.code = opts.code;\n this.summary = opts.summary;\n this.suggestions = opts.suggestions;\n this.docs = opts.docs;\n if (opts.cause !== undefined) {\n (this as { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n\nexport function isActionableError(e: unknown): e is ActionableError {\n return e instanceof ActionableError;\n}\n\n/**\n * Heuristic detection of native-binding load failures. better-sqlite3\n * (and other N-API addons) surface very platform-specific error\n * messages; this matcher captures the common shapes.\n */\nexport function looksLikeNativeBindingFailure(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n return (\n /Cannot find module .*\\.node/i.test(msg) ||\n /could not load the bindings file/i.test(msg) ||\n /better.sqlite3.*\\.node.*was compiled against/i.test(msg) ||\n /NODE_MODULE_VERSION/i.test(msg) ||\n /ERR_DLOPEN_FAILED/i.test(msg) ||\n /libstdc\\+\\+.*GLIBCXX/i.test(msg) ||\n /Symbol not found.*sqlite3/i.test(msg) ||\n /image not found/i.test(msg)\n );\n}\n\n/**\n * Heuristic detection of EACCES / EPERM errors from filesystem APIs.\n */\nexport function looksLikePermissionDenied(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n const code = e instanceof Error ? (e as { code?: string }).code : undefined;\n return code === \"EACCES\" || code === \"EPERM\" || /EACCES|EPERM/i.test(msg);\n}\n\n/**\n * Heuristic detection of file-lock contention errors. Obsidian holds\n * EBUSY/ETXTBSY on macOS, an undocumented lock state on Windows, and\n * EAGAIN on Linux when another process holds an exclusive lock.\n */\nexport function looksLikeFileLock(e: unknown): boolean {\n const code = e instanceof Error ? (e as { code?: string }).code : undefined;\n const msg = e instanceof Error ? e.message : String(e);\n return (\n code === \"EBUSY\" ||\n code === \"ETXTBSY\" ||\n code === \"EAGAIN\" ||\n /EBUSY|ETXTBSY|EAGAIN|file is locked/i.test(msg)\n );\n}\n\n/**\n * Heuristic detection of port-in-use binding failures. listen() rejects\n * with EADDRINUSE on every supported platform.\n */\nexport function looksLikePortInUse(e: unknown): boolean {\n const code = e instanceof Error ? (e as { code?: string }).code : undefined;\n const msg = e instanceof Error ? e.message : String(e);\n return code === \"EADDRINUSE\" || /EADDRINUSE/i.test(msg);\n}\n\n/**\n * Build an ActionableError for the OBSIDIAN_VAULT_PATH-missing path\n * (item 1 of the PASS-023 error audit).\n */\nexport function missingVaultPathError(): ActionableError {\n return new ActionableError({\n code: \"MISSING_VAULT_PATH\",\n summary: \"No Obsidian vault path was provided and none could be auto-detected.\",\n suggestions: [\n \"Run `wotw init` and pick a vault interactively.\",\n \"Pass `--path /path/to/vault` to point at a specific directory.\",\n \"Set OBSIDIAN_VAULT_PATH in your shell environment to a vault root.\",\n \"Install Obsidian and open at least one vault to register a default.\",\n ],\n docs: \"docs/init-walkthrough.md\",\n });\n}\n\n/**\n * Build an ActionableError for the malformed-config path (item 2).\n */\nexport function configParseError(configPath: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"CONFIG_PARSE_ERROR\",\n summary: `Could not parse wotw config at ${configPath}: ${cause instanceof Error ? cause.message : String(cause)}`,\n suggestions: [\n 'Validate the file with `node -e \\'console.log(JSON.parse(require(\"fs\").readFileSync(process.argv[1], \"utf8\")))\\' ' +\n configPath +\n \"` (for JSON) or `yamllint \" +\n configPath +\n \"` (for YAML).\",\n \"Check for unmatched braces, missing colons, or trailing commas.\",\n \"Compare against the example at docs/configuration.md.\",\n \"Delete the file and re-run `wotw init` to regenerate a fresh config.\",\n ],\n docs: \"docs/configuration.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the better-sqlite3 native-binding-load\n * path (item 3).\n */\nexport function nativeBindingLoadError(module: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"NATIVE_BINDING_LOAD_FAILURE\",\n summary: `Could not load the native binding for ${module} on this platform.`,\n suggestions: [\n \"Run `pnpm rebuild \" +\n module +\n \"` (or `npm rebuild \" +\n module +\n \"`) in the install directory.\",\n \"Verify Node.js >= 20 is installed (`node --version`).\",\n \"Confirm your platform matches one of: macOS arm64, macOS amd64, Linux amd64, Windows amd64.\",\n \"If running under Docker, ensure the image was built for the runtime arch (not host arch).\",\n \"Reinstall: `npm uninstall -g @3030-labs/wotw && npm install -g @3030-labs/wotw`.\",\n ],\n docs: \"docs/install-evidence/\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the invalid-API-key path (item 4).\n */\nexport function invalidApiKeyError(\n provider: \"anthropic\" | \"openai\" | \"gemini\" | \"other\",\n envVar: string,\n cause?: unknown,\n): ActionableError {\n return new ActionableError({\n code: \"INVALID_API_KEY\",\n summary: `Provider ${provider} returned 401 Unauthorized — the API key in ${envVar} is invalid or revoked.`,\n suggestions: [\n `Verify the key works: \\`curl -H \"x-api-key: $${envVar}\" https://api.${provider === \"anthropic\" ? \"anthropic.com\" : provider + \".com\"}/v1/models\\` (provider URL may vary).`,\n \"Regenerate the key at the provider's console if it's revoked.\",\n `Re-export the new key (\\`export ${envVar}=\"...\"\\`) and restart the daemon (\\`wotw stop && wotw start\\`).`,\n \"Daemon does NOT pick up env-var changes while running — restart is required.\",\n ],\n docs: \"docs/self-hosted-byok.md\",\n cause,\n });\n}\n\n/**\n * Detect a Claude Code CLI authentication failure in the CLI's output.\n * In CLI (subscription) runtime, a 401 surfaces in the agent's stdout as\n * an \"API Error: 401 ... authentication_error ... Please run /login\"\n * string rather than as an HTTP status the daemon can branch on\n * (PASS-023 dogfood finding #21).\n */\nexport function looksLikeCliAuthFailure(text: string): boolean {\n return (\n /API Error:\\s*401/i.test(text) ||\n /authentication_error/i.test(text) ||\n /invalid authentication credentials/i.test(text) ||\n /please run \\/login/i.test(text) ||\n /not (logged in|authenticated)/i.test(text)\n );\n}\n\n/**\n * Build an ActionableError for a Claude Code CLI auth failure (item 4,\n * CLI-runtime variant). Distinct remediation from the env-var API-key\n * path: the fix is `claude /login`, not rotating an env var.\n */\nexport function cliAuthError(cause?: unknown): ActionableError {\n return new ActionableError({\n code: \"INVALID_API_KEY\",\n summary:\n \"The Claude Code CLI is not authenticated (got 401 from the model API). \" +\n \"wotw is in CLI runtime mode and the `claude` binary has no valid session.\",\n suggestions: [\n \"Run `claude` to open the interactive shell, then type `/login` and complete the browser auth flow.\",\n 'Verify auth worked: `echo \"hi\" | claude -p` should print a reply, not a 401.',\n \"Then restart the daemon: `wotw stop && wotw start`.\",\n \"If you intended to use an API key instead of the subscription CLI, set `execution.mode: api` + `ANTHROPIC_API_KEY` in your config — see docs/self-hosted-byok.md.\",\n ],\n docs: \"docs/self-hosted-byok.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the rate-limited path (item 5).\n */\nexport function rateLimitedError(\n provider: \"anthropic\" | \"openai\" | \"gemini\" | \"other\",\n retryAfterSec?: number,\n cause?: unknown,\n): ActionableError {\n return new ActionableError({\n code: \"RATE_LIMITED\",\n summary: `Provider ${provider} returned 429 — rate limit exceeded${\n retryAfterSec !== undefined ? ` (retry after ${retryAfterSec}s)` : \"\"\n }.`,\n suggestions: [\n \"Lower `ingestion.concurrency` in wotw.config.yaml to spread requests further.\",\n `Wait ${retryAfterSec ?? \"the indicated\"} seconds and retry.`,\n \"Upgrade your provider tier if 429s are sustained over multiple ingest cycles.\",\n \"Check `wotw status` for dead-letter queue growth; clear with `wotw dlq retry` once the window recovers.\",\n ],\n docs: \"docs/self-hosted-byok.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the wiki-dir EACCES path (item 6).\n */\nexport function wikiDirPermissionError(path: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"WIKI_DIR_PERMISSION_DENIED\",\n summary: `Cannot create or write to wiki directory at ${path}: permission denied.`,\n suggestions: [\n `Check the directory's owner and permissions: \\`ls -la \"${path}\"\\`.`,\n `Ensure the daemon's user can write: \\`chmod u+w \"${path}\"\\` (or relocate to a writable parent).`,\n \"If running under Docker, confirm the volume mount has the right ownership.\",\n \"Choose a different `wiki_root` in wotw.config.yaml.\",\n ],\n docs: \"docs/configuration.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the locked-vault-file path (item 7).\n */\nexport function vaultFileLockedError(path: string, cause: unknown): ActionableError {\n return new ActionableError({\n code: \"VAULT_FILE_LOCKED\",\n summary: `Cannot write to ${path}: another process is holding an exclusive lock (usually Obsidian).`,\n suggestions: [\n \"Close the file in Obsidian (or close Obsidian entirely) and retry the operation.\",\n \"On Windows, check for indexing services or backup tools that may briefly lock files.\",\n \"Run `wotw status` to confirm the lock cleared after closing the holder.\",\n \"If the lock persists with no holder visible, restart your machine.\",\n ],\n docs: \"docs/obsidian-setup.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the port-in-use path (item 8).\n */\nexport function portInUseError(port: number, cause?: unknown): ActionableError {\n return new ActionableError({\n code: \"PORT_IN_USE\",\n summary: `Port ${port} is already in use; the MCP server cannot bind.`,\n suggestions: [\n `Find the process holding the port: \\`lsof -iTCP:${port} -sTCP:LISTEN\\` (macOS/Linux) or \\`netstat -ano | findstr :${port}\\` (Windows).`,\n \"Stop that process, or change `server.port` in wotw.config.yaml to a free port.\",\n `If it's an orphaned wotw daemon, \\`wotw stop\\` (or kill the PID) and retry.`,\n ],\n docs: \"docs/configuration.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the daemon-already-running path (item 9).\n */\nexport function daemonAlreadyRunningError(lockPath: string, cause?: unknown): ActionableError {\n return new ActionableError({\n code: \"DAEMON_ALREADY_RUNNING\",\n summary: `Another wotw daemon already holds the lock at ${lockPath}.`,\n suggestions: [\n \"Run `wotw status` to see the live daemon's PID and health.\",\n \"If you intend to replace it, `wotw stop` first, then `wotw start`.\",\n \"If the lock is stale (e.g. after a crash), `wotw stop --force` clears it.\",\n ],\n docs: \"docs/cli-reference.md\",\n cause,\n });\n}\n\n/**\n * Build an ActionableError for the non-empty-init-target path (item 10).\n */\nexport function initTargetNotEmptyError(\n path: string,\n conflictingEntries: readonly string[],\n): ActionableError {\n return new ActionableError({\n code: \"INIT_TARGET_NOT_EMPTY\",\n summary: `Cannot scaffold a new vault at ${path}: target directory is not empty.`,\n suggestions: [\n `Conflicting entries: ${conflictingEntries.slice(0, 5).join(\", \")}${conflictingEntries.length > 5 ? ` … (+${conflictingEntries.length - 5} more)` : \"\"}.`,\n \"Pick a different `--path` (or an empty subdirectory of this one).\",\n \"If you meant to overlay onto an existing vault, this is the wrong code path — the wizard would have offered an overlay prompt. Confirm the target has `.obsidian/` and re-run.\",\n \"If you're sure the existing files are safe to overwrite, re-run with `--force`.\",\n ],\n docs: \"docs/init-walkthrough.md\",\n });\n}\n","/**\n * File system utilities: atomic writes, directory scaffolding, path resolution.\n */\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, resolve } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Expand a leading `~` to the current user's home directory.\n */\nexport function expandHome(path: string): string {\n if (path.startsWith(\"~\")) {\n return resolve(homedir(), path.slice(path.startsWith(\"~/\") ? 2 : 1));\n }\n return path;\n}\n\n/**\n * Resolve a path against an optional base directory, expanding `~`.\n */\nexport function resolvePath(path: string, base?: string): string {\n const expanded = expandHome(path);\n if (isAbsolute(expanded)) return expanded;\n return resolve(base ?? process.cwd(), expanded);\n}\n\n/**\n * Ensure the directory at `dir` exists, creating it and all parents if needed.\n */\nexport function ensureDirSync(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\n/**\n * Async version of {@link ensureDirSync}.\n */\nexport async function ensureDir(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n}\n\n/**\n * Atomic write: write to a temp file in the same directory and rename.\n * This guarantees readers never see a partial file on POSIX systems.\n */\nexport function atomicWriteSync(filePath: string, contents: string | Buffer): void {\n ensureDirSync(dirname(filePath));\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n let renamed = false;\n try {\n writeFileSync(tmp, contents);\n renameSync(tmp, filePath);\n renamed = true;\n } finally {\n if (!renamed) {\n try {\n rmSync(tmp, { force: true });\n } catch {\n /* best effort */\n }\n }\n }\n}\n\n/**\n * Async version of {@link atomicWriteSync}.\n */\nexport async function atomicWrite(filePath: string, contents: string | Buffer): Promise<void> {\n await ensureDir(dirname(filePath));\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n let renamed = false;\n try {\n await writeFile(tmp, contents);\n await rename(tmp, filePath);\n renamed = true;\n } finally {\n if (!renamed) {\n try {\n rmSync(tmp, { force: true });\n } catch {\n /* best effort */\n }\n }\n }\n}\n\n/**\n * Read a UTF-8 file, returning null if it does not exist.\n */\nexport function readTextOrNull(filePath: string): string | null {\n try {\n return readFileSync(filePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/**\n * Async version of {@link readTextOrNull}.\n */\nexport async function readTextOrNullAsync(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/**\n * Delete a file if it exists, otherwise no-op.\n */\nexport function removeIfExistsSync(filePath: string): void {\n if (existsSync(filePath)) {\n rmSync(filePath, { force: true });\n }\n}\n\n/**\n * Check if a path points to an existing file.\n */\nexport function fileExists(filePath: string): boolean {\n try {\n return statSync(filePath).isFile();\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw err;\n }\n}\n\n/**\n * Check if a path points to an existing directory.\n */\nexport function dirExists(dirPath: string): boolean {\n try {\n return statSync(dirPath).isDirectory();\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw err;\n }\n}\n","/**\n * Daemon lifecycle: PID file management, file locking, graceful shutdown.\n *\n * A running daemon has three facts associated with it:\n * 1. A PID file containing its numeric PID and a timestamp.\n * 2. A lock file that prevents two daemons from starting simultaneously.\n * 3. A log file where pino writes structured events.\n */\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { atomicWriteSync, ensureDirSync, removeIfExistsSync } from \"../utils/fs.js\";\n\nexport interface PidFileContents {\n pid: number;\n started_at: string;\n version: string;\n}\n\n/**\n * Write the PID file atomically.\n */\nexport function writePidFile(pidFilePath: string, contents: PidFileContents): void {\n ensureDirSync(dirname(pidFilePath));\n atomicWriteSync(pidFilePath, JSON.stringify(contents));\n}\n\n/**\n * Read a PID file, returning null if missing or malformed.\n */\nexport function readPidFile(pidFilePath: string): PidFileContents | null {\n if (!existsSync(pidFilePath)) return null;\n try {\n const raw = readFileSync(pidFilePath, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed &&\n typeof parsed === \"object\" &&\n \"pid\" in parsed &&\n typeof (parsed as { pid: unknown }).pid === \"number\"\n ) {\n return parsed as PidFileContents;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Remove the PID file, ignoring missing files.\n */\nexport function removePidFile(pidFilePath: string): void {\n removeIfExistsSync(pidFilePath);\n}\n\n/**\n * Check whether a process is alive by sending signal 0. Returns true if it is\n * still alive, false otherwise. Handles EPERM (process owned by another user)\n * by returning true, since the process exists even if we cannot signal it.\n */\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ESRCH\") return false;\n if (code === \"EPERM\") return true;\n return false;\n }\n}\n\n/**\n * Check liveness based on the PID file: returns {alive, pid, contents}.\n * If the PID file references a dead process, the file is considered stale.\n */\nexport function checkDaemonAlive(pidFilePath: string): {\n alive: boolean;\n pid: number | null;\n stale: boolean;\n contents: PidFileContents | null;\n} {\n const contents = readPidFile(pidFilePath);\n if (!contents) return { alive: false, pid: null, stale: false, contents: null };\n const alive = isProcessAlive(contents.pid);\n return { alive, pid: contents.pid, stale: !alive, contents };\n}\n\n/**\n * Acquire the daemon start-lock. Returns a release function on success.\n * Throws if another process holds the lock.\n */\nexport async function acquireStartLock(lockPath: string): Promise<() => Promise<void>> {\n ensureDirSync(dirname(lockPath));\n // proper-lockfile requires the target file to exist; create a zero-byte stub if missing.\n if (!existsSync(lockPath)) writeFileSync(lockPath, \"\");\n const release = await lockfile.lock(lockPath, {\n stale: 10_000,\n retries: { retries: 0 },\n });\n return release;\n}\n\n/**\n * Send SIGTERM to a PID and wait up to `timeoutMs` for it to exit. Returns true\n * if the process exited, false if it is still alive after the timeout.\n */\nexport async function terminateAndWait(pid: number, timeoutMs = 10_000): Promise<boolean> {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ESRCH\") return true;\n throw err;\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isProcessAlive(pid)) return true;\n await new Promise((r) => setTimeout(r, 100));\n }\n return !isProcessAlive(pid);\n}\n","/**\n * Centralized pino logger factory. All modules should import `getLogger()` rather\n * than constructing their own logger so we have one configuration surface.\n */\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport pino, { type Logger, type LoggerOptions } from \"pino\";\n\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\" | \"fatal\";\n\nlet rootLogger: Logger | null = null;\n\n/**\n * Initialize the root logger. Idempotent — calling again replaces the existing logger.\n *\n * @param level minimum log level to emit\n * @param logFile optional path to a log file. If provided, JSON lines are written there.\n * If omitted, logs go to stdout in pretty format for humans.\n */\n/**\n * Pino redact paths — review item 1 closure.\n *\n * Pino's `redact.paths` walks the structured log object before serialization\n * and replaces matching values with `[Redacted]`. We list the common shapes\n * the daemon's log sites produce when error / response objects flow\n * through: headers.authorization, ANTHROPIC_API_KEY-shaped env values,\n * SDK error payloads with embedded keys, etc.\n *\n * This is BELT to the SUSPENDERS of src/utils/sanitize.ts's string-level\n * regex redaction. Sanitize covers free-form text in prompts; redact\n * covers structured fields the call site might have forgotten to pass\n * through sanitize.\n */\nconst REDACT_PATHS = [\n \"headers.authorization\",\n \"headers.Authorization\",\n \"*.headers.authorization\",\n \"*.headers.Authorization\",\n \"req.headers.authorization\",\n \"req.headers.Authorization\",\n \"request.headers.authorization\",\n \"request.headers.Authorization\",\n \"response.headers.authorization\",\n \"response.headers.Authorization\",\n \"headers['x-admin-key']\",\n \"headers['x-api-key']\",\n \"headers.cookie\",\n \"*.headers.cookie\",\n \"config.api_key\",\n \"config.headers.authorization\",\n \"err.config.headers.authorization\",\n \"err.request.headers.authorization\",\n \"err.response.headers.authorization\",\n // Provider SDK error shapes — observed leaking via err.headers.* on Axios-based clients.\n \"err.headers.authorization\",\n \"err.headers.Authorization\",\n // Env-bag dumps (occasionally added by ad-hoc debug logs).\n \"env.ANTHROPIC_API_KEY\",\n \"env.OPENAI_API_KEY\",\n \"env.GOOGLE_API_KEY\",\n \"env.ADMIN_SERVICE_KEY\",\n \"env.WOTW_MCP_BEARER\",\n \"env.WOTW_INTERNAL_ADMIN_KEY\",\n \"env.WOTW_CLOUD_SINK_SECRET\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GOOGLE_API_KEY\",\n \"ADMIN_SERVICE_KEY\",\n \"WOTW_MCP_BEARER\",\n \"WOTW_INTERNAL_ADMIN_KEY\",\n \"WOTW_CLOUD_SINK_SECRET\",\n \"apiKey\",\n \"api_key\",\n \"*.apiKey\",\n \"*.api_key\",\n \"secret\",\n \"*.secret\",\n];\n\n/**\n * Review item 6: Pino's default err serializer dumps arbitrary error\n * properties via `pino.stdSerializers.err`. Empirically verified that\n * SDK errors carry `err.headers.authorization` / `err.response.headers.*`\n * verbatim. The redact-paths layer (item 1) catches the common shapes,\n * but a custom serializer makes the safety guarantee load-bearing:\n * only the small allowlist of fields below is ever emitted; everything\n * else is dropped, regardless of where it sits on the error object.\n */\nfunction safeErrSerializer(err: unknown): Record<string, unknown> {\n if (!(err instanceof Error)) {\n return { message: String(err) };\n }\n const out: Record<string, unknown> = {\n type: err.constructor.name,\n message: err.message,\n };\n if (err.stack) out.stack = err.stack;\n // Some custom errors (SafeFetchError, OrchestratorError) carry a\n // `code` field that's safe to surface — it's an enum, never user\n // content.\n const maybeCode = (err as unknown as { code?: unknown }).code;\n if (typeof maybeCode === \"string\") out.code = maybeCode;\n // `cause` is usually another Error — recurse one level (cap depth\n // to avoid stack overflow on circular chains).\n const maybeCause = (err as unknown as { cause?: unknown }).cause;\n if (maybeCause && typeof maybeCause === \"object\" && \"message\" in maybeCause) {\n out.cause = {\n type: (maybeCause as { constructor?: { name: string } }).constructor?.name ?? \"Error\",\n message: (maybeCause as { message: unknown }).message,\n };\n }\n // INTENTIONALLY DROPPED (item 6 fix surface):\n // err.headers — Axios-shape; carries authorization\n // err.config — Axios-shape; carries authorization header\n // err.request — Axios-shape; carries headers\n // err.response — Axios-shape; carries headers + body\n // any other property — unknown SDK shapes default to \"drop\"\n return out;\n}\n\nexport function initLogger(level: LogLevel = \"info\", logFile?: string): Logger {\n const options: LoggerOptions = {\n level,\n base: { pid: process.pid, hostname: undefined },\n timestamp: pino.stdTimeFunctions.isoTime,\n redact: {\n paths: REDACT_PATHS,\n censor: \"[Redacted]\",\n remove: false,\n },\n serializers: {\n err: safeErrSerializer,\n error: safeErrSerializer,\n },\n };\n\n if (logFile) {\n mkdirSync(dirname(logFile), { recursive: true });\n rootLogger = pino(options, pino.destination({ dest: logFile, sync: false, mkdir: true }));\n } else {\n rootLogger = pino({\n ...options,\n transport: {\n target: \"pino-pretty\",\n options: { colorize: true, translateTime: \"HH:MM:ss.l\", ignore: \"pid,hostname\" },\n },\n });\n }\n return rootLogger;\n}\n\n/** Default context fields merged into every child logger (e.g. tenantId in hosted mode). */\nlet defaultContext: Record<string, unknown> = {};\n\n/**\n * Set default context fields that are merged into every child logger.\n * Call once at startup when hosted.enabled is true.\n */\nexport function setLoggerContext(ctx: Record<string, unknown>): void {\n defaultContext = ctx;\n}\n\n/**\n * Return the root logger, initializing with defaults if none exists.\n * When defaultContext has been set (hosted mode), every child logger\n * automatically includes those fields.\n */\nexport function getLogger(module?: string, extra?: Record<string, unknown>): Logger {\n if (!rootLogger) {\n rootLogger = initLogger(\"info\");\n }\n const ctx = { ...defaultContext, ...extra, ...(module ? { module } : {}) };\n return Object.keys(ctx).length > 0 ? rootLogger.child(ctx) : rootLogger;\n}\n","/**\n * Single source of truth for the package version.\n *\n * Uses `createRequire` to read `package.json` at runtime so the version\n * can never drift from the declared value. This is safe in ESM because\n * `createRequire` resolves relative to the compiled output directory and\n * `package.json` sits at the repo root (two levels up from `dist/utils/`).\n */\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../../package.json\") as { version: string };\n\nexport const VERSION: string = pkg.version;\n","/**\n * Execution mode detection. Resolves the configured {@link ExecutionMode}\n * (\"auto\" | \"cli\" | \"api\") into a concrete {@link RuntimeMode} (\"cli\" | \"api\")\n * at daemon startup.\n *\n * Rules:\n * - mode=auto: prefer the `claude` CLI binary on PATH. If absent, fall back\n * to the Agent SDK iff `ANTHROPIC_API_KEY` (or the configured env var) is\n * set. If neither is available, refuse to start.\n * - mode=cli: require the CLI binary on PATH; refuse to start if missing.\n * - mode=api: require the API key env var to be set; refuse to start if\n * missing.\n *\n * The detection is synchronous and cheap: one `which`/`where` invocation and\n * one env lookup. It must be called exactly once, during daemon init, so the\n * resolved mode can be logged prominently.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { platform } from \"node:os\";\nimport type { ExecutionMode, RuntimeMode, WotwConfig } from \"../utils/types.js\";\n\n/** Summary of a successful resolution. */\nexport interface ResolvedExecutionMode {\n /** Concrete runtime to use. */\n mode: RuntimeMode;\n /** Configured mode that produced this result. */\n configuredMode: ExecutionMode;\n /** Absolute path to the `claude` binary, if CLI mode was resolved. */\n cliPath: string | null;\n /** Name of the env var that supplied the API key, if API mode was resolved. */\n apiKeyEnv: string | null;\n /** Model identifier that will be used (cli_model in CLI mode, router in API mode). */\n effectiveModelHint: string;\n /** One-line human-readable summary, suitable for logging or CLI output. */\n description: string;\n}\n\n/**\n * Error thrown when the daemon cannot resolve a usable runtime mode.\n * Intentionally a subclass of Error with a `.code` field so callers can\n * surface a clean exit.\n */\nexport class ExecutionModeError extends Error {\n readonly code: string;\n constructor(message: string, code: string) {\n super(message);\n this.name = \"ExecutionModeError\";\n this.code = code;\n }\n}\n\n/**\n * Look up the absolute path of a binary on PATH. Returns null if not found.\n * Uses `which` on Unix and `where` on Windows. Safe to call on WSL — the\n * Linux `which` is used there since WSL reports `platform() === 'linux'`.\n */\nexport function findOnPath(binary: string): string | null {\n // `command -v` would be slightly more portable on Unix, but it is a shell\n // builtin and spawnSync won't invoke a shell. `which` is universally\n // available on macOS, Linux, and WSL; `where` is the Windows equivalent.\n const prog = platform() === \"win32\" ? \"where\" : \"which\";\n try {\n const result = spawnSync(prog, [binary], { encoding: \"utf8\" });\n if (result.status !== 0) return null;\n const stdout = result.stdout.trim();\n if (!stdout) return null;\n // `where` may list multiple matches; take the first line.\n const first = stdout.split(/\\r?\\n/)[0]?.trim();\n return first && first.length > 0 ? first : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Inspect the environment for an API key under the configured env var name.\n * Returns the var name if set (non-empty), null otherwise.\n */\nexport function findApiKey(envVarName: string): string | null {\n const value = process.env[envVarName];\n if (value && value.trim().length > 0) return envVarName;\n return null;\n}\n\n/**\n * Resolve {@link WotwConfig.execution} into a concrete {@link RuntimeMode}.\n * Throws {@link ExecutionModeError} with a precise reason if neither runtime\n * is available.\n */\nexport function resolveExecutionMode(config: WotwConfig): ResolvedExecutionMode {\n const { mode, cli_path: cliPath, cli_model: cliModel, api_key_env: apiKeyEnv } = config.execution;\n\n const detectCli = (): string | null => findOnPath(cliPath);\n const detectKey = (): string | null => findApiKey(apiKeyEnv);\n\n if (mode === \"cli\") {\n const path = detectCli();\n if (!path) {\n throw new ExecutionModeError(\n `execution.mode is 'cli' but the '${cliPath}' binary was not found on PATH. ` +\n \"Install Claude Code CLI (https://docs.claude.com/claude-code) or set execution.mode to 'api'.\",\n \"CLI_BINARY_NOT_FOUND\",\n );\n }\n return {\n mode: \"cli\",\n configuredMode: \"cli\",\n cliPath: path,\n apiKeyEnv: null,\n effectiveModelHint: cliModel,\n description: `CLI mode: using claude binary at ${path}, model ${cliModel}, zero marginal cost (subscription-covered)`,\n };\n }\n\n if (mode === \"api\") {\n const keyEnv = detectKey();\n if (!keyEnv) {\n throw new ExecutionModeError(\n `execution.mode is 'api' but ${apiKeyEnv} is not set. ` +\n \"Set the env var or change execution.mode to 'cli'/'auto'.\",\n \"API_KEY_NOT_SET\",\n );\n }\n return {\n mode: \"api\",\n configuredMode: \"api\",\n cliPath: null,\n apiKeyEnv: keyEnv,\n effectiveModelHint: `model-router (ingest=${config.models.ingest}, query=${config.models.query})`,\n description: `API mode: using Agent SDK with ${keyEnv}, model routing enabled (per-token billing)`,\n };\n }\n\n // mode === \"auto\"\n // Review item 62: when WOTW_LLM_PROVIDER (config.llm.provider) is\n // set to anything other than \"anthropic\", we MUST NOT silently auto-\n // detect into CLI mode. Pre-fix, an OpenAI / Gemini / Ollama tenant\n // with the claude binary on PATH (always true in the container)\n // would silently dispatch through CLI mode — billing the wrong key\n // for the wrong provider. Honor the explicit llm.provider.\n if (config.llm.provider !== \"anthropic\") {\n const keyEnv2 = detectKey();\n if (!keyEnv2 && config.llm.provider !== \"ollama\") {\n throw new ExecutionModeError(\n `auto-detect: llm.provider='${config.llm.provider}' but ${apiKeyEnv} is not set. ` +\n \"Refusing to fall back to CLI mode for non-anthropic provider.\",\n \"API_KEY_NOT_SET\",\n );\n }\n return {\n mode: \"api\",\n configuredMode: \"auto\",\n cliPath: null,\n apiKeyEnv: keyEnv2,\n effectiveModelHint: `model-router (ingest=${config.models.ingest}, query=${config.models.query})`,\n description: `API mode (auto-detected, provider=${config.llm.provider}): using ${keyEnv2 ?? \"no-key\"}, model routing enabled`,\n };\n }\n const cli = detectCli();\n if (cli) {\n return {\n mode: \"cli\",\n configuredMode: \"auto\",\n cliPath: cli,\n apiKeyEnv: null,\n effectiveModelHint: cliModel,\n description: `CLI mode (auto-detected): using claude binary at ${cli}, model ${cliModel}, zero marginal cost (subscription-covered)`,\n };\n }\n const keyEnv = detectKey();\n if (keyEnv) {\n return {\n mode: \"api\",\n configuredMode: \"auto\",\n cliPath: null,\n apiKeyEnv: keyEnv,\n effectiveModelHint: `model-router (ingest=${config.models.ingest}, query=${config.models.query})`,\n description: `API mode (auto-detected): using Agent SDK with ${keyEnv}, model routing enabled (per-token billing)`,\n };\n }\n throw new ExecutionModeError(\n `No '${cliPath}' binary on PATH and no ${apiKeyEnv} env var set. ` +\n \"Install Claude Code CLI (https://docs.claude.com/claude-code) or set an API key to run watcher-on-the-wall.\",\n \"NO_RUNTIME_AVAILABLE\",\n );\n}\n","/**\n * Daemon main loop. Initializes subsystems, wires up signal handlers, and keeps\n * the event loop alive until a graceful shutdown is requested.\n *\n * In Phase 1 the daemon owns the PID file, logger, and signal handlers. Phases 2-4\n * progressively attach watcher, ingestion, MCP server, and provenance subsystems\n * by calling {@link Daemon.attachSubsystem}.\n */\nimport { loadConfig, resolveConfigPaths } from \"./config.js\";\nimport { acquireStartLock, checkDaemonAlive, removePidFile, writePidFile } from \"./lifecycle.js\";\nimport { getLogger, initLogger } from \"../utils/logger.js\";\nimport type { WotwConfig } from \"../utils/types.js\";\nimport {\n daemonAlreadyRunningError,\n looksLikePermissionDenied,\n wikiDirPermissionError,\n} from \"../utils/actionable-error.js\";\nimport { ensureDirSync } from \"../utils/fs.js\";\n\nfunction ensureDirSyncOrActionable(path: string): void {\n try {\n ensureDirSync(path);\n } catch (err) {\n if (looksLikePermissionDenied(err)) {\n throw wikiDirPermissionError(path, err);\n }\n throw err;\n }\n}\nimport { VERSION } from \"../utils/version.js\";\nimport {\n resolveExecutionMode,\n type ResolvedExecutionMode,\n ExecutionModeError,\n} from \"../ingestion/execution-mode.js\";\n\n/** A daemon subsystem that can start and stop cleanly. */\nexport interface DaemonSubsystem {\n name: string;\n start(): Promise<void>;\n stop(): Promise<void>;\n}\n\nexport interface DaemonOptions {\n configPath: string | null;\n workingDir: string;\n}\n\n/**\n * The Daemon class holds runtime state and coordinates subsystem lifecycle.\n */\nexport class Daemon {\n private readonly subsystems: DaemonSubsystem[] = [];\n private shuttingDown = false;\n private readonly opts: DaemonOptions;\n private config: WotwConfig | null = null;\n private executionMode: ResolvedExecutionMode | null = null;\n private releaseLock: (() => Promise<void>) | null = null;\n\n constructor(opts: DaemonOptions) {\n this.opts = opts;\n }\n\n /** Resolve config and return it. */\n async init(): Promise<WotwConfig> {\n const loaded = await loadConfig(this.opts.workingDir);\n this.config = resolveConfigPaths(loaded.config, this.opts.workingDir);\n\n // Initialize logger against the resolved log file. Foreground runs set\n // WOTW_LOG_STDOUT=1 so logs stream (pretty) to the inherited terminal\n // instead of disappearing into the log file — otherwise `wotw start\n // --foreground` looks silent (PASS-023 dogfood finding #18).\n const logToStdout =\n process.env.WOTW_LOG_STDOUT === \"1\" || process.env.WOTW_LOG_STDOUT === \"true\";\n initLogger(this.config.daemon.log_level, logToStdout ? undefined : this.config.daemon.log_file);\n const log = getLogger(\"daemon\");\n log.info(\n {\n pid: process.pid,\n cwd: this.opts.workingDir,\n configPath: loaded.path,\n },\n \"daemon initializing\",\n );\n\n ensureDirSyncOrActionable(this.config.wiki_root);\n ensureDirSyncOrActionable(this.config.raw_path);\n\n // Resolve execution mode BEFORE starting any subsystem. If neither a\n // claude CLI binary nor an API key is available, refuse to start with a\n // precise error message — downstream code would crash in confusing ways\n // without this guard.\n try {\n this.executionMode = resolveExecutionMode(this.config);\n } catch (err) {\n if (err instanceof ExecutionModeError) {\n log.fatal({ code: err.code }, err.message);\n } else {\n log.fatal({ err }, \"failed to resolve execution mode\");\n }\n throw err;\n }\n // Log the resolved mode prominently so the user always knows which\n // runtime is active and what it costs.\n log.info(\n {\n mode: this.executionMode.mode,\n configured: this.executionMode.configuredMode,\n cliPath: this.executionMode.cliPath,\n apiKeyEnv: this.executionMode.apiKeyEnv,\n model: this.executionMode.effectiveModelHint,\n },\n this.executionMode.description,\n );\n\n return this.config;\n }\n\n /** Return the resolved execution mode, or null if init() hasn't run yet. */\n getExecutionMode(): ResolvedExecutionMode | null {\n return this.executionMode;\n }\n\n /** Attach a subsystem for start/stop management. */\n attachSubsystem(sub: DaemonSubsystem): void {\n this.subsystems.push(sub);\n }\n\n /**\n * Main run loop. Acquires the start lock, writes the PID file, starts all\n * subsystems, installs signal handlers, and blocks until shutdown.\n */\n async run(): Promise<void> {\n if (!this.config) throw new Error(\"Daemon.init() must be called before run()\");\n const log = getLogger(\"daemon\");\n\n // Guard against double-start\n const alive = checkDaemonAlive(this.config.daemon.pid_file);\n if (alive.alive) {\n log.error({ pid: alive.pid }, \"another daemon instance is already running\");\n throw daemonAlreadyRunningError(this.config.daemon.pid_file);\n }\n\n // Acquire lock to prevent simultaneous starts\n try {\n this.releaseLock = await acquireStartLock(this.config.daemon.lock_file);\n } catch (err) {\n log.error({ err }, \"failed to acquire start lock\");\n throw daemonAlreadyRunningError(this.config.daemon.lock_file, err);\n }\n\n // Write PID file\n writePidFile(this.config.daemon.pid_file, {\n pid: process.pid,\n started_at: new Date().toISOString(),\n version: VERSION,\n });\n log.info({ pidFile: this.config.daemon.pid_file }, \"PID file written\");\n\n // Install signal handlers BEFORE starting subsystems so any crash during\n // startup is handled cleanly.\n this.installSignalHandlers();\n\n // Start subsystems in order\n for (const sub of this.subsystems) {\n try {\n log.info({ subsystem: sub.name }, \"starting subsystem\");\n await sub.start();\n } catch (err) {\n log.error({ err, subsystem: sub.name }, \"subsystem failed to start\");\n await this.shutdown(1);\n return;\n }\n }\n\n log.info({ subsystems: this.subsystems.map((s) => s.name) }, \"daemon running\");\n\n // Block until shutdown. The check interval intentionally does NOT unref,\n // so it keeps the event loop alive even when no subsystems are attached.\n // Once this.shuttingDown flips true we clear it and resolve.\n await new Promise<void>((resolve) => {\n const check = setInterval(() => {\n if (this.shuttingDown) {\n clearInterval(check);\n resolve();\n }\n }, 250);\n });\n }\n\n /** Install SIGTERM / SIGINT handlers for graceful shutdown. */\n private installSignalHandlers(): void {\n const handle = (signal: NodeJS.Signals): void => {\n const log = getLogger(\"daemon\");\n log.info({ signal }, \"received shutdown signal\");\n void this.shutdown(0);\n };\n process.on(\"SIGTERM\", handle);\n process.on(\"SIGINT\", handle);\n process.on(\"uncaughtException\", (err) => {\n const log = getLogger(\"daemon\");\n log.fatal({ err }, \"uncaught exception\");\n void this.shutdown(1);\n });\n process.on(\"unhandledRejection\", (reason) => {\n const log = getLogger(\"daemon\");\n log.fatal(\n { reason: reason instanceof Error ? reason.message : String(reason) },\n \"unhandled rejection — shutting down\",\n );\n void this.shutdown(1);\n });\n }\n\n /** Stop all subsystems, release the lock, remove the PID file, and exit. */\n async shutdown(exitCode: number): Promise<void> {\n if (this.shuttingDown) return;\n this.shuttingDown = true;\n const log = getLogger(\"daemon\");\n log.info(\"daemon shutting down\");\n\n // Stop subsystems in reverse order\n for (const sub of [...this.subsystems].reverse()) {\n try {\n await sub.stop();\n log.info({ subsystem: sub.name }, \"subsystem stopped\");\n } catch (err) {\n log.error({ err, subsystem: sub.name }, \"subsystem stop failed\");\n }\n }\n\n // Remove PID file\n if (this.config) {\n removePidFile(this.config.daemon.pid_file);\n }\n\n // Release lock\n if (this.releaseLock) {\n try {\n await this.releaseLock();\n } catch {\n /* ignore */\n }\n }\n\n log.info(\"daemon shutdown complete\");\n // Give pino a tick to flush\n await new Promise((r) => setTimeout(r, 50));\n process.exit(exitCode);\n }\n}\n","/**\n * Hashing utilities used by the provenance chain and other subsystems.\n * Hashes are SHA-256 over canonical JSON (recursively sorted keys, no\n * whitespace, UTF-8). The canonical form is deterministic across machines\n * and language runtimes so verification does not depend on JSON.stringify\n * key-order quirks.\n *\n * This module is the single source of truth for hashing in `wotw` —\n * `src/utils/hash.ts` was previously a second copy with overlapping and\n * subtly-different sync/async signatures for `sha256File`. That duplication\n * has been removed (L-DUP-1). Prefer named exports from this module\n * (`sha256`/`sha256Hex`, `sha256File`, `sha256FileSync`, `canonicalJson`,\n * `sha256Canonical`) throughout the codebase.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/** Special value used as `previous_chain_hash` for the first record in a chain. */\nexport const GENESIS_HASH = \"0\".repeat(64);\n\n/**\n * Produce a canonical JSON string for any JSON-serializable input.\n * Keys in every object are sorted lexicographically, recursively.\n */\nexport function canonicalJson(value: unknown): string {\n const normalize = (v: unknown): unknown => {\n if (v === null || typeof v !== \"object\") return v;\n if (Array.isArray(v)) return v.map(normalize);\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(v as Record<string, unknown>).sort()) {\n sorted[key] = normalize((v as Record<string, unknown>)[key]);\n }\n return sorted;\n };\n return JSON.stringify(normalize(value));\n}\n\n/** SHA-256 of a string or buffer, returned as hex. */\nexport function sha256Hex(input: string | Buffer): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Alias for {@link sha256Hex}. Kept for ergonomic call sites\n * (`sha256(contents)` reads more naturally than `sha256Hex(contents)` in\n * non-provenance code) and for the stable public API in `src/index.ts`.\n */\nexport const sha256 = sha256Hex;\n\n/** SHA-256 of the canonical JSON form of a value. */\nexport function sha256Canonical(value: unknown): string {\n return sha256Hex(canonicalJson(value));\n}\n\n/** Alias for {@link sha256Canonical} — hashes the canonical JSON of a value. */\nexport const sha256Json = sha256Canonical;\n\n/** Alias for {@link canonicalJson} — kept for the stable public API. */\nexport const stableStringify = canonicalJson;\n\n/**\n * Synchronous SHA-256 of a file on disk. Reads the whole file into memory,\n * which is fine for wiki pages. Throws if the file does not exist — callers\n * that need ENOENT tolerance should use the async {@link sha256File} which\n * returns null. Used by the watcher's in-memory event classifier where\n * synchronous semantics simplify the seed flow.\n */\nexport function sha256FileSync(filePath: string): string {\n return sha256Hex(readFileSync(filePath));\n}\n\n/**\n * SHA-256 of a file on disk. Returns null if the file does not exist.\n * Reads the whole file into memory — fine for wiki pages which are small.\n */\nexport async function sha256File(path: string): Promise<string | null> {\n try {\n const buf = await readFile(path);\n return sha256Hex(buf);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/**\n * Hash many files in parallel. Missing files map to null entries.\n * Used when recording the post-ingestion state of the wiki.\n */\nexport async function sha256Files(paths: string[]): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n await Promise.all(\n paths.map(async (p) => {\n const h = await sha256File(p);\n if (h !== null) out[p] = h;\n }),\n );\n return out;\n}\n","/**\n * Content sanitization: strip credentials and PII patterns from text before LLM ingestion.\n *\n * This is a best-effort redaction layer. Users can extend the patterns list via\n * configuration. The goal is to keep secrets out of logs, prompts, and wiki pages.\n */\n\nexport interface RedactionRule {\n name: string;\n pattern: RegExp;\n replacement: string;\n /**\n * Cloud-side rule_id this rule maps to in the PASS-024 `redaction_log`\n * whitelist (`credential_pattern_01..10` + `truncation_32kb`). When set,\n * sanitizeWithEvents() emits a row tagged with this id. When omitted,\n * the redaction still fires but no outbound event is recorded — used\n * for PII rules (credit-card, us-ssn) that stay daemon-local per the\n * cloud's explicit whitelist (FEATURE-PASS-011 rule mapping).\n */\n cloud_rule_id?: string;\n}\n\n/**\n * Default redaction rules. Ordered by likely-to-match first for efficiency.\n */\nexport const DEFAULT_REDACTIONS: readonly RedactionRule[] = [\n {\n name: \"aws-access-key\",\n pattern: /\\bAKIA[0-9A-Z]{16}\\b/g,\n replacement: \"[REDACTED:AWS_ACCESS_KEY]\",\n cloud_rule_id: \"credential_pattern_01\",\n },\n {\n name: \"aws-secret-key\",\n pattern: /\\b[A-Za-z0-9/+=]{40}\\b(?=.*(?:secret|aws))/gi,\n replacement: \"[REDACTED:AWS_SECRET_KEY]\",\n cloud_rule_id: \"credential_pattern_02\",\n },\n {\n name: \"github-token\",\n // Review item 2: also catch GitHub fine-grained personal access\n // tokens (`github_pat_*`, 82+ chars per docs).\n pattern: /\\bgh[pousr]_[A-Za-z0-9]{36,255}\\b|\\bgithub_pat_[A-Za-z0-9_]{50,}\\b/g,\n replacement: \"[REDACTED:GITHUB_TOKEN]\",\n cloud_rule_id: \"credential_pattern_03\",\n },\n {\n name: \"anthropic-api-key\",\n // Anthropic keys span ~95-115 chars after `sk-ant-`. The 80,120\n // window stays generous enough to catch both legacy and current\n // formats including api03- prefix.\n pattern: /\\bsk-ant-[A-Za-z0-9-_]{80,120}\\b/g,\n replacement: \"[REDACTED:ANTHROPIC_API_KEY]\",\n cloud_rule_id: \"credential_pattern_04\",\n },\n {\n name: \"openai-api-key\",\n // Review item 2: original `\\bsk-[A-Za-z0-9]{20,}\\b` missed modern\n // formats with `-` and `_` in the body — `sk-proj-*`,\n // `sk-svcacct-*`, `sk-admin-*` all use `-` separators after the\n // prefix and longer character set. Updated character class.\n pattern: /\\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,200}\\b|\\bsk-[A-Za-z0-9]{20,200}\\b/g,\n replacement: \"[REDACTED:OPENAI_API_KEY]\",\n cloud_rule_id: \"credential_pattern_05\",\n },\n {\n name: \"gemini-api-key\",\n // Google AI Studio API keys come in two formats:\n // - legacy: `AIza` + 35 chars\n // - current: `AQ.` + ~40-80 url-safe chars (rolled out 2024-2025; the\n // `AIza`-only rule silently missed these, leaking new-format keys)\n pattern: /\\bAIza[A-Za-z0-9_-]{35}\\b|\\bAQ\\.[A-Za-z0-9_-]{40,80}\\b/g,\n replacement: \"[REDACTED:GEMINI_API_KEY]\",\n cloud_rule_id: \"credential_pattern_06\",\n },\n {\n name: \"wotw-daemon-token\",\n // Review item 2: daemon tokens emitted by `wotw user add` are\n // `wotw_` + base64url chars. Pre-fix these went unredacted.\n pattern: /\\bwotw_[A-Za-z0-9_-]{30,200}\\b/g,\n replacement: \"[REDACTED:WOTW_TOKEN]\",\n cloud_rule_id: \"credential_pattern_07\",\n },\n {\n name: \"private-key-block\",\n pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,\n replacement: \"[REDACTED:PRIVATE_KEY_BLOCK]\",\n cloud_rule_id: \"credential_pattern_08\",\n },\n {\n name: \"jwt\",\n pattern: /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\b/g,\n replacement: \"[REDACTED:JWT]\",\n cloud_rule_id: \"credential_pattern_09\",\n },\n {\n // L-SEC-3: This pattern is deliberately scoped to full `scheme://`\n // URIs with a `user:password@` userinfo component. The `\\w+:\\/\\/`\n // prefix is load-bearing — without it the pattern would also match\n // bare `user@host` email addresses and `mailto:user@host` URIs,\n // both of which carry no password and must not be over-redacted.\n // Verified by unit tests in test/unit/sanitize.test.ts.\n name: \"password-in-url\",\n pattern: /(\\w+:\\/\\/[^:/\\s]+:)[^@\\s]+(@)/g,\n replacement: \"$1[REDACTED]$2\",\n cloud_rule_id: \"credential_pattern_10\",\n },\n {\n // PII — stays daemon-local. cloud_rule_id intentionally omitted: the\n // PASS-024 cloud whitelist accepts only credential_pattern_01..10 +\n // truncation_32kb, treating PII metadata as data-that-shouldn't-leave-\n // the-daemon. Redaction still fires on-disk; sink emission is skipped.\n name: \"credit-card\",\n pattern: /\\b(?:\\d[ -]*?){13,16}\\b/g,\n replacement: \"[REDACTED:PAN]\",\n },\n {\n // PII — see credit-card note above.\n name: \"us-ssn\",\n pattern: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\n replacement: \"[REDACTED:SSN]\",\n },\n];\n\n/**\n * One captured redaction occurrence — the shape consumed by the\n * RedactionEmitStore. byte_count is the UTF-8 byte length of the\n * material that was replaced (sum across all matches of the rule in\n * one pass over the input).\n */\nexport interface RedactionEvent {\n rule_name: string;\n cloud_rule_id: string;\n byte_count: number;\n}\n\n/**\n * Redact sensitive content from a text blob using the supplied rules (or defaults).\n */\nexport function sanitize(\n input: string,\n rules: readonly RedactionRule[] = DEFAULT_REDACTIONS,\n): string {\n let out = input;\n for (const rule of rules) {\n out = out.replace(rule.pattern, rule.replacement);\n }\n return out;\n}\n\n/**\n * Redact and return the list of rule names that triggered.\n */\nexport function sanitizeWithReport(\n input: string,\n rules: readonly RedactionRule[] = DEFAULT_REDACTIONS,\n): { output: string; triggered: string[] } {\n const triggered: string[] = [];\n let out = input;\n for (const rule of rules) {\n if (rule.pattern.test(out)) {\n triggered.push(rule.name);\n // Reset lastIndex for global regexes before replacing\n rule.pattern.lastIndex = 0;\n out = out.replace(rule.pattern, rule.replacement);\n }\n }\n return { output: out, triggered };\n}\n\n/**\n * Redact + capture per-rule byte counts as RedactionEvents.\n *\n * Rules without a `cloud_rule_id` (PII: credit-card, us-ssn) still apply\n * their redaction but are NOT included in the events array — the cloud\n * whitelist treats those as data-that-shouldn't-leave-the-daemon.\n *\n * byte_count semantics: for each match of a rule's pattern, we add the\n * UTF-8 byte length of the matched substring. This is the size of the\n * material that *was* removed — useful for compliance auditors to see\n * redaction volume per rule per source file.\n *\n * Used by `src/ingestion/prompt-builder.ts` to feed the\n * `pending_redaction_emits` SQLite queue. See FEATURE-PASS-011.\n */\nexport function sanitizeWithEvents(\n input: string,\n rules: readonly RedactionRule[] = DEFAULT_REDACTIONS,\n): { output: string; events: RedactionEvent[] } {\n const events: RedactionEvent[] = [];\n let out = input;\n for (const rule of rules) {\n // Reset lastIndex defensively — global regexes are stateful across\n // matchAll/test/exec calls and this code shares the DEFAULT_REDACTIONS\n // instance with sanitize()/sanitizeWithReport().\n rule.pattern.lastIndex = 0;\n let matchedBytes = 0;\n for (const m of out.matchAll(rule.pattern)) {\n matchedBytes += Buffer.byteLength(m[0], \"utf8\");\n }\n if (matchedBytes === 0) continue;\n rule.pattern.lastIndex = 0;\n out = out.replace(rule.pattern, rule.replacement);\n if (rule.cloud_rule_id) {\n events.push({\n rule_name: rule.name,\n cloud_rule_id: rule.cloud_rule_id,\n byte_count: matchedBytes,\n });\n }\n }\n return { output: out, events };\n}\n"],"mappings":";;;;;AAIA,SAAS,mBAA2C;AACpD,SAAS,SAAS,iBAAiB;AACnC,SAAS,SAAS;;;AC2BX,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAjC3C,OAiC2C;AAAA;AAAA;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAA8B;AACxC,UAAM,QAAkB,CAAC,KAAK,OAAO;AACrC,QAAI,KAAK,YAAY,SAAS,GAAG;AAC/B,YAAM,KAAK,IAAI,cAAc;AAC7B,iBAAW,cAAc,KAAK,aAAa;AACzC,cAAM,KAAK,OAAO,UAAU,EAAE;AAAA,MAChC;AAAA,IACF;AACA,QAAI,KAAK,MAAM;AACb,YAAM,KAAK,IAAI,SAAS,KAAK,IAAI,EAAE;AAAA,IACrC;AACA,UAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAK,OAAO;AACZ,SAAK,OAAO,KAAK;AACjB,SAAK,UAAU,KAAK;AACpB,SAAK,cAAc,KAAK;AACxB,SAAK,OAAO,KAAK;AACjB,QAAI,KAAK,UAAU,QAAW;AAC5B,MAAC,KAA6B,QAAQ,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;AA4BO,SAAS,0BAA0B,GAAqB;AAC7D,QAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACrD,QAAM,OAAO,aAAa,QAAS,EAAwB,OAAO;AAClE,SAAO,SAAS,YAAY,SAAS,WAAW,gBAAgB,KAAK,GAAG;AAC1E;AAJgB;AAqDT,SAAS,iBAAiB,YAAoB,OAAiC;AACpF,SAAO,IAAI,gBAAgB;AAAA,IACzB,MAAM;AAAA,IACN,SAAS,kCAAkC,UAAU,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IAChH,aAAa;AAAA,MACX,qHACE,aACA,+BACA,aACA;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,MAAM;AAAA,IACN;AAAA,EACF,CAAC;AACH;AAjBgB;AAmIT,SAAS,uBAAuB,MAAc,OAAiC;AACpF,SAAO,IAAI,gBAAgB;AAAA,IACzB,MAAM;AAAA,IACN,SAAS,+CAA+C,IAAI;AAAA,IAC5D,aAAa;AAAA,MACX,0DAA0D,IAAI;AAAA,MAC9D,oDAAoD,IAAI;AAAA,MACxD;AAAA,MACA;AAAA,IACF;AAAA,IACA,MAAM;AAAA,IACN;AAAA,EACF,CAAC;AACH;AAbgB;AAqDT,SAAS,0BAA0B,UAAkB,OAAkC;AAC5F,SAAO,IAAI,gBAAgB;AAAA,IACzB,MAAM;AAAA,IACN,SAAS,iDAAiD,QAAQ;AAAA,IAClE,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,MAAM;AAAA,IACN;AAAA,EACF,CAAC;AACH;AAZgB;;;AClUhB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,OAAO,UAAU,QAAQ,iBAAiB;AACnD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY,eAAe;AAC7C,SAAS,kBAAkB;AAKpB,SAAS,WAAW,MAAsB;AAC/C,MAAI,KAAK,WAAW,GAAG,GAAG;AACxB,WAAO,QAAQ,QAAQ,GAAG,KAAK,MAAM,KAAK,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC;AAAA,EACrE;AACA,SAAO;AACT;AALgB;AAUT,SAAS,YAAY,MAAc,MAAuB;AAC/D,QAAM,WAAW,WAAW,IAAI;AAChC,MAAI,WAAW,QAAQ,EAAG,QAAO;AACjC,SAAO,QAAQ,QAAQ,QAAQ,IAAI,GAAG,QAAQ;AAChD;AAJgB;AAST,SAAS,cAAc,KAAmB;AAC/C,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACF;AAJgB;AAiBT,SAAS,gBAAgB,UAAkB,UAAiC;AACjF,gBAAc,QAAQ,QAAQ,CAAC;AAC/B,QAAM,MAAM,GAAG,QAAQ,IAAI,WAAW,CAAC;AACvC,MAAI,UAAU;AACd,MAAI;AACF,kBAAc,KAAK,QAAQ;AAC3B,eAAW,KAAK,QAAQ;AACxB,cAAU;AAAA,EACZ,UAAE;AACA,QAAI,CAAC,SAAS;AACZ,UAAI;AACF,eAAO,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,MAC7B,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAjBgB;AAoET,SAAS,mBAAmB,UAAwB;AACzD,MAAI,WAAW,QAAQ,GAAG;AACxB,WAAO,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,EAClC;AACF;AAJgB;;;AFjHhB,IAAM,cAAc;AAGb,IAAM,gBAAgB;AAAA,EAC3B,UAAU;AAAA,IACR,eAAe,IAAI,QAAQ;AAAA;AAAA,IAC3B,mBAAmB;AAAA,IACnB,qBAAqB,KAAK,QAAQ;AAAA;AAAA,IAClC,0BAA0B,MAAM,QAAQ;AAAA;AAAA,IACxC,uBAAuB;AAAA;AAAA,IACvB,2BAA2B;AAAA,EAC7B;AAAA,EACA,KAAK;AAAA,IACH,eAAe,KAAK,QAAQ;AAAA;AAAA,IAC5B,mBAAmB;AAAA,IACnB,qBAAqB,MAAM,QAAQ;AAAA;AAAA,IACnC,0BAA0B,QAAQ;AAAA;AAAA,IAClC,uBAAuB;AAAA;AAAA,IACvB,2BAA2B;AAAA,EAC7B;AACF;AAKO,SAAS,gBAA4B;AAC1C,SAAO;AAAA,IACL,WAAW;AAAA,IACX,UAAU;AAAA,IACV,KAAK;AAAA,MACH,UAAU;AAAA,MACV,OAAO;AAAA,IACT;AAAA,IACA,WAAW;AAAA,MACT,MAAM;AAAA,MACN,UAAU;AAAA,MACV,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA,SAAS;AAAA,MACP,qBAAqB;AAAA,MACrB,iBAAiB;AAAA,MACjB,wBAAwB;AAAA,MACxB,iBAAiB;AAAA,MACjB,gBAAgB;AAAA,MAChB,iBAAiB,CAAC,cAAc,sBAAsB,gBAAgB,cAAc;AAAA,IACtF;AAAA,IACA,WAAW;AAAA,MACT,WAAW;AAAA,MACX,0BAA0B;AAAA,MAC1B,gBAAgB;AAAA,MAChB,kBAAkB;AAAA,MAClB,SAAS;AAAA,IACX;AAAA,IACA,MAAM;AAAA,MACJ,eAAe;AAAA,MACf,mBAAmB;AAAA,MACnB,oBAAoB;AAAA,MACpB,YAAY;AAAA,IACd;AAAA,IACA,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,MACN,UAAU;AAAA,MACV,WAAW;AAAA,MACX,UAAU;AAAA,MACV,WAAW;AAAA,IACb;AAAA,IACA,aAAa;AAAA,MACX,SAAS;AAAA,MACT,kBAAkB;AAAA,MAClB,sBAAsB;AAAA,IACxB;AAAA,IACA,YAAY;AAAA,MACV,SAAS;AAAA,MACT,YAAY;AAAA;AAAA;AAAA;AAAA,MAIZ,mBAAmB;AAAA,IACrB;AAAA,IACA,YAAY;AAAA,MACV,SAAS;AAAA,MACT,gBAAgB;AAAA,IAClB;AAAA,IACA,OAAO;AAAA,MACL,QAAQ;AAAA,IACV;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,eAAe;AAAA,MACf,oBAAoB;AAAA,IACtB;AAAA,IACA,MAAM;AAAA,MACJ,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,UAAU;AAAA,IACZ;AAAA,IACA,QAAQ;AAAA,MACN,sBAAsB,CAAC,GAAG,IAAI,IAAI,KAAK,GAAG;AAAA,MAC1C,kBAAkB,CAAC,KAAK,IAAI,IAAI,IAAI,IAAI,CAAC;AAAA,MACzC,SAAS;AAAA,QACP,WAAW;AAAA,QACX,qBAAqB;AAAA,QACrB,aAAa;AAAA,QACb,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,MACtB;AAAA,MACA,qBAAqB;AAAA,MACrB,0BAA0B;AAAA,MAC1B,mBAAmB;AAAA,MACnB,uBAAuB;AAAA,MACvB,yBAAyB;AAAA,MACzB,uBAAuB;AAAA,MACvB,oBAAoB;AAAA,MACpB,oBAAoB;AAAA,MACpB,gBAAgB;AAAA,IAClB;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,WAAW;AAAA,MACX,iBAAiB;AAAA,MACjB,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,eAAe,cAAc,IAAI;AAAA,QACjC,mBAAmB,cAAc,IAAI;AAAA,QACrC,qBAAqB,cAAc,IAAI;AAAA,QACvC,0BAA0B,cAAc,IAAI;AAAA,QAC5C,uBAAuB,cAAc,IAAI;AAAA,QACzC,2BAA2B,cAAc,IAAI;AAAA,QAC7C,6BAA6B;AAAA,QAC7B,wBAAwB;AAAA,MAC1B;AAAA,MACA,UAAU;AAAA,MACV,YAAY;AAAA,IACd;AAAA,EACF;AACF;AA5HgB;AAoJhB,eAAsB,WAAW,YAAgD;AAC/E,QAAM,WAAW,YAAY,aAAa;AAAA,IACxC,cAAc;AAAA,MACZ;AAAA,MACA,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,MAKf,GAAG,WAAW;AAAA,MACd,GAAG,WAAW;AAAA,MACd,GAAG,WAAW;AAAA,MACd,GAAG,WAAW;AAAA,MACd,GAAG,WAAW;AAAA,IAChB;AAAA,IACA,SAAS;AAAA,MACP,SAAS,wBAAC,WAAW,YAAY,UAAU,OAAO,GAAzC;AAAA,MACT,QAAQ,wBAAC,WAAW,YAAY,UAAU,OAAO,GAAzC;AAAA,IACV;AAAA,EACF,CAAC;AAED,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,SAAS,OAAO,cAAc,QAAQ,IAAI,CAAC;AAAA,EAC5D,SAAS,KAAK;AAIZ,UAAM,WACJ,eAAe,SAAU,IAA8B,WACjD,IAA8B,WAC/B,cAAc,QAAQ,IAAI;AACjC,UAAM,iBAAiB,UAAU,GAAG;AAAA,EACtC;AACA,QAAM,WAAW,cAAc;AAC/B,MAAI;AACJ,MAAI,OAAsB;AAC1B,MAAI,CAAC,UAAU,CAAC,OAAO,QAAQ;AAE7B,YAAQ;AAAA,MACN;AAAA,IACF;AACA,aAAS;AAAA,EACX,OAAO;AACL,aAAS,YAAY,UAAU,OAAO,MAA6B;AACnE,WAAO,OAAO;AAAA,EAChB;AAGA,QAAM,UAAU,kBAAkB,MAAM;AACxC,QAAM,YAAY,eAAe,OAAO;AACxC,uBAAqB,SAAS;AAC9B,8BAA4B,SAAS;AACrC,SAAO,EAAE,QAAQ,WAAW,KAAK;AACnC;AAzDsB;AA4Ef,SAAS,kBAAkB,QAAgC;AAChE,QAAM,MAAM,gBAAgB,MAAM;AAClC,QAAM,MAAM,QAAQ;AAEpB,MAAI,IAAI,gBAAgB,QAAW;AAKjC,UAAM,IAAI,IAAI,YAAY,KAAK,EAAE,YAAY;AAC7C,QAAI,OAAO,UAAU,MAAM,UAAU,MAAM,OAAO,MAAM,SAAS,MAAM;AAAA,EACzE;AACA,MAAI,IAAI,aAAa,IAAI,UAAU,SAAS,GAAG;AAC7C,QAAI,OAAO,YAAY,IAAI;AAAA,EAC7B;AACA,MAAI,IAAI,aAAa,IAAI,UAAU,SAAS,GAAG;AAC7C,QAAI,YAAY,IAAI;AAKpB,QAAI,IAAI,aAAa,oBAAoB;AACvC,UAAI,WAAW;AAAA,IACjB;AAAA,EACF;AACA,MAAI,IAAI,cAAc,cAAc,IAAI,cAAc,OAAO;AAC3D,QAAI,OAAO,OAAO,IAAI;AAAA,EACxB;AACA,MAAI,IAAI,iBAAiB,IAAI,cAAc,SAAS,GAAG;AACrD,QAAI,OAAO,WAAW,IAAI;AAAA,EAC5B;AACA,MAAI,IAAI,WAAW;AACjB,UAAM,IAAI,OAAO,SAAS,IAAI,WAAW,EAAE;AAC3C,QAAI,OAAO,SAAS,CAAC,KAAK,IAAI,KAAK,IAAI,OAAO;AAC5C,UAAI,OAAO,OAAO;AAAA,IACpB;AAAA,EACF;AACA,MAAI,IAAI,aAAa,IAAI,UAAU,SAAS,GAAG;AAC7C,QAAI,OAAO,OAAO,IAAI;AAAA,EACxB;AACA,MAAI,IAAI,gBAAgB;AACtB,UAAM,MAAM,IAAI;AAChB,QACE,QAAQ,WACR,QAAQ,WACR,QAAQ,UACR,QAAQ,UACR,QAAQ,WACR,QAAQ,SACR;AACA,UAAI,OAAO,YAAY;AAAA,IACzB;AAAA,EACF;AACA,MACE,IAAI,sBAAsB,UAC1B,IAAI,sBAAsB,SAC1B,IAAI,sBAAsB,OAC1B;AACA,QAAI,UAAU,OAAO,IAAI;AAAA,EAC3B;AAIA,MACE,IAAI,sBAAsB,eAC1B,IAAI,sBAAsB,YAC1B,IAAI,sBAAsB,YAC1B,IAAI,sBAAsB,UAC1B;AACA,QAAI,IAAI,WAAW,IAAI;AAKvB,YAAQ,IAAI,mBAAmB;AAAA,MAC7B,KAAK;AACH,YAAI,UAAU,cAAc;AAC5B;AAAA,MACF,KAAK;AACH,YAAI,UAAU,cAAc;AAC5B;AAAA,MACF,KAAK;AACH,YAAI,UAAU,cAAc;AAC5B;AAAA,MACF,KAAK;AAGH;AAAA,IACJ;AAAA,EACF;AACA,MAAI,IAAI,kBAAkB,IAAI,eAAe,SAAS,GAAG;AACvD,QAAI,IAAI,QAAQ,IAAI;AAAA,EACtB;AACA,MAAI,IAAI,mBAAmB,IAAI,gBAAgB,SAAS,GAAG;AACzD,QAAI,IAAI,aAAa,IAAI;AAAA,EAC3B;AAIA,MAAI,IAAI,mBAAmB,IAAI,gBAAgB,SAAS,GAAG;AACzD,QAAI,OAAO,aAAa,IAAI;AAAA,EAC9B,WAAW,IAAI,qBAAqB,IAAI,kBAAkB,SAAS,GAAG;AACpE,QAAI,OAAO,aAAa,IAAI;AAAA,EAC9B;AAMA,MAAI,IAAI,kBAAkB,QAAW;AACnC,QAAI,OAAO,WAAW,IAAI;AAAA,EAC5B,WAAW,IAAI,OAAO,SAAS;AAC7B,QAAI,OAAO,WAAW;AAAA,EACxB;AAaA,MAAI,IAAI,OAAO,SAAS;AACtB,QAAI,UAAU,UAAU;AAWxB,QAAI,KAAK,mBAAmB;AAC5B,QAAI,KAAK,WAAW;AAAA,EACtB;AACA,SAAO;AACT;AA9IgB;AAgJhB,IAAM,aAAa;AAgBZ,SAAS,4BACd,QACA,MAAyB,QAAQ,KAC3B;AACN,MAAI,CAAC,OAAO,OAAO,QAAS;AAC5B,QAAM,SAAS,IAAI;AACnB,MAAI,CAAC,UAAU,OAAO,WAAW,GAAG;AAClC,UAAM,IAAI;AAAA,MACR;AAAA,IAIF;AAAA,EACF;AACF;AAdgB;AA0BT,SAAS,qBAAqB,QAA0B;AAC7D,MAAI,CAAC,OAAO,OAAO,QAAS;AAC5B,MAAI,CAAC,OAAO,OAAO,aAAa,OAAO,OAAO,UAAU,WAAW,GAAG;AACpE,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,WAAW,KAAK,OAAO,OAAO,SAAS,GAAG;AAC7C,UAAM,IAAI;AAAA,MACR,mCAAmC,OAAO,OAAO,SAAS;AAAA,IAC5D;AAAA,EACF;AACA,MAAI,CAAC,OAAO,aAAa,OAAO,UAAU,WAAW,GAAG;AACtD,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AAKA,MAAI,CAAC,OAAO,UAAU,WAAW,GAAG,GAAG;AACrC,UAAM,IAAI;AAAA,MACR,uDAAuD,OAAO,SAAS;AAAA,IAGzE;AAAA,EACF;AACF;AA1BgB;AAgCT,SAAS,YAAY,MAAkB,UAA2C;AACvF,QAAM,MAAkB,gBAAgB,IAAI;AAC5C,QAAM,SAAS,wBAA6B,KAAQ,UAAwC;AAC1F,QAAI,GAAG,IAAI,EAAE,GAAI,IAAI,GAAG,GAAc,GAAI,MAAiB;AAAA,EAC7D,GAFe;AAIf,MAAI,SAAS,cAAc,OAAW,KAAI,YAAY,SAAS;AAC/D,MAAI,SAAS,aAAa,OAAW,KAAI,WAAW,SAAS;AAC7D,MAAI,SAAS,IAAK,QAAO,OAAO,SAAS,GAAG;AAC5C,MAAI,SAAS,UAAW,QAAO,aAAa,SAAS,SAAS;AAC9D,MAAI,SAAS,OAAQ,QAAO,UAAU,SAAS,MAAM;AACrD,MAAI,SAAS,QAAS,QAAO,WAAW,SAAS,OAAO;AACxD,MAAI,SAAS,UAAW,QAAO,aAAa,SAAS,SAAS;AAC9D,MAAI,SAAS,KAAM,QAAO,QAAQ,SAAS,IAAI;AAC/C,MAAI,SAAS,OAAQ,QAAO,UAAU,SAAS,MAAM;AACrD,MAAI,SAAS,OAAQ,QAAO,UAAU,SAAS,MAAM;AACrD,MAAI,SAAS,YAAa,QAAO,eAAe,SAAS,WAAW;AACpE,MAAI,SAAS,WAAY,QAAO,cAAc,SAAS,UAAU;AACjE,MAAI,SAAS,WAAY,QAAO,cAAc,SAAS,UAAU;AACjE,MAAI,SAAS,MAAO,QAAO,SAAS,SAAS,KAAK;AAClD,MAAI,SAAS,gBAAiB,QAAO,mBAAmB,SAAS,eAAe;AAChF,MAAI,SAAS,KAAM,QAAO,QAAQ,SAAS,IAAI;AAC/C,MAAI,SAAS,QAAQ;AAEnB,UAAM,aAAa,IAAI;AACvB,UAAM,iBAAiB,SAAS;AAChC,QAAI,SAAS,EAAE,GAAG,YAAY,GAAG,eAAe;AAChD,QAAI,eAAe,SAAS;AAC1B,UAAI,OAAO,UAAU,EAAE,GAAG,WAAW,SAAS,GAAG,eAAe,QAAQ;AAAA,IAC1E;AAAA,EACF;AACA,MAAI,SAAS,OAAQ,QAAO,UAAU,SAAS,MAAM;AACrD,SAAO;AACT;AAjCgB;AAuChB,IAAM,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAC3C,IAAM,oBAAoB,EAAE,OAAO,EAAE,IAAI,CAAC;AAC1C,IAAM,iBAAiB,EAAE,KAAK,CAAC,SAAS,SAAS,QAAQ,QAAQ,SAAS,OAAO,CAAC;AAClF,IAAM,oBAAoB,EAAE,KAAK,CAAC,aAAa,UAAU,UAAU,QAAQ,CAAC;AAM5E,IAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,KAAK,EAAE,OAAO;AAAA,IACZ,UAAU;AAAA,IACV,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACzC,CAAC;AAAA,EACD,WAAW,EAAE,OAAO;AAAA,IAClB,MAAM,EAAE,KAAK,CAAC,QAAQ,OAAO,KAAK,CAAC;AAAA,IACnC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC1B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC3B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC/B,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IACf,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACxB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACtB,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACjC,CAAC;AAAA,EACD,SAAS,EAAE,OAAO;AAAA,IAChB,qBAAqB;AAAA,IACrB,iBAAiB;AAAA,IACjB,wBAAwB;AAAA,IACxB,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IAC3C,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IAC1C,iBAAiB,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,EACrC,CAAC;AAAA,EACD,WAAW,EAAE,OAAO;AAAA,IAClB,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IACrC,0BAA0B;AAAA,IAC1B,gBAAgB,EAAE,QAAQ;AAAA,IAC1B,kBAAkB,EAAE,OAAO;AAAA,IAC3B,SAAS,EAAE,QAAQ;AAAA,EACrB,CAAC;AAAA,EACD,MAAM,EAAE,OAAO;AAAA,IACb,eAAe;AAAA,IACf,mBAAmB;AAAA,IACnB,oBAAoB;AAAA,IACpB,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC9B,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IACf,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK;AAAA,IACvC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACtB,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,IAChC,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IAC1C,aAAa,EAAE,QAAQ;AAAA,EACzB,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IACf,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC1B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,IAI3B,UAAU,EAAE,OAAO;AAAA,IACnB,WAAW;AAAA,EACb,CAAC;AAAA,EACD,aAAa,EAAE,OAAO;AAAA,IACpB,SAAS,EAAE,QAAQ;AAAA,IACnB,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,IACxC,sBAAsB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACjD,CAAC;AAAA,EACD,YAAY,EAAE,OAAO;AAAA,IACnB,SAAS,EAAE,QAAQ;AAAA,IACnB,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC5B,mBAAmB,EAAE,QAAQ;AAAA,EAC/B,CAAC;AAAA,EACD,YAAY,EAAE,OAAO;AAAA,IACnB,SAAS,EAAE,QAAQ;AAAA,IACnB,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAClC,CAAC;AAAA,EACD,OAAO,EAAE,OAAO;AAAA,IACd,QAAQ,EAAE,QAAQ;AAAA,EACpB,CAAC;AAAA,EACD,iBAAiB,EAAE,OAAO;AAAA,IACxB,SAAS,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,EAAE,QAAQ,MAAM,CAAC,CAAC;AAAA,IACjD,eAAe,EAAE,QAAQ;AAAA,IACzB,oBAAoB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,IACjD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACpC,CAAC;AAAA,EACD,MAAM,EAAE,OAAO;AAAA,IACb,kBAAkB,EAAE,QAAQ;AAAA,IAC5B,gBAAgB;AAAA,IAChB,UAAU,EAAE,QAAQ;AAAA,EACtB,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IACf,SAAS,EAAE,QAAQ;AAAA,IACnB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IAC/B,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IAC3C,QAAQ,EAAE,QAAQ;AAAA,IAClB,MAAM,EAAE,KAAK,CAAC,YAAY,KAAK,CAAC;AAAA,IAChC,QAAQ,EAAE,OAAO;AAAA,MACf,eAAe;AAAA,MACf,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,MAC7C,qBAAqB;AAAA,MACrB,0BAA0B;AAAA,MAC1B,uBAAuB;AAAA,MACvB,2BAA2B,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,MACrD,6BAA6B;AAAA,MAC7B,wBAAwB;AAAA,IAC1B,CAAC;AAAA,IACD,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC1B,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IACf,sBAAsB,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAAA,IACrD,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC;AAAA,IACpD,SAAS,EAAE,OAAO;AAAA,MAChB,WAAW;AAAA,MACX,qBAAqB;AAAA,MACrB,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,oBAAoB;AAAA,IACtB,CAAC;AAAA,IACD,qBAAqB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,IAC9C,0BAA0B,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,IACnD,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,IACzC,uBAAuB,EAAE,QAAQ;AAAA,IACjC,yBAAyB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,IAC/C,uBAAuB,EAAE,QAAQ;AAAA,IACjC,oBAAoB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,IAC3C,oBAAoB,EAAE,QAAQ;AAAA,IAC9B,gBAAgB,EAAE,OAAO;AAAA,EAC3B,CAAC;AACH,CAAC;AAMM,SAAS,eAAe,QAAgC;AAC7D,QAAM,SAAS,iBAAiB,UAAU,MAAM;AAChD,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,QAAQ,OAAO,MAAM,OAAO,CAAC;AACnC,UAAM,OAAO,MAAM,KAAK,KAAK,GAAG;AAChC,UAAM,IAAI,MAAM,kBAAkB,IAAI,KAAK,MAAM,OAAO,EAAE;AAAA,EAC5D;AACA,SAAO,OAAO;AAChB;AARgB;AAcT,SAAS,mBAAmB,QAAoB,SAA8B;AACnF,QAAM,MAAM,gBAAgB,MAAM;AAClC,MAAI,YAAY,YAAY,IAAI,WAAW,OAAO;AAClD,MAAI,WAAW,YAAY,IAAI,UAAU,OAAO;AAChD,MAAI,KAAK,aAAa,YAAY,IAAI,KAAK,YAAY,OAAO;AAC9D,MAAI,OAAO,WAAW,YAAY,IAAI,OAAO,UAAU,OAAO;AAC9D,MAAI,OAAO,YAAY,YAAY,IAAI,OAAO,WAAW,OAAO;AAIhE,MAAI,IAAI,OAAO,SAAS,SAAS,GAAG;AAClC,QAAI,OAAO,WAAW,YAAY,IAAI,OAAO,UAAU,OAAO;AAAA,EAChE;AACA,MAAI,WAAW,iBAAiB,YAAY,IAAI,WAAW,gBAAgB,OAAO;AAIlF,MAAI,WAAW,aAAa,YAAY,IAAI,WAAW,YAAY,IAAI,SAAS;AAGhF,MAAI,IAAI,UAAU,iBAAiB,SAAS,GAAG;AAC7C,QAAI,UAAU,mBAAmB,YAAY,IAAI,UAAU,kBAAkB,IAAI,SAAS;AAAA,EAC5F;AAEA,MAAI,IAAI,OAAO,eAAe,SAAS,GAAG;AACxC,QAAI,OAAO,iBAAiB,YAAY,IAAI,OAAO,gBAAgB,IAAI,SAAS;AAAA,EAClF;AACA,SAAO;AACT;AA5BgB;;;AGtpBhB,SAAS,cAAAA,aAAY,gBAAAC,eAAc,iBAAAC,sBAAqB;AACxD,SAAS,WAAAC,gBAAe;AACxB,OAAO,cAAc;AAYd,SAAS,aAAa,aAAqB,UAAiC;AACjF,gBAAcC,SAAQ,WAAW,CAAC;AAClC,kBAAgB,aAAa,KAAK,UAAU,QAAQ,CAAC;AACvD;AAHgB;AAQT,SAAS,YAAY,aAA6C;AACvE,MAAI,CAACC,YAAW,WAAW,EAAG,QAAO;AACrC,MAAI;AACF,UAAM,MAAMC,cAAa,aAAa,MAAM;AAC5C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QACE,UACA,OAAO,WAAW,YAClB,SAAS,UACT,OAAQ,OAA4B,QAAQ,UAC5C;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAjBgB;AAsBT,SAAS,cAAc,aAA2B;AACvD,qBAAmB,WAAW;AAChC;AAFgB;AAST,SAAS,eAAe,KAAsB;AACnD,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,QAAS,QAAO;AAC7B,QAAI,SAAS,QAAS,QAAO;AAC7B,WAAO;AAAA,EACT;AACF;AAVgB;AAgBT,SAAS,iBAAiB,aAK/B;AACA,QAAM,WAAW,YAAY,WAAW;AACxC,MAAI,CAAC,SAAU,QAAO,EAAE,OAAO,OAAO,KAAK,MAAM,OAAO,OAAO,UAAU,KAAK;AAC9E,QAAM,QAAQ,eAAe,SAAS,GAAG;AACzC,SAAO,EAAE,OAAO,KAAK,SAAS,KAAK,OAAO,CAAC,OAAO,SAAS;AAC7D;AAVgB;AAgBhB,eAAsB,iBAAiB,UAAgD;AACrF,gBAAcF,SAAQ,QAAQ,CAAC;AAE/B,MAAI,CAACC,YAAW,QAAQ,EAAG,CAAAE,eAAc,UAAU,EAAE;AACrD,QAAM,UAAU,MAAM,SAAS,KAAK,UAAU;AAAA,IAC5C,OAAO;AAAA,IACP,SAAS,EAAE,SAAS,EAAE;AAAA,EACxB,CAAC;AACD,SAAO;AACT;AATsB;;;ACzFtB,SAAS,aAAAC,kBAAiB;AAC1B,SAAS,WAAAC,gBAAe;AACxB,OAAO,UAA+C;AAItD,IAAI,aAA4B;AAuBhC,IAAM,eAAe;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWA,SAAS,kBAAkB,KAAuC;AAChE,MAAI,EAAE,eAAe,QAAQ;AAC3B,WAAO,EAAE,SAAS,OAAO,GAAG,EAAE;AAAA,EAChC;AACA,QAAM,MAA+B;AAAA,IACnC,MAAM,IAAI,YAAY;AAAA,IACtB,SAAS,IAAI;AAAA,EACf;AACA,MAAI,IAAI,MAAO,KAAI,QAAQ,IAAI;AAI/B,QAAM,YAAa,IAAsC;AACzD,MAAI,OAAO,cAAc,SAAU,KAAI,OAAO;AAG9C,QAAM,aAAc,IAAuC;AAC3D,MAAI,cAAc,OAAO,eAAe,YAAY,aAAa,YAAY;AAC3E,QAAI,QAAQ;AAAA,MACV,MAAO,WAAkD,aAAa,QAAQ;AAAA,MAC9E,SAAU,WAAoC;AAAA,IAChD;AAAA,EACF;AAOA,SAAO;AACT;AA9BS;AAgCF,SAAS,WAAW,QAAkB,QAAQ,SAA0B;AAC7E,QAAM,UAAyB;AAAA,IAC7B;AAAA,IACA,MAAM,EAAE,KAAK,QAAQ,KAAK,UAAU,OAAU;AAAA,IAC9C,WAAW,KAAK,iBAAiB;AAAA,IACjC,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,IACA,aAAa;AAAA,MACX,KAAK;AAAA,MACL,OAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,SAAS;AACX,IAAAC,WAAUC,SAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AAC/C,iBAAa,KAAK,SAAS,KAAK,YAAY,EAAE,MAAM,SAAS,MAAM,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,EAC1F,OAAO;AACL,iBAAa,KAAK;AAAA,MAChB,GAAG;AAAA,MACH,WAAW;AAAA,QACT,QAAQ;AAAA,QACR,SAAS,EAAE,UAAU,MAAM,eAAe,cAAc,QAAQ,eAAe;AAAA,MACjF;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AA7BgB;AAgChB,IAAI,iBAA0C,CAAC;AAexC,SAAS,UAAU,QAAiB,OAAyC;AAClF,MAAI,CAAC,YAAY;AACf,iBAAa,WAAW,MAAM;AAAA,EAChC;AACA,QAAM,MAAM,EAAE,GAAG,gBAAgB,GAAG,OAAO,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC,EAAG;AACzE,SAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,WAAW,MAAM,GAAG,IAAI;AAC/D;AANgB;;;AC/JhB,SAAS,qBAAqB;AAE9B,IAAMC,WAAU,cAAc,YAAY,GAAG;AAC7C,IAAM,MAAMA,SAAQ,oBAAoB;AAEjC,IAAM,UAAkB,IAAI;;;ACInC,SAAS,iBAAiB;AAC1B,SAAS,gBAAgB;AAwBlB,IAAM,qBAAN,cAAiC,MAAM;AAAA,EA1C9C,OA0C8C;AAAA;AAAA;AAAA,EACnC;AAAA,EACT,YAAY,SAAiB,MAAc;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,WAAW,QAA+B;AAIxD,QAAM,OAAO,SAAS,MAAM,UAAU,UAAU;AAChD,MAAI;AACF,UAAM,SAAS,UAAU,MAAM,CAAC,MAAM,GAAG,EAAE,UAAU,OAAO,CAAC;AAC7D,QAAI,OAAO,WAAW,EAAG,QAAO;AAChC,UAAM,SAAS,OAAO,OAAO,KAAK;AAClC,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,QAAQ,OAAO,MAAM,OAAO,EAAE,CAAC,GAAG,KAAK;AAC7C,WAAO,SAAS,MAAM,SAAS,IAAI,QAAQ;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAhBgB;AAsBT,SAAS,WAAW,YAAmC;AAC5D,QAAM,QAAQ,QAAQ,IAAI,UAAU;AACpC,MAAI,SAAS,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO;AAC7C,SAAO;AACT;AAJgB;AAWT,SAAS,qBAAqB,QAA2C;AAC9E,QAAM,EAAE,MAAM,UAAU,SAAS,WAAW,UAAU,aAAa,UAAU,IAAI,OAAO;AAExF,QAAM,YAAY,6BAAqB,WAAW,OAAO,GAAvC;AAClB,QAAM,YAAY,6BAAqB,WAAW,SAAS,GAAzC;AAElB,MAAI,SAAS,OAAO;AAClB,UAAM,OAAO,UAAU;AACvB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI;AAAA,QACR,oCAAoC,OAAO;AAAA,QAE3C;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,WAAW;AAAA,MACX,oBAAoB;AAAA,MACpB,aAAa,oCAAoC,IAAI,WAAW,QAAQ;AAAA,IAC1E;AAAA,EACF;AAEA,MAAI,SAAS,OAAO;AAClB,UAAMC,UAAS,UAAU;AACzB,QAAI,CAACA,SAAQ;AACX,YAAM,IAAI;AAAA,QACR,+BAA+B,SAAS;AAAA,QAExC;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,WAAWA;AAAA,MACX,oBAAoB,wBAAwB,OAAO,OAAO,MAAM,WAAW,OAAO,OAAO,KAAK;AAAA,MAC9F,aAAa,kCAAkCA,OAAM;AAAA,IACvD;AAAA,EACF;AASA,MAAI,OAAO,IAAI,aAAa,aAAa;AACvC,UAAM,UAAU,UAAU;AAC1B,QAAI,CAAC,WAAW,OAAO,IAAI,aAAa,UAAU;AAChD,YAAM,IAAI;AAAA,QACR,8BAA8B,OAAO,IAAI,QAAQ,SAAS,SAAS;AAAA,QAEnE;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,WAAW;AAAA,MACX,oBAAoB,wBAAwB,OAAO,OAAO,MAAM,WAAW,OAAO,OAAO,KAAK;AAAA,MAC9F,aAAa,qCAAqC,OAAO,IAAI,QAAQ,YAAY,WAAW,QAAQ;AAAA,IACtG;AAAA,EACF;AACA,QAAM,MAAM,UAAU;AACtB,MAAI,KAAK;AACP,WAAO;AAAA,MACL,MAAM;AAAA,MACN,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,WAAW;AAAA,MACX,oBAAoB;AAAA,MACpB,aAAa,oDAAoD,GAAG,WAAW,QAAQ;AAAA,IACzF;AAAA,EACF;AACA,QAAM,SAAS,UAAU;AACzB,MAAI,QAAQ;AACV,WAAO;AAAA,MACL,MAAM;AAAA,MACN,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,WAAW;AAAA,MACX,oBAAoB,wBAAwB,OAAO,OAAO,MAAM,WAAW,OAAO,OAAO,KAAK;AAAA,MAC9F,aAAa,kDAAkD,MAAM;AAAA,IACvE;AAAA,EACF;AACA,QAAM,IAAI;AAAA,IACR,OAAO,OAAO,2BAA2B,SAAS;AAAA,IAElD;AAAA,EACF;AACF;AAhGgB;;;ACtEhB,SAAS,0BAA0B,MAAoB;AACrD,MAAI;AACF,kBAAc,IAAI;AAAA,EACpB,SAAS,KAAK;AACZ,QAAI,0BAA0B,GAAG,GAAG;AAClC,YAAM,uBAAuB,MAAM,GAAG;AAAA,IACxC;AACA,UAAM;AAAA,EACR;AACF;AATS;AAgCF,IAAM,SAAN,MAAa;AAAA,EAnDpB,OAmDoB;AAAA;AAAA;AAAA,EACD,aAAgC,CAAC;AAAA,EAC1C,eAAe;AAAA,EACN;AAAA,EACT,SAA4B;AAAA,EAC5B,gBAA8C;AAAA,EAC9C,cAA4C;AAAA,EAEpD,YAAY,MAAqB;AAC/B,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,OAA4B;AAChC,UAAM,SAAS,MAAM,WAAW,KAAK,KAAK,UAAU;AACpD,SAAK,SAAS,mBAAmB,OAAO,QAAQ,KAAK,KAAK,UAAU;AAMpE,UAAM,cACJ,QAAQ,IAAI,oBAAoB,OAAO,QAAQ,IAAI,oBAAoB;AACzE,eAAW,KAAK,OAAO,OAAO,WAAW,cAAc,SAAY,KAAK,OAAO,OAAO,QAAQ;AAC9F,UAAM,MAAM,UAAU,QAAQ;AAC9B,QAAI;AAAA,MACF;AAAA,QACE,KAAK,QAAQ;AAAA,QACb,KAAK,KAAK,KAAK;AAAA,QACf,YAAY,OAAO;AAAA,MACrB;AAAA,MACA;AAAA,IACF;AAEA,8BAA0B,KAAK,OAAO,SAAS;AAC/C,8BAA0B,KAAK,OAAO,QAAQ;AAM9C,QAAI;AACF,WAAK,gBAAgB,qBAAqB,KAAK,MAAM;AAAA,IACvD,SAAS,KAAK;AACZ,UAAI,eAAe,oBAAoB;AACrC,YAAI,MAAM,EAAE,MAAM,IAAI,KAAK,GAAG,IAAI,OAAO;AAAA,MAC3C,OAAO;AACL,YAAI,MAAM,EAAE,IAAI,GAAG,kCAAkC;AAAA,MACvD;AACA,YAAM;AAAA,IACR;AAGA,QAAI;AAAA,MACF;AAAA,QACE,MAAM,KAAK,cAAc;AAAA,QACzB,YAAY,KAAK,cAAc;AAAA,QAC/B,SAAS,KAAK,cAAc;AAAA,QAC5B,WAAW,KAAK,cAAc;AAAA,QAC9B,OAAO,KAAK,cAAc;AAAA,MAC5B;AAAA,MACA,KAAK,cAAc;AAAA,IACrB;AAEA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,mBAAiD;AAC/C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,gBAAgB,KAA4B;AAC1C,SAAK,WAAW,KAAK,GAAG;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAqB;AACzB,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,2CAA2C;AAC7E,UAAM,MAAM,UAAU,QAAQ;AAG9B,UAAM,QAAQ,iBAAiB,KAAK,OAAO,OAAO,QAAQ;AAC1D,QAAI,MAAM,OAAO;AACf,UAAI,MAAM,EAAE,KAAK,MAAM,IAAI,GAAG,4CAA4C;AAC1E,YAAM,0BAA0B,KAAK,OAAO,OAAO,QAAQ;AAAA,IAC7D;AAGA,QAAI;AACF,WAAK,cAAc,MAAM,iBAAiB,KAAK,OAAO,OAAO,SAAS;AAAA,IACxE,SAAS,KAAK;AACZ,UAAI,MAAM,EAAE,IAAI,GAAG,8BAA8B;AACjD,YAAM,0BAA0B,KAAK,OAAO,OAAO,WAAW,GAAG;AAAA,IACnE;AAGA,iBAAa,KAAK,OAAO,OAAO,UAAU;AAAA,MACxC,KAAK,QAAQ;AAAA,MACb,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACnC,SAAS;AAAA,IACX,CAAC;AACD,QAAI,KAAK,EAAE,SAAS,KAAK,OAAO,OAAO,SAAS,GAAG,kBAAkB;AAIrE,SAAK,sBAAsB;AAG3B,eAAW,OAAO,KAAK,YAAY;AACjC,UAAI;AACF,YAAI,KAAK,EAAE,WAAW,IAAI,KAAK,GAAG,oBAAoB;AACtD,cAAM,IAAI,MAAM;AAAA,MAClB,SAAS,KAAK;AACZ,YAAI,MAAM,EAAE,KAAK,WAAW,IAAI,KAAK,GAAG,2BAA2B;AACnE,cAAM,KAAK,SAAS,CAAC;AACrB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,KAAK,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,gBAAgB;AAK7E,UAAM,IAAI,QAAc,CAACC,aAAY;AACnC,YAAM,QAAQ,YAAY,MAAM;AAC9B,YAAI,KAAK,cAAc;AACrB,wBAAc,KAAK;AACnB,UAAAA,SAAQ;AAAA,QACV;AAAA,MACF,GAAG,GAAG;AAAA,IACR,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,wBAA8B;AACpC,UAAM,SAAS,wBAAC,WAAiC;AAC/C,YAAM,MAAM,UAAU,QAAQ;AAC9B,UAAI,KAAK,EAAE,OAAO,GAAG,0BAA0B;AAC/C,WAAK,KAAK,SAAS,CAAC;AAAA,IACtB,GAJe;AAKf,YAAQ,GAAG,WAAW,MAAM;AAC5B,YAAQ,GAAG,UAAU,MAAM;AAC3B,YAAQ,GAAG,qBAAqB,CAAC,QAAQ;AACvC,YAAM,MAAM,UAAU,QAAQ;AAC9B,UAAI,MAAM,EAAE,IAAI,GAAG,oBAAoB;AACvC,WAAK,KAAK,SAAS,CAAC;AAAA,IACtB,CAAC;AACD,YAAQ,GAAG,sBAAsB,CAAC,WAAW;AAC3C,YAAM,MAAM,UAAU,QAAQ;AAC9B,UAAI;AAAA,QACF,EAAE,QAAQ,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,EAAE;AAAA,QACpE;AAAA,MACF;AACA,WAAK,KAAK,SAAS,CAAC;AAAA,IACtB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,SAAS,UAAiC;AAC9C,QAAI,KAAK,aAAc;AACvB,SAAK,eAAe;AACpB,UAAM,MAAM,UAAU,QAAQ;AAC9B,QAAI,KAAK,sBAAsB;AAG/B,eAAW,OAAO,CAAC,GAAG,KAAK,UAAU,EAAE,QAAQ,GAAG;AAChD,UAAI;AACF,cAAM,IAAI,KAAK;AACf,YAAI,KAAK,EAAE,WAAW,IAAI,KAAK,GAAG,mBAAmB;AAAA,MACvD,SAAS,KAAK;AACZ,YAAI,MAAM,EAAE,KAAK,WAAW,IAAI,KAAK,GAAG,uBAAuB;AAAA,MACjE;AAAA,IACF;AAGA,QAAI,KAAK,QAAQ;AACf,oBAAc,KAAK,OAAO,OAAO,QAAQ;AAAA,IAC3C;AAGA,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,cAAM,KAAK,YAAY;AAAA,MACzB,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,KAAK,0BAA0B;AAEnC,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC1C,YAAQ,KAAK,QAAQ;AAAA,EACvB;AACF;;;AC5OA,SAAS,kBAAkB;AAC3B,SAAS,gBAAAC,qBAAoB;AAC7B,SAAS,YAAAC,iBAAgB;AAGlB,IAAM,eAAe,IAAI,OAAO,EAAE;AAMlC,SAAS,cAAc,OAAwB;AACpD,QAAM,YAAY,wBAAC,MAAwB;AACzC,QAAI,MAAM,QAAQ,OAAO,MAAM,SAAU,QAAO;AAChD,QAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,IAAI,SAAS;AAC5C,UAAM,SAAkC,CAAC;AACzC,eAAW,OAAO,OAAO,KAAK,CAA4B,EAAE,KAAK,GAAG;AAClE,aAAO,GAAG,IAAI,UAAW,EAA8B,GAAG,CAAC;AAAA,IAC7D;AACA,WAAO;AAAA,EACT,GARkB;AASlB,SAAO,KAAK,UAAU,UAAU,KAAK,CAAC;AACxC;AAXgB;AAcT,SAAS,UAAU,OAAgC;AACxD,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AAFgB;AAST,IAAM,SAAS;AAGf,SAAS,gBAAgB,OAAwB;AACtD,SAAO,UAAU,cAAc,KAAK,CAAC;AACvC;AAFgB;AAKT,IAAM,aAAa;AAGnB,IAAM,kBAAkB;AASxB,SAAS,eAAe,UAA0B;AACvD,SAAO,UAAUC,cAAa,QAAQ,CAAC;AACzC;AAFgB;AAQhB,eAAsB,WAAW,MAAsC;AACrE,MAAI;AACF,UAAM,MAAM,MAAMC,UAAS,IAAI;AAC/B,WAAO,UAAU,GAAG;AAAA,EACtB,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,UAAM;AAAA,EACR;AACF;AARsB;AActB,eAAsB,YAAY,OAAkD;AAClF,QAAM,MAA8B,CAAC;AACrC,QAAM,QAAQ;AAAA,IACZ,MAAM,IAAI,OAAO,MAAM;AACrB,YAAM,IAAI,MAAM,WAAW,CAAC;AAC5B,UAAI,MAAM,KAAM,KAAI,CAAC,IAAI;AAAA,IAC3B,CAAC;AAAA,EACH;AACA,SAAO;AACT;AATsB;;;ACjEf,IAAM,qBAA+C;AAAA,EAC1D;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA;AAAA;AAAA,IAGN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA;AAAA;AAAA,IAGN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,eAAe;AAAA,EACjB;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,EACf;AAAA,EACA;AAAA;AAAA,IAEE,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,EACf;AACF;AAiBO,SAAS,SACd,OACA,QAAkC,oBAC1B;AACR,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,QAAQ,KAAK,SAAS,KAAK,WAAW;AAAA,EAClD;AACA,SAAO;AACT;AATgB;AAcT,SAAS,mBACd,OACA,QAAkC,oBACO;AACzC,QAAM,YAAsB,CAAC;AAC7B,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,QAAQ,KAAK,GAAG,GAAG;AAC1B,gBAAU,KAAK,KAAK,IAAI;AAExB,WAAK,QAAQ,YAAY;AACzB,YAAM,IAAI,QAAQ,KAAK,SAAS,KAAK,WAAW;AAAA,IAClD;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,KAAK,UAAU;AAClC;AAfgB;","names":["existsSync","readFileSync","writeFileSync","dirname","dirname","existsSync","readFileSync","writeFileSync","mkdirSync","dirname","mkdirSync","dirname","require","keyEnv","resolve","readFileSync","readFile","readFileSync","readFile"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3030-labs/wotw",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "Self-bootstrapping AI knowledge daemon that turns a folder of raw files into a persistent, compounding LLM wiki with provenance signing, MCP serving, and zero manual maintenance.",
5
5
  "type": "module",
6
6
  "bin": {