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.
- package/MANIFEST.SHA256 +58 -36
- package/PROVENANCE.json +11 -10
- package/README.txt +18 -7
- package/RELEASE.json +26 -39
- package/SBOM.cdx.json +3 -3
- package/TEST-MATRIX.json +32 -86
- package/package.json +4 -3
- package/proof/commander-closure-20260928/DIFFERENTIAL-CHECKPOINT.json +111 -0
- package/scripts/activate-skill-creator-1.1.1.mjs +46 -0
- package/scripts/build-advanced-pcb-skill.mjs +208 -0
- package/scripts/commander-real-performance-benchmark-r47.mjs +12 -3
- package/scripts/diagnose-skill-tree.mjs +8 -0
- package/scripts/npm-release-r48.mjs +252 -0
- package/scripts/pcb-skill-src-SKILL.md.txt +67 -0
- package/scripts/pcb-skill-src-bringup-evidence.md.txt +22 -0
- package/scripts/pcb-skill-src-constraint-model.md.txt +17 -0
- package/scripts/pcb-skill-src-emc-mechanical-dfm.md.txt +17 -0
- package/scripts/pcb-skill-src-high-speed-layout.md.txt +27 -0
- package/scripts/pcb-skill-src-kicad-native.md.txt +30 -0
- package/scripts/pcb-skill-src-kicad_pcb_probe.py.txt +159 -0
- package/scripts/pcb-skill-src-pcb_constraint_gate.py.txt +76 -0
- package/scripts/pcb-skill-src-power-integrity.md.txt +15 -0
- package/scripts/prepare-skill-creator-1.1.1.mjs +113 -0
- package/scripts/test-skill-creator-semver-matrix.mjs +30 -0
- package/scripts/test-skill-creator-semver.mjs +22 -0
- package/src/atomic-mutation.mjs +12 -4
- package/src/coding-mcp-tools.mjs +15 -1
- package/src/coding-output-contracts.mjs +9 -3
- package/src/commander-office-reader-r48.mjs +36 -3
- package/src/commander-pdf-r48.mjs +36 -1
- package/src/commander-wire-adapter-r47.mjs +56 -2
- package/src/deadbyte-cli.mjs +70 -15
- package/src/host-plane.mjs +10 -31
- package/src/machine-fs-mcp-tools.mjs +7 -1
- package/src/machine-fs-output-contracts.mjs +6 -0
- package/src/mcp-server.mjs +14 -2
- package/src/output-contracts.mjs +6 -4
- package/src/remote-live-log.mjs +189 -35
- package/src/stdio-protocol-guard.mjs +71 -0
- package/src/tool-audit.mjs +1 -1
- package/src/version.mjs +1 -1
- package/test/atomic-mutation.test.mjs +61 -0
- package/test/coding-mcp-unknown-effect.test.mjs +42 -0
- package/test/commander-biff-reference-r48.test.mjs +38 -0
- package/test/commander-core-bridge-r47.test.mjs +19 -0
- package/test/commander-office-reader-r48.test.mjs +18 -2
- package/test/commander-pdf-r48.test.mjs +35 -0
- package/test/commander-protocol-adversarial-r48.test.mjs +166 -0
- package/test/commander-real-parity-r47.test.mjs +37 -3
- package/test/commander-schema-differential-r48.test.mjs +1 -1
- package/test/commander-wire-contract-r47.test.mjs +32 -0
- package/test/full-max-catalog-r48.test.mjs +1 -1
- package/test/machine-fs-mcp-tools.test.mjs +12 -0
- package/test/output-schema.test.mjs +20 -1
- package/test/r48-release-contract.test.mjs +27 -21
- package/test/remote-live-log-r37.test.mjs +106 -7
- package/test/remote-session-cli.test.mjs +40 -0
- package/test/tool-audit.test.mjs +17 -0
- 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
|
+
}
|
package/src/atomic-mutation.mjs
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|
package/src/coding-mcp-tools.mjs
CHANGED
|
@@ -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) {
|
|
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
|
-
|
|
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))
|
|
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.
|
|
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);}
|