@juno-ai/bind 10.0.0 → 11.0.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 +146 -0
- package/index.d.ts +6 -2
- package/index.js +6 -2
- package/package.json +6 -2
- package/skills/activation.d.ts +64 -0
- package/skills/activation.js +39 -0
- package/skills/admission.d.ts +61 -0
- package/skills/admission.js +41 -0
- package/skills/catalog.d.ts +54 -0
- package/skills/catalog.js +77 -0
- package/skills/discovery.d.ts +82 -0
- package/skills/discovery.js +91 -0
- package/skills/index.d.ts +19 -0
- package/skills/index.js +19 -0
- package/skills/refs.d.ts +21 -0
- package/skills/refs.js +27 -0
- package/skills/registry.d.ts +57 -0
- package/skills/registry.js +94 -0
- package/skills/resolve.d.ts +89 -0
- package/skills/resolve.js +124 -0
- package/skills/sha.d.ts +53 -0
- package/skills/sha.js +60 -0
- package/skills/sha256.d.ts +38 -0
- package/skills/sha256.js +122 -0
- package/skills/skill-md.d.ts +73 -0
- package/skills/skill-md.js +149 -0
- package/skills/types.d.ts +174 -0
- package/skills/types.js +55 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 over a UTF-8 string, in plain TypeScript.
|
|
3
|
+
*
|
|
4
|
+
* **Why the package carries an implementation instead of taking a port**, when
|
|
5
|
+
* `run/receipts.ts` deliberately does the opposite. Two differences decide it.
|
|
6
|
+
*
|
|
7
|
+
* The first is *when* the digest is needed. A receipt's hash is taken inside an
|
|
8
|
+
* already-async tool dispatch, so `crypto.subtle.digest` — which is async
|
|
9
|
+
* everywhere, and is the only digest workerd has — fits without changing any
|
|
10
|
+
* signature. A skill's `contentSha` is computed at **registration**, which is
|
|
11
|
+
* the synchronous act of a module declaring what it contributes. Taking an
|
|
12
|
+
* async digest there forces either an async `register()` (so a plugin's
|
|
13
|
+
* registration becomes something a host must await and order) or a lazily
|
|
14
|
+
* resolved hash on every read path that wants one. Both cost more than this
|
|
15
|
+
* file.
|
|
16
|
+
*
|
|
17
|
+
* The second is *what the value means*. A receipt hash is persisted by the host
|
|
18
|
+
* forever and compared against rows written by earlier versions, so the host
|
|
19
|
+
* must own the algorithm choice — and its input is tool arguments, routinely
|
|
20
|
+
* user data, which is why that doc argues preimage resistance. A `contentSha`
|
|
21
|
+
* identifies build content within one deployment: it is derived from source the
|
|
22
|
+
* deployment already ships, and nothing reads it that did not just compute it.
|
|
23
|
+
*
|
|
24
|
+
* A host with a native digest can still supply one — {@link Sha256Hex} is a
|
|
25
|
+
* parameter everywhere this is used — but it must be synchronous and it must be
|
|
26
|
+
* SHA-256, or the hashes two hosts compute for the same skill diverge.
|
|
27
|
+
*
|
|
28
|
+
* The implementation is the FIPS 180-4 reference algorithm with no shortcuts.
|
|
29
|
+
* `__tests__/sha256.test.ts` pins it against the published vectors, plus a
|
|
30
|
+
* multi-block and a multi-byte-UTF-8 case, because a hash that is subtly wrong
|
|
31
|
+
* fails silently: every value still looks like a hash.
|
|
32
|
+
*/
|
|
33
|
+
/** A synchronous SHA-256 that returns lowercase hex. */
|
|
34
|
+
export type Sha256Hex = (text: string) => string;
|
|
35
|
+
/** SHA-256 (lowercase hex) of a UTF-8 string. */
|
|
36
|
+
export declare const sha256Hex: Sha256Hex;
|
|
37
|
+
/** Byte length of a string in UTF-8 — what a resource listing reports. */
|
|
38
|
+
export declare function utf8ByteLength(text: string): number;
|
package/skills/sha256.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 over a UTF-8 string, in plain TypeScript.
|
|
3
|
+
*
|
|
4
|
+
* **Why the package carries an implementation instead of taking a port**, when
|
|
5
|
+
* `run/receipts.ts` deliberately does the opposite. Two differences decide it.
|
|
6
|
+
*
|
|
7
|
+
* The first is *when* the digest is needed. A receipt's hash is taken inside an
|
|
8
|
+
* already-async tool dispatch, so `crypto.subtle.digest` — which is async
|
|
9
|
+
* everywhere, and is the only digest workerd has — fits without changing any
|
|
10
|
+
* signature. A skill's `contentSha` is computed at **registration**, which is
|
|
11
|
+
* the synchronous act of a module declaring what it contributes. Taking an
|
|
12
|
+
* async digest there forces either an async `register()` (so a plugin's
|
|
13
|
+
* registration becomes something a host must await and order) or a lazily
|
|
14
|
+
* resolved hash on every read path that wants one. Both cost more than this
|
|
15
|
+
* file.
|
|
16
|
+
*
|
|
17
|
+
* The second is *what the value means*. A receipt hash is persisted by the host
|
|
18
|
+
* forever and compared against rows written by earlier versions, so the host
|
|
19
|
+
* must own the algorithm choice — and its input is tool arguments, routinely
|
|
20
|
+
* user data, which is why that doc argues preimage resistance. A `contentSha`
|
|
21
|
+
* identifies build content within one deployment: it is derived from source the
|
|
22
|
+
* deployment already ships, and nothing reads it that did not just compute it.
|
|
23
|
+
*
|
|
24
|
+
* A host with a native digest can still supply one — {@link Sha256Hex} is a
|
|
25
|
+
* parameter everywhere this is used — but it must be synchronous and it must be
|
|
26
|
+
* SHA-256, or the hashes two hosts compute for the same skill diverge.
|
|
27
|
+
*
|
|
28
|
+
* The implementation is the FIPS 180-4 reference algorithm with no shortcuts.
|
|
29
|
+
* `__tests__/sha256.test.ts` pins it against the published vectors, plus a
|
|
30
|
+
* multi-block and a multi-byte-UTF-8 case, because a hash that is subtly wrong
|
|
31
|
+
* fails silently: every value still looks like a hash.
|
|
32
|
+
*/
|
|
33
|
+
/** Round constants: the first 32 bits of the fractional parts of the cube roots of the first 64 primes. */
|
|
34
|
+
// prettier-ignore
|
|
35
|
+
const K = new Uint32Array([
|
|
36
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
37
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
38
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
39
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
40
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
41
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
42
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
43
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
44
|
+
]);
|
|
45
|
+
function rotr(x, n) {
|
|
46
|
+
return (x >>> n) | (x << (32 - n));
|
|
47
|
+
}
|
|
48
|
+
const encoder = new TextEncoder();
|
|
49
|
+
/** SHA-256 (lowercase hex) of a UTF-8 string. */
|
|
50
|
+
export const sha256Hex = (text) => {
|
|
51
|
+
const data = encoder.encode(text);
|
|
52
|
+
// Pad to a whole number of 512-bit blocks: the 0x80 terminator, zeroes, then
|
|
53
|
+
// the message length in bits as a 64-bit big-endian integer.
|
|
54
|
+
const total = (((data.length + 9 + 63) / 64) | 0) * 64;
|
|
55
|
+
const buf = new Uint8Array(total);
|
|
56
|
+
buf.set(data);
|
|
57
|
+
buf[data.length] = 0x80;
|
|
58
|
+
const view = new DataView(buf.buffer);
|
|
59
|
+
// The bit length can exceed 32 bits for inputs over 512 MiB. Split it rather
|
|
60
|
+
// than relying on a single `setUint32`, which would silently truncate.
|
|
61
|
+
const bitLength = data.length * 8;
|
|
62
|
+
view.setUint32(total - 8, Math.floor(bitLength / 0x1_0000_0000));
|
|
63
|
+
view.setUint32(total - 4, bitLength >>> 0);
|
|
64
|
+
// Initial hash values: the first 32 bits of the fractional parts of the
|
|
65
|
+
// square roots of the first eight primes.
|
|
66
|
+
const h = new Uint32Array([
|
|
67
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
68
|
+
0x1f83d9ab, 0x5be0cd19,
|
|
69
|
+
]);
|
|
70
|
+
const w = new Uint32Array(64);
|
|
71
|
+
for (let offset = 0; offset < total; offset += 64) {
|
|
72
|
+
for (let i = 0; i < 16; i += 1)
|
|
73
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
74
|
+
for (let i = 16; i < 64; i += 1) {
|
|
75
|
+
const x = w[i - 15];
|
|
76
|
+
const y = w[i - 2];
|
|
77
|
+
const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3);
|
|
78
|
+
const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10);
|
|
79
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
|
80
|
+
}
|
|
81
|
+
let a = h[0];
|
|
82
|
+
let b = h[1];
|
|
83
|
+
let c = h[2];
|
|
84
|
+
let d = h[3];
|
|
85
|
+
let e = h[4];
|
|
86
|
+
let f = h[5];
|
|
87
|
+
let g = h[6];
|
|
88
|
+
let hh = h[7];
|
|
89
|
+
for (let i = 0; i < 64; i += 1) {
|
|
90
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
91
|
+
const ch = (e & f) ^ (~e & g);
|
|
92
|
+
const t1 = (hh + s1 + ch + K[i] + w[i]) | 0;
|
|
93
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
94
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
95
|
+
const t2 = (s0 + maj) | 0;
|
|
96
|
+
hh = g;
|
|
97
|
+
g = f;
|
|
98
|
+
f = e;
|
|
99
|
+
e = (d + t1) | 0;
|
|
100
|
+
d = c;
|
|
101
|
+
c = b;
|
|
102
|
+
b = a;
|
|
103
|
+
a = (t1 + t2) | 0;
|
|
104
|
+
}
|
|
105
|
+
h[0] += a;
|
|
106
|
+
h[1] += b;
|
|
107
|
+
h[2] += c;
|
|
108
|
+
h[3] += d;
|
|
109
|
+
h[4] += e;
|
|
110
|
+
h[5] += f;
|
|
111
|
+
h[6] += g;
|
|
112
|
+
h[7] += hh;
|
|
113
|
+
}
|
|
114
|
+
let hex = "";
|
|
115
|
+
for (let i = 0; i < 8; i += 1)
|
|
116
|
+
hex += h[i].toString(16).padStart(8, "0");
|
|
117
|
+
return hex;
|
|
118
|
+
};
|
|
119
|
+
/** Byte length of a string in UTF-8 — what a resource listing reports. */
|
|
120
|
+
export function utf8ByteLength(text) {
|
|
121
|
+
return encoder.encode(text).length;
|
|
122
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SKILL.md` — the interchange format.
|
|
3
|
+
*
|
|
4
|
+
* A deployment's canonical storage is its own (a database row, a directory of
|
|
5
|
+
* files), but the portable form is the de-facto `SKILL.md` bundle: a `---`
|
|
6
|
+
* fenced YAML frontmatter block followed by a markdown body, as used by
|
|
7
|
+
* Anthropic's skills and agentskills.io. It is how a skill authored in one
|
|
8
|
+
* place is read in another, which makes both directions of this file a
|
|
9
|
+
* compatibility surface rather than an internal convenience.
|
|
10
|
+
*
|
|
11
|
+
* **Import is deliberately lenient and export is deliberately strict.** The
|
|
12
|
+
* input is a document a person wrote, often in another tool, and the two
|
|
13
|
+
* outcomes are not symmetric: refusing a skill over a cosmetic frontmatter
|
|
14
|
+
* mistake loses knowledge the author already wrote down, while accepting a
|
|
15
|
+
* malformed one costs a warning. So parsing repairs what it can, warns about
|
|
16
|
+
* what it repaired, and fails on exactly two things — frontmatter that is not
|
|
17
|
+
* YAML at all, and a missing `description`, which is the one field with no
|
|
18
|
+
* sensible default because it is the entire Tier-1 catalog line.
|
|
19
|
+
*
|
|
20
|
+
* **YAML arrives as a port.** The package takes peer dependencies only, and a
|
|
21
|
+
* YAML implementation is neither a peer the harness can assume nor something
|
|
22
|
+
* it should bundle. Every host that reads `SKILL.md` already has one — the
|
|
23
|
+
* seam is two functions wide.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The host's YAML implementation. `js-yaml`'s `load`/`dump` and the `yaml`
|
|
27
|
+
* package's `parse`/`stringify` both satisfy it directly.
|
|
28
|
+
*
|
|
29
|
+
* `parse` must **throw** on invalid YAML rather than returning a sentinel: the
|
|
30
|
+
* repair pass below is driven by the throw, so a parser that returns
|
|
31
|
+
* `undefined` instead would skip the repair and reject documents this module
|
|
32
|
+
* is meant to accept.
|
|
33
|
+
*/
|
|
34
|
+
export interface SkillYamlCodec {
|
|
35
|
+
parse: (text: string) => unknown;
|
|
36
|
+
/** Must emit block-style YAML and must not wrap long lines. */
|
|
37
|
+
stringify: (value: Record<string, unknown>) => string;
|
|
38
|
+
}
|
|
39
|
+
export interface ParsedSkillMd {
|
|
40
|
+
/** `null` when frontmatter omits it — the caller supplies a fallback (usually the filename). */
|
|
41
|
+
name: string | null;
|
|
42
|
+
description: string;
|
|
43
|
+
whenToUse: string | null;
|
|
44
|
+
body: string;
|
|
45
|
+
/** Unknown frontmatter keys, preserved for round-trip and read-only display. */
|
|
46
|
+
metadata: Record<string, unknown> | null;
|
|
47
|
+
}
|
|
48
|
+
export type SkillMdParseResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
skill: ParsedSkillMd;
|
|
51
|
+
warnings: string[];
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
error: string;
|
|
55
|
+
warnings: string[];
|
|
56
|
+
};
|
|
57
|
+
/** Parse a raw `SKILL.md` (frontmatter + body) into importable fields. */
|
|
58
|
+
export declare function parseSkillMarkdown(raw: string, yaml: SkillYamlCodec): SkillMdParseResult;
|
|
59
|
+
/**
|
|
60
|
+
* Serialize a stored skill back to canonical `SKILL.md` text.
|
|
61
|
+
*
|
|
62
|
+
* Preserved unknown keys are written first and the canonical keys last, so a
|
|
63
|
+
* stray `name` / `description` / `when_to_use` that ended up inside `metadata`
|
|
64
|
+
* — which an earlier lenient import makes possible — cannot clobber the real
|
|
65
|
+
* value on the way out.
|
|
66
|
+
*/
|
|
67
|
+
export declare function serializeSkillMarkdown(skill: {
|
|
68
|
+
name: string;
|
|
69
|
+
description: string;
|
|
70
|
+
whenToUse: string | null;
|
|
71
|
+
body: string;
|
|
72
|
+
metadata: Record<string, unknown> | null;
|
|
73
|
+
}, yaml: SkillYamlCodec): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SKILL.md` — the interchange format.
|
|
3
|
+
*
|
|
4
|
+
* A deployment's canonical storage is its own (a database row, a directory of
|
|
5
|
+
* files), but the portable form is the de-facto `SKILL.md` bundle: a `---`
|
|
6
|
+
* fenced YAML frontmatter block followed by a markdown body, as used by
|
|
7
|
+
* Anthropic's skills and agentskills.io. It is how a skill authored in one
|
|
8
|
+
* place is read in another, which makes both directions of this file a
|
|
9
|
+
* compatibility surface rather than an internal convenience.
|
|
10
|
+
*
|
|
11
|
+
* **Import is deliberately lenient and export is deliberately strict.** The
|
|
12
|
+
* input is a document a person wrote, often in another tool, and the two
|
|
13
|
+
* outcomes are not symmetric: refusing a skill over a cosmetic frontmatter
|
|
14
|
+
* mistake loses knowledge the author already wrote down, while accepting a
|
|
15
|
+
* malformed one costs a warning. So parsing repairs what it can, warns about
|
|
16
|
+
* what it repaired, and fails on exactly two things — frontmatter that is not
|
|
17
|
+
* YAML at all, and a missing `description`, which is the one field with no
|
|
18
|
+
* sensible default because it is the entire Tier-1 catalog line.
|
|
19
|
+
*
|
|
20
|
+
* **YAML arrives as a port.** The package takes peer dependencies only, and a
|
|
21
|
+
* YAML implementation is neither a peer the harness can assume nor something
|
|
22
|
+
* it should bundle. Every host that reads `SKILL.md` already has one — the
|
|
23
|
+
* seam is two functions wide.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Frontmatter keys this module owns. Everything else is preserved verbatim in
|
|
27
|
+
* `metadata` — a host or another tool may put anything here, and dropping it
|
|
28
|
+
* would make an import/export round-trip lossy.
|
|
29
|
+
*/
|
|
30
|
+
const KNOWN_KEYS = new Set(["name", "description", "when_to_use", "whenToUse", "when-to-use"]);
|
|
31
|
+
/** Lower-kebab, at most 64 characters — the shape a skill name must canonicalize to. */
|
|
32
|
+
const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
33
|
+
const NAME_MAX = 64;
|
|
34
|
+
function splitFrontmatter(raw) {
|
|
35
|
+
const text = raw.replace(/^/, ""); // a BOM before the fence is common from Windows editors
|
|
36
|
+
if (!text.startsWith("---"))
|
|
37
|
+
return null;
|
|
38
|
+
const rest = text.slice(3).replace(/^\r?\n/, "");
|
|
39
|
+
const endIndex = rest.search(/\r?\n---\s*(\r?\n|$)/);
|
|
40
|
+
if (endIndex === -1)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
frontmatter: rest.slice(0, endIndex),
|
|
44
|
+
body: rest.slice(endIndex).replace(/^\r?\n---\s*(\r?\n)?/, ""),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Repair the one frontmatter mistake that dominates real imports: an unquoted
|
|
49
|
+
* value containing a colon (`description: Use when X: do Y`), which YAML reads
|
|
50
|
+
* as a nested mapping and rejects. Quote any `key: value` whose value has an
|
|
51
|
+
* unquoted inner colon.
|
|
52
|
+
*
|
|
53
|
+
* A value opening with `[` or `{` is a flow collection — valid YAML where the
|
|
54
|
+
* colons are structural — and is skipped. Without that exception a single
|
|
55
|
+
* unrelated bad line, which forces this whole-block pass, would collateral-
|
|
56
|
+
* quote a legitimate inline array or object into a plain string.
|
|
57
|
+
*/
|
|
58
|
+
function repairUnquotedColons(frontmatter) {
|
|
59
|
+
return frontmatter
|
|
60
|
+
.split(/\r?\n/)
|
|
61
|
+
.map((line) => {
|
|
62
|
+
const match = line.match(/^(\s*[A-Za-z0-9_-]+:\s+)(.*)$/);
|
|
63
|
+
if (!match)
|
|
64
|
+
return line;
|
|
65
|
+
const value = match[2];
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
const structured = /^["'[{]/.test(trimmed) || trimmed.length === 0;
|
|
68
|
+
if (structured || !value.includes(":"))
|
|
69
|
+
return line;
|
|
70
|
+
return `${match[1]}"${value.replace(/"/g, '\\"')}"`;
|
|
71
|
+
})
|
|
72
|
+
.join("\n");
|
|
73
|
+
}
|
|
74
|
+
function loadYaml(frontmatter, yaml) {
|
|
75
|
+
const warnings = [];
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = yaml.parse(frontmatter);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
try {
|
|
82
|
+
parsed = yaml.parse(repairUnquotedColons(frontmatter));
|
|
83
|
+
warnings.push("Frontmatter had unquoted colon values; auto-quoted them on import.");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
90
|
+
return null;
|
|
91
|
+
return { object: parsed, warnings };
|
|
92
|
+
}
|
|
93
|
+
/** Parse a raw `SKILL.md` (frontmatter + body) into importable fields. */
|
|
94
|
+
export function parseSkillMarkdown(raw, yaml) {
|
|
95
|
+
const split = splitFrontmatter(raw);
|
|
96
|
+
if (!split) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: "Not a SKILL.md — expected a `---` YAML frontmatter block.",
|
|
100
|
+
warnings: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const loaded = loadYaml(split.frontmatter, yaml);
|
|
104
|
+
if (!loaded) {
|
|
105
|
+
return { ok: false, error: "Frontmatter is not valid YAML.", warnings: [] };
|
|
106
|
+
}
|
|
107
|
+
const { object, warnings } = loaded;
|
|
108
|
+
const description = typeof object.description === "string" ? object.description.trim() : "";
|
|
109
|
+
if (!description) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: "SKILL.md is missing a required `description`.",
|
|
113
|
+
warnings,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const rawName = typeof object.name === "string" ? object.name.trim() : "";
|
|
117
|
+
const name = rawName.length > 0 ? rawName : null;
|
|
118
|
+
if (name && (name.length > NAME_MAX || !NAME_PATTERN.test(name))) {
|
|
119
|
+
warnings.push(`Frontmatter name "${name}" is not lower-kebab-case ≤ ${NAME_MAX} chars; it will be normalized.`);
|
|
120
|
+
}
|
|
121
|
+
// Three spellings, because all three appear in the wild.
|
|
122
|
+
const hintRaw = object.when_to_use ?? object.whenToUse ?? object["when-to-use"] ?? null;
|
|
123
|
+
const whenToUse = typeof hintRaw === "string" && hintRaw.trim() ? hintRaw.trim() : null;
|
|
124
|
+
const extra = Object.entries(object).filter(([key]) => !KNOWN_KEYS.has(key));
|
|
125
|
+
const metadata = extra.length > 0 ? Object.fromEntries(extra) : null;
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
skill: { name, description, whenToUse, body: split.body.trim(), metadata },
|
|
129
|
+
warnings,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Serialize a stored skill back to canonical `SKILL.md` text.
|
|
134
|
+
*
|
|
135
|
+
* Preserved unknown keys are written first and the canonical keys last, so a
|
|
136
|
+
* stray `name` / `description` / `when_to_use` that ended up inside `metadata`
|
|
137
|
+
* — which an earlier lenient import makes possible — cannot clobber the real
|
|
138
|
+
* value on the way out.
|
|
139
|
+
*/
|
|
140
|
+
export function serializeSkillMarkdown(skill, yaml) {
|
|
141
|
+
const front = { ...(skill.metadata ?? {}) };
|
|
142
|
+
front.name = skill.name;
|
|
143
|
+
front.description = skill.description;
|
|
144
|
+
if (skill.whenToUse)
|
|
145
|
+
front.when_to_use = skill.whenToUse;
|
|
146
|
+
else
|
|
147
|
+
delete front.when_to_use;
|
|
148
|
+
return `---\n${yaml.stringify(front).trimEnd()}\n---\n\n${skill.body.trim()}\n`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skill vocabulary — the *knowledge* sibling of `src/plugins/tool.ts`.
|
|
3
|
+
*
|
|
4
|
+
* A skill is a markdown-shaped procedure or reference module, disclosed to a
|
|
5
|
+
* model in three tiers: a one-line catalog entry it always sees (Tier 1), a
|
|
6
|
+
* body injected into the system prompt once loaded (Tier 2), and bundled
|
|
7
|
+
* resources it may read after loading (Tier 3). That progression is the whole
|
|
8
|
+
* point: a workspace's accumulated know-how does not fit in a context window,
|
|
9
|
+
* and a catalog line costs ~50 tokens where a body costs thousands.
|
|
10
|
+
*
|
|
11
|
+
* Two sources of skill exist and they differ in exactly one way — where the
|
|
12
|
+
* body comes from. A **code skill** is registered from the deployment's own
|
|
13
|
+
* source (see {@link SkillDef}) and its body is a build artifact; an
|
|
14
|
+
* **external skill** is data the host stores and can edit at runtime. The
|
|
15
|
+
* harness owns the first entirely and knows nothing about the second beyond
|
|
16
|
+
* the shape it resolves to, which is why {@link SkillSummary} is the only type
|
|
17
|
+
* both sides share.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Where a resolved skill came from.
|
|
21
|
+
*
|
|
22
|
+
* - `platform` — code-registered and unowned: available to every agent.
|
|
23
|
+
* - `plugin` — code-registered and contributed by a tool plugin: available
|
|
24
|
+
* only to an agent that can load that plugin, since a recipe for tools the
|
|
25
|
+
* agent cannot call is noise.
|
|
26
|
+
* - `tenant` — authored in the **host's data store** rather than in code. The
|
|
27
|
+
* name is Monad's (a tenant is its workspace unit) and is kept because it is
|
|
28
|
+
* the value hosts already persist and render; read it as "host-managed".
|
|
29
|
+
*
|
|
30
|
+
* The order is also the precedence order — see `compareSkillSummaries`.
|
|
31
|
+
*/
|
|
32
|
+
export type SkillOrigin = "platform" | "plugin" | "tenant";
|
|
33
|
+
/** `reference` = inline text the model reads; `asset` = an opaque blob, summarised rather than inlined. */
|
|
34
|
+
export type SkillResourceKind = "reference" | "asset";
|
|
35
|
+
/**
|
|
36
|
+
* A Tier-3 resource bundled with a code skill. Code-skill resources are static
|
|
37
|
+
* assets of the deploy artifact, so their text is a function rather than a
|
|
38
|
+
* handle: `render()` is called to hash the resource at registration and again
|
|
39
|
+
* to read it. Binary `asset` resources belong to host-stored skills, where
|
|
40
|
+
* there is somewhere to put the bytes.
|
|
41
|
+
*/
|
|
42
|
+
export interface SkillResourceDef {
|
|
43
|
+
/** Relative path, e.g. `references/spec.md`. Unique within the skill. */
|
|
44
|
+
path: string;
|
|
45
|
+
/** Always `reference` for a code skill — there is no blob store in a build artifact. */
|
|
46
|
+
kind: Extract<SkillResourceKind, "reference">;
|
|
47
|
+
/** MIME type, e.g. `text/markdown`. */
|
|
48
|
+
contentType: string;
|
|
49
|
+
/** The inline text body. Must be deterministic: it feeds the content hash. */
|
|
50
|
+
render: () => string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A code-registered skill — what a plugin contributes or a standalone module
|
|
54
|
+
* registers. `render()` must be deterministic for a given build: its output is
|
|
55
|
+
* hashed into the skill's `contentSha`, which is what pins an eval or a replay
|
|
56
|
+
* to the exact instructions a run actually saw.
|
|
57
|
+
*/
|
|
58
|
+
export interface SkillDef {
|
|
59
|
+
/** Lower-kebab; unique across a registry. */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Tier-1 catalog line: what the skill does. */
|
|
62
|
+
description: string;
|
|
63
|
+
/**
|
|
64
|
+
* Activation hint appended to the catalog line — when to reach for this.
|
|
65
|
+
* Required for a code skill: without it the catalog line says what the skill
|
|
66
|
+
* is but not when it applies, which is the half that decides routing.
|
|
67
|
+
*/
|
|
68
|
+
whenToUse: string;
|
|
69
|
+
/**
|
|
70
|
+
* Set for a plugin-contributed skill. Filled by the registration path from
|
|
71
|
+
* the owning plugin's name rather than read from the def — see
|
|
72
|
+
* `createSkillRegistry`.
|
|
73
|
+
*/
|
|
74
|
+
ownerPlugin?: string;
|
|
75
|
+
/** The Tier-2 markdown body. */
|
|
76
|
+
render: () => string;
|
|
77
|
+
/** Tier-3 bundled reference resources. */
|
|
78
|
+
resources?: SkillResourceDef[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The Tier-1 projection every source resolves to — the analogue of
|
|
82
|
+
* `PluginSummary`. The catalog and the activation set are both built from
|
|
83
|
+
* these, so neither has to know whether a skill came from code or from the
|
|
84
|
+
* host's store.
|
|
85
|
+
*/
|
|
86
|
+
export interface SkillSummary {
|
|
87
|
+
/**
|
|
88
|
+
* The activation ref: what a host persists as the run's active-skill set.
|
|
89
|
+
* `platform:<name>` for a code skill (see `refs.ts`), the host's own opaque
|
|
90
|
+
* identifier for an external one. One namespaced form spans both sources
|
|
91
|
+
* without ambiguity; the catalog still renders the plain `name`.
|
|
92
|
+
*/
|
|
93
|
+
ref: string;
|
|
94
|
+
/** Lower-kebab identifier, unique within the resolved catalog. */
|
|
95
|
+
name: string;
|
|
96
|
+
/** Tier-1 catalog line. */
|
|
97
|
+
description: string;
|
|
98
|
+
/** Optional activation hint, appended to the catalog line. */
|
|
99
|
+
whenToUse: string | null;
|
|
100
|
+
/** Set for a plugin-contributed skill (and for a host skill that shadows one). */
|
|
101
|
+
ownerPlugin: string | null;
|
|
102
|
+
/** Which of the three sources this came from. */
|
|
103
|
+
origin: SkillOrigin;
|
|
104
|
+
}
|
|
105
|
+
/** A registered code skill, as the registry hands it back. */
|
|
106
|
+
export interface RegisteredSkill {
|
|
107
|
+
def: SkillDef;
|
|
108
|
+
/** `platform` (standalone) or `plugin` (contributed). Never `tenant`: code only. */
|
|
109
|
+
origin: Extract<SkillOrigin, "platform" | "plugin">;
|
|
110
|
+
/** The owning plugin's name for a `plugin` skill; null for `platform`. */
|
|
111
|
+
ownerPlugin: string | null;
|
|
112
|
+
/** Content hash over every version-defining input, computed at registration. */
|
|
113
|
+
contentSha: string;
|
|
114
|
+
}
|
|
115
|
+
/** A Tier-3 resource as the catalog and the wrapper see it — metadata, not bytes. */
|
|
116
|
+
export interface SkillResourceRef {
|
|
117
|
+
path: string;
|
|
118
|
+
kind: SkillResourceKind;
|
|
119
|
+
/** Size in bytes. Rendered only for `asset`, which is never inlined. */
|
|
120
|
+
bytes: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A skill body resolved for injection, whatever its source. The host's
|
|
124
|
+
* external source returns these; the registry-backed code path produces them
|
|
125
|
+
* internally.
|
|
126
|
+
*/
|
|
127
|
+
export interface ResolvedSkillBody {
|
|
128
|
+
name: string;
|
|
129
|
+
ownerPlugin: string | null;
|
|
130
|
+
/**
|
|
131
|
+
* Monotonic revision for a host-stored skill, rendered into the wrapper so
|
|
132
|
+
* the model can cite what it read. `null` for a code skill, whose version
|
|
133
|
+
* history is the deployment's source control rather than a number.
|
|
134
|
+
*/
|
|
135
|
+
version: number | null;
|
|
136
|
+
body: string;
|
|
137
|
+
/** The content hash pinned into the run's `loadedSkillShas`. */
|
|
138
|
+
contentSha: string;
|
|
139
|
+
resources: SkillResourceRef[];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Rough token budget for the Tier-1 catalog. Past it the catalog demotes its
|
|
143
|
+
* overflow to names only rather than dropping entries: a name the model can
|
|
144
|
+
* still pass to `load_skill` beats a skill it cannot discover at all.
|
|
145
|
+
*/
|
|
146
|
+
export declare const SKILL_CATALOG_TOKEN_BUDGET = 2500;
|
|
147
|
+
/**
|
|
148
|
+
* Max skills one session may hold loaded at once. The count half of the active
|
|
149
|
+
* bound — cheap to check before anything is resolved.
|
|
150
|
+
*/
|
|
151
|
+
export declare const SKILL_MAX_ACTIVE_PER_SESSION = 12;
|
|
152
|
+
/**
|
|
153
|
+
* Combined token budget for all loaded bodies. The other half of the active
|
|
154
|
+
* bound, and the one that actually protects the context window: twelve small
|
|
155
|
+
* skills are fine and three large ones are not, so a count cap alone does not
|
|
156
|
+
* bound the prompt.
|
|
157
|
+
*/
|
|
158
|
+
export declare const SKILL_MAX_ACTIVE_BODY_TOKENS = 40000;
|
|
159
|
+
/**
|
|
160
|
+
* Characters per token, for every estimate in this module. Deliberately crude:
|
|
161
|
+
* these budgets choose between "render the line" and "render the name", and a
|
|
162
|
+
* real tokenizer would cost more than the decision is worth.
|
|
163
|
+
*/
|
|
164
|
+
export declare const SKILL_CHARS_PER_TOKEN = 4;
|
|
165
|
+
/** The module's shared estimator. */
|
|
166
|
+
export declare function estimateSkillTokens(text: string): number;
|
|
167
|
+
/**
|
|
168
|
+
* Observe something the harness will otherwise carry on past — a name
|
|
169
|
+
* collision, a pinned body that has drifted, an activation of a skill that is
|
|
170
|
+
* not in the catalog. A port rather than a logger because the package has no
|
|
171
|
+
* opinion about where a host's warnings go, and because a test needs to see
|
|
172
|
+
* them without scraping stdout.
|
|
173
|
+
*/
|
|
174
|
+
export type SkillWarn = (message: string, fields: Record<string, unknown>) => void;
|
package/skills/types.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skill vocabulary — the *knowledge* sibling of `src/plugins/tool.ts`.
|
|
3
|
+
*
|
|
4
|
+
* A skill is a markdown-shaped procedure or reference module, disclosed to a
|
|
5
|
+
* model in three tiers: a one-line catalog entry it always sees (Tier 1), a
|
|
6
|
+
* body injected into the system prompt once loaded (Tier 2), and bundled
|
|
7
|
+
* resources it may read after loading (Tier 3). That progression is the whole
|
|
8
|
+
* point: a workspace's accumulated know-how does not fit in a context window,
|
|
9
|
+
* and a catalog line costs ~50 tokens where a body costs thousands.
|
|
10
|
+
*
|
|
11
|
+
* Two sources of skill exist and they differ in exactly one way — where the
|
|
12
|
+
* body comes from. A **code skill** is registered from the deployment's own
|
|
13
|
+
* source (see {@link SkillDef}) and its body is a build artifact; an
|
|
14
|
+
* **external skill** is data the host stores and can edit at runtime. The
|
|
15
|
+
* harness owns the first entirely and knows nothing about the second beyond
|
|
16
|
+
* the shape it resolves to, which is why {@link SkillSummary} is the only type
|
|
17
|
+
* both sides share.
|
|
18
|
+
*/
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Bounds
|
|
21
|
+
//
|
|
22
|
+
// Every one of these bounds the *prompt*, not a database. They are defaults: a
|
|
23
|
+
// host passes its own where the call takes one. What they must not be is
|
|
24
|
+
// absent — an unbounded catalog and an unbounded active set are the two ways a
|
|
25
|
+
// skill library silently degrades every run in the deployment rather than
|
|
26
|
+
// failing one of them.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Rough token budget for the Tier-1 catalog. Past it the catalog demotes its
|
|
30
|
+
* overflow to names only rather than dropping entries: a name the model can
|
|
31
|
+
* still pass to `load_skill` beats a skill it cannot discover at all.
|
|
32
|
+
*/
|
|
33
|
+
export const SKILL_CATALOG_TOKEN_BUDGET = 2500;
|
|
34
|
+
/**
|
|
35
|
+
* Max skills one session may hold loaded at once. The count half of the active
|
|
36
|
+
* bound — cheap to check before anything is resolved.
|
|
37
|
+
*/
|
|
38
|
+
export const SKILL_MAX_ACTIVE_PER_SESSION = 12;
|
|
39
|
+
/**
|
|
40
|
+
* Combined token budget for all loaded bodies. The other half of the active
|
|
41
|
+
* bound, and the one that actually protects the context window: twelve small
|
|
42
|
+
* skills are fine and three large ones are not, so a count cap alone does not
|
|
43
|
+
* bound the prompt.
|
|
44
|
+
*/
|
|
45
|
+
export const SKILL_MAX_ACTIVE_BODY_TOKENS = 40_000;
|
|
46
|
+
/**
|
|
47
|
+
* Characters per token, for every estimate in this module. Deliberately crude:
|
|
48
|
+
* these budgets choose between "render the line" and "render the name", and a
|
|
49
|
+
* real tokenizer would cost more than the decision is worth.
|
|
50
|
+
*/
|
|
51
|
+
export const SKILL_CHARS_PER_TOKEN = 4;
|
|
52
|
+
/** The module's shared estimator. */
|
|
53
|
+
export function estimateSkillTokens(text) {
|
|
54
|
+
return Math.ceil(text.length / SKILL_CHARS_PER_TOKEN);
|
|
55
|
+
}
|