@trazum/cli 1.39.0 → 1.41.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/README.md +2 -0
- package/dist/connect.d.ts +72 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +204 -0
- package/dist/connect.js.map +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +89 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +91 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +52 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +279 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/connect.ts +245 -0
- package/src/i18n/en.ts +109 -0
- package/src/i18n/es.ts +111 -0
- package/src/i18n/types.ts +54 -0
- package/src/index.ts +353 -35
package/src/index.ts
CHANGED
|
@@ -11,7 +11,15 @@ import {
|
|
|
11
11
|
cacheableMinimum,
|
|
12
12
|
analyzeCachePrefix,
|
|
13
13
|
billLevers,
|
|
14
|
+
bucketedCacheEconomics,
|
|
15
|
+
bucketedProfile,
|
|
16
|
+
buildHistory,
|
|
14
17
|
buildPlan,
|
|
18
|
+
connectorFor,
|
|
19
|
+
CONNECTORS,
|
|
20
|
+
normalizeAnthropicUsage,
|
|
21
|
+
normalizeOpenAIUsage,
|
|
22
|
+
storedReportFrom,
|
|
15
23
|
verifyPlan,
|
|
16
24
|
cacheEconomics,
|
|
17
25
|
cacheHitRate,
|
|
@@ -78,9 +86,12 @@ import {
|
|
|
78
86
|
import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
|
|
79
87
|
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
80
88
|
import type {
|
|
89
|
+
BucketedReport,
|
|
81
90
|
FleetSource,
|
|
91
|
+
HistoryRun,
|
|
82
92
|
MeasuredUsage,
|
|
83
93
|
PlanDocument,
|
|
94
|
+
StoredReport,
|
|
84
95
|
VerifiedAction,
|
|
85
96
|
BaselineBreach,
|
|
86
97
|
BaselineChange,
|
|
@@ -129,6 +140,7 @@ import {
|
|
|
129
140
|
revisionsFor,
|
|
130
141
|
} from './git.js';
|
|
131
142
|
import type { Revision } from './git.js';
|
|
143
|
+
import { fetchProviderUsage } from './connect.js';
|
|
132
144
|
import { detectLocale, getCliMessages } from './i18n/index.js';
|
|
133
145
|
import {
|
|
134
146
|
MAX_SUMMARY_CHARS,
|
|
@@ -177,6 +189,7 @@ const VALUE_FLAGS = new Set([
|
|
|
177
189
|
'against',
|
|
178
190
|
'from-log',
|
|
179
191
|
'min-usd',
|
|
192
|
+
'payload',
|
|
180
193
|
// `route` takes a path here, and the flag is deliberately not `--prompt`:
|
|
181
194
|
// everywhere else in this tool `--prompt` names a marked prompt *inside* a
|
|
182
195
|
// source file, and reusing it for a path would be a trap laid for the reader.
|
|
@@ -479,6 +492,8 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
479
492
|
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'],
|
|
480
493
|
plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
|
|
481
494
|
verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
|
|
495
|
+
history: ['json', 'markdown-out'],
|
|
496
|
+
connect: ['since', 'until', 'payload', 'json', 'out', 'markdown-out', 'pricing', 'pricing-live', 'dry-run'],
|
|
482
497
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
483
498
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
484
499
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2218,6 +2233,333 @@ function isoDate(): string {
|
|
|
2218
2233
|
* metered API calls somebody was actually billed for — the bill exists wherever
|
|
2219
2234
|
* Trazum happens to be running, so the host has no bearing on it.
|
|
2220
2235
|
*/
|
|
2236
|
+
/**
|
|
2237
|
+
* One end of a time window, from a flag.
|
|
2238
|
+
*
|
|
2239
|
+
* A UTC day (`2026-08-14`), a full ISO 8601 timestamp, a relative window
|
|
2240
|
+
* (`7d`, `24h`) or `now`. A bare day means the whole of it — since its first
|
|
2241
|
+
* instant, until its last — because `--until 2026-08-14` excluding the named
|
|
2242
|
+
* day is a trap sprung on everyone who reads dates the way humans do.
|
|
2243
|
+
*
|
|
2244
|
+
* `relative` comes back so the caller can state the caveat: a relative window
|
|
2245
|
+
* is measured against **the machine's clock, not the data's**, and a log
|
|
2246
|
+
* exported last month answers `--since 7d` with nothing.
|
|
2247
|
+
*/
|
|
2248
|
+
function parseWhen(
|
|
2249
|
+
args: Args,
|
|
2250
|
+
flag: string,
|
|
2251
|
+
endOfDay: boolean,
|
|
2252
|
+
t: CliMessages,
|
|
2253
|
+
now: number,
|
|
2254
|
+
): { ms: number | undefined; relative: boolean } {
|
|
2255
|
+
const value = stringFlag(args, flag);
|
|
2256
|
+
if (value === undefined) return { ms: undefined, relative: false };
|
|
2257
|
+
|
|
2258
|
+
const relative = /^(\d+)([dh])$/.exec(value);
|
|
2259
|
+
if (relative) {
|
|
2260
|
+
const amount = Number(relative[1]);
|
|
2261
|
+
if (amount > 0) {
|
|
2262
|
+
const span = relative[2] === 'd' ? 86_400_000 : 3_600_000;
|
|
2263
|
+
return { ms: now - amount * span, relative: true };
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
2267
|
+
const midnight = Date.parse(`${value}T00:00:00Z`);
|
|
2268
|
+
if (Number.isFinite(midnight)) {
|
|
2269
|
+
return { ms: endOfDay ? midnight + 86_400_000 : midnight, relative: false };
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (value === 'now') return { ms: now, relative: false };
|
|
2273
|
+
const exact = Date.parse(value);
|
|
2274
|
+
if (Number.isFinite(exact)) return { ms: exact, relative: false };
|
|
2275
|
+
throw new Error(t.profile.badWhen(flag, value));
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* `trazum connect <provider>` — the bill, read from the provider.
|
|
2280
|
+
*
|
|
2281
|
+
* The pull and the pricing live elsewhere; this owns the window, the
|
|
2282
|
+
* rendering and the refusals. The report it prints is deliberately a
|
|
2283
|
+
* *restricted* one: a usage API serves sums, so every per-call finding is
|
|
2284
|
+
* listed as unavailable rather than computed from a zero nobody measured.
|
|
2285
|
+
*/
|
|
2286
|
+
async function commandConnect(
|
|
2287
|
+
args: Args,
|
|
2288
|
+
pricing: PricingCatalogue,
|
|
2289
|
+
t: CliMessages,
|
|
2290
|
+
): Promise<void> {
|
|
2291
|
+
const id = args.positional[0];
|
|
2292
|
+
if (id === undefined) {
|
|
2293
|
+
throw new Error(t.connect.noTarget(CONNECTORS.map((c) => c.id).join(', ')));
|
|
2294
|
+
}
|
|
2295
|
+
const descriptor = connectorFor(id);
|
|
2296
|
+
if (descriptor === null) {
|
|
2297
|
+
throw new Error(t.connect.unknownProvider(id, CONNECTORS.map((c) => c.id).join(', ')));
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
const now = Date.now();
|
|
2301
|
+
const since = parseWhen(args, 'since', false, t, now);
|
|
2302
|
+
const until = parseWhen(args, 'until', true, t, now);
|
|
2303
|
+
// A month back by default: long enough to be a bill, short enough that a
|
|
2304
|
+
// first run against a busy organisation does not walk fifty pages.
|
|
2305
|
+
const fromMs = since.ms ?? now - 30 * 86_400_000;
|
|
2306
|
+
const toMs = until.ms ?? now;
|
|
2307
|
+
if (fromMs >= toMs) throw new Error(t.profile.sinceAfterUntil());
|
|
2308
|
+
|
|
2309
|
+
const day = (msValue: number): string => new Date(msValue).toISOString().slice(0, 10);
|
|
2310
|
+
|
|
2311
|
+
if (boolFlag(args, 'dry-run')) {
|
|
2312
|
+
console.log(
|
|
2313
|
+
wrap(
|
|
2314
|
+
t.connect.dryRun(
|
|
2315
|
+
descriptor.displayName,
|
|
2316
|
+
day(fromMs),
|
|
2317
|
+
day(toMs),
|
|
2318
|
+
descriptor.credentialEnv.join(' or '),
|
|
2319
|
+
descriptor.keyKind,
|
|
2320
|
+
),
|
|
2321
|
+
76,
|
|
2322
|
+
' ',
|
|
2323
|
+
),
|
|
2324
|
+
);
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
/**
|
|
2329
|
+
* A payload somebody already has is priced without a pull.
|
|
2330
|
+
*
|
|
2331
|
+
* People save API responses — from a support thread, from a curl in a
|
|
2332
|
+
* runbook, from a colleague who has the admin key and they do not. Pricing
|
|
2333
|
+
* one needs no credential and no network, and it is the same arithmetic on
|
|
2334
|
+
* the same shape, so refusing it would be ceremony rather than safety.
|
|
2335
|
+
*/
|
|
2336
|
+
const payloadPath = stringFlag(args, 'payload');
|
|
2337
|
+
const pulled =
|
|
2338
|
+
payloadPath === undefined
|
|
2339
|
+
? await fetchProviderUsage({ descriptor, fromMs, toMs, env: process.env })
|
|
2340
|
+
: {
|
|
2341
|
+
pull: (descriptor.id === 'anthropic' ? normalizeAnthropicUsage : normalizeOpenAIUsage)(
|
|
2342
|
+
JSON.parse(await readFile(payloadPath, 'utf8')),
|
|
2343
|
+
),
|
|
2344
|
+
source: { variable: payloadPath },
|
|
2345
|
+
};
|
|
2346
|
+
const { pull, source } = pulled;
|
|
2347
|
+
const report = bucketedProfile(pull, { catalogue: pricing });
|
|
2348
|
+
const cache = bucketedCacheEconomics(report);
|
|
2349
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2350
|
+
|
|
2351
|
+
const outPath = stringFlag(args, 'out');
|
|
2352
|
+
if (outPath !== undefined) {
|
|
2353
|
+
await writeFile(outPath, `${JSON.stringify({ ...report, pulledFrom: source.variable }, null, 2)}\n`);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
if (boolFlag(args, 'json')) {
|
|
2357
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const lines = (md: boolean): string[] => {
|
|
2362
|
+
const out: string[] = [];
|
|
2363
|
+
const heading = t.connect.heading(
|
|
2364
|
+
descriptor.displayName,
|
|
2365
|
+
report.span === null ? day(fromMs) : day(report.span.fromMs),
|
|
2366
|
+
report.span === null ? day(toMs) : day(report.span.toMs),
|
|
2367
|
+
formatUsd(report.total.totalUsd),
|
|
2368
|
+
report.total.calls === null ? null : n(report.total.calls),
|
|
2369
|
+
);
|
|
2370
|
+
out.push(md ? `## ${heading}` : heading);
|
|
2371
|
+
|
|
2372
|
+
const modelWidth = Math.max(0, ...report.byModel.map((s) => s.model.length));
|
|
2373
|
+
for (const slice of report.byModel) {
|
|
2374
|
+
const share = report.total.totalUsd > 0 ? slice.totalUsd / report.total.totalUsd : 0;
|
|
2375
|
+
const row = t.connect.modelRow(
|
|
2376
|
+
md ? slice.model : slice.model.padEnd(modelWidth),
|
|
2377
|
+
formatUsd(slice.totalUsd).padStart(9),
|
|
2378
|
+
`${(share * 100).toFixed(1)}%`.padStart(6),
|
|
2379
|
+
slice.calls === null ? null : n(slice.calls),
|
|
2380
|
+
);
|
|
2381
|
+
out.push(md ? `- ${row}` : row);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
if (report.byModel.length === 0) {
|
|
2385
|
+
out.push(md ? `_${t.connect.nothingBilled()}_` : t.connect.nothingBilled());
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
if (cache.verdict !== 'no-cache') {
|
|
2389
|
+
out.push('');
|
|
2390
|
+
const line =
|
|
2391
|
+
cache.verdict === 'paid-off'
|
|
2392
|
+
? t.connect.cachePaid(formatUsd(-cache.deltaUsd))
|
|
2393
|
+
: t.connect.cacheLost(formatUsd(cache.deltaUsd));
|
|
2394
|
+
out.push(line);
|
|
2395
|
+
if (cache.worstCaseVerdict !== cache.verdict) {
|
|
2396
|
+
const unsettled = t.connect.cacheUnsettled();
|
|
2397
|
+
out.push(md ? `_${unsettled}_` : unsettled);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
if (report.total.calls === null) {
|
|
2402
|
+
out.push('');
|
|
2403
|
+
const line = t.connect.noCallCount(descriptor.displayName);
|
|
2404
|
+
out.push(md ? `_${line}_` : line);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
for (const model of report.unpricedModels) {
|
|
2408
|
+
out.push('');
|
|
2409
|
+
const line = t.connect.unpriced(model.model, n(model.inputTokens + model.outputTokens));
|
|
2410
|
+
out.push(md ? `- ${line}` : `! ${line}`);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (report.gaps.length > 0) out.push('');
|
|
2414
|
+
for (const gap of report.gaps) {
|
|
2415
|
+
const line = t.connect.gap(gap.detail);
|
|
2416
|
+
out.push(md ? `- ${line}` : `! ${line}`);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
out.push('');
|
|
2420
|
+
const unavailable = t.connect.unavailable(
|
|
2421
|
+
report.unavailable.map((u) => u.finding).join(', '),
|
|
2422
|
+
);
|
|
2423
|
+
out.push(md ? `_${unavailable}_` : unavailable);
|
|
2424
|
+
out.push('');
|
|
2425
|
+
out.push(md ? `_${t.connect.footer()}_` : t.connect.footer());
|
|
2426
|
+
return out;
|
|
2427
|
+
};
|
|
2428
|
+
|
|
2429
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
2430
|
+
|
|
2431
|
+
const [head, ...rest] = lines(false);
|
|
2432
|
+
console.log(c.bold(head!));
|
|
2433
|
+
for (const row of rest) {
|
|
2434
|
+
// Short rows print as written so the columns stay aligned; `wrap` collapses
|
|
2435
|
+
// runs of spaces, which is right for prose and wrong for a table.
|
|
2436
|
+
if (row === '') console.log('');
|
|
2437
|
+
else if (row.length <= 74) console.log(` ${row}`);
|
|
2438
|
+
else console.log(` ${wrap(row, 74, ' ')}`);
|
|
2439
|
+
}
|
|
2440
|
+
if (outPath !== undefined) console.log(c.dim(t.connect.wrote(outPath)));
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* `trazum history <dir>` — many reports over many periods, as one series.
|
|
2445
|
+
*
|
|
2446
|
+
* Derived from *stored* `--json` documents, never re-parsed logs: a team can
|
|
2447
|
+
* keep a year of reports and throw the raw logs away, which is what the
|
|
2448
|
+
* privacy story requires anyway. Shapes are named — a climb, a decay, the
|
|
2449
|
+
* same action planned twice — and no series, however long, becomes a
|
|
2450
|
+
* forecast.
|
|
2451
|
+
*/
|
|
2452
|
+
async function commandHistory(args: Args, t: CliMessages): Promise<void> {
|
|
2453
|
+
const path = args.positional[0];
|
|
2454
|
+
if (path === undefined) throw new Error(t.history.noTarget());
|
|
2455
|
+
const target = await stat(path).catch(() => null);
|
|
2456
|
+
if (!target?.isDirectory()) throw new Error(t.history.noTarget());
|
|
2457
|
+
|
|
2458
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
2459
|
+
const files = entries
|
|
2460
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
2461
|
+
.map((entry) => join(path, entry.name))
|
|
2462
|
+
.sort((a, b) => a.localeCompare(b));
|
|
2463
|
+
|
|
2464
|
+
const reports: StoredReport[] = [];
|
|
2465
|
+
const plans: (PlanDocument & { createdAt?: string })[] = [];
|
|
2466
|
+
const unrecognized: string[] = [];
|
|
2467
|
+
for (const file of files) {
|
|
2468
|
+
let parsed: unknown;
|
|
2469
|
+
try {
|
|
2470
|
+
parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
2471
|
+
} catch {
|
|
2472
|
+
unrecognized.push(file);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const report = storedReportFrom(file, parsed);
|
|
2476
|
+
if (report !== null) {
|
|
2477
|
+
reports.push(report);
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
const maybePlan = parsed as PlanDocument & { createdAt?: string };
|
|
2481
|
+
if (maybePlan?.schemaVersion === 1 && Array.isArray(maybePlan.actions)) {
|
|
2482
|
+
plans.push(maybePlan);
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
unrecognized.push(file);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const history = buildHistory(reports, plans);
|
|
2489
|
+
if (history.periods.length < 3) {
|
|
2490
|
+
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
const stamped = { ...history, unrecognizedFiles: unrecognized };
|
|
2494
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2495
|
+
const day = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
2496
|
+
const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
2497
|
+
|
|
2498
|
+
const runLine = (run: HistoryRun): string => {
|
|
2499
|
+
if (run.kind === 'label-spend-climbing') {
|
|
2500
|
+
const name = run.subject === UNLABELLED ? t.profile.unlabelled() : run.subject;
|
|
2501
|
+
return t.history.runLabel(name, n(run.periods), run.sinceName, formatUsd(run.from), formatUsd(run.to));
|
|
2502
|
+
}
|
|
2503
|
+
if (run.kind === 'model-share-climbing') {
|
|
2504
|
+
return t.history.runModel(run.subject, n(run.periods), run.sinceName, pct(run.from), pct(run.to));
|
|
2505
|
+
}
|
|
2506
|
+
return t.history.runCache(n(run.periods), run.sinceName, pct(run.from), pct(run.to));
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
const lines = (md: boolean): string[] => {
|
|
2510
|
+
const out: string[] = [];
|
|
2511
|
+
const first = history.periods[0]!;
|
|
2512
|
+
const last = history.periods[history.periods.length - 1]!;
|
|
2513
|
+
const heading = t.history.heading(n(history.periods.length), day(first.fromMs), day(last.toMs));
|
|
2514
|
+
out.push(md ? `## ${heading}` : heading);
|
|
2515
|
+
for (const period of history.periods) {
|
|
2516
|
+
const row = t.history.periodRow(
|
|
2517
|
+
period.name,
|
|
2518
|
+
formatUsd(period.totalUsd),
|
|
2519
|
+
n(period.calls),
|
|
2520
|
+
((period.toMs - period.fromMs) / 86_400_000).toFixed(1),
|
|
2521
|
+
);
|
|
2522
|
+
out.push(md ? `- ${row}` : ` ${row}`);
|
|
2523
|
+
}
|
|
2524
|
+
if (history.runs.length > 0) out.push('');
|
|
2525
|
+
for (const run of history.runs) {
|
|
2526
|
+
out.push(md ? `- ${runLine(run)}` : ` ! ${runLine(run)}`);
|
|
2527
|
+
}
|
|
2528
|
+
if (history.repeatedPlanActions.length > 0) out.push('');
|
|
2529
|
+
for (const repeat of history.repeatedPlanActions) {
|
|
2530
|
+
const name = repeat.label === UNLABELLED ? t.profile.unlabelled() : repeat.label;
|
|
2531
|
+
const row = t.history.repeated(
|
|
2532
|
+
repeat.kind,
|
|
2533
|
+
name,
|
|
2534
|
+
repeat.model,
|
|
2535
|
+
n(repeat.appearances),
|
|
2536
|
+
repeat.firstPlanned?.slice(0, 10) ?? null,
|
|
2537
|
+
repeat.lastPlanned?.slice(0, 10) ?? null,
|
|
2538
|
+
);
|
|
2539
|
+
out.push(md ? `- ${row}` : ` ! ${row}`);
|
|
2540
|
+
}
|
|
2541
|
+
for (const name of history.undatedReports) {
|
|
2542
|
+
out.push(md ? `- ${t.history.undated(name)}` : ` ${t.history.undated(name)}`);
|
|
2543
|
+
}
|
|
2544
|
+
for (const name of unrecognized) {
|
|
2545
|
+
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
2546
|
+
}
|
|
2547
|
+
out.push('');
|
|
2548
|
+
out.push(md ? `_${t.history.footer()}_` : ` ${t.history.footer()}`);
|
|
2549
|
+
return out;
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
2553
|
+
|
|
2554
|
+
if (boolFlag(args, 'json')) {
|
|
2555
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const [head, ...rest] = lines(false);
|
|
2559
|
+
console.log(c.bold(head!));
|
|
2560
|
+
for (const row of rest) console.log(row === '' ? '' : wrap(row, 76, ' '));
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2221
2563
|
/**
|
|
2222
2564
|
* `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
|
|
2223
2565
|
*
|
|
@@ -2574,41 +2916,11 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
2574
2916
|
* no record.
|
|
2575
2917
|
*/
|
|
2576
2918
|
const now = Date.now();
|
|
2577
|
-
|
|
2578
|
-
const
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
* A relative window — `7d`, `24h` — because "the last week" is what a
|
|
2583
|
-
* nightly job actually wants, and computing a date in a shell to say it
|
|
2584
|
-
* is the step that gets skipped.
|
|
2585
|
-
*
|
|
2586
|
-
* Relative to **the machine's clock, not the log's**, which is a real
|
|
2587
|
-
* difference: a log exported last month answers `--since 7d` with
|
|
2588
|
-
* nothing, and the report says so rather than reporting $0. That caveat
|
|
2589
|
-
* is stated beside the window line, because a period the reader did not
|
|
2590
|
-
* name is a period they will misread.
|
|
2591
|
-
*/
|
|
2592
|
-
const relative = /^(\d+)([dh])$/.exec(value);
|
|
2593
|
-
if (relative) {
|
|
2594
|
-
const amount = Number(relative[1]);
|
|
2595
|
-
if (amount > 0) {
|
|
2596
|
-
relativeWindow = true;
|
|
2597
|
-
const span = relative[2] === 'd' ? 86_400_000 : 3_600_000;
|
|
2598
|
-
return now - amount * span;
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
2602
|
-
const midnight = Date.parse(`${value}T00:00:00Z`);
|
|
2603
|
-
if (Number.isFinite(midnight)) return endOfDay ? midnight + 86_400_000 : midnight;
|
|
2604
|
-
}
|
|
2605
|
-
if (value === 'now') return now;
|
|
2606
|
-
const exact = Date.parse(value);
|
|
2607
|
-
if (Number.isFinite(exact)) return exact;
|
|
2608
|
-
throw new Error(t.profile.badWhen(flag, value));
|
|
2609
|
-
};
|
|
2610
|
-
const sinceMs = parseWhen('since', false);
|
|
2611
|
-
const untilMs = parseWhen('until', true);
|
|
2919
|
+
const sinceWhen = parseWhen(args, 'since', false, t, now);
|
|
2920
|
+
const untilWhen = parseWhen(args, 'until', true, t, now);
|
|
2921
|
+
const relativeWindow = sinceWhen.relative || untilWhen.relative;
|
|
2922
|
+
const sinceMs = sinceWhen.ms;
|
|
2923
|
+
const untilMs = untilWhen.ms;
|
|
2612
2924
|
if (sinceMs !== undefined && untilMs !== undefined && sinceMs >= untilMs) {
|
|
2613
2925
|
throw new Error(t.profile.sinceAfterUntil());
|
|
2614
2926
|
}
|
|
@@ -6302,6 +6614,12 @@ async function main(): Promise<void> {
|
|
|
6302
6614
|
case 'verify':
|
|
6303
6615
|
await commandVerify(args, pricing, t);
|
|
6304
6616
|
break;
|
|
6617
|
+
case 'history':
|
|
6618
|
+
await commandHistory(args, t);
|
|
6619
|
+
break;
|
|
6620
|
+
case 'connect':
|
|
6621
|
+
await commandConnect(args, pricing, t);
|
|
6622
|
+
break;
|
|
6305
6623
|
case 'route':
|
|
6306
6624
|
await commandRoute(args, pricing, t);
|
|
6307
6625
|
break;
|