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
|
@@ -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
|