@agentex/agent 0.0.26 → 0.0.28

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.
Files changed (172) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +58 -7
  3. package/dist/derived.d.ts.map +1 -1
  4. package/dist/derived.js +99 -2
  5. package/dist/derived.js.map +1 -1
  6. package/dist/goals/controller.d.ts.map +1 -1
  7. package/dist/goals/controller.js +21 -0
  8. package/dist/goals/controller.js.map +1 -1
  9. package/dist/goals/normalize.d.ts +1 -0
  10. package/dist/goals/normalize.d.ts.map +1 -1
  11. package/dist/goals/normalize.js +5 -0
  12. package/dist/goals/normalize.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/providers/claude/attach.d.ts.map +1 -1
  17. package/dist/providers/claude/attach.js +50 -9
  18. package/dist/providers/claude/attach.js.map +1 -1
  19. package/dist/providers/claude/index.d.ts.map +1 -1
  20. package/dist/providers/claude/index.js +8 -0
  21. package/dist/providers/claude/index.js.map +1 -1
  22. package/dist/providers/claude/session.d.ts +2 -0
  23. package/dist/providers/claude/session.d.ts.map +1 -1
  24. package/dist/providers/claude/session.js +17 -10
  25. package/dist/providers/claude/session.js.map +1 -1
  26. package/dist/providers/codex/attach.d.ts.map +1 -1
  27. package/dist/providers/codex/attach.js +40 -3
  28. package/dist/providers/codex/attach.js.map +1 -1
  29. package/dist/providers/codex/index.d.ts.map +1 -1
  30. package/dist/providers/codex/index.js +8 -0
  31. package/dist/providers/codex/index.js.map +1 -1
  32. package/dist/providers/codex/session.d.ts.map +1 -1
  33. package/dist/providers/codex/session.js +6 -0
  34. package/dist/providers/codex/session.js.map +1 -1
  35. package/dist/providers/codex/transcript.d.ts +3 -1
  36. package/dist/providers/codex/transcript.d.ts.map +1 -1
  37. package/dist/providers/codex/transcript.js +3 -1
  38. package/dist/providers/codex/transcript.js.map +1 -1
  39. package/dist/providers/cursor/discovery.d.ts +6 -0
  40. package/dist/providers/cursor/discovery.d.ts.map +1 -0
  41. package/dist/providers/cursor/discovery.js +118 -0
  42. package/dist/providers/cursor/discovery.js.map +1 -0
  43. package/dist/providers/cursor/execute.d.ts.map +1 -1
  44. package/dist/providers/cursor/execute.js +83 -45
  45. package/dist/providers/cursor/execute.js.map +1 -1
  46. package/dist/providers/cursor/index.d.ts.map +1 -1
  47. package/dist/providers/cursor/index.js +17 -4
  48. package/dist/providers/cursor/index.js.map +1 -1
  49. package/dist/providers/cursor/parse.js +1 -1
  50. package/dist/providers/cursor/parse.js.map +1 -1
  51. package/dist/providers/cursor/probe.d.ts +3 -0
  52. package/dist/providers/cursor/probe.d.ts.map +1 -0
  53. package/dist/providers/cursor/probe.js +89 -0
  54. package/dist/providers/cursor/probe.js.map +1 -0
  55. package/dist/providers/cursor/runtime.d.ts +4 -0
  56. package/dist/providers/cursor/runtime.d.ts.map +1 -0
  57. package/dist/providers/cursor/runtime.js +12 -0
  58. package/dist/providers/cursor/runtime.js.map +1 -0
  59. package/dist/providers/cursor/session.d.ts +3 -0
  60. package/dist/providers/cursor/session.d.ts.map +1 -0
  61. package/dist/providers/cursor/session.js +14 -0
  62. package/dist/providers/cursor/session.js.map +1 -0
  63. package/dist/providers/opencode/client.d.ts +14 -0
  64. package/dist/providers/opencode/client.d.ts.map +1 -0
  65. package/dist/providers/opencode/client.js +40 -0
  66. package/dist/providers/opencode/client.js.map +1 -0
  67. package/dist/providers/opencode/discovery.d.ts +4 -0
  68. package/dist/providers/opencode/discovery.d.ts.map +1 -0
  69. package/dist/providers/opencode/discovery.js +68 -0
  70. package/dist/providers/opencode/discovery.js.map +1 -0
  71. package/dist/providers/opencode/execute.d.ts.map +1 -1
  72. package/dist/providers/opencode/execute.js +219 -164
  73. package/dist/providers/opencode/execute.js.map +1 -1
  74. package/dist/providers/opencode/history.d.ts +24 -0
  75. package/dist/providers/opencode/history.d.ts.map +1 -0
  76. package/dist/providers/opencode/history.js +242 -0
  77. package/dist/providers/opencode/history.js.map +1 -0
  78. package/dist/providers/opencode/http-session.d.ts.map +1 -1
  79. package/dist/providers/opencode/http-session.js +333 -49
  80. package/dist/providers/opencode/http-session.js.map +1 -1
  81. package/dist/providers/opencode/index.d.ts.map +1 -1
  82. package/dist/providers/opencode/index.js +26 -3
  83. package/dist/providers/opencode/index.js.map +1 -1
  84. package/dist/providers/opencode/manager.d.ts +11 -0
  85. package/dist/providers/opencode/manager.d.ts.map +1 -0
  86. package/dist/providers/opencode/manager.js +240 -0
  87. package/dist/providers/opencode/manager.js.map +1 -0
  88. package/dist/providers/opencode/modes.d.ts +3 -0
  89. package/dist/providers/opencode/modes.d.ts.map +1 -0
  90. package/dist/providers/opencode/modes.js +30 -0
  91. package/dist/providers/opencode/modes.js.map +1 -0
  92. package/dist/providers/opencode/parse.d.ts.map +1 -1
  93. package/dist/providers/opencode/parse.js +2 -22
  94. package/dist/providers/opencode/parse.js.map +1 -1
  95. package/dist/providers/opencode/probe.d.ts +3 -0
  96. package/dist/providers/opencode/probe.d.ts.map +1 -0
  97. package/dist/providers/opencode/probe.js +107 -0
  98. package/dist/providers/opencode/probe.js.map +1 -0
  99. package/dist/providers/opencode/runtime.d.ts +8 -0
  100. package/dist/providers/opencode/runtime.d.ts.map +1 -0
  101. package/dist/providers/opencode/runtime.js +12 -0
  102. package/dist/providers/opencode/runtime.js.map +1 -0
  103. package/dist/providers/opencode/server.d.ts +18 -3
  104. package/dist/providers/opencode/server.d.ts.map +1 -1
  105. package/dist/providers/opencode/server.js +0 -0
  106. package/dist/providers/opencode/server.js.map +1 -1
  107. package/dist/providers/opencode/skill-config.d.ts +11 -0
  108. package/dist/providers/opencode/skill-config.d.ts.map +1 -0
  109. package/dist/providers/opencode/skill-config.js +57 -0
  110. package/dist/providers/opencode/skill-config.js.map +1 -0
  111. package/dist/sessions/exec-backed.d.ts +10 -0
  112. package/dist/sessions/exec-backed.d.ts.map +1 -0
  113. package/dist/sessions/exec-backed.js +141 -0
  114. package/dist/sessions/exec-backed.js.map +1 -0
  115. package/dist/sessions/history.d.ts +4 -0
  116. package/dist/sessions/history.d.ts.map +1 -0
  117. package/dist/sessions/history.js +59 -0
  118. package/dist/sessions/history.js.map +1 -0
  119. package/dist/sessions/index.d.ts +3 -0
  120. package/dist/sessions/index.d.ts.map +1 -1
  121. package/dist/sessions/index.js +2 -0
  122. package/dist/sessions/index.js.map +1 -1
  123. package/dist/types.d.ts +149 -5
  124. package/dist/types.d.ts.map +1 -1
  125. package/dist/types.js.map +1 -1
  126. package/dist/utils/auth.d.ts.map +1 -1
  127. package/dist/utils/auth.js +36 -6
  128. package/dist/utils/auth.js.map +1 -1
  129. package/dist/utils/binary.d.ts.map +1 -1
  130. package/dist/utils/binary.js +3 -0
  131. package/dist/utils/binary.js.map +1 -1
  132. package/dist/utils/endpoint.js +3 -3
  133. package/dist/utils/endpoint.js.map +1 -1
  134. package/package.json +4 -2
  135. package/src/derived.ts +126 -3
  136. package/src/goals/controller.ts +17 -0
  137. package/src/goals/normalize.ts +5 -0
  138. package/src/index.ts +13 -0
  139. package/src/providers/claude/attach.ts +48 -6
  140. package/src/providers/claude/index.ts +8 -0
  141. package/src/providers/claude/session.ts +15 -10
  142. package/src/providers/codex/attach.ts +48 -3
  143. package/src/providers/codex/index.ts +8 -0
  144. package/src/providers/codex/session.ts +5 -0
  145. package/src/providers/codex/transcript.ts +5 -1
  146. package/src/providers/cursor/discovery.ts +114 -0
  147. package/src/providers/cursor/execute.ts +67 -30
  148. package/src/providers/cursor/index.ts +19 -5
  149. package/src/providers/cursor/parse.ts +1 -1
  150. package/src/providers/cursor/probe.ts +90 -0
  151. package/src/providers/cursor/runtime.ts +11 -0
  152. package/src/providers/cursor/session.ts +15 -0
  153. package/src/providers/opencode/client.ts +44 -0
  154. package/src/providers/opencode/discovery.ts +69 -0
  155. package/src/providers/opencode/execute.ts +64 -12
  156. package/src/providers/opencode/history.ts +270 -0
  157. package/src/providers/opencode/http-session.ts +319 -50
  158. package/src/providers/opencode/index.ts +26 -3
  159. package/src/providers/opencode/manager.ts +269 -0
  160. package/src/providers/opencode/modes.ts +31 -0
  161. package/src/providers/opencode/parse.ts +2 -22
  162. package/src/providers/opencode/probe.ts +113 -0
  163. package/src/providers/opencode/runtime.ts +13 -0
  164. package/src/providers/opencode/server.ts +0 -0
  165. package/src/providers/opencode/skill-config.ts +64 -0
  166. package/src/sessions/exec-backed.ts +169 -0
  167. package/src/sessions/history.ts +76 -0
  168. package/src/sessions/index.ts +3 -0
  169. package/src/types.ts +160 -3
  170. package/src/utils/auth.ts +35 -6
  171. package/src/utils/binary.ts +3 -0
  172. package/src/utils/endpoint.ts +3 -3
