@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +17 -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 +14790 -902
  7. package/lib/index.js +760 -265
  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 +16 -4
  24. package/lib/types/index.js +32 -53
  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
@@ -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, readModeFor(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
- save: actions.save,
476
- adopt: actions.adopt,
477
- release: actions.release,
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
- save: (presets: readonly string[]) => Promise<CatalogPresetScopeEditResult>;
29
- adopt: (presets: readonly string[]) => Promise<CatalogPresetScopeEditResult>;
30
- release: () => Promise<CatalogPresetScopeEditResult>;
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: [scope.managed ? (_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy, onClick: () => void runScope(() => scope.save(pickedScope)), children: scopeBusy ? t('scopeSaving') : t('scopeSave') })) : null, scope.managed ? (_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy, onClick: () => void runScope(() => scope.release()), children: t('scopeRelease') })) : scope.adoptable ? (_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy, onClick: () => void runScope(() => scope.adopt(pickedScope)), children: t('scopeAdopt') })) : null, 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) => {
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;一个都不勾表示所有模式可见。改的是 skill 自己的 frontmatter。',
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. This edits the skill\'s own frontmatter.',
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',
@@ -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.
@@ -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
- const scope = await getScope().catch(() => undefined);
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 || credentials === 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 decls = mergeCredentialDecls(decodeCredentialDecls(def.metadata), decodeEnvDecls(def.content ?? ''));
49
- const configured = [];
50
- for (const decl of decls) {
51
- const info = await credentials.describe(decl.key).catch(() => undefined);
52
- if (info?.configured === true)
53
- configured.push(decl.key);
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 (configured.length === 0)
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: hint }],
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