@rama_nigg/open-cursor 2.1.6 → 2.2.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/README.md CHANGED
@@ -15,20 +15,18 @@ No prompt limits. No broken streams. Full thinking + tool support in Opencode. Y
15
15
  curl -fsSL https://raw.githubusercontent.com/Nomadcxx/opencode-cursor/main/install.sh | bash
16
16
  ```
17
17
 
18
- Note: if `npm` is available, `install.sh` installs/upgrades `@rama_nigg/open-cursor` and configures OpenCode automatically. Otherwise it falls back to building from source.
19
-
20
18
  **Option B: npm Package (Recommended)**
21
19
 
22
20
  ```bash
23
21
  npm install -g @rama_nigg/open-cursor
24
- npm exec -- @rama_nigg/open-cursor install
22
+ open-cursor install
25
23
  ```
26
24
 
27
25
  Upgrade later with:
28
26
 
29
27
  ```bash
30
28
  npm update -g @rama_nigg/open-cursor
31
- npm exec -- @rama_nigg/open-cursor install
29
+ open-cursor install
32
30
  ```
33
31
 
34
32
  **Option C: TUI Installer**
@@ -48,7 +46,7 @@ Install the cursor-acp plugin for OpenCode:
48
46
 
49
47
  1. Preferred install (npm):
50
48
  npm install -g @rama_nigg/open-cursor
51
- npm exec -- @rama_nigg/open-cursor install
49
+ open-cursor install
52
50
 
53
51
  2. Fallback (build from source):
54
52
  git clone https://github.com/Nomadcxx/opencode-cursor.git
@@ -89,7 +87,39 @@ Or if you'd rather do it by hand, add this to `~/.config/opencode/opencode.json`
89
87
  "npm": "@ai-sdk/openai-compatible",
90
88
  "options": { "baseURL": "http://127.0.0.1:32124/v1" },
91
89
  "models": {
92
- "auto": { "name": "Auto" }
90
+ "auto": { "name": "Auto" },
91
+ "composer-1.5": { "name": "Composer 1.5" },
92
+ "composer-1": { "name": "Composer 1" },
93
+ "gpt-5.3-codex": { "name": "GPT-5.3 Codex" },
94
+ "gpt-5.3-codex-low": { "name": "GPT-5.3 Codex Low" },
95
+ "gpt-5.3-codex-high": { "name": "GPT-5.3 Codex High" },
96
+ "gpt-5.3-codex-xhigh": { "name": "GPT-5.3 Codex Extra High" },
97
+ "gpt-5.3-codex-fast": { "name": "GPT-5.3 Codex Fast" },
98
+ "gpt-5.3-codex-low-fast": { "name": "GPT-5.3 Codex Low Fast" },
99
+ "gpt-5.3-codex-high-fast": { "name": "GPT-5.3 Codex High Fast" },
100
+ "gpt-5.3-codex-xhigh-fast": { "name": "GPT-5.3 Codex Extra High Fast" },
101
+ "gpt-5.2": { "name": "GPT-5.2" },
102
+ "gpt-5.2-codex": { "name": "GPT-5.2 Codex" },
103
+ "gpt-5.2-codex-high": { "name": "GPT-5.2 Codex High" },
104
+ "gpt-5.2-codex-low": { "name": "GPT-5.2 Codex Low" },
105
+ "gpt-5.2-codex-xhigh": { "name": "GPT-5.2 Codex Extra High" },
106
+ "gpt-5.2-codex-fast": { "name": "GPT-5.2 Codex Fast" },
107
+ "gpt-5.2-codex-high-fast": { "name": "GPT-5.2 Codex High Fast" },
108
+ "gpt-5.2-codex-low-fast": { "name": "GPT-5.2 Codex Low Fast" },
109
+ "gpt-5.2-codex-xhigh-fast": { "name": "GPT-5.2 Codex Extra High Fast" },
110
+ "gpt-5.1-codex-max": { "name": "GPT-5.1 Codex Max" },
111
+ "gpt-5.1-codex-max-high": { "name": "GPT-5.1 Codex Max High" },
112
+ "opus-4.6-thinking": { "name": "Claude 4.6 Opus (Thinking)" },
113
+ "sonnet-4.5-thinking": { "name": "Claude 4.5 Sonnet (Thinking)" },
114
+ "gpt-5.2-high": { "name": "GPT-5.2 High" },
115
+ "opus-4.6": { "name": "Claude 4.6 Opus" },
116
+ "opus-4.5": { "name": "Claude 4.5 Opus" },
117
+ "opus-4.5-thinking": { "name": "Claude 4.5 Opus (Thinking)" },
118
+ "sonnet-4.5": { "name": "Claude 4.5 Sonnet" },
119
+ "gpt-5.1-high": { "name": "GPT-5.1 High" },
120
+ "gemini-3-pro": { "name": "Gemini 3 Pro" },
121
+ "gemini-3-flash": { "name": "Gemini 3 Flash" },
122
+ "grok": { "name": "Grok" }
93
123
  }
94
124
  }
95
125
  }
@@ -131,28 +161,6 @@ opencode run "your prompt" --model cursor-acp/auto
131
161
  opencode run "your prompt" --model cursor-acp/sonnet-4.5
132
162
  ```
