@massu/core 1.10.7 → 1.12.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 +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
package/src/cost-tracker.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
-
import type { TranscriptEntry } from './transcript-parser.ts';
|
|
7
|
-
import { getConfig } from './config.ts';
|
|
8
|
-
|
|
9
|
-
// ============================================================
|
|
10
|
-
// Cost Attribution Tracking
|
|
11
|
-
// ============================================================
|
|
12
|
-
|
|
13
|
-
/** Prefix a base tool name with the configured tool prefix. */
|
|
14
|
-
function p(baseName: string): string {
|
|
15
|
-
return `${getConfig().toolPrefix}_${baseName}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Default model pricing (Claude models). Can be overridden via config.analytics.cost.models */
|
|
19
|
-
const DEFAULT_MODEL_PRICING: Record<string, { input_per_million: number; output_per_million: number; cache_read_per_million?: number; cache_write_per_million?: number }> = {
|
|
20
|
-
'claude-opus-4-6': { input_per_million: 15.00, output_per_million: 75.00, cache_read_per_million: 1.50, cache_write_per_million: 18.75 },
|
|
21
|
-
'claude-sonnet-4-6': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
|
|
22
|
-
'claude-sonnet-4-5': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
|
|
23
|
-
'claude-haiku-4-5-20251001': { input_per_million: 0.80, output_per_million: 4.00, cache_read_per_million: 0.08, cache_write_per_million: 1.00 },
|
|
24
|
-
'default': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export interface TokenUsage {
|
|
28
|
-
inputTokens: number;
|
|
29
|
-
outputTokens: number;
|
|
30
|
-
cacheReadTokens: number;
|
|
31
|
-
cacheWriteTokens: number;
|
|
32
|
-
model: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface CostResult {
|
|
36
|
-
totalCost: number;
|
|
37
|
-
inputCost: number;
|
|
38
|
-
outputCost: number;
|
|
39
|
-
cacheReadCost: number;
|
|
40
|
-
cacheWriteCost: number;
|
|
41
|
-
currency: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get model pricing from config or defaults.
|
|
46
|
-
*/
|
|
47
|
-
function getModelPricing(): Record<string, { input_per_million: number; output_per_million: number; cache_read_per_million?: number; cache_write_per_million?: number }> {
|
|
48
|
-
return getConfig().analytics?.cost?.models ?? DEFAULT_MODEL_PRICING;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get currency from config or default.
|
|
53
|
-
*/
|
|
54
|
-
function getCurrency(): string {
|
|
55
|
-
return getConfig().analytics?.cost?.currency ?? 'USD';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Extract token usage from transcript entries.
|
|
60
|
-
*/
|
|
61
|
-
export function extractTokenUsage(entries: TranscriptEntry[]): TokenUsage {
|
|
62
|
-
let inputTokens = 0;
|
|
63
|
-
let outputTokens = 0;
|
|
64
|
-
let cacheReadTokens = 0;
|
|
65
|
-
let cacheWriteTokens = 0;
|
|
66
|
-
let model = 'unknown';
|
|
67
|
-
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
// Usage and model are present on raw API responses but not typed on TranscriptMessage
|
|
70
|
-
const msg = entry.message as (Record<string, unknown> | undefined);
|
|
71
|
-
if (entry.type === 'assistant' && msg?.usage) {
|
|
72
|
-
const usage = msg.usage as Record<string, number>;
|
|
73
|
-
inputTokens += usage.input_tokens ?? 0;
|
|
74
|
-
outputTokens += usage.output_tokens ?? 0;
|
|
75
|
-
cacheReadTokens += usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? 0;
|
|
76
|
-
cacheWriteTokens += usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
|
|
77
|
-
}
|
|
78
|
-
if (entry.type === 'assistant' && msg?.model) {
|
|
79
|
-
model = msg.model as string;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Calculate cost from token usage.
|
|
88
|
-
*/
|
|
89
|
-
export function calculateCost(usage: TokenUsage): CostResult {
|
|
90
|
-
const pricing = getModelPricing();
|
|
91
|
-
const modelPricing = pricing[usage.model] ?? pricing['default'] ?? pricing['claude-sonnet-4-5'] ?? { input_per_million: 3.00, output_per_million: 15.00 };
|
|
92
|
-
|
|
93
|
-
const inputCost = (usage.inputTokens / 1_000_000) * modelPricing.input_per_million;
|
|
94
|
-
const outputCost = (usage.outputTokens / 1_000_000) * modelPricing.output_per_million;
|
|
95
|
-
const cacheReadCost = (usage.cacheReadTokens / 1_000_000) * (modelPricing.cache_read_per_million ?? 0);
|
|
96
|
-
const cacheWriteCost = (usage.cacheWriteTokens / 1_000_000) * (modelPricing.cache_write_per_million ?? 0);
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
|
|
100
|
-
inputCost,
|
|
101
|
-
outputCost,
|
|
102
|
-
cacheReadCost,
|
|
103
|
-
cacheWriteCost,
|
|
104
|
-
currency: getCurrency(),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Store session cost data.
|
|
110
|
-
*/
|
|
111
|
-
export function storeSessionCost(
|
|
112
|
-
db: Database.Database,
|
|
113
|
-
sessionId: string,
|
|
114
|
-
usage: TokenUsage,
|
|
115
|
-
cost: CostResult
|
|
116
|
-
): void {
|
|
117
|
-
const totalTokens = usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
118
|
-
db.prepare(`
|
|
119
|
-
INSERT INTO session_costs
|
|
120
|
-
(session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
121
|
-
total_tokens, estimated_cost_usd)
|
|
122
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
123
|
-
`).run(
|
|
124
|
-
sessionId, usage.model,
|
|
125
|
-
usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens,
|
|
126
|
-
totalTokens, cost.totalCost
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Backfill cost data from transcript files.
|
|
132
|
-
*/
|
|
133
|
-
export function backfillSessionCosts(db: Database.Database): number {
|
|
134
|
-
// Check for sessions without cost data
|
|
135
|
-
const sessions = db.prepare(`
|
|
136
|
-
SELECT DISTINCT s.session_id
|
|
137
|
-
FROM sessions s
|
|
138
|
-
LEFT JOIN session_costs c ON s.session_id = c.session_id
|
|
139
|
-
WHERE c.session_id IS NULL
|
|
140
|
-
`).all() as Array<{ session_id: string }>;
|
|
141
|
-
|
|
142
|
-
// Backfilling requires transcript data which may not be available
|
|
143
|
-
// Return count of sessions that need backfilling
|
|
144
|
-
return sessions.length;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ============================================================
|
|
148
|
-
// MCP Tool Definitions & Handlers
|
|
149
|
-
// ============================================================
|
|
150
|
-
|
|
151
|
-
export function getCostToolDefinitions(): ToolDefinition[] {
|
|
152
|
-
return [
|
|
153
|
-
{
|
|
154
|
-
name: p('cost_session'),
|
|
155
|
-
description: 'Show cost breakdown for a session. Includes token counts, model pricing, and cost by category.',
|
|
156
|
-
inputSchema: {
|
|
157
|
-
type: 'object',
|
|
158
|
-
properties: {
|
|
159
|
-
session_id: { type: 'string', description: 'Session ID to analyze' },
|
|
160
|
-
},
|
|
161
|
-
required: ['session_id'],
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
name: p('cost_trend'),
|
|
166
|
-
description: 'Cost trend over time. Shows daily/weekly spending and identifies cost drivers.',
|
|
167
|
-
inputSchema: {
|
|
168
|
-
type: 'object',
|
|
169
|
-
properties: {
|
|
170
|
-
days: { type: 'number', description: 'Days to look back (default: 30)' },
|
|
171
|
-
group_by: { type: 'string', description: 'Group by: day, week (default: day)' },
|
|
172
|
-
},
|
|
173
|
-
required: [],
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
name: p('cost_feature'),
|
|
178
|
-
description: 'Cost attribution by feature. Shows which features consume the most tokens.',
|
|
179
|
-
inputSchema: {
|
|
180
|
-
type: 'object',
|
|
181
|
-
properties: {
|
|
182
|
-
days: { type: 'number', description: 'Days to look back (default: 30)' },
|
|
183
|
-
},
|
|
184
|
-
required: [],
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const COST_BASE_NAMES = new Set(['cost_session', 'cost_trend', 'cost_feature']);
|
|
191
|
-
|
|
192
|
-
export function isCostTool(name: string): boolean {
|
|
193
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
194
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
195
|
-
return COST_BASE_NAMES.has(baseName);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function handleCostToolCall(
|
|
199
|
-
name: string,
|
|
200
|
-
args: Record<string, unknown>,
|
|
201
|
-
memoryDb: Database.Database
|
|
202
|
-
): ToolResult {
|
|
203
|
-
try {
|
|
204
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
205
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
206
|
-
|
|
207
|
-
switch (baseName) {
|
|
208
|
-
case 'cost_session':
|
|
209
|
-
return handleCostSession(args, memoryDb);
|
|
210
|
-
case 'cost_trend':
|
|
211
|
-
return handleCostTrend(args, memoryDb);
|
|
212
|
-
case 'cost_feature':
|
|
213
|
-
return handleCostFeature(args, memoryDb);
|
|
214
|
-
default:
|
|
215
|
-
return text(`Unknown cost tool: ${name}`);
|
|
216
|
-
}
|
|
217
|
-
} catch (error) {
|
|
218
|
-
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('cost_session')} { session_id: "..." }, ${p('cost_trend')} { days: 30 }`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function handleCostSession(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
223
|
-
const sessionId = args.session_id as string;
|
|
224
|
-
if (!sessionId) return text(`Usage: ${p('cost_session')} { session_id: "abc123" } - Show cost breakdown for a specific session.`);
|
|
225
|
-
|
|
226
|
-
const cost = db.prepare(
|
|
227
|
-
'SELECT * FROM session_costs WHERE session_id = ? ORDER BY created_at DESC LIMIT 1'
|
|
228
|
-
).get(sessionId) as Record<string, unknown> | undefined;
|
|
229
|
-
|
|
230
|
-
if (!cost) {
|
|
231
|
-
return text(`No cost data found for session ${sessionId}. Cost data is recorded automatically when sessions end via the session-end hook. If the session is still active, data will appear after it completes. Try: ${p('cost_trend')} { days: 30 } to see aggregate cost data instead.`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Recalculate per-category costs from token counts and model
|
|
235
|
-
const modelPricing = getModelPricing();
|
|
236
|
-
const mp = modelPricing[cost.model as string] ?? modelPricing['claude-sonnet-4-5'] ?? { input_per_million: 3.00, output_per_million: 15.00 };
|
|
237
|
-
const inputCost = ((cost.input_tokens as number) / 1_000_000) * mp.input_per_million;
|
|
238
|
-
const outputCost = ((cost.output_tokens as number) / 1_000_000) * mp.output_per_million;
|
|
239
|
-
const cacheReadCost = ((cost.cache_read_tokens as number) / 1_000_000) * (mp.cache_read_per_million ?? 0);
|
|
240
|
-
const cacheWriteCost = ((cost.cache_write_tokens as number) / 1_000_000) * (mp.cache_write_per_million ?? 0);
|
|
241
|
-
|
|
242
|
-
const lines = [
|
|
243
|
-
`## Session Cost: $${(cost.estimated_cost_usd as number).toFixed(4)}`,
|
|
244
|
-
`Model: ${cost.model}`,
|
|
245
|
-
'',
|
|
246
|
-
'### Token Usage',
|
|
247
|
-
`| Type | Tokens | Est. Cost |`,
|
|
248
|
-
`|------|--------|-----------|`,
|
|
249
|
-
`| Input | ${(cost.input_tokens as number).toLocaleString()} | $${inputCost.toFixed(4)} |`,
|
|
250
|
-
`| Output | ${(cost.output_tokens as number).toLocaleString()} | $${outputCost.toFixed(4)} |`,
|
|
251
|
-
`| Cache Read | ${(cost.cache_read_tokens as number).toLocaleString()} | $${cacheReadCost.toFixed(4)} |`,
|
|
252
|
-
`| Cache Write | ${(cost.cache_write_tokens as number).toLocaleString()} | $${cacheWriteCost.toFixed(4)} |`,
|
|
253
|
-
`| **Total** | ${(cost.total_tokens as number).toLocaleString()} | **$${(cost.estimated_cost_usd as number).toFixed(4)}** |`,
|
|
254
|
-
];
|
|
255
|
-
|
|
256
|
-
return text(lines.join('\n'));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function handleCostTrend(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
260
|
-
const days = (args.days as number) ?? 30;
|
|
261
|
-
const groupBy = (args.group_by as string) ?? 'day';
|
|
262
|
-
|
|
263
|
-
if (!['day', 'week'].includes(groupBy)) {
|
|
264
|
-
return text(`Invalid group_by value: "${groupBy}". Use "day" or "week". Example: ${p('cost_trend')} { days: 30, group_by: "week" }`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Use separate prepared statements to avoid SQL string interpolation.
|
|
268
|
-
// dateFormat is validated by the allowlist above, but parameterized SQL
|
|
269
|
-
// is the enterprise-grade approach regardless.
|
|
270
|
-
const sql = groupBy === 'week'
|
|
271
|
-
? `SELECT strftime('%Y-W%W', created_at) as period,
|
|
272
|
-
COUNT(*) as sessions,
|
|
273
|
-
SUM(estimated_cost_usd) as total_cost,
|
|
274
|
-
SUM(total_tokens) as total_tokens,
|
|
275
|
-
AVG(estimated_cost_usd) as avg_cost
|
|
276
|
-
FROM session_costs
|
|
277
|
-
WHERE created_at >= datetime('now', ?)
|
|
278
|
-
GROUP BY period
|
|
279
|
-
ORDER BY period ASC`
|
|
280
|
-
: `SELECT strftime('%Y-%m-%d', created_at) as period,
|
|
281
|
-
COUNT(*) as sessions,
|
|
282
|
-
SUM(estimated_cost_usd) as total_cost,
|
|
283
|
-
SUM(total_tokens) as total_tokens,
|
|
284
|
-
AVG(estimated_cost_usd) as avg_cost
|
|
285
|
-
FROM session_costs
|
|
286
|
-
WHERE created_at >= datetime('now', ?)
|
|
287
|
-
GROUP BY period
|
|
288
|
-
ORDER BY period ASC`;
|
|
289
|
-
|
|
290
|
-
const rows = db.prepare(sql).all(`-${days} days`) as Array<Record<string, unknown>>;
|
|
291
|
-
|
|
292
|
-
if (rows.length === 0) {
|
|
293
|
-
return text(`No cost data found in the last ${days} days. Cost tracking records token usage automatically at session end via hooks. Ensure session-end hooks are configured in your settings. Try: ${p('cost_session')} { session_id: "..." } to check a specific session.`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const totalCost = rows.reduce((sum, r) => sum + (r.total_cost as number), 0);
|
|
297
|
-
const totalSessions = rows.reduce((sum, r) => sum + (r.sessions as number), 0);
|
|
298
|
-
|
|
299
|
-
const lines = [
|
|
300
|
-
`## Cost Trend (${days} days)`,
|
|
301
|
-
`Total: $${totalCost.toFixed(2)} across ${totalSessions} sessions`,
|
|
302
|
-
`Average per session: $${(totalCost / totalSessions).toFixed(4)}`,
|
|
303
|
-
'',
|
|
304
|
-
`### By ${groupBy === 'week' ? 'Week' : 'Day'}`,
|
|
305
|
-
`| Period | Sessions | Cost | Avg/Session |`,
|
|
306
|
-
`|--------|----------|------|-------------|`,
|
|
307
|
-
];
|
|
308
|
-
|
|
309
|
-
for (const row of rows) {
|
|
310
|
-
lines.push(
|
|
311
|
-
`| ${row.period} | ${row.sessions} | $${(row.total_cost as number).toFixed(2)} | $${(row.avg_cost as number).toFixed(4)} |`
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return text(lines.join('\n'));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function handleCostFeature(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
319
|
-
const days = (args.days as number) ?? 30;
|
|
320
|
-
|
|
321
|
-
const rows = db.prepare(`
|
|
322
|
-
SELECT feature_key, SUM(estimated_cost_usd) as total_cost, SUM(tokens_used) as total_tokens, COUNT(*) as entries
|
|
323
|
-
FROM feature_costs
|
|
324
|
-
WHERE created_at >= datetime('now', ?)
|
|
325
|
-
GROUP BY feature_key
|
|
326
|
-
ORDER BY total_cost DESC
|
|
327
|
-
`).all(`-${days} days`) as Array<Record<string, unknown>>;
|
|
328
|
-
|
|
329
|
-
if (rows.length === 0) {
|
|
330
|
-
return text(`No feature cost data found in the last ${days} days. Feature costs are attributed automatically when sessions work on registered features. Ensure features are registered, then costs are tracked per-session. Try: ${p('cost_trend')} { days: 30 } for session-level cost trends instead.`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const totalCost = rows.reduce((sum, r) => sum + (r.total_cost as number), 0);
|
|
334
|
-
|
|
335
|
-
const lines = [
|
|
336
|
-
`## Feature Cost Attribution (${days} days)`,
|
|
337
|
-
`Total: $${totalCost.toFixed(2)} across ${rows.length} features`,
|
|
338
|
-
'',
|
|
339
|
-
'| Feature | Cost | Tokens | Sessions | % of Total |',
|
|
340
|
-
'|---------|------|--------|----------|------------|',
|
|
341
|
-
];
|
|
342
|
-
|
|
343
|
-
for (const row of rows) {
|
|
344
|
-
const pct = totalCost > 0 ? ((row.total_cost as number) / totalCost * 100).toFixed(1) : '0';
|
|
345
|
-
lines.push(
|
|
346
|
-
`| ${row.feature_key} | $${(row.total_cost as number).toFixed(2)} | ${(row.total_tokens as number).toLocaleString()} | ${row.entries} | ${pct}% |`
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return text(lines.join('\n'));
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function text(content: string): ToolResult {
|
|
354
|
-
return { content: [{ type: 'text', text: content }] };
|
|
355
|
-
}
|