aura-basetools 0.14.0 → 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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # aura-basetools 0.14.0
1
+ # aura-basetools 0.15.0
2
2
 
3
3
  Byte-exact file tools for coding agents: read by intent, edit by address, and write whole files without corrupting bytes. One engine shared by a standalone CLI and an MCP server.
4
4
 
@@ -21,7 +21,7 @@ The engine is intentionally not a general file manager: it reads and edits UTF-8
21
21
  ### Install
22
22
 
23
23
  ```bash
24
- npm i -g aura-basetools@0.14.0
24
+ npm i -g aura-basetools@0.15.0
25
25
  aura --version
26
26
  ```
27
27
 
@@ -90,6 +90,7 @@ Compared with the DSH built-in `dsh-tool-fs` tools, the differences are delibera
90
90
  - **Read by intent, not whole file.** `map` / `digest` / `lines` / `search` return the structural slice you ask for; the customer comparison measured about 1/24 of the context for the same file.
91
91
  - **Edits refuse stale bases.** A region-level gate rejects an edit when the addressed region changed after the read that authorized it, and points at the lines to re-read; `expect` makes the intended text explicit.
92
92
  - **Writes are verified, not assumed.** Every write goes through a temp file, flush, rename, and a byte-for-byte read-back; multi-target edits plan first and write nothing if any target fails.
93
+ - **Changes can prove themselves.** `edit` / `write` accept an explicit `verify` command; a non-zero exit or timeout rolls the transaction back and returns the command output plus the rollback state. No auto-discovery of repo scripts.
93
94
  - **Addressing is structural.** `target` / `lines` / `old` / `expect` / handles replace "copy the literal text exactly"; failure receipts include ready-to-use retry addresses.
94
95
  - **The core is DSH-free.** `aura-basetools` has no runtime dependencies and no DSH imports; the DSH adapter is a separate optional package.
95
96
 
@@ -134,7 +135,7 @@ MIT.
134
135
  ### 安装
135
136
 
136
137
  ```bash
137
- npm i -g aura-basetools@0.14.0
138
+ npm i -g aura-basetools@0.15.0
138
139
  aura --version
139
140
  ```
140
141
 
@@ -203,6 +204,7 @@ npx -y --package aura-basetools aura basetools mcp
203
204
  - **按意图读,不读整份。** `map` / `digest` / `lines` / `search` 只返回你要的结构切片;客户版对照实测同一文件上下文约 1/24。
204
205
  - **依据过期就拒绝。** 区域级陈旧闸门在“授权读取之后该区域被改过”时拒绝,并指出重读哪几行;`expect` 让目标原文显式化。
205
206
  - **写完要验,不靠相信。** 每次写盘走临时文件、flush、rename,再做逐字节回读;多目标先全量规划,任一失败则一个字节都不写。
207
+ - **变更可以自证。** `edit` / `write` 接受显式 `verify` 命令;非零退出或超时会把整个事务回滚,回执带命令输出与回滚状态。不自动发现仓库脚本。
206
208
  - **寻址是结构化的。** `target` / `lines` / `old` / `expect` / 句柄替代“把原文一字不差抄对”;失败回执带可直接用的重试地址。
207
209
  - **核心不依赖 DSH。** `aura-basetools` 零运行时依赖、无 DSH import;DSH 适配器是独立可选包。
208
210
 
package/bin/aura-mcp.mjs CHANGED
@@ -200,6 +200,10 @@ const TOOLS = [
200
200
  type: 'boolean',
201
201
  description: '严格新鲜度:要求这一版文件在本会话里被读过(或由本工具写过);否则 PATCH_UNOBSERVED。create 不受影响;dry_run 同样执行这条判决。',
202
202
  },
203
+ verify: {
204
+ type: 'string',
205
+ description: '写后验证命令(显式给出才执行;在 workspace 根下、受超时/输出上限约束)。非零退出 ⇒ 自动回滚并返回 PATCH_VERIFY_FAILED。不自动发现/执行仓库脚本。',
206
+ },
203
207
  create: { type: 'string', description: '文件不存在时用它创建(整份内容)' },
204
208
  targets: TARGETS_SCHEMA,
205
209
  },
@@ -237,6 +241,10 @@ const TOOLS = [
237
241
  enum: ['auto', 'preserve', 'lf', 'crlf'],
238
242
  description: '覆写时的换行风格:auto(默认)适配文件已有风格;preserve 按 content 原样写;lf/crlf 强制。新建文件始终按 content 原样写(非 auto 会被拒绝)。',
239
243
  },
