@gakim-digital/dexter-bridge 0.11.17 → 0.11.20

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/README.md CHANGED
@@ -166,11 +166,18 @@ for support and automation:
166
166
  npx @gakim-digital/dexter-bridge pair 123456 --api http://localhost:3800/iwm-api/0.0.1
167
167
  npx @gakim-digital/dexter-bridge start --agent claude-code
168
168
  npx @gakim-digital/dexter-bridge start --agent codex
169
+ npx @gakim-digital/dexter-bridge start --agent cursor
169
170
  npx @gakim-digital/dexter-bridge status
170
171
  npx @gakim-digital/dexter-bridge doctor
171
172
  npx @gakim-digital/dexter-bridge logout
172
173
  ```
173
174
 
175
+ Cursor needs the Cursor CLI signed in on the same computer
176
+ (`curl https://cursor.com/install -fsS | bash`, then `cursor-agent login`).
177
+ The bridge runs `cursor-agent acp` with Cursor's shell, file, and web tools
178
+ denied, so Cursor can use only Dexter's Framer tools. Set
179
+ `DEXTER_BRIDGE_CURSOR_BIN` if `cursor-agent` is installed elsewhere.
180
+
174
181
  On Windows, the bridge checks the native Claude Code install at
175
182
  `%USERPROFILE%\.local\bin\claude.exe` in addition to `PATH`. Before pairing,
176
183
  verify the same PowerShell window can run:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.17",
3
+ "version": "0.11.20",
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
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import crypto from 'node:crypto';
2
3
  import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
@@ -27,6 +28,7 @@ import {
27
28
  } from './config.js';
28
29
  import {
29
30
  assertOutcomeResponseContract,
31
+ buildFramerOutcomePrompt,
30
32
  buildOutcomePrompt,
31
33
  buildModelTurnDeltaPrompt,
32
34
  buildModelTurnFallbackPrompt,
@@ -172,6 +174,15 @@ export const AGENT_DEFINITIONS = {
172
174
  model: 'codex:gpt-5.5',
173
175
  statusStage: 'codex_start',
174
176
  },
177
+ cursor: {
178
+ id: 'cursor',
179
+ label: 'Cursor',
180
+ commandEnv: 'DEXTER_BRIDGE_CURSOR_BIN',
181
+ fallbackCommand: 'cursor-agent',
182
+ promptMode: 'stdin',
183
+ model: 'cursor:grok-4.7',
184
+ statusStage: 'cursor_start',
185
+ },
175
186
  };
176
187
 
177
188
  export const AGENT_AUTHENTICATION_REQUIRED_CODE = 'DEXTER_AGENT_AUTHENTICATION_REQUIRED';
@@ -328,6 +339,23 @@ function claudeModelEngineSystemPrompt(product) {
328
339
  ].join(' ');
329
340
  }
330
341
 
