@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 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 and key source |
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. To override manually:
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
- # Via environment variable
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 set a custom base URL
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qverisai/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "QVeris CLI -- discover, inspect, and call 10,000+ capabilities",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # QVeris CLI Installer
3
- # Usage: curl -fsSL https://qveris.ai/install | bash
3
+ # Usage: curl -fsSL https://qveris.ai/cli/install | bash
4
4
  set -euo pipefail
5
5
 
6
6
  BOLD='\033[1m'
@@ -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
 
@@ -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("https://qveris.ai/account")}\n`);
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("https://qveris.ai/account")}\n`);
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,
@@ -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
- console.log(`\n ${bold("QVeris Interactive Mode")}`);
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
  }
@@ -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
- console.log(`\n Get your API key at: ${cyan(ACCOUNT_URL)}\n`);
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(ACCOUNT_URL);
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")}`);
@@ -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(`v${VERSION}`)} -- discover, inspect, and call 10,000+ capabilities
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
+ }
@@ -1,9 +1,13 @@
1
- const enabled =
2
- !process.env.NO_COLOR &&
3
- (process.env.FORCE_COLOR === "1" || (process.stdout.isTTY && process.stderr.isTTY));
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
- enabled ? (s) => `\x1b[${open}m${s}\x1b[${close}m` : (s) => s;
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;