@zvec/zvec-grep 0.1.9 → 0.2.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 +76 -18
- package/README_CN.md +67 -13
- package/dist/authorization/prompt.d.ts +1 -0
- package/dist/authorization/prompt.js +1 -0
- package/dist/authorization/prompt.js.map +1 -1
- package/dist/cli/commands.js +31 -12
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/help.js +25 -9
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/install.js +724 -26
- package/dist/cli/install.js.map +1 -1
- package/dist/daemon/backend.js +13 -8
- package/dist/daemon/backend.js.map +1 -1
- package/dist/daemon/root-runtime.d.ts +1 -0
- package/dist/daemon/root-runtime.js +4 -0
- package/dist/daemon/root-runtime.js.map +1 -1
- package/dist/mcp/input-normalization.js +2 -2
- package/dist/mcp/input-normalization.js.map +1 -1
- package/dist/mcp/schemas.d.ts +9 -17
- package/dist/mcp/schemas.js +6 -2
- package/dist/mcp/schemas.js.map +1 -1
- package/dist/mcp/stdio-bridge.d.ts +3 -0
- package/dist/mcp/stdio-bridge.js +46 -2
- package/dist/mcp/stdio-bridge.js.map +1 -1
- package/dist/mcp/tools.js +2 -2
- package/dist/mcp/tools.js.map +1 -1
- package/dist/prompts/zvec-grep-guidance.d.ts +1 -1
- package/dist/prompts/zvec-grep-guidance.js +1 -1
- package/dist/prompts/zvec-grep-guidance.js.map +1 -1
- package/docs/01-agents.md +87 -10
- package/docs/02-cli.md +11 -4
- package/docs/04-pipeline.md +2 -2
- package/docs/06-server.md +4 -3
- package/docs/07-embedding.md +21 -0
- package/docs/README.md +1 -1
- package/package.json +3 -2
package/dist/cli/install.js
CHANGED
|
@@ -1,46 +1,82 @@
|
|
|
1
|
+
import { applyEdits, createScanner, findNodeAtLocation, modify, parse as parseJsonWithComments, parseTree, } from "jsonc-parser";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { constants as fileSystemConstants } from "node:fs";
|
|
3
4
|
import { access, chmod, lstat, mkdir, open, readFile, readlink, rename, stat, unlink, } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
7
|
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { parseEnv } from "node:util";
|
|
9
|
+
import { REMOTE_EMBEDDING_ELICITATION_UNSUPPORTED_MESSAGE } from "../authorization/prompt.js";
|
|
7
10
|
import { resolveServerUrl } from "../client/mode-router.js";
|
|
8
11
|
import { formatPromptRules, ZVEC_GREP_WORKSPACE_EVIDENCE_RULES, } from "../prompts/zvec-grep-guidance.js";
|
|
9
12
|
const AGENT_INSTALLERS = [
|
|
10
13
|
{
|
|
11
14
|
id: "claude",
|
|
15
|
+
aliases: ["cc", "claude-code"],
|
|
12
16
|
label: "Claude Code",
|
|
13
|
-
|
|
17
|
+
executables: ["claude"],
|
|
14
18
|
install: installClaudeIntegration,
|
|
15
19
|
uninstall: uninstallClaudeIntegration,
|
|
16
20
|
},
|
|
17
21
|
{
|
|
18
22
|
id: "codex",
|
|
19
23
|
label: "Codex",
|
|
20
|
-
|
|
24
|
+
executables: ["codex"],
|
|
21
25
|
install: installCodexIntegration,
|
|
22
26
|
uninstall: uninstallCodexIntegration,
|
|
23
27
|
},
|
|
24
28
|
{
|
|
25
29
|
id: "opencode",
|
|
26
30
|
label: "OpenCode",
|
|
27
|
-
|
|
31
|
+
executables: ["opencode"],
|
|
28
32
|
install: installOpenCodeIntegration,
|
|
29
33
|
uninstall: uninstallOpenCodeIntegration,
|
|
30
34
|
},
|
|
31
35
|
{
|
|
32
36
|
id: "cursor",
|
|
33
37
|
label: "Cursor",
|
|
34
|
-
|
|
38
|
+
executables: ["cursor"],
|
|
35
39
|
install: installCursorIntegration,
|
|
36
40
|
uninstall: uninstallCursorIntegration,
|
|
37
41
|
},
|
|
42
|
+
{
|
|
43
|
+
id: "qwen",
|
|
44
|
+
aliases: ["qwen-code", "qwencode"],
|
|
45
|
+
label: "Qwen Code",
|
|
46
|
+
executables: ["qwen"],
|
|
47
|
+
install: installQwenIntegration,
|
|
48
|
+
uninstall: uninstallQwenIntegration,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "qoder",
|
|
52
|
+
label: "Qoder",
|
|
53
|
+
executables: ["qoder", "qodercli", "qoder-ide"],
|
|
54
|
+
detect: qoderIdeIsAvailable,
|
|
55
|
+
install: installQoderIntegration,
|
|
56
|
+
uninstall: uninstallQoderIntegration,
|
|
57
|
+
},
|
|
38
58
|
];
|
|
39
59
|
const ZVEC_GREP_CONFIG_START = "# ZVEC_GREP_START";
|
|
40
60
|
const ZVEC_GREP_CONFIG_END = "# ZVEC_GREP_END";
|
|
41
61
|
const ZVEC_GREP_AGENTS_START = "<!-- ZVEC_GREP_START -->";
|
|
42
62
|
const ZVEC_GREP_AGENTS_END = "<!-- ZVEC_GREP_END -->";
|
|
43
63
|
const CLAUDE_MCP_PERMISSION = "mcp__zvec_grep__*";
|
|
64
|
+
const NAMESPACED_SEARCH_TOOL = "mcp__zvec_grep__zvec_grep_search";
|
|
65
|
+
const NAMESPACED_RG_TOOL = "mcp__zvec_grep__zvec_grep_rg";
|
|
66
|
+
const QODER_MCP_PERMISSION_RULES = [
|
|
67
|
+
{
|
|
68
|
+
permission: NAMESPACED_SEARCH_TOOL,
|
|
69
|
+
tool: "zvec_grep_search",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
permission: NAMESPACED_RG_TOOL,
|
|
73
|
+
tool: "zvec_grep_rg",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
const QODER_MCP_PERMISSIONS = QODER_MCP_PERMISSION_RULES.map(({ permission }) => permission);
|
|
77
|
+
const QODER_CLI_MCP_DESCRIPTION = "Managed by zg install";
|
|
78
|
+
const QODER_PERMISSION_OWNERSHIP_PREFIX = `${QODER_CLI_MCP_DESCRIPTION}; managed permissions=`;
|
|
79
|
+
const QODER_IDE_MCP_DESCRIPTION = "Managed by zg install";
|
|
44
80
|
const DEFAULT_MCP_TOOL_TIMEOUT_SECONDS = 600;
|
|
45
81
|
export async function runInstall(parsed) {
|
|
46
82
|
printInstallHeader();
|
|
@@ -252,6 +288,86 @@ async function uninstallCursorIntegration() {
|
|
|
252
288
|
await uninstallJsonMcpServer(configPath, "mcpServers");
|
|
253
289
|
return { files: [configPath] };
|
|
254
290
|
}
|
|
291
|
+
async function installQwenIntegration(options) {
|
|
292
|
+
const qwenHome = await resolveQwenHome();
|
|
293
|
+
const settingsPath = resolve(qwenHome, "settings.json");
|
|
294
|
+
const guidancePath = resolve(qwenHome, "QWEN.md");
|
|
295
|
+
const warnings = await updateQwenSettings({
|
|
296
|
+
...options,
|
|
297
|
+
path: settingsPath,
|
|
298
|
+
});
|
|
299
|
+
await writeMarkedFile({
|
|
300
|
+
path: guidancePath,
|
|
301
|
+
startMarker: ZVEC_GREP_AGENTS_START,
|
|
302
|
+
endMarker: ZVEC_GREP_AGENTS_END,
|
|
303
|
+
block: agentGuidanceBlock({
|
|
304
|
+
search: NAMESPACED_SEARCH_TOOL,
|
|
305
|
+
rg: NAMESPACED_RG_TOOL,
|
|
306
|
+
}),
|
|
307
|
+
force: true,
|
|
308
|
+
});
|
|
309
|
+
for (const warning of warnings) {
|
|
310
|
+
console.warn(` warning ${warning}`);
|
|
311
|
+
}
|
|
312
|
+
return { files: [settingsPath, guidancePath] };
|
|
313
|
+
}
|
|
314
|
+
async function uninstallQwenIntegration() {
|
|
315
|
+
const qwenHome = await resolveQwenHome();
|
|
316
|
+
const settingsPath = resolve(qwenHome, "settings.json");
|
|
317
|
+
const guidancePath = resolve(qwenHome, "QWEN.md");
|
|
318
|
+
await removeQwenSettings(settingsPath);
|
|
319
|
+
await removeMarkedFile({
|
|
320
|
+
path: guidancePath,
|
|
321
|
+
startMarker: ZVEC_GREP_AGENTS_START,
|
|
322
|
+
endMarker: ZVEC_GREP_AGENTS_END,
|
|
323
|
+
});
|
|
324
|
+
return { files: [settingsPath, guidancePath] };
|
|
325
|
+
}
|
|
326
|
+
async function installQoderIntegration(options) {
|
|
327
|
+
const qoderHome = resolveQoderHome();
|
|
328
|
+
const settingsPath = resolve(qoderHome, "settings.json");
|
|
329
|
+
const ideMcpPath = resolveQoderIdeMcpPath();
|
|
330
|
+
const guidancePath = resolve(qoderHome, "AGENTS.md");
|
|
331
|
+
await assertQoderMcpSettingsReplaceable(settingsPath, "Qoder CLI", options.force, isManagedJsonMcpServer);
|
|
332
|
+
await assertQoderPermissionSettingsValid(settingsPath);
|
|
333
|
+
await assertQoderMcpSettingsReplaceable(ideMcpPath, "Qoder IDE", options.force, isManagedQoderIdeMcpServer);
|
|
334
|
+
const warnings = await updateQoderSettings({
|
|
335
|
+
...options,
|
|
336
|
+
path: settingsPath,
|
|
337
|
+
});
|
|
338
|
+
await updateQoderIdeSettings({ ...options, path: ideMcpPath });
|
|
339
|
+
await writeMarkedFile({
|
|
340
|
+
path: guidancePath,
|
|
341
|
+
startMarker: ZVEC_GREP_AGENTS_START,
|
|
342
|
+
endMarker: ZVEC_GREP_AGENTS_END,
|
|
343
|
+
block: agentGuidanceBlock({
|
|
344
|
+
search: NAMESPACED_SEARCH_TOOL,
|
|
345
|
+
rg: NAMESPACED_RG_TOOL,
|
|
346
|
+
qoderAuthorizationRecovery: true,
|
|
347
|
+
}),
|
|
348
|
+
force: true,
|
|
349
|
+
});
|
|
350
|
+
for (const warning of warnings) {
|
|
351
|
+
console.warn(` warning ${warning}`);
|
|
352
|
+
}
|
|
353
|
+
return { files: [settingsPath, ideMcpPath, guidancePath] };
|
|
354
|
+
}
|
|
355
|
+
async function uninstallQoderIntegration() {
|
|
356
|
+
const qoderHome = resolveQoderHome();
|
|
357
|
+
const settingsPath = resolve(qoderHome, "settings.json");
|
|
358
|
+
const ideMcpPaths = qoderIdeMcpPathsForUninstall();
|
|
359
|
+
const guidancePath = resolve(qoderHome, "AGENTS.md");
|
|
360
|
+
await removeQoderSettings(settingsPath);
|
|
361
|
+
for (const path of ideMcpPaths) {
|
|
362
|
+
await removeQoderIdeSettings(path);
|
|
363
|
+
}
|
|
364
|
+
await removeMarkedFile({
|
|
365
|
+
path: guidancePath,
|
|
366
|
+
startMarker: ZVEC_GREP_AGENTS_START,
|
|
367
|
+
endMarker: ZVEC_GREP_AGENTS_END,
|
|
368
|
+
});
|
|
369
|
+
return { files: [settingsPath, ...ideMcpPaths, guidancePath] };
|
|
370
|
+
}
|
|
255
371
|
async function resolveInstallers(parsed, action) {
|
|
256
372
|
const detected = await detectAgentInstallers();
|
|
257
373
|
const targetTokens = [
|
|
@@ -308,8 +424,7 @@ function installersFromTokens(tokens, detected = new Set()) {
|
|
|
308
424
|
}
|
|
309
425
|
const selected = new Map();
|
|
310
426
|
for (const token of normalized) {
|
|
311
|
-
const
|
|
312
|
-
const lower = input === "cc" || input === "claude-code" ? "claude" : input;
|
|
427
|
+
const lower = token.toLowerCase();
|
|
313
428
|
if (lower === "none") {
|
|
314
429
|
return [];
|
|
315
430
|
}
|
|
@@ -335,7 +450,9 @@ function installersFromTokens(tokens, detected = new Set()) {
|
|
|
335
450
|
selected.set(installer.id, installer);
|
|
336
451
|
continue;
|
|
337
452
|
}
|
|
338
|
-
const installer = AGENT_INSTALLERS.find((candidate) => candidate.id === lower ||
|
|
453
|
+
const installer = AGENT_INSTALLERS.find((candidate) => candidate.id === lower ||
|
|
454
|
+
candidate.aliases?.includes(lower) ||
|
|
455
|
+
candidate.label.toLowerCase() === lower);
|
|
339
456
|
if (!installer) {
|
|
340
457
|
throw new Error(`Unknown install target: ${token}`);
|
|
341
458
|
}
|
|
@@ -345,10 +462,13 @@ function installersFromTokens(tokens, detected = new Set()) {
|
|
|
345
462
|
}
|
|
346
463
|
async function detectAgentInstallers() {
|
|
347
464
|
const detected = new Set();
|
|
348
|
-
const checks = await Promise.all(AGENT_INSTALLERS.map(async (installer) =>
|
|
349
|
-
installer
|
|
350
|
-
|
|
351
|
-
|
|
465
|
+
const checks = await Promise.all(AGENT_INSTALLERS.map(async (installer) => {
|
|
466
|
+
const executableDetected = (await Promise.all(installer.executables.map((executable) => executableIsAvailable(executable)))).some(Boolean);
|
|
467
|
+
return {
|
|
468
|
+
installer,
|
|
469
|
+
available: executableDetected || (await installer.detect?.()) === true,
|
|
470
|
+
};
|
|
471
|
+
}));
|
|
352
472
|
for (const check of checks) {
|
|
353
473
|
if (check.available) {
|
|
354
474
|
detected.add(check.installer.id);
|
|
@@ -516,6 +636,101 @@ function resolveOpenCodeConfigPath() {
|
|
|
516
636
|
function resolveCursorConfigPath() {
|
|
517
637
|
return resolve(process.env.CURSOR_CONFIG_DIR ?? resolve(homedir(), ".cursor"), "mcp.json");
|
|
518
638
|
}
|
|
639
|
+
function resolveQoderHome() {
|
|
640
|
+
return resolve(process.env.QODER_CONFIG_DIR || resolve(homedir(), ".qoder"));
|
|
641
|
+
}
|
|
642
|
+
function resolveQoderIdeMcpPath() {
|
|
643
|
+
const configured = process.env.QODER_IDE_MCP_PATH?.trim();
|
|
644
|
+
if (configured)
|
|
645
|
+
return resolve(configured);
|
|
646
|
+
return resolve(homedir(), ".qoder", "mcp.json");
|
|
647
|
+
}
|
|
648
|
+
function qoderIdeMcpPathsForUninstall() {
|
|
649
|
+
return [resolveQoderIdeMcpPath()];
|
|
650
|
+
}
|
|
651
|
+
async function qoderIdeIsAvailable() {
|
|
652
|
+
const configured = process.env.QODER_IDE_EXECUTABLE?.trim();
|
|
653
|
+
const candidates = configured
|
|
654
|
+
? [resolve(configured)]
|
|
655
|
+
: qoderIdeExecutableCandidates();
|
|
656
|
+
for (const candidate of candidates) {
|
|
657
|
+
try {
|
|
658
|
+
await access(candidate, fileSystemConstants.X_OK);
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// Try the next platform-specific installation path.
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
function qoderIdeExecutableCandidates() {
|
|
668
|
+
if (process.platform === "darwin") {
|
|
669
|
+
return [
|
|
670
|
+
resolve(homedir(), "Applications", "Qoder IDE.app", "Contents", "MacOS", "Qoder"),
|
|
671
|
+
resolve("/Applications", "Qoder IDE.app", "Contents", "MacOS", "Qoder"),
|
|
672
|
+
resolve(homedir(), "Applications", "Qoder.app", "Contents", "MacOS", "Qoder"),
|
|
673
|
+
resolve("/Applications", "Qoder.app", "Contents", "MacOS", "Qoder"),
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
if (process.platform === "win32") {
|
|
677
|
+
const localPrograms = resolve(process.env.LOCALAPPDATA || resolve(homedir(), "AppData", "Local"), "Programs");
|
|
678
|
+
return [
|
|
679
|
+
resolve(localPrograms, "Qoder IDE", "Qoder IDE.exe"),
|
|
680
|
+
resolve(localPrograms, "Qoder", "Qoder.exe"),
|
|
681
|
+
...(process.env.ProgramFiles
|
|
682
|
+
? [
|
|
683
|
+
resolve(process.env.ProgramFiles, "Qoder IDE", "Qoder IDE.exe"),
|
|
684
|
+
resolve(process.env.ProgramFiles, "Qoder", "Qoder.exe"),
|
|
685
|
+
]
|
|
686
|
+
: []),
|
|
687
|
+
];
|
|
688
|
+
}
|
|
689
|
+
return [
|
|
690
|
+
"/usr/share/qoder-ide/qoder-ide",
|
|
691
|
+
"/usr/share/qoder-ide/bin/qoder-ide",
|
|
692
|
+
"/usr/bin/qoder-ide",
|
|
693
|
+
"/usr/share/qoder/bin/qoder",
|
|
694
|
+
];
|
|
695
|
+
}
|
|
696
|
+
async function resolveQwenHome() {
|
|
697
|
+
const defaultQwenHome = resolve(homedir(), ".qwen");
|
|
698
|
+
const configured = process.env.QWEN_HOME;
|
|
699
|
+
if (configured) {
|
|
700
|
+
return resolveQwenHomeValue(configured);
|
|
701
|
+
}
|
|
702
|
+
// Qwen Code treats an explicitly exported empty QWEN_HOME as the default
|
|
703
|
+
// directory and does not replace it from a user .env file.
|
|
704
|
+
if (Object.hasOwn(process.env, "QWEN_HOME")) {
|
|
705
|
+
return defaultQwenHome;
|
|
706
|
+
}
|
|
707
|
+
const discovered = (await readQwenHomeFromEnv(resolve(defaultQwenHome, ".env"))) ??
|
|
708
|
+
(await readQwenHomeFromEnv(resolve(homedir(), ".env")));
|
|
709
|
+
return discovered ? resolveQwenHomeValue(discovered) : defaultQwenHome;
|
|
710
|
+
}
|
|
711
|
+
async function readQwenHomeFromEnv(path) {
|
|
712
|
+
const source = await readTextFileIfExists(path);
|
|
713
|
+
if (!source)
|
|
714
|
+
return undefined;
|
|
715
|
+
try {
|
|
716
|
+
return parseEnv(source).QWEN_HOME || undefined;
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// Match Qwen Code's quiet dotenv bootstrap behavior.
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function resolveQwenHomeValue(value) {
|
|
724
|
+
if (value === "~")
|
|
725
|
+
return homedir();
|
|
726
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
727
|
+
return resolve(homedir(), ...value
|
|
728
|
+
.slice(2)
|
|
729
|
+
.split(/[/\\]+/)
|
|
730
|
+
.filter(Boolean));
|
|
731
|
+
}
|
|
732
|
+
return resolve(value);
|
|
733
|
+
}
|
|
519
734
|
async function installJsonMcpServer(options) {
|
|
520
735
|
const config = await readJsonObject(options.path);
|
|
521
736
|
const existingContainer = config[options.containerKey];
|
|
@@ -652,6 +867,465 @@ async function updateClaudePermissionSettings(path, grant) {
|
|
|
652
867
|
}
|
|
653
868
|
await writeJsonObject(path, root);
|
|
654
869
|
}
|
|
870
|
+
async function updateQwenSettings(options) {
|
|
871
|
+
const root = await updateJsoncMcpSettings({
|
|
872
|
+
path: options.path,
|
|
873
|
+
force: options.force,
|
|
874
|
+
label: "Qwen Code",
|
|
875
|
+
server: qwenMcpServer(options),
|
|
876
|
+
isManaged: isManagedQwenMcpServer,
|
|
877
|
+
});
|
|
878
|
+
return contextFileWarnings(root, "QWEN.md");
|
|
879
|
+
}
|
|
880
|
+
async function removeQwenSettings(path) {
|
|
881
|
+
await removeJsoncMcpSettings(path, "Qwen Code", isManagedQwenMcpServer);
|
|
882
|
+
}
|
|
883
|
+
async function updateQoderSettings(options) {
|
|
884
|
+
const existing = await readTextFileIfExists(options.path);
|
|
885
|
+
let source = existing.trim() ? existing : "{}\n";
|
|
886
|
+
const root = parseJsoncSettings(options.path, source, "Qoder");
|
|
887
|
+
validateMcpSettingsContainer(options.path, root);
|
|
888
|
+
const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
|
|
889
|
+
const current = mcpServers.zvec_grep;
|
|
890
|
+
const currentIsManaged = isManagedJsonMcpServer(current);
|
|
891
|
+
if (current !== undefined && !currentIsManaged && !options.force) {
|
|
892
|
+
throw new Error(`Existing unmanaged zvec_grep MCP server found in ${options.path}. Re-run with --force to replace it for Qoder.`);
|
|
893
|
+
}
|
|
894
|
+
const allow = qoderPermissionAllowRules(options.path, root);
|
|
895
|
+
const ownedPermissions = new Set([
|
|
896
|
+
...(currentIsManaged ? qoderOwnedPermissionRules(current) : []),
|
|
897
|
+
...QODER_MCP_PERMISSIONS.filter((permission) => !allow.includes(permission)),
|
|
898
|
+
]);
|
|
899
|
+
const alwaysAllow = currentIsManaged ? qoderAlwaysAllowTools(current) : [];
|
|
900
|
+
for (const { tool } of QODER_MCP_PERMISSION_RULES) {
|
|
901
|
+
if (!alwaysAllow.includes(tool)) {
|
|
902
|
+
alwaysAllow.push(tool);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
source = editJsonWithComments(source, ["mcpServers", "zvec_grep"], qoderMcpServer(options, alwaysAllow, [...ownedPermissions]));
|
|
906
|
+
source = addQoderPermissionRules(source, options.path, root);
|
|
907
|
+
await writeTextFileAtomic(options.path, ensureTrailingNewline(source));
|
|
908
|
+
return contextFileWarnings(root, "AGENTS.md");
|
|
909
|
+
}
|
|
910
|
+
async function removeQoderSettings(path) {
|
|
911
|
+
const existing = await readTextFileIfExists(path);
|
|
912
|
+
if (!existing.trim())
|
|
913
|
+
return;
|
|
914
|
+
let source = existing;
|
|
915
|
+
const root = parseJsoncSettings(path, source, "Qoder");
|
|
916
|
+
validateMcpSettingsContainer(path, root);
|
|
917
|
+
qoderPermissionAllowRules(path, root);
|
|
918
|
+
const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
|
|
919
|
+
const current = mcpServers.zvec_grep;
|
|
920
|
+
if (!isManagedJsonMcpServer(current))
|
|
921
|
+
return;
|
|
922
|
+
const ownedPermissions = qoderOwnedPermissionRules(current);
|
|
923
|
+
source = hasJsoncComments(source)
|
|
924
|
+
? removeJsoncPropertyPreservingComments(source, ["mcpServers", "zvec_grep"])
|
|
925
|
+
: editJsonWithComments(source, Object.keys(mcpServers).length === 1
|
|
926
|
+
? ["mcpServers"]
|
|
927
|
+
: ["mcpServers", "zvec_grep"], undefined);
|
|
928
|
+
source = removeQoderPermissionRules(source, path, root, ownedPermissions);
|
|
929
|
+
if (source !== existing) {
|
|
930
|
+
await writeTextFileAtomic(path, ensureTrailingNewline(source));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function assertQoderPermissionSettingsValid(path) {
|
|
934
|
+
const source = await readTextFileIfExists(path);
|
|
935
|
+
if (!source.trim())
|
|
936
|
+
return;
|
|
937
|
+
const root = parseJsoncSettings(path, source, "Qoder");
|
|
938
|
+
qoderPermissionAllowRules(path, root);
|
|
939
|
+
}
|
|
940
|
+
function addQoderPermissionRules(source, path, root) {
|
|
941
|
+
const allow = qoderPermissionAllowRules(path, root);
|
|
942
|
+
const missing = QODER_MCP_PERMISSIONS.filter((permission) => !allow.includes(permission));
|
|
943
|
+
if (missing.length === 0)
|
|
944
|
+
return source;
|
|
945
|
+
const hasAllowArray = isJsonObject(root.permissions) && Array.isArray(root.permissions.allow);
|
|
946
|
+
if (!hasAllowArray) {
|
|
947
|
+
return editJsonWithComments(source, ["permissions", "allow"], [...allow, ...missing]);
|
|
948
|
+
}
|
|
949
|
+
return insertJsoncArrayValuesAtStart(source, ["permissions", "allow"], missing);
|
|
950
|
+
}
|
|
951
|
+
function removeQoderPermissionRules(source, path, root, ownedPermissions) {
|
|
952
|
+
const allow = qoderPermissionAllowRules(path, root);
|
|
953
|
+
let removedCount = 0;
|
|
954
|
+
for (const permission of ownedPermissions) {
|
|
955
|
+
const removal = removeFirstJsoncArrayStringValue(source, ["permissions", "allow"], permission);
|
|
956
|
+
source = removal.source;
|
|
957
|
+
if (removal.removed)
|
|
958
|
+
removedCount += 1;
|
|
959
|
+
}
|
|
960
|
+
if (removedCount === 0)
|
|
961
|
+
return source;
|
|
962
|
+
if (removedCount < allow.length)
|
|
963
|
+
return source;
|
|
964
|
+
const permissions = root.permissions;
|
|
965
|
+
const hasOtherPermissionSettings = Object.keys(permissions).some((key) => key !== "allow");
|
|
966
|
+
if (hasJsoncComments(source)) {
|
|
967
|
+
return source;
|
|
968
|
+
}
|
|
969
|
+
if (hasOtherPermissionSettings) {
|
|
970
|
+
return editJsonWithComments(source, ["permissions", "allow"], undefined);
|
|
971
|
+
}
|
|
972
|
+
return editJsonWithComments(source, ["permissions"], undefined);
|
|
973
|
+
}
|
|
974
|
+
function qoderOwnedPermissionRules(server) {
|
|
975
|
+
if (!isJsonObject(server) ||
|
|
976
|
+
typeof server.description !== "string" ||
|
|
977
|
+
!server.description.startsWith(QODER_PERMISSION_OWNERSHIP_PREFIX)) {
|
|
978
|
+
return [];
|
|
979
|
+
}
|
|
980
|
+
const alwaysAllow = server.description
|
|
981
|
+
.slice(QODER_PERMISSION_OWNERSHIP_PREFIX.length)
|
|
982
|
+
.split(",")
|
|
983
|
+
.filter(Boolean);
|
|
984
|
+
return QODER_MCP_PERMISSION_RULES.filter(({ tool }) => alwaysAllow.includes(tool)).map(({ permission }) => permission);
|
|
985
|
+
}
|
|
986
|
+
function qoderMcpDescription(ownedPermissions) {
|
|
987
|
+
const tools = QODER_MCP_PERMISSION_RULES.filter(({ permission }) => ownedPermissions.includes(permission)).map(({ tool }) => tool);
|
|
988
|
+
return tools.length > 0
|
|
989
|
+
? `${QODER_PERMISSION_OWNERSHIP_PREFIX}${tools.join(",")}`
|
|
990
|
+
: QODER_CLI_MCP_DESCRIPTION;
|
|
991
|
+
}
|
|
992
|
+
function qoderAlwaysAllowTools(server) {
|
|
993
|
+
if (!isJsonObject(server) || !Array.isArray(server.alwaysAllow))
|
|
994
|
+
return [];
|
|
995
|
+
return server.alwaysAllow.filter((tool) => typeof tool === "string");
|
|
996
|
+
}
|
|
997
|
+
function insertJsoncArrayValuesAtStart(source, path, values) {
|
|
998
|
+
if (values.length === 0)
|
|
999
|
+
return source;
|
|
1000
|
+
const root = parseTree(source);
|
|
1001
|
+
const arrayNode = root ? findNodeAtLocation(root, [...path]) : undefined;
|
|
1002
|
+
if (arrayNode?.type !== "array")
|
|
1003
|
+
return source;
|
|
1004
|
+
const children = arrayNode.children ?? [];
|
|
1005
|
+
const insertionOffset = arrayNode.offset + 1;
|
|
1006
|
+
const closingBracketOffset = arrayNode.offset + arrayNode.length - 1;
|
|
1007
|
+
const multiline = source
|
|
1008
|
+
.slice(arrayNode.offset, closingBracketOffset + 1)
|
|
1009
|
+
.includes("\n");
|
|
1010
|
+
const serialized = values.map((value) => JSON.stringify(value));
|
|
1011
|
+
let insertion;
|
|
1012
|
+
if (multiline) {
|
|
1013
|
+
const formatting = jsonFormattingOptions(source);
|
|
1014
|
+
const lineStart = source.lastIndexOf("\n", arrayNode.offset - 1) + 1;
|
|
1015
|
+
const propertyIndent = source.slice(lineStart, arrayNode.offset).match(/^[\t ]*/)?.[0] ?? "";
|
|
1016
|
+
const indentUnit = formatting.insertSpaces
|
|
1017
|
+
? " ".repeat(formatting.tabSize ?? 2)
|
|
1018
|
+
: "\t";
|
|
1019
|
+
const itemIndent = `${propertyIndent}${indentUnit}`;
|
|
1020
|
+
const lines = serialized.map((value, index) => `${itemIndent}${value}${index < serialized.length - 1 || children.length > 0 ? "," : ""}`);
|
|
1021
|
+
insertion = `${formatting.eol}${lines.join(formatting.eol)}`;
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
insertion = `${serialized.join(", ")}${children.length > 0 ? ", " : ""}`;
|
|
1025
|
+
}
|
|
1026
|
+
return (source.slice(0, insertionOffset) + insertion + source.slice(insertionOffset));
|
|
1027
|
+
}
|
|
1028
|
+
function removeFirstJsoncArrayStringValue(source, path, value) {
|
|
1029
|
+
const root = parseTree(source);
|
|
1030
|
+
const arrayNode = root ? findNodeAtLocation(root, [...path]) : undefined;
|
|
1031
|
+
if (arrayNode?.type !== "array" || !arrayNode.children) {
|
|
1032
|
+
return { source, removed: false };
|
|
1033
|
+
}
|
|
1034
|
+
const index = arrayNode.children.findIndex((child) => child.type === "string" && child.value === value);
|
|
1035
|
+
if (index < 0)
|
|
1036
|
+
return { source, removed: false };
|
|
1037
|
+
const target = arrayNode.children[index];
|
|
1038
|
+
const previous = arrayNode.children[index - 1];
|
|
1039
|
+
const next = arrayNode.children[index + 1];
|
|
1040
|
+
const ranges = [nodeRange(target)];
|
|
1041
|
+
const commaOffset = next
|
|
1042
|
+
? findComma(source, target.offset + target.length, next.offset)
|
|
1043
|
+
: previous
|
|
1044
|
+
? findComma(source, previous.offset + previous.length, target.offset)
|
|
1045
|
+
: undefined;
|
|
1046
|
+
if (commaOffset !== undefined) {
|
|
1047
|
+
ranges.push({ offset: commaOffset, length: 1 });
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
source: ranges
|
|
1051
|
+
.sort((left, right) => right.offset - left.offset)
|
|
1052
|
+
.reduce((current, range) => current.slice(0, range.offset) +
|
|
1053
|
+
current.slice(range.offset + range.length), source),
|
|
1054
|
+
removed: true,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function qoderPermissionAllowRules(path, root) {
|
|
1058
|
+
const currentPermissions = root.permissions;
|
|
1059
|
+
if (currentPermissions !== undefined && !isJsonObject(currentPermissions)) {
|
|
1060
|
+
throw new Error(`Invalid permissions configuration in ${path}.`);
|
|
1061
|
+
}
|
|
1062
|
+
const permissions = isJsonObject(currentPermissions)
|
|
1063
|
+
? currentPermissions
|
|
1064
|
+
: {};
|
|
1065
|
+
const currentAllow = permissions.allow;
|
|
1066
|
+
if (currentAllow !== undefined && !Array.isArray(currentAllow)) {
|
|
1067
|
+
throw new Error(`Invalid permissions.allow configuration in ${path}.`);
|
|
1068
|
+
}
|
|
1069
|
+
if (Array.isArray(currentAllow) &&
|
|
1070
|
+
currentAllow.some((permission) => typeof permission !== "string")) {
|
|
1071
|
+
throw new Error(`Invalid permissions.allow rule in ${path}.`);
|
|
1072
|
+
}
|
|
1073
|
+
return Array.isArray(currentAllow) ? currentAllow : [];
|
|
1074
|
+
}
|
|
1075
|
+
async function updateQoderIdeSettings(options) {
|
|
1076
|
+
await updateJsoncMcpSettings({
|
|
1077
|
+
path: options.path,
|
|
1078
|
+
force: options.force,
|
|
1079
|
+
label: "Qoder IDE",
|
|
1080
|
+
server: qoderIdeMcpServer(options),
|
|
1081
|
+
isManaged: isManagedQoderIdeMcpServer,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
async function removeQoderIdeSettings(path) {
|
|
1085
|
+
await removeJsoncMcpSettings(path, "Qoder IDE", isManagedQoderIdeMcpServer);
|
|
1086
|
+
}
|
|
1087
|
+
function qwenMcpServer(options) {
|
|
1088
|
+
const timeout = options.mcpToolTimeoutSeconds * 1_000;
|
|
1089
|
+
// Qwen otherwise defers MCP schemas behind tool_search, so the managed
|
|
1090
|
+
// search tool would not be directly callable on the first turn.
|
|
1091
|
+
if (options.transport === "stdio") {
|
|
1092
|
+
return {
|
|
1093
|
+
command: "zg",
|
|
1094
|
+
args: stdioArgs(options.mcpToolset),
|
|
1095
|
+
timeout,
|
|
1096
|
+
alwaysLoadTools: true,
|
|
1097
|
+
trust: true,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
httpUrl: resolveServerUrl(),
|
|
1102
|
+
timeout,
|
|
1103
|
+
alwaysLoadTools: true,
|
|
1104
|
+
trust: true,
|
|
1105
|
+
...(options.mcpTokenEnv
|
|
1106
|
+
? {
|
|
1107
|
+
headers: {
|
|
1108
|
+
Authorization: `Bearer \${${options.mcpTokenEnv}}`,
|
|
1109
|
+
},
|
|
1110
|
+
}
|
|
1111
|
+
: {}),
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function qoderMcpServer(options, alwaysAllow = [], ownedPermissions = []) {
|
|
1115
|
+
const timeout = options.mcpToolTimeoutSeconds * 1_000;
|
|
1116
|
+
const permissionPolicy = alwaysAllow.length > 0 ? { alwaysAllow: [...alwaysAllow] } : {};
|
|
1117
|
+
if (options.transport === "stdio") {
|
|
1118
|
+
return {
|
|
1119
|
+
command: "zg",
|
|
1120
|
+
args: stdioArgs(options.mcpToolset),
|
|
1121
|
+
timeout,
|
|
1122
|
+
trust: true,
|
|
1123
|
+
description: qoderMcpDescription(ownedPermissions),
|
|
1124
|
+
...permissionPolicy,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
type: "http",
|
|
1129
|
+
url: resolveServerUrl(),
|
|
1130
|
+
timeout,
|
|
1131
|
+
trust: true,
|
|
1132
|
+
description: qoderMcpDescription(ownedPermissions),
|
|
1133
|
+
...permissionPolicy,
|
|
1134
|
+
...(options.mcpTokenEnv
|
|
1135
|
+
? {
|
|
1136
|
+
headers: {
|
|
1137
|
+
Authorization: `Bearer \${${options.mcpTokenEnv}}`,
|
|
1138
|
+
},
|
|
1139
|
+
}
|
|
1140
|
+
: {}),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function qoderIdeMcpServer(options) {
|
|
1144
|
+
const timeout = options.mcpToolTimeoutSeconds * 1_000;
|
|
1145
|
+
if (options.transport === "stdio") {
|
|
1146
|
+
const launch = stableQoderIdeStdioLaunch(options.mcpToolset);
|
|
1147
|
+
return {
|
|
1148
|
+
command: launch.command,
|
|
1149
|
+
args: launch.args,
|
|
1150
|
+
timeout,
|
|
1151
|
+
description: QODER_IDE_MCP_DESCRIPTION,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
type: "sse",
|
|
1156
|
+
url: resolveServerUrl(),
|
|
1157
|
+
timeout,
|
|
1158
|
+
description: QODER_IDE_MCP_DESCRIPTION,
|
|
1159
|
+
...(options.mcpTokenEnv
|
|
1160
|
+
? {
|
|
1161
|
+
headers: {
|
|
1162
|
+
Authorization: `Bearer \${${options.mcpTokenEnv}}`,
|
|
1163
|
+
},
|
|
1164
|
+
}
|
|
1165
|
+
: {}),
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
async function assertQoderMcpSettingsReplaceable(path, label, force, isManaged) {
|
|
1169
|
+
const existing = await readTextFileIfExists(path);
|
|
1170
|
+
const source = existing.trim() ? existing : "{}\n";
|
|
1171
|
+
const root = parseJsoncSettings(path, source, label);
|
|
1172
|
+
validateMcpSettingsContainer(path, root);
|
|
1173
|
+
const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
|
|
1174
|
+
if (mcpServers.zvec_grep !== undefined &&
|
|
1175
|
+
!isManaged(mcpServers.zvec_grep) &&
|
|
1176
|
+
!force) {
|
|
1177
|
+
throw new Error(`Existing unmanaged zvec_grep MCP server found in ${path}. Re-run with --force to replace it for ${label}.`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function updateJsoncMcpSettings(options) {
|
|
1181
|
+
const existing = await readTextFileIfExists(options.path);
|
|
1182
|
+
let source = existing.trim() ? existing : "{}\n";
|
|
1183
|
+
const root = parseJsoncSettings(options.path, source, options.label);
|
|
1184
|
+
validateMcpSettingsContainer(options.path, root);
|
|
1185
|
+
const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
|
|
1186
|
+
const current = mcpServers.zvec_grep;
|
|
1187
|
+
if (current !== undefined && !options.isManaged(current) && !options.force) {
|
|
1188
|
+
throw new Error(`Existing unmanaged zvec_grep MCP server found in ${options.path}. Re-run with --force to replace it for ${options.label}.`);
|
|
1189
|
+
}
|
|
1190
|
+
source = editJsonWithComments(source, ["mcpServers", "zvec_grep"], options.server);
|
|
1191
|
+
await writeTextFileAtomic(options.path, ensureTrailingNewline(source));
|
|
1192
|
+
return root;
|
|
1193
|
+
}
|
|
1194
|
+
async function removeJsoncMcpSettings(path, label, isManaged) {
|
|
1195
|
+
const existing = await readTextFileIfExists(path);
|
|
1196
|
+
if (!existing.trim())
|
|
1197
|
+
return;
|
|
1198
|
+
let source = existing;
|
|
1199
|
+
const root = parseJsoncSettings(path, source, label);
|
|
1200
|
+
validateMcpSettingsContainer(path, root);
|
|
1201
|
+
const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
|
|
1202
|
+
if (isManaged(mcpServers.zvec_grep)) {
|
|
1203
|
+
source = hasJsoncComments(source)
|
|
1204
|
+
? removeJsoncPropertyPreservingComments(source, [
|
|
1205
|
+
"mcpServers",
|
|
1206
|
+
"zvec_grep",
|
|
1207
|
+
])
|
|
1208
|
+
: editJsonWithComments(source, Object.keys(mcpServers).length === 1
|
|
1209
|
+
? ["mcpServers"]
|
|
1210
|
+
: ["mcpServers", "zvec_grep"], undefined);
|
|
1211
|
+
}
|
|
1212
|
+
if (source !== existing) {
|
|
1213
|
+
await writeTextFileAtomic(path, ensureTrailingNewline(source));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function parseJsoncSettings(path, source, label) {
|
|
1217
|
+
const errors = [];
|
|
1218
|
+
const parsed = parseJsonWithComments(source, errors, {
|
|
1219
|
+
allowTrailingComma: false,
|
|
1220
|
+
disallowComments: false,
|
|
1221
|
+
});
|
|
1222
|
+
if (errors.length > 0 || !isJsonObject(parsed)) {
|
|
1223
|
+
throw new Error(`Invalid ${label} configuration in ${path}.`);
|
|
1224
|
+
}
|
|
1225
|
+
return parsed;
|
|
1226
|
+
}
|
|
1227
|
+
function validateMcpSettingsContainer(path, root) {
|
|
1228
|
+
if (root.mcpServers !== undefined && !isJsonObject(root.mcpServers)) {
|
|
1229
|
+
throw new Error(`Invalid mcpServers configuration in ${path}.`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function editJsonWithComments(source, path, value) {
|
|
1233
|
+
return applyEdits(source, modify(source, [...path], value, {
|
|
1234
|
+
formattingOptions: jsonFormattingOptions(source),
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1237
|
+
function hasJsoncComments(source) {
|
|
1238
|
+
const scanner = createScanner(source, false);
|
|
1239
|
+
for (let token = scanner.scan(); token !== 17 /* SyntaxKind.EOF */; token = scanner.scan()) {
|
|
1240
|
+
if (token === 12 /* SyntaxKind.LineCommentTrivia */ ||
|
|
1241
|
+
token === 13 /* SyntaxKind.BlockCommentTrivia */) {
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
function removeJsoncPropertyPreservingComments(source, path) {
|
|
1248
|
+
const root = parseTree(source);
|
|
1249
|
+
if (!root)
|
|
1250
|
+
return source;
|
|
1251
|
+
const valueNode = findNodeAtLocation(root, [...path]);
|
|
1252
|
+
const propertyNode = valueNode?.parent;
|
|
1253
|
+
const objectNode = propertyNode?.parent;
|
|
1254
|
+
if (propertyNode?.type !== "property" ||
|
|
1255
|
+
objectNode?.type !== "object" ||
|
|
1256
|
+
!objectNode.children) {
|
|
1257
|
+
return source;
|
|
1258
|
+
}
|
|
1259
|
+
const propertyIndex = objectNode.children.indexOf(propertyNode);
|
|
1260
|
+
if (propertyIndex < 0)
|
|
1261
|
+
return source;
|
|
1262
|
+
const ranges = [nodeRange(propertyNode)];
|
|
1263
|
+
const previousProperty = objectNode.children[propertyIndex - 1];
|
|
1264
|
+
const nextProperty = objectNode.children[propertyIndex + 1];
|
|
1265
|
+
const separatorOffset = nextProperty
|
|
1266
|
+
? findComma(source, propertyNode.offset + propertyNode.length, nextProperty.offset)
|
|
1267
|
+
: previousProperty
|
|
1268
|
+
? findComma(source, previousProperty.offset + previousProperty.length, propertyNode.offset)
|
|
1269
|
+
: undefined;
|
|
1270
|
+
if (separatorOffset !== undefined) {
|
|
1271
|
+
ranges.push({ offset: separatorOffset, length: 1 });
|
|
1272
|
+
}
|
|
1273
|
+
return ranges
|
|
1274
|
+
.sort((left, right) => right.offset - left.offset)
|
|
1275
|
+
.reduce((current, range) => current.slice(0, range.offset) +
|
|
1276
|
+
current.slice(range.offset + range.length), source);
|
|
1277
|
+
}
|
|
1278
|
+
function nodeRange(node) {
|
|
1279
|
+
return { offset: node.offset, length: node.length };
|
|
1280
|
+
}
|
|
1281
|
+
function findComma(source, startOffset, endOffset) {
|
|
1282
|
+
const scanner = createScanner(source, false);
|
|
1283
|
+
scanner.setPosition(startOffset);
|
|
1284
|
+
for (let token = scanner.scan(); token !== 17 /* SyntaxKind.EOF */; token = scanner.scan()) {
|
|
1285
|
+
const tokenOffset = scanner.getTokenOffset();
|
|
1286
|
+
if (tokenOffset >= endOffset)
|
|
1287
|
+
return undefined;
|
|
1288
|
+
if (token === 5 /* SyntaxKind.CommaToken */)
|
|
1289
|
+
return tokenOffset;
|
|
1290
|
+
}
|
|
1291
|
+
return undefined;
|
|
1292
|
+
}
|
|
1293
|
+
function jsonFormattingOptions(source) {
|
|
1294
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
1295
|
+
const indentation = source.match(/^[ \t]+(?=")/m)?.[0];
|
|
1296
|
+
if (indentation?.startsWith("\t")) {
|
|
1297
|
+
return { insertSpaces: false, tabSize: 1, eol };
|
|
1298
|
+
}
|
|
1299
|
+
return {
|
|
1300
|
+
insertSpaces: true,
|
|
1301
|
+
tabSize: indentation?.length ?? 2,
|
|
1302
|
+
eol,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function contextFileWarnings(root, fileName) {
|
|
1306
|
+
if (!isJsonObject(root.context))
|
|
1307
|
+
return [];
|
|
1308
|
+
const configured = root.context.fileName;
|
|
1309
|
+
const fileNames = typeof configured === "string"
|
|
1310
|
+
? [configured]
|
|
1311
|
+
: Array.isArray(configured) &&
|
|
1312
|
+
configured.every((value) => typeof value === "string")
|
|
1313
|
+
? configured
|
|
1314
|
+
: undefined;
|
|
1315
|
+
if (!fileNames || fileNames.includes(fileName))
|
|
1316
|
+
return [];
|
|
1317
|
+
return [
|
|
1318
|
+
`context.fileName does not include ${fileName}; the installed zvec-grep guidance may not be loaded.`,
|
|
1319
|
+
];
|
|
1320
|
+
}
|
|
1321
|
+
function isManagedQwenMcpServer(value) {
|
|
1322
|
+
return (isJsonObject(value) &&
|
|
1323
|
+
(value.httpUrl === resolveServerUrl() ||
|
|
1324
|
+
(value.command === "zg" && isStdioArgs(value.args))));
|
|
1325
|
+
}
|
|
1326
|
+
function ensureTrailingNewline(source) {
|
|
1327
|
+
return source.endsWith("\n") ? source : `${source}\n`;
|
|
1328
|
+
}
|
|
655
1329
|
async function readJsonObject(path) {
|
|
656
1330
|
const source = await readTextFileIfExists(path);
|
|
657
1331
|
return source.trim() ? parseJsonObject(path, source) : {};
|
|
@@ -689,6 +1363,12 @@ function isManagedJsonMcpServer(value) {
|
|
|
689
1363
|
(value.command[3] === "--mcp-toolset" &&
|
|
690
1364
|
(value.command[4] === "agent" || value.command[4] === "full"))));
|
|
691
1365
|
}
|
|
1366
|
+
function isManagedQoderIdeMcpServer(value) {
|
|
1367
|
+
return (isJsonObject(value) &&
|
|
1368
|
+
value.description === QODER_IDE_MCP_DESCRIPTION &&
|
|
1369
|
+
((typeof value.command === "string" && isStableStdioArgs(value.args)) ||
|
|
1370
|
+
(value.type === "sse" && typeof value.url === "string")));
|
|
1371
|
+
}
|
|
692
1372
|
function isStdioArgs(value) {
|
|
693
1373
|
return (Array.isArray(value) &&
|
|
694
1374
|
(value.length === 2 || value.length === 4) &&
|
|
@@ -698,6 +1378,16 @@ function isStdioArgs(value) {
|
|
|
698
1378
|
(value[2] === "--mcp-toolset" &&
|
|
699
1379
|
(value[3] === "agent" || value[3] === "full"))));
|
|
700
1380
|
}
|
|
1381
|
+
function isStableStdioArgs(value) {
|
|
1382
|
+
return (Array.isArray(value) &&
|
|
1383
|
+
(value.length === 3 || value.length === 5) &&
|
|
1384
|
+
typeof value[0] === "string" &&
|
|
1385
|
+
value[1] === "server" &&
|
|
1386
|
+
value[2] === "--stdio" &&
|
|
1387
|
+
(value.length === 3 ||
|
|
1388
|
+
(value[3] === "--mcp-toolset" &&
|
|
1389
|
+
(value[4] === "agent" || value[4] === "full"))));
|
|
1390
|
+
}
|
|
701
1391
|
function stdioArgs(mcpToolset) {
|
|
702
1392
|
return [
|
|
703
1393
|
"server",
|
|
@@ -708,6 +1398,16 @@ function stdioArgs(mcpToolset) {
|
|
|
708
1398
|
function stdioCommand(mcpToolset) {
|
|
709
1399
|
return ["zg", ...stdioArgs(mcpToolset)];
|
|
710
1400
|
}
|
|
1401
|
+
function stableQoderIdeStdioLaunch(mcpToolset) {
|
|
1402
|
+
const cliPath = process.argv[1];
|
|
1403
|
+
if (!cliPath) {
|
|
1404
|
+
return { command: "zg", args: stdioArgs(mcpToolset) };
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
command: process.execPath,
|
|
1408
|
+
args: [resolve(cliPath), ...stdioArgs(mcpToolset)],
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
711
1411
|
function tomlStringArray(values) {
|
|
712
1412
|
return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
713
1413
|
}
|
|
@@ -918,6 +1618,17 @@ function agentGuidanceBlock(toolNames) {
|
|
|
918
1618
|
const searchTool = toolNames?.search ?? "zvec_grep_search";
|
|
919
1619
|
const rgTool = toolNames?.rg ?? "zvec_grep_rg";
|
|
920
1620
|
const exactLookupRoute = `\`${rgTool}\` when it is listed by the current host; otherwise native Grep or \`rg\``;
|
|
1621
|
+
const qoderAuthorizationRecovery = toolNames?.qoderAuthorizationRecovery
|
|
1622
|
+
? `
|
|
1623
|
+
|
|
1624
|
+
${formatPromptRules("### Qoder Remote Embedding authorization recovery", [
|
|
1625
|
+
`When \`${searchTool}\` needs \`remote_embedding_authorization\` and the current Qoder host returns \`code = 51500 message = method not found: No request handler configured\` without showing an authorization form, returns \`${REMOTE_EMBEDDING_ELICITATION_UNSUPPORTED_MESSAGE}\`, or reports that authorization was declined or cancelled without showing the user an authorization form, treat it as a Qoder client interaction limitation: the host lacks the server-to-client \`elicitation/create\` request handler. The outer MCP \`tools/call\` has already reached the registered MCP server and tool, so do not diagnose this error as a disconnected or missing MCP server or as a missing tool. Apply this recovery only to the Remote Embedding authorization path, not to arbitrary 51500 failures. Do not immediately fall back to broad file reads, do not treat it as a missing API credential, and do not grant access silently. If the user actually declined a displayed authorization form, respect that decision and do not ask again.`,
|
|
1626
|
+
"Use the current Qoder host's built-in user-question tool (`ask_user_question` in Qoder IDE or `AskUserQuestion` in Qoder CLI/SDK) to offer exactly these choices: allow Remote Embedding for this workspace, use local FTS only, or cancel. Explain that workspace approval may send query text and selected workspace content to the configured provider and endpoint and may incur provider charges.",
|
|
1627
|
+
'Only after the user explicitly chooses workspace approval, run `zg auth grant "<absolute-root>" --capability embedding --scope workspace`, substituting the same absolute root used by the failed search, and then retry the original search call once. Do not use `--allow-remote`; it applies only to one CLI command and does not authorize the MCP retry.',
|
|
1628
|
+
`If the user chooses local FTS, retry \`${searchTool}\` once with the original search text in \`fts\`, omit \`query\`, \`queries\`, and \`vector\`, set \`autoUpdate\` to \`false\` and \`freshness\` to \`eventual\`, and preserve \`root\`, filters, and limits. This route is lexical-only, does not refresh the remote-embedding index, and sends no query text or workspace content to a remote Embedding provider.`,
|
|
1629
|
+
"If the user cancels, the grant command fails, or interactive user input is unavailable, stop and report that no remote data was sent. Provider credentials and Remote Embedding data authorization are separate; never request or modify an API key merely to resolve this interaction error.",
|
|
1630
|
+
])}`
|
|
1631
|
+
: "";
|
|
921
1632
|
return `${ZVEC_GREP_AGENTS_START}
|
|
922
1633
|
## zvec-grep
|
|
923
1634
|
|
|
@@ -933,24 +1644,10 @@ ${formatPromptRules("### Retrieval routing", [
|
|
|
933
1644
|
"Before broad file reads or delegating workspace discovery, use the appropriate search route. Do not delegate solely to locate material, and stop when the evidence is sufficient.",
|
|
934
1645
|
])}
|
|
935
1646
|
|
|
936
|
-
${formatPromptRules("### Search
|
|
937
|
-
"`query` creates one primary hybrid result group; `queries` creates one or more primary hybrid result groups.",
|
|
938
|
-
"`fts` and `vector` add supplemental lexical and semantic routes. `fts` is a retrieval route, not a hard filter or constraint.",
|
|
939
|
-
"Without `fuse`, search groups are evaluated separately and returned as one deduplicated, reranked list with query-group metadata. Set `fuse: true` to collapse the supplied primary and supplemental intents into one search plan.",
|
|
1647
|
+
${formatPromptRules("### Search evidence", [
|
|
940
1648
|
"Search results include bounded source snippets. Treat a sufficient snippet as already-read evidence, and read a cited file only when a required detail falls outside the snippet.",
|
|
941
1649
|
])}
|
|
942
1650
|
|
|
943
|
-
Example mixed search:
|
|
944
|
-
|
|
945
|
-
\`\`\`json
|
|
946
|
-
{
|
|
947
|
-
"root": "/absolute/workspace",
|
|
948
|
-
"query": "how are search results ranked and fused",
|
|
949
|
-
"fts": ["RRF", "score"],
|
|
950
|
-
"fuse": true
|
|
951
|
-
}
|
|
952
|
-
\`\`\`
|
|
953
|
-
|
|
954
1651
|
${formatPromptRules("### Freshness and index lifecycle", [
|
|
955
1652
|
"Pass a daemon-visible absolute `root` on every zvec-grep workspace call.",
|
|
956
1653
|
"Read `freshness` and `background_refresh` from search results without a status preflight.",
|
|
@@ -958,6 +1655,7 @@ ${formatPromptRules("### Freshness and index lifecycle", [
|
|
|
958
1655
|
`If the index is missing but exact or regex lookup can answer the task, use ${exactLookupRoute}.`,
|
|
959
1656
|
"Creating, rebuilding, or dropping a persistent index requires an explicit user request or authorization; never do so silently.",
|
|
960
1657
|
])}
|
|
1658
|
+
${qoderAuthorizationRecovery}
|
|
961
1659
|
${ZVEC_GREP_AGENTS_END}`;
|
|
962
1660
|
}
|
|
963
1661
|
//# sourceMappingURL=install.js.map
|