@massu/core 1.10.8 → 1.12.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.
Files changed (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -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
- }