vibe-roast 0.98.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 (164) hide show
  1. package/README.md +418 -0
  2. package/assests/README.md +43 -0
  3. package/assests/badges/.gitkeep +0 -0
  4. package/assests/badges/01-builder/builder-badge.png +0 -0
  5. package/assests/badges/01-builder/builder-badge.svg +4 -0
  6. package/assests/badges/02-debugger/debugger-badge.png +0 -0
  7. package/assests/badges/02-debugger/debugger-badge.svg +4 -0
  8. package/assests/badges/03-architect/architect-badge.png +0 -0
  9. package/assests/badges/03-architect/architect-badge.svg +4 -0
  10. package/assests/badges/04-codex-believer/codex-believer-badge.png +0 -0
  11. package/assests/badges/04-codex-believer/codex-believer-badge.svg +4 -0
  12. package/assests/badges/05-prompt-priest/prompt-priest-badge.png +0 -0
  13. package/assests/badges/05-prompt-priest/prompt-priest-badge.svg +4 -0
  14. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.png +0 -0
  15. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.svg +4 -0
  16. package/assests/badges/07-context-maxxer/context-maxxer-badge.png +0 -0
  17. package/assests/badges/07-context-maxxer/context-maxxer-badge.svg +4 -0
  18. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.png +0 -0
  19. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.svg +4 -0
  20. package/assests/banners/.gitkeep +0 -0
  21. package/assests/banners/home-hero.png +0 -0
  22. package/assests/banners/home-hero.svg +3 -0
  23. package/assests/banners/result-header.png +0 -0
  24. package/assests/banners/result-header.svg +3 -0
  25. package/assests/banners/share-strip.png +0 -0
  26. package/assests/banners/share-strip.svg +3 -0
  27. package/assests/characters/01-builder/builder-card.sbti-archive.svg +5 -0
  28. package/assests/characters/01-builder/builder-card.svg +3 -0
  29. package/assests/characters/01-builder/builder-character.sbti-archive.svg +3 -0
  30. package/assests/characters/01-builder/builder-character.svg +3 -0
  31. package/assests/characters/01-builder/builder-figure.png +0 -0
  32. package/assests/characters/01-builder/builder-mascot.png +0 -0
  33. package/assests/characters/02-debugger/debugger-card.sbti-archive.svg +5 -0
  34. package/assests/characters/02-debugger/debugger-card.svg +3 -0
  35. package/assests/characters/02-debugger/debugger-character.sbti-archive.svg +3 -0
  36. package/assests/characters/02-debugger/debugger-character.svg +3 -0
  37. package/assests/characters/02-debugger/debugger-figure.png +0 -0
  38. package/assests/characters/02-debugger/debugger-mascot.png +0 -0
  39. package/assests/characters/03-architect/architect-card.sbti-archive.svg +5 -0
  40. package/assests/characters/03-architect/architect-card.svg +3 -0
  41. package/assests/characters/03-architect/architect-character.sbti-archive.svg +3 -0
  42. package/assests/characters/03-architect/architect-character.svg +3 -0
  43. package/assests/characters/03-architect/architect-figure.png +0 -0
  44. package/assests/characters/03-architect/architect-mascot.png +0 -0
  45. package/assests/characters/04-codex-believer/codex-believer-card.sbti-archive.svg +5 -0
  46. package/assests/characters/04-codex-believer/codex-believer-card.svg +3 -0
  47. package/assests/characters/04-codex-believer/codex-believer-character.sbti-archive.svg +3 -0
  48. package/assests/characters/04-codex-believer/codex-believer-character.svg +3 -0
  49. package/assests/characters/04-codex-believer/codex-believer-figure.png +0 -0
  50. package/assests/characters/04-codex-believer/codex-believer-mascot.png +0 -0
  51. package/assests/characters/05-prompt-priest/prompt-priest-card.sbti-archive.svg +5 -0
  52. package/assests/characters/05-prompt-priest/prompt-priest-card.svg +3 -0
  53. package/assests/characters/05-prompt-priest/prompt-priest-character.sbti-archive.svg +3 -0
  54. package/assests/characters/05-prompt-priest/prompt-priest-character.svg +3 -0
  55. package/assests/characters/05-prompt-priest/prompt-priest-figure.png +0 -0
  56. package/assests/characters/05-prompt-priest/prompt-priest-mascot.png +0 -0
  57. package/assests/characters/06-tab-hoarder/tab-hoarder-card.sbti-archive.svg +5 -0
  58. package/assests/characters/06-tab-hoarder/tab-hoarder-card.svg +3 -0
  59. package/assests/characters/06-tab-hoarder/tab-hoarder-character.sbti-archive.svg +3 -0
  60. package/assests/characters/06-tab-hoarder/tab-hoarder-character.svg +3 -0
  61. package/assests/characters/06-tab-hoarder/tab-hoarder-figure.png +0 -0
  62. package/assests/characters/06-tab-hoarder/tab-hoarder-mascot.png +0 -0
  63. package/assests/characters/07-context-maxxer/context-maxxer-card.sbti-archive.svg +5 -0
  64. package/assests/characters/07-context-maxxer/context-maxxer-card.svg +3 -0
  65. package/assests/characters/07-context-maxxer/context-maxxer-character.sbti-archive.svg +3 -0
  66. package/assests/characters/07-context-maxxer/context-maxxer-character.svg +3 -0
  67. package/assests/characters/07-context-maxxer/context-maxxer-figure.png +0 -0
  68. package/assests/characters/07-context-maxxer/context-maxxer-mascot.png +0 -0
  69. package/assests/characters/08-yolo-shipper/yolo-shipper-card.sbti-archive.svg +5 -0
  70. package/assests/characters/08-yolo-shipper/yolo-shipper-card.svg +3 -0
  71. package/assests/characters/08-yolo-shipper/yolo-shipper-character.sbti-archive.svg +3 -0
  72. package/assests/characters/08-yolo-shipper/yolo-shipper-character.svg +3 -0
  73. package/assests/characters/08-yolo-shipper/yolo-shipper-figure.png +0 -0
  74. package/assests/characters/08-yolo-shipper/yolo-shipper-mascot.png +0 -0
  75. package/assests/characters-vibe-types/aosf-strategy-shipper/strategy-shipper-figure.png +0 -0
  76. package/assests/characters-vibe-types/aosx-agent-believer/agent-believer-figure.png +0 -0
  77. package/assests/characters-vibe-types/aovf-systems-architect/systems-architect-figure.png +0 -0
  78. package/assests/characters-vibe-types/aovx-context-cartographer/context-cartographer-figure.png +0 -0
  79. package/assests/characters-vibe-types/apsf-diagram-sprinter/diagram-sprinter-figure.png +0 -0
  80. package/assests/characters-vibe-types/apsx-infinite-planner/infinite-planner-figure.png +0 -0
  81. package/assests/characters-vibe-types/apvf-architect/architect-figure.png +0 -0
  82. package/assests/characters-vibe-types/apvx-prompt-priest/prompt-priest-figure.png +0 -0
  83. package/assests/characters-vibe-types/mosf-yolo-shipper/yolo-shipper-figure.png +0 -0
  84. package/assests/characters-vibe-types/mosx-agent-commander/agent-commander-figure.png +0 -0
  85. package/assests/characters-vibe-types/movf-agent-engineer/agent-engineer-figure.png +0 -0
  86. package/assests/characters-vibe-types/movx-systems-wrangler/systems-wrangler-figure.png +0 -0
  87. package/assests/characters-vibe-types/mpsf-builder/builder-figure.png +0 -0
  88. package/assests/characters-vibe-types/mpsx-tab-hoarder/tab-hoarder-figure.png +0 -0
  89. package/assests/characters-vibe-types/mpvf-debugger/debugger-figure.png +0 -0
  90. package/assests/characters-vibe-types/mpvx-context-maxxer/context-maxxer-figure.png +0 -0
  91. package/assests/live-report.html +1403 -0
  92. package/assests/notes/live-inspect-summary.json +3487 -0
  93. package/assests/scripts/compose-banners.py +148 -0
  94. package/assests/scripts/live-report-boot.js +2 -0
  95. package/assests/scripts/profiles.json +104 -0
  96. package/assests/scripts/qa-faces.mjs +72 -0
  97. package/assests/scripts/roast-extras.js +619 -0
  98. package/assests/scripts/score-engine.js +62 -0
  99. package/assests/scripts/split-badges.py +49 -0
  100. package/assests/scripts/split-reference.py +38 -0
  101. package/assests/scripts/sync-banners.mjs +45 -0
  102. package/assests/scripts/sync-cards.mjs +50 -0
  103. package/assests/scripts/sync-raster.mjs +118 -0
  104. package/assests/scripts/vibe-types.json +22 -0
  105. package/assests/source/design-system.md +50 -0
  106. package/assests/source/prompts/01-builder.md +19 -0
  107. package/assests/source/prompts/02-debugger.md +17 -0
  108. package/assests/source/prompts/03-architect.md +16 -0
  109. package/assests/source/prompts/04-codex-believer.md +13 -0
  110. package/assests/source/prompts/05-prompt-priest.md +13 -0
  111. package/assests/source/prompts/06-tab-hoarder.md +13 -0
  112. package/assests/source/prompts/07-context-maxxer.md +13 -0
  113. package/assests/source/prompts/08-yolo-shipper.md +13 -0
  114. package/assests/source/references/sbti-style-reference-board.png +0 -0
  115. package/assests/source/references/vibe-badges-v6-board.png +0 -0
  116. package/assests/source/references/vibe-mascots-v4-reference.png +0 -0
  117. package/assests/source/references/vibe-profiles-v5-board-a.png +0 -0
  118. package/assests/source/references/vibe-profiles-v5-board-b.png +0 -0
  119. package/assests/source/vibe-types-visual-brief.md +38 -0
  120. package/bin/hook.js +121 -0
  121. package/bin/vibe-roast.js +98 -0
  122. package/bin/vibe-wrapper.js +4 -0
  123. package/dashboard/dist/assets/index-DlBycTcO.css +1 -0
  124. package/dashboard/dist/assets/index-t8m64Hm9.js +168 -0
  125. package/dashboard/dist/index.html +32 -0
  126. package/package.json +58 -0
  127. package/src/extract/environment.js +160 -0
  128. package/src/extract/phrase-stats.js +684 -0
  129. package/src/extract/prompt-analysis.js +368 -0
  130. package/src/extract/text.js +30 -0
  131. package/src/hooks/install.js +184 -0
  132. package/src/inspect.js +261 -0
  133. package/src/lib/activity-metrics.js +238 -0
  134. package/src/lib/agent-score.js +187 -0
  135. package/src/lib/ai-roast.js +446 -0
  136. package/src/lib/dates.js +43 -0
  137. package/src/lib/hashtags.js +150 -0
  138. package/src/lib/jsonl.js +48 -0
  139. package/src/lib/load-env.js +59 -0
  140. package/src/lib/roast-evidence.js +186 -0
  141. package/src/lib/roast-snapshot.js +58 -0
  142. package/src/server.js +644 -0
  143. package/src/sources/aider.js +150 -0
  144. package/src/sources/amazonq.js +113 -0
  145. package/src/sources/antigravity.js +147 -0
  146. package/src/sources/claude.js +262 -0
  147. package/src/sources/cline.js +11 -0
  148. package/src/sources/codex.js +222 -0
  149. package/src/sources/common.js +133 -0
  150. package/src/sources/continue.js +67 -0
  151. package/src/sources/copilot.js +170 -0
  152. package/src/sources/cursor.js +238 -0
  153. package/src/sources/gemini.js +75 -0
  154. package/src/sources/index.js +61 -0
  155. package/src/sources/roo.js +11 -0
  156. package/src/sources/token-tracker.js +171 -0
  157. package/src/sources/vibe-tracker.js +79 -0
  158. package/src/sources/vscode-tasks.js +142 -0
  159. package/src/sources/windsurf.js +109 -0
  160. package/worker/README.md +96 -0
  161. package/worker/package-lock.json +1547 -0
  162. package/worker/package.json +20 -0
  163. package/worker/src/index.mjs +694 -0
  164. package/worker/wrangler.jsonc +40 -0
