@makerbi/remodex 2.0.0 → 2.3.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/bin/remodex.js +1 -1
- package/package.json +2 -2
- package/src/account-status.js +5 -4
- package/src/bridge.js +1809 -689
- package/src/codex-desktop-refresher.js +35 -7
- package/src/cursor-acp-client.js +242 -0
- package/src/cursor-models.js +134 -0
- package/src/cursor-provider.js +1197 -0
- package/src/desktop-ipc-action-follower.js +2323 -123
- package/src/desktop-ipc-conversation-adapter.js +1132 -0
- package/src/desktop-ipc-conversation-projector.js +1169 -0
- package/src/desktop-ipc-live-owner.js +1790 -0
- package/src/desktop-ipc-owner-transport.js +750 -0
- package/src/desktop-ipc-shared.js +473 -0
- package/src/desktop-ipc-state-patches.js +218 -0
- package/src/opencode-models.js +108 -0
- package/src/opencode-provider.js +1151 -0
- package/src/project-handler.js +50 -7
- package/src/project-registry.js +466 -0
- package/src/push-notification-tracker.js +4 -4
- package/src/rollout-live-mirror.js +946 -78
- package/src/rollout-turn-semantics.js +20 -0
- package/src/runtime-provider-models.js +164 -0
- package/src/runtime-provider-router.js +365 -0
- package/src/scripts/codex-refresh.applescript +26 -15
- package/src/secure-transport.js +204 -9
- package/src/session-jsonl-history.js +429 -39
- package/src/thread-context-handler.js +8 -6
- package/src/thread-runtime-settings-store.js +247 -0
- package/src/voice-audio.js +344 -0
- package/src/voice-handler.js +363 -173
|
@@ -24,6 +24,12 @@ const NEW_THREAD_DEEP_LINK = "codex://threads/new";
|
|
|
24
24
|
class CodexDesktopRefresher {
|
|
25
25
|
constructor({
|
|
26
26
|
enabled = true,
|
|
27
|
+
// When desktop IPC live sync streams conversation content, the refresher's
|
|
28
|
+
// only remaining job is navigation: bring Codex to the phone-driven thread
|
|
29
|
+
// when a phone message starts. Mid-run and completion refreshes are content
|
|
30
|
+
// reload workarounds from the pre-IPC era and would repeatedly deep-link
|
|
31
|
+
// and steal focus, so navigation-only mode drops them.
|
|
32
|
+
navigationOnly = false,
|
|
27
33
|
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
28
34
|
refreshCommand = "",
|
|
29
35
|
bundleId = DEFAULT_BUNDLE_ID,
|
|
@@ -40,6 +46,7 @@ class CodexDesktopRefresher {
|
|
|
40
46
|
customRefreshFailureThreshold = DEFAULT_CUSTOM_REFRESH_FAILURE_THRESHOLD,
|
|
41
47
|
} = {}) {
|
|
42
48
|
this.enabled = enabled;
|
|
49
|
+
this.navigationOnly = navigationOnly;
|
|
43
50
|
this.debounceMs = debounceMs;
|
|
44
51
|
this.refreshCommand = refreshCommand;
|
|
45
52
|
this.bundleId = bundleId;
|
|
@@ -82,8 +89,8 @@ class CodexDesktopRefresher {
|
|
|
82
89
|
this.unavailableLogged = false;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
handleInbound(rawMessage) {
|
|
86
|
-
const parsed = safeParseJSON(rawMessage);
|
|
92
|
+
handleInbound(rawMessage, parsedMessage = null) {
|
|
93
|
+
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
87
94
|
if (!parsed) {
|
|
88
95
|
return;
|
|
89
96
|
}
|
|
@@ -117,8 +124,8 @@ class CodexDesktopRefresher {
|
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
handleOutbound(rawMessage) {
|
|
121
|
-
const parsed = safeParseJSON(rawMessage);
|
|
127
|
+
handleOutbound(rawMessage, parsedMessage = null) {
|
|
128
|
+
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
122
129
|
if (!parsed) {
|
|
123
130
|
return;
|
|
124
131
|
}
|
|
@@ -126,6 +133,9 @@ class CodexDesktopRefresher {
|
|
|
126
133
|
const method = parsed.method;
|
|
127
134
|
if (method === "turn/completed") {
|
|
128
135
|
this.clearFallbackTimer();
|
|
136
|
+
if (this.navigationOnly) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
129
139
|
const turnId = extractTurnId(parsed);
|
|
130
140
|
if (turnId && turnId === this.lastTurnIdRefreshed) {
|
|
131
141
|
this.log(`refresh skipped (debounced): completion already refreshed for ${turnId}`);
|
|
@@ -317,6 +327,10 @@ class CodexDesktopRefresher {
|
|
|
317
327
|
this.bundleId,
|
|
318
328
|
this.appPath,
|
|
319
329
|
targetUrl || "",
|
|
330
|
+
// Navigation-only mode must never launch Codex from closed: content
|
|
331
|
+
// already syncs over IPC live sync, so a deep link while the app is
|
|
332
|
+
// down would only cold-start it to show a thread nobody asked to see.
|
|
333
|
+
this.navigationOnly ? "0" : "1",
|
|
320
334
|
]);
|
|
321
335
|
}
|
|
322
336
|
|
|
@@ -372,7 +386,7 @@ class CodexDesktopRefresher {
|
|
|
372
386
|
|
|
373
387
|
// Keeps one lightweight rollout watcher alive for the current Remodex-controlled thread.
|
|
374
388
|
ensureWatcher(threadId) {
|
|
375
|
-
if (!this.canRefresh() || !threadId) {
|
|
389
|
+
if (this.navigationOnly || !this.canRefresh() || !threadId) {
|
|
376
390
|
return;
|
|
377
391
|
}
|
|
378
392
|
|
|
@@ -558,12 +572,19 @@ function readBridgeConfig({
|
|
|
558
572
|
env
|
|
559
573
|
);
|
|
560
574
|
const explicitRefreshEnabled = readOptionalBooleanEnv(["REMODEX_REFRESH_ENABLED"], env);
|
|
575
|
+
const explicitDesktopIpcLiveSyncEnabled = readOptionalBooleanEnv(["REMODEX_DESKTOP_IPC_LIVE_SYNC"], env);
|
|
561
576
|
const explicitKeepMacAwakeEnabled = readOptionalBooleanEnv(["REMODEX_KEEP_MAC_AWAKE"], env);
|
|
562
577
|
const persistedKeepMacAwakeEnabled = typeof daemonConfig.keepMacAwakeEnabled === "boolean"
|
|
563
578
|
? daemonConfig.keepMacAwakeEnabled
|
|
564
579
|
: null;
|
|
565
|
-
//
|
|
566
|
-
|
|
580
|
+
// The deep-link refresh workaround stays opt-in; local IPC live sync is the primary desktop path.
|
|
581
|
+
// Once opted in, the persisted choice must survive restarts: `remodex restart`
|
|
582
|
+
// rewrites daemon-config.json from this computed config, so ignoring the
|
|
583
|
+
// persisted flag silently disabled the refresher on every restart.
|
|
584
|
+
const persistedRefreshEnabled = typeof daemonConfig.refreshEnabled === "boolean"
|
|
585
|
+
? daemonConfig.refreshEnabled
|
|
586
|
+
: null;
|
|
587
|
+
const defaultRefreshEnabled = persistedRefreshEnabled == null ? false : persistedRefreshEnabled;
|
|
567
588
|
return {
|
|
568
589
|
relayUrl,
|
|
569
590
|
relayAccessToken,
|
|
@@ -588,6 +609,13 @@ function readBridgeConfig({
|
|
|
588
609
|
: explicitKeepMacAwakeEnabled,
|
|
589
610
|
codexEndpoint,
|
|
590
611
|
desktopIpcSocketPath: readFirstDefinedEnv(["REMODEX_DESKTOP_IPC_SOCKET"], "", env),
|
|
612
|
+
desktopIpcLiveSyncEnabled: explicitDesktopIpcLiveSyncEnabled == null
|
|
613
|
+
? true
|
|
614
|
+
: explicitDesktopIpcLiveSyncEnabled,
|
|
615
|
+
desktopIpcSnapshotDebounceMs: parseIntegerEnv(
|
|
616
|
+
readFirstDefinedEnv(["REMODEX_DESKTOP_IPC_SNAPSHOT_DEBOUNCE_MS"], "75", env),
|
|
617
|
+
75
|
|
618
|
+
),
|
|
591
619
|
refreshCommand,
|
|
592
620
|
codexBundleId: readFirstDefinedEnv(["REMODEX_CODEX_BUNDLE_ID"], DEFAULT_BUNDLE_ID, env),
|
|
593
621
|
codexAppPath: DEFAULT_APP_PATH,
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// FILE: cursor-acp-client.js
|
|
2
|
+
// Purpose: Small JSON-RPC stdio client for Cursor's `cursor-agent acp` server.
|
|
3
|
+
// Layer: Bridge runtime provider transport
|
|
4
|
+
// Exports: createCursorAcpClient
|
|
5
|
+
// Depends on: child_process
|
|
6
|
+
|
|
7
|
+
const { spawn } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ACP_REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
|
|
11
|
+
function createCursorAcpClient({
|
|
12
|
+
command = "cursor-agent",
|
|
13
|
+
args = ["acp"],
|
|
14
|
+
cwd = process.cwd(),
|
|
15
|
+
env = process.env,
|
|
16
|
+
spawnImpl = spawn,
|
|
17
|
+
requestTimeoutMs = DEFAULT_ACP_REQUEST_TIMEOUT_MS,
|
|
18
|
+
onNotification = null,
|
|
19
|
+
onRequest = null,
|
|
20
|
+
} = {}) {
|
|
21
|
+
return new CursorAcpClient({
|
|
22
|
+
args,
|
|
23
|
+
command,
|
|
24
|
+
cwd,
|
|
25
|
+
env,
|
|
26
|
+
onNotification,
|
|
27
|
+
onRequest,
|
|
28
|
+
requestTimeoutMs,
|
|
29
|
+
spawnImpl,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class CursorAcpClient {
|
|
34
|
+
constructor({
|
|
35
|
+
command,
|
|
36
|
+
args,
|
|
37
|
+
cwd,
|
|
38
|
+
env,
|
|
39
|
+
spawnImpl,
|
|
40
|
+
requestTimeoutMs,
|
|
41
|
+
onNotification,
|
|
42
|
+
onRequest,
|
|
43
|
+
}) {
|
|
44
|
+
this.command = command;
|
|
45
|
+
this.args = args;
|
|
46
|
+
this.cwd = cwd;
|
|
47
|
+
this.env = env;
|
|
48
|
+
this.spawn = spawnImpl;
|
|
49
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
50
|
+
this.onNotification = onNotification;
|
|
51
|
+
this.onRequest = onRequest;
|
|
52
|
+
this.child = null;
|
|
53
|
+
this.nextRequestId = 1;
|
|
54
|
+
this.pendingRequests = new Map();
|
|
55
|
+
this.stdoutBuffer = "";
|
|
56
|
+
this.stderr = "";
|
|
57
|
+
this.closed = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
start() {
|
|
61
|
+
if (this.child) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.child = this.spawn(this.command, this.args, {
|
|
66
|
+
cwd: this.cwd,
|
|
67
|
+
env: this.env,
|
|
68
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
this.child.stdout?.setEncoding?.("utf8");
|
|
71
|
+
this.child.stderr?.setEncoding?.("utf8");
|
|
72
|
+
|
|
73
|
+
this.child.stdout?.on("data", (chunk) => this.handleStdout(chunk));
|
|
74
|
+
this.child.stderr?.on("data", (chunk) => {
|
|
75
|
+
this.stderr = truncateTail(`${this.stderr}${chunk}`, 4_000);
|
|
76
|
+
});
|
|
77
|
+
this.child.on("error", (error) => this.failAll(error));
|
|
78
|
+
this.child.on("close", (code, signal) => {
|
|
79
|
+
this.closed = true;
|
|
80
|
+
this.failAll(new Error(`Cursor ACP exited with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}.`));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
request(method, params = {}, timeoutMs = this.requestTimeoutMs) {
|
|
85
|
+
this.start();
|
|
86
|
+
const id = this.nextRequestId;
|
|
87
|
+
this.nextRequestId += 1;
|
|
88
|
+
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const timeout = setTimeout(() => {
|
|
91
|
+
this.pendingRequests.delete(id);
|
|
92
|
+
reject(new Error(`Cursor ACP request timed out: ${method}`));
|
|
93
|
+
}, timeoutMs);
|
|
94
|
+
|
|
95
|
+
this.pendingRequests.set(id, {
|
|
96
|
+
method,
|
|
97
|
+
reject,
|
|
98
|
+
resolve,
|
|
99
|
+
timeout,
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
this.writeFrame({ jsonrpc: "2.0", id, method, params });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
this.pendingRequests.delete(id);
|
|
106
|
+
reject(error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
notify(method, params = {}) {
|
|
112
|
+
this.start();
|
|
113
|
+
this.writeFrame({ jsonrpc: "2.0", method, params });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
respond(id, result) {
|
|
117
|
+
this.writeFrame({ jsonrpc: "2.0", id, result });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
rejectRequest(id, code, message) {
|
|
121
|
+
this.writeFrame({
|
|
122
|
+
jsonrpc: "2.0",
|
|
123
|
+
id,
|
|
124
|
+
error: {
|
|
125
|
+
code,
|
|
126
|
+
message,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
kill(signal = "SIGTERM") {
|
|
132
|
+
for (const waiter of this.pendingRequests.values()) {
|
|
133
|
+
clearTimeout(waiter.timeout);
|
|
134
|
+
waiter.reject(new Error("Cursor ACP client stopped."));
|
|
135
|
+
}
|
|
136
|
+
this.pendingRequests.clear();
|
|
137
|
+
try {
|
|
138
|
+
this.child?.kill(signal);
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore shutdown races; the process may already have exited.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
writeFrame(frame) {
|
|
145
|
+
if (this.closed || !this.child?.stdin?.writable) {
|
|
146
|
+
throw new Error("Cursor ACP stdin is not writable.");
|
|
147
|
+
}
|
|
148
|
+
this.child.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
handleStdout(chunk) {
|
|
152
|
+
this.stdoutBuffer += String(chunk);
|
|
153
|
+
for (;;) {
|
|
154
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
155
|
+
if (newlineIndex === -1) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
159
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
160
|
+
if (line) {
|
|
161
|
+
this.handleFrame(line);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
handleFrame(line) {
|
|
167
|
+
const frame = safeParseJSON(line);
|
|
168
|
+
if (!frame || typeof frame !== "object") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (frame.id != null && !frame.method) {
|
|
173
|
+
this.handleResponse(frame);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (frame.id != null && frame.method) {
|
|
178
|
+
this.handleRequest(frame);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (frame.method) {
|
|
183
|
+
this.onNotification?.(frame);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
handleResponse(frame) {
|
|
188
|
+
const waiter = this.pendingRequests.get(frame.id);
|
|
189
|
+
if (!waiter) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.pendingRequests.delete(frame.id);
|
|
194
|
+
clearTimeout(waiter.timeout);
|
|
195
|
+
if (frame.error) {
|
|
196
|
+
const error = new Error(frame.error.message || `Cursor ACP request failed: ${waiter.method}`);
|
|
197
|
+
error.code = frame.error.code;
|
|
198
|
+
error.data = frame.error.data;
|
|
199
|
+
waiter.reject(error);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
waiter.resolve(frame.result ?? null);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
handleRequest(frame) {
|
|
206
|
+
Promise.resolve()
|
|
207
|
+
.then(() => this.onRequest?.(frame))
|
|
208
|
+
.then((result) => {
|
|
209
|
+
if (result !== undefined) {
|
|
210
|
+
this.respond(frame.id, result);
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
this.rejectRequest(frame.id, error?.code || -32603, error?.message || "Cursor ACP client request failed.");
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
failAll(error) {
|
|
219
|
+
for (const waiter of this.pendingRequests.values()) {
|
|
220
|
+
clearTimeout(waiter.timeout);
|
|
221
|
+
waiter.reject(error);
|
|
222
|
+
}
|
|
223
|
+
this.pendingRequests.clear();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function safeParseJSON(rawValue) {
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(String(rawValue || ""));
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function truncateTail(value, maxChars) {
|
|
236
|
+
const text = String(value || "");
|
|
237
|
+
return text.length <= maxChars ? text : text.slice(-maxChars);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
createCursorAcpClient,
|
|
242
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// FILE: cursor-models.js
|
|
2
|
+
// Purpose: Converts Cursor ACP model config options into Remodex model/list entries.
|
|
3
|
+
// Layer: Bridge runtime provider helper
|
|
4
|
+
// Exports: Cursor provider constants plus ACP model parsing helpers.
|
|
5
|
+
// Depends on: ./runtime-provider-models
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
CURSOR_PROVIDER_ID,
|
|
9
|
+
buildRuntimeModelOption,
|
|
10
|
+
} = require("./runtime-provider-models");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CURSOR_MODEL = "composer-2.5";
|
|
13
|
+
|
|
14
|
+
function parseCursorModelsFromSessionResult(sessionResult = {}) {
|
|
15
|
+
const modelConfig = readModelConfigOption(sessionResult.configOptions);
|
|
16
|
+
if (!modelConfig) {
|
|
17
|
+
return [buildCursorModelOption(DEFAULT_CURSOR_MODEL, { name: "Composer 2.5" }, true)].filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const currentValue = readString(modelConfig.currentValue);
|
|
21
|
+
const options = flattenModelOptions(modelConfig.options);
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const models = [];
|
|
24
|
+
|
|
25
|
+
for (const option of options) {
|
|
26
|
+
const modelId = readString(option.value || option.modelId || option.id);
|
|
27
|
+
if (!modelId || seen.has(modelId)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
seen.add(modelId);
|
|
31
|
+
const model = buildCursorModelOption(modelId, option, modelId === currentValue);
|
|
32
|
+
if (model) {
|
|
33
|
+
models.push(model);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return models.length
|
|
38
|
+
? models
|
|
39
|
+
: [buildCursorModelOption(currentValue || DEFAULT_CURSOR_MODEL, { name: "Cursor" }, true)].filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readModelConfigOption(configOptions) {
|
|
43
|
+
const options = Array.isArray(configOptions) ? configOptions : [];
|
|
44
|
+
return options.find((option) => {
|
|
45
|
+
const category = readString(option?.category).toLowerCase();
|
|
46
|
+
const id = readString(option?.id).toLowerCase();
|
|
47
|
+
return category === "model" || id === "model";
|
|
48
|
+
}) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function flattenModelOptions(options) {
|
|
52
|
+
if (!Array.isArray(options)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const flattened = [];
|
|
57
|
+
for (const option of options) {
|
|
58
|
+
if (Array.isArray(option?.options)) {
|
|
59
|
+
flattened.push(...flattenModelOptions(option.options));
|
|
60
|
+
} else if (option && typeof option === "object") {
|
|
61
|
+
flattened.push(option);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return flattened;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildCursorModelOption(modelReference, option = {}, isDefault = false) {
|
|
68
|
+
const normalizedReference = normalizeCursorModelReference(modelReference);
|
|
69
|
+
if (!normalizedReference) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return buildRuntimeModelOption({
|
|
74
|
+
provider: CURSOR_PROVIDER_ID,
|
|
75
|
+
id: normalizedReference,
|
|
76
|
+
model: normalizedReference,
|
|
77
|
+
displayName: readString(option.name) || displayNameForCursorModel(normalizedReference),
|
|
78
|
+
description: readString(option.description) || `Cursor ACP model (${normalizedReference})`,
|
|
79
|
+
isDefault,
|
|
80
|
+
supportsFastMode: false,
|
|
81
|
+
supportedReasoningEfforts: [],
|
|
82
|
+
defaultReasoningEffort: null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeCursorModelReference(value) {
|
|
87
|
+
const normalized = readString(value);
|
|
88
|
+
if (!normalized || normalized.startsWith("{") || normalized.startsWith("[")) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
if (!/^[A-Za-z0-9._:-]+(?:\[[^\]\r\n]*\])?$/.test(normalized)) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function displayNameForCursorModel(modelReference) {
|
|
98
|
+
const normalized = normalizeCursorModelReference(modelReference);
|
|
99
|
+
const base = normalized.split("[")[0] || normalized;
|
|
100
|
+
const lowered = base.toLowerCase();
|
|
101
|
+
|
|
102
|
+
if (lowered === "default") {
|
|
103
|
+
return "Auto";
|
|
104
|
+
}
|
|
105
|
+
if (lowered.startsWith("gpt-")) {
|
|
106
|
+
return `GPT-${base.slice(4)}`;
|
|
107
|
+
}
|
|
108
|
+
return base
|
|
109
|
+
.split(/[-_]/)
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.map(titleCase)
|
|
112
|
+
.join(" ");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function titleCase(value) {
|
|
116
|
+
if (!value) {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readString(value) {
|
|
123
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
CURSOR_PROVIDER_ID,
|
|
128
|
+
DEFAULT_CURSOR_MODEL,
|
|
129
|
+
buildCursorModelOption,
|
|
130
|
+
displayNameForCursorModel,
|
|
131
|
+
normalizeCursorModelReference,
|
|
132
|
+
parseCursorModelsFromSessionResult,
|
|
133
|
+
readModelConfigOption,
|
|
134
|
+
};
|