@rckflr/llms-skills 0.1.0 → 0.2.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/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
@@ -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/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.0",
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.0",
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": ["llms.txt", "agent-skills", "skills", "mcp", "publisher", "cli", "ed25519", "sha256"],
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,13 @@
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": [
28
38
  "bin/llms-skills.mjs",
29
39
  "lib/core.mjs",
40
+ "lib/memory.mjs",
30
41
  "README.md"
31
42
  ],
32
43
  "publishConfig": {
@@ -34,5 +45,8 @@
34
45
  },
35
46
  "scripts": {
36
47
  "test": "node test.mjs"
48
+ },
49
+ "optionalDependencies": {
50
+ "@rckflr/minimemory": "^3.2.0"
37
51
  }
38
52
  }