akm-cli 0.9.0-beta.54 → 0.9.0-beta.55
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/cli.js +5 -3
- package/dist/commands/agent/contribute-cli.js +2 -3
- package/dist/commands/env/env-cli.js +187 -202
- package/dist/commands/env/secret-cli.js +109 -121
- package/dist/commands/feedback-cli.js +152 -155
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/improve-metrics.js +754 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +18 -0
- package/dist/commands/health/windows.js +196 -0
- package/dist/commands/health.js +14 -1624
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/collapse-detector.js +3 -2
- package/dist/commands/improve/consolidate.js +636 -633
- package/dist/commands/improve/dedup.js +1 -1
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +228 -0
- package/dist/commands/improve/distill/quality-gate.js +233 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill.js +49 -575
- package/dist/commands/improve/extract-cli.js +74 -76
- package/dist/commands/improve/extract.js +6 -4
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +3 -2
- package/dist/commands/improve/improve-cli.js +14 -13
- package/dist/commands/improve/improve-result-file.js +2 -1
- package/dist/commands/improve/improve.js +6 -5
- package/dist/commands/improve/loop-stages.js +19 -21
- package/dist/commands/improve/preparation.js +4 -2
- package/dist/commands/improve/procedural.js +10 -31
- package/dist/commands/improve/recombine.js +19 -43
- package/dist/commands/improve/reflect.js +1 -1
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +48 -0
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +2 -2
- package/dist/commands/proposal/drain.js +1 -1
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +3 -3
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +1 -1
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposals.js +5 -920
- package/dist/commands/read/remember-cli.js +132 -137
- package/dist/commands/read/search-cli.js +1 -1
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/sources/add-cli.js +90 -94
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/schema-repair.js +1 -1
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +1 -2
- package/dist/commands/wiki-cli.js +2 -3
- package/dist/core/common.js +3 -3
- package/dist/core/config/config-schema.js +6 -0
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/events.js +2 -1
- package/dist/core/logs-db.js +8 -13
- package/dist/core/paths.js +14 -14
- package/dist/core/state-db.js +13 -1140
- package/dist/indexer/db/db.js +66 -709
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/feedback/utility-policy.js +85 -0
- package/dist/indexer/graph/graph-extraction.js +2 -1
- package/dist/indexer/index-writer-lock.js +9 -0
- package/dist/indexer/indexer.js +78 -23
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/integrations/agent/spawn.js +15 -66
- package/dist/output/text/helpers.js +13 -0
- package/dist/scripts/migrate-storage.js +6891 -7436
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +24 -1607
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +130 -0
- package/dist/storage/repositories/index-db.js +4 -7
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +3 -3
- package/dist/tasks/runner.js +2 -1
- package/package.json +1 -1
- package/dist/commands/improve/homeostatic.js +0 -497
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Setup wizard connection steps: embedding (Ollama) detection, LLM provider
|
|
6
|
+
* selection, and the two-step small-model + agent connection configuration.
|
|
7
|
+
*/
|
|
8
|
+
import * as p from "../../cli/clack.js";
|
|
9
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../integrations/agent/index.js";
|
|
10
|
+
import { probeLlmCapabilities } from "../../llm/client.js";
|
|
11
|
+
import { detectLMStudio, detectOllama } from "../detect.js";
|
|
12
|
+
import { cloneLlmConfig, getCurrentAgentBlock, getCurrentLlm } from "../legacy-config.js";
|
|
13
|
+
import { prompt, promptOrBack } from "../prompt.js";
|
|
14
|
+
export async function stepOllama(current) {
|
|
15
|
+
const spin = p.spinner();
|
|
16
|
+
spin.start("Checking for Ollama...");
|
|
17
|
+
const ollama = await detectOllama();
|
|
18
|
+
if (!ollama.available) {
|
|
19
|
+
spin.stop("Ollama not detected");
|
|
20
|
+
p.log.info("Ollama is not running. Embeddings will use the built-in local model.\n" +
|
|
21
|
+
"To use Ollama later, install it from https://ollama.com and re-run `akm setup`.");
|
|
22
|
+
// Preserve existing embedding config when Ollama is not available
|
|
23
|
+
return { embedding: current.embedding };
|
|
24
|
+
}
|
|
25
|
+
spin.stop(`Ollama detected at ${ollama.endpoint}`);
|
|
26
|
+
if (ollama.models.length > 0) {
|
|
27
|
+
p.log.info(`Available models: ${ollama.models.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
// Embedding model selection
|
|
30
|
+
const embeddingModels = ollama.models.filter((m) => m.includes("embed") || m.includes("nomic") || m.includes("minilm") || m.includes("bge"));
|
|
31
|
+
const hasEmbeddingModels = embeddingModels.length > 0;
|
|
32
|
+
let embedding;
|
|
33
|
+
const embeddingOptions = [];
|
|
34
|
+
for (const m of embeddingModels) {
|
|
35
|
+
embeddingOptions.push({ value: m, label: m, hint: "Ollama" });
|
|
36
|
+
}
|
|
37
|
+
embeddingOptions.push({
|
|
38
|
+
value: "local",
|
|
39
|
+
label: "Built-in local embeddings",
|
|
40
|
+
hint: "no server needed",
|
|
41
|
+
});
|
|
42
|
+
if (current.embedding) {
|
|
43
|
+
embeddingOptions.push({
|
|
44
|
+
value: "keep",
|
|
45
|
+
label: `Keep current: ${current.embedding.provider ?? current.embedding.endpoint}`,
|
|
46
|
+
hint: current.embedding.model,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const embChoice = await prompt(() => p.select({
|
|
50
|
+
message: "Which embedding provider should akm use?",
|
|
51
|
+
options: embeddingOptions,
|
|
52
|
+
initialValue: hasEmbeddingModels ? embeddingModels[0] : "local",
|
|
53
|
+
}));
|
|
54
|
+
if (embChoice === "keep") {
|
|
55
|
+
embedding = current.embedding;
|
|
56
|
+
}
|
|
57
|
+
else if (embChoice !== "local") {
|
|
58
|
+
// Ask for dimension — different models produce different sizes.
|
|
59
|
+
// Common dimensions: nomic-embed-text=768, mxbai-embed-large=1024,
|
|
60
|
+
// all-minilm/bge-small=384. Default based on selected model.
|
|
61
|
+
const knownDims = {
|
|
62
|
+
nomic: 768,
|
|
63
|
+
mxbai: 1024,
|
|
64
|
+
minilm: 384,
|
|
65
|
+
bge: 384,
|
|
66
|
+
qwen3: 1024,
|
|
67
|
+
};
|
|
68
|
+
const guessedDim = Object.entries(knownDims).find(([k]) => embChoice.includes(k))?.[1] ?? 384;
|
|
69
|
+
p.note("Embedding dimension must match the model. Common values: 384 (BGE small), 768 (BGE base), 1024 (BGE large). Press Enter to accept the detected default.", "Embedding dimension");
|
|
70
|
+
const dimChoice = await prompt(() => p.text({
|
|
71
|
+
message: `Embedding dimension for ${embChoice}:`,
|
|
72
|
+
placeholder: String(guessedDim),
|
|
73
|
+
defaultValue: String(guessedDim),
|
|
74
|
+
validate: (v) => {
|
|
75
|
+
const n = Number(v);
|
|
76
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
77
|
+
return "Must be a positive integer";
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
embedding = {
|
|
81
|
+
provider: "ollama",
|
|
82
|
+
endpoint: `${ollama.endpoint}/v1/embeddings`,
|
|
83
|
+
model: embChoice,
|
|
84
|
+
dimension: Number(dimChoice),
|
|
85
|
+
};
|
|
86
|
+
p.note([
|
|
87
|
+
"Recommended Qwen embedding models (modern, high context support):",
|
|
88
|
+
" • qwen3-embedding-0.6b — fast and lightweight (ollama pull qwen3-embedding-0.6b)",
|
|
89
|
+
" • qwen3-embedding-4b — higher quality (ollama pull qwen3-embedding-4b)",
|
|
90
|
+
"",
|
|
91
|
+
"For long documents (wiki pages, large files), set context length to avoid 400 errors:",
|
|
92
|
+
" akm config set embedding.contextLength 8192",
|
|
93
|
+
].join("\n"), "Embedding tips");
|
|
94
|
+
}
|
|
95
|
+
// else: undefined → use built-in local
|
|
96
|
+
// Surface Ollama details to the LLM step so it can offer Ollama as a preset.
|
|
97
|
+
const ollamaChatModels = ollama.models.filter((m) => !embeddingModels.includes(m));
|
|
98
|
+
return { embedding, ollamaEndpoint: ollama.endpoint, ollamaChatModels };
|
|
99
|
+
}
|
|
100
|
+
const LLM_PRESETS = [
|
|
101
|
+
{
|
|
102
|
+
value: "anthropic",
|
|
103
|
+
label: "Anthropic Claude (OpenAI SDK compat beta)",
|
|
104
|
+
endpoint: "https://api.anthropic.com/v1/chat/completions",
|
|
105
|
+
defaultModel: "claude-sonnet-4-5",
|
|
106
|
+
hint: "beta OpenAI-compat layer; set AKM_LLM_API_KEY; override the model if the default is unavailable",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
value: "openai",
|
|
110
|
+
label: "OpenAI",
|
|
111
|
+
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
112
|
+
defaultModel: "gpt-4o-mini",
|
|
113
|
+
hint: "AKM_LLM_API_KEY required",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: "google",
|
|
117
|
+
label: "Google Gemini (OpenAI-compat)",
|
|
118
|
+
endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
|
|
119
|
+
defaultModel: "gemini-2.0-flash",
|
|
120
|
+
hint: "OpenAI-compat endpoint, AKM_LLM_API_KEY required",
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* Step 3a: pick an LLM provider. Used for indexing-time metadata enhancement.
|
|
125
|
+
*
|
|
126
|
+
* @internal Exported for testing only.
|
|
127
|
+
*/
|
|
128
|
+
export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudio, harnessConfigs) {
|
|
129
|
+
// Build "Import from <Harness>" options and prepend them before LLM_PRESETS
|
|
130
|
+
const harnessOptions = (harnessConfigs ?? []).map((h) => ({
|
|
131
|
+
value: `harness:${h.harnessName}`,
|
|
132
|
+
label: `Import from ${h.harnessName}`,
|
|
133
|
+
hint: [h.provider, h.model].filter(Boolean).join(" / ") || "detected",
|
|
134
|
+
}));
|
|
135
|
+
const options = [
|
|
136
|
+
...harnessOptions,
|
|
137
|
+
...LLM_PRESETS.map((preset) => ({
|
|
138
|
+
value: preset.value,
|
|
139
|
+
label: preset.label,
|
|
140
|
+
hint: preset.hint,
|
|
141
|
+
})),
|
|
142
|
+
];
|
|
143
|
+
const ollamaAvailable = Boolean(ollamaEndpoint && ollamaChatModels && ollamaChatModels.length > 0);
|
|
144
|
+
if (ollamaAvailable) {
|
|
145
|
+
options.push({
|
|
146
|
+
value: "ollama",
|
|
147
|
+
label: "Ollama (local)",
|
|
148
|
+
hint: ollamaChatModels?.[0] ?? "local",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const lmStudioHint = lmStudio?.available
|
|
152
|
+
? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
|
|
153
|
+
: "http://localhost:1234";
|
|
154
|
+
options.push({ value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint });
|
|
155
|
+
options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
|
|
156
|
+
options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
|
|
157
|
+
const currentLlm = getCurrentLlm(current);
|
|
158
|
+
if (currentLlm) {
|
|
159
|
+
options.push({
|
|
160
|
+
value: "keep",
|
|
161
|
+
label: `Keep current: ${currentLlm.provider ?? currentLlm.endpoint}`,
|
|
162
|
+
hint: currentLlm.model,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const initialValue = currentLlm ? "keep" : ollamaAvailable ? "ollama" : (LLM_PRESETS[0]?.value ?? "none");
|
|
166
|
+
const choice = await prompt(() => p.select({
|
|
167
|
+
message: "Configure an LLM for richer metadata during indexing:",
|
|
168
|
+
options,
|
|
169
|
+
initialValue,
|
|
170
|
+
}));
|
|
171
|
+
if (choice === "keep")
|
|
172
|
+
return cloneLlmConfig(currentLlm);
|
|
173
|
+
if (choice === "none")
|
|
174
|
+
return undefined;
|
|
175
|
+
// Handle "Import from <Harness>" choices
|
|
176
|
+
if (typeof choice === "string" && choice.startsWith("harness:")) {
|
|
177
|
+
const harness = (harnessConfigs ?? []).find((h) => `harness:${h.harnessName}` === choice);
|
|
178
|
+
if (!harness)
|
|
179
|
+
return undefined;
|
|
180
|
+
// Show a summary before accepting
|
|
181
|
+
p.log.info(`Importing LLM config from ${harness.harnessName}: ` +
|
|
182
|
+
[harness.provider, harness.model, harness.baseUrl].filter(Boolean).join(", "));
|
|
183
|
+
const llmConfig = {
|
|
184
|
+
endpoint: harness.baseUrl ?? "",
|
|
185
|
+
model: harness.model ?? "",
|
|
186
|
+
temperature: 0.3,
|
|
187
|
+
maxTokens: 1024,
|
|
188
|
+
};
|
|
189
|
+
if (harness.provider)
|
|
190
|
+
llmConfig.provider = harness.provider;
|
|
191
|
+
if (harness.baseUrl)
|
|
192
|
+
llmConfig.endpoint = harness.baseUrl;
|
|
193
|
+
return llmConfig;
|
|
194
|
+
}
|
|
195
|
+
let llm;
|
|
196
|
+
if (choice === "ollama") {
|
|
197
|
+
const modelChoice = await prompt(() => p.select({
|
|
198
|
+
message: "Which Ollama model?",
|
|
199
|
+
options: (ollamaChatModels ?? []).map((m) => ({ value: m, label: m })),
|
|
200
|
+
initialValue: ollamaChatModels?.[0],
|
|
201
|
+
}));
|
|
202
|
+
llm = {
|
|
203
|
+
provider: "ollama",
|
|
204
|
+
endpoint: `${ollamaEndpoint}/v1/chat/completions`,
|
|
205
|
+
model: modelChoice,
|
|
206
|
+
temperature: 0.3,
|
|
207
|
+
maxTokens: 1024,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
else if (choice === "lmstudio") {
|
|
211
|
+
const currentLmsLlm = currentLlm?.provider === "lmstudio" ? currentLlm : undefined;
|
|
212
|
+
const defaultEndpoint = currentLmsLlm?.endpoint ??
|
|
213
|
+
(lmStudio?.endpoint ? `${lmStudio.endpoint}/v1/chat/completions` : "http://localhost:1234/v1/chat/completions");
|
|
214
|
+
const endpoint = await prompt(() => p.text({
|
|
215
|
+
message: "Endpoint URL:",
|
|
216
|
+
placeholder: defaultEndpoint,
|
|
217
|
+
defaultValue: defaultEndpoint,
|
|
218
|
+
validate: (v) => {
|
|
219
|
+
if (!v?.trim())
|
|
220
|
+
return "Endpoint cannot be empty";
|
|
221
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
222
|
+
return "Must start with http:// or https://";
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
let model;
|
|
226
|
+
const lmsModels = lmStudio?.available && lmStudio.models.length > 0 ? lmStudio.models : [];
|
|
227
|
+
if (lmsModels.length > 0) {
|
|
228
|
+
const modelChoice = await prompt(() => p.select({
|
|
229
|
+
message: "Model name:",
|
|
230
|
+
options: [
|
|
231
|
+
...lmsModels.map((m) => ({ value: m, label: m })),
|
|
232
|
+
{ value: "__manual__", label: "Enter manually..." },
|
|
233
|
+
],
|
|
234
|
+
initialValue: currentLmsLlm?.model && lmsModels.includes(currentLmsLlm.model) ? currentLmsLlm.model : lmsModels[0],
|
|
235
|
+
}));
|
|
236
|
+
if (modelChoice === "__manual__") {
|
|
237
|
+
model = await prompt(() => p.text({
|
|
238
|
+
message: "Model name:",
|
|
239
|
+
placeholder: currentLmsLlm?.model ?? "local-model",
|
|
240
|
+
...(currentLmsLlm?.model ? { defaultValue: currentLmsLlm.model } : {}),
|
|
241
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
model = modelChoice;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
model = await prompt(() => p.text({
|
|
250
|
+
message: "Model name:",
|
|
251
|
+
placeholder: currentLmsLlm?.model ?? "local-model",
|
|
252
|
+
...(currentLmsLlm?.model ? { defaultValue: currentLmsLlm.model } : {}),
|
|
253
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
llm = {
|
|
257
|
+
provider: "lmstudio",
|
|
258
|
+
endpoint: endpoint.trim(),
|
|
259
|
+
model: model.trim(),
|
|
260
|
+
temperature: 0.3,
|
|
261
|
+
maxTokens: 1024,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
else if (choice === "custom") {
|
|
265
|
+
const currentCustomLlm = currentLlm?.provider === "custom" ? currentLlm : undefined;
|
|
266
|
+
const endpoint = await prompt(() => p.text({
|
|
267
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
268
|
+
placeholder: currentCustomLlm?.endpoint ?? "https://your-host/v1/chat/completions",
|
|
269
|
+
...(currentCustomLlm?.endpoint ? { defaultValue: currentCustomLlm.endpoint } : {}),
|
|
270
|
+
validate: (v) => {
|
|
271
|
+
if (!v?.trim())
|
|
272
|
+
return "Endpoint cannot be empty";
|
|
273
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
274
|
+
return "Endpoint must start with http:// or https://";
|
|
275
|
+
},
|
|
276
|
+
}));
|
|
277
|
+
const model = await prompt(() => p.text({
|
|
278
|
+
message: "Model name:",
|
|
279
|
+
placeholder: currentCustomLlm?.model ?? "gpt-4o-mini",
|
|
280
|
+
...(currentCustomLlm?.model ? { defaultValue: currentCustomLlm.model } : {}),
|
|
281
|
+
validate: (v) => {
|
|
282
|
+
if (!v?.trim())
|
|
283
|
+
return "Model name cannot be empty";
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
llm = {
|
|
287
|
+
provider: "custom",
|
|
288
|
+
endpoint: endpoint.trim(),
|
|
289
|
+
model: model.trim(),
|
|
290
|
+
temperature: 0.3,
|
|
291
|
+
maxTokens: 1024,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const preset = LLM_PRESETS.find((p) => p.value === choice);
|
|
296
|
+
if (!preset)
|
|
297
|
+
return undefined;
|
|
298
|
+
const model = await prompt(() => p.text({
|
|
299
|
+
message: `Model for ${preset.label}:`,
|
|
300
|
+
placeholder: preset.defaultModel,
|
|
301
|
+
defaultValue: preset.defaultModel,
|
|
302
|
+
validate: (v) => {
|
|
303
|
+
if (!v?.trim())
|
|
304
|
+
return "Model name cannot be empty";
|
|
305
|
+
},
|
|
306
|
+
}));
|
|
307
|
+
llm = {
|
|
308
|
+
provider: preset.value,
|
|
309
|
+
endpoint: preset.endpoint,
|
|
310
|
+
model: model.trim() || preset.defaultModel,
|
|
311
|
+
temperature: 0.3,
|
|
312
|
+
maxTokens: 1024,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
// Remind the user about API key placement. We do not offer a "store in config"
|
|
316
|
+
// option because saveConfig() strips apiKey fields before writing — persisting
|
|
317
|
+
// secrets would need an encrypted/secure store that we don't ship.
|
|
318
|
+
const needsKey = llm.provider !== "ollama" && !llm.endpoint.includes("localhost");
|
|
319
|
+
if (needsKey && !process.env.AKM_LLM_API_KEY) {
|
|
320
|
+
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`.");
|
|
321
|
+
}
|
|
322
|
+
// Capability probe — best-effort, never blocks setup.
|
|
323
|
+
const probeSpin = p.spinner();
|
|
324
|
+
probeSpin.start("Probing LLM (structured-output round-trip)...");
|
|
325
|
+
const probe = await probeLlmCapabilities(llm);
|
|
326
|
+
if (probe.reachable && probe.structuredOutput) {
|
|
327
|
+
probeSpin.stop("LLM reachable; structured output verified.");
|
|
328
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
|
|
329
|
+
}
|
|
330
|
+
else if (probe.reachable) {
|
|
331
|
+
probeSpin.stop("LLM reachable but structured-output probe failed.");
|
|
332
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
probeSpin.stop("LLM not reachable.");
|
|
336
|
+
p.log.warn(`Could not reach the LLM endpoint${probe.error ? ` (${probe.error})` : ""}. Configuration was saved; verify your endpoint and API key, then retry.`);
|
|
337
|
+
}
|
|
338
|
+
return llm;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Step 1/2: Configure the small model connection used for metadata and bounded LLM features.
|
|
342
|
+
*
|
|
343
|
+
* Detects Ollama automatically and pre-selects it. The user may also choose
|
|
344
|
+
* OpenAI, LM Studio, a custom endpoint, or skip the step entirely.
|
|
345
|
+
*/
|
|
346
|
+
export async function stepSmallModelConnection(current) {
|
|
347
|
+
p.log.step("Step 1/2: Configure your small model connection");
|
|
348
|
+
p.note([
|
|
349
|
+
"This connection is used for background processing:",
|
|
350
|
+
" • akm index (metadata enhancement)",
|
|
351
|
+
" • akm distill (lesson distillation)",
|
|
352
|
+
" • akm remember --enrich (memory compression)",
|
|
353
|
+
" • akm curate --rerank (search reranking)",
|
|
354
|
+
].join("\n"));
|
|
355
|
+
// Probe for Ollama and LM Studio in the background while showing the note.
|
|
356
|
+
const spin = p.spinner();
|
|
357
|
+
spin.start("Detecting local services...");
|
|
358
|
+
const [ollama, lmStudio] = await Promise.all([detectOllama(), detectLMStudio()]);
|
|
359
|
+
const detectedServices = [
|
|
360
|
+
ollama.available ? `Ollama at ${ollama.endpoint}` : null,
|
|
361
|
+
lmStudio.available ? `LM Studio at ${lmStudio.endpoint}` : null,
|
|
362
|
+
]
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
.join(", ");
|
|
365
|
+
spin.stop(detectedServices ? `Detected: ${detectedServices}` : "No local services detected");
|
|
366
|
+
const ollamaEndpoint = ollama.available ? ollama.endpoint : undefined;
|
|
367
|
+
const providerOptions = [];
|
|
368
|
+
if (ollama.available) {
|
|
369
|
+
providerOptions.push({
|
|
370
|
+
value: "ollama",
|
|
371
|
+
label: "Ollama (local)",
|
|
372
|
+
hint: `detected at ${ollama.endpoint}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const lmStudioHint = lmStudio.available
|
|
376
|
+
? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
|
|
377
|
+
: "http://localhost:1234";
|
|
378
|
+
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" });
|
|
379
|
+
const currentLlmSmall = getCurrentLlm(current);
|
|
380
|
+
if (currentLlmSmall) {
|
|
381
|
+
providerOptions.push({
|
|
382
|
+
value: "keep",
|
|
383
|
+
label: `Keep current: ${currentLlmSmall.provider ?? currentLlmSmall.endpoint}`,
|
|
384
|
+
hint: currentLlmSmall.model,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const initialValue = currentLlmSmall ? "keep" : ollama.available ? "ollama" : "openai";
|
|
388
|
+
const providerChoice = await prompt(() => p.select({
|
|
389
|
+
message: "Provider:",
|
|
390
|
+
options: providerOptions,
|
|
391
|
+
initialValue,
|
|
392
|
+
}));
|
|
393
|
+
if (providerChoice === "keep") {
|
|
394
|
+
return { llm: cloneLlmConfig(currentLlmSmall), skipped: false, ollamaEndpoint };
|
|
395
|
+
}
|
|
396
|
+
if (providerChoice === "skip") {
|
|
397
|
+
p.note([
|
|
398
|
+
"Enrichment features disabled:",
|
|
399
|
+
" • akm index — metadata enhancement disabled",
|
|
400
|
+
" • akm distill — lesson generation",
|
|
401
|
+
" • akm remember --enrich",
|
|
402
|
+
" • akm curate --rerank",
|
|
403
|
+
"",
|
|
404
|
+
"You can configure this later with `akm setup`.",
|
|
405
|
+
].join("\n"), "Warning");
|
|
406
|
+
return { llm: undefined, skipped: true, ollamaEndpoint };
|
|
407
|
+
}
|
|
408
|
+
let llm;
|
|
409
|
+
if (providerChoice === "ollama") {
|
|
410
|
+
const ollamaChatModels = ollama.models.filter((m) => !m.includes("embed") && !m.includes("nomic") && !m.includes("minilm") && !m.includes("bge"));
|
|
411
|
+
let model;
|
|
412
|
+
if (ollamaChatModels.length > 0) {
|
|
413
|
+
model = await prompt(() => p.select({
|
|
414
|
+
message: "Model name:",
|
|
415
|
+
options: [
|
|
416
|
+
...ollamaChatModels.map((m) => ({ value: m, label: m })),
|
|
417
|
+
{ value: "__custom__", label: "Enter a model name manually..." },
|
|
418
|
+
],
|
|
419
|
+
initialValue: ollamaChatModels[0],
|
|
420
|
+
}));
|
|
421
|
+
if (model === "__custom__") {
|
|
422
|
+
model = await prompt(() => p.text({
|
|
423
|
+
message: "Model name:",
|
|
424
|
+
placeholder: "llama3.2",
|
|
425
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
const currentOllamaModel = currentLlmSmall?.provider === "ollama" ? (currentLlmSmall.model ?? "llama3.2") : "llama3.2";
|
|
431
|
+
model = await prompt(() => p.text({
|
|
432
|
+
message: "Model name (e.g. llama3.2):",
|
|
433
|
+
placeholder: currentOllamaModel,
|
|
434
|
+
defaultValue: currentOllamaModel,
|
|
435
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
llm = {
|
|
439
|
+
provider: "ollama",
|
|
440
|
+
endpoint: `${ollama.endpoint}/v1/chat/completions`,
|
|
441
|
+
model: model.trim(),
|
|
442
|
+
temperature: 0.3,
|
|
443
|
+
maxTokens: 1024,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
else if (providerChoice === "openai") {
|
|
447
|
+
const currentOpenAiModel = currentLlmSmall?.provider === "openai" ? (currentLlmSmall.model ?? "gpt-4o-mini") : "gpt-4o-mini";
|
|
448
|
+
const model = await prompt(() => p.text({
|
|
449
|
+
message: "Model name:",
|
|
450
|
+
placeholder: currentOpenAiModel,
|
|
451
|
+
defaultValue: currentOpenAiModel,
|
|
452
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
453
|
+
}));
|
|
454
|
+
if (!process.env.AKM_LLM_API_KEY) {
|
|
455
|
+
p.log.info("Set AKM_LLM_API_KEY in your shell before running `akm index`.");
|
|
456
|
+
}
|
|
457
|
+
llm = {
|
|
458
|
+
provider: "openai",
|
|
459
|
+
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
460
|
+
model: model.trim() || currentOpenAiModel,
|
|
461
|
+
temperature: 0.3,
|
|
462
|
+
maxTokens: 1024,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
else if (providerChoice === "lmstudio") {
|
|
466
|
+
const currentLmsEndpoint = currentLlmSmall?.provider === "lmstudio"
|
|
467
|
+
? (currentLlmSmall.endpoint ?? `${lmStudio.endpoint}/v1/chat/completions`)
|
|
468
|
+
: `${lmStudio.endpoint}/v1/chat/completions`;
|
|
469
|
+
const currentLmsModel = currentLlmSmall?.provider === "lmstudio" ? currentLlmSmall.model : undefined;
|
|
470
|
+
const endpoint = await prompt(() => p.text({
|
|
471
|
+
message: "Endpoint URL:",
|
|
472
|
+
placeholder: currentLmsEndpoint,
|
|
473
|
+
defaultValue: currentLmsEndpoint,
|
|
474
|
+
validate: (v) => {
|
|
475
|
+
if (!v?.trim())
|
|
476
|
+
return "Endpoint cannot be empty";
|
|
477
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
478
|
+
return "Must start with http:// or https://";
|
|
479
|
+
},
|
|
480
|
+
}));
|
|
481
|
+
let model;
|
|
482
|
+
const lmsModels = lmStudio.available && lmStudio.models.length > 0 ? lmStudio.models : [];
|
|
483
|
+
if (lmsModels.length > 0) {
|
|
484
|
+
const modelChoice = await prompt(() => p.select({
|
|
485
|
+
message: "Model name:",
|
|
486
|
+
options: [
|
|
487
|
+
...lmsModels.map((m) => ({ value: m, label: m })),
|
|
488
|
+
{ value: "__manual__", label: "Enter manually..." },
|
|
489
|
+
],
|
|
490
|
+
initialValue: currentLmsModel && lmsModels.includes(currentLmsModel) ? currentLmsModel : lmsModels[0],
|
|
491
|
+
}));
|
|
492
|
+
if (modelChoice === "__manual__") {
|
|
493
|
+
model = await prompt(() => p.text({
|
|
494
|
+
message: "Model name:",
|
|
495
|
+
placeholder: currentLmsModel ?? "local-model",
|
|
496
|
+
...(currentLmsModel ? { defaultValue: currentLmsModel } : {}),
|
|
497
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
model = modelChoice;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
model = await prompt(() => p.text({
|
|
506
|
+
message: "Model name:",
|
|
507
|
+
placeholder: currentLmsModel ?? "local-model",
|
|
508
|
+
...(currentLmsModel ? { defaultValue: currentLmsModel } : {}),
|
|
509
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
llm = {
|
|
513
|
+
provider: "lmstudio",
|
|
514
|
+
endpoint: endpoint.trim(),
|
|
515
|
+
model: model.trim(),
|
|
516
|
+
temperature: 0.3,
|
|
517
|
+
maxTokens: 1024,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// custom
|
|
522
|
+
const currentCustomEndpoint = currentLlmSmall?.provider === "custom" ? currentLlmSmall.endpoint : undefined;
|
|
523
|
+
const currentCustomModel = currentLlmSmall?.provider === "custom" ? currentLlmSmall.model : undefined;
|
|
524
|
+
const endpoint = await prompt(() => p.text({
|
|
525
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
526
|
+
placeholder: currentCustomEndpoint ?? "https://your-host/v1/chat/completions",
|
|
527
|
+
...(currentCustomEndpoint ? { defaultValue: currentCustomEndpoint } : {}),
|
|
528
|
+
validate: (v) => {
|
|
529
|
+
if (!v?.trim())
|
|
530
|
+
return "Endpoint cannot be empty";
|
|
531
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
532
|
+
return "Must start with http:// or https://";
|
|
533
|
+
},
|
|
534
|
+
}));
|
|
535
|
+
const model = await prompt(() => p.text({
|
|
536
|
+
message: "Model name:",
|
|
537
|
+
placeholder: currentCustomModel ?? "gpt-4o-mini",
|
|
538
|
+
...(currentCustomModel ? { defaultValue: currentCustomModel } : {}),
|
|
539
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
540
|
+
}));
|
|
541
|
+
const apiKeyInput = await promptOrBack(() => p.text({
|
|
542
|
+
message: "API key (optional — press Enter to skip):",
|
|
543
|
+
placeholder: "",
|
|
544
|
+
}));
|
|
545
|
+
llm = {
|
|
546
|
+
provider: "custom",
|
|
547
|
+
endpoint: endpoint.trim(),
|
|
548
|
+
model: model.trim(),
|
|
549
|
+
temperature: 0.3,
|
|
550
|
+
maxTokens: 1024,
|
|
551
|
+
...(apiKeyInput?.trim() ? { apiKey: apiKeyInput.trim() } : {}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
// Best-effort probe — never blocks setup.
|
|
555
|
+
const probeSpin = p.spinner();
|
|
556
|
+
probeSpin.start("Probing LLM (structured-output round-trip)...");
|
|
557
|
+
const probe = await probeLlmCapabilities(llm);
|
|
558
|
+
if (probe.reachable && probe.structuredOutput) {
|
|
559
|
+
probeSpin.stop("LLM reachable; structured output verified.");
|
|
560
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
|
|
561
|
+
}
|
|
562
|
+
else if (probe.reachable) {
|
|
563
|
+
probeSpin.stop("LLM reachable but structured-output probe failed.");
|
|
564
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
probeSpin.stop("LLM not reachable.");
|
|
568
|
+
p.log.warn(`Could not reach the LLM endpoint${probe.error ? ` (${probe.error})` : ""}. Configuration was saved; verify your endpoint and API key, then retry.`);
|
|
569
|
+
}
|
|
570
|
+
return { llm, skipped: false, ollamaEndpoint };
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Step 2/2: Configure the agent connection used for agentic features.
|
|
574
|
+
*
|
|
575
|
+
* Options depend on whether Step 1 was completed or skipped.
|
|
576
|
+
*/
|
|
577
|
+
export async function stepAgentConnection(current, smallModel) {
|
|
578
|
+
p.log.step("Step 2/2: Configure your agent connection");
|
|
579
|
+
p.note([
|
|
580
|
+
"This connection is used for agentic commands:",
|
|
581
|
+
" • akm propose (generate improvement proposals)",
|
|
582
|
+
" • akm improve (run the reflect/distill/consolidate self-improvement pipeline)",
|
|
583
|
+
" • akm tasks run (run automated task prompts)",
|
|
584
|
+
].join("\n"));
|
|
585
|
+
// Detect available CLI agents.
|
|
586
|
+
const detections = detectAgentCliProfiles(current);
|
|
587
|
+
const currentAgentBlock = getCurrentAgentBlock(current);
|
|
588
|
+
const availableClis = detections.filter((d) => d.available);
|
|
589
|
+
const agentOptions = [];
|
|
590
|
+
if (!smallModel.skipped && smallModel.llm) {
|
|
591
|
+
agentOptions.push({
|
|
592
|
+
value: "same-connection",
|
|
593
|
+
label: "Same connection, select model",
|
|
594
|
+
hint: `uses ${smallModel.llm.endpoint.replace("/v1/chat/completions", "")}`,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
agentOptions.push({ value: "new-connection", label: "New connection (different endpoint)" });
|
|
598
|
+
if (availableClis.length > 0) {
|
|
599
|
+
agentOptions.push({
|
|
600
|
+
value: "cli-agent",
|
|
601
|
+
label: "Installed CLI agent",
|
|
602
|
+
hint: `${availableClis.map((d) => d.name).join(", ")} detected`,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
agentOptions.push({ value: "none", label: "None — disable agentic features" });
|
|
606
|
+
if (currentAgentBlock) {
|
|
607
|
+
const currentDesc = currentAgentBlock.default
|
|
608
|
+
? `CLI: ${currentAgentBlock.default}`
|
|
609
|
+
: currentAgentBlock.profiles?.default?.model
|
|
610
|
+
? `SDK: ${currentAgentBlock.profiles.default.model}`
|
|
611
|
+
: "configured";
|
|
612
|
+
agentOptions.push({ value: "keep", label: `Keep current: ${currentDesc}` });
|
|
613
|
+
}
|
|
614
|
+
const initialAgentValue = currentAgentBlock
|
|
615
|
+
? "keep"
|
|
616
|
+
: availableClis.length > 0 && smallModel.skipped
|
|
617
|
+
? "cli-agent"
|
|
618
|
+
: !smallModel.skipped && smallModel.llm
|
|
619
|
+
? "same-connection"
|
|
620
|
+
: availableClis.length > 0
|
|
621
|
+
? "cli-agent"
|
|
622
|
+
: "none";
|
|
623
|
+
const agentChoice = await prompt(() => p.select({
|
|
624
|
+
message: "How do you want to run agent commands?",
|
|
625
|
+
options: agentOptions,
|
|
626
|
+
initialValue: initialAgentValue,
|
|
627
|
+
}));
|
|
628
|
+
if (agentChoice === "keep") {
|
|
629
|
+
return currentAgentBlock;
|
|
630
|
+
}
|
|
631
|
+
if (agentChoice === "none") {
|
|
632
|
+
p.note([
|
|
633
|
+
"Agentic features disabled:",
|
|
634
|
+
' • akm propose — will show "no agent configured" error',
|
|
635
|
+
' • akm improve — will show "no agent configured" error',
|
|
636
|
+
' • akm tasks run — will show "no agent configured" error',
|
|
637
|
+
"",
|
|
638
|
+
"You can configure this later with `akm setup`.",
|
|
639
|
+
].join("\n"), "Warning");
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
if (agentChoice === "same-connection") {
|
|
643
|
+
if (smallModel.skipped || !smallModel.llm) {
|
|
644
|
+
p.log.warn("You skipped the small model connection. Configure one to use the same connection. Falling back to 'new connection'.");
|
|
645
|
+
// Fall through to new-connection flow
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
|
|
649
|
+
p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
|
|
650
|
+
const profileName = smallModel.llm.provider ?? "default";
|
|
651
|
+
// Pre-populate from existing agent profile for this provider, if any.
|
|
652
|
+
const existingAgentModel = currentAgentBlock?.profiles?.[profileName]?.model ?? smallModel.llm.model ?? undefined;
|
|
653
|
+
const agentModel = await prompt(() => p.text({
|
|
654
|
+
message: "Model to use for agent tasks (same model is fine, larger models work better):",
|
|
655
|
+
placeholder: existingAgentModel ?? "qwen2.5-coder:32b",
|
|
656
|
+
...(existingAgentModel ? { defaultValue: existingAgentModel } : {}),
|
|
657
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
658
|
+
}));
|
|
659
|
+
return {
|
|
660
|
+
...(currentAgentBlock ?? {}),
|
|
661
|
+
profiles: {
|
|
662
|
+
...(currentAgentBlock?.profiles ?? {}),
|
|
663
|
+
[profileName]: {
|
|
664
|
+
...(currentAgentBlock?.profiles?.[profileName] ?? {}),
|
|
665
|
+
sdkMode: true,
|
|
666
|
+
model: agentModel.trim(),
|
|
667
|
+
endpoint: smallModel.llm.endpoint,
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
default: profileName,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (agentChoice === "cli-agent") {
|
|
675
|
+
if (availableClis.length === 0) {
|
|
676
|
+
p.log.warn("No agent CLIs detected on PATH.");
|
|
677
|
+
return currentAgentBlock;
|
|
678
|
+
}
|
|
679
|
+
const initialCli = pickDefaultAgentProfile(detections, currentAgentBlock?.default) ?? availableClis[0]?.name;
|
|
680
|
+
const selectedCli = await prompt(() => p.select({
|
|
681
|
+
message: "Which CLI agent?",
|
|
682
|
+
options: availableClis.map((d) => ({
|
|
683
|
+
value: d.name,
|
|
684
|
+
label: d.name,
|
|
685
|
+
hint: d.resolvedPath ?? d.bin,
|
|
686
|
+
})),
|
|
687
|
+
initialValue: initialCli,
|
|
688
|
+
}));
|
|
689
|
+
return {
|
|
690
|
+
...(currentAgentBlock ?? {}),
|
|
691
|
+
default: selectedCli,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
|
|
695
|
+
// Pre-populate from current "custom" agent profile if available.
|
|
696
|
+
const currentCustomAgentProfile = currentAgentBlock?.profiles?.custom;
|
|
697
|
+
const currentNewEndpoint = currentCustomAgentProfile?.endpoint ?? undefined;
|
|
698
|
+
const currentNewModel = currentCustomAgentProfile?.model ?? undefined;
|
|
699
|
+
const newEndpoint = await prompt(() => p.text({
|
|
700
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
701
|
+
placeholder: currentNewEndpoint ?? "https://your-host/v1/chat/completions",
|
|
702
|
+
...(currentNewEndpoint ? { defaultValue: currentNewEndpoint } : {}),
|
|
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
|
+
const newApiKeyInput = await promptOrBack(() => p.text({
|
|
711
|
+
message: "API key (optional — press Enter to skip):",
|
|
712
|
+
placeholder: "",
|
|
713
|
+
}));
|
|
714
|
+
const newModel = await prompt(() => p.text({
|
|
715
|
+
message: "Model name (larger is better, e.g. gpt-4o):",
|
|
716
|
+
placeholder: currentNewModel ?? "gpt-4o",
|
|
717
|
+
...(currentNewModel ? { defaultValue: currentNewModel } : {}),
|
|
718
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
719
|
+
}));
|
|
720
|
+
const customProfile = {
|
|
721
|
+
sdkMode: true,
|
|
722
|
+
endpoint: newEndpoint.trim(),
|
|
723
|
+
model: newModel.trim(),
|
|
724
|
+
...(newApiKeyInput?.trim() ? { apiKey: newApiKeyInput.trim() } : {}),
|
|
725
|
+
};
|
|
726
|
+
return {
|
|
727
|
+
...(currentAgentBlock ?? {}),
|
|
728
|
+
profiles: {
|
|
729
|
+
...(currentAgentBlock?.profiles ?? {}),
|
|
730
|
+
custom: customProfile,
|
|
731
|
+
},
|
|
732
|
+
default: "custom",
|
|
733
|
+
};
|
|
734
|
+
}
|