akm-cli 0.7.1 → 0.7.3

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 (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +62 -16
  3. package/dist/commands/history.js +2 -7
  4. package/dist/commands/info.js +2 -2
  5. package/dist/commands/installed-stashes.js +45 -1
  6. package/dist/commands/search.js +2 -2
  7. package/dist/commands/show.js +4 -19
  8. package/dist/commands/source-add.js +1 -1
  9. package/dist/core/common.js +16 -1
  10. package/dist/core/config.js +18 -3
  11. package/dist/indexer/db-search.js +33 -39
  12. package/dist/indexer/db.js +51 -1
  13. package/dist/indexer/graph-extraction.js +5 -3
  14. package/dist/indexer/indexer.js +334 -121
  15. package/dist/indexer/manifest.js +18 -23
  16. package/dist/indexer/memory-inference.js +47 -58
  17. package/dist/indexer/metadata.js +253 -21
  18. package/dist/indexer/search-source.js +11 -5
  19. package/dist/llm/client.js +61 -1
  20. package/dist/llm/embedder.js +8 -5
  21. package/dist/llm/embedders/local.js +8 -2
  22. package/dist/llm/embedders/remote.js +4 -2
  23. package/dist/llm/graph-extract.js +4 -4
  24. package/dist/llm/memory-infer.js +61 -33
  25. package/dist/llm/metadata-enhance.js +2 -2
  26. package/dist/output/cli-hints.js +5 -2
  27. package/dist/output/renderers.js +22 -49
  28. package/dist/registry/build-index.js +13 -18
  29. package/dist/setup/setup.js +238 -96
  30. package/dist/sources/providers/git.js +14 -2
  31. package/dist/sources/providers/website.js +4 -460
  32. package/dist/sources/website-ingest.js +470 -0
  33. package/dist/wiki/wiki.js +11 -1
  34. package/dist/workflows/parser.js +19 -4
  35. package/dist/workflows/runs.js +3 -3
  36. package/docs/README.md +10 -3
  37. package/docs/migration/release-notes/0.7.0.md +22 -0
  38. package/package.json +5 -2
@@ -5,6 +5,8 @@
5
5
  * registry selection, stash sources, and agent platform discovery.
6
6
  * Collects all choices and writes config once at the end.
7
7
  */
8
+ import fs from "node:fs";
9
+ import os from "node:os";
8
10
  import path from "node:path";
9
11
  import * as p from "@clack/prompts";
10
12
  import { akmInit } from "../commands/init";
@@ -19,15 +21,22 @@ import { probeLlmCapabilities } from "../llm/client";
19
21
  import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
20
22
  import { detectAgentPlatforms, detectOllama } from "./detect";
21
23
  import { createSetupContext, runSetupSteps } from "./steps";
22
- // ── Constants ───────────────────────────────────────────────────────────────
23
24
  /**
24
25
  * Recommended GitHub repositories shown during setup.
25
- *
26
- * Currently empty — populating from the akm-registry at runtime is a
27
- * separate feature. The wizard prompt infrastructure is retained for that
28
- * future use.
29
26
  */
30
- const RECOMMENDED_GITHUB_REPOS = [];
27
+ const RECOMMENDED_GITHUB_REPOS = [
28
+ {
29
+ url: "https://github.com/itlackey/akm-stash",
30
+ name: "itlackey/akm-stash",
31
+ hint: "official onboarding stash",
32
+ defaultSelected: true,
33
+ },
34
+ {
35
+ url: "https://github.com/andrewyng/context-hub",
36
+ name: "andrewyng/context-hub",
37
+ hint: "optional community prompt and context stash",
38
+ },
39
+ ];
31
40
  // Approximate first-download sizes used in the setup note.
32
41
  // LOCAL_MODEL_APPROX_SIZE_MB tracks the default local model (DEFAULT_LOCAL_MODEL).
33
42
  const LOCAL_MODEL_APPROX_SIZE_MB = 130;
@@ -83,6 +92,114 @@ async function promptOrBack(fn) {
83
92
  return null;
84
93
  return result;
85
94
  }
95
+ function configuredSourceKey(source) {
96
+ return `${source.type}:${source.path ?? source.url ?? source.name ?? "unknown"}`;
97
+ }
98
+ function describeConfiguredSource(source) {
99
+ const target = source.path ?? source.url ?? "(unknown target)";
100
+ const typeLabel = source.type === "git" ? "Git" : source.type === "filesystem" ? "Filesystem" : source.type;
101
+ return {
102
+ value: configuredSourceKey(source),
103
+ label: source.name ?? target,
104
+ hint: `${typeLabel}: ${target}`,
105
+ };
106
+ }
107
+ function renderConfiguredSourceList(sources) {
108
+ return sources
109
+ .map((source) => {
110
+ const described = describeConfiguredSource(source);
111
+ return `- ${described.label} (${described.hint})`;
112
+ })
113
+ .join("\n");
114
+ }
115
+ function renderInstalledSourceList(installed) {
116
+ return installed.map((entry) => `- ${entry.id} (${entry.source})`).join("\n");
117
+ }
118
+ function cloneLlmConfig(llm) {
119
+ if (!llm)
120
+ return undefined;
121
+ return {
122
+ ...llm,
123
+ ...(llm.capabilities ? { capabilities: { ...llm.capabilities } } : {}),
124
+ ...(llm.features ? { features: { ...llm.features } } : {}),
125
+ ...(llm.extraParams ? { extraParams: { ...llm.extraParams } } : {}),
126
+ };
127
+ }
128
+ async function stepAdditionalSources(currentSources) {
129
+ const sources = [...currentSources];
130
+ let addMore = true;
131
+ while (addMore) {
132
+ const action = await prompt(() => p.select({
133
+ message: "Add another stash source?",
134
+ options: [
135
+ { value: "done", label: "Done — no more sources" },
136
+ { value: "github-repo", label: "GitHub repository", hint: "custom URL" },
137
+ { value: "filesystem", label: "Filesystem path", hint: "local directory" },
138
+ ],
139
+ initialValue: "done",
140
+ }));
141
+ if (action === "done") {
142
+ addMore = false;
143
+ break;
144
+ }
145
+ if (action === "github-repo") {
146
+ const url = await promptOrBack(() => p.text({
147
+ message: "Enter the GitHub repository URL:",
148
+ placeholder: "https://github.com/owner/repo",
149
+ validate: (v) => {
150
+ if (!v?.trim())
151
+ return "URL cannot be empty";
152
+ },
153
+ }));
154
+ if (url === null)
155
+ continue;
156
+ const name = await promptOrBack(() => p.text({
157
+ message: "Give this stash a name (optional):",
158
+ placeholder: "my-repo",
159
+ }));
160
+ if (name === null)
161
+ continue;
162
+ const entry = { type: "git", url: url.trim() };
163
+ if (name.trim())
164
+ entry.name = name.trim();
165
+ if (!sources.some((s) => s.url === entry.url)) {
166
+ sources.push(entry);
167
+ }
168
+ else {
169
+ p.log.warn("This URL is already configured.");
170
+ }
171
+ }
172
+ if (action === "filesystem") {
173
+ const fsPath = await promptOrBack(() => p.text({
174
+ message: "Enter the directory path:",
175
+ placeholder: "/path/to/stash",
176
+ validate: (v) => {
177
+ if (!v?.trim())
178
+ return "Path cannot be empty";
179
+ },
180
+ }));
181
+ if (fsPath === null)
182
+ continue;
183
+ const resolved = fsPath.trim();
184
+ const name = await promptOrBack(() => p.text({
185
+ message: "Give this stash a name (optional):",
186
+ placeholder: "my-stash",
187
+ }));
188
+ if (name === null)
189
+ continue;
190
+ const entry = { type: "filesystem", path: resolved };
191
+ if (name.trim())
192
+ entry.name = name.trim();
193
+ if (!sources.some((s) => s.path === entry.path)) {
194
+ sources.push(entry);
195
+ }
196
+ else {
197
+ p.log.warn("This path is already configured.");
198
+ }
199
+ }
200
+ }
201
+ return sources;
202
+ }
86
203
  /**
87
204
  * Quick connectivity check. Returns true if we can reach a public
88
205
  * endpoint within 3 seconds, false otherwise. Used to skip network-
@@ -189,8 +306,10 @@ async function prepareSemanticSearchAssets(config) {
189
306
  }
190
307
  spin.stop(remote ? "Remote embedding endpoint is ready." : "Local embedding model downloaded and ready.");
191
308
  let db;
309
+ let probeDir;
192
310
  try {
193
- db = openDatabase();
311
+ probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-setup-vec-probe-"));
312
+ db = openDatabase(path.join(probeDir, "probe.db"), config.embedding?.dimension ? { embeddingDim: config.embedding.dimension } : undefined);
194
313
  if (isVecAvailable(db)) {
195
314
  p.log.info("sqlite-vec is available for fast vector search.");
196
315
  }
@@ -206,6 +325,14 @@ async function prepareSemanticSearchAssets(config) {
206
325
  finally {
207
326
  if (db)
208
327
  closeDatabase(db);
328
+ if (probeDir) {
329
+ try {
330
+ fs.rmSync(probeDir, { recursive: true, force: true });
331
+ }
332
+ catch {
333
+ /* ignore cleanup failure */
334
+ }
335
+ }
209
336
  }