244
+ verify: {
245
+ type: 'string',
246
+ description: '写后验证命令(显式给出才执行;非零退出 ⇒ 自动回滚并返回 PATCH_VERIFY_FAILED)。',
247
+ },
240
248
  },
241
249
  required: ['file_path', 'content'],
242
250
  },
@@ -324,6 +332,7 @@ async function editTargetsCall(provider, args) {
324
332
  targets,
325
333
  dryRun: args.dry_run === true,
326
334
  ...(args.require_observation === true ? { requireObservation: true } : {}),
335
+ ...(typeof args.verify === 'string' ? { verify: args.verify } : {}),
327
336
  })
328
337
  }
329
338
 
@@ -408,6 +417,7 @@ async function callTool(provider, name, args = {}) {
408
417
  dryRun: args.dry_run === true,
409
418
  create: creating ? args.create : undefined,
410
419
  ...(args.require_observation === true ? { requireObservation: true } : {}),
420
+ ...(typeof args.verify === 'string' ? { verify: args.verify } : {}),
411
421
  })
412
422
  }
413
423
  if (name === 'aura_basetools_write') {
@@ -418,6 +428,7 @@ async function callTool(provider, name, args = {}) {
418
428
  noOverwrite: args.no_overwrite === true,
419
429
  allowUnobserved: args.allow_unobserved === true,
420
430
  ...(typeof args.eol === 'string' ? { eol: args.eol } : {}),
431
+ ...(typeof args.verify === 'string' ? { verify: args.verify } : {}),
421
432
  })
422
433
  }
423
434
  return { ok: false, error: `unknown tool ${JSON.stringify(name)}`, code: 'UNKNOWN_TOOL' }
package/bin/aura.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * aura CLI(薄壳):参数解析 → `lib/ops.mjs` → 一行 JSON。
4
4
  *
5
5
  * aura basetools read --file F [--mode …] [--around X] [--lines a,b] [--search S] [--since V] [--max-chars N]
6
- * aura basetools edit --file F (--hunks '<json>' | --hunks-file P) [--dry-run] [--create <文本|@文件>]
6
+ * aura basetools edit --file F (--hunks '<json>' | --hunks-file P) [--dry-run] [--verify "命令"] [--create <文本|@文件>]
7
7
  * aura basetools read --targets '[...]|@spec.json' 一次读多个目标(与 --file 互斥)
8
8
  * aura basetools edit --targets '[...]|@spec.json' 一次改多个文件(与 --file 互斥;任一规划失败 ⇒ 零写入)
9
9
  * aura basetools mcp
@@ -86,11 +86,11 @@ const COMMAND_FLAGS = {
86
86
  bool: ['pretty'],
87
87
  },
88
88
  edit: {
89
- value: ['cwd', 'file', 'targets', 'hunks', 'hunks-file', 'create', 'target', 'lines', 'start-line', 'end-line', 'line-cue', 'anchor', 'old', 'context', 'handle', 'line', 'position', 'replace', 'expect', 'near', 'eol'],
89
+ value: ['cwd', 'file', 'targets', 'hunks', 'hunks-file', 'create', 'target', 'lines', 'start-line', 'end-line', 'line-cue', 'anchor', 'old', 'context', 'handle', 'line', 'position', 'replace', 'expect', 'near', 'eol', 'verify'],
90
90
  bool: ['pretty', 'dry-run', 'plan', 'replace-all', 'require-observation'],
91
91
  },
92
92
  write: {
93
- value: ['cwd', 'file', 'content', 'eol'],
93
+ value: ['cwd', 'file', 'content', 'eol', 'verify'],
94
94
  bool: ['pretty', 'dry-run', 'plan', 'no-overwrite', 'allow-unobserved'],
95
95
  },
96
96
  doctor: { value: ['cwd'], bool: ['pretty', 'json'] },
