agent-from-zero 0.35.0 → 0.35.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,244 +1,142 @@
1
1
  # mini-agent
2
2
 
3
- **跟着 AI,从零搓一个自己的 Claude Code**——一个能读文件、改代码、跑命令的命令行 Agent。TypeScript 实现,默认 DeepSeek 驱动(任何 OpenAI 兼容模型可配)。
3
+ 一个照着 Claude Code 做出来的命令行 coding agent,TypeScript 实现,默认接 DeepSeek(任何支持 function calling 的 OpenAI 兼容模型都能换上)。
4
4
 
5
- 人定方向、做决策、验收,AI 写大部分代码——这既是本仓库的诞生方式,也是这门课教你的工作方式。
5
+ ## 这是什么
6
6
 
7
- 课程共 35 天——主线 10 天搓出一个能用的 agent,进阶 25 天打磨成工业级 CLI(MCP / 权限裁判 / 长期记忆 / 成本核算 / 调用校验 / 计划模式 / diff 预览 / 撤销 / 模型分级 / 远程 MCP / 会话改动总览 / @file 引用 / 会话选择器 / 终端 UI / 方向键审批 / 多问题表单 / 模型切换 / 推理流 / Skill 系统 / 记忆深化 / Hook 深化);一天一个 commit、一晚一章的节奏跟做。**commit 历史就是课程目录**:`git log --oneline` 看大纲,`git checkout dayN` 拿到第 N 天对应的完整代码。
7
+ mini-agent 是我模仿 Claude Code 自己搓的。从终端 UI 的样子,到背后的各种机制,基本都是照着 Claude Code 的做法来实现的:
8
8
 
9
- > **设计有出处,不是凭感觉搭的**:蓝本是《Harness Engineering:从 Claude Code 看 AI 编码工程》提炼的 22 个工程模式——博主在公众号用 30+ 篇拆完了全书(本仓库就是那次拆解的实践篇)。书的收官章,作者用 800 行 Rust 写了一个代码审查 Agent,验证这些模式能跨语言、跨场景活下来;**这个仓库是同一套模式的又一次迁移**:场景换成 Claude Code 式的 CLI 编码 agent,语言换成 TypeScript——不跟着用 Rust,是因为新手教程的第一原则是别让语言难度挡路。
10
- >
11
- > **用 AI 搓出一个类 Claude Code,这件事本身就是这套方法最好的 demo**——教程里每个「踩过的坑」都真实发生在开发过程中,不是编的。
12
- >
13
- > 代码、注释、commit message 用英文——模型的工具调用主要在英文数据上训练,英文指令引导更稳,也方便国际读者;配套教程是中文。
9
+ - **交互界面**:Ink 终端 UI、方向键审批菜单、多问题表单、`@file` 引用、diff 预览、推理过程流式展示
10
+ - **Skill**:`SKILL.md` 按需加载,项目级放 `.mini-agent/skills/`,全局放 `~/.config/mini-agent/skills/`。和 Claude Code 一样,skill 列表通过带 `<system-reminder>` 的 user message 告诉模型,新增或修改 skill 不用重启;可以用 `/skill <名字> [参数]` 或 `/<名字> [参数]` 手动运行
11
+ - **MCP**:stdio 和 HTTP 两种传输都支持,远程服务器走 OAuth
12
+ - **权限**:allow / ask / deny 三档,deny 永远优先;另外有 plan 模式、auto 模式(交给模型审核)、`--dangerously-skip-permissions`
13
+ - **其他**:上下文压缩、长期记忆、hooks、子 agent、后台任务、todo、撤销、会话续接、成本统计、非交互的 `-p` 模式
14
14
 
15
- ## 快速开始
15
+ 这个版本是我自己一直在 dogfooding、边用边改出来的,到现在已经挺顺手了。我把自己工作用的 **Slack** 和 **Linear** MCP 都接了进来,底层模型用的是 DeepSeek。平时主要让它干这些活:
16
16
 
17
- 最简方式(npm 包,包名 `agent-from-zero`,装完命令叫 `mini-agent`):
17
+ - 查 bug、看日志、定位问题
18
+ - 回 Slack 消息
19
+ - 跑数据 backfill 之类的脏活
18
20
 
19
- ```bash
20
- echo 'DEEPSEEK_API_KEY=sk-你的key' > .env # key 在 platform.deepseek.com 获取
21
- npx agent-from-zero
22
- ```
21
+ ## 用下来的感受
22
+
23
+ 老实说,跟 Claude Code 比体验还是差一截。但跟我直接用 DeepSeek 自带的 harness 相比,mini-agent 好用太多了。
24
+
25
+ 说句题外话:我对 A 社这家公司的人品不太认可,但产品确实做得好。至少从 Claude Code 的体验来看,我觉得它远远好过 Codex 和 DeepSeek 的 harness。所以与其从头去想一个 agent 该长什么样,不如直接照着最好的那个来做。
23
26
 
24
- 从源码跑:
27
+ ## 安装与配置
28
+
29
+ ### 安装
30
+
31
+ 需要 Node >= 20.6。不用 clone 仓库,直接用 npx 运行 npm 包(包名是 `agent-from-zero`):
25
32
 
26
33
  ```bash
27
- git clone https://github.com/Michael-crazyman/mini-agent.git && cd mini-agent
28
- npm install
29
- cp .env.example .env # 填入你的 DeepSeek API Key
30
- npm start # 跑最新版
31
- npm run eval # 10 道题的回归考试
34
+ npx agent-from-zero
32
35
  ```
33
36
 
34
- ## 换个模型用
37
+ ### 配置 API key
35
38
 
