@graphorin/skills 0.5.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/anthropic-spec-snapshot.json +114 -0
  5. package/dist/activation/index.d.ts +30 -0
  6. package/dist/activation/index.d.ts.map +1 -0
  7. package/dist/activation/index.js +71 -0
  8. package/dist/activation/index.js.map +1 -0
  9. package/dist/errors/index.d.ts +110 -0
  10. package/dist/errors/index.d.ts.map +1 -0
  11. package/dist/errors/index.js +126 -0
  12. package/dist/errors/index.js.map +1 -0
  13. package/dist/frontmatter/index.d.ts +120 -0
  14. package/dist/frontmatter/index.d.ts.map +1 -0
  15. package/dist/frontmatter/index.js +451 -0
  16. package/dist/frontmatter/index.js.map +1 -0
  17. package/dist/index.d.ts +52 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +51 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/loader/index.d.ts +55 -0
  22. package/dist/loader/index.d.ts.map +1 -0
  23. package/dist/loader/index.js +463 -0
  24. package/dist/loader/index.js.map +1 -0
  25. package/dist/migration/index.d.ts +62 -0
  26. package/dist/migration/index.d.ts.map +1 -0
  27. package/dist/migration/index.js +131 -0
  28. package/dist/migration/index.js.map +1 -0
  29. package/dist/registry/bridge.d.ts +47 -0
  30. package/dist/registry/bridge.d.ts.map +1 -0
  31. package/dist/registry/bridge.js +91 -0
  32. package/dist/registry/bridge.js.map +1 -0
  33. package/dist/registry/index.d.ts +140 -0
  34. package/dist/registry/index.d.ts.map +1 -0
  35. package/dist/registry/index.js +193 -0
  36. package/dist/registry/index.js.map +1 -0
  37. package/dist/spec/index.d.ts +92 -0
  38. package/dist/spec/index.d.ts.map +1 -0
  39. package/dist/spec/index.js +105 -0
  40. package/dist/spec/index.js.map +1 -0
  41. package/dist/types/index.d.ts +305 -0
  42. package/dist/types/index.d.ts.map +1 -0
  43. package/package.json +104 -0
