qodewk 0.2.3 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +7 -9
  2. package/dist/index.cjs +385 -96
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -78,15 +78,13 @@ npx qodewk audit --base origin/main --head HEAD --format markdown --out receipt.
78
78
 
79
79
  Unlike single-vendor utilities (like Chit, which only support Claude Code CLI), **Qodewk operates globally across all major AI coding platforms**. It reads your local agent transcripts, matches them with your repository's Git revision graph, and proves what was actually shipped.
80
80
 
81
- ### Supported Platforms & Harvesters
82
-
83
- | Platform | Local Data Source / Footprint | Captured Telemetry |
84
- |---|---|---|
85
- | **Google Antigravity** | `~/.gemini/antigravity/conversation_summaries.db` & `brain/<id>/.../transcript.jsonl` | Shipped task titles, exact models (`Claude Opus 4.6 Thinking`, `Gemini 3.8 Flash`, etc.), tool calls (`replace_file_content`, `write_to_file`), and multi-turn tokens |
86
- | **Claude Code CLI** | `~/.claude/projects/<slug>/sessions/*.jsonl` | User prompts, exact input/output/cached token usage, and `Edit`/`Write` file mutations |
87
- | **Cursor IDE** | `%APPDATA%\Cursor\User\workspaceStorage\*\state.vscdb` (Win) / `Library/Application Support/Cursor/...` (Mac) | Workspace session bubbles, model types (`claude-3-5-sonnet`, `gpt-4o`), and generation timestamps |
88
- | **Aider** | `.aider.chat.history.md` (Repository root) | Session prompts, model IDs, and code file diffs |
89
- | **GitHub Copilot** | Git commit revision graph trailers (`Co-authored-by: Copilot`) | Commit-level author attribution |
81
+ | Platform | Local Data Source / Footprint | Captured Telemetry | Status |
82
+ |---|---|---|---|
83
+ | **Google Antigravity** | `~/.gemini/antigravity/conversation_summaries.db` & `brain/<id>/.../transcript.jsonl` | Shipped task titles, exact models (`Claude Opus 4.6 Thinking`, `Gemini 3.8 Flash`, etc.), tool calls (`replace_file_content`, `write_to_file`), and multi-turn tokens | **Tier A (Verified)** |
84
+ | **Claude Code CLI** | `~/.claude/projects/<slug>/sessions/*.jsonl` | User prompts, exact input/output/cached token usage, and `Edit`/`Write` file mutations | **Tier A (Verified)** |
85
+ | **Cursor IDE** | `~/.cursor/projects/<slug>/agent-transcripts/*.jsonl` & `workspaceStorage/*/state.vscdb` | Commit hash binding (`aiCodeTracking.recentCommit`), agent transcripts, tool edits, and token expenditures | **Tier A (Verified / Imported)** |
86
+ | **GitHub Copilot** | Git commit revision trailers (`Co-authored-by: Copilot`) | Commit-level author attribution | **Heuristic (Observed)** |
87
+ | **Aider** | `.aider.chat.history.md` (Repository root) | Session prompts, model IDs, and code file diffs | *Tier B (Experimental Roadmap)* |
90
88
 
91
89
  ### Key Capabilities
92
90
 
package/dist/index.cjs CHANGED
@@ -13506,6 +13506,7 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
13506
13506
  let headSha = options.headSha || "0000000000000000000000000000000000000000";
13507
13507
  let baseSha = options.baseSha;
13508
13508
  let commitMessage = void 0;
13509
+ let commitDate = void 0;
13509
13510
  let commitsCount = 1;
13510
13511
  const sinceDate = parseSinceOption(options.since);
13511
13512
  if (sinceDate && !options.baseSha) {
@@ -13537,6 +13538,7 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
13537
13538
  headSha = log.latest.hash;
13538
13539
  }
13539
13540
  commitMessage = log.latest.message;
13541
+ commitDate = log.latest.date;
13540
13542
  if (!options.baseSha && log.all.length > 1 && log.all[1]) {
13541
13543
  baseSha = log.all[1].hash;
13542
13544
  }
@@ -13556,22 +13558,34 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
13556
13558
  const projectAlias = path.basename(path.resolve(repoPath)).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
