@qverisai/cli 0.4.0 → 0.6.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,378 @@
1
+ import { resolveApiKey } from "../client/auth.mjs";
2
+ import { callTool, discoverTools, inspectToolsByIds } from "../client/api.mjs";
3
+ import { resolve } from "../config/resolve.mjs";
4
+ import { resolveBaseUrl } from "../config/region.mjs";
5
+ import { CliError } from "../errors/handler.mjs";
6
+ import { bold, cyan, dim, green, red, yellow } from "../output/colors.mjs";
7
+ import { outputJson } from "../output/json.mjs";
8
+ import { readSession, writeSession } from "../session/session.mjs";
9
+ import { resolveParams } from "../utils/params.mjs";
10
+
11
+ const DEFAULT_QUERY = "weather forecast API";
12
+ const DEFAULT_LIMIT = 5;
13
+ const DEFAULT_MAX_RESPONSE_SIZE = 20480;
14
+
15
+ export async function runInit(queryArg, flags) {
16
+ const steps = [];
17
+ const startedAt = Date.now();
18
+
19
+ if (!flags.json) {
20
+ console.log(`\n ${bold("QVeris init")} ${dim("first-call wizard")}\n`);
21
+ }
22
+
23
+ const timeoutMs = (parseInt(flags.timeout, 10) || 60) * 1000;
24
+ const apiKey = getInitApiKey(flags);
25
+ const { region, baseUrl, source: regionSource } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
26
+ record(steps, "auth", "ok", "API key resolved", {
27
+ source: resolve("api_key", flags.apiKey || flags.token).source,
28
+ key: maskKey(apiKey),
29
+ region,
30
+ base_url: baseUrl,
31
+ region_source: regionSource,
32
+ });
33
+
34
+ let discovery;
35
+ let discoveryId;
36
+ let selected;
37
+ let candidateTools = [];
38
+ const query = flags.query || queryArg || DEFAULT_QUERY;
39
+ const limit = parseInt(flags.limit, 10) || DEFAULT_LIMIT;
40
+ const maxResponseSize = resolveInitMaxResponseSize(flags);
41
+ const flagParameters = flags.params ? resolveParams(flags.params) : null;
42
+
43
+ if (flags.resume) {
44
+ const session = readSession();
45
+ if (!session?.discoveryId || !Array.isArray(session.results) || session.results.length === 0) {
46
+ throw new CliError("SESSION_EXPIRED", "No resumable init session found. Run 'qveris init' without --resume first.");
47
+ }
48
+ discoveryId = session.discoveryId;
49
+ candidateTools = flags.toolId
50
+ ? [{ tool_id: flags.toolId, name: flags.toolId }]
51
+ : session.results;
52
+ discovery = {
53
+ search_id: discoveryId,
54
+ query: session.query,
55
+ results: session.results,
56
+ resumed: true,
57
+ };
58
+ record(steps, "discover", "ok", "Resumed previous discovery session", {
59
+ query: session.query,
60
+ search_id: discoveryId,
61
+ });
62
+ } else {
63
+ record(steps, "discover", "running", `Discovering capabilities for "${query}"`, { query, limit });
64
+ discovery = await discoverTools({ apiKey, baseUrl, query, limit, timeoutMs });
65
+ const results = discovery.results ?? [];
66
+ if (results.length === 0) {
67
+ throw new CliError("TOOL_NOT_FOUND", `No capabilities matched "${query}". Try 'qveris init --query <broader-query>'.`);
68
+ }
69
+ discoveryId = discovery.search_id;
70
+ candidateTools = flags.toolId
71
+ ? [{ tool_id: flags.toolId, name: flags.toolId }]
72
+ : results;
73
+ writeSession({
74
+ discoveryId,
75
+ query,
76
+ region,
77
+ baseUrl,
78
+ results: results.map((t, i) => ({
79
+ index: i + 1,
80
+ tool_id: t.tool_id,
81
+ name: t.name,
82
+ provider_name: t.provider_name,
83
+ })),
84
+ });
85
+ updateLast(steps, "ok", "Discovery completed", {
86
+ query,
87
+ search_id: discoveryId,
88
+ total: discovery.total ?? results.length,
89
+ });
90
+ }
91
+
92
+ const candidateToolIds = candidateTools.map((tool) => tool?.tool_id).filter(Boolean);
93
+ if (candidateToolIds.length === 0) {
94
+ throw new CliError("TOOL_NOT_FOUND", "No selectable capabilities were returned.");
95
+ }
96
+
97
+ record(
98
+ steps,
99
+ "inspect",
100
+ "running",
101
+ candidateToolIds.length === 1 ? "Inspecting selected capability" : "Inspecting candidate capabilities",
102
+ { tool_ids: candidateToolIds }
103
+ );
104
+ const inspected = await inspectToolsByIds({
105
+ apiKey,
106
+ baseUrl,
107
+ toolIds: candidateToolIds,
108
+ discoveryId,
109
+ timeoutMs,
110
+ });
111
+ const inspectedTools = mergeCandidateTools(candidateTools, normalizeToolList(inspected));
112
+ selected = pickInitTool(inspectedTools, { toolId: flags.toolId, parameters: flagParameters });
113
+ const inspectedTool = pickInspectedTool(inspectedTools, selected.tool_id) || selected;
114
+ updateLast(steps, "ok", "Inspection completed", {
115
+ tool_id: inspectedTool.tool_id || selected.tool_id,
116
+ tool_name: inspectedTool.name || selected.name,
117
+ selected_reason: flagParameters && !flags.toolId ? "params_match" : flags.toolId ? "explicit_tool" : "first_candidate",
118
+ has_sample_parameters: Boolean(getSampleParameters(inspectedTool)),
119
+ });
120
+
121
+ const { parameters, source: paramsSource } = getInitParameters(flags, inspectedTool, flagParameters);
122
+ record(steps, "call", flags.dryRun ? "skipped" : "running", flags.dryRun ? "Dry run requested" : "Calling selected capability", {
123
+ tool_id: selected.tool_id,
124
+ params_source: paramsSource,
125
+ max_response_size: maxResponseSize,
126
+ });
127
+
128
+ const nextCommands = buildNextCommands({ selected, discoveryId, parameters });
129
+
130
+ if (flags.dryRun) {
131
+ const payload = buildPayload({ steps, discovery, inspectedTool, parameters, callResult: null, nextCommands, startedAt, dryRun: true });
132
+ if (flags.json) outputJson(payload);
133
+ else printHumanSummary(payload);
134
+ return;
135
+ }
136
+
137
+ const callResult = await callTool({
138
+ apiKey,
139
+ baseUrl,
140
+ toolId: selected.tool_id,
141
+ discoveryId,
142
+ parameters,
143
+ maxResponseSize,
144
+ timeoutMs,
145
+ });
146
+
147
+ if (!callResult.success) {
148
+ const code = looksLikeProviderFailure(callResult.error_message) ? "PROVIDER_FAILURE" : "TOOL_CALL_FAILED";
149
+ const err = new CliError(code, callResult.error_message || "The selected capability returned success=false.");
150
+ if (code === "TOOL_CALL_FAILED") {
151
+ err.hint = `Rerun with adjusted params: qveris init --resume --params ${shellSingleQuote(JSON.stringify(parameters))}`;
152
+ }
153
+ throw err;
154
+ }
155
+
156
+ updateLast(steps, "ok", "First call succeeded", {
157
+ execution_id: callResult.execution_id,
158
+ elapsed_time_ms: callResult.elapsed_time_ms ?? callResult.execution_time,
159
+ });
160
+ const finalNextCommands = {
161
+ ...nextCommands,
162
+ usage: nextCommands.usage.replace("<execution_id>", callResult.execution_id),
163
+ };
164
+ record(steps, "audit", "ok", "Use usage and ledger commands to reconcile final billing", {
165
+ usage: finalNextCommands.usage,
166
+ ledger: finalNextCommands.ledger,
167
+ });
168
+
169
+ const payload = buildPayload({ steps, discovery, inspectedTool, parameters, callResult, nextCommands, startedAt, dryRun: false });
170
+ if (flags.json) outputJson(payload);
171
+ else printHumanSummary(payload);
172
+ }
173
+
174
+ function getInitApiKey(flags) {
175
+ try {
176
+ return resolveApiKey(flags.apiKey || flags.token);
177
+ } catch (err) {
178
+ if (err instanceof CliError && err.code === "AUTH_MISSING_KEY") {
179
+ err.hint = "Run 'qveris login', set QVERIS_API_KEY, or pass --api-key/--token to qveris init.";
180
+ }
181
+ throw err;
182
+ }
183
+ }
184
+
185
+ function getInitParameters(flags, tool, flagParameters = null) {
186
+ if (flags.params) {
187
+ return { parameters: flagParameters ?? resolveParams(flags.params), source: "flag" };
188
+ }
189
+
190
+ const sample = getSampleParameters(tool);
191
+ if (sample) {
192
+ return { parameters: sample, source: "example" };
193
+ }
194
+
195
+ throw new CliError("INIT_PARAMS_REQUIRED");
196
+ }
197
+
198
+ function getSampleParameters(tool) {
199
+ const examples = tool?.examples;
200
+ if (!examples || typeof examples !== "object") return null;
201
+ if (examples.sample_parameters && typeof examples.sample_parameters === "object") {
202
+ return examples.sample_parameters;
203
+ }
204
+ if (Array.isArray(examples) && examples[0]?.parameters && typeof examples[0].parameters === "object") {
205
+ return examples[0].parameters;
206
+ }
207
+ if (examples.parameters && typeof examples.parameters === "object") {
208
+ return examples.parameters;
209
+ }
210
+ return null;
211
+ }
212
+
213
+ function pickInspectedTool(response, toolId) {
214
+ const list = normalizeToolList(response);
215
+ if (!Array.isArray(list)) return null;
216
+ return (toolId ? list.find((t) => t?.tool_id === toolId) : list[0]) || null;
217
+ }
218
+
219
+ function normalizeToolList(response) {
220
+ if (Array.isArray(response)) return response;
221
+ return response?.results ?? response?.tools ?? [];
222
+ }
223
+
224
+ function mergeCandidateTools(discoveredTools, inspectedTools) {
225
+ const inspectedById = new Map(
226
+ normalizeToolList(inspectedTools)
227
+ .filter((tool) => tool?.tool_id)
228
+ .map((tool) => [tool.tool_id, tool])
229
+ );
230
+ return discoveredTools.map((tool) => ({
231
+ ...tool,
232
+ ...(inspectedById.get(tool.tool_id) ?? {}),
233
+ }));
234
+ }
235
+
236
+ export function pickInitTool(tools, { toolId, parameters } = {}) {
237
+ const list = normalizeToolList(tools).filter((tool) => tool?.tool_id);
238
+ if (toolId) return list.find((tool) => tool.tool_id === toolId) || { tool_id: toolId, name: toolId };
239
+ if (!parameters) return list[0];
240
+
241
+ const ranked = list
242
+ .map((tool, index) => ({ tool, index, score: scoreParameterMatch(tool, parameters) }))
243
+ .filter((item) => item.score > Number.NEGATIVE_INFINITY)
244
+ .sort((a, b) => b.score - a.score || a.index - b.index);
245
+
246
+ if (ranked.length > 0) return ranked[0].tool;
247
+
248
+ const err = new CliError("INIT_PARAMS_REQUIRED", "Provided --params do not satisfy any discovered capability.");
249
+ err.hint = `Inspect candidates and retry with matching params: ${summarizeRequiredParams(list)}`;
250
+ throw err;
251
+ }
252
+
253
+ function scoreParameterMatch(tool, parameters) {
254
+ const schema = Array.isArray(tool?.params) ? tool.params : [];
255
+ if (schema.length === 0) return 0;
256
+
257
+ const provided = new Set(Object.keys(parameters ?? {}));
258
+ const required = schema.filter((param) => param?.required).map((param) => param.name).filter(Boolean);
259
+ const allNames = schema.map((param) => param?.name).filter(Boolean);
260
+ const missingRequired = required.filter((name) => !provided.has(name));
261
+ if (missingRequired.length > 0) return Number.NEGATIVE_INFINITY;
262
+
263
+ const overlap = allNames.filter((name) => provided.has(name)).length;
264
+ if (overlap === 0 && required.length > 0) return Number.NEGATIVE_INFINITY;
265
+ return required.length * 10 + overlap;
266
+ }
267
+
268
+ function summarizeRequiredParams(tools) {
269
+ return tools.slice(0, 5).map((tool, index) => {
270
+ const required = (Array.isArray(tool?.params) ? tool.params : [])
271
+ .filter((param) => param?.required)
272
+ .map((param) => param.name)
273
+ .filter(Boolean);
274
+ return `${index + 1}. ${tool.tool_id}: ${required.length ? required.join(", ") : "no required params"}`;
275
+ }).join("; ");
276
+ }
277
+
278
+ export function resolveInitMaxResponseSize(flags) {
279
+ if (flags.maxSize === undefined) return DEFAULT_MAX_RESPONSE_SIZE;
280
+ const parsed = parseInt(flags.maxSize, 10);
281
+ if (!Number.isFinite(parsed)) throw new CliError("API_ERROR", "Invalid --max-size: must be an integer");
282
+ return parsed;
283
+ }
284
+
285
+ export function shellSingleQuote(value) {
286
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
287
+ }
288
+
289
+ export function buildNextCommands({ selected, discoveryId, parameters }) {
290
+ const paramsJson = shellSingleQuote(JSON.stringify(parameters));
291
+ const executionPlaceholder = "<execution_id>";
292
+ return {
293
+ retry: `qveris call ${selected.tool_id} --discovery-id ${discoveryId} --params ${paramsJson}`,
294
+ usage: `qveris usage --mode search --execution-id ${executionPlaceholder}`,
295
+ ledger: "qveris ledger --mode summary",
296
+ };
297
+ }
298
+
299
+ function buildPayload({ steps, discovery, inspectedTool, parameters, callResult, nextCommands, startedAt, dryRun }) {
300
+ return {
301
+ ok: dryRun ? true : Boolean(callResult?.success),
302
+ dry_run: dryRun,
303
+ elapsed_ms: Date.now() - startedAt,
304
+ steps,
305
+ discovery: {
306
+ query: discovery?.query,
307
+ search_id: discovery?.search_id,
308
+ total: discovery?.total ?? discovery?.results?.length,
309
+ },
310
+ selected_tool: {
311
+ tool_id: inspectedTool?.tool_id,
312
+ name: inspectedTool?.name,
313
+ provider_name: inspectedTool?.provider_name,
314
+ },
315
+ parameters,
316
+ call: callResult ? {
317
+ success: callResult.success,
318
+ execution_id: callResult.execution_id,
319
+ elapsed_time_ms: callResult.elapsed_time_ms ?? callResult.execution_time,
320
+ billing: callResult.billing ?? callResult.pre_settlement_bill,
321
+ } : null,
322
+ next_commands: callResult?.execution_id
323
+ ? { ...nextCommands, usage: nextCommands.usage.replace("<execution_id>", callResult.execution_id) }
324
+ : nextCommands,
325
+ };
326
+ }
327
+
328
+ function record(steps, name, status, message, data = {}) {
329
+ steps.push({ name, status, message, ...data });
330
+ }
331
+
332
+ function updateLast(steps, status, message, data = {}) {
333
+ const last = steps[steps.length - 1];
334
+ Object.assign(last, { status, message }, data);
335
+ }
336
+
337
+ function printHumanSummary(payload) {
338
+ for (let i = 0; i < payload.steps.length; i++) {
339
+ const step = payload.steps[i];
340
+ const icon = step.status === "ok" ? green("\u2713") : step.status === "skipped" ? yellow("!") : red("\u2718");
341
+ console.log(` ${icon} ${i + 1}/5 ${bold(step.name)} ${dim(step.message)}`);
342
+ if (step.name === "auth") {
343
+ console.log(` ${dim("key")} ${step.key} ${dim("region")} ${step.region} ${dim(step.base_url)}`);
344
+ }
345
+ if (step.name === "discover") {
346
+ console.log(` ${dim("search_id")} ${step.search_id || payload.discovery.search_id}`);
347
+ }
348
+ if (step.name === "inspect" && step.tool_id) {
349
+ console.log(` ${dim("selected")} ${cyan(step.tool_id)}`);
350
+ }
351
+ if (step.name === "call" && step.execution_id) {
352
+ console.log(` ${dim("execution_id")} ${step.execution_id}`);
353
+ }
354
+ }
355
+
356
+ console.log(`\n ${green("First-call path complete.")}`);
357
+ if (payload.dry_run) {
358
+ console.log(` ${yellow("Dry run only:")} remove --dry-run to perform the call.`);
359
+ }
360
+ if (payload.call?.execution_id) {
361
+ console.log(`\n ${bold("Reconcile final billing:")}`);
362
+ console.log(` ${cyan(payload.next_commands.usage)}`);
363
+ console.log(` ${cyan(payload.next_commands.ledger)}`);
364
+ }
365
+ console.log(`\n ${dim("Retry selected capability:")}`);
366
+ console.log(` ${cyan(payload.next_commands.retry)}\n`);
367
+ }
368
+
369
+ function maskKey(key) {
370
+ if (!key) return "none";
371
+ if (key.length <= 10) return "***";
372
+ return `${key.slice(0, 6)}...${key.slice(-4)}`;
373
+ }
374
+
375
+ function looksLikeProviderFailure(message = "") {
376
+ const text = String(message).toLowerCase();
377
+ return text.includes("provider") || text.includes("upstream") || text.includes("third-party");
378
+ }
@@ -14,8 +14,7 @@ import { createSpinner } from "../output/spinner.mjs";
14
14
 
