@tokscale/cli 1.0.16 → 1.0.18

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 (108) hide show
  1. package/dist/cli.js +220 -96
  2. package/dist/cli.js.map +1 -1
  3. package/dist/graph-types.d.ts +1 -1
  4. package/dist/graph-types.d.ts.map +1 -1
  5. package/dist/native-runner.js +5 -5
  6. package/dist/native-runner.js.map +1 -1
  7. package/dist/native.d.ts +9 -30
  8. package/dist/native.d.ts.map +1 -1
  9. package/dist/native.js +18 -134
  10. package/dist/native.js.map +1 -1
  11. package/dist/sessions/types.d.ts +1 -1
  12. package/dist/sessions/types.d.ts.map +1 -1
  13. package/dist/submit.d.ts +2 -0
  14. package/dist/submit.d.ts.map +1 -1
  15. package/dist/submit.js +32 -16
  16. package/dist/submit.js.map +1 -1
  17. package/dist/tui/App.d.ts.map +1 -1
  18. package/dist/tui/App.js +13 -6
  19. package/dist/tui/App.js.map +1 -1
  20. package/dist/tui/components/DailyView.d.ts.map +1 -1
  21. package/dist/tui/components/DailyView.js +25 -8
  22. package/dist/tui/components/DailyView.js.map +1 -1
  23. package/dist/tui/components/DateBreakdownPanel.js +2 -2
  24. package/dist/tui/components/DateBreakdownPanel.js.map +1 -1
  25. package/dist/tui/components/Footer.d.ts.map +1 -1
  26. package/dist/tui/components/Footer.js +2 -3
  27. package/dist/tui/components/Footer.js.map +1 -1
  28. package/dist/tui/components/LoadingSpinner.d.ts.map +1 -1
  29. package/dist/tui/components/LoadingSpinner.js +1 -2
  30. package/dist/tui/components/LoadingSpinner.js.map +1 -1
  31. package/dist/tui/components/ModelView.js +2 -2
  32. package/dist/tui/components/ModelView.js.map +1 -1
  33. package/dist/tui/config/settings.d.ts +4 -4
  34. package/dist/tui/config/settings.d.ts.map +1 -1
  35. package/dist/tui/config/settings.js +11 -4
  36. package/dist/tui/config/settings.js.map +1 -1
  37. package/dist/tui/hooks/useData.d.ts.map +1 -1
  38. package/dist/tui/hooks/useData.js +29 -42
  39. package/dist/tui/hooks/useData.js.map +1 -1
  40. package/dist/tui/types/index.d.ts +2 -2
  41. package/dist/tui/types/index.d.ts.map +1 -1
  42. package/dist/tui/types/index.js +3 -1
  43. package/dist/tui/types/index.js.map +1 -1
  44. package/dist/tui/utils/colors.d.ts +1 -0
  45. package/dist/tui/utils/colors.d.ts.map +1 -1
  46. package/dist/tui/utils/colors.js +7 -0
  47. package/dist/tui/utils/colors.js.map +1 -1
  48. package/dist/wrapped.d.ts.map +1 -1
  49. package/dist/wrapped.js +35 -53
  50. package/dist/wrapped.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/cli.ts +240 -103
  53. package/src/graph-types.ts +1 -1
  54. package/src/native-runner.ts +5 -5
  55. package/src/native.ts +35 -200
  56. package/src/sessions/types.ts +1 -1
  57. package/src/submit.ts +36 -22
  58. package/src/tui/App.tsx +9 -6
  59. package/src/tui/components/DailyView.tsx +29 -11
  60. package/src/tui/components/DateBreakdownPanel.tsx +2 -2
  61. package/src/tui/components/Footer.tsx +7 -2
  62. package/src/tui/components/LoadingSpinner.tsx +1 -2
  63. package/src/tui/components/ModelView.tsx +2 -2
  64. package/src/tui/config/settings.ts +18 -9
  65. package/src/tui/hooks/useData.ts +36 -47
  66. package/src/tui/types/index.ts +5 -4
  67. package/src/tui/utils/colors.ts +7 -0
  68. package/src/wrapped.ts +39 -59
  69. package/dist/graph.d.ts +0 -29
  70. package/dist/graph.d.ts.map +0 -1
  71. package/dist/graph.js +0 -383
  72. package/dist/graph.js.map +0 -1
  73. package/dist/pricing.d.ts +0 -58
  74. package/dist/pricing.d.ts.map +0 -1
  75. package/dist/pricing.js +0 -232
  76. package/dist/pricing.js.map +0 -1
  77. package/dist/sessions/claudecode.d.ts +0 -8
  78. package/dist/sessions/claudecode.d.ts.map +0 -1
  79. package/dist/sessions/claudecode.js +0 -84
  80. package/dist/sessions/claudecode.js.map +0 -1
  81. package/dist/sessions/codex.d.ts +0 -8
  82. package/dist/sessions/codex.d.ts.map +0 -1
  83. package/dist/sessions/codex.js +0 -158
  84. package/dist/sessions/codex.js.map +0 -1
  85. package/dist/sessions/gemini.d.ts +0 -8
  86. package/dist/sessions/gemini.d.ts.map +0 -1
  87. package/dist/sessions/gemini.js +0 -66
  88. package/dist/sessions/gemini.js.map +0 -1
  89. package/dist/sessions/index.d.ts +0 -32
  90. package/dist/sessions/index.d.ts.map +0 -1
  91. package/dist/sessions/index.js +0 -96
  92. package/dist/sessions/index.js.map +0 -1
  93. package/dist/sessions/opencode.d.ts +0 -9
  94. package/dist/sessions/opencode.d.ts.map +0 -1
  95. package/dist/sessions/opencode.js +0 -69
  96. package/dist/sessions/opencode.js.map +0 -1
  97. package/dist/sessions/reports.d.ts +0 -58
  98. package/dist/sessions/reports.d.ts.map +0 -1
  99. package/dist/sessions/reports.js +0 -337
  100. package/dist/sessions/reports.js.map +0 -1
  101. package/src/graph.ts +0 -485
  102. package/src/pricing.ts +0 -309
  103. package/src/sessions/claudecode.ts +0 -119
  104. package/src/sessions/codex.ts +0 -227
  105. package/src/sessions/gemini.ts +0 -108
  106. package/src/sessions/index.ts +0 -126
  107. package/src/sessions/opencode.ts +0 -117
  108. package/src/sessions/reports.ts +0 -475
