@rckflr/llms-skills 0.1.0 → 0.2.1
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/CHANGELOG.md +49 -0
- package/README.md +27 -0
- package/bin/llms-skills.mjs +50 -1
- package/lib/core.mjs +10 -1
- package/lib/memory.mjs +260 -0
- package/package.json +19 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the [`@rckflr/llms-skills`](https://www.npmjs.com/package/@rckflr/llms-skills)
|
|
4
|
+
package. Format based on [Keep a Changelog](https://keepachangelog.com/); dates
|
|
5
|
+
are the npm publish dates.
|
|
6
|
+
|
|
7
|
+
## [0.2.0] — 2026-07-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`memory <bundle-dir>` — the RAG-OKF builder.** Turns a directory of
|
|
11
|
+
[OKF 0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
|
|
12
|
+
concepts (`*.md` with `type`/`title` frontmatter; `index.md`/`log.md`
|
|
13
|
+
reserved) into a serverless RAG:
|
|
14
|
+
- a **byte-deterministic** BM25 snapshot (`minimemory-okf-v1`), pinned by
|
|
15
|
+
`snapshot_sha256` in the `skills-memory` line. Determinism requires
|
|
16
|
+
canonicalization: minimemory's export order comes from a Rust HashMap and
|
|
17
|
+
varies run to run — chunks are sorted by id and re-serialized, which is
|
|
18
|
+
what makes `--check`/CI content-addressing possible at all;
|
|
19
|
+
- three generated executable skills: `search_knowledge` (a **universal
|
|
20
|
+
template** — identical bytes for every publisher, so its `tool_sha256` is
|
|
21
|
+
a stable ecosystem-wide constant: audit once, attest once), `get_concept`
|
|
22
|
+
and `list_concepts` (concept metadata embedded at build time,
|
|
23
|
+
content-addressed);
|
|
24
|
+
- manifest wiring (`memory` block + `published` entries) so a plain
|
|
25
|
+
`publish` emits everything, and a `.gitattributes` pin
|
|
26
|
+
(`skills-index.snapshot -text`) so git CRLF conversion can never break
|
|
27
|
+
the published hash.
|
|
28
|
+
- `memory <bundle> --check` is the CI drift guard. Consumers need nothing
|
|
29
|
+
new: `@rckflr/mcpwasm` ≥ 0.4.0 verifies the snapshot and injects
|
|
30
|
+
`host.memorySearch` on both runtimes (covered by a real end-to-end test
|
|
31
|
+
against the published mcpwasm tarball).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `@rckflr/minimemory` (~630 KB wasm BM25 engine) added as an
|
|
35
|
+
**optionalDependency**: installed by default, lazily imported by `memory`;
|
|
36
|
+
if omitted, `memory` fails with a clear message and every other command
|
|
37
|
+
works exactly as before (the CLI keeps zero hard dependencies).
|
|
38
|
+
|
|
39
|
+
## [0.1.0] — 2026-07-10
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- Initial release: one-command publisher CLI for the llms.txt Skills
|
|
43
|
+
standard — `init` (scaffold SKILL.md + optional tool.js + manifest),
|
|
44
|
+
`publish [--check]` (sha256, `## Skills` into llms.txt,
|
|
45
|
+
`.well-known/agent-skills/index.json`, ed25519 signing), `validate`
|
|
46
|
+
(llms.txt + skills, local path or URL), `keygen`. Zero dependencies
|
|
47
|
+
(hashing and ed25519 via `node:crypto`); output **byte-identical** to the
|
|
48
|
+
reference Python generator (`scripts/generate.py`), enforced by
|
|
49
|
+
`cli/test.mjs` in CI.
|
package/README.md
CHANGED
|
@@ -47,6 +47,33 @@ bridge out is the host capabilities the runtime injects (`host.fetchOrigin`,
|
|
|
47
47
|
scoped to your origin). `publish` computes `tool_sha256` and carries `tool` +
|
|
48
48
|
`tool_sha256` in both the inline metadata and `index.json`.
|
|
49
49
|
|
|
50
|
+
## Knowledge / serverless RAG (`memory`)
|
|
51
|
+
|
|
52
|
+
Turn a directory of [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
|
|
53
|
+
concepts (`*.md` with `type`/`title` frontmatter; `index.md`/`log.md` reserved)
|
|
54
|
+
into a serverless RAG any consumer can query — no vector DB, no server:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @rckflr/llms-skills memory ./knowledge # snapshot + 3 knowledge skills + manifest wiring
|
|
58
|
+
npx @rckflr/llms-skills publish # as always
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
What it generates: a **byte-deterministic** BM25 snapshot (canonicalized, so
|
|
62
|
+
`--check`/CI content-addressing works), pinned by `snapshot_sha256` in the
|
|
63
|
+
`skills-memory` line, plus three executable skills — `search_knowledge`
|
|
64
|
+
(a **universal template**: identical bytes for every publisher, so its
|
|
65
|
+
`tool_sha256` is a stable ecosystem-wide constant — audit once, attest once),
|
|
66
|
+
`get_concept`, and `list_concepts` (concept metadata embedded at build time,
|
|
67
|
+
content-addressed). It also pins the snapshot as `-text` in `.gitattributes`
|
|
68
|
+
so git's CRLF conversion can never break the published hash.
|
|
69
|
+
|
|
70
|
+
Consumers need nothing new: `npx -y @rckflr/mcpwasm <origin>` (>= 0.4.0)
|
|
71
|
+
verifies the snapshot and injects `host.memorySearch` on both runtimes.
|
|
72
|
+
`memory <bundle> --check` is the CI guard. The BM25 engine
|
|
73
|
+
(`@rckflr/minimemory`, ~630 KB wasm) is an optionalDependency — installed by
|
|
74
|
+
default; if omitted, `memory` fails with a clear message and every other
|
|
75
|
+
command works as before.
|
|
76
|
+
|
|
50
77
|
## Signing / attestation (L3)
|
|
51
78
|
|
|
52
79
|
```bash
|
package/bin/llms-skills.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
loadSkills, loadMemory, renderSkillsSection, renderSkillsMemoryLine, renderLlmsTxt,
|
|
17
17
|
renderIndex, loadSigningKey, signSkills, publicKeyRawB64, generateKeyPair, validateLlmsTxt,
|
|
18
18
|
} from "../lib/core.mjs";
|
|
19
|
+
import { buildMemoryTargets } from "../lib/memory.mjs";
|
|
19
20
|
|
|
20
21
|
const args = process.argv.slice(2);
|
|
21
22
|
const cmd = args[0];
|
|
@@ -28,12 +29,17 @@ const USAGE = `llms-skills — publisher CLI for the llms.txt Skills standard
|
|
|
28
29
|
|
|
29
30
|
Usage:
|
|
30
31
|
llms-skills init <name> [--tool] [--root <dir>] scaffold SKILL.md (+ tool.js) + manifest entry
|
|
32
|
+
llms-skills memory <bundle-dir> [--check] [--root <dir>] [--license <spdx>]
|
|
33
|
+
OKF bundle -> BM25 snapshot + knowledge skills (RAG)
|
|
31
34
|
llms-skills publish [--check] [--manifest <p>] [--root <dir>] generate llms.txt ## Skills + index.json
|
|
32
35
|
llms-skills validate <src> [--strict] validate an llms.txt (path or URL)
|
|
33
36
|
llms-skills keygen [--out <keyfile>] generate an ed25519 signing keypair
|
|
34
37
|
|
|
35
38
|
The minimal case (L0) needs no signing key and no tool.js — just init + publish.
|
|
36
|
-
Add --tool for an executable skill; add a "signing" block to the manifest for L3 attestation
|
|
39
|
+
Add --tool for an executable skill; add a "signing" block to the manifest for L3 attestation.
|
|
40
|
+
\`memory\` turns a directory of OKF concepts (*.md with type/title frontmatter) into a
|
|
41
|
+
serverless RAG: a hash-pinned snapshot + search_knowledge/get_concept/list_concepts skills.
|
|
42
|
+
Run \`publish\` afterwards, as always.`;
|
|
37
43
|
|
|
38
44
|
function findManifest(root) {
|
|
39
45
|
const cands = [flag("--manifest"), "llms-skills.json", "scripts/skills-manifest.json"].filter(Boolean);
|
|
@@ -105,6 +111,48 @@ registerTool({
|
|
|
105
111
|
console.log(`\nNext: fill in the SKILL.md${withTool ? " and tool.js" : ""}, then run llms-skills publish`);
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
// ---- memory (RAG-OKF builder) ----
|
|
115
|
+
async function cmdMemory() {
|
|
116
|
+
const bundleArg = args[1];
|
|
117
|
+
if (!bundleArg || bundleArg.startsWith("--")) die("memory: needs a bundle directory, e.g. `llms-skills memory ./knowledge`");
|
|
118
|
+
const root = flag("--root") || process.cwd();
|
|
119
|
+
const bundleDir = join(root, bundleArg) === bundleArg ? bundleArg : join(root, bundleArg);
|
|
120
|
+
if (!existsSync(bundleDir)) die(`memory: bundle directory not found: ${bundleDir}`);
|
|
121
|
+
|
|
122
|
+
const manifestPath = findManifest(root) || join(root, "llms-skills.json");
|
|
123
|
+
const manifest = existsSync(manifestPath)
|
|
124
|
+
? JSON.parse(readFileSync(manifestPath, "utf8"))
|
|
125
|
+
: { section_intro: "Remote Agent Skills published by this domain.", published: [] };
|
|
126
|
+
|
|
127
|
+
const { targets, warnings, manifest: newManifest, snapshotSha, conceptCount } =
|
|
128
|
+
await buildMemoryTargets({ root, bundleDir, manifest, license: flag("--license") });
|
|
129
|
+
for (const w of warnings) console.error(`[warn] ${w}`);
|
|
130
|
+
|
|
131
|
+
const rel = (p) => (relative(root, p) || p).replaceAll("\\", "/");
|
|
132
|
+
const norm = (s) => s.replaceAll("\r\n", "\n");
|
|
133
|
+
const manifestContent = JSON.stringify(newManifest, null, 2) + "\n";
|
|
134
|
+
const all = [...targets, [manifestPath, manifestContent]];
|
|
135
|
+
|
|
136
|
+
if (has("--check")) {
|
|
137
|
+
const drift = all.filter(([p, c]) => (existsSync(p) ? norm(readFileSync(p, "utf8")) : null) !== norm(c));
|
|
138
|
+
if (drift.length) {
|
|
139
|
+
console.error("[DRIFT] Out-of-date memory artifacts. Run: llms-skills memory " + bundleArg);
|
|
140
|
+
for (const [p] of drift) console.error(` - ${rel(p)}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(`[OK] Memory in sync (${conceptCount} concepts, snapshot ${snapshotSha.slice(0, 12)}…).`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const [p, c] of all) {
|
|
148
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
149
|
+
writeFileSync(p, c, "utf8");
|
|
150
|
+
console.log(`[WRITE] ${rel(p)}`);
|
|
151
|
+
}
|
|
152
|
+
console.log(`\n${conceptCount} concept(s) indexed; snapshot sha256 ${snapshotSha.slice(0, 12)}…`);
|
|
153
|
+
console.log("Next: run llms-skills publish to emit the skills-memory line and hash the tools.");
|
|
154
|
+
}
|
|
155
|
+
|
|
108
156
|
// ---- publish ----
|
|
109
157
|
function cmdPublish() {
|
|
110
158
|
const root = flag("--root") || process.cwd();
|
|
@@ -193,6 +241,7 @@ function cmdKeygen() {
|
|
|
193
241
|
try {
|
|
194
242
|
switch (cmd) {
|
|
195
243
|
case "init": cmdInit(); break;
|
|
244
|
+
case "memory": await cmdMemory(); break;
|
|
196
245
|
case "publish": cmdPublish(); break;
|
|
197
246
|
case "validate": await cmdValidate(); break;
|
|
198
247
|
case "keygen": cmdKeygen(); break;
|
package/lib/core.mjs
CHANGED
|
@@ -30,7 +30,16 @@ export function parseFrontmatter(text) {
|
|
|
30
30
|
const result = {};
|
|
31
31
|
for (const line of body.split(/\r?\n/)) {
|
|
32
32
|
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/);
|
|
33
|
-
if (m)
|
|
33
|
+
if (m) {
|
|
34
|
+
let v = m[2].trim();
|
|
35
|
+
// strip one MATCHING pair of quotes (double or single) — real-world YAML
|
|
36
|
+
// frontmatter uses both (e.g. type: 'Concept'), and stripping only one
|
|
37
|
+
// style leaves quote characters inside the value.
|
|
38
|
+
if (v.length >= 2 && ((v[0] === '"' && v[v.length - 1] === '"') || (v[0] === "'" && v[v.length - 1] === "'"))) {
|
|
39
|
+
v = v.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
result[m[1]] = v;
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
44
|
return result;
|
|
36
45
|
}
|
package/lib/memory.mjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// memory.mjs — RAG-OKF builder behind `llms-skills memory`.
|
|
2
|
+
//
|
|
3
|
+
// Turns an OKF bundle (https://github.com/GoogleCloudPlatform/knowledge-catalog
|
|
4
|
+
// okf/SPEC.md v0.1: *.md concepts with `type` frontmatter; index.md/log.md
|
|
5
|
+
// reserved) into the origin-memory artifacts the Executable Skills extension
|
|
6
|
+
// v0.4 §2.4 defines and the mcpwasm runtimes already consume:
|
|
7
|
+
//
|
|
8
|
+
// - a BM25 snapshot (@rckflr/minimemory, format minimemory-okf-v1),
|
|
9
|
+
// CANONICALIZED for byte-determinism (see canonicalizeSnapshot),
|
|
10
|
+
// - three generated knowledge skills (search_knowledge / get_concept /
|
|
11
|
+
// list_concepts) as tool.js + SKILL.md,
|
|
12
|
+
// - manifest wiring (`memory` block + published entries) so a plain
|
|
13
|
+
// `llms-skills publish` emits the skills-memory line and all hashes.
|
|
14
|
+
//
|
|
15
|
+
// The engine is an optionalDependency: lazily imported; a clear error (not a
|
|
16
|
+
// crash) if missing. The consumer side needs nothing new — mcpwasm >= 0.4.0
|
|
17
|
+
// verifies snapshot_sha256 and injects host.memorySearch on both runtimes.
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
20
|
+
import { join, relative, basename } from "node:path";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import { parseFrontmatter, sha256NormalizedBuf } from "./core.mjs";
|
|
23
|
+
|
|
24
|
+
export const SNAPSHOT_FORMAT = "minimemory-okf-v1";
|
|
25
|
+
export const RESERVED_FILES = new Set(["index.md", "log.md"]); // OKF 0.1 reserved names
|
|
26
|
+
const CHUNK_SIZE = 800; // validated values (same as mcpwasm's docs-site publisher)
|
|
27
|
+
const CHUNK_OVERLAP = 50;
|
|
28
|
+
const MAX_SNAPSHOT_BYTES = 4194304; // 4 MB — the cap both mcpwasm runtimes enforce
|
|
29
|
+
const MAX_EMBED_LIST_BYTES = 262144; // embedded concept list must stay well under the 1 MB tool.js cap
|
|
30
|
+
|
|
31
|
+
// ---- engine (optionalDependency, lazy) --------------------------------------
|
|
32
|
+
export async function loadMemoryEngine() {
|
|
33
|
+
try {
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const mod = await import("@rckflr/minimemory");
|
|
36
|
+
mod.initSync({ module: readFileSync(require.resolve("@rckflr/minimemory/minimemory_bg.wasm")) });
|
|
37
|
+
return mod;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
const err = new Error(
|
|
40
|
+
"memory: the BM25 engine (@rckflr/minimemory) is not installed. It is an " +
|
|
41
|
+
"optionalDependency — `npm install @rckflr/minimemory` (or reinstall without " +
|
|
42
|
+
"--omit=optional) and retry. Underlying error: " + (e && e.message ? e.message : String(e))
|
|
43
|
+
);
|
|
44
|
+
err.code = "ENGINE_MISSING";
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- bundle scan -------------------------------------------------------------
|
|
50
|
+
// Walks the bundle, returns concepts sorted by id (determinism) plus warnings.
|
|
51
|
+
// concept id = bundle-relative posix path (the okf-integration convention).
|
|
52
|
+
export function scanBundle(bundleDir) {
|
|
53
|
+
const concepts = [];
|
|
54
|
+
const warnings = [];
|
|
55
|
+
const walk = (dir) => {
|
|
56
|
+
for (const name of readdirSync(dir).sort()) {
|
|
57
|
+
const p = join(dir, name);
|
|
58
|
+
if (statSync(p).isDirectory()) { walk(p); continue; }
|
|
59
|
+
if (!name.endsWith(".md") || RESERVED_FILES.has(name)) continue;
|
|
60
|
+
const id = relative(bundleDir, p).replaceAll("\\", "/");
|
|
61
|
+
const raw = readFileSync(p, "utf8");
|
|
62
|
+
const fm = parseFrontmatter(raw);
|
|
63
|
+
let type = fm.type;
|
|
64
|
+
if (!type) {
|
|
65
|
+
warnings.push(`${id}: frontmatter missing 'type' (OKF 0.1 requires it) — defaulting to "Documentation"`);
|
|
66
|
+
type = "Documentation";
|
|
67
|
+
}
|
|
68
|
+
// body = content after the frontmatter block (or the whole file if none)
|
|
69
|
+
let body = raw;
|
|
70
|
+
if (raw.startsWith("---")) {
|
|
71
|
+
const end = raw.indexOf("---", 3);
|
|
72
|
+
if (end !== -1) body = raw.slice(end + 3);
|
|
73
|
+
}
|
|
74
|
+
body = body.replace(/^\s+/, "");
|
|
75
|
+
const h1 = body.match(/^#\s+(.+)$/m);
|
|
76
|
+
const title = fm.title || (h1 ? h1[1].trim() : basename(id, ".md"));
|
|
77
|
+
const description = fm.description || "";
|
|
78
|
+
if (!body.trim()) warnings.push(`${id}: empty body — concept will not be searchable`);
|
|
79
|
+
concepts.push({ id, type, title, description, body });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
walk(bundleDir);
|
|
83
|
+
concepts.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
84
|
+
return { concepts, warnings };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- snapshot ----------------------------------------------------------------
|
|
88
|
+
// minimemory's export_snapshot() iterates a Rust HashMap: chunk ORDER varies
|
|
89
|
+
// between builds (verified empirically: identical content, shuffled array).
|
|
90
|
+
// The snapshot is a plain JSON array with already-sorted object keys, so
|
|
91
|
+
// sorting by chunk id and re-serializing compact yields a byte-deterministic,
|
|
92
|
+
// import-compatible snapshot. This canonicalization is REQUIRED for the
|
|
93
|
+
// content-addressing story (`--check`, CI, sha-pinning) to work at all.
|
|
94
|
+
export function canonicalizeSnapshot(snapshot) {
|
|
95
|
+
const arr = JSON.parse(snapshot);
|
|
96
|
+
if (!Array.isArray(arr)) throw new Error("memory: unexpected snapshot shape (not a JSON array)");
|
|
97
|
+
arr.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
98
|
+
return JSON.stringify(arr);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildSnapshot(mem, concepts) {
|
|
102
|
+
const idx = mem.WasmOkfIndex.with_chunk_size(CHUNK_SIZE, CHUNK_OVERLAP);
|
|
103
|
+
for (const c of concepts) {
|
|
104
|
+
const content = `---\ntype: ${c.type}\ntitle: ${c.title}\n---\n${c.body}`;
|
|
105
|
+
idx.ingest_concept(c.id, content);
|
|
106
|
+
}
|
|
107
|
+
const snapshot = canonicalizeSnapshot(idx.export_snapshot());
|
|
108
|
+
if (Buffer.byteLength(snapshot, "utf8") > MAX_SNAPSHOT_BYTES) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`memory: snapshot exceeds the 4 MB cap the runtimes enforce (${Buffer.byteLength(snapshot, "utf8")} bytes). ` +
|
|
111
|
+
"Split the bundle or trim concept bodies."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return snapshot;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- generated skills ---------------------------------------------------------
|
|
118
|
+
// search_knowledge is a UNIVERSAL template: identical bytes for every
|
|
119
|
+
// publisher, so its tool_sha256 is stable ecosystem-wide — audit once,
|
|
120
|
+
// attest once. Do not interpolate anything publisher-specific into it.
|
|
121
|
+
export function searchToolSource() {
|
|
122
|
+
return `// search_knowledge — universal origin-memory search tool (llms-skills memory).
|
|
123
|
+
// Identical for every publisher: its sha256 is a stable, ecosystem-wide constant.
|
|
124
|
+
registerTool({
|
|
125
|
+
name: "search_knowledge",
|
|
126
|
+
description: "BM25 search over this origin's published knowledge (OKF bundle). Returns the most relevant chunks with concept ids.",
|
|
127
|
+
inputSchema: { type: "object", properties: { q: { type: "string", description: "search query" }, k: { type: "number", description: "max results (1-10, default 5)" } }, required: ["q"] },
|
|
128
|
+
handler: async function (args) {
|
|
129
|
+
return await host.memorySearch(args.q, typeof args.k === "number" ? args.k : 5);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getConceptToolSource(conceptUrlById) {
|
|
136
|
+
return `// get_concept — fetch a full concept document from this origin's OKF bundle.
|
|
137
|
+
// The id->url map is embedded at build time (content-addressed via tool_sha256).
|
|
138
|
+
var CONCEPTS = ${JSON.stringify(conceptUrlById)};
|
|
139
|
+
registerTool({
|
|
140
|
+
name: "get_concept",
|
|
141
|
+
description: "Fetch the full markdown of a knowledge concept by id (as returned by search_knowledge / list_concepts).",
|
|
142
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "concept id, e.g. policies/refunds.md" } }, required: ["id"] },
|
|
143
|
+
handler: async function (args) {
|
|
144
|
+
var url = CONCEPTS[args.id];
|
|
145
|
+
if (!url) throw new Error("unknown concept id: " + args.id);
|
|
146
|
+
var r = await host.fetchOrigin(url);
|
|
147
|
+
if (r.status !== 200) throw new Error("fetch failed: HTTP " + r.status);
|
|
148
|
+
return { id: args.id, markdown: r.body };
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function listConceptsToolSource(conceptMeta) {
|
|
155
|
+
const embedded = JSON.stringify(conceptMeta);
|
|
156
|
+
if (Buffer.byteLength(embedded, "utf8") > MAX_EMBED_LIST_BYTES) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"memory: the concept list is too large to embed in list_concepts/tool.js (" +
|
|
159
|
+
Buffer.byteLength(embedded, "utf8") + " bytes > " + MAX_EMBED_LIST_BYTES + "). " +
|
|
160
|
+
"Reduce concepts or descriptions."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return `// list_concepts — enumerate this origin's published knowledge concepts.
|
|
164
|
+
// The list is embedded at build time (content-addressed via tool_sha256).
|
|
165
|
+
var CONCEPTS = ${embedded};
|
|
166
|
+
registerTool({
|
|
167
|
+
name: "list_concepts",
|
|
168
|
+
description: "List all knowledge concepts published by this origin (id, type, title, description).",
|
|
169
|
+
inputSchema: { type: "object", properties: {} },
|
|
170
|
+
handler: function () { return { concepts: CONCEPTS }; }
|
|
171
|
+
});
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function knowledgeSkillMd(name, description, license) {
|
|
176
|
+
return `---
|
|
177
|
+
name: ${name}
|
|
178
|
+
description: ${description}
|
|
179
|
+
version: 0.1.0
|
|
180
|
+
license: ${license}
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
# ${name}
|
|
184
|
+
|
|
185
|
+
${description} Generated by \`llms-skills memory\` from this origin's OKF
|
|
186
|
+
knowledge bundle; backed by a hash-verified BM25 snapshot (origin memory,
|
|
187
|
+
Executable Skills v0.4 §2.4). Call the tool rather than improvising: the
|
|
188
|
+
runtime executes it verbatim in a sandbox.
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- orchestrator --------------------------------------------------------------
|
|
193
|
+
// Builds everything under `root`. Returns { targets, warnings, snapshotSha, manifest }
|
|
194
|
+
// where targets is [path, content] pairs (the caller writes or checks them).
|
|
195
|
+
export async function buildMemoryTargets({ root, bundleDir, manifest, license }) {
|
|
196
|
+
const bundleRel = relative(root, bundleDir).replaceAll("\\", "/");
|
|
197
|
+
if (bundleRel.startsWith("..")) {
|
|
198
|
+
throw new Error("memory: the bundle must live inside the publisher root (it is served as static content)");
|
|
199
|
+
}
|
|
200
|
+
const { concepts, warnings } = scanBundle(bundleDir);
|
|
201
|
+
if (concepts.length === 0) throw new Error(`memory: no OKF concepts (*.md) found in ${bundleDir}`);
|
|
202
|
+
|
|
203
|
+
const mem = await loadMemoryEngine();
|
|
204
|
+
const snapshot = buildSnapshot(mem, concepts);
|
|
205
|
+
|
|
206
|
+
const urlById = {};
|
|
207
|
+
for (const c of concepts) urlById[c.id] = "/" + (bundleRel === "" ? "" : bundleRel + "/") + c.id;
|
|
208
|
+
const conceptMeta = concepts.map((c) => ({ id: c.id, type: c.type, title: c.title, description: c.description }));
|
|
209
|
+
|
|
210
|
+
const lic = license || manifest.license || "MIT";
|
|
211
|
+
const SKILLS = [
|
|
212
|
+
["search_knowledge", searchToolSource(), "BM25 search over this origin's published knowledge bundle. Use it to find which concept answers a question before fetching it."],
|
|
213
|
+
["get_concept", getConceptToolSource(urlById), "fetch the full markdown of a knowledge concept by id from this origin's bundle."],
|
|
214
|
+
["list_concepts", listConceptsToolSource(conceptMeta), "list every knowledge concept this origin publishes (id, type, title, description)."],
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const targets = [[join(root, "skills-index.snapshot"), snapshot]];
|
|
218
|
+
for (const [name, toolSrc, summary] of SKILLS) {
|
|
219
|
+
targets.push([join(root, "skills", name, "tool.js"), toolSrc]);
|
|
220
|
+
targets.push([join(root, "skills", name, "SKILL.md"), knowledgeSkillMd(name, summary.charAt(0).toUpperCase() + summary.slice(1), lic)]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// .gitattributes: git autocrlf mangling the snapshot on checkout would break
|
|
224
|
+
// the published sha (real-world footgun) — pin it as -text.
|
|
225
|
+
const gaPath = join(root, ".gitattributes");
|
|
226
|
+
const gaLine = "skills-index.snapshot -text";
|
|
227
|
+
const gaCurrent = existsSync(gaPath) ? readFileSync(gaPath, "utf8") : "";
|
|
228
|
+
if (!gaCurrent.split(/\r?\n/).some((l) => l.trim() === gaLine)) {
|
|
229
|
+
targets.push([gaPath, (gaCurrent ? gaCurrent.replace(/\n?$/, "\n") : "") + gaLine + "\n"]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// manifest wiring (idempotent): memory block + one published entry per skill.
|
|
233
|
+
const newManifest = JSON.parse(JSON.stringify(manifest));
|
|
234
|
+
newManifest.memory = {
|
|
235
|
+
snapshot_path: "skills-index.snapshot",
|
|
236
|
+
snapshot_url: "/skills-index.snapshot",
|
|
237
|
+
format: SNAPSHOT_FORMAT,
|
|
238
|
+
};
|
|
239
|
+
if (!Array.isArray(newManifest.published)) newManifest.published = [];
|
|
240
|
+
for (const [name, , summary] of SKILLS) {
|
|
241
|
+
const path = `skills/${name}/SKILL.md`;
|
|
242
|
+
if (!newManifest.published.some((e) => e && e.path === path)) {
|
|
243
|
+
newManifest.published.push({
|
|
244
|
+
path,
|
|
245
|
+
url: `/skills/${name}/SKILL.md`,
|
|
246
|
+
summary,
|
|
247
|
+
tool: `skills/${name}/tool.js`,
|
|
248
|
+
tool_url: `/skills/${name}/tool.js`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
targets,
|
|
255
|
+
warnings,
|
|
256
|
+
manifest: newManifest,
|
|
257
|
+
snapshotSha: sha256NormalizedBuf(Buffer.from(snapshot, "utf8")),
|
|
258
|
+
conceptCount: concepts.length,
|
|
259
|
+
};
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rckflr/llms-skills",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "One-command publisher CLI for the llms.txt Skills standard: scaffold, hash, sign, and sync the ## Skills section + .well-known/agent-skills/index.json. Byte-identical to the reference Python generator; zero dependencies.",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "One-command publisher CLI for the llms.txt Skills standard: scaffold, hash, sign, and sync the ## Skills section + .well-known/agent-skills/index.json. Byte-identical to the reference Python generator; zero hard dependencies (BM25 knowledge snapshots via an optional wasm engine).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Mauricio Perera <mauricio.perera@gmail.com>",
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/MauricioPerera/llms-txt-skills/issues"
|
|
16
16
|
},
|
|
17
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"llms.txt",
|
|
19
|
+
"agent-skills",
|
|
20
|
+
"skills",
|
|
21
|
+
"mcp",
|
|
22
|
+
"publisher",
|
|
23
|
+
"cli",
|
|
24
|
+
"ed25519",
|
|
25
|
+
"sha256"
|
|
26
|
+
],
|
|
18
27
|
"engines": {
|
|
19
28
|
"node": ">=18"
|
|
20
29
|
},
|
|
@@ -22,11 +31,14 @@
|
|
|
22
31
|
"llms-skills": "bin/llms-skills.mjs"
|
|
23
32
|
},
|
|
24
33
|
"exports": {
|
|
25
|
-
".": "./lib/core.mjs"
|
|
34
|
+
".": "./lib/core.mjs",
|
|
35
|
+
"./memory": "./lib/memory.mjs"
|
|
26
36
|
},
|
|
27
37
|
"files": [
|
|
38
|
+
"CHANGELOG.md",
|
|
28
39
|
"bin/llms-skills.mjs",
|
|
29
40
|
"lib/core.mjs",
|
|
41
|
+
"lib/memory.mjs",
|
|
30
42
|
"README.md"
|
|
31
43
|
],
|
|
32
44
|
"publishConfig": {
|
|
@@ -34,5 +46,8 @@
|
|
|
34
46
|
},
|
|
35
47
|
"scripts": {
|
|
36
48
|
"test": "node test.mjs"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"@rckflr/minimemory": "^3.2.0"
|
|
37
52
|
}
|
|
38
53
|
}
|