342
+ // Content-addressed, so identical instructions reuse one file and stay
343
+ // byte-identical for Claude Code's prompt cache.
344
+ function writeClaudeSystemPromptFile(content) {
345
+ const directory = path.join(os.tmpdir(), 'instawebai-outcome-prompts');
346
+ const file = path.join(
347
+ directory,
348
+ `${crypto.createHash('sha256').update(content).digest('hex').slice(0, 32)}.md`,
349
+ );
350
+ if (!fs.existsSync(file)) {
351
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
352
+ const partial = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
353
+ fs.writeFileSync(partial, content, { mode: 0o600 });
354
+ fs.renameSync(partial, file);
355
+ }
356
+ return file;
357
+ }
358
+
331
359
  function claudeOutcomeSystemPrompt(product) {
332
360
  const name = normalizeBridgeProduct(product).name;
333
361
  if (normalizeBridgeProduct(product).id === 'dexter') {
@@ -337,7 +365,6 @@ function claudeOutcomeSystemPrompt(product) {
337
365
  'Inspect the project, perform the requested work, verify the result, and return the structured outcome.',
338
366
  'Do not access local credentials, unrelated files, other projects, account settings, or billing.',
339
367
  'Do not publish unless the assignment explicitly authorizes publishing.',
340
- 'Call progress_update before the first inspection or edit and at meaningful phase changes. Use one or two natural first-person sentences that name the specific page area, component, or behavior and the concrete change you are about to make or are making now; optionally say what you will check next. Avoid generic updates such as "working on the next step", "thinking", "loading", or "making changes".',
341
368
  'If framer_write is rejected by the safety boundary, do not retry JavaScript variants. Switch to framer_apply_changes, inspect the relevant state, or report the concrete blocker.',
342
369
  `Return only the structured outcome result requested by ${name}.`,
343
370
  ].join(' ');
@@ -510,13 +537,18 @@ function argsWithClaudeModelEngine(args, definition, options = {}, env = process
510
537
  let isolated = argsWithoutFlagValue(args, '--system-prompt');
511
538
  isolated = argsWithoutFlagValue(isolated, '--append-system-prompt');
512
539
  isolated = argsWithoutFlagValue(isolated, '--agent');
540
+ isolated = argsWithoutFlagValue(isolated, '--system-prompt-file');
513
541
  isolated = argsWithoutFlagValue(isolated, '--effort');
514
542
  return [
515
543
  ...isolated,
516
- '--system-prompt',
517
- options.harnessMode
518
- ? claudeOutcomeSystemPrompt(options.product)
519
- : claudeModelEngineSystemPrompt(options.product),
544
+ ...(options.harnessMode && options.systemPromptFile
545
+ ? ['--system-prompt-file', options.systemPromptFile]
546
+ : [
547
+ '--system-prompt',
548
+ options.harnessMode
549
+ ? claudeOutcomeSystemPrompt(options.product)
550
+ : claudeModelEngineSystemPrompt(options.product),
551
+ ]),
520
552
  '--effort',
521
553
  claudeEffortForStep(options.step, env),
522
554
  ];
@@ -864,7 +896,15 @@ export function processEnvWithCliPath(baseEnv = process.env, platform = process.
864
896
  baseEnv.APPDATA ? pathApi.join(baseEnv.APPDATA, 'npm') : '',
865
897
  baseEnv.LOCALAPPDATA ? pathApi.join(baseEnv.LOCALAPPDATA, 'Programs') : '',
866
898
  ]
867
- : ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
899
+ : [
900
+ homeDirectory ? pathApi.join(homeDirectory, '.local', 'bin') : '',
901
+ '/opt/homebrew/bin',
902
+ '/usr/local/bin',
903
+ '/usr/bin',
904
+ '/bin',
905
+ '/usr/sbin',
906
+ '/sbin',
907
+ ];
868
908
  const shellEnv = platform === 'win32' ? {} : loginShellEnv();
869
909
  const existingPath = String(baseEnv.PATH || '');
870
910
  const mergedPath = mergePathEntries([shellEnv.PATH, existingPath, ...fallbackPath], platform);
@@ -1393,6 +1433,7 @@ function definitionForRuntimeProfile(profile, fallbackAgent) {
1393
1433
  if (!profile) return definitionForAgent(fallbackAgent);
1394
1434
  if (profile.driver === 'claude-code-cli') return AGENT_DEFINITIONS['claude-code'];
1395
1435
  if (profile.driver === 'codex-app-server') return AGENT_DEFINITIONS.codex;
1436
+ if (profile.driver === 'cursor-acp') return AGENT_DEFINITIONS.cursor;
1396
1437
  return {
1397
1438
  id: profile.id || profile.driver,
1398
1439
  label: profile.label || profile.driver || 'Local runtime',
@@ -2314,7 +2355,15 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2314
2355
  1_000,
2315
2356
  ),
2316
2357
  );
2317
- const sessionKey = companionSessionKey(run);
2358
+ const framerAgentOutcome =
2359
+ run?.outcome?.kind === 'framer-project'
2360
+ || run?.outcome?.context?.executionMode === 'framer-agent';
2361
+ // Every Dexter message starts a fresh provider session. The assignment
2362
+ // already carries the recent conversation, and resuming would make every
2363
+ // turn of this run re-read the whole previous run as well.
2364
+ const sessionKey = framerAgentOutcome
2365
+ ? run?.runId || companionSessionKey(run)
2366
+ : companionSessionKey(run);
2318
2367
  const infrastructureAbortController = new AbortController();
2319
2368
  const controlMonitorAbort = new AbortController();
2320
2369
  let finished = false;
@@ -2462,9 +2511,15 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2462
2511
  Boolean(event.message || event.name) ||
2463
2512
  event.kind === 'turn_started';
2464
2513
  if (!meaningful) return;
2514
+ // Framer runs narrate in plain text rather than through a progress tool
2515
+ // (a tool call costs a whole turn), so that text is the user's progress.
2516
+ const narration =
2517
+ framerAgentOutcome
2518
+ && event.kind === 'output_delta'
2519
+ && String(event.activity || '').startsWith('assistant:');
2465
2520
  const userFacingUpdate =
2466
- event.kind === 'status_update'
2467
- && event.source === 'harness';
2521
+ (event.kind === 'status_update' && event.source === 'harness')
2522
+ || narration;
2468
2523
  void postActivity({
2469
2524
  stage: 'outcome',
2470
2525
  outcomeId: run?.outcome?.outcomeId,
@@ -2474,9 +2529,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2474
2529
  provider: definition.id,
2475
2530
  tool: event.name || undefined,
2476
2531
  toolPhase: event.phase || undefined,
2477
- activity:
2478
- event.activity ||
2479
- (userFacingUpdate ? `assistant-progress:${Date.now()}` : undefined),
2532
+ activity: narration
2533
+ ? event.activity.replace(/^assistant:/, 'assistant-progress:')
2534
+ : event.activity ||
2535
+ (userFacingUpdate ? `assistant-progress:${Date.now()}` : undefined),
2480
2536
  paths: event.paths || undefined,
2481
2537
  tokenUsage: event.tokenUsage || undefined,
2482
2538
  occurredAt: event.occurredAt || new Date().toISOString(),
@@ -2522,18 +2578,16 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2522
2578
  }
2523
2579
  };
2524
2580
  const controlMonitor = monitorControl();
2525
- const rememberedSession =
2526
- rememberedCompanionSession(sessionKey) ||
2527
- (run?.outcome?.session?.providerSessionId
2528
- ? {
2529
- providerSessionId: run.outcome.session.providerSessionId,
2530
- agent: definition.id,
2531
- updatedAt: Date.now(),
2532
- }
2533
- : null);
2534
- const framerAgentOutcome =
2535
- run?.outcome?.kind === 'framer-project'
2536
- || run?.outcome?.context?.executionMode === 'framer-agent';
2581
+ const rememberedSession = framerAgentOutcome
2582
+ ? null
2583
+ : rememberedCompanionSession(sessionKey) ||
2584
+ (run?.outcome?.session?.providerSessionId
2585
+ ? {
2586
+ providerSessionId: run.outcome.session.providerSessionId,
2587
+ agent: definition.id,
2588
+ updatedAt: Date.now(),
2589
+ }
2590
+ : null);
2537
2591
  const materialized = materializeOutcomeWorkspace(run?.outcome, {
2538
2592
  sessionId: run?.outcome?.context?.harnessSessionId || sessionKey,
2539
2593
  });
@@ -2677,11 +2731,34 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2677
2731
  cancellation,
2678
2732
  ])
2679
2733
  : null;
2680
- prompt = buildOutcomePrompt(executionOutcome, product, {
2734
+ const promptContext = {
2681
2735
  doctrine: run?.doctrine,
2682
2736
  doctrineVersion: run?.doctrineVersion,
2683
2737
  framerGuidance: framerGuidance?.content,
2684
- });
2738
+ projectInventory: framerGuidance?.inventory,
2739
+ };
2740
+ // Claude Code gets the run-independent instructions as its system prompt,
2741
+ // which it caches across sessions: later runs and follow-up messages read
2742
+ // them from the cache instead of writing them again.
2743
+ let systemPromptFile = null;
2744
+ let systemPromptBytes = 0;
2745
+ if (
2746
+ framerAgentOutcome
2747
+ && definition.id === 'claude-code'
2748
+ && !options.providerAdapter?.runOutcome
2749
+ ) {
2750
+ const { instructions, task } = buildFramerOutcomePrompt(
2751
+ executionOutcome,
2752
+ product,
2753
+ promptContext,
2754
+ );
2755
+ const systemPrompt = `${claudeOutcomeSystemPrompt(product)}\n\n${instructions}`;
2756
+ systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
2757
+ systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
2758
+ prompt = task;
2759
+ } else {
2760
+ prompt = buildOutcomePrompt(executionOutcome, product, promptContext);
2761
+ }
2685
2762
  const unresolvedTargets = preflight?.unresolvedTargets || [];
2686
2763
  if (unresolvedTargets.length > 0) {
2687
2764
  prompt = [
@@ -2696,7 +2773,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2696
2773
  harnessTools.configurePromptContext?.({
2697
2774
  doctrineLoaded: Boolean(run?.doctrine),
2698
2775
  doctrineVersion: run?.doctrineVersion,
2699
- promptBytes: Buffer.byteLength(prompt, 'utf8'),
2776
+ promptBytes: systemPromptBytes + Buffer.byteLength(prompt, 'utf8'),
2700
2777
  });
2701
2778
  const gateway =
2702
2779
  definition.id === 'codex' ? null : await harnessTools.startGateway();
@@ -2798,6 +2875,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2798
2875
  maxDurationMs: outcomeMaxDurationMs,
2799
2876
  cwd: materialized.directory,
2800
2877
  harnessMode: true,
2878
+ systemPromptFile,
2801
2879
  framerHarness: framerAgentOutcome ? executionOutcome : false,
2802
2880
  harnessToolNames: harnessTools.toolNames,
2803
2881
  nativeSkillsEnabled: Boolean(materializedNativeSkills),
@@ -2821,6 +2899,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2821
2899
  maxDurationMs: outcomeMaxDurationMs,
2822
2900
  cwd: materialized.directory,
2823
2901
  harnessMode: true,
2902
+ systemPromptFile,
2824
2903
  framerHarness: framerAgentOutcome ? executionOutcome : false,
2825
2904
  harnessToolNames: harnessTools.toolNames,
2826
2905
  nativeSkillsEnabled: Boolean(materializedNativeSkills),
@@ -3083,7 +3162,8 @@ export async function executeRun(
3083
3162
  });
3084
3163
  const runtimeProfile = run?.runtimeProfile || suppliedRuntimeProfile || null;
3085
3164
  const normalizedAgent = normalizeAgentName(
3086
- run?.companion?.agent || (runtimeProfile?.driver === 'claude-code-cli' ? 'claude-code' : agent),
3165
+ run?.companion?.agent
3166
+ || ({ 'claude-code-cli': 'claude-code', 'cursor-acp': 'cursor' }[runtimeProfile?.driver] || agent),
3087
3167
  );
3088
3168
  const runModel = runtimeProfile?.model?.id || run?.companion?.model?.id || run?.model || selectedModel;
3089
3169
  const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
@@ -3274,10 +3354,10 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3274
3354
  modelCatalogRefreshedAt,
3275
3355
  };
3276
3356
  }
