codealmanac 0.1.10 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +124 -104
  2. package/dist/agents-A4II4YJC.js +15 -0
  3. package/dist/auth-S5DVUIUJ.js +18 -0
  4. package/dist/{chunk-Z4MWLVS2.js → chunk-447U3GQJ.js} +162 -5
  5. package/dist/chunk-447U3GQJ.js.map +1 -0
  6. package/dist/{chunk-QLHJP2XK.js → chunk-B2AGSRXL.js} +13 -9
  7. package/dist/{chunk-QLHJP2XK.js.map → chunk-B2AGSRXL.js.map} +1 -1
  8. package/dist/{chunk-AXFPUHBN.js → chunk-F53U6JQG.js} +8 -49
  9. package/dist/chunk-F53U6JQG.js.map +1 -0
  10. package/dist/{chunk-3C5SY5SE.js → chunk-KQUVMF27.js} +5 -2
  11. package/dist/chunk-KQUVMF27.js.map +1 -0
  12. package/dist/{chunk-BJVZLP6O.js → chunk-MX2EW5MR.js} +3 -3
  13. package/dist/{chunk-Z6MBJ3D2.js → chunk-QQHIVTXT.js} +6 -4
  14. package/dist/{chunk-Z6MBJ3D2.js.map → chunk-QQHIVTXT.js.map} +1 -1
  15. package/dist/chunk-R3URPHGH.js +194 -0
  16. package/dist/chunk-R3URPHGH.js.map +1 -0
  17. package/dist/chunk-SSYMRT4I.js +126 -0
  18. package/dist/chunk-SSYMRT4I.js.map +1 -0
  19. package/dist/{chunk-QHQ6YH7U.js → chunk-V3QOQSXI.js} +5 -3
  20. package/dist/{chunk-QHQ6YH7U.js.map → chunk-V3QOQSXI.js.map} +1 -1
  21. package/dist/chunk-WRUSDYYE.js +97 -0
  22. package/dist/chunk-WRUSDYYE.js.map +1 -0
  23. package/dist/{chunk-3LC55TG6.js → chunk-ZDJSJIB6.js} +77 -126
  24. package/dist/chunk-ZDJSJIB6.js.map +1 -0
  25. package/dist/{cli-W3OYVJYH.js → cli-MZEXRV6E.js} +238 -24
  26. package/dist/cli-MZEXRV6E.js.map +1 -0
  27. package/dist/codealmanac.js +1 -1
  28. package/dist/doctor-3BYSF3JD.js +17 -0
  29. package/dist/{hook-CRJMWSSO.js → hook-2NP3UE7U.js} +2 -2
  30. package/dist/{register-commands-JHC2OFKM.js → register-commands-DPH4ZWEE.js} +621 -60
  31. package/dist/register-commands-DPH4ZWEE.js.map +1 -0
  32. package/dist/uninstall-FDIOBAAR.js +15 -0
  33. package/dist/uninstall-FDIOBAAR.js.map +1 -0
  34. package/dist/update-RAF7QRYF.js +11 -0
  35. package/dist/update-RAF7QRYF.js.map +1 -0
  36. package/dist/{wiki-IPSRRGOT.js → wiki-IGNRNLUZ.js} +2 -2
  37. package/hooks/almanac-capture.sh +40 -7
  38. package/package.json +3 -2
  39. package/dist/chunk-3C5SY5SE.js.map +0 -1
  40. package/dist/chunk-3LC55TG6.js.map +0 -1
  41. package/dist/chunk-AXFPUHBN.js.map +0 -1
  42. package/dist/chunk-Z4MWLVS2.js.map +0 -1
  43. package/dist/cli-W3OYVJYH.js.map +0 -1
  44. package/dist/doctor-ODFNJUKH.js +0 -15
  45. package/dist/register-commands-JHC2OFKM.js.map +0 -1
  46. package/dist/uninstall-HE2Z2LN2.js +0 -12
  47. package/dist/update-IL243I4E.js +0 -10
  48. /package/dist/{doctor-ODFNJUKH.js.map → agents-A4II4YJC.js.map} +0 -0
  49. /package/dist/{hook-CRJMWSSO.js.map → auth-S5DVUIUJ.js.map} +0 -0
  50. /package/dist/{chunk-BJVZLP6O.js.map → chunk-MX2EW5MR.js.map} +0 -0
  51. /package/dist/{uninstall-HE2Z2LN2.js.map → doctor-3BYSF3JD.js.map} +0 -0
  52. /package/dist/{update-IL243I4E.js.map → hook-2NP3UE7U.js.map} +0 -0
  53. /package/dist/{wiki-IPSRRGOT.js.map → wiki-IGNRNLUZ.js.map} +0 -0
@@ -1,4 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ collectOption,
4
+ emit,
5
+ parsePositiveInt,
6
+ readStdin
7
+ } from "./chunk-P3LDTCLB.js";
8
+ import {
9
+ assertAgentAuth,
10
+ runAgentsList,
11
+ runSetAgentModel,
12
+ runSetDefaultAgent
13
+ } from "./chunk-R3URPHGH.js";
2
14
  import {
3
15
  addEntry,
4
16
  ancestorsInFile,
@@ -21,24 +33,12 @@ import {
21
33
  titleCase,
22
34
  toKebabCase,
23
35
  writeTopicsFile
24
- } from "./chunk-3C5SY5SE.js";
36
+ } from "./chunk-KQUVMF27.js";
25
37
  import {
26
38
  runDoctor
27
- } from "./chunk-QLHJP2XK.js";
39
+ } from "./chunk-B2AGSRXL.js";
40
+ import "./chunk-V3QOQSXI.js";
28
41
  import "./chunk-4CODZRHH.js";