package/src/inspect.js ADDED
@@ -0,0 +1,261 @@
1
+ const { dayBounds } = require("./lib/dates");
2
+ const { wordCloudRecords, wordFrequenciesFromRecords } = require("./extract/phrase-stats");
3
+ const { analyzePrompts } = require("./extract/prompt-analysis");
4
+ const { inspectEnvironment } = require("./extract/environment");
5
+ const { buildVibeProfile } = require("./lib/agent-score");
6
+ const { buildRoastEvidence } = require("./lib/roast-evidence");
7
+ const { inspectTokenTracker } = require("./sources/token-tracker");
8
+ const { SOURCE_INSPECTORS, normalizeSources } = require("./sources");
9
+
10
+ async function inspectSources({ from, to, sources, roots = {}, injectedReports = {} } = {}) {
11
+ const range = dayBounds(from, to);
12
+ const injectedSourceNames = Object.keys(injectedReports || {});
13
+ const selected = Array.isArray(sources) && sources.length === 0 && injectedSourceNames.length > 0
14
+ ? injectedSourceNames
15
+ : normalizeSources(sources);
16
+ const sourceReports = {};
17
+ const prompts = [];
18
+ const tokenTrackerActivity = await inspectTokenTracker({
19
+ queuePath: roots.tokenTrackerQueue,
20
+ range,
21
+ });
22
+
23
+ for (const source of selected) {
24
+ const inspect = SOURCE_INSPECTORS[source];
25
+ if (!inspect && !injectedReports[source]) continue;
26
+ const report = injectedReports[source] || await inspect({
27
+ root: roots[source],
28
+ dbPath: roots.cursorDb || roots.cursor,
29
+ range,
30
+ home: roots.home,
31
+ });
32
+ sourceReports[source] = stripPrompts(report);
33
+ prompts.push(...(report.prompts || []));
34
+ }
35
+
36
+ prompts.sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
37
+ const promptAnalysis = analyzePrompts(prompts);
38
+ const wordSourcePrompts = prompts.filter((prompt) => prompt.timestamp);
39
+ const wordPromptAnalysis = analyzePrompts(wordSourcePrompts.length > 0 ? wordSourcePrompts : prompts, {
40
+ usefulLimit: prompts.length,
41
+ });
42
+ const environment = await inspectEnvironment({
43
+ home: roots.home,
44
+ codexHome: roots.codexHome,
45
+ });
46
+ const activity = enrichActivity(buildActivity(tokenTrackerActivity, prompts, sourceReports));
47
+ const summary = buildSummary(sourceReports, prompts, activity);
48
+ const vibe_profile = buildVibeProfile({
49
+ categories: promptAnalysis.categories || {},
50
+ env: environment?.codex || {},
51
+ summary,
52
+ promptAnalysis,
53
+ activity,
54
+ });
55
+ const word_cloud_records = wordCloudRecords(
56
+ wordPromptAnalysis.useful_for_stats || wordPromptAnalysis.useful_prompts,
57
+ );
58
+
59
+ const report = {
60
+ generated_at: new Date().toISOString(),
61
+ range: { from: from || null, to: to || null },
62
+ summary,
63
+ sources: sourceReports,
64
+ activity,
65
+ word_frequencies: wordFrequenciesFromRecords(word_cloud_records),
66
+ word_cloud_records,
67
+ profile_signals: {
68
+ prompt_analysis: promptAnalysis,
69
+ environment,
70
+ },
71
+ vibe_profile,
72
+ prompts,
73
+ };
74
+ report.roast_evidence = buildRoastEvidence(report);
75
+ return report;
76
+ }
77
+
78
+ function buildActivity(tokenTrackerActivity, prompts = [], sourceReports = {}) {
79
+ if (tokenTrackerActivity?.daily_rows?.length > 0) {
80
+ return {
81
+ source: "token-tracker",
82
+ metric: "tokens",
83
+ daily_rows: attachContextBreakdown(tokenTrackerActivity.daily_rows, sourceReports),
84
+ total_tokens: tokenTrackerActivity.token_totals?.total_tokens || 0,
85
+ active_day_count: tokenTrackerActivity.active_day_count || 0,
86
+ bucket_count: tokenTrackerActivity.bucket_count || 0,
87
+ daily_row_count: tokenTrackerActivity.daily_rows.length,
88
+ root: tokenTrackerActivity.root,
89
+ estimated_cost_usd: parseOptionalCost(tokenTrackerActivity.estimated_cost_usd),
90
+ };
91
+ }
92
+
93
+ const byDay = new Map();
94
+ for (const prompt of prompts || []) {
95
+ if (!prompt?.timestamp) continue;
96
+ const day = String(prompt.timestamp).slice(0, 10);
97
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
98
+ const row = byDay.get(day) || { day, value: 0, conversation_count: 0, models: {}, sources: {} };
99
+ row.value += 1;
100
+ row.conversation_count += 1;
101
+ const source = prompt.source || "unknown";
102
+ row.sources[source] = (row.sources[source] || 0) + 1;
103
+ row.models[source] = (row.models[source] || 0) + 1;
104
+ byDay.set(day, row);
105
+ }
106
+
107
+ const daily_rows = [...byDay.values()]
108
+ .sort((a, b) => a.day.localeCompare(b.day))
109
+ .map((row) => ({
110
+ day: row.day,
111
+ value: row.value,
112
+ total_tokens: row.value,
113
+ billable_total_tokens: row.value,
114
+ conversation_count: row.conversation_count,
115
+ models: row.models,
116
+ sources: row.sources,
117
+ }));
118
+
119
+ return {
120
+ source: "prompts",
121
+ metric: "prompts",
122
+ daily_rows,
123
+ total_tokens: 0,
124
+ active_day_count: daily_rows.length,
125
+ bucket_count: 0,
126
+ daily_row_count: daily_rows.length,
127
+ root: null,
128
+ estimated_cost_usd: null,
129
+ };
130
+ }
131
+
132
+ function attachContextBreakdown(dailyRows, sourceReports) {
133
+ const contextByDay = new Map();
134
+ for (const [source, report] of Object.entries(sourceReports || {})) {
135
+ for (const row of report?.context_breakdown_daily || []) {
136
+ if (!row?.day) continue;
137
+ const day = contextByDay.get(row.day) || {};
138
+ day[source] = row;
139
+ contextByDay.set(row.day, day);
140
+ }
141
+ }
142
+ return (dailyRows || []).map((row) => ({
143
+ ...row,
144
+ context_breakdown: contextByDay.get(row.day) || {},
145
+ }));
146
+ }
147
+
148
+ function enrichActivity(activity) {
149
+ const { summarizeActivity } = require("./lib/activity-metrics");
150
+ const stats = summarizeActivity(activity);
151
+ return {
152
+ ...activity,
153
+ total_tokens: activity.metric === "tokens"
154
+ ? (activity.total_tokens || stats.totalValue)
155
+ : activity.total_tokens || 0,
156
+ active_day_count: stats.activeDays || activity.active_day_count || 0,
157
+ longest_streak: stats.maxStreak,
158
+ peak_day: stats.peakDay.day
159
+ ? { day: stats.peakDay.day, value: stats.peakDay.value }
160
+ : null,
161
+ top_agent: stats.topAgent,
162
+ top_provider: stats.topProvider,
163
+ top_model: stats.topModel,
164
+ active_rate: stats.activeRate,
165
+ estimated_cost_usd: parseOptionalCost(activity.estimated_cost_usd),
166
+ };
167
+ }
168
+
169
+ function parseOptionalCost(value) {
170
+ if (value == null || value === "") return null;
171
+ const n = Number(value);
172
+ return Number.isFinite(n) && n > 0 ? n : null;
173
+ }
174
+
175
+ function stripPrompts(report) {
176
+ const { prompts: _prompts, ...rest } = report;
177
+ return rest;
178
+ }
179
+
180
+ function buildSummary(sourceReports, prompts, activity) {
181
+ const tokenTotals = emptyTotals();
182
+ let filesScanned = 0;
183
+ for (const report of Object.values(sourceReports)) {
184
+ filesScanned += report.files_scanned || 0;
185
+ addTotals(tokenTotals, report.token_totals || {});
186
+ }
187
+
188
+ const timestampedPrompts = prompts.filter((prompt) => prompt.timestamp);
189
+ const activeSources = rankActiveSources(sourceReports, prompts, activity);
190
+
191
+ return {
192
+ // Kept for CLI/API compatibility: this is the number of adapters inspected.
193
+ source_count: Object.keys(sourceReports).length,
194
+ active_source_count: activeSources.length,
195
+ active_sources: activeSources,
196
+ files_scanned: filesScanned,
197
+ prompt_count: prompts.length,
198
+ first_prompt_at: timestampedPrompts[0]?.timestamp || null,
199
+ last_prompt_at: timestampedPrompts[timestampedPrompts.length - 1]?.timestamp || null,
200
+ token_totals: tokenTotals,
201
+ };
202
+ }
203
+
204
+ function rankActiveSources(sourceReports, prompts, activity) {
205
+ const scores = new Map();
206
+ const add = (source, value = 1) => {
207
+ const key = normalizeSourceId(source);
208
+ const amount = Number(value);
209
+ if (!key || !Number.isFinite(amount) || amount <= 0) return;
210
+ scores.set(key, (scores.get(key) || 0) + amount);
211
+ };
212
+
213
+ for (const prompt of prompts || []) add(prompt?.source);
214
+
215
+ for (const [key, report] of Object.entries(sourceReports || {})) {
216
+ const promptCount = Number(report?.prompt_count || 0);
217
+ const tokenCount = Number(report?.token_totals?.total_tokens || 0);
218
+ if (promptCount > 0 || tokenCount > 0) add(report?.source || key, promptCount || tokenCount);
219
+ }
220
+
221
+ for (const row of activity?.daily_rows || []) {
222
+ for (const [source, value] of Object.entries(row?.sources || {})) add(source, value);
223
+ }
224
+
225
+ return [...scores.entries()]
226
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
227
+ .map(([source]) => source);
228
+ }
229
+
230
+ function normalizeSourceId(source) {
231
+ const key = String(source || "").trim().toLowerCase().replace(/[\s_]+/g, "-");
232
+ if (!key || key === "unknown") return null;
233
+ const aliases = {
234
+ "claude-code": "claude",
235
+ "github-copilot": "copilot",
236
+ "amazon-q": "amazonq",
237
+ "gemini-cli": "gemini",
238
+ "roo-code": "roo",
239
+ };
240
+ return aliases[key] || key;
241
+ }
242
+
243
+ function emptyTotals() {
244
+ return {
245
+ input_tokens: 0,
246
+ cached_input_tokens: 0,
247
+ cache_creation_input_tokens: 0,
248
+ output_tokens: 0,
249
+ reasoning_output_tokens: 0,
250
+ total_tokens: 0,
251
+ };
252
+ }
253
+
254
+ function addTotals(target, delta) {
255
+ for (const key of Object.keys(target)) {
256
+ const n = Number(delta[key] || 0);
257
+ target[key] += Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
258
+ }
259
+ }
260
+
261
+ module.exports = { inspectSources, SOURCE_INSPECTORS, normalizeSources };
@@ -0,0 +1,238 @@
1
+ function number(value) {
2
+ const n = Number(value || 0);
3
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
4
+ }
5
+
6
+ function compactNumber(value) {
7
+ const n = Number(value) || 0;
8
+ if (n < 1000) return String(Math.round(n));
9
+ if (n < 1000000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
10
+ if (n < 10000000) return `${(n / 1000000).toFixed(2)}M`;
11
+ if (n < 1000000000) return `${(n / 1000000).toFixed(1)}M`;
12
+ return `${(n / 1000000000).toFixed(n >= 10000000000 ? 0 : 2)}B`;
13
+ }
14
+
15
+ function formatUsd(value) {
16
+ if (value == null || value === "") return null;
17
+ const n = Number(value);
18
+ if (!Number.isFinite(n) || n <= 0) return null;
19
+ return `$${n.toFixed(n >= 100 ? 0 : 2)}`;
20
+ }
21
+
22
+ /** Derive streak / peak / totals from activity.daily_rows (sorted or unsorted). */
23
+ function summarizeActivity(activity = {}) {
24
+ const rows = Array.isArray(activity.daily_rows) ? [...activity.daily_rows] : [];
25
+ rows.sort((a, b) => String(a.day || "").localeCompare(String(b.day || "")));
26
+
27
+ let totalValue = 0;
28
+ let activeDays = 0;
29
+ let maxStreak = 0;
30
+ let currentStreak = 0;
31
+ let peakDay = { day: null, value: 0 };
32
+ const sourceTotals = new Map();
33
+ const providerTotals = new Map();
34
+ const modelTotals = new Map();
35
+
36
+ let prevDay = null;
37
+ for (const row of rows) {
38
+ const value = number(row.value ?? row.billable_total_tokens ?? row.total_tokens);
39
+ totalValue += value;
40
+ if (value > 0) {
41
+ activeDays += 1;
42
+ if (prevDay && consecutiveDays(prevDay, row.day)) currentStreak += 1;
43
+ else currentStreak = 1;
44
+ maxStreak = Math.max(maxStreak, currentStreak);
45
+ prevDay = row.day;
46
+ } else {
47
+ currentStreak = 0;
48
+ prevDay = row.day;
49
+ }
50
+ if (value > peakDay.value) peakDay = { day: row.day, value };
51
+
52
+ for (const [source, raw] of Object.entries(row.sources || {})) {
53
+ sourceTotals.set(source, (sourceTotals.get(source) || 0) + number(raw));
54
+ }
55
+ for (const [key, raw] of Object.entries(row.models || {})) {
56
+ const value = number(raw);
57
+ if (!value) continue;
58
+ const model = modelFromKey(key);
59
+ if (!isConcreteModel(model)) continue;
60
+ modelTotals.set(model, (modelTotals.get(model) || 0) + value);
61
+ const provider = inferModelProvider(model);
62
+ if (provider) providerTotals.set(provider, (providerTotals.get(provider) || 0) + value);
63
+ }
64
+ }
65
+
66
+ if (!totalValue && number(activity.total_tokens)) {
67
+ totalValue = number(activity.total_tokens);
68
+ }
69
+ if (!activeDays && number(activity.active_day_count)) {
70
+ activeDays = number(activity.active_day_count);
71
+ }
72
+
73
+ const topAgent = topKey(sourceTotals);
74
+ const topProvider = topKey(providerTotals);
75
+ const topModel = topKey(modelTotals);
76
+ const metric = activity.metric || (totalValue > 0 && activity.source === "token-tracker" ? "tokens" : "prompts");
77
+ const spanDays = rows.length > 0
78
+ ? Math.max(1, daysBetween(rows[0].day, rows[rows.length - 1].day) + 1)
79
+ : 0;
80
+ const activeRate = spanDays ? ((activeDays / spanDays) * 100).toFixed(1) : null;
81
+
82
+ return {
83
+ metric,
84
+ totalValue,
85
+ activeDays,
86
+ maxStreak,
87
+ peakDay,
88
+ topAgent,
89
+ topProvider,
90
+ topModel,
91
+ activeRate,
92
+ estimatedCostUsd: activity.estimated_cost_usd == null || activity.estimated_cost_usd === ""
93
+ ? null
94
+ : (Number.isFinite(Number(activity.estimated_cost_usd)) ? Number(activity.estimated_cost_usd) : null),
95
+ };
96
+ }
97
+
98
+ function topKey(totals) {
99
+ return [...totals.entries()]
100
+ .sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0])))[0]?.[0] || null;
101
+ }
102
+
103
+ function modelFromKey(key) {
104
+ const [, ...modelParts] = String(key || "").split("/");
105
+ return (modelParts.join("/") || String(key || "")).trim();
106
+ }
107
+
108
+ function isConcreteModel(model) {
109
+ const normalized = String(model || "").trim().toLowerCase();
110
+ return Boolean(normalized) && !["auto", "unknown", "default", "other", "n/a"].includes(normalized);
111
+ }
112
+
113
+ function inferModelProvider(model) {
114
+ const value = String(model || "").trim().toLowerCase();
115
+ if (!isConcreteModel(value)) return null;
116
+ if (/(^|[-_/.])(gpt|chatgpt|codex|o[1-9])([-_/.]|$)/.test(value)) return "OpenAI";
117
+ if (/(claude|sonnet|opus|haiku)/.test(value)) return "Anthropic";
118
+ if (/(gemini|gemma)/.test(value)) return "Google";
119
+ if (/deepseek/.test(value)) return "DeepSeek";
120
+ if (/grok/.test(value)) return "xAI";
121
+ if (/(qwen|qwq)/.test(value)) return "Alibaba";
122
+ if (/llama/.test(value)) return "Meta";
123
+ if (/(mistral|mixtral|codestral)/.test(value)) return "Mistral";
124
+ if (/(command-r|cohere)/.test(value)) return "Cohere";
125
+ if (/(kimi|moonshot)/.test(value)) return "Moonshot";
126
+ if (/(glm|zhipu)/.test(value)) return "Zhipu";
127
+ if (/(ernie|baidu)/.test(value)) return "Baidu";
128
+ if (/(doubao|seed-code)/.test(value)) return "ByteDance";
129
+ if (/minimax/.test(value)) return "MiniMax";
130
+ if (/(nova|amazon)/.test(value)) return "Amazon";
131
+ if (/(phi[-_.]|^phi$)/.test(value)) return "Microsoft";
132
+ if (/composer/.test(value)) return "Cursor";
133
+ return null;
134
+ }
135
+
136
+ function formatModelName(model) {
137
+ return String(model || "")
138
+ .replace(/^gpt(?=[-_.]|$)/i, "GPT")
139
+ .replace(/^claude(?=[-_.]|$)/i, "Claude")
140
+ .replace(/^gemini(?=[-_.]|$)/i, "Gemini")
141
+ .replace(/^deepseek(?=[-_.]|$)/i, "DeepSeek")
142
+ .replace(/^grok(?=[-_.]|$)/i, "Grok");
143
+ }
144
+
145
+ function consecutiveDays(prev, next) {
146
+ if (!prev || !next) return false;
147
+ return daysBetween(prev, next) === 1;
148
+ }
149
+
150
+ function daysBetween(a, b) {
151
+ const am = Date.parse(`${a}T00:00:00.000Z`);
152
+ const bm = Date.parse(`${b}T00:00:00.000Z`);
153
+ if (!Number.isFinite(am) || !Number.isFinite(bm)) return NaN;
154
+ return Math.round((bm - am) / 86400000);
155
+ }
156
+
157
+ /**
158
+ * Score-card signals for Roast Result — prefers TokenTracker token KPIs
159
+ * over useful-prompt counts when daily token rows exist.
160
+ */
161
+ function buildActivitySignals({ activity = {}, summary = {}, categories = {} } = {}) {
162
+ const stats = summarizeActivity(activity);
163
+ const topCategory = Object.entries(categories)
164
+ .filter(([key]) => key !== "reference")
165
+ .map(([key, row]) => [key, Number(row?.count || 0)])
166
+ .filter(([, count]) => count > 0)
167
+ .sort((a, b) => b[1] - a[1])[0];
168
+
169
+ if (stats.metric === "tokens" && stats.totalValue > 0) {
170
+ const signals = [
171
+ { label: "Total tokens", labelZh: "总 Tokens", value: compactNumber(stats.totalValue) },
172
+ {
173
+ label: "Active days",
174
+ labelZh: "活跃天数",
175
+ value: String(stats.activeDays || summary.active_day_count || 0),
176
+ },
177
+ ];
178
+ const cost = formatUsd(stats.estimatedCostUsd);
179
+ if (cost) {
180
+ signals.push({ label: "Est. cost", labelZh: "估算费用", value: cost });
181
+ }
182
+ if (stats.topAgent) {
183
+ signals.push({
184
+ label: "Top agent",
185
+ labelZh: "主 Agent",
186
+ value: String(stats.topAgent).toUpperCase(),
187
+ });
188
+ }
189
+ if (stats.topProvider) {
190
+ signals.push({
191
+ label: "Top provider",
192
+ labelZh: "主供应商",
193
+ value: stats.topProvider,
194
+ });
195
+ }
196
+ if (stats.topModel) {
197
+ signals.push({
198
+ label: "Top model",
199
+ labelZh: "主模型",
200
+ value: formatModelName(stats.topModel),
201
+ });
202
+ }
203
+ if (signals.length === 2) {
204
+ signals.push({
205
+ label: "Peak day",
206
+ labelZh: "峰值日",
207
+ value: stats.peakDay.value > 0 ? compactNumber(stats.peakDay.value) : "—",
208
+ });
209
+ }
210
+ return signals;
211
+ }
212
+
213
+ return [
214
+ {
215
+ label: "Active days",
216
+ labelZh: "活跃天数",
217
+ value: String(stats.activeDays || activity.active_day_count || 0),
218
+ },
219
+ {
220
+ label: "Agents found",
221
+ labelZh: "检测到 Agent",
222
+ value: String(summary.active_source_count ?? summary.source_count ?? 0),
223
+ },
224
+ {
225
+ label: "Top category",
226
+ labelZh: "主类别",
227
+ value: topCategory ? String(topCategory[0]) : "—",
228
+ },
229
+ ];
230
+ }
231
+
232
+ module.exports = {
233
+ compactNumber,
234
+ formatUsd,
235
+ inferModelProvider,
236
+ summarizeActivity,
237
+ buildActivitySignals,
238
+ };