akm-cli 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,8 @@ import path from "node:path";
15
15
  import * as p from "@clack/prompts";
16
16
  import { akmInit } from "../commands/init";
17
17
  import { isHttpUrl } from "../core/common";
18
- import { DEFAULT_CONFIG, getDefaultLlmConfig, loadUserConfig, saveConfig } from "../core/config";
18
+ import { DEFAULT_CONFIG, getDefaultLlmConfig, getEffectiveRegistries, loadUserConfig, saveConfig, } from "../core/config";
19
+ import { backupExistingConfig } from "../core/config-io";
19
20
  import { ConfigError } from "../core/errors";
20
21
  import { assertSafeStashDir, getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths";
21
22
  import { warn } from "../core/warn";
@@ -25,7 +26,9 @@ import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticSt
25
26
  import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent";
26
27
  import { probeLlmCapabilities } from "../llm/client";
27
28
  import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
28
- import { detectAgentPlatforms, detectOllama } from "./detect";
29
+ import { detectAgentPlatforms, detectLMStudio, detectOllama } from "./detect";
30
+ import { detectHarnessConfigs } from "./harness-config-import";
31
+ import { loadSetupStashes } from "./registry-stash-loader";
29
32
  import { createSetupContext, runSetupSteps } from "./steps";
30
33
  // ── Setup sandbox guard ─────────────────────────────────────────────────────
31
34
  /**
@@ -149,22 +152,7 @@ function applyLegacyAgent(config, agent) {
149
152
  defaults: { ...(config.defaults ?? {}), agent: agent.default },
150
153
  };
151
154
  }
152
- /**
153
- * Recommended GitHub repositories shown during setup.
154
- */
155
- const RECOMMENDED_GITHUB_REPOS = [
156
- {
157
- url: "https://github.com/itlackey/akm-stash",
158
- name: "itlackey/akm-stash",
159
- hint: "official onboarding stash",
160
- defaultSelected: true,
161
- },
162
- {
163
- url: "https://github.com/andrewyng/context-hub",
164
- name: "andrewyng/context-hub",
165
- hint: "optional community prompt and context stash",
166
- },
167
- ];
155
+ // ── Constants ───────────────────────────────────────────────────────────────
168
156
  // Approximate first-download sizes used in the setup note.
169
157
  // LOCAL_MODEL_APPROX_SIZE_MB tracks the default local model (DEFAULT_LOCAL_MODEL).
170
158
  const LOCAL_MODEL_APPROX_SIZE_MB = 130;
@@ -622,12 +610,21 @@ const LLM_PRESETS = [
622
610
  *
623
611
  * @internal Exported for testing only.
624
612
  */
625
- export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
626
- const options = LLM_PRESETS.map((preset) => ({
627
- value: preset.value,
628
- label: preset.label,
629
- hint: preset.hint,
613
+ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudio, harnessConfigs) {
614
+ // Build "Import from <Harness>" options and prepend them before LLM_PRESETS
615
+ const harnessOptions = (harnessConfigs ?? []).map((h) => ({
616
+ value: `harness:${h.harnessName}`,
617
+ label: `Import from ${h.harnessName}`,
618
+ hint: [h.provider, h.model].filter(Boolean).join(" / ") || "detected",
630
619
  }));
620
+ const options = [
621
+ ...harnessOptions,
622
+ ...LLM_PRESETS.map((preset) => ({
623
+ value: preset.value,
624
+ label: preset.label,
625
+ hint: preset.hint,
626
+ })),
627
+ ];
631
628
  const ollamaAvailable = Boolean(ollamaEndpoint && ollamaChatModels && ollamaChatModels.length > 0);
632
629
  if (ollamaAvailable) {
633
630
  options.push({
@@ -636,6 +633,10 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
636
633
  hint: ollamaChatModels?.[0] ?? "local",
637
634
  });
638
635
  }
636
+ const lmStudioHint = lmStudio?.available
637
+ ? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
638
+ : "http://localhost:1234";
639
+ options.push({ value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint });
639
640
  options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
640
641
  options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
641
642
  const currentLlm = getCurrentLlm(current);
@@ -656,6 +657,26 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
656
657
  return cloneLlmConfig(currentLlm);
657
658
  if (choice === "none")
658
659
  return undefined;
660
+ // Handle "Import from <Harness>" choices
661
+ if (typeof choice === "string" && choice.startsWith("harness:")) {
662
+ const harness = (harnessConfigs ?? []).find((h) => `harness:${h.harnessName}` === choice);
663
+ if (!harness)
664
+ return undefined;
665
+ // Show a summary before accepting
666
+ p.log.info(`Importing LLM config from ${harness.harnessName}: ` +
667
+ [harness.provider, harness.model, harness.baseUrl].filter(Boolean).join(", "));
668
+ const llmConfig = {
669
+ endpoint: harness.baseUrl ?? "",
670
+ model: harness.model ?? "",
671
+ temperature: 0.3,
672
+ maxTokens: 1024,
673
+ };
674
+ if (harness.provider)
675
+ llmConfig.provider = harness.provider;
676
+ if (harness.baseUrl)
677
+ llmConfig.endpoint = harness.baseUrl;
678
+ return llmConfig;
679
+ }
659
680
  let llm;
660
681
  if (choice === "ollama") {
661
682
  const modelChoice = await prompt(() => p.select({
@@ -671,10 +692,66 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
671
692
  maxTokens: 1024,
672
693
  };
673
694
  }
695
+ else if (choice === "lmstudio") {
696
+ const currentLmsLlm = currentLlm?.provider === "lmstudio" ? currentLlm : undefined;
697
+ const defaultEndpoint = currentLmsLlm?.endpoint ??
698
+ (lmStudio?.endpoint ? `${lmStudio.endpoint}/v1/chat/completions` : "http://localhost:1234/v1/chat/completions");
699
+ const endpoint = await prompt(() => p.text({
700
+ message: "Endpoint URL:",
701
+ placeholder: defaultEndpoint,
702
+ defaultValue: defaultEndpoint,
703
+ validate: (v) => {
704
+ if (!v?.trim())
705
+ return "Endpoint cannot be empty";
706
+ if (!v.startsWith("http://") && !v.startsWith("https://"))
707
+ return "Must start with http:// or https://";
708
+ },
709
+ }));
710
+ let model;
711
+ const lmsModels = lmStudio?.available && lmStudio.models.length > 0 ? lmStudio.models : [];
712
+ if (lmsModels.length > 0) {
713
+ const modelChoice = await prompt(() => p.select({
714
+ message: "Model name:",
715
+ options: [
716
+ ...lmsModels.map((m) => ({ value: m, label: m })),
717
+ { value: "__manual__", label: "Enter manually..." },
718
+ ],
719
+ initialValue: currentLmsLlm?.model && lmsModels.includes(currentLmsLlm.model) ? currentLmsLlm.model : lmsModels[0],
720
+ }));
721
+ if (modelChoice === "__manual__") {
722
+ model = await prompt(() => p.text({
723
+ message: "Model name:",
724
+ placeholder: currentLmsLlm?.model ?? "local-model",
725
+ ...(currentLmsLlm?.model ? { defaultValue: currentLmsLlm.model } : {}),
726
+ validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
727
+ }));
728
+ }
729
+ else {
730
+ model = modelChoice;
731
+ }
732
+ }
733
+ else {
734
+ model = await prompt(() => p.text({
735
+ message: "Model name:",
736
+ placeholder: currentLmsLlm?.model ?? "local-model",
737
+ ...(currentLmsLlm?.model ? { defaultValue: currentLmsLlm.model } : {}),
738
+ validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
739
+ }));
740
+ }
741
+ llm = {
742
+ provider: "lmstudio",
743
+ endpoint: endpoint.trim(),
744
+ model: model.trim(),
745
+ temperature: 0.3,
746
+ maxTokens: 1024,
747
+ };
748
+ }
674
749
  else if (choice === "custom") {
750
+ const currentCustomLlm = currentLlm?.provider === "custom" ? currentLlm : undefined;
675
751
  const endpoint = await prompt(() => p.text({
676
752
  message: "OpenAI-compatible chat completions endpoint:",
677
- placeholder: "https://your-host/v1/chat/completions",
753
+ placeholder: currentCustomLlm?.endpoint ?? "https://your-host/v1/chat/completions",
754
+ ...(currentCustomLlm?.endpoint ? { defaultValue: currentCustomLlm.endpoint } : {}),
678
755
  validate: (v) => {
679
756
  if (!v?.trim())
680
757
  return "Endpoint cannot be empty";
@@ -684,7 +761,8 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
684
761
  }));
685
762
  const model = await prompt(() => p.text({
686
763
  message: "Model name:",
687
- placeholder: "gpt-4o-mini",
764
+ placeholder: currentCustomLlm?.model ?? "gpt-4o-mini",
765
+ ...(currentCustomLlm?.model ? { defaultValue: currentCustomLlm.model } : {}),
688
766
  validate: (v) => {
689
767
  if (!v?.trim())
690
768
  return "Model name cannot be empty";
@@ -806,41 +884,47 @@ export async function stepAddSources(current, options) {
806
884
  if ((current.installed?.length ?? 0) > 0) {
807
885
  p.note(renderInstalledSourceList(current.installed ?? []), "Installed managed stashes (preserved)");
808
886
  }
809
- // ── Recommended GitHub repos ───────────────────────────────────────────
810
- // Skip the prompt entirely when there are no recommendations to show.
811
- // The infrastructure is retained for a future registry-driven version.
812
- if (RECOMMENDED_GITHUB_REPOS.length > 0) {
887
+ // ── Registry-driven stash recommendations ─────────────────────────────
888
+ // Fetch available stashes from the official registry (cached, stale-ok).
889
+ // Falls back to the bundled list when the registry is unreachable.
890
+ const registryUrl = getEffectiveRegistries(current)[0]?.url ??
891
+ "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json";
892
+ const availableStashes = await loadSetupStashes(registryUrl);
893
+ if (availableStashes.length > 0) {
813
894
  const existingUrls = new Set(sources.map((s) => s.url));
814
- const repoOptions = RECOMMENDED_GITHUB_REPOS.map((r) => ({
815
- value: r.url,
816
- label: r.name,
817
- hint: existingUrls.has(r.url) ? `${r.hint} (already added)` : r.hint,
895
+ const stashOptions = availableStashes.map((s) => ({
896
+ value: s.url,
897
+ label: s.name,
898
+ hint: existingUrls.has(s.url) ? `${s.description} (already added)` : s.description || s.source,
818
899
  }));
900
+ // Pre-check: already-installed stashes OR default-selected on fresh install
819
901
  const initialValues = sources.length > 0
820
- ? repoOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value)
821
- : RECOMMENDED_GITHUB_REPOS.filter((r) => r.defaultSelected).map((r) => r.url);
822
- const selectedRepos = await prompt(() => p.multiselect({
823
- message: "Recommended GitHub repositories — toggle to add or remove:",
824
- options: repoOptions,
902
+ ? stashOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value)
903
+ : availableStashes.filter((s) => s.defaultSelected).map((s) => s.url);
904
+ const selectedUrls = await prompt(() => p.multiselect({
905
+ message: availableStashes[0]?.source === "registry"
906
+ ? "Available stashes from the AKM registry — toggle to add or remove:"
907
+ : "Recommended stash sources — toggle to add or remove:",
908
+ options: stashOptions,
825
909
  initialValues,
826
910
  required: false,
827
911
  }));
828
- // Add newly selected repos
829
- for (const url of selectedRepos) {
912
+ // Add newly selected stashes
913
+ for (const url of selectedUrls) {
830
914
  if (!existingUrls.has(url)) {
831
- const rec = RECOMMENDED_GITHUB_REPOS.find((r) => r.url === url);
832
- sources.push({ type: "git", url, name: rec?.name });
915
+ const entry = availableStashes.find((s) => s.url === url);
916
+ sources.push({ type: "git", url, name: entry?.name });
833
917
  existingUrls.add(url);
834
918
  }
835
919
  }
836
- // Remove deselected repos that were previously configured
837
- for (const rec of RECOMMENDED_GITHUB_REPOS) {
838
- if (existingUrls.has(rec.url) && !selectedRepos.includes(rec.url)) {
839
- const idx = sources.findIndex((s) => s.url === rec.url);
920
+ // Remove deselected stashes that were previously configured
921
+ for (const entry of availableStashes) {
922
+ if (existingUrls.has(entry.url) && !selectedUrls.includes(entry.url)) {
923
+ const idx = sources.findIndex((s) => s.url === entry.url);
840
924
  if (idx !== -1) {
841
925
  sources.splice(idx, 1);
842
- existingUrls.delete(rec.url);
843
- p.log.info(`Removed ${rec.name}.`);
926
+ existingUrls.delete(entry.url);
927
+ p.log.info(`Removed ${entry.name}.`);
844
928
  }
845
929
  }
846
930
  }
@@ -900,11 +984,17 @@ export async function stepSmallModelConnection(current) {
900
984
  " • akm remember --enrich (memory compression)",
901
985
  " • akm curate --rerank (search reranking)",
902
986
  ].join("\n"));
903
- // Probe for Ollama in the background while showing the note.
987
+ // Probe for Ollama and LM Studio in the background while showing the note.
904
988
  const spin = p.spinner();
905
989
  spin.start("Detecting local services...");
906
- const ollama = await detectOllama();
907
- spin.stop(ollama.available ? `Ollama detected at ${ollama.endpoint}` : "No local services detected");
990
+ const [ollama, lmStudio] = await Promise.all([detectOllama(), detectLMStudio()]);
991
+ const detectedServices = [
992
+ ollama.available ? `Ollama at ${ollama.endpoint}` : null,
993
+ lmStudio.available ? `LM Studio at ${lmStudio.endpoint}` : null,
994
+ ]
995
+ .filter(Boolean)
996
+ .join(", ");
997
+ spin.stop(detectedServices ? `Detected: ${detectedServices}` : "No local services detected");
908
998
  const ollamaEndpoint = ollama.available ? ollama.endpoint : undefined;
909
999
  const providerOptions = [];
910
1000
  if (ollama.available) {
@@ -914,7 +1004,10 @@ export async function stepSmallModelConnection(current) {
914
1004
  hint: `detected at ${ollama.endpoint}`,
915
1005
  });
916
1006
  }
917
- providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: "http://localhost:1234" }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
1007
+ const lmStudioHint = lmStudio.available
1008
+ ? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
1009
+ : "http://localhost:1234";
1010
+ providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
918
1011
  const currentLlmSmall = getCurrentLlm(current);
919
1012
  if (currentLlmSmall) {
920
1013
  providerOptions.push({
@@ -966,10 +1059,11 @@ export async function stepSmallModelConnection(current) {
966
1059
  }
967
1060
  }
968
1061
  else {
1062
+ const currentOllamaModel = currentLlmSmall?.provider === "ollama" ? (currentLlmSmall.model ?? "llama3.2") : "llama3.2";
969
1063
  model = await prompt(() => p.text({
970
1064
  message: "Model name (e.g. llama3.2):",
971
- placeholder: "llama3.2",
972
- defaultValue: "llama3.2",
1065
+ placeholder: currentOllamaModel,
1066
+ defaultValue: currentOllamaModel,
973
1067
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
974
1068
  }));
975
1069
  }
@@ -982,10 +1076,11 @@ export async function stepSmallModelConnection(current) {
982
1076
  };
983
1077
  }
984
1078
  else if (providerChoice === "openai") {
1079
+ const currentOpenAiModel = currentLlmSmall?.provider === "openai" ? (currentLlmSmall.model ?? "gpt-4o-mini") : "gpt-4o-mini";
985
1080
  const model = await prompt(() => p.text({
986
1081
  message: "Model name:",
987
- placeholder: "gpt-4o-mini",
988
- defaultValue: "gpt-4o-mini",
1082
+ placeholder: currentOpenAiModel,
1083
+ defaultValue: currentOpenAiModel,
989
1084
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
990
1085
  }));
991
1086
  if (!process.env.AKM_LLM_API_KEY) {
@@ -994,16 +1089,20 @@ export async function stepSmallModelConnection(current) {
994
1089
  llm = {
995
1090
  provider: "openai",
996
1091
  endpoint: "https://api.openai.com/v1/chat/completions",
997
- model: model.trim() || "gpt-4o-mini",
1092
+ model: model.trim() || currentOpenAiModel,
998
1093
  temperature: 0.3,
999
1094
  maxTokens: 1024,
1000
1095
  };
1001
1096
  }
1002
1097
  else if (providerChoice === "lmstudio") {
1098
+ const currentLmsEndpoint = currentLlmSmall?.provider === "lmstudio"
1099
+ ? (currentLlmSmall.endpoint ?? `${lmStudio.endpoint}/v1/chat/completions`)
1100
+ : `${lmStudio.endpoint}/v1/chat/completions`;
1101
+ const currentLmsModel = currentLlmSmall?.provider === "lmstudio" ? currentLlmSmall.model : undefined;
1003
1102
  const endpoint = await prompt(() => p.text({
1004
1103
  message: "Endpoint URL:",
1005
- placeholder: "http://localhost:1234/v1/chat/completions",
1006
- defaultValue: "http://localhost:1234/v1/chat/completions",
1104
+ placeholder: currentLmsEndpoint,
1105
+ defaultValue: currentLmsEndpoint,
1007
1106
  validate: (v) => {
1008
1107
  if (!v?.trim())
1009
1108
  return "Endpoint cannot be empty";
@@ -1011,11 +1110,37 @@ export async function stepSmallModelConnection(current) {
1011
1110
  return "Must start with http:// or https://";
1012
1111
  },
1013
1112
  }));
1014
- const model = await prompt(() => p.text({
1015
- message: "Model name:",
1016
- placeholder: "local-model",
1017
- validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
1018
- }));
1113
+ let model;
1114
+ const lmsModels = lmStudio.available && lmStudio.models.length > 0 ? lmStudio.models : [];
1115
+ if (lmsModels.length > 0) {
1116
+ const modelChoice = await prompt(() => p.select({
1117
+ message: "Model name:",
1118
+ options: [
1119
+ ...lmsModels.map((m) => ({ value: m, label: m })),
1120
+ { value: "__manual__", label: "Enter manually..." },
1121
+ ],
1122
+ initialValue: currentLmsModel && lmsModels.includes(currentLmsModel) ? currentLmsModel : lmsModels[0],
1123
+ }));
1124
+ if (modelChoice === "__manual__") {
1125
+ model = await prompt(() => p.text({
1126
+ message: "Model name:",
1127
+ placeholder: currentLmsModel ?? "local-model",
1128
+ ...(currentLmsModel ? { defaultValue: currentLmsModel } : {}),
1129
+ validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
1130
+ }));
1131
+ }
1132
+ else {
1133
+ model = modelChoice;
1134
+ }
1135
+ }
1136
+ else {
1137
+ model = await prompt(() => p.text({
1138
+ message: "Model name:",
1139
+ placeholder: currentLmsModel ?? "local-model",
1140
+ ...(currentLmsModel ? { defaultValue: currentLmsModel } : {}),
1141
+ validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
1142
+ }));
1143
+ }
1019
1144
  llm = {
1020
1145
  provider: "lmstudio",
1021
1146
  endpoint: endpoint.trim(),
@@ -1026,9 +1151,12 @@ export async function stepSmallModelConnection(current) {
1026
1151
  }
1027
1152
  else {
1028
1153
  // custom
1154
+ const currentCustomEndpoint = currentLlmSmall?.provider === "custom" ? currentLlmSmall.endpoint : undefined;
1155
+ const currentCustomModel = currentLlmSmall?.provider === "custom" ? currentLlmSmall.model : undefined;
1029
1156
  const endpoint = await prompt(() => p.text({
1030
1157
  message: "OpenAI-compatible chat completions endpoint:",
1031
- placeholder: "https://your-host/v1/chat/completions",
1158
+ placeholder: currentCustomEndpoint ?? "https://your-host/v1/chat/completions",
1159
+ ...(currentCustomEndpoint ? { defaultValue: currentCustomEndpoint } : {}),
1032
1160
  validate: (v) => {
1033
1161
  if (!v?.trim())
1034
1162
  return "Endpoint cannot be empty";
@@ -1038,7 +1166,8 @@ export async function stepSmallModelConnection(current) {
1038
1166
  }));
1039
1167
  const model = await prompt(() => p.text({
1040
1168
  message: "Model name:",
1041
- placeholder: "gpt-4o-mini",
1169
+ placeholder: currentCustomModel ?? "gpt-4o-mini",
1170
+ ...(currentCustomModel ? { defaultValue: currentCustomModel } : {}),
1042
1171
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
1043
1172
  }));
1044
1173
  const apiKeyInput = await promptOrBack(() => p.text({
@@ -1150,12 +1279,15 @@ export async function stepAgentConnection(current, smallModel) {
1150
1279
  else {
1151
1280
  const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
1152
1281
  p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
1282
+ const profileName = smallModel.llm.provider ?? "default";
1283
+ // Pre-populate from existing agent profile for this provider, if any.
1284
+ const existingAgentModel = currentAgentBlock?.profiles?.[profileName]?.model ?? smallModel.llm.model ?? undefined;
1153
1285
  const agentModel = await prompt(() => p.text({
1154
1286
  message: "Model to use for agent tasks (same model is fine, larger models work better):",
1155
- placeholder: "qwen2.5-coder:32b",
1287
+ placeholder: existingAgentModel ?? "qwen2.5-coder:32b",
1288
+ ...(existingAgentModel ? { defaultValue: existingAgentModel } : {}),
1156
1289
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
1157
1290
  }));
1158
- const profileName = smallModel.llm.provider ?? "default";
1159
1291
  return {
1160
1292
  ...(currentAgentBlock ?? {}),
1161
1293
  profiles: {
@@ -1192,9 +1324,14 @@ export async function stepAgentConnection(current, smallModel) {
1192
1324
  };
1193
1325
  }
1194
1326
  // "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
1327
+ // Pre-populate from current "custom" agent profile if available.
1328
+ const currentCustomAgentProfile = currentAgentBlock?.profiles?.custom;
1329
+ const currentNewEndpoint = currentCustomAgentProfile?.endpoint ?? undefined;
1330
+ const currentNewModel = currentCustomAgentProfile?.model ?? undefined;
1195
1331
  const newEndpoint = await prompt(() => p.text({
1196
1332
  message: "OpenAI-compatible chat completions endpoint:",
1197
- placeholder: "https://your-host/v1/chat/completions",
1333
+ placeholder: currentNewEndpoint ?? "https://your-host/v1/chat/completions",
1334
+ ...(currentNewEndpoint ? { defaultValue: currentNewEndpoint } : {}),
1198
1335
  validate: (v) => {
1199
1336
  if (!v?.trim())
1200
1337
  return "Endpoint cannot be empty";
@@ -1208,7 +1345,8 @@ export async function stepAgentConnection(current, smallModel) {
1208
1345
  }));
1209
1346
  const newModel = await prompt(() => p.text({
1210
1347
  message: "Model name (larger is better, e.g. gpt-4o):",
1211
- placeholder: "gpt-4o",
1348
+ placeholder: currentNewModel ?? "gpt-4o",
1349
+ ...(currentNewModel ? { defaultValue: currentNewModel } : {}),
1212
1350
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
1213
1351
  }));
1214
1352
  const customProfile = {
@@ -1340,6 +1478,9 @@ export function buildSetupSteps(options) {
1340
1478
  // to the LLM step. Mutable by design — `stepLlm` needs them.
1341
1479
  let ollamaEndpoint;
1342
1480
  let ollamaChatModels;
1481
+ let lmStudioResult;
1482
+ // Harness configs detected once and shared with the LLM step.
1483
+ const harnessConfigs = detectHarnessConfigs();
1343
1484
  const steps = [
1344
1485
  {
1345
1486
  id: "stash-dir",
@@ -1361,9 +1502,10 @@ export function buildSetupSteps(options) {
1361
1502
  ctx.apply({ embedding: ctx.config.embedding });
1362
1503
  return;
1363
1504
  }
1364
- const result = await stepOllama(ctx.config);
1505
+ const [result, lmStudio] = await Promise.all([stepOllama(ctx.config), detectLMStudio()]);
1365
1506
  ollamaEndpoint = result.ollamaEndpoint;
1366
1507
  ollamaChatModels = result.ollamaChatModels;
1508
+ lmStudioResult = lmStudio;
1367
1509
  ctx.apply({ embedding: result.embedding });
1368
1510
  },
1369
1511
  },
@@ -1374,7 +1516,7 @@ export function buildSetupSteps(options) {
1374
1516
  if (!options.online) {
1375
1517
  return;
1376
1518
  }
1377
- const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels);
1519
+ const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels, lmStudioResult, harnessConfigs);
1378
1520
  ctx.apply(applyLegacyLlm(ctx.config, llm));
1379
1521
  },
1380
1522
  },
@@ -1521,6 +1663,11 @@ export async function runSetupWizard(opts) {
1521
1663
  if (!shouldSave)
1522
1664
  bail();
1523
1665
  // Save config
1666
+ const cfgPath1 = getConfigPath();
1667
+ if (fs.existsSync(cfgPath1)) {
1668
+ backupExistingConfig(cfgPath1);
1669
+ p.log.info(`Config backed up to ~/.cache/akm/config-backups/`);
1670
+ }
1524
1671
  saveConfig(newConfig);
1525
1672
  if (semanticSearchMode.mode === "off") {
1526
1673
  clearSemanticStatus();
@@ -1637,6 +1784,11 @@ export async function runSetupWithDefaults(opts) {
1637
1784
  ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
1638
1785
  }
1639
1786
  }
1787
+ const cfgPath2 = getConfigPath();
1788
+ if (fs.existsSync(cfgPath2)) {
1789
+ backupExistingConfig(cfgPath2);
1790
+ p.log.info(`Config backed up to ~/.cache/akm/config-backups/`);
1791
+ }
1640
1792
  saveConfig(ctx.config);
1641
1793
  return {
1642
1794
  configPath: getConfigPath(),
@@ -1738,6 +1890,11 @@ export async function runSetupFromConfig(opts) {
1738
1890
  // Non-fatal: probe failure is informational only
1739
1891
  }
1740
1892
  }
1893
+ const cfgPath3 = getConfigPath();
1894
+ if (fs.existsSync(cfgPath3)) {
1895
+ backupExistingConfig(cfgPath3);
1896
+ p.log.info(`Config backed up to ~/.cache/akm/config-backups/`);
1897
+ }
1741
1898
  saveConfig(merged);
1742
1899
  return {
1743
1900
  configPath: getConfigPath(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [