dsh-xray 0.3.2 → 0.4.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/README.md CHANGED
@@ -1,18 +1,39 @@
1
- # dsh-xray
1
+ <p align="center">
2
+ <img src="./assets/hero.svg" width="100%" alt="dsh-xray — X-ray for your DeepSeek Harness">
3
+ </p>
2
4
 
3
- [![npm](https://img.shields.io/npm/v/dsh-xray)](https://www.npmjs.com/package/dsh-xray)
4
- [![CI](https://github.com/alloevil/dsh-xray/actions/workflows/check.yml/badge.svg)](https://github.com/alloevil/dsh-xray/actions/workflows/check.yml)
5
- [![license](https://img.shields.io/npm/l/dsh-xray)](./LICENSE)
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/dsh-xray"><img src="https://img.shields.io/npm/v/dsh-xray?style=flat-square&color=00d4aa" alt="npm"></a>
7
+ <a href="https://github.com/alloevil/dsh-xray/actions/workflows/check.yml"><img src="https://img.shields.io/github/actions/workflow/status/alloevil/dsh-xray/check.yml?style=flat-square&label=CI" alt="CI"></a>
8
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/dsh-xray?style=flat-square" alt="license"></a>
9
+ <img src="https://img.shields.io/badge/language-JavaScript-f7df1e?style=flat-square&logo=javascript&logoColor=black" alt="JavaScript">
10
+ </p>
6
11
 
7
- X-ray for your [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — see what's actually loaded, why, and what it costs you.
12
+ <p align="center">
13
+ <strong>X-ray for your <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a></strong> — see what's actually loaded, why, and what it costs you.
14
+ </p>
8
15
 
9
- [中文](./README.zh.md)
16
+ <p align="center">
17
+ <a href="./README.zh.md">🇨🇳 中文文档</a>
18
+ </p>
10
19
 
11
- > **Status: 0.2.x — static + runtime imaging.** Static commands work even when dsh cannot boot; `deps`/`health` and the agent tool need the plugin mounted.
20
+ ![dsh-xray demo](./docs/demo.svg)
12
21
 
13
- `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*. dsh-xray does.
22
+ ---
14
23
 
15
- ## CLI
24
+ ## The Problem
25
+
26
+ `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**.
27
+
28
+ **dsh-xray does.**
29
+
30
+ > **Status: 0.4.x — static + runtime imaging with context-cost attribution.** Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
31
+
32
+ ---
33
+
34
+ <p align="center">
35
+ <img src="./assets/section-cli.svg" width="100%" alt="CLI Commands">
36
+ </p>
16
37
 
17
38
  ```sh
18
39
  npx dsh-xray attribute # which layer introduced each row, and who patched it since
@@ -21,88 +42,131 @@ npx dsh-xray diff # declared (static layers) vs actual (dump-config) tree
21
42
  npx dsh-xray snapshot # content-addressed lockfile of the effective composition
22
43
  npx dsh-xray deps [svc] # service dependency graph: providers, consumers, disable-cascade
23
44
  npx dsh-xray health # plugin lifecycle health: failed fibers, pending injects, transitions
24
- npx dsh-xray cost # estimated context-token cost per model-facing tool schema
45
+ npx dsh-xray cost # context cost: prompt sections + tool schemas, estimated tokens
25
46
  npx dsh-xray shadow # services provided by multiple plugins
26
47
  npx dsh-xray audit # static scan of out-of-tree plugins for sensitive touchpoints
27
48
  ```
28
49
 
29
- All commands take `--profile <name>` (default `web`) and `--json`. `diff` exits `1` when the trees disagree; `health` exits `1` when any plugin is unhealthy. `attribute`, `conflicts`, and `snapshot` are fully static: they work even when dsh cannot start. `deps` and `health` read the runtime snapshot the mounted plugin maintains at `$DSH_HOME/xray/runtime.json`.
50
+ <p align="center">
51
+ <img src="./assets/section-features.svg" width="100%" alt="Features">
52
+ </p>
30
53
 
31
- ## What it looks like
54
+ <table>
55
+ <tr>
56
+ <td width="50%">
32
57
 
33
- Every row of the booted tree, attributed to the layer that introduced it — and who patched it since:
58
+ ### 🔍 Layer Attribution
59
+ Which layer introduced each active plugin: kernel bundle, profile dependency, `cordis.patch.yml` insert, or repository source.
34
60
 
35
- ```console
36
- $ npx dsh-xray attribute
37
- # 130 rows in profile "web"
61
+ ### 📊 Declared vs. Actual Diff
62
+ Installed-but-inactive, uninstalled-but-lingering patch rows — all surfaced.
38
63
 
39
- timer @deepseek-ai/dsh-base
40
- hmr @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app [disabled]
41
- llm @deepseek-ai/dsh-base
42
- session-query-sqlite @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app
43
- ...
44
- ```
64
+ ### ⚡ Conflict Detection
65
+ Plugins patching the same config row, and which one silently wins.
45
66
 
46
- What breaks if you disable a provider — computed from the live service store, not guesses:
67
+ ### 📸 Composition Snapshot
68
+ Export the effective composition as a lockfile; reproduce it elsewhere.
47
69
 
48
- ```console
49
- $ npx dsh-xray deps
50
- # disable-cascade (transitive consumers of each provider):
51
- Loader 5 plugin(s): AgentPresets, ClientModuleRegistry, Hmr, Include, PluginInventoryGateway
52
- TimerService 1 plugin(s): Hmr
53
- SessionProjectionRegistry → 1 plugin(s): SessionProjectionCache
54
- ```
70
+ </td>
71
+ <td width="50%">
72
+
73
+ ### 🌐 Service Dependency Graph
74
+ Who provides and consumes each service; what cascades if you disable X.
75
+
76
+ ### 💊 Runtime Health
77
+ Per-plugin fiber lifecycle state, startup failures, transition history.
78
+
79
+ ### 🤖 Agent Self-Introspection
80
+ The `xray_composition` tool lets agents inspect their own capability set.
55
81
 
56
- Which fields have multiple writers, and who silently wins:
82
+ What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
57
83
 
58
84
  ```console
59
- $ npx dsh-xray conflicts
60
- session-query-sqlite
61
- .config: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
62
- tool-bash
63
- .disabled: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
85
+ $ npx dsh-xray cost
86
+ ~1625 tokens: 1 tool schema(s) ~121 + 19 prompt section(s) ~1504
87
+
88
+ # prompt sections (observed at last assembly):
89
+ app:web-surface ~248 15.3% ████████
90
+ tool:goal ~184 11.3% ██████
91
+ tool:ralph ~109 6.7% ███
92
+ harness:source ~94 5.8% ███
93
+ ...
64
94
  ```
65
95
 
66
96
  And when a patch row targets an id that doesn't exist (dsh skips it silently), `diff` catches it:
67
97
 
68
- ```console
69
- $ npx dsh-xray diff
70
- orphan overrides (silently skipped) (1)
71
- no-such-row in ~/.dsh/profiles/web/cordis.patch.yml
72
- ```
98
+ ### 🛡️ Capability Audit
99
+ Heuristic static scan: network egress, shell, filesystem, env, eval.
73
100
 
74
- ## Agent tool
101
+ </td>
102
+ </tr>
103
+ </table>
75
104
 
76
- Mounted in the tree, dsh-xray registers an `xray_composition` tool (`view: summary | deps | health | cost | shadow`), so an agent can answer "what capabilities do I have / what plugin provides X / why is Y unavailable" about itself.
105
+ <p align="center">
106
+ <img src="./assets/section-agent.svg" width="100%" alt="Agent Tool">
107
+ </p>
77
108
 
78
- ## Safety stance
109
+ Mounted in the tree, dsh-xray registers an `xray_composition` tool (`view: summary | deps | health | cost | shadow`), so an agent can answer:
79
110
 
80
- dsh-xray reads; it never runs. Loader `!!js` expressions in patch files are parsed as opaque markers and never evaluated, the CLI never executes plugin code (`audit` is a pattern scan over source text), and the mounted plugin writes only under `$DSH_HOME/xray/`. See [SECURITY.md](./SECURITY.md).
111
+ > *"What capabilities do I have?" / "What plugin provides X?" / "Why is Y unavailable?"*
81
112
 
82
- ## Capabilities
113
+ about itself.
83
114
 
84
- Diagnostic imaging for a running composition — complementary to [dsh-doctor](https://www.npmjs.com/package/dsh-doctor) (rescue & recovery).
115
+ ---
116
+
117
+ <p align="center">
118
+ <img src="./assets/section-safety.svg" width="100%" alt="Safety Stance">
119
+ </p>
85
120
 
86
- Shipped in 0.3.x:
121
+ **dsh-xray reads; it never runs.**
87
122
 
88
- - **Layer attribution** which layer introduced each active plugin: kernel bundle, profile dependency, `cordis.patch.yml` insert, or repository source
89
- - **Declared vs. actual diff** installed-but-inactive, uninstalled-but-lingering patch rows
90
- - **Conflict detection** plugins patching the same config row, and which one silently wins
91
- - **Composition snapshot** — export the effective composition as a lockfile; reproduce it elsewhere
92
- - **Service dependency graph** — who provides and consumes each service; what cascades if you disable X (`deps`)
93
- - **Runtime health** — per-plugin fiber lifecycle state, startup failures, transition history (`health`)
94
- - **Agent self-introspection** — the `xray_composition` tool lets agents inspect their own capability set
123
+ - Loader `!!js` expressions in patch files are parsed as opaque markers and **never evaluated**
124
+ - The CLI **never executes** plugin code (`audit` is a pattern scan over source text)
125
+ - The mounted plugin writes only under `$DSH_HOME/xray/`
126
+ - See [SECURITY.md](./SECURITY.md)
95
127
 
96
- - **Capability audit** — heuristic static scan of out-of-tree plugins: network egress, shell, filesystem, env, eval (`audit`)
97
- - **Service shadowing** — services claimed by multiple plugins, per-plugin tool/command registrations (`shadow`)
98
- - **Context cost** — estimated tokens each model-facing tool schema occupies (`cost`)
128
+ ---
99
129
 
100
- ## Install
130
+ <p align="center">
131
+ <img src="./assets/section-install.svg" width="100%" alt="Install">
132
+ </p>
101
133
 
102
134
  ```sh
103
135
  dsh plugin --profile web add dsh-xray
104
136
  ```
105
137
 
138
+ All commands take `--profile <name>` (default `web`) and `--json`.
139
+
140
+ | Command | Behavior |
141
+ | --- | --- |
142
+ | `diff` | Exits `1` when the trees disagree |
143
+ | `health` | Exits `1` when any plugin is unhealthy |
144
+ | `attribute`, `conflicts`, `snapshot` | Fully static — work even when dsh cannot start |
145
+ | `deps`, `health` | Read runtime snapshot at `$DSH_HOME/xray/runtime.json` |
146
+
147
+ ---
148
+
149
+ <p align="center">
150
+ <img src="./assets/section-capabilities.svg" width="100%" alt="Capabilities">
151
+ </p>
152
+
153
+ Diagnostic imaging for a running composition — complementary to [dsh-doctor](https://www.npmjs.com/package/dsh-doctor) (rescue & recovery).
154
+
155
+ | Feature | Category |
156
+ | --- | --- |
157
+ | Layer attribution | 🔍 Inspection |
158
+ | Declared vs. actual diff | 🔍 Inspection |
159
+ | Conflict detection | 🔍 Inspection |
160
+ | Composition snapshot | 📦 Export |
161
+ | Service dependency graph | 🌐 Runtime |
162
+ | Runtime health | 🌐 Runtime |
163
+ | Agent self-introspection | 🤖 AI |
164
+ | Capability audit | 🛡️ Security |
165
+ | Service shadowing | 🌐 Runtime |
166
+ | Context cost | 💰 Optimization |
167
+
168
+ ---
169
+
106
170
  ## License
107
171
 
108
- MIT
172
+ [MIT](./LICENSE)
package/README.zh.md CHANGED
@@ -6,9 +6,11 @@
6
6
 
7
7
  给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 拍 X 光——看清到底加载了什么、为什么在那、以及它悄悄花掉了你什么。
8
8
 
9
+ ![dsh-xray 演示](./docs/demo.svg)
10
+
9
11
  [English](./README.md)
10
12
 
11
- > **状态:0.2.x — 静态 + 运行时成像。** 静态命令在 dsh 起不来时照样能用;`deps`/`health` 和 agent 工具需要插件已挂载。
13
+ > **状态:0.4.x — 静态 + 运行时成像,含上下文成本归因。** 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
12
14
 
13
15
  `dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件*为什么*在这、停用它会*连带瘫掉什么*、它在*悄悄消耗什么*。dsh-xray 回答这些。
14
16
 
@@ -21,7 +23,7 @@ npx dsh-xray diff # 声明(静态层)vs 实际(dump-config)组合树
21
23
  npx dsh-xray snapshot # 当前生效组合的内容寻址 lockfile
22
24
  npx dsh-xray deps [svc] # 服务依赖图:提供者、消费者、停用级联
23
25
  npx dsh-xray health # 插件生命周期健康:失败 fiber、等待中的注入、状态迁移史
24
- npx dsh-xray cost # 每个模型可见工具 schema 的估算 token 占用
26
+ npx dsh-xray cost # 上下文成本:prompt sections + 工具 schema 的估算 token 占用
25
27
  npx dsh-xray shadow # 被多个插件同时提供的服务
26
28
  npx dsh-xray audit # 对 out-of-tree 插件做敏感触点静态扫描
27
29
  ```
@@ -63,6 +65,20 @@ tool-bash
63
65
  .disabled: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
64
66
  ```
65
67
 
68
+ 每次请求实际携带什么——assembly 时观测到的 prompt sections,与工具 schema 合并计价:
69
+
70
+ ```console
71
+ $ npx dsh-xray cost
72
+ ~1625 tokens: 1 tool schema(s) ~121 + 19 prompt section(s) ~1504
73
+
74
+ # prompt sections (observed at last assembly):
75
+ app:web-surface ~248 15.3% ████████
76
+ tool:goal ~184 11.3% ██████
77
+ tool:ralph ~109 6.7% ███
78
+ harness:source ~94 5.8% ███
79
+ ...
80
+ ```
81
+
66
82
  patch 行指向不存在的 id 时(dsh 静默跳过),`diff` 能抓到:
67
83
 
68
84
  ```console
@@ -83,7 +99,7 @@ dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为
83
99
 
84
100
  对运行中组合树的诊断成像——与 [dsh-doctor](https://www.npmjs.com/package/dsh-doctor)(救援与恢复)互补。
85
101
 
86
- 0.3.x 已交付:
102
+ 0.4.x 已交付:
87
103
 
88
104
  - **来源归因** — 每个活跃插件来自哪一层:内核 bundle / profile 依赖 / `cordis.patch.yml` insert / repository 源
89
105
  - **声明 vs 实际 diff** — 装了但没生效、卸了但残留 patch 行
@@ -95,7 +111,7 @@ dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为
95
111
 
96
112
  - **能力审计** — 对 out-of-tree 插件的启发式静态扫描:网络外发、shell、文件系统、环境变量、动态求值(`audit`)
97
113
  - **服务重名检测** — 被多个插件同时提供的服务,及每插件工具/命令注册数(`shadow`)
98
- - **上下文成本** — 每个模型可见工具 schema 的估算 token 占用(`cost`)
114
+ - **上下文成本** — prompt sections(观测自 system-prompt/assemble)+ 工具 schema 的估算 token 占用(`cost`)
99
115
 
100
116
  ## 安装
101
117
 
@@ -0,0 +1,156 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="900" height="360" viewBox="0 0 900 360">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#0a0e17"/>
5
+ <stop offset="100%" style="stop-color:#111827"/>
6
+ </linearGradient>
7
+ <linearGradient id="scanLine" x1="0%" y1="0%" x2="100%" y2="0%">
8
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
9
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.6"/>
10
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
11
+ </linearGradient>
12
+ <linearGradient id="glow" x1="0%" y1="0%" x2="0%" y2="100%">
13
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0.15"/>
14
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
15
+ </linearGradient>
16
+ <filter id="blur">
17
+ <feGaussianBlur in="SourceGraphic" stdDeviation="2"/>
18
+ </filter>
19
+ <filter id="glowFilter">
20
+ <feGaussianBlur in="SourceGraphic" stdDeviation="4"/>
21
+ <feComposite in="SourceGraphic"/>
22
+ </filter>
23
+ </defs>
24
+
25
+ <!-- Background -->
26
+ <rect width="900" height="360" fill="url(#bg)" rx="12"/>
27
+
28
+ <!-- Grid pattern -->
29
+ <g opacity="0.06" stroke="#00d4aa" stroke-width="0.5">
30
+ <line x1="0" y1="0" x2="0" y2="360"/><line x1="50" y1="0" x2="50" y2="360"/>
31
+ <line x1="100" y1="0" x2="100" y2="360"/><line x1="150" y1="0" x2="150" y2="360"/>
32
+ <line x1="200" y1="0" x2="200" y2="360"/><line x1="250" y1="0" x2="250" y2="360"/>
33
+ <line x1="300" y1="0" x2="300" y2="360"/><line x1="350" y1="0" x2="350" y2="360"/>
34
+ <line x1="400" y1="0" x2="400" y2="360"/><line x1="450" y1="0" x2="450" y2="360"/>
35
+ <line x1="500" y1="0" x2="500" y2="360"/><line x1="550" y1="0" x2="550" y2="360"/>
36
+ <line x1="600" y1="0" x2="600" y2="360"/><line x1="650" y1="0" x2="650" y2="360"/>
37
+ <line x1="700" y1="0" x2="700" y2="360"/><line x1="750" y1="0" x2="750" y2="360"/>
38
+ <line x1="800" y1="0" x2="800" y2="360"/><line x1="850" y1="0" x2="850" y2="360"/>
39
+ <line x1="900" y1="0" x2="900" y2="360"/>
40
+ <line x1="0" y1="0" x2="900" y2="0"/><line x1="0" y1="50" x2="900" y2="50"/>
41
+ <line x1="0" y1="100" x2="900" y2="100"/><line x1="0" y1="150" x2="900" y2="150"/>
42
+ <line x1="0" y1="200" x2="900" y2="200"/><line x1="0" y1="250" x2="900" y2="250"/>
43
+ <line x1="0" y1="300" x2="900" y2="300"/><line x1="0" y1="350" x2="900" y2="350"/>
44
+ </g>
45
+
46
+ <!-- Scan lines (x-ray effect) -->
47
+ <rect x="0" y="120" width="900" height="1" fill="url(#scanLine)" opacity="0.4"/>
48
+ <rect x="0" y="180" width="900" height="2" fill="url(#scanLine)" opacity="0.6"/>
49
+ <rect x="0" y="240" width="900" height="1" fill="url(#scanLine)" opacity="0.3"/>
50
+
51
+ <!-- Vertical scan glow -->
52
+ <rect x="280" y="0" width="340" height="360" fill="url(#glow)"/>
53
+
54
+ <!-- Composition tree diagram (left side) -->
55
+ <g transform="translate(60, 80)" opacity="0.3" stroke="#00d4aa" stroke-width="1.5" fill="none">
56
+ <!-- Root node -->
57
+ <rect x="0" y="0" width="120" height="28" rx="4" stroke="#00d4aa" stroke-width="1"/>
58
+ <rect x="0" y="0" width="120" height="28" rx="4" fill="#00d4aa" opacity="0.08"/>
59
+ <text x="60" y="18" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="11">dsh-root</text>
60
+ <!-- Branches -->
61
+ <line x1="60" y1="28" x2="30" y2="60"/>
62
+ <line x1="60" y1="28" x2="60" y2="60"/>
63
+ <line x1="60" y1="28" x2="90" y2="60"/>
64
+ <!-- Child nodes -->
65
+ <rect x="0" y="60" width="60" height="22" rx="3" stroke="#00d4aa" stroke-width="1"/>
66
+ <text x="30" y="75" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="9">kernel</text>
67
+ <rect x="30" y="60" width="60" height="22" rx="3" stroke="#00d4aa" stroke-width="1"/>
68
+ <text x="60" y="75" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="9">profile</text>
69
+ <rect x="60" y="60" width="60" height="22" rx="3" stroke="#00d4aa" stroke-width="1"/>
70
+ <text x="90" y="75" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="9">plugin</text>
71
+ <!-- Deeper branches -->
72
+ <line x1="30" y1="82" x2="15" y2="110"/>
73
+ <line x1="30" y1="82" x2="45" y2="110"/>
74
+ <line x1="90" y1="82" x2="75" y2="110"/>
75
+ <line x1="90" y1="82" x2="105" y2="110"/>
76
+ <rect x="0" y="110" width="30" height="18" rx="2" stroke="#00d4aa" stroke-width="1"/>
77
+ <text x="15" y="123" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="8">svc</text>
78
+ <rect x="30" y="110" width="30" height="18" rx="2" stroke="#00d4aa" stroke-width="1"/>
79
+ <text x="45" y="123" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="8">cfg</text>
80
+ <rect x="60" y="110" width="30" height="18" rx="2" stroke="#00d4aa" stroke-width="1"/>
81
+ <text x="75" y="123" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="8">dep</text>
82
+ <rect x="90" y="110" width="30" height="18" rx="2" stroke="#00d4aa" stroke-width="1"/>
83
+ <text x="105" y="123" text-anchor="middle" fill="#00d4aa" font-family="monospace" font-size="8">ext</text>
84
+ </g>
85
+
86
+ <!-- Scan pulse ring (right side) -->
87
+ <g transform="translate(730, 160)">
88
+ <circle cx="0" cy="0" r="30" fill="none" stroke="#00d4aa" stroke-width="1" opacity="0.15"/>
89
+ <circle cx="0" cy="0" r="50" fill="none" stroke="#00d4aa" stroke-width="0.5" opacity="0.1"/>
90
+ <circle cx="0" cy="0" r="70" fill="none" stroke="#00d4aa" stroke-width="0.5" opacity="0.06"/>
91
+ <circle cx="0" cy="0" r="12" fill="#00d4aa" opacity="0.08" filter="url(#glowFilter)"/>
92
+ <circle cx="0" cy="0" r="5" fill="#00d4aa" opacity="0.4"/>
93
+ <!-- Data points around the scan -->
94
+ <circle cx="45" cy="-25" r="3" fill="#00d4aa" opacity="0.3"/>
95
+ <circle cx="-35" cy="40" r="2" fill="#00d4aa" opacity="0.25"/>
96
+ <circle cx="60" cy="15" r="2.5" fill="#00d4aa" opacity="0.2"/>
97
+ <circle cx="-20" cy="-55" r="2" fill="#00d4aa" opacity="0.2"/>
98
+ <circle cx="30" cy="50" r="3" fill="#00d4aa" opacity="0.15"/>
99
+ <!-- Connection lines -->
100
+ <line x1="5" y1="-5" x2="45" y2="-25" stroke="#00d4aa" stroke-width="0.5" opacity="0.2"/>
101
+ <line x1="-5" y1="5" x2="-35" y2="40" stroke="#00d4aa" stroke-width="0.5" opacity="0.2"/>
102
+ <line x1="8" y1="0" x2="60" y2="15" stroke="#00d4aa" stroke-width="0.5" opacity="0.2"/>
103
+ </g>
104
+
105
+ <!-- Category label -->
106
+ <text x="450" y="80" text-anchor="middle" fill="#00d4aa" font-family="'SF Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="13" letter-spacing="4" opacity="0.7">DIAGNOSTIC IMAGING</text>
107
+
108
+ <!-- Main title -->
109
+ <text x="450" y="140" text-anchor="middle" fill="#ffffff" font-family="'Inter', 'SF Pro Display', -apple-system, sans-serif" font-size="56" font-weight="700" letter-spacing="-1">dsh-xray</text>
110
+
111
+ <!-- Subtitle -->
112
+ <text x="450" y="178" text-anchor="middle" fill="#94a3b8" font-family="'Inter', -apple-system, sans-serif" font-size="18" font-weight="400">X-ray for your DeepSeek Harness</text>
113
+
114
+ <!-- Tagline -->
115
+ <text x="450" y="215" text-anchor="middle" fill="#64748b" font-family="'Inter', -apple-system, sans-serif" font-size="14">see what's actually loaded, why, and what it costs you</text>
116
+
117
+ <!-- Status badge -->
118
+ <g transform="translate(450, 250)">
119
+ <rect x="-100" y="-12" width="200" height="24" rx="12" fill="#00d4aa" opacity="0.1" stroke="#00d4aa" stroke-width="0.5" stroke-opacity="0.3"/>
120
+ <text x="0" y="4" text-anchor="middle" fill="#00d4aa" font-family="'SF Mono', monospace" font-size="11">v0.2.x · static + runtime</text>
121
+ </g>
122
+
123
+ <!-- Feature pills -->
124
+ <g transform="translate(450, 295)">
125
+ <g transform="translate(-220, 0)">
126
+ <rect x="-40" y="-10" width="80" height="20" rx="10" fill="#1e293b" stroke="#334155" stroke-width="0.5"/>
127
+ <text x="0" y="4" text-anchor="middle" fill="#94a3b8" font-family="'SF Mono', monospace" font-size="10">attribute</text>
128
+ </g>
129
+ <g transform="translate(-110, 0)">
130
+ <rect x="-40" y="-10" width="80" height="20" rx="10" fill="#1e293b" stroke="#334155" stroke-width="0.5"/>
131
+ <text x="0" y="4" text-anchor="middle" fill="#94a3b8" font-family="'SF Mono', monospace" font-size="10">conflicts</text>
132
+ </g>
133
+ <g transform="translate(0, 0)">
134
+ <rect x="-40" y="-10" width="80" height="20" rx="10" fill="#1e293b" stroke="#334155" stroke-width="0.5"/>
135
+ <text x="0" y="4" text-anchor="middle" fill="#94a3b8" font-family="'SF Mono', monospace" font-size="10">snapshot</text>
136
+ </g>
137
+ <g transform="translate(110, 0)">
138
+ <rect x="-40" y="-10" width="80" height="20" rx="10" fill="#1e293b" stroke="#334155" stroke-width="0.5"/>
139
+ <text x="0" y="4" text-anchor="middle" fill="#94a3b8" font-family="'SF Mono', monospace" font-size="10">health</text>
140
+ </g>
141
+ <g transform="translate(220, 0)">
142
+ <rect x="-40" y="-10" width="80" height="20" rx="10" fill="#1e293b" stroke="#334155" stroke-width="0.5"/>
143
+ <text x="0" y="4" text-anchor="middle" fill="#94a3b8" font-family="'SF Mono', monospace" font-size="10">cost</text>
144
+ </g>
145
+ </g>
146
+
147
+ <!-- Corner accent lines -->
148
+ <line x1="20" y1="20" x2="60" y2="20" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
149
+ <line x1="20" y1="20" x2="20" y2="60" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
150
+ <line x1="880" y1="20" x2="840" y2="20" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
151
+ <line x1="880" y1="20" x2="880" y2="60" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
152
+ <line x1="20" y1="340" x2="60" y2="340" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
153
+ <line x1="20" y1="340" x2="20" y2="300" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
154
+ <line x1="880" y1="340" x2="840" y2="340" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
155
+ <line x1="880" y1="340" x2="880" y2="300" stroke="#00d4aa" stroke-width="1" opacity="0.3"/>
156
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="260" height="60" viewBox="0 0 260 60">
2
+ <defs>
3
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
5
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
6
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <line x1="0" y1="30" x2="260" y2="30" stroke="url(#line)" stroke-width="1"/>
10
+ <g transform="translate(130, 30)">
11
+ <rect x="-100" y="-16" width="200" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
12
+ <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">◈ AGENT TOOL</text>
13
+ </g>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="288" height="60" viewBox="0 0 288 60">
2
+ <defs>
3
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
5
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
6
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <line x1="0" y1="30" x2="288" y2="30" stroke="url(#line)" stroke-width="1"/>
10
+ <g transform="translate(144, 30)">
11
+ <rect x="-114" y="-16" width="228" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
12
+ <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">▦ CAPABILITIES</text>
13
+ </g>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="288" height="60" viewBox="0 0 288 60">
2
+ <defs>
3
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
5
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
6
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <line x1="0" y1="30" x2="288" y2="30" stroke="url(#line)" stroke-width="1"/>
10
+ <g transform="translate(144, 30)">
11
+ <rect x="-114" y="-16" width="228" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
12
+ <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">▸ CLI COMMANDS</text>
13
+ </g>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="232" height="60" viewBox="0 0 232 60">
2
+ <defs>
3
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
5
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
6
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <line x1="0" y1="30" x2="232" y2="30" stroke="url(#line)" stroke-width="1"/>
10
+ <g transform="translate(116, 30)">
11
+ <rect x="-86" y="-16" width="172" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
12
+ <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">◆ FEATURES</text>
13
+ </g>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="218" height="60" viewBox="0 0 218 60">
2
+ <defs>
3
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
5
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
6
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <line x1="0" y1="30" x2="218" y2="30" stroke="url(#line)" stroke-width="1"/>
10
+ <g transform="translate(109, 30)">
11
+ <rect x="-79" y="-16" width="158" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
12
+ <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">▷ INSTALL</text>
13
+ </g>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="302" height="60" viewBox="0 0 302 60">
2
+ <defs>
3
+ <linearGradient id="line" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" style="stop-color:#00d4aa;stop-opacity:0"/>
5
+ <stop offset="50%" style="stop-color:#00d4aa;stop-opacity:0.4"/>
6
+ <stop offset="100%" style="stop-color:#00d4aa;stop-opacity:0"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <line x1="0" y1="30" x2="302" y2="30" stroke="url(#line)" stroke-width="1"/>
10
+ <g transform="translate(151, 30)">
11
+ <rect x="-121" y="-16" width="242" height="32" rx="16" fill="#0a0e17" stroke="#1e293b" stroke-width="1"/>
12
+ <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">◇ SAFETY STANCE</text>
13
+ </g>
14
+ </svg>
package/bin/xray.js CHANGED
@@ -183,11 +183,25 @@ function cmdCost(args) {
183
183
  const result = model.contextCost(snap);
184
184
  if (args.json) return console.log(JSON.stringify(result, null, 2));
185
185
  console.log(
186
- `~${result.totalTokens} tokens across ${result.toolCount} tool schema(s) (captured ${result.capturedAt})\n`,
186
+ `~${result.totalTokens} tokens: ${result.toolCount} tool schema(s) ~${result.toolTokens} + ${result.sectionCount} prompt section(s) ~${result.sectionTokens} (captured ${result.capturedAt})\n`,
187
187
  );
188
+ const bar = (share) => '█'.repeat(Math.max(1, Math.round(share / 2)));
189
+ if (result.sections.length) {
190
+ console.log('# prompt sections (observed at last assembly):');
191
+ for (const s of result.sections) {
192
+ console.log(
193
+ `${pad(s.name, 32)} ${pad(`~${s.tokens}`, 8)} ${pad(`${s.share}%`, 7)} ${bar(s.share)}`,
194
+ );
195
+ }
196
+ console.log();
197
+ } else {
198
+ console.log('# no prompt assembly observed yet — send one agent message first\n');
199
+ }
200
+ console.log('# tool schemas:');
188
201
  for (const t of result.tools) {
189
- const bar = '█'.repeat(Math.max(1, Math.round(t.share / 2)));
190
- console.log(`${pad(t.name, 28)} ${pad(`~${t.tokens}`, 8)} ${pad(`${t.share}%`, 7)} ${bar}`);
202
+ console.log(
203
+ `${pad(t.name, 32)} ${pad(`~${t.tokens}`, 8)} ${pad(`${t.share}%`, 7)} ${bar(t.share)}`,
204
+ );
191
205
  }
192
206
  }
193
207
 
package/docs/demo.svg ADDED
@@ -0,0 +1,22 @@
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
+ <rect width="720" height="446" rx="8" fill="#0d1117" stroke="#30363d"/>
3
+ <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
+ <text x="360" y="21" fill="#8b949e" text-anchor="middle" font-size="11.5">dsh-xray — composition X-ray for DeepSeek Harness</text>
5
+ <text x="16" y="63" fill="#7ee787">$ npx dsh-xray cost</text>
6
+ <text x="16" y="82" fill="#e6edf3">~1625 tokens: 1 tool schema(s) ~121 + 19 prompt section(s) ~1504</text>
7
+ <text x="16" y="120" fill="#8b949e"># prompt sections (observed at last assembly):</text>
8
+ <text x="16" y="139" fill="#e6edf3">app:web-surface ~248 15.3% ████████</text>
9
+ <text x="16" y="158" fill="#e6edf3">tool:goal ~184 11.3% ██████</text>
10
+ <text x="16" y="177" fill="#e6edf3">tool:ralph ~109 6.7% ███</text>
11
+ <text x="16" y="196" fill="#e6edf3">tool:glob ~98 6% ███</text>
12
+ <text x="16" y="215" fill="#e6edf3">harness:source ~94 5.8% ███</text>
13
+ <text x="16" y="234" fill="#8b949e">…</text>
14
+ <text x="16" y="272" fill="#7ee787">$ npx dsh-xray attribute</text>
15
+ <text x="16" y="291" fill="#8b949e"># 130 rows in profile "web"</text>
16
+ <text x="16" y="310" fill="#e6edf3">hmr @deepseek-ai/dsh-base ← patched by dsh-web-app [disabled]</text>
17
+ <text x="16" y="329" fill="#e6edf3">llm @deepseek-ai/dsh-base</text>
18
+ <text x="16" y="348" fill="#e6edf3">session-query @deepseek-ai/dsh-base ← patched by dsh-web-app</text>
19
+ <text x="16" y="386" fill="#7ee787">$ npx dsh-xray deps</text>
20
+ <text x="16" y="405" fill="#8b949e"># disable-cascade (transitive consumers of each provider):</text>
21
+ <text x="16" y="424" fill="#e6edf3"> Loader → 5 plugin(s): AgentPresets, ClientModuleRegistry, Hmr, …</text>
22
+ </svg>
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
  const os = require('node:os');
4
- const { snapshotRegistry, stateName } = require('./collect/runtime.js');
4
+ const { snapshotRegistry, stateName, estimateTokens } = require('./collect/runtime.js');
5
5
  const { serviceGraph, health, shadowing, contextCost } = require('./model.js');
6
6
 
7
7
  const name = 'dsh-xray';
@@ -26,6 +26,7 @@ function apply(ctx) {
26
26
  const dir = xrayDir();
27
27
  const file = path.join(dir, 'runtime.json');
28
28
  const transitions = new Map(); // plugin name -> [{state, at}] ring buffer
29
+ let lastAssembly = null; // latest system-prompt assembly observation
29
30
 
30
31
  let timer = null;
31
32
  const writeSnapshot = () => {
@@ -33,6 +34,7 @@ function apply(ctx) {
33
34
  try {
34
35
  const snap = snapshotRegistry(ctx);
35
36
  snap.transitions = Object.fromEntries(transitions);
37
+ snap.promptAssembly = lastAssembly;
36
38
  fs.mkdirSync(dir, { recursive: true });
37
39
  const tmp = `${file}.tmp`;
38
40
  fs.writeFileSync(tmp, JSON.stringify(snap, null, 2));
@@ -59,9 +61,29 @@ function apply(ctx) {
59
61
  }
60
62
  schedule();
61
63
  });
64
+ // system-prompt/assemble is an expert waterfall: delegate via next(),
65
+ // then observe the final assembly. Purely observational — the assembly
66
+ // is returned unmodified.
67
+ const disposeAssemble = ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
68
+ const result = await next();
69
+ try {
70
+ lastAssembly = {
71
+ at: Date.now(),
72
+ sections: (result?.sections ?? []).map((s) => ({
73
+ name: s.name,
74
+ tokens: estimateTokens(s.text ?? ''),
75
+ })),
76
+ };
77
+ } catch {
78
+ /* observation must never break assembly */
79
+ }
80
+ schedule();
81
+ return result;
82
+ });
62
83
  schedule(); // initial snapshot
63
84
  return [
64
85
  disposeStatus,
86
+ disposeAssemble,
65
87
  () => {
66
88
  clearTimeout(timer);
67
89
  writeSnapshot(); // final state on unload
@@ -111,6 +133,7 @@ function apply(ctx) {
111
133
  async execute(args) {
112
134
  const snap = snapshotRegistry(ctx);
113
135
  snap.transitions = Object.fromEntries(transitions);
136
+ snap.promptAssembly = lastAssembly;
114
137
  if (args.view === 'deps') return serviceGraph(snap);
115
138
  if (args.view === 'health') return health(snap);
116
139
  if (args.view === 'cost') return contextCost(snap);
package/lib/model.js CHANGED
@@ -277,18 +277,25 @@ function shadowing(snap) {
277
277
  return out;
278
278
  }
279
279
 
280
- /** F8: estimated context cost of each model-facing tool schema. */
280
+ /** F8: estimated context cost tool schemas plus prompt sections. */
281
281
  function contextCost(snap) {
282
282
  const tools = (snap.tools ?? []).slice().sort((a, b) => b.tokens - a.tokens);
283
- const total = tools.reduce((sum, t) => sum + t.tokens, 0);
283
+ const toolTokens = tools.reduce((sum, t) => sum + t.tokens, 0);
284
+ const sections = (snap.promptAssembly?.sections ?? [])
285
+ .slice()
286
+ .sort((a, b) => b.tokens - a.tokens);
287
+ const sectionTokens = sections.reduce((sum, s) => sum + s.tokens, 0);
288
+ const total = toolTokens + sectionTokens;
289
+ const share = (n) => (total ? Math.round((n / total) * 1000) / 10 : 0);
284
290
  return {
285
291
  totalTokens: total,
292
+ toolTokens,
293
+ sectionTokens,
286
294
  toolCount: tools.length,
287
- tools: tools.map((t) => ({
288
- name: t.name,
289
- tokens: t.tokens,
290
- share: total ? Math.round((t.tokens / total) * 1000) / 10 : 0,
291
- })),
295
+ sectionCount: sections.length,
296
+ tools: tools.map((t) => ({ name: t.name, tokens: t.tokens, share: share(t.tokens) })),
297
+ sections: sections.map((s) => ({ name: s.name, tokens: s.tokens, share: share(s.tokens) })),
298
+ promptObservedAt: snap.promptAssembly?.at ?? null,
292
299
  capturedAt: snap.capturedAt,
293
300
  };
294
301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xray",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
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",
@@ -23,7 +23,9 @@
23
23
  "bin",
24
24
  "cordis.patch.yml",
25
25
  "README.md",
26
- "README.zh.md"
26
+ "README.zh.md",
27
+ "docs",
28
+ "assets"
27
29
  ],
28
30
  "dependencies": {
29
31
  "yaml": "^2.6.0"