agent-from-zero 0.35.1 → 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 +141 -0
- package/dist/agent.js +39 -24
- package/dist/completion.js +1 -1
- package/dist/config.js +36 -1
- package/dist/context.js +46 -6
- package/dist/ink/app.js +103 -25
- package/dist/ink/chat.js +3 -1
- package/dist/ink/commands.js +5 -2
- package/dist/ink/setup.js +15 -15
- package/dist/loop.js +34 -8
- package/dist/mcp.js +332 -16
- package/dist/skills.js +189 -41
- package/dist/ui.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1 +1,142 @@
|
|
|
1
1
|
# mini-agent
|
|
2
|
+
|
|
3
|
+
一个照着 Claude Code 做出来的命令行 coding agent,TypeScript 实现,默认接 DeepSeek(任何支持 function calling 的 OpenAI 兼容模型都能换上)。
|
|
4
|
+
|
|
5
|
+
## 这是什么
|
|
6
|
+
|
|
7
|
+
mini-agent 是我模仿 Claude Code 自己搓的。从终端 UI 的样子,到背后的各种机制,基本都是照着 Claude Code 的做法来实现的:
|
|
8
|
+
|
|
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
|
+
|
|
15
|
+
这个版本是我自己一直在 dogfooding、边用边改出来的,到现在已经挺顺手了。我把自己工作用的 **Slack** 和 **Linear** MCP 都接了进来,底层模型用的是 DeepSeek。平时主要让它干这些活:
|
|
16
|
+
|
|
17
|
+
- 查 bug、看日志、定位问题
|
|
18
|
+
- 回 Slack 消息
|
|
19
|
+
- 跑数据 backfill 之类的脏活
|
|
20
|
+
|
|
21
|
+
## 用下来的感受
|
|
22
|
+
|
|
23
|
+
老实说,跟 Claude Code 比体验还是差一截。但跟我直接用 DeepSeek 自带的 harness 相比,mini-agent 好用太多了。
|
|
24
|
+
|
|
25
|
+
说句题外话:我对 A 社这家公司的人品不太认可,但产品确实做得好。至少从 Claude Code 的体验来看,我觉得它远远好过 Codex 和 DeepSeek 的 harness。所以与其从头去想一个 agent 该长什么样,不如直接照着最好的那个来做。
|
|
26
|
+
|
|
27
|
+
## 安装与配置
|
|
28
|
+
|
|
29
|
+
### 安装
|
|
30
|
+
|
|
31
|
+
需要 Node >= 20.6。不用 clone 仓库,直接用 npx 运行 npm 包(包名是 `agent-from-zero`):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx agent-from-zero
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 配置 API key
|
|
38
|
+
|
|
39
|
+
推荐把 API key 写进 **shell 的启动配置文件**(zsh 是 `~/.zshrc`,bash 是 `~/.bashrc`),这样在任何目录启动 mini-agent 都能用。默认用 DeepSeek,只需要一个 key(在 [platform.deepseek.com](https://platform.deepseek.com) 获取),变量名是 `DEEPSEEK_API_KEY`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# ~/.zshrc 或 ~/.bashrc
|
|
43
|
+
export DEEPSEEK_API_KEY=sk-你的key
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
改完运行 `source ~/.zshrc`(或 `source ~/.bashrc`),或者新开一个终端。
|
|
47
|
+
|
|
48
|
+
如果某个项目想单独用一个 key,也可以在那个目录下放一个 `.env` 文件,写 `DEEPSEEK_API_KEY=sk-...`,不带 `export`。mini-agent 启动时会读当前目录的 `.env`,但**环境变量优先**:shell 配置文件里已经导出的变量,`.env` 里的同名值会被忽略。
|
|
49
|
+
|
|
50
|
+
### 配置审核模型 judge.model
|
|
51
|
+
|
|
52
|
+
开启 auto 模式(会话里输入 `/auto`,或启动时加 `--auto`)后,每个需要审批的工具调用会先交给一个审核模型判断:安全的直接执行,有风险或拿不准的仍然问你。审核模型只有两种选择:
|
|
53
|
+
|
|
54
|
+
1. **主 agent 用的模型**(默认):和主 agent 共用同一个 API key 和 base URL。没配 `judge.model` 就直接用主 agent 当前的模型。
|
|
55
|
+
2. **Jev**:配好 Jev 的 key 后自动优先用它(见下一节)。
|
|
56
|
+
|
|
57
|
+
`judge.model` 写在 `~/.config/mini-agent/settings.json`(全局)或 `.mini-agent/settings.json`(项目级):
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"judge": { "model": "deepseek-flash" }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
注意:`judge.model` 走的是主 agent 的 API 端点,所以只能填主 agent 那个服务商下的模型,不能填别家的模型,也不能在这里填 Jev。不确定的话就不要配,默认用主 agent 的模型最稳妥。
|
|
66
|
+
|
|
67
|
+
### 配置 Jev(可选)
|
|
68
|
+
|
|
69
|
+
**Jev 不是必须的**,不配也能正常使用 auto 模式,审核会用上面说的主 agent 模型。
|
|
70
|
+
|
|
71
|
+
Jev 是 TypeSafe 提供的审核模型。想用的话,和 DeepSeek 的 key 放在同一个地方,加上 Jev 的 key(两个变量名都可以,二选一):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
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
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 换别的模型
|
|
89
|
+
|
|
90
|
+
和 API key 放在同一个地方,设置这三个变量就行,各家的 base URL 写在 [.env.example](.env.example) 里:
|
|
91
|
+
|
|
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
|
+
```
|
|
97
|
+
|
|
98
|
+
模型必须支持 function calling。如果它的上下文窗口比 DeepSeek 小(默认按约 1M 算),在 settings.json 里按模型配置窗口大小,`/model` 切换模型时会跟着变:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"contextWindows": { "gpt-4.1-mini": 1000000, "deepseek-v4-pro": 1000000 }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
也可以用环境变量 `MINI_AGENT_CONTEXT_WINDOW` 统一覆盖所有模型。
|
|
107
|
+
|
|
108
|
+
### 上下文和自动压缩
|
|
109
|
+
|
|
110
|
+
上下文大小用的是 API 每次返回的真实 `prompt_tokens`,再加上之后新增消息的估算值,所以基本是准的。快满时自动压缩:触发点是窗口减去给回复预留的 20k 和 13k 安全余量(约 1M 的窗口大概在 97% 左右)。状态栏的 `ctx N%` 表示离自动压缩还有多远,到 100% 时下一次调用模型前就会先压缩。也可以随时手动 `/compact`。
|
|
111
|
+
|
|
112
|
+
### 配置 MCP
|
|
113
|
+
|
|
114
|
+
在 `~/.config/mini-agent/settings.json`(全局)或 `.mini-agent/settings.json`(项目级)的 `mcpServers` 里添加服务器,写法和 Claude Code 一样。保存后会话里自动生效,不用重启:新加的服务器会连上,删掉的会断开,改过的会重连。也可以手动运行 `/mcp reload`。服务器自己的工具有增减时(MCP 的 `tools/list_changed` 通知),工具列表也会自动更新。启动时 MCP 在后台并行连接,不会卡住输入框,哪个连上了它的工具就马上可用;有连接失败的,状态栏右下角会用红色标出是哪个(需要登录的是黄色)。
|
|
115
|
+
|
|
116
|
+
输入 `/mcp` 选一个服务器,会看到和 Claude Code 一样的详情(状态、登录、协议版本、URL、配置文件位置、能力、工具数)和操作:View tools、Re-authenticate、Clear authentication、Reconnect、Disable。也可以直接敲 `/mcp tools|auth|clear-auth|reconnect|disable|enable <名字>`。
|
|
117
|
+
|
|
118
|
+
### 非交互模式
|
|
119
|
+
|
|
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
|
+
```
|
|
125
|
+
|
|
126
|
+
## 常用命令
|
|
127
|
+
|
|
128
|
+
在会话里输入 `/help` 可以看到全部命令,比较常用的有:`/model` `/effort` `/plan` `/auto` `/skills` `/mcp` `/memory` `/compact` `/diff` `/undo` `/resume` `/status` `/cost`。
|
|
129
|
+
|
|
130
|
+
## 开发
|
|
131
|
+
|
|
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 # 回归评测
|
|
140
|
+
```
|
|
141
|
+
|
|
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,
|
|
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 {
|
|
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
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
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: ${
|
|
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
|
-
|
|
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 =
|
|
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
|
|
584
|
-
//
|
|
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
|
|
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
|
-
|
|
597
|
-
|
|
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 =
|
|
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
|
}
|
package/dist/completion.js
CHANGED
|
@@ -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" }, ...["
|
|
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-
|
|
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 ||
|
package/dist/context.js
CHANGED
|
@@ -2,14 +2,33 @@ import { effortMessages } from "./effort.js";
|
|
|
2
2
|
import fs from "node:fs"; // re-reading files from disk during recovery
|
|
3
3
|
import chalk from "chalk"; // status lines
|
|
4
4
|
import { CONFIG } from "./config.js"; // the resolved context window (env > settings files > default)
|
|
5
|
-
import { recentFiles, forgetFilesExcept } from "./tools.js"; // session file state (what was read/edited, and when)
|
|
5
|
+
import { recentFiles, forgetFilesExcept, toolDefinitions } from "./tools.js"; // session file state (what was read/edited, and when) + the tool manuals we send
|
|
6
6
|
// ---- The context window ------------------------------------------------------
|
|
7
7
|
// The default (1,048,565) came from a real API error message, not from docs.
|
|
8
8
|
// Trust what the API tells you over what you remember reading.
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
9
|
+
//
|
|
10
|
+
// The window belongs to a MODEL, not to the session: /model can switch to one
|
|
11
|
+
// with a smaller window mid-conversation. Resolution, most specific first:
|
|
12
|
+
// env MINI_AGENT_CONTEXT_WINDOW > settings contextWindows[<model>] > settings contextWindow > default
|
|
13
|
+
export function contextWindowFor(model = CONFIG.model) {
|
|
14
|
+
return Number(process.env.MINI_AGENT_CONTEXT_WINDOW) || CONFIG.contextWindows[model] || CONFIG.contextWindow;
|
|
15
|
+
}
|
|
16
|
+
// When to compact, the Claude Code way: not at a fixed percentage, but at
|
|
17
|
+
// "window minus what we must keep free". Two reservations:
|
|
18
|
+
// - room for the model's reply (the compaction call itself writes a summary)
|
|
19
|
+
// - a safety buffer for what we can't count exactly (the tail estimate)
|
|
20
|
+
// On a ~1M window that's ~97% full; on a 128k window it's ~74%, because the
|
|
21
|
+
// same 33k is a bigger slice of a smaller window. The floor keeps a tiny window
|
|
22
|
+
// from getting a threshold at or below zero.
|
|
23
|
+
export const RESERVED_OUTPUT_TOKENS = 20_000; // Claude Code reserves min(max output, 20k)
|
|
24
|
+
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000; // Claude Code's AUTOCOMPACT_BUFFER_TOKENS
|
|
25
|
+
export function compactThreshold(model = CONFIG.model) {
|
|
26
|
+
const env = Number(process.env.MINI_AGENT_COMPACT_AT); // explicit override (tests use it to trigger compaction cheaply)
|
|
27
|
+
if (env)
|
|
28
|
+
return env;
|
|
29
|
+
const window = contextWindowFor(model);
|
|
30
|
+
return Math.max(Math.floor(window * 0.6), window - RESERVED_OUTPUT_TOKENS - AUTOCOMPACT_BUFFER_TOKENS);
|
|
31
|
+
}
|
|
13
32
|
// How many times one query may compact / fail to compact before we give up.
|
|
14
33
|
export const MAX_COMPACTIONS_PER_QUERY = 4; // a query that needs more is too big — stop, don't loop
|
|
15
34
|
export const MAX_COMPACT_FAILURES = 3; // the compaction circuit breaker
|
|
@@ -37,6 +56,27 @@ export function estimateHistoryTokens(messages) {
|
|
|
37
56
|
}
|
|
38
57
|
return total;
|
|
39
58
|
}
|
|
59
|
+
const usageAnchors = new WeakMap();
|
|
60
|
+
export function recordContextUsage(messages, sentCount, usage) {
|
|
61
|
+
const promptTokens = Number(usage?.prompt_tokens ?? 0);
|
|
62
|
+
if (!sentCount || !promptTokens)
|
|
63
|
+
return; // nothing sent, or a provider that doesn't report input tokens
|
|
64
|
+
usageAnchors.set(messages, { last: messages[sentCount - 1], count: sentCount, promptTokens });
|
|
65
|
+
}
|
|
66
|
+
// The tool manuals ride along on every request but aren't messages — a
|
|
67
|
+
// history-only estimate misses them (dozens of MCP tools can be tens of thousands of tokens).
|
|
68
|
+
export const estimateToolTokens = () => estimateTokens(JSON.stringify(toolDefinitions()), true);
|
|
69
|
+
export function contextTokens(messages) {
|
|
70
|
+
const a = usageAnchors.get(messages);
|
|
71
|
+
if (a && messages.length >= a.count && messages[a.count - 1] === a.last)
|
|
72
|
+
return a.promptTokens + estimateHistoryTokens(messages.slice(a.count));
|
|
73
|
+
return estimateHistoryTokens(messages) + estimateToolTokens(); // no (valid) measurement yet — estimate it all
|
|
74
|
+
}
|
|
75
|
+
// For the status bar: how close we are to AUTO-COMPACTION (not to the hard
|
|
76
|
+
// limit). 100% means the next model call compacts first.
|
|
77
|
+
export function contextPercent(messages, model = CONFIG.model) {
|
|
78
|
+
return Math.min(100, Math.round((contextTokens(messages) / compactThreshold(model)) * 100));
|
|
79
|
+
}
|
|
40
80
|
// ---- The summary prompt ---------------------------------------------------------
|
|
41
81
|
// Six numbered sections. Each exists because losing that piece of information
|
|
42
82
|
// causes a specific, real failure mode after compaction (e.g. losing section 4
|
|
@@ -88,7 +128,7 @@ client, // the same client the loop uses
|
|
|
88
128
|
model, // the same model summarizes its own conversation
|
|
89
129
|
signal, // Ctrl+C must abort compaction too
|
|
90
130
|
log = (s) => console.log(s)) {
|
|
91
|
-
const before =
|
|
131
|
+
const before = contextTokens(messages); // for the log line — measured, not just estimated
|
|
92
132
|
log(chalk.magenta(`📦 compacting context (~${before} tokens)...`)); // automatic behavior must be visible
|
|
93
133
|
const res = await client.chat.completions.create({
|
|
94
134
|
model, // same model — no need for a fancier one to summarize
|