akm-cli 0.4.0 → 0.5.0-rc1

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.
package/dist/renderers.js CHANGED
@@ -9,10 +9,14 @@
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
11
  import { hasErrnoCode } from "./common";
12
+ import { UsageError } from "./errors";
12
13
  import { registerRenderer } from "./file-context";
13
14
  import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
14
15
  import { extractFrontmatterOnly, extractLineRange, extractSection, formatToc, parseMarkdownToc } from "./markdown";
15
16
  import { extractDescriptionFromComments, loadStashFile } from "./metadata";
17
+ import { makeAssetRef } from "./stash-ref";
18
+ import { listKeys as listVaultKeys } from "./vault";
19
+ import { parseWorkflowMarkdown, WorkflowValidationError } from "./workflow-markdown";
16
20
  // ── Interpreter auto-detection map ───────────────────────────────────────────
17
21
  const INTERPRETER_MAP = {
18
22
  ".sh": "bash",
@@ -149,6 +153,12 @@ function deriveName(ctx) {
149
153
  const ext = path.extname(ctx.relPath);
150
154
  return ext ? ctx.relPath.slice(0, -ext.length) : ctx.relPath;
151
155
  }
156
+ function shellQuote(value) {
157
+ return `'${value.replace(/'/g, `'\\''`)}'`;
158
+ }
159
+ export function buildWorkflowAction(ref) {
160
+ return `Resume the active run or start a new run with \`akm workflow next ${shellQuote(ref)}\`.`;
161
+ }
152
162
  /**
153
163
  * Load the matching StashEntry for a file path from the directory's .stash.json.
154
164
  */
@@ -309,6 +319,85 @@ const knowledgeMdRenderer = {
309
319
  }
310
320
  },
311
321
  };
322
+ // ── 4b. wiki-md ──────────────────────────────────────────────────────────────
323
+ const WIKI_PAGE_ACTION = "Wiki page — read below. Use 'toc' to scan, 'section <heading>' for depth.";
324
+ const wikiMdRenderer = {
325
+ name: "wiki-md",
326
+ buildShowResponse(ctx) {
327
+ const name = deriveName(ctx);
328
+ const v = ctx.matchResult.meta?.view ?? { mode: "full" };
329
+ const content = ctx.content();
330
+ switch (v.mode) {
331
+ case "toc": {
332
+ const toc = parseMarkdownToc(content);
333
+ return {
334
+ type: "wiki",
335
+ name,
336
+ path: ctx.absPath,
337
+ action: WIKI_PAGE_ACTION,
338
+ content: formatToc(toc),
339
+ };
340
+ }
341
+ case "frontmatter": {
342
+ const fm = extractFrontmatterOnly(content);
343
+ return {
344
+ type: "wiki",
345
+ name,
346
+ path: ctx.absPath,
347
+ action: WIKI_PAGE_ACTION,
348
+ content: fm ?? "(no frontmatter)",
349
+ };
350
+ }
351
+ case "section": {
352
+ const section = extractSection(content, v.heading);
353
+ if (!section) {
354
+ return {
355
+ type: "wiki",
356
+ name,
357
+ path: ctx.absPath,
358
+ action: WIKI_PAGE_ACTION,
359
+ content: `Section "${v.heading}" not found in ${name}. Try \`akm show wiki:${name} toc\` to discover available headings.`,
360
+ };
361
+ }
362
+ return {
363
+ type: "wiki",
364
+ name,
365
+ path: ctx.absPath,
366
+ action: WIKI_PAGE_ACTION,
367
+ content: section.content,
368
+ };
369
+ }
370
+ case "lines": {
371
+ return {
372
+ type: "wiki",
373
+ name,
374
+ path: ctx.absPath,
375
+ action: WIKI_PAGE_ACTION,
376
+ content: extractLineRange(content, v.start, v.end),
377
+ };
378
+ }
379
+ default: {
380
+ return {
381
+ type: "wiki",
382
+ name,
383
+ path: ctx.absPath,
384
+ action: WIKI_PAGE_ACTION,
385
+ content,
386
+ };
387
+ }
388
+ }
389
+ },
390
+ extractMetadata(entry, ctx) {
391
+ try {
392
+ const toc = parseMarkdownToc(ctx.content());
393
+ if (toc.headings.length > 0)
394
+ entry.toc = toc.headings;
395
+ }
396
+ catch {
397
+ // Non-fatal: skip TOC if file can't be read
398
+ }
399
+ },
400
+ };
312
401
  // ── 5. memory-md ─────────────────────────────────────────────────────────────