@@ -0,0 +1,114 @@
1
+ import type { AgentMode, ListModelsOptions, ListModesOptions, ProviderModel } from "../../types.js";
2
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
3
+ import { runChildProcess } from "../../utils/process.js";
4
+ import { findCursorBinary } from "./runtime.js";
5
+
6
+ function rec(value: unknown): Record<string, unknown> {
7
+ return value && typeof value === "object" && !Array.isArray(value)
8
+ ? value as Record<string, unknown>
9
+ : {};
10
+ }
11
+
12
+ export function parseCursorModels(output: string): ProviderModel[] {
13
+ const trimmed = output.trim();
14
+ if (!trimmed) return [];
15
+ try {
16
+ const parsed = JSON.parse(trimmed) as unknown;
17
+ const values = Array.isArray(parsed) ? parsed : Array.isArray(rec(parsed)["models"]) ? rec(parsed)["models"] as unknown[] : [];
18
+ const models = values.map((value) => {
19
+ if (typeof value === "string") return { id: value, name: value };
20
+ const model = rec(value);
21
+ const id = typeof model["id"] === "string"
22
+ ? model["id"]
23
+ : typeof model["slug"] === "string" ? model["slug"] : "";
24
+ return {
25
+ id,
26
+ name: typeof model["name"] === "string" ? model["name"] : id,
27
+ };
28
+ }).filter((model) => model.id);
29
+ if (models.length) return models;
30
+ } catch {
31
+ // Fall through to the version-aware text parser.
32
+ }
33
+
34
+ const seen = new Set<string>();
35
+ const models: ProviderModel[] = [];
36
+ for (const line of trimmed.split(/\r?\n/)) {
37
+ const cleaned = line.replace(/^\s*[-*•]\s*/, "").trim();
38
+ if (!cleaned || /^(available\s+)?models?:?$/i.test(cleaned)) continue;
39
+ const match = cleaned.match(/^([\w./:-]+)(?:\s{2,}|\s+-\s+|\s+\()?(.*)$/);
40
+ const id = match?.[1] ?? "";
41
+ if (!id || seen.has(id) || /^(usage|error|warning)$/i.test(id)) continue;
42
+ seen.add(id);
43
+ const label = match?.[2]?.replace(/[()]$/, "").trim();
44
+ models.push({ id, name: label || id });
45
+ }
46
+ return models;
47
+ }
48
+
49
+ export function cursorModesFromHelp(output: string): AgentMode[] {
50
+ if (!/--mode\b/.test(output)) return [];
51
+ const advertised = new Set<string>();
52
+ const modeLine = output.split(/\r?\n/).find((line) => /--mode\b/.test(line)) ?? "";
53
+ const match = modeLine.match(/--mode[^\n]*(?:<|\[)([^>\]]+)(?:>|\])/i);
54
+ if (match?.[1]) {
55
+ for (const value of match[1].split(/[|,\s]+/)) {
56
+ const mode = value.trim().toLowerCase();
57
+ if (mode) advertised.add(mode);
58
+ }
59
+ }
60
+ const candidates: AgentMode[] = [
61
+ { id: "agent", name: "Agent" },
62
+ { id: "plan", name: "Plan", description: "Read and plan before making changes" },
63
+ { id: "ask", name: "Ask", description: "Answer questions without editing" },
64
+ ];
65
+ for (const candidate of candidates) {
66
+ if (new RegExp(`\\b${candidate.id}\\b`, "i").test(modeLine)) {
67
+ advertised.add(candidate.id);
68
+ }
69
+ }
70
+ return candidates.filter((mode) => advertised.has(mode.id));
71
+ }
72
+
73
+ export async function listCursorModels(options: ListModelsOptions = {}): Promise<ProviderModel[]> {
74
+ const resolved = await findCursorBinary(options);
75
+ const env = buildEnv(options.env);
76
+ ensurePathInEnv(env);
77
+ const cwd = options.cwd ?? process.cwd();
78
+ const attempts = [
79
+ ["models", "--output-format", "json"],
80
+ ["models", "--json"],
81
+ ["--list-models"],
82
+ ["models"],
83
+ ];
84
+ for (const args of attempts) {
85
+ const result = await runChildProcess({
86
+ runId: "cursor-model-discovery",
87
+ command: resolved.bin,
88
+ args: [...resolved.prefixArgs, ...args],
89
+ cwd,
90
+ env,
91
+ timeoutSec: 15,
92
+ });
93
+ if (result.exitCode !== 0) continue;
94
+ const models = parseCursorModels(result.stdout || result.stderr);
95
+ if (models.length) return models;
96
+ }
97
+ throw new Error("The installed Cursor CLI does not expose model discovery");
98
+ }
99
+
100
+ export async function listCursorModes(options: ListModesOptions = {}): Promise<AgentMode[]> {
101
+ const resolved = await findCursorBinary(options);
102
+ const env = buildEnv(options.env);
103
+ ensurePathInEnv(env);
104
+ const result = await runChildProcess({
105
+ runId: "cursor-mode-probe",
106
+ command: resolved.bin,
107
+ args: [...resolved.prefixArgs, "--help"],
108
+ cwd: options.cwd ?? process.cwd(),
109
+ env,
110
+ timeoutSec: 10,
111
+ });
112
+ const output = `${result.stdout}\n${result.stderr}`;
113
+ return cursorModesFromHelp(output);
114
+ }
@@ -1,6 +1,5 @@
1
1
  import * as path from "node:path";
