@tokscale/cli 1.0.17 → 1.0.19

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 (111) hide show
  1. package/dist/cli.js +214 -91
  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.d.ts +1 -2
  6. package/dist/native-runner.d.ts.map +1 -1
  7. package/dist/native-runner.js +11 -39
  8. package/dist/native-runner.js.map +1 -1
  9. package/dist/native.d.ts +9 -30
  10. package/dist/native.d.ts.map +1 -1
  11. package/dist/native.js +31 -138
  12. package/dist/native.js.map +1 -1
  13. package/dist/sessions/types.d.ts +1 -1
  14. package/dist/sessions/types.d.ts.map +1 -1
  15. package/dist/submit.d.ts +2 -0
  16. package/dist/submit.d.ts.map +1 -1
  17. package/dist/submit.js +32 -16
  18. package/dist/submit.js.map +1 -1
  19. package/dist/tui/App.d.ts.map +1 -1
  20. package/dist/tui/App.js +14 -7
  21. package/dist/tui/App.js.map +1 -1
  22. package/dist/tui/components/DailyView.d.ts.map +1 -1
  23. package/dist/tui/components/DailyView.js +25 -8
  24. package/dist/tui/components/DailyView.js.map +1 -1
  25. package/dist/tui/components/DateBreakdownPanel.js +2 -2
  26. package/dist/tui/components/DateBreakdownPanel.js.map +1 -1
  27. package/dist/tui/components/Footer.d.ts.map +1 -1
  28. package/dist/tui/components/Footer.js +2 -3
  29. package/dist/tui/components/Footer.js.map +1 -1
  30. package/dist/tui/components/LoadingSpinner.d.ts.map +1 -1
  31. package/dist/tui/components/LoadingSpinner.js +1 -2
  32. package/dist/tui/components/LoadingSpinner.js.map +1 -1
  33. package/dist/tui/components/ModelView.js +2 -2
  34. package/dist/tui/components/ModelView.js.map +1 -1
  35. package/dist/tui/config/settings.d.ts +4 -4
  36. package/dist/tui/config/settings.d.ts.map +1 -1
  37. package/dist/tui/config/settings.js +11 -4
  38. package/dist/tui/config/settings.js.map +1 -1
  39. package/dist/tui/hooks/useData.d.ts.map +1 -1
  40. package/dist/tui/hooks/useData.js +29 -42
  41. package/dist/tui/hooks/useData.js.map +1 -1
  42. package/dist/tui/types/index.d.ts +2 -2
  43. package/dist/tui/types/index.d.ts.map +1 -1
  44. package/dist/tui/types/index.js +3 -1
  45. package/dist/tui/types/index.js.map +1 -1
  46. package/dist/tui/utils/colors.d.ts +1 -0
  47. package/dist/tui/utils/colors.d.ts.map +1 -1
  48. package/dist/tui/utils/colors.js +7 -0
  49. package/dist/tui/utils/colors.js.map +1 -1
  50. package/dist/wrapped.d.ts.map +1 -1
  51. package/dist/wrapped.js +20 -48
  52. package/dist/wrapped.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/cli.ts +232 -97
  55. package/src/graph-types.ts +1 -1
  56. package/src/native-runner.js +4 -0
  57. package/src/native-runner.ts +12 -42
  58. package/src/native.ts +47 -207
  59. package/src/sessions/types.ts +1 -1
  60. package/src/submit.ts +36 -22
  61. package/src/tui/App.tsx +10 -7
  62. package/src/tui/components/DailyView.tsx +29 -11
  63. package/src/tui/components/DateBreakdownPanel.tsx +2 -2
  64. package/src/tui/components/Footer.tsx +7 -2
  65. package/src/tui/components/LoadingSpinner.tsx +1 -2
  66. package/src/tui/components/ModelView.tsx +2 -2
  67. package/src/tui/config/settings.ts +18 -9
  68. package/src/tui/hooks/useData.ts +36 -47
  69. package/src/tui/types/index.ts +5 -4
  70. package/src/tui/utils/colors.ts +7 -0
  71. package/src/wrapped.ts +21 -54
  72. package/dist/graph.d.ts +0 -29
  73. package/dist/graph.d.ts.map +0 -1
  74. package/dist/graph.js +0 -383
  75. package/dist/graph.js.map +0 -1
  76. package/dist/pricing.d.ts +0 -58
  77. package/dist/pricing.d.ts.map +0 -1
  78. package/dist/pricing.js +0 -232
  79. package/dist/pricing.js.map +0 -1
  80. package/dist/sessions/claudecode.d.ts +0 -8
  81. package/dist/sessions/claudecode.d.ts.map +0 -1
  82. package/dist/sessions/claudecode.js +0 -84
  83. package/dist/sessions/claudecode.js.map +0 -1
  84. package/dist/sessions/codex.d.ts +0 -8
  85. package/dist/sessions/codex.d.ts.map +0 -1
  86. package/dist/sessions/codex.js +0 -158
  87. package/dist/sessions/codex.js.map +0 -1
  88. package/dist/sessions/gemini.d.ts +0 -8
  89. package/dist/sessions/gemini.d.ts.map +0 -1
  90. package/dist/sessions/gemini.js +0 -66
  91. package/dist/sessions/gemini.js.map +0 -1
  92. package/dist/sessions/index.d.ts +0 -32
  93. package/dist/sessions/index.d.ts.map +0 -1
  94. package/dist/sessions/index.js +0 -96
  95. package/dist/sessions/index.js.map +0 -1
  96. package/dist/sessions/opencode.d.ts +0 -9
  97. package/dist/sessions/opencode.d.ts.map +0 -1
  98. package/dist/sessions/opencode.js +0 -69
  99. package/dist/sessions/opencode.js.map +0 -1
  100. package/dist/sessions/reports.d.ts +0 -58
  101. package/dist/sessions/reports.d.ts.map +0 -1
  102. package/dist/sessions/reports.js +0 -337
  103. package/dist/sessions/reports.js.map +0 -1
  104. package/src/graph.ts +0 -485
  105. package/src/pricing.ts +0 -309
  106. package/src/sessions/claudecode.ts +0 -119
  107. package/src/sessions/codex.ts +0 -227
  108. package/src/sessions/gemini.ts +0 -108
  109. package/src/sessions/index.ts +0 -126
  110. package/src/sessions/opencode.ts +0 -117
  111. package/src/sessions/reports.ts +0 -475