13557
13559
  const repoHash = computeSaltedHash(repoIdentifier);
13558
13560
  let diffSummary;
13559
- if (baseSha && headSha && baseSha !== headSha) {
13560
- const status = await git.status();
13561
- if (status.files.length > 0) {
13562
- diffSummary = await git.diffSummary([baseSha]);
13561
+ try {
13562
+ if (baseSha && headSha && baseSha !== headSha) {
13563
+ const status = await git.status();
13564
+ if (status.files.length > 0) {
13565
+ diffSummary = await git.diffSummary([baseSha]);
13566
+ } else {
13567
+ diffSummary = await git.diffSummary([`${baseSha}...${headSha}`]).catch(async () => {
13568
+ return await git.diffSummary([`${baseSha}..${headSha}`]);
13569
+ });
13570
+ }
13563
13571
  } else {
13564
- diffSummary = await git.diffSummary([`${baseSha}..${headSha}`]);
13572
+ const status = await git.status();
13573
+ if (status.files.length > 0) {
13574
+ diffSummary = await git.diffSummary(["HEAD"]);
13575
+ } else if (baseSha) {
13576
+ diffSummary = await git.diffSummary([`${baseSha}..${headSha}`]);
13577
+ } else if (headSha !== "0000000000000000000000000000000000000000") {
13578
+ diffSummary = await git.diffSummary([`${headSha}~1..${headSha}`]).catch(async () => {
13579
+ return await git.diffSummary([headSha]);
13580
+ });
13581
+ } else {
13582
+ diffSummary = { changed: 0, insertions: 0, deletions: 0, files: [] };
13583
+ }
13565
13584
  }
13566
- } else {
13567
- const status = await git.status();
13568
- if (status.files.length > 0) {
13585
+ } catch {
13586
+ try {
13569
13587
  diffSummary = await git.diffSummary(["HEAD"]);
13570
- } else if (baseSha) {
13571
- diffSummary = await git.diffSummary([`${baseSha}..${headSha}`]);
13572
- } else if (headSha !== "0000000000000000000000000000000000000000") {
13573
- diffSummary = await git.diffSummary([headSha]);
13574
- } else {
13588
+ } catch {
13575
13589
  diffSummary = { changed: 0, insertions: 0, deletions: 0, files: [] };
13576
13590
  }
13577
13591
  }
@@ -13621,6 +13635,7 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
13621
13635
  repoHash,
13622
13636
  projectAlias,
13623
13637
  commitMessage,
13638
+ commitDate,
13624
13639
  commitsCount
13625
13640
  };
13626
13641
  }
@@ -13916,6 +13931,120 @@ function computeCost(inputTokens, outputTokens, cachedTokens, card) {
13916
13931
  return Number((freshCost + cacheCost + outCost).toFixed(4));
13917
13932
  }
13918
13933
 
13934
+ // ../core/dist/harvester/scoring.js
13935
+ function normalizeFilePath(filePath, repoPath) {
13936
+ let p2 = filePath.replace(/\\/g, "/").trim();
13937
+ p2 = p2.replace(/^file:\/\/\/?/, "");
13938
+ if (repoPath) {
13939
+ const normRepo = repoPath.replace(/\\/g, "/").replace(/\/$/, "");
13940
+ if (p2.toLowerCase().startsWith(normRepo.toLowerCase())) {
13941
+ p2 = p2.slice(normRepo.length);
13942
+ }
13943
+ }
13944
+ p2 = p2.replace(/^\.?\//, "");
13945
+ return p2.toLowerCase();
13946
+ }
13947
+ function scoreFootprint(fp, gitCtx) {
13948
+ if (!gitCtx) {
13949
+ return 0.5;
13950
+ }
13951
+ let score = 0.1;
13952
+ if (fp.boundCommitSha && gitCtx.headSha) {
13953
+ const bound = fp.boundCommitSha.toLowerCase().trim();
13954
+ const head = gitCtx.headSha.toLowerCase().trim();
13955
+ if (bound === head || bound.startsWith(head.slice(0, 7)) || head.startsWith(bound.slice(0, 7))) {
13956
+ score += 1;
13957
+ } else {
13958
+ score -= 0.5;
13959
+ }
13960
+ }
13961
+ if (gitCtx.changedFiles && gitCtx.changedFiles.length > 0) {
13962
+ const normChanged = gitCtx.changedFiles.map((f) => normalizeFilePath(f, fp.repoPath));
13963
+ const normEdited = (fp.filesEdited || []).map((f) => normalizeFilePath(f, fp.repoPath));
13964
+ if (normEdited.length > 0) {
13965
+ let matches = 0;
13966
+ for (const cf of normChanged) {
13967
+ const found = normEdited.some((ef) => ef === cf || cf.endsWith("/" + ef) || ef.endsWith("/" + cf) || cf.endsWith(ef) || ef.endsWith(cf));
13968
+ if (found)
13969
+ matches++;
13970
+ }
13971
+ const ratio = matches / normChanged.length;
13972
+ if (matches > 0) {
13973
+ score += ratio * 0.4;
13974
+ } else {
13975
+ score -= 0.15;
13976
+ }
13977
+ }
13978
+ }
13979
+ if (gitCtx.commitDate && fp.timestamp) {
13980
+ try {
13981
+ const commitTime = new Date(gitCtx.commitDate).getTime();
13982
+ const fpTime = new Date(fp.timestamp).getTime();
13983
+ if (!isNaN(commitTime) && !isNaN(fpTime)) {
13984
+ const diffMinutes = Math.abs(commitTime - fpTime) / (60 * 1e3);
13985
+ if (diffMinutes <= 15) {
13986
+ score += 0.25;
13987
+ } else if (diffMinutes <= 60) {
13988
+ score += 0.2;
13989
+ } else if (diffMinutes <= 180) {
13990
+ score += 0.15;
13991
+ } else if (diffMinutes <= 720) {
13992
+ score += 0.08;
13993
+ } else if (diffMinutes <= 1440) {
13994
+ score += 0.03;
13995
+ } else {
13996
+ score -= 0.1;
13997
+ }
13998
+ }
13999
+ } catch {
14000
+ }
14001
+ }
14002
+ switch (fp.mode) {
14003
+ case "verified":
14004
+ score += 0.1;
14005
+ break;
14006
+ case "observed":
14007
+ score += 0.08;
14008
+ break;
14009
+ case "imported":
14010
+ score += 0.05;
14011
+ break;
14012
+ case "estimated":
14013
+ score += 0.02;
14014
+ break;
14015
+ default:
14016
+ break;
14017
+ }
14018
+ return Math.max(0, Math.round(score * 100) / 100);
14019
+ }
14020
+ function selectPrimaryFootprint(footprints, gitCtx) {
14021
+ if (footprints.length === 0) {
14022
+ return { primary: void 0, rankedFootprints: [] };
14023
+ }
14024
+ if (!gitCtx || !gitCtx.headSha && !gitCtx.commitDate && (!gitCtx.changedFiles || gitCtx.changedFiles.length === 0)) {
14025
+ const sorted2 = [...footprints].sort((a, b2) => new Date(b2.timestamp).getTime() - new Date(a.timestamp).getTime());
14026
+ return { primary: sorted2[0], rankedFootprints: sorted2 };
14027
+ }
14028
+ const scored = footprints.map((fp) => {
14029
+ const score = scoreFootprint(fp, gitCtx);
14030
+ return {
14031
+ ...fp,
14032
+ attributionScore: score
14033
+ };
14034
+ });
14035
+ scored.sort((a, b2) => {
14036
+ const scoreDiff = (b2.attributionScore || 0) - (a.attributionScore || 0);
14037
+ if (Math.abs(scoreDiff) > 1e-3) {
14038
+ return scoreDiff;
14039
+ }
14040
+ return new Date(b2.timestamp).getTime() - new Date(a.timestamp).getTime();
14041
+ });
14042
+ return {
14043
+ primary: scored[0],
14044
+ rankedFootprints: scored
14045
+ };
14046
+ }
14047
+
13919
14048
  // ../core/dist/harvester/antigravity.js
13920
14049
  var import_meta = {};
13921
14050
  function getSqliteDatabase() {
@@ -14002,7 +14131,7 @@ function harvestAntigravityFootprints(repoPath, sinceDate) {
14002
14131
  if ((tc.name === "replace_file_content" || tc.name === "write_to_file") && tc.args) {
14003
14132
  const file = tc.args.TargetFile || tc.args.targetFile;
14004
14133
  if (file) {
14005
- filesEdited.add(path3.relative(repoPath, file.replace(/"/g, "")).replace(/\\/g, "/"));
14134
+ filesEdited.add(normalizeFilePath(String(file), repoPath));
14006
14135
  }
14007
14136
  }
14008
14137
  }
@@ -14094,7 +14223,7 @@ function harvestClaudeFootprints(repoPath, projectAlias, sinceDate) {
14094
14223
  }
14095
14224
  if (data.tool_name === "Edit" || data.tool_name === "Write") {
14096
14225
  if (data.tool_input?.file_path) {
14097
- filesEdited.add(path4.relative(repoPath, data.tool_input.file_path).replace(/\\/g, "/"));
14226
+ filesEdited.add(normalizeFilePath(String(data.tool_input.file_path), repoPath));
14098
14227
  }
14099
14228
  }
14100
14229
  } catch {
@@ -14154,88 +14283,236 @@ function getSqliteDatabase2() {
14154
14283
  }
14155
14284
  function harvestCursorFootprints(repoPath, sinceDate) {
14156
14285
  const footprints = [];
14157
- const DatabaseSync2 = getSqliteDatabase2();
14158
- if (!DatabaseSync2)
14159
- return footprints;
14160
- let storageDir = "";
14161
- if (process.platform === "win32") {
14162
- storageDir = path5.join(process.env.APPDATA || path5.join(os4.homedir(), "AppData", "Roaming"), "Cursor", "User", "workspaceStorage");
14163
- } else if (process.platform === "darwin") {
14164
- storageDir = path5.join(os4.homedir(), "Library", "Application Support", "Cursor", "User", "workspaceStorage");
14165
- } else {
14166
- storageDir = path5.join(os4.homedir(), ".config", "Cursor", "User", "workspaceStorage");
14167
- }
14168
- if (!fs4.existsSync(storageDir))
14169
- return footprints;
14286
+ const normTarget = repoPath.toLowerCase().replace(/\\/g, "/").replace(/\/$/, "");
14287
+ const targetBasename = path5.basename(repoPath).toLowerCase();
14170
14288
  try {
14171
- const normTarget = repoPath.toLowerCase().replace(/\\/g, "/");
14172
- const wsDirs = fs4.readdirSync(storageDir);
14173
- for (const ws of wsDirs) {
14174
- const wsPath = path5.join(storageDir, ws);
14175
- const wsJsonPath = path5.join(wsPath, "workspace.json");
14176
- if (!fs4.existsSync(wsJsonPath))
14177
- continue;
14178
- try {
14179
- const wsJson = JSON.parse(fs4.readFileSync(wsJsonPath, "utf-8"));
14180
- const folder = (wsJson.folder || wsJson.workspace || "").toLowerCase().replace(/\\/g, "/");
14181
- if (!folder.includes(normTarget) && !normTarget.includes(folder))
14182
- continue;
14183
- const dbFile = path5.join(wsPath, "state.vscdb");
14184
- if (!fs4.existsSync(dbFile))
14185
- continue;
14186
- const dbStat = fs4.statSync(dbFile);
14187
- if (sinceDate && dbStat.mtime < sinceDate)
14188
- continue;
14189
- const db = new DatabaseSync2(dbFile, { readOnly: true });
14190
- const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'workbench.panel.aichat.chatdata' OR key = 'aiService.generations' LIMIT 1").get();
14191
- if (row && row.value) {
14192
- const parsed = JSON.parse(row.value);
14193
- const tabs = parsed.tabs || (Array.isArray(parsed) ? parsed : []);
14194
- let stepCount = 0;
14195
- let taskTitle = "";
14196
- let model = "claude-3-5-sonnet";
14197
- for (const tab of tabs) {
14198
- const bubbles = tab.bubbles || [];
14199
- stepCount += bubbles.length;
14200
- if (!taskTitle && tab.chatTitle)
14201
- taskTitle = tab.chatTitle;
14202
- for (const b2 of bubbles) {
14203
- if (b2.modelType)
14204
- model = b2.modelType;
14289
+ const cursorProjectsDir = path5.join(os4.homedir(), ".cursor", "projects");
14290
+ if (fs4.existsSync(cursorProjectsDir)) {
14291
+ const projectDirs = fs4.readdirSync(cursorProjectsDir);
14292
+ for (const pDir of projectDirs) {
14293
+ const cleanPDir = pDir.toLowerCase().replace(/[^a-z0-9]/g, "");
14294
+ const cleanTarget = normTarget.toLowerCase().replace(/[^a-z0-9]/g, "");
14295
+ const cleanBase = targetBasename.replace(/[^a-z0-9]/g, "");
14296
+ if (cleanPDir.includes(cleanTarget) || cleanTarget.includes(cleanPDir) || cleanPDir.endsWith(cleanBase)) {
14297
+ const transcriptsDir = path5.join(cursorProjectsDir, pDir, "agent-transcripts");
14298
+ if (!fs4.existsSync(transcriptsDir))
14299
+ continue;
14300
+ const sessionDirs = fs4.readdirSync(transcriptsDir);
14301
+ for (const sDir of sessionDirs) {
14302
+ const sPath = path5.join(transcriptsDir, sDir);
14303
+ if (!fs4.statSync(sPath).isDirectory())
14304
+ continue;
14305
+ const jsonlFile = path5.join(sPath, `${sDir}.jsonl`);
14306
+ if (!fs4.existsSync(jsonlFile))
14307
+ continue;
14308
+ const stat = fs4.statSync(jsonlFile);
14309
+ if (sinceDate && stat.mtime < sinceDate)
14310
+ continue;
14311
+ try {
14312
+ const content = fs4.readFileSync(jsonlFile, "utf-8");
14313
+ const lines = content.split("\n");
14314
+ let stepCount = 0;
14315
+ let taskTitle = "";
14316
+ let model = "claude-3-5-sonnet";
14317
+ const filesEditedSet = /* @__PURE__ */ new Set();
14318
+ for (const line of lines) {
14319
+ if (!line.trim())
14320
+ continue;
14321
+ try {
14322
+ const entry = JSON.parse(line);
14323
+ if (!taskTitle && entry.role === "user" && entry.message?.content) {
14324
+ for (const c3 of entry.message.content) {
14325
+ if (c3.type === "text" && c3.text) {
14326
+ const m = c3.text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
14327
+ if (m && m[1]) {
14328
+ taskTitle = m[1].replace(/\n/g, " ").trim().slice(0, 60);
14329
+ } else if (!c3.text.startsWith("<timestamp>")) {
14330
+ taskTitle = c3.text.replace(/\n/g, " ").trim().slice(0, 60);
14331
+ }
14332
+ }
14333
+ }
14334
+ }
14335
+ if (entry.role === "assistant" && entry.message?.content) {
14336
+ for (const c3 of entry.message.content) {
14337
+ if (c3.type === "tool_use") {
14338
+ stepCount++;
14339
+ const name = (c3.name || "").toLowerCase();
14340
+ const inp = c3.input || {};
14341
+ if (name.includes("replace") || name.includes("edit") || name.includes("write") || name.includes("diff") || name.includes("apply")) {
14342
+ const target = inp.path || inp.file_path || inp.target_file || inp.file;
14343
+ if (target && typeof target === "string") {
14344
+ filesEditedSet.add(normalizeFilePath(target, repoPath));
14345
+ }
14346
+ }
14347
+ }
14348
+ }
14349
+ }
14350
+ } catch {
14351
+ }
14352
+ }
14353
+ if (stepCount > 0) {
14354
+ const rateCard = getRateCard(model);
14355
+ const estInput = stepCount * 3e3;
14356
+ const estCached = Math.round(estInput * 0.6);
14357
+ const estOutput = stepCount * 450;
14358
+ const cost = computeCost(estInput, estOutput, estCached, rateCard);
14359
+ footprints.push({
14360
+ id: `cursor_transcript_${sDir}`,
14361
+ platform: "cursor",
14362
+ sessionId: sDir,
14363
+ repoPath,
14364
+ taskTitle: taskTitle || "Cursor Agent Session",
14365
+ model,
14366
+ stepsCount: stepCount,
14367
+ filesEdited: Array.from(filesEditedSet),
14368
+ timestamp: stat.mtime.toISOString(),
14369
+ tokens: {
14370
+ input: estInput,
14371
+ output: estOutput,
14372
+ cached: estCached
14373
+ },
14374
+ cost,
14375
+ mode: "imported",
14376
+ confidence: 0.85,
14377
+ rawTranscriptPath: jsonlFile
14378
+ });
14379
+ }
14380
+ } catch {
14205
14381
  }
14206
14382
  }
14207
- if (stepCount > 0) {
14208
- const rateCard = getRateCard(model);
14209
- const estInput = stepCount * 2500;
14210
- const estCached = Math.round(estInput * 0.6);
14211
- const estOutput = stepCount * 250;
14212
- const cost = computeCost(estInput, estOutput, estCached, rateCard);
14213
- footprints.push({
14214
- id: `cursor_${ws}`,
14215
- platform: "cursor",
14216
- sessionId: ws,
14217
- repoPath,
14218
- taskTitle: taskTitle || "Cursor AI Session",
14219
- model,
14220
- stepsCount: stepCount,
14221
- filesEdited: [],
14222
- timestamp: dbStat.mtime.toISOString(),
14223
- tokens: {
14224
- input: estInput,
14225
- output: estOutput,
14226
- cached: estCached
14227
- },
14228
- cost,
14229
- mode: "imported",
14230
- confidence: 0.85,
14231
- rawTranscriptPath: dbFile
14232
- });
14383
+ }
14384
+ }
14385
+ }
14386
+ } catch {
14387
+ }
14388
+ const DatabaseSync2 = getSqliteDatabase2();
14389
+ if (DatabaseSync2) {
14390
+ let storageDir = "";
14391
+ if (process.platform === "win32") {
14392
+ storageDir = path5.join(process.env.APPDATA || path5.join(os4.homedir(), "AppData", "Roaming"), "Cursor", "User", "workspaceStorage");
14393
+ } else if (process.platform === "darwin") {
14394
+ storageDir = path5.join(os4.homedir(), "Library", "Application Support", "Cursor", "User", "workspaceStorage");
14395
+ } else {
14396
+ storageDir = path5.join(os4.homedir(), ".config", "Cursor", "User", "workspaceStorage");
14397
+ }
14398
+ if (fs4.existsSync(storageDir)) {
14399
+ try {
14400
+ const wsDirs = fs4.readdirSync(storageDir);
14401
+ for (const ws of wsDirs) {
14402
+ const wsPath = path5.join(storageDir, ws);
14403
+ const wsJsonPath = path5.join(wsPath, "workspace.json");
14404
+ if (!fs4.existsSync(wsJsonPath))
14405
+ continue;
14406
+ try {
14407
+ const wsJson = JSON.parse(fs4.readFileSync(wsJsonPath, "utf-8"));
14408
+ let folderUri = wsJson.folder || wsJson.workspace || "";
14409
+ try {
14410
+ folderUri = decodeURIComponent(folderUri.replace(/^file:\/\/\/?/, ""));
14411
+ } catch {
14412
+ }
14413
+ const folder = folderUri.toLowerCase().replace(/\\/g, "/");
14414
+ if (!folder.includes(normTarget) && !normTarget.includes(folder) && !folder.endsWith(targetBasename)) {
14415
+ continue;
14416
+ }
14417
+ const dbFile = path5.join(wsPath, "state.vscdb");
14418
+ if (!fs4.existsSync(dbFile))
14419
+ continue;
14420
+ const dbStat = fs4.statSync(dbFile);
14421
+ if (sinceDate && dbStat.mtime < sinceDate)
14422
+ continue;
14423
+ const db = new DatabaseSync2(dbFile, { readOnly: true });
14424
+ try {
14425
+ const rowRecent = db.prepare("SELECT value FROM ItemTable WHERE key = 'aiCodeTracking.recentCommit' LIMIT 1").get();
14426
+ if (rowRecent && rowRecent.value) {
14427
+ const recent = JSON.parse(rowRecent.value);
14428
+ if (recent && recent.commitHash) {
14429
+ const added = Number(recent.linesAdded || recent.composerLinesAdded || 0);
14430
+ const deleted = Number(recent.linesDeleted || recent.composerLinesDeleted || 0);
14431
+ const estOut = Math.max(Math.round((added + deleted) * 35 / 4), 100);
14432
+ const estIn = estOut * 20;
14433
+ const estCached = Math.round(estIn * 0.6);
14434
+ const rateCard = getRateCard("claude-3-7-sonnet");
14435
+ const cost = computeCost(estIn, estOut, estCached, rateCard);
14436
+ const commitDate = recent.commitDate ? new Date(recent.commitDate).toISOString() : recent.timestamp ? new Date(recent.timestamp).toISOString() : dbStat.mtime.toISOString();
14437
+ footprints.push({
14438
+ id: `cursor_commit_${recent.commitHash.slice(0, 12)}`,
14439
+ platform: "cursor",
14440
+ sessionId: ws,
14441
+ repoPath,
14442
+ taskTitle: recent.commitMessage || "Cursor AI Commit",
14443
+ model: "claude-3-7-sonnet",
14444
+ stepsCount: Math.max(Math.round((added + deleted) / 10), 1),
14445
+ filesEdited: [],
14446
+ timestamp: commitDate,
14447
+ boundCommitSha: recent.commitHash,
14448
+ tokens: {
14449
+ input: estIn,
14450
+ output: estOut,
14451
+ cached: estCached
14452
+ },
14453
+ cost,
14454
+ mode: "verified",
14455
+ confidence: 0.95,
14456
+ rawTranscriptPath: dbFile
14457
+ });
14458
+ }
14459
+ }
14460
+ } catch {
14461
+ }
14462
+ const rowLegacy = db.prepare("SELECT value FROM ItemTable WHERE key = 'workbench.panel.aichat.chatdata' OR key = 'aiService.generations' LIMIT 1").get();
14463
+ if (rowLegacy && rowLegacy.value) {
14464
+ try {
14465
+ const parsed = JSON.parse(rowLegacy.value);
14466
+ const tabs = parsed.tabs || (Array.isArray(parsed) ? parsed : []);
14467
+ let stepCount = 0;
14468
+ let taskTitle = "";
14469
+ let model = "claude-3-5-sonnet";
14470
+ for (const tab of tabs) {
14471
+ const bubbles = tab.bubbles || [];
14472
+ stepCount += bubbles.length;
14473
+ if (!taskTitle && tab.chatTitle)
14474
+ taskTitle = tab.chatTitle;
14475
+ for (const b2 of bubbles) {
14476
+ if (b2.modelType)
14477
+ model = b2.modelType;
14478
+ }
14479
+ }
14480
+ if (stepCount > 0) {
14481
+ const rateCard = getRateCard(model);
14482
+ const estInput = stepCount * 2500;
14483
+ const estCached = Math.round(estInput * 0.6);
14484
+ const estOutput = stepCount * 250;
14485
+ const cost = computeCost(estInput, estOutput, estCached, rateCard);
14486
+ footprints.push({
14487
+ id: `cursor_chat_${ws}`,
14488
+ platform: "cursor",
14489
+ sessionId: ws,
14490
+ repoPath,
14491
+ taskTitle: taskTitle || "Cursor Chat Session",
14492
+ model,
14493
+ stepsCount: stepCount,
14494
+ filesEdited: [],
14495
+ timestamp: dbStat.mtime.toISOString(),
14496
+ tokens: {
14497
+ input: estInput,
14498
+ output: estOutput,
14499
+ cached: estCached
14500
+ },
14501
+ cost,
14502
+ mode: "imported",
14503
+ confidence: 0.8,
14504
+ rawTranscriptPath: dbFile
14505
+ });
14506
+ }
14507
+ } catch {
14508
+ }
14509
+ }
14510
+ } catch {
14233
14511
  }
14234
14512
  }
14235
14513
  } catch {
14236
14514
  }
14237
14515
  }
14238
- } catch {
14239
14516
  }
14240
14517
  return footprints;
14241
14518
  }
@@ -14440,7 +14717,8 @@ async function harvestUniversalFootprints(options) {
14440
14717
  const cursorFootprints = harvestCursorFootprints(options.repoPath, sinceDate);
14441
14718
  allFootprints.push(...cursorFootprints);
14442
14719
  }
14443
- allFootprints.sort((a, b2) => new Date(b2.timestamp).getTime() - new Date(a.timestamp).getTime());
14720
+ const { primary, rankedFootprints } = selectPrimaryFootprint(allFootprints, options.gitContext);
14721
+ allFootprints = rankedFootprints;
14444
14722
  try {
14445
14723
  const db = new LocalStateDB();
14446
14724
  for (const fp of allFootprints) {
@@ -14467,7 +14745,6 @@ async function harvestUniversalFootprints(options) {
14467
14745
  totalCached += fp.tokens.cached;
14468
14746
  totalCost += fp.cost;
14469
14747
  }
14470
- const primary = allFootprints[0];
14471
14748
  return {
14472
14749
  footprints: allFootprints,
14473
14750
  primaryFootprint: primary,
@@ -14501,16 +14778,16 @@ function estimateCost(options) {
14501
14778
  totalCost += s.cost;
14502
14779
  }
14503
14780
  return {
14504
- provider: primary.provider,
14505
- model: primary.model || "claude-3-7-sonnet",
14781
+ provider: options.provider || primary.provider,
14782
+ model: options.model || primary.model || "claude-3-7-sonnet",
14506
14783
  tokens: {
14507
14784
  input: totalInput,
14508
14785
  output: totalOutput,
14509
14786
  cached: totalCached
14510
14787
  },
14511
14788
  cost: Number(totalCost.toFixed(4)),
14512
- mode: primary.mode,
14513
- confidence: primary.confidence,
14789
+ mode: options.mode || primary.mode,
14790
+ confidence: options.confidence !== void 0 ? options.confidence : primary.confidence,
14514
14791
  sessions: options.sessions
14515
14792
  };
14516
14793
  }
@@ -14630,7 +14907,13 @@ async function generateReceipt(options = {}) {
14630
14907
  repoPath,
14631
14908
  projectAlias: metrics.projectAlias,
14632
14909
  since: options.since,
14633
- platform: options.platform
14910
+ platform: options.platform,
14911
+ gitContext: {
14912
+ headSha: metrics.headSha,
14913
+ commitDate: metrics.commitDate,
14914
+ changedFiles: metrics.changedFiles,
14915
+ branch: metrics.branch
14916
+ }
14634
14917
  });
14635
14918
  const primaryFp = harvestResult.primaryFootprint;
14636
14919
  const detected = detectProviderFromCommit(metrics.commitMessage);
@@ -15073,6 +15356,12 @@ function buildTerminalReceipt(receipt, publicUrl) {
15073
15356
  const provString = receipt.ai.provider + " \xB7 " + (receipt.ai.model || "Unknown");
15074
15357
  const cleanProv = provString.length > 40 ? provString.slice(0, 39) + "\u2026" : provString;
15075
15358
  printRow(`Provider: ${cleanProv}`);
15359
+ if (receipt.ai.sessions && receipt.ai.sessions.length > 1) {
15360
+ const others = receipt.ai.sessions.slice(1).map((s) => s.provider).filter((p2, i2, arr) => arr.indexOf(p2) === i2 && p2 !== receipt.ai.provider);
15361
+ if (others.length > 0) {
15362
+ printRow(import_picocolors.default.dim(`Also seen: ${others.join(", ")}`));
15363
+ }
15364
+ }
15076
15365
  if (receipt.ai.aiWrittenRatio !== void 0) {
15077
15366
  const aiPct = Math.round(receipt.ai.aiWrittenRatio * 100);
15078
15367
  printRowSplit("AI Written Code", `${aiPct}% (Human: ${100 - aiPct}%)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qodewk",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Universal telemetry and digital receipt generator for the AI coding agent era",
5
5
  "type": "module",
6
6
  "bin": {