codealmanac 0.2.5 → 0.2.6

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/COMMERCIAL.md +9 -0
  2. package/LICENSE +133 -21
  3. package/README.md +11 -11
  4. package/dist/{agents-RVTQYE6A.js → agents-HYRWRHRX.js} +4 -4
  5. package/dist/{chunk-TT6ZP4GS.js → chunk-2BNDNGUR.js} +8 -4
  6. package/dist/{chunk-TT6ZP4GS.js.map → chunk-2BNDNGUR.js.map} +1 -1
  7. package/dist/{chunk-P5WGG4FJ.js → chunk-3E7JNMTZ.js} +28 -3
  8. package/dist/chunk-3E7JNMTZ.js.map +1 -0
  9. package/dist/{chunk-SMIK2YLU.js → chunk-DW32TL5W.js} +117 -83
  10. package/dist/chunk-DW32TL5W.js.map +1 -0
  11. package/dist/{chunk-6BJUYZ43.js → chunk-GPFVEF6V.js} +28 -18
  12. package/dist/chunk-GPFVEF6V.js.map +1 -0
  13. package/dist/{chunk-TILAKDN6.js → chunk-HJ3WREGP.js} +2 -2
  14. package/dist/{chunk-BGUID5BS.js → chunk-J7DNV2DH.js} +219 -26
  15. package/dist/chunk-J7DNV2DH.js.map +1 -0
  16. package/dist/{chunk-DL5BXZCX.js → chunk-K2JBCB7R.js} +40 -54
  17. package/dist/chunk-K2JBCB7R.js.map +1 -0
  18. package/dist/{chunk-MRRX4UQB.js → chunk-ODJAAJGZ.js} +2 -2
  19. package/dist/{chunk-447U3GQJ.js → chunk-PDFS5VFE.js} +17 -5
  20. package/dist/chunk-PDFS5VFE.js.map +1 -0
  21. package/dist/{chunk-GFUB57IT.js → chunk-VXDPUOQ5.js} +384 -207
  22. package/dist/chunk-VXDPUOQ5.js.map +1 -0
  23. package/dist/{cli-CL4ID7EO.js → cli-MKXCNEMW.js} +14 -14
  24. package/dist/codealmanac.js +1 -1
  25. package/dist/{config-ML2RCR7J.js → config-F7FKEQ7F.js} +3 -3
  26. package/dist/doctor-37UH3HT5.js +17 -0
  27. package/dist/{hook-2NP3UE7U.js → hook-4SVX446M.js} +4 -2
  28. package/dist/{register-commands-FBJ6XQ3L.js → register-commands-2F6SXLDI.js} +30 -21
  29. package/dist/register-commands-2F6SXLDI.js.map +1 -0
  30. package/dist/uninstall-C62ZOK32.js +17 -0
  31. package/dist/{update-P2IPG7RO.js → update-2UGOFN5C.js} +3 -3
  32. package/guides/mini.md +3 -3
  33. package/guides/reference.md +7 -7
  34. package/package.json +4 -3
  35. package/dist/chunk-447U3GQJ.js.map +0 -1
  36. package/dist/chunk-6BJUYZ43.js.map +0 -1
  37. package/dist/chunk-BGUID5BS.js.map +0 -1
  38. package/dist/chunk-DL5BXZCX.js.map +0 -1
  39. package/dist/chunk-GFUB57IT.js.map +0 -1
  40. package/dist/chunk-P5WGG4FJ.js.map +0 -1
  41. package/dist/chunk-SMIK2YLU.js.map +0 -1
  42. package/dist/doctor-DOLJRGS4.js +0 -17
  43. package/dist/register-commands-FBJ6XQ3L.js.map +0 -1
  44. package/dist/uninstall-DX6LFKMX.js +0 -15
  45. /package/dist/{agents-RVTQYE6A.js.map → agents-HYRWRHRX.js.map} +0 -0
  46. /package/dist/{chunk-TILAKDN6.js.map → chunk-HJ3WREGP.js.map} +0 -0
  47. /package/dist/{chunk-MRRX4UQB.js.map → chunk-ODJAAJGZ.js.map} +0 -0
  48. /package/dist/{cli-CL4ID7EO.js.map → cli-MKXCNEMW.js.map} +0 -0
  49. /package/dist/{config-ML2RCR7J.js.map → config-F7FKEQ7F.js.map} +0 -0
  50. /package/dist/{doctor-DOLJRGS4.js.map → doctor-37UH3HT5.js.map} +0 -0
  51. /package/dist/{hook-2NP3UE7U.js.map → hook-4SVX446M.js.map} +0 -0
  52. /package/dist/{uninstall-DX6LFKMX.js.map → uninstall-C62ZOK32.js.map} +0 -0
  53. /package/dist/{update-P2IPG7RO.js.map → update-2UGOFN5C.js.map} +0 -0
