taskchef 7.28.1 → 7.29.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.
@@ -19,6 +19,20 @@ vi.mock('@mantine/core', async () => {
19
19
 
20
20
  afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.clearAllMocks(); });
21
21
 
22
+ test('shows the version reported by the active usage provider', async () => {
23
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
24
+ ok: true,
25
+ json: async () => ({
26
+ profiles: [],
27
+ usageProvider: { provider: 'ccusage', status: 'available', version: '20.0.21' },
28
+ }),
29
+ }));
30
+ render(<MantineProvider><ModelSettings /></MantineProvider>);
31
+ expect(await screen.findByText('ccusage 20.0.21')).toBeInTheDocument();
32
+ expect(screen.getByText('Usage provider')).toBeInTheDocument();
33
+ expect(screen.getByText(/Version reported by the executable/)).toBeInTheDocument();
34
+ });
35
+
22
36
  test('refresh shows configuration failures and does not label them honored', async () => {
23
37
  const role = { role: 'reviewer', source: '/fixture/reviewer.toml', effectiveSource: '/fixture/reviewer.toml', model: 'fixture', effort: 'low', status: 'configured', availability: 'not checked', fallback: 'inherit parent settings', problems: [] as string[] };
24
38
  const profile = { id: 'personal', project: 'Personal', roles: [role], problems: [], catalogSource: null };
@@ -6,9 +6,11 @@ import { updateModelRole } from '../api';
6
6
  type ModelOption = { value: string; label: string; efforts: string[] };
7
7
  type Role = { role: string; source: string | null; displaySource?: string | null; model: unknown; effort: unknown; status: string; problems: string[] };
8
8
  type Profile = { id: string; roles: Role[]; problems: string[]; modelOptions?: ModelOption[] };
9
+ type UsageProvider = { provider: string; status: 'available' | 'unavailable'; version: string | null };
9
10
 
10
11
  export function ModelSettings() {
11
12
  const [profiles, setProfiles] = useState<Profile[]>([]);
13
+ const [usageProvider, setUsageProvider] = useState<UsageProvider | null>(null);
12
14
  const [error, setError] = useState<string | null>(null);
13
15
  const [loading, setLoading] = useState(true);
14
16
  const [saving, setSaving] = useState<string | null>(null);
@@ -20,8 +22,9 @@ export function ModelSettings() {
20
22
  setError(null);
21
23
  fetch('/api/settings', { signal: controller.signal }).then(async (response) => {
22
24
  if (!response.ok) throw new Error('Settings preview could not be loaded.');
23
- const data = await response.json() as { profiles: Profile[] };
25
+ const data = await response.json() as { profiles: Profile[]; usageProvider?: UsageProvider };
24
26
  setProfiles(data.profiles);
27
+ setUsageProvider(data.usageProvider ?? null);
25
28
  }).catch((failure: Error) => {
26
29
  if (!controller.signal.aborted) setError(failure.message);
27
30
  }).finally(() => { if (!controller.signal.aborted) setLoading(false); });
@@ -87,5 +90,15 @@ export function ModelSettings() {
87
90
  </Stack>
88
91
  </Paper>)}
89
92
  </>}
93
+ {usageProvider && <Paper withBorder p="md">
94
+ <Stack gap="xs">
95
+ <Group justify="space-between">
96
+ <Title order={3} size="h4">Usage provider</Title>
97
+ <Badge color={usageProvider.status === 'available' ? 'teal' : 'red'}>{usageProvider.status}</Badge>
98
+ </Group>
99
+ <Text>{usageProvider.provider}{usageProvider.version ? ` ${usageProvider.version}` : ' version unavailable'}</Text>
100
+ <Text c="dimmed" size="sm">Version reported by the executable TaskChef uses for token and cost estimates.</Text>
101
+ </Stack>
102
+ </Paper>}
90
103
  </Stack>;
91
104
  }
package/src/dashboard.js CHANGED
@@ -27,7 +27,7 @@ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
27
27
  import { taskGitHubProjection } from "./dashboard/github-links.js";
28
28
  import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
29
29
  import { createUsageTracker } from "./usage-tracker.js";
30
- import { readUsageStore, usageStorePath } from "./usage.js";
30
+ import { readCcusageRuntimeInfo, readUsageStore, usageStorePath } from "./usage.js";
31
31
  import { reportedWorkSummary } from "./reported-work.js";
32
32
  import {
33
33
  MAX_DASHBOARD_SESSION_PIDS,
@@ -804,7 +804,9 @@ export async function createDashboardServer({
804
804
  serverVersion = DASHBOARD_SERVER_VERSION,
805
805
  usageTracker = null,
806
806
  usageSummaryMonitor = null,
807
+ usagePreloadIntervalMs = 30_000,
807
808
  resolveRoles = resolveModelRoles,
809
+ resolveUsageProvider = readCcusageRuntimeInfo,
808
810
  updateRole = updateModelRole,
809
811
  control = null,
810
812
  controlReplayCache = createDashboardControlReplayCache(),
@@ -818,6 +820,9 @@ export async function createDashboardServer({
818
820
  if (!Number.isInteger(maxEventClients) || maxEventClients < 0) {
819
821
  throw new Error("dashboard event-client limit must be a non-negative integer");
820
822
  }
823
+ if (!Number.isFinite(usagePreloadIntervalMs) || usagePreloadIntervalMs < 1_000) {
824
+ throw new Error("dashboard usage preload interval must be at least one second");
825
+ }
821
826
  if (!new Set(["mcp", "session", "standalone"]).has(launcher)) {
822
827
  throw new Error("dashboard launcher must be mcp, session, or standalone");
823
828
  }
@@ -851,11 +856,13 @@ export async function createDashboardServer({
851
856
  }
852
857
  } catch (error) {
853
858
  taskUsageSummaryMonitor?.close();
859
+ taskUsageTracker?.close?.();
854
860
  monitor.close();
855
861
  throw error;
856
862
  }
857
863
  const clients = new Set();
858
864
  const archiveRequests = new Set();
865
+ let usageProviderPromise = null;
859
866
  let controlHandoff = null;
860
867
  let controlHandoffTimer = null;
861
868
  let allowedAuthority;
@@ -875,12 +882,21 @@ export async function createDashboardServer({
875
882
  })),
876
883
  };
877
884
  };
878
- const snapshotListener = () => broadcast("snapshot", dashboardSnapshot());
885
+ const preloadUsage = () => {
886
+ void Promise.resolve(taskUsageTracker.preload?.(monitor.tasks)).catch(() => {});
887
+ };
888
+ const snapshotListener = () => {
889
+ preloadUsage();
890
+ broadcast("snapshot", dashboardSnapshot());
891
+ };
879
892
  const usageSummaryListener = () => broadcast("snapshot", dashboardSnapshot());
880
893
  const errorListener = () => broadcast("dashboard-error", publicMonitorError());
881
894
  monitor.on("snapshot", snapshotListener);
882
895
  monitor.on("monitorError", errorListener);
883
896
  taskUsageSummaryMonitor.on("change", usageSummaryListener);
897
+ preloadUsage();
898
+ const usagePreloadTimer = setInterval(preloadUsage, usagePreloadIntervalMs);
899
+ usagePreloadTimer.unref?.();
884
900
 
885
901
  const handleRequest = async (request, response) => {
886
902
  const method = request.method ?? "GET";
@@ -1183,7 +1199,12 @@ export async function createDashboardServer({
1183
1199
 
1184
1200
  if (url.pathname === "/api/settings" && method === "GET") {
1185
1201
  const profiles = [{ id: "personal", project: "Personal", ...await resolveRoles({ includeCatalog: true }) }];
1186
- sendJson(response, 200, { profiles });
1202
+ usageProviderPromise ??= Promise.resolve(resolveUsageProvider()).catch(() => ({
1203
+ provider: "ccusage",
1204
+ status: "unavailable",
1205
+ version: null,
1206
+ }));
1207
+ sendJson(response, 200, { profiles, usageProvider: await usageProviderPromise });
1187
1208
  return;
1188
1209
  }
1189
1210
 
@@ -1469,8 +1490,10 @@ export async function createDashboardServer({
1469
1490
  server.once("error", reject);
1470
1491
  server.listen(port, host, resolve);
1471
1492
  }).catch((error) => {
1493
+ clearInterval(usagePreloadTimer);
1472
1494
  monitor.close();
1473
1495
  taskUsageSummaryMonitor.close();
1496
+ taskUsageTracker.close?.();
1474
1497
  throw error;
1475
1498
  });
1476
1499
 
@@ -1493,6 +1516,8 @@ export async function createDashboardServer({
1493
1516
  taskUsageSummaryMonitor.off("change", usageSummaryListener);
1494
1517
  monitor.close();
1495
1518
  taskUsageSummaryMonitor.close();
1519
+ taskUsageTracker.close?.();
1520
+ clearInterval(usagePreloadTimer);
1496
1521
  for (const client of clients) client.close();
1497
1522
  const closed = new Promise((resolve, reject) => server.close((error) =>
1498
1523
  error ? reject(error) : resolve()));
@@ -17,6 +17,10 @@ function isAdministrativeTurn(turn) {
17
17
  return turn?.provenance?.kind === "dashboard_manual";
18
18
  }
19
19
 
20
+ function isInterruptedTurn(turn) {
21
+ return turn?.result?.status === "interrupted";
22
+ }
23
+
20
24
  function hasTerminalLatestTurn(task) {
21
25
  return task.latestTurn !== null
22
26
  && task.latestTurn !== undefined
@@ -128,6 +132,19 @@ function administrativeTask(task, existing = null, now = new Date().toISOString(
128
132
  };
129
133
  }
130
134
 
135
+ function interruptedTask(task, existing = null, now = new Date().toISOString()) {
136
+ const usage = calculatingTask(task, existing, now);
137
+ const hasTaskProjection = existing?.task !== null && existing?.task !== undefined;
138
+ return {
139
+ ...usage,
140
+ status: hasTaskProjection ? "available" : "unavailable",
141
+ ...(hasTaskProjection ? {} : { reason: "The turn ended without a terminal usage boundary." }),
142
+ updatedAt: now,
143
+ retryAfter: null,
144
+ task: existing?.task ?? null,
145
+ };
146
+ }
147
+
131
148
  async function updateStore(workspace, taskId, transform) {
132
149
  const release = await acquireWorkspaceLock(workspace);
133
150
  try {
@@ -167,6 +184,47 @@ function snapshotSupersedes(current, incoming) {
167
184
  ].every((field) => incoming[field] >= current[field]);
168
185
  }
169
186
 
187
+ function snapshotCorrects(current, incoming) {
188
+ if (!current) return false;
189
+ const currentSampledAt = Date.parse(current.sampledAt ?? 0);
190
+ const incomingSampledAt = Date.parse(incoming.sampledAt ?? 0);
191
+ return Number.isFinite(incomingSampledAt)
192
+ && incomingSampledAt >= currentSampledAt
193
+ && [
194
+ "inputTokens",
195
+ "cachedInputTokens",
196
+ "outputTokens",
197
+ "reasoningOutputTokens",
198
+ "totalTokens",
199
+ ].some((field) => incoming[field] < current[field]);
200
+ }
201
+
202
+ function correctedRecord(task, snapshot) {
203
+ const now = snapshot.sampledAt;
204
+ const version = snapshot.provenance.version ? ` ${snapshot.provenance.version}` : "";
205
+ const reason = `ccusage${version} corrected an older cumulative total; reliable per-turn usage is unavailable.`;
206
+ const turns = Object.fromEntries(task.turns.slice(-MAX_TRACKED_TURNS).map((turn) => [
207
+ turn.turnRef,
208
+ isAdministrativeTurn(turn)
209
+ ? { status: "unavailable", reason: ADMINISTRATIVE_USAGE_REASON, updatedAt: now }
210
+ : { status: "unavailable", reason, updatedAt: now },
211
+ ]));
212
+ const generation = lifecycleGeneration(task);
213
+ return {
214
+ threadId: task.threadId,
215
+ generationTurnRef: task.latestTurn?.turnRef ?? null,
216
+ generationTurnCount: generation.turnCount,
217
+ generationTerminal: generation.terminal,
218
+ zeroBaselineTurnRef: null,
219
+ status: "available",
220
+ updatedAt: now,
221
+ retryAfter: null,
222
+ task: snapshot,
223
+ turns,
224
+ boundaries: { [task.latestTurn.turnRef]: snapshot },
225
+ };
226
+ }
227
+
170
228
  function candidateHasAdvanced(task, existing, snapshot) {
171
229
  const latestIndex = task.turns.findIndex((turn) => turn.turnRef === task.latestTurn?.turnRef);
172
230
  if (latestIndex <= 0) return true;
@@ -269,14 +327,22 @@ function reconcileRecord(task, existing, snapshot, { boundaryReliable = true } =
269
327
 
270
328
  export function createUsageTracker({
271
329
  workspace,
330
+ maxConcurrentJobs = 2,
272
331
  readThreadUsage = readCcusageThreadUsage,
273
332
  retryDelaysMs = [2_000, 3_000, 4_000, 1_000],
274
333
  retryCooldownMs = 60_000,
275
334
  setTimer = setTimeout,
276
335
  } = {}) {
336
+ if (!Number.isSafeInteger(maxConcurrentJobs) || maxConcurrentJobs < 1 || maxConcurrentJobs > 8) {
337
+ throw new Error("usage tracker concurrency must be an integer from 1 to 8");
338
+ }
277
339
  const jobs = new Map();
278
340
  const observationChains = new Map();
341
+ const readyJobs = [];
342
+ let activeJobs = 0;
279
343
  let canonicalWorkspace = null;
344
+ let closed = false;
345
+ let preloadChain = Promise.resolve();
280
346
 
281
347
  const root = async () => {
282
348
  canonicalWorkspace ??= await realpath(path.resolve(workspace));
@@ -289,6 +355,9 @@ export function createUsageTracker({
289
355
  const markAdministrative = async (task) => updateStore(await root(), task.id, (existing) => (
290
356
  generationIsOlder(task, existing) ? existing : administrativeTask(task, existing)
291
357
  ));
358
+ const markInterrupted = async (task) => updateStore(await root(), task.id, (existing) => (
359
+ generationIsOlder(task, existing) ? existing : interruptedTask(task, existing)
360
+ ));
292
361
 
293
362
  const reconcile = async (task, {
294
363
  finalAttempt = false,
@@ -332,6 +401,9 @@ export function createUsageTracker({
332
401
  return updateStore(await root(), task.id, (existing) => {
333
402
  const latestTurnRef = task.latestTurn?.turnRef;
334
403
  if (existing?.generationTurnRef !== latestTurnRef) return existing;
404
+ if (stable && snapshotCorrects(existing?.task, snapshot)) {
405
+ return correctedRecord(task, snapshot);
406
+ }
335
407
  const supersedes = snapshotSupersedes(existing?.task, snapshot);
336
408
  if (existing?.boundaries?.[latestTurnRef]
337
409
  || existing?.turns?.[latestTurnRef]?.status === "available") {
@@ -344,15 +416,49 @@ export function createUsageTracker({
344
416
  });
345
417
  };
346
418
 
419
+ const drainReadyJobs = () => {
420
+ while (!closed && activeJobs < maxConcurrentJobs && readyJobs.length > 0) {
421
+ const job = readyJobs.shift();
422
+ job.queued = false;
423
+ if (job.cancelled || jobs.get(job.task.id) !== job) {
424
+ for (const resolve of job.waiters.splice(0)) resolve();
425
+ continue;
426
+ }
427
+ const waiters = job.waiters.splice(0);
428
+ activeJobs += 1;
429
+ void job.run().finally(() => {
430
+ activeJobs -= 1;
431
+ for (const resolve of waiters) resolve();
432
+ drainReadyJobs();
433
+ });
434
+ }
435
+ };
436
+
437
+ const queueJob = (job) => {
438
+ if (closed || job.cancelled || jobs.get(job.task.id) !== job) return Promise.resolve();
439
+ return new Promise((resolve) => {
440
+ job.waiters.push(resolve);
441
+ if (!job.queued) {
442
+ job.queued = true;
443
+ readyJobs.push(job);
444
+ drainReadyJobs();
445
+ }
446
+ });
447
+ };
448
+
347
449
  const schedule = (task, { immediate = false } = {}) => {
348
- if (!task.threadId || jobs.has(task.id)) return;
450
+ if (closed || !task.threadId || jobs.has(task.id)) return;
349
451
  const job = {
350
452
  cancelled: false,
351
453
  previousFingerprint: null,
454
+ queued: false,
455
+ run: null,
456
+ task,
352
457
  turnRef: task.latestTurn?.turnRef ?? null,
458
+ waiters: [],
353
459
  };
354
460
  let attempt = 0;
355
- const run = async () => {
461
+ job.run = async () => {
356
462
  if (job.cancelled || jobs.get(task.id) !== job) return;
357
463
  let complete = false;
358
464
  try {
@@ -372,36 +478,89 @@ export function createUsageTracker({
372
478
  }
373
479
  const delay = retryDelaysMs[attempt] ?? 0;
374
480
  attempt += 1;
375
- const timer = setTimer(run, delay);
481
+ const timer = setTimer(() => queueJob(job), delay);
376
482
  timer?.unref?.();
377
483
  };
378
484
  jobs.set(task.id, job);
379
- const timer = setTimer(run, immediate ? 0 : retryDelaysMs[attempt++]);
485
+ const timer = setTimer(() => queueJob(job), immediate ? 0 : retryDelaysMs[attempt++]);
380
486
  timer?.unref?.();
381
487
  };
382
488
 
489
+ const observe = (task) => {
490
+ if (closed) return Promise.resolve(null);
491
+ const previous = observationChains.get(task.id) ?? Promise.resolve();
492
+ const observation = previous.catch(() => {}).then(async () => {
493
+ if (closed || !task.threadId) return null;
494
+ const active = jobs.get(task.id);
495
+ const latestTurnRef = task.latestTurn?.turnRef ?? null;
496
+ if (active && active.turnRef !== latestTurnRef) {
497
+ active.cancelled = true;
498
+ jobs.delete(task.id);
499
+ }
500
+ const usage = isAdministrativeTurn(task.latestTurn)
501
+ ? await markAdministrative(task)
502
+ : isInterruptedTurn(task.latestTurn)
503
+ ? await markInterrupted(task)
504
+ : await markCalculating(task);
505
+ if (hasTerminalLatestTurn(task)) schedule(task);
506
+ return usage;
507
+ });
508
+ observationChains.set(task.id, observation);
509
+ void observation.finally(() => {
510
+ if (observationChains.get(task.id) === observation) observationChains.delete(task.id);
511
+ }).catch(() => {});
512
+ return observation;
513
+ };
514
+
515
+ const needsPreload = (task, usage, now = Date.now()) => {
516
+ if (!task.threadId || !task.latestTurn) return false;
517
+ const latestTurnRef = task.latestTurn.turnRef ?? null;
518
+ if (isAdministrativeTurn(task.latestTurn)) {
519
+ return !usage
520
+ || usage.threadId !== task.threadId
521
+ || usage.generationTurnRef !== latestTurnRef;
522
+ }
523
+ if (isInterruptedTurn(task.latestTurn)) {
524
+ const latestUsage = usage?.turns?.[latestTurnRef];
525
+ const expectedStatus = usage?.task ? "available" : "unavailable";
526
+ return !usage
527
+ || usage.threadId !== task.threadId
528
+ || usage.generationTurnRef !== latestTurnRef
529
+ || usage.status !== expectedStatus
530
+ || latestUsage?.status !== "unavailable";
531
+ }
532
+ if (!usage || usage.threadId !== task.threadId || usage.generationTurnRef !== latestTurnRef) {
533
+ return true;
534
+ }
535
+ if (!hasTerminalLatestTurn(task)) return false;
536
+ if (usage.status === "available") return false;
537
+ if (usage.status === "calculating") return true;
538
+ if (usage.status === "unavailable") {
539
+ return !usage.retryAfter || Date.parse(usage.retryAfter) <= now;
540
+ }
541
+ return false;
542
+ };
543
+
383
544
  return {
384
- observe(task) {
385
- const previous = observationChains.get(task.id) ?? Promise.resolve();
386
- const observation = previous.catch(() => {}).then(async () => {
387
- if (!task.threadId) return null;
388
- const active = jobs.get(task.id);
389
- const latestTurnRef = task.latestTurn?.turnRef ?? null;
390
- if (active && active.turnRef !== latestTurnRef) {
391
- active.cancelled = true;
392
- jobs.delete(task.id);
545
+ close() {
546
+ closed = true;
547
+ readyJobs.length = 0;
548
+ for (const job of jobs.values()) {
549
+ job.cancelled = true;
550
+ for (const resolve of job.waiters.splice(0)) resolve();
551
+ }
552
+ jobs.clear();
553
+ },
554
+ observe,
555
+ preload(tasks) {
556
+ preloadChain = preloadChain.catch(() => {}).then(async () => {
557
+ if (closed) return;
558
+ const store = await readUsageStore(await root());
559
+ for (const task of tasks) {
560
+ if (needsPreload(task, store.tasks[task.id] ?? null)) await observe(task);
393
561
  }
394
- const usage = isAdministrativeTurn(task.latestTurn)
395
- ? await markAdministrative(task)
396
- : await markCalculating(task);
397
- if (hasTerminalLatestTurn(task)) schedule(task);
398
- return usage;
399
562
  });
400
- observationChains.set(task.id, observation);
401
- void observation.finally(() => {
402
- if (observationChains.get(task.id) === observation) observationChains.delete(task.id);
403
- }).catch(() => {});
404
- return observation;
563
+ return preloadChain;
405
564
  },
406
565
  async get(task) {
407
566
  const store = await readUsageStore(await root());
@@ -433,6 +592,16 @@ export function createUsageTracker({
433
592
  void markAdministrative(task).catch(() => {});
434
593
  return administrative;
435
594
  }
595
+ if (isInterruptedTurn(task.latestTurn)) {
596
+ const latestUsage = usage?.turns?.[task.latestTurn.turnRef];
597
+ const expectedStatus = usage?.task ? "available" : "unavailable";
598
+ if (usage?.generationTurnRef === task.latestTurn.turnRef
599
+ && usage.status === expectedStatus
600
+ && latestUsage?.status === "unavailable") return usage;
601
+ const interrupted = interruptedTask(task, usage);
602
+ void markInterrupted(task).catch(() => {});
603
+ return interrupted;
604
+ }
436
605
  if (!usage || usage.threadId !== task.threadId) {
437
606
  const calculating = calculatingTask(task);
438
607
  void markCalculating(task)
package/src/usage.js CHANGED
@@ -538,18 +538,7 @@ export async function readCcusageThreadUsage(threadId, {
538
538
  [...resolvedInvocation.args, ...args],
539
539
  options,
540
540
  );
541
- let version = resolvedInvocation.version;
542
- if (version === null) {
543
- try {
544
- const result = await invoke(["--version"], {
545
- timeout: Math.min(timeoutMs, 2_000),
546
- maxBuffer: 64 * 1024,
547
- });
548
- version = String(result.stdout).trim().replace(/^ccusage\s+/i, "") || null;
549
- } catch {
550
- // Usage remains useful if a custom compatible ccusage cannot print its version.
551
- }
552
- }
541
+ const version = await readCcusageInvocationVersion(invoke, timeoutMs);
553
542
  const report = async (pricingMode) => {
554
543
  let result;
555
544
  try {
@@ -589,6 +578,45 @@ export async function readCcusageThreadUsage(threadId, {
589
578
  }
590
579
  }
591
580
 
581
+ async function readCcusageInvocationVersion(invoke, timeoutMs) {
582
+ try {
583
+ const result = await invoke(["--version"], {
584
+ timeout: Math.min(timeoutMs, 2_000),
585
+ maxBuffer: 64 * 1024,
586
+ });
587
+ const version = String(result.stdout).trim().replace(/^ccusage\s+/i, "");
588
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(version) ? version : null;
589
+ } catch {
590
+ return null;
591
+ }
592
+ }
593
+
594
+ export async function readCcusageRuntimeInfo({
595
+ run = runBoundedProcess,
596
+ timeoutMs = 8_000,
597
+ } = {}) {
598
+ try {
599
+ const resolved = await resolveManagedCcusageInvocation(
600
+ managedCcusageInvocation(),
601
+ run,
602
+ timeoutMs,
603
+ );
604
+ const invoke = (args, options) => run(
605
+ resolved.command,
606
+ [...resolved.args, ...args],
607
+ options,
608
+ );
609
+ const version = await readCcusageInvocationVersion(invoke, timeoutMs);
610
+ return {
611
+ provider: "ccusage",
612
+ status: version === null ? "unavailable" : "available",
613
+ version,
614
+ };
615
+ } catch {
616
+ return { provider: "ccusage", status: "unavailable", version: null };
617
+ }
618
+ }
619
+
592
620
  export function usageStorePath(workspace) {
593
621
  return path.join(workspace, USAGE_FILE_NAME);
594
622
  }