minimal-agent 0.6.1 → 0.6.2

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 CHANGED
@@ -1,573 +1,231 @@
1
1
  # minimal-agent
2
2
 
3
- > 最小化的 AI 编程助手生态 —— ReAct CLI + 插件系统 + 可视化 workflow 编辑器
4
- > 10 个内置工具 · 23 个 Skills · 2 个内置插件 · OpenAI 兼容协议 · Ink TUI · Bun-first
3
+ > 一个**最小化但功能完整**的 AI 编程助手 —— Bun + React + TypeScript + Ink TUI 构建,
4
+ > OpenAI 兼容、多 provider,10 个内置工具 + Skills + 插件 + Workflow DSL + 自动上下文压缩。
5
+ > 为**学习与教学**而生:代码量克制、注释充分,把一个 agent「从输入到工具循环到落盘」讲透。
5
6
 
6
- 一个用于**学习和教学**的 Agent 项目,从 [kakadeai](../) 主仓库的 Claude Code 源码中抽取并简化而来,把"一个能调工具、能自动压缩上下文、能跑插件、能可视化编排 workflow agent"在合理规模的 TypeScript 里讲清楚。
7
-
8
- > 项目由**两个独立的 npm 包**组成:`minimal-agent`(主体 ReAct CLI)+ `minimal-workflw`(可视化 workflow 编辑器)。两边只通过 `workflows/*.yaml` 文件耦合,零运行时依赖。
7
+ `v0.5.1` · Runtime: **Bun**(主)/ Node 20 · License: **PolyForm Noncommercial 1.0.0**
9
8
 
10
9
  ---
11
10
 
12
- ## 一图看全
11
+ ## 这是什么
12
+
13
+ 一个能在终端里跟你对话、并真正动手改代码的 AI agent。核心是一个 **AsyncGenerator 主循环**
14
+ (`src/loop.ts`):
13
15
 
14
16
  ```
15
- ┌──────────────────────────────────────────────────────────────────┐
16
- │ minimal-agent (主包) │
17
- │ │
18
- │ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
19
- │ │ ReAct │ ─→ │ Tools │ │ Plugins/ │ │
20
- │ │ runQuery│ │ 10 个 │ │ ralph-loop │ │
21
- │ └─────────┘ └─────────┘ │ workflow-* │ │
22
- │ ↑ └─────────────┘ │
23
- │ │ 调用 ↑ │
24
- │ ↓ │ 读 │
25
- │ ┌─────────┐ │ │
26
- │ │ Skills/ │ ─── 23 个 SKILL.md ──────┘ │
27
- │ └─────────┘ │
28
- └──────────────────────────────────┬───────────────────────────────┘
29
-
30
- ↓ 读写 workflows/*.yaml
31
-
32
- ┌──────────────────────────────────┴───────────────────────────────┐
33
- │ minimal-workflw (独立编辑器包, editor/) │
34
- │ │
35
- │ Bun + Vite + React + React Flow │
36
- │ ┌────────┐ ┌────────┐ ┌────────┐ │
37
- │ │ Palette│ │ Canvas │ │Inspector│ │
38
- │ └────────┘ └────────┘ └────────┘ │
39
- └──────────────────────────────────────────────────────────────────┘
17
+ 用户输入 → 调 LLM → LLM 要调工具就执行 → 结果喂回去 → 继续调 LLM → … → 不再调工具 → 输出最终回答
40
18
  ```
41
19
 
42
- ---
20
+ 每轮 `yield` 一个事件给 UI 实时刷新。上下文快撑爆时自动压缩成结构化摘要。麻雀虽小,
21
+ 工具系统、Skills、插件、Workflow、多 session、自动压缩、可编程子进程契约一应俱全。
43
22
 
44
23
  ## 特性
45
24
 
