mocode-ai 1.0.12 → 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
+ }
@@ -240,12 +240,14 @@ const zhCN = {
240
240
  'upgrade.latestVersion': '最新版本:{version}',
241
241
  'upgrade.noUpdate': '已是最新版本 v{version}',
242
242
  'upgrade.hasUpdate': '发现新版本:当前 v{current} → 最新 v{latest}',
243
+ 'upgrade.checkHint': '运行 /upgrade 即可升级到该版本。',
243
244
  'upgrade.upgradeStarted': '已启动后台升级 → {version}',
244
245
  'upgrade.upgradeHint': '后台升级进程已启动。当前 mocode 进程退出后才会执行 npm install -g mocode-ai@latest,完成后下次启动即生效。日志:~/.mocode/upgrade.log',
245
246
  'upgrade.begin': '开始升级 mocode-ai → {version}',
246
247
  'upgrade.completed': '升级完成,mocode 即将退出,请重新启动以使用新版本。',
247
248
  'upgrade.failedWithCode': '升级失败(退出码 {code})。',
248
249
  'upgrade.manualHint': '可尝试退出 mocode 后手动运行:npm install -g mocode-ai@latest',
250
+ 'upgrade.etargetHint': '(npm 找不到该版本,可能是 registry 镜像缓存未同步,建议稍后再试或手动指定源:npm install -g mocode-ai@latest --registry=https://registry.npmjs.org/)',
249
251
  'upgrade.devSkip': '当前为开发运行(tsx),已跳过真实 npm install,避免误改全局包。',
250
252
  'upgrade.fetchFailed': '无法获取最新版本(请检查网络或 npm registry)。',
251
253
  'upgrade.pkgName': '包名:mocode-ai',
@@ -495,12 +497,14 @@ const en = {
495
497
  'upgrade.latestVersion': 'Latest version: {version}',
496
498
  'upgrade.noUpdate': 'Already up to date: v{version}',
497
499
  'upgrade.hasUpdate': 'New version available: v{current} → v{latest}',
500
+ 'upgrade.checkHint': 'Run /upgrade to install it.',
498
501
  'upgrade.upgradeStarted': 'Background upgrade started → {version}',
499
502
  'upgrade.upgradeHint': 'Background upgrade process started. npm install -g mocode-ai@latest will run after you exit mocode. Log: ~/.mocode/upgrade.log',
500
503
  'upgrade.begin': 'Starting upgrade mocode-ai → {version}',
501
504
  'upgrade.completed': 'Upgrade complete. mocode will exit now; please restart to use the new version.',
502
505
  'upgrade.failedWithCode': 'Upgrade failed (exit code {code}).',
503
506
  'upgrade.manualHint': 'Try exiting mocode and run: npm install -g mocode-ai@latest',
507
+ 'upgrade.etargetHint': '(npm cannot find this version, possibly due to registry mirror cache lag. Retry later or use the source registry: npm install -g mocode-ai@latest --registry=https://registry.npmjs.org/)',
504
508
  'upgrade.devSkip': 'Running in dev mode (tsx); skipped real npm install to avoid touching the global package.',
505
509
  'upgrade.fetchFailed': 'Unable to fetch the latest version (check network or npm registry).',
506
510
  'upgrade.pkgName': 'Package: mocode-ai',
@@ -1171,7 +1171,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
1171
1171
  }
1172
1172
  else if (info.hasUpdate) {
1173
1173
  layout.contentWrite(`${ui.cyan}● ${t('upgrade.hasUpdate', { current: info.current, latest: info.latest })}${ui.reset}\n`);
1174
- layout.contentWrite(`${ui.dim}${t('upgrade.upgradeStarted', { version: `v${info.latest}` })}${ui.reset}\n`);
1174
+ layout.contentWrite(`${ui.dim}${t('upgrade.checkHint')}${ui.reset}\n`);
1175
1175
  }
1176
1176
  else {
1177
1177
  layout.contentWrite(`${ui.green}✓ ${t('upgrade.noUpdate', { version: info.current })}${ui.reset}\n`);
@@ -1216,7 +1216,12 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
1216
1216
  }
1217
1217
  else {
1218
1218
  layout.contentWrite(`${ui.red}✗ ${t('upgrade.failedWithCode', { code: String(result.exitCode ?? 'unknown') })}${ui.reset}\n`);
1219
- layout.contentWrite(`${ui.dim}${t('upgrade.manualHint')}${ui.reset}\n`);
1219
+ if (result.output.includes('ETARGET') || result.output.includes('No matching version found')) {
1220
+ layout.contentWrite(`${ui.yellow}${t('upgrade.etargetHint')}${ui.reset}\n`);
1221
+ }
1222
+ else {
1223
+ layout.contentWrite(`${ui.dim}${t('upgrade.manualHint')}${ui.reset}\n`);
1224
+ }
1220
1225
  }
1221
1226
  }
1222
1227
  catch (e) {
@@ -1420,16 +1425,16 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
1420
1425
  continue;
1421
1426
  }
1422
1427
  if (line === '/compact' || line.startsWith('/compact ')) {
1423
- // /compact 可选语法:/compact [focus] 或 /compact --force [focus]
1424
- // --force:即便 oldGroups (history 全在保护区)也强行把早期消息降级压一次。
1428
+ // /compact 默认强制压缩(force=true),不受阈值/保护区限制
1429
+ // 语法:/compact [focus] 或 /compact --no-force [focus] (显式关闭强制)
1425
1430
  const rest = line.slice('/compact'.length).trim();
1426
- let force = false;
1431
+ let force = true; // 默认强制
1427
1432
  let focus;
1428
- if (rest === '--force')
1429
- force = true;
1430
- else if (rest.startsWith('--force ')) {
1431
- force = true;
1432
- 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;
1433
1438
  }
1434
1439
  else if (rest)
1435
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.12",
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",