@qverisai/cli 0.4.0 → 0.6.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 +210 -42
- package/package.json +6 -3
- package/src/client/api.mjs +63 -1
- package/src/client/auth.mjs +8 -4
- package/src/commands/completions.mjs +1 -1
- package/src/commands/credits.mjs +18 -4
- package/src/commands/discover.mjs +2 -2
- package/src/commands/doctor.mjs +1 -1
- package/src/commands/init.mjs +378 -0
- package/src/commands/interactive.mjs +4 -5
- package/src/commands/ledger.mjs +186 -0
- package/src/commands/mcp.mjs +623 -0
- package/src/commands/usage.mjs +190 -0
- package/src/compat/aliases.mjs +4 -0
- package/src/errors/codes.mjs +18 -3
- package/src/main.mjs +101 -0
- package/src/output/audit.mjs +536 -0
- package/src/output/formatter.mjs +59 -6
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { isPlaceholderApiKey, resolveApiKey } from "../client/auth.mjs";
|
|
7
|
+
import { resolveBaseUrl, detectRegionFromKey } from "../config/region.mjs";
|
|
8
|
+
import { CliError } from "../errors/handler.mjs";
|
|
9
|
+
import { bold, cyan, dim, green, red, yellow } from "../output/colors.mjs";
|
|
10
|
+
import { outputJson } from "../output/json.mjs";
|
|
11
|
+
|
|
12
|
+
const TARGETS = new Set(["cursor", "claude-desktop", "claude-code", "opencode", "openclaw", "generic"]);
|
|
13
|
+
const EXPECTED_TOOLS = ["discover", "inspect", "call", "usage_history", "credits_ledger"];
|
|
14
|
+
const API_KEY_PLACEHOLDER = "YOUR_QVERIS_API_KEY";
|
|
15
|
+
|
|
16
|
+
export async function runMcp(subcommand, args, flags) {
|
|
17
|
+
switch (subcommand) {
|
|
18
|
+
case "configure":
|
|
19
|
+
return configure(args, flags);
|
|
20
|
+
case "validate":
|
|
21
|
+
return validate(args, flags);
|
|
22
|
+
default:
|
|
23
|
+
console.error(` Unknown mcp subcommand: ${subcommand}`);
|
|
24
|
+
console.error(" Usage: qveris mcp <configure|validate> [target]");
|
|
25
|
+
process.exitCode = 2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function configure(args, flags) {
|
|
30
|
+
const target = resolveTarget(args, flags);
|
|
31
|
+
const includeKey = Boolean(flags.includeKey);
|
|
32
|
+
const apiKey = includeKey ? resolveApiKey(flags.apiKey || flags.token) : API_KEY_PLACEHOLDER;
|
|
33
|
+
const { baseUrl, region } = resolveBaseUrl({
|
|
34
|
+
baseUrlFlag: flags.baseUrl,
|
|
35
|
+
apiKey: includeKey ? apiKey : undefined,
|
|
36
|
+
});
|
|
37
|
+
const outputPath = flags.output || defaultConfigPath(target);
|
|
38
|
+
const fragment = buildTargetFragment(target, { apiKey, baseUrl, includeKey });
|
|
39
|
+
const printable = buildPrintablePayload(target, {
|
|
40
|
+
fragment,
|
|
41
|
+
outputPath,
|
|
42
|
+
includeKey,
|
|
43
|
+
baseUrl,
|
|
44
|
+
region: includeKey ? region : "auto",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (flags.write) {
|
|
48
|
+
if (target === "claude-code") {
|
|
49
|
+
throw new CliError("API_ERROR", "claude-code target produces shell commands; use --print and run the generated command.");
|
|
50
|
+
}
|
|
51
|
+
const written = writeTargetConfig(target, outputPath, fragment);
|
|
52
|
+
const validation = validateConfigObject(target, written.config);
|
|
53
|
+
const payload = { ...printable, wrote: true, path: written.path, validation };
|
|
54
|
+
const outputPayload = redactWrittenPayload(payload);
|
|
55
|
+
if (flags.json) outputJson(outputPayload);
|
|
56
|
+
else printConfigureResult(outputPayload);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const validation = target === "claude-code"
|
|
61
|
+
? validateClaudeCodeCommand(fragment)
|
|
62
|
+
: validateConfigObject(target, fragmentToConfig(target, fragment));
|
|
63
|
+
const payload = { ...printable, wrote: false, validation };
|
|
64
|
+
if (flags.json) outputJson(payload);
|
|
65
|
+
else printConfigureResult(payload);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function validate(args, flags) {
|
|
69
|
+
const target = resolveTarget(args, flags);
|
|
70
|
+
if (target === "claude-code") {
|
|
71
|
+
const payload = {
|
|
72
|
+
target,
|
|
73
|
+
ok: true,
|
|
74
|
+
checks: [
|
|
75
|
+
{ name: "manual_command", ok: true, message: "Claude Code uses `claude mcp add`; run `qveris mcp configure --target claude-code --print`." },
|
|
76
|
+
],
|
|
77
|
+
expected_tools: EXPECTED_TOOLS,
|
|
78
|
+
};
|
|
79
|
+
if (flags.json) outputJson(payload);
|
|
80
|
+
else printValidation(payload);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const configPath = flags.output || defaultConfigPath(target);
|
|
85
|
+
const config = readJsonFile(configPath);
|
|
86
|
+
const payload = { target, path: configPath, ...validateConfigObject(target, config) };
|
|
87
|
+
if (flags.probe) {
|
|
88
|
+
const probe = await probeVisibleTools(target, config, flags);
|
|
89
|
+
payload.probe = probe;
|
|
90
|
+
payload.checks = [...payload.checks, ...probe.checks];
|
|
91
|
+
payload.ok = payload.ok && probe.ok;
|
|
92
|
+
}
|
|
93
|
+
if (flags.json) outputJson(payload);
|
|
94
|
+
else printValidation(payload);
|
|
95
|
+
if (!payload.ok) process.exitCode = 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveTarget(args, flags) {
|
|
99
|
+
const target = (flags.target || args[0] || "cursor").toLowerCase();
|
|
100
|
+
if (!TARGETS.has(target)) {
|
|
101
|
+
throw new CliError("API_ERROR", `Unknown MCP target "${target}". Expected one of: ${Array.from(TARGETS).join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildTargetFragment(target, { apiKey, baseUrl, includeKey }) {
|
|
107
|
+
const env = { QVERIS_API_KEY: apiKey };
|
|
108
|
+
if (baseUrl && (includeKey || baseUrl !== "https://qveris.ai/api/v1")) env.QVERIS_BASE_URL = baseUrl;
|
|
109
|
+
const stdioServer = {
|
|
110
|
+
command: "npx",
|
|
111
|
+
args: ["-y", "@qverisai/mcp"],
|
|
112
|
+
env,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (target === "cursor" || target === "claude-desktop") {
|
|
116
|
+
return { mcpServers: { qveris: stdioServer } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (target === "opencode") {
|
|
120
|
+
return {
|
|
121
|
+
mcp: {
|
|
122
|
+
qveris: {
|
|
123
|
+
type: "local",
|
|
124
|
+
command: ["npx", "-y", "@qverisai/mcp"],
|
|
125
|
+
environment: env,
|
|
126
|
+
enabled: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
tools: { "qveris*": true },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (target === "openclaw") {
|
|
134
|
+
const region = includeKey ? detectRegionFromKey(apiKey) : "global";
|
|
135
|
+
return {
|
|
136
|
+
plugins: {
|
|
137
|
+
allow: ["qveris"],
|
|
138
|
+
entries: {
|
|
139
|
+
qveris: {
|
|
140
|
+
enabled: true,
|
|
141
|
+
config: {
|
|
142
|
+
apiKey,
|
|
143
|
+
region,
|
|
144
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
tools: { alsoAllow: ["qveris"] },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (target === "claude-code") {
|
|
154
|
+
const envArgs = buildClaudeCodeEnvArgs(env, platform());
|
|
155
|
+
const windowsEnvArgs = buildClaudeCodeEnvArgs(env, "win32");
|
|
156
|
+
return {
|
|
157
|
+
command: `claude mcp add qveris --transport stdio --scope user ${envArgs.join(" ")} -- npx -y @qverisai/mcp`,
|
|
158
|
+
windows_command: `claude mcp add qveris --transport stdio --scope user ${windowsEnvArgs.join(" ")} -- cmd /c npx -y @qverisai/mcp`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return stdioServer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildPrintablePayload(target, { fragment, outputPath, includeKey, baseUrl, region }) {
|
|
166
|
+
return {
|
|
167
|
+
target,
|
|
168
|
+
mode: "stdio",
|
|
169
|
+
path: outputPath,
|
|
170
|
+
safe_to_share: !includeKey,
|
|
171
|
+
includes_real_api_key: includeKey,
|
|
172
|
+
expected_tools: EXPECTED_TOOLS,
|
|
173
|
+
base_url: baseUrl,
|
|
174
|
+
region,
|
|
175
|
+
config: fragment,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function writeTargetConfig(target, path, fragment) {
|
|
180
|
+
const existing = existsSync(path) ? readJsonFile(path) : {};
|
|
181
|
+
const merged = mergeConfig(target, existing, fragment);
|
|
182
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
183
|
+
writeFileSync(path, JSON.stringify(merged, null, 2) + "\n", { mode: 0o600 });
|
|
184
|
+
if (platform() !== "win32") chmodSync(path, 0o600);
|
|
185
|
+
return { path, config: merged };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function mergeConfig(target, existing, fragment) {
|
|
189
|
+
if (target === "generic") return fragment;
|
|
190
|
+
if (target === "cursor" || target === "claude-desktop") {
|
|
191
|
+
return {
|
|
192
|
+
...existing,
|
|
193
|
+
mcpServers: {
|
|
194
|
+
...(existing.mcpServers || {}),
|
|
195
|
+
qveris: fragment.mcpServers.qveris,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (target === "opencode") {
|
|
200
|
+
return {
|
|
201
|
+
...existing,
|
|
202
|
+
mcp: {
|
|
203
|
+
...(existing.mcp || {}),
|
|
204
|
+
qveris: fragment.mcp.qveris,
|
|
205
|
+
},
|
|
206
|
+
tools: {
|
|
207
|
+
...(existing.tools || {}),
|
|
208
|
+
...fragment.tools,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (target === "openclaw") {
|
|
213
|
+
return {
|
|
214
|
+
...existing,
|
|
215
|
+
plugins: {
|
|
216
|
+
...(existing.plugins || {}),
|
|
217
|
+
allow: unique([...(existing.plugins?.allow || []), "qveris"]),
|
|
218
|
+
entries: {
|
|
219
|
+
...(existing.plugins?.entries || {}),
|
|
220
|
+
qveris: fragment.plugins.entries.qveris,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
tools: {
|
|
224
|
+
...(existing.tools || {}),
|
|
225
|
+
alsoAllow: unique([...(existing.tools?.alsoAllow || []), "qveris"]),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return fragment;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fragmentToConfig(target, fragment) {
|
|
233
|
+
if (target === "generic" || target === "claude-code") return fragment;
|
|
234
|
+
return mergeConfig(target, {}, fragment);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validateConfigObject(target, config) {
|
|
238
|
+
if (target === "openclaw") return validateOpenClawConfig(config);
|
|
239
|
+
|
|
240
|
+
const checks = [];
|
|
241
|
+
const server = extractServer(target, config);
|
|
242
|
+
checks.push(check("config_present", Boolean(config && typeof config === "object"), "Config JSON is readable"));
|
|
243
|
+
checks.push(check("qveris_entry", Boolean(server), "QVeris MCP entry exists"));
|
|
244
|
+
checks.push(check("uses_qveris_mcp", serverUsesMcpPackage(server), "Config runs @qverisai/mcp"));
|
|
245
|
+
checks.push(check("api_key_env", hasUsableApiKey(server, target), "QVERIS_API_KEY is configured and is not a placeholder"));
|
|
246
|
+
|
|
247
|
+
if (target === "opencode") {
|
|
248
|
+
checks.push(check("tools_enabled", config?.tools?.["qveris*"] === true, "OpenCode qveris tools are enabled"));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ok = checks.every((item) => item.ok);
|
|
252
|
+
return { ok, checks, expected_tools: EXPECTED_TOOLS };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateOpenClawConfig(config) {
|
|
256
|
+
const entry = config?.plugins?.entries?.qveris;
|
|
257
|
+
const checks = [
|
|
258
|
+
check("config_present", Boolean(config && typeof config === "object"), "Config JSON is readable"),
|
|
259
|
+
check("plugin_allowed", Array.isArray(config?.plugins?.allow) && config.plugins.allow.includes("qveris"), "OpenClaw allows the qveris plugin"),
|
|
260
|
+
check("qveris_entry", Boolean(entry), "QVeris OpenClaw plugin entry exists"),
|
|
261
|
+
check("plugin_enabled", entry?.enabled === true, "QVeris OpenClaw plugin is enabled"),
|
|
262
|
+
check("api_key_config", hasUsableApiKey(entry, "openclaw"), "OpenClaw qveris apiKey is configured and is not a placeholder"),
|
|
263
|
+
check("tools_enabled", Array.isArray(config?.tools?.alsoAllow) && config.tools.alsoAllow.includes("qveris"), "OpenClaw qveris tools are enabled"),
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
return { ok: checks.every((item) => item.ok), checks, expected_tools: EXPECTED_TOOLS };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function validateClaudeCodeCommand(fragment) {
|
|
270
|
+
const commandPresent = Boolean(fragment?.command);
|
|
271
|
+
const usesPackage = Boolean(fragment?.command?.includes("@qverisai/mcp"));
|
|
272
|
+
const hasUsableKey = commandHasUsableApiKey(fragment?.command);
|
|
273
|
+
const checks = [
|
|
274
|
+
check("command_present", commandPresent, "Claude Code command was generated"),
|
|
275
|
+
check("uses_qveris_mcp", usesPackage, "Command runs @qverisai/mcp"),
|
|
276
|
+
check("api_key_env", hasUsableKey, "Command includes a usable QVERIS_API_KEY value"),
|
|
277
|
+
];
|
|
278
|
+
return {
|
|
279
|
+
ok: checks.every((item) => item.ok),
|
|
280
|
+
checks,
|
|
281
|
+
expected_tools: EXPECTED_TOOLS,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function probeVisibleTools(target, config, flags) {
|
|
286
|
+
if (target === "openclaw") {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
checks: [
|
|
290
|
+
check("tools_visible", false, "Live stdio probe is not available for OpenClaw plugin configs; use the OpenClaw plugin manager to confirm tool visibility."),
|
|
291
|
+
],
|
|
292
|
+
tool_names: [],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const server = extractServer(target, config);
|
|
297
|
+
const spec = stdioSpecFromServer(target, server);
|
|
298
|
+
if (!spec) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
checks: [
|
|
302
|
+
check("tools_visible", false, "No stdio server command is available to probe"),
|
|
303
|
+
],
|
|
304
|
+
tool_names: [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const timeoutMs = resolveProbeTimeoutMs(flags.timeout);
|
|
310
|
+
const toolNames = await listMcpTools(spec, timeoutMs);
|
|
311
|
+
const required = ["discover", "inspect", "call"];
|
|
312
|
+
const missing = required.filter((name) => !toolNames.includes(name));
|
|
313
|
+
return {
|
|
314
|
+
ok: missing.length === 0,
|
|
315
|
+
checks: [
|
|
316
|
+
check(
|
|
317
|
+
"tools_visible",
|
|
318
|
+
missing.length === 0,
|
|
319
|
+
missing.length === 0
|
|
320
|
+
? "Live MCP probe can see discover, inspect, and call"
|
|
321
|
+
: `Live MCP probe is missing tools: ${missing.join(", ")}`
|
|
322
|
+
),
|
|
323
|
+
],
|
|
324
|
+
tool_names: toolNames,
|
|
325
|
+
};
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
checks: [
|
|
330
|
+
check("tools_visible", false, `Live MCP probe failed: ${err instanceof Error ? err.message : String(err)}`),
|
|
331
|
+
],
|
|
332
|
+
tool_names: [],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function stdioSpecFromServer(target, server) {
|
|
338
|
+
if (!server) return null;
|
|
339
|
+
if (target === "opencode") {
|
|
340
|
+
if (Array.isArray(server.command)) {
|
|
341
|
+
const [command, ...args] = server.command;
|
|
342
|
+
return { command, args, env: server.environment || {} };
|
|
343
|
+
}
|
|
344
|
+
if (typeof server.command === "string") {
|
|
345
|
+
return { command: server.command, args: server.args || [], env: server.environment || {} };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (typeof server.command === "string") {
|
|
351
|
+
return { command: server.command, args: server.args || [], env: server.env || {} };
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function listMcpTools(spec, timeoutMs) {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const child = spawn(spec.command, spec.args || [], mcpSpawnOptions(spec.env));
|
|
359
|
+
let stderr = "";
|
|
360
|
+
let settled = false;
|
|
361
|
+
let timer;
|
|
362
|
+
let rl;
|
|
363
|
+
|
|
364
|
+
const finish = (fn, value) => {
|
|
365
|
+
if (settled) return;
|
|
366
|
+
settled = true;
|
|
367
|
+
clearTimeout(timer);
|
|
368
|
+
if (rl) rl.close();
|
|
369
|
+
child.kill();
|
|
370
|
+
fn(value);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
timer = setTimeout(() => {
|
|
374
|
+
finish(reject, new Error(`timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
375
|
+
}, timeoutMs);
|
|
376
|
+
|
|
377
|
+
const writeJson = (message) => {
|
|
378
|
+
child.stdin.write(JSON.stringify(message) + "\n");
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const handleMessage = (message) => {
|
|
382
|
+
if (message.id === 1) {
|
|
383
|
+
if (message.error) {
|
|
384
|
+
finish(reject, new Error(message.error.message || "initialize failed"));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
writeJson({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
388
|
+
writeJson({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (message.id === 2) {
|
|
392
|
+
if (message.error) {
|
|
393
|
+
finish(reject, new Error(message.error.message || "tools/list failed"));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const toolNames = Array.isArray(message.result?.tools)
|
|
397
|
+
? message.result.tools.map((tool) => tool.name).filter(Boolean)
|
|
398
|
+
: [];
|
|
399
|
+
finish(resolve, toolNames);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
rl = createInterface({ input: child.stdout, terminal: false });
|
|
404
|
+
rl.on("line", (line) => {
|
|
405
|
+
if (!line.trim()) return;
|
|
406
|
+
try {
|
|
407
|
+
handleMessage(JSON.parse(line));
|
|
408
|
+
} catch {
|
|
409
|
+
finish(reject, new Error(`invalid MCP JSON response: ${line.slice(0, 120)}`));
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
child.stderr.setEncoding("utf8");
|
|
414
|
+
child.stderr.on("data", (chunk) => {
|
|
415
|
+
stderr += chunk;
|
|
416
|
+
});
|
|
417
|
+
child.stdin.on("error", (err) => finish(reject, err));
|
|
418
|
+
|
|
419
|
+
child.on("error", (err) => finish(reject, err));
|
|
420
|
+
child.on("exit", (code) => {
|
|
421
|
+
if (!settled) {
|
|
422
|
+
const detail = stderr.trim() ? `: ${stderr.trim().split("\n").slice(-2).join(" ")}` : "";
|
|
423
|
+
finish(reject, new Error(`server exited before tools/list (code ${code})${detail}`));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
writeJson({
|
|
428
|
+
jsonrpc: "2.0",
|
|
429
|
+
id: 1,
|
|
430
|
+
method: "initialize",
|
|
431
|
+
params: {
|
|
432
|
+
protocolVersion: "2024-11-05",
|
|
433
|
+
capabilities: {},
|
|
434
|
+
clientInfo: { name: "qveris-cli", version: "mcp-validate" },
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function resolveProbeTimeoutMs(timeout) {
|
|
441
|
+
const seconds = Number.parseFloat(timeout ?? "15");
|
|
442
|
+
const usableSeconds = Number.isFinite(seconds) && seconds > 0 ? seconds : 15;
|
|
443
|
+
return Math.max(1000, usableSeconds * 1000);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function mcpSpawnOptions(env = {}, os = platform()) {
|
|
447
|
+
return {
|
|
448
|
+
env: { ...process.env, ...(env || {}) },
|
|
449
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
450
|
+
shell: os === "win32",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function extractServer(target, config) {
|
|
455
|
+
if (target === "cursor" || target === "claude-desktop") return config?.mcpServers?.qveris;
|
|
456
|
+
if (target === "opencode") return config?.mcp?.qveris;
|
|
457
|
+
if (target === "openclaw") return config?.plugins?.entries?.qveris;
|
|
458
|
+
if (target === "generic") return config;
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function serverUsesMcpPackage(server) {
|
|
463
|
+
if (!server) return false;
|
|
464
|
+
const serialized = JSON.stringify(server);
|
|
465
|
+
return serialized.includes("@qverisai/mcp");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function hasUsableApiKey(server, target) {
|
|
469
|
+
if (!server) return false;
|
|
470
|
+
const value = target === "openclaw"
|
|
471
|
+
? server.config?.apiKey
|
|
472
|
+
: server.env?.QVERIS_API_KEY || server.environment?.QVERIS_API_KEY;
|
|
473
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
474
|
+
return !isPlaceholderApiKey(value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function check(name, ok, message) {
|
|
478
|
+
return { name, ok, message };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function readJsonFile(path) {
|
|
482
|
+
try {
|
|
483
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
484
|
+
} catch (err) {
|
|
485
|
+
const detail = err.code === "ENOENT" ? `File not found: ${path}` : `Invalid JSON in ${path}: ${err.message}`;
|
|
486
|
+
throw new CliError("API_ERROR", detail);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function defaultConfigPath(target) {
|
|
491
|
+
const home = homedir();
|
|
492
|
+
const os = platform();
|
|
493
|
+
if (target === "cursor") return join(home, ".cursor", "mcp.json");
|
|
494
|
+
if (target === "opencode") return join(home, ".config", "opencode", "opencode.json");
|
|
495
|
+
if (target === "openclaw") return join(home, ".openclaw", "openclaw.json");
|
|
496
|
+
if (target === "generic") return join(process.cwd(), "qveris-mcp.json");
|
|
497
|
+
if (target === "claude-desktop") {
|
|
498
|
+
if (os === "darwin") return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
499
|
+
if (os === "win32") return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
500
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
501
|
+
}
|
|
502
|
+
return "";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function printConfigureResult(payload) {
|
|
506
|
+
console.log(`\n ${bold("QVeris MCP configure")} ${dim(payload.target)}\n`);
|
|
507
|
+
if (payload.wrote) {
|
|
508
|
+
console.log(` ${green("✓")} Wrote config: ${cyan(payload.path)}`);
|
|
509
|
+
} else {
|
|
510
|
+
console.log(` ${yellow("!")} Dry run / print mode. Nothing was written.`);
|
|
511
|
+
if (payload.path) console.log(` ${dim("Default path:")} ${payload.path}`);
|
|
512
|
+
}
|
|
513
|
+
console.log(` ${dim("Includes real API key:")} ${payload.includes_real_api_key ? red("yes") : green("no")}`);
|
|
514
|
+
console.log(` ${dim("Expected tools:")} ${payload.expected_tools.join(", ")}`);
|
|
515
|
+
console.log("\n" + JSON.stringify(payload.config, null, 2));
|
|
516
|
+
if (!payload.wrote && payload.target !== "claude-code") {
|
|
517
|
+
console.log(`\n ${dim("Write placeholder config with:")} qveris mcp configure --target ${payload.target} --write`);
|
|
518
|
+
console.log(` ${dim("Write working config with current key:")} qveris mcp configure --target ${payload.target} --write --include-key`);
|
|
519
|
+
}
|
|
520
|
+
if (payload.validation) printValidation(payload.validation);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function redactWrittenPayload(payload) {
|
|
524
|
+
if (!payload.wrote || !payload.includes_real_api_key) return payload;
|
|
525
|
+
return { ...payload, config: redactConfigSecrets(payload.config) };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function redactConfigSecrets(value) {
|
|
529
|
+
if (Array.isArray(value)) return value.map((item) => redactConfigSecrets(item));
|
|
530
|
+
if (!value || typeof value !== "object") return value;
|
|
531
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
|
|
532
|
+
key,
|
|
533
|
+
isSecretConfigKey(key) ? "********" : redactConfigSecrets(nested),
|
|
534
|
+
]));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function isSecretConfigKey(key) {
|
|
538
|
+
return key === "QVERIS_API_KEY" || key === "apiKey";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function printValidation(payload) {
|
|
542
|
+
console.log(`\n ${bold("MCP validation")}${payload.target ? ` ${dim(payload.target)}` : ""}\n`);
|
|
543
|
+
for (const item of payload.checks || []) {
|
|
544
|
+
console.log(` ${item.ok ? green("✓") : red("✘")} ${item.message}`);
|
|
545
|
+
}
|
|
546
|
+
console.log(` ${dim("Expected canonical tools:")} ${(payload.expected_tools || EXPECTED_TOOLS).join(", ")}`);
|
|
547
|
+
console.log(payload.ok ? `\n ${green("MCP config looks valid.")}\n` : `\n ${red("MCP config needs attention.")}\n`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function buildClaudeCodeEnvArgs(env, os) {
|
|
551
|
+
const envArgs = [`--env QVERIS_API_KEY=${shellQuoteForPlatform(env.QVERIS_API_KEY, os)}`];
|
|
552
|
+
if (env.QVERIS_BASE_URL) {
|
|
553
|
+
envArgs.push(`--env QVERIS_BASE_URL=${shellQuoteForPlatform(env.QVERIS_BASE_URL, os)}`);
|
|
554
|
+
}
|
|
555
|
+
return envArgs;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function shellQuoteForPlatform(value, os = platform()) {
|
|
559
|
+
if (os === "win32") return `"${String(value).replaceAll("\"", "\"\"")}"`;
|
|
560
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function commandHasUsableApiKey(command) {
|
|
564
|
+
const value = extractEnvAssignmentValue(command, "QVERIS_API_KEY");
|
|
565
|
+
return typeof value === "string" && value.trim() !== "" && !isPlaceholderApiKey(value);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function extractEnvAssignmentValue(command, name) {
|
|
569
|
+
if (typeof command !== "string") return null;
|
|
570
|
+
const marker = `${name}=`;
|
|
571
|
+
let start = command.indexOf(marker);
|
|
572
|
+
while (start !== -1 && start > 0 && !/\s/.test(command[start - 1])) {
|
|
573
|
+
start = command.indexOf(marker, start + marker.length);
|
|
574
|
+
}
|
|
575
|
+
if (start === -1) return null;
|
|
576
|
+
const valueStart = start + marker.length;
|
|
577
|
+
return readShellToken(command, valueStart);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function readShellToken(command, start) {
|
|
581
|
+
let value = "";
|
|
582
|
+
let quote = null;
|
|
583
|
+
for (let i = start; i < command.length; i++) {
|
|
584
|
+
const char = command[i];
|
|
585
|
+
if (!quote && /\s/.test(char)) break;
|
|
586
|
+
|
|
587
|
+
if (quote === "'") {
|
|
588
|
+
if (char === "'") quote = null;
|
|
589
|
+
else value += char;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (quote === "\"") {
|
|
594
|
+
if (char === "\"") {
|
|
595
|
+
if (command[i + 1] === "\"") {
|
|
596
|
+
value += "\"";
|
|
597
|
+
i += 1;
|
|
598
|
+
} else {
|
|
599
|
+
quote = null;
|
|
600
|
+
}
|
|
601
|
+
} else if (char === "\\" && i + 1 < command.length) {
|
|
602
|
+
value += command[i + 1];
|
|
603
|
+
i += 1;
|
|
604
|
+
} else {
|
|
605
|
+
value += char;
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (char === "'" || char === "\"") quote = char;
|
|
611
|
+
else if (char === "\\" && i + 1 < command.length) {
|
|
612
|
+
value += command[i + 1];
|
|
613
|
+
i += 1;
|
|
614
|
+
} else {
|
|
615
|
+
value += char;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return value;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function unique(values) {
|
|
622
|
+
return Array.from(new Set(values));
|
|
623
|
+
}
|