mocode-ai 0.6.8 → 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.
- package/README.md +9 -4
- package/README.zh-CN.md +5 -0
- package/dist/agent/core.js +5 -5
- package/dist/agent/index.js +10 -8
- package/dist/agent/spawn.js +1 -5
- package/dist/commands/config.js +9 -7
- package/dist/config/index.js +49 -41
- package/dist/i18n/index.js +450 -0
- package/dist/index.js +8 -4
- package/dist/llm/index.js +19 -16
- package/dist/mcp/client.js +389 -0
- package/dist/mcp/config.js +128 -0
- package/dist/mcp/index.js +105 -0
- package/dist/mcp/registry.js +5 -0
- package/dist/mcp/types.js +1 -0
- package/dist/permissions/index.js +16 -12
- package/dist/pet/state.js +2 -1
- package/dist/project-skill/index.js +9 -94
- package/dist/project-snapshot/llm-snapshot.js +40 -17
- package/dist/repl/index.js +237 -141
- package/dist/rollback/index.js +311 -119
- package/dist/session/persist.js +11 -5
- package/dist/tools/builtins/ask-human.js +4 -3
- package/dist/tools/builtins/project-skill-update.js +3 -5
- package/dist/tools/builtins/run-command.js +6 -5
- package/dist/tools/builtins/task.js +5 -4
- package/dist/tools/registry.js +58 -22
- package/dist/tools/result.js +7 -0
- package/dist/ui/intervention.js +12 -7
- package/dist/ui/layout.js +4 -3
- package/dist/ui/prompt.js +83 -28
- package/dist/ui/render.js +15 -12
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promptIntervention } from '../ui/intervention.js';
|
|
5
5
|
import { config } from '../config/index.js';
|
|
6
|
+
import { t } from '../i18n/index.js';
|
|
6
7
|
/**
|
|
7
8
|
* 工具权限系统:基于 risk 字段在执行前拦截确认。
|
|
8
9
|
*
|
|
@@ -60,14 +61,14 @@ export function getToolRisk(tool) {
|
|
|
60
61
|
function summarizeArgs(tool, args) {
|
|
61
62
|
const lines = [];
|
|
62
63
|
if (typeof args.path === 'string')
|
|
63
|
-
lines.push(
|
|
64
|
+
lines.push(t('permission.path', { value: args.path }));
|
|
64
65
|
if (typeof args.command === 'string')
|
|
65
|
-
lines.push(
|
|
66
|
+
lines.push(t('permission.command', { value: args.command }));
|
|
66
67
|
if (typeof args.prompt === 'string') {
|
|
67
68
|
const preview = String(args.prompt).slice(0, 100);
|
|
68
|
-
lines.push(
|
|
69
|
+
lines.push(t('permission.task', { value: `${preview}${String(args.prompt).length > 100 ? '…' : ''}` }));
|
|
69
70
|
}
|
|
70
|
-
return lines.length > 0 ? lines.join('\n') : '
|
|
71
|
+
return lines.length > 0 ? lines.join('\n') : t('permission.noArgs');
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* 检查权限:safe 直接放行;confirm/dangerous 弹面板让用户确认。
|
|
@@ -90,14 +91,17 @@ export async function checkPermission(tool, args, signal) {
|
|
|
90
91
|
return 'allow';
|
|
91
92
|
// 构建确认面板
|
|
92
93
|
const isDangerous = risk === 'dangerous';
|
|
94
|
+
const denyOption = t('permission.deny');
|
|
95
|
+
const foreverOption = t('permission.allowForever');
|
|
96
|
+
const sessionOption = t('permission.allowSession');
|
|
93
97
|
const title = isDangerous
|
|
94
|
-
?
|
|
95
|
-
:
|
|
96
|
-
const detail = summarizeArgs(tool, args) + (isDangerous ?
|
|
98
|
+
? t('permission.dangerTitle', { tool: tool.name })
|
|
99
|
+
: t('permission.confirmTitle', { tool: tool.name });
|
|
100
|
+
const detail = summarizeArgs(tool, args) + (isDangerous ? `\n\n${t('permission.dangerWarning')}` : '');
|
|
97
101
|
// 选项统一结构:dangerous 也提供"以后不再询问"(用户明确授权即尊重,即使 run_command)
|
|
98
102
|
const options = isDangerous
|
|
99
|
-
? ['
|
|
100
|
-
: ['
|
|
103
|
+
? [t('permission.confirmExecute'), foreverOption, denyOption]
|
|
104
|
+
: [t('permission.allow'), sessionOption, foreverOption, denyOption];
|
|
101
105
|
// 弹面板(阻塞直到用户选择;signal 中断时 promptIntervention 内部处理)
|
|
102
106
|
const result = await promptIntervention({
|
|
103
107
|
type: 'choice',
|
|
@@ -111,16 +115,16 @@ export async function checkPermission(tool, args, signal) {
|
|
|
111
115
|
return 'deny';
|
|
112
116
|
// 解析选择
|
|
113
117
|
const value = result.value ?? '';
|
|
114
|
-
if (value ===
|
|
118
|
+
if (value === denyOption)
|
|
115
119
|
return 'deny';
|
|
116
120
|
// 永久允许:写入磁盘 + 加入内存集合(跨会话生效)
|
|
117
|
-
if (value ===
|
|
121
|
+
if (value === foreverOption) {
|
|
118
122
|
permanentAllow.add(tool.name);
|
|
119
123
|
savePermanent();
|
|
120
124
|
return 'allow';
|
|
121
125
|
}
|
|
122
126
|
// 会话允许(confirm 级):加入内存缓存,本次进程内不再弹
|
|
123
|
-
if (!isDangerous && value ===
|
|
127
|
+
if (!isDangerous && value === sessionOption) {
|
|
124
128
|
approvedTools.add(tool.name);
|
|
125
129
|
}
|
|
126
130
|
return 'allow';
|
package/dist/pet/state.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 与既有 TUI hooks 并列注入——两组 hooks 各自独立触发,互不干扰,不修改 core.ts 的任何行为。
|
|
4
4
|
// 子 agent(src/agent/spawn.ts 的 spawnAgent)不引用本模块,故子 agent 永不广播桌宠状态。
|
|
5
5
|
import * as bridge from './bridge.js';
|
|
6
|
+
import { isToolErrorOutput } from '../tools/result.js';
|
|
6
7
|
/**
|
|
7
8
|
* 纯函数:给定当前 hook 事件与其参数,推导下一个 PetState。
|
|
8
9
|
* 前置条件:event 是 AgentHooks 定义的方法名之一。
|
|
@@ -19,7 +20,7 @@ export function deriveState(event, args) {
|
|
|
19
20
|
case 'onToolStart':
|
|
20
21
|
return 'tool_call';
|
|
21
22
|
case 'onToolResult':
|
|
22
|
-
if (args?.toolOutput && args.toolOutput
|
|
23
|
+
if (args?.toolOutput && isToolErrorOutput(args.toolOutput))
|
|
23
24
|
return 'error';
|
|
24
25
|
return 'tool_call';
|
|
25
26
|
case 'onDone':
|
|
@@ -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 {
|