@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,519 @@
|
|
|
1
|
+
(function installCursorMaxModeGuard() {
|
|
2
|
+
const NAME = "__cursorMaxModeGuard";
|
|
3
|
+
|
|
4
|
+
if (window[NAME] && typeof window[NAME].uninstall === "function") {
|
|
5
|
+
window[NAME].uninstall();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const config = {
|
|
9
|
+
scanMs: 300,
|
|
10
|
+
sendDisableMs: 100,
|
|
11
|
+
menuOpenCooldownMs: 1200,
|
|
12
|
+
menuVerifyDelayMs: 180,
|
|
13
|
+
verifiedOffTtlMs: 3000,
|
|
14
|
+
loadedToastMs: 4000,
|
|
15
|
+
borderId: "cursor-max-input-warning-border",
|
|
16
|
+
loadedToastId: "cursor-max-mode-guard-loaded-toast",
|
|
17
|
+
styleId: "cursor-max-input-warning-style",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const state = {
|
|
21
|
+
installedAt: new Date().toISOString(),
|
|
22
|
+
badgeSeen: false,
|
|
23
|
+
composerHasText: false,
|
|
24
|
+
warningBorder: false,
|
|
25
|
+
sendBlocked: false,
|
|
26
|
+
sendControlsSeen: 0,
|
|
27
|
+
sendControlsDisabled: 0,
|
|
28
|
+
sendControlsRestored: 0,
|
|
29
|
+
lastMenuOpenAt: 0,
|
|
30
|
+
lastMaxModeClickAt: 0,
|
|
31
|
+
verifiedOffUntil: 0,
|
|
32
|
+
verifyInProgress: false,
|
|
33
|
+
last: null,
|
|
34
|
+
lastSend: null,
|
|
35
|
+
lastBlockedEvent: null,
|
|
36
|
+
timers: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function norm(text) {
|
|
40
|
+
return String(text || "").replace(/\s+/g, " ").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function textOf(el) {
|
|
44
|
+
return norm(el && el.textContent);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function visible(el) {
|
|
48
|
+
if (!el || !el.isConnected) return false;
|
|
49
|
+
const style = window.getComputedStyle(el);
|
|
50
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const rect = el.getBoundingClientRect();
|
|
54
|
+
return rect.width > 0 && rect.height > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasComposerContext(el) {
|
|
58
|
+
return Boolean(
|
|
59
|
+
el.closest(".composer-bar, .aislash-editor, .full-input-box, .anysphere-markdown-container-root") ||
|
|
60
|
+
el.closest("[class*='composer'], [class*='chat'], [class*='input']")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isStandaloneMaxBadge(el) {
|
|
65
|
+
if (!visible(el) || textOf(el) !== "MAX") return false;
|
|
66
|
+
if (el.closest("[role='menu'], [role='listbox'], .monaco-menu-container")) return false;
|
|
67
|
+
|
|
68
|
+
const cls = String(el.className || "");
|
|
69
|
+
if (cls.includes("ui-model-picker__trigger-max-badge")) return true;
|
|
70
|
+
if (cls.includes("max-badge") && hasComposerContext(el)) return true;
|
|
71
|
+
|
|
72
|
+
const trigger = el.closest("button, [role='button']");
|
|
73
|
+
return Boolean(trigger && /model-picker|trigger|max/i.test(String(trigger.className || "")));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function findMaxBadge() {
|
|
77
|
+
const preferred = Array.from(
|
|
78
|
+
document.querySelectorAll(".ui-model-picker__trigger-max-badge, .ui-model-picker__trigger-max-badge-text")
|
|
79
|
+
).find(isStandaloneMaxBadge);
|
|
80
|
+
if (preferred) return preferred;
|
|
81
|
+
|
|
82
|
+
return Array.from(document.querySelectorAll("span, div, button"))
|
|
83
|
+
.filter((el) => textOf(el) === "MAX")
|
|
84
|
+
.find(isStandaloneMaxBadge) || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function composerHasText() {
|
|
88
|
+
const candidates = Array.from(
|
|
89
|
+
document.querySelectorAll(
|
|
90
|
+
"textarea, [contenteditable='true'], [role='textbox'], .aislash-editor-input, .aislash-editor-placeholder"
|
|
91
|
+
)
|
|
92
|
+
).filter((el) => visible(el) && hasComposerContext(el));
|
|
93
|
+
|
|
94
|
+
return candidates.some((el) => {
|
|
95
|
+
const value = "value" in el ? el.value : el.textContent;
|
|
96
|
+
const text = norm(value)
|
|
97
|
+
.replace(/^Plan, Build, \/ for skills, @ for context$/i, "")
|
|
98
|
+
.replace(/^Ask, Build, .*$/i, "");
|
|
99
|
+
return text.length > 0;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function shouldBlockSendNow() {
|
|
104
|
+
const badge = findMaxBadge();
|
|
105
|
+
const hasText = composerHasText();
|
|
106
|
+
state.badgeSeen = Boolean(badge);
|
|
107
|
+
state.composerHasText = hasText;
|
|
108
|
+
state.sendBlocked = Boolean(badge && hasText);
|
|
109
|
+
return state.sendBlocked;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findMaxModeRow() {
|
|
113
|
+
const rows = Array.from(
|
|
114
|
+
document.querySelectorAll(
|
|
115
|
+
"[role='menuitemcheckbox'], [role='menuitem'], li, div, button"
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
return rows.find((el) => visible(el) && textOf(el) === "MAX Mode") || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function rowIsOn(row) {
|
|
122
|
+
return row && row.getAttribute("aria-checked") === "true";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function syntheticClick(el) {
|
|
126
|
+
const rect = el.getBoundingClientRect();
|
|
127
|
+
const x = rect.left + rect.width / 2;
|
|
128
|
+
const y = rect.top + rect.height / 2;
|
|
129
|
+
const hit = document.elementFromPoint(x, y);
|
|
130
|
+
const target = hit && el.contains(hit) ? hit : el;
|
|
131
|
+
const base = {
|
|
132
|
+
bubbles: true,
|
|
133
|
+
cancelable: true,
|
|
134
|
+
composed: true,
|
|
135
|
+
view: window,
|
|
136
|
+
clientX: x,
|
|
137
|
+
clientY: y,
|
|
138
|
+
screenX: window.screenX + x,
|
|
139
|
+
screenY: window.screenY + y,
|
|
140
|
+
button: 0,
|
|
141
|
+
buttons: 1,
|
|
142
|
+
detail: 1,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (typeof target.focus === "function") {
|
|
146
|
+
target.focus({ preventScroll: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof PointerEvent === "function") {
|
|
150
|
+
const pointer = {
|
|
151
|
+
...base,
|
|
152
|
+
pointerId: 1,
|
|
153
|
+
pointerType: "mouse",
|
|
154
|
+
isPrimary: true,
|
|
155
|
+
buttons: 1,
|
|
156
|
+
};
|
|
157
|
+
target.dispatchEvent(new PointerEvent("pointerover", pointer));
|
|
158
|
+
target.dispatchEvent(new PointerEvent("pointerenter", pointer));
|
|
159
|
+
target.dispatchEvent(new PointerEvent("pointermove", pointer));
|
|
160
|
+
target.dispatchEvent(new PointerEvent("pointerdown", pointer));
|
|
161
|
+
target.dispatchEvent(new PointerEvent("pointerup", { ...pointer, buttons: 0 }));
|
|
162
|
+
} else {
|
|
163
|
+
target.dispatchEvent(new MouseEvent("pointerdown", base));
|
|
164
|
+
target.dispatchEvent(new MouseEvent("pointerup", { ...base, buttons: 0 }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
target.dispatchEvent(new MouseEvent("mouseover", base));
|
|
168
|
+
target.dispatchEvent(new MouseEvent("mousemove", base));
|
|
169
|
+
target.dispatchEvent(new MouseEvent("mousedown", base));
|
|
170
|
+
target.dispatchEvent(new MouseEvent("mouseup", { ...base, buttons: 0 }));
|
|
171
|
+
target.dispatchEvent(new MouseEvent("click", { ...base, buttons: 0 }));
|
|
172
|
+
|
|
173
|
+
if (target !== el) {
|
|
174
|
+
el.dispatchEvent(new MouseEvent("click", { ...base, buttons: 0 }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof target.click === "function") {
|
|
178
|
+
target.click();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function pressEscape() {
|
|
183
|
+
document.dispatchEvent(
|
|
184
|
+
new KeyboardEvent("keydown", {
|
|
185
|
+
key: "Escape",
|
|
186
|
+
code: "Escape",
|
|
187
|
+
bubbles: true,
|
|
188
|
+
cancelable: true,
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function markVerifiedOff(row) {
|
|
194
|
+
const off = row && row.getAttribute("aria-checked") === "false";
|
|
195
|
+
if (off) {
|
|
196
|
+
state.verifiedOffUntil = Date.now() + config.verifiedOffTtlMs;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
state.last = {
|
|
200
|
+
step: "verify-max-mode-off",
|
|
201
|
+
checked: row ? row.getAttribute("aria-checked") : null,
|
|
202
|
+
off,
|
|
203
|
+
at: new Date().toISOString(),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function verifyMaxModeOff(row) {
|
|
208
|
+
const current = findMaxModeRow() || (row && row.isConnected ? row : null);
|
|
209
|
+
if (current) {
|
|
210
|
+
markVerifiedOff(current);
|
|
211
|
+
state.verifyInProgress = false;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const badge = findMaxBadge();
|
|
216
|
+
if (!badge) {
|
|
217
|
+
markVerifiedOff(null);
|
|
218
|
+
state.verifyInProgress = false;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const trigger = badge.closest("button, [role='button']") || badge;
|
|
223
|
+
syntheticClick(trigger);
|
|
224
|
+
state.lastMenuOpenAt = Date.now();
|
|
225
|
+
|
|
226
|
+
window.setTimeout(() => {
|
|
227
|
+
const reopenedRow = findMaxModeRow();
|
|
228
|
+
markVerifiedOff(reopenedRow);
|
|
229
|
+
if (reopenedRow && reopenedRow.getAttribute("aria-checked") === "false") {
|
|
230
|
+
pressEscape();
|
|
231
|
+
}
|
|
232
|
+
state.verifyInProgress = false;
|
|
233
|
+
}, config.menuVerifyDelayMs);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ensureMenuMaxModeOff() {
|
|
237
|
+
if (state.verifyInProgress || Date.now() < state.verifiedOffUntil) return;
|
|
238
|
+
|
|
239
|
+
const row = findMaxModeRow();
|
|
240
|
+
if (row) {
|
|
241
|
+
if (rowIsOn(row)) {
|
|
242
|
+
syntheticClick(row);
|
|
243
|
+
state.lastMaxModeClickAt = Date.now();
|
|
244
|
+
state.verifyInProgress = true;
|
|
245
|
+
state.last = {
|
|
246
|
+
step: "clicked-max-mode-row-to-turn-off",
|
|
247
|
+
checkedBefore: "true",
|
|
248
|
+
at: new Date().toISOString(),
|
|
249
|
+
};
|
|
250
|
+
window.setTimeout(() => verifyMaxModeOff(row), config.menuVerifyDelayMs);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
state.last = {
|
|
255
|
+
step: "max-mode-already-off",
|
|
256
|
+
checked: row.getAttribute("aria-checked"),
|
|
257
|
+
at: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const badge = findMaxBadge();
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
if (!badge || now - state.lastMenuOpenAt < config.menuOpenCooldownMs) return;
|
|
265
|
+
|
|
266
|
+
const trigger = badge.closest("button, [role='button']") || badge;
|
|
267
|
+
syntheticClick(trigger);
|
|
268
|
+
state.lastMenuOpenAt = now;
|
|
269
|
+
state.last = {
|
|
270
|
+
step: "clicked-badge-to-open-menu",
|
|
271
|
+
at: new Date().toISOString(),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ensureBorderStyle() {
|
|
276
|
+
if (document.getElementById(config.styleId)) return;
|
|
277
|
+
|
|
278
|
+
const style = document.createElement("style");
|
|
279
|
+
style.id = config.styleId;
|
|
280
|
+
style.textContent = `
|
|
281
|
+
#${config.borderId} {
|
|
282
|
+
position: fixed;
|
|
283
|
+
inset: 0;
|
|
284
|
+
z-index: 2147483646;
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#${config.borderId}::before {
|
|
289
|
+
content: "";
|
|
290
|
+
position: absolute;
|
|
291
|
+
inset: 0;
|
|
292
|
+
padding: 50px;
|
|
293
|
+
background:
|
|
294
|
+
radial-gradient(circle at 50% 50%, rgba(255, 40, 40, 0) 44%, rgba(255, 40, 40, .42) 66%, rgba(255, 30, 30, .9) 100%),
|
|
295
|
+
linear-gradient(135deg, rgba(255, 42, 42, .95), rgba(255, 149, 0, .72), rgba(255, 0, 84, .95));
|
|
296
|
+
-webkit-mask:
|
|
297
|
+
linear-gradient(#000 0 0) content-box,
|
|
298
|
+
linear-gradient(#000 0 0);
|
|
299
|
+
-webkit-mask-composite: xor;
|
|
300
|
+
mask-composite: exclude;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#${config.borderId}::after {
|
|
304
|
+
content: "";
|
|
305
|
+
position: absolute;
|
|
306
|
+
inset: 50px;
|
|
307
|
+
border-radius: 24px;
|
|
308
|
+
box-shadow: inset 0 0 26px rgba(255, 35, 35, .52), 0 0 20px rgba(255, 35, 35, .28);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.cursor-max-send-disabled {
|
|
312
|
+
pointer-events: none !important;
|
|
313
|
+
cursor: not-allowed !important;
|
|
314
|
+
opacity: .36 !important;
|
|
315
|
+
filter: grayscale(1) !important;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#${config.loadedToastId} {
|
|
319
|
+
position: fixed;
|
|
320
|
+
right: 16px;
|
|
321
|
+
bottom: 18px;
|
|
322
|
+
z-index: 2147483647;
|
|
323
|
+
max-width: min(360px, calc(100vw - 32px));
|
|
324
|
+
padding: 9px 12px;
|
|
325
|
+
border: 1px solid rgba(71, 207, 127, .45);
|
|
326
|
+
border-radius: 8px;
|
|
327
|
+
background: rgba(24, 28, 26, .92);
|
|
328
|
+
color: rgba(238, 255, 244, .96);
|
|
329
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, .28), inset 0 0 0 1px rgba(255, 255, 255, .04);
|
|
330
|
+
font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
331
|
+
letter-spacing: 0;
|
|
332
|
+
pointer-events: none;
|
|
333
|
+
opacity: 1;
|
|
334
|
+
transform: translateY(0);
|
|
335
|
+
transition: opacity .24s ease, transform .24s ease;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#${config.loadedToastId}[data-hiding="true"] {
|
|
339
|
+
opacity: 0;
|
|
340
|
+
transform: translateY(8px);
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
343
|
+
document.documentElement.appendChild(style);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function showLoadedToast() {
|
|
347
|
+
ensureBorderStyle();
|
|
348
|
+
|
|
349
|
+
const existing = document.getElementById(config.loadedToastId);
|
|
350
|
+
if (existing) {
|
|
351
|
+
existing.remove();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const toast = document.createElement("div");
|
|
355
|
+
toast.id = config.loadedToastId;
|
|
356
|
+
toast.textContent = "Cursor Extension loaded";
|
|
357
|
+
document.documentElement.appendChild(toast);
|
|
358
|
+
|
|
359
|
+
const hideTimer = window.setTimeout(() => {
|
|
360
|
+
toast.setAttribute("data-hiding", "true");
|
|
361
|
+
const removeTimer = window.setTimeout(() => {
|
|
362
|
+
toast.remove();
|
|
363
|
+
}, 280);
|
|
364
|
+
state.timers.push(removeTimer);
|
|
365
|
+
}, config.loadedToastMs);
|
|
366
|
+
|
|
367
|
+
state.timers.push(hideTimer);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setWarningBorder(on) {
|
|
371
|
+
ensureBorderStyle();
|
|
372
|
+
let border = document.getElementById(config.borderId);
|
|
373
|
+
|
|
374
|
+
if (on && !border) {
|
|
375
|
+
border = document.createElement("div");
|
|
376
|
+
border.id = config.borderId;
|
|
377
|
+
document.documentElement.appendChild(border);
|
|
378
|
+
} else if (!on && border) {
|
|
379
|
+
border.remove();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
state.warningBorder = Boolean(on);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function findSendControls() {
|
|
386
|
+
return Array.from(
|
|
387
|
+
document.querySelectorAll(".send-with-mode, [class*='send-with-mode'], [aria-label*='Send'], [title*='Send']")
|
|
388
|
+
).filter((el) => visible(el) && hasComposerContext(el));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function hardDisableSendControls() {
|
|
392
|
+
const block = shouldBlockSendNow();
|
|
393
|
+
const controls = findSendControls();
|
|
394
|
+
state.sendControlsSeen = controls.length;
|
|
395
|
+
|
|
396
|
+
let disabled = 0;
|
|
397
|
+
let restored = 0;
|
|
398
|
+
|
|
399
|
+
for (const el of controls) {
|
|
400
|
+
if (block) {
|
|
401
|
+
el.classList.add("cursor-max-send-disabled");
|
|
402
|
+
el.setAttribute("aria-disabled", "true");
|
|
403
|
+
el.setAttribute("data-cursor-max-send-disabled", "true");
|
|
404
|
+
el.setAttribute("tabindex", "-1");
|
|
405
|
+
disabled += 1;
|
|
406
|
+
} else if (el.getAttribute("data-cursor-max-send-disabled") === "true") {
|
|
407
|
+
el.classList.remove("cursor-max-send-disabled");
|
|
408
|
+
el.removeAttribute("aria-disabled");
|
|
409
|
+
el.removeAttribute("data-cursor-max-send-disabled");
|
|
410
|
+
el.removeAttribute("tabindex");
|
|
411
|
+
restored += 1;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
state.sendControlsDisabled = disabled;
|
|
416
|
+
state.sendControlsRestored = restored;
|
|
417
|
+
state.lastSend = {
|
|
418
|
+
blocked: block,
|
|
419
|
+
badgeSeen: state.badgeSeen,
|
|
420
|
+
composerHasText: state.composerHasText,
|
|
421
|
+
seen: controls.length,
|
|
422
|
+
disabled,
|
|
423
|
+
restored,
|
|
424
|
+
at: new Date().toISOString(),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function eventInsideComposer(event) {
|
|
429
|
+
return Boolean(event.target && event.target.closest && hasComposerContext(event.target));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function blockEvent(event) {
|
|
433
|
+
if (!shouldBlockSendNow()) return;
|
|
434
|
+
|
|
435
|
+
const sendControl = event.target && event.target.closest && event.target.closest(".send-with-mode, [class*='send-with-mode']");
|
|
436
|
+
const enterInComposer =
|
|
437
|
+
event.type === "keydown" &&
|
|
438
|
+
(event.key === "Enter" || event.code === "Enter" || event.code === "NumpadEnter") &&
|
|
439
|
+
!event.shiftKey &&
|
|
440
|
+
eventInsideComposer(event);
|
|
441
|
+
|
|
442
|
+
if (!sendControl && !enterInComposer) return;
|
|
443
|
+
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
event.stopPropagation();
|
|
446
|
+
event.stopImmediatePropagation();
|
|
447
|
+
|
|
448
|
+
state.lastBlockedEvent = {
|
|
449
|
+
type: event.type,
|
|
450
|
+
key: event.key || null,
|
|
451
|
+
code: event.code || null,
|
|
452
|
+
targetClass: String((event.target && event.target.className) || ""),
|
|
453
|
+
at: new Date().toISOString(),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function scan() {
|
|
458
|
+
const block = shouldBlockSendNow();
|
|
459
|
+
setWarningBorder(block);
|
|
460
|
+
|
|
461
|
+
if (state.badgeSeen) {
|
|
462
|
+
ensureMenuMaxModeOff();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
hardDisableSendControls();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function uninstall() {
|
|
469
|
+
for (const timer of state.timers) {
|
|
470
|
+
window.clearInterval(timer);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for (const type of ["pointerdown", "mousedown", "mouseup", "click", "keydown"]) {
|
|
474
|
+
window.removeEventListener(type, blockEvent, true);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
setWarningBorder(false);
|
|
478
|
+
document.getElementById(config.loadedToastId)?.remove();
|
|
479
|
+
for (const el of document.querySelectorAll("[data-cursor-max-send-disabled='true']")) {
|
|
480
|
+
el.classList.remove("cursor-max-send-disabled");
|
|
481
|
+
el.removeAttribute("aria-disabled");
|
|
482
|
+
el.removeAttribute("data-cursor-max-send-disabled");
|
|
483
|
+
el.removeAttribute("tabindex");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
for (const type of ["pointerdown", "mousedown", "mouseup", "click", "keydown"]) {
|
|
488
|
+
window.addEventListener(type, blockEvent, true);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
state.timers.push(window.setInterval(scan, config.scanMs));
|
|
492
|
+
state.timers.push(window.setInterval(hardDisableSendControls, config.sendDisableMs));
|
|
493
|
+
|
|
494
|
+
window[NAME] = {
|
|
495
|
+
config,
|
|
496
|
+
state,
|
|
497
|
+
scan,
|
|
498
|
+
ensureMenuMaxModeOff,
|
|
499
|
+
uninstall,
|
|
500
|
+
status() {
|
|
501
|
+
return {
|
|
502
|
+
installedAt: state.installedAt,
|
|
503
|
+
badgeSeen: state.badgeSeen,
|
|
504
|
+
composerHasText: state.composerHasText,
|
|
505
|
+
warningBorder: state.warningBorder,
|
|
506
|
+
sendBlocked: state.sendBlocked,
|
|
507
|
+
sendControlsSeen: state.sendControlsSeen,
|
|
508
|
+
sendControlsDisabled: state.sendControlsDisabled,
|
|
509
|
+
last: state.last,
|
|
510
|
+
lastSend: state.lastSend,
|
|
511
|
+
lastBlockedEvent: state.lastBlockedEvent,
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
showLoadedToast();
|
|
517
|
+
scan();
|
|
518
|
+
return window[NAME].status();
|
|
519
|
+
})();
|
|
@@ -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-max-mode-guard.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));
|