15
15
  export async function runInteractive(flags) {
16
16
  const apiKey = resolveApiKey(flags.apiKey);
17
- const baseUrl = flags.baseUrl;
18
- const { region, baseUrl: resolvedBaseUrl } = resolveBaseUrl({ baseUrlFlag: baseUrl, apiKey });
17
+ const { region, baseUrl: resolvedBaseUrl } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
19
18
  const limit = parseInt(flags.limit, 10) || 5;
20
19
  const discoverTimeout = (parseInt(flags.timeout, 10) || 30) * 1000;
21
20
  const callTimeout = (parseInt(flags.timeout, 10) || 60) * 1000;
@@ -58,7 +57,7 @@ export async function runInteractive(flags) {
58
57
  const query = rest.join(" ");
59
58
  if (!query) { console.log(" Usage: discover <query>"); break; }
60
59
  const sp = createSpinner("Discovering...");
61
- const result = await discoverTools({ apiKey, baseUrl, query, limit, timeoutMs: discoverTimeout });
60
+ const result = await discoverTools({ apiKey, baseUrl: resolvedBaseUrl, query, limit, timeoutMs: discoverTimeout });
62
61
  sp.stop();
63
62
  state.discoveryId = result.search_id;
64
63
  state.results = (result.results ?? []).map((t, i) => ({
@@ -73,7 +72,7 @@ export async function runInteractive(flags) {
73
72
  const toolId = resolveId(rest[0], state);
74
73
  if (!toolId) { console.log(" Usage: inspect <index|tool_id>"); break; }
75
74
  const sp2 = createSpinner("Inspecting...");
76
- const result = await inspectToolsByIds({ apiKey, baseUrl, toolIds: [toolId], discoveryId: state.discoveryId, timeoutMs: discoverTimeout });
75
+ const result = await inspectToolsByIds({ apiKey, baseUrl: resolvedBaseUrl, toolIds: [toolId], discoveryId: state.discoveryId, timeoutMs: discoverTimeout });
77
76
  sp2.stop();
78
77
  console.log(formatInspectResult(result));
79
78
  break;
@@ -85,7 +84,7 @@ export async function runInteractive(flags) {
85
84
  const paramsStr = rest.slice(1).join(" ") || "{}";
86
85
  const parameters = resolveParams(paramsStr);
87
86
  const sp3 = createSpinner("Calling...");
88
- const result = await callTool({ apiKey, baseUrl, toolId, discoveryId: state.discoveryId, parameters, timeoutMs: callTimeout });
87
+ const result = await callTool({ apiKey, baseUrl: resolvedBaseUrl, toolId, discoveryId: state.discoveryId, parameters, timeoutMs: callTimeout });
89
88
  sp3.stop();
90
89
  console.log(formatCallResult(result));
91
90
  if (result.success) {
@@ -0,0 +1,186 @@
1
+ import { resolveApiKey } from "../client/auth.mjs";
2
+ import { getCreditsLedger, unwrapApiResponse } from "../client/api.mjs";
3
+ import { outputJson } from "../output/json.mjs";
4
+ import { createSpinner } from "../output/spinner.mjs";
5
+ import {
6
+ DEFAULT_PAGE_SIZE,
7
+ MAX_EXPORT_ROWS,
8
+ MAX_SEARCH_SCAN_ROWS,
9
+ MAX_SUMMARY_ROWS,
10
+ buildLedgerQuery,
11
+ buildLedgerSummary,
12
+ buildLedgerSummaryFromServer,
13
+ chooseBucket,
14
+ clampLimit,
15
+ extractItems,
16
+ extractTotal,
17
+ formatExportMetadata,
18
+ formatLedgerRows,
19
+ formatLedgerSummary,
20
+ matchesLedgerFilters,
21
+ pickLedgerRow,
22
+ resolveDateRange,
23
+ resolveMode,
24
+ writeJsonlExport,
25
+ } from "../output/audit.mjs";
26
+
27
+ export async function runLedger(flags) {
28
+ const apiKey = resolveApiKey(flags.apiKey);
29
+ const mode = resolveMode(flags.mode);
30
+ const timeoutMs = (parseInt(flags.timeout, 10) || 30) * 1000;
31
+ const limit = clampLimit(flags.limit);
32
+ const { startDate, endDate } = resolveDateRange(flags);
33
+ const bucket = chooseBucket(startDate, endDate, flags.bucket);
34
+
35
+ const spinner = flags.json ? { stop() {} } : createSpinner("Querying credits ledger...");
36
+
37
+ try {
38
+ if (mode === "search") {
39
+ const { rows, total, partial } = await collectLedgerRows({
40
+ apiKey,
41
+ flags,
42
+ timeoutMs,
43
+ limit,
44
+ maxRows: MAX_SEARCH_SCAN_ROWS,
45
+ stopWhenLimitReached: true,
46
+ });
47
+ spinner.stop();
48
+ const payload = {
49
+ mode,
50
+ start_date: startDate,
51
+ end_date: endDate,
52
+ shown_records: rows.length,
53
+ matched_records: total,
54
+ truncated: partial,
55
+ items: rows.map(pickLedgerRow),
56
+ };
57
+ if (flags.json) outputJson(payload);
58
+ else console.log(formatLedgerRows(rows, { total, partial }));
59
+ return;
60
+ }
61
+
62
+ if (mode === "export_file") {
63
+ const { rows, total, partial } = await collectLedgerRows({
64
+ apiKey,
65
+ flags,
66
+ timeoutMs,
67
+ limit: MAX_EXPORT_ROWS,
68
+ maxRows: MAX_EXPORT_ROWS,
69
+ stopWhenLimitReached: false,
70
+ });
71
+ spinner.stop();
72
+ const metadata = writeJsonlExport("credits_ledger", rows, {
73
+ start_date: startDate,
74
+ end_date: endDate,
75
+ matched_records: total,
76
+ truncated: partial,
77
+ filters: ledgerFilters(flags),
78
+ });
79
+ if (flags.json) outputJson(metadata);
80
+ else console.log(formatExportMetadata(metadata));
81
+ return;
82
+ }
83
+
84
+ const summaryResponse = unwrapApiResponse(await getCreditsLedger({
85
+ apiKey,
86
+ baseUrl: flags.baseUrl,
87
+ query: buildLedgerQuery(flags, {
88
+ page: 1,
89
+ pageSize: limit,
90
+ mode: "summary",
91
+ }),
92
+ timeoutMs,
93
+ }));
94
+ if (summaryResponse?.summary) {
95
+ spinner.stop();
96
+ const rows = extractItems(summaryResponse);
97
+ const total = extractTotal(summaryResponse, rows.length);
98
+ const summary = buildLedgerSummaryFromServer(summaryResponse.summary, { startDate, endDate, bucket });
99
+ if (flags.json) {
100
+ outputJson({
101
+ mode,
102
+ summary,
103
+ scanned_records: rows.length,
104
+ matched_records: total,
105
+ truncated: false,
106
+ });
107
+ } else {
108
+ console.log(formatLedgerSummary(summary, { scannedRows: rows.length, total, partial: false }));
109
+ }
110
+ return;
111
+ }
112
+
113
+ const { rows, total, partial, scannedRows } = await collectLedgerRows({
114
+ apiKey,
115
+ flags,
116
+ timeoutMs,
117
+ limit: MAX_SUMMARY_ROWS,
118
+ maxRows: MAX_SUMMARY_ROWS,
119
+ stopWhenLimitReached: false,
120
+ });
121
+ spinner.stop();
122
+ const summary = buildLedgerSummary(rows, { startDate, endDate, bucket });
123
+ if (flags.json) {
124
+ outputJson({
125
+ mode,
126
+ summary,
127
+ scanned_records: scannedRows,
128
+ matched_records: total,
129
+ truncated: partial,
130
+ });
131
+ } else {
132
+ console.log(formatLedgerSummary(summary, { scannedRows, total, partial }));
133
+ }
134
+ } catch (err) {
135
+ spinner.stop();
136
+ throw err;
137
+ }
138
+ }
139
+
140
+ async function collectLedgerRows({ apiKey, flags, timeoutMs, limit, maxRows, stopWhenLimitReached }) {
141
+ const rows = [];
142
+ let page = 1;
143
+ let total;
144
+ let scannedRows = 0;
145
+ let partial = false;
146
+
147
+ while (rows.length < limit && scannedRows < maxRows) {
148
+ const query = buildLedgerQuery(flags, { page, pageSize: Math.min(DEFAULT_PAGE_SIZE, maxRows - scannedRows) });
149
+ const response = unwrapApiResponse(await getCreditsLedger({
150
+ apiKey,
151
+ baseUrl: flags.baseUrl,
152
+ query,
153
+ timeoutMs,
154
+ }));
155
+ const items = extractItems(response);
156
+ if (total === undefined) total = extractTotal(response, undefined);
157
+ if (items.length === 0) break;
158
+ scannedRows += items.length;
159
+ for (const item of items) {
160
+ if (!matchesLedgerFilters(item, flags)) continue;
161
+ rows.push(item);
162
+ if (stopWhenLimitReached && rows.length >= limit) break;
163
+ }
164
+ if (stopWhenLimitReached && rows.length >= limit) {
165
+ partial = Boolean(total && total > scannedRows);
166
+ break;
167
+ }
168
+ if (total !== undefined && scannedRows >= total) break;
169
+ if (items.length < DEFAULT_PAGE_SIZE) break;
170
+ page += 1;
171
+ }
172
+
173
+ if (total !== undefined && scannedRows < total) partial = true;
174
+ if (scannedRows >= maxRows && (total === undefined || scannedRows < total)) partial = true;
175
+
176
+ return { rows: rows.slice(0, limit), total, partial, scannedRows };
177
+ }
178
+
179
+ function ledgerFilters(flags) {
180
+ return {
181
+ entry_type: flags.entryType,
182
+ direction: flags.direction,
183
+ min_credits: flags.minCredits,
184
+ max_credits: flags.maxCredits,
185
+ };
186
+ }