@kirkchinese/dsh-capability-catalog 0.1.2 → 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 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
- // MCP sources. A server switched off globally is listed but never started.
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 () => {
@@ -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
  /**
@@ -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.2' });
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
- await withTimeout(client.connect(transport), this.config.discoveryTimeoutMs, `${this.key} connect`);
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.2",
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",