@@ -1,53 +1,113 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runHookInstall
4
- } from "./chunk-447U3GQJ.js";
4
+ } from "./chunk-PDFS5VFE.js";
5
5
  import {
6
6
  buildProviderModelChoices,
7
7
  buildProviderSetupView,
8
- getProviderLabel,
9
8
  parseAgentSelection
10
- } from "./chunk-BGUID5BS.js";
9
+ } from "./chunk-J7DNV2DH.js";
11
10
  import {
11
+ disabledAgentProviderMessage,
12
+ formatEnabledAgentProviderList,
12
13
  isAgentProviderId,
14
+ isEnabledAgentProviderId,
13
15
  readConfig,
14
16
  writeConfig
15
- } from "./chunk-P5WGG4FJ.js";
17
+ } from "./chunk-3E7JNMTZ.js";
18
+
19
+ // src/agent/providers/codex-instructions.ts
20
+ import { existsSync } from "fs";
21
+ import { mkdir, readFile, writeFile } from "fs/promises";
22
+ import path from "path";
23
+ var CODEX_INSTRUCTIONS_START = "<!-- codealmanac:start -->";
24
+ var CODEX_INSTRUCTIONS_END = "<!-- codealmanac:end -->";
25
+ var CODEX_INSTRUCTIONS_BODY = `${CODEX_INSTRUCTIONS_START}
26
+ ## codealmanac
27
+
28
+ Use codealmanac before answering codebase questions:
29
+ - Search the wiki for relevant context.
30
+ - Read matching pages before making claims about the codebase.
31
+ - Update the wiki when implementation decisions, workflows, invariants, or gotchas change.
32
+
33
+ ${CODEX_INSTRUCTIONS_END}`;
34
+ async function ensureCodexInstructions(codexDir) {
35
+ await mkdir(codexDir, { recursive: true });
36
+ const agentsPath = await resolveCodexAgentsPath(codexDir);
37
+ let existing = "";
38
+ if (existsSync(agentsPath)) {
39
+ existing = await readFile(agentsPath, "utf8");
40
+ }
41
+ const next = upsertManagedBlock(
42
+ existing,
43
+ CODEX_INSTRUCTIONS_START,
44
+ CODEX_INSTRUCTIONS_END,
45
+ CODEX_INSTRUCTIONS_BODY
46
+ );
47
+ if (next === existing) return false;
48
+ await writeFile(agentsPath, next, "utf8");
49
+ return true;
50
+ }
51
+ async function resolveCodexAgentsPath(codexDir) {
52
+ const overridePath = path.join(codexDir, "AGENTS.override.md");
53
+ if (existsSync(overridePath)) {
54
+ try {
55
+ const body = await readFile(overridePath, "utf8");
56
+ if (body.trim().length > 0) return overridePath;
57
+ } catch {
58
+ }
59
+ }
60
+ return path.join(codexDir, "AGENTS.md");
61
+ }
62
+ function hasCodexInstructions(contents) {
63
+ return contents.includes(CODEX_INSTRUCTIONS_START) && contents.includes(CODEX_INSTRUCTIONS_END);
64
+ }
65
+ function upsertManagedBlock(contents, start, end, block) {
66
+ const startIndex = contents.indexOf(start);
67
+ const endIndex = contents.indexOf(end);
68
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
69
+ const afterEnd = endIndex + end.length;
70
+ return `${contents.slice(0, startIndex)}${block}${contents.slice(afterEnd)}`;
71
+ }
72
+ const sep = contents.length === 0 ? "" : contents.endsWith("\n") ? "\n" : "\n\n";
73
+ return `${contents}${sep}${block}
74
+ `;
75
+ }
16
76
 
17
77
  // src/commands/setup.ts
18
- import { existsSync as existsSync2 } from "fs";
78
+ import { existsSync as existsSync3 } from "fs";
19
79
  import { spawn } from "child_process";
20
80
  import {
21
81
  copyFile,
22
- mkdir,
23
- readFile,
24
- writeFile
82
+ mkdir as mkdir2,
83
+ readFile as readFile2,
84
+ writeFile as writeFile2
25
85
  } from "fs/promises";
26
86
  import { createRequire as createRequire2 } from "module";
27
87
  import { homedir as homedir2 } from "os";
28
- import path3 from "path";
88
+ import path4 from "path";
29
89
  import { fileURLToPath as fileURLToPath2 } from "url";
30
90
 
31
91
  // src/commands/setup/install-path.ts
32
92
  import { execFile } from "child_process";
33
93
  import { createRequire } from "module";
34
94
  import { homedir } from "os";
35
- import path from "path";
95
+ import path2 from "path";
36
96
  import { fileURLToPath } from "url";
