min-agent 0.4.1 → 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 +46 -2
- package/dist/agent.js +89 -29
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +32 -7
- package/dist/cli/commands/memory.js +33 -15
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- package/dist/cli/commands/think.js +12 -0
- package/dist/cli/commands/write-config.js +22 -0
- package/dist/cli/option-helpers.js +13 -1
- package/dist/cli/program.js +57 -14
- 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/code-mode.js +1 -1
- package/dist/config.js +93 -159
- package/dist/context-window.js +39 -49
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -0
- package/dist/permission-cli.js +1 -4
- package/dist/provider.js +4 -1
- package/dist/reasoning-stream.js +158 -0
- package/dist/sandbox-cli.js +1 -4
- package/dist/scope.js +23 -0
- package/dist/serve/common.js +22 -1
- package/dist/serve/routes-chat.js +21 -1
- package/dist/serve/routes-memory.js +31 -2
- package/dist/serve/routes-meta.js +69 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +239 -0
- package/dist/thinking.js +166 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +48 -8
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +112 -37
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +75 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +13 -1
- package/dist/tui/layout.js +17 -0
- package/dist/tui/overlay-input.js +12 -0
- package/dist/tui/paste-draft.js +173 -0
- package/dist/tui/selection.js +8 -2
- package/dist/tui/slash-commands.js +24 -1
- package/dist/tui/slash-handler.js +88 -18
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +85 -7
- package/docs/API.md +69 -6
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +7 -4
- package/skills/self-config/reference.md +12 -6
|
@@ -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();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { runThinkCli } from "../../think-cli.js";
|
|
2
|
+
export async function runThinkCommand(args, opts, parentOpts) {
|
|
3
|
+
const scope = opts.project ? "project" : opts.global ? "global" : undefined;
|
|
4
|
+
const result = runThinkCli({
|
|
5
|
+
positionals: args,
|
|
6
|
+
flagEffort: parentOpts.think,
|
|
7
|
+
scope,
|
|
8
|
+
});
|
|
9
|
+
console.log(result.lines.join("\n"));
|
|
10
|
+
if (!result.ok)
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { runSandboxCli } from "../../sandbox-cli.js";
|
|
2
2
|
import { runPermissionCli } from "../../permission-cli.js";
|
|
3
|
+
import { runThinkCli } from "../../think-cli.js";
|
|
4
|
+
import { runMemoryCli } from "../../memory-cli.js";
|
|
3
5
|
export async function runWriteConfig(input) {
|
|
4
6
|
const lines = [];
|
|
5
7
|
let ok = true;
|
|
@@ -24,6 +26,26 @@ export async function runWriteConfig(input) {
|
|
|
24
26
|
if (!result.ok)
|
|
25
27
|
ok = false;
|
|
26
28
|
}
|
|
29
|
+
if (input.think !== undefined) {
|
|
30
|
+
const result = runThinkCli({
|
|
31
|
+
positionals: [],
|
|
32
|
+
flagEffort: input.think,
|
|
33
|
+
scope: input.scope,
|
|
34
|
+
});
|
|
35
|
+
lines.push(...result.lines);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
ok = false;
|
|
38
|
+
}
|
|
39
|
+
if (input.memory !== undefined) {
|
|
40
|
+
const result = runMemoryCli({
|
|
41
|
+
positionals: [],
|
|
42
|
+
flagMode: input.memory,
|
|
43
|
+
scope: input.scope,
|
|
44
|
+
});
|
|
45
|
+
lines.push(...result.lines);
|
|
46
|
+
if (!result.ok)
|
|
47
|
+
ok = false;
|
|
48
|
+
}
|
|
27
49
|
console.log(lines.join("\n"));
|
|
28
50
|
if (!ok)
|
|
29
51
|
process.exitCode = 1;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
2
|
import { parseNetworkPolicy, parseSandboxMode } from "../sandbox.js";
|
|
3
|
-
import { parsePermissionMode } from "../config.js";
|
|
3
|
+
import { parsePermissionMode, parseThinkingEffort, parseMemoryMode, } from "../config.js";
|
|
4
4
|
import { userError } from "./errors.js";
|
|
5
5
|
/** Reusable --project / --global mutually exclusive scope selector. */
|
|
6
6
|
export function scopeOptions() {
|
|
@@ -35,6 +35,18 @@ export function parsePermissionModeArg(value) {
|
|
|
35
35
|
throw userError(`Invalid --permission value: "${value}"`, "use one of: ask, accept-edits, allow-all");
|
|
36
36
|
return parsed;
|
|
37
37
|
}
|
|
38
|
+
export function parseThinkingEffortArg(value) {
|
|
39
|
+
const parsed = parseThinkingEffort(value);
|
|
40
|
+
if (!parsed)
|
|
41
|
+
throw userError(`Invalid --think value: "${value}"`, "use one of: off, low, medium, high, max");
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
export function parseMemoryModeArg(value) {
|
|
45
|
+
const parsed = parseMemoryMode(value);
|
|
46
|
+
if (!parsed)
|
|
47
|
+
throw userError(`Invalid --memory value: "${value}"`, "use one of: on, off");
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
38
50
|
export function parsePortArg(value) {
|
|
39
51
|
const n = Number.parseInt(value, 10);
|
|
40
52
|
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
package/dist/cli/program.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
2
|
import { setAutoApprove } from "../confirm.js";
|
|
3
3
|
import { CliError, formatCliError, exitCodeFor } from "./errors.js";
|
|
4
|
-
import { parseNetworkPolicyArg, parsePermissionModeArg, parseSandboxModeArg } from "./option-helpers.js";
|
|
4
|
+
import { parseNetworkPolicyArg, parsePermissionModeArg, parseSandboxModeArg, parseThinkingEffortArg, parseMemoryModeArg, } from "./option-helpers.js";
|
|
5
5
|
import { localVersion } from "../updater.js";
|
|
6
6
|
function collectImage(value, previous) {
|
|
7
7
|
return [...previous, value];
|
|
@@ -16,8 +16,10 @@ export function addSessionOptions(cmd) {
|
|
|
16
16
|
.addOption(new Option("--sandbox <mode>", "Isolation mode (off, workspace, strict)").argParser(parseSandboxModeArg))
|
|
17
17
|
.addOption(new Option("--network <policy>", "Network policy (allow, deny)").argParser(parseNetworkPolicyArg))
|
|
18
18
|
.addOption(new Option("--permission <mode>", "Confirmation mode (ask, accept-edits, allow-all)").argParser(parsePermissionModeArg))
|
|
19
|
-
.
|
|
20
|
-
.
|
|
19
|
+
.addOption(new Option("--think <level>", "Thinking intensity (off, low, medium, high, max)").argParser(parseThinkingEffortArg))
|
|
20
|
+
.addOption(new Option("--memory <mode>", "Memory across sessions (on, off; default off)").argParser(parseMemoryModeArg))
|
|
21
|
+
.option("--project", "Write --permission/--sandbox/--think/--memory to this repo's config")
|
|
22
|
+
.option("--global", "Write --permission/--sandbox/--think/--memory to the global config")
|
|
21
23
|
.option("-y, --yes", "This session only: same as --permission allow-all");
|
|
22
24
|
}
|
|
23
25
|
export function buildProgram() {
|
|
@@ -45,14 +47,17 @@ Session:
|
|
|
45
47
|
min-agent exec --resume <id> <msg> Continue a saved exec session
|
|
46
48
|
|
|
47
49
|
Setup & config:
|
|
48
|
-
min-agent setup
|
|
50
|
+
min-agent setup [--type T] Configure providers (interactively, or with flags)
|
|
49
51
|
min-agent init Initialize .min-agent/ in current directory
|
|
50
52
|
min-agent models List available models
|
|
51
53
|
min-agent rules [edit] Show or edit instruction rules
|
|
52
|
-
min-agent memory
|
|
54
|
+
min-agent memory [on|off] Show or set memory (off by default)
|
|
55
|
+
min-agent memory <add|search|delete> Manage saved memories
|
|
53
56
|
min-agent sandbox [off|workspace|strict] Show or set isolation mode
|
|
54
57
|
min-agent sandbox network <allow|deny> Show or set network policy
|
|
55
58
|
min-agent permission [ask|accept-edits|allow-all] Show or set confirmation mode
|
|
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
|
|
56
61
|
|
|
57
62
|
Integrations:
|
|
58
63
|
min-agent mcp <list|add|remove|info|check|enable|disable> Manage MCP servers
|
|
@@ -68,6 +73,21 @@ Confirmation modes (default: ask; independent of isolation):
|
|
|
68
73
|
accept-edits Auto-approve file writes; still prompt for dangerous commands
|
|
69
74
|
allow-all Auto-approve everything
|
|
70
75
|
|
|
76
|
+
Thinking intensity (default: medium):
|
|
77
|
+
off Disable thinking
|
|
78
|
+
low Light reasoning
|
|
79
|
+
medium Standard reasoning
|
|
80
|
+
high Strong reasoning
|
|
81
|
+
max Highest reasoning
|
|
82
|
+
|
|
83
|
+
Ollama context window:
|
|
84
|
+
2k … 256k Loaded window size
|
|
85
|
+
auto Use the model's default
|
|
86
|
+
|
|
87
|
+
Memory (default: off):
|
|
88
|
+
off Do not inject memories or expose memory tools
|
|
89
|
+
on Remember facts across sessions
|
|
90
|
+
|
|
71
91
|
Isolation modes (default: off; independent of confirmation):
|
|
72
92
|
off No sandboxing
|
|
73
93
|
workspace Restrict file writes to the project workspace
|
|
@@ -81,12 +101,15 @@ Rules (loaded as system instructions):
|
|
|
81
101
|
|
|
82
102
|
Examples:
|
|
83
103
|
min-agent setup
|
|
104
|
+
min-agent setup --type ollama
|
|
84
105
|
min-agent
|
|
85
106
|
min-agent "hello"
|
|
86
107
|
min-agent exec "hello"
|
|
87
108
|
min-agent exec --resume <id> "continue"
|
|
88
109
|
min-agent --resume <id>
|
|
89
110
|
min-agent --permission ask
|
|
111
|
+
min-agent --think max
|
|
112
|
+
min-agent --memory on
|
|
90
113
|
min-agent serve --port 8787
|
|
91
114
|
min-agent update
|
|
92
115
|
min-agent rules edit
|
|
@@ -100,6 +123,27 @@ export function applySessionOverrides(opts) {
|
|
|
100
123
|
if (opts.yes)
|
|
101
124
|
setAutoApprove(true);
|
|
102
125
|
}
|
|
126
|
+
export async function applySessionRuntimeOverrides(opts) {
|
|
127
|
+
if (opts.sandbox !== undefined || opts.network !== undefined) {
|
|
128
|
+
const { setSandboxOverride } = await import("../sandbox.js");
|
|
129
|
+
if (opts.sandbox !== undefined)
|
|
130
|
+
setSandboxOverride({ mode: opts.sandbox });
|
|
131
|
+
if (opts.network !== undefined)
|
|
132
|
+
setSandboxOverride({ network: opts.network });
|
|
133
|
+
}
|
|
134
|
+
if (opts.permission !== undefined) {
|
|
135
|
+
const { setPermissionOverride } = await import("../confirm.js");
|
|
136
|
+
setPermissionOverride(opts.permission);
|
|
137
|
+
}
|
|
138
|
+
if (opts.think !== undefined) {
|
|
139
|
+
const { setThinkingOverride } = await import("../thinking.js");
|
|
140
|
+
setThinkingOverride(opts.think);
|
|
141
|
+
}
|
|
142
|
+
if (opts.memory !== undefined) {
|
|
143
|
+
const { setMemoryOverride } = await import("../memory.js");
|
|
144
|
+
setMemoryOverride(opts.memory);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
103
147
|
export function hasSessionIntent(opts, prompt) {
|
|
104
148
|
return (prompt.length > 0 || Boolean(opts.resume) || opts.image.length > 0 || Boolean(opts.model) || Boolean(opts.provider));
|
|
105
149
|
}
|
|
@@ -117,27 +161,26 @@ export function extractUnknownOptionErrors(positionals) {
|
|
|
117
161
|
}
|
|
118
162
|
async function runRootAction(promptTokens, opts) {
|
|
119
163
|
applySessionOverrides(opts);
|
|
120
|
-
const { setSandboxOverride } = await import("../sandbox.js");
|
|
121
|
-
const { setPermissionOverride } = await import("../confirm.js");
|
|
122
164
|
const { rest, errors } = extractUnknownOptionErrors(promptTokens);
|
|
123
165
|
const sessionIntent = hasSessionIntent(opts, rest);
|
|
124
|
-
const wantsConfigWrite = opts.sandbox !== undefined ||
|
|
166
|
+
const wantsConfigWrite = opts.sandbox !== undefined ||
|
|
167
|
+
opts.network !== undefined ||
|
|
168
|
+
opts.permission !== undefined ||
|
|
169
|
+
opts.think !== undefined ||
|
|
170
|
+
opts.memory !== undefined;
|
|
125
171
|
if (wantsConfigWrite && !sessionIntent) {
|
|
126
172
|
const { runWriteConfig } = await import("./commands/write-config.js");
|
|
127
173
|
await runWriteConfig({
|
|
128
174
|
sandbox: opts.sandbox,
|
|
129
175
|
network: opts.network,
|
|
130
176
|
permission: opts.permission,
|
|
177
|
+
think: opts.think,
|
|
178
|
+
memory: opts.memory,
|
|
131
179
|
scope: opts.project ? "project" : opts.global ? "global" : undefined,
|
|
132
180
|
});
|
|
133
181
|
return;
|
|
134
182
|
}
|
|
135
|
-
|
|
136
|
-
setSandboxOverride({ mode: opts.sandbox });
|
|
137
|
-
if (opts.network !== undefined)
|
|
138
|
-
setSandboxOverride({ network: opts.network });
|
|
139
|
-
if (opts.permission !== undefined)
|
|
140
|
-
setPermissionOverride(opts.permission);
|
|
183
|
+
await applySessionRuntimeOverrides(opts);
|
|
141
184
|
if (errors.length > 0) {
|
|
142
185
|
console.error(errors[0]);
|
|
143
186
|
process.exitCode = 1;
|
|
@@ -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
|
+
}
|