codealmanac 0.1.1 → 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
 
@@ -225,7 +226,7 @@ function findNearestAlmanacDir(startDir) {
225
226
  }
226
227
  }
227
228
 
228
- // src/commands/init.ts
229
+ // src/commands/_init.ts
229
230
  import { existsSync as existsSync3 } from "fs";
230
231
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
231
232
  import { basename, join as join3 } from "path";
@@ -239,10 +240,10 @@ function toKebabCase(input) {
239
240
  import { mkdir, readFile as readFile2, rename, writeFile } from "fs/promises";
240
241
  import { dirname as dirname3 } from "path";
241
242
  async function readRegistry() {
242
- const path3 = getRegistryPath();
243
+ const path5 = getRegistryPath();
243
244
  let raw;
244
245
  try {
245
- raw = await readFile2(path3, "utf8");
246
+ raw = await readFile2(path5, "utf8");
246
247
  } catch (err) {
247
248
  if (isNodeError(err) && err.code === "ENOENT") {
248
249
  return [];
@@ -258,10 +259,10 @@ async function readRegistry() {
258
259
  parsed = JSON.parse(trimmed);
259
260
  } catch (err) {
260
261
  const message = err instanceof Error ? err.message : String(err);
261
- throw new Error(`registry at ${path3} is not valid JSON: ${message}`);
262
+ throw new Error(`registry at ${path5} is not valid JSON: ${message}`);
262
263
  }
263
264
  if (!Array.isArray(parsed)) {
264
- throw new Error(`registry at ${path3} must be a JSON array`);
265
+ throw new Error(`registry at ${path5} must be a JSON array`);
265
266
  }
266
267
  return parsed.map((item, idx) => {
267
268
  if (typeof item !== "object" || item === null) {
@@ -269,29 +270,29 @@ async function readRegistry() {
269
270
  }
270
271
  const e = item;
271
272
  const name = typeof e.name === "string" ? e.name : "";
272
- const path4 = typeof e.path === "string" ? e.path : "";
273
+ const path6 = typeof e.path === "string" ? e.path : "";
273
274
  if (name.length === 0) {
274
275
  throw new Error(`registry entry ${idx} is missing a non-empty "name"`);
275
276
  }
276
- if (path4.length === 0) {
277
+ if (path6.length === 0) {
277
278
  throw new Error(`registry entry ${idx} is missing a non-empty "path"`);
278
279
  }
279
280
  return {
280
281
  name,
281
282
  description: typeof e.description === "string" ? e.description : "",
282
- path: path4,
283
+ path: path6,
283
284
  registered_at: typeof e.registered_at === "string" ? e.registered_at : ""
284
285
  };
285
286
  });
286
287
  }
287
288
  async function writeRegistry(entries) {
288
- const path3 = getRegistryPath();
289
- await mkdir(dirname3(path3), { recursive: true });
289
+ const path5 = getRegistryPath();
290
+ await mkdir(dirname3(path5), { recursive: true });
290
291
  const body = `${JSON.stringify(entries, null, 2)}
291
292
  `;
292
- const tmpPath = `${path3}.tmp`;
293
+ const tmpPath = `${path5}.tmp`;
293
294
  await writeFile(tmpPath, body, "utf8");
294
- await rename(tmpPath, path3);
295
+ await rename(tmpPath, path5);
295
296
  }
296
297
  function pathsEqual(a, b) {
297
298
  if (process.platform === "darwin" || process.platform === "win32") {
@@ -335,7 +336,7 @@ function isNodeError(err) {
335
336
  return err instanceof Error && "code" in err;
336
337
  }
337
338
 
338
- // src/commands/init.ts
339
+ // src/commands/_init.ts
339
340
  async function initWiki(options) {
340
341
  const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;
341
342
  const almanacDir = getRepoAlmanacDir(repoRoot);
@@ -365,15 +366,15 @@ async function initWiki(options) {
365
366
  return { entry, almanacDir, created: !alreadyExisted };
366
367
  }
367
368
  async function ensureGitignoreHasIndexDb(cwd) {
368
- const path3 = join3(cwd, ".gitignore");
369
+ const path5 = join3(cwd, ".gitignore");
369
370
  const targets = [
370
371
  ".almanac/index.db",
371
372
  ".almanac/index.db-wal",
372
373
  ".almanac/index.db-shm"
373
374
  ];
374
375
  let existing = "";
375
- if (existsSync3(path3)) {
376
- existing = await readFile3(path3, "utf8");
376
+ if (existsSync3(path5)) {
377
+ existing = await readFile3(path5, "utf8");
377
378
  }
378
379
  const lines = existing.split(/\r?\n/).map((l) => l.trim());
379
380
  const missing = targets.filter((t) => !lines.includes(t));
@@ -383,7 +384,7 @@ async function ensureGitignoreHasIndexDb(cwd) {
383
384
  ${missing.join("\n")}
384
385
  `;
385
386
  const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
386
- await writeFile2(path3, `${existing}${sep}${block}`, "utf8");
387
+ await writeFile2(path5, `${existing}${sep}${block}`, "utf8");
387
388
  }
388
389
  function starterReadme() {
389
390
  return `# Wiki
@@ -986,8 +987,8 @@ async function filterTranscriptsByCwd(transcripts, repoRoot) {
986
987
  }
987
988
  return hits;
988
989
  }
989
- async function readHead(path3, bytes) {
990
- const content = await readFile4(path3, "utf8");
990
+ async function readHead(path5, bytes) {
991
+ const content = await readFile4(path5, "utf8");
991
992
  return content.length > bytes ? content.slice(0, bytes) : content;
992
993
  }
993
994
  async function snapshotPages(pagesDir) {
@@ -1294,13 +1295,13 @@ import { existsSync as existsSync7 } from "fs";
1294
1295
  import { mkdir as mkdir4, readFile as readFile6, rename as rename3, writeFile as writeFile4 } from "fs/promises";
1295
1296
  import { dirname as dirname4 } from "path";
1296
1297
  import yaml2 from "js-yaml";
1297
- async function loadTopicsFile(path3) {
1298
- if (!existsSync7(path3)) {
1298
+ async function loadTopicsFile(path5) {
1299
+ if (!existsSync7(path5)) {
1299
1300
  return { topics: [] };
1300
1301
  }
1301
1302
  let raw;
1302
1303
  try {
1303
- raw = await readFile6(path3, "utf8");
1304
+ raw = await readFile6(path5, "utf8");
1304
1305
  } catch (err) {
1305
1306
  if (isNodeError2(err) && err.code === "ENOENT") {
1306
1307
  return { topics: [] };
@@ -1316,13 +1317,13 @@ async function loadTopicsFile(path3) {
1316
1317
  parsed = yaml2.load(raw);
1317
1318
  } catch (err) {
1318
1319
  const message = err instanceof Error ? err.message : String(err);
1319
- 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}`);
1320
1321
  }
1321
1322
  if (parsed === null || parsed === void 0) {
1322
1323
  return { topics: [] };
1323
1324
  }
1324
1325
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
1325
- throw new Error(`topics.yaml at ${path3} must be a mapping`);
1326
+ throw new Error(`topics.yaml at ${path5} must be a mapping`);
1326
1327
  }
1327
1328
  const obj = parsed;
1328
1329
  const rawTopics = obj.topics;
@@ -1330,7 +1331,7 @@ async function loadTopicsFile(path3) {
1330
1331
  return { topics: [] };
1331
1332
  }
1332
1333
  if (!Array.isArray(rawTopics)) {
1333
- 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`);
1334
1335
  }
1335
1336
  const topics = [];
1336
1337
  for (const item of rawTopics) {
@@ -1359,7 +1360,7 @@ async function loadTopicsFile(path3) {
1359
1360
  }
1360
1361
  return { topics };
1361
1362
  }
1362
- async function writeTopicsFile(path3, file) {
1363
+ async function writeTopicsFile(path5, file) {
1363
1364
  const sorted = [...file.topics].sort((a, b) => a.slug.localeCompare(b.slug));
1364
1365
  const doc = {
1365
1366
  topics: sorted.map((t) => {
@@ -1384,13 +1385,13 @@ async function writeTopicsFile(path3, file) {
1384
1385
  sortKeys: false
1385
1386
  });
1386
1387
  const content = `${header}${body}`;
1387
- const tmpPath = `${path3}.tmp`;
1388
- const parent = dirname4(path3);
1388
+ const tmpPath = `${path5}.tmp`;
1389
+ const parent = dirname4(path5);
1389
1390
  if (!existsSync7(parent)) {
1390
1391
  await mkdir4(parent, { recursive: true });
1391
1392
  }
1392
1393
  await writeFile4(tmpPath, content, "utf8");
1393
- await rename3(tmpPath, path3);
1394
+ await rename3(tmpPath, path5);
1394
1395
  }
1395
1396
  function findTopic(file, slug) {
1396
1397
  for (const t of file.topics) {
@@ -1539,10 +1540,10 @@ function classifyWikilink(raw) {
1539
1540
  }
1540
1541
  if (firstSlash !== -1) {
1541
1542
  const isDir = looksLikeDir(body);
1542
- const path3 = normalizePath(body, isDir);
1543
+ const path5 = normalizePath(body, isDir);
1543
1544
  const originalPath = normalizePathPreservingCase(body, isDir);
1544
- if (path3.length === 0) return null;
1545
- 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 };
1546
1547
  }
1547
1548
  const target = toKebabCase(body);
1548
1549
  if (target.length === 0) return null;
@@ -1762,10 +1763,10 @@ async function indexPagesInto(db, pagesDir) {
1762
1763
  }
1763
1764
  for (const raw of p.frontmatterFiles) {
1764
1765
  const isDir = looksLikeDir(raw);
1765
- const path3 = normalizePath(raw, isDir);
1766
+ const path5 = normalizePath(raw, isDir);
1766
1767
  const originalPath = normalizePathPreservingCase(raw, isDir);
1767
- if (path3.length === 0) continue;
1768
- 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);
1769
1770
  }
1770
1771
  for (const ref of p.wikilinks) {
1771
1772
  switch (ref.kind) {
@@ -1821,8 +1822,8 @@ function hashContent(raw) {
1821
1822
  return createHash2("sha256").update(raw).digest("hex");
1822
1823
  }
1823
1824
  function topicsYamlNewerThan(almanacDir, dbPath) {
1824
- const path3 = join6(almanacDir, "topics.yaml");
1825
- if (!existsSync8(path3)) return false;
1825
+ const path5 = join6(almanacDir, "topics.yaml");
1826
+ if (!existsSync8(path5)) return false;
1826
1827
  let dbMtime;
1827
1828
  try {
1828
1829
  dbMtime = statSync2(dbPath).mtimeMs;
@@ -1830,7 +1831,7 @@ function topicsYamlNewerThan(almanacDir, dbPath) {
1830
1831
  return true;
1831
1832
  }
1832
1833
  try {
1833
- const st = statSync2(path3);
1834
+ const st = statSync2(path5);
1834
1835
  return st.mtimeMs > dbMtime;
1835
1836
  } catch {
1836
1837
  return false;
@@ -2252,153 +2253,6 @@ function section(label, count, lines) {
2252
2253
  ${lines.join("\n")}`;
2253
2254
  }
2254
2255
 
2255
- // src/commands/info.ts
2256
- import { join as join9 } from "path";
2257
- async function runInfo(options) {
2258
- const repoRoot = await resolveWikiRoot({
2259
- cwd: options.cwd,
2260
- wiki: options.wiki
2261
- });
2262
- await ensureFreshIndex({ repoRoot });
2263
- const dbPath = join9(repoRoot, ".almanac", "index.db");
2264
- const db = openIndex(dbPath);
2265
- try {
2266
- const slugs = collectSlugs(options);
2267
- if (slugs.length === 0) {
2268
- return {
2269
- stdout: "",
2270
- stderr: "almanac: info requires a slug (or --stdin)\n",
2271
- exitCode: 1
2272
- };
2273
- }
2274
- const records = [];
2275
- const missing = [];
2276
- for (const slug of slugs) {
2277
- const rec = fetchInfo(db, slug);
2278
- if (rec === null) {
2279
- missing.push(slug);
2280
- continue;
2281
- }
2282
- records.push(rec);
2283
- }
2284
- const bulk = options.stdin === true;
2285
- const jsonOut = options.json === true || bulk;
2286
- let stdout;
2287
- if (jsonOut) {
2288
- if (bulk) {
2289
- stdout = `${JSON.stringify(records, null, 2)}
2290
- `;
2291
- } else {
2292
- const only = records[0] ?? null;
2293
- stdout = `${JSON.stringify(only, null, 2)}
2294
- `;
2295
- }
2296
- } else {
2297
- stdout = records.map(formatHumanReadable).join("\n");
2298
- }
2299
- const stderr = missing.map((s) => `almanac: no such page "${s}"
2300
- `).join("");
2301
- return {
2302
- stdout,
2303
- stderr,
2304
- exitCode: missing.length > 0 ? 1 : 0
2305
- };
2306
- } finally {
2307
- db.close();
2308
- }
2309
- }
2310
- function fetchInfo(db, slug) {
2311
- const pageRow = db.prepare(
2312
- "SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?"
2313
- ).get(slug);
2314
- if (pageRow === void 0) return null;
2315
- const topics = db.prepare(
2316
- "SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug"
2317
- ).all(slug).map((r) => r.topic_slug);
2318
- const refs = db.prepare(
2319
- // Display the author's casing (`original_path`), not the
2320
- // lowercased lookup form. The lowercased `path` column is the
2321
- // query key for `--mentions`; it's not a user-facing string.
2322
- "SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path"
2323
- ).all(slug).map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));
2324
- const linksOut = db.prepare(
2325
- "SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug"
2326
- ).all(slug).map((r) => r.target_slug);
2327
- const linksIn = db.prepare(
2328
- "SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug"
2329
- ).all(slug).map((r) => r.source_slug);
2330
- const xwiki = db.prepare(
2331
- "SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug"
2332
- ).all(slug).map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));
2333
- const supersedesRows = db.prepare(
2334
- "SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug"
2335
- ).all(slug).map((r) => r.slug);
2336
- return {
2337
- slug: pageRow.slug,
2338
- title: pageRow.title,
2339
- file_path: pageRow.file_path,
2340
- updated_at: pageRow.updated_at,
2341
- archived_at: pageRow.archived_at,
2342
- superseded_by: pageRow.superseded_by,
2343
- supersedes: supersedesRows,
2344
- topics,
2345
- file_refs: refs,
2346
- wikilinks_out: linksOut,
2347
- wikilinks_in: linksIn,
2348
- cross_wiki_links: xwiki
2349
- };
2350
- }
2351
- function collectSlugs(options) {
2352
- if (options.stdin === true && options.stdinInput !== void 0) {
2353
- return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
2354
- }
2355
- if (options.slug !== void 0 && options.slug.length > 0) {
2356
- return [options.slug];
2357
- }
2358
- return [];
2359
- }
2360
- function formatHumanReadable(rec) {
2361
- const lines = [];
2362
- lines.push(`slug: ${rec.slug}`);
2363
- lines.push(`title: ${rec.title ?? "\u2014"}`);
2364
- lines.push(`file: ${rec.file_path}`);
2365
- lines.push(`updated_at: ${new Date(rec.updated_at * 1e3).toISOString()}`);
2366
- if (rec.archived_at !== null) {
2367
- lines.push(
2368
- `archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
2369
- );
2370
- }
2371
- if (rec.superseded_by !== null) {
2372
- lines.push(`superseded_by: ${rec.superseded_by}`);
2373
- }
2374
- if (rec.supersedes.length > 0) {
2375
- lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
2376
- }
2377
- lines.push(`topics: ${rec.topics.length > 0 ? rec.topics.join(", ") : "\u2014"}`);
2378
- lines.push("file_refs:");
2379
- if (rec.file_refs.length === 0) {
2380
- lines.push(" \u2014");
2381
- } else {
2382
- for (const r of rec.file_refs) {
2383
- lines.push(` ${r.path}${r.is_dir ? " (dir)" : ""}`);
2384
- }
2385
- }
2386
- lines.push("wikilinks_out:");
2387
- if (rec.wikilinks_out.length === 0) lines.push(" \u2014");
2388
- else for (const t of rec.wikilinks_out) lines.push(` ${t}`);
2389
- lines.push("wikilinks_in:");
2390
- if (rec.wikilinks_in.length === 0) lines.push(" \u2014");
2391
- else for (const s of rec.wikilinks_in) lines.push(` ${s}`);
2392
- if (rec.cross_wiki_links.length > 0) {
2393
- lines.push("cross_wiki_links:");
2394
- for (const x of rec.cross_wiki_links) {
2395
- lines.push(` ${x.wiki}:${x.target}`);
2396
- }
2397
- }
2398
- return `${lines.join("\n")}
2399
- `;
2400
- }
2401
-
2402
2256
  // src/commands/list.ts
2403
2257
  import { existsSync as existsSync11 } from "fs";
2404
2258
  async function listWikis(options) {
@@ -2451,61 +2305,6 @@ function formatPretty(entries) {
2451
2305
  `;
2452
2306
  }
2453
2307
 
2454
- // src/commands/path.ts
2455
- import { join as join10 } from "path";
2456
- async function runPath(options) {
2457
- const repoRoot = await resolveWikiRoot({
2458
- cwd: options.cwd,
2459
- wiki: options.wiki
2460
- });
2461
- await ensureFreshIndex({ repoRoot });
2462
- const dbPath = join10(repoRoot, ".almanac", "index.db");
2463
- const db = openIndex(dbPath);
2464
- try {
2465
- const slugs = collectSlugs2(options);
2466
- if (slugs.length === 0) {
2467
- return {
2468
- stdout: "",
2469
- stderr: "almanac: path requires a slug (or --stdin)\n",
2470
- exitCode: 1
2471
- };
2472
- }
2473
- const stmt = db.prepare(
2474
- "SELECT file_path FROM pages WHERE slug = ?"
2475
- );
2476
- const resolved = [];
2477
- const missing = [];
2478
- for (const slug of slugs) {
2479
- const row = stmt.get(slug);
2480
- if (row === void 0) {
2481
- missing.push(slug);
2482
- continue;
2483
- }
2484
- resolved.push(row.file_path);
2485
- }
2486
- const stdout = resolved.length > 0 ? `${resolved.join("\n")}
2487
- ` : "";
2488
- const stderr = missing.map((s) => `almanac: no such page "${s}"
2489
- `).join("");
2490
- return {
2491
- stdout,
2492
- stderr,
2493
- exitCode: missing.length > 0 ? 1 : 0
2494
- };
2495
- } finally {
2496
- db.close();
2497
- }
2498
- }
2499
- function collectSlugs2(options) {
2500
- if (options.stdin === true && options.stdinInput !== void 0) {
2501
- return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
2502
- }
2503
- if (options.slug !== void 0 && options.slug.length > 0) {
2504
- return [options.slug];
2505
- }
2506
- return [];
2507
- }
2508
-
2509
2308
  // src/commands/reindex.ts
2510
2309
  async function runReindex(options) {
2511
2310
  const repoRoot = await resolveWikiRoot({
@@ -2520,14 +2319,14 @@ async function runReindex(options) {
2520
2319
  }
2521
2320
 
2522
2321
  // src/commands/search.ts
2523
- import { join as join11 } from "path";
2322
+ import { join as join9 } from "path";
2524
2323
  async function runSearch(options) {
2525
2324
  const repoRoot = await resolveWikiRoot({
2526
2325
  cwd: options.cwd,
2527
2326
  wiki: options.wiki
2528
2327
  });
2529
2328
  await ensureFreshIndex({ repoRoot });
2530
- const dbPath = join11(repoRoot, ".almanac", "index.db");
2329
+ const dbPath = join9(repoRoot, ".almanac", "index.db");
2531
2330
  const db = openIndex(dbPath);
2532
2331
  try {
2533
2332
  const rows = executeQuery(db, options);
@@ -2695,19 +2494,313 @@ function buildStderr(rows, options) {
2695
2494
  return "";
2696
2495
  }
2697
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
+
2698
2791
  // src/commands/show.ts
2699
- import { readFile as readFile9 } from "fs/promises";
2700
- import { join as join12 } from "path";
2792
+ import { readFile as readFile10 } from "fs/promises";
2793
+ import { join as join10 } from "path";
2701
2794
  async function runShow(options) {
2702
2795
  const repoRoot = await resolveWikiRoot({
2703
2796
  cwd: options.cwd,
2704
2797
  wiki: options.wiki
2705
2798
  });
2706
2799
  await ensureFreshIndex({ repoRoot });
2707
- const dbPath = join12(repoRoot, ".almanac", "index.db");
2800
+ const dbPath = join10(repoRoot, ".almanac", "index.db");
2708
2801
  const db = openIndex(dbPath);
2709
2802
  try {
2710
- const slugs = collectSlugs3(options);
2803
+ const slugs = collectSlugs(options);
2711
2804
  if (slugs.length === 0) {
2712
2805
  return {
2713
2806
  stdout: "",
@@ -2715,44 +2808,289 @@ async function runShow(options) {
2715
2808
  exitCode: 1
2716
2809
  };
2717
2810
  }
2718
- const stmt = db.prepare(
2719
- "SELECT file_path FROM pages WHERE slug = ?"
2720
- );
2721
2811
  const records = [];
2722
2812
  const missing = [];
2723
2813
  for (const slug of slugs) {
2724
- const row = stmt.get(slug);
2725
- if (row === void 0) {
2814
+ const rec = await fetchRecord(db, slug);
2815
+ if (rec === null) {
2726
2816
  missing.push(slug);
2727
2817
  continue;
2728
2818
  }
2729
- try {
2730
- records.push({ slug, content: await readFile9(row.file_path, "utf8") });
2731
- } catch (err) {
2732
- const message = err instanceof Error ? err.message : String(err);
2733
- missing.push(`${slug} (${message})`);
2819
+ records.push(rec);
2820
+ }
2821
+ const bulk = options.stdin === true;
2822
+ const stdout = bulk ? formatBulk(records) : formatSingle(records, options);
2823
+ const stderr = missing.map((s) => `almanac: no such page "${s}"
2824
+ `).join("");
2825
+ return {
2826
+ stdout,
2827
+ stderr,
2828
+ exitCode: missing.length > 0 ? 1 : 0
2829
+ };
2830
+ } finally {
2831
+ db.close();
2832
+ }
2833
+ }
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}`);
2734
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";
2735
3018
  }
2736
- const bulk = options.stdin === true;
2737
- let stdout;
2738
- if (bulk) {
2739
- stdout = records.map((r) => JSON.stringify(r)).join("\n");
2740
- if (stdout.length > 0) stdout += "\n";
2741
- } else {
2742
- stdout = records.map((r) => r.content).join("");
2743
- }
2744
- const stderr = missing.map((s) => `almanac: no such page "${s}"
2745
- `).join("");
2746
- return {
2747
- stdout,
2748
- stderr,
2749
- exitCode: missing.length > 0 ? 1 : 0
2750
- };
2751
- } finally {
2752
- db.close();
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();
2753
3088
  }
3089
+ const blank = src.search(/\n[ \t]*\n/);
3090
+ if (blank === -1) return src.trimEnd();
3091
+ return src.slice(0, blank).trimEnd();
2754
3092
  }
2755
- function collectSlugs3(options) {
3093
+ function collectSlugs(options) {
2756
3094
  if (options.stdin === true && options.stdinInput !== void 0) {
2757
3095
  return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
2758
3096
  }
@@ -2763,17 +3101,17 @@ function collectSlugs3(options) {
2763
3101
  }
2764
3102
 
2765
3103
  // src/topics/frontmatterRewrite.ts
2766
- 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";
2767
3105
  import yaml3 from "js-yaml";
2768
3106
  async function rewritePageTopics(filePath, transform) {
2769
- const raw = await readFile10(filePath, "utf8");
3107
+ const raw = await readFile11(filePath, "utf8");
2770
3108
  const { before, after, output, changed } = applyTopicsTransform(
2771
3109
  raw,
2772
3110
  transform
2773
3111
  );
2774
3112
  if (changed) {
2775
3113
  const tmp = `${filePath}.tmp`;
2776
- await writeFile5(tmp, output, "utf8");
3114
+ await writeFile6(tmp, output, "utf8");
2777
3115
  await rename4(tmp, filePath);
2778
3116
  }
2779
3117
  return { before, after, changed };
@@ -2980,12 +3318,12 @@ function arraysEqual(a, b) {
2980
3318
  }
2981
3319
 
2982
3320
  // src/topics/paths.ts
2983
- import { join as join13 } from "path";
3321
+ import { join as join11 } from "path";
2984
3322
  function topicsYamlPath(repoRoot) {
2985
- return join13(repoRoot, ".almanac", "topics.yaml");
3323
+ return join11(repoRoot, ".almanac", "topics.yaml");
2986
3324
  }
2987
3325
  function indexDbPath(repoRoot) {
2988
- return join13(repoRoot, ".almanac", "index.db");
3326
+ return join11(repoRoot, ".almanac", "index.db");
2989
3327
  }
2990
3328
 
2991
3329
  // src/commands/tag.ts
@@ -3144,8 +3482,8 @@ async function runUntag(options) {
3144
3482
  }
3145
3483
 
3146
3484
  // src/commands/topics.ts
3147
- import { readFile as readFile11 } from "fs/promises";
3148
- import { join as join14 } from "path";
3485
+ import { readFile as readFile12 } from "fs/promises";
3486
+ import { join as join12 } from "path";
3149
3487
  import fg3 from "fast-glob";
3150
3488
  async function runTopicsList(options) {
3151
3489
  const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
@@ -3617,7 +3955,7 @@ async function runTopicsDescribe(options) {
3617
3955
  };
3618
3956
  }
3619
3957
  async function rewriteTopicOnPages(repoRoot, transform) {
3620
- const pagesDir = join14(repoRoot, ".almanac", "pages");
3958
+ const pagesDir = join12(repoRoot, ".almanac", "pages");
3621
3959
  const files = await fg3("**/*.md", {
3622
3960
  cwd: pagesDir,
3623
3961
  absolute: true,
@@ -3625,7 +3963,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
3625
3963
  });
3626
3964
  let changed = 0;
3627
3965
  for (const filePath of files) {
3628
- const raw = await readFile11(filePath, "utf8");
3966
+ const raw = await readFile12(filePath, "utf8");
3629
3967
  const applied = applyTopicsTransform(raw, transform);
3630
3968
  if (!applied.changed) continue;
3631
3969
  await rewritePageTopics(filePath, transform);
@@ -3634,14 +3972,146 @@ async function rewriteTopicOnPages(repoRoot, transform) {
3634
3972
  return changed;
3635
3973
  }
3636
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
+
3637
4107
  // src/registry/autoregister.ts
3638
- import { existsSync as existsSync12 } from "fs";
4108
+ import { existsSync as existsSync14 } from "fs";
3639
4109
  import { basename as basename5 } from "path";
3640
4110
  async function autoRegisterIfNeeded(cwd) {
3641
4111
  try {
3642
4112
  const repoRoot = findNearestAlmanacDir(cwd);
3643
4113
  if (repoRoot === null) return null;
3644
- if (!existsSync12(repoRoot)) return null;
4114
+ if (!existsSync14(repoRoot)) return null;
3645
4115
  const entries = await readRegistry();
3646
4116
  const existing = entries.find((e) => samePath(e.path, repoRoot));
3647
4117
  if (existing !== void 0) return existing;
@@ -3686,38 +4156,20 @@ function samePath(a, b) {
3686
4156
 
3687
4157
  // src/cli.ts
3688
4158
  async function run(argv) {
3689
- const program = new Command();
3690
4159
  const invoked = argv[1] !== void 0 ? basename6(argv[1]) : "almanac";
3691
4160
  const programName = invoked === "codealmanac" ? "codealmanac" : "almanac";
4161
+ const program = new Command();
3692
4162
  program.name(programName).description(
3693
4163
  "codealmanac \u2014 a living wiki for codebases, maintained by AI agents"
3694
- ).version("0.1.0", "-v, --version", "print version");
3695
- 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) => {
3696
- const result = await initWiki({
3697
- cwd: process.cwd(),
3698
- name: opts.name,
3699
- description: opts.description
3700
- });
3701
- const verb = result.created ? "initialized" : "updated";
3702
- process.stdout.write(
3703
- `${verb} wiki "${result.entry.name}" at ${result.almanacDir}
3704
- `
3705
- );
3706
- });
3707
- program.command("list").description("list registered wikis").option("--json", "emit structured JSON").option(
3708
- "--drop <name>",
3709
- "remove a wiki from the registry (the only way entries are ever removed)"
3710
- ).action(async (opts) => {
3711
- if (opts.drop === void 0) {
3712
- await autoRegisterIfNeeded(process.cwd());
3713
- }
3714
- const result = await listWikis(opts);
3715
- process.stdout.write(result.stdout);
3716
- if (result.exitCode !== 0) {
3717
- process.exitCode = result.exitCode;
3718
- }
3719
- });
3720
- 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(
3721
4173
  "--topic <name...>",
3722
4174
  "filter by topic (repeat for intersection)",
3723
4175
  collectOption,
@@ -3748,12 +4200,10 @@ async function run(argv) {
3748
4200
  json: opts.json,
3749
4201
  limit: opts.limit
3750
4202
  });
3751
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3752
- process.stdout.write(result.stdout);
3753
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4203
+ emit(result);
3754
4204
  }
3755
4205
  );
3756
- 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(
3757
4207
  async (slug, opts) => {
3758
4208
  await autoRegisterIfNeeded(process.cwd());
3759
4209
  const result = await runShow({
@@ -3761,54 +4211,85 @@ async function run(argv) {
3761
4211
  slug,
3762
4212
  stdin: opts.stdin,
3763
4213
  stdinInput: opts.stdin === true ? await readStdin() : void 0,
3764
- 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
3765
4231
  });
3766
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3767
- process.stdout.write(result.stdout);
3768
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4232
+ emit(result);
3769
4233
  }
3770
4234
  );
3771
- 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(
3772
- 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) => {
3773
4237
  await autoRegisterIfNeeded(process.cwd());
3774
- const result = await runPath({
4238
+ const result = await runHealth({
3775
4239
  cwd: process.cwd(),
3776
- slug,
4240
+ topic: opts.topic,
4241
+ stale: opts.stale,
3777
4242
  stdin: opts.stdin,
3778
4243
  stdinInput: opts.stdin === true ? await readStdin() : void 0,
4244
+ json: opts.json,
3779
4245
  wiki: opts.wiki
3780
4246
  });
3781
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3782
- process.stdout.write(result.stdout);
3783
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4247
+ emit(result);
3784
4248
  }
3785
4249
  );
3786
- 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(
3787
- 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) => {
3788
4265
  await autoRegisterIfNeeded(process.cwd());
3789
- 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({
3790
4270
  cwd: process.cwd(),
3791
- slug,
4271
+ page: opts.stdin === true ? void 0 : page,
4272
+ topics: resolvedTopics,
3792
4273
  stdin: opts.stdin,
3793
4274
  stdinInput: opts.stdin === true ? await readStdin() : void 0,
3794
- json: opts.json,
3795
4275
  wiki: opts.wiki
3796
4276
  });
3797
- if (result.stderr.length > 0) process.stderr.write(result.stderr);
3798
- process.stdout.write(result.stdout);
3799
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
4277
+ emit(result);
3800
4278
  }
3801
4279
  );
3802
- program.command("reindex").description("force a full rebuild of .almanac/index.db").option("--wiki <name>", "target a specific registered wiki").action(async (opts) => {
3803
- await autoRegisterIfNeeded(process.cwd());
3804
- const result = await runReindex({
3805
- cwd: process.cwd(),
3806
- wiki: opts.wiki
3807
- });
3808
- process.stdout.write(result.stdout);
3809
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
3810
- });
3811
- 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");
3812
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) => {
3813
4294
  await autoRegisterIfNeeded(process.cwd());
3814
4295
  const result = await runTopicsList({
@@ -3905,37 +4386,8 @@ async function run(argv) {
3905
4386
  emit(result);
3906
4387
  }
3907
4388
  );
3908
- 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(
3909
- async (page, topicsArg, opts) => {
3910
- await autoRegisterIfNeeded(process.cwd());
3911
- const resolvedTopics = opts.stdin === true ? [page, ...topicsArg].filter(
3912
- (t) => typeof t === "string" && t.length > 0
3913
- ) : topicsArg;
3914
- const result = await runTag({
3915
- cwd: process.cwd(),
3916
- page: opts.stdin === true ? void 0 : page,
3917
- topics: resolvedTopics,
3918
- stdin: opts.stdin,
3919
- stdinInput: opts.stdin === true ? await readStdin() : void 0,
3920
- wiki: opts.wiki
3921
- });
3922
- emit(result);
3923
- }
3924
- );
3925
- program.command("untag <page> <topic>").description("remove a topic from a page's frontmatter").option("--wiki <name>", "target a specific registered wiki").action(
3926
- async (page, topic, opts) => {
3927
- await autoRegisterIfNeeded(process.cwd());
3928
- const result = await runUntag({
3929
- cwd: process.cwd(),
3930
- page,
3931
- topic,
3932
- wiki: opts.wiki
3933
- });
3934
- emit(result);
3935
- }
3936
- );
3937
4389
  program.command("bootstrap").description(
3938
- "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)"
3939
4391
  ).option("--quiet", "suppress per-tool streaming; print only the final line").option("--model <model>", "override the agent model").option(
3940
4392
  "--force",
3941
4393
  "overwrite an existing populated wiki (default: refuse)"
@@ -3951,7 +4403,7 @@ async function run(argv) {
3951
4403
  }
3952
4404
  );
3953
4405
  program.command("capture [transcript]").description(
3954
- "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)"
3955
4407
  ).option("--session <id>", "target a specific session by ID").option(
3956
4408
  "--quiet",
3957
4409
  "suppress per-tool streaming; print only the final summary"
@@ -3968,9 +4420,7 @@ async function run(argv) {
3968
4420
  emit(result);
3969
4421
  }
3970
4422
  );
3971
- const hook = program.command("hook").description(
3972
- "install, uninstall, or inspect the SessionEnd hook in ~/.claude/settings.json"
3973
- );
4423
+ const hook = program.command("hook").description("manage the SessionEnd auto-capture hook");
3974
4424
  hook.command("install").description("add a SessionEnd entry that runs 'almanac capture' on session end").action(async () => {
3975
4425
  const result = await runHookInstall();
3976
4426
  emit(result);
@@ -3983,23 +4433,165 @@ async function run(argv) {
3983
4433
  const result = await runHookStatus();
3984
4434
  emit(result);
3985
4435
  });
3986
- 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(
3987
4446
  async (opts) => {
3988
- await autoRegisterIfNeeded(process.cwd());
3989
- const result = await runHealth({
3990
- cwd: process.cwd(),
3991
- topic: opts.topic,
3992
- stale: opts.stale,
3993
- stdin: opts.stdin,
3994
- stdinInput: opts.stdin === true ? await readStdin() : void 0,
3995
- json: opts.json,
3996
- wiki: opts.wiki
4447
+ const result = await runSetup({
4448
+ yes: opts.yes,
4449
+ skipHook: opts.skipHook,
4450
+ skipGuides: opts.skipGuides
3997
4451
  });
3998
4452
  emit(result);
3999
4453
  }
4000
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
4461
+ });
4462
+ emit(result);
4463
+ }
4464
+ );
4465
+ configureGroupedHelp(program);
4001
4466
  await program.parseAsync(argv);
4002
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
+ }
4003
4595
  function emit(result) {
4004
4596
  if (result.stderr.length > 0) process.stderr.write(result.stderr);
4005
4597
  if (result.stdout.length > 0) process.stdout.write(result.stdout);