qmai-cursor-agent.cjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/usr/bin/env node
  2. const { spawn } = require("child_process");
  3. const fs = require("fs");
  4. const path = require("path");
  5. const ACP_ALIASES = {
  6. auto: "default",
  7. "composer-2": "composer-2.5",
  8. };
  9. const EFFORTS = [
  10. "extra-high",
  11. "xhigh",
  12. "minimal",
  13. "medium",
  14. "high",
  15. "none",
  16. "low",
  17. "max",
  18. ];
  19. function bakedAgent() {
  20. try {
  21. return JSON.parse(String.raw`__QMAI_BAKED_AGENT__`);
  22. } catch {
  23. return "";
  24. }
  25. }
  26. function normalizeEffort(raw) {
  27. const value = String(raw).trim().toLowerCase();
  28. if (value === "extra-high" || value === "max") return "xhigh";
  29. return value;
  30. }
  31. function toCursorAcpModelId(raw) {
  32. const trimmed = String(raw || "").trim();
  33. if (!trimmed) return "";
  34. if (trimmed === "default") return "default";
  35. const parameterized = trimmed.includes("[")
  36. ? trimmed.replace(/\[.*$/, "").trim()
  37. : trimmed;
  38. let id = parameterized;
  39. if (id.toLowerCase().startsWith("cursor-")) id = id.slice("cursor-".length);
  40. if (/-fast$/i.test(id)) id = id.replace(/-fast$/i, "");
  41. for (const effort of EFFORTS) {
  42. const re = new RegExp(`[-_]${effort}$`, "i");
  43. if (re.test(id)) {
  44. id = id.replace(re, "");
  45. break;
  46. }
  47. }
  48. id = id.replace(/[-_]thinking$/i, "");
  49. return ACP_ALIASES[id.toLowerCase()] ?? ACP_ALIASES[trimmed.toLowerCase()] ?? id;
  50. }
  51. function inferFast(model) {
  52. if (/(?:^|[\[,])\s*fast\s*=\s*true(?:[,\]]|$)/i.test(model)) return true;
  53. if (/(?:^|[\[,])\s*fast\s*=\s*false(?:[,\]]|$)/i.test(model)) return false;
  54. return /-fast$/i.test(model.replace(/\[.*$/, ""));
  55. }
  56. function inferEffort(model) {
  57. const param = /(?:^|[\[,])\s*effort\s*=\s*([^,\]]+)/i.exec(model);
  58. if (param) return normalizeEffort(param[1]);
  59. const id = model.replace(/\[.*$/, "").replace(/-fast$/i, "");
  60. for (const effort of EFFORTS) {
  61. if (new RegExp(`[-_]${effort}$`, "i").test(id)) return normalizeEffort(effort);
  62. }
  63. return "";
  64. }
  65. function toCursorAcpModelValue(base, fast, effort) {
  66. const params = [];
  67. if (effort) params.push(`effort=${effort}`);
  68. if (fast === true) params.push("fast=true");
  69. else if (fast === false) params.push("fast=false");
  70. return params.length === 0 ? base : `${base}[${params.join(",")}]`;
  71. }
  72. function isDefaultModel(model) {
  73. const trimmed = String(model || "").trim();
  74. return !trimmed || trimmed === "default" || trimmed === "auto";
  75. }
  76. function asCliModel(model) {
  77. const trimmed = String(model || "").trim();
  78. if (isDefaultModel(trimmed) || trimmed.includes("[")) return "";
  79. return trimmed;
  80. }
  81. function cliModelToAcpValue(model) {
  82. const trimmed = String(model || "").trim();
  83. if (isDefaultModel(trimmed)) return "";
  84. const acp = toCursorAcpModelId(trimmed);
  85. if (!acp || acp === "default") return "";
  86. const baseId = trimmed.replace(/\[.*$/, "");
  87. const hasFastSuffix = /-fast$/i.test(baseId);
  88. const hasCursorPrefix = baseId.toLowerCase().startsWith("cursor-");
  89. const effort = inferEffort(trimmed);
  90. if (trimmed.includes("[")) {
  91. return toCursorAcpModelValue(acp, inferFast(trimmed), effort || undefined);
  92. }
  93. if (!hasFastSuffix && !hasCursorPrefix && !effort) return acp;
  94. return toCursorAcpModelValue(
  95. acp,
  96. hasFastSuffix ? true : hasCursorPrefix ? false : undefined,
  97. effort || undefined,
  98. );
  99. }
  100. function resolveAcpArgvModel(argvModel, pinned) {
  101. const passed = asCliModel(argvModel);
  102. if (passed) return passed;
  103. return asCliModel(pinned);
  104. }
  105. function readPinnedModel() {
  106. const files = [];
  107. if (process.env.CURSOR_CONFIG_DIR) {
  108. files.push(path.join(process.env.CURSOR_CONFIG_DIR, "qmai-acp-model"));
  109. }
  110. files.push(path.join(__dirname, "qmai-acp-model"));
  111. for (const file of files) {
  112. try {
  113. const pinned = fs.readFileSync(file, "utf8").trim();
  114. if (pinned && pinned !== "default" && pinned !== "auto") return pinned;
  115. } catch {
  116. /* ignore */
  117. }
  118. }
  119. return "";
  120. }
  121. function withModel(args, model) {
  122. const next = args.slice();
  123. const i = next.indexOf("--model");
  124. if (i >= 0) {
  125. if (i + 1 < next.length) next[i + 1] = model;
  126. else next.push(model);
  127. return next;
  128. }
  129. const acp = next.indexOf("acp");
  130. if (acp >= 0) {
  131. next.splice(acp + 1, 0, "--model", model);
  132. }
  133. return next;
  134. }
  135. function rewriteAcpArgs(args, pinned) {
  136. if (!args.includes("acp")) return args;
  137. const i = args.indexOf("--model");
  138. const argvModel = i >= 0 && i + 1 < args.length ? args[i + 1] : "";
  139. const next = resolveAcpArgvModel(argvModel, pinned);
  140. return next ? withModel(args, next) : args;
  141. }
  142. function run(argv) {
  143. const real = process.env.QMAI_CURSOR_AGENT_REAL || bakedAgent();
  144. if (!real) {
  145. console.error("QMAI cursor agent wrapper: no real agent path");
  146. process.exit(1);
  147. }
  148. const args = rewriteAcpArgs(argv, readPinnedModel());
  149. const child = spawn(real, args, { stdio: "inherit" });
  150. child.on("error", (error) => {
  151. console.error(error);
  152. process.exit(1);
  153. });
  154. child.on("exit", (code, signal) => {
  155. if (signal) process.kill(process.pid, signal);
  156. process.exit(code ?? 1);
  157. });
  158. }
  159. if (require.main === module) {
  160. run(process.argv.slice(2));
  161. }
  162. module.exports = {
  163. cliModelToAcpValue,
  164. resolveAcpArgvModel,
  165. rewriteAcpArgs,
  166. };