3277
- if (normalizedAgent === 'codex') {
3357
+ if (normalizedAgent === 'codex' || normalizedAgent === 'cursor') {
3278
3358
  const adapter =
3279
3359
  options.providerAdapter
3280
- || createLocalAgentAdapter('codex', {
3360
+ || createLocalAgentAdapter(normalizedAgent, {
3281
3361
  env: options.env,
3282
3362
  });
3283
3363
  try {
@@ -3297,7 +3377,7 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3297
3377
  code: detection.installed === false ? 'DEXTER_AGENT_NOT_FOUND' : AGENT_AUTHENTICATION_REQUIRED_CODE,
3298
3378
  error:
3299
3379
  detection.error
3300
- || 'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
3380
+ || `${definition.label} is not signed in. Sign in on this computer, then run the bridge command again.`,
3301
3381
  modelCatalogSource: 'fallback',
3302
3382
  modelCatalogRefreshedAt,
3303
3383
  };
@@ -3377,6 +3457,7 @@ export async function checkAllAgents(options = {}) {
3377
3457
  const agents = await Promise.all([
3378
3458
  checkAgentAvailability('claude-code', undefined, options),
3379
3459
  checkAgentAvailability('codex', undefined, options),
3460
+ checkAgentAvailability('cursor', undefined, options),
3380
3461
  ]);
3381
3462
  return {
3382
3463
  agents,
package/src/cli.js CHANGED
@@ -16,7 +16,8 @@ import {
16
16
  resolveCompanionModelName,
17
17
  saveConfigPatch,
18
18
  } from './config.js';
19
- import { checkAgentAvailability, checkAllAgents, executeRun } from './agent.js';
19
+ import { checkAgentAvailability, checkAllAgents, executeRun, processEnvWithCliPath } from './agent.js';
20
+ import { setUpCursorCli } from './providers/cursorAcp.js';
20
21
  import {
21
22
  claimPairing,
22
23
  heartbeat,
@@ -55,11 +56,12 @@ function usage() {
55
56
  'Environment:',
56
57
  ' DEXTER_API_URL API base URL, e.g. https://api.example.com/iwm-api/0.0.1',
57
58
  ' DEXTER_BRIDGE_RUNTIME codex, claude-code, or opencode',
58
- ' DEXTER_BRIDGE_AGENT claude-code, codex, opencode, or dry-run',
59
+ ' DEXTER_BRIDGE_AGENT claude-code, codex, cursor, opencode, or dry-run',
59
60
  ' DEXTER_BRIDGE_MODEL Companion model, e.g. claude-code:sonnet or codex:gpt-5.5',
60
61
  ' DEXTER_BRIDGE_CLAUDE_BIN Claude CLI binary name/path',
61
62
  ' DEXTER_BRIDGE_CLAUDE_ARGS Claude CLI args, default: -p',
62
63
  ' DEXTER_BRIDGE_CODEX_BIN Codex CLI binary name/path',
64
+ ' DEXTER_BRIDGE_CURSOR_BIN Cursor CLI binary name/path, default: cursor-agent',
63
65
  ' DEXTER_BRIDGE_CODEX_MAX_THREADS Maximum retained App Server threads, default: 32',
64
66
  ' DEXTER_BRIDGE_LOG_DIR Log directory, default: ~/.dexter-bridge/logs',
65
67
  ].join('\n');
@@ -134,6 +136,28 @@ function requireDeviceToken(config) {
134
136
  return config.deviceToken;
135
137
  }
136
138
 
139
+ async function confirmPrompt(question) {
140
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
141
+ try {
142
+ const answer = (await rl.question(question)).trim().toLowerCase();
143
+ return answer === '' || answer === 'y' || answer === 'yes';
144
+ } catch {
145
+ return false;
146
+ } finally {
147
+ rl.close();
148
+ }
149
+ }
150
+
151
+ // One `connect --agent cursor` command installs and signs in to the Cursor CLI
152
+ // when needed, so the Cursor setup has no separate steps.
153
+ async function prepareRequestedAgent(flags, config) {
154
+ if (normalizeAgentName(flags.agent || config.agent) !== 'cursor') return;
155
+ await setUpCursorCli({
156
+ env: processEnvWithCliPath(agentEnvironment(config)),
157
+ confirm: process.stdin.isTTY ? confirmPrompt : null,
158
+ });
159
+ }
160
+
137
161
  async function promptForPairingCode() {
138
162
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
139
163
  try {
@@ -309,6 +333,9 @@ function agentEnvironment(config = {}, baseEnv = process.env) {
309
333
  if (!env.DEXTER_BRIDGE_CODEX_BIN && commands.codex) {
310
334
  env.DEXTER_BRIDGE_CODEX_BIN = commands.codex;
311
335
  }
336
+ if (!env.DEXTER_BRIDGE_CURSOR_BIN && commands.cursor) {
337
+ env.DEXTER_BRIDGE_CURSOR_BIN = commands.cursor;
338
+ }
312
339
  return env;
313
340
  }
314
341
 
@@ -316,11 +343,11 @@ function modelCatalogStatus(runtimeProfiles = []) {
316
343
  return Object.fromEntries(
317
344
  runtimeProfiles.flatMap((profile) => {
318
345
  const agent =
319
- profile.driver === 'codex-app-server'
320
- ? 'codex'
321
- : profile.driver === 'claude-code-cli'
322
- ? 'claude-code'
323
- : null;
346
+ {
347
+ 'codex-app-server': 'codex',
348
+ 'claude-code-cli': 'claude-code',
349
+ 'cursor-acp': 'cursor',
350
+ }[profile.driver] || null;
324
351
  return agent ? [[agent, profile.modelCatalog]] : [];
325
352
  }),
326
353
  );
@@ -466,7 +493,8 @@ function requireAvailableAgent(availability, agent, platform = process.platform)
466
493
  const check = selectedAgentCheck(availability, agent);
467
494
  if (check?.ok) return check;
468
495
 
469
- const label = agent === 'claude-code' ? 'Claude Code' : agent === 'codex' ? 'Codex' : 'The selected agent';
496
+ const label = { 'claude-code': 'Claude Code', codex: 'Codex', cursor: 'Cursor' }[agent]
497
+ || 'The selected agent';
470
498
  let message = check?.error || `${label} is not installed or could not be started.`;
471
499
  if (agent === 'claude-code' && check?.status === 'authentication_required') {
472
500
  message = check.error || 'Claude Code is not signed in. Run `claude auth login`, then try again.';
@@ -778,11 +806,11 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
778
806
  const runAgent = normalizeAgentName(
779
807
  poll.run?.companion?.agent
780
808
  || (
781
- runtimeProfile?.driver === 'claude-code-cli'
782
- ? 'claude-code'
783
- : runtimeProfile?.driver === 'opencode'
784
- ? 'opencode'
785
- : 'codex'
809
+ {
810
+ 'claude-code-cli': 'claude-code',
811
+ 'cursor-acp': 'cursor',
812
+ opencode: 'opencode',
813
+ }[runtimeProfile?.driver] || 'codex'
786
814
  ),
787
815
  );
788
816
  await executeClaimedRun({
@@ -865,6 +893,7 @@ export async function runCli(argv) {
865
893
  // The one-command production path: claim the pairing code (when given or
866
894
  // not yet paired), save config, and go straight into the polling loop.
867
895
  case 'connect':
896
+ await prepareRequestedAgent(parsed.flags, config);
868
897
  if (parsed.args[0]) {
869
898
  await pairCommand({ apiBaseUrl, args: parsed.args, flags: parsed.flags, configDir });
870
899
  }
package/src/config.js CHANGED
@@ -31,6 +31,7 @@ export const BRIDGE_BUILD_FINGERPRINT = crypto
31
31
  './providers/directByok.js',
32
32
  './providers/openCode.js',
33
33
  './providers/acp.js',
34
+ './providers/cursorAcp.js',
34
35
  ].map((relativePath) => {
35
36
  const url = new URL(relativePath, import.meta.url);
36
37
  return `${relativePath}\u0000${fs.readFileSync(url, 'utf8')}`;
@@ -52,6 +53,7 @@ export const BRIDGE_CAPABILITIES = [
52
53
  'native-skills-v1',
53
54
  'model-progress-v1',
54
55
  'model-catalog-refresh-v1',
56
+ 'cursor-acp-v1',
55
57
  ];
56
58
  export const BRIDGE_PRODUCTS = {
57
59
  dexter: {
@@ -69,7 +71,7 @@ export const BRIDGE_PRODUCTS = {
69
71
  // user explicitly connects the locally installed CLI and chooses one of its
70
72
  // reported models.
71
73
  export const DEFAULT_BRIDGE_AGENT = 'codex';
72
- export const BRIDGE_AGENTS = ['claude-code', 'codex', 'opencode', 'dry-run'];
74
+ export const BRIDGE_AGENTS = ['claude-code', 'codex', 'cursor', 'opencode', 'dry-run'];
73
75
  export const COMPANION_MODEL_DEFINITIONS = [
74
76
  {
75
77
  id: 'claude-code:fable',
@@ -162,6 +164,63 @@ export const COMPANION_MODEL_DEFINITIONS = [
162
164
  costTier: '$',
163
165
  description: 'Low-latency Codex model for quick coding iterations.',
164
166
  },
167
+ // Cursor lists every model it offers, whatever the user's plan; Dexter
168
+ // shows frontier models plus Composer. Paid Cursor plans include Grok and
169
+ // Composer; Claude, GPT and Gemini are billed from Cursor credits.
170
+ {
171
+ id: 'cursor:grok-4.7',
172
+ agent: 'cursor',
173
+ provider: 'cursor',
174
+ displayName: 'Grok 4.7',
175
+ invocationName: 'grok-4.7',
176
+ costTier: '$$$',
177
+ description: 'Grok 4.7, included in paid Cursor plans.',
178
+ },
179
+ {
180
+ id: 'cursor:composer-2.5',
181
+ agent: 'cursor',
182
+ provider: 'cursor',
183
+ displayName: 'Composer 2.5',
184
+ invocationName: 'composer-2.5',
185
+ costTier: '$$',
186
+ description: "Cursor's fast model, included in paid Cursor plans.",
187
+ },
188
+ {
189
+ id: 'cursor:claude-opus-5-5',
190
+ agent: 'cursor',
191
+ provider: 'cursor',
192
+ displayName: 'Claude Opus 5.5',
193
+ invocationName: 'claude-opus-5-5',
194
+ costTier: '$$$',
195
+ description: 'Claude Opus 5.5 via your Cursor plan.',
196
+ },
197
+ {
198
+ id: 'cursor:claude-fable-5-1',
199
+ agent: 'cursor',
200
+ provider: 'cursor',
201
+ displayName: 'Claude Fable 5.1',
202
+ invocationName: 'claude-fable-5-1',
203
+ costTier: '$$$',
204
+ description: 'Claude Fable 5.1 via your Cursor plan.',
205
+ },
206
+ {
207
+ id: 'cursor:gpt-5.6-sol',
208
+ agent: 'cursor',
209
+ provider: 'cursor',
210
+ displayName: 'GPT-5.6 Sol',
211
+ invocationName: 'gpt-5.6-sol',
212
+ costTier: '$$$',
213
+ description: 'GPT-5.6 Sol via your Cursor plan.',
214
+ },
215
+ {
216
+ id: 'cursor:gemini-4-argon',
217
+ agent: 'cursor',
218
+ provider: 'cursor',
219
+ displayName: 'Gemini 4 Argon',
220
+ invocationName: 'gemini-4-argon',
221
+ costTier: '$$$',
222
+ description: 'Gemini 4 Argon via your Cursor plan.',
223
+ },
165
224
  {
166
225
  id: 'dry-run:default',
167
226
  agent: 'dry-run',
@@ -176,6 +235,7 @@ export const COMPANION_MODEL_DEFINITIONS = [
176
235
  const DEFAULT_MODEL_BY_AGENT = {
177
236
  'claude-code': 'claude-code:sonnet',
178
237
  codex: 'codex:gpt-5.5',
238
+ cursor: 'cursor:grok-4.7',
179
239
  'dry-run': 'dry-run:default',
180
240
  };
181
241
 
@@ -237,6 +297,7 @@ export function normalizeAgentName(value, fallback = DEFAULT_BRIDGE_AGENT) {
237
297
  const raw = String(value || '').trim().toLowerCase();
238
298
  if (raw === 'claude' || raw === 'claude_code' || raw === 'claude-code') return 'claude-code';
239
299
  if (raw === 'openai-codex' || raw === 'openai_codex' || raw === 'codex') return 'codex';
300
+ if (raw === 'cursor' || raw === 'cursor-agent' || raw === 'cursor_agent') return 'cursor';
240
301
  if (raw === 'open-code' || raw === 'open_code' || raw === 'opencode') return 'opencode';
241
302
  if (raw === 'dryrun' || raw === 'dry_run' || raw === 'dry-run') return 'dry-run';
242
303
  return BRIDGE_AGENTS.includes(fallback) ? fallback : DEFAULT_BRIDGE_AGENT;
@@ -269,11 +330,11 @@ export function companionModelDefinition(value, agent = DEFAULT_BRIDGE_AGENT) {
269
330
  return {
270
331
  id: `${normalizedAgent}:${invocationName}`,
271
332
  agent: normalizedAgent,
272
- provider: normalizedAgent === 'codex' ? 'openai' : normalizedAgent === 'claude-code' ? 'anthropic' : 'companion',
333
+ provider: normalizedAgent === 'codex' ? 'openai' : normalizedAgent === 'claude-code' ? 'anthropic' : normalizedAgent === 'cursor' ? 'cursor' : 'companion',
273
334
  displayName: invocationName,
274
335
  invocationName,
275
336
  costTier: '$$',
276
- description: `${normalizedAgent === 'codex' ? 'Codex' : 'Claude Code'} model reported by the connected bridge.`,
337
+ description: `${{ codex: 'Codex', cursor: 'Cursor' }[normalizedAgent] || 'Claude Code'} model reported by the connected bridge.`,
277
338
  };
278
339
  }
279
340
  }