@tokscale/cli 1.4.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +128 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +19 -26
  6. package/dist/auth.d.ts +0 -17
  7. package/dist/auth.d.ts.map +0 -1
  8. package/dist/auth.js +0 -162
  9. package/dist/auth.js.map +0 -1
  10. package/dist/cli.d.ts +0 -9
  11. package/dist/cli.d.ts.map +0 -1
  12. package/dist/cli.js +0 -1550
  13. package/dist/cli.js.map +0 -1
  14. package/dist/credentials.d.ts +0 -50
  15. package/dist/credentials.d.ts.map +0 -1
  16. package/dist/credentials.js +0 -151
  17. package/dist/credentials.js.map +0 -1
  18. package/dist/cursor.d.ts +0 -167
  19. package/dist/cursor.d.ts.map +0 -1
  20. package/dist/cursor.js +0 -906
  21. package/dist/cursor.js.map +0 -1
  22. package/dist/date-utils.d.ts +0 -10
  23. package/dist/date-utils.d.ts.map +0 -1
  24. package/dist/date-utils.js +0 -47
  25. package/dist/date-utils.js.map +0 -1
  26. package/dist/graph-types.d.ts +0 -142
  27. package/dist/graph-types.d.ts.map +0 -1
  28. package/dist/graph-types.js +0 -6
  29. package/dist/graph-types.js.map +0 -1
  30. package/dist/native-runner.d.ts +0 -11
  31. package/dist/native-runner.d.ts.map +0 -1
  32. package/dist/native-runner.js +0 -77
  33. package/dist/native-runner.js.map +0 -1
  34. package/dist/native.d.ts +0 -106
  35. package/dist/native.d.ts.map +0 -1
  36. package/dist/native.js +0 -302
  37. package/dist/native.js.map +0 -1
  38. package/dist/sessions/types.d.ts +0 -28
  39. package/dist/sessions/types.d.ts.map +0 -1
  40. package/dist/sessions/types.js +0 -27
  41. package/dist/sessions/types.js.map +0 -1
  42. package/dist/spinner.d.ts +0 -75
  43. package/dist/spinner.d.ts.map +0 -1
  44. package/dist/spinner.js +0 -203
  45. package/dist/spinner.js.map +0 -1
  46. package/dist/submit.d.ts +0 -23
  47. package/dist/submit.d.ts.map +0 -1
  48. package/dist/submit.js +0 -294
  49. package/dist/submit.js.map +0 -1
  50. package/dist/table.d.ts +0 -42
  51. package/dist/table.d.ts.map +0 -1
  52. package/dist/table.js +0 -181
  53. package/dist/table.js.map +0 -1
  54. package/dist/tui/App.d.ts +0 -4
  55. package/dist/tui/App.d.ts.map +0 -1
  56. package/dist/tui/App.js +0 -333
  57. package/dist/tui/App.js.map +0 -1
  58. package/dist/tui/components/BarChart.d.ts +0 -17
  59. package/dist/tui/components/BarChart.d.ts.map +0 -1
  60. package/dist/tui/components/BarChart.js +0 -146
  61. package/dist/tui/components/BarChart.js.map +0 -1
  62. package/dist/tui/components/DailyView.d.ts +0 -13
  63. package/dist/tui/components/DailyView.d.ts.map +0 -1
  64. package/dist/tui/components/DailyView.js +0 -86
  65. package/dist/tui/components/DailyView.js.map +0 -1
  66. package/dist/tui/components/DateBreakdownPanel.d.ts +0 -7
  67. package/dist/tui/components/DateBreakdownPanel.d.ts.map +0 -1
  68. package/dist/tui/components/DateBreakdownPanel.js +0 -36
  69. package/dist/tui/components/DateBreakdownPanel.js.map +0 -1
  70. package/dist/tui/components/Footer.d.ts +0 -28
  71. package/dist/tui/components/Footer.d.ts.map +0 -1
  72. package/dist/tui/components/Footer.js +0 -130
  73. package/dist/tui/components/Footer.js.map +0 -1
  74. package/dist/tui/components/Header.d.ts +0 -9
  75. package/dist/tui/components/Header.d.ts.map +0 -1
  76. package/dist/tui/components/Header.js +0 -20
  77. package/dist/tui/components/Header.js.map +0 -1
  78. package/dist/tui/components/Legend.d.ts +0 -7
  79. package/dist/tui/components/Legend.d.ts.map +0 -1
  80. package/dist/tui/components/Legend.js +0 -16
  81. package/dist/tui/components/Legend.js.map +0 -1
  82. package/dist/tui/components/LoadingSpinner.d.ts +0 -8
  83. package/dist/tui/components/LoadingSpinner.d.ts.map +0 -1
  84. package/dist/tui/components/LoadingSpinner.js +0 -55
  85. package/dist/tui/components/LoadingSpinner.js.map +0 -1
  86. package/dist/tui/components/ModelRow.d.ts +0 -13
  87. package/dist/tui/components/ModelRow.d.ts.map +0 -1
  88. package/dist/tui/components/ModelRow.js +0 -15
  89. package/dist/tui/components/ModelRow.js.map +0 -1
  90. package/dist/tui/components/ModelView.d.ts +0 -13
  91. package/dist/tui/components/ModelView.d.ts.map +0 -1
  92. package/dist/tui/components/ModelView.js +0 -96
  93. package/dist/tui/components/ModelView.js.map +0 -1
  94. package/dist/tui/components/OverviewView.d.ts +0 -14
  95. package/dist/tui/components/OverviewView.d.ts.map +0 -1
  96. package/dist/tui/components/OverviewView.js +0 -65
  97. package/dist/tui/components/OverviewView.js.map +0 -1
  98. package/dist/tui/components/StatsView.d.ts +0 -14
  99. package/dist/tui/components/StatsView.d.ts.map +0 -1
  100. package/dist/tui/components/StatsView.js +0 -102
  101. package/dist/tui/components/StatsView.js.map +0 -1
  102. package/dist/tui/components/TokenBreakdown.d.ts +0 -14
  103. package/dist/tui/components/TokenBreakdown.d.ts.map +0 -1
  104. package/dist/tui/components/TokenBreakdown.js +0 -10
  105. package/dist/tui/components/TokenBreakdown.js.map +0 -1
  106. package/dist/tui/components/index.d.ts +0 -16
  107. package/dist/tui/components/index.d.ts.map +0 -1
  108. package/dist/tui/components/index.js +0 -13
  109. package/dist/tui/components/index.js.map +0 -1
  110. package/dist/tui/config/settings.d.ts +0 -15
  111. package/dist/tui/config/settings.d.ts.map +0 -1
  112. package/dist/tui/config/settings.js +0 -147
  113. package/dist/tui/config/settings.js.map +0 -1
  114. package/dist/tui/config/themes.d.ts +0 -15
  115. package/dist/tui/config/themes.d.ts.map +0 -1
  116. package/dist/tui/config/themes.js +0 -82
  117. package/dist/tui/config/themes.js.map +0 -1
  118. package/dist/tui/hooks/useData.d.ts +0 -19
  119. package/dist/tui/hooks/useData.d.ts.map +0 -1
  120. package/dist/tui/hooks/useData.js +0 -468
  121. package/dist/tui/hooks/useData.js.map +0 -1
  122. package/dist/tui/index.d.ts +0 -4
  123. package/dist/tui/index.d.ts.map +0 -1
  124. package/dist/tui/index.js +0 -36
  125. package/dist/tui/index.js.map +0 -1
  126. package/dist/tui/types/index.d.ts +0 -137
  127. package/dist/tui/types/index.d.ts.map +0 -1
  128. package/dist/tui/types/index.js +0 -26
  129. package/dist/tui/types/index.js.map +0 -1
  130. package/dist/tui/utils/cleanup.d.ts +0 -22
  131. package/dist/tui/utils/cleanup.d.ts.map +0 -1
  132. package/dist/tui/utils/cleanup.js +0 -59
  133. package/dist/tui/utils/cleanup.js.map +0 -1
  134. package/dist/tui/utils/colors.d.ts +0 -19
  135. package/dist/tui/utils/colors.d.ts.map +0 -1
  136. package/dist/tui/utils/colors.js +0 -71
  137. package/dist/tui/utils/colors.js.map +0 -1
  138. package/dist/tui/utils/format.d.ts +0 -7
  139. package/dist/tui/utils/format.d.ts.map +0 -1
  140. package/dist/tui/utils/format.js +0 -45
  141. package/dist/tui/utils/format.js.map +0 -1
  142. package/dist/tui/utils/responsive.d.ts +0 -5
  143. package/dist/tui/utils/responsive.d.ts.map +0 -1
  144. package/dist/tui/utils/responsive.js +0 -5
  145. package/dist/tui/utils/responsive.js.map +0 -1
  146. package/dist/wrapped.d.ts +0 -43
  147. package/dist/wrapped.d.ts.map +0 -1
  148. package/dist/wrapped.js +0 -719
  149. package/dist/wrapped.js.map +0 -1
  150. package/src/auth.ts +0 -211
  151. package/src/cli.ts +0 -1892
  152. package/src/credentials.ts +0 -176
  153. package/src/cursor.ts +0 -1044
  154. package/src/date-utils.ts +0 -51
  155. package/src/graph-types.ts +0 -175
  156. package/src/native-runner.js +0 -4
  157. package/src/native-runner.ts +0 -91
  158. package/src/native.ts +0 -633
  159. package/src/sessions/types.ts +0 -59
  160. package/src/spinner.ts +0 -283
  161. package/src/submit.ts +0 -360
  162. package/src/table.ts +0 -233
  163. package/src/tui/App.tsx +0 -453
  164. package/src/tui/components/BarChart.tsx +0 -205
  165. package/src/tui/components/DailyView.tsx +0 -132
  166. package/src/tui/components/DateBreakdownPanel.tsx +0 -79
  167. package/src/tui/components/Footer.tsx +0 -380
  168. package/src/tui/components/Header.tsx +0 -68
  169. package/src/tui/components/Legend.tsx +0 -39
  170. package/src/tui/components/LoadingSpinner.tsx +0 -81
  171. package/src/tui/components/ModelRow.tsx +0 -47
  172. package/src/tui/components/ModelView.tsx +0 -147
  173. package/src/tui/components/OverviewView.tsx +0 -121
  174. package/src/tui/components/StatsView.tsx +0 -249
  175. package/src/tui/components/TokenBreakdown.tsx +0 -46
  176. package/src/tui/components/index.ts +0 -15
  177. package/src/tui/config/settings.ts +0 -183
  178. package/src/tui/config/themes.ts +0 -115
  179. package/src/tui/hooks/useData.ts +0 -558
  180. package/src/tui/index.tsx +0 -44
  181. package/src/tui/opentui.d.ts +0 -166
  182. package/src/tui/types/index.ts +0 -173
  183. package/src/tui/utils/cleanup.ts +0 -65
  184. package/src/tui/utils/colors.ts +0 -78
  185. package/src/tui/utils/format.ts +0 -36
  186. package/src/tui/utils/responsive.ts +0 -8
  187. package/src/types.d.ts +0 -28
  188. package/src/wrapped.ts +0 -848
