analytics.ts 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { Command } from "commander";
  2. import { StateManager, computeAnalytics } from "@actalk/inkos-core";
  3. import { loadConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
  4. export const analyticsCommand = new Command("analytics")
  5. .alias("stats")
  6. .description("Show analytics and token stats for a book")
  7. .argument("[book-id]", "Book ID (auto-detected if only one book)")
  8. .option("--json", "Output JSON")
  9. .action(async (bookIdArg: string | undefined, opts) => {
  10. try {
  11. await loadConfig();
  12. const root = findProjectRoot();
  13. const bookId = await resolveBookId(bookIdArg, root);
  14. const state = new StateManager(root);
  15. const chapters = await state.loadChapterIndex(bookId);
  16. const analytics = computeAnalytics(bookId, chapters);
  17. if (opts.json) {
  18. log(JSON.stringify(analytics, null, 2));
  19. } else {
  20. log(`Analytics for "${bookId}":`);
  21. log("");
  22. log(` Total chapters: ${analytics.totalChapters}`);
  23. log(` Total words: ${analytics.totalWords.toLocaleString()}`);
  24. log(` Avg words/chapter: ${analytics.avgWordsPerChapter.toLocaleString()}`);
  25. log(` Audit pass rate: ${analytics.auditPassRate}%`);
  26. log("");
  27. if (Object.keys(analytics.statusDistribution).length > 0) {
  28. log(" Status distribution:");
  29. for (const [status, count] of Object.entries(analytics.statusDistribution)) {
  30. log(` ${status}: ${count}`);
  31. }
  32. log("");
  33. }
  34. if (analytics.tokenStats) {
  35. log(" Token usage:");
  36. log(` Total tokens: ${analytics.tokenStats.totalTokens.toLocaleString()}`);
  37. log(` Prompt tokens: ${analytics.tokenStats.totalPromptTokens.toLocaleString()}`);
  38. log(` Completion tokens: ${analytics.tokenStats.totalCompletionTokens.toLocaleString()}`);
  39. log(` Avg tokens/chapter: ${analytics.tokenStats.avgTokensPerChapter.toLocaleString()}`);
  40. if (analytics.tokenStats.recentTrend.length > 0) {
  41. log(" Recent trend:");
  42. for (const { chapter, totalTokens } of analytics.tokenStats.recentTrend) {
  43. log(` Ch.${chapter}: ${totalTokens.toLocaleString()} tokens`);
  44. }
  45. }
  46. log("");
  47. }
  48. if (analytics.topIssueCategories.length > 0) {
  49. log(" Most common issue categories:");
  50. for (const { category, count } of analytics.topIssueCategories) {
  51. log(` ${category}: ${count}`);
  52. }
  53. log("");
  54. }
  55. if (analytics.chaptersWithMostIssues.length > 0) {
  56. log(" Chapters with most issues:");
  57. for (const { chapter, issueCount } of analytics.chaptersWithMostIssues) {
  58. log(` Ch.${chapter}: ${issueCount} issues`);
  59. }
  60. }
  61. }
  62. } catch (e) {
  63. if (opts.json) {
  64. log(JSON.stringify({ error: String(e) }));
  65. } else {
  66. logError(`Analytics failed: ${e}`);
  67. }
  68. process.exit(1);
  69. }
  70. });