@khorsheed/dsh-capability-catalog 0.1.95
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +286 -0
- package/README.zh.md +138 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +10100 -0
- package/lib/index.js +4022 -0
- package/lib/invariant.js +25 -0
- package/lib/mcpConnector-C2TbkPBY.js +69 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1417 -0
- package/lib/typert.remote-client.d.ts +68 -0
- package/lib/typert.remote-client.js +1136 -0
- package/lib/types/capabilities.d.ts +103 -0
- package/lib/types/capabilities.js +116 -0
- package/lib/types/channels.d.ts +41 -0
- package/lib/types/channels.js +75 -0
- package/lib/types/client/AddMcpDialog.d.ts +13 -0
- package/lib/types/client/AddMcpDialog.js +79 -0
- package/lib/types/client/AddSkillModal.d.ts +17 -0
- package/lib/types/client/AddSkillModal.js +138 -0
- package/lib/types/client/BundleFileTree.d.ts +7 -0
- package/lib/types/client/BundleFileTree.js +74 -0
- package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
- package/lib/types/client/CapabilityCatalogCard.js +479 -0
- package/lib/types/client/CredentialField.d.ts +14 -0
- package/lib/types/client/CredentialField.js +9 -0
- package/lib/types/client/McpCards.d.ts +13 -0
- package/lib/types/client/McpCards.js +12 -0
- package/lib/types/client/McpServerManageModal.d.ts +18 -0
- package/lib/types/client/McpServerManageModal.js +63 -0
- package/lib/types/client/MetadataRow.d.ts +10 -0
- package/lib/types/client/MetadataRow.js +6 -0
- package/lib/types/client/ModalShell.d.ts +16 -0
- package/lib/types/client/ModalShell.js +27 -0
- package/lib/types/client/ModeChips.d.ts +38 -0
- package/lib/types/client/ModeChips.js +42 -0
- package/lib/types/client/SchemaView.d.ts +11 -0
- package/lib/types/client/SchemaView.js +143 -0
- package/lib/types/client/SkillCards.d.ts +28 -0
- package/lib/types/client/SkillCards.js +43 -0
- package/lib/types/client/SkillDetailModal.d.ts +69 -0
- package/lib/types/client/SkillDetailModal.js +167 -0
- package/lib/types/client/ToolCards.d.ts +28 -0
- package/lib/types/client/ToolCards.js +52 -0
- package/lib/types/client/ToolDetailModal.d.ts +12 -0
- package/lib/types/client/ToolDetailModal.js +29 -0
- package/lib/types/client/icons.d.ts +26 -0
- package/lib/types/client/icons.js +10 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +196 -0
- package/lib/types/client/locales.d.ts +392 -0
- package/lib/types/client/locales.js +395 -0
- package/lib/types/client/mcp-model.d.ts +24 -0
- package/lib/types/client/mcp-model.js +87 -0
- package/lib/types/client/mode-model.d.ts +108 -0
- package/lib/types/client/mode-model.js +140 -0
- package/lib/types/client/preset-display.d.ts +21 -0
- package/lib/types/client/preset-display.js +20 -0
- package/lib/types/client/settle.d.ts +40 -0
- package/lib/types/client/settle.js +57 -0
- package/lib/types/client/slots.d.ts +86 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/envHint.d.ts +22 -0
- package/lib/types/envHint.js +76 -0
- package/lib/types/import.d.ts +106 -0
- package/lib/types/import.js +635 -0
- package/lib/types/index.d.ts +216 -0
- package/lib/types/index.js +739 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/mcpConnector.d.ts +24 -0
- package/lib/types/mcpConnector.js +73 -0
- package/lib/types/mcpStore.d.ts +68 -0
- package/lib/types/mcpStore.js +235 -0
- package/lib/types/mcpTools.d.ts +45 -0
- package/lib/types/mcpTools.js +125 -0
- package/lib/types/mcps.d.ts +64 -0
- package/lib/types/mcps.js +157 -0
- package/lib/types/modes.d.ts +65 -0
- package/lib/types/modes.js +97 -0
- package/lib/types/namespace.d.ts +9 -0
- package/lib/types/namespace.js +8 -0
- package/lib/types/official-tools.d.ts +3 -0
- package/lib/types/official-tools.js +44 -0
- package/lib/types/preset-scope.d.ts +105 -0
- package/lib/types/preset-scope.js +117 -0
- package/lib/types/remote.d.ts +48 -0
- package/lib/types/remote.js +120 -0
- package/lib/types/scoped-delivery.d.ts +243 -0
- package/lib/types/scoped-delivery.js +686 -0
- package/lib/types/scoped-edits.d.ts +90 -0
- package/lib/types/scoped-edits.js +234 -0
- package/lib/types/settings.d.ts +28 -0
- package/lib/types/settings.js +28 -0
- package/lib/types/shellEnv.d.ts +65 -0
- package/lib/types/shellEnv.js +232 -0
- package/lib/types/skills.d.ts +103 -0
- package/lib/types/skills.js +303 -0
- package/lib/types/tool-origin.d.ts +29 -0
- package/lib/types/tool-origin.js +30 -0
- package/lib/types/types.d.ts +315 -0
- package/lib/types/types.js +8 -0
- package/lib/types/zip.d.ts +24 -0
- package/lib/types/zip.js +80 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +119 -0
- package/scripts/gen-official-tools.mts +52 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability catalog host service: a Typert Remote over the running skill and
|
|
3
|
+
* tool registries. It projects `ctx.skills.snapshot()` into catalog rows,
|
|
4
|
+
* attributes `ctx.tools.schemas()` to a channel (mcp prefix / official
|
|
5
|
+
* whitelist / baseline-diff), loads per-skill detail on demand, writes
|
|
6
|
+
* declared credentials, and adds skills into the managed root. The browser
|
|
7
|
+
* half binds through the generated `capabilityCatalog` Remote namespace.
|
|
8
|
+
*
|
|
9
|
+
* Zero host edits: everything rides existing official services (`ctx.skills`,
|
|
10
|
+
* `ctx.tools`, `ctx.credentials`) through `ctx.get`, degrading silently when a
|
|
11
|
+
* capability is absent.
|
|
12
|
+
* @module @khorsheed/dsh-capability-catalog
|
|
13
|
+
*/
|
|
14
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
15
|
+
var useValue = arguments.length > 2;
|
|
16
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
17
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
18
|
+
}
|
|
19
|
+
return useValue ? value : void 0;
|
|
20
|
+
};
|
|
21
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
22
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
23
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
24
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
25
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
26
|
+
var _, done = false;
|
|
27
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
28
|
+
var context = {};
|
|
29
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
30
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
31
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
32
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
33
|
+
if (kind === "accessor") {
|
|
34
|
+
if (result === void 0) continue;
|
|
35
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
36
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
37
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
38
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
39
|
+
}
|
|
40
|
+
else if (_ = accept(result)) {
|
|
41
|
+
if (kind === "field") initializers.unshift(_);
|
|
42
|
+
else descriptor[key] = _;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
46
|
+
done = true;
|
|
47
|
+
};
|
|
48
|
+
import { homedir } from 'node:os';
|
|
49
|
+
import { join } from 'node:path';
|
|
50
|
+
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
51
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
52
|
+
import { catalogAddSkill, catalogDetail, catalogListDirSkills, catalogReadSkillFile, catalogSetCredential, catalogSnapshot, catalogDeleteSkill, catalogPickDirectory } from './remote.js';
|
|
53
|
+
import { resolveServices } from './skills.js';
|
|
54
|
+
import { definitionFor, scanManagedSkills, ScopedSkillDelivery, scopedSkillsRoot, } from './scoped-delivery.js';
|
|
55
|
+
import { adoptManagedSkill, releaseManagedSkill, setManagedPresetScope, } from './scoped-edits.js';
|
|
56
|
+
import { installSkillEnvInjection } from './shellEnv.js';
|
|
57
|
+
import { McpStore } from './mcpStore.js';
|
|
58
|
+
import { reconcileRegisteredMcpTools, desiredMcpTools } from './mcpTools.js';
|
|
59
|
+
import { maskConfig } from './mcps.js';
|
|
60
|
+
import { installSkillEnvHint } from './envHint.js';
|
|
61
|
+
import { MCP_TOOL_PREFIX } from './channels.js';
|
|
62
|
+
import { toolOrigin, isValidOrigin } from './tool-origin.js';
|
|
63
|
+
import { CAPABILITY_CATALOG_NS } from './namespace.js';
|
|
64
|
+
import { CapabilityCatalogSettingsSchema } from './settings.js';
|
|
65
|
+
import { capsTag, hashOf } from './capabilities.js';
|
|
66
|
+
import { resolvePresetScope } from './preset-scope.js';
|
|
67
|
+
import { modeOptions, readModeFaces } from './modes.js';
|
|
68
|
+
// Community tool-origin convention: re-export the tag helpers so any plugin can
|
|
69
|
+
// `import { setToolOrigin } from '@khorsheed/dsh-capability-catalog'`.
|
|
70
|
+
export { TOOL_ORIGIN, setToolOrigin, toolOrigin } from './tool-origin.js';
|
|
71
|
+
export { resolveSkillNameFromContent } from './import.js';
|
|
72
|
+
export { resolvePresetScope } from './preset-scope.js';
|
|
73
|
+
// The mode view: the roster a picker offers and one face per mode.
|
|
74
|
+
export { modeOptions, readModeFaces } from './modes.js';
|
|
75
|
+
export { ScopedSkillDelivery, scopedSkillsRoot, scanManagedSkills, managedSkillFrom, parsePresetScopeFrontmatter, defaultSkillRoots, detectConflicts, SCOPED_PROVIDER_NAME, MANAGED_SKILL_RANK, } from './scoped-delivery.js';
|
|
76
|
+
export { adoptManagedSkill, releaseManagedSkill, setManagedPresetScope, withPresetScope, findSkillSource, managedLocation, isDirectory, } from './scoped-edits.js';
|
|
77
|
+
// The capability fingerprint: the canonical form, its digest, and the tag.
|
|
78
|
+
// Exported from the package root so a host reader can hash a snapshot it
|
|
79
|
+
// already holds without a second Remote round-trip.
|
|
80
|
+
export { canonicalCapabilities, canonicalJson, capsTag, hashOf, hashSkillBody, CAPS_TAG_PREFIX, } from './capabilities.js';
|
|
81
|
+
/** The remote namespace name (matches the generated remote-client type). */
|
|
82
|
+
export const REMOTE_NS = 'capabilityCatalog';
|
|
83
|
+
/** Custom inject: subscribe no extra service beyond the two we probe. */
|
|
84
|
+
function resolveServicesHelper(ctx) {
|
|
85
|
+
return resolveServices(ctx);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Capability catalog Remote service.
|
|
89
|
+
* @module @khorsheed/dsh-capability-catalog
|
|
90
|
+
*/
|
|
91
|
+
let CapabilityCatalogService = (() => {
|
|
92
|
+
let _classSuper = TypertRemoteService;
|
|
93
|
+
let _instanceExtraInitializers = [];
|
|
94
|
+
let _presetScopeStatus_decorators;
|
|
95
|
+
let _presetScopeRoster_decorators;
|
|
96
|
+
let _modeFaces_decorators;
|
|
97
|
+
let _presetScopeSet_decorators;
|
|
98
|
+
let _presetScopeAdopt_decorators;
|
|
99
|
+
let _presetScopeRelease_decorators;
|
|
100
|
+
let _snapshot_decorators;
|
|
101
|
+
let _snapshotAt_decorators;
|
|
102
|
+
let _snapshotFor_decorators;
|
|
103
|
+
let _detail_decorators;
|
|
104
|
+
let _readSkillFile_decorators;
|
|
105
|
+
let _listDirSkills_decorators;
|
|
106
|
+
let _setCredential_decorators;
|
|
107
|
+
let _addSkill_decorators;
|
|
108
|
+
let _deleteSkill_decorators;
|
|
109
|
+
let _pickDirectory_decorators;
|
|
110
|
+
let _mcpList_decorators;
|
|
111
|
+
let _mcpAdd_decorators;
|
|
112
|
+
let _mcpRemove_decorators;
|
|
113
|
+
let _mcpSetEnabled_decorators;
|
|
114
|
+
let _mcpSetCredential_decorators;
|
|
115
|
+
let _mcpSetToolEnabled_decorators;
|
|
116
|
+
let _mcpDiscover_decorators;
|
|
117
|
+
let _mcpSnapshot_decorators;
|
|
118
|
+
return class CapabilityCatalogService extends _classSuper {
|
|
119
|
+
static {
|
|
120
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
121
|
+
_presetScopeStatus_decorators = [Remote('presetScopeStatus')];
|
|
122
|
+
_presetScopeRoster_decorators = [Remote('presetScopeRoster')];
|
|
123
|
+
_modeFaces_decorators = [Remote('modeFaces')];
|
|
124
|
+
_presetScopeSet_decorators = [Remote('presetScopeSet')];
|
|
125
|
+
_presetScopeAdopt_decorators = [Remote('presetScopeAdopt')];
|
|
126
|
+
_presetScopeRelease_decorators = [Remote('presetScopeRelease')];
|
|
127
|
+
_snapshot_decorators = [Remote('snapshot')];
|
|
128
|
+
_snapshotAt_decorators = [Remote('snapshotAt')];
|
|
129
|
+
_snapshotFor_decorators = [Remote('snapshotFor')];
|
|
130
|
+
_detail_decorators = [Remote('detail')];
|
|
131
|
+
_readSkillFile_decorators = [Remote('readSkillFile')];
|
|
132
|
+
_listDirSkills_decorators = [Remote('listDirSkills')];
|
|
133
|
+
_setCredential_decorators = [Remote('setCredential')];
|
|
134
|
+
_addSkill_decorators = [Remote('addSkill')];
|
|
135
|
+
_deleteSkill_decorators = [Remote('deleteSkill')];
|
|
136
|
+
_pickDirectory_decorators = [Remote('pickDirectory')];
|
|
137
|
+
_mcpList_decorators = [Remote('mcpList')];
|
|
138
|
+
_mcpAdd_decorators = [Remote('mcpAdd')];
|
|
139
|
+
_mcpRemove_decorators = [Remote('mcpRemove')];
|
|
140
|
+
_mcpSetEnabled_decorators = [Remote('mcpSetEnabled')];
|
|
141
|
+
_mcpSetCredential_decorators = [Remote('mcpSetCredential')];
|
|
142
|
+
_mcpSetToolEnabled_decorators = [Remote('mcpSetToolEnabled')];
|
|
143
|
+
_mcpDiscover_decorators = [Remote('mcpDiscover')];
|
|
144
|
+
_mcpSnapshot_decorators = [Remote('mcpSnapshot')];
|
|
145
|
+
__esDecorate(this, null, _presetScopeStatus_decorators, { kind: "method", name: "presetScopeStatus", static: false, private: false, access: { has: obj => "presetScopeStatus" in obj, get: obj => obj.presetScopeStatus }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
146
|
+
__esDecorate(this, null, _presetScopeRoster_decorators, { kind: "method", name: "presetScopeRoster", static: false, private: false, access: { has: obj => "presetScopeRoster" in obj, get: obj => obj.presetScopeRoster }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
147
|
+
__esDecorate(this, null, _modeFaces_decorators, { kind: "method", name: "modeFaces", static: false, private: false, access: { has: obj => "modeFaces" in obj, get: obj => obj.modeFaces }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
148
|
+
__esDecorate(this, null, _presetScopeSet_decorators, { kind: "method", name: "presetScopeSet", static: false, private: false, access: { has: obj => "presetScopeSet" in obj, get: obj => obj.presetScopeSet }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
149
|
+
__esDecorate(this, null, _presetScopeAdopt_decorators, { kind: "method", name: "presetScopeAdopt", static: false, private: false, access: { has: obj => "presetScopeAdopt" in obj, get: obj => obj.presetScopeAdopt }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
150
|
+
__esDecorate(this, null, _presetScopeRelease_decorators, { kind: "method", name: "presetScopeRelease", static: false, private: false, access: { has: obj => "presetScopeRelease" in obj, get: obj => obj.presetScopeRelease }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
151
|
+
__esDecorate(this, null, _snapshot_decorators, { kind: "method", name: "snapshot", static: false, private: false, access: { has: obj => "snapshot" in obj, get: obj => obj.snapshot }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
152
|
+
__esDecorate(this, null, _snapshotAt_decorators, { kind: "method", name: "snapshotAt", static: false, private: false, access: { has: obj => "snapshotAt" in obj, get: obj => obj.snapshotAt }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
153
|
+
__esDecorate(this, null, _snapshotFor_decorators, { kind: "method", name: "snapshotFor", static: false, private: false, access: { has: obj => "snapshotFor" in obj, get: obj => obj.snapshotFor }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
154
|
+
__esDecorate(this, null, _detail_decorators, { kind: "method", name: "detail", static: false, private: false, access: { has: obj => "detail" in obj, get: obj => obj.detail }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
155
|
+
__esDecorate(this, null, _readSkillFile_decorators, { kind: "method", name: "readSkillFile", static: false, private: false, access: { has: obj => "readSkillFile" in obj, get: obj => obj.readSkillFile }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
156
|
+
__esDecorate(this, null, _listDirSkills_decorators, { kind: "method", name: "listDirSkills", static: false, private: false, access: { has: obj => "listDirSkills" in obj, get: obj => obj.listDirSkills }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
157
|
+
__esDecorate(this, null, _setCredential_decorators, { kind: "method", name: "setCredential", static: false, private: false, access: { has: obj => "setCredential" in obj, get: obj => obj.setCredential }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
158
|
+
__esDecorate(this, null, _addSkill_decorators, { kind: "method", name: "addSkill", static: false, private: false, access: { has: obj => "addSkill" in obj, get: obj => obj.addSkill }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
159
|
+
__esDecorate(this, null, _deleteSkill_decorators, { kind: "method", name: "deleteSkill", static: false, private: false, access: { has: obj => "deleteSkill" in obj, get: obj => obj.deleteSkill }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
160
|
+
__esDecorate(this, null, _pickDirectory_decorators, { kind: "method", name: "pickDirectory", static: false, private: false, access: { has: obj => "pickDirectory" in obj, get: obj => obj.pickDirectory }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
161
|
+
__esDecorate(this, null, _mcpList_decorators, { kind: "method", name: "mcpList", static: false, private: false, access: { has: obj => "mcpList" in obj, get: obj => obj.mcpList }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
162
|
+
__esDecorate(this, null, _mcpAdd_decorators, { kind: "method", name: "mcpAdd", static: false, private: false, access: { has: obj => "mcpAdd" in obj, get: obj => obj.mcpAdd }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
163
|
+
__esDecorate(this, null, _mcpRemove_decorators, { kind: "method", name: "mcpRemove", static: false, private: false, access: { has: obj => "mcpRemove" in obj, get: obj => obj.mcpRemove }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
164
|
+
__esDecorate(this, null, _mcpSetEnabled_decorators, { kind: "method", name: "mcpSetEnabled", static: false, private: false, access: { has: obj => "mcpSetEnabled" in obj, get: obj => obj.mcpSetEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
165
|
+
__esDecorate(this, null, _mcpSetCredential_decorators, { kind: "method", name: "mcpSetCredential", static: false, private: false, access: { has: obj => "mcpSetCredential" in obj, get: obj => obj.mcpSetCredential }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
166
|
+
__esDecorate(this, null, _mcpSetToolEnabled_decorators, { kind: "method", name: "mcpSetToolEnabled", static: false, private: false, access: { has: obj => "mcpSetToolEnabled" in obj, get: obj => obj.mcpSetToolEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
167
|
+
__esDecorate(this, null, _mcpDiscover_decorators, { kind: "method", name: "mcpDiscover", static: false, private: false, access: { has: obj => "mcpDiscover" in obj, get: obj => obj.mcpDiscover }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
168
|
+
__esDecorate(this, null, _mcpSnapshot_decorators, { kind: "method", name: "mcpSnapshot", static: false, private: false, access: { has: obj => "mcpSnapshot" in obj, get: obj => obj.mcpSnapshot }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
169
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
170
|
+
}
|
|
171
|
+
config = __runInitializers(this, _instanceExtraInitializers);
|
|
172
|
+
static inject = [];
|
|
173
|
+
static Config = CapabilityCatalogSettingsSchema;
|
|
174
|
+
baseline;
|
|
175
|
+
appearedAfterApply;
|
|
176
|
+
mcp;
|
|
177
|
+
/** The live `ctx.tools` (traceable proxy) captured from the tools inject. */
|
|
178
|
+
mcpToolsRegistry;
|
|
179
|
+
/** Registered MCP tool disposers, keyed by model-facing public name. */
|
|
180
|
+
mcpToolDisposers = new Map();
|
|
181
|
+
/** Preset-scoped delivery of the plugin's own managed skill root. */
|
|
182
|
+
scoped;
|
|
183
|
+
/** The workspace the catalog was last asked about, for project-root conflict checks. */
|
|
184
|
+
observedWorkdir;
|
|
185
|
+
constructor(ctx, config) {
|
|
186
|
+
super(ctx, 'capabilityCatalog');
|
|
187
|
+
this.config = config;
|
|
188
|
+
this.mcp = new McpStore();
|
|
189
|
+
this.baseline = new Set();
|
|
190
|
+
this.appearedAfterApply = new Set();
|
|
191
|
+
// Persist the MCP state so servers survive a restart: seed the in-process
|
|
192
|
+
// store at boot, persist every mutation, and reload when the config
|
|
193
|
+
// document changes externally. Two host lines, one outcome: 0.1.5 serves a
|
|
194
|
+
// free-form `settings.register` namespace (whose scope.watch round-trips
|
|
195
|
+
// both our own and external writes); 0.1.7 persists the plugin Config's
|
|
196
|
+
// volatile `mcp` field through SettingsForms (writes ride the profile
|
|
197
|
+
// patch, the running fiber's Volatile reference tracks them, and
|
|
198
|
+
// `settings/document-updated` is the round-trip channel). The card reads
|
|
199
|
+
// its data through the Remote on both lines. Degrades silently when
|
|
200
|
+
// settings is absent (in-process store only, resets on restart).
|
|
201
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
202
|
+
const svc = settingsCtx.settings;
|
|
203
|
+
if (typeof svc.register === 'function') {
|
|
204
|
+
const scope = svc.register(CAPABILITY_CATALOG_NS, CapabilityCatalogSettingsSchema);
|
|
205
|
+
const persisted = scope.get()?.mcp;
|
|
206
|
+
if (persisted !== undefined)
|
|
207
|
+
this.mcp.loadFrom(persisted);
|
|
208
|
+
this.mcp.onPersist = () => {
|
|
209
|
+
void scope.update({ mcp: this.mcp.toPersisted() }).catch((error) => this.ctx.logger.error(error));
|
|
210
|
+
};
|
|
211
|
+
scope.watch((next) => {
|
|
212
|
+
this.mcp.loadFrom(next?.mcp);
|
|
213
|
+
this.afterMcpMutation();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else if (typeof svc.update === 'function') {
|
|
217
|
+
// The catalog ships its own card; keep the machine-state block out of
|
|
218
|
+
// any auto-generated form.
|
|
219
|
+
svc.configure?.({ auto: false }, ctx.fiber);
|
|
220
|
+
const readVolatile = () => {
|
|
221
|
+
const field = this.config.mcp;
|
|
222
|
+
if (field !== null && typeof field === 'object' && typeof field.get === 'function') {
|
|
223
|
+
return field.get();
|
|
224
|
+
}
|
|
225
|
+
return field;
|
|
226
|
+
};
|
|
227
|
+
const persisted = readVolatile();
|
|
228
|
+
if (persisted !== undefined)
|
|
229
|
+
this.mcp.loadFrom(persisted);
|
|
230
|
+
this.mcp.onPersist = () => {
|
|
231
|
+
// Wholesale write, NOT merge: `update` mergeLayers recurses plain
|
|
232
|
+
// objects, so a REMOVED server/credential/tool key would survive in
|
|
233
|
+
// the document and come back through the document-updated
|
|
234
|
+
// round-trip below. `replace` resets the live fields first; the mcp
|
|
235
|
+
// block is this entry's only volatile field, so a replace is exactly
|
|
236
|
+
// the block swap. rc.1's SettingsForms carries both.
|
|
237
|
+
const write = typeof svc.replace === 'function'
|
|
238
|
+
? svc.replace(CAPABILITY_CATALOG_NS, { mcp: this.mcp.toPersisted() })
|
|
239
|
+
: svc.update(CAPABILITY_CATALOG_NS, { mcp: this.mcp.toPersisted() });
|
|
240
|
+
void write.catch((error) => this.ctx.logger.error(error));
|
|
241
|
+
};
|
|
242
|
+
settingsCtx.on('settings/document-updated', (ns) => {
|
|
243
|
+
if (ns !== CAPABILITY_CATALOG_NS)
|
|
244
|
+
return;
|
|
245
|
+
this.mcp.loadFrom(readVolatile());
|
|
246
|
+
this.afterMcpMutation();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// Re-sync after loading persisted state (the tools inject may have fired
|
|
250
|
+
// before the store was seeded).
|
|
251
|
+
this.syncRegisteredMcpTools();
|
|
252
|
+
// The persisted block stores only tool names + toggles (descriptions and
|
|
253
|
+
// parameter schemas would bloat settings.yaml and go stale), so refill
|
|
254
|
+
// them by re-connecting the enabled servers at boot, async so boot is not
|
|
255
|
+
// blocked. Re-syncs registration once the descriptions are back.
|
|
256
|
+
void this.reconnectEnabledMcp();
|
|
257
|
+
});
|
|
258
|
+
// Baseline snapshot of the tools visible once the registry is ready; tools
|
|
259
|
+
// that appear later (a tools/change diff) are marked plugin/inferred. Set it
|
|
260
|
+
// inside the DEFERRED tools inject — an apply-time ctx.get probe races the
|
|
261
|
+
// registry's own mount order and can lose on the real composition tree (tools
|
|
262
|
+
// may compose after the catalog), yielding an empty baseline and mislabeling
|
|
263
|
+
// every visible tool as post-apply/plugin.
|
|
264
|
+
ctx.inject(['tools'], (toolCtx) => {
|
|
265
|
+
this.mcpToolsRegistry = toolCtx.tools;
|
|
266
|
+
this.syncRegisteredMcpTools();
|
|
267
|
+
this.registerListTool(toolCtx.tools);
|
|
268
|
+
// Baseline for the channel heuristics (set where the registry is known).
|
|
269
|
+
this.baseline = new Set(toolCtx.tools.schemas().map(t => t.name) ?? []);
|
|
270
|
+
this.appearedAfterApply.clear();
|
|
271
|
+
ctx.on('tools/change', () => this.markNewTools());
|
|
272
|
+
});
|
|
273
|
+
// Expose each configured skill credential as a trusted per-execution
|
|
274
|
+
// `DSH_<KEY>` env var so the agent's shell can use it (shell expansion),
|
|
275
|
+
// without the raw value entering the model's context (default-hide). The
|
|
276
|
+
// catalog is service-agnostic — it knows nothing about the specific service;
|
|
277
|
+
// the skill tells the agent how to query. Defer to composition time via
|
|
278
|
+
// inject so the env-injection runs once shellEnv/credentials/skills are
|
|
279
|
+
// registered (at the catalog's apply they may not be composed yet).
|
|
280
|
+
ctx.inject(['shellEnv', 'credentials', 'skills'], () => {
|
|
281
|
+
installSkillEnvInjection(ctx, () => this.catalogScope());
|
|
282
|
+
});
|
|
283
|
+
// Runtime companion hint (generic): when the skill tool loads a skill,
|
|
284
|
+
// tell the model the configured credential env mappings so it uses the
|
|
285
|
+
// DSH_<KEY> alias without the skill being modified.
|
|
286
|
+
installSkillEnvHint(ctx, () => this.catalogScope());
|
|
287
|
+
// Preset-scoped delivery: the plugin's own managed root holds skills whose
|
|
288
|
+
// frontmatter names the presets they belong to, and each of those presets
|
|
289
|
+
// gets a provider registered into ITS scope layer. Deferred via inject so
|
|
290
|
+
// the registry is present, and owned by an effect so the delivery scopes
|
|
291
|
+
// (and the filesystem watcher) unwind with the plugin.
|
|
292
|
+
ctx.inject(['skills'], (skillsCtx) => {
|
|
293
|
+
const delivery = new ScopedSkillDelivery(skillsCtx, {
|
|
294
|
+
dshHome: () => this.dshHome(),
|
|
295
|
+
roster: () => this.agentPresets(),
|
|
296
|
+
registry: () => this.ctx.get?.('skills'),
|
|
297
|
+
workdir: () => this.observedWorkdir,
|
|
298
|
+
log: (message) => this.ctx.logger.warn(message),
|
|
299
|
+
});
|
|
300
|
+
this.scoped = delivery;
|
|
301
|
+
skillsCtx.effect(() => {
|
|
302
|
+
void delivery.start().catch((error) => {
|
|
303
|
+
this.ctx.logger.warn(`capability-catalog: preset-scoped delivery failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
+
});
|
|
305
|
+
return () => { void delivery.dispose(); };
|
|
306
|
+
}, 'capability-catalog: preset-scoped skills');
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/** The preset-scoped delivery's current state, for the settings surface. */
|
|
310
|
+
async presetScopeStatus() {
|
|
311
|
+
const status = this.scoped?.status();
|
|
312
|
+
return status === undefined
|
|
313
|
+
? {
|
|
314
|
+
enabled: false,
|
|
315
|
+
reason: 'preset-scoped delivery is not composed',
|
|
316
|
+
root: scopedSkillsRoot(this.dshHome()),
|
|
317
|
+
watching: false,
|
|
318
|
+
skills: [],
|
|
319
|
+
presets: [],
|
|
320
|
+
customRootsUnverifiable: true,
|
|
321
|
+
}
|
|
322
|
+
: status;
|
|
323
|
+
}
|
|
324
|
+
/** Every preset the roster supplies, for the scope picker. */
|
|
325
|
+
async presetScopeRoster() {
|
|
326
|
+
return modeOptions(await this.rosterRows(), this.agentPresets()?.defaultId);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Every mode's capability face in ONE call, for the cross-mode comparison
|
|
330
|
+
* view ("which modes load this tool/skill?").
|
|
331
|
+
*
|
|
332
|
+
* This is the expensive verb of the pair: a mode whose standing scope has not
|
|
333
|
+
* been composed yet is MOUNTED by its read (see modes.ts — there is no cheap
|
|
334
|
+
* honest source, because a composition file names plugins, not the tools and
|
|
335
|
+
* skills those plugins register). The browser therefore calls it only when a
|
|
336
|
+
* human chooses to compare modes, never on opening the section.
|
|
337
|
+
*
|
|
338
|
+
* A mode that cannot be read keeps its row with `unavailable` set rather than
|
|
339
|
+
* an empty face, so the comparison never claims that a broken mode loads
|
|
340
|
+
* nothing. `preset` on a face is always the id that was asked for: a read
|
|
341
|
+
* that silently degraded to the global layer is reported as unavailable, not
|
|
342
|
+
* as that mode's face.
|
|
343
|
+
* @param workdir - optional cwd for project-scoped skill roots.
|
|
344
|
+
*/
|
|
345
|
+
async modeFaces(workdir) {
|
|
346
|
+
const options = modeOptions(await this.rosterRows(), this.agentPresets()?.defaultId);
|
|
347
|
+
return readModeFaces(options, async (id) => {
|
|
348
|
+
try {
|
|
349
|
+
const face = await this.collect(id, workdir, false);
|
|
350
|
+
if (face.preset !== id) {
|
|
351
|
+
this.ctx.logger.warn(`capability-catalog: preset "${id}" resolved no standing scope — its mode face reads as unavailable`);
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
return face;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
this.ctx.logger.warn(`capability-catalog: mode face read for preset "${id}" failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** The roster rows, or [] when this composition mounts no agent-preset service. */
|
|
363
|
+
async rosterRows() {
|
|
364
|
+
const roster = this.agentPresets();
|
|
365
|
+
if (roster?.list === undefined)
|
|
366
|
+
return [];
|
|
367
|
+
try {
|
|
368
|
+
return await roster.list();
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
this.ctx.logger.warn(`capability-catalog: the preset roster could not be listed: ${error instanceof Error ? error.message : String(error)}`);
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Declare which presets one managed skill is delivered to. */
|
|
376
|
+
async presetScopeSet(request) {
|
|
377
|
+
const result = await setManagedPresetScope(this.dshHome(), request.name, request.presets);
|
|
378
|
+
await this.reconcileScoped();
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
/** Move an installed skill into the managed root with a preset scope. */
|
|
382
|
+
async presetScopeAdopt(request) {
|
|
383
|
+
const result = await adoptManagedSkill(this.dshHome(), request.name, request.presets, request.workdir ?? this.observedWorkdir);
|
|
384
|
+
await this.reconcileScoped();
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
/** Move a managed skill back to the user skill root (the recovery direction). */
|
|
388
|
+
async presetScopeRelease(name) {
|
|
389
|
+
const result = await releaseManagedSkill(this.dshHome(), name);
|
|
390
|
+
await this.reconcileScoped();
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
/** Re-deliver after a write; a delivery that is absent is not an error. */
|
|
394
|
+
async reconcileScoped() {
|
|
395
|
+
try {
|
|
396
|
+
await this.scoped?.reconcile();
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
this.ctx.logger.warn(`capability-catalog: scoped skill reconcile failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/** Record tools that appeared after the apply-time baseline (reads the live
|
|
403
|
+
* registry each time, so a change event never sees a stale/undefined ref). */
|
|
404
|
+
markNewTools() {
|
|
405
|
+
const tools = this.ctx.get?.('tools');
|
|
406
|
+
const current = new Set(tools?.schemas().map(t => t.name) ?? []);
|
|
407
|
+
for (const name of current) {
|
|
408
|
+
if (!this.baseline.has(name))
|
|
409
|
+
this.appearedAfterApply.add(name);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** The managed skills root under `$DSH_HOME`. */
|
|
413
|
+
dshHome() {
|
|
414
|
+
const settings = this.ctx.get?.('settings');
|
|
415
|
+
const home = settings?.home;
|
|
416
|
+
return home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh-official');
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The standing skill view scope of ONE preset. In the web bundle the
|
|
420
|
+
* host-global skill-filesystem provider is mounted under each agent
|
|
421
|
+
* preset's standing scope, so a host-global snapshot sees only runtime
|
|
422
|
+
* skills; reading at the preset's scope surfaces the official/plugin/user
|
|
423
|
+
* skills the model actually sees. When the presets service is absent the
|
|
424
|
+
* catalog degrades to the global layer alone.
|
|
425
|
+
*
|
|
426
|
+
* `presetId` names WHICH preset — the deployment default when omitted, the
|
|
427
|
+
* way every reader before `snapshotFor` behaved. The roster's scope face
|
|
428
|
+
* (0.1.5's `standingKeyFor`, rc.1's leased `acquireScope`) has always taken
|
|
429
|
+
* an id; the catalog simply never passed one, so "the capability face of
|
|
430
|
+
* preset X" was unaskable and a condition declaring `preset: X` could only
|
|
431
|
+
* be believed.
|
|
432
|
+
*
|
|
433
|
+
* `strict` is the listing/fingerprint split — see
|
|
434
|
+
* {@link resolvePresetScope}, which owns the policy.
|
|
435
|
+
* @param presetId - the preset to read, or undefined for the default.
|
|
436
|
+
* @param strict - throw instead of degrading when the roster cannot answer.
|
|
437
|
+
*/
|
|
438
|
+
async catalogScope(presetId, strict = false) {
|
|
439
|
+
const { scope, dispose } = await resolvePresetScope(this.agentPresets(), presetId, strict);
|
|
440
|
+
// Release before the caller reads: the roster reaps a generation only
|
|
441
|
+
// once its preset is unregistered AND unleased, so a live preset's scope
|
|
442
|
+
// survives the release — the same guarantee a lease-free 0.1.5 key had.
|
|
443
|
+
await this.releaseScope(dispose);
|
|
444
|
+
return scope;
|
|
445
|
+
}
|
|
446
|
+
/** Release a scope lease, logging rather than failing the read it followed. */
|
|
447
|
+
async releaseScope(dispose) {
|
|
448
|
+
if (dispose === undefined)
|
|
449
|
+
return;
|
|
450
|
+
try {
|
|
451
|
+
await dispose();
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
this.ctx.logger.warn(`capability-catalog: scope lease release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** The optional agent-preset roster (absent in a rosterless composition). */
|
|
458
|
+
agentPresets() {
|
|
459
|
+
return this.ctx.get?.('agentPresets');
|
|
460
|
+
}
|
|
461
|
+
/** Register the model-facing `list_capabilities` tool (optional; called from the
|
|
462
|
+
* tools inject so the registry is guaranteed present). */
|
|
463
|
+
registerListTool(tools) {
|
|
464
|
+
if (tools.register === undefined)
|
|
465
|
+
return;
|
|
466
|
+
tools.register(defineTool({
|
|
467
|
+
name: 'list_capabilities',
|
|
468
|
+
description: 'List the capabilities (skills and tools) registered in this instance and their source channels. Call to discover what is available.',
|
|
469
|
+
parameters: {
|
|
470
|
+
kind: {
|
|
471
|
+
type: 'string',
|
|
472
|
+
enum: ['skill', 'tool'],
|
|
473
|
+
description: 'Optional: filter to skills or tools.',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
output: {
|
|
477
|
+
schema: { type: 'string' },
|
|
478
|
+
render: (_args, value) => [{ type: 'text', text: String(value) }],
|
|
479
|
+
},
|
|
480
|
+
// Arrow captures `this` so the tool can read the live registries. It runs
|
|
481
|
+
// in the caller agent's scope (exec.agent), so it sees the model's actual
|
|
482
|
+
// view: official, plugin and user/project skills alike.
|
|
483
|
+
execute: async (args, exec) => {
|
|
484
|
+
const { registry } = resolveServicesHelper(this.ctx);
|
|
485
|
+
if (registry === undefined)
|
|
486
|
+
return 'skills service absent in this composition';
|
|
487
|
+
// The FULL face is fingerprinted even when the answer is filtered:
|
|
488
|
+
// `sha` names the instance's capabilities, so it must not change with
|
|
489
|
+
// what the caller asked to see.
|
|
490
|
+
const scope = exec?.agent ?? await this.catalogScope();
|
|
491
|
+
const snapshot = await catalogSnapshot(undefined, registry, this.toolSchemasIn(scope), this.mcpServerNamesIn(scope), this.appearedAfterApply, [scope], this.toolOriginsMap(scope), { fingerprint: true });
|
|
492
|
+
const filter = args?.kind;
|
|
493
|
+
const skills = filter === undefined || filter === 'skill' ? snapshot.skills : [];
|
|
494
|
+
const toolsRows = filter === undefined || filter === 'tool' ? snapshot.tools : [];
|
|
495
|
+
return JSON.stringify({ capabilities: capsTag(snapshot.sha), skills, tools: toolsRows }, null, 2);
|
|
496
|
+
},
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
async snapshot(workdir) {
|
|
500
|
+
return this.collect(undefined, workdir, false);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* The capability face of ONE mode as a LISTING — {@link snapshot} scoped to a
|
|
504
|
+
* named preset, for the settings surface's mode picker.
|
|
505
|
+
*
|
|
506
|
+
* The LISTING/FINGERPRINT split matters here the way it does everywhere else:
|
|
507
|
+
* a viewer wants rows as fast as the registries can produce them, not every
|
|
508
|
+
* skill body loaded and digested. Asking for a preset's IDENTITY is
|
|
509
|
+
* {@link snapshotFor}. Like every scope resolution, this MOUNTS a preset
|
|
510
|
+
* nothing has composed yet.
|
|
511
|
+
*
|
|
512
|
+
* Degrades like {@link snapshot}: a preset whose standing scope refuses
|
|
513
|
+
* answers with the global layer and NO `preset` stamp, which is how a caller
|
|
514
|
+
* tells "this mode" from "the fallback" and says so.
|
|
515
|
+
* @param presetId - the mode to read, or undefined for the deployment default.
|
|
516
|
+
* @param workdir - optional cwd for project-scoped skill roots.
|
|
517
|
+
*/
|
|
518
|
+
async snapshotAt(presetId, workdir) {
|
|
519
|
+
return this.collect(presetId, workdir, false);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* The capability face of ONE preset, with its {@link hashOf} digest.
|
|
523
|
+
*
|
|
524
|
+
* This is the fingerprint verb: every skill body is loaded (so the rows
|
|
525
|
+
* carry `bodySha`) and `sha` is stamped, which is what makes the answer an
|
|
526
|
+
* identity rather than a listing. `presetId` omitted reads the deployment
|
|
527
|
+
* default — the same face `snapshot` reads, now hashable.
|
|
528
|
+
*
|
|
529
|
+
* Degrades like every other verb: without a skills registry the answer is
|
|
530
|
+
* an empty face with the hash of an empty face, not a throw. Note that a
|
|
531
|
+
* preset nobody has composed yet is MOUNTED by the read (the roster's
|
|
532
|
+
* single-flight standing mount), so asking is not free.
|
|
533
|
+
* @param presetId - the preset to fingerprint, or undefined for the default.
|
|
534
|
+
* @param workdir - optional cwd for project-scoped skill roots.
|
|
535
|
+
*/
|
|
536
|
+
async snapshotFor(presetId, workdir) {
|
|
537
|
+
return this.collect(presetId, workdir, true);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Shared body of {@link snapshot} and {@link snapshotFor}. The scope is
|
|
541
|
+
* resolved ONCE — three resolutions of a mounting preset would race the
|
|
542
|
+
* roster's single-flight for no reason — and `fingerprint` makes the
|
|
543
|
+
* resolution strict.
|
|
544
|
+
*/
|
|
545
|
+
async collect(presetId, workdir, fingerprint) {
|
|
546
|
+
const { registry } = resolveServicesHelper(this.ctx);
|
|
547
|
+
if (workdir !== undefined && workdir !== '')
|
|
548
|
+
this.observedWorkdir = workdir;
|
|
549
|
+
// The scope may be LEASED (rc.1 roster): every read below must finish
|
|
550
|
+
// before the release, so the whole body sits in one try.
|
|
551
|
+
const { scope, preset, dispose } = await resolvePresetScope(this.agentPresets(), presetId, fingerprint);
|
|
552
|
+
try {
|
|
553
|
+
if (registry === undefined) {
|
|
554
|
+
const empty = {
|
|
555
|
+
skills: [], tools: [], mcpServers: [], channels: [],
|
|
556
|
+
...preset !== undefined ? { preset } : {},
|
|
557
|
+
};
|
|
558
|
+
return fingerprint ? { ...empty, sha: hashOf(empty) } : empty;
|
|
559
|
+
}
|
|
560
|
+
return await catalogSnapshot(workdir, registry, this.toolSchemasIn(scope), this.mcpServerNamesIn(scope), this.appearedAfterApply, [scope], this.toolOriginsMap(scope), { fingerprint, ...preset !== undefined ? { preset } : {} });
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
await this.releaseScope(dispose);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async detail(name, workdir, presetId) {
|
|
567
|
+
const { registry } = resolveServicesHelper(this.ctx);
|
|
568
|
+
if (registry === undefined)
|
|
569
|
+
return undefined;
|
|
570
|
+
// A managed skill scoped to another preset is absent from the default
|
|
571
|
+
// preset's scope; the plugin's own copy keeps it visible and editable here.
|
|
572
|
+
// `presetId` carries the mode the viewer is LOOKING AT, so a card opened in
|
|
573
|
+
// mode X reads mode X's body rather than the default mode's.
|
|
574
|
+
return catalogDetail(this.ctx, registry, name, workdir, await this.catalogScope(presetId), await this.managedDefinition(name));
|
|
575
|
+
}
|
|
576
|
+
async readSkillFile(name, filePath, workdir, presetId) {
|
|
577
|
+
const { registry } = resolveServicesHelper(this.ctx);
|
|
578
|
+
if (registry === undefined)
|
|
579
|
+
return undefined;
|
|
580
|
+
return catalogReadSkillFile(registry, name, filePath, workdir, await this.catalogScope(presetId), await this.managedDefinition(name));
|
|
581
|
+
}
|
|
582
|
+
/** One managed skill's definition, for management reads outside its scope. */
|
|
583
|
+
async managedDefinition(name) {
|
|
584
|
+
const skill = (await scanManagedSkills(scopedSkillsRoot(this.dshHome()))).find(entry => entry.name === name);
|
|
585
|
+
return skill === undefined ? undefined : definitionFor(skill);
|
|
586
|
+
}
|
|
587
|
+
async listDirSkills(dirPath) {
|
|
588
|
+
return catalogListDirSkills(dirPath);
|
|
589
|
+
}
|
|
590
|
+
async setCredential(request) {
|
|
591
|
+
return catalogSetCredential(this.ctx, request);
|
|
592
|
+
}
|
|
593
|
+
async addSkill(request) {
|
|
594
|
+
return catalogAddSkill(this.ctx, request, this.dshHome());
|
|
595
|
+
}
|
|
596
|
+
async deleteSkill(name, workdir, presetId) {
|
|
597
|
+
const { registry } = resolveServicesHelper(this.ctx);
|
|
598
|
+
if (registry === undefined)
|
|
599
|
+
return { ok: false, error: 'skills service absent in this composition' };
|
|
600
|
+
return catalogDeleteSkill(registry, name, workdir, await this.catalogScope(presetId));
|
|
601
|
+
}
|
|
602
|
+
async pickDirectory() {
|
|
603
|
+
return catalogPickDirectory(this.ctx);
|
|
604
|
+
}
|
|
605
|
+
async mcpList() {
|
|
606
|
+
// Masked DTO: never surface secretRef markers (or the secret key names) to the browser.
|
|
607
|
+
return this.mcp.list().map(maskConfig);
|
|
608
|
+
}
|
|
609
|
+
async mcpAdd(config) {
|
|
610
|
+
if (config.serverName.trim() === '')
|
|
611
|
+
return false;
|
|
612
|
+
this.mcp.add(config);
|
|
613
|
+
this.afterMcpMutation();
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
async mcpRemove(serverName) {
|
|
617
|
+
this.mcp.remove(serverName);
|
|
618
|
+
this.afterMcpMutation();
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
async mcpSetEnabled(serverName, enabled) {
|
|
622
|
+
this.mcp.setEnabled(serverName, enabled);
|
|
623
|
+
this.afterMcpMutation();
|
|
624
|
+
}
|
|
625
|
+
async mcpSetCredential(ref, value) {
|
|
626
|
+
this.mcp.setCredential(ref, value);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
async mcpSetToolEnabled(serverName, tool, enabled) {
|
|
630
|
+
this.mcp.setToolEnabled(serverName, tool, enabled);
|
|
631
|
+
this.afterMcpMutation();
|
|
632
|
+
}
|
|
633
|
+
async mcpDiscover(serverName) {
|
|
634
|
+
const tools = await this.mcp.discover(serverName);
|
|
635
|
+
this.afterMcpMutation();
|
|
636
|
+
return tools;
|
|
637
|
+
}
|
|
638
|
+
async mcpSnapshot() {
|
|
639
|
+
return {
|
|
640
|
+
servers: this.mcp.list().map(maskConfig),
|
|
641
|
+
tools: this.mcp.toolsByServer(),
|
|
642
|
+
credentials: this.mcp.credentials(),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** The visible tool schemas in the standing scope (the set the model sees),
|
|
646
|
+
* or the global layer when no preset standing key resolves. Skills already
|
|
647
|
+
* enumerate through the same scope; tools must use it too so the
|
|
648
|
+
* settings reader sees the official/plugin/MCP tools the model actually has. */
|
|
649
|
+
toolSchemasIn(scope) {
|
|
650
|
+
const tools = this.ctx.get?.('tools');
|
|
651
|
+
if (tools?.schemas === undefined)
|
|
652
|
+
return [];
|
|
653
|
+
return scope === undefined ? tools.schemas() : tools.schemas(scope);
|
|
654
|
+
}
|
|
655
|
+
/** tool name → author-declared origin, read via `ctx.tools.get(name)` so the
|
|
656
|
+
* `Symbol.for('dsh.tool.origin')` tag on the retained definition survives (the
|
|
657
|
+
* system-prompt assembly strips it, but `get()` returns the full definition). */
|
|
658
|
+
toolOriginsMap(scope) {
|
|
659
|
+
const tools = this.ctx.get?.('tools');
|
|
660
|
+
if (tools?.get === undefined)
|
|
661
|
+
return new Map();
|
|
662
|
+
const schemas = scope === undefined ? tools.schemas() : tools.schemas(scope);
|
|
663
|
+
const out = new Map();
|
|
664
|
+
for (const schema of schemas) {
|
|
665
|
+
const def = scope === undefined ? tools.get(schema.name) : tools.get(schema.name, scope);
|
|
666
|
+
if (def === undefined) {
|
|
667
|
+
// `schemas()` and `get()` disagree for this scope — the author's origin
|
|
668
|
+
// tag (if any) cannot be read, so the tool falls back to heuristics.
|
|
669
|
+
this.ctx.logger.warn(`capability-catalog: tool "${schema.name}" is visible but ctx.tools.get() is undefined in this scope — its origin tag cannot be read; it will be classified by heuristics`);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const origin = toolOrigin(def);
|
|
673
|
+
if (origin !== undefined) {
|
|
674
|
+
if (!isValidOrigin(origin)) {
|
|
675
|
+
this.ctx.logger.warn(`capability-catalog: tool "${schema.name}" origin channel "${String(origin.channel)}" is not a known channel (plugin/builtin/mcp) — ignoring the tag, falling back to heuristics`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
out.set(schema.name, origin);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
/** MCP server names from live `mcp__`-prefixed tools (prefix-derived baseline). */
|
|
684
|
+
mcpServerNamesIn(scope) {
|
|
685
|
+
const names = [];
|
|
686
|
+
for (const schema of this.toolSchemasIn(scope)) {
|
|
687
|
+
if (!schema.name.startsWith(MCP_TOOL_PREFIX))
|
|
688
|
+
continue;
|
|
689
|
+
const rest = schema.name.slice(MCP_TOOL_PREFIX.length);
|
|
690
|
+
const sep = rest.indexOf('__');
|
|
691
|
+
const server = sep > 0 ? rest.slice(0, sep) : rest;
|
|
692
|
+
if (server.length > 0 && !names.includes(server))
|
|
693
|
+
names.push(server);
|
|
694
|
+
}
|
|
695
|
+
return names;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Reconcile which discovered MCP tools are registered on `ctx.tools` so the
|
|
699
|
+
* model can call them. `mcpToolsRegistry` is the traceable `ctx.tools` proxy,
|
|
700
|
+
* so `register` is always invoked as a MEMBER call (`reconcile` calls
|
|
701
|
+
* `registry.register(...)`) — extracting it to a standalone variable loses the
|
|
702
|
+
* `this.ctx` binding and breaks the disposal effect.
|
|
703
|
+
*/
|
|
704
|
+
syncRegisteredMcpTools() {
|
|
705
|
+
const registry = this.mcpToolsRegistry;
|
|
706
|
+
if (registry === undefined)
|
|
707
|
+
return;
|
|
708
|
+
const desired = desiredMcpTools(this.mcp.list(), this.mcp.toolsByServer());
|
|
709
|
+
reconcileRegisteredMcpTools(desired, {
|
|
710
|
+
register: (definition) => registry.register(definition),
|
|
711
|
+
onRegisterError: (error) => this.ctx.logger.error(`capability-catalog: MCP tool registration failed: ${String(error)}`),
|
|
712
|
+
}, this.mcpToolDisposers, (serverName, rawName, args) => this.mcp.callTool(serverName, rawName, args));
|
|
713
|
+
}
|
|
714
|
+
/** Re-sync registered MCP tools after any MCP store mutation. */
|
|
715
|
+
afterMcpMutation() {
|
|
716
|
+
this.syncRegisteredMcpTools();
|
|
717
|
+
}
|
|
718
|
+
/** Refill the persisted MCP tools' descriptions/parameters by re-connecting the
|
|
719
|
+
* enabled servers at boot (the persisted `mcp` block stores only name+enabled to
|
|
720
|
+
* keep settings.yaml small). Async: boot is not blocked; each server's errors are
|
|
721
|
+
* contained, and registration is re-synced once the descriptions are back. */
|
|
722
|
+
async reconnectEnabledMcp() {
|
|
723
|
+
const enabled = this.mcp.list().filter(s => s.enabled);
|
|
724
|
+
if (enabled.length === 0)
|
|
725
|
+
return;
|
|
726
|
+
for (const server of enabled) {
|
|
727
|
+
try {
|
|
728
|
+
await this.mcp.discover(server.serverName);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
this.ctx.logger.error(`capability-catalog: reconnect ${server.serverName} failed: ${String(error)}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
this.syncRegisteredMcpTools();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
})();
|
|
738
|
+
export { CapabilityCatalogService };
|
|
739
|
+
export default CapabilityCatalogService;
|