aura-basetools 0.7.0 → 0.13.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.
@@ -101,8 +101,8 @@ const layoutRoundTrip = (binPath, label) => {
101
101
  fs.writeFileSync(file, 'function alpha() {\n const a = 1\n return a\n}\n')
102
102
  const env2 = { ...process.env, AURA_CACHE_DIR: path.join(work, 'cache') }
103
103
  const spawn = (args) => spawnSync(process.execPath, [binPath, ...args], { cwd: work, encoding: 'utf8', env: env2 })
104
- const read = spawn(['read', '--file', file, '--mode', 'map'])
105
- const edited = spawn(['edit', '--file', file, '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])])
104
+ const read = spawn(['basetools', 'read', '--file', file, '--mode', 'map'])
105
+ const edited = spawn(['basetools', 'edit', '--file', file, '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])])
106
106
  const text = fs.readFileSync(file, 'utf8')
107
107
  fs.rmSync(work, { recursive: true, force: true })
108
108
  const parse = (res) => {
@@ -140,15 +140,15 @@ const countBom = (b) => {
140
140
  say('A 读:取景与令牌')
141
141
  {
142
142
  const f = write('a.js', utf8('function alpha() {\n const a = 1\n return a\n}\n\nfunction beta() {\n const b = 2\n}\n'))
143
- const map = json(run(['read', '--file', f.path, '--mode', 'map']))
143
+ const map = json(run(['basetools', 'read', '--file', f.path, '--mode', 'map']))
144
144
  // 信封里 `symbols` 是**个数**,渲染好的目录在 `text` —— 断言照实际形状写(我第一版写成数组,当场红)
145
145
  check(map.ok === true && typeof map.text === 'string' && map.symbols >= 2, 'map 返回符号目录(text + 计数)', JSON.stringify(map).slice(0, 120))
146
146
  check(String(map.text).includes('beta'), 'map 里有 beta')
147
- const around = json(run(['read', '--file', f.path, '--mode', 'around', '--around', 'beta']))
147
+ const around = json(run(['basetools', 'read', '--file', f.path, '--mode', 'around', '--around', 'beta']))
148
148
  check(around.ok === true && String(around.text ?? '').includes('beta'), 'around 窗口里有 beta')
149
- const full = json(run(['read', '--file', f.path, '--mode', 'full']))
149
+ const full = json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
150
150
  check(typeof full.version === 'string' && full.version.length > 0, `full 给出令牌 ${full.version}`)
151
- const changed = json(run(['read', '--file', f.path, '--mode', 'changed', '--since', String(full.version)]))
151
+ const changed = json(run(['basetools', 'read', '--file', f.path, '--mode', 'changed', '--since', String(full.version)]))
152
152
  check(changed.ok === true && String(changed.note ?? '').includes('no change'), '立即 changed = 无变化', JSON.stringify(changed).slice(0, 120))
153
153
  }
154
154
 
@@ -156,14 +156,61 @@ say('A 读:取景与令牌')
156
156
  say('B 改:按符号 + 回读证据')
157
157
  {
158
158
  const f = write('b.js', utf8('function alpha() {\n const a = 1\n return a\n}\n'))
159
- const res = run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])])
159
+ const res = run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])])
160
160
  const env2 = json(res)
161
161
  check(env2.ok === true, '按符号改成功', JSON.stringify(env2).slice(0, 160))
162
162
  check(String(fs.readFileSync(f.path, 'utf8')).includes('return 42'), '磁盘上真的改了')
163
163
  check(env2.result?.write_verify?.level === 'bytes', `回读证据 level=${env2.result?.write_verify?.level}`)
164
- const dry = json(run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'x' }]), '--dry-run']))
164
+ const dry = json(run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'x' }]), '--dry-run']))
165
165
  check(dry.ok === true && dry.result?.dryRun === true, 'dry_run 不落盘')
166
166
  check(String(fs.readFileSync(f.path, 'utf8')).includes('return 42'), 'dry_run 之后磁盘没变')
167
+ const beforePlan = fs.readFileSync(f.path, 'utf8')
168
+ const plan = json(run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'x' }]), '--plan']))
169
+ check(plan.ok === true && plan.result?.dryRun === true, '--plan 是 --dry-run 的同义旗标(回执照常给 applied)', JSON.stringify(plan).slice(0, 120))
170
+ check(fs.readFileSync(f.path, 'utf8') === beforePlan, '--plan 之后磁盘逐字节没变')
171
+ }
172
+
173
+ // ── B2 能力清单:运行时可查(0.9.9) ─────────────────────────────────────
174
+ say('B2 能力清单:capabilities 从引擎常量派生')
175
+ {
176
+ const caps = json(run(['basetools', 'capabilities']))
177
+ const readTool = caps.tools?.find((tool) => tool.name === 'aura_basetools_read')
178
+ const editTool = caps.tools?.find((tool) => tool.name === 'aura_basetools_edit')
179
+ check(caps.ok === true && caps.langsCount === caps.langs?.length, `langsCount=${caps.langsCount}`, JSON.stringify(caps.langs?.length))
180
+ check(caps.langsCount === 65, '语言表 65 种(与 LANGS 同源)', String(caps.langsCount))
181
+ check(readTool?.modes?.includes('auto') && readTool.modes.includes('map') && readTool.modes.includes('changed'), '读模式含 auto/map/changed', JSON.stringify(readTool?.modes))
182
+ check(editTool?.addressKinds?.includes('intent') && editTool.addressKinds.includes('target') && editTool.addressKinds.includes('handle'), '编辑地址档含 intent/target/handle', JSON.stringify(editTool?.addressKinds))
183
+ check(editTool?.options?.includes('replace_all'), '编辑选项含 replace_all', JSON.stringify(editTool?.options))
184
+ check(editTool?.options?.includes('require_observation'), '编辑选项含 require_observation', JSON.stringify(editTool?.options))
185
+ check(caps.limits?.multiTargetsMax === 32 && caps.limits?.defaultPruneBudget === 8192 && caps.limits?.contentBudget === 7492, '限制与常量一致', JSON.stringify(caps.limits))
186
+ check(caps.commands?.includes('basetools doctor') && caps.commands.includes('basetools capabilities') && caps.commands.includes('basetools write'), '命令表含 doctor/capabilities/write', JSON.stringify(caps.commands))
187
+ const writeTool = caps.tools?.find((tool) => tool.name === 'aura_basetools_write')
188
+ check(writeTool?.writeSafety?.dsh?.includes('observation-gate') && !writeTool.writeSafety.dsh.includes('transaction'), 'write 能力按入口分:DSH 有观察闸、无事务', JSON.stringify(writeTool?.writeSafety))
189
+ }
190
+
191
+ // ── B3 平铺 CLI 的空 replace = 删除(0.9.10 评审发现) ─────────────────────
192
+ say('B3 平铺 CLI:--replace "" 是删除,不是缺参数')
193
+ {
194
+ const f = write('del.txt', 'a\nb\n')
195
+ const res = run(['basetools', 'edit', '--file', f.path, '--lines', '1,1', '--replace', ''])
196
+ const env = json(res)
197
+ check(res.status === 0 && env.ok === true, '空 replace 被当成删除(exit 0)', JSON.stringify(env).slice(0, 140))
198
+ check(fs.readFileSync(f.path, 'utf8') === 'b\n', '第一行被删除,剩余字节不动', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
199
+ }
200
+
201
+ // ── B4 分页读:内置 Read 的 offset/limit 形状(0.10.0) ───────────────────
202
+ say('B4 分页读:offset/limit 与 lines 互斥')
203
+ {
204
+ const f = write('page.txt', Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join('\n'))
205
+ const page = json(run(['basetools', 'read', '--file', f.path, '--offset', '2', '--limit', '3']))
206
+ check(page.ok === true && page.mode === 'lines' && page.totalLines === 10 && page.shown === '3-5', 'offset=2 limit=3 ⇒ 第 3-5 行', JSON.stringify({ mode: page.mode, totalLines: page.totalLines, shown: page.shown }))
207
+ check(page.text.includes('3: line 3') && page.text.includes('5: line 5') && !page.text.includes('6: line 6'), '正文只含 3-5 行', JSON.stringify(page.text))
208
+ const conflict = json(run(['basetools', 'read', '--file', f.path, '--lines', '1,2', '--offset', '0', '--limit', '2']))
209
+ check(conflict.ok === false && conflict.code === 'READ_BAD_ARGS', 'lines 与 offset/limit 同给 ⇒ 拒绝', JSON.stringify(conflict).slice(0, 160))
210
+ const oob = json(run(['basetools', 'read', '--file', f.path, '--offset', '99', '--limit', '2']))
211
+ check(oob.ok === false && oob.code === 'READ_BAD_ARGS', 'offset 越界 ⇒ 拒绝', JSON.stringify(oob).slice(0, 160))
212
+ const modeOffset = json(run(['basetools', 'read', '--file', f.path, '--mode', 'map', '--offset', '0', '--limit', '2']))
213
+ check(modeOffset.ok === false && modeOffset.code === 'READ_BAD_ARGS', '显式 mode 与 offset/limit 同给 ⇒ 拒绝(不静默换成 lines)', JSON.stringify(modeOffset).slice(0, 160))
167
214
  }
168
215
 
169
216
  // ── C 字节保真 ────────────────────────────────────────────────────────────
@@ -183,7 +230,7 @@ say('C 字节保真:改第 4 行,其余字节不动')
183
230
  }
184
231
  for (const [name, buf] of Object.entries(shapes)) {
185
232
  const f = write(name, buf)
186
- const res = run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], replace: 'EDITED' }])])
233
+ const res = run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], replace: 'EDITED' }])])
187
234
  const env2 = json(res)
