@kirkchinese/dsh-capability-catalog 0.1.1 → 0.1.3
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/README.md +3 -2
- package/client/client.js +17 -3
- package/lib/catalog.d.ts +2 -0
- package/lib/catalog.js +3 -0
- package/lib/index.js +31 -12
- package/lib/sources/mcp.d.ts +5 -1
- package/lib/sources/mcp.js +17 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,8 +71,9 @@ Lazy capability catalog for DeepSeek Harness: one compact index plus three fixed
|
|
|
71
71
|
|
|
72
72
|
- 全局:「插件」页本插件(或整合包)详情中的设置卡片。
|
|
73
73
|
- 会话:输入框左侧「能力」按钮,每项「默认 / 开 / 关」。
|
|
74
|
-
- 生效顺序:会话 > 全局 > 配置(`mcp[].enabled`,skill
|
|
75
|
-
- 状态文件:`cacheDir/toggles.json`,最多保留最近 500
|
|
74
|
+
- 生效顺序:会话 > 全局 > 配置(`mcp[].enabled`,skill 默认开启)。全局关闭的服务器断开且不在启动时运行,也不进其他会话的索引;某会话单独设为「开」时,只为该会话按需发现和启动。会话关闭只对该会话隐藏。
|
|
75
|
+
- 状态文件:`cacheDir/toggles.json`,最多保留最近 500 个会话的覆盖。插件只在启动时读取它,请通过界面修改;直接编辑文件需重启 DSH。
|
|
76
|
+
- 界面在窗口重新获得焦点、弹窗每次打开时重新读取状态,因此其他浏览器或主机重启后的变化会被拾取。
|
|
76
77
|
|
|
77
78
|
## 行为细节
|
|
78
79
|
|
package/client/client.js
CHANGED
|
@@ -51,6 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
filterOff: '已关闭',
|
|
52
52
|
allOn: '全开',
|
|
53
53
|
allOff: '全关',
|
|
54
|
+
familyScope: '只作用于当前列表中显示的条目',
|
|
54
55
|
disabled: '能力目录已关闭(配置 enabled: false):不注册工具、不进索引,MCP 服务器不运行。',
|
|
55
56
|
globalNote: '这里的开关作用于所有会话,立即生效。单个会话可以在输入框左侧的「能力」按钮中覆盖。关闭的服务器会停止运行。',
|
|
56
57
|
noTakeover: '未接管 skill 目录(skills.takeOverCatalog 为 false):skill 开关不会影响 DSH 原生的 skill 目录。',
|
|
@@ -84,6 +85,7 @@ window.__ModuleLoader__.load({
|
|
|
84
85
|
filterOff: 'Off',
|
|
85
86
|
allOn: 'All on',
|
|
86
87
|
allOff: 'All off',
|
|
88
|
+
familyScope: 'Applies to the rows currently shown',
|
|
87
89
|
disabled: 'The capability catalog is switched off (enabled: false): no tools, no index, no MCP servers running.',
|
|
88
90
|
globalNote: 'These switches apply to every session immediately. A session can override them from the Capabilities button left of the input box. A server switched off stops running.',
|
|
89
91
|
noTakeover: 'The skill catalog is not taken over (skills.takeOverCatalog is false): skill switches do not affect the native skill catalog.',
|
|
@@ -199,6 +201,17 @@ window.__ModuleLoader__.load({
|
|
|
199
201
|
}, [api, sessionId])
|
|
200
202
|
useEffect(() => (active ? load() : undefined), [load, active])
|
|
201
203
|
useEffect(() => api.onChange(() => { if (active) load() }), [api, load, active])
|
|
204
|
+
// Other browsers and host restarts change the state behind this view; refresh on return.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!active) return undefined
|
|
207
|
+
const onVisible = () => { if (document.visibilityState === 'visible') load() }
|
|
208
|
+
window.addEventListener('focus', load)
|
|
209
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
210
|
+
return () => {
|
|
211
|
+
window.removeEventListener('focus', load)
|
|
212
|
+
document.removeEventListener('visibilitychange', onVisible)
|
|
213
|
+
}
|
|
214
|
+
}, [load, active])
|
|
202
215
|
const set = useCallback(async (request) => {
|
|
203
216
|
const data = await api.request({ action: 'set', ...request, ...(sessionId ? { sessionId } : {}) })
|
|
204
217
|
generation.current++
|
|
@@ -307,8 +320,8 @@ window.__ModuleLoader__.load({
|
|
|
307
320
|
h('div', { className: 'cc-family' },
|
|
308
321
|
h('span', null, `${family} (${skills.length})`),
|
|
309
322
|
h('span', { className: 'cc-toolbar' },
|
|
310
|
-
h('button', { type: 'button', className: 'cc-btn', disabled: busy, onClick: () => run(skills.map((skill) => ({ kind: 'skill', name: skill.name, value: null }))) }, t.allOn),
|
|
311
|
-
h('button', { type: 'button', className: 'cc-btn', disabled: busy, onClick: () => run(skills.map((skill) => ({ kind: 'skill', name: skill.name, value: false }))) }, t.allOff))),
|
|
323
|
+
h('button', { type: 'button', className: 'cc-btn', disabled: busy, title: t.familyScope, onClick: () => run(skills.map((skill) => ({ kind: 'skill', name: skill.name, value: null }))) }, t.allOn),
|
|
324
|
+
h('button', { type: 'button', className: 'cc-btn', disabled: busy, title: t.familyScope, onClick: () => run(skills.map((skill) => ({ kind: 'skill', name: skill.name, value: false }))) }, t.allOff))),
|
|
312
325
|
skills.map((skill) => h('div', { className: 'cc-row', key: skill.name },
|
|
313
326
|
h('div', { className: 'cc-row-main' },
|
|
314
327
|
h('div', { className: 'cc-row-name' }, skill.name),
|
|
@@ -325,9 +338,10 @@ window.__ModuleLoader__.load({
|
|
|
325
338
|
const [open, setOpen] = useState(false)
|
|
326
339
|
const [query, setQuery] = useState('')
|
|
327
340
|
const anchor = useRef(null)
|
|
328
|
-
const { data, error, set } = useOverview(api, sessionId, true)
|
|
341
|
+
const { data, error, set, reload } = useOverview(api, sessionId, true)
|
|
329
342
|
const [actionError, setActionError] = useState(null)
|
|
330
343
|
|
|
344
|
+
useEffect(() => { if (open) reload() }, [open, reload])
|
|
331
345
|
useEffect(() => {
|
|
332
346
|
if (!open) return undefined
|
|
333
347
|
const onKey = (event) => { if (event.key === 'Escape') setOpen(false) }
|
package/lib/catalog.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare class Catalog {
|
|
|
30
30
|
private cache;
|
|
31
31
|
/** Decides whether an entry is enabled for a viewer; everything is enabled by default. */
|
|
32
32
|
isEnabled: (entry: CapabilityEntry, view: ViewContext) => boolean;
|
|
33
|
+
/** Decides whether a whole source is switched on for a viewer; a switched-off source leaves the index. */
|
|
34
|
+
isSourceEnabled: (source: CapabilitySource, view: ViewContext) => boolean;
|
|
33
35
|
/**
|
|
34
36
|
* Add a source; returns its disposer.
|
|
35
37
|
* @param source - capability source with a unique key.
|
package/lib/catalog.js
CHANGED
|
@@ -43,6 +43,8 @@ export class Catalog {
|
|
|
43
43
|
cache;
|
|
44
44
|
/** Decides whether an entry is enabled for a viewer; everything is enabled by default. */
|
|
45
45
|
isEnabled = () => true;
|
|
46
|
+
/** Decides whether a whole source is switched on for a viewer; a switched-off source leaves the index. */
|
|
47
|
+
isSourceEnabled = () => true;
|
|
46
48
|
/**
|
|
47
49
|
* Add a source; returns its disposer.
|
|
48
50
|
* @param source - capability source with a unique key.
|
|
@@ -110,6 +112,7 @@ export class Catalog {
|
|
|
110
112
|
/** Index blocks of every source, in registration order. */
|
|
111
113
|
indexes(view) {
|
|
112
114
|
return this.list()
|
|
115
|
+
.filter((source) => this.isSourceEnabled(source, view))
|
|
113
116
|
.map((source) => source.index(view, source.entries(view).filter((entry) => this.isEnabled(entry, view))))
|
|
114
117
|
.filter((index) => index !== undefined);
|
|
115
118
|
}
|
package/lib/index.js
CHANGED
|
@@ -137,7 +137,12 @@ export async function apply(rawCtx, config) {
|
|
|
137
137
|
catalog.isEnabled = (entry, view) => entry.kind === 'skill'
|
|
138
138
|
? toggles.enabled('skill', entry.name, view.sessionId)
|
|
139
139
|
: toggles.enabled('server', entry.source.replace(/^mcp:/u, ''), view.sessionId, serverOf(entry.source)?.configuredEnabled ?? true);
|
|
140
|
-
|
|
140
|
+
catalog.isSourceEnabled = (source, view) => {
|
|
141
|
+
const server = serverOf(source.key);
|
|
142
|
+
return server === undefined || toggles.enabled('server', server.name, view.sessionId, server.configuredEnabled);
|
|
143
|
+
};
|
|
144
|
+
// MCP sources. A server switched off globally does not start at launch; a session that
|
|
145
|
+
// switches it on discovers and runs it on demand for that session only.
|
|
141
146
|
const startIfEnabled = async (source) => {
|
|
142
147
|
if (toggles.globallyEnabled(source.name, source.configuredEnabled))
|
|
143
148
|
await source.start();
|
|
@@ -148,6 +153,7 @@ export async function apply(rawCtx, config) {
|
|
|
148
153
|
if (servers.has(server.name))
|
|
149
154
|
throw new Error(`capability-catalog: MCP server "${server.name}" is configured twice`);
|
|
150
155
|
const source = new McpSource(server, cacheDir, logger);
|
|
156
|
+
source.isWanted = (view) => toggles.enabled('server', server.name, view.sessionId, server.enabled);
|
|
151
157
|
const remove = catalog.add(source);
|
|
152
158
|
servers.set(server.name, source);
|
|
153
159
|
const dispose = async () => {
|
|
@@ -225,19 +231,32 @@ export async function apply(rawCtx, config) {
|
|
|
225
231
|
dispose();
|
|
226
232
|
owned.clear();
|
|
227
233
|
}, 'capability-catalog.skill-shadows');
|
|
228
|
-
|
|
234
|
+
const shadow = (agent) => {
|
|
229
235
|
const agentCtx = agent.ctx;
|
|
230
|
-
if (agentCtx === undefined)
|
|
236
|
+
if (agentCtx === undefined || owned.has(agent.id))
|
|
231
237
|
return;
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
try {
|
|
239
|
+
const dispose = agentCtx.effect(() => {
|
|
240
|
+
const unregister = agentCtx.tools.register(tools.skill);
|
|
241
|
+
return () => {
|
|
242
|
+
unregister();
|
|
243
|
+
owned.delete(agent.id);
|
|
244
|
+
};
|
|
245
|
+
}, 'capability-catalog.skill');
|
|
246
|
+
owned.set(agent.id, dispose);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
logger.warn(`capability-catalog: could not shadow the skill tool for ${agent.id}`, error);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
ctx.on('agent/created', ({ agent }) => shadow(agent));
|
|
253
|
+
// A reload (plugin update, HMR) finds agents that are already live; shadow them too, or
|
|
254
|
+
// the native catalog would come back for every open session.
|
|
255
|
+
const liveAgents = ctx.get('agents');
|
|
256
|
+
for (const agent of liveAgents?.list() ?? []) {
|
|
257
|
+
shadow(agent);
|
|
258
|
+
void viewFor(agent, AbortSignal.timeout(30_000)).then((view) => catalog.refresh(view, AbortSignal.timeout(30_000))).catch(() => undefined);
|
|
259
|
+
}
|
|
241
260
|
}
|
|
242
261
|
// Settings page and per-session controls.
|
|
243
262
|
const agents = ctx.get('agents');
|
package/lib/sources/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FamilyRule } from '../families.js';
|
|
2
|
-
import type { CallOutcome, CapabilityEntry, CapabilitySource, SourceIndex } from '../types.js';
|
|
2
|
+
import type { CallOutcome, CapabilityEntry, CapabilitySource, SourceIndex, ViewContext } from '../types.js';
|
|
3
3
|
/** Resolved configuration of one MCP server. */
|
|
4
4
|
export interface McpServerConfig {
|
|
5
5
|
readonly name: string;
|
|
@@ -49,6 +49,10 @@ export declare class McpSource implements CapabilitySource {
|
|
|
49
49
|
private started;
|
|
50
50
|
/** Whether discovery has been allowed to run (the server is not switched off). */
|
|
51
51
|
get discovered(): boolean;
|
|
52
|
+
/** Decides whether a viewer wants this server; set by the plugin from the switches. */
|
|
53
|
+
isWanted: (view: ViewContext) => boolean;
|
|
54
|
+
/** Discover lazily for a viewer that switched on a server that did not start globally. */
|
|
55
|
+
refresh(view: ViewContext): Promise<void>;
|
|
52
56
|
constructor(config: McpServerConfig, cacheDir: string, logger: Logger);
|
|
53
57
|
private get cachePath();
|
|
54
58
|
/**
|
package/lib/sources/mcp.js
CHANGED
|
@@ -74,6 +74,13 @@ export class McpSource {
|
|
|
74
74
|
get discovered() {
|
|
75
75
|
return this.started;
|
|
76
76
|
}
|
|
77
|
+
/** Decides whether a viewer wants this server; set by the plugin from the switches. */
|
|
78
|
+
isWanted = () => true;
|
|
79
|
+
/** Discover lazily for a viewer that switched on a server that did not start globally. */
|
|
80
|
+
async refresh(view) {
|
|
81
|
+
if (!this.started && !this.disposed && this.isWanted(view))
|
|
82
|
+
await this.start();
|
|
83
|
+
}
|
|
77
84
|
constructor(config, cacheDir, logger) {
|
|
78
85
|
this.config = config;
|
|
79
86
|
this.cacheDir = cacheDir;
|
|
@@ -255,12 +262,20 @@ export class McpSource {
|
|
|
255
262
|
throw new Error(`${this.key}: streamable-http transport requires "url"`);
|
|
256
263
|
transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: { ...config.headers } } });
|
|
257
264
|
}
|
|
258
|
-
const client = new Client({ name: 'dsh-capability-catalog', version: '0.1.
|
|
265
|
+
const client = new Client({ name: 'dsh-capability-catalog', version: '0.1.3' });
|
|
259
266
|
client.onclose = () => {
|
|
260
267
|
if (this.client === client)
|
|
261
268
|
this.client = undefined;
|
|
262
269
|
};
|
|
263
|
-
|
|
270
|
+
try {
|
|
271
|
+
await withTimeout(client.connect(transport), this.config.discoveryTimeoutMs, `${this.key} connect`);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
// A timed-out or failed handshake must not leave the child process behind.
|
|
275
|
+
await client.close().catch(() => undefined);
|
|
276
|
+
await transport.close().catch(() => undefined);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
264
279
|
if (this.disposed) {
|
|
265
280
|
await client.close().catch(() => undefined);
|
|
266
281
|
throw new Error(`${this.key} was unloaded while connecting`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirkchinese/dsh-capability-catalog",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Lazy capability catalog for DeepSeek Harness: one compact index plus search/describe/call meta-tools for MCP tools and skills, instead of every schema on every request",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|