@qverisai/cli 0.2.0 → 0.4.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 +12 -7
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/src/commands/config.mjs +9 -1
- package/src/commands/credits.mjs +5 -2
- package/src/commands/discover.mjs +4 -0
- package/src/commands/doctor.mjs +5 -0
- package/src/commands/interactive.mjs +10 -2
- package/src/commands/login.mjs +83 -9
- package/src/config/region.mjs +20 -0
- package/src/main.mjs +19 -4
- package/src/output/banner.mjs +241 -0
- package/src/output/colors.mjs +9 -7
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ tool: weather_gov... · id: 7ebcaf9d-...
|
|
|
57
57
|
**One-liner (recommended):**
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
curl -fsSL https://qveris.ai/install | bash
|
|
60
|
+
curl -fsSL https://qveris.ai/cli/install | bash
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
**Or via npm:**
|
|
@@ -104,9 +104,9 @@ qveris call 1 --params '{"wfo": "LWX", "x": 90, "y": 90}'
|
|
|
104
104
|
|
|
105
105
|
| Command | Description |
|
|
106
106
|
|---------|-------------|
|
|
107
|
-
| `qveris login` | Authenticate with API key (opens browser or `--token` for direct input) |
|
|
107
|
+
| `qveris login` | Authenticate with API key (interactive region selection, opens browser, or `--token` for direct input) |
|
|
108
108
|
| `qveris logout` | Remove stored key |
|
|
109
|
-
| `qveris whoami` | Show current auth status
|
|
109
|
+
| `qveris whoami` | Show current auth status, key source, and region |
|
|
110
110
|
| `qveris credits` | Check credit balance |
|
|
111
111
|
|
|
112
112
|
### Utilities
|
|
@@ -114,7 +114,7 @@ qveris call 1 --params '{"wfo": "LWX", "x": 90, "y": 90}'
|
|
|
114
114
|
| Command | Description |
|
|
115
115
|
|---------|-------------|
|
|
116
116
|
| `qveris interactive` | Launch REPL mode (discover/inspect/call/codegen in one session) |
|
|
117
|
-
| `qveris doctor` | Self-check: Node.js version, API key, connectivity |
|
|
117
|
+
| `qveris doctor` | Self-check: Node.js version, API key, region, connectivity |
|
|
118
118
|
| `qveris config <subcommand>` | Manage CLI settings (set, get, list, reset, path) |
|
|
119
119
|
| `qveris completions <shell>` | Generate shell completions (bash/zsh/fish) |
|
|
120
120
|
|
|
@@ -251,13 +251,18 @@ The API region is auto-detected from your key prefix:
|
|
|
251
251
|
| `sk-xxx` | Global | `https://qveris.ai/api/v1` |
|
|
252
252
|
| `sk-cn-xxx` | China | `https://qveris.cn/api/v1` |
|
|
253
253
|
|
|
254
|
-
No extra configuration needed.
|
|
254
|
+
No extra configuration needed. `qveris login` prompts for region selection interactively on first use.
|
|
255
|
+
|
|
256
|
+
**Agent / script usage:** Use `--token` with a region-prefixed key, or set environment variables:
|
|
255
257
|
|
|
256
258
|
```bash
|
|
257
|
-
#
|
|
259
|
+
# Key prefix auto-detection (recommended)
|
|
260
|
+
qveris login --token "sk-cn-xxx"
|
|
261
|
+
|
|
262
|
+
# Or environment variable
|
|
258
263
|
export QVERIS_REGION=cn
|
|
259
264
|
|
|
260
|
-
# Or
|
|
265
|
+
# Or custom base URL
|
|
261
266
|
export QVERIS_BASE_URL=https://custom.endpoint/api/v1
|
|
262
267
|
|
|
263
268
|
# Or per-command
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
package/src/commands/config.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getConfigPath, getConfigValue, setConfigValue, writeConfig } from "../config/store.mjs";
|
|
2
|
-
import { resolveAll } from "../config/resolve.mjs";
|
|
2
|
+
import { resolve, resolveAll } from "../config/resolve.mjs";
|
|
3
|
+
import { resolveBaseUrl } from "../config/region.mjs";
|
|
3
4
|
import { bold, dim, cyan } from "../output/colors.mjs";
|
|
4
5
|
import { outputJson } from "../output/json.mjs";
|
|
5
6
|
|
|
@@ -64,11 +65,17 @@ function configGet(key, flags) {
|
|
|
64
65
|
|
|
65
66
|
function configList(flags) {
|
|
66
67
|
const all = resolveAll();
|
|
68
|
+
|
|
69
|
+
// Resolve effective region
|
|
70
|
+
const { value: apiKey } = resolve("api_key", flags.apiKey);
|
|
71
|
+
const { region, source: regionSource, baseUrl } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
72
|
+
|
|
67
73
|
if (flags.json) {
|
|
68
74
|
const obj = {};
|
|
69
75
|
for (const [k, v] of Object.entries(all)) {
|
|
70
76
|
obj[k] = { value: k === "api_key" && v.value ? mask(v.value) : v.value, source: v.source };
|
|
71
77
|
}
|
|
78
|
+
obj._region = { region, source: regionSource, baseUrl };
|
|
72
79
|
outputJson(obj);
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
@@ -78,6 +85,7 @@ function configList(flags) {
|
|
|
78
85
|
const display = key === "api_key" && value ? mask(value) : String(value ?? dim("(not set)"));
|
|
79
86
|
console.log(` ${cyan(key.padEnd(20))}${display.padEnd(25)}${dim(source)}`);
|
|
80
87
|
}
|
|
88
|
+
console.log(`\n ${bold("Effective region:")} ${region} ${dim(`(${regionSource})`)} → ${dim(baseUrl)}`);
|
|
81
89
|
console.log();
|
|
82
90
|
}
|
|
83
91
|
|
package/src/commands/credits.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { resolveApiKey } from "../client/auth.mjs";
|
|
2
2
|
import { discoverTools } from "../client/api.mjs";
|
|
3
|
+
import { resolveBaseUrl, getSiteUrl } from "../config/region.mjs";
|
|
3
4
|
import { bold, dim, cyan, yellow, green } from "../output/colors.mjs";
|
|
4
5
|
import { outputJson } from "../output/json.mjs";
|
|
5
6
|
import { createSpinner } from "../output/spinner.mjs";
|
|
6
7
|
|
|
7
8
|
export async function runCredits(flags) {
|
|
8
9
|
const apiKey = resolveApiKey(flags.apiKey);
|
|
10
|
+
const { region, baseUrl } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
11
|
+
const accountUrl = `${getSiteUrl(region, baseUrl)}/account`;
|
|
9
12
|
|
|
10
13
|
const spinner = flags.json ? { stop() {} } : createSpinner("Checking credits...");
|
|
11
14
|
|
|
@@ -20,10 +23,10 @@ export async function runCredits(flags) {
|
|
|
20
23
|
outputJson({ remaining_credits: credits ?? null });
|
|
21
24
|
} else if (credits !== undefined && credits !== null) {
|
|
22
25
|
console.log(`\n ${green("\u2713")} Credits remaining: ${bold(yellow(String(credits)))}`);
|
|
23
|
-
console.log(` ${dim("Manage at:")} ${cyan(
|
|
26
|
+
console.log(` ${dim("Manage at:")} ${cyan(accountUrl)}\n`);
|
|
24
27
|
} else {
|
|
25
28
|
console.log(`\n ${dim("Credit balance not available in API response.")}`);
|
|
26
|
-
console.log(` Check at: ${cyan(
|
|
29
|
+
console.log(` Check at: ${cyan(accountUrl)}\n`);
|
|
27
30
|
}
|
|
28
31
|
} catch (err) {
|
|
29
32
|
spinner.stop();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveApiKey } from "../client/auth.mjs";
|
|
2
2
|
import { discoverTools } from "../client/api.mjs";
|
|
3
|
+
import { resolveBaseUrl } from "../config/region.mjs";
|
|
3
4
|
import { writeSession } from "../session/session.mjs";
|
|
4
5
|
import { formatDiscoverResult } from "../output/formatter.mjs";
|
|
5
6
|
import { outputJson } from "../output/json.mjs";
|
|
@@ -25,9 +26,12 @@ export async function runDiscover(query, flags) {
|
|
|
25
26
|
|
|
26
27
|
// Store richer session data for index resolution
|
|
27
28
|
const tools = result.results ?? [];
|
|
29
|
+
const { region, baseUrl: resolvedBaseUrl } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
28
30
|
writeSession({
|
|
29
31
|
discoveryId: result.search_id,
|
|
30
32
|
query,
|
|
33
|
+
region,
|
|
34
|
+
baseUrl: resolvedBaseUrl,
|
|
31
35
|
results: tools.map((t, i) => ({
|
|
32
36
|
index: i + 1,
|
|
33
37
|
tool_id: t.tool_id,
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "../config/resolve.mjs";
|
|
2
2
|
import { discoverTools } from "../client/api.mjs";
|
|
3
|
+
import { resolveBaseUrl } from "../config/region.mjs";
|
|
3
4
|
import { bold, green, red, dim, cyan } from "../output/colors.mjs";
|
|
4
5
|
|
|
5
6
|
export async function runDoctor(flags) {
|
|
@@ -22,6 +23,10 @@ export async function runDoctor(flags) {
|
|
|
22
23
|
const masked = apiKey.slice(0, 6) + "..." + apiKey.slice(-4);
|
|
23
24
|
console.log(` ${green("\u2713")} API key configured (${masked} via ${source})`);
|
|
24
25
|
|
|
26
|
+
// Show resolved region
|
|
27
|
+
const { baseUrl, region, source: regionSource } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
28
|
+
console.log(` ${green("\u2713")} Region: ${region} ${dim(`(${regionSource})`)} → ${dim(baseUrl)}`);
|
|
29
|
+
|
|
25
30
|
// Test connectivity
|
|
26
31
|
process.stderr.write(` \u2026 Testing API connectivity...\r`);
|
|
27
32
|
try {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { resolveApiKey } from "../client/auth.mjs";
|
|
3
3
|
import { discoverTools, inspectToolsByIds, callTool } from "../client/api.mjs";
|
|
4
|
+
import { resolveBaseUrl } from "../config/region.mjs";
|
|
4
5
|
import { resolveParams } from "../utils/params.mjs";
|
|
5
6
|
import { formatDiscoverResult, formatInspectResult, formatCallResult } from "../output/formatter.mjs";
|
|
6
7
|
import { generateSnippet } from "../output/codegen.mjs";
|
|
7
8
|
import { writeSession } from "../session/session.mjs";
|
|
9
|
+
import { VERSION } from "../config/defaults.mjs";
|
|
8
10
|
import { bold, dim, cyan } from "../output/colors.mjs";
|
|
11
|
+
import { printWelcomeBanner } from "../output/banner.mjs";
|
|
9
12
|
import { handleError } from "../errors/handler.mjs";
|
|
10
13
|
import { createSpinner } from "../output/spinner.mjs";
|
|
11
14
|
|
|
12
15
|
export async function runInteractive(flags) {
|
|
13
16
|
const apiKey = resolveApiKey(flags.apiKey);
|
|
14
17
|
const baseUrl = flags.baseUrl;
|
|
18
|
+
const { region, baseUrl: resolvedBaseUrl } = resolveBaseUrl({ baseUrlFlag: baseUrl, apiKey });
|
|
15
19
|
const limit = parseInt(flags.limit, 10) || 5;
|
|
16
20
|
const discoverTimeout = (parseInt(flags.timeout, 10) || 30) * 1000;
|
|
17
21
|
const callTimeout = (parseInt(flags.timeout, 10) || 60) * 1000;
|
|
@@ -22,7 +26,11 @@ export async function runInteractive(flags) {
|
|
|
22
26
|
lastCallContext: null,
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
if (!flags.json) {
|
|
30
|
+
printWelcomeBanner({ version: VERSION, noColor: flags.noColor, compact: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(` ${bold("QVeris Interactive Mode")}`);
|
|
26
34
|
console.log(` ${dim("Type 'help' for commands, 'exit' to quit.")}\n`);
|
|
27
35
|
|
|
28
36
|
const rl = createInterface({
|
|
@@ -57,7 +65,7 @@ export async function runInteractive(flags) {
|
|
|
57
65
|
index: i + 1, tool_id: t.tool_id, name: t.name, provider_name: t.provider_name,
|
|
58
66
|
}));
|
|
59
67
|
// Persist session so index shortcuts work in subsequent non-interactive calls
|
|
60
|
-
writeSession({ discoveryId: result.search_id, query, results: state.results });
|
|
68
|
+
writeSession({ discoveryId: result.search_id, query, region, baseUrl: resolvedBaseUrl, results: state.results });
|
|
61
69
|
console.log(formatDiscoverResult(result));
|
|
62
70
|
break;
|
|
63
71
|
}
|
package/src/commands/login.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import { platform } from "node:os";
|
|
|
4
4
|
import { resolve } from "../config/resolve.mjs";
|
|
5
5
|
import { setConfigValue, deleteConfigValue } from "../config/store.mjs";
|
|
6
6
|
import { discoverTools } from "../client/api.mjs";
|
|
7
|
+
import { VERSION } from "../config/defaults.mjs";
|
|
8
|
+
import { resolveBaseUrl, getSiteUrl } from "../config/region.mjs";
|
|
7
9
|
import { bold, green, red, dim, cyan } from "../output/colors.mjs";
|
|
8
|
-
|
|
9
|
-
const ACCOUNT_URL = "https://qveris.ai/account?page=api-keys";
|
|
10
|
+
import { printLoginBanner } from "../output/banner.mjs";
|
|
10
11
|
|
|
11
12
|
function openBrowser(url) {
|
|
12
13
|
const cmds = { darwin: "open", win32: "cmd", linux: "xdg-open" };
|
|
@@ -85,16 +86,84 @@ function prompt(question) {
|
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
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
|
+
|
|
88
134
|
export async function runLogin(flags) {
|
|
89
135
|
if (flags.token) {
|
|
90
|
-
await validateAndSave(flags.token);
|
|
136
|
+
await validateAndSave(flags.token, flags.baseUrl);
|
|
91
137
|
return;
|
|
92
138
|
}
|
|
93
139
|
|
|
94
|
-
|
|
140
|
+
if (!flags.json) {
|
|
141
|
+
printLoginBanner({ version: VERSION, noColor: flags.noColor });
|
|
142
|
+
}
|
|
143
|
+
|
|
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`);
|
|
95
164
|
|
|
96
165
|
if (!flags.noBrowser) {
|
|
97
|
-
openBrowser(
|
|
166
|
+
openBrowser(accountUrl);
|
|
98
167
|
}
|
|
99
168
|
|
|
100
169
|
let key;
|
|
@@ -110,18 +179,21 @@ export async function runLogin(flags) {
|
|
|
110
179
|
return;
|
|
111
180
|
}
|
|
112
181
|
|
|
113
|
-
await validateAndSave(key);
|
|
182
|
+
await validateAndSave(key, flags.baseUrl);
|
|
114
183
|
}
|
|
115
184
|
|
|
116
|
-
async function validateAndSave(key) {
|
|
185
|
+
async function validateAndSave(key, baseUrlFlag) {
|
|
117
186
|
process.stderr.write(` Validating key...`);
|
|
118
187
|
|
|
188
|
+
const { baseUrl, region, source } = resolveBaseUrl({ baseUrlFlag, apiKey: key });
|
|
189
|
+
|
|
119
190
|
try {
|
|
120
|
-
await discoverTools({ apiKey: key, query: "test", limit: 1, timeoutMs: 10000 });
|
|
191
|
+
await discoverTools({ apiKey: key, baseUrl, query: "test", limit: 1, timeoutMs: 10000 });
|
|
121
192
|
setConfigValue("api_key", key);
|
|
122
193
|
const masked = key.slice(0, 6) + "..." + key.slice(-4);
|
|
123
194
|
console.error(`\r\x1b[K`);
|
|
124
195
|
console.log(` ${green("\u2713")} Authenticated as ${bold(masked)}`);
|
|
196
|
+
console.log(` ${dim("Region:")} ${region} ${dim(`(${source})`)}`);
|
|
125
197
|
console.log(` ${dim("Key saved to config.")}`);
|
|
126
198
|
} catch {
|
|
127
199
|
console.error(`\r\x1b[K`);
|
|
@@ -145,15 +217,17 @@ export async function runWhoami(flags) {
|
|
|
145
217
|
}
|
|
146
218
|
|
|
147
219
|
const masked = key.slice(0, 6) + "..." + key.slice(-4);
|
|
220
|
+
const { baseUrl, region, source: regionSource } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey: key });
|
|
148
221
|
|
|
149
222
|
process.stderr.write(` Validating...`);
|
|
150
223
|
|
|
151
224
|
try {
|
|
152
|
-
await discoverTools({ apiKey: key, query: "test", limit: 1, timeoutMs: 10000 });
|
|
225
|
+
await discoverTools({ apiKey: key, baseUrl, query: "test", limit: 1, timeoutMs: 10000 });
|
|
153
226
|
process.stderr.write("\r\x1b[K");
|
|
154
227
|
console.log(`\n ${green("\u2713")} Authenticated`);
|
|
155
228
|
console.log(` Key: ${bold(masked)}`);
|
|
156
229
|
console.log(` Source: ${dim(source)}`);
|
|
230
|
+
console.log(` Region: ${region} ${dim(`(${regionSource})`)}`);
|
|
157
231
|
} catch {
|
|
158
232
|
console.error(`\r\x1b[K`);
|
|
159
233
|
console.log(`\n ${red("\u2718")} Key ${masked} is ${red("invalid")}`);
|
package/src/config/region.mjs
CHANGED
|
@@ -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
|
@@ -2,6 +2,7 @@ import { normalizeLegacyArgs } from "./compat/aliases.mjs";
|
|
|
2
2
|
import { handleError } from "./errors/handler.mjs";
|
|
3
3
|
import { VERSION } from "./config/defaults.mjs";
|
|
4
4
|
import { bold, dim, cyan } from "./output/colors.mjs";
|
|
5
|
+
import { printWelcomeBanner } from "./output/banner.mjs";
|
|
5
6
|
|
|
6
7
|
export async function main(argv) {
|
|
7
8
|
const rawArgs = argv.slice(2);
|
|
@@ -12,6 +13,10 @@ export async function main(argv) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const flags = extractGlobalFlags(args);
|
|
16
|
+
if (flags.noColor) {
|
|
17
|
+
process.env.NO_COLOR = "1";
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const positional = flags._positional;
|
|
16
21
|
const command = positional[0];
|
|
17
22
|
const rest = positional.slice(1);
|
|
@@ -22,7 +27,7 @@ export async function main(argv) {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
if (!command || flags.help) {
|
|
25
|
-
printUsage();
|
|
30
|
+
printUsage(flags);
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -202,9 +207,13 @@ function extractGlobalFlags(args) {
|
|
|
202
207
|
return flags;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
|
-
function printUsage() {
|
|
210
|
+
function printUsage(flags = {}) {
|
|
211
|
+
if (!flags.json) {
|
|
212
|
+
printWelcomeBanner({ version: VERSION, noColor: flags.noColor, compact: false });
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
console.log(`
|
|
207
|
-
${bold("QVeris CLI")} ${dim(
|
|
216
|
+
${bold("QVeris CLI")} — ${dim("discover, inspect, and call 10,000+ capabilities")}
|
|
208
217
|
|
|
209
218
|
${bold("Usage:")}
|
|
210
219
|
qveris <command> [args] [flags]
|
|
@@ -232,12 +241,18 @@ function printUsage() {
|
|
|
232
241
|
${bold("Global Flags:")}
|
|
233
242
|
--json, -j Output raw JSON
|
|
234
243
|
--api-key <key> Override API key
|
|
244
|
+
--base-url <url> Override API base URL
|
|
235
245
|
--timeout <seconds> Request timeout
|
|
236
246
|
--no-color Disable colors
|
|
237
247
|
--verbose, -v Show request details
|
|
238
248
|
--version, -V Print version
|
|
239
249
|
--help, -h Show help
|
|
240
250
|
|
|
251
|
+
${bold("Environment Variables:")}
|
|
252
|
+
QVERIS_API_KEY API key
|
|
253
|
+
QVERIS_REGION Region override (global | cn)
|
|
254
|
+
QVERIS_BASE_URL Custom API base URL
|
|
255
|
+
|
|
241
256
|
${bold("Examples:")}
|
|
242
257
|
qveris discover "weather forecast API"
|
|
243
258
|
qveris inspect 1
|
|
@@ -245,6 +260,6 @@ function printUsage() {
|
|
|
245
260
|
qveris call 1 --params @params.json --codegen curl
|
|
246
261
|
qveris interactive
|
|
247
262
|
|
|
248
|
-
${dim("https://qveris.ai")}
|
|
263
|
+
${dim("https://qveris.ai (global) / https://qveris.cn (China)")}
|
|
249
264
|
`);
|
|
250
265
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome / login banners: gradient on solid block letters (brand cyan → indigo).
|
|
3
|
+
* Figlet font "Banner3" (filled #), mapped to █ for stronger contrast.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { cwd as processCwd } from "node:process";
|
|
8
|
+
import { dim, cyan, isColorEnabled } from "./colors.mjs";
|
|
9
|
+
|
|
10
|
+
/** @type {readonly [number, number, number][]} brand stops: highlight → mid → shadow */
|
|
11
|
+
const BRAND_STOPS = [
|
|
12
|
+
[0, 242, 255], // #00F2FF
|
|
13
|
+
[0, 153, 255], // #0099FF
|
|
14
|
+
[26, 0, 255], // #1A00FF
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const RESET = "\x1b[0m";
|
|
18
|
+
|
|
19
|
+
/** Figlet -f Banner3 QVERIS (53 cols × 7 rows). # → █ at render. */
|
|
20
|
+
const ASCII_QVERIS_HASH = [
|
|
21
|
+
" ####### ## ## ######## ######## #### ###### ",
|
|
22
|
+
"## ## ## ## ## ## ## ## ## ## ",
|
|
23
|
+
"## ## ## ## ## ## ## ## ## ",
|
|
24
|
+
"## ## ## ## ###### ######## ## ###### ",
|
|
25
|
+
"## ## ## ## ## ## ## ## ## ## ",
|
|
26
|
+
"## ## ## ## ## ## ## ## ## ## ",
|
|
27
|
+
" ##### ## ### ######## ## ## #### ###### ",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
function solidBlockLines() {
|
|
31
|
+
return ASCII_QVERIS_HASH.split("\n").map((line) => line.replace(/#/g, "█"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function colorDepth() {
|
|
35
|
+
try {
|
|
36
|
+
return typeof process.stdout.getColorDepth === "function"
|
|
37
|
+
? process.stdout.getColorDepth()
|
|
38
|
+
: 8;
|
|
39
|
+
} catch {
|
|
40
|
+
return 8;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lerp(a, b, t) {
|
|
45
|
+
return Math.round(a + (b - a) * t);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** RGB along multi-stop gradient, t in [0,1]. */
|
|
49
|
+
function rgbAt(t) {
|
|
50
|
+
const n = BRAND_STOPS.length - 1;
|
|
51
|
+
const x = Math.min(1, Math.max(0, t)) * n;
|
|
52
|
+
const i = Math.min(Math.floor(x), n - 1);
|
|
53
|
+
const f = x - i;
|
|
54
|
+
const [r1, g1, b1] = BRAND_STOPS[i];
|
|
55
|
+
const [r2, g2, b2] = BRAND_STOPS[i + 1];
|
|
56
|
+
return [lerp(r1, r2, f), lerp(g1, g2, f), lerp(b1, b2, f)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function truecolorFg(r, g, b) {
|
|
60
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Map column index to gradient color (horizontal sweep across the banner).
|
|
65
|
+
* Spaces stay uncolored. Consecutive non-space chars with identical RGB share one SGR span.
|
|
66
|
+
*/
|
|
67
|
+
function paintLineTruecolor(line, maxCols) {
|
|
68
|
+
if (!line.length) return "";
|
|
69
|
+
const denom = Math.max(1, maxCols - 1);
|
|
70
|
+
let out = "";
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < line.length) {
|
|
73
|
+
const ch = line[i];
|
|
74
|
+
if (ch === " ") {
|
|
75
|
+
out += ch;
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const t0 = i / denom;
|
|
80
|
+
const [r0, g0, b0] = rgbAt(t0);
|
|
81
|
+
let j = i + 1;
|
|
82
|
+
while (j < line.length) {
|
|
83
|
+
const chj = line[j];
|
|
84
|
+
if (chj === " ") break;
|
|
85
|
+
const t = j / denom;
|
|
86
|
+
const [r, g, b] = rgbAt(t);
|
|
87
|
+
if (r !== r0 || g !== g0 || b !== b0) break;
|
|
88
|
+
j++;
|
|
89
|
+
}
|
|
90
|
+
out += truecolorFg(r0, g0, b0) + line.slice(i, j) + RESET;
|
|
91
|
+
i = j;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 16-color fallback: batched SGR per band (cyan / bold cyan / dim cyan). */
|
|
97
|
+
function paintLineFallback(line, maxCols) {
|
|
98
|
+
if (!isColorEnabled()) return line;
|
|
99
|
+
const denom = Math.max(1, maxCols - 1);
|
|
100
|
+
let out = "";
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < line.length) {
|
|
103
|
+
const ch = line[i];
|
|
104
|
+
if (ch === " ") {
|
|
105
|
+
out += ch;
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const band0 = Math.min(2, Math.floor((i / denom) * 3));
|
|
110
|
+
let j = i + 1;
|
|
111
|
+
while (j < line.length) {
|
|
112
|
+
const chj = line[j];
|
|
113
|
+
if (chj === " ") break;
|
|
114
|
+
const band = Math.min(2, Math.floor((j / denom) * 3));
|
|
115
|
+
if (band !== band0) break;
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
const chunk = line.slice(i, j);
|
|
119
|
+
if (band0 === 0) out += `\x1b[36m${chunk}\x1b[0m`;
|
|
120
|
+
else if (band0 === 1) out += `\x1b[1;36m${chunk}\x1b[0m`;
|
|
121
|
+
else out += `\x1b[2;36m${chunk}\x1b[0m`;
|
|
122
|
+
i = j;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function bannerColorAllowed(noColorFlag) {
|
|
128
|
+
if (noColorFlag) return false;
|
|
129
|
+
return isColorEnabled();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shortenPath(p) {
|
|
133
|
+
const home = homedir();
|
|
134
|
+
if (p === home) return "~";
|
|
135
|
+
if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
|
|
136
|
+
return p;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Strip SGR ANSI sequences (truecolor / 16-color). */
|
|
140
|
+
function stripAnsi(s) {
|
|
141
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const RE_HAN = /\p{Script=Han}/u;
|
|
145
|
+
const RE_HANGUL = /\p{Script=Hangul}/u;
|
|
146
|
+
|
|
147
|
+
/** Terminal display width: CJK/Hangul/fullwidth = 2 cols (matches typical monospace). */
|
|
148
|
+
function displayWidth(str) {
|
|
149
|
+
let w = 0;
|
|
150
|
+
for (const ch of str) {
|
|
151
|
+
const cp = ch.codePointAt(0);
|
|
152
|
+
if (RE_HAN.test(ch) || RE_HANGUL.test(ch)) w += 2;
|
|
153
|
+
else if (cp >= 0xff01 && cp <= 0xff60) w += 2; // fullwidth forms
|
|
154
|
+
else w += 1;
|
|
155
|
+
}
|
|
156
|
+
return w;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function centerBlock(lines, termCols) {
|
|
160
|
+
const widths = lines.map((l) => displayWidth(stripAnsi(l)));
|
|
161
|
+
const maxW = Math.max(...widths, 1);
|
|
162
|
+
const pad = termCols >= maxW + 4 ? Math.max(0, Math.floor((termCols - maxW) / 2)) : 2;
|
|
163
|
+
const prefix = " ".repeat(pad);
|
|
164
|
+
return lines.map((l) => prefix + l);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {object} opts
|
|
169
|
+
* @param {string} opts.version
|
|
170
|
+
* @param {boolean} [opts.noColor]
|
|
171
|
+
* @param {boolean} [opts.compact] — fewer blank lines (e.g. interactive)
|
|
172
|
+
*/
|
|
173
|
+
export function printWelcomeBanner(opts) {
|
|
174
|
+
const { version, noColor = false, compact = false } = opts;
|
|
175
|
+
if (!bannerColorAllowed(noColor)) {
|
|
176
|
+
printPlainBanner({ version, compact });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rawLines = solidBlockLines();
|
|
181
|
+
const maxW = Math.max(...rawLines.map((l) => l.length));
|
|
182
|
+
const termCols = process.stdout.columns || 80;
|
|
183
|
+
const useTc = colorDepth() >= 24;
|
|
184
|
+
|
|
185
|
+
const painted = rawLines.map((line) => {
|
|
186
|
+
const padded = line.padEnd(maxW);
|
|
187
|
+
return useTc ? paintLineTruecolor(padded, maxW) : paintLineFallback(padded, maxW);
|
|
188
|
+
});
|
|
189
|
+
const centered = centerBlock(painted, termCols);
|
|
190
|
+
|
|
191
|
+
const nl = compact ? "\n" : "\n\n";
|
|
192
|
+
console.log(nl + centered.join("\n"));
|
|
193
|
+
|
|
194
|
+
const tag = "✦ Discover · Inspect · Call · 10,000+ capabilities · intelligent orchestration ✦";
|
|
195
|
+
const meta = dim(`v${version} · ${shortenPath(processCwd())}`);
|
|
196
|
+
const tagLine = centerLine(dim(cyan(tag)), termCols);
|
|
197
|
+
const metaLine = centerLine(meta, termCols);
|
|
198
|
+
|
|
199
|
+
console.log("\n" + tagLine + "\n" + metaLine + (compact ? "\n" : "\n\n"));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function centerLine(text, termCols) {
|
|
203
|
+
const len = displayWidth(stripAnsi(text));
|
|
204
|
+
const pad = termCols >= len + 4 ? Math.max(0, Math.floor((termCols - len) / 2)) : 2;
|
|
205
|
+
return " ".repeat(pad) + text;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printPlainBanner({ version, compact }) {
|
|
209
|
+
const rawLines = solidBlockLines();
|
|
210
|
+
const termCols = process.stdout.columns || 80;
|
|
211
|
+
const centered = centerBlock(rawLines, termCols);
|
|
212
|
+
const nl = compact ? "\n" : "\n\n";
|
|
213
|
+
console.log(nl + centered.join("\n"));
|
|
214
|
+
const tag = "✦ Discover · Inspect · Call · 10,000+ capabilities · intelligent orchestration ✦";
|
|
215
|
+
const meta = `v${version} · ${shortenPath(processCwd())}`;
|
|
216
|
+
const tagLine = centerLine(dim(tag), termCols);
|
|
217
|
+
const metaLine = centerLine(dim(meta), termCols);
|
|
218
|
+
console.log("\n" + tagLine + "\n" + metaLine + (compact ? "\n" : "\n\n"));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Login screen: banner + framed hint (cyan border).
|
|
223
|
+
*/
|
|
224
|
+
export function printLoginBanner(opts) {
|
|
225
|
+
const { version, noColor = false } = opts;
|
|
226
|
+
printWelcomeBanner({ version, noColor, compact: true });
|
|
227
|
+
|
|
228
|
+
if (!bannerColorAllowed(noColor)) {
|
|
229
|
+
console.log(dim(" ─ Secure login · paste your API key below"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const termCols = process.stdout.columns || 80;
|
|
234
|
+
const inner = " Secure login · API key ";
|
|
235
|
+
const maxInner = Math.min(inner.length + 4, termCols - 4);
|
|
236
|
+
const bar = "─".repeat(Math.max(8, maxInner));
|
|
237
|
+
const top = centerLine(cyan("╭" + bar + "╮"), termCols);
|
|
238
|
+
const mid = centerLine(cyan("│") + dim(inner) + cyan("│"), termCols);
|
|
239
|
+
const bot = centerLine(cyan("╰" + bar + "╯"), termCols);
|
|
240
|
+
console.log(top + "\n" + mid + "\n" + bot + "\n");
|
|
241
|
+
}
|
package/src/output/colors.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
(
|
|
1
|
+
let memoizedEnabled;
|
|
2
|
+
export const isColorEnabled = () => {
|
|
3
|
+
if (memoizedEnabled !== undefined) return memoizedEnabled;
|
|
4
|
+
memoizedEnabled = !process.env.NO_COLOR &&
|
|
5
|
+
(process.env.FORCE_COLOR === "1" || (process.stdout.isTTY && process.stderr.isTTY));
|
|
6
|
+
return memoizedEnabled;
|
|
7
|
+
};
|
|
4
8
|
|
|
5
|
-
const code = (open, close) =>
|
|
6
|
-
|
|
9
|
+
const code = (open, close) => (s) =>
|
|
10
|
+
isColorEnabled() ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
7
11
|
|
|
8
12
|
export const bold = code("1", "22");
|
|
9
13
|
export const dim = code("2", "22");
|
|
@@ -12,5 +16,3 @@ export const green = code("32", "39");
|
|
|
12
16
|
export const yellow = code("33", "39");
|
|
13
17
|
export const cyan = code("36", "39");
|
|
14
18
|
export const gray = code("90", "39");
|
|
15
|
-
|
|
16
|
-
export const isColorEnabled = () => enabled;
|