deadbyte-mcp 0.15.22 → 0.15.24

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.
Files changed (59) hide show
  1. package/MANIFEST.SHA256 +58 -36
  2. package/PROVENANCE.json +11 -10
  3. package/README.txt +18 -7
  4. package/RELEASE.json +26 -39
  5. package/SBOM.cdx.json +3 -3
  6. package/TEST-MATRIX.json +32 -86
  7. package/package.json +4 -3
  8. package/proof/commander-closure-20260928/DIFFERENTIAL-CHECKPOINT.json +111 -0
  9. package/scripts/activate-skill-creator-1.1.1.mjs +46 -0
  10. package/scripts/build-advanced-pcb-skill.mjs +208 -0
  11. package/scripts/commander-real-performance-benchmark-r47.mjs +12 -3
  12. package/scripts/diagnose-skill-tree.mjs +8 -0
  13. package/scripts/npm-release-r48.mjs +252 -0
  14. package/scripts/pcb-skill-src-SKILL.md.txt +67 -0
  15. package/scripts/pcb-skill-src-bringup-evidence.md.txt +22 -0
  16. package/scripts/pcb-skill-src-constraint-model.md.txt +17 -0
  17. package/scripts/pcb-skill-src-emc-mechanical-dfm.md.txt +17 -0
  18. package/scripts/pcb-skill-src-high-speed-layout.md.txt +27 -0
  19. package/scripts/pcb-skill-src-kicad-native.md.txt +30 -0
  20. package/scripts/pcb-skill-src-kicad_pcb_probe.py.txt +159 -0
  21. package/scripts/pcb-skill-src-pcb_constraint_gate.py.txt +76 -0
  22. package/scripts/pcb-skill-src-power-integrity.md.txt +15 -0
  23. package/scripts/prepare-skill-creator-1.1.1.mjs +113 -0
  24. package/scripts/test-skill-creator-semver-matrix.mjs +30 -0
  25. package/scripts/test-skill-creator-semver.mjs +22 -0
  26. package/src/atomic-mutation.mjs +12 -4
  27. package/src/coding-mcp-tools.mjs +15 -1
  28. package/src/coding-output-contracts.mjs +9 -3
  29. package/src/commander-office-reader-r48.mjs +36 -3
  30. package/src/commander-pdf-r48.mjs +36 -1
  31. package/src/commander-wire-adapter-r47.mjs +56 -2
  32. package/src/deadbyte-cli.mjs +70 -15
  33. package/src/host-plane.mjs +10 -31
  34. package/src/machine-fs-mcp-tools.mjs +7 -1
  35. package/src/machine-fs-output-contracts.mjs +6 -0
  36. package/src/mcp-server.mjs +14 -2
  37. package/src/output-contracts.mjs +6 -4
  38. package/src/remote-live-log.mjs +189 -35
  39. package/src/stdio-protocol-guard.mjs +71 -0
  40. package/src/tool-audit.mjs +1 -1
  41. package/src/version.mjs +1 -1
  42. package/test/atomic-mutation.test.mjs +61 -0
  43. package/test/coding-mcp-unknown-effect.test.mjs +42 -0
  44. package/test/commander-biff-reference-r48.test.mjs +38 -0
  45. package/test/commander-core-bridge-r47.test.mjs +19 -0
  46. package/test/commander-office-reader-r48.test.mjs +18 -2
  47. package/test/commander-pdf-r48.test.mjs +35 -0
  48. package/test/commander-protocol-adversarial-r48.test.mjs +166 -0
  49. package/test/commander-real-parity-r47.test.mjs +37 -3
  50. package/test/commander-schema-differential-r48.test.mjs +1 -1
  51. package/test/commander-wire-contract-r47.test.mjs +32 -0
  52. package/test/full-max-catalog-r48.test.mjs +1 -1
  53. package/test/machine-fs-mcp-tools.test.mjs +12 -0
  54. package/test/output-schema.test.mjs +20 -1
  55. package/test/r48-release-contract.test.mjs +27 -21
  56. package/test/remote-live-log-r37.test.mjs +106 -7
  57. package/test/remote-session-cli.test.mjs +40 -0
  58. package/test/tool-audit.test.mjs +17 -0
  59. package/test/tunnel-stdio-compat.test.mjs +3 -1
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ import json, sys
3
+
4
+ def load(path):
5
+ with open(path, "r", encoding="utf-8") as f:
6
+ return json.load(f)
7
+
8
+ def main():
9
+ if len(sys.argv) != 3:
10
+ raise SystemExit("usage: pcb_constraint_gate.py board-probe.json board-contract.json")
11
+ probe = load(sys.argv[1])
12
+ contract = load(sys.argv[2])
13
+ if probe.get("schema") != "deadbyte.kicad-pcb-probe.v1":
14
+ raise SystemExit("unsupported probe schema")
15
+
16
+ checks = []
17
+ def add(rule, ok, observed, required):
18
+ checks.append({"rule": rule, "ok": bool(ok), "observed": observed, "required": required})
19
+
20
+ if "required_layers" in contract:
21
+ observed = set(x.get("name") for x in probe.get("layers", []))
22
+ req = list(contract["required_layers"])
23
+ add("required_layers", all(x in observed for x in req), sorted(observed), req)
24
+
25
+ if "min_copper_layers" in contract:
26
+ observed = probe.get("copper_layer_count")
27
+ req = int(contract["min_copper_layers"])
28
+ add("min_copper_layers", isinstance(observed, int) and observed >= req, observed, req)
29
+
30
+ for key, probe_key in [
31
+ ("min_track_width_mm", "track_width_mm"),
32
+ ("min_via_diameter_mm", "via_diameter_mm"),
33
+ ("min_via_drill_mm", "via_drill_mm"),
34
+ ]:
35
+ if key in contract:
36
+ observed = (probe.get(probe_key) or {}).get("min")
37
+ req = float(contract[key])
38
+ add(key, observed is not None and float(observed) >= req, observed, req)
39
+
40
+ if "required_nets" in contract:
41
+ observed = set(probe.get("nets", []))
42
+ req = list(contract["required_nets"])
43
+ add("required_nets", all(x in observed for x in req), sorted(observed), req)
44
+
45
+ for key, count_key in [
46
+ ("min_footprints", "footprints"),
47
+ ("min_segments", "segments"),
48
+ ("min_vias", "vias"),
49
+ ("min_zones", "zones"),
50
+ ]:
51
+ if key in contract:
52
+ observed = (probe.get("counts") or {}).get(count_key)
53
+ req = int(contract[key])
54
+ add(key, isinstance(observed, int) and observed >= req, observed, req)
55
+
56
+ allowed = {
57
+ "required_layers", "min_copper_layers", "min_track_width_mm",
58
+ "min_via_diameter_mm", "min_via_drill_mm", "required_nets",
59
+ "min_footprints", "min_segments", "min_vias", "min_zones"
60
+ }
61
+ unknown = sorted(set(contract) - allowed)
62
+ if unknown:
63
+ checks.append({"rule": "unknown_contract_keys", "ok": False, "observed": unknown, "required": "no unsupported keys"})
64
+
65
+ passed = all(x["ok"] for x in checks)
66
+ out = {
67
+ "schema": "deadbyte.pcb-constraint-gate.v1",
68
+ "status": "PASS" if passed else "FAIL",
69
+ "probe_sha256": probe.get("sha256"),
70
+ "checks": checks
71
+ }
72
+ print(json.dumps(out, indent=2, sort_keys=True))
73
+ raise SystemExit(0 if passed else 2)
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,15 @@
1
+ # Power Integrity and Thermal
2
+
3
+ Power integrity begins with current loops, not capacitor count.
4
+
5
+ For each regulator trace the input current path, keep the high di/dt input loop compact, bound switch-node copper, place bootstrap/gate-drive parts by topology, protect the feedback sense path, close the output-current loop, and provide a credible thermal path.
6
+
7
+ For decoupling separate bulk-energy storage from local high-frequency charge. Account for capacitor mounting inductance, vias, planes, and package inductance. Printed capacitance alone does not prove useful impedance at frequency.
8
+
9
+ For planes and pours verify neck-downs at pads, vias, connectors, and thermal spokes; identify slots caused by keepouts and antipads; preserve return continuity; and check both DC drop and transient current paths.
10
+
11
+ For sensitive analog control where digital and switching currents return. Do not split ground reflexively. Placement, loop control, filtering, and current-path ownership matter more than labels.
12
+
13
+ Thermal claims require dissipation, package thermal path, copper geometry, ambient, airflow, and enclosure assumptions. Missing inputs mean thermal margin UNKNOWN.
14
+
15
+ Bench evidence should include rail voltage, ripple/noise with correct probing, startup sequencing, relevant load transient behavior, and regulator temperature under representative load.
@@ -0,0 +1,113 @@
1
+ import { readFile, writeFile, mkdir, rm, cp } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+ import { createHash, createPrivateKey, createPublicKey, sign } from "node:crypto";
7
+ import { createSkillPlane } from "../src/skill-plane-r44.mjs";
8
+
9
+ const ex = promisify(execFile);
10
+ const home = os.homedir();
11
+ const stateDir = path.join(home, ".deadbyte-mcp");
12
+ const skillsRoot = path.join(stateDir, "skills");
13
+ const active = path.join(skillsRoot, "skill-creator");
14
+ const expectedOldManifest = "f34b3953db176c8ae9a5e08e7702b39f12b60b013edcd2f4d63a6268af9ba432";
15
+ const oldEnv = JSON.parse(await readFile(path.join(active, "skill.json"), "utf8"));
16
+ if (oldEnv.manifest_sha256 !== expectedOldManifest) {
17
+ throw new Error("unexpected active skill-creator manifest: " + oldEnv.manifest_sha256);
18
+ }
19
+ if (oldEnv.body.version !== "1.1.0") throw new Error("expected active skill-creator 1.1.0");
20
+
21
+ const history = path.join(stateDir, "skill-history", "skill-creator", "1.1.0-" + expectedOldManifest);
22
+ await mkdir(path.dirname(history), {recursive:true});
23
+ try {
24
+ const prior = JSON.parse(await readFile(path.join(history, "skill.json"), "utf8"));
25
+ if (prior.manifest_sha256 !== expectedOldManifest) throw new Error("history path collision");
26
+ } catch (e) {
27
+ if (e.code !== "ENOENT") throw e;
28
+ await cp(active, history, {recursive:true, errorOnExist:true});
29
+ }
30
+ const archived = JSON.parse(await readFile(path.join(history, "skill.json"), "utf8"));
31
+ if (archived.manifest_sha256 !== expectedOldManifest) throw new Error("archive manifest mismatch");
32
+
33
+ const stageParent = path.join(stateDir, "skill-staging", "skill-creator-1.1.1");
34
+ const stage = path.join(stageParent, "skill-creator");
35
+ await rm(stageParent, {recursive:true, force:true});
36
+ await mkdir(stageParent, {recursive:true});
37
+ await cp(active, stage, {recursive:true});
38
+ await rm(path.join(stage, "references", "skills_cache.json"), {force:true});
39
+
40
+ const createPath = path.join(stage, "scripts", "create-skill.mjs");
41
+ const oldCreate = await readFile(createPath, "utf8");
42
+ const oldRegex = "const VERSION=/^[0-9]+\\\\.[0-9]+\\\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;";
43
+ const newRegex = "const VERSION=/^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;";
44
+ if (!oldCreate.includes(oldRegex)) throw new Error("expected defective VERSION regex not found");
45
+ if (oldCreate.split(oldRegex).length !== 2) throw new Error("VERSION regex occurrence count != 1");
46
+ await writeFile(createPath, oldCreate.replace(oldRegex, newRegex), "utf8");
47
+
48
+ const skillPath = path.join(stage, "SKILL.md");
49
+ const oldSkill = await readFile(skillPath, "utf8");
50
+ const versionLine = "version: 1.1.0";
51
+ if (!oldSkill.includes(versionLine) || oldSkill.split(versionLine).length !== 2) {
52
+ throw new Error("SKILL.md version occurrence count != 1");
53
+ }
54
+ await writeFile(skillPath, oldSkill.replace(versionLine, "version: 1.1.1"), "utf8");
55
+
56
+ const sha = b => createHash("sha256").update(b).digest("hex");
57
+ const canonical = v => Array.isArray(v) ? v.map(canonical) : v && typeof v === "object"
58
+ ? Object.fromEntries(Object.keys(v).sort((a,b)=>a.localeCompare(b,"en")).map(k=>[k,canonical(v[k])]))
59
+ : v;
60
+
61
+ const body = structuredClone(oldEnv.body);
62
+ body.version = "1.1.1";
63
+ body.skill_md_sha256 = sha(await readFile(skillPath));
64
+ body.provenance = {
65
+ ...body.provenance,
66
+ source_revision: "r47.1-semver-regex-repair-2026-09-27"
67
+ };
68
+ for (const resource of body.resources) {
69
+ resource.sha256 = sha(await readFile(path.join(stage, ...resource.path.split("/"))));
70
+ }
71
+ const bodyBytes = Buffer.from(JSON.stringify(canonical(body)), "utf8");
72
+ const body_sha256 = sha(bodyBytes);
73
+ const priv = createPrivateKey(await readFile(path.join(stateDir, "trust", "ed25519-private.pem")));
74
+ const pub = createPublicKey(priv);
75
+ const keyId = sha(pub.export({type:"spki", format:"der"}));
76
+ const attestation = {
77
+ algorithm: "ed25519",
78
+ domain: "DEADBYTE-SKILL-MANIFEST-V1",
79
+ key_id: keyId,
80
+ signature_base64: sign(null, Buffer.concat([Buffer.from("DEADBYTE-SKILL-MANIFEST-V1\0", "utf8"), bodyBytes]), priv).toString("base64")
81
+ };
82
+ const unsigned = {body, body_sha256, attestation};
83
+ const envelope = {
84
+ ...unsigned,
85
+ manifest_sha256: sha(Buffer.from(JSON.stringify(canonical(unsigned)), "utf8"))
86
+ };
87
+ await writeFile(path.join(stage, "skill.json"), JSON.stringify(envelope, null, 2), "utf8");
88
+
89
+ const validator = await ex(process.execPath, [path.join(stage, "scripts", "validate-skill.mjs"), stage], {
90
+ timeout: 30000,
91
+ maxBuffer: 1024 * 1024
92
+ });
93
+
94
+ const plane = await createSkillPlane({
95
+ root: stageParent,
96
+ verifyKeyPath: path.join(stateDir, "trust", "ed25519-public.pem")
97
+ });
98
+ const loaded = await plane.load({name:"skill-creator"});
99
+ if (!loaded.verified || loaded.version !== "1.1.1") throw new Error("staging Skill Plane verification failed");
100
+
101
+ console.log(JSON.stringify({
102
+ status:"STAGED",
103
+ active_manifest:oldEnv.manifest_sha256,
104
+ archived_path:history,
105
+ archived_manifest:archived.manifest_sha256,
106
+ stage_parent:stageParent,
107
+ stage_skill:stage,
108
+ candidate_manifest:envelope.manifest_sha256,
109
+ candidate_body_sha256:body_sha256,
110
+ verify_key_id:keyId,
111
+ validator:validator.stdout.trim(),
112
+ skill_plane_verified:loaded.verified
113
+ }, null, 2));
@@ -0,0 +1,30 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+ const ex=promisify(execFile);
7
+ const creator=process.argv[2];
8
+ if(!creator) throw new Error("creator path required");
9
+ const valid=["1.0.0","0.0.1","1.1.1","12.34.56","1.0.0-rc.1"];
10
+ const invalid=["1.0","v1.0.0","1.a.0","",String.fromCharCode(49,92,97,48,92,98,48)];
11
+ const results=[];
12
+ for(const version of valid){
13
+ const tmp=await mkdtemp(path.join(os.tmpdir(),"deadbyte-semver-valid-"));
14
+ try{
15
+ await ex(process.execPath,[creator,tmp,"fixture",version],{timeout:30000,maxBuffer:1048576});
16
+ results.push({version,expected:"PASS",observed:"PASS"});
17
+ }finally{await rm(tmp,{recursive:true,force:true});}
18
+ }
19
+ for(const version of invalid){
20
+ const tmp=await mkdtemp(path.join(os.tmpdir(),"deadbyte-semver-invalid-"));
21
+ let rejected=false;
22
+ try{
23
+ await ex(process.execPath,[creator,tmp,"fixture",version],{timeout:30000,maxBuffer:1048576});
24
+ }catch(e){
25
+ if(e.code===1 && String(e.stderr).includes("invalid version")) rejected=true;
26
+ }finally{await rm(tmp,{recursive:true,force:true});}
27
+ if(!rejected) throw new Error("invalid version accepted: "+JSON.stringify(version));
28
+ results.push({version,expected:"FAIL",observed:"FAIL"});
29
+ }
30
+ console.log(JSON.stringify({status:"PASS",cases:results},null,2));
@@ -0,0 +1,22 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+
7
+ const ex = promisify(execFile);
8
+ const creator = process.argv[2];
9
+ if (!creator) throw new Error("usage: node test-skill-creator-semver.mjs <create-skill.mjs>");
10
+ const tmp = await mkdtemp(path.join(os.tmpdir(), "deadbyte-skill-creator-semver-"));
11
+ try {
12
+ const r = await ex(process.execPath, [creator, tmp, "semver-regression-fixture", "1.0.0"], {
13
+ timeout: 30000,
14
+ maxBuffer: 1024 * 1024
15
+ });
16
+ if (!r.stdout.includes("semver-regression-fixture")) {
17
+ throw new Error("create-skill returned success without fixture path");
18
+ }
19
+ console.log(JSON.stringify({status:"PASS", version:"1.0.0", stdout:r.stdout.trim()}));
20
+ } finally {
21
+ await rm(tmp, {recursive:true, force:true});
22
+ }
@@ -74,11 +74,19 @@ export async function atomicMutation({
74
74
  throw error;
75
75
  }
76
76
 
77
- const after = await readState(target);
78
77
  const expectedAfter = sha256Bytes(bytes);
79
- if (!after.exists || after.sha256 !== expectedAfter) throw new Error('atomic mutation post-commit hash mismatch');
80
- if (verifyPostcondition) await verifyPostcondition({ before, after, expected_after_sha256: expectedAfter });
81
- return { before, after, expected_after_sha256: expectedAfter };
78
+ try {
79
+ const after = await readState(target);
80
+ if (!after.exists || after.sha256 !== expectedAfter) throw new Error('atomic mutation post-commit hash mismatch');
81
+ if (verifyPostcondition) await verifyPostcondition({ before, after, expected_after_sha256: expectedAfter });
82
+ return { before, after, expected_after_sha256: expectedAfter };
83
+ } catch (error) {
84
+ const failure = error instanceof Error ? error : new Error(String(error));
85
+ failure.outcome = 'FAILED_AFTER_UNKNOWN_EFFECT';
86
+ failure.side_effect_state = 'unknown';
87
+ failure.expected_after_sha256 = expectedAfter;
88
+ throw failure;
89
+ }
82
90
  } finally {
83
91
  await releaseLock();
84
92
  }
