min-agent 0.2.1 → 0.4.0
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 +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# 根命令默认 TUI + 合并 chat/code — 实现计划
|
|
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` 无子命令进入交互 TUI(可带首条 prompt);`min-agent exec` 非交互跑完退出;运行时与 HTTP 取消 chat/code 双模式,统一为原 code 能力。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 分发逻辑抽到 `src/cli-invoke.ts`(`cli.ts` 会在 import 时跑 `main`,测试不能碰它)。agent 去掉 `mode`,工具只留 `createTools()`。TUI 始终 coding prompt;HTTP `/v1/chat` 与 `/v1/code` 走同一 handler。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, bun:test
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-08-20-unified-tui-default-design.md`
|
|
12
|
+
|
|
13
|
+
测试命令(`packages/min-agent` 下):`bun test tests/<file>`;类型:`npx tsc --noEmit`;仓库根目录:`bun run typecheck`。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 文件结构
|
|
18
|
+
|
|
19
|
+
| 文件 | 职责 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `src/cli-invoke.ts`(新) | `parseFlags`、`resolveCliInvocation`、`resolveExecPrompt`、保留子命令表 |
|
|
22
|
+
| `tests/cli-invoke.test.ts`(新) | 分发与 exec prompt 纯函数测试 |
|
|
23
|
+
| `src/cli.ts`(改) | 用 invocation 分发;空参数进 TUI;`exec`;Usage |
|
|
24
|
+
| `src/tools/index.ts`(改) | `createChatTools`/`createCodeTools` → `createTools()` |
|
|
25
|
+
| `src/tools/task.ts`(改) | 改调 `createTools()` |
|
|
26
|
+
| `src/agent.ts`(改) | 去 mode;`runOnce` 用 coding prompt;返回 `{ hasError }` |
|
|
27
|
+
| `src/tui-chat.ts`(改) | 去 `mode`,加 `initialPrompt` |
|
|
28
|
+
| `src/tui/agent-runner.ts`(改) | 去 mode 分支,始终 `runOnceWithSystem` |
|
|
29
|
+
| `src/tui/slash-handler.ts`(改) | `/sessions` 恢复示例改为根命令 `--resume` |
|
|
30
|
+
| `src/serve.ts`(改) | chat/code/paste 统一项目感知,去掉 `mode` 字段 |
|
|
31
|
+
| `tests/serve.test.ts`(改) | 断言 `project`、无 `mode` |
|
|
32
|
+
| `README.md`(改) | Quick Start / Commands |
|
|
33
|
+
| `docs/API.md`(改) | chat/code/paste 文档 |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Task 1: cli-invoke.ts — 分发纯函数
|
|
38
|
+
|
|
39
|
+
**Files:**
|
|
40
|
+
- Create: `src/cli-invoke.ts`
|
|
41
|
+
- Test: `tests/cli-invoke.test.ts`
|
|
42
|
+
- Modify: `src/cli.ts`(本 task 只把 `parseFlags`/`ParsedFlags` 迁走并改 import,**不改 main 行为**)
|
|
43
|
+
|
|
44
|
+
- [ ] **Step 1: 写失败测试**
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { expect, test } from "bun:test"
|
|
48
|
+
import { parseFlags, resolveCliInvocation, resolveExecPrompt } from "../src/cli-invoke.js"
|
|
49
|
+
|
|
50
|
+
test("empty argv is tui with no prompt", () => {
|
|
51
|
+
const inv = resolveCliInvocation([])
|
|
52
|
+
expect(inv).toEqual({ kind: "tui", flags: { images: [], positionals: [] } })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("first token --help is help", () => {
|
|
56
|
+
expect(resolveCliInvocation(["--help"])).toEqual({ kind: "help" })
|
|
57
|
+
expect(resolveCliInvocation(["-h"])).toEqual({ kind: "help" })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("first token --version is version", () => {
|
|
61
|
+
expect(resolveCliInvocation(["--version"])).toEqual({ kind: "version" })
|
|
62
|
+
expect(resolveCliInvocation(["-v"])).toEqual({ kind: "version" })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("reserved command strips the command name from positionals", () => {
|
|
66
|
+
const inv = resolveCliInvocation(["setup"])
|
|
67
|
+
expect(inv.kind).toBe("command")
|
|
68
|
+
if (inv.kind !== "command") return
|
|
69
|
+
expect(inv.command).toBe("setup")
|
|
70
|
+
expect(inv.flags.positionals).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test("exec with message is command", () => {
|
|
74
|
+
const inv = resolveCliInvocation(["exec", "hello", "world"])
|
|
75
|
+
expect(inv).toMatchObject({ kind: "command", command: "exec" })
|
|
76
|
+
if (inv.kind !== "command") return
|
|
77
|
+
expect(inv.flags.positionals).toEqual(["hello", "world"])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("chat and code are reserved aliases", () => {
|
|
81
|
+
expect(resolveCliInvocation(["chat"]).kind).toBe("command")
|
|
82
|
+
expect(resolveCliInvocation(["code", "fix", "it"])).toMatchObject({
|
|
83
|
+
kind: "command",
|
|
84
|
+
command: "code",
|
|
85
|
+
flags: { positionals: ["fix", "it"], images: [] },
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("bare prompt is tui", () => {
|
|
90
|
+
expect(resolveCliInvocation(["修", "lint"])).toMatchObject({
|
|
91
|
+
kind: "tui",
|
|
92
|
+
flags: { positionals: ["修", "lint"], images: [] },
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("flags before a reserved command still dispatch the command", () => {
|
|
97
|
+
const inv = resolveCliInvocation(["--model", "gpt-4o", "exec", "hi"])
|
|
98
|
+
expect(inv.kind).toBe("command")
|
|
99
|
+
if (inv.kind !== "command") return
|
|
100
|
+
expect(inv.command).toBe("exec")
|
|
101
|
+
expect(inv.flags.model).toBe("gpt-4o")
|
|
102
|
+
expect(inv.flags.positionals).toEqual(["hi"])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("flags without a command are tui", () => {
|
|
106
|
+
const inv = resolveCliInvocation(["-m", "gpt-4o", "--resume", "abc"])
|
|
107
|
+
expect(inv.kind).toBe("tui")
|
|
108
|
+
if (inv.kind !== "tui") return
|
|
109
|
+
expect(inv.flags.model).toBe("gpt-4o")
|
|
110
|
+
expect(inv.flags.resume).toBe("abc")
|
|
111
|
+
expect(inv.flags.positionals).toEqual([])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("flag then prompt is tui", () => {
|
|
115
|
+
const inv = resolveCliInvocation(["-i", "a.png", "look"])
|
|
116
|
+
expect(inv.kind).toBe("tui")
|
|
117
|
+
if (inv.kind !== "tui") return
|
|
118
|
+
expect(inv.flags.images).toEqual(["a.png"])
|
|
119
|
+
expect(inv.flags.positionals).toEqual(["look"])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("typo of a subcommand is treated as a prompt", () => {
|
|
123
|
+
const inv = resolveCliInvocation(["setpu"])
|
|
124
|
+
expect(inv.kind).toBe("tui")
|
|
125
|
+
if (inv.kind !== "tui") return
|
|
126
|
+
expect(inv.flags.positionals).toEqual(["setpu"])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("resolveExecPrompt uses joined positionals on a tty", () => {
|
|
130
|
+
expect(resolveExecPrompt(["hello", "world"], "", true)).toEqual({ ok: true, prompt: "hello world" })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("resolveExecPrompt reads stdin when prompt is dash", () => {
|
|
134
|
+
expect(resolveExecPrompt(["-"], "from stdin", false)).toEqual({ ok: true, prompt: "from stdin" })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test("resolveExecPrompt reads stdin when no args and not a tty", () => {
|
|
138
|
+
expect(resolveExecPrompt([], "piped", false)).toEqual({ ok: true, prompt: "piped" })
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test("resolveExecPrompt errors when no args on a tty", () => {
|
|
142
|
+
expect(resolveExecPrompt([], "", true).ok).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("resolveExecPrompt appends piped stdin to a prompt", () => {
|
|
146
|
+
expect(resolveExecPrompt(["do this"], "file contents", false)).toEqual({
|
|
147
|
+
ok: true,
|
|
148
|
+
prompt: "do this\n\nfile contents",
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("resolveExecPrompt dash with empty stdin is an error", () => {
|
|
153
|
+
expect(resolveExecPrompt(["-"], "", true).ok).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- [ ] **Step 2: 运行确认失败**
|
|
158
|
+
|
|
159
|
+
Run: `bun test tests/cli-invoke.test.ts`
|
|
160
|
+
Expected: FAIL(模块不存在)
|
|
161
|
+
|
|
162
|
+
- [ ] **Step 3: 实现 `src/cli-invoke.ts`**
|
|
163
|
+
|
|
164
|
+
把 `cli.ts` 里的 `ParsedFlags` 与 `parseFlags` **原样搬过来并 export**(行为必须与现在一致,含 `--yes` 调用 `setAutoApprove`)。再加:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
export const RESERVED_COMMANDS = [
|
|
168
|
+
"setup", "models", "serve", "mcp", "history", "memory",
|
|
169
|
+
"skills", "update", "init", "rules", "exec", "chat", "code",
|
|
170
|
+
] as const
|
|
171
|
+
|
|
172
|
+
export type ReservedCommand = (typeof RESERVED_COMMANDS)[number]
|
|
173
|
+
|
|
174
|
+
export type CliInvocation =
|
|
175
|
+
| { kind: "help" }
|
|
176
|
+
| { kind: "version" }
|
|
177
|
+
| { kind: "tui"; flags: ParsedFlags }
|
|
178
|
+
| { kind: "command"; command: ReservedCommand; flags: ParsedFlags }
|
|
179
|
+
|
|
180
|
+
export function resolveCliInvocation(argv: string[]): CliInvocation {
|
|
181
|
+
if (argv[0] === "--help" || argv[0] === "-h") return { kind: "help" }
|
|
182
|
+
if (argv[0] === "--version" || argv[0] === "-v") return { kind: "version" }
|
|
183
|
+
const flags = parseFlags(argv)
|
|
184
|
+
const command = flags.positionals[0]
|
|
185
|
+
if (command && (RESERVED_COMMANDS as readonly string[]).includes(command)) {
|
|
186
|
+
return {
|
|
187
|
+
kind: "command",
|
|
188
|
+
command: command as ReservedCommand,
|
|
189
|
+
flags: { ...flags, positionals: flags.positionals.slice(1) },
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { kind: "tui", flags }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveExecPrompt(
|
|
196
|
+
positionals: string[],
|
|
197
|
+
stdinText: string,
|
|
198
|
+
stdinIsTty: boolean,
|
|
199
|
+
): { ok: true; prompt: string } | { ok: false; error: string } {
|
|
200
|
+
const joined = positionals.join(" ")
|
|
201
|
+
const piped = stdinText.length > 0
|
|
202
|
+
if (joined === "-") {
|
|
203
|
+
const prompt = stdinText
|
|
204
|
+
if (!prompt.trim()) return { ok: false, error: "Usage: min-agent exec <message>" }
|
|
205
|
+
return { ok: true, prompt }
|
|
206
|
+
}
|
|
207
|
+
if (!joined) {
|
|
208
|
+
if (stdinIsTty || !piped) return { ok: false, error: "Usage: min-agent exec <message>" }
|
|
209
|
+
return { ok: true, prompt: stdinText }
|
|
210
|
+
}
|
|
211
|
+
if (piped && !stdinIsTty) return { ok: true, prompt: `${joined}\n\n${stdinText}` }
|
|
212
|
+
return { ok: true, prompt: joined }
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`cli.ts`:删除本地 `ParsedFlags`/`parseFlags`,改为 `import { parseFlags, type ParsedFlags } from "./cli-invoke.js"`。`main` 仍按 `args[0]` switch,行为不变。
|
|
217
|
+
|
|
218
|
+
- [ ] **Step 4: 运行确认通过**
|
|
219
|
+
|
|
220
|
+
Run: `bun test tests/cli-invoke.test.ts`
|
|
221
|
+
Expected: PASS
|
|
222
|
+
|
|
223
|
+
- [ ] **Step 5: 提交**
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
git add src/cli-invoke.ts tests/cli-invoke.test.ts src/cli.ts
|
|
227
|
+
git commit -m "feat: 抽出 CLI 分发纯函数"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Task 2: 工具集合并
|
|
233
|
+
|
|
234
|
+
**Files:**
|
|
235
|
+
- Modify: `src/tools/index.ts`
|
|
236
|
+
- Modify: `src/tools/task.ts`
|
|
237
|
+
|
|
238
|
+
- [ ] **Step 1: 无单独测试文件**(现有测试不直接 import 这两套工厂)。实现后 `npx tsc --noEmit`。
|
|
239
|
+
|
|
240
|
+
- [ ] **Step 2: 实现**
|
|
241
|
+
|
|
242
|
+
`src/tools/index.ts`:删除 `createChatTools`。把 `createCodeTools` 改名为 `createTools`(函数体保持原 code 工具集,含 `EXA_API_KEY` 时的 `codesearch`)。
|
|
243
|
+
|
|
244
|
+
`src/tools/task.ts`:`createCodeTools` → `createTools`。
|
|
245
|
+
|
|
246
|
+
`src/agent.ts` 本 task 先改 import 与 `buildTools` 里工厂调用为 `createTools()`,**暂时仍留 `mode` 参数**(下一 task 删),避免一次 diff 过大。若 typecheck 因 `createChatTools` 引用失败,本 task 就把 `buildTools` 改成只调 `createTools()`,`mode === "code"` 的 `task` 工具改为**始终**挂上 `createTaskTool`。
|
|
247
|
+
|
|
248
|
+
推荐本 task 一次做完 `buildTools` 去 mode 中与工具相关的部分:
|
|
249
|
+
|
|
250
|
+
- `builtinTools = createTools()`
|
|
251
|
+
- 始终 `allTools["task"] = createTaskTool(...)`
|
|
252
|
+
- 删掉 `if (mode === "code")` 包 task 的分支
|
|
253
|
+
- `mode` 参数先留着给 `runOnceCore` 日志,下一 task 再删
|
|
254
|
+
|
|
255
|
+
- [ ] **Step 3: 类型检查**
|
|
256
|
+
|
|
257
|
+
Run: `npx tsc --noEmit`
|
|
258
|
+
Expected: PASS
|
|
259
|
+
|
|
260
|
+
- [ ] **Step 4: 提交**
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
git add src/tools/index.ts src/tools/task.ts src/agent.ts
|
|
264
|
+
git commit -m "feat: 合并 chat/code 工具集为 createTools"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Task 3: agent 去掉 mode,runOnce 走 coding prompt
|
|
270
|
+
|
|
271
|
+
**Files:**
|
|
272
|
+
- Modify: `src/agent.ts`
|
|
273
|
+
|
|
274
|
+
- [ ] **Step 1: 改 `runOnceCore`**
|
|
275
|
+
|
|
276
|
+
- 去掉参数 `mode: "chat" | "code"`
|
|
277
|
+
- 日志改为 `run start messages=${...} model=${...}`
|
|
278
|
+
- `buildTools` 去掉 `mode` 参数(签名同步)
|
|
279
|
+
- 所有 `return` 改为 `return { hasError }`(成功路径 `hasError` 为当时的值;abort 路径按现有 `hasError`/`aborted` 逻辑,**返回值只暴露 hasError**:abort 不算 hasError,与 `onRunFinish` 一致)
|
|
280
|
+
- `runOnce` / `runOnceWithSystem` 的返回类型改为 `Promise<{ hasError: boolean }>`
|
|
281
|
+
|
|
282
|
+
`runOnce` 改为:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
export async function runOnce(...) {
|
|
286
|
+
const { loadInstructions } = await import("./instructions.js") // 若 instructions 已传入则不必
|
|
287
|
+
const project = scanProject()
|
|
288
|
+
const systemPrompt = buildCodeSystemPrompt(project, instructions)
|
|
289
|
+
return runOnceCore(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options)
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`instructions` 已是参数,直接 `buildCodeSystemPrompt(scanProject(), instructions)`。顶部 import `scanProject, buildCodeSystemPrompt` from `./code-mode.js`。
|
|
294
|
+
|
|
295
|
+
删除旧的 `buildSystemPrompt(instructions)` 调用路径。若 `buildSystemPrompt` 仅被 `runOnce` 使用,保留函数本身(可能仍被别处用);grep 后无引用则删除。
|
|
296
|
+
|
|
297
|
+
`runAgent`:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const { hasError } = await runOnce(messages, instructions, modelId, undefined, undefined, tracker, { providerName })
|
|
301
|
+
return hasError
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
返回类型 `Promise<boolean>`(true = 失败)。不要给 `runOnce` 传 callbacks(否则会关掉 TTY 输出)。
|
|
305
|
+
|
|
306
|
+
- [ ] **Step 2: 类型检查 + agent 测试**
|
|
307
|
+
|
|
308
|
+
Run: `npx tsc --noEmit` 且 `bun test tests/agent-loop.test.ts`
|
|
309
|
+
Expected: PASS(测试继续用 `runOnceWithSystem`)
|
|
310
|
+
|
|
311
|
+
- [ ] **Step 3: 提交**
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
git add src/agent.ts
|
|
315
|
+
git commit -m "feat: agent 统一 coding prompt 并返回 hasError"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Task 4: TUI 去掉 mode,支持首条 prompt
|
|
321
|
+
|
|
322
|
+
**Files:**
|
|
323
|
+
- Modify: `src/tui-chat.ts`
|
|
324
|
+
- Modify: `src/tui/agent-runner.ts`
|
|
325
|
+
- Modify: `src/tui/slash-handler.ts`
|
|
326
|
+
- Modify: `src/cli.ts`(仅让现有 `chat`/`code` 分支能编译:暂时仍传 `mode` 的话本 task 必须改掉调用处)
|
|
327
|
+
|
|
328
|
+
- [ ] **Step 1: `AgentRunnerDeps`**
|
|
329
|
+
|
|
330
|
+
删除 `mode`。`codeSystemPrompt` 改为必填 `string`。`run()` 只调用 `runOnceWithSystem`。
|
|
331
|
+
|
|
332
|
+
- [ ] **Step 2: `TuiOptions`**
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
export interface TuiOptions {
|
|
336
|
+
modelId?: string
|
|
337
|
+
providerName?: string
|
|
338
|
+
resumeSessionId?: string
|
|
339
|
+
images?: string[]
|
|
340
|
+
initialPrompt?: string
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`runTui`:始终 `scanProject()` + `buildCodeSystemPrompt`。`createAgentRunner({ ..., codeSystemPrompt })` 不再传 `mode`。
|
|
345
|
+
|
|
346
|
+
启动后、`tui.start()` 之后、现有 images 注入处,合并为首条 user 消息:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
const prompt = opts.initialPrompt?.trim() ?? ""
|
|
350
|
+
const notify = ... // 保持现有三种提示
|
|
351
|
+
if (images?.length) {
|
|
352
|
+
const parts = [{ type: "text", text: prompt }, ...loadImageParts(images, notify)]
|
|
353
|
+
if (parts.length > 1 || prompt) {
|
|
354
|
+
messages.push({ role: "user", content: prompt ? parts : parts })
|
|
355
|
+
tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: prompt ? prompt : `[📎 图片] ${images.join(", ")}`, timestamp: Date.now() })
|
|
356
|
+
await runner.run()
|
|
357
|
+
}
|
|
358
|
+
} else if (prompt) {
|
|
359
|
+
tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: prompt, timestamp: Date.now() })
|
|
360
|
+
messages.push({ role: "user", content: prompt })
|
|
361
|
+
await runner.run()
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
细节(必须遵守 spec):
|
|
366
|
+
|
|
367
|
+
- **仅 prompt**:文本 user 消息 + `run()`
|
|
368
|
+
- **仅图片**:现有逻辑(空文本 + 图片 parts,length>1 才 run)
|
|
369
|
+
- **prompt + 图片**:同一条 user,`text` 为 prompt,加上图片 parts
|
|
370
|
+
- **resume + prompt**:在已 load 的 messages 后追加,再 run
|
|
371
|
+
- 皆无:不 run
|
|
372
|
+
|
|
373
|
+
TUI 展示:有 prompt 时 user 气泡用 prompt 文本;仅图片时保持 `[📎 图片] ...`。两者都有时气泡用 prompt(图片仍在消息 content 里)。
|
|
374
|
+
|
|
375
|
+
- [ ] **Step 3: slash-handler**
|
|
376
|
+
|
|
377
|
+
`/sessions` 里 `min-agent chat --resume <id>` 改为 `min-agent --resume <id>`。
|
|
378
|
+
|
|
379
|
+
- [ ] **Step 4: cli.ts 过渡**
|
|
380
|
+
|
|
381
|
+
`chat`:无 message 时 `runTui({ ..., initialPrompt: undefined })`;**有 message 时暂时仍 `runAgent`**(下一 task 改为 TUI)。`code`:`runTui({ ... })` 去掉 `mode: "code"`。此步只要 typecheck 过。
|
|
382
|
+
|
|
383
|
+
- [ ] **Step 5: 类型检查**
|
|
384
|
+
|
|
385
|
+
Run: `npx tsc --noEmit`
|
|
386
|
+
Expected: PASS
|
|
387
|
+
|
|
388
|
+
- [ ] **Step 6: 提交**
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
git add src/tui-chat.ts src/tui/agent-runner.ts src/tui/slash-handler.ts src/cli.ts
|
|
392
|
+
git commit -m "feat: TUI 统一 coding 会话并支持首条 prompt"
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Task 5: CLI 默认 TUI + exec
|
|
398
|
+
|
|
399
|
+
**Files:**
|
|
400
|
+
- Modify: `src/cli.ts`
|
|
401
|
+
|
|
402
|
+
- [ ] **Step 1: 改 `main` 入口**
|
|
403
|
+
|
|
404
|
+
删除「`args.length === 0` 打印 Usage」。保留第一个 token 为 `-h`/`--help`、`-v`/`--version` 的分支,**或**直接:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const invocation = resolveCliInvocation(args)
|
|
408
|
+
switch (invocation.kind) {
|
|
409
|
+
case "help":
|
|
410
|
+
printUsage()
|
|
411
|
+
process.exit(0)
|
|
412
|
+
case "version":
|
|
413
|
+
// 现有读 package.json 逻辑
|
|
414
|
+
process.exit(0)
|
|
415
|
+
case "tui":
|
|
416
|
+
await startTuiSession(invocation.flags)
|
|
417
|
+
break
|
|
418
|
+
case "command":
|
|
419
|
+
// 原 switch,但 command 来自 invocation.command
|
|
420
|
+
// flags 用 invocation.flags,不要再 parseFlags(args.slice(1))
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
抽出 `startTuiSession(flags: ParsedFlags)`:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
async function startTuiSession(flags: ParsedFlags) {
|
|
428
|
+
if (!isConfigured()) {
|
|
429
|
+
console.error("Not configured. Run: min-agent setup")
|
|
430
|
+
process.exit(1)
|
|
431
|
+
}
|
|
432
|
+
const { runTui } = await import("./tui-chat.js")
|
|
433
|
+
const prompt = flags.positionals.join(" ").trim()
|
|
434
|
+
await runTui({
|
|
435
|
+
modelId: flags.model,
|
|
436
|
+
providerName: flags.provider,
|
|
437
|
+
resumeSessionId: flags.resume,
|
|
438
|
+
images: flags.images,
|
|
439
|
+
initialPrompt: prompt || undefined,
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`chat` / `code` 两个 case 都只调用 `startTuiSession(invocation.flags)`(有无位置参数都进 TUI,**不再** `runAgent`)。
|
|
445
|
+
|
|
446
|
+
`exec` case:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
case "exec": {
|
|
450
|
+
if (!isConfigured()) {
|
|
451
|
+
console.error("Not configured. Run: min-agent setup")
|
|
452
|
+
process.exit(1)
|
|
453
|
+
}
|
|
454
|
+
const flags = invocation.flags
|
|
455
|
+
const stdinIsTty = Boolean(process.stdin.isTTY)
|
|
456
|
+
let stdinText = ""
|
|
457
|
+
const joined = flags.positionals.join(" ")
|
|
458
|
+
const shouldReadStdin = joined === "-" || !stdinIsTty
|
|
459
|
+
if (shouldReadStdin) {
|
|
460
|
+
try {
|
|
461
|
+
stdinText = await Bun.readableStreamToText(Bun.stdin.stream())
|
|
462
|
+
} catch {
|
|
463
|
+
console.error("Failed to read stdin")
|
|
464
|
+
process.exit(1)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const resolved = resolveExecPrompt(flags.positionals, stdinText, stdinIsTty)
|
|
468
|
+
if (!resolved.ok) {
|
|
469
|
+
console.error(resolved.error)
|
|
470
|
+
process.exit(1)
|
|
471
|
+
}
|
|
472
|
+
const failed = await runAgent(
|
|
473
|
+
resolved.prompt,
|
|
474
|
+
flags.model,
|
|
475
|
+
flags.images.length > 0 ? flags.images : undefined,
|
|
476
|
+
flags.provider,
|
|
477
|
+
)
|
|
478
|
+
if (failed) process.exit(1)
|
|
479
|
+
break
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
注意:无位置参数且是 TTY 时 **不要**读 stdin(会挂起等 EOF)。`shouldReadStdin = joined === "-" || !stdinIsTty` 已覆盖:TTY 且无参数 → 不读,`resolveExecPrompt` 报 Usage。
|
|
484
|
+
|
|
485
|
+
原 `default: Unknown command` 删除(非保留词已在 `kind: "tui"`)。
|
|
486
|
+
|
|
487
|
+
各子命令里原来的 `parseFlags(args.slice(1))` 改为使用 `invocation.flags`(positionals 已去掉命令名)。`mcp`/`history` 等仍看 `args[1]` 也可以,但 flag 覆盖必须来自 `invocation.flags`。为减少行为漂移:子命令内部若仍用 `args.slice(1)` 的局部 parse,**允许保留**(与现在一致),只要 `chat`/`code`/`exec`/根 TUI 走 `invocation`。`models`/`serve` 继续 `parseFlags(args.slice(1))` 即可。
|
|
488
|
+
|
|
489
|
+
- [ ] **Step 2: 改 `printUsage`**
|
|
490
|
+
|
|
491
|
+
Usage 开头改为(风格与现有英文 Usage 一致,不出现 chat/code 两种模式):
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
Usage:
|
|
495
|
+
min-agent Start interactive session
|
|
496
|
+
min-agent <message> Start interactive session and send message
|
|
497
|
+
min-agent exec <message> Run one message non-interactively
|
|
498
|
+
min-agent --resume <id> Resume a previous session
|
|
499
|
+
min-agent chat | code Same as interactive session (compat)
|
|
500
|
+
...其余子命令保持...
|
|
501
|
+
Examples:
|
|
502
|
+
min-agent setup
|
|
503
|
+
min-agent
|
|
504
|
+
min-agent "hello"
|
|
505
|
+
min-agent exec "hello"
|
|
506
|
+
min-agent --resume <id>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
- [ ] **Step 3: 类型检查**
|
|
510
|
+
|
|
511
|
+
Run: `npx tsc --noEmit`
|
|
512
|
+
Expected: PASS
|
|
513
|
+
|
|
514
|
+
- [ ] **Step 4: 提交**
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
git add src/cli.ts
|
|
518
|
+
git commit -m "feat: 默认进入交互会话并新增 exec"
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Task 6: HTTP API 统一
|
|
524
|
+
|
|
525
|
+
**Files:**
|
|
526
|
+
- Modify: `src/serve.ts`
|
|
527
|
+
- Modify: `tests/serve.test.ts`
|
|
528
|
+
- Modify: `docs/API.md`
|
|
529
|
+
|
|
530
|
+
- [ ] **Step 1: 改测试**
|
|
531
|
+
|
|
532
|
+
`F7: POST /v1/paste 支持 code 模式` 改为例如「paste 响应带 project、不带 mode」:
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
test("F7: POST /v1/paste 带 project 且无 mode", async () => {
|
|
536
|
+
const { status, text } = await postJson("/v1/paste", {
|
|
537
|
+
image_base64: PNG_B64,
|
|
538
|
+
message: "分析",
|
|
539
|
+
stream: false,
|
|
540
|
+
})
|
|
541
|
+
expect(status).toBe(200)
|
|
542
|
+
const body = JSON.parse(text)
|
|
543
|
+
expect(body.mode).toBeUndefined()
|
|
544
|
+
expect(body.project).toBeTruthy()
|
|
545
|
+
}, 90000)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
再加(或改现有 chat 用例)断言 `POST /v1/chat` 非流式带 `project`、无 `mode`;`POST /v1/code` 与 chat 结构一致。
|
|
549
|
+
|
|
550
|
+
- [ ] **Step 2: 运行确认失败**
|
|
551
|
+
|
|
552
|
+
Run: `bun test tests/serve.test.ts`
|
|
553
|
+
Expected: 新断言 FAIL(chat 无 project / paste 仍可能因 `code: true` 才有 project)
|
|
554
|
+
|
|
555
|
+
- [ ] **Step 3: 改 `serve.ts`**
|
|
556
|
+
|
|
557
|
+
- `handleChatRequest` 去掉 `codeMode` 参数。始终 `project = scanProject()`,`runOnceWithSystem(messages, buildCodeSystemPrompt(project, instructions), ...)`
|
|
558
|
+
- SSE/JSON 响应:去掉 `mode: "code"`,始终带 `project`
|
|
559
|
+
- `createSseCallbacks` 去掉 `codeMode`,始终附 `project`
|
|
560
|
+
- 路由:`handleChatRequest(req, res)`,`/v1/chat` 与 `/v1/code` 同一调用
|
|
561
|
+
- paste:去掉 `body.code` 分支,始终扫描项目 + `runOnceWithSystem`;响应带 `project`、不带 `mode`;忽略多余字段 `code`
|
|
562
|
+
|
|
563
|
+
- [ ] **Step 4: 改 `docs/API.md`**
|
|
564
|
+
|
|
565
|
+
- 端点表:`/v1/code` 写成「与 `/v1/chat` 相同(兼容路径)」
|
|
566
|
+
- `POST /v1/chat` 非流式示例加上 `"project": { ... }`
|
|
567
|
+
- `done` 事件说明加上 `project`
|
|
568
|
+
- 删除「Code 模式对话」独立语义;`/v1/code` 一节改为别名
|
|
569
|
+
- paste:删除 `code` 字段;写明始终返回 `project`;多传 `code` 忽略
|
|
570
|
+
- 示例里 `# Code 模式` 改为 `# 兼容路径 /v1/code`
|
|
571
|
+
|
|
572
|
+
顶部并发说明仍可提 `/v1/chat`、`/v1/code`、`/v1/paste`。
|
|
573
|
+
|
|
574
|
+
- [ ] **Step 5: 测试通过**
|
|
575
|
+
|
|
576
|
+
Run: `bun test tests/serve.test.ts`
|
|
577
|
+
Expected: PASS
|
|
578
|
+
|
|
579
|
+
- [ ] **Step 6: 提交**
|
|
580
|
+
|
|
581
|
+
```bash
|
|
582
|
+
git add src/serve.ts tests/serve.test.ts docs/API.md
|
|
583
|
+
git commit -m "feat: HTTP 对话接口统一为项目感知"
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## Task 7: README 与残留文案
|
|
589
|
+
|
|
590
|
+
**Files:**
|
|
591
|
+
- Modify: `README.md`
|
|
592
|
+
- 全文搜 `packages/min-agent` 里用户可见的「chat 模式 / code 模式 / coding mode」(不含历史 spec/plan)
|
|
593
|
+
|
|
594
|
+
- [ ] **Step 1: README Quick Start / Commands**
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
min-agent setup # Configure provider
|
|
598
|
+
min-agent # Interactive session
|
|
599
|
+
min-agent "hello" # Interactive session, send immediately
|
|
600
|
+
min-agent exec "hello" # One-shot, then exit
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Commands 列表把 `chat`/`code` 收成兼容一行,补上 `exec`、根命令 `--resume` / `-m` / `-i` / `--provider`。
|
|
604
|
+
|
|
605
|
+
- [ ] **Step 2: grep 残留**
|
|
606
|
+
|
|
607
|
+
`rg -n "mode: \"chat\"|createChatTools|createCodeTools|codeMode|coding mode|Code 模式" packages/min-agent/src packages/min-agent/README.md packages/min-agent/docs/API.md`
|
|
608
|
+
|
|
609
|
+
只允许旧 spec/plan 文档里出现。源码与用户文档必须清掉。
|
|
610
|
+
|
|
611
|
+
- [ ] **Step 3: 全量验证**
|
|
612
|
+
|
|
613
|
+
Run: `npx tsc --noEmit` 且 `bun test`(在 `packages/min-agent`)且仓库根 `bun run typecheck`
|
|
614
|
+
Expected: PASS
|
|
615
|
+
|
|
616
|
+
- [ ] **Step 4: 提交**
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
git add README.md
|
|
620
|
+
git commit -m "docs: 默认交互会话与 exec 的使用说明"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 实现注意事项
|
|
626
|
+
|
|
627
|
+
- **不要** import `cli.ts` 做测试(模块加载会执行 `main`)。
|
|
628
|
+
- `exec` 在 TTY 且无参数时不得读 stdin,否则会卡住。
|
|
629
|
+
- `runAgent` 不能靠传入 `callbacks` 来取 `hasError`,那会关掉终端打印。
|
|
630
|
+
- `/plan` 与双模式无关,不要动其语义。
|
|
631
|
+
- 用户可见文案不要写「原 code 模式」「TUI」等内部叫法;Usage/README 用 interactive session / 交互会话。
|