package/src/spinner.ts DELETED
@@ -1,283 +0,0 @@
1
- /**
2
- * OpenCode-style Knight Rider Spinner
3
- * Accurate port from: https://github.com/sst/opencode/blob/dev/packages/opencode/src/cli/cmd/tui/ui/spinner.ts
4
- *
5
- * Features:
6
- * - Bidirectional sweep (left→right→left)
7
- * - Hold frames at each end (pause effect)
8
- * - Color gradient trail using ANSI 256 colors
9
- * - Same characters as OpenCode: ■ (active) / ⬝ (inactive)
10
- */
11
-
12
- // =============================================================================
13
- // ANSI Color Helpers
14
- // =============================================================================
15
-
16
- function ansi256Fg(code: number): string {
17
- return `\x1b[38;5;${code}m`;
18
- }
19
-
20
- const RESET = "\x1b[0m";
21
- const HIDE_CURSOR = "\x1B[?25l";
22
- const SHOW_CURSOR = "\x1B[?25h";
23
- const CLEAR_LINE = "\r\x1B[K";
24
-
25
- // =============================================================================
26
- // Color Gradients (ANSI 256)
27
- // Approximates OpenCode's RGBA alpha-based trail fade
28
- // =============================================================================
29
-
30
- type ColorName = "cyan" | "green" | "magenta" | "yellow" | "red" | "blue" | "white";
31
-
32
- const COLOR_GRADIENTS: Record<ColorName, number[]> = {
33
- // Bright → dim (6 steps for trail)
34
- cyan: [51, 44, 37, 30, 23, 17],
35
- green: [46, 40, 34, 28, 22, 22],
36
- magenta: [201, 165, 129, 93, 57, 53],
37
- yellow: [226, 220, 214, 178, 136, 94],
38
- red: [196, 160, 124, 88, 52, 52],
39
- blue: [33, 27, 21, 18, 17, 17],
40
- white: [255, 250, 245, 240, 236, 232],
41
- };
42
-
43
- const INACTIVE_COLOR = 240; // Gray for ⬝
44
-
45
- // =============================================================================
46
- // Frame Generation (matches OpenCode exactly)
47
- // =============================================================================
48
-
49
- interface SpinnerOptions {
50
- /** Width of the spinner in characters (default: 8) */
51
- width?: number;
52
- /** Frames to hold at start position (default: 30) */
53
- holdStart?: number;
54
- /** Frames to hold at end position (default: 9) */
55
- holdEnd?: number;
56
- /** Trail length - number of colored blocks behind lead (default: 4) */
57
- trailLength?: number;
58
- /** Color theme (default: "cyan") */
59
- color?: ColorName;
60
- /** Frame interval in ms (default: 40) */
61
- interval?: number;
62
- }
63
-
64
- interface ScannerState {
65
- activePosition: number;
66
- isMovingForward: boolean;
67
- }
68
-
69
- /**
70
- * Calculate scanner state for a given frame index
71
- * Matches OpenCode's getScannerState() exactly
72
- */
73
- function getScannerState(
74
- frameIndex: number,
75
- width: number,
76
- holdStart: number,
77
- holdEnd: number
78
- ): ScannerState {
79
- const forwardFrames = width;
80
- const backwardFrames = width - 1;
81
- const totalCycle = forwardFrames + holdEnd + backwardFrames + holdStart;
82
-
83
- // Normalize frame index to cycle
84
- const normalizedFrame = frameIndex % totalCycle;
85
-
86
- if (normalizedFrame < forwardFrames) {
87
- // Phase 1: Moving forward (0 → width-1)
88
- return {
89
- activePosition: normalizedFrame,
90
- isMovingForward: true,
91
- };
92
- } else if (normalizedFrame < forwardFrames + holdEnd) {
93
- // Phase 2: Holding at end
94
- return {
95
- activePosition: width - 1,
96
- isMovingForward: true,
97
- };
98
- } else if (normalizedFrame < forwardFrames + holdEnd + backwardFrames) {
99
- // Phase 3: Moving backward (width-2 → 0)
100
- const backwardIndex = normalizedFrame - forwardFrames - holdEnd;
101
- return {
102
- activePosition: width - 2 - backwardIndex,
103
- isMovingForward: false,
104
- };
105
- } else {
106
- // Phase 4: Holding at start
107
- return {
108
- activePosition: 0,
109
- isMovingForward: false,
110
- };
111
- }
112
- }
113
-
114
- /**
115
- * Generate a single frame string with colors
116
- */
117
- function generateColoredFrame(
118
- frameIndex: number,
119
- width: number,
120
- holdStart: number,
121
- holdEnd: number,
122
- trailLength: number,
123
- gradient: number[]
124
- ): string {
125
- const state = getScannerState(frameIndex, width, holdStart, holdEnd);
126
- const { activePosition, isMovingForward } = state;
127
-
128
- let result = "";
129
-
130
- for (let i = 0; i < width; i++) {
131
- // Calculate directional distance (positive = trailing behind)
132
- const directionalDistance = isMovingForward
133
- ? activePosition - i // Forward: trail is to the left
134
- : i - activePosition; // Backward: trail is to the right
135
-
136
- if (directionalDistance >= 0 && directionalDistance < trailLength) {
137
- // Active position with color gradient
138
- const colorIdx = Math.min(directionalDistance, gradient.length - 1);
139
- result += ansi256Fg(gradient[colorIdx]) + "■" + RESET;
140
- } else {
141
- // Inactive position
142
- result += ansi256Fg(INACTIVE_COLOR) + "⬝" + RESET;
143
- }
144
- }
145
-
146
- return result;
147
- }
148
-
149
- /**
150
- * Calculate total frames in one complete cycle
151
- */
152
- function getTotalFrames(width: number, holdStart: number, holdEnd: number): number {
153
- return width + holdEnd + (width - 1) + holdStart;
154
- }
155
-
156
- // =============================================================================
157
- // Spinner Class
158
- // =============================================================================
159
-
160
- export class Spinner {
161
- private intervalId: NodeJS.Timeout | null = null;
162
- private frameIndex = 0;
163
- private width: number;
164
- private holdStart: number;
165
- private holdEnd: number;
166
- private trailLength: number;
167
- private gradient: number[];
168
- private interval: number;
169
- private message: string = "";
170
- private totalFrames: number;
171
-
172
- constructor(options: SpinnerOptions = {}) {
173
- this.width = options.width ?? 8;
174
- this.holdStart = options.holdStart ?? 30;
175
- this.holdEnd = options.holdEnd ?? 9;
176
- this.trailLength = options.trailLength ?? 4;
177
- this.interval = options.interval ?? 40;
178
-
179
- const colorName = options.color ?? "cyan";
180
- this.gradient = COLOR_GRADIENTS[colorName] || COLOR_GRADIENTS.cyan;
181
-
182
- this.totalFrames = getTotalFrames(this.width, this.holdStart, this.holdEnd);
183
- }
184
-
185
- /**
186
- * Start the spinner with a message
187
- */
188
- start(message: string): void {
189
- this.message = message;
190
- this.frameIndex = 0;
191
-
192
- // Hide cursor
193
- process.stdout.write(HIDE_CURSOR);
194
-
195
- this.intervalId = setInterval(() => {
196
- const frame = generateColoredFrame(
197
- this.frameIndex,
198
- this.width,
199
- this.holdStart,
200
- this.holdEnd,
201
- this.trailLength,
202
- this.gradient
203
- );
204
-
205
- process.stdout.write(`${CLEAR_LINE} ${frame} ${this.message}`);
206
- this.frameIndex = (this.frameIndex + 1) % this.totalFrames;
207
- }, this.interval);
208
- }
209
-
210
- /**
211
- * Update the spinner message while running
212
- */
213
- update(message: string): void {
214
- this.message = message;
215
- }
216
-
217
- /**
218
- * Stop the spinner and show a success message
219
- */
220
- success(message: string): void {
221
- this.stop();
222
- console.log(` \x1b[32m✓\x1b[0m ${message}`);
223
- }
224
-
225
- /**
226
- * Stop the spinner and show an error message
227
- */
228
- error(message: string): void {
229
- this.stop();
230
- console.log(` \x1b[31m✗\x1b[0m ${message}`);
231
- }
232
-
233
- /**
234
- * Stop the spinner without a message
235
- */
236
- stop(): void {
237
- if (this.intervalId) {
238
- clearInterval(this.intervalId);
239
- this.intervalId = null;
240
- }
241
- process.stdout.write(CLEAR_LINE);
242
- process.stdout.write(SHOW_CURSOR);
243
- }
244
-
245
- /**
246
- * Check if spinner is currently running
247
- */
248
- isSpinning(): boolean {
249
- return this.intervalId !== null;
250
- }
251
- }
252
-
253
- // =============================================================================
254
- // Convenience Functions
255
- // =============================================================================
256
-
257
- /**
258
- * Create a spinner with default OpenCode settings
259
- */
260
- export function createSpinner(options?: SpinnerOptions): Spinner {
261
- return new Spinner(options);
262
- }
263
-
264
- /**
265
- * Run an async function with a spinner
266
- */
267
- export async function withSpinner<T>(
268
- message: string,
269
- fn: () => Promise<T>,
270
- options?: SpinnerOptions & { successMessage?: string; errorMessage?: string }
271
- ): Promise<T> {
272
- const spinner = new Spinner(options);
273
- spinner.start(message);
274
-
275
- try {
276
- const result = await fn();
277
- spinner.success(options?.successMessage ?? message);
278
- return result;
279
- } catch (error) {
280
- spinner.error(options?.errorMessage ?? `Failed: ${message}`);
281
- throw error;
282
- }
283
- }
package/src/submit.ts DELETED
@@ -1,360 +0,0 @@
1
- /**
2
- * Tokscale CLI Submit Command
3
- * Submits local token usage data to the social platform
4
- */
5
-
6
- import pc from "picocolors";
7
- import * as readline from "node:readline/promises";
8
- import { stdin as input, stdout as output } from "node:process";
9
- import { exec } from "node:child_process";
10
- import { promisify } from "node:util";
11
- import { loadCredentials, getApiBaseUrl, loadStarCache, saveStarCache } from "./credentials.js";
12
- import { parseLocalSourcesAsync, finalizeReportAndGraphAsync, type ParsedMessages } from "./native.js";
13
- import { syncCursorCache, isCursorLoggedIn, hasCursorUsageCache } from "./cursor.js";
14
- import type { TokenContributionData } from "./graph-types.js";
15
- import { formatCurrency } from "./table.js";
16
- import { parseDateStringToLocal, getStartOfDayTimestamp, getEndOfDayTimestamp, validateTimestampMs } from "./date-utils.js";
17
-
18
- const execAsync = promisify(exec);
19
-
20
- function getTimestampFilters(since?: string, until?: string): { sinceTs?: number; untilTs?: number } {
21
- let sinceTs: number | undefined;
22
- let untilTs: number | undefined;
23
-
24
- if (since) {
25
- const sinceDate = parseDateStringToLocal(since);
26
- if (sinceDate) {
27
- sinceTs = getStartOfDayTimestamp(sinceDate);
28
- sinceTs = validateTimestampMs(sinceTs, '--since');
29
- }
30
- }
31
-
32
- if (until) {
33
- const untilDate = parseDateStringToLocal(until);
34
- if (untilDate) {
35
- untilTs = getEndOfDayTimestamp(untilDate);
36
- untilTs = validateTimestampMs(untilTs, '--until');
37
- }
38
- }
39
-
40
- return { sinceTs, untilTs };
41
- }
42
-
43
- interface SubmitOptions {
44
- opencode?: boolean;
45
- claude?: boolean;
46
- codex?: boolean;
47
- gemini?: boolean;
48
- cursor?: boolean;
49
- amp?: boolean;
50
- droid?: boolean;
51
- openclaw?: boolean;
52
- pi?: boolean;
53
- kimi?: boolean;
54
- since?: string;
55
- until?: string;
56
- year?: string;
57
- dryRun?: boolean;
58
- }
59
-
60
- interface SubmitResponse {
61
- success: boolean;
62
- submissionId?: string;
63
- username?: string;
64
- metrics?: {
65
- totalTokens: number;
66
- totalCost: number;
67
- dateRange: {
68
- start: string;
69
- end: string;
70
- };
71
- activeDays: number;
72
- sources: string[];
73
- };
74
- warnings?: string[];
75
- error?: string;
76
- details?: string[];
77
- }
78
-
79
- type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor" | "amp" | "droid" | "openclaw" | "pi" | "kimi";
80
-
81
- async function checkGhCliExists(): Promise<boolean> {
82
- try {
83
- await execAsync("gh --version");
84
- return true;
85
- } catch {
86
- return false;
87
- }
88
- }
89
-
90
- async function checkGitHubStarStatus(): Promise<boolean> {
91
- try {
92
- await execAsync("gh api /user/starred/junhoyeo/tokscale");
93
- return true;
94
- } catch (error: any) {
95
- if (error.code === 1 || error.stderr?.includes("404")) {
96
- return false;
97
- }
98
- throw error;
99
- }
100
- }
101
-
102
- async function attemptToStarRepo(): Promise<boolean> {
103
- try {
104
- await execAsync("gh api --silent --method PUT /user/starred/junhoyeo/tokscale >/dev/null 2>&1 || true");
105
- return true;
106
- } catch {
107
- return false;
108
- }
109
- }
110
-
111
- async function promptUserToStar(): Promise<'star' | 'decline'> {
112
- const rl = readline.createInterface({ input, output });
113
-
114
- return new Promise((resolve) => {
115
- const cleanup = () => {
116
- rl.close();
117
- };
118
-
119
- const handleSigint = () => {
120
- cleanup();
121
- resolve('decline');
122
- };
123
-
124
- process.once('SIGINT', handleSigint);
125
-
126
- rl.question(pc.white(" ⭐ Would you like to star tokscale? (Y/n): "))
127
- .then((answer) => {
128
- process.off('SIGINT', handleSigint);
129
- cleanup();
130
- const normalized = answer.trim().toLowerCase();
131
- resolve(normalized === 'n' ? 'decline' : 'star');
132
- })
133
- .catch(() => {
134
- process.off('SIGINT', handleSigint);
135
- cleanup();
136
- resolve('decline');
137
- });
138
- });
139
- }
140
-
141
- async function handleStarPrompt(username: string): Promise<void> {
142
- const starCache = loadStarCache(username);
143
- if (starCache?.hasStarred) {
144
- return;
145
- }
146
-
147
- const ghExists = await checkGhCliExists();
148
-
149
- if (ghExists) {
150
- try {
151
- const hasStarred = await checkGitHubStarStatus();
152
- if (hasStarred) {
153
- saveStarCache({
154
- username,
155
- hasStarred: true,
156
- checkedAt: new Date().toISOString(),
157
- });
158
- return;
159
- }
160
- } catch (error: any) {
161
- if (
162
- error.code === 'ENOTFOUND' ||
163
- error.code === 'ETIMEDOUT' ||
164
- error.stderr?.includes('404') ||
165
- error.stderr?.includes('Could not resolve')
166
- ) {
167
- return;
168
- }
169
- throw error;
170
- }
171
- }
172
-
173
- console.log();
174
- console.log(pc.cyan(" Help us grow! ⭐"));
175
- console.log(pc.gray(" Starring tokscale helps others discover the project.\n"));
176
-
177
- const userChoice = await promptUserToStar();
178
-
179
- if (userChoice === 'decline') {
180
- console.log();
181
- return;
182
- }
183
-
184
- if (!ghExists) {
185
- console.log();
186
- console.log(pc.yellow(" GitHub CLI (gh) not found."));
187
- console.log(pc.white(" Please star the repo manually:"));
188
- console.log(pc.cyan(" https://github.com/junhoyeo/tokscale\n"));
189
- return;
190
- }
191
-
192
- console.log(pc.gray(" Starring repository..."));
193
- const starred = await attemptToStarRepo();
194
-
195
- if (starred) {
196
- console.log(pc.green(" ✓ Starred! Thank you for your support.\n"));
197
- saveStarCache({
198
- username,
199
- hasStarred: true,
200
- checkedAt: new Date().toISOString(),
201
- });
202
- } else {
203
- console.log(pc.yellow(" Failed to star via gh CLI."));
204
- console.log(pc.gray(" Continuing to submit...\n"));
205
- }
206
- }
207
-
208
- export async function submit(options: SubmitOptions = {}): Promise<void> {
209
- const credentials = loadCredentials();
210
- if (!credentials) {
211
- console.log(pc.yellow("\n Not logged in."));
212
- console.log(pc.gray(" Run 'tokscale login' first.\n"));
213
- process.exit(1);
214
- }
215
-
216
- await handleStarPrompt(credentials.username);
217
-
218
- console.log(pc.cyan("\n Tokscale - Submit Usage Data\n"));
219
-
220
- console.log(pc.gray(" Scanning local session data..."));
221
-
222
- const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid || options.openclaw || options.pi || options.kimi;
223
- let sources: SourceType[] | undefined;
224
- let includeCursor = true;
225
- if (hasFilter) {
226
- sources = [];
227
- if (options.opencode) sources.push("opencode");
228
- if (options.claude) sources.push("claude");
229
- if (options.codex) sources.push("codex");
230
- if (options.gemini) sources.push("gemini");
231
- if (options.cursor) sources.push("cursor");
232
- if (options.amp) sources.push("amp");
233
- if (options.droid) sources.push("droid");
234
- if (options.openclaw) sources.push("openclaw");
235
- if (options.pi) sources.push("pi");
236
- if (options.kimi) sources.push("kimi");
237
- includeCursor = sources.includes("cursor");
238
- }
239
-
240
- // Filter out cursor from local sources (it's handled separately via sync)
241
- const localSources = sources?.filter((s): s is Exclude<SourceType, "cursor"> => s !== "cursor");
242
- const { sinceTs, untilTs } = getTimestampFilters(options.since, options.until);
243
-
244
- let data: TokenContributionData;
245
- try {
246
- // Two-phase processing (same as TUI) for consistency:
247
- // Phase 1: Parse local sources + sync cursor in parallel
248
- const [localMessages, cursorSync] = await Promise.all([
249
- parseLocalSourcesAsync({
250
- sources: localSources,
251
- since: options.since,
252
- until: options.until,
253
- year: options.year,
254
- sinceTs,
255
- untilTs,
256
- }),
257
- includeCursor && isCursorLoggedIn()
258
- ? syncCursorCache()
259
- : Promise.resolve({ synced: false, rows: 0, error: undefined }),
260
- ]);
261
-
262
- if (includeCursor && cursorSync.error && (cursorSync.synced || hasCursorUsageCache())) {
263
- const prefix = cursorSync.synced ? "Cursor sync warning" : "Cursor sync failed; using cached data";
264
- console.log(pc.yellow(` ${prefix}: ${cursorSync.error}`));
265
- }
266
-
267
- // Phase 2: Finalize with pricing (combines local + cursor)
268
- // Single subprocess call ensures consistent pricing for both report and graph
269
- const { report, graph } = await finalizeReportAndGraphAsync({
270
- localMessages,
271
- includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
272
- since: options.since,
273
- until: options.until,
274
- year: options.year,
275
- sinceTs,
276
- untilTs,
277
- });
278
-
279
- // Use graph structure for submission, report's cost for display
280
- data = graph;
281
- data.summary.totalCost = report.totalCost;
282
- } catch (error) {
283
- console.error(pc.red(`\n Error generating data: ${(error as Error).message}\n`));
284
- process.exit(1);
285
- }
286
-
287
- // Step 4: Show summary
288
- console.log(pc.white(" Data to submit:"));
289
- console.log(pc.gray(` Date range: ${data.meta.dateRange.start} to ${data.meta.dateRange.end}`));
290
- console.log(pc.gray(` Active days: ${data.summary.activeDays}`));
291
- console.log(pc.gray(` Total tokens: ${data.summary.totalTokens.toLocaleString()}`));
292
- console.log(pc.gray(` Total cost: ${formatCurrency(data.summary.totalCost)}`));
293
- console.log(pc.gray(` Sources: ${data.summary.sources.join(", ")}`));
294
- console.log(pc.gray(` Models: ${data.summary.models.length} models`));
295
- console.log();
296
-
297
- if (data.summary.totalTokens === 0) {
298
- console.log(pc.yellow(" No usage data found to submit.\n"));
299
- return;
300
- }
301
-
302
- // Step 5: Dry run check
303
- if (options.dryRun) {
304
- console.log(pc.yellow(" Dry run - not submitting data.\n"));
305
- return;
306
- }
307
-
308
- // Step 6: Submit to server
309
- console.log(pc.gray(" Submitting to server..."));
310
-
311
- const baseUrl = getApiBaseUrl();
312
-
313
- try {
314
- const response = await fetch(`${baseUrl}/api/submit`, {
315
- method: "POST",
316
- headers: {
317
- "Content-Type": "application/json",
318
- Authorization: `Bearer ${credentials.token}`,
319
- },
320
- body: JSON.stringify(data),
321
- });
322
-
323
- const result: SubmitResponse = await response.json();
324
-
325
- if (!response.ok) {
326
- console.error(pc.red(`\n Error: ${result.error || "Submission failed"}`));
327
- if (result.details) {
328
- for (const detail of result.details) {
329
- console.error(pc.gray(` - ${detail}`));
330
- }
331
- }
332
- console.log();
333
- process.exit(1);
334
- }
335
-
336
- // Success!
337
- console.log(pc.green("\n Successfully submitted!"));
338
- console.log();
339
- console.log(pc.white(" Summary:"));
340
- console.log(pc.gray(` Submission ID: ${result.submissionId}`));
341
- console.log(pc.gray(` Total tokens: ${result.metrics?.totalTokens?.toLocaleString()}`));
342
- console.log(pc.gray(` Total cost: ${formatCurrency(result.metrics?.totalCost || 0)}`));
343
- console.log(pc.gray(` Active days: ${result.metrics?.activeDays}`));
344
- console.log();
345
- console.log(pc.cyan(` View your profile: ${baseUrl}/u/${credentials.username}`));
346
- console.log();
347
-
348
- if (result.warnings && result.warnings.length > 0) {
349
- console.log(pc.yellow(" Warnings:"));
350
- for (const warning of result.warnings) {
351
- console.log(pc.gray(` - ${warning}`));
352
- }
353
- console.log();
354
- }
355
- } catch (error) {
356
- console.error(pc.red(`\n Error: Failed to connect to server.`));
357
- console.error(pc.gray(` ${(error as Error).message}\n`));
358
- process.exit(1);
359
- }
360
- }