@qverisai/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import { resolve } from "../config/resolve.mjs";
2
+
3
+ export function generateSnippet(lang, { toolId, discoveryId, parameters, maxResponseSize = 20480 }) {
4
+ const baseUrl = resolve("base_url").value;
5
+
6
+ switch (lang) {
7
+ case "curl":
8
+ return generateCurl({ baseUrl, toolId, discoveryId, parameters, maxResponseSize });
9
+ case "js":
10
+ case "javascript":
11
+ return generateJs({ baseUrl, toolId, discoveryId, parameters, maxResponseSize });
12
+ case "python":
13
+ case "py":
14
+ return generatePython({ baseUrl, toolId, discoveryId, parameters, maxResponseSize });
15
+ default:
16
+ return `Unsupported language: ${lang}. Use: curl, js, python`;
17
+ }
18
+ }
19
+
20
+ function generateCurl({ baseUrl, toolId, discoveryId, parameters, maxResponseSize }) {
21
+ const body = JSON.stringify({
22
+ search_id: discoveryId,
23
+ parameters,
24
+ max_response_size: maxResponseSize,
25
+ }, null, 2);
26
+
27
+ // Use heredoc to avoid single-quote escaping issues in parameters
28
+ return `curl -sS -X POST "${baseUrl}/tools/execute?tool_id=${toolId}" \\
29
+ -H "Authorization: Bearer $QVERIS_API_KEY" \\
30
+ -H "Content-Type: application/json" \\
31
+ -d @- <<'EOF'
32
+ ${body}
33
+ EOF`;
34
+ }
35
+
36
+ function generateJs({ baseUrl, toolId, discoveryId, parameters, maxResponseSize }) {
37
+ const paramsStr = JSON.stringify(parameters, null, 4);
38
+ return `const resp = await fetch(
39
+ "${baseUrl}/tools/execute?tool_id=${toolId}",
40
+ {
41
+ method: "POST",
42
+ headers: {
43
+ Authorization: \`Bearer \${process.env.QVERIS_API_KEY}\`,
44
+ "Content-Type": "application/json",
45
+ },
46
+ body: JSON.stringify({
47
+ search_id: "${discoveryId}",
48
+ parameters: ${paramsStr},
49
+ max_response_size: ${maxResponseSize},
50
+ }),
51
+ }
52
+ );
53
+ const data = await resp.json();
54
+ console.log(data);`;
55
+ }
56
+
57
+ function generatePython({ baseUrl, toolId, discoveryId, parameters, maxResponseSize }) {
58
+ const paramsStr = JSON.stringify(parameters, null, 8);
59
+ return `import os
60
+ import requests
61
+
62
+ resp = requests.post(
63
+ "${baseUrl}/tools/execute",
64
+ params={"tool_id": "${toolId}"},
65
+ headers={"Authorization": f"Bearer {os.environ['QVERIS_API_KEY']}"},
66
+ json={
67
+ "search_id": "${discoveryId}",
68
+ "parameters": ${paramsStr},
69
+ "max_response_size": ${maxResponseSize},
70
+ },
71
+ timeout=60,
72
+ )
73
+ data = resp.json()
74
+ print(data)`;
75
+ }
@@ -0,0 +1,16 @@
1
+ const enabled =
2
+ !process.env.NO_COLOR &&
3
+ (process.env.FORCE_COLOR === "1" || (process.stdout.isTTY && process.stderr.isTTY));
4
+
5
+ const code = (open, close) =>
6
+ enabled ? (s) => `\x1b[${open}m${s}\x1b[${close}m` : (s) => s;
7
+
8
+ export const bold = code("1", "22");
9
+ export const dim = code("2", "22");
10
+ export const red = code("31", "39");
11
+ export const green = code("32", "39");
12
+ export const yellow = code("33", "39");
13
+ export const cyan = code("36", "39");
14
+ export const gray = code("90", "39");
15
+
16
+ export const isColorEnabled = () => enabled;
@@ -0,0 +1,280 @@
1
+ import { bold, cyan, dim, green, yellow, red } from "./colors.mjs";
2
+
3
+ // ── discover ──────────────────────────────────────────────────────────
4
+
5
+ export function formatDiscoverResult(result) {
6
+ const tools = result.results ?? [];
7
+ const total = result.total ?? tools.length;
8
+ const searchTime = result.elapsed_time_ms;
9
+ const remaining = result.remaining_credits;
10
+ const lines = [];
11
+
12
+ if (tools.length === 0) {
13
+ lines.push("No tools matched your query. Try a broader description.");
14
+ return lines.join("\n");
15
+ }
16
+
17
+ lines.push(`Found ${bold(String(total))} capabilities matching your query`);
18
+ lines.push("");
19
+
20
+ for (let i = 0; i < tools.length; i++) {
21
+ const t = tools[i];
22
+ const name = t.name ?? "N/A";
23
+ const toolId = t.tool_id ?? "";
24
+ const desc = stringifyDesc(t.description);
25
+ const provider = t.provider_name ?? "";
26
+ const categories = Array.isArray(t.categories) ? t.categories.join(", ") : "";
27
+ const region = t.region ?? "global";
28
+ const stats = t.stats ?? {};
29
+
30
+ // Relevance score
31
+ const score = t.final_score;
32
+ const scoreStr = typeof score === "number" ? `${(score * 100).toFixed(0)}%` : "";
33
+
34
+ // Quality metrics
35
+ let successRate = stats.success_rate;
36
+ successRate = typeof successRate === "number" ? `${(successRate * 100).toFixed(1)}%` : "N/A";
37
+ let avgTime = stats.avg_execution_time_ms;
38
+ avgTime = typeof avgTime === "number" ? `~${Math.round(avgTime)}ms` : "N/A";
39
+ const cost = stats.cost ?? "?";
40
+
41
+ // Verified indicator
42
+ const verified = t.has_last_execution ? green(" \u2713") : "";
43
+
44
+ // Line 1: index + name + provider
45
+ const providerPart = provider ? ` ${dim("by")} ${provider}` : "";
46
+ lines.push(`${bold(String(i + 1) + ".")} ${cyan(name)}${providerPart}${verified}`);
47
+
48
+ // Line 2: tool_id
49
+ lines.push(` ${dim(toolId)}`);
50
+
51
+ // Line 3: description (truncated)
52
+ if (desc) {
53
+ lines.push(` ${desc.length > 100 ? desc.slice(0, 100) + "..." : desc}`);
54
+ }
55
+
56
+ // Line 4: metrics row
57
+ const metricParts = [];
58
+ if (scoreStr) metricParts.push(`relevance: ${bold(scoreStr)}`);
59
+ metricParts.push(`success: ${green(successRate)}`);
60
+ metricParts.push(`latency: ${avgTime}`);
61
+ metricParts.push(`cost: ${yellow(String(cost))} cr`);
62
+ if (region !== "global") metricParts.push(`region: ${region}`);
63
+ lines.push(` ${dim(metricParts.join(" \u00b7 "))}`);
64
+
65
+ // Line 5: categories (if present)
66
+ if (categories) {
67
+ lines.push(` ${dim("tags:")} ${dim(categories)}`);
68
+ }
69
+
70
+ lines.push("");
71
+ }
72
+
73
+ // Footer
74
+ const footerParts = [`Discovery ID: ${result.search_id ?? "N/A"}`];
75
+ if (typeof searchTime === "number") footerParts.push(`${Math.round(searchTime)}ms`);
76
+ if (typeof remaining === "number") footerParts.push(`${remaining} credits remaining`);
77
+ lines.push(dim(footerParts.join(" \u00b7 ")));
78
+
79
+ return lines.join("\n");
80
+ }
81
+
82
+ // ── inspect ───────────────────────────────────────────────────────────
83
+
84
+ export function formatInspectResult(tools) {
85
+ const list = Array.isArray(tools) ? tools : tools?.results ?? tools?.tools ?? [tools];
86
+ const remaining = tools?.remaining_credits;
87
+ const lines = [];
88
+
89
+ for (const t of list) {
90
+ const name = t.name ?? t.tool_id ?? "N/A";
91
+ const toolId = t.tool_id ?? "";
92
+ const desc = stringifyDesc(t.description);
93
+ const provider = t.provider_name ?? "";
94
+ const providerDesc = stringifyDesc(t.provider_description);
95
+ const categories = Array.isArray(t.categories) ? t.categories.join(", ") : "";
96
+ const region = t.region ?? "global";
97
+ const docsUrl = t.docs_url ?? "";
98
+ const stats = t.stats ?? {};
99
+
100
+ let successRate = stats.success_rate;
101
+ successRate = typeof successRate === "number" ? `${(successRate * 100).toFixed(1)}%` : "N/A";
102
+ let avgTime = stats.avg_execution_time_ms;
103
+ avgTime = typeof avgTime === "number" ? `~${Math.round(avgTime)}ms` : "N/A";
104
+ const cost = stats.cost ?? "?";
105
+
106
+ // Header
107
+ lines.push(bold(cyan(name)));
108
+ if (toolId && toolId !== name) lines.push(dim(toolId));
109
+ if (desc) lines.push(desc);
110
+ lines.push("");
111
+
112
+ // Metadata table
113
+ if (provider) {
114
+ const pLine = providerDesc ? `${provider} ${dim("— " + providerDesc)}` : provider;
115
+ lines.push(` Provider: ${pLine}`);
116
+ }
117
+ if (categories) lines.push(` Categories: ${categories}`);
118
+ lines.push(` Region: ${region}`);
119
+ lines.push(` Latency: ${bold(avgTime)}`);
120
+ lines.push(` Success: ${green(successRate)}`);
121
+ lines.push(` Cost: ${yellow(String(cost))} credits`);
122
+ if (t.has_last_execution) lines.push(` Verified: ${green("\u2713 has execution history")}`);
123
+ if (docsUrl) lines.push(` Docs: ${cyan(docsUrl)}`);
124
+
125
+ // Parameters
126
+ const params = t.params ?? [];
127
+ if (params.length > 0) {
128
+ lines.push("");
129
+ lines.push(bold(" Parameters:"));
130
+ for (const p of params) {
131
+ const req = p.required ? bold("required") : dim("optional");
132
+ const pType = dim(p.type ?? "string");
133
+ const pDesc = p.description ? stringifyDesc(p.description) : "";
134
+ lines.push(` ${cyan(p.name)} ${pType} ${req}`);
135
+ if (pDesc) lines.push(` ${dim(pDesc)}`);
136
+ if (Array.isArray(p.enum) && p.enum.length > 0) {
137
+ lines.push(` ${dim("values:")} ${p.enum.map((v) => yellow(JSON.stringify(v))).join(", ")}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ // Example
143
+ const examples = t.examples ?? {};
144
+ if (examples.sample_parameters) {
145
+ lines.push("");
146
+ lines.push(bold(" Example:"));
147
+ lines.push(` ${JSON.stringify(examples.sample_parameters)}`);
148
+ }
149
+
150
+ // Last execution record
151
+ if (t.last_execution_record && typeof t.last_execution_record === "object") {
152
+ const rec = t.last_execution_record;
153
+ lines.push("");
154
+ lines.push(dim(" Last execution:"));
155
+ if (rec.success !== undefined) lines.push(` Status: ${rec.success ? green("success") : red("failed")}`);
156
+ if (rec.execution_time) lines.push(` Time: ${rec.execution_time}`);
157
+ if (rec.error_message) lines.push(` Error: ${red(rec.error_message)}`);
158
+ }
159
+ }
160
+
161
+ // Footer
162
+ if (typeof remaining === "number") {
163
+ lines.push("");
164
+ lines.push(dim(`${remaining} credits remaining`));
165
+ }
166
+
167
+ return lines.join("\n");
168
+ }
169
+
170
+ // ── call ──────────────────────────────────────────────────────────────
171
+
172
+ export function formatCallResult(result) {
173
+ const success = result.success ?? false;
174
+ const cost = result.cost ?? result.credits_used ?? 0;
175
+ const remaining = result.remaining_credits;
176
+ const executionId = result.execution_id;
177
+ const toolId = result.tool_id;
178
+ const lines = [];
179
+
180
+ // Status line
181
+ if (success) {
182
+ let timePart = "";
183
+ // elapsed_time_ms is in milliseconds; execution_time is in seconds
184
+ if (typeof result.elapsed_time_ms === "number") {
185
+ timePart = `${Math.round(result.elapsed_time_ms)}ms`;
186
+ } else if (typeof result.execution_time === "number") {
187
+ timePart = `${Math.round(result.execution_time * 1000)}ms`;
188
+ }
189
+ const parts = [`${green("\u2713")} ${bold("success")}`];
190
+ if (timePart) parts.push(dim(timePart));
191
+ if (cost) parts.push(`${yellow(String(cost))} credits`);
192
+ if (typeof remaining === "number") parts.push(dim(`(${remaining} remaining)`));
193
+ lines.push(parts.join(" \u00b7 "));
194
+ } else {
195
+ lines.push(`${red("\u2718")} ${bold("failed")}`);
196
+ const errMsg = result.error_message ?? "Unknown error";
197
+ lines.push(red(errMsg));
198
+ }
199
+
200
+ // Execution metadata
201
+ const metaParts = [];
202
+ if (toolId) metaParts.push(`tool: ${toolId}`);
203
+ if (executionId) metaParts.push(`id: ${executionId}`);
204
+ if (metaParts.length > 0) lines.push(dim(metaParts.join(" \u00b7 ")));
205
+
206
+ // Result data
207
+ const data = result.result ?? {};
208
+ const fullContentUrl = typeof data.full_content_file_url === "string" ? data.full_content_file_url : null;
209
+
210
+ if (fullContentUrl) {
211
+ // ── Truncated result ──
212
+ const msg = data.message;
213
+ if (msg) lines.push(`\n${yellow(msg)}`);
214
+
215
+ lines.push(`\n${bold("Full content (valid 120 min):")}`);
216
+ lines.push(` ${cyan(fullContentUrl)}`);
217
+ lines.push(` ${dim("Download:")} curl -o result.json '${fullContentUrl}'`);
218
+
219
+ // Schema: compact one-line summary of the data structure
220
+ if (data.content_schema) {
221
+ lines.push(`\n${bold("Schema:")}`);
222
+ lines.push(formatSchema(data.content_schema, " "));
223
+ }
224
+
225
+ // Truncated content preview
226
+ if (data.truncated_content) {
227
+ lines.push(`\n${bold("Preview:")}`);
228
+ const raw = typeof data.truncated_content === "string"
229
+ ? data.truncated_content
230
+ : JSON.stringify(data.truncated_content, null, 2);
231
+ const previewLines = raw.slice(0, 800).split("\n").slice(0, 15);
232
+ for (const l of previewLines) lines.push(dim(` ${l}`));
233
+ if (raw.length > 800) lines.push(dim(" ..."));
234
+ lines.push(`\n${dim("Use --max-size -1 for full output, or download the file above.")}`);
235
+ }
236
+ } else if (data.data !== undefined) {
237
+ // Standard result with data field
238
+ lines.push(`\n${JSON.stringify(data.data, null, 2)}`);
239
+ } else if (Object.keys(data).length > 0) {
240
+ // Fallback: raw result object
241
+ lines.push(`\n${JSON.stringify(data, null, 2)}`);
242
+ }
243
+
244
+ return lines.join("\n");
245
+ }
246
+
247
+ // ── helpers ───────────────────────────────────────────────────────────
248
+
249
+ /** Format a JSON Schema into a compact readable tree. */
250
+ function formatSchema(schema, indent = "", depth = 0) {
251
+ if (depth > 8 || !schema || typeof schema !== "object") return `${indent}${dim(schema?.type ?? "(unknown)")}`;
252
+ const lines = [];
253
+ if (schema.type === "object" && schema.properties) {
254
+ for (const [key, val] of Object.entries(schema.properties)) {
255
+ const type = val.type ?? "any";
256
+ if (type === "array" && val.items?.properties) {
257
+ lines.push(`${indent}${cyan(key)}: ${dim(type + " of")}`);
258
+ lines.push(formatSchema(val.items, indent + " ", depth + 1));
259
+ } else if (type === "object" && val.properties) {
260
+ lines.push(`${indent}${cyan(key)}: ${dim(type)}`);
261
+ lines.push(formatSchema(val, indent + " ", depth + 1));
262
+ } else {
263
+ lines.push(`${indent}${cyan(key)}: ${dim(type)}`);
264
+ }
265
+ }
266
+ } else {
267
+ lines.push(`${indent}${dim(schema.type ?? "any")}`);
268
+ }
269
+ return lines.join("\n");
270
+ }
271
+
272
+ /** Handle description that may be a string, i18n object {en: "...", zh: "..."}, or other. */
273
+ function stringifyDesc(desc) {
274
+ if (!desc) return "";
275
+ if (typeof desc === "string") return desc;
276
+ if (typeof desc === "object") {
277
+ return desc.en || desc.zh || desc["zh-CN"] || Object.values(desc).find((v) => typeof v === "string") || "";
278
+ }
279
+ return String(desc);
280
+ }
@@ -0,0 +1,11 @@
1
+ export function outputJson(data) {
2
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
3
+ }
4
+
5
+ export function outputJsonError(error, exitCode = 1) {
6
+ const obj = { error: error.message || String(error) };
7
+ if (error.code) obj.code = error.code;
8
+ if (error.hint) obj.hint = error.hint;
9
+ obj.exit_code = exitCode;
10
+ process.stderr.write(JSON.stringify(obj) + "\n");
11
+ }
@@ -0,0 +1,17 @@
1
+ const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2
+
3
+ export function createSpinner(message) {
4
+ if (!process.stderr.isTTY) return { stop() {} };
5
+
6
+ let i = 0;
7
+ const timer = setInterval(() => {
8
+ process.stderr.write(`\r ${FRAMES[i++ % FRAMES.length]} ${message}`);
9
+ }, 80);
10
+
11
+ return {
12
+ stop(clearLine = true) {
13
+ clearInterval(timer);
14
+ if (clearLine) process.stderr.write("\r\x1b[K");
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,53 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ const SESSION_TTL_MS = 30 * 60 * 1000;
6
+
7
+ function sessionPath() {
8
+ const xdg = process.env.XDG_CONFIG_HOME;
9
+ const base = xdg || join(homedir(), ".config");
10
+ return join(base, "qveris", ".session.json");
11
+ }
12
+
13
+ export function readSession() {
14
+ try {
15
+ const data = JSON.parse(readFileSync(sessionPath(), "utf-8"));
16
+ if (data.timestamp && Date.now() - data.timestamp > SESSION_TTL_MS) {
17
+ return null;
18
+ }
19
+ return data;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export function writeSession(data) {
26
+ const dir = dirname(sessionPath());
27
+ mkdirSync(dir, { recursive: true });
28
+ writeFileSync(sessionPath(), JSON.stringify({ ...data, timestamp: Date.now() }, null, 2) + "\n", { mode: 0o600 });
29
+ }
30
+
31
+ export function resolveToolId(idOrIndex) {
32
+ if (!/^\d+$/.test(idOrIndex)) return { toolId: idOrIndex, fromSession: false };
33
+
34
+ const session = readSession();
35
+ if (!session || !session.results) return { toolId: idOrIndex, fromSession: false };
36
+
37
+ const index = parseInt(idOrIndex, 10) - 1;
38
+ if (index < 0 || index >= session.results.length) {
39
+ return { toolId: idOrIndex, fromSession: false };
40
+ }
41
+
42
+ return {
43
+ toolId: session.results[index].tool_id,
44
+ name: session.results[index].name,
45
+ discoveryId: session.discoveryId,
46
+ fromSession: true,
47
+ };
48
+ }
49
+
50
+ export function getSessionDiscoveryId() {
51
+ const session = readSession();
52
+ return session?.discoveryId || null;
53
+ }
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { CliError } from "../errors/handler.mjs";
3
+
4
+ export function resolveParams(value) {
5
+ if (!value || value === "{}") return {};
6
+
7
+ let raw = value;
8
+
9
+ if (value === "-") {
10
+ if (process.stdin.isTTY) {
11
+ throw new CliError("PARAMS_INVALID_JSON", "No data piped to stdin. Usage: echo '{...}' | qveris call ... --params -");
12
+ }
13
+ try {
14
+ // Use fd 0 (stdin) which works cross-platform (Linux, macOS, Windows)
15
+ // Limit to 1MB to prevent excessive memory usage from large piped input
16
+ const MAX_STDIN = 1024 * 1024;
17
+ raw = readFileSync(0, "utf-8").slice(0, MAX_STDIN);
18
+ } catch {
19
+ throw new CliError("PARAMS_INVALID_JSON", "Failed to read from stdin");
20
+ }
21
+ } else if (value.startsWith("@")) {
22
+ const filePath = value.slice(1);
23
+ try {
24
+ raw = readFileSync(filePath, "utf-8");
25
+ } catch (err) {
26
+ throw new CliError("PARAMS_INVALID_JSON", `Cannot read params file: ${filePath}`);
27
+ }
28
+ }
29
+
30
+ try {
31
+ return JSON.parse(raw);
32
+ } catch (err) {
33
+ throw new CliError("PARAMS_INVALID_JSON", `${err.message}`);
34
+ }
35
+ }