@qverisai/cli 0.5.0 → 0.6.1
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 +169 -41
- package/package.json +6 -3
- package/src/client/auth.mjs +8 -4
- package/src/commands/init.mjs +378 -0
- package/src/commands/mcp.mjs +686 -0
- package/src/errors/codes.mjs +18 -3
- package/src/main.mjs +49 -0
- package/src/output/formatter.mjs +23 -2
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { resolveApiKey } from "../client/auth.mjs";
|
|
2
|
+
import { callTool, discoverTools, inspectToolsByIds } from "../client/api.mjs";
|
|
3
|
+
import { resolve } from "../config/resolve.mjs";
|
|
4
|
+
import { resolveBaseUrl } from "../config/region.mjs";
|
|
5
|
+
import { CliError } from "../errors/handler.mjs";
|
|
6
|
+
import { bold, cyan, dim, green, red, yellow } from "../output/colors.mjs";
|
|
7
|
+
import { outputJson } from "../output/json.mjs";
|
|
8
|
+
import { readSession, writeSession } from "../session/session.mjs";
|
|
9
|
+
import { resolveParams } from "../utils/params.mjs";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_QUERY = "weather forecast API";
|
|
12
|
+
const DEFAULT_LIMIT = 5;
|
|
13
|
+
const DEFAULT_MAX_RESPONSE_SIZE = 20480;
|
|
14
|
+
|
|
15
|
+
export async function runInit(queryArg, flags) {
|
|
16
|
+
const steps = [];
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
|
|
19
|
+
if (!flags.json) {
|
|
20
|
+
console.log(`\n ${bold("QVeris init")} ${dim("first-call wizard")}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const timeoutMs = (parseInt(flags.timeout, 10) || 60) * 1000;
|
|
24
|
+
const apiKey = getInitApiKey(flags);
|
|
25
|
+
const { region, baseUrl, source: regionSource } = resolveBaseUrl({ baseUrlFlag: flags.baseUrl, apiKey });
|
|
26
|
+
record(steps, "auth", "ok", "API key resolved", {
|
|
27
|
+
source: resolve("api_key", flags.apiKey || flags.token).source,
|
|
28
|
+
key: maskKey(apiKey),
|
|
29
|
+
region,
|
|
30
|
+
base_url: baseUrl,
|
|
31
|
+
region_source: regionSource,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let discovery;
|
|
35
|
+
let discoveryId;
|
|
36
|
+
let selected;
|
|
37
|
+
let candidateTools = [];
|
|
38
|
+
const query = flags.query || queryArg || DEFAULT_QUERY;
|
|
39
|
+
const limit = parseInt(flags.limit, 10) || DEFAULT_LIMIT;
|
|
40
|
+
const maxResponseSize = resolveInitMaxResponseSize(flags);
|
|
41
|
+
const flagParameters = flags.params ? resolveParams(flags.params) : null;
|
|
42
|
+
|
|
43
|
+
if (flags.resume) {
|
|
44
|
+
const session = readSession();
|
|
45
|
+
if (!session?.discoveryId || !Array.isArray(session.results) || session.results.length === 0) {
|
|
46
|
+
throw new CliError("SESSION_EXPIRED", "No resumable init session found. Run 'qveris init' without --resume first.");
|
|
47
|
+
}
|
|
48
|
+
discoveryId = session.discoveryId;
|
|
49
|
+
candidateTools = flags.toolId
|
|
50
|
+
? [{ tool_id: flags.toolId, name: flags.toolId }]
|
|
51
|
+
: session.results;
|
|
52
|
+
discovery = {
|
|
53
|
+
search_id: discoveryId,
|
|
54
|
+
query: session.query,
|
|
55
|
+
results: session.results,
|
|
56
|
+
resumed: true,
|
|
57
|
+
};
|
|
58
|
+
record(steps, "discover", "ok", "Resumed previous discovery session", {
|
|
59
|
+
query: session.query,
|
|
60
|
+
search_id: discoveryId,
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
record(steps, "discover", "running", `Discovering capabilities for "${query}"`, { query, limit });
|
|
64
|
+
discovery = await discoverTools({ apiKey, baseUrl, query, limit, timeoutMs });
|
|
65
|
+
const results = discovery.results ?? [];
|
|
66
|
+
if (results.length === 0) {
|
|
67
|
+
throw new CliError("TOOL_NOT_FOUND", `No capabilities matched "${query}". Try 'qveris init --query <broader-query>'.`);
|
|
68
|
+
}
|
|
69
|
+
discoveryId = discovery.search_id;
|
|
70
|
+
candidateTools = flags.toolId
|
|
71
|
+
? [{ tool_id: flags.toolId, name: flags.toolId }]
|
|
72
|
+
: results;
|
|
73
|
+
writeSession({
|
|
74
|
+
discoveryId,
|
|
75
|
+
query,
|
|
76
|
+
region,
|
|
77
|
+
baseUrl,
|
|
78
|
+
results: results.map((t, i) => ({
|
|
79
|
+
index: i + 1,
|
|
80
|
+
tool_id: t.tool_id,
|
|
81
|
+
name: t.name,
|
|
82
|
+
provider_name: t.provider_name,
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
updateLast(steps, "ok", "Discovery completed", {
|
|
86
|
+
query,
|
|
87
|
+
search_id: discoveryId,
|
|
88
|
+
total: discovery.total ?? results.length,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const candidateToolIds = candidateTools.map((tool) => tool?.tool_id).filter(Boolean);
|
|
93
|
+
if (candidateToolIds.length === 0) {
|
|
94
|
+
throw new CliError("TOOL_NOT_FOUND", "No selectable capabilities were returned.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
record(
|
|
98
|
+
steps,
|
|
99
|
+
"inspect",
|
|
100
|
+
"running",
|
|
101
|
+
candidateToolIds.length === 1 ? "Inspecting selected capability" : "Inspecting candidate capabilities",
|
|
102
|
+
{ tool_ids: candidateToolIds }
|
|
103
|
+
);
|
|
104
|
+
const inspected = await inspectToolsByIds({
|
|
105
|
+
apiKey,
|
|
106
|
+
baseUrl,
|
|
107
|
+
toolIds: candidateToolIds,
|
|
108
|
+
discoveryId,
|
|
109
|
+
timeoutMs,
|
|
110
|
+
});
|
|
111
|
+
const inspectedTools = mergeCandidateTools(candidateTools, normalizeToolList(inspected));
|
|
112
|
+
selected = pickInitTool(inspectedTools, { toolId: flags.toolId, parameters: flagParameters });
|
|
113
|
+
const inspectedTool = pickInspectedTool(inspectedTools, selected.tool_id) || selected;
|
|
114
|
+
updateLast(steps, "ok", "Inspection completed", {
|
|
115
|
+
tool_id: inspectedTool.tool_id || selected.tool_id,
|
|
116
|
+
tool_name: inspectedTool.name || selected.name,
|
|
117
|
+
selected_reason: flagParameters && !flags.toolId ? "params_match" : flags.toolId ? "explicit_tool" : "first_candidate",
|
|
118
|
+
has_sample_parameters: Boolean(getSampleParameters(inspectedTool)),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { parameters, source: paramsSource } = getInitParameters(flags, inspectedTool, flagParameters);
|
|
122
|
+
record(steps, "call", flags.dryRun ? "skipped" : "running", flags.dryRun ? "Dry run requested" : "Calling selected capability", {
|
|
123
|
+
tool_id: selected.tool_id,
|
|
124
|
+
params_source: paramsSource,
|
|
125
|
+
max_response_size: maxResponseSize,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const nextCommands = buildNextCommands({ selected, discoveryId, parameters });
|
|
129
|
+
|
|
130
|
+
if (flags.dryRun) {
|
|
131
|
+
const payload = buildPayload({ steps, discovery, inspectedTool, parameters, callResult: null, nextCommands, startedAt, dryRun: true });
|
|
132
|
+
if (flags.json) outputJson(payload);
|
|
133
|
+
else printHumanSummary(payload);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const callResult = await callTool({
|
|
138
|
+
apiKey,
|
|
139
|
+
baseUrl,
|
|
140
|
+
toolId: selected.tool_id,
|
|
141
|
+
discoveryId,
|
|
142
|
+
parameters,
|
|
143
|
+
maxResponseSize,
|
|
144
|
+
timeoutMs,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!callResult.success) {
|
|
148
|
+
const code = looksLikeProviderFailure(callResult.error_message) ? "PROVIDER_FAILURE" : "TOOL_CALL_FAILED";
|
|
149
|
+
const err = new CliError(code, callResult.error_message || "The selected capability returned success=false.");
|
|
150
|
+
if (code === "TOOL_CALL_FAILED") {
|
|
151
|
+
err.hint = `Rerun with adjusted params: qveris init --resume --params ${shellSingleQuote(JSON.stringify(parameters))}`;
|
|
152
|
+
}
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
updateLast(steps, "ok", "First call succeeded", {
|
|
157
|
+
execution_id: callResult.execution_id,
|
|
158
|
+
elapsed_time_ms: callResult.elapsed_time_ms ?? callResult.execution_time,
|
|
159
|
+
});
|
|
160
|
+
const finalNextCommands = {
|
|
161
|
+
...nextCommands,
|
|
162
|
+
usage: nextCommands.usage.replace("<execution_id>", callResult.execution_id),
|
|
163
|
+
};
|
|
164
|
+
record(steps, "audit", "ok", "Use usage and ledger commands to reconcile final billing", {
|
|
165
|
+
usage: finalNextCommands.usage,
|
|
166
|
+
ledger: finalNextCommands.ledger,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const payload = buildPayload({ steps, discovery, inspectedTool, parameters, callResult, nextCommands, startedAt, dryRun: false });
|
|
170
|
+
if (flags.json) outputJson(payload);
|
|
171
|
+
else printHumanSummary(payload);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getInitApiKey(flags) {
|
|
175
|
+
try {
|
|
176
|
+
return resolveApiKey(flags.apiKey || flags.token);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof CliError && err.code === "AUTH_MISSING_KEY") {
|
|
179
|
+
err.hint = "Run 'qveris login', set QVERIS_API_KEY, or pass --api-key/--token to qveris init.";
|
|
180
|
+
}
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getInitParameters(flags, tool, flagParameters = null) {
|
|
186
|
+
if (flags.params) {
|
|
187
|
+
return { parameters: flagParameters ?? resolveParams(flags.params), source: "flag" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const sample = getSampleParameters(tool);
|
|
191
|
+
if (sample) {
|
|
192
|
+
return { parameters: sample, source: "example" };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw new CliError("INIT_PARAMS_REQUIRED");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getSampleParameters(tool) {
|
|
199
|
+
const examples = tool?.examples;
|
|
200
|
+
if (!examples || typeof examples !== "object") return null;
|
|
201
|
+
if (examples.sample_parameters && typeof examples.sample_parameters === "object") {
|
|
202
|
+
return examples.sample_parameters;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(examples) && examples[0]?.parameters && typeof examples[0].parameters === "object") {
|
|
205
|
+
return examples[0].parameters;
|
|
206
|
+
}
|
|
207
|
+
if (examples.parameters && typeof examples.parameters === "object") {
|
|
208
|
+
return examples.parameters;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function pickInspectedTool(response, toolId) {
|
|
214
|
+
const list = normalizeToolList(response);
|
|
215
|
+
if (!Array.isArray(list)) return null;
|
|
216
|
+
return (toolId ? list.find((t) => t?.tool_id === toolId) : list[0]) || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeToolList(response) {
|
|
220
|
+
if (Array.isArray(response)) return response;
|
|
221
|
+
return response?.results ?? response?.tools ?? [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function mergeCandidateTools(discoveredTools, inspectedTools) {
|
|
225
|
+
const inspectedById = new Map(
|
|
226
|
+
normalizeToolList(inspectedTools)
|
|
227
|
+
.filter((tool) => tool?.tool_id)
|
|
228
|
+
.map((tool) => [tool.tool_id, tool])
|
|
229
|
+
);
|
|
230
|
+
return discoveredTools.map((tool) => ({
|
|
231
|
+
...tool,
|
|
232
|
+
...(inspectedById.get(tool.tool_id) ?? {}),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function pickInitTool(tools, { toolId, parameters } = {}) {
|
|
237
|
+
const list = normalizeToolList(tools).filter((tool) => tool?.tool_id);
|
|
238
|
+
if (toolId) return list.find((tool) => tool.tool_id === toolId) || { tool_id: toolId, name: toolId };
|
|
239
|
+
if (!parameters) return list[0];
|
|
240
|
+
|
|
241
|
+
const ranked = list
|
|
242
|
+
.map((tool, index) => ({ tool, index, score: scoreParameterMatch(tool, parameters) }))
|
|
243
|
+
.filter((item) => item.score > Number.NEGATIVE_INFINITY)
|
|
244
|
+
.sort((a, b) => b.score - a.score || a.index - b.index);
|
|
245
|
+
|
|
246
|
+
if (ranked.length > 0) return ranked[0].tool;
|
|
247
|
+
|
|
248
|
+
const err = new CliError("INIT_PARAMS_REQUIRED", "Provided --params do not satisfy any discovered capability.");
|
|
249
|
+
err.hint = `Inspect candidates and retry with matching params: ${summarizeRequiredParams(list)}`;
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function scoreParameterMatch(tool, parameters) {
|
|
254
|
+
const schema = Array.isArray(tool?.params) ? tool.params : [];
|
|
255
|
+
if (schema.length === 0) return 0;
|
|
256
|
+
|
|
257
|
+
const provided = new Set(Object.keys(parameters ?? {}));
|
|
258
|
+
const required = schema.filter((param) => param?.required).map((param) => param.name).filter(Boolean);
|
|
259
|
+
const allNames = schema.map((param) => param?.name).filter(Boolean);
|
|
260
|
+
const missingRequired = required.filter((name) => !provided.has(name));
|
|
261
|
+
if (missingRequired.length > 0) return Number.NEGATIVE_INFINITY;
|
|
262
|
+
|
|
263
|
+
const overlap = allNames.filter((name) => provided.has(name)).length;
|
|
264
|
+
if (overlap === 0 && required.length > 0) return Number.NEGATIVE_INFINITY;
|
|
265
|
+
return required.length * 10 + overlap;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function summarizeRequiredParams(tools) {
|
|
269
|
+
return tools.slice(0, 5).map((tool, index) => {
|
|
270
|
+
const required = (Array.isArray(tool?.params) ? tool.params : [])
|
|
271
|
+
.filter((param) => param?.required)
|
|
272
|
+
.map((param) => param.name)
|
|
273
|
+
.filter(Boolean);
|
|
274
|
+
return `${index + 1}. ${tool.tool_id}: ${required.length ? required.join(", ") : "no required params"}`;
|
|
275
|
+
}).join("; ");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function resolveInitMaxResponseSize(flags) {
|
|
279
|
+
if (flags.maxSize === undefined) return DEFAULT_MAX_RESPONSE_SIZE;
|
|
280
|
+
const parsed = parseInt(flags.maxSize, 10);
|
|
281
|
+
if (!Number.isFinite(parsed)) throw new CliError("API_ERROR", "Invalid --max-size: must be an integer");
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function shellSingleQuote(value) {
|
|
286
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function buildNextCommands({ selected, discoveryId, parameters }) {
|
|
290
|
+
const paramsJson = shellSingleQuote(JSON.stringify(parameters));
|
|
291
|
+
const executionPlaceholder = "<execution_id>";
|
|
292
|
+
return {
|
|
293
|
+
retry: `qveris call ${selected.tool_id} --discovery-id ${discoveryId} --params ${paramsJson}`,
|
|
294
|
+
usage: `qveris usage --mode search --execution-id ${executionPlaceholder}`,
|
|
295
|
+
ledger: "qveris ledger --mode summary",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function buildPayload({ steps, discovery, inspectedTool, parameters, callResult, nextCommands, startedAt, dryRun }) {
|
|
300
|
+
return {
|
|
301
|
+
ok: dryRun ? true : Boolean(callResult?.success),
|
|
302
|
+
dry_run: dryRun,
|
|
303
|
+
elapsed_ms: Date.now() - startedAt,
|
|
304
|
+
steps,
|
|
305
|
+
discovery: {
|
|
306
|
+
query: discovery?.query,
|
|
307
|
+
search_id: discovery?.search_id,
|
|
308
|
+
total: discovery?.total ?? discovery?.results?.length,
|
|
309
|
+
},
|
|
310
|
+
selected_tool: {
|
|
311
|
+
tool_id: inspectedTool?.tool_id,
|
|
312
|
+
name: inspectedTool?.name,
|
|
313
|
+
provider_name: inspectedTool?.provider_name,
|
|
314
|
+
},
|
|
315
|
+
parameters,
|
|
316
|
+
call: callResult ? {
|
|
317
|
+
success: callResult.success,
|
|
318
|
+
execution_id: callResult.execution_id,
|
|
319
|
+
elapsed_time_ms: callResult.elapsed_time_ms ?? callResult.execution_time,
|
|
320
|
+
billing: callResult.billing ?? callResult.pre_settlement_bill,
|
|
321
|
+
} : null,
|
|
322
|
+
next_commands: callResult?.execution_id
|
|
323
|
+
? { ...nextCommands, usage: nextCommands.usage.replace("<execution_id>", callResult.execution_id) }
|
|
324
|
+
: nextCommands,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function record(steps, name, status, message, data = {}) {
|
|
329
|
+
steps.push({ name, status, message, ...data });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function updateLast(steps, status, message, data = {}) {
|
|
333
|
+
const last = steps[steps.length - 1];
|
|
334
|
+
Object.assign(last, { status, message }, data);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function printHumanSummary(payload) {
|
|
338
|
+
for (let i = 0; i < payload.steps.length; i++) {
|
|
339
|
+
const step = payload.steps[i];
|
|
340
|
+
const icon = step.status === "ok" ? green("\u2713") : step.status === "skipped" ? yellow("!") : red("\u2718");
|
|
341
|
+
console.log(` ${icon} ${i + 1}/5 ${bold(step.name)} ${dim(step.message)}`);
|
|
342
|
+
if (step.name === "auth") {
|
|
343
|
+
console.log(` ${dim("key")} ${step.key} ${dim("region")} ${step.region} ${dim(step.base_url)}`);
|
|
344
|
+
}
|
|
345
|
+
if (step.name === "discover") {
|
|
346
|
+
console.log(` ${dim("search_id")} ${step.search_id || payload.discovery.search_id}`);
|
|
347
|
+
}
|
|
348
|
+
if (step.name === "inspect" && step.tool_id) {
|
|
349
|
+
console.log(` ${dim("selected")} ${cyan(step.tool_id)}`);
|
|
350
|
+
}
|
|
351
|
+
if (step.name === "call" && step.execution_id) {
|
|
352
|
+
console.log(` ${dim("execution_id")} ${step.execution_id}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log(`\n ${green("First-call path complete.")}`);
|
|
357
|
+
if (payload.dry_run) {
|
|
358
|
+
console.log(` ${yellow("Dry run only:")} remove --dry-run to perform the call.`);
|
|
359
|
+
}
|
|
360
|
+
if (payload.call?.execution_id) {
|
|
361
|
+
console.log(`\n ${bold("Reconcile final billing:")}`);
|
|
362
|
+
console.log(` ${cyan(payload.next_commands.usage)}`);
|
|
363
|
+
console.log(` ${cyan(payload.next_commands.ledger)}`);
|
|
364
|
+
}
|
|
365
|
+
console.log(`\n ${dim("Retry selected capability:")}`);
|
|
366
|
+
console.log(` ${cyan(payload.next_commands.retry)}\n`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function maskKey(key) {
|
|
370
|
+
if (!key) return "none";
|
|
371
|
+
if (key.length <= 10) return "***";
|
|
372
|
+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function looksLikeProviderFailure(message = "") {
|
|
376
|
+
const text = String(message).toLowerCase();
|
|
377
|
+
return text.includes("provider") || text.includes("upstream") || text.includes("third-party");
|
|
378
|
+
}
|