@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -140,6 +140,10 @@ let blobOldestEvictableAt: number | null = null;
|
|
|
140
140
|
let rejectedEntryTooLarge = 0;
|
|
141
141
|
let rejectedPinnedSaturation = 0;
|
|
142
142
|
let blobExpiryAccountingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
143
|
+
/** Earliest unpinned storedAt+ttl; skip the write-time TTL walk while this is in the future. */
|
|
144
|
+
let blobNextUnpinnedExpiryAt: number | null = null;
|
|
145
|
+
/** Earliest unpinned remote expiry strictly in the future; drives the single reclassify timer. */
|
|
146
|
+
let blobNextRemoteExpiryAt: number | null = null;
|
|
143
147
|
|
|
144
148
|
function isExpired(entry: CursorBlobEntry, now: number): boolean {
|
|
145
149
|
return now - entry.storedAt >= blobLimits.ttlMs;
|
|
@@ -157,6 +161,8 @@ function recomputeBlobClassAccounting(): void {
|
|
|
157
161
|
let pinnedBytes = 0;
|
|
158
162
|
let evictableBytes = 0;
|
|
159
163
|
let oldestAt: number | null = null;
|
|
164
|
+
let nextUnpinnedExpiry = Number.POSITIVE_INFINITY;
|
|
165
|
+
let nextRemoteExpiry = Number.POSITIVE_INFINITY;
|
|
160
166
|
for (const [k, entry] of blobs) {
|
|
161
167
|
const requestPinned = entry.requestPins.size > 0;
|
|
162
168
|
const provenancePinned = entry.provenance === "remote-setBlobArgs" && !isExpired(entry, now);
|
|
@@ -169,11 +175,20 @@ function recomputeBlobClassAccounting(): void {
|
|
|
169
175
|
evictableBytes += entry.sizeBytes + k.length;
|
|
170
176
|
oldestAt = oldestAt === null ? entry.storedAt : Math.min(oldestAt, entry.storedAt);
|
|
171
177
|
}
|
|
178
|
+
if (!requestPinned) {
|
|
179
|
+
const expiresAt = entry.storedAt + blobLimits.ttlMs;
|
|
180
|
+
nextUnpinnedExpiry = Math.min(nextUnpinnedExpiry, expiresAt);
|
|
181
|
+
if (entry.provenance === "remote-setBlobArgs" && expiresAt > now) {
|
|
182
|
+
nextRemoteExpiry = Math.min(nextRemoteExpiry, expiresAt);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
186
|
blobLocalBytes = localBytes;
|
|
174
187
|
blobPinnedBytes = pinnedBytes;
|
|
175
188
|
blobEvictableBytes = evictableBytes;
|
|
176
189
|
blobOldestEvictableAt = oldestAt;
|
|
190
|
+
blobNextUnpinnedExpiryAt = Number.isFinite(nextUnpinnedExpiry) ? nextUnpinnedExpiry : null;
|
|
191
|
+
blobNextRemoteExpiryAt = Number.isFinite(nextRemoteExpiry) ? nextRemoteExpiry : null;
|
|
177
192
|
scheduleBlobExpiryAccounting(now);
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -185,13 +200,8 @@ function reconcileBlobClassAccountingAndEnforce(): void {
|
|
|
185
200
|
function scheduleBlobExpiryAccounting(now: number): void {
|
|
186
201
|
if (blobExpiryAccountingTimer) clearTimeout(blobExpiryAccountingTimer);
|
|
187
202
|
blobExpiryAccountingTimer = undefined;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (entry.provenance !== "remote-setBlobArgs" || entry.requestPins.size > 0) continue;
|
|
191
|
-
const expiresAt = entry.storedAt + blobLimits.ttlMs;
|
|
192
|
-
if (expiresAt > now) nextExpiry = Math.min(nextExpiry, expiresAt);
|
|
193
|
-
}
|
|
194
|
-
if (!Number.isFinite(nextExpiry)) return;
|
|
203
|
+
const nextExpiry = blobNextRemoteExpiryAt;
|
|
204
|
+
if (nextExpiry === null || nextExpiry <= now || !Number.isFinite(nextExpiry)) return;
|
|
195
205
|
blobExpiryAccountingTimer = setTimeout(() => {
|
|
196
206
|
blobExpiryAccountingTimer = undefined;
|
|
197
207
|
reconcileBlobClassAccountingAndEnforce();
|
|
@@ -199,6 +209,41 @@ function scheduleBlobExpiryAccounting(now: number): void {
|
|
|
199
209
|
blobExpiryAccountingTimer.unref?.();
|
|
200
210
|
}
|
|
201
211
|
|
|
212
|
+
/**
|
|
213
|
+
* O(1) class/timer update for a newly admitted key when no other row changed.
|
|
214
|
+
* Full-map recompute stays on replacement, eviction, pin changes, and TTL fire —
|
|
215
|
+
* the 4096-entry ceiling fill must not walk the store on every remote admit.
|
|
216
|
+
*/
|
|
217
|
+
function accountAdmittedBlob(k: string, entry: CursorBlobEntry, now: number): void {
|
|
218
|
+
const requestPinned = entry.requestPins.size > 0;
|
|
219
|
+
const expired = isExpired(entry, now);
|
|
220
|
+
const provenancePinned = entry.provenance === "remote-setBlobArgs" && !expired;
|
|
221
|
+
const logicalBytes = entry.sizeBytes + k.length;
|
|
222
|
+
if (entry.provenance === "local-regenerated") blobLocalBytes += entry.sizeBytes;
|
|
223
|
+
if (requestPinned || provenancePinned) blobPinnedBytes += logicalBytes;
|
|
224
|
+
if (!requestPinned && (entry.provenance === "local-regenerated" || expired)) {
|
|
225
|
+
blobEvictableBytes += logicalBytes;
|
|
226
|
+
blobOldestEvictableAt = blobOldestEvictableAt === null ? entry.storedAt : Math.min(blobOldestEvictableAt, entry.storedAt);
|
|
227
|
+
}
|
|
228
|
+
if (requestPinned) return;
|
|
229
|
+
const expiresAt = entry.storedAt + blobLimits.ttlMs;
|
|
230
|
+
blobNextUnpinnedExpiryAt = blobNextUnpinnedExpiryAt === null
|
|
231
|
+
? expiresAt
|
|
232
|
+
: Math.min(blobNextUnpinnedExpiryAt, expiresAt);
|
|
233
|
+
if (entry.provenance !== "remote-setBlobArgs" || expiresAt <= now) return;
|
|
234
|
+
const previousRemoteExpiry = blobNextRemoteExpiryAt;
|
|
235
|
+
blobNextRemoteExpiryAt = previousRemoteExpiry === null
|
|
236
|
+
? expiresAt
|
|
237
|
+
: Math.min(previousRemoteExpiry, expiresAt);
|
|
238
|
+
if (
|
|
239
|
+
!blobExpiryAccountingTimer
|
|
240
|
+
|| previousRemoteExpiry === null
|
|
241
|
+
|| expiresAt < previousRemoteExpiry
|
|
242
|
+
) {
|
|
243
|
+
scheduleBlobExpiryAccounting(now);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
202
247
|
function deleteBlob(k: string, recompute = true): number {
|
|
203
248
|
const entry = blobs.get(k);
|
|
204
249
|
if (!entry) return 0;
|
|
@@ -246,9 +291,11 @@ function setBlob(
|
|
|
246
291
|
}
|
|
247
292
|
|
|
248
293
|
const removals = new Set<string>();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
294
|
+
if (blobNextUnpinnedExpiryAt !== null && now >= blobNextUnpinnedExpiryAt) {
|
|
295
|
+
for (const [candidateKey, entry] of blobs) {
|
|
296
|
+
if (candidateKey === k && sameData) continue;
|
|
297
|
+
if (entry.requestPins.size === 0 && isExpired(entry, now)) removals.add(candidateKey);
|
|
298
|
+
}
|
|
252
299
|
}
|
|
253
300
|
|
|
254
301
|
const existingRemovedByTtl = existing !== undefined && removals.has(k);
|
|
@@ -326,7 +373,12 @@ function setBlob(
|
|
|
326
373
|
blobBytes += entry.sizeBytes;
|
|
327
374
|
blobKeyBytes += k.length;
|
|
328
375
|
for (const scope of entry.requestPins) blobRequestScopes.get(scope)?.keys.add(k);
|
|
329
|
-
|
|
376
|
+
if (removals.size > 0 || existing !== undefined) {
|
|
377
|
+
reconcileBlobClassAccountingAndEnforce();
|
|
378
|
+
} else {
|
|
379
|
+
accountAdmittedBlob(k, entry, now);
|
|
380
|
+
enforceAppOwnedMemoryBudget();
|
|
381
|
+
}
|
|
330
382
|
return { admitted: true, replaced: existing !== undefined };
|
|
331
383
|
}
|
|
332
384
|
|
|
@@ -5,7 +5,7 @@ import { McpToolDefinitionSchema, McpToolsSchema, type McpToolDefinition } from
|
|
|
5
5
|
import { CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, cursorRequestAdvertisesApplyPatch, cursorToolAllowedByChoice, cursorToolWireName, OCX_RESPONSES_TOOL_PROVIDER } from "./tool-naming";
|
|
6
6
|
import { CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, cursorToolInputSchema } from "./tool-schemas";
|
|
7
7
|
export { OCX_RESPONSES_TOOL_PROVIDER, CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL, CODEX_UNIFIED_EXEC_TOOL, CODEX_WAIT_TOOL, CODEX_APPLY_PATCH_TOOL, CODEX_TOOL_SEARCH_TOOL, CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, CURSOR_STRUCTURED_EDIT_TOOLS, CURSOR_EXEC_COMMAND_TOOL, CODEX_SHELL_BRIDGE_TOOL_NAMES, isCodexShellBridgeToolName, resolveShellBridgeAliasKey, cursorToolChoiceAliases, isBareCodexShellBridgeTool, isCursorExecutionPathTool, isCursorWaitTool, isCursorCodeModeExecTool, cursorRequestUsesCodeMode, cursorRequestHasShellAlias, cursorRequestAdvertisesApplyPatch, isCursorStructuredEditToolName, isCursorSyntheticStructuredEditTool, cursorToolWireName, normalizeCursorWireName, normalizeCursorTextToolMarkers, responsesToolNameFromCursorWire, cursorToolAllowedByChoice } from "./tool-naming";
|
|
8
|
-
export { CURSOR_EXEC_COMMAND_INPUT_SCHEMA, CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, cursorToolInputSchema, cursorToolArgNormalizeSchema, shellBridgeRequiredCommandKeys, defaultShellBridgeArgNormalizeSchema, cursorShellBridgeDropError, nonEmptyShellBridgeCommandFromArgs, cursorShellBridgeArgsValid } from "./tool-schemas";
|
|
8
|
+
export { CURSOR_EXEC_COMMAND_INPUT_SCHEMA, CURSOR_FREEFORM_INPUT_SCHEMA, CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, cursorToolInputSchema, cursorToolArgNormalizeSchema, shellBridgeRequiredCommandKeys, defaultShellBridgeArgNormalizeSchema, cursorShellBridgeDropError, nonEmptyShellBridgeCommandFromArgs, cursorShellBridgeArgsValid } from "./tool-schemas";
|
|
9
9
|
export { CURSOR_SHELL_ALIAS_SYSTEM_NOTE, CURSOR_GENERIC_TOOL_USE_USER_HINT, isGenericToolUseCountDemoPrompt, requestedCursorToolUseCount, shouldAppendCursorGenericToolUseHint, appendCursorGenericToolUseHint, shouldUseNativeExecOnlyForGenericToolUse, cursorToolsForActivePrompt, buildCursorToolGuidanceSystemNote } from "./tool-guidance";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -10,11 +10,54 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
|
10
10
|
tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
|
|
11
11
|
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
12
12
|
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
13
|
+
sandbox_permissions: {
|
|
14
|
+
type: "string",
|
|
15
|
+
enum: ["use_default", "require_escalated"],
|
|
16
|
+
description: "Per-command sandbox override. Defaults to use_default; use require_escalated for unsandboxed execution.",
|
|
17
|
+
},
|
|
18
|
+
justification: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "User-facing approval question for require_escalated; omit otherwise.",
|
|
21
|
+
},
|
|
22
|
+
prefix_rule: {
|
|
23
|
+
type: "array",
|
|
24
|
+
items: { type: "string" },
|
|
25
|
+
description: "Reusable approval prefix for cmd, only with sandbox_permissions: require_escalated.",
|
|
26
|
+
},
|
|
27
|
+
login: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
description: "True runs the shell with login semantics; false disables them. Defaults to true.",
|
|
30
|
+
},
|
|
13
31
|
},
|
|
14
32
|
required: ["cmd"],
|
|
15
33
|
additionalProperties: false,
|
|
16
34
|
} as const;
|
|
17
35
|
|
|
36
|
+
/** Cursor represents a Responses freeform tool body as one string-valued input field. */
|
|
37
|
+
export const CURSOR_FREEFORM_INPUT_SCHEMA = {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: { input: { type: "string" } },
|
|
40
|
+
required: ["input"],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
function cursorFreeformInputSchema(tool: OcxTool): unknown {
|
|
45
|
+
const properties = tool.parameters?.properties;
|
|
46
|
+
const input = properties && typeof properties === "object" && !Array.isArray(properties)
|
|
47
|
+
? (properties as Record<string, unknown>).input
|
|
48
|
+
: undefined;
|
|
49
|
+
const description = input && typeof input === "object" && !Array.isArray(input)
|
|
50
|
+
? (input as Record<string, unknown>).description
|
|
51
|
+
: undefined;
|
|
52
|
+
if (typeof description !== "string") return CURSOR_FREEFORM_INPUT_SCHEMA;
|
|
53
|
+
return {
|
|
54
|
+
...CURSOR_FREEFORM_INPUT_SCHEMA,
|
|
55
|
+
properties: {
|
|
56
|
+
input: { ...CURSOR_FREEFORM_INPUT_SCHEMA.properties.input, description },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
/**
|
|
19
62
|
* Structured single-replacement schema advertised to Cursor models in addition to the freeform
|
|
20
63
|
* `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native
|
|
@@ -71,6 +114,10 @@ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
|
|
|
71
114
|
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
72
115
|
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
73
116
|
max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
|
|
117
|
+
sandbox_permissions: { type: "string", enum: ["use_default", "require_escalated"] },
|
|
118
|
+
justification: { type: "string" },
|
|
119
|
+
prefix_rule: { type: "array", items: { type: "string" } },
|
|
120
|
+
login: { type: "boolean" },
|
|
74
121
|
},
|
|
75
122
|
required: ["command"],
|
|
76
123
|
} as const;
|
|
@@ -78,6 +125,12 @@ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
|
|
|
78
125
|
|
|
79
126
|
/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
|
|
80
127
|
export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
128
|
+
if (tool.freeform) {
|
|
129
|
+
if (isBareCodexShellBridgeTool(tool)) {
|
|
130
|
+
throw new Error(`freeform Cursor tools cannot use reserved shell bridge name ${tool.name}; use a namespace`);
|
|
131
|
+
}
|
|
132
|
+
return cursorFreeformInputSchema(tool);
|
|
133
|
+
}
|
|
81
134
|
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
|
|
82
135
|
}
|
|
83
136
|
|
|
@@ -87,6 +140,12 @@ export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
|
87
140
|
* treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
|
|
88
141
|
*/
|
|
89
142
|
export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
|
|
143
|
+
if (tool.freeform) {
|
|
144
|
+
if (isBareCodexShellBridgeTool(tool)) {
|
|
145
|
+
throw new Error(`freeform Cursor tools cannot use reserved shell bridge name ${tool.name}; use a namespace`);
|
|
146
|
+
}
|
|
147
|
+
return cursorFreeformInputSchema(tool);
|
|
148
|
+
}
|
|
90
149
|
if (isBareCodexShellBridgeTool(tool)) {
|
|
91
150
|
return shellBridgeArgNormalizeSchema(tool);
|
|
92
151
|
}
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { extractKiroImages, normalizeKiroImages, type KiroImage } from "./kiro-i
|
|
|
44
44
|
import { sniffImageDimensions } from "./anthropic-image-guard";
|
|
45
45
|
import { fetchKiroWithRetry, noteKiroTransientThrottle } from "./kiro-retry";
|
|
46
46
|
import { convertKiroToolContext } from "./kiro-tools";
|
|
47
|
-
import { normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize";
|
|
47
|
+
import { EMPTY_EXEC_OUTPUT_MESSAGE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize";
|
|
48
48
|
import { identifyRoutedModel } from "./identity";
|
|
49
49
|
import { buildNonOpenAIToolCatalogNudgeFromNames, isBareShellBridgeTool, isCodexCodeModeExecTool } from "./tool-catalog-nudge";
|
|
50
50
|
import {
|
|
@@ -663,7 +663,7 @@ export function buildKiroPayload(
|
|
|
663
663
|
}
|
|
664
664
|
const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
|
|
665
665
|
const turns: KiroTurn[] = [];
|
|
666
|
-
const priorCalls = new Map<string, { wireName: string }>();
|
|
666
|
+
const priorCalls = new Map<string, { wireName: string; rawId: string }>();
|
|
667
667
|
const pushUser = (content: string, images: KiroImage[] = [], toolResults: KiroToolResult[] = []): void => {
|
|
668
668
|
const last = turns.at(-1);
|
|
669
669
|
if (last?.kind === "user") {
|
|
@@ -695,7 +695,27 @@ export function buildKiroPayload(
|
|
|
695
695
|
}
|
|
696
696
|
};
|
|
697
697
|
|
|
698
|
+
let adjacentResult: {
|
|
699
|
+
rawId: string;
|
|
700
|
+
result: KiroToolResult;
|
|
701
|
+
texts: string[];
|
|
702
|
+
count: number;
|
|
703
|
+
hasImages: boolean;
|
|
704
|
+
} | undefined;
|
|
705
|
+
const finishAdjacentResult = (): void => {
|
|
706
|
+
if (adjacentResult && adjacentResult.count > 1) {
|
|
707
|
+
if (adjacentResult.texts.some(text => text.trim())) {
|
|
708
|
+
adjacentResult.result.content = adjacentResult.texts.map(text => ({ text }));
|
|
709
|
+
} else if (adjacentResult.hasImages || adjacentResult.result.status === "error") {
|
|
710
|
+
adjacentResult.result.content = [{ text: KIRO_EMPTY_TOOL_RESULT_MESSAGE }];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
adjacentResult = undefined;
|
|
714
|
+
};
|
|
715
|
+
|
|
698
716
|
for (const msg of kiroPayloadMessages(parsed)) {
|
|
717
|
+
// Original-message adjacency matters even when a turn is collapsed or skipped below.
|
|
718
|
+
if (msg.role !== "toolResult") finishAdjacentResult();
|
|
699
719
|
if (msg.role === "user" || msg.role === "developer") {
|
|
700
720
|
const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
|
|
701
721
|
const images = extractKiroImages((msg as { content: string | OcxContentPart[] }).content);
|
|
@@ -714,7 +734,7 @@ export function buildKiroPayload(
|
|
|
714
734
|
if (priorCalls.has(toolUseId)) throw new Error(`Kiro history contains duplicate tool call id ${JSON.stringify(tc.id)}`);
|
|
715
735
|
const wireName = namespacedToolName(tc.namespace, tc.name);
|
|
716
736
|
const name = registry.alias(wireName);
|
|
717
|
-
priorCalls.set(toolUseId, { wireName });
|
|
737
|
+
priorCalls.set(toolUseId, { wireName, rawId: tc.id });
|
|
718
738
|
return { name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
|
|
719
739
|
});
|
|
720
740
|
if (!text && toolUses.length === 0) {
|
|
@@ -735,26 +755,52 @@ export function buildKiroPayload(
|
|
|
735
755
|
// the task instead of calling text()/notify(). Checked before `text.trim()` because the
|
|
736
756
|
// wrapper form ("Script completed\nWall time ...\nOutput:\n") is non-blank and would
|
|
737
757
|
// otherwise pass through as if it were real output.
|
|
738
|
-
const
|
|
758
|
+
const normalizedExecText = normalizeEmptyExecToolResultText(text, {
|
|
739
759
|
toolName: tr.toolName,
|
|
740
760
|
toolNamespace: tr.toolNamespace,
|
|
741
|
-
})
|
|
761
|
+
});
|
|
762
|
+
const resultText = normalizedExecText ?? (text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE);
|
|
742
763
|
const images = extractKiroImages(tr.content);
|
|
743
764
|
const toolUseId = normalizeToolId(tr.toolCallId);
|
|
744
|
-
|
|
765
|
+
const call = priorCalls.get(toolUseId);
|
|
766
|
+
if (!call || call.rawId !== tr.toolCallId) {
|
|
745
767
|
throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
|
|
746
768
|
}
|
|
769
|
+
// Keep real whitespace and failed wrappers, but no empty-success wrapper boilerplate.
|
|
770
|
+
const rawGroupText = text.length > 0 && (!text.trim() || normalizedExecText !== EMPTY_EXEC_OUTPUT_MESSAGE)
|
|
771
|
+
? text : undefined;
|
|
772
|
+
const last = turns.at(-1);
|
|
773
|
+
if (
|
|
774
|
+
adjacentResult?.rawId === tr.toolCallId
|
|
775
|
+
&& last?.kind === "user"
|
|
776
|
+
&& last.toolResults.at(-1) === adjacentResult.result
|
|
777
|
+
) {
|
|
778
|
+
adjacentResult.count += 1;
|
|
779
|
+
adjacentResult.hasImages ||= images.length > 0;
|
|
780
|
+
if (rawGroupText !== undefined) adjacentResult.texts.push(rawGroupText);
|
|
781
|
+
last.images.push(...images);
|
|
782
|
+
if (tr.isError) adjacentResult.result.status = "error";
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
finishAdjacentResult();
|
|
747
786
|
// Carrier text is a placeholder for an OTHERWISE EMPTY tool-result turn, not a prefix.
|
|
748
787
|
// Passing it here would push proxy filler AHEAD of a human instruction that Claude Code
|
|
749
788
|
// sends in the same turn (mid-turn steering / queued_command, issue #543), burying the
|
|
750
789
|
// newest user intent behind boilerplate. Backfill below only when nothing else speaks.
|
|
751
|
-
|
|
790
|
+
const result: KiroToolResult = {
|
|
752
791
|
content: [{ text: resultText }],
|
|
753
792
|
status: tr.isError ? "error" : "success",
|
|
754
793
|
toolUseId,
|
|
755
|
-
}
|
|
794
|
+
};
|
|
795
|
+
pushUser("", images, [result]);
|
|
796
|
+
adjacentResult = {
|
|
797
|
+
rawId: tr.toolCallId, result,
|
|
798
|
+
texts: rawGroupText === undefined ? [] : [rawGroupText],
|
|
799
|
+
count: 1, hasImages: images.length > 0,
|
|
800
|
+
};
|
|
756
801
|
}
|
|
757
802
|
}
|
|
803
|
+
finishAdjacentResult();
|
|
758
804
|
|
|
759
805
|
if (turns.length === 0 || turns[0].kind === "assistant") {
|
|
760
806
|
turns.unshift({ kind: "user", content: KIRO_CONTINUATION_MESSAGE, images: [], toolResults: [] });
|
|
@@ -1660,6 +1660,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1660
1660
|
let bufferBytes = 0;
|
|
1661
1661
|
interface PendingToolCall {
|
|
1662
1662
|
key: string;
|
|
1663
|
+
indexKey?: string;
|
|
1663
1664
|
id: string;
|
|
1664
1665
|
name: string;
|
|
1665
1666
|
args: string;
|
|
@@ -1848,17 +1849,29 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1848
1849
|
const rawId = rawToolCall.id;
|
|
1849
1850
|
const idDelta = typeof rawId === "string" ? rawId : "";
|
|
1850
1851
|
const rawIndex = rawToolCall.index;
|
|
1852
|
+
// Only missing/null indexes are absent; every claimed index must be valid.
|
|
1853
|
+
// Unsafe integers can collapse distinct wire indexes onto the same JS number.
|
|
1854
|
+
// Reject before an alias can bind or any pending call can consume the fragment.
|
|
1855
|
+
if (rawIndex !== undefined && rawIndex !== null
|
|
1856
|
+
&& (typeof rawIndex !== "number"
|
|
1857
|
+
|| !Number.isSafeInteger(rawIndex)
|
|
1858
|
+
|| rawIndex < 0)) {
|
|
1859
|
+
return yield* terminateWithError({
|
|
1860
|
+
...invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage),
|
|
1861
|
+
message: "upstream response contained invalid tool calls (invalid index)",
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1851
1864
|
|
|
1852
|
-
// Resolve the pending call BEFORE judging
|
|
1865
|
+
// Resolve the pending call BEFORE judging repeated string fields. Some OpenAI-compatible
|
|
1853
1866
|
// streamers repeat an already-sent field as a non-string placeholder on a
|
|
1854
1867
|
// continuation delta; judging first meant the whole stream died with a 502 even
|
|
1855
1868
|
// though the value being repeated was already held in canonical form.
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
: pendingToolCalls[pendingToolCalls.length - 1]?.key;
|
|
1869
|
+
const indexKey = typeof rawIndex === "number" ? `i:${rawIndex}` : undefined;
|
|
1870
|
+
const key = indexKey ?? (idDelta
|
|
1871
|
+
? `id:${idDelta}`
|
|
1872
|
+
: pendingToolCalls[pendingToolCalls.length - 1]?.key);
|
|
1861
1873
|
let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
|
|
1874
|
+
if (!call && indexKey !== undefined) call = pendingToolCalls.find(c => c.indexKey === indexKey);
|
|
1862
1875
|
if (!call && idDelta) call = pendingToolCalls.find(c => c.id === idDelta);
|
|
1863
1876
|
if (!call) {
|
|
1864
1877
|
call = {
|
|
@@ -1872,6 +1885,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1872
1885
|
pendingToolCalls.push(call);
|
|
1873
1886
|
budget.openCall(call.key);
|
|
1874
1887
|
}
|
|
1888
|
+
// An ID-only call may learn its index from a later ID+index fragment. Retain that
|
|
1889
|
+
// alias without changing the key that owns its argument budget. Only the first
|
|
1890
|
+
// observed index binds: a repeated ID on a different index must not alias both.
|
|
1891
|
+
if (indexKey !== undefined && call.indexKey === undefined) call.indexKey = indexKey;
|
|
1875
1892
|
|
|
1876
1893
|
// Tolerance is per FIELD, keyed on that field's own provenance. A canonical name
|
|
1877
1894
|
// says nothing about whether `arguments` was ever sent as a string, so it cannot
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOpenCodeGo, normalizeOpenCodeGoAgentMessages } from "./opencode-go";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
3
4
|
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
|
|
@@ -2355,6 +2356,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2355
2356
|
parsed._rawBody,
|
|
2356
2357
|
forward || parsed._previousResponseInputExpanded === true,
|
|
2357
2358
|
);
|
|
2359
|
+
if (!forward && isOpenCodeGo(provider.baseUrl)) outBody = normalizeOpenCodeGoAgentMessages(outBody);
|
|
2358
2360
|
outBody = mapRoutedResponsesReasoningEffort(outBody, provider, parsed.modelId);
|
|
2359
2361
|
// stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
|
|
2360
2362
|
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
|
|
@@ -2427,7 +2429,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2427
2429
|
// Codex 0.147 emits private namespace tool groups, while public/third-party Responses
|
|
2428
2430
|
// gateways accept only flat tool variants. Run after custom/tool-search lowering so
|
|
2429
2431
|
// namespace children already carry their final public kind before they are promoted.
|
|
2430
|
-
const rewritten = rewriteRoutedNamespaceToolsForUpstream(outBody);
|
|
2432
|
+
const rewritten = rewriteRoutedNamespaceToolsForUpstream(outBody, convertedRoutedCustomToolNames);
|
|
2431
2433
|
outBody = rewritten.body;
|
|
2432
2434
|
convertedRoutedNamespaceToolAliases = rewritten.aliases;
|
|
2433
2435
|
// Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Match the Go destination, including user-renamed provider entries. */
|
|
2
|
+
export function isOpenCodeGo(baseUrl: string): boolean {
|
|
3
|
+
try {
|
|
4
|
+
const url = new URL(baseUrl);
|
|
5
|
+
return url.origin === "https://opencode.ai" && url.pathname.replace(/\/+$/, "") === "/zen/go/v1";
|
|
6
|
+
} catch { return false; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Public Responses rejects Codex's private agent_message variant, even with plaintext content. */
|
|
10
|
+
export function normalizeOpenCodeGoAgentMessages(body: unknown): unknown {
|
|
11
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
12
|
+
const record = body as Record<string, unknown>;
|
|
13
|
+
if (!Array.isArray(record.input)) return body;
|
|
14
|
+
let changed = false;
|
|
15
|
+
const input = record.input.map((item: unknown) => {
|
|
16
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
17
|
+
const message = item as Record<string, unknown>;
|
|
18
|
+
if (message.type !== "agent_message" || !Array.isArray(message.content) || message.content.length === 0) return item;
|
|
19
|
+
// Genuine ciphertext and unknown part types must retain their existing fail-closed path.
|
|
20
|
+
if (!message.content.every(part => part && typeof part === "object"
|
|
21
|
+
&& ["input_text", "input_image", "input_file"].includes(part.type))) return item;
|
|
22
|
+
const identities = Object.fromEntries(["author", "recipient"]
|
|
23
|
+
.filter(key => typeof message[key] === "string")
|
|
24
|
+
.map(key => [key, message[key]]));
|
|
25
|
+
changed = true;
|
|
26
|
+
return {
|
|
27
|
+
type: "message", role: "user",
|
|
28
|
+
content: [
|
|
29
|
+
...(Object.keys(identities).length ? [{ type: "input_text", text: `Agent message ${JSON.stringify(identities)}` }] : []),
|
|
30
|
+
...message.content,
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
return changed ? { ...record, input } : body;
|
|
35
|
+
}
|
|
@@ -20,6 +20,7 @@ import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, withOneM
|
|
|
20
20
|
import { claudeConfigDir } from "./gateway-cache";
|
|
21
21
|
import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config";
|
|
22
22
|
import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
|
|
23
|
+
import { AnthropicRequestError } from "./inbound-records";
|
|
23
24
|
import { knownModelIdsForProvider } from "../router";
|
|
24
25
|
import { decodeRoutedModelIdOrThrow } from "../providers/slug-codec";
|
|
25
26
|
|
|
@@ -97,11 +98,17 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
|
|
|
97
98
|
const blockedSkills = effectiveBlockedSkillNames(config.claudeCode);
|
|
98
99
|
const blockedSkillsFor = (model: string): readonly string[] => {
|
|
99
100
|
const unmarked = stripOneMillionMarker(model);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
try {
|
|
102
|
+
const nativePassthrough = config.claudeCode?.nativePassthrough !== false
|
|
103
|
+
&& !unmarked.includes("/")
|
|
104
|
+
&& /^(claude|anthropic)(?:-|$)/i.test(unmarked)
|
|
105
|
+
&& resolveInboundModel(unmarked, config.claudeCode) === unmarked;
|
|
106
|
+
return nativePassthrough ? [] : blockedSkills;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// A stale Desktop selector must not break the roster or acquire native exemptions.
|
|
109
|
+
if (error instanceof AnthropicRequestError) return blockedSkills;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
105
112
|
};
|
|
106
113
|
const defs: ClaudeAgentDef[] = [];
|
|
107
114
|
const usedNames = new Set<string>();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
|
|
5
|
+
|
|
6
|
+
export interface Desktop3pConfigLibraryOptions {
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
homeDir?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the config library from the same user-data root Claude Desktop uses. Keeping this in one
|
|
14
|
+
* helper prevents the writer and dashboard status probe from agreeing on a path Desktop never reads.
|
|
15
|
+
*
|
|
16
|
+
* The resolution itself lives in `./desktop-3p-paths`, which ports Claude Desktop's own `GE()`
|
|
17
|
+
* branch for branch — including the `-3p` suffix the app appends to its userData root. Dropping
|
|
18
|
+
* that suffix points us at a directory Desktop never reads (GitHub #539).
|
|
19
|
+
*/
|
|
20
|
+
export function resolveDesktop3pConfigLibraryPath(
|
|
21
|
+
options: Desktop3pConfigLibraryOptions = {},
|
|
22
|
+
): string {
|
|
23
|
+
if (options.env === undefined && options.platform === undefined && options.homeDir === undefined) {
|
|
24
|
+
return claudeDesktopConfigLibraryDir();
|
|
25
|
+
}
|
|
26
|
+
return resolveConfigLibraryDir({
|
|
27
|
+
env: options.env ?? process.env,
|
|
28
|
+
platform: options.platform ?? process.platform,
|
|
29
|
+
home: options.homeDir ?? homedir(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Desktop3pMetadataEntry {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Desktop3pMetadata {
|
|
40
|
+
appliedId?: string;
|
|
41
|
+
entries: Desktop3pMetadataEntry[];
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseMetadata(path: string): Desktop3pMetadata {
|
|
46
|
+
if (!existsSync(path)) return { entries: [] };
|
|
47
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<Desktop3pMetadata>;
|
|
48
|
+
if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
|
|
49
|
+
return { ...parsed, entries: parsed.entries };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SAFE_DESKTOP_PROFILE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
53
|
+
|
|
54
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isOwnedDesktopEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
|
|
59
|
+
return entry?.name === "opencodex" || entry?.name === "opencodex-standard";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A gateway row is removable; the selected standard row must always remain. */
|
|
63
|
+
export function isOwnedDesktopGatewayEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
|
|
64
|
+
return entry?.name === "opencodex";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function profilePath(libraryPath: string, id: string): string {
|
|
68
|
+
if (!SAFE_DESKTOP_PROFILE_ID.test(id)) throw new Error("desktop_profile_id_unsafe");
|
|
69
|
+
return join(libraryPath, `${id}.json`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
|
|
73
|
+
"inferenceProvider",
|
|
74
|
+
"inferenceCredentialKind",
|
|
75
|
+
"inferenceGatewayBaseUrl",
|
|
76
|
+
"inferenceGatewayApiKey",
|
|
77
|
+
"modelDiscoveryEnabled",
|
|
78
|
+
"inferenceModels",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
|
|
82
|
+
if (!existsSync(path)) return {};
|
|
83
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
84
|
+
if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
|
|
85
|
+
return Object.fromEntries(
|
|
86
|
+
Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|