46
- | 模块 | 说明 |
47
- |---|---|
48
- | 🧠 主循环 | `runQuery` AsyncGenerator 把"调 LLM → 跑工具 → 喂回去"串成一条流 |
49
- | 🔧 内置工具 (10) | Read / Edit / MultiEdit / Write / Glob / Grep / Bash / WebSearch / WebFetch / WebBrowser |
50
- | 📦 自动压缩 | 接近 context window 时自动摘要历史(auto / micro / reactive 三层) |
51
- | 💾 单上下文 | `~/.minimal-agent/last-context.json` 自动保存/恢复,无 session 概念 |
52
- | 🎨 双模式 | Ink TUI 交互模式 + `-p` 非交互 CLI 模式(管道友好) |
53
- | 🌐 provider | 走 OpenAI Chat Completions 协议,配 .env 即可切换 MiniMax / DeepSeek / Kimi / OpenAI |
54
- | 🧰 Skills 系统 (23) | 扫描 `skills/<name>/SKILL.md` frontmatter,按需懒加载完整 prompt |
55
- | 🔌 插件系统 | `plugins/<id>/` drop-in 目录,声明式命令 / 富插件双契约;自带 ralph-loop + workflow-runner |
56
- | 🎬 Workflow 引擎 | YAML DAG,7 种 step 类型(tool / llm / skill / assert / pause / branch / loop) |
57
- | 🖼️ 可视化编辑器 | 独立 `minimal-workflw` 包,拖拽组装 workflow,节点面板从 `src/tools/` + `skills/` 自动派生 |
58
-
59
- ---
60
-
61
- > ⚠️ **0.2.0 已发布到 npm 但插件系统不可用**(dynamic import .ts 在 node_modules 下被 Node 拒绝)。请使用 **0.3.0+**:源码 import 统一 `.js` 后缀 + tsc 原地编译,Bun(dev 模式)和 Node(install 模式)共用一份目录布局。
62
-
63
- ## 系统要求
64
-
65
- - **Node.js ≥ 20**(npm install 后跑 bin 的运行时)或 **Bun ≥ 1.1**(推荐,dev 模式必需)
66
- - 一个 OpenAI 兼容的 LLM API(MiniMax / OpenAI / DeepSeek / Kimi / 自建 vLLM 均可)
67
- - *可选*:[Tavily](https://tavily.com) API key(开启 WebSearch)
68
- - *可选*:[OpenRouter](https://openrouter.ai) API key(用 `image-gen-openrouter` skill 生成图片)
69
- - *可选*:[MiniMax T2A](https://www.minimaxi.com/) 或 `pip install edge-tts`(用 `tts-zh` skill 做中文配音)
70
- - *可选*:`ffmpeg` + `ffprobe`(跑视频合成类 workflow)
71
- - *可选*:`playwright-core` + chromium(开启 WebBrowser;不装会自动降级到 WebFetch)
72
- - *无需安装 ripgrep* —— 项目自带 `vendor/ripgrep/` 6 个平台二进制(arm64/x64 × win32/darwin/linux)
25
+ - **10 个内置工具**:Read / Write / Edit / MultiEdit / Glob / Grep / WebSearch / WebFetch / WebBrowser / Bash
26
+ - **OpenAI 兼容**:Message 形状对齐 Chat Completions,一套代码接 MiniMax / 火山 ARK / DeepSeek / OpenAI / Moonshot…
27
+ - **Skills 系统**:放一个 `SKILL.md` 就能扩展能力,运行时实时扫描注入 prompt,改文件即生效
28
+ - **插件系统**:`plugins/<id>/` drop-in,自带 `/ralph-loop`(复制任务循环)与 `/workflow`(YAML DAG 执行器)
29
+ - **自动上下文压缩**:接近上限时把旧对话压成摘要,保留系统提示 + 摘要 + 最近几条
30
+ - **多 session + 归档**:同一工作目录多个并行会话,`/new` 归档、`/restore` 恢复
31
+ - **可编程子进程**:`-p --output-format json` 输出一行结构化结局契约,方便被宿主程序 spawn 调度
32
+ - **三种运行形态**:源码跑(dev)/ npm 全局安装 / **bun --compile 绿色单文件夹**(免安装,可装 U 盘)
73
33
 
74
34
  ---
75
35
 
76
- ## 安装
77
-
78
- ### A. 从 npm 安装(终端用户)
79
-
80
- ```bash
81
- # 1. 装主包(ReAct CLI + 内置工具/插件/skills)
82
- npm install -g minimal-agent
83
-
84
- # 2. (可选)装可视化 workflow 编辑器
85
- # ⚠ 当前 0.1 版编辑器 bin 直接吃 .ts,运行需要 Bun ≥1.1 在 PATH 上
86
- # (主包 minimal-agent 在纯 Node 下完全可用,不依赖 Bun)
87
- npm install -g minimal-workflw # 或:bun install -g minimal-workflw
88
-
89
- # 3. 跑起来
90
- minimal-agent # 启 TUI;首次会弹配置向导
91
- minimal-workflw ~/my-agent-project # 在指定项目目录里启编辑器
92
- ```
93
-
94
- 两个包**独立发版、独立安装**。只想要 ReAct CLI 就装 `minimal-agent`;想拖拽编排 workflow 再加 `minimal-workflw`。两者通过 `<project>/workflows/*.yaml` 文件耦合,没有进程间通信。
36
+ ## 快速开始
95
37
 
96
- ### B. 从源码运行(贡献者 / 想读源码)
38
+ ### 方式一:源码运行(推荐学习用)
97
39
 
98
40
  ```bash
99
41
  git clone https://github.com/billsoft/kakadeai.git
100
42
  cd kakadeai/minimal-agent
101
43
  bun install
102
-
103
- # 主体 ReAct CLI
104
- bun run start # TUI
105
- bun run src/main.tsx -p "hi" # 非交互
106
-
107
- # Workflow 编辑器(另开终端)
108
- cd editor && bun install
109
- bun scripts/cli.ts --project .. # 启编辑器并加载父项目的 workflows/
44
+ cp .env.example .env # 填入你的 API key(见下)
45
+ bun run start # 启动 TUI
110
46
  ```
111
47
 
112
- ---
48
+ > 没装 Bun?见 https://bun.sh 。也可在缺 Bun 时用 Node 跑发布产物(见「三种运行形态」)。
113
49
 
114
- ## 快速开始
115
-
116
- 支持两种配置方式,**任选其一**即可:
117
-
118
- ### 方式 A:手写 .env(适合开发者 / `-p` 非交互模式)
50
+ ### 方式二:全局安装后随处可用
119
51
 
120
52
  ```bash
121
- cp .env.example .env
122
- # 编辑 .env 填入 BASE_URL / API_KEY / MODEL
123
- bun run start
124
- ```
125
-
126
- ### 方式 B:首次启动配置向导(适合零配置上手)
127
-
128
- ```bash
129
- bun run start # 没检测到任何配置 → 自动弹 Ink 向导
53
+ npm install -g minimal-agent
54
+ minimal-agent # 首次运行进入配置向导
130
55
  ```
131
56
 
132
- 向导步骤:选 provider 预设(MiniMax / DeepSeek / OpenAI / Kimi / 自定义)→ 填 baseURL → 填 API key(掩码显示)→ 选 model → 自动做一次连通性测试 → 通过后写入 `~/.minimal-agent/config.json` 并直接进入对话界面。
133
-
134
- > 测试不通过或中途 ESC 退出 → **不写文件** → 下次启动继续走向导,直到成功为止。
57
+ ## 配置
135
58
 
136
- ### 配置优先级
59
+ 两种方式任选其一(**环境变量优先级高于配置文件**):
137
60
 
138
- ```
139
- process.env > ~/.minimal-agent/config.json > 弹向导
140
- ```
61
+ 1. **`.env` 文件**(源码模式,Bun 自动加载)—— 复制 `.env.example` 改值。
62
+ 2. **配置向导** —— 首次运行 `minimal-agent`(TUI)会引导你填,写到 `~/.minimal-agent/config.json`。
141
63
 
142
- env 永远盖过 JSON,**逐字段**生效。`-p` 模式不会弹向导(没 TTY),缺配置直接 stderr 报错退出。
143
-
144
- ### .env 字段
64
+ 必需三项 + 可选项:
145
65
 
146
66
  ```env
147
- # 必需
148
- MINIMAL_AGENT_BASE_URL=https://api.minimax.chat/v1
149
- MINIMAL_AGENT_API_KEY=your-api-key
150
- MINIMAL_AGENT_MODEL=MiniMax-M2.7
151
-
152
- # 可选
153
- MINIMAL_AGENT_PROVIDER=minimax # 仅显示用,默认 "env"
154
- MINIMAL_AGENT_CONTEXT_WINDOW=204800 # 上下文窗口,默认 128000
155
- TAVILY_API_KEY= # 启用 WebSearch
156
- OPENROUTER_API_KEY= # 启用 image-gen-openrouter skill
157
- MINIMAX_API_KEY= # 启用 tts-zh skill(minimax 通道)
158
- MINIMAX_GROUP_ID= # 同上
159
- ```
160
-
161
- ---
162
-
163
- ## 命令行参数
164
-
165
- ```
166
- minimal-agent [选项] [提示...]
167
- echo "提示" | minimal-agent [选项]
168
-
169
- 选项:
170
- -p, --print 非交互模式,问答一次后退出
171
- -v, --verbose 显示工具调用 / 压缩详情
172
- -d, --cwd <dir> 指定工作目录(不存在自动创建)
173
- -h, --help 帮助
174
- -V, --version 版本号
67
+ MINIMAL_AGENT_BASE_URL=https://api.minimax.chat/v1 # 必需:API 地址
68
+ MINIMAL_AGENT_API_KEY=your-api-key # 必需:API key
69
+ MINIMAL_AGENT_MODEL=MiniMax-M2.7 # 必需:模型名
70
+ MINIMAL_AGENT_PROVIDER=minimax # 可选:友好名,默认 "env"
71
+ MINIMAL_AGENT_CONTEXT_WINDOW=204800 # 可选:上下文窗口,默认 128000
72
+ TAVILY_API_KEY= # 可选:WebSearch 工具用(https://tavily.com)
175
73
  ```
176
74
 
177
- TUI 内部支持:
178
-
179
- | 输入 | 效果 |
180
- |---|---|
181
- | `/new` | 清空历史,重建 system prompt;同时清掉当前工作目录下所有 `.minimal-agent*/` 状态目录 |
182
- | `/compact` | 手动压缩当前对话 |
183
- | `/config` | 重新配置 LLM provider(LLM 驱动 skill,必须单独输入) |
184
- | `/init` | 让 LLM 扫一遍项目目录,生成或更新 `minimal-agent.md` |
185
- | `/ralph-loop "<目标>" [...]` | 复制任务循环(plugin: ralph-wiggum) |
186
- | `/workflow <name> [--input k=v]` | 执行 workflow yaml(plugin: workflow-runner) |
187
- | `/workflows` | 列出可用 workflow |
188
- | `Alt+Enter` | 输入框换行 |
189
- | `ESC` / `Ctrl+C` | 中断当前任务 |
190
-
191
- ---
192
-
193
- ## 内置工具(10 个)
194
-
195
- | 名称 | 用途 | 实现 |
196
- |---|---|---|
197
- | `Read` | 读文件(支持 offset / limit / 二进制嗅探) | `node:fs/promises` |
198
- | `Write` | 整体写文件(覆盖前要先 Read) | `node:fs/promises` |
199
- | `Edit` | 字符串精确替换(old_string 必须唯一) | `node:fs/promises` |
200
- | `MultiEdit` | 一次性多处替换(原子操作) | `node:fs/promises` |
201
- | `Glob` | 文件名模式匹配 | `fast-glob` |
202
- | `Grep` | 文件内容搜索 | spawn vendor 内置 ripgrep |
203
- | `Bash` | 终端命令执行(黑名单 + 超时双保险) | `node:child_process` |
204
- | `WebSearch` | Web 搜索(自动注入当前日期) | Tavily API |
205
- | `WebFetch` | 抓网页 → HTML→Markdown(LRU 缓存) | `fetch` + `turndown` |
206
- | `WebBrowser` | 浏览器交互(点击 / 填表单 / 截图 / JS 渲染) | playwright-core(**可选依赖**) |
207
-
208
- 工具入口:`src/tools/index.ts` 的 `ALL_TOOLS`。
209
-
210
- ---
211
-
212
- ## Skills 系统(23 个)
213
-
214
- 每个 `skills/<name>/SKILL.md` 都是一段 LLM 子系统提示词 + frontmatter 元数据。框架在每轮组装 system prompt 时**实时扫描**目录注入 `name + description`,完整 body 按需懒加载(用户输入命中 description 触发词时才载入)。**新增 skill 改文件即可,不用改代码。**
215
-
216
- 当前内置 skills:
217
-
218
- | 类别 | Skill |
219
- |---|---|
220
- | **配置 / 元** | `config` `init` `compact` `simplify` `batch` `skill-creator` `mcp-builder` |
221
- | **创作** | `algorithmic-art` `canvas-design` `frontend-design` `theme-factory` `web-artifacts-builder` `webapp-testing` |
222
- | **文档** | `docx` `pdf` `pptx` `xlsx` `diff` `commit` |
223
- | **多媒体流水线** | `image-gen-openrouter`(OpenRouter Nano Banana 文生图) `tts-zh`(MiniMax + edge-tts 中文配音) `subtitle-srt`(通用 SRT 字幕) `video-compose`(ffmpeg 视频合成) |
224
-
225
- **双源加载**:cwd 优先,packageRoot fallback。同名(frontmatter::name)时 cwd 覆盖。用户在自己项目里放 `skills/<name>/SKILL.md` 即可扩展或覆盖 npm 包自带的 skill。
75
+ `.env.example` 里附了 **MiniMax / 火山 ARK / DeepSeek / OpenAI / Moonshot** 五套现成模板,注释切换即可。
226
76
 
227
77
  ---
228
78
 
229
- ## 插件系统
230
-
231
- **硬约束**:`src/plugins/` 只放**框架**(discovery / 命令解析 / plugin.ts 加载 / hook 引擎 / transcript);任何具体插件功能都住在各自的 `plugins/<id>/` 目录内。**新增插件 = 把目录 drop 进 `plugins/`,`src/` 一行不动。**
79
+ ## 使用
232
80
 
233
- ### 内置插件
234
-
235
- #### `plugins/ralph-wiggum/` — `/ralph-loop`
236
-
237
- Karpathy 风格的复制任务循环:PLAN → BUILD → VERIFY → HEAL → DONE 五阶段 FSM,配合 verification gate 和 stop-hook 反馈。
81
+ ### 交互式 TUI
238
82
 
239
83
  ```bash
240
- /ralph-loop "在当前目录创建 hello.txt 内容为 hi" --max-iterations 5 --verify "file_exists:hello.txt"
84
+ bun run start # 或全局安装后直接 minimal-agent
241
85
  ```
242
86
 
243
- `--verify` 支持四种判据:`shell:<cmd>` / `file_exists:<path>` / `file_contains:<file>:<regex>` / `test_count:<N>`。FSM 状态文件落在 `<cwd>/.minimal-agent-ralph-wiggum/`,按当前 cwd 隔离,不同工作目录可并行。
87
+ 布局自上而下:消息列表(scrollback)→ 本轮过程区(流式思维链/工具进度)→ 输入框 状态栏。
88
+ `ESC` / `Ctrl+C` 中断当前任务,`Alt+Enter` 输入框换行。
244
89
 
245
- #### `plugins/workflow-runner/` `/workflow` `/workflows`
90
+ **内置 slash 命令**(不走 LLM,UI 层直接拦截):
246
91
 
247
- YAML DAG workflow 执行器,支持 7 种 step:
248
-
249
- | step 类型 | 用途 |
92
+ | 命令 | 作用 |
250
93
  |---|---|
251
- | `tool` | 直接调内置工具(Read / Bash / Write …) |
252
- | `llm` | 单轮 LLM 调用(可 `capture` 输出到变量) |
253
- | `skill` | LLM 驱动子循环,body 当子 system prompt |
254
- | `assert` | 表达式断言(`fileExists(...)` / `length(...)` / 比较运算) |
255
- | `branch` | 条件分支 |
256
- | `loop` | 循环(`over:` + `as:` + 自动 `${as}_idx`) |
257
- | `pause` | 等待用户输入 |
258
-
259
- ```bash
260
- /workflow youtube-shorts --input topic="猫咪赛博朋克城市探险"
261
- /workflow youtube-shorts "猫咪赛博朋克城市探险" # 位置参数:按 inputs 声明顺序映射,.yaml 后缀可省
262
- /workflows # 列出所有 workflow
263
- ```
264
-
265
- > ⚠ **位置参数依赖 yaml 文件 `inputs:` 数组的声明顺序**。重排 inputs 顺序会破坏所有现有 CLI 位置参数调用(位置参数会映射到错误的 input)。
266
- > 多 input workflow 推荐使用 `--input key=value` 显式 KV 形式,更稳。
267
-
268
- ### 写自己的插件
269
-
270
- 参考 `plugins/HOW-TO-WRITE-A-PLUGIN.md`。两种契约任选其一即可生效:
271
-
272
- - **声明式**(纯 markdown):`plugins/<id>/commands/<cmd>.md` 用 frontmatter + prompt 模板,框架自动注册命令,零 TS 代码。
273
- - **富插件**:根目录写 `plugin.ts` 导出 `default { runCommand }`,由插件自己 yield `LoopEvent` 流(可调 `runQuery` / 跑循环 / 起子进程)。
274
-
275
- 插件作者唯一稳定 import 路径:`from '../../src/plugin-sdk.ts'`。
276
-
277
- ---
278
-
279
- ## minimal-workflw 可视化编辑器(editor/ 子项目)
94
+ | `/new`(`/clear`) | 归档当前对话 清空 用最新 skills 重建 system prompt |
95
+ | `/compact` | 手动压缩当前对话 |
96
+ | `/session <name>` · `/sessions` | 切换 / 列出同目录下的并行会话 |
97
+ | `/list-archive` · `/restore <id>` | 列出 / 恢复归档对话 |
98
+ | `/show <id>` | 展开某次工具调用的完整输出(列表默认折叠到 3 行) |
99
+ | `/exit`(`/quit`) | 退出 |
280
100
 
281
- 独立的 npm 包,运行时与 `minimal-agent` **零耦合**,仅通过 `workflows/*.yaml` + `.editor-cache/manifest.json` 两个文件交换数据。
101
+ `/init`、`/config` 等是 **LLM 驱动的 skill**;`/workflow`、`/ralph-loop` 是**插件命令**(见下)。
282
102
 
283
- ### 启动方式
103
+ ### 非交互模式(`-p`)—— 脚本 / CI / 被宿主程序调用
284
104
 
285
105
  ```bash
286
- # 已全局安装时
287
- minimal-workflw # 当前 cwd minimal-agent 项目根
288
- minimal-workflw ~/my-project # 指定项目根
289
- minimal-workflw --refresh-manifest # 强制刷新工具/skill 清单
290
-
291
- # 从源码跑(在 editor/ 目录下)
292
- bun scripts/cli.ts --project .. # 一键起后端 + Vite
293
- bun run dev # 只起 Vite 前端(不带后端文件 IO)
294
- bun run server # 只起 Bun.serve() 后端
106
+ minimal-agent -p "帮我写一个 hello world"
107
+ echo "解释这段代码" | minimal-agent -p
108
+ minimal-agent -p "处理资料" -d /tmp/job-123 # -d 隔离工作目录
109
+ minimal-agent -p --output-format json "做点事" # 一行结构化结局契约
295
110
  ```
296
111
 
297
- > **为什么是 `bun scripts/cli.ts` 而不是 `bun run dev`?**
298
- > - `bun run dev` 只起 Vite 前端,UI 能渲染但**没有后端**,文件列表 / 读写 / manifest 端点全是 404
299
- > - `bun scripts/cli.ts` 是 bin 入口,**同时**起 `Bun.serve()` 文件 IO 后端 + Vite 前端,完整可用
300
- > - npm 发布后用户执行的 `minimal-workflw` 命令就是这个 cli.ts
301
-
302
- ### 启动序列(编辑器 CLI 内部)
303
-
304
- 1. 解析 `--project`(默认 cwd)→ 验证目录存在 + `workflows/` 子目录存在
305
- 2. 检查 `<project>/.editor-cache/manifest.json` 新鲜度,过期则 spawn `<project>/scripts/export-manifest.ts` 重新生成(脚本缺失时 fallback 到 `editor/public/manifest.fallback.json`)
306
- 3. 启动 `Bun.serve()` 在 127.0.0.1:5174(文件 IO + manifest 端点)
307
- 4. 启动 `bunx --bun vite` 在 127.0.0.1:5173
308
-
309
- ### 解耦红线(D2 红线)
310
-
311
- - ❌ 编辑器**绝不** import `minimal-agent/src/*` 任何模块
312
- - ❌ minimal-agent 主程序**绝不**在启动路径里 spawn 编辑器
313
- - ✅ 接触面只有:`workflows/*.yaml`(双向)+ `.editor-cache/manifest.json`(单向,主项目→编辑器)
314
-
315
- ---
316
-
317
- ## 示例:YouTube Shorts 生产流水线
318
-
319
- `workflows/youtube-shorts.yaml` 是一条端到端示例,演示如何在编辑器里拖出非编程用户场景:
320
-
321
- ```
322
- 输入主题
323
-
324
- LLM 写 5 幕剧本 (JSON)
325
-
326
- parse_script (jq 拆出 prompts.txt / narrations.txt)
327
-
328
- loop 5×: image-gen-openrouter ──→ assets/img_0..4.png
329
-
330
- loop 5×: tts-zh ──→ assets/audio_0..4.mp3
331
-
332
- probe_durations + merge_audio ──→ assets/audio.mp3
333
-
334
- subtitle-srt ──→ assets/subs.srt
335
-
336
- video-compose (ffmpeg) ──→ out/shorts.mp4
337
- ```
112
+ | 选项 | 说明 |
113
+ |---|---|
114
+ | `-p` / `--print` | 非交互单次问答,输出后退出 |
115
+ | `-v` / `--verbose` | stderr 显示工具调用、压缩等过程 |
116
+ | `-d <dir>` / `--cwd <dir>` | 指定工作目录(不存在自动创建),隔离文件视图与会话 |
117
+ | `--output-format <text\|json>` | `text`(默认,纯文本答案)/ `json`(一行结局契约) |
118
+ | `--max-turns <n>` | 最大工具循环轮数(防失控,默认 50) |
119
+ | `-V` / `--version` · `-h` / `--help` | 版本 / 帮助 |
338
120
 
339
- 跑起来:
121
+ **`--output-format json` 结局契约**(供宿主程序可靠 spawn):
340
122
 
341
- ```bash
342
- export OPENROUTER_API_KEY=sk-or-...
343
- bun run src/main.tsx -p "/workflow youtube-shorts --input topic='猫咪赛博朋克城市探险'"
123
+ ```json
124
+ { "ok": true, "result": "...", "is_error": false,
125
+ "stop_reason": "end_turn", "num_turns": 3, "error": null }
344
126
  ```
345
127
 
346
- 整条流水线**零代码改动** —— 4 skill 都是 markdown,1 workflow yaml。在 `minimal-workflw` 里打开 `youtube-shorts.yaml`,可视化拖拽改 prompt、换 skill、加节点。
128
+ `ok = (stop_reason === 'end_turn')`;`stop_reason end_turn | max_turns | error | interrupted | config_error`。
129
+ > ⚠️ `ok=true` 只代表「正常跑完未报错」,**不代表任务目标达成**——宿主应额外校验 `result` / 产物文件。
347
130
 
348
131
  ---
349
132
 
350
- ## 项目结构
133
+ ## 工具
351
134
 
352
- ```
353
- minimal-agent/
354
- ├── src/ # 主体内核
355
- │ ├── main.tsx # 入口:CLI 参数 + 模式分发
356
- │ ├── loop.ts # Agent 主循环 (runQuery)
357
- │ ├── config.ts # .env + ~/.minimal-agent/config.json
358
- │ ├── types.ts # 核心类型 (Message / Tool / LoopEvent)
359
- │ ├── plugin-sdk.ts # 插件作者唯一稳定 import 路径
360
- │ ├── plugins/ # 插件框架(discovery / loader / router / hooks)
361
- │ ├── llm/client.ts # OpenAI 兼容流式客户端
362
- │ ├── context/ # 压缩 / 持久化 / token 计数
363
- │ ├── prompts/ # system prompt 拼装
364
- │ ├── tools/ # 10 个内置工具
365
- │ └── ui/ # Ink TUI 组件
366
- ├── plugins/ # 实际插件(drop-in)
367
- │ ├── HOW-TO-WRITE-A-PLUGIN.md
368
- │ ├── ralph-wiggum/ # /ralph-loop
369
- │ └── workflow-runner/ # /workflow + /workflows
370
- ├── skills/ # 23 个 SKILL.md
371
- ├── workflows/ # YAML DAG 工作流定义
372
- │ ├── book-review-short.yaml
373
- │ ├── e2e-write-greet.yaml
374
- │ └── youtube-shorts.yaml
375
- ├── editor/ # minimal-workflw 独立子包
376
- │ ├── package.json # name: minimal-workflw, bin: minimal-workflw
377
- │ ├── scripts/
378
- │ │ ├── cli.ts # bin 入口(一键起后端+前端)
379
- │ │ └── server.ts # Bun.serve() 文件 IO + manifest 端点
380
- │ ├── public/manifest.fallback.json
381
- │ └── src/ # React + React Flow + Monaco UI
382
- ├── scripts/
383
- │ └── export-manifest.ts # 主项目 → editor 的工具/skill 元数据导出
384
- ├── vendor/ripgrep/ # 自带 ripgrep 二进制(6 个平台)
385
- ├── test/ # bun test 用例
386
- └── minimal-agent.md # 项目指令(类似 CLAUDE.md,自动注入 system prompt)
387
- ```
388
-
389
- ---
390
-
391
- ## 发布与安装(npm 工作流)
392
-
393
- 项目作为**两个独立 npm 包**发布:
394
-
395
- | 包 | 路径 | 命令 | 职责 |
396
- |---|---|---|---|
397
- | `minimal-agent` | 仓库根 | `minimal-agent` | ReAct CLI + 工具 + 插件 + skills |
398
- | `minimal-workflw` | `editor/` | `minimal-workflw` | 可视化 workflow 编辑器 |
399
-
400
- > **为什么不打成一个包**?90% 的 ReAct CLI 用户不需要编辑器,编辑器的依赖(React / Vite / @xyflow/react / Monaco 等)有几十 MB,不应该污染主包。两边解耦后用户按需取用,通过 `<project>/workflows/*.yaml` 文件交流,无进程间通信。
401
-
402
- ### 模块解析模式(dev vs install 通用范式)
403
-
404
- 主包采用**标准 NodeNext + tsc 原地编译**模式,解决了 npm 包"dev 跑 .ts,install 跑 .js"的通用目录差异问题:
135
+ | 工具 | 底层 | 说明 |
136
+ |---|---|---|
137
+ | **Read** | `node:fs` | 支持 offset/limit、二进制嗅探 |
138
+ | **Write** | `node:fs` | 整体写入;覆盖前需先 Read |
139
+ | **Edit** | `node:fs` | `old_string` 必须唯一 |
140
+ | **MultiEdit** | `node:fs` | 单文件一次多处替换 |
141
+ | **Glob** | `fast-glob` | 文件名 pattern |
142
+ | **Grep** | 自带 `ripgrep` | 内容搜索,**自带 6 平台二进制**,开箱即用 |
143
+ | **WebSearch** | Tavily API | `TAVILY_API_KEY` |
144
+ | **WebFetch** | `fetch` + `turndown` | HTML → Markdown,LRU 缓存 |
145
+ | **WebBrowser** | `playwright-core`(可选) | 缺依赖时自动降级 WebFetch |
146
+ | **Bash** | `child_process` | 黑名单 + 超时双保险 |
405
147
 
406
- - 源码(仓库 / dev)里 import 统一写 `.js` 后缀:`import { x } from './foo.js'`
407
- - dev 模式跑 `bun run dev`:Bun 把 `'./foo.js'` 透明解析到 `./foo.ts` 源文件,**无需任何构建**
408
- - publish 前 `tsc --build`:原地输出 `./foo.js`(与 .ts 兄弟位置,路径布局完全一致)
409
- - `.npmignore` 排除 `*.ts/*.tsx`,tarball 里**只发 .js**
410
- - install 后 Node 直接吃 .js,Bun 也吃 .js(行为统一)
411
- - 源码 `plugin.ts` 里 `import '../../src/plugin-sdk.js'` 在 dev / install 两种模式下都指向同一份模块(dev=plugin-sdk.ts,install=plugin-sdk.js),**模块身份天然 singleton**
148
+ 工具失败**不抛异常**——把 error 写进结果喂回 LLM 让它自己重试。
412
149
 
413
- 这是 `execa` / `p-queue` / `ink` 等纯 ESM TypeScript 库的通用做法。
150
+ ## Skills · 插件 · Workflow
414
151
 
415
- ### 用户安装(终端)
152
+ - **Skills**:每个 `skills/<name>/SKILL.md`(带 frontmatter)。运行时实时扫描注入 prompt,prompt 体按需懒加载。
153
+ cwd 优先、包内 fallback,用户可在自己项目里覆盖或新增。内置 20+ 个(`/init`、`/config`、文档生成、设计等)。
154
+ - **插件**(`plugins/<id>/`,drop-in,`src/` 一行不改):
155
+ - **`/ralph-loop`**(ralph-wiggum)—— Karpathy 式复制任务循环,自带 PLAN/BUILD/VERIFY/HEAL 状态机 + 验证门
156
+ - **`/workflow` · `/workflows`**(workflow-runner)—— YAML DAG 执行器,9 种 step(tool/llm/skill/assert/branch/loop/pause/parallel/vote)
157
+ - **可视化编辑器**:`editor/` 子项目(Bun + Vite + React Flow)拖拽编排 workflow YAML(不属于 npm 发布物)。
416
158
 
417
- ```bash
418
- # 只装主包
419
- npm install -g minimal-agent
420
- minimal-agent # 启 ReAct TUI
159
+ ---
421
160
 
422
- # 加装编辑器
423
- npm install -g minimal-workflw
424
- minimal-workflw /path/to/your-project # 启编辑器
425
- ```
161
+ ## 三种运行形态
426
162
 
427
- ### 开发者发布流程
163
+ | 形态 | 怎么跑 | 适合 |
164
+ |---|---|---|
165
+ | **源码 dev** | `bun run dev`(Bun 直接吃 `.ts`,watch 热重载) | 学习、改源码 |
166
+ | **npm 安装** | `npm i -g minimal-agent` → `minimal-agent`(Node 吃 tsc 编译的 `.js`) | 日常使用 |
167
+ | **绿色版** | `bun run compile:green` → 单文件夹(内嵌运行时,**免装 Node/Bun**) | 免安装分发 / 装 U 盘 |
428
168
 
429
- #### 1. 发布主包
169
+ **绿色版**把运行时 + 全部依赖 + 工具执行器编译进**单个可执行文件**,连同 `vendor/ripgrep`、`skills`、
170
+ `workflows`、插件一起组装成「拷到任意机器即插即用」的文件夹;支持把 `config.json` 预置在目录里做到
171
+ **零配置开箱即用**。三平台(Windows / macOS / Linux)各一份。详见 **[GREEN-BUILD.md](./GREEN-BUILD.md)**。
430
172
 
431
173
  ```bash
432
- cd minimal-agent
433
- npm version patch # 0.3.0 → 0.3.1
434
- npm publish --dry-run # 验证 tarball(src/*.js + plugins/*.js + skills + workflows + vendor)
435
- npm publish # prepublishOnly 自动跑 clean + tsc --build
174
+ bun run compile:green # 三主平台:win-x64 / mac-arm64 / linux-x64
175
+ bun run compile:green:host # 仅当前平台
176
+ bun run compile:green -- --config 你的config.json # 编译时烧录预配置
436
177
  ```
437
178
 
438
- 主包 `package.json` 关键字段:
439
-
440
- ```json
441
- {
442
- "name": "minimal-agent",
443
- "bin": { "minimal-agent": "src/main.js" },
444
- "files": ["src", "plugins", "skills", "workflows", "vendor/ripgrep", "README.md", "LICENSE"],
445
- "engines": { "node": ">=20" },
446
- "scripts": {
447
- "build": "tsc --build",
448
- "clean": "bun scripts/clean-build.ts",
449
- "prepublishOnly": "bun run clean && bun run build"
450
- }
451
- }
452
- ```
453
-
454
- `prepublishOnly` 先清旧产物再跑 tsc 原地编译,`.npmignore` 排除源码 .ts/.tsx 让 tarball 里**只有 .js**。`src/main.tsx` 首行 `#!/usr/bin/env node` 由 tsc 编译产物保留,直接作为 bin。
179
+ ---
455
180
 
456
- #### 2. 发布编辑器包
181
+ ## 开发
457
182
 
458
183
  ```bash
459
- cd minimal-agent/editor
460
- npm version patch
461
- bun run build # Vite 把前端打到 editor/dist/
462
- npm publish --dry-run
463
- npm publish
184
+ bun install # 安装依赖
185
+ bun run start # 运行(= bun run src/main.tsx)
186
+ bun run dev # watch 热重载
187
+ bun test # 跑所有测试
188
+ bun run typecheck # tsc --noEmit
189
+ bun run build # tsc --build → src/*.js + plugins/*.js(npm 发布用)
190
+ bun run clean # 删除原地编译产物
464
191
  ```
465
192
 
466
- 编辑器是独立子项目,Bun `.ts` 源码作为 bin(`scripts/cli.ts` 首行有 `#!/usr/bin/env bun`),与主包发版策略不同但**目录耦合零依赖**——只通过文件系统读 `<project>/workflows/*.yaml`。
193
+ 格式/检查用 **Biome**(2 空格缩进、单引号、80 列)。模块解析 NodeNext:源码 import 统一写 `.js` 后缀,
194
+ dev 下 Bun 透明回退到 `.ts` 兄弟,发布前 `tsc --build` 原地产出 `.js`。
467
195
 
468
- #### 3. 版本独立
196
+ ## 架构速览
469
197
 
470
- 两个包的 version 字段**互相独立**,不必同步。改主包内核 只升主包;改编辑器 UI → 只升编辑器。
198
+ | 模块 | 职责 |
199
+ |---|---|
200
+ | `src/loop.ts` | `runQuery()` 主循环(AsyncGenerator)—— 系统的「心脏」 |
201
+ | `src/llm/client.ts` | OpenAI 兼容流式客户端(text / tool_call / reasoning 三路) |
202
+ | `src/types.ts` | 对齐 Chat Completions 的 Message / ToolCall / 事件类型 |
203
+ | `src/tools/` | 10 个工具 + `executeTool()` 参数防线(JSON.parse → Zod → call) |
204
+ | `src/context/compact.ts` | 自动上下文压缩 |
205
+ | `src/prompts/` | system prompt 组装 + Skills 实时扫描注入 |
206
+ | `src/plugins/` | 插件框架(discovery / 命令路由 / hook 引擎 / transcript) |
207
+ | `src/ui/` | Ink TUI(MessageList / LiveArea / InputBox / StatusLine) |
208
+ | `src/config.ts` · `src/config/` | 分层 provider 配置(env → 配置文件) |
471
209
 
472
- ---
210
+ > 给 Claude Code / 维护者的深入说明见 **[CLAUDE.md](./CLAUDE.md)**。
473
211
 
474
- ## 工作原理(一句话版)
212
+ ## 目录结构
475
213
 
476
214
  ```
477
- 用户输入
478
-
479
-
480
- runQuery (loop.ts)
481
-
482
- ├── autoCompactIfNeeded # 接近上限就压缩
483
- ├── chat(LLM) # 流式 token + tool_calls
484
- ├── yield {text, ...} # UI 显示
485
-
486
- ├── if 没工具 → return # 任务完成
487
-
488
- ├── for each tool_call:
489
- │ executeTool # zod 校验 → tool.call(args)
490
- │ append tool result # 写回 history
491
-
492
- └── continue # 下一轮
493
- ```
494
-
495
- `/workflow` / `/ralph-loop` 这类插件命令在主循环**之前**被路由器拦截,由 `pluginRunner` 分发到对应插件的 `runCommand` 接管。详见 `src/loop.ts` + `src/plugins/pluginRunner.ts`。
496
-
497
- ---
498
-
499
- ## 开发命令
500
-
501
- ```bash
502
- # 主包
503
- bun run start # TUI
504
- bun run dev # 热重载
505
- bun test # 跑全部测试
506
- bun test test/llm.live.test.ts # 单个测试文件
507
- bun run typecheck # tsc --noEmit
508
- bun run build # tsup 打包
509
-
510
- # 编辑器
511
- cd editor
512
- bun scripts/cli.ts --project .. # 一键起后端 + 前端
513
- bun run dev # 只起前端
514
- bun run server # 只起后端
515
- bun test
215
+ minimal-agent/
216
+ ├─ src/ # 主程序(loop / tools / llm / ui / plugins / config …)
217
+ ├─ skills/ # 内置 skill(每个一个 SKILL.md)
218
+ ├─ plugins/ # 内置插件(ralph-wiggum / workflow-runner)
219
+ ├─ workflows/ # 内置 workflow(YAML)
220
+ ├─ vendor/ripgrep/ # Grep 自带的 6 平台 ripgrep 二进制
221
+ ├─ scripts/ # 构建脚本(含绿色版 build-green.ts)
222
+ ├─ editor/ # workflow 可视化编辑器(独立子项目)
223
+ ├─ test/ # 测试
224
+ ├─ GREEN-BUILD.md # 绿色版/U 盘分发指南
225
+ └─ CLAUDE.md # 架构与开发指南
516
226
  ```
517
227
 
518
- ---
519
-
520
- ## 与 [kakadeai](../) 主项目的关系
521
-
522
- minimal-agent 是 Claude Code 源码的**教学版**:
523
-
524
- | 维度 | kakadeai/Claude Code | minimal-agent |
525
- |---|---|---|
526
- | 代码量 | 数十万行 | 数千行 |
527
- | 主循环 | QueryEngine + 状态机 | 单层 while + AsyncGenerator |
528
- | Tool 数 | 60+(含 MCP / Agent / Notebook 等) | 10 个内置 |
529
- | Skill 加载 | `registerBundledSkill()` 注册 | 扫描目录 + frontmatter 解析 |
530
- | Plugin | builtin / bundled plugin registry | drop-in `plugins/<id>/` 目录 |
531
- | Workflow | / | YAML DAG + 可视化编辑器 |
532
- | Provider | Anthropic SDK | OpenAI Chat Completions(多家兼容) |
533
- | UI | 完整 Ink 框架 + 主题系统 | 基础 Ink 组件 |
534
-
535
- 设计目标是**砍掉一切非必要的抽象**,把"AI agent 是什么 / 怎么扩展"暴露在你眼前。
536
-
537
- ---
538
-
539
- ## 故障排查
540
-
541
- | 现象 | 原因 / 处理 |
542
- |---|---|
543
- | `-p` 模式报 `缺少必需的环境变量` | `-p` 不弹向导。先用 `bun run start` 走向导,或手动写 `.env` |
544
- | TUI 启动就弹向导,但我已经写了 .env | 检查 .env 是否在项目根(`bun run` 当前目录),三项变量名拼写无误 |
545
- | 向导测试卡 `HTTP 401` / `HTTP 404` | 401 = API key 错;404 = baseURL 没拼 `/v1` 或 model id 不对 |
546
- | 改完 `~/.minimal-agent/config.json` 没生效 | 当前进程仍持有旧配置;Ctrl+C 退出后重新 `bun run start` |
547
- | `rg: command not found` | 不该出现——项目自带 ripgrep;若出现说明 vendor 目录未随 npm tarball 复制 |
548
- | `WebBrowser 报"无法启动浏览器"` | 需 `npm install playwright-core && npx playwright install chromium`;或改用 WebFetch |
549
- | `minimal-workflw` 命令找不到 | 没装编辑器包;`npm install -g minimal-workflw` 即可 |
550
- | 编辑器空白 / 节点面板没工具 | `.editor-cache/manifest.json` 缺失;`minimal-workflw --refresh-manifest` 强制刷新 |
551
- | 编辑器看不到自定义 skill | 检查 `<project>/skills/<name>/SKILL.md` frontmatter 是否有 `name` 和 `description` 两个必填字段 |
552
- | ffmpeg / ffprobe 未找到 | `winget install Gyan.FFmpeg` / `brew install ffmpeg` / `apt install ffmpeg` |
553
- | 历史"被吃掉"了 | 上次进程崩溃前未保存;`~/.minimal-agent/last-context.json` 是最后写入的一次 |
554
-
555
- ---
556
-
557
228
  ## License
558
229
 
559
- 本项目使用 **PolyForm Noncommercial License 1.0.0**(详见根目录 `LICENSE` 文件)。
560
-
561
- **允许**:
562
- - 个人学习、教学、研究、非商业实验用途
563
- - 修改源代码、本地构建、自托管运行
564
- - 在非商业项目中引用 / fork
565
- - 慈善机构、教育机构、公共研究组织等非营利组织使用
566
-
567
- **禁止**:
568
- - 任何形式的商业使用(包括但不限于:销售本软件、用于公司内部生产环境、作为商业产品的组件、提供付费服务)
569
- - 未经授权的再分发(必须保留 LICENSE 与版权声明)
570
-
571
- 如需商业授权,请联系作者另行洽谈。
572
-
573
- `vendor/ripgrep/` 内的二进制为上游 ripgrep 项目的 MIT / Unlicense 双重许可,**不受**本项目 PolyForm Noncommercial License 约束。
230
+ [PolyForm Noncommercial License 1.0.0](./LICENSE) © 2026 billsoft。
231
+ 允许非商业用途的使用、修改与分发;商业用途请联系作者。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "最小化 Agent 系统 —— 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI;NodeNext + tsc 原地编译,dev 用 Bun .ts、install 用 Node .js(学习/教学用)",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Bill Wang <leiwang0359@gmail.com>",
@@ -54,6 +54,8 @@
54
54
  "clean": "bun scripts/clean-build.ts",
55
55
  "test": "bun test",
56
56
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
57
+ "compile:green": "bun scripts/build-green.ts",
58
+ "compile:green:host": "bun scripts/build-green.ts --host",
57
59
  "prepublishOnly": "bun run clean && bun run build"
58
60
  },
59
61
  "dependencies": {
@@ -20,18 +20,24 @@
20
20
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
21
21
  import { homedir } from 'node:os';
22
22
  import { dirname, join } from 'node:path';
23
- /** 配置文件路径(带环境变量覆盖,主要给测试用) */
23
+ import { execSiblingDir } from '../utils/greenRoot.js';
24
+ /**
25
+ * 配置文件的**写**路径(带环境变量覆盖,主要给测试用)。
26
+ * `saveConfig()` / 首次配置向导只写这里——env override 或 home,**绝不写 exe 同级**
27
+ * (U 盘可能只读,且不该污染产品目录)。读路径见 readSavedConfig 的分层。
28
+ */
24
29
  export function getConfigFilePath() {
25
30
  return (process.env.MINIMAL_AGENT_CONFIG_FILE ??
26
31
  join(homedir(), '.minimal-agent', 'config.json'));
27
32
  }
33
+ /** home 默认配置路径(读分层的最后一档)。 */
34
+ function homeConfigPath() {
35
+ return join(homedir(), '.minimal-agent', 'config.json');
36
+ }
28
37
  /**
29
- * 读取向导保存的配置。
30
- * - 文件不存在 / JSON 损坏 / 必填字段缺失 → 返回 null
31
- * - 成功 → 返回 SavedConfig
38
+ * 解析单个 config.json:不存在 / JSON 损坏 / 必填字段缺失 → null;成功 → SavedConfig。
32
39
  */
33
- export async function readSavedConfig() {
34
- const file = getConfigFilePath();
40
+ async function parseConfigFile(file) {
35
41
  try {
36
42
  const raw = await readFile(file, 'utf8');
37
43
  const data = JSON.parse(raw);
@@ -61,6 +67,30 @@ export async function readSavedConfig() {
61
67
  return null;
62
68
  }
63
69
  }
70
+ /**
71
+ * 读取已保存的 provider 配置(**分层**,按优先级返回第一个有效的):
72
+ * 1. `MINIMAL_AGENT_CONFIG_FILE` 显式覆盖 —— **只读这一个,不 fallthrough**
73
+ * (保持既有语义 + 测试隔离)
74
+ * 2. exe 同级 `config.json` —— **绿色版 / U 盘的卖家预置配置**(compile 后
75
+ * `execSiblingDir()` = 真实 exe 目录;dev/npm 下是 node/bun 二进制目录、无此文件 → 跳过)
76
+ * 3. `~/.minimal-agent/config.json` —— 首次配置向导写出的(原行为)
77
+ *
78
+ * 这层让卖家把配好的 config.json 放进绿色目录即完成预配置,客户端零配置开箱即用
79
+ * (loadProviderLayered 拿到 provider → 不进向导、`-p` 不报 config_error)。
80
+ *
81
+ * 加性保证:dev / npm 模式无 override 时,exe 同级无 config.json → 只命中 home → 行为不变。
82
+ */
83
+ export async function readSavedConfig() {
84
+ const override = process.env.MINIMAL_AGENT_CONFIG_FILE;
85
+ if (override)
86
+ return parseConfigFile(override);
87
+ for (const file of [join(execSiblingDir(), 'config.json'), homeConfigPath()]) {
88
+ const cfg = await parseConfigFile(file);
89
+ if (cfg)
90
+ return cfg;
91
+ }
92
+ return null;
93
+ }
64
94
  /**
65
95
  * 写入向导收集到的配置。
66
96
  * unix 上 chmod 600(仅本人可读写),Windows 上 chmod 是 no-op,靠 NTFS ACL。
package/src/main.js CHANGED
@@ -5,7 +5,18 @@ import { existsSync, mkdirSync } from 'node:fs';
5
5
  import { createRequire } from 'node:module';
6
6
  import { resolve } from 'node:path';
7
7
  const require = createRequire(import.meta.url);
8
- const pkg = require('../package.json');
8
+ const pkg = (() => {
9
+ try {
10
+ return require('../package.json');
11
+ }
12
+ catch {
13
+ return {
14
+ version: typeof __GREEN_BUILD_VERSION__ === 'string'
15
+ ? __GREEN_BUILD_VERSION__
16
+ : '0.0.0',
17
+ };
18
+ }
19
+ })();
9
20
  import { extractCwdArg } from './bootstrap/cwdArg.js';
10
21
  import { initWorkingDir, getWorkingDir } from './bootstrap/workingDir.js';
11
22
  import { applyToolKeysToEnv, loadProviderLayered } from './config.js';
@@ -24,9 +24,28 @@
24
24
  * ============================================================
25
25
  */
26
26
  import { existsSync, statSync } from 'node:fs';
27
- import { join } from 'node:path';
27
+ import { basename, join } from 'node:path';
28
28
  import { pathToFileURL } from 'node:url';
29
29
  const loaderCache = new Map();
30
+ /**
31
+ * 内置插件静态注册表(仅绿色版 compile 入口 entry.compiled.tsx 会填充)。
32
+ *
33
+ * 背景:`bun --compile` 后,rich 插件的 plugin.ts 顺链 import 的框架 SDK
34
+ * (`../../../src/plugin-sdk.js`)已被焊进 exe、磁盘上不存在 → 动态 import
35
+ * 外部 plugin.ts 必抛错。绿色版改为在 compile 入口**静态 import** 内置插件
36
+ * (`src/plugins/builtinRegistry.ts`),让 bun 把执行器整树打进 exe,再经
37
+ * setBuiltinPluginApi 注册到这里;loadPluginApi 在磁盘加载缺失/失败时回退到此表。
38
+ *
39
+ * dev / npm 模式:entry.compiled 永不被 import → 此表恒为空 → 回退分支不触发
40
+ * → loadPluginApi 行为与改动前逐字节一致。
41
+ *
42
+ * key = 插件目录名(`plugins/<id>/` 的 `<id>`,等于 `basename(pluginRoot)`)。
43
+ */
44
+ const builtinApis = new Map();
45
+ /** 由绿色版 compile 入口调用,注册内置插件执行器。dev/npm 不会调用。 */
46
+ export function setBuiltinPluginApi(key, api) {
47
+ builtinApis.set(key, api);
48
+ }
30
49
  function isPluginApi(value) {
31
50
  if (!value || typeof value !== 'object')
32
51
  return false;
@@ -62,8 +81,24 @@ export async function loadPluginApi(pluginRoot) {
62
81
  // stat 失败静默,不影响加载
63
82
  }
64
83
  }
84
+ // 绿色版回退:磁盘加载缺失/失败时,按目录名查 exe 内静态注册表。
85
+ // dev/npm 注册表恒空 → 永远返回 null → 不改变下方任何分支的最终结果。
86
+ const builtinKey = basename(pluginRoot);
87
+ const builtinFallback = () => {
88
+ const api = builtinApis.get(builtinKey);
89
+ if (api) {
90
+ loaderCache.set(pluginRoot, api);
91
+ return api;
92
+ }
93
+ return null;
94
+ };
65
95
  const pluginEntry = jsExists ? jsPath : tsExists ? tsPath : undefined;
66
96
  if (!pluginEntry) {
97
+ // 磁盘无 plugin.js/.ts:绿色目录只铺声明式部分(plugin.json + commands/),
98
+ // 执行器在 exe 内注册表 → 这里命中。dev/npm 注册表空 → 维持原 null。
99
+ const fb = builtinFallback();
100
+ if (fb)
101
+ return fb;
67
102
  loaderCache.set(pluginRoot, null);
68
103
  return null;
69
104
  }
@@ -79,6 +114,11 @@ export async function loadPluginApi(pluginRoot) {
79
114
  return candidate;
80
115
  }
81
116
  catch (err) {
117
+ // 绿色版:磁盘铺了 plugin.js 但它顺链 import 的框架 SDK 在 exe 内、磁盘没有
118
+ // → import 抛错 → 回退到 exe 内注册表。dev/npm 注册表空 → 维持原 warn + null。
119
+ const fb = builtinFallback();
120
+ if (fb)
121
+ return fb;
82
122
  console.warn(`[minimal-agent] failed to load plugin entry at ${pluginRoot}: ${err instanceof Error ? err.message : String(err)}`);
83
123
  loaderCache.set(pluginRoot, null);
84
124
  return null;
@@ -26,6 +26,7 @@
26
26
  import { spawn } from 'node:child_process';
27
27
  import { chmodSync, existsSync } from 'node:fs';
28
28
  import { resolve } from 'node:path';
29
+ import { execSiblingDir } from '../../utils/greenRoot.js';
29
30
  import { findPackageRoot } from '../../utils/packageRoot.js';
30
31
  let cached;
31
32
  /**
@@ -53,6 +54,15 @@ async function detect() {
53
54
  ensureExecutable(vendored);
54
55
  return vendored;
55
56
  }
57
+ // ②.5 绿色版(bun --compile 单文件):exe 同级 vendor/ripgrep/<arch>-<platform>/rg
58
+ // compile 后 import.meta.url 是虚拟路径,findPackageRoot 失效(② 命不中),
59
+ // 绿色目录把 vendor/ 摆在可执行文件旁边,从这里捞回来。dev/npm 下 exe 同级
60
+ // 没有 vendor/ → existsSync 过滤掉 → 不影响 ②/③/④ 原有优先级。
61
+ const sibling = resolve(execSiblingDir(), 'vendor', 'ripgrep', subdir(), exeName());
62
+ if (existsSync(sibling)) {
63
+ ensureExecutable(sibling);
64
+ return sibling;
65
+ }
56
66
  // ③ PATH 上的 rg —— 直接试 spawn rg --version
57
67
  if (await trySpawn('rg'))
58
68
  return 'rg';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ============================================================
3
+ * src/utils/greenRoot.ts —— 绿色版(bun --compile 单文件)资源探测锚点
4
+ * ------------------------------------------------------------
5
+ * 问题:`bun build --compile` 把运行时 + 全部 JS 焊进单个可执行文件,
6
+ * 此时 `import.meta.url` 指向嵌入式虚拟路径(`/$bunfs/...`),磁盘上
7
+ * 既没有 package.json 也没有源码树 —— `findPackageRoot` 会抛错,靠它
8
+ * 定位的 `vendor/ripgrep/`、`skills/`、`plugins/`、`workflows/` 全失效。
9
+ *
10
+ * 方案:绿色版把这些外部资产与可执行文件**放在同一目录**,运行时用
11
+ * `dirname(process.execPath)` 找回来(exe 同级)。
12
+ *
13
+ * 为什么 dev / npm 模式零影响:
14
+ * - dev(`bun run src/main.tsx`):execPath = bun 二进制,其同级目录
15
+ * (如 `~/.bun/bin/`)没有 vendor/skills/plugins。
16
+ * - npm 全局安装(`node dist/main.js`):execPath = node 二进制,其
17
+ * 同级目录同样没有这些资源目录。
18
+ * - 故调用方一律对返回值做 `existsSync` 过滤:dev/npm 下命不中 → 被
19
+ * 丢弃 → 行为与现状逐字节一致。只有真·绿色版(资产摆在 exe 旁边)
20
+ * 才会命中。因此这里**无条件返回** execPath 目录即可,无需判别
21
+ * "是否 compiled"。
22
+ * ============================================================
23
+ */
24
+ import { dirname } from 'node:path';
25
+ /**
26
+ * 返回可执行文件所在目录(绿色版资产的同级锚点)。
27
+ *
28
+ * 调用方必须用 existsSync 过滤返回路径下的目标子目录 —— dev/npm 模式
29
+ * execPath 指向 node/bun 二进制,其同级没有资源目录,过滤后无副作用。
30
+ */
31
+ export function execSiblingDir() {
32
+ return dirname(process.execPath);
33
+ }
@@ -21,6 +21,7 @@
21
21
  import { existsSync } from 'node:fs';
22
22
  import { join, resolve } from 'node:path';
23
23
  import { getWorkingDir } from '../bootstrap/workingDir.js';
24
+ import { execSiblingDir } from './greenRoot.js';
24
25
  import { findPackageRoot } from './packageRoot.js';
25
26
  /**
26
27
  * 返回资源目录的搜索顺序数组(cwd 优先 + packageRoot fallback)。
@@ -35,6 +36,13 @@ export function getResourceSearchPaths(name, metaUrl) {
35
36
  if (existsSync(cwdPath)) {
36
37
  paths.push(cwdPath);
37
38
  }
39
+ // 绿色版(bun --compile 单文件):exe 同级 <name>/ —— compile 后 findPackageRoot
40
+ // 失效,资源目录摆在可执行文件旁边从这里命中。dev/npm 下 exe 同级(node/bun
41
+ // 二进制所在目录)没有资源目录 → existsSync 过滤 → 数组与现状一致。
42
+ const sibPath = resolve(join(execSiblingDir(), name));
43
+ if (!paths.includes(sibPath) && existsSync(sibPath)) {
44
+ paths.push(sibPath);
45
+ }
38
46
  let pkgPath;
39
47
  try {
40
48
  pkgPath = resolve(join(findPackageRoot(metaUrl), name));
@@ -42,7 +50,7 @@ export function getResourceSearchPaths(name, metaUrl) {
42
50
  catch {
43
51
  return paths;
44
52
  }
45
- if (pkgPath !== cwdPath && existsSync(pkgPath)) {
53
+ if (!paths.includes(pkgPath) && existsSync(pkgPath)) {
46
54
  paths.push(pkgPath);
47
55
  }
48
56
  return paths;