mocode-ai 1.2.5 → 1.2.7
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 +6 -6
- package/README.zh-CN.md +4 -4
- package/dist/agent/core.js +77 -37
- package/dist/agent/index.js +70 -34
- package/dist/agent/spawn.js +4 -0
- package/dist/commands/skill.js +230 -0
- package/dist/config/index.js +138 -12
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +15 -6
- package/dist/context/encoders/table.js +1 -1
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/i18n/index.js +4 -4
- package/dist/llm/index.js +22 -3
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/memory/discover.js +8 -8
- package/dist/memory/index.js +3 -2
- package/dist/repl/index.js +94 -49
- package/dist/session/compact.js +24 -2
- package/dist/session/notes.js +233 -0
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- package/dist/skills/runner.js +0 -1
- package/dist/skills/skill-eval.js +345 -0
- package/dist/skills/skill-improve.js +221 -0
- package/dist/skills/stats.js +102 -0
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/note-append.js +103 -0
- package/dist/tools/registry.js +18 -5
- package/dist/ui/diff.js +1 -1
- package/dist/ui/layout.js +12 -1
- package/dist/ui/render.js +7 -1
- package/dist/verification/prompt.js +55 -0
- package/package.json +4 -3
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// skill 使用台账(自进化 Phase 0)。
|
|
2
|
+
// 为什么不在 trace.jsonl 上做:工具事件出于隐私只存参数指纹(sha256/keys,
|
|
3
|
+
// trace-sanitize.ts 刻意不存值),拿不到 skill name。台账因此在工具层直接记录——
|
|
4
|
+
// use_skill / run_skill 是唯一知道真实 skill name 的落点。
|
|
5
|
+
//
|
|
6
|
+
// 落盘:<cwd>/.mocode/skill-stats.jsonl(append-only JSONL,每行一次使用)。
|
|
7
|
+
// 纯观测:任何写失败静默吞掉,绝不阻断 agent 主流程(风格对齐 session/trace.ts)。
|
|
8
|
+
// 聚合是纯函数(aggregateSkillStats),吃记录数组吐按 skill 的计数,便于单测。
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
/**
|
|
12
|
+
* 台账路径(项目级,与 sessions 同根;使用是项目上下文相关的,不写全局)。
|
|
13
|
+
* baseDir 可选:测试指向临时目录用;缺省 process.cwd()。
|
|
14
|
+
*/
|
|
15
|
+
export function skillStatsPath(baseDir = process.cwd()) {
|
|
16
|
+
return path.join(baseDir, '.mocode', 'skill-stats.jsonl');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 追加一条台账记录;任何失败静默(观测不得阻断主流程)。
|
|
20
|
+
* MOCODE_SKILL_EVAL=1(触发评测进程内设置)时跳过:评测里的人工构造调用
|
|
21
|
+
* 是测量手段不是真实使用,记入会污染自进化的输入信号。
|
|
22
|
+
*/
|
|
23
|
+
export function recordSkillUsage(rec, baseDir = process.cwd()) {
|
|
24
|
+
if (process.env.MOCODE_SKILL_EVAL === '1')
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
const p = skillStatsPath(baseDir);
|
|
28
|
+
const dir = path.dirname(p);
|
|
29
|
+
if (!existsSync(dir))
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
appendFileSync(p, JSON.stringify(rec) + '\n', 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 观测失败静默
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** 读取台账原始记录;文件不存在 / 单行损坏 → 跳过(不抛)。 */
|
|
38
|
+
export function loadSkillUsage(baseDir = process.cwd()) {
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(skillStatsPath(baseDir), 'utf8');
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const line of content.split('\n')) {
|
|
43
|
+
const s = line.trim();
|
|
44
|
+
if (!s)
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const v = JSON.parse(s);
|
|
48
|
+
if (v && typeof v.skill === 'string' && (v.kind === 'use' || v.kind === 'run')) {
|
|
49
|
+
out.push(v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// 损坏行跳过
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 纯聚合:记录 → 按 skill 的计数视图。按 skill 名分组(大小写敏感),
|
|
64
|
+
* lastUsedAt 取 ts 字符串字典序最大(ISO 时间戳字典序 == 时间序)。
|
|
65
|
+
* 输入乱序也安全。空输入返回 []。
|
|
66
|
+
*/
|
|
67
|
+
export function aggregateSkillStats(records) {
|
|
68
|
+
const bySkill = new Map();
|
|
69
|
+
for (const r of records) {
|
|
70
|
+
const list = bySkill.get(r.skill);
|
|
71
|
+
if (list)
|
|
72
|
+
list.push(r);
|
|
73
|
+
else
|
|
74
|
+
bySkill.set(r.skill, [r]);
|
|
75
|
+
}
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const [skill, list] of bySkill) {
|
|
78
|
+
const runs = list.filter((r) => r.kind === 'run');
|
|
79
|
+
const runSuccess = runs.filter((r) => r.status === 'success').length;
|
|
80
|
+
let lastFailure = null;
|
|
81
|
+
let lastUsedAt = '';
|
|
82
|
+
for (const r of list) {
|
|
83
|
+
if (typeof r.ts === 'string' && r.ts > lastUsedAt)
|
|
84
|
+
lastUsedAt = r.ts;
|
|
85
|
+
if (r.status !== 'success' && (!lastFailure || r.ts > lastFailure.ts)) {
|
|
86
|
+
lastFailure = { ts: r.ts, status: r.status, code: r.code };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
out.push({
|
|
90
|
+
skill,
|
|
91
|
+
total: list.length,
|
|
92
|
+
uses: list.length - runs.length,
|
|
93
|
+
runs: runs.length,
|
|
94
|
+
runSuccessRate: runs.length ? runSuccess / runs.length : null,
|
|
95
|
+
lastFailure,
|
|
96
|
+
lastUsedAt,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// 按最近使用倒序,让 /skills 徽标与人工浏览都「最活跃的在前」。
|
|
100
|
+
out.sort((a, b) => (a.lastUsedAt < b.lastUsedAt ? 1 : a.lastUsedAt > b.lastUsedAt ? -1 : 0));
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
@@ -14,6 +14,7 @@ import { useSkillTool } from './use-skill.js';
|
|
|
14
14
|
import { runSkillTool } from './run-skill.js';
|
|
15
15
|
import { askHumanTool } from './ask-human.js';
|
|
16
16
|
import { planUpdateTool } from './plan-update.js';
|
|
17
|
+
import { noteAppendTool } from './note-append.js';
|
|
17
18
|
import { memorySaveTool } from './memory-save.js';
|
|
18
19
|
import { memorySearchTool } from './memory-search.js';
|
|
19
20
|
import { memoryListTool } from './memory-list.js';
|
|
@@ -68,6 +69,8 @@ const CAPABILITIES = {
|
|
|
68
69
|
// plan_update 只写内部 notes.md(session 工作面),不作为用户代码 mutation 追踪/回滚/diff;
|
|
69
70
|
// 串行即可(调用不频繁),固定资源键让并发调用排队。
|
|
70
71
|
plan_update: { effect: 'write', concurrency: 'serial', resources: () => ['session-notepad'] },
|
|
72
|
+
// note_append 与 plan_update 同款:只写内部 notes.md 笔记段,不作 project mutation 追踪/diff/回滚;串行 + 固定资源键排队。
|
|
73
|
+
note_append: { effect: 'write', concurrency: 'serial', resources: () => ['session-notepad'] },
|
|
71
74
|
memory_save: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
72
75
|
memory_search: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
73
76
|
memory_list: { effect: 'read', concurrency: 'serial', resources: memoryResource },
|
|
@@ -103,6 +106,7 @@ const rawBuiltinTools = [
|
|
|
103
106
|
runSkillTool,
|
|
104
107
|
askHumanTool,
|
|
105
108
|
planUpdateTool,
|
|
109
|
+
noteAppendTool,
|
|
106
110
|
..._memoryTools,
|
|
107
111
|
subAgentTool,
|
|
108
112
|
];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { appendNoteToSection, NOTE_SECTION_KEYS } from '../../session/notes.js';
|
|
2
|
+
/**
|
|
3
|
+
* note_append:往会话笔记 notes.md 的预设笔记段追加一条 finding/decision/
|
|
4
|
+
* open_question/risk。与 plan_update 的边界:
|
|
5
|
+
* - plan_update 维护执行计划(步骤进度),写 `## Plan:` 段;
|
|
6
|
+
* - note_append 记发现/决策/问题/风险,写 `## Findings` 等笔记段。
|
|
7
|
+
* 写入的笔记正文会由 reinject 注入 system prompt 并常驻(5k token 预算内),
|
|
8
|
+
* compact 后仍可恢复——构成单会话永久记忆。与 memory_* 的边界:
|
|
9
|
+
* - note_append 记本会话内、抗 compact 的笔记;
|
|
10
|
+
* - memory_* 记跨会话稳定事实(另一系统,默认关)。
|
|
11
|
+
*
|
|
12
|
+
* 仿 plan_update:risk=safe,免权限/免 diff/免回滚;capabilities 由 builtins/index.ts
|
|
13
|
+
* 声明为 session-notepad 资源串行(与 plan_update 同款)。
|
|
14
|
+
*/
|
|
15
|
+
function err(message) {
|
|
16
|
+
return { status: 'error', code: 'INVALID_ARGUMENTS', retryable: false, output: `错误:${message}` };
|
|
17
|
+
}
|
|
18
|
+
/** 归一化 section:兼容单复数、下划线/空格/连字符、大小写偏差。 */
|
|
19
|
+
function normalizeSection(raw) {
|
|
20
|
+
const s = String(raw ?? '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
21
|
+
if (NOTE_SECTION_KEYS.includes(s))
|
|
22
|
+
return s;
|
|
23
|
+
// 单数/别名归一
|
|
24
|
+
if (['finding', 'find', 'insight', 'insights'].includes(s))
|
|
25
|
+
return 'findings';
|
|
26
|
+
if (['decision', 'decide', 'choice', 'choices'].includes(s))
|
|
27
|
+
return 'decisions';
|
|
28
|
+
if (['open_question', 'question', 'questions', 'openquestion', 'openquestions'].includes(s))
|
|
29
|
+
return 'open_questions';
|
|
30
|
+
if (['risk', 'hazard', 'caveat', 'caveats'].includes(s))
|
|
31
|
+
return 'risks';
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export const noteAppendTool = {
|
|
35
|
+
name: 'note_append',
|
|
36
|
+
description: 'Append a decision-grade note (a finding, decision, open question, or risk) to the session notepad ' +
|
|
37
|
+
'(`.mocode/sessions/<id>/notes.md`) so it survives context compaction and stays resident in the prompt. ' +
|
|
38
|
+
'Use this for NON-OBVIOUS, lasting-value discoveries — subtle constraints, decisions with downstream impact, ' +
|
|
39
|
+
'open questions that block a choice, or risks that affect later steps. Do NOT use it for routine progress ' +
|
|
40
|
+
'(that is the plan via `plan_update`) or for stable cross-session facts (that is `memory_save`). ' +
|
|
41
|
+
'Notes you write here persist across compaction within this session and are re-injected into the prompt ' +
|
|
42
|
+
'automatically, so the agent keeps remembering what it found/decided. Call it the moment you make the ' +
|
|
43
|
+
'discovery or decision — do not batch to the end.',
|
|
44
|
+
risk: 'safe',
|
|
45
|
+
parameters: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
section: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
enum: NOTE_SECTION_KEYS,
|
|
51
|
+
description: 'Note category: findings (a non-obvious discovery/constraint), decisions (a choice with ' +
|
|
52
|
+
'lasting impact), open_questions (a blocker needing resolution), risks (a hazard affecting later work).',
|
|
53
|
+
},
|
|
54
|
+
entry: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'The note text. One concise, self-contained item: what was found/decided and why it matters. ' +
|
|
57
|
+
'Keep each call to one item — call again for a second item.',
|
|
58
|
+
},
|
|
59
|
+
tag: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Optional short label for grouping (e.g. "parser-bug", "api-shape"). Rendered as **[tag]**.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ['section', 'entry'],
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
// 兼容模型的 section 命名偏差:单复数/分隔符/别名归一到预设 key。
|
|
68
|
+
normalizeArguments(args) {
|
|
69
|
+
const s = normalizeSection(args.section);
|
|
70
|
+
if (s)
|
|
71
|
+
args.section = s;
|
|
72
|
+
if (typeof args.tag === 'string')
|
|
73
|
+
args.tag = args.tag.trim();
|
|
74
|
+
},
|
|
75
|
+
async execute(args) {
|
|
76
|
+
const section = normalizeSection(args.section);
|
|
77
|
+
if (!section) {
|
|
78
|
+
return err(`section 非法:"${String(args.section ?? '')}"(仅 ${NOTE_SECTION_KEYS.join('/')} 或常见别名)。`);
|
|
79
|
+
}
|
|
80
|
+
const entry = String(args.entry ?? '').trim();
|
|
81
|
+
if (!entry)
|
|
82
|
+
return err('entry 不能为空。');
|
|
83
|
+
if (entry.length > 2000) {
|
|
84
|
+
return err(`entry 过长(${entry.length} 字符,上限 2000)——拆成多条 note_append 或精简。`);
|
|
85
|
+
}
|
|
86
|
+
const tag = typeof args.tag === 'string' && args.tag.trim() ? args.tag.trim() : undefined;
|
|
87
|
+
const result = appendNoteToSection(section, entry, tag);
|
|
88
|
+
if ('error' in result) {
|
|
89
|
+
return { status: 'error', code: 'EXECUTION_ERROR', retryable: false, output: `错误:写入 notes.md 失败: ${result.error}` };
|
|
90
|
+
}
|
|
91
|
+
// note_append 写内部 notes.md,不作为用户代码 mutation 上报 changedFiles(与 plan_update 一致)。
|
|
92
|
+
const titleMap = {
|
|
93
|
+
findings: 'Findings', decisions: 'Decisions', open_questions: 'Open Questions', risks: 'Risks',
|
|
94
|
+
};
|
|
95
|
+
const rendered = tag ? `- **[${tag}]** ${entry}` : `- ${entry}`;
|
|
96
|
+
return {
|
|
97
|
+
status: 'success',
|
|
98
|
+
code: 'OK',
|
|
99
|
+
retryable: false,
|
|
100
|
+
output: `已追加笔记到 ## ${titleMap[section]} 段(将常驻 prompt,抗 compact):\n${rendered}`,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
package/dist/tools/registry.js
CHANGED
|
@@ -11,6 +11,11 @@ import { isToolErrorOutput } from './result.js';
|
|
|
11
11
|
*/
|
|
12
12
|
const extensions = new Map();
|
|
13
13
|
export const tools = [...builtinTools];
|
|
14
|
+
/** 名字 → 工具 的 O(1) 索引,与 tools 数组在每次 rebuild 时同步重建;findTool 走此索引。 */
|
|
15
|
+
let toolIndex = buildToolIndex(tools);
|
|
16
|
+
function buildToolIndex(list) {
|
|
17
|
+
return new Map(list.map((tool) => [tool.name, tool]));
|
|
18
|
+
}
|
|
14
19
|
export function registerToolsExtension(sourceOrAdditions, maybeAdditions) {
|
|
15
20
|
const source = typeof sourceOrAdditions === 'string' ? sourceOrAdditions : 'external';
|
|
16
21
|
const additions = typeof sourceOrAdditions === 'string' ? (maybeAdditions ?? []) : sourceOrAdditions;
|
|
@@ -35,13 +40,14 @@ function rebuildTools() {
|
|
|
35
40
|
next.push(tool);
|
|
36
41
|
}
|
|
37
42
|
tools.splice(0, tools.length, ...next);
|
|
43
|
+
toolIndex = buildToolIndex(next);
|
|
38
44
|
}
|
|
39
45
|
const DEFAULT_CAPABILITIES = Object.freeze({
|
|
40
46
|
effect: 'unknown',
|
|
41
47
|
concurrency: 'serial',
|
|
42
48
|
});
|
|
43
49
|
export function findTool(name) {
|
|
44
|
-
return
|
|
50
|
+
return toolIndex.get(name);
|
|
45
51
|
}
|
|
46
52
|
/** 缺少声明或找不到工具时返回保守能力,绝不把未知扩展并发执行。 */
|
|
47
53
|
export function getToolCapabilities(toolOrName) {
|
|
@@ -58,10 +64,13 @@ export function getToolResourceKeys(toolOrName, args) {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
/** resource-locked write 是可生成文件 diff/按路径记 rollback 的文件 mutation。 */
|
|
61
|
-
export function
|
|
62
|
-
const capabilities = getToolCapabilities(name);
|
|
67
|
+
export function isFileMutationCapabilities(capabilities) {
|
|
63
68
|
return capabilities.effect === 'write' && capabilities.concurrency === 'resource-locked';
|
|
64
69
|
}
|
|
70
|
+
/** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities 走 isFileMutationCapabilities。 */
|
|
71
|
+
export function isFileMutationTool(name) {
|
|
72
|
+
return isFileMutationCapabilities(getToolCapabilities(name));
|
|
73
|
+
}
|
|
65
74
|
function isStructuredOutcome(value) {
|
|
66
75
|
return typeof value === 'object' && value !== null &&
|
|
67
76
|
typeof value.status === 'string' && typeof value.code === 'string' &&
|
|
@@ -144,7 +153,7 @@ async function executeToolOnce(tool, args, signal, opts) {
|
|
|
144
153
|
mutationVersionBefore = mutationBefore.version;
|
|
145
154
|
// Transactional tools own their full write-set capture inside ChangeSet commit.
|
|
146
155
|
const pathCapture = !capabilities.delegatesResourceLocks &&
|
|
147
|
-
|
|
156
|
+
isFileMutationCapabilities(capabilities) && typeof args.path === 'string' && args.path
|
|
148
157
|
? beginPathMutation(args.path)
|
|
149
158
|
: null;
|
|
150
159
|
capturedPath = pathCapture?.path;
|
|
@@ -211,7 +220,11 @@ export async function executeToolOutcome(name, argsRaw, signal, opts) {
|
|
|
211
220
|
}
|
|
212
221
|
const validation = validateToolArguments(tool, parsed);
|
|
213
222
|
if (!validation.valid) {
|
|
214
|
-
|
|
223
|
+
const hint = opts?.argumentErrorHint?.trim();
|
|
224
|
+
const message = hint
|
|
225
|
+
? `错误:工具 ${name} 参数无效: ${validation.message}\n${hint}`
|
|
226
|
+
: `错误:工具 ${name} 参数无效: ${validation.message}`;
|
|
227
|
+
return terminalOutcome('error', validation.code, message, startedAt);
|
|
215
228
|
}
|
|
216
229
|
const args = parsed;
|
|
217
230
|
const sandboxError = enforceSandbox(name, args);
|
package/dist/ui/diff.js
CHANGED
|
@@ -254,7 +254,7 @@ export function renderFileChange(opts) {
|
|
|
254
254
|
/** 头行 + 计数行 + 正文(折叠 + 截断 + 行号),每行尾 \n。 */
|
|
255
255
|
function renderBody(head, counts, items, padW, startLine, lang) {
|
|
256
256
|
const lines = [head, counts];
|
|
257
|
-
const metaPrefix = `${BODY_INDENT}
|
|
257
|
+
const metaPrefix = `${BODY_INDENT}`; // 与正文行左对齐(batch 展开时还会再加外层 indent,避免双重缩进导致提示行突兀)
|
|
258
258
|
let oldLine = startLine;
|
|
259
259
|
let newLine = startLine;
|
|
260
260
|
let shown = 0;
|
package/dist/ui/layout.js
CHANGED
|
@@ -671,6 +671,17 @@ export function contentDeleteFrom(startIdx, n) {
|
|
|
671
671
|
export function totalRows() {
|
|
672
672
|
return content.totalRows();
|
|
673
673
|
}
|
|
674
|
+
/** 缓冲尾部(已提交行)是否已经是空白行(去掉 ANSI 后无可见字符)。
|
|
675
|
+
* 供 compact 等在 step 循环顶部写通知行前判断是否需要补空行分隔。 */
|
|
676
|
+
export function isLastContentRowBlank() {
|
|
677
|
+
const committed = content.committedRows();
|
|
678
|
+
if (committed === 0)
|
|
679
|
+
return false;
|
|
680
|
+
const line = content.lineAt(committed - 1);
|
|
681
|
+
if (line === null)
|
|
682
|
+
return false;
|
|
683
|
+
return line.replace(/\x1b\[[0-9;]*m/g, '').trim().length === 0;
|
|
684
|
+
}
|
|
674
685
|
/** 正文→mutation 首摘要前,把尾部间距强制归一为一条视觉空行。 */
|
|
675
686
|
export function normalizeMutationBoundary() {
|
|
676
687
|
if (!active || !ui.isTTY)
|
|
@@ -1936,7 +1947,7 @@ function renderDimInputRow(prompt, text, placeholder, cols) {
|
|
|
1936
1947
|
/**
|
|
1937
1948
|
* 单行运行态滑窗:以光标为中心,向左右扩展填满 contentW-1(留 1 cell 给光标),
|
|
1938
1949
|
* 返回可见子串与光标在子串内的显示列。保证光标恒可见,且不软折行(运行态输入框恒单行,
|
|
1939
|
-
* 不触发 setRegion/ED
|
|
1950
|
+
* 不触发 setRegion/ED,避免流式期间底栏抖动)。
|
|
1940
1951
|
*/
|
|
1941
1952
|
function windowSingleLine(text, cursor, contentW) {
|
|
1942
1953
|
const n = text.length;
|
package/dist/ui/render.js
CHANGED
|
@@ -2,7 +2,13 @@ import { stdout } from 'node:process';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { ui } from './theme.js';
|
|
4
4
|
import { t } from '../i18n/index.js';
|
|
5
|
-
|
|
5
|
+
let VERSION = '0.0.0';
|
|
6
|
+
try {
|
|
7
|
+
VERSION = createRequire(import.meta.url)('../../package.json').version;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// 非标准布局(测试编译产物、自定义打包)不含 package.json:版本号回落,不阻断模块加载。
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
13
|
* 清空整屏 + 滚动缓冲(向上滚动可见的历史输出),光标归位。
|
|
8
14
|
* 进入会话时调用,让终端只剩当前 agent 对话。非 TTY 时空操作。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { discoverPackageValidationCommands } from './discovery.js';
|
|
3
|
+
import { discoverProjectProfile } from './profile.js';
|
|
4
|
+
/** Keep the prompt section small on large monorepos; the agent can still discover the rest. */
|
|
5
|
+
const MAX_LISTED_PACKAGES = 8;
|
|
6
|
+
function displayRoot(profile, packageProfile) {
|
|
7
|
+
const relative = path.relative(profile.root, packageProfile.root);
|
|
8
|
+
return relative === '' ? '.' : relative.split(path.sep).join('/');
|
|
9
|
+
}
|
|
10
|
+
function lineFor(profile, packageProfile) {
|
|
11
|
+
const commands = discoverPackageValidationCommands(profile, packageProfile);
|
|
12
|
+
if (commands.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const cwd = displayRoot(profile, packageProfile);
|
|
15
|
+
const rendered = commands.map((item) => `\`${item.command}\``).join(', ');
|
|
16
|
+
return `- ${packageProfile.name} (cwd \`${cwd}\`): ${rendered}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic project validation map injected into the system prompt: which package owns which
|
|
20
|
+
* script, and the exact command plus cwd to run it. Commands are listed in increasing cost order
|
|
21
|
+
* (typecheck → build → test) and are never executed here — this is evidence, not a completion gate.
|
|
22
|
+
*
|
|
23
|
+
* Returns '' when no package exposes a validation script, or when discovery fails for any reason
|
|
24
|
+
* (missing/invalid manifest, unreadable workspace): prompt construction must never break.
|
|
25
|
+
*/
|
|
26
|
+
export function buildValidationCommandsSection(root = process.cwd()) {
|
|
27
|
+
try {
|
|
28
|
+
const profile = discoverProjectProfile(root);
|
|
29
|
+
const lines = [];
|
|
30
|
+
let omitted = 0;
|
|
31
|
+
for (const packageProfile of profile.packages) {
|
|
32
|
+
const line = lineFor(profile, packageProfile);
|
|
33
|
+
if (!line)
|
|
34
|
+
continue;
|
|
35
|
+
if (lines.length >= MAX_LISTED_PACKAGES)
|
|
36
|
+
omitted += 1;
|
|
37
|
+
else
|
|
38
|
+
lines.push(line);
|
|
39
|
+
}
|
|
40
|
+
if (lines.length === 0)
|
|
41
|
+
return '';
|
|
42
|
+
if (omitted > 0) {
|
|
43
|
+
lines.push(`- …${omitted} more package(s) with scripts: read their package.json when needed.`);
|
|
44
|
+
}
|
|
45
|
+
return [
|
|
46
|
+
'',
|
|
47
|
+
'## Validation commands (discovered from project manifests)',
|
|
48
|
+
'Listed in increasing cost order. Use them when a check is worth running; prefer the package that owns your change over repository-wide runs. Not a completion gate.',
|
|
49
|
+
...lines,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return ''; // Discovery is best-effort: never let it break prompt construction.
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "tsx src/index.ts",
|
|
23
23
|
"build": "tsc -p tsconfig.build.json",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"test": "tsc -p tsconfig.test-build.json && node --test --experimental-test-isolation=none \"dist-tests/tests/*.test.js\"",
|
|
25
|
+
"typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json && tsc -p evals/tsconfig.json",
|
|
26
|
+
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts && tsx evals/work-discipline.ts",
|
|
26
27
|
"eval:coding": "tsx evals/coding/runner.ts",
|
|
27
28
|
"eval:coding:list": "tsx evals/coding/runner.ts --list",
|
|
28
29
|
"prepare": "npm run build"
|