@khorsheed/dsh-capability-catalog 0.1.97 → 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.
- package/CHANGELOG.md +17 -0
- package/README.en.md +31 -7
- package/README.i18n.yaml +2 -2
- package/README.md +31 -7
- package/cordis.patch.yml +1 -1
- package/lib/client.js +14790 -902
- package/lib/index.js +760 -265
- package/lib/typert.host.js +614 -295
- package/lib/typert.remote-client.d.ts +27 -4
- package/lib/typert.remote-client.js +599 -288
- package/lib/types/client/CapabilityCatalogCard.js +20 -5
- package/lib/types/client/SkillDetailModal.d.ts +13 -5
- package/lib/types/client/SkillDetailModal.js +9 -3
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/locales.js +4 -8
- package/lib/types/envHint.d.ts +7 -1
- package/lib/types/envHint.js +52 -18
- package/lib/types/evidence.d.ts +142 -0
- package/lib/types/evidence.js +179 -0
- package/lib/types/frontmatter.d.ts +78 -0
- package/lib/types/frontmatter.js +186 -0
- package/lib/types/import.js +10 -19
- package/lib/types/index.d.ts +16 -4
- package/lib/types/index.js +32 -53
- package/lib/types/mcpStore.d.ts +6 -8
- package/lib/types/mcpStore.js +18 -43
- package/lib/types/mcps.d.ts +1 -6
- package/lib/types/mcps.js +0 -9
- package/lib/types/namespace.d.ts +2 -3
- package/lib/types/namespace.js +2 -3
- package/lib/types/remote.d.ts +11 -2
- package/lib/types/remote.js +4 -1
- package/lib/types/scoped-delivery.d.ts +36 -24
- package/lib/types/scoped-delivery.js +136 -100
- package/lib/types/scoped-edits.js +2 -1
- package/lib/types/shellEnv.d.ts +8 -1
- package/lib/types/shellEnv.js +37 -16
- package/lib/types/skills.d.ts +41 -0
- package/lib/types/skills.js +51 -9
- package/lib/types/tool-face.d.ts +68 -0
- package/lib/types/tool-face.js +82 -0
- package/lib/types/types.d.ts +40 -19
- package/package.json +20 -18
- package/lib/invariant.js +0 -25
- package/lib/types/invariant.d.ts +0 -16
- 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
|
+
}
|
package/lib/types/import.js
CHANGED
|
@@ -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
|
|
39
|
-
if (
|
|
38
|
+
const parsed = parseSkillFrontmatter(content);
|
|
39
|
+
if (parsed === undefined)
|
|
40
40
|
return undefined;
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
100
|
-
if (
|
|
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.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
|
@@ -97,7 +101,14 @@ export declare class CapabilityCatalogService extends TypertRemoteService {
|
|
|
97
101
|
/** Record tools that appeared after the apply-time baseline (reads the live
|
|
98
102
|
* registry each time, so a change event never sees a stale/undefined ref). */
|
|
99
103
|
private markNewTools;
|
|
100
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The managed skills root under the harness home. Mirrors the official
|
|
106
|
+
* precedence (`resolveDshHome` in `@deepseek-ai/dsh-home-paths`):
|
|
107
|
+
* the settings service's home, then a non-blank `$DSH_HOME`, then the
|
|
108
|
+
* `~/.dsh` installation default. The fallback was once hardcoded to
|
|
109
|
+
* `.dsh-official`, which silently pointed a desktop instance (no settings
|
|
110
|
+
* home, no env) at ANOTHER instance's managed skills (2026-09-30).
|
|
111
|
+
*/
|
|
101
112
|
private dshHome;
|
|
102
113
|
/**
|
|
103
114
|
* The standing skill view scope of ONE preset. In the web bundle the
|
|
@@ -177,7 +188,6 @@ export declare class CapabilityCatalogService extends TypertRemoteService {
|
|
|
177
188
|
addSkill(request: CatalogAddSkillRequest): Promise<CatalogAddSkillResult>;
|
|
178
189
|
deleteSkill(name: string, workdir?: string, presetId?: string): Promise<CatalogDeleteSkillResult>;
|
|
179
190
|
pickDirectory(): Promise<string | null>;
|
|
180
|
-
mcpList(): Promise<readonly CatalogMcpServerConfig[]>;
|
|
181
191
|
mcpAdd(config: CatalogMcpServerConfig): Promise<boolean>;
|
|
182
192
|
mcpRemove(serverName: string): Promise<boolean>;
|
|
183
193
|
mcpSetEnabled(serverName: string, enabled: boolean): Promise<void>;
|
|
@@ -185,6 +195,8 @@ export declare class CapabilityCatalogService extends TypertRemoteService {
|
|
|
185
195
|
mcpSetToolEnabled(serverName: string, tool: string, enabled: boolean): Promise<void>;
|
|
186
196
|
mcpDiscover(serverName: string): Promise<readonly CatalogMcpTool[]>;
|
|
187
197
|
mcpSnapshot(): Promise<CatalogMcpSnapshot>;
|
|
198
|
+
/** The live tools registry, when the composition provides one. */
|
|
199
|
+
private toolsRegistry;
|
|
188
200
|
/** The visible tool schemas in the standing scope (the set the model sees),
|
|
189
201
|
* or the global layer when no preset standing key resolves. Skills already
|
|
190
202
|
* enumerate through the same scope; tools must use it too so the
|