188
235
  if (env2.ok !== true) {
189
236
  check(false, `${name} 编辑成功`, JSON.stringify(env2).slice(0, 140))
@@ -208,14 +255,14 @@ say('C 字节保真:改第 4 行,其余字节不动')
208
255
  say('D 陈旧闸门:读之后区域被外部改过 ⇒ 拒绝')
209
256
  {
210
257
  const f = write('d.md', utf8('one\ntwo\nthree\nfour\n'))
211
- json(run(['read', '--file', f.path, '--mode', 'full'])) // 建"上次观测"(落盘缓存)
258
+ json(run(['basetools', 'read', '--file', f.path, '--mode', 'full'])) // 建"上次观测"(落盘缓存)
212
259
  fs.writeFileSync(f.path, 'one\nTWO-BY-SOMEONE-ELSE\nthree\nfour\n') // 外部改第 2 行
213
- const res = run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [2, 2], replace: 'mine' }])])
260
+ const res = run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [2, 2], replace: 'mine' }])])
214
261
  const env2 = json(res)
215
262
  check(env2.ok === false && env2.code === 'PATCH_STALE_REGION', `被拒绝(code=${env2.code})`, JSON.stringify(env2).slice(0, 160))
216
263
  check(fs.readFileSync(f.path, 'utf8').includes('TWO-BY-SOMEONE-ELSE'), '磁盘保留外部改动')
217
264
  // 带 expect 的老路不受影响:改第 4 行(未被动过)
218
- const ok = json(run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], expect: 'four', replace: 'FOUR' }])]))
265
+ const ok = json(run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], expect: 'four', replace: 'FOUR' }])]))
219
266
  check(ok.ok === true, '未被动过的区域照常可编辑', JSON.stringify(ok).slice(0, 140))
220
267
  }
221
268
 
@@ -225,14 +272,14 @@ say('D 陈旧闸门:读之后区域被外部改过 ⇒ 拒绝')
225
272
  say('E 建文件:create 可单独用(与 DSH 壳同一条规则)')
226
273
  {
227
274
  const p = path.join(tmp, 'made.md')
228
- const res = run(['edit', '--file', p, '--create', 'hello\n'])
275
+ const res = run(['basetools', 'edit', '--file', p, '--create', 'hello\n'])
229
276
  const env2 = json(res)
230
277
  check(env2.ok === true && env2.result?.operation === 'create', `不给 --hunks 也能建文件 operation=${env2.result?.operation}`, JSON.stringify(env2).slice(0, 140))
231
278
  check(fs.readFileSync(p, 'utf8') === 'hello\n', '内容是逐字节写下的')
232
279
  // 兼容:显式给 `--hunks '[]'` 仍然可用(旧脚本不该因此变红)
233
280
  const p2 = path.join(tmp, 'made2.md')
234
- check(json(run(['edit', '--file', p2, '--hunks', '[]', '--create', 'hi\n'])).ok === true, "显式 `--hunks '[]' --create` 仍可用(向后兼容)")
235
- const again = json(run(['edit', '--file', p, '--create', 'other\n']))
281
+ check(json(run(['basetools', 'edit', '--file', p2, '--hunks', '[]', '--create', 'hi\n'])).ok === true, "显式 `--hunks '[]' --create` 仍可用(向后兼容)")
282
+ const again = json(run(['basetools', 'edit', '--file', p, '--create', 'other\n']))
236
283
  check(again.ok === false, '已存在时不覆盖(create 只用于不存在)', JSON.stringify(again).slice(0, 140))
237
284
  }
238
285
 
@@ -244,21 +291,21 @@ say('E1 --create 的取值:front matter / 含换行 / 后面还跟旗标')
244
291
  {
245
292
  const fm = '---\ntitle: x\n---\n\nbody\n'
246
293
  const p = path.join(tmp, 'frontmatter.md')
247
- const res = json(run(['edit', '--file', p, '--create', fm]))
294
+ const res = json(run(['basetools', 'edit', '--file', p, '--create', fm]))
248
295
  check(res.ok === true && res.result?.operation === 'create', '值以 `---` 开头也能建文件', JSON.stringify(res).slice(0, 140))
249
296
  check(fs.readFileSync(p, 'utf8') === fm, '完整内容逐字节落下(换行与 `--` 都没丢)', JSON.stringify(fs.readFileSync(p, 'utf8')))
250
297
  // 值后面还跟着另一个旗标:最容易被误判成“没带值”的形状。
251
298
  // 注意**不能**用 `json()` 断这一条:`json()` 取的是 stdout 最后一行,而 `--pretty` 是多行输出
252
299
  // (我第一版就是这么写错的——产品是对的,断言在量错东西)。这里直接看退出码与磁盘。
253
300
  const p2 = path.join(tmp, 'frontmatter2.md')
254
- const withFlag = run(['edit', '--file', p2, '--create', fm, '--pretty'])
301
+ const withFlag = run(['basetools', 'edit', '--file', p2, '--create', fm, '--pretty'])
255
302
  check(withFlag.status === 0, `值后面再跟一个旗标仍然正确(exit=${withFlag.status})`, String(withFlag.stderr).slice(0, 90))
256
303
  check(fs.readFileSync(p2, 'utf8') === fm, '第二种形状下内容也完整')
257
304
  // `@文件` 形式:长内容绕开命令行引号(推荐写法,帮助文本里也写了)
258
305
  const bodyFile = path.join(tmp, 'body.txt')
259
306
  fs.writeFileSync(bodyFile, fm)
260
307
  const p3 = path.join(tmp, 'fromfile.md')
261
- check(json(run(['edit', '--file', p3, '--create', `@${bodyFile}`])).ok === true, '`--create @文件` 可用')
308
+ check(json(run(['basetools', 'edit', '--file', p3, '--create', `@${bodyFile}`])).ok === true, '`--create @文件` 可用')
262
309
  check(fs.readFileSync(p3, 'utf8') === fm, '`@文件` 形式下内容也完整')
263
310
  }
264
311
 
@@ -268,16 +315,16 @@ say('E1 --create 的取值:front matter / 含换行 / 后面还跟旗标')
268
315
  say('E3 --hunks-file:前导 BOM 不让它失败')
269
316
  {
270
317
  const f = write('bomhunks.md', utf8('alpha\nbravo\n'))
271
- json(run(['read', '--file', f.path, '--mode', 'full']))
318
+ json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
272
319
  const hunksPath = path.join(tmp, 'hunks-bom.json')
273
320
  fs.writeFileSync(hunksPath, Buffer.concat([BOM, utf8('[{"lines":[2,2],"replace":"BRAVO"}]')]))
274
- const res = json(run(['edit', '--file', f.path, '--hunks-file', hunksPath]))
321
+ const res = json(run(['basetools', 'edit', '--file', f.path, '--hunks-file', hunksPath]))
275
322
  check(res.ok === true, '带 BOM 的 hunks 文件照样能编辑', JSON.stringify(res).slice(0, 140))
276
323
  check(fs.readFileSync(f.path, 'utf8') === 'alpha\nBRAVO\n', '落盘正确', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
277
324
  // 真坏 JSON 时要报**实际来源**(旧版一律报 `--hunks`,把排查引到错方向)
278
325
  const badPath = path.join(tmp, 'hunks-bad.json')
279
326
  fs.writeFileSync(badPath, '{ not json')
280
- const bad = json(run(['edit', '--file', f.path, '--hunks-file', badPath]))
327
+ const bad = json(run(['basetools', 'edit', '--file', f.path, '--hunks-file', badPath]))
281
328
  check(bad.ok === false && String(bad.error).includes('--hunks-file'), '坏 JSON 报的是实际来源(--hunks-file)', String(bad.error).slice(0, 110))
282
329
  }
283
330
 
@@ -288,17 +335,17 @@ say('E2 平铺旗标:edit --lines / --replace')
288
335
  {
289
336
  const f = write('flat.md', utf8('alpha\nbravo\ncharlie\n'))
290
337
  // 先 read 建立观测(区域新鲜度闸门依赖它);本套件没有 readFull 助手,read 也要走 run。
291
- const seen = json(run(['read', '--file', f.path, '--mode', 'full']))
338
+ const seen = json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
292
339
  check(seen.ok === true, 'E2 先 read 建立观测', JSON.stringify(seen).slice(0, 120))
293
- const flat = json(run(['edit', '--file', f.path, '--lines', '3', '--replace', 'CHARLIE', '--expect', 'charlie']))
340
+ const flat = json(run(['basetools', 'edit', '--file', f.path, '--lines', '3', '--replace', 'CHARLIE', '--expect', 'charlie']))
294
341
  check(flat.ok === true, '平铺旗标可以改一行', JSON.stringify(flat).slice(0, 160))
295
342
  check(fs.readFileSync(f.path, 'utf8') === 'alpha\nbravo\nCHARLIE\n', '落盘正确', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
296
343
  // 半成品(只有 --replace、没有地址)必须继续报“需要 hunks”,不得被当成一个缺地址的 hunk
297
- const half = json(run(['edit', '--file', f.path, '--replace', 'X']))
344
+ const half = json(run(['basetools', 'edit', '--file', f.path, '--replace', 'X']))
298
345
  check(half.ok === false && half.code === 'USAGE', '只有 --replace ⇒ USAGE(不给假地址)', `${half.code}: ${String(half.error).slice(0, 70)}`)
299
346
  // 两种形状同时给:`--hunks` 优先(显式数组是更强的意图表达)
300
- json(run(['read', '--file', f.path, '--mode', 'full']))
301
- const both = json(run(['edit', '--file', f.path, '--lines', '1', '--replace', 'ZZ', '--hunks', '[{"lines":[2,2],"replace":"BRAVO"}]']))
347
+ json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
348
+ const both = json(run(['basetools', 'edit', '--file', f.path, '--lines', '1', '--replace', 'ZZ', '--hunks', '[{"lines":[2,2],"replace":"BRAVO"}]']))
302
349
  check(both.ok === true && both.result?.applied?.[0]?.line === 2, '--hunks 与旗标同时给 ⇒ 以 --hunks 为准', JSON.stringify(both).slice(0, 140))
303
350
  check(fs.readFileSync(f.path, 'utf8') === 'alpha\nBRAVO\nCHARLIE\n', '两种形状同给时只有 --hunks 那一条生效', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
304
351
  }
@@ -311,12 +358,12 @@ say('F MCP:initialize / tools/list / tools/call')
311
358
  { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {} } },
312
359
  { jsonrpc: '2.0', method: 'notifications/initialized' },
313
360
  { jsonrpc: '2.0', id: 2, method: 'tools/list' },
314
- { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_read', arguments: { file_path: f.path, mode: 'map' } } },
361
+ { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: f.path, mode: 'map' } } },
315
362
  {
316
363
  jsonrpc: '2.0',
317
364
  id: 4,
318
365
  method: 'tools/call',
319
- params: { name: 'aura_edit', arguments: { file_path: f.path, hunks: [{ lines: [4, 4], replace: 'EDITED' }] } },
366
+ params: { name: 'aura_basetools_edit', arguments: { file_path: f.path, hunks: [{ lines: [4, 4], replace: 'EDITED' }] } },
320
367
  },
321
368
  ]
