@qverisai/cli 0.3.0 → 0.5.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.
@@ -0,0 +1,536 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { bold, cyan, dim, green, red, yellow } from "./colors.mjs";
4
+
5
+ export const DEFAULT_DETAIL_LIMIT = 10;
6
+ export const MAX_DETAIL_LIMIT = 50;
7
+ export const DEFAULT_PAGE_SIZE = 500;
8
+ export const MAX_SUMMARY_ROWS = 5000;
9
+ export const MAX_SEARCH_SCAN_ROWS = 2000;
10
+ export const MAX_EXPORT_ROWS = 50000;
11
+
12
+ export function resolveMode(rawMode) {
13
+ const mode = String(rawMode || "summary").replace("_", "-");
14
+ if (mode === "export-file") return "export_file";
15
+ if (mode === "export_file") return "export_file";
16
+ if (mode === "search") return "search";
17
+ return "summary";
18
+ }
19
+
20
+ export function clampLimit(rawLimit) {
21
+ const parsed = Number.parseInt(rawLimit, 10);
22
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_DETAIL_LIMIT;
23
+ return Math.min(parsed, MAX_DETAIL_LIMIT);
24
+ }
25
+
26
+ export function parseBooleanFlag(value) {
27
+ if (value === undefined || value === null) return undefined;
28
+ if (typeof value === "boolean") return value;
29
+ const normalized = String(value).toLowerCase();
30
+ if (["true", "1", "yes"].includes(normalized)) return true;
31
+ if (["false", "0", "no"].includes(normalized)) return false;
32
+ return undefined;
33
+ }
34
+
35
+ export function resolveDateRange(flags = {}) {
36
+ const end = flags.endDate || isoDate(new Date());
37
+ const start = flags.startDate || isoDate(new Date(Date.now() - 24 * 60 * 60 * 1000));
38
+ return { startDate: start, endDate: end };
39
+ }
40
+
41
+ export function chooseBucket(startDate, endDate, requestedBucket) {
42
+ if (["hour", "day", "week"].includes(requestedBucket)) return requestedBucket;
43
+ const start = Date.parse(`${startDate}T00:00:00Z`);
44
+ const end = Date.parse(`${endDate}T23:59:59Z`);
45
+ const days = Number.isFinite(start) && Number.isFinite(end)
46
+ ? Math.max(1, Math.ceil((end - start) / (24 * 60 * 60 * 1000)))
47
+ : 1;
48
+ if (days <= 2) return "hour";
49
+ if (days <= 60) return "day";
50
+ return "week";
51
+ }
52
+
53
+ export function buildUsageQuery(flags, { page = 1, pageSize = DEFAULT_PAGE_SIZE, mode } = {}) {
54
+ const { startDate, endDate } = resolveDateRange(flags);
55
+ const query = {
56
+ start_date: startDate,
57
+ end_date: endDate,
58
+ page,
59
+ page_size: pageSize,
60
+ };
61
+ setIf(query, "execution_id", flags.executionId);
62
+ setIf(query, "search_id", flags.searchId);
63
+ setIf(query, "event_type", flags.eventType);
64
+ setIf(query, "kind", flags.kind);
65
+ setIf(query, "charge_outcome", flags.chargeOutcome);
66
+ setIf(query, "min_credits", flags.minCredits);
67
+ setIf(query, "max_credits", flags.maxCredits);
68
+ const success = parseBooleanFlag(flags.success);
69
+ if (success !== undefined) query.success = success;
70
+ if (mode === "summary") {
71
+ query.summary = true;
72
+ setIf(query, "bucket", flags.bucket);
73
+ setIf(query, "limit", flags.limit || DEFAULT_DETAIL_LIMIT);
74
+ }
75
+ return query;
76
+ }
77
+
78
+ export function buildLedgerQuery(flags, { page = 1, pageSize = DEFAULT_PAGE_SIZE, mode } = {}) {
79
+ const { startDate, endDate } = resolveDateRange(flags);
80
+ const query = {
81
+ start_date: startDate,
82
+ end_date: endDate,
83
+ page,
84
+ page_size: pageSize,
85
+ };
86
+ setIf(query, "entry_type", flags.entryType);
87
+ setIf(query, "direction", flags.direction);
88
+ setIf(query, "min_credits", flags.minCredits);
89
+ setIf(query, "max_credits", flags.maxCredits);
90
+ if (mode === "summary") {
91
+ query.summary = true;
92
+ setIf(query, "bucket", flags.bucket);
93
+ setIf(query, "limit", flags.limit || DEFAULT_DETAIL_LIMIT);
94
+ }
95
+ return query;
96
+ }
97
+
98
+ export function extractItems(response) {
99
+ if (!response || typeof response !== "object") return [];
100
+ if (Array.isArray(response.items)) return response.items;
101
+ if (Array.isArray(response.data?.items)) return response.data.items;
102
+ return [];
103
+ }
104
+
105
+ export function extractTotal(response, fallbackCount) {
106
+ const raw = response?.total ?? response?.data?.total;
107
+ return typeof raw === "number" ? raw : fallbackCount;
108
+ }
109
+
110
+ export function matchesUsageFilters(row, flags) {
111
+ const amount = usageAmount(row);
112
+ return matchesAmount(amount, flags) &&
113
+ matchesField(row.execution_id, flags.executionId) &&
114
+ matchesField(row.search_id, flags.searchId) &&
115
+ matchesField(row.event_type, flags.eventType) &&
116
+ matchesField(row.charge_outcome, flags.chargeOutcome) &&
117
+ matchesField(row.kind, flags.kind) &&
118
+ matchesBoolean(row.success, parseBooleanFlag(flags.success));
119
+ }
120
+
121
+ export function matchesLedgerFilters(row, flags) {
122
+ const amount = Math.abs(toNumber(row.amount_credits));
123
+ const direction = flags.direction || "any";
124
+ if (direction === "consume" && !(toNumber(row.amount_credits) < 0)) return false;
125
+ if (direction === "grant" && !(toNumber(row.amount_credits) > 0)) return false;
126
+ return matchesAmount(amount, flags) && matchesField(row.entry_type, flags.entryType);
127
+ }
128
+
129
+ export function usageAmount(row) {
130
+ return firstNumber(
131
+ row?.actual_amount_credits,
132
+ row?.settled_amount_credits,
133
+ row?.settlement_result?.settled_amount_credits,
134
+ row?.requested_amount_credits,
135
+ row?.pre_settlement_amount_credits,
136
+ row?.list_amount_credits,
137
+ );
138
+ }
139
+
140
+ export function ledgerAmount(row) {
141
+ return toNumber(row?.amount_credits);
142
+ }
143
+
144
+ export function buildUsageSummary(rows, { startDate, endDate, bucket }) {
145
+ const summary = {
146
+ start_date: startDate,
147
+ end_date: endDate,
148
+ bucket,
149
+ total_events: rows.length,
150
+ succeeded: 0,
151
+ failed: 0,
152
+ charge_outcomes: {},
153
+ requested_amount_credits: 0,
154
+ actual_amount_credits: 0,
155
+ buckets: {},
156
+ top_charges: [],
157
+ };
158
+
159
+ for (const row of rows) {
160
+ if (row.success) summary.succeeded += 1;
161
+ else summary.failed += 1;
162
+ const outcome = row.charge_outcome || "unknown";
163
+ summary.charge_outcomes[outcome] = (summary.charge_outcomes[outcome] || 0) + 1;
164
+ summary.requested_amount_credits += firstNumber(row.requested_amount_credits, row.pre_settlement_amount_credits);
165
+ summary.actual_amount_credits += usageAmount(row);
166
+ const key = bucketKey(row.created_at, bucket);
167
+ if (!summary.buckets[key]) {
168
+ summary.buckets[key] = { events: 0, succeeded: 0, failed: 0, actual_amount_credits: 0 };
169
+ }
170
+ summary.buckets[key].events += 1;
171
+ if (row.success) summary.buckets[key].succeeded += 1;
172
+ else summary.buckets[key].failed += 1;
173
+ summary.buckets[key].actual_amount_credits += usageAmount(row);
174
+ }
175
+
176
+ summary.requested_amount_credits = round(summary.requested_amount_credits);
177
+ summary.actual_amount_credits = round(summary.actual_amount_credits);
178
+ for (const value of Object.values(summary.buckets)) {
179
+ value.actual_amount_credits = round(value.actual_amount_credits);
180
+ }
181
+ summary.top_charges = rows
182
+ .map((row) => ({ ...pickUsageRow(row), amount: usageAmount(row) }))
183
+ .filter((row) => row.amount > 0)
184
+ .sort((a, b) => b.amount - a.amount)
185
+ .slice(0, DEFAULT_DETAIL_LIMIT);
186
+ return summary;
187
+ }
188
+
189
+ export function buildUsageSummaryFromServer(serverSummary, { startDate, endDate, bucket }) {
190
+ const buckets = {};
191
+ for (const row of Array.isArray(serverSummary?.buckets) ? serverSummary.buckets : []) {
192
+ const key = row.bucket_start || "unknown";
193
+ buckets[key] = {
194
+ events: numberValue(row.total_count),
195
+ succeeded: numberValue(row.success_count),
196
+ failed: numberValue(row.failure_count),
197
+ actual_amount_credits: round(row.settled_credits),
198
+ };
199
+ }
200
+ return {
201
+ start_date: dateOnly(serverSummary?.start_date) || startDate,
202
+ end_date: dateOnly(serverSummary?.end_date) || endDate,
203
+ bucket: serverSummary?.bucket || bucket,
204
+ total_events: numberValue(serverSummary?.total_count),
205
+ succeeded: numberValue(serverSummary?.success_count),
206
+ failed: numberValue(serverSummary?.failure_count),
207
+ charge_outcomes: serverSummary?.charge_outcome_counts || {},
208
+ requested_amount_credits: round(serverSummary?.pre_settlement_credits),
209
+ actual_amount_credits: round(serverSummary?.settled_credits),
210
+ buckets,
211
+ top_charges: (Array.isArray(serverSummary?.max_charge_items) ? serverSummary.max_charge_items : [])
212
+ .map((row) => ({ ...pickUsageRow(row), amount: usageAmount(row) }))
213
+ .slice(0, DEFAULT_DETAIL_LIMIT),
214
+ };
215
+ }
216
+
217
+ export function buildLedgerSummary(rows, { startDate, endDate, bucket }) {
218
+ const summary = {
219
+ start_date: startDate,
220
+ end_date: endDate,
221
+ bucket,
222
+ total_entries: rows.length,
223
+ consumed_credits: 0,
224
+ granted_credits: 0,
225
+ net_credits: 0,
226
+ entry_types: {},
227
+ buckets: {},
228
+ top_debits: [],
229
+ };
230
+
231
+ for (const row of rows) {
232
+ const amount = ledgerAmount(row);
233
+ if (amount < 0) summary.consumed_credits += Math.abs(amount);
234
+ if (amount > 0) summary.granted_credits += amount;
235
+ summary.net_credits += amount;
236
+ if (!summary.entry_types[row.entry_type || "unknown"]) {
237
+ summary.entry_types[row.entry_type || "unknown"] = { count: 0, amount_credits: 0 };
238
+ }
239
+ summary.entry_types[row.entry_type || "unknown"].count += 1;
240
+ summary.entry_types[row.entry_type || "unknown"].amount_credits += amount;
241
+ const key = bucketKey(row.created_at, bucket);
242
+ if (!summary.buckets[key]) {
243
+ summary.buckets[key] = { entries: 0, consumed_credits: 0, granted_credits: 0, net_credits: 0 };
244
+ }
245
+ summary.buckets[key].entries += 1;
246
+ if (amount < 0) summary.buckets[key].consumed_credits += Math.abs(amount);
247
+ if (amount > 0) summary.buckets[key].granted_credits += amount;
248
+ summary.buckets[key].net_credits += amount;
249
+ }
250
+
251
+ summary.consumed_credits = round(summary.consumed_credits);
252
+ summary.granted_credits = round(summary.granted_credits);
253
+ summary.net_credits = round(summary.net_credits);
254
+ for (const value of Object.values(summary.entry_types)) {
255
+ value.amount_credits = round(value.amount_credits);
256
+ }
257
+ for (const value of Object.values(summary.buckets)) {
258
+ value.consumed_credits = round(value.consumed_credits);
259
+ value.granted_credits = round(value.granted_credits);
260
+ value.net_credits = round(value.net_credits);
261
+ }
262
+ summary.top_debits = rows
263
+ .filter((row) => ledgerAmount(row) < 0)
264
+ .map((row) => pickLedgerRow(row))
265
+ .sort((a, b) => Math.abs(b.amount_credits) - Math.abs(a.amount_credits))
266
+ .slice(0, DEFAULT_DETAIL_LIMIT);
267
+ return summary;
268
+ }
269
+
270
+ export function buildLedgerSummaryFromServer(serverSummary, { startDate, endDate, bucket }) {
271
+ const buckets = {};
272
+ for (const row of Array.isArray(serverSummary?.buckets) ? serverSummary.buckets : []) {
273
+ const key = row.bucket_start || "unknown";
274
+ buckets[key] = {
275
+ entries: numberValue(row.entry_count),
276
+ consumed_credits: round(row.consumed_credits),
277
+ granted_credits: round(row.granted_credits),
278
+ net_credits: round(row.net_amount_credits),
279
+ };
280
+ }
281
+ return {
282
+ start_date: dateOnly(serverSummary?.start_date) || startDate,
283
+ end_date: dateOnly(serverSummary?.end_date) || endDate,
284
+ bucket: serverSummary?.bucket || bucket,
285
+ total_entries: numberValue(serverSummary?.total_entries),
286
+ consumed_credits: round(serverSummary?.consumed_credits),
287
+ granted_credits: round(serverSummary?.granted_credits),
288
+ net_credits: round(serverSummary?.net_amount_credits),
289
+ entry_types: {},
290
+ buckets,
291
+ top_debits: (Array.isArray(serverSummary?.max_amount_items) ? serverSummary.max_amount_items : [])
292
+ .filter((row) => ledgerAmount(row) < 0)
293
+ .map((row) => pickLedgerRow(row))
294
+ .slice(0, DEFAULT_DETAIL_LIMIT),
295
+ };
296
+ }
297
+
298
+ export function pickUsageRow(row) {
299
+ return {
300
+ created_at: row.created_at,
301
+ event_type: row.event_type,
302
+ success: row.success,
303
+ charge_outcome: row.charge_outcome,
304
+ target: row.tool_id || row.model || row.query || row.display_target || row.source_ref_id || "",
305
+ execution_id: row.execution_id || null,
306
+ search_id: row.search_id || null,
307
+ billing_summary: row.billing_summary || row.pre_settlement_bill?.summary || "",
308
+ requested_amount_credits: row.requested_amount_credits ?? row.pre_settlement_amount_credits ?? null,
309
+ actual_amount_credits: usageAmount(row),
310
+ credits_ledger_entry_id: row.credits_ledger_entry_id || null,
311
+ error_message: row.error_message || null,
312
+ };
313
+ }
314
+
315
+ export function pickLedgerRow(row) {
316
+ const settlement = row.settlement_result && typeof row.settlement_result === "object" ? row.settlement_result : {};
317
+ const preBill = row.pre_settlement_bill && typeof row.pre_settlement_bill === "object" ? row.pre_settlement_bill : {};
318
+ return {
319
+ created_at: row.created_at,
320
+ entry_type: row.entry_type,
321
+ amount_credits: ledgerAmount(row),
322
+ source_ref_type: row.source_ref_type || null,
323
+ source_ref_id: row.source_ref_id || null,
324
+ description: row.description || "",
325
+ billing_summary: preBill.summary || "",
326
+ settled_amount_credits: settlement.settled_amount_credits ?? null,
327
+ bucket_deductions: Array.isArray(settlement.bucket_deductions) ? settlement.bucket_deductions : [],
328
+ };
329
+ }
330
+
331
+ export function formatUsageSummary(summary, { scannedRows, total, partial }) {
332
+ const lines = [];
333
+ lines.push(`\n ${bold("Usage History Summary")}`);
334
+ lines.push(` ${dim("Range:")} ${summary.start_date} to ${summary.end_date} ${dim("bucket:")} ${summary.bucket}`);
335
+ lines.push(` ${dim("Events:")} ${bold(String(summary.total_events))} ${green(String(summary.succeeded))} succeeded ${summary.failed ? red(String(summary.failed)) : "0"} failed`);
336
+ lines.push(` ${dim("Credits:")} requested ${yellow(String(summary.requested_amount_credits))} actual ${yellow(String(summary.actual_amount_credits))}`);
337
+ lines.push(` ${dim("Charge outcomes:")} ${formatCounts(summary.charge_outcomes) || "none"}`);
338
+ if (partial) {
339
+ lines.push(` ${yellow("Partial summary:")} scanned ${scannedRows} of ${total ?? "unknown"} matching rows. Use --mode export-file for full analysis.`);
340
+ }
341
+ appendBuckets(lines, summary.buckets, (value) => `${value.events} events, ${value.actual_amount_credits} credits`);
342
+ appendUsageRows(lines, summary.top_charges, "Top charges");
343
+ return lines.join("\n");
344
+ }
345
+
346
+ export function formatUsageRows(rows, { total, partial }) {
347
+ const lines = [];
348
+ lines.push(`\n ${bold("Usage History Results")}`);
349
+ lines.push(` ${dim("Shown:")} ${rows.length}${total !== undefined ? ` of ${total}` : ""}${partial ? ` ${yellow("(truncated)")}` : ""}`);
350
+ appendUsageRows(lines, rows.map(pickUsageRow), "Records");
351
+ if (partial) lines.push(` ${dim("Use --mode export-file for full matching records.")}`);
352
+ return lines.join("\n");
353
+ }
354
+
355
+ export function formatLedgerSummary(summary, { scannedRows, total, partial }) {
356
+ const lines = [];
357
+ lines.push(`\n ${bold("Credits Ledger Summary")}`);
358
+ lines.push(` ${dim("Range:")} ${summary.start_date} to ${summary.end_date} ${dim("bucket:")} ${summary.bucket}`);
359
+ lines.push(` ${dim("Entries:")} ${bold(String(summary.total_entries))}`);
360
+ lines.push(` ${dim("Credits:")} consumed ${yellow(String(summary.consumed_credits))} granted ${green(String(summary.granted_credits))} net ${yellow(String(summary.net_credits))}`);
361
+ lines.push(` ${dim("Entry types:")} ${formatEntryTypes(summary.entry_types) || "none"}`);
362
+ if (partial) {
363
+ lines.push(` ${yellow("Partial summary:")} scanned ${scannedRows} of ${total ?? "unknown"} matching rows. Use --mode export-file for full analysis.`);
364
+ }
365
+ appendBuckets(lines, summary.buckets, (value) => `${value.entries} entries, net ${value.net_credits}`);
366
+ appendLedgerRows(lines, summary.top_debits, "Top debits");
367
+ return lines.join("\n");
368
+ }
369
+
370
+ export function formatLedgerRows(rows, { total, partial }) {
371
+ const lines = [];
372
+ lines.push(`\n ${bold("Credits Ledger Results")}`);
373
+ lines.push(` ${dim("Shown:")} ${rows.length}${total !== undefined ? ` of ${total}` : ""}${partial ? ` ${yellow("(truncated)")}` : ""}`);
374
+ appendLedgerRows(lines, rows.map(pickLedgerRow), "Records");
375
+ if (partial) lines.push(` ${dim("Use --mode export-file for full matching records.")}`);
376
+ return lines.join("\n");
377
+ }
378
+
379
+ export function writeJsonlExport(kind, rows, metadata) {
380
+ const dir = join(process.cwd(), ".qveris", "exports");
381
+ mkdirSync(dir, { recursive: true });
382
+ const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
383
+ const path = join(dir, `${kind}_${timestamp}.jsonl`);
384
+ const body = rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length ? "\n" : "");
385
+ writeFileSync(path, body, "utf-8");
386
+ return {
387
+ mode: "export_file",
388
+ file_path: path,
389
+ format: "jsonl",
390
+ record_count: rows.length,
391
+ ...metadata,
392
+ };
393
+ }
394
+
395
+ export function formatExportMetadata(metadata) {
396
+ return [
397
+ `\n ${bold("Export written")}`,
398
+ ` ${dim("File:")} ${cyan(metadata.file_path)}`,
399
+ ` ${dim("Records:")} ${metadata.record_count}`,
400
+ ` ${dim("Format:")} ${metadata.format}`,
401
+ ` ${dim("Read in chunks with rg, jq, or a script; raw rows were not printed to protect context.")}`,
402
+ ].join("\n");
403
+ }
404
+
405
+ function appendBuckets(lines, buckets, render) {
406
+ const entries = Object.entries(buckets || {}).sort(([a], [b]) => a.localeCompare(b)).slice(0, 12);
407
+ if (entries.length === 0) return;
408
+ lines.push("");
409
+ lines.push(` ${bold("Buckets:")}`);
410
+ for (const [key, value] of entries) {
411
+ lines.push(` ${dim(key)} ${render(value)}`);
412
+ }
413
+ }
414
+
415
+ function appendUsageRows(lines, rows, title) {
416
+ if (!rows.length) return;
417
+ lines.push("");
418
+ lines.push(` ${bold(title + ":")}`);
419
+ for (const row of rows) {
420
+ const status = row.success ? green("succeeded") : red("failed");
421
+ lines.push(` ${dim(formatDateTime(row.created_at))} ${status} ${yellow(String(row.actual_amount_credits ?? row.amount ?? 0))} cr ${row.charge_outcome || "unknown"} ${row.target || ""}`);
422
+ if (row.execution_id) lines.push(` ${dim("execution:")} ${row.execution_id}`);
423
+ if (row.billing_summary) lines.push(` ${dim(row.billing_summary)}`);
424
+ if (row.error_message) lines.push(` ${red(row.error_message)}`);
425
+ }
426
+ }
427
+
428
+ function appendLedgerRows(lines, rows, title) {
429
+ if (!rows.length) return;
430
+ lines.push("");
431
+ lines.push(` ${bold(title + ":")}`);
432
+ for (const row of rows) {
433
+ const amount = row.amount_credits;
434
+ const amountText = amount < 0 ? red(String(amount)) : green(String(amount));
435
+ lines.push(` ${dim(formatDateTime(row.created_at))} ${amountText} cr ${row.entry_type || "unknown"} ${row.source_ref_id || ""}`);
436
+ if (row.billing_summary) lines.push(` ${dim(row.billing_summary)}`);
437
+ if (row.bucket_deductions?.length) {
438
+ lines.push(` ${dim("buckets:")} ${row.bucket_deductions.map(formatBucketDeduction).join(", ")}`);
439
+ }
440
+ }
441
+ }
442
+
443
+ function formatBucketDeduction(item) {
444
+ if (!item || typeof item !== "object") return "";
445
+ return `${item.bucket_type || item.bucket || "bucket"}:${item.amount ?? "?"}`;
446
+ }
447
+
448
+ function formatCounts(counts) {
449
+ return Object.entries(counts || {})
450
+ .sort(([a], [b]) => a.localeCompare(b))
451
+ .map(([key, value]) => `${key}=${value}`)
452
+ .join(", ");
453
+ }
454
+
455
+ function formatEntryTypes(entryTypes) {
456
+ return Object.entries(entryTypes || {})
457
+ .sort(([a], [b]) => a.localeCompare(b))
458
+ .map(([key, value]) => `${key}=${value.count}/${round(value.amount_credits)}cr`)
459
+ .join(", ");
460
+ }
461
+
462
+ function bucketKey(createdAt, bucket) {
463
+ const date = new Date(createdAt || Date.now());
464
+ if (!Number.isFinite(date.getTime())) return "unknown";
465
+ if (bucket === "hour") return date.toISOString().slice(0, 13) + ":00:00Z";
466
+ if (bucket === "week") return isoWeek(date);
467
+ return date.toISOString().slice(0, 10);
468
+ }
469
+
470
+ function isoWeek(date) {
471
+ const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
472
+ const dayNum = d.getUTCDay() || 7;
473
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
474
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
475
+ const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
476
+ return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
477
+ }
478
+
479
+ function isoDate(date) {
480
+ return date.toISOString().slice(0, 10);
481
+ }
482
+
483
+ function formatDateTime(value) {
484
+ if (!value) return "unknown";
485
+ const date = new Date(value);
486
+ return Number.isFinite(date.getTime()) ? date.toISOString().replace("T", " ").slice(0, 19) : String(value);
487
+ }
488
+
489
+ function firstNumber(...values) {
490
+ for (const value of values) {
491
+ const number = toNumber(value);
492
+ if (number !== 0 || value === 0 || value === "0") return number;
493
+ }
494
+ return 0;
495
+ }
496
+
497
+ function toNumber(value) {
498
+ const number = Number(value);
499
+ return Number.isFinite(number) ? number : 0;
500
+ }
501
+
502
+ function matchesAmount(amount, flags) {
503
+ const min = flags.minCredits === undefined ? undefined : Number(flags.minCredits);
504
+ const max = flags.maxCredits === undefined ? undefined : Number(flags.maxCredits);
505
+ if (Number.isFinite(min) && amount < min) return false;
506
+ if (Number.isFinite(max) && amount > max) return false;
507
+ return true;
508
+ }
509
+
510
+ function matchesField(value, expected) {
511
+ if (expected === undefined || expected === null || expected === "") return true;
512
+ return String(value ?? "") === String(expected);
513
+ }
514
+
515
+ function matchesBoolean(value, expected) {
516
+ if (expected === undefined) return true;
517
+ return Boolean(value) === expected;
518
+ }
519
+
520
+ function round(value) {
521
+ return Math.round((Number(value) || 0) * 1000) / 1000;
522
+ }
523
+
524
+ function setIf(target, key, value) {
525
+ if (value !== undefined && value !== null && value !== "") target[key] = value;
526
+ }
527
+
528
+ function numberValue(value) {
529
+ const number = Number(value);
530
+ return Number.isFinite(number) ? number : 0;
531
+ }
532
+
533
+ function dateOnly(value) {
534
+ if (!value) return "";
535
+ return String(value).slice(0, 10);
536
+ }
@@ -36,7 +36,7 @@ export function formatDiscoverResult(result) {
36
36
  successRate = typeof successRate === "number" ? `${(successRate * 100).toFixed(1)}%` : "N/A";
37
37
  let avgTime = stats.avg_execution_time_ms;
38
38
  avgTime = typeof avgTime === "number" ? `~${Math.round(avgTime)}ms` : "N/A";
39
- const cost = stats.cost ?? "?";
39
+ const billingText = formatBillingRuleBrief(t.billing_rule, stats.cost);
40
40
 
41
41
  // Verified indicator
42
42
  const verified = t.has_last_execution ? green(" \u2713") : "";
@@ -58,7 +58,7 @@ export function formatDiscoverResult(result) {
58
58
  if (scoreStr) metricParts.push(`relevance: ${bold(scoreStr)}`);
59
59
  metricParts.push(`success: ${green(successRate)}`);
60
60
  metricParts.push(`latency: ${avgTime}`);
61
- metricParts.push(`cost: ${yellow(String(cost))} cr`);
61
+ if (billingText) metricParts.push(`billing: ${yellow(billingText)}`);
62
62
  if (region !== "global") metricParts.push(`region: ${region}`);
63
63
  lines.push(` ${dim(metricParts.join(" \u00b7 "))}`);
64
64
 
@@ -101,7 +101,7 @@ export function formatInspectResult(tools) {
101
101
  successRate = typeof successRate === "number" ? `${(successRate * 100).toFixed(1)}%` : "N/A";
102
102
  let avgTime = stats.avg_execution_time_ms;
103
103
  avgTime = typeof avgTime === "number" ? `~${Math.round(avgTime)}ms` : "N/A";
104
- const cost = stats.cost ?? "?";
104
+ const billingText = formatBillingRuleBrief(t.billing_rule, stats.cost);
105
105
 
106
106
  // Header
107
107
  lines.push(bold(cyan(name)));
@@ -118,7 +118,7 @@ export function formatInspectResult(tools) {
118
118
  lines.push(` Region: ${region}`);
119
119
  lines.push(` Latency: ${bold(avgTime)}`);
120
120
  lines.push(` Success: ${green(successRate)}`);
121
- lines.push(` Cost: ${yellow(String(cost))} credits`);
121
+ lines.push(` Billing: ${yellow(billingText || "N/A")}`);
122
122
  if (t.has_last_execution) lines.push(` Verified: ${green("\u2713 has execution history")}`);
123
123
  if (docsUrl) lines.push(` Docs: ${cyan(docsUrl)}`);
124
124
 
@@ -171,7 +171,8 @@ export function formatInspectResult(tools) {
171
171
 
172
172
  export function formatCallResult(result) {
173
173
  const success = result.success ?? false;
174
- const cost = result.cost ?? result.credits_used ?? 0;
174
+ const billing = getCompactBilling(result);
175
+ const cost = getPreSettlementAmount(result);
175
176
  const remaining = result.remaining_credits;
176
177
  const executionId = result.execution_id;
177
178
  const toolId = result.tool_id;
@@ -188,7 +189,7 @@ export function formatCallResult(result) {
188
189
  }
189
190
  const parts = [`${green("\u2713")} ${bold("success")}`];
190
191
  if (timePart) parts.push(dim(timePart));
191
- if (cost) parts.push(`${yellow(String(cost))} credits`);
192
+ if (cost) parts.push(`${yellow(String(cost))} credits pre-settlement`);
192
193
  if (typeof remaining === "number") parts.push(dim(`(${remaining} remaining)`));
193
194
  lines.push(parts.join(" \u00b7 "));
194
195
  } else {
@@ -203,6 +204,26 @@ export function formatCallResult(result) {
203
204
  if (executionId) metaParts.push(`id: ${executionId}`);
204
205
  if (metaParts.length > 0) lines.push(dim(metaParts.join(" \u00b7 ")));
205
206
 
207
+ if (billing) {
208
+ lines.push("");
209
+ lines.push(bold("Billing:"));
210
+ if (billing.summary) lines.push(` ${billing.summary}`);
211
+ if (billing.list_amount_credits !== undefined) {
212
+ lines.push(` Pre-settlement: ${yellow(String(billing.list_amount_credits))} credits`);
213
+ }
214
+ const chargeLines = Array.isArray(billing.charge_lines) ? billing.charge_lines : [];
215
+ for (const line of chargeLines.slice(0, 5)) {
216
+ if (!line || typeof line !== "object") continue;
217
+ const label = line.description || line.component_key || "charge";
218
+ const amount = line.amount_credits ?? line.price?.amount_credits ?? "?";
219
+ const quantity = line.quantity !== undefined ? ` x ${line.quantity}` : "";
220
+ lines.push(` - ${label}${quantity}: ${yellow(String(amount))} credits`);
221
+ }
222
+ if (chargeLines.length > 5) lines.push(dim(` ... ${chargeLines.length - 5} more charge lines`));
223
+ }
224
+
225
+ if (executionId) lines.push(dim(`Final charge status: qveris usage --mode search --execution-id ${executionId}`));
226
+
206
227
  // Result data
207
228
  const data = result.result ?? {};
208
229
  const fullContentUrl = typeof data.full_content_file_url === "string" ? data.full_content_file_url : null;
@@ -278,3 +299,35 @@ function stringifyDesc(desc) {
278
299
  }
279
300
  return String(desc);
280
301
  }
302
+
303
+ function formatBillingRuleBrief(rule, legacyCost) {
304
+ if (rule && typeof rule === "object") {
305
+ if (typeof rule.description === "string" && rule.description.trim()) {
306
+ return rule.description.trim();
307
+ }
308
+ const price = rule.price && typeof rule.price === "object" ? rule.price : null;
309
+ const amount = price?.amount_credits;
310
+ const unit = rule.billing_unit_label || rule.billing_unit || price?.unit_label || price?.unit;
311
+ if (amount !== undefined && unit) return `${amount} credits / ${unit}`;
312
+ if (amount !== undefined) return `${amount} credits`;
313
+ if (rule.billing_unit_label || rule.billing_unit) return String(rule.billing_unit_label || rule.billing_unit);
314
+ }
315
+ if (legacyCost !== undefined && legacyCost !== null) return `${legacyCost} credits (legacy estimate)`;
316
+ return "";
317
+ }
318
+
319
+ function getCompactBilling(result) {
320
+ if (!result || typeof result !== "object") return null;
321
+ if (result.billing && typeof result.billing === "object") return result.billing;
322
+ if (result.pre_settlement_bill && typeof result.pre_settlement_bill === "object") return result.pre_settlement_bill;
323
+ return null;
324
+ }
325
+
326
+ function getPreSettlementAmount(result) {
327
+ const billing = getCompactBilling(result);
328
+ if (billing) {
329
+ if (typeof billing.list_amount_credits === "number") return billing.list_amount_credits;
330
+ if (typeof billing.requested_amount_credits === "number") return billing.requested_amount_credits;
331
+ }
332
+ return result.cost ?? result.credits_used ?? 0;
333
+ }