omk-book-to-skill 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +56 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +109 -0
- package/dist/cli.js.map +1 -0
- package/dist/extension.d.ts +5 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +99 -0
- package/dist/extension.js.map +1 -0
- package/dist/file-digests.d.ts +4 -0
- package/dist/file-digests.d.ts.map +1 -0
- package/dist/file-digests.js +113 -0
- package/dist/file-digests.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +113 -0
- package/dist/manifest.js.map +1 -0
- package/dist/metadata.d.ts +4 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +11 -0
- package/dist/metadata.js.map +1 -0
- package/dist/provenance.d.ts +5 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +143 -0
- package/dist/provenance.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/skills/book-to-skill/SKILL.md +64 -0
- package/skills/book-to-skill/scripts/provenance.mjs +3 -0
- package/upstream.json +37 -0
- package/vendor/book-to-skill/LICENSE.md +21 -0
- package/vendor/book-to-skill/SKILL.md +647 -0
- package/vendor/book-to-skill/book_to_skill/__init__.py +4 -0
- package/vendor/book-to-skill/book_to_skill/__main__.py +4 -0
- package/vendor/book-to-skill/book_to_skill/cli.py +16 -0
- package/vendor/book-to-skill/book_to_skill/config.py +42 -0
- package/vendor/book-to-skill/book_to_skill/dependencies.py +289 -0
- package/vendor/book-to-skill/book_to_skill/exceptions.py +2 -0
- package/vendor/book-to-skill/book_to_skill/parsers/__init__.py +1 -0
- package/vendor/book-to-skill/book_to_skill/parsers/calibre.py +26 -0
- package/vendor/book-to-skill/book_to_skill/parsers/docx.py +140 -0
- package/vendor/book-to-skill/book_to_skill/parsers/epub.py +125 -0
- package/vendor/book-to-skill/book_to_skill/parsers/html.py +105 -0
- package/vendor/book-to-skill/book_to_skill/parsers/pdf.py +190 -0
- package/vendor/book-to-skill/book_to_skill/parsers/rtf.py +146 -0
- package/vendor/book-to-skill/book_to_skill/parsers/text.py +38 -0
- package/vendor/book-to-skill/book_to_skill/sanitize.py +96 -0
- package/vendor/book-to-skill/book_to_skill/utils.py +918 -0
- package/vendor/book-to-skill/pyproject.toml +42 -0
- package/vendor/book-to-skill/scripts/banner.txt +26 -0
- package/vendor/book-to-skill/scripts/extract.py +26 -0
- package/vendor/book-to-skill/tools/discovery_tax.py +195 -0
- package/vendor/book-to-skill/tools/scan_generated_skill.py +338 -0
- package/vendor/book-to-skill/tools/validate_skill.py +220 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
Copyright (c) 2025 virgiliojr94
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# omk-book-to-skill
|
|
2
|
+
|
|
3
|
+
Optional OMK package for compiling documents into reusable Agent Skills. It combines a pinned `book-to-skill` workflow with OMK commands and a local provenance verifier.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From npm after publication:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
omk install npm:omk-book-to-skill@0.95.2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
From this repository:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run build --workspace packages/book-to-skill
|
|
17
|
+
omk install ./packages/book-to-skill
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Python 3.9 or newer is required for extraction. Format-specific packages such as `pypdf`, `python-docx`, and Docling are optional and stay in the user's Python environment; this npm package installs none of them.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
/book-to-skill-compile <source...>
|
|
26
|
+
/book-to-skill-update <existing-skill> <new-source...>
|
|
27
|
+
/book-to-skill-verify <generated-skill-directory>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The package also installs the `book-to-skill` skill, so `/skill:book-to-skill` and `!book-to-skill` remain available.
|
|
31
|
+
|
|
32
|
+
The workflow records provenance after generation through `scripts/provenance.mjs`, resolved relative to the bundled skill. In a normal npm installation where the package bin is on `PATH`, the equivalent commands are:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
omk-book-to-skill record --skill ~/.omk/agent/skills/example --source ./book.pdf
|
|
36
|
+
omk-book-to-skill verify --skill ~/.omk/agent/skills/example --source ./book.pdf
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Repeat `--source` in the original order for multi-source compilations. Use `record --merge-sources` after folding new sources into an existing skill.
|
|
40
|
+
|
|
41
|
+
## Verification boundary
|
|
42
|
+
|
|
43
|
+
The v1 manifest records SHA-256 digests for source files and generated artifacts. `verify` also runs the pinned upstream advisory prompt-injection scanner.
|
|
44
|
+
|
|
45
|
+
This checks local byte-level consistency against an unsigned manifest. It does not authenticate the manifest or prove authorship, licensing, trusted attestation, or semantic claim-to-source grounding. The latter requires a future span/claim evaluation layer.
|
|
46
|
+
|
|
47
|
+
## Upstream
|
|
48
|
+
|
|
49
|
+
The runtime subset under `vendor/book-to-skill/` is an unmodified snapshot of [`virgiliojr94/book-to-skill`](https://github.com/virgiliojr94/book-to-skill) commit `c4c5e948caaa912c9e2024b925a7cdee9237b0c0` (declared upstream version `1.4.0`). `upstream.json` pins every vendored file by SHA-256.
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm test --workspace packages/book-to-skill
|
|
55
|
+
npm run build --workspace packages/book-to-skill
|
|
56
|
+
```
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { COMPILER_IDENTITY } from \"./metadata.ts\";\nimport { recordProvenance, verifyProvenance } from \"./provenance.ts\";\n\nconst PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst SCANNER_PATH = resolve(PACKAGE_ROOT, \"vendor\", \"book-to-skill\", \"tools\", \"scan_generated_skill.py\");\n\ninterface ScannerReport {\n\treadonly status: \"pass\" | \"fail\" | \"inconclusive\" | \"unavailable\" | \"skipped\";\n\treadonly command?: string;\n\treadonly stdout?: string;\n\treadonly stderr?: string;\n}\n\nfunction usage(): string {\n\treturn `Usage:\n omk-book-to-skill record --skill <dir> --source <file> [--source <file>...] [--merge-sources]\n omk-book-to-skill verify --skill <dir> [--source <file>...]\n\nrecord writes ${\".book-to-skill-provenance.json\"} after generation.\nverify checks artifact hashes, optionally rechecks exact source files, and runs the bundled advisory scanner.`;\n}\n\nfunction parseCommandOptions(args: readonly string[]) {\n\treturn parseArgs({\n\t\targs,\n\t\tallowPositionals: false,\n\t\tstrict: true,\n\t\toptions: {\n\t\t\tskill: { type: \"string\" },\n\t\t\tsource: { type: \"string\", multiple: true },\n\t\t\t\"merge-sources\": { type: \"boolean\", default: false },\n\t\t},\n\t});\n}\n\nfunction pythonCandidates(): string[] {\n\tconst configured = process.env.BOOK_TO_SKILL_PYTHON?.trim();\n\treturn [...new Set([configured, \"python3\", \"python\"].filter((value): value is string => Boolean(value)))];\n}\n\nfunction runScanner(skillDir: string): ScannerReport {\n\tif (!existsSync(SCANNER_PATH))\n\t\treturn { status: \"unavailable\", stderr: `Bundled scanner is missing: ${SCANNER_PATH}` };\n\tfor (const python of pythonCandidates()) {\n\t\tconst result = spawnSync(python, [SCANNER_PATH, skillDir], {\n\t\t\tencoding: \"utf8\",\n\t\t\tmaxBuffer: 10 * 1024 * 1024,\n\t\t\tshell: false,\n\t\t\ttimeout: 120_000,\n\t\t});\n\t\tif (result.error && \"code\" in result.error && result.error.code === \"ENOENT\") continue;\n\t\treturn {\n\t\t\tstatus: result.status === 0 ? \"pass\" : result.status === 1 ? \"fail\" : \"inconclusive\",\n\t\t\tcommand: python,\n\t\t\tstdout: result.stdout?.trim() || undefined,\n\t\t\tstderr: result.stderr?.trim() || result.error?.message,\n\t\t};\n\t}\n\treturn { status: \"unavailable\", stderr: \"Neither python3 nor python is available\" };\n}\n\nfunction writeJson(value: unknown): void {\n\tprocess.stdout.write(`${JSON.stringify(value, undefined, 2)}\\n`);\n}\n\nfunction requireSkill(value: string | undefined): string {\n\tif (!value) throw new Error(\"--skill is required\");\n\treturn resolve(value);\n}\n\nfunction record(args: readonly string[]): number {\n\tconst { values } = parseCommandOptions(args);\n\tconst sources = values.source ?? [];\n\tif (sources.length === 0 && !values[\"merge-sources\"]) throw new Error(\"record requires at least one --source\");\n\tconst manifest = recordProvenance({\n\t\tskillDir: requireSkill(values.skill),\n\t\tsources,\n\t\tcompiler: COMPILER_IDENTITY,\n\t\tmergeSources: values[\"merge-sources\"],\n\t});\n\twriteJson({ status: \"recorded\", manifest });\n\treturn 0;\n}\n\nfunction verify(args: readonly string[]): number {\n\tconst { values } = parseCommandOptions(args);\n\tconst skillDir = requireSkill(values.skill);\n\tconst provenance = verifyProvenance({ skillDir, sources: values.source });\n\tconst scanner = provenance.artifactIntegrity === \"pass\" ? runScanner(skillDir) : { status: \"skipped\" as const };\n\twriteJson({ provenance, scanner });\n\tif (provenance.status === \"fail\" || scanner.status === \"fail\") return 1;\n\tif (provenance.status === \"inconclusive\" || scanner.status !== \"pass\") return 2;\n\treturn 0;\n}\n\nfunction main(): number {\n\tconst [command, ...args] = process.argv.slice(2);\n\tif (!command || command === \"-h\" || command === \"--help\") {\n\t\tprocess.stdout.write(`${usage()}\\n`);\n\t\treturn command ? 0 : 2;\n\t}\n\tif (command === \"record\") return record(args);\n\tif (command === \"verify\") return verify(args);\n\tthrow new Error(`Unknown command: ${command}`);\n}\n\ntry {\n\tprocess.exitCode = main();\n} catch (error) {\n\tprocess.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n\\n${usage()}\\n`);\n\tprocess.exitCode = 2;\n}\n"]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { COMPILER_IDENTITY } from "./metadata.js";
|
|
8
|
+
import { recordProvenance, verifyProvenance } from "./provenance.js";
|
|
9
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const SCANNER_PATH = resolve(PACKAGE_ROOT, "vendor", "book-to-skill", "tools", "scan_generated_skill.py");
|
|
11
|
+
function usage() {
|
|
12
|
+
return `Usage:
|
|
13
|
+
omk-book-to-skill record --skill <dir> --source <file> [--source <file>...] [--merge-sources]
|
|
14
|
+
omk-book-to-skill verify --skill <dir> [--source <file>...]
|
|
15
|
+
|
|
16
|
+
record writes ${".book-to-skill-provenance.json"} after generation.
|
|
17
|
+
verify checks artifact hashes, optionally rechecks exact source files, and runs the bundled advisory scanner.`;
|
|
18
|
+
}
|
|
19
|
+
function parseCommandOptions(args) {
|
|
20
|
+
return parseArgs({
|
|
21
|
+
args,
|
|
22
|
+
allowPositionals: false,
|
|
23
|
+
strict: true,
|
|
24
|
+
options: {
|
|
25
|
+
skill: { type: "string" },
|
|
26
|
+
source: { type: "string", multiple: true },
|
|
27
|
+
"merge-sources": { type: "boolean", default: false },
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function pythonCandidates() {
|
|
32
|
+
const configured = process.env.BOOK_TO_SKILL_PYTHON?.trim();
|
|
33
|
+
return [...new Set([configured, "python3", "python"].filter((value) => Boolean(value)))];
|
|
34
|
+
}
|
|
35
|
+
function runScanner(skillDir) {
|
|
36
|
+
if (!existsSync(SCANNER_PATH))
|
|
37
|
+
return { status: "unavailable", stderr: `Bundled scanner is missing: ${SCANNER_PATH}` };
|
|
38
|
+
for (const python of pythonCandidates()) {
|
|
39
|
+
const result = spawnSync(python, [SCANNER_PATH, skillDir], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
42
|
+
shell: false,
|
|
43
|
+
timeout: 120_000,
|
|
44
|
+
});
|
|
45
|
+
if (result.error && "code" in result.error && result.error.code === "ENOENT")
|
|
46
|
+
continue;
|
|
47
|
+
return {
|
|
48
|
+
status: result.status === 0 ? "pass" : result.status === 1 ? "fail" : "inconclusive",
|
|
49
|
+
command: python,
|
|
50
|
+
stdout: result.stdout?.trim() || undefined,
|
|
51
|
+
stderr: result.stderr?.trim() || result.error?.message,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { status: "unavailable", stderr: "Neither python3 nor python is available" };
|
|
55
|
+
}
|
|
56
|
+
function writeJson(value) {
|
|
57
|
+
process.stdout.write(`${JSON.stringify(value, undefined, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
function requireSkill(value) {
|
|
60
|
+
if (!value)
|
|
61
|
+
throw new Error("--skill is required");
|
|
62
|
+
return resolve(value);
|
|
63
|
+
}
|
|
64
|
+
function record(args) {
|
|
65
|
+
const { values } = parseCommandOptions(args);
|
|
66
|
+
const sources = values.source ?? [];
|
|
67
|
+
if (sources.length === 0 && !values["merge-sources"])
|
|
68
|
+
throw new Error("record requires at least one --source");
|
|
69
|
+
const manifest = recordProvenance({
|
|
70
|
+
skillDir: requireSkill(values.skill),
|
|
71
|
+
sources,
|
|
72
|
+
compiler: COMPILER_IDENTITY,
|
|
73
|
+
mergeSources: values["merge-sources"],
|
|
74
|
+
});
|
|
75
|
+
writeJson({ status: "recorded", manifest });
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
function verify(args) {
|
|
79
|
+
const { values } = parseCommandOptions(args);
|
|
80
|
+
const skillDir = requireSkill(values.skill);
|
|
81
|
+
const provenance = verifyProvenance({ skillDir, sources: values.source });
|
|
82
|
+
const scanner = provenance.artifactIntegrity === "pass" ? runScanner(skillDir) : { status: "skipped" };
|
|
83
|
+
writeJson({ provenance, scanner });
|
|
84
|
+
if (provenance.status === "fail" || scanner.status === "fail")
|
|
85
|
+
return 1;
|
|
86
|
+
if (provenance.status === "inconclusive" || scanner.status !== "pass")
|
|
87
|
+
return 2;
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
function main() {
|
|
91
|
+
const [command, ...args] = process.argv.slice(2);
|
|
92
|
+
if (!command || command === "-h" || command === "--help") {
|
|
93
|
+
process.stdout.write(`${usage()}\n`);
|
|
94
|
+
return command ? 0 : 2;
|
|
95
|
+
}
|
|
96
|
+
if (command === "record")
|
|
97
|
+
return record(args);
|
|
98
|
+
if (command === "verify")
|
|
99
|
+
return verify(args);
|
|
100
|
+
throw new Error(`Unknown command: ${command}`);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
process.exitCode = main();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}\n`);
|
|
107
|
+
process.exitCode = 2;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { COMPILER_IDENTITY } from \"./metadata.ts\";\nimport { recordProvenance, verifyProvenance } from \"./provenance.ts\";\n\nconst PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst SCANNER_PATH = resolve(PACKAGE_ROOT, \"vendor\", \"book-to-skill\", \"tools\", \"scan_generated_skill.py\");\n\ninterface ScannerReport {\n\treadonly status: \"pass\" | \"fail\" | \"inconclusive\" | \"unavailable\" | \"skipped\";\n\treadonly command?: string;\n\treadonly stdout?: string;\n\treadonly stderr?: string;\n}\n\nfunction usage(): string {\n\treturn `Usage:\n omk-book-to-skill record --skill <dir> --source <file> [--source <file>...] [--merge-sources]\n omk-book-to-skill verify --skill <dir> [--source <file>...]\n\nrecord writes ${\".book-to-skill-provenance.json\"} after generation.\nverify checks artifact hashes, optionally rechecks exact source files, and runs the bundled advisory scanner.`;\n}\n\nfunction parseCommandOptions(args: readonly string[]) {\n\treturn parseArgs({\n\t\targs,\n\t\tallowPositionals: false,\n\t\tstrict: true,\n\t\toptions: {\n\t\t\tskill: { type: \"string\" },\n\t\t\tsource: { type: \"string\", multiple: true },\n\t\t\t\"merge-sources\": { type: \"boolean\", default: false },\n\t\t},\n\t});\n}\n\nfunction pythonCandidates(): string[] {\n\tconst configured = process.env.BOOK_TO_SKILL_PYTHON?.trim();\n\treturn [...new Set([configured, \"python3\", \"python\"].filter((value): value is string => Boolean(value)))];\n}\n\nfunction runScanner(skillDir: string): ScannerReport {\n\tif (!existsSync(SCANNER_PATH))\n\t\treturn { status: \"unavailable\", stderr: `Bundled scanner is missing: ${SCANNER_PATH}` };\n\tfor (const python of pythonCandidates()) {\n\t\tconst result = spawnSync(python, [SCANNER_PATH, skillDir], {\n\t\t\tencoding: \"utf8\",\n\t\t\tmaxBuffer: 10 * 1024 * 1024,\n\t\t\tshell: false,\n\t\t\ttimeout: 120_000,\n\t\t});\n\t\tif (result.error && \"code\" in result.error && result.error.code === \"ENOENT\") continue;\n\t\treturn {\n\t\t\tstatus: result.status === 0 ? \"pass\" : result.status === 1 ? \"fail\" : \"inconclusive\",\n\t\t\tcommand: python,\n\t\t\tstdout: result.stdout?.trim() || undefined,\n\t\t\tstderr: result.stderr?.trim() || result.error?.message,\n\t\t};\n\t}\n\treturn { status: \"unavailable\", stderr: \"Neither python3 nor python is available\" };\n}\n\nfunction writeJson(value: unknown): void {\n\tprocess.stdout.write(`${JSON.stringify(value, undefined, 2)}\\n`);\n}\n\nfunction requireSkill(value: string | undefined): string {\n\tif (!value) throw new Error(\"--skill is required\");\n\treturn resolve(value);\n}\n\nfunction record(args: readonly string[]): number {\n\tconst { values } = parseCommandOptions(args);\n\tconst sources = values.source ?? [];\n\tif (sources.length === 0 && !values[\"merge-sources\"]) throw new Error(\"record requires at least one --source\");\n\tconst manifest = recordProvenance({\n\t\tskillDir: requireSkill(values.skill),\n\t\tsources,\n\t\tcompiler: COMPILER_IDENTITY,\n\t\tmergeSources: values[\"merge-sources\"],\n\t});\n\twriteJson({ status: \"recorded\", manifest });\n\treturn 0;\n}\n\nfunction verify(args: readonly string[]): number {\n\tconst { values } = parseCommandOptions(args);\n\tconst skillDir = requireSkill(values.skill);\n\tconst provenance = verifyProvenance({ skillDir, sources: values.source });\n\tconst scanner = provenance.artifactIntegrity === \"pass\" ? runScanner(skillDir) : { status: \"skipped\" as const };\n\twriteJson({ provenance, scanner });\n\tif (provenance.status === \"fail\" || scanner.status === \"fail\") return 1;\n\tif (provenance.status === \"inconclusive\" || scanner.status !== \"pass\") return 2;\n\treturn 0;\n}\n\nfunction main(): number {\n\tconst [command, ...args] = process.argv.slice(2);\n\tif (!command || command === \"-h\" || command === \"--help\") {\n\t\tprocess.stdout.write(`${usage()}\\n`);\n\t\treturn command ? 0 : 2;\n\t}\n\tif (command === \"record\") return record(args);\n\tif (command === \"verify\") return verify(args);\n\tthrow new Error(`Unknown command: ${command}`);\n}\n\ntry {\n\tprocess.exitCode = main();\n} catch (error) {\n\tprocess.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n\\n${usage()}\\n`);\n\tprocess.exitCode = 2;\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "open-multi-agent-kit";
|
|
2
|
+
export declare function createCompilePrompt(args: string, skillPath?: string): string;
|
|
3
|
+
export declare function createUpdatePrompt(args: string, skillPath?: string): string;
|
|
4
|
+
export default function bookToSkillExtension(omk: ExtensionAPI): void;
|
|
5
|
+
//# sourceMappingURL=extension.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAc,YAAY,EAA2B,MAAM,sBAAsB,CAAC;AAiB9F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAa,GAAG,MAAM,CAEhF;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAa,GAAG,MAAM,CAE/E;AA6ED,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAepE","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExecResult, ExtensionAPI, ExtensionCommandContext } from \"open-multi-agent-kit\";\nimport { verifyProvenance } from \"./provenance.ts\";\n\nconst PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst SKILL_PATH = resolve(PACKAGE_ROOT, \"skills\", \"book-to-skill\", \"SKILL.md\");\nconst SCANNER_PATH = resolve(PACKAGE_ROOT, \"vendor\", \"book-to-skill\", \"tools\", \"scan_generated_skill.py\");\n\nfunction workflowPrompt(mode: \"Full Conversion\" | \"Update / Fold-in\", args: string, skillPath: string): string {\n\treturn [\n\t\t\"Use the bundled OMK book-to-skill workflow for this turn.\",\n\t\t`Read this file completely before acting: ${JSON.stringify(skillPath)}`,\n\t\t`Mode: ${mode}`,\n\t\t`User arguments: ${JSON.stringify(args)}`,\n\t\t\"Treat the arguments as user-supplied path/name data. Follow the skill's validation, confirmation, scan, and provenance gates.\",\n\t].join(\"\\n\");\n}\n\nexport function createCompilePrompt(args: string, skillPath = SKILL_PATH): string {\n\treturn workflowPrompt(\"Full Conversion\", args, skillPath);\n}\n\nexport function createUpdatePrompt(args: string, skillPath = SKILL_PATH): string {\n\treturn workflowPrompt(\"Update / Fold-in\", args, skillPath);\n}\n\nfunction notify(ctx: ExtensionCommandContext, message: string, level: \"info\" | \"warning\" | \"error\"): void {\n\tif (ctx.hasUI) ctx.ui.notify(message, level);\n}\n\nfunction dispatchWorkflow(\n\tomk: ExtensionAPI,\n\tctx: ExtensionCommandContext,\n\targs: string,\n\tusage: string,\n\tcreatePrompt: (args: string) => string,\n): void {\n\tconst trimmed = args.trim();\n\tif (!trimmed) {\n\t\tnotify(ctx, `Usage: ${usage}`, \"warning\");\n\t\treturn;\n\t}\n\tif (!ctx.isIdle()) {\n\t\tnotify(ctx, \"Agent is busy; retry when the current turn finishes.\", \"warning\");\n\t\treturn;\n\t}\n\tomk.sendUserMessage(createPrompt(trimmed));\n}\n\nfunction pythonCandidates(): string[] {\n\tconst configured = process.env.BOOK_TO_SKILL_PYTHON?.trim();\n\treturn [...new Set([configured, \"python3\", \"python\"].filter((value): value is string => Boolean(value)))];\n}\n\nasync function runScanner(omk: ExtensionAPI, skillDir: string, cwd: string): Promise<ExecResult | undefined> {\n\tif (!existsSync(SCANNER_PATH)) throw new Error(`Bundled scanner is missing: ${SCANNER_PATH}`);\n\tfor (const python of pythonCandidates()) {\n\t\tconst result = await omk.exec(python, [SCANNER_PATH, skillDir], { cwd, timeout: 120_000 });\n\t\tif (result.code === 1 && !result.stdout.trim() && !result.stderr.trim()) continue;\n\t\treturn result;\n\t}\n\treturn undefined;\n}\n\nfunction unquotePath(value: string): string {\n\tconst trimmed = value.trim();\n\tconst first = trimmed[0];\n\tif ((first === '\"' || first === \"'\") && trimmed.at(-1) === first) return trimmed.slice(1, -1);\n\treturn trimmed;\n}\n\nasync function verifySkill(omk: ExtensionAPI, args: string, ctx: ExtensionCommandContext): Promise<void> {\n\tconst requested = unquotePath(args);\n\tif (!requested) {\n\t\tnotify(ctx, \"Usage: /book-to-skill-verify <generated-skill-directory>\", \"warning\");\n\t\treturn;\n\t}\n\tconst skillDir = resolve(ctx.cwd, requested);\n\tconst report = verifyProvenance({ skillDir });\n\tif (report.artifactIntegrity === \"fail\") {\n\t\tconst summary = [...new Set(report.issues.map((item) => item.code))].slice(0, 3).join(\", \");\n\t\tnotify(ctx, `Provenance verification failed: ${summary || \"invalid manifest\"}`, \"error\");\n\t\treturn;\n\t}\n\n\tconst scanner = await runScanner(omk, skillDir, ctx.cwd);\n\tif (!scanner) {\n\t\tnotify(ctx, \"Artifact integrity passed, but Python was unavailable; advisory scan not verified.\", \"error\");\n\t\treturn;\n\t}\n\tif (scanner.code !== 0) {\n\t\tnotify(ctx, `Generated-skill scan failed with exit code ${scanner.code}; inspect it with the CLI.`, \"error\");\n\t\treturn;\n\t}\n\tnotify(\n\t\tctx,\n\t\t\"Artifact integrity and advisory scan passed. Source files were not rechecked; use the omk-book-to-skill CLI with --source for full provenance verification.\",\n\t\t\"info\",\n\t);\n}\n\nexport default function bookToSkillExtension(omk: ExtensionAPI): void {\n\tomk.registerCommand(\"book-to-skill-compile\", {\n\t\tdescription: \"Compile documents into an OMK skill with provenance\",\n\t\thandler: async (args, ctx) =>\n\t\t\tdispatchWorkflow(omk, ctx, args, \"/book-to-skill-compile <source...>\", createCompilePrompt),\n\t});\n\tomk.registerCommand(\"book-to-skill-update\", {\n\t\tdescription: \"Fold new documents into an existing generated skill\",\n\t\thandler: async (args, ctx) =>\n\t\t\tdispatchWorkflow(omk, ctx, args, \"/book-to-skill-update <skill> <source...>\", createUpdatePrompt),\n\t});\n\tomk.registerCommand(\"book-to-skill-verify\", {\n\t\tdescription: \"Verify generated-skill integrity and run the advisory scanner\",\n\t\thandler: async (args, ctx) => verifySkill(omk, args, ctx),\n\t});\n}\n"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { verifyProvenance } from "./provenance.js";
|
|
5
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const SKILL_PATH = resolve(PACKAGE_ROOT, "skills", "book-to-skill", "SKILL.md");
|
|
7
|
+
const SCANNER_PATH = resolve(PACKAGE_ROOT, "vendor", "book-to-skill", "tools", "scan_generated_skill.py");
|
|
8
|
+
function workflowPrompt(mode, args, skillPath) {
|
|
9
|
+
return [
|
|
10
|
+
"Use the bundled OMK book-to-skill workflow for this turn.",
|
|
11
|
+
`Read this file completely before acting: ${JSON.stringify(skillPath)}`,
|
|
12
|
+
`Mode: ${mode}`,
|
|
13
|
+
`User arguments: ${JSON.stringify(args)}`,
|
|
14
|
+
"Treat the arguments as user-supplied path/name data. Follow the skill's validation, confirmation, scan, and provenance gates.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
export function createCompilePrompt(args, skillPath = SKILL_PATH) {
|
|
18
|
+
return workflowPrompt("Full Conversion", args, skillPath);
|
|
19
|
+
}
|
|
20
|
+
export function createUpdatePrompt(args, skillPath = SKILL_PATH) {
|
|
21
|
+
return workflowPrompt("Update / Fold-in", args, skillPath);
|
|
22
|
+
}
|
|
23
|
+
function notify(ctx, message, level) {
|
|
24
|
+
if (ctx.hasUI)
|
|
25
|
+
ctx.ui.notify(message, level);
|
|
26
|
+
}
|
|
27
|
+
function dispatchWorkflow(omk, ctx, args, usage, createPrompt) {
|
|
28
|
+
const trimmed = args.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
notify(ctx, `Usage: ${usage}`, "warning");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!ctx.isIdle()) {
|
|
34
|
+
notify(ctx, "Agent is busy; retry when the current turn finishes.", "warning");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
omk.sendUserMessage(createPrompt(trimmed));
|
|
38
|
+
}
|
|
39
|
+
function pythonCandidates() {
|
|
40
|
+
const configured = process.env.BOOK_TO_SKILL_PYTHON?.trim();
|
|
41
|
+
return [...new Set([configured, "python3", "python"].filter((value) => Boolean(value)))];
|
|
42
|
+
}
|
|
43
|
+
async function runScanner(omk, skillDir, cwd) {
|
|
44
|
+
if (!existsSync(SCANNER_PATH))
|
|
45
|
+
throw new Error(`Bundled scanner is missing: ${SCANNER_PATH}`);
|
|
46
|
+
for (const python of pythonCandidates()) {
|
|
47
|
+
const result = await omk.exec(python, [SCANNER_PATH, skillDir], { cwd, timeout: 120_000 });
|
|
48
|
+
if (result.code === 1 && !result.stdout.trim() && !result.stderr.trim())
|
|
49
|
+
continue;
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
function unquotePath(value) {
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
const first = trimmed[0];
|
|
57
|
+
if ((first === '"' || first === "'") && trimmed.at(-1) === first)
|
|
58
|
+
return trimmed.slice(1, -1);
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
async function verifySkill(omk, args, ctx) {
|
|
62
|
+
const requested = unquotePath(args);
|
|
63
|
+
if (!requested) {
|
|
64
|
+
notify(ctx, "Usage: /book-to-skill-verify <generated-skill-directory>", "warning");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const skillDir = resolve(ctx.cwd, requested);
|
|
68
|
+
const report = verifyProvenance({ skillDir });
|
|
69
|
+
if (report.artifactIntegrity === "fail") {
|
|
70
|
+
const summary = [...new Set(report.issues.map((item) => item.code))].slice(0, 3).join(", ");
|
|
71
|
+
notify(ctx, `Provenance verification failed: ${summary || "invalid manifest"}`, "error");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const scanner = await runScanner(omk, skillDir, ctx.cwd);
|
|
75
|
+
if (!scanner) {
|
|
76
|
+
notify(ctx, "Artifact integrity passed, but Python was unavailable; advisory scan not verified.", "error");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (scanner.code !== 0) {
|
|
80
|
+
notify(ctx, `Generated-skill scan failed with exit code ${scanner.code}; inspect it with the CLI.`, "error");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
notify(ctx, "Artifact integrity and advisory scan passed. Source files were not rechecked; use the omk-book-to-skill CLI with --source for full provenance verification.", "info");
|
|
84
|
+
}
|
|
85
|
+
export default function bookToSkillExtension(omk) {
|
|
86
|
+
omk.registerCommand("book-to-skill-compile", {
|
|
87
|
+
description: "Compile documents into an OMK skill with provenance",
|
|
88
|
+
handler: async (args, ctx) => dispatchWorkflow(omk, ctx, args, "/book-to-skill-compile <source...>", createCompilePrompt),
|
|
89
|
+
});
|
|
90
|
+
omk.registerCommand("book-to-skill-update", {
|
|
91
|
+
description: "Fold new documents into an existing generated skill",
|
|
92
|
+
handler: async (args, ctx) => dispatchWorkflow(omk, ctx, args, "/book-to-skill-update <skill> <source...>", createUpdatePrompt),
|
|
93
|
+
});
|
|
94
|
+
omk.registerCommand("book-to-skill-verify", {
|
|
95
|
+
description: "Verify generated-skill integrity and run the advisory scanner",
|
|
96
|
+
handler: async (args, ctx) => verifySkill(omk, args, ctx),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=extension.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExecResult, ExtensionAPI, ExtensionCommandContext } from \"open-multi-agent-kit\";\nimport { verifyProvenance } from \"./provenance.ts\";\n\nconst PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst SKILL_PATH = resolve(PACKAGE_ROOT, \"skills\", \"book-to-skill\", \"SKILL.md\");\nconst SCANNER_PATH = resolve(PACKAGE_ROOT, \"vendor\", \"book-to-skill\", \"tools\", \"scan_generated_skill.py\");\n\nfunction workflowPrompt(mode: \"Full Conversion\" | \"Update / Fold-in\", args: string, skillPath: string): string {\n\treturn [\n\t\t\"Use the bundled OMK book-to-skill workflow for this turn.\",\n\t\t`Read this file completely before acting: ${JSON.stringify(skillPath)}`,\n\t\t`Mode: ${mode}`,\n\t\t`User arguments: ${JSON.stringify(args)}`,\n\t\t\"Treat the arguments as user-supplied path/name data. Follow the skill's validation, confirmation, scan, and provenance gates.\",\n\t].join(\"\\n\");\n}\n\nexport function createCompilePrompt(args: string, skillPath = SKILL_PATH): string {\n\treturn workflowPrompt(\"Full Conversion\", args, skillPath);\n}\n\nexport function createUpdatePrompt(args: string, skillPath = SKILL_PATH): string {\n\treturn workflowPrompt(\"Update / Fold-in\", args, skillPath);\n}\n\nfunction notify(ctx: ExtensionCommandContext, message: string, level: \"info\" | \"warning\" | \"error\"): void {\n\tif (ctx.hasUI) ctx.ui.notify(message, level);\n}\n\nfunction dispatchWorkflow(\n\tomk: ExtensionAPI,\n\tctx: ExtensionCommandContext,\n\targs: string,\n\tusage: string,\n\tcreatePrompt: (args: string) => string,\n): void {\n\tconst trimmed = args.trim();\n\tif (!trimmed) {\n\t\tnotify(ctx, `Usage: ${usage}`, \"warning\");\n\t\treturn;\n\t}\n\tif (!ctx.isIdle()) {\n\t\tnotify(ctx, \"Agent is busy; retry when the current turn finishes.\", \"warning\");\n\t\treturn;\n\t}\n\tomk.sendUserMessage(createPrompt(trimmed));\n}\n\nfunction pythonCandidates(): string[] {\n\tconst configured = process.env.BOOK_TO_SKILL_PYTHON?.trim();\n\treturn [...new Set([configured, \"python3\", \"python\"].filter((value): value is string => Boolean(value)))];\n}\n\nasync function runScanner(omk: ExtensionAPI, skillDir: string, cwd: string): Promise<ExecResult | undefined> {\n\tif (!existsSync(SCANNER_PATH)) throw new Error(`Bundled scanner is missing: ${SCANNER_PATH}`);\n\tfor (const python of pythonCandidates()) {\n\t\tconst result = await omk.exec(python, [SCANNER_PATH, skillDir], { cwd, timeout: 120_000 });\n\t\tif (result.code === 1 && !result.stdout.trim() && !result.stderr.trim()) continue;\n\t\treturn result;\n\t}\n\treturn undefined;\n}\n\nfunction unquotePath(value: string): string {\n\tconst trimmed = value.trim();\n\tconst first = trimmed[0];\n\tif ((first === '\"' || first === \"'\") && trimmed.at(-1) === first) return trimmed.slice(1, -1);\n\treturn trimmed;\n}\n\nasync function verifySkill(omk: ExtensionAPI, args: string, ctx: ExtensionCommandContext): Promise<void> {\n\tconst requested = unquotePath(args);\n\tif (!requested) {\n\t\tnotify(ctx, \"Usage: /book-to-skill-verify <generated-skill-directory>\", \"warning\");\n\t\treturn;\n\t}\n\tconst skillDir = resolve(ctx.cwd, requested);\n\tconst report = verifyProvenance({ skillDir });\n\tif (report.artifactIntegrity === \"fail\") {\n\t\tconst summary = [...new Set(report.issues.map((item) => item.code))].slice(0, 3).join(\", \");\n\t\tnotify(ctx, `Provenance verification failed: ${summary || \"invalid manifest\"}`, \"error\");\n\t\treturn;\n\t}\n\n\tconst scanner = await runScanner(omk, skillDir, ctx.cwd);\n\tif (!scanner) {\n\t\tnotify(ctx, \"Artifact integrity passed, but Python was unavailable; advisory scan not verified.\", \"error\");\n\t\treturn;\n\t}\n\tif (scanner.code !== 0) {\n\t\tnotify(ctx, `Generated-skill scan failed with exit code ${scanner.code}; inspect it with the CLI.`, \"error\");\n\t\treturn;\n\t}\n\tnotify(\n\t\tctx,\n\t\t\"Artifact integrity and advisory scan passed. Source files were not rechecked; use the omk-book-to-skill CLI with --source for full provenance verification.\",\n\t\t\"info\",\n\t);\n}\n\nexport default function bookToSkillExtension(omk: ExtensionAPI): void {\n\tomk.registerCommand(\"book-to-skill-compile\", {\n\t\tdescription: \"Compile documents into an OMK skill with provenance\",\n\t\thandler: async (args, ctx) =>\n\t\t\tdispatchWorkflow(omk, ctx, args, \"/book-to-skill-compile <source...>\", createCompilePrompt),\n\t});\n\tomk.registerCommand(\"book-to-skill-update\", {\n\t\tdescription: \"Fold new documents into an existing generated skill\",\n\t\thandler: async (args, ctx) =>\n\t\t\tdispatchWorkflow(omk, ctx, args, \"/book-to-skill-update <skill> <source...>\", createUpdatePrompt),\n\t});\n\tomk.registerCommand(\"book-to-skill-verify\", {\n\t\tdescription: \"Verify generated-skill integrity and run the advisory scanner\",\n\t\thandler: async (args, ctx) => verifySkill(omk, args, ctx),\n\t});\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ArtifactDigest, type SourceDigest } from "./types.ts";
|
|
2
|
+
export declare function digestSources(paths: readonly string[]): SourceDigest[];
|
|
3
|
+
export declare function collectArtifactDigests(skillDir: string): ArtifactDigest[];
|
|
4
|
+
//# sourceMappingURL=file-digests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-digests.d.ts","sourceRoot":"","sources":["../src/file-digests.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAwB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAyE1F,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,EAAE,CAKtE;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CA6BzE","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { closeSync, fstatSync, lstatSync, openSync, readdirSync, readSync, type Stats } from \"node:fs\";\nimport { basename, join, relative, sep } from \"node:path\";\nimport { type ArtifactDigest, PROVENANCE_FILE_NAME, type SourceDigest } from \"./types.ts\";\n\nconst MAX_ARTIFACT_FILES = 1_000;\nconst MAX_ARTIFACT_BYTES = 2 * 1024 * 1024;\nconst MAX_TOTAL_ARTIFACT_BYTES = 64 * 1024 * 1024;\nconst MAX_SOURCE_BYTES = 2 * 1024 * 1024 * 1024;\nconst HASH_BUFFER_BYTES = 64 * 1024;\n\nfunction regularFileStat(path: string, maxBytes: number) {\n\tconst stat = lstatSync(path);\n\tif (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link: ${path}`);\n\tif (!stat.isFile()) throw new Error(`Expected a regular file: ${path}`);\n\tif (!Number.isSafeInteger(stat.size) || stat.size > maxBytes) {\n\t\tthrow new Error(`File exceeds the ${maxBytes}-byte verification limit: ${path}`);\n\t}\n\treturn stat;\n}\n\nfunction sameFileIdentity(left: Stats, right: Stats): boolean {\n\treturn left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;\n}\n\nfunction hashFile(path: string, maxBytes: number): { bytes: number; sha256: string } {\n\tconst pathStat = regularFileStat(path, maxBytes);\n\tconst hash = createHash(\"sha256\");\n\tconst buffer = Buffer.allocUnsafe(HASH_BUFFER_BYTES);\n\tconst descriptor = openSync(path, \"r\");\n\tlet openedStat: ReturnType<typeof fstatSync>;\n\ttry {\n\t\topenedStat = fstatSync(descriptor);\n\t\tif (!openedStat.isFile() || !sameFileIdentity(pathStat, openedStat) || openedStat.size !== pathStat.size) {\n\t\t\tthrow new Error(`File changed while opening: ${path}`);\n\t\t}\n\t\tlet totalBytes = 0;\n\t\tlet bytesRead = 0;\n\t\tdo {\n\t\t\tbytesRead = readSync(descriptor, buffer, 0, buffer.length, null);\n\t\t\tif (bytesRead > 0) {\n\t\t\t\ttotalBytes += bytesRead;\n\t\t\t\tif (totalBytes > maxBytes) throw new Error(`File changed beyond the verification limit: ${path}`);\n\t\t\t\thash.update(buffer.subarray(0, bytesRead));\n\t\t\t}\n\t\t} while (bytesRead > 0);\n\t\tconst finalStat = fstatSync(descriptor);\n\t\tif (\n\t\t\ttotalBytes !== openedStat.size ||\n\t\t\t!sameFileIdentity(openedStat, finalStat) ||\n\t\t\tfinalStat.size !== openedStat.size ||\n\t\t\tfinalStat.mtimeMs !== openedStat.mtimeMs ||\n\t\t\tfinalStat.ctimeMs !== openedStat.ctimeMs\n\t\t) {\n\t\t\tthrow new Error(`File changed while hashing: ${path}`);\n\t\t}\n\t} finally {\n\t\tcloseSync(descriptor);\n\t}\n\tconst finalPathStat = regularFileStat(path, maxBytes);\n\tif (!sameFileIdentity(openedStat, finalPathStat) || finalPathStat.size !== openedStat.size) {\n\t\tthrow new Error(`File path changed while hashing: ${path}`);\n\t}\n\treturn { bytes: openedStat.size, sha256: hash.digest(\"hex\") };\n}\n\nfunction toPosixRelative(root: string, path: string): string {\n\treturn relative(root, path).split(sep).join(\"/\");\n}\n\nfunction assertSkillDirectory(skillDir: string): void {\n\tconst stat = lstatSync(skillDir);\n\tif (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link skill directory: ${skillDir}`);\n\tif (!stat.isDirectory()) throw new Error(`Expected a skill directory: ${skillDir}`);\n}\n\nexport function digestSources(paths: readonly string[]): SourceDigest[] {\n\treturn paths.map((path, index) => {\n\t\tconst digest = hashFile(path, MAX_SOURCE_BYTES);\n\t\treturn Object.freeze({ id: `source-${index + 1}`, name: basename(path), ...digest });\n\t});\n}\n\nexport function collectArtifactDigests(skillDir: string): ArtifactDigest[] {\n\tassertSkillDirectory(skillDir);\n\tconst files: string[] = [];\n\tconst visit = (directory: string): void => {\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t)) {\n\t\t\tconst path = join(directory, entry.name);\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link artifact: ${path}`);\n\t\t\tif (stat.isDirectory()) visit(path);\n\t\t\telse if (stat.isFile() && entry.name !== PROVENANCE_FILE_NAME) files.push(path);\n\t\t\telse if (!stat.isFile()) throw new Error(`Unsupported artifact type: ${path}`);\n\t\t}\n\t};\n\tvisit(skillDir);\n\tif (files.length > MAX_ARTIFACT_FILES) throw new Error(`Skill exceeds ${MAX_ARTIFACT_FILES} artifact files`);\n\n\tlet totalBytes = 0;\n\tconst digests = files.map((path) => {\n\t\tconst digest = hashFile(path, MAX_ARTIFACT_BYTES);\n\t\ttotalBytes += digest.bytes;\n\t\treturn Object.freeze({ path: toPosixRelative(skillDir, path), ...digest });\n\t});\n\tif (totalBytes > MAX_TOTAL_ARTIFACT_BYTES) {\n\t\tthrow new Error(`Skill artifacts exceed ${MAX_TOTAL_ARTIFACT_BYTES} bytes`);\n\t}\n\tif (!digests.some((entry) => entry.path === \"SKILL.md\")) throw new Error(\"Generated skill is missing SKILL.md\");\n\treturn digests.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, fstatSync, lstatSync, openSync, readdirSync, readSync } from "node:fs";
|
|
3
|
+
import { basename, join, relative, sep } from "node:path";
|
|
4
|
+
import { PROVENANCE_FILE_NAME } from "./types.js";
|
|
5
|
+
const MAX_ARTIFACT_FILES = 1_000;
|
|
6
|
+
const MAX_ARTIFACT_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const MAX_TOTAL_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
const MAX_SOURCE_BYTES = 2 * 1024 * 1024 * 1024;
|
|
9
|
+
const HASH_BUFFER_BYTES = 64 * 1024;
|
|
10
|
+
function regularFileStat(path, maxBytes) {
|
|
11
|
+
const stat = lstatSync(path);
|
|
12
|
+
if (stat.isSymbolicLink())
|
|
13
|
+
throw new Error(`Refusing symbolic link: ${path}`);
|
|
14
|
+
if (!stat.isFile())
|
|
15
|
+
throw new Error(`Expected a regular file: ${path}`);
|
|
16
|
+
if (!Number.isSafeInteger(stat.size) || stat.size > maxBytes) {
|
|
17
|
+
throw new Error(`File exceeds the ${maxBytes}-byte verification limit: ${path}`);
|
|
18
|
+
}
|
|
19
|
+
return stat;
|
|
20
|
+
}
|
|
21
|
+
function sameFileIdentity(left, right) {
|
|
22
|
+
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;
|
|
23
|
+
}
|
|
24
|
+
function hashFile(path, maxBytes) {
|
|
25
|
+
const pathStat = regularFileStat(path, maxBytes);
|
|
26
|
+
const hash = createHash("sha256");
|
|
27
|
+
const buffer = Buffer.allocUnsafe(HASH_BUFFER_BYTES);
|
|
28
|
+
const descriptor = openSync(path, "r");
|
|
29
|
+
let openedStat;
|
|
30
|
+
try {
|
|
31
|
+
openedStat = fstatSync(descriptor);
|
|
32
|
+
if (!openedStat.isFile() || !sameFileIdentity(pathStat, openedStat) || openedStat.size !== pathStat.size) {
|
|
33
|
+
throw new Error(`File changed while opening: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
let totalBytes = 0;
|
|
36
|
+
let bytesRead = 0;
|
|
37
|
+
do {
|
|
38
|
+
bytesRead = readSync(descriptor, buffer, 0, buffer.length, null);
|
|
39
|
+
if (bytesRead > 0) {
|
|
40
|
+
totalBytes += bytesRead;
|
|
41
|
+
if (totalBytes > maxBytes)
|
|
42
|
+
throw new Error(`File changed beyond the verification limit: ${path}`);
|
|
43
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
44
|
+
}
|
|
45
|
+
} while (bytesRead > 0);
|
|
46
|
+
const finalStat = fstatSync(descriptor);
|
|
47
|
+
if (totalBytes !== openedStat.size ||
|
|
48
|
+
!sameFileIdentity(openedStat, finalStat) ||
|
|
49
|
+
finalStat.size !== openedStat.size ||
|
|
50
|
+
finalStat.mtimeMs !== openedStat.mtimeMs ||
|
|
51
|
+
finalStat.ctimeMs !== openedStat.ctimeMs) {
|
|
52
|
+
throw new Error(`File changed while hashing: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeSync(descriptor);
|
|
57
|
+
}
|
|
58
|
+
const finalPathStat = regularFileStat(path, maxBytes);
|
|
59
|
+
if (!sameFileIdentity(openedStat, finalPathStat) || finalPathStat.size !== openedStat.size) {
|
|
60
|
+
throw new Error(`File path changed while hashing: ${path}`);
|
|
61
|
+
}
|
|
62
|
+
return { bytes: openedStat.size, sha256: hash.digest("hex") };
|
|
63
|
+
}
|
|
64
|
+
function toPosixRelative(root, path) {
|
|
65
|
+
return relative(root, path).split(sep).join("/");
|
|
66
|
+
}
|
|
67
|
+
function assertSkillDirectory(skillDir) {
|
|
68
|
+
const stat = lstatSync(skillDir);
|
|
69
|
+
if (stat.isSymbolicLink())
|
|
70
|
+
throw new Error(`Refusing symbolic link skill directory: ${skillDir}`);
|
|
71
|
+
if (!stat.isDirectory())
|
|
72
|
+
throw new Error(`Expected a skill directory: ${skillDir}`);
|
|
73
|
+
}
|
|
74
|
+
export function digestSources(paths) {
|
|
75
|
+
return paths.map((path, index) => {
|
|
76
|
+
const digest = hashFile(path, MAX_SOURCE_BYTES);
|
|
77
|
+
return Object.freeze({ id: `source-${index + 1}`, name: basename(path), ...digest });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export function collectArtifactDigests(skillDir) {
|
|
81
|
+
assertSkillDirectory(skillDir);
|
|
82
|
+
const files = [];
|
|
83
|
+
const visit = (directory) => {
|
|
84
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
85
|
+
const path = join(directory, entry.name);
|
|
86
|
+
const stat = lstatSync(path);
|
|
87
|
+
if (stat.isSymbolicLink())
|
|
88
|
+
throw new Error(`Refusing symbolic link artifact: ${path}`);
|
|
89
|
+
if (stat.isDirectory())
|
|
90
|
+
visit(path);
|
|
91
|
+
else if (stat.isFile() && entry.name !== PROVENANCE_FILE_NAME)
|
|
92
|
+
files.push(path);
|
|
93
|
+
else if (!stat.isFile())
|
|
94
|
+
throw new Error(`Unsupported artifact type: ${path}`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
visit(skillDir);
|
|
98
|
+
if (files.length > MAX_ARTIFACT_FILES)
|
|
99
|
+
throw new Error(`Skill exceeds ${MAX_ARTIFACT_FILES} artifact files`);
|
|
100
|
+
let totalBytes = 0;
|
|
101
|
+
const digests = files.map((path) => {
|
|
102
|
+
const digest = hashFile(path, MAX_ARTIFACT_BYTES);
|
|
103
|
+
totalBytes += digest.bytes;
|
|
104
|
+
return Object.freeze({ path: toPosixRelative(skillDir, path), ...digest });
|
|
105
|
+
});
|
|
106
|
+
if (totalBytes > MAX_TOTAL_ARTIFACT_BYTES) {
|
|
107
|
+
throw new Error(`Skill artifacts exceed ${MAX_TOTAL_ARTIFACT_BYTES} bytes`);
|
|
108
|
+
}
|
|
109
|
+
if (!digests.some((entry) => entry.path === "SKILL.md"))
|
|
110
|
+
throw new Error("Generated skill is missing SKILL.md");
|
|
111
|
+
return digests.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=file-digests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-digests.js","sourceRoot":"","sources":["../src/file-digests.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { closeSync, fstatSync, lstatSync, openSync, readdirSync, readSync, type Stats } from \"node:fs\";\nimport { basename, join, relative, sep } from \"node:path\";\nimport { type ArtifactDigest, PROVENANCE_FILE_NAME, type SourceDigest } from \"./types.ts\";\n\nconst MAX_ARTIFACT_FILES = 1_000;\nconst MAX_ARTIFACT_BYTES = 2 * 1024 * 1024;\nconst MAX_TOTAL_ARTIFACT_BYTES = 64 * 1024 * 1024;\nconst MAX_SOURCE_BYTES = 2 * 1024 * 1024 * 1024;\nconst HASH_BUFFER_BYTES = 64 * 1024;\n\nfunction regularFileStat(path: string, maxBytes: number) {\n\tconst stat = lstatSync(path);\n\tif (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link: ${path}`);\n\tif (!stat.isFile()) throw new Error(`Expected a regular file: ${path}`);\n\tif (!Number.isSafeInteger(stat.size) || stat.size > maxBytes) {\n\t\tthrow new Error(`File exceeds the ${maxBytes}-byte verification limit: ${path}`);\n\t}\n\treturn stat;\n}\n\nfunction sameFileIdentity(left: Stats, right: Stats): boolean {\n\treturn left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;\n}\n\nfunction hashFile(path: string, maxBytes: number): { bytes: number; sha256: string } {\n\tconst pathStat = regularFileStat(path, maxBytes);\n\tconst hash = createHash(\"sha256\");\n\tconst buffer = Buffer.allocUnsafe(HASH_BUFFER_BYTES);\n\tconst descriptor = openSync(path, \"r\");\n\tlet openedStat: ReturnType<typeof fstatSync>;\n\ttry {\n\t\topenedStat = fstatSync(descriptor);\n\t\tif (!openedStat.isFile() || !sameFileIdentity(pathStat, openedStat) || openedStat.size !== pathStat.size) {\n\t\t\tthrow new Error(`File changed while opening: ${path}`);\n\t\t}\n\t\tlet totalBytes = 0;\n\t\tlet bytesRead = 0;\n\t\tdo {\n\t\t\tbytesRead = readSync(descriptor, buffer, 0, buffer.length, null);\n\t\t\tif (bytesRead > 0) {\n\t\t\t\ttotalBytes += bytesRead;\n\t\t\t\tif (totalBytes > maxBytes) throw new Error(`File changed beyond the verification limit: ${path}`);\n\t\t\t\thash.update(buffer.subarray(0, bytesRead));\n\t\t\t}\n\t\t} while (bytesRead > 0);\n\t\tconst finalStat = fstatSync(descriptor);\n\t\tif (\n\t\t\ttotalBytes !== openedStat.size ||\n\t\t\t!sameFileIdentity(openedStat, finalStat) ||\n\t\t\tfinalStat.size !== openedStat.size ||\n\t\t\tfinalStat.mtimeMs !== openedStat.mtimeMs ||\n\t\t\tfinalStat.ctimeMs !== openedStat.ctimeMs\n\t\t) {\n\t\t\tthrow new Error(`File changed while hashing: ${path}`);\n\t\t}\n\t} finally {\n\t\tcloseSync(descriptor);\n\t}\n\tconst finalPathStat = regularFileStat(path, maxBytes);\n\tif (!sameFileIdentity(openedStat, finalPathStat) || finalPathStat.size !== openedStat.size) {\n\t\tthrow new Error(`File path changed while hashing: ${path}`);\n\t}\n\treturn { bytes: openedStat.size, sha256: hash.digest(\"hex\") };\n}\n\nfunction toPosixRelative(root: string, path: string): string {\n\treturn relative(root, path).split(sep).join(\"/\");\n}\n\nfunction assertSkillDirectory(skillDir: string): void {\n\tconst stat = lstatSync(skillDir);\n\tif (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link skill directory: ${skillDir}`);\n\tif (!stat.isDirectory()) throw new Error(`Expected a skill directory: ${skillDir}`);\n}\n\nexport function digestSources(paths: readonly string[]): SourceDigest[] {\n\treturn paths.map((path, index) => {\n\t\tconst digest = hashFile(path, MAX_SOURCE_BYTES);\n\t\treturn Object.freeze({ id: `source-${index + 1}`, name: basename(path), ...digest });\n\t});\n}\n\nexport function collectArtifactDigests(skillDir: string): ArtifactDigest[] {\n\tassertSkillDirectory(skillDir);\n\tconst files: string[] = [];\n\tconst visit = (directory: string): void => {\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t)) {\n\t\t\tconst path = join(directory, entry.name);\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link artifact: ${path}`);\n\t\t\tif (stat.isDirectory()) visit(path);\n\t\t\telse if (stat.isFile() && entry.name !== PROVENANCE_FILE_NAME) files.push(path);\n\t\t\telse if (!stat.isFile()) throw new Error(`Unsupported artifact type: ${path}`);\n\t\t}\n\t};\n\tvisit(skillDir);\n\tif (files.length > MAX_ARTIFACT_FILES) throw new Error(`Skill exceeds ${MAX_ARTIFACT_FILES} artifact files`);\n\n\tlet totalBytes = 0;\n\tconst digests = files.map((path) => {\n\t\tconst digest = hashFile(path, MAX_ARTIFACT_BYTES);\n\t\ttotalBytes += digest.bytes;\n\t\treturn Object.freeze({ path: toPosixRelative(skillDir, path), ...digest });\n\t});\n\tif (totalBytes > MAX_TOTAL_ARTIFACT_BYTES) {\n\t\tthrow new Error(`Skill artifacts exceed ${MAX_TOTAL_ARTIFACT_BYTES} bytes`);\n\t}\n\tif (!digests.some((entry) => entry.path === \"SKILL.md\")) throw new Error(\"Generated skill is missing SKILL.md\");\n\treturn digests.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseProvenanceManifest } from "./manifest.ts";
|
|
2
|
+
export { COMPILER_IDENTITY, PACKAGE_VERSION } from "./metadata.ts";
|
|
3
|
+
export { PROVENANCE_FILE_NAME, PROVENANCE_SCHEMA_VERSION, recordProvenance, verifyProvenance, } from "./provenance.ts";
|
|
4
|
+
export type { ArtifactDigest, CompilerIdentity, ProvenanceManifest, RecordProvenanceOptions, SourceDigest, UpstreamIdentity, VerificationIssue, VerificationReport, VerifyProvenanceOptions, } from "./types.ts";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EACN,oBAAoB,EACpB,yBAAyB,EACzB,gBAAgB,EAChB,gBAAgB,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACX,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,uBAAuB,EACvB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,GACvB,MAAM,YAAY,CAAC","sourcesContent":["export { parseProvenanceManifest } from \"./manifest.ts\";\nexport { COMPILER_IDENTITY, PACKAGE_VERSION } from \"./metadata.ts\";\nexport {\n\tPROVENANCE_FILE_NAME,\n\tPROVENANCE_SCHEMA_VERSION,\n\trecordProvenance,\n\tverifyProvenance,\n} from \"./provenance.ts\";\nexport type {\n\tArtifactDigest,\n\tCompilerIdentity,\n\tProvenanceManifest,\n\tRecordProvenanceOptions,\n\tSourceDigest,\n\tUpstreamIdentity,\n\tVerificationIssue,\n\tVerificationReport,\n\tVerifyProvenanceOptions,\n} from \"./types.ts\";\n"]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { parseProvenanceManifest } from "./manifest.js";
|
|
2
|
+
export { COMPILER_IDENTITY, PACKAGE_VERSION } from "./metadata.js";
|
|
3
|
+
export { PROVENANCE_FILE_NAME, PROVENANCE_SCHEMA_VERSION, recordProvenance, verifyProvenance, } from "./provenance.js";
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EACN,oBAAoB,EACpB,yBAAyB,EACzB,gBAAgB,EAChB,gBAAgB,GAChB,MAAM,iBAAiB,CAAC","sourcesContent":["export { parseProvenanceManifest } from \"./manifest.ts\";\nexport { COMPILER_IDENTITY, PACKAGE_VERSION } from \"./metadata.ts\";\nexport {\n\tPROVENANCE_FILE_NAME,\n\tPROVENANCE_SCHEMA_VERSION,\n\trecordProvenance,\n\tverifyProvenance,\n} from \"./provenance.ts\";\nexport type {\n\tArtifactDigest,\n\tCompilerIdentity,\n\tProvenanceManifest,\n\tRecordProvenanceOptions,\n\tSourceDigest,\n\tUpstreamIdentity,\n\tVerificationIssue,\n\tVerificationReport,\n\tVerifyProvenanceOptions,\n} from \"./types.ts\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,kBAAkB,EAGvB,MAAM,YAAY,CAAC;AA2FpB,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CA+B1E","sourcesContent":["import {\n\ttype ArtifactDigest,\n\ttype CompilerIdentity,\n\tPROVENANCE_SCHEMA_VERSION,\n\ttype ProvenanceManifest,\n\ttype SourceDigest,\n\ttype UpstreamIdentity,\n} from \"./types.ts\";\n\nconst SHA256_PATTERN = /^[a-f0-9]{64}$/;\nconst COMMIT_PATTERN = /^[a-f0-9]{40}$/;\n\nfunction record(value: unknown, label: string): Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value))\n\t\tthrow new Error(`${label} must be an object`);\n\treturn value as Record<string, unknown>;\n}\n\nfunction string(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || value.length === 0) throw new Error(`${label} must be a non-empty string`);\n\treturn value;\n}\n\nfunction bytes(value: unknown, label: string): number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 0) {\n\t\tthrow new Error(`${label} must be a non-negative safe integer`);\n\t}\n\treturn value;\n}\n\nfunction exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string): void {\n\tconst actual = Object.keys(value).sort();\n\tconst wanted = [...expected].sort();\n\tif (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {\n\t\tthrow new Error(`${label} has unexpected or missing fields`);\n\t}\n}\n\nfunction parseUpstream(value: unknown): UpstreamIdentity {\n\tconst input = record(value, \"compiler.upstream\");\n\texactKeys(input, [\"repository\", \"commit\", \"declaredVersion\"], \"compiler.upstream\");\n\tconst commit = string(input.commit, \"compiler.upstream.commit\");\n\tif (!COMMIT_PATTERN.test(commit)) throw new Error(\"compiler.upstream.commit must be a 40-character Git commit\");\n\treturn Object.freeze({\n\t\trepository: string(input.repository, \"compiler.upstream.repository\"),\n\t\tcommit,\n\t\tdeclaredVersion: string(input.declaredVersion, \"compiler.upstream.declaredVersion\"),\n\t});\n}\n\nfunction parseCompiler(value: unknown): CompilerIdentity {\n\tconst input = record(value, \"compiler\");\n\texactKeys(input, [\"package\", \"version\", \"upstream\"], \"compiler\");\n\tif (input.package !== \"omk-book-to-skill\") throw new Error(\"compiler.package must be omk-book-to-skill\");\n\treturn Object.freeze({\n\t\tpackage: \"omk-book-to-skill\",\n\t\tversion: string(input.version, \"compiler.version\"),\n\t\tupstream: parseUpstream(input.upstream),\n\t});\n}\n\nfunction parseSha(value: unknown, label: string): string {\n\tconst digest = string(value, label);\n\tif (!SHA256_PATTERN.test(digest)) throw new Error(`${label} must be a lowercase SHA-256 digest`);\n\treturn digest;\n}\n\nfunction parseSource(value: unknown, index: number): SourceDigest {\n\tconst input = record(value, `sources[${index}]`);\n\texactKeys(input, [\"id\", \"name\", \"sha256\", \"bytes\"], `sources[${index}]`);\n\tconst name = string(input.name, `sources[${index}].name`);\n\tif (name.includes(\"/\") || name.includes(\"\\\\\")) throw new Error(`sources[${index}].name must be a basename`);\n\treturn Object.freeze({\n\t\tid: string(input.id, `sources[${index}].id`),\n\t\tname,\n\t\tsha256: parseSha(input.sha256, `sources[${index}].sha256`),\n\t\tbytes: bytes(input.bytes, `sources[${index}].bytes`),\n\t});\n}\n\nfunction parseArtifact(value: unknown, index: number): ArtifactDigest {\n\tconst input = record(value, `artifacts[${index}]`);\n\texactKeys(input, [\"path\", \"sha256\", \"bytes\"], `artifacts[${index}]`);\n\tconst path = string(input.path, `artifacts[${index}].path`);\n\tif (path.startsWith(\"/\") || path.includes(\"\\\\\") || path.split(\"/\").includes(\"..\")) {\n\t\tthrow new Error(`artifacts[${index}].path must stay inside the skill directory`);\n\t}\n\treturn Object.freeze({\n\t\tpath,\n\t\tsha256: parseSha(input.sha256, `artifacts[${index}].sha256`),\n\t\tbytes: bytes(input.bytes, `artifacts[${index}].bytes`),\n\t});\n}\n\nfunction unique(values: readonly string[], label: string): void {\n\tif (new Set(values).size !== values.length) throw new Error(`${label} must not contain duplicates`);\n}\n\nexport function parseProvenanceManifest(value: unknown): ProvenanceManifest {\n\tconst input = record(value, \"manifest\");\n\texactKeys(input, [\"schemaVersion\", \"compiler\", \"recordedAt\", \"sources\", \"artifacts\"], \"manifest\");\n\tif (input.schemaVersion !== PROVENANCE_SCHEMA_VERSION)\n\t\tthrow new Error(`Unsupported schemaVersion: ${String(input.schemaVersion)}`);\n\tconst recordedAt = string(input.recordedAt, \"recordedAt\");\n\tif (!Number.isFinite(Date.parse(recordedAt)) || new Date(recordedAt).toISOString() !== recordedAt) {\n\t\tthrow new Error(\"recordedAt must be a canonical ISO timestamp\");\n\t}\n\tif (!Array.isArray(input.sources) || input.sources.length === 0)\n\t\tthrow new Error(\"sources must be a non-empty array\");\n\tif (!Array.isArray(input.artifacts) || input.artifacts.length === 0)\n\t\tthrow new Error(\"artifacts must be a non-empty array\");\n\tconst sources = input.sources.map(parseSource);\n\tconst artifacts = input.artifacts.map(parseArtifact);\n\tunique(\n\t\tsources.map((entry) => entry.id),\n\t\t\"source ids\",\n\t);\n\tunique(\n\t\tartifacts.map((entry) => entry.path),\n\t\t\"artifact paths\",\n\t);\n\tif (!artifacts.some((entry) => entry.path === \"SKILL.md\")) throw new Error(\"artifacts must include SKILL.md\");\n\treturn Object.freeze({\n\t\tschemaVersion: PROVENANCE_SCHEMA_VERSION,\n\t\tcompiler: parseCompiler(input.compiler),\n\t\trecordedAt,\n\t\tsources: Object.freeze(sources),\n\t\tartifacts: Object.freeze(artifacts),\n\t});\n}\n"]}
|