@bitkyc08/opencodex 2.6.17 → 2.6.18

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 (84) hide show
  1. package/README.md +9 -0
  2. package/bin/ocx.mjs +70 -5
  3. package/gui/dist/assets/index-DDcEW0Cm.css +1 -0
  4. package/gui/dist/assets/index-DbTEyo46.js +9 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +9 -2
  8. package/src/adapters/base.ts +6 -0
  9. package/src/adapters/cursor/arg-codec.ts +38 -0
  10. package/src/adapters/cursor/arg-normalize.ts +88 -0
  11. package/src/adapters/cursor/cursor-errors.ts +85 -0
  12. package/src/adapters/cursor/discovery.ts +144 -0
  13. package/src/adapters/cursor/effort-map.ts +74 -0
  14. package/src/adapters/cursor/exec-policy.ts +44 -0
  15. package/src/adapters/cursor/framing.ts +136 -0
  16. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  17. package/src/adapters/cursor/kv-store.ts +25 -0
  18. package/src/adapters/cursor/live-models.ts +93 -0
  19. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  20. package/src/adapters/cursor/live-transport.ts +758 -0
  21. package/src/adapters/cursor/mcp-config.ts +42 -0
  22. package/src/adapters/cursor/mcp-manager.ts +236 -0
  23. package/src/adapters/cursor/message-mapper.ts +46 -0
  24. package/src/adapters/cursor/native-exec-common.ts +55 -0
  25. package/src/adapters/cursor/native-exec-desktop.ts +177 -0
  26. package/src/adapters/cursor/native-exec-fs.ts +284 -0
  27. package/src/adapters/cursor/native-exec-mcp.ts +151 -0
  28. package/src/adapters/cursor/native-exec-network.ts +32 -0
  29. package/src/adapters/cursor/native-exec-shell.ts +191 -0
  30. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  31. package/src/adapters/cursor/native-exec.ts +177 -0
  32. package/src/adapters/cursor/protobuf-events.ts +309 -0
  33. package/src/adapters/cursor/protobuf-request.ts +347 -0
  34. package/src/adapters/cursor/request-builder.ts +98 -0
  35. package/src/adapters/cursor/tool-definitions.ts +301 -0
  36. package/src/adapters/cursor/transport-retry.ts +116 -0
  37. package/src/adapters/cursor/transport.ts +47 -0
  38. package/src/adapters/cursor/types.ts +36 -0
  39. package/src/adapters/cursor.ts +99 -0
  40. package/src/adapters/google.ts +7 -1
  41. package/src/adapters/kiro.ts +15 -0
  42. package/src/adapters/openai-chat.ts +7 -2
  43. package/src/adapters/run-turn-queue.ts +58 -0
  44. package/src/adapters/tool-catalog-nudge.ts +71 -0
  45. package/src/bridge.ts +7 -1
  46. package/src/cli-help.ts +9 -2
  47. package/src/cli-status.ts +7 -5
  48. package/src/cli.ts +122 -79
  49. package/src/codex-catalog.ts +213 -71
  50. package/src/codex-history-provider.ts +31 -14
  51. package/src/codex-inject.ts +17 -9
  52. package/src/codex-paths.ts +2 -1
  53. package/src/codex-shim.ts +30 -7
  54. package/src/codex-sync.ts +70 -0
  55. package/src/config.ts +58 -2
  56. package/src/doctor.ts +4 -2
  57. package/src/index.ts +1 -0
  58. package/src/model-cache.ts +22 -2
  59. package/src/oauth/callback-server.ts +44 -16
  60. package/src/oauth/cursor.ts +188 -0
  61. package/src/oauth/index.ts +29 -3
  62. package/src/oauth/key-providers.ts +20 -33
  63. package/src/oauth/login-cli.ts +7 -4
  64. package/src/open-url.ts +5 -1
  65. package/src/ports.ts +13 -0
  66. package/src/process-control.ts +76 -0
  67. package/src/provider-label.ts +10 -5
  68. package/src/providers/derive.ts +30 -3
  69. package/src/providers/registry.ts +39 -1
  70. package/src/proxy-liveness.ts +122 -0
  71. package/src/responses/parser.ts +1 -0
  72. package/src/responses/state.ts +83 -0
  73. package/src/router.ts +38 -23
  74. package/src/server/adapter-resolve.ts +3 -0
  75. package/src/server.ts +130 -18
  76. package/src/service.ts +94 -32
  77. package/src/types.ts +24 -1
  78. package/src/update-job.ts +360 -0
  79. package/src/update.ts +73 -11
  80. package/src/usage-log.ts +3 -3
  81. package/src/usage-summary.ts +3 -2
  82. package/src/win-paths.ts +68 -0
  83. package/gui/dist/assets/index-DIBiVVC0.css +0 -1
  84. package/gui/dist/assets/index-DcnD944i.js +0 -9
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-DcnD944i.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DIBiVVC0.css">
19
+ <script type="module" crossorigin src="/assets/index-DbTEyo46.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-DDcEW0Cm.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.17",
3
+ "version": "2.6.18",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -48,6 +48,8 @@
48
48
  "release:watch": "bun scripts/release.ts watch"