2
- import type { ExecutionContext, ExecutionResult } from "../../types.js";
3
- import { findBinary } from "../../utils/binary.js";
2
+ import type { ExecutionContext, ExecutionResult, StreamEvent } from "../../types.js";
4
3
  import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
5
4
  import { runChildProcess, deriveErrorCode } from "../../utils/process.js";
6
5
  import { detectAuth } from "../../utils/auth.js";
@@ -16,6 +15,7 @@ import {
16
15
  isCursorUnknownSessionError,
17
16
  isCursorAuthRequired,
18
17
  } from "./parse.js";
18
+ import { findCursorBinary } from "./runtime.js";
19
19
 
20
20
  export async function executeCursorProvider(ctx: ExecutionContext): Promise<ExecutionResult> {
21
21
  const runId = ctx.runId ?? uuidv7();
@@ -28,7 +28,7 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
28
28
  ctx.onLifecycle?.({ phase: "preparing", step: "binary" });
29
29
  let resolvedBinary;
30
30
  try {
31
- resolvedBinary = await findBinary("agent", config.command);
31
+ resolvedBinary = await findCursorBinary({ cwd, env: ctx.env, config });
32
32
  } catch (err) {
33
33
  const errorMessage = err instanceof Error ? err.message : "Binary not found";
34
34
  ctx.onLifecycle?.({ phase: "error", message: errorMessage });
@@ -96,11 +96,12 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
96
96
 
97
97
  // 5. Build args
98
98
  const buildArgs = (resumeSessionId: string | null): string[] => {
99
- const args = [...resolvedBinary.prefixArgs, "-p", "--output-format", "stream-json", "--workspace", cwd];
99
+ const args = [...resolvedBinary.prefixArgs, "-p", "--output-format", "stream-json"];
100
100
  if (resumeSessionId) args.push("--resume", resumeSessionId);
101
101
  if (model) args.push("--model", model);
102
- if (config.mode) args.push("--mode", config.mode);
103
- if (config.skipPermissions) args.push("--yolo");
102
+ const selectedMode = config.planMode ? "plan" : (config.modeId ?? config.mode);
103
+ if (selectedMode) args.push("--mode", selectedMode);
104
+ if (config.skipPermissions) args.push("--force");
104
105
  if (config.extraArgs) args.push(...config.extraArgs);
105
106
  return args;
106
107
  };
@@ -109,6 +110,49 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
109
110
  const runAttempt = async (resumeSessionId: string | null) => {
110
111
  const args = buildArgs(resumeSessionId);
111
112
  let stdoutLineBuffer = "";
113
+ let accepted = false;
114
+ let protocolViolation = false;
115
+ const bufferedEvents: StreamEvent[] = [];
116
+ const bufferedOutput: Array<{ stream: "stdout" | "stderr"; chunk: string }> = [];
117
+
118
+ const flushOutput = async (): Promise<void> => {
119
+ if (!ctx.onOutput) return;
120
+ for (const output of bufferedOutput.splice(0)) {
121
+ try { await ctx.onOutput(output.stream, output.chunk); } catch { /* swallow */ }
122
+ }
123
+ };
124
+
125
+ const dispatchOutput = async (stream: "stdout" | "stderr", chunk: string): Promise<void> => {
126
+ if (!accepted) {
127
+ bufferedOutput.push({ stream, chunk });
128
+ return;
129
+ }
130
+ if (ctx.onOutput) {
131
+ try { await ctx.onOutput(stream, chunk); } catch { /* swallow */ }
132
+ }
133
+ };
134
+
135
+ const dispatch = async (event: StreamEvent): Promise<void> => {
136
+ if (!accepted) {
137
+ bufferedEvents.push(event);
138
+ if (event.type !== "system" || event.subtype !== "init") {
139
+ if (event.type !== "unknown") protocolViolation = true;
140
+ return;
141
+ }
142
+ if (protocolViolation) return;
143
+ accepted = true;
144
+ await flushOutput();
145
+ if (ctx.onEvent) {
146
+ for (const buffered of bufferedEvents.splice(0)) {
147
+ try { await ctx.onEvent(buffered); } catch { /* swallow */ }
148
+ }
149
+ }
150
+ return;
151
+ }
152
+ if (ctx.onEvent) {
153
+ try { await ctx.onEvent(event); } catch { /* swallow */ }
154
+ }
155
+ };
112
156
 
113
157
  ctx.onLifecycle?.({ phase: "spawning" });
114
158
  const proc = await runChildProcess({
@@ -127,9 +171,7 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
127
171
  signal: ctx.signal,
128
172
  onOutput: async (stream, chunk) => {
129
173
  if (stream !== "stdout") {
130
- if (ctx.onOutput) {
131
- try { await ctx.onOutput(stream, chunk); } catch { /* swallow */ }
132
- }
174
+ await dispatchOutput(stream, chunk);
133
175
  return;
134
176
  }
135
177
 
@@ -142,16 +184,10 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
142
184
  const normalized = normalizeCursorStreamLine(rawLine);
143
185
  if (!normalized.line) continue;
144
186
 
145
- if (ctx.onOutput) {
146
- try { await ctx.onOutput(normalized.stream ?? "stdout", `${normalized.line}\n`); } catch { /* swallow */ }
147
- }
187
+ await dispatchOutput(normalized.stream ?? "stdout", `${normalized.line}\n`);
148
188
 
149
- if (ctx.onEvent) {
150
- const event = parseCursorStreamLine(rawLine);
151
- if (event) {
152
- try { await ctx.onEvent(event); } catch { /* swallow */ }
153
- }
154
- }
189
+ const event = parseCursorStreamLine(rawLine);
190
+ if (event) await dispatch(event);
155
191
  }
156
192
  },
157
193
  });
@@ -159,22 +195,17 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
159
195
  // Flush remaining buffer
160
196
  if (stdoutLineBuffer.trim()) {
161
197
  const normalized = normalizeCursorStreamLine(stdoutLineBuffer);
162
- if (normalized.line && ctx.onOutput) {
163
- try { await ctx.onOutput(normalized.stream ?? "stdout", `${normalized.line}\n`); } catch { /* swallow */ }
164
- }
165
- if (ctx.onEvent) {
166
- const event = parseCursorStreamLine(stdoutLineBuffer);
167
- if (event) {
168
- try { await ctx.onEvent(event); } catch { /* swallow */ }
169
- }
170
- }
198
+ if (normalized.line) await dispatchOutput(normalized.stream ?? "stdout", `${normalized.line}\n`);
199
+ const event = parseCursorStreamLine(stdoutLineBuffer);
200
+ if (event) await dispatch(event);
171
201
  }
172
202
 
173
- return proc;
203
+ return { proc, accepted, protocolViolation: protocolViolation || (!accepted && proc.exitCode === 0) };
174
204
  };
175
205
 
176
206
  // 7. Initial attempt
177
- let proc = await runAttempt(sessionId);
207
+ let attempt = await runAttempt(sessionId);
208
+ let proc = attempt.proc;
178
209
  let clearSession = false;
179
210
 
180
211
  // 8. Check for unknown session — retry once
@@ -182,9 +213,11 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
182
213
  sessionId &&
183
214
  !proc.timedOut &&
184
215
  (proc.exitCode ?? 0) !== 0 &&
216
+ !attempt.accepted &&
185
217
  isCursorUnknownSessionError(proc.stdout, proc.stderr)
186
218
  ) {
187
- proc = await runAttempt(null);
219
+ attempt = await runAttempt(null);
220
+ proc = attempt.proc;
188
221
  clearSession = true;
189
222
  }
