dsh-xray 0.3.1 → 0.4.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
@@ -6,6 +6,8 @@
6
6
 
7
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.
8
8
 
9
+ ![dsh-xray demo](./docs/demo.svg)
10
+
9
11
  [中文](./README.zh.md)
10
12
 
11
13
  > **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.
@@ -28,6 +30,63 @@ npx dsh-xray audit # static scan of out-of-tree plugins for sensitive touc
28
30
 
29
31
  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`.
30
32
 
33
+ ## What it looks like
34
+
35
+ Every row of the booted tree, attributed to the layer that introduced it — and who patched it since:
36
+
37
+ ```console
38
+ $ npx dsh-xray attribute
39
+ # 130 rows in profile "web"
40
+
41
+ timer @deepseek-ai/dsh-base
42
+ hmr @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app [disabled]
43
+ llm @deepseek-ai/dsh-base
44
+ session-query-sqlite @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app
45
+ ...
46
+ ```
47
+
48
+ What breaks if you disable a provider — computed from the live service store, not guesses:
49
+
50
+ ```console
51
+ $ npx dsh-xray deps
52
+ # disable-cascade (transitive consumers of each provider):
53
+ Loader → 5 plugin(s): AgentPresets, ClientModuleRegistry, Hmr, Include, PluginInventoryGateway
54
+ TimerService → 1 plugin(s): Hmr
55
+ SessionProjectionRegistry → 1 plugin(s): SessionProjectionCache
56
+ ```
57
+
58
+ Which fields have multiple writers, and who silently wins:
59
+
60
+ ```console
61
+ $ npx dsh-xray conflicts
62
+ session-query-sqlite
63
+ .config: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
64
+ tool-bash
65
+ .disabled: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
66
+ ```
67
+
68
+ What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
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
+
82
+ And when a patch row targets an id that doesn't exist (dsh skips it silently), `diff` catches it:
83
+
84
+ ```console
85
+ $ npx dsh-xray diff
86
+ orphan overrides (silently skipped) (1)
87
+ no-such-row in ~/.dsh/profiles/web/cordis.patch.yml
88
+ ```
89
+
31
90
  ## Agent tool
32
91
 
33
92
  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.
package/README.zh.md CHANGED
@@ -6,6 +6,8 @@
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
13
  > **状态:0.2.x — 静态 + 运行时成像。** 静态命令在 dsh 起不来时照样能用;`deps`/`health` 和 agent 工具需要插件已挂载。
@@ -28,6 +30,63 @@ npx dsh-xray audit # 对 out-of-tree 插件做敏感触点静态扫描
28
30
 
29
31
  所有命令支持 `--profile <name>`(默认 `web`)和 `--json`。`diff` 在两棵树不一致时退出码 `1`;`health` 在有插件不健康时退出码 `1`。`attribute`、`conflicts`、`snapshot` 是纯静态的:dsh 起不来时照样能跑。`deps` 和 `health` 读取已挂载插件维护在 `$DSH_HOME/xray/runtime.json` 的运行时快照。
30
32
 
33
+ ## 长什么样
34
+
35
+ 启动树的每一行,归因到引入它的层——以及之后谁 patch 过它:
36
+
37
+ ```console
38
+ $ npx dsh-xray attribute
39
+ # 130 rows in profile "web"
40
+
41
+ timer @deepseek-ai/dsh-base
42
+ hmr @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app [disabled]
43
+ llm @deepseek-ai/dsh-base
44
+ session-query-sqlite @deepseek-ai/dsh-base ← patched by @deepseek-ai/dsh-web-app
45
+ ...
46
+ ```
47
+
48
+ 停用一个 provider 会连带瘫掉什么——从真实服务存储算出来,不是猜的:
49
+
50
+ ```console
51
+ $ npx dsh-xray deps
52
+ # disable-cascade (transitive consumers of each provider):
53
+ Loader → 5 plugin(s): AgentPresets, ClientModuleRegistry, Hmr, Include, PluginInventoryGateway
54
+ TimerService → 1 plugin(s): Hmr
55
+ SessionProjectionRegistry → 1 plugin(s): SessionProjectionCache
56
+ ```
57
+
58
+ 哪些字段有多个写者、谁静默赢了:
59
+
60
+ ```console
61
+ $ npx dsh-xray conflicts
62
+ session-query-sqlite
63
+ .config: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
64
+ tool-bash
65
+ .disabled: @deepseek-ai/dsh-base → @deepseek-ai/dsh-web-app (winner: @deepseek-ai/dsh-web-app)
66
+ ```
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
+
82
+ patch 行指向不存在的 id 时(dsh 静默跳过),`diff` 能抓到:
83
+
84
+ ```console
85
+ $ npx dsh-xray diff
86
+ orphan overrides (silently skipped) (1)
87
+ no-such-row in ~/.dsh/profiles/web/cordis.patch.yml
88
+ ```
89
+
31
90
  ## Agent 工具
32
91
 
33
92
  挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
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.1",
3
+ "version": "0.4.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",
@@ -23,7 +23,8 @@
23
23
  "bin",
24
24
  "cordis.patch.yml",
25
25
  "README.md",
26
- "README.zh.md"
26
+ "README.zh.md",
27
+ "docs"
27
28
  ],
28
29
  "dependencies": {
29
30
  "yaml": "^2.6.0"
@@ -32,7 +33,8 @@
32
33
  "test": "node --test tests/*.spec.js",
33
34
  "lint": "biome check .",
34
35
  "format": "biome check --write .",
35
- "check": "biome check . && node --test tests/*.spec.js"
36
+ "check": "biome check . && node --test tests/*.spec.js",
37
+ "coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 tests/*.spec.js"
36
38
  },
37
39
  "keywords": [
38
40
  "dsh-plugin",
@@ -41,7 +43,14 @@
41
43
  "cordis",
42
44
  "diagnostics",
43
45
  "introspection",
44
- "plugin-tree"
46
+ "plugin-tree",
47
+ "dependency-graph",
48
+ "observability",
49
+ "audit",
50
+ "security-scan",
51
+ "token-cost",
52
+ "lockfile",
53
+ "troubleshooting"
45
54
  ],
46
55
  "dsh": {
47
56
  "bundle": {