@trazum/cli 1.40.0 → 1.42.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/index.ts CHANGED
@@ -11,8 +11,18 @@ import {
11
11
  cacheableMinimum,
12
12
  analyzeCachePrefix,
13
13
  billLevers,
14
+ bucketedCacheEconomics,
15
+ bucketedProfile,
14
16
  buildHistory,
15
17
  buildPlan,
18
+ connectorFor,
19
+ CONNECTORS,
20
+ normalizeAnthropicUsage,
21
+ normalizeOpenAIUsage,
22
+ bucketsFromRecords,
23
+ pruneRecords,
24
+ recordsFromBuckets,
25
+ storeInventory,
16
26
  storedReportFrom,
17
27
  verifyPlan,
18
28
  cacheEconomics,
@@ -80,6 +90,7 @@ import {
80
90
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
81
91
  import { dayOf, formatGap, median, spanDays } from './time.js';
82
92
  import type {
93
+ BucketedReport,
83
94
  FleetSource,
84
95
  HistoryRun,
85
96
  MeasuredUsage,
@@ -133,6 +144,8 @@ import {
133
144
  revisionsFor,
134
145
  } from './git.js';
135
146
  import type { Revision } from './git.js';
147
+ import { fetchProviderUsage } from './connect.js';
148
+ import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
136
149
  import { detectLocale, getCliMessages } from './i18n/index.js';
137
150
  import {
138
151
  MAX_SUMMARY_CHARS,
@@ -181,6 +194,8 @@ const VALUE_FLAGS = new Set([
181
194
  'against',
182
195
  'from-log',
183
196
  'min-usd',
197
+ 'payload',
198
+ 'keep',
184
199
  // `route` takes a path here, and the flag is deliberately not `--prompt`:
185
200
  // everywhere else in this tool `--prompt` names a marked prompt *inside* a
186
201
  // source file, and reusing it for a path would be a trap laid for the reader.
@@ -483,7 +498,9 @@ const COMMAND_FLAGS: Record<string, string[]> = {
483
498
  profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd', 'label', 'since', 'until', 'dry-run', 'markdown-summary', 'by-source'],
484
499
  plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
485
500
  verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
486
- history: ['json', 'markdown-out'],
501
+ history: ['store', 'json', 'markdown-out'],
502
+ connect: ['since', 'until', 'payload', 'store', 'json', 'out', 'markdown-out', 'pricing', 'pricing-live', 'dry-run'],
503
+ store: ['prune', 'keep', 'json', 'pricing', 'pricing-live', 'dry-run'],
487
504
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
488
505
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
489
506
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -2223,6 +2240,381 @@ function isoDate(): string {
2223
2240
  * metered API calls somebody was actually billed for — the bill exists wherever
2224
2241
  * Trazum happens to be running, so the host has no bearing on it.
2225
2242
  */
2243
+ /**
2244
+ * One end of a time window, from a flag.
2245
+ *
2246
+ * A UTC day (`2026-08-14`), a full ISO 8601 timestamp, a relative window
2247
+ * (`7d`, `24h`) or `now`. A bare day means the whole of it — since its first
2248
+ * instant, until its last — because `--until 2026-08-14` excluding the named
2249
+ * day is a trap sprung on everyone who reads dates the way humans do.
2250
+ *
2251
+ * `relative` comes back so the caller can state the caveat: a relative window
2252
+ * is measured against **the machine's clock, not the data's**, and a log
2253
+ * exported last month answers `--since 7d` with nothing.
2254
+ */
2255
+ function parseWhen(
2256
+ args: Args,
2257
+ flag: string,
2258
+ endOfDay: boolean,
2259
+ t: CliMessages,
2260
+ now: number,
2261
+ ): { ms: number | undefined; relative: boolean } {
2262
+ const value = stringFlag(args, flag);
2263
+ if (value === undefined) return { ms: undefined, relative: false };
2264
+
2265
+ const relative = /^(\d+)([dh])$/.exec(value);
2266
+ if (relative) {
2267
+ const amount = Number(relative[1]);
2268
+ if (amount > 0) {
2269
+ const span = relative[2] === 'd' ? 86_400_000 : 3_600_000;
2270
+ return { ms: now - amount * span, relative: true };
2271
+ }
2272
+ }
2273
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
2274
+ const midnight = Date.parse(`${value}T00:00:00Z`);
2275
+ if (Number.isFinite(midnight)) {
2276
+ return { ms: endOfDay ? midnight + 86_400_000 : midnight, relative: false };
2277
+ }
2278
+ }
2279
+ if (value === 'now') return { ms: now, relative: false };
2280
+ const exact = Date.parse(value);
2281
+ if (Number.isFinite(exact)) return { ms: exact, relative: false };
2282
+ throw new Error(t.profile.badWhen(flag, value));
2283
+ }
2284
+
2285
+ /**
2286
+ * `trazum store` — what is kept, and what a prune would take.
2287
+ *
2288
+ * The store is the one thing in this product that *deletes* something, so the
2289
+ * errands around it are written to make that visible: the inventory says what
2290
+ * is there and how far back, and `--prune` names what went with the span it
2291
+ * covered. Retention with no policy written down is refused rather than
2292
+ * defaulted — deleting measurements on a guess is not something anybody
2293
+ * should receive by accident.
2294
+ */
2295
+ async function commandStore(
2296
+ args: Args,
2297
+ config: TrazumConfig,
2298
+ pricing: PricingCatalogue,
2299
+ t: CliMessages,
2300
+ ): Promise<void> {
2301
+ const root = process.cwd();
2302
+ const { resolved, unreadable, files } = await readStore(root);
2303
+ const inventory = storeInventory(resolved);
2304
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
2305
+ const day = (msValue: number): string => new Date(msValue).toISOString().slice(0, 10);
2306
+
2307
+ const priced = bucketedProfile(
2308
+ {
2309
+ provider: 'store',
2310
+ granularity: 'bucketed',
2311
+ buckets: bucketsFromRecords(resolved.records),
2312
+ window: inventory.span,
2313
+ gaps: [],
2314
+ unavailable: [],
2315
+ },
2316
+ { catalogue: pricing },
2317
+ );
2318
+
2319
+ if (boolFlag(args, 'prune')) {
2320
+ const keepFlag = stringFlag(args, 'keep');
2321
+ const keepDays = keepFlag !== undefined
2322
+ ? Number(/^(\d+)d?$/.exec(keepFlag)?.[1] ?? NaN)
2323
+ : config.store?.keepDays;
2324
+ if (keepDays === undefined || !Number.isFinite(keepDays) || keepDays <= 0) {
2325
+ throw new Error(t.store.pruneNeedsPolicy());
2326
+ }
2327
+ const cutoff = Date.now() - keepDays * 86_400_000;
2328
+ const result = pruneRecords(resolved.records, cutoff);
2329
+ const droppedUsd = bucketedProfile(
2330
+ {
2331
+ provider: 'store',
2332
+ granularity: 'bucketed',
2333
+ buckets: bucketsFromRecords(result.dropped),
2334
+ window: null,
2335
+ gaps: [],
2336
+ unavailable: [],
2337
+ },
2338
+ { catalogue: pricing },
2339
+ ).total.totalUsd;
2340
+
2341
+ if (boolFlag(args, 'dry-run')) {
2342
+ console.log(
2343
+ wrap(
2344
+ t.store.pruneDryRun(
2345
+ n(result.dropped.length),
2346
+ String(keepDays),
2347
+ result.droppedSpan === null
2348
+ ? null
2349
+ : `${day(result.droppedSpan.fromMs)} → ${day(result.droppedSpan.toMs)}`,
2350
+ formatUsd(droppedUsd),
2351
+ ),
2352
+ 76,
2353
+ ' ',
2354
+ ),
2355
+ );
2356
+ return;
2357
+ }
2358
+
2359
+ // The prune also collapses the append log to what the store resolves to,
2360
+ // which is the only moment a rewrite is safe: it is what the reader was
2361
+ // already seeing.
2362
+ await rewriteStore(root, result.kept);
2363
+ console.log(
2364
+ wrap(
2365
+ t.store.pruned(
2366
+ n(result.dropped.length),
2367
+ String(keepDays),
2368
+ result.droppedSpan === null
2369
+ ? null
2370
+ : `${day(result.droppedSpan.fromMs)} → ${day(result.droppedSpan.toMs)}`,
2371
+ formatUsd(droppedUsd),
2372
+ n(result.kept.length),
2373
+ ),
2374
+ 76,
2375
+ ' ',
2376
+ ),
2377
+ );
2378
+ return;
2379
+ }
2380
+
2381
+ if (boolFlag(args, 'json')) {
2382
+ console.log(JSON.stringify({ ...inventory, totalUsd: priced.total.totalUsd, unreadable }, null, 2));
2383
+ return;
2384
+ }
2385
+
2386
+ /**
2387
+ * Empty means *nothing at all* — not "nothing I could resolve".
2388
+ *
2389
+ * Records the store could not tell apart, lines it could not parse and
2390
+ * records from a newer schema are all real measurements sitting on disk.
2391
+ * Reporting an empty store over them would hide exactly what the reader
2392
+ * needs to see, which is the failure this whole module is written against.
2393
+ */
2394
+ const nothingAtAll =
2395
+ inventory.totalRecords === 0 &&
2396
+ inventory.possiblyDouble === 0 &&
2397
+ inventory.unknownVersion === 0 &&
2398
+ unreadable.length === 0;
2399
+ if (nothingAtAll) {
2400
+ console.log(wrap(t.store.empty(STORE_DIR), 76, ' '));
2401
+ return;
2402
+ }
2403
+
2404
+ console.log(
2405
+ c.bold(
2406
+ t.store.heading(
2407
+ n(inventory.totalRecords),
2408
+ formatUsd(priced.total.totalUsd),
2409
+ inventory.span === null ? '' : day(inventory.span.fromMs),
2410
+ inventory.span === null ? '' : day(inventory.span.toMs),
2411
+ ),
2412
+ ),
2413
+ );
2414
+ for (const provider of inventory.providers) {
2415
+ console.log(
2416
+ ` ${t.store.providerRow(
2417
+ provider.provider,
2418
+ n(provider.records),
2419
+ provider.span === null ? '' : `${day(provider.span.fromMs)} → ${day(provider.span.toMs)}`,
2420
+ n(provider.models.length),
2421
+ )}`,
2422
+ );
2423
+ }
2424
+ console.log();
2425
+ console.log(` ${c.dim(wrap(t.store.holds(n(files.length)), 74, ' '))}`);
2426
+ if (inventory.possiblyDouble > 0) {
2427
+ console.log(` ${c.yellow(wrap(t.store.possiblyDouble(n(inventory.possiblyDouble)), 74, ' '))}`);
2428
+ }
2429
+ if (inventory.unknownVersion > 0) {
2430
+ console.log(` ${c.yellow(wrap(t.store.unknownVersion(n(inventory.unknownVersion)), 74, ' '))}`);
2431
+ }
2432
+ for (const bad of unreadable) {
2433
+ console.log(` ${c.yellow(wrap(t.store.unreadable(bad.file, String(bad.line)), 74, ' '))}`);
2434
+ }
2435
+ const keepDays = config.store?.keepDays;
2436
+ console.log(
2437
+ ` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`,
2438
+ );
2439
+ }
2440
+
2441
+ /**
2442
+ * `trazum connect <provider>` — the bill, read from the provider.
2443
+ *
2444
+ * The pull and the pricing live elsewhere; this owns the window, the
2445
+ * rendering and the refusals. The report it prints is deliberately a
2446
+ * *restricted* one: a usage API serves sums, so every per-call finding is
2447
+ * listed as unavailable rather than computed from a zero nobody measured.
2448
+ */
2449
+ async function commandConnect(
2450
+ args: Args,
2451
+ pricing: PricingCatalogue,
2452
+ t: CliMessages,
2453
+ ): Promise<void> {
2454
+ const id = args.positional[0];
2455
+ if (id === undefined) {
2456
+ throw new Error(t.connect.noTarget(CONNECTORS.map((c) => c.id).join(', ')));
2457
+ }
2458
+ const descriptor = connectorFor(id);
2459
+ if (descriptor === null) {
2460
+ throw new Error(t.connect.unknownProvider(id, CONNECTORS.map((c) => c.id).join(', ')));
2461
+ }
2462
+
2463
+ const now = Date.now();
2464
+ const since = parseWhen(args, 'since', false, t, now);
2465
+ const until = parseWhen(args, 'until', true, t, now);
2466
+ // A month back by default: long enough to be a bill, short enough that a
2467
+ // first run against a busy organisation does not walk fifty pages.
2468
+ const fromMs = since.ms ?? now - 30 * 86_400_000;
2469
+ const toMs = until.ms ?? now;
2470
+ if (fromMs >= toMs) throw new Error(t.profile.sinceAfterUntil());
2471
+
2472
+ const day = (msValue: number): string => new Date(msValue).toISOString().slice(0, 10);
2473
+
2474
+ if (boolFlag(args, 'dry-run')) {
2475
+ console.log(
2476
+ wrap(
2477
+ t.connect.dryRun(
2478
+ descriptor.displayName,
2479
+ day(fromMs),
2480
+ day(toMs),
2481
+ descriptor.credentialEnv.join(' or '),
2482
+ descriptor.keyKind,
2483
+ ),
2484
+ 76,
2485
+ ' ',
2486
+ ),
2487
+ );
2488
+ return;
2489
+ }
2490
+
2491
+ /**
2492
+ * A payload somebody already has is priced without a pull.
2493
+ *
2494
+ * People save API responses — from a support thread, from a curl in a
2495
+ * runbook, from a colleague who has the admin key and they do not. Pricing
2496
+ * one needs no credential and no network, and it is the same arithmetic on
2497
+ * the same shape, so refusing it would be ceremony rather than safety.
2498
+ */
2499
+ const payloadPath = stringFlag(args, 'payload');
2500
+ const pulled =
2501
+ payloadPath === undefined
2502
+ ? await fetchProviderUsage({ descriptor, fromMs, toMs, env: process.env })
2503
+ : {
2504
+ pull: (descriptor.id === 'anthropic' ? normalizeAnthropicUsage : normalizeOpenAIUsage)(
2505
+ JSON.parse(await readFile(payloadPath, 'utf8')),
2506
+ ),
2507
+ source: { variable: payloadPath },
2508
+ };
2509
+ const { pull, source } = pulled;
2510
+ const report = bucketedProfile(pull, { catalogue: pricing });
2511
+ const cache = bucketedCacheEconomics(report);
2512
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
2513
+
2514
+ /**
2515
+ * `--store` keeps what was pulled, so the next run does not download it
2516
+ * again and `history` has a series without anybody curating a folder. Opt
2517
+ * in rather than automatic: a command that starts writing to a hidden
2518
+ * directory on its own is a command nobody trusts twice.
2519
+ */
2520
+ let stored = 0;
2521
+ if (boolFlag(args, 'store')) {
2522
+ stored = await appendRecords(process.cwd(), recordsFromBuckets(pull.provider, pull.buckets, Date.now()));
2523
+ }
2524
+
2525
+ const outPath = stringFlag(args, 'out');
2526
+ if (outPath !== undefined) {
2527
+ await writeFile(outPath, `${JSON.stringify({ ...report, pulledFrom: source.variable }, null, 2)}\n`);
2528
+ }
2529
+
2530
+ if (boolFlag(args, 'json')) {
2531
+ console.log(JSON.stringify(report, null, 2));
2532
+ return;
2533
+ }
2534
+
2535
+ const lines = (md: boolean): string[] => {
2536
+ const out: string[] = [];
2537
+ const heading = t.connect.heading(
2538
+ descriptor.displayName,
2539
+ report.span === null ? day(fromMs) : day(report.span.fromMs),
2540
+ report.span === null ? day(toMs) : day(report.span.toMs),
2541
+ formatUsd(report.total.totalUsd),
2542
+ report.total.calls === null ? null : n(report.total.calls),
2543
+ );
2544
+ out.push(md ? `## ${heading}` : heading);
2545
+
2546
+ const modelWidth = Math.max(0, ...report.byModel.map((s) => s.model.length));
2547
+ for (const slice of report.byModel) {
2548
+ const share = report.total.totalUsd > 0 ? slice.totalUsd / report.total.totalUsd : 0;
2549
+ const row = t.connect.modelRow(
2550
+ md ? slice.model : slice.model.padEnd(modelWidth),
2551
+ formatUsd(slice.totalUsd).padStart(9),
2552
+ `${(share * 100).toFixed(1)}%`.padStart(6),
2553
+ slice.calls === null ? null : n(slice.calls),
2554
+ );
2555
+ out.push(md ? `- ${row}` : row);
2556
+ }
2557
+
2558
+ if (report.byModel.length === 0) {
2559
+ out.push(md ? `_${t.connect.nothingBilled()}_` : t.connect.nothingBilled());
2560
+ }
2561
+
2562
+ if (cache.verdict !== 'no-cache') {
2563
+ out.push('');
2564
+ const line =
2565
+ cache.verdict === 'paid-off'
2566
+ ? t.connect.cachePaid(formatUsd(-cache.deltaUsd))
2567
+ : t.connect.cacheLost(formatUsd(cache.deltaUsd));
2568
+ out.push(line);
2569
+ if (cache.worstCaseVerdict !== cache.verdict) {
2570
+ const unsettled = t.connect.cacheUnsettled();
2571
+ out.push(md ? `_${unsettled}_` : unsettled);
2572
+ }
2573
+ }
2574
+
2575
+ if (report.total.calls === null) {
2576
+ out.push('');
2577
+ const line = t.connect.noCallCount(descriptor.displayName);
2578
+ out.push(md ? `_${line}_` : line);
2579
+ }
2580
+
2581
+ for (const model of report.unpricedModels) {
2582
+ out.push('');
2583
+ const line = t.connect.unpriced(model.model, n(model.inputTokens + model.outputTokens));
2584
+ out.push(md ? `- ${line}` : `! ${line}`);
2585
+ }
2586
+
2587
+ if (report.gaps.length > 0) out.push('');
2588
+ for (const gap of report.gaps) {
2589
+ const line = t.connect.gap(gap.detail);
2590
+ out.push(md ? `- ${line}` : `! ${line}`);
2591
+ }
2592
+
2593
+ out.push('');
2594
+ const unavailable = t.connect.unavailable(
2595
+ report.unavailable.map((u) => u.finding).join(', '),
2596
+ );
2597
+ out.push(md ? `_${unavailable}_` : unavailable);
2598
+ out.push('');
2599
+ out.push(md ? `_${t.connect.footer()}_` : t.connect.footer());
2600
+ return out;
2601
+ };
2602
+
2603
+ await writeMarkdown(args, () => lines(true).join('\n'));
2604
+
2605
+ const [head, ...rest] = lines(false);
2606
+ console.log(c.bold(head!));
2607
+ for (const row of rest) {
2608
+ // Short rows print as written so the columns stay aligned; `wrap` collapses
2609
+ // runs of spaces, which is right for prose and wrong for a table.
2610
+ if (row === '') console.log('');
2611
+ else if (row.length <= 74) console.log(` ${row}`);
2612
+ else console.log(` ${wrap(row, 74, ' ')}`);
2613
+ }
2614
+ if (outPath !== undefined) console.log(c.dim(t.connect.wrote(outPath)));
2615
+ if (stored > 0) console.log(c.dim(t.store.appended(n(stored), STORE_DIR)));
2616
+ }
2617
+
2226
2618
  /**
2227
2619
  * `trazum history <dir>` — many reports over many periods, as one series.
2228
2620
  *
@@ -2232,40 +2624,103 @@ function isoDate(): string {
2232
2624
  * same action planned twice — and no series, however long, becomes a
2233
2625
  * forecast.
2234
2626
  */
2235
- async function commandHistory(args: Args, t: CliMessages): Promise<void> {
2236
- const path = args.positional[0];
2237
- if (path === undefined) throw new Error(t.history.noTarget());
2238
- const target = await stat(path).catch(() => null);
2239
- if (!target?.isDirectory()) throw new Error(t.history.noTarget());
2240
-
2241
- const entries = await readdir(path, { withFileTypes: true });
2242
- const files = entries
2243
- .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
2244
- .map((entry) => join(path, entry.name))
2245
- .sort((a, b) => a.localeCompare(b));
2246
-
2627
+ async function commandHistory(args: Args, pricing: PricingCatalogue, t: CliMessages): Promise<void> {
2628
+ /**
2629
+ * `--store` builds the series from measured spend already on disk.
2630
+ *
2631
+ * Bucketed sources carry no label — a usage API groups by model and
2632
+ * workspace, never by workload — so the label series is *absent and named*
2633
+ * rather than empty and misread, the same discipline the connected report
2634
+ * uses for the findings a sum cannot support. The model-share and
2635
+ * cache-share series are exactly what a series exists for, and both work.
2636
+ */
2247
2637
  const reports: StoredReport[] = [];
2248
2638
  const plans: (PlanDocument & { createdAt?: string })[] = [];
2249
2639
  const unrecognized: string[] = [];
2250
- for (const file of files) {
2251
- let parsed: unknown;
2252
- try {
2253
- parsed = JSON.parse(await readFile(file, 'utf8'));
2254
- } catch {
2255
- unrecognized.push(file);
2256
- continue;
2257
- }
2258
- const report = storedReportFrom(file, parsed);
2259
- if (report !== null) {
2260
- reports.push(report);
2261
- continue;
2640
+ const fromStore = boolFlag(args, 'store');
2641
+
2642
+ if (fromStore) {
2643
+ const { resolved } = await readStore(process.cwd());
2644
+ if (resolved.records.length === 0) throw new Error(t.store.empty(STORE_DIR));
2645
+
2646
+ /**
2647
+ * One period per UTC day of stored measurement, priced exactly as a fresh
2648
+ * pull prices it.
2649
+ *
2650
+ * The label series is deliberately absent: a usage API groups by model
2651
+ * and workspace, never by workload, so there is no label to carry.
2652
+ * Rendering an empty label series would read as "no workload moved",
2653
+ * which is a statement about traffic rather than about the source, and
2654
+ * the footer says which it is.
2655
+ */
2656
+ const byDay = new Map<string, typeof resolved.records>();
2657
+ for (const record of resolved.records) {
2658
+ const key = new Date(record.fromMs).toISOString().slice(0, 10);
2659
+ const list = byDay.get(key) ?? [];
2660
+ list.push(record);
2661
+ byDay.set(key, list);
2662
+ }
2663
+ for (const [dayKey, records] of [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
2664
+ const day = bucketedProfile(
2665
+ {
2666
+ provider: 'store',
2667
+ granularity: 'bucketed',
2668
+ buckets: bucketsFromRecords(records),
2669
+ window: {
2670
+ fromMs: Math.min(...records.map((r) => r.fromMs)),
2671
+ toMs: Math.max(...records.map((r) => r.toMs)),
2672
+ },
2673
+ gaps: [],
2674
+ unavailable: [],
2675
+ },
2676
+ { catalogue: pricing },
2677
+ );
2678
+ const cacheTouched = day.total.cacheReadTokens + day.total.cacheWriteTokens;
2679
+ reports.push({
2680
+ name: dayKey,
2681
+ span: day.span,
2682
+ totalUsd: day.total.totalUsd,
2683
+ calls: day.total.calls,
2684
+ byLabel: new Map(),
2685
+ byModel: new Map(day.byModel.map((slice) => [slice.model, slice.totalUsd])),
2686
+ cacheReadShare:
2687
+ day.total.inputTokens + cacheTouched > 0
2688
+ ? day.total.cacheReadTokens / (day.total.inputTokens + cacheTouched)
2689
+ : null,
2690
+ });
2262
2691
  }
2263
- const maybePlan = parsed as PlanDocument & { createdAt?: string };
2264
- if (maybePlan?.schemaVersion === 1 && Array.isArray(maybePlan.actions)) {
2265
- plans.push(maybePlan);
2266
- continue;
2692
+ } else {
2693
+ const path = args.positional[0];
2694
+ if (path === undefined) throw new Error(t.history.noTarget());
2695
+ const target = await stat(path).catch(() => null);
2696
+ if (!target?.isDirectory()) throw new Error(t.history.noTarget());
2697
+
2698
+ const entries = await readdir(path, { withFileTypes: true });
2699
+ const files = entries
2700
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
2701
+ .map((entry) => join(path, entry.name))
2702
+ .sort((a, b) => a.localeCompare(b));
2703
+
2704
+ for (const file of files) {
2705
+ let parsed: unknown;
2706
+ try {
2707
+ parsed = JSON.parse(await readFile(file, 'utf8'));
2708
+ } catch {
2709
+ unrecognized.push(file);
2710
+ continue;
2711
+ }
2712
+ const report = storedReportFrom(file, parsed);
2713
+ if (report !== null) {
2714
+ reports.push(report);
2715
+ continue;
2716
+ }
2717
+ const maybePlan = parsed as PlanDocument & { createdAt?: string };
2718
+ if (maybePlan?.schemaVersion === 1 && Array.isArray(maybePlan.actions)) {
2719
+ plans.push(maybePlan);
2720
+ continue;
2721
+ }
2722
+ unrecognized.push(file);
2267
2723
  }
2268
- unrecognized.push(file);
2269
2724
  }
2270
2725
 
2271
2726
  const history = buildHistory(reports, plans);
@@ -2299,7 +2754,7 @@ async function commandHistory(args: Args, t: CliMessages): Promise<void> {
2299
2754
  const row = t.history.periodRow(
2300
2755
  period.name,
2301
2756
  formatUsd(period.totalUsd),
2302
- n(period.calls),
2757
+ period.calls === null ? null : n(period.calls),
2303
2758
  ((period.toMs - period.fromMs) / 86_400_000).toFixed(1),
2304
2759
  );
2305
2760
  out.push(md ? `- ${row}` : ` ${row}`);
@@ -2327,6 +2782,11 @@ async function commandHistory(args: Args, t: CliMessages): Promise<void> {
2327
2782
  for (const name of unrecognized) {
2328
2783
  out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
2329
2784
  }
2785
+ if (fromStore) {
2786
+ out.push('');
2787
+ const note = t.history.storeNoLabels();
2788
+ out.push(md ? `_${note}_` : ` ${note}`);
2789
+ }
2330
2790
  out.push('');
2331
2791
  out.push(md ? `_${t.history.footer()}_` : ` ${t.history.footer()}`);
2332
2792
  return out;
@@ -2699,41 +3159,11 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2699
3159
  * no record.
2700
3160
  */
2701
3161
  const now = Date.now();
2702
- let relativeWindow = false;
2703
- const parseWhen = (flag: string, endOfDay: boolean): number | undefined => {
2704
- const value = stringFlag(args, flag);
2705
- if (value === undefined) return undefined;
2706
- /**
2707
- * A relative window — `7d`, `24h` — because "the last week" is what a
2708
- * nightly job actually wants, and computing a date in a shell to say it
2709
- * is the step that gets skipped.
2710
- *
2711
- * Relative to **the machine's clock, not the log's**, which is a real
2712
- * difference: a log exported last month answers `--since 7d` with
2713
- * nothing, and the report says so rather than reporting $0. That caveat
2714
- * is stated beside the window line, because a period the reader did not
2715
- * name is a period they will misread.
2716
- */
2717
- const relative = /^(\d+)([dh])$/.exec(value);
2718
- if (relative) {
2719
- const amount = Number(relative[1]);
2720
- if (amount > 0) {
2721
- relativeWindow = true;
2722
- const span = relative[2] === 'd' ? 86_400_000 : 3_600_000;
2723
- return now - amount * span;
2724
- }
2725
- }
2726
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
2727
- const midnight = Date.parse(`${value}T00:00:00Z`);
2728
- if (Number.isFinite(midnight)) return endOfDay ? midnight + 86_400_000 : midnight;
2729
- }
2730
- if (value === 'now') return now;
2731
- const exact = Date.parse(value);
2732
- if (Number.isFinite(exact)) return exact;
2733
- throw new Error(t.profile.badWhen(flag, value));
2734
- };
2735
- const sinceMs = parseWhen('since', false);
2736
- const untilMs = parseWhen('until', true);
3162
+ const sinceWhen = parseWhen(args, 'since', false, t, now);
3163
+ const untilWhen = parseWhen(args, 'until', true, t, now);
3164
+ const relativeWindow = sinceWhen.relative || untilWhen.relative;
3165
+ const sinceMs = sinceWhen.ms;
3166
+ const untilMs = untilWhen.ms;
2737
3167
  if (sinceMs !== undefined && untilMs !== undefined && sinceMs >= untilMs) {
2738
3168
  throw new Error(t.profile.sinceAfterUntil());
2739
3169
  }
@@ -6428,7 +6858,13 @@ async function main(): Promise<void> {
6428
6858
  await commandVerify(args, pricing, t);
6429
6859
  break;
6430
6860
  case 'history':
6431
- await commandHistory(args, t);
6861
+ await commandHistory(args, pricing, t);
6862
+ break;
6863
+ case 'connect':
6864
+ await commandConnect(args, pricing, t);
6865
+ break;
6866
+ case 'store':
6867
+ await commandStore(args, config, pricing, t);
6432
6868
  break;
6433
6869
  case 'route':
6434
6870
  await commandRoute(args, pricing, t);