@gakim-digital/dexter-bridge 0.11.20 → 0.11.22
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/package.json +1 -1
- package/src/agent.js +34 -6
- package/src/providers/cursorAcp.js +121 -20
- package/src/runtimeProfiles.js +2 -1
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
} from './nativeSkills.js';
|
|
53
53
|
import { createLocalAgentAdapter } from './providers/index.js';
|
|
54
54
|
import { discoverClaudeCodeModels } from './providers/claudeAgentSdk.js';
|
|
55
|
+
import { cursorInstallCommand, cursorLauncher, readCursorVersion } from './providers/cursorAcp.js';
|
|
55
56
|
import {
|
|
56
57
|
collectOutcomeWorkspaceChanges,
|
|
57
58
|
materializeModelTurnImages,
|
|
@@ -1531,6 +1532,8 @@ function controlCancellationSource(response) {
|
|
|
1531
1532
|
return 'unknown';
|
|
1532
1533
|
}
|
|
1533
1534
|
|
|
1535
|
+
const MAX_OUTCOME_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
1536
|
+
|
|
1534
1537
|
function boundedDurationMs(value, fallback, minimum = 10, maximum = 60 * 60 * 1000) {
|
|
1535
1538
|
const parsed = Number(value);
|
|
1536
1539
|
if (!Number.isFinite(parsed)) return fallback;
|
|
@@ -1734,6 +1737,26 @@ async function callProviderAdapter(
|
|
|
1734
1737
|
}
|
|
1735
1738
|
}
|
|
1736
1739
|
|
|
1740
|
+
// Cursor starts through its own launcher (on Windows, the bundled node.exe
|
|
1741
|
+
// behind cursor-agent.cmd), so its version is read through that launcher.
|
|
1742
|
+
async function resolveCursorRuntime(options = {}) {
|
|
1743
|
+
const platform = options.platform || process.platform;
|
|
1744
|
+
const launcher = cursorLauncher({
|
|
1745
|
+
env: processEnvWithCliPath(options.env || process.env, platform),
|
|
1746
|
+
platform,
|
|
1747
|
+
});
|
|
1748
|
+
const { error, output } = await readCursorVersion(launcher);
|
|
1749
|
+
if (error) {
|
|
1750
|
+
return {
|
|
1751
|
+
ok: false,
|
|
1752
|
+
command: launcher?.command || 'cursor-agent',
|
|
1753
|
+
code: 'DEXTER_AGENT_NOT_FOUND',
|
|
1754
|
+
error: `Cursor CLI is not installed. Run \`${cursorInstallCommand(platform)}\`, then \`cursor-agent login\`.`,
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
return { ok: true, command: launcher.command, output, version: parseAgentVersion(output) };
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1737
1760
|
export async function resolveAgentRuntime(definition, modelDefinition, options = {}) {
|
|
1738
1761
|
const platform = options.platform || process.platform;
|
|
1739
1762
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
@@ -2340,12 +2363,15 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2340
2363
|
? requestedModel.trim()
|
|
2341
2364
|
: definition.model
|
|
2342
2365
|
);
|
|
2366
|
+
// Dexter owns the run limit (a 2-hour safety limit for local runs), so it is
|
|
2367
|
+
// not clamped here; the inactivity timeout below is the stall guard.
|
|
2343
2368
|
const outcomeMaxDurationMs = boundedDurationMs(
|
|
2344
2369
|
run?.outcome?.maxDurationMs
|
|
2345
2370
|
?? options.maxDurationMs
|
|
2346
2371
|
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
2347
2372
|
20 * 60_000,
|
|
2348
2373
|
1_000,
|
|
2374
|
+
MAX_OUTCOME_DURATION_MS,
|
|
2349
2375
|
);
|
|
2350
2376
|
const outcomeInactivityTimeoutMs = Math.min(
|
|
2351
2377
|
outcomeMaxDurationMs,
|
|
@@ -3331,12 +3357,14 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
3331
3357
|
}
|
|
3332
3358
|
const definition = definitionForAgent(normalizedAgent);
|
|
3333
3359
|
const selectedModel = model ? companionModelDefinition(model?.id || model, normalizedAgent) : undefined;
|
|
3334
|
-
const runtime =
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3360
|
+
const runtime = normalizedAgent === 'cursor' && !options.inspectRuntime
|
|
3361
|
+
? await resolveCursorRuntime(options)
|
|
3362
|
+
: await resolveAgentRuntime(definition, selectedModel, {
|
|
3363
|
+
env: options.env,
|
|
3364
|
+
platform: options.platform,
|
|
3365
|
+
existsSync: options.existsSync,
|
|
3366
|
+
inspect: options.inspectRuntime,
|
|
3367
|
+
});
|
|
3340
3368
|
const supportedModels = companionModelsForAgent(normalizedAgent).filter((candidate) =>
|
|
3341
3369
|
modelSupportsAgentVersion(candidate, runtime.version),
|
|
3342
3370
|
);
|
|
@@ -108,15 +108,94 @@ export function parseCursorStatus(output) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
export const CURSOR_INSTALL_COMMAND = 'curl https://cursor.com/install -fsS | bash';
|
|
112
|
+
export const CURSOR_WINDOWS_INSTALL_COMMAND = "irm 'https://cursor.com/install?win32=true' | iex";
|
|
113
|
+
|
|
114
|
+
export function cursorInstallCommand(platform = process.platform) {
|
|
115
|
+
return platform === 'win32' ? CURSOR_WINDOWS_INSTALL_COMMAND : CURSOR_INSTALL_COMMAND;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const CURSOR_VERSION_DIRECTORY = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:-\d{2}-\d{2}-\d{2})?-[a-f0-9]+$/;
|
|
119
|
+
|
|
120
|
+
function cursorVersionRank(name) {
|
|
121
|
+
const [, year, month, day] = name.match(CURSOR_VERSION_DIRECTORY);
|
|
122
|
+
return Number(`${year}${month.padStart(2, '0')}${day.padStart(2, '0')}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Same choice as Cursor's own cursor-agent.ps1: a build next to the launcher,
|
|
126
|
+
// otherwise the newest build under versions\.
|
|
127
|
+
function newestCursorBuild(root, fileSystem) {
|
|
128
|
+
const hasBuild = (directory) =>
|
|
129
|
+
fileSystem.existsSync(path.win32.join(directory, 'node.exe'))
|
|
130
|
+
&& fileSystem.existsSync(path.win32.join(directory, 'index.js'));
|
|
131
|
+
if (hasBuild(root)) return root;
|
|
132
|
+
let names = [];
|
|
133
|
+
try {
|
|
134
|
+
names = fileSystem.readdirSync(path.win32.join(root, 'versions'));
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return names
|
|
139
|
+
.filter((name) => CURSOR_VERSION_DIRECTORY.test(name))
|
|
140
|
+
.sort((left, right) => cursorVersionRank(right) - cursorVersionRank(left) || right.localeCompare(left))
|
|
141
|
+
.map((name) => path.win32.join(root, 'versions', name))
|
|
142
|
+
.find(hasBuild) || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The command and leading arguments that start the Cursor CLI, or null when
|
|
147
|
+
* it is not installed. On Windows the CLI is a cursor-agent.cmd shim that runs
|
|
148
|
+
* PowerShell, which runs the node.exe bundled with the newest installed build.
|
|
149
|
+
* Node can start a .cmd only through a shell, and stopping that shell leaves
|
|
150
|
+
* Cursor running, so the bridge starts the bundled node.exe directly, as the
|
|
151
|
+
* shim does. The installer's folder is checked by path because a bridge that
|
|
152
|
+
* was already running does not see the PATH change the installer makes.
|
|
153
|
+
*/
|
|
154
|
+
export function cursorLauncher({ env = process.env, platform = process.platform, fileSystem = fs } = {}) {
|
|
155
|
+
const override = String(env.DEXTER_BRIDGE_CURSOR_BIN || '').trim();
|
|
156
|
+
if (platform !== 'win32') return { command: override || 'cursor-agent', args: [], env };
|
|
157
|
+
if (override && !/\.(cmd|bat|ps1)$/i.test(override)) return { command: override, args: [], env };
|
|
158
|
+
const roots = [
|
|
159
|
+
...(override ? [path.win32.dirname(override)] : []),
|
|
160
|
+
...String(env.PATH || env.Path || '')
|
|
161
|
+
.split(';')
|
|
162
|
+
.filter((directory) => directory && fileSystem.existsSync(path.win32.join(directory, 'cursor-agent.cmd'))),
|
|
163
|
+
...(env.LOCALAPPDATA ? [path.win32.join(env.LOCALAPPDATA, 'cursor-agent')] : []),
|
|
164
|
+
];
|
|
165
|
+
for (const root of new Set(roots)) {
|
|
166
|
+
const build = newestCursorBuild(root, fileSystem);
|
|
167
|
+
if (build) {
|
|
168
|
+
return {
|
|
169
|
+
command: path.win32.join(build, 'node.exe'),
|
|
170
|
+
args: [path.win32.join(build, 'index.js')],
|
|
171
|
+
env: { ...env, CURSOR_INVOKED_AS: 'cursor-agent.cmd' },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function notInstalledError(code = 'ENOENT') {
|
|
179
|
+
return Object.assign(new Error('Cursor CLI not found.'), { code });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function runCommand(launcher, args, timeoutMs = 15_000) {
|
|
183
|
+
if (!launcher) return Promise.resolve({ error: notInstalledError(), output: '' });
|
|
112
184
|
return new Promise((resolve) => {
|
|
113
|
-
execFile(
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
execFile(
|
|
186
|
+
launcher.command,
|
|
187
|
+
[...launcher.args, ...args],
|
|
188
|
+
{ env: launcher.env, timeout: timeoutMs, windowsHide: true },
|
|
189
|
+
(error, stdout, stderr) => {
|
|
190
|
+
resolve({ error, output: `${stdout || ''}${stderr || ''}`.trim() });
|
|
191
|
+
},
|
|
192
|
+
);
|
|
116
193
|
});
|
|
117
194
|
}
|
|
118
195
|
|
|
119
|
-
export
|
|
196
|
+
export function readCursorVersion(launcher) {
|
|
197
|
+
return runCommand(launcher, ['--version']);
|
|
198
|
+
}
|
|
120
199
|
|
|
121
200
|
function runInteractive(command, args, env, spawnImpl) {
|
|
122
201
|
return new Promise((resolve) => {
|
|
@@ -126,6 +205,17 @@ function runInteractive(command, args, env, spawnImpl) {
|
|
|
126
205
|
});
|
|
127
206
|
}
|
|
128
207
|
|
|
208
|
+
function installCursorCli(platform, env, spawnImpl) {
|
|
209
|
+
return platform === 'win32'
|
|
210
|
+
? runInteractive(
|
|
211
|
+
'powershell.exe',
|
|
212
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', CURSOR_WINDOWS_INSTALL_COMMAND],
|
|
213
|
+
env,
|
|
214
|
+
spawnImpl,
|
|
215
|
+
)
|
|
216
|
+
: runInteractive('bash', ['-c', CURSOR_INSTALL_COMMAND], env, spawnImpl);
|
|
217
|
+
}
|
|
218
|
+
|
|
129
219
|
function setupError(message) {
|
|
130
220
|
return Object.assign(new Error(message), { code: 'CURSOR_SETUP_INCOMPLETE', exitCode: 2 });
|
|
131
221
|
}
|
|
@@ -137,28 +227,31 @@ function setupError(message) {
|
|
|
137
227
|
*/
|
|
138
228
|
export async function setUpCursorCli({
|
|
139
229
|
env = process.env,
|
|
140
|
-
command = env.DEXTER_BRIDGE_CURSOR_BIN || 'cursor-agent',
|
|
141
230
|
confirm,
|
|
142
231
|
log = console.log,
|
|
143
232
|
platform = process.platform,
|
|
233
|
+
findCursor = () => cursorLauncher({ env, platform }),
|
|
144
234
|
run = runCommand,
|
|
145
235
|
spawnImpl = spawn,
|
|
146
236
|
} = {}) {
|
|
237
|
+
const installCommand = cursorInstallCommand(platform);
|
|
238
|
+
let launcher = null;
|
|
147
239
|
const readStatus = async () => {
|
|
148
|
-
|
|
149
|
-
|
|
240
|
+
launcher = findCursor();
|
|
241
|
+
const result = await run(launcher, ['status']);
|
|
242
|
+
if (!launcher || result.error?.code === 'ENOENT') return { installed: false, signedIn: false };
|
|
150
243
|
return { installed: true, ...parseCursorStatus(result.output) };
|
|
151
244
|
};
|
|
152
245
|
let status = await readStatus();
|
|
153
246
|
if (!status.installed) {
|
|
154
|
-
if (
|
|
155
|
-
throw setupError(`Cursor CLI is not installed. Run \`${
|
|
247
|
+
if (!confirm) {
|
|
248
|
+
throw setupError(`Cursor CLI is not installed. Run \`${installCommand}\`, then run this command again.`);
|
|
156
249
|
}
|
|
157
250
|
if (!(await confirm('Cursor CLI is not installed. Install it now? (Y/n) '))) {
|
|
158
251
|
throw setupError('Cursor CLI is required to connect Cursor to Dexter.');
|
|
159
252
|
}
|
|
160
|
-
if (!(await
|
|
161
|
-
throw setupError(`Cursor CLI install failed. Run \`${
|
|
253
|
+
if (!(await installCursorCli(platform, env, spawnImpl))) {
|
|
254
|
+
throw setupError(`Cursor CLI install failed. Run \`${installCommand}\` yourself, then run this command again.`);
|
|
162
255
|
}
|
|
163
256
|
status = await readStatus();
|
|
164
257
|
if (!status.installed) {
|
|
@@ -170,7 +263,7 @@ export async function setUpCursorCli({
|
|
|
170
263
|
throw setupError('Cursor is not signed in. Run `cursor-agent login`, then run this command again.');
|
|
171
264
|
}
|
|
172
265
|
log('Sign in to Cursor in your browser…');
|
|
173
|
-
await runInteractive(command, ['login'], env, spawnImpl);
|
|
266
|
+
await runInteractive(launcher.command, [...launcher.args, 'login'], launcher.env, spawnImpl);
|
|
174
267
|
status = await readStatus();
|
|
175
268
|
if (!status.signedIn) {
|
|
176
269
|
throw setupError('Cursor sign-in did not finish. Run this command again to retry.');
|
|
@@ -183,24 +276,28 @@ export async function setUpCursorCli({
|
|
|
183
276
|
export function createCursorAcpAdapter({
|
|
184
277
|
env = process.env,
|
|
185
278
|
trace,
|
|
186
|
-
|
|
279
|
+
platform = process.platform,
|
|
280
|
+
findCursor = () => cursorLauncher({ env, platform }),
|
|
187
281
|
models = [],
|
|
188
282
|
createClient = createJsonRpcClient,
|
|
189
283
|
} = {}) {
|
|
190
284
|
const activeTurns = new Map();
|
|
285
|
+
const notInstalledMessage =
|
|
286
|
+
`Cursor CLI is not installed. Run \`${cursorInstallCommand(platform)}\`, then \`cursor-agent login\`.`;
|
|
191
287
|
|
|
192
288
|
async function detect() {
|
|
193
|
-
const
|
|
289
|
+
const launcher = findCursor();
|
|
290
|
+
const version = await runCommand(launcher, ['--version']);
|
|
194
291
|
if (version.error) {
|
|
195
292
|
return {
|
|
196
293
|
ok: false,
|
|
197
294
|
installed: false,
|
|
198
295
|
signedIn: false,
|
|
199
296
|
agent: 'cursor',
|
|
200
|
-
error:
|
|
297
|
+
error: notInstalledMessage,
|
|
201
298
|
};
|
|
202
299
|
}
|
|
203
|
-
const status = parseCursorStatus((await runCommand(
|
|
300
|
+
const status = parseCursorStatus((await runCommand(launcher, ['status'])).output);
|
|
204
301
|
return {
|
|
205
302
|
ok: status.signedIn,
|
|
206
303
|
installed: true,
|
|
@@ -228,6 +325,10 @@ export function createCursorAcpAdapter({
|
|
|
228
325
|
}) {
|
|
229
326
|
const turnKey = sessionId || runId;
|
|
230
327
|
if (!turnKey) throw new Error('A Cursor turn requires a session id.');
|
|
328
|
+
const launcher = findCursor();
|
|
329
|
+
if (!launcher) {
|
|
330
|
+
throw Object.assign(new Error(notInstalledMessage), { code: 'DEXTER_AGENT_NOT_FOUND', retryable: false });
|
|
331
|
+
}
|
|
231
332
|
const temporaryWorkspace = cwd
|
|
232
333
|
? null
|
|
233
334
|
: fs.mkdtempSync(path.join(os.tmpdir(), 'dexter-cursor-'));
|
|
@@ -267,9 +368,9 @@ export function createCursorAcpAdapter({
|
|
|
267
368
|
};
|
|
268
369
|
|
|
269
370
|
const client = createClient({
|
|
270
|
-
command,
|
|
271
|
-
args: ['acp'],
|
|
272
|
-
env,
|
|
371
|
+
command: launcher.command,
|
|
372
|
+
args: [...launcher.args, 'acp'],
|
|
373
|
+
env: launcher.env,
|
|
273
374
|
cwd: workspace,
|
|
274
375
|
onServerRequest: async (message) => {
|
|
275
376
|
if (message.method === 'session/request_permission') {
|
package/src/runtimeProfiles.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { createLocalAgentAdapter } from './providers/index.js';
|
|
3
|
+
import { cursorInstallCommand } from './providers/cursorAcp.js';
|
|
3
4
|
|
|
4
5
|
const DRIVER_LABELS = {
|
|
5
6
|
'codex-app-server': 'Codex',
|
|
@@ -174,7 +175,7 @@ export function diagnosticsFromAgentCheck(check, env = process.env) {
|
|
|
174
175
|
check?.error || `${label} is not installed on this computer.`,
|
|
175
176
|
{
|
|
176
177
|
codex: 'Install Codex (npm i -g @openai/codex), then restart the bridge.',
|
|
177
|
-
cursor:
|
|
178
|
+
cursor: `Install the Cursor CLI (${cursorInstallCommand()}), then restart the bridge.`,
|
|
178
179
|
}[check?.agent]
|
|
179
180
|
|| 'Install Claude Code (npm i -g @anthropic-ai/claude-code), then restart the bridge.',
|
|
180
181
|
);
|