322
369
  const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((x) => JSON.stringify(x)).join('\n')}\n`, encoding: 'utf8', env })
@@ -338,11 +385,11 @@ say('F MCP:initialize / tools/list / tools/call')
338
385
  })()
339
386
  if (manifest !== null) check(byId.get(1)?.result?.serverInfo?.version === manifest.version, `serverInfo.version 跟包一致(${manifest.version})`, String(byId.get(1)?.result?.serverInfo?.version))
340
387
  const tools = byId.get(2)?.result?.tools ?? []
341
- check(tools.length === 4 && tools.some((t) => t.name === 'aura_read') && tools.some((t) => t.name === 'aura_edit') && tools.some((t) => t.name === 'aura_undo') && tools.some((t) => t.name === 'aura_transactions'), `tools/list 四个工具(${tools.map((t) => t.name).join(',')}`)
388
+ check(tools.length === 5 && tools.some((t) => t.name === 'aura_basetools_read') && tools.some((t) => t.name === 'aura_basetools_edit') && tools.some((t) => t.name === 'aura_basetools_write') && tools.some((t) => t.name === 'aura_basetools_undo') && tools.some((t) => t.name === 'aura_basetools_transactions'), `tools/list 五个工具(${tools.map((t) => t.name).join(',')}`)
342
389
  const readEnv = JSON.parse(byId.get(3)?.result?.content?.[0]?.text ?? '{}')
343
- check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), 'aura_read 经 MCP 可用', JSON.stringify(readEnv).slice(0, 120))
390
+ check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), 'aura_basetools_read 经 MCP 可用', JSON.stringify(readEnv).slice(0, 120))
344
391
  const editEnv = JSON.parse(byId.get(4)?.result?.content?.[0]?.text ?? '{}')
345
- check(editEnv.ok === true, 'aura_edit 经 MCP 可用', JSON.stringify(editEnv).slice(0, 140))
392
+ check(editEnv.ok === true, 'aura_basetools_edit 经 MCP 可用', JSON.stringify(editEnv).slice(0, 140))
346
393
  check(fs.readFileSync(f.path, 'utf8').includes('EDITED'), 'MCP 路径真的落盘了')
347
394
  check(replies.every((r) => r.jsonrpc === '2.0'), '每条回包都是 JSON-RPC 2.0')
348
395
  }
@@ -356,19 +403,19 @@ say('G MCP:坏参数 / 未知工具 / 服务存活')
356
403
  const frames = [
357
404
  { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
358
405
  { jsonrpc: '2.0', method: 'notifications/initialized' },
359
- { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'aura_read', arguments: { file: f.path, mode: 'map' } } },
360
- { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_read', arguments: { file_path: f.path, mode: 'outline' } } },
361
- { jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: 'aura_read', arguments: { file_path: path.join(tmp, 'nope.md'), mode: 'map' } } },
362
- { jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: 'aura_edit', arguments: { file_path: f.path, hunks: 'nope' } } },
406
+ { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file: f.path, mode: 'map' } } },
407
+ { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: f.path, mode: 'outline' } } },
408
+ { jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: path.join(tmp, 'nope.md'), mode: 'map' } } },
409
+ { jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: 'aura_basetools_edit', arguments: { file_path: f.path, hunks: 'nope' } } },
363
410
  { jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: 'aura_nope', arguments: {} } },
364
- { jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'aura_read', arguments: { file_path: f.path, mode: 'map' } } },
411
+ { jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: f.path, mode: 'map' } } },
365
412
  ]
366
413
  const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((x) => JSON.stringify(x)).join('\n')}\n`, encoding: 'utf8', env })
367
414
  const replies = res.stdout.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
368
415
  const byId = new Map(replies.map((r) => [r.id, r]))
369
416
  const envOf = (id) => JSON.parse(byId.get(id)?.result?.content?.[0]?.text ?? '{}')
370
417
  check(replies.length === 7, `7 条请求全部有回包(收到 ${replies.length})`)
371
- check(envOf(2).code === 'BAD_ARGS', `参数名写错 → BAD_ARGS(${envOf(2).code})`)
418
+ check(envOf(2).code === 'ARGS_UNKNOWN_KEY', `参数名写错 → ARGS_UNKNOWN_KEY(${envOf(2).code})`)
372
419
  check(envOf(3).code === 'READ_BAD_MODE', `mode 写错 → READ_BAD_MODE(${envOf(3).code})`)
373
420
  check(envOf(4).code === 'FS_NOT_FOUND', `文件不存在 → FS_NOT_FOUND(${envOf(4).code})`)
374
421
  check(envOf(5).code === 'BAD_ARGS', `hunks 不是数组 → BAD_ARGS(${envOf(5).code})`)
@@ -383,7 +430,7 @@ say('G MCP:坏参数 / 未知工具 / 服务存活')
383
430
  say('H MCP:参数形状宽容度')
384
431
  {
385
432
  const f = write('mcp-shapes.md', utf8('l1\nl2\nl3\nl4\n'))
386
- const call = (id, args) => ({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: 'aura_read', arguments: args } })
433
+ const call = (id, args) => ({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: args } })
387
434
  const frames = [
388
435
  { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
389
436
  { jsonrpc: '2.0', method: 'notifications/initialized' },
@@ -392,9 +439,9 @@ say('H MCP:参数形状宽容度')
392
439
  call(4, { file_path: f.path, lines: 2 }),
393
440
  call(5, { file_path: f.path, lines: { start: 1, end: 2 } }),
394
441
  // arguments 整体是 JSON 字符串(实测有客户端这么传)
395
- { jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: 'aura_read', arguments: JSON.stringify({ file_path: f.path, start_line: 1 }) } },
442
+ { jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: JSON.stringify({ file_path: f.path, start_line: 1 }) } },
396
443
  // arguments 是解不开的字符串:必须回结构化错误,不能挂死
397
- { jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'aura_read', arguments: '不是 JSON' } },
444
+ { jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: '不是 JSON' } },
398
445
  ]
