@zvec/zvec-grep 0.2.0 → 0.2.2

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.
Files changed (94) hide show
  1. package/README.md +42 -9
  2. package/README_CN.md +39 -11
  3. package/dist/authorization/prompt.d.ts +1 -0
  4. package/dist/authorization/prompt.js +1 -0
  5. package/dist/authorization/prompt.js.map +1 -1
  6. package/dist/cli/args.js +5 -2
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/commands.js +46 -1
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/cli/errors.d.ts +3 -0
  11. package/dist/cli/errors.js +39 -6
  12. package/dist/cli/errors.js.map +1 -1
  13. package/dist/cli/format/status.d.ts +2 -0
  14. package/dist/cli/format/status.js +24 -6
  15. package/dist/cli/format/status.js.map +1 -1
  16. package/dist/cli/help.js +24 -8
  17. package/dist/cli/help.js.map +1 -1
  18. package/dist/cli/index.js +3 -1
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/install.js +542 -34
  21. package/dist/cli/install.js.map +1 -1
  22. package/dist/cli/managed-rg.js +9 -1
  23. package/dist/cli/managed-rg.js.map +1 -1
  24. package/dist/daemon/backend.js +4 -0
  25. package/dist/daemon/backend.js.map +1 -1
  26. package/dist/daemon/config.d.ts +3 -0
  27. package/dist/daemon/config.js +19 -0
  28. package/dist/daemon/config.js.map +1 -1
  29. package/dist/daemon/job-scheduler.d.ts +7 -4
  30. package/dist/daemon/job-scheduler.js +80 -12
  31. package/dist/daemon/job-scheduler.js.map +1 -1
  32. package/dist/daemon/root-lease.js +18 -9
  33. package/dist/daemon/root-lease.js.map +1 -1
  34. package/dist/daemon/root-runtime.d.ts +1 -0
  35. package/dist/daemon/root-runtime.js +4 -1
  36. package/dist/daemon/root-runtime.js.map +1 -1
  37. package/dist/daemon/runtime-manager.d.ts +1 -0
  38. package/dist/daemon/runtime-manager.js +10 -4
  39. package/dist/daemon/runtime-manager.js.map +1 -1
  40. package/dist/daemon/runtime.js +3 -1
  41. package/dist/daemon/runtime.js.map +1 -1
  42. package/dist/daemon/server-controller.d.ts +0 -1
  43. package/dist/daemon/server-controller.js +37 -37
  44. package/dist/daemon/server-controller.js.map +1 -1
  45. package/dist/daemon/watch-manager.d.ts +1 -1
  46. package/dist/daemon/watch-manager.js +16 -9
  47. package/dist/daemon/watch-manager.js.map +1 -1
  48. package/dist/engine/errors.d.ts +1 -0
  49. package/dist/engine/errors.js +11 -0
  50. package/dist/engine/errors.js.map +1 -1
  51. package/dist/engine/models/artifact-cache-lock.d.ts +16 -0
  52. package/dist/engine/models/artifact-cache-lock.js +228 -0
  53. package/dist/engine/models/artifact-cache-lock.js.map +1 -0
  54. package/dist/engine/models/artifact-downloader.d.ts +77 -0
  55. package/dist/engine/models/artifact-downloader.js +766 -0
  56. package/dist/engine/models/artifact-downloader.js.map +1 -0
  57. package/dist/engine/models/backends/llama-cpp.d.ts +8 -8
  58. package/dist/engine/models/backends/llama-cpp.js +83 -21
  59. package/dist/engine/models/backends/llama-cpp.js.map +1 -1
  60. package/dist/engine/models/backends/model2vec.d.ts +6 -8
  61. package/dist/engine/models/backends/model2vec.js +142 -109
  62. package/dist/engine/models/backends/model2vec.js.map +1 -1
  63. package/dist/engine/models/backends/transformers-js.d.ts +9 -9
  64. package/dist/engine/models/backends/transformers-js.js +71 -27
  65. package/dist/engine/models/backends/transformers-js.js.map +1 -1
  66. package/dist/engine/models/catalog.d.ts +261 -2
  67. package/dist/engine/models/catalog.js +306 -2
  68. package/dist/engine/models/catalog.js.map +1 -1
  69. package/dist/engine/models/download-progress.d.ts +5 -4
  70. package/dist/engine/models/download-progress.js +26 -27
  71. package/dist/engine/models/download-progress.js.map +1 -1
  72. package/dist/engine/models/embeddings.d.ts +2 -0
  73. package/dist/engine/models/embeddings.js.map +1 -1
  74. package/dist/engine/pipeline/indexing/index.js +201 -45
  75. package/dist/engine/pipeline/indexing/index.js.map +1 -1
  76. package/dist/engine/utils/atomic-file.d.ts +3 -0
  77. package/dist/engine/utils/atomic-file.js +39 -0
  78. package/dist/engine/utils/atomic-file.js.map +1 -0
  79. package/dist/mcp/input-normalization.js +2 -2
  80. package/dist/mcp/input-normalization.js.map +1 -1
  81. package/dist/mcp/schemas.d.ts +13 -17
  82. package/dist/mcp/schemas.js +9 -3
  83. package/dist/mcp/schemas.js.map +1 -1
  84. package/dist/mcp/stdio-bridge.d.ts +4 -0
  85. package/dist/mcp/stdio-bridge.js +59 -3
  86. package/dist/mcp/stdio-bridge.js.map +1 -1
  87. package/dist/mcp/tools.d.ts +8 -8
  88. package/dist/mcp/tools.js +6 -0
  89. package/dist/mcp/tools.js.map +1 -1
  90. package/docs/01-agents.md +80 -9
  91. package/docs/02-cli.md +9 -4
  92. package/docs/06-server.md +13 -0
  93. package/docs/07-embedding.md +28 -1
  94. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { applyEdits, modify, parse as parseJsonWithComments, } from "jsonc-parser";