@@ -46,7 +46,21 @@ const taskStep = z.union([
46
46
  ]);
47
47
 
48
48
  function result(schema, value, isError = false) { return codingStructuredResult(schema, value, isError); }
49
- function fail(schema, error) { return result(schema, { status: 'failed', error: String(error) }, true); }
49
+ function fail(schema, error) {
50
+ const base = { status: 'failed', error: String(error) };
51
+ if (error?.outcome === 'FAILED_AFTER_UNKNOWN_EFFECT'
52
+ && error?.side_effect_state === 'unknown'
53
+ && HEX64.test(error?.expected_after_sha256 ?? '')) {
54
+ const enriched = {
55
+ ...base,
56
+ outcome: error.outcome,
57
+ side_effect_state: error.side_effect_state,
58
+ expected_after_sha256: error.expected_after_sha256
59
+ };
60
+ if (schema.safeParse(enriched).success) return result(schema, enriched, true);
61
+ }
62
+ return result(schema, base, true);
63
+ }
50
64
  export function registerCodingTools(server, { codingRuntime, taskRuntime, semanticRuntime }) {
51
65
  if (!codingRuntime || !taskRuntime || !semanticRuntime) throw new Error('coding tool registration requires runtimes');
52
66
 
@@ -168,7 +168,9 @@ export const codingSymbolReplaceOutputSchema = statusContract({
168
168
  shape: { root_id: z.string().min(1).max(64).optional(), path: z.string().min(1).max(512).optional(), previous_sha256: hex64.optional(),
169
169
  sha256: hex64.optional(), bytes: z.number().int().min(0).optional(), replacements: z.literal(1).optional(),
170
170
  receipt_id: receiptId.optional(), receipt_sha256: hex64.optional(), request_sha256: hex64.optional(),
171
- symbol_id: hex64.optional(), symbol_name: z.string().min(1).max(256).optional(), engine: z.string().min(1).max(64).optional() },
171
+ symbol_id: hex64.optional(), symbol_name: z.string().min(1).max(256).optional(), engine: z.string().min(1).max(64).optional(),
172
+ outcome: z.literal('FAILED_AFTER_UNKNOWN_EFFECT').optional(), side_effect_state: z.literal('unknown').optional(),
173
+ expected_after_sha256: hex64.optional() },
172
174
  successStatuses: ['committed'], successRequired: ['root_id','path','previous_sha256','sha256','bytes','replacements','receipt_id','receipt_sha256','request_sha256','symbol_id','symbol_name','engine']
173
175
  });
