@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 +35 -0
- package/README.md +15 -0
- package/bin/llms-skills.mjs +2 -2
- package/lib/core.mjs +39 -6
- package/lib/memory.mjs +17 -4
- package/package.json +1 -1
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
|
package/bin/llms-skills.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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.
|
|
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",
|