@khorsheed/dsh-capability-catalog 0.1.98 → 0.1.99

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 (46) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.en.md +31 -7
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +31 -7
  5. package/cordis.patch.yml +1 -1
  6. package/lib/client.js +14792 -904
  7. package/lib/index.js +748 -263
  8. package/lib/typert.host.js +614 -295
  9. package/lib/typert.remote-client.d.ts +27 -4
  10. package/lib/typert.remote-client.js +599 -288
  11. package/lib/types/client/CapabilityCatalogCard.js +20 -5
  12. package/lib/types/client/SkillDetailModal.d.ts +13 -5
  13. package/lib/types/client/SkillDetailModal.js +9 -3
  14. package/lib/types/client/locales.d.ts +0 -4
  15. package/lib/types/client/locales.js +4 -8
  16. package/lib/types/envHint.d.ts +7 -1
  17. package/lib/types/envHint.js +52 -18
  18. package/lib/types/evidence.d.ts +142 -0
  19. package/lib/types/evidence.js +179 -0
  20. package/lib/types/frontmatter.d.ts +78 -0
  21. package/lib/types/frontmatter.js +186 -0
  22. package/lib/types/import.js +10 -19
  23. package/lib/types/index.d.ts +8 -3
  24. package/lib/types/index.js +21 -50
  25. package/lib/types/mcpStore.d.ts +6 -8
  26. package/lib/types/mcpStore.js +18 -43
  27. package/lib/types/mcps.d.ts +1 -6
  28. package/lib/types/mcps.js +0 -9
  29. package/lib/types/namespace.d.ts +2 -3
  30. package/lib/types/namespace.js +2 -3
  31. package/lib/types/remote.d.ts +11 -2
  32. package/lib/types/remote.js +4 -1
  33. package/lib/types/scoped-delivery.d.ts +36 -24
  34. package/lib/types/scoped-delivery.js +136 -100
  35. package/lib/types/scoped-edits.js +2 -1
  36. package/lib/types/shellEnv.d.ts +8 -1
  37. package/lib/types/shellEnv.js +37 -16
  38. package/lib/types/skills.d.ts +41 -0
  39. package/lib/types/skills.js +51 -9
  40. package/lib/types/tool-face.d.ts +68 -0
  41. package/lib/types/tool-face.js +82 -0
  42. package/lib/types/types.d.ts +40 -19
  43. package/package.json +20 -18
  44. package/lib/invariant.js +0 -25
  45. package/lib/types/invariant.d.ts +0 -16
  46. package/lib/types/invariant.js +0 -23
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The PRODUCER side of capability-composition evidence: pure data, computed
3
+ * where the composition runs, for a consumer in another process to verify.
4
+ *
5
+ * {@link produceCapabilityEvidence} answers "what is the capability face of
6
+ * THIS composition, and how completely was it measured?" — the composition
7
+ * digest plus a per-section completeness descriptor, the measurement object
8
+ * (what claims to have been measured), and the caller's coverage declaration
9
+ * (what directories/services were read). It reuses this package's own
10
+ * snapshot and digest algorithms (`catalogSnapshot`, `hashOf` — via
11
+ * `remote.ts` and `capabilities.ts`), so the digest it reports is the digest
12
+ * `snapshotFor` would stamp over the same registries: parity is by
13
+ * construction, and a test pins it.
14
+ *
15
+ * Two deliberate absences, both by design:
16
+ *
17
+ * - **No standing-scope mount.** The function takes no roster and no cordis
18
+ * Context, so it CANNOT resolve a preset's standing scope — resolving one
19
+ * mounts the preset (see `preset-scope.ts`), and a target-side producer
20
+ * must not pay that side effect to measure. The normal target-side
21
+ * topology needs no scope at all: the producer's own registries already
22
+ * ARE the composition's face. A caller holding an already-resolved scope
23
+ * may pass it as `scope`; resolution and release stay the caller's
24
+ * business.
25
+ * - **No service registration.** Nothing is mounted, provided or injected as
26
+ * a side effect; the function reads the slices it is handed and returns
27
+ * plain JSON-safe data. It runs in a bare process (node builtins and this
28
+ * package's existing dependencies only).
29
+ *
30
+ * The digest algorithm is owned by THIS package. A consumer MUST NOT
31
+ * recompute it from the rows, and MUST treat a `partial` or `unavailable`
32
+ * section as "not verified" — never as complete, and never as "the section
33
+ * is empty". An empty-but-complete section (scanned, zero found) and an
34
+ * unmeasured section produce distinguishable descriptors, which is the
35
+ * point: an empty digest is not a complete one unless every section says so.
36
+ * @module @khorsheed/dsh-capability-catalog/evidence
37
+ */
38
+ import { isAbsolute } from 'node:path';
39
+ import { catalogSnapshot } from './remote.js';
40
+ import { collectToolFace } from './tool-face.js';
41
+ /** The registry that measures as "no skills", for a composition without a skills service. */
42
+ const EMPTY_REGISTRY = {
43
+ snapshot: async () => ({ skills: [], complete: true }),
44
+ get: async () => undefined,
45
+ };
46
+ /**
47
+ * Wrap the caller's tools slice so a `get()` that throws degrades to an
48
+ * unreadable origin tag (a partial section) instead of failing the whole
49
+ * measurement. Member-call semantics are preserved — the slice may be a
50
+ * traceable proxy whose methods need their receiver.
51
+ */
52
+ function guardToolReads(tools) {
53
+ if (tools.get === undefined)
54
+ return tools;
55
+ const get = tools.get;
56
+ return {
57
+ schemas: (scope) => tools.schemas(scope),
58
+ get: (name, scope) => {
59
+ try {
60
+ return get.call(tools, name, scope);
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ },
66
+ };
67
+ }
68
+ /** The skills section's descriptor, from the collection's own report. */
69
+ function skillsCompleteness(skills, report) {
70
+ if (skills === undefined)
71
+ return { state: 'unavailable', reason: 'the skills service is not installed' };
72
+ if (report === undefined || report.attemptedScopes === 0)
73
+ return { state: 'unavailable', reason: 'no skill scope was read' };
74
+ if (report.failedScopes === report.attemptedScopes) {
75
+ return { state: 'unavailable', reason: `the skill registry read failed for every scope (${report.attemptedScopes} attempted)` };
76
+ }
77
+ const problems = [];
78
+ if (report.failedScopes > 0)
79
+ problems.push(`the skill registry read failed for ${report.failedScopes} of ${report.attemptedScopes} scopes`);
80
+ if (report.incompleteReads > 0)
81
+ problems.push('the registry reported incomplete skill discovery');
82
+ if (report.unreadableBodies > 0)
83
+ problems.push(`${report.unreadableBodies} skill bodies could not be read`);
84
+ return problems.length === 0 ? { state: 'complete' } : { state: 'partial', reason: problems.join('; ') };
85
+ }
86
+ /** The tools section's descriptor, from how the tools-side read went. */
87
+ function toolsCompleteness(tools, readFailed, face) {
88
+ if (tools === undefined)
89
+ return { state: 'unavailable', reason: 'the tools service is not installed' };
90
+ if (readFailed || face === undefined)
91
+ return { state: 'unavailable', reason: 'the tool registry read failed' };
92
+ if (face.untaggedOrigins.length > 0) {
93
+ return { state: 'partial', reason: `origin tags unreadable for ${face.untaggedOrigins.length} visible tools — channel attribution fell back to heuristics` };
94
+ }
95
+ return { state: 'complete' };
96
+ }
97
+ /** The MCP section is derived from the tools section, so it inherits its failures. */
98
+ function mcpCompleteness(tools) {
99
+ if (tools.state === 'unavailable')
100
+ return { state: 'unavailable', reason: 'MCP servers are derived from the tools section, which is unavailable' };
101
+ if (tools.state === 'partial')
102
+ return { state: 'partial', reason: 'derived from a partially measured tools section' };
103
+ return { state: 'complete' };
104
+ }
105
+ /** The channel summary is derived from the skills and tools rows, so it is only as strong as they are. */
106
+ function channelsCompleteness(skills, tools) {
107
+ if (skills.state === 'unavailable' && tools.state === 'unavailable') {
108
+ return { state: 'unavailable', reason: 'neither the skills nor the tools section could be measured' };
109
+ }
110
+ if (skills.state !== 'complete' || tools.state !== 'complete') {
111
+ return { state: 'partial', reason: `summarizes sections that are not fully measured (skills: ${skills.state}, tools: ${tools.state})` };
112
+ }
113
+ return { state: 'complete' };
114
+ }
115
+ /**
116
+ * Produce the capability-composition evidence of the composition THIS
117
+ * process runs, as plain data another process can verify.
118
+ *
119
+ * The measurement reuses the package's own algorithms end to end: the tools
120
+ * side is read by `tool-face.ts` (the same reads the Remote verbs make), the
121
+ * snapshot is `catalogSnapshot` in fingerprint mode, and the digest is its
122
+ * stamped `sha` — nothing is re-implemented, so the answer is comparable
123
+ * with a `snapshotFor` digest taken over the same registries.
124
+ * @param options - every input, explicit; see {@link ProduceCapabilityEvidenceOptions}.
125
+ * @returns the evidence bundle — digest, completeness, measurement, coverage.
126
+ * @throws when `workdir` is given but not absolute, when `presetId` is given
127
+ * but blank, or when the coverage declaration is missing — the caller
128
+ * verifies identity and coverage; this function only measures.
129
+ */
130
+ export async function produceCapabilityEvidence(options) {
131
+ if (options.workdir !== undefined && !isAbsolute(options.workdir)) {
132
+ throw new Error(`produceCapabilityEvidence needs an absolute workdir (caller-verified); got ${JSON.stringify(options.workdir)}`);
133
+ }
134
+ if (options.presetId !== undefined && options.presetId.trim() === '') {
135
+ throw new Error(`produceCapabilityEvidence needs a non-empty presetId when one is given; got ${JSON.stringify(options.presetId)}`);
136
+ }
137
+ if (options.coverage === undefined || options.coverage === null) {
138
+ throw new Error('produceCapabilityEvidence needs the caller\'s coverage declaration (what directories/services were measured)');
139
+ }
140
+ const warn = options.warn ?? (() => { });
141
+ // The tools side: one read of the caller's slice, with a throwing `get`
142
+ // degraded to an untagged origin. A throwing `schemas()` is the caller's
143
+ // unavailable — nothing was measured.
144
+ let toolFace;
145
+ let toolsReadFailed = false;
146
+ if (options.tools !== undefined) {
147
+ try {
148
+ toolFace = collectToolFace(guardToolReads(options.tools), options.scope, warn);
149
+ }
150
+ catch {
151
+ toolsReadFailed = true;
152
+ }
153
+ }
154
+ // The skills side + the snapshot itself: the SAME catalogSnapshot call the
155
+ // fingerprint verb makes, with the health report the verbs never ask for.
156
+ const report = {};
157
+ const snapshot = await catalogSnapshot(options.workdir, options.skills ?? EMPTY_REGISTRY, toolFace?.schemas ?? [], toolFace?.mcpServerNames ?? [], options.appearedAfterApply ?? new Set(), [options.scope], toolFace?.origins ?? new Map(), { fingerprint: true, report, ...options.presetId !== undefined ? { preset: options.presetId } : {} });
158
+ const skills = skillsCompleteness(options.skills, report.skills);
159
+ const tools = toolsCompleteness(options.tools, toolsReadFailed, toolFace);
160
+ return {
161
+ measurement: {
162
+ kind: 'preset-composition',
163
+ ...options.presetId !== undefined ? { presetId: options.presetId } : {},
164
+ ...options.workdir !== undefined ? { workdir: options.workdir } : {},
165
+ ...options.revision !== undefined ? { revision: options.revision } : {},
166
+ },
167
+ sha: snapshot.sha,
168
+ completeness: {
169
+ skills,
170
+ tools,
171
+ mcpServers: mcpCompleteness(tools),
172
+ channels: channelsCompleteness(skills, tools),
173
+ },
174
+ coverage: {
175
+ ...options.coverage.directories !== undefined ? { directories: [...options.coverage.directories] } : {},
176
+ ...options.coverage.services !== undefined ? { services: [...options.coverage.services] } : {},
177
+ },
178
+ };
179
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * SKILL.md frontmatter: ONE real-YAML parse shared by every catalog reader.
3
+ *
4
+ * Before this module, each reader carried its own line-based scan: a regex
5
+ * split plus per-key patterns that matched at any indentation, so a nested
6
+ * same-named key could shadow a top-level one and a `presetScope` under an
7
+ * unrelated key still counted. Everything here is derived from a single
8
+ * js-yaml parse of the block (js-yaml is the repo's YAML convention and a
9
+ * shipped runtime dependency), with the scalar semantics mirrored from the
10
+ * host's skill loader (`skill-filesystem`): booleans accept true/false, 1/0,
11
+ * yes/no, on/off.
12
+ *
13
+ * Two deliberate degradations, both pinned by specs:
14
+ *
15
+ * - A malformed `metadata:` block must not take the skill down. A bad metadata
16
+ * value usually poisons the whole YAML parse, so on a parse failure the
17
+ * block is retried once with the top-level `metadata:` section stripped:
18
+ * the skill delivers minus metadata rather than being rejected.
19
+ * - A flag value outside the accepted variants reads as ABSENT (the caller's
20
+ * default applies) instead of rejecting the skill the way the host loader
21
+ * does — the catalog is a management/delivery surface, not the loader of
22
+ * record.
23
+ * @module @khorsheed/dsh-capability-catalog/frontmatter
24
+ */
25
+ /** Whether a string is a usable skill / preset id. */
26
+ export declare function isKebabId(value: string): boolean;
27
+ /** The frontmatter block split from the body it precedes. */
28
+ export interface FrontmatterSplit {
29
+ /** The text between the `---` fences. */
30
+ readonly front: string;
31
+ /** Everything after the closing fence. */
32
+ readonly body: string;
33
+ }
34
+ /** Split a SKILL.md into its frontmatter block and body. */
35
+ export declare function splitFrontmatter(content: string): FrontmatterSplit | undefined;
36
+ /** A SKILL.md frontmatter block, parsed once with real YAML semantics. */
37
+ export interface SkillFrontmatter {
38
+ /** Top-level `name`, when a non-empty string. */
39
+ readonly name?: string;
40
+ /** Top-level `description`, when a non-empty string (folded scalars included). */
41
+ readonly description?: string;
42
+ /** `when-to-use` / `whenToUse` routing guidance, kebab spelling first. */
43
+ readonly whenToUse?: string;
44
+ /** `disable-model-invocation`, inverted; absent/invalid means invocable. */
45
+ readonly modelInvocable: boolean;
46
+ /** `user-invocable`; absent/invalid means invocable. */
47
+ readonly userInvocable: boolean;
48
+ /**
49
+ * The declared preset ids: the canonical `metadata.presetScope`, falling
50
+ * back to a top-level `presetScope` (a form the line-based reader accepted
51
+ * and hand-edited files may still carry). A `presetScope` nested under any
52
+ * other key is NOT a declaration. `undefined` is absent; `[]` is the
53
+ * explicit every-preset form.
54
+ */
55
+ readonly presetScope?: readonly string[];
56
+ /** The verbatim top-level `metadata` map, when it is a plain object. */
57
+ readonly metadata?: Readonly<Record<string, unknown>>;
58
+ /** The body after the closing fence. */
59
+ readonly body: string;
60
+ }
61
+ /**
62
+ * Parse a SKILL.md's frontmatter block once, with js-yaml.
63
+ * @param content - the whole file text.
64
+ * @returns the parsed fields plus the body, or undefined when there is no
65
+ * frontmatter block or the block is not usable YAML even without its
66
+ * `metadata:` section.
67
+ */
68
+ export declare function parseSkillFrontmatter(content: string): SkillFrontmatter | undefined;
69
+ /**
70
+ * A frontmatter boolean with the host skill loader's accepted variants:
71
+ * booleans, 1/0 (number or string), and yes/no/on/off (case-insensitive —
72
+ * js-yaml 4 leaves those as strings, so the string branch carries them).
73
+ * Absent or anything else reads as undefined: the catalog degrades to the
74
+ * caller's default instead of rejecting the skill the way the host loader
75
+ * does (`frontmatter field "<key>" must be a boolean` → file ignored).
76
+ */
77
+ export declare function frontmatterBoolean(data: Record<string, unknown>, key: string): boolean | undefined;
78
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1,186 @@
1
+ /**
2
+ * SKILL.md frontmatter: ONE real-YAML parse shared by every catalog reader.
3
+ *
4
+ * Before this module, each reader carried its own line-based scan: a regex
5
+ * split plus per-key patterns that matched at any indentation, so a nested
6
+ * same-named key could shadow a top-level one and a `presetScope` under an
7
+ * unrelated key still counted. Everything here is derived from a single
8
+ * js-yaml parse of the block (js-yaml is the repo's YAML convention and a
9
+ * shipped runtime dependency), with the scalar semantics mirrored from the
10
+ * host's skill loader (`skill-filesystem`): booleans accept true/false, 1/0,
11
+ * yes/no, on/off.
12
+ *
13
+ * Two deliberate degradations, both pinned by specs:
14
+ *
15
+ * - A malformed `metadata:` block must not take the skill down. A bad metadata
16
+ * value usually poisons the whole YAML parse, so on a parse failure the
17
+ * block is retried once with the top-level `metadata:` section stripped:
18
+ * the skill delivers minus metadata rather than being rejected.
19
+ * - A flag value outside the accepted variants reads as ABSENT (the caller's
20
+ * default applies) instead of rejecting the skill the way the host loader
21
+ * does — the catalog is a management/delivery surface, not the loader of
22
+ * record.
23
+ * @module @khorsheed/dsh-capability-catalog/frontmatter
24
+ */
25
+ import yaml from 'js-yaml';
26
+ /** Kebab-case skill / preset id grammar (the registry's own). */
27
+ const KEBAB_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
28
+ /** Whether a string is a usable skill / preset id. */
29
+ export function isKebabId(value) {
30
+ return KEBAB_ID.test(value);
31
+ }
32
+ /** Frontmatter block, then the body it precedes (CRLF-tolerant). */
33
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
34
+ /** Split a SKILL.md into its frontmatter block and body. */
35
+ export function splitFrontmatter(content) {
36
+ const match = FRONTMATTER.exec(content);
37
+ if (match === null)
38
+ return undefined;
39
+ return { front: match[1] ?? '', body: match[2] ?? '' };
40
+ }
41
+ /**
42
+ * Parse a SKILL.md's frontmatter block once, with js-yaml.
43
+ * @param content - the whole file text.
44
+ * @returns the parsed fields plus the body, or undefined when there is no
45
+ * frontmatter block or the block is not usable YAML even without its
46
+ * `metadata:` section.
47
+ */
48
+ export function parseSkillFrontmatter(content) {
49
+ const split = splitFrontmatter(content);
50
+ if (split === undefined)
51
+ return undefined;
52
+ const parsed = parseFrontmatterData(split.front);
53
+ if (parsed === undefined)
54
+ return undefined;
55
+ const { data, metadataDropped } = parsed;
56
+ const rawMetadata = data['metadata'];
57
+ const metadata = !metadataDropped && typeof rawMetadata === 'object' && rawMetadata !== null && !Array.isArray(rawMetadata)
58
+ ? rawMetadata
59
+ : undefined;
60
+ const name = stringField(data, 'name');
61
+ const description = stringField(data, 'description');
62
+ const whenToUse = whenToUseOf(data);
63
+ const presetScope = presetScopeOf(data, metadata);
64
+ return {
65
+ ...name === undefined ? {} : { name },
66
+ ...description === undefined ? {} : { description },
67
+ ...whenToUse === undefined ? {} : { whenToUse },
68
+ modelInvocable: frontmatterBoolean(data, 'disable-model-invocation') !== true,
69
+ userInvocable: frontmatterBoolean(data, 'user-invocable') !== false,
70
+ ...presetScope === undefined ? {} : { presetScope },
71
+ ...metadata === undefined ? {} : { metadata },
72
+ body: split.body,
73
+ };
74
+ }
75
+ /**
76
+ * A frontmatter boolean with the host skill loader's accepted variants:
77
+ * booleans, 1/0 (number or string), and yes/no/on/off (case-insensitive —
78
+ * js-yaml 4 leaves those as strings, so the string branch carries them).
79
+ * Absent or anything else reads as undefined: the catalog degrades to the
80
+ * caller's default instead of rejecting the skill the way the host loader
81
+ * does (`frontmatter field "<key>" must be a boolean` → file ignored).
82
+ */
83
+ export function frontmatterBoolean(data, key) {
84
+ if (!Object.hasOwn(data, key))
85
+ return undefined;
86
+ const value = data[key];
87
+ if (typeof value === 'boolean')
88
+ return value;
89
+ if (value === 1 || value === '1')
90
+ return true;
91
+ if (value === 0 || value === '0')
92
+ return false;
93
+ if (typeof value === 'string') {
94
+ switch (value.toLowerCase()) {
95
+ case 'true':
96
+ case 'yes':
97
+ case 'on':
98
+ return true;
99
+ case 'false':
100
+ case 'no':
101
+ case 'off':
102
+ return false;
103
+ }
104
+ }
105
+ return undefined;
106
+ }
107
+ /** A frontmatter string field: non-empty strings only (the host's stringField). */
108
+ function stringField(data, key) {
109
+ const value = data[key];
110
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
111
+ }
112
+ /** The routing guidance, kebab spelling first (the catalog has always accepted both). */
113
+ function whenToUseOf(data) {
114
+ return stringField(data, 'when-to-use') ?? stringField(data, 'whenToUse');
115
+ }
116
+ /** The declared preset scope; the canonical metadata-nested key wins over the legacy top-level one. */
117
+ function presetScopeOf(data, metadata) {
118
+ const nested = metadata === undefined ? undefined : normalizePresetScope(metadata['presetScope']);
119
+ if (nested !== undefined)
120
+ return nested;
121
+ return normalizePresetScope(data['presetScope']);
122
+ }
123
+ /** Normalize a raw presetScope value: arrays filter to kebab ids, a scalar is a one-item list. */
124
+ function normalizePresetScope(value) {
125
+ if (Array.isArray(value))
126
+ return presetIdsOf(value.map(String));
127
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
128
+ return presetIdsOf([String(value)]);
129
+ }
130
+ return undefined;
131
+ }
132
+ /** Normalize YAML scalars onto the preset-id grammar (deduped, order preserved). */
133
+ function presetIdsOf(values) {
134
+ const ids = [];
135
+ for (const value of values) {
136
+ const id = value.trim().replace(/^['"]|['"]$/g, '');
137
+ if (id === '' || !KEBAB_ID.test(id))
138
+ continue;
139
+ if (!ids.includes(id))
140
+ ids.push(id);
141
+ }
142
+ return ids;
143
+ }
144
+ /** Parse the block as a YAML mapping; on a YAML failure retry once without the
145
+ * top-level `metadata:` section (a malformed metadata value degrades to
146
+ * no-metadata instead of rejecting the file). */
147
+ function parseFrontmatterData(front) {
148
+ try {
149
+ const data = loadYamlMapping(front);
150
+ return data === undefined ? undefined : { data, metadataDropped: false };
151
+ }
152
+ catch {
153
+ const stripped = stripTopLevelBlock(front, 'metadata');
154
+ if (stripped === undefined)
155
+ return undefined;
156
+ try {
157
+ const data = loadYamlMapping(stripped);
158
+ return data === undefined ? undefined : { data, metadataDropped: true };
159
+ }
160
+ catch {
161
+ return undefined;
162
+ }
163
+ }
164
+ }
165
+ /** js-yaml parse of the block; only a plain object is a usable frontmatter map. */
166
+ function loadYamlMapping(front) {
167
+ const parsed = yaml.load(front);
168
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
169
+ return undefined;
170
+ return parsed;
171
+ }
172
+ /** Remove a top-level `key:` line and its indented block; undefined when the key is absent. */
173
+ function stripTopLevelBlock(front, key) {
174
+ const lines = front.split('\n');
175
+ const start = lines.findIndex(line => new RegExp(`^${key}\\s*:`).test(line));
176
+ if (start < 0)
177
+ return undefined;
178
+ let end = start + 1;
179
+ while (end < lines.length) {
180
+ const line = lines[end] ?? '';
181
+ if (line.trim() !== '' && !/^\s/.test(line))
182
+ break;
183
+ end += 1;
184
+ }
185
+ return [...lines.slice(0, start), ...lines.slice(end)].join('\n');
186
+ }
@@ -21,9 +21,9 @@ import { cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node
21
21
  import { homedir, tmpdir } from 'node:os';
22
22
  import { basename, dirname, join } from 'node:path';
23
23
  import { promisify } from 'node:util';
24
+ import { isKebabId, parseSkillFrontmatter, splitFrontmatter } from './frontmatter.js';
24
25
  import { extractZip } from './zip.js';
25
26
  const execFileAsync = promisify(execFile);
26
- const FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
27
27
  /** First bytes of a ZIP archive (local file header / empty / spanned). */
28
28
  const ZIP_MAGIC_PK = 0x50; // 'P'
29
29
  const ZIP_MAGIC_K = 0x4b; // 'K'
@@ -35,13 +35,11 @@ export function managedRoot(root, dshHome) {
35
35
  }
36
36
  /** Extract the kebab-case `name` and `description` from a SKILL.md body. */
37
37
  export function resolveSkillNameFromContent(content) {
38
- const m = FRONTMATTER.exec(content);
39
- if (m === null)
38
+ const parsed = parseSkillFrontmatter(content);
39
+ if (parsed === undefined)
40
40
  return undefined;
41
- const front = m[1] ?? '';
42
- const name = /^name:\s*(.+)$/m.exec(front)?.[1]?.trim();
43
- const description = /^description:\s*(.+)$/m.exec(front)?.[1]?.trim();
44
- if (name === undefined || description === undefined || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
41
+ const { name, description } = parsed;
42
+ if (name === undefined || description === undefined || !isKebabId(name))
45
43
  return undefined;
46
44
  return { name, description };
47
45
  }
@@ -51,12 +49,7 @@ function decodeBase64(payload) {
51
49
  }
52
50
  /** Read the frontmatter `disable-model-invocation` flag (default model-invocable true). */
53
51
  export function inferModelInvocable(content) {
54
- const m = FRONTMATTER.exec(content);
55
- if (m === null)
56
- return true;
57
- const front = m[1] ?? '';
58
- const line = /^disable-model-invocation:\s*(true|false)$/m.exec(front)?.[1];
59
- return line !== 'true';
52
+ return parseSkillFrontmatter(content)?.modelInvocable ?? true;
60
53
  }
61
54
  /** Whether a filesystem path exists (dir or file). */
62
55
  async function pathExists(p) {
@@ -96,22 +89,20 @@ function sanitizeRelPath(p) {
96
89
  }
97
90
  /** Apply the frontmatter `disable-model-invocation` line to match the request. */
98
91
  function finalizeSkillText(content, modelInvocable) {
99
- const m = FRONTMATTER.exec(content);
100
- if (m === null)
92
+ const split = splitFrontmatter(content);
93
+ if (split === undefined)
101
94
  return content;
102
- const front = String(m[1] ?? '');
103
- const body = String(m[2] ?? '');
104
95
  const effective = inferModelInvocable(content);
105
96
  if (effective === modelInvocable)
106
97
  return content;
107
- const without = front
98
+ const without = split.front
108
99
  .split('\n')
109
100
  .filter(line => !line.trim().startsWith('disable-model-invocation:'))
110
101
  .join('\n');
111
102
  const withFlag = modelInvocable
112
103
  ? without
113
104
  : `${without}${without.endsWith('\n') ? '' : '\n'}disable-model-invocation: true`;
114
- return `---\n${withFlag}\n---\n${body}`;
105
+ return `---\n${withFlag}\n---\n${split.body}`;
115
106
  }
116
107
  /**
117
108
  * Add a skill from a base64 SKILL.md text payload into the managed root.
@@ -16,17 +16,21 @@ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
16
16
  import type { CapabilityCatalogSnapshot, CatalogAddSkillRequest, CatalogAddSkillResult, CatalogCredentialSetRequest, CatalogDeleteSkillResult, CatalogMcpServerConfig, CatalogMcpSnapshot, CatalogMcpTool, CatalogModeFace, CatalogSkillDetail, CatalogSkillFileRead, CatalogDirSkillInfo, CatalogPresetScopeStatus, CatalogPresetOption, CatalogPresetScopeSetRequest, CatalogPresetScopeAdoptRequest, CatalogPresetScopeEditResult } from './types.ts';
17
17
  export { TOOL_ORIGIN, setToolOrigin, toolOrigin } from './tool-origin.ts';
18
18
  export type { ToolOrigin } from './tool-origin.ts';
19
- export type { CapabilityCatalogSnapshot, CatalogJsonValue, CatalogSkillRow, CatalogToolRow, CatalogMcpServerRow, CatalogChannelSummary, CatalogSkillDetail, CatalogSkillFileRead, CatalogCredentialState, CatalogAddSkillRequest, CatalogAddSkillResult, CatalogDeleteSkillResult, CatalogCredentialSetRequest, CatalogPresetScopeStatus, CatalogScopedSkillRow, CatalogScopedPresetRow, CatalogPresetOption, CatalogModeFace, CatalogPresetScopeSetRequest, CatalogPresetScopeAdoptRequest, CatalogPresetScopeEditResult, } from './types.ts';
19
+ export type { CapabilityCatalogSnapshot, CatalogSectionCompleteness, CatalogSnapshotCompleteness, CatalogJsonValue, CatalogSkillRow, CatalogToolRow, CatalogMcpServerRow, CatalogChannelSummary, CatalogSkillDetail, CatalogSkillFileRead, CatalogCredentialState, CatalogAddSkillRequest, CatalogAddSkillResult, CatalogDeleteSkillResult, CatalogCredentialSetRequest, CatalogPresetScopeStatus, CatalogScopedSkillRow, CatalogScopedPresetRow, CatalogPresetOption, CatalogModeFace, CatalogPresetScopeSetRequest, CatalogPresetScopeAdoptRequest, CatalogPresetScopeEditResult, } from './types.ts';
20
20
  export { resolveSkillNameFromContent } from './import.ts';
21
21
  export { resolvePresetScope } from './preset-scope.ts';
22
22
  export type { PresetRosterSlice, PresetRosterRow, ResolvedPresetScope } from './preset-scope.ts';
23
23
  export { modeOptions, readModeFaces } from './modes.ts';
24
- export { ScopedSkillDelivery, scopedSkillsRoot, scanManagedSkills, managedSkillFrom, parsePresetScopeFrontmatter, defaultSkillRoots, detectConflicts, SCOPED_PROVIDER_NAME, MANAGED_SKILL_RANK, } from './scoped-delivery.ts';
24
+ export { ScopedSkillDelivery, scopedSkillsRoot, scanManagedSkills, managedSkillFrom, defaultSkillRoots, detectConflicts, SCOPED_PROVIDER_NAME, MANAGED_SKILL_RANK, } from './scoped-delivery.ts';
25
25
  export { adoptManagedSkill, releaseManagedSkill, setManagedPresetScope, withPresetScope, findSkillSource, managedLocation, isDirectory, } from './scoped-edits.ts';
26
26
  export type { ScopedEditResult, SkillSource, ManagedSkillLocation } from './scoped-edits.ts';
27
27
  export type { ManagedSkill, ScopedDeliveryDeps, ScopedDeliveryRegistry, ScopedDeliveryStatus, ScopedPresetStatus, ScopedSkillStatus, } from './scoped-delivery.ts';
28
28
  export { canonicalCapabilities, canonicalJson, capsTag, hashOf, hashSkillBody, CAPS_TAG_PREFIX, } from './capabilities.ts';
29
29
  export type { CanonicalCapabilities, CanonicalMcpServer, CanonicalSkill, CanonicalTool, } from './capabilities.ts';
30
+ export { produceCapabilityEvidence } from './evidence.ts';
31
+ export type { CapabilityEvidenceCompleteness, CapabilityEvidenceCoverage, CapabilityEvidenceMeasurement, ProduceCapabilityEvidenceOptions, ProducedCapabilityEvidence, } from './evidence.ts';
32
+ export type { RegistrySlice, SkillsCollectionReport } from './skills.ts';
33
+ export type { CatalogToolsSlice, ToolSchemaLike } from './tool-face.ts';
30
34
  declare module '@deepseek-ai/cordis' {
31
35
  interface Context {
32
36
  capabilityCatalog: CapabilityCatalogService;
@@ -184,7 +188,6 @@ export declare class CapabilityCatalogService extends TypertRemoteService {
184
188
  addSkill(request: CatalogAddSkillRequest): Promise<CatalogAddSkillResult>;
185
189
  deleteSkill(name: string, workdir?: string, presetId?: string): Promise<CatalogDeleteSkillResult>;
186
190
  pickDirectory(): Promise<string | null>;
187
- mcpList(): Promise<readonly CatalogMcpServerConfig[]>;
188
191
  mcpAdd(config: CatalogMcpServerConfig): Promise<boolean>;
189
192
  mcpRemove(serverName: string): Promise<boolean>;
190
193
  mcpSetEnabled(serverName: string, enabled: boolean): Promise<void>;
@@ -192,6 +195,8 @@ export declare class CapabilityCatalogService extends TypertRemoteService {
192
195
  mcpSetToolEnabled(serverName: string, tool: string, enabled: boolean): Promise<void>;
193
196
  mcpDiscover(serverName: string): Promise<readonly CatalogMcpTool[]>;
194
197
  mcpSnapshot(): Promise<CatalogMcpSnapshot>;
198
+ /** The live tools registry, when the composition provides one. */
199
+ private toolsRegistry;
195
200
  /** The visible tool schemas in the standing scope (the set the model sees),
196
201
  * or the global layer when no preset standing key resolves. Skills already
197
202
  * enumerate through the same scope; tools must use it too so the