@@ -131,6 +131,12 @@ export function Footer(props: FooterProps) {
131
131
  enabled={props.enabledSources.has("gemini")}
132
132
  onToggle={props.onSourceToggle}
133
133
  />
134
+ <SourceBadge
135
+ name={isVeryNarrowTerminal() ? "6" : "6:AM"}
136
+ source="amp"
137
+ enabled={props.enabledSources.has("amp")}
138
+ onToggle={props.onSourceToggle}
139
+ />
134
140
  <Show when={!isVeryNarrowTerminal()}>
135
141
  <text dim>|</text>
136
142
  <SortButton
@@ -279,9 +285,8 @@ const SPINNER_INTERVAL = 40;
279
285
 
280
286
  const PHASE_MESSAGES: Record<LoadingPhase, string> = {
281
287
  idle: "Initializing...",
288
+ "parsing-sources": "Scanning session data...",
282
289
  "loading-pricing": "Loading pricing data...",
283
- "syncing-cursor": "Syncing Cursor data...",
284
- "parsing-sources": "Parsing session files...",
285
290
  "finalizing-report": "Finalizing report...",
286
291
  complete: "Complete",
287
292
  };
@@ -31,9 +31,8 @@ function getScannerState(frame: number): SpinnerState {
31
31
 
32
32
  const PHASE_MESSAGES: Record<LoadingPhase, string> = {
33
33
  "idle": "Initializing...",
34
+ "parsing-sources": "Scanning session data...",
34
35
  "loading-pricing": "Loading pricing data...",
35
- "syncing-cursor": "Syncing Cursor data...",
36
- "parsing-sources": "Parsing session files...",
37
36
  "finalizing-report": "Finalizing report...",
38
37
  "complete": "Complete",
39
38
  };
@@ -1,6 +1,6 @@
1
1
  import { For, createMemo, type Accessor } from "solid-js";
2
2
  import type { TUIData, SortType } from "../hooks/useData.js";
3
- import { getModelColor } from "../utils/colors.js";
3
+ import { getModelColor, getSourceDisplayName } from "../utils/colors.js";
4
4
  import { formatTokensCompact, formatCostFull } from "../utils/format.js";
5
5
  import { isNarrow, isVeryNarrow } from "../utils/responsive.js";
6
6
 
@@ -65,7 +65,7 @@ export function ModelView(props: ModelViewProps) {
65
65
  const formattedRows = createMemo(() => {
66
66
  const nameWidth = nameColumnWidths().text;
67
67
  return visibleEntries().map((entry) => {
68
- const sourceLabel = entry.source.charAt(0).toUpperCase() + entry.source.slice(1);
68
+ const sourceLabel = getSourceDisplayName(entry.source);
69
69
  const fullName = `${sourceLabel} ${entry.model}`;
70
70
  let displayName = fullName;
71
71
  if (fullName.length > nameWidth) {
@@ -5,7 +5,8 @@ import type { TUIData, DailyModelBreakdown } from "../types/index.js";
5
5
 
6
6
  const CONFIG_DIR = join(homedir(), ".config", "tokscale");
7
7
  const CACHE_DIR = join(homedir(), ".cache", "tokscale");
8
- const CONFIG_FILE = join(CONFIG_DIR, "tui-settings.json");
8
+ const CONFIG_FILE = join(CONFIG_DIR, "settings.json");
9
+ const LEGACY_CONFIG_FILE = join(CONFIG_DIR, "tui-settings.json");
9
10
  const CACHE_FILE = join(CACHE_DIR, "tui-data-cache.json");
10
11
 
11
12
  const CACHE_STALE_THRESHOLD_MS = 60 * 1000;
@@ -13,17 +14,19 @@ const MIN_AUTO_REFRESH_MS = 30000;
13
14
  const MAX_AUTO_REFRESH_MS = 3600000;
14
15
  const DEFAULT_AUTO_REFRESH_MS = 60000;
15
16
 
16
- interface TUISettings {
17
+ export interface TokscaleSettings {
17
18
  colorPalette: string;
18
19
  autoRefreshEnabled?: boolean;
19
20
  autoRefreshMs?: number;
21
+ includeUnusedModels?: boolean;
20
22
  }
21
23
 
22
- function validateSettings(raw: unknown): TUISettings {
23
- const defaults: TUISettings = {
24
+ function validateSettings(raw: unknown): TokscaleSettings {
25
+ const defaults: TokscaleSettings = {
24
26
  colorPalette: "blue",
25
27
  autoRefreshEnabled: false,
26
- autoRefreshMs: DEFAULT_AUTO_REFRESH_MS
28
+ autoRefreshMs: DEFAULT_AUTO_REFRESH_MS,
29
+ includeUnusedModels: false,
27
30
  };
28
31
 
29
32
  if (!raw || typeof raw !== "object") return defaults;
@@ -38,7 +41,9 @@ function validateSettings(raw: unknown): TUISettings {
38
41
  autoRefreshMs = Math.min(MAX_AUTO_REFRESH_MS, Math.max(MIN_AUTO_REFRESH_MS, obj.autoRefreshMs));
39
42
  }
40
43
 
41
- return { colorPalette, autoRefreshEnabled, autoRefreshMs };
44
+ const includeUnusedModels = typeof obj.includeUnusedModels === "boolean" ? obj.includeUnusedModels : defaults.includeUnusedModels;
45
+
46
+ return { colorPalette, autoRefreshEnabled, autoRefreshMs, includeUnusedModels };
42
47
  }
43
48
 
44
49
  interface CachedTUIData {
@@ -49,18 +54,22 @@ interface CachedTUIData {
49
54
  };
50
55
  }
51
56
 
52
- export function loadSettings(): TUISettings {
57
+ export function loadSettings(): TokscaleSettings {
53
58
  try {
54
59
  if (existsSync(CONFIG_FILE)) {
55
60
  const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
56
61
  return validateSettings(raw);
57
62
  }
63
+ if (existsSync(LEGACY_CONFIG_FILE)) {
64
+ const raw = JSON.parse(readFileSync(LEGACY_CONFIG_FILE, "utf-8"));
65
+ return validateSettings(raw);
66
+ }
58
67
  } catch {
59
68
  }
60
- return { colorPalette: "blue", autoRefreshEnabled: false, autoRefreshMs: DEFAULT_AUTO_REFRESH_MS };
69
+ return { colorPalette: "blue", autoRefreshEnabled: false, autoRefreshMs: DEFAULT_AUTO_REFRESH_MS, includeUnusedModels: false };
61
70
  }
62
71
 
63
- export function saveSettings(updates: Partial<TUISettings>): void {
72
+ export function saveSettings(updates: Partial<TokscaleSettings>): void {
64
73
  try {
65
74
  if (!existsSync(CONFIG_DIR)) {
66
75
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -16,14 +16,13 @@ import type {
16
16
  } from "../types/index.js";
17
17
  import {
18
18
  parseLocalSourcesAsync,
19
- finalizeReportAsync,
20
- finalizeGraphAsync,
19
+ finalizeReportAndGraphAsync,
21
20
  type ParsedMessages,
22
21
  } from "../../native.js";
23
- import { PricingFetcher } from "../../pricing.js";
22
+
24
23
  import { syncCursorCache, loadCursorCredentials } from "../../cursor.js";
25
24
  import { getModelColor } from "../utils/colors.js";
26
- import { loadCachedData, saveCachedData, isCacheStale } from "../config/settings.js";
25
+ import { loadCachedData, saveCachedData, isCacheStale, loadSettings } from "../config/settings.js";
27
26
 
28
27
  export type {
29
28
  SortType,
@@ -144,27 +143,30 @@ function calculateLongestSession(messages: Array<{ sessionId: string; timestamp:
144
143
  return `${totalSeconds}s`;
145
144
  }
146
145
 
147
- async function loadData(enabledSources: Set<SourceType>, dateFilters?: DateFilters): Promise<TUIData> {
146
+ async function loadData(
147
+ enabledSources: Set<SourceType>,
148
+ dateFilters?: DateFilters,
149
+ setPhase?: (phase: LoadingPhase) => void
150
+ ): Promise<TUIData> {
148
151
  const sources = Array.from(enabledSources);
149
152
  const localSources = sources.filter(s => s !== "cursor");
150
153
  const includeCursor = sources.includes("cursor");
151
154
  const { since, until, year } = dateFilters ?? {};
152
155
 
153
- const pricingFetcher = new PricingFetcher();
156
+ setPhase?.("parsing-sources");
154
157
 
155
158
  const phase1Results = await Promise.allSettled([
156
- pricingFetcher.fetchPricing(),
157
159
  includeCursor && loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 }),
158
160
  localSources.length > 0
159
- ? parseLocalSourcesAsync({ sources: localSources as ("opencode" | "claude" | "codex" | "gemini")[], since, until, year })
160
- : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 } as ParsedMessages),
161
+ ? parseLocalSourcesAsync({ sources: localSources as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid")[], since, until, year })
162
+ : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 } as ParsedMessages),
161
163
  ]);
162
164
 
163
- const cursorSync = phase1Results[1].status === "fulfilled"
164
- ? phase1Results[1].value
165
+ const cursorSync = phase1Results[0].status === "fulfilled"
166
+ ? phase1Results[0].value
165
167
  : { synced: false, rows: 0 };
166
- const localMessages = phase1Results[2].status === "fulfilled"
167
- ? phase1Results[2].value
168
+ const localMessages = phase1Results[1].status === "fulfilled"
169
+ ? phase1Results[1].value
168
170
  : null;
169
171
 
170
172
  const emptyMessages: ParsedMessages = {
@@ -173,39 +175,23 @@ async function loadData(enabledSources: Set<SourceType>, dateFilters?: DateFilte
173
175
  claudeCount: 0,
174
176
  codexCount: 0,
175
177
  geminiCount: 0,
178
+ ampCount: 0,
179
+ droidCount: 0,
176
180
  processingTimeMs: 0,
177
181
  };
178
182
 
179
- const phase2Results = await Promise.allSettled([
180
- finalizeReportAsync({
181
- localMessages: localMessages || emptyMessages,
182
- pricing: pricingFetcher.toPricingEntries(),
183
- includeCursor: includeCursor && cursorSync.synced,
184
- since,
185
- until,
186
- year,
187
- }),
188
- finalizeGraphAsync({
189
- localMessages: localMessages || emptyMessages,
190
- pricing: pricingFetcher.toPricingEntries(),
191
- includeCursor: includeCursor && cursorSync.synced,
192
- since,
193
- until,
194
- year,
195
- }),
196
- ]);
197
-
198
- if (phase2Results[0].status === "rejected") {
199
- throw new Error(`Failed to finalize report: ${phase2Results[0].reason}`);
200
- }
201
- if (phase2Results[1].status === "rejected") {
202
- throw new Error(`Failed to finalize graph: ${phase2Results[1].reason}`);
203
- }
204
-
205
- const report = phase2Results[0].value;
206
- const graph = phase2Results[1].value;
207
-
208
- const modelEntries: ModelEntry[] = report.entries.map(e => ({
183
+ setPhase?.("finalizing-report");
184
+ // Single call ensures consistent pricing between report and graph
185
+ const { report, graph } = await finalizeReportAndGraphAsync({
186
+ localMessages: localMessages || emptyMessages,
187
+ includeCursor: includeCursor && cursorSync.synced,
188
+ since,
189
+ until,
190
+ year,
191
+ });
192
+
193
+ const settings = loadSettings();
194
+ const allModelEntries: ModelEntry[] = report.entries.map(e => ({
209
195
  source: e.source,
210
196
  model: e.model,
211
197
  input: e.input,
@@ -216,6 +202,9 @@ async function loadData(enabledSources: Set<SourceType>, dateFilters?: DateFilte
216
202
  total: e.input + e.output + e.cacheWrite + e.cacheRead + e.reasoning,
217
203
  cost: e.cost,
218
204
  }));
205
+ const modelEntries = settings.includeUnusedModels
206
+ ? allModelEntries
207
+ : allModelEntries.filter(e => e.total > 0);
219
208
 
220
209
  const dailyMap = new Map<string, DailyEntry>();
221
210
  for (const contrib of graph.contributions) {
@@ -490,20 +479,20 @@ export function useData(enabledSources: Accessor<Set<SourceType>>, dateFilters?:
490
479
  setData(cachedData);
491
480
  setLoading(false);
492
481
  setIsRefreshing(true);
493
- setLoadingPhase("loading-pricing");
482
+ setLoadingPhase("idle");
494
483
  } else {
495
484
  setLoading(true);
496
- setLoadingPhase("loading-pricing");
485
+ setLoadingPhase("idle");
497
486
  }
498
487
  } else {
499
488
  setIsRefreshing(true);
500
- setLoadingPhase("loading-pricing");
489
+ setLoadingPhase("idle");
501
490
  setForceRefresh(false);
502
491
  }
503
492
 
504
493
  const requestId = currentRequestId;
505
494
  setError(null);
506
- loadData(sources, dateFilters)
495
+ loadData(sources, dateFilters, setLoadingPhase)
507
496
  .then((freshData) => {
508
497
  if (requestId !== currentRequestId) return;
509
498
  setData(freshData);
@@ -2,7 +2,7 @@ import type { ColorPaletteName } from "../config/themes.js";
2
2
 
3
3
  export type TabType = "overview" | "model" | "daily" | "stats";
4
4
  export type SortType = "cost" | "tokens";
5
- export type SourceType = "opencode" | "claude" | "codex" | "cursor" | "gemini";
5
+ export type SourceType = "opencode" | "claude" | "codex" | "cursor" | "gemini" | "amp" | "droid";
6
6
 
7
7
  export type { ColorPaletteName };
8
8
 
@@ -105,9 +105,8 @@ export interface TUISettings {
105
105
 
106
106
  export type LoadingPhase =
107
107
  | "idle"
108
- | "loading-pricing"
109
- | "syncing-cursor"
110
108
  | "parsing-sources"
109
+ | "loading-pricing"
111
110
  | "finalizing-report"
112
111
  | "complete";
113
112
 
@@ -161,7 +160,9 @@ export const SOURCE_LABELS: Record<SourceType, string> = {
161
160
  codex: "CX",
162
161
  cursor: "CR",
163
162
  gemini: "GM",
163
+ amp: "AM",
164
+ droid: "DR",
164
165
  } as const;
165
166
 
166
167
  export const TABS: readonly TabType[] = ["overview", "model", "daily", "stats"] as const;
167
- export const ALL_SOURCES: readonly SourceType[] = ["opencode", "claude", "codex", "cursor", "gemini"] as const;
168
+ export const ALL_SOURCES: readonly SourceType[] = ["opencode", "claude", "codex", "cursor", "gemini", "amp", "droid"] as const;
@@ -58,8 +58,15 @@ export const SOURCE_COLORS: Record<SourceType, string> = {
58
58
  codex: "#3b82f6",
59
59
  cursor: "#a855f7",
60
60
  gemini: "#06b6d4",
61
+ amp: "#EC4899",
62
+ droid: "#10b981",
61
63
  };
62
64
 
63
65
  export function getSourceColor(source: SourceType | string): string {
64
66
  return SOURCE_COLORS[source as SourceType] || "#888888";
65
67
  }
68
+
69
+ export function getSourceDisplayName(source: string): string {
70
+ if (source === "droid") return "Droid";
71
+ return source.charAt(0).toUpperCase() + source.slice(1);
72
+ }
package/src/wrapped.ts CHANGED
@@ -3,13 +3,12 @@ import { Resvg } from "@resvg/resvg-js";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import * as os from "node:os";
6
+ import pc from "picocolors";
6
7
  import {
7
8
  parseLocalSourcesAsync,
8
- finalizeReportAsync,
9
- finalizeGraphAsync,
9
+ finalizeReportAndGraphAsync,
10
10
  type ParsedMessages,
11
11
  } from "./native.js";
12
- import { PricingFetcher } from "./pricing.js";
13
12
  import { syncCursorCache, loadCursorCredentials } from "./cursor.js";
14
13
  import { loadCredentials } from "./credentials.js";
15
14
  import type { SourceType } from "./graph-types.js";
@@ -63,6 +62,7 @@ const SOURCE_DISPLAY_NAMES: Record<string, string> = {
63
62
  codex: "Codex CLI",
64
63
  gemini: "Gemini CLI",
65
64
  cursor: "Cursor IDE",
65
+ amp: "Amp",
66
66
  };
67
67
 
68
68
  const ASSETS_BASE_URL = "https://tokscale.ai/assets/logos";
@@ -92,6 +92,7 @@ const CLIENT_LOGO_URLS: Record<string, string> = {
92
92
  "Codex CLI": `${ASSETS_BASE_URL}/openai.jpg`,
93
93
  "Gemini CLI": `${ASSETS_BASE_URL}/gemini.png`,
94
94
  "Cursor IDE": `${ASSETS_BASE_URL}/cursor.jpg`,
95
+ "Amp": `${ASSETS_BASE_URL}/amp.png`,
95
96
  };
96
97
 
97
98
  const PROVIDER_LOGO_URLS: Record<string, string> = {
@@ -209,28 +210,25 @@ async function ensureFontsLoaded(): Promise<void> {
209
210
 
210
211
  async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
211
212
  const year = options.year || new Date().getFullYear().toString();
212
- const sources = options.sources || ["opencode", "claude", "codex", "gemini", "cursor"];
213
- const localSources = sources.filter(s => s !== "cursor") as ("opencode" | "claude" | "codex" | "gemini")[];
213
+ const sources = options.sources || ["opencode", "claude", "codex", "gemini", "cursor", "amp", "droid"];
214
+ const localSources = sources.filter(s => s !== "cursor") as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid")[];
214
215
  const includeCursor = sources.includes("cursor");
215
216
 
216
217
  const since = `${year}-01-01`;
217
218
  const until = `${year}-12-31`;
218
219
 
219
- const pricingFetcher = new PricingFetcher();
220
-
221
220
  const phase1Results = await Promise.allSettled([
222
- pricingFetcher.fetchPricing(),
223
221
  includeCursor && loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 }),
224
222
  localSources.length > 0
225
- ? parseLocalSourcesAsync({ sources: localSources, since, until, year, forceTypescript: options.includeAgents })
226
- : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 } as ParsedMessages),
223
+ ? parseLocalSourcesAsync({ sources: localSources, since, until, year })
224
+ : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 } as ParsedMessages),
227
225
  ]);
228
226
 
229
- const cursorSync = phase1Results[1].status === "fulfilled"
230
- ? phase1Results[1].value
227
+ const cursorSync = phase1Results[0].status === "fulfilled"
228
+ ? phase1Results[0].value
231
229
  : { synced: false, rows: 0 };
232
- const localMessages = phase1Results[2].status === "fulfilled"
233
- ? phase1Results[2].value
230
+ const localMessages = phase1Results[1].status === "fulfilled"
231
+ ? phase1Results[1].value
234
232
  : null;
235
233
 
236
234
  const emptyMessages: ParsedMessages = {
@@ -239,37 +237,18 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
239
237
  claudeCount: 0,
240
238
  codexCount: 0,
241
239
  geminiCount: 0,
240
+ ampCount: 0,
241
+ droidCount: 0,
242
242
  processingTimeMs: 0,
243
243
  };
244
244
 
245
- const [reportResult, graphResult] = await Promise.allSettled([
246
- finalizeReportAsync({
247
- localMessages: localMessages || emptyMessages,
248
- pricing: pricingFetcher.toPricingEntries(),
249
- includeCursor: includeCursor && cursorSync.synced,
250
- since,
251
- until,
252
- year,
253
- }),
254
- finalizeGraphAsync({
255
- localMessages: localMessages || emptyMessages,
256
- pricing: pricingFetcher.toPricingEntries(),
257
- includeCursor: includeCursor && cursorSync.synced,
258
- since,
259
- until,
260
- year,
261
- }),
262
- ]);
263
-
264
- if (reportResult.status === "rejected") {
265
- throw new Error(`Failed to generate report: ${reportResult.reason}`);
266
- }
267
- if (graphResult.status === "rejected") {
268
- throw new Error(`Failed to generate graph: ${graphResult.reason}`);
269
- }
270
-
271
- const report = reportResult.value;
272
- const graph = graphResult.value;
245
+ const { report, graph } = await finalizeReportAndGraphAsync({
246
+ localMessages: localMessages || emptyMessages,
247
+ includeCursor: includeCursor && cursorSync.synced,
248
+ since,
249
+ until,
250
+ year,
251
+ });
273
252
 
274
253
  const modelMap = new Map<string, { cost: number; tokens: number }>();
275
254
  for (const entry of report.entries) {
@@ -299,10 +278,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
299
278
  .slice(0, 3);
300
279
 
301
280
  let topAgents: Array<{ name: string; cost: number; tokens: number; messages: number }> | undefined;
302
- if (options.includeAgents && localMessages) {
303
- const pricingEntries = pricingFetcher.toPricingEntries();
304
- const pricingMap = new Map(pricingEntries.map(p => [p.modelId, p.pricing]));
305
-
281
+ if (options.includeAgents !== false && localMessages) {
306
282
  const agentMap = new Map<string, { cost: number; tokens: number; messages: number }>();
307
283
  for (const msg of localMessages.messages) {
308
284
  if (msg.source === "opencode" && msg.agent) {
@@ -310,17 +286,9 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
310
286
  const existing = agentMap.get(normalizedAgent) || { cost: 0, tokens: 0, messages: 0 };
311
287
 
312
288
  const msgTokens = msg.input + msg.output + msg.cacheRead + msg.cacheWrite + msg.reasoning;
313
- const pricing = pricingMap.get(msg.modelId);
314
- let msgCost = 0;
315
- if (pricing) {
316
- msgCost = (msg.input * pricing.inputCostPerToken) +
317
- (msg.output * pricing.outputCostPerToken) +
318
- (msg.cacheRead * (pricing.cacheReadInputTokenCost || 0)) +
319
- (msg.cacheWrite * (pricing.cacheCreationInputTokenCost || 0));
320
- }
321
289
 
322
290
  agentMap.set(normalizedAgent, {
323
- cost: existing.cost + msgCost,
291
+ cost: existing.cost,
324
292
  tokens: existing.tokens + msgTokens,
325
293
  messages: existing.messages + 1,
326
294
  });
@@ -330,7 +298,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
330
298
  let agentsList = Array.from(agentMap.entries())
331
299
  .map(([name, data]) => ({ name, ...data }));
332
300
 
333
- if (options.pinSisyphus) {
301
+ if (options.pinSisyphus !== false) {
334
302
  const pinned = agentsList.filter(a => PINNED_AGENTS.includes(a.name));
335
303
  const unpinned = agentsList.filter(a => !PINNED_AGENTS.includes(a.name));
336
304
 
@@ -718,7 +686,7 @@ async function generateWrappedImage(data: WrappedData, options: { short?: boolea
718
686
  }
719
687
  yPos += 40 * SCALE;
720
688
 
721
- if (options.includeAgents) {
689
+ if (options.includeAgents !== false) {
722
690
  ctx.fillStyle = COLORS.textSecondary;
723
691
  ctx.font = `${20 * SCALE}px Figtree, sans-serif`;
724
692
  ctx.fillText("Top OpenCode Agents", PADDING, yPos);
@@ -731,7 +699,7 @@ async function generateWrappedImage(data: WrappedData, options: { short?: boolea
731
699
  for (let i = 0; i < agents.length; i++) {
732
700
  const agent = agents[i];
733
701
  const isSisyphusAgent = PINNED_AGENTS.includes(agent.name);
734
- const showWithDash = options.pinSisyphus && isSisyphusAgent;
702
+ const showWithDash = options.pinSisyphus !== false && isSisyphusAgent;
735
703
 
736
704
  ctx.fillStyle = showWithDash ? SISYPHUS_COLOR : COLORS.textPrimary;
737
705
  ctx.font = `bold ${32 * SCALE}px Figtree, sans-serif`;
@@ -834,9 +802,21 @@ async function generateWrappedImage(data: WrappedData, options: { short?: boolea
834
802
 
835
803
  export async function generateWrapped(options: WrappedOptions): Promise<string> {
836
804
  const data = await loadWrappedData(options);
805
+
806
+ const agentsRequested = options.includeAgents !== false;
807
+ const hasAgentData = !!data.topAgents?.length;
808
+ const opencodeEnabled = !options.sources || options.sources.includes("opencode");
809
+ let effectiveIncludeAgents = agentsRequested && hasAgentData;
810
+
811
+ if (agentsRequested && opencodeEnabled && !hasAgentData) {
812
+ console.warn(pc.yellow(`\n ⚠ No OpenCode agent data found for ${data.year}.`));
813
+ console.warn(pc.gray(" Falling back to clients view."));
814
+ console.warn(pc.gray(" Use --clients to always show clients view.\n"));
815
+ }
816
+
837
817
  const imageBuffer = await generateWrappedImage(data, {
838
818
  short: options.short,
839
- includeAgents: options.includeAgents,
819
+ includeAgents: effectiveIncludeAgents,
840
820
  pinSisyphus: options.pinSisyphus,
841
821
  });
842
822
 
package/dist/graph.d.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * Graph data generation module
3
- * Aggregates token usage data by date for contribution graph visualization
4
- *
5
- * Key design: intensity is calculated based on COST ($), not tokens
6
- *
7
- * This module supports two implementations:
8
- * - Native Rust (fast, ~10x faster) - used when available
9
- * - Pure TypeScript (fallback) - always available
10
- */
11
- import type { TokenContributionData, GraphOptions } from "./graph-types.js";
12
- /**
13
- * Check if native implementation is available
14
- */
15
- export declare function isNativeAvailable(): boolean;
16
- /**
17
- * Generate contribution graph data from all sources
18
- *
19
- * Uses native Rust implementation if available, falls back to TypeScript.
20
- * Set `options.forceTypescript = true` to skip native module.
21
- */
22
- export declare function generateGraphData(options?: GraphOptions & {
23
- forceTypescript?: boolean;
24
- }): Promise<TokenContributionData>;
25
- /**
26
- * Pure TypeScript implementation of graph data generation
27
- */
28
- export declare function generateGraphDataTS(options?: GraphOptions): Promise<TokenContributionData>;
29
- //# sourceMappingURL=graph.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH,OAAO,KAAK,EACV,qBAAqB,EAIrB,YAAY,EAGb,MAAM,kBAAkB,CAAC;AAY1B;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,YAAY,GAAG;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GACzD,OAAO,CAAC,qBAAqB,CAAC,CAahC;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CAwChC"}