190
223
 
@@ -193,6 +226,7 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
193
226
  const processErrorCode = deriveErrorCode(proc);
194
227
 
195
228
  let errorCode = processErrorCode;
229
+ if (attempt.protocolViolation) errorCode = "protocol_degraded";
196
230
  if (!errorCode && isCursorAuthRequired(proc.stdout, proc.stderr)) {
197
231
  errorCode = "auth_required";
198
232
  if (ctx.onEvent) {
@@ -219,6 +253,9 @@ export async function executeCursorProvider(ctx: ExecutionContext): Promise<Exec
219
253
  const errorMessage = (() => {
220
254
  if (proc.timedOut) return `Timed out after ${config.timeoutSec ?? 0}s`;
221
255
  if (errorCode === "auth_required") return "Cursor requires authentication.";
256
+ if (errorCode === "protocol_degraded") {
257
+ return "Cursor did not emit the supported stream-json acceptance marker before visible output.";
258
+ }
222
259
  if (parsed.errorMessage) return parsed.errorMessage;
223
260
  if ((proc.exitCode ?? 0) !== 0 && !parsed.summary) {
224
261
  const stderrLine = proc.stderr.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
@@ -1,25 +1,39 @@
1
- import type { ProviderModule } from "../../types.js";
1
+ import type { AgentSession, ProviderModule, SessionContext } from "../../types.js";
2
2
  import { cursorSessionCodec } from "./codec.js";
3
3
  import { resolveAuthForProvider } from "../../utils/auth.js";
4
4
 
5
5
  export const cursorProvider: ProviderModule = {
6
6
  type: "cursor",
7
7
  capabilities: {
8
- sessions: false,
9
- modelDiscovery: false,
8
+ sessions: true,
9
+ modelDiscovery: true,
10
10
  quotaProbing: false,
11
11
  mcp: false,
12
12
  skills: true,
13
13
  instructions: true,
14
14
  workspace: true,
15
- planMode: false,
15
+ planMode: true,
16
16
  concurrentSend: false,
17
17
  cancelQueuedMessage: false,
18
18
  stopTask: false,
19
- modes: false,
19
+ modes: true,
20
+ resume: true,
21
+ modelVariants: false,
22
+ permissionRequests: false,
23
+ questionRequests: false,
24
+ upstreamProviderDisconnect: false,
25
+ sessionModelChange: false,
26
+ sessionVariantChange: false,
27
+ sessionEffortChange: false,
28
+ sessionModeChange: false,
20
29
  },
21
30
  // execute.ts loads lazily on first use.
22
31
  execute: async (ctx) => (await import("./execute.js")).executeCursorProvider(ctx),
32
+ createSession: async (ctx: SessionContext): Promise<AgentSession> =>
33
+ (await import("./session.js")).createCursorSession(ctx),
23
34
  resolveAuth: (ctx) => resolveAuthForProvider("cursor", ctx),
24
35
  sessionCodec: cursorSessionCodec,
36
+ listModels: (options) => import("./discovery.js").then((m) => m.listCursorModels(options)),
37
+ listModes: (options) => import("./discovery.js").then((m) => m.listCursorModes(options)),
38
+ probeCapabilities: (ctx) => import("./probe.js").then((m) => m.probeCursorCapabilities(ctx)),
25
39
  };
@@ -281,7 +281,7 @@ export function isCursorUnknownSessionError(stdout: string, stderr: string): boo
281
281
  return CURSOR_UNKNOWN_SESSION_RE.test(stdout) || CURSOR_UNKNOWN_SESSION_RE.test(stderr);
282
282
  }
283
283
 
284
- const CURSOR_AUTH_RE = /CURSOR_API_KEY\s+is\s+not\s+set|OPENAI_API_KEY\s+is\s+not\s+set|unauthorized|authentication.*required/i;
284
+ const CURSOR_AUTH_RE = /CURSOR_API_KEY\s+is\s+not\s+set|unauthorized|authentication.*required|not\s+logged\s+in/i;
285
285
 
286
286
  export function isCursorAuthRequired(stdout: string, stderr: string): boolean {
287
287
  return CURSOR_AUTH_RE.test(stdout) || CURSOR_AUTH_RE.test(stderr);
@@ -0,0 +1,90 @@
1
+ import type { ProviderRuntimeContext, ProviderRuntimeReport } from "../../types.js";
2
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
3
+ import { runChildProcess } from "../../utils/process.js";
4
+ import { listCursorModels, listCursorModes } from "./discovery.js";
5
+ import { findCursorBinary } from "./runtime.js";
6
+
7
+ export async function probeCursorCapabilities(
8
+ ctx: ProviderRuntimeContext = {},
9
+ ): Promise<ProviderRuntimeReport> {
10
+ let resolved;
11
+ try {
12
+ resolved = await findCursorBinary(ctx);
13
+ } catch (error) {
14
+ return {
15
+ binary: {
16
+ status: "missing",
17
+ command: null,
18
+ version: null,
19
+ protocolProfile: null,
20
+ reason: error instanceof Error ? error.message : String(error),
21
+ },
22
+ capabilities: {},
23
+ };
24
+ }
25
+ const env = buildEnv(ctx.env);
26
+ ensurePathInEnv(env);
27
+ let version: string | null = null;
28
+ try {
29
+ const result = await runChildProcess({
30
+ runId: "cursor-version-probe",
31
+ command: resolved.bin,
32
+ args: [...resolved.prefixArgs, "--version"],
33
+ cwd: ctx.cwd ?? process.cwd(),
34
+ env,
35
+ timeoutSec: 5,
36
+ });
37
+ version = (result.stdout || result.stderr).match(/\d+\.\d+(?:\.\d+)?/)?.[0] ?? null;
38
+ } catch {
39
+ // Feature probes remain authoritative.
40
+ }
41
+ const [models, modes] = await Promise.all([
42
+ listCursorModels(ctx).catch(() => []),
43
+ listCursorModes(ctx).catch(() => []),
44
+ ]);
45
+ const help = await runChildProcess({
46
+ runId: "cursor-protocol-probe",
47
+ command: resolved.bin,
48
+ args: [...resolved.prefixArgs, "--help"],
49
+ cwd: ctx.cwd ?? process.cwd(),
50
+ env,
51
+ timeoutSec: 10,
52
+ }).catch(() => null);
53
+ const helpOutput = help ? `${help.stdout}\n${help.stderr}` : "";
54
+ const protocolSupported = help?.exitCode === 0
55
+ && /(?:^|\s)(?:-p(?:,|\s))?.*--print\b/m.test(helpOutput)
56
+ && /--output-format\b/.test(helpOutput)
57
+ && /\bstream-json\b/i.test(helpOutput)
58
+ && /--resume\b/.test(helpOutput);
59
+ const modelDiscovery = models.length > 0;
60
+ const modeIds = new Set(modes.map((mode) => mode.id));
61
+ const supported = (value: boolean, reason?: string) => ({
62
+ supported: value,
63
+ status: value ? "supported" as const : "upgrade_required" as const,
64
+ ...(!value && reason ? { reason } : {}),
65
+ });
66
+ return {
67
+ binary: {
68
+ status: modelDiscovery && protocolSupported ? "supported" : "upgrade_required",
69
+ command: resolved.bin,
70
+ version,
71
+ protocolProfile: protocolSupported ? "cursor-stream-json-system-init-v1" : null,
72
+ ...(!modelDiscovery
73
+ ? { reason: "Upgrade Cursor CLI to a version with model listing" }
74
+ : !protocolSupported
75
+ ? { reason: "Cursor CLI does not advertise print, stream output, and resume support" }
76
+ : {}),
77
+ },
78
+ capabilities: {
79
+ sessions: supported(Boolean(protocolSupported), "Cursor stream-json session flags are unavailable"),
80
+ resume: supported(Boolean(protocolSupported), "Cursor resume support is unavailable"),
81
+ modelDiscovery: supported(modelDiscovery, "Cursor model listing is unavailable"),
82
+ planMode: supported(modeIds.has("plan")),
83
+ modes: supported(modes.length > 0),
84
+ sessionModelChange: supported(false),
85
+ sessionVariantChange: supported(false),
86
+ sessionEffortChange: supported(false),
87
+ sessionModeChange: supported(false),
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProviderRuntimeContext } from "../../types.js";
2
+ import { findBinary, type ResolvedBinary } from "../../utils/binary.js";
3
+
4
+ export async function findCursorBinary(ctx: ProviderRuntimeContext = {}): Promise<ResolvedBinary> {
5
+ if (ctx.config?.command) return findBinary("agent", ctx.config.command);
6
+ try {
7
+ return await findBinary("agent");
8
+ } catch {
9
+ return findBinary("cursor-agent");
10
+ }
11
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentSession, SessionContext } from "../../types.js";
2
+ import { createExecBackedSession } from "../../sessions/exec-backed.js";
3
+ import { EMULATED_GOAL_CAPABILITY } from "../../goals/index.js";
4
+ import { executeCursorProvider } from "./execute.js";
5
+ import { cursorSessionCodec } from "./codec.js";
6
+
7
+ export async function createCursorSession(ctx: SessionContext): Promise<AgentSession> {
8
+ return createExecBackedSession({
9
+ providerType: "cursor",
10
+ execute: executeCursorProvider,
11
+ sessionCodec: cursorSessionCodec,
12
+ ctx,
13
+ capabilities: { goals: EMULATED_GOAL_CAPABILITY },
14
+ });
15
+ }
@@ -0,0 +1,44 @@
1
+ export class OpenCodeHttpError extends Error {
2
+ constructor(
3
+ readonly status: number,
4
+ readonly code: string,
5
+ ) {
6
+ super(`OpenCode request failed (${status})`);
7
+ this.name = "OpenCodeHttpError";
8
+ }
9
+ }
10
+
11
+ export class OpenCodeClient {
12
+ private readonly authorization: string;
13
+
14
+ constructor(
15
+ readonly baseUrl: string,
16
+ password: string,
17
+ ) {
18
+ this.authorization = `Basic ${Buffer.from(`opencode:${password}`, "utf8").toString("base64")}`;
19
+ }
20
+
21
+ async request(path: string, init: RequestInit = {}): Promise<Response> {
22
+ const headers = new Headers(init.headers);
23
+ headers.set("authorization", this.authorization);
24
+ if (init.body !== undefined && !headers.has("content-type")) {
25
+ headers.set("content-type", "application/json");
26
+ }
27
+ return fetch(`${this.baseUrl}${path}`, { ...init, headers });
28
+ }
29
+
30
+ async json<T>(path: string, init: RequestInit = {}): Promise<T> {
31
+ const response = await this.request(path, init);
32
+ if (!response.ok) {
33
+ throw new OpenCodeHttpError(response.status, `http_${response.status}`);
34
+ }
35
+ return await response.json() as T;
36
+ }
37
+
38
+ async ok(path: string, init: RequestInit = {}): Promise<void> {
39
+ const response = await this.request(path, init);
40
+ if (!response.ok) {
41
+ throw new OpenCodeHttpError(response.status, `http_${response.status}`);
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,69 @@
1
+ import type { ListModelsOptions, ProviderModel } from "../../types.js";
2
+ import { acquireOpenCodeRuntime } from "./runtime.js";
3
+
4
+ function record(value: unknown): Record<string, unknown> {
5
+ return value && typeof value === "object" && !Array.isArray(value)
6
+ ? value as Record<string, unknown>
7
+ : {};
8
+ }
9
+
10
+ function number(value: unknown): number | undefined {
11
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
12
+ }
13
+
14
+ export function openCodeModelsFromPayload(payload: Record<string, unknown>): ProviderModel[] {
15
+ const providers = Array.isArray(payload["all"]) ? payload["all"] : [];
16
+ const models: ProviderModel[] = [];
17
+ for (const rawProvider of providers) {
18
+ const provider = record(rawProvider);
19
+ const providerId = typeof provider["id"] === "string" ? provider["id"] : "";
20
+ const providerName = typeof provider["name"] === "string" ? provider["name"] : providerId;
21
+ if (!providerId) continue;
22
+ for (const [key, rawModel] of Object.entries(record(provider["models"]))) {
23
+ const model = record(rawModel);
24
+ const modelId = typeof model["id"] === "string" ? model["id"] : key;
25
+ if (!modelId) continue;
26
+ const limits = record(model["limit"]);
27
+ const cost = record(model["cost"]);
28
+ const capabilities = record(model["capabilities"]);
29
+ const capabilityInput = record(capabilities["input"]);
30
+ const modalities = record(model["modalities"]);
31
+ const inputModalities = Array.isArray(modalities["input"])
32
+ ? modalities["input"].filter((item): item is string => typeof item === "string")
33
+ : [];
34
+ const variants = Object.entries(record(model["variants"])).map(([id, rawVariant]) => {
35
+ const variant = record(rawVariant);
36
+ return {
37
+ id,
38
+ name: typeof variant["name"] === "string" ? variant["name"] : id,
39
+ ...(typeof variant["description"] === "string" ? { description: variant["description"] } : {}),
40
+ ...(variant["disabled"] === true ? { disabled: true } : {}),
41
+ };
42
+ });
43
+ models.push({
44
+ id: `${providerId}/${modelId}`,
45
+ name: typeof model["name"] === "string" ? model["name"] : modelId,
46
+ provider: providerId,
47
+ providerName,
48
+ ...(variants.length ? { variants } : {}),
49
+ ...(number(limits["context"]) !== undefined ? { contextWindow: number(limits["context"]) } : {}),
50
+ ...(number(limits["output"]) !== undefined ? { maxOutputTokens: number(limits["output"]) } : {}),
51
+ ...(number(cost["input"]) !== undefined ? { inputCostPerMillion: number(cost["input"]) } : {}),
52
+ ...(number(cost["output"]) !== undefined ? { outputCostPerMillion: number(cost["output"]) } : {}),
53
+ supportsImages: capabilityInput["image"] === true || inputModalities.includes("image"),
54
+ supportsTools: capabilities["toolcall"] === true || model["tool_call"] === true,
55
+ });
56
+ }
57
+ }
58
+ return models;
59
+ }
60
+
61
+ export async function listOpenCodeModels(options: ListModelsOptions = {}): Promise<ProviderModel[]> {
62
+ const runtime = await acquireOpenCodeRuntime(options);
63
+ try {
64
+ const payload = await runtime.server.client.json<Record<string, unknown>>("/provider");
65
+ return openCodeModelsFromPayload(payload);
66
+ } finally {
67
+ runtime.server.release();
68
+ }
69
+ }