package/src/tui/App.tsx CHANGED
@@ -162,8 +162,12 @@ export function App(props: AppProps) {
162
162
  };
163
163
 
164
164
  const handleSortChange = (sort: SortType) => {
165
- setSortBy(sort);
166
- setSortDesc(true);
165
+ if (sortBy() === sort) {
166
+ setSortDesc(!sortDesc());
167
+ } else {
168
+ setSortBy(sort);
169
+ setSortDesc(true);
170
+ }
167
171
  };
168
172
 
169
173
  useKeyboard((key) => {
@@ -216,8 +220,7 @@ export function App(props: AppProps) {
216
220
  }
217
221
 
218
222
  if (key.name === "c" && !key.meta && !key.ctrl) {
219
- setSortBy("cost");
220
- setSortDesc(true);
223
+ handleSortChange("cost");
221
224
  return;
222
225
  }
223
226
 
@@ -263,8 +266,7 @@ export function App(props: AppProps) {
263
266
  return;
264
267
  }
265
268
  if (key.name === "t") {
266
- setSortBy("tokens");
267
- setSortDesc(true);
269
+ handleSortChange("tokens");
268
270
  return;
269
271
  }
270
272
 
@@ -278,6 +280,7 @@ export function App(props: AppProps) {
278
280
  if (key.name === "3") { handleSourceToggle("codex"); return; }
279
281
  if (key.name === "4") { handleSourceToggle("cursor"); return; }
280
282
  if (key.name === "5") { handleSourceToggle("gemini"); return; }
283
+ if (key.name === "6") { handleSourceToggle("amp"); return; }
281
284
 
282
285
  if (key.name === "up") {
283
286
  if (activeTab() === "overview") {
@@ -342,7 +345,7 @@ export function App(props: AppProps) {
342
345
  };
343
346
 
344
347
  return (
345
- <box flexDirection="column" width={columns()} height={rows()}>
348
+ <box flexDirection="column" width={columns()} height={rows()} backgroundColor="black">
346
349
  <Header activeTab={activeTab()} onTabClick={handleTabClick} width={columns()} />
347
350
 
348
351
  <box flexDirection="column" flexGrow={1} paddingX={1}>
@@ -55,7 +55,26 @@ export function DailyView(props: DailyViewProps) {
55
55
  });
56
56
  });
57
57
 
58
- const visibleEntries = createMemo(() => sortedEntries().slice(0, props.height - 3));
58
+ const visibleEntries = createMemo(() => {
59
+ const maxRows = Math.max(props.height - 3, 0);
60
+ return sortedEntries().slice(0, maxRows);
61
+ });
62
+
63
+ const formattedRows = createMemo(() => {
64
+ const dateColWidth = dateColumnWidths().column;
65
+ const narrow = isNarrowTerminal();
66
+ return visibleEntries().map((entry) => ({
67
+ entry,
68
+ dateColWidth,
69
+ narrow,
70
+ input: formatTokensCompact(entry.input),
71
+ output: formatTokensCompact(entry.output),
72
+ cacheRead: formatTokensCompact(entry.cacheRead),
73
+ cacheWrite: formatTokensCompact(entry.cacheWrite),
74
+ total: formatTokensCompact(entry.total),
75
+ cost: formatCostFull(entry.cost),
76
+ }));
77
+ });
59
78
 
60
79
  const sortArrow = () => (props.sortDesc ? "▼" : "▲");
61
80
  const dateHeader = () => "Date";
@@ -70,12 +89,11 @@ export function DailyView(props: DailyViewProps) {
70
89
  return `${(" " + dateHeader()).padEnd(dateColWidth)}${"Input".padStart(INPUT_COL_WIDTH)}${"Output".padStart(OUTPUT_COL_WIDTH)}${"C.Read".padStart(CACHE_READ_COL_WIDTH)}${"C.Write".padStart(CACHE_WRITE_COL_WIDTH)}${totalHeader().padStart(TOTAL_COL_WIDTH)}${costHeader().padStart(COST_COL_WIDTH)}`;
71
90
  };
72
91
 
73
- const renderRow = (entry: typeof visibleEntries extends () => (infer T)[] ? T : never) => {
74
- const dateColWidth = dateColumnWidths().column;
75
- if (isNarrowTerminal()) {
76
- return `${entry.date.padEnd(dateColWidth)}${formatTokensCompact(entry.total).padStart(TOTAL_COL_WIDTH)}`;
92
+ const renderRowData = (row: typeof formattedRows extends () => (infer T)[] ? T : never) => {
93
+ if (row.narrow) {
94
+ return `${row.entry.date.padEnd(row.dateColWidth)}${row.total.padStart(TOTAL_COL_WIDTH)}`;
77
95
  }
78
- return `${entry.date.padEnd(dateColWidth)}${formatTokensCompact(entry.input).padStart(INPUT_COL_WIDTH)}${formatTokensCompact(entry.output).padStart(OUTPUT_COL_WIDTH)}${formatTokensCompact(entry.cacheRead).padStart(CACHE_READ_COL_WIDTH)}${formatTokensCompact(entry.cacheWrite).padStart(CACHE_WRITE_COL_WIDTH)}${formatTokensCompact(entry.total).padStart(TOTAL_COL_WIDTH)}`;
96
+ return `${row.entry.date.padEnd(row.dateColWidth)}${row.input.padStart(INPUT_COL_WIDTH)}${row.output.padStart(OUTPUT_COL_WIDTH)}${row.cacheRead.padStart(CACHE_READ_COL_WIDTH)}${row.cacheWrite.padStart(CACHE_WRITE_COL_WIDTH)}${row.total.padStart(TOTAL_COL_WIDTH)}`;
79
97
  };
80
98
 
81
99
  return (
@@ -86,24 +104,24 @@ export function DailyView(props: DailyViewProps) {
86
104
  </text>
87
105
  </box>
88
106
 
89
- <For each={visibleEntries()}>
90
- {(entry, i) => {
107
+ <For each={formattedRows()}>
108
+ {(row, i) => {
91
109
  const isActive = createMemo(() => i() === props.selectedIndex());
92
110
  const rowBg = createMemo(() => isActive() ? "blue" : (i() % 2 === 1 ? STRIPE_BG : undefined));
93
-
111
+
94
112
  return (
95
113
  <box flexDirection="row">
96
114
  <text
97
115
  bg={rowBg()}
98
116
  fg={isActive() ? "white" : undefined}
99
117
  >
100
- {renderRow(entry)}
118
+ {renderRowData(row)}
101
119
  </text>
102
120
  <text
103
121
  fg="green"
104
122
  bg={rowBg()}
105
123
  >
106
- {formatCostFull(entry.cost).padStart(COST_COL_WIDTH)}
124
+ {row.cost.padStart(COST_COL_WIDTH)}
107
125
  </text>
108
126
  </box>
109
127
  );
@@ -1,6 +1,6 @@
1
1
  import { For, createMemo } from "solid-js";
2
2
  import type { DailyModelBreakdown } from "../types/index.js";
3
- import { getSourceColor } from "../utils/colors.js";
3
+ import { getSourceColor, getSourceDisplayName } from "../utils/colors.js";
4
4
  import { formatTokens, formatCost } from "../utils/format.js";
5
5
  import { ModelRow } from "./ModelRow.js";
6
6
 
@@ -48,7 +48,7 @@ export function DateBreakdownPanel(props: DateBreakdownPanelProps) {
48
48
  {([source, models]) => (
49
49
  <box flexDirection="column">
50
50
  <box flexDirection="row" gap={1}>
51
- <text fg={getSourceColor(source)} bold>{`● ${source.toUpperCase()}`}</text>
51
+ <text fg={getSourceColor(source)} bold>{`● ${getSourceDisplayName(source)}`}</text>
52
52
  <text dim>{`(${models.length} model${models.length > 1 ? "s" : ""})`}</text>
53
53
  </box>
54
54
  <For each={models}>
@@ -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
@@ -6,11 +6,9 @@ import * as os from "node:os";
6
6
  import pc from "picocolors";
7
7
  import {
8
8
  parseLocalSourcesAsync,
9
- finalizeReportAsync,
10
- finalizeGraphAsync,
9
+ finalizeReportAndGraphAsync,
11
10
  type ParsedMessages,
12
11
  } from "./native.js";
13
- import { PricingFetcher } from "./pricing.js";
14
12
  import { syncCursorCache, loadCursorCredentials } from "./cursor.js";
15
13
  import { loadCredentials } from "./credentials.js";
16
14
  import type { SourceType } from "./graph-types.js";
@@ -64,6 +62,7 @@ const SOURCE_DISPLAY_NAMES: Record<string, string> = {
64
62
  codex: "Codex CLI",
65
63
  gemini: "Gemini CLI",
66
64
  cursor: "Cursor IDE",
65
+ amp: "Amp",
67
66
  };
68
67
 
69
68
  const ASSETS_BASE_URL = "https://tokscale.ai/assets/logos";
@@ -93,6 +92,7 @@ const CLIENT_LOGO_URLS: Record<string, string> = {
93
92
  "Codex CLI": `${ASSETS_BASE_URL}/openai.jpg`,
94
93
  "Gemini CLI": `${ASSETS_BASE_URL}/gemini.png`,
95
94
  "Cursor IDE": `${ASSETS_BASE_URL}/cursor.jpg`,
95
+ "Amp": `${ASSETS_BASE_URL}/amp.png`,
96
96
  };
97
97
 
98
98
  const PROVIDER_LOGO_URLS: Record<string, string> = {
@@ -210,28 +210,25 @@ async function ensureFontsLoaded(): Promise<void> {
210
210
 
211
211
  async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
212
212
  const year = options.year || new Date().getFullYear().toString();
213
- const sources = options.sources || ["opencode", "claude", "codex", "gemini", "cursor"];
214
- 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")[];
215
215
  const includeCursor = sources.includes("cursor");
216
216
 
217
217
  const since = `${year}-01-01`;
218
218
  const until = `${year}-12-31`;
219
219
 
220
- const pricingFetcher = new PricingFetcher();
221
-
222
220
  const phase1Results = await Promise.allSettled([
223
- pricingFetcher.fetchPricing(),
224
221
  includeCursor && loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 }),
225
222
  localSources.length > 0
226
- ? parseLocalSourcesAsync({ sources: localSources, since, until, year, forceTypescript: options.includeAgents !== false })
227
- : 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),
228
225
  ]);
229
226
 
230
- const cursorSync = phase1Results[1].status === "fulfilled"
231
- ? phase1Results[1].value
227
+ const cursorSync = phase1Results[0].status === "fulfilled"
228
+ ? phase1Results[0].value
232
229
  : { synced: false, rows: 0 };
233
- const localMessages = phase1Results[2].status === "fulfilled"
234
- ? phase1Results[2].value
230
+ const localMessages = phase1Results[1].status === "fulfilled"
231
+ ? phase1Results[1].value
235
232
  : null;
236
233
 
237
234
  const emptyMessages: ParsedMessages = {
@@ -240,37 +237,18 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
240
237
  claudeCount: 0,
241
238
  codexCount: 0,
242
239
  geminiCount: 0,
240
+ ampCount: 0,
241
+ droidCount: 0,
243
242
  processingTimeMs: 0,
244
243
  };
245
244
 
246
- const [reportResult, graphResult] = await Promise.allSettled([
247
- finalizeReportAsync({
248
- localMessages: localMessages || emptyMessages,
249
- pricing: pricingFetcher.toPricingEntries(),
250
- includeCursor: includeCursor && cursorSync.synced,
251
- since,
252
- until,
253
- year,
254
- }),
255
- finalizeGraphAsync({
256
- localMessages: localMessages || emptyMessages,
257
- pricing: pricingFetcher.toPricingEntries(),
258
- includeCursor: includeCursor && cursorSync.synced,
259
- since,
260
- until,
261
- year,
262
- }),
263
- ]);
264
-
265
- if (reportResult.status === "rejected") {
266
- throw new Error(`Failed to generate report: ${reportResult.reason}`);
267
- }
268
- if (graphResult.status === "rejected") {
269
- throw new Error(`Failed to generate graph: ${graphResult.reason}`);
270
- }
271
-
272
- const report = reportResult.value;
273
- 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
+ });
274
252
 
275
253
  const modelMap = new Map<string, { cost: number; tokens: number }>();
276
254
  for (const entry of report.entries) {
@@ -301,9 +279,6 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
301
279
 
302
280
  let topAgents: Array<{ name: string; cost: number; tokens: number; messages: number }> | undefined;
303
281
  if (options.includeAgents !== false && localMessages) {
304
- const pricingEntries = pricingFetcher.toPricingEntries();
305
- const pricingMap = new Map(pricingEntries.map(p => [p.modelId, p.pricing]));
306
-
307
282
  const agentMap = new Map<string, { cost: number; tokens: number; messages: number }>();
308
283
  for (const msg of localMessages.messages) {
309
284
  if (msg.source === "opencode" && msg.agent) {
@@ -311,17 +286,9 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
311
286
  const existing = agentMap.get(normalizedAgent) || { cost: 0, tokens: 0, messages: 0 };
312
287
 
313
288
  const msgTokens = msg.input + msg.output + msg.cacheRead + msg.cacheWrite + msg.reasoning;
314
- const pricing = pricingMap.get(msg.modelId);
315
- let msgCost = 0;
316
- if (pricing) {
317
- msgCost = (msg.input * pricing.inputCostPerToken) +
318
- (msg.output * pricing.outputCostPerToken) +
319
- (msg.cacheRead * (pricing.cacheReadInputTokenCost || 0)) +
320
- (msg.cacheWrite * (pricing.cacheCreationInputTokenCost || 0));
321
- }
322
289
 
323
290
  agentMap.set(normalizedAgent, {
324
- cost: existing.cost + msgCost,
291
+ cost: existing.cost,
325
292
  tokens: existing.tokens + msgTokens,
326
293
  messages: existing.messages + 1,
327
294
  });
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"}