mocode-ai 0.1.8 → 0.1.10
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 +65 -52
- package/dist/agent/core.js +8 -4
- package/dist/agent/index.js +5 -10
- package/dist/commands/config.js +1 -1
- package/dist/config/file.js +1 -1
- package/dist/config/index.js +56 -13
- package/dist/context/classifier.js +2 -1
- package/dist/index.js +2 -1
- package/dist/llm/index.js +13 -1
- package/dist/repl/index.js +204 -15
- package/dist/session/drop.js +135 -0
- package/dist/session/index.js +1 -0
- package/dist/tools/builtins/ask-human.js +4 -4
- package/dist/tools/builtins/codegraph.js +5 -3
- package/dist/tools/builtins/drop-context.js +68 -0
- package/dist/tools/builtins/edit-file.js +1 -1
- package/dist/tools/builtins/glob.js +2 -2
- package/dist/tools/builtins/grep.js +2 -2
- package/dist/tools/builtins/index.js +2 -0
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-list.js +1 -1
- package/dist/tools/builtins/memory-save.js +1 -1
- package/dist/tools/builtins/memory-search.js +1 -1
- package/dist/tools/builtins/memory-update.js +1 -1
- package/dist/tools/builtins/read-file.js +2 -2
- package/dist/tools/builtins/run-command.js +1 -1
- package/dist/tools/builtins/switch-mode.js +3 -5
- package/dist/tools/builtins/task.js +4 -5
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/web-fetch.js +1 -1
- package/dist/tools/builtins/web-search.js +1 -1
- package/dist/tools/builtins/write-file.js +1 -1
- package/dist/tools/registry.js +6 -1
- package/dist/ui/intervention.js +0 -11
- package/dist/ui/layout.js +173 -91
- package/dist/ui/prompt.js +2 -14
- package/package.json +40 -40
package/README.md
CHANGED
|
@@ -58,11 +58,13 @@ npm start
|
|
|
58
58
|
mocode config
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
也可直接 `mocode` 进入 REPL 后用 `/model` 命令配置(交互选后端预设 + 逐项填写,即时生效 + 持久化)。未配置时 REPL 仍能打开,会提示你跑 `/model`。
|
|
62
|
+
|
|
61
63
|
也可手写配置文件。mocode 按以下优先级加载(后者覆盖前者,仅回填未设置的环境变量;shell 里 `export` 的永远最优先):
|
|
62
64
|
|
|
63
|
-
1.
|
|
64
|
-
2.
|
|
65
|
-
3. `<cwd>/.
|
|
65
|
+
1. `<cwd>/.env` — 旧用法兼容,优先级最低(源码仓库内有 `.env.example` 可参考)
|
|
66
|
+
2. `~/.mocode/config` — 全局(`/model` 与 `mocode config` 写此文件)
|
|
67
|
+
3. `<cwd>/.mocode/config` — 项目级覆盖,优先级最高
|
|
66
68
|
|
|
67
69
|
必填三项:
|
|
68
70
|
|
|
@@ -74,30 +76,35 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
74
76
|
|
|
75
77
|
常见后端 `base_url`:
|
|
76
78
|
|
|
77
|
-
| 后端
|
|
78
|
-
|
|
79
|
-
| GLM(智谱)
|
|
80
|
-
| DeepSeek
|
|
81
|
-
| Qwen(阿里)
|
|
82
|
-
| 本地 Ollama | `http://localhost:11434/v1`
|
|
83
|
-
| 本地 vLLM
|
|
79
|
+
| 后端 | base\_url |
|
|
80
|
+
| --------- | --------------------------------------------------- |
|
|
81
|
+
| GLM(智谱) | `https://open.bigmodel.cn/api/v3` |
|
|
82
|
+
| DeepSeek | `https://api.deepseek.com` |
|
|
83
|
+
| Qwen(阿里) | `https://dashscope.aliyuncs.com/compatible-mode/v1` |
|
|
84
|
+
| 本地 Ollama | `http://localhost:11434/v1` |
|
|
85
|
+
| 本地 vLLM | `http://localhost:8000/v1` |
|
|
84
86
|
|
|
85
87
|
> 模型必须支持 OpenAI 风格的 function calling,否则工具不会触发。
|
|
86
88
|
|
|
87
89
|
### 可选配置
|
|
88
90
|
|
|
89
|
-
| 环境变量
|
|
90
|
-
|
|
91
|
-
| `MAX_TOKENS`
|
|
92
|
-
| `CONTEXT_WINDOW_TOKENS` | 模型上下文窗口,须对齐真实模型
|
|
93
|
-
| `COMPACT_THRESHOLD`
|
|
94
|
-
| `LLM_STREAM_USAGE`
|
|
95
|
-
| `AUTO_COMPACT`
|
|
96
|
-
| `AUTO_REFLECT`
|
|
97
|
-
| `REFLECT_EVERY_N`
|
|
98
|
-
| `ANYSEARCH_API_KEY`
|
|
99
|
-
| `ANYSEARCH_BASE_URL`
|
|
100
|
-
| `SKILLS_DIRS`
|
|
91
|
+
| 环境变量 | 说明 | 默认值 |
|
|
92
|
+
| ----------------------- | ------------------------------------------ | --------------------------- |
|
|
93
|
+
| `MAX_TOKENS` | 单次回复最大 token | 不限 |
|
|
94
|
+
| `CONTEXT_WINDOW_TOKENS` | 模型上下文窗口,须对齐真实模型 | `128000` |
|
|
95
|
+
| `COMPACT_THRESHOLD` | 自动压缩触发阈值(占窗口比例) | `0.85` |
|
|
96
|
+
| `LLM_STREAM_USAGE` | 流式请求带 `stream_options.include_usage` 拿真实用量 | `true` |
|
|
97
|
+
| `AUTO_COMPACT` | 自动压缩总开关 | `true` |
|
|
98
|
+
| `AUTO_REFLECT` | 后台反思 pass 总开关(定期从会话挖掘记忆) | `true` |
|
|
99
|
+
| `REFLECT_EVERY_N` | 每 N 轮触发一次后台反思(与 agent 并发,不阻塞) | `5` |
|
|
100
|
+
| `ANYSEARCH_API_KEY` | 联网搜索 API key(不配走匿名免费额度) | 无 |
|
|
101
|
+
| `ANYSEARCH_BASE_URL` | 搜索 API 端点 | `https://api.anysearch.com` |
|
|
102
|
+
| `SKILLS_DIRS` | 覆盖默认 skill 扫描目录(平台分隔符) | 三目录自动扫描 |
|
|
103
|
+
| `MOCODE_CONTEXT_OPTIMIZE` | 工具结果进 LLM 前的类型化编码(树/搜索/日志…),关掉则原样进(仅长度裁剪) | `true` |
|
|
104
|
+
| `MAX_STEPS` | 每轮 agent 循环最大步数(防无限循环) | `200` |
|
|
105
|
+
| `SUB_AGENT_MAX_STEPS` | 子 agent(task 工具派生)默认步数上限 | `50` |
|
|
106
|
+
| `SANDBOX_ROOT` | 沙箱根目录(文件操作边界;未配则用 cwd 兜底) | 无 |
|
|
107
|
+
| `MOCODE_THEME` | 颜色主题(default/dark/light…;shell 设置优先于文件) | `default` |
|
|
101
108
|
|
|
102
109
|
## 运行
|
|
103
110
|
|
|
@@ -116,39 +123,45 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
116
123
|
|
|
117
124
|
## 工具
|
|
118
125
|
|
|
119
|
-
| 工具
|
|
120
|
-
|
|
121
|
-
| `read_file`
|
|
122
|
-
| `write_file`
|
|
123
|
-
| `edit_file`
|
|
124
|
-
| `run_command`
|
|
125
|
-
| `glob`
|
|
126
|
-
| `grep`
|
|
127
|
-
| `codegraph`
|
|
128
|
-
| `web_search`
|
|
129
|
-
| `web_fetch`
|
|
130
|
-
| `use_skill`
|
|
131
|
-
| `ask_human`
|
|
132
|
-
| `switch_mode`
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
126
|
+
| 工具 | 作用 |
|
|
127
|
+
| --------------- | -------------------------------------------------------- |
|
|
128
|
+
| `read_file` | 读文件,带行号,支持 `offset` / `limit` |
|
|
129
|
+
| `write_file` | 创建/覆盖文件,自动建父目录 |
|
|
130
|
+
| `edit_file` | 精确字符串替换(`old_string` 须唯一匹配) |
|
|
131
|
+
| `run_command` | 执行 shell 命令,合并 stdout+stderr,默认 120s 超时 |
|
|
132
|
+
| `glob` | 按 glob 模式找文件(排除 node\_modules/.git) |
|
|
133
|
+
| `grep` | 内容正则搜索,纯 JS 实现,不依赖 `rg` |
|
|
134
|
+
| `codegraph` | 已建 `.codegraph/` 索引时,查代码符号源码与调用链(比 read\_file/grep 更准更省) |
|
|
135
|
+
| `web_search` | 联网搜索(AnySearch),返回标题/URL/摘要/正文 |
|
|
136
|
+
| `web_fetch` | 抓取指定 URL,HTML 清洗成纯文本 |
|
|
137
|
+
| `use_skill` | 加载某 skill 的完整 SKILL.md 指令 |
|
|
138
|
+
| `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
|
|
139
|
+
| `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
|
|
140
|
+
| `drop_context` | 把历史里无关的旧工具结果替换为存根释放上下文(保 tool_call_id 配对,不动 system 与当前轮;幂等) |
|
|
141
|
+
| `task` | 派生子 agent 执行独立子任务(独立历史、可受限工具集、可设步数上限);连续多个自动并行,只回摘要 |
|
|
142
|
+
| `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
|
|
143
|
+
| `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
|
|
144
|
+
| `memory_list` | 列记忆索引(id/标题/摘要,无正文) |
|
|
145
|
+
| `memory_update` | 原地改一条记忆(id 不变;纠正过时事实 / 改摘要 / 改 pin) |
|
|
146
|
+
| `memory_forget` | 遗忘记忆:默认归档(可复活),`mode=delete` 硬删(pinned 拒删) |
|
|
139
147
|
|
|
140
148
|
## 斜杠命令
|
|
141
149
|
|
|
142
|
-
| 命令
|
|
143
|
-
|
|
144
|
-
| `/exit` `/quit` | 退出 mocode
|
|
145
|
-
| `/clear`
|
|
146
|
-
| `/context`
|
|
147
|
-
| `/skills`
|
|
148
|
-
| `/compact`
|
|
149
|
-
| `/resume`
|
|
150
|
-
| `/think`
|
|
151
|
-
| `/rollback`
|
|
150
|
+
| 命令 | 作用 |
|
|
151
|
+
| --------------- | -------------------------------------------------- |
|
|
152
|
+
| `/exit` `/quit` | 退出 mocode |
|
|
153
|
+
| `/clear` | 清空历史(保留系统提示)+ 清屏 |
|
|
154
|
+
| `/context` | 显示上下文用量条(token / 消息数 / 估算或实测) |
|
|
155
|
+
| `/skills` | 列出已发现的 skill |
|
|
156
|
+
| `/compact` | 压缩历史(可带焦点 `/compact …`) |
|
|
157
|
+
| `/resume` | 续接已保存的会话 |
|
|
158
|
+
| `/think` | 展开折叠思考段(`/think N`) |
|
|
159
|
+
| `/rollback` | 菜单选轮次回滚(↑↓ · Enter) |
|
|
160
|
+
| `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
|
|
161
|
+
| `/init` | 扫描项目生成 `MOCODE.md` 项目记忆(发给 agent 执行) |
|
|
162
|
+
| `/theme` | 切换颜色主题(↑↓ · Enter,或 `/theme <name>` 直切) |
|
|
163
|
+
| `/plan` | 切到 plan 模式(只读探查 + 产出计划,审批后切 auto 执行) |
|
|
164
|
+
| `/auto` | 切回 auto 模式(全量工具执行) |
|
|
152
165
|
|
|
153
166
|
输入 `/` 触发下拉菜单,继续打字过滤;Esc 取消。
|
|
154
167
|
|
package/dist/agent/core.js
CHANGED
|
@@ -9,7 +9,7 @@ import { chat, planChatTools, } from '../llm/index.js';
|
|
|
9
9
|
import { executeTool } from '../tools/registry.js';
|
|
10
10
|
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
11
11
|
import { getAgentMode, setAgentMode } from './mode.js';
|
|
12
|
-
import { maybeCompact, contextState } from '../session/index.js';
|
|
12
|
+
import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
|
|
13
13
|
import { optimizeToolResult } from '../context/index.js';
|
|
14
14
|
import { config } from '../config/index.js';
|
|
15
15
|
import { jailResolve } from '../sandbox/index.js';
|
|
@@ -107,6 +107,10 @@ export async function runAgentCore(opts) {
|
|
|
107
107
|
const t0 = Date.now();
|
|
108
108
|
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
109
109
|
history.push({ role: 'user', content: userInput });
|
|
110
|
+
// drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
|
|
111
|
+
// 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
|
|
112
|
+
// 子 agent 也在自己的 history 上操作(子 agent 独立 history);skipRollback 不影响此行为。
|
|
113
|
+
const dropContext = (filter) => dropContextFromHistory(history, filter);
|
|
110
114
|
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
111
115
|
let mode = 'idle';
|
|
112
116
|
let gotText = false;
|
|
@@ -202,7 +206,7 @@ export async function runAgentCore(opts) {
|
|
|
202
206
|
while (j < calls.length && READ_TOOL_NAMES.has(calls[j].name))
|
|
203
207
|
j++;
|
|
204
208
|
const batch = calls.slice(i, j);
|
|
205
|
-
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
209
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
|
|
206
210
|
for (let k = 0; k < batch.length; k++) {
|
|
207
211
|
const tc = batch[k];
|
|
208
212
|
hooks.onToolHeader?.(tc);
|
|
@@ -238,7 +242,7 @@ export async function runAgentCore(opts) {
|
|
|
238
242
|
while (j < calls.length && calls[j].name === 'task')
|
|
239
243
|
j++;
|
|
240
244
|
const batch = calls.slice(i, j);
|
|
241
|
-
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
245
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
|
|
242
246
|
// 先批量打印所有头 + 启 spinner(多 task 并发,spinner 只显一个,但 ● 头都打出来)
|
|
243
247
|
for (const tc of batch) {
|
|
244
248
|
hooks.onToolHeader?.(tc);
|
|
@@ -273,7 +277,7 @@ export async function runAgentCore(opts) {
|
|
|
273
277
|
: null;
|
|
274
278
|
const { preWriteOld, editStartLine } = readDiffContext(tc, parsed);
|
|
275
279
|
hooks.onToolStart?.(tc.name);
|
|
276
|
-
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback });
|
|
280
|
+
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext });
|
|
277
281
|
hooks.onToolDone?.();
|
|
278
282
|
hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
|
|
279
283
|
pushToolResult(history, tc, output);
|
package/dist/agent/index.js
CHANGED
|
@@ -55,16 +55,11 @@ onContextUpdate) {
|
|
|
55
55
|
// 开新轮次(回滚用):首行截断 40,供 /rollback 轮次菜单展示。
|
|
56
56
|
beginTurn(truncateDisplay(userInput.split('\n')[0] ?? '', 40));
|
|
57
57
|
layout.contentMode(); // 防御性:运行态光标归输入框光标位供 IME 锚定(enterRunningMode 已置,这里兜底)
|
|
58
|
-
// spinner
|
|
59
|
-
//
|
|
60
|
-
//
|
|
58
|
+
// spinner:状态行最前面转圈(思考中 / 生成 / 执行 工具时,状态栏 lead 位显帧 + 文字)。
|
|
59
|
+
// 经 setStatus 注入状态行(spinnerFrame + statusText),composeStatus 把帧 + 文字放 lead 位;
|
|
60
|
+
// 不画内容区续写位——内容区在等待期间保持干净,首 token 到达即从续写位开始写正文。
|
|
61
61
|
const spinner = new Spinner((msg, frame) => {
|
|
62
|
-
|
|
63
|
-
layout.paintLiveAtCursor(` ${ui.brightMagenta}${frame}${ui.reset} ${ui.dim}${msg}…${ui.reset}`);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
layout.clearLiveAtCursor();
|
|
67
|
-
}
|
|
62
|
+
layout.setStatus(frame ? `${msg}…` : '', frame ?? undefined);
|
|
68
63
|
});
|
|
69
64
|
// lastChar 镜像:core 跟踪流式末字符决定补换行,但 TUI hooks 需读它决定 layout.contentWrite('\n')。
|
|
70
65
|
// core 的 onTextEnd hook 只在 lastChar !== '\n' 时才调,调后置 '\n';镜像与此同步。
|
|
@@ -84,7 +79,7 @@ onContextUpdate) {
|
|
|
84
79
|
lastChar = '\n';
|
|
85
80
|
}
|
|
86
81
|
if (name)
|
|
87
|
-
spinner.start(`生成 ${name}
|
|
82
|
+
spinner.start(`生成 ${name}`);
|
|
88
83
|
},
|
|
89
84
|
onStepStart: () => spinner.start('思考中'),
|
|
90
85
|
onChatDone: () => spinner.stop(),
|
package/dist/commands/config.js
CHANGED
|
@@ -9,7 +9,7 @@ function ask(rl, q) {
|
|
|
9
9
|
* 首跑配置向导:交互填 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL,写 ~/.mocode/config。
|
|
10
10
|
* 只交互三键、保留文件里其它键(MAX_TOKENS / CONTEXT_WINDOW_TOKENS / MOCODE_THEME 等)。
|
|
11
11
|
* prompt 全纯文本(readline 光标按字符算,不能含 ANSI)。
|
|
12
|
-
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(
|
|
12
|
+
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(避免触发 config 单例初始化 / loadEnvFiles,
|
|
13
13
|
* 直接退出);只 import config/file.ts(纯 I/O 叶子,无 env 校验 / process.exit)。
|
|
14
14
|
*/
|
|
15
15
|
export async function runConfigWizard() {
|
package/dist/config/file.js
CHANGED
|
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
/**
|
|
6
6
|
* ~/.mocode/config 全局 dotenv 配置的读写叶子(纯 node:fs / node:os / node:path + dotenv)。
|
|
7
7
|
*
|
|
8
|
-
* 独立于 `config/index.ts`——**不**触发
|
|
8
|
+
* 独立于 `config/index.ts`——**不**触发 config 单例初始化 / loadEnvFiles,故 `commands/config.ts`
|
|
9
9
|
* (首跑向导,刻意不 import config/index.ts)与 `repl/index.ts`(`/theme` 持久化)都能安全共享。
|
|
10
10
|
* 主题等 UI 偏好走 `updateConfigKey` 单键写;向导多键一次性写走 `writeConfigKeys`。
|
|
11
11
|
*/
|
package/dist/config/index.js
CHANGED
|
@@ -4,15 +4,17 @@ import path from 'node:path';
|
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
/**
|
|
6
6
|
* 按优先级加载配置文件并回填 process.env:
|
|
7
|
-
* 候选(
|
|
7
|
+
* 候选(后者覆盖前者,优先级升序):<cwd>/.env(兼容旧用法,最低)→ ~/.mocode/config(全局)→ <cwd>/.mocode/config(项目级覆盖,最高)。
|
|
8
8
|
* 合并后只回填 process.env 里**尚未设置**的键——shell 里 export 的环境变量永远优先。
|
|
9
|
-
* 故 `mocode` 可在任意目录 / 任意终端启动:全局配置(~/.mocode/config)
|
|
9
|
+
* 故 `mocode` 可在任意目录 / 任意终端启动:全局配置(~/.mocode/config)兜底(/model 与 mocode config 写此),
|
|
10
|
+
* 项目级 .mocode/config 按需覆盖全局;旧用法 .env 优先级最低,不再盖过全局 config——
|
|
11
|
+
* 否则 /model 写入 ~/.mocode/config 的 LLM 键会被项目 .env 里的同名旧值盖回。
|
|
10
12
|
*/
|
|
11
13
|
function loadEnvFiles() {
|
|
12
14
|
const candidates = [
|
|
13
|
-
path.join(
|
|
14
|
-
path.join(
|
|
15
|
-
path.join(process.cwd(), '.
|
|
15
|
+
path.join(process.cwd(), '.env'), // 兼容旧用法,优先级最低
|
|
16
|
+
path.join(os.homedir(), '.mocode', 'config'), // 全局(/model 与 mocode config 写此)
|
|
17
|
+
path.join(process.cwd(), '.mocode', 'config'), // 项目级覆盖,优先级最高
|
|
16
18
|
];
|
|
17
19
|
const fromFiles = {};
|
|
18
20
|
for (const p of candidates) {
|
|
@@ -30,14 +32,26 @@ function loadEnvFiles() {
|
|
|
30
32
|
}
|
|
31
33
|
// 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
|
|
32
34
|
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
35
|
+
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
36
|
+
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
37
|
+
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
38
|
+
const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
|
|
39
|
+
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
33
40
|
loadEnvFiles();
|
|
41
|
+
/**
|
|
42
|
+
* 取环境变量;缺则返回空字符串(不退出)。
|
|
43
|
+
* 历史上缺 LLM_BASE_URL/LLM_API_KEY 会 process.exit(1),但 /model 命令已能在 REPL 内配置模型,
|
|
44
|
+
* 故首次未配置也应让 REPL 起来,由开场提示引导用户跑 /model。发消息时 chat() 会抛错被 runTurn catch,不崩。
|
|
45
|
+
*/
|
|
34
46
|
function requireEnv(key) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
return process.env[key] || '';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 模型是否已配置(baseURL + apiKey 非空)。REPL 开场据此决定是否提示 /model。
|
|
51
|
+
* 未配置时 config.model 仍回退 'gpt-4o-mini',但发消息会因 baseURL/apiKey 空而失败——由 runTurn catch 友好提示。
|
|
52
|
+
*/
|
|
53
|
+
export function isModelConfigured() {
|
|
54
|
+
return !!config.baseURL && !!config.apiKey;
|
|
41
55
|
}
|
|
42
56
|
const PLATFORM_NOTE = (() => {
|
|
43
57
|
if (process.platform === 'win32') {
|
|
@@ -63,6 +77,7 @@ ${PLATFORM_NOTE}
|
|
|
63
77
|
|
|
64
78
|
## Workflow
|
|
65
79
|
- Understand before acting: when unsure about requirements or code state, explore first; don't assume.
|
|
80
|
+
- **Code exploration first action**: before reading files with read_file or searching with grep, check if a .codegraph/ index exists. If it does, use the codegraph tool (explore for questions/features, node for a specific symbol) as your FIRST step — it returns source + call paths in one shot. Only fall back to read_file/grep when codegraph misses, you need just-changed content, or you're editing a known small file. Build the index with \`codegraph init\` if none exists.
|
|
66
81
|
- Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
|
|
67
82
|
- Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
|
|
68
83
|
|
|
@@ -76,6 +91,7 @@ ${PLATFORM_NOTE}
|
|
|
76
91
|
- Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
|
|
77
92
|
- Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
|
|
78
93
|
- Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) — list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
|
|
94
|
+
- **Drop irrelevant context** (use sparingly): call drop_context to stub-replace tool results in history that are BOTH (a) irrelevant to the current task AND (b) large (the freed tokens must clearly exceed the ~300 tokens the call itself costs — roughly only worth it when targeting ≥2 bulky results, e.g. wide grep/read sweeps that returned mostly-irrelevant hits). The call itself adds a tool-call round-trip, so don't call it for one small result or when you're near done. It preserves tool_call_id pairing (only content changes); the system prompt and current turn are never dropped. Use filters (toolNames / contains) to target precisely.
|
|
79
95
|
|
|
80
96
|
## Failure Handling
|
|
81
97
|
- Tools return errors as strings (edit_file no match or non-unique, run_command non-zero exit, etc.). Analyze the root cause, adjust, then retry — don't resend the same call verbatim.
|
|
@@ -108,10 +124,10 @@ export const PLAN_MODE_SUFFIX = `
|
|
|
108
124
|
## ⛯ PLAN MODE (active now)
|
|
109
125
|
You are in PLAN mode: investigate and design only — do NOT execute or change anything.
|
|
110
126
|
- Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
|
|
111
|
-
- Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing.
|
|
127
|
+
- Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
|
|
112
128
|
- Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
|
|
113
129
|
- Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
|
|
114
|
-
- If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP
|
|
130
|
+
- If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
|
|
115
131
|
export const config = {
|
|
116
132
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
117
133
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
@@ -133,4 +149,31 @@ export const config = {
|
|
|
133
149
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
134
150
|
theme: process.env.MOCODE_THEME || 'default',
|
|
135
151
|
themeFromShell,
|
|
152
|
+
llmKeysFromShell,
|
|
136
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* 运行时更新模型相关配置(/model 命令调)。
|
|
156
|
+
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
157
|
+
* - 同步 process.env(保持内存一致:其他读 process.env 的路径也拿到新值;且使新值在下次启动的
|
|
158
|
+
* loadEnvFiles 中被视为"已设",不被文件回填覆盖——即"优先拿这里的")。
|
|
159
|
+
* 持久化(写 ~/.mocode/config)由调用方走 writeConfigKeys,此处只管内存 + env。
|
|
160
|
+
* 重建 OpenAI 客户端(baseURL/apiKey 是构造时固化的实例字段)由调用方走 reconfigureClient。
|
|
161
|
+
*/
|
|
162
|
+
export function updateModelConfig(opts) {
|
|
163
|
+
if (opts.model !== undefined) {
|
|
164
|
+
config.model = opts.model;
|
|
165
|
+
process.env.LLM_MODEL = opts.model;
|
|
166
|
+
}
|
|
167
|
+
if (opts.baseURL !== undefined) {
|
|
168
|
+
config.baseURL = opts.baseURL;
|
|
169
|
+
process.env.LLM_BASE_URL = opts.baseURL;
|
|
170
|
+
}
|
|
171
|
+
if (opts.apiKey !== undefined) {
|
|
172
|
+
config.apiKey = opts.apiKey;
|
|
173
|
+
process.env.LLM_API_KEY = opts.apiKey;
|
|
174
|
+
}
|
|
175
|
+
if (opts.contextWindowTokens !== undefined) {
|
|
176
|
+
config.contextWindowTokens = opts.contextWindowTokens;
|
|
177
|
+
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
|
|
9
9
|
// 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
|
|
10
|
-
/** 工具名 → ContextKind 的强先验表(覆盖全部
|
|
10
|
+
/** 工具名 → ContextKind 的强先验表(覆盖全部 18 内置工具)。 */
|
|
11
11
|
const BY_NAME = {
|
|
12
12
|
// tree:路径列表 → 缩进树
|
|
13
13
|
glob: 'tree',
|
|
@@ -32,6 +32,7 @@ const BY_NAME = {
|
|
|
32
32
|
write_file: 'status',
|
|
33
33
|
ask_human: 'status',
|
|
34
34
|
switch_mode: 'status',
|
|
35
|
+
drop_context: 'status',
|
|
35
36
|
memory_save: 'status',
|
|
36
37
|
memory_update: 'status',
|
|
37
38
|
memory_forget: 'status',
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { exitAltScreen } from './ui/layout.js';
|
|
|
2
2
|
import { checkAndMaybeUpdate } from './updater/index.js';
|
|
3
3
|
// 终端恢复兜底:任一退出 / 中断 / 未捕获异常路径都要恢复 alt screen,避免残留备用屏 + 滚动区域。
|
|
4
4
|
// exitAltScreen 幂等(未激活时空操作),故全局注册安全——进 alt screen 前的路径(如 --resume 列表、缺环境变量、`mocode config`)调用它无副作用。
|
|
5
|
-
// 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(
|
|
5
|
+
// 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(模块加载触发 loadEnvFiles + config 单例初始化),
|
|
6
|
+
// 改动态按需加载——`mocode config` 向导只需读写文件(走 config/file.ts 叶子),不经 config 单例初始化,零配置也能跑。
|
|
6
7
|
process.on('exit', () => exitAltScreen());
|
|
7
8
|
process.on('SIGINT', () => {
|
|
8
9
|
exitAltScreen();
|
package/dist/llm/index.js
CHANGED
|
@@ -2,10 +2,22 @@ import OpenAI from 'openai';
|
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
3
|
import { tools } from '../tools/registry.js';
|
|
4
4
|
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
5
|
-
|
|
5
|
+
let client = new OpenAI({
|
|
6
6
|
baseURL: config.baseURL,
|
|
7
7
|
apiKey: config.apiKey,
|
|
8
8
|
});
|
|
9
|
+
/**
|
|
10
|
+
* 运行时重建 OpenAI 客户端(/model 切换 baseURL/apiKey 后调)。
|
|
11
|
+
* config.model 已在 chat() 每次读取(热切),但 client 的 baseURL/apiKey 是构造时固化的实例字段,
|
|
12
|
+
* 改 config 后必须重建 client 才能让新 baseURL/apiKey 对后续请求生效。
|
|
13
|
+
* 子 agent 复用本模块 chat(),故只此一处重建即全链路生效。
|
|
14
|
+
*/
|
|
15
|
+
export function reconfigureClient() {
|
|
16
|
+
client = new OpenAI({
|
|
17
|
+
baseURL: config.baseURL,
|
|
18
|
+
apiKey: config.apiKey,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
9
21
|
/** 把内部工具定义转成 OpenAI 的 tool 格式 */
|
|
10
22
|
export const chatTools = tools.map((t) => ({
|
|
11
23
|
type: 'function',
|