kodevu 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,3 +109,4 @@ Internal defaults:
109
109
  - If `outputFormats` includes `json`, matching `.json` files are generated alongside Markdown reports.
110
110
  - `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
111
111
  - If the reviewer command exits non-zero or times out, the report is still written, but the state is not advanced so the change can be retried later.
112
+ - Each report includes a `Token Usage` section recording token consumption for the review task. When the reviewer CLI outputs token statistics (via stderr), those are used directly (`source: "reviewer"`). Otherwise tokens are estimated at ~4 characters per token (`source: "estimate"`). The JSON report contains a `tokenUsage` object with `inputTokens`, `outputTokens`, `totalTokens`, and `source`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
6
6
  "bin": {
@@ -23,6 +23,85 @@ function debugLog(config, message) {
23
23
  }
24
24
  }
25
25
 
26
+ function estimateTokenCount(text) {
27
+ if (!text) {
28
+ return 0;
29
+ }
30
+
31
+ return Math.ceil(text.length / 4);
32
+ }
33
+
34
+ function parseGeminiTokenUsage(stderr) {
35
+ if (!stderr) {
36
+ return null;
37
+ }
38
+
39
+ const patterns = [
40
+ /input[_ ]tokens?\s*[:=]\s*(\d+)/i,
41
+ /output[_ ]tokens?\s*[:=]\s*(\d+)/i,
42
+ /total[_ ]tokens?\s*[:=]\s*(\d+)/i
43
+ ];
44
+
45
+ const inputMatch = stderr.match(patterns[0]);
46
+ const outputMatch = stderr.match(patterns[1]);
47
+ const totalMatch = stderr.match(patterns[2]);
48
+
49
+ if (!inputMatch && !outputMatch && !totalMatch) {
50
+ return null;
51
+ }
52
+
53
+ const inputTokens = inputMatch ? Number(inputMatch[1]) : 0;
54
+ const outputTokens = outputMatch ? Number(outputMatch[1]) : 0;
55
+ const totalTokens = totalMatch ? Number(totalMatch[1]) : inputTokens + outputTokens;
56
+
57
+ return { inputTokens, outputTokens, totalTokens };
58
+ }
59
+
60
+ function parseCodexTokenUsage(stderr) {
61
+ if (!stderr) {
62
+ return null;
63
+ }
64
+
65
+ const patterns = [
66
+ /input[_ ]tokens?\s*[:=]\s*(\d+)/i,
67
+ /output[_ ]tokens?\s*[:=]\s*(\d+)/i,
68
+ /total[_ ]tokens?\s*[:=]\s*(\d+)/i
69
+ ];
70
+
71
+ const inputMatch = stderr.match(patterns[0]);
72
+ const outputMatch = stderr.match(patterns[1]);
73
+ const totalMatch = stderr.match(patterns[2]);
74
+
75
+ if (!inputMatch && !outputMatch && !totalMatch) {
76
+ return null;
77
+ }
78
+
79
+ const inputTokens = inputMatch ? Number(inputMatch[1]) : 0;
80
+ const outputTokens = outputMatch ? Number(outputMatch[1]) : 0;
81
+ const totalTokens = totalMatch ? Number(totalMatch[1]) : inputTokens + outputTokens;
82
+
83
+ return { inputTokens, outputTokens, totalTokens };
84
+ }
85
+
86
+ function resolveTokenUsage(reviewerName, stderr, promptText, diffText, responseText) {
87
+ const parseFn = reviewerName === "gemini" ? parseGeminiTokenUsage : parseCodexTokenUsage;
88
+ const parsed = parseFn(stderr);
89
+
90
+ if (parsed && parsed.totalTokens > 0) {
91
+ return { ...parsed, source: "reviewer" };
92
+ }
93
+
94
+ const inputTokens = estimateTokenCount((promptText || "") + (diffText || ""));
95
+ const outputTokens = estimateTokenCount(responseText || "");
96
+
97
+ return {
98
+ inputTokens,
99
+ outputTokens,
100
+ totalTokens: inputTokens + outputTokens,
101
+ source: "estimate"
102
+ };
103
+ }
104
+
26
105
  const REVIEWERS = {
27
106
  codex: {
28
107
  displayName: "Codex",
@@ -314,7 +393,17 @@ function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
314
393
  ].join("\n\n");
315
394
  }
316
395
 
317
- function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
396
+ function formatTokenUsage(tokenUsage) {
397
+ const sourceLabel = tokenUsage.source === "reviewer" ? "reviewer reported" : "estimated (~4 chars/token)";
398
+ return [
399
+ `- Input Tokens: \`${tokenUsage.inputTokens}\``,
400
+ `- Output Tokens: \`${tokenUsage.outputTokens}\``,
401
+ `- Total Tokens: \`${tokenUsage.totalTokens}\``,
402
+ `- Token Source: \`${sourceLabel}\``
403
+ ].join("\n");
404
+ }
405
+
406
+ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
318
407
  const lines = [
319
408
  `# ${backend.displayName} Review Report: ${details.displayId}`,
320
409
  "",
@@ -328,6 +417,10 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
328
417
  `- Reviewer Exit Code: \`${reviewerResult.code}\``,
329
418
  `- Reviewer Timed Out: \`${reviewerResult.timedOut ? "yes" : "no"}\``,
330
419
  "",
420
+ "## Token Usage",
421
+ "",
422
+ formatTokenUsage(tokenUsage),
423
+ "",
331
424
  "## Changed Files",
332
425
  "",
333
426
  formatChangedPaths(details.changedPaths),
@@ -361,7 +454,7 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
361
454
  return `${lines.join("\n")}\n`;
362
455
  }
