@bitkyc08/opencodex 2.10.2 → 2.11.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 +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BynIEIV-.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bk-PN-70.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.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -49,8 +49,10 @@
|
|
|
49
49
|
"prepublishOnly": "bun run audit:high && bun run typecheck && bun run build:gui",
|
|
50
50
|
"release": "bun scripts/release.ts",
|
|
51
51
|
"release:watch": "bun scripts/release.ts watch",
|
|
52
|
-
"prepush": "bun run typecheck && bun run lint:gui && bun run test && bun run privacy:scan && bun run doctor:gui:if-changed",
|
|
52
|
+
"prepush": "bun run typecheck && bun run lint:gui:if-changed && bun run test && bun run privacy:scan && bun run doctor:gui:if-changed",
|
|
53
53
|
"lint:gui": "cd gui && bun run lint",
|
|
54
|
+
"lint:gui:if-changed": "bun scripts/lint-gui-if-changed.ts",
|
|
55
|
+
"postmerge": "bun scripts/build-gui-if-changed.ts",
|
|
54
56
|
"doctor:gui": "cd gui && bun run doctor",
|
|
55
57
|
"doctor:gui:full": "cd gui && bun run doctor:full",
|
|
56
58
|
"doctor:gui:if-changed": "bun scripts/doctor-gui-if-changed.ts",
|
|
@@ -126,3 +126,14 @@ export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: s
|
|
|
126
126
|
}
|
|
127
127
|
return `${baseModelId}-${effortSuffix}`;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compose the exact flattened id sent by AgentService/Run. Discovery normalizes Cursor's optional
|
|
132
|
+
* `cursor-` prefix only for catalog matching, but regular Grok 4.5 requests require that prefix on
|
|
133
|
+
* the wire. Keep this separate from {@link cursorWireModelIdWithEffort} so discovery can continue
|
|
134
|
+
* comparing canonical, prefix-free ids. Grok Fast uses requested_model parameters instead.
|
|
135
|
+
*/
|
|
136
|
+
export function cursorRequestWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
|
|
137
|
+
const flattened = cursorWireModelIdWithEffort(baseModelId, effortSuffix);
|
|
138
|
+
return baseModelId === "grok-4.5" ? `cursor-${flattened}` : flattened;
|
|
139
|
+
}
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
cursorToolArgNormalizeSchema,
|
|
71
71
|
cursorToolWireName,
|
|
72
72
|
cursorToolsForActivePrompt,
|
|
73
|
+
isCursorSyntheticStructuredEditTool,
|
|
73
74
|
isGenericToolUseCountDemoPrompt,
|
|
74
75
|
requestedCursorToolUseCount,
|
|
75
76
|
} from "./tool-definitions";
|
|
@@ -540,10 +541,19 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
540
541
|
this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(request, this.clientToolFinalizeGraceMs);
|
|
541
542
|
const cursorVisibleTools = cursorToolsForActivePrompt(request.tools, activeText, request.toolChoice);
|
|
542
543
|
const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, request.toolChoice);
|
|
544
|
+
// `request.tools` is the catalog already filtered and budgeted by request-builder. Derive
|
|
545
|
+
// conversion provenance only from tagged synthetic tools that also survive this final prompt
|
|
546
|
+
// filter; a client tool with the same wire name can never opt into conversion by collision.
|
|
547
|
+
const syntheticStructuredEditToolNames = new Set(
|
|
548
|
+
(cursorVisibleTools ?? [])
|
|
549
|
+
.filter(isCursorSyntheticStructuredEditTool)
|
|
550
|
+
.map(cursorToolWireName),
|
|
551
|
+
);
|
|
543
552
|
this.execContext = {
|
|
544
553
|
...this.execContext,
|
|
545
554
|
clientToolDefs,
|
|
546
555
|
rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
|
|
556
|
+
structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
|
|
547
557
|
};
|
|
548
558
|
const toolSchemas = new Map<string, unknown>();
|
|
549
559
|
const cursorToolNameMap = new Map<string, string>();
|
|
@@ -571,6 +581,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
571
581
|
parallelToolCalls: request.parallelToolCalls,
|
|
572
582
|
toolSchemas,
|
|
573
583
|
cursorToolNameMap,
|
|
584
|
+
syntheticStructuredEditToolNames,
|
|
574
585
|
translatorBudget: this.translatorBudget,
|
|
575
586
|
contextUsage,
|
|
576
587
|
...(prepared.estimatedInputTokens !== undefined
|
|
@@ -39,8 +39,11 @@ const MAX_GREP_FILES = 500;
|
|
|
39
39
|
const MAX_GREP_RESULTS = 200;
|
|
40
40
|
const MAX_FILE_BYTES = 1_000_000;
|
|
41
41
|
|
|
42
|
-
function codexNativeMutationRefusal(operation: "write" | "delete"): string {
|
|
43
|
-
|
|
42
|
+
function codexNativeMutationRefusal(operation: "write" | "delete", structuredEditAvailable: boolean): string {
|
|
43
|
+
const structuredHint = structuredEditAvailable
|
|
44
|
+
? " Use the structured edit tools (`edit_file` / `multi_edit`) or the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout."
|
|
45
|
+
: " Use the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout.";
|
|
46
|
+
return `Cursor-native ${operation} is disabled for this Codex request because apply_patch is available.${structuredHint} No file was changed.`;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const NATIVE_LOCAL_EXEC_DISABLED =
|
|
@@ -84,13 +87,13 @@ export function readExec(execMsg: ExecServerMessage): Uint8Array {
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage): Uint8Array {
|
|
90
|
+
export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false): Uint8Array {
|
|
88
91
|
if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
|
|
89
92
|
const path = resolve(execMsg.message.value.path);
|
|
90
93
|
return execBytes(execMsg, "writeResult", create(WriteResultSchema, {
|
|
91
94
|
result: {
|
|
92
95
|
case: "rejected",
|
|
93
|
-
value: create(WriteRejectedSchema, { path, reason: codexNativeMutationRefusal("write") }),
|
|
96
|
+
value: create(WriteRejectedSchema, { path, reason: codexNativeMutationRefusal("write", structuredEditAvailable) }),
|
|
94
97
|
},
|
|
95
98
|
}));
|
|
96
99
|
}
|
|
@@ -133,13 +136,13 @@ export function writeExec(execMsg: ExecServerMessage): Uint8Array {
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage): Uint8Array {
|
|
139
|
+
export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false): Uint8Array {
|
|
137
140
|
if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
|
|
138
141
|
const path = resolve(execMsg.message.value.path);
|
|
139
142
|
return execBytes(execMsg, "deleteResult", create(DeleteResultSchema, {
|
|
140
143
|
result: {
|
|
141
144
|
case: "rejected",
|
|
142
|
-
value: create(DeleteRejectedSchema, { path, reason: codexNativeMutationRefusal("delete") }),
|
|
145
|
+
value: create(DeleteRejectedSchema, { path, reason: codexNativeMutationRefusal("delete", structuredEditAvailable) }),
|
|
143
146
|
},
|
|
144
147
|
}));
|
|
145
148
|
}
|
|
@@ -70,6 +70,8 @@ export interface CursorNativeExecContext extends CursorNativeExecDeps {
|
|
|
70
70
|
unsafeAllowNativeLocalExec?: boolean;
|
|
71
71
|
/** apply_patch is visible for this request; Cursor-native write/delete must not bypass Codex. */
|
|
72
72
|
rejectNativeFileMutations?: boolean;
|
|
73
|
+
/** The synthetic exact-match edit tools (edit_file / multi_edit) are advertised this request. */
|
|
74
|
+
structuredEditAvailable?: boolean;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec"> = {}): boolean {
|
|
@@ -514,8 +516,8 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
|
|
|
514
516
|
if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg)];
|
|
515
517
|
}
|
|
516
518
|
if (execCase === "readArgs") return [readExec(execMsg)];
|
|
517
|
-
if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg) : writeExec(execMsg)];
|
|
518
|
-
if (execCase === "deleteArgs") return [deps.rejectNativeFileMutations ? rejectDeleteExecForApplyPatch(execMsg) : deleteExec(execMsg)];
|
|
519
|
+
if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : writeExec(execMsg)];
|
|
520
|
+
if (execCase === "deleteArgs") return [deps.rejectNativeFileMutations ? rejectDeleteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : deleteExec(execMsg)];
|
|
519
521
|
if (execCase === "lsArgs") return [lsExec(execMsg)];
|
|
520
522
|
if (execCase === "grepArgs") return [grepExec(execMsg)];
|
|
521
523
|
if (execCase === "shellArgs") return [shellExec(execMsg)];
|
|
@@ -3,10 +3,13 @@ import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
|
|
|
3
3
|
import { decodeCursorArgsMap } from "./arg-codec";
|
|
4
4
|
import { normalizeArgKeys } from "./arg-normalize";
|
|
5
5
|
import {
|
|
6
|
+
CODEX_APPLY_PATCH_TOOL,
|
|
7
|
+
CURSOR_MULTI_EDIT_TOOL,
|
|
6
8
|
cursorShellBridgeArgsValid,
|
|
7
9
|
cursorShellBridgeDropError,
|
|
8
10
|
defaultShellBridgeArgNormalizeSchema,
|
|
9
11
|
isCodexShellBridgeToolName,
|
|
12
|
+
isCursorStructuredEditToolName,
|
|
10
13
|
normalizeCursorWireName,
|
|
11
14
|
OCX_RESPONSES_TOOL_PROVIDER,
|
|
12
15
|
resolveShellBridgeAliasKey,
|
|
@@ -161,14 +164,41 @@ export interface CursorProtobufEventState {
|
|
|
161
164
|
toolSchemas?: Map<string, unknown>;
|
|
162
165
|
/** Cursor wire-name → original Responses/Codex tool name for this request. */
|
|
163
166
|
cursorToolNameMap?: Map<string, string>;
|
|
167
|
+
/**
|
|
168
|
+
* Bare names WE advertised as synthetic structured-edit tools on this request.
|
|
169
|
+
* See structuredEditCallIsOurs: conversion is gated on provenance, not on the name.
|
|
170
|
+
*/
|
|
171
|
+
syntheticStructuredEditToolNames?: ReadonlySet<string>;
|
|
164
172
|
translatorBudget?: TranslatorBudget;
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Did WE advertise this bare tool name as a synthetic structured-edit tool on this request?
|
|
178
|
+
*
|
|
179
|
+
* Provenance, not a name test. `edit_file` / `multi_edit` are ordinary names a client or MCP
|
|
180
|
+
* server may legitimately expose, and `cursorStructuredEditTools` already refuses to shadow one
|
|
181
|
+
* that exists. Converting on the name alone would undo that refusal at the other end of the
|
|
182
|
+
* request: the client's own call would be silently re-emitted as `apply_patch`, or dropped with
|
|
183
|
+
* an error naming a conversion the user never asked for.
|
|
184
|
+
*
|
|
185
|
+
* Absent set = we advertised nothing, so nothing converts. Fail-closed in the safe direction:
|
|
186
|
+
* an unconverted structured call is a visible, recoverable failure; a wrongly converted one
|
|
187
|
+
* edits a file.
|
|
188
|
+
*/
|
|
189
|
+
function structuredEditCallIsOurs(
|
|
190
|
+
advertised: ReadonlySet<string> | undefined,
|
|
191
|
+
toolName: string,
|
|
192
|
+
): boolean {
|
|
193
|
+
return advertised?.has(toolName) === true;
|
|
194
|
+
}
|
|
195
|
+
|
|
167
196
|
export function createCursorProtobufEventState(options: {
|
|
168
197
|
clientToolNames?: Iterable<string>;
|
|
169
198
|
parallelToolCalls?: boolean;
|
|
170
199
|
toolSchemas?: Map<string, unknown>;
|
|
171
200
|
cursorToolNameMap?: Map<string, string>;
|
|
201
|
+
syntheticStructuredEditToolNames?: Iterable<string>;
|
|
172
202
|
contextUsage?: CursorContextUsageControls;
|
|
173
203
|
/**
|
|
174
204
|
* Request-local input estimate derived from the payload actually sent. Used only
|
|
@@ -185,6 +215,9 @@ export function createCursorProtobufEventState(options: {
|
|
|
185
215
|
openToolCalls: new Map(),
|
|
186
216
|
completedToolCalls: new Set(),
|
|
187
217
|
...(options.clientToolNames ? { clientToolNames: new Set(options.clientToolNames) } : {}),
|
|
218
|
+
...(options.syntheticStructuredEditToolNames
|
|
219
|
+
? { syntheticStructuredEditToolNames: new Set(options.syntheticStructuredEditToolNames) }
|
|
220
|
+
: {}),
|
|
188
221
|
...(options.parallelToolCalls !== undefined ? { parallelToolCalls: options.parallelToolCalls } : {}),
|
|
189
222
|
startedClientToolCalls: 0,
|
|
190
223
|
...(options.toolSchemas ? { toolSchemas: options.toolSchemas } : {}),
|
|
@@ -311,6 +344,117 @@ function resolveCompletedArgs(buffered: string, args: McpArgs | undefined, state
|
|
|
311
344
|
return "";
|
|
312
345
|
}
|
|
313
346
|
|
|
347
|
+
const PATCH_BEGIN = "*** Begin Patch";
|
|
348
|
+
const PATCH_END = "*** End Patch";
|
|
349
|
+
|
|
350
|
+
function firstStringArg(args: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
|
351
|
+
for (const key of keys) {
|
|
352
|
+
const value = args[key];
|
|
353
|
+
if (typeof value === "string") return value;
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Split a replacement into patch lines, ignoring one trailing newline (line-based patch semantics). */
|
|
359
|
+
function patchLines(text: string): string[] {
|
|
360
|
+
const lines = text.split("\n");
|
|
361
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
362
|
+
return lines;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** One `@@` hunk replacing `oldString` with `newString`. */
|
|
366
|
+
function replacementHunk(oldString: string, newString: string): { hunk: string } | { error: string } {
|
|
367
|
+
if (oldString.length === 0) {
|
|
368
|
+
return {
|
|
369
|
+
error:
|
|
370
|
+
"structured edit requires a non-empty old_string to locate the replacement; for new files or insertions without existing text, call apply_patch with an `*** Add File` / context hunk or use the shell bridge",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const oldLines = patchLines(oldString);
|
|
374
|
+
const newLines = patchLines(newString);
|
|
375
|
+
// Line-based patch semantics cannot express an edit that only adds or removes the file's
|
|
376
|
+
// final newline, and an old/new pair that normalizes to the same lines is a silent no-op —
|
|
377
|
+
// reject it rather than emitting an empty hunk that apply_patch would drop.
|
|
378
|
+
if (oldLines.length === 0 && newLines.length === 0) {
|
|
379
|
+
return { error: "structured edit requires a non-empty old_string; an empty replacement is not a valid edit" };
|
|
380
|
+
}
|
|
381
|
+
if (oldLines.length === newLines.length && oldLines.every((line, i) => line === newLines[i])) {
|
|
382
|
+
return { error: "structured edit old_string and new_string are identical after line normalization; the replacement is a no-op and was dropped" };
|
|
383
|
+
}
|
|
384
|
+
const removed = oldLines.map(line => `-${line}`);
|
|
385
|
+
const added = newLines.map(line => `+${line}`);
|
|
386
|
+
return { hunk: ["@@", ...removed, ...added].join("\n") };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Convert a completed Cursor structured edit call (`edit_file` / `multi_edit`) into a valid Codex
|
|
391
|
+
* apply_patch freeform payload (#1017). Cursor-trained models cannot emit Codex's freeform patch
|
|
392
|
+
* grammar, so the adapter advertises exact-match replacement tools and performs the grammar here.
|
|
393
|
+
* Returns `{ patch }` for a valid conversion, `{ error }` for a malformed call (which must never be
|
|
394
|
+
* relayed verbatim: Codex would reject it locally after the HTTP 200, the reported failure mode),
|
|
395
|
+
* and `undefined` for tools that are not structured edits.
|
|
396
|
+
*/
|
|
397
|
+
export type StructuredEditTranslation =
|
|
398
|
+
| { patch: string; error?: undefined }
|
|
399
|
+
| { error: string; patch?: undefined };
|
|
400
|
+
|
|
401
|
+
export function translateStructuredEditCall(
|
|
402
|
+
toolName: string,
|
|
403
|
+
argsText: string,
|
|
404
|
+
): StructuredEditTranslation | undefined {
|
|
405
|
+
if (!isCursorStructuredEditToolName(toolName)) return undefined;
|
|
406
|
+
let parsed: unknown;
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(argsText);
|
|
409
|
+
} catch {
|
|
410
|
+
return {
|
|
411
|
+
error: `${toolName} arguments were not valid JSON; the call was dropped. ${
|
|
412
|
+
toolName === CURSOR_MULTI_EDIT_TOOL
|
|
413
|
+
? "Use file_path and edits[] (each edit with old_string and new_string)."
|
|
414
|
+
: "Use file_path, old_string and new_string."
|
|
415
|
+
}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
419
|
+
return { error: `${toolName} arguments must be a JSON object; the call was dropped.` };
|
|
420
|
+
}
|
|
421
|
+
const args = parsed as Record<string, unknown>;
|
|
422
|
+
const path = firstStringArg(args, ["file_path", "filePath", "path", "filepath", "filename"]);
|
|
423
|
+
if (!path || path.trim().length === 0) {
|
|
424
|
+
return { error: `${toolName} is missing a non-empty file_path; the call was dropped.` };
|
|
425
|
+
}
|
|
426
|
+
const hunks: string[] = [];
|
|
427
|
+
const addReplacement = (record: Record<string, unknown>): StructuredEditTranslation => {
|
|
428
|
+
const oldString = firstStringArg(record, ["old_string", "oldString", "oldtext", "old_text"]);
|
|
429
|
+
const newString = firstStringArg(record, ["new_string", "newString", "newtext", "new_text"]);
|
|
430
|
+
if (oldString === undefined || newString === undefined) {
|
|
431
|
+
return { error: `${toolName} requires old_string and new_string; the call was dropped.` };
|
|
432
|
+
}
|
|
433
|
+
const hunk = replacementHunk(oldString, newString);
|
|
434
|
+
if ("error" in hunk) return { error: hunk.error };
|
|
435
|
+
return { patch: hunk.hunk as string };
|
|
436
|
+
};
|
|
437
|
+
if (toolName === CURSOR_MULTI_EDIT_TOOL) {
|
|
438
|
+
const edits = args.edits;
|
|
439
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
440
|
+
return { error: "multi_edit requires a non-empty edits array; the call was dropped." };
|
|
441
|
+
}
|
|
442
|
+
for (const edit of edits) {
|
|
443
|
+
if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
|
|
444
|
+
return { error: "multi_edit edits entries must be objects with old_string and new_string; the call was dropped." };
|
|
445
|
+
}
|
|
446
|
+
const editResult = addReplacement(edit as Record<string, unknown>);
|
|
447
|
+
if (editResult.error !== undefined) return editResult;
|
|
448
|
+
hunks.push(editResult.patch);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
const editResult = addReplacement(args);
|
|
452
|
+
if (editResult.error !== undefined) return editResult;
|
|
453
|
+
hunks.push(editResult.patch);
|
|
454
|
+
}
|
|
455
|
+
return { patch: [PATCH_BEGIN, `*** Update File: ${path}`, ...hunks, PATCH_END].join("\n") };
|
|
456
|
+
}
|
|
457
|
+
|
|
314
458
|
export function mapSyntheticMcpExecToToolEvents(
|
|
315
459
|
args: McpArgs,
|
|
316
460
|
fallbackCallId = "cursor_mcp_exec",
|
|
@@ -341,9 +485,20 @@ export function mapSyntheticMcpExecToToolEvents(
|
|
|
341
485
|
}
|
|
342
486
|
}
|
|
343
487
|
// Stateless fallback (no shared event state): emit a complete, self-contained tool call.
|
|
488
|
+
//
|
|
489
|
+
// No conversion happens here by design (#1036 review). Structured-edit translation is gated on
|
|
490
|
+
// provenance — did WE advertise this bare name on THIS request — and that record lives on the
|
|
491
|
+
// request state, which this branch does not have. Converting anyway would reinstate the exact
|
|
492
|
+
// hazard the gate exists to close: a client or MCP tool legitimately named `edit_file` would be
|
|
493
|
+
// rewritten into an apply_patch it never asked for. The live path always carries state
|
|
494
|
+
// (live-transport seeds it), so this only affects direct/unit callers.
|
|
495
|
+
const emittedName = responsesName;
|
|
496
|
+
const emittedArgs = normalizedArgs;
|
|
344
497
|
return [
|
|
345
|
-
{ type: "tool_call_start", id: callId, name:
|
|
346
|
-
...(
|
|
498
|
+
{ type: "tool_call_start", id: callId, name: emittedName },
|
|
499
|
+
...(emittedArgs.length > 2
|
|
500
|
+
? [{ type: "tool_call_delta" as const, arguments: emittedArgs }]
|
|
501
|
+
: []),
|
|
347
502
|
{ type: "tool_call_end", id: callId },
|
|
348
503
|
];
|
|
349
504
|
}
|
|
@@ -385,6 +540,13 @@ function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, to
|
|
|
385
540
|
return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
|
|
386
541
|
}
|
|
387
542
|
|
|
543
|
+
function dropStructuredEditCall(state: CursorProtobufEventState, callId: string, toolName: string, reason: string): CursorServerMessage[] {
|
|
544
|
+
state.openToolCalls.delete(callId);
|
|
545
|
+
state.translatorBudget?.closeCall(callId);
|
|
546
|
+
state.completedToolCalls.add(callId);
|
|
547
|
+
return [{ type: "error", message: `${toolName} call was not converted to apply_patch: ${reason}` }];
|
|
548
|
+
}
|
|
549
|
+
|
|
388
550
|
function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
|
|
389
551
|
const open = state.openToolCalls.get(callId);
|
|
390
552
|
if (!open) return [];
|
|
@@ -392,6 +554,14 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
|
|
|
392
554
|
if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
|
|
393
555
|
if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
|
|
394
556
|
}
|
|
557
|
+
// Structured edit calls are converted to apply_patch here so both the interactionUpdate and the
|
|
558
|
+
// native-exec mcpArgs paths emit the same valid freeform payload (#1017).
|
|
559
|
+
const translation = structuredEditCallIsOurs(state.syntheticStructuredEditToolNames, open.name)
|
|
560
|
+
? translateStructuredEditCall(open.name, finalArgs)
|
|
561
|
+
: undefined;
|
|
562
|
+
if (translation?.error !== undefined) {
|
|
563
|
+
return dropStructuredEditCall(state, callId, open.name, translation.error);
|
|
564
|
+
}
|
|
395
565
|
if (finalArgs !== open.args) {
|
|
396
566
|
const previousBytes = Buffer.byteLength(open.args);
|
|
397
567
|
const reservation = state.translatorBudget?.reserveTransient(
|
|
@@ -402,8 +572,10 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
|
|
|
402
572
|
reservation?.commitRetained();
|
|
403
573
|
state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
|
|
404
574
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
575
|
+
const emittedName = translation ? CODEX_APPLY_PATCH_TOOL : open.name;
|
|
576
|
+
const emittedArgs = translation ? JSON.stringify({ input: translation.patch }) : finalArgs;
|
|
577
|
+
const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: emittedName }];
|
|
578
|
+
if (emittedArgs.length > 0) out.push({ type: "tool_call_delta", arguments: emittedArgs });
|
|
407
579
|
out.push(...endToolCall(state, callId));
|
|
408
580
|
return out;
|
|
409
581
|
}
|
|
@@ -10,14 +10,16 @@ import type {
|
|
|
10
10
|
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
|
|
11
11
|
import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types";
|
|
12
12
|
import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
|
|
13
|
-
import { cursorEffortSuffix,
|
|
13
|
+
import { cursorEffortSuffix, cursorRequestWireModelIdWithEffort } from "./effort-map";
|
|
14
14
|
import {
|
|
15
15
|
cursorMcpToolEncodedSize,
|
|
16
16
|
cursorMcpToolsEncodedSize,
|
|
17
17
|
cursorToolAllowedByChoice,
|
|
18
18
|
cursorToolChoiceAliases,
|
|
19
|
+
cursorStructuredEditTools,
|
|
19
20
|
cursorToolWireName,
|
|
20
21
|
cursorToolsForActivePrompt,
|
|
22
|
+
isCursorStructuredEditToolName,
|
|
21
23
|
isBareCodexShellBridgeTool,
|
|
22
24
|
} from "./tool-definitions";
|
|
23
25
|
import { lookupCursorThreadConversation } from "./thread-continuity";
|
|
@@ -41,6 +43,9 @@ function toolPriority(tool: OcxTool, selectedNames: ReadonlySet<string>): number
|
|
|
41
43
|
// selected filler cannot starve the Codex execution path during truncation (#399).
|
|
42
44
|
if (isBareCodexShellBridgeTool(tool)) return 0;
|
|
43
45
|
if (!tool.namespace && tool.name === "apply_patch") return 1;
|
|
46
|
+
// Structured edit tools convert to apply_patch on the return path, so they must survive the
|
|
47
|
+
// same byte/count truncation as the freeform tool they stand in for (#1017).
|
|
48
|
+
if (!tool.namespace && isCursorStructuredEditToolName(tool.name)) return 1;
|
|
44
49
|
if (cursorToolChoiceAliases(tool).some(name => selectedNames.has(name))) return 2;
|
|
45
50
|
if (tool.loadedFromToolSearch) return 3;
|
|
46
51
|
if (!tool.namespace) return 4;
|
|
@@ -61,7 +66,11 @@ export function applyCursorToolBudget(
|
|
|
61
66
|
toolChoice: OcxToolChoice | undefined,
|
|
62
67
|
): CursorToolBudgetResult {
|
|
63
68
|
const catalog = tools ?? [];
|
|
64
|
-
const
|
|
69
|
+
const baseEligible = catalog.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog));
|
|
70
|
+
// Synthetic structured edit tools ride along with the freeform apply_patch tool (#1017). They are
|
|
71
|
+
// part of the advertised catalog, so their serialized size counts toward the byte ceiling here.
|
|
72
|
+
const synthetic = cursorStructuredEditTools(catalog, toolChoice);
|
|
73
|
+
const eligible = [...baseEligible, ...synthetic];
|
|
65
74
|
if (
|
|
66
75
|
eligible.length <= CURSOR_TOOL_COUNT_LIMIT
|
|
67
76
|
&& cursorMcpToolsEncodedSize(eligible, toolChoice) <= CURSOR_TOOL_BYTES_LIMIT
|
|
@@ -101,7 +110,9 @@ export function applyCursorToolBudget(
|
|
|
101
110
|
|
|
102
111
|
return {
|
|
103
112
|
tools: eligible.filter(tool => keptSet.has(tool)),
|
|
104
|
-
omitted
|
|
113
|
+
// Synthetic tools are pinned in phase 1 and never reported as omitted; the note counts only
|
|
114
|
+
// tools the client itself requested.
|
|
115
|
+
omitted: baseEligible.filter(tool => !keptSet.has(tool)),
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -140,7 +151,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
140
151
|
],
|
|
141
152
|
};
|
|
142
153
|
}
|
|
143
|
-
return { ...selection, modelId: suffix ?
|
|
154
|
+
return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
|
|
@@ -8,6 +8,9 @@ export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses";
|
|
|
8
8
|
export const CODEX_EXEC_COMMAND_TOOL = "exec_command";
|
|
9
9
|
export const CODEX_SHELL_COMMAND_TOOL = "shell_command";
|
|
10
10
|
export const CODEX_APPLY_PATCH_TOOL = "apply_patch";
|
|
11
|
+
export const CURSOR_EDIT_FILE_TOOL = "edit_file";
|
|
12
|
+
export const CURSOR_MULTI_EDIT_TOOL = "multi_edit";
|
|
13
|
+
export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL] as const;
|
|
11
14
|
export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
|
|
12
15
|
export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
|
|
13
16
|
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
@@ -41,6 +44,47 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
|
41
44
|
additionalProperties: false,
|
|
42
45
|
} as const;
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Structured single-replacement schema advertised to Cursor models in addition to the freeform
|
|
49
|
+
* `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native
|
|
50
|
+
* Edit shape) but cannot produce Codex's freeform patch grammar, so every file edit attempt on the
|
|
51
|
+
* Cursor route produced malformed `apply_patch` payloads that the Codex client rejected locally
|
|
52
|
+
* (#1017). Calls to this tool are converted server-side into a valid apply_patch payload.
|
|
53
|
+
*/
|
|
54
|
+
export const CURSOR_EDIT_FILE_INPUT_SCHEMA = {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
|
|
58
|
+
old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
|
|
59
|
+
new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
|
|
60
|
+
},
|
|
61
|
+
required: ["file_path", "old_string", "new_string"],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
/** Structured multi-replacement schema; mirrors Cursor's native MultiEdit shape. */
|
|
66
|
+
export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
|
|
70
|
+
edits: {
|
|
71
|
+
type: "array",
|
|
72
|
+
items: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
|
|
76
|
+
new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
|
|
77
|
+
},
|
|
78
|
+
required: ["old_string", "new_string"],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
description: "Ordered replacement edits for this file. Each old_string must match the current file content.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ["file_path", "edits"],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
|
|
46
90
|
* Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
|
|
@@ -140,6 +184,73 @@ export function cursorRequestAdvertisesApplyPatch(
|
|
|
140
184
|
return catalog.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && cursorToolAllowedByChoice(tool, toolChoice, catalog));
|
|
141
185
|
}
|
|
142
186
|
|
|
187
|
+
export function isCursorStructuredEditToolName(name: string): boolean {
|
|
188
|
+
return (CURSOR_STRUCTURED_EDIT_TOOLS as readonly string[]).includes(name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Internal provenance gate for synthetic edits after prompt filtering and catalog budgeting. */
|
|
192
|
+
export function isCursorSyntheticStructuredEditTool(
|
|
193
|
+
tool: Pick<OcxTool, "namespace" | "name" | "cursorStructuredEdit">,
|
|
194
|
+
): boolean {
|
|
195
|
+
return !tool.namespace && tool.cursorStructuredEdit === true && isCursorStructuredEditToolName(tool.name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Synthetic structured edit tools for the Cursor route (#1017).
|
|
200
|
+
*
|
|
201
|
+
* Codex exposes `apply_patch` as a freeform custom tool whose body must be the exact Codex patch
|
|
202
|
+
* grammar (`*** Begin Patch` envelope, `@@` hunks, `-`/`+` prefixes). Cursor-trained models are
|
|
203
|
+
* trained on exact-match edit tools instead and emit malformed patch text on every attempt, which
|
|
204
|
+
* the Codex client then rejects locally ("invalid hunk"). When the request advertises the freeform
|
|
205
|
+
* `apply_patch` tool, also advertise Cursor-native-shaped `edit_file` / `multi_edit` tools; the
|
|
206
|
+
* adapter converts their exact-match replacements into a valid apply_patch payload (see
|
|
207
|
+
* protobuf-events.translateStructuredEditCall).
|
|
208
|
+
*
|
|
209
|
+
* Never widened when the caller pinned an explicit tool choice: a forced `apply_patch` selection
|
|
210
|
+
* must not gain sibling tools the client did not ask for.
|
|
211
|
+
*/
|
|
212
|
+
export function cursorStructuredEditTools(
|
|
213
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
214
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
215
|
+
): OcxTool[] {
|
|
216
|
+
if (!cursorRequestAdvertisesApplyPatch(tools, toolChoice)) return [];
|
|
217
|
+
if (toolChoice && toolChoice !== "auto" && toolChoice !== "required") return [];
|
|
218
|
+
// Never shadow an already-advertised bare tool with the same name (a client catalog could
|
|
219
|
+
// legitimately expose its own `edit_file` / `multi_edit` MCP-style tools).
|
|
220
|
+
const existingBareNames = new Set(
|
|
221
|
+
(tools ?? []).filter(tool => !tool.namespace).map(tool => tool.name),
|
|
222
|
+
);
|
|
223
|
+
const candidates: OcxTool[] = [
|
|
224
|
+
{
|
|
225
|
+
name: CURSOR_EDIT_FILE_TOOL,
|
|
226
|
+
cursorStructuredEdit: true,
|
|
227
|
+
description:
|
|
228
|
+
"Replace one block of exact text in a file. OpenCodex converts the replacement into a Codex apply_patch change, which the Codex client applies with its normal approval and sandbox policy. old_string must match the current file content exactly at exactly one location (apply_patch rejects ambiguous hunks). Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
|
|
229
|
+
parameters: { ...CURSOR_EDIT_FILE_INPUT_SCHEMA },
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: CURSOR_MULTI_EDIT_TOOL,
|
|
233
|
+
cursorStructuredEdit: true,
|
|
234
|
+
description:
|
|
235
|
+
"Apply several exact-text replacements to one file. OpenCodex converts the edits into a single Codex apply_patch change, which the Codex client applies with its normal approval and sandbox policy. Each old_string must match the current file content exactly at exactly one location (apply_patch rejects ambiguous hunks). Edits are independent: every old_string is matched against the ORIGINAL file content, so a later edit must not rely on text introduced by an earlier one. Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
|
|
236
|
+
parameters: { ...CURSOR_MULTI_EDIT_INPUT_SCHEMA },
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
return candidates.filter(tool => !existingBareNames.has(tool.name));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* True when this request actually advertises the synthetic structured edit tools (`edit_file` /
|
|
244
|
+
* `multi_edit`) — i.e. a freeform `apply_patch` is advertised, no tool-choice pin blocks widening,
|
|
245
|
+
* and neither name is shadowed by an existing bare tool in the client catalog.
|
|
246
|
+
*/
|
|
247
|
+
export function cursorRequestAdvertisesStructuredEdits(
|
|
248
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
249
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
250
|
+
): boolean {
|
|
251
|
+
return cursorStructuredEditTools(tools, toolChoice).length > 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
143
254
|
export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
|
|
144
255
|
return namespacedToolName(tool.namespace, tool.name);
|
|
145
256
|
}
|
|
@@ -415,6 +526,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
415
526
|
const hasBareExec = shellBridgeNames.length > 0;
|
|
416
527
|
const shellBridgeLabel = quotedNames(shellBridgeNames.length > 0 ? shellBridgeNames : [...CODEX_SHELL_BRIDGE_TOOL_NAMES]);
|
|
417
528
|
const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
|
|
529
|
+
const structuredEditNames = tools
|
|
530
|
+
?.filter(tool => !tool.namespace && isCursorStructuredEditToolName(tool.name))
|
|
531
|
+
.map(tool => tool.name) ?? [];
|
|
418
532
|
const discoveryTools = discoveryToolLabel(wireNames);
|
|
419
533
|
const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
|
|
420
534
|
// Host-shell-neutral: the Codex client executes bridge commands, and may differ from
|
|
@@ -443,7 +557,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
443
557
|
? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.`
|
|
444
558
|
: undefined,
|
|
445
559
|
hasApplyPatch
|
|
446
|
-
?
|
|
560
|
+
? structuredEditNames.length > 0
|
|
561
|
+
? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take exact-match replacements that OpenCodex converts into Codex \`apply_patch\` changes for approval. Use \`apply_patch\` directly only when you can emit its exact freeform syntax (\`*** Begin Patch\` envelope with \`@@\` hunks and \`-\`/\`+\` line prefixes); never emit patch-like plain text as tool arguments.`
|
|
562
|
+
: "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
447
563
|
: undefined,
|
|
448
564
|
hasBareExec
|
|
449
565
|
? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
|
|
@@ -457,7 +573,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
457
573
|
: undefined,
|
|
458
574
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
459
575
|
hasBareExec
|
|
460
|
-
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
|
|
576
|
+
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
|
|
461
577
|
: undefined,
|
|
462
578
|
].filter((note): note is string => typeof note === "string");
|
|
463
579
|
return notes.join(" ");
|