mocode-ai 0.4.9 → 0.4.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 +18 -0
- package/dist/agent/core.js +5 -2
- package/dist/agent/index.js +1 -1
- package/dist/config/index.js +16 -7
- package/dist/llm/index.js +22 -1
- package/dist/project-skill/index.js +81 -30
- package/dist/project-skill/initializer.js +45 -46
- package/dist/project-snapshot/index.js +39 -131
- package/dist/project-snapshot/llm-snapshot.js +127 -0
- package/dist/repl/index.js +132 -24
- package/dist/tools/builtins/project-skill-update.js +38 -7
- package/dist/tools/builtins/read-file.js +1 -17
- package/dist/tools/builtins/todolist.js +124 -8
- package/dist/ui/batch.js +19 -2
- package/dist/ui/content.js +54 -0
- package/dist/ui/layout.js +57 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
18
18
|
- **Plan / Auto dual mode** — In `plan` mode the agent is read-only (reads code, queries indexes, searches — never writes to disk, runs commands, or spawns sub-agents) and produces a plan; `auto` mode unlocks the full toolset. The agent can switch between the two on its own — scope out an unfamiliar codebase first, then start making changes.
|
|
19
19
|
- **Automatic context compression** — As the context window fills up, a three-tier compression kicks in (trim individual results → compact older tool results in place → summarize older turns), so long sessions never overflow. `/context` shows live token usage; `/compact` triggers manual compression (optionally with a focus hint to preserve what matters).
|
|
20
20
|
- **Cross-session long-term memory** — The agent can save project architecture, conventions, and lessons learned as long-term memory, auto-loaded in future sessions. A background process periodically reflects on conversations to mine things worth remembering. Memories can be created, searched, updated, and forgotten, with recall-based decay.
|
|
21
|
+
- **Project context (Snapshot + Skill)** — Two complementary systems help the agent understand your project: **Project Snapshot** automatically scans files and generates LLM-enhanced summaries (project description, tech stack, commands, module responsibilities, directory tree); **Project Skill** is a manually maintained knowledge base capturing design decisions, architectural insights, pitfalls, and conventions. Snapshot provides *what/where* (facts), Skill provides *why/how* (insights) — no duplication, ~46% token savings. See [docs/USAGE_SNAPSHOT_SKILL.md](./docs/USAGE_SNAPSHOT_SKILL.md) for details.
|
|
21
22
|
- **Working notepad (todolist)** — For complex multi-step tasks (≥3 file changes / ≥5 tool calls), the agent first writes a plan to `.mocode/plans/<id>.md` (file-based, survives context compression), then ticks each step as it goes. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]`. `finish` auto-archives completed plans to `plans/archive/`, with explicit `list / delete / unarchive` actions.
|
|
22
23
|
- **Interruptible and reversible** — Ctrl+C interrupts the current turn at any time (kills child processes recursively, rolls history back to before the turn started, leaves no half-finished tool calls). `/rollback` restores file changes from per-turn snapshots, with a per-file keep/undo choice — no git dependency required.
|
|
23
24
|
- **Sandbox protection** — File reads/writes go through a sandbox that blocks out-of-bounds paths (`../../`, absolute paths outside the root, symlink escapes, etc.), so the agent never touches files outside your working directory.
|
|
@@ -174,6 +175,8 @@ The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle
|
|
|
174
175
|
| `/pet` | Toggle the optional desktop pet (floating window mirroring agent state) |
|
|
175
176
|
| `/pet skin` | Pick a pet skin (↑↓ · Enter) |
|
|
176
177
|
| `/pet quit` | Fully shut down the pet process (not just disconnect) |
|
|
178
|
+
| `/snapshot_refresh` | Refresh project snapshot (re-scan files + regenerate LLM summary) |
|
|
179
|
+
| `/snapshot` | Toggle project snapshot on/off |
|
|
177
180
|
|
|
178
181
|
Type `/` to trigger the dropdown menu, keep typing to filter; Esc to cancel.
|
|
179
182
|
|
|
@@ -201,6 +204,21 @@ MoCode automatically scans the following directories for skills (each skill is a
|
|
|
201
204
|
|
|
202
205
|
A skill's `description` is injected into the system prompt (progressive disclosure, tier 1); the model calls `use_skill` to load the full body (tier 2) only when the task is relevant. Use `/skills` to see discovered skills.
|
|
203
206
|
|
|
207
|
+
## Project Context (Snapshot + Skill)
|
|
208
|
+
|
|
209
|
+
MoCode uses two complementary systems to help the agent understand your project:
|
|
210
|
+
|
|
211
|
+
- **Project Snapshot** (automatic + LLM-enhanced): Scans your project files and generates a structured summary including project description, tech stack, key commands, module responsibilities, and directory tree. Built automatically on startup (Phase 1: sync scan ~100ms, Phase 2: async LLM summary ~5s). Stored in `.mocode/snapshot.json`, reused across sessions. Refresh manually with `/snapshot_refresh` after major changes.
|
|
212
|
+
- **Project Skill** (manual + AI-assisted): A knowledge base you maintain capturing insights the agent can't auto-discover — design decisions, architectural patterns, pitfalls, conventions, and development workflow notes. Initialize with `/init` (AI explores your project and drafts the initial content), then edit `.mocode/project-skill.md` to refine. Update via the `project_skill_update` tool during conversations.
|
|
213
|
+
|
|
214
|
+
**Complementary principle**: Snapshot provides *what/where* (facts: files, structure, commands), Skill provides *why/how* (insights: decisions, behaviors, gotchas). No duplication, ~46% token savings compared to the previous approach.
|
|
215
|
+
|
|
216
|
+
Both are enabled by default. Control via environment variables:
|
|
217
|
+
- `MOCODE_PROJECT_SNAPSHOT=false` — disable snapshot
|
|
218
|
+
- `MOCODE_PROJECT_SKILL=false` — disable skill
|
|
219
|
+
|
|
220
|
+
See [docs/USAGE_SNAPSHOT_SKILL.md](./docs/USAGE_SNAPSHOT_SKILL.md) for detailed usage.
|
|
221
|
+
|
|
204
222
|
## Project memory (MOCODE.md)
|
|
205
223
|
|
|
206
224
|
MoCode has a **two-tier memory** model distinct from skills:
|
package/dist/agent/core.js
CHANGED
|
@@ -74,10 +74,13 @@ function readDiffContext(tc, parsed) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
if (tc.name === 'edit_file') {
|
|
77
|
-
|
|
77
|
+
// 行尾归一化:LLM 生成的 old_string 用 LF(\n),但 Windows 文件可能是 CRLF(\r\n),
|
|
78
|
+
// 不统一则 indexOf 必败、editStartLine 恒为 1。与 edit-file.ts 保持一致归一化为 LF。
|
|
79
|
+
const oldStr = String(parsed.old_string ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
78
80
|
try {
|
|
79
81
|
// jailResolve:同上,沙箱越界抛错 → catch 兜底,不泄露牢外内容
|
|
80
|
-
const
|
|
82
|
+
const raw = readFileSync(jailResolve(p), 'utf8');
|
|
83
|
+
const data = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
81
84
|
const idx = oldStr ? data.indexOf(oldStr) : -1;
|
|
82
85
|
return {
|
|
83
86
|
preWriteOld: null,
|
package/dist/agent/index.js
CHANGED
|
@@ -47,7 +47,7 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
const preview = diff ? '' : summarizeToolResult(tc.name, output);
|
|
50
|
-
batch.recordResult(currentBatchId, tc.name, preview, diff);
|
|
50
|
+
batch.recordResult(currentBatchId, tc.name, preview, diff, output);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* agent 核心循环(主 agent,TUI 渲染版):
|
package/dist/config/index.js
CHANGED
|
@@ -90,9 +90,8 @@ function buildSnapshotSection() {
|
|
|
90
90
|
const snap = loadSnapshot();
|
|
91
91
|
if (!snap)
|
|
92
92
|
return '';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return `\n## Project Snapshot (cross-session cache)\n- A project snapshot is available with cached static files: [${fileList}]. read_file for these files will hit the snapshot cache (mtime-verified) — no disk read needed.\n- Project structure — top-level modules: [${modules}].\n- You already know the project skeleton; don't read_file these cached files just to "get an overview".\n- Use cached content directly: when a question can be answered from the cached files above (e.g. dependencies → package.json, compiler options → tsconfig.json, env vars → .env.example, project intro → README.md), answer from the snapshot without calling read_file.\n- The module list above is a navigation aid: when locating files, prefer targeted \`glob\` into the relevant module (e.g. \`src/**/*.ts\`) over broad top-level scans.\n`;
|
|
93
|
+
// 快照内容已经是完整的 markdown,直接返回
|
|
94
|
+
return `\n${snap.content}\n`;
|
|
96
95
|
}
|
|
97
96
|
catch {
|
|
98
97
|
return '';
|
|
@@ -201,7 +200,7 @@ ${PLATFORM_NOTE}
|
|
|
201
200
|
- Use glob to find file paths, grep to search content. **Don't use run_command for file-level checks** (existence / listing / type) — those have no clean cmd.exe equivalent and Windows path escaping fails often. Use \`glob\` to list, and just call \`read_file\` to test existence (returns ENOENT as a clean error string).
|
|
202
201
|
- run_command has side effects on the host — state intent before invoking (delete, install, push, reset, etc.).
|
|
203
202
|
- 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.
|
|
204
|
-
- **Trim context when stale**: when an old tool result is dead weight (sub-goal done, no downstream consumer, or superseded by a later read), call drop_context to stub it; otherwise rely on automatic pruning.
|
|
203
|
+
- **Trim context when stale**: when an old tool result is dead weight (sub-goal done, no downstream consumer, or superseded by a later read), call drop_context to stub it; otherwise rely on automatic pruning. **When your context gets too long, don't hesitate to use drop_context proactively** — it's cheap and designed to be called, not saved for emergencies.
|
|
205
204
|
- **Batch writes and commands too, not just reads**: the executor runs ALL returned tool_calls (reads, writes, commands) before the next LLM call. Emit independent edit_file / write_file / run_command in one response when the chain is clear — don't serialize them across turns just because they have side effects. (The read-only batching note in Step Economy applies to writes the same way.)
|
|
206
205
|
- **Chain shell workflows in a single \`run_command\`**: use \`&&\`, \`;\`, \`|\`, \`>\`, heredocs to fold multi-step scripts (\`mkdir -p x && cat > x/file.ts <<'EOF' ... EOF && npm test\`) into one call. Only emit a follow-up turn when the result forces a decision (error, ambiguous output, branching logic).
|
|
207
206
|
|
|
@@ -222,9 +221,8 @@ ${PLATFORM_NOTE}
|
|
|
222
221
|
|
|
223
222
|
${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}
|
|
224
223
|
|
|
225
|
-
## Working notepad (todolist)
|
|
226
|
-
- For tasks
|
|
227
|
-
- Plan is file-backed (\`todolist read\` to re-orient). See the tool description for the full action set.
|
|
224
|
+
## Working notepad (todolist)
|
|
225
|
+
- For genuinely complex tasks only: explore codebase → clarify with user → create plan → execute step by step. See tool description for details.
|
|
228
226
|
|
|
229
227
|
## Termination & Reporting
|
|
230
228
|
- Stop immediately when no more tools are needed; give conclusions directly.
|
|
@@ -347,3 +345,14 @@ export function updateProjectSkillConfig(enabled) {
|
|
|
347
345
|
config.projectSkillEnabled = enabled;
|
|
348
346
|
process.env.MOCODE_PROJECT_SKILL = enabled ? 'true' : 'false';
|
|
349
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* 切换项目快照开关(/snapshot on|off 调)。
|
|
350
|
+
* - 更新 config 单例字段(其它模块下次读 config.projectSnapshotEnabled 即拿新值:
|
|
351
|
+
* buildSnapshotSection 现拼现读、read-file 每次 execute 现读)。
|
|
352
|
+
* - 同步 process.env.MOCODE_PROJECT_SNAPSHOT(下次启动 loadEnvFiles 不会被文件回填)。
|
|
353
|
+
* 持久化(写 ~/.mocode/config 的 MOCODE_PROJECT_SNAPSHOT 键)由调用方走 updateConfigKey。
|
|
354
|
+
*/
|
|
355
|
+
export function updateSnapshotConfig(enabled) {
|
|
356
|
+
config.projectSnapshotEnabled = enabled;
|
|
357
|
+
process.env.MOCODE_PROJECT_SNAPSHOT = enabled ? 'true' : 'false';
|
|
358
|
+
}
|
package/dist/llm/index.js
CHANGED
|
@@ -354,6 +354,24 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
354
354
|
buf = buf.slice(i);
|
|
355
355
|
}
|
|
356
356
|
if (delta.tool_calls) {
|
|
357
|
+
// 文本→工具转折点:若 buf 还残留普通文本的安全尾(为防跨 chunk 切分留的
|
|
358
|
+
// THINK_OPEN.length - 1 字符),立即 flush 到屏幕。否则这段尾巴会一直搁置到
|
|
359
|
+
// 流结束才由尾部防御输出,而那时 onToolCall 早已触发、TUI 已补换行+生成中
|
|
360
|
+
// spinner,用户看到「话没说完就去调工具」——history 完整但屏幕渲染顺序错位。
|
|
361
|
+
// inThink 段照旧丢弃(思考中模型不会同时吐 tool_call,理论上 buf 不会有思考段);
|
|
362
|
+
// 防御性保留 !inThink 判断。
|
|
363
|
+
if (buf && !inThink) {
|
|
364
|
+
visibleContent += buf;
|
|
365
|
+
// 给 onText 渲染时剥掉尾部 \n:md 渲染器(contentWriteMd)把尾部 \n 当段落分隔 → 产空行;
|
|
366
|
+
// 随后 onToolCall 检测到 lastChar !== '\n' 会经 contentWrite('\n') 补一个原始换行
|
|
367
|
+
// (不走 md,只是普通行分隔,无空行)—— 与改造前 onToolCall 补 \n 的行为一致。
|
|
368
|
+
// visibleContent 保留原 buf(含 \n),history 完整不受影响。
|
|
369
|
+
const tail = buf.replace(/\n+$/, '');
|
|
370
|
+
if (tail)
|
|
371
|
+
handlers.onText?.(tail);
|
|
372
|
+
consumedAny = true;
|
|
373
|
+
buf = '';
|
|
374
|
+
}
|
|
357
375
|
for (const tc of delta.tool_calls) {
|
|
358
376
|
const idx = tc.index ?? 0;
|
|
359
377
|
let entry = toolAcc.get(idx);
|
|
@@ -375,11 +393,14 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
375
393
|
}
|
|
376
394
|
}
|
|
377
395
|
// 防御:循环内 buf.slice 已把可确认部分消费;此处覆盖流末尾的"安全尾":
|
|
378
|
-
// - 普通段(stream 已结束,标签不会再出现):作为可见内容追加到 visibleContent
|
|
396
|
+
// - 普通段(stream 已结束,标签不会再出现):作为可见内容追加到 visibleContent + 调 onText
|
|
397
|
+
// (之前注释说"不再调 onText"是 bug——安全尾里的真实文本会被屏幕吞掉,用户看到模型
|
|
398
|
+
// 话没说完就去调工具 / 直接结束;history 有但显示缺。现在补上 onText 让屏幕与 history 一致。)
|
|
379
399
|
// - 思考段未闭合:丢弃,防 thinking 文本泄漏到 history
|
|
380
400
|
if (buf) {
|
|
381
401
|
if (!inThink) {
|
|
382
402
|
visibleContent += buf;
|
|
403
|
+
handlers.onText?.(buf);
|
|
383
404
|
consumedAny = true;
|
|
384
405
|
}
|
|
385
406
|
buf = '';
|
|
@@ -38,8 +38,8 @@ export function readProjectSkill() {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
/** 内容硬上限(字符数)
|
|
42
|
-
const MAX_SKILL_CHARS =
|
|
41
|
+
/** 内容硬上限(字符数)。超限拒绝写入,防止系统提示词膨胀。从 6000 降至 4000,与快照互补后内容更精简。 */
|
|
42
|
+
const MAX_SKILL_CHARS = 4000;
|
|
43
43
|
/**
|
|
44
44
|
* 写入/更新项目 skill。先备份旧内容再写新内容。
|
|
45
45
|
* 返回 { ok, error? }: ok=false 时 error 说明原因(超限/IO 失败)。
|
|
@@ -80,9 +80,88 @@ export function appendProjectSkill(addition) {
|
|
|
80
80
|
const merged = existing + separator + trimmed;
|
|
81
81
|
return writeProjectSkill(merged);
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 调用 LLM 压缩内容。超限时自动精简,保留关键信息。
|
|
85
|
+
* 返回压缩后的内容,失败返回 null。
|
|
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
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 写入时自动压缩:超限则调用 LLM 压缩,最多尝试 maxAttempts 次。
|
|
113
|
+
* 返回 { ok, error?, compressed? },compressed 标记是否经过压缩。
|
|
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);
|
|
159
|
+
}
|
|
83
160
|
/**
|
|
84
161
|
* 生成系统提示词注入段。
|
|
85
162
|
* 开关关闭或文件不存在 → 空串(零行为变化)。
|
|
163
|
+
*
|
|
164
|
+
* 精简版:只注入 skill 内容本身,维护指南移到 project_skill_update 工具描述中。
|
|
86
165
|
*/
|
|
87
166
|
export function buildProjectSkillSection() {
|
|
88
167
|
const content = readProjectSkill();
|
|
@@ -97,33 +176,5 @@ export function buildProjectSkillSection() {
|
|
|
97
176
|
'<project-skill>',
|
|
98
177
|
content,
|
|
99
178
|
'</project-skill>',
|
|
100
|
-
'',
|
|
101
|
-
'### Project Skill 维护指南',
|
|
102
|
-
'你可以(也应该)在开发过程中持续更新这个 skill,让它越来越了解项目:',
|
|
103
|
-
'',
|
|
104
|
-
'**何时更新**:',
|
|
105
|
-
'- 发现项目特有的架构模式、设计决策或数据流',
|
|
106
|
-
'- 踩坑后总结出避坑指南(命名冲突、API 行为、构建陷阱等)',
|
|
107
|
-
'- 学到新的命名约定、测试规范、代码风格',
|
|
108
|
-
'- 完成重要重构或引入新模块后',
|
|
109
|
-
'- 用户纠正了你对项目的错误理解',
|
|
110
|
-
'',
|
|
111
|
-
'**更新什么**:',
|
|
112
|
-
'- 项目概述、技术栈、核心模块职责',
|
|
113
|
-
'- 常见坑点和解决方案',
|
|
114
|
-
'- 开发流程(构建、测试、部署命令)',
|
|
115
|
-
'- 关键 API 的使用方式和限制',
|
|
116
|
-
'- 设计决策的 why(不只是 what)',
|
|
117
|
-
'',
|
|
118
|
-
'**如何更新**:',
|
|
119
|
-
'- `project_skill_update(action="read")` — 查看当前内容',
|
|
120
|
-
'- `project_skill_update(action="update", content="...")` — 全量替换(适合大改)',
|
|
121
|
-
'- `project_skill_update(action="append", content="...")` — 追加到末尾(适合加新发现)',
|
|
122
|
-
'',
|
|
123
|
-
'**注意事项**:',
|
|
124
|
-
'- 保持精简,硬上限 6000 字符(约 1500 token)',
|
|
125
|
-
'- 写可操作的内容,避免空泛描述',
|
|
126
|
-
'- 定期整理,删除过时信息',
|
|
127
|
-
'- 更新前建议先 `read` 看一下现有内容,避免重复',
|
|
128
179
|
].join('\n');
|
|
129
180
|
}
|
|
@@ -5,64 +5,63 @@ import { spawnAgent } from '../agent/spawn.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* 子 agent 的系统提示后缀:角色与输出格式约束
|
|
7
7
|
*/
|
|
8
|
-
const SKILL_INIT_SUFFIX = `You are a project exploration agent. Your task is to deeply understand this project and generate a concise
|
|
9
|
-
|
|
10
|
-
##
|
|
11
|
-
|
|
8
|
+
const SKILL_INIT_SUFFIX = `You are a project exploration agent. Your task is to deeply understand this project and generate a concise "Project Skill" document.
|
|
9
|
+
|
|
10
|
+
## ⚠️ Complementary to Snapshot — CRITICAL
|
|
11
|
+
The following information is ALREADY provided by Project Snapshot — **do NOT repeat**:
|
|
12
|
+
- ✗ Project one-liner description (快照已有)
|
|
13
|
+
- ✗ Module names and file locations (快照 srcTree 已有)
|
|
14
|
+
- ✗ Tech stack names and versions (快照 techStack 已有)
|
|
15
|
+
- ✗ Build/test commands (快照 keyCommands 已有)
|
|
16
|
+
- ✗ File lists and directory structure (快照 srcTree 已有)
|
|
17
|
+
|
|
18
|
+
**Your job is to capture INSIGHTS only:**
|
|
19
|
+
- **WHY** — design decisions, trade-offs, reasons for choosing X over Y
|
|
20
|
+
- **HOW** — module behaviors, data flows, call chains, non-obvious interactions
|
|
21
|
+
- **GOTCHAS** — pitfalls, edge cases, non-intuitive behaviors
|
|
22
|
+
- **CONVENTIONS** — naming patterns, code style, unwritten rules
|
|
12
23
|
|
|
13
24
|
## Exploration Strategy
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
18
|
-
5. **Find pitfalls**: Check for complex configs, unusual dependencies, build quirks
|
|
25
|
+
1. Use \`codegraph\` to trace call chains and understand module interactions
|
|
26
|
+
2. Read core modules to understand **behaviors** (not just names)
|
|
27
|
+
3. Look for complex logic, error handling patterns, unusual configs
|
|
28
|
+
4. Identify naming conventions, common abstractions
|
|
19
29
|
|
|
20
30
|
## Output Format
|
|
21
|
-
|
|
31
|
+
Output the skill document between these exact delimiters:
|
|
22
32
|
|
|
23
33
|
\`\`\`skill-start
|
|
24
|
-
(your skill content here)
|
|
25
|
-
\`\`\`skill-end
|
|
26
|
-
|
|
27
|
-
The skill content should follow this structure (use Chinese for headings, adapt sections based on what you find):
|
|
28
|
-
|
|
29
|
-
## 项目概述
|
|
30
|
-
- 一句话描述项目是什么、做什么
|
|
31
|
-
- 核心目标用户/场景
|
|
32
|
-
|
|
33
|
-
## 技术栈
|
|
34
|
-
- 主要语言、框架、工具(附版本号)
|
|
35
|
-
- 关键技术选型理由(如果能从文档/代码推断)
|
|
36
|
-
|
|
37
34
|
## 架构要点
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
35
|
+
### 核心模块及职责
|
|
36
|
+
- **\`path/to/module\`** — 行为描述(做什么 + 怎么做)
|
|
37
|
+
...
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## 开发流程
|
|
47
|
-
- 构建、测试、lint、部署命令
|
|
48
|
-
- 开发环境配置要点
|
|
39
|
+
### 关键设计决策
|
|
40
|
+
- 决策:原因
|
|
41
|
+
...
|
|
49
42
|
|
|
50
43
|
## 项目约定
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
- 约定(具体的,有例子的)
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
## 开发流程
|
|
48
|
+
- 命令 + 注意事项/坑点(命令本身快照已有,这里只写注意什么)
|
|
49
|
+
...
|
|
54
50
|
|
|
55
51
|
## 常见坑点
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
- 坑:解法
|
|
53
|
+
...
|
|
54
|
+
\`\`\`skill-end
|
|
55
|
+
|
|
56
|
+
## Rules — MUST FOLLOW
|
|
57
|
+
1. 禁止列出文件名清单或目录结构
|
|
58
|
+
2. 禁止列出依赖名和版本
|
|
59
|
+
3. 禁止写项目概述/一句话描述
|
|
60
|
+
4. 禁止只写命令本身(快照已有),只写注意事项
|
|
61
|
+
5. 每个模块描述必须包含行为(做什么 + 怎么协作),不只是名称
|
|
62
|
+
6. **总长度目标 3000-4000 字符,硬上限 4000**
|
|
63
|
+
7. 写可操作的内容,用具体路径和例子
|
|
64
|
+
8. 如果某个 section 没有实质内容,省略它
|
|
66
65
|
`;
|
|
67
66
|
/**
|
|
68
67
|
* 生成初始项目 Skill(使用子 agent 深度探索)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getSandboxRoot } from '../sandbox/root.js';
|
|
5
|
-
import { scanStaticFiles } from './static-files.js';
|
|
6
5
|
/** 内存缓存:当前 session 的快照(避免重复 IO) */
|
|
7
6
|
let currentSnapshot = null;
|
|
8
7
|
/** 计算 sandboxRoot 的 hash,用作目录名(避免路径特殊字符) */
|
|
@@ -16,7 +15,7 @@ function snapshotDir() {
|
|
|
16
15
|
}
|
|
17
16
|
/** 快照文件路径 */
|
|
18
17
|
function snapshotPath() {
|
|
19
|
-
return path.join(snapshotDir(), 'snapshot.
|
|
18
|
+
return path.join(snapshotDir(), 'snapshot.md');
|
|
20
19
|
}
|
|
21
20
|
/** 从磁盘加载快照(不存在/损坏返 null) */
|
|
22
21
|
export function loadSnapshot() {
|
|
@@ -26,10 +25,17 @@ export function loadSnapshot() {
|
|
|
26
25
|
if (!existsSync(p))
|
|
27
26
|
return null;
|
|
28
27
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const content = readFileSync(p, 'utf8');
|
|
29
|
+
// 从 markdown 文件头部的 YAML front matter 提取元数据
|
|
30
|
+
const match = content.match(/^---\nroot: (.+)\nbuiltAt: (.+)\nversion: (\d+)\n---\n\n([\s\S]+)$/);
|
|
31
|
+
if (!match)
|
|
32
32
|
return null;
|
|
33
|
+
const snap = {
|
|
34
|
+
version: parseInt(match[3]),
|
|
35
|
+
root: match[1],
|
|
36
|
+
builtAt: match[2],
|
|
37
|
+
content: match[4],
|
|
38
|
+
};
|
|
33
39
|
currentSnapshot = snap;
|
|
34
40
|
return snap;
|
|
35
41
|
}
|
|
@@ -37,141 +43,43 @@ export function loadSnapshot() {
|
|
|
37
43
|
return null;
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
|
-
/**
|
|
41
|
-
export function
|
|
46
|
+
/** 获取当前快照(内存缓存优先,然后磁盘) */
|
|
47
|
+
export function getSnapshot() {
|
|
48
|
+
return currentSnapshot ?? loadSnapshot();
|
|
49
|
+
}
|
|
50
|
+
export async function buildSnapshot(signal, force = false) {
|
|
51
|
+
// 检查缓存:已有快照且不强制刷新,直接返回
|
|
52
|
+
if (!force) {
|
|
53
|
+
const cached = getSnapshot();
|
|
54
|
+
if (cached)
|
|
55
|
+
return { snapshot: cached };
|
|
56
|
+
}
|
|
42
57
|
const root = getSandboxRoot() ?? process.cwd();
|
|
43
|
-
|
|
44
|
-
const
|
|
58
|
+
// 动态导入避免循环依赖
|
|
59
|
+
const { generateLLMSnapshot } = await import('./llm-snapshot.js');
|
|
60
|
+
const result = await generateLLMSnapshot(root, signal);
|
|
61
|
+
if (!result.ok || !result.content) {
|
|
62
|
+
return {
|
|
63
|
+
snapshot: null,
|
|
64
|
+
error: result.error || 'LLM 未返回有效结果',
|
|
65
|
+
transcript: result.transcript,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
45
68
|
const snap = {
|
|
46
69
|
version: 1,
|
|
47
70
|
root,
|
|
48
71
|
builtAt: new Date().toISOString(),
|
|
49
|
-
|
|
50
|
-
structure,
|
|
72
|
+
content: result.content,
|
|
51
73
|
};
|
|
52
|
-
//
|
|
74
|
+
// 落盘为 markdown 格式,带 YAML front matter
|
|
53
75
|
const dir = snapshotDir();
|
|
54
76
|
mkdirSync(dir, { recursive: true });
|
|
55
|
-
|
|
77
|
+
const mdContent = `---\nroot: ${snap.root}\nbuiltAt: ${snap.builtAt}\nversion: ${snap.version}\n---\n\n${snap.content}`;
|
|
78
|
+
writeFileSync(snapshotPath(), mdContent, 'utf8');
|
|
56
79
|
currentSnapshot = snap;
|
|
57
|
-
return snap;
|
|
58
|
-
}
|
|
59
|
-
/** 获取当前快照(优先内存 → 磁盘 → 构建) */
|
|
60
|
-
export function getSnapshot() {
|
|
61
|
-
return loadSnapshot() ?? buildSnapshot();
|
|
80
|
+
return { snapshot: snap };
|
|
62
81
|
}
|
|
63
|
-
/**
|
|
82
|
+
/** 清除内存缓存(强制下次重新加载) */
|
|
64
83
|
export function clearSnapshotCache() {
|
|
65
84
|
currentSnapshot = null;
|
|
66
85
|
}
|
|
67
|
-
/**
|
|
68
|
-
* 从快照中查找文件(带 mtime 校验)。
|
|
69
|
-
* 返回 null 表示:文件不在快照中 / mtime 已变 / 快照不存在。
|
|
70
|
-
* 调用方应 fallback 到真实 readFile。
|
|
71
|
-
*/
|
|
72
|
-
export function lookupSnapshotFile(absPath) {
|
|
73
|
-
const snap = loadSnapshot();
|
|
74
|
-
if (!snap)
|
|
75
|
-
return null;
|
|
76
|
-
// 转成相对路径(快照 key 是相对路径)
|
|
77
|
-
const root = snap.root;
|
|
78
|
-
if (!absPath.startsWith(root))
|
|
79
|
-
return null;
|
|
80
|
-
const relPath = path.relative(root, absPath).replace(/\\/g, '/');
|
|
81
|
-
const entry = snap.files[relPath];
|
|
82
|
-
if (!entry)
|
|
83
|
-
return null;
|
|
84
|
-
// mtime 校验:磁盘上的 mtime 必须与快照一致
|
|
85
|
-
try {
|
|
86
|
-
const st = statSync(absPath);
|
|
87
|
-
if (st.mtimeMs !== entry.mtime)
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
return { content: entry.content, mtime: entry.mtime };
|
|
94
|
-
}
|
|
95
|
-
/** 提取项目结构摘要(从静态文件 + 目录扫描) */
|
|
96
|
-
function extractStructure(root, files) {
|
|
97
|
-
const modules = [];
|
|
98
|
-
const entries = [];
|
|
99
|
-
const configFiles = [];
|
|
100
|
-
// 配置文件:直接看 files keys
|
|
101
|
-
for (const relPath of Object.keys(files)) {
|
|
102
|
-
if (isConfigFile(relPath)) {
|
|
103
|
-
configFiles.push(relPath);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// 入口文件:从 package.json 提取
|
|
107
|
-
const pkgEntry = files['package.json'];
|
|
108
|
-
if (pkgEntry) {
|
|
109
|
-
try {
|
|
110
|
-
const pkg = JSON.parse(pkgEntry.content);
|
|
111
|
-
if (typeof pkg.main === 'string')
|
|
112
|
-
entries.push(pkg.main);
|
|
113
|
-
if (typeof pkg.module === 'string')
|
|
114
|
-
entries.push(pkg.module);
|
|
115
|
-
if (pkg.bin) {
|
|
116
|
-
if (typeof pkg.bin === 'string')
|
|
117
|
-
entries.push(pkg.bin);
|
|
118
|
-
else if (typeof pkg.bin === 'object') {
|
|
119
|
-
for (const v of Object.values(pkg.bin)) {
|
|
120
|
-
if (typeof v === 'string')
|
|
121
|
-
entries.push(v);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// package.json 解析失败,跳过
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// 模块:扫描顶层目录(排除 node_modules, .git 等)
|
|
131
|
-
try {
|
|
132
|
-
const items = readdirSync(root, { withFileTypes: true });
|
|
133
|
-
for (const item of items) {
|
|
134
|
-
if (!item.isDirectory())
|
|
135
|
-
continue;
|
|
136
|
-
if (item.name.startsWith('.'))
|
|
137
|
-
continue;
|
|
138
|
-
if (item.name === 'node_modules')
|
|
139
|
-
continue;
|
|
140
|
-
if (item.name === 'dist' || item.name === 'build')
|
|
141
|
-
continue;
|
|
142
|
-
modules.push(item.name);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// 目录扫描失败,留空
|
|
147
|
-
}
|
|
148
|
-
return { modules, entries, configFiles };
|
|
149
|
-
}
|
|
150
|
-
/** 判断是否为配置文件 */
|
|
151
|
-
function isConfigFile(relPath) {
|
|
152
|
-
const configPatterns = [
|
|
153
|
-
'tsconfig.json',
|
|
154
|
-
'jsconfig.json',
|
|
155
|
-
'.eslintrc',
|
|
156
|
-
'.eslintrc.js',
|
|
157
|
-
'.eslintrc.json',
|
|
158
|
-
'.eslintrc.yml',
|
|
159
|
-
'.prettierrc',
|
|
160
|
-
'.prettierrc.js',
|
|
161
|
-
'.prettierrc.json',
|
|
162
|
-
'.env.example',
|
|
163
|
-
'jest.config.js',
|
|
164
|
-
'jest.config.ts',
|
|
165
|
-
'vitest.config.ts',
|
|
166
|
-
'vite.config.ts',
|
|
167
|
-
'next.config.js',
|
|
168
|
-
'next.config.ts',
|
|
169
|
-
'webpack.config.js',
|
|
170
|
-
'rollup.config.js',
|
|
171
|
-
'pyproject.toml',
|
|
172
|
-
'setup.py',
|
|
173
|
-
'go.mod',
|
|
174
|
-
'Cargo.toml',
|
|
175
|
-
];
|
|
176
|
-
return configPatterns.some((p) => relPath === p || relPath.endsWith('/' + p));
|
|
177
|
-
}
|