@rckflr/llms-skills 0.2.1 → 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 CHANGED
@@ -4,6 +4,41 @@ All notable changes to the [`@rckflr/llms-skills`](https://www.npmjs.com/package
4
4
  package. Format based on [Keep a Changelog](https://keepachangelog.com/); dates
5
5
  are the npm publish dates.
6
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
+
7
42
  ## [0.2.0] — 2026-07-10
8
43
 
9
44
  ### Added
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
@@ -46,6 +46,9 @@ export function parseFrontmatter(text) {
46
46
 
47
47
  const REQUIRED_FRONTMATTER = ["name", "description", "version", "license"];
48
48
 
49
+ // Executable Skills v0.5 SS2.5: namespace declarativo para origins multi-proyecto.
50
+ const SCOPE_RE = /^[a-z][a-z0-9_-]*$/;
51
+
49
52
  // ---- load skills from a manifest (mirrors generate.py load_skills) ----
50
53
  export function loadSkills(manifest, repoRoot) {
51
54
  const skills = [];
@@ -64,10 +67,14 @@ export function loadSkills(manifest, repoRoot) {
64
67
  toolUrl = entry.tool_url;
65
68
  toolSha256 = sha256NormalizedFile(toolPath);
66
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
+ }
67
73
  skills.push({
68
74
  name: fm.name, description: fm.description, version: fm.version, license: fm.license,
69
75
  homepage: fm.homepage || null, url: entry.url, summary: entry.summary,
70
76
  sha256: sha256NormalizedFile(path), path, toolUrl, toolSha256,
77
+ scope: entry.scope !== undefined ? String(entry.scope) : null,
71
78
  });
72
79
  }
73
80
  return skills;
@@ -81,7 +88,14 @@ export function loadMemory(manifest, repoRoot) {
81
88
  }
82
89
  const snapshotPath = join(repoRoot, memory.snapshot_path);
83
90
  if (!existsSync(snapshotPath)) throw new Error(`snapshot not found: ${memory.snapshot_path}`);
84
- 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
+ };
85
99
  }
86
100
 
87
101
  // ---- render: ## Skills section (compact JSON, exact key order of generate.py) ----
@@ -90,6 +104,7 @@ export function renderSkillsSection(manifest, skills) {
90
104
  for (const s of skills) {
91
105
  const meta = { version: s.version, license: s.license, sha256: s.sha256 };
92
106
  if (s.toolUrl && s.toolSha256) { meta.tool = s.toolUrl; meta.tool_sha256 = s.toolSha256; }
107
+ if (s.scope) meta.scope = s.scope;
93
108
  lines.push(`- [${s.name}](${s.url}): ${s.summary} <!-- skill: ${JSON.stringify(meta)} -->`);
94
109
  }
95
110
  return lines.join("\n") + "\n";
@@ -98,6 +113,7 @@ export function renderSkillsSection(manifest, skills) {
98
113
  export function renderSkillsMemoryLine(memory) {
99
114
  if (!memory) return "";
100
115
  const meta = { snapshot: memory.snapshot_url, snapshot_sha256: memory.snapshot_sha256, format: memory.format };
116
+ if (memory.scope) meta.scope = memory.scope;
101
117
  return `<!-- skills-memory: ${JSON.stringify(meta)} -->\n\n`;
102
118
  }
103
119
 
@@ -191,9 +207,20 @@ export function validateLlmsTxt(source, text) {
191
207
  const err = (message, line = "") => errors.push({ message, line });
192
208
  const warn = (message, line = "") => warnings.push({ message, line });
193
209
 
194
- // origin memory line (optional)
195
- const memMatch = text.split(/\r?\n/).map((l) => l.trim().match(MEMORY_CAPTURE_RE)).find(Boolean);
196
- 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
+ }
197
224
 
198
225
  // find ## Skills section
199
226
  const lines = text.split(/\r?\n/);
@@ -238,6 +265,8 @@ export function validateLlmsTxt(source, text) {
238
265
  if (hasToolSha && !hasTool) err("'tool_sha256' declared without 'tool' (both required together)", raw.slice(0, 80));
239
266
  if (hasToolSha && !/^[a-fA-F0-9]{64}$/.test(String(metaParsed.tool_sha256)))
240
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));
241
270
  } catch (e) { err(`Invalid metadata JSON: ${e.message}`, raw.slice(0, 80)); }
242
271
  }
243
272
  const resolved = resolveSkillPath(url, source);
@@ -272,12 +301,15 @@ const MEMORY_CAPTURE_RE = /^<!--\s*skills-memory:\s*(.*?)\s*-->\s*$/;
272
301
  function validateMemory(memJson, source, err, warn) {
273
302
  let meta;
274
303
  try { meta = JSON.parse(memJson); }
275
- 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)`);
276
308
  let ok = true;
277
309
  for (const key of ["snapshot", "snapshot_sha256", "format"]) {
278
310
  if (typeof meta[key] !== "string") { err(`skills-memory: missing or invalid '${key}' (must be string)`); ok = false; }
279
311
  }
280
- if (!ok) return;
312
+ if (!ok) return scopeKey;
281
313
  if (!/^[a-fA-F0-9]{64}$/.test(meta.snapshot_sha256)) err("skills-memory: invalid snapshot_sha256 (must be 64 hex chars)");
282
314
  if (meta.format !== "minimemory-okf-v1") warn(`skills-memory: format '${meta.format}' is not the only one recognized today (minimemory-okf-v1)`);
283
315
  const resolved = resolveSkillPath(meta.snapshot, source);
@@ -288,4 +320,5 @@ function validateMemory(memJson, source, err, warn) {
288
320
  if (actual !== meta.snapshot_sha256) err(`skills-memory: snapshot_sha256 mismatch: declared ${meta.snapshot_sha256}, actual ${actual}`);
289
321
  }
290
322
  }
323
+ return scopeKey;
291
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.1",
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",