Просмотр исходного кода

feat(cli): add token stats to analytics command + stats alias

Shows total/prompt/completion tokens, avg per chapter, and last 5
chapter trend. Register "stats" as alias for "analytics".
Ma 7 месяцев назад
Родитель
Сommit
3de5328a4f
1 измененных файлов с 56 добавлено и 1 удалено
  1. 56 1
      packages/cli/src/commands/analytics.ts

+ 56 - 1
packages/cli/src/commands/analytics.ts

@@ -2,6 +2,14 @@ import { Command } from "commander";
 import { StateManager } from "@actalk/inkos-core";
 import { StateManager } from "@actalk/inkos-core";
 import { loadConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
 import { loadConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
 
 
+interface TokenStats {
+  readonly totalPromptTokens: number;
+  readonly totalCompletionTokens: number;
+  readonly totalTokens: number;
+  readonly avgTokensPerChapter: number;
+  readonly recentTrend: ReadonlyArray<{ readonly chapter: number; readonly totalTokens: number }>;
+}
+
 interface AnalyticsData {
 interface AnalyticsData {
   readonly bookId: string;
   readonly bookId: string;
   readonly totalChapters: number;
   readonly totalChapters: number;
@@ -11,6 +19,7 @@ interface AnalyticsData {
   readonly topIssueCategories: ReadonlyArray<{ readonly category: string; readonly count: number }>;
   readonly topIssueCategories: ReadonlyArray<{ readonly category: string; readonly count: number }>;
   readonly chaptersWithMostIssues: ReadonlyArray<{ readonly chapter: number; readonly issueCount: number }>;
   readonly chaptersWithMostIssues: ReadonlyArray<{ readonly chapter: number; readonly issueCount: number }>;
   readonly statusDistribution: Record<string, number>;
   readonly statusDistribution: Record<string, number>;
+  readonly tokenStats?: TokenStats;
 }
 }
 
 
 export function computeAnalytics(
 export function computeAnalytics(
@@ -20,6 +29,11 @@ export function computeAnalytics(
     readonly status: string;
     readonly status: string;
     readonly wordCount: number;
     readonly wordCount: number;
     readonly auditIssues: ReadonlyArray<string>;
     readonly auditIssues: ReadonlyArray<string>;
+    readonly tokenUsage?: {
+      readonly promptTokens: number;
+      readonly completionTokens: number;
+      readonly totalTokens: number;
+    };
   }>,
   }>,
 ): AnalyticsData {
 ): AnalyticsData {
   const totalChapters = chapters.length;
   const totalChapters = chapters.length;
@@ -65,6 +79,30 @@ export function computeAnalytics(
     statusDistribution[ch.status] = (statusDistribution[ch.status] ?? 0) + 1;
     statusDistribution[ch.status] = (statusDistribution[ch.status] ?? 0) + 1;
   }
   }
 
 
+  // Token stats
+  const chaptersWithUsage = chapters.filter((ch) => ch.tokenUsage);
+  let tokenStats: TokenStats | undefined;
+  if (chaptersWithUsage.length > 0) {
+    const totalPromptTokens = chaptersWithUsage.reduce((sum, ch) => sum + (ch.tokenUsage?.promptTokens ?? 0), 0);
+    const totalCompletionTokens = chaptersWithUsage.reduce((sum, ch) => sum + (ch.tokenUsage?.completionTokens ?? 0), 0);
+    const totalTokens = chaptersWithUsage.reduce((sum, ch) => sum + (ch.tokenUsage?.totalTokens ?? 0), 0);
+    const avgTokensPerChapter = Math.round(totalTokens / chaptersWithUsage.length);
+
+    // Recent 5 chapters trend (sorted by chapter number)
+    const recentTrend = [...chaptersWithUsage]
+      .sort((a, b) => a.number - b.number)
+      .slice(-5)
+      .map((ch) => ({ chapter: ch.number, totalTokens: ch.tokenUsage?.totalTokens ?? 0 }));
+
+    tokenStats = {
+      totalPromptTokens,
+      totalCompletionTokens,
+      totalTokens,
+      avgTokensPerChapter,
+      recentTrend,
+    };
+  }
+
   return {
   return {
     bookId,
     bookId,
     totalChapters,
     totalChapters,
@@ -74,11 +112,13 @@ export function computeAnalytics(
     topIssueCategories,
     topIssueCategories,
     chaptersWithMostIssues,
     chaptersWithMostIssues,
     statusDistribution,
     statusDistribution,
+    tokenStats,
   };
   };
 }
 }
 
 
 export const analyticsCommand = new Command("analytics")
 export const analyticsCommand = new Command("analytics")
-  .description("Show analytics for a book")
+  .alias("stats")
+  .description("Show analytics and token stats for a book")
   .argument("[book-id]", "Book ID (auto-detected if only one book)")
   .argument("[book-id]", "Book ID (auto-detected if only one book)")
   .option("--json", "Output JSON")
   .option("--json", "Output JSON")
   .action(async (bookIdArg: string | undefined, opts) => {
   .action(async (bookIdArg: string | undefined, opts) => {
@@ -110,6 +150,21 @@ export const analyticsCommand = new Command("analytics")
           log("");
           log("");
         }
         }
 
 
+        if (analytics.tokenStats) {
+          log("  Token usage:");
+          log(`    Total tokens: ${analytics.tokenStats.totalTokens.toLocaleString()}`);
+          log(`    Prompt tokens: ${analytics.tokenStats.totalPromptTokens.toLocaleString()}`);
+          log(`    Completion tokens: ${analytics.tokenStats.totalCompletionTokens.toLocaleString()}`);
+          log(`    Avg tokens/chapter: ${analytics.tokenStats.avgTokensPerChapter.toLocaleString()}`);
+          if (analytics.tokenStats.recentTrend.length > 0) {
+            log("    Recent trend:");
+            for (const { chapter, totalTokens } of analytics.tokenStats.recentTrend) {
+              log(`      Ch.${chapter}: ${totalTokens.toLocaleString()} tokens`);
+            }
+          }
+          log("");
+        }
+
         if (analytics.topIssueCategories.length > 0) {
         if (analytics.topIssueCategories.length > 0) {
           log("  Most common issue categories:");
           log("  Most common issue categories:");
           for (const { category, count } of analytics.topIssueCategories) {
           for (const { category, count } of analytics.topIssueCategories) {