smart-context-mcp 1.1.0 → 1.2.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.
@@ -0,0 +1,410 @@
1
+ import { withStateDb } from './storage/sqlite.js';
2
+
3
+ const isWorkflowTrackingAvailable = () => {
4
+ try {
5
+ return withStateDb((db) => workflowTableExists(db));
6
+ } catch {
7
+ return false;
8
+ }
9
+ };
10
+
11
+ // Workflow definitions with typical tool sequences and baselines
12
+ const WORKFLOW_DEFINITIONS = {
13
+ debugging: {
14
+ name: 'Debugging',
15
+ description: 'Error-first, symbol-focused debugging workflow',
16
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_shell'],
17
+ minTools: 3,
18
+ baselineTokens: 150000, // Typical: read 10 full files, grep output, test logs
19
+ pattern: /debug|error|bug|fix|fail/i,
20
+ },
21
+ 'code-review': {
22
+ name: 'Code Review',
23
+ description: 'Diff-aware, API-focused code review workflow',
24
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
25
+ minTools: 3,
26
+ baselineTokens: 200000, // Typical: read 15 full files, diff output, test logs
27
+ pattern: /review|pr|pull.?request|approve/i,
28
+ },
29
+ refactoring: {
30
+ name: 'Refactoring',
31
+ description: 'Graph-aware, test-verified refactoring workflow',
32
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
33
+ minTools: 3,
34
+ baselineTokens: 180000, // Typical: read 12 full files, dependency graph, test logs
35
+ pattern: /refactor|extract|rename|move|restructure/i,
36
+ },
37
+ testing: {
38
+ name: 'Testing',
39
+ description: 'Coverage-aware, TDD-friendly testing workflow',
40
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_context', 'smart_shell'],
41
+ minTools: 3,
42
+ baselineTokens: 120000, // Typical: read 8 full files, test patterns, test logs
43
+ pattern: /test|spec|coverage|tdd/i,
44
+ },
45
+ architecture: {
46
+ name: 'Architecture Exploration',
47
+ description: 'Index-first, minimal-detail architecture exploration',
48
+ typicalTools: ['smart_turn', 'smart_context', 'smart_search', 'smart_read', 'cross_project'],
49
+ minTools: 3,
50
+ baselineTokens: 300000, // Typical: read 20 full files, explore structure
51
+ pattern: /architect|explore|understand|structure|design/i,
52
+ },
53
+ };
54
+
55
+ /**
56
+ * Detect workflow type based on session goal, tools used, and patterns
57
+ */
58
+ export const detectWorkflowType = (sessionGoal, toolsUsed) => {
59
+ if (!sessionGoal && toolsUsed.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ // Try to match based on session goal
64
+ if (sessionGoal) {
65
+ for (const [type, def] of Object.entries(WORKFLOW_DEFINITIONS)) {
66
+ if (def.pattern.test(sessionGoal)) {
67
+ return type;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Try to match based on tools used
73
+ for (const [type, def] of Object.entries(WORKFLOW_DEFINITIONS)) {
74
+ const matchingTools = toolsUsed.filter((tool) => def.typicalTools.includes(tool));
75
+ if (matchingTools.length >= def.minTools) {
76
+ return type;
77
+ }
78
+ }
79
+
80
+ return null;
81
+ };
82
+
83
+ /**
84
+ * Calculate baseline tokens for a workflow type
85
+ */
86
+ export const getWorkflowBaseline = (workflowType) => {
87
+ const def = WORKFLOW_DEFINITIONS[workflowType];
88
+ return def ? def.baselineTokens : 0;
89
+ };
90
+
91
+ /**
92
+ * Start tracking a workflow
93
+ */
94
+ export const startWorkflow = (workflowType, sessionId, metadata = {}) => {
95
+ try {
96
+ return withStateDb((db) => {
97
+ if (!workflowTableExists(db)) {
98
+ return null;
99
+ }
100
+
101
+ const now = new Date().toISOString();
102
+ const stmt = db.prepare(`
103
+ INSERT INTO workflow_metrics (
104
+ workflow_type,
105
+ session_id,
106
+ start_time,
107
+ baseline_tokens,
108
+ metadata_json,
109
+ created_at
110
+ ) VALUES (?, ?, ?, ?, ?, ?)
111
+ `);
112
+
113
+ const result = stmt.run(
114
+ workflowType,
115
+ sessionId,
116
+ now,
117
+ getWorkflowBaseline(workflowType),
118
+ JSON.stringify(metadata),
119
+ now,
120
+ );
121
+
122
+ return result.lastInsertRowid;
123
+ });
124
+ } catch {
125
+ return null;
126
+ }
127
+ };
128
+
129
+ /**
130
+ * End tracking a workflow and calculate metrics
131
+ */
132
+ export const endWorkflow = (workflowId) => {
133
+ try {
134
+ return withStateDb((db) => {
135
+ if (!workflowTableExists(db)) {
136
+ return null;
137
+ }
138
+
139
+ // Get workflow start time and session
140
+ const workflow = db
141
+ .prepare(
142
+ `
143
+ SELECT workflow_type, session_id, start_time, baseline_tokens
144
+ FROM workflow_metrics
145
+ WHERE workflow_id = ?
146
+ `,
147
+ )
148
+ .get(workflowId);
149
+
150
+ if (!workflow) {
151
+ throw new Error(`Workflow ${workflowId} not found`);
152
+ }
153
+
154
+ const now = new Date().toISOString();
155
+ const startTime = new Date(workflow.start_time);
156
+ const endTime = new Date(now);
157
+ const durationMs = endTime - startTime;
158
+
159
+ // Get all metrics for this session since workflow start
160
+ const metrics = db
161
+ .prepare(
162
+ `
163
+ SELECT tool, raw_tokens, compressed_tokens, saved_tokens
164
+ FROM metrics_events
165
+ WHERE session_id = ? AND created_at >= ?
166
+ ORDER BY created_at ASC
167
+ `,
168
+ )
169
+ .all(workflow.session_id, workflow.start_time);
170
+
171
+ // Calculate totals
172
+ const rawTokens = metrics.reduce((sum, m) => sum + (m.raw_tokens || 0), 0);
173
+ const compressedTokens = metrics.reduce((sum, m) => sum + (m.compressed_tokens || 0), 0);
174
+ const savedTokens = metrics.reduce((sum, m) => sum + (m.saved_tokens || 0), 0);
175
+ const savingsPct = rawTokens > 0 ? ((savedTokens / rawTokens) * 100).toFixed(2) : 0;
176
+
177
+ // Calculate vs baseline
178
+ const baselineTokens = workflow.baseline_tokens || 0;
179
+ const vsBaselinePct = baselineTokens > 0 ? (((baselineTokens - compressedTokens) / baselineTokens) * 100).toFixed(2) : 0;
180
+
181
+ // Get unique tools used
182
+ const toolsUsed = [...new Set(metrics.map((m) => m.tool))];
183
+
184
+ // Update workflow
185
+ const stmt = db.prepare(`
186
+ UPDATE workflow_metrics
187
+ SET end_time = ?,
188
+ duration_ms = ?,
189
+ tools_used_json = ?,
190
+ steps_count = ?,
191
+ raw_tokens = ?,
192
+ compressed_tokens = ?,
193
+ saved_tokens = ?,
194
+ savings_pct = ?,
195
+ vs_baseline_pct = ?
196
+ WHERE workflow_id = ?
197
+ `);
198
+
199
+ stmt.run(now, durationMs, JSON.stringify(toolsUsed), metrics.length, rawTokens, compressedTokens, savedTokens, savingsPct, vsBaselinePct, workflowId);
200
+
201
+ return {
202
+ workflowId,
203
+ workflowType: workflow.workflow_type,
204
+ durationMs,
205
+ toolsUsed,
206
+ stepsCount: metrics.length,
207
+ rawTokens,
208
+ compressedTokens,
209
+ savedTokens,
210
+ savingsPct: Number(savingsPct),
211
+ baselineTokens,
212
+ vsBaselinePct: Number(vsBaselinePct),
213
+ };
214
+ });
215
+ } catch {
216
+ return null;
217
+ }
218
+ };
219
+
220
+ /**
221
+ * Get workflow metrics summary
222
+ */
223
+ export const getWorkflowMetrics = (options = {}) => {
224
+ try {
225
+ return withStateDb((db) => {
226
+ if (!workflowTableExists(db)) {
227
+ return [];
228
+ }
229
+ let query = `
230
+ SELECT
231
+ workflow_id,
232
+ workflow_type,
233
+ session_id,
234
+ start_time,
235
+ end_time,
236
+ duration_ms,
237
+ tools_used_json,
238
+ steps_count,
239
+ raw_tokens,
240
+ compressed_tokens,
241
+ saved_tokens,
242
+ savings_pct,
243
+ baseline_tokens,
244
+ vs_baseline_pct,
245
+ metadata_json,
246
+ created_at
247
+ FROM workflow_metrics
248
+ WHERE 1=1
249
+ `;
250
+
251
+ const params = [];
252
+
253
+ if (options.workflowType) {
254
+ query += ' AND workflow_type = ?';
255
+ params.push(options.workflowType);
256
+ }
257
+
258
+ if (options.sessionId) {
259
+ query += ' AND session_id = ?';
260
+ params.push(options.sessionId);
261
+ }
262
+
263
+ if (options.completed !== undefined) {
264
+ if (options.completed) {
265
+ query += ' AND end_time IS NOT NULL';
266
+ } else {
267
+ query += ' AND end_time IS NULL';
268
+ }
269
+ }
270
+
271
+ query += ' ORDER BY created_at DESC';
272
+
273
+ if (options.limit) {
274
+ query += ' LIMIT ?';
275
+ params.push(options.limit);
276
+ }
277
+
278
+ const workflows = db.prepare(query).all(...params);
279
+
280
+ return workflows.map((w) => ({
281
+ ...w,
282
+ toolsUsed: JSON.parse(w.tools_used_json || '[]'),
283
+ metadata: JSON.parse(w.metadata_json || '{}'),
284
+ }));
285
+ });
286
+ } catch {
287
+ return [];
288
+ }
289
+ };
290
+
291
+ /**
292
+ * Get workflow summary by type
293
+ */
294
+ export const getWorkflowSummaryByType = () => {
295
+ try {
296
+ return withStateDb((db) => {
297
+ if (!workflowTableExists(db)) {
298
+ return [];
299
+ }
300
+ const summary = db
301
+ .prepare(
302
+ `
303
+ SELECT
304
+ workflow_type,
305
+ COUNT(*) as count,
306
+ SUM(raw_tokens) as total_raw_tokens,
307
+ SUM(compressed_tokens) as total_compressed_tokens,
308
+ SUM(saved_tokens) as total_saved_tokens,
309
+ AVG(savings_pct) as avg_savings_pct,
310
+ SUM(baseline_tokens) as total_baseline_tokens,
311
+ AVG(vs_baseline_pct) as avg_vs_baseline_pct,
312
+ AVG(duration_ms) as avg_duration_ms,
313
+ AVG(steps_count) as avg_steps_count
314
+ FROM workflow_metrics
315
+ WHERE end_time IS NOT NULL
316
+ GROUP BY workflow_type
317
+ ORDER BY count DESC
318
+ `,
319
+ )
320
+ .all();
321
+
322
+ return summary.map((s) => ({
323
+ ...s,
324
+ avgSavingsPct: Number(s.avg_savings_pct?.toFixed(2) || 0),
325
+ avgVsBaselinePct: Number(s.avg_vs_baseline_pct?.toFixed(2) || 0),
326
+ avgDurationMs: Math.round(s.avg_duration_ms || 0),
327
+ avgStepsCount: Math.round(s.avg_steps_count || 0),
328
+ }));
329
+ });
330
+ } catch {
331
+ return [];
332
+ }
333
+ };
334
+
335
+ /**
336
+ * Check if workflow_metrics table exists
337
+ */
338
+ const workflowTableExists = (db) => {
339
+ try {
340
+ const result = db
341
+ .prepare(
342
+ `
343
+ SELECT name FROM sqlite_master
344
+ WHERE type='table' AND name='workflow_metrics'
345
+ `,
346
+ )
347
+ .get();
348
+ return Boolean(result);
349
+ } catch {
350
+ return false;
351
+ }
352
+ };
353
+
354
+ /**
355
+ * Auto-detect and track workflow from session
356
+ */
357
+ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
358
+ try {
359
+ return withStateDb((db) => {
360
+ // Check if table exists (migration v5)
361
+ if (!workflowTableExists(db)) {
362
+ return null;
363
+ }
364
+
365
+ // Check if workflow already tracked for this session
366
+ const existing = db
367
+ .prepare(
368
+ `
369
+ SELECT workflow_id
370
+ FROM workflow_metrics
371
+ WHERE session_id = ? AND end_time IS NULL
372
+ ORDER BY created_at DESC
373
+ LIMIT 1
374
+ `,
375
+ )
376
+ .get(sessionId);
377
+
378
+ if (existing) {
379
+ return existing.workflow_id;
380
+ }
381
+
382
+ // Get tools used so far in this session
383
+ const metrics = db
384
+ .prepare(
385
+ `
386
+ SELECT DISTINCT tool
387
+ FROM metrics_events
388
+ WHERE session_id = ?
389
+ `,
390
+ )
391
+ .all(sessionId);
392
+
393
+ const toolsUsed = metrics.map((m) => m.tool);
394
+
395
+ // Detect workflow type
396
+ const workflowType = detectWorkflowType(sessionGoal, toolsUsed);
397
+
398
+ if (!workflowType) {
399
+ return null;
400
+ }
401
+
402
+ // Start tracking
403
+ return startWorkflow(workflowType, sessionId, { autoDetected: true, goal: sessionGoal });
404
+ });
405
+ } catch {
406
+ return null;
407
+ }
408
+ };
409
+
410
+ export { WORKFLOW_DEFINITIONS };