@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.20",
3
+ "version": "0.11.22",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
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 = await resolveAgentRuntime(definition, selectedModel, {
3335
- env: options.env,
3336
- platform: options.platform,
3337
- existsSync: options.existsSync,
3338
- inspect: options.inspectRuntime,
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
- function runCommand(command, args, env, timeoutMs = 15_000) {
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(command, args, { env, timeout: timeoutMs }, (error, stdout, stderr) => {
114
- resolve({ error, output: `${stdout || ''}${stderr || ''}`.trim() });
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 const CURSOR_INSTALL_COMMAND = 'curl https://cursor.com/install -fsS | bash';
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
- const result = await run(command, ['status'], env);
149
- if (result.error?.code === 'ENOENT') return { installed: false, signedIn: false };
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 (platform === 'win32' || !confirm) {
155
- throw setupError(`Cursor CLI is not installed. Run \`${CURSOR_INSTALL_COMMAND}\`, then run this command again.`);
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 runInteractive('bash', ['-c', CURSOR_INSTALL_COMMAND], env, spawnImpl))) {
161
- throw setupError(`Cursor CLI install failed. Run \`${CURSOR_INSTALL_COMMAND}\` yourself, then run this command again.`);
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
- command = env.DEXTER_BRIDGE_CURSOR_BIN || 'cursor-agent',
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 version = await runCommand(command, ['--version'], env);
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: 'Cursor CLI is not installed. Run `curl https://cursor.com/install -fsS | bash`, then `cursor-agent login`.',
297
+ error: notInstalledMessage,
201
298
  };
202
299
  }
203
- const status = parseCursorStatus((await runCommand(command, ['status'], env)).output);
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') {
@@ -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: 'Install the Cursor CLI (curl https://cursor.com/install -fsS | bash), then restart the bridge.',
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
  );