@@ -0,0 +1,131 @@
1
+ import { SkillManifestParseError } from "../errors/index.js";
2
+ import { getSpecSnapshot } from "../spec/index.js";
3
+ import { splitSkillMd } from "../frontmatter/index.js";
4
+ import { parse, stringify } from "yaml";
5
+
6
+ //#region src/migration/index.ts
7
+ /**
8
+ * `migrate-frontmatter` — idempotent rewrite helper that migrates
9
+ * legacy `graphorin-*` frontmatter fields onto their upstream
10
+ * equivalents per the `deprecate-graphorin-prefix` mappings recorded
11
+ * in the bundled spec snapshot.
12
+ *
13
+ * The function is dry-run by default — callers must opt in to
14
+ * persisting the rewritten bytes. The CLI binary in Phase 15 wraps
15
+ * this surface; the library is exposed here so other tooling can
16
+ * reuse it.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ /**
21
+ * Migrate the bundled `deprecate-graphorin-prefix` mappings on a
22
+ * single SKILL.md. The function is idempotent: re-running it on an
23
+ * already-migrated SKILL.md returns `changed: false` and an empty
24
+ * `rewrites` array.
25
+ *
26
+ * @stable
27
+ */
28
+ function migrateFrontmatter(skillMd, options = {}) {
29
+ const skillId = options.skillId ?? "<inline>";
30
+ const apply = options.apply ?? false;
31
+ const split = splitSkillMd(skillMd);
32
+ let frontmatter;
33
+ try {
34
+ frontmatter = parse(split.frontmatter);
35
+ } catch (err) {
36
+ throw new SkillManifestParseError(`Skill '${skillId}' frontmatter is not valid YAML.`, { cause: err });
37
+ }
38
+ if (frontmatter === null || typeof frontmatter !== "object" || Array.isArray(frontmatter)) throw new SkillManifestParseError(`Skill '${skillId}' frontmatter must be a top-level object.`);
39
+ const fm = { ...frontmatter };
40
+ const snapshot = getSpecSnapshot();
41
+ const rewrites = [];
42
+ let mutated = false;
43
+ for (const [graphorinField, mapping] of Object.entries(snapshot.graphorinMapping)) {
44
+ if (!(graphorinField in fm)) continue;
45
+ if (mapping.policy !== "deprecate-graphorin-prefix") {
46
+ rewrites.push(Object.freeze({
47
+ fromField: graphorinField,
48
+ toField: graphorinField,
49
+ reason: mapping.policy === "co-exist" ? "co-exist-noop" : "graphorin-only-noop",
50
+ applied: false
51
+ }));
52
+ continue;
53
+ }
54
+ if (mapping.anthropicEquivalent === null) {
55
+ rewrites.push(Object.freeze({
56
+ fromField: graphorinField,
57
+ toField: graphorinField,
58
+ reason: "graphorin-only-noop",
59
+ applied: false
60
+ }));
61
+ continue;
62
+ }
63
+ if (mapping.anthropicEquivalent in fm) {
64
+ rewrites.push(Object.freeze({
65
+ fromField: graphorinField,
66
+ toField: mapping.anthropicEquivalent,
67
+ reason: "deprecate-graphorin-prefix",
68
+ applied: false
69
+ }));
70
+ continue;
71
+ }
72
+ if (apply) {
73
+ fm[mapping.anthropicEquivalent] = fm[graphorinField];
74
+ delete fm[graphorinField];
75
+ mutated = true;
76
+ }
77
+ rewrites.push(Object.freeze({
78
+ fromField: graphorinField,
79
+ toField: mapping.anthropicEquivalent,
80
+ reason: "deprecate-graphorin-prefix",
81
+ applied: apply
82
+ }));
83
+ }
84
+ if (!apply || !mutated) return Object.freeze({
85
+ skillId,
86
+ rewrites: Object.freeze(rewrites),
87
+ originalSkillMd: skillMd,
88
+ migratedSkillMd: skillMd,
89
+ changed: false
90
+ });
91
+ const migratedSkillMd = `---\n${stringify(sortKeysAnthropicFirst(fm), {
92
+ sortMapEntries: false,
93
+ lineWidth: 0
94
+ }).trimEnd()}\n---\n${split.body}`;
95
+ return Object.freeze({
96
+ skillId,
97
+ rewrites: Object.freeze(rewrites),
98
+ originalSkillMd: skillMd,
99
+ migratedSkillMd,
100
+ changed: true
101
+ });
102
+ }
103
+ /**
104
+ * Stable key ordering: Anthropic-base fields first (in their snapshot
105
+ * insertion order), then the `metadata` bucket, then the
106
+ * `graphorin-*` fields, then anything else. The migrator emits in
107
+ * this order so re-running the migrator on the same input yields
108
+ * identical bytes (idempotence).
109
+ *
110
+ * @stable
111
+ */
112
+ function sortKeysAnthropicFirst(frontmatter) {
113
+ const snapshot = getSpecSnapshot();
114
+ const baseKeys = Object.keys(snapshot.knownFields);
115
+ const out = {};
116
+ for (const key of baseKeys) if (key in frontmatter) out[key] = frontmatter[key];
117
+ if ("metadata" in frontmatter) out.metadata = frontmatter.metadata;
118
+ const remaining = Object.keys(frontmatter).filter((k) => !(k in out)).sort((a, b) => {
119
+ const aGraphorin = a.startsWith("graphorin-");
120
+ const bGraphorin = b.startsWith("graphorin-");
121
+ if (aGraphorin && !bGraphorin) return 1;
122
+ if (!aGraphorin && bGraphorin) return -1;
123
+ return a < b ? -1 : a > b ? 1 : 0;
124
+ });
125
+ for (const key of remaining) out[key] = frontmatter[key];
126
+ return out;
127
+ }
128
+
129
+ //#endregion
130
+ export { migrateFrontmatter, sortKeysAnthropicFirst };
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["frontmatter: unknown","parseYaml","rewrites: MigrationRewrite[]","stringifyYaml","out: Record<string, unknown>"],"sources":["../../src/migration/index.ts"],"sourcesContent":["/**\n * `migrate-frontmatter` — idempotent rewrite helper that migrates\n * legacy `graphorin-*` frontmatter fields onto their upstream\n * equivalents per the `deprecate-graphorin-prefix` mappings recorded\n * in the bundled spec snapshot.\n *\n * The function is dry-run by default — callers must opt in to\n * persisting the rewritten bytes. The CLI binary in Phase 15 wraps\n * this surface; the library is exposed here so other tooling can\n * reuse it.\n *\n * @packageDocumentation\n */\n\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport { SkillManifestParseError } from '../errors/index.js';\nimport { splitSkillMd } from '../frontmatter/index.js';\nimport { getSpecSnapshot } from '../spec/index.js';\n\n/** A single rewrite the migrator applied (or would apply in a dry-run). */\nexport interface MigrationRewrite {\n readonly fromField: string;\n readonly toField: string;\n readonly reason: 'deprecate-graphorin-prefix' | 'co-exist-noop' | 'graphorin-only-noop';\n readonly applied: boolean;\n}\n\n/** Result of a single SKILL.md migration. */\nexport interface MigrationResult {\n readonly skillId: string;\n readonly rewrites: ReadonlyArray<MigrationRewrite>;\n readonly originalSkillMd: string;\n readonly migratedSkillMd: string;\n readonly changed: boolean;\n}\n\n/** Options accepted by {@link migrateFrontmatter}. */\nexport interface MigrateFrontmatterOptions {\n /** Identifier used in audit / error messages. Defaults to `'<inline>'`. */\n readonly skillId?: string;\n /**\n * When `true`, rewrites are applied to the returned `migratedSkillMd`.\n * When `false` (default), `migratedSkillMd === originalSkillMd` and\n * the function operates as a dry-run report.\n */\n readonly apply?: boolean;\n}\n\n/**\n * Migrate the bundled `deprecate-graphorin-prefix` mappings on a\n * single SKILL.md. The function is idempotent: re-running it on an\n * already-migrated SKILL.md returns `changed: false` and an empty\n * `rewrites` array.\n *\n * @stable\n */\nexport function migrateFrontmatter(\n skillMd: string,\n options: MigrateFrontmatterOptions = {},\n): MigrationResult {\n const skillId = options.skillId ?? '<inline>';\n const apply = options.apply ?? false;\n const split = splitSkillMd(skillMd);\n let frontmatter: unknown;\n try {\n frontmatter = parseYaml(split.frontmatter);\n } catch (err) {\n throw new SkillManifestParseError(`Skill '${skillId}' frontmatter is not valid YAML.`, {\n cause: err,\n });\n }\n if (frontmatter === null || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {\n throw new SkillManifestParseError(`Skill '${skillId}' frontmatter must be a top-level object.`);\n }\n const fm = { ...(frontmatter as Record<string, unknown>) };\n\n const snapshot = getSpecSnapshot();\n const rewrites: MigrationRewrite[] = [];\n let mutated = false;\n\n for (const [graphorinField, mapping] of Object.entries(snapshot.graphorinMapping)) {\n if (!(graphorinField in fm)) continue;\n if (mapping.policy !== 'deprecate-graphorin-prefix') {\n rewrites.push(\n Object.freeze({\n fromField: graphorinField,\n toField: graphorinField,\n reason: mapping.policy === 'co-exist' ? 'co-exist-noop' : 'graphorin-only-noop',\n applied: false,\n }),\n );\n continue;\n }\n if (mapping.anthropicEquivalent === null) {\n rewrites.push(\n Object.freeze({\n fromField: graphorinField,\n toField: graphorinField,\n reason: 'graphorin-only-noop',\n applied: false,\n }),\n );\n continue;\n }\n if (mapping.anthropicEquivalent in fm) {\n // Both fields are already set — the validator will surface the\n // diagnostic at load time. The migrator does not silently drop\n // either side; the operator is expected to remove the redundant\n // `graphorin-*` field manually after reviewing the diagnostic.\n rewrites.push(\n Object.freeze({\n fromField: graphorinField,\n toField: mapping.anthropicEquivalent,\n reason: 'deprecate-graphorin-prefix',\n applied: false,\n }),\n );\n continue;\n }\n if (apply) {\n fm[mapping.anthropicEquivalent] = fm[graphorinField];\n delete fm[graphorinField];\n mutated = true;\n }\n rewrites.push(\n Object.freeze({\n fromField: graphorinField,\n toField: mapping.anthropicEquivalent,\n reason: 'deprecate-graphorin-prefix',\n applied: apply,\n }),\n );\n }\n\n if (!apply || !mutated) {\n return Object.freeze({\n skillId,\n rewrites: Object.freeze(rewrites),\n originalSkillMd: skillMd,\n migratedSkillMd: skillMd,\n changed: false,\n });\n }\n\n const sortedFm = sortKeysAnthropicFirst(fm);\n const migratedFrontmatter = stringifyYaml(sortedFm, {\n sortMapEntries: false,\n lineWidth: 0,\n }).trimEnd();\n const migratedSkillMd = `---\\n${migratedFrontmatter}\\n---\\n${split.body}`;\n\n return Object.freeze({\n skillId,\n rewrites: Object.freeze(rewrites),\n originalSkillMd: skillMd,\n migratedSkillMd,\n changed: true,\n });\n}\n\n/**\n * Stable key ordering: Anthropic-base fields first (in their snapshot\n * insertion order), then the `metadata` bucket, then the\n * `graphorin-*` fields, then anything else. The migrator emits in\n * this order so re-running the migrator on the same input yields\n * identical bytes (idempotence).\n *\n * @stable\n */\nexport function sortKeysAnthropicFirst(\n frontmatter: Record<string, unknown>,\n): Record<string, unknown> {\n const snapshot = getSpecSnapshot();\n const baseKeys = Object.keys(snapshot.knownFields);\n const out: Record<string, unknown> = {};\n for (const key of baseKeys) {\n if (key in frontmatter) out[key] = frontmatter[key];\n }\n if ('metadata' in frontmatter) out.metadata = frontmatter.metadata;\n const remaining = Object.keys(frontmatter)\n .filter((k) => !(k in out))\n .sort((a, b) => {\n const aGraphorin = a.startsWith('graphorin-');\n const bGraphorin = b.startsWith('graphorin-');\n if (aGraphorin && !bGraphorin) return 1;\n if (!aGraphorin && bGraphorin) return -1;\n return a < b ? -1 : a > b ? 1 : 0;\n });\n for (const key of remaining) out[key] = frontmatter[key];\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,mBACd,SACA,UAAqC,EAAE,EACtB;CACjB,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,QAAQ,aAAa,QAAQ;CACnC,IAAIA;AACJ,KAAI;AACF,gBAAcC,MAAU,MAAM,YAAY;UACnC,KAAK;AACZ,QAAM,IAAI,wBAAwB,UAAU,QAAQ,mCAAmC,EACrF,OAAO,KACR,CAAC;;AAEJ,KAAI,gBAAgB,QAAQ,OAAO,gBAAgB,YAAY,MAAM,QAAQ,YAAY,CACvF,OAAM,IAAI,wBAAwB,UAAU,QAAQ,2CAA2C;CAEjG,MAAM,KAAK,EAAE,GAAI,aAAyC;CAE1D,MAAM,WAAW,iBAAiB;CAClC,MAAMC,WAA+B,EAAE;CACvC,IAAI,UAAU;AAEd,MAAK,MAAM,CAAC,gBAAgB,YAAY,OAAO,QAAQ,SAAS,iBAAiB,EAAE;AACjF,MAAI,EAAE,kBAAkB,IAAK;AAC7B,MAAI,QAAQ,WAAW,8BAA8B;AACnD,YAAS,KACP,OAAO,OAAO;IACZ,WAAW;IACX,SAAS;IACT,QAAQ,QAAQ,WAAW,aAAa,kBAAkB;IAC1D,SAAS;IACV,CAAC,CACH;AACD;;AAEF,MAAI,QAAQ,wBAAwB,MAAM;AACxC,YAAS,KACP,OAAO,OAAO;IACZ,WAAW;IACX,SAAS;IACT,QAAQ;IACR,SAAS;IACV,CAAC,CACH;AACD;;AAEF,MAAI,QAAQ,uBAAuB,IAAI;AAKrC,YAAS,KACP,OAAO,OAAO;IACZ,WAAW;IACX,SAAS,QAAQ;IACjB,QAAQ;IACR,SAAS;IACV,CAAC,CACH;AACD;;AAEF,MAAI,OAAO;AACT,MAAG,QAAQ,uBAAuB,GAAG;AACrC,UAAO,GAAG;AACV,aAAU;;AAEZ,WAAS,KACP,OAAO,OAAO;GACZ,WAAW;GACX,SAAS,QAAQ;GACjB,QAAQ;GACR,SAAS;GACV,CAAC,CACH;;AAGH,KAAI,CAAC,SAAS,CAAC,QACb,QAAO,OAAO,OAAO;EACnB;EACA,UAAU,OAAO,OAAO,SAAS;EACjC,iBAAiB;EACjB,iBAAiB;EACjB,SAAS;EACV,CAAC;CAQJ,MAAM,kBAAkB,QAJIC,UADX,uBAAuB,GAAG,EACS;EAClD,gBAAgB;EAChB,WAAW;EACZ,CAAC,CAAC,SAAS,CACwC,SAAS,MAAM;AAEnE,QAAO,OAAO,OAAO;EACnB;EACA,UAAU,OAAO,OAAO,SAAS;EACjC,iBAAiB;EACjB;EACA,SAAS;EACV,CAAC;;;;;;;;;;;AAYJ,SAAgB,uBACd,aACyB;CACzB,MAAM,WAAW,iBAAiB;CAClC,MAAM,WAAW,OAAO,KAAK,SAAS,YAAY;CAClD,MAAMC,MAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,SAChB,KAAI,OAAO,YAAa,KAAI,OAAO,YAAY;AAEjD,KAAI,cAAc,YAAa,KAAI,WAAW,YAAY;CAC1D,MAAM,YAAY,OAAO,KAAK,YAAY,CACvC,QAAQ,MAAM,EAAE,KAAK,KAAK,CAC1B,MAAM,GAAG,MAAM;EACd,MAAM,aAAa,EAAE,WAAW,aAAa;EAC7C,MAAM,aAAa,EAAE,WAAW,aAAa;AAC7C,MAAI,cAAc,CAAC,WAAY,QAAO;AACtC,MAAI,CAAC,cAAc,WAAY,QAAO;AACtC,SAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;GAChC;AACJ,MAAK,MAAM,OAAO,UAAW,KAAI,OAAO,YAAY;AACpD,QAAO"}
@@ -0,0 +1,47 @@
1
+ import { Skill, SkillMetadata } from "../types/index.js";
2
+ import { resolveSandbox } from "@graphorin/security/sandbox";
3
+ import { Tool, ToolSource } from "@graphorin/core";
4
+
5
+ //#region src/registry/bridge.d.ts
6
+
7
+ /** Result of {@link stampSkillTool}. */
8
+ interface StampedSkillTool<TInput = unknown, TOutput = unknown, TDeps = unknown> {
9
+ readonly tool: Tool<TInput, TOutput, TDeps>;
10
+ readonly source: ToolSource;
11
+ /** Resolved sandbox policy after the tier resolver ran. */
12
+ readonly resolvedSandbox: ReturnType<typeof resolveSandbox>;
13
+ /** `true` when the resolver overrode the operator's choice. */
14
+ readonly sandboxForced: boolean;
15
+ /** `true` when the inbound sanitization policy was upgraded to the untrusted default. */
16
+ readonly inboundSanitizationForced: boolean;
17
+ }
18
+ /**
19
+ * Stamp a skill-bundled tool with the metadata the
20
+ * `@graphorin/tools` registry expects:
21
+ *
22
+ * 1. Derive a `ToolSource` of kind `'skill'` carrying the skill's
23
+ * name and trust level.
24
+ * 2. Run `resolveSandbox(...)` so the resulting `Tool.sandboxPolicy`
25
+ * matches the mandatory tier for untrusted skills (DEC-148).
26
+ * 3. Default the `inboundSanitization` policy to
27
+ * `'detect-and-strip-and-wrap'` for untrusted skills when the tool
28
+ * author left it unset; trusted skills inherit the operator's
29
+ * choice.
30
+ *
31
+ * The function returns a freshly-frozen `Tool` with the resolved
32
+ * `sandboxPolicy` and `inboundSanitization` baked in so downstream
33
+ * registries cannot accidentally re-inherit a relaxed policy.
34
+ *
35
+ * @stable
36
+ */
37
+ declare function stampSkillTool<TInput = unknown, TOutput = unknown, TDeps = unknown>(tool: Tool<TInput, TOutput, TDeps>, skill: Pick<Skill, 'metadata'>): StampedSkillTool<TInput, TOutput, TDeps>;
38
+ /**
39
+ * Lower-level variant accepting a raw {@link SkillMetadata} so
40
+ * fixtures and tests do not have to materialise a full {@link Skill}.
41
+ *
42
+ * @stable
43
+ */
44
+ declare function stampSkillToolFromMetadata<TInput = unknown, TOutput = unknown, TDeps = unknown>(tool: Tool<TInput, TOutput, TDeps>, metadata: SkillMetadata): StampedSkillTool<TInput, TOutput, TDeps>;
45
+ //#endregion
46
+ export { StampedSkillTool, stampSkillTool, stampSkillToolFromMetadata };
47
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","names":[],"sources":["../../src/registry/bridge.ts"],"sourcesContent":[],"mappings":";;;;;;;AAsDc,UAhCG,gBAgCH,CAAA,SAAA,OAAA,EAAA,UAAA,OAAA,EAAA,QAAA,OAAA,CAAA,CAAA;EAAL,SAAA,IAAA,EA/BQ,IA+BR,CA/Ba,MA+Bb,EA/BqB,OA+BrB,EA/B8B,KA+B9B,CAAA;EACW,SAAA,MAAA,EA/BD,UA+BC;EAAQ;EAAS,SAAA,eAAA,EA7BT,UA6BS,CAAA,OA7BS,cA6BT,CAAA;EAAlC;EAAgB,SAAA,aAAA,EAAA,OAAA;EAUH;EACH,SAAA,yBAAA,EAAA,OAAA;;;;;;;;;;;;;;;;;;;;;iBAdG,2EACR,KAAK,QAAQ,SAAS,eACrB,KAAK,qBACX,iBAAiB,QAAQ,SAAS;;;;;;;iBAUrB,uFACR,KAAK,QAAQ,SAAS,kBAClB,gBACT,iBAAiB,QAAQ,SAAS"}
@@ -0,0 +1,91 @@
1
+ import { resolveSandbox } from "@graphorin/security/sandbox";
2
+
3
+ //#region src/registry/bridge.ts
4
+ /**
5
+ * Stamp a skill-bundled tool with the metadata the
6
+ * `@graphorin/tools` registry expects:
7
+ *
8
+ * 1. Derive a `ToolSource` of kind `'skill'` carrying the skill's
9
+ * name and trust level.
10
+ * 2. Run `resolveSandbox(...)` so the resulting `Tool.sandboxPolicy`
11
+ * matches the mandatory tier for untrusted skills (DEC-148).
12
+ * 3. Default the `inboundSanitization` policy to
13
+ * `'detect-and-strip-and-wrap'` for untrusted skills when the tool
14
+ * author left it unset; trusted skills inherit the operator's
15
+ * choice.
16
+ *
17
+ * The function returns a freshly-frozen `Tool` with the resolved
18
+ * `sandboxPolicy` and `inboundSanitization` baked in so downstream
19
+ * registries cannot accidentally re-inherit a relaxed policy.
20
+ *
21
+ * @stable
22
+ */
23
+ function stampSkillTool(tool, skill) {
24
+ return stampSkillToolFromMetadata(tool, skill.metadata);
25
+ }
26
+ /**
27
+ * Lower-level variant accepting a raw {@link SkillMetadata} so
28
+ * fixtures and tests do not have to materialise a full {@link Skill}.
29
+ *
30
+ * @stable
31
+ */
32
+ function stampSkillToolFromMetadata(tool, metadata) {
33
+ const sandboxResolverArgs = { trustLevel: mapTrustLevel(metadata.graphorinTrustLevel) };
34
+ if (tool.name !== void 0) sandboxResolverArgs.toolName = tool.name;
35
+ if (metadata.name !== void 0) sandboxResolverArgs.skillName = metadata.name;
36
+ const operatorOverride = sandboxOverrideFromTool(tool);
37
+ if (operatorOverride !== void 0) sandboxResolverArgs.override = operatorOverride;
38
+ const resolvedSandbox = resolveSandbox(sandboxResolverArgs);
39
+ const inboundSanitization = inboundSanitizationFor(tool, metadata.graphorinTrustLevel);
40
+ const stamped = {
41
+ ...tool,
42
+ sandboxPolicy: mapSandboxKindToPolicy(resolvedSandbox.kind),
43
+ ...inboundSanitization === void 0 ? {} : { inboundSanitization }
44
+ };
45
+ const source = Object.freeze({
46
+ kind: "skill",
47
+ skillName: metadata.name,
48
+ trustLevel: metadata.graphorinTrustLevel === "untrusted" || metadata.graphorinTrustLevel === "unknown" ? "untrusted" : "trusted"
49
+ });
50
+ return Object.freeze({
51
+ tool: Object.freeze(stamped),
52
+ source,
53
+ resolvedSandbox,
54
+ sandboxForced: resolvedSandbox.forced,
55
+ inboundSanitizationForced: inboundSanitization !== void 0 && tool.inboundSanitization === void 0
56
+ });
57
+ }
58
+ function mapTrustLevel(level) {
59
+ switch (level) {
60
+ case "untrusted":
61
+ case "unknown": return "untrusted";
62
+ case "trusted":
63
+ case "trusted-with-scripts": return "trusted";
64
+ default: return "user-defined";
65
+ }
66
+ }
67
+ function mapSandboxKindToPolicy(kind) {
68
+ if (kind === "none") return "none";
69
+ if (kind === "worker-threads") return "sandboxed";
70
+ if (kind === "isolated-vm") return "isolated";
71
+ if (kind === "docker") return "docker";
72
+ return "sandboxed";
73
+ }
74
+ function inboundSanitizationFor(tool, trustLevel) {
75
+ if (tool.inboundSanitization !== void 0) return tool.inboundSanitization;
76
+ if (trustLevel === "untrusted" || trustLevel === "unknown") return "detect-and-strip-and-wrap";
77
+ }
78
+ function sandboxOverrideFromTool(tool) {
79
+ switch (tool.sandboxPolicy) {
80
+ case void 0: return;
81
+ case "none": return { kind: "none" };
82
+ case "sandboxed": return { kind: "worker-threads" };
83
+ case "isolated": return { kind: "isolated-vm" };
84
+ case "docker": return { kind: "docker" };
85
+ default: return;
86
+ }
87
+ }
88
+
89
+ //#endregion
90
+ export { stampSkillTool, stampSkillToolFromMetadata };
91
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","names":["sandboxResolverArgs: Parameters<typeof resolveSandbox>[0]","stamped: Mutable<Tool<TInput, TOutput, TDeps>>","source: ToolSource"],"sources":["../../src/registry/bridge.ts"],"sourcesContent":["/**\n * Helper functions that bridge skill-bundled `Tool` records into the\n * `@graphorin/tools` registry.\n *\n * The skills loader does not import the runtime registry directly —\n * downstream callers (the agent runtime in Phase 12, the test suite,\n * and any user-supplied bootstrap script) materialise the actual\n * `Tool[]` from the skill module surface and feed each one through\n * {@link stampSkillTool} to get a {@link Tool} + a {@link ToolSource}\n * pair the registry can register with the correct trust-class\n * derivation, sandbox tier propagation, and inbound-sanitization\n * defaults.\n *\n * @packageDocumentation\n */\n\nimport type { InboundSanitizationPolicy, SandboxPolicy, Tool, ToolSource } from '@graphorin/core';\nimport { resolveSandbox, type SandboxTrustLevel } from '@graphorin/security/sandbox';\n\nimport type { Skill, SkillMetadata } from '../types/index.js';\n\n/** Result of {@link stampSkillTool}. */\nexport interface StampedSkillTool<TInput = unknown, TOutput = unknown, TDeps = unknown> {\n readonly tool: Tool<TInput, TOutput, TDeps>;\n readonly source: ToolSource;\n /** Resolved sandbox policy after the tier resolver ran. */\n readonly resolvedSandbox: ReturnType<typeof resolveSandbox>;\n /** `true` when the resolver overrode the operator's choice. */\n readonly sandboxForced: boolean;\n /** `true` when the inbound sanitization policy was upgraded to the untrusted default. */\n readonly inboundSanitizationForced: boolean;\n}\n\n/**\n * Stamp a skill-bundled tool with the metadata the\n * `@graphorin/tools` registry expects:\n *\n * 1. Derive a `ToolSource` of kind `'skill'` carrying the skill's\n * name and trust level.\n * 2. Run `resolveSandbox(...)` so the resulting `Tool.sandboxPolicy`\n * matches the mandatory tier for untrusted skills (DEC-148).\n * 3. Default the `inboundSanitization` policy to\n * `'detect-and-strip-and-wrap'` for untrusted skills when the tool\n * author left it unset; trusted skills inherit the operator's\n * choice.\n *\n * The function returns a freshly-frozen `Tool` with the resolved\n * `sandboxPolicy` and `inboundSanitization` baked in so downstream\n * registries cannot accidentally re-inherit a relaxed policy.\n *\n * @stable\n */\nexport function stampSkillTool<TInput = unknown, TOutput = unknown, TDeps = unknown>(\n tool: Tool<TInput, TOutput, TDeps>,\n skill: Pick<Skill, 'metadata'>,\n): StampedSkillTool<TInput, TOutput, TDeps> {\n return stampSkillToolFromMetadata(tool, skill.metadata);\n}\n\n/**\n * Lower-level variant accepting a raw {@link SkillMetadata} so\n * fixtures and tests do not have to materialise a full {@link Skill}.\n *\n * @stable\n */\nexport function stampSkillToolFromMetadata<TInput = unknown, TOutput = unknown, TDeps = unknown>(\n tool: Tool<TInput, TOutput, TDeps>,\n metadata: SkillMetadata,\n): StampedSkillTool<TInput, TOutput, TDeps> {\n const trustLevel = mapTrustLevel(metadata.graphorinTrustLevel);\n const sandboxResolverArgs: Parameters<typeof resolveSandbox>[0] = {\n trustLevel,\n };\n if (tool.name !== undefined)\n (sandboxResolverArgs as Mutable<typeof sandboxResolverArgs>).toolName = tool.name;\n if (metadata.name !== undefined)\n (sandboxResolverArgs as Mutable<typeof sandboxResolverArgs>).skillName = metadata.name;\n const operatorOverride = sandboxOverrideFromTool(tool);\n if (operatorOverride !== undefined)\n (sandboxResolverArgs as Mutable<typeof sandboxResolverArgs>).override = operatorOverride;\n const resolvedSandbox = resolveSandbox(sandboxResolverArgs);\n const inboundSanitization = inboundSanitizationFor(tool, metadata.graphorinTrustLevel);\n const stamped: Mutable<Tool<TInput, TOutput, TDeps>> = {\n ...tool,\n sandboxPolicy: mapSandboxKindToPolicy(resolvedSandbox.kind),\n ...(inboundSanitization === undefined ? {} : { inboundSanitization }),\n };\n const source: ToolSource = Object.freeze({\n kind: 'skill' as const,\n skillName: metadata.name,\n // 'unknown' inherits the strict sandbox policy of 'untrusted', so\n // the agent runtime registers the source as untrusted too — that\n // forces the inbound-sanitization default\n // ('detect-and-strip-and-wrap') and the precedence ladder used by\n // collision resolution to demote it relative to first-party\n // / trusted-skill registrations.\n trustLevel:\n metadata.graphorinTrustLevel === 'untrusted' || metadata.graphorinTrustLevel === 'unknown'\n ? 'untrusted'\n : 'trusted',\n });\n return Object.freeze({\n tool: Object.freeze(stamped) as Tool<TInput, TOutput, TDeps>,\n source,\n resolvedSandbox,\n sandboxForced: resolvedSandbox.forced,\n inboundSanitizationForced:\n inboundSanitization !== undefined && tool.inboundSanitization === undefined,\n });\n}\n\nfunction mapTrustLevel(level: SkillMetadata['graphorinTrustLevel']): SandboxTrustLevel {\n switch (level) {\n case 'untrusted':\n case 'unknown':\n // Phase 08 § Risks & mitigations: 'unknown' enforces sandbox\n // without requiring signature, so the sandbox tier resolver\n // applies the same strict policy as 'untrusted'.\n return 'untrusted';\n case 'trusted':\n case 'trusted-with-scripts':\n return 'trusted';\n default: {\n const exhaustive: never = level;\n void exhaustive;\n return 'user-defined';\n }\n }\n}\n\nfunction mapSandboxKindToPolicy(kind: ReturnType<typeof resolveSandbox>['kind']): SandboxPolicy {\n if (kind === 'none') return 'none';\n if (kind === 'worker-threads') return 'sandboxed';\n if (kind === 'isolated-vm') return 'isolated';\n if (kind === 'docker') return 'docker';\n return 'sandboxed';\n}\n\nfunction inboundSanitizationFor(\n tool: Pick<Tool, 'inboundSanitization'>,\n trustLevel: SkillMetadata['graphorinTrustLevel'],\n): InboundSanitizationPolicy | undefined {\n if (tool.inboundSanitization !== undefined) return tool.inboundSanitization;\n if (trustLevel === 'untrusted' || trustLevel === 'unknown') {\n return 'detect-and-strip-and-wrap';\n }\n return undefined;\n}\n\nfunction sandboxOverrideFromTool(\n tool: Pick<Tool, 'sandboxPolicy'>,\n): { readonly kind?: ReturnType<typeof resolveSandbox>['kind'] } | undefined {\n switch (tool.sandboxPolicy) {\n case undefined:\n return undefined;\n case 'none':\n return { kind: 'none' };\n case 'sandboxed':\n return { kind: 'worker-threads' };\n case 'isolated':\n return { kind: 'isolated-vm' };\n case 'docker':\n return { kind: 'docker' };\n default:\n return undefined;\n }\n}\n\ntype Mutable<T> = { -readonly [K in keyof T]: T[K] };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,eACd,MACA,OAC0C;AAC1C,QAAO,2BAA2B,MAAM,MAAM,SAAS;;;;;;;;AASzD,SAAgB,2BACd,MACA,UAC0C;CAE1C,MAAMA,sBAA4D,EAChE,YAFiB,cAAc,SAAS,oBAAoB,EAG7D;AACD,KAAI,KAAK,SAAS,OAChB,CAAC,oBAA4D,WAAW,KAAK;AAC/E,KAAI,SAAS,SAAS,OACpB,CAAC,oBAA4D,YAAY,SAAS;CACpF,MAAM,mBAAmB,wBAAwB,KAAK;AACtD,KAAI,qBAAqB,OACvB,CAAC,oBAA4D,WAAW;CAC1E,MAAM,kBAAkB,eAAe,oBAAoB;CAC3D,MAAM,sBAAsB,uBAAuB,MAAM,SAAS,oBAAoB;CACtF,MAAMC,UAAiD;EACrD,GAAG;EACH,eAAe,uBAAuB,gBAAgB,KAAK;EAC3D,GAAI,wBAAwB,SAAY,EAAE,GAAG,EAAE,qBAAqB;EACrE;CACD,MAAMC,SAAqB,OAAO,OAAO;EACvC,MAAM;EACN,WAAW,SAAS;EAOpB,YACE,SAAS,wBAAwB,eAAe,SAAS,wBAAwB,YAC7E,cACA;EACP,CAAC;AACF,QAAO,OAAO,OAAO;EACnB,MAAM,OAAO,OAAO,QAAQ;EAC5B;EACA;EACA,eAAe,gBAAgB;EAC/B,2BACE,wBAAwB,UAAa,KAAK,wBAAwB;EACrE,CAAC;;AAGJ,SAAS,cAAc,OAAgE;AACrF,SAAQ,OAAR;EACE,KAAK;EACL,KAAK,UAIH,QAAO;EACT,KAAK;EACL,KAAK,uBACH,QAAO;EACT,QAGE,QAAO;;;AAKb,SAAS,uBAAuB,MAAgE;AAC9F,KAAI,SAAS,OAAQ,QAAO;AAC5B,KAAI,SAAS,iBAAkB,QAAO;AACtC,KAAI,SAAS,cAAe,QAAO;AACnC,KAAI,SAAS,SAAU,QAAO;AAC9B,QAAO;;AAGT,SAAS,uBACP,MACA,YACuC;AACvC,KAAI,KAAK,wBAAwB,OAAW,QAAO,KAAK;AACxD,KAAI,eAAe,eAAe,eAAe,UAC/C,QAAO;;AAKX,SAAS,wBACP,MAC2E;AAC3E,SAAQ,KAAK,eAAb;EACE,KAAK,OACH;EACF,KAAK,OACH,QAAO,EAAE,MAAM,QAAQ;EACzB,KAAK,YACH,QAAO,EAAE,MAAM,kBAAkB;EACnC,KAAK,WACH,QAAO,EAAE,MAAM,eAAe;EAChC,KAAK,SACH,QAAO,EAAE,MAAM,UAAU;EAC3B,QACE"}
@@ -0,0 +1,140 @@
1
+ import { ActivatedSkill, InlineSkillTool, Skill, SkillMetadata, SkillToolDeclaration } from "../types/index.js";
2
+ import { StampedSkillTool, stampSkillTool, stampSkillToolFromMetadata } from "./bridge.js";
3
+ import { ResolvedTool } from "@graphorin/core";
4
+
5
+ //#region src/registry/index.d.ts
6
+
7
+ /**
8
+ * Stamping seam injected by the agent runtime (Phase 12). It turns a skill's
9
+ * pre-built `Tool` into a fully resolved `ResolvedTool` (trust class + sandbox
10
+ * tier + source). The skills package keeps no hard dependency on
11
+ * `@graphorin/tools`; when no stamper is configured, `activate()` surfaces no
12
+ * tools (the runtime resolves them itself).
13
+ */
14
+ type SkillToolStamper = (tool: InlineSkillTool, metadata: SkillMetadata) => ResolvedTool;
15
+ /** Options accepted by {@link createSkillRegistry}. */
16
+ interface SkillRegistryOptions {
17
+ /**
18
+ * Default activation behaviour. When `'metadata-only'` (default),
19
+ * `activate(...)` returns the parsed activation request without
20
+ * invoking `Skill.body()`; callers (the agent runtime) then invoke
21
+ * the body resolver themselves so the runtime can attach a span.
22
+ * When `'eager'`, the registry resolves the body before returning,
23
+ * suitable for tests.
24
+ */
25
+ readonly activationStrategy?: 'metadata-only' | 'eager';
26
+ /**
27
+ * Optional stamping function (RP-11). When supplied, `activate()` runs each
28
+ * skill's pre-built `Tool[]` through it and surfaces the results on
29
+ * {@link ActivatedSkill.tools}. Without it, `activate()` surfaces no tools —
30
+ * the agent runtime resolves and stamps them itself.
31
+ */
32
+ readonly stampTool?: SkillToolStamper;
33
+ }
34
+ /** Public registry surface. */
35
+ interface SkillRegistry {
36
+ register(skill: Skill): void;
37
+ /**
38
+ * Upsert a skill by name (RP-11). Unlike {@link SkillRegistry.register},
39
+ * `replace` overwrites an existing registration instead of throwing on a
40
+ * name collision — the upgrade path for hot-reloading a re-loaded skill.
41
+ */
42
+ replace(skill: Skill): void;
43
+ unregister(name: string): boolean;
44
+ getSkill(name: string): Skill | undefined;
45
+ has(name: string): boolean;
46
+ list(): ReadonlyArray<Skill>;
47
+ getMetadata(): ReadonlyArray<SkillMetadata>;
48
+ /**
49
+ * Skills surfaced into the system prompt for auto-activation.
50
+ * Skills with `disable-model-invocation: true` are excluded.
51
+ */
52
+ getAutoActivationMetadata(): ReadonlyArray<SkillMetadata>;
53
+ /**
54
+ * Render the auto-activation metadata as a string suitable for the
55
+ * system prompt. The format is bytes-stable and consumed verbatim
56
+ * by the ContextEngine layered template (Phase 10d). Skills with
57
+ * `disable-model-invocation: true` are excluded.
58
+ */
59
+ getMetadataBlock(): string;
60
+ /**
61
+ * Resolve a single trigger (model-emitted skill name OR the raw
62
+ * `/skill:<name>` slash-command body) into an {@link ActivationRequest}.
63
+ * Returns `null` when no skill matches and the trigger looked like a
64
+ * slash command — callers that want a strict mode should call
65
+ * {@link parseActivationTrigger} themselves.
66
+ */
67
+ resolveTrigger(trigger: string): ActivationRequest | null;
68
+ activate(triggers: ReadonlyArray<string>, signal?: AbortSignal): Promise<ReadonlyArray<ActivatedSkill>>;
69
+ /**
70
+ * Best-effort match: returns every skill whose name OR description
71
+ * contains all of the supplied trigger tokens (case-insensitive).
72
+ * The agent runtime uses this when the model emits a trigger phrase
73
+ * that does not directly map to a skill name.
74
+ */
75
+ search(triggers: ReadonlyArray<string>): ReadonlyArray<Skill>;
76
+ /**
77
+ * Flat, deduplicated list of every pre-built tool shipped by the
78
+ * registered skills. The first registration wins on a `tool.name`
79
+ * collision; later collisions surface a one-time WARN through the
80
+ * console so operators can resolve the conflict (Phase 12 will
81
+ * route these through the audit emitter).
82
+ */
83
+ tools(): ReadonlyArray<InlineSkillTool>;
84
+ toolDeclarations(): ReadonlyArray<RegisteredToolDeclaration>;
85
+ size(): number;
86
+ clear(): void;
87
+ }
88
+ /** Activation request produced by {@link SkillRegistry.resolveTrigger}. */
89
+ interface ActivationRequest {
90
+ readonly skill: Skill;
91
+ readonly activationKind: ActivatedSkill['activationKind'];
92
+ readonly args?: string;
93
+ }
94
+ /**
95
+ * Tool-declaration record exposed by {@link SkillRegistry.toolDeclarations}.
96
+ * Adds the owning skill's name and trust level so downstream
97
+ * registrations into `@graphorin/tools` can stamp the source.
98
+ *
99
+ * @stable
100
+ */
101
+ interface RegisteredToolDeclaration extends SkillToolDeclaration {
102
+ readonly skillName: string;
103
+ readonly trustLevel: SkillMetadata['graphorinTrustLevel'];
104
+ }
105
+ /**
106
+ * Build a fresh, empty registry. Multiple registries can co-exist
107
+ * within a single process; the framework defaults to a single shared
108
+ * instance per agent instance.
109
+ *
110
+ * @stable
111
+ */
112
+ declare function createSkillRegistry(options?: SkillRegistryOptions): SkillRegistry;
113
+ /**
114
+ * Parsed activation trigger. The registry uses this to discriminate
115
+ * slash-command activations (which override
116
+ * `disable-model-invocation: true`) from model-emitted auto
117
+ * activations (which honour it).
118
+ *
119
+ * @stable
120
+ */
121
+ interface ParsedActivationTrigger {
122
+ readonly name: string;
123
+ readonly activationKind: ActivatedSkill['activationKind'];
124
+ readonly args?: string;
125
+ }
126
+ /**
127
+ * Parse a single activation trigger. Slash-command bodies
128
+ * (`/skill:<name>`) are routed through the slash parser; bare names
129
+ * are treated as auto-activation requests emitted by the model.
130
+ *
131
+ * Throws {@link SlashCommandParseError} when the body looks like a
132
+ * slash command but does not match the grammar (so the caller can
133
+ * surface the error to the user).
134
+ *
135
+ * @stable
136
+ */
137
+ declare function parseActivationTrigger(raw: string): ParsedActivationTrigger;
138
+ //#endregion
139
+ export { ActivationRequest, ParsedActivationTrigger, RegisteredToolDeclaration, SkillRegistry, SkillRegistryOptions, SkillToolStamper, type StampedSkillTool, createSkillRegistry, parseActivationTrigger, stampSkillTool, stampSkillToolFromMetadata };
140
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/registry/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;AAsHW,KA1EC,gBAAA,GA0ED,CAAA,IAAA,EA1E2B,eA0E3B,EAAA,QAAA,EA1EsD,aA0EtD,EAAA,GA1EwE,YA0ExE;;AACW,UAxEL,oBAAA,CAwEK;EAAa;AAMnC;AAaA;AAYA;AA6NA;AAiBA;;;;;;;;;;uBArUuB;;;UAIN,aAAA;kBACC;;;;;;iBAMD;;0BAES;;UAEhB,cAAc;iBACP,cAAc;;;;;+BAKA,cAAc;;;;;;;;;;;;;;;mCAeV;qBAErB,gCACD,cACR,QAAQ,cAAc;;;;;;;mBAOR,wBAAwB,cAAc;;;;;;;;WAQ9C,cAAc;sBACH,cAAc;;;;;UAMnB,iBAAA;kBACC;2BACS;;;;;;;;;;UAWV,yBAAA,SAAkC;;uBAE5B;;;;;;;;;iBAUP,mBAAA,WAA6B,uBAA4B;;;;;;;;;UA6NxD,uBAAA;;2BAEU;;;;;;;;;;;;;;iBAeX,sBAAA,eAAqC"}
@@ -0,0 +1,193 @@
1
+ import { SkillNameCollisionError, SlashCommandParseError } from "../errors/index.js";
2
+ import { parseSlashCommand } from "../activation/index.js";
3
+ import { stampSkillTool, stampSkillToolFromMetadata } from "./bridge.js";
4
+
5
+ //#region src/registry/index.ts
6
+ /**
7
+ * Build a fresh, empty registry. Multiple registries can co-exist
8
+ * within a single process; the framework defaults to a single shared
9
+ * instance per agent instance.
10
+ *
11
+ * @stable
12
+ */
13
+ function createSkillRegistry(options = {}) {
14
+ const skillsByName = /* @__PURE__ */ new Map();
15
+ const strategy = options.activationStrategy ?? "metadata-only";
16
+ function register(skill) {
17
+ if (skillsByName.has(skill.metadata.name)) throw new SkillNameCollisionError(skill.metadata.name);
18
+ skillsByName.set(skill.metadata.name, skill);
19
+ }
20
+ function replace(skill) {
21
+ skillsByName.set(skill.metadata.name, skill);
22
+ }
23
+ function unregister(name) {
24
+ return skillsByName.delete(name);
25
+ }
26
+ function getSkill(name) {
27
+ return skillsByName.get(name);
28
+ }
29
+ function has(name) {
30
+ return skillsByName.has(name);
31
+ }
32
+ function list() {
33
+ return Object.freeze([...skillsByName.values()]);
34
+ }
35
+ function getMetadata() {
36
+ return Object.freeze([...skillsByName.values()].map((skill) => skill.metadata));
37
+ }
38
+ function getAutoActivationMetadata() {
39
+ return Object.freeze([...skillsByName.values()].map((skill) => skill.metadata).filter((metadata) => !metadata.disableModelInvocation));
40
+ }
41
+ function getMetadataBlock() {
42
+ const lines = [];
43
+ const auto = getAutoActivationMetadata();
44
+ if (auto.length === 0) return "";
45
+ lines.push("# Available skills");
46
+ lines.push("");
47
+ for (const meta of auto) {
48
+ lines.push(`## ${meta.name}`);
49
+ lines.push("");
50
+ const description = meta.description.replace(/\s+/gu, " ").trim();
51
+ if (description.length > 0) lines.push(description);
52
+ const cues = [];
53
+ if (meta.allowedTools !== void 0 && meta.allowedTools.length > 0) cues.push(`Allowed tools: ${[...meta.allowedTools].join(", ")}`);
54
+ if (meta.graphorinSensitivity !== void 0) cues.push(`Sensitivity: ${meta.graphorinSensitivity}`);
55
+ if (cues.length > 0) {
56
+ lines.push("");
57
+ for (const cue of cues) lines.push(`- ${cue}`);
58
+ }
59
+ lines.push("");
60
+ }
61
+ return lines.join("\n").replace(/\n+$/u, "\n");
62
+ }
63
+ function search(triggers) {
64
+ const tokens = [...triggers].flatMap((trigger) => trigger.toLowerCase().split(/\s+/u)).filter((tok) => tok.length > 0);
65
+ if (tokens.length === 0) return Object.freeze([]);
66
+ const matches = [];
67
+ const seen = /* @__PURE__ */ new Set();
68
+ for (const skill of skillsByName.values()) {
69
+ const haystack = `${skill.metadata.name}\n${skill.metadata.description}`.toLowerCase();
70
+ if (tokens.every((tok) => haystack.includes(tok)) && !seen.has(skill.metadata.name)) {
71
+ seen.add(skill.metadata.name);
72
+ matches.push(skill);
73
+ }
74
+ }
75
+ return Object.freeze(matches);
76
+ }
77
+ function tools() {
78
+ const seen = /* @__PURE__ */ new Set();
79
+ const out = [];
80
+ const collisions = /* @__PURE__ */ new Set();
81
+ for (const skill of skillsByName.values()) for (const tool of skill.tools()) {
82
+ if (seen.has(tool.name)) {
83
+ if (!collisions.has(tool.name)) {
84
+ collisions.add(tool.name);
85
+ console.warn(`[graphorin/skills] Duplicate tool name '${tool.name}' across skills; first registration wins.`);
86
+ }
87
+ continue;
88
+ }
89
+ seen.add(tool.name);
90
+ out.push(tool);
91
+ }
92
+ return Object.freeze(out);
93
+ }
94
+ function resolveTrigger(trigger) {
95
+ const parsed = parseActivationTrigger(trigger);
96
+ const skill = skillsByName.get(parsed.name);
97
+ if (skill === void 0) return null;
98
+ if (parsed.activationKind === "auto" && skill.metadata.disableModelInvocation) return null;
99
+ const request = {
100
+ skill,
101
+ activationKind: parsed.activationKind
102
+ };
103
+ if (parsed.args !== void 0 && parsed.args.length > 0) request.args = parsed.args;
104
+ return Object.freeze(request);
105
+ }
106
+ async function activate(triggers, signal) {
107
+ const out = [];
108
+ const seen = /* @__PURE__ */ new Set();
109
+ for (const trigger of triggers) {
110
+ const request = resolveTrigger(trigger);
111
+ if (request === null) continue;
112
+ if (seen.has(request.skill.metadata.name)) continue;
113
+ seen.add(request.skill.metadata.name);
114
+ const body = strategy === "eager" ? await request.skill.body(signal) : "";
115
+ const resources = strategy === "eager" ? await request.skill.resources(signal) : Object.freeze([]);
116
+ const stampedTools = options.stampTool !== void 0 ? Object.freeze(request.skill.tools().map((tool) => options.stampTool(tool, request.skill.metadata))) : Object.freeze([]);
117
+ out.push(Object.freeze({
118
+ skill: request.skill,
119
+ body,
120
+ resources,
121
+ tools: stampedTools,
122
+ activationKind: request.activationKind,
123
+ activatedAt: Date.now()
124
+ }));
125
+ }
126
+ return Object.freeze(out);
127
+ }
128
+ function toolDeclarations() {
129
+ const out = [];
130
+ for (const skill of skillsByName.values()) for (const decl of skill.toolDeclarations()) out.push(Object.freeze({
131
+ ...decl,
132
+ skillName: skill.metadata.name,
133
+ trustLevel: skill.metadata.graphorinTrustLevel
134
+ }));
135
+ return Object.freeze(out);
136
+ }
137
+ function size() {
138
+ return skillsByName.size;
139
+ }
140
+ function clear() {
141
+ skillsByName.clear();
142
+ }
143
+ return Object.freeze({
144
+ register,
145
+ replace,
146
+ unregister,
147
+ getSkill,
148
+ has,
149
+ list,
150
+ getMetadata,
151
+ getAutoActivationMetadata,
152
+ getMetadataBlock,
153
+ resolveTrigger,
154
+ activate,
155
+ search,
156
+ tools,
157
+ toolDeclarations,
158
+ size,
159
+ clear
160
+ });
161
+ }
162
+ /**
163
+ * Parse a single activation trigger. Slash-command bodies
164
+ * (`/skill:<name>`) are routed through the slash parser; bare names
165
+ * are treated as auto-activation requests emitted by the model.
166
+ *
167
+ * Throws {@link SlashCommandParseError} when the body looks like a
168
+ * slash command but does not match the grammar (so the caller can
169
+ * surface the error to the user).
170
+ *
171
+ * @stable
172
+ */
173
+ function parseActivationTrigger(raw) {
174
+ const trimmed = raw.trim();
175
+ if (trimmed.startsWith("/skill:") || /^\s*\/skill:/u.test(raw)) {
176
+ const parsed = parseSlashCommand(raw);
177
+ return Object.freeze({
178
+ name: parsed.name,
179
+ activationKind: "slash-command",
180
+ ...parsed.args.length > 0 ? { args: parsed.args } : {}
181
+ });
182
+ }
183
+ if (trimmed.length === 0) throw new SlashCommandParseError(raw, "activation trigger must be non-empty.");
184
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u.test(trimmed)) throw new SlashCommandParseError(raw, "auto-activation trigger must match /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.");
185
+ return Object.freeze({
186
+ name: trimmed,
187
+ activationKind: "auto"
188
+ });
189
+ }
190
+
191
+ //#endregion
192
+ export { createSkillRegistry, parseActivationTrigger, stampSkillTool, stampSkillToolFromMetadata };
193
+ //# sourceMappingURL=index.js.map