@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qverisai/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "QVeris CLI for agent capability discovery, inspection, calling, and audit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 child = spawn(spec.command, spec.args || [], mcpSpawnOptions(spec.env));
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 = {}, os = platform()) {
447
+ export function mcpSpawnOptions(env = {}, _os = platform()) {
447
448
  return {
448
449
  env: { ...process.env, ...(env || {}) },
449
450
  stdio: ["pipe", "pipe", "pipe"],
450
- shell: os === "win32",
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;
@@ -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 = Array.isArray(t.categories) ? t.categories.join(", ") : "";
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 = Array.isArray(t.categories) ? t.categories.join(", ") : "";
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 "";