mocode-ai 0.6.9 → 0.6.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.
|
@@ -38,24 +38,12 @@ export function readProjectSkill() {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
const MAX_SKILL_CHARS = 4000;
|
|
43
|
-
/**
|
|
44
|
-
* 写入/更新项目 skill。先备份旧内容再写新内容。
|
|
45
|
-
* 返回 { ok, error? }: ok=false 时 error 说明原因(超限/IO 失败)。
|
|
46
|
-
*/
|
|
41
|
+
/** 写入/更新项目 skill。内容不设字符上限;写入前备份旧内容。 */
|
|
47
42
|
export function writeProjectSkill(content) {
|
|
48
43
|
const trimmed = content.trim();
|
|
49
|
-
if (trimmed.length > MAX_SKILL_CHARS) {
|
|
50
|
-
return {
|
|
51
|
-
ok: false,
|
|
52
|
-
error: `内容超过上限(${trimmed.length}/${MAX_SKILL_CHARS} 字符)。请精简后重试。`,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
44
|
const p = skillPath();
|
|
56
45
|
const dir = path.dirname(p);
|
|
57
46
|
try {
|
|
58
|
-
// 备份旧内容(如果有)
|
|
59
47
|
if (existsSync(p)) {
|
|
60
48
|
copyFileSync(p, backupPath());
|
|
61
49
|
}
|
|
@@ -67,95 +55,22 @@ export function writeProjectSkill(content) {
|
|
|
67
55
|
return { ok: false, error: `写入失败: ${e.message}` };
|
|
68
56
|
}
|
|
69
57
|
}
|
|
70
|
-
/**
|
|
71
|
-
* 追加内容到项目 skill 末尾(以 \n\n 分隔)。
|
|
72
|
-
* 同样受 MAX_SKILL_CHARS 限制。
|
|
73
|
-
*/
|
|
58
|
+
/** 追加内容到项目 skill 末尾(以 \n\n 分隔),内容不设字符上限。 */
|
|
74
59
|
export function appendProjectSkill(addition) {
|
|
75
60
|
const existing = readProjectSkill() ?? '';
|
|
76
61
|
const trimmed = addition.trim();
|
|
77
62
|
if (!trimmed)
|
|
78
63
|
return { ok: false, error: '追加内容为空' };
|
|
79
64
|
const separator = existing ? '\n\n' : '';
|
|
80
|
-
|
|
81
|
-
return writeProjectSkill(merged);
|
|
65
|
+
return writeProjectSkill(existing + separator + trimmed);
|
|
82
66
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*/
|
|
87
|
-
export async function compressContent(content, signal) {
|
|
88
|
-
try {
|
|
89
|
-
// 动态导入避免循环依赖
|
|
90
|
-
const { chat } = await import('../llm/index.js');
|
|
91
|
-
const messages = [
|
|
92
|
-
{
|
|
93
|
-
role: 'system',
|
|
94
|
-
content: 'You are a technical writer. Compress the following project skill content to fit within ' +
|
|
95
|
-
`${MAX_SKILL_CHARS} characters while preserving the most important information. ` +
|
|
96
|
-
'Keep concrete examples, paths, and actionable insights. Remove redundancy and verbose explanations. ' +
|
|
97
|
-
'Output ONLY the compressed content, no explanations.',
|
|
98
|
-
},
|
|
99
|
-
{ role: 'user', content },
|
|
100
|
-
];
|
|
101
|
-
const result = await chat(messages, {}, signal);
|
|
102
|
-
const compressed = result.content?.trim();
|
|
103
|
-
if (!compressed)
|
|
104
|
-
return null;
|
|
105
|
-
return compressed;
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
67
|
+
/** 兼容旧调用名:字数限制取消后直接写入,不再调用 LLM 压缩。 */
|
|
68
|
+
export async function writeProjectSkillWithCompression(content, _maxAttempts = 3, _signal) {
|
|
69
|
+
return { ...writeProjectSkill(content), compressed: false };
|
|
110
70
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*/
|
|
115
|
-
export async function writeProjectSkillWithCompression(content, maxAttempts = 3, signal) {
|
|
116
|
-
let current = content;
|
|
117
|
-
let compressed = false;
|
|
118
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
119
|
-
const result = writeProjectSkill(current);
|
|
120
|
-
if (result.ok) {
|
|
121
|
-
return { ok: true, compressed };
|
|
122
|
-
}
|
|
123
|
-
// 非超限错误直接返回
|
|
124
|
-
if (!result.error?.includes('内容超过上限')) {
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
// 超限时调用 LLM 压缩
|
|
128
|
-
const compressedContent = await compressContent(current, signal);
|
|
129
|
-
if (!compressedContent) {
|
|
130
|
-
return {
|
|
131
|
-
ok: false,
|
|
132
|
-
error: `压缩失败: ${result.error}`,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
current = compressedContent;
|
|
136
|
-
compressed = true;
|
|
137
|
-
}
|
|
138
|
-
// 多次压缩后仍超限
|
|
139
|
-
const finalCheck = writeProjectSkill(current);
|
|
140
|
-
if (finalCheck.ok) {
|
|
141
|
-
return { ok: true, compressed: true };
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
ok: false,
|
|
145
|
-
error: `经过 ${maxAttempts} 次压缩仍超限: ${finalCheck.error}`,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* 追加时自动压缩:合并后超限则调用 LLM 压缩,最多尝试 maxAttempts 次。
|
|
150
|
-
*/
|
|
151
|
-
export async function appendProjectSkillWithCompression(addition, maxAttempts = 3, signal) {
|
|
152
|
-
const existing = readProjectSkill() ?? '';
|
|
153
|
-
const trimmed = addition.trim();
|
|
154
|
-
if (!trimmed)
|
|
155
|
-
return { ok: false, error: '追加内容为空' };
|
|
156
|
-
const separator = existing ? '\n\n' : '';
|
|
157
|
-
const merged = existing + separator + trimmed;
|
|
158
|
-
return writeProjectSkillWithCompression(merged, maxAttempts, signal);
|
|
71
|
+
/** 兼容旧调用名:字数限制取消后直接追加,不再调用 LLM 压缩。 */
|
|
72
|
+
export async function appendProjectSkillWithCompression(addition, _maxAttempts = 3, _signal) {
|
|
73
|
+
return { ...appendProjectSkill(addition), compressed: false };
|
|
159
74
|
}
|
|
160
75
|
/**
|
|
161
76
|
* 生成系统提示词注入段。
|
|
@@ -19,9 +19,9 @@ Do NOT explain WHY or HOW (that's Project Skill's job).
|
|
|
19
19
|
5. Identify tech stack from dependencies
|
|
20
20
|
|
|
21
21
|
## Output Format
|
|
22
|
-
Output a markdown document between these exact delimiters:
|
|
22
|
+
Output only a markdown document between these exact delimiter lines. The delimiters are not Markdown fences and must each appear on their own line:
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
<snapshot-md>
|
|
25
25
|
# Project Snapshot
|
|
26
26
|
|
|
27
27
|
## Description
|
|
@@ -49,7 +49,7 @@ src/
|
|
|
49
49
|
tools/
|
|
50
50
|
ui/
|
|
51
51
|
\`\`\`
|
|
52
|
-
|
|
52
|
+
</snapshot-md>
|
|
53
53
|
|
|
54
54
|
## Rules
|
|
55
55
|
1. **Description**: ≤80字,中文,从 README 和 package.json description 提炼。不要"一个..."开头。
|
|
@@ -60,7 +60,36 @@ src/
|
|
|
60
60
|
6. 不要包含:设计决策、注意事项、坑点、约定 → 这些是 Skill 的职责。
|
|
61
61
|
7. 总 markdown ≤ 2500 字符。
|
|
62
62
|
8. 使用中文。
|
|
63
|
+
9. 不要在 <snapshot-md> 和 </snapshot-md> 之外输出任何内容。
|
|
63
64
|
`;
|
|
65
|
+
/**
|
|
66
|
+
* 提取子 agent 输出中的 markdown。
|
|
67
|
+
* 优先使用无歧义标签,同时兼容历史自定义围栏和标准 Markdown 围栏。
|
|
68
|
+
*/
|
|
69
|
+
function extractSnapshotMarkdown(summary) {
|
|
70
|
+
const tagged = summary.match(/<snapshot-md>[ \t]*\r?\n?([\s\S]*?)\r?\n?[ \t]*<\/snapshot-md>/i);
|
|
71
|
+
if (tagged?.[1]?.trim())
|
|
72
|
+
return tagged[1].trim();
|
|
73
|
+
const legacy = summary.match(/```snapshot-md[ \t]*\r?\n([\s\S]*?)\r?\n```snapshot-md[ \t]*(?:\r?\n|$)/i);
|
|
74
|
+
if (legacy?.[1]?.trim())
|
|
75
|
+
return legacy[1].trim();
|
|
76
|
+
const opening = summary.match(/^```(?:snapshot-md|markdown)[ \t]*\r?$/im);
|
|
77
|
+
if (opening?.index === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
let bodyStart = opening.index + opening[0].length;
|
|
80
|
+
if (summary[bodyStart] === '\n')
|
|
81
|
+
bodyStart += 1;
|
|
82
|
+
const body = summary.slice(bodyStart);
|
|
83
|
+
const closings = [...body.matchAll(/^`{3,}[ \t]*\r?$/gm)];
|
|
84
|
+
const closing = closings.at(-1);
|
|
85
|
+
if (closing?.index === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
let content = body.slice(0, closing.index).trim();
|
|
88
|
+
// 有些模型会把内层和外层的结束围栏连成六个反引号。
|
|
89
|
+
if (closing[0].trim().length >= 6)
|
|
90
|
+
content = `${content}\n\`\`\``;
|
|
91
|
+
return content || undefined;
|
|
92
|
+
}
|
|
64
93
|
/**
|
|
65
94
|
* 生成 LLM 快照(markdown 格式)
|
|
66
95
|
* @param root 项目根目录
|
|
@@ -101,21 +130,15 @@ export async function generateLLMSnapshot(root, signal) {
|
|
|
101
130
|
transcript: result.transcript,
|
|
102
131
|
};
|
|
103
132
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
ok: false,
|
|
112
|
-
error: '无法从子 agent 输出中提取 markdown',
|
|
113
|
-
transcript: result.transcript,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
return { ok: true, content: fallbackMatch[1].trim(), transcript: result.transcript };
|
|
133
|
+
const content = extractSnapshotMarkdown(result.summary);
|
|
134
|
+
if (!content) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
error: '无法从子 agent 输出中提取 markdown',
|
|
138
|
+
transcript: result.transcript,
|
|
139
|
+
};
|
|
117
140
|
}
|
|
118
|
-
return { ok: true, content
|
|
141
|
+
return { ok: true, content, transcript: result.transcript };
|
|
119
142
|
}
|
|
120
143
|
catch (e) {
|
|
121
144
|
return {
|
package/dist/repl/index.js
CHANGED
|
@@ -1011,9 +1011,12 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1011
1011
|
const cmd = line.split(/\s+/)[0];
|
|
1012
1012
|
// 语言命令把视觉分隔放在确认文案之后;避免回显后先空一行、下一条命令却紧贴确认。
|
|
1013
1013
|
echoInput(input, cmd !== '/language');
|
|
1014
|
-
const
|
|
1014
|
+
const state = runningStateFor(cmd);
|
|
1015
|
+
const placeholder = line === '/snapshot_refresh' || line === '/project_skill init'
|
|
1016
|
+
? ''
|
|
1017
|
+
: state.placeholder;
|
|
1015
1018
|
refreshStatusBase(history);
|
|
1016
|
-
layout.enterRunningMode(status, placeholder);
|
|
1019
|
+
layout.enterRunningMode(state.status, placeholder);
|
|
1017
1020
|
if (line === '/help') {
|
|
1018
1021
|
layout.contentWrite(`${ui.bold}${t('help.title')}${ui.reset}\n`);
|
|
1019
1022
|
layout.contentWrite(`${ui.dim}${t('help.hint')}${ui.reset}\n`);
|
|
@@ -1311,10 +1314,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1311
1314
|
}
|
|
1312
1315
|
// buildSnapshot 是异步操作(完全由 LLM 生成),显示进度提示
|
|
1313
1316
|
layout.contentWrite(`${ui.dim}正在重新生成项目快照 (LLM 分析中)...${ui.reset}\n`);
|
|
1317
|
+
const signal = startRunningListener('');
|
|
1314
1318
|
try {
|
|
1315
1319
|
// 清除缓存,强制重新生成
|
|
1316
1320
|
clearSnapshotCache();
|
|
1317
|
-
const result = await buildSnapshot(
|
|
1321
|
+
const result = await buildSnapshot(signal, true);
|
|
1318
1322
|
if (result.snapshot) {
|
|
1319
1323
|
layout.contentWrite(`${ui.cyan}✓ 项目快照已刷新${ui.reset} (${result.snapshot.builtAt})\n`);
|
|
1320
1324
|
layout.contentWrite(`${ui.dim}已生成 markdown 快照,注入到系统提示词中${ui.reset}\n`);
|
|
@@ -1336,6 +1340,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1336
1340
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1337
1341
|
layout.contentWrite(`${ui.red}[错误]${ui.reset} 刷新快照失败: ${msg}\n`);
|
|
1338
1342
|
}
|
|
1343
|
+
finally {
|
|
1344
|
+
stopRunningListener();
|
|
1345
|
+
}
|
|
1339
1346
|
continue;
|
|
1340
1347
|
}
|
|
1341
1348
|
if (line === '/sessions') {
|
|
@@ -1903,48 +1910,29 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1903
1910
|
}
|
|
1904
1911
|
continue;
|
|
1905
1912
|
}
|
|
1906
|
-
// /project_skill init —
|
|
1913
|
+
// /project_skill init — 作为一条普通主 Agent 请求生成/优化 skill,不再派生高消耗子 agent。
|
|
1907
1914
|
if (arg === 'init') {
|
|
1908
1915
|
if (!isProjectSkillEnabled()) {
|
|
1909
1916
|
layout.contentWrite(`${ui.yellow}⚠ 项目专属 Skill 尚未开启${ui.reset},请先运行 ${ui.cyan}/project_skill on${ui.reset}\n`);
|
|
1910
1917
|
continue;
|
|
1911
1918
|
}
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
// 显示探索过程日志
|
|
1925
|
-
if (result.transcript) {
|
|
1926
|
-
layout.contentWrite(`${ui.dim}--- 探索过程 ---${ui.reset}\n`);
|
|
1927
|
-
layout.contentWrite(result.transcript);
|
|
1928
|
-
layout.contentWrite(`${ui.dim}--- 探索结束 ---${ui.reset}\n\n`);
|
|
1929
|
-
}
|
|
1930
|
-
if (result.ok && result.content) {
|
|
1931
|
-
const writeResult = writeProjectSkill(result.content);
|
|
1932
|
-
if (writeResult.ok) {
|
|
1933
|
-
layout.contentWrite(`${ui.green}✓ 项目 Skill 已生成${ui.reset}\n`);
|
|
1934
|
-
layout.contentWrite(`${ui.dim}内容预览:${ui.reset}\n${result.content.slice(0, 500)}${result.content.length > 500 ? '...' : ''}\n`);
|
|
1935
|
-
layout.contentWrite(`\n${ui.dim}文件: ${ui.accent}.mocode/project-skill.md${ui.reset} (${result.content.length} 字符)\n`);
|
|
1936
|
-
layout.contentWrite(`${ui.dim}下次启动时会自动注入到系统提示词。Agent 也会在开发过程中持续更新。${ui.reset}\n`);
|
|
1937
|
-
layout.contentWrite(`${ui.dim}提示: 可用 ${ui.cyan}/project_skill view${ui.reset} 查看完整内容,或手动编辑文件完善。${ui.dim}${ui.reset}\n`);
|
|
1938
|
-
}
|
|
1939
|
-
else {
|
|
1940
|
-
layout.contentWrite(`${ui.red}✗ 写入失败:${ui.reset} ${writeResult.error}\n`);
|
|
1941
|
-
}
|
|
1919
|
+
const initPrompt = `请直接初始化或优化当前项目的 Project Skill,并完成写入。
|
|
1920
|
+
|
|
1921
|
+
要求:
|
|
1922
|
+
1. 直接由你完成,禁止调用 task 工具或派生任何子 agent。
|
|
1923
|
+
2. 优先利用系统提示中已有的 Project Snapshot 和 Project Skill;不要重复扫描其中已有的目录、依赖、命令和模块清单。
|
|
1924
|
+
3. 最多进行 1 次 codegraph 探索;只有缺少关键依据时,才额外进行少量定点 read_file/grep。禁止全仓 glob 和逐文件扫描。
|
|
1925
|
+
4. Skill 只记录 Snapshot 无法提供的 WHY/HOW/GOTCHAS/CONVENTIONS:设计取舍、关键调用链、非直觉边界、项目约定和可操作坑点。使用具体路径和例子,删除重复或过时内容。
|
|
1926
|
+
5. 最终内容应完整、结构清晰。调用 project_skill_update,使用 action=write 一次性写入完整内容。
|
|
1927
|
+
6. 写入成功后只简短说明更新了哪些关键洞察,不要输出完整 Skill。`;
|
|
1928
|
+
const previousMode = getAgentMode();
|
|
1929
|
+
try {
|
|
1930
|
+
await runTurn(initPrompt, false, '');
|
|
1942
1931
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
if (
|
|
1946
|
-
|
|
1947
|
-
}
|
|
1932
|
+
finally {
|
|
1933
|
+
// init 是一次明确写操作,临时使用 auto;完成后恢复用户原来的模式。
|
|
1934
|
+
if (previousMode === 'plan')
|
|
1935
|
+
setAgentMode('plan');
|
|
1948
1936
|
}
|
|
1949
1937
|
continue;
|
|
1950
1938
|
}
|
|
@@ -38,13 +38,11 @@ export const projectSkillUpdateTool = {
|
|
|
38
38
|
'**何时更新**:发现新架构模式、踩坑后总结、学到新约定、完成重要重构、用户纠正理解时。\n' +
|
|
39
39
|
'\n' +
|
|
40
40
|
'**注意事项**:\n' +
|
|
41
|
-
'-
|
|
41
|
+
'- 内容长度不设硬上限,按项目实际复杂度完整记录有价值的信息\n' +
|
|
42
42
|
'- 写可操作的内容,避免空泛描述\n' +
|
|
43
43
|
'- 用具体路径和例子(不要写"有多个模块",要写"src/agent 负责 agent 循环")\n' +
|
|
44
|
-
'-
|
|
45
|
-
'- 更新前建议先 `read`
|
|
46
|
-
'\n' +
|
|
47
|
-
'**自动压缩**:内容超限时会自动调用 LLM 压缩(最多 3 次),无需手动精简。',
|
|
44
|
+
'- 定期整理,删除过时和重复信息\n' +
|
|
45
|
+
'- 更新前建议先 `read` 看一下现有内容,避免重复。',
|
|
48
46
|
risk: 'safe',
|
|
49
47
|
parameters: {
|
|
50
48
|
type: 'object',
|