chekk 0.2.2 → 0.2.4
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/bin/chekk.js +1 -1
- package/package.json +1 -1
- package/src/detect.js +76 -24
- package/src/display.js +72 -17
- package/src/index.js +9 -14
- package/src/metrics/ai-leverage.js +27 -3
- package/src/metrics/debug-cycles.js +24 -1
- package/src/metrics/decomposition.js +25 -0
- package/src/metrics/session-structure.js +20 -0
- package/src/parsers/codex.js +188 -0
- package/src/parsers/cursor.js +281 -0
- package/src/upload.js +14 -0
package/bin/chekk.js
CHANGED
|
@@ -8,7 +8,7 @@ const program = new Command();
|
|
|
8
8
|
program
|
|
9
9
|
.name('chekk')
|
|
10
10
|
.description('The engineering capability score. See how you prompt.')
|
|
11
|
-
.version('0.2.
|
|
11
|
+
.version('0.2.4')
|
|
12
12
|
.option('--offline', 'Skip AI prose generation, show data-driven output')
|
|
13
13
|
.option('--verbose', 'Show detailed per-project and per-metric breakdowns')
|
|
14
14
|
.option('--json', 'Output raw metrics as JSON')
|
package/package.json
CHANGED
package/src/detect.js
CHANGED
|
@@ -9,7 +9,7 @@ export function detectTools() {
|
|
|
9
9
|
const home = homedir();
|
|
10
10
|
const results = [];
|
|
11
11
|
|
|
12
|
-
// Claude Code: ~/.claude/projects/
|
|
12
|
+
// ── Claude Code: ~/.claude/projects/ ──
|
|
13
13
|
const claudeProjectsDir = join(home, '.claude', 'projects');
|
|
14
14
|
if (existsSync(claudeProjectsDir)) {
|
|
15
15
|
const projects = readdirSync(claudeProjectsDir).filter(f => {
|
|
@@ -29,11 +29,10 @@ export function detectTools() {
|
|
|
29
29
|
|
|
30
30
|
if (jsonlFiles.length > 0) {
|
|
31
31
|
totalSessions += jsonlFiles.length;
|
|
32
|
-
// Rough line count for display
|
|
33
32
|
for (const file of jsonlFiles) {
|
|
34
33
|
try {
|
|
35
34
|
const stat = statSync(join(projectDir, file));
|
|
36
|
-
totalLines += Math.floor(stat.size / 500);
|
|
35
|
+
totalLines += Math.floor(stat.size / 500);
|
|
37
36
|
} catch {}
|
|
38
37
|
}
|
|
39
38
|
projectDetails.push({
|
|
@@ -55,39 +54,92 @@ export function detectTools() {
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
// Cursor:
|
|
57
|
+
// ── Cursor: workspaceStorage with state.vscdb files ──
|
|
59
58
|
const cursorPaths = [
|
|
60
59
|
join(home, 'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage'), // macOS
|
|
61
60
|
join(home, '.config', 'Cursor', 'User', 'workspaceStorage'), // Linux
|
|
62
61
|
join(home, 'AppData', 'Roaming', 'Cursor', 'User', 'workspaceStorage'), // Windows
|
|
63
62
|
];
|
|
63
|
+
|
|
64
64
|
for (const cursorPath of cursorPaths) {
|
|
65
65
|
if (existsSync(cursorPath)) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
// Count workspaces that have state.vscdb
|
|
67
|
+
let workspaceCount = 0;
|
|
68
|
+
const projectDetails = [];
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const dirs = readdirSync(cursorPath).filter(f => {
|
|
72
|
+
try { return statSync(join(cursorPath, f)).isDirectory(); } catch { return false; }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
const dbPath = join(cursorPath, dir, 'state.vscdb');
|
|
77
|
+
if (existsSync(dbPath)) {
|
|
78
|
+
workspaceCount++;
|
|
79
|
+
projectDetails.push({
|
|
80
|
+
name: dir.slice(0, 8),
|
|
81
|
+
sessions: 1,
|
|
82
|
+
path: join(cursorPath, dir),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
if (workspaceCount > 0) {
|
|
89
|
+
results.push({
|
|
90
|
+
tool: 'Cursor',
|
|
91
|
+
sessions: workspaceCount,
|
|
92
|
+
projects: projectDetails,
|
|
93
|
+
estimatedPrompts: 0,
|
|
94
|
+
basePath: cursorPath,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
75
97
|
break;
|
|
76
98
|
}
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
// Codex: ~/.codex/
|
|
101
|
+
// ── Codex: ~/.codex/sessions/ ──
|
|
80
102
|
const codexPath = join(home, '.codex');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
const codexSessionsPath = join(codexPath, 'sessions');
|
|
104
|
+
if (existsSync(codexSessionsPath)) {
|
|
105
|
+
// Count JSONL session files recursively
|
|
106
|
+
let totalSessions = 0;
|
|
107
|
+
const projectDetails = [];
|
|
108
|
+
|
|
109
|
+
function countFiles(dir) {
|
|
110
|
+
try {
|
|
111
|
+
const entries = readdirSync(dir);
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const full = join(dir, entry);
|
|
114
|
+
try {
|
|
115
|
+
const stat = statSync(full);
|
|
116
|
+
if (stat.isDirectory()) {
|
|
117
|
+
countFiles(full);
|
|
118
|
+
} else if (entry.endsWith('.jsonl')) {
|
|
119
|
+
totalSessions++;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
countFiles(codexSessionsPath);
|
|
127
|
+
|
|
128
|
+
if (totalSessions > 0) {
|
|
129
|
+
projectDetails.push({
|
|
130
|
+
name: 'codex',
|
|
131
|
+
sessions: totalSessions,
|
|
132
|
+
path: codexSessionsPath,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
results.push({
|
|
136
|
+
tool: 'Codex',
|
|
137
|
+
sessions: totalSessions,
|
|
138
|
+
projects: projectDetails,
|
|
139
|
+
estimatedPrompts: 0,
|
|
140
|
+
basePath: codexPath,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
91
143
|
}
|
|
92
144
|
|
|
93
145
|
return results;
|
package/src/display.js
CHANGED
|
@@ -42,6 +42,28 @@ function pad(str, len) {
|
|
|
42
42
|
return str + ' '.repeat(Math.max(0, len - visible.length));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function snippet(prompt, maxLen = 70) {
|
|
46
|
+
if (!prompt) return null;
|
|
47
|
+
// Clean up whitespace / newlines
|
|
48
|
+
let clean = prompt.replace(/\s+/g, ' ').trim();
|
|
49
|
+
if (clean.length > maxLen) {
|
|
50
|
+
clean = clean.slice(0, maxLen - 1) + '\u2026';
|
|
51
|
+
}
|
|
52
|
+
return clean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function displaySnippet(prompt, maxLen = 70) {
|
|
56
|
+
const s = snippet(prompt, maxLen);
|
|
57
|
+
if (!s) return;
|
|
58
|
+
console.log(` ${dim('\u201C')}${dim.italic(s)}${dim('\u201D')}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pickExample(examples, type) {
|
|
62
|
+
if (!examples || !examples.length) return null;
|
|
63
|
+
const match = examples.find(e => e.type === type);
|
|
64
|
+
return match ? match.prompt : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
// ── Box drawing ──
|
|
46
68
|
|
|
47
69
|
function box(lines, width = 43) {
|
|
@@ -68,7 +90,7 @@ export function displayHeader() {
|
|
|
68
90
|
console.log();
|
|
69
91
|
const lines = [
|
|
70
92
|
'',
|
|
71
|
-
` ${bold.white('chekk')}${dim(' v0.2.
|
|
93
|
+
` ${bold.white('chekk')}${dim(' v0.2.4')}`,
|
|
72
94
|
` ${dim('the engineering capability score')}`,
|
|
73
95
|
'',
|
|
74
96
|
];
|
|
@@ -84,17 +106,12 @@ export function displayScan(tools) {
|
|
|
84
106
|
console.log(dim(' Scanning local AI tools...\n'));
|
|
85
107
|
|
|
86
108
|
for (const tool of tools) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(
|
|
94
|
-
` ${cyan('\u2726')} ${bold.white(tool.tool)} ` +
|
|
95
|
-
dim(`${sessions} sessions \u00B7 ${projects} projects`)
|
|
96
|
-
);
|
|
97
|
-
}
|
|
109
|
+
const sessions = numberFormat(tool.sessions);
|
|
110
|
+
const projects = tool.projects.length;
|
|
111
|
+
console.log(
|
|
112
|
+
` ${cyan('\u2726')} ${bold.white(tool.tool)} ` +
|
|
113
|
+
dim(`${sessions} sessions \u00B7 ${projects} projects`)
|
|
114
|
+
);
|
|
98
115
|
}
|
|
99
116
|
console.log();
|
|
100
117
|
}
|
|
@@ -186,27 +203,60 @@ export function displayScore(result, prose) {
|
|
|
186
203
|
// ══════════════════════════════════════════════
|
|
187
204
|
|
|
188
205
|
export function displayNarratives(metrics, prose) {
|
|
206
|
+
// Track shown snippets globally to avoid duplicates across sections
|
|
207
|
+
const shownSnippets = new Set();
|
|
208
|
+
function showUniqueSnippet(prompt) {
|
|
209
|
+
if (!prompt) return;
|
|
210
|
+
const s = snippet(prompt, 70);
|
|
211
|
+
if (shownSnippets.has(s)) return;
|
|
212
|
+
shownSnippets.add(s);
|
|
213
|
+
displaySnippet(prompt);
|
|
214
|
+
}
|
|
215
|
+
|
|
189
216
|
if (prose && prose.sections) {
|
|
190
|
-
// Use DeepSeek-generated prose
|
|
217
|
+
// Use DeepSeek-generated prose with inline prompt snippets per section
|
|
218
|
+
const sectionSnippetMap = {
|
|
219
|
+
'thinking': pickExample(metrics.decomposition.examples, 'decomposition'),
|
|
220
|
+
'debugging': pickExample(metrics.debugCycles.examples, 'specific_report') || pickExample(metrics.debugCycles.examples, 'quick_fix'),
|
|
221
|
+
'ai leverage': pickExample(metrics.aiLeverage.examples, 'architectural') || pickExample(metrics.aiLeverage.examples, 'planning'),
|
|
222
|
+
'workflow': pickExample(metrics.sessionStructure.examples, 'context_setting') || pickExample(metrics.sessionStructure.examples, 'refinement'),
|
|
223
|
+
};
|
|
224
|
+
|
|
191
225
|
for (const section of prose.sections) {
|
|
192
226
|
console.log(` ${section.emoji} ${bold(section.title.toUpperCase())}`);
|
|
193
227
|
const lines = section.description.split('\n').filter(l => l.trim());
|
|
194
228
|
for (const line of lines) {
|
|
195
229
|
console.log(` ${dim(line.trim())}`);
|
|
196
230
|
}
|
|
231
|
+
// Show relevant prompt snippet under this section
|
|
232
|
+
const titleLower = section.title.toLowerCase();
|
|
233
|
+
const matchedSnippet = sectionSnippetMap[titleLower];
|
|
234
|
+
if (matchedSnippet) showUniqueSnippet(matchedSnippet);
|
|
197
235
|
console.log();
|
|
198
236
|
}
|
|
199
237
|
} else {
|
|
200
|
-
// Fallback: data-driven bullet points
|
|
201
|
-
|
|
238
|
+
// Fallback: data-driven bullet points with inline snippets
|
|
239
|
+
displayDataNarrativesWithTracker(metrics, shownSnippets);
|
|
202
240
|
}
|
|
203
241
|
}
|
|
204
242
|
|
|
205
|
-
function
|
|
243
|
+
function displayDataNarrativesWithTracker(metrics, shownSnippets) {
|
|
206
244
|
const d = metrics.decomposition.details;
|
|
207
245
|
const db = metrics.debugCycles.details;
|
|
208
246
|
const ai = metrics.aiLeverage.details;
|
|
209
247
|
const ss = metrics.sessionStructure.details;
|
|
248
|
+
const dEx = metrics.decomposition.examples || [];
|
|
249
|
+
const dbEx = metrics.debugCycles.examples || [];
|
|
250
|
+
const aiEx = metrics.aiLeverage.examples || [];
|
|
251
|
+
const ssEx = metrics.sessionStructure.examples || [];
|
|
252
|
+
|
|
253
|
+
function showUniqueSnippet(prompt) {
|
|
254
|
+
if (!prompt) return;
|
|
255
|
+
const s = snippet(prompt, 70);
|
|
256
|
+
if (shownSnippets.has(s)) return;
|
|
257
|
+
shownSnippets.add(s);
|
|
258
|
+
displaySnippet(prompt);
|
|
259
|
+
}
|
|
210
260
|
|
|
211
261
|
// Thinking
|
|
212
262
|
console.log(` ${bold('\uD83E\uDDE0 THINKING')}`);
|
|
@@ -214,6 +264,7 @@ function displayDataNarratives(metrics) {
|
|
|
214
264
|
console.log(` ${dim(exchPerSession > 20 ? `${exchPerSession} avg exchanges/session \u2014 marathon builder` : exchPerSession > 8 ? `${exchPerSession} avg exchanges/session \u2014 iterative` : `${exchPerSession} avg exchanges/session \u2014 concise`)}`);
|
|
215
265
|
console.log(` ${dim(d.avgPromptLength > 500 ? `${numberFormat(d.avgPromptLength)} char avg prompt \u2014 thinks out loud` : `${d.avgPromptLength} char avg prompt \u2014 concise communicator`)}`);
|
|
216
266
|
console.log(` ${dim(d.multiStepSessions > d.singleShotSessions * 2 ? 'Multi-step decomposition over single-shot' : 'Mix of multi-step and single-shot sessions')}`);
|
|
267
|
+
showUniqueSnippet(pickExample(dEx, 'decomposition'));
|
|
217
268
|
console.log();
|
|
218
269
|
|
|
219
270
|
// Debugging
|
|
@@ -222,6 +273,7 @@ function displayDataNarratives(metrics) {
|
|
|
222
273
|
console.log(` ${dim(turns <= 2 ? `${turns} turns to resolve \u2014 surgical` : turns <= 4 ? `${turns} turns to resolve \u2014 efficient` : `${turns} turns to resolve \u2014 iterative`)}`);
|
|
223
274
|
console.log(` ${dim(`${db.specificReportRatio}% specific error reports`)}`);
|
|
224
275
|
console.log(` ${dim(db.longLoops === 0 ? 'Zero extended debug loops detected' : `${db.longLoops} extended debug loops`)}`);
|
|
276
|
+
showUniqueSnippet(pickExample(dbEx, 'specific_report') || pickExample(dbEx, 'quick_fix'));
|
|
225
277
|
console.log();
|
|
226
278
|
|
|
227
279
|
// AI Leverage
|
|
@@ -230,6 +282,7 @@ function displayDataNarratives(metrics) {
|
|
|
230
282
|
const codingRatio = ai.toolDiversity.coding > ai.toolDiversity.research ? 'Coding-heavy' : 'Research-heavy';
|
|
231
283
|
console.log(` ${dim(`${codingRatio} over ${ai.toolDiversity.coding > ai.toolDiversity.research ? 'research' : 'coding'}-heavy`)}`);
|
|
232
284
|
console.log(` ${dim(`${ai.architecturalPrompts} architectural, ${ai.planningPrompts} planning, ${ai.exploratoryPrompts} exploratory`)}`);
|
|
285
|
+
showUniqueSnippet(pickExample(aiEx, 'architectural') || pickExample(aiEx, 'planning'));
|
|
233
286
|
console.log();
|
|
234
287
|
|
|
235
288
|
// Workflow
|
|
@@ -237,9 +290,11 @@ function displayDataNarratives(metrics) {
|
|
|
237
290
|
console.log(` ${dim(`${ss.contextSetRatio}% context-setting rate \u2014 ${ss.contextSetRatio > 50 ? 'deliberate' : ss.contextSetRatio > 25 ? 'moderate' : 'low'}`)}`);
|
|
238
291
|
console.log(` ${dim(`${ss.reviewEndRatio}% sessions end with review`)}`);
|
|
239
292
|
console.log(` ${dim(`${ss.refinementRatio}% refinement rate \u2014 ${ss.refinementRatio > 20 ? 'critical eye' : 'accepts readily'}`)}`);
|
|
293
|
+
showUniqueSnippet(pickExample(ssEx, 'context_setting') || pickExample(ssEx, 'refinement'));
|
|
240
294
|
console.log();
|
|
241
295
|
}
|
|
242
296
|
|
|
297
|
+
|
|
243
298
|
// ══════════════════════════════════════════════
|
|
244
299
|
// EASTER EGGS
|
|
245
300
|
// ══════════════════════════════════════════════
|
|
@@ -364,7 +419,7 @@ export function displayVerbose(metrics, sessions) {
|
|
|
364
419
|
|
|
365
420
|
export function displayOffline(result, metrics) {
|
|
366
421
|
displayScore(result, null);
|
|
367
|
-
|
|
422
|
+
displayDataNarrativesWithTracker(metrics, new Set());
|
|
368
423
|
displayEasterEggs(result, metrics);
|
|
369
424
|
console.log(dim(' Run without --offline for personalized AI-generated insights\n'));
|
|
370
425
|
displayEnding(result);
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { detectTools } from './detect.js';
|
|
3
3
|
import { parseAllProjects } from './parsers/claude-code.js';
|
|
4
|
+
import { parseAllWorkspaces } from './parsers/cursor.js';
|
|
5
|
+
import { parseAllSessions as parseCodexSessions } from './parsers/codex.js';
|
|
4
6
|
import { computeDecomposition } from './metrics/decomposition.js';
|
|
5
7
|
import { computeDebugCycles } from './metrics/debug-cycles.js';
|
|
6
8
|
import { computeAILeverage } from './metrics/ai-leverage.js';
|
|
@@ -26,18 +28,7 @@ export async function run(options = {}) {
|
|
|
26
28
|
|
|
27
29
|
if (tools.length === 0) {
|
|
28
30
|
console.log(chalk.dim(' No AI coding tools detected.'));
|
|
29
|
-
console.log(chalk.dim(' Chekk
|
|
30
|
-
console.log(chalk.dim(' Cursor and Codex coming in V2.\n'));
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const supported = tools.filter(t => t.status !== 'detected_not_supported');
|
|
35
|
-
if (supported.length === 0) {
|
|
36
|
-
console.log(chalk.dim(' No supported tools found.\n'));
|
|
37
|
-
for (const t of tools) {
|
|
38
|
-
console.log(chalk.dim(` ${t.tool} \u2014 ${t.message}`));
|
|
39
|
-
}
|
|
40
|
-
console.log();
|
|
31
|
+
console.log(chalk.dim(' Chekk supports Claude Code, Cursor, and Codex.\n'));
|
|
41
32
|
process.exit(1);
|
|
42
33
|
}
|
|
43
34
|
|
|
@@ -45,9 +36,13 @@ export async function run(options = {}) {
|
|
|
45
36
|
|
|
46
37
|
// ── Step 2: Parse sessions ──
|
|
47
38
|
let allSessions = [];
|
|
48
|
-
for (const tool of
|
|
39
|
+
for (const tool of tools) {
|
|
49
40
|
if (tool.tool === 'Claude Code') {
|
|
50
41
|
allSessions.push(...parseAllProjects(tool.basePath));
|
|
42
|
+
} else if (tool.tool === 'Cursor') {
|
|
43
|
+
allSessions.push(...parseAllWorkspaces(tool.basePath));
|
|
44
|
+
} else if (tool.tool === 'Codex') {
|
|
45
|
+
allSessions.push(...parseCodexSessions(tool.basePath));
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
48
|
|
|
@@ -90,7 +85,7 @@ export async function run(options = {}) {
|
|
|
90
85
|
totalExchanges,
|
|
91
86
|
projectCount: projects.length,
|
|
92
87
|
dateRange: dateRangeFull,
|
|
93
|
-
tools:
|
|
88
|
+
tools: tools.map(t => t.tool),
|
|
94
89
|
};
|
|
95
90
|
|
|
96
91
|
// ── JSON output ──
|
|
@@ -37,15 +37,32 @@ export function computeAILeverage(sessions) {
|
|
|
37
37
|
let complexPrompts = 0; // > 200 chars with multiple sentences
|
|
38
38
|
let trivialPrompts = 0; // < 50 chars, simple commands
|
|
39
39
|
|
|
40
|
+
// Capture representative examples
|
|
41
|
+
let bestArchPrompt = null; // best architectural prompt
|
|
42
|
+
let bestPlanPrompt = null; // best planning prompt
|
|
43
|
+
let bestExplorePrompt = null; // best exploratory prompt
|
|
44
|
+
let bestArchLen = 0;
|
|
45
|
+
let bestPlanLen = 0;
|
|
46
|
+
let bestExploreLen = 0;
|
|
47
|
+
|
|
40
48
|
for (const session of sessions) {
|
|
41
49
|
for (const exchange of session.exchanges) {
|
|
42
50
|
const prompt = exchange.userPrompt || '';
|
|
43
51
|
totalPrompts++;
|
|
44
52
|
|
|
45
53
|
// Categorize prompt type
|
|
46
|
-
if (architecturalPatterns.test(prompt))
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
if (architecturalPatterns.test(prompt)) {
|
|
55
|
+
architecturalPrompts++;
|
|
56
|
+
if (prompt.length > bestArchLen) { bestArchLen = prompt.length; bestArchPrompt = prompt; }
|
|
57
|
+
}
|
|
58
|
+
if (planningPatterns.test(prompt)) {
|
|
59
|
+
planningPrompts++;
|
|
60
|
+
if (prompt.length > bestPlanLen) { bestPlanLen = prompt.length; bestPlanPrompt = prompt; }
|
|
61
|
+
}
|
|
62
|
+
if (exploratoryPatterns.test(prompt)) {
|
|
63
|
+
exploratoryPrompts++;
|
|
64
|
+
if (prompt.length > bestExploreLen) { bestExploreLen = prompt.length; bestExplorePrompt = prompt; }
|
|
65
|
+
}
|
|
49
66
|
if (boilerplatePatterns.test(prompt)) boilerplatePrompts++;
|
|
50
67
|
if (testingPatterns.test(prompt)) testingPrompts++;
|
|
51
68
|
|
|
@@ -103,6 +120,12 @@ export function computeAILeverage(sessions) {
|
|
|
103
120
|
50 * 0 // baseline filler
|
|
104
121
|
);
|
|
105
122
|
|
|
123
|
+
// Build examples — pick the best one available
|
|
124
|
+
const examples = [];
|
|
125
|
+
if (bestArchPrompt) examples.push({ type: 'architectural', prompt: bestArchPrompt });
|
|
126
|
+
if (bestPlanPrompt) examples.push({ type: 'planning', prompt: bestPlanPrompt });
|
|
127
|
+
if (bestExplorePrompt) examples.push({ type: 'exploratory', prompt: bestExplorePrompt });
|
|
128
|
+
|
|
106
129
|
return {
|
|
107
130
|
score: Math.max(0, Math.min(100, score)),
|
|
108
131
|
details: {
|
|
@@ -120,5 +143,6 @@ export function computeAILeverage(sessions) {
|
|
|
120
143
|
coding: codingToolUses,
|
|
121
144
|
},
|
|
122
145
|
},
|
|
146
|
+
examples,
|
|
123
147
|
};
|
|
124
148
|
}
|
|
@@ -26,10 +26,16 @@ export function computeDebugCycles(sessions) {
|
|
|
26
26
|
let quickFixes = 0; // resolved in 1-2 turns
|
|
27
27
|
let longLoops = 0; // > 5 turns to resolve
|
|
28
28
|
|
|
29
|
+
// Capture representative examples
|
|
30
|
+
let bestSpecificReport = null; // best specific error report
|
|
31
|
+
let bestQuickFix = null; // prompt that led to quick resolution
|
|
32
|
+
let bestSpecificLen = 0;
|
|
33
|
+
|
|
29
34
|
for (const session of sessions) {
|
|
30
35
|
const { exchanges } = session;
|
|
31
36
|
let inDebugMode = false;
|
|
32
37
|
let debugTurnCount = 0;
|
|
38
|
+
let debugStartPrompt = null;
|
|
33
39
|
|
|
34
40
|
for (let i = 0; i < exchanges.length; i++) {
|
|
35
41
|
const prompt = exchanges[i].userPrompt || '';
|
|
@@ -39,6 +45,7 @@ export function computeDebugCycles(sessions) {
|
|
|
39
45
|
// Starting a new debug sequence
|
|
40
46
|
inDebugMode = true;
|
|
41
47
|
debugTurnCount = 1;
|
|
48
|
+
debugStartPrompt = prompt;
|
|
42
49
|
totalDebugSequences++;
|
|
43
50
|
} else {
|
|
44
51
|
debugTurnCount++;
|
|
@@ -50,21 +57,31 @@ export function computeDebugCycles(sessions) {
|
|
|
50
57
|
}
|
|
51
58
|
if (specificDebugPatterns.test(prompt) || prompt.length > 200) {
|
|
52
59
|
specificReports++;
|
|
60
|
+
// Track best specific report
|
|
61
|
+
if (prompt.length > bestSpecificLen) {
|
|
62
|
+
bestSpecificLen = prompt.length;
|
|
63
|
+
bestSpecificReport = prompt;
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
66
|
} else if (inDebugMode) {
|
|
55
67
|
// Check if this exchange resolves the debug
|
|
56
68
|
if (resolutionPatterns.test(prompt)) {
|
|
57
69
|
totalTurnsToResolve += debugTurnCount;
|
|
58
|
-
if (debugTurnCount <= 2)
|
|
70
|
+
if (debugTurnCount <= 2) {
|
|
71
|
+
quickFixes++;
|
|
72
|
+
if (!bestQuickFix) bestQuickFix = debugStartPrompt;
|
|
73
|
+
}
|
|
59
74
|
if (debugTurnCount > 5) longLoops++;
|
|
60
75
|
inDebugMode = false;
|
|
61
76
|
debugTurnCount = 0;
|
|
77
|
+
debugStartPrompt = null;
|
|
62
78
|
} else {
|
|
63
79
|
// Moved on without explicit resolution
|
|
64
80
|
totalTurnsToResolve += debugTurnCount;
|
|
65
81
|
unresolvedSequences++;
|
|
66
82
|
inDebugMode = false;
|
|
67
83
|
debugTurnCount = 0;
|
|
84
|
+
debugStartPrompt = null;
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
}
|
|
@@ -109,6 +126,11 @@ export function computeDebugCycles(sessions) {
|
|
|
109
126
|
(100 - longLoopPenalty) * 0.15
|
|
110
127
|
);
|
|
111
128
|
|
|
129
|
+
// Build examples array
|
|
130
|
+
const examples = [];
|
|
131
|
+
if (bestSpecificReport) examples.push({ type: 'specific_report', prompt: bestSpecificReport });
|
|
132
|
+
if (bestQuickFix) examples.push({ type: 'quick_fix', prompt: bestQuickFix });
|
|
133
|
+
|
|
112
134
|
return {
|
|
113
135
|
score: Math.max(0, Math.min(100, score)),
|
|
114
136
|
details: {
|
|
@@ -120,5 +142,6 @@ export function computeDebugCycles(sessions) {
|
|
|
120
142
|
vagueReports,
|
|
121
143
|
specificReports,
|
|
122
144
|
},
|
|
145
|
+
examples,
|
|
123
146
|
};
|
|
124
147
|
}
|
|
@@ -26,6 +26,11 @@ export function computeDecomposition(sessions) {
|
|
|
26
26
|
const followupPatterns = /^(now |next |then |also |and |ok |okay |great |good |perfect |after that|building on|following up|continuing)/i;
|
|
27
27
|
const refinementPatterns = /^(actually |wait |hmm |instead |change |modify |update |tweak |adjust |fix |but )/i;
|
|
28
28
|
|
|
29
|
+
// Capture representative prompt examples
|
|
30
|
+
// Keep top 3 candidates and pick the 2nd-longest to avoid overlap with other metrics
|
|
31
|
+
const decompCandidates = [];
|
|
32
|
+
let bestFollowupPrompt = null;
|
|
33
|
+
|
|
29
34
|
for (const session of sessions) {
|
|
30
35
|
const { exchanges } = session;
|
|
31
36
|
totalExchanges += exchanges.length;
|
|
@@ -45,10 +50,19 @@ export function computeDecomposition(sessions) {
|
|
|
45
50
|
if (len > 500) longPromptCount++;
|
|
46
51
|
if (len < 100) shortPromptCount++;
|
|
47
52
|
|
|
53
|
+
// Track decomposition examples (multi-sentence prompts showing task breakdown)
|
|
54
|
+
if (len > 150 && len < 2000) {
|
|
55
|
+
decompCandidates.push(prompt);
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
// Check for contextual followups (not the first prompt in a session)
|
|
49
59
|
if (i > 0) {
|
|
50
60
|
if (followupPatterns.test(prompt) || refinementPatterns.test(prompt)) {
|
|
51
61
|
contextualFollowups++;
|
|
62
|
+
// Capture best followup example
|
|
63
|
+
if (!bestFollowupPrompt || prompt.length > bestFollowupPrompt.length) {
|
|
64
|
+
bestFollowupPrompt = prompt;
|
|
65
|
+
}
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
68
|
}
|
|
@@ -83,6 +97,16 @@ export function computeDecomposition(sessions) {
|
|
|
83
97
|
depthScore * 0.2
|
|
84
98
|
);
|
|
85
99
|
|
|
100
|
+
// Build examples array — pick a mid-length prompt to avoid overlap with other metrics
|
|
101
|
+
const examples = [];
|
|
102
|
+
if (decompCandidates.length > 0) {
|
|
103
|
+
decompCandidates.sort((a, b) => b.length - a.length);
|
|
104
|
+
// Pick ~median length to avoid the longest (which will also match ai-leverage)
|
|
105
|
+
const pickIdx = Math.min(Math.floor(decompCandidates.length / 3), decompCandidates.length - 1);
|
|
106
|
+
examples.push({ type: 'decomposition', prompt: decompCandidates[pickIdx] });
|
|
107
|
+
}
|
|
108
|
+
if (bestFollowupPrompt) examples.push({ type: 'followup', prompt: bestFollowupPrompt });
|
|
109
|
+
|
|
86
110
|
return {
|
|
87
111
|
score: Math.max(0, Math.min(100, score)),
|
|
88
112
|
details: {
|
|
@@ -94,5 +118,6 @@ export function computeDecomposition(sessions) {
|
|
|
94
118
|
longPromptRatio: promptCount > 0 ? Math.round(longPromptCount / promptCount * 100) : 0,
|
|
95
119
|
contextualFollowupRatio: promptCount > 0 ? Math.round(followupRatio * 100) : 0,
|
|
96
120
|
},
|
|
121
|
+
examples,
|
|
97
122
|
};
|
|
98
123
|
}
|
|
@@ -34,6 +34,11 @@ export function computeSessionStructure(sessions) {
|
|
|
34
34
|
// First prompt length distribution (longer first prompts = more context setting)
|
|
35
35
|
let firstPromptTotalLength = 0;
|
|
36
36
|
|
|
37
|
+
// Capture representative examples
|
|
38
|
+
let bestContextPrompt = null; // best context-setting opener
|
|
39
|
+
let bestRefinementPrompt = null; // best refinement/critical feedback
|
|
40
|
+
let bestContextLen = 0;
|
|
41
|
+
|
|
37
42
|
for (const session of sessions) {
|
|
38
43
|
const { exchanges, durationMinutes } = session;
|
|
39
44
|
if (exchanges.length === 0) continue;
|
|
@@ -46,6 +51,11 @@ export function computeSessionStructure(sessions) {
|
|
|
46
51
|
|
|
47
52
|
if (contextSettingPatterns.test(firstPrompt) || firstPrompt.length > 200) {
|
|
48
53
|
contextSetSessions++;
|
|
54
|
+
// Track best context-setting prompt
|
|
55
|
+
if (firstPrompt.length > bestContextLen) {
|
|
56
|
+
bestContextLen = firstPrompt.length;
|
|
57
|
+
bestContextPrompt = firstPrompt;
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
if (planningStartPatterns.test(firstPrompt)) {
|
|
@@ -66,6 +76,10 @@ export function computeSessionStructure(sessions) {
|
|
|
66
76
|
const prompt = exchanges[i].userPrompt || '';
|
|
67
77
|
if (refinementPatterns.test(prompt)) {
|
|
68
78
|
refinementCount++;
|
|
79
|
+
// Track best refinement example
|
|
80
|
+
if (!bestRefinementPrompt || prompt.length > bestRefinementPrompt.length) {
|
|
81
|
+
bestRefinementPrompt = prompt;
|
|
82
|
+
}
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
85
|
|
|
@@ -115,6 +129,11 @@ export function computeSessionStructure(sessions) {
|
|
|
115
129
|
firstPromptScore * 0.15
|
|
116
130
|
);
|
|
117
131
|
|
|
132
|
+
// Build examples array
|
|
133
|
+
const examples = [];
|
|
134
|
+
if (bestContextPrompt) examples.push({ type: 'context_setting', prompt: bestContextPrompt });
|
|
135
|
+
if (bestRefinementPrompt) examples.push({ type: 'refinement', prompt: bestRefinementPrompt });
|
|
136
|
+
|
|
118
137
|
return {
|
|
119
138
|
score: Math.max(0, Math.min(100, score)),
|
|
120
139
|
details: {
|
|
@@ -130,5 +149,6 @@ export function computeSessionStructure(sessions) {
|
|
|
130
149
|
focused: focusedSessions,
|
|
131
150
|
},
|
|
132
151
|
},
|
|
152
|
+
examples,
|
|
133
153
|
};
|
|
134
154
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse OpenAI Codex CLI session files into normalized format.
|
|
6
|
+
*
|
|
7
|
+
* Codex stores sessions under:
|
|
8
|
+
* ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
|
|
9
|
+
*
|
|
10
|
+
* Each JSONL line follows the OpenAI message format:
|
|
11
|
+
* { "role": "user"|"assistant"|"system", "content": "..." | [...], "timestamp": "..." }
|
|
12
|
+
*
|
|
13
|
+
* Tool calls appear as content blocks with type "function_call" or "tool_use".
|
|
14
|
+
* We normalize into the same session format as Claude Code.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function parseJsonlLine(line) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractText(content) {
|
|
26
|
+
if (typeof content === 'string') return content;
|
|
27
|
+
if (!Array.isArray(content)) return '';
|
|
28
|
+
return content
|
|
29
|
+
.filter(block => block.type === 'text' || block.type === 'output_text')
|
|
30
|
+
.map(block => block.text || block.content || '')
|
|
31
|
+
.join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractToolCalls(content) {
|
|
35
|
+
if (!Array.isArray(content)) return [];
|
|
36
|
+
return content
|
|
37
|
+
.filter(block =>
|
|
38
|
+
block.type === 'function_call' ||
|
|
39
|
+
block.type === 'tool_use' ||
|
|
40
|
+
block.type === 'tool_call'
|
|
41
|
+
)
|
|
42
|
+
.map(block => ({
|
|
43
|
+
tool: block.name || block.function?.name || 'unknown',
|
|
44
|
+
input: block.arguments || block.input || {},
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a single Codex rollout JSONL file.
|
|
50
|
+
*/
|
|
51
|
+
function parseSessionFile(filePath) {
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
60
|
+
const exchanges = [];
|
|
61
|
+
let current = null;
|
|
62
|
+
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const entry = parseJsonlLine(line);
|
|
65
|
+
if (!entry) continue;
|
|
66
|
+
|
|
67
|
+
const role = entry.role;
|
|
68
|
+
if (!role) continue;
|
|
69
|
+
|
|
70
|
+
// Skip system messages
|
|
71
|
+
if (role === 'system') continue;
|
|
72
|
+
|
|
73
|
+
// Skip tool result messages
|
|
74
|
+
if (role === 'tool' || role === 'function') continue;
|
|
75
|
+
|
|
76
|
+
const text = extractText(entry.content);
|
|
77
|
+
const tools = role === 'assistant' ? extractToolCalls(entry.content) : [];
|
|
78
|
+
const timestamp = entry.timestamp || entry.created_at || null;
|
|
79
|
+
|
|
80
|
+
if (role === 'user') {
|
|
81
|
+
if (!text.trim()) continue;
|
|
82
|
+
if (current) exchanges.push(current);
|
|
83
|
+
current = {
|
|
84
|
+
userPrompt: text,
|
|
85
|
+
userTimestamp: timestamp,
|
|
86
|
+
assistantResponses: [],
|
|
87
|
+
toolCalls: [],
|
|
88
|
+
thinkingContent: [],
|
|
89
|
+
};
|
|
90
|
+
} else if (role === 'assistant' && current) {
|
|
91
|
+
if (text) current.assistantResponses.push(text);
|
|
92
|
+
current.toolCalls.push(...tools);
|
|
93
|
+
|
|
94
|
+
// Codex may include reasoning/thinking
|
|
95
|
+
if (entry.reasoning || entry.thinking) {
|
|
96
|
+
current.thinkingContent.push(entry.reasoning || entry.thinking);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (current) exchanges.push(current);
|
|
102
|
+
return exchanges;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Recursively find all rollout-*.jsonl files under sessions dir.
|
|
107
|
+
*/
|
|
108
|
+
function findSessionFiles(dir) {
|
|
109
|
+
const files = [];
|
|
110
|
+
if (!existsSync(dir)) return files;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const entries = readdirSync(dir);
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const fullPath = join(dir, entry);
|
|
116
|
+
try {
|
|
117
|
+
const stat = statSync(fullPath);
|
|
118
|
+
if (stat.isDirectory()) {
|
|
119
|
+
files.push(...findSessionFiles(fullPath));
|
|
120
|
+
} else if (entry.endsWith('.jsonl')) {
|
|
121
|
+
files.push(fullPath);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore permission errors
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return files;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract project name from session file path or content.
|
|
136
|
+
* Codex sessions are in ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
|
|
137
|
+
* Try to get the working directory from the session content.
|
|
138
|
+
*/
|
|
139
|
+
function extractProjectName(filePath, exchanges) {
|
|
140
|
+
// Try to extract from the file path date structure
|
|
141
|
+
const parts = filePath.split('/');
|
|
142
|
+
const dateIdx = parts.findIndex(p => p === 'sessions');
|
|
143
|
+
if (dateIdx >= 0 && parts.length > dateIdx + 3) {
|
|
144
|
+
return `codex/${parts[dateIdx + 1]}/${parts[dateIdx + 2]}/${parts[dateIdx + 3]}`;
|
|
145
|
+
}
|
|
146
|
+
return 'codex';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse all Codex sessions from ~/.codex/sessions.
|
|
151
|
+
*/
|
|
152
|
+
export function parseAllSessions(basePath) {
|
|
153
|
+
const sessionsDir = join(basePath, 'sessions');
|
|
154
|
+
if (!existsSync(sessionsDir)) return [];
|
|
155
|
+
|
|
156
|
+
const sessionFiles = findSessionFiles(sessionsDir);
|
|
157
|
+
const allSessions = [];
|
|
158
|
+
|
|
159
|
+
for (const filePath of sessionFiles) {
|
|
160
|
+
const exchanges = parseSessionFile(filePath);
|
|
161
|
+
if (exchanges.length === 0) continue;
|
|
162
|
+
|
|
163
|
+
const timestamps = exchanges
|
|
164
|
+
.map(e => e.userTimestamp)
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.map(t => typeof t === 'number' ? t * 1000 : new Date(t).getTime())
|
|
167
|
+
.filter(t => !isNaN(t))
|
|
168
|
+
.sort();
|
|
169
|
+
|
|
170
|
+
const fileName = filePath.split('/').pop();
|
|
171
|
+
|
|
172
|
+
allSessions.push({
|
|
173
|
+
id: fileName.replace('.jsonl', ''),
|
|
174
|
+
file: fileName,
|
|
175
|
+
project: extractProjectName(filePath, exchanges),
|
|
176
|
+
exchanges,
|
|
177
|
+
turnCount: exchanges.length * 2,
|
|
178
|
+
exchangeCount: exchanges.length,
|
|
179
|
+
startTime: timestamps[0] ? new Date(timestamps[0]).toISOString() : null,
|
|
180
|
+
endTime: timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]).toISOString() : null,
|
|
181
|
+
durationMinutes: timestamps.length >= 2
|
|
182
|
+
? Math.round((timestamps[timestamps.length - 1] - timestamps[0]) / 60000)
|
|
183
|
+
: 0,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return allSessions;
|
|
188
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse Cursor chat history from SQLite state.vscdb files.
|
|
7
|
+
*
|
|
8
|
+
* Cursor stores chats in SQLite databases under:
|
|
9
|
+
* ~/Library/Application Support/Cursor/User/workspaceStorage/<hash>/state.vscdb
|
|
10
|
+
*
|
|
11
|
+
* Two storage formats exist:
|
|
12
|
+
* - Legacy: key = 'workbench.panel.aichat.view.aichat.chatdata' → JSON with tabs/bubbles
|
|
13
|
+
* - Current: key like 'composerData:%' in table cursorDiskKV → JSON with conversation data
|
|
14
|
+
* Bubble text stored separately under 'bubbleId:<composerId>:<bubbleId>'
|
|
15
|
+
*
|
|
16
|
+
* We normalize into the same session format as Claude Code.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function querySqlite(dbPath, query) {
|
|
20
|
+
try {
|
|
21
|
+
const result = execSync(
|
|
22
|
+
`sqlite3 -json "${dbPath}" "${query.replace(/"/g, '\\"')}"`,
|
|
23
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
24
|
+
);
|
|
25
|
+
return JSON.parse(result || '[]');
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function querySqliteRaw(dbPath, query) {
|
|
32
|
+
try {
|
|
33
|
+
return execSync(
|
|
34
|
+
`sqlite3 "${dbPath}" "${query.replace(/"/g, '\\"')}"`,
|
|
35
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
36
|
+
).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Try to detect which table the DB uses (ItemTable vs cursorDiskKV).
|
|
44
|
+
*/
|
|
45
|
+
function detectTable(dbPath) {
|
|
46
|
+
const tables = querySqliteRaw(dbPath, ".tables");
|
|
47
|
+
if (tables.includes('cursorDiskKV')) return 'cursorDiskKV';
|
|
48
|
+
if (tables.includes('ItemTable')) return 'ItemTable';
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse legacy aichat format (tabs with bubbles inline).
|
|
54
|
+
*/
|
|
55
|
+
function parseLegacyChat(dbPath, table) {
|
|
56
|
+
const raw = querySqliteRaw(
|
|
57
|
+
dbPath,
|
|
58
|
+
`SELECT value FROM ${table} WHERE key = 'workbench.panel.aichat.view.aichat.chatdata';`
|
|
59
|
+
);
|
|
60
|
+
if (!raw) return [];
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(raw);
|
|
64
|
+
if (!data.tabs || !Array.isArray(data.tabs)) return [];
|
|
65
|
+
|
|
66
|
+
const sessions = [];
|
|
67
|
+
for (const tab of data.tabs) {
|
|
68
|
+
if (!tab.bubbles || !Array.isArray(tab.bubbles)) continue;
|
|
69
|
+
|
|
70
|
+
const exchanges = [];
|
|
71
|
+
let current = null;
|
|
72
|
+
|
|
73
|
+
for (const bubble of tab.bubbles) {
|
|
74
|
+
const text = bubble.text || '';
|
|
75
|
+
if (!text.trim()) continue;
|
|
76
|
+
|
|
77
|
+
// type 1 = user, type 2 = assistant
|
|
78
|
+
if (bubble.type === 1) {
|
|
79
|
+
if (current) exchanges.push(current);
|
|
80
|
+
current = {
|
|
81
|
+
userPrompt: text,
|
|
82
|
+
userTimestamp: bubble.createdAt || null,
|
|
83
|
+
assistantResponses: [],
|
|
84
|
+
toolCalls: [],
|
|
85
|
+
thinkingContent: [],
|
|
86
|
+
};
|
|
87
|
+
} else if (bubble.type === 2 && current) {
|
|
88
|
+
current.assistantResponses.push(text);
|
|
89
|
+
// Extract tool info if available
|
|
90
|
+
if (bubble.toolFormerData && Array.isArray(bubble.toolFormerData)) {
|
|
91
|
+
for (const tool of bubble.toolFormerData) {
|
|
92
|
+
current.toolCalls.push({ tool: tool.toolName || 'unknown', input: {} });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (current) exchanges.push(current);
|
|
98
|
+
if (exchanges.length === 0) continue;
|
|
99
|
+
|
|
100
|
+
const timestamps = exchanges
|
|
101
|
+
.map(e => e.userTimestamp)
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map(t => new Date(t).getTime())
|
|
104
|
+
.sort();
|
|
105
|
+
|
|
106
|
+
sessions.push({
|
|
107
|
+
id: tab.tabId || `cursor-${sessions.length}`,
|
|
108
|
+
file: 'state.vscdb',
|
|
109
|
+
exchanges,
|
|
110
|
+
turnCount: exchanges.length * 2,
|
|
111
|
+
exchangeCount: exchanges.length,
|
|
112
|
+
startTime: timestamps[0] ? new Date(timestamps[0]).toISOString() : null,
|
|
113
|
+
endTime: timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]).toISOString() : null,
|
|
114
|
+
durationMinutes: timestamps.length >= 2
|
|
115
|
+
? Math.round((timestamps[timestamps.length - 1] - timestamps[0]) / 60000)
|
|
116
|
+
: 0,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return sessions;
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse composer format (cursorDiskKV with separate bubble keys).
|
|
127
|
+
*/
|
|
128
|
+
function parseComposerChat(dbPath) {
|
|
129
|
+
// Get all composer conversation metadata
|
|
130
|
+
const composerRows = querySqlite(
|
|
131
|
+
dbPath,
|
|
132
|
+
`SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%';`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (!composerRows.length) return [];
|
|
136
|
+
|
|
137
|
+
const sessions = [];
|
|
138
|
+
|
|
139
|
+
for (const row of composerRows) {
|
|
140
|
+
try {
|
|
141
|
+
const composerData = JSON.parse(row.value || '{}');
|
|
142
|
+
const composerId = composerData.composerId || row.key.replace('composerData:', '');
|
|
143
|
+
const headers = composerData.fullConversationHeadersOnly || [];
|
|
144
|
+
|
|
145
|
+
if (headers.length === 0) continue;
|
|
146
|
+
|
|
147
|
+
const exchanges = [];
|
|
148
|
+
let current = null;
|
|
149
|
+
|
|
150
|
+
for (const header of headers) {
|
|
151
|
+
const bubbleKey = `bubbleId:${composerId}:${header.bubbleId}`;
|
|
152
|
+
const bubbleRaw = querySqliteRaw(
|
|
153
|
+
dbPath,
|
|
154
|
+
`SELECT value FROM cursorDiskKV WHERE key = '${bubbleKey}';`
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!bubbleRaw) continue;
|
|
158
|
+
|
|
159
|
+
let bubbleData;
|
|
160
|
+
try {
|
|
161
|
+
bubbleData = JSON.parse(bubbleRaw);
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const text = bubbleData.text || '';
|
|
167
|
+
if (!text.trim()) continue;
|
|
168
|
+
|
|
169
|
+
// type 1 = user, type 2 = assistant
|
|
170
|
+
if (header.type === 1) {
|
|
171
|
+
if (current) exchanges.push(current);
|
|
172
|
+
current = {
|
|
173
|
+
userPrompt: text,
|
|
174
|
+
userTimestamp: bubbleData.createdAt || bubbleData.timingInfo?.startedAt || null,
|
|
175
|
+
assistantResponses: [],
|
|
176
|
+
toolCalls: [],
|
|
177
|
+
thinkingContent: [],
|
|
178
|
+
};
|
|
179
|
+
} else if (header.type === 2 && current) {
|
|
180
|
+
current.assistantResponses.push(text);
|
|
181
|
+
if (bubbleData.toolFormerData && Array.isArray(bubbleData.toolFormerData)) {
|
|
182
|
+
for (const tool of bubbleData.toolFormerData) {
|
|
183
|
+
current.toolCalls.push({ tool: tool.toolName || String(tool.tool || 'unknown'), input: {} });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (current) exchanges.push(current);
|
|
189
|
+
if (exchanges.length === 0) continue;
|
|
190
|
+
|
|
191
|
+
const timestamps = exchanges
|
|
192
|
+
.map(e => e.userTimestamp)
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.map(t => typeof t === 'number' ? t : new Date(t).getTime())
|
|
195
|
+
.sort();
|
|
196
|
+
|
|
197
|
+
sessions.push({
|
|
198
|
+
id: composerId,
|
|
199
|
+
file: 'state.vscdb',
|
|
200
|
+
exchanges,
|
|
201
|
+
turnCount: exchanges.length * 2,
|
|
202
|
+
exchangeCount: exchanges.length,
|
|
203
|
+
startTime: timestamps[0] ? new Date(timestamps[0]).toISOString() : null,
|
|
204
|
+
endTime: timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]).toISOString() : null,
|
|
205
|
+
durationMinutes: timestamps.length >= 2
|
|
206
|
+
? Math.round((timestamps[timestamps.length - 1] - timestamps[0]) / 60000)
|
|
207
|
+
: 0,
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return sessions;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get workspace name from workspace.json.
|
|
219
|
+
*/
|
|
220
|
+
function getWorkspaceName(workspaceDir) {
|
|
221
|
+
const wsFile = join(workspaceDir, 'workspace.json');
|
|
222
|
+
if (!existsSync(wsFile)) return workspaceDir.split('/').pop();
|
|
223
|
+
try {
|
|
224
|
+
const data = JSON.parse(execSync(`cat "${wsFile}"`, { encoding: 'utf-8' }));
|
|
225
|
+
// workspace.json has a "folder" field with the project path
|
|
226
|
+
const folder = data.folder || data.workspace || '';
|
|
227
|
+
// Extract meaningful project name from path
|
|
228
|
+
const decoded = decodeURIComponent(folder.replace('file://', ''));
|
|
229
|
+
const parts = decoded.split('/').filter(Boolean);
|
|
230
|
+
return parts.slice(-2).join('/') || workspaceDir.split('/').pop();
|
|
231
|
+
} catch {
|
|
232
|
+
return workspaceDir.split('/').pop();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse all Cursor workspaces from workspaceStorage.
|
|
238
|
+
*/
|
|
239
|
+
export function parseAllWorkspaces(basePath) {
|
|
240
|
+
if (!existsSync(basePath)) return [];
|
|
241
|
+
|
|
242
|
+
const allSessions = [];
|
|
243
|
+
let dirs;
|
|
244
|
+
try {
|
|
245
|
+
dirs = readdirSync(basePath).filter(f => {
|
|
246
|
+
try { return statSync(join(basePath, f)).isDirectory(); } catch { return false; }
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const dir of dirs) {
|
|
253
|
+
const workspaceDir = join(basePath, dir);
|
|
254
|
+
const dbPath = join(workspaceDir, 'state.vscdb');
|
|
255
|
+
if (!existsSync(dbPath)) continue;
|
|
256
|
+
|
|
257
|
+
const projectName = getWorkspaceName(workspaceDir);
|
|
258
|
+
const table = detectTable(dbPath);
|
|
259
|
+
if (!table) continue;
|
|
260
|
+
|
|
261
|
+
let sessions = [];
|
|
262
|
+
|
|
263
|
+
// Try composer format first (newer)
|
|
264
|
+
if (table === 'cursorDiskKV') {
|
|
265
|
+
sessions = parseComposerChat(dbPath);
|
|
266
|
+
// Also try legacy key in same table
|
|
267
|
+
if (sessions.length === 0) {
|
|
268
|
+
sessions = parseLegacyChat(dbPath, table);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
sessions = parseLegacyChat(dbPath, table);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const session of sessions) {
|
|
275
|
+
session.project = projectName;
|
|
276
|
+
allSessions.push(session);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return allSessions;
|
|
281
|
+
}
|
package/src/upload.js
CHANGED
|
@@ -5,24 +5,38 @@ const API_BASE = 'https://chekk-production.up.railway.app/api/v1';
|
|
|
5
5
|
/**
|
|
6
6
|
* Call the Chekk API to generate personalized prose from metrics.
|
|
7
7
|
*/
|
|
8
|
+
function truncateExamples(examples, maxLen = 120) {
|
|
9
|
+
if (!examples || !examples.length) return [];
|
|
10
|
+
return examples.map(e => ({
|
|
11
|
+
type: e.type,
|
|
12
|
+
prompt: e.prompt && e.prompt.length > maxLen
|
|
13
|
+
? e.prompt.replace(/\s+/g, ' ').trim().slice(0, maxLen) + '...'
|
|
14
|
+
: (e.prompt || '').replace(/\s+/g, ' ').trim(),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export async function generateProse(metrics, result, sessionStats) {
|
|
9
19
|
const payload = {
|
|
10
20
|
metrics: {
|
|
11
21
|
decomposition: {
|
|
12
22
|
score: metrics.decomposition.score,
|
|
13
23
|
details: metrics.decomposition.details,
|
|
24
|
+
examples: truncateExamples(metrics.decomposition.examples),
|
|
14
25
|
},
|
|
15
26
|
debugCycles: {
|
|
16
27
|
score: metrics.debugCycles.score,
|
|
17
28
|
details: metrics.debugCycles.details,
|
|
29
|
+
examples: truncateExamples(metrics.debugCycles.examples),
|
|
18
30
|
},
|
|
19
31
|
aiLeverage: {
|
|
20
32
|
score: metrics.aiLeverage.score,
|
|
21
33
|
details: metrics.aiLeverage.details,
|
|
34
|
+
examples: truncateExamples(metrics.aiLeverage.examples),
|
|
22
35
|
},
|
|
23
36
|
sessionStructure: {
|
|
24
37
|
score: metrics.sessionStructure.score,
|
|
25
38
|
details: metrics.sessionStructure.details,
|
|
39
|
+
examples: truncateExamples(metrics.sessionStructure.examples),
|
|
26
40
|
},
|
|
27
41
|
},
|
|
28
42
|
result: {
|