smart-context-mcp 1.0.4 → 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 +196 -586
- package/package.json +11 -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/cache-warming.js +131 -0
- package/src/context-patterns.js +192 -0
- package/src/cross-project.js +343 -0
- package/src/diff-analysis.js +291 -0
- package/src/git-blame.js +324 -0
- package/src/index.js +54 -5
- package/src/metrics.js +6 -1
- package/src/server.js +199 -13
- package/src/storage/sqlite.js +50 -1
- package/src/streaming.js +152 -0
- package/src/tools/smart-context.js +115 -6
- 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
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readMetricsEntries,
|
|
15
15
|
resolveMetricsInput,
|
|
16
16
|
} from '../metrics.js';
|
|
17
|
+
import { analyzeAdoption } from '../analytics/adoption.js';
|
|
17
18
|
|
|
18
19
|
const WINDOW_MS = {
|
|
19
20
|
'24h': 24 * 60 * 60 * 1000,
|
|
@@ -197,6 +198,8 @@ export const smartMetrics = async ({
|
|
|
197
198
|
.filter((entry) => (tool ? entry.tool === tool : true))
|
|
198
199
|
.filter((entry) => (resolvedSessionId ? entry.sessionId === resolvedSessionId : true));
|
|
199
200
|
|
|
201
|
+
const adoption = analyzeAdoption(filteredEntries);
|
|
202
|
+
|
|
200
203
|
return {
|
|
201
204
|
filePath: resolved.storagePath,
|
|
202
205
|
storagePath: resolved.storagePath,
|
|
@@ -212,6 +215,7 @@ export const smartMetrics = async ({
|
|
|
212
215
|
},
|
|
213
216
|
invalidLines,
|
|
214
217
|
summary: aggregateMetrics(filteredEntries),
|
|
218
|
+
adoption,
|
|
215
219
|
latestEntries: buildLatestEntries(filteredEntries, latest),
|
|
216
220
|
};
|
|
217
221
|
}
|
|
@@ -229,6 +233,8 @@ export const smartMetrics = async ({
|
|
|
229
233
|
window,
|
|
230
234
|
});
|
|
231
235
|
|
|
236
|
+
const adoption = analyzeAdoption(entries);
|
|
237
|
+
|
|
232
238
|
return {
|
|
233
239
|
filePath: resolved.storagePath,
|
|
234
240
|
storagePath: resolved.storagePath,
|
|
@@ -244,6 +250,7 @@ export const smartMetrics = async ({
|
|
|
244
250
|
},
|
|
245
251
|
invalidLines,
|
|
246
252
|
summary: aggregateMetrics(entries),
|
|
253
|
+
adoption,
|
|
247
254
|
latestEntries: buildLatestEntries(entries, latest),
|
|
248
255
|
};
|
|
249
256
|
};
|
|
@@ -18,6 +18,15 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
18
18
|
maxTokens: item.maxTokens,
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
if (readResult.error) {
|
|
22
|
+
results.push({
|
|
23
|
+
filePath: item.path,
|
|
24
|
+
mode: item.mode ?? 'outline',
|
|
25
|
+
error: readResult.error,
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
const itemTokens = countTokens(readResult.content);
|
|
22
31
|
|
|
23
32
|
if (maxTokens && totalTokens + itemTokens > maxTokens && results.length > 0) {
|
package/src/tools/smart-read.js
CHANGED
|
@@ -363,7 +363,27 @@ const formatContextSections = (sections) => {
|
|
|
363
363
|
};
|
|
364
364
|
|
|
365
365
|
export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext }) => {
|
|
366
|
-
|
|
366
|
+
let fullPath, content;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const result = readTextFile(filePath);
|
|
370
|
+
fullPath = result.fullPath;
|
|
371
|
+
content = result.content;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const errorMessage = error.message || String(error);
|
|
374
|
+
return {
|
|
375
|
+
error: errorMessage,
|
|
376
|
+
filePath,
|
|
377
|
+
mode,
|
|
378
|
+
metrics: buildMetrics({
|
|
379
|
+
tool: 'smart_read',
|
|
380
|
+
target: filePath,
|
|
381
|
+
rawText: '',
|
|
382
|
+
compressedText: errorMessage,
|
|
383
|
+
}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
367
387
|
const extension = path.extname(fullPath).toLowerCase();
|
|
368
388
|
const mtime = getFileMtime(fullPath);
|
|
369
389
|
|
package/src/tools/smart-shell.js
CHANGED
|
@@ -6,11 +6,21 @@ import { projectRoot } from '../utils/paths.js';
|
|
|
6
6
|
import { pickRelevantLines, truncate, uniqueLines } from '../utils/text.js';
|
|
7
7
|
|
|
8
8
|
const execFile = promisify(execFileCallback);
|
|
9
|
-
const
|
|
9
|
+
const isShellDisabled = () => process.env.DEVCTX_SHELL_DISABLED === 'true';
|
|
10
|
+
const blockedPattern = /[|&;<>`\n\r$()]/;
|
|
10
11
|
const allowedCommands = new Set(['pwd', 'ls', 'find', 'rg', 'git', 'npm', 'pnpm', 'yarn', 'bun']);
|
|
11
|
-
const allowedGitSubcommands = new Set(['status', 'diff', 'show', 'log', 'branch', 'rev-parse']);
|
|
12
|
+
const allowedGitSubcommands = new Set(['status', 'diff', 'show', 'log', 'branch', 'rev-parse', 'blame']);
|
|
12
13
|
const allowedPackageManagerSubcommands = new Set(['test', 'run', 'lint', 'build', 'typecheck', 'check']);
|
|
13
|
-
const safeRunScriptPattern = /^(test|lint|build|typecheck|check|smoke|verify)(:|$)/;
|
|
14
|
+
const safeRunScriptPattern = /^(test|lint|build|typecheck|check|smoke|verify|eval)(:|$)/;
|
|
15
|
+
const dangerousPatterns = [
|
|
16
|
+
/rm\s+-rf/i,
|
|
17
|
+
/sudo/i,
|
|
18
|
+
/curl.*\|/i,
|
|
19
|
+
/wget.*\|/i,
|
|
20
|
+
/eval/i,
|
|
21
|
+
/exec/i,
|
|
22
|
+
];
|
|
23
|
+
const MAX_COMMAND_LENGTH = 500;
|
|
14
24
|
|
|
15
25
|
const tokenize = (command) => {
|
|
16
26
|
const tokens = [];
|
|
@@ -67,12 +77,26 @@ const tokenize = (command) => {
|
|
|
67
77
|
};
|
|
68
78
|
|
|
69
79
|
const validateCommand = (command, tokens) => {
|
|
80
|
+
if (isShellDisabled()) {
|
|
81
|
+
return 'Shell execution is disabled (DEVCTX_SHELL_DISABLED=true)';
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
if (!command.trim()) {
|
|
71
85
|
return 'Command is empty';
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
88
|
+
if (command.length > MAX_COMMAND_LENGTH) {
|
|
89
|
+
return `Command too long (max ${MAX_COMMAND_LENGTH} chars)`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (blockedPattern.test(command)) {
|
|
93
|
+
return 'Shell operators are not allowed (|, &, ;, <, >, `, $, (, ))';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const pattern of dangerousPatterns) {
|
|
97
|
+
if (pattern.test(command)) {
|
|
98
|
+
return `Dangerous pattern detected: ${pattern.source}`;
|
|
99
|
+
}
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
if (tokens.length === 0) {
|
|
@@ -82,11 +106,11 @@ const validateCommand = (command, tokens) => {
|
|
|
82
106
|
const [baseCommand, subcommand, thirdToken] = tokens;
|
|
83
107
|
|
|
84
108
|
if (!allowedCommands.has(baseCommand)) {
|
|
85
|
-
return `Command not allowed: ${baseCommand}`;
|
|
109
|
+
return `Command not allowed: ${baseCommand}. Allowed: ${[...allowedCommands].join(', ')}`;
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
if (baseCommand === 'git' && !allowedGitSubcommands.has(subcommand)) {
|
|
89
|
-
return `Git subcommand not allowed: ${subcommand ?? '(missing)'}`;
|
|
113
|
+
return `Git subcommand not allowed: ${subcommand ?? '(missing)'}. Allowed: ${[...allowedGitSubcommands].join(', ')}`;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
if (baseCommand === 'find') {
|
|
@@ -99,11 +123,11 @@ const validateCommand = (command, tokens) => {
|
|
|
99
123
|
|
|
100
124
|
if (['npm', 'pnpm', 'yarn', 'bun'].includes(baseCommand)) {
|
|
101
125
|
if (!subcommand || !allowedPackageManagerSubcommands.has(subcommand)) {
|
|
102
|
-
return `Package manager subcommand not allowed: ${subcommand ?? '(missing)'}`;
|
|
126
|
+
return `Package manager subcommand not allowed: ${subcommand ?? '(missing)'}. Allowed: ${[...allowedPackageManagerSubcommands].join(', ')}`;
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
if (subcommand === 'run' && (!thirdToken || !safeRunScriptPattern.test(thirdToken))) {
|
|
106
|
-
return `Package manager script not allowed: ${thirdToken ?? '(missing)'}`;
|
|
130
|
+
return `Package manager script not allowed: ${thirdToken ?? '(missing)'}. Allowed pattern: ${safeRunScriptPattern.source}`;
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
133
|
|
package/src/tools/smart-turn.js
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Stub for workflow tracking (to be implemented in future version)
|
|
2
|
+
// This avoids SQLite issues in tests while keeping the API available
|
|
3
|
+
|
|
4
|
+
export const detectWorkflowType = () => null;
|
|
5
|
+
export const getWorkflowBaseline = () => 0;
|
|
6
|
+
export const startWorkflow = () => null;
|
|
7
|
+
export const endWorkflow = () => null;
|
|
8
|
+
export const getWorkflowMetrics = () => [];
|
|
9
|
+
export const getWorkflowSummaryByType = () => [];
|
|
10
|
+
export const autoTrackWorkflow = () => null;
|
|
11
|
+
|
|
12
|
+
export 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
51
|
+
pattern: /architect|explore|understand|structure|design/i,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -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 };
|