@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.
- package/dist/cli.js +214 -91
- package/dist/cli.js.map +1 -1
- package/dist/graph-types.d.ts +1 -1
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/native-runner.d.ts +1 -2
- package/dist/native-runner.d.ts.map +1 -1
- package/dist/native-runner.js +11 -39
- package/dist/native-runner.js.map +1 -1
- package/dist/native.d.ts +9 -30
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +31 -138
- package/dist/native.js.map +1 -1
- package/dist/sessions/types.d.ts +1 -1
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/submit.d.ts +2 -0
- package/dist/submit.d.ts.map +1 -1
- package/dist/submit.js +32 -16
- package/dist/submit.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +14 -7
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/DailyView.d.ts.map +1 -1
- package/dist/tui/components/DailyView.js +25 -8
- package/dist/tui/components/DailyView.js.map +1 -1
- package/dist/tui/components/DateBreakdownPanel.js +2 -2
- package/dist/tui/components/DateBreakdownPanel.js.map +1 -1
- package/dist/tui/components/Footer.d.ts.map +1 -1
- package/dist/tui/components/Footer.js +2 -3
- package/dist/tui/components/Footer.js.map +1 -1
- package/dist/tui/components/LoadingSpinner.d.ts.map +1 -1
- package/dist/tui/components/LoadingSpinner.js +1 -2
- package/dist/tui/components/LoadingSpinner.js.map +1 -1
- package/dist/tui/components/ModelView.js +2 -2
- package/dist/tui/components/ModelView.js.map +1 -1
- package/dist/tui/config/settings.d.ts +4 -4
- package/dist/tui/config/settings.d.ts.map +1 -1
- package/dist/tui/config/settings.js +11 -4
- package/dist/tui/config/settings.js.map +1 -1
- package/dist/tui/hooks/useData.d.ts.map +1 -1
- package/dist/tui/hooks/useData.js +29 -42
- package/dist/tui/hooks/useData.js.map +1 -1
- package/dist/tui/types/index.d.ts +2 -2
- package/dist/tui/types/index.d.ts.map +1 -1
- package/dist/tui/types/index.js +3 -1
- package/dist/tui/types/index.js.map +1 -1
- package/dist/tui/utils/colors.d.ts +1 -0
- package/dist/tui/utils/colors.d.ts.map +1 -1
- package/dist/tui/utils/colors.js +7 -0
- package/dist/tui/utils/colors.js.map +1 -1
- package/dist/wrapped.d.ts.map +1 -1
- package/dist/wrapped.js +20 -48
- package/dist/wrapped.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +232 -97
- package/src/graph-types.ts +1 -1
- package/src/native-runner.js +4 -0
- package/src/native-runner.ts +12 -42
- package/src/native.ts +47 -207
- package/src/sessions/types.ts +1 -1
- package/src/submit.ts +36 -22
- package/src/tui/App.tsx +10 -7
- package/src/tui/components/DailyView.tsx +29 -11
- package/src/tui/components/DateBreakdownPanel.tsx +2 -2
- package/src/tui/components/Footer.tsx +7 -2
- package/src/tui/components/LoadingSpinner.tsx +1 -2
- package/src/tui/components/ModelView.tsx +2 -2
- package/src/tui/config/settings.ts +18 -9
- package/src/tui/hooks/useData.ts +36 -47
- package/src/tui/types/index.ts +5 -4
- package/src/tui/utils/colors.ts +7 -0
- package/src/wrapped.ts +21 -54
- package/dist/graph.d.ts +0 -29
- package/dist/graph.d.ts.map +0 -1
- package/dist/graph.js +0 -383
- package/dist/graph.js.map +0 -1
- package/dist/pricing.d.ts +0 -58
- package/dist/pricing.d.ts.map +0 -1
- package/dist/pricing.js +0 -232
- package/dist/pricing.js.map +0 -1
- package/dist/sessions/claudecode.d.ts +0 -8
- package/dist/sessions/claudecode.d.ts.map +0 -1
- package/dist/sessions/claudecode.js +0 -84
- package/dist/sessions/claudecode.js.map +0 -1
- package/dist/sessions/codex.d.ts +0 -8
- package/dist/sessions/codex.d.ts.map +0 -1
- package/dist/sessions/codex.js +0 -158
- package/dist/sessions/codex.js.map +0 -1
- package/dist/sessions/gemini.d.ts +0 -8
- package/dist/sessions/gemini.d.ts.map +0 -1
- package/dist/sessions/gemini.js +0 -66
- package/dist/sessions/gemini.js.map +0 -1
- package/dist/sessions/index.d.ts +0 -32
- package/dist/sessions/index.d.ts.map +0 -1
- package/dist/sessions/index.js +0 -96
- package/dist/sessions/index.js.map +0 -1
- package/dist/sessions/opencode.d.ts +0 -9
- package/dist/sessions/opencode.d.ts.map +0 -1
- package/dist/sessions/opencode.js +0 -69
- package/dist/sessions/opencode.js.map +0 -1
- package/dist/sessions/reports.d.ts +0 -58
- package/dist/sessions/reports.d.ts.map +0 -1
- package/dist/sessions/reports.js +0 -337
- package/dist/sessions/reports.js.map +0 -1
- package/src/graph.ts +0 -485
- package/src/pricing.ts +0 -309
- package/src/sessions/claudecode.ts +0 -119
- package/src/sessions/codex.ts +0 -227
- package/src/sessions/gemini.ts +0 -108
- package/src/sessions/index.ts +0 -126
- package/src/sessions/opencode.ts +0 -117
- 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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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)}${
|
|
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={
|
|
90
|
-
{(
|
|
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
|
-
{
|
|
118
|
+
{renderRowData(row)}
|
|
101
119
|
</text>
|
|
102
120
|
<text
|
|
103
121
|
fg="green"
|
|
104
122
|
bg={rowBg()}
|
|
105
123
|
>
|
|
106
|
-
{
|
|
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
|
|
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 =
|
|
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, "
|
|
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
|
|
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):
|
|
23
|
-
const defaults:
|
|
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
|
-
|
|
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():
|
|
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<
|
|
72
|
+
export function saveSettings(updates: Partial<TokscaleSettings>): void {
|
|
64
73
|
try {
|
|
65
74
|
if (!existsSync(CONFIG_DIR)) {
|
|
66
75
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
package/src/tui/hooks/useData.ts
CHANGED
|
@@ -16,14 +16,13 @@ import type {
|
|
|
16
16
|
} from "../types/index.js";
|
|
17
17
|
import {
|
|
18
18
|
parseLocalSourcesAsync,
|
|
19
|
-
|
|
20
|
-
finalizeGraphAsync,
|
|
19
|
+
finalizeReportAndGraphAsync,
|
|
21
20
|
type ParsedMessages,
|
|
22
21
|
} from "../../native.js";
|
|
23
|
-
|
|
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(
|
|
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
|
-
|
|
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[
|
|
164
|
-
? phase1Results[
|
|
165
|
+
const cursorSync = phase1Results[0].status === "fulfilled"
|
|
166
|
+
? phase1Results[0].value
|
|
165
167
|
: { synced: false, rows: 0 };
|
|
166
|
-
const localMessages = phase1Results[
|
|
167
|
-
? phase1Results[
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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("
|
|
482
|
+
setLoadingPhase("idle");
|
|
494
483
|
} else {
|
|
495
484
|
setLoading(true);
|
|
496
|
-
setLoadingPhase("
|
|
485
|
+
setLoadingPhase("idle");
|
|
497
486
|
}
|
|
498
487
|
} else {
|
|
499
488
|
setIsRefreshing(true);
|
|
500
|
-
setLoadingPhase("
|
|
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);
|
package/src/tui/types/index.ts
CHANGED
|
@@ -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;
|
package/src/tui/utils/colors.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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[
|
|
231
|
-
? phase1Results[
|
|
227
|
+
const cursorSync = phase1Results[0].status === "fulfilled"
|
|
228
|
+
? phase1Results[0].value
|
|
232
229
|
: { synced: false, rows: 0 };
|
|
233
|
-
const localMessages = phase1Results[
|
|
234
|
-
? phase1Results[
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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
|
package/dist/graph.d.ts.map
DELETED
|
@@ -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"}
|