210
337
  return { ok: true };
211
338
  }
@@ -375,7 +502,7 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
375
502
  initialValue,
376
503
  }));
377
504
  if (choice === "keep")
378
- return current.llm;
505
+ return cloneLlmConfig(current.llm);
379
506
  if (choice === "none")
380
507
  return undefined;
381
508
  let llm;
@@ -466,7 +593,7 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
466
593
  }
467
594
  return llm;
468
595
  }
469
- async function stepRegistries(current) {
596
+ export async function stepRegistries(current) {
470
597
  const defaults = DEFAULT_CONFIG.registries ?? [];
471
598
  const currentRegistries = current.registries ?? defaults;
472
599
  const defaultUrls = new Set(defaults.map((r) => r.url));
@@ -507,119 +634,70 @@ async function stepRegistries(current) {
507
634
  /**
508
635
  * @internal Exported for testing only.
509
636
  */
510
- export async function stepAddSources(current) {
511
- const stashes = [...(current.sources ?? current.stashes ?? [])];
512
- if (stashes.length > 0) {
513
- p.log.info(`You have ${stashes.length} existing stash source(s).`);
637
+ export async function stepAddSources(current, options) {
638
+ const existingSources = [...(current.sources ?? current.stashes ?? [])];
639
+ const sources = [];
640
+ if (existingSources.length > 0) {
641
+ p.note(renderConfiguredSourceList(existingSources), "Configured stash sources");
642
+ const options = existingSources.map(describeConfiguredSource);
643
+ const selected = await prompt(() => p.multiselect({
644
+ message: "Configured stash sources — uncheck any you want to disable:",
645
+ options,
646
+ initialValues: options.map((option) => option.value),
647
+ required: false,
648
+ }));
649
+ for (const source of existingSources) {
650
+ if (selected.includes(configuredSourceKey(source))) {
651
+ sources.push(source);
652
+ }
653
+ }
654
+ }
655
+ if ((current.installed?.length ?? 0) > 0) {
656
+ p.note(renderInstalledSourceList(current.installed ?? []), "Installed managed stashes (preserved)");
514
657
  }
515
658
  // ── Recommended GitHub repos ───────────────────────────────────────────
516
659
  // Skip the prompt entirely when there are no recommendations to show.
517
660
  // The infrastructure is retained for a future registry-driven version.
518
661
  if (RECOMMENDED_GITHUB_REPOS.length > 0) {
519
- const existingUrls = new Set(stashes.map((s) => s.url));
662
+ const existingUrls = new Set(sources.map((s) => s.url));
520
663
  const repoOptions = RECOMMENDED_GITHUB_REPOS.map((r) => ({
521
664
  value: r.url,
522
665
  label: r.name,
523
666
  hint: existingUrls.has(r.url) ? `${r.hint} (already added)` : r.hint,
524
667
  }));
668
+ const initialValues = sources.length > 0
669
+ ? repoOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value)
670
+ : RECOMMENDED_GITHUB_REPOS.filter((r) => r.defaultSelected).map((r) => r.url);
525
671
  const selectedRepos = await prompt(() => p.multiselect({
526
672
  message: "Recommended GitHub repositories — toggle to add or remove:",
527
673
  options: repoOptions,
528
- initialValues: repoOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value),
674
+ initialValues,
529
675
  required: false,
530
676
  }));
531
677
  // Add newly selected repos
532
678
  for (const url of selectedRepos) {
533
679
  if (!existingUrls.has(url)) {
534
680
  const rec = RECOMMENDED_GITHUB_REPOS.find((r) => r.url === url);
535
- stashes.push({ type: "git", url, name: rec?.name });
681
+ sources.push({ type: "git", url, name: rec?.name });
536
682
  existingUrls.add(url);
537
683
  }
538
684
  }
539
685
  // Remove deselected repos that were previously configured
540
686
  for (const rec of RECOMMENDED_GITHUB_REPOS) {
541
687
  if (existingUrls.has(rec.url) && !selectedRepos.includes(rec.url)) {
542
- const idx = stashes.findIndex((s) => s.url === rec.url);
688
+ const idx = sources.findIndex((s) => s.url === rec.url);
543
689
  if (idx !== -1) {
544
- stashes.splice(idx, 1);
690
+ sources.splice(idx, 1);
545
691
  existingUrls.delete(rec.url);
546
692
  p.log.info(`Removed ${rec.name}.`);
547
693
  }
548
694
  }
549
695
  }
550
696
  }
551
- // ── Additional stash sources loop ──────────────────────────────────────
552
- let addMore = true;
553
- while (addMore) {
554
- const action = await prompt(() => p.select({
555
- message: "Add another stash source?",
556
- options: [
557
- { value: "github-repo", label: "GitHub repository", hint: "custom URL" },
558
- { value: "filesystem", label: "Filesystem path", hint: "local directory" },
559
- { value: "done", label: "Done — no more sources" },
560
- ],
561
- }));
562
- if (action === "done") {
563
- addMore = false;
564
- break;
565
- }
566
- if (action === "github-repo") {
567
- const url = await promptOrBack(() => p.text({
568
- message: "Enter the GitHub repository URL:",
569
- placeholder: "https://github.com/owner/repo",
570
- validate: (v) => {
571
- if (!v?.trim())
572
- return "URL cannot be empty";
573
- },
574
- }));
575
- if (url === null)
576
- continue;
577
- const name = await promptOrBack(() => p.text({
578
- message: "Give this stash a name (optional):",
579
- placeholder: "my-repo",
580
- }));
581
- if (name === null)
582
- continue;
583
- const entry = { type: "git", url: url.trim() };
584
- if (name.trim())
585
- entry.name = name.trim();
586
- if (!stashes.some((s) => s.url === entry.url)) {
587
- stashes.push(entry);
588
- }
589
- else {
590
- p.log.warn("This URL is already configured.");
591
- }
592
- }
593
- if (action === "filesystem") {
594
- const fsPath = await promptOrBack(() => p.text({
595
- message: "Enter the directory path:",
596
- placeholder: "/path/to/stash",
597
- validate: (v) => {
598
- if (!v?.trim())
599
- return "Path cannot be empty";
600
- },
601
- }));
602
- if (fsPath === null)
603
- continue;
604
- const resolved = fsPath.trim();
605
- const name = await promptOrBack(() => p.text({
606
- message: "Give this stash a name (optional):",
607
- placeholder: "my-stash",
608
- }));
609
- if (name === null)
610
- continue;
611
- const entry = { type: "filesystem", path: resolved };
612
- if (name.trim())
613
- entry.name = name.trim();
614
- if (!stashes.some((s) => s.path === entry.path)) {
615
- stashes.push(entry);
616
- }
617
- else {
618
- p.log.warn("This path is already configured.");
619
- }
620
- }
697
+ if (options?.promptForAdditional === false) {
698
+ return sources;
621
699
  }
622
- return stashes;
700
+ return stepAdditionalSources(sources);
623
701
  }
624
702
  async function stepAgentPlatforms(current) {
625
703
  const platforms = detectAgentPlatforms();
@@ -627,7 +705,7 @@ async function stepAgentPlatforms(current) {
627
705
  p.log.info("No agent platform configurations detected.");
628
706
  return [];
629
707
  }
630
- const existingPaths = new Set((current.stashes ?? []).map((s) => s.path));
708
+ const existingPaths = new Set((current.sources ?? current.stashes ?? []).map((s) => s.path));
631
709
  // Filter out platforms already configured
632
710
  const newPlatforms = platforms.filter((pl) => !existingPaths.has(pl.path));
633
711
  if (newPlatforms.length === 0) {
@@ -656,6 +734,60 @@ async function stepAgentPlatforms(current) {
656
734
  }
657
735
  return entries;
658
736
  }
737
+ export async function stepAgentSelection(current, detections) {
738
+ const available = detections.filter((d) => d.available);
739
+ if (available.length === 0) {
740
+ return current.agent;
741
+ }
742
+ const initialValue = pickDefaultAgentProfile(detections, current.agent?.default) ?? available[0]?.name;
743
+ const selectedDefault = await prompt(() => p.select({
744
+ message: "Which detected agent CLI should be the default?",
745
+ options: [
746
+ ...available.map((d) => ({
747
+ value: d.name,
748
+ label: d.name,
749
+ hint: d.resolvedPath ?? d.bin,
750
+ })),
751
+ { value: "disabled", label: "Disabled", hint: "do not configure a default agent CLI" },
752
+ ],
753
+ initialValue,
754
+ }));
755
+ if (selectedDefault === "disabled") {
756
+ if (!current.agent?.profiles && !current.agent?.timeoutMs) {
757
+ return undefined;
758
+ }
759
+ return {
760
+ ...(current.agent ?? {}),
761
+ default: undefined,
762
+ };
763
+ }
764
+ return {
765
+ ...(current.agent ?? {}),
766
+ default: selectedDefault,
767
+ };
768
+ }
769
+ export async function stepOutputConfig(current) {
770
+ const defaultOutput = current.output ?? DEFAULT_CONFIG.output ?? { format: "json", detail: "brief" };
771
+ const format = await prompt(() => p.select({
772
+ message: "Default output format?",
773
+ options: [
774
+ { value: "json", label: "json", hint: "structured default" },
775
+ { value: "text", label: "text", hint: "human-readable CLI output" },
776
+ { value: "yaml", label: "yaml", hint: "structured text" },
777
+ ],
778
+ initialValue: defaultOutput.format ?? "json",
779
+ }));
780
+ const detail = await prompt(() => p.select({
781
+ message: "Default output detail level?",
782
+ options: [
783
+ { value: "brief", label: "brief", hint: "compact summaries" },
784
+ { value: "normal", label: "normal", hint: "balanced detail" },
785
+ { value: "full", label: "full", hint: "max available detail" },
786
+ ],
787
+ initialValue: defaultOutput.detail ?? "brief",
788
+ }));
789
+ return { format: format, detail: detail };
790
+ }
659
791
  /**
660
792
  * Detect installed agent CLIs and produce an updated `agent` config block
661
793
  * with a sensible `default` (the first detected profile that the user has
@@ -751,20 +883,20 @@ export function buildSetupSteps(options) {
751
883
  id: "stash-sources",
752
884
  label: "Stash Sources",
753
885
  async run(ctx) {
754
- const stashes = await stepAddSources(ctx.config);
755
- const platforms = await stepAgentPlatforms(ctx.config);
886
+ const stashes = await stepAddSources(ctx.config, { promptForAdditional: false });
887
+ const platforms = await stepAgentPlatforms({ ...ctx.config, sources: stashes });
756
888
  const merged = [...stashes];
757
889
  for (const ps of platforms) {
758
890
  if (!merged.some((s) => s.path === ps.path))
759
891
  merged.push(ps);
760
892
  }
761
- ctx.apply({ sources: merged.length > 0 ? merged : undefined });
893
+ const withAdditional = await stepAdditionalSources(merged);
894
+ ctx.apply({ sources: withAdditional.length > 0 ? withAdditional : undefined });
762
895
  },
763
896
  },
764
897
  {
765
898
  id: "agent-cli",
766
899
  label: "Agent CLI",
767
- nonInteractive: true,
768
900
  async run(ctx) {
769
901
  const result = stepAgentCliDetection(ctx.config);
770
902
  const detected = result.detections.filter((d) => d.available);
@@ -775,7 +907,16 @@ export function buildSetupSteps(options) {
775
907
  else {
776
908
  p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
777
909
  }
778
- ctx.apply({ agent: result.agent });
910
+ const agent = await stepAgentSelection({ ...ctx.config, agent: result.agent }, result.detections);
911
+ ctx.apply({ agent });
912
+ },
913
+ },
914
+ {
915
+ id: "output",
916
+ label: "Output Defaults",
917
+ async run(ctx) {
918
+ const output = await stepOutputConfig(ctx.config);
919
+ ctx.apply({ output });
779
920
  },
780
921
  },
781
922
  ];
@@ -811,7 +952,6 @@ export async function runSetupWizard() {
811
952
  ...ctx.config,
812
953
  // Preserve fields the steps don't manage explicitly.
813
954
  installed: current.installed,
814
- output: current.output,
815
955
  };
816
956
  const semanticSearchMode = outcome.semantic;
817
957
  const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
@@ -828,6 +968,8 @@ export async function runSetupWizard() {
828
968
  `Semantic search: ${semanticSearchMode.mode}`,
829
969
  `Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
830
970
  `Stash sources: ${allStashes.length}`,
971
+ `Agent default: ${newConfig.agent?.default ?? "disabled"}`,
972
+ `Output: ${newConfig.output?.format ?? "json"} / ${newConfig.output?.detail ?? "brief"}`,
831
973
  ].join("\n"), "Configuration Summary");
832
974
  const shouldSave = await prompt(() => p.confirm({
833
975
  message: "Save this configuration?",
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { TYPE_DIRS } from "../../core/asset-spec";
5
6
  import { resolveStashDir } from "../../core/common";
6
7
  import { loadConfig } from "../../core/config";
7
8
  import { ConfigError, UsageError } from "../../core/errors";
@@ -123,7 +124,7 @@ async function ensureGitMirror(repo, cachePaths, options) {
123
124
  * shared registry-index cache (12h TTL) and exposes the working tree as the
124
125
  * stash content directory.
125
126
  */
126
- async function syncMirroredRepo(config, options) {
127
+ export async function syncMirroredRepo(config, options) {
127
128
  if (!config.url) {
128
129
  throw new ConfigError("git stash entry requires a URL when no install ref is supplied");
129
130
  }
@@ -303,7 +304,18 @@ function pullRepo(repoDir) {
303
304
  }
304
305
  function hasExtractedRepo(repoDir) {
305
306
  try {
306
- return fs.statSync(repoDir).isDirectory() && fs.statSync(path.join(repoDir, "content")).isDirectory();
307
+ if (!fs.statSync(repoDir).isDirectory())
308
+ return false;
309
+ if (fs.statSync(path.join(repoDir, "content")).isDirectory())
310
+ return true;
311
+ }
312
+ catch {
313
+ /* fall through to root-layout detection */
314
+ }
315
+ try {
316
+ if (!fs.statSync(repoDir).isDirectory())
317
+ return false;
318
+ return Object.values(TYPE_DIRS).some((dirName) => fs.existsSync(path.join(repoDir, dirName)));
307
319
  }
308
320
  catch {
309
321
  return false;