aura-basetools 0.13.3 → 0.15.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.
@@ -2577,35 +2577,6 @@ function locateByHandle(lines, hunk, parsed, ctx) {
2577
2577
  })
2578
2578
  }
2579
2579
 
2580
- /**
2581
- * `intent` 与哪些字段**不能同时出现**(T16)。
2582
- *
2583
- * `intent` 是**自成一体的一层地址**:它由引擎决定落到哪个精确区间。若同时给出任何别的地址,
2584
- * 谁说了算就没有正确答案——挑一个执行就是一次静默换语义(调用方写的两个地址指向不同地方时,
2585
- * 改错的那次不会有任何提示)。所以参数面直接拒绝,而不是挑一个。
2586
- *
2587
- * 名单与 {@link planHunks} 的地址判据同源:`target` / `lines`(含别名 `range`)/ `start_line` /
2588
- * `end_line` / `line_cue` / `anchor` / `old` / `context` / `handle`。
2589
- * `near` / `expect` / `replace` **不在**名单里:前两个是修饰,第三个是动作。
2590
- *
2591
- * 「在场」的判据逐字段照搬它自己那一层的:字符串看非空(`anchor` 看长度、其余看 trim),
2592
- * 坐标看 `!== undefined && !== null`。判据写宽或写窄都会让某个形状在参数面与引擎里得到两种结论。
2593
- *
2594
- * @returns 冲突的字段名(按上面的固定顺序);没有冲突时为空数组
2595
- */
2596
- function intentAddressConflicts(hunk) {
2597
- const clashes = []
2598
- if (typeof hunk.target === 'string' && hunk.target.trim().length > 0) clashes.push('target')
2599
- for (const key of ['lines', 'range', 'start_line', 'end_line']) {
2600
- if (hunk[key] !== undefined && hunk[key] !== null) clashes.push(key)
2601
- }
2602
- if (typeof hunk.line_cue === 'string' && hunk.line_cue.trim().length > 0) clashes.push('line_cue')
2603
- if (typeof hunk.anchor === 'string' && hunk.anchor.length > 0) clashes.push('anchor')
2604
- if (oldAddressOf(hunk) !== null) clashes.push('old')
2605
- if (typeof hunk.context === 'string' && hunk.context.trim().length > 0) clashes.push('context')
2606
- if (typeof hunk.handle === 'string' && hunk.handle.trim().length > 0) clashes.push('handle')
2607
- return clashes
2608
- }
2609
2580
 
2610
2581
  /**
2611
2582
  * 规划所有 hunk:逐个定位 → expect 核对 → 重叠检查 → 倒序拼接。
@@ -2661,7 +2632,7 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2661
2632
  *
2662
2633
  * 它不是第六种地址,而是 `old` 的一种作用范围;因此只认 `old` + 动作字段(`replace` /
2663
2634
  * `expect` / `eol`)。把 `target`/`lines`/`context`/`near` 混进来意味着"既想全改又想收敛到一处",
2664
- * 没有正确答案 —— 参数面直接拒绝,与 `intent` 的互斥同一条理由。
2635
+ * 没有正确答案 —— 参数面直接拒绝,与地址字段互斥同一条理由。
2665
2636
  */
2666
2637
  const replaceAll = hunk.replace_all
2667
2638
  if (replaceAll !== undefined && typeof replaceAll !== 'boolean') {
@@ -2673,7 +2644,7 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2673
2644
  if (replaceAll === true) {
2674
2645
  const clashes = []
2675
2646
  if (hasTarget) clashes.push('target')
2676
- if (typeof hunk.intent === 'string' && hunk.intent.trim().length > 0) clashes.push('intent')
2647
+
2677
2648
  for (const key of ['lines', 'range', 'start_line', 'end_line']) {
2678
2649
  if (hunk[key] !== undefined && hunk[key] !== null) clashes.push(key)
2679
2650
  }
@@ -2708,23 +2679,6 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2708
2679
  )
2709
2680
  }
2710
2681
  }
