mocode-ai 1.0.13 → 1.1.0
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 +2 -19
- package/README.zh-CN.md +1 -1
- package/bin/mocode-agent-host.js +3 -0
- package/dist/agent/core.js +10 -5
- package/dist/agent/mode.js +9 -10
- package/dist/agent/spawn.js +2 -2
- package/dist/config/index.js +43 -65
- package/dist/context/artifacts.js +2 -2
- package/dist/context/classifier.js +3 -6
- package/dist/context/encoders/_util.js +2 -1
- package/dist/context/encoders/index.js +3 -4
- package/dist/context/lifecycle.js +3 -23
- package/dist/context/relevance.js +3 -22
- package/dist/host/protocol.js +40 -0
- package/dist/host/stdio.js +203 -0
- package/dist/i18n/index.js +0 -30
- package/dist/memory/index.js +6 -0
- package/dist/repl/index.js +32 -284
- package/dist/sandbox/policy.js +6 -4
- package/dist/skills/builtin-skills.js +56 -0
- package/dist/skills/discover.js +7 -0
- package/dist/skills/index.js +23 -4
- package/dist/tools/builtins/edit-file.js +68 -14
- package/dist/tools/builtins/glob.js +1 -1
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +0 -14
- package/dist/tools/builtins/read-file.js +1 -1
- package/dist/tools/constants.js +2 -1
- package/package.json +3 -2
|
@@ -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,
|
|
@@ -5,7 +5,7 @@ import { getSandboxRoot, isInsideRoot } from '../../sandbox/index.js';
|
|
|
5
5
|
export const globTool = {
|
|
6
6
|
name: 'glob',
|
|
7
7
|
description: 'Find files matching a glob pattern (e.g. **/*.ts). Auto-excludes node_modules/.git.' +
|
|
8
|
-
' For architecture
|
|
8
|
+
' For architecture or call chains, prefer loading the `codegraph` skill (use_skill).',
|
|
9
9
|
parameters: {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
@@ -8,7 +8,7 @@ export const grepTool = {
|
|
|
8
8
|
description: 'Search file contents by regex (recursive, excludes node_modules/.git).\n' +
|
|
9
9
|
'Output: per-file header "<path>: N matches, lines [l1, l2, ...]" + first N matching lines.\n' +
|
|
10
10
|
'Use the line-number list to call read_file(offset=X, limit=Y) for each region — ' +
|
|
11
|
-
'do NOT read entire files after grepping.
|
|
11
|
+
'do NOT read entire files after grepping. For call chains across many files, prefer loading the `codegraph` skill (use_skill).',
|
|
12
12
|
parameters: {
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
@@ -8,8 +8,6 @@ import { webSearchTool } from './web-search.js';
|
|
|
8
8
|
import { webFetchTool } from './web-fetch.js';
|
|
9
9
|
import { useSkillTool } from './use-skill.js';
|
|
10
10
|
import { askHumanTool } from './ask-human.js';
|
|
11
|
-
import { codegraphTool } from './codegraph.js';
|
|
12
|
-
import { switchModeTool } from './switch-mode.js';
|
|
13
11
|
import { dropContextTool } from './drop-context.js';
|
|
14
12
|
import { memorySaveTool } from './memory-save.js';
|
|
15
13
|
import { memorySearchTool } from './memory-search.js';
|
|
@@ -17,7 +15,6 @@ import { memoryListTool } from './memory-list.js';
|
|
|
17
15
|
import { memoryUpdateTool } from './memory-update.js';
|
|
18
16
|
import { memoryForgetTool } from './memory-forget.js';
|
|
19
17
|
import { subAgentTool } from './task.js';
|
|
20
|
-
import { projectSkillUpdateTool } from './project-skill-update.js';
|
|
21
18
|
/**
|
|
22
19
|
* 所有内置工具,按注册顺序排列。
|
|
23
20
|
* 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
|
|
@@ -41,11 +38,6 @@ const _memoryTools = _memoryEnabledAtBoot
|
|
|
41
38
|
memoryForgetTool,
|
|
42
39
|
]
|
|
43
40
|
: [];
|
|
44
|
-
/** 项目专属 Skill 工具:仅在 MOCODE_PROJECT_SKILL=true 时注册,与 memory 同模式。 */
|
|
45
|
-
const _projectSkillEnabledAtBoot = process.env.MOCODE_PROJECT_SKILL === 'true';
|
|
46
|
-
const _projectSkillTools = _projectSkillEnabledAtBoot
|
|
47
|
-
? [projectSkillUpdateTool]
|
|
48
|
-
: [];
|
|
49
41
|
const pathResource = (args) => typeof args.path === 'string' && args.path ? [`file:${args.path}`] : ['workspace'];
|
|
50
42
|
const workspaceResource = () => ['workspace'];
|
|
51
43
|
const memoryResource = () => ['memory-store'];
|
|
@@ -56,19 +48,16 @@ const CAPABILITIES = {
|
|
|
56
48
|
run_command: { effect: 'process', concurrency: 'serial', retry: 'never', resources: workspaceResource, supportsAbort: true },
|
|
57
49
|
glob: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource },
|
|
58
50
|
grep: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource },
|
|
59
|
-
codegraph: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource, supportsAbort: true },
|
|
60
51
|
web_search: { effect: 'network', concurrency: 'parallel', retry: 'safe', supportsAbort: true },
|
|
61
52
|
web_fetch: { effect: 'network', concurrency: 'parallel', retry: 'safe', supportsAbort: true },
|
|
62
53
|
use_skill: { effect: 'read', concurrency: 'serial', retry: 'safe' },
|
|
63
54
|
ask_human: { effect: 'read', concurrency: 'serial', retry: 'never' },
|
|
64
|
-
switch_mode: { effect: 'write', concurrency: 'serial', retry: 'never', resources: () => ['agent-mode'] },
|
|
65
55
|
drop_context: { effect: 'write', concurrency: 'serial', retry: 'never', resources: () => ['conversation-context'] },
|
|
66
56
|
memory_save: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
67
57
|
memory_search: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
68
58
|
memory_list: { effect: 'read', concurrency: 'serial', retry: 'safe', resources: memoryResource },
|
|
69
59
|
memory_update: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
70
60
|
memory_forget: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
71
|
-
project_skill_update: { effect: 'write', concurrency: 'serial', retry: 'never', resources: workspaceResource },
|
|
72
61
|
// sub-agent 动态协调:只读任务无锁并行;写任务在 overlay 中执行,merge 时由 ChangeSet 持 canonical lock。
|
|
73
62
|
'sub-agent': {
|
|
74
63
|
effect: 'write',
|
|
@@ -88,15 +77,12 @@ const rawBuiltinTools = [
|
|
|
88
77
|
runCommandTool,
|
|
89
78
|
globTool,
|
|
90
79
|
grepTool,
|
|
91
|
-
codegraphTool,
|
|
92
80
|
webSearchTool,
|
|
93
81
|
webFetchTool,
|
|
94
82
|
useSkillTool,
|
|
95
83
|
askHumanTool,
|
|
96
|
-
switchModeTool,
|
|
97
84
|
dropContextTool,
|
|
98
85
|
..._memoryTools,
|
|
99
|
-
..._projectSkillTools,
|
|
100
86
|
subAgentTool,
|
|
101
87
|
];
|
|
102
88
|
/** 所有内置工具均携带显式能力;新增工具遗漏声明时 registry 会保守串行。 */
|
|
@@ -12,7 +12,7 @@ export const readFileTool = {
|
|
|
12
12
|
description: 'Read file content with line numbers. Read before editing.\n' +
|
|
13
13
|
'For files >500 lines: grep first to locate regions, then call read_file multiple times ' +
|
|
14
14
|
'with offset+limit (e.g. offset=350, limit=120). Do NOT read an entire large file in one call.\n' +
|
|
15
|
-
'
|
|
15
|
+
'For architecture or call-chain questions, prefer loading the `codegraph` skill (use_skill) over reading files one at a time.',
|
|
16
16
|
parameters: {
|
|
17
17
|
type: 'object',
|
|
18
18
|
properties: {
|
package/dist/tools/constants.js
CHANGED
|
@@ -28,7 +28,8 @@ export const IGNORE = ['**/node_modules/**', '**/.git/**'];
|
|
|
28
28
|
* plan 模式下从工具 schema 里剔除的工具(模型根本看不到 → 调不到):
|
|
29
29
|
* 写盘 / 命令 / 记忆写入类 + sub-agent(派生子 agent,plan 模式只读不可有副作用)。单一事实源,
|
|
30
30
|
* 被 llm(planChatTools)与 agent(防御 backstop)共用。
|
|
31
|
-
* 只读工具(read_file/glob/grep/
|
|
31
|
+
* 只读工具(read_file/glob/grep/web_search/web_fetch/use_skill/ask_human/memory_search/memory_list)保留。
|
|
32
|
+
* 注:codegraph 已 skill 化(用 run_command 调 CLI),不算核心工具,不在此屏蔽集。
|
|
32
33
|
*/
|
|
33
34
|
export const PLAN_DISABLED_TOOLS = new Set([
|
|
34
35
|
'write_file',
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|