@@ -166,9 +166,9 @@ const USAGE = [
166
166
  // `--file` 指向**目录**时直接给一张仓库地图(模块/目录/依赖方向),不需要额外旗标。
167
167
  ' (--file 传目录 ⇒ 仓库地图:模块清单 + 目录骨架 + 被依赖最多的模块 + 入口点)',
168
168
  ' aura basetools read --targets <json|@文件> # 一次读多个目标(≤32 项;与 --file 互斥)',
169
- ' aura basetools edit --targets <json|@文件> [--dry-run|--plan] # 一次改多个文件(先全量规划,任一失败则零写入)',
169
+ ' aura basetools edit --targets <json|@文件> [--dry-run|--plan] [--verify "命令"] # 一次改多个文件(先全量规划,任一失败则零写入)',
170
170
  " aura basetools edit --file <路径> --create <内容|@文件> # 建文件(不必再给 --hunks)",
171
- " aura basetools edit --file <路径> (--hunks '<json>' | --hunks-file <路径>) [--dry-run|--plan] [--eol auto|preserve|lf|crlf] [--require-observation]",
171
+ " aura basetools edit --file <路径> (--hunks '<json>' | --hunks-file <路径>) [--dry-run|--plan] [--verify \"命令\"] [--eol auto|preserve|lf|crlf] [--require-observation]",
172
172
  ' aura basetools edit --file <路径> --lines <a,b> --replace <文本> [--expect <文本>] [--near <n>]',
173
173
  ' aura basetools edit --file <路径> --old <原文> --replace <文本> --replace-all # 改全部非重叠出现',
174
174
  " (只改一处时不必包成 JSON:--target / --lines / --start-line+--end-line / --line-cue / --anchor 任选其一)",
@@ -178,7 +178,7 @@ const USAGE = [
178
178
  ' aura basetools edit --file <路径> --handle <h…> --position before|after --replace <文本> # 在区间边界插入,区间本身不动',
179
179
  " (--line / --position 是从属键:只有与 --handle 同时给才成立,单独给不构成一次编辑)",
180
180
  ' aura basetools mcp 启动 MCP stdio 服务',
181
- ' aura basetools write --file <路径> --content <文本|@文件> [--no-overwrite] [--dry-run|--plan] [--allow-unobserved] [--eol auto|preserve|lf|crlf]',
181
+ ' aura basetools write --file <路径> --content <文本|@文件> [--no-overwrite] [--dry-run|--plan] [--verify "命令"] [--allow-unobserved] [--eol auto|preserve|lf|crlf]',
182
182
  ' (覆写默认要求先读过这一版;--allow-unobserved 才允许盲写)',
183
183
  ' aura basetools doctor [--json] 自检:Node/版本/MCP 握手/缓存与锁目录可写',
184
184
  ' aura basetools capabilities [--pretty] 能力清单:读模式/地址档/语言/限制(JSON)',
@@ -352,6 +352,7 @@ if (command === 'write') {
352
352
  noOverwrite: has('no-overwrite'),
353
353
  allowUnobserved: has('allow-unobserved'),
354
354
  ...(typeof eol === 'string' ? { eol } : {}),
355
+ ...(typeof flag('verify') === 'string' ? { verify: flag('verify') } : {}),
355
356
  })
356
357
  emit(env, env.ok ? 0 : 2)
357
358
  }
@@ -473,6 +474,7 @@ if (command === 'edit') {
473
474
  targets: spec.targets,
474
475
  dryRun: has('dry-run') || has('plan') || spec.dryRun,
475
476
  ...(has('require-observation') ? { requireObservation: true } : {}),
477
+ ...(typeof flag('verify') === 'string' ? { verify: flag('verify') } : {}),
476
478
  })
477
479
  emit(env, env.ok ? 0 : 2)
478
480
  }
@@ -585,6 +587,7 @@ if (command === 'edit') {
585
587
  dryRun: has('dry-run') || has('plan'),
586
588
  create,
587
589
  ...(has('require-observation') ? { requireObservation: true } : {}),
590
+ ...(typeof flag('verify') === 'string' ? { verify: flag('verify') } : {}),
588
591
  })
589
592
  emit(env, env.ok ? 0 : 2)
590
593
  }
package/lib/ops.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  * (`flatHunk` 也从这里出去),于是「引擎在哪」这个问题在整棵树里只有一个答案。
11
11
  */
12
12
  import fs from 'node:fs'
13
+ import { spawnSync } from 'node:child_process'
13
14
  import { fileURLToPath } from 'node:url'
14
15
  import { createNodeFsProvider, DEFAULT_MAX_TEXT_BYTES } from './fs-provider.mjs'
15
16
  import { withWorkspaceLock } from './workspace-lock.mjs'
