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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Mocode Work 的本地 Agent Host。stdout 仅输出 NDJSON 协议事件;诊断写 stderr。
3
+ await import('../dist/host/stdio.js');
@@ -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
+ }
@@ -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 可选语法:/compact [focus] 或 /compact --force [focus]
1429
- // --force:即便 oldGroups (history 全在保护区)也强行把早期消息降级压一次。
1428
+ // /compact 默认强制压缩(force=true),不受阈值/保护区限制
1429
+ // 语法:/compact [focus] 或 /compact --no-force [focus] (显式关闭强制)
1430
1430
  const rest = line.slice('/compact'.length).trim();
1431
- let force = false;
1431
+ let force = true; // 默认强制
1432
1432
  let focus;
1433
- if (rest === '--force')
1434
- force = true;
1435
- else if (rest.startsWith('--force ')) {
1436
- force = true;
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: 'Replace one exact string in a file transactionally. expected_hash is required and must be copied from a fresh read_file artifact header. If the file changes after that read, the edit is rejected without writing.',
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: 'The original text; must occur exactly once.' },
23
- new_string: { type: 'string', description: 'Replacement text.' },
24
- expected_hash: { type: 'string', description: 'sha256 hash from the latest read_file artifact header.' },
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', 'old_string', 'new_string', 'expected_hash'],
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
- const count = normalized.split(oldNormalized).length - 1;
40
- if (count === 0) {
41
- return conflict(file, 'old_string 未找到;请重新 read_file 并复制最新内容。');
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
- if (count > 1) {
44
- return conflict(file, `old_string 出现 ${count} 次;请增加上下文使其唯一。`);
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.13",
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",