@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 srgay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Cursor Extension
|
|
2
|
+
|
|
3
|
+
一组本机 Cursor workbench 增强脚本,包含三部分:
|
|
4
|
+
|
|
5
|
+
1. **MAX Mode 守护**:保持 `MAX Mode` 关闭,并在彩色 `MAX` 标记出现且聊天框有内容时阻止发送。
|
|
6
|
+
2. **MCP Follow-up 面板**:在 Cursor 聊天框上方注入一个 MCP 反馈面板,连接 `mcp-feedback-enhanced` 的 WebSocket,让你直接在 Cursor 里回复 `interactive_feedback`,并支持端口自动扫描与自定义。
|
|
7
|
+
3. **输入法回车修复**:修复用中文输入法(拼音/注音等)组字时,按回车上屏候选词会被 Cursor 误当成「提交」的问题(含自带的 AskQuestion「Other」输入框)。
|
|
8
|
+
|
|
9
|
+
## 安装与使用(npx)
|
|
10
|
+
|
|
11
|
+
无需克隆仓库,直接用 `npx` 运行(需 Node.js ≥ 20):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 持久安装:patch Cursor 的 workbench.html,随 Cursor 启动自动加载三个脚本(推荐)
|
|
15
|
+
npx @srgay/cursor-extension install
|
|
16
|
+
|
|
17
|
+
# 还原安装
|
|
18
|
+
npx @srgay/cursor-extension uninstall
|
|
19
|
+
|
|
20
|
+
# CDP 临时注入(需先用调试端口启动 Cursor;重启 Cursor 后失效)
|
|
21
|
+
npx @srgay/cursor-extension inject # 注入全部三个
|
|
22
|
+
npx @srgay/cursor-extension inject followup # 只注入某一个:max | followup | ime | all
|
|
23
|
+
|
|
24
|
+
# 查看帮助
|
|
25
|
+
npx @srgay/cursor-extension help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `install` 后需**完整退出并重新打开 Cursor** 才生效。
|
|
29
|
+
- Cursor 更新可能覆盖 `workbench.html`,功能失效时重新执行 `install`。
|
|
30
|
+
- 自定义 Cursor 安装目录:设置环境变量 `CURSOR_WORKBENCH_DIR`。
|
|
31
|
+
- `inject` 自定义调试端口:设置环境变量 `CURSOR_DEBUG_PORT`(默认 `9222`)。
|
|
32
|
+
|
|
33
|
+
> 说明:下文「推荐方式 / 备用方式」里的 `npm run patch`、`npm run inject` 等是**本仓库源码开发**用法;最终用户用上面的 `npx` 命令即可,二者等价。
|
|
34
|
+
|
|
35
|
+
## 功能:MAX Mode 守护
|
|
36
|
+
|
|
37
|
+
- 检测彩色 `MAX` badge。
|
|
38
|
+
- 点击 badge 打开模型菜单。
|
|
39
|
+
- 只在菜单里的精确 `MAX Mode` 行是 `aria-checked="true"` 时点击关闭。
|
|
40
|
+
- 如果彩色 `MAX` 存在且聊天框有内容,显示 50px 红色渐变边框。
|
|
41
|
+
- 同一条件下禁用 `.send-with-mode` 发送控件,并拦截点击发送和回车发送。
|
|
42
|
+
- 脚本加载成功后,在 Cursor 右下角短暂显示 `Cursor Extension loaded`。
|
|
43
|
+
|
|
44
|
+
## 推荐方式:随 Cursor 启动加载
|
|
45
|
+
|
|
46
|
+
Patch Cursor 的 `workbench.html`,让三个脚本(**MAX Mode 守护**、**MCP Follow-up 面板**、**输入法回车修复**)都随 Cursor 启动自动加载,**无需调试端口、无需每次手动注入**:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run patch
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Patch 行为:
|
|
53
|
+
|
|
54
|
+
- 注入 `<script src="./cursor-max-mode-guard.js">`、`<script src="./cursor-mcp-followup.js">`、`<script src="./cursor-ime-enter-fix.js">` 到 `workbench.html`(各自带注释标记,便于幂等与回退)。
|
|
55
|
+
- 把 `cursor-mcp-followup.js` 拷入 workbench 目录时,将其中的 `__MCP_SETTINGS_PATH__` 占位符替换为本机真实路径 `~/.config/mcp-feedback-enhanced/ui_settings.json`(面板借 WS `run_command` 读写该文件,实现常用提示词与自动提交配置的查看/编辑)。
|
|
56
|
+
- 同步更新 Cursor `product.json` 里的 `workbench.html` checksum,避免 Cursor 把这次修改识别为安装损坏。
|
|
57
|
+
|
|
58
|
+
> 注意:占位符在 patch 时按当前用户主目录写死。若更换用户/主目录,重新执行 `npm run patch`。
|
|
59
|
+
|
|
60
|
+
然后完整退出 Cursor,再重新打开 Cursor。
|
|
61
|
+
|
|
62
|
+
恢复:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run unpatch
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Cursor 更新后可能会覆盖 `workbench.html`,如果功能失效,重新执行 `npm run patch`。
|
|
69
|
+
|
|
70
|
+
默认路径:
|
|
71
|
+
|
|
72
|
+
- macOS: `/Applications/Cursor.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench`
|
|
73
|
+
- Windows: 优先自动探测 `%LOCALAPPDATA%\Programs\Cursor\resources\app\out\vs\code\electron-sandbox\workbench`
|
|
74
|
+
- Windows: 如果用户目录不存在,会尝试 `C:\Program Files\cursor\resources\app\out\vs\code\electron-sandbox\workbench`
|
|
75
|
+
|
|
76
|
+
如果 Cursor 安装在其它目录,可以手动指定:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
CURSOR_WORKBENCH_DIR="/path/to/workbench" npm run patch
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Windows PowerShell:
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
$env:CURSOR_WORKBENCH_DIR="C:\Path\To\Cursor\resources\app\out\vs\code\electron-sandbox\workbench"
|
|
86
|
+
npm run patch
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
如果 Windows 报 `EPERM: operation not permitted`,说明 Cursor 安装在 `C:\Program Files` 等受保护目录。请用管理员身份打开 PowerShell 后重新执行:
|
|
90
|
+
|
|
91
|
+
```powershell
|
|
92
|
+
npm run patch
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 备用方式:临时注入
|
|
96
|
+
|
|
97
|
+
临时注入适合调试脚本,不推荐作为日常使用方式。它需要用调试端口启动 Cursor:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
open -na /Applications/Cursor.app --args --remote-debugging-port=9222
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
然后在本项目根目录执行:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run inject
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## MCP Follow-up 面板
|
|
110
|
+
|
|
111
|
+
在 Cursor 聊天输入框**上方**注入一个 MCP 反馈面板,连接 `mcp-feedback-enhanced` 的 WebSocket(`ws://127.0.0.1:{port}/ws`),让你直接在 Cursor 内回复 `interactive_feedback` 调用,无需切到独立的 WebUI。
|
|
112
|
+
|
|
113
|
+
### 功能
|
|
114
|
+
|
|
115
|
+
- 注入到聊天框上方,样式贴合 Cursor 原生输入框。
|
|
116
|
+
- 自动扫描端口 `8765–8769`,识别正在监听的 MCP 服务(连得上或返回 `4004 无 session` 即视为活跃)。
|
|
117
|
+
- 端口下拉:动态填充扫描到的活跃端口,末尾提供「自定义端口…」可手动输入任意端口。
|
|
118
|
+
- 下拉展开时每项显示「状态 · 项目名」(如 `Port 8766 · waiting · my-project`),收起时只显示「Port X · 状态」以贴合宽度,避免与右侧项目名重复。
|
|
119
|
+
- 放大镜按钮可随时重新扫描。
|
|
120
|
+
- 持续保持为 WebSocket「最后连接」(每 3 秒重连 + 输入框聚焦时重连),确保始终能收到当前会话并成功提交反馈。
|
|
121
|
+
- 底部「常用提示词」下拉:一键把 `ui_settings.json` 里的提示词追加到输入框;旁边刷新按钮可重新拉取配置。
|
|
122
|
+
- 齿轮按钮展开**配置抽屉**:
|
|
123
|
+
- **常用提示词**增删改(写回 `ui_settings.json`,删除带二次确认)。
|
|
124
|
+
- **自动提交**配置:启用开关、超时(秒)、选择提示词,状态实时回显。
|
|
125
|
+
- **自动提交运行时**:仅在「等待反馈」时倒计时,输入框右侧显示 `自动提交 m:ss` 小标签;到点自动以所选提示词发送(走与手动一致的重连抢占)。用户输入/手动插入提示词/手动发送/点击倒计时/会话离开等待态,本轮即取消自动提交。
|
|
126
|
+
- 配置读写均借现有 WebSocket 的 `run_command` 完成(读 `cat`、写 `python3` base64 解码落盘),不依赖本地文件系统访问或 CORS。
|
|
127
|
+
|
|
128
|
+
### 加载方式
|
|
129
|
+
|
|
130
|
+
- **推荐:随 Cursor 启动加载**(持久,不依赖调试端口)。执行 `npm run patch` 后完整重启 Cursor 即可,详见上文「推荐方式」。
|
|
131
|
+
- **备用:CDP 临时注入**(适合调试脚本,重启 Cursor 后失效)。先用调试端口启动 Cursor:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
open -na /Applications/Cursor.app --args --remote-debugging-port=9222
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
然后在本项目根目录执行:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run inject:followup
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> CDP 注入是临时的,**重启 Cursor 后需要重新执行**;`install-cursor-mcp-followup.mjs` 会在注入时替换 `__MCP_SETTINGS_PATH__` 占位符。
|
|
144
|
+
|
|
145
|
+
### 端口扫描与自定义
|
|
146
|
+
|
|
147
|
+
- 面板安装时自动扫描一次 `8765–8769`。
|
|
148
|
+
- 点击放大镜按钮可手动重扫。
|
|
149
|
+
- 在端口下拉里选择「自定义端口…」,就地输入端口号回车即可连接;输入框失焦或按 `Esc` 会回退到第一个可用端口。
|
|
150
|
+
|
|
151
|
+
### 查看状态
|
|
152
|
+
|
|
153
|
+
在 Cursor DevTools/CDP 里执行:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
window.__cursorMcpFollowup.status()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
返回挂载状态、当前连接端口、会话信息、扫描结果(`foundPorts`)、各端口项目名/状态等。
|
|
160
|
+
|
|
161
|
+
### 手动操作
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
window.__cursorMcpFollowup.scan() // 重新扫描端口
|
|
165
|
+
window.__cursorMcpFollowup.reconnect() // 重连当前选中端口
|
|
166
|
+
window.__cursorMcpFollowup.remount() // 重新挂载面板到聊天框上方
|
|
167
|
+
window.__cursorMcpFollowup.toggleConfig() // 展开/收起配置抽屉
|
|
168
|
+
window.__cursorMcpFollowup.uninstall() // 卸载面板
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 可调参数
|
|
172
|
+
|
|
173
|
+
脚本顶部 `config` 可调整:
|
|
174
|
+
|
|
175
|
+
- `scanStart` / `scanCount`:扫描起始端口与数量(默认从 `8765` 起、共 `5` 个,即 `8765–8769`)。
|
|
176
|
+
- `probeTimeoutMs`:单端口探测超时(默认 `1200` ms)。
|
|
177
|
+
- `reconnectMs`:自动重连间隔(默认 `3000` ms)。
|
|
178
|
+
|
|
179
|
+
### 排障
|
|
180
|
+
|
|
181
|
+
- **面板没出现**:确认 Cursor 用 `--remote-debugging-port=9222` 启动且聊天侧边栏已打开;可执行 `window.__cursorMcpFollowup.remount()`。
|
|
182
|
+
- **一直显示 Offline**:对应端口当前没有活跃的 MCP 会话;点放大镜重扫,或确认 `mcp-feedback-enhanced` 已在该端口监听。
|
|
183
|
+
- **收不到会话 / 提交无效**:面板靠定期重连抢占「最后连接」;若 Cursor 自带 WebUI 同时连着,聚焦输入框会触发立即重连抢回当前会话。
|
|
184
|
+
|
|
185
|
+
## 功能:输入法回车修复
|
|
186
|
+
|
|
187
|
+
修复用中文输入法(拼音/注音等)**组字时按回车上屏候选词被误当成「提交」**的问题。典型场景是 Cursor 自带的 **AskQuestion「Other / 其他」自定义输入框**(`textarea.composer-questionnaire-toolbar-freeform-input`)。
|
|
188
|
+
|
|
189
|
+
原理:在 `window` 捕获阶段最早拦截「组字进行中的回车」(依据 `isComposing` / `keyCode 229` / `compositionstart~end` 跟踪),`stopImmediatePropagation` 阻止其到达应用级回车提交处理器;**不调用 `preventDefault`**,保证输入法正常上屏。组字结束后回车一切照旧;代码编辑器(Monaco)自行处理组字,已跳过避免误伤。
|
|
190
|
+
|
|
191
|
+
### 加载方式
|
|
192
|
+
|
|
193
|
+
- **推荐:随 Cursor 启动加载**,执行 `npm run patch` 后重启 Cursor(见上文「推荐方式」)。
|
|
194
|
+
- **备用:CDP 临时注入**(重启失效):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
open -na /Applications/Cursor.app --args --remote-debugging-port=9222
|
|
198
|
+
npm run inject:ime
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 查看状态 / 卸载
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
window.__cursorImeEnterFix.status() // 含 guardedCount(已拦截次数)/ lastGuardedTarget(最近被拦输入框)
|
|
205
|
+
window.__cursorImeEnterFix.uninstall() // 卸载
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 目录
|
|
209
|
+
|
|
210
|
+
按功能分组,每个功能自成一夹,共享工具单独放在 `src/shared/`:
|
|
211
|
+
|
|
212
|
+
- `src/max-mode-guard/` — MAX Mode 守护
|
|
213
|
+
- `cursor-max-mode-guard.js`: 注入到 Cursor workbench 页面的守护脚本。
|
|
214
|
+
- `install-cursor-max-mode-guard.mjs`: 通过 CDP 临时注入 MAX Mode 守护脚本。
|
|
215
|
+
- `src/mcp-followup/` — MCP Follow-up 面板
|
|
216
|
+
- `cursor-mcp-followup.js`: MCP Follow-up 面板注入脚本。
|
|
217
|
+
- `install-cursor-mcp-followup.mjs`: 通过 CDP 临时注入 MCP Follow-up 面板。
|
|
218
|
+
- `src/ime-enter-fix/` — 输入法回车修复
|
|
219
|
+
- `cursor-ime-enter-fix.js`: 输入法组字回车修复脚本(全局生效)。
|
|
220
|
+
- `install-cursor-ime-enter-fix.mjs`: 通过 CDP 临时注入输入法回车修复。
|
|
221
|
+
- `src/shared/` — 共享工具
|
|
222
|
+
- `cursor-workbench-paths.mjs`: macOS/Windows workbench 路径解析。
|
|
223
|
+
- `patch-cursor-workbench.mjs`: patch Cursor 的 `workbench.html`(注入全部三个脚本)。
|
|
224
|
+
- `unpatch-cursor-workbench.mjs`: 恢复 patch。
|
|
225
|
+
|
|
226
|
+
## 查看状态(MAX Mode 守护)
|
|
227
|
+
|
|
228
|
+
在 Cursor DevTools/CDP 里执行:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
window.__cursorMaxModeGuard.status()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 页面内卸载(MAX Mode 守护)
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
window.__cursorMaxModeGuard.uninstall()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## 校验
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npm run check
|
|
244
|
+
```
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const root = resolve(here, "..");
|
|
8
|
+
|
|
9
|
+
const PATCH = "src/shared/patch-cursor-workbench.mjs";
|
|
10
|
+
const UNPATCH = "src/shared/unpatch-cursor-workbench.mjs";
|
|
11
|
+
const INJECT = {
|
|
12
|
+
max: "src/max-mode-guard/install-cursor-max-mode-guard.mjs",
|
|
13
|
+
followup: "src/mcp-followup/install-cursor-mcp-followup.mjs",
|
|
14
|
+
ime: "src/ime-enter-fix/install-cursor-ime-enter-fix.mjs",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const HELP = `cursor-extension — 本机 Cursor workbench 增强
|
|
18
|
+
(MAX Mode 守护 / MCP Follow-up 面板 / 输入法回车修复)
|
|
19
|
+
|
|
20
|
+
用法:
|
|
21
|
+
npx @srgay/cursor-extension <命令> [参数]
|
|
22
|
+
|
|
23
|
+
命令:
|
|
24
|
+
install patch Cursor 的 workbench.html,随 Cursor 启动自动加载三个脚本(持久,推荐)
|
|
25
|
+
uninstall 还原 install 的改动
|
|
26
|
+
inject [target] 通过 CDP 临时注入(重启 Cursor 后失效)
|
|
27
|
+
target: max | followup | ime | all(默认 all)
|
|
28
|
+
help 显示本帮助
|
|
29
|
+
|
|
30
|
+
别名:
|
|
31
|
+
patch = install unpatch = uninstall
|
|
32
|
+
|
|
33
|
+
环境变量:
|
|
34
|
+
CURSOR_WORKBENCH_DIR 手动指定 Cursor workbench 目录(install / uninstall)
|
|
35
|
+
CURSOR_DEBUG_PORT CDP 调试端口(inject,默认 9222)
|
|
36
|
+
|
|
37
|
+
示例:
|
|
38
|
+
npx @srgay/cursor-extension install
|
|
39
|
+
npx @srgay/cursor-extension uninstall
|
|
40
|
+
npx @srgay/cursor-extension inject # 注入全部三个
|
|
41
|
+
npx @srgay/cursor-extension inject followup # 只注入 MCP 面板
|
|
42
|
+
|
|
43
|
+
提示:
|
|
44
|
+
- install 后需完整退出并重新打开 Cursor 才生效。
|
|
45
|
+
- inject 需先用调试端口启动 Cursor,例如 macOS:
|
|
46
|
+
open -na /Applications/Cursor.app --args --remote-debugging-port=9222
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
function runScript(rel) {
|
|
50
|
+
const result = spawnSync(process.execPath, [resolve(root, rel)], {
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: process.env,
|
|
53
|
+
});
|
|
54
|
+
if (result.error) {
|
|
55
|
+
console.error(result.error.message);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
return result.status ?? 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runMany(rels) {
|
|
62
|
+
for (const rel of rels) {
|
|
63
|
+
const code = runScript(rel);
|
|
64
|
+
if (code !== 0) return code;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const [cmd, sub] = process.argv.slice(2);
|
|
70
|
+
let code = 0;
|
|
71
|
+
|
|
72
|
+
switch (cmd) {
|
|
73
|
+
case "install":
|
|
74
|
+
case "patch":
|
|
75
|
+
code = runScript(PATCH);
|
|
76
|
+
break;
|
|
77
|
+
case "uninstall":
|
|
78
|
+
case "unpatch":
|
|
79
|
+
code = runScript(UNPATCH);
|
|
80
|
+
break;
|
|
81
|
+
case "inject": {
|
|
82
|
+
const target = (sub || "all").toLowerCase();
|
|
83
|
+
const list = target === "all" ? ["max", "followup", "ime"] : [target];
|
|
84
|
+
const unknown = list.find((t) => !INJECT[t]);
|
|
85
|
+
if (unknown) {
|
|
86
|
+
console.error(`未知 inject 目标: ${unknown}(可选 max | followup | ime | all)`);
|
|
87
|
+
code = 1;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
code = runMany(list.map((t) => INJECT[t]));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "help":
|
|
94
|
+
case "--help":
|
|
95
|
+
case "-h":
|
|
96
|
+
case undefined:
|
|
97
|
+
console.log(HELP);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
console.error(`未知命令: ${cmd}\n`);
|
|
101
|
+
console.log(HELP);
|
|
102
|
+
code = 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.exit(code);
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@srgay/cursor-extension",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "本机 Cursor workbench 增强:MAX Mode 守护、MCP Follow-up 面板、输入法回车修复(支持随 Cursor 启动持久加载,或 CDP 临时注入)。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cursor-extension": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"check": "node --check src/max-mode-guard/cursor-max-mode-guard.js && node --check src/mcp-followup/cursor-mcp-followup.js && node --check src/ime-enter-fix/cursor-ime-enter-fix.js && node --check src/shared/cursor-workbench-paths.mjs && node --check src/max-mode-guard/install-cursor-max-mode-guard.mjs && node --check src/mcp-followup/install-cursor-mcp-followup.mjs && node --check src/ime-enter-fix/install-cursor-ime-enter-fix.mjs && node --check src/shared/patch-cursor-workbench.mjs && node --check src/shared/unpatch-cursor-workbench.mjs && node --check bin/cli.mjs",
|
|
20
|
+
"inject": "node src/max-mode-guard/install-cursor-max-mode-guard.mjs",
|
|
21
|
+
"inject:followup": "node src/mcp-followup/install-cursor-mcp-followup.mjs",
|
|
22
|
+
"inject:ime": "node src/ime-enter-fix/install-cursor-ime-enter-fix.mjs",
|
|
23
|
+
"patch": "node src/shared/patch-cursor-workbench.mjs",
|
|
24
|
+
"unpatch": "node src/shared/unpatch-cursor-workbench.mjs"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cursor",
|
|
28
|
+
"cursor-ide",
|
|
29
|
+
"workbench",
|
|
30
|
+
"mcp",
|
|
31
|
+
"mcp-feedback-enhanced",
|
|
32
|
+
"max-mode",
|
|
33
|
+
"ime",
|
|
34
|
+
"cli"
|
|
35
|
+
],
|
|
36
|
+
"author": "srgay",
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
(function installCursorImeEnterFix() {
|
|
2
|
+
const NAME = "__cursorImeEnterFix";
|
|
3
|
+
|
|
4
|
+
if (window[NAME] && typeof window[NAME].uninstall === "function") {
|
|
5
|
+
window[NAME].uninstall();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 全局修复输入法(拼音/注音等)组字期间按回车被当成「提交」的问题:
|
|
9
|
+
// 组字时的回车应仅用于上屏候选词,不应触发应用级回车提交(如 Cursor 自带的 AskQuestion「Other」输入框)。
|
|
10
|
+
// 做法:在 window 捕获阶段尽早拦截「组字中的回车」,stopImmediatePropagation 阻止其到达任何提交处理器;
|
|
11
|
+
// 不调用 preventDefault,保证输入法正常上屏。组字结束后回车一切照旧。
|
|
12
|
+
const state = {
|
|
13
|
+
installedAt: new Date().toISOString(),
|
|
14
|
+
composing: false,
|
|
15
|
+
guardedCount: 0, // 被拦截的组字回车次数(验证/排障用)
|
|
16
|
+
lastGuardedTarget: "", // 最近一次被拦截回车的目标元素描述(验证/排障用)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isTextEditable(el) {
|
|
20
|
+
if (!el || el.nodeType !== 1) return false;
|
|
21
|
+
// 代码编辑器(Monaco)自行处理组字,避免误伤,跳过。
|
|
22
|
+
if (typeof el.closest === "function" && el.closest(".monaco-editor")) return false;
|
|
23
|
+
const tag = el.tagName;
|
|
24
|
+
if (tag === "TEXTAREA") return true;
|
|
25
|
+
if (tag === "INPUT") {
|
|
26
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
27
|
+
return (
|
|
28
|
+
type === "text" ||
|
|
29
|
+
type === "search" ||
|
|
30
|
+
type === "url" ||
|
|
31
|
+
type === "email" ||
|
|
32
|
+
type === "tel" ||
|
|
33
|
+
type === "password" ||
|
|
34
|
+
type === "number" ||
|
|
35
|
+
type === ""
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (el.isContentEditable) return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function describe(el) {
|
|
43
|
+
if (!el || el.nodeType !== 1) return "";
|
|
44
|
+
let s = el.tagName.toLowerCase();
|
|
45
|
+
if (el.id) s += "#" + el.id;
|
|
46
|
+
if (typeof el.className === "string" && el.className.trim()) {
|
|
47
|
+
s += "." + el.className.trim().split(/\s+/).join(".");
|
|
48
|
+
}
|
|
49
|
+
return s.slice(0, 200);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onCompositionStart() {
|
|
53
|
+
state.composing = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onCompositionEnd() {
|
|
57
|
+
state.composing = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onKeyDownCapture(event) {
|
|
61
|
+
const isEnter = event.key === "Enter" || event.code === "Enter" || event.code === "NumpadEnter";
|
|
62
|
+
if (!isEnter) return;
|
|
63
|
+
|
|
64
|
+
// 组字判定:优先标准的 isComposing;keyCode 229 是输入法处理中的传统信号;
|
|
65
|
+
// state.composing 兜底(个别浏览器/输入法在提交回车的 keydown 上 isComposing 不可靠)。
|
|
66
|
+
const composing = state.composing || event.isComposing || event.keyCode === 229;
|
|
67
|
+
if (!composing) return;
|
|
68
|
+
|
|
69
|
+
if (!isTextEditable(event.target)) return;
|
|
70
|
+
|
|
71
|
+
event.stopImmediatePropagation();
|
|
72
|
+
state.guardedCount += 1;
|
|
73
|
+
state.lastGuardedTarget = describe(event.target);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
window.addEventListener("compositionstart", onCompositionStart, true);
|
|
77
|
+
window.addEventListener("compositionend", onCompositionEnd, true);
|
|
78
|
+
window.addEventListener("keydown", onKeyDownCapture, true);
|
|
79
|
+
|
|
80
|
+
function uninstall() {
|
|
81
|
+
window.removeEventListener("compositionstart", onCompositionStart, true);
|
|
82
|
+
window.removeEventListener("compositionend", onCompositionEnd, true);
|
|
83
|
+
window.removeEventListener("keydown", onKeyDownCapture, true);
|
|
84
|
+
delete window[NAME];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
window[NAME] = {
|
|
88
|
+
state,
|
|
89
|
+
uninstall,
|
|
90
|
+
status() {
|
|
91
|
+
return {
|
|
92
|
+
installedAt: state.installedAt,
|
|
93
|
+
composing: state.composing,
|
|
94
|
+
guardedCount: state.guardedCount,
|
|
95
|
+
lastGuardedTarget: state.lastGuardedTarget,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return window[NAME].status();
|
|
101
|
+
})();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const port = Number(process.env.CURSOR_DEBUG_PORT || "9222");
|
|
7
|
+
const sourcePath = resolve(__dirname, "cursor-ime-enter-fix.js");
|
|
8
|
+
|
|
9
|
+
if (typeof WebSocket !== "function") {
|
|
10
|
+
throw new Error("This script needs Node.js with global WebSocket support. Use Node.js 20 or newer.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function json(url) {
|
|
14
|
+
const response = await fetch(url);
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`${url} returned ${response.status}`);
|
|
17
|
+
}
|
|
18
|
+
return response.json();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pickWorkbenchTarget(targets) {
|
|
22
|
+
return targets.find((target) => {
|
|
23
|
+
return target.type === "page" && typeof target.url === "string" && target.url.includes("workbench.html");
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function connect(wsUrl) {
|
|
28
|
+
const ws = new WebSocket(wsUrl);
|
|
29
|
+
let id = 0;
|
|
30
|
+
|
|
31
|
+
const opened = new Promise((resolveOpen, rejectOpen) => {
|
|
32
|
+
ws.addEventListener("open", resolveOpen, { once: true });
|
|
33
|
+
ws.addEventListener("error", rejectOpen, { once: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function call(method, params = {}) {
|
|
37
|
+
return new Promise((resolveCall, rejectCall) => {
|
|
38
|
+
const msgId = ++id;
|
|
39
|
+
const timer = setTimeout(() => rejectCall(new Error(`CDP call timed out: ${method}`)), 5000);
|
|
40
|
+
|
|
41
|
+
function onMessage(event) {
|
|
42
|
+
const data = JSON.parse(event.data);
|
|
43
|
+
if (data.id !== msgId) return;
|
|
44
|
+
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
ws.removeEventListener("message", onMessage);
|
|
47
|
+
if (data.error) {
|
|
48
|
+
rejectCall(new Error(JSON.stringify(data.error)));
|
|
49
|
+
} else {
|
|
50
|
+
resolveCall(data.result);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ws.addEventListener("message", onMessage);
|
|
55
|
+
ws.send(JSON.stringify({ id: msgId, method, params }));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { ws, opened, call };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const targets = await json(`http://127.0.0.1:${port}/json/list`);
|
|
63
|
+
const target = pickWorkbenchTarget(targets);
|
|
64
|
+
|
|
65
|
+
if (!target) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Cursor workbench target was not found on port ${port}. Start Cursor with: open -na /Applications/Cursor.app --args --remote-debugging-port=${port}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const source = await readFile(sourcePath, "utf8");
|
|
72
|
+
const client = connect(target.webSocketDebuggerUrl);
|
|
73
|
+
|
|
74
|
+
await client.opened;
|
|
75
|
+
await client.call("Runtime.enable");
|
|
76
|
+
|
|
77
|
+
const result = await client.call("Runtime.evaluate", {
|
|
78
|
+
expression: source,
|
|
79
|
+
returnByValue: true,
|
|
80
|
+
awaitPromise: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
client.ws.close();
|
|
84
|
+
|
|
85
|
+
if (result.exceptionDetails) {
|
|
86
|
+
throw new Error(result.exceptionDetails.text || "Injection failed");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(JSON.stringify(result.result?.value || result.result || {}, null, 2));
|