174
176
 
@@ -189,14 +191,18 @@ export const codingFileWriteOutputSchema = statusContract({
189
191
  statuses: ['committed', 'failed'],
190
192
  shape: { root_id: z.string().min(1).max(64).optional(), path: z.string().min(1).max(512).optional(), previous_sha256: hex64.nullable().optional(),
191
193
  sha256: hex64.optional(), bytes: z.number().int().min(0).optional(), created: z.boolean().optional(), receipt_id: receiptId.optional(),
192
- receipt_sha256: hex64.optional(), request_sha256: hex64.optional() },
194
+ receipt_sha256: hex64.optional(), request_sha256: hex64.optional(),
195
+ outcome: z.literal('FAILED_AFTER_UNKNOWN_EFFECT').optional(), side_effect_state: z.literal('unknown').optional(),
196
+ expected_after_sha256: hex64.optional() },
193
197
  successStatuses: ['committed'], successRequired: ['root_id', 'path', 'previous_sha256', 'sha256', 'bytes', 'created', 'receipt_id', 'receipt_sha256', 'request_sha256']
194
198
  });
195
199
  export const codingTextReplaceOutputSchema = statusContract({
196
200
  statuses: ['committed', 'failed'],
197
201
  shape: { root_id: z.string().min(1).max(64).optional(), path: z.string().min(1).max(512).optional(), previous_sha256: hex64.optional(),
198
202
  sha256: hex64.optional(), bytes: z.number().int().min(0).optional(), replacements: z.number().int().min(1).max(64).optional(),
199
- receipt_id: receiptId.optional(), receipt_sha256: hex64.optional(), request_sha256: hex64.optional() },
203
+ receipt_id: receiptId.optional(), receipt_sha256: hex64.optional(), request_sha256: hex64.optional(),
204
+ outcome: z.literal('FAILED_AFTER_UNKNOWN_EFFECT').optional(), side_effect_state: z.literal('unknown').optional(),
205
+ expected_after_sha256: hex64.optional() },
200
206
  successStatuses: ['committed'], successRequired: ['root_id', 'path', 'previous_sha256', 'sha256', 'bytes', 'replacements', 'receipt_id', 'receipt_sha256', 'request_sha256']
201
207
  });
