@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.
@@ -5,11 +5,10 @@ import { resolve } from "../config/resolve.mjs";
5
5
  import { setConfigValue, deleteConfigValue } from "../config/store.mjs";
6
6
  import { discoverTools } from "../client/api.mjs";
7
7
  import { VERSION } from "../config/defaults.mjs";
8
+ import { resolveBaseUrl, getSiteUrl } from "../config/region.mjs";
8
9
  import { bold, green, red, dim, cyan } from "../output/colors.mjs";
9
10
  import { printLoginBanner } from "../output/banner.mjs";
10
11
 
11
- const ACCOUNT_URL = "https://qveris.ai/account?page=api-keys";
12
-
13
12
  function openBrowser(url) {
14
13
  const cmds = { darwin: "open", win32: "cmd", linux: "xdg-open" };
15
14
  const cmd = cmds[platform()] || "xdg-open";
@@ -87,9 +86,54 @@ function prompt(question) {
87
86
  });
88
87
  }
89
88
 
89
+ /**
90
+ * Prompt user to select region interactively.
91
+ * Skipped if region is already determined via --base-url, QVERIS_REGION, or QVERIS_BASE_URL.
92
+ */
93
+ function promptRegion() {
94
+ return new Promise((pResolve, pReject) => {
95
+ console.log(` ${bold("Select your region / 选择站点区域:")}\n`);
96
+ console.log(` ${cyan("1)")} Global — qveris.ai (International users)`);
97
+ console.log(` ${cyan("2)")} China — qveris.cn (中国大陆用户)\n`);
98
+
99
+ if (!process.stdin.isTTY) {
100
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
101
+ rl.question(" Enter 1 or 2: ", (answer) => { rl.close(); pResolve(answer.trim()); });
102
+ return;
103
+ }
104
+
105
+ process.stderr.write(" Enter 1 or 2: ");
106
+
107
+ const cleanup = () => {
108
+ process.stdin.removeListener("data", onData);
109
+ try { process.stdin.setRawMode(false); } catch {}
110
+ process.stdin.pause();
111
+ };
112
+ const onExit = () => { try { process.stdin.setRawMode(false); } catch {} };
113
+ process.once("exit", onExit);
114
+
115
+ process.stdin.setRawMode(true);
116
+ process.stdin.resume();
117
+ process.stdin.setEncoding("utf-8");
118
+
119
+ const onData = (chunk) => {
120
+ const ch = chunk[0];
121
+ if (ch === "\x03") { cleanup(); process.removeListener("exit", onExit); process.stderr.write("\n"); pReject(new Error("Aborted")); return; }
122
+ if (ch === "1" || ch === "2") {
123
+ cleanup();
124
+ process.removeListener("exit", onExit);
125
+ process.stderr.write(`${ch}\n`);
126
+ pResolve(ch);
127
+ }
128
+ // Ignore other keys
129
+ };
130
+ process.stdin.on("data", onData);
131
+ });
132
+ }
133
+
90
134
  export async function runLogin(flags) {
91
135
  if (flags.token) {
92
- await validateAndSave(flags.token);
136
+ await validateAndSave(flags.token, flags.baseUrl);
93
137
  return;
94
138
  }
95
139
 
@@ -97,10 +141,29 @@ export async function runLogin(flags) {
97
141
  printLoginBanner({ version: VERSION, noColor: flags.noColor });
98
142
  }
99
143
 
100
- console.log(` Get your API key at: ${cyan(ACCOUNT_URL)}\n`);
144
+ // Determine region: if already set via flag/env, use that; otherwise ask the user.
145
+ const { region: presetRegion, baseUrl: presetBaseUrl, source: regionSource } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl });
146
+ let region;
147
+ let baseUrl = presetBaseUrl;
148
+ if (regionSource !== "default") {
149
+ // Region explicitly configured — use it directly
150
+ region = presetRegion;
151
+ } else {
152
+ // No explicit region — let user choose interactively
153
+ let choice;
154
+ try {
155
+ choice = await promptRegion();
156
+ } catch {
157
+ return; // Ctrl+C
158
+ }
159
+ region = choice === "2" ? "cn" : "global";
160
+ }
161
+
162
+ const accountUrl = `${getSiteUrl(region, baseUrl)}/account?page=api-keys`;
163
+ console.log(`\n Get your API key at: ${cyan(accountUrl)}\n`);
101
164
 
