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 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.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chekk",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "See how you prompt. Chekk analyzes your AI coding workflow and tells you what kind of engineer you are.",
5
5
  "bin": {
6
6
  "chekk": "./bin/chekk.js"
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); // rough estimate
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: ~/Library/Application Support/Cursor/User/workspaceStorage/
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
- results.push({
67
- tool: 'Cursor',
68
- sessions: 0,
69
- projects: [],
70
- estimatedPrompts: 0,
71
- basePath: cursorPath,
72
- status: 'detected_not_supported',
73
- message: 'Cursor support coming in V2',
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
- if (existsSync(codexPath)) {
82
- results.push({
83
- tool: 'Codex',
84
- sessions: 0,
85
- projects: [],
86
- estimatedPrompts: 0,
87
- basePath: codexPath,
88
- status: 'detected_not_supported',
89
- message: 'Codex support coming in V2',
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.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
- if (tool.status === 'detected_not_supported') {
88
- console.log(` ${dim('\u25CB')} ${dim(tool.tool)}${dim(' detected \u2014 V2')}`);
89
- } else {
90
- const sessions = numberFormat(tool.sessions);
91
- const projects = tool.projects.length;
92
- const exchanges = numberFormat(tool.estimatedPrompts);
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
- displayDataNarratives(metrics);
238
+ // Fallback: data-driven bullet points with inline snippets
239
+ displayDataNarrativesWithTracker(metrics, shownSnippets);
202
240
  }
203
241
  }
204
242
 
205
- function displayDataNarratives(metrics) {
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
- displayDataNarratives(metrics);
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 currently supports Claude Code.'));
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 supported) {
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: supported.map(t => t.tool),
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)) architecturalPrompts++;
47
- if (planningPatterns.test(prompt)) planningPrompts++;
48
- if (exploratoryPatterns.test(prompt)) exploratoryPrompts++;
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) quickFixes++;
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: {