dsh-xray 0.8.1 → 0.9.0

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
@@ -19,17 +19,53 @@
19
19
  <a href="./README.zh.md">🇨🇳 中文文档</a>
20
20
  </p>
21
21
 
22
- ![dsh-xray demo](./docs/demo.svg)
22
+ ---
23
+
24
+ Every plugin you mount quietly bills every LLM request: prompt sections, tool schemas, tokens. dsh-xray sits inside your running harness as an **X-Ray tab beside Chat and Trajectory** and itemizes that bill — per plugin, per entry, down to the exact text:
25
+
26
+ ![The X-Ray tab: every plugin's per-request context tax, attributed and ranked](./assets/tab-cost.webp)
27
+
28
+ Unfold a plugin to see what it registered; click any entry to read the exact text it puts into every request:
29
+
30
+ <table>
31
+ <tr>
32
+ <td width="50%">
33
+
34
+ ![A plugin unfolds into its registered entries](./assets/tab-expand.webp)
35
+
36
+ </td>
37
+ <td width="50%">
38
+
39
+ ![The raw text behind ~184 tokens, with a chars/tokens ruler](./assets/tab-entry.webp)
40
+
41
+ </td>
42
+ </tr>
43
+ </table>
44
+
45
+ Three clicks: plugin rollup → entry list → the actual words. The number stops being an estimate you trust and becomes a fact you checked.
23
46
 
24
47
  ---
25
48
 
26
49
  ## The Problem
27
50
 
28
- `dsh --dump-config` shows you the composed tree. The plugin panel shows you a flat list. Neither tells you **why** a plugin is there, **what breaks** if you disable it, or **what it silently costs you**.
51
+ `dsh --dump-config` shows you the composed tree. The plugin panel shows you a flat list. Neither tells you **why** a plugin is there, **what breaks** if you disable it, or **what it silently costs you** on every single request.
52
+
53
+ **dsh-xray does.** And when the answer is "this plugin taxes every request and nothing depends on it" — the `deps` view confirms the disable is safe, one patch line removes it, and `attribute` verifies it took.
54
+
55
+ ---
56
+
57
+ <p align="center">
58
+ <img src="./assets/section-context-tax.svg" width="100%" alt="Context Tax">
59
+ </p>
60
+
61
+ The cost view answers the question no other tool asks: **who put this in my context, and what does it cost?**
29
62
 
30
- **dsh-xray does.**
63
+ - **Attribution** — every prompt section and tool schema is joined to the plugin that registered it, reconstructed live from the registries (ambiguous entries stay `unattributed`, never guessed).
64
+ - **By-plugin rollup** — each plugin's per-request context tax: sections + schemas + tokens + share, ranked.
65
+ - **Entry inspection** — `/xray/api/entry` returns any entry's live text with a chars/tokens ruler. Computed per request, never persisted.
66
+ - **Explained UI** — every view opens with a one-line "what am I looking at"; terms carry plain-language tooltips; the whole tab is localized (English / 中文) through the host locale service.
31
67
 
32
- > Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
68
+ The same data flows through three surfaces: the **X-Ray tab** (native GUI), the standalone **`/xray` page** (works even when the client-module pipeline it diagnoses is broken), and the **CLI**.
33
69
 
34
70
  ---
35
71
 
@@ -49,6 +85,12 @@ npx dsh-xray shadow # services provided by multiple plugins
49
85
  npx dsh-xray audit # static scan of out-of-tree plugins for sensitive touchpoints
50
86
  ```
51
87
 
88
+ ![dsh-xray demo](./docs/demo.svg)
89
+
90
+ `attribute`, `conflicts`, and `snapshot` are fully static — they work even when dsh cannot boot. All commands take `--profile <name>` (default `web`) and `--json`; `diff` and `health` exit `1` on drift/unhealth, so they slot into CI.
91
+
92
+ ---
93
+
52
94
  <p align="center">
53
95
  <img src="./assets/section-features.svg" width="100%" alt="Features">
54
96
  </p>
@@ -61,56 +103,35 @@ npx dsh-xray audit # static scan of out-of-tree plugins for sensitive touc
61
103
  Which layer introduced each active plugin: kernel bundle, profile dependency, `cordis.patch.yml` insert, or repository source.
62
104
 
63
105
  ### 📊 Declared vs. Actual Diff
64
- Installed-but-inactive, uninstalled-but-lingering patch rows — all surfaced.
106
+ Installed-but-inactive, uninstalled-but-lingering patch rows — including patch rows targeting ids that don't exist (dsh skips them silently).
65
107
 
66
108
  ### ⚡ Conflict Detection
67
109
  Plugins patching the same config row, and which one silently wins.
68
110
 
69
111
  ### 📸 Composition Snapshot
70
- Export the effective composition as a lockfile; reproduce it elsewhere.
112
+ Export the effective composition as a lockfile; reproduce it elsewhere, diff against it later.
71
113
 
72
114
  </td>
73
115
  <td width="50%">
74
116
 
75
117
  ### 🌐 Service Dependency Graph
76
- Who provides and consumes each service; what cascades if you disable X.
118
+ Who provides and consumes each service and the **disable-cascade**: exactly which dependents go down if you disable X.
77
119
 
78
120
  ### 💊 Runtime Health
79
- Per-plugin fiber lifecycle state, startup failures, transition history.
80
-
81
- ### 🤖 Agent Self-Introspection
82
- The `xray_composition` tool lets agents inspect their own capability set.
83
-
84
- ### 🖥️ Web Panel
85
- Mounted in `dsh web`, the plugin serves a zero-dependency panel at **`/xray`** — summary, health, deps (with the disable-cascade table), cost, and shadow views, live from the running composition. JSON endpoints under `/xray/api/*` serve the same data.
121
+ Per-plugin fiber lifecycle state, startup failures, pending injects, transition history.
86
122
 
87
- The package also ships a client half (`dsh.client` declaration + `exports["./client"]`): the host discovers it automatically and mounts an **X-ray tab beside Chat / Trajectory** in every session, rendering the same five views natively in the GUI with the host's design tokens. The standalone `/xray` page stays available as the degradation path — it only needs the web server, not the client-module pipeline it helps diagnose.
88
-
89
- ![The /xray panel: deps view with the disable-cascade table](./assets/panel-deps.webp)
90
-
91
- What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
92
-
93
- ```console
94
- $ npx dsh-xray cost
95
- ~1625 tokens: 1 tool schema(s) ~121 + 19 prompt section(s) ~1504
96
-
97
- # prompt sections (observed at last assembly):
98
- app:web-surface ~248 15.3% ████████
99
- tool:goal ~184 11.3% ██████
100
- tool:ralph ~109 6.7% ███
101
- harness:source ~94 5.8% ███
102
- ...
103
- ```
104
-
105
- And when a patch row targets an id that doesn't exist (dsh skips it silently), `diff` catches it:
123
+ ### 👥 Service Shadowing
124
+ Same-name registrations where a later writer silently wins — usually an intended override, occasionally a conflict.
106
125
 
107
126
  ### 🛡️ Capability Audit
108
- Heuristic static scan: network egress, shell, filesystem, env, eval.
127
+ Heuristic static scan of out-of-tree plugins: network egress, shell, filesystem, env, eval.
109
128
 
110
129
  </td>
111
130
  </tr>
112
131
  </table>
113
132
 
133
+ ---
134
+
114
135
  <p align="center">
115
136
  <img src="./assets/section-agent.svg" width="100%" alt="Agent Tool">
116
137
  </p>
@@ -131,7 +152,8 @@ Mounted in the tree, dsh-xray registers an `xray_composition` tool (`view: summa
131
152
 
132
153
  - Loader `!!js` expressions in patch files are parsed as opaque markers and **never evaluated**
133
154
  - The CLI **never executes** plugin code (`audit` is a pattern scan over source text)
134
- - The mounted plugin writes only under `$DSH_HOME/xray/`
155
+ - The mounted plugin writes only under `$DSH_HOME/xray/` — entry text is served live, **never persisted**
156
+ - The entry endpoint returns composition-layer text only, **never session messages**
135
157
  - See [SECURITY.md](./SECURITY.md)
136
158
 
137
159
  ---
@@ -148,7 +170,7 @@ Two ways to use it — they're independent:
148
170
  npx dsh-xray attribute # requires Node >= 22
149
171
  ```
150
172
 
151
- **2. Mount the plugin** (adds the runtime commands, the `/xray` panel, and the agent tool):
173
+ **2. Mount the plugin** (adds the runtime commands, the X-Ray tab, the `/xray` panel, and the agent tool):
152
174
 
153
175
  ```sh
154
176
  dsh plugin --profile web add dsh-xray
@@ -160,13 +182,12 @@ Verify it took:
160
182
  ```sh
161
183
  dsh --profile web --dump-config | grep dsh-xray # row present in the composed tree
162
184
  npx dsh-xray health # reads the runtime snapshot
163
- # then open http://localhost:3080/xray for the live panel
185
+ # then open any session and click the X-Ray tab,
186
+ # or http://localhost:3080/xray for the standalone panel
164
187
  ```
165
188
 
166
189
  Uninstall: `dsh plugin --profile web remove dsh-xray`.
167
190
 
168
- All commands take `--profile <name>` (default `web`) and `--json`.
169
-
170
191
  | Command | Behavior |
171
192
  | --- | --- |
172
193
  | `diff` | Exits `1` when the trees disagree |
@@ -184,16 +205,16 @@ Diagnostic imaging for a running composition — complementary to [dsh-doctor](h
184
205
 
185
206
  | Feature | Category |
186
207
  | --- | --- |
208
+ | Context-tax attribution & entry inspection | 💰 Optimization |
187
209
  | Layer attribution | 🔍 Inspection |
188
210
  | Declared vs. actual diff | 🔍 Inspection |
189
211
  | Conflict detection | 🔍 Inspection |
190
212
  | Composition snapshot | 📦 Export |
191
213
  | Service dependency graph | 🌐 Runtime |
192
214
  | Runtime health | 🌐 Runtime |
215
+ | Service shadowing | 🌐 Runtime |
193
216
  | Agent self-introspection | 🤖 AI |
194
217
  | Capability audit | 🛡️ Security |
195
- | Service shadowing | 🌐 Runtime |
196
- | Context cost | 💰 Optimization |
197
218
 
198
219
  ---
199
220
 
package/README.zh.md CHANGED
@@ -18,13 +18,59 @@
18
18
  <a href="./README.md">English</a>
19
19
  </p>
20
20
 
21
- ![dsh-xray 演示](./docs/demo.svg)
21
+ ---
22
+
23
+ 你挂载的每个插件都在悄悄向每次 LLM 请求收费:prompt sections、工具 schema、token。dsh-xray 以 **与 Chat / Trajectory 并列的 X 光标签页** 长在你运行中的 harness 里,把这份账单逐项摊开——按插件、按条目,直到具体到每一个字:
24
+
25
+ ![X 光标签页:每个插件的每请求上下文税,已归因并排序](./assets/tab-cost.webp)
26
+
27
+ 展开插件看它注册了什么;点击任何条目,读它注入每次请求的原文:
28
+
29
+ <table>
30
+ <tr>
31
+ <td width="50%">
32
+
33
+ ![插件展开为其注册的条目](./assets/tab-expand.webp)
34
+
35
+ </td>
36
+ <td width="50%">
37
+
38
+ ![~184 token 背后的原文,附字符/token 标尺](./assets/tab-entry.webp)
39
+
40
+ </td>
41
+ </tr>
42
+ </table>
43
+
44
+ 三次点击:插件总账 → 条目清单 → 实际文字。数字不再是你被迫相信的估算,而是你亲手核对过的事实。
45
+
46
+ ---
47
+
48
+ ## 问题
22
49
 
23
- > 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
50
+ `dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件**为什么**在这、停用它会**连带瘫掉什么**、它在每次请求里**悄悄消耗什么**。
24
51
 
25
- `dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件*为什么*在这、停用它会*连带瘫掉什么*、它在*悄悄消耗什么*。dsh-xray 回答这些。
52
+ **dsh-xray 回答这些。** 当答案是"这个插件对每次请求收税、却没有任何东西依赖它"——`deps` 视图确认可以安全停用,一行 patch 移除它,`attribute` 验证生效。
26
53
 
27
- ## CLI
54
+ ---
55
+
56
+ <p align="center">
57
+ <img src="./assets/section-context-tax.svg" width="100%" alt="上下文税">
58
+ </p>
59
+
60
+ cost 视图回答一个别的工具都不问的问题:**这段上下文是谁放进来的、花掉我多少?**
61
+
62
+ - **来源归因** —— 每个 prompt section 和工具 schema 都关联到注册它的插件,实时从 registry 重建(无法唯一归因的条目诚实标注 `unattributed`,绝不猜测)。
63
+ - **按插件汇总** —— 每个插件的每请求上下文税:sections + schemas + tokens + 占比,排序呈现。
64
+ - **条目原文查看** —— `/xray/api/entry` 返回任意条目的实时文本,附字符/token 标尺。按请求现算,绝不落盘。
65
+ - **界面自解释** —— 每个视图开头一句"你在看什么";术语带白话提示;整个标签页通过宿主 locale 服务双语呈现(English / 中文)。
66
+
67
+ 同一份数据流经三个界面:**X 光标签页**(原生 GUI)、独立 **`/xray` 页面**(连它所诊断的 client-module 加载链路挂了都能用)、以及 **CLI**。
68
+
69
+ ---
70
+
71
+ <p align="center">
72
+ <img src="./assets/section-cli.svg" width="100%" alt="CLI 命令">
73
+ </p>
28
74
 
29
75
  ```sh
30
76
  npx dsh-xray attribute # 每一行由哪层引入、之后被谁 patch 过
@@ -38,126 +84,139 @@ npx dsh-xray shadow # 被多个插件同时提供的服务
38
84
  npx dsh-xray audit # 对 out-of-tree 插件做敏感触点静态扫描
39
85
  ```
40
86
 
41
- 所有命令支持 `--profile <name>`(默认 `web`)和 `--json`。`diff` 在两棵树不一致时退出码 `1`;`health` 在有插件不健康时退出码 `1`。`attribute`、`conflicts`、`snapshot` 是纯静态的:dsh 起不来时照样能跑。`deps` 和 `health` 读取已挂载插件维护在 `$DSH_HOME/xray/runtime.json` 的运行时快照。
42
-
43
- ## 长什么样
87
+ ![dsh-xray 演示](./docs/demo.svg)
44
88
 
45
- 启动树的每一行,归因到引入它的层——以及之后谁 patch 过它:
89
+ `attribute`、`conflicts`、`snapshot` 是纯静态的——dsh 起不来时照样能跑。所有命令支持 `--profile <name>`(默认 `web`)和 `--json`;`diff` 与 `health` 在漂移/不健康时退出码 `1`,可直接进 CI。
46
90
 
47
- ```console
48
- $ npx dsh-xray attribute
49
- # 130 rows in profile "web"
91
+ ---
50
92
 
51
- timer @deepseek-ai/dsh-base
52
- hmr @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app [disabled]
53
- llm @deepseek-ai/dsh-base
54
- session-query-sqlite @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app
55
- ...
56
- ```
93
+ <p align="center">
94
+ <img src="./assets/section-features.svg" width="100%" alt="能力">
95
+ </p>
57
96
 
58
- 停用一个 provider 会连带瘫掉什么——从真实服务存储算出来,不是猜的:
97
+ <table>
98
+ <tr>
99
+ <td width="50%">
59
100
 
60
- ```console
61
- $ npx dsh-xray deps
62
- # disable-cascade (transitive consumers of each provider):
63
- Loader → 5 plugin(s): AgentPresets, ClientModuleRegistry, Hmr, Include, PluginInventoryGateway
64
- TimerService → 1 plugin(s): Hmr
65
- SessionProjectionRegistry → 1 plugin(s): SessionProjectionCache
66
- ```
101
+ ### 🔍 层归因
102
+ 每个活跃插件来自哪一层:内核 bundle / profile 依赖 / `cordis.patch.yml` insert / repository 源。
67
103
 
68
- 哪些字段有多个写者、谁静默赢了:
104
+ ### 📊 声明 vs 实际 diff
105
+ 装了但没生效、卸了但残留 patch 行——包括指向不存在 id 的 patch 行(dsh 会静默跳过)。
69
106
 
70
- ```console
71
- $ npx dsh-xray conflicts
72
- session-query-sqlite
73
- .config: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
74
- tool-bash
75
- .disabled: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
76
- ```
107
+ ### ⚡ 冲突检测
108
+ 多个插件 patch 同一配置行时,谁静默赢了。
77
109
 
78
- 每次请求实际携带什么——assembly 时观测到的 prompt sections,与工具 schema 合并计价:
110
+ ### 📸 组合快照
111
+ 把当前生效组合导出为 lockfile,异地复现、事后对比。
79
112
 
80
- ```console
81
- $ npx dsh-xray cost
82
- ~1625 tokens: 1 tool schema(s) ~121 + 19 prompt section(s) ~1504
113
+ </td>
114
+ <td width="50%">
83
115
 
84
- # prompt sections (observed at last assembly):
85
- app:web-surface ~248 15.3% ████████
86
- tool:goal ~184 11.3% ██████
87
- tool:ralph ~109 6.7% ███
88
- harness:source ~94 5.8% ███
89
- ...
90
- ```
116
+ ### 🌐 服务依赖图
117
+ 每个服务谁提供、谁消费——以及**停用级联**:停用 X 会连带瘫掉哪些依赖方。
91
118
 
92
- patch 行指向不存在的 id 时(dsh 静默跳过),`diff` 能抓到:
119
+ ### 💊 运行时健康
120
+ 每个插件的 fiber 生命周期状态、启动失败、等待中的注入、状态迁移史。
93
121
 
94
- ```console
95
- $ npx dsh-xray diff
96
- orphan overrides (silently skipped) (1)
97
- no-such-row in ~/.dsh/profiles/web/cordis.patch.yml
98
- ```
122
+ ### 👥 服务遮蔽
123
+ 同名注册中后来者静默胜出——通常是有意覆盖,偶尔是冲突。
99
124
 
100
- ## Agent 工具
125
+ ### 🛡️ 能力审计
126
+ 对 out-of-tree 插件的启发式静态扫描:网络外发、shell、文件系统、环境变量、eval。
101
127
 
102
- 挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
128
+ </td>
129
+ </tr>
130
+ </table>
103
131
 
104
- ## Web 面板
132
+ ---
105
133
 
106
- `dsh web` 中挂载后,插件在 **`/xray`** 提供零依赖面板——summary、health、deps(含停用级联表)、cost、shadow 五个视图,数据实时来自运行中的组合树;`/xray/api/*` 提供同源 JSON。
134
+ <p align="center">
135
+ <img src="./assets/section-agent.svg" width="100%" alt="Agent 工具">
136
+ </p>
107
137
 
108
- 包同时携带 client 半(`dsh.client` 声明 + `exports["./client"]`):宿主自动发现并在每个会话中挂载 **与 Chat / Trajectory 并列的 X-ray 标签页**,用宿主设计变量原生渲染同样五个视图。独立的 `/xray` 页面保留为降级通道——它只依赖 web 服务器,不依赖它所诊断的 client-module 加载链路。
138
+ 挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答:
109
139
 
110
- ![/xray 面板:deps 视图与停用级联表](./assets/panel-deps.webp)
140
+ > *"我有哪些能力?" / "哪个插件提供 X?" / "为什么 Y 不可用?"*
111
141
 
112
- ## 安全立场
142
+ ——关于它自己。
113
143
 
114
- dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为不透明标记、绝不求值;CLI 从不执行插件代码(`audit` 是对源码文本的模式扫描);挂载的插件只写 `$DSH_HOME/xray/` 目录。详见 [SECURITY.md](./SECURITY.md)。
144
+ ---
115
145
 
116
- ## 能力
146
+ <p align="center">
147
+ <img src="./assets/section-safety.svg" width="100%" alt="安全立场">
148
+ </p>
117
149
 
118
- 对运行中组合树的诊断成像——与 [dsh-doctor](https://www.npmjs.com/package/dsh-doctor)(救援与恢复)互补。
150
+ **dsh-xray 只读,不执行。**
119
151
 
120
- 已交付:
152
+ - patch 文件里的 loader `!!js` 表达式解析为不透明标记,**绝不求值**
153
+ - CLI **从不执行**插件代码(`audit` 是对源码文本的模式扫描)
154
+ - 挂载的插件只写 `$DSH_HOME/xray/` 目录——条目原文实时返回,**绝不落盘**
155
+ - entry 端点只返回组合层文本,**绝不返回会话消息**
156
+ - 详见 [SECURITY.md](./SECURITY.md)
121
157
 
122
- - **来源归因** — 每个活跃插件来自哪一层:内核 bundle / profile 依赖 / `cordis.patch.yml` insert / repository 源
123
- - **声明 vs 实际 diff** — 装了但没生效、卸了但残留 patch 行
124
- - **冲突检测** — 多个插件 patch 同一配置行时,谁静默赢了
125
- - **组合快照** — 把当前生效组合导出为 lockfile;`--against` 对比漂移
126
- - **服务依赖图** — 每个服务谁提供、谁消费;停用 X 会级联影响什么(`deps`)
127
- - **运行时健康** — 每个插件的 fiber 生命周期状态、启动失败、状态迁移史(`health`)
128
- - **Agent 自省** — `xray_composition` 工具让 agent 检视自己的能力集
129
- - **能力审计** — 对 out-of-tree 插件的启发式静态扫描:网络外发、shell、文件系统、环境变量、动态求值(`audit`)
130
- - **服务重名检测** — 被多个插件同时提供的服务,及每插件工具/命令注册数(`shadow`)
131
- - **上下文成本** — prompt sections(观测自 system-prompt/assemble)+ 工具 schema 的估算 token 占用(`cost`)
132
- - **Web 面板** — `/xray` 五视图,零依赖零构建
158
+ ---
133
159
 
134
- ## 安装
160
+ <p align="center">
161
+ <img src="./assets/section-install.svg" width="100%" alt="安装">
162
+ </p>
135
163
 
136
- 两条使用路径,互相独立:
164
+ 两种用法,彼此独立:
137
165
 
138
- **1. 只用静态 CLI**(不装进 dsh;dsh 起不来时也能用):
166
+ **1. 只用静态 CLI**(不装进 dsh;dsh 起不来时照样能用):
139
167
 
140
168
  ```sh
141
169
  npx dsh-xray attribute # 需要 Node >= 22
142
170
  ```
143
171
 
144
- **2. 挂载插件**(解锁运行时命令、`/xray` 面板和 agent 工具):
172
+ **2. 挂载插件**(解锁运行时命令、X 光标签页、`/xray` 面板和 agent 工具):
145
173
 
146
174
  ```sh
147
175
  dsh plugin --profile web add dsh-xray
148
- # bundle 插件重启后生效——重启 dsh web
176
+ # bundle 插件下次启动生效——重启 dsh web
149
177
  ```
150
178
 
151
- 验证装好了:
179
+ 验证生效:
152
180
 
153
181
  ```sh
154
- dsh --profile web --dump-config | grep dsh-xray # 组合树里有这一行
155
- npx dsh-xray health # 能读到运行时快照
156
- # 然后打开 http://localhost:3080/xray 看实时面板
182
+ dsh --profile web --dump-config | grep dsh-xray # 组合树中出现该行
183
+ npx dsh-xray health # 读取运行时快照
184
+ # 然后打开任意会话点 X 光标签页,
185
+ # 或访问 http://localhost:3080/xray 看独立面板
157
186
  ```
158
187
 
159
188
  卸载:`dsh plugin --profile web remove dsh-xray`。
160
189
 
161
- ## 许可证
190
+ | 命令 | 行为 |
191
+ | --- | --- |
192
+ | `diff` | 两棵树不一致时退出码 `1` |
193
+ | `health` | 有插件不健康时退出码 `1` |
194
+ | `attribute`、`conflicts`、`snapshot` | 纯静态——dsh 起不来时照样能跑 |
195
+ | `deps`、`health` | 读取 `$DSH_HOME/xray/runtime.json` 运行时快照 |
196
+
197
+ ---
198
+
199
+ <p align="center">
200
+ <img src="./assets/section-capabilities.svg" width="100%" alt="能力总表">
201
+ </p>
202
+
203
+ 对运行中组合树的诊断成像——与 [dsh-doctor](https://www.npmjs.com/package/dsh-doctor)(救援与恢复)互补。
204
+
205
+ | 能力 | 类别 |
206
+ | --- | --- |
207
+ | 上下文税归因 & 条目原文查看 | 💰 优化 |
208
+ | 层归因 | 🔍 检视 |
209
+ | 声明 vs 实际 diff | 🔍 检视 |
210
+ | 冲突检测 | 🔍 检视 |
211
+ | 组合快照 | 📦 导出 |
212
+ | 服务依赖图 | 🌐 运行时 |
213
+ | 运行时健康 | 🌐 运行时 |
214
+ | 服务遮蔽 | 🌐 运行时 |
215
+ | Agent 自省 | 🤖 AI |
216
+ | 能力审计 | 🛡️ 安全 |
217
+
218
+ ---
219
+
220
+ ## 许可
162
221
 
163
- MIT
222
+ [MIT](./LICENSE)
package/assets/hero.svg CHANGED
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="900" height="360" viewBox="0 0 900 360">
2
+ <title>dsh-xray — X-ray for your DeepSeek Harness</title>
2
3
  <defs>
3
4
  <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
5
  <stop offset="0%" style="stop-color:#0a0e17"/>
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="260" height="60" viewBox="0 0 260 60">
2
+ <title>Agent Tool</title>
2
3
  <defs>
3
4
  <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
5
  <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="288" height="60" viewBox="0 0 288 60">
2
+ <title>Capabilities</title>
2
3
  <defs>
3
4
  <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
5
  <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="288" height="60" viewBox="0 0 288 60">
2
+ <title>CLI Commands</title>
2
3
  <defs>
3
4
  <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
5
  <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="288" height="60" viewBox="0 0 288 60">
2
+ <title>Context Tax</title>
3
+ <defs>
4
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
5
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
6
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
7
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
8
+ </linearGradient>
9
+ </defs>
10
+ <line x1="0" y1="30" x2="288" y2="30" stroke="url(#line)" stroke-width="1"/>
11
+ <g transform="translate(144, 30)">
12
+ <rect x="-114" y="-16" width="228" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
13
+ <text x="0" y="5" text-anchor="middle" fill="#00d4aa" font-family="'SF Mono', 'Fira Code', monospace" font-size="14" font-weight="600" letter-spacing="1">▸ CONTEXT TAX</text>
14
+ </g>
15
+ </svg>
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="232" height="60" viewBox="0 0 232 60">
2
+ <title>Features</title>
2
3
  <defs>
3
4
  <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
5
  <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="218" height="60" viewBox="0 0 218 60">
2
+ <title>Install</title>
2
3
  <defs>
3
4
  <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
5
  <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="302" height="60" viewBox="0 0 302 60">
2
+ <title>Safety Stance</title>
2
3
  <defs>
3
4
  <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
5
  <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
Binary file
Binary file
Binary file
package/docs/demo.svg CHANGED
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="720" height="446" viewBox="0 0 720 446" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="12.5">
2
+ <title>dsh-xray CLI demo</title>
2
3
  <rect width="720" height="446" rx="8" fill="#0d1117" stroke="#30363d"/>
3
4
  <circle cx="20" cy="17" r="5.5" fill="#ff5f56"/><circle cx="40" cy="17" r="5.5" fill="#ffbd2e"/><circle cx="60" cy="17" r="5.5" fill="#27c93f"/>
4
5
  <text x="360" y="21" fill="#8b949e" text-anchor="middle" font-size="11.5">dsh-xray — composition X-ray for DeepSeek Harness</text>
package/lib/client.js CHANGED
@@ -68,7 +68,7 @@ window.__ModuleLoader__.load({
68
68
  //#endregion
69
69
 
70
70
  //#region tiny view primitives
71
- const VIEWS = ['summary', 'health', 'deps', 'cost', 'shadow'];
71
+ const VIEWS = ['summary', 'health', 'deps', 'cost', 'shadow', 'skills'];
72
72
  const NS = 'xray';
73
73
 
74
74
  /** English messages (also the key vocabulary; zh mirrors every key). */
@@ -150,6 +150,25 @@ window.__ModuleLoader__.load({
150
150
  'cost.noAssembly': 'no prompt assembly observed yet — send one agent message first',
151
151
  'cost.unattributed': 'unattributed',
152
152
  'shadow.clean': 'no service is provided by more than one plugin',
153
+ 'intro.skills':
154
+ 'What each skill costs: its catalog line rides every request once any model-invocable skill exists; its body is billed only when loaded. Pricing only — to enable/disable skills, use a skill manager.',
155
+ 'tip.catalog':
156
+ "Tokens of this skill's line in the durable session catalog, carried on every request",
157
+ 'tip.body':
158
+ 'Tokens of the full rendered skill body, billed per load and then resident in history',
159
+ 'tip.invocable':
160
+ 'Whether the model may load this skill itself; user-only skills stay out of the catalog',
161
+ 'col.skill': 'skill',
162
+ 'col.provider2': 'provider',
163
+ 'col.catalog': 'catalog',
164
+ 'col.body': 'body',
165
+ 'col.invocable': 'invocable',
166
+ 'skills.summary':
167
+ '{count} skill(s), {invocable} model-invocable · resident catalog ~{resident} tokens (~{entries} entries + ~{framing} framing) on every request',
168
+ 'skills.none':
169
+ 'no skill observation yet — the skills service is absent or discovery has not run',
170
+ 'skills.yes': 'yes',
171
+ 'skills.userOnly': 'user-only',
153
172
  'entry.loading': 'loading entry…',
154
173
  'entry.stats': '{chars} chars · ~{tokens} tokens ({estimator})',
155
174
  'entry.close': 'close',
@@ -230,6 +249,21 @@ window.__ModuleLoader__.load({
230
249
  'cost.noAssembly': '尚未观测到 prompt 装配——先发送一条 agent 消息',
231
250
  'cost.unattributed': '未归因',
232
251
  'shadow.clean': '没有服务被多个插件同时提供',
252
+ 'intro.skills':
253
+ '每个 skill 的价格:只要存在模型可调用的 skill,它的 catalog 行就随每次请求发送;正文只在加载时计费。这里只计价——启用/禁用请使用 skill 管理器。',
254
+ 'tip.catalog': '该 skill 在持久会话 catalog 中那一行的 token,每次请求都携带',
255
+ 'tip.body': '完整渲染正文的 token,每次加载时计费,之后驻留在历史中',
256
+ 'tip.invocable': '模型能否自行加载该 skill;仅用户可调用的 skill 不进入 catalog',
257
+ 'col.skill': 'skill',
258
+ 'col.provider2': '提供者',
259
+ 'col.catalog': 'catalog 行',
260
+ 'col.body': '正文',
261
+ 'col.invocable': '可调用',
262
+ 'skills.summary':
263
+ '{count} 个 skill,{invocable} 个模型可调用 · 常驻 catalog 约 {resident} tokens(条目约 {entries} + 框架约 {framing}),每次请求都携带',
264
+ 'skills.none': '尚无 skill 观测——skills 服务缺失或发现未运行',
265
+ 'skills.yes': '是',
266
+ 'skills.userOnly': '仅用户',
233
267
  'entry.loading': '正在加载条目…',
234
268
  'entry.stats': '{chars} 字符 · 约 {tokens} tokens({estimator})',
235
269
  'entry.close': '关闭',
@@ -633,6 +667,40 @@ window.__ModuleLoader__.load({
633
667
  )
634
668
  : null,
635
669
  ),
670
+
671
+ skills: (d, t) =>
672
+ !d.available
673
+ ? h('p', { className: 'xray-muted' }, t('skills.none'))
674
+ : h(
675
+ react.Fragment,
676
+ null,
677
+ h(
678
+ 'p',
679
+ null,
680
+ fill(t('skills.summary'), {
681
+ count: d.totals.count,
682
+ invocable: d.totals.invocable,
683
+ resident: d.totals.residentTokens,
684
+ entries: d.totals.catalogEntryTokens,
685
+ framing: d.totals.catalogOverheadTokens,
686
+ }),
687
+ ),
688
+ h(Table, {
689
+ t,
690
+ headers: ['skill', 'provider2', 'catalog', 'body', 'invocable'],
691
+ rows: d.skills.map((s) =>
692
+ Row(s.name, [
693
+ s.name,
694
+ { cls: 'xray-muted', text: s.provider ?? '—' },
695
+ { cls: 'xray-num', text: `~${s.catalogTokens}` },
696
+ { cls: 'xray-num', text: s.bodyTokens === null ? '—' : `~${s.bodyTokens}` },
697
+ s.modelInvocable
698
+ ? h('span', { className: 'xray-ok' }, t('skills.yes'))
699
+ : h('span', { className: 'xray-muted' }, t('skills.userOnly')),
700
+ ]),
701
+ ),
702
+ }),
703
+ ),
636
704
  };
637
705
  //#endregion
638
706
 
@@ -207,10 +207,75 @@ function snapshotEntry(ctx, kind, name) {
207
207
  }
208
208
  }
209
209
 
210
+ /**
211
+ * Estimate every skill's context cost from the live skills registry.
212
+ * Async (discovery may hit the filesystem); callers cache the result.
213
+ *
214
+ * Two prices per skill:
215
+ * - `catalogTokens` — its line in the durable session catalog
216
+ * (`- \`name\`: description`, description normalized/truncated the way
217
+ * dsh-tool-skill renders it). This line rides EVERY request once any
218
+ * model-invocable skill exists.
219
+ * - `bodyTokens` — the full rendered body, billed only when the skill is
220
+ * loaded (per call, then resident in history). Read via skills.get;
221
+ * unreadable bodies report null rather than a guess.
222
+ *
223
+ * `catalogOverheadTokens` prices the fixed catalog framing (the
224
+ * system-reminder prose around the entries).
225
+ */
226
+ async function snapshotSkills(ctx, options = {}) {
227
+ const skills = ctx.get?.('skills');
228
+ if (!skills?.list) return null;
229
+ const maxLength = options.descriptionMaxLength ?? 120;
230
+ const catalogLine = (name, description) => {
231
+ const normalized = String(description ?? '')
232
+ .replaceAll(/\s+/g, ' ')
233
+ .trim();
234
+ const truncated =
235
+ normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
236
+ return `- \`${name}\`: ${truncated}`;
237
+ };
238
+ // The fixed framing dsh-tool-skill wraps around the entry lines.
239
+ const FRAMING =
240
+ "<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n</system-reminder>";
241
+ try {
242
+ const list = await skills.list(options.lookup ?? {});
243
+ const rows = [];
244
+ for (const summary of list) {
245
+ const line = catalogLine(summary.name, summary.description);
246
+ const row = {
247
+ name: summary.name,
248
+ provider: summary.provider ?? null,
249
+ source: summary.source ?? null,
250
+ modelInvocable: summary.invocation?.modelInvocable !== false,
251
+ catalogLine: line,
252
+ catalogTokens: estimateTokens(line),
253
+ bodyTokens: null,
254
+ };
255
+ try {
256
+ const full = await skills.get(summary.name, options.lookup ?? {});
257
+ if (full?.content !== undefined) row.bodyTokens = estimateTokens(String(full.content));
258
+ else if (typeof full?.body === 'string') row.bodyTokens = estimateTokens(full.body);
259
+ } catch {
260
+ /* body unreadable: null, never a guess */
261
+ }
262
+ rows.push(row);
263
+ }
264
+ return {
265
+ capturedAt: new Date().toISOString(),
266
+ catalogOverheadTokens: estimateTokens(FRAMING),
267
+ skills: rows,
268
+ };
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
210
274
  module.exports = {
211
275
  snapshotRegistry,
212
276
  snapshotTools,
213
277
  snapshotEntry,
278
+ snapshotSkills,
214
279
  injectNames,
215
280
  provideNames,
216
281
  stateName,
package/lib/index.js CHANGED
@@ -4,11 +4,12 @@ const os = require('node:os');
4
4
  const {
5
5
  snapshotRegistry,
6
6
  snapshotEntry,
7
+ snapshotSkills,
7
8
  stateName,
8
9
  estimateTokens,
9
10
  } = require('./collect/runtime.js');
10
11
  const { installSectionAttribution } = require('./collect/attribution.js');
11
- const { serviceGraph, health, shadowing, contextCost } = require('./model.js');
12
+ const { serviceGraph, health, shadowing, contextCost, skillCost } = require('./model.js');
12
13
 
13
14
  const name = 'dsh-xray';
14
15
 
@@ -34,6 +35,26 @@ function apply(ctx) {
34
35
  const transitions = new Map(); // plugin name -> [{state, at}] ring buffer
35
36
  let lastAssembly = null; // latest system-prompt assembly observation
36
37
  let attribution = null; // section name -> plugin name (live table)
38
+ let skillCatalog = null; // latest skill-cost observation (async, throttled)
39
+ let skillRefreshing = false;
40
+ const refreshSkills = (lookup) => {
41
+ // Discovery hits the filesystem and is scope/cwd-sensitive: the
42
+ // filesystem provider resolves user/project roots from the viewing
43
+ // agent, so a bare-context list() sees nothing. We piggyback on the
44
+ // assemble waterfall, whose context carries the agent scope (and the
45
+ // session cwd through it). One refresh in flight; failures keep
46
+ // last-good state.
47
+ if (skillRefreshing) return;
48
+ skillRefreshing = true;
49
+ snapshotSkills(ctx, { lookup })
50
+ .then((observed) => {
51
+ if (observed) skillCatalog = observed;
52
+ })
53
+ .catch(() => {})
54
+ .finally(() => {
55
+ skillRefreshing = false;
56
+ });
57
+ };
37
58
 
38
59
  let timer = null;
39
60
  const writeSnapshot = () => {
@@ -44,6 +65,7 @@ function apply(ctx) {
44
65
  snap.promptAssembly = lastAssembly;
45
66
  snap.sectionOwners = attribution ? Object.fromEntries(attribution.table) : {};
46
67
  snap.toolOwners = attribution ? Object.fromEntries(attribution.toolTable) : {};
68
+ snap.skillCatalog = skillCatalog;
47
69
  fs.mkdirSync(dir, { recursive: true });
48
70
  const tmp = `${file}.tmp`;
49
71
  fs.writeFileSync(tmp, JSON.stringify(snap, null, 2));
@@ -73,7 +95,7 @@ function apply(ctx) {
73
95
  // system-prompt/assemble is an expert waterfall: delegate via next(),
74
96
  // then observe the final assembly. Purely observational — the assembly
75
97
  // is returned unmodified.
76
- const disposeAssemble = ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
98
+ const disposeAssemble = ctx.on('system-prompt/assemble', async (assembly, context, next) => {
77
99
  const result = await next();
78
100
  try {
79
101
  lastAssembly = {
@@ -86,10 +108,20 @@ function apply(ctx) {
86
108
  } catch {
87
109
  /* observation must never break assembly */
88
110
  }
111
+ try {
112
+ const agent = context?.agent ?? context?.scope;
113
+ refreshSkills({
114
+ ...(context?.scope !== undefined ? { scope: context.scope } : {}),
115
+ ...(agent?.session?.header?.cwd !== undefined ? { cwd: agent.session.header.cwd } : {}),
116
+ }); // piggyback: one throttled async catalog refresh per assembly, in the assembling agent's view
117
+ } catch {
118
+ /* skill observation must never break assembly */
119
+ }
89
120
  schedule();
90
121
  return result;
91
122
  });
92
123
  schedule(); // initial snapshot
124
+ refreshSkills(); // initial skill-cost observation
93
125
  // Section attribution: diff-based name->plugin table over
94
126
  // system-prompt/change (see collect/attribution.js for the strategy).
95
127
  attribution = installSectionAttribution(ctx);
@@ -120,6 +152,7 @@ function apply(ctx) {
120
152
  snap.promptAssembly = lastAssembly;
121
153
  snap.sectionOwners = attribution ? Object.fromEntries(attribution.table) : {};
122
154
  snap.toolOwners = attribution ? Object.fromEntries(attribution.toolTable) : {};
155
+ snap.skillCatalog = skillCatalog;
123
156
  return snap;
124
157
  };
125
158
  wctx.effect(
@@ -141,6 +174,7 @@ function apply(ctx) {
141
174
  health: () => health(freshSnap()),
142
175
  cost: () => contextCost(freshSnap()),
143
176
  shadow: () => shadowing(freshSnap()),
177
+ skills: () => skillCost(freshSnap()),
144
178
  },
145
179
  // Entry text is computed per request from the live registries and
146
180
  // never persisted — the audit answer to "what exactly is ~N tokens?".
@@ -185,7 +219,7 @@ function apply(ctx) {
185
219
  parameters: {
186
220
  view: {
187
221
  type: 'string',
188
- description: 'summary | deps | health | cost | shadow (default summary)',
222
+ description: 'summary | deps | health | cost | shadow | skills (default summary)',
189
223
  },
190
224
  },
191
225
  output: {
@@ -198,10 +232,12 @@ function apply(ctx) {
198
232
  snap.promptAssembly = lastAssembly;
199
233
  snap.sectionOwners = attribution ? Object.fromEntries(attribution.table) : {};
200
234
  snap.toolOwners = attribution ? Object.fromEntries(attribution.toolTable) : {};
235
+ snap.skillCatalog = skillCatalog;
201
236
  if (args.view === 'deps') return serviceGraph(snap);
202
237
  if (args.view === 'health') return health(snap);
203
238
  if (args.view === 'cost') return contextCost(snap);
204
239
  if (args.view === 'shadow') return shadowing(snap);
240
+ if (args.view === 'skills') return skillCost(snap);
205
241
  return {
206
242
  plugins: snap.plugins.length,
207
243
  unhealthy: health(snap).unhealthy.length,
package/lib/model.js CHANGED
@@ -340,6 +340,43 @@ function contextCost(snap) {
340
340
  };
341
341
  }
342
342
 
343
+ /** F10: skill context cost — the catalog line every request carries per
344
+ * skill, plus each skill's on-load body size. Read-only companion to the
345
+ * ecosystem's skill managers: xray prices, it never toggles. */
346
+ function skillCost(snap) {
347
+ const observed = snap.skillCatalog;
348
+ if (!observed) return { available: false, skills: [], totals: null, capturedAt: snap.capturedAt };
349
+ const rows = observed.skills
350
+ .slice()
351
+ .sort((a, b) => b.catalogTokens - a.catalogTokens)
352
+ .map((s) => ({
353
+ name: s.name,
354
+ provider: s.provider,
355
+ source: s.source,
356
+ modelInvocable: s.modelInvocable,
357
+ catalogTokens: s.catalogTokens,
358
+ bodyTokens: s.bodyTokens,
359
+ }));
360
+ const invocable = rows.filter((s) => s.modelInvocable);
361
+ const catalogEntryTokens = invocable.reduce((sum, s) => sum + s.catalogTokens, 0);
362
+ return {
363
+ available: true,
364
+ skills: rows,
365
+ totals: {
366
+ count: rows.length,
367
+ invocable: invocable.length,
368
+ // Only model-invocable skills enter the durable catalog; its framing
369
+ // is a fixed one-off on top of the per-skill lines.
370
+ catalogEntryTokens,
371
+ catalogOverheadTokens: observed.catalogOverheadTokens,
372
+ residentTokens:
373
+ invocable.length > 0 ? catalogEntryTokens + observed.catalogOverheadTokens : 0,
374
+ },
375
+ observedAt: observed.capturedAt,
376
+ capturedAt: snap.capturedAt,
377
+ };
378
+ }
379
+
343
380
  module.exports = {
344
381
  replayLayers,
345
382
  attribute,
@@ -350,4 +387,5 @@ module.exports = {
350
387
  health,
351
388
  shadowing,
352
389
  contextCost,
390
+ skillCost,
353
391
  };
package/lib/panel.js CHANGED
@@ -38,7 +38,7 @@ const PAGE = `<!doctype html>
38
38
  <div id="status"></div>
39
39
  <div id="content"></div>
40
40
  <script>
41
- const views = ['summary', 'health', 'deps', 'cost', 'shadow'];
41
+ const views = ['summary', 'health', 'deps', 'cost', 'shadow', 'skills'];
42
42
  const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
43
43
  const escAttr = (s) => esc(s).replace(/"/g, '&quot;');
44
44
 
@@ -49,6 +49,7 @@ const INTRO = {
49
49
  deps: 'Who provides and consumes each service. The disable-cascade table answers: if I disable this plugin, which dependents stop working with it?',
50
50
  cost: 'What every LLM request carries before your message: prompt sections + tool schemas, attributed to the plugin that registered each. "By plugin" is each plugin\\'s per-request context tax.',
51
51
  shadow: 'Same-name registrations. A service provided by two plugins means one silently wins — usually intended (an override), occasionally a conflict.',
52
+ skills: 'What each skill costs: its catalog line rides every request once any model-invocable skill exists; its body is billed only when loaded. Pricing only — to enable/disable skills, use a skill manager.',
52
53
  };
53
54
 
54
55
  // Hover glossary: term -> plain-language meaning (native title tooltips).
@@ -65,6 +66,9 @@ const TIPS = {
65
66
  registrations: 'How many tools/commands this plugin registered on the shared registries',
66
67
  sections: 'Prompt sections this plugin contributes to the system prompt',
67
68
  tools: 'Tool schemas this plugin registers (each costs context on every request)',
69
+ catalog: 'Tokens of this skill's line in the durable session catalog, carried on every request',
70
+ body: 'Tokens of the full rendered skill body, billed per load and then resident in history',
71
+ invocable: 'Whether the model may load this skill itself; user-only skills stay out of the catalog',
68
72
  };
69
73
  const th = (h) => '<th' + (TIPS[h] ? ' title="' + escAttr(TIPS[h]) + '"' : '') + '>' + esc(h) + '</th>';
70
74
  const intro = (v) => '<p class="muted" style="margin:0 0 10px">' + INTRO[v] + '</p>';
@@ -166,6 +170,17 @@ const renderers = {
166
170
  }
167
171
  return html;
168
172
  },
173
+ skills(d) {
174
+ if (!d.available) return '<p class="muted">no skill observation yet — the skills service is absent or discovery has not run</p>';
175
+ let html = '<p>' + d.totals.count + ' skill(s), ' + d.totals.invocable + ' model-invocable · resident catalog ~'
176
+ + d.totals.residentTokens + ' tokens (' + '~' + d.totals.catalogEntryTokens + ' entries + ~'
177
+ + d.totals.catalogOverheadTokens + ' framing) on every request</p>';
178
+ html += table(['skill', 'provider', 'catalog', 'body', 'invocable'], d.skills.map((s) =>
179
+ '<tr><td>' + esc(s.name) + '</td><td class="muted">' + esc(s.provider ?? '—')
180
+ + '</td><td class="num">~' + s.catalogTokens + '</td><td class="num">' + (s.bodyTokens === null ? '—' : '~' + s.bodyTokens)
181
+ + '</td><td>' + (s.modelInvocable ? '<span class="ok">yes</span>' : '<span class="muted">user-only</span>') + '</td></tr>'));
182
+ return html;
183
+ },
169
184
  };
170
185
 
171
186
  async function render() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xray",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "X-ray for your DeepSeek Harness — see what's actually loaded, why, and what it costs you.",
5
5
  "repository": {
6
6
  "type": "git",