399
446
  const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((x) => JSON.stringify(x)).join('\n')}\n`, encoding: 'utf8', env })
400
447
  const replies = res.stdout.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
@@ -417,7 +464,7 @@ say('I MCP:hunk 行区间的宽容度')
417
464
  jsonrpc: '2.0',
418
465
  id,
419
466
  method: 'tools/call',
420
- params: { name: 'aura_edit', arguments: { file_path: f.path, hunks: [hunk] } },
467
+ params: { name: 'aura_basetools_edit', arguments: { file_path: f.path, hunks: [hunk] } },
421
468
  })
422
469
  const frames = [
423
470
  { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
@@ -552,45 +599,45 @@ const normalizeGolden = (text, dir) => {
552
599
  * `changed` 之前要先拿到令牌。**采集与比对共用这一份定义** ⇒ 黄金集不可能与用例漂开。
553
600
  */
554
601
  const CLI_GOLDEN_CALLS = [
555
- { name: 'read-map', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'map'] },
556
- { name: 'read-digest', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'digest'] },
557
- { name: 'read-full', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'full'] },
558
- { name: 'read-around', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'around', '--around', 'beta'] },
559
- { name: 'read-lines', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'lines', '--lines', '1,3'] },
560
- { name: 'read-search', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'search', '--search', 'return'] },
602
+ { name: 'read-map', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'map'] },
603
+ { name: 'read-digest', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'digest'] },
604
+ { name: 'read-full', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'full'] },
605
+ { name: 'read-around', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'around', '--around', 'beta'] },
606
+ { name: 'read-lines', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'lines', '--lines', '1,3'] },
607
+ { name: 'read-search', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'search', '--search', 'return'] },
561
608
  {
562
609
  name: 'read-changed',
563
610
  file: 'scene.js',
564
611
  // 令牌是内容指纹(`<hash>:<字节数>`):现场读一次取回来,不在黄金集里钉一个手抄的令牌。
565
- prepare: (spawn) => JSON.parse(spawn(['read', '--file', 'scene.js', '--mode', 'full']).stdout).version,
566
- args: (token) => ['read', '--file', 'scene.js', '--mode', 'changed', '--since', String(token)],
612
+ prepare: (spawn) => JSON.parse(spawn(['basetools', 'read', '--file', 'scene.js', '--mode', 'full']).stdout).version,
613
+ args: (token) => ['basetools', 'read', '--file', 'scene.js', '--mode', 'changed', '--since', String(token)],
567
614
  },
568
615
  {
569
616
  name: 'edit-target',
570
617
  file: 'w.js',
571
- prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
572
- args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])],
618
+ prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
619
+ args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])],
573
620
  },
574
621
  {
575
622
  name: 'edit-lines',
576
623
  file: 'w.js',
577
- prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
578
- args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])],
624
+ prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
625
+ args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])],
579
626
  },
580
627
  {
581
628
  name: 'edit-line-cue',
582
629
  file: 'w.js',
583
- prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
584
- args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ line_cue: 'beta', replace: 'function beta() { /* cue */' }])],
630
+ prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
631
+ args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ line_cue: 'beta', replace: 'function beta() { /* cue */' }])],
585
632
  },
586
633
  {
587
634
  name: 'edit-anchor',
588
635
  file: 'w.js',
589
- prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
590
- args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ anchor: ' const b = 2', replace: ' const b = 22' }])],
636
+ prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
637
+ args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ anchor: ' const b = 2', replace: ' const b = 22' }])],
591
638
  },
592
639
  // `create` 没有“读一次”这回事:文件本来就还不存在。
593
- { name: 'edit-create', file: null, args: () => ['edit', '--file', 'made.md', '--create', 'hello\n'] },
640
+ { name: 'edit-create', file: null, args: () => ['basetools', 'edit', '--file', 'made.md', '--create', 'hello\n'] },
594
641
  ]
595
642
 
596
643
  /** 跑一次黄金集用例(干净的目录 + 干净的缓存 ⇒ 同一份输入恒定给同一份回执)。 */
@@ -617,54 +664,55 @@ const runGoldenCall = (call) => {
617
664
  const CLI_GOLDEN = {
618
665
  "read-map": {
619
666
  "exit": 0,
620
- "stdout": "{\"ok\":true,\"mode\":\"map\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"shown\":4,\"byKind\":{\"function\":2,\"variable\":2},\"text\":\"10 lines · 4 symbols (function:2 variable:2)\\nexported (0):\\ninternal:\\n function alpha 1-4\\n function beta 6-9\",\"omitted\":\"2 nested locals omitted (inside a function — reach them with mode=lines, not target=)\",\"next\":\"mode=around around=<name> · mode=lines lines=[a,b] · mode=digest for signatures\"}\n"
667
+ "stdout": "{\"ok\":true,\"mode\":\"map\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"shown\":4,\"byKind\":{\"function\":2,\"variable\":2},\"text\":\"10 lines · 4 symbols (function:2 variable:2)\\nexported (0):\\ninternal:\\n function alpha 1-4\\n function beta 6-9\",\"omitted\":\"2 nested locals omitted (inside a function — reach them with mode=lines, not target=)\",\"next\":\"mode=around around=<name> · mode=lines lines=[a,b] · mode=digest for signatures\",\"metrics\":{\"mode\":\"map\",\"lines\":10,\"chars\":154,\"symbols\":4,\"truncated\":false}}\n"
621
668
  },
622
669
  "read-digest": {
623
670
  "exit": 0,
624
- "stdout": "{\"ok\":true,\"mode\":\"digest\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"exported\":[],\"text\":\"10 lines · 4 symbols · 0 control-flow\\nexported: (none)\\nimports: (none)\\nexternal calls: (none)\\n--- signatures ---\\n1-4 ( 4 lines) function alpha()\\n6-9 ( 4 lines) function beta()\",\"next\":\"mode=around around=<name> to see one function\"}\n"
671
+ "stdout": "{\"ok\":true,\"mode\":\"digest\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"exported\":[],\"text\":\"10 lines · 4 symbols · 0 control-flow\\nexported: (none)\\nimports: (none)\\nexternal calls: (none)\\n--- signatures ---\\n1-4 ( 4 lines) function alpha()\\n6-9 ( 4 lines) function beta()\",\"next\":\"mode=around around=<name> to see one function\",\"metrics\":{\"mode\":\"digest\",\"lines\":10,\"chars\":177,\"symbols\":4,\"truncated\":false}}\n"
625
672
  },
626
673
  "read-full": {
627
674
  "exit": 0,
628
- "stdout": "{\"ok\":true,\"mode\":\"full\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"full file\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \"}\n"
675
+ "stdout": "{\"ok\":true,\"mode\":\"full\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"full file\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \",\"metrics\":{\"mode\":\"full\",\"lines\":10,\"chars\":123,\"truncated\":false}}\n"
629
676
  },
630
677
  "read-around": {
631
678
  "exit": 0,
632
- "stdout": "{\"ok\":true,\"mode\":\"around\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":6,\"label\":\"around symbol function:beta (6-9)\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \"}\n"
679
+ "stdout": "{\"ok\":true,\"mode\":\"around\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":6,\"label\":\"around symbol function:beta (6-9)\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \",\"metrics\":{\"mode\":\"around\",\"lines\":10,\"chars\":123,\"truncated\":false}}\n"
633
680
  },
634
681
  "read-lines": {
635
682
  "exit": 0,
636
- "stdout": "{\"ok\":true,\"mode\":\"lines\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"requested range\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-3\",\"handle\":\"h<ID>a8d61e88:1-3\",\"text\":\"1: function alpha() { <h<ID>a8d61e88:1-3>\\n2: const a = 1\\n3: return a\"}\n"
683
+ "stdout": "{\"ok\":true,\"mode\":\"lines\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"requested range\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-3\",\"handle\":\"h<ID>a8d61e88:1-3\",\"text\":\"1: function alpha() { <h<ID>a8d61e88:1-3>\\n2: const a = 1\\n3: return a\",\"metrics\":{\"mode\":\"lines\",\"lines\":10,\"chars\":73,\"truncated\":false}}\n"
637
684
  },
638
685
  "read-search": {
639
686
  "exit": 0,
640
- "stdout": "{\"ok\":true,\"mode\":\"search\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"matches\":2,\"handles\":[{\"line\":3,\"handle\":\"h<ID>82986ddc:3-3\"},{\"line\":8,\"handle\":\"h<ID>85987295:8-8\"}],\"relocatable\":[{\"line\":3,\"target\":\"alpha\",\"lines\":[1,4]},{\"line\":8,\"target\":\"beta\",\"lines\":[6,9]}],\"text\":\"3: return a <h<ID>82986ddc:3-3>\\n8: return b <h<ID>85987295:8-8>\"}\n"
687
+ "stdout": "{\"ok\":true,\"mode\":\"search\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"matches\":2,\"handles\":[{\"line\":3,\"handle\":\"h<ID>82986ddc:3-3\"},{\"line\":8,\"handle\":\"h<ID>85987295:8-8\"}],\"relocatable\":[{\"line\":3,\"target\":\"alpha\",\"lines\":[1,4]},{\"line\":8,\"target\":\"beta\",\"lines\":[6,9]}],\"text\":\"3: return a <h<ID>82986ddc:3-3>\\n8: return b <h<ID>85987295:8-8>\",\"metrics\":{\"mode\":\"search\",\"lines\":10,\"chars\":65,\"truncated\":false}}\n"
641
688
  },
642
689
  "read-changed": {
643
690
  "exit": 0,
644
- "stdout": "{\"ok\":true,\"mode\":\"changed\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"note\":\"no change since that version\",\"text\":\"no change since that version\"}\n"
691
+ "stdout": "{\"ok\":true,\"mode\":\"changed\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"note\":\"no change since that version\",\"text\":\"no change since that version\",\"metrics\":{\"mode\":\"changed\",\"lines\":10,\"chars\":28,\"truncated\":false}}\n"
645
692
  },
646
693
  "edit-target": {
647
694
  "exit": 0,
648
- "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>\\\\w.js\",\"applied\":[{\"line\":1,\"tier\":\"symbol\",\"byNear\":false,\"actual\":\"function alpha() {\\n const a = 1\\n return a\\n}\",\"replaced\":\"function alpha() {\\n return 42\\n}\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":79}}}\n"
695
+ "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":1,\"tier\":\"symbol\",\"byNear\":false,\"actual\":\"function alpha() {\\n const a = 1\\n return a\\n}\",\"replaced\":\"function alpha() {\\n return 42\\n}\",\"afterLine\":1,\"afterEndLine\":3,\"delta\":-1,\"handle\":\"h<ID>370ea564:1-3\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":79}}}\n"
649
696
  },
650
697
  "edit-lines": {
651
698
  "exit": 0,
652
- "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>\\\\w.js\",\"applied\":[{\"line\":2,\"tier\":\"lines\",\"byNear\":false,\"actual\":\" const a = 1\",\"replaced\":\" const a = 42\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":93}}}\n"
699
+ "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":2,\"tier\":\"lines\",\"byNear\":false,\"actual\":\" const a = 1\",\"replaced\":\" const a = 42\",\"afterLine\":2,\"afterEndLine\":2,\"delta\":0,\"handle\":\"h<ID>9edee6fa:2-2\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":93}}}\n"
653
700
  },
654
701
  "edit-line-cue": {
655
702
  "exit": 0,
656
- "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>\\\\w.js\",\"applied\":[{\"line\":6,\"tier\":\"lineCue\",\"byNear\":false,\"actual\":\"function beta() {\",\"replaced\":\"function beta() { /* cue */\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":102}}}\n"
703
+ "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":6,\"tier\":\"lineCue\",\"byNear\":false,\"actual\":\"function beta() {\",\"replaced\":\"function beta() { /* cue */\",\"afterLine\":6,\"afterEndLine\":6,\"delta\":0,\"handle\":\"h<ID>dfda3c32:6-6\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":102}}}\n"
657
704
  },
658
705
  "edit-anchor": {
659
706
  "exit": 0,
660
- "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>\\\\w.js\",\"applied\":[{\"line\":7,\"tier\":\"exact\",\"byNear\":false,\"actual\":\" const b = 2\",\"replaced\":\" const b = 22\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":93}}}\n"
707
+ "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":7,\"tier\":\"exact\",\"byNear\":false,\"actual\":\" const b = 2\",\"replaced\":\" const b = 22\",\"afterLine\":7,\"afterEndLine\":7,\"delta\":0,\"handle\":\"h<ID>6f2dcb63:7-7\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":93}}}\n"
661
708
  },
662
709
  "edit-create": {
663
710
  "exit": 0,
664
- "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>\\\\made.md\",\"operation\":\"create\",\"applied\":[{\"line\":1,\"tier\":\"create\",\"actual\":\"\",\"replaced\":\"hello\\n\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":6}}}\n"
711
+ "stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/made.md\",\"operation\":\"create\",\"applied\":[{\"line\":1,\"tier\":\"create\",\"actual\":\"\",\"replaced\":\"hello\\n\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":6}}}\n"
665
712
  }
666
713
  }
667
714
 
715
+
668
716
  if (process.argv.includes('--capture-cli-goldens')) {
669
717
  const captured = {}
670
718
  for (const call of CLI_GOLDEN_CALLS) {
@@ -705,6 +753,7 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
705
753
  scene.write('a.js', MT_SOURCE)
706
754
  scene.write('b.md', 'one\ntwo\nthree\n')
707
755
  const res = scene.run([
756
+ 'basetools',
708
757
  'read',
709
758
  '--targets',
710
759
  JSON.stringify([
@@ -721,8 +770,8 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
721
770
  JSON.stringify(batch.entries?.map((e) => e.file_path)),
722
771
  )
723
772
  // ① 每项与“单独一次单目标读”的同一字段值相同:拿整个信封逐字节比
724
- const soloA = json(scene.run(['read', '--file', 'a.js', '--mode', 'digest']))
725
- const soloB = json(scene.run(['read', '--file', 'b.md', '--mode', 'lines', '--lines', '1,2']))
773
+ const soloA = json(scene.run(['basetools', 'read', '--file', 'a.js', '--mode', 'digest']))
774
+ const soloB = json(scene.run(['basetools', 'read', '--file', 'b.md', '--mode', 'lines', '--lines', '1,2']))
726
775
  const entryA = JSON.stringify(batch.entries?.[0]?.envelope)
727
776
  const entryB = JSON.stringify(batch.entries?.[1]?.envelope)
728
777
  check(entryA === JSON.stringify(soloA), '① A 项信封与单独读逐字节相同', byteDiff(entryA, JSON.stringify(soloA)) ?? '')
@@ -730,6 +779,7 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
730
779
  // ② 一项目标不存在:整批 ok=false,另一项照常可读(不连坐)
731
780
  const partial = json(
732
781
  scene.run([
782
+ 'basetools',
733
783
  'read',
734
784
  '--targets',
735
785
  JSON.stringify([
@@ -743,7 +793,7 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
743
793
  check(partial.entries?.[1]?.ok === false && partial.entries?.[1]?.code === 'FS_NOT_FOUND', `② 缺的那项报 FS_NOT_FOUND(${partial.entries?.[1]?.code})`)
744
794
  check(partial.summary === 'targets: 2 | ok 1 | not_found 1', `③ 汇总行 ${JSON.stringify(partial.summary)}`)
745
795
  check(partial.counts?.ok === 1 && partial.counts?.failed === 1, `③ counts ok=1 failed=1(${JSON.stringify(partial.counts)})`)
746
- const missingOnly = scene.run(['read', '--targets', JSON.stringify([{ file_path: 'nope.md', mode: 'full' }])])
796
+ const missingOnly = scene.run(['basetools', 'read', '--targets', JSON.stringify([{ file_path: 'nope.md', mode: 'full' }])])
747
797
  check(missingOnly.status === 2 && json(missingOnly).ok === false, `④ 逐项失败 ⇒ 走结果通道、退出码 2(实测 ${missingOnly.status})`)
748
798
  }
749
799
 
@@ -754,11 +804,12 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
754
804
  const scene = mtScene('h')
755
805
  scene.write('c.md', 'one\ntwo\n')
756
806
  scene.write('d.md', 'alpha\nbravo\n')
757
- scene.run(['read', '--file', 'c.md', '--mode', 'full'])
758
- scene.run(['read', '--file', 'd.md', '--mode', 'full'])
807
+ scene.run(['basetools', 'read', '--file', 'c.md', '--mode', 'full'])
808
+ scene.run(['basetools', 'read', '--file', 'd.md', '--mode', 'full'])
759
809
  const beforeC = fs.readFileSync(path.join(scene.dir, 'c.md'))
760
810
  const beforeD = fs.readFileSync(path.join(scene.dir, 'd.md'))
761
811
  const res = scene.run([
812
+ 'basetools',
762
813
  'edit',
763
814
  '--targets',
764
815
  JSON.stringify([
@@ -775,8 +826,8 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
775
826
  // ① 逐项回执与单目标同形:字段集合相同(同一个渲染,不是另写一套)
776
827
  const soloScene = mtScene('h-solo')
777
828
  soloScene.write('solo.md', 'one\ntwo\n')
778
- soloScene.run(['read', '--file', 'solo.md', '--mode', 'full'])
779
- const solo = json(soloScene.run(['edit', '--file', 'solo.md', '--hunks', JSON.stringify([{ lines: [1, 1], replace: 'ONE' }])]))
829
+ soloScene.run(['basetools', 'read', '--file', 'solo.md', '--mode', 'full'])
830
+ const solo = json(soloScene.run(['basetools', 'edit', '--file', 'solo.md', '--hunks', JSON.stringify([{ lines: [1, 1], replace: 'ONE' }])]))
780
831
  const keysOf = (o) => Object.keys(o ?? {}).sort().join(',')
781
832
  check(keysOf(batch.edits?.[0]?.receipt) === keysOf(solo.result), '① 逐项回执与单目标同形(字段集合相同)', `${keysOf(batch.edits?.[0]?.receipt)} vs ${keysOf(solo.result)}`)
782
833
 
@@ -784,11 +835,12 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
784
835
  const zero = mtScene('h-zero')
785
836
  zero.write('c.md', 'one\ntwo\n')
786
837
  zero.write('d.md', 'alpha\nbravo\n')
787
- zero.run(['read', '--file', 'c.md', '--mode', 'full'])
788
- zero.run(['read', '--file', 'd.md', '--mode', 'full'])
838
+ zero.run(['basetools', 'read', '--file', 'c.md', '--mode', 'full'])
839
+ zero.run(['basetools', 'read', '--file', 'd.md', '--mode', 'full'])
789
840
  const keepC = fs.readFileSync(path.join(zero.dir, 'c.md'))
790
841
  const keepD = fs.readFileSync(path.join(zero.dir, 'd.md'))
791
842
  const failed = zero.run([
843
+ 'basetools',
792
844
  'edit',
793
845
  '--targets',
794
846
  JSON.stringify([
@@ -812,9 +864,10 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
812
864
  // ③ 同文件两个目标 ⇒ 合并成一次提交
813
865
  const merged = mtScene('h-merge')
814
866
  merged.write('m.md', 'one\ntwo\nthree\n')
815
- merged.run(['read', '--file', 'm.md', '--mode', 'full'])
867
+ merged.run(['basetools', 'read', '--file', 'm.md', '--mode', 'full'])
816
868
  const mergeEnv = json(
817
869
  merged.run([
870
+ 'basetools',
818
871
  'edit',
819
872
  '--targets',
820
873
  JSON.stringify([
@@ -830,10 +883,10 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
830
883
  // ④ dry-run 不落盘
831
884
  const dry = mtScene('h-dry')
832
885
  dry.write('c.md', 'one\ntwo\n')
833
- dry.run(['read', '--file', 'c.md', '--mode', 'full'])
886
+ dry.run(['basetools', 'read', '--file', 'c.md', '--mode', 'full'])
834
887
  const keep = fs.readFileSync(path.join(dry.dir, 'c.md'))
835
888
  const dryEnv = json(
836
- dry.run(['edit', '--dry-run', '--targets', JSON.stringify([{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ONE' }] }])]),
889
+ dry.run(['basetools', 'edit', '--dry-run', '--targets', JSON.stringify([{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ONE' }] }])]),
837
890
  )
838
891
  check(dryEnv.ok === true && dryEnv.edits?.[0]?.outcome === 'dry_run', `④ dryRun 的回执是 dry_run(${dryEnv.edits?.[0]?.outcome})`, JSON.stringify(dryEnv).slice(0, 160))
839
892
  check(fs.readFileSync(path.join(dry.dir, 'c.md')).equals(keep), '④ dryRun 不落盘(逐字节不变)')
@@ -965,42 +1018,42 @@ say('MT-I 形状与边界:参数面拒绝(exit 1),判据只有在引擎
965
1018
  scene.write('a.js', MT_SOURCE)
966
1019
  const one = JSON.stringify([{ file_path: 'a.js', mode: 'digest' }])
967
1020
  // ① 两种形状同时给 ⇒ 参数面拒绝(读侧与写侧各一次)
968
- const readBoth = scene.run(['read', '--file', 'a.js', '--targets', one])
1021
+ const readBoth = scene.run(['basetools', 'read', '--file', 'a.js', '--targets', one])
969
1022
  const readBothEnv = json(readBoth)
970
1023
  check(readBoth.status === 1, `① read:--file 与 --targets 同时给 ⇒ exit 1(实测 ${readBoth.status})`, String(readBothEnv.error).slice(0, 120))
971
1024
  check(readBothEnv.ok === false && readBothEnv.shape === 'ARGS_SHAPE_EXCLUSIVE', `① 读侧点名互斥判据(shape=${readBothEnv.shape})`, JSON.stringify(readBothEnv).slice(0, 160))
972
- const editBoth = scene.run(['edit', '--file', 'a.js', '--targets', JSON.stringify([{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }])])
1025
+ const editBoth = scene.run(['basetools', 'edit', '--file', 'a.js', '--targets', JSON.stringify([{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }])])
973
1026
  const editBothEnv = json(editBoth)
974
1027
  check(editBoth.status === 1 && editBothEnv.shape === 'ARGS_SHAPE_EXCLUSIVE', `① edit:同样被拒(exit=${editBoth.status},shape=${editBothEnv.shape})`, String(editBothEnv.error).slice(0, 120))
975
1028
  check(fs.readFileSync(path.join(scene.dir, 'a.js'), 'utf8') === MT_SOURCE, '① 被拒的写没有碰磁盘(逐字节不变)')
976
1029
 
977
1030
  // ② 超 32 项
978
1031
  const tooMany = Array.from({ length: 33 }, () => ({ file_path: 'a.js', mode: 'digest' }))
979
- const over = scene.run(['read', '--targets', JSON.stringify(tooMany)])
1032
+ const over = scene.run(['basetools', 'read', '--targets', JSON.stringify(tooMany)])
980
1033
  const overEnv = json(over)
981
1034
  check(over.status === 1, `② 33 项 ⇒ exit 1(实测 ${over.status})`)
982
1035
  check(overEnv.shape === 'ARGS_TARGETS_LIMIT' && overEnv.got === 33, `② 报出实得数(got=${overEnv.got},shape=${overEnv.shape})`, String(overEnv.error).slice(0, 120))
983
1036
 
984
1037
  // ③ 空数组不是“成功读了 0 个”
985
- const empty = scene.run(['read', '--targets', '[]'])
1038
+ const empty = scene.run(['basetools', 'read', '--targets', '[]'])
986
1039
  const emptyEnv = json(empty)
987
1040
  check(empty.status === 1 && emptyEnv.shape === 'ARGS_TARGETS_SHAPE', `③ 空数组 ⇒ 拒绝(exit=${empty.status},shape=${emptyEnv.shape})`, String(emptyEnv.error).slice(0, 120))
988
1041
 
989
1042
  // ④ 不是 JSON:说清来源与接受的形状
990
- const notJson = json(scene.run(['read', '--targets', '[{file_path: a.js}]']))
1043
+ const notJson = json(scene.run(['basetools', 'read', '--targets', '[{file_path: a.js}]']))
991
1044
  check(notJson.code === 'USAGE' && String(notJson.error).includes('--targets') && String(notJson.error).includes('JSON'), '④ 字面 JSON 坏了 ⇒ 说清来源与形状', String(notJson.error).slice(0, 140))
992
- const missingSpec = json(scene.run(['read', '--targets', '@nope.json']))
1045
+ const missingSpec = json(scene.run(['basetools', 'read', '--targets', '@nope.json']))
993
1046
  check(missingSpec.code === 'USAGE' && String(missingSpec.error).includes('@nope.json'), '④ @文件 读不到 ⇒ 报的是实际来源', String(missingSpec.error).slice(0, 140))
994
- const bare = json(scene.run(['read', '--targets']))
1047
+ const bare = json(scene.run(['basetools', 'read', '--targets']))
995
1048
  check(bare.code === 'USAGE', `④ \`--targets\` 没带值 ⇒ USAGE(收到 ${JSON.stringify(String(bare.error).slice(0, 60))})`)
