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.
- package/README.md +110 -20
- package/package.json +10 -7
- package/scripts/init-clients.js +56 -27
- package/scripts/report-metrics.js +5 -0
- package/scripts/report-workflow-metrics.js +255 -0
- package/src/analytics/adoption.js +197 -0
- package/src/storage/sqlite.js +30 -1
- package/src/tools/smart-metrics.js +7 -0
- package/src/tools/smart-read-batch.js +9 -0
- package/src/tools/smart-read.js +21 -1
- package/src/tools/smart-shell.js +33 -9
- package/src/tools/smart-turn.js +1 -0
- package/src/workflow-tracker-stub.js +53 -0
- package/src/workflow-tracker.js +410 -0
|
@@ -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 };
|