102
165
  if (!flags.noBrowser) {
103
- openBrowser(ACCOUNT_URL);
166
+ openBrowser(accountUrl);
104
167
  }
105
168
 
106
169
  let key;
@@ -116,18 +179,21 @@ export async function runLogin(flags) {
116
179
  return;
117
180
  }
118
181
 
119
- await validateAndSave(key);
182
+ await validateAndSave(key, flags.baseUrl);
120
183
  }
121
184
 
122
- async function validateAndSave(key) {
185
+ async function validateAndSave(key, baseUrlFlag) {
123
186
  process.stderr.write(` Validating key...`);
124
187
 
188
+ const { baseUrl, region, source } = resolveBaseUrl({ baseUrlFlag, apiKey: key });
189
+
125
190
  try {
126
- await discoverTools({ apiKey: key, query: "test", limit: 1, timeoutMs: 10000 });
191
+ await discoverTools({ apiKey: key, baseUrl, query: "test", limit: 1, timeoutMs: 10000 });
127
192
  setConfigValue("api_key", key);
128
193
  const masked = key.slice(0, 6) + "..." + key.slice(-4);
129
194
  console.error(`\r\x1b[K`);
130
195
  console.log(` ${green("\u2713")} Authenticated as ${bold(masked)}`);
196
+ console.log(` ${dim("Region:")} ${region} ${dim(`(${source})`)}`);
131
197
  console.log(` ${dim("Key saved to config.")}`);
132
198
  } catch {
133
199
  console.error(`\r\x1b[K`);
@@ -151,15 +217,17 @@ export async function runWhoami(flags) {
151
217
  }
152
218
 
153
219
  const masked = key.slice(0, 6) + "..." + key.slice(-4);
220
+ const { baseUrl, region, source: regionSource } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey: key });
154
221
 
155
222
  process.stderr.write(` Validating...`);
156
223
 
157
224
  try {
158
- await discoverTools({ apiKey: key, query: "test", limit: 1, timeoutMs: 10000 });
225
+ await discoverTools({ apiKey: key, baseUrl, query: "test", limit: 1, timeoutMs: 10000 });
159
226
  process.stderr.write("\r\x1b[K");
160
227
  console.log(`\n ${green("\u2713")} Authenticated`);
161
228
  console.log(` Key: ${bold(masked)}`);
162
229
  console.log(` Source: ${dim(source)}`);
230
+ console.log(` Region: ${region} ${dim(`(${regionSource})`)}`);
163
231
  } catch {
164
232
  console.error(`\r\x1b[K`);
165
233
  console.log(`\n ${red("\u2718")} Key ${masked} is ${red("invalid")}`);
@@ -0,0 +1,190 @@
1
+ import { resolveApiKey } from "../client/auth.mjs";
2
+ import { getUsageHistory, 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
+ buildUsageQuery,
11
+ buildUsageSummary,
12
+ buildUsageSummaryFromServer,
13
+ chooseBucket,
14
+ clampLimit,
15
+ extractItems,
16
+ extractTotal,
17
+ formatExportMetadata,
18
+ formatUsageRows,
19
+ formatUsageSummary,
20
+ matchesUsageFilters,
21
+ pickUsageRow,
22
+ resolveDateRange,
23
+ resolveMode,
24
+ writeJsonlExport,
25
+ } from "../output/audit.mjs";
26
+
27
+ export async function runUsage(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 usage history...");
36
+
37
+ try {
38
+ if (mode === "search") {
39
+ const { rows, total, partial } = await collectUsageRows({
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(pickUsageRow),
56
+ };
57
+ if (flags.json) outputJson(payload);
58
+ else console.log(formatUsageRows(rows, { total, partial }));
59
+ return;
60
+ }
61
+
62
+ if (mode === "export_file") {
63
+ const { rows, total, partial } = await collectUsageRows({
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("usage_history", rows, {
73
+ start_date: startDate,
74
+ end_date: endDate,
75
+ matched_records: total,
76
+ truncated: partial,
77
+ filters: usageFilters(flags),
78
+ });
79
+ if (flags.json) outputJson(metadata);
80
+ else console.log(formatExportMetadata(metadata));
81
+ return;
82
+ }
83
+
84
+ const summaryResponse = unwrapApiResponse(await getUsageHistory({
85
+ apiKey,
86
+ baseUrl: flags.baseUrl,
87
+ query: buildUsageQuery(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 = buildUsageSummaryFromServer(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(formatUsageSummary(summary, { scannedRows: rows.length, total, partial: false }));
109
+ }
110
+ return;
111
+ }
112
+
113
+ const { rows, total, partial, scannedRows } = await collectUsageRows({
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 = buildUsageSummary(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(formatUsageSummary(summary, { scannedRows, total, partial }));
133
+ }
134
+ } catch (err) {
135
+ spinner.stop();
136
+ throw err;
137
+ }
138
+ }
139
+
140
+ async function collectUsageRows({ 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 = buildUsageQuery(flags, { page, pageSize: Math.min(DEFAULT_PAGE_SIZE, maxRows - scannedRows) });
149
+ const response = unwrapApiResponse(await getUsageHistory({
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 (!matchesUsageFilters(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 usageFilters(flags) {
180
+ return {
181
+ execution_id: flags.executionId,
182
+ search_id: flags.searchId,
183
+ event_type: flags.eventType,
184
+ kind: flags.kind,
185
+ success: flags.success,
186
+ charge_outcome: flags.chargeOutcome,
187
+ min_credits: flags.minCredits,
188
+ max_credits: flags.maxCredits,
189
+ };
190
+ }
@@ -13,6 +13,7 @@ const FLAG_ALIASES = {
13
13
  export function normalizeLegacyArgs(args) {
14
14
  const result = [...args];
15
15
  const warnings = [];
16
+ const originalCommand = result[0];
16
17
 
17
18
  if (result.length > 0 && COMMAND_ALIASES[result[0]]) {
18
19
  warnings.push(`'${result[0]}' is deprecated; use '${COMMAND_ALIASES[result[0]]}' instead.`);
@@ -20,6 +21,9 @@ export function normalizeLegacyArgs(args) {
20
21
  }
21
22
 
22
23
  for (let i = 0; i < result.length; i++) {
24
+ if (originalCommand === "usage" && result[i] === "--search-id") {
25
+ continue;
26
+ }
23
27
  if (FLAG_ALIASES[result[i]]) {
24
28
  warnings.push(`'${result[i]}' is deprecated; use '${FLAG_ALIASES[result[i]]}' instead.`);
25
29
  result[i] = FLAG_ALIASES[result[i]];
@@ -13,6 +13,26 @@ const REGION_URLS = {
13
13
  cn: "https://qveris.cn/api/v1",
14
14
  };
15
15
 
16
+ const SITE_URLS = {
17
+ global: "https://qveris.ai",
18
+ cn: "https://qveris.cn",
19
+ };
20
+
21
+ /**
22
+ * Get the site URL (not API URL) for a given region.
23
+ * For custom regions, attempts to infer from the base URL domain.
24
+ * @param {string} region
25
+ * @param {string} [baseUrl] - The resolved API base URL (used to infer site for custom regions)
26
+ * @returns {string}
27
+ */
28
+ export function getSiteUrl(region, baseUrl) {
29
+ if (region === "custom" && baseUrl) {
30
+ if (baseUrl.includes("qveris.cn")) return SITE_URLS.cn;
31
+ if (baseUrl.includes("qveris.ai")) return SITE_URLS.global;
32
+ }
33
+ return SITE_URLS[region] || SITE_URLS.global;
34
+ }
35
+
16
36
  /**
17
37
  * Detect region from API key prefix.
18
38
  * @param {string} apiKey
package/src/main.mjs CHANGED
@@ -72,6 +72,16 @@ export async function main(argv) {
72
72
  await runCredits(flags);
73
73
  break;
74
74
  }
75
+ case "usage": {
76
+ const { runUsage } = await import("./commands/usage.mjs");
77
+ await runUsage(flags);
78
+ break;
79
+ }
80
+ case "ledger": {
81
+ const { runLedger } = await import("./commands/ledger.mjs");
82
+ await runLedger(flags);
83
+ break;
84
+ }
75
85
  case "config": {
76
86
  const subcommand = rest[0];
77
87
  const subArgs = rest.slice(1);
@@ -119,6 +129,11 @@ const VALUE_FLAGS = {
119
129
  "api-key": "apiKey", "base-url": "baseUrl", timeout: "timeout",
120
130
  limit: "limit", "discovery-id": "discoveryId", params: "params",
121
131
  "max-size": "maxSize", codegen: "codegen", token: "token",
132
+ mode: "mode", "start-date": "startDate", "end-date": "endDate",
133
+ bucket: "bucket", "execution-id": "executionId", "search-id": "searchId",
134
+ "event-type": "eventType", kind: "kind", success: "success",
135
+ "charge-outcome": "chargeOutcome", "entry-type": "entryType",
136
+ direction: "direction", "min-credits": "minCredits", "max-credits": "maxCredits",
122
137
  };
123
138
 
124
139
  function takeNext(args, i, flag) {
@@ -199,6 +214,34 @@ function extractGlobalFlags(args) {
199
214
  flags.codegen = takeNext(args, i++, arg); break;
200
215
  case "--token":
201
216
  flags.token = takeNext(args, i++, arg); break;
217
+ case "--mode":
218
+ flags.mode = takeNext(args, i++, arg); break;
219
+ case "--start-date":
220
+ flags.startDate = takeNext(args, i++, arg); break;
221
+ case "--end-date":
222
+ flags.endDate = takeNext(args, i++, arg); break;
223
+ case "--bucket":
224
+ flags.bucket = takeNext(args, i++, arg); break;
225
+ case "--execution-id":
226
+ flags.executionId = takeNext(args, i++, arg); break;
227
+ case "--search-id":
228
+ flags.searchId = takeNext(args, i++, arg); break;
229
+ case "--event-type":
230
+ flags.eventType = takeNext(args, i++, arg); break;
231
+ case "--kind":
232
+ flags.kind = takeNext(args, i++, arg); break;
233
+ case "--success":
234
+ flags.success = takeNext(args, i++, arg); break;
235
+ case "--charge-outcome":
236
+ flags.chargeOutcome = takeNext(args, i++, arg); break;
237
+ case "--entry-type":
238
+ flags.entryType = takeNext(args, i++, arg); break;
239
+ case "--direction":
240
+ flags.direction = takeNext(args, i++, arg); break;
241
+ case "--min-credits":
242
+ flags.minCredits = takeNext(args, i++, arg); break;
243
+ case "--max-credits":
244
+ flags.maxCredits = takeNext(args, i++, arg); break;
202
245
  default:
203
246
  flags._positional.push(arg);
204
247
  }
@@ -228,6 +271,8 @@ function printUsage(flags = {}) {
228
271
  ${cyan("logout")} Remove stored API key
229
272
  ${cyan("whoami")} Show current auth status
230
273
  ${cyan("credits")} Show credit balance
274
+ ${cyan("usage")} Summarize or search usage audit history
275
+ ${cyan("ledger")} Summarize or search credit ledger entries
231
276
 
232
277
  ${bold("Configuration:")}
233
278
  ${cyan("config")} set|get|list|reset Manage CLI settings
@@ -241,19 +286,32 @@ function printUsage(flags = {}) {
241
286
  ${bold("Global Flags:")}
242
287
  --json, -j Output raw JSON
243
288
  --api-key <key> Override API key
289
+ --base-url <url> Override API base URL
244
290
  --timeout <seconds> Request timeout
291
+ --mode <mode> summary | search | export-file for usage/ledger
292
+ --start-date <date> Usage/ledger range start (YYYY-MM-DD)
293
+ --end-date <date> Usage/ledger range end (YYYY-MM-DD)
294
+ --min-credits <n> Usage/ledger amount lower bound
295
+ --max-credits <n> Usage/ledger amount upper bound
245
296
  --no-color Disable colors
246
297
  --verbose, -v Show request details
247
298
  --version, -V Print version
248
299
  --help, -h Show help
249
300
 
301
+ ${bold("Environment Variables:")}
302
+ QVERIS_API_KEY API key
303
+ QVERIS_REGION Region override (global | cn)
304
+ QVERIS_BASE_URL Custom API base URL
305
+
250
306
  ${bold("Examples:")}
251
307
  qveris discover "weather forecast API"
252
308
  qveris inspect 1
253
309
  qveris call 1 --params '{"city": "London"}'
254
310
  qveris call 1 --params @params.json --codegen curl
311
+ qveris usage --mode search --execution-id <id>
312
+ qveris ledger --mode search --min-credits 50 --direction consume
255
313
  qveris interactive
256
314
 
257
- ${dim("https://qveris.ai")}
315
+ ${dim("https://qveris.ai (global) / https://qveris.cn (China)")}
258
316
  `);
259
317
  }