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.
- package/CHANGELOG.md +71 -0
- package/dist/assets/stash-skeleton/README.md +76 -0
- package/dist/cli.js +8 -3
- package/dist/commands/consolidate.js +4 -4
- package/dist/commands/health.js +20 -0
- package/dist/commands/improve-cli.js +1 -1
- package/dist/commands/improve-result-file.js +9 -4
- package/dist/commands/improve.js +49 -25
- package/dist/commands/init.js +6 -1
- package/dist/commands/{proposal-drain-policies.js → proposal/drain-policies.js} +2 -2
- package/dist/commands/{proposal-drain.js → proposal/drain.js} +10 -10
- package/dist/commands/show.js +47 -0
- package/dist/commands/stash-skeleton.js +78 -0
- package/dist/{setup/ripgrep-install.js → core/ripgrep/install.js} +2 -2
- package/dist/{setup/ripgrep-resolve.js → core/ripgrep/resolve.js} +2 -2
- package/dist/core/stash-meta.js +110 -0
- package/dist/setup/detect.js +27 -0
- package/dist/setup/harness-config-import.js +170 -0
- package/dist/setup/registry-stash-loader.js +99 -0
- package/dist/setup/setup.js +229 -72
- package/package.json +1 -1
package/dist/setup/setup.js
CHANGED
|
@@ -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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
// ──
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
|
|
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
|
|
815
|
-
value:
|
|
816
|
-
label:
|
|
817
|
-
hint: existingUrls.has(
|
|
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
|
-
?
|
|
821
|
-
:
|
|
822
|
-
const
|
|
823
|
-
message:
|
|
824
|
-
|
|
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
|
|
829
|
-
for (const url of
|
|
912
|
+
// Add newly selected stashes
|
|
913
|
+
for (const url of selectedUrls) {
|
|
830
914
|
if (!existingUrls.has(url)) {
|
|
831
|
-
const
|
|
832
|
-
sources.push({ type: "git", url, 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
|
|
837
|
-
for (const
|
|
838
|
-
if (existingUrls.has(
|
|
839
|
-
const idx = sources.findIndex((s) => s.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(
|
|
843
|
-
p.log.info(`Removed ${
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
972
|
-
defaultValue:
|
|
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:
|
|
988
|
-
defaultValue:
|
|
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() ||
|
|
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:
|
|
1006
|
-
defaultValue:
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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.
|
|
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": [
|