29
- import {
30
- runUninstall
31
- } from "./chunk-BJVZLP6O.js";
32
- import {
33
- runUpdate
34
- } from "./chunk-Z6MBJ3D2.js";
35
- import {
36
- collectOption,
37
- emit,
38
- parsePositiveInt,
39
- readStdin
40
- } from "./chunk-P3LDTCLB.js";
41
- import "./chunk-QHQ6YH7U.js";
42
42
  import {
43
43
  BLUE,
44
44
  BOLD,
@@ -46,16 +46,27 @@ import {
46
46
  RST
47
47
  } from "./chunk-FM3VRDK7.js";
48
48
  import {
49
- assertClaudeAuth,
50
- resolveClaudeExecutable,
49
+ runUninstall
50
+ } from "./chunk-MX2EW5MR.js";
51
+ import {
51
52
  runSetup
52
- } from "./chunk-3LC55TG6.js";
53
+ } from "./chunk-ZDJSJIB6.js";
53
54
  import {
54
55
  runHookInstall,
55
56
  runHookStatus,
56
57
  runHookUninstall
57
- } from "./chunk-Z4MWLVS2.js";
58
- import "./chunk-AXFPUHBN.js";
58
+ } from "./chunk-447U3GQJ.js";
59
+ import {
60
+ resolveClaudeExecutable
61
+ } from "./chunk-SSYMRT4I.js";
62
+ import {
63
+ runUpdate
64
+ } from "./chunk-QQHIVTXT.js";
65
+ import "./chunk-F53U6JQG.js";
66
+ import {
67
+ isAgentProviderId,
68
+ readConfig
69
+ } from "./chunk-WRUSDYYE.js";
59
70
  import {
60
71
  findNearestAlmanacDir,
61
72
  getRepoAlmanacDir
@@ -1761,10 +1772,33 @@ function registerQueryCommands(program) {
1761
1772
 
1762
1773
  // src/cli/register-setup-commands.ts
1763
1774
  function registerSetupCommands(program) {
1764
- 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(
1775
+ const agents = program.command("agents").description("list supported AI agent providers and readiness");
1776
+ agents.command("list").description("show Claude, Codex, and Cursor provider status").action(async () => {
1777
+ emit(await runAgentsList());
1778
+ });
1779
+ program.command("set").description("configure codealmanac defaults").argument("<key>", "setting key, e.g. default-agent or model").argument("[value...]", "setting value").action(async (key, value) => {
1780
+ if (key === "default-agent") {
1781
+ emit(await runSetDefaultAgent({ provider: value[0] ?? "" }));
1782
+ return;
1783
+ }
1784
+ if (key === "model") {
1785
+ emit(await runSetAgentModel({
1786
+ provider: value[0] ?? "",
1787
+ model: value[1]
1788
+ }));
1789
+ return;
1790
+ }
1791
+ emit({
1792
+ stdout: "",
1793
+ stderr: "almanac: unknown setting. Use `default-agent` or `model`.\n",
1794
+ exitCode: 1
1795
+ });
1796
+ });
1797
+ program.command("setup").description("install the hook + CLAUDE.md guides (bare codealmanac alias)").option("-y, --yes", "skip prompts; install everything").option("--agent <agent>", "default agent: claude, codex, or cursor").option("--skip-hook", "opt out of the SessionEnd hook").option("--skip-guides", "opt out of the CLAUDE.md guides").action(
1765
1798
  async (opts) => {
1766
1799
  const result = await runSetup({
1767
1800
  yes: opts.yes,
1801
+ agent: opts.agent,
1768
1802
  skipHook: opts.skipHook,
1769
1803
  skipGuides: opts.skipGuides
1770
1804
  });
@@ -1869,8 +1903,20 @@ async function loadPrompt(name) {
1869
1903
  }
1870
1904
 
1871
1905
  // src/agent/sdk.ts
1906
+ import { spawn } from "child_process";
1872
1907
  import { query } from "@anthropic-ai/claude-agent-sdk";
1908
+ var DEFAULT_AGENT_MODEL = "claude-sonnet-4-6";
1873
1909
  async function runAgent(opts) {
1910
+ const provider = opts.provider ?? "claude";
1911
+ if (provider === "codex") {
1912
+ return await runCodexAgent(opts);
1913
+ }
1914
+ if (provider === "cursor") {
1915
+ return await runCursorAgent(opts);
1916
+ }
1917
+ return await runClaudeAgent(opts);
1918
+ }
1919
+ async function runClaudeAgent(opts) {
1874
1920
  const claudeExecutable = resolveClaudeExecutable();
1875
1921
  const q = query({
1876
1922
  prompt: opts.prompt,
@@ -1879,7 +1925,7 @@ async function runAgent(opts) {
1879
1925
  allowedTools: opts.allowedTools,
1880
1926
  agents: opts.agents ?? {},
1881
1927
  cwd: opts.cwd,
1882
- model: opts.model ?? "claude-sonnet-4-6",
1928
+ model: opts.model ?? DEFAULT_AGENT_MODEL,
1883
1929
  maxTurns: opts.maxTurns ?? 100,
1884
1930
  ...claudeExecutable !== void 0 ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
1885
1931
  env: {
@@ -1925,6 +1971,186 @@ async function runAgent(opts) {
1925
1971
  }
1926
1972
  return { success, cost, turns, result, sessionId, error: errorMsg };
1927
1973
  }
1974
+ function combinedPrompt(opts) {
1975
+ const reviewerFallback = buildReviewerFallback(opts);
1976
+ return `${opts.systemPrompt}${reviewerFallback}
1977
+
1978
+ ---
1979
+
1980
+ ${opts.prompt}`;
1981
+ }
1982
+ function buildReviewerFallback(opts) {
1983
+ if ((opts.provider ?? "claude") === "claude") return "";
1984
+ const reviewer = opts.agents?.reviewer;
1985
+ if (reviewer === void 0) return "";
1986
+ return "\n\nNon-Claude provider note: this runtime does not receive Claude's nested Agent tool contract. When the writer prompt asks you to invoke the reviewer subagent, perform that review pass yourself before final wiki edits. Treat this reviewer prompt as read-only review guidance:\n\n" + reviewer.prompt;
1987
+ }
1988
+ async function runCodexAgent(opts) {
1989
+ const args = [
1990
+ "exec",
1991
+ "--json",
1992
+ "--sandbox",
1993
+ "workspace-write",
1994
+ "--skip-git-repo-check",
1995
+ "-C",
1996
+ opts.cwd
1997
+ ];
1998
+ if (opts.model !== void 0 && opts.model.length > 0) {
1999
+ args.push("--model", opts.model);
2000
+ }
2001
+ args.push(combinedPrompt(opts));
2002
+ return await runJsonlCli({
2003
+ command: "codex",
2004
+ args,
2005
+ cwd: opts.cwd,
2006
+ env: { ...process.env, CODEALMANAC_INTERNAL_SESSION: "1" },
2007
+ onMessage: opts.onMessage,
2008
+ parseFinal: parseCodexFinal
2009
+ });
2010
+ }
2011
+ async function runCursorAgent(opts) {
2012
+ const args = [
2013
+ "--print",
2014
+ "--output-format",
2015
+ "stream-json",
2016
+ "--stream-partial-output",
2017
+ "--trust",
2018
+ "--workspace",
2019
+ opts.cwd
2020
+ ];
2021
+ if (opts.model !== void 0 && opts.model.length > 0) {
2022
+ args.push("--model", opts.model);
2023
+ }
2024
+ args.push(combinedPrompt(opts));
2025
+ return await runJsonlCli({
2026
+ command: "cursor-agent",
2027
+ args,
2028
+ cwd: opts.cwd,
2029
+ env: { ...process.env, CODEALMANAC_INTERNAL_SESSION: "1" },
2030
+ onMessage: opts.onMessage,
2031
+ parseFinal: parseCursorFinal
2032
+ });
2033
+ }
2034
+ function runJsonlCli(opts) {
2035
+ return new Promise((resolve) => {
2036
+ const child = spawn(opts.command, opts.args, {
2037
+ cwd: opts.cwd,
2038
+ env: opts.env,
2039
+ stdio: ["ignore", "pipe", "pipe"]
2040
+ });
2041
+ let stdoutBuf = "";
2042
+ let stderr = "";
2043
+ let cost = 0;
2044
+ let turns = 0;
2045
+ let result = "";
2046
+ let sessionId;
2047
+ let success = false;
2048
+ let finalSeen = false;
2049
+ let error;
2050
+ const observe = (msg) => {
2051
+ opts.onMessage?.(msg);
2052
+ if (sessionId === void 0 && typeof msg.session_id === "string" && msg.session_id.length > 0) {
2053
+ sessionId = msg.session_id;
2054
+ }
2055
+ const final = opts.parseFinal(msg);
2056
+ if (final === null) return;
2057
+ finalSeen = true;
2058
+ if (final.cost !== void 0) cost = final.cost;
2059
+ if (final.turns !== void 0) turns = final.turns;
2060
+ if (final.result !== void 0) result = final.result;
2061
+ if (final.sessionId !== void 0) sessionId = final.sessionId;
2062
+ if (final.success !== void 0) success = final.success;
2063
+ if (final.error !== void 0) error = final.error;
2064
+ };
2065
+ const flushLines = () => {
2066
+ let idx = stdoutBuf.indexOf("\n");
2067
+ while (idx !== -1) {
2068
+ const rawLine = stdoutBuf.slice(0, idx);
2069
+ stdoutBuf = stdoutBuf.slice(idx + 1);
2070
+ const line = rawLine.trim();
2071
+ if (line.length > 0) {
2072
+ try {
2073
+ const parsed = JSON.parse(line);
2074
+ observe(parsed);
2075
+ } catch {
2076
+ }
2077
+ }
2078
+ idx = stdoutBuf.indexOf("\n");
2079
+ }
2080
+ };
2081
+ child.stdout.on("data", (chunk) => {
2082
+ stdoutBuf += chunk.toString("utf8");
2083
+ flushLines();
2084
+ });
2085
+ child.stderr.on("data", (chunk) => {
2086
+ stderr += chunk.toString("utf8");
2087
+ });
2088
+ child.on("error", (err) => {
2089
+ resolve({
2090
+ success: false,
2091
+ cost,
2092
+ turns,
2093
+ result,
2094
+ sessionId,
2095
+ error: err.code === "ENOENT" ? `${opts.command} not found on PATH` : err.message
2096
+ });
2097
+ });
2098
+ child.on("close", (code) => {
2099
+ flushLines();
2100
+ if (stdoutBuf.trim().length > 0) {
2101
+ try {
2102
+ observe(JSON.parse(stdoutBuf.trim()));
2103
+ } catch {
2104
+ }
2105
+ }
2106
+ if (code === 0 && finalSeen && success) {
2107
+ resolve({ success, cost, turns, result, sessionId });
2108
+ return;
2109
+ }
2110
+ const firstStderr = stderr.trim().split("\n")[0];
2111
+ resolve({
2112
+ success: false,
2113
+ cost,
2114
+ turns,
2115
+ result,
2116
+ sessionId,
2117
+ error: error ?? (firstStderr !== void 0 && firstStderr.length > 0 ? firstStderr : `${opts.command} exited ${code ?? 1}`)
2118
+ });
2119
+ });
2120
+ });
2121
+ }
2122
+ function parseCodexFinal(msg) {
2123
+ if (msg.type === "item.completed") {
2124
+ const item = msg.item;
2125
+ if (item !== null && typeof item === "object") {
2126
+ const obj = item;
2127
+ if (obj.type === "agent_message" && typeof obj.text === "string") {
2128
+ return { result: obj.text };
2129
+ }
2130
+ }
2131
+ return null;
2132
+ }
2133
+ if (msg.type === "turn.completed") {
2134
+ return { success: true };
2135
+ }
2136
+ if (msg.type === "turn.failed" || msg.type === "error") {
2137
+ return {
2138
+ success: false,
2139
+ error: typeof msg.message === "string" ? msg.message : typeof msg.error === "string" ? msg.error : "codex turn failed"
2140
+ };
2141
+ }
2142
+ return null;
2143
+ }
2144
+ function parseCursorFinal(msg) {
2145
+ if (msg.type !== "result") return null;
2146
+ const isError = msg.is_error === true || msg.subtype !== "success";
2147
+ return {
2148
+ success: !isError,
2149
+ result: typeof msg.result === "string" ? msg.result : "",
2150
+ sessionId: typeof msg.session_id === "string" ? msg.session_id : void 0,
2151
+ error: isError ? typeof msg.result === "string" ? msg.result : `cursor result: ${String(msg.subtype ?? "error")}` : void 0
2152
+ };
2153
+ }
1928
2154
 
1929
2155
  // src/commands/init.ts
1930
2156
  import { existsSync as existsSync4 } from "fs";
@@ -2069,8 +2295,21 @@ and optional \`files:\`. The rest is prose.
2069
2295
  // src/commands/bootstrap.ts
2070
2296
  var BOOTSTRAP_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
2071
2297
  async function runBootstrap(options) {
2298
+ const providerResolution = await resolveAgentSelection({
2299
+ agent: options.agent,
2300
+ model: options.model
2301
+ });
2302
+ if (!providerResolution.ok) {
2303
+ return {
2304
+ stdout: "",
2305
+ stderr: `almanac: ${providerResolution.error}
2306
+ `,
2307
+ exitCode: 1
2308
+ };
2309
+ }
2310
+ const { provider, model } = providerResolution;
2072
2311
  try {
2073
- await assertClaudeAuth(options.spawnCli);
2312
+ await assertAgentAuth({ provider, spawnCli: options.spawnCli });
2074
2313
  } catch (err) {
2075
2314
  const msg = err instanceof Error ? err.message : String(err);
2076
2315
  return {
@@ -2137,7 +2376,8 @@ async function runBootstrap(options) {
2137
2376
  prompt: userPrompt,
2138
2377
  allowedTools: BOOTSTRAP_TOOLS,
2139
2378
  cwd: repoRoot,
2140
- model: options.model,
2379
+ provider,
2380
+ model,
2141
2381
  onMessage
2142
2382
  });
2143
2383
  } finally {
@@ -2160,6 +2400,19 @@ async function runBootstrap(options) {
2160
2400
  exitCode: 1
2161
2401
  };
2162
2402
  }
2403
+ async function resolveAgentSelection(args) {
2404
+ const config = await readConfig();
2405
+ const rawProvider = args.agent ?? config.agent.default;
2406
+ if (!isAgentProviderId(rawProvider)) {
2407
+ return {
2408
+ ok: false,
2409
+ error: `unknown agent '${rawProvider}'. Expected one of: claude, codex, cursor.`
2410
+ };
2411
+ }
2412
+ const configuredModel = config.agent.models[rawProvider] ?? void 0;
2413
+ const model = args.model !== void 0 ? args.model : configuredModel === null ? void 0 : configuredModel;
2414
+ return { ok: true, provider: rawProvider, model };
2415
+ }
2163
2416
  function formatFinalLine(result, logPath, repoRoot) {
2164
2417
  const status = result.success ? "done" : "failed";
2165
2418
  const rel = relative(repoRoot, logPath);
@@ -2210,17 +2463,23 @@ var StreamingFormatter = class {
2210
2463
  this.currentAgent = name;
2211
2464
  }
2212
2465
  handle(msg) {
2213
- if (msg.type === "assistant") {
2466
+ if (!isRecord(msg)) return;
2467
+ if (msg.type === "assistant" && isRecord(msg.message)) {
2468
+ const content = msg.message.content;
2469
+ if (!Array.isArray(content)) return;
2214
2470
  for (const block of msg.message.content) {
2215
- if (block.type !== "tool_use") continue;
2471
+ if (!isRecord(block) || block.type !== "tool_use") continue;
2472
+ if (typeof block.name !== "string") continue;
2216
2473
  this.handleToolUse(block.name, block.input);
2217
2474
  }
2218
2475
  return;
2219
2476
  }
2220
2477
  if (msg.type === "result") {
2221
2478
  const status = msg.subtype === "success" ? "done" : `failed (${msg.subtype})`;
2479
+ const cost = typeof msg.total_cost_usd === "number" ? msg.total_cost_usd : 0;
2480
+ const turns = typeof msg.num_turns === "number" ? msg.num_turns : 0;
2222
2481
  this.sink.write(
2223
- `[${status}] cost: $${msg.total_cost_usd.toFixed(3)}, turns: ${msg.num_turns}
2482
+ `[${status}] cost: $${cost.toFixed(3)}, turns: ${turns}
2224
2483
  `
2225
2484
  );
2226
2485
  return;
@@ -2292,23 +2551,246 @@ function stringField(input, key) {
2292
2551
  const value = input[key];
2293
2552
  return typeof value === "string" ? value : void 0;
2294
2553
  }
2554
+ function isRecord(value) {
2555
+ return value !== null && typeof value === "object";
2556
+ }
2295
2557
 
2296
2558
  // src/commands/capture.ts
2297
2559
  import { createHash } from "crypto";
2298
2560
  import {
2299
2561
  createWriteStream as createWriteStream2,
2300
- existsSync as existsSync6,
2562
+ existsSync as existsSync7,
2301
2563
  statSync
2302
2564
  } from "fs";
2303
- import { mkdir as mkdir3, readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
2565
+ import { mkdir as mkdir4, readFile as readFile7, readdir as readdir3, stat } from "fs/promises";
2304
2566
  import { homedir } from "os";
2305
- import { basename as basename3, join as join7, relative as relative2 } from "path";
2567
+ import { basename as basename3, join as join8, relative as relative3 } from "path";
2568
+
2569
+ // src/commands/captureStatus.ts
2570
+ import { existsSync as existsSync6 } from "fs";
2571
+ import { mkdir as mkdir3, readFile as readFile6, readdir as readdir2, rename as rename2, writeFile as writeFile3 } from "fs/promises";
2572
+ import { dirname, join as join7, relative as relative2 } from "path";
2573
+ function captureStatePath(dir, stem) {
2574
+ return join7(dir, `.capture-${stem}.state.json`);
2575
+ }
2576
+ async function writeCaptureRunRecord(path2, record) {
2577
+ await mkdir3(dirname(path2), { recursive: true });
2578
+ const tmp = `${path2}.tmp-${process.pid}`;
2579
+ await writeFile3(tmp, `${JSON.stringify(record, null, 2)}
2580
+ `, "utf8");
2581
+ await rename2(tmp, path2);
2582
+ }
2583
+ function buildStartedCaptureRecord(args) {
2584
+ return {
2585
+ version: 1,
2586
+ kind: "capture",
2587
+ status: "running",
2588
+ sessionId: args.sessionId ?? args.stem,
2589
+ repoRoot: args.repoRoot,
2590
+ pid: process.pid,
2591
+ model: args.model ?? DEFAULT_AGENT_MODEL,
2592
+ transcriptPath: args.transcriptPath,
2593
+ startedAt: args.startedAt.toISOString(),
2594
+ logPath: join7(args.almanacDir, `.capture-${args.stem}.log`),
2595
+ jsonlPath: join7(args.almanacDir, `.capture-${args.stem}.jsonl`)
2596
+ };
2597
+ }
2598
+ function finishCaptureRecord(args) {
2599
+ const started = Date.parse(args.record.startedAt);
2600
+ const finished = args.finishedAt.getTime();
2601
+ return {
2602
+ ...args.record,
2603
+ status: args.status,
2604
+ finishedAt: args.finishedAt.toISOString(),
2605
+ durationMs: Number.isFinite(started) ? Math.max(0, finished - started) : void 0,
2606
+ summary: args.summary,
2607
+ error: args.error
2608
+ };
2609
+ }
2610
+ async function runCaptureStatus(options) {
2611
+ const repoRoot = findNearestAlmanacDir(options.cwd);
2612
+ if (repoRoot === null) {
2613
+ return {
2614
+ stdout: "",
2615
+ stderr: "almanac: no .almanac/ found in this directory or any parent. Run 'almanac bootstrap' first.\n",
2616
+ exitCode: 1
2617
+ };
2618
+ }
2619
+ const almanacDir = getRepoAlmanacDir(repoRoot);
2620
+ const records = await readCaptureRecords(almanacDir);
2621
+ const now = options.now?.() ?? /* @__PURE__ */ new Date();
2622
+ const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
2623
+ const views = records.map((record) => toView(record, repoRoot, now, isPidAlive)).sort((a, b) => b.sortTime - a.sortTime);
2624
+ if (options.json === true) {
2625
+ return {
2626
+ stdout: `${JSON.stringify(
2627
+ {
2628
+ repo: repoRoot,
2629
+ captures: views.map(({ sortTime: _sortTime, ...v }) => v)
2630
+ },
2631
+ null,
2632
+ 2
2633
+ )}
2634
+ `,
2635
+ stderr: "",
2636
+ exitCode: 0
2637
+ };
2638
+ }
2639
+ return {
2640
+ stdout: formatCaptureStatus(views),
2641
+ stderr: "",
2642
+ exitCode: 0
2643
+ };
2644
+ }
2645
+ async function readCaptureRecords(almanacDir) {
2646
+ if (!existsSync6(almanacDir)) return [];
2647
+ const out = [];
2648
+ const dirs = [join7(almanacDir, "logs"), almanacDir];
2649
+ for (const dir of dirs) {
2650
+ let entries;
2651
+ try {
2652
+ entries = await readdir2(dir);
2653
+ } catch {
2654
+ continue;
2655
+ }
2656
+ for (const entry of entries) {
2657
+ if (!entry.startsWith(".capture-") || !entry.endsWith(".state.json")) {
2658
+ continue;
2659
+ }
2660
+ try {
2661
+ const parsed = JSON.parse(await readFile6(join7(dir, entry), "utf8"));
2662
+ if (isCaptureRunRecord(parsed)) out.push(parsed);
2663
+ } catch {
2664
+ continue;
2665
+ }
2666
+ }
2667
+ }
2668
+ return out;
2669
+ }
2670
+ function isCaptureRunRecord(value) {
2671
+ if (value === null || typeof value !== "object") return false;
2672
+ const v = value;
2673
+ return v.version === 1 && v.kind === "capture" && (v.status === "running" || v.status === "done" || v.status === "failed") && typeof v.sessionId === "string" && typeof v.repoRoot === "string" && typeof v.pid === "number" && typeof v.model === "string" && typeof v.transcriptPath === "string" && typeof v.startedAt === "string" && typeof v.logPath === "string" && typeof v.jsonlPath === "string";
2674
+ }
2675
+ function toView(record, repoRoot, now, isPidAlive) {
2676
+ const started = Date.parse(record.startedAt);
2677
+ const finished = record.finishedAt !== void 0 ? Date.parse(record.finishedAt) : void 0;
2678
+ const elapsedMs = record.durationMs ?? (Number.isFinite(started) ? Math.max(0, (finished ?? now.getTime()) - started) : 0);
2679
+ const status = record.status === "running" && !isPidAlive(record.pid) ? "stale" : record.status;
2680
+ return {
2681
+ status,
2682
+ sessionId: record.sessionId,
2683
+ model: record.model,
2684
+ elapsedMs,
2685
+ startedAt: record.startedAt,
2686
+ finishedAt: record.finishedAt,
2687
+ pid: record.pid,
2688
+ logPath: relative2(repoRoot, record.logPath),
2689
+ jsonlPath: relative2(repoRoot, record.jsonlPath),
2690
+ summary: record.summary,
2691
+ error: status === "stale" ? "process ended without a final status" : record.error,
2692
+ sortTime: finished ?? (Number.isFinite(started) ? started : 0)
2693
+ };
2694
+ }
2695
+ function formatCaptureStatus(views) {
2696
+ const lines = ["Capture jobs", ""];
2697
+ if (views.length === 0) {
2698
+ lines.push("No capture jobs found.");
2699
+ return `${lines.join("\n")}
2700
+ `;
2701
+ }
2702
+ const active = views.filter((v) => v.status === "running" || v.status === "stale");
2703
+ const finished = views.filter((v) => v.status === "done" || v.status === "failed");
2704
+ if (active.length === 0) {
2705
+ lines.push("No active captures.", "");
2706
+ } else {
2707
+ for (const view of active) {
2708
+ lines.push(formatRow(view));
2709
+ lines.push(` log: ${view.logPath}`);
2710
+ if (view.error !== void 0) lines.push(` error: ${view.error}`);
2711
+ lines.push("");
2712
+ }
2713
+ }
2714
+ if (finished.length > 0) {
2715
+ lines.push(active.length === 0 ? "Last capture:" : "Last finished:");
2716
+ for (const view of finished.slice(0, 3)) {
2717
+ lines.push(formatRow(view));
2718
+ if (view.status === "failed") {
2719
+ lines.push(` log: ${view.logPath}`);
2720
+ if (view.error !== void 0) lines.push(` error: ${view.error}`);
2721
+ }
2722
+ }
2723
+ }
2724
+ return `${trimTrailingBlank(lines).join("\n")}
2725
+ `;
2726
+ }
2727
+ function formatRow(view) {
2728
+ const status = view.status.padEnd(7, " ");
2729
+ const session = view.sessionId.padEnd(12, " ");
2730
+ const model = view.model.padEnd(17, " ");
2731
+ const elapsed = formatDuration(view.elapsedMs);
2732
+ const summary = formatSummary(view);
2733
+ return `${status} ${session} ${model} ${elapsed}${summary.length > 0 ? ` ${summary}` : ""}`;
2734
+ }
2735
+ function formatSummary(view) {
2736
+ if (view.status === "failed") return "failed; see log";
2737
+ if (view.summary === void 0) return "";
2738
+ const parts = [];
2739
+ if (view.summary.updated > 0) {
2740
+ parts.push(`${view.summary.updated} updated`);
2741
+ }
2742
+ if (view.summary.created > 0) {
2743
+ parts.push(`${view.summary.created} created`);
2744
+ }
2745
+ if (view.summary.archived > 0) {
2746
+ parts.push(`${view.summary.archived} archived`);
2747
+ }
2748
+ return parts.length > 0 ? parts.join(", ") : "0 pages written";
2749
+ }
2750
+ function formatDuration(ms) {
2751
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
2752
+ const minutes = Math.floor(totalSeconds / 60);
2753
+ const seconds = totalSeconds % 60;
2754
+ if (minutes < 60) return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
2755
+ const hours = Math.floor(minutes / 60);
2756
+ const restMinutes = minutes % 60;
2757
+ return `${hours}h${restMinutes.toString().padStart(2, "0")}m`;
2758
+ }
2759
+ function trimTrailingBlank(lines) {
2760
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
2761
+ lines.pop();
2762
+ }
2763
+ return lines;
2764
+ }
2765
+ function defaultIsPidAlive(pid) {
2766
+ try {
2767
+ process.kill(pid, 0);
2768
+ return true;
2769
+ } catch {
2770
+ return false;
2771
+ }
2772
+ }
2773
+
2774
+ // src/commands/capture.ts
2306
2775
  var WRITER_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Agent"];
2307
2776
  var REVIEWER_TOOLS = ["Read", "Grep", "Glob", "Bash"];
2308
2777
  var REVIEWER_DESCRIPTION = "Reviews proposed wiki changes against the full knowledge base for cohesion, duplication, missing links, notability, and writing conventions.";
2309
2778
  async function runCapture(options) {
2779
+ const providerResolution = await resolveAgentSelection2({
2780
+ agent: options.agent,
2781
+ model: options.model
2782
+ });
2783
+ if (!providerResolution.ok) {
2784
+ return {
2785
+ stdout: "",
2786
+ stderr: `almanac: ${providerResolution.error}
2787
+ `,
2788
+ exitCode: 1
2789
+ };
2790
+ }
2791
+ const { provider, model } = providerResolution;
2310
2792
  try {
2311
- await assertClaudeAuth(options.spawnCli);
2793
+ await assertAgentAuth({ provider, spawnCli: options.spawnCli });
2312
2794
  } catch (err) {
2313
2795
  const msg = err instanceof Error ? err.message : String(err);
2314
2796
  return {
@@ -2327,7 +2809,7 @@ async function runCapture(options) {
2327
2809
  };
2328
2810
  }
2329
2811
  const almanacDir = getRepoAlmanacDir(repoRoot);
2330
- const pagesDir = join7(almanacDir, "pages");
2812
+ const pagesDir = join8(almanacDir, "pages");
2331
2813
  const transcriptResolution = await resolveTranscript({
2332
2814
  repoRoot,
2333
2815
  explicit: options.transcriptPath,
@@ -2353,12 +2835,24 @@ async function runCapture(options) {
2353
2835
  tools: REVIEWER_TOOLS
2354
2836
  }
2355
2837
  };
2356
- const now = options.now?.() ?? /* @__PURE__ */ new Date();
2357
- const logStem = options.sessionId !== void 0 && options.sessionId.length > 0 ? options.sessionId : formatTimestamp2(now);
2358
- const logsDir = join7(almanacDir, "logs");
2359
- await mkdir3(logsDir, { recursive: true });
2838
+ const startedAt = options.now?.() ?? /* @__PURE__ */ new Date();
2839
+ const logStem = options.sessionId !== void 0 && options.sessionId.length > 0 ? options.sessionId : formatTimestamp2(startedAt);
2840
+ const logsDir = join8(almanacDir, "logs");
2841
+ await mkdir4(logsDir, { recursive: true });
2360
2842
  const logName = `.capture-${logStem}.jsonl`;
2361
- const logPath = join7(logsDir, logName);
2843
+ const logPath = join8(logsDir, logName);
2844
+ const statePath = captureStatePath(logsDir, logStem);
2845
+ const stateRecord = buildStartedCaptureRecord({
2846
+ repoRoot,
2847
+ almanacDir: logsDir,
2848
+ stem: logStem,
2849
+ sessionId: options.sessionId,
2850
+ transcriptPath,
2851
+ model: options.model,
2852
+ startedAt
2853
+ });
2854
+ await writeCaptureRunRecord(statePath, stateRecord).catch(() => {
2855
+ });
2362
2856
  const logStream = createWriteStream2(logPath, { flags: "w" });
2363
2857
  const out = process.stdout;
2364
2858
  const formatter = new StreamingFormatter({
@@ -2387,7 +2881,8 @@ Working directory: ${repoRoot}.`;
2387
2881
  allowedTools: WRITER_TOOLS,
2388
2882
  agents,
2389
2883
  cwd: repoRoot,
2390
- model: options.model,
2884
+ provider,
2885
+ model,
2391
2886
  // Capture sessions can touch many pages; give it more headroom than
2392
2887
  // bootstrap. The SDK treats `maxTurns` as a hard stop — better to
2393
2888
  // overshoot than to cut off mid-review.
@@ -2399,16 +2894,43 @@ Working directory: ${repoRoot}.`;
2399
2894
  }
2400
2895
  const snapshotAfter = await snapshotPages(pagesDir);
2401
2896
  const delta = diffSnapshots(snapshotBefore, snapshotAfter);
2897
+ const finishedAt = options.now?.() ?? /* @__PURE__ */ new Date();
2898
+ const captureSummary = {
2899
+ ...delta,
2900
+ cost: result.cost,
2901
+ turns: result.turns
2902
+ };
2402
2903
  if (!result.success) {
2904
+ await writeCaptureRunRecord(
2905
+ statePath,
2906
+ finishCaptureRecord({
2907
+ record: stateRecord,
2908
+ status: "failed",
2909
+ finishedAt,
2910
+ summary: captureSummary,
2911
+ error: result.error ?? "unknown error"
2912
+ })
2913
+ ).catch(() => {
2914
+ });
2403
2915
  return {
2404
2916
  stdout: "",
2405
2917
  stderr: `almanac: capture failed: ${result.error ?? "unknown error"}
2406
- (transcript: ${relative2(repoRoot, logPath)})
2918
+ (transcript: ${relative3(repoRoot, logPath)})
2407
2919
  `,
2408
2920
  exitCode: 1
2409
2921
  };
2410
2922
  }
2411
- const summary = formatSummary(result, delta, logPath, repoRoot);
2923
+ await writeCaptureRunRecord(
2924
+ statePath,
2925
+ finishCaptureRecord({
2926
+ record: stateRecord,
2927
+ status: "done",
2928
+ finishedAt,
2929
+ summary: captureSummary
2930
+ })
2931
+ ).catch(() => {
2932
+ });
2933
+ const summary = formatSummary2(result, delta, logPath, repoRoot);
2412
2934
  return {
2413
2935
  stdout: `${summary}
2414
2936
  `,
@@ -2416,9 +2938,22 @@ Working directory: ${repoRoot}.`;
2416
2938
  exitCode: 0
2417
2939
  };
2418
2940
  }
2941
+ async function resolveAgentSelection2(args) {
2942
+ const config = await readConfig();
2943
+ const rawProvider = args.agent ?? config.agent.default;
2944
+ if (!isAgentProviderId(rawProvider)) {
2945
+ return {
2946
+ ok: false,
2947
+ error: `unknown agent '${rawProvider}'. Expected one of: claude, codex, cursor.`
2948
+ };
2949
+ }
2950
+ const configuredModel = config.agent.models[rawProvider] ?? void 0;
2951
+ const model = args.model !== void 0 ? args.model : configuredModel === null ? void 0 : configuredModel;
2952
+ return { ok: true, provider: rawProvider, model };
2953
+ }
2419
2954
  async function resolveTranscript(args) {
2420
2955
  if (args.explicit !== void 0 && args.explicit.length > 0) {
2421
- if (!existsSync6(args.explicit)) {
2956
+ if (!existsSync7(args.explicit)) {
2422
2957
  return {
2423
2958
  ok: false,
2424
2959
  error: `transcript not found: ${args.explicit}`
@@ -2426,8 +2961,8 @@ async function resolveTranscript(args) {
2426
2961
  }
2427
2962
  return { ok: true, path: args.explicit };
2428
2963
  }
2429
- const projectsDir = args.claudeProjectsDir ?? join7(homedir(), ".claude", "projects");
2430
- if (!existsSync6(projectsDir)) {
2964
+ const projectsDir = args.claudeProjectsDir ?? join8(homedir(), ".claude", "projects");
2965
+ if (!existsSync7(projectsDir)) {
2431
2966
  return {
2432
2967
  ok: false,
2433
2968
  error: `could not auto-resolve transcript; ${projectsDir} does not exist. Pass --session <id> or <transcript-path>.`
@@ -2459,21 +2994,21 @@ async function collectTranscripts(projectsDir) {
2459
2994
  const out = [];
2460
2995
  let topLevel;
2461
2996
  try {
2462
- topLevel = await readdir2(projectsDir);
2997
+ topLevel = await readdir3(projectsDir);
2463
2998
  } catch {
2464
2999
  return out;
2465
3000
  }
2466
3001
  for (const name of topLevel) {
2467
- const projectDir = join7(projectsDir, name);
3002
+ const projectDir = join8(projectsDir, name);
2468
3003
  let entries;
2469
3004
  try {
2470
- entries = await readdir2(projectDir);
3005
+ entries = await readdir3(projectDir);
2471
3006
  } catch {
2472
3007
  continue;
2473
3008
  }
2474
3009
  for (const entry of entries) {
2475
3010
  if (!entry.endsWith(".jsonl")) continue;
2476
- const full = join7(projectDir, entry);
3011
+ const full = join8(projectDir, entry);
2477
3012
  try {
2478
3013
  const st = await stat(full);
2479
3014
  if (st.isFile()) {
@@ -2488,7 +3023,7 @@ async function collectTranscripts(projectsDir) {
2488
3023
  async function filterTranscriptsByCwd(transcripts, repoRoot) {
2489
3024
  const dirHash = `-${repoRoot.replace(/^\/+/, "").replace(/\//g, "-")}`;
2490
3025
  const byDirName = transcripts.filter((t) => {
2491
- const parent = basename3(join7(t.path, ".."));
3026
+ const parent = basename3(join8(t.path, ".."));
2492
3027
  return parent === dirHash || parent.endsWith(dirHash);
2493
3028
  });
2494
3029
  if (byDirName.length > 0) return byDirName;
@@ -2505,26 +3040,26 @@ async function filterTranscriptsByCwd(transcripts, repoRoot) {
2505
3040
  return hits;
2506
3041
  }
2507
3042
  async function readHead(path2, bytes) {
2508
- const content = await readFile6(path2, "utf8");
3043
+ const content = await readFile7(path2, "utf8");
2509
3044
  return content.length > bytes ? content.slice(0, bytes) : content;
2510
3045
  }
2511
3046
  async function snapshotPages(pagesDir) {
2512
3047
  const out = /* @__PURE__ */ new Map();
2513
- if (!existsSync6(pagesDir)) return out;
3048
+ if (!existsSync7(pagesDir)) return out;
2514
3049
  let entries;
2515
3050
  try {
2516
- entries = await readdir2(pagesDir);
3051
+ entries = await readdir3(pagesDir);
2517
3052
  } catch {
2518
3053
  return out;
2519
3054
  }
2520
3055
  for (const entry of entries) {
2521
3056
  if (!entry.endsWith(".md")) continue;
2522
3057
  const slug = entry.slice(0, -3);
2523
- const full = join7(pagesDir, entry);
3058
+ const full = join8(pagesDir, entry);
2524
3059
  try {
2525
3060
  const st = statSync(full);
2526
3061
  if (!st.isFile()) continue;
2527
- const content = await readFile6(full, "utf8");
3062
+ const content = await readFile7(full, "utf8");
2528
3063
  const hash = createHash("sha256").update(content).digest("hex");
2529
3064
  const fm = parseFrontmatter(content);
2530
3065
  out.set(slug, {
@@ -2558,8 +3093,8 @@ function diffSnapshots(before, after) {
2558
3093
  }
2559
3094
  return { created, updated, archived };
2560
3095
  }
2561
- function formatSummary(result, delta, logPath, repoRoot) {
2562
- const rel = relative2(repoRoot, logPath);
3096
+ function formatSummary2(result, delta, logPath, repoRoot) {
3097
+ const rel = relative3(repoRoot, logPath);
2563
3098
  const cost = `$${result.cost.toFixed(3)}`;
2564
3099
  const { created, updated, archived } = delta;
2565
3100
  if (created === 0 && updated === 0 && archived === 0) {
@@ -2594,18 +3129,19 @@ async function runReindex(options) {
2594
3129
  function registerWikiLifecycleCommands(program) {
2595
3130
  program.command("bootstrap").description(
2596
3131
  "scaffold a wiki in this repo via an AI agent (requires ANTHROPIC_API_KEY or Claude subscription)"
2597
- ).option("--quiet", "suppress per-tool streaming; print only the final line").option("--model <model>", "override the agent model").option("--force", "overwrite an existing populated wiki (default: refuse)").action(
3132
+ ).option("--quiet", "suppress per-tool streaming; print only the final line").option("--agent <agent>", "agent provider: claude, codex, or cursor").option("--model <model>", "override the agent model").option("--force", "overwrite an existing populated wiki (default: refuse)").action(
2598
3133
  async (opts) => {
2599
3134
  const result = await runBootstrap({
2600
3135
  cwd: process.cwd(),
2601
3136
  quiet: opts.quiet,
3137
+ agent: opts.agent,
2602
3138
  model: opts.model,
2603
3139
  force: opts.force
2604
3140
  });
2605
3141
  emit(result);
2606
3142
  }
2607
3143
  );
2608
- program.command("capture [transcript]").description("run the writer/reviewer pipeline on a session (usually automatic)").option("--session <id>", "target a specific session by ID").option("--quiet", "suppress per-tool streaming; print only the final summary").option("--model <model>", "override the agent model").action(
3144
+ const capture = program.command("capture [transcript]").alias("c").description("run the writer/reviewer pipeline on a session (usually automatic)").option("--session <id>", "target a specific session by ID").option("--quiet", "suppress per-tool streaming; print only the final summary").option("--agent <agent>", "agent provider: claude, codex, or cursor").option("--model <model>", "override the agent model").action(
2609
3145
  async (transcript, opts) => {
2610
3146
  await autoRegisterIfNeeded(process.cwd());
2611
3147
  const result = await runCapture({
@@ -2613,14 +3149,33 @@ function registerWikiLifecycleCommands(program) {
2613
3149
  transcriptPath: transcript,
2614
3150
  sessionId: opts.session,
2615
3151
  quiet: opts.quiet,
3152
+ agent: opts.agent,
2616
3153
  model: opts.model
2617
3154
  });
2618
3155
  emit(result);
2619
3156
  }
2620
3157
  );
3158
+ capture.command("status").description("show running and recent capture jobs").option("--json", "emit structured JSON").action(async (opts) => {
3159
+ await autoRegisterIfNeeded(process.cwd());
3160
+ const result = await runCaptureStatus({
3161
+ cwd: process.cwd(),
3162
+ json: opts.json
3163
+ });
3164
+ emit(result);
3165
+ });
3166
+ program.command("ps").description("show running and recent capture jobs").option("--json", "emit structured JSON").action(async (opts) => {
3167
+ await autoRegisterIfNeeded(process.cwd());
3168
+ const result = await runCaptureStatus({
3169
+ cwd: process.cwd(),
3170
+ json: opts.json
3171
+ });
3172
+ emit(result);
3173
+ });
2621
3174
  const hook = program.command("hook").description("manage the SessionEnd auto-capture hook");
2622
- hook.command("install").description("add a SessionEnd entry that runs 'almanac capture' on session end").action(async () => {
2623
- const result = await runHookInstall();
3175
+ hook.command("install").description("add a SessionEnd entry that runs 'almanac capture' on session end").option("--source <source>", "claude, codex, cursor, or all").action(async (opts) => {
3176
+ const result = await runHookInstall({
3177
+ source: normalizeHookSource(opts.source)
3178
+ });
2624
3179
  emit(result);
2625
3180
  });
2626
3181
  hook.command("uninstall").description("remove codealmanac's SessionEnd entry; leaves foreign entries alone").action(async () => {
@@ -2641,6 +3196,12 @@ function registerWikiLifecycleCommands(program) {
2641
3196
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
2642
3197
  });
2643
3198
  }
3199
+ function normalizeHookSource(source) {
3200
+ if (source === "claude" || source === "codex" || source === "cursor" || source === "all") {
3201
+ return source;
3202
+ }
3203
+ return void 0;
3204
+ }
2644
3205
 
2645
3206
  // src/cli/register-commands.ts
2646
3207
  function registerCommands(program) {
@@ -2652,4 +3213,4 @@ function registerCommands(program) {
2652
3213
  export {
2653
3214
  registerCommands
2654
3215
  };
2655
- //# sourceMappingURL=register-commands-JHC2OFKM.js.map
3216
+ //# sourceMappingURL=register-commands-DPH4ZWEE.js.map