2711
- /**
2712
- * 意图地址(T16):`intent` 是**自成一体**的一层地址,与其余地址字段互斥。
2713
- *
2714
- * 冲突在**定位之前**就拒(参数面没成形,与 `PATCH_NO_ADDRESS` 同族),而不是在引擎里挑一个:
2715
- * 调用方写了两个地址说明它自己也没定,工具替它选一次就是一次看不见的换语义。
2716
- * `near` / `expect` / `replace` 不在此列——前两个是修饰,第三个是动作。
2717
- */
2718
- const hasIntent = typeof hunk.intent === 'string' && hunk.intent.trim().length > 0
2719
- if (hasIntent) {
2720
- const clashed = intentAddressConflicts(hunk)
2721
- if (clashed.length > 0) {
2722
- throw new PatchError(
2723
- `\`intent\` is a complete address by itself — it cannot be combined with ${clashed.join(', ')} (near / expect / replace are fine); drop either intent or those fields — nothing was written`,
2724
- 'PATCH_INTENT_CONFLICT',
2725
- )
2726
- }
2727
- }
2728
2682
  /** 句柄的形状在这里判(不对就抛错);`{handle, old}` 那一支的 `old` 仍然要命中区间内部。 */
2729
2683
  const parsedHandle = handleAddressOf(hunk)
2730
2684
  /**
@@ -2752,9 +2706,9 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2752
2706
  'PATCH_NO_ADDRESS',
2753
2707
  )
2754
2708
  }
2755
- if (oldText === null && parsedHandle === null && !hasTarget && range === undefined && !hasCue && !hasAnchor && !hasIntent) {
2709
+ if (oldText === null && parsedHandle === null && !hasTarget && range === undefined && !hasCue && !hasAnchor) {
2756
2710
  throw new PatchError(
2757
- gaveLineAddress(hunk) ? LINE_ADDRESS_HINT : 'each hunk needs one of: old, intent, target, lines, line_cue, anchor',
2711
+ gaveLineAddress(hunk) ? LINE_ADDRESS_HINT : 'each hunk needs one of: old, target, lines, line_cue, anchor',
2758
2712
  'PATCH_NO_ADDRESS',
2759
2713
  )
2760
2714
  }
@@ -2776,29 +2730,16 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2776
2730
  ? locateAllByOld(lines, hunk, oldText, filePath === null ? 'this file' : filePath, handleCtx)
2777
2731
  : [locateByOld(lines, hunk, oldText, filePath === null ? 'this file' : filePath, handleCtx)]
2778
2732
  } else {
2779
- /**
2780
- * 意图地址(T16)单独走一条请求:只带 `intent` / `near` / `expect` / `filePath`。
2781
- * 冲突字段在上面已经拒了,这里刻意**不再把其余地址字段递下去**——即便哪天冲突判据放宽,
2782
- * 引擎也不该从第二个地址里"顺手"拿到位置(那正是静默换语义的来路)。
2783
- */
2784
- const request = hasIntent
2785
- ? {
2786
- intent: hunk.intent,
2787
- ...(hunk.near === undefined ? {} : { near: hunk.near }),
2788
- ...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
2789
- ...(filePath === null ? {} : { filePath }),
2790
- ...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
2791
- }
2792
- : {
2793
- ...(hasTarget ? { target: hunk.target } : {}),
2794
- ...(range === undefined ? {} : { lines: range }),
2795
- ...(hunk.line_cue === undefined ? {} : { lineCue: hunk.line_cue }),
2796
- ...(hasTarget || range !== undefined || hunk.line_cue !== undefined ? {} : { anchor: hunk.anchor }),
2797
- ...(hunk.near === undefined ? {} : { near: hunk.near }),
2798
- ...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
2799
- ...(filePath === null ? {} : { filePath }),
2800
- ...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
2801
- }
2733
+ const request = {
2734
+ ...(hasTarget ? { target: hunk.target } : {}),
2735
+ ...(range === undefined ? {} : { lines: range }),
2736
+ ...(hunk.line_cue === undefined ? {} : { lineCue: hunk.line_cue }),
2737
+ ...(hasTarget || range !== undefined || hunk.line_cue !== undefined ? {} : { anchor: hunk.anchor }),
2738
+ ...(hunk.near === undefined ? {} : { near: hunk.near }),
2739
+ ...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
2740
+ ...(filePath === null ? {} : { filePath }),
2741
+ ...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
2742
+ }
2802
2743
  const found = locate(content, request)
