codealmanac 0.1.0 → 0.1.2

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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { createRequire as createRequire3 } from "module";
4
5
  import { basename as basename6 } from "path";
5
6
  import { Command } from "commander";
6
7
 
@@ -16,10 +17,8 @@ import { dirname, join } from "path";
16
17
  var AUTH_TIMEOUT_MS = 1e4;
17
18
  function resolveCliJsPath() {
18
19
  const require2 = createRequire(import.meta.url);
19
- const pkgJsonPath = require2.resolve(
20
- "@anthropic-ai/claude-agent-sdk/package.json"
21
- );
22
- return join(dirname(pkgJsonPath), "cli.js");
20
+ const entry = require2.resolve("@anthropic-ai/claude-agent-sdk");
21
+ return join(dirname(entry), "cli.js");
23
22
  }
24
23
  var defaultSpawnCli = (args) => {
25
24
  const cliPath = resolveCliJsPath();
@@ -227,7 +226,7 @@ function findNearestAlmanacDir(startDir) {
227
226
  }
228
227
  }
229
228
 
230
- // src/commands/init.ts
229
+ // src/commands/_init.ts
231
230
  import { existsSync as existsSync3 } from "fs";
232
231
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
233
232
  import { basename, join as join3 } from "path";
@@ -241,10 +240,10 @@ function toKebabCase(input) {
241
240
  import { mkdir, readFile as readFile2, rename, writeFile } from "fs/promises";
242
241
  import { dirname as dirname3 } from "path";
243
242
  async function readRegistry() {
244
- const path3 = getRegistryPath();
243
+ const path5 = getRegistryPath();
245
244
  let raw;
246
245
  try {
247
- raw = await readFile2(path3, "utf8");
246
+ raw = await readFile2(path5, "utf8");
248
247
  } catch (err) {
249
248
  if (isNodeError(err) && err.code === "ENOENT") {
250
249
  return [];
@@ -260,10 +259,10 @@ async function readRegistry() {
260
259
  parsed = JSON.parse(trimmed);
261
260
  } catch (err) {
262
261
  const message = err instanceof Error ? err.message : String(err);
263
- throw new Error(`registry at ${path3} is not valid JSON: ${message}`);
262
+ throw new Error(`registry at ${path5} is not valid JSON: ${message}`);
264
263
  }
265
264
  if (!Array.isArray(parsed)) {
266
- throw new Error(`registry at ${path3} must be a JSON array`);
265
+ throw new Error(`registry at ${path5} must be a JSON array`);
267
266
  }
268
267
  return parsed.map((item, idx) => {
269
268
  if (typeof item !== "object" || item === null) {
@@ -271,29 +270,29 @@ async function readRegistry() {
271
270
  }
272
271
  const e = item;
273
272
  const name = typeof e.name === "string" ? e.name : "";
274
- const path4 = typeof e.path === "string" ? e.path : "";
273
+ const path6 = typeof e.path === "string" ? e.path : "";
275
274
  if (name.length === 0) {
276
275
  throw new Error(`registry entry ${idx} is missing a non-empty "name"`);
277
276
  }
278
- if (path4.length === 0) {
277
+ if (path6.length === 0) {
279
278
  throw new Error(`registry entry ${idx} is missing a non-empty "path"`);
280
279
  }
281
280
  return {
282
281
  name,
283
282
  description: typeof e.description === "string" ? e.description : "",
284
- path: path4,
283
+ path: path6,
285
284
  registered_at: typeof e.registered_at === "string" ? e.registered_at : ""
286
285
  };
287
286
  });
288
287
  }
289
288
  async function writeRegistry(entries) {
290
- const path3 = getRegistryPath();
291
- await mkdir(dirname3(path3), { recursive: true });
289
+ const path5 = getRegistryPath();
290
+ await mkdir(dirname3(path5), { recursive: true });
292
291
  const body = `${JSON.stringify(entries, null, 2)}
293
292
  `;
294
- const tmpPath = `${path3}.tmp`;
293
+ const tmpPath = `${path5}.tmp`;
295
294
  await writeFile(tmpPath, body, "utf8");
296
- await rename(tmpPath, path3);
295
+ await rename(tmpPath, path5);
297
296
  }
298
297
  function pathsEqual(a, b) {
299
298
  if (process.platform === "darwin" || process.platform === "win32") {
@@ -337,7 +336,7 @@ function isNodeError(err) {
337
336
  return err instanceof Error && "code" in err;
338
337
  }
339
338
 
340
- // src/commands/init.ts
339
+ // src/commands/_init.ts
341
340
  async function initWiki(options) {
342
341
  const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;
343
342
  const almanacDir = getRepoAlmanacDir(repoRoot);
@@ -367,15 +366,15 @@ async function initWiki(options) {
367
366
  return { entry, almanacDir, created: !alreadyExisted };
368
367
  }
369
368
  async function ensureGitignoreHasIndexDb(cwd) {
370
- const path3 = join3(cwd, ".gitignore");
369
+ const path5 = join3(cwd, ".gitignore");
371
370
  const targets = [
372
371
  ".almanac/index.db",
373
372
  ".almanac/index.db-wal",
374
373
  ".almanac/index.db-shm"
375
374
  ];
376
375
  let existing = "";
377
- if (existsSync3(path3)) {
378
- existing = await readFile3(path3, "utf8");
376
+ if (existsSync3(path5)) {
377
+ existing = await readFile3(path5, "utf8");
379
378
  }
380
379
  const lines = existing.split(/\r?\n/).map((l) => l.trim());
381
380
  const missing = targets.filter((t) => !lines.includes(t));
@@ -385,7 +384,7 @@ async function ensureGitignoreHasIndexDb(cwd) {
385
384
  ${missing.join("\n")}
386
385
  `;
387
386
  const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
388
- await writeFile2(path3, `${existing}${sep}${block}`, "utf8");
387
+ await writeFile2(path5, `${existing}${sep}${block}`, "utf8");
389
388
  }
390
389
  function starterReadme() {
391
390
  return `# Wiki
@@ -988,8 +987,8 @@ async function filterTranscriptsByCwd(transcripts, repoRoot) {
988
987
  }
989
988
  return hits;
990
989
  }
991
- async function readHead(path3, bytes) {
992
- const content = await readFile4(path3, "utf8");
990
+ async function readHead(path5, bytes) {
991
+ const content = await readFile4(path5, "utf8");
993
992
  return content.length > bytes ? content.slice(0, bytes) : content;
994
993
  }
995
994
  async function snapshotPages(pagesDir) {
@@ -1296,13 +1295,13 @@ import { existsSync as existsSync7 } from "fs";
1296
1295
  import { mkdir as mkdir4, readFile as readFile6, rename as rename3, writeFile as writeFile4 } from "fs/promises";
1297
1296
  import { dirname as dirname4 } from "path";
1298
1297
  import yaml2 from "js-yaml";
1299
- async function loadTopicsFile(path3) {
1300
- if (!existsSync7(path3)) {
1298
+ async function loadTopicsFile(path5) {
1299
+ if (!existsSync7(path5)) {
1301
1300
  return { topics: [] };
1302
1301
  }
1303
1302
  let raw;
1304
1303
  try {
1305
- raw = await readFile6(path3, "utf8");
1304
+ raw = await readFile6(path5, "utf8");
1306
1305
  } catch (err) {
1307
1306
  if (isNodeError2(err) && err.code === "ENOENT") {
1308
1307
  return { topics: [] };
@@ -1318,13 +1317,13 @@ async function loadTopicsFile(path3) {
1318
1317
  parsed = yaml2.load(raw);
1319
1318
  } catch (err) {
1320
1319
  const message = err instanceof Error ? err.message : String(err);
1321
- throw new Error(`topics.yaml at ${path3} is not valid YAML: ${message}`);
1320
+ throw new Error(`topics.yaml at ${path5} is not valid YAML: ${message}`);
1322
1321
  }
1323
1322
  if (parsed === null || parsed === void 0) {
1324
1323
  return { topics: [] };
1325
1324
  }
1326
1325
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
1327
- throw new Error(`topics.yaml at ${path3} must be a mapping`);
1326
+ throw new Error(`topics.yaml at ${path5} must be a mapping`);
1328
1327
  }
1329
1328
  const obj = parsed;
1330
1329
  const rawTopics = obj.topics;
@@ -1332,7 +1331,7 @@ async function loadTopicsFile(path3) {
1332
1331
  return { topics: [] };
1333
1332
  }
1334
1333
  if (!Array.isArray(rawTopics)) {
1335
- throw new Error(`topics.yaml at ${path3} \u2014 "topics" must be a list`);
1334
+ throw new Error(`topics.yaml at ${path5} \u2014 "topics" must be a list`);
1336
1335
  }
1337
1336
  const topics = [];
1338
1337
  for (const item of rawTopics) {
@@ -1361,7 +1360,7 @@ async function loadTopicsFile(path3) {
1361
1360
  }
1362
1361
  return { topics };
1363
1362
  }
1364
- async function writeTopicsFile(path3, file) {
1363
+ async function writeTopicsFile(path5, file) {
1365
1364
  const sorted = [...file.topics].sort((a, b) => a.slug.localeCompare(b.slug));
1366
1365
  const doc = {
1367
1366
  topics: sorted.map((t) => {
@@ -1386,13 +1385,13 @@ async function writeTopicsFile(path3, file) {
1386
1385
  sortKeys: false
1387
1386
  });
1388
1387
  const content = `${header}${body}`;
1389
- const tmpPath = `${path3}.tmp`;
1390
- const parent = dirname4(path3);
1388
+ const tmpPath = `${path5}.tmp`;
1389
+ const parent = dirname4(path5);
1391
1390
  if (!existsSync7(parent)) {
1392
1391
  await mkdir4(parent, { recursive: true });
1393
1392
  }
1394
1393
  await writeFile4(tmpPath, content, "utf8");
1395
- await rename3(tmpPath, path3);
1394
+ await rename3(tmpPath, path5);
1396
1395
  }
1397
1396
  function findTopic(file, slug) {
1398
1397
  for (const t of file.topics) {
@@ -1541,10 +1540,10 @@ function classifyWikilink(raw) {
1541
1540
  }
1542
1541
  if (firstSlash !== -1) {
1543
1542
  const isDir = looksLikeDir(body);
1544
- const path3 = normalizePath(body, isDir);
1543
+ const path5 = normalizePath(body, isDir);
1545
1544
  const originalPath = normalizePathPreservingCase(body, isDir);
1546
- if (path3.length === 0) return null;
1547
- return isDir ? { kind: "folder", path: path3, originalPath } : { kind: "file", path: path3, originalPath };
1545
+ if (path5.length === 0) return null;
1546
+ return isDir ? { kind: "folder", path: path5, originalPath } : { kind: "file", path: path5, originalPath };
1548
1547
  }
1549
1548
  const target = toKebabCase(body);
1550
1549
  if (target.length === 0) return null;
@@ -1764,10 +1763,10 @@ async function indexPagesInto(db, pagesDir) {
1764
1763
  }
1765
1764
  for (const raw of p.frontmatterFiles) {
1766
1765
  const isDir = looksLikeDir(raw);
1767
- const path3 = normalizePath(raw, isDir);
1766
+ const path5 = normalizePath(raw, isDir);
1768
1767
  const originalPath = normalizePathPreservingCase(raw, isDir);
1769
- if (path3.length === 0) continue;
1770
- insertFileRef.run(p.slug, path3, originalPath, isDir ? 1 : 0);
1768
+ if (path5.length === 0) continue;
1769
+ insertFileRef.run(p.slug, path5, originalPath, isDir ? 1 : 0);
1771
1770
  }
1772
1771
  for (const ref of p.wikilinks) {
1773
1772
  switch (ref.kind) {
@@ -1823,8 +1822,8 @@ function hashContent(raw) {
1823
1822
  return createHash2("sha256").update(raw).digest("hex");
1824
1823
  }
1825
1824
  function topicsYamlNewerThan(almanacDir, dbPath) {
1826
- const path3 = join6(almanacDir, "topics.yaml");
1827
- if (!existsSync8(path3)) return false;
1825
+ const path5 = join6(almanacDir, "topics.yaml");
1826
+ if (!existsSync8(path5)) return false;
1828
1827
  let dbMtime;
1829
1828
  try {
1830
1829
  dbMtime = statSync2(dbPath).mtimeMs;
@@ -1832,7 +1831,7 @@ function topicsYamlNewerThan(almanacDir, dbPath) {
1832
1831
  return true;
1833
1832
  }
1834
1833
  try {
1835
- const st = statSync2(path3);
1834
+ const st = statSync2(path5);
1836
1835
  return st.mtimeMs > dbMtime;
1837
1836
  } catch {
1838
1837
  return false;
@@ -2254,153 +2253,6 @@ function section(label, count, lines) {
2254
2253
  ${lines.join("\n")}`;
2255
2254
  }
2256
2255
 
2257
- // src/commands/info.ts
2258
- import { join as join9 } from "path";
2259
- async function runInfo(options) {
2260
- const repoRoot = await resolveWikiRoot({
2261
- cwd: options.cwd,
2262
- wiki: options.wiki
2263
- });
2264
- await ensureFreshIndex({ repoRoot });
2265
- const dbPath = join9(repoRoot, ".almanac", "index.db");
2266
- const db = openIndex(dbPath);
2267
- try {
2268
- const slugs = collectSlugs(options);
2269
- if (slugs.length === 0) {
2270
- return {
2271
- stdout: "",
2272
- stderr: "almanac: info requires a slug (or --stdin)\n",
2273
- exitCode: 1
2274
- };
2275
- }
2276
- const records = [];
2277
- const missing = [];
2278
- for (const slug of slugs) {
2279
- const rec = fetchInfo(db, slug);
2280
- if (rec === null) {
2281
- missing.push(slug);
2282
- continue;
2283
- }
2284
- records.push(rec);
2285
- }
2286
- const bulk = options.stdin === true;
2287
- const jsonOut = options.json === true || bulk;
2288
- let stdout;
2289
- if (jsonOut) {
2290
- if (bulk) {
2291
- stdout = `${JSON.stringify(records, null, 2)}
2292
- `;
2293
- } else {
2294
- const only = records[0] ?? null;
2295
- stdout = `${JSON.stringify(only, null, 2)}
2296
- `;
2297
- }
2298
- } else {
2299
- stdout = records.map(formatHumanReadable).join("\n");
2300
- }
2301
- const stderr = missing.map((s) => `almanac: no such page "${s}"
2302
- `).join("");
2303
- return {
2304
- stdout,
2305
- stderr,
2306
- exitCode: missing.length > 0 ? 1 : 0
2307
- };
2308
- } finally {
2309
- db.close();
2310
- }
2311
- }
2312
- function fetchInfo(db, slug) {
2313
- const pageRow = db.prepare(
2314
- "SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?"
2315
- ).get(slug);
2316
- if (pageRow === void 0) return null;
2317
- const topics = db.prepare(
2318
- "SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug"
2319
- ).all(slug).map((r) => r.topic_slug);
2320
- const refs = db.prepare(
2321
- // Display the author's casing (`original_path`), not the
2322
- // lowercased lookup form. The lowercased `path` column is the
2323
- // query key for `--mentions`; it's not a user-facing string.
2324
- "SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path"
2325
- ).all(slug).map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));
2326
- const linksOut = db.prepare(
2327
- "SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug"
2328
- ).all(slug).map((r) => r.target_slug);
2329
- const linksIn = db.prepare(
2330
- "SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug"
2331
- ).all(slug).map((r) => r.source_slug);
2332
- const xwiki = db.prepare(
2333
- "SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug"
2334
- ).all(slug).map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));
2335
- const supersedesRows = db.prepare(
2336
- "SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug"
2337
- ).all(slug).map((r) => r.slug);
2338
- return {
2339
- slug: pageRow.slug,
2340
- title: pageRow.title,
2341
- file_path: pageRow.file_path,
2342
- updated_at: pageRow.updated_at,
2343
- archived_at: pageRow.archived_at,
2344
- superseded_by: pageRow.superseded_by,
2345
- supersedes: supersedesRows,
2346
- topics,
2347
- file_refs: refs,
2348
- wikilinks_out: linksOut,
2349
- wikilinks_in: linksIn,
2350
- cross_wiki_links: xwiki
2351
- };
2352
- }
2353
- function collectSlugs(options) {
2354
- if (options.stdin === true && options.stdinInput !== void 0) {
2355
- return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
2356
- }
2357
- if (options.slug !== void 0 && options.slug.length > 0) {
2358
- return [options.slug];
2359
- }
2360
- return [];
2361
- }
2362
- function formatHumanReadable(rec) {
2363
- const lines = [];
2364
- lines.push(`slug: ${rec.slug}`);
2365
- lines.push(`title: ${rec.title ?? "\u2014"}`);
2366
- lines.push(`file: ${rec.file_path}`);
2367
- lines.push(`updated_at: ${new Date(rec.updated_at * 1e3).toISOString()}`);
2368
- if (rec.archived_at !== null) {
2369
- lines.push(
2370
- `archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
2371
- );
2372
- }
2373
- if (rec.superseded_by !== null) {
2374
- lines.push(`superseded_by: ${rec.superseded_by}`);
2375
- }
2376
- if (rec.supersedes.length > 0) {
2377
- lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
2378
- }
2379
- lines.push(`topics: ${rec.topics.length > 0 ? rec.topics.join(", ") : "\u2014"}`);
2380
- lines.push("file_refs:");
2381
- if (rec.file_refs.length === 0) {
2382
- lines.push(" \u2014");
2383
- } else {
2384
- for (const r of rec.file_refs) {
2385
- lines.push(` ${r.path}${r.is_dir ? " (dir)" : ""}`);
2386
- }
2387
- }
2388
- lines.push("wikilinks_out:");
2389
- if (rec.wikilinks_out.length === 0) lines.push(" \u2014");
2390
- else for (const t of rec.wikilinks_out) lines.push(` ${t}`);
2391
- lines.push("wikilinks_in:");
2392
- if (rec.wikilinks_in.length === 0) lines.push(" \u2014");
2393
- else for (const s of rec.wikilinks_in) lines.push(` ${s}`);
2394
- if (rec.cross_wiki_links.length > 0) {
2395
- lines.push("cross_wiki_links:");
2396
- for (const x of rec.cross_wiki_links) {
2397
- lines.push(` ${x.wiki}:${x.target}`);
2398
- }
2399
- }
2400
- return `${lines.join("\n")}
2401
- `;
2402
- }
2403
-
2404
2256
  // src/commands/list.ts
2405
2257
  import { existsSync as existsSync11 } from "fs";
2406
2258
  async function listWikis(options) {
@@ -2453,61 +2305,6 @@ function formatPretty(entries) {
2453
2305
  `;
2454
2306
  }
2455
2307
 
2456
- // src/commands/path.ts
2457
- import { join as join10 } from "path";
2458
- async function runPath(options) {
2459
- const repoRoot = await resolveWikiRoot({
2460
- cwd: options.cwd,
2461
- wiki: options.wiki
2462
- });
2463
- await ensureFreshIndex({ repoRoot });
2464
- const dbPath = join10(repoRoot, ".almanac", "index.db");
2465
- const db = openIndex(dbPath);
2466
- try {
2467
- const slugs = collectSlugs2(options);
2468
- if (slugs.length === 0) {
2469
- return {
2470
- stdout: "",
2471
- stderr: "almanac: path requires a slug (or --stdin)\n",
2472
- exitCode: 1
2473
- };
2474
- }
2475
- const stmt = db.prepare(
2476
- "SELECT file_path FROM pages WHERE slug = ?"
2477
- );
2478
- const resolved = [];
2479
- const missing = [];
2480
- for (const slug of slugs) {
2481
- const row = stmt.get(slug);
2482
- if (row === void 0) {
2483
- missing.push(slug);
2484
- continue;
2485
- }
2486
- resolved.push(row.file_path);
2487
- }
2488
- const stdout = resolved.length > 0 ? `${resolved.join("\n")}
2489
- ` : "";
2490
- const stderr = missing.map((s) => `almanac: no such page "${s}"
2491
- `).join("");
2492
- return {
2493
- stdout,
2494
- stderr,
2495
- exitCode: missing.length > 0 ? 1 : 0
2496
- };
2497
- } finally {
2498
- db.close();
2499
- }
2500
- }
2501
- function collectSlugs2(options) {
2502
- if (options.stdin === true && options.stdinInput !== void 0) {
2503
- return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
2504
- }
2505
- if (options.slug !== void 0 && options.slug.length > 0) {
2506
- return [options.slug];
2507
- }
2508
- return [];
2509
- }
2510
-
2511
2308
  // src/commands/reindex.ts
2512
2309
  async function runReindex(options) {
2513
2310
  const repoRoot = await resolveWikiRoot({
@@ -2522,14 +2319,14 @@ async function runReindex(options) {
2522
2319
  }
2523
2320
 
2524
2321
  // src/commands/search.ts
2525
- import { join as join11 } from "path";
2322
+ import { join as join9 } from "path";
2526
2323
  async function runSearch(options) {
2527
2324
  const repoRoot = await resolveWikiRoot({
2528
2325
  cwd: options.cwd,
2529
2326
  wiki: options.wiki
2530
2327
  });
2531
2328
  await ensureFreshIndex({ repoRoot });
2532
- const dbPath = join11(repoRoot, ".almanac", "index.db");
2329
+ const dbPath = join9(repoRoot, ".almanac", "index.db");
2533
2330
  const db = openIndex(dbPath);
2534
2331
  try {
2535
2332
  const rows = executeQuery(db, options);
@@ -2697,19 +2494,313 @@ function buildStderr(rows, options) {
2697
2494
  return "";
2698
2495
  }
2699
2496
 
2497
+ // src/commands/setup.ts
2498
+ import { existsSync as existsSync12 } from "fs";
2499
+ import {
2500
+ copyFile,
2501
+ mkdir as mkdir5,
2502
+ readFile as readFile9,
2503
+ writeFile as writeFile5
2504
+ } from "fs/promises";
2505
+ import { createRequire as createRequire2 } from "module";
2506
+ import { homedir as homedir4 } from "os";
2507
+ import path3 from "path";
2508
+ import { fileURLToPath as fileURLToPath3 } from "url";
2509
+ var RST = "\x1B[0m";
2510
+ var BOLD = "\x1B[1m";
2511
+ var DIM = "\x1B[2m";
2512
+ var WHITE_BOLD = "\x1B[1;37m";
2513
+ var BLUE = "\x1B[38;5;75m";
2514
+ var BLUE_DIM = "\x1B[38;5;69m";
2515
+ var ACCENT_BG = "\x1B[48;5;252m\x1B[38;5;16m";
2516
+ var GRADIENT = [
2517
+ "\x1B[38;5;255m",
2518
+ "\x1B[38;5;253m",
2519
+ "\x1B[38;5;251m",
2520
+ "\x1B[38;5;249m",
2521
+ "\x1B[38;5;246m",
2522
+ "\x1B[38;5;243m"
2523
+ ];
2524
+ var LOGO_LINES = [
2525
+ " ___ ___ ___ ___ _ _ __ __ _ _ _ _ ___ ",
2526
+ " / __/ _ \\| \\| __| /_\\ | | | \\/ | /_\\ | \\| | /_\\ / __|",
2527
+ "| (_| (_) | |) | _| / _ \\| |__| |\\/| |/ _ \\| .` |/ _ \\ (__ ",
2528
+ " \\___\\___/|___/|___/_/ \\_\\____|_| |_/_/ \\_\\_|\\_/_/ \\_\\___|",
2529
+ " ",
2530
+ " a living wiki for codebases, for your agent "
2531
+ ];
2532
+ var BAR = ` ${DIM}\u2502${RST}`;
2533
+ function printBanner(out) {
2534
+ out.write("\n");
2535
+ for (let i = 0; i < LOGO_LINES.length; i++) {
2536
+ const color = GRADIENT[Math.min(i, GRADIENT.length - 1)] ?? "";
2537
+ out.write(`${color}${LOGO_LINES[i]}${RST}
2538
+ `);
2539
+ }
2540
+ out.write(`
2541
+ ${WHITE_BOLD} Install the hook + agent guides${RST}
2542
+ `);
2543
+ }
2544
+ function printBadge(out) {
2545
+ out.write(`
2546
+ ${ACCENT_BG} codealmanac ${RST}
2547
+
2548
+ `);
2549
+ }
2550
+ function stepDone(out, msg) {
2551
+ out.write(` ${BLUE}\u25C7${RST} ${msg}
2552
+ `);
2553
+ }
2554
+ function stepActive(out, msg) {
2555
+ out.write(` ${BLUE}\u25C6${RST} ${msg}
2556
+ `);
2557
+ }
2558
+ function stepSkipped(out, msg) {
2559
+ out.write(` ${DIM}\u25CB ${msg}${RST}
2560
+ `);
2561
+ }
2562
+ async function runSetup(options = {}) {
2563
+ const out = options.stdout ?? process.stdout;
2564
+ const isTTY = options.isTTY ?? process.stdin.isTTY === true;
2565
+ const interactive = isTTY && options.yes !== true;
2566
+ printBanner(out);
2567
+ printBadge(out);
2568
+ const auth = await safeCheckAuth(options.spawnCli);
2569
+ reportAuth(out, auth);
2570
+ out.write(BAR + "\n");
2571
+ let hookAction = "install";
2572
+ if (options.skipHook === true) {
2573
+ hookAction = "skip";
2574
+ } else if (interactive) {
2575
+ hookAction = await confirm(
2576
+ out,
2577
+ "Install the SessionEnd hook so capture runs at the end of every Claude Code session?",
2578
+ true
2579
+ );
2580
+ }
2581
+ let hookResultLine = "";
2582
+ if (hookAction === "install") {
2583
+ const res = await runHookInstall({
2584
+ settingsPath: options.settingsPath,
2585
+ hookScriptPath: options.hookScriptPath
2586
+ });
2587
+ if (res.exitCode !== 0) {
2588
+ stepActive(out, `SessionEnd hook: ${res.stderr.trim()}`);
2589
+ return {
2590
+ stdout: "",
2591
+ stderr: res.stderr,
2592
+ exitCode: res.exitCode
2593
+ };
2594
+ }
2595
+ hookResultLine = res.stdout.includes("already installed") ? `SessionEnd hook ${DIM}already installed${RST}` : `SessionEnd hook installed`;
2596
+ stepDone(out, hookResultLine);
2597
+ } else {
2598
+ stepSkipped(out, `SessionEnd hook ${DIM}skipped${RST}`);
2599
+ }
2600
+ out.write(BAR + "\n");
2601
+ let guidesAction = "install";
2602
+ if (options.skipGuides === true) {
2603
+ guidesAction = "skip";
2604
+ } else if (interactive) {
2605
+ guidesAction = await confirm(
2606
+ out,
2607
+ "Install the codealmanac usage guides into ~/.claude/ and import them from CLAUDE.md?",
2608
+ true
2609
+ );
2610
+ }
2611
+ let guidesSummary;
2612
+ if (guidesAction === "install") {
2613
+ try {
2614
+ const summary = await installGuides({
2615
+ claudeDir: options.claudeDir ?? path3.join(homedir4(), ".claude"),
2616
+ guidesDir: options.guidesDir ?? resolveGuidesDir()
2617
+ });
2618
+ guidesSummary = summary.anyChanges ? `Guides installed (${summary.filesWritten.join(", ")})` : `Guides ${DIM}already installed${RST}`;
2619
+ stepDone(out, guidesSummary);
2620
+ } catch (err) {
2621
+ const msg = err instanceof Error ? err.message : String(err);
2622
+ return {
2623
+ stdout: "",
2624
+ stderr: `almanac: guide install failed: ${msg}
2625
+ `,
2626
+ exitCode: 1
2627
+ };
2628
+ }
2629
+ } else {
2630
+ stepSkipped(out, `Guides ${DIM}skipped${RST}`);
2631
+ }
2632
+ out.write(BAR + "\n");
2633
+ stepDone(out, `${BLUE}Setup complete${RST}`);
2634
+ out.write("\n");
2635
+ printNextSteps(out);
2636
+ return { stdout: "", stderr: "", exitCode: 0 };
2637
+ }
2638
+ async function safeCheckAuth(spawnCli) {
2639
+ try {
2640
+ return await checkClaudeAuth(spawnCli);
2641
+ } catch {
2642
+ return { loggedIn: false };
2643
+ }
2644
+ }
2645
+ function reportAuth(out, auth) {
2646
+ if (auth.loggedIn) {
2647
+ const who = auth.email ?? "Claude account";
2648
+ const plan = auth.subscriptionType !== void 0 ? ` ${DIM}(${auth.subscriptionType})${RST}` : "";
2649
+ stepDone(out, `Claude auth: ${WHITE_BOLD}${who}${RST}${plan}`);
2650
+ return;
2651
+ }
2652
+ if (process.env.ANTHROPIC_API_KEY !== void 0 && process.env.ANTHROPIC_API_KEY.length > 0) {
2653
+ stepDone(out, `Claude auth: ${WHITE_BOLD}ANTHROPIC_API_KEY${RST} set`);
2654
+ return;
2655
+ }
2656
+ stepActive(out, `Claude auth: ${DIM}not signed in${RST}`);
2657
+ for (const line of UNAUTHENTICATED_MESSAGE.split("\n")) {
2658
+ out.write(` ${DIM}\u2502 ${line}${RST}
2659
+ `);
2660
+ }
2661
+ }
2662
+ async function installGuides(options) {
2663
+ await mkdir5(options.claudeDir, { recursive: true });
2664
+ const srcMini = path3.join(options.guidesDir, "mini.md");
2665
+ const srcRef = path3.join(options.guidesDir, "reference.md");
2666
+ if (!existsSync12(srcMini)) {
2667
+ throw new Error(`missing bundled guide: ${srcMini}`);
2668
+ }
2669
+ if (!existsSync12(srcRef)) {
2670
+ throw new Error(`missing bundled guide: ${srcRef}`);
2671
+ }
2672
+ const destMini = path3.join(options.claudeDir, "codealmanac.md");
2673
+ const destRef = path3.join(options.claudeDir, "codealmanac-reference.md");
2674
+ const miniChanged = await copyIfChanged(srcMini, destMini);
2675
+ const refChanged = await copyIfChanged(srcRef, destRef);
2676
+ const claudeMd = path3.join(options.claudeDir, "CLAUDE.md");
2677
+ const importChanged = await ensureImport(claudeMd);
2678
+ const filesWritten = [];
2679
+ if (miniChanged) filesWritten.push("codealmanac.md");
2680
+ if (refChanged) filesWritten.push("codealmanac-reference.md");
2681
+ if (importChanged) filesWritten.push("CLAUDE.md");
2682
+ return { anyChanges: filesWritten.length > 0, filesWritten };
2683
+ }
2684
+ async function copyIfChanged(src, dest) {
2685
+ const srcBytes = await readFile9(src);
2686
+ if (existsSync12(dest)) {
2687
+ try {
2688
+ const destBytes = await readFile9(dest);
2689
+ if (srcBytes.equals(destBytes)) return false;
2690
+ } catch {
2691
+ }
2692
+ }
2693
+ await copyFile(src, dest);
2694
+ return true;
2695
+ }
2696
+ var IMPORT_LINE = "@~/.claude/codealmanac.md";
2697
+ async function ensureImport(claudeMdPath) {
2698
+ let existing = "";
2699
+ if (existsSync12(claudeMdPath)) {
2700
+ existing = await readFile9(claudeMdPath, "utf8");
2701
+ }
2702
+ if (hasImportLine(existing)) return false;
2703
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
2704
+ const body = `${existing}${sep}${IMPORT_LINE}
2705
+ `;
2706
+ await writeFile5(claudeMdPath, body, "utf8");
2707
+ return true;
2708
+ }
2709
+ function hasImportLine(contents) {
2710
+ const lines = contents.split(/\r?\n/).map((l) => l.trim());
2711
+ return lines.includes(IMPORT_LINE);
2712
+ }
2713
+ function confirm(out, question, defaultYes) {
2714
+ return new Promise((resolve2) => {
2715
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
2716
+ out.write(` ${BLUE}\u25C6${RST} ${question} ${DIM}${hint}${RST} `);
2717
+ let buf = "";
2718
+ const onData = (chunk) => {
2719
+ buf += chunk.toString("utf8");
2720
+ const nl = buf.indexOf("\n");
2721
+ if (nl === -1) return;
2722
+ process.stdin.removeListener("data", onData);
2723
+ process.stdin.pause();
2724
+ const answer = buf.slice(0, nl).trim().toLowerCase();
2725
+ const accepted = answer.length === 0 ? defaultYes : answer === "y" || answer === "yes";
2726
+ resolve2(accepted ? "install" : "skip");
2727
+ };
2728
+ process.stdin.resume();
2729
+ process.stdin.on("data", onData);
2730
+ });
2731
+ }
2732
+ function printNextSteps(out) {
2733
+ const innerW = 62;
2734
+ const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
2735
+ const row = (content) => {
2736
+ const padding = Math.max(0, innerW - vis(content));
2737
+ return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}
2738
+ `;
2739
+ };
2740
+ const empty = row("");
2741
+ out.write(` ${BLUE_DIM}\u256D${"\u2500".repeat(innerW)}\u256E${RST}
2742
+ `);
2743
+ out.write(empty);
2744
+ out.write(row(` ${WHITE_BOLD}Next steps${RST}`));
2745
+ out.write(empty);
2746
+ out.write(
2747
+ row(` ${BLUE}1.${RST} ${BOLD}cd${RST} into a repo you want to document`)
2748
+ );
2749
+ out.write(
2750
+ row(
2751
+ ` ${BLUE}2.${RST} ${BOLD}almanac bootstrap${RST} ${DIM}# scaffold the wiki${RST}`
2752
+ )
2753
+ );
2754
+ out.write(
2755
+ row(
2756
+ ` ${BLUE}3.${RST} Work normally \u2014 capture runs on session end`
2757
+ )
2758
+ );
2759
+ out.write(empty);
2760
+ out.write(` ${BLUE_DIM}\u2570${"\u2500".repeat(innerW)}\u256F${RST}
2761
+
2762
+ `);
2763
+ }
2764
+ function resolveGuidesDir() {
2765
+ const here = path3.dirname(fileURLToPath3(import.meta.url));
2766
+ const candidates = [
2767
+ path3.resolve(here, "..", "guides"),
2768
+ // dist layout
2769
+ path3.resolve(here, "..", "..", "guides"),
2770
+ // src layout
2771
+ path3.resolve(here, "..", "..", "..", "guides")
2772
+ ];
2773
+ for (const dir of candidates) {
2774
+ if (looksLikeGuidesDir(dir)) return dir;
2775
+ }
2776
+ try {
2777
+ const require2 = createRequire2(import.meta.url);
2778
+ const pkgJson = require2.resolve("codealmanac/package.json");
2779
+ const guides = path3.join(path3.dirname(pkgJson), "guides");
2780
+ if (looksLikeGuidesDir(guides)) return guides;
2781
+ } catch {
2782
+ }
2783
+ throw new Error(
2784
+ "could not locate bundled guides/ directory. Tried:\n" + candidates.map((c) => ` - ${c}`).join("\n")
2785
+ );
2786
+ }
2787
+ function looksLikeGuidesDir(dir) {
2788
+ return existsSync12(path3.join(dir, "mini.md"));
2789
+ }
2790
+
2700
2791
  // src/commands/show.ts
2701
- import { readFile as readFile9 } from "fs/promises";
2702
- import { join as join12 } from "path";
2792
+ import { readFile as readFile10 } from "fs/promises";
2793
+ import { join as join10 } from "path";
2703
2794
  async function runShow(options) {
2704
2795
  const repoRoot = await resolveWikiRoot({
2705
2796
  cwd: options.cwd,
2706
2797
  wiki: options.wiki
2707
2798
  });
2708
2799
  await ensureFreshIndex({ repoRoot });
2709
- const dbPath = join12(repoRoot, ".almanac", "index.db");
2800
+ const dbPath = join10(repoRoot, ".almanac", "index.db");
2710
2801
  const db = openIndex(dbPath);
2711
2802
  try {
2712
- const slugs = collectSlugs3(options);
2803
+ const slugs = collectSlugs(options);
2713
2804
  if (slugs.length === 0) {
2714
2805
  return {
2715
2806
  stdout: "",
@@ -2717,32 +2808,18 @@ async function runShow(options) {
2717
2808
  exitCode: 1
2718
2809
  };
2719
2810
  }
2720
- const stmt = db.prepare(
2721
- "SELECT file_path FROM pages WHERE slug = ?"
2722
- );
2723
2811
  const records = [];
2724
2812
  const missing = [];
2725
2813
  for (const slug of slugs) {
2726
- const row = stmt.get(slug);
2727
- if (row === void 0) {
2814
+ const rec = await fetchRecord(db, slug);
2815
+ if (rec === null) {
2728
2816
  missing.push(slug);
2729
2817
  continue;
2730
2818
  }
2731
- try {
2732
- records.push({ slug, content: await readFile9(row.file_path, "utf8") });
2733
- } catch (err) {
2734
- const message = err instanceof Error ? err.message : String(err);
2735
- missing.push(`${slug} (${message})`);
2736
- }
2819
+ records.push(rec);
2737
2820
  }
2738
2821
  const bulk = options.stdin === true;
2739
- let stdout;
2740
- if (bulk) {
2741
- stdout = records.map((r) => JSON.stringify(r)).join("\n");
2742
- if (stdout.length > 0) stdout += "\n";
2743
- } else {
2744
- stdout = records.map((r) => r.content).join("");
2745
- }
2822
+ const stdout = bulk ? formatBulk(records) : formatSingle(records, options);
2746
2823
  const stderr = missing.map((s) => `almanac: no such page "${s}"
2747
2824
  `).join("");
2748
2825
  return {
@@ -2754,7 +2831,266 @@ async function runShow(options) {
2754
2831
  db.close();
2755
2832
  }
2756
2833
  }
2757
- function collectSlugs3(options) {
2834
+ async function fetchRecord(db, slug) {
2835
+ const pageRow = db.prepare(
2836
+ "SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?"
2837
+ ).get(slug);
2838
+ if (pageRow === void 0) return null;
2839
+ const topics = db.prepare(
2840
+ "SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug"
2841
+ ).all(slug).map((r) => r.topic_slug);
2842
+ const refs = db.prepare(
2843
+ "SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path"
2844
+ ).all(slug).map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));
2845
+ const linksOut = db.prepare(
2846
+ "SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug"
2847
+ ).all(slug).map((r) => r.target_slug);
2848
+ const linksIn = db.prepare(
2849
+ "SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug"
2850
+ ).all(slug).map((r) => r.source_slug);
2851
+ const xwiki = db.prepare(
2852
+ "SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug"
2853
+ ).all(slug).map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));
2854
+ const supersedesRows = db.prepare(
2855
+ "SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug"
2856
+ ).all(slug).map((r) => r.slug);
2857
+ let body = "";
2858
+ try {
2859
+ body = stripFrontmatter(await readFile10(pageRow.file_path, "utf8"));
2860
+ } catch {
2861
+ }
2862
+ return {
2863
+ slug: pageRow.slug,
2864
+ title: pageRow.title,
2865
+ file_path: pageRow.file_path,
2866
+ updated_at: pageRow.updated_at,
2867
+ archived_at: pageRow.archived_at,
2868
+ superseded_by: pageRow.superseded_by,
2869
+ supersedes: supersedesRows,
2870
+ topics,
2871
+ file_refs: refs,
2872
+ wikilinks_out: linksOut,
2873
+ wikilinks_in: linksIn,
2874
+ cross_wiki_links: xwiki,
2875
+ body
2876
+ };
2877
+ }
2878
+ function formatBulk(records) {
2879
+ if (records.length === 0) return "";
2880
+ return records.map((r) => JSON.stringify(r)).join("\n") + "\n";
2881
+ }
2882
+ function formatSingle(records, options) {
2883
+ if (options.json === true) {
2884
+ const only = records[0] ?? null;
2885
+ return `${JSON.stringify(only, null, 2)}
2886
+ `;
2887
+ }
2888
+ return records.map((r) => formatRecord(r, options)).join("");
2889
+ }
2890
+ var FIELD_ORDER = [
2891
+ "title",
2892
+ "topics",
2893
+ "files",
2894
+ "links",
2895
+ "backlinks",
2896
+ "xwiki",
2897
+ "lineage",
2898
+ "updated",
2899
+ "path"
2900
+ ];
2901
+ function selectedFields(options) {
2902
+ const selected = [];
2903
+ for (const f of FIELD_ORDER) {
2904
+ if (options[f] === true) selected.push(f);
2905
+ }
2906
+ return selected;
2907
+ }
2908
+ function formatRecord(rec, options) {
2909
+ if (options.raw === true) {
2910
+ return rec.body;
2911
+ }
2912
+ const fields = selectedFields(options);
2913
+ if (fields.length > 0) {
2914
+ if (fields.length === 1) {
2915
+ return bareField(rec, fields[0]);
2916
+ }
2917
+ return labeledFields(rec, fields);
2918
+ }
2919
+ if (options.meta === true) {
2920
+ return metadataHeader(rec) + "\n";
2921
+ }
2922
+ if (options.lead === true) {
2923
+ return firstParagraph(rec.body) + "\n";
2924
+ }
2925
+ const header = metadataHeader(rec);
2926
+ const body = rec.body;
2927
+ const sep = body.length > 0 ? "\n\n---\n\n" : "\n";
2928
+ return header + sep + body;
2929
+ }
2930
+ function bareField(rec, field) {
2931
+ switch (field) {
2932
+ case "title":
2933
+ return (rec.title ?? "") + "\n";
2934
+ case "topics":
2935
+ return rec.topics.map((t) => `${t}
2936
+ `).join("");
2937
+ case "files":
2938
+ return rec.file_refs.map((r) => `${r.path}
2939
+ `).join("");
2940
+ case "links":
2941
+ return rec.wikilinks_out.map((t) => `${t}
2942
+ `).join("");
2943
+ case "backlinks":
2944
+ return rec.wikilinks_in.map((t) => `${t}
2945
+ `).join("");
2946
+ case "xwiki":
2947
+ return rec.cross_wiki_links.map((x) => `${x.wiki}:${x.target}
2948
+ `).join("");
2949
+ case "lineage": {
2950
+ const lines = [];
2951
+ if (rec.archived_at !== null) {
2952
+ lines.push(
2953
+ `archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
2954
+ );
2955
+ }
2956
+ if (rec.superseded_by !== null) {
2957
+ lines.push(`superseded_by: ${rec.superseded_by}`);
2958
+ }
2959
+ if (rec.supersedes.length > 0) {
2960
+ lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
2961
+ }
2962
+ return lines.length > 0 ? `${lines.join("\n")}
2963
+ ` : "";
2964
+ }
2965
+ case "updated":
2966
+ return `${new Date(rec.updated_at * 1e3).toISOString()}
2967
+ `;
2968
+ case "path":
2969
+ return `${rec.file_path}
2970
+ `;
2971
+ }
2972
+ }
2973
+ function labeledFields(rec, fields) {
2974
+ const parts = [];
2975
+ for (const f of fields) {
2976
+ parts.push(labeledSection(rec, f));
2977
+ }
2978
+ return parts.join("\n") + (parts.length > 0 ? "" : "");
2979
+ }
2980
+ function labeledSection(rec, field) {
2981
+ switch (field) {
2982
+ case "title":
2983
+ return `title: ${rec.title ?? "\u2014"}
2984
+ `;
2985
+ case "topics":
2986
+ return rec.topics.length > 0 ? `topics: ${rec.topics.join(", ")}
2987
+ ` : `topics: \u2014
2988
+ `;
2989
+ case "files":
2990
+ return formatListSection(
2991
+ "files",
2992
+ rec.file_refs.map((r) => `${r.path}`)
2993
+ );
2994
+ case "links":
2995
+ return formatListSection("links", rec.wikilinks_out);
2996
+ case "backlinks":
2997
+ return formatListSection("backlinks", rec.wikilinks_in);
2998
+ case "xwiki":
2999
+ return formatListSection(
3000
+ "xwiki",
3001
+ rec.cross_wiki_links.map((x) => `${x.wiki}:${x.target}`)
3002
+ );
3003
+ case "lineage": {
3004
+ const lines = ["lineage:"];
3005
+ if (rec.archived_at !== null) {
3006
+ lines.push(
3007
+ ` archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
3008
+ );
3009
+ }
3010
+ if (rec.superseded_by !== null) {
3011
+ lines.push(` superseded_by: ${rec.superseded_by}`);
3012
+ }
3013
+ if (rec.supersedes.length > 0) {
3014
+ lines.push(` supersedes: ${rec.supersedes.join(", ")}`);
3015
+ }
3016
+ if (lines.length === 1) lines.push(" \u2014");
3017
+ return lines.join("\n") + "\n";
3018
+ }
3019
+ case "updated":
3020
+ return `updated: ${new Date(rec.updated_at * 1e3).toISOString()}
3021
+ `;
3022
+ case "path":
3023
+ return `path: ${rec.file_path}
3024
+ `;
3025
+ }
3026
+ }
3027
+ function formatListSection(label, items) {
3028
+ if (items.length === 0) return `${label}: \u2014
3029
+ `;
3030
+ if (items.length <= 3) return `${label}: ${items.join(", ")}
3031
+ `;
3032
+ return `${label}:
3033
+ ${items.map((i) => ` ${i}`).join("\n")}
3034
+ `;
3035
+ }
3036
+ function metadataHeader(rec) {
3037
+ const lines = [];
3038
+ lines.push(`slug: ${rec.slug}`);
3039
+ lines.push(`title: ${rec.title ?? "\u2014"}`);
3040
+ lines.push(
3041
+ `topics: ${rec.topics.length > 0 ? rec.topics.join(", ") : "\u2014"}`
3042
+ );
3043
+ if (rec.file_refs.length > 0) {
3044
+ const parts = rec.file_refs.map(
3045
+ (r) => `${r.path}`
3046
+ );
3047
+ lines.push(`files: ${parts.join(", ")}`);
3048
+ }
3049
+ lines.push(
3050
+ `updated: ${new Date(rec.updated_at * 1e3).toISOString()}`
3051
+ );
3052
+ if (rec.wikilinks_out.length > 0) {
3053
+ lines.push(`links: ${rec.wikilinks_out.join(", ")}`);
3054
+ }
3055
+ if (rec.wikilinks_in.length > 0) {
3056
+ lines.push(`backlinks: ${rec.wikilinks_in.join(", ")}`);
3057
+ }
3058
+ if (rec.cross_wiki_links.length > 0) {
3059
+ lines.push(
3060
+ `xwiki: ${rec.cross_wiki_links.map((x) => `${x.wiki}:${x.target}`).join(", ")}`
3061
+ );
3062
+ }
3063
+ if (rec.archived_at !== null) {
3064
+ lines.push(
3065
+ `archived: ${new Date(rec.archived_at * 1e3).toISOString()}`
3066
+ );
3067
+ }
3068
+ if (rec.superseded_by !== null) {
3069
+ lines.push(`superseded_by: ${rec.superseded_by}`);
3070
+ }
3071
+ if (rec.supersedes.length > 0) {
3072
+ lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
3073
+ }
3074
+ return lines.join("\n");
3075
+ }
3076
+ function stripFrontmatter(src) {
3077
+ if (!src.startsWith("---\n") && !src.startsWith("---\r\n")) return src;
3078
+ const rest = src.slice(src.indexOf("\n") + 1);
3079
+ const endMatch = rest.match(/^---[ \t]*\r?\n/m);
3080
+ if (endMatch === null || endMatch.index === void 0) return src;
3081
+ return rest.slice(endMatch.index + endMatch[0].length);
3082
+ }
3083
+ function firstParagraph(body) {
3084
+ let src = body.trimStart();
3085
+ if (src.startsWith("# ")) {
3086
+ const nl = src.indexOf("\n");
3087
+ src = nl === -1 ? "" : src.slice(nl + 1).trimStart();
3088
+ }
3089
+ const blank = src.search(/\n[ \t]*\n/);
3090
+ if (blank === -1) return src.trimEnd();
3091
+ return src.slice(0, blank).trimEnd();
3092
+ }
3093
+ function collectSlugs(options) {
2758
3094
  if (options.stdin === true && options.stdinInput !== void 0) {
2759
3095
  return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
2760
3096
  }
@@ -2765,17 +3101,17 @@ function collectSlugs3(options) {
2765
3101
  }
2766
3102
 
2767
3103
  // src/topics/frontmatterRewrite.ts
2768
- import { readFile as readFile10, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3104
+ import { readFile as readFile11, rename as rename4, writeFile as writeFile6 } from "fs/promises";
2769
3105
  import yaml3 from "js-yaml";
2770
3106
  async function rewritePageTopics(filePath, transform) {
2771
- const raw = await readFile10(filePath, "utf8");
3107
+ const raw = await readFile11(filePath, "utf8");
2772
3108
  const { before, after, output, changed } = applyTopicsTransform(
2773
3109
  raw,
2774
3110
  transform
2775
3111
  );
2776
3112
  if (changed) {
2777
3113
  const tmp = `${filePath}.tmp`;
2778
- await writeFile5(tmp, output, "utf8");
3114
+ await writeFile6(tmp, output, "utf8");
2779
3115
  await rename4(tmp, filePath);
2780
3116
  }
2781
3117
  return { before, after, changed };
@@ -2982,12 +3318,12 @@ function arraysEqual(a, b) {
2982
3318
  }
2983
3319
 
2984
3320
  // src/topics/paths.ts
2985
- import { join as join13 } from "path";
3321
+ import { join as join11 } from "path";
2986
3322
  function topicsYamlPath(repoRoot) {
2987
- return join13(repoRoot, ".almanac", "topics.yaml");
3323
+ return join11(repoRoot, ".almanac", "topics.yaml");
2988
3324
  }
2989
3325
  function indexDbPath(repoRoot) {
2990
- return join13(repoRoot, ".almanac", "index.db");
3326
+ return join11(repoRoot, ".almanac", "index.db");
2991
3327
  }
2992
3328
 
2993
3329
  // src/commands/tag.ts
@@ -3146,8 +3482,8 @@ async function runUntag(options) {
3146
3482
  }
3147
3483
 
3148
3484
  // src/commands/topics.ts
3149
- import { readFile as readFile11 } from "fs/promises";
3150
- import { join as join14 } from "path";
3485
+ import { readFile as readFile12 } from "fs/promises";
3486
+ import { join as join12 } from "path";
3151
3487
  import fg3 from "fast-glob";
3152
3488
  async function runTopicsList(options) {
3153
3489
  const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
@@ -3619,7 +3955,7 @@ async function runTopicsDescribe(options) {
3619
3955
  };
3620
3956
  }
3621
3957
  async function rewriteTopicOnPages(repoRoot, transform) {
3622
- const pagesDir = join14(repoRoot, ".almanac", "pages");
3958
+ const pagesDir = join12(repoRoot, ".almanac", "pages");
3623
3959
  const files = await fg3("**/*.md", {
3624
3960
  cwd: pagesDir,
3625
3961
  absolute: true,
@@ -3627,7 +3963,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
3627
3963
  });
3628
3964
  let changed = 0;
3629
3965
  for (const filePath of files) {
3630
- const raw = await readFile11(filePath, "utf8");
3966
+ const raw = await readFile12(filePath, "utf8");
3631
3967
  const applied = applyTopicsTransform(raw, transform);
3632
3968
  if (!applied.changed) continue;
3633
3969
  await rewritePageTopics(filePath, transform);
@@ -3636,14 +3972,146 @@ async function rewriteTopicOnPages(repoRoot, transform) {
3636
3972
  return changed;
3637
3973
  }
3638
3974
 
3975
+ // src/commands/uninstall.ts
3976
+ import { existsSync as existsSync13 } from "fs";
3977
+ import { readFile as readFile13, rm, writeFile as writeFile7 } from "fs/promises";
3978
+ import { homedir as homedir5 } from "os";
3979
+ import path4 from "path";
3980
+ var BLUE2 = "\x1B[38;5;75m";
3981
+ var DIM2 = "\x1B[2m";
3982
+ var RST2 = "\x1B[0m";
3983
+ async function runUninstall(options = {}) {
3984
+ const out = options.stdout ?? process.stdout;
3985
+ const isTTY = options.isTTY ?? process.stdin.isTTY === true;
3986
+ const interactive = isTTY && options.yes !== true;
3987
+ const claudeDir = options.claudeDir ?? path4.join(homedir5(), ".claude");
3988
+ out.write("\n");
3989
+ let removeHook = true;
3990
+ if (options.keepHook === true) {
3991
+ removeHook = false;
3992
+ } else if (interactive) {
3993
+ removeHook = await confirm2(
3994
+ out,
3995
+ "Remove the SessionEnd hook from ~/.claude/settings.json?",
3996
+ true
3997
+ );
3998
+ }
3999
+ if (removeHook) {
4000
+ const res = await runHookUninstall({
4001
+ settingsPath: options.settingsPath,
4002
+ hookScriptPath: options.hookScriptPath
4003
+ });
4004
+ if (res.exitCode !== 0) {
4005
+ return { stdout: "", stderr: res.stderr, exitCode: res.exitCode };
4006
+ }
4007
+ out.write(` ${BLUE2}\u25C7${RST2} ${res.stdout.trim()}
4008
+ `);
4009
+ } else {
4010
+ out.write(` ${DIM2}\u25CB Hook kept${RST2}
4011
+ `);
4012
+ }
4013
+ let removeGuides = true;
4014
+ if (options.keepGuides === true) {
4015
+ removeGuides = false;
4016
+ } else if (interactive) {
4017
+ removeGuides = await confirm2(
4018
+ out,
4019
+ "Remove the guides + CLAUDE.md import line?",
4020
+ true
4021
+ );
4022
+ }
4023
+ if (removeGuides) {
4024
+ const summary = await removeGuideFiles(claudeDir);
4025
+ if (summary.anyChanges) {
4026
+ out.write(
4027
+ ` ${BLUE2}\u25C7${RST2} Guides removed (${summary.filesTouched.join(", ")})
4028
+ `
4029
+ );
4030
+ } else {
4031
+ out.write(` ${DIM2}\u25CB Guides not installed${RST2}
4032
+ `);
4033
+ }
4034
+ } else {
4035
+ out.write(` ${DIM2}\u25CB Guides kept${RST2}
4036
+ `);
4037
+ }
4038
+ out.write(`
4039
+ ${BLUE2}\u25C7${RST2} ${BLUE2}Uninstall complete${RST2}
4040
+
4041
+ `);
4042
+ return { stdout: "", stderr: "", exitCode: 0 };
4043
+ }
4044
+ async function removeGuideFiles(claudeDir) {
4045
+ const touched = [];
4046
+ const mini = path4.join(claudeDir, "codealmanac.md");
4047
+ const ref = path4.join(claudeDir, "codealmanac-reference.md");
4048
+ const claudeMd = path4.join(claudeDir, "CLAUDE.md");
4049
+ if (existsSync13(mini)) {
4050
+ await rm(mini, { force: true });
4051
+ touched.push("codealmanac.md");
4052
+ }
4053
+ if (existsSync13(ref)) {
4054
+ await rm(ref, { force: true });
4055
+ touched.push("codealmanac-reference.md");
4056
+ }
4057
+ if (existsSync13(claudeMd)) {
4058
+ const existing = await readFile13(claudeMd, "utf8");
4059
+ const { changed, body } = removeImportLine(existing);
4060
+ if (changed) {
4061
+ if (body.trim().length === 0) {
4062
+ await rm(claudeMd, { force: true });
4063
+ touched.push("CLAUDE.md (deleted)");
4064
+ } else {
4065
+ await writeFile7(claudeMd, body, "utf8");
4066
+ touched.push("CLAUDE.md");
4067
+ }
4068
+ }
4069
+ }
4070
+ return { anyChanges: touched.length > 0, filesTouched: touched };
4071
+ }
4072
+ function removeImportLine(contents) {
4073
+ const eol = contents.includes("\r\n") ? "\r\n" : "\n";
4074
+ const lines = contents.split(/\r?\n/);
4075
+ const indices = [];
4076
+ for (let i = 0; i < lines.length; i++) {
4077
+ if (lines[i].trim() === IMPORT_LINE) indices.push(i);
4078
+ }
4079
+ if (indices.length === 0) return { changed: false, body: contents };
4080
+ for (let i = indices.length - 1; i >= 0; i--) {
4081
+ lines.splice(indices[i], 1);
4082
+ }
4083
+ let body = lines.join(eol);
4084
+ body = body.replace(/\n\n\n+/g, "\n\n");
4085
+ return { changed: true, body };
4086
+ }
4087
+ function confirm2(out, question, defaultYes) {
4088
+ return new Promise((resolve2) => {
4089
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
4090
+ out.write(` ${BLUE2}\u25C6${RST2} ${question} ${DIM2}${hint}${RST2} `);
4091
+ let buf = "";
4092
+ const onData = (chunk) => {
4093
+ buf += chunk.toString("utf8");
4094
+ const nl = buf.indexOf("\n");
4095
+ if (nl === -1) return;
4096
+ process.stdin.removeListener("data", onData);
4097
+ process.stdin.pause();
4098
+ const answer = buf.slice(0, nl).trim().toLowerCase();
4099
+ const accepted = answer.length === 0 ? defaultYes : answer === "y" || answer === "yes";
4100
+ resolve2(accepted);
4101
+ };
4102
+ process.stdin.resume();
4103
+ process.stdin.on("data", onData);
4104
+ });
4105
+ }
4106
+
3639
4107
  // src/registry/autoregister.ts
3640
- import { existsSync as existsSync12 } from "fs";
4108
+ import { existsSync as existsSync14 } from "fs";
3641
4109
  import { basename as basename5 } from "path";
3642
4110
  async function autoRegisterIfNeeded(cwd) {
3643
4111
  try {
3644
4112
  const repoRoot = findNearestAlmanacDir(cwd);
3645
4113
  if (repoRoot === null) return null;
3646
- if (!existsSync12(repoRoot)) return null;
4114
+ if (!existsSync14(repoRoot)) return null;
3647
4115
  const entries = await readRegistry();
3648
4116
  const existing = entries.find((e) => samePath(e.path, repoRoot));
3649
4117
  if (existing !== void 0) return existing;
@@ -3688,38 +4156,20 @@ function samePath(a, b) {
3688
4156
 
3689
4157
  // src/cli.ts
3690
4158
  async function run(argv) {
3691
- const program = new Command();
3692
4159
  const invoked = argv[1] !== void 0 ? basename6(argv[1]) : "almanac";
3693
4160
  const programName = invoked === "codealmanac" ? "codealmanac" : "almanac";
4161
+ const program = new Command();
3694
4162
  program.name(programName).description(
3695
4163
  "codealmanac \u2014 a living wiki for codebases, maintained by AI agents"
3696
- ).version("0.1.0", "-v, --version", "print version");
3697
- program.command("init").description("scaffold .almanac/ in the current directory and register it").option("--name <name>", "wiki name (defaults to the directory name)").option("--description <text>", "one-line description of this wiki").action(async (opts) => {
3698
- const result = await initWiki({
3699
- cwd: process.cwd(),
3700
- name: opts.name,
3701
- description: opts.description
3702
- });
3703
- const verb = result.created ? "initialized" : "updated";
3704
- process.stdout.write(
3705
- `${verb} wiki "${result.entry.name}" at ${result.almanacDir}
3706
- `
3707
- );
3708
- });
3709
- program.command("list").description("list registered wikis").option("--json", "emit structured JSON").option(
3710
- "--drop <name>",
3711
- "remove a wiki from the registry (the only way entries are ever removed)"
3712
- ).action(async (opts) => {
3713
- if (opts.drop === void 0) {
3714
- await autoRegisterIfNeeded(process.cwd());
3715
- }
3716
- const result = await listWikis(opts);
3717
- process.stdout.write(result.stdout);
3718
- if (result.exitCode !== 0) {
3719
- process.exitCode = result.exitCode;
3720
- }
3721
- });
3722
- program.command("search [query]").description("query pages by text, topic, file mentions, or freshness").option(
4164
+ ).version(readPackageVersion(), "-v, --version", "print version");
4165
+ if (programName === "codealmanac" && argv.length === 2) {
4166
+ const result = await runSetup({});
4167
+ if (result.stderr.length > 0) process.stderr.write(result.stderr);
4168
+ if (result.stdout.length > 0) process.stdout.write(result.stdout);
4169
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
4170
+ return;
4171
+ }
4172
+ program.command("search [query]").description("find pages by text, topic, file mentions, freshness").option(
3723
4173
  "--topic <name...>",
3724
4174
  "filter by topic (repeat for intersection)",
3725
4175
  collectOption,
@@ -3750,12 +4200,10 @@ async function run(argv) {
3750
4200
  json: opts.json,
3751
4201
  limit: opts.limit
3752
4202
  });
3753
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3754
- process.stdout.write(result.stdout);
3755
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4203
+ emit(result);
3756
4204
  }
3757
4205
  );
3758
- program.command("show [slug]").description("print the markdown content of a page").option("--stdin", "read slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").action(
4206
+ program.command("show [slug]").description("print a page (metadata + body; flags to narrow)").option("--stdin", "read slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").option("--json", "structured JSON (overrides other view/field flags)").option("--raw", "body only (alias: --body)").option("--body", "body only (alias: --raw)").option("--meta", "metadata only, no body").option("--lead", "first paragraph of the body only").option("--title", "print title").option("--topics", "print topics").option("--files", "print file refs").option("--links", "print outgoing wikilinks").option("--backlinks", "print incoming wikilinks").option("--xwiki", "print cross-wiki links").option("--lineage", "print archived_at / supersedes / superseded_by").option("--updated", "print updated timestamp").option("--path", "print absolute file path").action(
3759
4207
  async (slug, opts) => {
3760
4208
  await autoRegisterIfNeeded(process.cwd());
3761
4209
  const result = await runShow({
@@ -3763,54 +4211,85 @@ async function run(argv) {
3763
4211
  slug,
3764
4212
  stdin: opts.stdin,
3765
4213
  stdinInput: opts.stdin === true ? await readStdin() : void 0,
3766
- wiki: opts.wiki
4214
+ wiki: opts.wiki,
4215
+ json: opts.json,
4216
+ // `--body` is a surface alias for `--raw`. Fold it into one
4217
+ // field at the CLI boundary so the implementation never has
4218
+ // to care which spelling the user typed.
4219
+ raw: opts.raw === true || opts.body === true,
4220
+ meta: opts.meta,
4221
+ lead: opts.lead,
4222
+ title: opts.title,
4223
+ topics: opts.topics,
4224
+ files: opts.files,
4225
+ links: opts.links,
4226
+ backlinks: opts.backlinks,
4227
+ xwiki: opts.xwiki,
4228
+ lineage: opts.lineage,
4229
+ updated: opts.updated,
4230
+ path: opts.path
3767
4231
  });
3768
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3769
- process.stdout.write(result.stdout);
3770
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4232
+ emit(result);
3771
4233
  }
3772
4234
  );
3773
- program.command("path [slug]").description("resolve a slug to its absolute file path").option("--stdin", "read slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").action(
3774
- async (slug, opts) => {
4235
+ program.command("health").description("report graph integrity problems").option("--topic <name>", "scope to a topic + its descendants").option("--stale <duration>", "stale threshold (default 90d)").option("--stdin", "read page slugs from stdin (limit to these pages)").option("--json", "emit structured JSON").option("--wiki <name>", "target a specific registered wiki").action(
4236
+ async (opts) => {
3775
4237
  await autoRegisterIfNeeded(process.cwd());
3776
- const result = await runPath({
4238
+ const result = await runHealth({
3777
4239
  cwd: process.cwd(),
3778
- slug,
4240
+ topic: opts.topic,
4241
+ stale: opts.stale,
3779
4242
  stdin: opts.stdin,
3780
4243
  stdinInput: opts.stdin === true ? await readStdin() : void 0,
4244
+ json: opts.json,
3781
4245
  wiki: opts.wiki
3782
4246
  });
3783
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3784
- process.stdout.write(result.stdout);
3785
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4247
+ emit(result);
3786
4248
  }
3787
4249
  );
3788
- program.command("info [slug]").description("print metadata for a page (topics, refs, links, lineage)").option("--stdin", "read slugs from stdin (one per line)").option("--json", "emit structured JSON").option("--wiki <name>", "target a specific registered wiki").action(
3789
- async (slug, opts) => {
4250
+ program.command("list").description("list registered wikis").option("--json", "emit structured JSON").option(
4251
+ "--drop <name>",
4252
+ "remove a wiki from the registry (the only way entries are ever removed)"
4253
+ ).action(async (opts) => {
4254
+ if (opts.drop === void 0) {
4255
+ await autoRegisterIfNeeded(process.cwd());
4256
+ }
4257
+ const result = await listWikis(opts);
4258
+ process.stdout.write(result.stdout);
4259
+ if (result.exitCode !== 0) {
4260
+ process.exitCode = result.exitCode;
4261
+ }
4262
+ });
4263
+ program.command("tag [page] [topics...]").description("add topics to a page (auto-creates missing topics)").option("--stdin", "read page slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").action(
4264
+ async (page, topicsArg, opts) => {
3790
4265
  await autoRegisterIfNeeded(process.cwd());
3791
- const result = await runInfo({
4266
+ const resolvedTopics = opts.stdin === true ? [page, ...topicsArg].filter(
4267
+ (t) => typeof t === "string" && t.length > 0
4268
+ ) : topicsArg;
4269
+ const result = await runTag({
3792
4270
  cwd: process.cwd(),
3793
- slug,
4271
+ page: opts.stdin === true ? void 0 : page,
4272
+ topics: resolvedTopics,
3794
4273
  stdin: opts.stdin,
3795
4274
  stdinInput: opts.stdin === true ? await readStdin() : void 0,
3796
- json: opts.json,
3797
4275
  wiki: opts.wiki
3798
4276
  });
3799
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3800
- process.stdout.write(result.stdout);
3801
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4277
+ emit(result);
3802
4278
  }
3803
4279
  );
3804
- program.command("reindex").description("force a full rebuild of .almanac/index.db").option("--wiki <name>", "target a specific registered wiki").action(async (opts) => {
3805
- await autoRegisterIfNeeded(process.cwd());
3806
- const result = await runReindex({
3807
- cwd: process.cwd(),
3808
- wiki: opts.wiki
3809
- });
3810
- process.stdout.write(result.stdout);
3811
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
3812
- });
3813
- const topics = program.command("topics").description("manage the topic DAG (list, create, link, rename, delete)");
4280
+ program.command("untag <page> <topic>").description("remove a topic from a page's frontmatter").option("--wiki <name>", "target a specific registered wiki").action(
4281
+ async (page, topic, opts) => {
4282
+ await autoRegisterIfNeeded(process.cwd());
4283
+ const result = await runUntag({
4284
+ cwd: process.cwd(),
4285
+ page,
4286
+ topic,
4287
+ wiki: opts.wiki
4288
+ });
4289
+ emit(result);
4290
+ }
4291
+ );
4292
+ const topics = program.command("topics").description("manage the topic DAG");
3814
4293
  topics.command("list", { isDefault: true }).description("list all topics with page counts").option("--wiki <name>", "target a specific registered wiki").option("--json", "emit structured JSON").action(async (opts) => {
3815
4294
  await autoRegisterIfNeeded(process.cwd());
3816
4295
  const result = await runTopicsList({
@@ -3907,37 +4386,8 @@ async function run(argv) {
3907
4386
  emit(result);
3908
4387
  }
3909
4388
  );
3910
- program.command("tag [page] [topics...]").description("add topics to a page (auto-creates missing topics)").option("--stdin", "read page slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").action(
3911
- async (page, topicsArg, opts) => {
3912
- await autoRegisterIfNeeded(process.cwd());
3913
- const resolvedTopics = opts.stdin === true ? [page, ...topicsArg].filter(
3914
- (t) => typeof t === "string" && t.length > 0
3915
- ) : topicsArg;
3916
- const result = await runTag({
3917
- cwd: process.cwd(),
3918
- page: opts.stdin === true ? void 0 : page,
3919
- topics: resolvedTopics,
3920
- stdin: opts.stdin,
3921
- stdinInput: opts.stdin === true ? await readStdin() : void 0,
3922
- wiki: opts.wiki
3923
- });
3924
- emit(result);
3925
- }
3926
- );
3927
- program.command("untag <page> <topic>").description("remove a topic from a page's frontmatter").option("--wiki <name>", "target a specific registered wiki").action(
3928
- async (page, topic, opts) => {
3929
- await autoRegisterIfNeeded(process.cwd());
3930
- const result = await runUntag({
3931
- cwd: process.cwd(),
3932
- page,
3933
- topic,
3934
- wiki: opts.wiki
3935
- });
3936
- emit(result);
3937
- }
3938
- );
3939
4389
  program.command("bootstrap").description(
3940
- "spawn an agent to scan the repo and create initial wiki stubs (requires ANTHROPIC_API_KEY)"
4390
+ "scaffold a wiki in this repo via an AI agent (requires ANTHROPIC_API_KEY or Claude subscription)"
3941
4391
  ).option("--quiet", "suppress per-tool streaming; print only the final line").option("--model <model>", "override the agent model").option(
3942
4392
  "--force",
3943
4393
  "overwrite an existing populated wiki (default: refuse)"
@@ -3953,7 +4403,7 @@ async function run(argv) {
3953
4403
  }
3954
4404
  );
3955
4405
  program.command("capture [transcript]").description(
3956
- "capture knowledge from a Claude Code session transcript (auto-resolves the most recent session for this repo when no path is given; requires ANTHROPIC_API_KEY)"
4406
+ "run the writer/reviewer pipeline on a session (usually automatic)"
3957
4407
  ).option("--session <id>", "target a specific session by ID").option(
3958
4408
  "--quiet",
3959
4409
  "suppress per-tool streaming; print only the final summary"
@@ -3970,9 +4420,7 @@ async function run(argv) {
3970
4420
  emit(result);
3971
4421
  }
3972
4422
  );
3973
- const hook = program.command("hook").description(
3974
- "install, uninstall, or inspect the SessionEnd hook in ~/.claude/settings.json"
3975
- );
4423
+ const hook = program.command("hook").description("manage the SessionEnd auto-capture hook");
3976
4424
  hook.command("install").description("add a SessionEnd entry that runs 'almanac capture' on session end").action(async () => {
3977
4425
  const result = await runHookInstall();
3978
4426
  emit(result);
@@ -3985,23 +4433,165 @@ async function run(argv) {
3985
4433
  const result = await runHookStatus();
3986
4434
  emit(result);
3987
4435
  });
3988
- program.command("health").description("report wiki problems (orphans, dead refs, broken links, \u2026)").option("--topic <name>", "scope to a topic + its descendants").option("--stale <duration>", "stale threshold (default 90d)").option("--stdin", "read page slugs from stdin (limit to these pages)").option("--json", "emit structured JSON").option("--wiki <name>", "target a specific registered wiki").action(
4436
+ program.command("reindex").description("force a full rebuild of .almanac/index.db").option("--wiki <name>", "target a specific registered wiki").action(async (opts) => {
4437
+ await autoRegisterIfNeeded(process.cwd());
4438
+ const result = await runReindex({
4439
+ cwd: process.cwd(),
4440
+ wiki: opts.wiki
4441
+ });
4442
+ process.stdout.write(result.stdout);
4443
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
4444
+ });
4445
+ program.command("setup").description("install the hook + CLAUDE.md guides (bare codealmanac alias)").option("-y, --yes", "skip prompts; install everything").option("--skip-hook", "opt out of the SessionEnd hook").option("--skip-guides", "opt out of the CLAUDE.md guides").action(
3989
4446
  async (opts) => {
3990
- await autoRegisterIfNeeded(process.cwd());
3991
- const result = await runHealth({
3992
- cwd: process.cwd(),
3993
- topic: opts.topic,
3994
- stale: opts.stale,
3995
- stdin: opts.stdin,
3996
- stdinInput: opts.stdin === true ? await readStdin() : void 0,
3997
- json: opts.json,
3998
- wiki: opts.wiki
4447
+ const result = await runSetup({
4448
+ yes: opts.yes,
4449
+ skipHook: opts.skipHook,
4450
+ skipGuides: opts.skipGuides
4451
+ });
4452
+ emit(result);
4453
+ }
4454
+ );
4455
+ program.command("uninstall").description("remove the hook + guides + import line").option("-y, --yes", "skip confirmations; remove everything").option("--keep-hook", "leave the hook alone").option("--keep-guides", "leave the guides + CLAUDE.md import alone").action(
4456
+ async (opts) => {
4457
+ const result = await runUninstall({
4458
+ yes: opts.yes,
4459
+ keepHook: opts.keepHook,
4460
+ keepGuides: opts.keepGuides
3999
4461
  });
4000
4462
  emit(result);
4001
4463
  }
4002
4464
  );
4465
+ configureGroupedHelp(program);
4003
4466
  await program.parseAsync(argv);
4004
4467
  }
4468
+ var HELP_GROUPS = [
4469
+ {
4470
+ title: "Query",
4471
+ commands: ["search", "show", "health", "list"]
4472
+ },
4473
+ {
4474
+ title: "Edit",
4475
+ commands: ["tag", "untag", "topics"]
4476
+ },
4477
+ {
4478
+ title: "Wiki lifecycle",
4479
+ commands: ["bootstrap", "capture", "hook", "reindex"]
4480
+ },
4481
+ {
4482
+ title: "Setup",
4483
+ commands: ["setup", "uninstall"]
4484
+ }
4485
+ ];
4486
+ function configureGroupedHelp(program) {
4487
+ program.configureHelp({
4488
+ formatHelp(cmd, helper) {
4489
+ if (cmd.parent !== null) {
4490
+ return renderDefault(cmd, helper);
4491
+ }
4492
+ const termWidth = helper.padWidth(cmd, helper);
4493
+ const helpWidth = helper.helpWidth ?? process.stdout.columns ?? 80;
4494
+ const itemSepWidth = 2;
4495
+ const out = [];
4496
+ out.push(`Usage: ${helper.commandUsage(cmd)}
4497
+ `);
4498
+ const description = helper.commandDescription(cmd);
4499
+ if (description.length > 0) {
4500
+ out.push(
4501
+ helper.wrap(description, helpWidth, 0) + "\n"
4502
+ );
4503
+ }
4504
+ const optionList = helper.visibleOptions(cmd).map(
4505
+ (o) => `${helper.optionTerm(o)}${" ".repeat(Math.max(0, termWidth - helper.optionTerm(o).length) + itemSepWidth)}${helper.optionDescription(o)}`
4506
+ );
4507
+ if (optionList.length > 0) {
4508
+ out.push("Options:");
4509
+ for (const l of optionList) out.push(` ${l}`);
4510
+ out.push("");
4511
+ }
4512
+ const visible = helper.visibleCommands(cmd);
4513
+ const byName = /* @__PURE__ */ new Map();
4514
+ for (const c of visible) byName.set(c.name(), c);
4515
+ for (const group of HELP_GROUPS) {
4516
+ const members = group.commands.map((n) => byName.get(n)).filter((c) => c !== void 0);
4517
+ if (members.length === 0) continue;
4518
+ out.push(`${group.title}:`);
4519
+ for (const c of members) {
4520
+ const term = helper.subcommandTerm(c);
4521
+ const desc = helper.subcommandDescription(c);
4522
+ const padding = Math.max(
4523
+ 0,
4524
+ termWidth - term.length + itemSepWidth
4525
+ );
4526
+ out.push(` ${term}${" ".repeat(padding)}${desc}`);
4527
+ byName.delete(c.name());
4528
+ }
4529
+ out.push("");
4530
+ }
4531
+ if (byName.size > 0) {
4532
+ out.push("Other:");
4533
+ for (const c of byName.values()) {
4534
+ const term = helper.subcommandTerm(c);
4535
+ const desc = helper.subcommandDescription(c);
4536
+ const padding = Math.max(
4537
+ 0,
4538
+ termWidth - term.length + itemSepWidth
4539
+ );
4540
+ out.push(` ${term}${" ".repeat(padding)}${desc}`);
4541
+ }
4542
+ out.push("");
4543
+ }
4544
+ return out.join("\n");
4545
+ }
4546
+ });
4547
+ }
4548
+ function renderDefault(cmd, helper) {
4549
+ const termWidth = helper.padWidth(cmd, helper);
4550
+ const helpWidth = helper.helpWidth ?? process.stdout.columns ?? 80;
4551
+ const itemSepWidth = 2;
4552
+ const lines = [`Usage: ${helper.commandUsage(cmd)}
4553
+ `];
4554
+ const description = helper.commandDescription(cmd);
4555
+ if (description.length > 0) {
4556
+ lines.push(helper.wrap(description, helpWidth, 0) + "\n");
4557
+ }
4558
+ const args = helper.visibleArguments(cmd).map(
4559
+ (a) => `${helper.argumentTerm(a)}${" ".repeat(Math.max(0, termWidth - helper.argumentTerm(a).length) + itemSepWidth)}${helper.argumentDescription(a)}`
4560
+ );
4561
+ if (args.length > 0) {
4562
+ lines.push("Arguments:");
4563
+ for (const a of args) lines.push(` ${a}`);
4564
+ lines.push("");
4565
+ }
4566
+ const opts = helper.visibleOptions(cmd).map(
4567
+ (o) => `${helper.optionTerm(o)}${" ".repeat(Math.max(0, termWidth - helper.optionTerm(o).length) + itemSepWidth)}${helper.optionDescription(o)}`
4568
+ );
4569
+ if (opts.length > 0) {
4570
+ lines.push("Options:");
4571
+ for (const o of opts) lines.push(` ${o}`);
4572
+ lines.push("");
4573
+ }
4574
+ const subs = helper.visibleCommands(cmd).map(
4575
+ (c) => `${helper.subcommandTerm(c)}${" ".repeat(Math.max(0, termWidth - helper.subcommandTerm(c).length) + itemSepWidth)}${helper.subcommandDescription(c)}`
4576
+ );
4577
+ if (subs.length > 0) {
4578
+ lines.push("Commands:");
4579
+ for (const s of subs) lines.push(` ${s}`);
4580
+ lines.push("");
4581
+ }
4582
+ return lines.join("\n");
4583
+ }
4584
+ function readPackageVersion() {
4585
+ try {
4586
+ const require2 = createRequire3(import.meta.url);
4587
+ const pkg = require2("../package.json");
4588
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
4589
+ return pkg.version;
4590
+ }
4591
+ } catch {
4592
+ }
4593
+ return "unknown";
4594
+ }
4005
4595
  function emit(result) {
4006
4596
  if (result.stderr.length > 0) process.stderr.write(result.stderr);
4007
4597
  if (result.stdout.length > 0) process.stdout.write(result.stdout);