@qverisai/cli 0.4.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.
- package/README.md +48 -8
- package/package.json +1 -1
- package/src/client/api.mjs +63 -1
- package/src/commands/completions.mjs +1 -1
- package/src/commands/credits.mjs +18 -4
- package/src/commands/discover.mjs +2 -2
- package/src/commands/doctor.mjs +1 -1
- package/src/commands/interactive.mjs +4 -5
- package/src/commands/ledger.mjs +186 -0
- package/src/commands/usage.mjs +190 -0
- package/src/compat/aliases.mjs +4 -0
- package/src/main.mjs +52 -0
- package/src/output/audit.mjs +536 -0
- package/src/output/formatter.mjs +59 -6
|
@@ -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
|
+
}
|
package/src/output/formatter.mjs
CHANGED
|
@@ -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
|
|
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(`
|
|
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
|
|
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(`
|
|
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
|
|
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
|
+
}
|