313
402
  const memoryMdRenderer = {
314
403
  name: "memory-md",
@@ -323,7 +412,58 @@ const memoryMdRenderer = {
323
412
  };
324
413
  },
325
414
  };
326
- // ── 6. script-source ─────────────────────────────────────────────────────────
415
+ // ── 6. workflow-md ───────────────────────────────────────────────────────────
416
+ const workflowMdRenderer = {
417
+ name: "workflow-md",
418
+ buildShowResponse(ctx) {
419
+ const name = deriveName(ctx);
420
+ const workflow = parseWorkflowForRendering(ctx.content());
421
+ const ref = makeAssetRef("workflow", name, ctx.origin);
422
+ return {
423
+ type: "workflow",
424
+ name,
425
+ path: ctx.absPath,
426
+ action: buildWorkflowAction(ref),
427
+ description: workflow.description,
428
+ workflowTitle: workflow.title,
429
+ parameters: workflow.parameters?.map((parameter) => parameter.name),
430
+ workflowParameters: workflow.parameters,
431
+ steps: workflow.steps,
432
+ };
433
+ },
434
+ extractMetadata(entry, ctx) {
435
+ const workflow = parseWorkflowForRendering(ctx.content());
436
+ const hints = new Set(entry.searchHints ?? []);
437
+ hints.add(workflow.title);
438
+ for (const step of workflow.steps) {
439
+ hints.add(step.title);
440
+ hints.add(step.id);
441
+ hints.add(step.instructions);
442
+ for (const criterion of step.completionCriteria ?? []) {
443
+ hints.add(criterion);
444
+ }
445
+ }
446
+ entry.searchHints = Array.from(hints).filter(Boolean);
447
+ if (workflow.parameters?.length) {
448
+ entry.parameters = workflow.parameters.map((parameter) => ({
449
+ name: parameter.name,
450
+ ...(parameter.description ? { description: parameter.description } : {}),
451
+ }));
452
+ }
453
+ },
454
+ };
455
+ function parseWorkflowForRendering(content) {
456
+ try {
457
+ return parseWorkflowMarkdown(content);
458
+ }
459
+ catch (error) {
460
+ if (error instanceof WorkflowValidationError) {
461
+ throw new UsageError(error.message);
462
+ }
463
+ throw error;
464
+ }
465
+ }
466
+ // ── 7. script-source ─────────────────────────────────────────────────────────
327
467
  const scriptSourceRenderer = {
328
468
  name: "script-source",
329
469
  buildShowResponse(ctx) {
@@ -379,6 +519,43 @@ const scriptSourceRenderer = {
379
519
  }
380
520
  },
381
521
  };
522
+ // ── 8. vault-env ─────────────────────────────────────────────────────────────
523
+ /**
524
+ * Vault renderer. Returns ONLY key names and start-of-line comments — never
525
+ * values. Deliberately omits content/template/prompt so vault values cannot
526
+ * leak through `akm show`.
527
+ */
528
+ const vaultEnvRenderer = {
529
+ name: "vault-env",
530
+ buildShowResponse(ctx) {
531
+ const name = deriveName(ctx);
532
+ const { keys, comments } = listVaultKeys(ctx.absPath);
533
+ return {
534
+ type: "vault",
535
+ name,
536
+ path: ctx.absPath,
537
+ action: 'Vault — keys + comments only. Use `eval "$(akm vault load <ref>)"` to load values into the current shell. Values stay on disk and are never written to akm\'s stdout.',
538
+ description: comments.length > 0 ? comments.join("\n") : undefined,
539
+ keys,
540
+ comments,
541
+ };
542
+ },
543
+ extractMetadata(entry, ctx) {
544
+ // Re-derive from the file directly to guarantee no value ever transits
545
+ // through any other code path. Caller already short-circuits in
546
+ // generateMetadata{,Flat}, but this is defense in depth.
547
+ const { keys, comments } = listVaultKeys(ctx.absPath);
548
+ if (comments.length > 0 && !entry.description) {
549
+ entry.description = comments.join(" ").slice(0, 500);
550
+ entry.source = "comments";
551
+ entry.confidence = 0.7;
552
+ }
553
+ if (keys.length > 0) {
554
+ entry.searchHints = keys;
555
+ }
556
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), "vault", "secrets"]));
557
+ },
558
+ };
382
559
  // ── Registration ─────────────────────────────────────────────────────────────