2803
2744
  if (!found.ok) {
2804
2745
  throw new PatchError(found.message, found.code, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aura-basetools",
3
- "version": "0.13.3",
3
+ "version": "0.15.0",
4
4
  "description": "Byte-exact read, edit and write tools for coding agents. Standalone CLI and MCP server; no DSH required. 面向 coding agent 的逐字节读写工具;独立 CLI / MCP,可选 DSH 适配器。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -179,7 +179,7 @@ say('B2 能力清单:capabilities 从引擎常量派生')
179
179
  check(caps.ok === true && caps.langsCount === caps.langs?.length, `langsCount=${caps.langsCount}`, JSON.stringify(caps.langs?.length))
180
180
  check(caps.langsCount === 65, '语言表 65 种(与 LANGS 同源)', String(caps.langsCount))
181
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))
182
+ check(editTool?.addressKinds?.includes('target') && editTool.addressKinds.includes('handle'), '编辑地址档含 target/handle', JSON.stringify(editTool?.addressKinds))
183
183
  check(editTool?.options?.includes('replace_all'), '编辑选项含 replace_all', JSON.stringify(editTool?.options))
184
184
  check(editTool?.options?.includes('require_observation'), '编辑选项含 require_observation', JSON.stringify(editTool?.options))
185
185
  check(caps.limits?.multiTargetsMax === 32 && caps.limits?.defaultPruneBudget === 8192 && caps.limits?.contentBudget === 7492, '限制与常量一致', JSON.stringify(caps.limits))
@@ -385,7 +385,7 @@ say('F MCP:initialize / tools/list / tools/call')
385
385
  })()
386
386
  if (manifest !== null) check(byId.get(1)?.result?.serverInfo?.version === manifest.version, `serverInfo.version 跟包一致(${manifest.version})`, String(byId.get(1)?.result?.serverInfo?.version))
387
387
  const tools = byId.get(2)?.result?.tools ?? []
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(',')}`)
388
+ check(tools.length === 3 && 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/list 三个工具(${tools.map((t) => t.name).join(',')}`)
389
389
  const readEnv = JSON.parse(byId.get(3)?.result?.content?.[0]?.text ?? '{}')
390
390
  check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), 'aura_basetools_read 经 MCP 可用', JSON.stringify(readEnv).slice(0, 120))
391
391
  const editEnv = JSON.parse(byId.get(4)?.result?.content?.[0]?.text ?? '{}')
@@ -1537,49 +1537,9 @@ say('N provider:mode / symlink / hardlink / range / 上限 / cache')
1537
1537
  }
1538
1538
  }
1539
1539
 
