codealmanac 0.2.6 → 0.2.7
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/LICENSE +21 -133
- package/README.md +20 -15
- package/dist/{agents-HYRWRHRX.js → agents-V2ZOIACP.js} +6 -5
- package/dist/{chunk-PDFS5VFE.js → chunk-447U3GQJ.js} +5 -17
- package/dist/chunk-447U3GQJ.js.map +1 -0
- package/dist/{chunk-3E7JNMTZ.js → chunk-5BWUMAOX.js} +4 -29
- package/dist/chunk-5BWUMAOX.js.map +1 -0
- package/dist/{chunk-KQUVMF27.js → chunk-BFIG2CXM.js} +2 -516
- package/dist/chunk-BFIG2CXM.js.map +1 -0
- package/dist/{chunk-K2JBCB7R.js → chunk-BQY5L3DL.js} +7 -43
- package/dist/chunk-BQY5L3DL.js.map +1 -0
- package/dist/{chunk-F53U6JQG.js → chunk-CQJVM34R.js} +2 -2
- package/dist/chunk-FUBE6KCO.js +124 -0
- package/dist/chunk-FUBE6KCO.js.map +1 -0
- package/dist/chunk-IZBXXAVL.js +524 -0
- package/dist/chunk-IZBXXAVL.js.map +1 -0
- package/dist/{chunk-7JUX4ADQ.js → chunk-IZT6RBHS.js} +1 -1
- package/dist/{chunk-DW32TL5W.js → chunk-JLQZELHQ.js} +18 -58
- package/dist/chunk-JLQZELHQ.js.map +1 -0
- package/dist/{chunk-2BNDNGUR.js → chunk-KZXWPG4P.js} +4 -8
- package/dist/{chunk-2BNDNGUR.js.map → chunk-KZXWPG4P.js.map} +1 -1
- package/dist/{chunk-GPFVEF6V.js → chunk-QIA22IAM.js} +6 -24
- package/dist/chunk-QIA22IAM.js.map +1 -0
- package/dist/{chunk-J7DNV2DH.js → chunk-RALBM6HZ.js} +43 -355
- package/dist/chunk-RALBM6HZ.js.map +1 -0
- package/dist/{chunk-HJ3WREGP.js → chunk-U5DLLWIC.js} +3 -3
- package/dist/chunk-WL4UE7Q6.js +1386 -0
- package/dist/chunk-WL4UE7Q6.js.map +1 -0
- package/dist/{chunk-VXDPUOQ5.js → chunk-ZUQN5Y3K.js} +129 -382
- package/dist/chunk-ZUQN5Y3K.js.map +1 -0
- package/dist/{chunk-ODJAAJGZ.js → chunk-ZZLLOAI6.js} +3 -3
- package/dist/{cli-MKXCNEMW.js → cli-XWPNARA6.js} +37 -20
- package/dist/cli-XWPNARA6.js.map +1 -0
- package/dist/codealmanac.js +1 -1
- package/dist/{config-F7FKEQ7F.js → config-KH3JUMG6.js} +4 -4
- package/dist/doctor-ENJT665Z.js +18 -0
- package/dist/{hook-4SVX446M.js → hook-2NP3UE7U.js} +2 -4
- package/dist/paths-O5CZADP2.js +14 -0
- package/dist/process-KFSLENL3.js +61 -0
- package/dist/{register-commands-2F6SXLDI.js → register-commands-LULZUSPO.js} +999 -1030
- package/dist/register-commands-LULZUSPO.js.map +1 -0
- package/dist/uninstall-BD4MMQ7M.js +16 -0
- package/dist/uninstall-BD4MMQ7M.js.map +1 -0
- package/dist/update-XSKPDFMJ.js +11 -0
- package/dist/update-XSKPDFMJ.js.map +1 -0
- package/dist/{wiki-IGNRNLUZ.js → wiki-O4RWMAE6.js} +8 -6
- package/dist/wiki-O4RWMAE6.js.map +1 -0
- package/guides/mini.md +8 -6
- package/guides/reference.md +89 -32
- package/hooks/almanac-capture.sh +7 -8
- package/package.json +3 -4
- package/prompts/agents/.gitkeep +1 -0
- package/prompts/base/notability.md +139 -0
- package/prompts/base/purpose.md +85 -0
- package/prompts/base/syntax.md +114 -0
- package/prompts/operations/absorb.md +43 -0
- package/prompts/operations/build.md +49 -0
- package/prompts/operations/garden.md +51 -0
- package/COMMERCIAL.md +0 -9
- package/dist/chunk-3E7JNMTZ.js.map +0 -1
- package/dist/chunk-DW32TL5W.js.map +0 -1
- package/dist/chunk-GPFVEF6V.js.map +0 -1
- package/dist/chunk-J7DNV2DH.js.map +0 -1
- package/dist/chunk-K2JBCB7R.js.map +0 -1
- package/dist/chunk-KQUVMF27.js.map +0 -1
- package/dist/chunk-PDFS5VFE.js.map +0 -1
- package/dist/chunk-VXDPUOQ5.js.map +0 -1
- package/dist/cli-MKXCNEMW.js.map +0 -1
- package/dist/doctor-37UH3HT5.js +0 -17
- package/dist/register-commands-2F6SXLDI.js.map +0 -1
- package/dist/uninstall-C62ZOK32.js +0 -17
- package/dist/update-2UGOFN5C.js +0 -11
- package/dist/wiki-IGNRNLUZ.js.map +0 -1
- package/prompts/bootstrap.md +0 -176
- package/prompts/reviewer.md +0 -129
- package/prompts/writer.md +0 -134
- /package/dist/{agents-HYRWRHRX.js.map → agents-V2ZOIACP.js.map} +0 -0
- /package/dist/{chunk-F53U6JQG.js.map → chunk-CQJVM34R.js.map} +0 -0
- /package/dist/{chunk-7JUX4ADQ.js.map → chunk-IZT6RBHS.js.map} +0 -0
- /package/dist/{chunk-HJ3WREGP.js.map → chunk-U5DLLWIC.js.map} +0 -0
- /package/dist/{chunk-ODJAAJGZ.js.map → chunk-ZZLLOAI6.js.map} +0 -0
- /package/dist/{config-F7FKEQ7F.js.map → config-KH3JUMG6.js.map} +0 -0
- /package/dist/{doctor-37UH3HT5.js.map → doctor-ENJT665Z.js.map} +0 -0
- /package/dist/{hook-4SVX446M.js.map → hook-2NP3UE7U.js.map} +0 -0
- /package/dist/{uninstall-C62ZOK32.js.map → paths-O5CZADP2.js.map} +0 -0
- /package/dist/{update-2UGOFN5C.js.map → process-KFSLENL3.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent/providers/claude/index.ts","../src/agent/providers/claude/auth.ts","../src/agent/providers/cli-status.ts","../src/agent/providers/jsonl-cli.ts","../src/agent/providers/prompt.ts","../src/agent/providers/codex-cli.ts","../src/agent/providers/cursor-cli.ts","../src/agent/providers/status.ts","../src/agent/providers/index.ts","../src/agent/provider-view.ts"],"sourcesContent":["import { query } from \"@anthropic-ai/claude-agent-sdk\";\n\nimport type {\n AgentProvider,\n AgentProviderMetadata,\n AgentResult,\n ProviderModelChoice,\n ProviderStatus,\n RunAgentOptions,\n SpawnCliFn,\n} from \"../../types.js\";\nimport {\n assertClaudeAuth,\n checkClaudeAuth,\n resolveClaudeExecutable,\n UNAUTHENTICATED_MESSAGE,\n type ClaudeAuthStatus,\n} from \"./auth.js\";\n\nexport const DEFAULT_AGENT_MODEL = \"claude-sonnet-4-6\";\n\nconst metadata: AgentProviderMetadata = {\n id: \"claude\",\n displayName: \"Claude\",\n defaultModel: DEFAULT_AGENT_MODEL,\n executable: \"claude\",\n capabilities: {\n transport: \"sdk\",\n writesFiles: true,\n supportsModelOverride: true,\n supportsStreaming: true,\n supportsSessionId: true,\n supportsUsage: false,\n supportsCost: true,\n supportsProviderReportedTurns: true,\n supportsProgrammaticSubagents: true,\n supportsStrictToolAllowlist: false,\n },\n};\n\nexport const claudeProvider: AgentProvider = {\n metadata,\n checkStatus,\n assertReady,\n run,\n modelChoices,\n};\n\nconst CLAUDE_MODELS = [\n {\n value: \"claude-opus-4-7\",\n label: \"Opus 4.7\",\n recommended: false,\n },\n {\n value: \"claude-sonnet-4-6\",\n label: \"Sonnet 4.6\",\n recommended: true,\n },\n {\n value: \"claude-haiku-4-5-20251001\",\n label: \"Haiku 4.5\",\n recommended: false,\n },\n] as const;\n\nfunction modelChoices(args: {\n configuredModel: string | null;\n}): ProviderModelChoice[] {\n const choices: ProviderModelChoice[] = [];\n if (\n args.configuredModel !== null &&\n !CLAUDE_MODELS.some((model) => model.value === args.configuredModel)\n ) {\n choices.push({\n value: args.configuredModel,\n label: args.configuredModel,\n recommended: false,\n source: \"configured\",\n });\n }\n for (const model of CLAUDE_MODELS) {\n choices.push({\n value: model.value,\n label: model.label,\n recommended: model.recommended,\n source: \"catalog\",\n });\n }\n choices.push({\n value: \"__custom__\",\n label: \"Enter a model name\",\n recommended: false,\n source: \"custom\",\n });\n return choices;\n}\n\nasync function run(opts: RunAgentOptions): Promise<AgentResult> {\n const claudeExecutable = resolveClaudeExecutable();\n\n const q = query({\n prompt: opts.prompt,\n options: {\n systemPrompt: opts.systemPrompt,\n allowedTools: opts.allowedTools,\n agents: opts.agents ?? {},\n cwd: opts.cwd,\n model: opts.model ?? metadata.defaultModel ?? undefined,\n maxTurns: opts.maxTurns ?? 100,\n ...(claudeExecutable !== undefined\n ? { pathToClaudeCodeExecutable: claudeExecutable }\n : {}),\n env: {\n ...process.env,\n CODEALMANAC_INTERNAL_SESSION: \"1\",\n },\n includePartialMessages: true,\n },\n });\n\n let cost = 0;\n let turns = 0;\n let result = \"\";\n let sessionId: string | undefined;\n let success = false;\n let errorMsg: string | undefined;\n\n try {\n for await (const msg of q) {\n opts.onMessage?.(msg);\n\n if (\n sessionId === undefined &&\n typeof (msg as { session_id?: unknown }).session_id === \"string\"\n ) {\n sessionId = (msg as { session_id: string }).session_id;\n }\n\n if (msg.type === \"result\") {\n cost = msg.total_cost_usd;\n turns = msg.num_turns;\n if (msg.subtype === \"success\") {\n success = true;\n result = msg.result;\n } else {\n success = false;\n errorMsg =\n (msg.errors?.join(\"; \") ?? \"\") || `agent error: ${msg.subtype}`;\n }\n }\n }\n } catch (err: unknown) {\n errorMsg = err instanceof Error ? err.message : String(err);\n success = false;\n }\n\n return { success, cost, turns, result, sessionId, error: errorMsg };\n}\n\nasync function checkStatus(spawnCli?: SpawnCliFn): Promise<ProviderStatus> {\n let auth: ClaudeAuthStatus = { loggedIn: false };\n try {\n auth = await checkClaudeAuth(spawnCli);\n } catch {\n auth = { loggedIn: false };\n }\n const hasApiKey =\n process.env.ANTHROPIC_API_KEY !== undefined &&\n process.env.ANTHROPIC_API_KEY.length > 0;\n const installed = resolveClaudeExecutable() !== undefined;\n const authenticated = auth.loggedIn || hasApiKey;\n const detail = authenticated\n ? auth.email ?? (hasApiKey ? \"ANTHROPIC_API_KEY set\" : \"logged in\")\n : installed\n ? \"not logged in\"\n : `${metadata.executable} not found on PATH`;\n return { id: metadata.id, installed, authenticated, detail };\n}\n\nasync function assertReady(spawnCli?: SpawnCliFn): Promise<void> {\n await assertClaudeAuth(spawnCli);\n}\n\nexport { assertClaudeAuth, checkClaudeAuth, UNAUTHENTICATED_MESSAGE };\nexport type { ClaudeAuthStatus } from \"./auth.js\";\nexport type { SpawnCliFn, SpawnedProcess } from \"../../types.js\";\n","import { spawn, spawnSync, type ChildProcess } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\nimport type { SpawnCliFn, SpawnedProcess } from \"../../types.js\";\n\n/**\n * Claude auth gate — accepts either an active Claude subscription login\n * OR an `ANTHROPIC_API_KEY` environment variable.\n *\n * Claude Code owns subscription OAuth credentials. Users who are logged in\n * via `claude auth login --claudeai` should be able to run bootstrap/capture\n * without exporting an API key. Conversely, users on pay-per-token API keys\n * shouldn't be required to go through the OAuth flow.\n *\n * Current Claude Agent SDK packages no longer ship the old private\n * `cli.js` entrypoint, so the primary probe is the public Claude Code CLI:\n * `claude auth status --json`. We keep the SDK `cli.js` probe as a legacy\n * fallback for older SDK layouts.\n */\n\nexport interface ClaudeAuthStatus {\n loggedIn: boolean;\n email?: string;\n subscriptionType?: string;\n authMethod?: string;\n}\n\nconst AUTH_TIMEOUT_MS = 10_000;\n\n/**\n * Resolve the installed Claude Code executable from PATH. The Agent SDK can\n * accept this path via `pathToClaudeCodeExecutable`, and the auth probe uses\n * the same binary so CodeAlmanac agrees with `claude auth status`.\n */\nexport function resolveClaudeExecutable(): string | undefined {\n const result = spawnSync(\"sh\", [\"-lc\", \"command -v claude\"], {\n encoding: \"utf8\",\n });\n if (result.status !== 0) return undefined;\n const found = result.stdout.trim().split(\"\\n\")[0]?.trim();\n return found !== undefined && found.length > 0 ? found : undefined;\n}\n\n/**\n * Resolve legacy `cli.js` from older `@anthropic-ai/claude-agent-sdk`\n * installs. SDK 0.2.129+ no longer ships this file; callers must treat\n * failure as expected and fall back to the public `claude` binary.\n */\nfunction resolveCliJsPath(): string {\n const require = createRequire(import.meta.url);\n const entry = require.resolve(\"@anthropic-ai/claude-agent-sdk\");\n return join(dirname(entry), \"cli.js\");\n}\n\n/**\n * Default subprocess spawner for production use — invokes the installed\n * Claude Code CLI.\n */\nexport const defaultSpawnCli: SpawnCliFn = (args: string[]) => {\n const command = resolveClaudeExecutable() ?? \"claude\";\n const child = spawn(command, args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n return child as unknown as SpawnedProcess;\n};\n\nexport const legacySdkSpawnCli: SpawnCliFn = (args: string[]) => {\n const cliPath = resolveCliJsPath();\n const child = spawn(process.execPath, [cliPath, ...args], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n return child as unknown as SpawnedProcess;\n};\n\n/**\n * Check whether the user is authenticated via Claude subscription OAuth.\n *\n * Spawns `claude auth status --json`, falling back to the legacy SDK CLI\n * layout when available. On any failure (spawn error, non-JSON stdout,\n * non-zero exit, timeout) we return `{ loggedIn: false }` rather than\n * propagating the error — the caller will fall back to the\n * `ANTHROPIC_API_KEY` path and, if that's also missing, produce a clean\n * two-option error message.\n *\n * The 10s timeout guards against the CLI hanging on a broken network or\n * keychain prompt. In practice `auth status` is a cheap local read.\n */\nexport async function checkClaudeAuth(\n spawnCli: SpawnCliFn = defaultSpawnCli,\n): Promise<ClaudeAuthStatus> {\n if (spawnCli === defaultSpawnCli) {\n const status = await checkClaudeAuthWith(defaultSpawnCli);\n if (status.loggedIn) return status;\n return await checkClaudeAuthWith(legacySdkSpawnCli);\n }\n return await checkClaudeAuthWith(spawnCli);\n}\n\nasync function checkClaudeAuthWith(\n spawnCli: SpawnCliFn,\n): Promise<ClaudeAuthStatus> {\n let child: SpawnedProcess;\n try {\n child = spawnCli([\"auth\", \"status\", \"--json\"]);\n } catch {\n return { loggedIn: false };\n }\n\n return new Promise<ClaudeAuthStatus>((resolve) => {\n let stdout = \"\";\n let stderr = \"\";\n let settled = false;\n\n const settle = (value: ClaudeAuthStatus): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(value);\n };\n\n const timer = setTimeout(() => {\n try {\n child.kill(\"SIGTERM\");\n } catch {\n // Kill can fail if the process already exited; nothing we can do.\n }\n settle({ loggedIn: false });\n }, AUTH_TIMEOUT_MS);\n\n child.stdout.on(\"data\", (data) => {\n stdout += data.toString();\n });\n child.stderr.on(\"data\", (data) => {\n stderr += data.toString();\n });\n\n child.on(\"error\", () => {\n settle({ loggedIn: false });\n });\n\n child.on(\"close\", (code) => {\n // The SDK writes `{\"loggedIn\": false, ...}` to stdout with a zero\n // exit code when the user isn't signed in, so we only reject on\n // non-zero + empty stdout. An empty stdout with zero exit (shouldn't\n // happen in practice) also fails safely to `loggedIn: false`.\n if (code !== 0 && stdout.trim().length === 0) {\n // `stderr` isn't surfaced to the user here — the caller's error\n // message covers both auth paths — but it would be captured by\n // `stderr` if we ever wanted to log it for debugging.\n void stderr;\n settle({ loggedIn: false });\n return;\n }\n try {\n settle(parseClaudeAuthStatus(stdout.trim()));\n } catch {\n settle({ loggedIn: false });\n }\n });\n });\n}\n\nfunction parseClaudeAuthStatus(raw: string): ClaudeAuthStatus {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const loggedIn = parsed.loggedIn === true;\n const out: ClaudeAuthStatus = { loggedIn };\n if (typeof parsed.email === \"string\") out.email = parsed.email;\n if (typeof parsed.subscriptionType === \"string\") {\n out.subscriptionType = parsed.subscriptionType;\n }\n if (typeof parsed.authMethod === \"string\") {\n out.authMethod = parsed.authMethod;\n }\n return out;\n}\n\n/**\n * Human-readable error when neither auth path is available. The text is\n * deliberately verbose — users hitting this wall for the first time\n * deserve both options in front of them, not a terse hint.\n */\nexport const UNAUTHENTICATED_MESSAGE =\n \"not authenticated to Claude.\\n\\n\" +\n \"Option 1 — use your Claude subscription (Pro/Max):\\n\" +\n \" claude auth login --claudeai\\n\\n\" +\n \"Option 2 — use a pay-per-token API key:\\n\" +\n \" Get one at https://console.anthropic.com\\n\" +\n \" export ANTHROPIC_API_KEY=sk-ant-...\\n\\n\" +\n \"Verify with: claude auth status\";\n\n/**\n * Assert that at least one auth path is satisfied. Prefers subscription\n * auth (fewer surprises for Claude Pro/Max users) but accepts\n * `ANTHROPIC_API_KEY` as a fallback. On failure throws with\n * `code = \"CLAUDE_AUTH_MISSING\"` so callers can distinguish this from\n * other errors if they ever want to.\n *\n * Returns the resolved auth status so callers that want to display the\n * logged-in email in a preamble can do so without a second subprocess.\n */\nexport async function assertClaudeAuth(\n spawnCli: SpawnCliFn = defaultSpawnCli,\n): Promise<ClaudeAuthStatus> {\n const status = await checkClaudeAuth(spawnCli);\n if (status.loggedIn) {\n return status;\n }\n const apiKey = process.env.ANTHROPIC_API_KEY;\n if (apiKey !== undefined && apiKey.length > 0) {\n // Signal to callers that we're on the API-key path. Not \"loggedIn\"\n // in the OAuth sense, but the SDK will pick up the env var and\n // succeed — so we return a status that tells bootstrap/capture the\n // gate is open.\n return { loggedIn: true, authMethod: \"apiKey\" };\n }\n const err = new Error(UNAUTHENTICATED_MESSAGE);\n (err as { code?: string }).code = \"CLAUDE_AUTH_MISSING\";\n throw err;\n}\n\n// Internal re-export — helps keep the public type surface minimal while\n// still letting tests import the `ChildProcess` shape when needed.\nexport type { ChildProcess };\nexport type { SpawnCliFn, SpawnedProcess };\n","import { spawn, spawnSync, type ChildProcess } from \"node:child_process\";\n\nimport type { SpawnCliFn } from \"../types.js\";\n\nconst STATUS_TIMEOUT_MS = 3_000;\n\nexport function commandExists(command: string): boolean {\n const result = spawnSync(\"sh\", [\"-lc\", `command -v ${command}`], {\n encoding: \"utf8\",\n });\n return result.status === 0 && result.stdout.trim().length > 0;\n}\n\nexport function runStatusCommand(\n command: string,\n args: string[],\n): Promise<{ ok: boolean; detail: string }> {\n return new Promise((resolve) => {\n let stdout = \"\";\n let stderr = \"\";\n let child: ChildProcess;\n let settled = false;\n const settle = (value: { ok: boolean; detail: string }): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(value);\n };\n try {\n child = spawn(command, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n resolve({ ok: false, detail: msg });\n return;\n }\n const timer = setTimeout(() => {\n try {\n child.kill(\"SIGTERM\");\n setTimeout(() => {\n if (child.exitCode === null && child.signalCode === null) {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n // already exited\n }\n }\n }, 500).unref();\n } catch {\n // already exited\n }\n settle({ ok: false, detail: `${command} status timed out` });\n }, STATUS_TIMEOUT_MS);\n child.stdout?.on(\"data\", (chunk) => {\n stdout += chunk.toString(\"utf8\");\n });\n child.stderr?.on(\"data\", (chunk) => {\n stderr += chunk.toString(\"utf8\");\n });\n child.on(\"error\", (err) => {\n settle({ ok: false, detail: err.message });\n });\n child.on(\"close\", (code) => {\n const text = `${stdout}\\n${stderr}`.trim();\n settle({\n ok: code === 0,\n detail:\n text\n .split(\"\\n\")\n .find((line) => line.trim().length > 0)\n ?.trim() ?? (code === 0 ? \"ready\" : `${command} exited ${code ?? 1}`),\n });\n });\n });\n}\n\nexport function runInjectedStatusCommand(\n spawnCli: SpawnCliFn,\n args: string[],\n): Promise<{ ok: boolean; detail: string }> {\n return new Promise((resolve) => {\n let stdout = \"\";\n let stderr = \"\";\n let settled = false;\n const settle = (value: { ok: boolean; detail: string }): void => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n try {\n const child = spawnCli(args);\n child.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n child.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n child.on(\"error\", (err) => {\n settle({\n ok: false,\n detail: err instanceof Error ? err.message : String(err),\n });\n });\n child.on(\"close\", (codeOrError) => {\n const code = typeof codeOrError === \"number\" ? codeOrError : 1;\n const text = `${stdout}\\n${stderr}`.trim();\n settle({\n ok: code === 0,\n detail:\n text\n .split(\"\\n\")\n .find((line) => line.trim().length > 0)\n ?.trim() ?? (code === 0 ? \"ready\" : `${args[0] ?? \"command\"} exited ${code}`),\n });\n });\n } catch (err: unknown) {\n settle({\n ok: false,\n detail: err instanceof Error ? err.message : String(err),\n });\n }\n });\n}\n","import { spawn } from \"node:child_process\";\n\nimport type {\n AgentResult,\n AgentStreamMessage,\n AgentUsage,\n} from \"../types.js\";\n\nexport interface JsonlCliOptions {\n command: string;\n args: string[];\n cwd: string;\n env: NodeJS.ProcessEnv;\n onMessage?: (msg: AgentStreamMessage) => void;\n parseFinal: (msg: Record<string, unknown>) => Partial<AgentResult> | null;\n}\n\nexport function runJsonlCli(opts: JsonlCliOptions): Promise<AgentResult> {\n return new Promise((resolve) => {\n const child = spawn(opts.command, opts.args, {\n cwd: opts.cwd,\n env: opts.env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdoutBuf = \"\";\n let stderr = \"\";\n let cost = 0;\n let turns = 0;\n let result = \"\";\n let sessionId: string | undefined;\n let usage: AgentUsage | undefined;\n let success = false;\n let finalSeen = false;\n let error: string | undefined;\n\n const observe = (msg: Record<string, unknown>): void => {\n opts.onMessage?.(msg);\n if (\n sessionId === undefined &&\n typeof msg.session_id === \"string\" &&\n msg.session_id.length > 0\n ) {\n sessionId = msg.session_id;\n }\n if (\n sessionId === undefined &&\n typeof msg.thread_id === \"string\" &&\n msg.thread_id.length > 0\n ) {\n sessionId = msg.thread_id;\n }\n const final = opts.parseFinal(msg);\n if (final === null) return;\n finalSeen = true;\n if (final.cost !== undefined) cost = final.cost;\n if (final.turns !== undefined) turns = final.turns;\n if (final.result !== undefined) result = final.result;\n if (final.sessionId !== undefined) sessionId = final.sessionId;\n if (final.usage !== undefined) usage = final.usage;\n if (final.success !== undefined) success = final.success;\n if (final.error !== undefined) error = final.error;\n };\n\n const flushLines = (): void => {\n let idx = stdoutBuf.indexOf(\"\\n\");\n while (idx !== -1) {\n const rawLine = stdoutBuf.slice(0, idx);\n stdoutBuf = stdoutBuf.slice(idx + 1);\n const line = rawLine.trim();\n if (line.length > 0) {\n try {\n observe(JSON.parse(line) as Record<string, unknown>);\n } catch {\n // Ignore non-JSON chatter; stderr is captured for failures.\n }\n }\n idx = stdoutBuf.indexOf(\"\\n\");\n }\n };\n\n child.stdout.on(\"data\", (chunk) => {\n stdoutBuf += chunk.toString(\"utf8\");\n flushLines();\n });\n child.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString(\"utf8\");\n });\n child.on(\"error\", (err: NodeJS.ErrnoException) => {\n resolve({\n success: false,\n cost,\n turns,\n result,\n sessionId,\n usage,\n error:\n err.code === \"ENOENT\"\n ? `${opts.command} not found on PATH`\n : err.message,\n });\n });\n child.on(\"close\", (code) => {\n flushLines();\n if (stdoutBuf.trim().length > 0) {\n try {\n observe(JSON.parse(stdoutBuf.trim()) as Record<string, unknown>);\n } catch {\n // Ignore trailing non-JSON.\n }\n }\n\n if (code === 0 && finalSeen && success) {\n resolve({ success, cost, turns, result, sessionId, usage });\n return;\n }\n\n const firstStderr = stderr.trim().split(\"\\n\")[0];\n resolve({\n success: false,\n cost,\n turns,\n result,\n sessionId,\n usage,\n error:\n error ??\n (firstStderr !== undefined && firstStderr.length > 0\n ? firstStderr\n : `${opts.command} exited ${code ?? 1}`),\n });\n });\n });\n}\n\nexport function parseUsage(value: unknown): AgentUsage | undefined {\n if (value === null || typeof value !== \"object\") return undefined;\n const obj = value as Record<string, unknown>;\n return {\n inputTokens: numberField(obj, \"input_tokens\") ?? numberField(obj, \"inputTokens\"),\n cachedInputTokens:\n numberField(obj, \"cached_input_tokens\") ??\n numberField(obj, \"cachedInputTokens\") ??\n numberField(obj, \"cacheReadTokens\"),\n outputTokens:\n numberField(obj, \"output_tokens\") ?? numberField(obj, \"outputTokens\"),\n reasoningOutputTokens:\n numberField(obj, \"reasoning_output_tokens\") ??\n numberField(obj, \"reasoningOutputTokens\"),\n };\n}\n\nfunction numberField(\n input: Record<string, unknown>,\n key: string,\n): number | undefined {\n const value = input[key];\n return typeof value === \"number\" ? value : undefined;\n}\n","import type { AgentProviderMetadata, RunAgentOptions } from \"../types.js\";\n\nexport function combinedPrompt(\n opts: RunAgentOptions,\n metadata: AgentProviderMetadata,\n): string {\n const reviewerFallback = buildReviewerFallback(opts, metadata);\n return `${opts.systemPrompt}${reviewerFallback}\\n\\n---\\n\\n${opts.prompt}`;\n}\n\nfunction buildReviewerFallback(\n opts: RunAgentOptions,\n metadata: AgentProviderMetadata,\n): string {\n if (metadata.capabilities.supportsProgrammaticSubagents) return \"\";\n\n const reviewer = opts.agents?.reviewer;\n if (reviewer === undefined) return \"\";\n return (\n \"\\n\\nNon-Claude provider note: this runtime does not receive Claude's \" +\n \"nested Agent tool contract. When the writer prompt asks you to invoke \" +\n \"the reviewer subagent, perform that review pass yourself before final \" +\n \"wiki edits. Treat this reviewer prompt as read-only review guidance:\\n\\n\" +\n reviewer.prompt\n );\n}\n","import type {\n AgentProvider,\n AgentProviderMetadata,\n AgentResult,\n ProviderModelChoice,\n ProviderStatus,\n RunAgentOptions,\n SpawnCliFn,\n} from \"../types.js\";\nimport {\n commandExists,\n runInjectedStatusCommand,\n runStatusCommand,\n} from \"./cli-status.js\";\nimport { parseUsage, runJsonlCli } from \"./jsonl-cli.js\";\nimport { combinedPrompt } from \"./prompt.js\";\n\nconst metadata: AgentProviderMetadata = {\n id: \"codex\",\n displayName: \"Codex\",\n defaultModel: null,\n executable: \"codex\",\n capabilities: {\n transport: \"cli-jsonl\",\n writesFiles: true,\n supportsModelOverride: true,\n supportsStreaming: true,\n supportsSessionId: false,\n supportsUsage: true,\n supportsCost: false,\n supportsProviderReportedTurns: false,\n supportsProgrammaticSubagents: false,\n supportsStrictToolAllowlist: false,\n },\n};\n\nexport const codexProvider: AgentProvider = {\n metadata,\n checkStatus,\n assertReady,\n run,\n modelChoices,\n};\n\ninterface CodexCatalogModel {\n slug: string;\n displayName: string;\n}\n\nconst CODEX_MODEL_ORDER = [\n \"gpt-5.5\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.3-codex\",\n] as const;\n\nconst CODEX_MODEL_LABELS: Record<string, string> = {\n \"gpt-5.5\": \"GPT-5.5\",\n \"gpt-5.4\": \"GPT-5.4\",\n \"gpt-5.4-mini\": \"GPT-5.4 Mini\",\n \"gpt-5.3-codex\": \"GPT-5.3 Codex\",\n};\n\nasync function modelChoices(args: {\n configuredModel: string | null;\n spawnCli?: SpawnCliFn;\n}): Promise<ProviderModelChoice[]> {\n const catalog = await readCodexModelCatalog(args.spawnCli);\n const choices: ProviderModelChoice[] = [];\n if (args.configuredModel !== null) {\n choices.push({\n value: args.configuredModel,\n label: modelLabel(args.configuredModel, catalog),\n recommended: false,\n source: \"configured\",\n });\n }\n for (const slug of CODEX_MODEL_ORDER) {\n if (choices.some((choice) => choice.value === slug)) continue;\n if (catalog !== undefined && !catalog.some((model) => model.slug === slug)) {\n continue;\n }\n choices.push({\n value: slug,\n label: modelLabel(slug, catalog),\n recommended: slug === \"gpt-5.4\",\n source: \"catalog\",\n });\n }\n choices.push({\n value: \"__custom__\",\n label: \"Enter a model name\",\n recommended: false,\n source: \"custom\",\n });\n return choices;\n}\n\nasync function readCodexModelCatalog(\n spawnCli?: SpawnCliFn,\n): Promise<CodexCatalogModel[] | undefined> {\n if (spawnCli === undefined) return undefined;\n try {\n const result = await collectSpawn(spawnCli([\"codex\", \"debug\", \"models\"]));\n if (result.code !== 0) return undefined;\n const parsed = JSON.parse(result.stdout) as unknown;\n if (parsed === null || typeof parsed !== \"object\") return undefined;\n const models = (parsed as { models?: unknown }).models;\n if (!Array.isArray(models)) return undefined;\n const out: CodexCatalogModel[] = [];\n for (const model of models) {\n if (model === null || typeof model !== \"object\") continue;\n const record = model as Record<string, unknown>;\n if (record.visibility !== \"list\") continue;\n if (typeof record.slug !== \"string\") continue;\n out.push({\n slug: record.slug,\n displayName: typeof record.display_name === \"string\"\n ? record.display_name\n : record.slug,\n });\n }\n return out;\n } catch {\n return undefined;\n }\n}\n\nfunction collectSpawn(\n child: ReturnType<SpawnCliFn>,\n): Promise<{ stdout: string; code: number }> {\n return new Promise((resolve) => {\n let stdout = \"\";\n let settled = false;\n const settle = (code: number): void => {\n if (settled) return;\n settled = true;\n resolve({ stdout, code });\n };\n child.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n child.on(\"error\", () => {\n settle(1);\n });\n child.on(\"close\", (codeOrError) => {\n settle(typeof codeOrError === \"number\" ? codeOrError ?? 1 : 1);\n });\n });\n}\n\nfunction modelLabel(\n slug: string,\n catalog: CodexCatalogModel[] | undefined,\n): string {\n return CODEX_MODEL_LABELS[slug] ??\n catalog?.find((model) => model.slug === slug)?.displayName ??\n slug;\n}\n\nasync function run(opts: RunAgentOptions): Promise<AgentResult> {\n const args = [\n \"exec\",\n \"--json\",\n \"--sandbox\",\n \"workspace-write\",\n \"--skip-git-repo-check\",\n \"-C\",\n opts.cwd,\n ];\n if (opts.model !== undefined && opts.model.length > 0) {\n args.push(\"--model\", opts.model);\n }\n args.push(combinedPrompt({ ...opts, provider: \"codex\" }, metadata));\n\n return await runJsonlCli({\n command: metadata.executable,\n args,\n cwd: opts.cwd,\n env: { ...process.env, CODEALMANAC_INTERNAL_SESSION: \"1\" },\n onMessage: opts.onMessage,\n parseFinal: parseCodexFinal,\n });\n}\n\nasync function checkStatus(spawnCli?: SpawnCliFn): Promise<ProviderStatus> {\n if (spawnCli === undefined && !commandExists(metadata.executable)) {\n return {\n id: metadata.id,\n installed: false,\n authenticated: false,\n detail: `${metadata.executable} not found on PATH`,\n };\n }\n\n const auth = spawnCli !== undefined\n ? await runInjectedStatusCommand(spawnCli, [\n metadata.executable,\n \"login\",\n \"status\",\n ])\n : await runStatusCommand(metadata.executable, [\"login\", \"status\"]);\n return {\n id: metadata.id,\n installed: true,\n authenticated: auth.ok,\n detail: auth.detail,\n };\n}\n\nasync function assertReady(spawnCli?: SpawnCliFn): Promise<void> {\n const status = await checkStatus(spawnCli);\n if (!status.installed || !status.authenticated) {\n const err = new Error(`${status.id} not ready: ${status.detail}`);\n (err as { code?: string }).code = \"AGENT_AUTH_MISSING\";\n throw err;\n }\n}\n\nfunction parseCodexFinal(\n msg: Record<string, unknown>,\n): Partial<AgentResult> | null {\n if (msg.type === \"item.completed\") {\n const item = msg.item;\n if (item !== null && typeof item === \"object\") {\n const obj = item as Record<string, unknown>;\n if (obj.type === \"agent_message\" && typeof obj.text === \"string\") {\n return { result: obj.text };\n }\n }\n return null;\n }\n if (msg.type === \"turn.completed\") {\n return { success: true, turns: 1, usage: parseUsage(msg.usage) };\n }\n if (msg.type === \"turn.failed\" || msg.type === \"error\") {\n return {\n success: false,\n error:\n typeof msg.message === \"string\"\n ? msg.message\n : typeof msg.error === \"string\"\n ? msg.error\n : \"codex turn failed\",\n };\n }\n return null;\n}\n","import type {\n AgentProvider,\n AgentProviderMetadata,\n AgentResult,\n ProviderStatus,\n RunAgentOptions,\n SpawnCliFn,\n} from \"../types.js\";\nimport {\n commandExists,\n runInjectedStatusCommand,\n runStatusCommand,\n} from \"./cli-status.js\";\nimport { parseUsage, runJsonlCli } from \"./jsonl-cli.js\";\nimport { combinedPrompt } from \"./prompt.js\";\n\nconst metadata: AgentProviderMetadata = {\n id: \"cursor\",\n displayName: \"Cursor\",\n defaultModel: null,\n executable: \"cursor-agent\",\n capabilities: {\n transport: \"cli-jsonl\",\n writesFiles: true,\n supportsModelOverride: true,\n supportsStreaming: true,\n supportsSessionId: true,\n supportsUsage: true,\n supportsCost: false,\n supportsProviderReportedTurns: false,\n supportsProgrammaticSubagents: false,\n supportsStrictToolAllowlist: false,\n },\n};\n\nexport const cursorProvider: AgentProvider = {\n metadata,\n checkStatus,\n assertReady,\n run,\n};\n\nasync function run(opts: RunAgentOptions): Promise<AgentResult> {\n const args = [\n \"--print\",\n \"--output-format\",\n \"stream-json\",\n \"--stream-partial-output\",\n \"--trust\",\n \"--workspace\",\n opts.cwd,\n ];\n if (opts.model !== undefined && opts.model.length > 0) {\n args.push(\"--model\", opts.model);\n }\n args.push(combinedPrompt({ ...opts, provider: \"cursor\" }, metadata));\n\n return await runJsonlCli({\n command: metadata.executable,\n args,\n cwd: opts.cwd,\n env: { ...process.env, CODEALMANAC_INTERNAL_SESSION: \"1\" },\n onMessage: opts.onMessage,\n parseFinal: parseCursorFinal,\n });\n}\n\nasync function checkStatus(spawnCli?: SpawnCliFn): Promise<ProviderStatus> {\n if (spawnCli === undefined && !commandExists(metadata.executable)) {\n return {\n id: metadata.id,\n installed: false,\n authenticated: false,\n detail: `${metadata.executable} not found on PATH`,\n };\n }\n\n const auth = spawnCli !== undefined\n ? await runInjectedStatusCommand(spawnCli, [\n metadata.executable,\n \"status\",\n ])\n : await runStatusCommand(metadata.executable, [\"status\"]);\n return {\n id: metadata.id,\n installed: true,\n authenticated: auth.ok,\n detail: auth.detail,\n };\n}\n\nasync function assertReady(spawnCli?: SpawnCliFn): Promise<void> {\n const status = await checkStatus(spawnCli);\n if (!status.installed || !status.authenticated) {\n const err = new Error(`${status.id} not ready: ${status.detail}`);\n (err as { code?: string }).code = \"AGENT_AUTH_MISSING\";\n throw err;\n }\n}\n\nfunction parseCursorFinal(\n msg: Record<string, unknown>,\n): Partial<AgentResult> | null {\n if (msg.type !== \"result\") return null;\n const isError = msg.is_error === true || msg.subtype !== \"success\";\n return {\n success: !isError,\n turns: 1,\n result: typeof msg.result === \"string\" ? msg.result : \"\",\n sessionId:\n typeof msg.session_id === \"string\" ? msg.session_id : undefined,\n usage: parseUsage(msg.usage),\n error: isError\n ? typeof msg.result === \"string\"\n ? msg.result\n : `cursor result: ${String(msg.subtype ?? \"error\")}`\n : undefined,\n };\n}\n","import {\n getEnabledAgentProviderIds,\n type AgentProviderId,\n} from \"../../update/config.js\";\nimport type { ProviderStatus, SpawnCliFn } from \"../types.js\";\nimport { getAgentProvider } from \"./index.js\";\n\nexport async function assertAgentAuth(args: {\n provider: AgentProviderId;\n spawnCli?: SpawnCliFn;\n}): Promise<void> {\n await getAgentProvider(args.provider).assertReady(args.spawnCli);\n}\n\nexport async function listProviderStatuses(\n spawnCli?: SpawnCliFn,\n): Promise<ProviderStatus[]> {\n const out: ProviderStatus[] = [];\n for (const id of getEnabledAgentProviderIds()) {\n out.push(await getAgentProvider(id).checkStatus(spawnCli));\n }\n return out;\n}\n","import type { AgentProvider } from \"../types.js\";\nimport { claudeProvider, DEFAULT_AGENT_MODEL } from \"./claude/index.js\";\nimport { codexProvider } from \"./codex-cli.js\";\nimport { cursorProvider } from \"./cursor-cli.js\";\n\nconst AGENT_PROVIDERS = {\n claude: claudeProvider,\n codex: codexProvider,\n cursor: cursorProvider,\n} satisfies Record<string, AgentProvider>;\n\nexport function getAgentProvider(id: keyof typeof AGENT_PROVIDERS): AgentProvider {\n return AGENT_PROVIDERS[id];\n}\n\nexport const AGENT_PROVIDER_METADATA = {\n claude: claudeProvider.metadata,\n codex: codexProvider.metadata,\n cursor: cursorProvider.metadata,\n};\nexport { DEFAULT_AGENT_MODEL };\nexport { assertAgentAuth, listProviderStatuses } from \"./status.js\";\n","import {\n AGENT_PROVIDER_METADATA,\n getAgentProvider,\n listProviderStatuses,\n} from \"./providers.js\";\nimport type {\n ProviderModelChoice,\n ProviderStatus,\n SpawnCliFn,\n} from \"./types.js\";\nimport {\n formatEnabledAgentProviderList,\n getEnabledAgentProviderIds,\n isAgentProviderId,\n readConfig,\n type AgentProviderId,\n type GlobalConfig,\n} from \"../update/config.js\";\n\nexport type ProviderReadiness = \"ready\" | \"not-authenticated\" | \"missing\";\n\nexport interface ProviderSetupChoice {\n id: AgentProviderId;\n label: string;\n selected: boolean;\n recommended: boolean;\n readiness: ProviderReadiness;\n ready: boolean;\n installed: boolean;\n authenticated: boolean;\n effectiveModel: string | null;\n providerDefaultModel: string | null;\n configuredModel: string | null;\n account: string | null;\n detail: string;\n fixCommand: string | null;\n modelChoices: ProviderModelChoice[];\n}\n\nexport interface ProviderSetupView {\n defaultProvider: AgentProviderId;\n recommendedProvider: AgentProviderId;\n choices: ProviderSetupChoice[];\n}\n\nexport interface ProviderViewOptions {\n config?: GlobalConfig;\n statuses?: ProviderStatus[];\n spawnCli?: SpawnCliFn;\n}\n\nconst LOGIN_FIXES: Record<AgentProviderId, string> = {\n claude: \"run: claude auth login --claudeai\",\n codex: \"run: codex login\",\n cursor: \"run: cursor-agent login\",\n};\n\nconst INSTALL_FIXES: Record<AgentProviderId, string> = {\n claude: \"install Claude Code, then run: claude auth login --claudeai\",\n codex: \"install Codex CLI, then run: codex login\",\n cursor: \"install cursor-agent, then run: cursor-agent login\",\n};\n\nexport function getProviderLabel(id: AgentProviderId): string {\n return AGENT_PROVIDER_METADATA[id].displayName;\n}\n\nexport function getProviderDefaultModel(id: AgentProviderId): string | null {\n return AGENT_PROVIDER_METADATA[id].defaultModel;\n}\n\nexport async function buildProviderSetupView(\n opts: ProviderViewOptions = {},\n): Promise<ProviderSetupView> {\n const config = opts.config ?? await readConfig();\n const statuses = opts.statuses ?? await listProviderStatuses(opts.spawnCli);\n const statusById = new Map(statuses.map((status) => [status.id, status]));\n const recommendedProvider = chooseRecommendedProvider(statuses);\n const choices: ProviderSetupChoice[] = [];\n for (const id of getEnabledAgentProviderIds()) {\n const status = statusById.get(id) ?? missingStatus(id);\n const readiness = getReadiness(status);\n const configuredModel = normalizeModel(config.agent.models[id]);\n const providerDefaultModel = getProviderDefaultModel(id);\n const effectiveModel = configuredModel ?? providerDefaultModel;\n choices.push({\n id,\n label: getProviderLabel(id),\n selected: id === config.agent.default,\n recommended: id === recommendedProvider,\n readiness,\n ready: readiness === \"ready\",\n installed: status.installed,\n authenticated: status.authenticated,\n effectiveModel,\n providerDefaultModel,\n configuredModel,\n account: status.authenticated ? accountFromDetail(status.detail) : null,\n detail: status.detail,\n fixCommand: fixFor(id, readiness),\n modelChoices: await buildProviderModelChoices(id, configuredModel, {\n spawnCli: opts.spawnCli,\n }),\n });\n }\n return {\n defaultProvider: config.agent.default,\n recommendedProvider,\n choices,\n };\n}\n\nexport function buildProviderModelChoices(\n id: AgentProviderId,\n configuredModel: string | null = null,\n opts: { spawnCli?: SpawnCliFn } = {},\n): Promise<ProviderModelChoice[]> | ProviderModelChoice[] {\n const provider = getAgentProvider(id);\n if (provider.modelChoices !== undefined) {\n return provider.modelChoices({ configuredModel, spawnCli: opts.spawnCli });\n }\n const choices: ProviderModelChoice[] = [];\n if (configuredModel !== null) {\n choices.push({\n value: configuredModel,\n label: configuredModel,\n recommended: false,\n source: \"configured\",\n });\n }\n\n const providerDefault = getProviderDefaultModel(id);\n if (providerDefault !== null) {\n if (!choices.some((choice) => choice.value === providerDefault)) {\n choices.push({\n value: providerDefault,\n label: providerDefault,\n recommended: true,\n source: \"provider-default\",\n });\n } else {\n choices[0] = { ...choices[0]!, recommended: true };\n }\n } else {\n choices.push({\n value: null,\n label: \"provider default\",\n recommended: true,\n source: \"provider-default\",\n });\n }\n\n choices.push({\n value: \"__custom__\",\n label: \"Enter a model name\",\n recommended: false,\n source: \"custom\",\n });\n return choices;\n}\n\nexport function chooseRecommendedProvider(\n statuses: ProviderStatus[],\n): AgentProviderId {\n const ready = statuses\n .filter((status) => getReadiness(status) === \"ready\")\n .map((status) => status.id);\n if (ready.includes(\"claude\")) return \"claude\";\n for (const id of getEnabledAgentProviderIds()) {\n if (ready.includes(id)) return id;\n }\n return \"claude\";\n}\n\nexport function parseAgentSelection(value: string): {\n provider: AgentProviderId | null;\n model?: string;\n} {\n const [rawProvider, ...modelParts] = value.split(\"/\");\n if (rawProvider === undefined || !isAgentProviderId(rawProvider)) {\n return { provider: null };\n }\n const model = modelParts.join(\"/\");\n return {\n provider: rawProvider,\n model: model.length > 0 ? model : undefined,\n };\n}\n\nfunction getReadiness(status: ProviderStatus): ProviderReadiness {\n if (!status.installed) return \"missing\";\n if (/not (logged|signed) in|not authenticated/i.test(status.detail)) {\n return \"not-authenticated\";\n }\n if (!status.authenticated) return \"not-authenticated\";\n return \"ready\";\n}\n\nfunction fixFor(\n id: AgentProviderId,\n readiness: ProviderReadiness,\n): string | null {\n if (readiness === \"ready\") return null;\n if (readiness === \"missing\") return INSTALL_FIXES[id];\n return LOGIN_FIXES[id];\n}\n\nfunction accountFromDetail(detail: string): string | null {\n const clean = detail.trim();\n if (\n clean.length === 0 ||\n clean === \"ready\" ||\n clean === \"logged in\" ||\n clean === \"ANTHROPIC_API_KEY set\"\n ) {\n return null;\n }\n return clean;\n}\n\nfunction normalizeModel(value: string | null | undefined): string | null {\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\nfunction missingStatus(id: AgentProviderId): ProviderStatus {\n return {\n id,\n installed: false,\n authenticated: false,\n detail: \"provider status unavailable\",\n };\n}\n\nexport function enabledProviderListForMessage(): string {\n return formatEnabledAgentProviderList();\n}\n"],"mappings":"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","names":["require","spawn","spawnSync","spawn","metadata","metadata","checkStatus","assertReady","run","modelChoices","metadata","checkStatus","assertReady","run"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/uninstall.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nimport { runHookUninstall } from \"./hook.js\";\nimport {\n CODEX_INSTRUCTIONS_END,\n CODEX_INSTRUCTIONS_START,\n IMPORT_LINE,\n} from \"./setup.js\";\n\n/**\n * `almanac uninstall` — the reverse of `setup`.\n *\n * Idempotent and order-insensitive: each step is a no-op if that\n * artifact was never installed. We remove exactly the things setup added,\n * nothing else:\n *\n * 1. The `@~/.claude/codealmanac.md` line from `~/.claude/CLAUDE.md`.\n * Other content stays untouched. If removing our line leaves the\n * file empty, we delete the file so our fingerprint doesn't persist\n * as zero bytes.\n * 2. The guide files `~/.claude/codealmanac.md` and\n * `~/.claude/codealmanac-reference.md`.\n * 3. The managed codealmanac block from Codex's global AGENTS file.\n * 4. The SessionEnd hook entry (delegated to `runHookUninstall`, which\n * already knows how to leave foreign entries alone).\n *\n * Flags:\n * --yes skip confirmations; remove everything\n * --keep-hook leave the hook alone\n * --keep-guides leave the guides + CLAUDE.md import alone\n *\n * Non-interactive (no TTY) → behaves as if `--yes` was passed. Same\n * contract as `setup`.\n */\n\nexport interface UninstallOptions {\n yes?: boolean;\n keepHook?: boolean;\n keepGuides?: boolean;\n\n // ─── Injection points ────────────────────────────────────────────\n settingsPath?: string;\n hookScriptPath?: string;\n claudeDir?: string;\n codexDir?: string;\n isTTY?: boolean;\n stdout?: NodeJS.WritableStream;\n}\n\nexport interface UninstallResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst BLUE = \"\\x1b[38;5;75m\";\nconst DIM = \"\\x1b[2m\";\nconst RST = \"\\x1b[0m\";\n\nexport async function runUninstall(\n options: UninstallOptions = {},\n): Promise<UninstallResult> {\n const out = options.stdout ?? process.stdout;\n const isTTY =\n options.isTTY ?? (process.stdin.isTTY === true);\n const interactive = isTTY && options.yes !== true;\n const claudeDir = options.claudeDir ?? path.join(homedir(), \".claude\");\n const codexDir = options.codexDir ?? path.join(homedir(), \".codex\");\n\n out.write(\"\\n\");\n\n // Hook removal.\n let removeHook = true;\n if (options.keepHook === true) {\n removeHook = false;\n } else if (interactive) {\n removeHook = await confirm(\n out,\n \"Remove the SessionEnd hook from ~/.claude/settings.json?\",\n true,\n );\n }\n if (removeHook) {\n const res = await runHookUninstall({\n settingsPath: options.settingsPath,\n hookScriptPath: options.hookScriptPath,\n });\n if (res.exitCode !== 0) {\n return { stdout: \"\", stderr: res.stderr, exitCode: res.exitCode };\n }\n out.write(` ${BLUE}\\u25c7${RST} ${res.stdout.trim()}\\n`);\n } else {\n out.write(` ${DIM}\\u25cb Hook kept${RST}\\n`);\n }\n\n // Guide + import removal.\n let removeGuides = true;\n if (options.keepGuides === true) {\n removeGuides = false;\n } else if (interactive) {\n removeGuides = await confirm(\n out,\n \"Remove agent instructions?\",\n true,\n );\n }\n if (removeGuides) {\n const summary = await removeGuideFiles(claudeDir, codexDir);\n if (summary.anyChanges) {\n out.write(\n ` ${BLUE}\\u25c7${RST} Guides removed (${summary.filesTouched.join(\", \")})\\n`,\n );\n } else {\n out.write(` ${DIM}\\u25cb Guides not installed${RST}\\n`);\n }\n } else {\n out.write(` ${DIM}\\u25cb Guides kept${RST}\\n`);\n }\n\n out.write(`\\n ${BLUE}\\u25c7${RST} ${BLUE}Uninstall complete${RST}\\n\\n`);\n\n return { stdout: \"\", stderr: \"\", exitCode: 0 };\n}\n\ninterface RemoveGuidesResult {\n anyChanges: boolean;\n filesTouched: string[];\n}\n\nasync function removeGuideFiles(\n claudeDir: string,\n codexDir: string,\n): Promise<RemoveGuidesResult> {\n const touched: string[] = [];\n\n const mini = path.join(claudeDir, \"codealmanac.md\");\n const ref = path.join(claudeDir, \"codealmanac-reference.md\");\n const claudeMd = path.join(claudeDir, \"CLAUDE.md\");\n\n if (existsSync(mini)) {\n await rm(mini, { force: true });\n touched.push(\"codealmanac.md\");\n }\n if (existsSync(ref)) {\n await rm(ref, { force: true });\n touched.push(\"codealmanac-reference.md\");\n }\n\n if (existsSync(claudeMd)) {\n const existing = await readFile(claudeMd, \"utf8\");\n const { changed, body } = removeImportLine(existing);\n if (changed) {\n // If the file is now content-free, delete it outright so our\n // installation leaves no trace. A user who was using CLAUDE.md\n // before we touched it still has their content; only the case\n // where CLAUDE.md contained nothing but our line gets cleaned up.\n if (body.trim().length === 0) {\n await rm(claudeMd, { force: true });\n touched.push(\"CLAUDE.md (deleted)\");\n } else {\n await writeFile(claudeMd, body, \"utf8\");\n touched.push(\"CLAUDE.md\");\n }\n }\n }\n\n for (const agentsFile of [\n path.join(codexDir, \"AGENTS.md\"),\n path.join(codexDir, \"AGENTS.override.md\"),\n ]) {\n if (!existsSync(agentsFile)) continue;\n const existing = await readFile(agentsFile, \"utf8\");\n const { changed, body } = removeManagedBlock(\n existing,\n CODEX_INSTRUCTIONS_START,\n CODEX_INSTRUCTIONS_END,\n );\n if (!changed) continue;\n if (body.trim().length === 0) {\n await rm(agentsFile, { force: true });\n touched.push(`${path.basename(agentsFile)} (deleted)`);\n } else {\n await writeFile(agentsFile, body, \"utf8\");\n touched.push(path.basename(agentsFile));\n }\n }\n\n return { anyChanges: touched.length > 0, filesTouched: touched };\n}\n\n/**\n * Remove the import line from a CLAUDE.md body. Match is line-anchored\n * (trimmed equality) so we don't munge a line that happens to include\n * the token as part of a longer string. Returns the unchanged body (and\n * `changed: false`) if the line isn't present — this is what makes the\n * command safe to run repeatedly.\n */\nexport function removeImportLine(contents: string): {\n changed: boolean;\n body: string;\n} {\n const eol = contents.includes(\"\\r\\n\") ? \"\\r\\n\" : \"\\n\";\n const lines = contents.split(/\\r?\\n/);\n\n const indices: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i]!.trim() === IMPORT_LINE) indices.push(i);\n }\n if (indices.length === 0) return { changed: false, body: contents };\n\n // Remove the line(s). Iterate from the end so earlier indices stay\n // valid as we splice.\n for (let i = indices.length - 1; i >= 0; i--) {\n lines.splice(indices[i]!, 1);\n }\n\n let body = lines.join(eol);\n\n // Cleanup: collapse any double-blank that our removal created at the\n // spot the line used to live. A best-effort tidy — we don't try to\n // normalize the whole file.\n body = body.replace(/\\n\\n\\n+/g, \"\\n\\n\");\n\n return { changed: true, body };\n}\n\nexport function removeManagedBlock(\n contents: string,\n start: string,\n end: string,\n): { changed: boolean; body: string } {\n const startIndex = contents.indexOf(start);\n const endIndex = contents.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {\n return { changed: false, body: contents };\n }\n\n const afterEnd = endIndex + end.length;\n let body = `${contents.slice(0, startIndex)}${contents.slice(afterEnd)}`;\n body = body.replace(/\\n\\n\\n+/g, \"\\n\\n\");\n body = body.replace(/^\\n+/, \"\");\n return { changed: true, body };\n}\n\nfunction confirm(\n out: NodeJS.WritableStream,\n question: string,\n defaultYes: boolean,\n): Promise<boolean> {\n return new Promise((resolve) => {\n const hint = defaultYes ? \"[Y/n]\" : \"[y/N]\";\n out.write(` ${BLUE}\\u25c6${RST} ${question} ${DIM}${hint}${RST} `);\n\n let buf = \"\";\n const onData = (chunk: Buffer): void => {\n buf += chunk.toString(\"utf8\");\n const nl = buf.indexOf(\"\\n\");\n if (nl === -1) return;\n process.stdin.removeListener(\"data\", onData);\n process.stdin.pause();\n\n const answer = buf.slice(0, nl).trim().toLowerCase();\n const accepted =\n answer.length === 0\n ? defaultYes\n : answer === \"y\" || answer === \"yes\";\n resolve(accepted);\n };\n\n process.stdin.resume();\n process.stdin.on(\"data\", onData);\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/indexer/schema.ts","../src/indexer/index.ts","../src/slug.ts","../src/topics/yaml.ts","../src/indexer/frontmatter.ts","../src/indexer/freshness.ts","../src/indexer/paths.ts","../src/indexer/topics-yaml.ts","../src/indexer/wikilinks.ts","../src/registry/index.ts","../src/commands/health.ts","../src/indexer/duration.ts","../src/indexer/resolve-wiki.ts","../src/topics/dag.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\n\n/**\n * Schema DDL, applied on every open. All statements are `CREATE ... IF NOT\n * EXISTS` so this is idempotent — handy when the file already exists but\n * was written by an older version, and tolerable because the schema is\n * append-only (new tables don't collide).\n *\n * Departures from the raw spec, explained:\n * - `page_topics.topic_slug` has no FK to `topics(slug)`. Topics are\n * created lazily when a page declares them; a strict FK would force us\n * to upsert topic rows before the page rows, which doesn't buy us\n * anything in slice 2 and locks us out of slice 3's \"no explicit topic\n * registration needed\" behavior.\n * - `wikilinks.target_slug` / `cross_wiki_links.target_slug` also have\n * no FK — these can be intentionally broken (unwritten target page),\n * and `almanac health` will surface them in slice 3.\n *\n * `file_refs` carries TWO forms of each path:\n * - `path` — normalized + lowercased, used for GLOB/equality\n * queries (`--mentions`). Stable across casing\n * choices on macOS/Windows.\n * - `original_path` — as-written (normalized slashes, no `./`, trailing\n * `/` for dirs), preserving the author's casing.\n * Used for filesystem stats (dead-refs on\n * case-sensitive filesystems like Linux) and for\n * user-facing display (`almanac info`).\n *\n * See also: `SCHEMA_VERSION` below and the migration logic in `openIndex`.\n */\nconst SCHEMA_DDL = `\nCREATE TABLE IF NOT EXISTS pages (\n slug TEXT PRIMARY KEY,\n title TEXT,\n file_path TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n superseded_by TEXT\n);\n\nCREATE TABLE IF NOT EXISTS topics (\n slug TEXT PRIMARY KEY,\n title TEXT,\n description TEXT\n);\n\nCREATE TABLE IF NOT EXISTS page_topics (\n page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n topic_slug TEXT NOT NULL,\n PRIMARY KEY (page_slug, topic_slug)\n);\n\nCREATE TABLE IF NOT EXISTS topic_parents (\n child_slug TEXT NOT NULL,\n parent_slug TEXT NOT NULL,\n PRIMARY KEY (child_slug, parent_slug),\n CHECK (child_slug != parent_slug)\n);\n\nCREATE TABLE IF NOT EXISTS file_refs (\n page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n path TEXT NOT NULL,\n original_path TEXT NOT NULL,\n is_dir INTEGER NOT NULL,\n PRIMARY KEY (page_slug, path)\n);\nCREATE INDEX IF NOT EXISTS idx_file_refs_path ON file_refs(path);\n\nCREATE TABLE IF NOT EXISTS wikilinks (\n source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n target_slug TEXT NOT NULL,\n PRIMARY KEY (source_slug, target_slug)\n);\n\nCREATE TABLE IF NOT EXISTS cross_wiki_links (\n source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n target_wiki TEXT NOT NULL,\n target_slug TEXT NOT NULL,\n PRIMARY KEY (source_slug, target_wiki, target_slug)\n);\n\n-- NOTE: virtual FTS5 table — ON DELETE CASCADE from pages does NOT apply.\n-- The indexer must explicitly DELETE FROM fts_pages whenever it removes\n-- or replaces a page row, or we leak orphaned FTS rows.\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(slug, title, content);\n`;\n\n/**\n * Bump this whenever the schema changes in a backwards-incompatible way.\n * On open we compare the stored `user_version` against this constant; if\n * it's lower, we drop the affected tables so the next `runIndexer` can\n * rebuild them. Full reindex is cheap (everything lives on disk as\n * markdown), so \"drop + recreate\" is simpler than ALTER TABLE migrations.\n *\n * Version history:\n * 1 — initial slice-2 schema\n * 2 — slice-3-review: added `file_refs.original_path`\n */\nconst SCHEMA_VERSION = 2;\n\n/**\n * Open `index.db` and apply the schema. Foreign keys are off by default in\n * SQLite; we turn them on per-connection so the ON DELETE CASCADE on\n * `pages` actually fires when we delete stale rows during incremental\n * reindex.\n *\n * We don't wrap this open in a transaction — `CREATE ... IF NOT EXISTS` is\n * safe to run repeatedly and the FTS5 virtual-table creation is already\n * atomic.\n *\n * Migration: if the DB was created by an older schema (`user_version` <\n * `SCHEMA_VERSION`), we drop the tables whose shape changed and let the\n * CREATE IF NOT EXISTS below rebuild them. The next `runIndexer` repopulates\n * from the filesystem — cheap and avoids the ALTER TABLE dance.\n */\nexport function openIndex(dbPath: string): Database.Database {\n const db = new Database(dbPath);\n // WAL journal mode is persistent — once set, it's recorded in the DB\n // header and survives close/open cycles. Check first and only switch if\n // we're not already there; this avoids a redundant pragma write on every\n // query command.\n const mode = db.pragma(\"journal_mode\", { simple: true });\n if (typeof mode !== \"string\" || mode.toLowerCase() !== \"wal\") {\n db.pragma(\"journal_mode = WAL\");\n }\n db.pragma(\"foreign_keys = ON\");\n\n const rawVersion = db.pragma(\"user_version\", { simple: true });\n const currentVersion = typeof rawVersion === \"number\" ? rawVersion : 0;\n if (currentVersion < SCHEMA_VERSION) {\n // Drop tables whose shape changed. `file_refs` got `original_path`\n // as of v2; easiest to drop it entirely so CREATE IF NOT EXISTS\n // runs with the new definition. Pages/topics/links are untouched.\n db.exec(\"DROP TABLE IF EXISTS file_refs\");\n // The indexer's fast-path skips pages whose content_hash matches,\n // which means a migration-dropped `file_refs` wouldn't get\n // repopulated until a page changed. Clear the hash column so the\n // next reindex treats every page as changed and rebuilds its\n // file_refs/wikilinks/cross-wiki rows. Table may not exist yet on\n // a brand-new DB, so swallow errors.\n try {\n db.exec(\"UPDATE pages SET content_hash = ''\");\n } catch {\n // pages table didn't exist yet; the upcoming CREATE IF NOT EXISTS\n // takes care of a fresh install.\n }\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n }\n\n db.exec(SCHEMA_DDL);\n return db;\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { readFile, utimes } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { toKebabCase } from \"../slug.js\";\nimport { titleCase } from \"../topics/yaml.js\";\nimport { firstH1, parseFrontmatter } from \"./frontmatter.js\";\nimport {\n PAGES_GLOB,\n pagesNewerThan,\n topicsYamlNewerThan,\n} from \"./freshness.js\";\nimport {\n normalizePath,\n normalizePathPreservingCase,\n looksLikeDir,\n} from \"./paths.js\";\nimport { openIndex } from \"./schema.js\";\nimport { applyTopicsYaml, TOPICS_YAML_FILENAME } from \"./topics-yaml.js\";\nimport { extractWikilinks } from \"./wikilinks.js\";\n\nexport interface IndexContext {\n /** Absolute path to the repo root (the dir containing `.almanac/`). */\n repoRoot: string;\n}\n\nexport interface IndexResult {\n /** Pages parsed or re-parsed during this run. Zero when the DB was already up to date. */\n changed: number;\n /** Pages present in the DB before this run but missing from disk. */\n removed: number;\n /**\n * Pages on disk at the end of this run — i.e. files that made it all the\n * way through to the index. Skipped files (slug collisions, unreadable,\n * un-sluggable filenames) are NOT counted here. Use `filesSeen` for the\n * raw count of `.md` files encountered on disk.\n *\n * Alias retained for backwards-compat with existing tests/consumers; new\n * code should prefer `pagesIndexed` for clarity.\n */\n total: number;\n /** Pages that made it into the index. Same number as `total`. */\n pagesIndexed: number;\n /**\n * Count of `.md` files found under `pages/` before any filtering. Always\n * `>= pagesIndexed`; the difference is `filesSkipped`.\n */\n filesSeen: number;\n /**\n * Files dropped before making it into the index — slug collisions,\n * un-sluggable filenames, or filesystem races (deleted/unreadable mid-run).\n * Covered by stderr warnings when non-zero.\n */\n filesSkipped: number;\n}\n\n/**\n * The \"front door\" for query commands. Runs the indexer only if the DB is\n * missing or at least one page is newer than it. Meant to be cheap — the\n * common case is \"nothing changed, mtime check returns fast, we're done\".\n *\n * The spec is explicit: \"Reindex is implicit and invisible. If the user\n * didn't didn't explicitly run `reindex`, they shouldn't see reindex\n * output. Silent by default.\" So this function never writes to stdout;\n * warnings (slug collisions, bad frontmatter) still go to stderr.\n */\nexport async function ensureFreshIndex(ctx: IndexContext): Promise<IndexResult> {\n const almanacDir = join(ctx.repoRoot, \".almanac\");\n const dbPath = join(almanacDir, \"index.db\");\n const pagesDir = join(almanacDir, \"pages\");\n\n if (!existsSync(pagesDir)) {\n // No pages dir = nothing to index. Open/create the DB so downstream\n // queries can run against an empty schema rather than crashing on a\n // missing file.\n const db = openIndex(dbPath);\n db.close();\n return emptyResult();\n }\n\n if (\n !existsSync(dbPath) ||\n // Keep read-side freshness even when CLI/agent write paths eagerly\n // reindex: users can still change `.almanac/pages/` directly via\n // manual edits, git pulls, merges, or branch switches.\n pagesNewerThan(pagesDir, dbPath) ||\n topicsYamlNewerThan(almanacDir, dbPath)\n ) {\n return runIndexer(ctx);\n }\n return emptyResult();\n}\n\nfunction emptyResult(): IndexResult {\n return {\n changed: 0,\n removed: 0,\n total: 0,\n pagesIndexed: 0,\n filesSeen: 0,\n filesSkipped: 0,\n };\n}\n\n/**\n * Force a full reindex. Identical to `ensureFreshIndex` except it runs\n * the indexer unconditionally. Exposed for `almanac reindex`.\n */\nexport async function runIndexer(ctx: IndexContext): Promise<IndexResult> {\n const almanacDir = join(ctx.repoRoot, \".almanac\");\n const dbPath = join(almanacDir, \"index.db\");\n const pagesDir = join(almanacDir, \"pages\");\n\n const db = openIndex(dbPath);\n let result: IndexResult;\n try {\n result = await indexPagesInto(db, pagesDir);\n // After pages are indexed, reconcile the topics table against\n // `.almanac/topics.yaml` (if present). `indexPagesInto` has already\n // lazily inserted rows for every topic slug mentioned in page\n // frontmatter with a title-cased title; `applyTopicsYaml` now\n // promotes the declared title/description and rewrites parent edges\n // for those topics that live in the file.\n await applyTopicsYaml(db, join(almanacDir, TOPICS_YAML_FILENAME));\n } finally {\n db.close();\n }\n\n // Bump the DB mtime to \"now\" after a successful reindex (even a no-op\n // one). Otherwise, a page file with a future mtime (clock skew,\n // `git checkout` preserving source mtimes) would trigger `ensureFreshIndex`\n // on every query: the freshness check sees `page.mtime > db.mtime`,\n // reindex runs, finds no content-hash changes, and the DB mtime stays\n // stale — locking us into a reindex-on-every-query loop. Touching the\n // DB mtime makes the comparison monotonic.\n try {\n const now = new Date();\n await utimes(dbPath, now, now);\n } catch {\n // Touching mtime is a freshness optimization; failures here are\n // non-fatal and the reindex result is still correct.\n }\n return result;\n}\n\ninterface ExistingRow {\n slug: string;\n content_hash: string;\n file_path: string;\n}\n\nasync function indexPagesInto(\n db: Database.Database,\n pagesDir: string,\n): Promise<IndexResult> {\n const files = await fg(PAGES_GLOB, {\n cwd: pagesDir,\n absolute: false,\n onlyFiles: true,\n caseSensitiveMatch: true,\n });\n\n // Load the current state of the index into memory so we can diff against\n // what's on disk. This is cheap even at 10k pages (one INTEGER + two\n // short strings per row).\n const existingRows = db\n .prepare<[], ExistingRow>(\"SELECT slug, content_hash, file_path FROM pages\")\n .all();\n const existingBySlug = new Map<string, ExistingRow>();\n for (const row of existingRows) existingBySlug.set(row.slug, row);\n\n // First pass: decide what to do with each file on disk. We record the\n // intent here so the transaction below can run synchronously — mixing\n // async file reads into a better-sqlite3 transaction doesn't work\n // (transactions are sync).\n const planned: Array<{\n slug: string;\n title: string;\n filePath: string;\n fullPath: string;\n contentHash: string;\n updatedAt: number;\n archivedAt: number | null;\n supersededBy: string | null;\n topics: string[];\n frontmatterFiles: string[];\n wikilinks: ReturnType<typeof extractWikilinks>;\n content: string;\n }> = [];\n const seenSlugs = new Set<string>();\n let filesSkipped = 0;\n\n for (const rel of files) {\n const fullPath = join(pagesDir, rel);\n const base = basename(rel, \".md\");\n const slug = toKebabCase(base);\n if (slug.length === 0) {\n process.stderr.write(\n `almanac: skipping \"${rel}\" — filename has no slug-able characters\\n`,\n );\n filesSkipped++;\n continue;\n }\n if (slug !== base) {\n // Filename isn't already canonical kebab-case. Warn, but still\n // index under the canonical slug. `almanac health` (slice 3) will\n // surface these as a proper report.\n process.stderr.write(\n `almanac: warning — \"${rel}\" is not canonical; indexed as slug \"${slug}\"\\n`,\n );\n }\n if (seenSlugs.has(slug)) {\n // Two files slugify to the same slug. Keep the first, skip the\n // rest — health will flag this properly in slice 3.\n process.stderr.write(\n `almanac: warning — slug \"${slug}\" collides with an earlier file; skipping \"${rel}\"\\n`,\n );\n filesSkipped++;\n continue;\n }\n\n // `fast-glob` gave us the list in one shot, but by the time we stat\n // and read each file it can have been deleted, renamed, or swapped\n // (editors that save via rename-swap expose this briefly). A single\n // such race shouldn't tank the whole reindex — matches the malformed-\n // YAML behavior (\"one bad file doesn't stop the others\"). We narrow\n // to ENOENT/EACCES so genuine I/O failures (EIO, EMFILE, etc.) still\n // surface.\n let st: ReturnType<typeof statSync>;\n let raw: string;\n try {\n st = statSync(fullPath);\n raw = await readFile(fullPath, \"utf8\");\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n (err.code === \"ENOENT\" || err.code === \"EACCES\")\n ) {\n process.stderr.write(\n `almanac: skipping \"${rel}\" — ${err.message}\\n`,\n );\n filesSkipped++;\n continue;\n }\n throw err;\n }\n\n seenSlugs.add(slug);\n const updatedAt = Math.floor(st.mtimeMs / 1000);\n\n // Content-hash skip: if the hash matches what's in the DB and the\n // file path hasn't moved, we can leave this page's rows alone. This\n // is the fast-path for \"user ran a query; one page was touched\".\n const contentHash = hashContent(raw);\n const existing = existingBySlug.get(slug);\n if (\n existing !== undefined &&\n existing.content_hash === contentHash &&\n existing.file_path === fullPath\n ) {\n continue;\n }\n\n const fm = parseFrontmatter(raw);\n const title = fm.title ?? firstH1(fm.body) ?? base;\n const links = extractWikilinks(fm.body);\n\n planned.push({\n slug,\n title,\n filePath: rel,\n fullPath,\n contentHash,\n updatedAt,\n archivedAt: fm.archived_at,\n supersededBy: fm.superseded_by,\n topics: fm.topics,\n frontmatterFiles: fm.files,\n wikilinks: links,\n content: fm.body,\n });\n }\n\n // Compute deletions: anything in the DB whose slug isn't on disk\n // anymore (or whose file slugifies to a different slug now).\n const toDelete: string[] = [];\n for (const slug of existingBySlug.keys()) {\n if (!seenSlugs.has(slug)) toDelete.push(slug);\n }\n\n const deleteByPage = db.prepare<[string]>(\"DELETE FROM pages WHERE slug = ?\");\n const deleteFtsByPage = db.prepare<[string]>(\n \"DELETE FROM fts_pages WHERE slug = ?\",\n );\n\n const replacePage = db.prepare<\n [string, string, string, string, number, number | null, string | null]\n >(\n `INSERT INTO pages (slug, title, file_path, content_hash, updated_at, archived_at, superseded_by)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(slug) DO UPDATE SET\n title = excluded.title,\n file_path = excluded.file_path,\n content_hash = excluded.content_hash,\n updated_at = excluded.updated_at,\n archived_at = excluded.archived_at,\n superseded_by = excluded.superseded_by`,\n );\n\n const deletePageTopics = db.prepare<[string]>(\n \"DELETE FROM page_topics WHERE page_slug = ?\",\n );\n const insertPageTopic = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO page_topics (page_slug, topic_slug) VALUES (?, ?)\",\n );\n // Seed ad-hoc topics with a title-cased default. If the topic is\n // later declared in `.almanac/topics.yaml`, `applyTopicsYaml` will\n // promote the title/description to whatever the file says. We set the\n // title here (rather than leaving NULL) so `topics list` and\n // `health --topic` have a display name even before a user writes to\n // topics.yaml.\n const insertTopic = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO topics (slug, title) VALUES (?, ?)\",\n );\n\n const deleteFileRefs = db.prepare<[string]>(\n \"DELETE FROM file_refs WHERE page_slug = ?\",\n );\n const insertFileRef = db.prepare<[string, string, string, number]>(\n \"INSERT OR IGNORE INTO file_refs (page_slug, path, original_path, is_dir) VALUES (?, ?, ?, ?)\",\n );\n\n const deleteWikilinks = db.prepare<[string]>(\n \"DELETE FROM wikilinks WHERE source_slug = ?\",\n );\n const insertWikilink = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO wikilinks (source_slug, target_slug) VALUES (?, ?)\",\n );\n\n const deleteXwiki = db.prepare<[string]>(\n \"DELETE FROM cross_wiki_links WHERE source_slug = ?\",\n );\n const insertXwiki = db.prepare<[string, string, string]>(\n \"INSERT OR IGNORE INTO cross_wiki_links (source_slug, target_wiki, target_slug) VALUES (?, ?, ?)\",\n );\n\n const insertFts = db.prepare<[string, string, string]>(\n \"INSERT INTO fts_pages (slug, title, content) VALUES (?, ?, ?)\",\n );\n\n const apply = db.transaction(() => {\n for (const slug of toDelete) {\n // `fts_pages` is an FTS5 virtual table — FK cascades do NOT propagate\n // into it, so we must delete FTS rows explicitly before relying on\n // `DELETE FROM pages` to cascade-clean the four real tables\n // (page_topics, file_refs, wikilinks, cross_wiki_links). If this\n // explicit delete ever gets removed, orphaned FTS rows will show up\n // as phantom search hits pointing at non-existent slugs.\n deleteFtsByPage.run(slug);\n deleteByPage.run(slug); // CASCADE cleans page_topics, file_refs, wikilinks, cross_wiki_links\n }\n\n for (const p of planned) {\n // page_topics/file_refs/wikilinks/cross_wiki_links all cascade on\n // delete, so the cleanest \"replace\" story is: delete-then-insert\n // the per-page rows under the same transaction. Doing it this way\n // (rather than `ON CONFLICT DO UPDATE` per row) keeps the logic\n // uniform and makes \"remove a topic from frontmatter\" work.\n deletePageTopics.run(p.slug);\n deleteFileRefs.run(p.slug);\n deleteWikilinks.run(p.slug);\n deleteXwiki.run(p.slug);\n // Same virtual-table reason as the deletion branch above — FTS5\n // rows do not cascade, so clean them by hand before reinserting.\n deleteFtsByPage.run(p.slug);\n\n replacePage.run(\n p.slug,\n p.title,\n p.fullPath,\n p.contentHash,\n p.updatedAt,\n p.archivedAt,\n p.supersededBy,\n );\n\n for (const topic of p.topics) {\n const topicSlug = toKebabCase(topic);\n if (topicSlug.length === 0) continue;\n insertTopic.run(topicSlug, titleCase(topicSlug));\n insertPageTopic.run(p.slug, topicSlug);\n }\n\n // Frontmatter `files:` — normalize each entry, inferring directness\n // from its trailing slash. Authors who write `src/payments` (no\n // trailing slash) are asserting a file; this matches how `[[...]]`\n // classifies the same string. We store both the lowercased form\n // (for `--mentions` GLOB queries) and the casing-preserving form\n // (for dead-ref `existsSync` on case-sensitive filesystems).\n for (const raw of p.frontmatterFiles) {\n const isDir = looksLikeDir(raw);\n const path = normalizePath(raw, isDir);\n const originalPath = normalizePathPreservingCase(raw, isDir);\n if (path.length === 0) continue;\n insertFileRef.run(p.slug, path, originalPath, isDir ? 1 : 0);\n }\n\n // Inline `[[...]]` extracted from body.\n for (const ref of p.wikilinks) {\n switch (ref.kind) {\n case \"page\":\n insertWikilink.run(p.slug, ref.target);\n break;\n case \"file\":\n insertFileRef.run(p.slug, ref.path, ref.originalPath, 0);\n break;\n case \"folder\":\n insertFileRef.run(p.slug, ref.path, ref.originalPath, 1);\n break;\n case \"xwiki\":\n insertXwiki.run(p.slug, ref.wiki, ref.target);\n break;\n }\n }\n\n insertFts.run(p.slug, p.title, p.content);\n }\n });\n apply();\n\n const pagesIndexed = seenSlugs.size;\n return {\n changed: planned.length,\n removed: toDelete.length,\n total: pagesIndexed,\n pagesIndexed,\n filesSeen: files.length,\n filesSkipped,\n };\n}\n\nfunction hashContent(raw: string): string {\n return createHash(\"sha256\").update(raw).digest(\"hex\");\n}\n","/**\n * Canonical kebab-case slugifier used across the codebase.\n *\n * One function, three callers:\n * - `registry/index.ts` — wiki name slugs (both auto-derived and\n * user-supplied via `--name`)\n * - `indexer/index.ts` — page filename → slug and topic → slug\n * - `indexer/wikilinks.ts` — wikilink target → slug for resolution\n *\n * All three want the same behavior: lowercased, non-alphanumeric runs\n * collapse to a single hyphen, leading/trailing hyphens trimmed. Keeping\n * this in one place avoids a class of bug where an unusual input (e.g.\n * `Checkout_Flow`) produces different slugs depending on which layer\n * slugified it.\n *\n * Rules:\n * - Lowercase\n * - Non-alphanumeric runs collapse to a single hyphen\n * - Leading/trailing hyphens trimmed\n */\nexport function toKebabCase(input: string): string {\n return input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n","import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport yaml from \"js-yaml\";\n\nimport { toKebabCase } from \"../slug.js\";\n\n/**\n * One entry in `.almanac/topics.yaml` — the source of truth for topic\n * metadata (title, description, DAG parents). Pages are still the source\n * of truth for which pages belong to which topics; this file only holds\n * the topic-level attributes.\n *\n * `slug` is the canonical kebab-case key used everywhere downstream\n * (SQLite `topics.slug`, page frontmatter `topics:` entries, wikilink\n * targets). `title` is the human-readable name the user typed at create\n * time. `description` is a free-form one-liner (or null when unset).\n * `parents` is the DAG edge list — kept as an array of slugs rather than\n * a nested structure so round-tripping stays boring and a user eyeballing\n * the file can see the full graph.\n */\nexport interface TopicEntry {\n slug: string;\n title: string;\n description: string | null;\n parents: string[];\n}\n\nexport interface TopicsFile {\n topics: TopicEntry[];\n}\n\n/**\n * Load `.almanac/topics.yaml` into a `TopicsFile`. A missing file is not\n * an error — it's the first-run state, which we treat as \"no topic\n * metadata, only whatever the pages declare in frontmatter\". Malformed\n * YAML IS an error; we surface it rather than silently clobbering the\n * user's committed source of truth.\n *\n * The return shape is always normalized — callers don't have to guard\n * for missing `topics` key, wrong types, or absent `parents` arrays.\n */\nexport async function loadTopicsFile(path: string): Promise<TopicsFile> {\n if (!existsSync(path)) {\n return { topics: [] };\n }\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err: unknown) {\n if (isNodeError(err) && err.code === \"ENOENT\") {\n return { topics: [] };\n }\n throw err;\n }\n\n const trimmed = raw.trim();\n if (trimmed.length === 0) {\n return { topics: [] };\n }\n\n let parsed: unknown;\n try {\n parsed = yaml.load(raw);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`topics.yaml at ${path} is not valid YAML: ${message}`);\n }\n\n if (parsed === null || parsed === undefined) {\n return { topics: [] };\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`topics.yaml at ${path} must be a mapping`);\n }\n\n const obj = parsed as Record<string, unknown>;\n const rawTopics = obj.topics;\n if (rawTopics === undefined || rawTopics === null) {\n return { topics: [] };\n }\n if (!Array.isArray(rawTopics)) {\n throw new Error(`topics.yaml at ${path} — \"topics\" must be a list`);\n }\n\n const topics: TopicEntry[] = [];\n for (const item of rawTopics) {\n if (typeof item !== \"object\" || item === null || Array.isArray(item)) {\n continue;\n }\n const entry = item as Record<string, unknown>;\n const slugRaw = entry.slug;\n if (typeof slugRaw !== \"string\" || slugRaw.trim().length === 0) continue;\n const slug = toKebabCase(slugRaw);\n if (slug.length === 0) continue;\n const title =\n typeof entry.title === \"string\" && entry.title.trim().length > 0\n ? entry.title.trim()\n : titleCase(slug);\n const description =\n typeof entry.description === \"string\" &&\n entry.description.trim().length > 0\n ? entry.description.trim()\n : null;\n const parents: string[] = [];\n if (Array.isArray(entry.parents)) {\n for (const p of entry.parents) {\n if (typeof p === \"string\" && p.trim().length > 0) {\n const ps = toKebabCase(p);\n if (ps.length > 0 && ps !== slug && !parents.includes(ps)) {\n parents.push(ps);\n }\n }\n }\n }\n topics.push({ slug, title, description, parents });\n }\n\n return { topics };\n}\n\n/**\n * Write a `TopicsFile` atomically — tmp file + rename, same pattern as\n * the registry. A half-written topics.yaml would corrupt the user's\n * committed source of truth, so we never write in place.\n *\n * Ordering: topics are sorted by slug for stable diffs. Parents within\n * each entry stay in the order the caller passed them (semantically an\n * ordered list — topics.yaml is the place a user can visibly reason\n * about \"primary parent first\", even though SQLite treats them as a\n * set).\n *\n * We emit a leading comment so first-time readers know the file is\n * edited by the CLI and what its role is.\n */\nexport async function writeTopicsFile(\n path: string,\n file: TopicsFile,\n): Promise<void> {\n const sorted = [...file.topics].sort((a, b) => a.slug.localeCompare(b.slug));\n const doc = {\n topics: sorted.map((t) => {\n // Emit all four keys in a stable order: slug, title, description,\n // parents. description is emitted as `null` in YAML when unset so\n // the schema stays consistent across entries (js-yaml renders the\n // literal word `null`, not the `~` shorthand).\n return {\n slug: t.slug,\n title: t.title,\n description: t.description,\n parents: t.parents,\n };\n }),\n };\n\n const header =\n `# .almanac/topics.yaml — source of truth for topic metadata.\\n` +\n `# Managed by \\`almanac topics\\` commands. User-added comments\\n` +\n `# between entries will be stripped on the next write (js-yaml\\n` +\n `# doesn't round-trip comments). Edit at your own risk — or use the\\n` +\n `# CLI (\\`almanac topics create|link|describe|rename|delete\\`)\\n` +\n `# which preserves the structure correctly.\\n`;\n const body = yaml.dump(doc, {\n lineWidth: 100,\n noRefs: true,\n sortKeys: false,\n });\n const content = `${header}${body}`;\n const tmpPath = `${path}.tmp`;\n // mkdir parent in case `.almanac/` vanished (shouldn't, but cheap insurance)\n const parent = dirname(path);\n if (!existsSync(parent)) {\n await mkdir(parent, { recursive: true });\n }\n await writeFile(tmpPath, content, \"utf8\");\n await rename(tmpPath, path);\n}\n\n/**\n * Look up a topic by slug. Returns `null` when the slug is absent —\n * callers distinguish \"declared in topics.yaml\" from \"ad-hoc (only\n * appears in page frontmatter)\" based on this.\n */\nexport function findTopic(file: TopicsFile, slug: string): TopicEntry | null {\n for (const t of file.topics) {\n if (t.slug === slug) return t;\n }\n return null;\n}\n\n/**\n * Ensure a topic entry exists. If missing, inserts a minimal entry with\n * title-cased title and null description. Returns the (possibly new)\n * entry. Used by `tag`, `topics create` (with `--parent auto-creating`),\n * and `topics link` (auto-creating child/parent on demand).\n */\nexport function ensureTopic(file: TopicsFile, slug: string): TopicEntry {\n const existing = findTopic(file, slug);\n if (existing !== null) return existing;\n const entry: TopicEntry = {\n slug,\n title: titleCase(slug),\n description: null,\n parents: [],\n };\n file.topics.push(entry);\n return entry;\n}\n\n/**\n * Convert a slug back to a human-ish title: `auth-flow` → `Auth Flow`.\n * Used as the fallback title when the caller didn't provide one\n * (auto-creation paths, ad-hoc slugs coming from page frontmatter).\n */\nexport function titleCase(slug: string): string {\n if (slug.length === 0) return slug;\n return slug\n .split(\"-\")\n .filter((s) => s.length > 0)\n .map((s) => `${s[0]?.toUpperCase() ?? \"\"}${s.slice(1)}`)\n .join(\" \");\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n","import yaml from \"js-yaml\";\n\nexport interface Frontmatter {\n title?: string;\n topics: string[];\n files: string[];\n archived_at: number | null;\n superseded_by: string | null;\n supersedes: string | null;\n /**\n * The body of the file with frontmatter removed, for FTS5 and H1 fallback.\n * Always populated even when the file has no frontmatter.\n */\n body: string;\n}\n\n/**\n * Pull YAML frontmatter off the top of a markdown file and coerce the\n * relevant fields. Unknown fields are tolerated silently — the wiki should\n * accept fields we don't understand yet without spewing warnings at the\n * user (future slices might consume them).\n *\n * Failure modes:\n * - No frontmatter at all → `{ topics: [], files: [], ..., body: raw }`.\n * This is legal; a heading + prose is a valid page.\n * - Malformed YAML → warning to stderr, treated as \"no frontmatter\". We\n * choose not to throw so a single bad file doesn't tank a reindex.\n *\n * Note on `archived_at`: authors write this as a YAML date (`2026-04-15`),\n * which `js-yaml` parses to a JS `Date`. We also tolerate ISO-8601 strings\n * and raw numbers. Everything else gets dropped (treated as \"not\n * archived\"). Storing epoch seconds keeps `--since`/`--stale`/`archived`\n * arithmetic trivial at query time.\n */\nexport function parseFrontmatter(raw: string): Frontmatter {\n const empty: Frontmatter = {\n topics: [],\n files: [],\n archived_at: null,\n superseded_by: null,\n supersedes: null,\n body: raw,\n };\n\n // Frontmatter fence MUST start on line 1 — a `---` partway through the\n // document is just a horizontal rule. Be strict about the opening delim\n // so we don't accidentally strip section headers.\n if (!raw.startsWith(\"---\")) {\n return empty;\n }\n\n // Tolerate either Unix or Windows line endings. We read the first line\n // explicitly to confirm it's only `---` (no trailing content).\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (match === null) {\n return empty;\n }\n\n const yamlBody = match[1] ?? \"\";\n const body = match[2] ?? \"\";\n\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlBody);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`almanac: malformed frontmatter (${message})\\n`);\n return empty;\n }\n\n if (parsed === null || parsed === undefined) {\n return { ...empty, body };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n // Someone wrote a YAML scalar or list as the document root — not a\n // mapping, so no fields for us to extract. Treat as empty but keep the\n // post-fence body so FTS5 still gets content.\n return { ...empty, body };\n }\n\n const obj = parsed as Record<string, unknown>;\n\n return {\n title: coerceString(obj.title),\n topics: coerceStringArray(obj.topics),\n files: coerceStringArray(obj.files),\n archived_at: coerceEpochSeconds(obj.archived_at),\n superseded_by: coerceString(obj.superseded_by) ?? null,\n supersedes: coerceString(obj.supersedes) ?? null,\n body,\n };\n}\n\n/**\n * H1 fallback for title when frontmatter has none.\n *\n * Only considers the first 40 lines of the body — any real wiki page has\n * its H1 near the top. NOTE: `String.prototype.split(sep, limit)` still\n * splits the whole string internally and then truncates; it's not an\n * early-bail iteration. For the multi-megabyte files we might see in\n * practice this is still cheap (one regex pass, no allocation per-line\n * beyond the 40 we keep), so we favor the clearer code over hand-rolled\n * line iteration.\n */\nexport function firstH1(body: string): string | undefined {\n const lines = body.split(/\\r?\\n/, 40);\n for (const line of lines) {\n const m = line.match(/^#\\s+(.+?)\\s*#*\\s*$/);\n if (m !== null) {\n return m[1];\n }\n }\n return undefined;\n}\n\nfunction coerceString(v: unknown): string | undefined {\n if (typeof v === \"string\" && v.trim().length > 0) return v.trim();\n return undefined;\n}\n\nfunction coerceStringArray(v: unknown): string[] {\n if (!Array.isArray(v)) return [];\n const out: string[] = [];\n for (const item of v) {\n if (typeof item === \"string\" && item.trim().length > 0) {\n out.push(item.trim());\n }\n }\n return out;\n}\n\n/**\n * Coerce a frontmatter `archived_at` value (YAML Date, ISO string, or raw\n * epoch number) into epoch seconds. Returns `null` for anything we can't\n * make sense of — pages with an unrecognizable `archived_at` are treated as\n * active rather than silently marked archived, which is the safer default.\n */\nfunction coerceEpochSeconds(v: unknown): number | null {\n if (v instanceof Date) {\n return Math.floor(v.getTime() / 1000);\n }\n if (typeof v === \"number\" && Number.isFinite(v)) {\n return Math.floor(v);\n }\n if (typeof v === \"string\" && v.trim().length > 0) {\n const t = Date.parse(v.trim());\n if (!Number.isNaN(t)) {\n return Math.floor(t / 1000);\n }\n }\n return null;\n}\n","import { existsSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport fg from \"fast-glob\";\n\n// Glob is relative to `.almanac/pages/`, so this is every markdown page\n// beneath pages without repeating the `pages/` prefix.\nexport const PAGES_GLOB = \"**/*.md\";\n\n/**\n * Return true if any page file has an mtime strictly greater than the\n * index DB's mtime. This is intentionally cheap: `fast-glob` with\n * `stats: true` gives us mtimes without a second `stat` round-trip, and\n * the synchronous walk keeps the decision path simple for CLI entrypoints.\n */\nexport function pagesNewerThan(pagesDir: string, dbPath: string): boolean {\n let dbMtime: number;\n try {\n dbMtime = statSync(dbPath).mtimeMs;\n } catch {\n return true;\n }\n\n const entries = fg.sync(PAGES_GLOB, {\n cwd: pagesDir,\n absolute: true,\n onlyFiles: true,\n stats: true,\n }) as Array<{ path: string; stats?: { mtimeMs: number } }>;\n\n for (const entry of entries) {\n const mtime = entry.stats?.mtimeMs;\n if (mtime !== undefined && mtime > dbMtime) return true;\n }\n return false;\n}\n\n/**\n * Return true if `topics.yaml` has an mtime strictly greater than the\n * index DB's mtime. Missing `topics.yaml` is legal: it means \"no topic\n * metadata yet\", not \"the index is stale\".\n */\nexport function topicsYamlNewerThan(\n almanacDir: string,\n dbPath: string,\n): boolean {\n const path = join(almanacDir, \"topics.yaml\");\n if (!existsSync(path)) return false;\n let dbMtime: number;\n try {\n dbMtime = statSync(dbPath).mtimeMs;\n } catch {\n return true;\n }\n try {\n const st = statSync(path);\n return st.mtimeMs > dbMtime;\n } catch {\n return false;\n }\n}\n","/**\n * Path normalization for the file/folder references stored in `file_refs`\n * and for the query input passed to `--mentions`.\n *\n * The same function runs over both sides so a value written at index time\n * and a value looked up at query time compare byte-for-byte. If this ever\n * drifts between writers and readers, `--mentions` starts silently missing\n * matches — apply one canonicalization, not two.\n *\n * Rules (from the spec, Correctness):\n * - Lowercase (macOS filesystems are case-insensitive, so the wiki treats\n * `Src/Checkout/` and `src/checkout/` as the same path)\n * - Forward slashes only (never backslashes from Windows-authored content)\n * - No leading `./`\n * - Collapse redundant slashes (`src//checkout/` → `src/checkout/`)\n * - Trailing `/` iff the caller says it's a directory\n *\n * The `isDir` flag is a signal carried alongside the path — we don't infer\n * it from the raw string here, because frontmatter `files:` entries and the\n * inline `[[...]]` classifier both decide directness themselves and pass\n * the answer in. Having one place decide and one place normalize keeps the\n * directory inference rule testable in isolation.\n */\nexport function normalizePath(raw: string, isDir: boolean): string {\n const normalized = normalizeShape(raw, isDir);\n return normalized.toLowerCase();\n}\n\n/**\n * Normalize shape without lowercasing — preserves the author's casing.\n * Used to store `original_path` in `file_refs` so dead-ref checks on\n * case-sensitive filesystems (Linux, `git` checkouts with core.ignorecase\n * false) stat the actual path on disk rather than a lowercased alias.\n *\n * Everything else about the result is identical to `normalizePath`:\n * forward slashes, no `./`, no duplicate slashes, trailing `/` iff\n * `isDir`. The ONLY difference is the final `.toLowerCase()` is skipped.\n */\nexport function normalizePathPreservingCase(raw: string, isDir: boolean): string {\n return normalizeShape(raw, isDir);\n}\n\nfunction normalizeShape(raw: string, isDir: boolean): string {\n let s = raw.trim();\n\n // Windows-style backslashes → forward slashes. We never want to store\n // backslashes; a path authored on Windows and checked in should match\n // the same path authored on macOS.\n s = s.replace(/\\\\+/g, \"/\");\n\n // Drop a leading `./` — it's syntactic noise and authors inconsistently\n // include it. `./src/checkout/` and `src/checkout/` must hash equal.\n while (s.startsWith(\"./\")) s = s.slice(2);\n\n // Collapse any run of slashes to a single slash. This also normalizes\n // `src//checkout/` and accidental doubled slashes from string concat.\n s = s.replace(/\\/+/g, \"/\");\n\n // Strip any trailing slashes before re-applying the directory marker —\n // this way we don't care if the caller fed us `src/checkout` or\n // `src/checkout/` as a directory; we impose our own rule.\n s = s.replace(/\\/+$/, \"\");\n\n if (isDir) {\n // Directories ALWAYS end with a trailing slash. This is what lets the\n // GLOB queries distinguish `src/checkout/` (the directory) from\n // `src/checkout` (a file with no extension) without ambiguity.\n return `${s}/`;\n }\n return s;\n}\n\n/**\n * Infer `isDir` from the raw string the author wrote. Only used at the\n * point of parsing frontmatter `files:` entries and inline `[[...]]`\n * references — everywhere else, `isDir` is already known from context.\n *\n * Rule: trailing `/` (after backslash normalization) means directory.\n */\nexport function looksLikeDir(raw: string): boolean {\n const s = raw.trim().replace(/\\\\+/g, \"/\");\n return s.endsWith(\"/\");\n}\n","import { existsSync } from \"node:fs\";\n\nimport type Database from \"better-sqlite3\";\n\nimport { loadTopicsFile } from \"../topics/yaml.js\";\n\nexport const TOPICS_YAML_FILENAME = \"topics.yaml\";\n\n/**\n * Apply the contents of `.almanac/topics.yaml` to SQLite.\n *\n * Called at the tail of every reindex. For each entry in the file we\n * upsert a row into `topics` and rewrite that topic's parent edges.\n *\n * Important invariants:\n * - Missing `topics.yaml` is a no-op. Absence is legal and means \"no\n * topic metadata yet\".\n * - Topics mentioned only in page frontmatter are legal ad-hoc topics.\n * Do not delete them just because they are absent from `topics.yaml`.\n * - Stale topic rows are pruned only after upserting declared topics and\n * collecting current `page_topics`, so `health` does not flag topics\n * that were removed from every page after a rename/delete.\n */\nexport async function applyTopicsYaml(\n db: Database.Database,\n topicsYamlPath: string,\n): Promise<void> {\n if (!existsSync(topicsYamlPath)) return;\n let file;\n try {\n file = await loadTopicsFile(topicsYamlPath);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`almanac: ${message}\\n`);\n return;\n }\n\n const upsertTopic = db.prepare<[string, string, string | null]>(\n `INSERT INTO topics (slug, title, description) VALUES (?, ?, ?)\n ON CONFLICT(slug) DO UPDATE SET\n title = excluded.title,\n description = excluded.description`,\n );\n const clearParents = db.prepare<[string]>(\n \"DELETE FROM topic_parents WHERE child_slug = ?\",\n );\n const insertParent = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO topic_parents (child_slug, parent_slug) VALUES (?, ?)\",\n );\n\n // Declared means either explicitly present in topics.yaml or currently\n // referenced by page frontmatter. Anything outside this set is stale.\n const declared = new Set<string>();\n for (const t of file.topics) declared.add(t.slug);\n const adHoc = db\n .prepare<[], { topic_slug: string }>(\n \"SELECT DISTINCT topic_slug FROM page_topics\",\n )\n .all();\n for (const r of adHoc) declared.add(r.topic_slug);\n\n const apply = db.transaction(() => {\n for (const t of file.topics) {\n upsertTopic.run(t.slug, t.title, t.description);\n clearParents.run(t.slug);\n for (const parent of t.parents) {\n if (parent === t.slug) continue;\n insertParent.run(t.slug, parent);\n }\n }\n\n // Prune stale topic rows + any edges attached to them last, after\n // the upserts above have promoted declared slugs.\n const existing = db\n .prepare<[], { slug: string }>(\"SELECT slug FROM topics\")\n .all();\n const deleteTopic = db.prepare<[string]>(\"DELETE FROM topics WHERE slug = ?\");\n const deleteEdgesByChild = db.prepare<[string]>(\n \"DELETE FROM topic_parents WHERE child_slug = ?\",\n );\n const deleteEdgesByParent = db.prepare<[string]>(\n \"DELETE FROM topic_parents WHERE parent_slug = ?\",\n );\n for (const r of existing) {\n if (declared.has(r.slug)) continue;\n deleteEdgesByChild.run(r.slug);\n deleteEdgesByParent.run(r.slug);\n deleteTopic.run(r.slug);\n }\n });\n apply();\n}\n","import { toKebabCase } from \"../slug.js\";\nimport {\n looksLikeDir,\n normalizePath,\n normalizePathPreservingCase,\n} from \"./paths.js\";\n\n/**\n * One parsed `[[...]]` reference from a page body. Classification is\n * deterministic and content-based — see `classifyWikilink` below.\n *\n * Callers dispatch on `kind`:\n * - `page` → row in `wikilinks`\n * - `file` → row in `file_refs` with `is_dir = 0`\n * - `folder` → row in `file_refs` with `is_dir = 1`\n * - `xwiki` → row in `cross_wiki_links`\n *\n * File/folder refs carry TWO forms of the path:\n * - `path` — lowercased (for `--mentions` lookups)\n * - `originalPath` — as-written (for filesystem stats on case-sensitive\n * systems and for user-facing display)\n */\nexport type WikilinkRef =\n | { kind: \"page\"; target: string }\n | { kind: \"file\"; path: string; originalPath: string }\n | { kind: \"folder\"; path: string; originalPath: string }\n | { kind: \"xwiki\"; wiki: string; target: string };\n\n/**\n * Rules from the spec (\"Classification rules\"), applied in order:\n *\n * 1. Contains `:` before any `/` → cross-wiki reference (`wiki:slug`)\n * 2. Contains `/` → file or folder reference\n * - Trailing `/` = folder\n * - Otherwise = file\n * 3. Otherwise → page slug wikilink\n *\n * Edge cases the test suite pins down:\n * - `[[a:b/c]]` → xwiki (colon is before the slash, rule 1 wins)\n * - `[[src/a:b]]` → file (slash is before the colon, rule 2 wins)\n * - `[[./x]]` → the leading `./` is stripped by `normalizePath`,\n * so this lands in `file_refs` as `x`. A bare `./x`\n * with no inner slash would classify as a file.\n * - `[[foo|display]]`→ Obsidian-style display text is stripped; we key\n * on the target only. A future slice could surface\n * display text in `almanac info`.\n */\nexport function classifyWikilink(raw: string): WikilinkRef | null {\n // Strip Obsidian-style `|display` suffix — we don't index display text\n // in slice 2, but we want the classifier to see the real target.\n const pipe = raw.indexOf(\"|\");\n let body = pipe === -1 ? raw : raw.slice(0, pipe);\n body = body.trim();\n if (body.length === 0) return null;\n\n const firstColon = body.indexOf(\":\");\n const firstSlash = body.indexOf(\"/\");\n\n // Rule 1: cross-wiki, `wiki:slug`. Only if the colon comes before any\n // slash — otherwise `src/urls.ts:42` (hypothetical) would wrongly\n // classify as xwiki.\n if (firstColon !== -1 && (firstSlash === -1 || firstColon < firstSlash)) {\n const wiki = body.slice(0, firstColon).trim();\n const target = body.slice(firstColon + 1).trim();\n if (wiki.length === 0 || target.length === 0) return null;\n return { kind: \"xwiki\", wiki, target };\n }\n\n // Rule 2: file or folder. The `/` may be anywhere including trailing.\n if (firstSlash !== -1) {\n const isDir = looksLikeDir(body);\n const path = normalizePath(body, isDir);\n const originalPath = normalizePathPreservingCase(body, isDir);\n if (path.length === 0) return null;\n return isDir\n ? { kind: \"folder\", path, originalPath }\n : { kind: \"file\", path, originalPath };\n }\n\n // Rule 3: page slug wikilink. Authors might write `Checkout Flow` or\n // `Checkout_Flow` by accident — slugify defensively so backlinks still\n // resolve in those cases.\n const target = toKebabCase(body);\n if (target.length === 0) return null;\n return { kind: \"page\", target };\n}\n\n/**\n * Walk a markdown body and pull every `[[...]]` reference. We scan the\n * whole body rather than try to skip code blocks — the spec is explicit:\n * \"Prose outside `[[...]]` is just prose. No backtick-path heuristics, no\n * false positives from code blocks or log output.\" A `[[foo]]` inside a\n * fenced code block is still a wikilink. Authors who genuinely need a\n * literal `[[x]]` in code can escape one of the brackets.\n */\nexport function extractWikilinks(body: string): WikilinkRef[] {\n const out: WikilinkRef[] = [];\n const re = /\\[\\[([^\\]\\n]+)\\]\\]/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(body)) !== null) {\n const ref = classifyWikilink(m[1] ?? \"\");\n if (ref !== null) out.push(ref);\n }\n return out;\n}\n","import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport { getGlobalAlmanacDir, getRegistryPath } from \"../paths.js\";\nimport { toKebabCase } from \"../slug.js\";\n\n// Re-export so existing import sites (`from \"../registry/index.js\"`) keep\n// working without a mechanical fan-out. The canonical home is `../slug.js`.\nexport { toKebabCase };\n\n/**\n * One entry in `~/.almanac/registry.json`.\n *\n * `name` is the canonical kebab-case slug the user types. `path` is the\n * absolute repo root (the directory that contains `.almanac/`). We store\n * absolute paths so cross-wiki resolution works regardless of the caller's\n * cwd.\n */\nexport interface RegistryEntry {\n name: string;\n description: string;\n path: string;\n registered_at: string;\n}\n\n/**\n * Read the registry file into memory.\n *\n * A missing file is not an error — it's the first-run state, which we\n * treat as an empty registry. A malformed file IS an error; we surface it\n * rather than silently clobbering the user's data.\n */\nexport async function readRegistry(): Promise<RegistryEntry[]> {\n const path = getRegistryPath();\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err: unknown) {\n if (isNodeError(err) && err.code === \"ENOENT\") {\n return [];\n }\n throw err;\n }\n\n const trimmed = raw.trim();\n if (trimmed.length === 0) {\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`registry at ${path} is not valid JSON: ${message}`);\n }\n\n if (!Array.isArray(parsed)) {\n throw new Error(`registry at ${path} must be a JSON array`);\n }\n\n // Validate every entry. We do NOT silently coerce missing `name` or\n // `path` — an entry with `name: \"\"` would be unremovable via `--drop`\n // and an empty `path` would match any `findEntry({ path: \"\" })` call.\n // If someone hand-edited the registry into a bad state, surfacing the\n // error is strictly better than limping along with corrupt data.\n return parsed.map((item, idx) => {\n if (typeof item !== \"object\" || item === null) {\n throw new Error(`registry entry ${idx} is not an object`);\n }\n const e = item as Record<string, unknown>;\n const name = typeof e.name === \"string\" ? e.name : \"\";\n const path = typeof e.path === \"string\" ? e.path : \"\";\n if (name.length === 0) {\n throw new Error(`registry entry ${idx} is missing a non-empty \"name\"`);\n }\n if (path.length === 0) {\n throw new Error(`registry entry ${idx} is missing a non-empty \"path\"`);\n }\n return {\n name,\n description: typeof e.description === \"string\" ? e.description : \"\",\n path,\n registered_at:\n typeof e.registered_at === \"string\" ? e.registered_at : \"\",\n };\n });\n}\n\n/**\n * Persist the registry to disk. Creates `~/.almanac/` if it doesn't exist.\n *\n * We write with a trailing newline and 2-space indentation so the file is\n * diff-friendly if someone ever commits or inspects it manually.\n *\n * The write is atomic: we write to `registry.json.tmp` and then rename,\n * which is an atomic operation on every mainstream filesystem. This\n * matters because two concurrent `almanac init` (or autoregister) calls\n * from different shells would otherwise race on a partial write and\n * corrupt the file — a single `rename` means one wins cleanly and the\n * other's contents are simply dropped.\n */\nexport async function writeRegistry(entries: RegistryEntry[]): Promise<void> {\n const path = getRegistryPath();\n await mkdir(dirname(path), { recursive: true });\n const body = `${JSON.stringify(entries, null, 2)}\\n`;\n const tmpPath = `${path}.tmp`;\n await writeFile(tmpPath, body, \"utf8\");\n await rename(tmpPath, path);\n}\n\n/**\n * macOS (HFS+/APFS default) and Windows (NTFS default) are case-insensitive\n * but case-preserving. `/Users/x/Project` and `/Users/x/project` are the\n * same directory. We must treat them as the same registry entry, or a\n * single `almanac init` from a differently-cased cwd would duplicate the\n * row. Linux is case-sensitive — do not normalize there.\n *\n * Callers still store the original casing; only comparisons are lowercased.\n */\nfunction pathsEqual(a: string, b: string): boolean {\n if (process.platform === \"darwin\" || process.platform === \"win32\") {\n return a.toLowerCase() === b.toLowerCase();\n }\n return a === b;\n}\n\n/**\n * Add (or replace) an entry in the registry.\n *\n * Uniqueness is enforced on BOTH `name` and `path`: a repo can only appear\n * once, and a name can only refer to one repo. If either matches, we\n * replace the existing entry rather than creating a duplicate. This is\n * what makes auto-registration idempotent.\n */\nexport async function addEntry(entry: RegistryEntry): Promise<RegistryEntry[]> {\n const existing = await readRegistry();\n const filtered = existing.filter(\n (e) => e.name !== entry.name && !pathsEqual(e.path, entry.path),\n );\n filtered.push(entry);\n await writeRegistry(filtered);\n return filtered;\n}\n\n/**\n * Remove an entry by name. Returns the removed entry (or `null` if none\n * matched). Only `almanac list --drop <name>` calls this — we never drop\n * automatically, even for unreachable paths.\n */\nexport async function dropEntry(name: string): Promise<RegistryEntry | null> {\n const existing = await readRegistry();\n const idx = existing.findIndex((e) => e.name === name);\n if (idx === -1) {\n return null;\n }\n const [removed] = existing.splice(idx, 1);\n await writeRegistry(existing);\n return removed ?? null;\n}\n\n/**\n * Find an entry by either name or absolute path. Used by auto-registration\n * to decide whether the current repo is already known.\n *\n * Path comparison is case-insensitive on macOS/Windows (see `pathsEqual`).\n */\nexport async function findEntry(params: {\n name?: string;\n path?: string;\n}): Promise<RegistryEntry | null> {\n const entries = await readRegistry();\n for (const entry of entries) {\n if (params.name !== undefined && entry.name === params.name) return entry;\n if (params.path !== undefined && pathsEqual(entry.path, params.path)) {\n return entry;\n }\n }\n return null;\n}\n\n/**\n * Ensure the global `.almanac/` directory exists. Safe to call repeatedly;\n * `mkdir recursive` is a no-op when the directory already exists.\n */\nexport async function ensureGlobalDir(): Promise<void> {\n await mkdir(getGlobalAlmanacDir(), { recursive: true });\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { BLUE, BOLD, DIM, GREEN, RED, RST } from \"../ansi.js\";\nimport { parseDuration } from \"../indexer/duration.js\";\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolve-wiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\nimport { findEntry } from \"../registry/index.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport { subtreeInDb } from \"../topics/dag.js\";\n\n/**\n * `almanac health` — flag problems in the wiki.\n *\n * Eight independent categories, each checked against the current index\n * and filesystem. Categories never throw each other off; one failing\n * is not a reason to skip the others.\n *\n * Scoping:\n * - `--topic <slug>` narrows every page-scoped category to pages\n * tagged with that topic OR any descendant topic (DAG traversal).\n * Topic-level categories (`empty_topics`) are narrowed to the\n * subtree itself.\n * - `--stdin` reads page slugs from stdin and limits page-scoped\n * categories to that set.\n *\n * Output:\n * - default: human-readable, grouped by category with counts.\n * - `--json`: one big object, shape = `HealthReport`.\n */\n\nexport interface HealthReport {\n orphans: { slug: string }[];\n stale: { slug: string; days_since_update: number }[];\n dead_refs: { slug: string; path: string }[];\n broken_links: { source_slug: string; target_slug: string }[];\n broken_xwiki: { source_slug: string; target_wiki: string; target_slug: string }[];\n empty_topics: { slug: string }[];\n empty_pages: { slug: string }[];\n slug_collisions: { slug: string; paths: string[] }[];\n}\n\nexport interface HealthOptions {\n cwd: string;\n wiki?: string;\n topic?: string;\n stale?: string;\n stdin?: boolean;\n stdinInput?: string;\n json?: boolean;\n}\n\nexport interface HealthCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Default `--stale` window. 90 days matches the spec. Users can tune\n * with `--stale <duration>` using the shared parser.\n */\nconst DEFAULT_STALE_SECONDS = 90 * 24 * 60 * 60;\n\nexport async function runHealth(\n options: HealthOptions,\n): Promise<HealthCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n await ensureFreshIndex({ repoRoot });\n\n const almanacDir = join(repoRoot, \".almanac\");\n const pagesDir = join(almanacDir, \"pages\");\n const db = openIndex(join(almanacDir, \"index.db\"));\n\n try {\n const staleSeconds = options.stale !== undefined\n ? parseDuration(options.stale)\n : DEFAULT_STALE_SECONDS;\n\n const scope = resolveScope(db, options);\n\n const report: HealthReport = {\n orphans: findOrphans(db, scope),\n stale: findStale(db, scope, staleSeconds),\n dead_refs: await findDeadRefs(db, scope, repoRoot),\n broken_links: findBrokenLinks(db, scope),\n broken_xwiki: await findBrokenXwiki(db, scope),\n empty_topics: findEmptyTopics(db, scope),\n empty_pages: await findEmptyPages(db, scope, pagesDir),\n slug_collisions: await findSlugCollisions(pagesDir),\n };\n\n if (options.json === true) {\n return {\n stdout: `${JSON.stringify(report, null, 2)}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout: formatReport(report),\n stderr: \"\",\n exitCode: 0,\n };\n } finally {\n db.close();\n }\n}\n\ninterface HealthScope {\n /** When non-null, restrict page-scoped checks to these slugs. */\n pages: Set<string> | null;\n /** When non-null, restrict topic-scoped checks to these slugs. */\n topics: Set<string> | null;\n}\n\n/**\n * Compute the active page/topic scope from `--topic` and `--stdin`\n * flags. Both null = no restriction (report everything).\n */\nfunction resolveScope(db: Database.Database, options: HealthOptions): HealthScope {\n let pages: Set<string> | null = null;\n let topics: Set<string> | null = null;\n\n if (options.topic !== undefined) {\n const rootSlug = toKebabCase(options.topic);\n if (rootSlug.length > 0) {\n const subtree = subtreeInDb(db, rootSlug);\n topics = new Set(subtree);\n const placeholders = subtree.map(() => \"?\").join(\", \");\n const rows = db\n .prepare<unknown[], { page_slug: string }>(\n `SELECT DISTINCT page_slug FROM page_topics\n WHERE topic_slug IN (${placeholders})`,\n )\n .all(...subtree);\n pages = new Set(rows.map((r) => r.page_slug));\n }\n }\n\n if (options.stdin === true && options.stdinInput !== undefined) {\n const stdinPages = new Set<string>();\n for (const line of options.stdinInput.split(/\\r?\\n/)) {\n const s = line.trim();\n if (s.length > 0) stdinPages.add(s);\n }\n // Intersect with any existing topic-scoped set.\n if (pages === null) pages = stdinPages;\n else {\n const out = new Set<string>();\n for (const s of stdinPages) if (pages.has(s)) out.add(s);\n pages = out;\n }\n }\n\n return { pages, topics };\n}\n\nfunction inPageScope(scope: HealthScope, slug: string): boolean {\n if (scope.pages === null) return true;\n return scope.pages.has(slug);\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// individual checks\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * Pages with zero `topics:`. Archived pages are exempt — the spec\n * excludes them from search by default and they're inherently\n * \"retired\", not \"abandoned\".\n */\nfunction findOrphans(\n db: Database.Database,\n scope: HealthScope,\n): { slug: string }[] {\n const rows = db\n .prepare<[], { slug: string }>(\n `SELECT p.slug FROM pages p\n WHERE p.archived_at IS NULL\n AND NOT EXISTS (\n SELECT 1 FROM page_topics pt WHERE pt.page_slug = p.slug\n )\n ORDER BY p.slug`,\n )\n .all();\n return rows.filter((r) => inPageScope(scope, r.slug));\n}\n\n/**\n * Active pages whose `updated_at` is older than `staleSeconds`. We\n * report `days_since_update` rather than a raw timestamp because the\n * spec's example output (\"old-architecture (124 days)\") shows that.\n */\nfunction findStale(\n db: Database.Database,\n scope: HealthScope,\n staleSeconds: number,\n): { slug: string; days_since_update: number }[] {\n const now = Math.floor(Date.now() / 1000);\n const threshold = now - staleSeconds;\n const rows = db\n .prepare<[number], { slug: string; updated_at: number }>(\n `SELECT slug, updated_at FROM pages\n WHERE archived_at IS NULL AND updated_at < ?\n ORDER BY updated_at ASC`,\n )\n .all(threshold);\n return rows\n .filter((r) => inPageScope(scope, r.slug))\n .map((r) => ({\n slug: r.slug,\n days_since_update: Math.floor((now - r.updated_at) / (60 * 60 * 24)),\n }));\n}\n\n/**\n * `file_refs` whose target paths no longer exist on disk. We `stat`\n * each referenced path, relative to the repo root, and report misses.\n *\n * Only checks active pages — archived pages are allowed to reference\n * files that have since been deleted (that's often why they were\n * archived in the first place).\n *\n * We stat the `original_path` (author's casing) rather than the\n * lowercased `path` — on case-sensitive filesystems like Linux, stat\n * of a lowercased alias of `src/Dockerfile` returns ENOENT even\n * though the file exists. macOS and Windows are case-insensitive so\n * either form resolves there; using the original consistently means\n * the code behaves identically on every host.\n */\nasync function findDeadRefs(\n db: Database.Database,\n scope: HealthScope,\n repoRoot: string,\n): Promise<{ slug: string; path: string }[]> {\n const rows = db\n .prepare<\n [],\n { slug: string; path: string; original_path: string; is_dir: number }\n >(\n `SELECT p.slug, r.path, r.original_path, r.is_dir\n FROM file_refs r\n JOIN pages p ON p.slug = r.page_slug\n WHERE p.archived_at IS NULL\n ORDER BY p.slug, r.path`,\n )\n .all();\n const out: { slug: string; path: string }[] = [];\n for (const r of rows) {\n if (!inPageScope(scope, r.slug)) continue;\n const abs = join(repoRoot, r.original_path);\n if (!existsSync(abs)) {\n // Surface the author's casing in the report — matches what's in\n // the user's frontmatter/wikilink, which is what they'll search\n // for when fixing the miss.\n out.push({ slug: r.slug, path: r.original_path });\n }\n }\n return out;\n}\n\n/**\n * Wikilinks whose target slug has no row in `pages`. Every other\n * page-scoped check filters archived source pages out; this one and\n * `findBrokenXwiki` follow the same rule so the report doesn't flag\n * broken links from pages that have been retired.\n */\nfunction findBrokenLinks(\n db: Database.Database,\n scope: HealthScope,\n): { source_slug: string; target_slug: string }[] {\n const rows = db\n .prepare<[], { source_slug: string; target_slug: string }>(\n `SELECT w.source_slug, w.target_slug\n FROM wikilinks w\n JOIN pages src ON src.slug = w.source_slug\n LEFT JOIN pages tgt ON tgt.slug = w.target_slug\n WHERE tgt.slug IS NULL AND src.archived_at IS NULL\n ORDER BY w.source_slug, w.target_slug`,\n )\n .all();\n return rows.filter((r) => inPageScope(scope, r.source_slug));\n}\n\n/**\n * Cross-wiki links whose target wiki isn't registered OR whose path\n * is unreachable. Per the plan we stop at \"wiki unregistered or path\n * missing\" — walking into the other wiki's `index.db` to check the\n * slug exists is explicitly out of scope for slice 3 (documented in\n * the plan). A follow-up slice can deepen this.\n */\nasync function findBrokenXwiki(\n db: Database.Database,\n scope: HealthScope,\n): Promise<{ source_slug: string; target_wiki: string; target_slug: string }[]> {\n const rows = db\n .prepare<\n [],\n { source_slug: string; target_wiki: string; target_slug: string }\n >(\n // Same archived-source filter as `findBrokenLinks`. Retired pages\n // shouldn't spam the report with links to wikis that may have\n // been intentionally retired too.\n `SELECT x.source_slug, x.target_wiki, x.target_slug\n FROM cross_wiki_links x\n JOIN pages src ON src.slug = x.source_slug\n WHERE src.archived_at IS NULL\n ORDER BY x.source_slug, x.target_wiki, x.target_slug`,\n )\n .all();\n const out: { source_slug: string; target_wiki: string; target_slug: string }[] = [];\n // Cache the registry lookup so we only resolve each wiki once.\n const reachableCache = new Map<string, boolean>();\n for (const r of rows) {\n if (!inPageScope(scope, r.source_slug)) continue;\n let ok = reachableCache.get(r.target_wiki);\n if (ok === undefined) {\n const entry = await findEntry({ name: r.target_wiki });\n ok = entry !== null && existsSync(join(entry.path, \".almanac\"));\n reachableCache.set(r.target_wiki, ok);\n }\n if (!ok) {\n out.push({\n source_slug: r.source_slug,\n target_wiki: r.target_wiki,\n target_slug: r.target_slug,\n });\n }\n }\n return out;\n}\n\n/** Topics with zero pages. */\nfunction findEmptyTopics(\n db: Database.Database,\n scope: HealthScope,\n): { slug: string }[] {\n const rows = db\n .prepare<[], { slug: string }>(\n `SELECT t.slug FROM topics t\n WHERE NOT EXISTS (\n SELECT 1 FROM page_topics pt WHERE pt.topic_slug = t.slug\n )\n ORDER BY t.slug`,\n )\n .all();\n if (scope.topics === null) return rows;\n return rows.filter((r) => scope.topics!.has(r.slug));\n}\n\n/**\n * Pages whose body is effectively empty — only frontmatter, maybe a\n * heading, no prose. \"Empty\" = after dropping frontmatter and heading\n * lines, the remaining non-blank non-whitespace content is < 40\n * characters. This matches the test from the plan: \"a page with only\n * frontmatter + heading is empty; with a paragraph it's not.\"\n *\n * Archived pages are exempt — deliberately minimal archive stubs\n * shouldn't be flagged.\n */\nasync function findEmptyPages(\n db: Database.Database,\n scope: HealthScope,\n pagesDir: string,\n): Promise<{ slug: string }[]> {\n const rows = db\n .prepare<[], { slug: string; file_path: string }>(\n `SELECT slug, file_path FROM pages\n WHERE archived_at IS NULL\n ORDER BY slug`,\n )\n .all();\n const out: { slug: string }[] = [];\n for (const r of rows) {\n if (!inPageScope(scope, r.slug)) continue;\n let raw: string;\n try {\n raw = await readFile(r.file_path, \"utf8\");\n } catch {\n continue;\n }\n // Strip frontmatter if present.\n const m = raw.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n const body = m !== null ? (m[1] ?? \"\") : raw;\n // \"Empty\" = after dropping frontmatter, heading lines, and blank\n // lines, nothing non-trivial remains. A single-line wikilink or\n // one-sentence paragraph counts as content; a page with only a\n // heading (or a heading + whitespace) does not.\n //\n // `pagesDir` is accepted for parity with future content-resolution\n // checks (e.g., resolving includes); referenced so lint doesn't\n // complain about an unused parameter.\n void pagesDir;\n const hasSubstance = body\n .split(/\\r?\\n/)\n .some((l) => {\n const t = l.trim();\n if (t.length === 0) return false;\n if (t.startsWith(\"#\")) return false;\n return true;\n });\n if (!hasSubstance) {\n out.push({ slug: r.slug });\n }\n }\n return out;\n}\n\n/**\n * Walk `.almanac/pages/` and group filenames by their kebab-cased\n * slug. Any slug with >1 filename is a collision. We rescan rather\n * than reading a persisted table — indexing surfaces collisions only\n * as warnings, so a dedicated rescan gives us a definitive answer\n * without adding a new table.\n */\nasync function findSlugCollisions(\n pagesDir: string,\n): Promise<{ slug: string; paths: string[] }[]> {\n if (!existsSync(pagesDir)) return [];\n const files = await fg(\"**/*.md\", {\n cwd: pagesDir,\n absolute: false,\n onlyFiles: true,\n caseSensitiveMatch: true,\n });\n const bySlug = new Map<string, string[]>();\n for (const rel of files) {\n const slug = toKebabCase(basename(rel, \".md\"));\n if (slug.length === 0) continue;\n const list = bySlug.get(slug) ?? [];\n list.push(rel);\n bySlug.set(slug, list);\n }\n const out: { slug: string; paths: string[] }[] = [];\n for (const [slug, paths] of bySlug.entries()) {\n if (paths.length > 1) {\n out.push({ slug, paths: paths.sort() });\n }\n }\n out.sort((a, b) => a.slug.localeCompare(b.slug));\n return out;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// pretty-print\n// ─────────────────────────────────────────────────────────────────────\n\nfunction formatReport(r: HealthReport): string {\n const sections: string[] = [];\n sections.push(\n section(\n \"orphans\",\n r.orphans.length,\n r.orphans.map((o) => ` ${BLUE}${o.slug}${RST}`),\n ),\n );\n sections.push(\n section(\n \"stale\",\n r.stale.length,\n r.stale.map((s) => ` ${BLUE}${s.slug}${RST} ${DIM}(${s.days_since_update} days)${RST}`),\n ),\n );\n sections.push(\n section(\n \"dead-refs\",\n r.dead_refs.length,\n r.dead_refs.map((d) => ` ${BLUE}${d.slug}${RST} references ${d.path} ${DIM}(missing)${RST}`),\n ),\n );\n sections.push(\n section(\n \"broken-links\",\n r.broken_links.length,\n r.broken_links.map(\n (b) => ` ${BLUE}${b.source_slug}${RST} → ${b.target_slug} ${DIM}(target does not exist)${RST}`,\n ),\n ),\n );\n sections.push(\n section(\n \"broken-xwiki\",\n r.broken_xwiki.length,\n r.broken_xwiki.map(\n (b) =>\n ` ${BLUE}${b.source_slug}${RST} → ${b.target_wiki}:${b.target_slug} ${DIM}(wiki unregistered or unreachable)${RST}`,\n ),\n ),\n );\n sections.push(\n section(\n \"empty-topics\",\n r.empty_topics.length,\n r.empty_topics.map((e) => ` ${BLUE}${e.slug}${RST}`),\n ),\n );\n sections.push(\n section(\n \"empty-pages\",\n r.empty_pages.length,\n r.empty_pages.map((e) => ` ${BLUE}${e.slug}${RST}`),\n ),\n );\n sections.push(\n section(\n \"slug-collisions\",\n r.slug_collisions.length,\n r.slug_collisions.map((c) => ` ${BLUE}${c.slug}${RST}: ${c.paths.join(\", \")}`),\n ),\n );\n return `${sections.join(\"\\n\\n\")}\\n`;\n}\n\nfunction section(label: string, count: number, lines: string[]): string {\n if (count === 0) return `${BOLD}${label}${RST} ${GREEN}(0): (ok)${RST}`;\n return `${BOLD}${label}${RST} ${RED}(${count})${RST}:\\n${lines.join(\"\\n\")}`;\n}\n","/**\n * Parse a compact duration string of the form `<N><unit>` into seconds.\n *\n * Accepted units (from the spec, `--since` / `--stale`):\n * - `m` — minutes\n * - `h` — hours\n * - `d` — days\n * - `w` — weeks\n *\n * Examples: `2w` → 1209600, `30d` → 2592000, `12h` → 43200.\n *\n * Anything else throws — the CLI surfaces the error with the usual\n * `almanac: <message>` prefix, which is clearer than silently treating\n * `2weeks` or `30 days` as zero.\n */\nexport function parseDuration(input: string): number {\n const trimmed = input.trim();\n const m = trimmed.match(/^(\\d+)([mhdw])$/);\n if (m === null) {\n throw new Error(\n `invalid duration \"${input}\" (expected Nw, Nd, Nh, or Nm — e.g. 2w, 30d)`,\n );\n }\n const n = Number.parseInt(m[1] ?? \"0\", 10);\n const unit = m[2];\n switch (unit) {\n case \"m\":\n return n * 60;\n case \"h\":\n return n * 60 * 60;\n case \"d\":\n return n * 60 * 60 * 24;\n case \"w\":\n return n * 60 * 60 * 24 * 7;\n default:\n // Unreachable — regex pins the unit — but satisfies exhaustiveness.\n throw new Error(`invalid duration unit \"${unit ?? \"\"}\"`);\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { findNearestAlmanacDir } from \"../paths.js\";\nimport { findEntry } from \"../registry/index.js\";\n\n/**\n * Figure out which repo root a query command should run against.\n *\n * Two modes, in order of precedence:\n * 1. `--wiki <name>` — look it up in the global registry. Fails\n * explicitly if the name isn't registered or its path has gone\n * missing (unmounted drive, deleted repo). No silent fallback, which\n * would hide the real problem from the user.\n * 2. default — walk up from `cwd` like git does. Fails if we're not\n * inside a `.almanac/` repo.\n *\n * Returns the absolute path to the repo root (the directory containing\n * `.almanac/`).\n *\n * NOTE (spec contract, not yet implemented): when `--all` lands in a\n * future slice, it must silently skip wikis whose paths have gone\n * unreachable — the asymmetry with `--wiki <name>` is deliberate.\n * Explicit lookup is loud about failures (user named a specific wiki);\n * bulk `--all` is quiet (user asked \"whatever's available\"). Don't\n * unify the error behavior when adding `--all`.\n */\nexport async function resolveWikiRoot(params: {\n cwd: string;\n wiki?: string;\n}): Promise<string> {\n if (params.wiki !== undefined) {\n const entry = await findEntry({ name: params.wiki });\n if (entry === null) {\n throw new Error(`no registered wiki named \"${params.wiki}\"`);\n }\n if (!existsSync(join(entry.path, \".almanac\"))) {\n throw new Error(\n `wiki \"${params.wiki}\" path is unreachable (${entry.path})`,\n );\n }\n return entry.path;\n }\n\n const nearest = findNearestAlmanacDir(params.cwd);\n if (nearest === null) {\n throw new Error(\n \"no .almanac/ found in this directory or any parent; run `almanac bootstrap` first\",\n );\n }\n return nearest;\n}\n","import type Database from \"better-sqlite3\";\n\nimport type { TopicsFile } from \"./yaml.js\";\n\n/**\n * Depth cap for all recursive traversals of the topics DAG. Belt and\n * suspenders alongside the `CHECK (child_slug != parent_slug)` on the\n * `topic_parents` table — even if a cycle somehow slipped into the data\n * (hand-edited `topics.yaml`, past bug), the CTE can't runaway.\n *\n * 32 is chosen as \"deeper than any real human-authored taxonomy will\n * ever go\". A 32-level topic hierarchy is absurd; anything hitting this\n * cap is almost certainly a cycle.\n */\nexport const DAG_DEPTH_CAP = 32;\n\n/**\n * Given a `topics.yaml` in memory, compute the set of ancestors of a\n * given slug (not including the slug itself). Used by `topics link`\n * to check whether a proposed edge would create a cycle.\n *\n * Running off the in-memory file lets `link` validate BEFORE touching\n * either the DB or the YAML, so a refusal doesn't leave half the state\n * mutated. Depth-capped with the same constant as the SQLite CTE.\n */\nexport function ancestorsInFile(\n file: TopicsFile,\n slug: string,\n): Set<string> {\n // Build a child → parents map once.\n const parentsOf = new Map<string, string[]>();\n for (const t of file.topics) {\n parentsOf.set(t.slug, t.parents);\n }\n const ancestors = new Set<string>();\n // BFS, depth-capped. We stop descending when we've hit the cap or\n // revisit an already-seen node (self-loop defense).\n let frontier: string[] = parentsOf.get(slug) ?? [];\n let depth = 0;\n while (frontier.length > 0 && depth < DAG_DEPTH_CAP) {\n const next: string[] = [];\n for (const node of frontier) {\n if (ancestors.has(node)) continue;\n ancestors.add(node);\n const ps = parentsOf.get(node);\n if (ps !== undefined) next.push(...ps);\n }\n frontier = next;\n depth += 1;\n }\n return ancestors;\n}\n\n/**\n * Return all descendants of a given topic slug via the SQLite\n * `topic_parents` table. Depth-capped at `DAG_DEPTH_CAP`.\n *\n * Used by `topics show --descendants` to expand a topic's page list\n * through its subtopics. The query is a canonical recursive CTE; we\n * `UNION` (not `UNION ALL`) so cycles in the data don't spin forever.\n */\nexport function descendantsInDb(\n db: Database.Database,\n slug: string,\n): string[] {\n const rows = db\n .prepare<[string, number], { slug: string }>(\n `WITH RECURSIVE desc(slug, depth) AS (\n SELECT child_slug, 1 FROM topic_parents WHERE parent_slug = ?\n UNION\n SELECT tp.child_slug, d.depth + 1\n FROM topic_parents tp\n JOIN desc d ON tp.parent_slug = d.slug\n WHERE d.depth < ?\n )\n SELECT DISTINCT slug FROM desc ORDER BY slug`,\n )\n .all(slug, DAG_DEPTH_CAP)\n .map((r) => r.slug);\n return rows;\n}\n\n/**\n * Return the subtree rooted at `slug` (the slug itself + all\n * descendants). Convenience wrapper used by `health --topic` to scope\n * reports through the DAG.\n */\nexport function subtreeInDb(db: Database.Database, slug: string): string[] {\n return [slug, ...descendantsInDb(db, slug)];\n}\n"],"mappings":"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","names":["existsSync","statSync","readFile","join","fg","yaml","existsSync","existsSync","existsSync","target","join","existsSync","fg","statSync","readFile","mkdir","readFile","rename","writeFile","dirname","readFile","isNodeError","path","mkdir","dirname","writeFile","rename","existsSync","readFile","basename","join","fg","existsSync","join","existsSync","join","join","existsSync","readFile","fg","basename"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/hook.ts","../src/commands/hook/script.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nimport {\n copyToStableHooksDir,\n resolveHookScriptPath,\n resolveSettingsPath,\n type ScriptResolution,\n} from \"./hook/script.js\";\nimport { isCursorEnabled } from \"../update/config.js\";\n\n/**\n * `almanac hook install|uninstall|status` — wires the bundled\n * `hooks/almanac-capture.sh` into `~/.claude/settings.json` as a\n * `SessionEnd` hook.\n *\n * Design notes:\n *\n * - **Schema.** Claude Code validates `settings.json` against a strict\n * schema: each entry in an event array (like `SessionEnd`) is a\n * `{matcher, hooks: [...]}` container, and the actual command objects\n * live in the nested `hooks` array. v0.1.0–v0.1.4 wrote command objects\n * directly at the event-array level; newer Claude Code versions now\n * reject that shape. We produce the wrapped form on install, and when\n * encountering a legacy unwrapped entry that we recognize as ours (by\n * `command` ending in `almanac-capture.sh`) we migrate it on next\n * install. `SessionEnd` never uses the `matcher` field to discriminate\n * anything — we always emit an empty `matcher: \"\"` (matches\n * everything, which is what session-end lifecycle hooks want).\n *\n * - **Idempotent.** `install` twice leaves one entry, not two. We match by\n * `command` string equality on the inner `hooks[]` entries. If the user\n * replaces our absolute path with a symlink pointing at the same\n * script, we'll treat it as foreign. That's acceptable; the `status`\n * output shows the path we'd use, so the user can reconcile manually.\n *\n * - **Refuse foreign entries.** If `SessionEnd` is already populated with\n * a command we don't recognize, we print the existing value and exit\n * non-zero. Claude Code lets users wire their own hooks (notifications,\n * git autocommit scripts, etc.) and silently replacing them would be\n * rude. Foreign wrapped containers that don't reference our script are\n * preserved byte-for-byte.\n *\n * - **Atomic write.** `settings.json` is small but heavily touched by\n * Claude Code. Writing via tmp-file + rename avoids corrupting the file\n * if we crash mid-write.\n *\n * - **Non-interactive.** No prompts, no confirmations. The caller is\n * already making an intentional choice by running `almanac hook\n * install`.\n */\n\nexport interface HookCommandOptions {\n /** Which agent app to install hooks for. Default keeps legacy Claude behavior. */\n source?: \"claude\" | \"codex\" | \"cursor\" | \"all\";\n /**\n * Override the hook script path. Production code leaves this undefined\n * and we resolve the bundled `hooks/almanac-capture.sh`. Tests pass a\n * fixture path to avoid depending on the runtime-install layout.\n */\n hookScriptPath?: string;\n /**\n * Override `~/.claude/settings.json`. Tests sandbox this to a tmpdir;\n * production code leaves it undefined.\n */\n settingsPath?: string;\n /**\n * Override the stable hooks directory where we copy the script.\n * Defaults to `~/.claude/hooks/`. Tests sandbox this to a tmpdir.\n *\n * Bug #1 fix: we always copy the bundled script to this stable path\n * before writing it into settings.json. This way the settings entry\n * points at a user-owned location that survives npm version bumps,\n * npx cache evictions, and nvm version switches — instead of an\n * ephemeral path inside ~/.npm/_npx/<sha>/... or the nvm-versioned\n * node_modules/.\n */\n stableHooksDir?: string;\n}\n\nexport interface HookCommandResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst HOOK_TIMEOUT_SECONDS = 10;\n\n/** A single command invocation inside a wrapper's `hooks[]` array. */\ninterface HookCommand {\n type: \"command\";\n command: string;\n timeout?: number;\n}\n\n/** A wrapped SessionEnd entry per Claude Code's schema. */\ninterface WrappedEntry {\n matcher: string;\n hooks: HookCommand[];\n}\n\n/**\n * What we read from `settings.hooks.SessionEnd`. During a read we may\n * encounter the legacy unwrapped shape (`HookCommand` directly) written\n * by v0.1.0–v0.1.4 — we recognize and migrate it. Unknown entries we\n * can't classify are preserved as-is via `unknown`.\n */\ntype RawEntry = WrappedEntry | HookCommand | unknown;\n\n/**\n * Claude Code's `settings.json` is a free-form JSON object; we only care\n * about the `hooks.SessionEnd` array. Preserve everything else verbatim\n * so we don't drop user settings when we write the file back.\n */\ntype SettingsJson = Record<string, unknown> & {\n hooks?: Record<string, RawEntry[] | undefined>;\n};\n\n/**\n * Heuristic: does this command path look like one we installed?\n *\n * We match on the filename `almanac-capture.sh` regardless of the parent\n * directory. This covers:\n * - the stable path: `~/.claude/hooks/almanac-capture.sh`\n * - legacy paths from v0.1.0–v0.1.5: inside the nvm node_modules or\n * npx cache\n * The stable path is what new installs produce; legacy paths are what\n * we migrate when the user runs `almanac hook install` again.\n */\nfunction isOurCommandPath(command: string): boolean {\n return command.endsWith(\"almanac-capture.sh\");\n}\n\n/**\n * Classify a raw SessionEnd entry. Wrapped entries are the canonical\n * shape; unwrapped-command entries are legacy output from v0.1.0–v0.1.4.\n * Anything else (random user JSON) is `unknown` and we leave it alone.\n */\ntype Classified =\n | { kind: \"wrapped\"; entry: WrappedEntry }\n | { kind: \"legacy\"; entry: HookCommand }\n | { kind: \"unknown\"; entry: unknown };\n\nfunction classifyEntry(raw: RawEntry): Classified {\n if (raw === null || typeof raw !== \"object\") {\n return { kind: \"unknown\", entry: raw };\n }\n const obj = raw as Record<string, unknown>;\n if (Array.isArray(obj.hooks)) {\n // Wrapped shape. `matcher` may be absent in hand-edited files; treat\n // absent as \"\" so we don't throw on slightly malformed input.\n const matcher = typeof obj.matcher === \"string\" ? obj.matcher : \"\";\n const hooks: HookCommand[] = [];\n for (const h of obj.hooks as unknown[]) {\n if (h !== null && typeof h === \"object\") {\n const ho = h as Record<string, unknown>;\n if (ho.type === \"command\" && typeof ho.command === \"string\") {\n const cmd: HookCommand = {\n type: \"command\",\n command: ho.command,\n };\n if (typeof ho.timeout === \"number\") cmd.timeout = ho.timeout;\n hooks.push(cmd);\n }\n }\n }\n return { kind: \"wrapped\", entry: { matcher, hooks } };\n }\n if (obj.type === \"command\" && typeof obj.command === \"string\") {\n // Legacy unwrapped shape — v0.1.0–v0.1.4 wrote this form.\n const cmd: HookCommand = {\n type: \"command\",\n command: obj.command as string,\n };\n if (typeof obj.timeout === \"number\") cmd.timeout = obj.timeout;\n return { kind: \"legacy\", entry: cmd };\n }\n return { kind: \"unknown\", entry: raw };\n}\n\n/** True when the entry references our script and is safely ours to manage. */\nfunction isOurWrapped(entry: WrappedEntry): boolean {\n return entry.hooks.some((h) => isOurCommandPath(h.command));\n}\n\nexport async function runHookInstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const bundled = resolveHookScriptPath(options);\n if (!bundled.ok) {\n return { stdout: \"\", stderr: `almanac: ${bundled.error}\\n`, exitCode: 1 };\n }\n\n // Copy the bundled hook script to a stable user-owned location before\n // writing that path into settings.json. This is the Bug #1 fix:\n //\n // OLD behavior: settings.json pointed at the bundled path (inside\n // ~/.nvm/versions/node/<ver>/lib/node_modules/codealmanac/hooks/... or\n // ~/.npm/_npx/<sha>/node_modules/codealmanac/hooks/...). When the user\n // switches Node versions or the npx cache is evicted, the path breaks\n // silently and captures stop firing.\n //\n // NEW behavior: we copy almanac-capture.sh to ~/.claude/hooks/ (same\n // directory Claude Code uses for its own built-in hooks, always present)\n // and point settings.json there. The stable path is independent of\n // Node version and npm cache state. When the user upgrades codealmanac,\n // `almanac hook install` copies a fresh script and updates settings.json\n // if the path changed.\n //\n // When `hookScriptPath` is explicitly provided (test injection), the\n // caller has already specified the destination path — skip the copy and\n // use that path directly. The stable-copy concern only applies to the\n // production flow where we resolved from the bundled package layout.\n const script: ScriptResolution = options.hookScriptPath !== undefined\n ? bundled // already the caller-provided path, no copy needed\n : await copyToStableHooksDir(bundled.path, options);\n if (!script.ok) {\n return { stdout: \"\", stderr: `almanac: ${script.error}\\n`, exitCode: 1 };\n }\n\n const source = options.source ?? \"claude\";\n if (source === \"all\") {\n const results = [\n await installClaudeHook(options, script.path),\n await installGenericHook({\n label: \"Codex Stop\",\n settingsPath: path.join(homedir(), \".codex\", \"hooks.json\"),\n eventName: \"Stop\",\n shape: \"wrapped\",\n scriptPath: script.path,\n }),\n ];\n if (isCursorEnabled()) {\n results.push(await installGenericHook({\n label: \"Cursor sessionEnd\",\n settingsPath: path.join(homedir(), \".cursor\", \"hooks.json\"),\n eventName: \"sessionEnd\",\n shape: \"flat\",\n scriptPath: script.path,\n }));\n }\n const failed = results.find((r) => r.exitCode !== 0);\n if (failed !== undefined) return failed;\n return {\n stdout: results.map((r) => r.stdout.trimEnd()).join(\"\\n\") + \"\\n\",\n stderr: \"\",\n exitCode: 0,\n };\n }\n if (source === \"codex\") {\n return await installGenericHook({\n label: \"Codex Stop\",\n settingsPath: path.join(homedir(), \".codex\", \"hooks.json\"),\n eventName: \"Stop\",\n shape: \"wrapped\",\n scriptPath: script.path,\n });\n }\n if (source === \"cursor\") {\n if (!isCursorEnabled()) {\n return {\n stdout: \"\",\n stderr:\n \"almanac: cursor hooks are disabled. Set CODEALMANAC_ENABLE_CURSOR=1 to enable experimental Cursor support.\\n\",\n exitCode: 1,\n };\n }\n return await installGenericHook({\n label: \"Cursor sessionEnd\",\n settingsPath: path.join(homedir(), \".cursor\", \"hooks.json\"),\n eventName: \"sessionEnd\",\n shape: \"flat\",\n scriptPath: script.path,\n });\n }\n\n return await installClaudeHook(options, script.path);\n}\n\nasync function installClaudeHook(\n options: HookCommandOptions,\n scriptPath: string,\n): Promise<HookCommandResult> {\n\n const settingsPath = resolveSettingsPath(options);\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n // Walk existing entries and split them into buckets:\n // - `preserved` — foreign wrapped/unknown entries we leave alone.\n // - `oursAlready` — a wrapped entry that already points at OUR exact\n // script path (makes install a no-op).\n // - `oursStale` — a wrapped or legacy entry that references our\n // capture script but at a different absolute path\n // (old install, `npm i` moved us) or in the legacy\n // unwrapped shape. We'll collapse these into a\n // single fresh entry at the new path.\n const preserved: RawEntry[] = [];\n let oursAlready: WrappedEntry | null = null;\n const staleCount = { n: 0 };\n\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n if (!isOurWrapped(c.entry)) {\n preserved.push(raw);\n continue;\n }\n // Entry belongs to us. Does it already point at the exact script\n // path? If every command in its `hooks[]` that looks like ours is\n // already at `script.path`, it's up to date.\n const exactMatch = c.entry.hooks.some(\n (h) => h.command === scriptPath,\n );\n if (exactMatch && oursAlready === null) {\n oursAlready = c.entry;\n } else {\n staleCount.n += 1;\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n // Legacy unwrapped entry of ours — always migrate to wrapped.\n staleCount.n += 1;\n } else {\n // Foreign legacy entry (user had their own script before\n // settings.json required wrapping). Leave it alone.\n preserved.push(raw);\n }\n } else {\n // Unknown shape — we can't classify it. Preserve verbatim.\n preserved.push(raw);\n }\n }\n\n // If every non-ours entry is a foreign unwrapped command (not a\n // wrapped one) we refuse to touch the file — Claude Code's newer\n // schema will already reject such files, but surfacing it here lets\n // the user clean up before we stack our entry on top. Wrapped foreign\n // entries are fine to leave alongside ours.\n const foreignLegacy = preserved.filter((raw) => {\n const c = classifyEntry(raw);\n return c.kind === \"legacy\";\n });\n if (foreignLegacy.length > 0) {\n const lines = foreignLegacy\n .map((raw) => {\n const c = classifyEntry(raw);\n if (c.kind === \"legacy\") return ` - ${c.entry.command}`;\n return \" - <unrecognized>\";\n })\n .join(\"\\n\");\n return {\n stdout: \"\",\n stderr:\n `almanac: SessionEnd has a foreign legacy entry:\\n${lines}\\n` +\n `Remove or rewrap it manually in ${settingsPath} before installing.\\n`,\n exitCode: 1,\n };\n }\n\n if (oursAlready !== null && staleCount.n === 0) {\n return {\n stdout: `almanac: SessionEnd hook already installed at ${scriptPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Build the fresh wrapped entry and append to preserved foreign\n // entries. Stale entries of ours are dropped (we only ever want a\n // single active entry; multiple copies of the capture hook would\n // double-fire on session end).\n const fresh: WrappedEntry = {\n matcher: \"\",\n hooks: [\n {\n type: \"command\",\n command: scriptPath,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ],\n };\n\n const newEntries: RawEntry[] = [...preserved, fresh];\n\n settings.hooks = { ...(settings.hooks ?? {}), SessionEnd: newEntries };\n await writeSettings(settingsPath, settings);\n\n return {\n stdout:\n `almanac: SessionEnd hook installed\\n` +\n ` script: ${scriptPath}\\n` +\n ` settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nasync function installGenericHook(args: {\n label: string;\n settingsPath: string;\n eventName: string;\n shape: \"flat\" | \"wrapped\";\n scriptPath: string;\n}): Promise<HookCommandResult> {\n const settings = await readSettings(args.settingsPath);\n const hooksObj =\n settings.hooks !== undefined &&\n settings.hooks !== null &&\n typeof settings.hooks === \"object\"\n ? settings.hooks\n : {};\n const existing = Array.isArray(hooksObj[args.eventName])\n ? (hooksObj[args.eventName] as RawEntry[])\n : [];\n const kept = existing.filter((entry) => !entryHasOurCommand(entry));\n const already = existing.some((entry) =>\n entryHasExactCommand(entry, args.scriptPath),\n );\n if (already && kept.length === existing.length - 1) {\n return {\n stdout: `almanac: ${args.label} hook already installed at ${args.scriptPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n const fresh =\n args.shape === \"wrapped\"\n ? {\n hooks: [\n {\n type: \"command\",\n command: args.scriptPath,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ],\n }\n : {\n command: args.scriptPath,\n timeout: HOOK_TIMEOUT_SECONDS,\n };\n hooksObj[args.eventName] = [\n ...kept,\n fresh,\n ];\n settings.hooks = hooksObj;\n await writeSettings(args.settingsPath, settings);\n if (args.label.startsWith(\"Codex \")) {\n await ensureCodexHooksFeature(path.join(homedir(), \".codex\", \"config.toml\"));\n }\n return {\n stdout:\n `almanac: ${args.label} hook installed\\n` +\n ` script: ${args.scriptPath}\\n` +\n ` settings: ${args.settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nfunction entryHasOurCommand(entry: unknown): boolean {\n return collectHookCommands(entry).some(isOurCommandPath);\n}\n\nfunction entryHasExactCommand(entry: unknown, command: string): boolean {\n return collectHookCommands(entry).some((candidate) => candidate === command);\n}\n\nfunction collectHookCommands(entry: unknown): string[] {\n if (entry === null || typeof entry !== \"object\") return [];\n const obj = entry as Record<string, unknown>;\n const direct = typeof obj.command === \"string\" ? [obj.command] : [];\n const nested = Array.isArray(obj.hooks)\n ? obj.hooks.flatMap((hook) => collectHookCommands(hook))\n : [];\n return [...direct, ...nested];\n}\n\nasync function ensureCodexHooksFeature(configPath: string): Promise<void> {\n let body = \"\";\n if (existsSync(configPath)) {\n body = await readFile(configPath, \"utf8\");\n }\n if (/^\\s*codex_hooks\\s*=\\s*true\\s*$/m.test(body)) return;\n\n const next = setTomlFeatureFlag(body, \"codex_hooks\", true);\n await mkdir(path.dirname(configPath), { recursive: true });\n const tmp = `${configPath}.almanac-tmp-${process.pid}`;\n await writeFile(tmp, next.endsWith(\"\\n\") ? next : `${next}\\n`, \"utf8\");\n await rename(tmp, configPath);\n}\n\nfunction setTomlFeatureFlag(\n body: string,\n key: string,\n value: boolean,\n): string {\n const desired = `${key} = ${value ? \"true\" : \"false\"}`;\n const lines = body.split(/\\r?\\n/);\n let featuresStart = -1;\n let featuresEnd = lines.length;\n\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*\\[features\\]\\s*$/.test(lines[i] ?? \"\")) {\n featuresStart = i;\n continue;\n }\n if (featuresStart !== -1 && i > featuresStart && /^\\s*\\[.*\\]\\s*$/.test(lines[i] ?? \"\")) {\n featuresEnd = i;\n break;\n }\n }\n\n if (featuresStart === -1) {\n const prefix = body.trim().length === 0 ? \"\" : `${body.trimEnd()}\\n\\n`;\n return `${prefix}[features]\\n${desired}\\n`;\n }\n\n const keyPattern = new RegExp(`^\\\\s*${escapeRegex(key)}\\\\s*=`);\n for (let i = featuresStart + 1; i < featuresEnd; i++) {\n if (keyPattern.test(lines[i] ?? \"\")) {\n lines[i] = desired;\n return lines.join(\"\\n\");\n }\n }\n\n lines.splice(featuresStart + 1, 0, desired);\n return lines.join(\"\\n\");\n}\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport async function runHookUninstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const settingsPath = resolveSettingsPath(options);\n\n if (!existsSync(settingsPath)) {\n return {\n stdout: `almanac: SessionEnd hook not installed (no settings file)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n const kept: RawEntry[] = [];\n let removed = 0;\n\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n // Filter out our command(s) from the inner hooks array. Keep\n // anything else in the array intact — a foreign wrapper that\n // happened to include our script alongside its own commands\n // (unusual, but survivable) loses our entry and keeps theirs.\n const innerKept = c.entry.hooks.filter(\n (h) => !isOurCommandPath(h.command),\n );\n const innerRemoved = c.entry.hooks.length - innerKept.length;\n removed += innerRemoved;\n if (innerKept.length === 0) {\n // Only drop the outer wrapper when it was entirely ours. A\n // foreign wrapper that never contained our script stays verbatim\n // below (handled by `innerRemoved === 0`, which leaves\n // `innerKept.length === c.entry.hooks.length`, hence we fall\n // through to the else-branch).\n if (innerRemoved === 0) kept.push(raw);\n // else: fully owned by us, drop the container.\n } else if (innerRemoved === 0) {\n // Untouched foreign wrapper — preserve the raw object to keep\n // any fields (like matcher) byte-for-byte.\n kept.push(raw);\n } else {\n // Partial: rebuild with just the kept inner entries, preserving\n // the original matcher string.\n kept.push({ matcher: c.entry.matcher, hooks: innerKept });\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n removed += 1;\n } else {\n kept.push(raw);\n }\n } else {\n kept.push(raw);\n }\n }\n\n if (removed === 0) {\n return {\n stdout: `almanac: SessionEnd hook not installed\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n if (settings.hooks !== undefined) {\n if (kept.length === 0) {\n // Empty SessionEnd array confuses some linters; drop the key when\n // nothing's left.\n const { SessionEnd: _dropped, ...rest } = settings.hooks;\n void _dropped;\n settings.hooks = rest;\n } else {\n settings.hooks = { ...settings.hooks, SessionEnd: kept };\n }\n\n // If `hooks` itself is now empty (user had only our SessionEnd entry\n // and no other hook categories), drop the `hooks` key entirely so\n // uninstall leaves the settings file in the same shape it would be\n // in had we never run install. An empty `\"hooks\": {}` is an obvious\n // breadcrumb in commit diffs.\n if (Object.keys(settings.hooks).length === 0) {\n delete settings.hooks;\n }\n }\n\n await writeSettings(settingsPath, settings);\n\n return {\n stdout: `almanac: SessionEnd hook removed\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nexport async function runHookStatus(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const script = resolveHookScriptPath(options);\n const settingsPath = resolveSettingsPath(options);\n\n if (!existsSync(settingsPath)) {\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath} (does not exist)\\n` +\n (script.ok ? `script would be: ${script.path}\\n` : \"\"),\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const settings = await readSettings(settingsPath);\n const existing = settings.hooks?.SessionEnd ?? [];\n\n // Walk the array looking for any entry (wrapped or legacy) that\n // references our capture script. Gathering foreign entries separately\n // lets us show them to the user if nothing of ours was found.\n let ourCommand: string | null = null;\n const foreignSummary: string[] = [];\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n for (const h of c.entry.hooks) {\n if (isOurCommandPath(h.command)) {\n ourCommand ??= h.command;\n } else {\n foreignSummary.push(h.command);\n }\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n ourCommand ??= c.entry.command;\n } else {\n foreignSummary.push(c.entry.command);\n }\n }\n }\n\n if (ourCommand === null) {\n const foreignLines = foreignSummary\n .map((c) => ` - ${c}`)\n .join(\"\\n\");\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath}\\n` +\n (foreignSummary.length > 0\n ? `(${foreignSummary.length} foreign entr${foreignSummary.length === 1 ? \"y\" : \"ies\"} present:\\n${foreignLines})\\n`\n : \"\") +\n (script.ok ? `script would be: ${script.path}\\n` : \"\"),\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout:\n `SessionEnd hook: installed\\n` +\n `script: ${ourCommand}\\n` +\n `settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Settings JSON helpers ───────────────────────────────────────────\n\nasync function readSettings(settingsPath: string): Promise<SettingsJson> {\n if (!existsSync(settingsPath)) return {};\n try {\n const raw = await readFile(settingsPath, \"utf8\");\n if (raw.trim().length === 0) return {};\n const parsed = JSON.parse(raw) as unknown;\n if (parsed === null || typeof parsed !== \"object\") return {};\n return parsed as SettingsJson;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`failed to read ${settingsPath}: ${msg}`);\n }\n}\n\nasync function writeSettings(\n settingsPath: string,\n settings: SettingsJson,\n): Promise<void> {\n const dir = path.dirname(settingsPath);\n await mkdir(dir, { recursive: true });\n\n // Atomic write: JSON.stringify → tmp file → rename. `rename` within the\n // same filesystem is atomic on POSIX; Claude Code never sees a partial\n // file. Formatted with 2-space indent to match the existing settings.\n const tmp = `${settingsPath}.almanac-tmp-${process.pid}`;\n const body = `${JSON.stringify(settings, null, 2)}\\n`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, settingsPath);\n}\n","import { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface HookPathOptions {\n hookScriptPath?: string;\n settingsPath?: string;\n stableHooksDir?: string;\n}\n\nexport type ScriptResolution =\n | { ok: true; path: string }\n | { ok: false; error: string };\n\nexport function resolveSettingsPath(options: HookPathOptions): string {\n if (options.settingsPath !== undefined) return options.settingsPath;\n return path.join(homedir(), \".claude\", \"settings.json\");\n}\n\n/**\n * Copy the bundled hook script to `~/.claude/hooks/almanac-capture.sh`.\n *\n * This stable, user-owned destination survives Node version switches and\n * npm/npx cache evictions. The copy is idempotent: if bytes already match\n * we skip writing so repeated setup runs do not bump mtimes.\n */\nexport async function copyToStableHooksDir(\n bundledPath: string,\n options: HookPathOptions,\n): Promise<ScriptResolution> {\n const stableHooksDir =\n options.stableHooksDir ?? path.join(homedir(), \".claude\", \"hooks\");\n const dest = path.join(stableHooksDir, \"almanac-capture.sh\");\n\n try {\n await mkdir(stableHooksDir, { recursive: true });\n const srcBytes = await readFile(bundledPath);\n let needsCopy = true;\n if (existsSync(dest)) {\n try {\n const destBytes = await readFile(dest);\n if (srcBytes.equals(destBytes)) needsCopy = false;\n } catch {\n // Can't read dest — overwrite.\n }\n }\n if (needsCopy) {\n await copyFile(bundledPath, dest);\n }\n return { ok: true, path: dest };\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n ok: false,\n error: `could not copy hook script to ${dest}: ${msg}`,\n };\n }\n}\n\n/**\n * Locate the bundled `hooks/almanac-capture.sh`. Mirrors\n * `resolvePromptsDir` from `src/agent/prompts.ts`: two plausible layouts\n * (installed dist vs. source dev), probe each.\n */\nexport function resolveHookScriptPath(\n options: HookPathOptions,\n): ScriptResolution {\n if (options.hookScriptPath !== undefined) {\n return { ok: true, path: options.hookScriptPath };\n }\n\n const here = path.dirname(fileURLToPath(import.meta.url));\n\n const candidates = [\n // Bundled: `.../codealmanac/dist/codealmanac.js` → `../hooks/…`\n path.resolve(here, \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Source after ts-node-style module layout or nested dist helpers.\n path.resolve(here, \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Source: `.../codealmanac/src/commands/hook/script.ts` → `../../../hooks/…`\n path.resolve(here, \"..\", \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Defensive nested fallback.\n path.resolve(here, \"..\", \"..\", \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return { ok: true, path: candidate };\n }\n }\n\n return {\n ok: false,\n error:\n `could not locate hooks/almanac-capture.sh. Tried:\\n` +\n candidates.map((c) => ` - ${c}`).join(\"\\n\"),\n };\n}\n"],"mappings":"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","names":["existsSync","mkdir","readFile","homedir","path","path","homedir","existsSync","readFile","mkdir"]}
|