@srgay/cursor-extension 1.0.0
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/LICENSE +21 -0
- package/README.md +244 -0
- package/bin/cli.mjs +105 -0
- package/package.json +38 -0
- package/src/ime-enter-fix/cursor-ime-enter-fix.js +101 -0
- package/src/ime-enter-fix/install-cursor-ime-enter-fix.mjs +89 -0
- package/src/max-mode-guard/cursor-max-mode-guard.js +519 -0
- package/src/max-mode-guard/install-cursor-max-mode-guard.mjs +89 -0
- package/src/mcp-followup/cursor-mcp-followup.js +1636 -0
- package/src/mcp-followup/install-cursor-mcp-followup.mjs +93 -0
- package/src/shared/cursor-workbench-paths.mjs +46 -0
- package/src/shared/patch-cursor-workbench.mjs +177 -0
- package/src/shared/unpatch-cursor-workbench.mjs +96 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const port = Number(process.env.CURSOR_DEBUG_PORT || "9222");
|
|
8
|
+
const sourcePath = resolve(__dirname, "cursor-mcp-followup.js");
|
|
9
|
+
|
|
10
|
+
if (typeof WebSocket !== "function") {
|
|
11
|
+
throw new Error("This script needs Node.js with global WebSocket support. Use Node.js 20 or newer.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function json(url) {
|
|
15
|
+
const response = await fetch(url);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`${url} returned ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
return response.json();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pickWorkbenchTarget(targets) {
|
|
23
|
+
return targets.find((target) => {
|
|
24
|
+
return target.type === "page" && typeof target.url === "string" && target.url.includes("workbench.html");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function connect(wsUrl) {
|
|
29
|
+
const ws = new WebSocket(wsUrl);
|
|
30
|
+
let id = 0;
|
|
31
|
+
|
|
32
|
+
const opened = new Promise((resolveOpen, rejectOpen) => {
|
|
33
|
+
ws.addEventListener("open", resolveOpen, { once: true });
|
|
34
|
+
ws.addEventListener("error", rejectOpen, { once: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function call(method, params = {}) {
|
|
38
|
+
return new Promise((resolveCall, rejectCall) => {
|
|
39
|
+
const msgId = ++id;
|
|
40
|
+
const timer = setTimeout(() => rejectCall(new Error(`CDP call timed out: ${method}`)), 5000);
|
|
41
|
+
|
|
42
|
+
function onMessage(event) {
|
|
43
|
+
const data = JSON.parse(event.data);
|
|
44
|
+
if (data.id !== msgId) return;
|
|
45
|
+
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
ws.removeEventListener("message", onMessage);
|
|
48
|
+
if (data.error) {
|
|
49
|
+
rejectCall(new Error(JSON.stringify(data.error)));
|
|
50
|
+
} else {
|
|
51
|
+
resolveCall(data.result);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ws.addEventListener("message", onMessage);
|
|
56
|
+
ws.send(JSON.stringify({ id: msgId, method, params }));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ws, opened, call };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const targets = await json(`http://127.0.0.1:${port}/json/list`);
|
|
64
|
+
const target = pickWorkbenchTarget(targets);
|
|
65
|
+
|
|
66
|
+
if (!target) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Cursor workbench target was not found on port ${port}. Start Cursor with: open -na /Applications/Cursor.app --args --remote-debugging-port=${port}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 面板运行在浏览器环境(无 fs),改用 WS run_command 读取常用提示词;
|
|
73
|
+
// 这里在 Node 侧把占位符替换成 ui_settings.json 的真实绝对路径(shell=False 下 ~ 不展开)。
|
|
74
|
+
const settingsPath = join(homedir(), ".config", "mcp-feedback-enhanced", "ui_settings.json");
|
|
75
|
+
const source = (await readFile(sourcePath, "utf8")).replace("__MCP_SETTINGS_PATH__", settingsPath);
|
|
76
|
+
const client = connect(target.webSocketDebuggerUrl);
|
|
77
|
+
|
|
78
|
+
await client.opened;
|
|
79
|
+
await client.call("Runtime.enable");
|
|
80
|
+
|
|
81
|
+
const result = await client.call("Runtime.evaluate", {
|
|
82
|
+
expression: source,
|
|
83
|
+
returnByValue: true,
|
|
84
|
+
awaitPromise: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
client.ws.close();
|
|
88
|
+
|
|
89
|
+
if (result.exceptionDetails) {
|
|
90
|
+
throw new Error(result.exceptionDetails.text || "Injection failed");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(JSON.stringify(result.result?.value || result.result || {}, null, 2));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const relativeWorkbenchPath = "resources/app/out/vs/code/electron-sandbox/workbench";
|
|
6
|
+
|
|
7
|
+
function windowsLocalAppData() {
|
|
8
|
+
return process.env.LOCALAPPDATA || resolve(homedir(), "AppData/Local");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultWorkbenchDir() {
|
|
12
|
+
if (process.platform === "darwin") {
|
|
13
|
+
return "/Applications/Cursor.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
const candidates = [
|
|
18
|
+
resolve(windowsLocalAppData(), "Programs/Cursor", relativeWorkbenchPath),
|
|
19
|
+
resolve("C:/Program Files/cursor", relativeWorkbenchPath),
|
|
20
|
+
resolve("C:/Program Files/Cursor", relativeWorkbenchPath),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
return candidates.find((path) => existsSync(resolve(path, "workbench.html"))) || candidates[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Unsupported platform. Set CURSOR_WORKBENCH_DIR to Cursor's workbench directory."
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveWorkbenchPaths() {
|
|
32
|
+
const workbenchDir = process.env.CURSOR_WORKBENCH_DIR || defaultWorkbenchDir();
|
|
33
|
+
const appRoot = resolve(workbenchDir, "../../../../..");
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
appRoot,
|
|
37
|
+
workbenchDir,
|
|
38
|
+
workbenchHtml: resolve(workbenchDir, "workbench.html"),
|
|
39
|
+
guardTarget: resolve(workbenchDir, "cursor-max-mode-guard.js"),
|
|
40
|
+
followupTarget: resolve(workbenchDir, "cursor-mcp-followup.js"),
|
|
41
|
+
imeFixTarget: resolve(workbenchDir, "cursor-ime-enter-fix.js"),
|
|
42
|
+
backupHtml: resolve(workbenchDir, "workbench.html.cursor-max-guard.bak"),
|
|
43
|
+
productJson: resolve(appRoot, "product.json"),
|
|
44
|
+
backupProductJson: resolve(appRoot, "product.json.cursor-max-guard.bak"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFile, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveWorkbenchPaths } from "./cursor-workbench-paths.mjs";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
const guardSource = resolve(__dirname, "../max-mode-guard/cursor-max-mode-guard.js");
|
|
11
|
+
const followupSource = resolve(__dirname, "../mcp-followup/cursor-mcp-followup.js");
|
|
12
|
+
const imeFixSource = resolve(__dirname, "../ime-enter-fix/cursor-ime-enter-fix.js");
|
|
13
|
+
// 面板在浏览器环境无 fs,靠 WS run_command 读取 ui_settings.json;patch 时把占位符替换成真实绝对路径。
|
|
14
|
+
const settingsPath = join(homedir(), ".config", "mcp-feedback-enhanced", "ui_settings.json");
|
|
15
|
+
const {
|
|
16
|
+
workbenchDir,
|
|
17
|
+
workbenchHtml,
|
|
18
|
+
guardTarget,
|
|
19
|
+
followupTarget,
|
|
20
|
+
imeFixTarget,
|
|
21
|
+
backupHtml,
|
|
22
|
+
productJson,
|
|
23
|
+
backupProductJson,
|
|
24
|
+
} = resolveWorkbenchPaths();
|
|
25
|
+
|
|
26
|
+
const markerStart = "<!-- cursor-max-mode-guard:start -->";
|
|
27
|
+
const markerEnd = "<!-- cursor-max-mode-guard:end -->";
|
|
28
|
+
const injection = `${markerStart}
|
|
29
|
+
\t<script src="./cursor-max-mode-guard.js" defer></script>
|
|
30
|
+
\t${markerEnd}`;
|
|
31
|
+
|
|
32
|
+
const followupMarkerStart = "<!-- cursor-mcp-followup:start -->";
|
|
33
|
+
const followupMarkerEnd = "<!-- cursor-mcp-followup:end -->";
|
|
34
|
+
const followupInjection = `${followupMarkerStart}
|
|
35
|
+
\t<script src="./cursor-mcp-followup.js" defer></script>
|
|
36
|
+
\t${followupMarkerEnd}`;
|
|
37
|
+
|
|
38
|
+
const imeMarkerStart = "<!-- cursor-ime-enter-fix:start -->";
|
|
39
|
+
const imeMarkerEnd = "<!-- cursor-ime-enter-fix:end -->";
|
|
40
|
+
const imeInjection = `${imeMarkerStart}
|
|
41
|
+
\t<script src="./cursor-ime-enter-fix.js" defer></script>
|
|
42
|
+
\t${imeMarkerEnd}`;
|
|
43
|
+
|
|
44
|
+
function explainPermissionError(error) {
|
|
45
|
+
if (error && error.code === "EPERM" && process.platform === "win32") {
|
|
46
|
+
return [
|
|
47
|
+
"Permission denied while patching Cursor.",
|
|
48
|
+
"",
|
|
49
|
+
`Target: ${workbenchDir}`,
|
|
50
|
+
"",
|
|
51
|
+
"Cursor is installed under a protected Windows directory, such as C:\\Program Files.",
|
|
52
|
+
"Run PowerShell as Administrator and execute npm run patch again.",
|
|
53
|
+
"",
|
|
54
|
+
"Alternative:",
|
|
55
|
+
'$env:CURSOR_WORKBENCH_DIR="C:\\Path\\To\\Cursor\\resources\\app\\out\\vs\\code\\electron-sandbox\\workbench"',
|
|
56
|
+
"npm run patch",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function exists(path) {
|
|
64
|
+
try {
|
|
65
|
+
await stat(path);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checksumBase64NoPadding(content) {
|
|
73
|
+
return createHash("sha256").update(content).digest("base64").replace(/=+$/, "");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function updateProductChecksum() {
|
|
77
|
+
if (!(await exists(productJson))) {
|
|
78
|
+
return { updated: false, reason: "product.json not found" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const productText = await readFile(productJson, "utf8");
|
|
82
|
+
if (!(await exists(backupProductJson))) {
|
|
83
|
+
await writeFile(backupProductJson, productText);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const product = JSON.parse(productText);
|
|
87
|
+
product.checksums ||= {};
|
|
88
|
+
|
|
89
|
+
const checksumKey = "vs/code/electron-sandbox/workbench/workbench.html";
|
|
90
|
+
const htmlBuffer = await readFile(workbenchHtml);
|
|
91
|
+
const checksum = checksumBase64NoPadding(htmlBuffer);
|
|
92
|
+
const previous = product.checksums[checksumKey];
|
|
93
|
+
|
|
94
|
+
product.checksums[checksumKey] = checksum;
|
|
95
|
+
await writeFile(productJson, `${JSON.stringify(product, null, 2)}\n`);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
updated: previous !== checksum,
|
|
99
|
+
key: checksumKey,
|
|
100
|
+
previous,
|
|
101
|
+
checksum,
|
|
102
|
+
productJson,
|
|
103
|
+
backupProductJson,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const html = await readFile(workbenchHtml, "utf8");
|
|
109
|
+
|
|
110
|
+
if (!(await exists(backupHtml))) {
|
|
111
|
+
await writeFile(backupHtml, html);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await copyFile(guardSource, guardTarget);
|
|
115
|
+
|
|
116
|
+
// MCP 面板:不是单纯拷贝,需把 __MCP_SETTINGS_PATH__ 占位符替换成真实路径后再写入。
|
|
117
|
+
const followupCode = (await readFile(followupSource, "utf8")).replace("__MCP_SETTINGS_PATH__", settingsPath);
|
|
118
|
+
await writeFile(followupTarget, followupCode);
|
|
119
|
+
|
|
120
|
+
// 输入法回车修复:无占位符,直接拷贝。
|
|
121
|
+
await copyFile(imeFixSource, imeFixTarget);
|
|
122
|
+
|
|
123
|
+
const needle = '<script src="./workbench.js" type="module"></script>';
|
|
124
|
+
|
|
125
|
+
let nextHtml = html;
|
|
126
|
+
if (!nextHtml.includes(markerStart)) {
|
|
127
|
+
if (!nextHtml.includes(needle)) {
|
|
128
|
+
throw new Error(`Cannot find workbench startup script in ${workbenchHtml}`);
|
|
129
|
+
}
|
|
130
|
+
nextHtml = nextHtml.replace(needle, `${needle}\n\t${injection}`);
|
|
131
|
+
}
|
|
132
|
+
if (!nextHtml.includes(followupMarkerStart)) {
|
|
133
|
+
if (!nextHtml.includes(needle)) {
|
|
134
|
+
throw new Error(`Cannot find workbench startup script in ${workbenchHtml}`);
|
|
135
|
+
}
|
|
136
|
+
nextHtml = nextHtml.replace(needle, `${needle}\n\t${followupInjection}`);
|
|
137
|
+
}
|
|
138
|
+
if (!nextHtml.includes(imeMarkerStart)) {
|
|
139
|
+
if (!nextHtml.includes(needle)) {
|
|
140
|
+
throw new Error(`Cannot find workbench startup script in ${workbenchHtml}`);
|
|
141
|
+
}
|
|
142
|
+
nextHtml = nextHtml.replace(needle, `${needle}\n\t${imeInjection}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (nextHtml !== html) {
|
|
146
|
+
await writeFile(workbenchHtml, nextHtml);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const productChecksum = await updateProductChecksum();
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
JSON.stringify(
|
|
153
|
+
{
|
|
154
|
+
patched: true,
|
|
155
|
+
platform: process.platform,
|
|
156
|
+
workbenchDir,
|
|
157
|
+
workbenchHtml,
|
|
158
|
+
backupHtml,
|
|
159
|
+
guardTarget,
|
|
160
|
+
followupTarget,
|
|
161
|
+
imeFixTarget,
|
|
162
|
+
settingsPath,
|
|
163
|
+
productChecksum,
|
|
164
|
+
alreadyPatched: html.includes(markerStart),
|
|
165
|
+
followupAlreadyPatched: html.includes(followupMarkerStart),
|
|
166
|
+
imeFixAlreadyPatched: html.includes(imeMarkerStart),
|
|
167
|
+
},
|
|
168
|
+
null,
|
|
169
|
+
2
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((error) => {
|
|
175
|
+
console.error(explainPermissionError(error) || (error && error.stack) || error);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { copyFile, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolveWorkbenchPaths } from "./cursor-workbench-paths.mjs";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
workbenchDir,
|
|
6
|
+
workbenchHtml,
|
|
7
|
+
guardTarget,
|
|
8
|
+
followupTarget,
|
|
9
|
+
imeFixTarget,
|
|
10
|
+
backupHtml,
|
|
11
|
+
productJson,
|
|
12
|
+
backupProductJson,
|
|
13
|
+
} = resolveWorkbenchPaths();
|
|
14
|
+
|
|
15
|
+
const markerStart = "<!-- cursor-max-mode-guard:start -->";
|
|
16
|
+
const markerEnd = "<!-- cursor-max-mode-guard:end -->";
|
|
17
|
+
const followupMarkerStart = "<!-- cursor-mcp-followup:start -->";
|
|
18
|
+
const followupMarkerEnd = "<!-- cursor-mcp-followup:end -->";
|
|
19
|
+
const imeMarkerStart = "<!-- cursor-ime-enter-fix:start -->";
|
|
20
|
+
const imeMarkerEnd = "<!-- cursor-ime-enter-fix:end -->";
|
|
21
|
+
|
|
22
|
+
function explainPermissionError(error) {
|
|
23
|
+
if (error && error.code === "EPERM" && process.platform === "win32") {
|
|
24
|
+
return [
|
|
25
|
+
"Permission denied while restoring Cursor.",
|
|
26
|
+
"",
|
|
27
|
+
`Target: ${workbenchDir}`,
|
|
28
|
+
"",
|
|
29
|
+
"Cursor is installed under a protected Windows directory, such as C:\\Program Files.",
|
|
30
|
+
"Run PowerShell as Administrator and execute npm run unpatch again.",
|
|
31
|
+
"",
|
|
32
|
+
"Alternative:",
|
|
33
|
+
'$env:CURSOR_WORKBENCH_DIR="C:\\Path\\To\\Cursor\\resources\\app\\out\\vs\\code\\electron-sandbox\\workbench"',
|
|
34
|
+
"npm run unpatch",
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function exists(path) {
|
|
42
|
+
try {
|
|
43
|
+
await stat(path);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
if (await exists(backupHtml)) {
|
|
52
|
+
await copyFile(backupHtml, workbenchHtml);
|
|
53
|
+
} else {
|
|
54
|
+
const html = await readFile(workbenchHtml, "utf8");
|
|
55
|
+
const guardPattern = new RegExp(`\\n?\\t?${markerStart}[\\s\\S]*?${markerEnd}`, "m");
|
|
56
|
+
const followupPattern = new RegExp(`\\n?\\t?${followupMarkerStart}[\\s\\S]*?${followupMarkerEnd}`, "m");
|
|
57
|
+
const imePattern = new RegExp(`\\n?\\t?${imeMarkerStart}[\\s\\S]*?${imeMarkerEnd}`, "m");
|
|
58
|
+
await writeFile(
|
|
59
|
+
workbenchHtml,
|
|
60
|
+
html.replace(guardPattern, "").replace(followupPattern, "").replace(imePattern, "")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await rm(guardTarget, { force: true });
|
|
65
|
+
await rm(followupTarget, { force: true });
|
|
66
|
+
await rm(imeFixTarget, { force: true });
|
|
67
|
+
|
|
68
|
+
const backupProductJsonFound = await exists(backupProductJson);
|
|
69
|
+
if (backupProductJsonFound) {
|
|
70
|
+
await copyFile(backupProductJson, productJson);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(
|
|
74
|
+
JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
unpatched: true,
|
|
77
|
+
platform: process.platform,
|
|
78
|
+
workbenchDir,
|
|
79
|
+
workbenchHtml,
|
|
80
|
+
backupHtmlFound: await exists(backupHtml),
|
|
81
|
+
guardRemoved: guardTarget,
|
|
82
|
+
followupRemoved: followupTarget,
|
|
83
|
+
imeFixRemoved: imeFixTarget,
|
|
84
|
+
productJson,
|
|
85
|
+
backupProductJsonFound,
|
|
86
|
+
},
|
|
87
|
+
null,
|
|
88
|
+
2
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main().catch((error) => {
|
|
94
|
+
console.error(explainPermissionError(error) || (error && error.stack) || error);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
});
|