mocode-ai 0.1.3 → 0.1.5
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 +196 -161
- package/dist/agent/core.js +302 -0
- package/dist/agent/index.js +85 -202
- package/dist/agent/mode.js +46 -0
- package/dist/agent/spawn.js +123 -0
- package/dist/commands/config.js +8 -26
- package/dist/config/file.js +48 -0
- package/dist/config/index.js +76 -22
- package/dist/llm/index.js +11 -9
- package/dist/memory/discover.js +49 -0
- package/dist/memory/index.js +45 -0
- package/dist/memory/reflect.js +265 -0
- package/dist/memory/store.js +339 -0
- package/dist/repl/index.js +334 -87
- package/dist/session/compact.js +51 -3
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +50 -0
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +3 -2
- package/dist/tools/builtins/grep.js +4 -3
- package/dist/tools/builtins/index.js +19 -1
- package/dist/tools/builtins/memory-forget.js +32 -0
- package/dist/tools/builtins/memory-list.js +34 -0
- package/dist/tools/builtins/memory-save.js +51 -0
- package/dist/tools/builtins/memory-search.js +42 -0
- package/dist/tools/builtins/memory-update.js +41 -0
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +40 -7
- package/dist/tools/builtins/switch-mode.js +46 -0
- package/dist/tools/builtins/task.js +62 -0
- package/dist/tools/builtins/use-skill.js +2 -2
- package/dist/tools/builtins/web-fetch.js +16 -3
- package/dist/tools/builtins/web-search.js +6 -6
- package/dist/tools/builtins/write-file.js +3 -3
- package/dist/tools/constants.js +29 -0
- package/dist/tools/registry.js +11 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +318 -0
- package/dist/ui/layout.js +402 -103
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +469 -27
- package/dist/ui/render.js +19 -1
- package/dist/ui/spinner.js +15 -0
- package/dist/ui/theme.js +147 -13
- package/package.json +2 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent 模式的共享状态(零依赖纯叶子)。
|
|
3
|
+
*
|
|
4
|
+
* 把 `agentMode` 从 repl 的模块变量提到这里,让三方都能用、且不破坏依赖单向:
|
|
5
|
+
* - `tools/builtins/switch-mode.ts` 写它(setAgentMode)——LLM 在 agent 循环里自切 plan↔auto。
|
|
6
|
+
* - `agent/index.ts` 每步读它(getAgentMode)——决定 chat() 用全量 chatTools 还是 planChatTools 只读子集,
|
|
7
|
+
* 以及串行分支的 plan 防御 backstop。这样 LLM 中途切模式后,下一次 chat() 立即看到新工具集。
|
|
8
|
+
* - `repl/index.ts` 读写它 + 注册 onModeChange 监听器(做 applyMode 重写 history[0] + refreshStatusBase
|
|
9
|
+
* 刷状态行 modeTag)。
|
|
10
|
+
*
|
|
11
|
+
* 依赖方向无环:本模块不 import 任何业务模块。`agent/index → tools/registry → tools/builtins/switch-mode
|
|
12
|
+
* → agent/mode` 链终点是本叶子,不回到 agent/index。`tools → agent/mode` 与既有 `tools → ui`(ask-human)、
|
|
13
|
+
* `tools → rollback`(registry)同性质——都是引叶子。
|
|
14
|
+
*
|
|
15
|
+
* listener 同步触发:setAgentMode 在变更模式后同步调 listener(无 async)。switch_mode 工具在 executeTool
|
|
16
|
+
* 内调 setAgentMode → listener 同步 applyMode(重写 history[0])+ refreshStatusBase;execute 返回字符串后,
|
|
17
|
+
* agent 串行分支继续,下一步 chat() 读到新模式 + 新系统提示,一致。
|
|
18
|
+
*/
|
|
19
|
+
let currentMode = 'auto';
|
|
20
|
+
let listener = null;
|
|
21
|
+
/** 当前 agent 模式。auto=全工具执行;plan=只读探查 + 产出计划(写盘/命令/记忆写入工具被 schema 剔除)。 */
|
|
22
|
+
export function getAgentMode() {
|
|
23
|
+
return currentMode;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 设置 agent 模式。同模式 no-op(不触发 listener)。变更时同步触发 onModeChange 注册的监听器
|
|
27
|
+
* (repl 在 startRepl 注册:applyMode 重写 history[0] + refreshStatusBase 刷状态行)。
|
|
28
|
+
* 返回之前的模式,供调用方(如 runAgent 中断恢复)还原。
|
|
29
|
+
*/
|
|
30
|
+
export function setAgentMode(m) {
|
|
31
|
+
const prev = currentMode;
|
|
32
|
+
if (prev === m)
|
|
33
|
+
return prev;
|
|
34
|
+
currentMode = m;
|
|
35
|
+
try {
|
|
36
|
+
listener?.(m);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// listener 不应抛(applyMode/refreshStatusBase 都不抛);兜底,不阻断工具执行。
|
|
40
|
+
}
|
|
41
|
+
return prev;
|
|
42
|
+
}
|
|
43
|
+
/** 注册模式变更监听器(单一,后注册覆盖先注册)。repl 在 startRepl 启动时注册一次。 */
|
|
44
|
+
export function onModeChange(cb) {
|
|
45
|
+
listener = cb;
|
|
46
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// 子 agent 封装:runAgentCore + 静默 hooks。独立 history / 可受限工具子集 / 低步数上限。
|
|
2
|
+
// 供 task 工具(主 agent 派生子任务)调用——子 agent 的最终摘要回灌主 history。
|
|
3
|
+
//
|
|
4
|
+
// 与主 agent 的区别:
|
|
5
|
+
// - 不写主屏(layout.contentWrite):中间过程(流式正文 / 工具头 / diff)缓冲到内部字符串,
|
|
6
|
+
// 结束返回给 task 工具(task 把它当 tool 结果回灌主 history,主 agent 据此继续)。
|
|
7
|
+
// - 独立 history:不共享主对话,避免子任务的工具噪声污染主上下文。
|
|
8
|
+
// - 系统提示复用主 agent 组装链(config.systemPrompt + memory 段 + skills 段)+ 子 agent 角色后缀。
|
|
9
|
+
// - 工具子集:按白名单从 chatTools 过滤;无白名单 = 全量(但 task 工具调用方通常会限定只读)。
|
|
10
|
+
// - 不调 beginTurn(不进主回滚链);但文件 mutation 仍经 executeTool→recordMutation,
|
|
11
|
+
// 归入当前主轮次(语义:子 agent 的改动属于当前主轮,可随 /rollback 一起撤销)。
|
|
12
|
+
// - 步数上限默认更低(config.subAgentMaxSteps ?? 50),防子任务失控耗尽配额。
|
|
13
|
+
// - 中断透传:opts.signal(主 agent 的 abort signal)透传给 runAgentCore → chat/executeTool,
|
|
14
|
+
// 主 Ctrl+C 树杀子 agent(chat 流式 abort + run_command/web_fetch 即时取消)。
|
|
15
|
+
// - 逻辑隔离(回滚):skipRollback=true,子 agent 的 write_file/edit_file 改动不进主回滚快照链,
|
|
16
|
+
// 主 /rollback 不撤销子 agent 改动(靠 git 兜底)。子 agent 与主 agent 共享 cwd(文件改动可见)。
|
|
17
|
+
import { chatTools } from '../llm/index.js';
|
|
18
|
+
import { config } from '../config/index.js';
|
|
19
|
+
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
20
|
+
import { buildMemorySection, buildMemoryIndexSection } from '../memory/index.js';
|
|
21
|
+
import { ui } from '../ui/theme.js';
|
|
22
|
+
import { runAgentCore } from './core.js';
|
|
23
|
+
import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
|
|
24
|
+
/** 子 agent 系统提示后缀:角色与约束。 */
|
|
25
|
+
const SUBAGENT_SUFFIX = `
|
|
26
|
+
|
|
27
|
+
## ⛯ SUB-AGENT MODE (you are a sub-agent)
|
|
28
|
+
You are a sub-agent spawned by the main agent to handle an isolated sub-task. You have your own conversation history (independent of the main thread).
|
|
29
|
+
- Focus solely on the assigned sub-task. Do NOT attempt to call the "task" tool (no recursive spawning).
|
|
30
|
+
- Use the tools available to you to complete the sub-task.
|
|
31
|
+
- When done, your final text reply will be returned to the main agent as a summary — make it concise and actionable: what you did, key findings, files changed, and any issues. The main agent will decide the next step based on your summary.`;
|
|
32
|
+
/**
|
|
33
|
+
* 派生一个子 agent 执行独立子任务。
|
|
34
|
+
*
|
|
35
|
+
* 行为:
|
|
36
|
+
* - 构造独立 history:[{system: 主系统提示 + SUBAGENT_SUFFIX + 自定义后缀}, {user: prompt}]
|
|
37
|
+
* - 工具子集:按 opts.tools 白名单过滤 chatTools;无白名单 = 全量。
|
|
38
|
+
* - 静默 hooks:流式正文 / 工具头 / 结果缓冲到 transcript;不写主屏(layout)。
|
|
39
|
+
* - 返回 { summary, completed, transcript }。summary 给 task 工具回灌主 history。
|
|
40
|
+
*
|
|
41
|
+
* 中断:opts.signal 透传给子 runAgentCore——主 Ctrl+C 树杀子 agent(chat abort + 工具 abort)。
|
|
42
|
+
* 子 agent 跑在主 signal 下,主 abort 即子 abort;子 agent 的 abortRestore 还原子 history + 模式。
|
|
43
|
+
*/
|
|
44
|
+
export async function spawnAgent(opts) {
|
|
45
|
+
const maxSteps = opts.maxSteps ?? config.subAgentMaxSteps ?? 50;
|
|
46
|
+
// 构造子 agent 系统提示:复用主 agent 组装链 + 子 agent 角色后缀 + 自定义后缀。
|
|
47
|
+
const systemPrompt = effectiveSystemPrompt(config.systemPrompt +
|
|
48
|
+
buildMemorySection() +
|
|
49
|
+
buildMemoryIndexSection() +
|
|
50
|
+
SUBAGENT_SUFFIX +
|
|
51
|
+
(opts.systemPromptSuffix ? `\n\n${opts.systemPromptSuffix}` : ''));
|
|
52
|
+
// 工具子集:白名单过滤。无白名单 = 全量 chatTools,但始终剔除 task(防递归派生)。
|
|
53
|
+
let toolsOverride;
|
|
54
|
+
if (opts.tools && opts.tools.length > 0) {
|
|
55
|
+
const allow = new Set(opts.tools);
|
|
56
|
+
toolsOverride = chatTools.filter((t) => allow.has(t.function.name) && t.function.name !== 'task');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
toolsOverride = chatTools.filter((t) => t.function.name !== 'task');
|
|
60
|
+
}
|
|
61
|
+
// 独立 history(子 agent 自己持有,不共享主对话)。
|
|
62
|
+
// 只塞 system;user 消息由 runAgentCore 的 userInput 参数 push(与主 agent 一致)。
|
|
63
|
+
const history = [
|
|
64
|
+
{ role: 'system', content: systemPrompt },
|
|
65
|
+
];
|
|
66
|
+
// 静默 hooks:缓冲中间过程到 transcript,不写主屏。
|
|
67
|
+
let transcript = '';
|
|
68
|
+
const buf = [];
|
|
69
|
+
const writeBuf = (s) => {
|
|
70
|
+
buf.push(s);
|
|
71
|
+
transcript += s;
|
|
72
|
+
};
|
|
73
|
+
let lastChar = '';
|
|
74
|
+
const hooks = {
|
|
75
|
+
onText: (s) => {
|
|
76
|
+
writeBuf(s); // 缓冲流式正文(无 markdown 渲染,原始文本)
|
|
77
|
+
if (s)
|
|
78
|
+
lastChar = s[s.length - 1];
|
|
79
|
+
},
|
|
80
|
+
onToolCall: (name) => {
|
|
81
|
+
if (lastChar && lastChar !== '\n') {
|
|
82
|
+
writeBuf('\n');
|
|
83
|
+
lastChar = '\n';
|
|
84
|
+
}
|
|
85
|
+
// 子 agent 静默:不启 spinner(name 参数仅主 agent 用)
|
|
86
|
+
},
|
|
87
|
+
onToolHeader: (tc) => {
|
|
88
|
+
const summary = summarizeToolCall(tc.name, tc.arguments);
|
|
89
|
+
writeBuf(` ● ${tc.name} ${summary}\n`);
|
|
90
|
+
},
|
|
91
|
+
onToolResult: (tc, output) => {
|
|
92
|
+
const preview = summarizeToolResult(tc.name, output);
|
|
93
|
+
if (preview)
|
|
94
|
+
writeBuf(` ↳ ${preview}\n`);
|
|
95
|
+
},
|
|
96
|
+
onTextEnd: () => {
|
|
97
|
+
if (lastChar && lastChar !== '\n') {
|
|
98
|
+
writeBuf('\n');
|
|
99
|
+
lastChar = '\n';
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onToolBatchEnd: () => writeBuf('\n'),
|
|
103
|
+
onNoReply: () => writeBuf(`${ui.dim}(无回复)${ui.reset}\n`),
|
|
104
|
+
onMaxSteps: () => writeBuf(` ● 达到最大步数(${maxSteps}),子 agent 停止。\n`),
|
|
105
|
+
onDone: (elapsedMs) => writeBuf(` ✻ 子 agent 耗时 ${(elapsedMs / 1000).toFixed(1)}s\n`),
|
|
106
|
+
// onStepStart / onChatDone / onToolStart / onToolDone / onAbort:子 agent 静默,无需 spinner / 中断渲染。
|
|
107
|
+
// abort 还原(history 还原 + 模式还原)由 core 的 abortRestore 处理,hooks 只管展示。
|
|
108
|
+
};
|
|
109
|
+
const result = await runAgentCore({
|
|
110
|
+
history,
|
|
111
|
+
userInput: opts.prompt,
|
|
112
|
+
signal: opts.signal, // 主 Ctrl+C 树杀子 agent(chat abort + 工具 abort)
|
|
113
|
+
hooks,
|
|
114
|
+
maxSteps,
|
|
115
|
+
toolsOverride,
|
|
116
|
+
skipRollback: true, // 逻辑隔离:子 agent 文件改动不进主回滚快照链,主 /rollback 不撤销(靠 git 兜底)
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
summary: result.finalText,
|
|
120
|
+
completed: result.completed,
|
|
121
|
+
transcript: truncateDisplay(transcript, 20000), // 防过大;调试用,回灌主 history 的是 summary 不是 transcript
|
|
122
|
+
};
|
|
123
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
1
|
import * as readline from 'node:readline';
|
|
5
|
-
import
|
|
6
|
-
/** ~/.mocode/config:全局配置(与 ~/.mocode/skills 同目录,任意终端 / 任意目录生效)。 */
|
|
7
|
-
const CONFIG_PATH = path.join(os.homedir(), '.mocode', 'config');
|
|
2
|
+
import { CONFIG_PATH, readConfigFile, writeConfigKeys } from '../config/file.js';
|
|
8
3
|
function ask(rl, q) {
|
|
9
4
|
return new Promise((resolve) => {
|
|
10
5
|
rl.question(q, (ans) => resolve(ans.trim()));
|
|
11
6
|
});
|
|
12
7
|
}
|
|
13
|
-
/** 读已有配置(没有则空对象),用于回填默认值与保留其它键。 */
|
|
14
|
-
function readExisting() {
|
|
15
|
-
try {
|
|
16
|
-
return dotenv.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
8
|
/**
|
|
23
9
|
* 首跑配置向导:交互填 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL,写 ~/.mocode/config。
|
|
24
|
-
* 只交互三键、保留文件里其它键(MAX_TOKENS / CONTEXT_WINDOW_TOKENS
|
|
25
|
-
*
|
|
10
|
+
* 只交互三键、保留文件里其它键(MAX_TOKENS / CONTEXT_WINDOW_TOKENS / MOCODE_THEME 等)。
|
|
11
|
+
* prompt 全纯文本(readline 光标按字符算,不能含 ANSI)。
|
|
12
|
+
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(避免缺配置时 requireEnv
|
|
13
|
+
* 直接退出);只 import config/file.ts(纯 I/O 叶子,无 env 校验 / process.exit)。
|
|
26
14
|
*/
|
|
27
15
|
export async function runConfigWizard() {
|
|
28
|
-
const cur =
|
|
16
|
+
const cur = readConfigFile();
|
|
29
17
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
30
18
|
console.log(`mocode 配置向导 — 写入 ${CONFIG_PATH}(Ctrl+C 取消)\n`);
|
|
31
19
|
const baseURLIn = await ask(rl, `LLM_BASE_URL${cur.LLM_BASE_URL ? ` [${cur.LLM_BASE_URL}]` : ''}(如 https://open.bigmodel.cn/api/v3): `);
|
|
@@ -46,16 +34,10 @@ export async function runConfigWizard() {
|
|
|
46
34
|
const model = modelIn || cur.LLM_MODEL || 'gpt-4o-mini';
|
|
47
35
|
rl.close();
|
|
48
36
|
// 合并:保留其它键,只覆盖三键。
|
|
49
|
-
|
|
50
|
-
...cur,
|
|
37
|
+
writeConfigKeys({
|
|
51
38
|
LLM_BASE_URL: baseURL,
|
|
52
39
|
LLM_API_KEY: apiKey,
|
|
53
40
|
LLM_MODEL: model,
|
|
54
|
-
};
|
|
55
|
-
const body = Object.entries(merged)
|
|
56
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
57
|
-
.join('\n');
|
|
58
|
-
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
59
|
-
fs.writeFileSync(CONFIG_PATH, `${body}\n`, 'utf8');
|
|
41
|
+
});
|
|
60
42
|
console.log(`\n已写入 ${CONFIG_PATH}。现在运行 \`mocode\` 即可启动(任意目录、任意终端)。`);
|
|
61
43
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
/**
|
|
6
|
+
* ~/.mocode/config 全局 dotenv 配置的读写叶子(纯 node:fs / node:os / node:path + dotenv)。
|
|
7
|
+
*
|
|
8
|
+
* 独立于 `config/index.ts`——**不**触发 requireEnv / process.exit,故 `commands/config.ts`
|
|
9
|
+
* (首跑向导,刻意不 import config/index.ts)与 `repl/index.ts`(`/theme` 持久化)都能安全共享。
|
|
10
|
+
* 主题等 UI 偏好走 `updateConfigKey` 单键写;向导多键一次性写走 `writeConfigKeys`。
|
|
11
|
+
*/
|
|
12
|
+
/** ~/.mocode/config:全局配置(与 ~/.mocode/skills 同目录,任意终端 / 任意目录生效)。 */
|
|
13
|
+
export const CONFIG_PATH = path.join(os.homedir(), '.mocode', 'config');
|
|
14
|
+
/** 读已有配置(没有则空对象),用于回填默认值与保留其它键。 */
|
|
15
|
+
export function readConfigFile() {
|
|
16
|
+
try {
|
|
17
|
+
return dotenv.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 读-合并-写:把 entries 覆盖进 ~/.mocode/config,保留文件里其它键。
|
|
25
|
+
* mkdirSync 兜底(首跑无 ~/.mocode)。不吞 I/O 异常——向导路径让其抛(与历史行为一致);
|
|
26
|
+
* UI 偏好路径用 `updateConfigKey`(自带 try/catch 静默)。
|
|
27
|
+
*/
|
|
28
|
+
export function writeConfigKeys(entries) {
|
|
29
|
+
const merged = { ...readConfigFile(), ...entries };
|
|
30
|
+
const body = Object.entries(merged)
|
|
31
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
32
|
+
.join('\n');
|
|
33
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
34
|
+
fs.writeFileSync(CONFIG_PATH, `${body}\n`, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 单键持久化(主题等 UI 偏好):写失败静默——下次启动读不到则回退默认,不阻断 REPL。
|
|
38
|
+
* 注意:shell env 设了同名键时,`config/index.ts` 的 loadEnvFiles 只 backfill undefined 的
|
|
39
|
+
* process.env,故文件写对**下次启动**可能被 shell 盖——调用方据此给 dim 警告。
|
|
40
|
+
*/
|
|
41
|
+
export function updateConfigKey(key, value) {
|
|
42
|
+
try {
|
|
43
|
+
writeConfigKeys({ [key]: value });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 写失败不阻断(UI 偏好)
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/config/index.js
CHANGED
|
@@ -28,6 +28,8 @@ function loadEnvFiles() {
|
|
|
28
28
|
process.env[k] = v;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
|
|
32
|
+
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
31
33
|
loadEnvFiles();
|
|
32
34
|
function requireEnv(key) {
|
|
33
35
|
const v = process.env[key];
|
|
@@ -37,33 +39,79 @@ function requireEnv(key) {
|
|
|
37
39
|
}
|
|
38
40
|
return v;
|
|
39
41
|
}
|
|
40
|
-
const
|
|
42
|
+
const PLATFORM_NOTE = (() => {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
return `## Environment (Windows)
|
|
45
|
+
- You are on Windows; run_command runs commands via cmd.exe (/c). Unix shell builtins are NOT available here.
|
|
46
|
+
- Windows equivalents: which→where, cat→type, ls→dir, rm→del/rd, cp→copy, mv→move. cmd.exe uses %VAR% (not $VAR); pipes (|) and redirects (>, >>) work, but no $(...) command substitution or backticks.
|
|
47
|
+
- head/tail/find/grep/sed have no cmd.exe equivalent — use the dedicated tools (read_file for head/tail, glob for find, grep for grep), or invoke PowerShell via run_command if you need more.
|
|
48
|
+
- Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they're cross-platform and already wired in.`;
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'darwin') {
|
|
51
|
+
return `## Environment (macOS)
|
|
52
|
+
- You are on macOS; run_command runs via bash -c (user default shell may be zsh). BSD coreutils, not GNU.
|
|
53
|
+
- Pitfalls: sed -i needs an empty backup-ext arg (sed -i '' 's/x/y/' file); grep -P unavailable (use grep -E or the grep tool); find/readlink/date are BSD variants; readlink -f unsupported (use realpath, or greadlink -f if GNU coreutils installed via brew).
|
|
54
|
+
- Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they sidestep BSD/GNU differences.`;
|
|
55
|
+
}
|
|
56
|
+
return `## Environment (Linux/Unix)
|
|
57
|
+
- You are on ${process.platform}; run_command runs via bash -c. GNU coreutils — standard POSIX/GNU shell syntax is safe.
|
|
58
|
+
- Still prefer the dedicated tools (read_file/glob/grep) over hand-rolled shell where they fit — they avoid quoting pitfalls and are already wired in.`;
|
|
59
|
+
})();
|
|
60
|
+
const SYSTEM_PROMPT = `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
|
|
61
|
+
|
|
62
|
+
${PLATFORM_NOTE}
|
|
63
|
+
|
|
64
|
+
## Workflow
|
|
65
|
+
- Understand before acting: when unsure about requirements or code state, explore first; don't assume.
|
|
66
|
+
- Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
|
|
67
|
+
- Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
|
|
41
68
|
|
|
42
|
-
##
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
69
|
+
## Tool Guidelines
|
|
70
|
+
- See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
|
|
71
|
+
- **Prefer codegraph for code exploration**: when understanding/locating code, tracing call chains, or assessing impact of changes, if a .codegraph/ index exists, use the codegraph tool first (explore to query by question, node to look up a single symbol) — it returns relevant source + call paths in one shot, more accurate and economical than piecing together via read_file/grep. Fall back to read_file / grep / glob only when codegraph is unavailable (no index), misses, you need to see just-changed content, or you're editing a single known small file. Build the index first with \`codegraph init\` if none exists.
|
|
72
|
+
- Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory.
|
|
73
|
+
- For local edits use edit_file: old_string must be unique and match exactly (including indentation/newlines); include surrounding context lines to ensure uniqueness. Use write_file for new files or full rewrites.
|
|
74
|
+
- Use glob to find file paths, grep to search content; don't use run_command to pipe cat / sed / find / grep.
|
|
75
|
+
- run_command runs per platform (cmd on Windows, bash elsewhere); state intent before running commands with side effects (deleting files, installing packages, git push, resets, etc.).
|
|
76
|
+
- 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
|
+
- 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
|
+
- 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.
|
|
46
79
|
|
|
47
|
-
##
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
- 局部改用 edit_file:old_string 须唯一且精确匹配(含缩进/换行),多带上下文行确保唯一;新建或整体重写用 write_file。
|
|
51
|
-
- 找路径用 glob,找内容用 grep;不要用 run_command 拼 cat / sed / find / grep。
|
|
52
|
-
- run_command 按平台执行(Win 用 cmd、其他用 bash);有副作用的命令(删文件、装包、git push、重置等)执行前先简述意图。
|
|
53
|
-
- 需要训练数据之外的最新信息(新版本、新闻、实时数据、最新 API)时用 web_search 联网搜索,不要凭记忆答可能过时的内容。
|
|
54
|
-
- 要读取某个具体 URL 的内容(搜索结果里的链接、用户给的 URL)时用 web_fetch 抓取;它只抓静态 HTML,JS 渲染页面拿不到正文时改用 web_search(其结果自带清洗后的正文)。
|
|
80
|
+
## Failure Handling
|
|
81
|
+
- 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.
|
|
82
|
+
- When a command errors, read the actual output before judging; don't skip it.
|
|
55
83
|
|
|
56
|
-
##
|
|
57
|
-
-
|
|
58
|
-
-
|
|
84
|
+
## Safety & Boundaries
|
|
85
|
+
- Confirm with the user before irreversible or outward-facing operations (delete, overwrite existing files, push, request external services), unless explicitly authorized.
|
|
86
|
+
- Operate only within authorized scope; when unsure, ask — don't guess.
|
|
59
87
|
|
|
60
|
-
##
|
|
61
|
-
-
|
|
62
|
-
-
|
|
88
|
+
## Memory (cross-session long-term facts)
|
|
89
|
+
- A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
|
|
90
|
+
- Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
|
|
91
|
+
- If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
|
|
92
|
+
- Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
|
|
93
|
+
- A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.
|
|
94
|
+
|
|
95
|
+
## Plan vs Auto modes
|
|
96
|
+
- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).
|
|
97
|
+
- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.
|
|
98
|
+
|
|
99
|
+
## Termination & Reporting
|
|
100
|
+
- Stop immediately when no more tools are needed; give conclusions directly.
|
|
101
|
+
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.`;
|
|
102
|
+
/**
|
|
103
|
+
* plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
|
|
104
|
+
* 与 SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
|
|
105
|
+
*/
|
|
106
|
+
export const PLAN_MODE_SUFFIX = `
|
|
63
107
|
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
-
|
|
108
|
+
## ⛯ PLAN MODE (active now)
|
|
109
|
+
You are in PLAN mode: investigate and design only — do NOT execute or change anything.
|
|
110
|
+
- 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. Prefer codegraph when a .codegraph/ index exists.
|
|
112
|
+
- 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
|
+
- 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; the user will approve via a prompt and execution happens in a follow-up turn.`;
|
|
67
115
|
export const config = {
|
|
68
116
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
69
117
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
@@ -74,7 +122,13 @@ export const config = {
|
|
|
74
122
|
compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
|
|
75
123
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
76
124
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
125
|
+
autoReflect: process.env.AUTO_REFLECT !== 'false',
|
|
126
|
+
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
127
|
+
maxSteps: Number(process.env.MAX_STEPS) || 200,
|
|
128
|
+
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || 50,
|
|
77
129
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
78
130
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
79
131
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
132
|
+
theme: process.env.MOCODE_THEME || 'default',
|
|
133
|
+
themeFromShell,
|
|
80
134
|
};
|
package/dist/llm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
3
|
import { tools } from '../tools/registry.js';
|
|
4
|
+
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
4
5
|
const client = new OpenAI({
|
|
5
6
|
baseURL: config.baseURL,
|
|
6
7
|
apiKey: config.apiKey,
|
|
@@ -16,18 +17,23 @@ export const chatTools = tools.map((t) => ({
|
|
|
16
17
|
},
|
|
17
18
|
}));
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
20
|
+
* plan 模式用的受限工具 schema:剔除写盘 / 命令 / 记忆写入类(PLAN_DISABLED_TOOLS),
|
|
21
|
+
* 模型在 plan 模式下只看得到只读工具 → 调不到会改文件的工具。runAgent 在 plan 模式传给 chat()。
|
|
22
|
+
*/
|
|
23
|
+
export const planChatTools = chatTools.filter((t) => !PLAN_DISABLED_TOOLS.has(t.function.name));
|
|
24
|
+
/**
|
|
25
|
+
* 流式调一次 LLM:增量回调文本,内部累加 tool_calls 片段。
|
|
22
26
|
* tool_calls 跨 chunk 按 index 累加(id / name / arguments 拼接)。
|
|
23
27
|
* include_usage 时末尾 chunk 携带 usage,先读再 continue(末尾 chunk 无 delta)。
|
|
24
28
|
*/
|
|
25
|
-
export async function chat(messages, handlers = {}, signal
|
|
29
|
+
export async function chat(messages, handlers = {}, signal,
|
|
30
|
+
/** 覆盖默认工具 schema;plan 模式传 planChatTools(只读子集),缺省=全量 chatTools。 */
|
|
31
|
+
toolsOverride) {
|
|
26
32
|
// signal 透传给 SDK 第二参(RequestOptions);abort 后 for await 抛错,chat 不 catch,透传 runAgent 处理。
|
|
27
33
|
const stream = await client.chat.completions.create({
|
|
28
34
|
model: config.model,
|
|
29
35
|
messages,
|
|
30
|
-
tools: chatTools,
|
|
36
|
+
tools: toolsOverride ?? chatTools,
|
|
31
37
|
stream: true,
|
|
32
38
|
...(config.maxTokens ? { max_tokens: config.maxTokens } : {}),
|
|
33
39
|
...(config.includeUsage ? { stream_options: { include_usage: true } } : {}),
|
|
@@ -48,10 +54,6 @@ export async function chat(messages, handlers = {}, signal) {
|
|
|
48
54
|
const delta = chunk.choices[0]?.delta;
|
|
49
55
|
if (!delta)
|
|
50
56
|
continue; // 末尾 usage-only chunk 等无 delta
|
|
51
|
-
// 思考内容(非标准字段,SDK 类型无)
|
|
52
|
-
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
53
|
-
if (reasoning)
|
|
54
|
-
handlers.onThinking?.(reasoning);
|
|
55
57
|
if (delta.content) {
|
|
56
58
|
content += delta.content;
|
|
57
59
|
hasContent = true;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// memory 发现子系统:加载项目记忆 MOCODE.md(对标 skills/discover.ts 的叶子模式)。
|
|
2
|
+
// 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools/skills,避免环。
|
|
3
|
+
//
|
|
4
|
+
// 约定:纯 MOCODE.md(不读 CLAUDE.md——mocode 是独立工具,接非 Claude 后端,
|
|
5
|
+
// 有自己的工具集与约定,叫 CLAUDE.md 名不副实且可能读到 Claude 专属内容)。
|
|
6
|
+
// 项目级从 cwd 向上逐级找,全局 ~/.mocode/MOCODE.md。全量注入 systemPrompt(超长截断);
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
/**
|
|
11
|
+
* 返回要查找的 MOCODE.md 路径列表,按「远→近」顺序(合并时近的在后,更突出):
|
|
12
|
+
* 全局 ~/.mocode/MOCODE.md → 项目级从根到 cwd 逐级 MOCODE.md。
|
|
13
|
+
* 向上遍历 cwd 到根收集 [cwd..root],反转为 [root..cwd](远→近),前拼全局。
|
|
14
|
+
*/
|
|
15
|
+
export function resolveMemoryFiles() {
|
|
16
|
+
const globalPath = path.join(os.homedir(), '.mocode', 'MOCODE.md');
|
|
17
|
+
const projectFiles = [];
|
|
18
|
+
let dir = process.cwd();
|
|
19
|
+
const root = path.parse(dir).root; // win32 'C:\\', POSIX '/'
|
|
20
|
+
for (;;) {
|
|
21
|
+
projectFiles.push(path.join(dir, 'MOCODE.md'));
|
|
22
|
+
if (dir === root || dir === path.dirname(dir))
|
|
23
|
+
break; // 到根:dirname 自身
|
|
24
|
+
dir = path.dirname(dir);
|
|
25
|
+
}
|
|
26
|
+
projectFiles.reverse(); // root..cwd(远→近)
|
|
27
|
+
return [globalPath, ...projectFiles];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 读取所有存在的 MOCODE.md,返回 { path, content }(content 已 trim)。
|
|
31
|
+
* 全程静默容错(不存在 / 读失败 → 跳过,不抛),风格对齐 skills/discover.ts 与 session/persist.ts。
|
|
32
|
+
*/
|
|
33
|
+
export function loadMemoryFiles() {
|
|
34
|
+
const files = resolveMemoryFiles();
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const p of files) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(p))
|
|
39
|
+
continue;
|
|
40
|
+
const content = readFileSync(p, 'utf8').trim();
|
|
41
|
+
if (content)
|
|
42
|
+
out.push({ path: p, content });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue; // 读失败静默跳过
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// memory barrel:Tier-1 MOCODE.md 懒加载(buildMemorySection,叶子)+ Tier-2 工具库 re-export。
|
|
2
|
+
// Tier-2:store.ts(叶子,node:fs)做 JSONL CRUD/GC/索引段;reflect.ts(→llm,同 session/)做后台反思 pass。
|
|
3
|
+
// 被 repl 依赖(注入 systemPrompt + 轮末触发反思 + 退出 drain)。Tier-1 仅依赖 discover.ts。
|
|
4
|
+
import { loadMemoryFiles } from './discover.js';
|
|
5
|
+
export { buildMemoryIndexSection, loadAll, gcMemories, } from './store.js';
|
|
6
|
+
export { kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, runReflection, } from './reflect.js';
|
|
7
|
+
/** system 消息中 memory 段的字符上限(防过大占窗口——system 在 history[0],compactHistory 不压缩)。 */
|
|
8
|
+
const MAX_MEMORY_CHARS = 20000;
|
|
9
|
+
let cache = null;
|
|
10
|
+
/**
|
|
11
|
+
* 合并全局 + 项目各级 MOCODE.md(远→近拼接,各段空行分隔),超 MAX_MEMORY_CHARS 截断 + 提示。
|
|
12
|
+
* 懒加载(首次调用触发扫描;启动期 repl 调一次)。无 MOCODE.md 返空串。
|
|
13
|
+
*/
|
|
14
|
+
export function loadMemory() {
|
|
15
|
+
if (cache !== null)
|
|
16
|
+
return cache;
|
|
17
|
+
const files = loadMemoryFiles();
|
|
18
|
+
if (files.length === 0) {
|
|
19
|
+
cache = '';
|
|
20
|
+
return cache;
|
|
21
|
+
}
|
|
22
|
+
const body = files.map((f) => f.content).join('\n\n');
|
|
23
|
+
if (body.length <= MAX_MEMORY_CHARS) {
|
|
24
|
+
cache = body;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
cache =
|
|
28
|
+
body.slice(0, MAX_MEMORY_CHARS) +
|
|
29
|
+
`\n\n…(项目记忆已截断 ${body.length - MAX_MEMORY_CHARS} 字符,完整见各级 MOCODE.md)`;
|
|
30
|
+
}
|
|
31
|
+
return cache;
|
|
32
|
+
}
|
|
33
|
+
/** 拼进系统提示的 memory 段;无 memory 返空串(零行为变化)。 */
|
|
34
|
+
export function buildMemorySection() {
|
|
35
|
+
const mem = loadMemory();
|
|
36
|
+
if (!mem)
|
|
37
|
+
return '';
|
|
38
|
+
return [
|
|
39
|
+
'',
|
|
40
|
+
'',
|
|
41
|
+
'## Project Memory (MOCODE.md)',
|
|
42
|
+
'The following is project memory (architecture / conventions / commands and other cross-session long-term facts). Act accordingly:',
|
|
43
|
+
mem,
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|