36
- 默认 DeepSeek,但任何 **OpenAI 兼容接口**都能跑,三个环境变量搞定:
39
+ 推荐把 API key 写进 **shell 的启动配置文件**(zsh 是 `~/.zshrc`,bash 是 `~/.bashrc`),这样在任何目录启动 mini-agent 都能用。默认用 DeepSeek,只需要一个 key(在 [platform.deepseek.com](https://platform.deepseek.com) 获取),变量名是 `DEEPSEEK_API_KEY`:
37
40
 
38
41
  ```bash
39
- MINI_AGENT_API_KEY=sk-...
40
- MINI_AGENT_BASE_URL=https://api.openai.com/v1 # OpenAI / Kimi / Qwen / GLM / OpenRouter / Ollama 均可
41
- MINI_AGENT_MODEL=gpt-4.1-mini
42
+ # ~/.zshrc 或 ~/.bashrc
43
+ export DEEPSEEK_API_KEY=sk-你的key
42
44
  ```
43
45
 
44
- 各家 base URL 见 [.env.example](.env.example)。两个注意:模型必须支持 function calling;窗口比 DeepSeek 小的话设 `MINI_AGENT_CONTEXT_WINDOW`(单位 token)。换完跑一遍 `npm run eval`——同样 10 道题,正好当模型横评。
46
+ 改完运行 `source ~/.zshrc`(或 `source ~/.bashrc`),或者新开一个终端。
45
47
 
46
- ## 非交互执行(print / exec)
48
+ 如果某个项目想单独用一个 key,也可以在那个目录下放一个 `.env` 文件,写 `DEEPSEEK_API_KEY=sk-...`,不带 `export`。mini-agent 启动时会读当前目录的 `.env`,但**环境变量优先**:shell 配置文件里已经导出的变量,`.env` 里的同名值会被忽略。
47
49
 
48
- 像 `claude -p` 一样执行一次任务并退出,也可以使用 `exec` 入口:
50
+ ### 配置审核模型 judge.model
49
51
 
50
- ```bash
51
- mini-agent -p "解释这个项目" --model deepseek-flash --effort high
52
- mini-agent exec --model deepseek-flash --effort max --prompt "检查当前改动"
53
- cat error.log | mini-agent -p "分析这份日志"
54
- mini-agent -p "总结当前改动" --output-format json > result.json
52
+ 开启 auto 模式(会话里输入 `/auto`,或启动时加 `--auto`)后,每个需要审批的工具调用会先交给一个审核模型判断:安全的直接执行,有风险或拿不准的仍然问你。审核模型只有两种选择:
55
53
 
56
- # 从源码运行同样的参数
57
- npm start -- -p "解释这个项目" --model deepseek-flash --effort high
58
- ```
54
+ 1. **主 agent 用的模型**(默认):和主 agent 共用同一个 API key 和 base URL。没配 `judge.model` 就直接用主 agent 当前的模型。
55
+ 2. **Jev**:配好 Jev 的 key 后自动优先用它(见下一节)。
59
56
 
60
- `-p` / `--print` 是非交互开关,参数可以放在 prompt 前后;prompt 可以是位置参数或
61
- `--prompt`,二者不能同时使用。以 `-` 开头的任务文字放在 `--` 后,例如
62
- `mini-agent -p -- "--help 参数是什么意思"`。没有位置 prompt 时也可以只从 stdin 读取任务;
63
- 有 prompt 时,管道内容追加在其后。管道会读到 EOF,脚本调用时应关闭 stdin,或用 `< /dev/null`。
57
+ `judge.model` 写在 `~/.config/mini-agent/settings.json`(全局)或 `.mini-agent/settings.json`(项目级):
64
58
 
65
- `--model` 和 `--effort` 只覆盖本次进程,不修改保存的配置;交互启动也接受这两个选项。
66
- effort 使用 `/effort` 的同一套能力判断,不支持的值会列出可选项并以退出码 2 退出。
67
- 未知模型或代理需要在 settings 中配置 `effortProfiles`;`--effort default` 使用供应商默认值。
68
- `--model` 使用当前 endpoint 的模型 ID,不自动切换 vendor、endpoint 或 API key。
59
+ ```json
60
+ {
61
+ "judge": { "model": "deepseek-flash" }
62
+ }
63
+ ```
64
+
65
+ 注意:`judge.model` 走的是主 agent 的 API 端点,所以只能填主 agent 那个服务商下的模型,不能填别家的模型,也不能在这里填 Jev。不确定的话就不要配,默认用主 agent 的模型最稳妥。
69
66
 
70
- 默认 `text` 输出只包含最终回答,保留原始 Markdown;进度与诊断走 stderr,不输出动画或输入框。
71
- `--output-format json` 输出单个结果对象,含 `result`、`is_error`、`reason`、`session_id`、
72
- `model`、`effort`、`usage` 和 `estimated_cost_usd`。usage 与费用是本地会话计量,
73
- 不含 Jev 等辅助审核调用。当前支持 text / json,不兼容 Claude 的 stream-json SDK 协议。
67
+ ### 配置 Jev(可选)
74
68
 
75
- 退出码:`0` 正常完成,`1` 执行失败,`2` 参数错误,`130` 被中断。
76
- `-r` / `--resume` 仍可续接最近会话;`--auto` 仍经过原有审核,无人确认的操作直接拒绝。
77
- 单独使用 `-p` 不会放宽权限;它不能弹出交互审批,未获批准的操作会被拒绝。
69
+ **Jev 不是必须的**,不配也能正常使用 auto 模式,审核会用上面说的主 agent 模型。
78
70
 
79
- 需要显式跳过审批时,使用与 Claude Code 相同的参数:
71
+ Jev 是 TypeSafe 提供的审核模型。想用的话,和 DeepSeek 的 key 放在同一个地方,加上 Jev 的 key(两个变量名都可以,二选一):
80
72
 
81
73
  ```bash
82
- mini-agent -p "执行这项任务" --permission-mode bypassPermissions
83
- mini-agent exec "执行这项任务" --dangerously-skip-permissions
84
- npm start -- -p "执行这项任务" --model deepseek-flash --effort high --permission-mode bypassPermissions
74
+ # ~/.zshrc 或 ~/.bashrc
75
+ export JEV_API_KEY=你的key
76
+ # 或者
77
+ export TYPESAFE_API_KEY=你的key
78
+ ```
79
+
80
+ 配了 key 之后,下次启动会自动优先用 Jev 审核;Jev 额度用完或出错时,会自动退回主 agent 的模型。Jev 的模型版本默认是 `jev-1.13.0`,要换的话在 settings.json 里设置 `autoMode.model`:
81
+
82
+ ```json
83
+ {
84
+ "autoMode": { "model": "jev-1.13.0" }
85
+ }
85
86
  ```
86
87
 
87
- `bypassPermissions` 跳过普通人工确认和 Jev/模型审核,也适用于子 agent、后台 shell 和 MCP 调用。
88
- 它只对本次进程生效,不写入 settings;显式 bypass 优先于 `--auto` 和配置里的 auto mode。
89
- `--permission-mode auto` 启用自动审核,`--permission-mode default` 关闭 auto 和 bypass。
90
- `--dangerously-skip-permissions` 等价于 bypass 模式,并优先于同一命令中的 mode 参数。
91
- 交互界面会在底部显示 `bypass permissions on`;`/auto` 不会改变这个启动选项,需重新启动才能取消。
88
+ ### 换别的模型
92
89
 
93
- 显式 deny、受保护路径、plan 模式限制以及 PreToolUse hook 拦截仍然有效;
94
- 业务澄清问题不会自动代答。源码对照为 `utils/permissions/permissionSetup.ts` 的
95
- `initialPermissionModeFromCLI` 与 `utils/permissions/permissions.ts` 的 bypass 分支:
96
- 先检查规则,再放行普通审批。mini-agent 保留自己的硬性规则,不宣称完全复制 Claude 的全部例外。
90
+ 和 API key 放在同一个地方,设置这三个变量就行,各家的 base URL 写在 [.env.example](.env.example) 里:
97
91
 
98
- 交互参考 [Claude Code CLI](https://code.claude.com/docs/en/cli-reference) 和本地
99
- `claude-code-sourcemap/restored-src/src/main.tsx` 的参数及 stdin 处理;不是其全部参数的兼容实现。
92
+ ```bash
93
+ export MINI_AGENT_API_KEY=sk-...
94
+ export MINI_AGENT_BASE_URL=https://api.openai.com/v1
95
+ export MINI_AGENT_MODEL=gpt-4.1-mini
96
+ ```
100
97
 
101
- ## Usage and account status
98
+ 模型必须支持 function calling。如果它的上下文窗口比 DeepSeek 小(默认按约 1M 算),在 settings.json 里按模型配置窗口大小,`/model` 切换模型时会跟着变:
102
99
 
103
- 输入 `/status` 查看当前模型、本会话 token/缓存命中/估算费用,以及 vendor 支持的账户信息。
104
- DeepSeek 可直接查余额;OpenAI、Anthropic 的组织 usage/cost 需要另外配置
105
- `OPENAI_ADMIN_KEY` / `ANTHROPIC_ADMIN_KEY`,不支持查询的余额会明确显示不可用。
100
+ ```json
101
+ {
102
+ "contextWindows": { "gpt-4.1-mini": 1000000, "deepseek-v4-pro": 1000000 }
103
+ }
104
+ ```
106
105
 
107
- `/status local` 只看本地统计,不联网。使用代理时可显式输入 `/status deepseek`、
108
- `/status openai`、`/status anthropic` 查询对应的官方账户;不会把代理 key 当成官方账户 key。
109
- 组织报表按 UTC 本月至今统计,涵盖管理凭证所属组织,区别于当前会话。
110
- 接口、凭证和统计范围见 [Status 说明](docs/status.md)。
106
+ 也可以用环境变量 `MINI_AGENT_CONTEXT_WINDOW` 统一覆盖所有模型。
111
107
 
112
- ## Auto mode
108
+ ### 上下文和自动压缩
113
109
 
114
- ### Rule-based policy preview
110
+ 上下文大小用的是 API 每次返回的真实 `prompt_tokens`,再加上之后新增消息的估算值,所以基本是准的。快满时自动压缩:触发点是窗口减去给回复预留的 20k 和 13k 安全余量(约 1M 的窗口大概在 97% 左右)。状态栏的 `ctx N%` 表示离自动压缩还有多远,到 100% 时下一次调用模型前就会先压缩。也可以随时手动 `/compact`。
115
111
 
116
- 新规则策略已可选启用,审核结果分为 `allow / deny / ask`,不再用授权和风险两个分数作为放行门槛:
112
+ ### 配置 MCP
117
113
 
118
- ```sh
119
- MINI_AGENT_AUTO_POLICY=rules npm start -- --auto
120
- ```
114
+ 在 `~/.config/mini-agent/settings.json`(全局)或 `.mini-agent/settings.json`(项目级)的 `mcpServers` 里添加服务器,写法和 Claude Code 一样。保存后会话里自动生效,不用重启:新加的服务器会连上,删掉的会断开,改过的会重连。也可以手动运行 `/mcp reload`。服务器自己的工具有增减时(MCP 的 `tools/list_changed` 通知),工具列表也会自动更新。启动时 MCP 在后台并行连接,不会卡住输入框,哪个连上了它的工具就马上可用;有连接失败的,状态栏右下角会用红色标出是哪个(需要登录的是黄色)。
121
115
 
122
- 也可在 settings 中设置 `autoMode.policy: "rules"`。配置 `JEV_API_KEY` 或
123
- `TYPESAFE_API_KEY` 后先用 Jev 初筛;有疑点再用同一 vendor 的 `judge.model` 复核,
124
- 未指定则使用当前模型。没有 key 或 Jev 故障时直接走模型。普通任务内检查、测试、修改及明确要求的常规
125
- commit/push 按实际效果审核;删除、丢弃工作、改写共享历史、基础设施写入和权限修改
126
- 仍要求人工确认。违反明确限制的操作直接拒绝,让 agent 改用合规方案;连续 3 次或
127
- 累计 20 次自动拒绝会停止,等待用户给出方向。子 agent 和中断恢复共用预算。
116
+ 输入 `/mcp` 选一个服务器,会看到和 Claude Code 一样的详情(状态、登录、协议版本、URL、配置文件位置、能力、工具数)和操作:View tools、Re-authenticate、Clear authentication、Reconnect、Disable。也可以直接敲 `/mcp tools|auth|clear-auth|reconnect|disable|enable <名字>`。
128
117
 
129
- Jev 初筛要求每条阻断规则和不确定性概率均 `≤ 0.05`,否则交给模型复核一次,
130
- 不会直接因初筛分数弹人工审批。历史动作有缺失时也转模型。这个预览阈值尚未经过
131
- 真实数据校准,不代表整体误放率;新策略仍需显式选择,默认保留旧 `scores` 策略。
132
- Jev 额度耗尽或出错会提示并在本会话回退到模型。使用 `/auto debug on` 可记录两阶段
133
- 实际上下文、分流原因与最终决定,详见 [策略设计及评测](docs/auto-mode-redesign.md)。
118
+ ### 非交互模式
134
119
 
135
- ### Existing score policy (`scores`, default)
120
+ ```bash
121
+ npx agent-from-zero -p "总结当前改动"
122
+ cat error.log | npx agent-from-zero -p "分析这份日志"
123
+ npx agent-from-zero -p "总结当前改动" --output-format json
124
+ ```
136
125
 
137
- 启动时加 `--auto`,或在会话中输入 `/auto` 开关自动审核。默认关闭;开启后,审核通过的工具操作自动执行,有风险、判断不确定或审核失败时仍会询问。Ink、readline 和 `-p` 模式共用同一条权限路径,后台 shell 和子 agent 的操作也会经过检查。
126
+ ## 常用命令
138
127
 
139
- 审核后端自动选择:
128
+ 在会话里输入 `/help` 可以看到全部命令,比较常用的有:`/model` `/effort` `/plan` `/auto` `/skills` `/mcp` `/memory` `/compact` `/diff` `/undo` `/resume` `/status` `/cost`。
140
129
 
141
- - 配置了 `JEV_API_KEY` 或 `TYPESAFE_API_KEY`:优先使用 Jev。
142
- - 没有 Jev key:复用主 agent 的 API endpoint 和 key,默认使用当前模型;启动时显示一条 Jev 配置提示,不影响使用。
143
- - 在 `.mini-agent/settings.json` 或全局 settings 中设置 `judge.model`,可以选择同一 vendor 支持的其他审核模型。例如,你的 endpoint 支持 `deepseek-flash` 时可以这样配置:
130
+ ## 开发
144
131
 
145
- ```json
146
- {
147
- "autoMode": { "enabled": true },
148
- "judge": { "model": "deepseek-flash" }
149
- }
132
+ ```bash
133
+ git clone https://github.com/ftfw0621/mini-agent.git && cd mini-agent
134
+ npm install
135
+ cp .env.example .env # 填入 DEEPSEEK_API_KEY
136
+ npm start # 从源码运行
137
+ npm run typecheck # 类型检查
138
+ npm test # 单元测试
139
+ npm run eval # 回归评测
150
140
  ```
151
141
 
152
- 以后在环境变量或 `.env` 中配置 `JEV_API_KEY`,下次启动便会自动切换到 Jev,无须改模型配置。Jev 模型由 `autoMode.model` 配置,默认固定为 `jev-1.13.0`。自动审核会把真实用户请求、启动时的 `AGENT.md`、近期已完成工具调用及待执行工具参数发送到选中的审核服务。工具历史和委派任务只提供上下文;文件附件、hook 输出和模型摘要不会作为用户授权发送。
153
-
154
- Jev 额度或计费受限时会提示并自动切回同一 vendor 的 `judge.model`,未指定则使用当前模型。限流、服务故障也会回退,提示会区分原因;本次会话只提示一次,避免每次调用都重试已用完的额度。此外,授权概率在 `0.5 ≤ authorization < 0.8` 且风险 `≤ 0.2` 时,会用该模型对相同上下文复核一次;模型必须明确确认已授权、无需人工审核才放行。这个分支不会停用 Jev,也不会覆盖风险超标或授权概率低于 `0.5` 的判断。复核失败仍需人工确认。
155
-
156
- 硬性拒绝规则始终优先。auto mode 会审核每条 shell 命令,并忽略整类工具的免确认授权;`MINI_AGENT_AUTO_APPROVE` 也不能覆盖审核结果。`-p` 或无人值守的 teammate 无法确认时直接拒绝。实现机制、源码对照和限制见 [Auto mode 说明](docs/auto-mode.md)。
157
-
158
- 审核按实际副作用判断,MCP、API、命令行共用同一策略。任务内的只读查询、明确授权且收件人与内容匹配的普通消息发送可以自动通过;基础设施或运行配置变更、部署、删除数据、修改权限、未知脚本副作用仍需审核。Jev 的门槛为授权 `≥ 0.8` 且风险 `≤ 0.2`(风险问题问的是"能否撤回",阈值由实测数据确定,见 `docs/auto-mode.md`);主模型的布尔判断经同一决策层处理。近期工具返回的小型结构化 JSON 也会作为事实依据发送给审核服务,用于核对名称与 ID 等信息,其中的指令不构成用户授权。
159
-
160
- ## 工具进度、子 agent 和图片
161
-
162
- 输入 `/` 时,输入框下方会列出可用命令及说明,继续输入会过滤。`/effort` 展示当前模型支持的档位,`/skills` / `/skill` 展示可用技能,`/model` 从当前 endpoint 获取模型列表;`/auto debug` 和 `/mcp` 也支持参数补全。用 `↑` / `↓` 选择、`Tab` 补全、`Enter` 确认,`Esc` 收起并保留输入。长列表滚动展示,不会撑满终端。
163
-
164
- `/effort high` 设置当前模型的推理强度,`/effort default` 恢复服务商默认值。选择按模型和 endpoint 区分,仅在本次进程中保留。自定义网关需明确配置支持的档位,见 [effort 配置](docs/effort.md)。
165
-
166
- 默认交互界面把连续同类工具调用合并成实时摘要:高亮动作,下面灰字显示结果和耗时。工具执行、模型思考和回答期间都有动画,不再逐轮追加 `thought for…` 和工具计数。最终回答正常保留;审批、拒绝、错误和 Jev 回退提示仍然可见。
167
-
168
- | 操作 | 快捷键 |
169
- |---|---|
170
- | 展开或收起工具参数与结果 | `Ctrl+T` |
171
- | 展开或收起模型思考 | `Ctrl+R` |
172
- | 翻阅工具或思考详情 | `↑` / `↓` 或 `PgUp` / `PgDn` |
173
- | 进入 agent 列表 | `Tab`,或输入框开头的 `←` |
174
- | 选择 agent,查看实时过程 | `↑` / `↓`,`Enter` |
175
- | 翻阅 agent 过程 | `PgUp` / `PgDn` |
176
- | 返回主界面;无面板时中断任务 | `Esc` |
177
- | 粘贴剪贴板图片或文字 | `Ctrl+V` |
178
- | 工作中插入补充消息 | 输入后按 `Enter`,在下一个工具边界送入 |
179
- | 中断当前步骤并优先处理补充消息 | `Ctrl+Enter`;已有排队消息时也可按 `Esc` |
180
-
181
- 工作中的补充消息先以灰字显示在待处理区,送入模型时转为正常用户消息。支持多条消息和图片,保持发送顺序;当前已执行的操作不会因此撤销,尚未启动的旧工具调用会跳过并按新消息重新规划。`Esc` 优先关闭详情面板。部分终端不能区分 `Ctrl+Enter` 和 `Enter`,此时可先按 `Enter` 排队,再按 `Esc`。
182
-
183
- 审批或问题选项出现时,输入框仍可输入文字;有文字时按 `Enter` 会取消当前选择并发送 follow-up,空输入时按 `Enter` 才确认选项。只有一个问题时,选中答案后按一次 `Enter` 即提交,不再显示额外的 Submit answers;多个问题仍统一提交。
184
-
185
- 粘贴至少 4 行或 1000 字符的文本时,会折成 `[Pasted text #1 +13 lines]` 之类的占位符;长单行文本显示字符数。左右方向键跨过整个占位符,Backspace 整块删除。可以混合输入文字、多个文本块和图片。发送及权限审核收到完整原文,输入历史仍可恢复文本块;补充消息和已发送消息保留简洁展示。
186
-
187
- agent 列表竖排显示 `main`、subagent 和 teammate,右侧显示当前活动、`idle` / `done` / `failed`、耗时及输出 token。带 `~` 的 token 是流式估算;API 返回用量后使用实报数据。切换视图不会停止其他 agent,也不会把它们的详细过程塞进主对话。
188
-
189
- 粘贴图片后,输入框显示 `[Image #1]`;可以继续输入说明或粘贴多张图片。光标紧跟占位符时按 Backspace 可以删除附件。每条消息最多 4 张、每张最多 5 MB。发送时使用真实图像数据,当前模型须同时支持图片和工具调用;模型拒绝图片时会提示检查限制或用 `/model` 切换。图片随会话保存,恢复会话和后续追问可以继续引用。
190
-
191
- 剪贴板通过 `ClipboardSource` 接口接入:macOS 使用 AppKit,Windows 使用 PowerShell/Windows Forms,Linux Wayland 使用 `wl-paste`,X11 使用 `xclip`(Linux 需安装相应工具)。界面和图像消息构造不依赖操作系统。以上交互适用于默认 Ink 界面;`MINI_AGENT_NO_INK=1` 保留文字版 readline 界面。实现对照和验证范围见 [交互界面说明](docs/interactive-ui.md)。
192
-
193
- ## 进度
194
-
195
- | Tag | What it adds |
196
- |---|---|
197
- | `day1` | ~50-line raw tool-calling loop: one tool, one loop, two safety nets (errors fed back, a round cap) |
198
- | `day2` | The five core tools (read/write/edit/search/bash): read-before-edit, `old_string` uniqueness, a `dispatch` that never throws, the four-part tool manual |
199
- | `day3` | Main loop as a state machine: error classification, exponential backoff (with jitter), dual retry budgets, a circuit breaker, 8 terminal states each with a plain-English note, graceful Ctrl+C exit |
200
- | `day4` | Permission system (allow/ask/deny): hard-blocked no-fly zones (.git/.ssh/.env), deny always wins (even bypass can't override), fail-closed when non-interactive, bash risk graded by the command, secret files kept out of context |
201
- | `day5` | Context management: token estimation (byte-based, err high), an 80% trigger line, a 6-section structured summary (tools double-disabled), file recovery (last 5, re-read from disk), proactive + reactive compaction, a compaction circuit breaker |
202
- | `day6` | Multi-turn REPL (line-queue input, `/clear`, three-state Ctrl+C), a system-prompt constitution (number anchors / negative prohibitions / no false claims), AGENT.md project memory, streaming output + a dual watchdog (90s stall / 30s warn-only) |
203
- | `day7` | Sub-agents (the `task` tool: isolated context, one level deep with no nesting, the report is material not the verdict, file-state snapshot isolation) + a minimal eval (10 fixed tasks, evidence-based scoring, 8/10 pass bar, `npm run eval`) |
204
- | `day8` | Packaging & release: a `bin` entry + tsc build, one-line `npx` launch (package `agent-from-zero`); built-in .env loading; a plain-English error when the key is missing |
205
- | `day9` | The tutorial itself: 11 Chinese chapters open-sourced into [docs/](docs/README.md) |
206
- | `day10` | Bring your own model: three env vars swap in any OpenAI-compatible endpoint; `npm run eval` doubles as a model bake-off |
207
- | `day11` | Industrial-grade CLI surface: two-layer settings.json (user permission rules, deny always wins), slash commands (/help /model /compact), `-p` one-shot mode, `--version`, a test suite in the repo (`npm test`, 60 cases) |
208
- | `day12` | Session persistence (`--resume`, snapshot-style + atomic writes) + local telemetry (event JSONL + `/stats`, never leaves the machine, off via `MINI_AGENT_NO_TELEMETRY=1`) |
209
- | `day13` | Async execution: run_bash switched to spawn (no UI freeze, instant Ctrl+C, 30s timeout then SIGKILL), oversized output spilled to disk, read-only tools run in parallel; `npm test` 65 cases |
210
- | `day14` | Hook lifecycle system: PreToolUse (intercept a tool) / PostToolUse (append checks) / SessionStart (inject context) / Stop (test-driven AI), exit-code protocol, stderr fed back to the model; `npm test` 76 cases |
211
- | `day15` | MCP integration: a stdio JSON-RPC client (handshake / line buffering / id pairing), external-server tools registered just like built-ins (same dispatch + permission gate, ask by default); `npm test` 84 cases |
212
- | `day16` | Optional LLM permission judge: only for the ask gray-zone the rules left undecided, can only ask→allow, a rigid prompt format, fail-closed parsing, its own circuit breaker, off by default; `npm test` 98 cases |
213
- | `day17` | Long-term memory: a cross-session MEMORY.md (human-readable) + a remember tool, injected as "background" (vs AGENT.md's "commands"), bounded at 30 entries / 8KB and self-healing; `npm test` 114 cases |
214
- | `day18` | Cost accounting: capture usage from the stream (include_usage), smooth over DeepSeek/OpenAI format differences, pure-function cost + cache-hit-rate math, `/cost` to see spend (the hit rate doubles as a cache-bust warning); `npm test` 130 cases |
215
- | `day19` | Tool-call validation: just-enough schema checks (required / type / shape), pinned at dispatch's single entry, batched precise fix-it guidance, auto-covering built-in + MCP tools (bad args caught before they're sent); `npm test` 146 cases |
216
- | `day20` | Plan mode: read-only research, think it through, hand you a plan to approve; one outer filter in the permission layer (only tightens, never loosens, overrides pre-approvals, keeps no-fly reasons) + an `exit_plan_mode` tool that borrows the ask flow to close approval + a `/plan` toggle; `npm test` 167 cases |
217
- | `day21` | Diff preview: before a write, show the real change (green +/red -) for you to approve; a ~dozen-line LCS line-diff, collapsed unchanged lines, side-effect-free replay of tool semantics, never throws (fails to null); printed after the gate, before execution; `npm test` 189 cases |
218
- | `day22` | `/undo` reverts the last write: stash a "before" snapshot ahead of each write (a bounded stack), `/undo` restores the content or deletes a newly created file, reusing the Day 21 diff to draw the undo; `/clear` also clears the stack; `npm test` 204 cases |
219
- | `day23` | Sub-agent model tiering: a `subAgentModel` config runs delegation on a different tier (cheap model for chores / strong model as a reviewer), a pure `subAgentModelFor` defines the fallback, first end-to-end proof via a fake client that "orchestrate→delegate→regress" really switched models; `npm test` 212 cases |
220
- | `day24` | Remote MCP (HTTP/SSE): peel Day 15's transport out of the protocol into a Transport interface, add an HTTP transport (POST → parse JSON/SSE by Content-Type, carry Mcp-Session-Id, AbortSignal timeout); set `url` to go remote; zero changes to the protocol / discovery / registration; `npm test` 218 cases |
221
- | `day25` | `/diff` session change overview: alongside the undo ledger, a "session-start baseline" table (each file as first touched, recorded once), `/diff` compares each against disk, reuses Day 21 to render net changes, skips net-zero, three states created/modified/deleted; `npm test` 227 cases |
222
- | `day26` | `@file` mentions: an `@path` in your question attaches the file's content to the message, saving a read_file round-trip; recognizes `@` at line start / after whitespace, strips trailing punctuation; each mention passes the read_file gate — secret files (.env/*.pem) are refused, refusals voiced, misses silent; `npm test` 244 cases |
223
- | `day27` | `/resume` session picker: `session.ts` gains listSessions (summaries) / loadSession (by id) / sessionTitle (first line as title, strips @file attachments), a REPL list + pick-by-number + wholesale switch (rebuild the constitution, swap the id, clear read-state and the undo stack), sessions dir made lazy for testability; `npm test` 256 cases |
224
- | `day28` | Terminal UI: a new `ui.ts` owns the look — a rounded welcome box, the `❯` prompt, `⏺`/`⎿` activity markers (replacing emoji), a ticking spinner (riding the watchdog); explains why "color codes pollute width math" so you align on plain text first, then color; `npm test` 277 cases |
225
- | `day29` | Arrow-key approval menu: approval moves from "type y/N" to ↑↓ select (`menu.ts` borrows stdin into raw mode, redraws in place, hands it back), numbered + a footer hint; the middle "don't ask again this session" reuses the allow rule (run_bash only escalates ask, never touches hard deny); non-TTY falls back to typing; `npm test` 289 cases |
226
- | `day30` | Multi-question form: give the model an `ask_user` tool that pops a form when it needs your decision (`form.ts` pure state machine: flat cursor + reduce + submit validation + jump-to-gap, `promptForm` a thin keyboard layer), collected into question→answer fed back; the input area becomes Claude-Code-style "rule + ❯"; `npm test` 309 cases |
227
- | `day31` | Switch models at runtime: `/model` upgrades from read-only to switchable — `/model <name>` sets it, bare `/model` pulls the endpoint's `/models` list into the Day 29 menu; the switch only changes `CONFIG.model` and takes effect immediately via the loop re-reading it each round; `npm test` 314 cases |
228
- | `day32` | Make a model switch visible: the streaming loop also reads `reasoning_content`, streaming a reasoning model's (R1) thinking dimmed (render-only, never into history, never sent back); the spinner shows the current model as proof the switch took; `npm test` 324 cases |
229
- | `day33` | Skill system (Markdown as a plugin): a `SKILL.md` (frontmatter + body) = one plugin, executed by the model; progressive disclosure (the listing in context / the body loaded on demand), the `skill` tool manual is the listing, `disableModelInvocation` splits model-callable vs user-only, `/skills` `/skill <name>`; `npm test` 360 cases |
230
- | `day34` | Deeper memory: four types (user/feedback/project/reference, feedback the most valuable), auto-extraction each turn (a cheap model reads the conversation and stores "what the user corrected", off by default, never throws), grouped injection by type, backward-compatible with the old format; `npm test` 375 cases |
231
- | `day35` | Deeper hooks: 4→10 events (adding UserPromptSubmit/SessionEnd/Pre·PostCompact/Subagent*), config becomes an open record, exit-2 meaning interpreted per-event by the caller, an extra-short SessionEnd timeout; **`PreToolUse` can rewrite a tool's arguments** (the "side road" the three-state permission can't do, strictly after the safety check); `npm test` 381 cases |
232
- | `day36` | TodoWrite: a `todo_write` tool that lets the agent keep its own checklist (whole-list replace, at most one in_progress, ✓/▶/○ render, `/todos` to view) — planning, not new powers; plus a nag that injects a reminder when an unfinished plan goes stale for N rounds, so long tasks don't drift off the original goal; `npm test` 474 cases |
233
- | `day37` | Background tasks: a `run_bash_background` tool spawns a slow command (install/build/test/dev server) and returns a `bg_*` id immediately instead of blocking on run_bash's 30s watchdog — the agent keeps reasoning while it runs; when it finishes the loop injects a `<task_notification>` as the next turn (once per task), a `bash_output` tool polls a job's new output (essential for servers that never "finish"), `/bg` lists them and they're SIGKILLed on exit; same permission gate as run_bash, so the danger rules are identical; `npm test` 502 cases |
234
- | `day38` | Agent teams: past the one-shot sub-agent — a `spawn_teammate` tool starts PERSISTENT teammates that run concurrently (fire-and-forget async loops interleaving on the one event loop) and coordinate through `send_message`; mailboxes are real `.jsonl` files on disk (`.mini-agent/team/`, consumptive read = observable, no lock needed on a single event loop), the loop injects each agent's inbox as a turn and the lead WAITS on the team instead of ending early, teammates are bounded (round cap) with a focused toolset and a non-interactive permission policy (writes auto-proceed, risky bash declined, deny still wins), `/team` lists them; `npm test` 523 cases |
235
- | `day39` | Team protocols: explicit message contracts, not vibes — every protocol is the same request→response correlated by a `request_id`, tracked as `ProtocolState` (pending→approved/rejected) with a `match_response` that rejects a mismatched reply (a shutdown_response can't approve a plan). Two contracts: **graceful shutdown** (lead `request_shutdown` → teammate finishes, confirms, exits — no orphans/half-writes) and **plan approval** (teammate `submit_plan` → lead `review_plan` before risky work runs). Teammates go from bounded auto-exit to an IDLE LOOP (wait for work, leave on the shutdown handshake; bounded backstop so an abandoned one self-exits), and the lead disbands the team gracefully when it finishes; the inbox-consume step routes protocol messages then hands the rest to the model; `npm test` 545 cases |
236
- | `day40` | Autonomous agents: a shared TASK BOARD teammates pull from instead of being hand-fed work. `.mini-agent/tasks/<id>.json` files (on disk = observable), `Task {status, owner, blockedBy}` with `blockedBy` resolved by id OR subject; `create_task`/`list_tasks`/`claim_task`/`complete_task` tools, and `claimTask` is a synchronous read-check-write so two scanners can't both win the same task — no lock, same single-event-loop argument as the mailbox. The teammate IDLE loop now polls inbox-first (shutdown priority) then auto-claims the next ready task (`claimNextAvailable`) and resumes WORK; the lead lays out a dependency graph and teammates respect it (schema → routes → tests), `/tasks` shows the board; `npm test` 574 cases |
237
-
238
- ## 📖 配套教程(免费开源)
239
-
240
- **[完整教程在 docs/ 目录](docs/README.md)**——主线 + 进阶共三十多章,每章对应一个 tag:为什么这么写、Claude Code 原版怎么做、你会踩的坑。每章五段结构:做完长什么样 → 跟着写 → 为什么 → 坑 → 小结。
241
-
242
- 想看更深的「为什么」:公众号的 **Harness Engineering 拆书系列**(30+ 篇)逐章拆了《Harness Engineering:从 Claude Code 看 AI 编码工程》——本仓库每个设计决策背后的原理、数据和事故案例都在那里。公众号 @老李是谁。
243
-
244
- 如果这个项目帮你搞懂了 agent,点个 ⭐ 就是最大的支持。
142
+ 各个功能的实现思路见 [docs/](docs/)。
package/dist/agent.js CHANGED
@@ -8,12 +8,12 @@ import { CONFIG, requireApiKey, saveGlobalSetting, PROJECT_SETTINGS_PATH, GLOBAL
8
8
  import { runLoop, TerminateReason, MAX_RETRIES, killAllSubAgents } from "./loop.js"; // the state machine
9
9
  import { buildSystemMessage, readProjectInstructions } from "./prompt.js"; // the constitution + optional AGENT.md project memory
10
10
  import { forgetFilesExcept, registerExternalTool } from "./tools.js"; // file-state reset + tool registration
11
- import { compactHistory, estimateHistoryTokens, COMPACT_AT } from "./context.js"; // for the manual /compact command
11
+ import { compactHistory, compactThreshold, contextPercent, contextWindowFor } from "./context.js"; // /compact, /model info, the ctx % in the status line
12
12
  import { newSessionId, saveSession, latestSession, listSessions, loadSession, setSessionTitle } from "./session.js"; // conversation persistence (project-local) + the /resume picker
13
13
  import { generateSessionTitle, setTerminalTitle } from "./title.js"; // concise session name, generated after the first message + the terminal tab that shows it
14
14
  import { initTelemetry, emit, statsReport } from "./telemetry.js"; // local-only event log + /stats
15
15
  import { runHooks } from "./hooks.js"; // SessionStart lifecycle hook
16
- import { connectMcpServers, listMcpServers, mcpActionsFor, runMcpAction } from "./mcp.js"; // external tool servers (MCP) + /mcp
16
+ import { connectMcpServers, listMcpServers, MCP_SUBCOMMANDS, mcpActionsFor, mcpServerDetails, reloadMcpServers, runMcpAction, watchMcpConfig } from "./mcp.js"; // external tool servers (MCP) + /mcp
17
17
  import { Judge } from "./judge.js"; // optional LLM permission classifier
18
18
  import { AutoMode } from "./auto.js";
19
19
  import { effortMenu, setEffort, selectedEffort } from "./effort.js";
@@ -33,7 +33,7 @@ import { promptSelect, promptForm } from "./menu.js"; // arrow-key approval menu
33
33
  import { editLine } from "./editor.js"; // our own line editor (keeps the status footer pinned even when input wraps)
34
34
  import { normalizeDroppedPaths } from "./drop.js"; // drag-and-drop: a dropped file's path → a clean absolute path in the input
35
35
  import { rememberTool, readMemory, readMemoryTyped, extractMemories, MEMORY_PATH } from "./memory.js"; // long-term project memory + auto-extract
36
- import { loadSkills, buildSkillTool, findSkill, skillInstructions } from "./skills.js"; // Markdown-as-plugin skills
36
+ import { currentSkills, findSkill, userSkillMessages } from "./skills.js"; // Markdown-as-plugin skills
37
37
  import { initCostMeter, DEFAULT_PRICING } from "./cost.js"; // token & cost accounting for /cost
38
38
  import { launchInk } from "./ink/launch.js"; // the Ink REPL — the default front-end for interactive sessions
39
39
  import { listBackground, hasRunningBackground, killAllBackground } from "./background.js"; // background tasks: /bg view + kill-on-exit (Day 37)
@@ -96,7 +96,8 @@ const SESSION_HELP = `commands:
96
96
  /memory show the durable facts the agent remembers about this project
97
97
  /status session usage and vendor account balance/cost
98
98
  /cost tokens, cache hit rate and estimated spend this session (local)
99
- /mcp list configured MCP servers + status; select one to authenticate / reconnect / disable
99
+ /mcp list configured MCP servers + status; select one to view tools / authenticate / clear auth / reconnect / disable
100
+ /mcp reload re-read mcpServers from settings.json (saves are also picked up automatically)
100
101
  /plan toggle plan mode — research-only; the agent presents a plan you approve before any change
101
102
  /effort [level] list/select the current model’s supported reasoning effort
102
103
  /auto toggle auto mode — risky or uncertain actions still require approval
@@ -110,7 +111,7 @@ const SESSION_HELP = `commands:
110
111
  /diff show every file changed this session, as a diff from where it started
111
112
  /resume list recent sessions in this project and continue one of them
112
113
  /skills list the reusable skills available in this project
113
- /skill <name> run a skill yourself (works even for user-only skills)
114
+ /skill <name> [args] run a skill yourself (works even for user-only skills); /<name> [args] works too
114
115
  exit leave (Ctrl+C at the prompt does the same)
115
116
 
116
117
  keys (at the prompt):
@@ -197,6 +198,8 @@ async function main() {
197
198
  disconnectMcp();
198
199
  };
199
200
  process.on("exit", disconnectMcp); // best-effort cleanup of server subprocesses
201
+ if (!cli.print)
202
+ process.on("exit", watchMcpConfig()); // hot reload: a settings.json save adds/removes/reconnects servers, no restart (a one-shot -p run doesn't need it)
200
203
  process.on("exit", killAllBackground); // Day 37: SIGKILL any background job (dev server, slow install) so it never outlives the agent as an orphan
201
204
  process.on("exit", killAllSubAgents); // mark any still-running sub-agents as killed so they don't leave pending promises
202
205
  process.on("exit", stopCronScheduler); // Day s14: stop the setInterval on exit
@@ -219,14 +222,12 @@ async function main() {
219
222
  const memCount = readMemory().length;
220
223
  if (memCount)
221
224
  console.log(chalk.dim(`(long-term memory: ${memCount} facts loaded)`));
222
- // Skills: reusable Markdown procedures. The model-invocable ones are exposed
223
- // through a single `skill` tool whose description lists them (progressive
224
- // disclosure); user-only skills are reachable via /skill <name>.
225
- const skills = loadSkills();
226
- if (skills.some((s) => !s.disableModelInvocation))
227
- registerExternalTool(buildSkillTool(skills));
228
- if (skills.length)
229
- console.log(chalk.dim(`(skills: ${skills.length} loaded — ${skills.map((s) => s.name).join(", ")})`));
225
+ // Skills: reusable Markdown procedures, re-read from disk before every model
226
+ // call (so a new SKILL.md needs no restart). The loop registers the `skill`
227
+ // tool and sends the listing as a <system-reminder>; here we just announce them.
228
+ const startupSkills = currentSkills();
229
+ if (startupSkills.length)
230
+ console.log(chalk.dim(`(skills: ${startupSkills.length} loaded — ${startupSkills.map((s) => s.name).join(", ")})`));
230
231
  // Token/cost meter for this session. Prices come from settings, falling back
231
232
  // to the defaults; the loop records usage into it from every stream.
232
233
  const costMeter = initCostMeter({
@@ -476,7 +477,7 @@ async function main() {
476
477
  // No name: show the current model and a picker.
477
478
  console.log(chalk.dim(`model: ${CONFIG.model}\n` +
478
479
  (CONFIG.subAgentModel ? `sub-agent model: ${CONFIG.subAgentModel} (delegated task work)\n` : "") +
479
- `endpoint: ${CONFIG.baseURL}\ncontext window: ${CONFIG.contextWindow} tokens (compaction at ~${COMPACT_AT})`));
480
+ `endpoint: ${CONFIG.baseURL}\ncontext window: ${contextWindowFor()} tokens (auto-compact at ~${compactThreshold()})`));
480
481
  if (!process.stdin.isTTY)
481
482
  return; // no picker without a TTY — switch with: /model <name>
482
483
  const models = await listModels();
@@ -493,12 +494,18 @@ async function main() {
493
494
  apply(models[choice]); // picker is session-only too; "/model save <name>" to persist
494
495
  };
495
496
  // /mcp — list configured MCP servers + status, then manage one (like Claude Code).
496
- const mcpStatusText = (status) => status === "connected" ? chalk.green("connected") : status === "needs-auth" ? chalk.yellow("needs auth") : status === "failed" ? chalk.red("failed") : chalk.dim("disabled");
497
+ const mcpStatusText = (status) => status === "connected" ? chalk.green("connected") : status === "needs-auth" ? chalk.yellow("needs auth") : status === "failed" ? chalk.red("failed") : status === "pending" ? chalk.dim("connecting…") : chalk.dim("disabled");
497
498
  const handleMcpCommand = async (arg) => {
498
499
  const parts = arg.trim().split(/\s+/).filter(Boolean);
499
- if (parts.length >= 2 && ["reconnect", "auth", "authenticate", "enable", "disable"].includes(parts[0])) {
500
+ // /mcp reload — re-read settings.json now (the file watcher normally does this on save).
501
+ if (parts[0] === "reload") {
502
+ const changes = await reloadMcpServers();
503
+ console.log(chalk.dim(changes.length ? `(mcp: ${changes.join(", ")})` : "(mcp: no changes in settings.json)"));
504
+ return;
505
+ }
506
+ if (parts.length >= 2 && MCP_SUBCOMMANDS[parts[0]]) {
500
507
  const name = parts.slice(1).join(" ");
501
- const action = parts[0] === "auth" || parts[0] === "authenticate" ? "authenticate" : parts[0];
508
+ const action = MCP_SUBCOMMANDS[parts[0]];
502
509
  try {
503
510
  console.log(chalk.dim(await runMcpAction(name, action, (url) => console.log(chalk.dim(`authenticate in your browser: ${url}`)))));
504
511
  }
@@ -524,6 +531,7 @@ async function main() {
524
531
  return;
525
532
  }
526
533
  const server = servers[choice];
534
+ console.log(`\n${chalk.bold(`${server.name} MCP Server`)}\n\n${mcpServerDetails(server)}\n`); // Claude Code's server screen
527
535
  const actions = mcpActionsFor(server);
528
536
  const actionChoice = await select(actions.map((a) => a.label));
529
537
  if (actionChoice < 0) {
@@ -580,10 +588,13 @@ async function main() {
580
588
  await handleModelCommand(line.slice("/model".length).trim());
581
589
  return true;
582
590
  }
583
- // /skill <name> runs a skill on the user's behalf — inject its instructions
584
- // as a turn so the model follows them. Works for user-only skills too.
591
+ // /skill <name> [args] runs a skill on the user's behalf. Like Claude Code it
592
+ // sends two user messages: a <command-name> marker (the model then knows the
593
+ // skill is already loaded) and the body. Works for user-only skills too.
585
594
  if (line === "/skill" || line.startsWith("/skill ")) {
586
- const name = line.slice("/skill".length).trim();
595
+ const skills = currentSkills(); // live — a SKILL.md added mid-session is found
596
+ const [name = "", ...rest] = line.slice("/skill".length).trim().split(/\s+/);
597
+ const args = rest.join(" ");
587
598
  if (!name) {
588
599
  console.log(chalk.dim(skills.length ? `usage: /skill <name> — available: ${skills.map((s) => s.name).join(", ")}` : "(no skills in this project — add one at .mini-agent/skills/<name>/SKILL.md)"));
589
600
  return true;
@@ -593,8 +604,9 @@ async function main() {
593
604
  console.log(chalk.yellow(`(no skill named "${name}")`));
594
605
  return true;
595
606
  }
596
- messages.push({ role: "user", content: `Run the "${s.name}" skill.\n\n${skillInstructions(s)}` });
597
- autoMode.recordRequest(`Run the "${s.name}" skill.`);
607
+ for (const content of userSkillMessages(s, args))
608
+ messages.push({ role: "user", content });
609
+ autoMode.recordRequest(`/${s.name}${args ? ` ${args}` : ""}`);
598
610
  console.log(chalk.dim(`(running skill: ${s.name})`));
599
611
  running = true;
600
612
  interrupted = false;
@@ -606,13 +618,14 @@ async function main() {
606
618
  console.log(chalk.yellow(`\n⚠️ ${EXIT_NOTES[r.reason]}`));
607
619
  return true;
608
620
  }
609
- // /mcp [reconnect|auth|enable|disable <name>]
621
+ // /mcp [reload | reconnect|auth|enable|disable <name>]
610
622
  if (line === "/mcp" || line.startsWith("/mcp ")) {
611
623
  await handleMcpCommand(line.slice("/mcp".length).trim());
612
624
  return true;
613
625
  }
614
626
  switch (line) {
615
627
  case "/skills": {
628
+ const skills = currentSkills();
616
629
  if (!skills.length) {
617
630
  console.log(chalk.dim("(no skills — add one at .mini-agent/skills/<name>/SKILL.md or ~/.config/mini-agent/skills/)"));
618
631
  return true;
@@ -819,6 +832,8 @@ async function main() {
819
832
  return true;
820
833
  }
821
834
  default:
835
+ if (line.startsWith("/") && findSkill(currentSkills(), line.slice(1).split(/\s+/)[0]))
836
+ return handleCommand(`/skill ${line.slice(1)}`); // /<skill-name> [args], like Claude Code
822
837
  if (line.startsWith("/")) {
823
838
  console.log(chalk.dim(`unknown command: ${line} — try /help`)); // typo guard
824
839
  return true; // still consumed — don't send typos to the model
@@ -857,7 +872,7 @@ async function main() {
857
872
  // input stays clean.
858
873
  let footer;
859
874
  if (process.stdin.isTTY) {
860
- const ctxPct = Math.min(100, Math.round((estimateHistoryTokens(messages) / CONFIG.contextWindow) * 100));
875
+ const ctxPct = contextPercent(messages); // % of the way to auto-compaction
861
876
  console.log(""); // one blank line separates the previous answer from the prompt
862
877
  footer = statusLine(CONFIG.model, path.basename(process.cwd()), gitBranch(), ctxPct, costMeter.cost(), Date.now() - sessionStartedAt); // status pinned below the prompt
863
878
  }
@@ -58,7 +58,7 @@ export function slashCompletions(input, ctx) {
58
58
  ], input.trimEnd()) };
59
59
  }
60
60
  if (command === "/mcp") {
61
- const items = [{ value: "/mcp", description: "Open MCP server menu" }, ...["reconnect", "auth", "enable", "disable"].flatMap((action) => ctx.servers.map((name) => ({ value: `/mcp ${action} ${name}`, description: `${action} ${name}` })))];
61
+ const items = [{ value: "/mcp", description: "Open MCP server menu" }, { value: "/mcp reload", description: "Re-read MCP servers from settings.json" }, ...["tools", "auth", "clear-auth", "reconnect", "enable", "disable"].flatMap((action) => ctx.servers.map((name) => ({ value: `/mcp ${action} ${name}`, description: `${action} ${name}` })))];
62
62
  return { items: filter(items, input.trimEnd()) };
63
63
  }
64
64
  return { items: filter(COMMANDS), hint: "Commands" };
package/dist/config.js CHANGED
@@ -40,6 +40,39 @@ function readSettings(p) {
40
40
  }
41
41
  const globalSettings = readSettings(GLOBAL_SETTINGS_PATH); // the user's defaults
42
42
  const projectSettings = readSettings(PROJECT_SETTINGS_PATH); // this project's rules
43
+ // Re-read ONLY the mcpServers map from both layers, mid-session (the MCP hot
44
+ // reload in mcp.ts). Unlike startup, a file that won't parse is NOT fatal here:
45
+ // an editor saving in two steps can leave half-written JSON for a moment, and
46
+ // killing a live session over that would be absurd. Return null instead — the
47
+ // caller keeps the servers it already has and waits for the next save.
48
+ export function readMcpServers() {
49
+ const layers = [];
50
+ for (const p of [GLOBAL_SETTINGS_PATH, PROJECT_SETTINGS_PATH]) {
51
+ if (!fs.existsSync(p)) {
52
+ layers.push({});
53
+ continue;
54
+ }
55
+ try {
56
+ layers.push(JSON.parse(fs.readFileSync(p, "utf8")));
57
+ }
58
+ catch {
59
+ return null; // unparseable right now — don't guess, keep what is running
60
+ }
61
+ }
62
+ return { ...(layers[0].mcpServers ?? {}), ...(layers[1].mcpServers ?? {}) }; // same merge as startup: project wins
63
+ }
64
+ // Which settings file defines this MCP server (the /mcp panel's "Config
65
+ // location"). Project entries override global ones, so check the project first.
66
+ export function mcpConfigPath(name) {
67
+ try {
68
+ if (JSON.parse(fs.readFileSync(PROJECT_SETTINGS_PATH, "utf8")).mcpServers?.[name])
69
+ return PROJECT_SETTINGS_PATH;
70
+ }
71
+ catch {
72
+ /* no project settings (or unreadable) — it's global */
73
+ }
74
+ return GLOBAL_SETTINGS_PATH;
75
+ }
43
76
  // Provider-agnostic configuration. Resolution order, most specific wins:
44
77
  // env MINI_AGENT_* > project settings > global settings > legacy DEEPSEEK_* env > defaults
45
78
  export const CONFIG = {
@@ -51,12 +84,14 @@ export const CONFIG = {
51
84
  apiKey: process.env.MINI_AGENT_API_KEY || // generic key wins...
52
85
  process.env.DEEPSEEK_API_KEY || // ...over the back-compat one
53
86
  "", // empty = missing; requireApiKey() turns that into a helpful error
54
- model: process.env.MINI_AGENT_MODEL || projectSettings.model || globalSettings.model || "deepseek-chat", // must support function calling
87
+ model: process.env.MINI_AGENT_MODEL || projectSettings.model || globalSettings.model || "deepseek-flash", // must support function calling
55
88
  effortProfiles: { ...(globalSettings.effortProfiles ?? {}), ...(projectSettings.effortProfiles ?? {}) },
56
89
  // Sub-agents (the task tool) can run on a DIFFERENT model than the orchestrator:
57
90
  // a cheap/fast one for grunt work (reading many files, broad search), or a
58
91
  // strong one as an "advisor" to double-check. undefined → use the main model.
59
92
  subAgentModel: process.env.MINI_AGENT_SUBAGENT_MODEL || projectSettings.subAgentModel || globalSettings.subAgentModel || undefined,
93
+ // Per-model windows (context.ts contextWindowFor): project entries override global ones.
94
+ contextWindows: { ...(globalSettings.contextWindows ?? {}), ...(projectSettings.contextWindows ?? {}) },
60
95
  contextWindow: Number(process.env.MINI_AGENT_CONTEXT_WINDOW) || // env override first
61
96
  projectSettings.contextWindow || // then files
62
97
  globalSettings.contextWindow ||