996
1049
  // 与 `--hunks-file` 同一条理由:PowerShell 5.1 默认写 BOM,剥掉即可
997
1050
  fs.writeFileSync(path.join(scene.dir, 'spec.json'), Buffer.concat([BOM, Buffer.from(one, 'utf8')]))
998
- const bomSpec = scene.run(['read', '--targets', '@spec.json'])
1051
+ const bomSpec = scene.run(['basetools', 'read', '--targets', '@spec.json'])
999
1052
  check(bomSpec.status === 0 && json(bomSpec).ok === true, `④ \`@文件\` 带 BOM 照样能用(exit=${bomSpec.status})`, String(bomSpec.stderr).slice(0, 100))
1000
1053
 
1001
1054
  // ⑤ 顶层不认识的键不许静默忽略(`dryrun` 拼错而照写盘是最坏的一种)
1002
1055
  const unknown = json(
1003
- scene.run(['edit', '--targets', JSON.stringify({ targets: [{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }], dryrun: true })]),
1056
+ scene.run(['basetools', 'edit', '--targets', JSON.stringify({ targets: [{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }], dryrun: true })]),
1004
1057
  )
1005
1058
  check(unknown.code === 'USAGE' && unknown.shape === 'ARGS_UNKNOWN_KEY', `⑤ 未知顶层键 ⇒ 拒绝(shape=${unknown.shape})`, String(unknown.error).slice(0, 140))
