@rama_nigg/open-cursor 2.3.13 → 2.3.15

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": "@rama_nigg/open-cursor",
3
- "version": "2.3.13",
3
+ "version": "2.3.15",
4
4
  "description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
5
5
  "type": "module",
6
6
  "main": "dist/plugin-entry.js",
@@ -1,6 +1,8 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { stripAnsi } from "../utils/errors.js";
3
3
 
4
+ const MODEL_DISCOVERY_TIMEOUT_MS = 5000;
5
+
4
6
  export type DiscoveredModel = {
5
7
  id: string;
6
8
  name: string;
@@ -31,7 +33,9 @@ export function parseCursorModelsOutput(output: string): DiscoveredModel[] {
31
33
  export function discoverModelsFromCursorAgent(): DiscoveredModel[] {
32
34
  const raw = execFileSync("cursor-agent", ["models"], {
33
35
  encoding: "utf8",
36
+ killSignal: "SIGTERM",
34
37
  stdio: ["ignore", "pipe", "pipe"],
38
+ timeout: MODEL_DISCOVERY_TIMEOUT_MS,
35
39
  });
36
40
  const models = parseCursorModelsOutput(raw);
37
41
  if (models.length === 0) {
@@ -43,8 +47,24 @@ export function discoverModelsFromCursorAgent(): DiscoveredModel[] {
43
47
  export function fallbackModels(): DiscoveredModel[] {
44
48
  return [
45
49
  { id: "auto", name: "Auto" },
46
- { id: "sonnet-4.5", name: "Claude 4.5 Sonnet" },
50
+ { id: "composer-1.5", name: "Composer 1.5" },
51
+ { id: "composer-1", name: "Composer 1" },
52
+ { id: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" },
47
53
  { id: "opus-4.6", name: "Claude 4.6 Opus" },
54
+ { id: "sonnet-4.6", name: "Claude 4.6 Sonnet" },
55
+ { id: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
56
+ { id: "opus-4.5", name: "Claude 4.5 Opus" },
57
+ { id: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" },
58
+ { id: "sonnet-4.5", name: "Claude 4.5 Sonnet" },
59
+ { id: "sonnet-4.5-thinking", name: "Claude 4.5 Sonnet (Thinking)" },
60
+ { id: "gpt-5.4-high", name: "GPT-5.4 High" },
61
+ { id: "gpt-5.4-medium", name: "GPT-5.4" },
62
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
48
63
  { id: "gpt-5.2", name: "GPT-5.2" },
64
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
65
+ { id: "gemini-3-pro", name: "Gemini 3 Pro" },
66
+ { id: "gemini-3-flash", name: "Gemini 3 Flash" },
67
+ { id: "grok", name: "Grok" },
68
+ { id: "kimi-k2.5", name: "Kimi K2.5" },
49
69
  ];
50
70
  }
@@ -253,11 +253,24 @@ export class SimpleCursorClient {
253
253
  async getAvailableModels(): Promise<Array<{ id: string; name: string }>> {
254
254
  return [
255
255
  { id: 'auto', name: 'Cursor Agent Auto' },
256
+ { id: 'composer-1.5', name: 'Composer 1.5' },
257
+ { id: 'opus-4.6-thinking', name: 'Claude 4.6 Opus (Thinking)' },
258
+ { id: 'opus-4.6', name: 'Claude 4.6 Opus' },
259
+ { id: 'sonnet-4.6', name: 'Claude 4.6 Sonnet' },
260
+ { id: 'sonnet-4.6-thinking', name: 'Claude 4.6 Sonnet (Thinking)' },
261
+ { id: 'opus-4.5', name: 'Claude 4.5 Opus' },
262
+ { id: 'opus-4.5-thinking', name: 'Claude 4.5 Opus (Thinking)' },
263
+ { id: 'sonnet-4.5', name: 'Claude 4.5 Sonnet' },
264
+ { id: 'sonnet-4.5-thinking', name: 'Claude 4.5 Sonnet (Thinking)' },
265
+ { id: 'gpt-5.4-high', name: 'GPT-5.4 High' },
266
+ { id: 'gpt-5.4-medium', name: 'GPT-5.4' },
267
+ { id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex' },
256
268
  { id: 'gpt-5.2', name: 'GPT-5.2' },
269
+ { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro' },
257
270
  { id: 'gemini-3-pro', name: 'Gemini 3 Pro' },
258
- { id: 'opus-4.5-thinking', name: 'Claude 4.5 Opus Thinking' },
259
- { id: 'sonnet-4.5', name: 'Claude 4.5 Sonnet' },
260
- { id: 'deepseek-v3.2', name: 'DeepSeek V3.2' }
271
+ { id: 'gemini-3-flash', name: 'Gemini 3 Flash' },
272
+ { id: 'grok', name: 'Grok' },
273
+ { id: 'kimi-k2.5', name: 'Kimi K2.5' },
261
274
  ];
262
275
  }
263
276
 
@@ -119,10 +119,20 @@ export class ModelDiscoveryService {
119
119
  private getDefaultModels(): ModelInfo[] {
120
120
  return [
121
121
  { id: "auto", name: "Auto", description: "Auto-select best model" },
122
+ { id: "composer-1.5", name: "Composer 1.5" },
123
+ { id: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" },
124
+ { id: "opus-4.6", name: "Claude 4.6 Opus" },
125
+ { id: "sonnet-4.6", name: "Claude 4.6 Sonnet" },
126
+ { id: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
127
+ { id: "opus-4.5", name: "Claude 4.5 Opus" },
128
+ { id: "sonnet-4.5", name: "Claude 4.5 Sonnet" },
129
+ { id: "gpt-5.4-high", name: "GPT-5.4 High" },
130
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
122
131
  { id: "gpt-5.2", name: "GPT-5.2" },
123
- { id: "sonnet-4.5", name: "Sonnet 4.5" },
124
- { id: "opus-4.5", name: "Opus 4.5" },
125
- { id: "gemini-3-pro", name: "Gemini 3 Pro" }
132
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
133
+ { id: "gemini-3-pro", name: "Gemini 3 Pro" },
134
+ { id: "grok", name: "Grok" },
135
+ { id: "kimi-k2.5", name: "Kimi K2.5" },
126
136
  ];
127
137
  }
128
138
 
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Non-blocking model auto-refresh for plugin startup.
3
+ *
4
+ * Discovers currently available models from cursor-agent and merges them
5
+ * into the opencode.json config. Only adds new models — never removes
6
+ * user-configured ones. Safe to call fire-and-forget; all errors are
7
+ * caught and logged silently.
8
+ */
9
+ import {
10
+ existsSync as nodeExistsSync,
11
+ readFileSync as nodeReadFileSync,
12
+ writeFileSync as nodeWriteFileSync,
13
+ } from "node:fs";
14
+ import { discoverModelsFromCursorAgent, type DiscoveredModel } from "../cli/model-discovery.js";
15
+ import { resolveOpenCodeConfigPath } from "../plugin-toggle.js";
16
+ import { createLogger, type Logger } from "../utils/logger.js";
17
+
18
+ const log = createLogger("model-sync");
19
+ const PROVIDER_ID = "cursor-acp";
20
+
21
+ type ModelConfigEntry = { name: string };
22
+ type ProviderConfig = { models?: Record<string, unknown> } & Record<string, unknown>;
23
+ type OpenCodeConfig = {
24
+ provider?: Record<string, ProviderConfig | undefined>;
25
+ } & Record<string, unknown>;
26
+ type AutoRefreshModelsDeps = {
27
+ defer: () => Promise<void>;
28
+ discoverModels: () => DiscoveredModel[];
29
+ env: NodeJS.ProcessEnv;
30
+ existsSync: (path: string) => boolean;
31
+ log: Logger;
32
+ readFileSync: (path: string, encoding: BufferEncoding) => string;
33
+ writeFileSync: (path: string, data: string, encoding: BufferEncoding) => void;
34
+ };
35
+
36
+ const defaultDeps: AutoRefreshModelsDeps = {
37
+ defer: () => Promise.resolve(),
38
+ discoverModels: discoverModelsFromCursorAgent,
39
+ env: process.env,
40
+ existsSync: nodeExistsSync,
41
+ log,
42
+ readFileSync: nodeReadFileSync,
43
+ writeFileSync: nodeWriteFileSync,
44
+ };
45
+
46
+ function isRecord(value: unknown): value is Record<string, unknown> {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+
50
+ function parseConfig(raw: string): OpenCodeConfig | null {
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ return isRecord(parsed) ? (parsed as OpenCodeConfig) : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function getProviderConfig(config: OpenCodeConfig): ProviderConfig | null {
60
+ if (!isRecord(config.provider)) {
61
+ return null;
62
+ }
63
+
64
+ const provider = config.provider[PROVIDER_ID];
65
+ return isRecord(provider) ? (provider as ProviderConfig) : null;
66
+ }
67
+
68
+ function getExistingModels(provider: ProviderConfig): Record<string, unknown> {
69
+ return isRecord(provider.models) ? { ...provider.models } : {};
70
+ }
71
+
72
+ function yieldForFireAndForget(): Promise<void> {
73
+ return Promise.resolve();
74
+ }
75
+
76
+ /**
77
+ * Auto-refresh models at plugin startup.
78
+ *
79
+ * - Reads the current opencode.json config
80
+ * - Queries cursor-agent for available models
81
+ * - Merges discovered models into the provider config (additive only)
82
+ * - Writes back if any new models were added
83
+ *
84
+ * This function never throws. All failures are logged at debug level
85
+ * and silently ignored so plugin startup is never blocked.
86
+ */
87
+ export async function autoRefreshModels(
88
+ deps: Partial<AutoRefreshModelsDeps> = {},
89
+ ): Promise<void> {
90
+ const resolvedDeps: AutoRefreshModelsDeps = {
91
+ ...defaultDeps,
92
+ defer: yieldForFireAndForget,
93
+ ...deps,
94
+ };
95
+
96
+ await resolvedDeps.defer();
97
+
98
+ try {
99
+ const configPath = resolveOpenCodeConfigPath(resolvedDeps.env);
100
+ if (!resolvedDeps.existsSync(configPath)) {
101
+ resolvedDeps.log.debug("Config file not found, skipping model auto-refresh", { configPath });
102
+ return;
103
+ }
104
+
105
+ const raw = resolvedDeps.readFileSync(configPath, "utf8");
106
+ const config = parseConfig(raw);
107
+ if (!config) {
108
+ resolvedDeps.log.debug("Config file is not valid JSON, skipping model auto-refresh");
109
+ return;
110
+ }
111
+
112
+ const provider = getProviderConfig(config);
113
+ if (!provider) {
114
+ resolvedDeps.log.debug("Provider section not found in config, skipping model auto-refresh");
115
+ return;
116
+ }
117
+
118
+ const existingModels = getExistingModels(provider);
119
+ let discovered: DiscoveredModel[];
120
+ try {
121
+ discovered = resolvedDeps.discoverModels();
122
+ } catch (err) {
123
+ resolvedDeps.log.debug("cursor-agent model discovery failed, skipping auto-refresh", {
124
+ error: String(err),
125
+ });
126
+ return;
127
+ }
128
+
129
+ let addedCount = 0;
130
+ for (const model of discovered) {
131
+ if (Object.prototype.hasOwnProperty.call(existingModels, model.id)) continue;
132
+ existingModels[model.id] = { name: model.name } satisfies ModelConfigEntry;
133
+ addedCount++;
134
+ }
135
+
136
+ if (addedCount === 0) {
137
+ resolvedDeps.log.debug("Model auto-refresh: no new models found", {
138
+ existing: Object.keys(existingModels).length,
139
+ discovered: discovered.length,
140
+ });
141
+ return;
142
+ }
143
+
144
+ provider.models = existingModels;
145
+ resolvedDeps.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
146
+ resolvedDeps.log.info("Model auto-refresh: added new models", {
147
+ added: addedCount,
148
+ total: Object.keys(existingModels).length,
149
+ });
150
+ } catch (err) {
151
+ resolvedDeps.log.debug("Model auto-refresh failed", { error: String(err) });
152
+ }
153
+ }
package/src/plugin.ts CHANGED
@@ -24,6 +24,7 @@ import { toOpenAiParameters, describeTool } from "./tools/schema.js";
24
24
  import { ToolRouter } from "./tools/router.js";
25
25
  import { SkillLoader } from "./tools/skills/loader.js";
26
26
  import { SkillResolver } from "./tools/skills/resolver.js";
27
+ import { autoRefreshModels } from "./models/sync.js";
27
28
  import { createOpencodeClient } from "@opencode-ai/sdk";
28
29
  import { ToolRegistry as CoreRegistry } from "./tools/core/registry.js";
29
30
  import { LocalExecutor } from "./tools/executors/local.js";
@@ -1735,8 +1736,8 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
1735
1736
  });
1736
1737
  await ensurePluginDirectory();
1737
1738
 
1738
- // Initialize toast service for MCP pass-through notifications
1739
- toastService.setClient(client);
1739
+ // Auto-refresh model list from cursor-agent (non-blocking, fire-and-forget)
1740
+ autoRefreshModels().catch(() => {});
1740
1741
 
1741
1742
  // Initialize toast service for MCP pass-through notifications
1742
1743
  toastService.setClient(client);
@@ -44,9 +44,12 @@ export class StreamToAiSdkParts {
44
44
  if (isAssistantText(event)) {
45
45
  const isPartial = typeof event.timestamp_ms === "number";
46
46
  if (isPartial) {
47
- this.sawAssistantPartials = true;
48
47
  const text = extractText(event);
49
- return text ? [{ type: "text-delta", textDelta: text }] : [];
48
+ if (text) {
49
+ this.sawAssistantPartials = true;
50
+ return [{ type: "text-delta", textDelta: text }];
51
+ }
52
+ return [];
50
53
  }
51
54
  if (this.sawAssistantPartials) {
52
55
  return [];
@@ -58,9 +61,12 @@ export class StreamToAiSdkParts {
58
61
  if (isThinking(event)) {
59
62
  const isPartial = typeof event.timestamp_ms === "number";
60
63
  if (isPartial) {
61
- this.sawThinkingPartials = true;
62
64
  const text = extractThinking(event);
63
- return text ? [{ type: "text-delta", textDelta: text }] : [];
65
+ if (text) {
66
+ this.sawThinkingPartials = true;
67
+ return [{ type: "text-delta", textDelta: text }];
68
+ }
69
+ return [];
64
70
  }
65
71
  if (this.sawThinkingPartials) {
66
72
  return [];
@@ -77,9 +77,12 @@ export class StreamToSseConverter {
77
77
  if (isAssistantText(event)) {
78
78
  const isPartial = typeof event.timestamp_ms === "number";
79
79
  if (isPartial) {
80
- this.sawAssistantPartials = true;
81
80
  const text = extractText(event);
82
- return text ? [this.chunkWith({ content: text })] : [];
81
+ if (text) {
82
+ this.sawAssistantPartials = true;
83
+ return [this.chunkWith({ content: text })];
84
+ }
85
+ return [];
83
86
  }
84
87
  if (this.sawAssistantPartials) {
85
88
  return [];
@@ -91,9 +94,12 @@ export class StreamToSseConverter {
91
94
  if (isThinking(event)) {
92
95
  const isPartial = typeof event.timestamp_ms === "number";
93
96
  if (isPartial) {
94
- this.sawThinkingPartials = true;
95
97
  const text = extractThinking(event);
96
- return text ? [this.chunkWith({ reasoning_content: text })] : [];
98
+ if (text) {
99
+ this.sawThinkingPartials = true;
100
+ return [this.chunkWith({ reasoning_content: text })];
101
+ }
102
+ return [];
97
103
  }
98
104
  if (this.sawThinkingPartials) {
99
105
  return [];