202
208
  export const codingCommandRunOutputSchema = statusContract({
@@ -140,7 +140,7 @@ function prettyXml(xml){
140
140
  }
141
141
  return lines.join('\n');
142
142
  }
143
- function docxOutline(xml){
143
+ function docxOutline(xml,files){
144
144
  const body=xml.match(/<w:body\b[^>]*>([\s\S]*?)<\/w:body>/)?.[1];
145
145
  if(body===undefined) return '[No w:body found in document.xml]';
146
146
  const elements=[...body.matchAll(/<w:(p|tbl)\b[\s\S]*?<\/w:\1>/g)].map(m=>({kind:m[1],xml:m[0]}));
@@ -152,6 +152,8 @@ function docxOutline(xml){
152
152
  let line=`[${i}] w:p`;
153
153
  if(style) line+=` style="${xmlDecode(style)}"`;
154
154
  const value=wordText(item.xml);
155
+ const hasDrawing=item.xml.includes('<w:drawing')||item.xml.includes('<mc:AlternateContent');
156
+ if(hasDrawing) line+=value?' (+ drawing/image)':` [drawing/image, ${(Buffer.byteLength(item.xml)/1024).toFixed(1)}KB]`;
155
157
  if(value) line+='\n '+value;
156
158
  lines.push(line);
157
159
  }else{
@@ -163,7 +165,15 @@ function docxOutline(xml){
163
165
  }
164
166
  }
165
167
  }
166
- return lines.join('\n');
168
+ const headersFooters=[];
169
+ for(const [name,bytes] of [...files.entries()].sort(([a],[b])=>a.localeCompare(b))){
170
+ if(!/^word\/(?:header|footer)[^/]*\.xml$/i.test(name)) continue;
171
+ try{
172
+ const value=wordText(decodeUtf8(bytes,name));
173
+ headersFooters.push(`${name.slice('word/'.length)}: ${value?`"${value.length>100?value.slice(0,100)+'...':value}"`:'(no text content)'}`);
174
+ }catch{/* malformed optional part is omitted; document.xml remains authoritative */}
175
+ }
176
+ return lines.join('\n')+(headersFooters.length?`\n\nHeaders/Footers:\n${headersFooters.join('\n')}`:'');
167
177
  }
168
178
 
169
179
  export function readDocxBytes(input,{path:displayPath='document.docx',offset=0,length=1000}={}){
@@ -180,11 +190,34 @@ export function readDocxBytes(input,{path:displayPath='document.docx',offset=0,l
180
190
  return {content:`[DOCX XML: lines ${start}-${Math.max(start,start+sliced.length-1)} of ${totalLines} (${remaining} remaining)]\n${sliced.join('\n')}`,
181
191
  mimeType:'application/xml',metadata:{isDocx:true,lineCount:totalLines},outline:false};
182
192
  }
183
- const outline=docxOutline(xml);
193
+ const outline=docxOutline(xml,files);
184
194
  const rawSizeKB=(Buffer.byteLength(xml)/1024).toFixed(1);
185
195
  return {content:`${outline}\n\nRaw XML: ${totalLines} lines, ${rawSizeKB}KB.`,mimeType:'text/plain',metadata:{isDocx:true,lineCount:totalLines},outline:true};
186
196
  }
187
197
 
198
+ export function searchDocxTextBytes(input,{pattern,ignoreCase=true,maxResults=500}={}){
199
+ if(typeof pattern!=='string'||pattern.length<1||pattern.length>4096) throw new Error('DOCX search pattern invalid');
200
+ if(!Number.isInteger(maxResults)||maxResults<1||maxResults>5000) throw new Error('DOCX search maxResults invalid');
201
+ const files=unzipOffice(input),needle=ignoreCase?pattern.toLowerCase():pattern,results=[];
202
+ const names=[...files.keys()].filter(name=>name==='word/document.xml'||/^word\/(?:header|footer)[^/]*\.xml$/i.test(name)).sort((a,b)=>{
203
+ if(a==='word/document.xml')return -1;if(b==='word/document.xml')return 1;return a.localeCompare(b);
204
+ });
205
+ for(const name of names){
206
+ let xml;
207
+ try{xml=decodeUtf8(files.get(name),name);}catch{continue;}
208
+ let line=0;
209
+ for(const match of xml.matchAll(/<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g)){
210
+ const value=xmlDecode(match[1]??'');if(!value.trim())continue;line++;
211
+ const haystack=ignoreCase?value.toLowerCase():value,index=haystack.indexOf(needle);
212
+ if(index<0)continue;
213
+ const start=Math.max(0,index-50),end=Math.min(value.length,index+needle.length+50);
214
+ results.push({part:name,line,match:`${start>0?'...':''}${value.slice(start,end)}${end<value.length?'...':''}`});
215
+ if(results.length>=maxResults)return results;
216
+ }
217
+ }
218
+ return results;
219
+ }
220
+
188
221
 
189
222
  export function createDocxBytes(content){
190
223
  if(typeof content!=='string') throw new Error('DOCX content must be string');
@@ -110,6 +110,37 @@ export function parseDeadbytePdf(input){
110
110
  const MAX_GENERIC_OBJECTS=10000;
111
111
  const MAX_GENERIC_STREAM_BYTES=16*1024*1024;
112
112
 
113
+ function classicIncrementalObjectOffsets(text){
114
+ const starts=[...text.matchAll(/startxref\s+(\d+)\s+%%EOF/g)];
115
+ if(starts.length<2)return new Map();
116
+ const offsets=new Map(),visited=new Set();
117
+ let cursor=Number(starts.at(-1)[1]);
118
+ while(Number.isSafeInteger(cursor)&&cursor>=0&&!visited.has(cursor)){
119
+ visited.add(cursor);
120
+ if(text.slice(cursor,cursor+4)!=='xref')return new Map();
121
+ const trailerAt=text.indexOf('trailer',cursor+4);
122
+ if(trailerAt<0)return new Map();
123
+ const lines=text.slice(cursor+4,trailerAt).trim().split(/\r?\n/);
124
+ for(let i=0;i<lines.length;){
125
+ const header=lines[i++].trim().match(/^(\d+)\s+(\d+)$/);if(!header)return new Map();
126
+ const first=Number(header[1]),count=Number(header[2]);
127
+ if(!Number.isSafeInteger(first)||!Number.isSafeInteger(count)||count<0||count>MAX_GENERIC_OBJECTS)return new Map();
128
+ for(let n=0;n<count;n++){
129
+ const entry=lines[i++]?.trim().match(/^(\d{10})\s+(\d{5})\s+([nf])$/);if(!entry)return new Map();
130
+ if(entry[3]==='n'){
131
+ const key=`${first+n} ${Number(entry[2])}`;
132
+ const list=offsets.get(key)??[];list.push(Number(entry[1]));offsets.set(key,list);
133
+ }
134
+ }
135
+ }
136
+ const trailerEnd=text.indexOf('startxref',trailerAt+7);if(trailerEnd<0)return new Map();
137
+ const prev=text.slice(trailerAt+7,trailerEnd).match(/\/Prev\s+(\d+)/)?.[1];
138
+ if(prev===undefined)break;
139
+ const next=Number(prev);if(!Number.isSafeInteger(next)||next>=cursor)return new Map();cursor=next;
140
+ }
141
+ return offsets;
142
+ }
143
+
113
144
  function decodePdfTextBytes(bytes){
114
145
  if(bytes.length>=2&&bytes[0]===0xfe&&bytes[1]===0xff){
115
146
  let out='';
@@ -186,6 +217,7 @@ function parseGenericPdfObjects(bytes){
186
217
  const text=bytes.toString('latin1');
187
218
  if(!text.startsWith('%PDF-')) throw new Error('invalid PDF header');
188
219
  if(/\/Encrypt\b/.test(text)) throw new Error('encrypted PDF is unsupported');
220
+ const incrementalOffsets=classicIncrementalObjectOffsets(text);
189
221
  const objects=new Map(),header=/(\d+)\s+(\d+)\s+obj\b/g;
190
222
  let match,count=0;
191
223
  while((match=header.exec(text))){
@@ -193,7 +225,10 @@ function parseGenericPdfObjects(bytes){
193
225
  const end=text.indexOf('endobj',header.lastIndex);
194
226
  if(end<0) throw new Error(`PDF object ${match[1]} ${match[2]} missing endobj`);
195
227
  const key=`${match[1]} ${match[2]}`;
196
- if(objects.has(key)) throw new Error(`PDF duplicate object ${key}`);
228
+ if(objects.has(key)){
229
+ const revisions=incrementalOffsets.get(key)??[];
230
+ if(!revisions.includes(objects.get(key).offset)||!revisions.includes(match.index)) throw new Error(`PDF duplicate object ${key}`);
231
+ }
197
232
  objects.set(key,{id:Number(match[1]),generation:Number(match[2]),body:text.slice(header.lastIndex,end),offset:match.index});
198
233
  header.lastIndex=end+6;
199
234
  }
@@ -2,8 +2,9 @@ import * as z from 'zod/v4';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { readCommanderUrl } from './commander-url-reader-r48.mjs';
5
- import { readDocxBytes, createDocxBytes, editDocxBytes, readXlsxBytes, editXlsxBytes, createXlsxBytes, appendXlsxBytes } from './commander-office-reader-r48.mjs';
5
+ import { readDocxBytes, searchDocxTextBytes, createDocxBytes, editDocxBytes, readXlsxBytes, editXlsxBytes, createXlsxBytes, appendXlsxBytes } from './commander-office-reader-r48.mjs';
6
6
  import { editDeadbytePdf, parseDeadbytePdf, readPdfText } from './commander-pdf-r48.mjs';
7
+ import { decodeUtf8Strict } from './text-policy.mjs';
7
8
 
8
9
  const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
9
10
  const RDC_PROCESS_READ_CHUNK=256*1024;
@@ -122,6 +123,19 @@ function absoluteFor(runtime,rootId,relativePath){
122
123
  function linesFrom(bytes){
123
124
  return Buffer.from(bytes,'base64').toString('utf8').replaceAll('\\r\\n','\\n').replaceAll('\\r','\\n').split('\\n');
124
125
  }
126
+ function isUnknownBinary(bytes){
127
+ if(bytes.includes(0)) return true;
128
+ try{decodeUtf8Strict(bytes);return false;}catch{return true;}
129
+ }
130
+ function directoryRows(entries,baseRelative){
131
+ const base=String(baseRelative??'.').replaceAll('\\','/').replace(/^\.\//,'').replace(/\/$/,'');
132
+ return entries.map(entry=>{
133
+ const raw=String(entry.path??'').replaceAll('\\','/').replace(/^\.\//,'');
134
+ const relative=base&&base!=='.'&&raw.startsWith(`${base}/`)?raw.slice(base.length+1):raw;
135
+ const display=process.platform==='win32'?relative.replaceAll('/','\\'):relative;
136
+ return `${(entry.kind??entry.type)==='directory'?'[DIR]':'[FILE]'} ${display}`;
137
+ });
138
+ }
125
139
  function rdcFileResult(pathname,data){
126
140
  return {
127
141
  path:pathname,content:data.content,mimeType:data.mime_type,
@@ -171,6 +185,17 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
171
185
  }
172
186
  if(!machineFsRuntime) throw new Error('machine filesystem runtime is disabled');
173
187
  const target=resolveRdcPath(machineFsRuntime,args.path);
188
+ if(typeof machineFsRuntime.stat==='function'){
189
+ const info=await machineFsRuntime.stat({rootId:target.rootId,relativePath:target.relativePath,hash:false});
190
+ if(info?.kind==='directory'){
191
+ const listing=await listDirectory({path:args.path,depth:2});
192
+ const entries=listing.structuredContent?.entries??[];
193
+ const rows=directoryRows(entries,target.relativePath);
194
+ const content=`This is a directory, not a file. Use the list_directory tool instead of read_file for directories.\n\n${rows.join('\n')}`;
195
+ return text(content,false,{path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,isDirectory:true,
196
+ entries,truncated:listing.structuredContent?.truncated===true,entryCount:entries.length});
197
+ }
198
+ }
174
199
  const ext=path.extname(args.path).toLowerCase();
175
200
  const richDocument=ext==='.docx'||ext==='.xlsx'||ext==='.xlsm'||ext==='.pdf';
176
201
  const imageMime={'.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.gif':'image/gif','.webp':'image/webp'}[ext];
@@ -238,6 +263,10 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
238
263
  if(ext==='.xls') {
239
264
  throw new Error('format .xls requires a native BIFF adapter; request rejected rather than returned as plain text');
240
265
  }
266
+ if(isUnknownBinary(bytes)){
267
+ const content=`Cannot read binary file as text: ${path.basename(args.path)}\n\nUse start_process + interact_with_process to analyze binary files with appropriate tools (Node.js or Python libraries, command-line utilities, etc.).\n\nThe read_file tool only handles text files, images, and Excel files.`;
268
+ return text(content,false,{path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,isBinary:true,fileSha256,...optionMeta});
269
+ }
241
270
  const all=linesFrom(raw.data_base64);
242
271
  const start=requestedOffset<0?Math.max(0,all.length+requestedOffset):Math.min(requestedOffset,all.length);
243
272
  const end=requestedOffset<0?all.length:Math.min(all.length,start+requestedLength);
@@ -381,7 +410,7 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
381
410
  }
382
411
  frontier=next;
383
412
  }
384
- const rows=all.map(e=>`${(e.kind??e.type)==='directory'?'[DIR]':'[FILE]'} ${e.path}`);
413
+ const rows=directoryRows(all,target.relativePath);
385
414
  return text(rows.join('\n'),false,{path:args.path,depth,entries:all,truncated,entryCount:all.length});
386
415
  }
387
416
 
@@ -803,6 +832,31 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
803
832
  seenCursors.add(query.cursor);
804
833
  }
805
834
  }while(query.cursor!==null&&state.results.length<query.maxResults);
835
+ const docxSearchEnabled=String(args.filePattern??'').toLowerCase().includes('.docx')||String(args.path).toLowerCase().endsWith('.docx');
836
+ if(args.searchType==='content'&&machineFsRuntime&&docxSearchEnabled&&state.results.length<query.maxResults){
837
+ let docxEntries=[];
838
+ if(typeof machineFsRuntime.stat==='function'){
839
+ const info=await machineFsRuntime.stat({rootId:target.rootId,relativePath:target.relativePath,hash:false});
840
+ if(info?.kind==='file'&&target.relativePath.toLowerCase().endsWith('.docx'))docxEntries=[{path:target.relativePath,kind:'file'}];
841
+ }
842
+ if(docxEntries.length===0){
843
+ const listing=await listDirectory({path:args.path,depth:10});
844
+ docxEntries=(listing.structuredContent?.entries??[]).filter(entry=>(entry.kind??entry.type)==='file'&&String(entry.path).toLowerCase().endsWith('.docx'));
845
+ }
846
+ for(const entry of docxEntries){
847
+ if(state.cancelled||state.timedOut||state.results.length>=query.maxResults)break;
848
+ const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:entry.path,offset:0,
849
+ maxBytes:Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024),hashFile:true});
850
+ if(raw.eof===false)continue;
851
+ const matches=searchDocxTextBytes(Buffer.from(raw.data_base64,'base64'),{
852
+ pattern:args.pattern,ignoreCase:args.ignoreCase!==false,maxResults:query.maxResults-state.results.length
853
+ });
854
+ for(const match of matches)state.results.push({
855
+ file:absoluteFor(machineFsRuntime,target.rootId,entry.path)+(match.part==='word/document.xml'?'':`:${match.part.slice('word/'.length)}`),
856
+ line:match.line,match:match.match,type:'content'
857
+ });
858
+ }
859
+ }
806
860
  if(!state.cancelled&&!state.timedOut) state.isComplete=true;
807
861
  }catch(error){if(!state.cancelled&&!state.timedOut){state.isError=true;state.error=String(error?.message??error);}state.isComplete=true;}
808
862
  finally{if(timeoutHandle!==null) clearTimeout(timeoutHandle);}