@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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +286 -0
  4. package/README.zh.md +138 -0
  5. package/cordis.patch.yml +8 -0
  6. package/lib/client.js +10100 -0
  7. package/lib/index.js +4022 -0
  8. package/lib/invariant.js +25 -0
  9. package/lib/mcpConnector-C2TbkPBY.js +69 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +1417 -0
  12. package/lib/typert.remote-client.d.ts +68 -0
  13. package/lib/typert.remote-client.js +1136 -0
  14. package/lib/types/capabilities.d.ts +103 -0
  15. package/lib/types/capabilities.js +116 -0
  16. package/lib/types/channels.d.ts +41 -0
  17. package/lib/types/channels.js +75 -0
  18. package/lib/types/client/AddMcpDialog.d.ts +13 -0
  19. package/lib/types/client/AddMcpDialog.js +79 -0
  20. package/lib/types/client/AddSkillModal.d.ts +17 -0
  21. package/lib/types/client/AddSkillModal.js +138 -0
  22. package/lib/types/client/BundleFileTree.d.ts +7 -0
  23. package/lib/types/client/BundleFileTree.js +74 -0
  24. package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
  25. package/lib/types/client/CapabilityCatalogCard.js +479 -0
  26. package/lib/types/client/CredentialField.d.ts +14 -0
  27. package/lib/types/client/CredentialField.js +9 -0
  28. package/lib/types/client/McpCards.d.ts +13 -0
  29. package/lib/types/client/McpCards.js +12 -0
  30. package/lib/types/client/McpServerManageModal.d.ts +18 -0
  31. package/lib/types/client/McpServerManageModal.js +63 -0
  32. package/lib/types/client/MetadataRow.d.ts +10 -0
  33. package/lib/types/client/MetadataRow.js +6 -0
  34. package/lib/types/client/ModalShell.d.ts +16 -0
  35. package/lib/types/client/ModalShell.js +27 -0
  36. package/lib/types/client/ModeChips.d.ts +38 -0
  37. package/lib/types/client/ModeChips.js +42 -0
  38. package/lib/types/client/SchemaView.d.ts +11 -0
  39. package/lib/types/client/SchemaView.js +143 -0
  40. package/lib/types/client/SkillCards.d.ts +28 -0
  41. package/lib/types/client/SkillCards.js +43 -0
  42. package/lib/types/client/SkillDetailModal.d.ts +69 -0
  43. package/lib/types/client/SkillDetailModal.js +167 -0
  44. package/lib/types/client/ToolCards.d.ts +28 -0
  45. package/lib/types/client/ToolCards.js +52 -0
  46. package/lib/types/client/ToolDetailModal.d.ts +12 -0
  47. package/lib/types/client/ToolDetailModal.js +29 -0
  48. package/lib/types/client/icons.d.ts +26 -0
  49. package/lib/types/client/icons.js +10 -0
  50. package/lib/types/client/index.d.ts +21 -0
  51. package/lib/types/client/index.js +196 -0
  52. package/lib/types/client/locales.d.ts +392 -0
  53. package/lib/types/client/locales.js +395 -0
  54. package/lib/types/client/mcp-model.d.ts +24 -0
  55. package/lib/types/client/mcp-model.js +87 -0
  56. package/lib/types/client/mode-model.d.ts +108 -0
  57. package/lib/types/client/mode-model.js +140 -0
  58. package/lib/types/client/preset-display.d.ts +21 -0
  59. package/lib/types/client/preset-display.js +20 -0
  60. package/lib/types/client/settle.d.ts +40 -0
  61. package/lib/types/client/settle.js +57 -0
  62. package/lib/types/client/slots.d.ts +86 -0
  63. package/lib/types/client/slots.js +1 -0
  64. package/lib/types/envHint.d.ts +22 -0
  65. package/lib/types/envHint.js +76 -0
  66. package/lib/types/import.d.ts +106 -0
  67. package/lib/types/import.js +635 -0
  68. package/lib/types/index.d.ts +216 -0
  69. package/lib/types/index.js +739 -0
  70. package/lib/types/invariant.d.ts +16 -0
  71. package/lib/types/invariant.js +23 -0
  72. package/lib/types/mcpConnector.d.ts +24 -0
  73. package/lib/types/mcpConnector.js +73 -0
  74. package/lib/types/mcpStore.d.ts +68 -0
  75. package/lib/types/mcpStore.js +235 -0
  76. package/lib/types/mcpTools.d.ts +45 -0
  77. package/lib/types/mcpTools.js +125 -0
  78. package/lib/types/mcps.d.ts +64 -0
  79. package/lib/types/mcps.js +157 -0
  80. package/lib/types/modes.d.ts +65 -0
  81. package/lib/types/modes.js +97 -0
  82. package/lib/types/namespace.d.ts +9 -0
  83. package/lib/types/namespace.js +8 -0
  84. package/lib/types/official-tools.d.ts +3 -0
  85. package/lib/types/official-tools.js +44 -0
  86. package/lib/types/preset-scope.d.ts +105 -0
  87. package/lib/types/preset-scope.js +117 -0
  88. package/lib/types/remote.d.ts +48 -0
  89. package/lib/types/remote.js +120 -0
  90. package/lib/types/scoped-delivery.d.ts +243 -0
  91. package/lib/types/scoped-delivery.js +686 -0
  92. package/lib/types/scoped-edits.d.ts +90 -0
  93. package/lib/types/scoped-edits.js +234 -0
  94. package/lib/types/settings.d.ts +28 -0
  95. package/lib/types/settings.js +28 -0
  96. package/lib/types/shellEnv.d.ts +65 -0
  97. package/lib/types/shellEnv.js +232 -0
  98. package/lib/types/skills.d.ts +103 -0
  99. package/lib/types/skills.js +303 -0
  100. package/lib/types/tool-origin.d.ts +29 -0
  101. package/lib/types/tool-origin.js +30 -0
  102. package/lib/types/types.d.ts +315 -0
  103. package/lib/types/types.js +8 -0
  104. package/lib/types/zip.d.ts +24 -0
  105. package/lib/types/zip.js +80 -0
  106. package/locale/en.json +6 -0
  107. package/locale/zh.json +6 -0
  108. package/package.json +119 -0
  109. 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;