@kirkchinese/dsh-capability-catalog 0.1.2 → 0.1.4
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 +53 -5
- package/lib/catalog.d.ts +2 -0
- package/lib/catalog.js +3 -0
- package/lib/index.js +7 -1
- package/lib/schema-view.d.ts +1 -1
- package/lib/schema-view.js +19 -3
- 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.',
|
|
@@ -150,7 +152,7 @@ window.__ModuleLoader__.load({
|
|
|
150
152
|
.cc-trigger:hover,.cc-trigger[aria-expanded=true]{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
151
153
|
.cc-trigger-badge{color:var(--dsw-alias-state-warn-primary)}
|
|
152
154
|
.cc-anchor{position:relative;display:inline-flex}
|
|
153
|
-
.cc-popover{position:
|
|
155
|
+
.cc-popover{position:fixed;z-index:1000;box-sizing:border-box;width:380px;max-width:calc(100vw - 32px);display:flex;flex-direction:column;gap:10px;padding:14px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-overlay);box-shadow:0 8px 28px rgba(0,0,0,.18);color:var(--dsw-alias-label-primary);font-size:13px}
|
|
154
156
|
.cc-popover-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-weight:600}
|
|
155
157
|
.cc-popover-body{overflow:auto;display:flex;flex-direction:column;gap:12px;margin:0 -4px;padding:0 4px}
|
|
156
158
|
.cc-sub{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:4px}
|
|
@@ -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),
|
|
@@ -324,10 +337,45 @@ window.__ModuleLoader__.load({
|
|
|
324
337
|
function SessionControl({ api, t, sessionId }) {
|
|
325
338
|
const [open, setOpen] = useState(false)
|
|
326
339
|
const [query, setQuery] = useState('')
|
|
340
|
+
const [placement, setPlacement] = useState(null)
|
|
327
341
|
const anchor = useRef(null)
|
|
328
|
-
|
|
342
|
+
// Open toward the side with more room and stay inside the window. The popover is fixed to
|
|
343
|
+
// the viewport so scroll and clip containers of the composer cannot cut it off; the
|
|
344
|
+
// composer sits at the bottom in a session but in the middle of the window on a new one.
|
|
345
|
+
const place = useCallback(() => {
|
|
346
|
+
const node = anchor.current
|
|
347
|
+
if (!node) return
|
|
348
|
+
const rect = node.getBoundingClientRect()
|
|
349
|
+
const margin = 12
|
|
350
|
+
const gap = 8
|
|
351
|
+
const above = rect.top - margin - gap
|
|
352
|
+
const below = window.innerHeight - rect.bottom - margin - gap
|
|
353
|
+
const up = above >= 320 || above >= below
|
|
354
|
+
const room = Math.max(160, Math.min(560, up ? above : below))
|
|
355
|
+
const width = Math.min(380, window.innerWidth - 2 * margin)
|
|
356
|
+
const left = Math.max(margin, Math.min(rect.left, window.innerWidth - width - margin))
|
|
357
|
+
setPlacement({
|
|
358
|
+
position: 'fixed',
|
|
359
|
+
left: `${left}px`,
|
|
360
|
+
width: `${width}px`,
|
|
361
|
+
...(up ? { bottom: `${window.innerHeight - rect.top + gap}px` } : { top: `${rect.bottom + gap}px` }),
|
|
362
|
+
maxHeight: `${room}px`,
|
|
363
|
+
})
|
|
364
|
+
}, [])
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (!open) return undefined
|
|
367
|
+
place()
|
|
368
|
+
window.addEventListener('resize', place)
|
|
369
|
+
window.addEventListener('scroll', place, true)
|
|
370
|
+
return () => {
|
|
371
|
+
window.removeEventListener('resize', place)
|
|
372
|
+
window.removeEventListener('scroll', place, true)
|
|
373
|
+
}
|
|
374
|
+
}, [open, place])
|
|
375
|
+
const { data, error, set, reload } = useOverview(api, sessionId, true)
|
|
329
376
|
const [actionError, setActionError] = useState(null)
|
|
330
377
|
|
|
378
|
+
useEffect(() => { if (open) reload() }, [open, reload])
|
|
331
379
|
useEffect(() => {
|
|
332
380
|
if (!open) return undefined
|
|
333
381
|
const onKey = (event) => { if (event.key === 'Escape') setOpen(false) }
|
|
@@ -368,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
368
416
|
h('rect', { x: 2, y: 9, width: 5, height: 5, rx: 1 }), h('rect', { x: 9, y: 9, width: 5, height: 5, rx: 1 })),
|
|
369
417
|
t.button,
|
|
370
418
|
offCount > 0 ? h('span', { className: 'cc-trigger-badge' }, `−${offCount}`) : null),
|
|
371
|
-
open ? h('div', { className: 'cc-popover', role: 'dialog', 'aria-label': t.sessionTitle },
|
|
419
|
+
open ? h('div', { className: 'cc-popover', role: 'dialog', 'aria-label': t.sessionTitle, style: placement || { visibility: 'hidden' } },
|
|
372
420
|
h('div', { className: 'cc-popover-head' },
|
|
373
421
|
h('span', null, t.sessionTitle),
|
|
374
422
|
h('span', { className: 'cc-toolbar' },
|
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 () => {
|
package/lib/schema-view.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface CompactOptions {
|
|
|
23
23
|
* @param pointer - pointer such as `/properties/filters`; empty means the root.
|
|
24
24
|
* @returns the subtree, or undefined when the pointer does not resolve.
|
|
25
25
|
*/
|
|
26
|
-
export declare function resolvePointer(schema: unknown,
|
|
26
|
+
export declare function resolvePointer(schema: unknown, rawPointer: string): unknown;
|
|
27
27
|
/**
|
|
28
28
|
* Derive a compact, TypeScript-like signature from an object schema.
|
|
29
29
|
* @param name - tool name used as the signature head.
|
package/lib/schema-view.js
CHANGED
|
@@ -5,7 +5,9 @@ const DEFAULT_OPTIONS = { maxDepth: 2, maxDescription: 160, maxEnum: 12 };
|
|
|
5
5
|
* @param pointer - pointer such as `/properties/filters`; empty means the root.
|
|
6
6
|
* @returns the subtree, or undefined when the pointer does not resolve.
|
|
7
7
|
*/
|
|
8
|
-
export function resolvePointer(schema,
|
|
8
|
+
export function resolvePointer(schema, rawPointer) {
|
|
9
|
+
// Accept a pointer copied with a trailing array marker, as earlier signatures rendered it.
|
|
10
|
+
const pointer = rawPointer.trim().replace(/(\[\])+$/u, '');
|
|
9
11
|
if (pointer === '' || pointer === '/')
|
|
10
12
|
return schema;
|
|
11
13
|
if (!pointer.startsWith('/'))
|
|
@@ -96,7 +98,20 @@ function typeOf(schema, pointer, depth, options, lines, indent) {
|
|
|
96
98
|
const type = Array.isArray(schema.type) ? schema.type.join(' | ') : schema.type;
|
|
97
99
|
if (type === 'array') {
|
|
98
100
|
const items = asSchema(schema.items);
|
|
99
|
-
|
|
101
|
+
if (items === undefined)
|
|
102
|
+
return 'unknown[]';
|
|
103
|
+
const inner = typeOf(items, `${pointer}/items`, depth, options, lines, indent);
|
|
104
|
+
// Keep exactly one pointer, last and clean: `{…3 fields}[] → /a/items`, never `→ /a/items[]`.
|
|
105
|
+
const arrows = inner.split(' → ').length - 1;
|
|
106
|
+
if (arrows === 0)
|
|
107
|
+
return `${wrap(inner)}[]`;
|
|
108
|
+
if (arrows === 1 && !inner.includes(' | ')) {
|
|
109
|
+
const arrow = inner.indexOf(' → ');
|
|
110
|
+
return `${inner.slice(0, arrow)}[]${inner.slice(arrow)}`;
|
|
111
|
+
}
|
|
112
|
+
// A union with collapsed branches: point at the item schema as a whole.
|
|
113
|
+
const branches = Array.isArray(items.anyOf) ? items.anyOf.length : Array.isArray(items.oneOf) ? items.oneOf.length : 0;
|
|
114
|
+
return `${branches > 0 ? `union<${branches}>` : 'item'}[] → ${pointer}/items`;
|
|
100
115
|
}
|
|
101
116
|
if (type === 'object' || schema.properties !== undefined) {
|
|
102
117
|
const properties = asSchema(schema.properties);
|
|
@@ -132,7 +147,8 @@ function fields(schema, pointer, depth, options, lines, indent) {
|
|
|
132
147
|
lines.push(`${indent}}`);
|
|
133
148
|
continue;
|
|
134
149
|
}
|
|
135
|
-
|
|
150
|
+
const arrow = type.lastIndexOf(' → ');
|
|
151
|
+
type = arrow >= 0 ? `${type.slice(0, arrow)}${constraints(node)}${type.slice(arrow)}` : `${type}${constraints(node)}`;
|
|
136
152
|
lines.push(`${indent}${key}${optional}: ${type}${defaultText}${description}`);
|
|
137
153
|
lines.push(...nested);
|
|
138
154
|
}
|
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.4' });
|
|
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.4",
|
|
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",
|