mocode-ai 0.4.8 → 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.d.ts +112 -0
- package/dist/agent/core.js +56 -13
- package/dist/agent/index.d.ts +19 -0
- package/dist/agent/index.js +1 -1
- package/dist/agent/mode.d.ts +29 -0
- package/dist/agent/spawn.d.ts +36 -0
- package/dist/attachments/image.d.ts +22 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/config/file.d.ts +23 -0
- package/dist/config/index.d.ts +114 -0
- package/dist/config/index.js +61 -5
- package/dist/config/presets.d.ts +57 -0
- package/dist/context/budget.d.ts +79 -0
- package/dist/context/classifier.d.ts +14 -0
- package/dist/context/encoders/_util.d.ts +11 -0
- package/dist/context/encoders/code.d.ts +2 -0
- package/dist/context/encoders/doc.d.ts +14 -0
- package/dist/context/encoders/graph.d.ts +11 -0
- package/dist/context/encoders/index.d.ts +2 -0
- package/dist/context/encoders/log.d.ts +2 -0
- package/dist/context/encoders/memory.d.ts +2 -0
- package/dist/context/encoders/passthrough.d.ts +11 -0
- package/dist/context/encoders/search.d.ts +2 -0
- package/dist/context/encoders/summary.d.ts +12 -0
- package/dist/context/encoders/table.d.ts +2 -0
- package/dist/context/encoders/tree.d.ts +2 -0
- package/dist/context/index.d.ts +6 -0
- package/dist/context/lifecycle.d.ts +51 -0
- package/dist/context/pipeline.d.ts +9 -0
- package/dist/context/registry.d.ts +9 -0
- package/dist/context/relevance.d.ts +65 -0
- package/dist/context/types.d.ts +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/llm/capabilities.d.ts +19 -0
- package/dist/llm/index.d.ts +111 -0
- package/dist/llm/index.js +22 -1
- package/dist/memory/discover.d.ts +15 -0
- package/dist/memory/index.d.ts +13 -0
- package/dist/memory/reflect.d.ts +34 -0
- package/dist/memory/store.d.ts +124 -0
- package/dist/permissions/index.js +138 -0
- package/dist/pet/bridge.d.ts +73 -0
- package/dist/pet/protocol.d.ts +83 -0
- package/dist/pet/state.d.ts +28 -0
- package/dist/plan/active.d.ts +23 -0
- package/dist/plan/index.d.ts +13 -0
- package/dist/plan/state.d.ts +2 -0
- package/dist/plan/store.d.ts +78 -0
- package/dist/project-skill/index.js +180 -0
- package/dist/project-skill/initializer.js +178 -0
- package/dist/project-snapshot/index.js +85 -0
- package/dist/project-snapshot/llm-snapshot.js +127 -0
- package/dist/project-snapshot/static-files.js +87 -0
- package/dist/repl/index.d.ts +22 -0
- package/dist/repl/index.js +268 -2
- package/dist/rollback/index.d.ts +79 -0
- package/dist/sandbox/command.d.ts +3 -0
- package/dist/sandbox/index.d.ts +4 -0
- package/dist/sandbox/jail.d.ts +17 -0
- package/dist/sandbox/policy.d.ts +21 -0
- package/dist/sandbox/root.d.ts +5 -0
- package/dist/session/compact.d.ts +92 -0
- package/dist/session/drop.d.ts +19 -0
- package/dist/session/index.d.ts +13 -0
- package/dist/session/persist.d.ts +30 -0
- package/dist/session/scheduler.d.ts +53 -0
- package/dist/skills/discover.d.ts +33 -0
- package/dist/skills/index.d.ts +13 -0
- package/dist/tools/builtins/ask-human.d.ts +9 -0
- package/dist/tools/builtins/codegraph.d.ts +2 -0
- package/dist/tools/builtins/codegraph.js +17 -4
- package/dist/tools/builtins/drop-context.d.ts +18 -0
- package/dist/tools/builtins/edit-file.d.ts +2 -0
- package/dist/tools/builtins/edit-file.js +1 -0
- package/dist/tools/builtins/glob.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/index.d.ts +2 -0
- package/dist/tools/builtins/index.js +7 -0
- package/dist/tools/builtins/memory-forget.d.ts +2 -0
- package/dist/tools/builtins/memory-forget.js +1 -0
- package/dist/tools/builtins/memory-list.d.ts +2 -0
- package/dist/tools/builtins/memory-save.d.ts +2 -0
- package/dist/tools/builtins/memory-save.js +1 -0
- package/dist/tools/builtins/memory-search.d.ts +2 -0
- package/dist/tools/builtins/memory-update.d.ts +2 -0
- package/dist/tools/builtins/memory-update.js +1 -0
- package/dist/tools/builtins/project-skill-update.js +104 -0
- package/dist/tools/builtins/read-file.d.ts +2 -0
- package/dist/tools/builtins/run-command.d.ts +2 -0
- package/dist/tools/builtins/run-command.js +1 -0
- package/dist/tools/builtins/switch-mode.d.ts +2 -0
- package/dist/tools/builtins/task.d.ts +2 -0
- package/dist/tools/builtins/task.js +1 -0
- package/dist/tools/builtins/todolist.d.ts +2 -0
- package/dist/tools/builtins/todolist.js +124 -8
- package/dist/tools/builtins/use-skill.d.ts +2 -0
- package/dist/tools/builtins/web-fetch.d.ts +2 -0
- package/dist/tools/builtins/web-search.d.ts +2 -0
- package/dist/tools/builtins/web-search.js +12 -1
- package/dist/tools/builtins/write-file.d.ts +2 -0
- package/dist/tools/builtins/write-file.js +1 -0
- package/dist/tools/constants.d.ts +52 -0
- package/dist/tools/registry.d.ts +18 -0
- package/dist/tools/types.d.ts +42 -0
- package/dist/ui/batch.d.ts +67 -0
- package/dist/ui/batch.js +19 -2
- package/dist/ui/clipboard.d.ts +9 -0
- package/dist/ui/content.d.ts +55 -0
- package/dist/ui/content.js +68 -3
- package/dist/ui/diff.d.ts +34 -0
- package/dist/ui/diff.js +15 -24
- package/dist/ui/intervention.d.ts +46 -0
- package/dist/ui/layout.d.ts +225 -0
- package/dist/ui/layout.js +67 -11
- package/dist/ui/markdown.d.ts +46 -0
- package/dist/ui/mouse.d.ts +29 -0
- package/dist/ui/prompt.d.ts +61 -0
- package/dist/ui/render.d.ts +60 -0
- package/dist/ui/spinner.d.ts +26 -0
- package/dist/ui/theme.d.ts +47 -0
- package/dist/updater/index.d.ts +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Plan } from './store.js';
|
|
2
|
+
export interface ActivePlanSnapshot {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
status: Plan['status'];
|
|
6
|
+
done: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
type Listener = (snap: ActivePlanSnapshot | null) => void;
|
|
10
|
+
/** 取当前活跃 plan(完整对象,工具用)。无 → null。 */
|
|
11
|
+
export declare function getActivePlan(): Plan | null;
|
|
12
|
+
/** 设活跃 plan。同一 id 重复设也走 listener(repl 借此刷 status 行——即使内容未变,显式刷新有助)。 */
|
|
13
|
+
export declare function setActivePlan(plan: Plan | null): void;
|
|
14
|
+
/** 清活跃 plan(plan finish/abandon 后由 repl/todolist 调)。 */
|
|
15
|
+
export declare function clearActivePlan(): void;
|
|
16
|
+
/** 注册活跃 plan 变更监听器。返注销函数(不常用,模式 listener 一次性常驻)。 */
|
|
17
|
+
export declare function onActivePlanChange(cb: Listener): () => void;
|
|
18
|
+
/** 是否有活跃 plan(in_progress 状态)。finished/abandoned 不算「活跃」。 */
|
|
19
|
+
export declare function hasActivePlan(): boolean;
|
|
20
|
+
/** 给 status 行用的极简摘要(避免调用方读 Plan 全字段)。无 → 空串。 */
|
|
21
|
+
/** 状态行 chip 用的短摘要(无 ANSI 颜色,由 layout 上色)。maxWidth 默认 56,留 room 给右段。 */
|
|
22
|
+
export declare function getActivePlanSummary(maxWidth?: number): string;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { type Plan, type PlanStep, type PlanStatus, type StepStatus, plansDir, archiveDir, planPath, ensurePlansDir, ensureArchiveDir, newPlanId, parsePlan, serializePlan, readPlan, writePlan, deletePlan, deletePlanAnywhere, updatePlan, listPlans, archivePlan, unarchivePlan, renderPlanForLLM, renderPlanChip, } from './store.js';
|
|
2
|
+
export { type ActivePlanSnapshot, getActivePlan, setActivePlan, clearActivePlan, onActivePlanChange, hasActivePlan, getActivePlanSummary, } from './active.js';
|
|
3
|
+
/**
|
|
4
|
+
* 拼给 systemPrompt 注入的活跃 plan 摘要段。
|
|
5
|
+
* - 无活跃 plan → 空串(repl 直接跳过拼接,systemPrompt 长度不变)。
|
|
6
|
+
* - 有 → 紧凑 markdown(目标 + 步骤 checkbox + 进度日志末 5 条),header + 内容。
|
|
7
|
+
*
|
|
8
|
+
* 触发:setActivePlan 后由 repl listener 调;也供 buildSystemMessage 直接同步取(repl 入口)。
|
|
9
|
+
* 上限:超 MAX_ACTIVE_PLAN_CHARS 截到尾部(罕见,plan 文件本身就小)。
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildActivePlanSection(): string;
|
|
12
|
+
/** 注入 systemPrompt 的活跃 plan 摘要上限(字符)。 */
|
|
13
|
+
export declare const MAX_ACTIVE_PLAN_CHARS = 3000;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type StepStatus = 'pending' | 'in_progress' | 'done' | 'skipped' | 'failed';
|
|
2
|
+
export interface PlanStep {
|
|
3
|
+
/** 1-based 序号(在当前 plan 内稳定,即便中间 add_step 也不重用旧号——见 addStep)。 */
|
|
4
|
+
id: number;
|
|
5
|
+
title: string;
|
|
6
|
+
status: StepStatus;
|
|
7
|
+
}
|
|
8
|
+
export type PlanStatus = 'in_progress' | 'finished' | 'abandoned';
|
|
9
|
+
export interface Plan {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
status: PlanStatus;
|
|
13
|
+
created: string;
|
|
14
|
+
updated: string;
|
|
15
|
+
goal: string;
|
|
16
|
+
steps: PlanStep[];
|
|
17
|
+
/** 进度日志条目(时间戳 + 一行说明),append-only。 */
|
|
18
|
+
log: {
|
|
19
|
+
at: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
/** plans 目录:<sandboxRoot>/.mocode/plans/。无 sandboxRoot 退到 cwd(防御)。 */
|
|
24
|
+
export declare function plansDir(): string;
|
|
25
|
+
/** 归档目录:<root>/.mocode/plans/archive/。finish 自动归档、用户手动 unarchive 都走这里。 */
|
|
26
|
+
export declare function archiveDir(): string;
|
|
27
|
+
/** 单 plan 文件路径:<root>/.mocode/plans/<id>.md。可选 baseDir:默认 plans,也可指 archive。 */
|
|
28
|
+
export declare function planPath(id: string, baseDir?: string): string;
|
|
29
|
+
/** 确保 plans 目录存在(惰性,工具每次写前调)。 */
|
|
30
|
+
export declare function ensurePlansDir(): void;
|
|
31
|
+
/** 确保归档目录存在(惰性)。 */
|
|
32
|
+
export declare function ensureArchiveDir(): void;
|
|
33
|
+
/** 本地时间戳(无时区后缀),与 filename 的 YYYY-MM-DDTHH-mm-ss 风格一致。
|
|
34
|
+
* 例:2026-07-04T22:54:51.729(本地时,便于人对读 frontmatter / progress log,
|
|
35
|
+
* 且与同 plan id 文件名"2026-07-04T22-54-51"对应)。
|
|
36
|
+
* 不带 Z 后缀 → JS Date 解析会按本地时读(一致 round-trip);
|
|
37
|
+
* 反例:toISOString() 返 2026-07-04T14:54:51.729Z(UTC),跟用户当地差 8 小时,易误解。 */
|
|
38
|
+
export declare function localIsoTimestamp(d?: Date): string;
|
|
39
|
+
/** plan id:YYYY-MM-DDTHH-mm-ss-xxxxxx(本地时间,文件名安全)。xxxxxx = 4 字节随机 hex(防同秒并发撞)。 */
|
|
40
|
+
export declare function newPlanId(now?: Date): string;
|
|
41
|
+
/**
|
|
42
|
+
* 解析 plan 文件为 Plan 对象。失败(无 frontmatter / 段缺失 / 坏 checkbox)→ 退化:
|
|
43
|
+
* - 无 frontmatter:把整文当 title 段
|
|
44
|
+
* - 缺字段:空串 / 空数组
|
|
45
|
+
* - 坏 checkbox:跳过该行,不污染好行
|
|
46
|
+
* 解析成功但 step id 缺序 / 重号:按出现顺序重排为 1..N(保稳定)。
|
|
47
|
+
*/
|
|
48
|
+
export declare function parsePlan(raw: string, fallbackId: string): Plan;
|
|
49
|
+
/** 序列化为 markdown 文本。固定段序:frontmatter → 目标 → 步骤 → 进度日志。 */
|
|
50
|
+
export declare function serializePlan(p: Plan): string;
|
|
51
|
+
/** 读 plan 文件;不存在 → null,坏文件 → fallbackId 最小可用 Plan(不抛,工具契约)。
|
|
52
|
+
* 可选 baseDir:默认 plans,传 archiveDir() 可读归档。 */
|
|
53
|
+
export declare function readPlan(id: string, baseDir?: string): Plan | null;
|
|
54
|
+
/** 写 plan 文件(atomic:写 .tmp 再 rename)。返回是否成功。 */
|
|
55
|
+
export declare function writePlan(p: Plan): boolean;
|
|
56
|
+
/** 删 plan 文件;不存在静默 ok。可选 baseDir:默认 plans,传 archiveDir() 删归档。 */
|
|
57
|
+
export declare function deletePlan(id: string, baseDir?: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 原子读-改-写:读 → mutator 改 → 写。mutator 返回 false 视为"无改动",跳过写。
|
|
60
|
+
* mutator 抛错 → 写失败 → 返 null(不抛,工具契约)。
|
|
61
|
+
*
|
|
62
|
+
* mutator 同步:本 store 走同步 fs,mutator 必须同步(返 Promise 视为"无信号"——返原对象);
|
|
63
|
+
* 设计如此以避免把 fs API 改成 async(同步 fs 在 node 单 tick 原子,无需锁)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function updatePlan(id: string, mutator: (p: Plan) => Plan | false): Plan | null;
|
|
66
|
+
/** 列 plans 目录下所有 plan(按 updated 倒序,最新在前)。解析失败的跳过。
|
|
67
|
+
* scope: 'active'(默认)=仅 plans/;'archived'=仅 archive/;'all'=合并(去重按 id)。 */
|
|
68
|
+
export declare function listPlans(scope?: 'active' | 'archived' | 'all'): Plan[];
|
|
69
|
+
/** 把 plans/<id>.md mv 到 plans/archive/<id>.md(atomic rename)。
|
|
70
|
+
* 源不存在 → false(已归档 / 删了 / 从未存在都算)。 */
|
|
71
|
+
export declare function archivePlan(id: string): boolean;
|
|
72
|
+
/** 把 plans/archive/<id>.md 还原到 plans/<id>.md。源不存在 → false。 */
|
|
73
|
+
export declare function unarchivePlan(id: string): boolean;
|
|
74
|
+
/** 任一处存在就都删(活跃 + 归档)。用于 todolist delete 不关心来源。 */
|
|
75
|
+
export declare function deletePlanAnywhere(id: string): boolean;
|
|
76
|
+
/** 把 Plan 渲成给 LLM 看的紧凑摘要(多行文本)。 */
|
|
77
|
+
export declare function renderPlanForLLM(p: Plan): string;
|
|
78
|
+
export declare function renderPlanChip(p: Plan | null, maxWidth?: number): string;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目专属 Skill — 跨 session 持久化的项目开发知识。
|
|
3
|
+
*
|
|
4
|
+
* 每个项目可维护一个专属 skill 文件(.mocode/project-skill.md),
|
|
5
|
+
* 记录开发约定、架构决策、常见坑点等。开关启用后:
|
|
6
|
+
* 1. 每次启动时内容注入系统提示词(buildProjectSkillSection)
|
|
7
|
+
* 2. Agent 可通过 project_skill_update 工具动态更新
|
|
8
|
+
* 3. 随着项目开发,skill 越来越了解项目,开发效率越来越高
|
|
9
|
+
*
|
|
10
|
+
* 存储位置: <cwd>/.mocode/project-skill.md
|
|
11
|
+
* 开关: MOCODE_PROJECT_SKILL 环境变量,默认 false(零侵入)
|
|
12
|
+
*
|
|
13
|
+
* 与现有功能的关系:
|
|
14
|
+
* - 通用 skill(SKILLS_DIRS): 按需加载(use_skill);项目 skill: 始终注入
|
|
15
|
+
* - memory: 碎片化 fact/decision; 项目 skill: 结构化知识总结
|
|
16
|
+
* - snapshot: 静态文件缓存(只读); 项目 skill: 动态知识(可写)
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
/** 项目 skill 文件路径 */
|
|
21
|
+
function skillPath() {
|
|
22
|
+
return path.join(process.cwd(), '.mocode', 'project-skill.md');
|
|
23
|
+
}
|
|
24
|
+
/** 备份文件路径 */
|
|
25
|
+
function backupPath() {
|
|
26
|
+
return path.join(process.cwd(), '.mocode', 'project-skill.md.bak');
|
|
27
|
+
}
|
|
28
|
+
/** 读取项目 skill 内容。不存在/读失败 → null */
|
|
29
|
+
export function readProjectSkill() {
|
|
30
|
+
const p = skillPath();
|
|
31
|
+
if (!existsSync(p))
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const content = readFileSync(p, 'utf8').trim();
|
|
35
|
+
return content || null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** 内容硬上限(字符数)。超限拒绝写入,防止系统提示词膨胀。从 6000 降至 4000,与快照互补后内容更精简。 */
|
|
42
|
+
const MAX_SKILL_CHARS = 4000;
|
|
43
|
+
/**
|
|
44
|
+
* 写入/更新项目 skill。先备份旧内容再写新内容。
|
|
45
|
+
* 返回 { ok, error? }: ok=false 时 error 说明原因(超限/IO 失败)。
|
|
46
|
+
*/
|
|
47
|
+
export function writeProjectSkill(content) {
|
|
48
|
+
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
|
+
const p = skillPath();
|
|
56
|
+
const dir = path.dirname(p);
|
|
57
|
+
try {
|
|
58
|
+
// 备份旧内容(如果有)
|
|
59
|
+
if (existsSync(p)) {
|
|
60
|
+
copyFileSync(p, backupPath());
|
|
61
|
+
}
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
writeFileSync(p, trimmed + '\n', 'utf8');
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return { ok: false, error: `写入失败: ${e.message}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 追加内容到项目 skill 末尾(以 \n\n 分隔)。
|
|
72
|
+
* 同样受 MAX_SKILL_CHARS 限制。
|
|
73
|
+
*/
|
|
74
|
+
export function appendProjectSkill(addition) {
|
|
75
|
+
const existing = readProjectSkill() ?? '';
|
|
76
|
+
const trimmed = addition.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
return { ok: false, error: '追加内容为空' };
|
|
79
|
+
const separator = existing ? '\n\n' : '';
|
|
80
|
+
const merged = existing + separator + trimmed;
|
|
81
|
+
return writeProjectSkill(merged);
|
|
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
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 生成系统提示词注入段。
|
|
162
|
+
* 开关关闭或文件不存在 → 空串(零行为变化)。
|
|
163
|
+
*
|
|
164
|
+
* 精简版:只注入 skill 内容本身,维护指南移到 project_skill_update 工具描述中。
|
|
165
|
+
*/
|
|
166
|
+
export function buildProjectSkillSection() {
|
|
167
|
+
const content = readProjectSkill();
|
|
168
|
+
if (!content)
|
|
169
|
+
return '';
|
|
170
|
+
return [
|
|
171
|
+
'',
|
|
172
|
+
'',
|
|
173
|
+
'## Project Skill (project-specific knowledge)',
|
|
174
|
+
'The following is project-specific knowledge maintained across sessions. Use it as context for all tasks in this project.',
|
|
175
|
+
'',
|
|
176
|
+
'<project-skill>',
|
|
177
|
+
content,
|
|
178
|
+
'</project-skill>',
|
|
179
|
+
].join('\n');
|
|
180
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目 Skill 初始化器 - 派生子 agent 带工具深度探索项目,生成高质量的初始 skill
|
|
3
|
+
*/
|
|
4
|
+
import { spawnAgent } from '../agent/spawn.js';
|
|
5
|
+
/**
|
|
6
|
+
* 子 agent 的系统提示后缀:角色与输出格式约束
|
|
7
|
+
*/
|
|
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
|
|
23
|
+
|
|
24
|
+
## Exploration Strategy
|
|
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
|
|
29
|
+
|
|
30
|
+
## Output Format
|
|
31
|
+
Output the skill document between these exact delimiters:
|
|
32
|
+
|
|
33
|
+
\`\`\`skill-start
|
|
34
|
+
## 架构要点
|
|
35
|
+
### 核心模块及职责
|
|
36
|
+
- **\`path/to/module\`** — 行为描述(做什么 + 怎么做)
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
### 关键设计决策
|
|
40
|
+
- 决策:原因
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
## 项目约定
|
|
44
|
+
- 约定(具体的,有例子的)
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
## 开发流程
|
|
48
|
+
- 命令 + 注意事项/坑点(命令本身快照已有,这里只写注意什么)
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
## 常见坑点
|
|
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 没有实质内容,省略它
|
|
65
|
+
`;
|
|
66
|
+
/**
|
|
67
|
+
* 生成初始项目 Skill(使用子 agent 深度探索)
|
|
68
|
+
* @param existingSkill 已有的 skill 内容(如果有,子 agent 会在其基础上优化)
|
|
69
|
+
* @param signal AbortSignal
|
|
70
|
+
*/
|
|
71
|
+
export async function generateInitialSkill(existingSkill, signal) {
|
|
72
|
+
const hasExisting = !!existingSkill;
|
|
73
|
+
let prompt;
|
|
74
|
+
if (hasExisting) {
|
|
75
|
+
prompt = `以下是当前的项目 Skill 内容,请深度探索项目并优化/完善它:
|
|
76
|
+
|
|
77
|
+
\`\`\`markdown
|
|
78
|
+
${existingSkill}
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
## 优化方向
|
|
82
|
+
1. 补充缺失的信息(架构要点、设计决策、调用链等)
|
|
83
|
+
2. 修正过时或不准确的内容
|
|
84
|
+
3. 让描述更具体(用实际路径和例子)
|
|
85
|
+
4. 删除冗余或空泛的描述
|
|
86
|
+
5. 如果发现新的坑点或约定,添加进去
|
|
87
|
+
|
|
88
|
+
记住:保持精简,总长度控制在 4000-5000 字符。输出完整的优化后版本。`;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
prompt = `请深度探索当前项目,生成一份项目专属 Skill 文档。
|
|
92
|
+
|
|
93
|
+
## 探索步骤
|
|
94
|
+
1. 先用 glob 扫描项目结构(**/*.ts, **/*.json 等)
|
|
95
|
+
2. 读取 package.json、README.md、tsconfig.json 等关键文件
|
|
96
|
+
3. 用 codegraph 或 grep 找到主入口、核心模块、关键接口
|
|
97
|
+
4. 理解架构模式、数据流向、调用关系
|
|
98
|
+
5. 识别命名约定、代码风格、测试策略
|
|
99
|
+
6. 找出可能的坑点(复杂配置、特殊依赖、构建陷阱)
|
|
100
|
+
|
|
101
|
+
## 输出
|
|
102
|
+
探索完成后,将完整的 skill 文档输出在 \`\`\`skill-start 和 \`\`\`skill-end 之间。
|
|
103
|
+
|
|
104
|
+
记住:内容要精简、可操作、有具体路径和例子。总长度控制在 4000-5000 字符。`;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const result = await spawnAgent({
|
|
108
|
+
prompt,
|
|
109
|
+
systemPromptSuffix: SKILL_INIT_SUFFIX,
|
|
110
|
+
tools: ['read_file', 'glob', 'grep', 'codegraph'],
|
|
111
|
+
maxSteps: 30,
|
|
112
|
+
signal,
|
|
113
|
+
});
|
|
114
|
+
if (!result.completed) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: '子 agent 未完成(可能中断或超时)',
|
|
118
|
+
transcript: result.transcript,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (!result.summary) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: '子 agent 未返回内容',
|
|
125
|
+
transcript: result.transcript,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// 从 summary 中提取 skill 内容
|
|
129
|
+
const skillMatch = result.summary.match(/```skill-start\n([\s\S]*?)\n```skill-end/);
|
|
130
|
+
if (!skillMatch || !skillMatch[1]) {
|
|
131
|
+
// 如果没有找到 delimiters,尝试从最后一段 markdown 代码块提取
|
|
132
|
+
const codeBlockMatch = result.summary.match(/```(?:markdown|md)?\n([\s\S]*?)\n```/);
|
|
133
|
+
if (codeBlockMatch && codeBlockMatch[1]) {
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
content: codeBlockMatch[1].trim(),
|
|
137
|
+
transcript: result.transcript,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 如果连代码块都没有,直接用 summary(但去掉开头的探索日志)
|
|
141
|
+
const lines = result.summary.split('\n');
|
|
142
|
+
const skillStartIdx = lines.findIndex((l) => l.startsWith('## '));
|
|
143
|
+
if (skillStartIdx > 0) {
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
content: lines.slice(skillStartIdx).join('\n').trim(),
|
|
147
|
+
transcript: result.transcript,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: '无法从子 agent 输出中提取 skill 内容',
|
|
153
|
+
transcript: result.transcript,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const content = skillMatch[1].trim();
|
|
157
|
+
// 检查长度
|
|
158
|
+
if (content.length > 6000) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
error: `生成内容过长 (${content.length} 字符),请手动精简后重试`,
|
|
162
|
+
transcript: result.transcript,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
content,
|
|
168
|
+
transcript: result.transcript,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: e instanceof Error ? e.message : '未知错误',
|
|
175
|
+
transcript: '',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getSandboxRoot } from '../sandbox/root.js';
|
|
5
|
+
/** 内存缓存:当前 session 的快照(避免重复 IO) */
|
|
6
|
+
let currentSnapshot = null;
|
|
7
|
+
/** 计算 sandboxRoot 的 hash,用作目录名(避免路径特殊字符) */
|
|
8
|
+
export function hashRoot(root) {
|
|
9
|
+
return createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
10
|
+
}
|
|
11
|
+
/** 快照存储目录 */
|
|
12
|
+
function snapshotDir() {
|
|
13
|
+
const root = getSandboxRoot() ?? process.cwd();
|
|
14
|
+
return path.join(process.cwd(), '.mocode', 'projects', hashRoot(root));
|
|
15
|
+
}
|
|
16
|
+
/** 快照文件路径 */
|
|
17
|
+
function snapshotPath() {
|
|
18
|
+
return path.join(snapshotDir(), 'snapshot.md');
|
|
19
|
+
}
|
|
20
|
+
/** 从磁盘加载快照(不存在/损坏返 null) */
|
|
21
|
+
export function loadSnapshot() {
|
|
22
|
+
if (currentSnapshot)
|
|
23
|
+
return currentSnapshot;
|
|
24
|
+
const p = snapshotPath();
|
|
25
|
+
if (!existsSync(p))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
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
|
+
return null;
|
|
33
|
+
const snap = {
|
|
34
|
+
version: parseInt(match[3]),
|
|
35
|
+
root: match[1],
|
|
36
|
+
builtAt: match[2],
|
|
37
|
+
content: match[4],
|
|
38
|
+
};
|
|
39
|
+
currentSnapshot = snap;
|
|
40
|
+
return snap;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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
|
+
}
|
|
57
|
+
const root = getSandboxRoot() ?? process.cwd();
|
|
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
|
+
}
|
|
68
|
+
const snap = {
|
|
69
|
+
version: 1,
|
|
70
|
+
root,
|
|
71
|
+
builtAt: new Date().toISOString(),
|
|
72
|
+
content: result.content,
|
|
73
|
+
};
|
|
74
|
+
// 落盘为 markdown 格式,带 YAML front matter
|
|
75
|
+
const dir = snapshotDir();
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
const mdContent = `---\nroot: ${snap.root}\nbuiltAt: ${snap.builtAt}\nversion: ${snap.version}\n---\n\n${snap.content}`;
|
|
78
|
+
writeFileSync(snapshotPath(), mdContent, 'utf8');
|
|
79
|
+
currentSnapshot = snap;
|
|
80
|
+
return { snapshot: snap };
|
|
81
|
+
}
|
|
82
|
+
/** 清除内存缓存(强制下次重新加载) */
|
|
83
|
+
export function clearSnapshotCache() {
|
|
84
|
+
currentSnapshot = null;
|
|
85
|
+
}
|