@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
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@ Found 5 capabilities matching your query
|
|
|
9
9
|
1. gridpoint_forecast by Weather.gov
|
|
10
10
|
weather_gov.gridpoints.forecast.retrieve.v1
|
|
11
11
|
Returns a textual forecast for a 2.5km grid area
|
|
12
|
-
relevance: 95% · success: 99.8% · latency: ~180ms ·
|
|
12
|
+
relevance: 95% · success: 99.8% · latency: ~180ms · billing: 3 credits / request
|
|
13
13
|
|
|
14
14
|
2. current_weather by OpenWeather
|
|
15
15
|
openweathermap.weather.current.v1
|
|
16
16
|
Get current weather data for any location
|
|
17
|
-
relevance: 87% · success: 99.5% · latency: ~200ms ·
|
|
17
|
+
relevance: 87% · success: 99.5% · latency: ~200ms · billing: 2 credits / request
|
|
18
18
|
|
|
19
19
|
$ qveris inspect 1
|
|
20
20
|
gridpoint_forecast
|
|
@@ -23,7 +23,7 @@ Returns a textual forecast for a 2.5km grid area
|
|
|
23
23
|
Provider: Weather.gov
|
|
24
24
|
Latency: ~180ms
|
|
25
25
|
Success: 99.8%
|
|
26
|
-
|
|
26
|
+
Billing: 3 credits / request
|
|
27
27
|
|
|
28
28
|
Parameters:
|
|
29
29
|
wfo string required
|
|
@@ -38,9 +38,13 @@ Returns a textual forecast for a 2.5km grid area
|
|
|
38
38
|
{"wfo": "LWX", "x": 90, "y": 90}
|
|
39
39
|
|
|
40
40
|
$ qveris call 1 --params '{"wfo":"LWX","x":90,"y":90}'
|
|
41
|
-
✓ success · 523ms · 10 credits · (1368087.93 remaining)
|
|
41
|
+
✓ success · 523ms · 10 credits pre-settlement · (1368087.93 remaining)
|
|
42
42
|
tool: weather_gov... · id: 7ebcaf9d-...
|
|
43
43
|
|
|
44
|
+
Billing:
|
|
45
|
+
10 credits per successful request
|
|
46
|
+
Pre-settlement: 10 credits
|
|
47
|
+
|
|
44
48
|
{
|
|
45
49
|
"type": "Feature",
|
|
46
50
|
"properties": {
|
|
@@ -96,9 +100,9 @@ qveris call 1 --params '{"wfo": "LWX", "x": 90, "y": 90}'
|
|
|
96
100
|
|
|
97
101
|
| Command | Description |
|
|
98
102
|
|---------|-------------|
|
|
99
|
-
| `qveris discover <query>` | Find capabilities by natural language. Shows tool ID, provider, description, relevance, success rate, latency, and
|
|
103
|
+
| `qveris discover <query>` | Find capabilities by natural language. Shows tool ID, provider, description, relevance, success rate, latency, and billing rule when available. |
|
|
100
104
|
| `qveris inspect <id\|index>` | View full tool details: parameters (type, required, description, enum values), example, provider info, execution history. |
|
|
101
|
-
| `qveris call <id\|index>` | Execute a capability. Shows result data, execution time,
|
|
105
|
+
| `qveris call <id\|index>` | Execute a capability. Shows result data, execution time, pre-settlement billing, and remaining credits. |
|
|
102
106
|
|
|
103
107
|
### Account
|
|
104
108
|
|
|
@@ -108,6 +112,8 @@ qveris call 1 --params '{"wfo": "LWX", "x": 90, "y": 90}'
|
|
|
108
112
|
| `qveris logout` | Remove stored key |
|
|
109
113
|
| `qveris whoami` | Show current auth status, key source, and region |
|
|
110
114
|
| `qveris credits` | Check credit balance |
|
|
115
|
+
| `qveris usage` | Context-safe usage audit summary/search/export |
|
|
116
|
+
| `qveris ledger` | Context-safe credits ledger summary/search/export |
|
|
111
117
|
|
|
112
118
|
### Utilities
|
|
113
119
|
|
|
@@ -146,7 +152,7 @@ qveris inspect 1 2 3
|
|
|
146
152
|
|
|
147
153
|
### Call
|
|
148
154
|
|
|
149
|
-
Execute a tool. Results are automatically truncated for terminal display (4KB). Large results get an OSS download link.
|
|
155
|
+
Execute a tool. Results are automatically truncated for terminal display (4KB). Large results get an OSS download link. Billing shown here is pre-settlement; use `qveris usage` or `qveris ledger` for final charge status.
|
|
150
156
|
|
|
151
157
|
```bash
|
|
152
158
|
# Inline params
|
|
@@ -178,7 +184,7 @@ For terminal use, results larger than 4KB are automatically truncated. The CLI s
|
|
|
178
184
|
- The response schema so you know the data structure
|
|
179
185
|
|
|
180
186
|
```
|
|
181
|
-
✓ success · 1200ms · 5 credits
|
|
187
|
+
✓ success · 1200ms · 5 credits pre-settlement
|
|
182
188
|
|
|
183
189
|
Response truncated (32KB → 4KB preview)
|
|
184
190
|
|
|
@@ -199,6 +205,40 @@ Preview:
|
|
|
199
205
|
|
|
200
206
|
For agent/script use (`--json` or piped output), the default increases to 20KB (matching the MCP server). Use `--max-size -1` for unlimited.
|
|
201
207
|
|
|
208
|
+
### Usage Audit
|
|
209
|
+
|
|
210
|
+
Use `qveris usage` to answer whether recent calls succeeded, failed, or charged credits. It defaults to `summary` mode to protect Agent context.
|
|
211
|
+
Summary mode requests service-side `summary=true` aggregates when available and falls back to bounded client-side aggregation for older deployments.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Aggregate recent usage by hour/day/week
|
|
215
|
+
qveris usage --mode summary --bucket hour
|
|
216
|
+
|
|
217
|
+
# Check one execution precisely
|
|
218
|
+
qveris usage --mode search --execution-id <execution_id> --json
|
|
219
|
+
|
|
220
|
+
# Find high-cost or suspicious records
|
|
221
|
+
qveris usage --mode search --min-credits 30 --max-credits 100 --json
|
|
222
|
+
qveris usage --mode search --charge-outcome failed_charged_review --json
|
|
223
|
+
|
|
224
|
+
# Export raw matching rows to a local JSONL file instead of stdout
|
|
225
|
+
qveris usage --mode export-file --start-date 2026-05-01 --end-date 2026-05-04
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Credits Ledger
|
|
229
|
+
|
|
230
|
+
Use `qveris ledger` to explain final credit balance movements. It also defaults to `summary` mode.
|
|
231
|
+
Summary mode requests service-side `summary=true` aggregates when available and falls back to bounded client-side aggregation for older deployments.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
qveris ledger --mode summary --bucket day
|
|
235
|
+
qveris ledger --mode search --direction consume --min-credits 50 --json
|
|
236
|
+
qveris ledger --mode search --entry-type consume_tool_execute --json
|
|
237
|
+
qveris ledger --mode export-file --start-date 2026-05-01 --end-date 2026-05-04
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`summary` and `search` never print full history. Large record sets are written to `.qveris/exports/*.jsonl` with `--mode export-file`.
|
|
241
|
+
|
|
202
242
|
### Interactive Mode
|
|
203
243
|
|
|
204
244
|
```bash
|
package/package.json
CHANGED
package/src/client/api.mjs
CHANGED
|
@@ -36,7 +36,12 @@ async function requestJson(path, { method = "POST", query = {}, body, timeoutMs
|
|
|
36
36
|
errorDetail = jsonBody.error_message || jsonBody.message || null;
|
|
37
37
|
} catch { /* not JSON */ }
|
|
38
38
|
if (status === 401 || status === 403) throw new CliError("AUTH_INVALID_KEY", errorDetail);
|
|
39
|
-
if (status === 402)
|
|
39
|
+
if (status === 402) {
|
|
40
|
+
const pricingHost = baseUrl.includes("qveris.cn") ? "https://qveris.cn" : "https://qveris.ai";
|
|
41
|
+
const err = new CliError("CREDITS_INSUFFICIENT", errorDetail);
|
|
42
|
+
err.hint = `Purchase credits at ${pricingHost}/pricing`;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
40
45
|
if (status === 429) throw new CliError("RATE_LIMITED", errorDetail);
|
|
41
46
|
const err = new CliError("API_ERROR", `HTTP ${status}: ${errorDetail || rawText}`);
|
|
42
47
|
if (jsonBody) err.responseData = jsonBody;
|
|
@@ -57,6 +62,21 @@ async function requestJson(path, { method = "POST", query = {}, body, timeoutMs
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
export function unwrapApiResponse(response) {
|
|
66
|
+
if (
|
|
67
|
+
response &&
|
|
68
|
+
typeof response === "object" &&
|
|
69
|
+
Object.prototype.hasOwnProperty.call(response, "status") &&
|
|
70
|
+
Object.prototype.hasOwnProperty.call(response, "data")
|
|
71
|
+
) {
|
|
72
|
+
if (response.status === "failure") {
|
|
73
|
+
throw new CliError("API_ERROR", response.message || "API request failed");
|
|
74
|
+
}
|
|
75
|
+
return response.data;
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
export async function discoverTools({ apiKey, baseUrl: baseUrlFlag, query, limit = 5, timeoutMs = 30000 }) {
|
|
61
81
|
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
62
82
|
return requestJson("/search", { apiKey, baseUrl, body: { query, limit }, timeoutMs });
|
|
@@ -91,3 +111,45 @@ export async function callTool({
|
|
|
91
111
|
timeoutMs,
|
|
92
112
|
});
|
|
93
113
|
}
|
|
114
|
+
|
|
115
|
+
export async function getCredits({ apiKey, baseUrl: baseUrlFlag, timeoutMs = 30000 }) {
|
|
116
|
+
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
117
|
+
return requestJson("/auth/credits", {
|
|
118
|
+
method: "GET",
|
|
119
|
+
apiKey,
|
|
120
|
+
baseUrl,
|
|
121
|
+
timeoutMs,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function getUsageHistory({
|
|
126
|
+
apiKey,
|
|
127
|
+
baseUrl: baseUrlFlag,
|
|
128
|
+
query = {},
|
|
129
|
+
timeoutMs = 30000,
|
|
130
|
+
}) {
|
|
131
|
+
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
132
|
+
return requestJson("/auth/usage/history/v2", {
|
|
133
|
+
method: "GET",
|
|
134
|
+
apiKey,
|
|
135
|
+
baseUrl,
|
|
136
|
+
query,
|
|
137
|
+
timeoutMs,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function getCreditsLedger({
|
|
142
|
+
apiKey,
|
|
143
|
+
baseUrl: baseUrlFlag,
|
|
144
|
+
query = {},
|
|
145
|
+
timeoutMs = 30000,
|
|
146
|
+
}) {
|
|
147
|
+
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
148
|
+
return requestJson("/auth/credits/ledger", {
|
|
149
|
+
method: "GET",
|
|
150
|
+
apiKey,
|
|
151
|
+
baseUrl,
|
|
152
|
+
query,
|
|
153
|
+
timeoutMs,
|
|
154
|
+
});
|
|
155
|
+
}
|
package/src/commands/credits.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveApiKey } from "../client/auth.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { getCredits, unwrapApiResponse } from "../client/api.mjs";
|
|
3
3
|
import { resolveBaseUrl, getSiteUrl } from "../config/region.mjs";
|
|
4
4
|
import { bold, dim, cyan, yellow, green } from "../output/colors.mjs";
|
|
5
5
|
import { outputJson } from "../output/json.mjs";
|
|
@@ -13,16 +13,19 @@ export async function runCredits(flags) {
|
|
|
13
13
|
const spinner = flags.json ? { stop() {} } : createSpinner("Checking credits...");
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
|
-
|
|
17
|
-
const result = await discoverTools({ apiKey, baseUrl: flags.baseUrl, query: "credit balance", limit: 1, timeoutMs: 10000 });
|
|
16
|
+
const result = unwrapApiResponse(await getCredits({ apiKey, baseUrl, timeoutMs: 10000 }));
|
|
18
17
|
spinner.stop();
|
|
19
18
|
|
|
20
19
|
const credits = result.remaining_credits;
|
|
21
20
|
|
|
22
21
|
if (flags.json) {
|
|
23
|
-
outputJson(
|
|
22
|
+
outputJson(result);
|
|
24
23
|
} else if (credits !== undefined && credits !== null) {
|
|
25
24
|
console.log(`\n ${green("\u2713")} Credits remaining: ${bold(yellow(String(credits)))}`);
|
|
25
|
+
printBucket("Daily free", result.daily_free, "remaining", "total");
|
|
26
|
+
printBucket("Invite reward", result.invite_reward, "remaining", "total");
|
|
27
|
+
printBucket("Welcome bonus", result.welcome_bonus, "remaining", "initial");
|
|
28
|
+
printBucket("Purchased", result.purchased, "remaining", "total");
|
|
26
29
|
console.log(` ${dim("Manage at:")} ${cyan(accountUrl)}\n`);
|
|
27
30
|
} else {
|
|
28
31
|
console.log(`\n ${dim("Credit balance not available in API response.")}`);
|
|
@@ -33,3 +36,14 @@ export async function runCredits(flags) {
|
|
|
33
36
|
throw err;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
39
|
+
|
|
40
|
+
function printBucket(label, bucket, remainingKey, totalKey) {
|
|
41
|
+
if (!bucket || typeof bucket !== "object") return;
|
|
42
|
+
const remaining = bucket[remainingKey];
|
|
43
|
+
const total = bucket[totalKey];
|
|
44
|
+
if (remaining === undefined && total === undefined) return;
|
|
45
|
+
const parts = [];
|
|
46
|
+
if (remaining !== undefined) parts.push(`remaining ${yellow(String(remaining))}`);
|
|
47
|
+
if (total !== undefined) parts.push(`${dim("of")} ${String(total)}`);
|
|
48
|
+
console.log(` ${dim(label + ":")} ${parts.join(" ")}`);
|
|
49
|
+
}
|
|
@@ -8,6 +8,7 @@ import { createSpinner } from "../output/spinner.mjs";
|
|
|
8
8
|
|
|
9
9
|
export async function runDiscover(query, flags) {
|
|
10
10
|
const apiKey = resolveApiKey(flags.apiKey);
|
|
11
|
+
const { region, baseUrl: resolvedBaseUrl } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
11
12
|
const limit = parseInt(flags.limit, 10) || 5;
|
|
12
13
|
const timeoutMs = (parseInt(flags.timeout, 10) || 30) * 1000;
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@ export async function runDiscover(query, flags) {
|
|
|
16
17
|
try {
|
|
17
18
|
const result = await discoverTools({
|
|
18
19
|
apiKey,
|
|
19
|
-
baseUrl:
|
|
20
|
+
baseUrl: resolvedBaseUrl,
|
|
20
21
|
query,
|
|
21
22
|
limit,
|
|
22
23
|
timeoutMs,
|
|
@@ -26,7 +27,6 @@ export async function runDiscover(query, flags) {
|
|
|
26
27
|
|
|
27
28
|
// Store richer session data for index resolution
|
|
28
29
|
const tools = result.results ?? [];
|
|
29
|
-
const { region, baseUrl: resolvedBaseUrl } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
30
30
|
writeSession({
|
|
31
31
|
discoveryId: result.search_id,
|
|
32
32
|
query,
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export async function runDoctor(flags) {
|
|
|
30
30
|
// Test connectivity
|
|
31
31
|
process.stderr.write(` \u2026 Testing API connectivity...\r`);
|
|
32
32
|
try {
|
|
33
|
-
await discoverTools({ apiKey, baseUrl
|
|
33
|
+
await discoverTools({ apiKey, baseUrl, query: "test", limit: 1, timeoutMs: 10000 });
|
|
34
34
|
console.log(` ${green("\u2713")} API connectivity OK `);
|
|
35
35
|
} catch (err) {
|
|
36
36
|
console.log(` ${red("\u2718")} API connectivity failed: ${err.message} `);
|
|
@@ -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
|
+
}
|