min-agent 0.5.0 → 0.5.1
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/README.md +14 -0
- package/dist/agent.js +53 -7
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/index.js +10 -2
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- package/dist/cli/program.js +7 -1
- package/dist/cli/setup/detect.js +17 -0
- package/dist/cli/setup/flags.js +12 -0
- package/dist/cli/setup/flow.js +108 -0
- package/dist/cli/setup/provider-form.js +102 -0
- package/dist/cli/setup/ui.js +534 -0
- package/dist/config.js +52 -159
- package/dist/context-window.js +33 -23
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -0
- package/dist/serve/routes-meta.js +35 -0
- package/dist/thinking-wire.js +15 -4
- package/dist/thinking.js +26 -2
- package/dist/tui/App.js +18 -6
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +4 -2
- package/dist/tui/ThinkPicker.js +4 -6
- package/dist/tui/index.js +7 -1
- package/dist/tui/slash-commands.js +6 -0
- package/dist/tui/slash-handler.js +27 -1
- package/dist/tui-chat.js +25 -3
- package/docs/API.md +19 -2
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +3 -1
- package/skills/self-config/reference.md +4 -3
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ Type `/` to open a command menu. Use **↑ / ↓** to recall previous messages.
|
|
|
76
76
|
| `/mcp` | Show connected MCP servers |
|
|
77
77
|
| `/skills` | Manage skills |
|
|
78
78
|
| `/tokens` | Usage and estimated cost |
|
|
79
|
+
| `/ctx` | Choose local-model context window |
|
|
79
80
|
| `/help` | Show all commands |
|
|
80
81
|
| `/exit` | Exit |
|
|
81
82
|
|
|
@@ -117,6 +118,12 @@ In chat, `/memory on` or `/memory off` toggles it; `/memory` lists notes; `/memo
|
|
|
117
118
|
|
|
118
119
|
Run `min-agent setup` for a guided setup — this is the easiest way to add or switch AI providers.
|
|
119
120
|
|
|
121
|
+
```bash
|
|
122
|
+
min-agent setup --type ollama
|
|
123
|
+
min-agent setup --type openai-compatible --url https://api.example.com/v1 --api-key "$KEY" --name groq --default-model llama-3.1-70b
|
|
124
|
+
min-agent setup --switch ollama
|
|
125
|
+
```
|
|
126
|
+
|
|
120
127
|
Settings live in `~/.min-agent/config.json` (global). A project can have its own `.min-agent/config.json` that only applies there.
|
|
121
128
|
|
|
122
129
|
## Extending It
|
|
@@ -194,6 +201,7 @@ min-agent --provider anthropic "你的消息"
|
|
|
194
201
|
| `/mcp` | 查看已连接的 MCP 服务 |
|
|
195
202
|
| `/skills` | 管理技能 |
|
|
196
203
|
| `/tokens` | 查看用量与预估费用 |
|
|
204
|
+
| `/ctx` | 选择本地模型上下文窗口 |
|
|
197
205
|
| `/help` | 查看全部命令 |
|
|
198
206
|
| `/exit` | 退出 |
|
|
199
207
|
|
|
@@ -235,6 +243,12 @@ min-agent memory add "prefer bun"
|
|
|
235
243
|
|
|
236
244
|
运行 `min-agent setup` 即可通过引导式流程完成配置,这是添加或切换 AI 服务商最简单的方式。
|
|
237
245
|
|
|
246
|
+
```bash
|
|
247
|
+
min-agent setup --type ollama
|
|
248
|
+
min-agent setup --type openai-compatible --url https://api.example.com/v1 --api-key "$KEY" --name groq --default-model llama-3.1-70b
|
|
249
|
+
min-agent setup --switch ollama
|
|
250
|
+
```
|
|
251
|
+
|
|
238
252
|
配置保存在 `~/.min-agent/config.json`(全局生效)。也可以在某个项目下单独创建 `.min-agent/config.json`,仅对该项目生效。
|
|
239
253
|
|
|
240
254
|
## 扩展能力
|
package/dist/agent.js
CHANGED
|
@@ -21,6 +21,7 @@ import { XmlSearchSplitter } from "./xml-search.js";
|
|
|
21
21
|
import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
|
|
22
22
|
import { createTodoTool, captureGoal, copyTaskState, emptyTaskState, formatTaskStatePrompt, } from "./tools/todo.js";
|
|
23
23
|
import { getContextWindowInfo } from "./context-window.js";
|
|
24
|
+
import { formatTokenCount } from "./token-display.js";
|
|
24
25
|
import { log, logToolCall, logToolResult, startRunLog, nextRunPass, endRunLog } from "./logger.js";
|
|
25
26
|
import { markSyntheticMessage, isSyntheticMessage } from "./synthetic.js";
|
|
26
27
|
import { runWithInstructionTracker, resetActiveInstructionTracker } from "./instructions.js";
|
|
@@ -269,14 +270,34 @@ function sessionStateMessage(volatile) {
|
|
|
269
270
|
return null;
|
|
270
271
|
return { role: "system", content: [SESSION_STATE_HEADER, ...volatile].join("\n\n") };
|
|
271
272
|
}
|
|
273
|
+
export function parseContextOverflow(msg) {
|
|
274
|
+
if (!isContextOverflowError(msg))
|
|
275
|
+
return null;
|
|
276
|
+
const prompt = msg.match(/n_prompt_tokens["\s:=]+(\d+)/i)?.[1] ?? msg.match(/request\s*\((\d+)\s*tokens?\)/i)?.[1];
|
|
277
|
+
const ctx = msg.match(/n_ctx["\s:=]+(\d+)/i)?.[1] ?? msg.match(/context size\s*\((\d+)\s*tokens?\)/i)?.[1];
|
|
278
|
+
return {
|
|
279
|
+
...(prompt ? { promptTokens: Number(prompt) } : {}),
|
|
280
|
+
...(ctx ? { ctxTokens: Number(ctx) } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function formatContextOverflowMessage(msg) {
|
|
284
|
+
const info = parseContextOverflow(msg);
|
|
285
|
+
const prompt = info?.promptTokens != null ? formatTokenCount(info.promptTokens) : null;
|
|
286
|
+
const ctx = info?.ctxTokens != null ? formatTokenCount(info.ctxTokens) : null;
|
|
287
|
+
if (prompt && ctx)
|
|
288
|
+
return `请求内容约 ${prompt} token,超过当前上下文窗口 ${ctx}。请把窗口调大后再试。`;
|
|
289
|
+
return "请求内容超过当前上下文窗口。请把窗口调大后再试。";
|
|
290
|
+
}
|
|
272
291
|
function formatErrorMessage(msg) {
|
|
292
|
+
if (isContextOverflowError(msg))
|
|
293
|
+
return formatContextOverflowMessage(msg);
|
|
273
294
|
if (msg.includes("API key") || msg.includes("Unauthorized") || msg.includes("Forbidden")) {
|
|
274
295
|
return "Authentication error: Check your API key.";
|
|
275
296
|
}
|
|
276
297
|
if (msg.includes("429") || msg.includes("rate limit") || msg.includes("Rate limit")) {
|
|
277
298
|
return "Rate limited after retries. Please wait and try again.";
|
|
278
299
|
}
|
|
279
|
-
if (msg.includes("timeout") || msg.includes("ETIMEDOUT")
|
|
300
|
+
if (msg.includes("timeout") || msg.includes("ETIMEDOUT")) {
|
|
280
301
|
return `Network error (retries exhausted): ${msg}`;
|
|
281
302
|
}
|
|
282
303
|
return msg;
|
|
@@ -292,7 +313,7 @@ function safeText(value) {
|
|
|
292
313
|
}
|
|
293
314
|
}
|
|
294
315
|
function isContextOverflowError(msg) {
|
|
295
|
-
return /context[\s_-]*(length|window|size)|context_length_exceeded|maximum context|too many tokens|prompt (is )?too long|reduce the (prompt )?length|token limit exceeded/i.test(msg);
|
|
316
|
+
return /exceed_context_size_error|context[\s_-]*(length|window|size)|context_length_exceeded|maximum context|too many tokens|prompt (is )?too long|reduce the (prompt )?length|token limit exceeded/i.test(msg);
|
|
296
317
|
}
|
|
297
318
|
function invokeCallback(fn, ...args) {
|
|
298
319
|
if (!fn)
|
|
@@ -606,7 +627,7 @@ async function runOnceCoreLoop(messages, systemPrompt, modelId, abortSignal, cal
|
|
|
606
627
|
log("error", msg);
|
|
607
628
|
hasError = true;
|
|
608
629
|
if (cbs.onStreamError)
|
|
609
|
-
invokeCallback(cbs.onStreamError, msg);
|
|
630
|
+
invokeCallback(cbs.onStreamError, formatErrorMessage(msg));
|
|
610
631
|
else
|
|
611
632
|
console.error(`\x1b[31m${formatErrorMessage(msg)}\x1b[0m`);
|
|
612
633
|
return finish({ aborted: Boolean(abortSignal?.aborted), maxStepsReached: false });
|
|
@@ -630,6 +651,25 @@ async function runOnceCoreLoop(messages, systemPrompt, modelId, abortSignal, cal
|
|
|
630
651
|
applyToolPrune(messages, pruneBudget());
|
|
631
652
|
continue;
|
|
632
653
|
}
|
|
654
|
+
// Prompt (system + tools + messages) does not fit the loaded window.
|
|
655
|
+
// Retrying the same request cannot help; compaction only helps if history is long.
|
|
656
|
+
if (inner.contextPressure && inner.stepCount === 0 && !inner.lastStepHadTools) {
|
|
657
|
+
const compacted = await applyCompaction(messages, model, tracker, cbs, callbacks, { ...compactCfg, force: true }, pruneBudget());
|
|
658
|
+
if (compacted) {
|
|
659
|
+
continues++;
|
|
660
|
+
if (continues > maxContinues) {
|
|
661
|
+
return finish({ aborted: false, maxStepsReached: true, incomplete: true });
|
|
662
|
+
}
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
hasError = true;
|
|
666
|
+
const display = formatErrorMessage(inner.overflowError ?? "");
|
|
667
|
+
if (cbs.onStreamError)
|
|
668
|
+
invokeCallback(cbs.onStreamError, display);
|
|
669
|
+
else
|
|
670
|
+
console.error(`\x1b[31m${display}\x1b[0m`);
|
|
671
|
+
return finish({ aborted: false, maxStepsReached: false });
|
|
672
|
+
}
|
|
633
673
|
// The provider produced nothing at all (no text, no reasoning-backed reply,
|
|
634
674
|
// no tool call): a transport / gateway hiccup rather than model intent.
|
|
635
675
|
// Re-send the same request with backoff instead of nudging the model.
|
|
@@ -794,6 +834,7 @@ class StreamRenderer {
|
|
|
794
834
|
thinkingSplit = new ThinkingBodySplitter();
|
|
795
835
|
xmlSplit = new XmlSearchSplitter();
|
|
796
836
|
rawText = "";
|
|
837
|
+
hadThinking = false;
|
|
797
838
|
thinkingOpen = false;
|
|
798
839
|
constructor(cbs, interactive) {
|
|
799
840
|
this.cbs = cbs;
|
|
@@ -802,6 +843,7 @@ class StreamRenderer {
|
|
|
802
843
|
emitThinking(t) {
|
|
803
844
|
if (!t)
|
|
804
845
|
return;
|
|
846
|
+
this.hadThinking = true;
|
|
805
847
|
if (this.cbs.onThinkingDelta) {
|
|
806
848
|
invokeCallback(this.cbs.onThinkingDelta, t);
|
|
807
849
|
return;
|
|
@@ -926,6 +968,7 @@ class InnerStreamMachine {
|
|
|
926
968
|
stepFinishSeen = false;
|
|
927
969
|
stepUsage;
|
|
928
970
|
totalUsage;
|
|
971
|
+
overflowError;
|
|
929
972
|
constructor(params) {
|
|
930
973
|
this.params = params;
|
|
931
974
|
this.assembler = new TurnAssembler(params.messages, (info) => {
|
|
@@ -1200,6 +1243,7 @@ class InnerStreamMachine {
|
|
|
1200
1243
|
}
|
|
1201
1244
|
if (isContextOverflowError(errorMsg)) {
|
|
1202
1245
|
this.stop.context = true;
|
|
1246
|
+
this.overflowError = errorMsg;
|
|
1203
1247
|
log("warn", `context overflow: ${errorMsg}`);
|
|
1204
1248
|
await this.flushOutputAndStep();
|
|
1205
1249
|
this.innerController.abort();
|
|
@@ -1208,7 +1252,7 @@ class InnerStreamMachine {
|
|
|
1208
1252
|
this.hasError = true;
|
|
1209
1253
|
log("error", `stream error: ${errorMsg}`);
|
|
1210
1254
|
if (cbs.onStreamError)
|
|
1211
|
-
invokeCallback(cbs.onStreamError, errorMsg);
|
|
1255
|
+
invokeCallback(cbs.onStreamError, formatErrorMessage(errorMsg));
|
|
1212
1256
|
else
|
|
1213
1257
|
console.error(`\x1b[31m${formatErrorMessage(errorMsg)}\x1b[0m`);
|
|
1214
1258
|
return false;
|
|
@@ -1230,7 +1274,8 @@ class InnerStreamMachine {
|
|
|
1230
1274
|
wrapUp: this.stop.wrapUp,
|
|
1231
1275
|
lastStepHadTools: this.assembler.lastStepHadTools,
|
|
1232
1276
|
xmlToolFollowUp: this.xmlSearchRecovered && !this.answerAfterXml && !this.stop.doom,
|
|
1233
|
-
emptyCompletion: !this.assembler.hadAssistantText && !this.assembler.hadTools && !this.xmlSearchRecovered,
|
|
1277
|
+
emptyCompletion: !this.stop.context && !this.assembler.hadAssistantText && !this.assembler.hadTools && !this.xmlSearchRecovered,
|
|
1278
|
+
overflowError: this.overflowError,
|
|
1234
1279
|
providerStall: this.stop.stall,
|
|
1235
1280
|
stepCount: this.assembler.innerSteps,
|
|
1236
1281
|
usage,
|
|
@@ -1255,7 +1300,7 @@ class InnerStreamMachine {
|
|
|
1255
1300
|
// The provider went quiet and we cut the request. Nothing was produced →
|
|
1256
1301
|
// let the empty-reply retry path re-send it; otherwise keep the partial
|
|
1257
1302
|
// turn and continue like any other tool-only step.
|
|
1258
|
-
const produced = this.assembler.hadAssistantText || this.assembler.hadTools;
|
|
1303
|
+
const produced = this.assembler.hadAssistantText || this.assembler.hadTools || this.renderer.hadThinking;
|
|
1259
1304
|
log("warn", `provider stall: ${describeError(err)}`);
|
|
1260
1305
|
this.stop.stall = true;
|
|
1261
1306
|
return this.snapshot(usage, { hasError: false, userAborted: false, emptyCompletion: !produced });
|
|
@@ -1267,12 +1312,13 @@ class InnerStreamMachine {
|
|
|
1267
1312
|
log("error", msg);
|
|
1268
1313
|
if (isContextOverflowError(msg)) {
|
|
1269
1314
|
this.stop.context = true;
|
|
1315
|
+
this.overflowError = msg;
|
|
1270
1316
|
return this.snapshot(usage, { hasError: false, contextPressure: true, userAborted: false });
|
|
1271
1317
|
}
|
|
1272
1318
|
this.hasError = true;
|
|
1273
1319
|
const display = formatErrorMessage(msg);
|
|
1274
1320
|
if (this.params.cbs.onStreamError)
|
|
1275
|
-
invokeCallback(this.params.cbs.onStreamError,
|
|
1321
|
+
invokeCallback(this.params.cbs.onStreamError, display);
|
|
1276
1322
|
else if (display !== msg)
|
|
1277
1323
|
console.error(`\x1b[31m${display}\x1b[0m`);
|
|
1278
1324
|
else
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
2
|
import { resolveScope, parsePositiveIntArg } from "../option-helpers.js";
|
|
3
|
-
import {
|
|
3
|
+
import { registerSetupCommand } from "./setup.js";
|
|
4
4
|
import { runModelsCommand } from "./models.js";
|
|
5
5
|
import { runChat } from "./chat.js";
|
|
6
6
|
import { runExec } from "./exec.js";
|
|
@@ -8,6 +8,7 @@ import { runServeCommand } from "./serve.js";
|
|
|
8
8
|
import { runSandboxCommand } from "./sandbox.js";
|
|
9
9
|
import { runPermissionCommand } from "./permission.js";
|
|
10
10
|
import { runThinkCommand } from "./think.js";
|
|
11
|
+
import { runCtxCommand } from "./ctx.js";
|
|
11
12
|
import { runInitCommand } from "./init.js";
|
|
12
13
|
import { runRulesShow, runRulesEdit } from "./rules.js";
|
|
13
14
|
import { runUpdateCommand } from "./update.js";
|
|
@@ -45,7 +46,7 @@ export function registerCommands(program) {
|
|
|
45
46
|
await runExec(message, rootOpts());
|
|
46
47
|
});
|
|
47
48
|
// --- Setup & config ----------------------------------------------------
|
|
48
|
-
program
|
|
49
|
+
registerSetupCommand(program, rootOpts);
|
|
49
50
|
program.command("init").description("Initialize .min-agent/ in current directory").action(runInitCommand);
|
|
50
51
|
program
|
|
51
52
|
.command("models")
|
|
@@ -125,6 +126,13 @@ export function registerCommands(program) {
|
|
|
125
126
|
const opts = rootOpts();
|
|
126
127
|
await runThinkCommand(level ? [level] : [], opts, opts);
|
|
127
128
|
});
|
|
129
|
+
program
|
|
130
|
+
.command("ctx")
|
|
131
|
+
.description("Show or set the Ollama context window level")
|
|
132
|
+
.argument("[level]", "2k, 4k, 8k, 12k, 16k, 32k, 64k, 128k, 256k, or auto")
|
|
133
|
+
.action(async (level) => {
|
|
134
|
+
await runCtxCommand(level ? [level] : []);
|
|
135
|
+
});
|
|
128
136
|
// --- Integrations --------------------------------------------------------
|
|
129
137
|
const mcp = program.command("mcp").description("Manage MCP servers");
|
|
130
138
|
mcp
|
|
@@ -1,4 +1,56 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Option } from "commander";
|
|
3
|
+
import { fetchModelsLive, getConfigDir, loadConfig, saveConfig } from "../../config.js";
|
|
4
|
+
import { parsePositiveIntArg } from "../option-helpers.js";
|
|
5
|
+
import { parseProviderType } from "../setup/flags.js";
|
|
6
|
+
import { runSetup } from "../setup/flow.js";
|
|
7
|
+
export function registerSetupCommand(program, rootOpts) {
|
|
8
|
+
program
|
|
9
|
+
.command("setup")
|
|
10
|
+
.description("Configure providers (interactively, or with flags)")
|
|
11
|
+
.addOption(new Option("--type <type>", "openai-compatible, openai, or ollama").argParser(parseProviderType))
|
|
12
|
+
.option("--url <url>", "API base URL")
|
|
13
|
+
.option("--api-key <key>", "API key")
|
|
14
|
+
.option("--name <name>", "Provider name")
|
|
15
|
+
.option("--default-model <id>", "Default model to save")
|
|
16
|
+
.addOption(new Option("--context-window <n>", "Context window in tokens").argParser(parsePositiveIntArg("--context-window")))
|
|
17
|
+
.option("--switch <name>", "Set the active provider")
|
|
18
|
+
.option("--remove <name>", "Remove a provider")
|
|
19
|
+
.option("-y, --yes", "Skip overwrite and delete confirmation")
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
await runSetupCommand(opts, { yes: Boolean(opts.yes || rootOpts().yes) });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function createSetupDeps() {
|
|
25
|
+
return {
|
|
26
|
+
isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
27
|
+
load: loadConfig,
|
|
28
|
+
save: saveConfig,
|
|
29
|
+
fetchLive: fetchModelsLive,
|
|
30
|
+
log: (message) => {
|
|
31
|
+
console.log(message);
|
|
32
|
+
},
|
|
33
|
+
configPath: () => path.join(getConfigDir(), "config.json"),
|
|
34
|
+
runWizard: async (wizardMode) => {
|
|
35
|
+
const { renderSetupWizard } = await import("../setup/ui.js");
|
|
36
|
+
return renderSetupWizard(wizardMode);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function offerInteractiveSetup(mode) {
|
|
41
|
+
await runSetup({}, { ...createSetupDeps(), isTty: true }, mode);
|
|
42
|
+
}
|
|
43
|
+
export async function runSetupCommand(opts, session) {
|
|
44
|
+
const flags = {
|
|
45
|
+
type: opts.type,
|
|
46
|
+
url: opts.url,
|
|
47
|
+
apiKey: opts.apiKey,
|
|
48
|
+
name: opts.name,
|
|
49
|
+
defaultModel: opts.defaultModel,
|
|
50
|
+
contextWindow: opts.contextWindow,
|
|
51
|
+
switch: opts.switch,
|
|
52
|
+
remove: opts.remove,
|
|
53
|
+
yes: Boolean(session.yes || opts.yes),
|
|
54
|
+
};
|
|
55
|
+
await runSetup(flags, createSetupDeps());
|
|
4
56
|
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { isConfigured } from "../../config.js";
|
|
2
2
|
import { CliError } from "../errors.js";
|
|
3
|
+
import { shouldOfferSetupWizard } from "../setup/flow.js";
|
|
3
4
|
export async function startTuiSession(input) {
|
|
4
5
|
if (!isConfigured()) {
|
|
5
|
-
|
|
6
|
+
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
7
|
+
if (!shouldOfferSetupWizard(false, tty)) {
|
|
8
|
+
throw new CliError("Not configured.", { hint: "Run: min-agent setup" });
|
|
9
|
+
}
|
|
10
|
+
const { offerInteractiveSetup } = await import("./setup.js");
|
|
11
|
+
await offerInteractiveSetup("session-gate");
|
|
12
|
+
if (!isConfigured()) {
|
|
13
|
+
throw new CliError("Not configured.", { hint: "Run: min-agent setup" });
|
|
14
|
+
}
|
|
6
15
|
}
|
|
7
16
|
const { runTui } = await import("../../tui-chat.js");
|
|
8
17
|
const prompt = input.positionals.join(" ").trim();
|
package/dist/cli/program.js
CHANGED
|
@@ -47,7 +47,7 @@ Session:
|
|
|
47
47
|
min-agent exec --resume <id> <msg> Continue a saved exec session
|
|
48
48
|
|
|
49
49
|
Setup & config:
|
|
50
|
-
min-agent setup
|
|
50
|
+
min-agent setup [--type T] Configure providers (interactively, or with flags)
|
|
51
51
|
min-agent init Initialize .min-agent/ in current directory
|
|
52
52
|
min-agent models List available models
|
|
53
53
|
min-agent rules [edit] Show or edit instruction rules
|
|
@@ -57,6 +57,7 @@ Setup & config:
|
|
|
57
57
|
min-agent sandbox network <allow|deny> Show or set network policy
|
|
58
58
|
min-agent permission [ask|accept-edits|allow-all] Show or set confirmation mode
|
|
59
59
|
min-agent think [off|low|medium|high|max] Show or set thinking intensity
|
|
60
|
+
min-agent ctx [2k|4k|8k|12k|16k|32k|64k|128k|256k|auto] Show or set Ollama context window
|
|
60
61
|
|
|
61
62
|
Integrations:
|
|
62
63
|
min-agent mcp <list|add|remove|info|check|enable|disable> Manage MCP servers
|
|
@@ -79,6 +80,10 @@ Thinking intensity (default: medium):
|
|
|
79
80
|
high Strong reasoning
|
|
80
81
|
max Highest reasoning
|
|
81
82
|
|
|
83
|
+
Ollama context window:
|
|
84
|
+
2k … 256k Loaded window size
|
|
85
|
+
auto Use the model's default
|
|
86
|
+
|
|
82
87
|
Memory (default: off):
|
|
83
88
|
off Do not inject memories or expose memory tools
|
|
84
89
|
on Remember facts across sessions
|
|
@@ -96,6 +101,7 @@ Rules (loaded as system instructions):
|
|
|
96
101
|
|
|
97
102
|
Examples:
|
|
98
103
|
min-agent setup
|
|
104
|
+
min-agent setup --type ollama
|
|
99
105
|
min-agent
|
|
100
106
|
min-agent "hello"
|
|
101
107
|
min-agent exec "hello"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { normalizeOllamaBaseURL } from "../../config.js";
|
|
2
|
+
const DEFAULT_TAGS_URL = "http://localhost:11434/api/tags";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 800;
|
|
4
|
+
const DEFAULT_BASE = "http://localhost:11434/v1";
|
|
5
|
+
export async function detectLocalOllama(opts) {
|
|
6
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
7
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetchImpl(DEFAULT_TAGS_URL, { signal: AbortSignal.timeout(timeoutMs) });
|
|
10
|
+
if (!response.ok)
|
|
11
|
+
return { found: false };
|
|
12
|
+
return { found: true, baseURL: normalizeOllamaBaseURL(DEFAULT_BASE) };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { found: false };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { userError } from "../errors.js";
|
|
2
|
+
export function parseProviderType(value) {
|
|
3
|
+
if (value === "openai-compatible" || value === "openai" || value === "ollama")
|
|
4
|
+
return value;
|
|
5
|
+
throw userError(`Invalid --type value: "${value}"`, "use one of: openai-compatible, openai, ollama");
|
|
6
|
+
}
|
|
7
|
+
export function isNonInteractive(flags) {
|
|
8
|
+
return Boolean(flags.type || flags.switch || flags.remove);
|
|
9
|
+
}
|
|
10
|
+
export function exclusiveActionCount(flags) {
|
|
11
|
+
return [flags.type, flags.switch, flags.remove].filter(Boolean).length;
|
|
12
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getActiveProvider, normalizeOllamaBaseURL } from "../../config.js";
|
|
2
|
+
import { CliError, runtimeError } from "../errors.js";
|
|
3
|
+
import { applyAddProvider, applyRemoveProvider, applySwitchProvider, suggestName, } from "./provider-form.js";
|
|
4
|
+
import { exclusiveActionCount } from "./flags.js";
|
|
5
|
+
const OPENAI_URL = "https://api.openai.com/v1";
|
|
6
|
+
const OLLAMA_URL = "http://localhost:11434/v1";
|
|
7
|
+
const NON_INTERACTIVE_HINT = "使用 --type、--switch 或 --remove。例如: min-agent setup --type ollama";
|
|
8
|
+
export function shouldOfferSetupWizard(configured, isTty) {
|
|
9
|
+
return !configured && isTty;
|
|
10
|
+
}
|
|
11
|
+
export async function runSetup(flags, deps, wizardMode) {
|
|
12
|
+
if (exclusiveActionCount(flags) > 1) {
|
|
13
|
+
throw new CliError("一次只能执行一种操作。");
|
|
14
|
+
}
|
|
15
|
+
if (flags.remove) {
|
|
16
|
+
await runRemove(flags.remove, flags.yes === true, deps);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (flags.switch) {
|
|
20
|
+
persist(applySwitchProvider(deps.load(), flags.switch), deps);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (flags.type) {
|
|
24
|
+
await runAdd(flags, deps);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!deps.isTty) {
|
|
28
|
+
throw new CliError("无法在非交互环境完成配置。", { hint: NON_INTERACTIVE_HINT });
|
|
29
|
+
}
|
|
30
|
+
const config = deps.load();
|
|
31
|
+
const mode = wizardMode ?? (hasUsableProvider(config) ? "hub" : "first-run");
|
|
32
|
+
await deps.runWizard(mode);
|
|
33
|
+
}
|
|
34
|
+
function hasUsableProvider(config) {
|
|
35
|
+
const provider = getActiveProvider(config);
|
|
36
|
+
return Boolean(provider?.baseURL && provider?.apiKey);
|
|
37
|
+
}
|
|
38
|
+
async function runRemove(name, yes, deps) {
|
|
39
|
+
if (!yes) {
|
|
40
|
+
throw new CliError("删除需要确认。", { hint: "加上 --yes" });
|
|
41
|
+
}
|
|
42
|
+
persist(applyRemoveProvider(deps.load(), name), deps);
|
|
43
|
+
}
|
|
44
|
+
async function runAdd(flags, deps) {
|
|
45
|
+
const type = flags.type;
|
|
46
|
+
const baseURL = resolveBaseURL(type, flags.url);
|
|
47
|
+
const apiKey = resolveApiKey(type, flags.apiKey);
|
|
48
|
+
const defaultModel = await resolveDefaultModel(type, baseURL, apiKey, flags.defaultModel, deps.fetchLive);
|
|
49
|
+
const config = deps.load();
|
|
50
|
+
const existing = (config.providers ?? []).map((p) => p.name).filter((name) => Boolean(name));
|
|
51
|
+
const name = (flags.name?.trim() || suggestName(type, baseURL, existing)).trim();
|
|
52
|
+
const draft = {
|
|
53
|
+
name,
|
|
54
|
+
type,
|
|
55
|
+
baseURL,
|
|
56
|
+
apiKey,
|
|
57
|
+
defaultModel,
|
|
58
|
+
...(flags.contextWindow != null ? { contextWindow: flags.contextWindow } : {}),
|
|
59
|
+
};
|
|
60
|
+
const overwrite = flags.yes === true;
|
|
61
|
+
const result = applyAddProvider(config, draft, { overwrite });
|
|
62
|
+
if (isApplyError(result) && result.error.startsWith("已存在同名服务商") && !overwrite) {
|
|
63
|
+
throw new CliError(result.error, { hint: "加上 --yes 以覆盖" });
|
|
64
|
+
}
|
|
65
|
+
persist(result, deps);
|
|
66
|
+
}
|
|
67
|
+
function resolveBaseURL(type, url) {
|
|
68
|
+
if (type === "openai")
|
|
69
|
+
return OPENAI_URL;
|
|
70
|
+
if (type === "ollama")
|
|
71
|
+
return normalizeOllamaBaseURL(url?.trim() || OLLAMA_URL);
|
|
72
|
+
const trimmed = url?.trim() ?? "";
|
|
73
|
+
if (!trimmed)
|
|
74
|
+
throw new CliError("API 地址不能为空。");
|
|
75
|
+
return trimmed.replace(/\/$/, "");
|
|
76
|
+
}
|
|
77
|
+
function resolveApiKey(type, apiKey) {
|
|
78
|
+
if (type === "ollama")
|
|
79
|
+
return apiKey?.trim() || "ollama";
|
|
80
|
+
const trimmed = apiKey?.trim() ?? "";
|
|
81
|
+
if (!trimmed)
|
|
82
|
+
throw new CliError("API 密钥不能为空。");
|
|
83
|
+
return trimmed;
|
|
84
|
+
}
|
|
85
|
+
async function resolveDefaultModel(type, baseURL, apiKey, explicit, fetchLive) {
|
|
86
|
+
const given = explicit?.trim() ?? "";
|
|
87
|
+
if (given)
|
|
88
|
+
return given;
|
|
89
|
+
const live = await fetchLive(baseURL, apiKey);
|
|
90
|
+
if (live.models[0])
|
|
91
|
+
return live.models[0];
|
|
92
|
+
if (type === "ollama")
|
|
93
|
+
return "llama3";
|
|
94
|
+
if (!live.ok && live.status == null) {
|
|
95
|
+
throw runtimeError("无法连接到该服务商。");
|
|
96
|
+
}
|
|
97
|
+
throw new CliError("默认模型不能为空。");
|
|
98
|
+
}
|
|
99
|
+
function persist(result, deps) {
|
|
100
|
+
if (isApplyError(result))
|
|
101
|
+
throw new CliError(result.error);
|
|
102
|
+
deps.save(result);
|
|
103
|
+
deps.log(`已保存到 ${deps.configPath()}`);
|
|
104
|
+
}
|
|
105
|
+
function isApplyError(result) {
|
|
106
|
+
return Object.keys(result).length === 1 && "error" in result;
|
|
107
|
+
}
|
|
108
|
+
export { isNonInteractive } from "./flags.js";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export function suggestName(type, baseURL, existing) {
|
|
2
|
+
const base = type === "openai" ? "openai" : type === "ollama" ? "ollama" : (hostSlug(baseURL) ?? "provider");
|
|
3
|
+
return uniqueName(base, existing);
|
|
4
|
+
}
|
|
5
|
+
export function validateProviderName(name) {
|
|
6
|
+
const trimmed = name.trim();
|
|
7
|
+
if (!trimmed || trimmed.includes("/") || trimmed.includes(":")) {
|
|
8
|
+
return "名称不能为空,且不能包含 / 或 :。";
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export function maskApiKey(key) {
|
|
13
|
+
if (key.length < 4)
|
|
14
|
+
return "****";
|
|
15
|
+
return `…${key.slice(-4)}`;
|
|
16
|
+
}
|
|
17
|
+
export function filterChoices(query, items) {
|
|
18
|
+
const q = query.trim().toLowerCase();
|
|
19
|
+
if (!q)
|
|
20
|
+
return [...items];
|
|
21
|
+
return items.filter((item) => item.toLowerCase().includes(q));
|
|
22
|
+
}
|
|
23
|
+
export function applyAddProvider(config, draft, opts) {
|
|
24
|
+
const nameError = validateProviderName(draft.name);
|
|
25
|
+
if (nameError)
|
|
26
|
+
return { error: nameError };
|
|
27
|
+
const name = draft.name.trim();
|
|
28
|
+
const providers = [...(config.providers ?? [])];
|
|
29
|
+
const idx = providers.findIndex((p) => p.name === name);
|
|
30
|
+
if (idx >= 0 && !opts.overwrite)
|
|
31
|
+
return { error: `已存在同名服务商 "${name}"。` };
|
|
32
|
+
const entry = toProvider(draft, name);
|
|
33
|
+
if (idx >= 0)
|
|
34
|
+
providers[idx] = entry;
|
|
35
|
+
else
|
|
36
|
+
providers.push(entry);
|
|
37
|
+
return { ...config, providers, activeProvider: name };
|
|
38
|
+
}
|
|
39
|
+
export function applySwitchProvider(config, name) {
|
|
40
|
+
const target = (config.providers ?? []).find((p) => p.name === name);
|
|
41
|
+
if (!target)
|
|
42
|
+
return { error: `找不到服务商 "${name}"。` };
|
|
43
|
+
return { ...config, activeProvider: name };
|
|
44
|
+
}
|
|
45
|
+
export function applyUpdateProvider(config, oldName, draft) {
|
|
46
|
+
const nameError = validateProviderName(draft.name);
|
|
47
|
+
if (nameError)
|
|
48
|
+
return { error: nameError };
|
|
49
|
+
const name = draft.name.trim();
|
|
50
|
+
const providers = [...(config.providers ?? [])];
|
|
51
|
+
const idx = providers.findIndex((p) => p.name === oldName);
|
|
52
|
+
if (idx < 0)
|
|
53
|
+
return { error: `找不到服务商 "${oldName}"。` };
|
|
54
|
+
if (name !== oldName && providers.some((p) => p.name === name)) {
|
|
55
|
+
return { error: `已存在同名服务商 "${name}"。` };
|
|
56
|
+
}
|
|
57
|
+
providers[idx] = toProvider(draft, name);
|
|
58
|
+
const activeProvider = config.activeProvider === oldName ? name : config.activeProvider;
|
|
59
|
+
return { ...config, providers, activeProvider };
|
|
60
|
+
}
|
|
61
|
+
export function applyRemoveProvider(config, name) {
|
|
62
|
+
const providers = config.providers ?? [];
|
|
63
|
+
if (!providers.some((p) => p.name === name))
|
|
64
|
+
return { error: `找不到服务商 "${name}"。` };
|
|
65
|
+
const remaining = providers.filter((p) => p.name !== name);
|
|
66
|
+
if (remaining.length === 0) {
|
|
67
|
+
const { activeProvider: _dropped, ...rest } = config;
|
|
68
|
+
return { ...rest, providers: [] };
|
|
69
|
+
}
|
|
70
|
+
const activeProvider = config.activeProvider === name ? remaining[0]?.name : config.activeProvider;
|
|
71
|
+
return { ...config, providers: remaining, activeProvider };
|
|
72
|
+
}
|
|
73
|
+
function toProvider(draft, name) {
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
type: draft.type,
|
|
77
|
+
baseURL: draft.baseURL,
|
|
78
|
+
apiKey: draft.apiKey,
|
|
79
|
+
defaultModel: draft.defaultModel,
|
|
80
|
+
...(draft.contextWindow != null ? { contextWindow: draft.contextWindow } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function hostSlug(baseURL) {
|
|
84
|
+
try {
|
|
85
|
+
const host = new URL(baseURL).hostname.toLowerCase();
|
|
86
|
+
const trimmed = host.startsWith("www.") ? host.slice(4) : host;
|
|
87
|
+
if (!trimmed)
|
|
88
|
+
return undefined;
|
|
89
|
+
return trimmed.replace(/\./g, "-");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function uniqueName(base, existing) {
|
|
96
|
+
if (!existing.includes(base))
|
|
97
|
+
return base;
|
|
98
|
+
let n = 2;
|
|
99
|
+
while (existing.includes(`${base}-${n}`))
|
|
100
|
+
n += 1;
|
|
101
|
+
return `${base}-${n}`;
|
|
102
|
+
}
|