49
49
  },
50
50
  "dependencies": {
51
+ "@bufbuild/protobuf": "^2.12.0",
52
+ "@modelcontextprotocol/sdk": "^1",
51
53
  "bun": "1.3.14",
52
54
  "zod": "4.4.3"
53
55
  },
@@ -18,6 +18,7 @@ import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPr
18
18
  import { parseDataUrl } from "./image";
19
19
  import { neutralizeIdentity } from "./identity";
20
20
  import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
21
+ import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
21
22
 
22
23
  /** Map a user content part to an Anthropic content block (text or image source). */
23
24
  function toAnthropicContentPart(p: OcxContentPart): unknown {
@@ -140,8 +141,14 @@ function messagesToAnthropicFormat(
140
141
  parsed: OcxParsedRequest,
141
142
  toolNames: { toWire: (name: string) => string },
142
143
  ): { system: string | undefined; messages: unknown[] } {
143
- const system = parsed.context.systemPrompt?.length
144
- ? neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")) || undefined
144
+ const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(
145
+ parsed.context.tools,
146
+ parsed.options.toolChoice,
147
+ tool => toolNames.toWire(namespacedToolName(tool.namespace, tool.name)),
148
+ );
149
+ const systemParts = [...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : [])];
150
+ const system = systemParts.length
151
+ ? neutralizeIdentity(systemParts.join("\n\n")) || undefined
145
152
  : undefined;
146
153
  const messages: unknown[] = [];
147
154
 
@@ -3,6 +3,7 @@ import type { AdapterEvent, OcxParsedRequest } from "../types";
3
3
  /** Metadata about the caller's incoming request, for auth-forwarding adapters. */
4
4
  export interface IncomingMeta {
5
5
  headers: Headers;
6
+ abortSignal?: AbortSignal;
6
7
  }
7
8
 
8
9
  export interface ProviderAdapter {
@@ -19,6 +20,11 @@ export interface ProviderAdapter {
19
20
 
20
21
  parseStream(response: Response): AsyncGenerator<AdapterEvent>;
21
22
  parseResponse?(response: Response): Promise<AdapterEvent[]>;
23
+ runTurn?(
24
+ parsed: OcxParsedRequest,
25
+ incoming: IncomingMeta,
26
+ emit: (event: AdapterEvent) => void,
27
+ ): Promise<void>;
22
28
  }
23
29
 
24
30
  export interface AdapterRequest {
@@ -0,0 +1,38 @@
1
+ import { fromBinary, toBinary, toJson } from "@bufbuild/protobuf";
2
+ import { ValueSchema } from "@bufbuild/protobuf/wkt";
3
+ import { textDecoder } from "./native-exec-common";
4
+
5
+ function parseJsonText(text: string): unknown {
6
+ try {
7
+ return JSON.parse(text);
8
+ } catch {
9
+ return text;
10
+ }
11
+ }
12
+
13
+ function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
14
+ if (a.length !== b.length) return false;
15
+ for (let i = 0; i < a.length; i++) {
16
+ if (a[i] !== b[i]) return false;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ export function decodeCursorArgValue(value: Uint8Array): unknown {
22
+ try {
23
+ const parsed = fromBinary(ValueSchema, value);
24
+ if (!sameBytes(toBinary(ValueSchema, parsed), value)) throw new Error("not canonical protobuf Value bytes");
25
+ const jsonValue = toJson(ValueSchema, parsed);
26
+ return typeof jsonValue === "string" ? parseJsonText(jsonValue) : jsonValue;
27
+ } catch {
28
+ return parseJsonText(textDecoder.decode(value));
29
+ }
30
+ }
31
+
32
+ export function decodeCursorArgsMap(args: { [key: string]: Uint8Array } | undefined): Record<string, unknown> {
33
+ const out: Record<string, unknown> = {};
34
+ for (const [key, value] of Object.entries(args ?? {})) {
35
+ out[key] = decodeCursorArgValue(value);
36
+ }
37
+ return out;
38
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Schema-aware argument key normalization for Cursor tool calls.
3
+ *
4
+ * Cursor-trained models sometimes emit argument keys that differ from the tool's declared schema
5
+ * (e.g. `filepath` instead of `path`, `cmd` instead of `command`). This module maps common aliases
6
+ * to the canonical key name ONLY when the tool schema declares that canonical key. Keys already
7
+ * matching the schema are never touched.
8
+ *
9
+ * Derived from opencode-cursor reference (src/provider/tool-schema-compat.ts).
10
+ */
11
+
12
+ const KEY_ALIASES = new Map<string, string>([
13
+ ["filepath", "path"],
14
+ ["filename", "path"],
15
+ ["file", "path"],
16
+ ["targetpath", "path"],
17
+ ["directorypath", "path"],
18
+ ["dir", "path"],
19
+ ["folder", "path"],
20
+ ["directory", "path"],
21
+ ["targetdirectory", "path"],
22
+ ["targetfile", "path"],
23
+ ["globpattern", "pattern"],
24
+ ["filepattern", "pattern"],
25
+ ["searchpattern", "pattern"],
26
+ ["includepattern", "include"],
27
+ ["workingdirectory", "cwd"],
28
+ ["workdir", "cwd"],
29
+ ["currentdirectory", "cwd"],
30
+ ["cmd", "command"],
31
+ ["script", "command"],
32
+ ["shellcommand", "command"],
33
+ ["terminalcommand", "command"],
34
+ ["contents", "content"],
35
+ ["text", "content"],
36
+ ["body", "content"],
37
+ ["data", "content"],
38
+ ["payload", "content"],
39
+ ["streamcontent", "content"],
40
+ ["oldstring", "old_string"],
41
+ ["newstring", "new_string"],
42
+ ["oldtext", "old_string"],
43
+ ["newtext", "new_string"],
44
+ ["oldcontent", "old_string"],
45
+ ["newcontent", "new_string"],
46
+ ["recursive", "force"],
47
+ ]);
48
+
49
+ /**
50
+ * Extract the set of declared property names from a JSON Schema parameters object.
51
+ * Handles the common `{ type: "object", properties: { ... } }` shape.
52
+ */
53
+ function schemaPropertyNames(schema: unknown): Set<string> | undefined {
54
+ if (!schema || typeof schema !== "object") return undefined;
55
+ const obj = schema as Record<string, unknown>;
56
+ const props = obj.properties;
57
+ if (!props || typeof props !== "object") return undefined;
58
+ return new Set(Object.keys(props as Record<string, unknown>));
59
+ }
60
+
61
+ /**
62
+ * Normalize argument keys against the tool's declared schema. Keys not in the schema that have a
63
+ * known alias pointing to a schema-declared key are renamed. Keys already in the schema or with no
64
+ * matching alias are left untouched.
65
+ *
66
+ * Returns the original object reference if no changes were needed (cheap identity check for callers).
67
+ */
68
+ export function normalizeArgKeys(args: Record<string, unknown>, toolSchema: unknown): Record<string, unknown> {
69
+ const declared = schemaPropertyNames(toolSchema);
70
+ if (!declared || declared.size === 0) return args;
71
+
72
+ let changed = false;
73
+ const result: Record<string, unknown> = {};
74
+ for (const [key, value] of Object.entries(args)) {
75
+ if (declared.has(key)) {
76
+ result[key] = value;
77
+ continue;
78
+ }
79
+ const canonical = KEY_ALIASES.get(key.toLowerCase());
80
+ if (canonical && declared.has(canonical) && !(canonical in result)) {
81
+ result[canonical] = value;
82
+ changed = true;
83
+ } else {
84
+ result[key] = value;
85
+ }
86
+ }
87
+ return changed ? result : args;
88
+ }
@@ -0,0 +1,85 @@
1
+ import { redactSecretString } from "../../redact";
2
+
3
+ const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|[A-Za-z]:\\Users\\[^ "';,]+)/g;
4
+ // Cursor error messages can contain raw credential key=value pairs beyond what the shared
5
+ // redactSecretString covers. We handle the additional transport-specific patterns locally.
6
+ const CURSOR_CREDENTIAL_PATTERN = /\b(authorization|auth[_-]?token|cursor[_-]?token)=([^&\s"',;]+)/gi;
7
+
8
+ function sanitize(value: string): string {
9
+ return redactSecretString(value)
10
+ .replace(CURSOR_CREDENTIAL_PATTERN, "$1=[REDACTED]")
11
+ .replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
12
+ }
13
+
14
+ /**
15
+ * Classify a Cursor transport/Connect/gRPC error message into an actionable category.
16
+ * The returned prefix string is recognized by `src/errors.ts` `classifyError` keywords,
17
+ * so bridge-level error mapping produces the right Codex error type (rate_limit, auth, etc.).
18
+ */
19
+ export function classifyCursorError(message: string): string {
20
+ const lower = message.toLowerCase();
21
+
22
+ if (
23
+ lower.includes("resource_exhausted") ||
24
+ lower.includes("resource exhausted") ||
25
+ lower.includes("rate limit") ||
26
+ lower.includes("rate-limit") ||
27
+ lower.includes("too many requests") ||
28
+ lower.includes("quota")
29
+ ) return "Cursor rate limit exceeded";
30
+
31
+ if (
32
+ lower.includes("unauthenticated") ||
33
+ lower.includes("unauthorized") ||
34
+ lower.includes("permission_denied") ||
35
+ lower.includes("permission denied") ||
36
+ lower.includes("forbidden") ||
37
+ lower.includes("invalid token") ||
38
+ lower.includes("expired token") ||
39
+ lower.includes("authentication") ||
40
+ lower.includes("access denied")
41
+ ) return "Cursor authentication failed";
42
+
43
+ if (
44
+ lower.includes("unavailable") ||
45
+ lower.includes("overloaded") ||
46
+ lower.includes("temporarily") ||
47
+ lower.includes("server is busy")
48
+ ) return "Cursor server overloaded";
49
+
50
+ if (
51
+ lower.includes("invalid") ||
52
+ lower.includes("not found") ||
53
+ lower.includes("unsupported") ||
54
+ lower.includes("malformed") ||
55
+ lower.includes("unimplemented")
56
+ ) return "Cursor invalid request";
57
+
58
+ if (
59
+ lower.includes("timed out") ||
60
+ lower.includes("timeout") ||
61
+ lower.includes("etimedout") ||
62
+ lower.includes("deadline")
63
+ ) return "Cursor request timed out";
64
+
65
+ if (
66
+ lower.includes("econnreset") ||
67
+ lower.includes("econnrefused") ||
68
+ lower.includes("goaway") ||
69
+ lower.includes("nghttp2") ||
70
+ lower.includes("socket hang up") ||
71
+ lower.includes("connection reset")
72
+ ) return "Cursor connection failed";
73
+
74
+ return "Cursor upstream error";
75
+ }
76
+
77
+ /**
78
+ * Produce a user-facing, secret-safe Cursor error message with an actionable category prefix.
79
+ * Mirrors `safeKiroErrorMessage` / `safeKiroHttpErrorMessage` in kiro-errors.ts.
80
+ */
81
+ export function safeCursorErrorMessage(rawMessage: string): string {
82
+ const detail = sanitize(rawMessage).slice(0, 500);
83
+ const prefix = classifyCursorError(rawMessage);
84
+ return detail ? `${prefix}: ${detail}` : prefix;
85
+ }
@@ -0,0 +1,144 @@
1
+ export interface CursorModelInfo {
2
+ id: string;
3
+ contextWindow?: number;
4
+ supportsReasoningEffort?: boolean;
5
+ inputModalities?: string[];
6
+ }
7
+
8
+ export const CURSOR_DEFAULT_CONTEXT_WINDOW = 128_000;
9
+
10
+ const CURSOR_REASONING_EFFORTS = ["low", "medium", "high"] as const;
11
+ const CURSOR_DEFAULT_INPUT_MODALITIES = ["text", "image"] as const;
12
+ const CONTEXT_1M = 1_000_000;
13
+ const CONTEXT_GEMINI = 1_048_576;
14
+ const CONTEXT_272K = 272_000;
15
+ const CONTEXT_262K = 262_144;
16
+ const CONTEXT_256K = 256_000;
17
+ const CONTEXT_200K = 200_000;
18
+
19
+ export function inferCursorContextWindow(modelId: string): number {
20
+ const id = modelId.trim().toLowerCase();
21
+ if (id.includes("1m")) return CONTEXT_1M;
22
+ if (id.startsWith("gemini-")) return CONTEXT_1M;
23
+ if (id === "glm-5.2") return CONTEXT_1M;
24
+ if (id.startsWith("gpt-5") || id === "gpt-5-codex") return CONTEXT_272K;
25
+ if (id.startsWith("grok-")) return CONTEXT_256K;
26
+ if (id.includes("claude")) return CONTEXT_200K;
27
+ return CURSOR_DEFAULT_CONTEXT_WINDOW;
28
+ }
29
+
30
+ function normalizeInputModalities(input: string[] | undefined): string[] {
31
+ const values = (input ?? [...CURSOR_DEFAULT_INPUT_MODALITIES])
32
+ .map(item => item.trim())
33
+ .filter(Boolean);
34
+ return values.length > 0 ? [...new Set(values)] : [...CURSOR_DEFAULT_INPUT_MODALITIES];
35
+ }
36
+
37
+ export function normalizeCursorModels(models: readonly CursorModelInfo[]): CursorModelInfo[] {
38
+ const byId = new Map<string, CursorModelInfo>();
39
+ for (const model of models) {
40
+ const id = model.id.trim();
41
+ if (!id || byId.has(id)) continue;
42
+ byId.set(id, {
43
+ id,
44
+ contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
45
+ ? model.contextWindow
46
+ : inferCursorContextWindow(id),
47
+ supportsReasoningEffort: model.supportsReasoningEffort === true,
48
+ inputModalities: normalizeInputModalities(model.inputModalities),
49
+ });
50
+ }
51
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
52
+ }
53
+
54
+ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
55
+ // Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
56
+ // SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real
57
+ // GetUsableModels catalog. Live discovery is the preferred path when logged in; these ids seed the
58
+ // routed Codex catalog and provide a static fallback. Cursor base ids carry no effort suffix here —
59
+ // the request builder appends the per-model suffix (see effort-map.ts) and reasoning models
60
+ // advertise effort so Codex exposes the tier picker. `supportsReasoningEffort` tracks whether the
61
+ // model has *selectable effort tiers* (CURSOR_MODEL_EFFORT_TIERS), NOT merely whether it reasons:
62
+ // gemini/grok/kimi/gpt-5-mini are reasoning models in the SOT but are sent bare (no tier picker).
63
+ { id: "auto", contextWindow: CONTEXT_200K, supportsReasoningEffort: false },
64
+
65
+ { id: "claude-sonnet-5", contextWindow: CONTEXT_200K },
66
+ { id: "claude-4-sonnet", contextWindow: CONTEXT_200K },
67
+ { id: "claude-4-sonnet-1m", contextWindow: CONTEXT_1M },
68
+ { id: "claude-4.5-haiku", contextWindow: CONTEXT_200K },
69
+ { id: "claude-4.5-sonnet", contextWindow: CONTEXT_200K },
70
+ { id: "claude-4.5-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
71
+ { id: "claude-4.6-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
72
+ { id: "claude-4.6-sonnet", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
73
+ { id: "claude-opus-4-7", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
74
+ { id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
75
+ { id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
76
+
77
+ { id: "composer-1", contextWindow: CONTEXT_200K },
78
+ { id: "composer-1.5", contextWindow: CONTEXT_200K },
79
+ { id: "composer-2", contextWindow: CONTEXT_200K },
80
+ { id: "composer-2.5", contextWindow: CONTEXT_200K },
81
+ { id: "composer-2.5-fast", contextWindow: CONTEXT_200K },
82
+
83
+ { id: "gemini-2.5-flash", contextWindow: CONTEXT_GEMINI },
84
+ { id: "gemini-3-flash", contextWindow: CONTEXT_GEMINI },
85
+ { id: "gemini-3-pro", contextWindow: CONTEXT_GEMINI },
86
+ { id: "gemini-3-pro-image-preview", contextWindow: CONTEXT_200K },
87
+ { id: "gemini-3.1-pro", contextWindow: CONTEXT_GEMINI },
88
+ { id: "gemini-3.5-flash", contextWindow: CONTEXT_200K },
89
+
90
+ { id: "gpt-5-codex", contextWindow: CONTEXT_272K },
91
+ { id: "gpt-5-fast", contextWindow: CONTEXT_272K },
92
+ { id: "gpt-5-mini", contextWindow: CONTEXT_272K },
93
+ { id: "gpt-5.1", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
94
+ { id: "gpt-5.1-codex", contextWindow: CONTEXT_272K },
95
+ { id: "gpt-5.1-codex-max", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
96
+ { id: "gpt-5.1-codex-mini", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
97
+ { id: "gpt-5.2", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
98
+ { id: "gpt-5.2-codex", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
99
+ { id: "gpt-5.3-codex", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
100
+ { id: "gpt-5.4", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
101
+ { id: "gpt-5.4-mini", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
102
+ { id: "gpt-5.4-nano", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
103
+ { id: "gpt-5.5", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
104
+ { id: "gpt-5.5-extra", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
105
+
106
+ { id: "grok-4.3", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
107
+ { id: "grok-4.20", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
108
+ { id: "grok-build-0.1", contextWindow: CONTEXT_256K },
109
+ { id: "grok-code-fast-1", contextWindow: CONTEXT_256K },
110
+
111
+ { id: "glm-5.2", contextWindow: CONTEXT_200K },
112
+ { id: "kimi-k2.5", contextWindow: CONTEXT_262K },
113
+ ]);
114
+
115
+ export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
116
+ return normalizeCursorModels(models).map(model => model.id);
117
+ }
118
+
119
+ export function cursorModelContextWindows(
120
+ models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS,
121
+ ): Record<string, number> {
122
+ return Object.fromEntries(
123
+ normalizeCursorModels(models).map(model => [model.id, model.contextWindow ?? inferCursorContextWindow(model.id)]),
124
+ );
125
+ }
126
+
127
+ export function cursorModelInputModalities(
128
+ models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS,
129
+ ): Record<string, string[]> {
130
+ return Object.fromEntries(
131
+ normalizeCursorModels(models).map(model => [model.id, normalizeInputModalities(model.inputModalities)]),
132
+ );
133
+ }
134
+
135
+ export function cursorModelReasoningEfforts(
136
+ models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS,
137
+ ): Record<string, string[]> {
138
+ return Object.fromEntries(
139
+ normalizeCursorModels(models).map(model => [
140
+ model.id,
141
+ model.supportsReasoningEffort === true ? [...CURSOR_REASONING_EFFORTS] : [],
142
+ ]),
143
+ );
144
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Per-model Cursor reasoning-effort mapping.
3
+ *
4
+ * Cursor model ids encode the reasoning effort as a suffix (`claude-4.6-opus-high`), and the available
5
+ * tiers differ per model — `claude-4.6-opus` tops out at `-max`, `claude-opus-4-8` at `-xhigh`,
6
+ * `claude-4.6-sonnet` only has `-medium`, and `composer`/`grok`/`gemini` take no suffix at all. A bare
7
+ * id for a model that requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105).
8
+ *
9
+ * `CURSOR_MODEL_EFFORT_TIERS` is the real catalog (from the Cursor `GetUsableModels` naming, mirrored in
10
+ * jawcode's bundle), each base model -> its available suffixes in ascending order. `cursorEffortSuffix`
11
+ * clamps a Codex reasoning effort onto that model's tiers so Codex's TOP effort maps to the model's TOP
12
+ * tier (a `-max` model -> max, an `-xhigh` model -> xhigh), per the requested behavior.
13
+ */
14
+
15
+ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
16
+ "claude-4.5-opus": ["high"],
17
+ "claude-4.6-opus": ["high", "max"],
18
+ "claude-4.6-sonnet": ["medium"],
19
+ "claude-fable-5": ["low", "medium", "high", "max", "xhigh"],
20
+ "claude-opus-4-7": ["low", "medium", "high", "max", "xhigh"],
21
+ "claude-opus-4-8": ["low", "medium", "high", "max", "xhigh"],
22
+ "gpt-5.1": ["low", "high"],
23
+ "gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"],
24
+ "gpt-5.1-codex-mini": ["low", "high"],
25
+ "gpt-5.2": ["low", "high", "xhigh"],
26
+ "gpt-5.2-codex": ["low", "high", "xhigh"],
27
+ "gpt-5.3-codex": ["low", "high", "xhigh"],
28
+ "gpt-5.4": ["low", "medium", "high", "xhigh"],
29
+ "gpt-5.4-mini": ["low", "medium", "high", "xhigh"],
30
+ "gpt-5.4-nano": ["low", "medium", "high", "xhigh"],
31
+ "gpt-5.5": ["low", "medium", "high"],
32
+ "gpt-5.5-extra": ["high"],
33
+ };
34
+
35
+ /** Collapse a Codex reasoning-effort label to a low/medium/high rank for clamping onto a model's tiers. */
36
+ function codexEffortRank(reasoning: string | undefined): "low" | "medium" | "high" {
37
+ switch ((reasoning ?? "").toLowerCase()) {
38
+ case "none":
39
+ case "minimal":
40
+ case "low":
41
+ return "low";
42
+ case "medium":
43
+ return "medium";
44
+ case "high":
45
+ case "max":
46
+ case "xhigh":
47
+ return "high";
48
+ default:
49
+ // No explicit effort: a bare reasoning-model id is invalid, so pick the model's top tier.
50
+ return "high";
51
+ }
52
+ }
53
+
54
+ /**
55
+ * The Cursor effort suffix to use for `baseModelId` given a Codex reasoning effort, or `undefined` when
56
+ * the model takes no suffix (bare). Codex's top effort maps to the model's top tier (max / xhigh).
57
+ */
58
+ export function cursorEffortSuffix(baseModelId: string, reasoning: string | undefined): string | undefined {
59
+ const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
60
+ if (!tiers || tiers.length === 0) return undefined;
61
+ switch (codexEffortRank(reasoning)) {
62
+ case "low":
63
+ return tiers[0];
64
+ case "high":
65
+ return tiers[tiers.length - 1];
66
+ case "medium":
67
+ return tiers[Math.floor((tiers.length - 1) / 2)];
68
+ }
69
+ }
70
+
71
+ /** Base models known to carry a reasoning-effort suffix (everything else is sent bare). */
72
+ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
73
+ return (CURSOR_MODEL_EFFORT_TIERS[baseModelId]?.length ?? 0) > 0;
74
+ }
@@ -0,0 +1,44 @@
1
+ import type { CursorClientMessage } from "./types";
2
+
3
+ export const CURSOR_EXEC_CASES_DENIED = [
4
+ "readArgs",
5
+ "lsArgs",
6
+ "grepArgs",
7
+ "writeArgs",
8
+ "deleteArgs",
9
+ "shellArgs",
10
+ "shellStreamArgs",
11
+ "diagnosticsArgs",
12
+ "mcpArgs",
13
+ "fetchArgs",
14
+ "recordScreenArgs",
15
+ "computerUseArgs",
16
+ "unknownExecCase",
17
+ ] as const;
18
+
19
+ export type CursorDeniedExecCase = (typeof CURSOR_EXEC_CASES_DENIED)[number];
20
+
21
+ export function cursorExecDeniedMessage(execCase: string): string {
22
+ return [
23
+ `Cursor legacy mock transport cannot execute ${execCase}.`,
24
+ "Production Cursor requests use the live protobuf native exec bridge.",
25
+ "The legacy mock path returns a non-executing placeholder for tests only.",
26
+ ].join(" ");
27
+ }
28
+
29
+ export function cursorExecResult(requestId: string, execCase: string): CursorClientMessage {
30
+ if (execCase === "requestContextArgs") {
31
+ return {
32
+ type: "exec_result",
33
+ requestId,
34
+ ok: true,
35
+ message: "Cursor request context is empty in legacy mock transport mode.",
36
+ };
37
+ }
38
+ return {
39
+ type: "exec_result",
40
+ requestId,
41
+ ok: false,
42
+ message: cursorExecDeniedMessage(execCase),
43
+ };
44
+ }