@qverisai/cli 0.6.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/package.json +1 -1
- package/src/commands/mcp.mjs +66 -3
- package/src/output/formatter.mjs +23 -2
package/package.json
CHANGED
package/src/commands/mcp.mjs
CHANGED
|
@@ -355,7 +355,8 @@ function stdioSpecFromServer(target, server) {
|
|
|
355
355
|
|
|
356
356
|
function listMcpTools(spec, timeoutMs) {
|
|
357
357
|
return new Promise((resolve, reject) => {
|
|
358
|
-
const
|
|
358
|
+
const childSpec = mcpSpawnCommand(spec);
|
|
359
|
+
const child = spawn(childSpec.command, childSpec.args, mcpSpawnOptions(spec.env));
|
|
359
360
|
let stderr = "";
|
|
360
361
|
let settled = false;
|
|
361
362
|
let timer;
|
|
@@ -443,14 +444,76 @@ export function resolveProbeTimeoutMs(timeout) {
|
|
|
443
444
|
return Math.max(1000, usableSeconds * 1000);
|
|
444
445
|
}
|
|
445
446
|
|
|
446
|
-
export function mcpSpawnOptions(env = {},
|
|
447
|
+
export function mcpSpawnOptions(env = {}, _os = platform()) {
|
|
447
448
|
return {
|
|
448
449
|
env: { ...process.env, ...(env || {}) },
|
|
449
450
|
stdio: ["pipe", "pipe", "pipe"],
|
|
450
|
-
shell:
|
|
451
|
+
shell: false,
|
|
451
452
|
};
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
export function mcpSpawnCommand(spec, os = platform()) {
|
|
456
|
+
const args = spec.args || [];
|
|
457
|
+
if (os !== "win32" || isWindowsExecutable(spec.command)) {
|
|
458
|
+
return { command: spec.command, args };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
463
|
+
args: ["/d", "/s", "/c", wrapWindowsCommandLine([spec.command, ...args])],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isWindowsExecutable(command) {
|
|
468
|
+
return /\.(?:exe|com)$/i.test(command);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isWindowsBatchCommand(command) {
|
|
472
|
+
const name = String(command).split(/[\\/]/).pop() || "";
|
|
473
|
+
return (
|
|
474
|
+
/\.(?:cmd|bat)$/i.test(name) ||
|
|
475
|
+
/^(?:corepack|npm|npx|pnpm|yarn)$/i.test(name)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function wrapWindowsCommandLine(parts) {
|
|
480
|
+
const escapePercent = isWindowsBatchCommand(parts[0]);
|
|
481
|
+
const commandLine = parts
|
|
482
|
+
.map((part) => quoteWindowsCommandArgument(part, escapePercent))
|
|
483
|
+
.join(" ");
|
|
484
|
+
return `"${commandLine}"`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function quoteWindowsCommandArgument(value, escapePercent = false) {
|
|
488
|
+
const raw = String(value);
|
|
489
|
+
if (/[\0\r\n]/.test(raw)) {
|
|
490
|
+
throw new Error("MCP command arguments cannot contain CR, LF, or NUL characters.");
|
|
491
|
+
}
|
|
492
|
+
const text = escapePercent ? raw.replaceAll("%", "%%") : raw;
|
|
493
|
+
let quoted = "\"";
|
|
494
|
+
let backslashes = 0;
|
|
495
|
+
|
|
496
|
+
for (const char of text) {
|
|
497
|
+
if (char === "\\") {
|
|
498
|
+
backslashes += 1;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (char === "\"") {
|
|
502
|
+
quoted += "\\".repeat(backslashes * 2 + 1);
|
|
503
|
+
quoted += char;
|
|
504
|
+
backslashes = 0;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
quoted += "\\".repeat(backslashes);
|
|
508
|
+
backslashes = 0;
|
|
509
|
+
quoted += char;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
quoted += "\\".repeat(backslashes * 2);
|
|
513
|
+
quoted += "\"";
|
|
514
|
+
return quoted;
|
|
515
|
+
}
|
|
516
|
+
|
|
454
517
|
function extractServer(target, config) {
|
|
455
518
|
if (target === "cursor" || target === "claude-desktop") return config?.mcpServers?.qveris;
|
|
456
519
|
if (target === "opencode") return config?.mcp?.qveris;
|
package/src/output/formatter.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export function formatDiscoverResult(result) {
|
|
|
23
23
|
const toolId = t.tool_id ?? "";
|
|
24
24
|
const desc = stringifyDesc(t.description);
|
|
25
25
|
const provider = t.provider_name ?? "";
|
|
26
|
-
const categories =
|
|
26
|
+
const categories = formatCategories(t.categories);
|
|
27
27
|
const region = t.region ?? "global";
|
|
28
28
|
const stats = t.stats ?? {};
|
|
29
29
|
|
|
@@ -92,7 +92,7 @@ export function formatInspectResult(tools) {
|
|
|
92
92
|
const desc = stringifyDesc(t.description);
|
|
93
93
|
const provider = t.provider_name ?? "";
|
|
94
94
|
const providerDesc = stringifyDesc(t.provider_description);
|
|
95
|
-
const categories =
|
|
95
|
+
const categories = formatCategories(t.categories);
|
|
96
96
|
const region = t.region ?? "global";
|
|
97
97
|
const docsUrl = t.docs_url ?? "";
|
|
98
98
|
const stats = t.stats ?? {};
|
|
@@ -290,6 +290,27 @@ function formatSchema(schema, indent = "", depth = 0) {
|
|
|
290
290
|
return lines.join("\n");
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/** Categories may be strings or objects like {slug, name, description}. */
|
|
294
|
+
function formatCategories(categories) {
|
|
295
|
+
if (!Array.isArray(categories)) return "";
|
|
296
|
+
const seen = new Set();
|
|
297
|
+
const names = [];
|
|
298
|
+
for (const c of categories) {
|
|
299
|
+
let label = "";
|
|
300
|
+
if (typeof c === "string") label = c;
|
|
301
|
+
else if (c && typeof c === "object") {
|
|
302
|
+
label = stringifyDesc(c.name) || (typeof c.slug === "string" ? c.slug : "");
|
|
303
|
+
}
|
|
304
|
+
label = label.trim();
|
|
305
|
+
if (!label) continue;
|
|
306
|
+
const key = label.toLowerCase();
|
|
307
|
+
if (seen.has(key)) continue;
|
|
308
|
+
seen.add(key);
|
|
309
|
+
names.push(label);
|
|
310
|
+
}
|
|
311
|
+
return names.join(", ");
|
|
312
|
+
}
|
|
313
|
+
|
|
293
314
|
/** Handle description that may be a string, i18n object {en: "...", zh: "..."}, or other. */
|
|
294
315
|
function stringifyDesc(desc) {
|
|
295
316
|
if (!desc) return "";
|