@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.
- package/CHANGELOG.md +13 -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 +14792 -904
- package/lib/index.js +748 -263
- 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 +8 -3
- package/lib/types/index.js +21 -50
- 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
|
@@ -27,6 +27,9 @@ export function CapabilityCatalogCard({ useCatalog, detail, readSkillFile, listD
|
|
|
27
27
|
const [kind, setKind] = useState('skills');
|
|
28
28
|
const [selectedName, setSelectedName] = useState(null);
|
|
29
29
|
const [claim, setClaim] = useState({ status: 'idle', data: undefined });
|
|
30
|
+
/** The mode the open detail was read at (see openDetail); the modal's bundle
|
|
31
|
+
* file reads must resolve at the same scope. */
|
|
32
|
+
const [detailMode, setDetailMode] = useState(undefined);
|
|
30
33
|
const [showAdd, setShowAdd] = useState(false);
|
|
31
34
|
const [showAddMcp, setShowAddMcp] = useState(false);
|
|
32
35
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
@@ -301,14 +304,20 @@ export function CapabilityCatalogCard({ useCatalog, detail, readSkillFile, listD
|
|
|
301
304
|
});
|
|
302
305
|
};
|
|
303
306
|
const openDetail = async (name) => {
|
|
307
|
+
// The mode this detail is read at: the body AND any bundle file it points
|
|
308
|
+
// at resolve at the same scope, or a file that exists only in this mode
|
|
309
|
+
// reads as missing.
|
|
310
|
+
const readMode = readModeFor(name);
|
|
304
311
|
setSelectedName(name);
|
|
312
|
+
setDetailMode(readMode);
|
|
305
313
|
setClaim({ status: 'loading', data: undefined });
|
|
306
314
|
void loadDetailModes(name);
|
|
307
|
-
const data = await detail(name,
|
|
315
|
+
const data = await detail(name, readMode);
|
|
308
316
|
setClaim({ status: 'done', data });
|
|
309
317
|
};
|
|
310
318
|
const closeDetail = () => {
|
|
311
319
|
setSelectedName(null);
|
|
320
|
+
setDetailMode(undefined);
|
|
312
321
|
setClaim({ status: 'idle', data: undefined });
|
|
313
322
|
setDetailModes(null);
|
|
314
323
|
};
|
|
@@ -329,7 +338,7 @@ export function CapabilityCatalogCard({ useCatalog, detail, readSkillFile, listD
|
|
|
329
338
|
.replace('{skills}', String(skills.length))
|
|
330
339
|
.replace('{tools}', String(tools.length)), !modeBusy && comparison !== null && comparison.unavailable.length > 0 ? (_jsx("span", { className: css.modeWarn, title: comparison.unavailable.map(entry => `${entry.label}: ${entry.reason}`).join('\n'), children: t('modeUnavailableNote').replace('{n}', String(comparison.unavailable.length)) })) : null, !modeBusy ? _jsx("span", { className: css.modeHint, children: t('modeCompareCost') }) : null] })) : null, !comparing && modeBusy ? _jsx("div", { className: css.modeNote, children: t('modeBusy') }) : null, modeFellBack ? _jsx("div", { className: css.modeNote, children: t('modeFallback') }) : null, loading ? _jsx("div", { className: css.empty, children: t('loading') }) : null, !loading && kind === 'skills' && skills.length === 0 ? _jsx("div", { className: css.empty, children: t('empty') }) : null, !loading && kind === 'tools' && tools.length === 0 ? _jsx("div", { className: css.empty, children: t('toolNoMatch') }) : null, !loading && kind === 'skills' && skills.length > 0 ? (visibleSkills.length === 0
|
|
331
340
|
? _jsx("div", { className: css.empty, children: t('noFilterMatch') })
|
|
332
|
-
: (_jsx("div", { className: css.grid, children: visibleSkills.map((skill) => (_jsx(SkillPreviewCard, { skill: skill, modes: cardModeChips(comparison?.skillModes.get(skill.name)), modeTotal: comparison?.modes ?? 0, onMode: handleModeChange, onOpen: () => void openDetail(skill.name), onDelete: () => setDeleteTarget(skill.name), t: t }, skill.name))) }))) : null, !loading && kind === 'skills' && orphans.length > 0 ? (_jsxs("details", { className: css.orphanGroup, children: [_jsx("summary", { className: css.orphanSummary, children: t('orphanManaged').replace('{n}', String(orphans.length)) }), _jsx("p", { className: css.confHint, children: t('orphanManagedHint') }), _jsx("div", { className: css.grid, children: orphans.map(orphan => (_jsx(SkillPreviewCard, { skill: orphanSkillRow(orphan), tag: scopeTagOf(orphan.name), onMode: handleModeChange, onOpen: () => void openDetail(orphan.name), onDelete: () => setDeleteTarget(orphan.name), t: t }, orphan.name))) })] })) : null, kind === 'tools' ? (_jsx(ToolCards, { loading: loading, visibleTools: visibleTools, mcpGroups: mcpGroups, segment: toolSegment, comparison: comparison, modeChipsFor: modeChipsFor, onMode: handleModeChange, onOpenTool: (tool) => setToolDetail(tool), onOpenServer: (name) => setMcpDetailName(name), onSetEnabled: setMcpEnabled, onRemove: removeMcp, t: t })) : null, mcpDetail !== null ? (_jsx(McpServerManageModal, { group: mcpDetail, discovering: discoveringMcp.has(mcpDetail.serverName), onClose: () => setMcpDetailName(null), onSetCredential: mcpSetCredential, onSetToolEnabled: setMcpToolEnabled, onDiscover: discoverMcp, onOpenTool: (tool) => setToolDetail(tool), t: t })) : null, toolDetail !== null ? (_jsx(ToolDetailModal, { tool: toolDetail, onClose: () => setToolDetail(null), t: t })) : null, selectedName !== null ? (_jsx(SkillDetailModal, { name: selectedName, claim: claim, onClose: closeDetail, setCredential: setCredential, readSkillFile: readSkillFile, t: t, scope: scopeEditorFor(selectedName, skills, scopeStatus, localizedOptions, {
|
|
341
|
+
: (_jsx("div", { className: css.grid, children: visibleSkills.map((skill) => (_jsx(SkillPreviewCard, { skill: skill, modes: cardModeChips(comparison?.skillModes.get(skill.name)), modeTotal: comparison?.modes ?? 0, onMode: handleModeChange, onOpen: () => void openDetail(skill.name), onDelete: () => setDeleteTarget(skill.name), t: t }, skill.name))) }))) : null, !loading && kind === 'skills' && orphans.length > 0 ? (_jsxs("details", { className: css.orphanGroup, children: [_jsx("summary", { className: css.orphanSummary, children: t('orphanManaged').replace('{n}', String(orphans.length)) }), _jsx("p", { className: css.confHint, children: t('orphanManagedHint') }), _jsx("div", { className: css.grid, children: orphans.map(orphan => (_jsx(SkillPreviewCard, { skill: orphanSkillRow(orphan), tag: scopeTagOf(orphan.name), onMode: handleModeChange, onOpen: () => void openDetail(orphan.name), onDelete: () => setDeleteTarget(orphan.name), t: t }, orphan.name))) })] })) : null, kind === 'tools' ? (_jsx(ToolCards, { loading: loading, visibleTools: visibleTools, mcpGroups: mcpGroups, segment: toolSegment, comparison: comparison, modeChipsFor: modeChipsFor, onMode: handleModeChange, onOpenTool: (tool) => setToolDetail(tool), onOpenServer: (name) => setMcpDetailName(name), onSetEnabled: setMcpEnabled, onRemove: removeMcp, t: t })) : null, mcpDetail !== null ? (_jsx(McpServerManageModal, { group: mcpDetail, discovering: discoveringMcp.has(mcpDetail.serverName), onClose: () => setMcpDetailName(null), onSetCredential: mcpSetCredential, onSetToolEnabled: setMcpToolEnabled, onDiscover: discoverMcp, onOpenTool: (tool) => setToolDetail(tool), t: t })) : null, toolDetail !== null ? (_jsx(ToolDetailModal, { tool: toolDetail, onClose: () => setToolDetail(null), t: t })) : null, selectedName !== null ? (_jsx(SkillDetailModal, { name: selectedName, claim: claim, onClose: closeDetail, setCredential: setCredential, readSkillFile: readSkillFile, presetId: detailMode, t: t, scope: scopeEditorFor(selectedName, skills, scopeStatus, localizedOptions, {
|
|
333
342
|
save: async (presets) => {
|
|
334
343
|
const result = await presetScopeSet(selectedName, presets);
|
|
335
344
|
await Promise.all([refreshScope(), reloadView()]);
|
|
@@ -472,8 +481,14 @@ export function scopeEditorFor(name, skills, status, options, actions) {
|
|
|
472
481
|
declared: managedRow?.presets ?? [],
|
|
473
482
|
...managedRow?.conflict === undefined ? {} : { conflict: managedRow.conflict },
|
|
474
483
|
options,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
484
|
+
// One action for the modal, the managed-root mechanics hidden behind it:
|
|
485
|
+
// an emptied selection on a managed skill releases it back to the user
|
|
486
|
+
// root ("every preset" needs no managed delivery); an empty pick on an
|
|
487
|
+
// unmanaged skill is already the truth and needs no write at all.
|
|
488
|
+
apply: async (presets) => {
|
|
489
|
+
if (managedRow !== undefined)
|
|
490
|
+
return presets.length === 0 ? actions.release() : actions.save(presets);
|
|
491
|
+
return presets.length === 0 ? { ok: true } : actions.adopt(presets);
|
|
492
|
+
},
|
|
478
493
|
};
|
|
479
494
|
}
|
|
@@ -25,17 +25,25 @@ export interface ScopeEditor {
|
|
|
25
25
|
provider?: string;
|
|
26
26
|
/** Every preset the roster supplies. */
|
|
27
27
|
options: readonly CatalogPresetOption[];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Apply the picked presets. The routing stays invisible to the user: adopt
|
|
30
|
+
* for a skill outside the managed root, re-scope for a managed one, and
|
|
31
|
+
* release back to the user skill root when a managed skill's selection is
|
|
32
|
+
* emptied — an empty selection means "every preset", which needs no managed
|
|
33
|
+
* delivery. An empty pick on an unmanaged skill is already the truth and
|
|
34
|
+
* reports success without a write.
|
|
35
|
+
*/
|
|
36
|
+
apply: (presets: readonly string[]) => Promise<CatalogPresetScopeEditResult>;
|
|
31
37
|
}
|
|
32
38
|
/** Centered modal with a skill's full detail, source browser, metadata and credential config. */
|
|
33
|
-
export declare function SkillDetailModal({ name, claim, onClose, setCredential, readSkillFile, t, scope, modeChips, modesLoading, onModeSelect }: {
|
|
39
|
+
export declare function SkillDetailModal({ name, claim, onClose, setCredential, readSkillFile, presetId, t, scope, modeChips, modesLoading, onModeSelect }: {
|
|
34
40
|
name: string;
|
|
35
41
|
claim: DetailClaim;
|
|
36
42
|
onClose: () => void;
|
|
37
43
|
setCredential: (key: string, value: string) => Promise<boolean>;
|
|
38
|
-
readSkillFile: (name: string, path: string) => Promise<CatalogSkillFileRead | undefined>;
|
|
44
|
+
readSkillFile: (name: string, path: string, presetId?: string) => Promise<CatalogSkillFileRead | undefined>;
|
|
45
|
+
/** The mode this detail was read at; bundle files resolve at the same scope. */
|
|
46
|
+
presetId?: string | undefined;
|
|
39
47
|
t: (key: CapabilityCatalogKey) => string;
|
|
40
48
|
scope?: ScopeEditor | undefined;
|
|
41
49
|
/** The modes that load this skill, when its scope section cannot say (a
|
|
@@ -13,7 +13,7 @@ function providedScopeNote(t, provider) {
|
|
|
13
13
|
: t('scopeProvided').replace('{provider}', provider);
|
|
14
14
|
}
|
|
15
15
|
/** Centered modal with a skill's full detail, source browser, metadata and credential config. */
|
|
16
|
-
export function SkillDetailModal({ name, claim, onClose, setCredential, readSkillFile, t, scope, modeChips, modesLoading, onModeSelect }) {
|
|
16
|
+
export function SkillDetailModal({ name, claim, onClose, setCredential, readSkillFile, presetId, t, scope, modeChips, modesLoading, onModeSelect }) {
|
|
17
17
|
const [credValues, setCredValues] = useState({});
|
|
18
18
|
const [credState, setCredState] = useState({});
|
|
19
19
|
const [copied, setCopied] = useState(false);
|
|
@@ -47,6 +47,10 @@ export function SkillDetailModal({ name, claim, onClose, setCredential, readSkil
|
|
|
47
47
|
catch { /* clipboard may be blocked */ }
|
|
48
48
|
};
|
|
49
49
|
const pickedScope = scopePick ?? scope?.declared ?? [];
|
|
50
|
+
/** The pick matches the declared scope set-for-set (order-insensitive). */
|
|
51
|
+
const sameAsDeclared = scope !== undefined
|
|
52
|
+
&& pickedScope.length === scope.declared.length
|
|
53
|
+
&& pickedScope.every(id => scope.declared.includes(id));
|
|
50
54
|
/** Run one scope mutation, then report its outcome in place. */
|
|
51
55
|
const runScope = async (action) => {
|
|
52
56
|
setScopeBusy(true);
|
|
@@ -75,7 +79,7 @@ export function SkillDetailModal({ name, claim, onClose, setCredential, readSkil
|
|
|
75
79
|
}
|
|
76
80
|
setSrcLoading(true);
|
|
77
81
|
setSrcContent(undefined);
|
|
78
|
-
void readSkillFile(name, path).then(res => {
|
|
82
|
+
void readSkillFile(name, path, presetId).then(res => {
|
|
79
83
|
setSrcContent(res?.content);
|
|
80
84
|
setSrcLoading(false);
|
|
81
85
|
});
|
|
@@ -85,7 +89,9 @@ export function SkillDetailModal({ name, claim, onClose, setCredential, readSkil
|
|
|
85
89
|
{ label: t('provider'), value: data.provider },
|
|
86
90
|
{ label: t('modelInvocable'), value: data.modelInvocable ? t('yes') : t('no') },
|
|
87
91
|
...(data.whenToUse !== undefined ? [{ label: t('whenToUse'), value: data.whenToUse }] : []),
|
|
88
|
-
] }), _jsx("p", { className: css.detailDesc, children: data.description }), scope !== undefined ? (_jsxs("section", { className: css.scopeBox, children: [_jsxs("div", { className: css.scopeHead, children: [_jsx("span", { className: css.scopeTitle, children: t('scopeSection') }), _jsx("button", { type: "button", className: css.scopeInfo, title: `${t('scopeHint')}\n\n${t('scopeProbe')}`, "aria-label": t('scopeInfoLabel'), children: "i" })] }), scope.options.length === 0 ? (_jsx("div", { className: css.scopeHint, children: t('scopeUnavailable') })) : !scope.writable ? (_jsxs(_Fragment, { children: [_jsx("div", { className: css.scopeHint, children: providedScopeNote(t, scope.provider) }), modesLoading === true ? (_jsx("div", { className: css.scopeHint, children: t('modeBusy') })) : modeChips === undefined ? null : modeChips.length === 0 ? (_jsx("div", { className: css.scopeHint, children: t('modeNone') })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.scopeModes, children: [_jsx("span", { className: css.pvModesLabel, children: t('modeIn') }), modeChips.map(chip => (_jsx("button", { type: "button", className: css.modeChip, title: chip.isDefault ? t('modeChipDefaultHint') : t('modeChipHint'), onClick: () => onModeSelect?.(chip.id), children: chip.label }, chip.id)))] }), _jsx("div", { className: css.scopeHint, children: t('modeChipsCost') })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: css.scopeGrid, children: scope.options.map((option) => (_jsxs("label", { className: css.scopeItem, title: option.description, children: [_jsx("input", { type: "checkbox", checked: pickedScope.includes(option.id), disabled: scopeBusy, onChange: () => toggleScope(option.id) }), _jsx("span", { children: option.name ?? option.id }), option.broken !== undefined ? _jsx("span", { className: css.scopeBadge, children: t('scopeBroken') }) : null] }, option.id))) }), _jsxs("div", { className: css.scopeActions, children: [
|
|
92
|
+
] }), _jsx("p", { className: css.detailDesc, children: data.description }), scope !== undefined ? (_jsxs("section", { className: css.scopeBox, children: [_jsxs("div", { className: css.scopeHead, children: [_jsx("span", { className: css.scopeTitle, children: t('scopeSection') }), _jsx("button", { type: "button", className: css.scopeInfo, title: `${t('scopeHint')}\n\n${t('scopeProbe')}`, "aria-label": t('scopeInfoLabel'), children: "i" })] }), scope.options.length === 0 ? (_jsx("div", { className: css.scopeHint, children: t('scopeUnavailable') })) : !scope.writable ? (_jsxs(_Fragment, { children: [_jsx("div", { className: css.scopeHint, children: providedScopeNote(t, scope.provider) }), modesLoading === true ? (_jsx("div", { className: css.scopeHint, children: t('modeBusy') })) : modeChips === undefined ? null : modeChips.length === 0 ? (_jsx("div", { className: css.scopeHint, children: t('modeNone') })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.scopeModes, children: [_jsx("span", { className: css.pvModesLabel, children: t('modeIn') }), modeChips.map(chip => (_jsx("button", { type: "button", className: css.modeChip, title: chip.isDefault ? t('modeChipDefaultHint') : t('modeChipHint'), onClick: () => onModeSelect?.(chip.id), children: chip.label }, chip.id)))] }), _jsx("div", { className: css.scopeHint, children: t('modeChipsCost') })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: css.scopeGrid, children: scope.options.map((option) => (_jsxs("label", { className: css.scopeItem, title: option.description, children: [_jsx("input", { type: "checkbox", checked: pickedScope.includes(option.id), disabled: scopeBusy, onChange: () => toggleScope(option.id) }), _jsx("span", { children: option.name ?? option.id }), option.broken !== undefined ? _jsx("span", { className: css.scopeBadge, children: t('scopeBroken') }) : null] }, option.id))) }), _jsxs("div", { className: css.scopeActions, children: [_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy || (scope.managed
|
|
93
|
+
? sameAsDeclared && scope.declared.length > 0
|
|
94
|
+
: pickedScope.length === 0), onClick: () => void runScope(() => scope.apply(pickedScope)), children: scopeBusy ? t('scopeSaving') : t('scopeSave') }), scopeMsg !== null ? _jsx("span", { className: css.scopeHint, children: scopeMsg }) : null] }), scope.conflict !== undefined ? (_jsxs("div", { className: css.scopeConflict, children: [t('scopeConflict'), ": ", scope.conflict] })) : null] }))] })) : null, data.metadataText !== undefined && hasUnrenderedMetadata(data.metadataText) ? (_jsxs("details", { className: css.source, children: [_jsx("summary", { className: css.sourceTitle, children: t('frontmatterMetadata') }), _jsx("pre", { className: css.codeBlk, children: formatMetadata(data.metadataText) })] })) : null, data.credentials !== undefined && data.credentials.length > 0 ? (_jsxs("details", { className: css.conf, open: true, children: [_jsx("summary", { className: css.confTitle, children: t('credentials') }), _jsx("div", { className: css.confHint, children: t('credentialsHint') }), data.credentials.map((decl) => {
|
|
89
95
|
const key = decl.key;
|
|
90
96
|
const label = decl.label ?? key;
|
|
91
97
|
const state = credState[key] ?? 'idle';
|
|
@@ -165,8 +165,6 @@ export declare const zh: {
|
|
|
165
165
|
scopeSave: string;
|
|
166
166
|
scopeSaving: string;
|
|
167
167
|
scopeSaved: string;
|
|
168
|
-
scopeAdopt: string;
|
|
169
|
-
scopeRelease: string;
|
|
170
168
|
scopeConflict: string;
|
|
171
169
|
scopeUnavailable: string;
|
|
172
170
|
scopeBroken: string;
|
|
@@ -355,8 +353,6 @@ export declare const en: {
|
|
|
355
353
|
scopeSave: string;
|
|
356
354
|
scopeSaving: string;
|
|
357
355
|
scopeSaved: string;
|
|
358
|
-
scopeAdopt: string;
|
|
359
|
-
scopeRelease: string;
|
|
360
356
|
scopeConflict: string;
|
|
361
357
|
scopeUnavailable: string;
|
|
362
358
|
scopeBroken: string;
|
|
@@ -57,7 +57,7 @@ export const zh = {
|
|
|
57
57
|
credentials: '凭据配置',
|
|
58
58
|
credentialsHint: '该 skill 的 metadata 声明了以下凭据,配置后模型可读取。',
|
|
59
59
|
envRefs: '引用的环境变量',
|
|
60
|
-
envRefsHint: '该 skill
|
|
60
|
+
envRefsHint: '该 skill 的说明引用了以下变量;它们通常由运行环境或调用方提供,不在此页配置。若某项是需要用户提供的密钥,在该 skill 的 SKILL.md frontmatter 中以 metadata.credentials 声明(key 填变量名)后即可在此页配置。',
|
|
61
61
|
configured: '已配置',
|
|
62
62
|
notConfigured: '未配置',
|
|
63
63
|
configuredReplace: '已配置——输入新值可替换',
|
|
@@ -153,14 +153,12 @@ export const zh = {
|
|
|
153
153
|
scopeAllPresets: '全部',
|
|
154
154
|
scopeSection: '生效的 preset',
|
|
155
155
|
scopeEvery: '所有 preset',
|
|
156
|
-
scopeHint: '勾选这个 skill 生效的 preset
|
|
156
|
+
scopeHint: '勾选这个 skill 生效的 preset;一个都不勾表示所有模式可见。被限定的 skill 由本插件代管投递;把勾选全部取消并保存,即还原为所有模式可见的普通用户技能。',
|
|
157
157
|
scopeProvided: '该 skill 由插件「{provider}」提供:它在哪些模式生效取决于该插件挂在哪些预设的组合里,不能在这里单独限定。',
|
|
158
158
|
scopeBuiltin: '该 skill 由部署内置提供:生效模式跟随部署组合,不能在这里单独限定。',
|
|
159
159
|
scopeSave: '保存',
|
|
160
160
|
scopeSaving: '保存中…',
|
|
161
161
|
scopeSaved: '已保存',
|
|
162
|
-
scopeAdopt: '移入受管目录并可限定 preset',
|
|
163
|
-
scopeRelease: '释放回用户技能目录',
|
|
164
162
|
scopeConflict: '已拒绝投递:默认根里还有一份同名副本(迁移没完成的残留)',
|
|
165
163
|
scopeUnavailable: '此部署没有可用的 preset 名单,或受管投递未启用。',
|
|
166
164
|
scopeBroken: '加载失败',
|
|
@@ -253,7 +251,7 @@ export const en = {
|
|
|
253
251
|
credentials: 'Credential config',
|
|
254
252
|
credentialsHint: 'The skill metadata declares these credentials; configure them so the model can read them.',
|
|
255
253
|
envRefs: 'Referenced environment variables',
|
|
256
|
-
envRefsHint: 'The skill description references these variables; they are typically provided by the runtime or caller and are not configured here.',
|
|
254
|
+
envRefsHint: 'The skill description references these variables; they are typically provided by the runtime or caller and are not configured here. A user-supplied secret becomes configurable once declared under metadata.credentials in the SKILL.md frontmatter (key = the variable name).',
|
|
257
255
|
configured: 'Configured',
|
|
258
256
|
notConfigured: 'Not configured',
|
|
259
257
|
configuredReplace: 'Configured — enter a new value to replace',
|
|
@@ -349,14 +347,12 @@ export const en = {
|
|
|
349
347
|
scopeAllPresets: 'all',
|
|
350
348
|
scopeSection: 'Effective presets',
|
|
351
349
|
scopeEvery: 'All presets',
|
|
352
|
-
scopeHint: 'Tick the presets this skill is delivered to; none ticked means every preset sees it.
|
|
350
|
+
scopeHint: 'Tick the presets this skill is delivered to; none ticked means every preset sees it. A scoped skill\'s delivery is managed by this plugin — untick everything and save to restore it as a plain user skill visible everywhere.',
|
|
353
351
|
scopeProvided: 'This skill comes from the plugin "{provider}": which modes load it follows the plugin\'s row in each preset\'s composition, so its scope cannot be set here.',
|
|
354
352
|
scopeBuiltin: 'This skill ships with the deployment: its modes follow the deployment composition and cannot be set here.',
|
|
355
353
|
scopeSave: 'Save',
|
|
356
354
|
scopeSaving: 'Saving…',
|
|
357
355
|
scopeSaved: 'Saved',
|
|
358
|
-
scopeAdopt: 'Move into the managed root (preset-scoped)',
|
|
359
|
-
scopeRelease: 'Release back to the user skill root',
|
|
360
356
|
scopeConflict: 'Delivery refused: a same-named copy still exists in a default root',
|
|
361
357
|
scopeUnavailable: 'No preset roster is available in this deployment, or managed delivery is off.',
|
|
362
358
|
scopeBroken: 'Failed to load',
|
package/lib/types/envHint.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill env hint via `tools/post-execute` — a runtime companion context that
|
|
3
|
-
* tells the model about any skill's configured credential env mappings
|
|
3
|
+
* tells the model about any skill's configured credential env mappings, and a
|
|
4
|
+
* scoped-delivered skill's bundle files.
|
|
4
5
|
*
|
|
5
6
|
* The catalog injects credentials as trusted per-execution `DSH_<KEY>` env
|
|
6
7
|
* vars (see shellEnv.ts), but a skill's own text references the original
|
|
@@ -9,6 +10,11 @@
|
|
|
9
10
|
* a generic, per-skill note listing each configured mapping, so the model can
|
|
10
11
|
* use the value without us modifying the user's SKILL.md.
|
|
11
12
|
*
|
|
13
|
+
* A second block covers scoped-delivered skills (provider `capability-catalog`):
|
|
14
|
+
* their SKILL.md may reference bundle files (search.md etc.) in a mode that
|
|
15
|
+
* mounts no fs tools, so the note lists those files and names the scoped
|
|
16
|
+
* `read_skill_file` reader — only when the caller's own scope carries it.
|
|
17
|
+
*
|
|
12
18
|
* Generic: it derives the mappings from the loaded skill's own env-decl keys
|
|
13
19
|
* (decodeEnvDecls) + the credential store, so any future key / skill is
|
|
14
20
|
* covered. It never touches the skill file or the provider's returned content.
|
package/lib/types/envHint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill env hint via `tools/post-execute` — a runtime companion context that
|
|
3
|
-
* tells the model about any skill's configured credential env mappings
|
|
3
|
+
* tells the model about any skill's configured credential env mappings, and a
|
|
4
|
+
* scoped-delivered skill's bundle files.
|
|
4
5
|
*
|
|
5
6
|
* The catalog injects credentials as trusted per-execution `DSH_<KEY>` env
|
|
6
7
|
* vars (see shellEnv.ts), but a skill's own text references the original
|
|
@@ -9,12 +10,18 @@
|
|
|
9
10
|
* a generic, per-skill note listing each configured mapping, so the model can
|
|
10
11
|
* use the value without us modifying the user's SKILL.md.
|
|
11
12
|
*
|
|
13
|
+
* A second block covers scoped-delivered skills (provider `capability-catalog`):
|
|
14
|
+
* their SKILL.md may reference bundle files (search.md etc.) in a mode that
|
|
15
|
+
* mounts no fs tools, so the note lists those files and names the scoped
|
|
16
|
+
* `read_skill_file` reader — only when the caller's own scope carries it.
|
|
17
|
+
*
|
|
12
18
|
* Generic: it derives the mappings from the loaded skill's own env-decl keys
|
|
13
19
|
* (decodeEnvDecls) + the credential store, so any future key / skill is
|
|
14
20
|
* covered. It never touches the skill file or the provider's returned content.
|
|
15
21
|
* @module @khorsheed/dsh-capability-catalog/envHint
|
|
16
22
|
*/
|
|
17
|
-
import { decodeCredentialDecls, decodeEnvDecls, mergeCredentialDecls, resolveServices } from './skills.js';
|
|
23
|
+
import { collectSkillFiles, decodeCredentialDecls, decodeEnvDecls, mergeCredentialDecls, resolveServices } from './skills.js';
|
|
24
|
+
import { READ_SKILL_FILE_TOOL, SCOPED_PROVIDER_NAME } from './scoped-delivery.js';
|
|
18
25
|
const DSH_PREFIX = 'DSH_';
|
|
19
26
|
/** Subscribe to `tools/post-execute`; appends an env-mapping hint for the
|
|
20
27
|
* loaded skill. Returns a disposer. Degrades silently when tools/registry/
|
|
@@ -31,9 +38,12 @@ export function installSkillEnvHint(ctx, getScope) {
|
|
|
31
38
|
const skillName = exec.arguments?.name;
|
|
32
39
|
if (typeof skillName !== 'string' || skillName.length === 0)
|
|
33
40
|
return next();
|
|
34
|
-
|
|
41
|
+
// Read at the CALLER's scope, not the catalog's settings scope: a skill
|
|
42
|
+
// scoped to a non-default preset is invisible to the latter, and the hint
|
|
43
|
+
// would never fire exactly where that skill actually loads.
|
|
44
|
+
const scope = exec.agent ?? await getScope().catch(() => undefined);
|
|
35
45
|
const { registry, credentials } = resolveServices(ctx);
|
|
36
|
-
if (registry === undefined
|
|
46
|
+
if (registry === undefined)
|
|
37
47
|
return next();
|
|
38
48
|
const lookup = scope === undefined ? {} : { scope };
|
|
39
49
|
let def;
|
|
@@ -45,27 +55,51 @@ export function installSkillEnvHint(ctx, getScope) {
|
|
|
45
55
|
}
|
|
46
56
|
if (def === undefined)
|
|
47
57
|
return next();
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const blocks = [];
|
|
59
|
+
if (credentials !== undefined) {
|
|
60
|
+
const decls = mergeCredentialDecls(decodeCredentialDecls(def.metadata), decodeEnvDecls(def.content ?? ''));
|
|
61
|
+
const configured = [];
|
|
62
|
+
for (const decl of decls) {
|
|
63
|
+
const info = await credentials.describe(decl.key).catch(() => undefined);
|
|
64
|
+
if (info?.configured === true)
|
|
65
|
+
configured.push(decl.key);
|
|
66
|
+
}
|
|
67
|
+
if (configured.length > 0) {
|
|
68
|
+
blocks.push([
|
|
69
|
+
`Runtime credential mapping for ${skillName}:`,
|
|
70
|
+
...configured.map(k => `- ${k} is configured; in the model shell it is available as ${DSH_PREFIX}${k}. Use it as ${k}="${DSH_PREFIX}${k}" <command>.`),
|
|
71
|
+
`Do not search dotenv files or ask the user for this credential, and do not print either variable.`,
|
|
72
|
+
].join('\n'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A scoped-delivered skill's SKILL.md may point at bundle files (search.md
|
|
76
|
+
// etc.) while the mode mounts no fs tools to read them. Name the reader
|
|
77
|
+
// only when the caller's own scope carries it — naming an absent tool
|
|
78
|
+
// teaches the model a call that can only fail.
|
|
79
|
+
const bundleDir = def.resourceBase?.kind === 'directory' ? def.resourceBase.path : undefined;
|
|
80
|
+
if (def.provider === SCOPED_PROVIDER_NAME && bundleDir !== undefined && exec.agent !== undefined) {
|
|
81
|
+
const tools = ctx.get?.('tools');
|
|
82
|
+
const hasReader = tools?.schemas?.(exec.agent)?.some(schema => schema.name === READ_SKILL_FILE_TOOL) === true;
|
|
83
|
+
if (hasReader) {
|
|
84
|
+
const files = [];
|
|
85
|
+
await collectSkillFiles(bundleDir, bundleDir, files, 0);
|
|
86
|
+
const rest = files.filter(file => file !== 'SKILL.md').sort();
|
|
87
|
+
if (rest.length > 0) {
|
|
88
|
+
blocks.push([
|
|
89
|
+
`Bundle files for ${skillName}: ${rest.join(', ')}`,
|
|
90
|
+
`Read them with ${READ_SKILL_FILE_TOOL} (name: "${skillName}", path: "<file>") — this mode may have no fs tools.`,
|
|
91
|
+
].join('\n'));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
54
94
|
}
|
|
55
|
-
if (
|
|
95
|
+
if (blocks.length === 0)
|
|
56
96
|
return next();
|
|
57
|
-
const lines = [
|
|
58
|
-
`Runtime credential mapping for ${skillName}:`,
|
|
59
|
-
...configured.map(k => `- ${k} is configured; in the model shell it is available as ${DSH_PREFIX}${k}. Use it as ${k}="${DSH_PREFIX}${k}" <command>.`),
|
|
60
|
-
`Do not search dotenv files or ask the user for this credential, and do not print either variable.`,
|
|
61
|
-
];
|
|
62
|
-
const hint = lines.join('\n');
|
|
63
97
|
return {
|
|
64
98
|
kind: 'accept',
|
|
65
99
|
additionalContexts: [{
|
|
66
100
|
id: `capability-catalog-env-hint-${skillName}`,
|
|
67
101
|
role: 'user',
|
|
68
|
-
content: [{ type: 'text', text:
|
|
102
|
+
content: [{ type: 'text', text: blocks.join('\n\n') }],
|
|
69
103
|
// Producer-owned kind: the retired {kind: 'plugin'} wrapper fails the
|
|
70
104
|
// rc.1 native source admission at the durable write (turn-killing).
|
|
71
105
|
source: { kind: 'capability-catalog', plugin: 'capability-catalog', form: 'env-hint' },
|
|
@@ -0,0 +1,142 @@
|
|
|
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 type { RegistrySlice } from './skills.ts';
|
|
39
|
+
import { type CatalogToolsSlice } from './tool-face.ts';
|
|
40
|
+
import type { CatalogSectionCompleteness } from './types.ts';
|
|
41
|
+
/**
|
|
42
|
+
* What the evidence claims was measured: a preset composition at a given
|
|
43
|
+
* presetId and workdir, plus the caller-declared revision of that
|
|
44
|
+
* composition when one is available (e.g. its configuration digest). Every
|
|
45
|
+
* field is caller-verified input, echoed — the producer measures, it does
|
|
46
|
+
* not authenticate.
|
|
47
|
+
*/
|
|
48
|
+
export interface CapabilityEvidenceMeasurement {
|
|
49
|
+
readonly kind: 'preset-composition';
|
|
50
|
+
/** The preset the measured composition runs, as verified by the caller. */
|
|
51
|
+
readonly presetId?: string;
|
|
52
|
+
/** The workdir whose project-scoped roots were measured (absolute). */
|
|
53
|
+
readonly workdir?: string;
|
|
54
|
+
/** The measured composition's revision, when the caller has one. */
|
|
55
|
+
readonly revision?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The caller's declaration of what was measured: which directories and which
|
|
59
|
+
* services the measurement read. Echoed into the evidence verbatim so a
|
|
60
|
+
* consumer can check the measurement against what it expected to be
|
|
61
|
+
* measured — a digest says nothing about its own coverage.
|
|
62
|
+
*/
|
|
63
|
+
export interface CapabilityEvidenceCoverage {
|
|
64
|
+
/** Directories the measurement read (skill roots, project roots). */
|
|
65
|
+
readonly directories?: readonly string[];
|
|
66
|
+
/** Services the measurement read (e.g. `skills`, `tools`). */
|
|
67
|
+
readonly services?: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
/** What {@link produceCapabilityEvidence} needs. Every input is explicit. */
|
|
70
|
+
export interface ProduceCapabilityEvidenceOptions {
|
|
71
|
+
/**
|
|
72
|
+
* The preset the measured composition runs — caller-verified. Stamped onto
|
|
73
|
+
* the snapshot (and echoed into the measurement) as given; it never enters
|
|
74
|
+
* the digest, the way `snapshotFor`'s `preset` stamp never does.
|
|
75
|
+
*/
|
|
76
|
+
readonly presetId?: string;
|
|
77
|
+
/**
|
|
78
|
+
* The workdir whose project-scoped skill roots were measured — absolute
|
|
79
|
+
* and caller-verified. A relative path is refused rather than resolved:
|
|
80
|
+
* what it would resolve to depends on a cwd nobody verified.
|
|
81
|
+
*/
|
|
82
|
+
readonly workdir?: string;
|
|
83
|
+
/** The measured composition's revision, when the caller has one. */
|
|
84
|
+
readonly revision?: string;
|
|
85
|
+
/**
|
|
86
|
+
* An already-resolved reading scope (opaque to this package), passed
|
|
87
|
+
* through to the registry reads. Never resolved here — resolution mounts,
|
|
88
|
+
* and mounting is the caller's decision. The caller owns any lease.
|
|
89
|
+
*/
|
|
90
|
+
readonly scope?: unknown;
|
|
91
|
+
/** The skills registry to measure; absent = the skills section is `unavailable`. */
|
|
92
|
+
readonly skills?: RegistrySlice;
|
|
93
|
+
/** The tools registry to measure; absent = the tools section is `unavailable`. */
|
|
94
|
+
readonly tools?: CatalogToolsSlice;
|
|
95
|
+
/** Tools that appeared after the registry baseline (channel-heuristic input). */
|
|
96
|
+
readonly appearedAfterApply?: ReadonlySet<string>;
|
|
97
|
+
/** The caller's coverage declaration — required, because a digest without one cannot be checked against expectations. */
|
|
98
|
+
readonly coverage: CapabilityEvidenceCoverage;
|
|
99
|
+
/** Optional warning sink for the same diagnostics the Remote verbs log; dropped when absent. */
|
|
100
|
+
readonly warn?: (message: string) => void;
|
|
101
|
+
}
|
|
102
|
+
/** The per-section completeness of one produced evidence bundle (all four sections, always). */
|
|
103
|
+
export interface CapabilityEvidenceCompleteness {
|
|
104
|
+
readonly skills: CatalogSectionCompleteness;
|
|
105
|
+
readonly tools: CatalogSectionCompleteness;
|
|
106
|
+
readonly mcpServers: CatalogSectionCompleteness;
|
|
107
|
+
readonly channels: CatalogSectionCompleteness;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* What {@link produceCapabilityEvidence} answers. Plain JSON-safe data; safe
|
|
111
|
+
* across a JSON boundary, and the round-trip is pinned by a test.
|
|
112
|
+
*/
|
|
113
|
+
export interface ProducedCapabilityEvidence {
|
|
114
|
+
/** What claims to have been measured. */
|
|
115
|
+
readonly measurement: CapabilityEvidenceMeasurement;
|
|
116
|
+
/**
|
|
117
|
+
* The composition digest: `hashOf` over the fingerprinted snapshot, by this
|
|
118
|
+
* package's own algorithm. Consumers compare it, they never recompute it.
|
|
119
|
+
*/
|
|
120
|
+
readonly sha: string;
|
|
121
|
+
/** How completely each snapshot section was measured. */
|
|
122
|
+
readonly completeness: CapabilityEvidenceCompleteness;
|
|
123
|
+
/** The caller's coverage declaration, echoed. */
|
|
124
|
+
readonly coverage: CapabilityEvidenceCoverage;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Produce the capability-composition evidence of the composition THIS
|
|
128
|
+
* process runs, as plain data another process can verify.
|
|
129
|
+
*
|
|
130
|
+
* The measurement reuses the package's own algorithms end to end: the tools
|
|
131
|
+
* side is read by `tool-face.ts` (the same reads the Remote verbs make), the
|
|
132
|
+
* snapshot is `catalogSnapshot` in fingerprint mode, and the digest is its
|
|
133
|
+
* stamped `sha` — nothing is re-implemented, so the answer is comparable
|
|
134
|
+
* with a `snapshotFor` digest taken over the same registries.
|
|
135
|
+
* @param options - every input, explicit; see {@link ProduceCapabilityEvidenceOptions}.
|
|
136
|
+
* @returns the evidence bundle — digest, completeness, measurement, coverage.
|
|
137
|
+
* @throws when `workdir` is given but not absolute, when `presetId` is given
|
|
138
|
+
* but blank, or when the coverage declaration is missing — the caller
|
|
139
|
+
* verifies identity and coverage; this function only measures.
|
|
140
|
+
*/
|
|
141
|
+
export declare function produceCapabilityEvidence(options: ProduceCapabilityEvidenceOptions): Promise<ProducedCapabilityEvidence>;
|
|
142
|
+
//# sourceMappingURL=evidence.d.ts.map
|