@@ -91,7 +92,7 @@ export function capabilitiesEnvelope() {
91
92
  {
92
93
  name: 'aura_basetools_edit',
93
94
  addressKinds: [...new Set([...engineArgs.FLAT_ADDRESS_KEYS, 'old', 'context', 'position'])],
94
- options: ['replace_all', 'require_observation'],
95
+ options: ['replace_all', 'require_observation', 'verify'],
95
96
  writeSafety: {
96
97
  cli: ['workspace-lock', 'stale-region-gate', 'byte-verify'],
97
98
  mcp: ['workspace-lock', 'stale-region-gate', 'byte-verify'],
@@ -101,7 +102,7 @@ export function capabilitiesEnvelope() {
101
102
  {
102
103
  name: 'aura_basetools_write',
103
104
  operations: ['create', 'overwrite'],
104
- options: ['eol'],
105
+ options: ['eol', 'verify'],
105
106
  writeSafety: {
106
107
  cli: ['workspace-lock', 'stale-region-gate', 'observation-gate', 'byte-verify'],
107
108
  mcp: ['workspace-lock', 'stale-region-gate', 'observation-gate', 'byte-verify'],
@@ -479,6 +480,45 @@ function transactionError(error) {
479
480
  }
480
481
  }
481
482
 
483
+ const VERIFY_OUTPUT_LIMIT = 4000
484
+ const tailText = (value) => {
485
+ const text = String(value ?? '')
486
+ return text.length <= VERIFY_OUTPUT_LIMIT ? text : `…${text.slice(-VERIFY_OUTPUT_LIMIT)}`
487
+ }
488
+
489
+ /**
490
+ * 写后验证:**只在调用方显式给出命令时**执行。
491
+ *
492
+ * 不是自动发现仓库脚本,也不绕过沙箱/权限;命令在 workspace 根下跑,有超时与输出上限。
493
+ * 失败由外层事务回滚,调用方拿到的是可判定的 `PATCH_VERIFY_FAILED`,不是“写完再说”。
494
+ */
495
+ function runVerification(command, cwd) {
496
+ const timeout = Math.min(600000, Math.max(1000, Number(process.env.AURA_VERIFY_TIMEOUT_MS) || 120000))
497
+ const started = Date.now()
498
+ const res = spawnSync(command, {
499
+ cwd,
500
+ shell: true,
501
+ encoding: 'utf8',
502
+ timeout,
503
+ maxBuffer: 8 * 1024 * 1024,
504
+ env: process.env,
505
+ windowsHide: true,
506
+ })
507
+ const timedOut = res.error?.code === 'ETIMEDOUT'
508
+ return {
509
+ command,
510
+ cwd,
511
+ ok: res.status === 0 && res.error === undefined,
512
+ exitCode: res.status,
513
+ signal: res.signal ?? null,
514
+ timedOut,
515
+ durationMs: Date.now() - started,
516
+ stdout: tailText(res.stdout),
517
+ stderr: tailText(res.stderr),
518
+ ...(res.error === undefined ? {} : { error: String(res.error.message ?? res.error) }),
519
+ }
520
+ }
521
+
482
522
  /**
483
523
  * 写事务外壳:workspace 锁 + 事务日志 + 失败回滚。
484
524
  *
@@ -492,11 +532,34 @@ export async function editEnvelope(provider, request) {
492
532
  * 预演不建事务、不写盘,也不需要工作区锁;0.14.0 起事务日志只用于失败回滚。
493
533
  */
494
534
  if (request.dryRun === true) return editEnvelopeInner(provider, request)
535
+ const verifyCommand = typeof request.verify === 'string' && request.verify.trim().length > 0 ? request.verify.trim() : null
495
536
  try {
496
537
  return await withWorkspaceLock(provider.root, async () => {
497
538
  const tx = createTransaction(provider.root, 'aura_basetools_edit')
498
539
  const env = await editEnvelopeInner(tx.proxy(provider), request)
499
540
  if (env.ok) {
541
+ if (verifyCommand !== null) {
542
+ const verification = runVerification(verifyCommand, provider.root)
543
+ if (!verification.ok) {
544
+ const rollback = await tx.rollback(provider)
545
+ return {
546
+ ok: false,
547
+ error: `edit verification failed (exit ${verification.exitCode ?? 'no-status'}${verification.timedOut ? ', timed out' : ''}): ${verifyCommand}`,
548
+ code: 'PATCH_VERIFY_FAILED',
549
+ verification,
550
+ rollback,
551
+ }
552
+ }
553
+ try {
554
+ tx.commit()
555
+ } catch (error) {
556
+ const rollback = await tx.rollback(provider)
557
+ error.transaction = tx.summary()
558
+ error.rollback = rollback
559
+ throw error
560
+ }
561
+ return { ...env, verification }
562
+ }
500
563
  try {
501
564
  tx.commit()
502
565
  } catch (error) {
@@ -520,12 +583,37 @@ export async function editManyEnvelope(provider, request) {
520
583
  if (provider.root === undefined) return editManyEnvelopeInner(provider, request)
521
584
  // 与单目标同一条:预演不建事务、不写盘,不能顶掉真正的最近 committed 事务。
522
585
  if (request.dryRun === true) return editManyEnvelopeInner(provider, request)
586
+ const verifyCommand = typeof request.verify === 'string' && request.verify.trim().length > 0 ? request.verify.trim() : null
523
587
  try {
524
588
  return await withWorkspaceLock(provider.root, async () => {
525
589
  const tx = createTransaction(provider.root, 'aura_basetools_edit')
526
590
  try {
527
591
  const batch = await editManyEnvelopeInner(tx.proxy(provider), request)
528
592
  if (batch.ok) {
593
+ if (verifyCommand !== null) {
594
+ const verification = runVerification(verifyCommand, provider.root)
595
+ if (!verification.ok) {
596
+ const rollback = await tx.rollback(provider)
597
+ const finalBatch = writePath.rebuildAfterRollback(batch, rollback)
598
+ return {
599
+ ...finalBatch,
600
+ ok: false,
601
+ code: 'PATCH_VERIFY_FAILED',
602
+ error: `edit verification failed (exit ${verification.exitCode ?? 'no-status'}${verification.timedOut ? ', timed out' : ''}): ${verifyCommand}`,
603
+ verification,
604
+ rollback,
605
+ }
606
+ }
607
+ try {
608
+ tx.commit()
609
+ } catch (error) {
610
+ const rollback = await tx.rollback(provider)
611
+ error.transaction = tx.summary()
612
+ error.rollback = rollback
613
+ throw error
614
+ }
615
+ return { ...batch, verification }
616
+ }
529
617
  try {
530
618
  tx.commit()
531
619
  } catch (error) {
@@ -567,6 +655,7 @@ function compactWriteReceipt(env, content) {
567
655
  const bytes = result.write_verify?.bytes ?? (dryRun && result.operation === 'create' ? Buffer.byteLength(content, 'utf8') : undefined)
568
656
  return {
569
657
  ok: true,
658
+ ...(env.verification === undefined ? {} : { verification: env.verification }),
570
659
  result: {
571
660
  path: result.path,
572
661
  operation: result.operation ?? 'write',
@@ -594,7 +683,7 @@ function compactWriteReceipt(env, content) {
594
683
  * `preserve` 按 content 原样写(与内置 Write 的“给什么写什么”对齐),`lf` / `crlf` 强制统一。
595
684
  * 新建文件永远按 content 原样写;显式传非 `auto` 的 eol 会被拒绝(不做静默忽略)。
596
685
  */
597
- export async function writeEnvelope(provider, { file, content, dryRun = false, noOverwrite = false, allowUnobserved = false, eol = 'auto' } = {}) {
686
+ export async function writeEnvelope(provider, { file, content, dryRun = false, noOverwrite = false, allowUnobserved = false, eol = 'auto', verify } = {}) {
598
687
  if (typeof file !== 'string' || file.length === 0) {
599
688
  return { ok: false, error: 'write needs a file path', code: 'WRITE_BAD_ARGS' }
600
689
  }
@@ -621,7 +710,7 @@ export async function writeEnvelope(provider, { file, content, dryRun = false, n
621
710
  if (dryRun) {
622
711
  return { ok: true, result: { path: displayPath, operation: 'create', bytes: Buffer.byteLength(content, 'utf8'), dryRun: true } }
623
712
  }
624
- return compactWriteReceipt(await editEnvelope(provider, { file, create: content }), content)
713
+ return compactWriteReceipt(await editEnvelope(provider, { file, create: content, verify }), content)
625
714
  }
626
715
  return compactWriteReceipt(
627
716
  await editEnvelope(provider, {
@@ -632,6 +721,7 @@ export async function writeEnvelope(provider, { file, content, dryRun = false, n
632
721
  requireObservation: allowUnobserved !== true,
633
722
  skipStaleGate: allowUnobserved === true,
634
723
  toolName: 'aura_basetools_write',
724
+ verify,
635
725
  }),
636
726
  content,
637
727
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aura-basetools",
3
- "version": "0.14.0",
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",