codealmanac 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codealmanac.js +2 -4
- package/dist/codealmanac.js.map +1 -1
- package/package.json +2 -2
package/dist/codealmanac.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/commands/bootstrap.ts","../src/agent/auth.ts","../src/agent/prompts.ts","../src/agent/sdk.ts","../src/paths.ts","../src/commands/init.ts","../src/slug.ts","../src/registry/index.ts","../src/commands/capture.ts","../src/indexer/frontmatter.ts","../src/commands/hook.ts","../src/commands/health.ts","../src/indexer/duration.ts","../src/indexer/index.ts","../src/topics/yaml.ts","../src/indexer/paths.ts","../src/indexer/schema.ts","../src/indexer/wikilinks.ts","../src/indexer/resolveWiki.ts","../src/topics/dag.ts","../src/commands/info.ts","../src/commands/list.ts","../src/commands/path.ts","../src/commands/reindex.ts","../src/commands/search.ts","../src/commands/show.ts","../src/topics/frontmatterRewrite.ts","../src/topics/paths.ts","../src/commands/tag.ts","../src/commands/topics.ts","../src/registry/autoregister.ts","../bin/codealmanac.ts"],"sourcesContent":["import { basename } from \"node:path\";\n\nimport { Command } from \"commander\";\n\nimport { runBootstrap } from \"./commands/bootstrap.js\";\nimport { runCapture } from \"./commands/capture.js\";\nimport {\n runHookInstall,\n runHookStatus,\n runHookUninstall,\n} from \"./commands/hook.js\";\nimport { runHealth } from \"./commands/health.js\";\nimport { initWiki } from \"./commands/init.js\";\nimport { runInfo } from \"./commands/info.js\";\nimport { listWikis } from \"./commands/list.js\";\nimport { runPath } from \"./commands/path.js\";\nimport { runReindex } from \"./commands/reindex.js\";\nimport { runSearch } from \"./commands/search.js\";\nimport { runShow } from \"./commands/show.js\";\nimport { runTag, runUntag } from \"./commands/tag.js\";\nimport {\n runTopicsCreate,\n runTopicsDelete,\n runTopicsDescribe,\n runTopicsLink,\n runTopicsList,\n runTopicsRename,\n runTopicsShow,\n runTopicsUnlink,\n} from \"./commands/topics.js\";\nimport { autoRegisterIfNeeded } from \"./registry/autoregister.js\";\n\n/**\n * Entry point. `bin/codealmanac.ts` hands us `process.argv` and any errors\n * bubble up to the shim for a uniform \"almanac: <message>\" output format.\n *\n * Auto-registration runs before most commands. Two exceptions:\n * - `init` registers explicitly, so auto-register would be redundant and\n * would race with init's own write.\n * - `list --drop <name>` shouldn't silently re-register the repo whose\n * entry the user is trying to remove.\n */\nexport async function run(argv: string[]): Promise<void> {\n const program = new Command();\n\n // Both `almanac` and `codealmanac` point at the same entry (see\n // `package.json#bin`). Match the help header to whatever the user\n // invoked so `codealmanac --help` doesn't incongruously read\n // `Usage: almanac …`. Fall back to `almanac` when argv[1] isn't\n // available (programmatic invocation, tests).\n const invoked = argv[1] !== undefined ? basename(argv[1]) : \"almanac\";\n const programName =\n invoked === \"codealmanac\" ? \"codealmanac\" : \"almanac\";\n\n program\n .name(programName)\n .description(\n \"codealmanac — a living wiki for codebases, maintained by AI agents\",\n )\n .version(\"0.1.0\", \"-v, --version\", \"print version\");\n\n program\n .command(\"init\")\n .description(\"scaffold .almanac/ in the current directory and register it\")\n .option(\"--name <name>\", \"wiki name (defaults to the directory name)\")\n .option(\"--description <text>\", \"one-line description of this wiki\")\n .action(async (opts: { name?: string; description?: string }) => {\n const result = await initWiki({\n cwd: process.cwd(),\n name: opts.name,\n description: opts.description,\n });\n const verb = result.created ? \"initialized\" : \"updated\";\n process.stdout.write(\n `${verb} wiki \"${result.entry.name}\" at ${result.almanacDir}\\n`,\n );\n });\n\n program\n .command(\"list\")\n .description(\"list registered wikis\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\n \"--drop <name>\",\n \"remove a wiki from the registry (the only way entries are ever removed)\",\n )\n .action(async (opts: { json?: boolean; drop?: string }) => {\n // Auto-register only makes sense for default/JSON listing. Skipping\n // it on --drop keeps the removal operation predictable — the user's\n // intent is to shrink the registry, not grow it mid-command.\n if (opts.drop === undefined) {\n await autoRegisterIfNeeded(process.cwd());\n }\n const result = await listWikis(opts);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) {\n process.exitCode = result.exitCode;\n }\n });\n\n program\n .command(\"search [query]\")\n .description(\"query pages by text, topic, file mentions, or freshness\")\n .option(\n \"--topic <name...>\",\n \"filter by topic (repeat for intersection)\",\n collectOption,\n [] as string[],\n )\n .option(\n \"--mentions <path>\",\n \"pages referencing this file or folder (trailing / = folder)\",\n )\n .option(\n \"--since <duration>\",\n \"updated within duration, by file mtime (e.g. 2w, 30d)\",\n )\n .option(\n \"--stale <duration>\",\n \"NOT updated within duration, by file mtime\",\n )\n .option(\"--orphan\", \"pages with no topics\")\n .option(\"--include-archive\", \"include archived pages\")\n .option(\"--archived\", \"archived pages only\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\"--limit <n>\", \"cap results\", parsePositiveInt)\n .action(\n async (\n query: string | undefined,\n opts: {\n topic?: string[];\n mentions?: string;\n since?: string;\n stale?: string;\n orphan?: boolean;\n includeArchive?: boolean;\n archived?: boolean;\n wiki?: string;\n json?: boolean;\n limit?: number;\n },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runSearch({\n cwd: process.cwd(),\n query,\n topics: opts.topic ?? [],\n mentions: opts.mentions,\n since: opts.since,\n stale: opts.stale,\n orphan: opts.orphan,\n includeArchive: opts.includeArchive,\n archived: opts.archived,\n wiki: opts.wiki,\n json: opts.json,\n limit: opts.limit,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"show [slug]\")\n .description(\"print the markdown content of a page\")\n .option(\"--stdin\", \"read slugs from stdin (one per line)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n slug: string | undefined,\n opts: { stdin?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runShow({\n cwd: process.cwd(),\n slug,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n wiki: opts.wiki,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"path [slug]\")\n .description(\"resolve a slug to its absolute file path\")\n .option(\"--stdin\", \"read slugs from stdin (one per line)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n slug: string | undefined,\n opts: { stdin?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runPath({\n cwd: process.cwd(),\n slug,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n wiki: opts.wiki,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"info [slug]\")\n .description(\"print metadata for a page (topics, refs, links, lineage)\")\n .option(\"--stdin\", \"read slugs from stdin (one per line)\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n slug: string | undefined,\n opts: { stdin?: boolean; json?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runInfo({\n cwd: process.cwd(),\n slug,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n json: opts.json,\n wiki: opts.wiki,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"reindex\")\n .description(\"force a full rebuild of .almanac/index.db\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(async (opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runReindex({\n cwd: process.cwd(),\n wiki: opts.wiki,\n });\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n });\n\n // ─── topics (sub-tree) ────────────────────────────────────────────\n const topics = program\n .command(\"topics\")\n .description(\"manage the topic DAG (list, create, link, rename, delete)\");\n\n // Default action for `almanac topics` with no subcommand: list.\n topics\n .command(\"list\", { isDefault: true })\n .description(\"list all topics with page counts\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .option(\"--json\", \"emit structured JSON\")\n .action(async (opts: { wiki?: string; json?: boolean }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsList({\n cwd: process.cwd(),\n wiki: opts.wiki,\n json: opts.json,\n });\n emit(result);\n });\n\n topics\n .command(\"show <slug>\")\n .description(\"print a topic's metadata, parents, children, and pages\")\n .option(\"--descendants\", \"include pages tagged with descendant topics\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .option(\"--json\", \"emit structured JSON\")\n .action(\n async (\n slug: string,\n opts: { descendants?: boolean; wiki?: string; json?: boolean },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsShow({\n cwd: process.cwd(),\n slug,\n descendants: opts.descendants,\n wiki: opts.wiki,\n json: opts.json,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"create <name>\")\n .description(\"create a topic (rejects if --parent slug does not exist)\")\n .option(\n \"--parent <slug>\",\n \"parent topic slug (repeat for multiple parents)\",\n collectOption,\n [] as string[],\n )\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n name: string,\n opts: { parent?: string[]; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsCreate({\n cwd: process.cwd(),\n name,\n parents: opts.parent,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"link <child> <parent>\")\n .description(\"add a DAG edge (cycle-checked)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (child: string, parent: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsLink({\n cwd: process.cwd(),\n child,\n parent,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"unlink <child> <parent>\")\n .description(\"remove a DAG edge\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (child: string, parent: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsUnlink({\n cwd: process.cwd(),\n child,\n parent,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"rename <old> <new>\")\n .description(\"rename a topic; rewrites every affected page's frontmatter\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (oldSlug: string, newSlug: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsRename({\n cwd: process.cwd(),\n oldSlug,\n newSlug,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"delete <slug>\")\n .description(\"delete a topic; untags every affected page\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(async (slug: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsDelete({\n cwd: process.cwd(),\n slug,\n wiki: opts.wiki,\n });\n emit(result);\n });\n\n topics\n .command(\"describe <slug> <text>\")\n .description(\"set a topic's one-line description\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (slug: string, text: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsDescribe({\n cwd: process.cwd(),\n slug,\n description: text,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n // ─── tag / untag ─────────────────────────────────────────────────\n program\n .command(\"tag [page] [topics...]\")\n .description(\"add topics to a page (auto-creates missing topics)\")\n .option(\"--stdin\", \"read page slugs from stdin (one per line)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n page: string | undefined,\n topicsArg: string[],\n opts: { stdin?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n // `--stdin <topic> [<topic>...]` shape: no positional page,\n // all positionals are topics. commander gives us `page` =\n // first positional and `topicsArg` = rest, so in --stdin mode\n // we prepend whatever landed in `page` to the topics list.\n const resolvedTopics = opts.stdin === true\n ? [page, ...topicsArg].filter(\n (t): t is string => typeof t === \"string\" && t.length > 0,\n )\n : topicsArg;\n const result = await runTag({\n cwd: process.cwd(),\n page: opts.stdin === true ? undefined : page,\n topics: resolvedTopics,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n program\n .command(\"untag <page> <topic>\")\n .description(\"remove a topic from a page's frontmatter\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (page: string, topic: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runUntag({\n cwd: process.cwd(),\n page,\n topic,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n // ─── bootstrap ───────────────────────────────────────────────────\n // Slice 4: first Claude Agent SDK integration. Spawns the bootstrap\n // agent on the current repo to create initial entity pages + README +\n // topic DAG. Requires ANTHROPIC_API_KEY; refuses on populated wikis\n // unless --force.\n program\n .command(\"bootstrap\")\n .description(\n \"spawn an agent to scan the repo and create initial wiki stubs (requires ANTHROPIC_API_KEY)\",\n )\n .option(\"--quiet\", \"suppress per-tool streaming; print only the final line\")\n .option(\"--model <model>\", \"override the agent model\")\n .option(\n \"--force\",\n \"overwrite an existing populated wiki (default: refuse)\",\n )\n .action(\n async (opts: { quiet?: boolean; model?: string; force?: boolean }) => {\n // No auto-register here: if this is a fresh repo the bootstrap\n // command handles init (and therefore registration) itself. If\n // the repo is already a wiki, capture/init have already\n // registered it.\n const result = await runBootstrap({\n cwd: process.cwd(),\n quiet: opts.quiet,\n model: opts.model,\n force: opts.force,\n });\n emit(result);\n },\n );\n\n // ─── capture ─────────────────────────────────────────────────────\n // Slice 5: writer + reviewer subagent on a Claude Code session\n // transcript. Refuses if no `.almanac/` exists (capture is for\n // maintaining wikis, not creating them). Transcript path resolution:\n // - explicit positional arg wins\n // - `--session <id>` matches by filename under ~/.claude/projects/\n // - otherwise auto-resolve the most recent transcript whose cwd\n // matches this repo\n program\n .command(\"capture [transcript]\")\n .description(\n \"capture knowledge from a Claude Code session transcript \" +\n \"(auto-resolves the most recent session for this repo when no \" +\n \"path is given; requires ANTHROPIC_API_KEY)\",\n )\n .option(\"--session <id>\", \"target a specific session by ID\")\n .option(\n \"--quiet\",\n \"suppress per-tool streaming; print only the final summary\",\n )\n .option(\"--model <model>\", \"override the agent model\")\n .action(\n async (\n transcript: string | undefined,\n opts: { session?: string; quiet?: boolean; model?: string },\n ) => {\n // Auto-register the repo on capture: the user may have cloned a\n // repo with `.almanac/` committed but never run init.\n await autoRegisterIfNeeded(process.cwd());\n const result = await runCapture({\n cwd: process.cwd(),\n transcriptPath: transcript,\n sessionId: opts.session,\n quiet: opts.quiet,\n model: opts.model,\n });\n emit(result);\n },\n );\n\n // ─── hook ─────────────────────────────────────────────────────────\n // Wires codealmanac into Claude Code's SessionEnd hook via\n // ~/.claude/settings.json. Non-interactive install/uninstall/status.\n const hook = program\n .command(\"hook\")\n .description(\n \"install, uninstall, or inspect the SessionEnd hook in ~/.claude/settings.json\",\n );\n\n hook\n .command(\"install\")\n .description(\"add a SessionEnd entry that runs 'almanac capture' on session end\")\n .action(async () => {\n const result = await runHookInstall();\n emit(result);\n });\n\n hook\n .command(\"uninstall\")\n .description(\"remove codealmanac's SessionEnd entry; leaves foreign entries alone\")\n .action(async () => {\n const result = await runHookUninstall();\n emit(result);\n });\n\n hook\n .command(\"status\")\n .description(\"report whether the SessionEnd hook is installed\")\n .action(async () => {\n const result = await runHookStatus();\n emit(result);\n });\n\n // ─── health ──────────────────────────────────────────────────────\n program\n .command(\"health\")\n .description(\"report wiki problems (orphans, dead refs, broken links, …)\")\n .option(\"--topic <name>\", \"scope to a topic + its descendants\")\n .option(\"--stale <duration>\", \"stale threshold (default 90d)\")\n .option(\"--stdin\", \"read page slugs from stdin (limit to these pages)\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (opts: {\n topic?: string;\n stale?: string;\n stdin?: boolean;\n json?: boolean;\n wiki?: string;\n }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runHealth({\n cwd: process.cwd(),\n topic: opts.topic,\n stale: opts.stale,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n json: opts.json,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n await program.parseAsync(argv);\n}\n\n/**\n * Uniform writer for commands that produce `{stdout, stderr, exitCode}`.\n * Used by the slice-3 topics/tag/health commands; older commands still\n * inline the same three lines and can be collapsed in a follow-up.\n */\nfunction emit(result: {\n stdout: string;\n stderr: string;\n exitCode: number;\n}): void {\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n if (result.stdout.length > 0) process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n}\n\n/**\n * Commander's built-in collectable option helper. Repeatable `--topic`\n * appends; a bare call with no previous value starts a fresh array.\n */\nfunction collectOption(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\nfunction parsePositiveInt(value: string): number {\n const n = Number.parseInt(value, 10);\n if (!Number.isFinite(n) || n < 0) {\n throw new Error(`invalid --limit \"${value}\" (expected a non-negative integer)`);\n }\n return n;\n}\n\n/**\n * Drain stdin to a string. Used by the `--stdin` flag on show/path/info.\n * We require an explicit opt-in rather than auto-detect TTY because\n * relying on `process.stdin.isTTY` makes the behavior surprising when\n * invoked from scripts with stdin redirected.\n */\nasync function readStdin(): Promise<string> {\n if (process.stdin.isTTY === true) return \"\";\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n","import { createWriteStream, existsSync, type WriteStream } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\n\nimport type { SDKMessage } from \"@anthropic-ai/claude-agent-sdk\";\n\nimport { assertClaudeAuth, type SpawnCliFn } from \"../agent/auth.js\";\nimport { loadPrompt } from \"../agent/prompts.js\";\nimport {\n runAgent,\n type AgentResult,\n type RunAgentOptions,\n} from \"../agent/sdk.js\";\nimport { findNearestAlmanacDir, getRepoAlmanacDir } from \"../paths.js\";\nimport { initWiki } from \"./init.js\";\n\nexport interface BootstrapOptions {\n cwd: string;\n /** Suppress per-tool-use streaming output; print only errors + final line. */\n quiet?: boolean;\n /** Override the agent model. Defaults to the SDK default (sonnet-4-6). */\n model?: string;\n /** Overwrite a populated wiki. Default refuses with a pointer at `capture`. */\n force?: boolean;\n /** Injectable agent runner — tests replace this with a fake. */\n runAgent?: (opts: RunAgentOptions) => Promise<AgentResult>;\n /**\n * Injectable subprocess spawner for the Claude auth-status check.\n * Tests substitute a stub that emits canned JSON without running the\n * bundled CLI. Production leaves this undefined and `assertClaudeAuth`\n * falls through to `defaultSpawnCli`.\n */\n spawnCli?: SpawnCliFn;\n /**\n * Clock injection, for tests. Otherwise `Date.now()` timestamps the\n * transcript log filename and defaults to the SDK's session_id once\n * we receive one (the filename is chosen BEFORE the agent starts so\n * we can stream to it).\n */\n now?: () => Date;\n}\n\nexport interface BootstrapResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Tools the bootstrap agent is permitted to use. Bootstrap reads the repo\n * (Read/Glob/Grep), runs quick inspection commands (Bash — scoped by the\n * prompt, not by the SDK — the agent isn't expected to do anything\n * destructive), and writes scaffolding (Write/Edit). Notably absent:\n * - `Agent` — bootstrap has no subagents (reviewer is slice 5).\n * - `WebFetch` / `WebSearch` — prompt is explicit that we work from the\n * repo, not the internet. Adding these would invite drift.\n * - MCP servers — none needed for a local filesystem scan.\n */\nconst BOOTSTRAP_TOOLS = [\"Read\", \"Write\", \"Edit\", \"Glob\", \"Grep\", \"Bash\"];\n\n/**\n * `almanac bootstrap` — first Claude Agent SDK integration.\n *\n * Flow:\n * 1. Auth gate (ANTHROPIC_API_KEY). Fail fast with a clean error.\n * 2. Resolve repo root (existing `.almanac/` or cwd).\n * 3. Refuse-if-populated unless --force.\n * 4. Auto-init silently if `.almanac/` doesn't exist yet.\n * 5. Load `prompts/bootstrap.md`.\n * 6. Run the agent with BOOTSTRAP_TOOLS, cwd = repo root.\n * 7. Stream tool-uses to stdout (unless --quiet); write the full raw\n * transcript to `.almanac/.bootstrap-<session>.log`.\n * 8. Print a final `[done]` / `[failed]` line with cost + turns.\n *\n * Non-zero exit on failure so shell users can pipe into `&&`.\n */\nexport async function runBootstrap(\n options: BootstrapOptions,\n): Promise<BootstrapResult> {\n // Fail before loading prompts so we don't do filesystem work on a request\n // that can't succeed. `assertClaudeAuth` accepts either subscription\n // OAuth (via the bundled SDK CLI) or `ANTHROPIC_API_KEY`; missing both\n // surfaces a two-option error and MUST exit non-zero so the SessionEnd\n // hook (which backgrounds the process and ignores stderr) doesn't\n // treat silent auth failure as success.\n try {\n await assertClaudeAuth(options.spawnCli);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n stdout: \"\",\n stderr: `almanac: ${msg}\\n`,\n exitCode: 1,\n };\n }\n\n // Repo root: honor an already-initialized wiki anywhere above us.\n // Otherwise treat `cwd` as the root for a fresh wiki.\n const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;\n const almanacDir = getRepoAlmanacDir(repoRoot);\n const pagesDir = join(almanacDir, \"pages\");\n\n // Refuse to clobber a populated wiki. `almanac capture` is the tool for\n // maintaining wikis after bootstrap.\n if (options.force !== true && existsSync(pagesDir)) {\n const existing = await countMarkdownPages(pagesDir);\n if (existing > 0) {\n return {\n stdout: \"\",\n stderr:\n `almanac: .almanac/ already initialized with ${existing} page${existing === 1 ? \"\" : \"s\"}. ` +\n \"Use 'almanac capture' instead, or --force to overwrite.\\n\",\n exitCode: 1,\n };\n }\n }\n\n // Auto-init silently if missing. `initWiki` is idempotent — re-running\n // on an existing wiki is a no-op for the README / pages dir.\n if (!existsSync(almanacDir)) {\n try {\n await initWiki({ cwd: repoRoot });\n } catch (err: unknown) {\n // Per the slice spec: auto-init failures should be loud. The user\n // needs to know init is broken, not see a cascading agent error.\n const msg = err instanceof Error ? err.message : String(err);\n return {\n stdout: \"\",\n stderr: `almanac: init failed during bootstrap: ${msg}\\n`,\n exitCode: 1,\n };\n }\n }\n\n const systemPrompt = await loadPrompt(\"bootstrap\");\n\n // Transcript log filename: timestamp-based so it's sortable. The session\n // ID from the SDK isn't known until the first message arrives — by then\n // we'd already want somewhere to stream to. Use a clock-derived prefix\n // that's still meaningful even on a run that fails before producing a\n // session_id.\n const now = options.now?.() ?? new Date();\n const logName = `.bootstrap-${formatTimestamp(now)}.log`;\n const logPath = join(almanacDir, logName);\n const logStream = createWriteStream(logPath, { flags: \"w\" });\n\n // The streaming formatter is what the user sees on stdout unless\n // --quiet is set. The raw log captures EVERYTHING (including\n // `stream_event` partials) for postmortem.\n const out = process.stdout;\n const formatter = new StreamingFormatter({\n write: (line: string) => {\n if (options.quiet !== true) out.write(line);\n },\n });\n\n const onMessage = (msg: SDKMessage): void => {\n // Write the raw message to the transcript. Keep one JSON per line so\n // the log is grep-able and can be re-parsed if needed.\n try {\n logStream.write(`${JSON.stringify(msg)}\\n`);\n } catch {\n // Serialization failures are non-fatal — we'd rather keep streaming\n // to stdout than crash because one message had a circular ref.\n }\n formatter.handle(msg);\n };\n\n const runner = options.runAgent ?? runAgent;\n\n const userPrompt = `Begin the bootstrap now. Working directory: ${repoRoot}.`;\n\n let result: AgentResult;\n try {\n result = await runner({\n systemPrompt,\n prompt: userPrompt,\n allowedTools: BOOTSTRAP_TOOLS,\n cwd: repoRoot,\n model: options.model,\n onMessage,\n });\n } finally {\n await closeStream(logStream);\n }\n\n const finalLine = formatFinalLine(result, logPath, repoRoot);\n\n if (result.success) {\n return {\n stdout: `${finalLine}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout: options.quiet === true ? \"\" : `${finalLine}\\n`,\n stderr: `almanac: bootstrap failed: ${result.error ?? \"unknown error\"}\\n`,\n exitCode: 1,\n };\n}\n\n/**\n * Format the final line the user sees. On success it's a one-liner with\n * cost + turns; on failure we still print the cost/turns so the user\n * knows what the partial run used. The log path is shown relative to\n * `repoRoot` to keep the line short.\n */\nfunction formatFinalLine(\n result: AgentResult,\n logPath: string,\n repoRoot: string,\n): string {\n const status = result.success ? \"done\" : \"failed\";\n const rel = relative(repoRoot, logPath);\n const cost = `$${result.cost.toFixed(3)}`;\n return `[${status}] cost: ${cost}, turns: ${result.turns} (transcript: ${rel})`;\n}\n\nasync function countMarkdownPages(pagesDir: string): Promise<number> {\n try {\n const entries = await readdir(pagesDir, { withFileTypes: true });\n return entries.filter((e) => e.isFile() && e.name.endsWith(\".md\")).length;\n } catch {\n return 0;\n }\n}\n\nfunction closeStream(stream: WriteStream): Promise<void> {\n return new Promise((resolve) => {\n stream.end(() => resolve());\n });\n}\n\nfunction formatTimestamp(d: Date): string {\n // YYYYMMDD-HHMMSS, local time. Collision-proof enough for human use\n // (one bootstrap per second, per repo is an acceptable ceiling).\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n const y = d.getFullYear();\n const mo = pad(d.getMonth() + 1);\n const da = pad(d.getDate());\n const h = pad(d.getHours());\n const mi = pad(d.getMinutes());\n const s = pad(d.getSeconds());\n return `${y}${mo}${da}-${h}${mi}${s}`;\n}\n\n/**\n * Translates SDK messages into one-line-per-tool-use output.\n *\n * Design rules:\n * - One line per `tool_use` block, not per token. Users scanning the\n * output want to see \"what the agent did\", not a tail of the\n * assistant's prose.\n * - `Bash` gets special formatting because the input is most usefully\n * rendered as the command being run.\n * - On final `result`, emit a summary line. Callers can suppress all\n * intermediate output via `--quiet` and still get the summary.\n * - Tool paths are shown relative to the cwd (not implemented here\n * because the SDK doesn't give us cwd on every message; users get\n * the raw input). The cwd-relative rendering is a nice-to-have that\n * we can layer on later without changing the API.\n *\n * Exported for testing — `StreamingFormatter` is easier to unit-test in\n * isolation than the whole command.\n */\nexport class StreamingFormatter {\n private readonly sink: { write: (line: string) => void };\n /**\n * Current agent label. Starts as \"bootstrap\"; switches when we see an\n * `Agent` tool-use (slice 5 will exercise this). We still track it here\n * so the formatter can stay shared between bootstrap and capture.\n */\n private currentAgent = \"bootstrap\";\n\n constructor(sink: { write: (line: string) => void }) {\n this.sink = sink;\n }\n\n /**\n * Swap the top-level agent label. `capture` uses this to relabel from\n * the default \"bootstrap\" to \"writer\" — otherwise the writer's tool-use\n * output would render as `[bootstrap] …`, which is confusing when you're\n * reading capture logs.\n */\n setAgent(name: string): void {\n this.currentAgent = name;\n }\n\n handle(msg: SDKMessage): void {\n if (msg.type === \"assistant\") {\n for (const block of msg.message.content) {\n if (block.type !== \"tool_use\") continue;\n this.handleToolUse(block.name, block.input);\n }\n return;\n }\n\n if (msg.type === \"result\") {\n // The command-level finalLine is what the user sees at the end of\n // stdout; the formatter also emits one here so live-tailing a\n // transcript log shows the full run. Kept terse.\n const status =\n msg.subtype === \"success\" ? \"done\" : `failed (${msg.subtype})`;\n this.sink.write(\n `[${status}] cost: $${msg.total_cost_usd.toFixed(3)}, turns: ${msg.num_turns}\\n`,\n );\n return;\n }\n }\n\n private handleToolUse(name: string, rawInput: unknown): void {\n const input = normalizeToolInput(rawInput);\n\n if (name === \"Agent\") {\n // Subagent dispatch. Track the label so subsequent tool-uses show\n // up under the right agent bracket. The label is whatever the\n // parent passed as `subagent_type`, falling back to \"subagent\" if\n // the input was malformed (shouldn't happen, but defensively).\n const sub =\n typeof input.subagent_type === \"string\" ? input.subagent_type : \"subagent\";\n this.currentAgent = sub;\n this.sink.write(`[${sub}] starting\\n`);\n return;\n }\n\n const summary = formatToolSummary(name, input);\n this.sink.write(`[${this.currentAgent}] ${summary}\\n`);\n }\n}\n\n/**\n * SDK quirk: `tool_use.input` arrives as either an object OR a\n * JSON-encoded string. Always normalize before touching fields.\n */\nfunction normalizeToolInput(raw: unknown): Record<string, unknown> {\n if (typeof raw === \"string\") {\n try {\n const parsed = JSON.parse(raw);\n if (parsed !== null && typeof parsed === \"object\") {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // Fall through to empty object.\n }\n return {};\n }\n if (raw !== null && typeof raw === \"object\") {\n return raw as Record<string, unknown>;\n }\n return {};\n}\n\n/**\n * Render a tool call as a single human-readable line. Not exhaustive —\n * we cover the tools bootstrap actually invokes and fall back to the\n * tool name + a terse input summary for anything else.\n */\nfunction formatToolSummary(\n name: string,\n input: Record<string, unknown>,\n): string {\n switch (name) {\n case \"Read\": {\n const target = stringField(input, \"file_path\") ?? \"?\";\n return `reading ${target}`;\n }\n case \"Write\": {\n const target = stringField(input, \"file_path\") ?? \"?\";\n return `writing ${target}`;\n }\n case \"Edit\": {\n const target = stringField(input, \"file_path\") ?? \"?\";\n return `editing ${target}`;\n }\n case \"Glob\": {\n const pattern = stringField(input, \"pattern\") ?? \"?\";\n return `glob ${pattern}`;\n }\n case \"Grep\": {\n const pattern = stringField(input, \"pattern\") ?? \"?\";\n return `grep ${pattern}`;\n }\n case \"Bash\": {\n const command = stringField(input, \"command\") ?? \"?\";\n // Truncate long commands so one tool-use stays one line.\n const trimmed =\n command.length > 80 ? `${command.slice(0, 77)}...` : command;\n return `bash ${trimmed}`;\n }\n default: {\n // Unknown or MCP tool. Show the name; omit the input to avoid\n // spamming the terminal with arbitrary JSON.\n return name;\n }\n }\n}\n\nfunction stringField(\n input: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = input[key];\n return typeof value === \"string\" ? value : undefined;\n}\n","import { spawn, type ChildProcess } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Claude auth gate — accepts either an active Claude subscription login\n * OR an `ANTHROPIC_API_KEY` environment variable.\n *\n * The Claude Agent SDK delegates authentication to its bundled `cli.js`,\n * which reads OAuth credentials from `~/.claude/credentials/` (the same\n * store Claude Code uses). Users who are logged in via `claude auth login\n * --claudeai` should be able to run bootstrap/capture without ever\n * 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 * We spawn the bundled SDK's `cli.js auth status --json` to answer \"are\n * we logged in?\" rather than poking at the credentials file directly —\n * that's the SDK's contract, and it handles all the edge cases (token\n * expiry, org switching, consoleloginpath, …) for us.\n *\n * The CLI path is resolved via `require.resolve(\"@anthropic-ai/claude-\n * agent-sdk/package.json\")` + the `cli.js` sibling. Going through\n * `createRequire` keeps this compatible with both ESM dev mode (tsx) and\n * the bundled dist (tsup externalizes the SDK, so Node's own resolver\n * does the lookup at runtime). If the SDK isn't installed at all we fall\n * back to treating the user as unauthenticated — the assert will then\n * surface the familiar two-path error so they can at least fix it via\n * `ANTHROPIC_API_KEY`.\n */\n\nexport interface ClaudeAuthStatus {\n loggedIn: boolean;\n email?: string;\n subscriptionType?: string;\n authMethod?: string;\n}\n\nexport interface SpawnedProcess {\n stdout: { on: (event: \"data\", cb: (data: Buffer | string) => void) => void };\n stderr: { on: (event: \"data\", cb: (data: Buffer | string) => void) => void };\n on: (event: \"close\" | \"error\", cb: (arg: number | null | Error) => void) => void;\n kill: (signal?: string) => void;\n}\n\n/**\n * The subprocess spawner is injectable so tests can replace it with a\n * fake that emits canned JSON without touching the filesystem. Production\n * code uses `defaultSpawnCli` which invokes the bundled SDK CLI.\n */\nexport type SpawnCliFn = (args: string[]) => SpawnedProcess;\n\nconst AUTH_TIMEOUT_MS = 10_000;\n\n/**\n * Resolve `cli.js` from the bundled `@anthropic-ai/claude-agent-sdk`\n * install. Uses `createRequire` so the lookup works regardless of\n * whether we're running from `dist/` (where tsup externalized the SDK)\n * or directly from source.\n *\n * Throws if the SDK can't be located — `checkClaudeAuth` catches this\n * and treats the user as not-logged-in, which lets the env-var path\n * still work for users with a borked install.\n */\nfunction resolveCliJsPath(): string {\n // `import.meta.url` points at this module (dev or dist). `createRequire`\n // from that URL can then resolve sibling packages the same way Node's\n // own CJS resolver would.\n const require = createRequire(import.meta.url);\n const pkgJsonPath = require.resolve(\n \"@anthropic-ai/claude-agent-sdk/package.json\",\n );\n return join(dirname(pkgJsonPath), \"cli.js\");\n}\n\n/**\n * Default subprocess spawner for production use — invokes the bundled\n * SDK's `cli.js` via the same Node runtime that's running codealmanac.\n * Tests inject a fake via the `spawnCli` parameter.\n */\nexport const defaultSpawnCli: SpawnCliFn = (args: string[]) => {\n const cliPath = resolveCliJsPath();\n // Use `process.execPath` so we inherit the Node runtime codealmanac\n // itself is running under — avoids PATH weirdness on systems where\n // `node` isn't on PATH but codealmanac was installed via npm.\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 the bundled SDK CLI's `auth status --json`. On any failure\n * (spawn error, non-JSON stdout, non-zero exit, timeout) we return\n * `{ loggedIn: false }` rather than propagating the error — the caller\n * will fall back to the `ANTHROPIC_API_KEY` path and, if that's also\n * missing, produce a clean 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 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 const parsed = JSON.parse(stdout.trim()) 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 settle(out);\n } catch {\n settle({ loggedIn: false });\n }\n });\n });\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 };\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Loads bundled prompt text from the `prompts/` directory that ships with\n * the npm package. Used by `almanac bootstrap` (slice 4) and `almanac\n * capture` (slice 5).\n *\n * ## Why not embed the prompts as TS string literals?\n *\n * The non-negotiable from the spec (see CLAUDE.md → \"Non-negotiables\"):\n * \"Prompts are shipped from the npm package. They live in `prompts/` at\n * repo root, are bundled into `files` in `package.json`, and the agent\n * harness reads them from the package install path at runtime.\"\n *\n * Keeping them as separate files means they can be reviewed as prose,\n * diffed meaningfully, and (in the future) edited by users without\n * rebuilding the package.\n *\n * ## Path resolution\n *\n * Two runtime layouts need to work:\n *\n * 1. **Installed (`npm i -g codealmanac`).** The entry point lives at\n * `dist/codealmanac.js`; prompts at `prompts/*.md`. Walking up from\n * `import.meta.url` (`.../<pkg>/dist/codealmanac.js`) one level and\n * into `prompts/` hits the right directory.\n *\n * 2. **Source dev.** During `npm run dev`, tsup emits to `dist/` just\n * like in production, so case 1 applies. Tests import\n * `src/agent/prompts.ts` directly via tsx/vitest; `import.meta.url`\n * points at `src/agent/prompts.ts`. Walking up two levels from there\n * lands at the repo root, where `prompts/` also lives.\n *\n * We probe a small list of candidates in order and use the first that\n * contains all three expected prompt files. This keeps a single source of\n * truth — the `prompts/` directory on disk — without baking in whether\n * we're running from `dist/` or `src/`.\n */\n\nexport type PromptName = \"bootstrap\" | \"writer\" | \"reviewer\";\n\nconst PROMPT_NAMES: readonly PromptName[] = [\n \"bootstrap\",\n \"writer\",\n \"reviewer\",\n];\n\n/**\n * Override the prompts directory, for tests. Production code should never\n * call this — the auto-resolution handles both installed + source layouts.\n */\nlet overrideDir: string | null = null;\n\nexport function setPromptsDirForTesting(dir: string | null): void {\n overrideDir = dir;\n}\n\n/**\n * Resolve the prompts directory by probing candidate locations. Cached\n * after the first call so repeated `loadPrompt()` calls don't stat the\n * filesystem more than once per process.\n */\nlet resolvedDir: string | null = null;\n\nexport function resolvePromptsDir(): string {\n if (overrideDir !== null) return overrideDir;\n if (resolvedDir !== null) return resolvedDir;\n\n const here = path.dirname(fileURLToPath(import.meta.url));\n\n // Candidates, most-specific first. Each path is where `prompts/` MIGHT\n // live given some plausible bundle layout. The first one that exists\n // and contains our three expected files wins.\n const candidates = [\n // Bundled dist layout: `.../<pkg>/dist/codealmanac.js` → `../prompts`\n path.resolve(here, \"..\", \"prompts\"),\n // Source layout: `.../<pkg>/src/agent/prompts.ts` → `../../prompts`\n path.resolve(here, \"..\", \"..\", \"prompts\"),\n // Defensive fallback: if tsup someday emits a nested `dist/src/agent`,\n // walk up three levels.\n path.resolve(here, \"..\", \"..\", \"..\", \"prompts\"),\n ];\n\n for (const dir of candidates) {\n if (isPromptsDir(dir)) {\n resolvedDir = dir;\n return dir;\n }\n }\n\n // If none matched, give a helpful error with the candidates we tried.\n // This typically means the package was installed without the `prompts/`\n // dir included — shouldn't happen unless someone broke `files` in\n // package.json.\n throw new Error(\n \"could not locate bundled prompts/ directory. Tried:\\n\" +\n candidates.map((c) => ` - ${c}`).join(\"\\n\"),\n );\n}\n\nfunction isPromptsDir(dir: string): boolean {\n if (!existsSync(dir)) return false;\n // Require all three prompts to be present. A half-populated directory\n // is worse than not finding one — we'd rather error early.\n return PROMPT_NAMES.every((name) =>\n existsSync(path.join(dir, `${name}.md`)),\n );\n}\n\nexport async function loadPrompt(name: PromptName): Promise<string> {\n const dir = resolvePromptsDir();\n return readFile(path.join(dir, `${name}.md`), \"utf8\");\n}\n","import { query } from \"@anthropic-ai/claude-agent-sdk\";\nimport type {\n AgentDefinition,\n SDKMessage,\n} from \"@anthropic-ai/claude-agent-sdk\";\n\n/**\n * Thin wrapper around `@anthropic-ai/claude-agent-sdk`'s `query()`. This is\n * the ONLY module that imports from the SDK — every other command imports\n * from here. Slice 5 (capture) reuses this wrapper unchanged.\n *\n * Why a wrapper at all:\n * 1. Sets defaults (`maxTurns`, `includePartialMessages`, model) once so\n * commands stay small.\n * 2. Translates the SDK's rich message types into a {cost, turns, result}\n * summary the commands actually care about.\n *\n * The auth gate lives in `src/agent/auth.ts`. Commands call\n * `assertClaudeAuth()` BEFORE `runAgent` so we fail with a clean two-path\n * error before the SDK generator spins up. `runAgent` itself doesn't\n * re-check — the SDK reads whichever of (subscription OAuth,\n * `ANTHROPIC_API_KEY`) is present.\n *\n * Keep this module SMALL. If a feature can live in the caller, it should.\n */\n\nexport interface RunAgentOptions {\n /** Full system prompt text — usually loaded from `prompts/*.md`. */\n systemPrompt: string;\n /** User prompt / kick-off message. */\n prompt: string;\n /** Tool allowlist, e.g. `[\"Read\", \"Write\", \"Edit\", \"Glob\", \"Grep\", \"Bash\"]`. */\n allowedTools: string[];\n /**\n * Subagent definitions (slice 5 passes `{ reviewer: ... }`). Defaults to\n * `{}` — bootstrap has no subagents.\n */\n agents?: Record<string, AgentDefinition>;\n /** Working directory the agent's tools operate in (repo root). */\n cwd: string;\n /**\n * Model override. Defaults to `claude-sonnet-4-6`. Note the FULL form —\n * `options.model` requires `claude-sonnet-4-6`, not `sonnet`.\n */\n model?: string;\n /**\n * Hard cap on turns. Defaults to 100. SDK enforces this as a strict stop\n * (no graceful wrap-up turn), so set generously.\n */\n maxTurns?: number;\n /**\n * Observer called for every SDK message. The formatter (streaming\n * output, transcript log) runs here.\n */\n onMessage?: (msg: SDKMessage) => void;\n}\n\nexport interface AgentResult {\n /** `true` when the SDK emitted a `result` with `subtype: \"success\"`. */\n success: boolean;\n /** Total USD cost reported by the final `result` message. */\n cost: number;\n /** Number of turns the agent used. */\n turns: number;\n /** The assistant's final textual result, if any. */\n result: string;\n /** Session ID captured from the first assistant/result message. */\n sessionId?: string;\n /** Populated when `success === false`. */\n error?: string;\n}\n\n/**\n * Run an agent to completion. Iterates the SDK's `AsyncGenerator` and\n * returns a summary. Any thrown error in the `for await` becomes a\n * `success: false` result with the error message attached — we don't\n * propagate because the caller wants to write transcripts + print\n * formatted output regardless of outcome.\n *\n * The caller is responsible for running `assertClaudeAuth()` BEFORE\n * loading prompts or printing progress — see `bootstrap.ts`/`capture.ts`.\n * `runAgent` itself no longer re-checks; the SDK will happily pick up\n * whichever of (subscription OAuth, `ANTHROPIC_API_KEY`) the environment\n * provides.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentResult> {\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 ?? \"claude-sonnet-4-6\",\n maxTurns: opts.maxTurns ?? 100,\n // REQUIRED for streaming text deltas. Without it, `stream_event`\n // messages never fire and the CLI has no progress visibility during\n // long turns. See docs/research/agent-sdk.md §12 pitfall #1.\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 // Capture session_id from the first message that carries it.\n // Per the research doc, it appears on the first `assistant` or on\n // the `result` — whichever arrives first.\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 // `SDKResultMessage = SDKResultSuccess | SDKResultError`. Both\n // carry `total_cost_usd` and `num_turns`; only success has\n // `result` (the final assistant text).\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 // `SDKResultError` variants don't carry a `result` string; the\n // useful detail lives in `errors` (array of strings) or the\n // subtype itself (e.g. \"error_max_turns\").\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","import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\n\n/**\n * Absolute path to the user-level `~/.almanac/` directory.\n *\n * All global state (the registry, future global config) lives here, not in\n * the repo. We resolve this via `os.homedir()` rather than `$HOME` so the\n * CLI behaves the same on macOS, Linux, and Windows.\n */\nexport function getGlobalAlmanacDir(): string {\n return join(homedir(), \".almanac\");\n}\n\n/**\n * Absolute path to the global registry file.\n *\n * The registry is the single source of truth for \"which wikis exist on this\n * machine.\" It is intentionally stored outside any repo so it survives\n * branch switches, clones, and repo deletions.\n */\nexport function getRegistryPath(): string {\n return join(getGlobalAlmanacDir(), \"registry.json\");\n}\n\n/**\n * Repo-level `.almanac/` path for a given working directory (not resolved —\n * just `join(cwd, \".almanac\")`). Use `findNearestAlmanacDir` when you need\n * to walk upward like git does.\n */\nexport function getRepoAlmanacDir(cwd: string): string {\n return join(cwd, \".almanac\");\n}\n\n/**\n * Walk upward from `startDir` looking for a directory that contains\n * `.almanac/`. Returns the absolute path to the repo root (the directory\n * containing `.almanac/`), or `null` if none is found before hitting the\n * filesystem root.\n *\n * Mirrors how `git` locates the enclosing repository. This lets `almanac`\n * work from any subdirectory inside a repo, not just the root.\n *\n * We explicitly skip the global `~/.almanac/` directory. It shares the\n * `.almanac` name with the per-repo wiki dir, but it's not a wiki — it\n * only holds the registry and global state. If the user runs `almanac\n * init` anywhere inside their home directory (outside a real wiki), we\n * must NOT treat `~` as an enclosing wiki root. Otherwise init would try\n * to register the home dir itself as a wiki.\n */\nexport function findNearestAlmanacDir(startDir: string): string | null {\n const globalDir = getGlobalAlmanacDir();\n let current = isAbsolute(startDir) ? startDir : resolve(startDir);\n\n // Walk until we hit the filesystem root. `dirname(\"/\")` returns `\"/\"`,\n // so the loop terminates when we stop ascending.\n while (true) {\n const candidate = join(current, \".almanac\");\n if (candidate !== globalDir && existsSync(candidate)) {\n return current;\n }\n const parent = dirname(current);\n if (parent === current) {\n return null;\n }\n current = parent;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\nimport { findNearestAlmanacDir, getRepoAlmanacDir } from \"../paths.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport {\n addEntry,\n ensureGlobalDir,\n type RegistryEntry,\n} from \"../registry/index.js\";\n\nexport interface InitOptions {\n cwd: string;\n name?: string;\n description?: string;\n}\n\nexport interface InitResult {\n entry: RegistryEntry;\n almanacDir: string;\n created: boolean; // false if .almanac/ already existed (idempotent re-init)\n}\n\n/**\n * Scaffold `.almanac/` in the repo and register it globally.\n *\n * Idempotent: running `init` on a repo that already has `.almanac/` is\n * fine — we re-register (refreshing the name/description) and skip\n * anything that already exists. We never overwrite a user-authored\n * `README.md` or touch existing pages.\n *\n * If `cwd` lives inside a subdirectory of an existing wiki, we walk up to\n * the wiki root and operate there. `almanac init` from `src/nested/`\n * should update the enclosing wiki, not create a nested one at\n * `src/nested/.almanac/` (which would fragment the registry and leave a\n * confusing orphan `.almanac/` on disk).\n */\nexport async function initWiki(options: InitOptions): Promise<InitResult> {\n // If cwd is already inside a wiki, prefer that root. Otherwise treat\n // cwd as the new wiki root.\n const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;\n\n const almanacDir = getRepoAlmanacDir(repoRoot);\n const pagesDir = join(almanacDir, \"pages\");\n const readmePath = join(almanacDir, \"README.md\");\n\n const alreadyExisted = existsSync(almanacDir);\n\n await mkdir(pagesDir, { recursive: true });\n\n if (!existsSync(readmePath)) {\n await writeFile(readmePath, starterReadme(), \"utf8\");\n }\n\n await ensureGitignoreHasIndexDb(repoRoot);\n\n const name = toKebabCase(options.name ?? basename(repoRoot));\n if (name.length === 0) {\n throw new Error(\n \"could not derive a wiki name from the current directory; pass --name\",\n );\n }\n\n const description = (options.description ?? \"\").trim();\n\n await ensureGlobalDir();\n const entry: RegistryEntry = {\n name,\n description,\n path: repoRoot,\n registered_at: new Date().toISOString(),\n };\n await addEntry(entry);\n\n return { entry, almanacDir, created: !alreadyExisted };\n}\n\n/**\n * Ensure `.gitignore` in the repo root contains the codealmanac-derived\n * files that should never be committed.\n *\n * We ignore three paths:\n * - `.almanac/index.db` — the SQLite index (derived)\n * - `.almanac/index.db-wal` — WAL mode sidecar (present during writes)\n * - `.almanac/index.db-shm` — shared-memory sidecar (WAL mode)\n *\n * All three are derived from the markdown pages. The sidecars only show\n * up while better-sqlite3 has the DB open and can vanish between `git\n * status` calls, but explicitly ignoring them prevents \"dirty worktree\"\n * noise during active reindexing.\n *\n * We add the block regardless of whether the file exists (creating\n * `.gitignore` if needed), and we add any target lines that aren't\n * already present. Existing targets are left alone. If none of the\n * target lines need adding, the file is not touched at all.\n *\n * Formatting: when we do append, we guarantee exactly one blank line\n * between the prior content and our appended block. If the `# codealmanac`\n * header is already present but new targets need adding, we just append\n * the missing lines (no duplicate header).\n */\nasync function ensureGitignoreHasIndexDb(cwd: string): Promise<void> {\n const path = join(cwd, \".gitignore\");\n const targets = [\n \".almanac/index.db\",\n \".almanac/index.db-wal\",\n \".almanac/index.db-shm\",\n ];\n\n let existing = \"\";\n if (existsSync(path)) {\n existing = await readFile(path, \"utf8\");\n }\n\n // Normalize to line comparison to avoid false negatives on trailing\n // whitespace or CRLF line endings.\n const lines = existing.split(/\\r?\\n/).map((l) => l.trim());\n const missing = targets.filter((t) => !lines.includes(t));\n if (missing.length === 0) return;\n\n const hasHeader = lines.includes(\"# codealmanac\");\n const block = hasHeader\n ? missing.join(\"\\n\") + \"\\n\"\n : `# codealmanac\\n${missing.join(\"\\n\")}\\n`;\n\n // Three cases for the separator before the appended block:\n // - empty file: no separator needed\n // - ends with newline: one more newline produces a single blank line\n // - no trailing newline: two newlines (one to terminate the last line,\n // one for the blank separator)\n const sep =\n existing.length === 0 ? \"\" : existing.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n await writeFile(path, `${existing}${sep}${block}`, \"utf8\");\n}\n\n/**\n * The starter `.almanac/README.md` content. Based on the \"Wiki README\" and\n * \"Notability bar\" sections of the design spec. Kept opinionated but short\n * (~70 lines) — the user is expected to edit it to fit the repo.\n */\nfunction starterReadme(): string {\n return `# Wiki\n\nThis is the codealmanac wiki for this repository. It captures the knowledge\nthe code itself can't say — decisions, flows, invariants, gotchas, incidents.\n\nThe primary reader is an AI coding agent. The secondary reader is a human\nskimming to understand the shape of the codebase. Write accordingly: dense,\nfactual, linked.\n\n## Notability bar\n\nWrite a page when there is **non-obvious knowledge that will help a future\nagent**. Specifically:\n\n- A decision that took discussion, research, or trial-and-error\n- A gotcha discovered through failure\n- A cross-cutting flow that spans multiple files and isn't obvious from any\n one of them\n- A constraint or invariant not visible from the code\n- An entity (technology, service, system) referenced by multiple pages\n\nDo not write pages that restate what the code does. Do not write pages of\ninference — only of observation. Silence is an acceptable outcome.\n\n## Topic taxonomy\n\nTopics form a DAG; pages can belong to multiple topics. Start with these and\ngrow as the wiki does:\n\n- \\`stack\\` — technologies and services we use (frameworks, databases, APIs)\n- \\`systems\\` — custom systems we built (auth, billing, search)\n- \\`flows\\` — multi-file processes end-to-end (checkout-flow, publish-flow)\n- \\`decisions\\` — \"why X over Y\"\n- \\`incidents\\` — recorded failures and their fixes\n- \\`concepts\\` — shared vocabulary specific to this codebase\n\nDomain topics (\\`auth\\`, \\`payments\\`, \\`frontend\\`, \\`backend\\`) live alongside\nthese. A page about JWT rotation belongs to both \\`auth\\` and \\`decisions\\`.\n\n## Page shapes\n\nFour shapes cover most of what gets written. They are suggestions, not a\nschema — a page that fits none of them is fine.\n\n- **Entity** — a stable named thing (Supabase, Stripe, the search service)\n- **Decision** — why we chose X over Y\n- **Flow** — how a multi-file process works end-to-end\n- **Gotcha** — a specific surprise, failure, or constraint\n\n## Writing conventions\n\n- Every sentence contains a specific fact. If it doesn't, cut it.\n- Neutral tone. \"is\", not \"serves as\". No \"plays a pivotal role\", no\n interpretive \"-ing\" clauses, no vague attribution (\"experts argue\").\n- No hedging or knowledge-gap disclaimers. If you don't know, don't write\n the sentence.\n- Prose first. Bullets for genuine lists. Tables only for structured\n comparison.\n- No formulaic conclusions. End with the last substantive fact.\n\n## Linking\n\nOne \\`[[...]]\\` syntax for everything, disambiguated by content:\n\n- \\`[[checkout-flow]]\\` — page slug\n- \\`[[src/checkout/handler.ts]]\\` — file reference\n- \\`[[src/checkout/]]\\` — folder reference (trailing slash)\n- \\`[[other-wiki:slug]]\\` — cross-wiki reference\n\nEvery page should link to at least one entity when possible. A page with no\nentity link is suspect.\n\n## Pages live in \\`.almanac/pages/\\`\n\nOne markdown file per page, kebab-case slug. Frontmatter carries \\`topics:\\`\nand optional \\`files:\\`. The rest is prose.\n`;\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 { 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 { createHash } from \"node:crypto\";\nimport {\n createWriteStream,\n existsSync,\n statSync,\n type WriteStream,\n} from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, join, relative } from \"node:path\";\n\nimport type { AgentDefinition, SDKMessage } from \"@anthropic-ai/claude-agent-sdk\";\n\nimport { assertClaudeAuth, type SpawnCliFn } from \"../agent/auth.js\";\nimport { loadPrompt } from \"../agent/prompts.js\";\nimport {\n runAgent,\n type AgentResult,\n type RunAgentOptions,\n} from \"../agent/sdk.js\";\nimport { parseFrontmatter } from \"../indexer/frontmatter.js\";\nimport { findNearestAlmanacDir, getRepoAlmanacDir } from \"../paths.js\";\nimport { StreamingFormatter } from \"./bootstrap.js\";\n\nexport interface CaptureOptions {\n cwd: string;\n /** Explicit transcript path. Skips auto-resolution. */\n transcriptPath?: string;\n /** Target a specific session ID. */\n sessionId?: string;\n /** Suppress per-tool-use streaming; print only the final summary line. */\n quiet?: boolean;\n /** Model override. Defaults to the SDK default (sonnet-4-6). */\n model?: string;\n /** Injectable agent runner — tests replace this with a fake. */\n runAgent?: (opts: RunAgentOptions) => Promise<AgentResult>;\n /**\n * Injectable spawner for the Claude auth-status subprocess. Tests pass\n * a stub; production uses `defaultSpawnCli` which shells out to the\n * bundled SDK's `cli.js`.\n */\n spawnCli?: SpawnCliFn;\n /** Clock injection for deterministic log filenames in tests. */\n now?: () => Date;\n /**\n * Override the Claude Code projects directory when auto-resolving a\n * transcript. Production code leaves this undefined and we fall back to\n * `~/.claude/projects`; tests point it at a fixture dir.\n */\n claudeProjectsDir?: string;\n}\n\nexport interface CaptureResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Tools the writer agent is permitted to use.\n *\n * - `Read` — read the session transcript, existing wiki pages, source files\n * - `Write` / `Edit` — create and update pages under `.almanac/pages/`\n * - `Glob` / `Grep` — navigate the wiki and source code\n * - `Bash` — interrogate the wiki via `almanac search/show/info/list`\n * - `Agent` — invoke the reviewer subagent\n *\n * `WebFetch`/`WebSearch` are intentionally absent: the writer should work\n * from the transcript + repo, not the open internet.\n */\nconst WRITER_TOOLS = [\"Read\", \"Write\", \"Edit\", \"Glob\", \"Grep\", \"Bash\", \"Agent\"];\n\n/**\n * Tools the reviewer subagent is permitted to use. The absence of\n * `Write`/`Edit`/`Agent` is the *only* thing preventing the reviewer from\n * editing files or chaining to further subagents — the SDK enforces this\n * based on the `tools` field in the `AgentDefinition`.\n */\nconst REVIEWER_TOOLS = [\"Read\", \"Grep\", \"Glob\", \"Bash\"];\n\nconst REVIEWER_DESCRIPTION =\n \"Reviews proposed wiki changes against the full knowledge base for \" +\n \"cohesion, duplication, missing links, notability, and writing conventions.\";\n\n/**\n * `almanac capture` — writer agent + reviewer subagent on a session transcript.\n *\n * Flow:\n * 1. Auth gate (ANTHROPIC_API_KEY).\n * 2. Resolve repo root (walk up for `.almanac/`). Refuse if none.\n * 3. Resolve transcript path (arg, --session, or auto-resolve from\n * Claude Code's session storage).\n * 4. Snapshot `.almanac/pages/` BEFORE the agent runs so we can compute\n * a created/updated/archived summary when it finishes.\n * 5. Load `prompts/writer.md` + `prompts/reviewer.md`. Build a reviewer\n * `AgentDefinition` with read-only tools.\n * 6. Run the writer agent with the reviewer registered under `agents`.\n * 7. Stream tool-uses via the shared `StreamingFormatter` (unless --quiet).\n * 8. Diff the snapshot → emit `[done] N updated, M created, K archived …`.\n *\n * Empty outcomes (writer wrote nothing) exit 0 with a clear \"notability bar\"\n * message — per the writer prompt, silence is a valid output.\n */\nexport async function runCapture(\n options: CaptureOptions,\n): Promise<CaptureResult> {\n // Fail before any filesystem work. `assertClaudeAuth` accepts either\n // subscription OAuth (via the bundled SDK CLI) or `ANTHROPIC_API_KEY`;\n // missing both surfaces a two-option error with exit 1 so the\n // SessionEnd hook (which backgrounds + redirects to a sidecar log)\n // doesn't silently treat auth failure as a successful capture.\n try {\n await assertClaudeAuth(options.spawnCli);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n stdout: \"\",\n stderr: `almanac: ${msg}\\n`,\n exitCode: 1,\n };\n }\n\n // Resolve the repo root by walking up for `.almanac/`. Unlike bootstrap,\n // capture refuses to run when no wiki exists — the writer needs existing\n // pages to read against, and auto-initing here would hide the fact that\n // the user skipped `almanac bootstrap`.\n const repoRoot = findNearestAlmanacDir(options.cwd);\n if (repoRoot === null) {\n return {\n stdout: \"\",\n stderr:\n \"almanac: no .almanac/ found in this directory or any parent. \" +\n \"Run 'almanac init' or 'almanac bootstrap' first.\\n\",\n exitCode: 1,\n };\n }\n\n const almanacDir = getRepoAlmanacDir(repoRoot);\n const pagesDir = join(almanacDir, \"pages\");\n\n // Resolve the transcript path up front. Doing this before we open a log\n // stream keeps bad-arg errors uncluttered by side effects.\n const transcriptResolution = await resolveTranscript({\n repoRoot,\n explicit: options.transcriptPath,\n sessionId: options.sessionId,\n claudeProjectsDir: options.claudeProjectsDir,\n });\n if (!transcriptResolution.ok) {\n return {\n stdout: \"\",\n stderr: `almanac: ${transcriptResolution.error}\\n`,\n exitCode: 1,\n };\n }\n const transcriptPath = transcriptResolution.path;\n\n // Snapshot the pages dir BEFORE the writer runs. We compare against it\n // after the agent exits to compute a created/updated/archived tally.\n // Doing this in TS (not via the agent's self-reporting) means the summary\n // stays trustworthy even if the writer gets confused about what it did.\n const snapshotBefore = await snapshotPages(pagesDir);\n\n // Load the two prompts. Kept sequential rather than parallel — both files\n // are tiny and the second read is cache-warm.\n const systemPrompt = await loadPrompt(\"writer\");\n const reviewerPrompt = await loadPrompt(\"reviewer\");\n\n const agents: Record<string, AgentDefinition> = {\n reviewer: {\n description: REVIEWER_DESCRIPTION,\n prompt: reviewerPrompt,\n tools: REVIEWER_TOOLS,\n },\n };\n\n // Transcript log filename: timestamp-based so repeated runs don't clobber\n // each other. We don't have the SDK session_id yet (it's on the first\n // message), and filesystem writes need a destination before the stream\n // begins.\n const now = options.now?.() ?? new Date();\n const logName = `.capture-${formatTimestamp(now)}.log`;\n const logPath = join(almanacDir, logName);\n const logStream = createWriteStream(logPath, { flags: \"w\" });\n\n const out = process.stdout;\n const formatter = new StreamingFormatter({\n write: (line: string) => {\n if (options.quiet !== true) out.write(line);\n },\n });\n // The shared StreamingFormatter defaults its currentAgent to \"bootstrap\"\n // because bootstrap was the first command to use it. For capture the\n // writer owns the top-level turn, so relabel.\n formatter.setAgent(\"writer\");\n\n const onMessage = (msg: SDKMessage): void => {\n try {\n logStream.write(`${JSON.stringify(msg)}\\n`);\n } catch {\n // Best-effort: one unserializable message shouldn't kill the whole\n // stream. Humans read the log; if a line is missing they can re-run.\n }\n formatter.handle(msg);\n };\n\n // Pass an ABSOLUTE path for the transcript so the writer doesn't have to\n // guess at cwd semantics. Everything else (`.almanac/pages/`) is already\n // relative to the cwd the SDK gives its tools.\n const userPrompt =\n `Capture this coding session.\\n` +\n `Transcript: ${transcriptPath}.\\n` +\n `Working directory: ${repoRoot}.`;\n\n const runner = options.runAgent ?? runAgent;\n\n let result: AgentResult;\n try {\n result = await runner({\n systemPrompt,\n prompt: userPrompt,\n allowedTools: WRITER_TOOLS,\n agents,\n cwd: repoRoot,\n model: options.model,\n // Capture sessions can touch many pages; give it more headroom than\n // bootstrap. The SDK treats `maxTurns` as a hard stop — better to\n // overshoot than to cut off mid-review.\n maxTurns: 150,\n onMessage,\n });\n } finally {\n await closeStream(logStream);\n }\n\n const snapshotAfter = await snapshotPages(pagesDir);\n const delta = diffSnapshots(snapshotBefore, snapshotAfter);\n\n if (!result.success) {\n return {\n stdout: \"\",\n stderr:\n `almanac: capture failed: ${result.error ?? \"unknown error\"}\\n` +\n `(transcript: ${relative(repoRoot, logPath)})\\n`,\n exitCode: 1,\n };\n }\n\n const summary = formatSummary(result, delta, logPath, repoRoot);\n\n return {\n stdout: `${summary}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Transcript resolution ────────────────────────────────────────────────\n\ninterface ResolvedTranscript {\n ok: true;\n path: string;\n}\ninterface FailedTranscript {\n ok: false;\n error: string;\n}\n\n/**\n * Resolve the transcript path from the three possible sources, in priority:\n * 1. Explicit positional arg (`almanac capture <path>`).\n * 2. `--session <id>`: find the single `.jsonl` matching that ID.\n * 3. Auto-resolve: most recent `.jsonl` under Claude Code's projects dir\n * whose parent directory hashes to `repoRoot`. If multiple candidates\n * or none match, return an error directing the user to pass a path.\n *\n * Claude Code names the per-project directory with a path-hash that we\n * can't deterministically reproduce without reading Claude Code's source.\n * Rather than guess at the hashing scheme, we scan all project dirs, pick\n * the one whose most recent transcript mentions the `repoRoot` in its\n * `cwd` field, and take the newest `.jsonl` from there.\n */\nasync function resolveTranscript(args: {\n repoRoot: string;\n explicit?: string;\n sessionId?: string;\n claudeProjectsDir?: string;\n}): Promise<ResolvedTranscript | FailedTranscript> {\n if (args.explicit !== undefined && args.explicit.length > 0) {\n if (!existsSync(args.explicit)) {\n return {\n ok: false,\n error: `transcript not found: ${args.explicit}`,\n };\n }\n return { ok: true, path: args.explicit };\n }\n\n const projectsDir =\n args.claudeProjectsDir ?? join(homedir(), \".claude\", \"projects\");\n if (!existsSync(projectsDir)) {\n return {\n ok: false,\n error:\n `could not auto-resolve transcript; ${projectsDir} does not exist. ` +\n `Pass --session <id> or <transcript-path>.`,\n };\n }\n\n const allTranscripts = await collectTranscripts(projectsDir);\n\n if (args.sessionId !== undefined && args.sessionId.length > 0) {\n const expected = `${args.sessionId}.jsonl`;\n const match = allTranscripts.find((t) => basename(t.path) === expected);\n if (match === undefined) {\n return {\n ok: false,\n error:\n `no transcript found for session ${args.sessionId} under ${projectsDir}`,\n };\n }\n return { ok: true, path: match.path };\n }\n\n // Auto-resolve: prefer transcripts whose `cwd` field matches `repoRoot`,\n // then fall back to the most recently modified if no cwd match is found.\n // We read a peek of each transcript (not the whole file) to check the\n // cwd — JSONL's first line typically carries it.\n const matches = await filterTranscriptsByCwd(allTranscripts, args.repoRoot);\n\n if (matches.length === 0) {\n return {\n ok: false,\n error:\n `could not auto-resolve transcript under ${projectsDir}; ` +\n `no session matches cwd ${args.repoRoot}. ` +\n `Pass --session <id> or <transcript-path>.`,\n };\n }\n\n // Sort by mtime desc and pick the newest.\n matches.sort((a, b) => b.mtime - a.mtime);\n return { ok: true, path: matches[0]!.path };\n}\n\ninterface TranscriptEntry {\n path: string;\n mtime: number;\n}\n\nasync function collectTranscripts(\n projectsDir: string,\n): Promise<TranscriptEntry[]> {\n const out: TranscriptEntry[] = [];\n let topLevel: string[];\n try {\n topLevel = await readdir(projectsDir);\n } catch {\n return out;\n }\n for (const name of topLevel) {\n const projectDir = join(projectsDir, name);\n let entries: string[];\n try {\n entries = await readdir(projectDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (!entry.endsWith(\".jsonl\")) continue;\n const full = join(projectDir, entry);\n try {\n const st = await stat(full);\n if (st.isFile()) {\n out.push({ path: full, mtime: st.mtimeMs });\n }\n } catch {\n // Transient read error, skip.\n }\n }\n }\n return out;\n}\n\n/**\n * Keep only transcripts whose first session record mentions a `cwd` that\n * matches `repoRoot` (exact string match). We also match on the Claude\n * Code project-hash heuristic: the per-project dir name is usually the\n * absolute repo path with `/` replaced by `-`. Falling back to the hash\n * heuristic means we still resolve sanely when the JSONL format changes.\n */\nasync function filterTranscriptsByCwd(\n transcripts: TranscriptEntry[],\n repoRoot: string,\n): Promise<TranscriptEntry[]> {\n const dirHash = `-${repoRoot.replace(/^\\/+/, \"\").replace(/\\//g, \"-\")}`;\n\n const byDirName = transcripts.filter((t) => {\n const parent = basename(join(t.path, \"..\"));\n return parent === dirHash || parent.endsWith(dirHash);\n });\n if (byDirName.length > 0) return byDirName;\n\n // Fallback: peek into each JSONL for a `\"cwd\":\"<repoRoot>\"` needle.\n const needle = `\"cwd\":\"${repoRoot}\"`;\n const hits: TranscriptEntry[] = [];\n for (const t of transcripts) {\n try {\n const head = await readHead(t.path, 4096);\n if (head.includes(needle)) hits.push(t);\n } catch {\n continue;\n }\n }\n return hits;\n}\n\nasync function readHead(path: string, bytes: number): Promise<string> {\n // Small files — just read the whole thing. We only call this on .jsonl\n // files, which can be large, so cap at `bytes` via slicing.\n const content = await readFile(path, \"utf8\");\n return content.length > bytes ? content.slice(0, bytes) : content;\n}\n\n// ─── Snapshot / delta ─────────────────────────────────────────────────────\n\ninterface PageSnapshotEntry {\n slug: string;\n /** SHA-256 of file bytes — cheap, stable, avoids relying on mtime. */\n hash: string;\n /** `true` when the frontmatter has `archived_at` set. */\n archived: boolean;\n}\n\ntype PageSnapshot = Map<string, PageSnapshotEntry>;\n\nasync function snapshotPages(pagesDir: string): Promise<PageSnapshot> {\n const out: PageSnapshot = new Map();\n if (!existsSync(pagesDir)) return out;\n\n let entries: string[];\n try {\n entries = await readdir(pagesDir);\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (!entry.endsWith(\".md\")) continue;\n const slug = entry.slice(0, -3);\n const full = join(pagesDir, entry);\n try {\n const st = statSync(full);\n if (!st.isFile()) continue;\n const content = await readFile(full, \"utf8\");\n const hash = createHash(\"sha256\").update(content).digest(\"hex\");\n const fm = parseFrontmatter(content);\n out.set(slug, {\n slug,\n hash,\n archived: fm.archived_at !== null,\n });\n } catch {\n continue;\n }\n }\n return out;\n}\n\ninterface SnapshotDelta {\n created: number;\n updated: number;\n archived: number;\n}\n\nfunction diffSnapshots(\n before: PageSnapshot,\n after: PageSnapshot,\n): SnapshotDelta {\n let created = 0;\n let updated = 0;\n let archived = 0;\n\n for (const [slug, entry] of after) {\n const prev = before.get(slug);\n if (prev === undefined) {\n created += 1;\n continue;\n }\n if (prev.hash !== entry.hash) {\n // An edit that flips a page from active → archived counts as\n // \"archived\", not \"updated\" — the archive is the semantically\n // interesting thing.\n if (!prev.archived && entry.archived) {\n archived += 1;\n } else {\n updated += 1;\n }\n }\n }\n // Note: we deliberately don't track deleted pages. The writer prompt\n // tells agents to archive (via frontmatter), not delete — a page that\n // disappears entirely is a protocol violation worth surfacing, but not\n // by silently counting it in the summary.\n\n return { created, updated, archived };\n}\n\n// ─── Formatting ───────────────────────────────────────────────────────────\n\nfunction formatSummary(\n result: AgentResult,\n delta: SnapshotDelta,\n logPath: string,\n repoRoot: string,\n): string {\n const rel = relative(repoRoot, logPath);\n const cost = `$${result.cost.toFixed(3)}`;\n const { created, updated, archived } = delta;\n\n if (created === 0 && updated === 0 && archived === 0) {\n return (\n `[capture] no new knowledge met the notability bar (0 pages written), ` +\n `cost: ${cost}, turns: ${result.turns} (transcript: ${rel})`\n );\n }\n\n return (\n `[done] ${updated} page${updated === 1 ? \"\" : \"s\"} updated, ` +\n `${created} created, ` +\n `${archived} archived, ` +\n `cost: ${cost}, turns: ${result.turns} (transcript: ${rel})`\n );\n}\n\nfunction formatTimestamp(d: Date): string {\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n return (\n `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-` +\n `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`\n );\n}\n\nfunction closeStream(stream: WriteStream): Promise<void> {\n return new Promise((resolve) => {\n stream.end(() => resolve());\n });\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 } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\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 * - **Idempotent.** `install` twice leaves one entry, not two. We match by\n * `command` string equality — if the user replaces our absolute path\n * with a symlink pointing at the same script, we'll treat it as foreign.\n * That's acceptable; the `status` output shows the path we'd use, so the\n * 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.\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 /**\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\nexport interface HookCommandResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst HOOK_TIMEOUT_SECONDS = 10;\n\ninterface SessionEndEntry {\n type: \"command\";\n command: string;\n timeout?: number;\n}\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 so\n * we don't drop user settings when we write the file back.\n */\ntype SettingsJson = Record<string, unknown> & {\n hooks?: Record<string, SessionEndEntry[] | undefined>;\n};\n\nexport async function runHookInstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const script = resolveHookScriptPath(options);\n if (!script.ok) {\n return { stdout: \"\", stderr: `almanac: ${script.error}\\n`, exitCode: 1 };\n }\n\n const settingsPath = resolveSettingsPath(options);\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n // Find any existing entry with our exact command. If found, the install\n // is a no-op (idempotent). If SessionEnd has OTHER commands alongside\n // ours, leave them alone — the user might be composing multiple hooks.\n const ourEntries = existing.filter((e) => e.command === script.path);\n const foreignEntries = existing.filter((e) => e.command !== script.path);\n\n // If the sole existing entry looks like ours-but-on-a-different-path\n // (e.g. an old install from a different node_modules), replace it so we\n // don't double-fire on session end. Heuristic: command ends with\n // `almanac-capture.sh`. We accept this specific rename; unrelated\n // commands still block.\n const stale = foreignEntries.filter((e) =>\n e.command.endsWith(\"almanac-capture.sh\"),\n );\n const unrelated = foreignEntries.filter(\n (e) => !e.command.endsWith(\"almanac-capture.sh\"),\n );\n\n if (unrelated.length > 0) {\n const existingStr = unrelated.map((e) => ` - ${e.command}`).join(\"\\n\");\n return {\n stdout: \"\",\n stderr:\n `almanac: SessionEnd hook already has a foreign entry:\\n${existingStr}\\n` +\n `Remove it manually from ${settingsPath} if you want almanac to manage the hook.\\n`,\n exitCode: 1,\n };\n }\n\n if (ourEntries.length > 0 && stale.length === 0) {\n return {\n stdout: `almanac: SessionEnd hook already installed at ${script.path}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Compose the new SessionEnd array: keep our one entry (fresh), drop any\n // stale almanac entries.\n const newEntries: SessionEndEntry[] = [\n {\n type: \"command\",\n command: script.path,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ];\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: ${script.path}\\n` +\n ` settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\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 // Remove ONLY our entries — anything else stays. We treat any entry with\n // a command ending in `almanac-capture.sh` as ours (handles the case\n // where the bundled path moved between `npm i` locations).\n const kept = existing.filter((e) => !e.command.endsWith(\"almanac-capture.sh\"));\n const removed = existing.length - kept.length;\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 const ours = existing.find((e) => e.command.endsWith(\"almanac-capture.sh\"));\n\n if (ours === undefined) {\n const foreign = existing\n .map((e) => ` - ${e.command}`)\n .join(\"\\n\");\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath}\\n` +\n (existing.length > 0\n ? `(${existing.length} foreign entr${existing.length === 1 ? \"y\" : \"ies\"} present:\\n${foreign})\\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: ${ours.command}\\n` +\n `settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────\n\nfunction resolveSettingsPath(options: HookCommandOptions): string {\n if (options.settingsPath !== undefined) return options.settingsPath;\n return path.join(homedir(), \".claude\", \"settings.json\");\n}\n\ntype ScriptResolution =\n | { ok: true; path: string }\n | { ok: false; error: string };\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 */\nfunction resolveHookScriptPath(options: HookCommandOptions): 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: `.../codealmanac/src/commands/hook.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\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 { 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 { parseDuration } from \"../indexer/duration.js\";\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.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) => ` ${o.slug}`),\n ),\n );\n sections.push(\n section(\n \"stale\",\n r.stale.length,\n r.stale.map((s) => ` ${s.slug} (${s.days_since_update} days)`),\n ),\n );\n sections.push(\n section(\n \"dead-refs\",\n r.dead_refs.length,\n r.dead_refs.map((d) => ` ${d.slug} references ${d.path} (missing)`),\n ),\n );\n sections.push(\n section(\n \"broken-links\",\n r.broken_links.length,\n r.broken_links.map(\n (b) => ` ${b.source_slug} → ${b.target_slug} (target does not exist)`,\n ),\n ),\n );\n sections.push(\n section(\n \"broken-xwiki\",\n r.broken_xwiki.length,\n r.broken_xwiki.map(\n (b) =>\n ` ${b.source_slug} → ${b.target_wiki}:${b.target_slug} (wiki unregistered or unreachable)`,\n ),\n ),\n );\n sections.push(\n section(\n \"empty-topics\",\n r.empty_topics.length,\n r.empty_topics.map((e) => ` ${e.slug}`),\n ),\n );\n sections.push(\n section(\n \"empty-pages\",\n r.empty_pages.length,\n r.empty_pages.map((e) => ` ${e.slug}`),\n ),\n );\n sections.push(\n section(\n \"slug-collisions\",\n r.slug_collisions.length,\n r.slug_collisions.map((c) => ` ${c.slug}: ${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 `${label} (0): (ok)`;\n return `${label} (${count}):\\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 { createHash } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { readFile, utimes } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { toKebabCase } from \"../slug.js\";\nimport { loadTopicsFile, titleCase } from \"../topics/yaml.js\";\nimport { firstH1, parseFrontmatter } from \"./frontmatter.js\";\nimport {\n normalizePath,\n normalizePathPreservingCase,\n looksLikeDir,\n} from \"./paths.js\";\nimport { openIndex } from \"./schema.js\";\nimport { extractWikilinks } from \"./wikilinks.js\";\n\n/**\n * Filename relative to the `.almanac/` dir where the topic DAG + metadata\n * lives. The indexer loads it on every reindex; the topics commands\n * mutate it atomically. Keep this in sync with `src/topics/paths.ts`'s\n * `topicsYamlPath` — duplicated here only so the indexer doesn't import\n * a second \"where is it\" helper.\n */\nconst TOPICS_YAML_FILENAME = \"topics.yaml\";\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// Glob is relative to `pagesDir` (which is `.almanac/pages/`), so this is\n// just \"every .md at any depth\" — not `pages/**/*.md`, because we've\n// already `cd`'d into `pages/` logically.\nconst PAGES_GLOB = \"**/*.md\";\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 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 // `relative` keeps lint happy about unused imports; total is just the\n // count of .md files we saw on this pass.\n void relative;\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\n/**\n * Return true if any `pages/**\\/*.md` has an mtime strictly greater than\n * the index DB's mtime. We walk with `fast-glob` rather than shell out to\n * `find` for portability.\n *\n * This is the \"should we reindex?\" check. It's intentionally cheap —\n * `fast-glob` with `stats: true` gives us mtimes without a second `stat`\n * round-trip.\n */\nfunction 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 // Synchronous walk — `fg.sync` is fine at this scale and keeps the\n // decision path simple (we don't need to await inside every CLI entry).\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\nfunction hashContent(raw: string): string {\n return createHash(\"sha256\").update(raw).digest(\"hex\");\n}\n\n/**\n * Return true if `topics.yaml` has an mtime strictly greater than the\n * index DB's mtime. This is the topics-side mirror of `pagesNewerThan`\n * — mutations to `topics.yaml` (title/description/parents) aren't\n * visible from any page mtime, so we need a separate freshness hook\n * for the file itself.\n *\n * Missing `topics.yaml` → false. Absence is legal and means \"no topic\n * metadata, only whatever pages declare\". A missing file doesn't\n * invalidate the existing index.\n */\nfunction topicsYamlNewerThan(almanacDir: string, dbPath: string): 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/**\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` (with title + description) and rewrite\n * that topic's edges in `topic_parents`. Topics that were ad-hoc-only\n * before (mentioned in page frontmatter, never `almanac topics\n * create`d) get their display name promoted to whatever is in the\n * file.\n *\n * Importantly, we do NOT delete `topics` rows that live only in page\n * frontmatter — those are legal, per the spec (\"any slug mentioned in\n * pages' `topics:` frontmatter gets a row, even if not in\n * topics.yaml\"). We also do NOT clear `topic_parents` wholesale; we\n * rewrite edges for each declared topic but leave untouched rows for\n * ad-hoc topics (which by definition have no declared parents).\n *\n * Missing file = no-op. This is the \"no topic metadata yet\" state and\n * callers shouldn't have to paper over it.\n */\nasync 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 // Collect every slug we consider \"declared\" — in topics.yaml or\n // referenced by any page_topics row. Anything outside this set is\n // stale (e.g., a topic that used to be in the file before a rename\n // or delete, or an ad-hoc slug whose only page just got untagged).\n // Those get removed so `empty-topics` doesn't falsely flag them.\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. We do this\n // last so the upserts above have already 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 { 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","/**\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 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 { 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 { 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 init` 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","import { join } from \"node:path\";\n\nimport type Database from \"better-sqlite3\";\n\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface InfoOptions {\n cwd: string;\n slug?: string;\n stdin?: boolean;\n stdinInput?: string;\n json?: boolean;\n wiki?: string;\n}\n\nexport interface InfoRecord {\n slug: string;\n title: string | null;\n file_path: string;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n supersedes: string[]; // pages that declare superseded_by = this slug\n topics: string[];\n file_refs: Array<{ path: string; is_dir: boolean }>;\n wikilinks_out: string[];\n wikilinks_in: string[];\n cross_wiki_links: Array<{ wiki: string; target: string }>;\n}\n\nexport interface InfoCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac info <slug>` — structured view of one page.\n *\n * Human-readable output groups the fields into labeled sections; `--json`\n * emits the raw record. Bulk mode (`--stdin`) defaults to JSON output\n * (array of records) because interleaved human-readable blobs are hard to\n * parse downstream.\n *\n * Backlinks (`wikilinks_in`) are computed per call — there's no\n * materialized reverse index in slice 2, which keeps indexing simple and\n * the query is trivial at the size of a normal wiki.\n */\nexport async function runInfo(\n options: InfoOptions,\n): Promise<InfoCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const slugs = collectSlugs(options);\n if (slugs.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: info requires a slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n const records: InfoRecord[] = [];\n const missing: string[] = [];\n for (const slug of slugs) {\n const rec = fetchInfo(db, slug);\n if (rec === null) {\n missing.push(slug);\n continue;\n }\n records.push(rec);\n }\n\n const bulk = options.stdin === true;\n const jsonOut = options.json === true || bulk;\n\n // JSON shape rule (consumers depend on this being predictable):\n // --stdin → always array of records (even for a single slug)\n // positional → always a single object (never an array)\n // The previous implementation accidentally reshaped \"positional with\n // zero records\" as an array and positional with one record as an\n // object, which made downstream callers have to sniff the shape.\n // Now: `info --stdin` gives you a list to iterate; `info <slug>` gives\n // you one record to dot-access.\n let stdout: string;\n if (jsonOut) {\n if (bulk) {\n stdout = `${JSON.stringify(records, null, 2)}\\n`;\n } else {\n // Positional mode: we already short-circuited on empty slugs,\n // and a missing page is in `missing[]` — so `records` is either\n // length 1 (found) or length 0 (missing). For length 0 we emit\n // `null` so the shape is still object-ish, not an empty array.\n const only = records[0] ?? null;\n stdout = `${JSON.stringify(only, null, 2)}\\n`;\n }\n } else {\n stdout = records.map(formatHumanReadable).join(\"\\n\");\n }\n\n const stderr = missing\n .map((s) => `almanac: no such page \"${s}\"\\n`)\n .join(\"\");\n return {\n stdout,\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n } finally {\n db.close();\n }\n}\n\nfunction fetchInfo(db: Database.Database, slug: string): InfoRecord | null {\n const pageRow = db\n .prepare<\n [string],\n {\n slug: string;\n title: string | null;\n file_path: string;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n }\n >(\n \"SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?\",\n )\n .get(slug);\n if (pageRow === undefined) return null;\n\n const topics = db\n .prepare<[string], { topic_slug: string }>(\n \"SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug\",\n )\n .all(slug)\n .map((r) => r.topic_slug);\n\n const refs = db\n .prepare<[string], { original_path: string; is_dir: number }>(\n // Display the author's casing (`original_path`), not the\n // lowercased lookup form. The lowercased `path` column is the\n // query key for `--mentions`; it's not a user-facing string.\n \"SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path\",\n )\n .all(slug)\n .map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));\n\n const linksOut = db\n .prepare<[string], { target_slug: string }>(\n \"SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug\",\n )\n .all(slug)\n .map((r) => r.target_slug);\n\n const linksIn = db\n .prepare<[string], { source_slug: string }>(\n \"SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug\",\n )\n .all(slug)\n .map((r) => r.source_slug);\n\n const xwiki = db\n .prepare<[string], { target_wiki: string; target_slug: string }>(\n \"SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug\",\n )\n .all(slug)\n .map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));\n\n // \"Pages that say `supersedes: <me>`\" — the reverse side of this\n // page's `superseded_by` field. We read this from `pages` rather than\n // a dedicated column because the spec (deliberately) doesn't index\n // `supersedes:` as its own table — the relationship is already implicit\n // in whoever points at us.\n const supersedesRows = db\n .prepare<[string], { slug: string }>(\n \"SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug\",\n )\n .all(slug)\n .map((r) => r.slug);\n\n return {\n slug: pageRow.slug,\n title: pageRow.title,\n file_path: pageRow.file_path,\n updated_at: pageRow.updated_at,\n archived_at: pageRow.archived_at,\n superseded_by: pageRow.superseded_by,\n supersedes: supersedesRows,\n topics,\n file_refs: refs,\n wikilinks_out: linksOut,\n wikilinks_in: linksIn,\n cross_wiki_links: xwiki,\n };\n}\n\nfunction collectSlugs(options: InfoOptions): string[] {\n if (options.stdin === true && options.stdinInput !== undefined) {\n return options.stdinInput\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (options.slug !== undefined && options.slug.length > 0) {\n return [options.slug];\n }\n return [];\n}\n\nfunction formatHumanReadable(rec: InfoRecord): string {\n const lines: string[] = [];\n lines.push(`slug: ${rec.slug}`);\n lines.push(`title: ${rec.title ?? \"—\"}`);\n lines.push(`file: ${rec.file_path}`);\n lines.push(`updated_at: ${new Date(rec.updated_at * 1000).toISOString()}`);\n if (rec.archived_at !== null) {\n lines.push(\n `archived_at: ${new Date(rec.archived_at * 1000).toISOString()}`,\n );\n }\n if (rec.superseded_by !== null) {\n lines.push(`superseded_by: ${rec.superseded_by}`);\n }\n if (rec.supersedes.length > 0) {\n lines.push(`supersedes: ${rec.supersedes.join(\", \")}`);\n }\n lines.push(`topics: ${rec.topics.length > 0 ? rec.topics.join(\", \") : \"—\"}`);\n lines.push(\"file_refs:\");\n if (rec.file_refs.length === 0) {\n lines.push(\" —\");\n } else {\n for (const r of rec.file_refs) {\n lines.push(` ${r.path}${r.is_dir ? \" (dir)\" : \"\"}`);\n }\n }\n lines.push(\"wikilinks_out:\");\n if (rec.wikilinks_out.length === 0) lines.push(\" —\");\n else for (const t of rec.wikilinks_out) lines.push(` ${t}`);\n lines.push(\"wikilinks_in:\");\n if (rec.wikilinks_in.length === 0) lines.push(\" —\");\n else for (const s of rec.wikilinks_in) lines.push(` ${s}`);\n if (rec.cross_wiki_links.length > 0) {\n lines.push(\"cross_wiki_links:\");\n for (const x of rec.cross_wiki_links) {\n lines.push(` ${x.wiki}:${x.target}`);\n }\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import { existsSync } from \"node:fs\";\n\nimport {\n dropEntry,\n readRegistry,\n type RegistryEntry,\n} from \"../registry/index.js\";\n\nexport interface ListOptions {\n json?: boolean;\n drop?: string;\n}\n\nexport interface ListCommandOutput {\n stdout: string;\n exitCode: number;\n}\n\n/**\n * `almanac list` — the global discovery surface. Three modes:\n *\n * - default: pretty table of reachable wikis\n * - `--json`: structured output (reachable wikis only, by default)\n * - `--drop <name>`: explicit removal, then exits\n *\n * **Unreachable paths are silently skipped in the default output**, but\n * never auto-dropped. This is the registry hygiene rule from the design —\n * branch switches, unmounted drives, and VM-offline repos should not cost\n * the user a registration. Only `--drop` removes entries.\n */\nexport async function listWikis(\n options: ListOptions,\n): Promise<ListCommandOutput> {\n if (options.drop !== undefined) {\n return handleDrop(options.drop);\n }\n\n const entries = await readRegistry();\n const reachable = entries.filter((e) => isReachable(e));\n\n if (options.json === true) {\n return { stdout: `${JSON.stringify(reachable, null, 2)}\\n`, exitCode: 0 };\n }\n\n return { stdout: formatPretty(reachable), exitCode: 0 };\n}\n\nasync function handleDrop(name: string): Promise<ListCommandOutput> {\n const removed = await dropEntry(name);\n if (removed === null) {\n return {\n stdout: `no registry entry named \"${name}\"\\n`,\n exitCode: 1,\n };\n }\n return {\n stdout: `removed \"${removed.name}\" (${removed.path})\\n`,\n exitCode: 0,\n };\n}\n\n/**\n * A registry path is \"reachable\" if something still exists at that path.\n * We use `existsSync` rather than `stat` — we don't care whether the path\n * is a directory or has a `.almanac/` inside; we only hide it from default\n * output when the path itself is gone (e.g., repo deleted, drive\n * unmounted).\n */\nfunction isReachable(entry: RegistryEntry): boolean {\n if (entry.path.length === 0) return false;\n return existsSync(entry.path);\n}\n\n/**\n * Human-readable listing. Empty state prints a gentle hint rather than a\n * blank screen, and entries render in registration order (chronological,\n * since `addEntry` appends).\n */\nfunction formatPretty(entries: RegistryEntry[]): string {\n if (entries.length === 0) {\n return \"no wikis registered. run `almanac init` in a repo to create one.\\n\";\n }\n\n // Column-width the name for alignment; cap at 30 so absurd names don't\n // stretch the whole table.\n const nameWidth = Math.min(\n 30,\n entries.reduce((w, e) => Math.max(w, e.name.length), 0),\n );\n\n const lines: string[] = [];\n for (const entry of entries) {\n const name = entry.name.padEnd(nameWidth);\n const desc = entry.description.length > 0 ? entry.description : \"—\";\n lines.push(`${name} ${desc}`);\n lines.push(`${\" \".repeat(nameWidth)} ${entry.path}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import { join } from \"node:path\";\n\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface PathOptions {\n cwd: string;\n slug?: string;\n stdin?: boolean;\n stdinInput?: string;\n wiki?: string;\n}\n\nexport interface PathCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac path <slug>` — slug → absolute file path.\n *\n * Pure scripting helper. With `--stdin` it maps each line of input to its\n * resolved path; unresolvable slugs go to stderr with a non-zero exit\n * while the resolvable ones still print on stdout.\n */\nexport async function runPath(\n options: PathOptions,\n): Promise<PathCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const slugs = collectSlugs(options);\n if (slugs.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: path requires a slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n const stmt = db.prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n );\n const resolved: string[] = [];\n const missing: string[] = [];\n for (const slug of slugs) {\n const row = stmt.get(slug);\n if (row === undefined) {\n missing.push(slug);\n continue;\n }\n resolved.push(row.file_path);\n }\n\n const stdout = resolved.length > 0 ? `${resolved.join(\"\\n\")}\\n` : \"\";\n const stderr = missing\n .map((s) => `almanac: no such page \"${s}\"\\n`)\n .join(\"\");\n return {\n stdout,\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n } finally {\n db.close();\n }\n}\n\nfunction collectSlugs(options: PathOptions): string[] {\n if (options.stdin === true && options.stdinInput !== undefined) {\n return options.stdinInput\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (options.slug !== undefined && options.slug.length > 0) {\n return [options.slug];\n }\n return [];\n}\n","import { runIndexer, type IndexResult } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\n\nexport interface ReindexOptions {\n cwd: string;\n wiki?: string;\n}\n\nexport interface ReindexCommandOutput {\n result: IndexResult;\n stdout: string;\n exitCode: number;\n}\n\n/**\n * `almanac reindex` — force a full rebuild.\n *\n * Unlike the implicit reindex every query command triggers, this one\n * prints a summary line so the user gets feedback for an explicitly\n * requested action. The summary is terse on purpose (one line, three\n * numbers) — verbose progress reporting would fight the design rule that\n * the CLI stays quiet by default.\n */\nexport async function runReindex(\n options: ReindexOptions,\n): Promise<ReindexCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n const result = await runIndexer({ repoRoot });\n // Summary wording: \"reindexed: N pages (K updated, R removed)\". When\n // some files were on disk but never made it into the index\n // (slug collisions, ENOENT races, un-sluggable filenames), tack on a\n // `; S skipped` suffix so the user notices. The per-file reason was\n // already written to stderr at indexing time.\n const skipSuffix =\n result.filesSkipped > 0 ? `; ${result.filesSkipped} skipped` : \"\";\n const stdout = `reindexed: ${result.pagesIndexed} page${result.pagesIndexed === 1 ? \"\" : \"s\"} (${result.changed} updated, ${result.removed} removed${skipSuffix})\\n`;\n return { result, stdout, exitCode: 0 };\n}\n","import { join } from \"node:path\";\n\nimport type Database from \"better-sqlite3\";\n\nimport { parseDuration } from \"../indexer/duration.js\";\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { looksLikeDir, normalizePath } from \"../indexer/paths.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface SearchOptions {\n cwd: string;\n query?: string;\n topics: string[];\n mentions?: string;\n since?: string;\n stale?: string;\n orphan?: boolean;\n includeArchive?: boolean;\n archived?: boolean;\n wiki?: string;\n json?: boolean;\n limit?: number;\n}\n\nexport interface SearchResult {\n slug: string;\n title: string | null;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n topics: string[];\n}\n\nexport interface SearchCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac search` — the core query surface.\n *\n * Filters compose with AND logic. The implementation is deliberately\n * pedestrian: build a list of clauses + params, join them with `AND`,\n * intersect topic filters by requiring one subquery per `--topic`. No\n * clever query-planner tricks needed for the sizes we handle (<10k pages).\n *\n * All output ordering is stable: `updated_at DESC, slug ASC`. FTS5 rank\n * is layered on top when the user passed a text query — we ORDER BY rank\n * first, then fall through to the default.\n */\nexport async function runSearch(\n options: SearchOptions,\n): Promise<SearchCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const rows = executeQuery(db, options);\n const limited =\n options.limit !== undefined && options.limit >= 0\n ? rows.slice(0, options.limit)\n : rows;\n\n const stdout = formatResults(limited, options);\n const stderr = buildStderr(limited, options);\n return { stdout, stderr, exitCode: 0 };\n } finally {\n db.close();\n }\n}\n\ninterface PageRow {\n slug: string;\n title: string | null;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n}\n\nfunction executeQuery(\n db: Database.Database,\n options: SearchOptions,\n): SearchResult[] {\n const whereClauses: string[] = [];\n const params: (string | number)[] = [];\n\n // Archive scope. Three modes, mutually exclusive in practice:\n // - default → active only\n // - --include-archive → active + archived\n // - --archived → archived only\n // `--archived` wins over `--include-archive` when both are passed —\n // being explicit about \"only archived\" is strictly narrower than\n // \"include archived\", so intersecting them yields \"only archived\".\n if (options.archived === true) {\n whereClauses.push(\"p.archived_at IS NOT NULL\");\n } else if (options.includeArchive !== true) {\n whereClauses.push(\"p.archived_at IS NULL\");\n }\n\n // --topic foo --topic bar → page must have BOTH. We add one EXISTS\n // subquery per topic rather than grouping, which keeps param order\n // simple and the plan readable.\n for (const rawTopic of options.topics) {\n const topicSlug = slugForTopic(rawTopic);\n if (topicSlug.length === 0) continue;\n whereClauses.push(\n \"EXISTS (SELECT 1 FROM page_topics pt WHERE pt.page_slug = p.slug AND pt.topic_slug = ?)\",\n );\n params.push(topicSlug);\n }\n\n // --mentions: look for a file_refs row on this page that either\n // matches exactly, OR is a containing folder (is_dir=1 and the query\n // path starts with the row's path), OR the row itself lives inside\n // the queried folder. See spec → \"Graph querying → Query examples\".\n //\n // We deliberately avoid GLOB on the RHS of the comparison with stored\n // `r.path`, because stored paths can legitimately contain GLOB\n // metacharacters — Next.js dynamic routes like `src/[id]/page.tsx`\n // store a literal `[id]`, and `[abc]` is a SQL GLOB character class.\n // Concatenating `r.path || '*'` into a GLOB pattern would make\n // `src/[id]/page.tsx*` match `src/i/page.tsx` (spurious hit on the\n // character class).\n //\n // Instead we enumerate the prefix folders in JS and use parameterized\n // equality. For `src/checkout/handler.ts` the prefixes are\n // `['src/', 'src/checkout/']`; any file_refs row with is_dir=1 and\n // a path in that list is a containing folder of the queried file.\n // This also lets SQLite use `idx_file_refs_path` as an equality\n // probe rather than a range scan.\n if (options.mentions !== undefined && options.mentions.length > 0) {\n const isDir = looksLikeDir(options.mentions);\n const norm = normalizePath(options.mentions, isDir);\n if (isDir) {\n // Query is a folder. Match: the exact folder, OR any file/sub-\n // folder whose path starts with the folder prefix. The prefix\n // match is the one place we still need GLOB — but we escape any\n // wildcard metacharacters in `norm` first so a user-supplied\n // `src/[id]/` query is treated as a literal. Note: the query\n // path comes from the caller, not from stored data, but a user\n // typing `--mentions src/[id]/` should get the literal folder,\n // not a character class.\n const escaped = escapeGlobMeta(norm);\n whereClauses.push(\n `EXISTS (\n SELECT 1 FROM file_refs r\n WHERE r.page_slug = p.slug\n AND (r.path = ? OR r.path GLOB ?)\n )`,\n );\n params.push(norm, `${escaped}*`);\n } else {\n // Query is a file. Match: the exact file, OR any folder whose\n // path is a prefix of this file. Build the prefix list in JS and\n // probe file_refs with equality — no GLOB on stored values.\n const prefixes = parentFolderPrefixes(norm);\n if (prefixes.length === 0) {\n whereClauses.push(\n `EXISTS (\n SELECT 1 FROM file_refs r\n WHERE r.page_slug = p.slug AND r.path = ?\n )`,\n );\n params.push(norm);\n } else {\n const placeholders = prefixes.map(() => \"?\").join(\", \");\n whereClauses.push(\n `EXISTS (\n SELECT 1 FROM file_refs r\n WHERE r.page_slug = p.slug\n AND (\n r.path = ?\n OR (r.is_dir = 1 AND r.path IN (${placeholders}))\n )\n )`,\n );\n params.push(norm, ...prefixes);\n }\n }\n }\n\n const now = Math.floor(Date.now() / 1000);\n\n if (options.since !== undefined) {\n const seconds = parseDuration(options.since);\n whereClauses.push(\"p.updated_at >= ?\");\n params.push(now - seconds);\n }\n\n if (options.stale !== undefined) {\n const seconds = parseDuration(options.stale);\n whereClauses.push(\"p.updated_at < ?\");\n params.push(now - seconds);\n }\n\n if (options.orphan === true) {\n whereClauses.push(\n \"NOT EXISTS (SELECT 1 FROM page_topics pt WHERE pt.page_slug = p.slug)\",\n );\n }\n\n // FTS5 text query. When a text query is supplied we JOIN against\n // `fts_pages` so we can ORDER BY its `rank` column — lower (more\n // negative) ranks are better matches. Without a query we skip the\n // join entirely.\n let sql: string;\n if (options.query !== undefined && options.query.trim().length > 0) {\n const ftsExpr = buildFtsQuery(options.query);\n sql = `\n SELECT p.slug, p.title, p.updated_at, p.archived_at, p.superseded_by\n FROM pages p\n JOIN fts_pages f ON f.slug = p.slug\n WHERE fts_pages MATCH ?\n ${whereClauses.length > 0 ? `AND ${whereClauses.join(\" AND \")}` : \"\"}\n ORDER BY f.rank ASC, p.updated_at DESC, p.slug ASC\n `;\n // MATCH param goes first (it's the first `?` in the compiled SQL).\n params.unshift(ftsExpr);\n } else {\n sql = buildSql(whereClauses);\n }\n\n const rows = db.prepare<unknown[], PageRow>(sql).all(...params);\n\n // Attach topics in a second pass — simpler than a correlated\n // `GROUP_CONCAT`, and the output rows are small enough that N+1 on a\n // single prepared statement is fine.\n const topicStmt = db.prepare<[string], { topic_slug: string }>(\n \"SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug\",\n );\n const out: SearchResult[] = rows.map((row) => ({\n slug: row.slug,\n title: row.title,\n updated_at: row.updated_at,\n archived_at: row.archived_at,\n superseded_by: row.superseded_by,\n topics: topicStmt.all(row.slug).map((t) => t.topic_slug),\n }));\n\n return out;\n}\n\nfunction buildSql(whereClauses: string[]): string {\n const where =\n whereClauses.length > 0 ? `WHERE ${whereClauses.join(\" AND \")}` : \"\";\n return `\n SELECT p.slug, p.title, p.updated_at, p.archived_at, p.superseded_by\n FROM pages p\n ${where}\n ORDER BY p.updated_at DESC, p.slug ASC\n `;\n}\n\n/**\n * Turn a user query into an FTS5 MATCH expression. FTS5's default\n * grammar gets unhappy with punctuation (hyphens, colons, slashes), so\n * we tokenize into alphanumeric runs and emit a conjunction of prefixed\n * tokens. Each token is suffixed with `*` so \"stri\" matches \"stripe\".\n *\n * Quoted input (`\"stripe webhook\"`) is treated as an FTS5 phrase query\n * — tokens must appear contiguously in that order. This matches shell\n * conventions where quoting something means \"match it literally\". We\n * strip the surrounding quotes, collapse inner punctuation to spaces,\n * and re-wrap in quotes for FTS5's phrase syntax. Any embedded `\"` in\n * the user input is dropped (FTS5 phrase syntax has no escape).\n *\n * Anything that tokenizes to empty (e.g. pure punctuation) falls back\n * to an empty MATCH, which yields no rows — which is the right answer.\n */\nfunction buildFtsQuery(raw: string): string {\n const trimmed = raw.trim();\n if (\n trimmed.length >= 2 &&\n trimmed.startsWith(\"\\\"\") &&\n trimmed.endsWith(\"\\\"\")\n ) {\n // Phrase mode. Strip outer quotes, lowercase, collapse non-alnum\n // runs to a single space, trim. Any surviving inner `\"` are\n // removed since FTS5 phrase syntax has no escape mechanism.\n const inner = trimmed\n .slice(1, -1)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \")\n .trim();\n if (inner.length === 0) return \"\\\"\\\"\";\n return `\"${inner}\"`;\n }\n const tokens = trimmed\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 0);\n if (tokens.length === 0) return \"\\\"\\\"\";\n return tokens.map((t) => `${t}*`).join(\" AND \");\n}\n\nfunction slugForTopic(raw: string): string {\n return raw\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * For a normalized file path like `src/checkout/handler.ts`, enumerate\n * every containing folder in the form those folders are stored (trailing\n * slash): `['src/', 'src/checkout/']`. Empty for paths with no folder\n * separator (e.g. `README.md`) — a top-level file has no parent folder\n * recorded in `file_refs`.\n *\n * Used by the `--mentions <file>` path to let us probe `file_refs` with\n * equality instead of GLOB, sidestepping wildcard-escape bugs entirely.\n */\nfunction parentFolderPrefixes(filePath: string): string[] {\n const out: string[] = [];\n let cursor = 0;\n while (true) {\n const next = filePath.indexOf(\"/\", cursor);\n if (next === -1) break;\n // Slice includes the slash — matches how directories are stored.\n out.push(filePath.slice(0, next + 1));\n cursor = next + 1;\n }\n return out;\n}\n\n/**\n * Escape SQLite GLOB metacharacters (`*`, `?`, `[`) by wrapping each in\n * a single-character class. SQLite GLOB has no backslash-escape, so the\n * idiomatic trick is `[*]` → literal `*`, `[?]` → literal `?`, `[[]` →\n * literal `[`. `]` doesn't need escaping outside a class.\n *\n * We only need this on the *query* side — stored paths aren't\n * concatenated into GLOB patterns anymore (see --mentions handling).\n * But a user-typed `--mentions src/[id]/` should still match a stored\n * `src/[id]/` literally, not as a character class over `i` or `d`.\n */\nfunction escapeGlobMeta(s: string): string {\n return s.replace(/[\\*\\?\\[]/g, (ch) => `[${ch}]`);\n}\n\nfunction formatResults(\n rows: SearchResult[],\n options: SearchOptions,\n): string {\n if (options.json === true) {\n return `${JSON.stringify(rows, null, 2)}\\n`;\n }\n // Default output: one slug per line. Empty result = empty output (not\n // \"no results found\") — makes piping into xargs / subsequent commands\n // degrade gracefully.\n if (rows.length === 0) return \"\";\n return `${rows.map((r) => r.slug).join(\"\\n\")}\\n`;\n}\n\nfunction buildStderr(rows: SearchResult[], options: SearchOptions): string {\n // Spec: \"print warns if >50 when not --json\". The warning goes to\n // stderr so it doesn't corrupt pipelines that filter stdout.\n if (options.json === true) return \"\";\n if (options.limit !== undefined) return \"\";\n if (rows.length > 50) {\n return `almanac: ${rows.length} results — consider --limit or a narrower query\\n`;\n }\n return \"\";\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface ShowOptions {\n cwd: string;\n slug?: string;\n stdin?: boolean;\n stdinInput?: string; // injected by the CLI when --stdin is set; tests can pass directly\n wiki?: string;\n}\n\nexport interface ShowCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac show <slug>` — cat the markdown file for a page.\n *\n * This is the one command that is allowed to read page file contents;\n * everything else operates on the index (per the spec's design\n * principles).\n *\n * Output shapes:\n * - positional slug → raw markdown on stdout, unchanged (the cat case)\n * - `--stdin` → JSON Lines: one `{slug, content}` object per\n * successfully-read page, terminated by `\\n`. JSON\n * Lines is parseable, unlike the old `\\n---\\n`\n * separator which collided with YAML frontmatter\n * delimiters in the page bodies themselves.\n *\n * Unresolvable slugs go to stderr with a non-zero exit; we still print\n * whatever we could resolve on stdout so bulk runs don't have to fail\n * atomically.\n */\nexport async function runShow(\n options: ShowOptions,\n): Promise<ShowCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const slugs = collectSlugs(options);\n if (slugs.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: show requires a slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n const stmt = db.prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n );\n\n const records: Array<{ slug: string; content: string }> = [];\n const missing: string[] = [];\n for (const slug of slugs) {\n const row = stmt.get(slug);\n if (row === undefined) {\n missing.push(slug);\n continue;\n }\n try {\n records.push({ slug, content: await readFile(row.file_path, \"utf8\") });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n missing.push(`${slug} (${message})`);\n }\n }\n\n const bulk = options.stdin === true;\n let stdout: string;\n if (bulk) {\n // JSON Lines: one object per line, trailing newline on the last.\n // Consumers can split on `\\n` and `JSON.parse` each line.\n stdout = records\n .map((r) => JSON.stringify(r))\n .join(\"\\n\");\n if (stdout.length > 0) stdout += \"\\n\";\n } else {\n // Positional single-slug: just emit the raw markdown. No\n // separator, no wrapping — this is the \"cat\" case.\n stdout = records.map((r) => r.content).join(\"\");\n }\n const stderr = missing\n .map((s) => `almanac: no such page \"${s}\"\\n`)\n .join(\"\");\n return {\n stdout,\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n } finally {\n db.close();\n }\n}\n\nfunction collectSlugs(options: ShowOptions): string[] {\n if (options.stdin === true && options.stdinInput !== undefined) {\n return options.stdinInput\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (options.slug !== undefined && options.slug.length > 0) {\n return [options.slug];\n }\n return [];\n}\n","import { readFile, rename, writeFile } from \"node:fs/promises\";\n\nimport yaml from \"js-yaml\";\n\n/**\n * Rewrite the `topics:` field in a markdown file's YAML frontmatter.\n *\n * The absolute requirement is **body byte-preservation**. Tag/untag/\n * rename commands touch only the frontmatter; everything after the\n * closing `---` must be byte-identical to the input (same line endings,\n * same trailing whitespace, same final-newline-or-not). We lean on a\n * precise split rather than re-serializing the whole file so any\n * incidental bytes in the body (literal `\\r\\n`, unusual trailing\n * whitespace, triple-hyphen rulers) are left alone.\n *\n * For the frontmatter itself we do a **surgical rewrite of the `topics:`\n * field only** — not a full YAML roundtrip. Reasons:\n * - `js-yaml` normalizes quoting, re-orders keys alphabetically by\n * default, and can drop comments. Any of those would surprise a user\n * who hand-edited their own frontmatter and expects their formatting\n * preserved byte-for-byte.\n * - We only care about one field. Replacing just that field lets the\n * rest of the YAML survive verbatim.\n *\n * The strategy:\n * 1. Find the exact span of the `topics:` key (whether flow\n * `topics: [a, b]` or block `topics:\\n - a\\n - b\\n`) using a\n * small line scanner.\n * 2. Compute the new topics list from the caller's transform.\n * 3. Replace the span with a freshly-emitted `topics: [a, b, c]` line\n * (flow style — compact, readable, and what most authors write).\n * 4. If no `topics:` key exists and the new list is non-empty, append\n * a line to the end of the frontmatter block.\n * 5. If `topics:` exists but the new list is empty, drop the line\n * entirely rather than leaving `topics: []` around.\n *\n * The transform function gets the current deduplicated topic list and\n * returns the new list. Returning an empty array means \"remove the\n * `topics:` key entirely\".\n */\n\nexport interface RewriteResult {\n /** The page's topics before the rewrite (possibly empty). */\n before: string[];\n /** The page's topics after the rewrite (possibly empty). */\n after: string[];\n /** True iff the file content actually changed. */\n changed: boolean;\n}\n\n/**\n * Read `filePath`, compute the new topics via `transform`, and\n * atomically rewrite if the result differs.\n *\n * Atomic per file: write to `<path>.tmp` then rename, same pattern as\n * the registry and topics.yaml writers. A half-written page would\n * corrupt committed user content, so this is non-negotiable.\n */\nexport async function rewritePageTopics(\n filePath: string,\n transform: (current: string[]) => string[],\n): Promise<RewriteResult> {\n const raw = await readFile(filePath, \"utf8\");\n const { before, after, output, changed } = applyTopicsTransform(\n raw,\n transform,\n );\n if (changed) {\n const tmp = `${filePath}.tmp`;\n await writeFile(tmp, output, \"utf8\");\n await rename(tmp, filePath);\n }\n return { before, after, changed };\n}\n\ninterface TransformApplied {\n before: string[];\n after: string[];\n output: string;\n changed: boolean;\n}\n\n/**\n * Pure-string version of `rewritePageTopics`. Useful for tests and for\n * the few places (rename, delete) where we loop many files and want to\n * short-circuit no-op writes cheaply.\n */\nexport function applyTopicsTransform(\n raw: string,\n transform: (current: string[]) => string[],\n): TransformApplied {\n const parsed = splitFrontmatter(raw);\n if (parsed === null) {\n // No frontmatter at all. Tagging a topic on such a page means\n // creating a frontmatter block. We keep the body untouched and\n // prepend `---\\ntopics: [...]\\n---\\n\\n`. If the transform yields\n // an empty list, this is a no-op. Line endings: default to LF for a\n // brand-new frontmatter — we can't infer intent from a file that\n // doesn't have frontmatter yet, and LF is the committed default in\n // most modern repos.\n const next = dedupeSlugs(transform([]));\n if (next.length === 0) {\n return { before: [], after: [], output: raw, changed: false };\n }\n const fm = `---\\ntopics: ${flowList(next)}\\n---\\n\\n`;\n return {\n before: [],\n after: next,\n output: `${fm}${raw}`,\n changed: true,\n };\n }\n\n const { opener, fmLines, closer, body, eol } = parsed;\n const { before, existingRange } = readTopicsFromLines(fmLines);\n const beforeDeduped = dedupeSlugs(before);\n const after = dedupeSlugs(transform(beforeDeduped));\n\n if (arraysEqual(beforeDeduped, after)) {\n return { before: beforeDeduped, after, output: raw, changed: false };\n }\n\n let nextFmLines: string[];\n if (existingRange === null) {\n // No `topics:` key currently present. Add one (only if non-empty).\n if (after.length === 0) {\n return { before: beforeDeduped, after, output: raw, changed: false };\n }\n nextFmLines = [...fmLines, `topics: ${flowList(after)}`];\n } else {\n const replacement =\n after.length === 0 ? null : `topics: ${flowList(after)}`;\n // Interleaved comments/blank lines from a block-style list are\n // re-emitted BELOW the new flow-style `topics:` line so the\n // author's commentary sticks around. Flow/scalar inputs produce an\n // empty `preserved` array, so this collapses to the old behavior\n // for the common case. When we fully delete the key (empty after)\n // the preserved lines go too — without a `topics:` key to anchor\n // them to, trailing \"# below the topics list\" comments become\n // orphans that no longer mean what they said.\n const preservedTail =\n replacement === null ? [] : existingRange.preserved;\n nextFmLines = [\n ...fmLines.slice(0, existingRange.start),\n ...(replacement === null ? [] : [replacement]),\n ...preservedTail,\n ...fmLines.slice(existingRange.end),\n ];\n }\n\n // Rejoin with the same line ending the input frontmatter used so a\n // CRLF-authored file comes out CRLF end-to-end. `splitFrontmatter`\n // sniffed the dominant separator for us.\n const fmBlock =\n nextFmLines.length === 0 ? \"\" : `${nextFmLines.join(eol)}${eol}`;\n const output = `${opener}${fmBlock}${closer}${body}`;\n return {\n before: beforeDeduped,\n after,\n output,\n changed: true,\n };\n}\n\ninterface SplitFrontmatter {\n /** The opening `---\\n` or `---\\r\\n`. */\n opener: string;\n /** Frontmatter lines (no line-ending character included). */\n fmLines: string[];\n /** The closing `---\\n` (or `---\\r\\n`, possibly without trailing newline if EOF). */\n closer: string;\n /** Everything after the closing fence, byte-for-byte. */\n body: string;\n /**\n * Dominant line ending inside the frontmatter block. CRLF-authored\n * files stay CRLF on write; LF stays LF. We sniff once at split time\n * so rewriting doesn't have to re-inspect every line.\n */\n eol: \"\\n\" | \"\\r\\n\";\n}\n\n/**\n * Split a file into (opener, frontmatter lines, closer, body). The\n * regex mirrors `parseFrontmatter` in `indexer/frontmatter.ts` so the\n * indexer and the rewriter agree on what \"has frontmatter\" means.\n *\n * Returns `null` when the file doesn't start with a `---` fence or\n * lacks a matching closer — both cases are legal (a page with only\n * body content) and the caller treats them as \"no frontmatter to\n * rewrite\".\n */\nfunction splitFrontmatter(raw: string): SplitFrontmatter | null {\n if (!raw.startsWith(\"---\")) return null;\n // Match the exact opener (with its line ending) so we can preserve\n // it byte-for-byte.\n const openerMatch = raw.match(/^---(\\r?\\n)/);\n if (openerMatch === null) return null;\n const opener = `---${openerMatch[1] ?? \"\\n\"}`;\n const rest = raw.slice(opener.length);\n // Find the closing `---` that begins at the start of a line. We\n // also handle the edge case where the closer sits at position 0 of\n // `rest` (frontmatter was empty).\n let fenceIdx: number;\n if (rest.startsWith(\"---\")) {\n fenceIdx = 0;\n } else {\n const m = rest.match(/\\r?\\n---(\\r?\\n|$)/);\n if (m === null || m.index === undefined) return null;\n // `m.index` points at the `\\r?\\n` before `---`; skip that newline\n // so fenceIdx lands exactly on the `-`.\n const leadingNewlineLen = (m[0] ?? \"\").startsWith(\"\\r\\n\") ? 2 : 1;\n fenceIdx = m.index + leadingNewlineLen;\n }\n const fmBlock = rest.slice(0, fenceIdx);\n // Determine the closer's full span, including its trailing newline if any.\n const afterDashes = rest.slice(fenceIdx + 3);\n let closerTail = \"\";\n if (afterDashes.startsWith(\"\\r\\n\")) {\n closerTail = \"\\r\\n\";\n } else if (afterDashes.startsWith(\"\\n\")) {\n closerTail = \"\\n\";\n }\n const closer = `---${closerTail}`;\n const body = afterDashes.slice(closerTail.length);\n const fmLines =\n fmBlock.length === 0 ? [] : fmBlock.replace(/\\r?\\n$/, \"\").split(/\\r?\\n/);\n // Sniff the frontmatter's dominant line ending. We look at the\n // opener first (most reliable signal — it's always present and\n // always has an ending). Fall back to checking the fmBlock for any\n // `\\r\\n` runs so a frontmatter with a single-line opener and\n // multi-line body still gets classified right.\n const eol: \"\\n\" | \"\\r\\n\" =\n opener.endsWith(\"\\r\\n\") || /\\r\\n/.test(fmBlock) ? \"\\r\\n\" : \"\\n\";\n return { opener, fmLines, closer, body, eol };\n}\n\ninterface ExistingRange {\n /** Index in `fmLines` of the `topics:` key line (inclusive). */\n start: number;\n /** Index in `fmLines` one past the last line belonging to this key. */\n end: number;\n /**\n * Lines inside `[start+1, end)` that aren't `- entry` lines — i.e.\n * interleaved comments and blank lines a user wrote between entries.\n * We preserve these verbatim when rewriting block-style lists to\n * flow; otherwise a `tag` on a commented list would silently drop\n * the commentary. Empty for flow/scalar shapes.\n */\n preserved: string[];\n}\n\n/**\n * Find `topics:` in a frontmatter-lines array and read the values.\n *\n * Handles three YAML shapes authors commonly write:\n * - `topics: [a, b, c]` (flow sequence, one line)\n * - `topics:` followed by block entries like ` - a` (block sequence)\n * - `topics: a` (a single scalar — treated as one element)\n *\n * Also handles the empty case `topics:` with nothing after it, and the\n * \"no topics key\" case (returns `existingRange: null`).\n *\n * This is NOT a general YAML parser — it's intentionally scoped to the\n * one key we mutate, because using `js-yaml` for a round-trip would\n * lose comments and re-quote strings the user picked a specific way.\n */\nfunction readTopicsFromLines(fmLines: string[]): {\n before: string[];\n existingRange: ExistingRange | null;\n} {\n const keyLineIdx = findTopKey(fmLines, \"topics\");\n if (keyLineIdx === -1) {\n return { before: [], existingRange: null };\n }\n const keyLine = fmLines[keyLineIdx] ?? \"\";\n const colonIdx = keyLine.indexOf(\":\");\n // Everything to the right of the first colon, trimmed.\n const after = keyLine.slice(colonIdx + 1).trim();\n // Strip trailing `# ...` line-comment from a flow value so we don't\n // parse comments as list contents. (A block list's sub-items have\n // their own comments stripped in the block branch below.)\n const afterNoComment = stripTrailingComment(after);\n\n if (afterNoComment.length === 0) {\n // Block sequence style: collect subsequent `- item` lines. Between\n // entries a user may have written:\n // - interleaved `# comment` lines\n // - blank lines\n // We must NOT break the scan on those — doing so would drop every\n // entry after the first comment/blank when we rewrite (silent data\n // loss: the original bug that triggered this fix). We skip them in\n // the scan and stash them in `preserved` so the replacement step\n // can re-emit them verbatim BETWEEN the new flow-style line and\n // the rest of the frontmatter.\n //\n // Edge: comments/blanks that appear BEFORE the first `- entry` or\n // AFTER the last `- entry` count as part of the block too — pulling\n // them out keeps the author's commentary near the list it belongs\n // to. We cap the scan when we hit a real non-entry line (e.g. the\n // next top-level key), leaving everything from that line onward\n // outside the range.\n const values: string[] = [];\n const preserved: string[] = [];\n // Provisional scan cursor. `endIdx` only advances when we've seen\n // something we're sure belongs to this block (an entry line), so\n // trailing whitespace/comments that don't precede another entry\n // stay OUTSIDE the range and aren't shuffled on rewrite.\n let i = keyLineIdx + 1;\n let endIdx = i;\n // `pendingNonEntries` holds comments/blanks we've seen since the\n // last confirmed entry. They're committed to `preserved` only\n // when a subsequent `- entry` proves they live mid-list.\n let pendingNonEntries: string[] = [];\n while (i < fmLines.length) {\n const line = fmLines[i] ?? \"\";\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed.startsWith(\"#\")) {\n pendingNonEntries.push(line);\n i += 1;\n continue;\n }\n const m = line.match(/^\\s*-\\s+(.*)$/);\n if (m === null) break;\n // Promote any pending comments/blanks — they're between entries\n // (or before the first entry within the block).\n if (pendingNonEntries.length > 0) {\n preserved.push(...pendingNonEntries);\n pendingNonEntries = [];\n }\n const raw = stripTrailingComment((m[1] ?? \"\").trim());\n const parsed = parseScalar(raw);\n if (parsed.length > 0) values.push(parsed);\n i += 1;\n endIdx = i;\n }\n return {\n before: values,\n existingRange: { start: keyLineIdx, end: endIdx, preserved },\n };\n }\n\n // Flow / scalar shape on one line. Let js-yaml handle the value-parsing\n // (quoting, escapes, etc.) for the RHS only.\n let parsed: unknown;\n try {\n parsed = yaml.load(afterNoComment);\n } catch {\n parsed = null;\n }\n const values: string[] = [];\n if (Array.isArray(parsed)) {\n for (const v of parsed) {\n if (typeof v === \"string\" && v.trim().length > 0) {\n values.push(v.trim());\n }\n }\n } else if (typeof parsed === \"string\" && parsed.trim().length > 0) {\n values.push(parsed.trim());\n }\n return {\n before: values,\n existingRange: { start: keyLineIdx, end: keyLineIdx + 1, preserved: [] },\n };\n}\n\n/**\n * Find a top-level key line. \"Top-level\" means no leading whitespace —\n * we don't walk into nested mappings. The indexer's frontmatter parser\n * only reads top-level keys too, so this matches.\n */\nfunction findTopKey(fmLines: string[], key: string): number {\n const re = new RegExp(`^${escapeRegex(key)}\\\\s*:`);\n for (let i = 0; i < fmLines.length; i += 1) {\n if (re.test(fmLines[i] ?? \"\")) return i;\n }\n return -1;\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction stripTrailingComment(s: string): string {\n // Only strip `#` outside of quotes. For the shapes we handle —\n // slug-like kebab-case topics — quoted strings with `#` are rare, but\n // be defensive.\n let inSingle = false;\n let inDouble = false;\n for (let i = 0; i < s.length; i += 1) {\n const ch = s[i];\n if (ch === \"'\" && !inDouble) inSingle = !inSingle;\n else if (ch === '\"' && !inSingle) inDouble = !inDouble;\n else if (ch === \"#\" && !inSingle && !inDouble) {\n return s.slice(0, i).trimEnd();\n }\n }\n return s;\n}\n\n/**\n * Strip YAML quoting from a scalar. Block-sequence items might be\n * written as `- 'foo'` or `- \"foo\"` or bare `- foo`; we accept all\n * three and return the plain string.\n */\nfunction parseScalar(s: string): string {\n if (s.length === 0) return s;\n if (s.length >= 2 && s[0] === '\"' && s[s.length - 1] === '\"') {\n return s.slice(1, -1);\n }\n if (s.length >= 2 && s[0] === \"'\" && s[s.length - 1] === \"'\") {\n return s.slice(1, -1);\n }\n return s;\n}\n\n/**\n * Emit a flow-style YAML sequence like `[auth, jwt, security]`. We use\n * flow because it's the shape most authors write by hand and stays on\n * one line, which keeps diffs tight. Values are quoted only when\n * necessary — plain kebab-case slugs never need quoting.\n */\nfunction flowList(items: string[]): string {\n return `[${items.map((t) => formatScalar(t)).join(\", \")}]`;\n}\n\nfunction formatScalar(s: string): string {\n // If it's a bare kebab/alnum slug, no quotes. Otherwise fall back to\n // js-yaml for correct escaping. We check against a conservative\n // pattern — anything outside it gets YAML-quoted.\n if (/^[a-z0-9][a-z0-9-]*$/.test(s)) return s;\n return yaml\n .dump(s, { flowLevel: 0, lineWidth: Number.MAX_SAFE_INTEGER })\n .trimEnd();\n}\n\nfunction dedupeSlugs(list: string[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of list) {\n const s = raw.trim();\n if (s.length === 0) continue;\n if (seen.has(s)) continue;\n seen.add(s);\n out.push(s);\n }\n return out;\n}\n\nfunction arraysEqual(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n","import { join } from \"node:path\";\n\n/**\n * `.almanac/topics.yaml` inside a given repo root. Single helper so no\n * caller has to remember where the file lives.\n */\nexport function topicsYamlPath(repoRoot: string): string {\n return join(repoRoot, \".almanac\", \"topics.yaml\");\n}\n\n/**\n * `.almanac/index.db` inside a given repo root. Mirrors `topicsYamlPath`\n * so the topics commands don't have to import from scattered places.\n */\nexport function indexDbPath(repoRoot: string): string {\n return join(repoRoot, \".almanac\", \"index.db\");\n}\n","import { ensureFreshIndex, runIndexer } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport { rewritePageTopics } from \"../topics/frontmatterRewrite.js\";\nimport { indexDbPath, topicsYamlPath } from \"../topics/paths.js\";\nimport {\n ensureTopic,\n loadTopicsFile,\n writeTopicsFile,\n} from \"../topics/yaml.js\";\n\n/**\n * `almanac tag <page> <topic>...` and `almanac untag <page> <topic>`.\n *\n * These are the page-side of the topics system — `topics ...` manages\n * the DAG and metadata; `tag`/`untag` wires concrete pages into\n * topics. Both commands mutate page frontmatter atomically per file\n * and leave body bytes untouched.\n *\n * Auto-creation policy: if a topic passed to `tag` doesn't yet exist\n * in `topics.yaml`, we create a minimal entry for it (title-cased\n * title, no description, no parents). This matches the spec: \"Ensure\n * topic exists in topics.yaml; if not, create a minimal entry.\" We\n * don't silently create topics on `untag` — you can only untag\n * something that was already a topic.\n */\n\nexport interface TagCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport interface TagOptions {\n cwd: string;\n wiki?: string;\n page?: string;\n topics: string[];\n stdin?: boolean;\n stdinInput?: string;\n}\n\nexport interface UntagOptions {\n cwd: string;\n wiki?: string;\n page: string;\n topic: string;\n}\n\nexport async function runTag(options: TagOptions): Promise<TagCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n\n const topics = options.topics\n .map((t) => toKebabCase(t))\n .filter((t) => t.length > 0);\n if (topics.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: tag requires at least one topic\\n\",\n exitCode: 1,\n };\n }\n\n // Bulk mode reads slugs from stdin; single mode uses the positional.\n const pages: string[] = [];\n if (options.stdin === true) {\n if (options.stdinInput === undefined) {\n return {\n stdout: \"\",\n stderr: \"almanac: tag --stdin called without stdin input\\n\",\n exitCode: 1,\n };\n }\n for (const line of options.stdinInput.split(/\\r?\\n/)) {\n const s = line.trim();\n if (s.length > 0) pages.push(s);\n }\n } else if (options.page !== undefined && options.page.length > 0) {\n pages.push(options.page);\n } else {\n return {\n stdout: \"\",\n stderr: \"almanac: tag requires a page slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n // Resolve slugs to file paths from the DB. A stale index is fine for\n // `tag` — we just need to find each page's file; `ensureFreshIndex`\n // runs first so the common path is consistent.\n await ensureFreshIndex({ repoRoot });\n const db = openIndex(indexDbPath(repoRoot));\n\n // Validate every requested page exists BEFORE touching topics.yaml.\n // Previously we auto-created topics first, which meant\n // `almanac tag does-not-exist brand-new` left `brand-new` in\n // topics.yaml as a state leak even though the tag itself errored. We\n // resolve rows up front, then short-circuit with an error (and no\n // mutations) if none of the pages are valid.\n const stmt = db.prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n );\n const resolved: { page: string; filePath: string }[] = [];\n const missing: string[] = [];\n try {\n for (const page of pages) {\n const row = stmt.get(toKebabCase(page));\n if (row === undefined) {\n missing.push(page);\n } else {\n resolved.push({ page, filePath: row.file_path });\n }\n }\n } finally {\n db.close();\n }\n\n // Hard-fail when NO page resolved. We deliberately don't mutate\n // topics.yaml on this path — the user's intent (tag page X) is\n // inarguably unsatisfiable, so we shouldn't leave breadcrumbs.\n //\n // In bulk mode (`--stdin`) some pages might resolve and others\n // won't; keeping the original partial-progress behavior for that case\n // (topics get created, resolved pages get tagged, `missing` are\n // reported on stderr with exitCode 1). The state leak only matters\n // when NOTHING succeeds, and that's the case we're fixing.\n if (resolved.length === 0) {\n const stderr = missing.map((p) => `almanac: no such page \"${p}\"\\n`).join(\"\");\n return {\n stdout: \"\",\n stderr,\n exitCode: 1,\n };\n }\n\n // Auto-create missing topics in topics.yaml. Safe to do now — we have\n // at least one page that will actually end up tagged with them.\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n let fileChanged = false;\n for (const t of topics) {\n // ensureTopic mutates the file; we check presence beforehand so\n // we only write when something actually changes (skip a redundant\n // atomic rewrite + mtime bump).\n const before = file.topics.length;\n ensureTopic(file, t);\n if (file.topics.length > before) fileChanged = true;\n }\n if (fileChanged) {\n await writeTopicsFile(yamlPath, file);\n }\n\n const summary: string[] = [];\n let taggedPages = 0;\n for (const { page, filePath } of resolved) {\n const result = await rewritePageTopics(filePath, (current) => {\n // Preserve existing order; append new topics in the order\n // the caller supplied them. `applyTopicsTransform` will\n // dedupe for us, but we skip redundant work here too.\n const out = [...current];\n for (const t of topics) if (!current.includes(t)) out.push(t);\n return out;\n });\n if (result.changed) {\n taggedPages += 1;\n // Only surface the NEWLY ADDED topics — not the full request.\n // Reporting every requested topic (including ones the page\n // already had) reads like false positives in commit diffs.\n const added = result.after.filter((t) => !result.before.includes(t));\n summary.push(`tagged ${page}: ${added.join(\", \")}`);\n } else {\n summary.push(\n `no change ${page} (already tagged with ${topics.join(\", \")})`,\n );\n }\n }\n\n if (taggedPages > 0 || fileChanged) {\n // Trigger a reindex so downstream queries see the new rows\n // immediately. Writes to page files bumped their mtimes; writes to\n // topics.yaml are caught by `topicsYamlNewerThan`.\n await runIndexer({ repoRoot });\n }\n\n const stderr = missing.map((p) => `almanac: no such page \"${p}\"\\n`).join(\"\");\n return {\n stdout: summary.length > 0 ? `${summary.join(\"\\n\")}\\n` : \"\",\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n}\n\nexport async function runUntag(\n options: UntagOptions,\n): Promise<TagCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const page = toKebabCase(options.page);\n const topic = toKebabCase(options.topic);\n if (page.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: untag requires a page slug\\n\",\n exitCode: 1,\n };\n }\n if (topic.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: untag requires a topic\\n\",\n exitCode: 1,\n };\n }\n\n await ensureFreshIndex({ repoRoot });\n const db = openIndex(indexDbPath(repoRoot));\n let filePath: string;\n try {\n const row = db\n .prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n )\n .get(page);\n if (row === undefined) {\n return {\n stdout: \"\",\n stderr: `almanac: no such page \"${page}\"\\n`,\n exitCode: 1,\n };\n }\n filePath = row.file_path;\n } finally {\n db.close();\n }\n\n const result = await rewritePageTopics(filePath, (current) =>\n current.filter((t) => t !== topic),\n );\n if (result.changed) {\n await runIndexer({ repoRoot });\n }\n\n return {\n stdout: result.changed\n ? `untagged ${page}: ${topic}\\n`\n : `no change ${page} (not tagged with ${topic})\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { ensureFreshIndex, runIndexer } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport { ancestorsInFile, descendantsInDb } from \"../topics/dag.js\";\nimport {\n applyTopicsTransform,\n rewritePageTopics,\n} from \"../topics/frontmatterRewrite.js\";\nimport { indexDbPath, topicsYamlPath } from \"../topics/paths.js\";\nimport {\n ensureTopic,\n findTopic,\n loadTopicsFile,\n titleCase,\n writeTopicsFile,\n type TopicEntry,\n type TopicsFile,\n} from \"../topics/yaml.js\";\n\n/**\n * All `almanac topics <verb>` logic lives here. The CLI dispatches on\n * `verb` and forwards positionals/flags. One module per top-level\n * command group matches the pattern used by `search`, `info`, etc.\n *\n * Design notes:\n * - The module is stateless; every entry function takes a `cwd` and\n * optional `wiki` and is safe to call many times in a test suite.\n * - Mutations go file → DB: write `.almanac/topics.yaml` atomically,\n * then trigger a reindex so the DB reflects the new state by the\n * time the command prints its summary. Reads run after a cheap\n * `ensureFreshIndex` so they always see the latest committed data.\n * - The command functions return a `TopicsCommandOutput`; the CLI\n * layer in `cli.ts` decides how to print. This mirrors the shape of\n * `runSearch`, `runInfo`, and friends.\n */\n\nexport interface TopicsCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport interface TopicsBaseOptions {\n cwd: string;\n wiki?: string;\n json?: boolean;\n}\n\nexport interface TopicsListOptions extends TopicsBaseOptions {}\n\nexport interface TopicsShowOptions extends TopicsBaseOptions {\n slug: string;\n descendants?: boolean;\n}\n\nexport interface TopicsCreateOptions extends TopicsBaseOptions {\n name: string;\n parents?: string[];\n}\n\nexport interface TopicsLinkOptions extends TopicsBaseOptions {\n child: string;\n parent: string;\n}\n\nexport interface TopicsUnlinkOptions extends TopicsLinkOptions {}\n\nexport interface TopicsRenameOptions extends TopicsBaseOptions {\n oldSlug: string;\n newSlug: string;\n}\n\nexport interface TopicsDeleteOptions extends TopicsBaseOptions {\n slug: string;\n}\n\nexport interface TopicsDescribeOptions extends TopicsBaseOptions {\n slug: string;\n description: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// list — `almanac topics`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics` (and `almanac topics list`). Prints one line per\n * known topic — from the DB, which already unions topics.yaml with any\n * ad-hoc slugs found in page frontmatter. Page counts come straight\n * from `page_topics`, which the indexer rebuilt on entry.\n */\nexport async function runTopicsList(\n options: TopicsListOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n await ensureFreshIndex({ repoRoot });\n\n const db = openIndex(indexDbPath(repoRoot));\n try {\n const rows = db\n .prepare<\n [],\n { slug: string; title: string | null; description: string | null; page_count: number }\n >(\n // page_count excludes archived pages — matches the policy used\n // by `topics show` (see `pagesDirectlyTagged`) and by every\n // page-scoped check in `health`. Pick one rule and apply it\n // everywhere; a topic with \"5 pages\" in `topics list` and \"3\n // pages\" in `topics show` is a trust-eroding inconsistency.\n `SELECT t.slug, t.title, t.description,\n (SELECT COUNT(*)\n FROM page_topics pt\n JOIN pages p ON p.slug = pt.page_slug\n WHERE pt.topic_slug = t.slug AND p.archived_at IS NULL\n ) AS page_count\n FROM topics t\n ORDER BY t.slug`,\n )\n .all();\n\n if (options.json === true) {\n return {\n stdout: `${JSON.stringify(rows, null, 2)}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n if (rows.length === 0) {\n return {\n stdout:\n \"no topics. create one with `almanac topics create <name>` or tag a page.\\n\",\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const slugWidth = rows.reduce((w, r) => Math.max(w, r.slug.length), 0);\n const lines = rows.map((r) => {\n const slug = r.slug.padEnd(slugWidth);\n const count = `(${r.page_count} page${r.page_count === 1 ? \"\" : \"s\"})`;\n return `${slug} ${count}`;\n });\n return { stdout: `${lines.join(\"\\n\")}\\n`, stderr: \"\", exitCode: 0 };\n } finally {\n db.close();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// show — `almanac topics show <slug>`\n// ─────────────────────────────────────────────────────────────────────\n\nexport interface TopicsShowRecord {\n slug: string;\n title: string | null;\n description: string | null;\n parents: string[];\n children: string[];\n pages: string[];\n descendants_used?: boolean;\n}\n\n/**\n * `almanac topics show <slug>`. Prints metadata + parents, children,\n * and the page list. `--descendants` widens the page list to include\n * pages tagged with any descendant topic (via the DAG).\n */\nexport async function runTopicsShow(\n options: TopicsShowOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n await ensureFreshIndex({ repoRoot });\n\n const slug = toKebabCase(options.slug);\n if (slug.length === 0) {\n return {\n stdout: \"\",\n stderr: `almanac: empty topic slug\\n`,\n exitCode: 1,\n };\n }\n\n const db = openIndex(indexDbPath(repoRoot));\n try {\n const row = db\n .prepare<\n [string],\n { slug: string; title: string | null; description: string | null }\n >(\"SELECT slug, title, description FROM topics WHERE slug = ?\")\n .get(slug);\n if (row === undefined) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${slug}\"\\n`,\n exitCode: 1,\n };\n }\n\n const parents = db\n .prepare<[string], { parent_slug: string }>(\n \"SELECT parent_slug FROM topic_parents WHERE child_slug = ? ORDER BY parent_slug\",\n )\n .all(slug)\n .map((r) => r.parent_slug);\n\n const children = db\n .prepare<[string], { child_slug: string }>(\n \"SELECT child_slug FROM topic_parents WHERE parent_slug = ? ORDER BY child_slug\",\n )\n .all(slug)\n .map((r) => r.child_slug);\n\n const pageSlugs = options.descendants === true\n ? pagesForSubtree(db, slug)\n : pagesDirectlyTagged(db, slug);\n\n const record: TopicsShowRecord = {\n slug: row.slug,\n title: row.title,\n description: row.description,\n parents,\n children,\n pages: pageSlugs,\n descendants_used: options.descendants === true,\n };\n\n if (options.json === true) {\n return {\n stdout: `${JSON.stringify(record, null, 2)}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n return { stdout: formatShow(record), stderr: \"\", exitCode: 0 };\n } finally {\n db.close();\n }\n}\n\nfunction pagesDirectlyTagged(db: Database.Database, slug: string): string[] {\n return db\n .prepare<[string], { page_slug: string }>(\n `SELECT pt.page_slug\n FROM page_topics pt\n JOIN pages p ON p.slug = pt.page_slug\n WHERE pt.topic_slug = ? AND p.archived_at IS NULL\n ORDER BY pt.page_slug`,\n )\n .all(slug)\n .map((r) => r.page_slug);\n}\n\nfunction pagesForSubtree(db: Database.Database, slug: string): string[] {\n const slugs = [slug, ...descendantsInDb(db, slug)];\n // Deduplicate + preserve order via a Set — a page can belong to\n // multiple topics in the subtree and we only want one row per page.\n const placeholders = slugs.map(() => \"?\").join(\", \");\n const rows = db\n .prepare<unknown[], { page_slug: string }>(\n `SELECT DISTINCT pt.page_slug\n FROM page_topics pt\n JOIN pages p ON p.slug = pt.page_slug\n WHERE pt.topic_slug IN (${placeholders}) AND p.archived_at IS NULL\n ORDER BY pt.page_slug`,\n )\n .all(...slugs);\n return rows.map((r) => r.page_slug);\n}\n\nfunction formatShow(r: TopicsShowRecord): string {\n const lines: string[] = [];\n lines.push(`slug: ${r.slug}`);\n lines.push(`title: ${r.title ?? titleCase(r.slug)}`);\n lines.push(`description: ${r.description ?? \"—\"}`);\n lines.push(\n `parents: ${r.parents.length > 0 ? r.parents.join(\", \") : \"—\"}`,\n );\n lines.push(\n `children: ${r.children.length > 0 ? r.children.join(\", \") : \"—\"}`,\n );\n const pagesLabel = r.descendants_used === true ? \"pages (incl. descendants)\" : \"pages\";\n lines.push(`${pagesLabel}:`);\n if (r.pages.length === 0) {\n lines.push(\" —\");\n } else {\n for (const p of r.pages) lines.push(` ${p}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// create — `almanac topics create <name> [--parent <slug>]...`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics create <name> [--parent <slug>]...`.\n *\n * Policy: `--parent <slug>` MUST refer to an existing topic (created\n * earlier in topics.yaml or indexed from page frontmatter). Auto-\n * creating parents silently would let typos cascade — `create JWT\n * --parent secuirty` would quietly spawn a \"secuirty\" topic. Better to\n * refuse and point the user at `almanac topics create <parent>` first.\n *\n * Already-exists is not an error if no new parents are being added —\n * rerunning the same `create` is a no-op. If new parents are introduced\n * we add them (respecting cycle prevention, just like `link`).\n */\nexport async function runTopicsCreate(\n options: TopicsCreateOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n\n const slug = toKebabCase(options.name);\n if (slug.length === 0) {\n return {\n stdout: \"\",\n stderr: `almanac: topic name \"${options.name}\" has no slug-able characters\\n`,\n exitCode: 1,\n };\n }\n const title = options.name.trim().length > 0 ? options.name.trim() : titleCase(slug);\n\n // Reindex first so an ad-hoc `--parent <slug>` created by a\n // just-written page is visible to `topicExists`. Every other topics\n // command did this; `create` silently skipped the refresh which meant\n // a brand-new page's `topics: [newparent]` couldn't be used as a\n // parent until the next query touched the DB.\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n\n // Hoist the DB open out of the parents loop. We used to open + close\n // the DB inside `isAdHocTopicInDb` for every parent; opening is\n // cheap but not free, and the iteration pattern shows up in other\n // topics commands too.\n const db = openIndex(indexDbPath(repoRoot));\n try {\n // Resolve/validate parents BEFORE mutating the file. All-or-nothing.\n const requestedParents = (options.parents ?? [])\n .map((p) => toKebabCase(p))\n .filter((p) => p.length > 0);\n for (const p of requestedParents) {\n if (p === slug) {\n return {\n stdout: \"\",\n stderr: `almanac: topic cannot be its own parent\\n`,\n exitCode: 1,\n };\n }\n if (!topicExists(file, db, p)) {\n return {\n stdout: \"\",\n stderr: `almanac: parent topic \"${p}\" does not exist; create it first with \\`almanac topics create ${p}\\`\\n`,\n exitCode: 1,\n };\n }\n if (findTopic(file, p) === null) {\n // Topic exists only as an ad-hoc DB entry. Promote it into\n // topics.yaml so it has a proper record. `ensureTopic` is\n // idempotent so this is safe even if two loop iterations\n // reference the same ad-hoc parent.\n ensureTopic(file, p);\n }\n }\n\n const existing = findTopic(file, slug);\n if (existing === null) {\n const entry: TopicEntry = {\n slug,\n title,\n description: null,\n parents: requestedParents,\n };\n file.topics.push(entry);\n } else {\n // Add any new parents, skipping ones that already exist or would\n // create a cycle.\n for (const p of requestedParents) {\n if (existing.parents.includes(p)) continue;\n const ancestors = ancestorsInFile(file, p);\n if (ancestors.has(slug) || p === slug) {\n return {\n stdout: \"\",\n stderr: `almanac: adding \"${p}\" as a parent of \"${slug}\" would create a cycle\\n`,\n exitCode: 1,\n };\n }\n existing.parents.push(p);\n }\n // Promote the user-supplied title only if the existing one was a\n // title-cased default (i.e., they didn't describe it yet). Don't\n // clobber a deliberate title silently.\n if (\n existing.title === titleCase(existing.slug) &&\n title !== titleCase(slug) &&\n title !== existing.title\n ) {\n existing.title = title;\n }\n }\n\n await writeTopicsFile(yamlPath, file);\n await runIndexer({ repoRoot });\n return {\n stdout: existing === null\n ? `created topic \"${slug}\"\\n`\n : `updated topic \"${slug}\"\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n } finally {\n db.close();\n }\n}\n\n/**\n * Is `slug` a known topic anywhere — in `topics.yaml`, or as an ad-hoc\n * slug that a page's frontmatter mentioned and the indexer seeded?\n *\n * Collapses the previous `findTopic(file, s) === null &&\n * !isAdHocTopicInDb(root, s)` duplication into one intent-revealing\n * helper. Takes an open `db` handle so the caller can hoist DB open\n * out of tight loops — every earlier call site created + destroyed a\n * new connection per iteration.\n */\nfunction topicExists(\n file: TopicsFile,\n db: Database.Database,\n slug: string,\n): boolean {\n if (findTopic(file, slug) !== null) return true;\n const row = db\n .prepare<[string], { slug: string }>(\n \"SELECT slug FROM topics WHERE slug = ?\",\n )\n .get(slug);\n return row !== undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// link / unlink\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics link <child> <parent>`. Adds a DAG edge after\n * checking that it wouldn't close a cycle. Both topics must exist.\n */\nexport async function runTopicsLink(\n options: TopicsLinkOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const child = toKebabCase(options.child);\n const parent = toKebabCase(options.parent);\n if (child.length === 0 || parent.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n if (child === parent) {\n return {\n stdout: \"\",\n stderr: `almanac: topic cannot be its own parent\\n`,\n exitCode: 1,\n };\n }\n\n // Refresh the index so ad-hoc topics (ones the user tagged pages\n // with but never formally `topics create`d) are visible to\n // `topicExists` below.\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n\n const db = openIndex(indexDbPath(repoRoot));\n try {\n for (const slug of [child, parent]) {\n if (!topicExists(file, db, slug)) {\n return {\n stdout: \"\",\n stderr: `almanac: topic \"${slug}\" does not exist\\n`,\n exitCode: 1,\n };\n }\n if (findTopic(file, slug) === null) {\n // DB-only ad-hoc topic → promote it into topics.yaml so the\n // new DAG edge has a concrete home.\n ensureTopic(file, slug);\n }\n }\n\n const childEntry = findTopic(file, child);\n if (childEntry === null) {\n // Shouldn't happen after ensureTopic above — defensive.\n return {\n stdout: \"\",\n stderr: `almanac: topic \"${child}\" not found\\n`,\n exitCode: 1,\n };\n }\n\n if (childEntry.parents.includes(parent)) {\n return {\n stdout: `edge ${child} → ${parent} already exists\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Cycle check BEFORE mutation. Uses the in-memory file so the check\n // operates on the state we're about to write — no DB round-trip needed.\n const parentAncestors = ancestorsInFile(file, parent);\n if (parentAncestors.has(child) || parent === child) {\n return {\n stdout: \"\",\n stderr: `almanac: adding ${parent} as parent of ${child} would create a cycle\\n`,\n exitCode: 1,\n };\n }\n\n childEntry.parents.push(parent);\n await writeTopicsFile(yamlPath, file);\n await runIndexer({ repoRoot });\n return {\n stdout: `linked ${child} → ${parent}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n } finally {\n db.close();\n }\n}\n\n/**\n * `almanac topics unlink <child> <parent>`. Removes a DAG edge if it\n * exists. No-op (exit 0) if not. Never deletes topics.\n */\nexport async function runTopicsUnlink(\n options: TopicsUnlinkOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const child = toKebabCase(options.child);\n const parent = toKebabCase(options.parent);\n if (child.length === 0 || parent.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n const childEntry = findTopic(file, child);\n if (childEntry === null || !childEntry.parents.includes(parent)) {\n return {\n stdout: `no edge ${child} → ${parent}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n childEntry.parents = childEntry.parents.filter((p) => p !== parent);\n await writeTopicsFile(yamlPath, file);\n await runIndexer({ repoRoot });\n return {\n stdout: `unlinked ${child} → ${parent}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// rename — `almanac topics rename <old> <new>`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics rename <old> <new>`. Rewrites the slug both in\n * `topics.yaml` (as an entry key and in anyone who declared it as a\n * parent) and in every affected page's frontmatter.\n *\n * Refuses if `<new>` is already a distinct topic — \"merging\" two topics\n * should be explicit, not a silent side effect of a rename.\n */\nexport async function runTopicsRename(\n options: TopicsRenameOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const oldSlug = toKebabCase(options.oldSlug);\n const newSlug = toKebabCase(options.newSlug);\n if (oldSlug.length === 0 || newSlug.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n if (oldSlug === newSlug) {\n return {\n stdout: `topic \"${oldSlug}\" unchanged\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n\n // Hoist the DB handle so the existence checks below don't reopen\n // the index per-slug.\n const db = openIndex(indexDbPath(repoRoot));\n let pagesUpdated: number;\n try {\n // Fetch existence info. `oldInYaml` is kept as a direct reference\n // because we mutate the entry; the DB check is only needed when\n // the slug isn't in the file (ad-hoc-only).\n const oldInYaml = findTopic(file, oldSlug);\n if (!topicExists(file, db, oldSlug)) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${oldSlug}\"\\n`,\n exitCode: 1,\n };\n }\n\n if (topicExists(file, db, newSlug)) {\n return {\n stdout: \"\",\n stderr: `almanac: topic \"${newSlug}\" already exists; delete it first if you intend to merge\\n`,\n exitCode: 1,\n };\n }\n\n // Rewrite `topics.yaml`: the entry itself (if present) plus any\n // parent reference to `oldSlug`.\n if (oldInYaml !== null) {\n oldInYaml.slug = newSlug;\n if (oldInYaml.title === titleCase(oldSlug)) {\n // Title was the auto-generated default — refresh it to the new\n // slug's title-case. A custom title stays as-is.\n oldInYaml.title = titleCase(newSlug);\n }\n }\n for (const t of file.topics) {\n t.parents = t.parents.map((p) => (p === oldSlug ? newSlug : p));\n }\n\n // Write ordering matters: topics.yaml FIRST (atomic tmp+rename), THEN\n // the page rewrites. If topics.yaml write fails, no page was touched.\n // If a page rewrite fails midway, topics.yaml already reflects the\n // rename so the next reindex picks up the ad-hoc state and the user\n // can re-run to finish the remaining pages. The opposite ordering\n // would leave half-rewritten pages referencing a slug that\n // topics.yaml doesn't know about.\n await writeTopicsFile(yamlPath, file);\n\n // Rewrite every page that has `oldSlug` in `topics:` frontmatter.\n pagesUpdated = await rewriteTopicOnPages(repoRoot, (topics) =>\n topics.map((t) => (t === oldSlug ? newSlug : t)),\n );\n } finally {\n db.close();\n }\n\n await runIndexer({ repoRoot });\n return {\n stdout: `renamed ${oldSlug} → ${newSlug} (${pagesUpdated} page${pagesUpdated === 1 ? \"\" : \"s\"} updated)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// delete — `almanac topics delete <slug>`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics delete <slug>`. Removes the topic from `topics.yaml`\n * (if present), scrubs any parent edges pointing at it, and untags\n * every page that had it. Pages themselves are left alone — deleting a\n * topic doesn't delete pages, just the relationship.\n */\nexport async function runTopicsDelete(\n options: TopicsDeleteOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const slug = toKebabCase(options.slug);\n if (slug.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n const db = openIndex(indexDbPath(repoRoot));\n let pagesUpdated: number;\n try {\n if (!topicExists(file, db, slug)) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${slug}\"\\n`,\n exitCode: 1,\n };\n }\n\n // Remove the entry and strip it from everyone else's `parents` list.\n file.topics = file.topics.filter((t) => t.slug !== slug);\n for (const t of file.topics) {\n t.parents = t.parents.filter((p) => p !== slug);\n }\n\n // Same write ordering as rename: topics.yaml first (atomic), then\n // pages. A crash between the two leaves topics.yaml already scrubbed\n // and any remaining in-page references become ad-hoc topics — which\n // the reindex will pick up as empty-topics on next health, and the\n // user can re-run to finish untagging.\n await writeTopicsFile(yamlPath, file);\n\n pagesUpdated = await rewriteTopicOnPages(repoRoot, (topics) =>\n topics.filter((t) => t !== slug),\n );\n } finally {\n db.close();\n }\n\n await runIndexer({ repoRoot });\n return {\n stdout: `deleted topic \"${slug}\" (${pagesUpdated} page${pagesUpdated === 1 ? \"\" : \"s\"} untagged)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// describe — `almanac topics describe <slug> \"<text>\"`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics describe <slug> \"<text>\"`. Sets or updates the\n * one-liner description. An empty string clears it.\n */\nexport async function runTopicsDescribe(\n options: TopicsDescribeOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const slug = toKebabCase(options.slug);\n if (slug.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n const db = openIndex(indexDbPath(repoRoot));\n try {\n if (!topicExists(file, db, slug)) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${slug}\"\\n`,\n exitCode: 1,\n };\n }\n // `ensureTopic` is idempotent — if the topic was DB-only it\n // promotes into `file`; if already in `file` it returns the\n // existing entry. Either way we get a concrete entry to mutate.\n const entry = ensureTopic(file, slug);\n\n const text = options.description.trim();\n entry.description = text.length === 0 ? null : text;\n\n await writeTopicsFile(yamlPath, file);\n } finally {\n db.close();\n }\n\n await runIndexer({ repoRoot });\n return {\n stdout: `described ${slug}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// Shared helpers\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * Apply a `topic-list transform` to every `.almanac/pages/*.md` file\n * whose frontmatter contains a relevant topic. Returns the number of\n * files actually changed.\n *\n * We glob page files ourselves (not the DB) so this works even on a\n * stale index — `rename` and `delete` run the indexer AFTER mutation,\n * and we don't want the scan to miss a page that was just modified.\n *\n * `transform` operates on the full topic list of each page; returning\n * the same list = no-op (no write). We short-circuit cheaply via\n * `applyTopicsTransform` before touching the file.\n */\nasync function rewriteTopicOnPages(\n repoRoot: string,\n transform: (topics: string[]) => string[],\n): Promise<number> {\n const pagesDir = join(repoRoot, \".almanac\", \"pages\");\n const files = await fg(\"**/*.md\", {\n cwd: pagesDir,\n absolute: true,\n onlyFiles: true,\n });\n let changed = 0;\n for (const filePath of files) {\n // Cheap read → in-memory check. Skip files that wouldn't be\n // changed so we don't bump their mtime.\n const raw = await readFile(filePath, \"utf8\");\n const applied = applyTopicsTransform(raw, transform);\n if (!applied.changed) continue;\n await rewritePageTopics(filePath, transform);\n changed += 1;\n }\n return changed;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// dispatch helpers (used by cli.ts)\n// ─────────────────────────────────────────────────────────────────────\n\nexport {\n ensureTopic,\n findTopic,\n loadTopicsFile,\n writeTopicsFile,\n type TopicEntry,\n type TopicsFile,\n};\n","import { existsSync } from \"node:fs\";\nimport { basename } from \"node:path\";\n\nimport { findNearestAlmanacDir } from \"../paths.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport {\n addEntry,\n readRegistry,\n type RegistryEntry,\n} from \"./index.js\";\n\n/**\n * If the current working directory (or any parent) has a `.almanac/` that\n * isn't in the registry, silently add it. Runs as a side effect of every\n * command except `init` (which does its own registration) and `list --drop`\n * (which shouldn't resurrect the entry the user just removed).\n *\n * The contract is \"silent\" for environmental problems — missing home dir,\n * unreadable registry file, permission errors. Those shouldn't block the\n * real command from running. But a **malformed** registry IS surfaced: if\n * the JSON is corrupt, the user needs to know, not have auto-register\n * quietly pretend the registry was empty and start overwriting entries.\n */\nexport async function autoRegisterIfNeeded(\n cwd: string,\n): Promise<RegistryEntry | null> {\n try {\n const repoRoot = findNearestAlmanacDir(cwd);\n if (repoRoot === null) return null;\n\n // Double-check the directory still exists — `findNearestAlmanacDir`\n // already confirms this, but we're explicit about the precondition.\n if (!existsSync(repoRoot)) return null;\n\n // Read the registry ONCE. `resolveNameCollision` scans this snapshot\n // in memory; re-reading per iteration would be O(N²) in collision\n // count and needlessly hit the filesystem.\n const entries = await readRegistry();\n\n const existing = entries.find((e) => samePath(e.path, repoRoot));\n if (existing !== undefined) return existing;\n\n // Derive a kebab-case name from the directory. If the dir name is\n // somehow empty (e.g. repo is at filesystem root), skip — we don't\n // want to register a nameless entry.\n const name = toKebabCase(basename(repoRoot));\n if (name.length === 0) return null;\n\n // Resolve collisions on name by falling back to a disambiguated form.\n // Auto-registration should never overwrite an existing named entry\n // that points elsewhere.\n const finalName = resolveNameCollision(entries, name, repoRoot);\n if (finalName === null) return null;\n\n const entry: RegistryEntry = {\n name: finalName,\n description: \"\",\n path: repoRoot,\n registered_at: new Date().toISOString(),\n };\n await addEntry(entry);\n return entry;\n } catch (err: unknown) {\n // Only swallow errors that mean \"registry state isn't readable right\n // now\" — everything else (malformed JSON, programmer errors, bugs)\n // should propagate so the user can see it.\n if (\n err instanceof Error &&\n \"code\" in err &&\n (err.code === \"ENOENT\" ||\n err.code === \"EACCES\" ||\n err.code === \"EPERM\")\n ) {\n return null;\n }\n throw err;\n }\n}\n\n/**\n * If another repo already claims `name`, append `-2`, `-3`, ... until we\n * find an unused slug. Only relevant for auto-registration — `init` with\n * `--name` lets the user resolve collisions explicitly.\n *\n * Takes a snapshot of registry entries instead of re-reading the file per\n * iteration. Caps at 1000 attempts to prevent pathological loops if the\n * registry somehow contains every suffix (it can't, but we'd rather fail\n * explicitly than spin).\n */\nfunction resolveNameCollision(\n entries: RegistryEntry[],\n baseName: string,\n repoPath: string,\n): string | null {\n const owner = entries.find((e) => e.name === baseName);\n if (owner === undefined || samePath(owner.path, repoPath)) {\n return baseName;\n }\n const taken = new Set(entries.map((e) => e.name));\n const MAX_ATTEMPTS = 1000;\n for (let suffix = 2; suffix < MAX_ATTEMPTS + 2; suffix += 1) {\n const candidate = `${baseName}-${suffix}`;\n if (!taken.has(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * Mirror `pathsEqual` in `registry/index.ts` — case-insensitive on\n * macOS/Windows, case-sensitive on Linux. Duplicated here rather than\n * exported to keep the registry module's public surface small.\n */\nfunction samePath(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","import { run } from \"../src/cli.js\";\n\nrun(process.argv).catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`almanac: ${message}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["basename","existsSync","join","require","resolve","existsSync","dirname","join","existsSync","mkdir","readFile","writeFile","join","readFile","dirname","path","readFile","dirname","join","existsSync","mkdir","writeFile","path","readFile","join","existsSync","resolve","createWriteStream","existsSync","readFile","readdir","homedir","basename","join","relative","join","formatTimestamp","createWriteStream","closeStream","relative","existsSync","homedir","basename","readdir","path","readFile","resolve","existsSync","mkdir","readFile","rename","writeFile","homedir","path","fileURLToPath","existsSync","readFile","basename","join","fg","createHash","existsSync","statSync","readFile","basename","join","relative","existsSync","mkdir","readFile","rename","writeFile","dirname","yaml","path","existsSync","readFile","isNodeError","yaml","dirname","mkdir","writeFile","rename","target","path","join","existsSync","basename","statSync","readFile","path","relative","createHash","topicsYamlPath","existsSync","join","existsSync","join","join","existsSync","readFile","fg","basename","join","join","existsSync","existsSync","join","join","collectSlugs","join","join","readFile","join","join","collectSlugs","readFile","readFile","rename","writeFile","yaml","values","parsed","join","stderr","readFile","join","fg","join","fg","readFile","existsSync","basename","existsSync","basename","basename","query"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/bootstrap.ts","../src/agent/auth.ts","../src/agent/prompts.ts","../src/agent/sdk.ts","../src/paths.ts","../src/commands/init.ts","../src/slug.ts","../src/registry/index.ts","../src/commands/capture.ts","../src/indexer/frontmatter.ts","../src/commands/hook.ts","../src/commands/health.ts","../src/indexer/duration.ts","../src/indexer/index.ts","../src/topics/yaml.ts","../src/indexer/paths.ts","../src/indexer/schema.ts","../src/indexer/wikilinks.ts","../src/indexer/resolveWiki.ts","../src/topics/dag.ts","../src/commands/info.ts","../src/commands/list.ts","../src/commands/path.ts","../src/commands/reindex.ts","../src/commands/search.ts","../src/commands/show.ts","../src/topics/frontmatterRewrite.ts","../src/topics/paths.ts","../src/commands/tag.ts","../src/commands/topics.ts","../src/registry/autoregister.ts","../bin/codealmanac.ts"],"sourcesContent":["import { basename } from \"node:path\";\n\nimport { Command } from \"commander\";\n\nimport { runBootstrap } from \"./commands/bootstrap.js\";\nimport { runCapture } from \"./commands/capture.js\";\nimport {\n runHookInstall,\n runHookStatus,\n runHookUninstall,\n} from \"./commands/hook.js\";\nimport { runHealth } from \"./commands/health.js\";\nimport { initWiki } from \"./commands/init.js\";\nimport { runInfo } from \"./commands/info.js\";\nimport { listWikis } from \"./commands/list.js\";\nimport { runPath } from \"./commands/path.js\";\nimport { runReindex } from \"./commands/reindex.js\";\nimport { runSearch } from \"./commands/search.js\";\nimport { runShow } from \"./commands/show.js\";\nimport { runTag, runUntag } from \"./commands/tag.js\";\nimport {\n runTopicsCreate,\n runTopicsDelete,\n runTopicsDescribe,\n runTopicsLink,\n runTopicsList,\n runTopicsRename,\n runTopicsShow,\n runTopicsUnlink,\n} from \"./commands/topics.js\";\nimport { autoRegisterIfNeeded } from \"./registry/autoregister.js\";\n\n/**\n * Entry point. `bin/codealmanac.ts` hands us `process.argv` and any errors\n * bubble up to the shim for a uniform \"almanac: <message>\" output format.\n *\n * Auto-registration runs before most commands. Two exceptions:\n * - `init` registers explicitly, so auto-register would be redundant and\n * would race with init's own write.\n * - `list --drop <name>` shouldn't silently re-register the repo whose\n * entry the user is trying to remove.\n */\nexport async function run(argv: string[]): Promise<void> {\n const program = new Command();\n\n // Both `almanac` and `codealmanac` point at the same entry (see\n // `package.json#bin`). Match the help header to whatever the user\n // invoked so `codealmanac --help` doesn't incongruously read\n // `Usage: almanac …`. Fall back to `almanac` when argv[1] isn't\n // available (programmatic invocation, tests).\n const invoked = argv[1] !== undefined ? basename(argv[1]) : \"almanac\";\n const programName =\n invoked === \"codealmanac\" ? \"codealmanac\" : \"almanac\";\n\n program\n .name(programName)\n .description(\n \"codealmanac — a living wiki for codebases, maintained by AI agents\",\n )\n .version(\"0.1.0\", \"-v, --version\", \"print version\");\n\n program\n .command(\"init\")\n .description(\"scaffold .almanac/ in the current directory and register it\")\n .option(\"--name <name>\", \"wiki name (defaults to the directory name)\")\n .option(\"--description <text>\", \"one-line description of this wiki\")\n .action(async (opts: { name?: string; description?: string }) => {\n const result = await initWiki({\n cwd: process.cwd(),\n name: opts.name,\n description: opts.description,\n });\n const verb = result.created ? \"initialized\" : \"updated\";\n process.stdout.write(\n `${verb} wiki \"${result.entry.name}\" at ${result.almanacDir}\\n`,\n );\n });\n\n program\n .command(\"list\")\n .description(\"list registered wikis\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\n \"--drop <name>\",\n \"remove a wiki from the registry (the only way entries are ever removed)\",\n )\n .action(async (opts: { json?: boolean; drop?: string }) => {\n // Auto-register only makes sense for default/JSON listing. Skipping\n // it on --drop keeps the removal operation predictable — the user's\n // intent is to shrink the registry, not grow it mid-command.\n if (opts.drop === undefined) {\n await autoRegisterIfNeeded(process.cwd());\n }\n const result = await listWikis(opts);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) {\n process.exitCode = result.exitCode;\n }\n });\n\n program\n .command(\"search [query]\")\n .description(\"query pages by text, topic, file mentions, or freshness\")\n .option(\n \"--topic <name...>\",\n \"filter by topic (repeat for intersection)\",\n collectOption,\n [] as string[],\n )\n .option(\n \"--mentions <path>\",\n \"pages referencing this file or folder (trailing / = folder)\",\n )\n .option(\n \"--since <duration>\",\n \"updated within duration, by file mtime (e.g. 2w, 30d)\",\n )\n .option(\n \"--stale <duration>\",\n \"NOT updated within duration, by file mtime\",\n )\n .option(\"--orphan\", \"pages with no topics\")\n .option(\"--include-archive\", \"include archived pages\")\n .option(\"--archived\", \"archived pages only\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\"--limit <n>\", \"cap results\", parsePositiveInt)\n .action(\n async (\n query: string | undefined,\n opts: {\n topic?: string[];\n mentions?: string;\n since?: string;\n stale?: string;\n orphan?: boolean;\n includeArchive?: boolean;\n archived?: boolean;\n wiki?: string;\n json?: boolean;\n limit?: number;\n },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runSearch({\n cwd: process.cwd(),\n query,\n topics: opts.topic ?? [],\n mentions: opts.mentions,\n since: opts.since,\n stale: opts.stale,\n orphan: opts.orphan,\n includeArchive: opts.includeArchive,\n archived: opts.archived,\n wiki: opts.wiki,\n json: opts.json,\n limit: opts.limit,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"show [slug]\")\n .description(\"print the markdown content of a page\")\n .option(\"--stdin\", \"read slugs from stdin (one per line)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n slug: string | undefined,\n opts: { stdin?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runShow({\n cwd: process.cwd(),\n slug,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n wiki: opts.wiki,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"path [slug]\")\n .description(\"resolve a slug to its absolute file path\")\n .option(\"--stdin\", \"read slugs from stdin (one per line)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n slug: string | undefined,\n opts: { stdin?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runPath({\n cwd: process.cwd(),\n slug,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n wiki: opts.wiki,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"info [slug]\")\n .description(\"print metadata for a page (topics, refs, links, lineage)\")\n .option(\"--stdin\", \"read slugs from stdin (one per line)\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n slug: string | undefined,\n opts: { stdin?: boolean; json?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runInfo({\n cwd: process.cwd(),\n slug,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n json: opts.json,\n wiki: opts.wiki,\n });\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n },\n );\n\n program\n .command(\"reindex\")\n .description(\"force a full rebuild of .almanac/index.db\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(async (opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runReindex({\n cwd: process.cwd(),\n wiki: opts.wiki,\n });\n process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n });\n\n // ─── topics (sub-tree) ────────────────────────────────────────────\n const topics = program\n .command(\"topics\")\n .description(\"manage the topic DAG (list, create, link, rename, delete)\");\n\n // Default action for `almanac topics` with no subcommand: list.\n topics\n .command(\"list\", { isDefault: true })\n .description(\"list all topics with page counts\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .option(\"--json\", \"emit structured JSON\")\n .action(async (opts: { wiki?: string; json?: boolean }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsList({\n cwd: process.cwd(),\n wiki: opts.wiki,\n json: opts.json,\n });\n emit(result);\n });\n\n topics\n .command(\"show <slug>\")\n .description(\"print a topic's metadata, parents, children, and pages\")\n .option(\"--descendants\", \"include pages tagged with descendant topics\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .option(\"--json\", \"emit structured JSON\")\n .action(\n async (\n slug: string,\n opts: { descendants?: boolean; wiki?: string; json?: boolean },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsShow({\n cwd: process.cwd(),\n slug,\n descendants: opts.descendants,\n wiki: opts.wiki,\n json: opts.json,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"create <name>\")\n .description(\"create a topic (rejects if --parent slug does not exist)\")\n .option(\n \"--parent <slug>\",\n \"parent topic slug (repeat for multiple parents)\",\n collectOption,\n [] as string[],\n )\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n name: string,\n opts: { parent?: string[]; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsCreate({\n cwd: process.cwd(),\n name,\n parents: opts.parent,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"link <child> <parent>\")\n .description(\"add a DAG edge (cycle-checked)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (child: string, parent: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsLink({\n cwd: process.cwd(),\n child,\n parent,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"unlink <child> <parent>\")\n .description(\"remove a DAG edge\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (child: string, parent: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsUnlink({\n cwd: process.cwd(),\n child,\n parent,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"rename <old> <new>\")\n .description(\"rename a topic; rewrites every affected page's frontmatter\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (oldSlug: string, newSlug: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsRename({\n cwd: process.cwd(),\n oldSlug,\n newSlug,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n topics\n .command(\"delete <slug>\")\n .description(\"delete a topic; untags every affected page\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(async (slug: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsDelete({\n cwd: process.cwd(),\n slug,\n wiki: opts.wiki,\n });\n emit(result);\n });\n\n topics\n .command(\"describe <slug> <text>\")\n .description(\"set a topic's one-line description\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (slug: string, text: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runTopicsDescribe({\n cwd: process.cwd(),\n slug,\n description: text,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n // ─── tag / untag ─────────────────────────────────────────────────\n program\n .command(\"tag [page] [topics...]\")\n .description(\"add topics to a page (auto-creates missing topics)\")\n .option(\"--stdin\", \"read page slugs from stdin (one per line)\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (\n page: string | undefined,\n topicsArg: string[],\n opts: { stdin?: boolean; wiki?: string },\n ) => {\n await autoRegisterIfNeeded(process.cwd());\n // `--stdin <topic> [<topic>...]` shape: no positional page,\n // all positionals are topics. commander gives us `page` =\n // first positional and `topicsArg` = rest, so in --stdin mode\n // we prepend whatever landed in `page` to the topics list.\n const resolvedTopics = opts.stdin === true\n ? [page, ...topicsArg].filter(\n (t): t is string => typeof t === \"string\" && t.length > 0,\n )\n : topicsArg;\n const result = await runTag({\n cwd: process.cwd(),\n page: opts.stdin === true ? undefined : page,\n topics: resolvedTopics,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n program\n .command(\"untag <page> <topic>\")\n .description(\"remove a topic from a page's frontmatter\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (page: string, topic: string, opts: { wiki?: string }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runUntag({\n cwd: process.cwd(),\n page,\n topic,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n // ─── bootstrap ───────────────────────────────────────────────────\n // Slice 4: first Claude Agent SDK integration. Spawns the bootstrap\n // agent on the current repo to create initial entity pages + README +\n // topic DAG. Requires ANTHROPIC_API_KEY; refuses on populated wikis\n // unless --force.\n program\n .command(\"bootstrap\")\n .description(\n \"spawn an agent to scan the repo and create initial wiki stubs (requires ANTHROPIC_API_KEY)\",\n )\n .option(\"--quiet\", \"suppress per-tool streaming; print only the final line\")\n .option(\"--model <model>\", \"override the agent model\")\n .option(\n \"--force\",\n \"overwrite an existing populated wiki (default: refuse)\",\n )\n .action(\n async (opts: { quiet?: boolean; model?: string; force?: boolean }) => {\n // No auto-register here: if this is a fresh repo the bootstrap\n // command handles init (and therefore registration) itself. If\n // the repo is already a wiki, capture/init have already\n // registered it.\n const result = await runBootstrap({\n cwd: process.cwd(),\n quiet: opts.quiet,\n model: opts.model,\n force: opts.force,\n });\n emit(result);\n },\n );\n\n // ─── capture ─────────────────────────────────────────────────────\n // Slice 5: writer + reviewer subagent on a Claude Code session\n // transcript. Refuses if no `.almanac/` exists (capture is for\n // maintaining wikis, not creating them). Transcript path resolution:\n // - explicit positional arg wins\n // - `--session <id>` matches by filename under ~/.claude/projects/\n // - otherwise auto-resolve the most recent transcript whose cwd\n // matches this repo\n program\n .command(\"capture [transcript]\")\n .description(\n \"capture knowledge from a Claude Code session transcript \" +\n \"(auto-resolves the most recent session for this repo when no \" +\n \"path is given; requires ANTHROPIC_API_KEY)\",\n )\n .option(\"--session <id>\", \"target a specific session by ID\")\n .option(\n \"--quiet\",\n \"suppress per-tool streaming; print only the final summary\",\n )\n .option(\"--model <model>\", \"override the agent model\")\n .action(\n async (\n transcript: string | undefined,\n opts: { session?: string; quiet?: boolean; model?: string },\n ) => {\n // Auto-register the repo on capture: the user may have cloned a\n // repo with `.almanac/` committed but never run init.\n await autoRegisterIfNeeded(process.cwd());\n const result = await runCapture({\n cwd: process.cwd(),\n transcriptPath: transcript,\n sessionId: opts.session,\n quiet: opts.quiet,\n model: opts.model,\n });\n emit(result);\n },\n );\n\n // ─── hook ─────────────────────────────────────────────────────────\n // Wires codealmanac into Claude Code's SessionEnd hook via\n // ~/.claude/settings.json. Non-interactive install/uninstall/status.\n const hook = program\n .command(\"hook\")\n .description(\n \"install, uninstall, or inspect the SessionEnd hook in ~/.claude/settings.json\",\n );\n\n hook\n .command(\"install\")\n .description(\"add a SessionEnd entry that runs 'almanac capture' on session end\")\n .action(async () => {\n const result = await runHookInstall();\n emit(result);\n });\n\n hook\n .command(\"uninstall\")\n .description(\"remove codealmanac's SessionEnd entry; leaves foreign entries alone\")\n .action(async () => {\n const result = await runHookUninstall();\n emit(result);\n });\n\n hook\n .command(\"status\")\n .description(\"report whether the SessionEnd hook is installed\")\n .action(async () => {\n const result = await runHookStatus();\n emit(result);\n });\n\n // ─── health ──────────────────────────────────────────────────────\n program\n .command(\"health\")\n .description(\"report wiki problems (orphans, dead refs, broken links, …)\")\n .option(\"--topic <name>\", \"scope to a topic + its descendants\")\n .option(\"--stale <duration>\", \"stale threshold (default 90d)\")\n .option(\"--stdin\", \"read page slugs from stdin (limit to these pages)\")\n .option(\"--json\", \"emit structured JSON\")\n .option(\"--wiki <name>\", \"target a specific registered wiki\")\n .action(\n async (opts: {\n topic?: string;\n stale?: string;\n stdin?: boolean;\n json?: boolean;\n wiki?: string;\n }) => {\n await autoRegisterIfNeeded(process.cwd());\n const result = await runHealth({\n cwd: process.cwd(),\n topic: opts.topic,\n stale: opts.stale,\n stdin: opts.stdin,\n stdinInput: opts.stdin === true ? await readStdin() : undefined,\n json: opts.json,\n wiki: opts.wiki,\n });\n emit(result);\n },\n );\n\n await program.parseAsync(argv);\n}\n\n/**\n * Uniform writer for commands that produce `{stdout, stderr, exitCode}`.\n * Used by the slice-3 topics/tag/health commands; older commands still\n * inline the same three lines and can be collapsed in a follow-up.\n */\nfunction emit(result: {\n stdout: string;\n stderr: string;\n exitCode: number;\n}): void {\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n if (result.stdout.length > 0) process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n}\n\n/**\n * Commander's built-in collectable option helper. Repeatable `--topic`\n * appends; a bare call with no previous value starts a fresh array.\n */\nfunction collectOption(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\nfunction parsePositiveInt(value: string): number {\n const n = Number.parseInt(value, 10);\n if (!Number.isFinite(n) || n < 0) {\n throw new Error(`invalid --limit \"${value}\" (expected a non-negative integer)`);\n }\n return n;\n}\n\n/**\n * Drain stdin to a string. Used by the `--stdin` flag on show/path/info.\n * We require an explicit opt-in rather than auto-detect TTY because\n * relying on `process.stdin.isTTY` makes the behavior surprising when\n * invoked from scripts with stdin redirected.\n */\nasync function readStdin(): Promise<string> {\n if (process.stdin.isTTY === true) return \"\";\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n","import { createWriteStream, existsSync, type WriteStream } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\n\nimport type { SDKMessage } from \"@anthropic-ai/claude-agent-sdk\";\n\nimport { assertClaudeAuth, type SpawnCliFn } from \"../agent/auth.js\";\nimport { loadPrompt } from \"../agent/prompts.js\";\nimport {\n runAgent,\n type AgentResult,\n type RunAgentOptions,\n} from \"../agent/sdk.js\";\nimport { findNearestAlmanacDir, getRepoAlmanacDir } from \"../paths.js\";\nimport { initWiki } from \"./init.js\";\n\nexport interface BootstrapOptions {\n cwd: string;\n /** Suppress per-tool-use streaming output; print only errors + final line. */\n quiet?: boolean;\n /** Override the agent model. Defaults to the SDK default (sonnet-4-6). */\n model?: string;\n /** Overwrite a populated wiki. Default refuses with a pointer at `capture`. */\n force?: boolean;\n /** Injectable agent runner — tests replace this with a fake. */\n runAgent?: (opts: RunAgentOptions) => Promise<AgentResult>;\n /**\n * Injectable subprocess spawner for the Claude auth-status check.\n * Tests substitute a stub that emits canned JSON without running the\n * bundled CLI. Production leaves this undefined and `assertClaudeAuth`\n * falls through to `defaultSpawnCli`.\n */\n spawnCli?: SpawnCliFn;\n /**\n * Clock injection, for tests. Otherwise `Date.now()` timestamps the\n * transcript log filename and defaults to the SDK's session_id once\n * we receive one (the filename is chosen BEFORE the agent starts so\n * we can stream to it).\n */\n now?: () => Date;\n}\n\nexport interface BootstrapResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Tools the bootstrap agent is permitted to use. Bootstrap reads the repo\n * (Read/Glob/Grep), runs quick inspection commands (Bash — scoped by the\n * prompt, not by the SDK — the agent isn't expected to do anything\n * destructive), and writes scaffolding (Write/Edit). Notably absent:\n * - `Agent` — bootstrap has no subagents (reviewer is slice 5).\n * - `WebFetch` / `WebSearch` — prompt is explicit that we work from the\n * repo, not the internet. Adding these would invite drift.\n * - MCP servers — none needed for a local filesystem scan.\n */\nconst BOOTSTRAP_TOOLS = [\"Read\", \"Write\", \"Edit\", \"Glob\", \"Grep\", \"Bash\"];\n\n/**\n * `almanac bootstrap` — first Claude Agent SDK integration.\n *\n * Flow:\n * 1. Auth gate (ANTHROPIC_API_KEY). Fail fast with a clean error.\n * 2. Resolve repo root (existing `.almanac/` or cwd).\n * 3. Refuse-if-populated unless --force.\n * 4. Auto-init silently if `.almanac/` doesn't exist yet.\n * 5. Load `prompts/bootstrap.md`.\n * 6. Run the agent with BOOTSTRAP_TOOLS, cwd = repo root.\n * 7. Stream tool-uses to stdout (unless --quiet); write the full raw\n * transcript to `.almanac/.bootstrap-<session>.log`.\n * 8. Print a final `[done]` / `[failed]` line with cost + turns.\n *\n * Non-zero exit on failure so shell users can pipe into `&&`.\n */\nexport async function runBootstrap(\n options: BootstrapOptions,\n): Promise<BootstrapResult> {\n // Fail before loading prompts so we don't do filesystem work on a request\n // that can't succeed. `assertClaudeAuth` accepts either subscription\n // OAuth (via the bundled SDK CLI) or `ANTHROPIC_API_KEY`; missing both\n // surfaces a two-option error and MUST exit non-zero so the SessionEnd\n // hook (which backgrounds the process and ignores stderr) doesn't\n // treat silent auth failure as success.\n try {\n await assertClaudeAuth(options.spawnCli);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n stdout: \"\",\n stderr: `almanac: ${msg}\\n`,\n exitCode: 1,\n };\n }\n\n // Repo root: honor an already-initialized wiki anywhere above us.\n // Otherwise treat `cwd` as the root for a fresh wiki.\n const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;\n const almanacDir = getRepoAlmanacDir(repoRoot);\n const pagesDir = join(almanacDir, \"pages\");\n\n // Refuse to clobber a populated wiki. `almanac capture` is the tool for\n // maintaining wikis after bootstrap.\n if (options.force !== true && existsSync(pagesDir)) {\n const existing = await countMarkdownPages(pagesDir);\n if (existing > 0) {\n return {\n stdout: \"\",\n stderr:\n `almanac: .almanac/ already initialized with ${existing} page${existing === 1 ? \"\" : \"s\"}. ` +\n \"Use 'almanac capture' instead, or --force to overwrite.\\n\",\n exitCode: 1,\n };\n }\n }\n\n // Auto-init silently if missing. `initWiki` is idempotent — re-running\n // on an existing wiki is a no-op for the README / pages dir.\n if (!existsSync(almanacDir)) {\n try {\n await initWiki({ cwd: repoRoot });\n } catch (err: unknown) {\n // Per the slice spec: auto-init failures should be loud. The user\n // needs to know init is broken, not see a cascading agent error.\n const msg = err instanceof Error ? err.message : String(err);\n return {\n stdout: \"\",\n stderr: `almanac: init failed during bootstrap: ${msg}\\n`,\n exitCode: 1,\n };\n }\n }\n\n const systemPrompt = await loadPrompt(\"bootstrap\");\n\n // Transcript log filename: timestamp-based so it's sortable. The session\n // ID from the SDK isn't known until the first message arrives — by then\n // we'd already want somewhere to stream to. Use a clock-derived prefix\n // that's still meaningful even on a run that fails before producing a\n // session_id.\n const now = options.now?.() ?? new Date();\n const logName = `.bootstrap-${formatTimestamp(now)}.log`;\n const logPath = join(almanacDir, logName);\n const logStream = createWriteStream(logPath, { flags: \"w\" });\n\n // The streaming formatter is what the user sees on stdout unless\n // --quiet is set. The raw log captures EVERYTHING (including\n // `stream_event` partials) for postmortem.\n const out = process.stdout;\n const formatter = new StreamingFormatter({\n write: (line: string) => {\n if (options.quiet !== true) out.write(line);\n },\n });\n\n const onMessage = (msg: SDKMessage): void => {\n // Write the raw message to the transcript. Keep one JSON per line so\n // the log is grep-able and can be re-parsed if needed.\n try {\n logStream.write(`${JSON.stringify(msg)}\\n`);\n } catch {\n // Serialization failures are non-fatal — we'd rather keep streaming\n // to stdout than crash because one message had a circular ref.\n }\n formatter.handle(msg);\n };\n\n const runner = options.runAgent ?? runAgent;\n\n const userPrompt = `Begin the bootstrap now. Working directory: ${repoRoot}.`;\n\n let result: AgentResult;\n try {\n result = await runner({\n systemPrompt,\n prompt: userPrompt,\n allowedTools: BOOTSTRAP_TOOLS,\n cwd: repoRoot,\n model: options.model,\n onMessage,\n });\n } finally {\n await closeStream(logStream);\n }\n\n const finalLine = formatFinalLine(result, logPath, repoRoot);\n\n if (result.success) {\n return {\n stdout: `${finalLine}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout: options.quiet === true ? \"\" : `${finalLine}\\n`,\n stderr: `almanac: bootstrap failed: ${result.error ?? \"unknown error\"}\\n`,\n exitCode: 1,\n };\n}\n\n/**\n * Format the final line the user sees. On success it's a one-liner with\n * cost + turns; on failure we still print the cost/turns so the user\n * knows what the partial run used. The log path is shown relative to\n * `repoRoot` to keep the line short.\n */\nfunction formatFinalLine(\n result: AgentResult,\n logPath: string,\n repoRoot: string,\n): string {\n const status = result.success ? \"done\" : \"failed\";\n const rel = relative(repoRoot, logPath);\n const cost = `$${result.cost.toFixed(3)}`;\n return `[${status}] cost: ${cost}, turns: ${result.turns} (transcript: ${rel})`;\n}\n\nasync function countMarkdownPages(pagesDir: string): Promise<number> {\n try {\n const entries = await readdir(pagesDir, { withFileTypes: true });\n return entries.filter((e) => e.isFile() && e.name.endsWith(\".md\")).length;\n } catch {\n return 0;\n }\n}\n\nfunction closeStream(stream: WriteStream): Promise<void> {\n return new Promise((resolve) => {\n stream.end(() => resolve());\n });\n}\n\nfunction formatTimestamp(d: Date): string {\n // YYYYMMDD-HHMMSS, local time. Collision-proof enough for human use\n // (one bootstrap per second, per repo is an acceptable ceiling).\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n const y = d.getFullYear();\n const mo = pad(d.getMonth() + 1);\n const da = pad(d.getDate());\n const h = pad(d.getHours());\n const mi = pad(d.getMinutes());\n const s = pad(d.getSeconds());\n return `${y}${mo}${da}-${h}${mi}${s}`;\n}\n\n/**\n * Translates SDK messages into one-line-per-tool-use output.\n *\n * Design rules:\n * - One line per `tool_use` block, not per token. Users scanning the\n * output want to see \"what the agent did\", not a tail of the\n * assistant's prose.\n * - `Bash` gets special formatting because the input is most usefully\n * rendered as the command being run.\n * - On final `result`, emit a summary line. Callers can suppress all\n * intermediate output via `--quiet` and still get the summary.\n * - Tool paths are shown relative to the cwd (not implemented here\n * because the SDK doesn't give us cwd on every message; users get\n * the raw input). The cwd-relative rendering is a nice-to-have that\n * we can layer on later without changing the API.\n *\n * Exported for testing — `StreamingFormatter` is easier to unit-test in\n * isolation than the whole command.\n */\nexport class StreamingFormatter {\n private readonly sink: { write: (line: string) => void };\n /**\n * Current agent label. Starts as \"bootstrap\"; switches when we see an\n * `Agent` tool-use (slice 5 will exercise this). We still track it here\n * so the formatter can stay shared between bootstrap and capture.\n */\n private currentAgent = \"bootstrap\";\n\n constructor(sink: { write: (line: string) => void }) {\n this.sink = sink;\n }\n\n /**\n * Swap the top-level agent label. `capture` uses this to relabel from\n * the default \"bootstrap\" to \"writer\" — otherwise the writer's tool-use\n * output would render as `[bootstrap] …`, which is confusing when you're\n * reading capture logs.\n */\n setAgent(name: string): void {\n this.currentAgent = name;\n }\n\n handle(msg: SDKMessage): void {\n if (msg.type === \"assistant\") {\n for (const block of msg.message.content) {\n if (block.type !== \"tool_use\") continue;\n this.handleToolUse(block.name, block.input);\n }\n return;\n }\n\n if (msg.type === \"result\") {\n // The command-level finalLine is what the user sees at the end of\n // stdout; the formatter also emits one here so live-tailing a\n // transcript log shows the full run. Kept terse.\n const status =\n msg.subtype === \"success\" ? \"done\" : `failed (${msg.subtype})`;\n this.sink.write(\n `[${status}] cost: $${msg.total_cost_usd.toFixed(3)}, turns: ${msg.num_turns}\\n`,\n );\n return;\n }\n }\n\n private handleToolUse(name: string, rawInput: unknown): void {\n const input = normalizeToolInput(rawInput);\n\n if (name === \"Agent\") {\n // Subagent dispatch. Track the label so subsequent tool-uses show\n // up under the right agent bracket. The label is whatever the\n // parent passed as `subagent_type`, falling back to \"subagent\" if\n // the input was malformed (shouldn't happen, but defensively).\n const sub =\n typeof input.subagent_type === \"string\" ? input.subagent_type : \"subagent\";\n this.currentAgent = sub;\n this.sink.write(`[${sub}] starting\\n`);\n return;\n }\n\n const summary = formatToolSummary(name, input);\n this.sink.write(`[${this.currentAgent}] ${summary}\\n`);\n }\n}\n\n/**\n * SDK quirk: `tool_use.input` arrives as either an object OR a\n * JSON-encoded string. Always normalize before touching fields.\n */\nfunction normalizeToolInput(raw: unknown): Record<string, unknown> {\n if (typeof raw === \"string\") {\n try {\n const parsed = JSON.parse(raw);\n if (parsed !== null && typeof parsed === \"object\") {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // Fall through to empty object.\n }\n return {};\n }\n if (raw !== null && typeof raw === \"object\") {\n return raw as Record<string, unknown>;\n }\n return {};\n}\n\n/**\n * Render a tool call as a single human-readable line. Not exhaustive —\n * we cover the tools bootstrap actually invokes and fall back to the\n * tool name + a terse input summary for anything else.\n */\nfunction formatToolSummary(\n name: string,\n input: Record<string, unknown>,\n): string {\n switch (name) {\n case \"Read\": {\n const target = stringField(input, \"file_path\") ?? \"?\";\n return `reading ${target}`;\n }\n case \"Write\": {\n const target = stringField(input, \"file_path\") ?? \"?\";\n return `writing ${target}`;\n }\n case \"Edit\": {\n const target = stringField(input, \"file_path\") ?? \"?\";\n return `editing ${target}`;\n }\n case \"Glob\": {\n const pattern = stringField(input, \"pattern\") ?? \"?\";\n return `glob ${pattern}`;\n }\n case \"Grep\": {\n const pattern = stringField(input, \"pattern\") ?? \"?\";\n return `grep ${pattern}`;\n }\n case \"Bash\": {\n const command = stringField(input, \"command\") ?? \"?\";\n // Truncate long commands so one tool-use stays one line.\n const trimmed =\n command.length > 80 ? `${command.slice(0, 77)}...` : command;\n return `bash ${trimmed}`;\n }\n default: {\n // Unknown or MCP tool. Show the name; omit the input to avoid\n // spamming the terminal with arbitrary JSON.\n return name;\n }\n }\n}\n\nfunction stringField(\n input: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = input[key];\n return typeof value === \"string\" ? value : undefined;\n}\n","import { spawn, type ChildProcess } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Claude auth gate — accepts either an active Claude subscription login\n * OR an `ANTHROPIC_API_KEY` environment variable.\n *\n * The Claude Agent SDK delegates authentication to its bundled `cli.js`,\n * which reads OAuth credentials from `~/.claude/credentials/` (the same\n * store Claude Code uses). Users who are logged in via `claude auth login\n * --claudeai` should be able to run bootstrap/capture without ever\n * 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 * We spawn the bundled SDK's `cli.js auth status --json` to answer \"are\n * we logged in?\" rather than poking at the credentials file directly —\n * that's the SDK's contract, and it handles all the edge cases (token\n * expiry, org switching, consoleloginpath, …) for us.\n *\n * The CLI path is resolved via `require.resolve(\"@anthropic-ai/claude-\n * agent-sdk/package.json\")` + the `cli.js` sibling. Going through\n * `createRequire` keeps this compatible with both ESM dev mode (tsx) and\n * the bundled dist (tsup externalizes the SDK, so Node's own resolver\n * does the lookup at runtime). If the SDK isn't installed at all we fall\n * back to treating the user as unauthenticated — the assert will then\n * surface the familiar two-path error so they can at least fix it via\n * `ANTHROPIC_API_KEY`.\n */\n\nexport interface ClaudeAuthStatus {\n loggedIn: boolean;\n email?: string;\n subscriptionType?: string;\n authMethod?: string;\n}\n\nexport interface SpawnedProcess {\n stdout: { on: (event: \"data\", cb: (data: Buffer | string) => void) => void };\n stderr: { on: (event: \"data\", cb: (data: Buffer | string) => void) => void };\n on: (event: \"close\" | \"error\", cb: (arg: number | null | Error) => void) => void;\n kill: (signal?: string) => void;\n}\n\n/**\n * The subprocess spawner is injectable so tests can replace it with a\n * fake that emits canned JSON without touching the filesystem. Production\n * code uses `defaultSpawnCli` which invokes the bundled SDK CLI.\n */\nexport type SpawnCliFn = (args: string[]) => SpawnedProcess;\n\nconst AUTH_TIMEOUT_MS = 10_000;\n\n/**\n * Resolve `cli.js` from the bundled `@anthropic-ai/claude-agent-sdk`\n * install. Uses `createRequire` so the lookup works regardless of\n * whether we're running from `dist/` (where tsup externalized the SDK)\n * or directly from source.\n *\n * Throws if the SDK can't be located — `checkClaudeAuth` catches this\n * and treats the user as not-logged-in, which lets the env-var path\n * still work for users with a borked install.\n */\nfunction resolveCliJsPath(): string {\n // `import.meta.url` points at this module (dev or dist). `createRequire`\n // from that URL can then resolve sibling packages the same way Node's\n // own CJS resolver would.\n //\n // We resolve the main entry (not `./package.json`) because the SDK's\n // `exports` field locks subpath access — `require.resolve(\".../package.json\")`\n // fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` on current SDK versions.\n // The main entry resolves fine; `cli.js` is its sibling in the install\n // layout the SDK has used since day one.\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 bundled\n * SDK's `cli.js` via the same Node runtime that's running codealmanac.\n * Tests inject a fake via the `spawnCli` parameter.\n */\nexport const defaultSpawnCli: SpawnCliFn = (args: string[]) => {\n const cliPath = resolveCliJsPath();\n // Use `process.execPath` so we inherit the Node runtime codealmanac\n // itself is running under — avoids PATH weirdness on systems where\n // `node` isn't on PATH but codealmanac was installed via npm.\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 the bundled SDK CLI's `auth status --json`. On any failure\n * (spawn error, non-JSON stdout, non-zero exit, timeout) we return\n * `{ loggedIn: false }` rather than propagating the error — the caller\n * will fall back to the `ANTHROPIC_API_KEY` path and, if that's also\n * missing, produce a clean 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 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 const parsed = JSON.parse(stdout.trim()) 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 settle(out);\n } catch {\n settle({ loggedIn: false });\n }\n });\n });\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 };\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Loads bundled prompt text from the `prompts/` directory that ships with\n * the npm package. Used by `almanac bootstrap` (slice 4) and `almanac\n * capture` (slice 5).\n *\n * ## Why not embed the prompts as TS string literals?\n *\n * The non-negotiable from the spec (see CLAUDE.md → \"Non-negotiables\"):\n * \"Prompts are shipped from the npm package. They live in `prompts/` at\n * repo root, are bundled into `files` in `package.json`, and the agent\n * harness reads them from the package install path at runtime.\"\n *\n * Keeping them as separate files means they can be reviewed as prose,\n * diffed meaningfully, and (in the future) edited by users without\n * rebuilding the package.\n *\n * ## Path resolution\n *\n * Two runtime layouts need to work:\n *\n * 1. **Installed (`npm i -g codealmanac`).** The entry point lives at\n * `dist/codealmanac.js`; prompts at `prompts/*.md`. Walking up from\n * `import.meta.url` (`.../<pkg>/dist/codealmanac.js`) one level and\n * into `prompts/` hits the right directory.\n *\n * 2. **Source dev.** During `npm run dev`, tsup emits to `dist/` just\n * like in production, so case 1 applies. Tests import\n * `src/agent/prompts.ts` directly via tsx/vitest; `import.meta.url`\n * points at `src/agent/prompts.ts`. Walking up two levels from there\n * lands at the repo root, where `prompts/` also lives.\n *\n * We probe a small list of candidates in order and use the first that\n * contains all three expected prompt files. This keeps a single source of\n * truth — the `prompts/` directory on disk — without baking in whether\n * we're running from `dist/` or `src/`.\n */\n\nexport type PromptName = \"bootstrap\" | \"writer\" | \"reviewer\";\n\nconst PROMPT_NAMES: readonly PromptName[] = [\n \"bootstrap\",\n \"writer\",\n \"reviewer\",\n];\n\n/**\n * Override the prompts directory, for tests. Production code should never\n * call this — the auto-resolution handles both installed + source layouts.\n */\nlet overrideDir: string | null = null;\n\nexport function setPromptsDirForTesting(dir: string | null): void {\n overrideDir = dir;\n}\n\n/**\n * Resolve the prompts directory by probing candidate locations. Cached\n * after the first call so repeated `loadPrompt()` calls don't stat the\n * filesystem more than once per process.\n */\nlet resolvedDir: string | null = null;\n\nexport function resolvePromptsDir(): string {\n if (overrideDir !== null) return overrideDir;\n if (resolvedDir !== null) return resolvedDir;\n\n const here = path.dirname(fileURLToPath(import.meta.url));\n\n // Candidates, most-specific first. Each path is where `prompts/` MIGHT\n // live given some plausible bundle layout. The first one that exists\n // and contains our three expected files wins.\n const candidates = [\n // Bundled dist layout: `.../<pkg>/dist/codealmanac.js` → `../prompts`\n path.resolve(here, \"..\", \"prompts\"),\n // Source layout: `.../<pkg>/src/agent/prompts.ts` → `../../prompts`\n path.resolve(here, \"..\", \"..\", \"prompts\"),\n // Defensive fallback: if tsup someday emits a nested `dist/src/agent`,\n // walk up three levels.\n path.resolve(here, \"..\", \"..\", \"..\", \"prompts\"),\n ];\n\n for (const dir of candidates) {\n if (isPromptsDir(dir)) {\n resolvedDir = dir;\n return dir;\n }\n }\n\n // If none matched, give a helpful error with the candidates we tried.\n // This typically means the package was installed without the `prompts/`\n // dir included — shouldn't happen unless someone broke `files` in\n // package.json.\n throw new Error(\n \"could not locate bundled prompts/ directory. Tried:\\n\" +\n candidates.map((c) => ` - ${c}`).join(\"\\n\"),\n );\n}\n\nfunction isPromptsDir(dir: string): boolean {\n if (!existsSync(dir)) return false;\n // Require all three prompts to be present. A half-populated directory\n // is worse than not finding one — we'd rather error early.\n return PROMPT_NAMES.every((name) =>\n existsSync(path.join(dir, `${name}.md`)),\n );\n}\n\nexport async function loadPrompt(name: PromptName): Promise<string> {\n const dir = resolvePromptsDir();\n return readFile(path.join(dir, `${name}.md`), \"utf8\");\n}\n","import { query } from \"@anthropic-ai/claude-agent-sdk\";\nimport type {\n AgentDefinition,\n SDKMessage,\n} from \"@anthropic-ai/claude-agent-sdk\";\n\n/**\n * Thin wrapper around `@anthropic-ai/claude-agent-sdk`'s `query()`. This is\n * the ONLY module that imports from the SDK — every other command imports\n * from here. Slice 5 (capture) reuses this wrapper unchanged.\n *\n * Why a wrapper at all:\n * 1. Sets defaults (`maxTurns`, `includePartialMessages`, model) once so\n * commands stay small.\n * 2. Translates the SDK's rich message types into a {cost, turns, result}\n * summary the commands actually care about.\n *\n * The auth gate lives in `src/agent/auth.ts`. Commands call\n * `assertClaudeAuth()` BEFORE `runAgent` so we fail with a clean two-path\n * error before the SDK generator spins up. `runAgent` itself doesn't\n * re-check — the SDK reads whichever of (subscription OAuth,\n * `ANTHROPIC_API_KEY`) is present.\n *\n * Keep this module SMALL. If a feature can live in the caller, it should.\n */\n\nexport interface RunAgentOptions {\n /** Full system prompt text — usually loaded from `prompts/*.md`. */\n systemPrompt: string;\n /** User prompt / kick-off message. */\n prompt: string;\n /** Tool allowlist, e.g. `[\"Read\", \"Write\", \"Edit\", \"Glob\", \"Grep\", \"Bash\"]`. */\n allowedTools: string[];\n /**\n * Subagent definitions (slice 5 passes `{ reviewer: ... }`). Defaults to\n * `{}` — bootstrap has no subagents.\n */\n agents?: Record<string, AgentDefinition>;\n /** Working directory the agent's tools operate in (repo root). */\n cwd: string;\n /**\n * Model override. Defaults to `claude-sonnet-4-6`. Note the FULL form —\n * `options.model` requires `claude-sonnet-4-6`, not `sonnet`.\n */\n model?: string;\n /**\n * Hard cap on turns. Defaults to 100. SDK enforces this as a strict stop\n * (no graceful wrap-up turn), so set generously.\n */\n maxTurns?: number;\n /**\n * Observer called for every SDK message. The formatter (streaming\n * output, transcript log) runs here.\n */\n onMessage?: (msg: SDKMessage) => void;\n}\n\nexport interface AgentResult {\n /** `true` when the SDK emitted a `result` with `subtype: \"success\"`. */\n success: boolean;\n /** Total USD cost reported by the final `result` message. */\n cost: number;\n /** Number of turns the agent used. */\n turns: number;\n /** The assistant's final textual result, if any. */\n result: string;\n /** Session ID captured from the first assistant/result message. */\n sessionId?: string;\n /** Populated when `success === false`. */\n error?: string;\n}\n\n/**\n * Run an agent to completion. Iterates the SDK's `AsyncGenerator` and\n * returns a summary. Any thrown error in the `for await` becomes a\n * `success: false` result with the error message attached — we don't\n * propagate because the caller wants to write transcripts + print\n * formatted output regardless of outcome.\n *\n * The caller is responsible for running `assertClaudeAuth()` BEFORE\n * loading prompts or printing progress — see `bootstrap.ts`/`capture.ts`.\n * `runAgent` itself no longer re-checks; the SDK will happily pick up\n * whichever of (subscription OAuth, `ANTHROPIC_API_KEY`) the environment\n * provides.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentResult> {\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 ?? \"claude-sonnet-4-6\",\n maxTurns: opts.maxTurns ?? 100,\n // REQUIRED for streaming text deltas. Without it, `stream_event`\n // messages never fire and the CLI has no progress visibility during\n // long turns. See docs/research/agent-sdk.md §12 pitfall #1.\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 // Capture session_id from the first message that carries it.\n // Per the research doc, it appears on the first `assistant` or on\n // the `result` — whichever arrives first.\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 // `SDKResultMessage = SDKResultSuccess | SDKResultError`. Both\n // carry `total_cost_usd` and `num_turns`; only success has\n // `result` (the final assistant text).\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 // `SDKResultError` variants don't carry a `result` string; the\n // useful detail lives in `errors` (array of strings) or the\n // subtype itself (e.g. \"error_max_turns\").\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","import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\n\n/**\n * Absolute path to the user-level `~/.almanac/` directory.\n *\n * All global state (the registry, future global config) lives here, not in\n * the repo. We resolve this via `os.homedir()` rather than `$HOME` so the\n * CLI behaves the same on macOS, Linux, and Windows.\n */\nexport function getGlobalAlmanacDir(): string {\n return join(homedir(), \".almanac\");\n}\n\n/**\n * Absolute path to the global registry file.\n *\n * The registry is the single source of truth for \"which wikis exist on this\n * machine.\" It is intentionally stored outside any repo so it survives\n * branch switches, clones, and repo deletions.\n */\nexport function getRegistryPath(): string {\n return join(getGlobalAlmanacDir(), \"registry.json\");\n}\n\n/**\n * Repo-level `.almanac/` path for a given working directory (not resolved —\n * just `join(cwd, \".almanac\")`). Use `findNearestAlmanacDir` when you need\n * to walk upward like git does.\n */\nexport function getRepoAlmanacDir(cwd: string): string {\n return join(cwd, \".almanac\");\n}\n\n/**\n * Walk upward from `startDir` looking for a directory that contains\n * `.almanac/`. Returns the absolute path to the repo root (the directory\n * containing `.almanac/`), or `null` if none is found before hitting the\n * filesystem root.\n *\n * Mirrors how `git` locates the enclosing repository. This lets `almanac`\n * work from any subdirectory inside a repo, not just the root.\n *\n * We explicitly skip the global `~/.almanac/` directory. It shares the\n * `.almanac` name with the per-repo wiki dir, but it's not a wiki — it\n * only holds the registry and global state. If the user runs `almanac\n * init` anywhere inside their home directory (outside a real wiki), we\n * must NOT treat `~` as an enclosing wiki root. Otherwise init would try\n * to register the home dir itself as a wiki.\n */\nexport function findNearestAlmanacDir(startDir: string): string | null {\n const globalDir = getGlobalAlmanacDir();\n let current = isAbsolute(startDir) ? startDir : resolve(startDir);\n\n // Walk until we hit the filesystem root. `dirname(\"/\")` returns `\"/\"`,\n // so the loop terminates when we stop ascending.\n while (true) {\n const candidate = join(current, \".almanac\");\n if (candidate !== globalDir && existsSync(candidate)) {\n return current;\n }\n const parent = dirname(current);\n if (parent === current) {\n return null;\n }\n current = parent;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\nimport { findNearestAlmanacDir, getRepoAlmanacDir } from \"../paths.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport {\n addEntry,\n ensureGlobalDir,\n type RegistryEntry,\n} from \"../registry/index.js\";\n\nexport interface InitOptions {\n cwd: string;\n name?: string;\n description?: string;\n}\n\nexport interface InitResult {\n entry: RegistryEntry;\n almanacDir: string;\n created: boolean; // false if .almanac/ already existed (idempotent re-init)\n}\n\n/**\n * Scaffold `.almanac/` in the repo and register it globally.\n *\n * Idempotent: running `init` on a repo that already has `.almanac/` is\n * fine — we re-register (refreshing the name/description) and skip\n * anything that already exists. We never overwrite a user-authored\n * `README.md` or touch existing pages.\n *\n * If `cwd` lives inside a subdirectory of an existing wiki, we walk up to\n * the wiki root and operate there. `almanac init` from `src/nested/`\n * should update the enclosing wiki, not create a nested one at\n * `src/nested/.almanac/` (which would fragment the registry and leave a\n * confusing orphan `.almanac/` on disk).\n */\nexport async function initWiki(options: InitOptions): Promise<InitResult> {\n // If cwd is already inside a wiki, prefer that root. Otherwise treat\n // cwd as the new wiki root.\n const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;\n\n const almanacDir = getRepoAlmanacDir(repoRoot);\n const pagesDir = join(almanacDir, \"pages\");\n const readmePath = join(almanacDir, \"README.md\");\n\n const alreadyExisted = existsSync(almanacDir);\n\n await mkdir(pagesDir, { recursive: true });\n\n if (!existsSync(readmePath)) {\n await writeFile(readmePath, starterReadme(), \"utf8\");\n }\n\n await ensureGitignoreHasIndexDb(repoRoot);\n\n const name = toKebabCase(options.name ?? basename(repoRoot));\n if (name.length === 0) {\n throw new Error(\n \"could not derive a wiki name from the current directory; pass --name\",\n );\n }\n\n const description = (options.description ?? \"\").trim();\n\n await ensureGlobalDir();\n const entry: RegistryEntry = {\n name,\n description,\n path: repoRoot,\n registered_at: new Date().toISOString(),\n };\n await addEntry(entry);\n\n return { entry, almanacDir, created: !alreadyExisted };\n}\n\n/**\n * Ensure `.gitignore` in the repo root contains the codealmanac-derived\n * files that should never be committed.\n *\n * We ignore three paths:\n * - `.almanac/index.db` — the SQLite index (derived)\n * - `.almanac/index.db-wal` — WAL mode sidecar (present during writes)\n * - `.almanac/index.db-shm` — shared-memory sidecar (WAL mode)\n *\n * All three are derived from the markdown pages. The sidecars only show\n * up while better-sqlite3 has the DB open and can vanish between `git\n * status` calls, but explicitly ignoring them prevents \"dirty worktree\"\n * noise during active reindexing.\n *\n * We add the block regardless of whether the file exists (creating\n * `.gitignore` if needed), and we add any target lines that aren't\n * already present. Existing targets are left alone. If none of the\n * target lines need adding, the file is not touched at all.\n *\n * Formatting: when we do append, we guarantee exactly one blank line\n * between the prior content and our appended block. If the `# codealmanac`\n * header is already present but new targets need adding, we just append\n * the missing lines (no duplicate header).\n */\nasync function ensureGitignoreHasIndexDb(cwd: string): Promise<void> {\n const path = join(cwd, \".gitignore\");\n const targets = [\n \".almanac/index.db\",\n \".almanac/index.db-wal\",\n \".almanac/index.db-shm\",\n ];\n\n let existing = \"\";\n if (existsSync(path)) {\n existing = await readFile(path, \"utf8\");\n }\n\n // Normalize to line comparison to avoid false negatives on trailing\n // whitespace or CRLF line endings.\n const lines = existing.split(/\\r?\\n/).map((l) => l.trim());\n const missing = targets.filter((t) => !lines.includes(t));\n if (missing.length === 0) return;\n\n const hasHeader = lines.includes(\"# codealmanac\");\n const block = hasHeader\n ? missing.join(\"\\n\") + \"\\n\"\n : `# codealmanac\\n${missing.join(\"\\n\")}\\n`;\n\n // Three cases for the separator before the appended block:\n // - empty file: no separator needed\n // - ends with newline: one more newline produces a single blank line\n // - no trailing newline: two newlines (one to terminate the last line,\n // one for the blank separator)\n const sep =\n existing.length === 0 ? \"\" : existing.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n await writeFile(path, `${existing}${sep}${block}`, \"utf8\");\n}\n\n/**\n * The starter `.almanac/README.md` content. Based on the \"Wiki README\" and\n * \"Notability bar\" sections of the design spec. Kept opinionated but short\n * (~70 lines) — the user is expected to edit it to fit the repo.\n */\nfunction starterReadme(): string {\n return `# Wiki\n\nThis is the codealmanac wiki for this repository. It captures the knowledge\nthe code itself can't say — decisions, flows, invariants, gotchas, incidents.\n\nThe primary reader is an AI coding agent. The secondary reader is a human\nskimming to understand the shape of the codebase. Write accordingly: dense,\nfactual, linked.\n\n## Notability bar\n\nWrite a page when there is **non-obvious knowledge that will help a future\nagent**. Specifically:\n\n- A decision that took discussion, research, or trial-and-error\n- A gotcha discovered through failure\n- A cross-cutting flow that spans multiple files and isn't obvious from any\n one of them\n- A constraint or invariant not visible from the code\n- An entity (technology, service, system) referenced by multiple pages\n\nDo not write pages that restate what the code does. Do not write pages of\ninference — only of observation. Silence is an acceptable outcome.\n\n## Topic taxonomy\n\nTopics form a DAG; pages can belong to multiple topics. Start with these and\ngrow as the wiki does:\n\n- \\`stack\\` — technologies and services we use (frameworks, databases, APIs)\n- \\`systems\\` — custom systems we built (auth, billing, search)\n- \\`flows\\` — multi-file processes end-to-end (checkout-flow, publish-flow)\n- \\`decisions\\` — \"why X over Y\"\n- \\`incidents\\` — recorded failures and their fixes\n- \\`concepts\\` — shared vocabulary specific to this codebase\n\nDomain topics (\\`auth\\`, \\`payments\\`, \\`frontend\\`, \\`backend\\`) live alongside\nthese. A page about JWT rotation belongs to both \\`auth\\` and \\`decisions\\`.\n\n## Page shapes\n\nFour shapes cover most of what gets written. They are suggestions, not a\nschema — a page that fits none of them is fine.\n\n- **Entity** — a stable named thing (Supabase, Stripe, the search service)\n- **Decision** — why we chose X over Y\n- **Flow** — how a multi-file process works end-to-end\n- **Gotcha** — a specific surprise, failure, or constraint\n\n## Writing conventions\n\n- Every sentence contains a specific fact. If it doesn't, cut it.\n- Neutral tone. \"is\", not \"serves as\". No \"plays a pivotal role\", no\n interpretive \"-ing\" clauses, no vague attribution (\"experts argue\").\n- No hedging or knowledge-gap disclaimers. If you don't know, don't write\n the sentence.\n- Prose first. Bullets for genuine lists. Tables only for structured\n comparison.\n- No formulaic conclusions. End with the last substantive fact.\n\n## Linking\n\nOne \\`[[...]]\\` syntax for everything, disambiguated by content:\n\n- \\`[[checkout-flow]]\\` — page slug\n- \\`[[src/checkout/handler.ts]]\\` — file reference\n- \\`[[src/checkout/]]\\` — folder reference (trailing slash)\n- \\`[[other-wiki:slug]]\\` — cross-wiki reference\n\nEvery page should link to at least one entity when possible. A page with no\nentity link is suspect.\n\n## Pages live in \\`.almanac/pages/\\`\n\nOne markdown file per page, kebab-case slug. Frontmatter carries \\`topics:\\`\nand optional \\`files:\\`. The rest is prose.\n`;\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 { 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 { createHash } from \"node:crypto\";\nimport {\n createWriteStream,\n existsSync,\n statSync,\n type WriteStream,\n} from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, join, relative } from \"node:path\";\n\nimport type { AgentDefinition, SDKMessage } from \"@anthropic-ai/claude-agent-sdk\";\n\nimport { assertClaudeAuth, type SpawnCliFn } from \"../agent/auth.js\";\nimport { loadPrompt } from \"../agent/prompts.js\";\nimport {\n runAgent,\n type AgentResult,\n type RunAgentOptions,\n} from \"../agent/sdk.js\";\nimport { parseFrontmatter } from \"../indexer/frontmatter.js\";\nimport { findNearestAlmanacDir, getRepoAlmanacDir } from \"../paths.js\";\nimport { StreamingFormatter } from \"./bootstrap.js\";\n\nexport interface CaptureOptions {\n cwd: string;\n /** Explicit transcript path. Skips auto-resolution. */\n transcriptPath?: string;\n /** Target a specific session ID. */\n sessionId?: string;\n /** Suppress per-tool-use streaming; print only the final summary line. */\n quiet?: boolean;\n /** Model override. Defaults to the SDK default (sonnet-4-6). */\n model?: string;\n /** Injectable agent runner — tests replace this with a fake. */\n runAgent?: (opts: RunAgentOptions) => Promise<AgentResult>;\n /**\n * Injectable spawner for the Claude auth-status subprocess. Tests pass\n * a stub; production uses `defaultSpawnCli` which shells out to the\n * bundled SDK's `cli.js`.\n */\n spawnCli?: SpawnCliFn;\n /** Clock injection for deterministic log filenames in tests. */\n now?: () => Date;\n /**\n * Override the Claude Code projects directory when auto-resolving a\n * transcript. Production code leaves this undefined and we fall back to\n * `~/.claude/projects`; tests point it at a fixture dir.\n */\n claudeProjectsDir?: string;\n}\n\nexport interface CaptureResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Tools the writer agent is permitted to use.\n *\n * - `Read` — read the session transcript, existing wiki pages, source files\n * - `Write` / `Edit` — create and update pages under `.almanac/pages/`\n * - `Glob` / `Grep` — navigate the wiki and source code\n * - `Bash` — interrogate the wiki via `almanac search/show/info/list`\n * - `Agent` — invoke the reviewer subagent\n *\n * `WebFetch`/`WebSearch` are intentionally absent: the writer should work\n * from the transcript + repo, not the open internet.\n */\nconst WRITER_TOOLS = [\"Read\", \"Write\", \"Edit\", \"Glob\", \"Grep\", \"Bash\", \"Agent\"];\n\n/**\n * Tools the reviewer subagent is permitted to use. The absence of\n * `Write`/`Edit`/`Agent` is the *only* thing preventing the reviewer from\n * editing files or chaining to further subagents — the SDK enforces this\n * based on the `tools` field in the `AgentDefinition`.\n */\nconst REVIEWER_TOOLS = [\"Read\", \"Grep\", \"Glob\", \"Bash\"];\n\nconst REVIEWER_DESCRIPTION =\n \"Reviews proposed wiki changes against the full knowledge base for \" +\n \"cohesion, duplication, missing links, notability, and writing conventions.\";\n\n/**\n * `almanac capture` — writer agent + reviewer subagent on a session transcript.\n *\n * Flow:\n * 1. Auth gate (ANTHROPIC_API_KEY).\n * 2. Resolve repo root (walk up for `.almanac/`). Refuse if none.\n * 3. Resolve transcript path (arg, --session, or auto-resolve from\n * Claude Code's session storage).\n * 4. Snapshot `.almanac/pages/` BEFORE the agent runs so we can compute\n * a created/updated/archived summary when it finishes.\n * 5. Load `prompts/writer.md` + `prompts/reviewer.md`. Build a reviewer\n * `AgentDefinition` with read-only tools.\n * 6. Run the writer agent with the reviewer registered under `agents`.\n * 7. Stream tool-uses via the shared `StreamingFormatter` (unless --quiet).\n * 8. Diff the snapshot → emit `[done] N updated, M created, K archived …`.\n *\n * Empty outcomes (writer wrote nothing) exit 0 with a clear \"notability bar\"\n * message — per the writer prompt, silence is a valid output.\n */\nexport async function runCapture(\n options: CaptureOptions,\n): Promise<CaptureResult> {\n // Fail before any filesystem work. `assertClaudeAuth` accepts either\n // subscription OAuth (via the bundled SDK CLI) or `ANTHROPIC_API_KEY`;\n // missing both surfaces a two-option error with exit 1 so the\n // SessionEnd hook (which backgrounds + redirects to a sidecar log)\n // doesn't silently treat auth failure as a successful capture.\n try {\n await assertClaudeAuth(options.spawnCli);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n stdout: \"\",\n stderr: `almanac: ${msg}\\n`,\n exitCode: 1,\n };\n }\n\n // Resolve the repo root by walking up for `.almanac/`. Unlike bootstrap,\n // capture refuses to run when no wiki exists — the writer needs existing\n // pages to read against, and auto-initing here would hide the fact that\n // the user skipped `almanac bootstrap`.\n const repoRoot = findNearestAlmanacDir(options.cwd);\n if (repoRoot === null) {\n return {\n stdout: \"\",\n stderr:\n \"almanac: no .almanac/ found in this directory or any parent. \" +\n \"Run 'almanac init' or 'almanac bootstrap' first.\\n\",\n exitCode: 1,\n };\n }\n\n const almanacDir = getRepoAlmanacDir(repoRoot);\n const pagesDir = join(almanacDir, \"pages\");\n\n // Resolve the transcript path up front. Doing this before we open a log\n // stream keeps bad-arg errors uncluttered by side effects.\n const transcriptResolution = await resolveTranscript({\n repoRoot,\n explicit: options.transcriptPath,\n sessionId: options.sessionId,\n claudeProjectsDir: options.claudeProjectsDir,\n });\n if (!transcriptResolution.ok) {\n return {\n stdout: \"\",\n stderr: `almanac: ${transcriptResolution.error}\\n`,\n exitCode: 1,\n };\n }\n const transcriptPath = transcriptResolution.path;\n\n // Snapshot the pages dir BEFORE the writer runs. We compare against it\n // after the agent exits to compute a created/updated/archived tally.\n // Doing this in TS (not via the agent's self-reporting) means the summary\n // stays trustworthy even if the writer gets confused about what it did.\n const snapshotBefore = await snapshotPages(pagesDir);\n\n // Load the two prompts. Kept sequential rather than parallel — both files\n // are tiny and the second read is cache-warm.\n const systemPrompt = await loadPrompt(\"writer\");\n const reviewerPrompt = await loadPrompt(\"reviewer\");\n\n const agents: Record<string, AgentDefinition> = {\n reviewer: {\n description: REVIEWER_DESCRIPTION,\n prompt: reviewerPrompt,\n tools: REVIEWER_TOOLS,\n },\n };\n\n // Transcript log filename: timestamp-based so repeated runs don't clobber\n // each other. We don't have the SDK session_id yet (it's on the first\n // message), and filesystem writes need a destination before the stream\n // begins.\n const now = options.now?.() ?? new Date();\n const logName = `.capture-${formatTimestamp(now)}.log`;\n const logPath = join(almanacDir, logName);\n const logStream = createWriteStream(logPath, { flags: \"w\" });\n\n const out = process.stdout;\n const formatter = new StreamingFormatter({\n write: (line: string) => {\n if (options.quiet !== true) out.write(line);\n },\n });\n // The shared StreamingFormatter defaults its currentAgent to \"bootstrap\"\n // because bootstrap was the first command to use it. For capture the\n // writer owns the top-level turn, so relabel.\n formatter.setAgent(\"writer\");\n\n const onMessage = (msg: SDKMessage): void => {\n try {\n logStream.write(`${JSON.stringify(msg)}\\n`);\n } catch {\n // Best-effort: one unserializable message shouldn't kill the whole\n // stream. Humans read the log; if a line is missing they can re-run.\n }\n formatter.handle(msg);\n };\n\n // Pass an ABSOLUTE path for the transcript so the writer doesn't have to\n // guess at cwd semantics. Everything else (`.almanac/pages/`) is already\n // relative to the cwd the SDK gives its tools.\n const userPrompt =\n `Capture this coding session.\\n` +\n `Transcript: ${transcriptPath}.\\n` +\n `Working directory: ${repoRoot}.`;\n\n const runner = options.runAgent ?? runAgent;\n\n let result: AgentResult;\n try {\n result = await runner({\n systemPrompt,\n prompt: userPrompt,\n allowedTools: WRITER_TOOLS,\n agents,\n cwd: repoRoot,\n model: options.model,\n // Capture sessions can touch many pages; give it more headroom than\n // bootstrap. The SDK treats `maxTurns` as a hard stop — better to\n // overshoot than to cut off mid-review.\n maxTurns: 150,\n onMessage,\n });\n } finally {\n await closeStream(logStream);\n }\n\n const snapshotAfter = await snapshotPages(pagesDir);\n const delta = diffSnapshots(snapshotBefore, snapshotAfter);\n\n if (!result.success) {\n return {\n stdout: \"\",\n stderr:\n `almanac: capture failed: ${result.error ?? \"unknown error\"}\\n` +\n `(transcript: ${relative(repoRoot, logPath)})\\n`,\n exitCode: 1,\n };\n }\n\n const summary = formatSummary(result, delta, logPath, repoRoot);\n\n return {\n stdout: `${summary}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Transcript resolution ────────────────────────────────────────────────\n\ninterface ResolvedTranscript {\n ok: true;\n path: string;\n}\ninterface FailedTranscript {\n ok: false;\n error: string;\n}\n\n/**\n * Resolve the transcript path from the three possible sources, in priority:\n * 1. Explicit positional arg (`almanac capture <path>`).\n * 2. `--session <id>`: find the single `.jsonl` matching that ID.\n * 3. Auto-resolve: most recent `.jsonl` under Claude Code's projects dir\n * whose parent directory hashes to `repoRoot`. If multiple candidates\n * or none match, return an error directing the user to pass a path.\n *\n * Claude Code names the per-project directory with a path-hash that we\n * can't deterministically reproduce without reading Claude Code's source.\n * Rather than guess at the hashing scheme, we scan all project dirs, pick\n * the one whose most recent transcript mentions the `repoRoot` in its\n * `cwd` field, and take the newest `.jsonl` from there.\n */\nasync function resolveTranscript(args: {\n repoRoot: string;\n explicit?: string;\n sessionId?: string;\n claudeProjectsDir?: string;\n}): Promise<ResolvedTranscript | FailedTranscript> {\n if (args.explicit !== undefined && args.explicit.length > 0) {\n if (!existsSync(args.explicit)) {\n return {\n ok: false,\n error: `transcript not found: ${args.explicit}`,\n };\n }\n return { ok: true, path: args.explicit };\n }\n\n const projectsDir =\n args.claudeProjectsDir ?? join(homedir(), \".claude\", \"projects\");\n if (!existsSync(projectsDir)) {\n return {\n ok: false,\n error:\n `could not auto-resolve transcript; ${projectsDir} does not exist. ` +\n `Pass --session <id> or <transcript-path>.`,\n };\n }\n\n const allTranscripts = await collectTranscripts(projectsDir);\n\n if (args.sessionId !== undefined && args.sessionId.length > 0) {\n const expected = `${args.sessionId}.jsonl`;\n const match = allTranscripts.find((t) => basename(t.path) === expected);\n if (match === undefined) {\n return {\n ok: false,\n error:\n `no transcript found for session ${args.sessionId} under ${projectsDir}`,\n };\n }\n return { ok: true, path: match.path };\n }\n\n // Auto-resolve: prefer transcripts whose `cwd` field matches `repoRoot`,\n // then fall back to the most recently modified if no cwd match is found.\n // We read a peek of each transcript (not the whole file) to check the\n // cwd — JSONL's first line typically carries it.\n const matches = await filterTranscriptsByCwd(allTranscripts, args.repoRoot);\n\n if (matches.length === 0) {\n return {\n ok: false,\n error:\n `could not auto-resolve transcript under ${projectsDir}; ` +\n `no session matches cwd ${args.repoRoot}. ` +\n `Pass --session <id> or <transcript-path>.`,\n };\n }\n\n // Sort by mtime desc and pick the newest.\n matches.sort((a, b) => b.mtime - a.mtime);\n return { ok: true, path: matches[0]!.path };\n}\n\ninterface TranscriptEntry {\n path: string;\n mtime: number;\n}\n\nasync function collectTranscripts(\n projectsDir: string,\n): Promise<TranscriptEntry[]> {\n const out: TranscriptEntry[] = [];\n let topLevel: string[];\n try {\n topLevel = await readdir(projectsDir);\n } catch {\n return out;\n }\n for (const name of topLevel) {\n const projectDir = join(projectsDir, name);\n let entries: string[];\n try {\n entries = await readdir(projectDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (!entry.endsWith(\".jsonl\")) continue;\n const full = join(projectDir, entry);\n try {\n const st = await stat(full);\n if (st.isFile()) {\n out.push({ path: full, mtime: st.mtimeMs });\n }\n } catch {\n // Transient read error, skip.\n }\n }\n }\n return out;\n}\n\n/**\n * Keep only transcripts whose first session record mentions a `cwd` that\n * matches `repoRoot` (exact string match). We also match on the Claude\n * Code project-hash heuristic: the per-project dir name is usually the\n * absolute repo path with `/` replaced by `-`. Falling back to the hash\n * heuristic means we still resolve sanely when the JSONL format changes.\n */\nasync function filterTranscriptsByCwd(\n transcripts: TranscriptEntry[],\n repoRoot: string,\n): Promise<TranscriptEntry[]> {\n const dirHash = `-${repoRoot.replace(/^\\/+/, \"\").replace(/\\//g, \"-\")}`;\n\n const byDirName = transcripts.filter((t) => {\n const parent = basename(join(t.path, \"..\"));\n return parent === dirHash || parent.endsWith(dirHash);\n });\n if (byDirName.length > 0) return byDirName;\n\n // Fallback: peek into each JSONL for a `\"cwd\":\"<repoRoot>\"` needle.\n const needle = `\"cwd\":\"${repoRoot}\"`;\n const hits: TranscriptEntry[] = [];\n for (const t of transcripts) {\n try {\n const head = await readHead(t.path, 4096);\n if (head.includes(needle)) hits.push(t);\n } catch {\n continue;\n }\n }\n return hits;\n}\n\nasync function readHead(path: string, bytes: number): Promise<string> {\n // Small files — just read the whole thing. We only call this on .jsonl\n // files, which can be large, so cap at `bytes` via slicing.\n const content = await readFile(path, \"utf8\");\n return content.length > bytes ? content.slice(0, bytes) : content;\n}\n\n// ─── Snapshot / delta ─────────────────────────────────────────────────────\n\ninterface PageSnapshotEntry {\n slug: string;\n /** SHA-256 of file bytes — cheap, stable, avoids relying on mtime. */\n hash: string;\n /** `true` when the frontmatter has `archived_at` set. */\n archived: boolean;\n}\n\ntype PageSnapshot = Map<string, PageSnapshotEntry>;\n\nasync function snapshotPages(pagesDir: string): Promise<PageSnapshot> {\n const out: PageSnapshot = new Map();\n if (!existsSync(pagesDir)) return out;\n\n let entries: string[];\n try {\n entries = await readdir(pagesDir);\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (!entry.endsWith(\".md\")) continue;\n const slug = entry.slice(0, -3);\n const full = join(pagesDir, entry);\n try {\n const st = statSync(full);\n if (!st.isFile()) continue;\n const content = await readFile(full, \"utf8\");\n const hash = createHash(\"sha256\").update(content).digest(\"hex\");\n const fm = parseFrontmatter(content);\n out.set(slug, {\n slug,\n hash,\n archived: fm.archived_at !== null,\n });\n } catch {\n continue;\n }\n }\n return out;\n}\n\ninterface SnapshotDelta {\n created: number;\n updated: number;\n archived: number;\n}\n\nfunction diffSnapshots(\n before: PageSnapshot,\n after: PageSnapshot,\n): SnapshotDelta {\n let created = 0;\n let updated = 0;\n let archived = 0;\n\n for (const [slug, entry] of after) {\n const prev = before.get(slug);\n if (prev === undefined) {\n created += 1;\n continue;\n }\n if (prev.hash !== entry.hash) {\n // An edit that flips a page from active → archived counts as\n // \"archived\", not \"updated\" — the archive is the semantically\n // interesting thing.\n if (!prev.archived && entry.archived) {\n archived += 1;\n } else {\n updated += 1;\n }\n }\n }\n // Note: we deliberately don't track deleted pages. The writer prompt\n // tells agents to archive (via frontmatter), not delete — a page that\n // disappears entirely is a protocol violation worth surfacing, but not\n // by silently counting it in the summary.\n\n return { created, updated, archived };\n}\n\n// ─── Formatting ───────────────────────────────────────────────────────────\n\nfunction formatSummary(\n result: AgentResult,\n delta: SnapshotDelta,\n logPath: string,\n repoRoot: string,\n): string {\n const rel = relative(repoRoot, logPath);\n const cost = `$${result.cost.toFixed(3)}`;\n const { created, updated, archived } = delta;\n\n if (created === 0 && updated === 0 && archived === 0) {\n return (\n `[capture] no new knowledge met the notability bar (0 pages written), ` +\n `cost: ${cost}, turns: ${result.turns} (transcript: ${rel})`\n );\n }\n\n return (\n `[done] ${updated} page${updated === 1 ? \"\" : \"s\"} updated, ` +\n `${created} created, ` +\n `${archived} archived, ` +\n `cost: ${cost}, turns: ${result.turns} (transcript: ${rel})`\n );\n}\n\nfunction formatTimestamp(d: Date): string {\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n return (\n `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-` +\n `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`\n );\n}\n\nfunction closeStream(stream: WriteStream): Promise<void> {\n return new Promise((resolve) => {\n stream.end(() => resolve());\n });\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 } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\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 * - **Idempotent.** `install` twice leaves one entry, not two. We match by\n * `command` string equality — if the user replaces our absolute path\n * with a symlink pointing at the same script, we'll treat it as foreign.\n * That's acceptable; the `status` output shows the path we'd use, so the\n * 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.\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 /**\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\nexport interface HookCommandResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst HOOK_TIMEOUT_SECONDS = 10;\n\ninterface SessionEndEntry {\n type: \"command\";\n command: string;\n timeout?: number;\n}\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 so\n * we don't drop user settings when we write the file back.\n */\ntype SettingsJson = Record<string, unknown> & {\n hooks?: Record<string, SessionEndEntry[] | undefined>;\n};\n\nexport async function runHookInstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const script = resolveHookScriptPath(options);\n if (!script.ok) {\n return { stdout: \"\", stderr: `almanac: ${script.error}\\n`, exitCode: 1 };\n }\n\n const settingsPath = resolveSettingsPath(options);\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n // Find any existing entry with our exact command. If found, the install\n // is a no-op (idempotent). If SessionEnd has OTHER commands alongside\n // ours, leave them alone — the user might be composing multiple hooks.\n const ourEntries = existing.filter((e) => e.command === script.path);\n const foreignEntries = existing.filter((e) => e.command !== script.path);\n\n // If the sole existing entry looks like ours-but-on-a-different-path\n // (e.g. an old install from a different node_modules), replace it so we\n // don't double-fire on session end. Heuristic: command ends with\n // `almanac-capture.sh`. We accept this specific rename; unrelated\n // commands still block.\n const stale = foreignEntries.filter((e) =>\n e.command.endsWith(\"almanac-capture.sh\"),\n );\n const unrelated = foreignEntries.filter(\n (e) => !e.command.endsWith(\"almanac-capture.sh\"),\n );\n\n if (unrelated.length > 0) {\n const existingStr = unrelated.map((e) => ` - ${e.command}`).join(\"\\n\");\n return {\n stdout: \"\",\n stderr:\n `almanac: SessionEnd hook already has a foreign entry:\\n${existingStr}\\n` +\n `Remove it manually from ${settingsPath} if you want almanac to manage the hook.\\n`,\n exitCode: 1,\n };\n }\n\n if (ourEntries.length > 0 && stale.length === 0) {\n return {\n stdout: `almanac: SessionEnd hook already installed at ${script.path}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Compose the new SessionEnd array: keep our one entry (fresh), drop any\n // stale almanac entries.\n const newEntries: SessionEndEntry[] = [\n {\n type: \"command\",\n command: script.path,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ];\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: ${script.path}\\n` +\n ` settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\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 // Remove ONLY our entries — anything else stays. We treat any entry with\n // a command ending in `almanac-capture.sh` as ours (handles the case\n // where the bundled path moved between `npm i` locations).\n const kept = existing.filter((e) => !e.command.endsWith(\"almanac-capture.sh\"));\n const removed = existing.length - kept.length;\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 const ours = existing.find((e) => e.command.endsWith(\"almanac-capture.sh\"));\n\n if (ours === undefined) {\n const foreign = existing\n .map((e) => ` - ${e.command}`)\n .join(\"\\n\");\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath}\\n` +\n (existing.length > 0\n ? `(${existing.length} foreign entr${existing.length === 1 ? \"y\" : \"ies\"} present:\\n${foreign})\\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: ${ours.command}\\n` +\n `settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────\n\nfunction resolveSettingsPath(options: HookCommandOptions): string {\n if (options.settingsPath !== undefined) return options.settingsPath;\n return path.join(homedir(), \".claude\", \"settings.json\");\n}\n\ntype ScriptResolution =\n | { ok: true; path: string }\n | { ok: false; error: string };\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 */\nfunction resolveHookScriptPath(options: HookCommandOptions): 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: `.../codealmanac/src/commands/hook.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\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 { 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 { parseDuration } from \"../indexer/duration.js\";\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.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) => ` ${o.slug}`),\n ),\n );\n sections.push(\n section(\n \"stale\",\n r.stale.length,\n r.stale.map((s) => ` ${s.slug} (${s.days_since_update} days)`),\n ),\n );\n sections.push(\n section(\n \"dead-refs\",\n r.dead_refs.length,\n r.dead_refs.map((d) => ` ${d.slug} references ${d.path} (missing)`),\n ),\n );\n sections.push(\n section(\n \"broken-links\",\n r.broken_links.length,\n r.broken_links.map(\n (b) => ` ${b.source_slug} → ${b.target_slug} (target does not exist)`,\n ),\n ),\n );\n sections.push(\n section(\n \"broken-xwiki\",\n r.broken_xwiki.length,\n r.broken_xwiki.map(\n (b) =>\n ` ${b.source_slug} → ${b.target_wiki}:${b.target_slug} (wiki unregistered or unreachable)`,\n ),\n ),\n );\n sections.push(\n section(\n \"empty-topics\",\n r.empty_topics.length,\n r.empty_topics.map((e) => ` ${e.slug}`),\n ),\n );\n sections.push(\n section(\n \"empty-pages\",\n r.empty_pages.length,\n r.empty_pages.map((e) => ` ${e.slug}`),\n ),\n );\n sections.push(\n section(\n \"slug-collisions\",\n r.slug_collisions.length,\n r.slug_collisions.map((c) => ` ${c.slug}: ${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 `${label} (0): (ok)`;\n return `${label} (${count}):\\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 { createHash } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { readFile, utimes } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { toKebabCase } from \"../slug.js\";\nimport { loadTopicsFile, titleCase } from \"../topics/yaml.js\";\nimport { firstH1, parseFrontmatter } from \"./frontmatter.js\";\nimport {\n normalizePath,\n normalizePathPreservingCase,\n looksLikeDir,\n} from \"./paths.js\";\nimport { openIndex } from \"./schema.js\";\nimport { extractWikilinks } from \"./wikilinks.js\";\n\n/**\n * Filename relative to the `.almanac/` dir where the topic DAG + metadata\n * lives. The indexer loads it on every reindex; the topics commands\n * mutate it atomically. Keep this in sync with `src/topics/paths.ts`'s\n * `topicsYamlPath` — duplicated here only so the indexer doesn't import\n * a second \"where is it\" helper.\n */\nconst TOPICS_YAML_FILENAME = \"topics.yaml\";\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// Glob is relative to `pagesDir` (which is `.almanac/pages/`), so this is\n// just \"every .md at any depth\" — not `pages/**/*.md`, because we've\n// already `cd`'d into `pages/` logically.\nconst PAGES_GLOB = \"**/*.md\";\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 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 // `relative` keeps lint happy about unused imports; total is just the\n // count of .md files we saw on this pass.\n void relative;\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\n/**\n * Return true if any `pages/**\\/*.md` has an mtime strictly greater than\n * the index DB's mtime. We walk with `fast-glob` rather than shell out to\n * `find` for portability.\n *\n * This is the \"should we reindex?\" check. It's intentionally cheap —\n * `fast-glob` with `stats: true` gives us mtimes without a second `stat`\n * round-trip.\n */\nfunction 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 // Synchronous walk — `fg.sync` is fine at this scale and keeps the\n // decision path simple (we don't need to await inside every CLI entry).\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\nfunction hashContent(raw: string): string {\n return createHash(\"sha256\").update(raw).digest(\"hex\");\n}\n\n/**\n * Return true if `topics.yaml` has an mtime strictly greater than the\n * index DB's mtime. This is the topics-side mirror of `pagesNewerThan`\n * — mutations to `topics.yaml` (title/description/parents) aren't\n * visible from any page mtime, so we need a separate freshness hook\n * for the file itself.\n *\n * Missing `topics.yaml` → false. Absence is legal and means \"no topic\n * metadata, only whatever pages declare\". A missing file doesn't\n * invalidate the existing index.\n */\nfunction topicsYamlNewerThan(almanacDir: string, dbPath: string): 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/**\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` (with title + description) and rewrite\n * that topic's edges in `topic_parents`. Topics that were ad-hoc-only\n * before (mentioned in page frontmatter, never `almanac topics\n * create`d) get their display name promoted to whatever is in the\n * file.\n *\n * Importantly, we do NOT delete `topics` rows that live only in page\n * frontmatter — those are legal, per the spec (\"any slug mentioned in\n * pages' `topics:` frontmatter gets a row, even if not in\n * topics.yaml\"). We also do NOT clear `topic_parents` wholesale; we\n * rewrite edges for each declared topic but leave untouched rows for\n * ad-hoc topics (which by definition have no declared parents).\n *\n * Missing file = no-op. This is the \"no topic metadata yet\" state and\n * callers shouldn't have to paper over it.\n */\nasync 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 // Collect every slug we consider \"declared\" — in topics.yaml or\n // referenced by any page_topics row. Anything outside this set is\n // stale (e.g., a topic that used to be in the file before a rename\n // or delete, or an ad-hoc slug whose only page just got untagged).\n // Those get removed so `empty-topics` doesn't falsely flag them.\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. We do this\n // last so the upserts above have already 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 { 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","/**\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 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 { 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 { 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 init` 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","import { join } from \"node:path\";\n\nimport type Database from \"better-sqlite3\";\n\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface InfoOptions {\n cwd: string;\n slug?: string;\n stdin?: boolean;\n stdinInput?: string;\n json?: boolean;\n wiki?: string;\n}\n\nexport interface InfoRecord {\n slug: string;\n title: string | null;\n file_path: string;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n supersedes: string[]; // pages that declare superseded_by = this slug\n topics: string[];\n file_refs: Array<{ path: string; is_dir: boolean }>;\n wikilinks_out: string[];\n wikilinks_in: string[];\n cross_wiki_links: Array<{ wiki: string; target: string }>;\n}\n\nexport interface InfoCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac info <slug>` — structured view of one page.\n *\n * Human-readable output groups the fields into labeled sections; `--json`\n * emits the raw record. Bulk mode (`--stdin`) defaults to JSON output\n * (array of records) because interleaved human-readable blobs are hard to\n * parse downstream.\n *\n * Backlinks (`wikilinks_in`) are computed per call — there's no\n * materialized reverse index in slice 2, which keeps indexing simple and\n * the query is trivial at the size of a normal wiki.\n */\nexport async function runInfo(\n options: InfoOptions,\n): Promise<InfoCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const slugs = collectSlugs(options);\n if (slugs.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: info requires a slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n const records: InfoRecord[] = [];\n const missing: string[] = [];\n for (const slug of slugs) {\n const rec = fetchInfo(db, slug);\n if (rec === null) {\n missing.push(slug);\n continue;\n }\n records.push(rec);\n }\n\n const bulk = options.stdin === true;\n const jsonOut = options.json === true || bulk;\n\n // JSON shape rule (consumers depend on this being predictable):\n // --stdin → always array of records (even for a single slug)\n // positional → always a single object (never an array)\n // The previous implementation accidentally reshaped \"positional with\n // zero records\" as an array and positional with one record as an\n // object, which made downstream callers have to sniff the shape.\n // Now: `info --stdin` gives you a list to iterate; `info <slug>` gives\n // you one record to dot-access.\n let stdout: string;\n if (jsonOut) {\n if (bulk) {\n stdout = `${JSON.stringify(records, null, 2)}\\n`;\n } else {\n // Positional mode: we already short-circuited on empty slugs,\n // and a missing page is in `missing[]` — so `records` is either\n // length 1 (found) or length 0 (missing). For length 0 we emit\n // `null` so the shape is still object-ish, not an empty array.\n const only = records[0] ?? null;\n stdout = `${JSON.stringify(only, null, 2)}\\n`;\n }\n } else {\n stdout = records.map(formatHumanReadable).join(\"\\n\");\n }\n\n const stderr = missing\n .map((s) => `almanac: no such page \"${s}\"\\n`)\n .join(\"\");\n return {\n stdout,\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n } finally {\n db.close();\n }\n}\n\nfunction fetchInfo(db: Database.Database, slug: string): InfoRecord | null {\n const pageRow = db\n .prepare<\n [string],\n {\n slug: string;\n title: string | null;\n file_path: string;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n }\n >(\n \"SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?\",\n )\n .get(slug);\n if (pageRow === undefined) return null;\n\n const topics = db\n .prepare<[string], { topic_slug: string }>(\n \"SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug\",\n )\n .all(slug)\n .map((r) => r.topic_slug);\n\n const refs = db\n .prepare<[string], { original_path: string; is_dir: number }>(\n // Display the author's casing (`original_path`), not the\n // lowercased lookup form. The lowercased `path` column is the\n // query key for `--mentions`; it's not a user-facing string.\n \"SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path\",\n )\n .all(slug)\n .map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));\n\n const linksOut = db\n .prepare<[string], { target_slug: string }>(\n \"SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug\",\n )\n .all(slug)\n .map((r) => r.target_slug);\n\n const linksIn = db\n .prepare<[string], { source_slug: string }>(\n \"SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug\",\n )\n .all(slug)\n .map((r) => r.source_slug);\n\n const xwiki = db\n .prepare<[string], { target_wiki: string; target_slug: string }>(\n \"SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug\",\n )\n .all(slug)\n .map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));\n\n // \"Pages that say `supersedes: <me>`\" — the reverse side of this\n // page's `superseded_by` field. We read this from `pages` rather than\n // a dedicated column because the spec (deliberately) doesn't index\n // `supersedes:` as its own table — the relationship is already implicit\n // in whoever points at us.\n const supersedesRows = db\n .prepare<[string], { slug: string }>(\n \"SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug\",\n )\n .all(slug)\n .map((r) => r.slug);\n\n return {\n slug: pageRow.slug,\n title: pageRow.title,\n file_path: pageRow.file_path,\n updated_at: pageRow.updated_at,\n archived_at: pageRow.archived_at,\n superseded_by: pageRow.superseded_by,\n supersedes: supersedesRows,\n topics,\n file_refs: refs,\n wikilinks_out: linksOut,\n wikilinks_in: linksIn,\n cross_wiki_links: xwiki,\n };\n}\n\nfunction collectSlugs(options: InfoOptions): string[] {\n if (options.stdin === true && options.stdinInput !== undefined) {\n return options.stdinInput\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (options.slug !== undefined && options.slug.length > 0) {\n return [options.slug];\n }\n return [];\n}\n\nfunction formatHumanReadable(rec: InfoRecord): string {\n const lines: string[] = [];\n lines.push(`slug: ${rec.slug}`);\n lines.push(`title: ${rec.title ?? \"—\"}`);\n lines.push(`file: ${rec.file_path}`);\n lines.push(`updated_at: ${new Date(rec.updated_at * 1000).toISOString()}`);\n if (rec.archived_at !== null) {\n lines.push(\n `archived_at: ${new Date(rec.archived_at * 1000).toISOString()}`,\n );\n }\n if (rec.superseded_by !== null) {\n lines.push(`superseded_by: ${rec.superseded_by}`);\n }\n if (rec.supersedes.length > 0) {\n lines.push(`supersedes: ${rec.supersedes.join(\", \")}`);\n }\n lines.push(`topics: ${rec.topics.length > 0 ? rec.topics.join(\", \") : \"—\"}`);\n lines.push(\"file_refs:\");\n if (rec.file_refs.length === 0) {\n lines.push(\" —\");\n } else {\n for (const r of rec.file_refs) {\n lines.push(` ${r.path}${r.is_dir ? \" (dir)\" : \"\"}`);\n }\n }\n lines.push(\"wikilinks_out:\");\n if (rec.wikilinks_out.length === 0) lines.push(\" —\");\n else for (const t of rec.wikilinks_out) lines.push(` ${t}`);\n lines.push(\"wikilinks_in:\");\n if (rec.wikilinks_in.length === 0) lines.push(\" —\");\n else for (const s of rec.wikilinks_in) lines.push(` ${s}`);\n if (rec.cross_wiki_links.length > 0) {\n lines.push(\"cross_wiki_links:\");\n for (const x of rec.cross_wiki_links) {\n lines.push(` ${x.wiki}:${x.target}`);\n }\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import { existsSync } from \"node:fs\";\n\nimport {\n dropEntry,\n readRegistry,\n type RegistryEntry,\n} from \"../registry/index.js\";\n\nexport interface ListOptions {\n json?: boolean;\n drop?: string;\n}\n\nexport interface ListCommandOutput {\n stdout: string;\n exitCode: number;\n}\n\n/**\n * `almanac list` — the global discovery surface. Three modes:\n *\n * - default: pretty table of reachable wikis\n * - `--json`: structured output (reachable wikis only, by default)\n * - `--drop <name>`: explicit removal, then exits\n *\n * **Unreachable paths are silently skipped in the default output**, but\n * never auto-dropped. This is the registry hygiene rule from the design —\n * branch switches, unmounted drives, and VM-offline repos should not cost\n * the user a registration. Only `--drop` removes entries.\n */\nexport async function listWikis(\n options: ListOptions,\n): Promise<ListCommandOutput> {\n if (options.drop !== undefined) {\n return handleDrop(options.drop);\n }\n\n const entries = await readRegistry();\n const reachable = entries.filter((e) => isReachable(e));\n\n if (options.json === true) {\n return { stdout: `${JSON.stringify(reachable, null, 2)}\\n`, exitCode: 0 };\n }\n\n return { stdout: formatPretty(reachable), exitCode: 0 };\n}\n\nasync function handleDrop(name: string): Promise<ListCommandOutput> {\n const removed = await dropEntry(name);\n if (removed === null) {\n return {\n stdout: `no registry entry named \"${name}\"\\n`,\n exitCode: 1,\n };\n }\n return {\n stdout: `removed \"${removed.name}\" (${removed.path})\\n`,\n exitCode: 0,\n };\n}\n\n/**\n * A registry path is \"reachable\" if something still exists at that path.\n * We use `existsSync` rather than `stat` — we don't care whether the path\n * is a directory or has a `.almanac/` inside; we only hide it from default\n * output when the path itself is gone (e.g., repo deleted, drive\n * unmounted).\n */\nfunction isReachable(entry: RegistryEntry): boolean {\n if (entry.path.length === 0) return false;\n return existsSync(entry.path);\n}\n\n/**\n * Human-readable listing. Empty state prints a gentle hint rather than a\n * blank screen, and entries render in registration order (chronological,\n * since `addEntry` appends).\n */\nfunction formatPretty(entries: RegistryEntry[]): string {\n if (entries.length === 0) {\n return \"no wikis registered. run `almanac init` in a repo to create one.\\n\";\n }\n\n // Column-width the name for alignment; cap at 30 so absurd names don't\n // stretch the whole table.\n const nameWidth = Math.min(\n 30,\n entries.reduce((w, e) => Math.max(w, e.name.length), 0),\n );\n\n const lines: string[] = [];\n for (const entry of entries) {\n const name = entry.name.padEnd(nameWidth);\n const desc = entry.description.length > 0 ? entry.description : \"—\";\n lines.push(`${name} ${desc}`);\n lines.push(`${\" \".repeat(nameWidth)} ${entry.path}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import { join } from \"node:path\";\n\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface PathOptions {\n cwd: string;\n slug?: string;\n stdin?: boolean;\n stdinInput?: string;\n wiki?: string;\n}\n\nexport interface PathCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac path <slug>` — slug → absolute file path.\n *\n * Pure scripting helper. With `--stdin` it maps each line of input to its\n * resolved path; unresolvable slugs go to stderr with a non-zero exit\n * while the resolvable ones still print on stdout.\n */\nexport async function runPath(\n options: PathOptions,\n): Promise<PathCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const slugs = collectSlugs(options);\n if (slugs.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: path requires a slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n const stmt = db.prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n );\n const resolved: string[] = [];\n const missing: string[] = [];\n for (const slug of slugs) {\n const row = stmt.get(slug);\n if (row === undefined) {\n missing.push(slug);\n continue;\n }\n resolved.push(row.file_path);\n }\n\n const stdout = resolved.length > 0 ? `${resolved.join(\"\\n\")}\\n` : \"\";\n const stderr = missing\n .map((s) => `almanac: no such page \"${s}\"\\n`)\n .join(\"\");\n return {\n stdout,\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n } finally {\n db.close();\n }\n}\n\nfunction collectSlugs(options: PathOptions): string[] {\n if (options.stdin === true && options.stdinInput !== undefined) {\n return options.stdinInput\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (options.slug !== undefined && options.slug.length > 0) {\n return [options.slug];\n }\n return [];\n}\n","import { runIndexer, type IndexResult } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\n\nexport interface ReindexOptions {\n cwd: string;\n wiki?: string;\n}\n\nexport interface ReindexCommandOutput {\n result: IndexResult;\n stdout: string;\n exitCode: number;\n}\n\n/**\n * `almanac reindex` — force a full rebuild.\n *\n * Unlike the implicit reindex every query command triggers, this one\n * prints a summary line so the user gets feedback for an explicitly\n * requested action. The summary is terse on purpose (one line, three\n * numbers) — verbose progress reporting would fight the design rule that\n * the CLI stays quiet by default.\n */\nexport async function runReindex(\n options: ReindexOptions,\n): Promise<ReindexCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n const result = await runIndexer({ repoRoot });\n // Summary wording: \"reindexed: N pages (K updated, R removed)\". When\n // some files were on disk but never made it into the index\n // (slug collisions, ENOENT races, un-sluggable filenames), tack on a\n // `; S skipped` suffix so the user notices. The per-file reason was\n // already written to stderr at indexing time.\n const skipSuffix =\n result.filesSkipped > 0 ? `; ${result.filesSkipped} skipped` : \"\";\n const stdout = `reindexed: ${result.pagesIndexed} page${result.pagesIndexed === 1 ? \"\" : \"s\"} (${result.changed} updated, ${result.removed} removed${skipSuffix})\\n`;\n return { result, stdout, exitCode: 0 };\n}\n","import { join } from \"node:path\";\n\nimport type Database from \"better-sqlite3\";\n\nimport { parseDuration } from \"../indexer/duration.js\";\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { looksLikeDir, normalizePath } from \"../indexer/paths.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface SearchOptions {\n cwd: string;\n query?: string;\n topics: string[];\n mentions?: string;\n since?: string;\n stale?: string;\n orphan?: boolean;\n includeArchive?: boolean;\n archived?: boolean;\n wiki?: string;\n json?: boolean;\n limit?: number;\n}\n\nexport interface SearchResult {\n slug: string;\n title: string | null;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n topics: string[];\n}\n\nexport interface SearchCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac search` — the core query surface.\n *\n * Filters compose with AND logic. The implementation is deliberately\n * pedestrian: build a list of clauses + params, join them with `AND`,\n * intersect topic filters by requiring one subquery per `--topic`. No\n * clever query-planner tricks needed for the sizes we handle (<10k pages).\n *\n * All output ordering is stable: `updated_at DESC, slug ASC`. FTS5 rank\n * is layered on top when the user passed a text query — we ORDER BY rank\n * first, then fall through to the default.\n */\nexport async function runSearch(\n options: SearchOptions,\n): Promise<SearchCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const rows = executeQuery(db, options);\n const limited =\n options.limit !== undefined && options.limit >= 0\n ? rows.slice(0, options.limit)\n : rows;\n\n const stdout = formatResults(limited, options);\n const stderr = buildStderr(limited, options);\n return { stdout, stderr, exitCode: 0 };\n } finally {\n db.close();\n }\n}\n\ninterface PageRow {\n slug: string;\n title: string | null;\n updated_at: number;\n archived_at: number | null;\n superseded_by: string | null;\n}\n\nfunction executeQuery(\n db: Database.Database,\n options: SearchOptions,\n): SearchResult[] {\n const whereClauses: string[] = [];\n const params: (string | number)[] = [];\n\n // Archive scope. Three modes, mutually exclusive in practice:\n // - default → active only\n // - --include-archive → active + archived\n // - --archived → archived only\n // `--archived` wins over `--include-archive` when both are passed —\n // being explicit about \"only archived\" is strictly narrower than\n // \"include archived\", so intersecting them yields \"only archived\".\n if (options.archived === true) {\n whereClauses.push(\"p.archived_at IS NOT NULL\");\n } else if (options.includeArchive !== true) {\n whereClauses.push(\"p.archived_at IS NULL\");\n }\n\n // --topic foo --topic bar → page must have BOTH. We add one EXISTS\n // subquery per topic rather than grouping, which keeps param order\n // simple and the plan readable.\n for (const rawTopic of options.topics) {\n const topicSlug = slugForTopic(rawTopic);\n if (topicSlug.length === 0) continue;\n whereClauses.push(\n \"EXISTS (SELECT 1 FROM page_topics pt WHERE pt.page_slug = p.slug AND pt.topic_slug = ?)\",\n );\n params.push(topicSlug);\n }\n\n // --mentions: look for a file_refs row on this page that either\n // matches exactly, OR is a containing folder (is_dir=1 and the query\n // path starts with the row's path), OR the row itself lives inside\n // the queried folder. See spec → \"Graph querying → Query examples\".\n //\n // We deliberately avoid GLOB on the RHS of the comparison with stored\n // `r.path`, because stored paths can legitimately contain GLOB\n // metacharacters — Next.js dynamic routes like `src/[id]/page.tsx`\n // store a literal `[id]`, and `[abc]` is a SQL GLOB character class.\n // Concatenating `r.path || '*'` into a GLOB pattern would make\n // `src/[id]/page.tsx*` match `src/i/page.tsx` (spurious hit on the\n // character class).\n //\n // Instead we enumerate the prefix folders in JS and use parameterized\n // equality. For `src/checkout/handler.ts` the prefixes are\n // `['src/', 'src/checkout/']`; any file_refs row with is_dir=1 and\n // a path in that list is a containing folder of the queried file.\n // This also lets SQLite use `idx_file_refs_path` as an equality\n // probe rather than a range scan.\n if (options.mentions !== undefined && options.mentions.length > 0) {\n const isDir = looksLikeDir(options.mentions);\n const norm = normalizePath(options.mentions, isDir);\n if (isDir) {\n // Query is a folder. Match: the exact folder, OR any file/sub-\n // folder whose path starts with the folder prefix. The prefix\n // match is the one place we still need GLOB — but we escape any\n // wildcard metacharacters in `norm` first so a user-supplied\n // `src/[id]/` query is treated as a literal. Note: the query\n // path comes from the caller, not from stored data, but a user\n // typing `--mentions src/[id]/` should get the literal folder,\n // not a character class.\n const escaped = escapeGlobMeta(norm);\n whereClauses.push(\n `EXISTS (\n SELECT 1 FROM file_refs r\n WHERE r.page_slug = p.slug\n AND (r.path = ? OR r.path GLOB ?)\n )`,\n );\n params.push(norm, `${escaped}*`);\n } else {\n // Query is a file. Match: the exact file, OR any folder whose\n // path is a prefix of this file. Build the prefix list in JS and\n // probe file_refs with equality — no GLOB on stored values.\n const prefixes = parentFolderPrefixes(norm);\n if (prefixes.length === 0) {\n whereClauses.push(\n `EXISTS (\n SELECT 1 FROM file_refs r\n WHERE r.page_slug = p.slug AND r.path = ?\n )`,\n );\n params.push(norm);\n } else {\n const placeholders = prefixes.map(() => \"?\").join(\", \");\n whereClauses.push(\n `EXISTS (\n SELECT 1 FROM file_refs r\n WHERE r.page_slug = p.slug\n AND (\n r.path = ?\n OR (r.is_dir = 1 AND r.path IN (${placeholders}))\n )\n )`,\n );\n params.push(norm, ...prefixes);\n }\n }\n }\n\n const now = Math.floor(Date.now() / 1000);\n\n if (options.since !== undefined) {\n const seconds = parseDuration(options.since);\n whereClauses.push(\"p.updated_at >= ?\");\n params.push(now - seconds);\n }\n\n if (options.stale !== undefined) {\n const seconds = parseDuration(options.stale);\n whereClauses.push(\"p.updated_at < ?\");\n params.push(now - seconds);\n }\n\n if (options.orphan === true) {\n whereClauses.push(\n \"NOT EXISTS (SELECT 1 FROM page_topics pt WHERE pt.page_slug = p.slug)\",\n );\n }\n\n // FTS5 text query. When a text query is supplied we JOIN against\n // `fts_pages` so we can ORDER BY its `rank` column — lower (more\n // negative) ranks are better matches. Without a query we skip the\n // join entirely.\n let sql: string;\n if (options.query !== undefined && options.query.trim().length > 0) {\n const ftsExpr = buildFtsQuery(options.query);\n sql = `\n SELECT p.slug, p.title, p.updated_at, p.archived_at, p.superseded_by\n FROM pages p\n JOIN fts_pages f ON f.slug = p.slug\n WHERE fts_pages MATCH ?\n ${whereClauses.length > 0 ? `AND ${whereClauses.join(\" AND \")}` : \"\"}\n ORDER BY f.rank ASC, p.updated_at DESC, p.slug ASC\n `;\n // MATCH param goes first (it's the first `?` in the compiled SQL).\n params.unshift(ftsExpr);\n } else {\n sql = buildSql(whereClauses);\n }\n\n const rows = db.prepare<unknown[], PageRow>(sql).all(...params);\n\n // Attach topics in a second pass — simpler than a correlated\n // `GROUP_CONCAT`, and the output rows are small enough that N+1 on a\n // single prepared statement is fine.\n const topicStmt = db.prepare<[string], { topic_slug: string }>(\n \"SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug\",\n );\n const out: SearchResult[] = rows.map((row) => ({\n slug: row.slug,\n title: row.title,\n updated_at: row.updated_at,\n archived_at: row.archived_at,\n superseded_by: row.superseded_by,\n topics: topicStmt.all(row.slug).map((t) => t.topic_slug),\n }));\n\n return out;\n}\n\nfunction buildSql(whereClauses: string[]): string {\n const where =\n whereClauses.length > 0 ? `WHERE ${whereClauses.join(\" AND \")}` : \"\";\n return `\n SELECT p.slug, p.title, p.updated_at, p.archived_at, p.superseded_by\n FROM pages p\n ${where}\n ORDER BY p.updated_at DESC, p.slug ASC\n `;\n}\n\n/**\n * Turn a user query into an FTS5 MATCH expression. FTS5's default\n * grammar gets unhappy with punctuation (hyphens, colons, slashes), so\n * we tokenize into alphanumeric runs and emit a conjunction of prefixed\n * tokens. Each token is suffixed with `*` so \"stri\" matches \"stripe\".\n *\n * Quoted input (`\"stripe webhook\"`) is treated as an FTS5 phrase query\n * — tokens must appear contiguously in that order. This matches shell\n * conventions where quoting something means \"match it literally\". We\n * strip the surrounding quotes, collapse inner punctuation to spaces,\n * and re-wrap in quotes for FTS5's phrase syntax. Any embedded `\"` in\n * the user input is dropped (FTS5 phrase syntax has no escape).\n *\n * Anything that tokenizes to empty (e.g. pure punctuation) falls back\n * to an empty MATCH, which yields no rows — which is the right answer.\n */\nfunction buildFtsQuery(raw: string): string {\n const trimmed = raw.trim();\n if (\n trimmed.length >= 2 &&\n trimmed.startsWith(\"\\\"\") &&\n trimmed.endsWith(\"\\\"\")\n ) {\n // Phrase mode. Strip outer quotes, lowercase, collapse non-alnum\n // runs to a single space, trim. Any surviving inner `\"` are\n // removed since FTS5 phrase syntax has no escape mechanism.\n const inner = trimmed\n .slice(1, -1)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \")\n .trim();\n if (inner.length === 0) return \"\\\"\\\"\";\n return `\"${inner}\"`;\n }\n const tokens = trimmed\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 0);\n if (tokens.length === 0) return \"\\\"\\\"\";\n return tokens.map((t) => `${t}*`).join(\" AND \");\n}\n\nfunction slugForTopic(raw: string): string {\n return raw\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * For a normalized file path like `src/checkout/handler.ts`, enumerate\n * every containing folder in the form those folders are stored (trailing\n * slash): `['src/', 'src/checkout/']`. Empty for paths with no folder\n * separator (e.g. `README.md`) — a top-level file has no parent folder\n * recorded in `file_refs`.\n *\n * Used by the `--mentions <file>` path to let us probe `file_refs` with\n * equality instead of GLOB, sidestepping wildcard-escape bugs entirely.\n */\nfunction parentFolderPrefixes(filePath: string): string[] {\n const out: string[] = [];\n let cursor = 0;\n while (true) {\n const next = filePath.indexOf(\"/\", cursor);\n if (next === -1) break;\n // Slice includes the slash — matches how directories are stored.\n out.push(filePath.slice(0, next + 1));\n cursor = next + 1;\n }\n return out;\n}\n\n/**\n * Escape SQLite GLOB metacharacters (`*`, `?`, `[`) by wrapping each in\n * a single-character class. SQLite GLOB has no backslash-escape, so the\n * idiomatic trick is `[*]` → literal `*`, `[?]` → literal `?`, `[[]` →\n * literal `[`. `]` doesn't need escaping outside a class.\n *\n * We only need this on the *query* side — stored paths aren't\n * concatenated into GLOB patterns anymore (see --mentions handling).\n * But a user-typed `--mentions src/[id]/` should still match a stored\n * `src/[id]/` literally, not as a character class over `i` or `d`.\n */\nfunction escapeGlobMeta(s: string): string {\n return s.replace(/[\\*\\?\\[]/g, (ch) => `[${ch}]`);\n}\n\nfunction formatResults(\n rows: SearchResult[],\n options: SearchOptions,\n): string {\n if (options.json === true) {\n return `${JSON.stringify(rows, null, 2)}\\n`;\n }\n // Default output: one slug per line. Empty result = empty output (not\n // \"no results found\") — makes piping into xargs / subsequent commands\n // degrade gracefully.\n if (rows.length === 0) return \"\";\n return `${rows.map((r) => r.slug).join(\"\\n\")}\\n`;\n}\n\nfunction buildStderr(rows: SearchResult[], options: SearchOptions): string {\n // Spec: \"print warns if >50 when not --json\". The warning goes to\n // stderr so it doesn't corrupt pipelines that filter stdout.\n if (options.json === true) return \"\";\n if (options.limit !== undefined) return \"\";\n if (rows.length > 50) {\n return `almanac: ${rows.length} results — consider --limit or a narrower query\\n`;\n }\n return \"\";\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\n\nexport interface ShowOptions {\n cwd: string;\n slug?: string;\n stdin?: boolean;\n stdinInput?: string; // injected by the CLI when --stdin is set; tests can pass directly\n wiki?: string;\n}\n\nexport interface ShowCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * `almanac show <slug>` — cat the markdown file for a page.\n *\n * This is the one command that is allowed to read page file contents;\n * everything else operates on the index (per the spec's design\n * principles).\n *\n * Output shapes:\n * - positional slug → raw markdown on stdout, unchanged (the cat case)\n * - `--stdin` → JSON Lines: one `{slug, content}` object per\n * successfully-read page, terminated by `\\n`. JSON\n * Lines is parseable, unlike the old `\\n---\\n`\n * separator which collided with YAML frontmatter\n * delimiters in the page bodies themselves.\n *\n * Unresolvable slugs go to stderr with a non-zero exit; we still print\n * whatever we could resolve on stdout so bulk runs don't have to fail\n * atomically.\n */\nexport async function runShow(\n options: ShowOptions,\n): Promise<ShowCommandOutput> {\n const repoRoot = await resolveWikiRoot({\n cwd: options.cwd,\n wiki: options.wiki,\n });\n await ensureFreshIndex({ repoRoot });\n\n const dbPath = join(repoRoot, \".almanac\", \"index.db\");\n const db = openIndex(dbPath);\n\n try {\n const slugs = collectSlugs(options);\n if (slugs.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: show requires a slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n const stmt = db.prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n );\n\n const records: Array<{ slug: string; content: string }> = [];\n const missing: string[] = [];\n for (const slug of slugs) {\n const row = stmt.get(slug);\n if (row === undefined) {\n missing.push(slug);\n continue;\n }\n try {\n records.push({ slug, content: await readFile(row.file_path, \"utf8\") });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n missing.push(`${slug} (${message})`);\n }\n }\n\n const bulk = options.stdin === true;\n let stdout: string;\n if (bulk) {\n // JSON Lines: one object per line, trailing newline on the last.\n // Consumers can split on `\\n` and `JSON.parse` each line.\n stdout = records\n .map((r) => JSON.stringify(r))\n .join(\"\\n\");\n if (stdout.length > 0) stdout += \"\\n\";\n } else {\n // Positional single-slug: just emit the raw markdown. No\n // separator, no wrapping — this is the \"cat\" case.\n stdout = records.map((r) => r.content).join(\"\");\n }\n const stderr = missing\n .map((s) => `almanac: no such page \"${s}\"\\n`)\n .join(\"\");\n return {\n stdout,\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n } finally {\n db.close();\n }\n}\n\nfunction collectSlugs(options: ShowOptions): string[] {\n if (options.stdin === true && options.stdinInput !== undefined) {\n return options.stdinInput\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (options.slug !== undefined && options.slug.length > 0) {\n return [options.slug];\n }\n return [];\n}\n","import { readFile, rename, writeFile } from \"node:fs/promises\";\n\nimport yaml from \"js-yaml\";\n\n/**\n * Rewrite the `topics:` field in a markdown file's YAML frontmatter.\n *\n * The absolute requirement is **body byte-preservation**. Tag/untag/\n * rename commands touch only the frontmatter; everything after the\n * closing `---` must be byte-identical to the input (same line endings,\n * same trailing whitespace, same final-newline-or-not). We lean on a\n * precise split rather than re-serializing the whole file so any\n * incidental bytes in the body (literal `\\r\\n`, unusual trailing\n * whitespace, triple-hyphen rulers) are left alone.\n *\n * For the frontmatter itself we do a **surgical rewrite of the `topics:`\n * field only** — not a full YAML roundtrip. Reasons:\n * - `js-yaml` normalizes quoting, re-orders keys alphabetically by\n * default, and can drop comments. Any of those would surprise a user\n * who hand-edited their own frontmatter and expects their formatting\n * preserved byte-for-byte.\n * - We only care about one field. Replacing just that field lets the\n * rest of the YAML survive verbatim.\n *\n * The strategy:\n * 1. Find the exact span of the `topics:` key (whether flow\n * `topics: [a, b]` or block `topics:\\n - a\\n - b\\n`) using a\n * small line scanner.\n * 2. Compute the new topics list from the caller's transform.\n * 3. Replace the span with a freshly-emitted `topics: [a, b, c]` line\n * (flow style — compact, readable, and what most authors write).\n * 4. If no `topics:` key exists and the new list is non-empty, append\n * a line to the end of the frontmatter block.\n * 5. If `topics:` exists but the new list is empty, drop the line\n * entirely rather than leaving `topics: []` around.\n *\n * The transform function gets the current deduplicated topic list and\n * returns the new list. Returning an empty array means \"remove the\n * `topics:` key entirely\".\n */\n\nexport interface RewriteResult {\n /** The page's topics before the rewrite (possibly empty). */\n before: string[];\n /** The page's topics after the rewrite (possibly empty). */\n after: string[];\n /** True iff the file content actually changed. */\n changed: boolean;\n}\n\n/**\n * Read `filePath`, compute the new topics via `transform`, and\n * atomically rewrite if the result differs.\n *\n * Atomic per file: write to `<path>.tmp` then rename, same pattern as\n * the registry and topics.yaml writers. A half-written page would\n * corrupt committed user content, so this is non-negotiable.\n */\nexport async function rewritePageTopics(\n filePath: string,\n transform: (current: string[]) => string[],\n): Promise<RewriteResult> {\n const raw = await readFile(filePath, \"utf8\");\n const { before, after, output, changed } = applyTopicsTransform(\n raw,\n transform,\n );\n if (changed) {\n const tmp = `${filePath}.tmp`;\n await writeFile(tmp, output, \"utf8\");\n await rename(tmp, filePath);\n }\n return { before, after, changed };\n}\n\ninterface TransformApplied {\n before: string[];\n after: string[];\n output: string;\n changed: boolean;\n}\n\n/**\n * Pure-string version of `rewritePageTopics`. Useful for tests and for\n * the few places (rename, delete) where we loop many files and want to\n * short-circuit no-op writes cheaply.\n */\nexport function applyTopicsTransform(\n raw: string,\n transform: (current: string[]) => string[],\n): TransformApplied {\n const parsed = splitFrontmatter(raw);\n if (parsed === null) {\n // No frontmatter at all. Tagging a topic on such a page means\n // creating a frontmatter block. We keep the body untouched and\n // prepend `---\\ntopics: [...]\\n---\\n\\n`. If the transform yields\n // an empty list, this is a no-op. Line endings: default to LF for a\n // brand-new frontmatter — we can't infer intent from a file that\n // doesn't have frontmatter yet, and LF is the committed default in\n // most modern repos.\n const next = dedupeSlugs(transform([]));\n if (next.length === 0) {\n return { before: [], after: [], output: raw, changed: false };\n }\n const fm = `---\\ntopics: ${flowList(next)}\\n---\\n\\n`;\n return {\n before: [],\n after: next,\n output: `${fm}${raw}`,\n changed: true,\n };\n }\n\n const { opener, fmLines, closer, body, eol } = parsed;\n const { before, existingRange } = readTopicsFromLines(fmLines);\n const beforeDeduped = dedupeSlugs(before);\n const after = dedupeSlugs(transform(beforeDeduped));\n\n if (arraysEqual(beforeDeduped, after)) {\n return { before: beforeDeduped, after, output: raw, changed: false };\n }\n\n let nextFmLines: string[];\n if (existingRange === null) {\n // No `topics:` key currently present. Add one (only if non-empty).\n if (after.length === 0) {\n return { before: beforeDeduped, after, output: raw, changed: false };\n }\n nextFmLines = [...fmLines, `topics: ${flowList(after)}`];\n } else {\n const replacement =\n after.length === 0 ? null : `topics: ${flowList(after)}`;\n // Interleaved comments/blank lines from a block-style list are\n // re-emitted BELOW the new flow-style `topics:` line so the\n // author's commentary sticks around. Flow/scalar inputs produce an\n // empty `preserved` array, so this collapses to the old behavior\n // for the common case. When we fully delete the key (empty after)\n // the preserved lines go too — without a `topics:` key to anchor\n // them to, trailing \"# below the topics list\" comments become\n // orphans that no longer mean what they said.\n const preservedTail =\n replacement === null ? [] : existingRange.preserved;\n nextFmLines = [\n ...fmLines.slice(0, existingRange.start),\n ...(replacement === null ? [] : [replacement]),\n ...preservedTail,\n ...fmLines.slice(existingRange.end),\n ];\n }\n\n // Rejoin with the same line ending the input frontmatter used so a\n // CRLF-authored file comes out CRLF end-to-end. `splitFrontmatter`\n // sniffed the dominant separator for us.\n const fmBlock =\n nextFmLines.length === 0 ? \"\" : `${nextFmLines.join(eol)}${eol}`;\n const output = `${opener}${fmBlock}${closer}${body}`;\n return {\n before: beforeDeduped,\n after,\n output,\n changed: true,\n };\n}\n\ninterface SplitFrontmatter {\n /** The opening `---\\n` or `---\\r\\n`. */\n opener: string;\n /** Frontmatter lines (no line-ending character included). */\n fmLines: string[];\n /** The closing `---\\n` (or `---\\r\\n`, possibly without trailing newline if EOF). */\n closer: string;\n /** Everything after the closing fence, byte-for-byte. */\n body: string;\n /**\n * Dominant line ending inside the frontmatter block. CRLF-authored\n * files stay CRLF on write; LF stays LF. We sniff once at split time\n * so rewriting doesn't have to re-inspect every line.\n */\n eol: \"\\n\" | \"\\r\\n\";\n}\n\n/**\n * Split a file into (opener, frontmatter lines, closer, body). The\n * regex mirrors `parseFrontmatter` in `indexer/frontmatter.ts` so the\n * indexer and the rewriter agree on what \"has frontmatter\" means.\n *\n * Returns `null` when the file doesn't start with a `---` fence or\n * lacks a matching closer — both cases are legal (a page with only\n * body content) and the caller treats them as \"no frontmatter to\n * rewrite\".\n */\nfunction splitFrontmatter(raw: string): SplitFrontmatter | null {\n if (!raw.startsWith(\"---\")) return null;\n // Match the exact opener (with its line ending) so we can preserve\n // it byte-for-byte.\n const openerMatch = raw.match(/^---(\\r?\\n)/);\n if (openerMatch === null) return null;\n const opener = `---${openerMatch[1] ?? \"\\n\"}`;\n const rest = raw.slice(opener.length);\n // Find the closing `---` that begins at the start of a line. We\n // also handle the edge case where the closer sits at position 0 of\n // `rest` (frontmatter was empty).\n let fenceIdx: number;\n if (rest.startsWith(\"---\")) {\n fenceIdx = 0;\n } else {\n const m = rest.match(/\\r?\\n---(\\r?\\n|$)/);\n if (m === null || m.index === undefined) return null;\n // `m.index` points at the `\\r?\\n` before `---`; skip that newline\n // so fenceIdx lands exactly on the `-`.\n const leadingNewlineLen = (m[0] ?? \"\").startsWith(\"\\r\\n\") ? 2 : 1;\n fenceIdx = m.index + leadingNewlineLen;\n }\n const fmBlock = rest.slice(0, fenceIdx);\n // Determine the closer's full span, including its trailing newline if any.\n const afterDashes = rest.slice(fenceIdx + 3);\n let closerTail = \"\";\n if (afterDashes.startsWith(\"\\r\\n\")) {\n closerTail = \"\\r\\n\";\n } else if (afterDashes.startsWith(\"\\n\")) {\n closerTail = \"\\n\";\n }\n const closer = `---${closerTail}`;\n const body = afterDashes.slice(closerTail.length);\n const fmLines =\n fmBlock.length === 0 ? [] : fmBlock.replace(/\\r?\\n$/, \"\").split(/\\r?\\n/);\n // Sniff the frontmatter's dominant line ending. We look at the\n // opener first (most reliable signal — it's always present and\n // always has an ending). Fall back to checking the fmBlock for any\n // `\\r\\n` runs so a frontmatter with a single-line opener and\n // multi-line body still gets classified right.\n const eol: \"\\n\" | \"\\r\\n\" =\n opener.endsWith(\"\\r\\n\") || /\\r\\n/.test(fmBlock) ? \"\\r\\n\" : \"\\n\";\n return { opener, fmLines, closer, body, eol };\n}\n\ninterface ExistingRange {\n /** Index in `fmLines` of the `topics:` key line (inclusive). */\n start: number;\n /** Index in `fmLines` one past the last line belonging to this key. */\n end: number;\n /**\n * Lines inside `[start+1, end)` that aren't `- entry` lines — i.e.\n * interleaved comments and blank lines a user wrote between entries.\n * We preserve these verbatim when rewriting block-style lists to\n * flow; otherwise a `tag` on a commented list would silently drop\n * the commentary. Empty for flow/scalar shapes.\n */\n preserved: string[];\n}\n\n/**\n * Find `topics:` in a frontmatter-lines array and read the values.\n *\n * Handles three YAML shapes authors commonly write:\n * - `topics: [a, b, c]` (flow sequence, one line)\n * - `topics:` followed by block entries like ` - a` (block sequence)\n * - `topics: a` (a single scalar — treated as one element)\n *\n * Also handles the empty case `topics:` with nothing after it, and the\n * \"no topics key\" case (returns `existingRange: null`).\n *\n * This is NOT a general YAML parser — it's intentionally scoped to the\n * one key we mutate, because using `js-yaml` for a round-trip would\n * lose comments and re-quote strings the user picked a specific way.\n */\nfunction readTopicsFromLines(fmLines: string[]): {\n before: string[];\n existingRange: ExistingRange | null;\n} {\n const keyLineIdx = findTopKey(fmLines, \"topics\");\n if (keyLineIdx === -1) {\n return { before: [], existingRange: null };\n }\n const keyLine = fmLines[keyLineIdx] ?? \"\";\n const colonIdx = keyLine.indexOf(\":\");\n // Everything to the right of the first colon, trimmed.\n const after = keyLine.slice(colonIdx + 1).trim();\n // Strip trailing `# ...` line-comment from a flow value so we don't\n // parse comments as list contents. (A block list's sub-items have\n // their own comments stripped in the block branch below.)\n const afterNoComment = stripTrailingComment(after);\n\n if (afterNoComment.length === 0) {\n // Block sequence style: collect subsequent `- item` lines. Between\n // entries a user may have written:\n // - interleaved `# comment` lines\n // - blank lines\n // We must NOT break the scan on those — doing so would drop every\n // entry after the first comment/blank when we rewrite (silent data\n // loss: the original bug that triggered this fix). We skip them in\n // the scan and stash them in `preserved` so the replacement step\n // can re-emit them verbatim BETWEEN the new flow-style line and\n // the rest of the frontmatter.\n //\n // Edge: comments/blanks that appear BEFORE the first `- entry` or\n // AFTER the last `- entry` count as part of the block too — pulling\n // them out keeps the author's commentary near the list it belongs\n // to. We cap the scan when we hit a real non-entry line (e.g. the\n // next top-level key), leaving everything from that line onward\n // outside the range.\n const values: string[] = [];\n const preserved: string[] = [];\n // Provisional scan cursor. `endIdx` only advances when we've seen\n // something we're sure belongs to this block (an entry line), so\n // trailing whitespace/comments that don't precede another entry\n // stay OUTSIDE the range and aren't shuffled on rewrite.\n let i = keyLineIdx + 1;\n let endIdx = i;\n // `pendingNonEntries` holds comments/blanks we've seen since the\n // last confirmed entry. They're committed to `preserved` only\n // when a subsequent `- entry` proves they live mid-list.\n let pendingNonEntries: string[] = [];\n while (i < fmLines.length) {\n const line = fmLines[i] ?? \"\";\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed.startsWith(\"#\")) {\n pendingNonEntries.push(line);\n i += 1;\n continue;\n }\n const m = line.match(/^\\s*-\\s+(.*)$/);\n if (m === null) break;\n // Promote any pending comments/blanks — they're between entries\n // (or before the first entry within the block).\n if (pendingNonEntries.length > 0) {\n preserved.push(...pendingNonEntries);\n pendingNonEntries = [];\n }\n const raw = stripTrailingComment((m[1] ?? \"\").trim());\n const parsed = parseScalar(raw);\n if (parsed.length > 0) values.push(parsed);\n i += 1;\n endIdx = i;\n }\n return {\n before: values,\n existingRange: { start: keyLineIdx, end: endIdx, preserved },\n };\n }\n\n // Flow / scalar shape on one line. Let js-yaml handle the value-parsing\n // (quoting, escapes, etc.) for the RHS only.\n let parsed: unknown;\n try {\n parsed = yaml.load(afterNoComment);\n } catch {\n parsed = null;\n }\n const values: string[] = [];\n if (Array.isArray(parsed)) {\n for (const v of parsed) {\n if (typeof v === \"string\" && v.trim().length > 0) {\n values.push(v.trim());\n }\n }\n } else if (typeof parsed === \"string\" && parsed.trim().length > 0) {\n values.push(parsed.trim());\n }\n return {\n before: values,\n existingRange: { start: keyLineIdx, end: keyLineIdx + 1, preserved: [] },\n };\n}\n\n/**\n * Find a top-level key line. \"Top-level\" means no leading whitespace —\n * we don't walk into nested mappings. The indexer's frontmatter parser\n * only reads top-level keys too, so this matches.\n */\nfunction findTopKey(fmLines: string[], key: string): number {\n const re = new RegExp(`^${escapeRegex(key)}\\\\s*:`);\n for (let i = 0; i < fmLines.length; i += 1) {\n if (re.test(fmLines[i] ?? \"\")) return i;\n }\n return -1;\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction stripTrailingComment(s: string): string {\n // Only strip `#` outside of quotes. For the shapes we handle —\n // slug-like kebab-case topics — quoted strings with `#` are rare, but\n // be defensive.\n let inSingle = false;\n let inDouble = false;\n for (let i = 0; i < s.length; i += 1) {\n const ch = s[i];\n if (ch === \"'\" && !inDouble) inSingle = !inSingle;\n else if (ch === '\"' && !inSingle) inDouble = !inDouble;\n else if (ch === \"#\" && !inSingle && !inDouble) {\n return s.slice(0, i).trimEnd();\n }\n }\n return s;\n}\n\n/**\n * Strip YAML quoting from a scalar. Block-sequence items might be\n * written as `- 'foo'` or `- \"foo\"` or bare `- foo`; we accept all\n * three and return the plain string.\n */\nfunction parseScalar(s: string): string {\n if (s.length === 0) return s;\n if (s.length >= 2 && s[0] === '\"' && s[s.length - 1] === '\"') {\n return s.slice(1, -1);\n }\n if (s.length >= 2 && s[0] === \"'\" && s[s.length - 1] === \"'\") {\n return s.slice(1, -1);\n }\n return s;\n}\n\n/**\n * Emit a flow-style YAML sequence like `[auth, jwt, security]`. We use\n * flow because it's the shape most authors write by hand and stays on\n * one line, which keeps diffs tight. Values are quoted only when\n * necessary — plain kebab-case slugs never need quoting.\n */\nfunction flowList(items: string[]): string {\n return `[${items.map((t) => formatScalar(t)).join(\", \")}]`;\n}\n\nfunction formatScalar(s: string): string {\n // If it's a bare kebab/alnum slug, no quotes. Otherwise fall back to\n // js-yaml for correct escaping. We check against a conservative\n // pattern — anything outside it gets YAML-quoted.\n if (/^[a-z0-9][a-z0-9-]*$/.test(s)) return s;\n return yaml\n .dump(s, { flowLevel: 0, lineWidth: Number.MAX_SAFE_INTEGER })\n .trimEnd();\n}\n\nfunction dedupeSlugs(list: string[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of list) {\n const s = raw.trim();\n if (s.length === 0) continue;\n if (seen.has(s)) continue;\n seen.add(s);\n out.push(s);\n }\n return out;\n}\n\nfunction arraysEqual(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n","import { join } from \"node:path\";\n\n/**\n * `.almanac/topics.yaml` inside a given repo root. Single helper so no\n * caller has to remember where the file lives.\n */\nexport function topicsYamlPath(repoRoot: string): string {\n return join(repoRoot, \".almanac\", \"topics.yaml\");\n}\n\n/**\n * `.almanac/index.db` inside a given repo root. Mirrors `topicsYamlPath`\n * so the topics commands don't have to import from scattered places.\n */\nexport function indexDbPath(repoRoot: string): string {\n return join(repoRoot, \".almanac\", \"index.db\");\n}\n","import { ensureFreshIndex, runIndexer } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport { rewritePageTopics } from \"../topics/frontmatterRewrite.js\";\nimport { indexDbPath, topicsYamlPath } from \"../topics/paths.js\";\nimport {\n ensureTopic,\n loadTopicsFile,\n writeTopicsFile,\n} from \"../topics/yaml.js\";\n\n/**\n * `almanac tag <page> <topic>...` and `almanac untag <page> <topic>`.\n *\n * These are the page-side of the topics system — `topics ...` manages\n * the DAG and metadata; `tag`/`untag` wires concrete pages into\n * topics. Both commands mutate page frontmatter atomically per file\n * and leave body bytes untouched.\n *\n * Auto-creation policy: if a topic passed to `tag` doesn't yet exist\n * in `topics.yaml`, we create a minimal entry for it (title-cased\n * title, no description, no parents). This matches the spec: \"Ensure\n * topic exists in topics.yaml; if not, create a minimal entry.\" We\n * don't silently create topics on `untag` — you can only untag\n * something that was already a topic.\n */\n\nexport interface TagCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport interface TagOptions {\n cwd: string;\n wiki?: string;\n page?: string;\n topics: string[];\n stdin?: boolean;\n stdinInput?: string;\n}\n\nexport interface UntagOptions {\n cwd: string;\n wiki?: string;\n page: string;\n topic: string;\n}\n\nexport async function runTag(options: TagOptions): Promise<TagCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n\n const topics = options.topics\n .map((t) => toKebabCase(t))\n .filter((t) => t.length > 0);\n if (topics.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: tag requires at least one topic\\n\",\n exitCode: 1,\n };\n }\n\n // Bulk mode reads slugs from stdin; single mode uses the positional.\n const pages: string[] = [];\n if (options.stdin === true) {\n if (options.stdinInput === undefined) {\n return {\n stdout: \"\",\n stderr: \"almanac: tag --stdin called without stdin input\\n\",\n exitCode: 1,\n };\n }\n for (const line of options.stdinInput.split(/\\r?\\n/)) {\n const s = line.trim();\n if (s.length > 0) pages.push(s);\n }\n } else if (options.page !== undefined && options.page.length > 0) {\n pages.push(options.page);\n } else {\n return {\n stdout: \"\",\n stderr: \"almanac: tag requires a page slug (or --stdin)\\n\",\n exitCode: 1,\n };\n }\n\n // Resolve slugs to file paths from the DB. A stale index is fine for\n // `tag` — we just need to find each page's file; `ensureFreshIndex`\n // runs first so the common path is consistent.\n await ensureFreshIndex({ repoRoot });\n const db = openIndex(indexDbPath(repoRoot));\n\n // Validate every requested page exists BEFORE touching topics.yaml.\n // Previously we auto-created topics first, which meant\n // `almanac tag does-not-exist brand-new` left `brand-new` in\n // topics.yaml as a state leak even though the tag itself errored. We\n // resolve rows up front, then short-circuit with an error (and no\n // mutations) if none of the pages are valid.\n const stmt = db.prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n );\n const resolved: { page: string; filePath: string }[] = [];\n const missing: string[] = [];\n try {\n for (const page of pages) {\n const row = stmt.get(toKebabCase(page));\n if (row === undefined) {\n missing.push(page);\n } else {\n resolved.push({ page, filePath: row.file_path });\n }\n }\n } finally {\n db.close();\n }\n\n // Hard-fail when NO page resolved. We deliberately don't mutate\n // topics.yaml on this path — the user's intent (tag page X) is\n // inarguably unsatisfiable, so we shouldn't leave breadcrumbs.\n //\n // In bulk mode (`--stdin`) some pages might resolve and others\n // won't; keeping the original partial-progress behavior for that case\n // (topics get created, resolved pages get tagged, `missing` are\n // reported on stderr with exitCode 1). The state leak only matters\n // when NOTHING succeeds, and that's the case we're fixing.\n if (resolved.length === 0) {\n const stderr = missing.map((p) => `almanac: no such page \"${p}\"\\n`).join(\"\");\n return {\n stdout: \"\",\n stderr,\n exitCode: 1,\n };\n }\n\n // Auto-create missing topics in topics.yaml. Safe to do now — we have\n // at least one page that will actually end up tagged with them.\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n let fileChanged = false;\n for (const t of topics) {\n // ensureTopic mutates the file; we check presence beforehand so\n // we only write when something actually changes (skip a redundant\n // atomic rewrite + mtime bump).\n const before = file.topics.length;\n ensureTopic(file, t);\n if (file.topics.length > before) fileChanged = true;\n }\n if (fileChanged) {\n await writeTopicsFile(yamlPath, file);\n }\n\n const summary: string[] = [];\n let taggedPages = 0;\n for (const { page, filePath } of resolved) {\n const result = await rewritePageTopics(filePath, (current) => {\n // Preserve existing order; append new topics in the order\n // the caller supplied them. `applyTopicsTransform` will\n // dedupe for us, but we skip redundant work here too.\n const out = [...current];\n for (const t of topics) if (!current.includes(t)) out.push(t);\n return out;\n });\n if (result.changed) {\n taggedPages += 1;\n // Only surface the NEWLY ADDED topics — not the full request.\n // Reporting every requested topic (including ones the page\n // already had) reads like false positives in commit diffs.\n const added = result.after.filter((t) => !result.before.includes(t));\n summary.push(`tagged ${page}: ${added.join(\", \")}`);\n } else {\n summary.push(\n `no change ${page} (already tagged with ${topics.join(\", \")})`,\n );\n }\n }\n\n if (taggedPages > 0 || fileChanged) {\n // Trigger a reindex so downstream queries see the new rows\n // immediately. Writes to page files bumped their mtimes; writes to\n // topics.yaml are caught by `topicsYamlNewerThan`.\n await runIndexer({ repoRoot });\n }\n\n const stderr = missing.map((p) => `almanac: no such page \"${p}\"\\n`).join(\"\");\n return {\n stdout: summary.length > 0 ? `${summary.join(\"\\n\")}\\n` : \"\",\n stderr,\n exitCode: missing.length > 0 ? 1 : 0,\n };\n}\n\nexport async function runUntag(\n options: UntagOptions,\n): Promise<TagCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const page = toKebabCase(options.page);\n const topic = toKebabCase(options.topic);\n if (page.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: untag requires a page slug\\n\",\n exitCode: 1,\n };\n }\n if (topic.length === 0) {\n return {\n stdout: \"\",\n stderr: \"almanac: untag requires a topic\\n\",\n exitCode: 1,\n };\n }\n\n await ensureFreshIndex({ repoRoot });\n const db = openIndex(indexDbPath(repoRoot));\n let filePath: string;\n try {\n const row = db\n .prepare<[string], { file_path: string }>(\n \"SELECT file_path FROM pages WHERE slug = ?\",\n )\n .get(page);\n if (row === undefined) {\n return {\n stdout: \"\",\n stderr: `almanac: no such page \"${page}\"\\n`,\n exitCode: 1,\n };\n }\n filePath = row.file_path;\n } finally {\n db.close();\n }\n\n const result = await rewritePageTopics(filePath, (current) =>\n current.filter((t) => t !== topic),\n );\n if (result.changed) {\n await runIndexer({ repoRoot });\n }\n\n return {\n stdout: result.changed\n ? `untagged ${page}: ${topic}\\n`\n : `no change ${page} (not tagged with ${topic})\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { ensureFreshIndex, runIndexer } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolveWiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport { ancestorsInFile, descendantsInDb } from \"../topics/dag.js\";\nimport {\n applyTopicsTransform,\n rewritePageTopics,\n} from \"../topics/frontmatterRewrite.js\";\nimport { indexDbPath, topicsYamlPath } from \"../topics/paths.js\";\nimport {\n ensureTopic,\n findTopic,\n loadTopicsFile,\n titleCase,\n writeTopicsFile,\n type TopicEntry,\n type TopicsFile,\n} from \"../topics/yaml.js\";\n\n/**\n * All `almanac topics <verb>` logic lives here. The CLI dispatches on\n * `verb` and forwards positionals/flags. One module per top-level\n * command group matches the pattern used by `search`, `info`, etc.\n *\n * Design notes:\n * - The module is stateless; every entry function takes a `cwd` and\n * optional `wiki` and is safe to call many times in a test suite.\n * - Mutations go file → DB: write `.almanac/topics.yaml` atomically,\n * then trigger a reindex so the DB reflects the new state by the\n * time the command prints its summary. Reads run after a cheap\n * `ensureFreshIndex` so they always see the latest committed data.\n * - The command functions return a `TopicsCommandOutput`; the CLI\n * layer in `cli.ts` decides how to print. This mirrors the shape of\n * `runSearch`, `runInfo`, and friends.\n */\n\nexport interface TopicsCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport interface TopicsBaseOptions {\n cwd: string;\n wiki?: string;\n json?: boolean;\n}\n\nexport interface TopicsListOptions extends TopicsBaseOptions {}\n\nexport interface TopicsShowOptions extends TopicsBaseOptions {\n slug: string;\n descendants?: boolean;\n}\n\nexport interface TopicsCreateOptions extends TopicsBaseOptions {\n name: string;\n parents?: string[];\n}\n\nexport interface TopicsLinkOptions extends TopicsBaseOptions {\n child: string;\n parent: string;\n}\n\nexport interface TopicsUnlinkOptions extends TopicsLinkOptions {}\n\nexport interface TopicsRenameOptions extends TopicsBaseOptions {\n oldSlug: string;\n newSlug: string;\n}\n\nexport interface TopicsDeleteOptions extends TopicsBaseOptions {\n slug: string;\n}\n\nexport interface TopicsDescribeOptions extends TopicsBaseOptions {\n slug: string;\n description: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// list — `almanac topics`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics` (and `almanac topics list`). Prints one line per\n * known topic — from the DB, which already unions topics.yaml with any\n * ad-hoc slugs found in page frontmatter. Page counts come straight\n * from `page_topics`, which the indexer rebuilt on entry.\n */\nexport async function runTopicsList(\n options: TopicsListOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n await ensureFreshIndex({ repoRoot });\n\n const db = openIndex(indexDbPath(repoRoot));\n try {\n const rows = db\n .prepare<\n [],\n { slug: string; title: string | null; description: string | null; page_count: number }\n >(\n // page_count excludes archived pages — matches the policy used\n // by `topics show` (see `pagesDirectlyTagged`) and by every\n // page-scoped check in `health`. Pick one rule and apply it\n // everywhere; a topic with \"5 pages\" in `topics list` and \"3\n // pages\" in `topics show` is a trust-eroding inconsistency.\n `SELECT t.slug, t.title, t.description,\n (SELECT COUNT(*)\n FROM page_topics pt\n JOIN pages p ON p.slug = pt.page_slug\n WHERE pt.topic_slug = t.slug AND p.archived_at IS NULL\n ) AS page_count\n FROM topics t\n ORDER BY t.slug`,\n )\n .all();\n\n if (options.json === true) {\n return {\n stdout: `${JSON.stringify(rows, null, 2)}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n if (rows.length === 0) {\n return {\n stdout:\n \"no topics. create one with `almanac topics create <name>` or tag a page.\\n\",\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const slugWidth = rows.reduce((w, r) => Math.max(w, r.slug.length), 0);\n const lines = rows.map((r) => {\n const slug = r.slug.padEnd(slugWidth);\n const count = `(${r.page_count} page${r.page_count === 1 ? \"\" : \"s\"})`;\n return `${slug} ${count}`;\n });\n return { stdout: `${lines.join(\"\\n\")}\\n`, stderr: \"\", exitCode: 0 };\n } finally {\n db.close();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// show — `almanac topics show <slug>`\n// ─────────────────────────────────────────────────────────────────────\n\nexport interface TopicsShowRecord {\n slug: string;\n title: string | null;\n description: string | null;\n parents: string[];\n children: string[];\n pages: string[];\n descendants_used?: boolean;\n}\n\n/**\n * `almanac topics show <slug>`. Prints metadata + parents, children,\n * and the page list. `--descendants` widens the page list to include\n * pages tagged with any descendant topic (via the DAG).\n */\nexport async function runTopicsShow(\n options: TopicsShowOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n await ensureFreshIndex({ repoRoot });\n\n const slug = toKebabCase(options.slug);\n if (slug.length === 0) {\n return {\n stdout: \"\",\n stderr: `almanac: empty topic slug\\n`,\n exitCode: 1,\n };\n }\n\n const db = openIndex(indexDbPath(repoRoot));\n try {\n const row = db\n .prepare<\n [string],\n { slug: string; title: string | null; description: string | null }\n >(\"SELECT slug, title, description FROM topics WHERE slug = ?\")\n .get(slug);\n if (row === undefined) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${slug}\"\\n`,\n exitCode: 1,\n };\n }\n\n const parents = db\n .prepare<[string], { parent_slug: string }>(\n \"SELECT parent_slug FROM topic_parents WHERE child_slug = ? ORDER BY parent_slug\",\n )\n .all(slug)\n .map((r) => r.parent_slug);\n\n const children = db\n .prepare<[string], { child_slug: string }>(\n \"SELECT child_slug FROM topic_parents WHERE parent_slug = ? ORDER BY child_slug\",\n )\n .all(slug)\n .map((r) => r.child_slug);\n\n const pageSlugs = options.descendants === true\n ? pagesForSubtree(db, slug)\n : pagesDirectlyTagged(db, slug);\n\n const record: TopicsShowRecord = {\n slug: row.slug,\n title: row.title,\n description: row.description,\n parents,\n children,\n pages: pageSlugs,\n descendants_used: options.descendants === true,\n };\n\n if (options.json === true) {\n return {\n stdout: `${JSON.stringify(record, null, 2)}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n return { stdout: formatShow(record), stderr: \"\", exitCode: 0 };\n } finally {\n db.close();\n }\n}\n\nfunction pagesDirectlyTagged(db: Database.Database, slug: string): string[] {\n return db\n .prepare<[string], { page_slug: string }>(\n `SELECT pt.page_slug\n FROM page_topics pt\n JOIN pages p ON p.slug = pt.page_slug\n WHERE pt.topic_slug = ? AND p.archived_at IS NULL\n ORDER BY pt.page_slug`,\n )\n .all(slug)\n .map((r) => r.page_slug);\n}\n\nfunction pagesForSubtree(db: Database.Database, slug: string): string[] {\n const slugs = [slug, ...descendantsInDb(db, slug)];\n // Deduplicate + preserve order via a Set — a page can belong to\n // multiple topics in the subtree and we only want one row per page.\n const placeholders = slugs.map(() => \"?\").join(\", \");\n const rows = db\n .prepare<unknown[], { page_slug: string }>(\n `SELECT DISTINCT pt.page_slug\n FROM page_topics pt\n JOIN pages p ON p.slug = pt.page_slug\n WHERE pt.topic_slug IN (${placeholders}) AND p.archived_at IS NULL\n ORDER BY pt.page_slug`,\n )\n .all(...slugs);\n return rows.map((r) => r.page_slug);\n}\n\nfunction formatShow(r: TopicsShowRecord): string {\n const lines: string[] = [];\n lines.push(`slug: ${r.slug}`);\n lines.push(`title: ${r.title ?? titleCase(r.slug)}`);\n lines.push(`description: ${r.description ?? \"—\"}`);\n lines.push(\n `parents: ${r.parents.length > 0 ? r.parents.join(\", \") : \"—\"}`,\n );\n lines.push(\n `children: ${r.children.length > 0 ? r.children.join(\", \") : \"—\"}`,\n );\n const pagesLabel = r.descendants_used === true ? \"pages (incl. descendants)\" : \"pages\";\n lines.push(`${pagesLabel}:`);\n if (r.pages.length === 0) {\n lines.push(\" —\");\n } else {\n for (const p of r.pages) lines.push(` ${p}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// create — `almanac topics create <name> [--parent <slug>]...`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics create <name> [--parent <slug>]...`.\n *\n * Policy: `--parent <slug>` MUST refer to an existing topic (created\n * earlier in topics.yaml or indexed from page frontmatter). Auto-\n * creating parents silently would let typos cascade — `create JWT\n * --parent secuirty` would quietly spawn a \"secuirty\" topic. Better to\n * refuse and point the user at `almanac topics create <parent>` first.\n *\n * Already-exists is not an error if no new parents are being added —\n * rerunning the same `create` is a no-op. If new parents are introduced\n * we add them (respecting cycle prevention, just like `link`).\n */\nexport async function runTopicsCreate(\n options: TopicsCreateOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n\n const slug = toKebabCase(options.name);\n if (slug.length === 0) {\n return {\n stdout: \"\",\n stderr: `almanac: topic name \"${options.name}\" has no slug-able characters\\n`,\n exitCode: 1,\n };\n }\n const title = options.name.trim().length > 0 ? options.name.trim() : titleCase(slug);\n\n // Reindex first so an ad-hoc `--parent <slug>` created by a\n // just-written page is visible to `topicExists`. Every other topics\n // command did this; `create` silently skipped the refresh which meant\n // a brand-new page's `topics: [newparent]` couldn't be used as a\n // parent until the next query touched the DB.\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n\n // Hoist the DB open out of the parents loop. We used to open + close\n // the DB inside `isAdHocTopicInDb` for every parent; opening is\n // cheap but not free, and the iteration pattern shows up in other\n // topics commands too.\n const db = openIndex(indexDbPath(repoRoot));\n try {\n // Resolve/validate parents BEFORE mutating the file. All-or-nothing.\n const requestedParents = (options.parents ?? [])\n .map((p) => toKebabCase(p))\n .filter((p) => p.length > 0);\n for (const p of requestedParents) {\n if (p === slug) {\n return {\n stdout: \"\",\n stderr: `almanac: topic cannot be its own parent\\n`,\n exitCode: 1,\n };\n }\n if (!topicExists(file, db, p)) {\n return {\n stdout: \"\",\n stderr: `almanac: parent topic \"${p}\" does not exist; create it first with \\`almanac topics create ${p}\\`\\n`,\n exitCode: 1,\n };\n }\n if (findTopic(file, p) === null) {\n // Topic exists only as an ad-hoc DB entry. Promote it into\n // topics.yaml so it has a proper record. `ensureTopic` is\n // idempotent so this is safe even if two loop iterations\n // reference the same ad-hoc parent.\n ensureTopic(file, p);\n }\n }\n\n const existing = findTopic(file, slug);\n if (existing === null) {\n const entry: TopicEntry = {\n slug,\n title,\n description: null,\n parents: requestedParents,\n };\n file.topics.push(entry);\n } else {\n // Add any new parents, skipping ones that already exist or would\n // create a cycle.\n for (const p of requestedParents) {\n if (existing.parents.includes(p)) continue;\n const ancestors = ancestorsInFile(file, p);\n if (ancestors.has(slug) || p === slug) {\n return {\n stdout: \"\",\n stderr: `almanac: adding \"${p}\" as a parent of \"${slug}\" would create a cycle\\n`,\n exitCode: 1,\n };\n }\n existing.parents.push(p);\n }\n // Promote the user-supplied title only if the existing one was a\n // title-cased default (i.e., they didn't describe it yet). Don't\n // clobber a deliberate title silently.\n if (\n existing.title === titleCase(existing.slug) &&\n title !== titleCase(slug) &&\n title !== existing.title\n ) {\n existing.title = title;\n }\n }\n\n await writeTopicsFile(yamlPath, file);\n await runIndexer({ repoRoot });\n return {\n stdout: existing === null\n ? `created topic \"${slug}\"\\n`\n : `updated topic \"${slug}\"\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n } finally {\n db.close();\n }\n}\n\n/**\n * Is `slug` a known topic anywhere — in `topics.yaml`, or as an ad-hoc\n * slug that a page's frontmatter mentioned and the indexer seeded?\n *\n * Collapses the previous `findTopic(file, s) === null &&\n * !isAdHocTopicInDb(root, s)` duplication into one intent-revealing\n * helper. Takes an open `db` handle so the caller can hoist DB open\n * out of tight loops — every earlier call site created + destroyed a\n * new connection per iteration.\n */\nfunction topicExists(\n file: TopicsFile,\n db: Database.Database,\n slug: string,\n): boolean {\n if (findTopic(file, slug) !== null) return true;\n const row = db\n .prepare<[string], { slug: string }>(\n \"SELECT slug FROM topics WHERE slug = ?\",\n )\n .get(slug);\n return row !== undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// link / unlink\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics link <child> <parent>`. Adds a DAG edge after\n * checking that it wouldn't close a cycle. Both topics must exist.\n */\nexport async function runTopicsLink(\n options: TopicsLinkOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const child = toKebabCase(options.child);\n const parent = toKebabCase(options.parent);\n if (child.length === 0 || parent.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n if (child === parent) {\n return {\n stdout: \"\",\n stderr: `almanac: topic cannot be its own parent\\n`,\n exitCode: 1,\n };\n }\n\n // Refresh the index so ad-hoc topics (ones the user tagged pages\n // with but never formally `topics create`d) are visible to\n // `topicExists` below.\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n\n const db = openIndex(indexDbPath(repoRoot));\n try {\n for (const slug of [child, parent]) {\n if (!topicExists(file, db, slug)) {\n return {\n stdout: \"\",\n stderr: `almanac: topic \"${slug}\" does not exist\\n`,\n exitCode: 1,\n };\n }\n if (findTopic(file, slug) === null) {\n // DB-only ad-hoc topic → promote it into topics.yaml so the\n // new DAG edge has a concrete home.\n ensureTopic(file, slug);\n }\n }\n\n const childEntry = findTopic(file, child);\n if (childEntry === null) {\n // Shouldn't happen after ensureTopic above — defensive.\n return {\n stdout: \"\",\n stderr: `almanac: topic \"${child}\" not found\\n`,\n exitCode: 1,\n };\n }\n\n if (childEntry.parents.includes(parent)) {\n return {\n stdout: `edge ${child} → ${parent} already exists\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Cycle check BEFORE mutation. Uses the in-memory file so the check\n // operates on the state we're about to write — no DB round-trip needed.\n const parentAncestors = ancestorsInFile(file, parent);\n if (parentAncestors.has(child) || parent === child) {\n return {\n stdout: \"\",\n stderr: `almanac: adding ${parent} as parent of ${child} would create a cycle\\n`,\n exitCode: 1,\n };\n }\n\n childEntry.parents.push(parent);\n await writeTopicsFile(yamlPath, file);\n await runIndexer({ repoRoot });\n return {\n stdout: `linked ${child} → ${parent}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n } finally {\n db.close();\n }\n}\n\n/**\n * `almanac topics unlink <child> <parent>`. Removes a DAG edge if it\n * exists. No-op (exit 0) if not. Never deletes topics.\n */\nexport async function runTopicsUnlink(\n options: TopicsUnlinkOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const child = toKebabCase(options.child);\n const parent = toKebabCase(options.parent);\n if (child.length === 0 || parent.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n const childEntry = findTopic(file, child);\n if (childEntry === null || !childEntry.parents.includes(parent)) {\n return {\n stdout: `no edge ${child} → ${parent}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n childEntry.parents = childEntry.parents.filter((p) => p !== parent);\n await writeTopicsFile(yamlPath, file);\n await runIndexer({ repoRoot });\n return {\n stdout: `unlinked ${child} → ${parent}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// rename — `almanac topics rename <old> <new>`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics rename <old> <new>`. Rewrites the slug both in\n * `topics.yaml` (as an entry key and in anyone who declared it as a\n * parent) and in every affected page's frontmatter.\n *\n * Refuses if `<new>` is already a distinct topic — \"merging\" two topics\n * should be explicit, not a silent side effect of a rename.\n */\nexport async function runTopicsRename(\n options: TopicsRenameOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const oldSlug = toKebabCase(options.oldSlug);\n const newSlug = toKebabCase(options.newSlug);\n if (oldSlug.length === 0 || newSlug.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n if (oldSlug === newSlug) {\n return {\n stdout: `topic \"${oldSlug}\" unchanged\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n\n // Hoist the DB handle so the existence checks below don't reopen\n // the index per-slug.\n const db = openIndex(indexDbPath(repoRoot));\n let pagesUpdated: number;\n try {\n // Fetch existence info. `oldInYaml` is kept as a direct reference\n // because we mutate the entry; the DB check is only needed when\n // the slug isn't in the file (ad-hoc-only).\n const oldInYaml = findTopic(file, oldSlug);\n if (!topicExists(file, db, oldSlug)) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${oldSlug}\"\\n`,\n exitCode: 1,\n };\n }\n\n if (topicExists(file, db, newSlug)) {\n return {\n stdout: \"\",\n stderr: `almanac: topic \"${newSlug}\" already exists; delete it first if you intend to merge\\n`,\n exitCode: 1,\n };\n }\n\n // Rewrite `topics.yaml`: the entry itself (if present) plus any\n // parent reference to `oldSlug`.\n if (oldInYaml !== null) {\n oldInYaml.slug = newSlug;\n if (oldInYaml.title === titleCase(oldSlug)) {\n // Title was the auto-generated default — refresh it to the new\n // slug's title-case. A custom title stays as-is.\n oldInYaml.title = titleCase(newSlug);\n }\n }\n for (const t of file.topics) {\n t.parents = t.parents.map((p) => (p === oldSlug ? newSlug : p));\n }\n\n // Write ordering matters: topics.yaml FIRST (atomic tmp+rename), THEN\n // the page rewrites. If topics.yaml write fails, no page was touched.\n // If a page rewrite fails midway, topics.yaml already reflects the\n // rename so the next reindex picks up the ad-hoc state and the user\n // can re-run to finish the remaining pages. The opposite ordering\n // would leave half-rewritten pages referencing a slug that\n // topics.yaml doesn't know about.\n await writeTopicsFile(yamlPath, file);\n\n // Rewrite every page that has `oldSlug` in `topics:` frontmatter.\n pagesUpdated = await rewriteTopicOnPages(repoRoot, (topics) =>\n topics.map((t) => (t === oldSlug ? newSlug : t)),\n );\n } finally {\n db.close();\n }\n\n await runIndexer({ repoRoot });\n return {\n stdout: `renamed ${oldSlug} → ${newSlug} (${pagesUpdated} page${pagesUpdated === 1 ? \"\" : \"s\"} updated)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// delete — `almanac topics delete <slug>`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics delete <slug>`. Removes the topic from `topics.yaml`\n * (if present), scrubs any parent edges pointing at it, and untags\n * every page that had it. Pages themselves are left alone — deleting a\n * topic doesn't delete pages, just the relationship.\n */\nexport async function runTopicsDelete(\n options: TopicsDeleteOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const slug = toKebabCase(options.slug);\n if (slug.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n const db = openIndex(indexDbPath(repoRoot));\n let pagesUpdated: number;\n try {\n if (!topicExists(file, db, slug)) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${slug}\"\\n`,\n exitCode: 1,\n };\n }\n\n // Remove the entry and strip it from everyone else's `parents` list.\n file.topics = file.topics.filter((t) => t.slug !== slug);\n for (const t of file.topics) {\n t.parents = t.parents.filter((p) => p !== slug);\n }\n\n // Same write ordering as rename: topics.yaml first (atomic), then\n // pages. A crash between the two leaves topics.yaml already scrubbed\n // and any remaining in-page references become ad-hoc topics — which\n // the reindex will pick up as empty-topics on next health, and the\n // user can re-run to finish untagging.\n await writeTopicsFile(yamlPath, file);\n\n pagesUpdated = await rewriteTopicOnPages(repoRoot, (topics) =>\n topics.filter((t) => t !== slug),\n );\n } finally {\n db.close();\n }\n\n await runIndexer({ repoRoot });\n return {\n stdout: `deleted topic \"${slug}\" (${pagesUpdated} page${pagesUpdated === 1 ? \"\" : \"s\"} untagged)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// describe — `almanac topics describe <slug> \"<text>\"`\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * `almanac topics describe <slug> \"<text>\"`. Sets or updates the\n * one-liner description. An empty string clears it.\n */\nexport async function runTopicsDescribe(\n options: TopicsDescribeOptions,\n): Promise<TopicsCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n const slug = toKebabCase(options.slug);\n if (slug.length === 0) {\n return { stdout: \"\", stderr: `almanac: empty topic slug\\n`, exitCode: 1 };\n }\n\n await ensureFreshIndex({ repoRoot });\n\n const yamlPath = topicsYamlPath(repoRoot);\n const file = await loadTopicsFile(yamlPath);\n const db = openIndex(indexDbPath(repoRoot));\n try {\n if (!topicExists(file, db, slug)) {\n return {\n stdout: \"\",\n stderr: `almanac: no such topic \"${slug}\"\\n`,\n exitCode: 1,\n };\n }\n // `ensureTopic` is idempotent — if the topic was DB-only it\n // promotes into `file`; if already in `file` it returns the\n // existing entry. Either way we get a concrete entry to mutate.\n const entry = ensureTopic(file, slug);\n\n const text = options.description.trim();\n entry.description = text.length === 0 ? null : text;\n\n await writeTopicsFile(yamlPath, file);\n } finally {\n db.close();\n }\n\n await runIndexer({ repoRoot });\n return {\n stdout: `described ${slug}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// Shared helpers\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * Apply a `topic-list transform` to every `.almanac/pages/*.md` file\n * whose frontmatter contains a relevant topic. Returns the number of\n * files actually changed.\n *\n * We glob page files ourselves (not the DB) so this works even on a\n * stale index — `rename` and `delete` run the indexer AFTER mutation,\n * and we don't want the scan to miss a page that was just modified.\n *\n * `transform` operates on the full topic list of each page; returning\n * the same list = no-op (no write). We short-circuit cheaply via\n * `applyTopicsTransform` before touching the file.\n */\nasync function rewriteTopicOnPages(\n repoRoot: string,\n transform: (topics: string[]) => string[],\n): Promise<number> {\n const pagesDir = join(repoRoot, \".almanac\", \"pages\");\n const files = await fg(\"**/*.md\", {\n cwd: pagesDir,\n absolute: true,\n onlyFiles: true,\n });\n let changed = 0;\n for (const filePath of files) {\n // Cheap read → in-memory check. Skip files that wouldn't be\n // changed so we don't bump their mtime.\n const raw = await readFile(filePath, \"utf8\");\n const applied = applyTopicsTransform(raw, transform);\n if (!applied.changed) continue;\n await rewritePageTopics(filePath, transform);\n changed += 1;\n }\n return changed;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// dispatch helpers (used by cli.ts)\n// ─────────────────────────────────────────────────────────────────────\n\nexport {\n ensureTopic,\n findTopic,\n loadTopicsFile,\n writeTopicsFile,\n type TopicEntry,\n type TopicsFile,\n};\n","import { existsSync } from \"node:fs\";\nimport { basename } from \"node:path\";\n\nimport { findNearestAlmanacDir } from \"../paths.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport {\n addEntry,\n readRegistry,\n type RegistryEntry,\n} from \"./index.js\";\n\n/**\n * If the current working directory (or any parent) has a `.almanac/` that\n * isn't in the registry, silently add it. Runs as a side effect of every\n * command except `init` (which does its own registration) and `list --drop`\n * (which shouldn't resurrect the entry the user just removed).\n *\n * The contract is \"silent\" for environmental problems — missing home dir,\n * unreadable registry file, permission errors. Those shouldn't block the\n * real command from running. But a **malformed** registry IS surfaced: if\n * the JSON is corrupt, the user needs to know, not have auto-register\n * quietly pretend the registry was empty and start overwriting entries.\n */\nexport async function autoRegisterIfNeeded(\n cwd: string,\n): Promise<RegistryEntry | null> {\n try {\n const repoRoot = findNearestAlmanacDir(cwd);\n if (repoRoot === null) return null;\n\n // Double-check the directory still exists — `findNearestAlmanacDir`\n // already confirms this, but we're explicit about the precondition.\n if (!existsSync(repoRoot)) return null;\n\n // Read the registry ONCE. `resolveNameCollision` scans this snapshot\n // in memory; re-reading per iteration would be O(N²) in collision\n // count and needlessly hit the filesystem.\n const entries = await readRegistry();\n\n const existing = entries.find((e) => samePath(e.path, repoRoot));\n if (existing !== undefined) return existing;\n\n // Derive a kebab-case name from the directory. If the dir name is\n // somehow empty (e.g. repo is at filesystem root), skip — we don't\n // want to register a nameless entry.\n const name = toKebabCase(basename(repoRoot));\n if (name.length === 0) return null;\n\n // Resolve collisions on name by falling back to a disambiguated form.\n // Auto-registration should never overwrite an existing named entry\n // that points elsewhere.\n const finalName = resolveNameCollision(entries, name, repoRoot);\n if (finalName === null) return null;\n\n const entry: RegistryEntry = {\n name: finalName,\n description: \"\",\n path: repoRoot,\n registered_at: new Date().toISOString(),\n };\n await addEntry(entry);\n return entry;\n } catch (err: unknown) {\n // Only swallow errors that mean \"registry state isn't readable right\n // now\" — everything else (malformed JSON, programmer errors, bugs)\n // should propagate so the user can see it.\n if (\n err instanceof Error &&\n \"code\" in err &&\n (err.code === \"ENOENT\" ||\n err.code === \"EACCES\" ||\n err.code === \"EPERM\")\n ) {\n return null;\n }\n throw err;\n }\n}\n\n/**\n * If another repo already claims `name`, append `-2`, `-3`, ... until we\n * find an unused slug. Only relevant for auto-registration — `init` with\n * `--name` lets the user resolve collisions explicitly.\n *\n * Takes a snapshot of registry entries instead of re-reading the file per\n * iteration. Caps at 1000 attempts to prevent pathological loops if the\n * registry somehow contains every suffix (it can't, but we'd rather fail\n * explicitly than spin).\n */\nfunction resolveNameCollision(\n entries: RegistryEntry[],\n baseName: string,\n repoPath: string,\n): string | null {\n const owner = entries.find((e) => e.name === baseName);\n if (owner === undefined || samePath(owner.path, repoPath)) {\n return baseName;\n }\n const taken = new Set(entries.map((e) => e.name));\n const MAX_ATTEMPTS = 1000;\n for (let suffix = 2; suffix < MAX_ATTEMPTS + 2; suffix += 1) {\n const candidate = `${baseName}-${suffix}`;\n if (!taken.has(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * Mirror `pathsEqual` in `registry/index.ts` — case-insensitive on\n * macOS/Windows, case-sensitive on Linux. Duplicated here rather than\n * exported to keep the registry module's public surface small.\n */\nfunction samePath(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","import { run } from \"../src/cli.js\";\n\nrun(process.argv).catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`almanac: ${message}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["basename","existsSync","join","require","resolve","existsSync","dirname","join","existsSync","mkdir","readFile","writeFile","join","readFile","dirname","path","readFile","dirname","join","existsSync","mkdir","writeFile","path","readFile","join","existsSync","resolve","createWriteStream","existsSync","readFile","readdir","homedir","basename","join","relative","join","formatTimestamp","createWriteStream","closeStream","relative","existsSync","homedir","basename","readdir","path","readFile","resolve","existsSync","mkdir","readFile","rename","writeFile","homedir","path","fileURLToPath","existsSync","readFile","basename","join","fg","createHash","existsSync","statSync","readFile","basename","join","relative","existsSync","mkdir","readFile","rename","writeFile","dirname","yaml","path","existsSync","readFile","isNodeError","yaml","dirname","mkdir","writeFile","rename","target","path","join","existsSync","basename","statSync","readFile","path","relative","createHash","topicsYamlPath","existsSync","join","existsSync","join","join","existsSync","readFile","fg","basename","join","join","existsSync","existsSync","join","join","collectSlugs","join","join","readFile","join","join","collectSlugs","readFile","readFile","rename","writeFile","yaml","values","parsed","join","stderr","readFile","join","fg","join","fg","readFile","existsSync","basename","existsSync","basename","basename","query"]}
|