363
456
 
364
- function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
457
+ function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
365
458
  return {
366
459
  repositoryType: backend.displayName,
367
460
  target: targetInfo.targetDisplay || config.target,
@@ -374,6 +467,12 @@ function buildJsonReport(config, backend, targetInfo, details, diffPayloads, rev
374
467
  exitCode: reviewerResult.code,
375
468
  timedOut: Boolean(reviewerResult.timedOut)
376
469
  },
470
+ tokenUsage: {
471
+ inputTokens: tokenUsage.inputTokens,
472
+ outputTokens: tokenUsage.outputTokens,
473
+ totalTokens: tokenUsage.totalTokens,
474
+ source: tokenUsage.source
475
+ },
377
476
  changedFiles: details.changedPaths.map((item) => ({
378
477
  action: item.action,
379
478
  path: item.relativePath,
@@ -407,10 +506,20 @@ async function runReviewerPrompt(config, backend, targetInfo, details, diffText)
407
506
  const reviewWorkspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
408
507
  const diffPayloads = prepareDiffPayloads(config, diffText);
409
508
  const promptText = buildPrompt(config, backend, targetInfo, details, diffPayloads.review);
509
+ const result = await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text);
510
+ const tokenUsage = resolveTokenUsage(
511
+ config.reviewer,
512
+ result.stderr,
513
+ promptText,
514
+ diffPayloads.review.text,
515
+ result.message
516
+ );
517
+
410
518
  return {
411
519
  reviewer,
412
520
  diffPayloads,
413
- result: await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text)
521
+ result,
522
+ tokenUsage
414
523
  };
415
524
  }
416
525
 
@@ -469,6 +578,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
469
578
  let reviewer;
470
579
  let diffPayloads;
471
580
  let reviewerResult;
581
+ let tokenUsage;
472
582
  let currentReviewerConfig;
473
583
 
474
584
  for (const reviewerName of reviewersToTry) {
@@ -486,6 +596,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
486
596
  reviewer = res.reviewer;
487
597
  diffPayloads = res.diffPayloads;
488
598
  reviewerResult = res.result;
599
+ tokenUsage = res.tokenUsage;
489
600
 
490
601
  if (reviewerResult.code === 0 && !reviewerResult.timedOut) {
491
602
  break;
@@ -497,7 +608,8 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
497
608
  }
498
609
 
499
610
  progress?.update(0.82, "writing report");
500
- const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult);
611
+ debugLog(config, `Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
612
+ const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
501
613
  const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
502
614
  const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
503
615
 
@@ -508,7 +620,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
508
620
  if (shouldWriteFormat(config, "json")) {
509
621
  await writeJsonFile(
510
622
  jsonOutputFile,
511
- buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult)
623
+ buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage)
512
624
  );
513
625
  }
514
626