1006
1059
  check(fs.readFileSync(path.join(scene.dir, 'a.js'), 'utf8') === MT_SOURCE, '⑤ 被拒的写没有碰磁盘(逐字节不变)')
@@ -1018,11 +1071,11 @@ say('MT-K MCP:targets 的声明、逐项回执、汇总行与既有形状')
1018
1071
  { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
1019
1072
  { jsonrpc: '2.0', method: 'notifications/initialized' },
1020
1073
  { jsonrpc: '2.0', id: 2, method: 'tools/list' },
1021
- call(3, 'aura_read', { targets: [{ file_path: 'a.js', mode: 'digest' }, { file_path: 'b.md', mode: 'lines', lines: [1, 2] }] }),
1022
- call(4, 'aura_read', { targets: [{ file_path: 'a.js', mode: 'digest' }, { file_path: 'nope.md', mode: 'full' }] }),
1023
- call(5, 'aura_edit', { targets: [{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ALPHA' }] }] }),
1024
- call(6, 'aura_edit', { file_path: 'c.md', targets: [{ file_path: 'c.md', hunks: [{ lines: [2, 2], replace: 'B' }] }] }),
1025
- call(7, 'aura_read', { file: 'a.js', mode: 'digest' }),
1074
+ call(3, 'aura_basetools_read', { targets: [{ file_path: 'a.js', mode: 'digest' }, { file_path: 'b.md', mode: 'lines', lines: [1, 2] }] }),
1075
+ call(4, 'aura_basetools_read', { targets: [{ file_path: 'a.js', mode: 'digest' }, { file_path: 'nope.md', mode: 'full' }] }),
1076
+ call(5, 'aura_basetools_edit', { targets: [{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ALPHA' }] }] }),
1077
+ call(6, 'aura_basetools_edit', { file_path: 'c.md', targets: [{ file_path: 'c.md', hunks: [{ lines: [2, 2], replace: 'B' }] }] }),
1078
+ call(7, 'aura_basetools_read', { file: 'a.js', mode: 'digest' }),
1026
1079
  ])
1027
1080
  const replies = res.stdout.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
1028
1081
  const byId = new Map(replies.map((r) => [r.id, r]))
@@ -1030,9 +1083,9 @@ say('MT-K MCP:targets 的声明、逐项回执、汇总行与既有形状')
1030
1083
  const tools = byId.get(2)?.result?.tools ?? []
1031
1084
  const schemaOf = (name) => tools.find((t) => t.name === name)?.inputSchema ?? {}
1032
1085
  check(
1033
- schemaOf('aura_read').properties?.targets?.type === 'array' && schemaOf('aura_edit').properties?.targets?.type === 'array',
1086
+ schemaOf('aura_basetools_read').properties?.targets?.type === 'array' && schemaOf('aura_basetools_edit').properties?.targets?.type === 'array',
1034
1087
  'K 两个工具的 inputSchema 都声明了 targets 数组',
1035
- JSON.stringify({ read: schemaOf('aura_read').properties?.targets, edit: schemaOf('aura_edit').properties?.targets }).slice(0, 160),
1088
+ JSON.stringify({ read: schemaOf('aura_basetools_read').properties?.targets, edit: schemaOf('aura_basetools_edit').properties?.targets }).slice(0, 160),
1036
1089
  )
1037
1090
  const batch = envOf(3)
1038
1091
  check(batch.ok === true && batch.entries?.length === 2, 'K file_path 缺席 + targets 在场 ⇒ 不再 BAD_ARGS,逐项照给', JSON.stringify(batch).slice(0, 180))
@@ -1043,10 +1096,10 @@ say('MT-K MCP:targets 的声明、逐项回执、汇总行与既有形状')
1043
1096
  check(partial.summary === 'targets: 2 | ok 1 | not_found 1', `K 缺一项时的汇总行 ${JSON.stringify(partial.summary)}`)
1044
1097
  check(byId.get(4)?.result?.isError === true, `K 有失败 ⇒ isError=true(${byId.get(4)?.result?.isError})`)
1045
1098
  const editBatch = envOf(5)
1046
- check(editBatch.ok === true && editBatch.edits?.[0]?.landed === true, 'K aura_edit 带 targets 走引擎批入口', JSON.stringify(editBatch).slice(0, 180))
1099
+ check(editBatch.ok === true && editBatch.edits?.[0]?.landed === true, 'K aura_basetools_edit 带 targets 走引擎批入口', JSON.stringify(editBatch).slice(0, 180))
1047
1100
  check(fs.readFileSync(path.join(scene.dir, 'c.md'), 'utf8') === 'ALPHA\nbravo\n', 'K MCP 批编辑真落盘', JSON.stringify(fs.readFileSync(path.join(scene.dir, 'c.md'), 'utf8')))
1048
1101
  check(envOf(6).code === 'ARGS_SHAPE_EXCLUSIVE', `K file_path 与 targets 同时给 ⇒ 互斥判据(${envOf(6).code})`, String(envOf(6).error).slice(0, 140))
1049
- check(envOf(7).code === 'BAD_ARGS', `K 两种形状都没给 ⇒ 仍是 BAD_ARGS(既有行为不变,${envOf(7).code})`, String(envOf(7).error).slice(0, 140))
1102
+ check(envOf(7).code === 'ARGS_UNKNOWN_KEY', `K 两种形状都没给仍被未知键守卫点名(${envOf(7).code})`, String(envOf(7).error).slice(0, 140))
1050
1103
  check(res.stderr.trim() === '', 'K 没靠 stderr 兑底(不用抛出当回包)', res.stderr.trim().slice(0, 80))
1051
1104
  }
1052
1105
 
@@ -1088,7 +1141,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1088
1141
  }
1089
1142
  const bytesOf = (p) => fs.readFileSync(p)
1090
1143
  const tierOf = (env) => String(env?.result?.applied?.[0]?.tier ?? env?.applied?.[0]?.tier ?? '')
1091
- const readHandle = (scene) => json(scene.run(['read', '--file', 'a.js', '--mode', 'lines', '--lines', '1,4'])).handle
1144
+ const readHandle = (scene) => json(scene.run(['basetools', 'read', '--file', 'a.js', '--mode', 'lines', '--lines', '1,4'])).handle
1092
1145
  /** 逐字节只有第 n 行变:行数、每行的行尾字节、其余每一行都要一模一样(返回 null = 通过)。 */
1093
1146
  const onlyLineChanged = (before, after, lineNo) => {
1094
1147
  const a = split(String(before))
@@ -1103,20 +1156,20 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1103
1156
  const declScene = mtScene('l-decl')
1104
1157
  declScene.write('a.js', MT_SOURCE)
1105
1158
  const decl = mcpReplies(
1106
- declScene.mcp([...MCP_HELLO, { jsonrpc: '2.0', id: 2, method: 'tools/list' }, mcpCall(3, 'aura_read', { file_path: 'a.js', mode: 'lines', lines: [1, 4] })]),
1159
+ declScene.mcp([...MCP_HELLO, { jsonrpc: '2.0', id: 2, method: 'tools/list' }, mcpCall(3, 'aura_basetools_read', { file_path: 'a.js', mode: 'lines', lines: [1, 4] })]),
1107
1160
  )
1108
- const editTool = decl.tools.find((t) => t.name === 'aura_edit')
1161
+ const editTool = decl.tools.find((t) => t.name === 'aura_basetools_edit')
1109
1162
  const hunkProps = Object.keys(editTool?.inputSchema?.properties?.hunks?.items?.properties ?? {})
1110
1163
  const missing = ['handle', 'line', 'position'].filter((key) => !hunkProps.includes(key))
1111
1164
  check(
1112
1165
  missing.length === 0,
1113
- 'L① MCP 声明面:aura_edit 的 hunks.items.properties 收得下 handle / line / position',
1166
+ 'L① MCP 声明面:aura_basetools_edit 的 hunks.items.properties 收得下 handle / line / position',
1114
1167
  `缺 ${missing.join(',') || '(无)'};实得 ${hunkProps.join(',')}`,
1115
1168
  )
1116
1169
  const hunkRequired = editTool?.inputSchema?.properties?.hunks?.items?.required ?? []
1117
1170
  check(JSON.stringify(hunkRequired) === '["replace"]', 'L① required 仍是 ["replace"](三个从属键一个都不进去)', JSON.stringify(hunkRequired))
1118
1171
 
1119
- // ② MCP 真改一次:柄从上面那次 `aura_read` 拿;正文行尾的 <h…> 与结构化 handle 必须同值
1172
+ // ② MCP 真改一次:柄从上面那次 `aura_basetools_read` 拿;正文行尾的 <h…> 与结构化 handle 必须同值
1120
1173
  const mcpRead = decl.env(3)
1121
1174
  const mcpHandle = mcpRead.handle
1122
1175
  const marker = (String(mcpRead.text).match(HANDLE_RE) ?? [null])[0]
@@ -1127,7 +1180,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1127
1180
  )
1128
1181
  const mcpBefore = bytesOf(path.join(declScene.dir, 'a.js'))
1129
1182
  const mcpEdit = mcpReplies(
1130
- declScene.mcp([...MCP_HELLO, mcpCall(4, 'aura_edit', { file_path: 'a.js', hunks: [{ handle: mcpHandle, line: 2, replace: LINE2 }] })]),
1183
+ declScene.mcp([...MCP_HELLO, mcpCall(4, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ handle: mcpHandle, line: 2, replace: LINE2 }] })]),
1131
1184
  ).env(4)
1132
1185
  const mcpAfter = bytesOf(path.join(declScene.dir, 'a.js'))
1133
1186
  check(
@@ -1142,7 +1195,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1142
1195
  const cliScene = mtScene('l-cli')
1143
1196
  cliScene.write('a.js', MT_SOURCE)
1144
1197
  const cliHandle = readHandle(cliScene)
1145
- const cliRun = cliScene.run(['edit', '--file', 'a.js', '--handle', String(cliHandle), '--line', '2', '--replace', LINE2])
1198
+ const cliRun = cliScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', String(cliHandle), '--line', '2', '--replace', LINE2])
1146
1199
  const cliEdit = json(cliRun)
1147
1200
  check(
1148
1201
  cliRun.status === 0 && cliEdit.ok === true,
@@ -1156,7 +1209,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1156
1209
  const subScene = mtScene('l-sub')
1157
1210
  subScene.write('a.js', MT_SOURCE)
1158
1211
  const subBefore = bytesOf(path.join(subScene.dir, 'a.js'))
1159
- const subRun = subScene.run(['edit', '--file', 'a.js', '--line', '2', '--replace', 'X'])
1212
+ const subRun = subScene.run(['basetools', 'edit', '--file', 'a.js', '--line', '2', '--replace', 'X'])
1160
1213
  const subEnv = json(subRun)
1161
1214
  check(
1162
1215
  subRun.status === 1 && subEnv.code === 'USAGE',
@@ -1168,7 +1221,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1168
1221
  // `--position after`:在区间**边界**插入,区间本身一个字节不动
1169
1222
  const posScene = mtScene('l-pos')
1170
1223
  posScene.write('a.js', MT_SOURCE)
1171
- const posRun = posScene.run(['edit', '--file', 'a.js', '--handle', String(readHandle(posScene)), '--position', 'after', '--replace', 'POST'])
1224
+ const posRun = posScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', String(readHandle(posScene)), '--position', 'after', '--replace', 'POST'])
1172
1225
  const posEnv = json(posRun)
1173
1226
  const posLines = split(bytesOf(path.join(posScene.dir, 'a.js')).toString('utf8'))
1174
1227
  const origLines = split(MT_SOURCE)
@@ -1192,16 +1245,16 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
1192
1245
  const reset = () => fs.writeFileSync(triFile, Buffer.from(MT_SOURCE, 'utf8'))
1193
1246
  reset()
1194
1247
  const triCliHandle = readHandle(triScene)
1195
- const triCli = json(triScene.run(['edit', '--file', 'a.js', '--handle', String(triCliHandle), '--line', '2', '--replace', LINE2]))
1248
+ const triCli = json(triScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', String(triCliHandle), '--line', '2', '--replace', LINE2]))
1196
1249
  const triCliDisk = bytesOf(triFile)
1197
1250
 
1198
1251
  // 回到同一份字节:柄是「账本键 + 区间文本 + 行区间」的函数 ⇒ 重读一次的柄必须逐字符相同
1199
1252
  reset()
1200
1253
  const triMcpRead = mcpReplies(
1201
- triScene.mcp([...MCP_HELLO, mcpCall(3, 'aura_read', { file_path: 'a.js', mode: 'lines', lines: [1, 4] })]),
1254
+ triScene.mcp([...MCP_HELLO, mcpCall(3, 'aura_basetools_read', { file_path: 'a.js', mode: 'lines', lines: [1, 4] })]),
1202
1255
  ).env(3)
1203
1256
  const triMcp = mcpReplies(
1204
- triScene.mcp([...MCP_HELLO, mcpCall(4, 'aura_edit', { file_path: 'a.js', hunks: [{ handle: triMcpRead.handle, line: 2, replace: LINE2 }] })]),
1257
+ triScene.mcp([...MCP_HELLO, mcpCall(4, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ handle: triMcpRead.handle, line: 2, replace: LINE2 }] })]),
1205
1258
  ).env(4)
1206
1259
  const triMcpDisk = bytesOf(triFile)
1207
1260
 
@@ -1265,8 +1318,8 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
1265
1318
  .map((m) => { try { return JSON.parse(m[1]) } catch { return null } })
1266
1319
  .filter((o) => o !== null && typeof o.handle === 'string')
1267
1320
 
1268
- const notFoundRun = (scene) => scene.run(['edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: NF_QUOTE, replace: '// nope' }])])
1269
- const retryRun = (scene, handle) => scene.run(['edit', '--file', 'a.js', '--handle', handle, '--lines', `${NF_REGION.start},${NF_REGION.end}`, '--replace', NF_NEW.join('\n')])
1321
+ const notFoundRun = (scene) => scene.run(['basetools', 'edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: NF_QUOTE, replace: '// nope' }])])
1322
+ const retryRun = (scene, handle) => scene.run(['basetools', 'edit', '--file', 'a.js', '--handle', handle, '--lines', `${NF_REGION.start},${NF_REGION.end}`, '--replace', NF_NEW.join('\n')])
1270
1323
 
1271
1324
  // ── ① CLI:一次注定找不到的编辑 ──
1272
1325
  const cliScene = mtScene('m-cli')
@@ -1312,7 +1365,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
1312
1365
  }
1313
1366
  const mcpScene = mtScene('m-mcp')
1314
1367
  mcpScene.write('a.js', NF_SRC)
1315
- const mcpFailRes = mcpScene.mcp([...MCP_HELLO_M, mcpCallM(3, 'aura_edit', { file_path: 'a.js', hunks: [{ old: NF_QUOTE, replace: '// nope' }] })])
1368
+ const mcpFailRes = mcpScene.mcp([...MCP_HELLO_M, mcpCallM(3, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ old: NF_QUOTE, replace: '// nope' }] })])
1316
1369
  const mcpFail = envOfM(mcpFailRes, 3)
1317
1370
  const mcpRetry = nearHandles(textOf(mcpFail))
1318
1371
  check(
@@ -1322,7 +1375,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
1322
1375
  )
1323
1376
  const mcpRetryRes = mcpScene.mcp([
1324
1377
  ...MCP_HELLO_M,
1325
- mcpCallM(3, 'aura_edit', { file_path: 'a.js', hunks: [{ handle: mcpRetry[0]?.handle, lines: [NF_REGION.start, NF_REGION.end], replace: NF_NEW.join('\n') }] }),
1378
+ mcpCallM(3, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ handle: mcpRetry[0]?.handle, lines: [NF_REGION.start, NF_REGION.end], replace: NF_NEW.join('\n') }] }),
1326
1379
  ])
