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.
@@ -0,0 +1,501 @@
1
+ # CLI setup 向导改版 — 实现计划
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** `min-agent setup` 成为全局服务商向导与循环管理器;flag 齐了可非交互;TTY 下未配置的交互会话直进同一套首次向导。密钥不回显;Ink 全向导不在 CI 跑。
6
+
7
+ **Architecture:** 纯函数在 `provider-form.ts`(名称、掩码、增删改切)。`fetchModelsLive` 返回实时结果且不把缓存当成功;`fetchModels` 仍给 TUI/`models` 做按 baseURL 分桶的缓存回退。`flow.ts` 注入 `isTty` / `fetchLive` / `wizard`。一次性 Ink 应用在 `ui.tsx`,不抽 `ModelPicker`。根命令 `--model` 不复用;写入默认模型用 `--default-model`。`--yes` 读根 option,setup 里只表示跳过确认。
8
+
9
+ **Tech Stack:** TypeScript, React 18 + Ink 5, commander 14, bun:test
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-23-cli-setup-design.md`
12
+
13
+ 测试命令(`packages/min-agent` 下):`bun test tests/<file>`;类型:`npx tsc --noEmit`;仓库根目录:`bun run typecheck`、`bun run check`。
14
+
15
+ ---
16
+
17
+ ## 文件结构
18
+
19
+ | 文件 | 职责 |
20
+ |------|------|
21
+ | `src/cli/setup/provider-form.ts`(新) | `suggestName`、`validateProviderName`、`maskApiKey`、`filterChoices`、`applyAdd`/`applySwitch`/`applyUpdate`/`applyRemove` |
22
+ | `src/cli/setup/detect.ts`(新) | 800ms 探测 `localhost:11434/api/tags` |
23
+ | `src/cli/setup/flow.ts`(新) | 非交互编排 + TTY 分流;可注入 deps |
24
+ | `src/cli/setup/ui.tsx`(新) | Ink 首次向导 + 管理器 |
25
+ | `src/cli/commands/setup.ts`(改) | flag、调用 flow |
26
+ | `src/cli/commands/index.ts`(改) | 注册 setup options |
27
+ | `src/cli/commands/shared.ts`(改) | TTY 未配置直进向导 |
28
+ | `src/config.ts`(改) | `fetchModelsLive`、缓存分桶;删除 `runSetup`/`ask`/`collectProvider` |
29
+ | `src/cli/program.ts`(改) | help 文案 |
30
+ | `README.md`、`skills/self-config/SKILL.md`、`reference.md`(改) | 用户/agent 文档 |
31
+ | `tests/setup-form.test.ts`(新) | 纯函数 |
32
+ | `tests/setup-detect.test.ts`(新) | 探测 |
33
+ | `tests/models-cache.test.ts`(新) | 分桶缓存 + live |
34
+ | `tests/setup-cli.test.ts`(新) | 非交互 CLI |
35
+ | `tests/setup-flow.test.ts`(新) | 分流、不启动 Ink |
36
+
37
+ `docs/API.md`、`exec`/`serve`/`models` 未配置硬错误、TUI `/help`、权限/沙箱/思考/记忆命令 **不改**。
38
+
39
+ 非交互 CLI 的 flag 校验文案跟现有命令一致(英文 `Invalid --context-window value`)。向导里的窗口/名称错误用 spec 中文。
40
+
41
+ ---
42
+
43
+ ## Task 1: provider-form 纯函数
44
+
45
+ **Files:**
46
+ - Create: `src/cli/setup/provider-form.ts`
47
+ - Test: `tests/setup-form.test.ts`
48
+
49
+ - [ ] **Step 1: 写失败测试**
50
+
51
+ ```ts
52
+ import { expect, test } from "bun:test"
53
+ import type { AppConfig } from "../src/config.js"
54
+ import {
55
+ applyAddProvider,
56
+ applyRemoveProvider,
57
+ applySwitchProvider,
58
+ applyUpdateProvider,
59
+ filterChoices,
60
+ maskApiKey,
61
+ suggestName,
62
+ validateProviderName,
63
+ } from "../src/cli/setup/provider-form.js"
64
+
65
+ test("suggestName uses type defaults and host slug", () => {
66
+ expect(suggestName("openai", "https://api.openai.com/v1", [])).toBe("openai")
67
+ expect(suggestName("ollama", "http://localhost:11434/v1", [])).toBe("ollama")
68
+ expect(suggestName("openai-compatible", "https://www.OpenRouter.ai/api/v1", [])).toBe("openrouter-ai")
69
+ expect(suggestName("openai-compatible", "not-a-url", [])).toBe("provider")
70
+ })
71
+
72
+ test("suggestName suffixes on conflict", () => {
73
+ expect(suggestName("openai", "", ["openai"])).toBe("openai-2")
74
+ expect(suggestName("openai", "", ["openai", "openai-2"])).toBe("openai-3")
75
+ })
76
+
77
+ test("validateProviderName rejects empty slash and colon", () => {
78
+ expect(validateProviderName("")).toBeTruthy()
79
+ expect(validateProviderName(" ")).toBeTruthy()
80
+ expect(validateProviderName("a/b")).toBeTruthy()
81
+ expect(validateProviderName("a:b")).toBeTruthy()
82
+ expect(validateProviderName("groq")).toBeNull()
83
+ })
84
+
85
+ test("maskApiKey never returns the full key", () => {
86
+ expect(maskApiKey("")).toBe("****")
87
+ expect(maskApiKey("abc")).toBe("****")
88
+ expect(maskApiKey("sk-abcdefghijklmnop")).toBe("…mnop")
89
+ expect(maskApiKey("sk-abcdefghijklmnop")).not.toContain("sk-abcdef")
90
+ })
91
+
92
+ test("filterChoices is case-insensitive", () => {
93
+ expect(filterChoices("gpt", ["GPT-4o", "claude", "gpt-4o-mini"])).toEqual(["GPT-4o", "gpt-4o-mini"])
94
+ })
95
+
96
+ const base: AppConfig = {
97
+ providers: [{ name: "a", type: "openai", baseURL: "https://api.openai.com/v1", apiKey: "k1", defaultModel: "m1" }],
98
+ activeProvider: "a",
99
+ futureKey: 1,
100
+ } as AppConfig & { futureKey: number }
101
+
102
+ test("applyAddProvider appends and activates", () => {
103
+ const next = applyAddProvider(base, {
104
+ name: "b",
105
+ type: "ollama",
106
+ baseURL: "http://localhost:11434/v1",
107
+ apiKey: "ollama",
108
+ defaultModel: "llama3",
109
+ }, { overwrite: false })
110
+ expect("error" in next).toBe(false)
111
+ if ("error" in next) return
112
+ expect(next.activeProvider).toBe("b")
113
+ expect(next.providers).toHaveLength(2)
114
+ expect((next as { futureKey?: number }).futureKey).toBe(1)
115
+ })
116
+
117
+ test("applyAddProvider rejects duplicate without overwrite", () => {
118
+ const next = applyAddProvider(base, {
119
+ name: "a",
120
+ type: "openai",
121
+ baseURL: "https://api.openai.com/v1",
122
+ apiKey: "k2",
123
+ defaultModel: "m2",
124
+ }, { overwrite: false })
125
+ expect("error" in next).toBe(true)
126
+ })
127
+
128
+ test("applyAddProvider overwrite replaces same name", () => {
129
+ const next = applyAddProvider(base, {
130
+ name: "a",
131
+ type: "openai",
132
+ baseURL: "https://api.openai.com/v1",
133
+ apiKey: "k2",
134
+ defaultModel: "m2",
135
+ }, { overwrite: true })
136
+ expect("error" in next).toBe(false)
137
+ if ("error" in next) return
138
+ expect(next.providers?.[0]?.apiKey).toBe("k2")
139
+ expect(next.activeProvider).toBe("a")
140
+ })
141
+
142
+ test("applySwitchProvider and missing name", () => {
143
+ expect("error" in applySwitchProvider(base, "nope")).toBe(true)
144
+ const next = applySwitchProvider(
145
+ { ...base, providers: [...(base.providers ?? []), { name: "b", baseURL: "u", apiKey: "k" }] },
146
+ "b",
147
+ )
148
+ expect("error" in next).toBe(false)
149
+ if ("error" in next) return
150
+ expect(next.activeProvider).toBe("b")
151
+ })
152
+
153
+ test("applyUpdateProvider renames and syncs activeProvider", () => {
154
+ const next = applyUpdateProvider(base, "a", {
155
+ name: "renamed",
156
+ type: "openai",
157
+ baseURL: "https://api.openai.com/v1",
158
+ apiKey: "k1",
159
+ defaultModel: "m1",
160
+ })
161
+ expect("error" in next).toBe(false)
162
+ if ("error" in next) return
163
+ expect(next.providers?.[0]?.name).toBe("renamed")
164
+ expect(next.activeProvider).toBe("renamed")
165
+ })
166
+
167
+ test("applyRemoveProvider drops active then last", () => {
168
+ const two: AppConfig = {
169
+ providers: [
170
+ { name: "a", baseURL: "u1", apiKey: "k1" },
171
+ { name: "b", baseURL: "u2", apiKey: "k2" },
172
+ ],
173
+ activeProvider: "a",
174
+ }
175
+ const afterA = applyRemoveProvider(two, "a")
176
+ expect("error" in afterA).toBe(false)
177
+ if ("error" in afterA) return
178
+ expect(afterA.activeProvider).toBe("b")
179
+ const empty = applyRemoveProvider(afterA, "b")
180
+ expect("error" in empty).toBe(false)
181
+ if ("error" in empty) return
182
+ expect(empty.providers ?? []).toEqual([])
183
+ expect(empty.activeProvider).toBeUndefined()
184
+ })
185
+ ```
186
+
187
+ `apply*` 返回 `AppConfig` 或 `{ error: string }`(中文错误,与 spec 一致:找不到、同名、非法名)。`error` 字符串给 CLI 包一层 `CliError`,本文件不 import commander。
188
+
189
+ - [ ] **Step 2: 实现并跑测试**
190
+
191
+ ```bash
192
+ cd packages/min-agent && bun test tests/setup-form.test.ts
193
+ ```
194
+
195
+ - [ ] **Step 3: 提交**
196
+
197
+ ```bash
198
+ git add packages/min-agent/src/cli/setup/provider-form.ts packages/min-agent/tests/setup-form.test.ts
199
+ git commit -m "feat: 抽出 setup 的服务商表单纯函数"
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Task 2: 模型列表 live 结果与按 URL 分桶缓存
205
+
206
+ **Files:**
207
+ - Modify: `src/config.ts`
208
+ - Test: `tests/models-cache.test.ts`
209
+
210
+ 把 `loadModelsCache`/`saveModelsCache` 改为 `Record<string, string[]>`,key = `baseURL.replace(/\/$/, "")`。旧文件若是 `string[]` 或坏 JSON → 当作空。
211
+
212
+ 新增并导出:
213
+
214
+ ```ts
215
+ export interface FetchModelsLiveResult {
216
+ models: string[]
217
+ ok: boolean
218
+ status?: number
219
+ }
220
+
221
+ export async function fetchModelsLive(baseURL: string, apiKey: string): Promise<FetchModelsLiveResult>
222
+ ```
223
+
224
+ - 实时请求现有 `/models`(及无 `/v1` 时的 `/v1/models` 回退)
225
+ - `ok: true` 且 `models.length > 0` 时写入该 URL 的缓存桶
226
+ - HTTP 非 2xx:`ok: false`,带 `status`,`models: []`,**不读缓存**
227
+ - 网络抛错:`ok: false`,无 status,`models: []`,不读缓存
228
+
229
+ `fetchModels(baseURL, apiKey)` 保持给 TUI/`models`:先 live,空则回退**该 URL** 的缓存(不是全局一份列表)。
230
+
231
+ - [ ] **Step 1: 写失败测试**(`globalThis.fetch` 替换,隔离 `MIN_AGENT_CONFIG_DIR`)
232
+
233
+ 覆盖:
234
+
235
+ 1. live 200 写入分桶;换另一个 baseURL 读不到上一份
236
+ 2. 旧 `models-cache.json` 为 `["a"]` 时 `fetchModels` 在 live 空时不把 `a` 当成该 URL 的缓存
237
+ 3. `fetchModelsLive` 在 401 时 `ok: false`、`status: 401`、`models: []`;随后 `fetchModels` 仍可因缓存回退(若之前成功写过)——401 这次不得把缓存写进 live 结果
238
+ 4. live 成功后 `fetchModels` 网络失败则回退该 URL 缓存
239
+
240
+ - [ ] **Step 2: 实现并跑测试**
241
+
242
+ ```bash
243
+ cd packages/min-agent && bun test tests/models-cache.test.ts tests/config.test.ts
244
+ ```
245
+
246
+ - [ ] **Step 3: 提交**
247
+
248
+ ```bash
249
+ git add packages/min-agent/src/config.ts packages/min-agent/tests/models-cache.test.ts
250
+ git commit -m "feat: 模型列表按服务商缓存,并提供实时拉取"
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Task 3: 探测本机 Ollama
256
+
257
+ **Files:**
258
+ - Create: `src/cli/setup/detect.ts`
259
+ - Test: `tests/setup-detect.test.ts`
260
+
261
+ ```ts
262
+ export interface OllamaDetectResult {
263
+ found: boolean
264
+ baseURL?: string // 已 normalizeOllamaBaseURL
265
+ }
266
+
267
+ export async function detectLocalOllama(opts?: {
268
+ fetchImpl?: typeof fetch
269
+ timeoutMs?: number
270
+ }): Promise<OllamaDetectResult>
271
+ ```
272
+
273
+ 默认 `GET http://localhost:11434/api/tags`,`AbortSignal.timeout(800)`。2xx 视为 found,`baseURL` 为 `http://localhost:11434/v1`。超时/网络/非 2xx → `{ found: false }`,不抛。
274
+
275
+ - [ ] **Step 1: 写失败测试**(注入 `fetchImpl`)
276
+
277
+ - [ ] **Step 2: 实现并跑**
278
+
279
+ ```bash
280
+ cd packages/min-agent && bun test tests/setup-detect.test.ts
281
+ ```
282
+
283
+ - [ ] **Step 3: 提交**
284
+
285
+ ```bash
286
+ git add packages/min-agent/src/cli/setup/detect.ts packages/min-agent/tests/setup-detect.test.ts
287
+ git commit -m "feat: setup 可探测本机 Ollama"
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Task 4: 非交互 flow + CLI flag
293
+
294
+ **Files:**
295
+ - Create: `src/cli/setup/flow.ts`
296
+ - Create: `src/cli/setup/flags.ts`(类型 + 互斥/缺项校验,可选,避免 flow 过大)
297
+ - Modify: `src/cli/commands/setup.ts`
298
+ - Modify: `src/cli/commands/index.ts`
299
+ - Test: `tests/setup-cli.test.ts`、`tests/setup-flow.test.ts`
300
+
301
+ `SetupFlags`:
302
+
303
+ ```ts
304
+ export interface SetupFlags {
305
+ type?: "openai-compatible" | "openai" | "ollama"
306
+ url?: string
307
+ apiKey?: string
308
+ name?: string
309
+ defaultModel?: string
310
+ contextWindow?: number
311
+ switch?: string
312
+ remove?: string
313
+ yes?: boolean
314
+ }
315
+ ```
316
+
317
+ `isNonInteractive(flags)`:`--type` / `--switch` / `--remove` 任一出现。
318
+
319
+ 互斥:`type` 与 `switch`/`remove` 同时出现 → `CliError("一次只能执行一种操作。")`。
320
+
321
+ `runSetup(flags, deps)`:
322
+
323
+ - 非交互:不调用 `deps.runWizard`
324
+ - `--remove`:找不到 → 中文 CliError;非 TTY 且无 `yes` → `删除需要确认。` hint `加上 --yes`;否则 `applyRemoveProvider` + `saveConfig`,打印已保存路径
325
+ - `--switch`:`applySwitchProvider` + 保存
326
+ - `--type`:补默认 URL/Key(ollama:`normalizeOllamaBaseURL(url ?? localhost)`,key `ollama`;openai:官方 URL);缺 URL/Key 按 spec 中文 CliError;无 `--default-model` 则 `fetchLive`,ollama 失败用 `llama3`,openai / compatible 空模型 → 码 1「默认模型不能为空。」;`fetchLive` 抛错且非 ollama 回退 → 码 2;名称 `flags.name ?? suggestName(...)`;同名无 `yes` → 码 1 + hint `加上 --yes 以覆盖`;有 yes 则 overwrite;`--context-window` 写入 draft
327
+ - 无非交互 flag:若 `!deps.isTty` → `无法在非交互环境完成配置。` + hint 列出 flag;若 TTY → `deps.runWizard({ mode: hasProvider ? "hub" : "first-run" })`
328
+
329
+ 本任务 `runWizard` 可先做成:**仍调用现有 `runSetup()` from config.ts**,保证交互不被破坏。下一任务再换 Ink。
330
+
331
+ `setup.ts` 从 `rootOpts().yes` 取 `--yes`,`isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY)`。
332
+
333
+ index.ts:setup 子命令声明 `--type/--url/--api-key/--name/--default-model/--context-window/--switch/--remove`。`--context-window` 用 `parsePositiveIntArg("--context-window")`。`--type` 非法值 → `CliError` 列出三个合法值。
334
+
335
+ description:`Configure providers (interactively, or with flags)`。
336
+
337
+ - [ ] **Step 1: 写 `setup-flow.test.ts`**
338
+
339
+ 注入内存 config + mock `fetchLive` / `runWizard`:
340
+
341
+ - 非 TTY 无 flag → 抛错且 `runWizard` 未被调用
342
+ - `--type ollama` 写入 llama3(fetch 空)
343
+ - `--type openai` 无 key → 码 1
344
+ - 同名无 yes 失败;有 yes 覆盖
345
+ - `--switch` / `--remove --yes`
346
+ - `--type` + `--remove` 互斥
347
+ - stdout 不含完整 key(`save` 后打印掩码或只打印路径)
348
+
349
+ - [ ] **Step 2: 写 `setup-cli.test.ts`**
350
+
351
+ 仿 `cli-invoke.test.ts` 的 `runCli` + `MIN_AGENT_CONFIG_DIR`,`stdin: "ignore"`:
352
+
353
+ - `setup --type ollama` 退出 0,config 可 `isConfigured` 语义(有 baseURL+apiKey+model)
354
+ - `setup --type openai --api-key SECRETKEY1234 --default-model gpt-4o`;stderr/stdout 不含 `SECRETKEY1234`
355
+ - 缺 key 退出 1
356
+ - 同名第二次无 `--yes` 退出 1;带 `--yes` 覆盖
357
+ - `--switch` / `--remove --yes`
358
+ - `--type ollama --remove x` 退出 1
359
+ - 无 flag 的 `setup` 退出 1 且不写 `providers`
360
+ - 空 argv 仍 `Not configured`(可放在本文件或依赖现有 `cli-invoke.test.ts`)
361
+
362
+ - [ ] **Step 3: 实现,跑测试**
363
+
364
+ ```bash
365
+ cd packages/min-agent && bun test tests/setup-flow.test.ts tests/setup-cli.test.ts tests/cli-invoke.test.ts
366
+ ```
367
+
368
+ - [ ] **Step 4: 提交**
369
+
370
+ ```bash
371
+ git add packages/min-agent/src/cli/setup/flow.ts packages/min-agent/src/cli/setup/flags.ts packages/min-agent/src/cli/commands/setup.ts packages/min-agent/src/cli/commands/index.ts packages/min-agent/tests/setup-flow.test.ts packages/min-agent/tests/setup-cli.test.ts
372
+ git commit -m "feat: setup 支持非交互 flag 管理服务商"
373
+ ```
374
+
375
+ ---
376
+
377
+ ## Task 5: Ink 首次向导与管理器
378
+
379
+ **Files:**
380
+ - Create: `src/cli/setup/ui.tsx`
381
+ - Modify: `src/cli/setup/flow.ts`(`runWizard` 默认实现改为 `renderSetupWizard`)
382
+ - Modify: `src/config.ts`(本任务结束时删除 `runSetup`/`ask`/`collectProvider` 及 `readline` import)
383
+
384
+ `renderSetupWizard(opts: { mode: "session-gate" | "first-run" | "hub" }): Promise<"saved" | "aborted">`
385
+
386
+ 用 `ink.render()`,结束 `unmount`。主题可复用 `src/tui/theme.ts` 的颜色 token,不要 import TUI overlay 组件。
387
+
388
+ 屏幕按 spec §4–§5:S0–S5、H0 及子动作。Key 输入掩码;修改时空提交保留原 Key。拉模型用 `fetchModelsLive`,空 → S3b。Esc/Ctrl+C → aborted,除管理器里**已经 saveConfig 过的步骤**外不写盘。
389
+
390
+ 管理器「测试连接」展示 `可访问,N 个模型` 或 `无法连接到该服务商。` + status + 提示;剥掉 body 里的 `Bearer` / `sk-`。
391
+
392
+ S1 进入前 `detectLocalOllama()`;found 则标注「已检测到」并默认选 Ollama。
393
+
394
+ 不在 CI 跑 Ink。`filterChoices` 已在 Task 1。
395
+
396
+ - [ ] **Step 1: 实现 ui + 把 flow 的 TTY 分支接到它;删掉 config.ts 旧向导**
397
+
398
+ 确认无残留 `runSetup` 引用(`rg runSetup packages/min-agent/src`)。
399
+
400
+ - [ ] **Step 2: 跑已有测试 + typecheck**
401
+
402
+ ```bash
403
+ cd packages/min-agent && bun test tests/setup-form.test.ts tests/setup-flow.test.ts tests/setup-cli.test.ts tests/models-cache.test.ts
404
+ npx tsc --noEmit
405
+ ```
406
+
407
+ - [ ] **Step 3: 提交**
408
+
409
+ ```bash
410
+ git add packages/min-agent/src/cli/setup packages/min-agent/src/config.ts packages/min-agent/src/cli/commands/setup.ts
411
+ git commit -m "feat: setup 用 Ink 向导配置服务商"
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Task 6: 未配置会话直进向导
417
+
418
+ **Files:**
419
+ - Modify: `src/cli/commands/shared.ts`
420
+
421
+ `startTuiSession`:`!isConfigured()` 时若 `process.stdin.isTTY && process.stdout.isTTY`,调用 `runSetup({} as flags, deps)` 且 wizard mode 为 `session-gate`(S0)。成功且 `isConfigured()` 后继续 `runTui`,**原样传递** `model/provider/resume/images/positionals`。向导 aborted 或仍未配置 → `CliError("Not configured.", { hint: "Run: min-agent setup" })` 码 1。
422
+
423
+ 非 TTY 保持现有硬错误。`exec`/`serve`/`models` 不调用 `startTuiSession` 的此分支(它们自己检查 `isConfigured`)。
424
+
425
+ - [ ] **Step 1: `setup-flow.test.ts` 增测**
426
+
427
+ 导出 `shouldOfferSetupWizard(isConfigured, isTty)` 或测 `startTuiSession` 的纯前置:非 TTY 不 offer。不要在 CI 真 render Ink。
428
+
429
+ 现有 `cli-invoke.test.ts` 空 argv 仍匹配 `/Not configured/`。
430
+
431
+ - [ ] **Step 2: 实现并跑**
432
+
433
+ ```bash
434
+ cd packages/min-agent && bun test tests/cli-invoke.test.ts tests/setup-flow.test.ts
435
+ ```
436
+
437
+ - [ ] **Step 3: 提交**
438
+
439
+ ```bash
440
+ git add packages/min-agent/src/cli/commands/shared.ts packages/min-agent/tests/setup-flow.test.ts
441
+ git commit -m "feat: 未配置的交互会话在终端里直接进入 setup"
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Task 7: README、help、self-config
447
+
448
+ **Files:**
449
+ - Modify: `README.md`(中英 Configuration / 配置:保留引导句;各加三例)
450
+
451
+ ```bash
452
+ min-agent setup --type ollama
453
+ min-agent setup --type openai-compatible --url https://api.example.com/v1 --api-key "$KEY" --name groq --default-model llama-3.1-70b
454
+ min-agent setup --switch ollama
455
+ ```
456
+
457
+ 不写模块路径、Ink、缓存文件名。
458
+
459
+ - Modify: `src/cli/program.ts` after-help:`min-agent setup` 一行改为带常用 flag;Examples 加一条非交互
460
+ - Modify: `skills/self-config/SKILL.md` Providers:CLI `min-agent setup` 及 `--type/--url/--api-key/--name/--default-model/--switch/--remove`;`--yes` 在 setup 中跳过覆盖/删除确认,不写 permission
461
+ - Modify: `skills/self-config/reference.md`:字段示例不变;补一句交互与非交互添加都走 setup,密钥只写全局
462
+
463
+ `docs/API.md`、`/help` 不改。
464
+
465
+ - [ ] **Step 1: 改文案**
466
+
467
+ - [ ] **Step 2: 提交**
468
+
469
+ ```bash
470
+ git add packages/min-agent/README.md packages/min-agent/src/cli/program.ts packages/min-agent/skills/self-config/SKILL.md packages/min-agent/skills/self-config/reference.md
471
+ git commit -m "docs: 说明 setup 的引导与非交互用法"
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Task 8: 回归
477
+
478
+ - [ ] **Step 1:**
479
+
480
+ ```bash
481
+ cd packages/min-agent && bun test tests/setup-form.test.ts tests/setup-detect.test.ts tests/models-cache.test.ts tests/setup-flow.test.ts tests/setup-cli.test.ts tests/cli-invoke.test.ts tests/config.test.ts
482
+ # 仓库根目录
483
+ bun run typecheck
484
+ bun run check
485
+ ```
486
+
487
+ - [ ] **Step 2: 若格式问题则 `bun run format` 后再 check,不要 `--no-verify`**
488
+
489
+ ---
490
+
491
+ ## 手工验收
492
+
493
+ 在 `packages/min-agent` 下(隔离配置目录更佳:`MIN_AGENT_CONFIG_DIR=/tmp/min-agent-setup-demo`):
494
+
495
+ 1. `bun run src/cli.ts setup --type ollama`(无 TTY 也行)写入后 `cat $MIN_AGENT_CONFIG_DIR/config.json` 有 ollama provider
496
+ 2. TTY:`bun run src/cli.ts setup` 出现可方向键选择的类型列表;Ollama 若本机在跑应标注已检测到
497
+ 3. 添加兼容接口,模型列表可输入过滤;Key 不在屏幕明文回显
498
+ 4. 再次 setup 进入管理器:切换、修改(回车保留 Key)、测连、删除确认、Esc 退出码 0
499
+ 5. 清空 config 后 TTY 跑 `bun run src/cli.ts` → S0「开始配置」;配完进入 TUI。管道:`bun run src/cli.ts </dev/null` 仍 `Not configured`
500
+ 6. `bun run src/cli.ts exec ping` 未配置仍硬错误,不进向导
501
+ 7. `bun run src/cli.ts setup --type openai --api-key sk-testdemokey9999 --default-model gpt-4o` 的终端输出不含完整 key