@kirkchinese/dsh-capability-catalog 0.0.0-stage → 0.1.1
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.md +88 -2
- package/client/client.js +435 -0
- package/cordis.patch.yml +6 -0
- package/lib/catalog.d.ts +76 -0
- package/lib/catalog.js +167 -0
- package/lib/families.d.ts +34 -0
- package/lib/families.js +97 -0
- package/lib/index.d.ts +46 -0
- package/lib/index.js +283 -0
- package/lib/remote.d.ts +144 -0
- package/lib/remote.js +127 -0
- package/lib/schema-view.d.ts +37 -0
- package/lib/schema-view.js +178 -0
- package/lib/search.d.ts +34 -0
- package/lib/search.js +110 -0
- package/lib/server.d.ts +10 -0
- package/lib/server.js +24 -0
- package/lib/sources/mcp.d.ts +90 -0
- package/lib/sources/mcp.js +327 -0
- package/lib/sources/skills.d.ts +32 -0
- package/lib/sources/skills.js +120 -0
- package/lib/toggles.d.ts +43 -0
- package/lib/toggles.js +134 -0
- package/lib/tools.d.ts +33 -0
- package/lib/tools.js +320 -0
- package/lib/typert.host.d.ts +129 -0
- package/lib/typert.host.js +20 -0
- package/lib/types.d.ts +186 -0
- package/lib/types.js +9 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +94 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kirkchinese
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kirkchinese/dsh-capability-catalog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness 的统一能力目录:一份紧凑索引加三个固定工具,按需提供 MCP 工具与 skill。
|
|
4
|
+
|
|
5
|
+
Lazy capability catalog for DeepSeek Harness: one compact index plus three fixed tools serve MCP tools and skills on demand, instead of every schema on every request.
|
|
6
|
+
|
|
7
|
+
## 模型看到什么
|
|
8
|
+
|
|
9
|
+
- 系统提示词中的 `## Capability catalog` 段:每个来源的能力族、数量和代表名称;skill 列出名称与短描述。
|
|
10
|
+
- 固定工具:
|
|
11
|
+
|
|
12
|
+
| 工具 | 作用 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `capability_search` | 关键词检索(BM25,拆分 snake_case),或用 `family` 浏览一族;可按 `source`、`kind` 过滤 |
|
|
15
|
+
| `capability_describe` | 完整说明与参数。schema 不超过 `schemaInlineMaxBytes` 时原样返回,否则返回紧凑签名,折叠处标出 JSON Pointer,用 `path` 展开 |
|
|
16
|
+
| `capability_call` | 用原始 schema 校验参数(错误带字段路径),再调用 MCP |
|
|
17
|
+
| `skill`(接管时) | 按名称加载 skill,输出与原生 `skill` 工具一致 |
|
|
18
|
+
|
|
19
|
+
工具列表在会话中不变;索引只随目录内容变化,同一配置下逐字节稳定。
|
|
20
|
+
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
- id: capability-catalog
|
|
25
|
+
config:
|
|
26
|
+
enabled: true # 总开关
|
|
27
|
+
mcp:
|
|
28
|
+
- name: rea # 条目 id 形如 mcp:rea/<tool>
|
|
29
|
+
enabled: true
|
|
30
|
+
title: REA reverse engineering
|
|
31
|
+
description: 索引中显示的一两句话;默认取服务器 instructions
|
|
32
|
+
transport: stdio # 或 streamable-http(配 url、headers)
|
|
33
|
+
command: rea
|
|
34
|
+
args: [mcp]
|
|
35
|
+
env: {} # 叠加在已清洗凭据的父进程环境之上
|
|
36
|
+
allowTools: [] # 通配符白名单,空表示全部
|
|
37
|
+
denyTools: [] # 通配符黑名单,优先于白名单
|
|
38
|
+
families: # 人工能力族;未匹配的按名称自动分组,服务器在 _meta 中声明的 family 也会被采用
|
|
39
|
+
- name: javascript
|
|
40
|
+
description: JS/Electron 应用
|
|
41
|
+
match: ['*javascript*', '*electron*']
|
|
42
|
+
idleDisconnectMs: 300000 # 空闲断开
|
|
43
|
+
callTimeoutMs: 600000
|
|
44
|
+
discoveryTimeoutMs: 60000
|
|
45
|
+
catalogTtlMs: 86400000 # 缓存超过此时长后后台刷新
|
|
46
|
+
skills:
|
|
47
|
+
enabled: true
|
|
48
|
+
takeOverCatalog: true # 用索引替换原生 skill 目录
|
|
49
|
+
indexMode: brief # brief:名称 + 短描述;names:只列名称
|
|
50
|
+
briefChars: 80
|
|
51
|
+
families: []
|
|
52
|
+
allow: []
|
|
53
|
+
deny: []
|
|
54
|
+
toolNames: { search: capability_search, describe: capability_describe, call: capability_call }
|
|
55
|
+
limits: { searchLimit: 8, maxSearchLimit: 60, schemaInlineMaxBytes: 6000, describeMaxChars: 14000, callResultMaxChars: 120000 }
|
|
56
|
+
cacheDir: '' # 默认 $DSH_HOME/capability-catalog
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
覆盖这一行时会替换整个 `config`,请写全自己要保留的字段。
|
|
60
|
+
|
|
61
|
+
其他包添加服务器而不覆盖主配置:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- insert:
|
|
65
|
+
- id: capability-catalog-github
|
|
66
|
+
name: '@kirkchinese/dsh-capability-catalog/server'
|
|
67
|
+
config: { name: github, command: npx, args: ['-y', '@modelcontextprotocol/server-github'] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 开关与设置界面
|
|
71
|
+
|
|
72
|
+
- 全局:「插件」页本插件(或整合包)详情中的设置卡片。
|
|
73
|
+
- 会话:输入框左侧「能力」按钮,每项「默认 / 开 / 关」。
|
|
74
|
+
- 生效顺序:会话 > 全局 > 配置(`mcp[].enabled`,skill 默认开启)。全局关闭的服务器不运行;会话关闭只对该会话隐藏。
|
|
75
|
+
- 状态文件:`cacheDir/toggles.json`,最多保留最近 500 个会话的覆盖。
|
|
76
|
+
|
|
77
|
+
## 行为细节
|
|
78
|
+
|
|
79
|
+
- **缓存:** 工具清单写入 `cacheDir/mcp-<name>.json`,键为命令、参数、环境、URL 的哈希。有缓存时启动不连接服务器;启动时的首次发现结束后立即断开。
|
|
80
|
+
- **连接:** 检索与说明只读缓存,只有 `capability_call` 会连接服务器;空闲 `idleDisconnectMs` 后断开;插件卸载或关闭时立即断开。
|
|
81
|
+
- **skill 接管:** 为每个 agent 注册同名的作用域 `skill` 工具,原生目录因此停止发送;`/name` 手势由原生插件继续处理。
|
|
82
|
+
- **安全:** 子进程环境沿用 DSH 的凭据清洗;本插件不提升任何权限。`capability_call` 是普通工具,照常经过 DSH 的审批流程,但审批粒度是这一个工具名,而不是每个 MCP 工具。
|
|
83
|
+
|
|
84
|
+
## 已知限制
|
|
85
|
+
|
|
86
|
+
- 中文查询与英文工具描述之间没有语义匹配;索引用英文族名,模型通常用英文检索。
|
|
87
|
+
- 多一步检索与说明;对每轮都要用到的少数工具,直接注册更简单。
|
|
88
|
+
- MCP 结果中的图片、音频以文字占位,不进入模型上下文。
|
|
89
|
+
- 原生延迟加载(`defer_loading` + `tool_reference`)需要 DSH DeepSeek 适配器支持,见仓库 `docs/upstream/`。
|
package/client/client.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// Browser half of @kirkchinese/dsh-capability-catalog.
|
|
2
|
+
// Plain JavaScript loaded by the Harness client module loader: React and Cordis come from
|
|
3
|
+
// the module table, styles use only --dsw-alias-* theme tokens, and every Host call goes
|
|
4
|
+
// through the authenticated Typert Remote channel (`ctx.remote.capabilityCatalog`).
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: '@kirkchinese/dsh-capability-catalog',
|
|
7
|
+
factory(require) {
|
|
8
|
+
const React = require('react')
|
|
9
|
+
const h = React.createElement
|
|
10
|
+
const { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } = React
|
|
11
|
+
|
|
12
|
+
const PACKAGE = '@kirkchinese/dsh-capability-catalog'
|
|
13
|
+
const BUNDLE_KEYS = [PACKAGE, '@kirkchinese/dsh-re-kit']
|
|
14
|
+
const identity = { parse: (value) => value }
|
|
15
|
+
const TYPERT_REMOTE = {
|
|
16
|
+
package: PACKAGE,
|
|
17
|
+
descriptors: [{
|
|
18
|
+
id: `${PACKAGE}#capabilityCatalog/request`,
|
|
19
|
+
service: 'capabilityCatalog',
|
|
20
|
+
namespace: 'capabilityCatalog',
|
|
21
|
+
method: 'request',
|
|
22
|
+
invocation: { kind: 'direct' },
|
|
23
|
+
parameters: [{
|
|
24
|
+
name: 'rawRequest',
|
|
25
|
+
wire: 'rawRequest',
|
|
26
|
+
source: 'json',
|
|
27
|
+
codec: { mode: 'strict', typeSymbol: `${PACKAGE}#CatalogRequest`, create: () => identity },
|
|
28
|
+
}],
|
|
29
|
+
cancellation: { parameter: 'signal' },
|
|
30
|
+
result: { mode: 'strict', typeSymbol: `${PACKAGE}#CatalogOverview`, create: () => identity },
|
|
31
|
+
sourceLocation: { file: 'src/remote.ts', line: 1, column: 1 },
|
|
32
|
+
}],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------- locale
|
|
36
|
+
const DICT = {
|
|
37
|
+
zh: {
|
|
38
|
+
title: '能力目录',
|
|
39
|
+
summary: '常驻索引约 {chars} 字符 · {tools} 个工具 · {skills} 个 skill',
|
|
40
|
+
servers: 'MCP 服务器',
|
|
41
|
+
skills: 'Skill',
|
|
42
|
+
noServers: '没有配置 MCP 服务器。在 profile 的 cordis.patch.yml 中添加,或安装 dsh-rea。',
|
|
43
|
+
noSkills: '没有可用的 skill,或 skill 来源未启用。',
|
|
44
|
+
tools: '{n} 个工具',
|
|
45
|
+
connected: '运行中',
|
|
46
|
+
idle: '未运行',
|
|
47
|
+
problem: '异常',
|
|
48
|
+
search: '搜索 skill',
|
|
49
|
+
filterAll: '全部',
|
|
50
|
+
filterOn: '已开启',
|
|
51
|
+
filterOff: '已关闭',
|
|
52
|
+
allOn: '全开',
|
|
53
|
+
allOff: '全关',
|
|
54
|
+
disabled: '能力目录已关闭(配置 enabled: false):不注册工具、不进索引,MCP 服务器不运行。',
|
|
55
|
+
globalNote: '这里的开关作用于所有会话,立即生效。单个会话可以在输入框左侧的「能力」按钮中覆盖。关闭的服务器会停止运行。',
|
|
56
|
+
noTakeover: '未接管 skill 目录(skills.takeOverCatalog 为 false):skill 开关不会影响 DSH 原生的 skill 目录。',
|
|
57
|
+
loading: '加载中…',
|
|
58
|
+
retry: '重试',
|
|
59
|
+
button: '能力',
|
|
60
|
+
sessionTitle: '本会话的能力',
|
|
61
|
+
sessionNote: '「默认」跟随全局设置。改动从下一步开始生效。',
|
|
62
|
+
reset: '恢复默认',
|
|
63
|
+
stateDefault: '默认',
|
|
64
|
+
stateOn: '开',
|
|
65
|
+
stateOff: '关',
|
|
66
|
+
offCount: '{n} 项已关闭',
|
|
67
|
+
close: '关闭',
|
|
68
|
+
error: '出错了:{message}',
|
|
69
|
+
},
|
|
70
|
+
en: {
|
|
71
|
+
title: 'Capability Catalog',
|
|
72
|
+
summary: 'Standing index about {chars} chars · {tools} tools · {skills} skills',
|
|
73
|
+
servers: 'MCP servers',
|
|
74
|
+
skills: 'Skills',
|
|
75
|
+
noServers: 'No MCP servers configured. Add one in the profile cordis.patch.yml, or install dsh-rea.',
|
|
76
|
+
noSkills: 'No skills available, or the skill source is off.',
|
|
77
|
+
tools: '{n} tools',
|
|
78
|
+
connected: 'Running',
|
|
79
|
+
idle: 'Idle',
|
|
80
|
+
problem: 'Problem',
|
|
81
|
+
search: 'Search skills',
|
|
82
|
+
filterAll: 'All',
|
|
83
|
+
filterOn: 'On',
|
|
84
|
+
filterOff: 'Off',
|
|
85
|
+
allOn: 'All on',
|
|
86
|
+
allOff: 'All off',
|
|
87
|
+
disabled: 'The capability catalog is switched off (enabled: false): no tools, no index, no MCP servers running.',
|
|
88
|
+
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
|
+
noTakeover: 'The skill catalog is not taken over (skills.takeOverCatalog is false): skill switches do not affect the native skill catalog.',
|
|
90
|
+
loading: 'Loading…',
|
|
91
|
+
retry: 'Retry',
|
|
92
|
+
button: 'Capabilities',
|
|
93
|
+
sessionTitle: 'Capabilities in this session',
|
|
94
|
+
sessionNote: '"Default" follows the global setting. Changes apply from the next step.',
|
|
95
|
+
reset: 'Reset',
|
|
96
|
+
stateDefault: 'Default',
|
|
97
|
+
stateOn: 'On',
|
|
98
|
+
stateOff: 'Off',
|
|
99
|
+
offCount: '{n} off',
|
|
100
|
+
close: 'Close',
|
|
101
|
+
error: 'Something went wrong: {message}',
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function format(text, values) {
|
|
106
|
+
return text.replace(/\{(\w+)\}/g, (_, key) => (values && key in values ? String(values[key]) : `{${key}}`))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------- styles
|
|
110
|
+
const STYLE_ID = 'capability-catalog-style'
|
|
111
|
+
const CSS = `
|
|
112
|
+
.cc-page{display:flex;flex-direction:column;gap:20px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}
|
|
113
|
+
.cc-summary{color:var(--dsw-alias-label-secondary)}
|
|
114
|
+
.cc-note{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
115
|
+
.cc-warn{color:var(--dsw-alias-state-warn-primary);font-size:12px}
|
|
116
|
+
.cc-section{display:flex;flex-direction:column;gap:8px}
|
|
117
|
+
.cc-section-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
118
|
+
.cc-section-title{font-size:14px;font-weight:600}
|
|
119
|
+
.cc-list{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}
|
|
120
|
+
.cc-row{display:flex;align-items:center;gap:12px;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
121
|
+
.cc-row:first-child{border-top:none}
|
|
122
|
+
.cc-row-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
123
|
+
.cc-row-name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
124
|
+
.cc-row-desc{color:var(--dsw-alias-label-secondary);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
125
|
+
.cc-family{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:var(--dsw-alias-bg-layer-2);border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
126
|
+
.cc-family:first-child{border-top:none}
|
|
127
|
+
.cc-pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
128
|
+
.cc-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-idle-primary)}
|
|
129
|
+
.cc-dot[data-state=on]{background:var(--dsw-alias-state-success-primary)}
|
|
130
|
+
.cc-dot[data-state=error]{background:var(--dsw-alias-state-error-primary)}
|
|
131
|
+
.cc-switch{position:relative;flex:none;width:34px;height:20px;border-radius:10px;border:none;padding:0;cursor:pointer;background:var(--dsw-alias-border-l2);transition:background .15s}
|
|
132
|
+
.cc-switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}
|
|
133
|
+
.cc-switch::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-base);transition:transform .15s}
|
|
134
|
+
.cc-switch[aria-checked=true]::after{transform:translateX(14px)}
|
|
135
|
+
.cc-switch:disabled{opacity:.5;cursor:default}
|
|
136
|
+
.cc-switch:focus-visible,.cc-btn:focus-visible,.cc-seg button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
137
|
+
.cc-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
138
|
+
.cc-input{flex:1;min-width:160px;height:30px;padding:0 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px}
|
|
139
|
+
.cc-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}
|
|
140
|
+
.cc-btn{height:26px;padding:0 10px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);font-size:12px;cursor:pointer}
|
|
141
|
+
.cc-btn:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
142
|
+
.cc-btn[aria-pressed=true]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
|
|
143
|
+
.cc-seg{display:inline-flex;flex:none;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;overflow:hidden}
|
|
144
|
+
.cc-seg button{height:24px;padding:0 9px;border:none;border-left:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
|
|
145
|
+
.cc-seg button:first-child{border-left:none}
|
|
146
|
+
.cc-seg button[aria-pressed=true]{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600}
|
|
147
|
+
.cc-seg button[data-value=off][aria-pressed=true]{color:var(--dsw-alias-state-error-primary)}
|
|
148
|
+
.cc-seg button[data-value=on][aria-pressed=true]{color:var(--dsw-alias-state-success-primary)}
|
|
149
|
+
.cc-trigger{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 8px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
|
|
150
|
+
.cc-trigger:hover,.cc-trigger[aria-expanded=true]{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
151
|
+
.cc-trigger-badge{color:var(--dsw-alias-state-warn-primary)}
|
|
152
|
+
.cc-anchor{position:relative;display:inline-flex}
|
|
153
|
+
.cc-popover{position:absolute;bottom:calc(100% + 8px);left:0;z-index:50;width:380px;max-width:calc(100vw - 32px);max-height:min(560px,70vh);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
|
+
.cc-popover-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-weight:600}
|
|
155
|
+
.cc-popover-body{overflow:auto;display:flex;flex-direction:column;gap:12px;margin:0 -4px;padding:0 4px}
|
|
156
|
+
.cc-sub{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:4px}
|
|
157
|
+
.cc-mini-row{display:flex;align-items:center;gap:8px;padding:5px 0}
|
|
158
|
+
.cc-mini-row .cc-row-main{gap:0}
|
|
159
|
+
`
|
|
160
|
+
|
|
161
|
+
function ensureStyle() {
|
|
162
|
+
if (document.getElementById(STYLE_ID)) return () => undefined
|
|
163
|
+
const style = document.createElement('style')
|
|
164
|
+
style.id = STYLE_ID
|
|
165
|
+
style.setAttribute('data-plugin', PACKAGE)
|
|
166
|
+
style.textContent = CSS
|
|
167
|
+
document.head.appendChild(style)
|
|
168
|
+
return () => style.remove()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------- data
|
|
172
|
+
function createApi(remoteCtx) {
|
|
173
|
+
const listeners = new Set()
|
|
174
|
+
return {
|
|
175
|
+
async request(request, signal) {
|
|
176
|
+
const response = await remoteCtx.remote.capabilityCatalog.request(request, signal)
|
|
177
|
+
if (!response || response.ok !== true) throw new Error((response && response.error && response.error.message) || 'Remote call failed')
|
|
178
|
+
if (request.action === 'set') for (const listener of [...listeners]) listener()
|
|
179
|
+
return response.value
|
|
180
|
+
},
|
|
181
|
+
onChange(listener) {
|
|
182
|
+
listeners.add(listener)
|
|
183
|
+
return () => listeners.delete(listener)
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function useOverview(api, sessionId, active) {
|
|
189
|
+
const [state, setState] = useState({ data: null, error: null, loading: true })
|
|
190
|
+
const generation = useRef(0)
|
|
191
|
+
const load = useCallback(() => {
|
|
192
|
+
const current = ++generation.current
|
|
193
|
+
const controller = new AbortController()
|
|
194
|
+
setState((previous) => ({ ...previous, loading: true }))
|
|
195
|
+
api.request(sessionId ? { action: 'overview', sessionId } : { action: 'overview' }, controller.signal)
|
|
196
|
+
.then((data) => { if (current === generation.current) setState({ data, error: null, loading: false }) })
|
|
197
|
+
.catch((error) => { if (current === generation.current) setState((previous) => ({ data: previous.data, error, loading: false })) })
|
|
198
|
+
return () => controller.abort()
|
|
199
|
+
}, [api, sessionId])
|
|
200
|
+
useEffect(() => (active ? load() : undefined), [load, active])
|
|
201
|
+
useEffect(() => api.onChange(() => { if (active) load() }), [api, load, active])
|
|
202
|
+
const set = useCallback(async (request) => {
|
|
203
|
+
const data = await api.request({ action: 'set', ...request, ...(sessionId ? { sessionId } : {}) })
|
|
204
|
+
generation.current++
|
|
205
|
+
setState({ data, error: null, loading: false })
|
|
206
|
+
}, [api, sessionId])
|
|
207
|
+
return { ...state, reload: load, set }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------- controls
|
|
211
|
+
function Switch({ checked, onChange, label, disabled }) {
|
|
212
|
+
return h('button', {
|
|
213
|
+
type: 'button', role: 'switch', className: 'cc-switch', 'aria-checked': checked, 'aria-label': label, disabled,
|
|
214
|
+
onClick: () => onChange(!checked),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Segmented({ value, onChange, t, label }) {
|
|
219
|
+
const options = [['default', t.stateDefault, null], ['on', t.stateOn, true], ['off', t.stateOff, false]]
|
|
220
|
+
return h('div', { className: 'cc-seg', role: 'group', 'aria-label': label },
|
|
221
|
+
options.map(([key, text, next]) => h('button', {
|
|
222
|
+
key, type: 'button', 'data-value': key, 'aria-pressed': value === next,
|
|
223
|
+
onClick: () => onChange(next),
|
|
224
|
+
}, text)))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function StatusPill({ server, t }) {
|
|
228
|
+
const state = server.problem ? 'error' : server.connected ? 'on' : 'idle'
|
|
229
|
+
return h('span', { className: 'cc-pill', title: server.problem || undefined },
|
|
230
|
+
h('span', { className: 'cc-dot', 'data-state': state }),
|
|
231
|
+
state === 'error' ? t.problem : state === 'on' ? t.connected : t.idle)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function groupByFamily(skills) {
|
|
235
|
+
const groups = new Map()
|
|
236
|
+
for (const skill of skills) {
|
|
237
|
+
if (!groups.has(skill.family)) groups.set(skill.family, [])
|
|
238
|
+
groups.get(skill.family).push(skill)
|
|
239
|
+
}
|
|
240
|
+
return [...groups.entries()].sort((a, b) => (a[0] === 'general') - (b[0] === 'general') || b[1].length - a[1].length || a[0].localeCompare(b[0]))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function matches(skill, query) {
|
|
244
|
+
if (!query) return true
|
|
245
|
+
const q = query.toLowerCase()
|
|
246
|
+
return skill.name.toLowerCase().includes(q) || skill.summary.toLowerCase().includes(q) || skill.family.toLowerCase().includes(q)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------- settings page
|
|
250
|
+
function SettingsPage({ api, t }) {
|
|
251
|
+
const { data, error, loading, reload, set } = useOverview(api, undefined, true)
|
|
252
|
+
const [query, setQuery] = useState('')
|
|
253
|
+
const [filter, setFilter] = useState('all')
|
|
254
|
+
const [busy, setBusy] = useState(false)
|
|
255
|
+
const [actionError, setActionError] = useState(null)
|
|
256
|
+
const run = useCallback(async (requests) => {
|
|
257
|
+
setBusy(true)
|
|
258
|
+
setActionError(null)
|
|
259
|
+
try {
|
|
260
|
+
for (const request of requests) await set({ scope: 'global', ...request })
|
|
261
|
+
} catch (err) {
|
|
262
|
+
setActionError(err)
|
|
263
|
+
} finally {
|
|
264
|
+
setBusy(false)
|
|
265
|
+
}
|
|
266
|
+
}, [set])
|
|
267
|
+
|
|
268
|
+
if (!data) {
|
|
269
|
+
return h('div', { className: 'cc-page' },
|
|
270
|
+
error ? h('div', { className: 'cc-warn' }, format(t.error, { message: error.message }), ' ', h('button', { className: 'cc-btn', type: 'button', onClick: reload }, t.retry))
|
|
271
|
+
: h('div', { className: 'cc-note' }, t.loading))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!data.enabled) return h('div', { className: 'cc-page' }, h('div', { className: 'cc-note' }, t.disabled))
|
|
275
|
+
const visibleSkills = data.skills.filter((skill) => matches(skill, query) && (filter === 'all' || (filter === 'on') === skill.effective))
|
|
276
|
+
const totalTools = data.servers.reduce((sum, server) => sum + (server.effective ? server.tools : 0), 0)
|
|
277
|
+
const enabledSkills = data.skills.filter((skill) => skill.effective).length
|
|
278
|
+
|
|
279
|
+
return h('div', { className: 'cc-page' },
|
|
280
|
+
h('div', { className: 'cc-summary' }, format(t.summary, { chars: data.indexChars.toLocaleString(), tools: totalTools, skills: enabledSkills })),
|
|
281
|
+
h('div', { className: 'cc-note' }, t.globalNote),
|
|
282
|
+
actionError ? h('div', { className: 'cc-warn' }, format(t.error, { message: actionError.message })) : null,
|
|
283
|
+
|
|
284
|
+
h('section', { className: 'cc-section' },
|
|
285
|
+
h('div', { className: 'cc-section-head' }, h('div', { className: 'cc-section-title' }, t.servers)),
|
|
286
|
+
data.servers.length === 0 ? h('div', { className: 'cc-note' }, t.noServers)
|
|
287
|
+
: h('div', { className: 'cc-list' }, data.servers.map((server) => h('div', { className: 'cc-row', key: server.name },
|
|
288
|
+
h('div', { className: 'cc-row-main' },
|
|
289
|
+
h('div', { className: 'cc-row-name' }, server.title, server.title !== server.name ? h('span', { className: 'cc-row-desc' }, ` mcp:${server.name}`) : null),
|
|
290
|
+
h('div', { className: 'cc-row-desc' }, format(t.tools, { n: server.tools }))),
|
|
291
|
+
h(StatusPill, { server, t }),
|
|
292
|
+
h(Switch, {
|
|
293
|
+
checked: server.global ?? server.configured, label: server.title, disabled: busy,
|
|
294
|
+
onChange: (next) => run([{ kind: 'server', name: server.name, value: next === server.configured ? null : next }]),
|
|
295
|
+
}))))),
|
|
296
|
+
|
|
297
|
+
h('section', { className: 'cc-section' },
|
|
298
|
+
h('div', { className: 'cc-section-head' }, h('div', { className: 'cc-section-title' }, `${t.skills} (${enabledSkills}/${data.skills.length})`)),
|
|
299
|
+
data.takeOverSkills ? null : h('div', { className: 'cc-warn' }, t.noTakeover),
|
|
300
|
+
h('div', { className: 'cc-toolbar' },
|
|
301
|
+
h('input', { className: 'cc-input', type: 'search', placeholder: t.search, value: query, onChange: (event) => setQuery(event.target.value), 'aria-label': t.search }),
|
|
302
|
+
[['all', t.filterAll], ['on', t.filterOn], ['off', t.filterOff]].map(([key, text]) => h('button', {
|
|
303
|
+
key, type: 'button', className: 'cc-btn', 'aria-pressed': filter === key, onClick: () => setFilter(key),
|
|
304
|
+
}, text))),
|
|
305
|
+
data.skills.length === 0 ? h('div', { className: 'cc-note' }, t.noSkills)
|
|
306
|
+
: h('div', { className: 'cc-list' }, groupByFamily(visibleSkills).map(([family, skills]) => h(React.Fragment, { key: family },
|
|
307
|
+
h('div', { className: 'cc-family' },
|
|
308
|
+
h('span', null, `${family} (${skills.length})`),
|
|
309
|
+
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))),
|
|
312
|
+
skills.map((skill) => h('div', { className: 'cc-row', key: skill.name },
|
|
313
|
+
h('div', { className: 'cc-row-main' },
|
|
314
|
+
h('div', { className: 'cc-row-name' }, skill.name),
|
|
315
|
+
h('div', { className: 'cc-row-desc', title: skill.summary }, skill.summary)),
|
|
316
|
+
h(Switch, {
|
|
317
|
+
checked: skill.global ?? true, label: skill.name, disabled: busy,
|
|
318
|
+
onChange: (next) => run([{ kind: 'skill', name: skill.name, value: next ? null : false }]),
|
|
319
|
+
}))))))),
|
|
320
|
+
loading ? h('div', { className: 'cc-note' }, t.loading) : null)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------- session control
|
|
324
|
+
function SessionControl({ api, t, sessionId }) {
|
|
325
|
+
const [open, setOpen] = useState(false)
|
|
326
|
+
const [query, setQuery] = useState('')
|
|
327
|
+
const anchor = useRef(null)
|
|
328
|
+
const { data, error, set } = useOverview(api, sessionId, true)
|
|
329
|
+
const [actionError, setActionError] = useState(null)
|
|
330
|
+
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (!open) return undefined
|
|
333
|
+
const onKey = (event) => { if (event.key === 'Escape') setOpen(false) }
|
|
334
|
+
const onPointer = (event) => { if (anchor.current && !anchor.current.contains(event.target)) setOpen(false) }
|
|
335
|
+
document.addEventListener('keydown', onKey)
|
|
336
|
+
document.addEventListener('pointerdown', onPointer)
|
|
337
|
+
return () => {
|
|
338
|
+
document.removeEventListener('keydown', onKey)
|
|
339
|
+
document.removeEventListener('pointerdown', onPointer)
|
|
340
|
+
}
|
|
341
|
+
}, [open])
|
|
342
|
+
|
|
343
|
+
if (!sessionId || (data && !data.enabled)) return null
|
|
344
|
+
const offCount = data ? data.servers.filter((server) => !server.effective).length + data.skills.filter((skill) => !skill.effective).length : 0
|
|
345
|
+
const change = async (kind, name, value) => {
|
|
346
|
+
setActionError(null)
|
|
347
|
+
try {
|
|
348
|
+
await set({ scope: 'session', kind, name, value })
|
|
349
|
+
} catch (err) {
|
|
350
|
+
setActionError(err)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const reset = async () => {
|
|
354
|
+
if (!data) return
|
|
355
|
+
for (const server of data.servers) if (server.session !== null) await change('server', server.name, null)
|
|
356
|
+
for (const skill of data.skills) if (skill.session !== null) await change('skill', skill.name, null)
|
|
357
|
+
}
|
|
358
|
+
const skills = data ? data.skills.filter((skill) => matches(skill, query)) : []
|
|
359
|
+
|
|
360
|
+
return h('span', { className: 'cc-anchor', ref: anchor },
|
|
361
|
+
h('button', {
|
|
362
|
+
type: 'button', className: 'cc-trigger', 'aria-expanded': open, 'aria-haspopup': 'dialog',
|
|
363
|
+
title: offCount > 0 ? format(t.offCount, { n: offCount }) : t.sessionTitle,
|
|
364
|
+
onClick: () => setOpen((value) => !value),
|
|
365
|
+
},
|
|
366
|
+
h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', 'aria-hidden': true, fill: 'none', stroke: 'currentColor', strokeWidth: 1.5 },
|
|
367
|
+
h('rect', { x: 2, y: 2, width: 5, height: 5, rx: 1 }), h('rect', { x: 9, y: 2, width: 5, height: 5, rx: 1 }),
|
|
368
|
+
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
|
+
t.button,
|
|
370
|
+
offCount > 0 ? h('span', { className: 'cc-trigger-badge' }, `−${offCount}`) : null),
|
|
371
|
+
open ? h('div', { className: 'cc-popover', role: 'dialog', 'aria-label': t.sessionTitle },
|
|
372
|
+
h('div', { className: 'cc-popover-head' },
|
|
373
|
+
h('span', null, t.sessionTitle),
|
|
374
|
+
h('span', { className: 'cc-toolbar' },
|
|
375
|
+
h('button', { type: 'button', className: 'cc-btn', onClick: reset }, t.reset),
|
|
376
|
+
h('button', { type: 'button', className: 'cc-btn', onClick: () => setOpen(false) }, t.close))),
|
|
377
|
+
h('div', { className: 'cc-note' }, t.sessionNote),
|
|
378
|
+
error ? h('div', { className: 'cc-warn' }, format(t.error, { message: error.message })) : null,
|
|
379
|
+
actionError ? h('div', { className: 'cc-warn' }, format(t.error, { message: actionError.message })) : null,
|
|
380
|
+
!data ? h('div', { className: 'cc-note' }, t.loading) : h('div', { className: 'cc-popover-body' },
|
|
381
|
+
data.servers.length > 0 ? h('div', null,
|
|
382
|
+
h('div', { className: 'cc-sub' }, t.servers),
|
|
383
|
+
data.servers.map((server) => h('div', { className: 'cc-mini-row', key: server.name },
|
|
384
|
+
h('div', { className: 'cc-row-main' },
|
|
385
|
+
h('div', { className: 'cc-row-name' }, server.title),
|
|
386
|
+
h('div', { className: 'cc-row-desc' }, format(t.tools, { n: server.tools }))),
|
|
387
|
+
h(Segmented, { t, label: server.title, value: server.session, onChange: (value) => change('server', server.name, value) })))) : null,
|
|
388
|
+
data.skills.length > 0 ? h('div', null,
|
|
389
|
+
h('div', { className: 'cc-sub' }, `${t.skills} (${data.skills.filter((skill) => skill.effective).length}/${data.skills.length})`),
|
|
390
|
+
h('input', { className: 'cc-input', type: 'search', placeholder: t.search, value: query, onChange: (event) => setQuery(event.target.value), 'aria-label': t.search, style: { width: '100%', marginBottom: 6 } }),
|
|
391
|
+
skills.map((skill) => h('div', { className: 'cc-mini-row', key: skill.name },
|
|
392
|
+
h('div', { className: 'cc-row-main' },
|
|
393
|
+
h('div', { className: 'cc-row-name', title: skill.summary }, skill.name, skill.effective ? null : h('span', { className: 'cc-row-desc' }, ` · ${t.stateOff}`))),
|
|
394
|
+
h(Segmented, { t, label: skill.name, value: skill.session, onChange: (value) => change('skill', skill.name, value) })))) : null)) : null)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------- plugin
|
|
398
|
+
return {
|
|
399
|
+
inject: ['slots', 'remote'],
|
|
400
|
+
async apply(ctx) {
|
|
401
|
+
ctx.effect(ensureStyle, 'capability-catalog: styles')
|
|
402
|
+
const unmountRemote = await ctx.remote.$mount(TYPERT_REMOTE)
|
|
403
|
+
ctx.effect(() => unmountRemote, 'capability-catalog: Remote contribution')
|
|
404
|
+
|
|
405
|
+
const localeService = ctx.get('locale')
|
|
406
|
+
const subscribe = (listener) => (localeService && localeService.subscribe ? localeService.subscribe(listener) : () => undefined)
|
|
407
|
+
const snapshot = () => {
|
|
408
|
+
const value = localeService && localeService.getSnapshot ? localeService.getSnapshot() : undefined
|
|
409
|
+
const id = (value && value.active) || navigator.language || 'en'
|
|
410
|
+
return String(id).toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
411
|
+
}
|
|
412
|
+
const useT = () => DICT[useSyncExternalStore(subscribe, snapshot, snapshot)]
|
|
413
|
+
|
|
414
|
+
ctx.inject(['remote.capabilityCatalog'], (remoteCtx) => {
|
|
415
|
+
const api = createApi(remoteCtx)
|
|
416
|
+
function BundleConfig(props) {
|
|
417
|
+
const t = useT()
|
|
418
|
+
if (props.view !== 'page') return t.title
|
|
419
|
+
return h(SettingsPage, { api, t })
|
|
420
|
+
}
|
|
421
|
+
function ComposerControl(props) {
|
|
422
|
+
const t = useT()
|
|
423
|
+
return h(SessionControl, { api, t, sessionId: props.sessionId })
|
|
424
|
+
}
|
|
425
|
+
for (const key of BUNDLE_KEYS) {
|
|
426
|
+
remoteCtx.slots.inject('plugins.bundle.config', () => remoteCtx.slots.register({ name: 'plugins.bundle.config', key }, BundleConfig))
|
|
427
|
+
}
|
|
428
|
+
remoteCtx.slots.inject('conversation.input.left', () => remoteCtx.slots.register({
|
|
429
|
+
name: 'conversation.input.left', id: 'capability-catalog', order: 40, label: () => DICT[snapshot()].button,
|
|
430
|
+
}, ComposerControl))
|
|
431
|
+
})
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Capability catalog: one compact index plus search/describe/call tools.
|
|
2
|
+
# Add MCP servers under config.mcp in your own cordis.patch.yml (an override replaces the whole config).
|
|
3
|
+
- insert:
|
|
4
|
+
- id: capability-catalog
|
|
5
|
+
name: '@kirkchinese/dsh-capability-catalog'
|
|
6
|
+
config: {}
|
package/lib/catalog.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalog: merges sources, renders the standing index, and answers lookups.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import { type SearchHit } from './search.js';
|
|
6
|
+
import type { CapabilityEntry, CapabilitySource, FamilySummary, SourceIndex, ViewContext } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Derive a one-line summary from a description.
|
|
9
|
+
* @param description - full text.
|
|
10
|
+
* @param max - character budget.
|
|
11
|
+
*/
|
|
12
|
+
export declare function summarize(description: string, max?: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* Summarize families of a source's entries.
|
|
15
|
+
* @param entries - entries of one source.
|
|
16
|
+
* @param descriptions - optional family descriptions.
|
|
17
|
+
* @param examples - representative names per family.
|
|
18
|
+
*/
|
|
19
|
+
export declare function summarizeFamilies(entries: readonly CapabilityEntry[], descriptions?: ReadonlyMap<string, string>, examples?: number): FamilySummary[];
|
|
20
|
+
/** The tool names the index tells the model to use. */
|
|
21
|
+
export interface MetaToolNames {
|
|
22
|
+
readonly search: string;
|
|
23
|
+
readonly describe: string;
|
|
24
|
+
readonly call: string;
|
|
25
|
+
readonly skill: string;
|
|
26
|
+
}
|
|
27
|
+
/** Merged view over every registered source. */
|
|
28
|
+
export declare class Catalog {
|
|
29
|
+
private readonly sources;
|
|
30
|
+
private cache;
|
|
31
|
+
/** Decides whether an entry is enabled for a viewer; everything is enabled by default. */
|
|
32
|
+
isEnabled: (entry: CapabilityEntry, view: ViewContext) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Add a source; returns its disposer.
|
|
35
|
+
* @param source - capability source with a unique key.
|
|
36
|
+
*/
|
|
37
|
+
add(source: CapabilitySource): () => void;
|
|
38
|
+
list(): CapabilitySource[];
|
|
39
|
+
source(key: string): CapabilitySource | undefined;
|
|
40
|
+
/** Every entry enabled for the viewer. */
|
|
41
|
+
entries(view: ViewContext): CapabilityEntry[];
|
|
42
|
+
/** Every entry, enabled or not (for settings views and precise error messages). */
|
|
43
|
+
allEntries(view: ViewContext): CapabilityEntry[];
|
|
44
|
+
/**
|
|
45
|
+
* Find one entry by id, accepting a bare name when it is unambiguous.
|
|
46
|
+
* @param id - entry id or bare name.
|
|
47
|
+
* @param view - viewing scope.
|
|
48
|
+
*/
|
|
49
|
+
find(id: string, view: ViewContext, includeDisabled?: boolean): CapabilityEntry | {
|
|
50
|
+
ambiguous: CapabilityEntry[];
|
|
51
|
+
} | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Keyword search across sources.
|
|
54
|
+
* @param query - free text.
|
|
55
|
+
* @param view - viewing scope.
|
|
56
|
+
* @param filter - optional source, family and kind filters.
|
|
57
|
+
* @param limit - maximum hits.
|
|
58
|
+
*/
|
|
59
|
+
search(query: string, view: ViewContext, filter: {
|
|
60
|
+
source?: string;
|
|
61
|
+
family?: string;
|
|
62
|
+
kind?: string;
|
|
63
|
+
}, limit: number): SearchHit[];
|
|
64
|
+
/** Index blocks of every source, in registration order. */
|
|
65
|
+
indexes(view: ViewContext): SourceIndex[];
|
|
66
|
+
/**
|
|
67
|
+
* Render the standing index text.
|
|
68
|
+
* @param view - viewing scope.
|
|
69
|
+
* @param names - meta-tool names to reference.
|
|
70
|
+
* @param skillTakeover - whether skills load through the replacement `skill` tool.
|
|
71
|
+
* @returns prompt text, or an empty string when there is nothing to index.
|
|
72
|
+
*/
|
|
73
|
+
renderIndex(view: ViewContext, names: MetaToolNames, skillTakeover: boolean): string;
|
|
74
|
+
refresh(view: ViewContext, signal: AbortSignal): Promise<void>;
|
|
75
|
+
dispose(): Promise<void>;
|
|
76
|
+
}
|