@linghun/tools 0.1.2 → 0.1.3
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/dist/chunk-236QJATI.js +678 -0
- package/dist/{chunk-VZWMPUSS.js → chunk-2DIXRINW.js} +1 -1
- package/dist/{chunk-DWXJBDPQ.js → chunk-F7UOI3YR.js} +16 -16
- package/dist/chunk-FI4YPDFE.js +38 -0
- package/dist/{chunk-2R75FXZG.js → chunk-J7O24FJB.js} +16 -16
- package/dist/chunk-PKMIDA5A.js +6 -0
- package/dist/chunk-SPAD4DZS.js +319 -0
- package/dist/chunk-XG6HNLY6.js +166 -0
- package/dist/index.d.ts +81 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +984 -26
- package/dist/tools/Bash/BashTool.d.ts +1 -1
- package/dist/tools/Bash/BashTool.js +2 -2
- package/dist/tools/Bash/artifact-checker.d.ts +33 -0
- package/dist/tools/Bash/artifact-checker.d.ts.map +1 -0
- package/dist/tools/Bash/artifact-checker.js +6 -0
- package/dist/tools/Bash/binary-helper.d.ts +7 -0
- package/dist/tools/Bash/binary-helper.d.ts.map +1 -0
- package/dist/tools/Bash/binary-helper.js +6 -0
- package/dist/tools/Bash/command-semantics.d.ts +11 -0
- package/dist/tools/Bash/command-semantics.d.ts.map +1 -0
- package/dist/tools/Bash/command-semantics.js +6 -0
- package/dist/tools/Bash/prompt.d.ts +1 -1
- package/dist/tools/Bash/prompt.d.ts.map +1 -1
- package/dist/tools/Bash/prompt.js +1 -1
- package/dist/tools/Bash/service-kernel.d.ts +75 -0
- package/dist/tools/Bash/service-kernel.d.ts.map +1 -0
- package/dist/tools/Bash/service-kernel.js +8 -0
- package/dist/tools/Glob/GlobTool.js +2 -2
- package/dist/tools/Todo/TodoTool.js +2 -2
- package/dist/tools/prompts.js +18 -18
- package/dist/tools/ui.js +18 -18
- package/package.json +4 -1
- package/dist/chunk-E75PYM7V.js +0 -6
- package/dist/{chunk-MFHY7PBM.js → chunk-4567YAXM.js} +3 -3
- package/dist/{chunk-DN3KJZRV.js → chunk-Z3H6SZBS.js} +3 -3
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WebSearchTool
|
|
3
|
+
} from "./chunk-4EZHIDZ6.js";
|
|
1
4
|
import {
|
|
2
5
|
WriteTool
|
|
3
6
|
} from "./chunk-I3Z4OKMS.js";
|
|
7
|
+
import {
|
|
8
|
+
MultiEditTool
|
|
9
|
+
} from "./chunk-VJDLOFCF.js";
|
|
4
10
|
import {
|
|
5
11
|
WebFetchTool
|
|
6
12
|
} from "./chunk-LGZOG7IQ.js";
|
|
7
13
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
ReadTool
|
|
15
|
+
} from "./chunk-TZ6QK52L.js";
|
|
10
16
|
import {
|
|
11
17
|
ReadSnippetsTool
|
|
12
18
|
} from "./chunk-GI5XI4HN.js";
|
|
@@ -15,28 +21,22 @@ import {
|
|
|
15
21
|
} from "./chunk-A6FUAM64.js";
|
|
16
22
|
import {
|
|
17
23
|
TodoTool
|
|
18
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-4567YAXM.js";
|
|
25
|
+
import {
|
|
26
|
+
EditTool
|
|
27
|
+
} from "./chunk-WLQ7DGMG.js";
|
|
19
28
|
import {
|
|
20
29
|
GrepTool
|
|
21
30
|
} from "./chunk-AZCQL24J.js";
|
|
22
31
|
import {
|
|
23
|
-
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import {
|
|
26
|
-
ReadTool
|
|
27
|
-
} from "./chunk-TZ6QK52L.js";
|
|
32
|
+
DiffTool
|
|
33
|
+
} from "./chunk-7ZOADACP.js";
|
|
28
34
|
import {
|
|
29
35
|
GlobTool
|
|
30
|
-
} from "./chunk-
|
|
31
|
-
import {
|
|
32
|
-
EditTool
|
|
33
|
-
} from "./chunk-WLQ7DGMG.js";
|
|
36
|
+
} from "./chunk-Z3H6SZBS.js";
|
|
34
37
|
import {
|
|
35
38
|
BashTool
|
|
36
|
-
} from "./chunk-
|
|
37
|
-
import {
|
|
38
|
-
DiffTool
|
|
39
|
-
} from "./chunk-7ZOADACP.js";
|
|
39
|
+
} from "./chunk-2DIXRINW.js";
|
|
40
40
|
|
|
41
41
|
// src/tools/prompts.ts
|
|
42
42
|
var toolPrompts = {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/tools/Bash/command-semantics.ts
|
|
2
|
+
var GREP_LIKE = /* @__PURE__ */ new Set(["grep", "rg", "egrep", "fgrep", "ag", "ack"]);
|
|
3
|
+
var DIFF_LIKE = /* @__PURE__ */ new Set(["diff", "colordiff", "icdiff"]);
|
|
4
|
+
var TEST_LIKE = /* @__PURE__ */ new Set(["test", "["]);
|
|
5
|
+
var FIND_LIKE = /* @__PURE__ */ new Set(["find", "fd"]);
|
|
6
|
+
function extractLastCommand(command) {
|
|
7
|
+
const segments = command.split(/\s*(?:\|\|?|&&|;)\s*/);
|
|
8
|
+
const last = (segments[segments.length - 1] ?? "").trimStart();
|
|
9
|
+
const envPrefixStripped = last.replace(/^(?:\w+=\S+\s+)+/, "");
|
|
10
|
+
const first = envPrefixStripped.split(/\s/)[0] ?? "";
|
|
11
|
+
const base = first.replace(/^.*[\\/]/, "");
|
|
12
|
+
return base.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function interpretCommandResult(command, exitCode) {
|
|
15
|
+
if (exitCode === 0) return { isError: false };
|
|
16
|
+
const cmd = extractLastCommand(command);
|
|
17
|
+
if (GREP_LIKE.has(cmd)) {
|
|
18
|
+
if (exitCode === 1) return { isError: false, message: "no matches found" };
|
|
19
|
+
return { isError: true, message: `${cmd} error (exit ${exitCode})` };
|
|
20
|
+
}
|
|
21
|
+
if (DIFF_LIKE.has(cmd)) {
|
|
22
|
+
if (exitCode === 1) return { isError: false, message: "files differ" };
|
|
23
|
+
return { isError: true, message: `${cmd} error (exit ${exitCode})` };
|
|
24
|
+
}
|
|
25
|
+
if (TEST_LIKE.has(cmd)) {
|
|
26
|
+
if (exitCode === 1) return { isError: false, message: "condition false" };
|
|
27
|
+
return { isError: true, message: `${cmd} error (exit ${exitCode})` };
|
|
28
|
+
}
|
|
29
|
+
if (FIND_LIKE.has(cmd)) {
|
|
30
|
+
if (exitCode === 1) return { isError: false, message: "partial results" };
|
|
31
|
+
return { isError: true, message: `${cmd} error (exit ${exitCode})` };
|
|
32
|
+
}
|
|
33
|
+
return { isError: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
interpretCommandResult
|
|
38
|
+
};
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WebSearchTool
|
|
3
|
+
} from "./chunk-4EZHIDZ6.js";
|
|
1
4
|
import {
|
|
2
5
|
WriteTool
|
|
3
6
|
} from "./chunk-I3Z4OKMS.js";
|
|
7
|
+
import {
|
|
8
|
+
MultiEditTool
|
|
9
|
+
} from "./chunk-VJDLOFCF.js";
|
|
4
10
|
import {
|
|
5
11
|
WebFetchTool
|
|
6
12
|
} from "./chunk-LGZOG7IQ.js";
|
|
7
13
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
ReadTool
|
|
15
|
+
} from "./chunk-TZ6QK52L.js";
|
|
10
16
|
import {
|
|
11
17
|
ReadSnippetsTool
|
|
12
18
|
} from "./chunk-GI5XI4HN.js";
|
|
@@ -15,28 +21,22 @@ import {
|
|
|
15
21
|
} from "./chunk-A6FUAM64.js";
|
|
16
22
|
import {
|
|
17
23
|
TodoTool
|
|
18
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-4567YAXM.js";
|
|
25
|
+
import {
|
|
26
|
+
EditTool
|
|
27
|
+
} from "./chunk-WLQ7DGMG.js";
|
|
19
28
|
import {
|
|
20
29
|
GrepTool
|
|
21
30
|
} from "./chunk-AZCQL24J.js";
|
|
22
31
|
import {
|
|
23
|
-
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import {
|
|
26
|
-
ReadTool
|
|
27
|
-
} from "./chunk-TZ6QK52L.js";
|
|
32
|
+
DiffTool
|
|
33
|
+
} from "./chunk-7ZOADACP.js";
|
|
28
34
|
import {
|
|
29
35
|
GlobTool
|
|
30
|
-
} from "./chunk-
|
|
31
|
-
import {
|
|
32
|
-
EditTool
|
|
33
|
-
} from "./chunk-WLQ7DGMG.js";
|
|
36
|
+
} from "./chunk-Z3H6SZBS.js";
|
|
34
37
|
import {
|
|
35
38
|
BashTool
|
|
36
|
-
} from "./chunk-
|
|
37
|
-
import {
|
|
38
|
-
DiffTool
|
|
39
|
-
} from "./chunk-7ZOADACP.js";
|
|
39
|
+
} from "./chunk-2DIXRINW.js";
|
|
40
40
|
|
|
41
41
|
// src/tools/ui.ts
|
|
42
42
|
var toolUserFacingNames = {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// src/tools/Bash/prompt.ts
|
|
2
|
+
var prompt = "Use Bash for verification and project commands after considering risk. Prefer read-only commands unless the user asked for changes and permissions allow them. Prefer python3 over bare python; probe imports before relying on optional Python packages. After starting a server, poll the port or health endpoint and inspect logs before claiming it is ready.";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
prompt
|
|
6
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// src/tools/Bash/artifact-checker.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { access, readFile, stat } from "fs/promises";
|
|
4
|
+
import { constants } from "fs";
|
|
5
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
6
|
+
import { parse, parseFragment } from "parse5";
|
|
7
|
+
async function checkArtifact(input) {
|
|
8
|
+
const diagnostics = [];
|
|
9
|
+
const protectedFiles = [];
|
|
10
|
+
let artifact;
|
|
11
|
+
try {
|
|
12
|
+
const info = await stat(input.target.absolute);
|
|
13
|
+
const isFile = info.isFile();
|
|
14
|
+
const isDirectory = info.isDirectory();
|
|
15
|
+
const content = isFile ? await readFile(input.target.absolute) : Buffer.alloc(0);
|
|
16
|
+
const sha256 = isFile ? hashBuffer(content) : void 0;
|
|
17
|
+
const checks = {
|
|
18
|
+
header: checkHeader(content, input.input.expectHeader ?? input.input.expectMagic),
|
|
19
|
+
json: input.input.json ? checkJson(content) : void 0,
|
|
20
|
+
executable: input.input.executable ? await checkExecutable(input.target.absolute) : void 0,
|
|
21
|
+
text: checkText(content, input.input.text),
|
|
22
|
+
preserve: await checkPreserve(content, input.input.preserve, input.context)
|
|
23
|
+
};
|
|
24
|
+
artifact = {
|
|
25
|
+
path: input.target.relative,
|
|
26
|
+
exists: true,
|
|
27
|
+
isFile,
|
|
28
|
+
isDirectory,
|
|
29
|
+
size: info.size,
|
|
30
|
+
sha256,
|
|
31
|
+
permissions: {
|
|
32
|
+
mode: `0o${(info.mode & 511).toString(8)}`,
|
|
33
|
+
executable: await canExecute(input.target.absolute)
|
|
34
|
+
},
|
|
35
|
+
checks
|
|
36
|
+
};
|
|
37
|
+
addCheckDiagnostics(diagnostics, input.target.relative, checks);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
addDiagnostic(
|
|
41
|
+
diagnostics,
|
|
42
|
+
"artifact_preservation",
|
|
43
|
+
"blocking",
|
|
44
|
+
`artifact missing or unreadable ${input.target.relative}: ${message}`,
|
|
45
|
+
"Verify the artifact path and preserve expected outputs before retrying.",
|
|
46
|
+
input.target.relative
|
|
47
|
+
);
|
|
48
|
+
artifact = {
|
|
49
|
+
path: input.target.relative,
|
|
50
|
+
exists: false,
|
|
51
|
+
error: message
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
for (const protectedPath of input.protectPaths) {
|
|
55
|
+
const result = await checkProtectedPath(protectedPath, input.context);
|
|
56
|
+
protectedFiles.push(result);
|
|
57
|
+
if (result.modified) {
|
|
58
|
+
addDiagnostic(
|
|
59
|
+
diagnostics,
|
|
60
|
+
"artifact_preservation",
|
|
61
|
+
"blocking",
|
|
62
|
+
`protected input modified ${result.path}`,
|
|
63
|
+
"Restore or intentionally account for protected input files before continuing.",
|
|
64
|
+
result.path
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const text = [
|
|
69
|
+
`Artifact check ${input.target.relative}`,
|
|
70
|
+
`exists ${artifact.exists ? "yes" : "no"}`,
|
|
71
|
+
"size" in artifact && typeof artifact.size === "number" ? `size ${artifact.size}` : "",
|
|
72
|
+
"sha256" in artifact && typeof artifact.sha256 === "string" ? `sha256 ${artifact.sha256}` : "",
|
|
73
|
+
formatCheckSummary("header", artifact.checks?.header),
|
|
74
|
+
formatCheckSummary("json", artifact.checks?.json),
|
|
75
|
+
formatCheckSummary("executable", artifact.checks?.executable),
|
|
76
|
+
formatCheckSummary("text", artifact.checks?.text),
|
|
77
|
+
formatCheckSummary("preserve", artifact.checks?.preserve),
|
|
78
|
+
...protectedFiles.map((item) => `protect ${item.path} ${item.modified ? "modified" : "clean"}`),
|
|
79
|
+
diagnostics.length > 0 ? `diagnostics ${diagnostics.length}` : ""
|
|
80
|
+
].filter(Boolean).join("\n");
|
|
81
|
+
return {
|
|
82
|
+
text,
|
|
83
|
+
data: {
|
|
84
|
+
exitCode: diagnostics.some((item) => item.severity === "blocking") ? 1 : 0,
|
|
85
|
+
outcome: diagnostics.length > 0 ? "artifact_check_failed" : "artifact_checked",
|
|
86
|
+
artifact,
|
|
87
|
+
protectPaths: protectedFiles,
|
|
88
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function checkHeader(content, expected) {
|
|
93
|
+
if (expected === void 0) return void 0;
|
|
94
|
+
const actual = content.subarray(0, Buffer.byteLength(expected)).toString("utf8");
|
|
95
|
+
return {
|
|
96
|
+
ok: actual === expected,
|
|
97
|
+
expected,
|
|
98
|
+
actual
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function checkJson(content) {
|
|
102
|
+
try {
|
|
103
|
+
JSON.parse(content.toString("utf8"));
|
|
104
|
+
return { ok: true };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function checkText(content, expected) {
|
|
113
|
+
if (!expected) return void 0;
|
|
114
|
+
const actual = content.toString("utf8");
|
|
115
|
+
const contains = normalizeStringList(expected.contains);
|
|
116
|
+
const exact = expected.exact === void 0 ? void 0 : actual === expected.exact;
|
|
117
|
+
const missingContains = contains.filter((item) => !actual.includes(item));
|
|
118
|
+
const lines = new Set(actual.split(/\r?\n/u));
|
|
119
|
+
const missingLines = (expected.lineSet ?? []).filter((line) => !lines.has(line));
|
|
120
|
+
return {
|
|
121
|
+
ok: exact !== false && missingContains.length === 0 && missingLines.length === 0,
|
|
122
|
+
...expected.exact !== void 0 ? { exact: { ok: exact === true } } : {},
|
|
123
|
+
...contains.length > 0 ? { contains: { ok: missingContains.length === 0, missing: missingContains } } : {},
|
|
124
|
+
...expected.lineSet && expected.lineSet.length > 0 ? { lineSet: { ok: missingLines.length === 0, missing: missingLines } } : {}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function checkPreserve(content, preserve, context) {
|
|
128
|
+
if (!preserve) return void 0;
|
|
129
|
+
const actual = content.toString("utf8");
|
|
130
|
+
const expected = await readPreserveExpected(preserve, context);
|
|
131
|
+
if (expected.error) return { ok: false, error: expected.error };
|
|
132
|
+
const expectedText = expected.text ?? "";
|
|
133
|
+
const ok = preserve.mode === "rawPreserve" ? actual === expectedText : canonicalizeHtmlForPreserve(actual) === canonicalizeHtmlForPreserve(expectedText);
|
|
134
|
+
return { ok, mode: preserve.mode };
|
|
135
|
+
}
|
|
136
|
+
async function readPreserveExpected(preserve, context) {
|
|
137
|
+
if (preserve.expectedText !== void 0) return { text: preserve.expectedText };
|
|
138
|
+
if (!preserve.expectedPath) return { error: "preserve expectedPath or expectedText is required" };
|
|
139
|
+
try {
|
|
140
|
+
const target = resolve(context.workspaceRoot, preserve.expectedPath);
|
|
141
|
+
const rel = relative(context.workspaceRoot, target);
|
|
142
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
143
|
+
return { error: `path outside workspace: ${preserve.expectedPath}` };
|
|
144
|
+
}
|
|
145
|
+
return { text: await readFile(target, "utf8") };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function normalizeStringList(value) {
|
|
151
|
+
if (value === void 0) return [];
|
|
152
|
+
return Array.isArray(value) ? value : [value];
|
|
153
|
+
}
|
|
154
|
+
function canonicalizeHtmlForPreserve(value) {
|
|
155
|
+
const normalized = value.replace(/\r\n/gu, "\n");
|
|
156
|
+
const document = parse(normalized);
|
|
157
|
+
const fragment = parseFragment(normalized);
|
|
158
|
+
const documentSemantic = canonicalizeHtmlDocument(document);
|
|
159
|
+
const fragmentSemantic = canonicalizeHtmlFragment(fragment);
|
|
160
|
+
return [
|
|
161
|
+
documentSemantic,
|
|
162
|
+
fragmentSemantic === documentSemantic ? "" : fragmentSemantic
|
|
163
|
+
].join("\n---fragment---\n");
|
|
164
|
+
}
|
|
165
|
+
var RAW_TEXT_PRESERVE_TAGS = /* @__PURE__ */ new Set(["script", "style", "pre", "textarea"]);
|
|
166
|
+
function canonicalizeHtmlNode(node, parentTag = "") {
|
|
167
|
+
const nodeName = node.nodeName ?? "";
|
|
168
|
+
if (nodeName === "#text") {
|
|
169
|
+
const text = RAW_TEXT_PRESERVE_TAGS.has(parentTag) ? node.value ?? "" : normalizeHtmlText(node.value ?? "");
|
|
170
|
+
return text ? `text:${text}` : "";
|
|
171
|
+
}
|
|
172
|
+
if (nodeName === "#comment") return `comment:${node.data ?? ""}`;
|
|
173
|
+
if (nodeName === "#documentType") {
|
|
174
|
+
return `doctype:${node.name ?? "html"}:${node.publicId ?? ""}:${node.systemId ?? ""}`;
|
|
175
|
+
}
|
|
176
|
+
const tag = (node.tagName ?? nodeName).toLowerCase();
|
|
177
|
+
const attrs = (node.attrs ?? []).map((attr) => `${attr.name.toLowerCase()}=${JSON.stringify(normalizeHtmlText(attr.value))}`).sort().join(",");
|
|
178
|
+
const children = (node.childNodes ?? []).map((child) => canonicalizeHtmlNode(child, tag)).filter(Boolean).join("");
|
|
179
|
+
return `<${tag}${attrs ? ` ${attrs}` : ""}>${children}</${tag}>`;
|
|
180
|
+
}
|
|
181
|
+
function canonicalizeHtmlDocument(document) {
|
|
182
|
+
const html = findChild(document, "html");
|
|
183
|
+
if (!html) return canonicalizeHtmlChildren(document.childNodes ?? []);
|
|
184
|
+
const head = findChild(html, "head");
|
|
185
|
+
const body = findChild(html, "body");
|
|
186
|
+
return [
|
|
187
|
+
canonicalizeHtmlChildren(head?.childNodes ?? []),
|
|
188
|
+
canonicalizeHtmlChildren(body?.childNodes ?? [])
|
|
189
|
+
].filter(Boolean).join("");
|
|
190
|
+
}
|
|
191
|
+
function canonicalizeHtmlFragment(fragment) {
|
|
192
|
+
const children = (fragment.childNodes ?? []).filter((child) => child.nodeName !== "#documentType");
|
|
193
|
+
const html = children.find((child) => (child.tagName ?? child.nodeName ?? "").toLowerCase() === "html");
|
|
194
|
+
if (!html) return canonicalizeHtmlChildren(children);
|
|
195
|
+
const head = findChild(html, "head");
|
|
196
|
+
const body = findChild(html, "body");
|
|
197
|
+
return [
|
|
198
|
+
canonicalizeHtmlChildren(head?.childNodes ?? []),
|
|
199
|
+
canonicalizeHtmlChildren(body?.childNodes ?? [])
|
|
200
|
+
].filter(Boolean).join("");
|
|
201
|
+
}
|
|
202
|
+
function canonicalizeHtmlChildren(children) {
|
|
203
|
+
return children.map((child) => canonicalizeHtmlNode(child)).filter(Boolean).join("");
|
|
204
|
+
}
|
|
205
|
+
function findChild(node, name) {
|
|
206
|
+
return (node.childNodes ?? []).find((child) => (child.tagName ?? child.nodeName ?? "").toLowerCase() === name);
|
|
207
|
+
}
|
|
208
|
+
function normalizeHtmlText(value) {
|
|
209
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
210
|
+
}
|
|
211
|
+
async function checkExecutable(path) {
|
|
212
|
+
return { ok: await canExecute(path) };
|
|
213
|
+
}
|
|
214
|
+
async function canExecute(path) {
|
|
215
|
+
try {
|
|
216
|
+
await access(path, constants.X_OK);
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function checkProtectedPath(path, context) {
|
|
223
|
+
try {
|
|
224
|
+
const content = await readFile(path.absolute);
|
|
225
|
+
const sha256 = hashBuffer(content);
|
|
226
|
+
const snapshot = context.readSnapshots?.[path.relative];
|
|
227
|
+
const changed = context.changedFiles.includes(path.relative);
|
|
228
|
+
const modified = snapshot ? snapshot.hash !== hashText(content.toString("utf8")) : changed;
|
|
229
|
+
return {
|
|
230
|
+
path: path.relative,
|
|
231
|
+
exists: true,
|
|
232
|
+
size: content.length,
|
|
233
|
+
sha256,
|
|
234
|
+
modified,
|
|
235
|
+
baseline: snapshot ? "readSnapshot" : changed ? "changedFiles" : "none"
|
|
236
|
+
};
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
path: path.relative,
|
|
240
|
+
exists: false,
|
|
241
|
+
modified: true,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
baseline: "missing"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function addCheckDiagnostics(diagnostics, path, checks) {
|
|
248
|
+
if (checks.header && !checks.header.ok) {
|
|
249
|
+
addDiagnostic(
|
|
250
|
+
diagnostics,
|
|
251
|
+
"artifact_preservation",
|
|
252
|
+
"blocking",
|
|
253
|
+
`wrong header ${path}: expected ${checks.header.expected}`,
|
|
254
|
+
"Preserve the expected artifact format and regenerate only the output if needed.",
|
|
255
|
+
path
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (checks.json && !checks.json.ok) {
|
|
259
|
+
addDiagnostic(
|
|
260
|
+
diagnostics,
|
|
261
|
+
"artifact_preservation",
|
|
262
|
+
"blocking",
|
|
263
|
+
`output format mismatch ${path}: invalid JSON`,
|
|
264
|
+
"Fix the artifact format before verification.",
|
|
265
|
+
path
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (checks.executable && !checks.executable.ok) {
|
|
269
|
+
addDiagnostic(
|
|
270
|
+
diagnostics,
|
|
271
|
+
"artifact_preservation",
|
|
272
|
+
"blocking",
|
|
273
|
+
`permission denied ${path}: executable bit is not set`,
|
|
274
|
+
"Set executable permission when the artifact is expected to run.",
|
|
275
|
+
path
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (checks.text && !checks.text.ok) {
|
|
279
|
+
const missing = [
|
|
280
|
+
...checks.text.contains?.missing ?? [],
|
|
281
|
+
...checks.text.lineSet?.missing ?? []
|
|
282
|
+
];
|
|
283
|
+
addDiagnostic(
|
|
284
|
+
diagnostics,
|
|
285
|
+
"artifact_preservation",
|
|
286
|
+
"blocking",
|
|
287
|
+
`text expectation mismatch ${path}${missing.length > 0 ? `: missing ${missing.join(", ")}` : ""}`,
|
|
288
|
+
"Update the artifact so the explicitly requested text expectations pass.",
|
|
289
|
+
path
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (checks.preserve && !checks.preserve.ok) {
|
|
293
|
+
addDiagnostic(
|
|
294
|
+
diagnostics,
|
|
295
|
+
"artifact_preservation",
|
|
296
|
+
"blocking",
|
|
297
|
+
`preservation mismatch ${path}${checks.preserve.mode ? ` mode=${checks.preserve.mode}` : ""}`,
|
|
298
|
+
"Preserve the explicitly requested source content or confirm intentional changes before continuing.",
|
|
299
|
+
path
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function addDiagnostic(diagnostics, type, severity, evidence, suggestion, path) {
|
|
304
|
+
diagnostics.push({ type, severity, evidence, suggestion, ...path ? { path } : {} });
|
|
305
|
+
}
|
|
306
|
+
function formatCheckSummary(name, check) {
|
|
307
|
+
if (!check) return "";
|
|
308
|
+
return `${name} ${check.ok ? "ok" : "failed"}`;
|
|
309
|
+
}
|
|
310
|
+
function hashBuffer(content) {
|
|
311
|
+
return createHash("sha256").update(content).digest("hex");
|
|
312
|
+
}
|
|
313
|
+
function hashText(content) {
|
|
314
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export {
|
|
318
|
+
checkArtifact
|
|
319
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/tools/Bash/binary-helper.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { createReadStream } from "fs";
|
|
4
|
+
import { open, stat } from "fs/promises";
|
|
5
|
+
var DEFAULT_PREVIEW_BYTES = 64;
|
|
6
|
+
var MAX_PREVIEW_BYTES = 512;
|
|
7
|
+
var ELF64_HEADER_BYTES = 64;
|
|
8
|
+
var SQLITE_HEADER = "SQLite format 3\0";
|
|
9
|
+
async function inspectBinaryFile(input, targetPath) {
|
|
10
|
+
const previewBytes = Math.min(input.previewBytes ?? DEFAULT_PREVIEW_BYTES, MAX_PREVIEW_BYTES);
|
|
11
|
+
try {
|
|
12
|
+
const fileStat = await stat(targetPath);
|
|
13
|
+
const header = await readFileHeader(
|
|
14
|
+
targetPath,
|
|
15
|
+
Math.min(fileStat.size, Math.max(previewBytes, ELF64_HEADER_BYTES, SQLITE_HEADER.length))
|
|
16
|
+
);
|
|
17
|
+
const preview = header.subarray(0, previewBytes);
|
|
18
|
+
const magic = detectMagic(header);
|
|
19
|
+
const elf = magic.type === "elf" ? parseElf(header) : void 0;
|
|
20
|
+
const sha256 = await hashFileSha256(targetPath);
|
|
21
|
+
const text = [
|
|
22
|
+
`Binary inspect ${input.path}`,
|
|
23
|
+
`size ${fileStat.size}`,
|
|
24
|
+
`sha256 ${sha256}`,
|
|
25
|
+
`magic ${magic.label}`,
|
|
26
|
+
elf ? formatElfSummary(elf) : "",
|
|
27
|
+
`hex ${formatHex(preview)}`,
|
|
28
|
+
`ascii ${formatAscii(preview)}`
|
|
29
|
+
].filter(Boolean).join("\n");
|
|
30
|
+
return {
|
|
31
|
+
text,
|
|
32
|
+
data: {
|
|
33
|
+
exitCode: 0,
|
|
34
|
+
outcome: "binary_inspected",
|
|
35
|
+
binary: {
|
|
36
|
+
path: targetPath,
|
|
37
|
+
size: fileStat.size,
|
|
38
|
+
sha256,
|
|
39
|
+
previewBytes: preview.length,
|
|
40
|
+
hexPreview: formatHex(preview),
|
|
41
|
+
asciiPreview: formatAscii(preview),
|
|
42
|
+
magic,
|
|
43
|
+
...elf ? { elf } : {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
+
const diagnostic = {
|
|
50
|
+
type: "artifact_preservation",
|
|
51
|
+
severity: "blocking",
|
|
52
|
+
evidence: `binary inspect failed for ${input.path}: ${message}`,
|
|
53
|
+
suggestion: "Verify the file path and preserve expected input artifacts before retrying.",
|
|
54
|
+
path: input.path
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
text: [
|
|
58
|
+
`Binary inspect failed ${input.path}`,
|
|
59
|
+
diagnostic.evidence
|
|
60
|
+
].join("\n"),
|
|
61
|
+
data: {
|
|
62
|
+
exitCode: 1,
|
|
63
|
+
outcome: "binary_inspect_failed",
|
|
64
|
+
diagnostics: [diagnostic]
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function detectMagic(content) {
|
|
70
|
+
if (content.length >= 4 && content[0] === 127 && content[1] === 69 && content[2] === 76 && content[3] === 70) {
|
|
71
|
+
return { type: "elf", label: "ELF executable/shared object" };
|
|
72
|
+
}
|
|
73
|
+
if (content.length >= 2 && content[0] === 31 && content[1] === 139) {
|
|
74
|
+
return { type: "gzip", label: "gzip compressed data" };
|
|
75
|
+
}
|
|
76
|
+
if (content.length >= 4 && content[0] === 80 && content[1] === 75 && content[2] === 3 && content[3] === 4) {
|
|
77
|
+
return { type: "zip", label: "zip archive" };
|
|
78
|
+
}
|
|
79
|
+
if (content.length >= 6 && content[0] === 55 && content[1] === 122 && content[2] === 188 && content[3] === 175 && content[4] === 39 && content[5] === 28) {
|
|
80
|
+
return { type: "7z", label: "7z archive" };
|
|
81
|
+
}
|
|
82
|
+
if (content.length >= 8 && content[0] === 137 && content[1] === 80 && content[2] === 78 && content[3] === 71 && content[4] === 13 && content[5] === 10 && content[6] === 26 && content[7] === 10) {
|
|
83
|
+
return { type: "png", label: "PNG image" };
|
|
84
|
+
}
|
|
85
|
+
if (content.length >= 3 && content[0] === 255 && content[1] === 216 && content[2] === 255) {
|
|
86
|
+
return { type: "jpeg", label: "JPEG image" };
|
|
87
|
+
}
|
|
88
|
+
if (content.length >= 4 && content.subarray(0, 4).toString("ascii") === "%PDF") {
|
|
89
|
+
return { type: "pdf", label: "PDF document" };
|
|
90
|
+
}
|
|
91
|
+
if (content.length >= SQLITE_HEADER.length && content.subarray(0, SQLITE_HEADER.length).toString("binary") === SQLITE_HEADER) {
|
|
92
|
+
return { type: "sqlite", label: "SQLite database" };
|
|
93
|
+
}
|
|
94
|
+
return { type: "unknown", label: "unknown binary/text" };
|
|
95
|
+
}
|
|
96
|
+
function parseElf(content) {
|
|
97
|
+
const elfClass = content[4] === 1 ? "32" : content[4] === 2 ? "64" : "unknown";
|
|
98
|
+
const requiredBytes = elfClass === "64" ? 64 : 52;
|
|
99
|
+
if (content.length < requiredBytes) {
|
|
100
|
+
return { error: "ELF header is truncated" };
|
|
101
|
+
}
|
|
102
|
+
const endian = content[5] === 1 ? "little" : content[5] === 2 ? "big" : "unknown";
|
|
103
|
+
const littleEndian = endian !== "big";
|
|
104
|
+
const readUInt16 = (offset) => littleEndian ? content.readUInt16LE(offset) : content.readUInt16BE(offset);
|
|
105
|
+
const readUInt32 = (offset) => littleEndian ? content.readUInt32LE(offset) : content.readUInt32BE(offset);
|
|
106
|
+
const readUInt64 = (offset) => {
|
|
107
|
+
const value = littleEndian ? content.readBigUInt64LE(offset) : content.readBigUInt64BE(offset);
|
|
108
|
+
return `0x${value.toString(16)}`;
|
|
109
|
+
};
|
|
110
|
+
const is64 = elfClass === "64";
|
|
111
|
+
return {
|
|
112
|
+
class: elfClass,
|
|
113
|
+
endianness: endian,
|
|
114
|
+
type: elfTypeName(readUInt16(16)),
|
|
115
|
+
machine: elfMachineName(readUInt16(18)),
|
|
116
|
+
entry: is64 ? readUInt64(24) : `0x${readUInt32(24).toString(16)}`,
|
|
117
|
+
programHeaderOffset: is64 ? readUInt64(32) : `0x${readUInt32(28).toString(16)}`,
|
|
118
|
+
sectionHeaderOffset: is64 ? readUInt64(40) : `0x${readUInt32(32).toString(16)}`,
|
|
119
|
+
headerSize: readUInt16(is64 ? 52 : 40),
|
|
120
|
+
programHeaderEntrySize: readUInt16(is64 ? 54 : 42),
|
|
121
|
+
programHeaderCount: readUInt16(is64 ? 56 : 44),
|
|
122
|
+
sectionHeaderEntrySize: readUInt16(is64 ? 58 : 46),
|
|
123
|
+
sectionHeaderCount: readUInt16(is64 ? 60 : 48),
|
|
124
|
+
sectionNameStringTableIndex: readUInt16(is64 ? 62 : 50)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function formatElfSummary(elf) {
|
|
128
|
+
if ("error" in elf) {
|
|
129
|
+
return `elf ${elf.error}`;
|
|
130
|
+
}
|
|
131
|
+
return `elf ${elf.class} ${elf.endianness} type=${elf.type} machine=${elf.machine} entry=${elf.entry}`;
|
|
132
|
+
}
|
|
133
|
+
async function readFileHeader(path, bytes) {
|
|
134
|
+
if (bytes <= 0) return Buffer.alloc(0);
|
|
135
|
+
const handle = await open(path, "r");
|
|
136
|
+
try {
|
|
137
|
+
const buffer = Buffer.alloc(bytes);
|
|
138
|
+
const result = await handle.read(buffer, 0, bytes, 0);
|
|
139
|
+
return buffer.subarray(0, result.bytesRead);
|
|
140
|
+
} finally {
|
|
141
|
+
await handle.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function hashFileSha256(path) {
|
|
145
|
+
const hash = createHash("sha256");
|
|
146
|
+
for await (const chunk of createReadStream(path)) {
|
|
147
|
+
hash.update(chunk);
|
|
148
|
+
}
|
|
149
|
+
return hash.digest("hex");
|
|
150
|
+
}
|
|
151
|
+
function elfTypeName(value) {
|
|
152
|
+
return { 0: "none", 1: "relocatable", 2: "executable", 3: "shared", 4: "core" }[value] ?? `unknown(${value})`;
|
|
153
|
+
}
|
|
154
|
+
function elfMachineName(value) {
|
|
155
|
+
return { 3: "x86", 40: "arm", 62: "x86-64", 183: "aarch64", 243: "riscv" }[value] ?? `unknown(${value})`;
|
|
156
|
+
}
|
|
157
|
+
function formatHex(buffer) {
|
|
158
|
+
return [...buffer].map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
|
|
159
|
+
}
|
|
160
|
+
function formatAscii(buffer) {
|
|
161
|
+
return [...buffer].map((byte) => byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : ".").join("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
inspectBinaryFile
|
|
166
|
+
};
|