@rckflr/llms-skills 0.2.0 → 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,84 @@
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
+ ## [Unreleased] — 0.3.0
8
+
9
+ ### Added
10
+ - **`--scope <name>` on `memory`** (Executable Skills v0.5 §2.5, resolves
11
+ core RFC v0.10 Open Question 6). Declares the project namespace for
12
+ multi-project origins: the manifest's `memory` block and every generated
13
+ `published` entry carry `scope`, and `publish` renders it as the **last**
14
+ key of both the skill lines and the `skills-memory` line
15
+ (`"scope":"kdd"`). Runtimes (mcpwasm ≥ 0.6.0) expose the tools as
16
+ `<scope>__<toolName>` and bind each scope's memory to its own snapshot.
17
+ Pattern `^[a-z][a-z0-9_-]*$`; invalid values fail fast. Re-running
18
+ `memory` with a different `--scope` (or none) refreshes the manifest
19
+ entries idempotently.
20
+ - `validate` understands scopes: accepts multiple `skills-memory` lines when
21
+ each carries a distinct `scope` (at most one unscoped), and reports invalid
22
+ `scope` values and duplicated scopes as errors.
23
+ - New test part: scoped fixture end-to-end (manifest wiring, key order,
24
+ validate, and **byte-identity with `scripts/generate.py`** on scoped
25
+ output — the mirror contract now covers scopes).
26
+
27
+ ### Changed
28
+ - `scripts/generate.py` mirrored: manifest entries and the `memory` block
29
+ accept `scope` and render it identically (byte-identity enforced by
30
+ `cli/test.mjs` Part 6). No `scope` anywhere ⇒ output identical to 0.2.1.
31
+
32
+ ## [0.2.1] — 2026-07-10
33
+
34
+ ### Fixed
35
+ - **Frontmatter values quoted with single quotes** (`type: 'Concept'`) are now
36
+ unquoted correctly: one *matching* pair of quotes (double or single) is
37
+ stripped. Found with a real OKF bundle (KDD) whose frontmatter uses single
38
+ quotes; before this fix the quotes leaked into concept `type`/`title`.
39
+ Mirrored in `scripts/generate.py` (`parse_yaml_frontmatter`) — byte-identity
40
+ between both generators re-verified.
41
+
42
+ ## [0.2.0] — 2026-07-10
43
+
44
+ ### Added
45
+ - **`memory <bundle-dir>` — the RAG-OKF builder.** Turns a directory of
46
+ [OKF 0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
47
+ concepts (`*.md` with `type`/`title` frontmatter; `index.md`/`log.md`
48
+ reserved) into a serverless RAG:
49
+ - a **byte-deterministic** BM25 snapshot (`minimemory-okf-v1`), pinned by
50
+ `snapshot_sha256` in the `skills-memory` line. Determinism requires
51
+ canonicalization: minimemory's export order comes from a Rust HashMap and
52
+ varies run to run — chunks are sorted by id and re-serialized, which is
53
+ what makes `--check`/CI content-addressing possible at all;
54
+ - three generated executable skills: `search_knowledge` (a **universal
55
+ template** — identical bytes for every publisher, so its `tool_sha256` is
56
+ a stable ecosystem-wide constant: audit once, attest once), `get_concept`
57
+ and `list_concepts` (concept metadata embedded at build time,
58
+ content-addressed);
59
+ - manifest wiring (`memory` block + `published` entries) so a plain
60
+ `publish` emits everything, and a `.gitattributes` pin
61
+ (`skills-index.snapshot -text`) so git CRLF conversion can never break
62
+ the published hash.
63
+ - `memory <bundle> --check` is the CI drift guard. Consumers need nothing
64
+ new: `@rckflr/mcpwasm` ≥ 0.4.0 verifies the snapshot and injects
65
+ `host.memorySearch` on both runtimes (covered by a real end-to-end test
66
+ against the published mcpwasm tarball).
67
+
68
+ ### Changed
69
+ - `@rckflr/minimemory` (~630 KB wasm BM25 engine) added as an
70
+ **optionalDependency**: installed by default, lazily imported by `memory`;
71
+ if omitted, `memory` fails with a clear message and every other command
72
+ works exactly as before (the CLI keeps zero hard dependencies).
73
+
74
+ ## [0.1.0] — 2026-07-10
75
+
76
+ ### Added
77
+ - Initial release: one-command publisher CLI for the llms.txt Skills
78
+ standard — `init` (scaffold SKILL.md + optional tool.js + manifest),
79
+ `publish [--check]` (sha256, `## Skills` into llms.txt,
80
+ `.well-known/agent-skills/index.json`, ed25519 signing), `validate`
81
+ (llms.txt + skills, local path or URL), `keygen`. Zero dependencies
82
+ (hashing and ed25519 via `node:crypto`); output **byte-identical** to the
83
+ reference Python generator (`scripts/generate.py`), enforced by
84
+ `cli/test.mjs` in CI.
package/README.md CHANGED
@@ -67,6 +67,21 @@ What it generates: a **byte-deterministic** BM25 snapshot (canonicalized, so
67
67
  content-addressed). It also pins the snapshot as `-text` in `.gitattributes`
68
68
  so git's CRLF conversion can never break the published hash.
69
69
 
70
+ **Multi-project origins (scopes).** Publishing several projects on one origin
71
+ (e.g. a GitHub Pages root)? Add `--scope <name>` (pattern
72
+ `^[a-z][a-z0-9_-]*$`, Executable Skills v0.5 §2.5):
73
+
74
+ ```bash
75
+ npx @rckflr/llms-skills memory ./knowledge --scope kdd
76
+ ```
77
+
78
+ The manifest and the rendered lines carry `"scope":"kdd"`; runtimes
79
+ (mcpwasm ≥ 0.6.0) expose the tools as `kdd__search_knowledge` etc. and bind
80
+ each scope's memory to its own snapshot — no name collisions, one
81
+ `skills-memory` line per scope. Published bytes and hashes are untouched
82
+ (the rename is runtime-side), so `search_knowledge` stays the universal
83
+ template.
84
+
70
85
  Consumers need nothing new: `npx -y @rckflr/mcpwasm <origin>` (>= 0.4.0)
71
86
  verifies the snapshot and injects `host.memorySearch` on both runtimes.
72
87
  `memory <bundle> --check` is the CI guard. The BM25 engine
@@ -29,7 +29,7 @@ const USAGE = `llms-skills — publisher CLI for the llms.txt Skills standard
29
29
 
30
30
  Usage:
31
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>]
32
+ llms-skills memory <bundle-dir> [--check] [--root <dir>] [--license <spdx>] [--scope <name>]
33
33
  OKF bundle -> BM25 snapshot + knowledge skills (RAG)
34
34
  llms-skills publish [--check] [--manifest <p>] [--root <dir>] generate llms.txt ## Skills + index.json
35
35
  llms-skills validate <src> [--strict] validate an llms.txt (path or URL)
@@ -125,7 +125,7 @@ async function cmdMemory() {
125
125
  : { section_intro: "Remote Agent Skills published by this domain.", published: [] };
126
126
 
127
127
  const { targets, warnings, manifest: newManifest, snapshotSha, conceptCount } =
128
- await buildMemoryTargets({ root, bundleDir, manifest, license: flag("--license") });
128
+ await buildMemoryTargets({ root, bundleDir, manifest, license: flag("--license"), scope: flag("--scope") });
129
129
  for (const w of warnings) console.error(`[warn] ${w}`);
130
130
 
131
131
  const rel = (p) => (relative(root, p) || p).replaceAll("\\", "/");
package/lib/core.mjs CHANGED
@@ -30,13 +30,25 @@ 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) result[m[1]] = m[2].trim().replace(/^"|"$/g, "");
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
  }
37
46
 
38
47
  const REQUIRED_FRONTMATTER = ["name", "description", "version", "license"];
39
48
 
49
+ // Executable Skills v0.5 SS2.5: namespace declarativo para origins multi-proyecto.
50
+ const SCOPE_RE = /^[a-z][a-z0-9_-]*$/;
51
+
40
52
  // ---- load skills from a manifest (mirrors generate.py load_skills) ----
41
53
  export function loadSkills(manifest, repoRoot) {
42
54
  const skills = [];
@@ -55,10 +67,14 @@ export function loadSkills(manifest, repoRoot) {
55
67
  toolUrl = entry.tool_url;
56
68
  toolSha256 = sha256NormalizedFile(toolPath);
57
69
  }
70
+ if (entry.scope !== undefined && !SCOPE_RE.test(String(entry.scope))) {
71
+ throw new Error(`${entry.path}: invalid 'scope' (pattern ^[a-z][a-z0-9_-]*$, Executable Skills v0.5 §2.5)`);
72
+ }
58
73
  skills.push({
59
74
  name: fm.name, description: fm.description, version: fm.version, license: fm.license,
60
75
  homepage: fm.homepage || null, url: entry.url, summary: entry.summary,
61
76
  sha256: sha256NormalizedFile(path), path, toolUrl, toolSha256,
77
+ scope: entry.scope !== undefined ? String(entry.scope) : null,
62
78
  });
63
79
  }
64
80
  return skills;
@@ -72,7 +88,14 @@ export function loadMemory(manifest, repoRoot) {
72
88
  }
73
89
  const snapshotPath = join(repoRoot, memory.snapshot_path);
74
90
  if (!existsSync(snapshotPath)) throw new Error(`snapshot not found: ${memory.snapshot_path}`);
75
- return { snapshot_url: memory.snapshot_url, format: memory.format, snapshot_sha256: sha256NormalizedFile(snapshotPath) };
91
+ if (memory.scope !== undefined && !SCOPE_RE.test(String(memory.scope))) {
92
+ throw new Error("manifest.memory: invalid 'scope' (pattern ^[a-z][a-z0-9_-]*$, Executable Skills v0.5 §2.5)");
93
+ }
94
+ return {
95
+ snapshot_url: memory.snapshot_url, format: memory.format,
96
+ snapshot_sha256: sha256NormalizedFile(snapshotPath),
97
+ scope: memory.scope !== undefined ? String(memory.scope) : null,
98
+ };
76
99
  }
77
100
 
78
101
  // ---- render: ## Skills section (compact JSON, exact key order of generate.py) ----
@@ -81,6 +104,7 @@ export function renderSkillsSection(manifest, skills) {
81
104
  for (const s of skills) {
82
105
  const meta = { version: s.version, license: s.license, sha256: s.sha256 };
83
106
  if (s.toolUrl && s.toolSha256) { meta.tool = s.toolUrl; meta.tool_sha256 = s.toolSha256; }
107
+ if (s.scope) meta.scope = s.scope;
84
108
  lines.push(`- [${s.name}](${s.url}): ${s.summary} <!-- skill: ${JSON.stringify(meta)} -->`);
85
109
  }
86
110
  return lines.join("\n") + "\n";
@@ -89,6 +113,7 @@ export function renderSkillsSection(manifest, skills) {
89
113
  export function renderSkillsMemoryLine(memory) {
90
114
  if (!memory) return "";
91
115
  const meta = { snapshot: memory.snapshot_url, snapshot_sha256: memory.snapshot_sha256, format: memory.format };
116
+ if (memory.scope) meta.scope = memory.scope;
92
117
  return `<!-- skills-memory: ${JSON.stringify(meta)} -->\n\n`;
93
118
  }
94
119
 
@@ -182,9 +207,20 @@ export function validateLlmsTxt(source, text) {
182
207
  const err = (message, line = "") => errors.push({ message, line });
183
208
  const warn = (message, line = "") => warnings.push({ message, line });
184
209
 
185
- // origin memory line (optional)
186
- const memMatch = text.split(/\r?\n/).map((l) => l.trim().match(MEMORY_CAPTURE_RE)).find(Boolean);
187
- if (memMatch) validateMemory(memMatch[1], source, err, warn);
210
+ // origin memory lines (optional; Executable Skills v0.5 SS2.5: one per
211
+ // scope, at most one without scope)
212
+ const memScopes = [];
213
+ for (const l of text.split(/\r?\n/)) {
214
+ const memMatch = l.trim().match(MEMORY_CAPTURE_RE);
215
+ if (!memMatch) continue;
216
+ const scopeKey = validateMemory(memMatch[1], source, err, warn);
217
+ if (scopeKey === undefined) continue; // JSON invalido, ya reportado
218
+ if (memScopes.includes(scopeKey)) {
219
+ err(scopeKey === ""
220
+ ? "skills-memory: more than one line without 'scope' (at most one allowed)"
221
+ : `skills-memory: duplicate line for scope '${scopeKey}'`);
222
+ } else memScopes.push(scopeKey);
223
+ }
188
224
 
189
225
  // find ## Skills section
190
226
  const lines = text.split(/\r?\n/);
@@ -229,6 +265,8 @@ export function validateLlmsTxt(source, text) {
229
265
  if (hasToolSha && !hasTool) err("'tool_sha256' declared without 'tool' (both required together)", raw.slice(0, 80));
230
266
  if (hasToolSha && !/^[a-fA-F0-9]{64}$/.test(String(metaParsed.tool_sha256)))
231
267
  err("Invalid tool_sha256 (must be 64 hex chars)", raw.slice(0, 80));
268
+ if ("scope" in metaParsed && !SCOPE_RE.test(String(metaParsed.scope)))
269
+ err("Invalid 'scope' (pattern ^[a-z][a-z0-9_-]*$, Executable Skills v0.5 §2.5)", raw.slice(0, 80));
232
270
  } catch (e) { err(`Invalid metadata JSON: ${e.message}`, raw.slice(0, 80)); }
233
271
  }
234
272
  const resolved = resolveSkillPath(url, source);
@@ -263,12 +301,15 @@ const MEMORY_CAPTURE_RE = /^<!--\s*skills-memory:\s*(.*?)\s*-->\s*$/;
263
301
  function validateMemory(memJson, source, err, warn) {
264
302
  let meta;
265
303
  try { meta = JSON.parse(memJson); }
266
- catch (e) { err(`skills-memory: invalid JSON: ${e.message}`); return; }
304
+ catch (e) { err(`skills-memory: invalid JSON: ${e.message}`); return undefined; }
305
+ const scopeKey = meta.scope === undefined ? "" : String(meta.scope);
306
+ if (meta.scope !== undefined && !SCOPE_RE.test(String(meta.scope)))
307
+ err(`skills-memory: invalid 'scope' (pattern ^[a-z][a-z0-9_-]*$, Executable Skills v0.5 §2.5)`);
267
308
  let ok = true;
268
309
  for (const key of ["snapshot", "snapshot_sha256", "format"]) {
269
310
  if (typeof meta[key] !== "string") { err(`skills-memory: missing or invalid '${key}' (must be string)`); ok = false; }
270
311
  }
271
- if (!ok) return;
312
+ if (!ok) return scopeKey;
272
313
  if (!/^[a-fA-F0-9]{64}$/.test(meta.snapshot_sha256)) err("skills-memory: invalid snapshot_sha256 (must be 64 hex chars)");
273
314
  if (meta.format !== "minimemory-okf-v1") warn(`skills-memory: format '${meta.format}' is not the only one recognized today (minimemory-okf-v1)`);
274
315
  const resolved = resolveSkillPath(meta.snapshot, source);
@@ -279,4 +320,5 @@ function validateMemory(memJson, source, err, warn) {
279
320
  if (actual !== meta.snapshot_sha256) err(`skills-memory: snapshot_sha256 mismatch: declared ${meta.snapshot_sha256}, actual ${actual}`);
280
321
  }
281
322
  }
323
+ return scopeKey;
282
324
  }
package/lib/memory.mjs CHANGED
@@ -192,7 +192,11 @@ runtime executes it verbatim in a sandbox.
192
192
  // ---- orchestrator --------------------------------------------------------------
193
193
  // Builds everything under `root`. Returns { targets, warnings, snapshotSha, manifest }
194
194
  // where targets is [path, content] pairs (the caller writes or checks them).
195
- export async function buildMemoryTargets({ root, bundleDir, manifest, license }) {
195
+ export async function buildMemoryTargets({ root, bundleDir, manifest, license, scope }) {
196
+ // Executable Skills v0.5 SS2.5: namespace declarativo para origins multi-proyecto.
197
+ if (scope !== undefined && scope !== null && !/^[a-z][a-z0-9_-]*$/.test(String(scope))) {
198
+ throw new Error("memory: invalid --scope (pattern ^[a-z][a-z0-9_-]*$, Executable Skills v0.5 §2.5)");
199
+ }
196
200
  const bundleRel = relative(root, bundleDir).replaceAll("\\", "/");
197
201
  if (bundleRel.startsWith("..")) {
198
202
  throw new Error("memory: the bundle must live inside the publisher root (it is served as static content)");
@@ -236,17 +240,26 @@ export async function buildMemoryTargets({ root, bundleDir, manifest, license })
236
240
  snapshot_url: "/skills-index.snapshot",
237
241
  format: SNAPSHOT_FORMAT,
238
242
  };
243
+ if (scope) newManifest.memory.scope = String(scope);
244
+ else delete newManifest.memory.scope;
239
245
  if (!Array.isArray(newManifest.published)) newManifest.published = [];
240
246
  for (const [name, , summary] of SKILLS) {
241
247
  const path = `skills/${name}/SKILL.md`;
242
- if (!newManifest.published.some((e) => e && e.path === path)) {
243
- newManifest.published.push({
248
+ const existing = newManifest.published.find((e) => e && e.path === path);
249
+ if (!existing) {
250
+ const entry = {
244
251
  path,
245
252
  url: `/skills/${name}/SKILL.md`,
246
253
  summary,
247
254
  tool: `skills/${name}/tool.js`,
248
255
  tool_url: `/skills/${name}/tool.js`,
249
- });
256
+ };
257
+ if (scope) entry.scope = String(scope);
258
+ newManifest.published.push(entry);
259
+ } else if (scope) {
260
+ existing.scope = String(scope); // re-run con --scope: refresca el namespace
261
+ } else {
262
+ delete existing.scope; // re-run sin --scope: vuelve al default global
250
263
  }
251
264
  }
252
265
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rckflr/llms-skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
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",
@@ -35,6 +35,7 @@
35
35
  "./memory": "./lib/memory.mjs"
36
36
  },
37
37
  "files": [
38
+ "CHANGELOG.md",
38
39
  "bin/llms-skills.mjs",
39
40
  "lib/core.mjs",
40
41
  "lib/memory.mjs",