@tokscale/cli 1.0.24 → 1.1.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/src/submit.ts CHANGED
@@ -10,7 +10,7 @@ import { exec } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  import { loadCredentials, getApiBaseUrl, loadStarCache, saveStarCache } from "./credentials.js";
12
12
  import { parseLocalSourcesAsync, finalizeReportAndGraphAsync, type ParsedMessages } from "./native.js";
13
- import { syncCursorCache, loadCursorCredentials } from "./cursor.js";
13
+ import { syncCursorCache, isCursorLoggedIn, hasCursorUsageCache } from "./cursor.js";
14
14
  import type { TokenContributionData } from "./graph-types.js";
15
15
  import { formatCurrency } from "./table.js";
16
16
 
@@ -179,6 +179,9 @@ async function handleStarPrompt(username: string): Promise<void> {
179
179
  }
180
180
 
181
181
  export async function submit(options: SubmitOptions = {}): Promise<void> {
182
+ const submitStart = performance.now();
183
+ const timing: Record<string, number> = {};
184
+
182
185
  const credentials = loadCredentials();
183
186
  if (!credentials) {
184
187
  console.log(pc.yellow("\n Not logged in."));
@@ -186,7 +189,9 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
186
189
  process.exit(1);
187
190
  }
188
191
 
192
+ let t0 = performance.now();
189
193
  await handleStarPrompt(credentials.username);
194
+ timing["star-prompt"] = performance.now() - t0;
190
195
 
191
196
  console.log(pc.cyan("\n Tokscale - Submit Usage Data\n"));
192
197
 
@@ -214,6 +219,7 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
214
219
  try {
215
220
  // Two-phase processing (same as TUI) for consistency:
216
221
  // Phase 1: Parse local sources + sync cursor in parallel
222
+ t0 = performance.now();
217
223
  const [localMessages, cursorSync] = await Promise.all([
218
224
  parseLocalSourcesAsync({
219
225
  sources: localSources,
@@ -221,20 +227,28 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
221
227
  until: options.until,
222
228
  year: options.year,
223
229
  }),
224
- includeCursor && loadCursorCredentials()
230
+ includeCursor && isCursorLoggedIn()
225
231
  ? syncCursorCache()
226
- : Promise.resolve({ synced: false, rows: 0 }),
232
+ : Promise.resolve({ synced: false, rows: 0, error: undefined }),
227
233
  ]);
234
+ timing["parse-local+cursor-sync"] = performance.now() - t0;
235
+
236
+ if (includeCursor && cursorSync.error && (cursorSync.synced || hasCursorUsageCache())) {
237
+ const prefix = cursorSync.synced ? "Cursor sync warning" : "Cursor sync failed; using cached data";
238
+ console.log(pc.yellow(` ${prefix}: ${cursorSync.error}`));
239
+ }
228
240
 
229
241
  // Phase 2: Finalize with pricing (combines local + cursor)
230
242
  // Single subprocess call ensures consistent pricing for both report and graph
243
+ t0 = performance.now();
231
244
  const { report, graph } = await finalizeReportAndGraphAsync({
232
245
  localMessages,
233
- includeCursor: includeCursor && cursorSync.synced,
246
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
234
247
  since: options.since,
235
248
  until: options.until,
236
249
  year: options.year,
237
250
  });
251
+ timing["finalize-report+graph"] = performance.now() - t0;
238
252
 
239
253
  // Use graph structure for submission, report's cost for display
240
254
  data = graph;
@@ -252,6 +266,8 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
252
266
  console.log(pc.gray(` Total cost: ${formatCurrency(data.summary.totalCost)}`));
253
267
  console.log(pc.gray(` Sources: ${data.summary.sources.join(", ")}`));
254
268
  console.log(pc.gray(` Models: ${data.summary.models.length} models`));
269
+ console.log(pc.gray(` Contributions: ${data.contributions.length} days`));
270
+ console.log(pc.gray(` Payload size: ${(JSON.stringify(data).length / 1024).toFixed(1)} KB`));
255
271
  console.log();
256
272
 
257
273
  if (data.summary.totalTokens === 0) {
@@ -271,16 +287,22 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
271
287
  const baseUrl = getApiBaseUrl();
272
288
 
273
289
  try {
290
+ t0 = performance.now();
291
+ const body = JSON.stringify(data);
292
+ timing["json-serialize"] = performance.now() - t0;
293
+
294
+ t0 = performance.now();
274
295
  const response = await fetch(`${baseUrl}/api/submit`, {
275
296
  method: "POST",
276
297
  headers: {
277
298
  "Content-Type": "application/json",
278
299
  Authorization: `Bearer ${credentials.token}`,
279
300
  },
280
- body: JSON.stringify(data),
301
+ body,
281
302
  });
282
303
 
283
304
  const result: SubmitResponse = await response.json();
305
+ timing["server-roundtrip"] = performance.now() - t0;
284
306
 
285
307
  if (!response.ok) {
286
308
  console.error(pc.red(`\n Error: ${result.error || "Submission failed"}`));
@@ -317,4 +339,14 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
317
339
  console.error(pc.gray(` ${(error as Error).message}\n`));
318
340
  process.exit(1);
319
341
  }
342
+
343
+ // Timing summary
344
+ timing["total"] = performance.now() - submitStart;
345
+ console.log(pc.gray(" ⏱ Timing breakdown:"));
346
+ for (const [label, ms] of Object.entries(timing)) {
347
+ const formatted = ms < 1000 ? `${ms.toFixed(0)}ms` : `${(ms / 1000).toFixed(2)}s`;
348
+ const bar = "█".repeat(Math.max(1, Math.round(ms / timing["total"] * 20)));
349
+ console.log(pc.gray(` ${label.padEnd(24)} ${formatted.padStart(8)} ${bar}`));
350
+ }
351
+ console.log();
320
352
  }
@@ -20,7 +20,7 @@ import {
20
20
  type ParsedMessages,
21
21
  } from "../../native.js";
22
22
 
23
- import { syncCursorCache, loadCursorCredentials } from "../../cursor.js";
23
+ import { syncCursorCache, isCursorLoggedIn, hasCursorUsageCache } from "../../cursor.js";
24
24
  import { getModelColor } from "../utils/colors.js";
25
25
  import { loadCachedData, saveCachedData, isCacheStale, loadSettings } from "../config/settings.js";
26
26
 
@@ -156,7 +156,7 @@ async function loadData(
156
156
  setPhase?.("parsing-sources");
157
157
 
158
158
  const phase1Results = await Promise.allSettled([
159
- includeCursor && loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 }),
159
+ includeCursor && isCursorLoggedIn() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0, error: undefined }),
160
160
  localSources.length > 0
161
161
  ? parseLocalSourcesAsync({ sources: localSources as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid")[], since, until, year })
162
162
  : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 } as ParsedMessages),
@@ -164,11 +164,17 @@ async function loadData(
164
164
 
165
165
  const cursorSync = phase1Results[0].status === "fulfilled"
166
166
  ? phase1Results[0].value
167
- : { synced: false, rows: 0 };
167
+ : { synced: false, rows: 0, error: "Cursor sync failed" };
168
168
  const localMessages = phase1Results[1].status === "fulfilled"
169
169
  ? phase1Results[1].value
170
170
  : null;
171
171
 
172
+ if (includeCursor && cursorSync.error && (cursorSync.synced || hasCursorUsageCache())) {
173
+ // TUI should keep working; just emit a warning.
174
+ const prefix = cursorSync.synced ? "Cursor sync warning" : "Cursor sync failed; using cached data";
175
+ console.warn(`${prefix}: ${cursorSync.error}`);
176
+ }
177
+
172
178
  const emptyMessages: ParsedMessages = {
173
179
  messages: [],
174
180
  opencodeCount: 0,
@@ -184,7 +190,7 @@ async function loadData(
184
190
  // Single call ensures consistent pricing between report and graph
185
191
  const { report, graph } = await finalizeReportAndGraphAsync({
186
192
  localMessages: localMessages || emptyMessages,
187
- includeCursor: includeCursor && cursorSync.synced,
193
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
188
194
  since,
189
195
  until,
190
196
  year,
package/src/wrapped.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  finalizeReportAndGraphAsync,
10
10
  type ParsedMessages,
11
11
  } from "./native.js";
12
- import { syncCursorCache, loadCursorCredentials } from "./cursor.js";
12
+ import { syncCursorCache, isCursorLoggedIn, hasCursorUsageCache } from "./cursor.js";
13
13
  import { loadCredentials } from "./credentials.js";
14
14
  import type { SourceType } from "./graph-types.js";
15
15
 
@@ -218,7 +218,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
218
218
  const until = `${year}-12-31`;
219
219
 
220
220
  const phase1Results = await Promise.allSettled([
221
- includeCursor && loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 }),
221
+ includeCursor && isCursorLoggedIn() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0, error: undefined }),
222
222
  localSources.length > 0
223
223
  ? parseLocalSourcesAsync({ sources: localSources, since, until, year })
224
224
  : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 } as ParsedMessages),
@@ -226,11 +226,16 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
226
226
 
227
227
  const cursorSync = phase1Results[0].status === "fulfilled"
228
228
  ? phase1Results[0].value
229
- : { synced: false, rows: 0 };
229
+ : { synced: false, rows: 0, error: "Cursor sync failed" };
230
230
  const localMessages = phase1Results[1].status === "fulfilled"
231
231
  ? phase1Results[1].value
232
232
  : null;
233
233
 
234
+ if (includeCursor && cursorSync.error && (cursorSync.synced || hasCursorUsageCache())) {
235
+ const prefix = cursorSync.synced ? "Cursor sync warning" : "Cursor sync failed; using cached data";
236
+ console.log(pc.yellow(` ${prefix}: ${cursorSync.error}`));
237
+ }
238
+
234
239
  const emptyMessages: ParsedMessages = {
235
240
  messages: [],
236
241
  opencodeCount: 0,
@@ -244,7 +249,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
244
249
 
245
250
  const { report, graph } = await finalizeReportAndGraphAsync({
246
251
  localMessages: localMessages || emptyMessages,
247
- includeCursor: includeCursor && cursorSync.synced,
252
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
248
253
  since,
249
254
  until,
250
255
  year,