133
163
 
134
- ## Models
135
-
136
- Models are pulled from `cursor-agent models` and written to your config during installation. If Cursor adds new models later:
137
-
138
- ```bash
139
- npm exec -- @rama_nigg/open-cursor install
140
- ```
141
-
142
- Or, if you installed from source (no npm CLI):
143
-
144
- ```bash
145
- ./scripts/sync-models.sh
146
- ```
147
-
148
- The proxy also exposes a `/v1/models` endpoint that fetches models in real-time:
149
-
150
- ```bash
151
- curl http://127.0.0.1:32124/v1/models
152
- ```
153
-
154
- Common models: `auto`, `composer-1.5`, `gpt-5.3-codex`, `opus-4.6-thinking`, `sonnet-4.5`, `gemini-3-pro`, `grok`
155
-
156
164
  ## Architecture
157
165
 
158
166
  ```mermaid
@@ -196,15 +204,6 @@ Detailed architecture: [docs/architecture/runtime-tool-loop.md](docs/architectur
196
204
  | **Dependencies** | bun, cursor-agent | npm | bun, cursor-agent | Node.js 18+ |
197
205
  | **Port** | 32124 | 18741 | 32123 | 4141 |
198
206
 
199
- **Key advantages of cursor-acp:**
200
-
201
- - Avoids E2BIG errors with large prompts (uses HTTP body, not CLI args)
202
- - Structured error parsing with actionable suggestions
203
- - Cross-platform (not locked to macOS Keychain)
204
- - TUI installer for easy setup
205
- - Native tool calling with 10 built-in tools, SDK/MCP executor support, and a skills/alias system
206
- - Uses official cursor-agent CLI (more stable than reverse-engineering Connect-RPC)
207
-
208
207
  ## Prerequisites
209
208
 
210
209
  - [Bun](https://bun.sh/)
@@ -247,10 +246,6 @@ Integration CI pins OpenCode-owned loop mode to deterministic settings:
247
246
  - `CURSOR_ACP_FORWARD_TOOL_CALLS=false`
248
247
  - `CURSOR_ACP_EMIT_TOOL_UPDATES=false`
249
248
 
250
- ## Publishing
251
-
252
- For maintainers, release and npm publish steps are documented in [docs/PUBLISHING.md](docs/PUBLISHING.md).
253
-
254
249
  ## Troubleshooting
255
250
 
256
251
  **"fetch() URL is invalid"** - Run `opencode auth login` without arguments
@@ -271,26 +266,10 @@ Common causes:
271
266
 
272
267
  ### Debug Logging
273
268
 
274
- Set the log level via environment variable:
275
- - `CURSOR_ACP_LOG_LEVEL=debug` - Verbose output for troubleshooting
276
- - `CURSOR_ACP_LOG_LEVEL=info` - Default level
277
- - `CURSOR_ACP_LOG_LEVEL=warn` - Warnings and errors only
278
- - `CURSOR_ACP_LOG_LEVEL=error` - Errors only
279
-
280
- Provider-boundary rollout:
281
- - Default: `CURSOR_ACP_PROVIDER_BOUNDARY=v1`
282
- - Default: `CURSOR_ACP_PROVIDER_BOUNDARY_AUTOFALLBACK=true`
283
- - `CURSOR_ACP_PROVIDER_BOUNDARY=legacy` - Original provider/runtime boundary behavior
284
- - `CURSOR_ACP_PROVIDER_BOUNDARY=v1` - Shared boundary/interception path
285
- - `CURSOR_ACP_PROVIDER_BOUNDARY_AUTOFALLBACK=false` - Disable fallback and keep strict `v1` behavior
286
- - `CURSOR_ACP_TOOL_LOOP_MAX_REPEAT=3` - Max repeated failing tool-call fingerprints before guard termination (or fallback when enabled)
287
-
288
- Auto-fallback trigger conditions:
289
- - Only active when `CURSOR_ACP_PROVIDER_BOUNDARY=v1`
290
- - Triggered when `v1` boundary extraction throws during tool-call interception
291
- - Triggered when the tool-loop guard threshold is reached (same tool + arg shape + error class)
292
- - Does not trigger for normal cases like disallowed tools or no tool call
293
- - Does not trigger for unrelated runtime errors (for example, tool mapper/tool execution failures)
269
+ Enable verbose logs:
270
+ ```bash
271
+ CURSOR_ACP_LOG_LEVEL=debug opencode run "your prompt" --model cursor-acp/auto
272
+ ```
294
273
 
295
274
  Disable log output entirely:
296
275
  ```bash