37
97
  function detectCurrentInstallPath() {
38
98
  try {
39
99
  const req = createRequire(import.meta.url);
40
100
  const here = fileURLToPath(import.meta.url);
41
- let dir = path.dirname(here);
101
+ let dir = path2.dirname(here);
42
102
  for (let i = 0; i < 6; i++) {
43
- const pkgPath = path.join(dir, "package.json");
103
+ const pkgPath = path2.join(dir, "package.json");
44
104
  try {
45
105
  const raw = req("fs").readFileSync(pkgPath, "utf-8");
46
106
  const pkg = JSON.parse(raw);
47
107
  if (pkg.name === "codealmanac") return dir;
48
108
  } catch {
49
109
  }
50
- const parent = path.dirname(dir);
110
+ const parent = path2.dirname(dir);
51
111
  if (parent === dir) break;
52
112
  dir = parent;
53
113
  }
@@ -58,8 +118,8 @@ function detectCurrentInstallPath() {
58
118
  function detectEphemeral(installPath) {
59
119
  if (installPath.length === 0) return false;
60
120
  const home = homedir();
61
- if (installPath.startsWith(path.join(home, ".npm", "_npx"))) return true;
62
- if (installPath.startsWith(path.join(home, ".local", "share", "pnpm", "dlx"))) return true;
121
+ if (installPath.startsWith(path2.join(home, ".npm", "_npx"))) return true;
122
+ if (installPath.startsWith(path2.join(home, ".local", "share", "pnpm", "dlx"))) return true;
63
123
  if (installPath.startsWith("/tmp/")) return true;
64
124
  if (installPath.startsWith("/var/folders/")) return true;
65
125
  return false;
@@ -86,8 +146,8 @@ function spawnGlobalInstall() {
86
146
  }
87
147
 
88
148
  // src/commands/setup/next-steps.ts
89
- import { existsSync, readdirSync } from "fs";
90
- import path2 from "path";
149
+ import { existsSync as existsSync2, readdirSync } from "fs";
150
+ import path3 from "path";
91
151
  var RST = "\x1B[0m";
92
152
  var BOLD = "\x1B[1m";
93
153
  var DIM = "\x1B[2m";
@@ -142,8 +202,8 @@ function countExistingPages(cwd) {
142
202
  try {
143
203
  let dir = cwd;
144
204
  for (let i = 0; i < 10; i++) {
145
- const pagesDir = path2.join(dir, ".almanac", "pages");
146
- if (existsSync(pagesDir)) {
205
+ const pagesDir = path3.join(dir, ".almanac", "pages");
206
+ if (existsSync2(pagesDir)) {
147
207
  try {
148
208
  const entries = readdirSync(pagesDir);
149
209
  return entries.filter((e) => e.endsWith(".md")).length;
@@ -151,7 +211,7 @@ function countExistingPages(cwd) {
151
211
  return 0;
152
212
  }
153
213
  }
154
- const parent = path2.dirname(dir);
214
+ const parent = path3.dirname(dir);
155
215
  if (parent === dir) break;
156
216
  dir = parent;
157
217
  }
@@ -191,7 +251,7 @@ function printBanner(out) {
191
251
  `);
192
252
  }
193
253
  out.write(`
194
- ${WHITE_BOLD2} Install provider integrations${RST2}
254
+ ${WHITE_BOLD2} Set up your automatic codebase wiki${RST2}
195
255
  `);
196
256
  }
197
257
  function printBadge(out) {
@@ -224,13 +284,25 @@ async function runSetup(options = {}) {
224
284
  }
225
285
  printBanner(out);
226
286
  printBadge(out);
227
- const agentChoice = await chooseDefaultAgent({
228
- out,
229
- interactive,
230
- requested: options.agent,
231
- requestedModel: options.model,
232
- spawnCli: options.spawnCli
233
- });
287
+ let agentChoice;
288
+ try {
289
+ agentChoice = await chooseDefaultAgent({
290
+ out,
291
+ interactive,
292
+ requested: options.agent,
293
+ requestedModel: options.model,
294
+ spawnCli: options.spawnCli
295
+ });
296
+ } catch (err) {
297
+ if (isSetupInterrupted(err)) {
298
+ return {
299
+ stdout: "",
300
+ stderr: "almanac: setup cancelled\n",
301
+ exitCode: 130
302
+ };
303
+ }
304
+ throw err;
305
+ }
234
306
  if (!agentChoice.ok) {
235
307
  return {
236
308
  stdout: "",
@@ -241,7 +313,7 @@ async function runSetup(options = {}) {
241
313
  }
242
314
  stepDone(
243
315
  out,
244
- `Default provider: ${WHITE_BOLD2}${agentChoice.provider}${RST2} (${agentChoice.model ?? "provider default"})`
316
+ `Agent: ${WHITE_BOLD2}${agentChoice.provider}${RST2} (${agentChoice.model ?? "provider default"})`
245
317
  );
246
318
  out.write(BAR + "\n");
247
319
  const ephem = options.installPath !== void 0 ? options.installPath !== null ? detectEphemeral(options.installPath) : false : detectEphemeral(detectCurrentInstallPath());
@@ -281,7 +353,7 @@ async function runSetup(options = {}) {
281
353
  } else if (interactive) {
282
354
  hookAction = await confirm(
283
355
  out,
284
- "Install auto-capture hooks for supported agents?",
356
+ "Keep your codebase wiki up to date automatically?",
285
357
  true
286
358
  );
287
359
  }
@@ -294,27 +366,26 @@ async function runSetup(options = {}) {
294
366
  stableHooksDir: options.stableHooksDir
295
367
  });
296
368
  if (res.exitCode !== 0) {
297
- stepActive(out, `Auto-capture hook: ${res.stderr.trim()}`);
369
+ stepActive(out, `SessionEnd hook: ${res.stderr.trim()}`);
298
370
  return {
299
371
  stdout: "",
300
372
  stderr: res.stderr,
301
373
  exitCode: res.exitCode
302
374
  };
303
375
  }
304
- hookResultLine = res.stdout.includes("already installed") ? `Auto-capture hooks ${DIM2}already installed${RST2}` : `Auto-capture hooks installed`;
376
+ hookResultLine = res.stdout.includes("already installed") ? `Auto-capture hooks ${DIM2}already installed${RST2}` : `Auto-capture installed`;
305
377
  stepDone(out, hookResultLine);
306
378
  } else {
307
- stepSkipped(out, `Auto-capture hooks ${DIM2}skipped${RST2}`);
379
+ stepSkipped(out, `Auto-capture ${DIM2}skipped${RST2}`);
308
380
  }
309
381
  out.write(BAR + "\n");
310
382
  let guidesAction = "install";
311
- const providerLabel = getProviderLabel(agentChoice.provider);
312
383
  if (options.skipGuides === true) {
313
384
  guidesAction = "skip";
314
385
  } else if (interactive) {
315
386
  guidesAction = await confirm(
316
387
  out,
317
- `Install the codealmanac guide for ${providerLabel}?`,
388
+ "Add codealmanac instructions for your AI agents?",
318
389
  true
319
390
  );
320
391
  }
@@ -322,12 +393,11 @@ async function runSetup(options = {}) {
322
393
  if (guidesAction === "install") {
323
394
  try {
324
395
  const summary = await installGuides({
325
- provider: agentChoice.provider,
326
- claudeDir: options.claudeDir ?? path3.join(homedir2(), ".claude"),
327
- guidesDir: options.guidesDir ?? resolveGuidesDir(),
328
- cwd: process.cwd()
396
+ claudeDir: options.claudeDir ?? path4.join(homedir2(), ".claude"),
397
+ codexDir: options.codexDir ?? path4.join(homedir2(), ".codex"),
398
+ guidesDir: options.guidesDir ?? resolveGuidesDir()
329
399
  });
330
- guidesSummary = summary.anyChanges ? `Guide installed for ${summary.providerLabel} (${summary.filesWritten.join(", ")})` : `Guide for ${summary.providerLabel} ${DIM2}already installed${RST2}`;
400
+ guidesSummary = summary.anyChanges ? `Agent instructions added` : `Agent instructions ${DIM2}already added${RST2}`;
331
401
  stepDone(out, guidesSummary);
332
402
  } catch (err) {
333
403
  const msg = err instanceof Error ? err.message : String(err);
@@ -339,14 +409,13 @@ async function runSetup(options = {}) {
339
409
  };
340
410
  }
341
411
  } else {
342
- stepSkipped(out, `Guides ${DIM2}skipped${RST2}`);
412
+ stepSkipped(out, `Agent instructions ${DIM2}skipped${RST2}`);
343
413
  }
344
414
  out.write(BAR + "\n");
345
415
  stepDone(out, `${BLUE2}Setup complete${RST2}`);
346
416
  out.write("\n");
347
417
  const existingPageCount = countExistingPages(process.cwd());
348
418
  printNextSteps(out, existingPageCount);
349
- printProviderNextSteps(out);
350
419
  return { stdout: "", stderr: "", exitCode: 0 };
351
420
  }
352
421
  async function chooseDefaultAgent(args) {
@@ -357,34 +426,66 @@ async function chooseDefaultAgent(args) {
357
426
  view = await buildProviderSetupView({ config, spawnCli: args.spawnCli });
358
427
  }
359
428
  if (args.interactive && args.requested === void 0 && view !== null) {
360
- args.out.write(" Choose default provider for bootstrap/capture:\n");
361
- view.choices.forEach((choice, index) => {
362
- const tag = choice.recommended ? " recommended" : "";
363
- const status = choice.ready ? "ready" : "not ready";
364
- const detail = choice.account ?? choice.fixCommand ?? choice.detail;
365
- args.out.write(
366
- ` ${index + 1}. ${choice.label.padEnd(6)} ${status.padEnd(9)}${tag} ${detail}
367
- `
368
- );
369
- });
370
- selected = (await promptText(
371
- args.out,
372
- "Default provider",
373
- view.recommendedProvider
374
- )).toLowerCase();
375
- const number = Number.parseInt(selected, 10);
376
- if (Number.isInteger(number) && number >= 1 && number <= view.choices.length) {
377
- selected = view.choices[number - 1]?.id ?? selected;
429
+ while (true) {
430
+ const choice = await selectChoice({
431
+ out: args.out,
432
+ title: "Choose your agent",
433
+ help: "Choose the AI agent codealmanac should use.",
434
+ choices: view.choices.map((choice2) => ({
435
+ value: choice2,
436
+ line: formatProviderChoice(choice2),
437
+ aliases: [choice2.id, choice2.label.toLowerCase()]
438
+ })),
439
+ defaultIndex: Math.max(
440
+ 0,
441
+ view.choices.findIndex(
442
+ (choice2) => choice2.id === view?.recommendedProvider
443
+ )
444
+ )
445
+ });
446
+ if (choice.ready) {
447
+ selected = choice.id;
448
+ break;
449
+ }
450
+ if (choice.readiness === "not-authenticated" && choice.fixCommand !== null) {
451
+ const command = choice.fixCommand.startsWith("run: ") ? choice.fixCommand.slice("run: ".length) : choice.fixCommand;
452
+ const runLogin = await confirm(
453
+ args.out,
454
+ `${choice.label} sign-in is needed. Run '${command}' now?`,
455
+ true
456
+ );
457
+ if (runLogin === "install") {
458
+ const login = await runLoginCommand(command);
459
+ if (!login.ok) {
460
+ stepActive(args.out, `${choice.label} login failed: ${login.error}`);
461
+ }
462
+ view = await buildProviderSetupView({ config, spawnCli: args.spawnCli });
463
+ const refreshed = view.choices.find((next) => next.id === choice.id);
464
+ if (refreshed?.ready === true) {
465
+ selected = refreshed.id;
466
+ break;
467
+ }
468
+ }
469
+ continue;
470
+ }
471
+ showUnavailableProvider(args.out, choice);
472
+ await waitForEnter(args.out, "Press Enter to choose a different agent.");
378
473
  }
379
474
  }
380
475
  const parsed = parseAgentSelection(selected);
381
476
  if (parsed.provider === null || !isAgentProviderId(parsed.provider)) {
382
477
  return {
383
478
  ok: false,
384
- error: `unknown agent '${selected}'. Expected one of: claude, codex, cursor.`
479
+ error: `unknown agent '${selected}'. Expected one of: ${formatEnabledAgentProviderList()}.`
385
480
  };
386
481
  }
387
482
  const provider = parsed.provider;
483
+ if (!isEnabledAgentProviderId(provider)) {
484
+ return {
485
+ ok: false,
486
+ error: disabledAgentProviderMessage(provider)
487
+ };
488
+ }
388
489
  let selectedChoice = view?.choices.find((choice) => choice.id === provider);
389
490
  if (args.interactive && selectedChoice !== void 0 && !selectedChoice.ready && selectedChoice.fixCommand?.startsWith("run: ") === true) {
390
491
  const command = selectedChoice.fixCommand.slice("run: ".length);
@@ -403,6 +504,12 @@ async function chooseDefaultAgent(args) {
403
504
  }
404
505
  }
405
506
  }
507
+ if (selectedChoice !== void 0 && !selectedChoice.ready) {
508
+ return {
509
+ ok: false,
510
+ error: `${selectedChoice.label} is not ready: ${selectedChoice.fixCommand ?? selectedChoice.detail}`
511
+ };
512
+ }
406
513
  const requestedModel = args.requestedModel ?? parsed.model;
407
514
  const model = requestedModel ?? await chooseProviderModel({
408
515
  out: args.out,
@@ -422,47 +529,210 @@ async function chooseDefaultAgent(args) {
422
529
  }
423
530
  }
424
531
  });
425
- if (!args.interactive || args.requested !== void 0) {
532
+ if ((!args.interactive || args.requested !== void 0) && selectedChoice !== void 0) {
426
533
  const detail = selectedChoice?.ready === true ? "ready" : selectedChoice?.fixCommand ?? selectedChoice?.detail ?? "status unknown";
427
534
  stepDone(args.out, `Agent readiness: ${detail}`);
428
535
  }
429
536
  return { ok: true, provider, model };
430
537
  }
431
538
  async function chooseProviderModel(args) {
432
- const choices = args.choice?.modelChoices ?? buildProviderModelChoices(args.provider, args.configuredModel);
539
+ const choices = args.choice?.modelChoices ?? await buildProviderModelChoices(args.provider, args.configuredModel);
433
540
  const recommended = choices.find((choice) => choice.recommended) ?? choices.find((choice) => choice.source === "provider-default");
434
541
  if (!args.interactive) {
435
542
  return args.configuredModel ?? recommended?.value ?? null;
436
543
  }
437
- args.out.write(` Choose model for ${getProviderLabel(args.provider)}:
438
- `);
439
- choices.forEach((choice, index) => {
440
- const marker = choice.recommended ? " recommended" : "";
441
- const current = choice.value === args.configuredModel ? " current" : "";
442
- args.out.write(
443
- ` ${index + 1}. ${choice.label}${marker}${current}
444
- `
445
- );
446
- });
447
544
  const currentIndex = choices.findIndex(
448
545
  (choice) => choice.value === args.configuredModel
449
546
  );
450
547
  const recommendedIndex = choices.findIndex((choice) => choice.recommended);
451
- const defaultIndex = currentIndex >= 0 ? currentIndex + 1 : recommendedIndex >= 0 ? recommendedIndex + 1 : 1;
452
- const selected = await promptText(args.out, "Model", String(defaultIndex));
453
- const number = Number.parseInt(selected, 10);
454
- let modelChoice;
455
- if (Number.isInteger(number) && number >= 1 && number <= choices.length) {
456
- modelChoice = choices[number - 1];
457
- } else {
458
- modelChoice = choices.find((choice) => choice.value === selected);
459
- }
548
+ const defaultIndex = Math.max(
549
+ 0,
550
+ currentIndex >= 0 ? currentIndex : recommendedIndex >= 0 ? recommendedIndex : 0
551
+ );
552
+ const modelChoice = await selectChoice({
553
+ out: args.out,
554
+ title: `Choose ${providerDisplayName(args.provider)} model`,
555
+ choices: choices.map((choice) => ({
556
+ value: choice,
557
+ line: formatModelChoice(choice, args.configuredModel),
558
+ aliases: choice.value === null ? ["default", "provider default"] : [String(choice.value)]
559
+ })),
560
+ defaultIndex
561
+ });
460
562
  if (modelChoice?.source === "custom") {
461
- const custom = await promptText(args.out, "Custom model id", "");
563
+ const custom = await promptText(args.out, "Model name", "");
462
564
  return custom.length > 0 ? custom : recommended?.value ?? null;
463
565
  }
464
566
  return modelChoice?.value ?? recommended?.value ?? null;
465
567
  }
568
+ async function selectChoice(args) {
569
+ const selected = clampIndex(args.defaultIndex, args.choices.length);
570
+ if (canUseRawSelect()) {
571
+ return await selectChoiceRaw({ ...args, defaultIndex: selected });
572
+ }
573
+ renderSelect(args.out, {
574
+ title: args.title,
575
+ help: args.help,
576
+ choices: args.choices,
577
+ selected,
578
+ raw: false
579
+ });
580
+ const answer = await promptText(args.out, "Select", String(selected + 1));
581
+ const index = Number.parseInt(answer, 10);
582
+ if (Number.isInteger(index) && index >= 1 && index <= args.choices.length) {
583
+ return args.choices[index - 1].value;
584
+ }
585
+ const normalized = answer.trim().toLowerCase();
586
+ const matched = args.choices.find(
587
+ (choice) => choice.aliases?.some((alias) => alias.toLowerCase() === normalized)
588
+ );
589
+ return (matched ?? args.choices[selected]).value;
590
+ }
591
+ async function selectChoiceRaw(args) {
592
+ return new Promise((resolve, reject) => {
593
+ let selected = args.defaultIndex;
594
+ let renderedLines = 0;
595
+ const input = process.stdin;
596
+ const render = () => {
597
+ if (renderedLines > 0) {
598
+ args.out.write(`\x1B[${renderedLines}A\x1B[0J`);
599
+ }
600
+ renderedLines = renderSelect(args.out, {
601
+ title: args.title,
602
+ help: args.help,
603
+ choices: args.choices,
604
+ selected,
605
+ raw: true
606
+ });
607
+ };
608
+ const cleanup = () => {
609
+ input.removeListener("data", onData);
610
+ input.setRawMode?.(false);
611
+ input.pause();
612
+ };
613
+ const onData = (chunk) => {
614
+ const key = chunk.toString("utf8");
615
+ if (key === "") {
616
+ cleanup();
617
+ args.out.write("\n");
618
+ reject(new SetupInterruptedError());
619
+ return;
620
+ }
621
+ if (key === "\r" || key === "\n") {
622
+ cleanup();
623
+ args.out.write("\n");
624
+ resolve(args.choices[selected].value);
625
+ return;
626
+ }
627
+ if (key === "\x1B[A") {
628
+ selected = selected === 0 ? args.choices.length - 1 : selected - 1;
629
+ render();
630
+ } else if (key === "\x1B[B") {
631
+ selected = selected === args.choices.length - 1 ? 0 : selected + 1;
632
+ render();
633
+ }
634
+ };
635
+ input.setRawMode?.(true);
636
+ input.resume();
637
+ input.on("data", onData);
638
+ render();
639
+ });
640
+ }
641
+ function renderSelect(out, args) {
642
+ let lines = 0;
643
+ out.write(` ${WHITE_BOLD2}${args.title}${RST2}
644
+ `);
645
+ lines++;
646
+ if (args.help !== void 0) {
647
+ out.write(` ${DIM2}${args.help}${RST2}
648
+ `);
649
+ lines++;
650
+ }
651
+ out.write("\n");
652
+ lines++;
653
+ args.choices.forEach((choice, index) => {
654
+ const pointer = index === args.selected ? `${BLUE2}\u203A${RST2}` : " ";
655
+ out.write(` ${pointer} ${choice.line}
656
+ `);
657
+ lines++;
658
+ });
659
+ const hint = args.raw ? `Use \u2191/\u2193 to move, Enter to select` : `Type a number or name, then press Enter`;
660
+ out.write(`
661
+ ${DIM2}${hint}${RST2}
662
+ `);
663
+ lines += 2;
664
+ return lines;
665
+ }
666
+ var SetupInterruptedError = class extends Error {
667
+ constructor() {
668
+ super("setup interrupted");
669
+ }
670
+ };
671
+ function isSetupInterrupted(err) {
672
+ return err instanceof SetupInterruptedError;
673
+ }
674
+ function canUseRawSelect() {
675
+ const input = process.stdin;
676
+ return process.stdin.isTTY === true && typeof input.setRawMode === "function";
677
+ }
678
+ function clampIndex(index, length) {
679
+ if (length <= 0) return 0;
680
+ if (index < 0) return 0;
681
+ if (index >= length) return length - 1;
682
+ return index;
683
+ }
684
+ function formatProviderChoice(choice) {
685
+ const status = providerStatusLabel(choice);
686
+ const detail = providerDetailLabel(choice);
687
+ const tag = choice.recommended ? ` ${DIM2}recommended${RST2}` : "";
688
+ return `${choice.label.padEnd(8)} ${status.padEnd(15)} ${detail}${tag}`;
689
+ }
690
+ function providerStatusLabel(choice) {
691
+ if (choice.ready) {
692
+ return choice.detail === "ANTHROPIC_API_KEY set" ? "API key set" : "signed in";
693
+ }
694
+ return choice.readiness === "missing" ? "not installed" : "sign in needed";
695
+ }
696
+ function providerDetailLabel(choice) {
697
+ if (choice.ready) return choice.account ?? choice.detail;
698
+ if (choice.fixCommand === null) return choice.detail;
699
+ return choice.fixCommand.startsWith("run: ") ? choice.fixCommand.slice("run: ".length) : choice.fixCommand;
700
+ }
701
+ function showUnavailableProvider(out, choice) {
702
+ if (choice.readiness === "missing") {
703
+ out.write(
704
+ `
705
+ ${WHITE_BOLD2}${choice.label} is not installed.${RST2}
706
+ ${providerDetailLabel(choice)}
707
+
708
+ `
709
+ );
710
+ return;
711
+ }
712
+ out.write(
713
+ `
714
+ ${WHITE_BOLD2}${choice.label} is not signed in.${RST2}
715
+ Run: ${providerDetailLabel(choice)}
716
+
717
+ `
718
+ );
719
+ }
720
+ function formatModelChoice(choice, configuredModel) {
721
+ const marker = choice.recommended ? ` ${DIM2}recommended${RST2}` : choice.value === configuredModel ? ` ${DIM2}current${RST2}` : "";
722
+ const label = choice.source === "provider-default" && choice.value !== null ? friendlyModelLabel(choice.value) : choice.label;
723
+ return `${label}${marker}`;
724
+ }
725
+ function friendlyModelLabel(value) {
726
+ if (value === "claude-sonnet-4-6") return "Sonnet 4.6";
727
+ if (value === "claude-opus-4-7") return "Opus 4.7";
728
+ if (value === "claude-haiku-4-5-20251001") return "Haiku 4.5";
729
+ return value;
730
+ }
731
+ function providerDisplayName(provider) {
732
+ if (provider === "claude") return "Claude";
733
+ if (provider === "codex") return "Codex";
734
+ return "Cursor";
735
+ }
466
736
  async function runLoginCommand(command) {
467
737
  return new Promise((resolve) => {
468
738
  const child = spawn(command, {
@@ -481,83 +751,35 @@ async function runLoginCommand(command) {
481
751
  });
482
752
  });
483
753
  }
484
- var CODEX_BLOCK_START = "<!-- codealmanac:start -->";
485
- var CODEX_BLOCK_END = "<!-- codealmanac:end -->";
486
754
  async function installGuides(options) {
487
- const srcMini = path3.join(options.guidesDir, "mini.md");
488
- const srcRef = path3.join(options.guidesDir, "reference.md");
489
- if (!existsSync2(srcMini)) {
755
+ await mkdir2(options.claudeDir, { recursive: true });
756
+ const srcMini = path4.join(options.guidesDir, "mini.md");
757
+ const srcRef = path4.join(options.guidesDir, "reference.md");
758
+ if (!existsSync3(srcMini)) {
490
759
  throw new Error(`missing bundled guide: ${srcMini}`);
491
760
  }
492
- if (!existsSync2(srcRef)) {
761
+ if (!existsSync3(srcRef)) {
493
762
  throw new Error(`missing bundled guide: ${srcRef}`);
494
763
  }
495
- switch (options.provider) {
496
- case "claude":
497
- return await installClaudeGuides(options, srcMini, srcRef);
498
- case "codex":
499
- return await installCodexGuides(srcMini, srcRef);
500
- case "cursor":
501
- return await installCursorGuides(options.cwd, srcMini);
502
- }
503
- }
504
- async function installClaudeGuides(options, srcMini, srcRef) {
505
- await mkdir(options.claudeDir, { recursive: true });
506
- const destMini = path3.join(options.claudeDir, "codealmanac.md");
507
- const destRef = path3.join(options.claudeDir, "codealmanac-reference.md");
764
+ const destMini = path4.join(options.claudeDir, "codealmanac.md");
765
+ const destRef = path4.join(options.claudeDir, "codealmanac-reference.md");
508
766
  const miniChanged = await copyIfChanged(srcMini, destMini);
509
767
  const refChanged = await copyIfChanged(srcRef, destRef);
510
- const claudeMd = path3.join(options.claudeDir, "CLAUDE.md");
768
+ const claudeMd = path4.join(options.claudeDir, "CLAUDE.md");
511
769
  const importChanged = await ensureImport(claudeMd);
770
+ const codexChanged = await ensureCodexInstructions(options.codexDir);
512
771
  const filesWritten = [];
513
772
  if (miniChanged) filesWritten.push("codealmanac.md");
514
773
  if (refChanged) filesWritten.push("codealmanac-reference.md");
515
774
  if (importChanged) filesWritten.push("CLAUDE.md");
516
- return {
517
- providerLabel: "Claude",
518
- anyChanges: filesWritten.length > 0,
519
- filesWritten
520
- };
521
- }
522
- async function installCodexGuides(srcMini, srcRef) {
523
- const codexDir = path3.join(homedir2(), ".codex");
524
- await mkdir(codexDir, { recursive: true });
525
- const destMini = path3.join(codexDir, "codealmanac.md");
526
- const destRef = path3.join(codexDir, "codealmanac-reference.md");
527
- const miniChanged = await copyIfChanged(srcMini, destMini);
528
- const refChanged = await copyIfChanged(srcRef, destRef);
529
- const agentsChanged = await ensureManagedBlock(
530
- path3.join(codexDir, "AGENTS.md"),
531
- codexGuideBlock()
532
- );
533
- const filesWritten = [];
534
- if (miniChanged) filesWritten.push("codealmanac.md");
535
- if (refChanged) filesWritten.push("codealmanac-reference.md");
536
- if (agentsChanged) filesWritten.push("AGENTS.md");
537
- return {
538
- providerLabel: "Codex",
539
- anyChanges: filesWritten.length > 0,
540
- filesWritten
541
- };
542
- }
543
- async function installCursorGuides(cwd, srcMini) {
544
- const rulesDir = path3.join(cwd, ".cursor", "rules");
545
- await mkdir(rulesDir, { recursive: true });
546
- const mini = await readFile(srcMini, "utf8");
547
- const dest = path3.join(rulesDir, "codealmanac.mdc");
548
- const body = "---\ndescription: Use codealmanac wiki context during coding work\nalwaysApply: true\n---\n\n" + mini;
549
- const changed = await writeIfChanged(dest, body);
550
- return {
551
- providerLabel: "Cursor",
552
- anyChanges: changed,
553
- filesWritten: changed ? [".cursor/rules/codealmanac.mdc"] : []
554
- };
775
+ if (codexChanged) filesWritten.push("AGENTS.md");
776
+ return { anyChanges: filesWritten.length > 0, filesWritten };
555
777
  }
556
778
  async function copyIfChanged(src, dest) {
557
- const srcBytes = await readFile(src);
558
- if (existsSync2(dest)) {
779
+ const srcBytes = await readFile2(src);
780
+ if (existsSync3(dest)) {
559
781
  try {
560
- const destBytes = await readFile(dest);
782
+ const destBytes = await readFile2(dest);
561
783
  if (srcBytes.equals(destBytes)) return false;
562
784
  } catch {
563
785
  }
@@ -565,56 +787,17 @@ async function copyIfChanged(src, dest) {
565
787
  await copyFile(src, dest);
566
788
  return true;
567
789
  }
568
- async function writeIfChanged(dest, body) {
569
- if (existsSync2(dest)) {
570
- try {
571
- if (await readFile(dest, "utf8") === body) return false;
572
- } catch {
573
- }
574
- }
575
- await writeFile(dest, body, "utf8");
576
- return true;
577
- }
578
- async function ensureManagedBlock(file, block) {
579
- let existing = "";
580
- if (existsSync2(file)) existing = await readFile(file, "utf8");
581
- const pattern = new RegExp(
582
- `${escapeRegex(CODEX_BLOCK_START)}[\\s\\S]*?${escapeRegex(CODEX_BLOCK_END)}`
583
- );
584
- const next = pattern.test(existing) ? existing.replace(pattern, block.trimEnd()) : `${existing.trimEnd()}${existing.trim().length > 0 ? "\n\n" : ""}${block.trimEnd()}
585
- `;
586
- const normalized = next.endsWith("\n") ? next : `${next}
587
- `;
588
- if (normalized === existing) return false;
589
- await writeFile(file, normalized, "utf8");
590
- return true;
591
- }
592
- function codexGuideBlock() {
593
- return [
594
- CODEX_BLOCK_START,
595
- "# codealmanac",
596
- "",
597
- "This machine has codealmanac installed: a local wiki for codebases.",
598
- "Before codealmanac/wiki-related work, read `~/.codex/codealmanac.md`.",
599
- "For the full command reference, read `~/.codex/codealmanac-reference.md` on demand.",
600
- CODEX_BLOCK_END,
601
- ""
602
- ].join("\n");
603
- }
604
- function escapeRegex(value) {
605
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
606
- }
607
790
  var IMPORT_LINE = "@~/.claude/codealmanac.md";
608
791
  async function ensureImport(claudeMdPath) {
609
792
  let existing = "";
610
- if (existsSync2(claudeMdPath)) {
611
- existing = await readFile(claudeMdPath, "utf8");
793
+ if (existsSync3(claudeMdPath)) {
794
+ existing = await readFile2(claudeMdPath, "utf8");
612
795
  }
613
796
  if (hasImportLine(existing)) return false;
614
797
  const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
615
798
  const body = `${existing}${sep}${IMPORT_LINE}
616
799
  `;
617
- await writeFile(claudeMdPath, body, "utf8");
800
+ await writeFile2(claudeMdPath, body, "utf8");
618
801
  return true;
619
802
  }
620
803
  function hasImportLine(contents) {
@@ -664,14 +847,17 @@ function promptText(out, question, defaultValue) {
664
847
  process.stdin.on("data", onData);
665
848
  });
666
849
  }
850
+ async function waitForEnter(out, message) {
851
+ await promptText(out, message, "");
852
+ }
667
853
  function resolveGuidesDir() {
668
- const here = path3.dirname(fileURLToPath2(import.meta.url));
854
+ const here = path4.dirname(fileURLToPath2(import.meta.url));
669
855
  const candidates = [
670
- path3.resolve(here, "..", "guides"),
856
+ path4.resolve(here, "..", "guides"),
671
857
  // dist layout
672
- path3.resolve(here, "..", "..", "guides"),
858
+ path4.resolve(here, "..", "..", "guides"),
673
859
  // src layout
674
- path3.resolve(here, "..", "..", "..", "guides")
860
+ path4.resolve(here, "..", "..", "..", "guides")
675
861
  ];
676
862
  for (const dir of candidates) {
677
863
  if (looksLikeGuidesDir(dir)) return dir;
@@ -679,7 +865,7 @@ function resolveGuidesDir() {
679
865
  try {
680
866
  const require2 = createRequire2(import.meta.url);
681
867
  const pkgJson = require2.resolve("codealmanac/package.json");
682
- const guides = path3.join(path3.dirname(pkgJson), "guides");
868
+ const guides = path4.join(path4.dirname(pkgJson), "guides");
683
869
  if (looksLikeGuidesDir(guides)) return guides;
684
870
  } catch {
685
871
  }
@@ -688,24 +874,15 @@ function resolveGuidesDir() {
688
874
  );
689
875
  }
690
876
  function looksLikeGuidesDir(dir) {
691
- return existsSync2(path3.join(dir, "mini.md"));
692
- }
693
- function printProviderNextSteps(out) {
694
- out.write(` ${DIM2}Change provider/model later:${RST2}
695
- `);
696
- out.write(` ${DIM2} almanac agents list${RST2}
697
- `);
698
- out.write(` ${DIM2} almanac agents use <claude|codex|cursor>${RST2}
699
- `);
700
- out.write(
701
- ` ${DIM2} almanac agents model <provider> <model|--default>${RST2}
702
-
703
- `
704
- );
877
+ return existsSync3(path4.join(dir, "mini.md"));
705
878
  }
706
879
 
707
880
  export {
881
+ CODEX_INSTRUCTIONS_START,
882
+ CODEX_INSTRUCTIONS_END,
883
+ resolveCodexAgentsPath,
884
+ hasCodexInstructions,
708
885
  runSetup,
709
886
  IMPORT_LINE
710
887
  };
711
- //# sourceMappingURL=chunk-GFUB57IT.js.map
888
+ //# sourceMappingURL=chunk-VXDPUOQ5.js.map