dsh-xray 0.2.0 → 0.3.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 +8 -7
- package/README.zh.md +8 -7
- package/bin/xray.js +58 -1
- package/lib/collect/audit.js +115 -0
- package/lib/collect/runtime.js +30 -1
- package/lib/index.js +5 -2
- package/lib/model.js +57 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,19 +17,22 @@ npx dsh-xray diff # declared (static layers) vs actual (dump-config) tree
|
|
|
17
17
|
npx dsh-xray snapshot # content-addressed lockfile of the effective composition
|
|
18
18
|
npx dsh-xray deps [svc] # service dependency graph: providers, consumers, disable-cascade
|
|
19
19
|
npx dsh-xray health # plugin lifecycle health: failed fibers, pending injects, transitions
|
|
20
|
+
npx dsh-xray cost # estimated context-token cost per model-facing tool schema
|
|
21
|
+
npx dsh-xray shadow # services provided by multiple plugins
|
|
22
|
+
npx dsh-xray audit # static scan of out-of-tree plugins for sensitive touchpoints
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
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`.
|
|
23
26
|
|
|
24
27
|
## Agent tool
|
|
25
28
|
|
|
26
|
-
Mounted in the tree, dsh-xray registers an `xray_composition` tool (`view: summary | deps | health`), so an agent can answer "what capabilities do I have / what plugin provides X / why is Y unavailable" about itself.
|
|
29
|
+
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.
|
|
27
30
|
|
|
28
31
|
## Capabilities
|
|
29
32
|
|
|
30
33
|
Diagnostic imaging for a running composition — complementary to [dsh-doctor](https://www.npmjs.com/package/dsh-doctor) (rescue & recovery).
|
|
31
34
|
|
|
32
|
-
Shipped in 0.
|
|
35
|
+
Shipped in 0.3.x:
|
|
33
36
|
|
|
34
37
|
- **Layer attribution** — which layer introduced each active plugin: kernel bundle, profile dependency, `cordis.patch.yml` insert, or repository source
|
|
35
38
|
- **Declared vs. actual diff** — installed-but-inactive, uninstalled-but-lingering patch rows
|
|
@@ -39,11 +42,9 @@ Shipped in 0.2.x:
|
|
|
39
42
|
- **Runtime health** — per-plugin fiber lifecycle state, startup failures, transition history (`health`)
|
|
40
43
|
- **Agent self-introspection** — the `xray_composition` tool lets agents inspect their own capability set
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
- **Command/tool shadowing** — plugins registering the same command/tool, and which one silently wins
|
|
46
|
-
- **Context cost** — tokens each plugin injects into agent context: tool schemas, prompt sections, skills
|
|
45
|
+
- **Capability audit** — heuristic static scan of out-of-tree plugins: network egress, shell, filesystem, env, eval (`audit`)
|
|
46
|
+
- **Service shadowing** — services claimed by multiple plugins, per-plugin tool/command registrations (`shadow`)
|
|
47
|
+
- **Context cost** — estimated tokens each model-facing tool schema occupies (`cost`)
|
|
47
48
|
|
|
48
49
|
## Install
|
|
49
50
|
|
package/README.zh.md
CHANGED
|
@@ -17,19 +17,22 @@ npx dsh-xray diff # 声明(静态层)vs 实际(dump-config)组合树
|
|
|
17
17
|
npx dsh-xray snapshot # 当前生效组合的内容寻址 lockfile
|
|
18
18
|
npx dsh-xray deps [svc] # 服务依赖图:提供者、消费者、停用级联
|
|
19
19
|
npx dsh-xray health # 插件生命周期健康:失败 fiber、等待中的注入、状态迁移史
|
|
20
|
+
npx dsh-xray cost # 每个模型可见工具 schema 的估算 token 占用
|
|
21
|
+
npx dsh-xray shadow # 被多个插件同时提供的服务
|
|
22
|
+
npx dsh-xray audit # 对 out-of-tree 插件做敏感触点静态扫描
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
所有命令支持 `--profile <name>`(默认 `web`)和 `--json`。`diff` 在两棵树不一致时退出码 `1`;`health` 在有插件不健康时退出码 `1`。`attribute`、`conflicts`、`snapshot` 是纯静态的:dsh 起不来时照样能跑。`deps` 和 `health` 读取已挂载插件维护在 `$DSH_HOME/xray/runtime.json` 的运行时快照。
|
|
23
26
|
|
|
24
27
|
## Agent 工具
|
|
25
28
|
|
|
26
|
-
挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
|
|
29
|
+
挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
|
|
27
30
|
|
|
28
31
|
## 能力
|
|
29
32
|
|
|
30
33
|
对运行中组合树的诊断成像——与 [dsh-doctor](https://www.npmjs.com/package/dsh-doctor)(救援与恢复)互补。
|
|
31
34
|
|
|
32
|
-
0.
|
|
35
|
+
0.3.x 已交付:
|
|
33
36
|
|
|
34
37
|
- **来源归因** — 每个活跃插件来自哪一层:内核 bundle / profile 依赖 / `cordis.patch.yml` insert / repository 源
|
|
35
38
|
- **声明 vs 实际 diff** — 装了但没生效、卸了但残留 patch 行
|
|
@@ -39,11 +42,9 @@ npx dsh-xray health # 插件生命周期健康:失败 fiber、等待中的
|
|
|
39
42
|
- **运行时健康** — 每个插件的 fiber 生命周期状态、启动失败、状态迁移史(`health`)
|
|
40
43
|
- **Agent 自省** — `xray_composition` 工具让 agent 检视自己的能力集
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
- **命令/工具重名检测** — 注册同名命令/工具时,谁静默覆盖谁
|
|
46
|
-
- **上下文成本** — 每个插件往 agent 上下文注入多少 token:工具 schema、prompt 片段、skill 文档
|
|
45
|
+
- **能力审计** — 对 out-of-tree 插件的启发式静态扫描:网络外发、shell、文件系统、环境变量、动态求值(`audit`)
|
|
46
|
+
- **服务重名检测** — 被多个插件同时提供的服务,及每插件工具/命令注册数(`shadow`)
|
|
47
|
+
- **上下文成本** — 每个模型可见工具 schema 的估算 token 占用(`cost`)
|
|
47
48
|
|
|
48
49
|
## 安装
|
|
49
50
|
|
package/bin/xray.js
CHANGED
|
@@ -178,6 +178,57 @@ function cmdHealth(args) {
|
|
|
178
178
|
if (result.unhealthy.length) process.exitCode = 1;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
function cmdCost(args) {
|
|
182
|
+
const snap = readRuntimeSnapshot();
|
|
183
|
+
const result = model.contextCost(snap);
|
|
184
|
+
if (args.json) return console.log(JSON.stringify(result, null, 2));
|
|
185
|
+
console.log(
|
|
186
|
+
`~${result.totalTokens} tokens across ${result.toolCount} tool schema(s) (captured ${result.capturedAt})\n`,
|
|
187
|
+
);
|
|
188
|
+
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}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cmdShadow(args) {
|
|
195
|
+
const snap = readRuntimeSnapshot();
|
|
196
|
+
const result = model.shadowing(snap);
|
|
197
|
+
if (args.json) return console.log(JSON.stringify(result, null, 2));
|
|
198
|
+
if (!result.services.length) console.log('no service is provided by more than one plugin');
|
|
199
|
+
for (const s of result.services) {
|
|
200
|
+
console.log(`${s.service}: provided by ${s.providers.join(' AND ')}`);
|
|
201
|
+
}
|
|
202
|
+
if (result.registrars.length) {
|
|
203
|
+
console.log('\n# tool/command registrars:');
|
|
204
|
+
for (const r of result.registrars) {
|
|
205
|
+
console.log(` ${r.plugin}: ${r.registrations} registration(s)`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (result.services.length) process.exitCode = 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function cmdAudit(args) {
|
|
212
|
+
const { collectAudit } = require('../lib/collect/audit.js');
|
|
213
|
+
const data = collectStatic(args.profile);
|
|
214
|
+
const result = collectAudit(data);
|
|
215
|
+
if (args.json) return console.log(JSON.stringify(result, null, 2));
|
|
216
|
+
if (!result.plugins.length) {
|
|
217
|
+
return console.log(
|
|
218
|
+
'no out-of-tree plugins installed (kernel bundles are the trusted baseline)',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
for (const p of result.plugins) {
|
|
222
|
+
console.log(`${p.name}@${p.version} (${p.scannedFiles} file(s) scanned)`);
|
|
223
|
+
if (!p.categories.length) console.log(' no sensitive touchpoints detected');
|
|
224
|
+
for (const c of p.categories) {
|
|
225
|
+
console.log(
|
|
226
|
+
` ${c.label}: ${c.files.slice(0, 3).join(', ')}${c.files.length > 3 ? ', …' : ''}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
181
232
|
const commands = {
|
|
182
233
|
attribute: cmdAttribute,
|
|
183
234
|
conflicts: cmdConflicts,
|
|
@@ -185,6 +236,9 @@ const commands = {
|
|
|
185
236
|
snapshot: cmdSnapshot,
|
|
186
237
|
deps: cmdDeps,
|
|
187
238
|
health: cmdHealth,
|
|
239
|
+
cost: cmdCost,
|
|
240
|
+
shadow: cmdShadow,
|
|
241
|
+
audit: cmdAudit,
|
|
188
242
|
};
|
|
189
243
|
|
|
190
244
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -201,8 +255,11 @@ Commands:
|
|
|
201
255
|
snapshot content-addressed lockfile of the effective composition
|
|
202
256
|
deps service dependency graph from the live runtime snapshot
|
|
203
257
|
health plugin lifecycle health from the live runtime snapshot
|
|
258
|
+
cost estimated context-token cost of each model-facing tool schema
|
|
259
|
+
shadow services provided by multiple plugins, and per-plugin registrations
|
|
260
|
+
audit static scan of out-of-tree plugins for sensitive touchpoints
|
|
204
261
|
|
|
205
|
-
deps/health need the plugin mounted: dsh plugin --profile web add dsh-xray`);
|
|
262
|
+
deps/health/cost/shadow need the plugin mounted: dsh plugin --profile web add dsh-xray`);
|
|
206
263
|
process.exit(args._[0] ? 2 : 0);
|
|
207
264
|
}
|
|
208
265
|
try {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Audit collector: static scan of installed plugin sources for sensitive
|
|
2
|
+
// capability touchpoints. Heuristic by design — a flag means "this pattern
|
|
3
|
+
// appears in the shipped code", not "this plugin is malicious".
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const CATEGORIES = [
|
|
9
|
+
{
|
|
10
|
+
id: 'network',
|
|
11
|
+
label: 'network egress',
|
|
12
|
+
patterns: [
|
|
13
|
+
/\bfetch\s*\(/,
|
|
14
|
+
/require\(['"](?:node:)?https?['"]\)/,
|
|
15
|
+
/from\s+['"](?:node:)?https?['"]/,
|
|
16
|
+
/\bWebSocket\b/,
|
|
17
|
+
/\baxios\b/,
|
|
18
|
+
/\bnode-fetch\b/,
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'shell',
|
|
23
|
+
label: 'subprocess / shell',
|
|
24
|
+
patterns: [
|
|
25
|
+
/require\(['"](?:node:)?child_process['"]\)/,
|
|
26
|
+
/from\s+['"](?:node:)?child_process['"]/,
|
|
27
|
+
/\bexecSync|\bexecFile|\bspawnSync?\b/,
|
|
28
|
+
/\bnode-pty\b/,
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'fs',
|
|
33
|
+
label: 'filesystem',
|
|
34
|
+
patterns: [
|
|
35
|
+
/require\(['"](?:node:)?fs(?:\/promises)?['"]\)/,
|
|
36
|
+
/from\s+['"](?:node:)?fs(?:\/promises)?['"]/,
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'env',
|
|
41
|
+
label: 'environment variables',
|
|
42
|
+
patterns: [/\bprocess\.env\b/],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'eval',
|
|
46
|
+
label: 'dynamic code evaluation',
|
|
47
|
+
patterns: [/\beval\s*\(/, /new\s+Function\s*\(/],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const SCAN_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.ts']);
|
|
52
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
53
|
+
const MAX_FILES = 400;
|
|
54
|
+
|
|
55
|
+
function* walkFiles(dir, depth = 0) {
|
|
56
|
+
if (depth > 6) return;
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
65
|
+
const full = path.join(dir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) yield* walkFiles(full, depth + 1);
|
|
67
|
+
else if (entry.isFile() && SCAN_EXTENSIONS.has(path.extname(entry.name))) yield full;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Scan one plugin package directory. Returns per-category file hits. */
|
|
72
|
+
function auditPackage(dir) {
|
|
73
|
+
const hits = new Map(); // category id -> Set of relative files
|
|
74
|
+
let scanned = 0;
|
|
75
|
+
for (const file of walkFiles(dir)) {
|
|
76
|
+
if (++scanned > MAX_FILES) break;
|
|
77
|
+
let text;
|
|
78
|
+
try {
|
|
79
|
+
if (fs.statSync(file).size > MAX_FILE_BYTES) continue;
|
|
80
|
+
text = fs.readFileSync(file, 'utf8');
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const cat of CATEGORIES) {
|
|
85
|
+
if (hits.get(cat.id)?.size >= 5) continue; // enough evidence
|
|
86
|
+
if (cat.patterns.some((p) => p.test(text))) {
|
|
87
|
+
if (!hits.has(cat.id)) hits.set(cat.id, new Set());
|
|
88
|
+
hits.get(cat.id).add(path.relative(dir, file));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
scannedFiles: scanned,
|
|
94
|
+
categories: CATEGORIES.filter((c) => hits.has(c.id)).map((c) => ({
|
|
95
|
+
id: c.id,
|
|
96
|
+
label: c.label,
|
|
97
|
+
files: [...hits.get(c.id)],
|
|
98
|
+
})),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Audit every out-of-tree plugin in a profile (kernel @deepseek-ai/* packages
|
|
104
|
+
* are the trusted baseline; auditing them adds noise, not signal).
|
|
105
|
+
*/
|
|
106
|
+
function collectAudit(staticData) {
|
|
107
|
+
const results = [];
|
|
108
|
+
for (const pkg of staticData.packages) {
|
|
109
|
+
if (!pkg.dir) continue;
|
|
110
|
+
results.push({ name: pkg.name, version: pkg.version, ...auditPackage(pkg.dir) });
|
|
111
|
+
}
|
|
112
|
+
return { schema: 'dsh-xray/audit@1', capturedAt: new Date().toISOString(), plugins: results };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { collectAudit, auditPackage, CATEGORIES };
|
package/lib/collect/runtime.js
CHANGED
|
@@ -55,6 +55,27 @@ function summarizeEffect(meta, depth = 0) {
|
|
|
55
55
|
return out;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Rough token estimate: ~4 chars per token for English/JSON. */
|
|
59
|
+
function estimateTokens(text) {
|
|
60
|
+
return Math.ceil(text.length / 4);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Capture the model-facing tool schemas (name/description/parameters). */
|
|
64
|
+
function snapshotTools(ctx) {
|
|
65
|
+
try {
|
|
66
|
+
const tools = ctx.get?.('tools') ?? ctx.root?.tools;
|
|
67
|
+
const schemas = tools?.schemas?.();
|
|
68
|
+
if (!Array.isArray(schemas)) return [];
|
|
69
|
+
return schemas.map((s) => ({
|
|
70
|
+
name: s.name,
|
|
71
|
+
description: s.description ?? '',
|
|
72
|
+
tokens: estimateTokens(JSON.stringify(s)),
|
|
73
|
+
}));
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
/**
|
|
59
80
|
* Snapshot every registered plugin runtime plus the service store.
|
|
60
81
|
* @param ctx a live Cordis context (any fiber's ctx reaches the shared registry)
|
|
@@ -100,7 +121,15 @@ function snapshotRegistry(ctx) {
|
|
|
100
121
|
registrySize: ctx.registry.size,
|
|
101
122
|
plugins,
|
|
102
123
|
services,
|
|
124
|
+
tools: snapshotTools(ctx),
|
|
103
125
|
};
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
module.exports = {
|
|
128
|
+
module.exports = {
|
|
129
|
+
snapshotRegistry,
|
|
130
|
+
snapshotTools,
|
|
131
|
+
injectNames,
|
|
132
|
+
provideNames,
|
|
133
|
+
stateName,
|
|
134
|
+
estimateTokens,
|
|
135
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const { snapshotRegistry, stateName } = require('./collect/runtime.js');
|
|
5
|
-
const { serviceGraph, health } = require('./model.js');
|
|
5
|
+
const { serviceGraph, health, shadowing, contextCost } = require('./model.js');
|
|
6
6
|
|
|
7
7
|
const name = 'dsh-xray';
|
|
8
8
|
|
|
@@ -101,7 +101,7 @@ function apply(ctx) {
|
|
|
101
101
|
parameters: {
|
|
102
102
|
view: {
|
|
103
103
|
type: 'string',
|
|
104
|
-
description: 'summary | deps | health (default summary)',
|
|
104
|
+
description: 'summary | deps | health | cost | shadow (default summary)',
|
|
105
105
|
},
|
|
106
106
|
},
|
|
107
107
|
output: {
|
|
@@ -113,10 +113,13 @@ function apply(ctx) {
|
|
|
113
113
|
snap.transitions = Object.fromEntries(transitions);
|
|
114
114
|
if (args.view === 'deps') return serviceGraph(snap);
|
|
115
115
|
if (args.view === 'health') return health(snap);
|
|
116
|
+
if (args.view === 'cost') return contextCost(snap);
|
|
117
|
+
if (args.view === 'shadow') return shadowing(snap);
|
|
116
118
|
return {
|
|
117
119
|
plugins: snap.plugins.length,
|
|
118
120
|
unhealthy: health(snap).unhealthy.length,
|
|
119
121
|
services: Object.keys(serviceGraph(snap).services).length,
|
|
122
|
+
toolSchemaTokens: contextCost(snap).totalTokens,
|
|
120
123
|
capturedAt: snap.capturedAt,
|
|
121
124
|
};
|
|
122
125
|
},
|
package/lib/model.js
CHANGED
|
@@ -247,4 +247,60 @@ function health(snap) {
|
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
/** F7: same-name registrations where a later writer silently shadows. */
|
|
251
|
+
function shadowing(snap) {
|
|
252
|
+
// Service ownership comes from the reflect store; callback `provide` is a
|
|
253
|
+
// secondary source. Cordis rejects same-layer duplicate tools itself, so
|
|
254
|
+
// what we surface is cross-source duplication: one service claimed by
|
|
255
|
+
// multiple plugins (isolation scopes make this legal — and invisible).
|
|
256
|
+
const out = { services: [], registrars: [] };
|
|
257
|
+
const providersByService = new Map();
|
|
258
|
+
for (const s of snap.services ?? []) {
|
|
259
|
+
if (!providersByService.has(s.name)) providersByService.set(s.name, []);
|
|
260
|
+
if (s.provider) providersByService.get(s.name).push(s.provider);
|
|
261
|
+
}
|
|
262
|
+
for (const p of snap.plugins) {
|
|
263
|
+
for (const s of p.provide) {
|
|
264
|
+
const list = providersByService.get(s) ?? [];
|
|
265
|
+
if (!list.includes(p.name)) providersByService.set(s, [...list, p.name]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const [service, providers] of providersByService) {
|
|
269
|
+
if (providers.length > 1) out.services.push({ service, providers });
|
|
270
|
+
}
|
|
271
|
+
for (const p of snap.plugins) {
|
|
272
|
+
const registrations = p.fibers
|
|
273
|
+
.flatMap((f) => f.effects)
|
|
274
|
+
.filter((e) => /tools\.register|commands?\./.test(e?.label ?? '')).length;
|
|
275
|
+
if (registrations > 0) out.registrars.push({ plugin: p.name, registrations });
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** F8: estimated context cost of each model-facing tool schema. */
|
|
281
|
+
function contextCost(snap) {
|
|
282
|
+
const tools = (snap.tools ?? []).slice().sort((a, b) => b.tokens - a.tokens);
|
|
283
|
+
const total = tools.reduce((sum, t) => sum + t.tokens, 0);
|
|
284
|
+
return {
|
|
285
|
+
totalTokens: total,
|
|
286
|
+
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
|
+
})),
|
|
292
|
+
capturedAt: snap.capturedAt,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
replayLayers,
|
|
298
|
+
attribute,
|
|
299
|
+
conflicts,
|
|
300
|
+
diff,
|
|
301
|
+
snapshot,
|
|
302
|
+
serviceGraph,
|
|
303
|
+
health,
|
|
304
|
+
shadowing,
|
|
305
|
+
contextCost,
|
|
306
|
+
};
|