@@ -177,18 +177,170 @@ function fallbackModels() {
177
177
  }
178
178
 
179
179
  // src/cli/opencode-cursor.ts
180
+ var BRANDING_HEADER = `
181
+ ▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
182
+ ██ ██ ██ ██ ██▄▄ ███▄██ ▄▄▄ ██ ▀▀ ██ ██ ██ ██ ██▄▄▄ ██ ██ ██ ██
183
+ ▀█▄█▀ ██▀▀ ██▄▄▄ ██ ▀██ ▀█▄█▀ ▀█▄█▀ ██▀█▄ ▄▄▄█▀ ▀█▄█▀ ██▀█▄
184
+ `;
185
+ function getBrandingHeader() {
186
+ return BRANDING_HEADER.trim();
187
+ }
188
+ function checkBun() {
189
+ try {
190
+ const version = execFileSync2("bun", ["--version"], { encoding: "utf8" }).trim();
191
+ return { name: "bun", passed: true, message: `v${version}` };
192
+ } catch {
193
+ return {
194
+ name: "bun",
195
+ passed: false,
196
+ message: "not found - install with: curl -fsSL https://bun.sh/install | bash"
197
+ };
198
+ }
199
+ }
200
+ function checkCursorAgent() {
201
+ try {
202
+ const output = execFileSync2("cursor-agent", ["--version"], { encoding: "utf8" }).trim();
203
+ const version = output.split(`
204
+ `)[0] || "installed";
205
+ return { name: "cursor-agent", passed: true, message: version };
206
+ } catch {
207
+ return {
208
+ name: "cursor-agent",
209
+ passed: false,
210
+ message: "not found - install with: curl -fsS https://cursor.com/install | bash"
211
+ };
212
+ }
213
+ }
214
+ function checkCursorAgentLogin() {
215
+ try {
216
+ execFileSync2("cursor-agent", ["models"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
217
+ return { name: "cursor-agent login", passed: true, message: "logged in" };
218
+ } catch {
219
+ return {
220
+ name: "cursor-agent login",
221
+ passed: false,
222
+ message: "not logged in - run: cursor-agent login",
223
+ warning: true
224
+ };
225
+ }
226
+ }
227
+ function checkOpenCode() {
228
+ try {
229
+ const version = execFileSync2("opencode", ["--version"], { encoding: "utf8" }).trim();
230
+ return { name: "OpenCode", passed: true, message: version };
231
+ } catch {
232
+ return {
233
+ name: "OpenCode",
234
+ passed: false,
235
+ message: "not found - install with: curl -fsSL https://opencode.ai/install | bash"
236
+ };
237
+ }
238
+ }
239
+ function checkPluginFile(pluginPath) {
240
+ try {
241
+ if (!existsSync(pluginPath)) {
242
+ return {
243
+ name: "Plugin file",
244
+ passed: false,
245
+ message: "not found - run: open-cursor install"
246
+ };
247
+ }
248
+ const stat = lstatSync(pluginPath);
249
+ if (stat.isSymbolicLink()) {
250
+ const target = readFileSync(pluginPath, "utf8");
251
+ return { name: "Plugin file", passed: true, message: `symlink → ${target}` };
252
+ }
253
+ return { name: "Plugin file", passed: true, message: "file (copy)" };
254
+ } catch {
255
+ return {
256
+ name: "Plugin file",
257
+ passed: false,
258
+ message: "error reading plugin file"
259
+ };
260
+ }
261
+ }
262
+ function checkProviderConfig(configPath) {
263
+ try {
264
+ if (!existsSync(configPath)) {
265
+ return {
266
+ name: "Provider config",
267
+ passed: false,
268
+ message: "config not found - run: open-cursor install"
269
+ };
270
+ }
271
+ const config = readConfig(configPath);
272
+ const provider = config.provider?.["cursor-acp"];
273
+ if (!provider) {
274
+ return {
275
+ name: "Provider config",
276
+ passed: false,
277
+ message: "cursor-acp provider missing - run: open-cursor install"
278
+ };
279
+ }
280
+ const modelCount = Object.keys(provider.models || {}).length;
281
+ return { name: "Provider config", passed: true, message: `${modelCount} models` };
282
+ } catch {
283
+ return {
284
+ name: "Provider config",
285
+ passed: false,
286
+ message: "error reading config"
287
+ };
288
+ }
289
+ }
290
+ function checkAiSdk(opencodeDir) {
291
+ try {
292
+ const sdkPath = join(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
293
+ if (existsSync(sdkPath)) {
294
+ return { name: "AI SDK", passed: true, message: "@ai-sdk/openai-compatible installed" };
295
+ }
296
+ return {
297
+ name: "AI SDK",
298
+ passed: false,
299
+ message: "not installed - run: open-cursor install"
300
+ };
301
+ } catch {
302
+ return {
303
+ name: "AI SDK",
304
+ passed: false,
305
+ message: "error checking AI SDK"
306
+ };
307
+ }
308
+ }
309
+ function runDoctorChecks(configPath, pluginPath) {
310
+ const opencodeDir = dirname(configPath);
311
+ return [
312
+ checkBun(),
313
+ checkCursorAgent(),
314
+ checkCursorAgentLogin(),
315
+ checkOpenCode(),
316
+ checkPluginFile(pluginPath),
317
+ checkProviderConfig(configPath),
318
+ checkAiSdk(opencodeDir)
319
+ ];
320
+ }
180
321
  var PROVIDER_ID = "cursor-acp";
181
322
  var DEFAULT_BASE_URL = "http://127.0.0.1:32124/v1";
182
323
  function printHelp() {
183
324
  const binName = basename(process.argv[1] || "open-cursor");
325
+ console.log(getBrandingHeader());
184
326
  console.log(`${binName}
185
327
 
186
- Usage:
187
- ${binName} install [--config <path>] [--plugin-dir <path>] [--base-url <url>] [--copy] [--skip-models] [--no-backup]
188
- ${binName} sync-models [--config <path>] [--no-backup]
189
- ${binName} uninstall [--config <path>] [--plugin-dir <path>] [--no-backup]
190
- ${binName} status [--config <path>] [--plugin-dir <path>]
191
- ${binName} help
328
+ Commands:
329
+ install Configure OpenCode for Cursor (idempotent, safe to re-run)
330
+ sync-models Refresh model list from cursor-agent
331
+ status Show current configuration state
332
+ doctor Diagnose common issues
333
+ uninstall Remove cursor-acp from OpenCode config
334
+ help Show this help message
335
+
336
+ Options:
337
+ --config <path> Path to opencode.json (default: ~/.config/opencode/opencode.json)
338
+ --plugin-dir <path> Path to plugin directory (default: ~/.config/opencode/plugin)
339
+ --base-url <url> Proxy base URL (default: http://127.0.0.1:32124/v1)
340
+ --copy Copy plugin instead of symlink
341
+ --skip-models Skip model sync during install
342
+ --no-backup Don't create config backup
343
+ --json Output in JSON format (status command only)
192
344
  `);
193
345
  }
194
346
  function parseArgs(argv) {
@@ -212,6 +364,8 @@ function parseArgs(argv) {
212
364
  } else if (arg === "--base-url" && rest[i + 1]) {
213
365
  options.baseUrl = rest[i + 1];
214
366
  i += 1;
367
+ } else if (arg === "--json") {
368
+ options.json = true;
215
369
  } else {
216
370
  throw new Error(`Unknown argument: ${arg}`);
217
371
  }
@@ -219,13 +373,14 @@ function parseArgs(argv) {
219
373
  return { command, options };
220
374
  }
221
375
  function normalizeCommand(value) {
222
- switch ((value || "install").toLowerCase()) {
376
+ switch ((value || "help").toLowerCase()) {
223
377
  case "install":
224
378
  case "sync-models":
225
379
  case "uninstall":
226
380
  case "status":
381
+ case "doctor":
227
382
  case "help":
228
- return value ? value.toLowerCase() : "install";
383
+ return value ? value.toLowerCase() : "help";
229
384
  default:
230
385
  throw new Error(`Unknown command: ${value}`);
231
386
  }
@@ -377,21 +532,97 @@ function commandUninstall(options) {
377
532
  console.log(`Removed plugin link: ${pluginPath}`);
378
533
  console.log(`Removed provider "${PROVIDER_ID}" from ${configPath}`);
379
534
  }
380
- function commandStatus(options) {
381
- const { configPath, pluginPath } = resolvePaths(options);
382
- const pluginExists = existsSync(pluginPath);
383
- const pluginType = pluginExists ? lstatSync(pluginPath).isSymbolicLink() ? "symlink" : "file" : "missing";
384
- let providerExists = false;
385
- let pluginEnabled = false;
535
+ function getStatusResult(configPath, pluginPath) {
536
+ let pluginType = "missing";
537
+ let pluginTarget;
538
+ if (existsSync(pluginPath)) {
539
+ const stat = lstatSync(pluginPath);
540
+ pluginType = stat.isSymbolicLink() ? "symlink" : "file";
541
+ if (pluginType === "symlink") {
542
+ try {
543
+ pluginTarget = readFileSync(pluginPath, "utf8");
544
+ } catch {
545
+ pluginTarget = undefined;
546
+ }
547
+ }
548
+ }
549
+ let providerEnabled = false;
550
+ let baseUrl = "http://127.0.0.1:32124/v1";
551
+ let modelCount = 0;
386
552
  if (existsSync(configPath)) {
387
553
  const config = readConfig(configPath);
388
- providerExists = Boolean(config.provider?.[PROVIDER_ID]);
389
- pluginEnabled = Array.isArray(config.plugin) && config.plugin.includes(PROVIDER_ID);
554
+ const provider = config.provider?.["cursor-acp"];
555
+ providerEnabled = !!provider;
556
+ if (provider?.options?.baseURL) {
557
+ baseUrl = provider.options.baseURL;
558
+ }
559
+ modelCount = Object.keys(provider?.models || {}).length;
560
+ }
561
+ const opencodeDir = dirname(configPath);
562
+ const sdkPath = join(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
563
+ const aiSdkInstalled = existsSync(sdkPath);
564
+ return {
565
+ plugin: {
566
+ path: pluginPath,
567
+ type: pluginType,
568
+ target: pluginTarget
569
+ },
570
+ provider: {
571
+ configPath,
572
+ name: "cursor-acp",
573
+ enabled: providerEnabled,
574
+ baseUrl,
575
+ modelCount
576
+ },
577
+ aiSdk: {
578
+ installed: aiSdkInstalled
579
+ }
580
+ };
581
+ }
582
+ function commandStatus(options) {
583
+ const { configPath, pluginPath } = resolvePaths(options);
584
+ const result = getStatusResult(configPath, pluginPath);
585
+ if (options.json) {
586
+ console.log(JSON.stringify(result, null, 2));
587
+ return;
588
+ }
589
+ console.log("");
590
+ console.log("Plugin");
591
+ console.log(` Path: ${result.plugin.path}`);
592
+ if (result.plugin.type === "symlink" && result.plugin.target) {
593
+ console.log(` Type: symlink → ${result.plugin.target}`);
594
+ } else if (result.plugin.type === "file") {
595
+ console.log(` Type: file (copy)`);
596
+ } else {
597
+ console.log(` Type: missing`);
598
+ }
599
+ console.log("");
600
+ console.log("Provider");
601
+ console.log(` Config: ${result.provider.configPath}`);
602
+ console.log(` Name: ${result.provider.name}`);
603
+ console.log(` Enabled: ${result.provider.enabled ? "yes" : "no"}`);
604
+ console.log(` Base URL: ${result.provider.baseUrl}`);
605
+ console.log(` Models: ${result.provider.modelCount}`);
606
+ console.log("");
607
+ console.log("AI SDK");
608
+ console.log(` @ai-sdk/openai-compatible: ${result.aiSdk.installed ? "installed" : "not installed"}`);
609
+ }
610
+ function commandDoctor(options) {
611
+ const { configPath, pluginPath } = resolvePaths(options);
612
+ const checks = runDoctorChecks(configPath, pluginPath);
613
+ console.log("");
614
+ for (const check of checks) {
615
+ const symbol = check.passed ? "✓" : check.warning ? "⚠" : "✗";
616
+ const color = check.passed ? "\x1B[32m" : check.warning ? "\x1B[33m" : "\x1B[31m";
617
+ console.log(` ${color}${symbol}\x1B[0m ${check.name}: ${check.message}`);
618
+ }
619
+ const failed = checks.filter((c) => !c.passed && !c.warning);
620
+ console.log("");
621
+ if (failed.length === 0) {
622
+ console.log("All checks passed!");
623
+ } else {
624
+ console.log(`${failed.length} check(s) failed. See messages above.`);
390
625
  }
391
- console.log(`Plugin file: ${pluginPath} (${pluginType})`);
392
- console.log(`Provider in config: ${providerExists ? "yes" : "no"}`);
393
- console.log(`Plugin enabled in config: ${pluginEnabled ? "yes" : "no"}`);
394
- console.log(`Config path: ${configPath}`);
395
626
  }
396
627
  function main() {
397
628
  let parsed;
@@ -418,6 +649,9 @@ function main() {
418
649
  case "status":
419
650
  commandStatus(parsed.options);
420
651
  return;
652
+ case "doctor":
653
+ commandDoctor(parsed.options);
654
+ return;
421
655
  case "help":
422
656
  printHelp();
423
657
  return;
@@ -429,3 +663,11 @@ function main() {
429
663
  }
430
664
  }
431
665
  main();
666
+ export {
667
+ runDoctorChecks,
668
+ getStatusResult,
669
+ getBrandingHeader,
670
+ checkCursorAgentLogin,
671
+ checkCursorAgent,
672
+ checkBun
673
+ };