1327
1380
  const mcpRetryEnv = envOfM(mcpRetryRes, 3)
1328
1381
  const mcpDisk = fs.readFileSync(path.join(mcpScene.dir, 'a.js')).toString('utf8')
@@ -1374,7 +1427,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
1374
1427
  const AMB_SRC = ['const head = 0', ...Array.from({ length: 10 }, (_, i) => `const pad${String(i + 1).padStart(2, '0')} = ${i + 1}`), 'const dup = value * 2', ...Array.from({ length: 7 }, (_, i) => `const pad${String(i + 11).padStart(2, '0')} = ${i + 11}`), 'const dup = value * 2', ...Array.from({ length: 2 }, (_, i) => `const pad${String(i + 18).padStart(2, '0')} = ${i + 18}`), 'const dup = value * 2', 'const tail = 0'].join('\n') + '\n'
1375
1428
  const ambScene = mtScene('m-amb')
1376
1429
  ambScene.write('a.js', AMB_SRC)
1377
- const ambFail = json(ambScene.run(['edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: 'const dup = value * 2', replace: 'const dup = 0' }])]))
1430
+ const ambFail = json(ambScene.run(['basetools', 'edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: 'const dup = value * 2', replace: 'const dup = 0' }])]))
1378
1431
  const ambRetry = nearHandles(textOf(ambFail))
1379
1432
  // 候选在哪个键上:引擎的错误对象把细节挂在 `error` 下(DSH 壳再把它抬成外套字段)。
1380
1433
  // CLI 收到的是 `{ok,error,code}`(`error` 是**消息字符串**)⇒ 细节只能从 `result`/外套里找;
@@ -1390,7 +1443,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
1390
1443
  // 逐行都真能用:拿第三个候选的地址改一次(刻意不取第一个 "" 就近挑会蒙对)
1391
1444
  const ambPick = ambRetry[2]
1392
1445
  const ambBeforeDisk = fs.readFileSync(path.join(ambScene.dir, 'a.js')).toString('utf8')
1393
- const ambUse = json(ambScene.run(['edit', '--file', 'a.js', '--handle', ambPick.handle, '--replace', 'const dup = 99']))
1446
+ const ambUse = json(ambScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', ambPick.handle, '--replace', 'const dup = 99']))
1394
1447
  const ambAfterDisk = fs.readFileSync(path.join(ambScene.dir, 'a.js')).toString('utf8')
1395
1448
  check(
1396
1449
  ambUse.ok === true && ambAfterDisk !== ambBeforeDisk && ambAfterDisk.split('const dup = 99').length === 2,
@@ -1489,11 +1542,11 @@ say('O 写事务:journal / undo / 多目标回滚')
1489
1542
  const txFile = path.join(tmp, 'tx-single.txt')
1490
1543
  const txOriginal = 'one\ntwo\n'
1491
1544
  fs.writeFileSync(txFile, txOriginal)
1492
- const txEdit = json(run(['edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
1545
+ const txEdit = json(run(['basetools', 'edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
1493
1546
  check(txEdit.ok === true && typeof txEdit.result?.transaction?.id === 'string', 'O1 单目标编辑返回 transaction id', JSON.stringify(txEdit.result?.transaction ?? txEdit.code))
1494
1547
  check(fs.readFileSync(txFile, 'utf8') === 'ONE\ntwo\n', 'O1 编辑已落盘')
1495
1548
  const txId = txEdit.result?.transaction?.id
1496
- const txUndo = json(run(['undo', '--id', String(txId)]))
1549
+ const txUndo = json(run(['basetools', 'undo', '--id', String(txId)]))
1497
1550
  check(txUndo.ok === true && txUndo.status === 'undone', 'O2 undo 成功', JSON.stringify(txUndo))
1498
1551
  check(fs.readFileSync(txFile, 'utf8') === txOriginal, 'O2 undo 恢复原始字节')
1499
1552
 
@@ -1505,25 +1558,25 @@ say('O 写事务:journal / undo / 多目标回滚')
1505
1558
  { file_path: a, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1506
1559
  { file_path: b, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1507
1560
  ]
1508
- const multi = json(run(['edit', '--targets', JSON.stringify(targets), '--transaction']))
1561
+ const multi = json(run(['basetools', 'edit', '--targets', JSON.stringify(targets), '--transaction']))
1509
1562
  check(multi.ok === true && typeof multi.transaction?.id === 'string', 'O3 多目标编辑返回 transaction id', JSON.stringify(multi.transaction ?? multi.code))
1510
1563
  check(fs.readFileSync(a, 'utf8') === 'ALPHA\n' && fs.readFileSync(b, 'utf8') === 'BETA\n', 'O3 两个文件都已落盘')
1511
- const multiUndo = json(run(['undo', '--id', String(multi.transaction?.id)]))
1564
+ const multiUndo = json(run(['basetools', 'undo', '--id', String(multi.transaction?.id)]))
1512
1565
  check(multiUndo.ok === true && multiUndo.restored === 2, 'O4 多目标 undo 恢复两个文件', JSON.stringify(multiUndo))
1513
1566
  check(fs.readFileSync(a, 'utf8') === 'alpha\n' && fs.readFileSync(b, 'utf8') === 'beta\n', 'O4 两个文件都回到原始字节')
1514
1567
 
1515
- const list = json(run(['transactions']))
1568
+ const list = json(run(['basetools', 'transactions']))
1516
1569
  check(list.ok === true && Array.isArray(list.transactions) && list.transactions.length > 0, 'O5 transactions 列出当前工作区事务')
1517
1570
 
1518
1571
  // 事务后又被改过 ⇒ 默认拒绝 undo,force 才覆盖
1519
1572
  const guardFile = path.join(tmp, 'tx-guard.txt')
1520
1573
  fs.writeFileSync(guardFile, 'v0\n')
1521
- const guardEdit = json(run(['edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
1574
+ const guardEdit = json(run(['basetools', 'edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
1522
1575
  const guardId = guardEdit.result?.transaction?.id
1523
1576
  fs.writeFileSync(guardFile, 'external\n')
1524
- const blocked = json(run(['undo', '--id', String(guardId)]))
1577
+ const blocked = json(run(['basetools', 'undo', '--id', String(guardId)]))
1525
1578
  check(blocked.ok === false && blocked.code === 'TRANSACTION_STALE', 'O6 事务后文件被外部改过 ⇒ undo 默认拒绝', JSON.stringify({ code: blocked.code, error: blocked.error }))
1526
- const forced = json(run(['undo', '--id', String(guardId), '--force']))
1579
+ const forced = json(run(['basetools', 'undo', '--id', String(guardId), '--force']))
1527
1580
  check(forced.ok === true && fs.readFileSync(guardFile, 'utf8') === 'v0\n', 'O6 force 后仍可恢复')
1528
1581
 
1529
1582
  // O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚
@@ -1541,10 +1594,31 @@ say('O 写事务:journal / undo / 多目标回滚')
1541
1594
  { file_path: okFile, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1542
1595
  { file_path: hardFile, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1543
1596
  ]
1544
- const rollback = json(run(['edit', '--targets', JSON.stringify(rollbackTargets), '--transaction']))
1597
+ const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets), '--transaction']))
1545
1598
  check(rollback.ok === false && rollback.transaction?.status === 'rolled_back', 'O7 多目标部分失败 ⇒ transaction rolled_back', JSON.stringify({ ok: rollback.ok, tx: rollback.transaction, rollback: rollback.rollback }))
1546
1599
  check(rollback.rollback?.restored === 1 && fs.readFileSync(okFile, 'utf8') === 'alpha\n', 'O7 已落地的那个文件被恢复')
1547
1600
  check(fs.readFileSync(hardBase, 'utf8') === 'beta\n' && fs.readFileSync(hardFile, 'utf8') === 'beta\n', 'O7 硬链接目标未被改动')
1601
+ // 回执必须等于**回滚后的最终磁盘状态**,不能继续报 landed/applied。
1602
+ check(rollback.applied === 0 && rollback.landed_paths?.length === 0 && rollback.unchanged === true, 'O7 回滚后 applied=0 / landed_paths=[] / unchanged=true', JSON.stringify({ applied: rollback.applied, landed_paths: rollback.landed_paths, unchanged: rollback.unchanged }))
1603
+ check(rollback.rolled_back === true && rollback.edits?.filter((e) => e.outcome === 'rolled_back').every((e) => e.landed === false), 'O7 逐项 outcome=rolled_back 且 landed=false', JSON.stringify(rollback.edits?.map((e) => ({ outcome: e.outcome, landed: e.landed }))))
1604
+ check(!/applied [1-9]|landed [1-9]/.test(rollback.summary ?? ''), 'O7 汇总行不再出现 applied N / landed N', rollback.summary)
1605
+
1606
+ // 不带 --transaction 也必须能看出“已回滚”:回执事实不依赖是否索要事务 id。
1607
+ // 用一组**新文件**:上一轮回滚后观测账本里还留着“写下去的那版”,同一文件立刻重试会先被
1608
+ // 区域闸拦下(那是另一条保护,别把它混进回执断言)。
1609
+ const okFile2 = path.join(tmp, 'tx-rollback-ok2.txt')
1610
+ const hardBase2 = path.join(tmp, 'tx-rollback-hard-base2.txt')
1611
+ const hardFile2 = path.join(tmp, 'tx-rollback-hard2.txt')
1612
+ fs.writeFileSync(okFile2, 'alpha\n')
1613
+ fs.writeFileSync(hardBase2, 'beta\n')
1614
+ fs.linkSync(hardBase2, hardFile2)
1615
+ const rollbackTargets2 = [
1616
+ { file_path: okFile2, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1617
+ { file_path: hardFile2, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1618
+ ]
1619
+ const rollbackNoTx = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets2)]))
1620
+ check(rollbackNoTx.ok === false && rollbackNoTx.rollback?.status === 'rolled_back' && rollbackNoTx.rolled_back === true, 'O7 include_transaction=false 仍返回 rollback/rolled_back', JSON.stringify({ rollback: rollbackNoTx.rollback, rolled_back: rollbackNoTx.rolled_back }))
1621
+ check(rollbackNoTx.applied === 0 && rollbackNoTx.landed_paths?.length === 0 && fs.readFileSync(okFile2, 'utf8') === 'alpha\n', 'O7 不索要事务 id 时回执仍与最终磁盘一致', JSON.stringify({ applied: rollbackNoTx.applied, landed_paths: rollbackNoTx.landed_paths }))
1548
1622
  }
1549
1623
 
1550
1624
  // O8 锁:同一工作区已有写事务时,第二个编辑必须超时拒绝
@@ -1554,7 +1628,7 @@ say('O 写事务:journal / undo / 多目标回滚')
1554
1628
  process.env.AURA_CACHE_DIR = cache
1555
1629
  try {
1556
1630
  await withWorkspaceLock(tmp, async () => {
1557
- const res = spawnSync(process.execPath, [bin, 'edit', '--file', lockFile, '--lines', '1', '--replace', 'LOCK'], {
1631
+ const res = spawnSync(process.execPath, [bin, 'basetools', 'edit', '--file', lockFile, '--lines', '1', '--replace', 'LOCK'], {
1558
1632
  cwd: tmp,
1559
1633
  encoding: 'utf8',
1560
1634
  env: { ...env, AURA_LOCK_TIMEOUT_MS: '200' },