1
+ import { applyEdits, createScanner, findNodeAtLocation, modify, parse as parseJsonWithComments, parseTree, } from "jsonc-parser";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { constants as fileSystemConstants } from "node:fs";
4
4
  import { access, chmod, lstat, mkdir, open, readFile, readlink, rename, stat, unlink, } from "node:fs/promises";
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
6
6
  import { delimiter, dirname, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { parseEnv } from "node:util";
9
+ import { REMOTE_EMBEDDING_ELICITATION_UNSUPPORTED_MESSAGE } from "../authorization/prompt.js";
9
10
  import { resolveServerUrl } from "../client/mode-router.js";
10
11
  import { formatPromptRules, ZVEC_GREP_WORKSPACE_EVIDENCE_RULES, } from "../prompts/zvec-grep-guidance.js";
11
12
  const AGENT_INSTALLERS = [
@@ -13,28 +14,28 @@ const AGENT_INSTALLERS = [
13
14
  id: "claude",
14
15
  aliases: ["cc", "claude-code"],
15
16
  label: "Claude Code",
16
- executable: "claude",
17
+ executables: ["claude"],
17
18
  install: installClaudeIntegration,
18
19
  uninstall: uninstallClaudeIntegration,
19
20
  },
20
21
  {
21
22
  id: "codex",
22
23
  label: "Codex",
23
- executable: "codex",
24
+ executables: ["codex"],
24
25
  install: installCodexIntegration,
25
26
  uninstall: uninstallCodexIntegration,
26
27
  },
27
28
  {
28
29
  id: "opencode",
29
30
  label: "OpenCode",
30
- executable: "opencode",
31
+ executables: ["opencode"],
31
32
  install: installOpenCodeIntegration,
32
33
  uninstall: uninstallOpenCodeIntegration,
33
34
  },
34
35
  {
35
36
  id: "cursor",
36
37
  label: "Cursor",
37
- executable: "cursor",
38
+ executables: ["cursor"],
38
39
  install: installCursorIntegration,
39
40
  uninstall: uninstallCursorIntegration,
40
41
  },
@@ -42,18 +43,40 @@ const AGENT_INSTALLERS = [
42
43
  id: "qwen",
43
44
  aliases: ["qwen-code", "qwencode"],
44
45
  label: "Qwen Code",
45
- executable: "qwen",
46
+ executables: ["qwen"],
46
47
  install: installQwenIntegration,
47
48
  uninstall: uninstallQwenIntegration,
48
49
  },
50
+ {
51
+ id: "qoder",
52
+ label: "Qoder",
53
+ executables: ["qoder", "qodercli", "qoder-ide"],
54
+ detect: qoderIdeIsAvailable,
55
+ install: installQoderIntegration,
56
+ uninstall: uninstallQoderIntegration,
57
+ },
49
58
  ];
50
59
  const ZVEC_GREP_CONFIG_START = "# ZVEC_GREP_START";
51
60
  const ZVEC_GREP_CONFIG_END = "# ZVEC_GREP_END";
52
61
  const ZVEC_GREP_AGENTS_START = "<!-- ZVEC_GREP_START -->";
53
62
  const ZVEC_GREP_AGENTS_END = "<!-- ZVEC_GREP_END -->";
54
63
  const CLAUDE_MCP_PERMISSION = "mcp__zvec_grep__*";
55
- const QWEN_SEARCH_TOOL = "mcp__zvec_grep__zvec_grep_search";
56
- const QWEN_RG_TOOL = "mcp__zvec_grep__zvec_grep_rg";
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";
57
80
  const DEFAULT_MCP_TOOL_TIMEOUT_SECONDS = 600;
58
81
  export async function runInstall(parsed) {
59
82
  printInstallHeader();
@@ -278,8 +301,8 @@ async function installQwenIntegration(options) {
278
301
  startMarker: ZVEC_GREP_AGENTS_START,
279
302
  endMarker: ZVEC_GREP_AGENTS_END,
280
303
  block: agentGuidanceBlock({
281
- search: QWEN_SEARCH_TOOL,
282
- rg: QWEN_RG_TOOL,
304
+ search: NAMESPACED_SEARCH_TOOL,
305
+ rg: NAMESPACED_RG_TOOL,
283
306
  }),
284
307
  force: true,
285
308
  });
@@ -300,6 +323,51 @@ async function uninstallQwenIntegration() {
300
323
  });
