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.
Files changed (69) hide show
  1. package/README.md +46 -2
  2. package/dist/agent.js +89 -29
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/ctx.js +7 -0
  5. package/dist/cli/commands/exec.js +3 -0
  6. package/dist/cli/commands/index.js +32 -7
  7. package/dist/cli/commands/memory.js +33 -15
  8. package/dist/cli/commands/setup.js +55 -3
  9. package/dist/cli/commands/shared.js +10 -1
  10. package/dist/cli/commands/think.js +12 -0
  11. package/dist/cli/commands/write-config.js +22 -0
  12. package/dist/cli/option-helpers.js +13 -1
  13. package/dist/cli/program.js +57 -14
  14. package/dist/cli/setup/detect.js +17 -0
  15. package/dist/cli/setup/flags.js +12 -0
  16. package/dist/cli/setup/flow.js +108 -0
  17. package/dist/cli/setup/provider-form.js +102 -0
  18. package/dist/cli/setup/ui.js +534 -0
  19. package/dist/code-mode.js +1 -1
  20. package/dist/config.js +93 -159
  21. package/dist/context-window.js +39 -49
  22. package/dist/ctx-cli.js +30 -0
  23. package/dist/ctx.js +80 -0
  24. package/dist/memory-cli.js +33 -0
  25. package/dist/memory.js +127 -46
  26. package/dist/model-catalog.js +285 -0
  27. package/dist/ollama-model.js +234 -0
  28. package/dist/ollama-openai-bridge.js +383 -0
  29. package/dist/permission-cli.js +1 -4
  30. package/dist/provider.js +4 -1
  31. package/dist/reasoning-stream.js +158 -0
  32. package/dist/sandbox-cli.js +1 -4
  33. package/dist/scope.js +23 -0
  34. package/dist/serve/common.js +22 -1
  35. package/dist/serve/routes-chat.js +21 -1
  36. package/dist/serve/routes-memory.js +31 -2
  37. package/dist/serve/routes-meta.js +69 -6
  38. package/dist/think-cli.js +36 -0
  39. package/dist/thinking-wire.js +239 -0
  40. package/dist/thinking.js +166 -0
  41. package/dist/token-display.js +10 -7
  42. package/dist/tools/todo.js +22 -8
  43. package/dist/tui/App.js +48 -8
  44. package/dist/tui/CtxPicker.js +68 -0
  45. package/dist/tui/InputBar.js +112 -37
  46. package/dist/tui/MessageList.js +53 -22
  47. package/dist/tui/StatusBar.js +7 -3
  48. package/dist/tui/ThinkPicker.js +75 -0
  49. package/dist/tui/bracketed-paste.js +37 -0
  50. package/dist/tui/caret-pos.js +10 -8
  51. package/dist/tui/index.js +13 -1
  52. package/dist/tui/layout.js +17 -0
  53. package/dist/tui/overlay-input.js +12 -0
  54. package/dist/tui/paste-draft.js +173 -0
  55. package/dist/tui/selection.js +8 -2
  56. package/dist/tui/slash-commands.js +24 -1
  57. package/dist/tui/slash-handler.js +88 -18
  58. package/dist/tui/text-width.js +6 -6
  59. package/dist/tui-chat.js +85 -7
  60. package/docs/API.md +69 -6
  61. package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
  62. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  63. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  64. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
  65. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  66. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  67. package/package.json +1 -1
  68. package/skills/self-config/SKILL.md +7 -4
  69. 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
- throw new CliError("Not configured.", { hint: "Run: min-agent setup" });
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) {
@@ -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
- .option("--project", "Write --permission/--sandbox to this repo's config")
20
- .option("--global", "Write --permission/--sandbox to the global config")
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 Configure providers (interactively)
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 <add|search|delete|list> Manage saved memories
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 || opts.network !== undefined || opts.permission !== 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
- if (opts.sandbox !== undefined)
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
+ }