1540
- say('O 写事务:journal / undo / 多目标回滚')
1540
+ say('O 写失败回滚与锁(0.14.0 已移除公开 undo/transactions)')
1541
1541
  {
1542
- const txFile = path.join(tmp, 'tx-single.txt')
1543
- const txOriginal = 'one\ntwo\n'
1544
- fs.writeFileSync(txFile, txOriginal)
1545
- const txEdit = json(run(['basetools', 'edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
1546
- check(txEdit.ok === true && typeof txEdit.result?.transaction?.id === 'string', 'O1 单目标编辑返回 transaction id', JSON.stringify(txEdit.result?.transaction ?? txEdit.code))
1547
- check(fs.readFileSync(txFile, 'utf8') === 'ONE\ntwo\n', 'O1 编辑已落盘')
1548
- const txId = txEdit.result?.transaction?.id
1549
- const txUndo = json(run(['basetools', 'undo', '--id', String(txId)]))
1550
- check(txUndo.ok === true && txUndo.status === 'undone', 'O2 undo 成功', JSON.stringify(txUndo))
1551
- check(fs.readFileSync(txFile, 'utf8') === txOriginal, 'O2 undo 恢复原始字节')
1552
-
1553
- const a = path.join(tmp, 'tx-a.txt')
1554
- const b = path.join(tmp, 'tx-b.txt')
1555
- fs.writeFileSync(a, 'alpha\n')
1556
- fs.writeFileSync(b, 'beta\n')
1557
- const targets = [
1558
- { file_path: a, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1559
- { file_path: b, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1560
- ]
1561
- const multi = json(run(['basetools', 'edit', '--targets', JSON.stringify(targets), '--transaction']))
1562
- check(multi.ok === true && typeof multi.transaction?.id === 'string', 'O3 多目标编辑返回 transaction id', JSON.stringify(multi.transaction ?? multi.code))
1563
- check(fs.readFileSync(a, 'utf8') === 'ALPHA\n' && fs.readFileSync(b, 'utf8') === 'BETA\n', 'O3 两个文件都已落盘')
1564
- const multiUndo = json(run(['basetools', 'undo', '--id', String(multi.transaction?.id)]))
1565
- check(multiUndo.ok === true && multiUndo.restored === 2, 'O4 多目标 undo 恢复两个文件', JSON.stringify(multiUndo))
1566
- check(fs.readFileSync(a, 'utf8') === 'alpha\n' && fs.readFileSync(b, 'utf8') === 'beta\n', 'O4 两个文件都回到原始字节')
1567
-
1568
- const list = json(run(['basetools', 'transactions']))
1569
- check(list.ok === true && Array.isArray(list.transactions) && list.transactions.length > 0, 'O5 transactions 列出当前工作区事务')
1570
-
1571
- // 事务后又被改过 ⇒ 默认拒绝 undo,force 才覆盖
1572
- const guardFile = path.join(tmp, 'tx-guard.txt')
1573
- fs.writeFileSync(guardFile, 'v0\n')
1574
- const guardEdit = json(run(['basetools', 'edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
1575
- const guardId = guardEdit.result?.transaction?.id
1576
- fs.writeFileSync(guardFile, 'external\n')
1577
- const blocked = json(run(['basetools', 'undo', '--id', String(guardId)]))
1578
- check(blocked.ok === false && blocked.code === 'TRANSACTION_STALE', 'O6 事务后文件被外部改过 ⇒ undo 默认拒绝', JSON.stringify({ code: blocked.code, error: blocked.error }))
1579
- const forced = json(run(['basetools', 'undo', '--id', String(guardId), '--force']))
1580
- check(forced.ok === true && fs.readFileSync(guardFile, 'utf8') === 'v0\n', 'O6 force 后仍可恢复')
1581
-
1582
- // O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚
1542
+ // O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚(内部事务日志仍保留)
1583
1543
  const okFile = path.join(tmp, 'tx-rollback-ok.txt')
1584
1544
  const hardBase = path.join(tmp, 'tx-rollback-hard-base.txt')
1585
1545
  const hardFile = path.join(tmp, 'tx-rollback-hard.txt')
@@ -1594,31 +1554,13 @@ say('O 写事务:journal / undo / 多目标回滚')
1594
1554
  { file_path: okFile, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1595
1555
  { file_path: hardFile, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1596
1556
  ]
1597
- const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets), '--transaction']))
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 }))
1557
+ const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets)]))
1558
+ check(rollback.ok === false && rollback.rollback?.status === 'rolled_back' && rollback.rolled_back === true, 'O7 多目标部分失败 ⇒ 自动回滚', JSON.stringify({ ok: rollback.ok, rollback: rollback.rollback }))
1599
1559
  check(rollback.rollback?.restored === 1 && fs.readFileSync(okFile, 'utf8') === 'alpha\n', 'O7 已落地的那个文件被恢复')
1600
1560
  check(fs.readFileSync(hardBase, 'utf8') === 'beta\n' && fs.readFileSync(hardFile, 'utf8') === 'beta\n', 'O7 硬链接目标未被改动')
1601
- // 回执必须等于**回滚后的最终磁盘状态**,不能继续报 landed/applied。
1602
1561
  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
1562
  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
1563
  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 }))
1622
1564
  }
1623
1565
 
1624
1566
  // O8 锁:同一工作区已有写事务时,第二个编辑必须超时拒绝
@@ -4,7 +4,7 @@
4
4
  * 覆盖:
5
5
  * A 新建:真正建盘、回执 operation=create、dry-run 不建盘、事务可撤销(撤销后文件消失)
6
6
  * B 覆写:整份替换、BOM 与文件既有行尾保留、有无尾换行精确、no-overwrite、dry-run、
7
- * 事务 + undo 逐字节恢复、`--content @file`
7
+ * `--content @file`(0.14.0 移除公开事务/undo)
8
8
  * C MCP:aura_basetools_write 出现在 tools/list,create/overwrite/no_overwrite/dry_run/坏参数都走同一引擎
9
9
  * D 能力清单:commands 与 tools 都包含 write
10
10
  *
@@ -55,7 +55,7 @@ process.on('exit', () => {
55
55
  })
56
56
 
57
57
  // ── A 新建 ────────────────────────────────────────────────────────────────