301
324
  return { files: [settingsPath, guidancePath] };
302
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
+ }
303
371
  async function resolveInstallers(parsed, action) {
304
372
  const detected = await detectAgentInstallers();
305
373
  const targetTokens = [
@@ -394,10 +462,13 @@ function installersFromTokens(tokens, detected = new Set()) {
394
462
  }
395
463
  async function detectAgentInstallers() {
396
464
  const detected = new Set();
397
- const checks = await Promise.all(AGENT_INSTALLERS.map(async (installer) => ({
398
- installer,
399
- available: await executableIsAvailable(installer.executable),
400
- })));
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
+ }));
401
472
  for (const check of checks) {
402
473
  if (check.available) {
403
474
  detected.add(check.installer.id);
@@ -565,6 +636,63 @@ function resolveOpenCodeConfigPath() {
565
636
  function resolveCursorConfigPath() {
566
637
  return resolve(process.env.CURSOR_CONFIG_DIR ?? resolve(homedir(), ".cursor"), "mcp.json");
567
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
+ }
568
696
  async function resolveQwenHome() {
569
697
  const defaultQwenHome = resolve(homedir(), ".qwen");
570
698
  const configured = process.env.QWEN_HOME;
@@ -740,38 +868,222 @@ async function updateClaudePermissionSettings(path, grant) {
740
868
  await writeJsonObject(path, root);
741
869
  }
742
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) {
743
884
  const existing = await readTextFileIfExists(options.path);
744
885
  let source = existing.trim() ? existing : "{}\n";
745
- const root = parseQwenSettings(options.path, source);
746
- validateQwenSettingsContainers(options.path, root);
886
+ const root = parseJsoncSettings(options.path, source, "Qoder");
887
+ validateMcpSettingsContainer(options.path, root);
747
888
  const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
748
889
  const current = mcpServers.zvec_grep;
749
- const managed = isManagedQwenMcpServer(current);
750
- if (current !== undefined && !managed && !options.force) {
751
- throw new Error(`Existing unmanaged zvec_grep MCP server found in ${options.path}. Re-run with --force to replace it for Qwen Code.`);
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
+ }
752
904
  }
753
- source = editJsonWithComments(source, ["mcpServers", "zvec_grep"], qwenMcpServer(options));
905
+ source = editJsonWithComments(source, ["mcpServers", "zvec_grep"], qoderMcpServer(options, alwaysAllow, [...ownedPermissions]));
906
+ source = addQoderPermissionRules(source, options.path, root);
754
907
  await writeTextFileAtomic(options.path, ensureTrailingNewline(source));
755
- return qwenSettingsWarnings(root);
908
+ return contextFileWarnings(root, "AGENTS.md");
756
909
  }
757
- async function removeQwenSettings(path) {
910
+ async function removeQoderSettings(path) {
758
911
  const existing = await readTextFileIfExists(path);
759
912
  if (!existing.trim())
760
913
  return;
761
914
  let source = existing;
762
- const root = parseQwenSettings(path, source);
763
- validateQwenSettingsContainers(path, root);
915
+ const root = parseJsoncSettings(path, source, "Qoder");
916
+ validateMcpSettingsContainer(path, root);
917
+ qoderPermissionAllowRules(path, root);
764
918
  const mcpServers = isJsonObject(root.mcpServers) ? root.mcpServers : {};
765
- const managed = isManagedQwenMcpServer(mcpServers.zvec_grep);
766
- if (managed) {
767
- source = editJsonWithComments(source, Object.keys(mcpServers).length === 1
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
768
926
  ? ["mcpServers"]
769
927
  : ["mcpServers", "zvec_grep"], undefined);
770
- }
928
+ source = removeQoderPermissionRules(source, path, root, ownedPermissions);
771
929
  if (source !== existing) {
772
930
  await writeTextFileAtomic(path, ensureTrailingNewline(source));
773
931
  }
774
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
+ }
775
1087
  function qwenMcpServer(options) {
776
1088
  const timeout = options.mcpToolTimeoutSeconds * 1_000;
777
1089
  // Qwen otherwise defers MCP schemas behind tool_search, so the managed
@@ -799,18 +1111,120 @@ function qwenMcpServer(options) {
799
1111
  : {}),
800
1112
  };
801
1113
  }
802
- function parseQwenSettings(path, source) {
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) {
803
1217
  const errors = [];
804
1218
  const parsed = parseJsonWithComments(source, errors, {
805
1219
  allowTrailingComma: false,
806
1220
  disallowComments: false,
807
1221
  });
808
1222
  if (errors.length > 0 || !isJsonObject(parsed)) {
809
- throw new Error(`Invalid Qwen Code configuration in ${path}.`);
1223
+ throw new Error(`Invalid ${label} configuration in ${path}.`);
810
1224
  }
811
1225
  return parsed;
812
1226
  }
813
- function validateQwenSettingsContainers(path, root) {
1227
+ function validateMcpSettingsContainer(path, root) {
814
1228
  if (root.mcpServers !== undefined && !isJsonObject(root.mcpServers)) {
815
1229
  throw new Error(`Invalid mcpServers configuration in ${path}.`);
816
1230
  }
@@ -820,6 +1234,62 @@ function editJsonWithComments(source, path, value) {
820
1234
  formattingOptions: jsonFormattingOptions(source),
821
1235
  }));
822
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
+ }
823
1293
  function jsonFormattingOptions(source) {
824
1294
  const eol = source.includes("\r\n") ? "\r\n" : "\n";
825
1295
  const indentation = source.match(/^[ \t]+(?=")/m)?.[0];
@@ -832,7 +1302,7 @@ function jsonFormattingOptions(source) {
832
1302
  eol,
833
1303
  };
834
1304
  }
835
- function qwenSettingsWarnings(root) {
1305
+ function contextFileWarnings(root, fileName) {
836
1306
  if (!isJsonObject(root.context))
837
1307
  return [];
838
1308
  const configured = root.context.fileName;
@@ -842,10 +1312,10 @@ function qwenSettingsWarnings(root) {
842
1312
  configured.every((value) => typeof value === "string")
843
1313
  ? configured
844
1314
  : undefined;
845
- if (!fileNames || fileNames.includes("QWEN.md"))
1315
+ if (!fileNames || fileNames.includes(fileName))
846
1316
  return [];
847
1317
  return [
848
- "context.fileName does not include QWEN.md; the installed zvec-grep guidance may not be loaded.",
1318
+ `context.fileName does not include ${fileName}; the installed zvec-grep guidance may not be loaded.`,
849
1319
  ];
850
1320
  }
851
1321
  function isManagedQwenMcpServer(value) {
@@ -893,6 +1363,12 @@ function isManagedJsonMcpServer(value) {
893
1363
  (value.command[3] === "--mcp-toolset" &&
894
1364
  (value.command[4] === "agent" || value.command[4] === "full"))));
895
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
+ }
896
1372
  function isStdioArgs(value) {
897
1373
  return (Array.isArray(value) &&
898
1374
  (value.length === 2 || value.length === 4) &&
@@ -902,6 +1378,16 @@ function isStdioArgs(value) {
902
1378
  (value[2] === "--mcp-toolset" &&
903
1379
  (value[3] === "agent" || value[3] === "full"))));
904
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
+ }
905
1391
  function stdioArgs(mcpToolset) {
906
1392
  return [
907
1393
  "server",
@@ -912,6 +1398,16 @@ function stdioArgs(mcpToolset) {
912
1398
  function stdioCommand(mcpToolset) {
913
1399
  return ["zg", ...stdioArgs(mcpToolset)];
914
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
+ }
915
1411
  function tomlStringArray(values) {
916
1412
  return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
917
1413
  }
@@ -1122,6 +1618,17 @@ function agentGuidanceBlock(toolNames) {
1122
1618
  const searchTool = toolNames?.search ?? "zvec_grep_search";
1123
1619
  const rgTool = toolNames?.rg ?? "zvec_grep_rg";
1124
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
+ : "";
1125
1632
  return `${ZVEC_GREP_AGENTS_START}
1126
1633
  ## zvec-grep
1127
1634
 
@@ -1148,6 +1655,7 @@ ${formatPromptRules("### Freshness and index lifecycle", [
1148
1655
  `If the index is missing but exact or regex lookup can answer the task, use ${exactLookupRoute}.`,
1149
1656
  "Creating, rebuilding, or dropping a persistent index requires an explicit user request or authorization; never do so silently.",
1150
1657
  ])}
1658
+ ${qoderAuthorizationRecovery}
1151
1659
  ${ZVEC_GREP_AGENTS_END}`;
1152
1660
  }
1153
1661
  //# sourceMappingURL=install.js.map