383
560
  /** All built-in renderers. */
384
561
  const builtinRenderers = [
@@ -386,8 +563,11 @@ const builtinRenderers = [
386
563
  commandMdRenderer,
387
564
  agentMdRenderer,
388
565
  knowledgeMdRenderer,
566
+ wikiMdRenderer,
389
567
  memoryMdRenderer,
568
+ workflowMdRenderer,
390
569
  scriptSourceRenderer,
570
+ vaultEnvRenderer,
391
571
  ];
392
572
  /**
393
573
  * Register all built-in renderers with the file-context registry.
@@ -399,4 +579,4 @@ export function registerBuiltinRenderers() {
399
579
  }
400
580
  }
401
581
  // ── Named exports for testing ────────────────────────────────────────────────
402
- export { agentMdRenderer, commandMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, skillMdRenderer, };
582
+ export { agentMdRenderer, commandMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, skillMdRenderer, vaultEnvRenderer, wikiMdRenderer, workflowMdRenderer, };
@@ -41,6 +41,10 @@ export function buildSearchFields(entry) {
41
41
  if (entry.intent.output)
42
42
  hintParts.push(entry.intent.output);
43
43
  }
44
+ if (entry.xrefs)
45
+ hintParts.push(entry.xrefs.join(" "));
46
+ if (entry.pageKind)
47
+ hintParts.push(entry.pageKind);
44
48
  const hints = hintParts.join(" ").toLowerCase();
45
49
  const contentParts = [];
46
50
  if (entry.toc) {
@@ -20,7 +20,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
20
20
  const config = existingConfig ?? loadConfig();
21
21
  const sources = [{ path: stashDir }];
22
22
  const seen = new Set([path.resolve(stashDir)]);
23
- const addSource = (dir, registryId) => {
23
+ const addSource = (dir, registryId, wikiName) => {
24
24
  const resolved = path.resolve(dir);
25
25
  if (seen.has(resolved))
26
26
  return;
@@ -29,17 +29,21 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
29
29
  warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
30
30
  }
31
31
  if (isValidDirectory(dir)) {
32
- sources.push({ path: resolved, ...(registryId ? { registryId } : {}) });
32
+ sources.push({
33
+ path: resolved,
34
+ ...(registryId ? { registryId } : {}),
35
+ ...(wikiName ? { wikiName } : {}),
36
+ });
33
37
  }
34
38
  };
35
39
  // Filesystem entries from stashes[]
36
40
  for (const entry of config.stashes ?? []) {
37
41
  if (entry.type === "filesystem" && entry.path && entry.enabled !== false) {
38
- addSource(entry.path, entry.name);
42
+ addSource(entry.path, entry.name, entry.wikiName);
39
43
  }
40
44
  }
41
45
  // Git stash entries: resolve cache directory so the indexer can walk it.
42
- // "context-hub", "github", and "git" provider types are handled.
46
+ // "git" provider type (and its legacy aliases "context-hub", "github") are handled.
43
47
  for (const entry of config.stashes ?? []) {
44
48
  if (GIT_STASH_TYPES.has(entry.type) && entry.url && entry.enabled !== false) {
45
49
  try {
@@ -48,7 +52,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
48
52
  // The content/ subdirectory inside the extracted repo is the actual
49
53
  // stash root containing DOC.md / SKILL.md files that the walker indexes.
50
54
  const contentDir = path.join(cachePaths.repoDir, "content");
51
- addSource(contentDir, entry.name);
55
+ addSource(contentDir, entry.name, entry.wikiName);
52
56
  }
53
57
  catch (err) {
54
58
  warn(`Warning: failed to resolve git stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
@@ -61,7 +65,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
61
65
  if (entry.type === "website" && entry.url && entry.enabled !== false) {
62
66
  try {
63
67
  const cachePaths = getWebsiteCachePaths(entry.url);
64
- addSource(cachePaths.stashDir, entry.name ?? entry.url);
68
+ addSource(cachePaths.stashDir, entry.name ?? entry.url, entry.wikiName);
65
69
  }
66
70
  catch (err) {
67
71
  warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
@@ -70,7 +74,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
70
74
  }
71
75
  // Installed kits (registry and local)
72
76
  for (const entry of config.installed ?? []) {
73
- addSource(entry.stashRoot, entry.id);
77
+ addSource(entry.stashRoot, entry.id, entry.wikiName);
74
78
  }
75
79
  return sources;
76
80
  }
@@ -180,7 +184,7 @@ export async function ensureStashCaches(config) {
180
184
  try {
181
185
  const repo = parseGitRepoUrl(entry.url);
182
186
  const cachePaths = getCachePaths(repo.canonicalUrl);
183
- await ensureGitMirror(repo, cachePaths, { requireRepoDir: true });
187
+ await ensureGitMirror(repo, cachePaths, { requireRepoDir: true, writable: entry.writable === true });
184
188
  }
185
189
  catch (err) {
186
190
  warn(`Warning: failed to refresh git mirror for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
package/dist/setup.js CHANGED
@@ -14,6 +14,7 @@ import { detectAgentPlatforms, detectOllama, detectOpenViking } from "./detect";
14
14
  import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedder";
15
15
  import { akmIndex } from "./indexer";
16
16
  import { akmInit } from "./init";
17
+ import { probeLlmCapabilities } from "./llm";
17
18
  import { getDefaultStashDir } from "./paths";
18
19
  import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./semantic-status";
19
20
  // ── Constants ───────────────────────────────────────────────────────────────
@@ -236,8 +237,8 @@ async function stepOllama(current) {
236
237
  spin.stop("Ollama not detected");
237
238
  p.log.info("Ollama is not running. Embeddings will use the built-in local model.\n" +
238
239
  "To use Ollama later, install it from https://ollama.com and re-run `akm setup`.");
239
- // Preserve existing embedding/LLM config when Ollama is not available
240
- return { embedding: current.embedding, llm: current.llm };
240
+ // Preserve existing embedding config when Ollama is not available
241
+ return { embedding: current.embedding };
241
242
  }
242
243
  spin.stop(`Ollama detected at ${ollama.endpoint}`);
243
244
  if (ollama.models.length > 0) {
@@ -300,44 +301,162 @@ async function stepOllama(current) {
300
301
  };
301
302
  }
302
303
  // else: undefined → use built-in local
303
- // LLM model selection
304
- const chatModels = ollama.models.filter((m) => !embeddingModels.includes(m));
305
- const allLlmCandidates = chatModels.length > 0 ? chatModels : ollama.models;
306
- let llm;
307
- const llmOptions = [];
308
- for (const m of allLlmCandidates) {
309
- llmOptions.push({ value: m, label: m, hint: "Ollama" });
310
- }
311
- llmOptions.push({
312
- value: "none",
313
- label: "Skip LLM enhancement",
314
- hint: "use heuristic metadata",
315
- });
304
+ // Surface Ollama details to the LLM step so it can offer Ollama as a preset.
305
+ const ollamaChatModels = ollama.models.filter((m) => !embeddingModels.includes(m));
306
+ return { embedding, ollamaEndpoint: ollama.endpoint, ollamaChatModels };
307
+ }
308
+ const LLM_PRESETS = [
309
+ {
310
+ value: "anthropic",
311
+ label: "Anthropic Claude (OpenAI SDK compat beta)",
312
+ endpoint: "https://api.anthropic.com/v1/chat/completions",
313
+ defaultModel: "claude-sonnet-4-5",
314
+ hint: "beta OpenAI-compat layer; set AKM_LLM_API_KEY; override the model if the default is unavailable",
315
+ contextWindow: 200_000,
316
+ },
317
+ {
318
+ value: "openai",
319
+ label: "OpenAI",
320
+ endpoint: "https://api.openai.com/v1/chat/completions",
321
+ defaultModel: "gpt-4o-mini",
322
+ hint: "AKM_LLM_API_KEY required",
323
+ contextWindow: 128_000,
324
+ },
325
+ {
326
+ value: "google",
327
+ label: "Google Gemini (OpenAI-compat)",
328
+ endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
329
+ defaultModel: "gemini-2.0-flash",
330
+ hint: "OpenAI-compat endpoint, AKM_LLM_API_KEY required",
331
+ contextWindow: 1_000_000,
332
+ },
333
+ ];
334
+ /**
335
+ * Step 3a: pick an LLM provider. Used for indexing-time metadata enhancement.
336
+ *
337
+ * @internal Exported for testing only.
338
+ */
339
+ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
340
+ const options = LLM_PRESETS.map((preset) => ({
341
+ value: preset.value,
342
+ label: preset.label,
343
+ hint: preset.hint,
344
+ }));
345
+ const ollamaAvailable = Boolean(ollamaEndpoint && ollamaChatModels && ollamaChatModels.length > 0);
346
+ if (ollamaAvailable) {
347
+ options.push({
348
+ value: "ollama",
349
+ label: "Ollama (local)",
350
+ hint: ollamaChatModels?.[0] ?? "local",
351
+ });
352
+ }
353
+ options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
354
+ options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
316
355
  if (current.llm) {
317
- llmOptions.push({
356
+ options.push({
318
357
  value: "keep",
319
358
  label: `Keep current: ${current.llm.provider ?? current.llm.endpoint}`,
320
359
  hint: current.llm.model,
321
360
  });
322
361
  }
323
- const llmChoice = await prompt(() => p.select({
324
- message: "Use an LLM for richer metadata during indexing?",
325
- options: llmOptions,
326
- initialValue: allLlmCandidates.length > 0 ? allLlmCandidates[0] : "none",
362
+ const initialValue = current.llm ? "keep" : ollamaAvailable ? "ollama" : (LLM_PRESETS[0]?.value ?? "none");
363
+ const choice = await prompt(() => p.select({
364
+ message: "Configure an LLM for richer metadata during indexing:",
365
+ options,
366
+ initialValue,
327
367
  }));
328
- if (llmChoice === "keep") {
329
- llm = current.llm;
330
- }
331
- else if (llmChoice !== "none") {
368
+ if (choice === "keep")
369
+ return current.llm;
370
+ if (choice === "none")
371
+ return undefined;
372
+ let llm;
373
+ if (choice === "ollama") {
374
+ const modelChoice = await prompt(() => p.select({
375
+ message: "Which Ollama model?",
376
+ options: (ollamaChatModels ?? []).map((m) => ({ value: m, label: m })),
377
+ initialValue: ollamaChatModels?.[0],
378
+ }));
332
379
  llm = {
333
380
  provider: "ollama",
334
- endpoint: `${ollama.endpoint}/v1/chat/completions`,
335
- model: llmChoice,
381
+ endpoint: `${ollamaEndpoint}/v1/chat/completions`,
382
+ model: modelChoice,
383
+ temperature: 0.3,
384
+ maxTokens: 1024,
385
+ };
386
+ }
387
+ else if (choice === "custom") {
388
+ const endpoint = await prompt(() => p.text({
389
+ message: "OpenAI-compatible chat completions endpoint:",
390
+ placeholder: "https://your-host/v1/chat/completions",
391
+ validate: (v) => {
392
+ if (!v?.trim())
393
+ return "Endpoint cannot be empty";
394
+ if (!v.startsWith("http://") && !v.startsWith("https://"))
395
+ return "Endpoint must start with http:// or https://";
396
+ },
397
+ }));
398
+ const model = await prompt(() => p.text({
399
+ message: "Model name:",
400
+ placeholder: "gpt-4o-mini",
401
+ validate: (v) => {
402
+ if (!v?.trim())
403
+ return "Model name cannot be empty";
404
+ },
405
+ }));
406
+ llm = {
407
+ provider: "custom",
408
+ endpoint: endpoint.trim(),
409
+ model: model.trim(),
410
+ temperature: 0.3,
411
+ maxTokens: 1024,
412
+ };
413
+ }
414
+ else {
415
+ const preset = LLM_PRESETS.find((p) => p.value === choice);
416
+ if (!preset)
417
+ return undefined;
418
+ const model = await prompt(() => p.text({
419
+ message: `Model for ${preset.label}:`,
420
+ placeholder: preset.defaultModel,
421
+ defaultValue: preset.defaultModel,
422
+ validate: (v) => {
423
+ if (!v?.trim())
424
+ return "Model name cannot be empty";
425
+ },
426
+ }));
427
+ llm = {
428
+ provider: preset.value,
429
+ endpoint: preset.endpoint,
430
+ model: model.trim() || preset.defaultModel,
336
431
  temperature: 0.3,
337
- maxTokens: 512,
432
+ maxTokens: 1024,
433
+ contextWindow: preset.contextWindow,
338
434
  };
339
435
  }
340
- return { embedding, llm };
436
+ // Remind the user about API key placement. We do not offer a "store in config"
437
+ // option because saveConfig() strips apiKey fields before writing — persisting
438
+ // secrets would need an encrypted/secure store that we don't ship.
439
+ const needsKey = llm.provider !== "ollama" && !llm.endpoint.includes("localhost");
440
+ if (needsKey && !process.env.AKM_LLM_API_KEY) {
441
+ p.log.info("This provider requires an API key. Set AKM_LLM_API_KEY in your shell (e.g. `export AKM_LLM_API_KEY=...`) before running `akm index`.");
442
+ }
443
+ // Capability probe — best-effort, never blocks setup.
444
+ const probeSpin = p.spinner();
445
+ probeSpin.start("Probing LLM (structured-output round-trip)...");
446
+ const probe = await probeLlmCapabilities(llm);
447
+ if (probe.reachable && probe.structuredOutput) {
448
+ probeSpin.stop("LLM reachable; structured output verified.");
449
+ llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
450
+ }
451
+ else if (probe.reachable) {
452
+ probeSpin.stop("LLM reachable but structured-output probe failed.");
453
+ llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
454
+ }
455
+ else {
456
+ probeSpin.stop("LLM not reachable.");
457
+ p.log.warn(`Could not reach the LLM endpoint${probe.error ? ` (${probe.error})` : ""}. Configuration was saved; verify your endpoint and API key, then retry.`);
458
+ }
459
+ return llm;
341
460
  }
342
461
  async function stepRegistries(current) {
343
462
  const defaults = DEFAULT_CONFIG.registries ?? [];
@@ -402,7 +521,7 @@ export async function stepStashSources(current) {
402
521
  for (const url of selectedRepos) {
403
522
  if (!existingUrls.has(url)) {
404
523
  const rec = RECOMMENDED_GITHUB_REPOS.find((r) => r.url === url);
405
- stashes.push({ type: "github", url, name: rec?.name });
524
+ stashes.push({ type: "git", url, name: rec?.name });
406
525
  existingUrls.add(url);
407
526
  }
408
527
  }
@@ -489,7 +608,7 @@ export async function stepStashSources(current) {
489
608
  }));
490
609
  if (name === null)
491
610
  continue;
492
- const entry = { type: "github", url: url.trim() };
611
+ const entry = { type: "git", url: url.trim() };
493
612
  if (name.trim())
494
613
  entry.name = name.trim();
495
614
  if (!stashes.some((s) => s.url === entry.url)) {
@@ -579,9 +698,15 @@ export async function runSetupWizard() {
579
698
  p.log.warn("No network connectivity detected. Skipping Ollama detection and remote embedding checks.\n" +
580
699
  "Local-only setup will continue. Re-run `akm setup` when online for full configuration.");
581
700
  }
582
- // Step 2: Ollama / Embedding / LLM
583
- p.log.step("Step 2: Embedding & LLM");
584
- const { embedding, llm } = online ? await stepOllama(current) : { embedding: current.embedding, llm: current.llm };
701
+ // Step 2: Embedding (Ollama detection drives the embedding choice + surfaces
702
+ // the Ollama endpoint to the LLM step that follows).
703
+ p.log.step("Step 2: Embedding");
704
+ const { embedding, ollamaEndpoint, ollamaChatModels } = online
705
+ ? await stepOllama(current)
706
+ : { embedding: current.embedding };
707
+ // Step 2b: LLM provider — Anthropic / OpenAI / Gemini / Ollama / custom.
708
+ p.log.step("Step 2b: LLM Provider");
709
+ const llm = online ? await stepLlm(current, ollamaEndpoint, ollamaChatModels) : current.llm;
585
710
  // Step 3: Semantic search assets
586
711
  p.log.step("Step 3: Semantic Search");
587
712
  const semanticSearchMode = await stepSemanticSearch(current, embedding);