dsh-tool-stats 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/cordis.patch.yml +4 -0
- package/lib/analyzer.d.ts +5 -0
- package/lib/analyzer.js +92 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +3 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +58 -0
- package/lib/client/index.d.ts +4 -0
- package/lib/client/index.js +16 -0
- package/lib/client/view.d.ts +2 -0
- package/lib/client/view.js +19 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +14 -0
- package/lib/routes.d.ts +5 -0
- package/lib/routes.js +15 -0
- package/lib/stats.d.ts +9 -0
- package/lib/stats.js +25 -0
- package/lib/stats.web.js +33 -0
- package/lib/store.d.ts +10 -0
- package/lib/store.js +29 -0
- package/lib/types.d.ts +40 -0
- package/lib/types.js +2 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hj01857655
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# dsh-tool-stats
|
|
2
|
+
|
|
3
|
+
Every tool call is counted, and the ones that never fire are named so you can remove them.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dsh plugin --profile web add dsh-tool-stats
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Hook tool calls.** Records tool name, success/failure, latency, session.
|
|
14
|
+
- **Dead-tool detection.** Tools configured but never invoked across N sessions are flagged.
|
|
15
|
+
- **Failure analysis.** Tools with high failure rates are identified with their last error.
|
|
16
|
+
- **Recommendations.** "Removing 12 unused tools would save ~1.8k tokens per context window."
|
|
17
|
+
- **Panel.** Usage table, dead/failing sections, recommendations.
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
dsh-tool-stats summary # show tool usage, dead tools, failures, recommendations
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ToolCall, ToolCounter, DeadTool, FailingTool, Recommendation } from './types.js';
|
|
2
|
+
export declare function aggregate(calls: ToolCall[]): Map<string, ToolCounter>;
|
|
3
|
+
export declare function findDeadTools(configuredTools: string[], calls: ToolCall[], threshold?: number): DeadTool[];
|
|
4
|
+
export declare function findFailingTools(calls: ToolCall[], threshold?: number): FailingTool[];
|
|
5
|
+
export declare function recommend(configuredTools: string[], calls: ToolCall[], deadThreshold?: number, estimatedTokensPerTool?: number): Recommendation[];
|
package/lib/analyzer.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function percentile(sorted, p) {
|
|
2
|
+
if (sorted.length === 0)
|
|
3
|
+
return 0;
|
|
4
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
5
|
+
return sorted[Math.max(0, idx)];
|
|
6
|
+
}
|
|
7
|
+
export function aggregate(calls) {
|
|
8
|
+
const map = new Map();
|
|
9
|
+
for (const call of calls) {
|
|
10
|
+
const c = map.get(call.tool) ?? {
|
|
11
|
+
tool: call.tool,
|
|
12
|
+
invocations: 0,
|
|
13
|
+
successes: 0,
|
|
14
|
+
failures: 0,
|
|
15
|
+
failureRate: 0,
|
|
16
|
+
p50Latency: 0,
|
|
17
|
+
p95Latency: 0,
|
|
18
|
+
lastUsed: 0,
|
|
19
|
+
};
|
|
20
|
+
c.invocations++;
|
|
21
|
+
if (call.success)
|
|
22
|
+
c.successes++;
|
|
23
|
+
else {
|
|
24
|
+
c.failures++;
|
|
25
|
+
if (call.errorMessage)
|
|
26
|
+
c.lastError = call.errorMessage;
|
|
27
|
+
}
|
|
28
|
+
c.lastUsed = Math.max(c.lastUsed, call.timestamp);
|
|
29
|
+
map.set(call.tool, c);
|
|
30
|
+
}
|
|
31
|
+
// Compute derived fields
|
|
32
|
+
const latenciesByTool = new Map();
|
|
33
|
+
for (const call of calls) {
|
|
34
|
+
const arr = latenciesByTool.get(call.tool) ?? [];
|
|
35
|
+
arr.push(call.latencyMs);
|
|
36
|
+
latenciesByTool.set(call.tool, arr);
|
|
37
|
+
}
|
|
38
|
+
for (const c of map.values()) {
|
|
39
|
+
c.failureRate = c.invocations > 0 ? c.failures / c.invocations : 0;
|
|
40
|
+
const lats = (latenciesByTool.get(c.tool) ?? []).sort((a, b) => a - b);
|
|
41
|
+
c.p50Latency = percentile(lats, 50);
|
|
42
|
+
c.p95Latency = percentile(lats, 95);
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
export function findDeadTools(configuredTools, calls, threshold = 10) {
|
|
47
|
+
const counters = aggregate(calls);
|
|
48
|
+
const sessions = new Set(calls.map((c) => c.sessionId));
|
|
49
|
+
const sessionCount = sessions.size;
|
|
50
|
+
return configuredTools
|
|
51
|
+
.filter((tool) => {
|
|
52
|
+
const c = counters.get(tool);
|
|
53
|
+
return !c || c.invocations === 0 || (sessionCount - new Set(calls.filter((c) => c.tool === tool).map((c) => c.sessionId)).size) >= threshold;
|
|
54
|
+
})
|
|
55
|
+
.map((tool) => ({
|
|
56
|
+
tool,
|
|
57
|
+
sessionsSinceLastUse: sessionCount - (counters.get(tool)?.invocations ?? 0),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
export function findFailingTools(calls, threshold = 0.3) {
|
|
61
|
+
const counters = aggregate(calls);
|
|
62
|
+
return [...counters.values()]
|
|
63
|
+
.filter((c) => c.invocations >= 5 && c.failureRate >= threshold)
|
|
64
|
+
.map((c) => {
|
|
65
|
+
const ft = { tool: c.tool, failureRate: c.failureRate };
|
|
66
|
+
if (c.lastError !== undefined)
|
|
67
|
+
ft.lastError = c.lastError;
|
|
68
|
+
return ft;
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.failureRate - a.failureRate);
|
|
71
|
+
}
|
|
72
|
+
export function recommend(configuredTools, calls, deadThreshold = 10, estimatedTokensPerTool = 150) {
|
|
73
|
+
const recs = [];
|
|
74
|
+
const dead = findDeadTools(configuredTools, calls, deadThreshold);
|
|
75
|
+
if (dead.length > 0) {
|
|
76
|
+
recs.push({
|
|
77
|
+
type: 'dead-tools',
|
|
78
|
+
message: `You have ${configuredTools.length} tools, but ${dead.length} haven't been used in ${deadThreshold}+ sessions. Removing them would save ~${dead.length * estimatedTokensPerTool} tokens per context window.`,
|
|
79
|
+
tools: dead.map((d) => d.tool),
|
|
80
|
+
estimatedTokenSavings: dead.length * estimatedTokensPerTool,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const failing = findFailingTools(calls);
|
|
84
|
+
if (failing.length > 0) {
|
|
85
|
+
recs.push({
|
|
86
|
+
type: 'failing-tools',
|
|
87
|
+
message: `${failing.length} tools have a failure rate above 30%. Check their configuration.`,
|
|
88
|
+
tools: failing.map((f) => f.tool),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return recs;
|
|
92
|
+
}
|
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(argv: string[]): number;
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { ToolStats } from './stats.js';
|
|
3
|
+
export function run(argv) {
|
|
4
|
+
const { positionals } = parseArgs({
|
|
5
|
+
args: argv,
|
|
6
|
+
options: {
|
|
7
|
+
'tool': { type: 'string' },
|
|
8
|
+
'success': { type: 'boolean' },
|
|
9
|
+
'latency': { type: 'string' },
|
|
10
|
+
'session': { type: 'string' },
|
|
11
|
+
},
|
|
12
|
+
allowPositionals: true,
|
|
13
|
+
});
|
|
14
|
+
const projectDir = process.cwd();
|
|
15
|
+
const stats = new ToolStats(projectDir);
|
|
16
|
+
const cmd = positionals[0] ?? 'summary';
|
|
17
|
+
switch (cmd) {
|
|
18
|
+
case 'summary': {
|
|
19
|
+
const s = stats.summary();
|
|
20
|
+
if (s.tools.length === 0) {
|
|
21
|
+
console.log('No tool calls recorded.');
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
console.log('Tool usage:');
|
|
25
|
+
for (const t of s.tools) {
|
|
26
|
+
console.log(` ${t.tool}: ${t.invocations} calls, ${Math.round(t.failureRate * 100)}% fail, p50=${t.p50Latency}ms`);
|
|
27
|
+
}
|
|
28
|
+
if (s.deadTools.length > 0) {
|
|
29
|
+
console.log(`\nDead tools (${s.deadTools.length}):`);
|
|
30
|
+
for (const d of s.deadTools)
|
|
31
|
+
console.log(` ${d.tool}`);
|
|
32
|
+
}
|
|
33
|
+
if (s.failingTools.length > 0) {
|
|
34
|
+
console.log(`\nFailing tools (${s.failingTools.length}):`);
|
|
35
|
+
for (const f of s.failingTools)
|
|
36
|
+
console.log(` ${f.tool}: ${Math.round(f.failureRate * 100)}% fail`);
|
|
37
|
+
}
|
|
38
|
+
for (const r of s.recommendations) {
|
|
39
|
+
console.log(`\n💡 ${r.message}`);
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
case 'record': {
|
|
44
|
+
const tool = positionals[1] ?? '';
|
|
45
|
+
const success = positionals.includes('--success');
|
|
46
|
+
const latency = parseInt(positionals[positionals.indexOf('--latency') + 1] ?? '0', 10);
|
|
47
|
+
const session = positionals[positionals.indexOf('--session') + 1] ?? 'cli';
|
|
48
|
+
stats.record({ tool, timestamp: Date.now(), success, latencyMs: latency, sessionId: session });
|
|
49
|
+
console.log(`Recorded: ${tool} ${success ? 'ok' : 'fail'}`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
case 'help':
|
|
53
|
+
default:
|
|
54
|
+
console.log('Usage: dsh-tool-stats <command> [options]');
|
|
55
|
+
console.log('Commands: summary, record');
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { renderPanel } from './view.js';
|
|
2
|
+
export const inject = ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-connection'];
|
|
3
|
+
export function apply(ctx) {
|
|
4
|
+
ctx.inject(inject, (settings, connection) => {
|
|
5
|
+
const s = settings;
|
|
6
|
+
const c = connection;
|
|
7
|
+
s.section('tool-stats', {
|
|
8
|
+
title: 'Tool Stats',
|
|
9
|
+
render: async () => {
|
|
10
|
+
const res = await c.fetch('/api/stats.panel');
|
|
11
|
+
const payload = await res.json();
|
|
12
|
+
return renderPanel(payload);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function renderPanel(payload) {
|
|
2
|
+
const toolRows = payload.tools
|
|
3
|
+
.map((t) => `<tr><td>${t.tool}</td><td>${t.invocations}</td><td>${Math.round(t.failureRate * 100)}%</td><td>${t.p50Latency}ms</td><td>${t.p95Latency}ms</td></tr>`)
|
|
4
|
+
.join('');
|
|
5
|
+
const deadSection = payload.deadTools.length > 0
|
|
6
|
+
? `<h3>Dead tools</h3><ul>${payload.deadTools.map((d) => `<li>${d.tool}</li>`).join('')}</ul>`
|
|
7
|
+
: '';
|
|
8
|
+
const failSection = payload.failingTools.length > 0
|
|
9
|
+
? `<h3>Failing tools</h3><ul>${payload.failingTools.map((f) => `<li>${f.tool}: ${Math.round(f.failureRate * 100)}%</li>`).join('')}</ul>`
|
|
10
|
+
: '';
|
|
11
|
+
const recSection = payload.recommendations.length > 0
|
|
12
|
+
? `<h3>Recommendations</h3><ul>${payload.recommendations.map((r) => `<li>${r.message}</li>`).join('')}</ul>`
|
|
13
|
+
: '';
|
|
14
|
+
return `<div class="stats-panel">
|
|
15
|
+
<h2>Tool Stats</h2>
|
|
16
|
+
${toolRows ? `<table><thead><tr><th>Tool</th><th>Calls</th><th>Fail</th><th>p50</th><th>p95</th></tr></thead><tbody>${toolRows}</tbody></table>` : '<p>No calls recorded.</p>'}
|
|
17
|
+
${deadSection}${failSection}${recSection}
|
|
18
|
+
</div>`;
|
|
19
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { ToolStats } from './stats.js';
|
|
3
|
+
export declare const name = "dsh-tool-stats";
|
|
4
|
+
export interface StatsService {
|
|
5
|
+
record(call: {
|
|
6
|
+
tool: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
success: boolean;
|
|
9
|
+
latencyMs: number;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
errorMessage?: string;
|
|
12
|
+
}): void;
|
|
13
|
+
summary(configuredTools?: string[]): ReturnType<ToolStats['summary']>;
|
|
14
|
+
}
|
|
15
|
+
export declare function apply(ctx: Context): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { ToolStats } from './stats.js';
|
|
3
|
+
import { registerStatsRoutes } from './routes.js';
|
|
4
|
+
export const name = 'dsh-tool-stats';
|
|
5
|
+
export function apply(ctx) {
|
|
6
|
+
const root = resolve(process.cwd());
|
|
7
|
+
const stats = new ToolStats(root);
|
|
8
|
+
const service = {
|
|
9
|
+
record: (call) => stats.record(call),
|
|
10
|
+
summary: (configuredTools) => stats.summary(configuredTools ?? []),
|
|
11
|
+
};
|
|
12
|
+
ctx.provide('toolStats', service);
|
|
13
|
+
registerStatsRoutes(ctx, service);
|
|
14
|
+
}
|
package/lib/routes.d.ts
ADDED
package/lib/routes.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { STATS_PANEL_PATH } from './stats.js';
|
|
2
|
+
export { STATS_PANEL_PATH };
|
|
3
|
+
export function registerStatsRoutes(ctx, stats) {
|
|
4
|
+
ctx.inject(['connection'], (connectionCtx) => {
|
|
5
|
+
const connection = connectionCtx.connection;
|
|
6
|
+
connection.fetch.register({
|
|
7
|
+
path: STATS_PANEL_PATH,
|
|
8
|
+
methods: ['GET'],
|
|
9
|
+
requestBody: 'buffered',
|
|
10
|
+
fetch: () => Promise.resolve(Response.json(stats.summary(), {
|
|
11
|
+
headers: { 'cache-control': 'no-store' },
|
|
12
|
+
})),
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
package/lib/stats.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolCall, PanelPayload } from './types.js';
|
|
2
|
+
export declare const STATS_PANEL_PATH = "/api/stats.panel";
|
|
3
|
+
export declare class ToolStats {
|
|
4
|
+
private projectDir;
|
|
5
|
+
private store;
|
|
6
|
+
constructor(projectDir: string);
|
|
7
|
+
record(call: ToolCall): void;
|
|
8
|
+
summary(configuredTools?: string[]): PanelPayload;
|
|
9
|
+
}
|
package/lib/stats.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StatsStore } from './store.js';
|
|
2
|
+
import { aggregate, findDeadTools, findFailingTools, recommend } from './analyzer.js';
|
|
3
|
+
export const STATS_PANEL_PATH = '/api/stats.panel';
|
|
4
|
+
export class ToolStats {
|
|
5
|
+
projectDir;
|
|
6
|
+
store;
|
|
7
|
+
constructor(projectDir) {
|
|
8
|
+
this.projectDir = projectDir;
|
|
9
|
+
this.store = new StatsStore(projectDir);
|
|
10
|
+
}
|
|
11
|
+
record(call) {
|
|
12
|
+
this.store.append(call);
|
|
13
|
+
}
|
|
14
|
+
summary(configuredTools = []) {
|
|
15
|
+
const calls = this.store.readAll();
|
|
16
|
+
const counters = aggregate(calls);
|
|
17
|
+
const tools = [...counters.values()].sort((a, b) => b.invocations - a.invocations);
|
|
18
|
+
return {
|
|
19
|
+
tools,
|
|
20
|
+
deadTools: findDeadTools(configuredTools, calls),
|
|
21
|
+
failingTools: findFailingTools(calls),
|
|
22
|
+
recommendations: recommend(configuredTools, calls),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
package/lib/stats.web.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/client/view.tsx
|
|
2
|
+
function renderPanel(payload) {
|
|
3
|
+
const toolRows = payload.tools.map((t) => `<tr><td>${t.tool}</td><td>${t.invocations}</td><td>${Math.round(t.failureRate * 100)}%</td><td>${t.p50Latency}ms</td><td>${t.p95Latency}ms</td></tr>`).join("");
|
|
4
|
+
const deadSection = payload.deadTools.length > 0 ? `<h3>Dead tools</h3><ul>${payload.deadTools.map((d) => `<li>${d.tool}</li>`).join("")}</ul>` : "";
|
|
5
|
+
const failSection = payload.failingTools.length > 0 ? `<h3>Failing tools</h3><ul>${payload.failingTools.map((f) => `<li>${f.tool}: ${Math.round(f.failureRate * 100)}%</li>`).join("")}</ul>` : "";
|
|
6
|
+
const recSection = payload.recommendations.length > 0 ? `<h3>Recommendations</h3><ul>${payload.recommendations.map((r) => `<li>${r.message}</li>`).join("")}</ul>` : "";
|
|
7
|
+
return `<div class="stats-panel">
|
|
8
|
+
<h2>Tool Stats</h2>
|
|
9
|
+
${toolRows ? `<table><thead><tr><th>Tool</th><th>Calls</th><th>Fail</th><th>p50</th><th>p95</th></tr></thead><tbody>${toolRows}</tbody></table>` : "<p>No calls recorded.</p>"}
|
|
10
|
+
${deadSection}${failSection}${recSection}
|
|
11
|
+
</div>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/client/index.tsx
|
|
15
|
+
var inject = ["@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-connection"];
|
|
16
|
+
function apply(ctx) {
|
|
17
|
+
ctx.inject(inject, (settings, connection) => {
|
|
18
|
+
const s = settings;
|
|
19
|
+
const c = connection;
|
|
20
|
+
s.section("tool-stats", {
|
|
21
|
+
title: "Tool Stats",
|
|
22
|
+
render: async () => {
|
|
23
|
+
const res = await c.fetch("/api/stats.panel");
|
|
24
|
+
const payload = await res.json();
|
|
25
|
+
return renderPanel(payload);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
apply,
|
|
32
|
+
inject
|
|
33
|
+
};
|
package/lib/store.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ToolCall } from './types.js';
|
|
2
|
+
export declare class StatsStore {
|
|
3
|
+
private readonly projectDir;
|
|
4
|
+
private readonly statsDir;
|
|
5
|
+
private readonly callsPath;
|
|
6
|
+
constructor(projectDir: string);
|
|
7
|
+
private ensureDir;
|
|
8
|
+
append(call: ToolCall): void;
|
|
9
|
+
readAll(): ToolCall[];
|
|
10
|
+
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export class StatsStore {
|
|
4
|
+
projectDir;
|
|
5
|
+
statsDir;
|
|
6
|
+
callsPath;
|
|
7
|
+
constructor(projectDir) {
|
|
8
|
+
this.projectDir = projectDir;
|
|
9
|
+
this.statsDir = join(projectDir, '.toolstats');
|
|
10
|
+
this.callsPath = join(this.statsDir, 'calls.jsonl');
|
|
11
|
+
}
|
|
12
|
+
ensureDir() {
|
|
13
|
+
if (!existsSync(this.statsDir))
|
|
14
|
+
mkdirSync(this.statsDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
append(call) {
|
|
17
|
+
this.ensureDir();
|
|
18
|
+
appendFileSync(this.callsPath, JSON.stringify(call) + '\n', 'utf8');
|
|
19
|
+
}
|
|
20
|
+
readAll() {
|
|
21
|
+
if (!existsSync(this.callsPath))
|
|
22
|
+
return [];
|
|
23
|
+
return readFileSync(this.callsPath, 'utf8')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((line) => JSON.parse(line));
|
|
28
|
+
}
|
|
29
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ToolCall {
|
|
2
|
+
tool: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
success: boolean;
|
|
5
|
+
latencyMs: number;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolCounter {
|
|
10
|
+
tool: string;
|
|
11
|
+
invocations: number;
|
|
12
|
+
successes: number;
|
|
13
|
+
failures: number;
|
|
14
|
+
failureRate: number;
|
|
15
|
+
p50Latency: number;
|
|
16
|
+
p95Latency: number;
|
|
17
|
+
lastUsed: number;
|
|
18
|
+
lastError?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface DeadTool {
|
|
21
|
+
tool: string;
|
|
22
|
+
sessionsSinceLastUse: number;
|
|
23
|
+
}
|
|
24
|
+
export interface FailingTool {
|
|
25
|
+
tool: string;
|
|
26
|
+
failureRate: number;
|
|
27
|
+
lastError?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Recommendation {
|
|
30
|
+
type: 'dead-tools' | 'failing-tools';
|
|
31
|
+
message: string;
|
|
32
|
+
tools: string[];
|
|
33
|
+
estimatedTokenSavings?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface PanelPayload {
|
|
36
|
+
tools: ToolCounter[];
|
|
37
|
+
deadTools: DeadTool[];
|
|
38
|
+
failingTools: FailingTool[];
|
|
39
|
+
recommendations: Recommendation[];
|
|
40
|
+
}
|
package/lib/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-tool-stats",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Every tool call is counted, and the ones that never fire are named so you can remove them.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"bin": { "dsh-tool-stats": "lib/bin.js" },
|
|
9
|
+
"exports": {
|
|
10
|
+
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
|
|
11
|
+
"./client": { "types": "./lib/client.d.ts", "default": "./lib/stats.web.js" },
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": ["lib", "cordis.patch.yml", "README.md", "CHANGELOG.md"],
|
|
15
|
+
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "tools", "analytics", "usage"],
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
18
|
+
"client": {
|
|
19
|
+
"platform": "web",
|
|
20
|
+
"inject": ["@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-connection"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "hj01857655",
|
|
25
|
+
"repository": { "type": "git", "url": "git+https://github.com/hj01857655/dsh-tool-stats.git" },
|
|
26
|
+
"bugs": { "url": "https://github.com/hj01857655/dsh-tool-stats/issues" },
|
|
27
|
+
"homepage": "https://github.com/hj01857655/dsh-tool-stats#readme",
|
|
28
|
+
"engines": { "node": ">=20" },
|
|
29
|
+
"peerDependencies": { "@deepseek-ai/cordis": "^4.0.1" },
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
32
|
+
"@types/node": "^22.20.3",
|
|
33
|
+
"@types/react": "^19.3.0",
|
|
34
|
+
"esbuild": "^0.28.2",
|
|
35
|
+
"react": "^19.3.0",
|
|
36
|
+
"react-dom": "^19.3.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json && node scripts/bundle-client.mjs",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
|
|
42
|
+
"test": "tsc -p tsconfig.json && node --test tests/*.test.mjs",
|
|
43
|
+
"prepublishOnly": "npm run build && npm test"
|
|
44
|
+
}
|
|
45
|
+
}
|