mocode-ai 1.0.13 → 1.0.14
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/bin/mocode-agent-host.js +3 -0
- package/dist/agent/core.js +6 -2
- package/dist/host/protocol.js +22 -0
- package/dist/repl/index.js +8 -8
- package/dist/tools/builtins/edit-file.js +68 -14
- package/package.json +3 -2
package/dist/agent/core.js
CHANGED
|
@@ -693,7 +693,9 @@ export async function runAgentCore(opts) {
|
|
|
693
693
|
: false;
|
|
694
694
|
let denied;
|
|
695
695
|
if (tool && argumentsValid) {
|
|
696
|
-
const perm = await checkPermission(tool, parsed ?? {}, signal
|
|
696
|
+
const perm = await checkPermission(tool, parsed ?? {}, signal, {
|
|
697
|
+
prompt: opts.permissionPrompt,
|
|
698
|
+
});
|
|
697
699
|
emitTrace('permission', {
|
|
698
700
|
source: 'agent_tool',
|
|
699
701
|
tool: tc.name,
|
|
@@ -785,7 +787,9 @@ export async function runAgentCore(opts) {
|
|
|
785
787
|
? validateToolArguments(tool, parsed).valid
|
|
786
788
|
: false;
|
|
787
789
|
if (tool && argumentsValid) {
|
|
788
|
-
const perm = await checkPermission(tool, parsed ?? {}, signal
|
|
790
|
+
const perm = await checkPermission(tool, parsed ?? {}, signal, {
|
|
791
|
+
prompt: opts.permissionPrompt,
|
|
792
|
+
});
|
|
789
793
|
emitTrace('permission', {
|
|
790
794
|
source: 'agent_tool',
|
|
791
795
|
tool: tc.name,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function parseCommand(value) {
|
|
2
|
+
if (!value || typeof value !== 'object')
|
|
3
|
+
return null;
|
|
4
|
+
const input = value;
|
|
5
|
+
if (typeof input.id !== 'string' || typeof input.type !== 'string')
|
|
6
|
+
return null;
|
|
7
|
+
if (input.type === 'run' && typeof input.prompt === 'string') {
|
|
8
|
+
return { id: input.id, type: 'run', prompt: input.prompt };
|
|
9
|
+
}
|
|
10
|
+
if (input.type === 'cancel')
|
|
11
|
+
return { id: input.id, type: 'cancel' };
|
|
12
|
+
if (input.type === 'approval' && typeof input.approvalId === 'string') {
|
|
13
|
+
return {
|
|
14
|
+
id: input.id,
|
|
15
|
+
type: 'approval',
|
|
16
|
+
approvalId: input.approvalId,
|
|
17
|
+
action: input.action === 'selected' ? 'selected' : 'cancelled',
|
|
18
|
+
value: typeof input.value === 'string' ? input.value : undefined,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
package/dist/repl/index.js
CHANGED
|
@@ -1425,16 +1425,16 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1425
1425
|
continue;
|
|
1426
1426
|
}
|
|
1427
1427
|
if (line === '/compact' || line.startsWith('/compact ')) {
|
|
1428
|
-
// /compact
|
|
1429
|
-
// --force
|
|
1428
|
+
// /compact 默认强制压缩(force=true),不受阈值/保护区限制
|
|
1429
|
+
// 语法:/compact [focus] 或 /compact --no-force [focus] (显式关闭强制)
|
|
1430
1430
|
const rest = line.slice('/compact'.length).trim();
|
|
1431
|
-
let force =
|
|
1431
|
+
let force = true; // 默认强制
|
|
1432
1432
|
let focus;
|
|
1433
|
-
if (rest === '--force')
|
|
1434
|
-
force =
|
|
1435
|
-
else if (rest.startsWith('--force ')) {
|
|
1436
|
-
force =
|
|
1437
|
-
focus = rest.slice('--force '.length).trim() || undefined;
|
|
1433
|
+
if (rest === '--no-force')
|
|
1434
|
+
force = false;
|
|
1435
|
+
else if (rest.startsWith('--no-force ')) {
|
|
1436
|
+
force = false;
|
|
1437
|
+
focus = rest.slice('--no-force '.length).trim() || undefined;
|
|
1438
1438
|
}
|
|
1439
1439
|
else if (rest)
|
|
1440
1440
|
focus = rest;
|
|
@@ -13,37 +13,91 @@ function conflict(path, details) {
|
|
|
13
13
|
}
|
|
14
14
|
export const editFileTool = {
|
|
15
15
|
name: 'edit_file',
|
|
16
|
-
description:
|
|
16
|
+
description: `Replace content in a file transactionally. Supports two modes:
|
|
17
|
+
|
|
18
|
+
**String replacement mode (default):** Provide old_string that occurs exactly once in the file. The old_string must be copied verbatim from a fresh read_file output — do NOT reconstruct from memory, summaries, or grep output, as these lose whitespace/indentation details. Common failure modes: trailing whitespace, tabs vs spaces, indentation changes, line-ending mismatches (CRLF vs LF).
|
|
19
|
+
|
|
20
|
+
**Line-range mode:** Provide line_start and line_end (1-based, inclusive) instead of old_string. Use this when the exact text is hard to reproduce or when replacing a large block.
|
|
21
|
+
|
|
22
|
+
expected_hash is required (sha256 from read_file artifact header) and must match the current file hash. If the file changed after your read, the edit is rejected. Recovery: call read_file again on the same path and copy both the new hash and exact text.
|
|
23
|
+
|
|
24
|
+
**When to use which mode:**
|
|
25
|
+
- String replacement: small, unique text fragments (function signatures, config keys, error messages)
|
|
26
|
+
- Line-range: large blocks, repeated patterns, or when whitespace precision is critical
|
|
27
|
+
|
|
28
|
+
**Anti-patterns (will fail):**
|
|
29
|
+
- old_string reconstructed from memory or a summary
|
|
30
|
+
- old_string copied from a previous tool call that may be stale
|
|
31
|
+
- old_string that appears multiple times (add more context to make it unique)
|
|
32
|
+
- expected_hash from a different file or an old read_file call`,
|
|
17
33
|
risk: 'confirm',
|
|
18
34
|
parameters: {
|
|
19
35
|
type: 'object',
|
|
20
36
|
properties: {
|
|
21
|
-
path: { type: 'string' },
|
|
22
|
-
old_string: { type: 'string', description: '
|
|
23
|
-
new_string: { type: 'string', description: '
|
|
24
|
-
|
|
37
|
+
path: { type: 'string', description: 'Absolute file path.' },
|
|
38
|
+
old_string: { type: 'string', description: 'String replacement mode: the exact text to replace (must occur once). Mutually exclusive with line_start/line_end.' },
|
|
39
|
+
new_string: { type: 'string', description: 'The replacement text.' },
|
|
40
|
+
line_start: { type: 'integer', description: 'Line-range mode: start line (1-based, inclusive). Mutually exclusive with old_string.' },
|
|
41
|
+
line_end: { type: 'integer', description: 'Line-range mode: end line (1-based, inclusive). Mutually exclusive with old_string.' },
|
|
42
|
+
expected_hash: { type: 'string', description: 'sha256 hash from the latest read_file artifact header (sha256:<64 hex>).' },
|
|
25
43
|
},
|
|
26
|
-
required: ['path', '
|
|
44
|
+
required: ['path', 'new_string', 'expected_hash'],
|
|
27
45
|
},
|
|
28
46
|
async execute(args, ctx) {
|
|
29
47
|
const file = String(args.path);
|
|
30
|
-
const oldString = String(args.old_string);
|
|
31
48
|
const newString = String(args.new_string);
|
|
32
49
|
const expectedHash = normalizeContentHash(String(args.expected_hash));
|
|
33
50
|
if (!expectedHash)
|
|
34
51
|
return conflict(file, 'expected_hash 必须是 sha256:<64 hex>。');
|
|
52
|
+
const hasOldString = args.old_string !== undefined;
|
|
53
|
+
const hasLineStart = args.line_start !== undefined;
|
|
54
|
+
const hasLineEnd = args.line_end !== undefined;
|
|
55
|
+
// 参数互斥检查
|
|
56
|
+
if (hasOldString && (hasLineStart || hasLineEnd)) {
|
|
57
|
+
return conflict(file, 'old_string 和 line_start/line_end 互斥,请选择一种模式。');
|
|
58
|
+
}
|
|
59
|
+
if (!hasOldString && (!hasLineStart || !hasLineEnd)) {
|
|
60
|
+
return conflict(file, '必须提供 old_string 或同时提供 line_start 和 line_end。');
|
|
61
|
+
}
|
|
35
62
|
const data = await readFile(jailResolve(file), 'utf8');
|
|
36
63
|
const normalized = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
37
|
-
const oldNormalized = oldString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
38
64
|
const newNormalized = newString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
65
|
+
let updated;
|
|
66
|
+
if (hasOldString) {
|
|
67
|
+
// String replacement mode
|
|
68
|
+
const oldString = String(args.old_string);
|
|
69
|
+
const oldNormalized = oldString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
70
|
+
const count = normalized.split(oldNormalized).length - 1;
|
|
71
|
+
if (count === 0) {
|
|
72
|
+
return conflict(file, 'old_string 未找到;请重新 read_file 并复制最新内容。');
|
|
73
|
+
}
|
|
74
|
+
if (count > 1) {
|
|
75
|
+
return conflict(file, `old_string 出现 ${count} 次;请增加上下文使其唯一。`);
|
|
76
|
+
}
|
|
77
|
+
updated = normalized.replace(oldNormalized, () => newNormalized);
|
|
42
78
|
}
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
else {
|
|
80
|
+
// Line-range mode
|
|
81
|
+
const lineStart = Number(args.line_start);
|
|
82
|
+
const lineEnd = Number(args.line_end);
|
|
83
|
+
if (!Number.isInteger(lineStart) || !Number.isInteger(lineEnd)) {
|
|
84
|
+
return conflict(file, 'line_start 和 line_end 必须是整数。');
|
|
85
|
+
}
|
|
86
|
+
if (lineStart < 1 || lineEnd < lineStart) {
|
|
87
|
+
return conflict(file, `无效的 line range: line_start=${lineStart}, line_end=${lineEnd}。要求 line_start >= 1 且 line_end >= line_start。`);
|
|
88
|
+
}
|
|
89
|
+
const lines = normalized.split('\n');
|
|
90
|
+
if (lineEnd > lines.length) {
|
|
91
|
+
return conflict(file, `line_end=${lineEnd} 超出文件范围 (共 ${lines.length} 行)。`);
|
|
92
|
+
}
|
|
93
|
+
// 替换指定行范围(1-based 转 0-based)
|
|
94
|
+
const startIndex = lineStart - 1;
|
|
95
|
+
const endIndex = lineEnd - 1;
|
|
96
|
+
const newLines = newNormalized.split('\n');
|
|
97
|
+
// 替换 lines[startIndex..endIndex] 为 newLines
|
|
98
|
+
lines.splice(startIndex, endIndex - startIndex + 1, ...newLines);
|
|
99
|
+
updated = lines.join('\n');
|
|
45
100
|
}
|
|
46
|
-
const updated = normalized.replace(oldNormalized, () => newNormalized);
|
|
47
101
|
const replacement = data.includes('\r\n') ? updated.replace(/\n/g, '\r\n') : updated;
|
|
48
102
|
const result = await commitChangeSet(createChangeSet([{
|
|
49
103
|
path: file,
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mocode": "bin/mocode.js"
|
|
7
|
+
"mocode": "bin/mocode.js",
|
|
8
|
+
"mocode-agent-host": "bin/mocode-agent-host.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|