mocode-ai 1.0.13 → 1.1.0
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 +2 -19
- package/README.zh-CN.md +1 -1
- package/bin/mocode-agent-host.js +3 -0
- package/dist/agent/core.js +10 -5
- package/dist/agent/mode.js +9 -10
- package/dist/agent/spawn.js +2 -2
- package/dist/config/index.js +43 -65
- package/dist/context/artifacts.js +2 -2
- package/dist/context/classifier.js +3 -6
- package/dist/context/encoders/_util.js +2 -1
- package/dist/context/encoders/index.js +3 -4
- package/dist/context/lifecycle.js +3 -23
- package/dist/context/relevance.js +3 -22
- package/dist/host/protocol.js +40 -0
- package/dist/host/stdio.js +203 -0
- package/dist/i18n/index.js +0 -30
- package/dist/memory/index.js +6 -0
- package/dist/repl/index.js +32 -284
- package/dist/sandbox/policy.js +6 -4
- package/dist/skills/builtin-skills.js +56 -0
- package/dist/skills/discover.js +7 -0
- package/dist/skills/index.js +23 -4
- package/dist/tools/builtins/edit-file.js +68 -14
- package/dist/tools/builtins/glob.js +1 -1
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +0 -14
- package/dist/tools/builtins/read-file.js +1 -1
- package/dist/tools/constants.js +2 -1
- package/package.json +3 -2
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig,
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
6
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
7
7
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
@@ -26,8 +26,7 @@ import { formatArtifactTokenSources } from '../context/artifacts.js';
|
|
|
26
26
|
import { manualCompact, contextState, newSessionId, saveSession, loadSession, listSessions, appendCurrentSessionRuntimeEvent, hashTraceValue, } from '../session/index.js';
|
|
27
27
|
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, getCurrentTurnId, } from '../rollback/index.js';
|
|
28
28
|
import { listSkills, effectiveSystemPrompt, } from '../skills/index.js';
|
|
29
|
-
import { buildMemorySection, buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, loadAll, } from '../memory/index.js';
|
|
30
|
-
import { buildSnapshot, clearSnapshotCache } from '../project-snapshot/index.js';
|
|
29
|
+
import { buildMemorySection, buildMemoryIndexSection, invalidateMemoryCache, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, loadAll, } from '../memory/index.js';
|
|
31
30
|
import fs from 'node:fs';
|
|
32
31
|
import path from 'node:path';
|
|
33
32
|
import { getSandboxRoot } from '../sandbox/root.js';
|
|
@@ -72,24 +71,6 @@ function buildSlashCommands() {
|
|
|
72
71
|
{ name: 'status', value: '/subagent status', desc: d('commands.subagentStatus') },
|
|
73
72
|
],
|
|
74
73
|
},
|
|
75
|
-
{
|
|
76
|
-
name: '/project_skill', desc: d('commands.skill'), children: [
|
|
77
|
-
{ name: 'toggle', value: '/project_skill', desc: d('commands.toggle') },
|
|
78
|
-
{ name: 'on', value: '/project_skill on', desc: d('commands.skillOn') },
|
|
79
|
-
{ name: 'off', value: '/project_skill off', desc: d('commands.skillOff') },
|
|
80
|
-
{ name: 'view', value: '/project_skill view', desc: d('commands.skillView') },
|
|
81
|
-
{ name: 'init', value: '/project_skill init', desc: d('commands.skillInit') },
|
|
82
|
-
],
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: '/snapshot', desc: d('commands.snapshot'), children: [
|
|
86
|
-
{ name: 'toggle', value: '/snapshot', desc: d('commands.snapshotToggle') },
|
|
87
|
-
{ name: 'on', value: '/snapshot on', desc: d('commands.snapshotOn') },
|
|
88
|
-
{ name: 'off', value: '/snapshot off', desc: d('commands.snapshotOff') },
|
|
89
|
-
{ name: 'status', value: '/snapshot status', desc: d('commands.snapshotStatus') },
|
|
90
|
-
{ name: 'refresh', value: '/snapshot_refresh', desc: d('commands.snapshotRefresh') },
|
|
91
|
-
],
|
|
92
|
-
},
|
|
93
74
|
{ name: '/theme', desc: d('commands.theme') },
|
|
94
75
|
{
|
|
95
76
|
name: '/model', desc: d('commands.model'), children: [
|
|
@@ -181,12 +162,18 @@ function maskKey(k) {
|
|
|
181
162
|
}
|
|
182
163
|
/**
|
|
183
164
|
* /init 指令:发给 agent 扫描项目并生成 MOCODE.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
165
|
+
*
|
|
166
|
+
* 函数化(非 const):.codegraph/ 索引是否存在的探测放在调用瞬间,没索引时不提 codegraph,
|
|
167
|
+
* 避免 LLM 调出失败。/init 是冷启动动作,IO 开销可忽略。
|
|
184
168
|
*/
|
|
185
|
-
|
|
169
|
+
function buildInitPrompt() {
|
|
170
|
+
const cg = hasCodegraphIndex()
|
|
171
|
+
? '- 若有 .codegraph/:用 use_skill 加载 codegraph skill 后用 run_command 调 codegraph explore "<架构或入口符号>" 一次拿相关源码+调用路径,别逐文件读!!!\n'
|
|
172
|
+
: '';
|
|
173
|
+
return `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
186
174
|
|
|
187
175
|
先探查(尽量少调用拿全貌):
|
|
188
|
-
-
|
|
189
|
-
- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
|
|
176
|
+
${cg}- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
|
|
190
177
|
- glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
|
|
191
178
|
- 若 MOCODE.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
|
|
192
179
|
|
|
@@ -204,9 +191,10 @@ install / dev / build / test / typecheck / lint 等——从 package.json script
|
|
|
204
191
|
|
|
205
192
|
硬要求:
|
|
206
193
|
- 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
|
|
207
|
-
- 总长 ≤
|
|
194
|
+
- 总长 ≤ 3000 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
|
|
208
195
|
- 用 write_file 写入项目根 MOCODE.md。
|
|
209
196
|
- 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
|
|
197
|
+
}
|
|
210
198
|
/** 临时 readline 读一行(cooked,用于子提问;主输入走 promptWithSlashMenu)。 */
|
|
211
199
|
async function askLine(prompt) {
|
|
212
200
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
@@ -324,8 +312,6 @@ function runningStateFor(cmd) {
|
|
|
324
312
|
return { status: t('running.rollback'), placeholder: t('running.chooseTurn') };
|
|
325
313
|
case '/init':
|
|
326
314
|
return { status: t('running.init'), placeholder: t('running.generateMemory') };
|
|
327
|
-
case '/snapshot_refresh':
|
|
328
|
-
return { status: t('running.snapshot'), placeholder: t('running.scanning') };
|
|
329
315
|
case '/plan':
|
|
330
316
|
return { status: t('running.plan'), placeholder: '…' };
|
|
331
317
|
case '/auto':
|
|
@@ -344,10 +330,6 @@ function runningStateFor(cmd) {
|
|
|
344
330
|
return { status: t('running.memoryStatus'), placeholder: '…' };
|
|
345
331
|
case '/subagent':
|
|
346
332
|
return { status: t('running.subagent'), placeholder: t('running.switching') };
|
|
347
|
-
case '/project_skill':
|
|
348
|
-
return { status: t('running.skill'), placeholder: t('running.processing') };
|
|
349
|
-
case '/snapshot':
|
|
350
|
-
return { status: t('running.snapshot'), placeholder: t('running.switching') };
|
|
351
333
|
case '/language':
|
|
352
334
|
return { status: t('running.language'), placeholder: t('running.chooseLanguage') };
|
|
353
335
|
case '/upgrade':
|
|
@@ -724,12 +706,6 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
724
706
|
const mcpReport = await initializeAllMcp();
|
|
725
707
|
registerToolsExtension('mcp', getMcpTools());
|
|
726
708
|
refreshChatTools();
|
|
727
|
-
// 项目快照:sandboxRoot 设定后异步构建(完全由 LLM 生成)。
|
|
728
|
-
// 构建失败不阻断 REPL:snapshot 内部 catch 所有异常。
|
|
729
|
-
if (config.projectSnapshotEnabled) {
|
|
730
|
-
// 异步触发 LLM 快照生成(不阻塞 REPL 启动)
|
|
731
|
-
buildSnapshot().catch(() => { });
|
|
732
|
-
}
|
|
733
709
|
// --resume:读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
734
710
|
let currentSessionId = sessionId;
|
|
735
711
|
if (!currentSessionId)
|
|
@@ -1118,9 +1094,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1118
1094
|
// 语言命令把视觉分隔放在确认文案之后;避免回显后先空一行、下一条命令却紧贴确认。
|
|
1119
1095
|
echoInput(input, cmd !== '/language');
|
|
1120
1096
|
const state = runningStateFor(cmd);
|
|
1121
|
-
const placeholder =
|
|
1122
|
-
? ''
|
|
1123
|
-
: state.placeholder;
|
|
1097
|
+
const placeholder = state.placeholder;
|
|
1124
1098
|
refreshStatusBase(history);
|
|
1125
1099
|
layout.enterRunningMode(state.status, placeholder);
|
|
1126
1100
|
if (line === '/help') {
|
|
@@ -1157,7 +1131,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1157
1131
|
}
|
|
1158
1132
|
if (line === '/init') {
|
|
1159
1133
|
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 MOCODE.md),fall through 走 runAgent
|
|
1160
|
-
joined =
|
|
1134
|
+
joined = buildInitPrompt();
|
|
1161
1135
|
}
|
|
1162
1136
|
if (line === '/upgrade' || line.startsWith('/upgrade ')) {
|
|
1163
1137
|
const arg = line === '/upgrade' ? '' : line.slice('/upgrade '.length).trim().toLowerCase();
|
|
@@ -1425,16 +1399,16 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1425
1399
|
continue;
|
|
1426
1400
|
}
|
|
1427
1401
|
if (line === '/compact' || line.startsWith('/compact ')) {
|
|
1428
|
-
// /compact
|
|
1429
|
-
// --force
|
|
1402
|
+
// /compact 默认强制压缩(force=true),不受阈值/保护区限制
|
|
1403
|
+
// 语法:/compact [focus] 或 /compact --no-force [focus] (显式关闭强制)
|
|
1430
1404
|
const rest = line.slice('/compact'.length).trim();
|
|
1431
|
-
let force =
|
|
1405
|
+
let force = true; // 默认强制
|
|
1432
1406
|
let focus;
|
|
1433
|
-
if (rest === '--force')
|
|
1434
|
-
force =
|
|
1435
|
-
else if (rest.startsWith('--force ')) {
|
|
1436
|
-
force =
|
|
1437
|
-
focus = rest.slice('--force '.length).trim() || undefined;
|
|
1407
|
+
if (rest === '--no-force')
|
|
1408
|
+
force = false;
|
|
1409
|
+
else if (rest.startsWith('--no-force ')) {
|
|
1410
|
+
force = false;
|
|
1411
|
+
focus = rest.slice('--no-force '.length).trim() || undefined;
|
|
1438
1412
|
}
|
|
1439
1413
|
else if (rest)
|
|
1440
1414
|
focus = rest;
|
|
@@ -1500,44 +1474,6 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1500
1474
|
}
|
|
1501
1475
|
continue;
|
|
1502
1476
|
}
|
|
1503
|
-
if (line === '/snapshot_refresh') {
|
|
1504
|
-
if (!config.projectSnapshotEnabled) {
|
|
1505
|
-
layout.contentWrite(`${ui.dim}(项目快照功能已关闭,MOCODE_PROJECT_SNAPSHOT=false)${ui.reset}\n`);
|
|
1506
|
-
continue;
|
|
1507
|
-
}
|
|
1508
|
-
// buildSnapshot 是异步操作(完全由 LLM 生成),显示进度提示
|
|
1509
|
-
layout.contentWrite(`${ui.dim}正在重新生成项目快照 (LLM 分析中)...${ui.reset}\n`);
|
|
1510
|
-
const signal = startRunningListener('');
|
|
1511
|
-
try {
|
|
1512
|
-
// 清除缓存,强制重新生成
|
|
1513
|
-
clearSnapshotCache();
|
|
1514
|
-
const result = await buildSnapshot(signal, true);
|
|
1515
|
-
if (result.snapshot) {
|
|
1516
|
-
layout.contentWrite(`${ui.cyan}✓ 项目快照已刷新${ui.reset} (${result.snapshot.builtAt})\n`);
|
|
1517
|
-
layout.contentWrite(`${ui.dim}已生成 markdown 快照,注入到系统提示词中${ui.reset}\n`);
|
|
1518
|
-
// 刷新 history[0]:buildSnapshotSection 内部 loadSnapshot() 会拿到新快照,
|
|
1519
|
-
// buildSystemMessage 重新拼 system prompt,快照段落就更新了。
|
|
1520
|
-
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1521
|
-
layout.contentWrite(`${ui.dim}(system prompt 已同步刷新)${ui.reset}\n`);
|
|
1522
|
-
}
|
|
1523
|
-
else {
|
|
1524
|
-
layout.contentWrite(`${ui.red}✗ 快照生成失败${ui.reset}: ${result.error || '未知错误'}\n`);
|
|
1525
|
-
if (result.transcript) {
|
|
1526
|
-
layout.contentWrite(`${ui.dim}--- 子 agent 日志 ---${ui.reset}\n`);
|
|
1527
|
-
layout.contentWrite(`${ui.dim}${result.transcript}${ui.reset}\n`);
|
|
1528
|
-
layout.contentWrite(`${ui.dim}--- 日志结束 ---${ui.reset}\n`);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
catch (e) {
|
|
1533
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1534
|
-
layout.contentWrite(`${ui.red}[错误]${ui.reset} 刷新快照失败: ${msg}\n`);
|
|
1535
|
-
}
|
|
1536
|
-
finally {
|
|
1537
|
-
stopRunningListener();
|
|
1538
|
-
}
|
|
1539
|
-
continue;
|
|
1540
|
-
}
|
|
1541
1477
|
if (line === '/sessions') {
|
|
1542
1478
|
// /sessions:浏览全部已保存会话(慢路径,readdir+全量 JSON.parse,目录 N 大时会有可感知卡顿)。
|
|
1543
1479
|
// 默认走 /resume(仅最近 10 条,瞬开);要翻历史续接更早的会话才用这条。
|
|
@@ -2098,201 +2034,6 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
2098
2034
|
}
|
|
2099
2035
|
continue;
|
|
2100
2036
|
}
|
|
2101
|
-
if (line === '/project_skill' ||
|
|
2102
|
-
line.startsWith('/project_skill ')) {
|
|
2103
|
-
// /project_skill — 项目专属 Skill 总开关 + 查看/编辑。
|
|
2104
|
-
// 用法:
|
|
2105
|
-
// /project_skill 切换 on/off
|
|
2106
|
-
// /project_skill on|off 显式开关
|
|
2107
|
-
// /project_skill view 查看当前内容
|
|
2108
|
-
// /project_skill edit 打开编辑器修改
|
|
2109
|
-
try {
|
|
2110
|
-
const arg = line.startsWith('/project_skill ')
|
|
2111
|
-
? line.slice('/project_skill '.length).trim().toLowerCase()
|
|
2112
|
-
: '';
|
|
2113
|
-
// /project_skill view — 查看当前内容
|
|
2114
|
-
if (arg === 'view') {
|
|
2115
|
-
const enabled = isProjectSkillEnabled();
|
|
2116
|
-
layout.contentWrite(`${ui.accent}项目专属 Skill:${ui.reset} ${enabled ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
2117
|
-
if (enabled) {
|
|
2118
|
-
const { readProjectSkill } = await import('../project-skill/index.js');
|
|
2119
|
-
const content = readProjectSkill();
|
|
2120
|
-
if (content) {
|
|
2121
|
-
layout.contentWrite(`${ui.dim}--- 内容 ---${ui.reset}\n${content}\n${ui.dim}--- 结束 ---${ui.reset}\n`);
|
|
2122
|
-
}
|
|
2123
|
-
else {
|
|
2124
|
-
layout.contentWrite(`${ui.dim}(尚未创建,可用 /project_skill init 生成初始内容)${ui.reset}\n`);
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
else {
|
|
2128
|
-
layout.contentWrite(`${ui.dim}(功能已关闭,用 /project_skill on 开启)${ui.reset}\n`);
|
|
2129
|
-
}
|
|
2130
|
-
continue;
|
|
2131
|
-
}
|
|
2132
|
-
// /project_skill init — 作为一条普通主 Agent 请求生成/优化 skill,不再派生高消耗子 agent。
|
|
2133
|
-
if (arg === 'init') {
|
|
2134
|
-
if (!isProjectSkillEnabled()) {
|
|
2135
|
-
layout.contentWrite(`${ui.yellow}⚠ 项目专属 Skill 尚未开启${ui.reset},请先运行 ${ui.cyan}/project_skill on${ui.reset}\n`);
|
|
2136
|
-
continue;
|
|
2137
|
-
}
|
|
2138
|
-
const initPrompt = `请直接初始化或优化当前项目的 Project Skill,并完成写入。
|
|
2139
|
-
|
|
2140
|
-
要求:
|
|
2141
|
-
1. 直接由你完成,禁止调用 sub-agent 工具或派生任何子 agent。
|
|
2142
|
-
2. 优先利用系统提示中已有的 Project Snapshot 和 Project Skill;不要重复扫描其中已有的目录、依赖、命令和模块清单。
|
|
2143
|
-
3. 最多进行 1 次 codegraph 探索;只有缺少关键依据时,才额外进行少量定点 read_file/grep。禁止全仓 glob 和逐文件扫描。
|
|
2144
|
-
4. Skill 只记录 Snapshot 无法提供的 WHY/HOW/GOTCHAS/CONVENTIONS:设计取舍、关键调用链、非直觉边界、项目约定和可操作坑点。使用具体路径和例子,删除重复或过时内容。
|
|
2145
|
-
5. 最终内容应完整、结构清晰。调用 project_skill_update,使用 action=write 一次性写入完整内容。
|
|
2146
|
-
6. 写入成功后只简短说明更新了哪些关键洞察,不要输出完整 Skill。`;
|
|
2147
|
-
const previousMode = getAgentMode();
|
|
2148
|
-
try {
|
|
2149
|
-
await runTurn(initPrompt, false, '');
|
|
2150
|
-
}
|
|
2151
|
-
finally {
|
|
2152
|
-
// init 是一次明确写操作,临时使用 auto;完成后恢复用户原来的模式。
|
|
2153
|
-
if (previousMode === 'plan')
|
|
2154
|
-
setAgentMode('plan');
|
|
2155
|
-
}
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
// 开关切换
|
|
2159
|
-
let nextEnabled;
|
|
2160
|
-
if (arg === '') {
|
|
2161
|
-
nextEnabled = !isProjectSkillEnabled();
|
|
2162
|
-
}
|
|
2163
|
-
else if (['on', 'true', '1', 'yes', 'y', 'enable', 'enabled'].includes(arg)) {
|
|
2164
|
-
nextEnabled = true;
|
|
2165
|
-
}
|
|
2166
|
-
else if (['off', 'false', '0', 'no', 'n', 'disable', 'disabled'].includes(arg)) {
|
|
2167
|
-
nextEnabled = false;
|
|
2168
|
-
}
|
|
2169
|
-
else {
|
|
2170
|
-
layout.contentWrite(`${ui.yellow}/project_skill 用法:${ui.reset}\n` +
|
|
2171
|
-
` /project_skill 切换(开↔关)\n` +
|
|
2172
|
-
` /project_skill on|off 显式设值\n` +
|
|
2173
|
-
` /project_skill view 查看当前内容\n` +
|
|
2174
|
-
` /project_skill init 扫描项目生成/优化 skill(可重复运行)\n`);
|
|
2175
|
-
continue;
|
|
2176
|
-
}
|
|
2177
|
-
const prev = isProjectSkillEnabled();
|
|
2178
|
-
if (nextEnabled === prev) {
|
|
2179
|
-
layout.contentWrite(`${ui.dim}(已是 ${nextEnabled ? '开启' : '关闭'},未变更 — 持久化字段未写入)${ui.reset}\n`);
|
|
2180
|
-
continue;
|
|
2181
|
-
}
|
|
2182
|
-
updateProjectSkillConfig(nextEnabled);
|
|
2183
|
-
updateConfigKey('MOCODE_PROJECT_SKILL', nextEnabled ? 'true' : 'false');
|
|
2184
|
-
const note = nextEnabled
|
|
2185
|
-
? `${ui.green}已开启项目专属 Skill${ui.reset} — project_skill_update 工具进入工具表;` +
|
|
2186
|
-
`项目 Skill 内容会在下次拼 system message 时注入。工具表快照需重启 REPL 才完整刷新。`
|
|
2187
|
-
: `${ui.yellow}已关闭项目专属 Skill${ui.reset} — project_skill_update 工具将在下次拼 system message 时从工具表过滤;` +
|
|
2188
|
-
`项目 Skill 内容不再注入。重启 REPL 后工具表完全不出现。`;
|
|
2189
|
-
layout.contentWrite(`${note}\n`);
|
|
2190
|
-
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MOCODE_PROJECT_SKILL=${nextEnabled ? 'true' : 'false'};${ui.reset}` +
|
|
2191
|
-
(process.env.MOCODE_PROJECT_SKILL
|
|
2192
|
-
? `${ui.dim}同 session shell 未 export,文件写入即时生效)${ui.reset}\n`
|
|
2193
|
-
: `${ui.dim}下次启动仍生效)${ui.reset}\n`));
|
|
2194
|
-
}
|
|
2195
|
-
catch (e) {
|
|
2196
|
-
layout.contentWrite(`${ui.red}/project_skill 失败:${ui.reset} ${e.message}\n`);
|
|
2197
|
-
}
|
|
2198
|
-
continue;
|
|
2199
|
-
}
|
|
2200
|
-
if (line === '/snapshot' ||
|
|
2201
|
-
line.startsWith('/snapshot ')) {
|
|
2202
|
-
// /snapshot — 项目快照总开关。无参切换 on/off;/on 或 /off 显式;/status 只读。
|
|
2203
|
-
//
|
|
2204
|
-
// 设计原则(与 /project_skill / /memory_switch 对齐):
|
|
2205
|
-
// - 单一来源 config.projectSnapshotEnabled(被 buildSnapshotSection() 现拼 +
|
|
2206
|
-
// read_file 工具每次 execute 现读);改完下一轮 chat 的 system prompt 即时反映。
|
|
2207
|
-
// - 持久化字段 MOCODE_PROJECT_SNAPSHOT,默认值 true(默认开启,启动 lazy build)。
|
|
2208
|
-
// - 关闭时:system prompt 不注入快照段,read_file 不查 cache 直接走真实 readFile。
|
|
2209
|
-
// - 开启时:若还没有 snapshot.json,主动 buildProjectSnapshot() 一次(仿 startRepl
|
|
2210
|
-
// 655-658 的 lazy build 模式),否则 buildSnapshotSection() 会因 loadSnapshot=null
|
|
2211
|
-
// 返空串,用户感知不到自己刚打开的开关已经生效。
|
|
2212
|
-
try {
|
|
2213
|
-
const arg = line.startsWith('/snapshot ')
|
|
2214
|
-
? line.slice('/snapshot '.length).trim().toLowerCase()
|
|
2215
|
-
: '';
|
|
2216
|
-
// /snapshot status — 只读查询
|
|
2217
|
-
if (arg === 'status') {
|
|
2218
|
-
const on = isProjectSnapshotEnabled();
|
|
2219
|
-
layout.contentWrite(`${ui.accent}项目快照:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
2220
|
-
layout.contentWrite(`${ui.dim} 单一来源 config.projectSnapshotEnabled(${on ? 'true' : 'false'});` +
|
|
2221
|
-
`持久化 ${ui.accent}MOCODE_PROJECT_SNAPSHOT${ui.dim};` +
|
|
2222
|
-
`配置文件 ${CONFIG_PATH}${ui.reset}\n`);
|
|
2223
|
-
layout.contentWrite(`${ui.dim} 关闭时:buildBasePrompt() 不含「## Project Snapshot」段;` +
|
|
2224
|
-
`read_file 工具直接走真实 readFile,不走 snapshot cache。${ui.reset}\n`);
|
|
2225
|
-
layout.contentWrite(`${ui.dim} 切换后下次 chat 即时反映(本轮已发出的请求不会回滚)。${ui.reset}\n`);
|
|
2226
|
-
continue;
|
|
2227
|
-
}
|
|
2228
|
-
// /snapshot(无参=切换;有参=按值设)
|
|
2229
|
-
let nextEnabled;
|
|
2230
|
-
if (arg === '') {
|
|
2231
|
-
nextEnabled = !isProjectSnapshotEnabled();
|
|
2232
|
-
}
|
|
2233
|
-
else if (['on', 'true', '1', 'yes', 'y', 'enable', 'enabled'].includes(arg)) {
|
|
2234
|
-
nextEnabled = true;
|
|
2235
|
-
}
|
|
2236
|
-
else if (['off', 'false', '0', 'no', 'n', 'disable', 'disabled'].includes(arg)) {
|
|
2237
|
-
nextEnabled = false;
|
|
2238
|
-
}
|
|
2239
|
-
else {
|
|
2240
|
-
layout.contentWrite(`${ui.yellow}/snapshot 用法:${ui.reset}\n` +
|
|
2241
|
-
` /snapshot 切换(开↔关)\n` +
|
|
2242
|
-
` /snapshot on|off 显式设值\n` +
|
|
2243
|
-
` /snapshot status 查看当前状态\n`);
|
|
2244
|
-
continue;
|
|
2245
|
-
}
|
|
2246
|
-
const prev = isProjectSnapshotEnabled();
|
|
2247
|
-
if (nextEnabled === prev) {
|
|
2248
|
-
layout.contentWrite(`${ui.dim}(已是 ${nextEnabled ? '开启' : '关闭'},未变更 — 持久化字段未写入)${ui.reset}\n`);
|
|
2249
|
-
continue;
|
|
2250
|
-
}
|
|
2251
|
-
updateSnapshotConfig(nextEnabled);
|
|
2252
|
-
updateConfigKey('MOCODE_PROJECT_SNAPSHOT', nextEnabled ? 'true' : 'false');
|
|
2253
|
-
// 开启时异步 build 一次,避免 buildSnapshotSection 因 loadSnapshot=null 返空串。
|
|
2254
|
-
let built = false;
|
|
2255
|
-
if (nextEnabled) {
|
|
2256
|
-
try {
|
|
2257
|
-
clearSnapshotCache();
|
|
2258
|
-
const result = await buildSnapshot();
|
|
2259
|
-
if (result.snapshot) {
|
|
2260
|
-
built = true;
|
|
2261
|
-
}
|
|
2262
|
-
else {
|
|
2263
|
-
layout.contentWrite(`${ui.yellow}⚠ 快照构建失败:${ui.reset} ${result.error || '未知错误'} ` +
|
|
2264
|
-
`${ui.dim}(开关已切换,系统提示词中的快照段将为空,稍后可用 /snapshot_refresh 重试)${ui.reset}\n`);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
catch (e) {
|
|
2268
|
-
layout.contentWrite(`${ui.yellow}⚠ 快照构建失败:${ui.reset} ${e.message} ` +
|
|
2269
|
-
`${ui.dim}(开关已切换,系统提示词中的快照段将为空,稍后可用 /snapshot_refresh 重试)${ui.reset}\n`);
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
// 刷新 history[0] 使 system prompt 立即反映新值(仿 /snapshot_refresh)。
|
|
2273
|
-
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
2274
|
-
const note = nextEnabled
|
|
2275
|
-
? `${ui.green}已开启项目快照${ui.reset} — ` +
|
|
2276
|
-
`system prompt 将在下次 chat 注入「## Project Snapshot」段;` +
|
|
2277
|
-
`read_file 工具优先走 snapshot cache(mtime 校验失败时回退真实 readFile)。`
|
|
2278
|
-
: `${ui.yellow}已关闭项目快照${ui.reset} — ` +
|
|
2279
|
-
`system prompt 不再注入「## Project Snapshot」段;` +
|
|
2280
|
-
`read_file 工具直接走真实 readFile,不再查 cache。`;
|
|
2281
|
-
let extra = '';
|
|
2282
|
-
if (nextEnabled && built) {
|
|
2283
|
-
extra = `${ui.dim}(已构建快照;如需刷新用 /snapshot_refresh)${ui.reset}\n`;
|
|
2284
|
-
}
|
|
2285
|
-
layout.contentWrite(`${note}\n${extra}`);
|
|
2286
|
-
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MOCODE_PROJECT_SNAPSHOT=${nextEnabled ? 'true' : 'false'};` +
|
|
2287
|
-
(process.env.MOCODE_PROJECT_SNAPSHOT
|
|
2288
|
-
? `${ui.dim}同 session shell 已 export MOCODE_PROJECT_SNAPSHOT,文件写入下次启动仍以 shell 值为准;取消 shell 设置后生效)${ui.reset}\n`
|
|
2289
|
-
: `${ui.dim}下次启动仍生效)${ui.reset}\n`));
|
|
2290
|
-
}
|
|
2291
|
-
catch (e) {
|
|
2292
|
-
layout.contentWrite(`${ui.red}/snapshot 失败:${ui.reset} ${e.message}\n`);
|
|
2293
|
-
}
|
|
2294
|
-
continue;
|
|
2295
|
-
}
|
|
2296
2037
|
const bubbleRows = input.length + 2 + pendingAttachments.length; // N 行 message + 2 行尾随空(含 \n\n 留下的 open current 行) + 每附件 1 行
|
|
2297
2038
|
const shouldCommit = await awaitPendingRecall(input, pendingAttachments.length, placeholder);
|
|
2298
2039
|
if (!shouldCommit) {
|
|
@@ -2308,9 +2049,16 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
2308
2049
|
queryHistory.push(joined);
|
|
2309
2050
|
const initialPlan = getAgentMode() === 'plan'; // 轮首模式(在 runTurn 之前读)
|
|
2310
2051
|
const ok = await runTurn(joined, initialPlan, placeholder);
|
|
2052
|
+
// /init 收尾:Agent 已 write_file 写完新 MOCODE.md,失效 cache 并用新内容重建 history[0],
|
|
2053
|
+
// 否则下一轮拿到的还是写之前的旧 memory 段,必须重启 REPL 才生效。
|
|
2054
|
+
if (ok && line === '/init') {
|
|
2055
|
+
invalidateMemoryCache();
|
|
2056
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
2057
|
+
refreshStatusBase(history);
|
|
2058
|
+
}
|
|
2311
2059
|
// plan 轮正常结束(未中断 / 未抛错)→ 看轮末模式决定:
|
|
2312
|
-
// - 仍 plan
|
|
2313
|
-
// - 已 auto
|
|
2060
|
+
// - 仍 plan:模型只产计划就 STOP(模型已无 switch_mode 工具)→ 弹审批面板(原行为)。
|
|
2061
|
+
// - 已 auto:本轮被切到 auto 模式(用户用 /auto 触发的合成执行轮)→ 跳过审批,不重复打扰。
|
|
2314
2062
|
if (initialPlan && ok && getAgentMode() === 'plan') {
|
|
2315
2063
|
// 桌宠:计划审批面板弹出期间广播 waiting_human(红灯闪烁);面板不在 runAgent/hooks 体系内,
|
|
2316
2064
|
// 需在此单独广播——用户响应后由下一次 /pet 状态事件(或 idle 兜底)覆盖。
|
package/dist/sandbox/policy.js
CHANGED
|
@@ -6,16 +6,18 @@ import { jailResolve, jailGlobPattern } from './jail.js';
|
|
|
6
6
|
* - memory_*:操作 ~/.mocode 与 <cwd>/.mocode,本就该在外圈
|
|
7
7
|
* - use_skill:读 ~/.claude/skills、~/.mocode/skills、<cwd>/.mocode/skills,部分在外圈
|
|
8
8
|
* - web_*:跨网络,非文件路径
|
|
9
|
-
* - ask_human
|
|
10
|
-
* - codegraph:只读 cwd 下 .codegraph/ 索引(只读、不写盘)
|
|
9
|
+
* - ask_human:无文件路径
|
|
11
10
|
* - sub-agent:派生子 agent,通过 scoped sandbox root 使用隔离 overlay
|
|
11
|
+
*
|
|
12
|
+
* 注:codegraph 不再是核心工具,已 skill 化(用 run_command 调 CLI),
|
|
13
|
+
* 不在此处登记。switch_mode 工具已砍,模式切换仅由 repl 的 /plan / /auto /
|
|
14
|
+
* Shift+Tab 等用户面触发,不经工具路径。
|
|
12
15
|
*/
|
|
13
16
|
export const SANDBOX_EXEMPT_TOOLS = new Set([
|
|
14
17
|
'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list',
|
|
15
18
|
'use_skill',
|
|
16
19
|
'web_search', 'web_fetch',
|
|
17
|
-
'ask_human',
|
|
18
|
-
'codegraph',
|
|
20
|
+
'ask_human',
|
|
19
21
|
'sub-agent',
|
|
20
22
|
]);
|
|
21
23
|
/**
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// 内置 Skill:随 mocode 一起发布的 skill,不依赖用户目录,作为最低优先级兜底。
|
|
2
|
+
// 用户在 ~/.mocode/skills/<name>/SKILL.md 放置同名 skill 时覆盖内置版(更具体的优先)。
|
|
3
|
+
//
|
|
4
|
+
// 设计动机:把 "可被 use_skill 加载、按需激活" 的领域知识从「核心工具」剥离出来。
|
|
5
|
+
// 典型用例:codegraph 工具原本是薄壳 CLI 包装,现改成 skill,模型按需加载说明后
|
|
6
|
+
// 用 run_command 调用 codegraph CLI,核心工具集更瘦,无 codegraph 索引的项目零负担。
|
|
7
|
+
const CODEGRAPH_BODY = `# Code Graph Query
|
|
8
|
+
|
|
9
|
+
Pre-built \`.codegraph/\` 索引查询技能。**当 \`.codegraph/\` 存在时**,用它代替逐文件 grep+read,
|
|
10
|
+
能一次拿到相关源码 + 调用链 + 影响面;不存在时**不要**使用本 skill(用 read_file / glob / grep)。
|
|
11
|
+
|
|
12
|
+
## 调用方式(用 run_command 工具)
|
|
13
|
+
|
|
14
|
+
codegraph 不再是核心工具,而是 skill 化后的 CLI 调用。模型在 REPL 中这样调:
|
|
15
|
+
|
|
16
|
+
\`\`\`
|
|
17
|
+
run_command({ command: "codegraph explore <query...>" })
|
|
18
|
+
run_command({ command: "codegraph node <symbol-or-path>" })
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
注意:
|
|
22
|
+
|
|
23
|
+
- Windows 上 \`codegraph\` 是 .cmd,必须走 cmd.exe /c 包装(run_command 内部已处理,直接传 \`codegraph ...\` 即可)。
|
|
24
|
+
- 单次查询 ≤ 60s 超时,模型按需重试/换 query。
|
|
25
|
+
|
|
26
|
+
## 何时用
|
|
27
|
+
|
|
28
|
+
- **理解架构/入口模块** → \`codegraph explore "<模块名或入口符号>"\`,一次拿相关源码 + 调用路径,别再逐文件读。
|
|
29
|
+
- **定位单个符号** → \`codegraph node <symbol>\`(返回该符号源码 + callers + callees)。
|
|
30
|
+
- **读一个文件 + 依赖** → \`codegraph node <file-path> --file <file>\`(file 模式)。
|
|
31
|
+
- **评估改动影响面** → 先 \`explore\` 看 callers,再 \`node\` 单点深挖。
|
|
32
|
+
|
|
33
|
+
## 何时不用
|
|
34
|
+
|
|
35
|
+
- 没建过 \`.codegraph/\` 索引(运行 \`codegraph init\` 即可,需先 \`npm i -g @colbymchenry/codegraph\`)→ 用 read_file / glob / grep。
|
|
36
|
+
- 单个已知小文件/刚编辑过的文件 → 直接 read_file / edit_file。
|
|
37
|
+
- 同一会话内已经查过同一区域 → 复用历史结果,不要重复调。
|
|
38
|
+
|
|
39
|
+
## 输出格式
|
|
40
|
+
|
|
41
|
+
CLI dump 一般是 \`path/to/file.ts:line:col symbol\` 形式,与 grep 一样可被 read_file 消费。
|
|
42
|
+
`;
|
|
43
|
+
/** 单一事实源:加新内置 skill 在此数组加一条。 */
|
|
44
|
+
export const builtinSkills = [
|
|
45
|
+
{
|
|
46
|
+
name: 'codegraph',
|
|
47
|
+
description: 'Query a pre-built .codegraph/ index for symbol lookup, call chains, and impact analysis. ' +
|
|
48
|
+
'First choice over read_file/grep when the .codegraph/ index exists. ' +
|
|
49
|
+
'Invoke via run_command("codegraph explore …" | "codegraph node …").',
|
|
50
|
+
body: CODEGRAPH_BODY,
|
|
51
|
+
dir: 'builtin',
|
|
52
|
+
skillMdPath: 'builtin',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/** 内置 skill 名字集,供 discoverSkills 跳过/覆盖时使用。 */
|
|
56
|
+
export const builtinSkillNames = new Set(builtinSkills.map((s) => s.name));
|
package/dist/skills/discover.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import { builtinSkillNames } from './builtin-skills.js';
|
|
10
11
|
/** 把开头的 ~/ 或 ~ 展开为 home 目录(Windows 与 POSIX 通用)。 */
|
|
11
12
|
function expandHome(p) {
|
|
12
13
|
if (p === '~')
|
|
@@ -110,6 +111,12 @@ export function discoverSkills() {
|
|
|
110
111
|
continue; // 目录不存在或无权限,静默跳过
|
|
111
112
|
}
|
|
112
113
|
for (const name of entries) {
|
|
114
|
+
// 跳过内置 skill 同名目录(项目级用户版可放在 ~/.mocode/skills/<name>/
|
|
115
|
+
// 覆盖内置版;cwd/.mocode/skills/<name>/ 也算项目级覆盖)。discoverSkills 按
|
|
116
|
+
// 优先级升序遍历、项目级 Map.set 后写覆盖先写,所以这里只是「不重复登记内置」,
|
|
117
|
+
// 真实覆盖逻辑在 listSkills 的合并步骤。
|
|
118
|
+
if (builtinSkillNames.has(name))
|
|
119
|
+
continue;
|
|
113
120
|
const skillDir = path.join(dir, name);
|
|
114
121
|
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
115
122
|
try {
|
package/dist/skills/index.js
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
// skills barrel:缓存已发现的 skill、按需读正文、拼系统提示段。
|
|
2
2
|
// 被 repl(注入 systemPrompt)与 tools/builtins/use-skill(加载正文)依赖;
|
|
3
|
-
// 自身仅依赖 discover.ts + node:fs,是叶子级业务模块。
|
|
3
|
+
// 自身仅依赖 discover.ts + builtin-skills.ts + node:fs,是叶子级业务模块。
|
|
4
|
+
//
|
|
5
|
+
// 优先级:内置 skill < 用户/系统 skill < 项目级 skill(后注册覆盖先注册,
|
|
6
|
+
// 项目级最后扫描,自然在 Map 中胜出)。同名用户 skill 完全替换内置版。
|
|
4
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
8
|
import { discoverSkills, parseFrontmatter } from './discover.js';
|
|
9
|
+
import { builtinSkills } from './builtin-skills.js';
|
|
6
10
|
let cache = null;
|
|
7
|
-
/** 已发现的 skill 列表(懒加载,首次调用触发扫描;启动期 repl 调一次)。
|
|
11
|
+
/** 已发现的 skill 列表(懒加载,首次调用触发扫描;启动期 repl 调一次)。
|
|
12
|
+
* 合并策略:内置 skill 作兜底,discoverSkills 的输出(按目录优先级后写覆盖)优先生效。 */
|
|
8
13
|
export function listSkills() {
|
|
9
|
-
if (cache
|
|
10
|
-
cache
|
|
14
|
+
if (cache !== null)
|
|
15
|
+
return cache;
|
|
16
|
+
const discovered = discoverSkills();
|
|
17
|
+
const byName = new Map();
|
|
18
|
+
// 先放内置(最低优先级)
|
|
19
|
+
for (const skill of builtinSkills)
|
|
20
|
+
byName.set(skill.name, skill);
|
|
21
|
+
// 后放用户/项目级(覆盖内置)
|
|
22
|
+
for (const skill of discovered)
|
|
23
|
+
byName.set(skill.name, skill);
|
|
24
|
+
cache = Array.from(byName.values());
|
|
11
25
|
return cache;
|
|
12
26
|
}
|
|
13
27
|
/**
|
|
14
28
|
* 读取某 skill 的 SKILL.md 正文(去掉 frontmatter)。
|
|
15
29
|
* 纯函数:找不到 / 读失败返 null(错误字符串交给调用方工具层生成)。
|
|
30
|
+
* 内置 skill 直接返回 body 字段(无 fs 依赖)。
|
|
16
31
|
*/
|
|
17
32
|
export function getSkillBody(name) {
|
|
18
33
|
const skill = listSkills().find((s) => s.name === name);
|
|
19
34
|
if (!skill)
|
|
20
35
|
return null;
|
|
36
|
+
if (skill.body) {
|
|
37
|
+
const trimmed = skill.body.trim();
|
|
38
|
+
return trimmed || '(skill 正文为空)';
|
|
39
|
+
}
|
|
21
40
|
try {
|
|
22
41
|
if (!existsSync(skill.skillMdPath))
|
|
23
42
|
return null;
|