58
- say('A 新建:整份建盘,dry-run 不建盘,事务可撤销')
58
+ say('A 新建:整份建盘,dry-run 不建盘')
59
59
  {
60
60
  const created = json(run(['basetools', 'write', '--file', 'created.txt', '--content', 'alpha\nbeta\n']))
61
61
  check(created.ok === true && created.result?.operation === 'create', 'A1 新建回执 operation=create', JSON.stringify(created).slice(0, 160))
@@ -66,13 +66,6 @@ say('A 新建:整份建盘,dry-run 不建盘,事务可撤销')
66
66
  check(dry.ok === true && dry.result?.operation === 'create' && dry.result?.dryRun === true && dry.result?.bytes === 1, 'A4 新建 dry-run 回执给 create/bytes,但不落盘', JSON.stringify(dry).slice(0, 160))
67
67
  check(fs.existsSync(file('dry-new.txt')) === false, 'A5 新建 dry-run 之后文件仍不存在')
68
68
 
69
- const tx = json(run(['basetools', 'write', '--file', 'tx-created.txt', '--content', 'gone\n', '--transaction']))
70
- const txId = tx.result?.transaction?.id
71
- check(tx.ok === true && typeof txId === 'string' && tx.result.transaction.status === 'committed', 'A6 --transaction 返回 committed 事务', JSON.stringify(tx.result?.transaction))
72
- const undone = json(run(['basetools', 'undo', '--id', txId]))
73
- check(undone.ok === true && undone.status === 'undone', 'A7 undo 成功', JSON.stringify(undone).slice(0, 160))
74
- check(fs.existsSync(file('tx-created.txt')) === false, 'A8 撤销“新建”后文件消失(不留下空文件)')
75
-
76
69
  const missingFile = json(run(['basetools', 'write', '--content', 'x']))
77
70
  const missingContent = json(run(['basetools', 'write', '--file', 'x.txt']))
78
71
  check(missingFile.ok === false && missingFile.code === 'USAGE', 'A9 缺 --file ⇒ USAGE', JSON.stringify(missingFile).slice(0, 140))
@@ -119,14 +112,6 @@ say('B 覆写:整份替换,BOM / 既有行尾 / 尾换行精确')
119
112
  const newNoOverwrite = json(run(['basetools', 'write', '--file', 'brand-new.txt', '--content', 'ok\n', '--no-overwrite']))
120
113
  check(newNoOverwrite.ok === true, 'B11 no-overwrite 对不存在的文件照常创建')
121
114
 
122
- fs.writeFileSync(file('tx-restore.txt'), Buffer.concat([BOM, Buffer.from('orig\r\nbytes\r\n', 'utf8')]))
123
- prime('tx-restore.txt')
124
- const original = fs.readFileSync(file('tx-restore.txt'))
125
- const tx = json(run(['basetools', 'write', '--file', 'tx-restore.txt', '--content', 'changed\n', '--transaction']))
126
- check(!fs.readFileSync(file('tx-restore.txt')).equals(original), 'B12 覆写真的改了盘')
127
- const undone = json(run(['basetools', 'undo', '--id', tx.result?.transaction?.id]))
128
- check(undone.ok === true && fs.readFileSync(file('tx-restore.txt')).equals(original), 'B13 undo 逐字节恢复覆写前的原文(含 BOM/CRLF)')
129
-
130
115
  fs.writeFileSync(file('content-source.txt'), 'from @file\nsecond line\n')
131
116
  const atFile = json(run(['basetools', 'write', '--file', 'from-at.txt', '--content', '@' + file('content-source.txt')]))
132
117
  check(atFile.ok === true && fs.readFileSync(file('from-at.txt'), 'utf8') === fs.readFileSync(file('content-source.txt'), 'utf8'), 'B14 --content @file 读入整份内容')
@@ -225,7 +210,7 @@ say('C MCP:aura_basetools_write 声明面与调用面')
225
210
 
226
211
  const tools = byId.get(2)?.result?.tools ?? []
227
212
  const writeTool = tools.find((tool) => tool.name === 'aura_basetools_write')
228
- check(tools.length === 5 && writeTool !== undefined, `C1 tools/list 五个工具且含 aura_basetools_write(${tools.map((tool) => tool.name).join(',')})`)
213
+ check(tools.length === 3 && writeTool !== undefined, `C1 tools/list 三个工具且含 aura_basetools_write(${tools.map((tool) => tool.name).join(',')})`)
229
214
  check(JSON.stringify(writeTool?.inputSchema?.required) === JSON.stringify(['file_path', 'content']), 'C2 aura_basetools_write schema 必填 file_path + content', JSON.stringify(writeTool?.inputSchema?.required))
230
215
  check(envOf(3).ok === true && envOf(3).result?.operation === 'create', 'C3 经 MCP 新建成功', JSON.stringify(envOf(3)).slice(0, 160))
231
216
  check(fs.readFileSync(file('mcp-created.txt'), 'utf8') === 'mcp\ncreate\n', 'C4 MCP 新建的内容真的落盘')