@tokscale/cli 1.4.2 → 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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -26
- package/dist/auth.d.ts +0 -17
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -162
- package/dist/auth.js.map +0 -1
- package/dist/cli.d.ts +0 -9
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -1523
- package/dist/cli.js.map +0 -1
- package/dist/credentials.d.ts +0 -50
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js +0 -151
- package/dist/credentials.js.map +0 -1
- package/dist/cursor.d.ts +0 -167
- package/dist/cursor.d.ts.map +0 -1
- package/dist/cursor.js +0 -906
- package/dist/cursor.js.map +0 -1
- package/dist/date-utils.d.ts +0 -10
- package/dist/date-utils.d.ts.map +0 -1
- package/dist/date-utils.js +0 -47
- package/dist/date-utils.js.map +0 -1
- package/dist/graph-types.d.ts +0 -142
- package/dist/graph-types.d.ts.map +0 -1
- package/dist/graph-types.js +0 -6
- package/dist/graph-types.js.map +0 -1
- package/dist/native-runner.d.ts +0 -11
- package/dist/native-runner.d.ts.map +0 -1
- package/dist/native-runner.js +0 -77
- package/dist/native-runner.js.map +0 -1
- package/dist/native.d.ts +0 -105
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -302
- package/dist/native.js.map +0 -1
- package/dist/sessions/types.d.ts +0 -28
- package/dist/sessions/types.d.ts.map +0 -1
- package/dist/sessions/types.js +0 -27
- package/dist/sessions/types.js.map +0 -1
- package/dist/spinner.d.ts +0 -75
- package/dist/spinner.d.ts.map +0 -1
- package/dist/spinner.js +0 -203
- package/dist/spinner.js.map +0 -1
- package/dist/submit.d.ts +0 -22
- package/dist/submit.d.ts.map +0 -1
- package/dist/submit.js +0 -292
- package/dist/submit.js.map +0 -1
- package/dist/table.d.ts +0 -42
- package/dist/table.d.ts.map +0 -1
- package/dist/table.js +0 -181
- package/dist/table.js.map +0 -1
- package/dist/tui/App.d.ts +0 -4
- package/dist/tui/App.d.ts.map +0 -1
- package/dist/tui/App.js +0 -350
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/components/BarChart.d.ts +0 -17
- package/dist/tui/components/BarChart.d.ts.map +0 -1
- package/dist/tui/components/BarChart.js +0 -146
- package/dist/tui/components/BarChart.js.map +0 -1
- package/dist/tui/components/DailyView.d.ts +0 -13
- package/dist/tui/components/DailyView.d.ts.map +0 -1
- package/dist/tui/components/DailyView.js +0 -86
- package/dist/tui/components/DailyView.js.map +0 -1
- package/dist/tui/components/DateBreakdownPanel.d.ts +0 -7
- package/dist/tui/components/DateBreakdownPanel.d.ts.map +0 -1
- package/dist/tui/components/DateBreakdownPanel.js +0 -36
- package/dist/tui/components/DateBreakdownPanel.js.map +0 -1
- package/dist/tui/components/Footer.d.ts +0 -28
- package/dist/tui/components/Footer.d.ts.map +0 -1
- package/dist/tui/components/Footer.js +0 -130
- package/dist/tui/components/Footer.js.map +0 -1
- package/dist/tui/components/Header.d.ts +0 -9
- package/dist/tui/components/Header.d.ts.map +0 -1
- package/dist/tui/components/Header.js +0 -20
- package/dist/tui/components/Header.js.map +0 -1
- package/dist/tui/components/Legend.d.ts +0 -7
- package/dist/tui/components/Legend.d.ts.map +0 -1
- package/dist/tui/components/Legend.js +0 -16
- package/dist/tui/components/Legend.js.map +0 -1
- package/dist/tui/components/LoadingSpinner.d.ts +0 -8
- package/dist/tui/components/LoadingSpinner.d.ts.map +0 -1
- package/dist/tui/components/LoadingSpinner.js +0 -55
- package/dist/tui/components/LoadingSpinner.js.map +0 -1
- package/dist/tui/components/ModelRow.d.ts +0 -13
- package/dist/tui/components/ModelRow.d.ts.map +0 -1
- package/dist/tui/components/ModelRow.js +0 -15
- package/dist/tui/components/ModelRow.js.map +0 -1
- package/dist/tui/components/ModelView.d.ts +0 -13
- package/dist/tui/components/ModelView.d.ts.map +0 -1
- package/dist/tui/components/ModelView.js +0 -96
- package/dist/tui/components/ModelView.js.map +0 -1
- package/dist/tui/components/OverviewView.d.ts +0 -14
- package/dist/tui/components/OverviewView.d.ts.map +0 -1
- package/dist/tui/components/OverviewView.js +0 -65
- package/dist/tui/components/OverviewView.js.map +0 -1
- package/dist/tui/components/StatsView.d.ts +0 -14
- package/dist/tui/components/StatsView.d.ts.map +0 -1
- package/dist/tui/components/StatsView.js +0 -102
- package/dist/tui/components/StatsView.js.map +0 -1
- package/dist/tui/components/TokenBreakdown.d.ts +0 -14
- package/dist/tui/components/TokenBreakdown.d.ts.map +0 -1
- package/dist/tui/components/TokenBreakdown.js +0 -10
- package/dist/tui/components/TokenBreakdown.js.map +0 -1
- package/dist/tui/components/index.d.ts +0 -16
- package/dist/tui/components/index.d.ts.map +0 -1
- package/dist/tui/components/index.js +0 -13
- package/dist/tui/components/index.js.map +0 -1
- package/dist/tui/config/settings.d.ts +0 -15
- package/dist/tui/config/settings.d.ts.map +0 -1
- package/dist/tui/config/settings.js +0 -147
- package/dist/tui/config/settings.js.map +0 -1
- package/dist/tui/config/themes.d.ts +0 -15
- package/dist/tui/config/themes.d.ts.map +0 -1
- package/dist/tui/config/themes.js +0 -82
- package/dist/tui/config/themes.js.map +0 -1
- package/dist/tui/hooks/useData.d.ts +0 -19
- package/dist/tui/hooks/useData.d.ts.map +0 -1
- package/dist/tui/hooks/useData.js +0 -467
- package/dist/tui/hooks/useData.js.map +0 -1
- package/dist/tui/index.d.ts +0 -4
- package/dist/tui/index.d.ts.map +0 -1
- package/dist/tui/index.js +0 -36
- package/dist/tui/index.js.map +0 -1
- package/dist/tui/types/index.d.ts +0 -137
- package/dist/tui/types/index.d.ts.map +0 -1
- package/dist/tui/types/index.js +0 -25
- package/dist/tui/types/index.js.map +0 -1
- package/dist/tui/utils/cleanup.d.ts +0 -22
- package/dist/tui/utils/cleanup.d.ts.map +0 -1
- package/dist/tui/utils/cleanup.js +0 -59
- package/dist/tui/utils/cleanup.js.map +0 -1
- package/dist/tui/utils/colors.d.ts +0 -19
- package/dist/tui/utils/colors.d.ts.map +0 -1
- package/dist/tui/utils/colors.js +0 -68
- package/dist/tui/utils/colors.js.map +0 -1
- package/dist/tui/utils/format.d.ts +0 -7
- package/dist/tui/utils/format.d.ts.map +0 -1
- package/dist/tui/utils/format.js +0 -45
- package/dist/tui/utils/format.js.map +0 -1
- package/dist/tui/utils/responsive.d.ts +0 -5
- package/dist/tui/utils/responsive.d.ts.map +0 -1
- package/dist/tui/utils/responsive.js +0 -5
- package/dist/tui/utils/responsive.js.map +0 -1
- package/dist/wrapped.d.ts +0 -43
- package/dist/wrapped.d.ts.map +0 -1
- package/dist/wrapped.js +0 -716
- package/dist/wrapped.js.map +0 -1
- package/src/auth.ts +0 -211
- package/src/cli.ts +0 -1865
- package/src/credentials.ts +0 -176
- package/src/cursor.ts +0 -1044
- package/src/date-utils.ts +0 -51
- package/src/graph-types.ts +0 -175
- package/src/native-runner.js +0 -4
- package/src/native-runner.ts +0 -91
- package/src/native.ts +0 -631
- package/src/sessions/types.ts +0 -59
- package/src/spinner.ts +0 -283
- package/src/submit.ts +0 -358
- package/src/table.ts +0 -233
- package/src/tui/App.tsx +0 -443
- package/src/tui/components/BarChart.tsx +0 -205
- package/src/tui/components/DailyView.tsx +0 -132
- package/src/tui/components/DateBreakdownPanel.tsx +0 -79
- package/src/tui/components/Footer.tsx +0 -368
- package/src/tui/components/Header.tsx +0 -68
- package/src/tui/components/Legend.tsx +0 -39
- package/src/tui/components/LoadingSpinner.tsx +0 -81
- package/src/tui/components/ModelRow.tsx +0 -47
- package/src/tui/components/ModelView.tsx +0 -147
- package/src/tui/components/OverviewView.tsx +0 -121
- package/src/tui/components/StatsView.tsx +0 -249
- package/src/tui/components/TokenBreakdown.tsx +0 -46
- package/src/tui/components/index.ts +0 -15
- package/src/tui/config/settings.ts +0 -183
- package/src/tui/config/themes.ts +0 -115
- package/src/tui/hooks/useData.ts +0 -557
- package/src/tui/index.tsx +0 -44
- package/src/tui/opentui.d.ts +0 -166
- package/src/tui/types/index.ts +0 -172
- package/src/tui/utils/cleanup.ts +0 -65
- package/src/tui/utils/colors.ts +0 -76
- package/src/tui/utils/format.ts +0 -36
- package/src/tui/utils/responsive.ts +0 -8
- package/src/types.d.ts +0 -28
- package/src/wrapped.ts +0 -845
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,358 +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
|
-
since?: string;
|
|
54
|
-
until?: string;
|
|
55
|
-
year?: string;
|
|
56
|
-
dryRun?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface SubmitResponse {
|
|
60
|
-
success: boolean;
|
|
61
|
-
submissionId?: string;
|
|
62
|
-
username?: string;
|
|
63
|
-
metrics?: {
|
|
64
|
-
totalTokens: number;
|
|
65
|
-
totalCost: number;
|
|
66
|
-
dateRange: {
|
|
67
|
-
start: string;
|
|
68
|
-
end: string;
|
|
69
|
-
};
|
|
70
|
-
activeDays: number;
|
|
71
|
-
sources: string[];
|
|
72
|
-
};
|
|
73
|
-
warnings?: string[];
|
|
74
|
-
error?: string;
|
|
75
|
-
details?: string[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor" | "amp" | "droid" | "openclaw" | "pi";
|
|
79
|
-
|
|
80
|
-
async function checkGhCliExists(): Promise<boolean> {
|
|
81
|
-
try {
|
|
82
|
-
await execAsync("gh --version");
|
|
83
|
-
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function checkGitHubStarStatus(): Promise<boolean> {
|
|
90
|
-
try {
|
|
91
|
-
await execAsync("gh api /user/starred/junhoyeo/tokscale");
|
|
92
|
-
return true;
|
|
93
|
-
} catch (error: any) {
|
|
94
|
-
if (error.code === 1 || error.stderr?.includes("404")) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function attemptToStarRepo(): Promise<boolean> {
|
|
102
|
-
try {
|
|
103
|
-
await execAsync("gh api --silent --method PUT /user/starred/junhoyeo/tokscale >/dev/null 2>&1 || true");
|
|
104
|
-
return true;
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function promptUserToStar(): Promise<'star' | 'decline'> {
|
|
111
|
-
const rl = readline.createInterface({ input, output });
|
|
112
|
-
|
|
113
|
-
return new Promise((resolve) => {
|
|
114
|
-
const cleanup = () => {
|
|
115
|
-
rl.close();
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const handleSigint = () => {
|
|
119
|
-
cleanup();
|
|
120
|
-
resolve('decline');
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
process.once('SIGINT', handleSigint);
|
|
124
|
-
|
|
125
|
-
rl.question(pc.white(" ⭐ Would you like to star tokscale? (Y/n): "))
|
|
126
|
-
.then((answer) => {
|
|
127
|
-
process.off('SIGINT', handleSigint);
|
|
128
|
-
cleanup();
|
|
129
|
-
const normalized = answer.trim().toLowerCase();
|
|
130
|
-
resolve(normalized === 'n' ? 'decline' : 'star');
|
|
131
|
-
})
|
|
132
|
-
.catch(() => {
|
|
133
|
-
process.off('SIGINT', handleSigint);
|
|
134
|
-
cleanup();
|
|
135
|
-
resolve('decline');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function handleStarPrompt(username: string): Promise<void> {
|
|
141
|
-
const starCache = loadStarCache(username);
|
|
142
|
-
if (starCache?.hasStarred) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const ghExists = await checkGhCliExists();
|
|
147
|
-
|
|
148
|
-
if (ghExists) {
|
|
149
|
-
try {
|
|
150
|
-
const hasStarred = await checkGitHubStarStatus();
|
|
151
|
-
if (hasStarred) {
|
|
152
|
-
saveStarCache({
|
|
153
|
-
username,
|
|
154
|
-
hasStarred: true,
|
|
155
|
-
checkedAt: new Date().toISOString(),
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
} catch (error: any) {
|
|
160
|
-
if (
|
|
161
|
-
error.code === 'ENOTFOUND' ||
|
|
162
|
-
error.code === 'ETIMEDOUT' ||
|
|
163
|
-
error.stderr?.includes('404') ||
|
|
164
|
-
error.stderr?.includes('Could not resolve')
|
|
165
|
-
) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
throw error;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
console.log();
|
|
173
|
-
console.log(pc.cyan(" Help us grow! ⭐"));
|
|
174
|
-
console.log(pc.gray(" Starring tokscale helps others discover the project.\n"));
|
|
175
|
-
|
|
176
|
-
const userChoice = await promptUserToStar();
|
|
177
|
-
|
|
178
|
-
if (userChoice === 'decline') {
|
|
179
|
-
console.log();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!ghExists) {
|
|
184
|
-
console.log();
|
|
185
|
-
console.log(pc.yellow(" GitHub CLI (gh) not found."));
|
|
186
|
-
console.log(pc.white(" Please star the repo manually:"));
|
|
187
|
-
console.log(pc.cyan(" https://github.com/junhoyeo/tokscale\n"));
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
console.log(pc.gray(" Starring repository..."));
|
|
192
|
-
const starred = await attemptToStarRepo();
|
|
193
|
-
|
|
194
|
-
if (starred) {
|
|
195
|
-
console.log(pc.green(" ✓ Starred! Thank you for your support.\n"));
|
|
196
|
-
saveStarCache({
|
|
197
|
-
username,
|
|
198
|
-
hasStarred: true,
|
|
199
|
-
checkedAt: new Date().toISOString(),
|
|
200
|
-
});
|
|
201
|
-
} else {
|
|
202
|
-
console.log(pc.yellow(" Failed to star via gh CLI."));
|
|
203
|
-
console.log(pc.gray(" Continuing to submit...\n"));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export async function submit(options: SubmitOptions = {}): Promise<void> {
|
|
208
|
-
const credentials = loadCredentials();
|
|
209
|
-
if (!credentials) {
|
|
210
|
-
console.log(pc.yellow("\n Not logged in."));
|
|
211
|
-
console.log(pc.gray(" Run 'tokscale login' first.\n"));
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
await handleStarPrompt(credentials.username);
|
|
216
|
-
|
|
217
|
-
console.log(pc.cyan("\n Tokscale - Submit Usage Data\n"));
|
|
218
|
-
|
|
219
|
-
console.log(pc.gray(" Scanning local session data..."));
|
|
220
|
-
|
|
221
|
-
const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid || options.openclaw || options.pi;
|
|
222
|
-
let sources: SourceType[] | undefined;
|
|
223
|
-
let includeCursor = true;
|
|
224
|
-
if (hasFilter) {
|
|
225
|
-
sources = [];
|
|
226
|
-
if (options.opencode) sources.push("opencode");
|
|
227
|
-
if (options.claude) sources.push("claude");
|
|
228
|
-
if (options.codex) sources.push("codex");
|
|
229
|
-
if (options.gemini) sources.push("gemini");
|
|
230
|
-
if (options.cursor) sources.push("cursor");
|
|
231
|
-
if (options.amp) sources.push("amp");
|
|
232
|
-
if (options.droid) sources.push("droid");
|
|
233
|
-
if (options.openclaw) sources.push("openclaw");
|
|
234
|
-
if (options.pi) sources.push("pi");
|
|
235
|
-
includeCursor = sources.includes("cursor");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Filter out cursor from local sources (it's handled separately via sync)
|
|
239
|
-
const localSources = sources?.filter((s): s is Exclude<SourceType, "cursor"> => s !== "cursor");
|
|
240
|
-
const { sinceTs, untilTs } = getTimestampFilters(options.since, options.until);
|
|
241
|
-
|
|
242
|
-
let data: TokenContributionData;
|
|
243
|
-
try {
|
|
244
|
-
// Two-phase processing (same as TUI) for consistency:
|
|
245
|
-
// Phase 1: Parse local sources + sync cursor in parallel
|
|
246
|
-
const [localMessages, cursorSync] = await Promise.all([
|
|
247
|
-
parseLocalSourcesAsync({
|
|
248
|
-
sources: localSources,
|
|
249
|
-
since: options.since,
|
|
250
|
-
until: options.until,
|
|
251
|
-
year: options.year,
|
|
252
|
-
sinceTs,
|
|
253
|
-
untilTs,
|
|
254
|
-
}),
|
|
255
|
-
includeCursor && isCursorLoggedIn()
|
|
256
|
-
? syncCursorCache()
|
|
257
|
-
: Promise.resolve({ synced: false, rows: 0, error: undefined }),
|
|
258
|
-
]);
|
|
259
|
-
|
|
260
|
-
if (includeCursor && cursorSync.error && (cursorSync.synced || hasCursorUsageCache())) {
|
|
261
|
-
const prefix = cursorSync.synced ? "Cursor sync warning" : "Cursor sync failed; using cached data";
|
|
262
|
-
console.log(pc.yellow(` ${prefix}: ${cursorSync.error}`));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Phase 2: Finalize with pricing (combines local + cursor)
|
|
266
|
-
// Single subprocess call ensures consistent pricing for both report and graph
|
|
267
|
-
const { report, graph } = await finalizeReportAndGraphAsync({
|
|
268
|
-
localMessages,
|
|
269
|
-
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
270
|
-
since: options.since,
|
|
271
|
-
until: options.until,
|
|
272
|
-
year: options.year,
|
|
273
|
-
sinceTs,
|
|
274
|
-
untilTs,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Use graph structure for submission, report's cost for display
|
|
278
|
-
data = graph;
|
|
279
|
-
data.summary.totalCost = report.totalCost;
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.error(pc.red(`\n Error generating data: ${(error as Error).message}\n`));
|
|
282
|
-
process.exit(1);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Step 4: Show summary
|
|
286
|
-
console.log(pc.white(" Data to submit:"));
|
|
287
|
-
console.log(pc.gray(` Date range: ${data.meta.dateRange.start} to ${data.meta.dateRange.end}`));
|
|
288
|
-
console.log(pc.gray(` Active days: ${data.summary.activeDays}`));
|
|
289
|
-
console.log(pc.gray(` Total tokens: ${data.summary.totalTokens.toLocaleString()}`));
|
|
290
|
-
console.log(pc.gray(` Total cost: ${formatCurrency(data.summary.totalCost)}`));
|
|
291
|
-
console.log(pc.gray(` Sources: ${data.summary.sources.join(", ")}`));
|
|
292
|
-
console.log(pc.gray(` Models: ${data.summary.models.length} models`));
|
|
293
|
-
console.log();
|
|
294
|
-
|
|
295
|
-
if (data.summary.totalTokens === 0) {
|
|
296
|
-
console.log(pc.yellow(" No usage data found to submit.\n"));
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Step 5: Dry run check
|
|
301
|
-
if (options.dryRun) {
|
|
302
|
-
console.log(pc.yellow(" Dry run - not submitting data.\n"));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Step 6: Submit to server
|
|
307
|
-
console.log(pc.gray(" Submitting to server..."));
|
|
308
|
-
|
|
309
|
-
const baseUrl = getApiBaseUrl();
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const response = await fetch(`${baseUrl}/api/submit`, {
|
|
313
|
-
method: "POST",
|
|
314
|
-
headers: {
|
|
315
|
-
"Content-Type": "application/json",
|
|
316
|
-
Authorization: `Bearer ${credentials.token}`,
|
|
317
|
-
},
|
|
318
|
-
body: JSON.stringify(data),
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
const result: SubmitResponse = await response.json();
|
|
322
|
-
|
|
323
|
-
if (!response.ok) {
|
|
324
|
-
console.error(pc.red(`\n Error: ${result.error || "Submission failed"}`));
|
|
325
|
-
if (result.details) {
|
|
326
|
-
for (const detail of result.details) {
|
|
327
|
-
console.error(pc.gray(` - ${detail}`));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
console.log();
|
|
331
|
-
process.exit(1);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Success!
|
|
335
|
-
console.log(pc.green("\n Successfully submitted!"));
|
|
336
|
-
console.log();
|
|
337
|
-
console.log(pc.white(" Summary:"));
|
|
338
|
-
console.log(pc.gray(` Submission ID: ${result.submissionId}`));
|
|
339
|
-
console.log(pc.gray(` Total tokens: ${result.metrics?.totalTokens?.toLocaleString()}`));
|
|
340
|
-
console.log(pc.gray(` Total cost: ${formatCurrency(result.metrics?.totalCost || 0)}`));
|
|
341
|
-
console.log(pc.gray(` Active days: ${result.metrics?.activeDays}`));
|
|
342
|
-
console.log();
|
|
343
|
-
console.log(pc.cyan(` View your profile: ${baseUrl}/u/${credentials.username}`));
|
|
344
|
-
console.log();
|
|
345
|
-
|
|
346
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
347
|
-
console.log(pc.yellow(" Warnings:"));
|
|
348
|
-
for (const warning of result.warnings) {
|
|
349
|
-
console.log(pc.gray(` - ${warning}`));
|
|
350
|
-
}
|
|
351
|
-
console.log();
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
console.error(pc.red(`\n Error: Failed to connect to server.`));
|
|
355
|
-
console.error(pc.gray(` ${(error as Error).message}\n`));
|
|
356
|
-
process.exit(1);
|
|
357
|
-
}
|
|
358
|
-
}
|