@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.
- package/README.md +9 -0
- package/bin/ocx.mjs +70 -5
- package/gui/dist/assets/index-DDcEW0Cm.css +1 -0
- package/gui/dist/assets/index-DbTEyo46.js +9 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +9 -2
- package/src/adapters/base.ts +6 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +88 -0
- package/src/adapters/cursor/cursor-errors.ts +85 -0
- package/src/adapters/cursor/discovery.ts +144 -0
- package/src/adapters/cursor/effort-map.ts +74 -0
- package/src/adapters/cursor/exec-policy.ts +44 -0
- package/src/adapters/cursor/framing.ts +136 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +25 -0
- package/src/adapters/cursor/live-models.ts +93 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +758 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +236 -0
- package/src/adapters/cursor/message-mapper.ts +46 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +177 -0
- package/src/adapters/cursor/native-exec-fs.ts +284 -0
- package/src/adapters/cursor/native-exec-mcp.ts +151 -0
- package/src/adapters/cursor/native-exec-network.ts +32 -0
- package/src/adapters/cursor/native-exec-shell.ts +191 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +177 -0
- package/src/adapters/cursor/protobuf-events.ts +309 -0
- package/src/adapters/cursor/protobuf-request.ts +347 -0
- package/src/adapters/cursor/request-builder.ts +98 -0
- package/src/adapters/cursor/tool-definitions.ts +301 -0
- package/src/adapters/cursor/transport-retry.ts +116 -0
- package/src/adapters/cursor/transport.ts +47 -0
- package/src/adapters/cursor/types.ts +36 -0
- package/src/adapters/cursor.ts +99 -0
- package/src/adapters/google.ts +7 -1
- package/src/adapters/kiro.ts +15 -0
- package/src/adapters/openai-chat.ts +7 -2
- package/src/adapters/run-turn-queue.ts +58 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/bridge.ts +7 -1
- package/src/cli-help.ts +9 -2
- package/src/cli-status.ts +7 -5
- package/src/cli.ts +122 -79
- package/src/codex-catalog.ts +213 -71
- package/src/codex-history-provider.ts +31 -14
- package/src/codex-inject.ts +17 -9
- package/src/codex-paths.ts +2 -1
- package/src/codex-shim.ts +30 -7
- package/src/codex-sync.ts +70 -0
- package/src/config.ts +58 -2
- package/src/doctor.ts +4 -2
- package/src/index.ts +1 -0
- package/src/model-cache.ts +22 -2
- package/src/oauth/callback-server.ts +44 -16
- package/src/oauth/cursor.ts +188 -0
- package/src/oauth/index.ts +29 -3
- package/src/oauth/key-providers.ts +20 -33
- package/src/oauth/login-cli.ts +7 -4
- package/src/open-url.ts +5 -1
- package/src/ports.ts +13 -0
- package/src/process-control.ts +76 -0
- package/src/provider-label.ts +10 -5
- package/src/providers/derive.ts +30 -3
- package/src/providers/registry.ts +39 -1
- package/src/proxy-liveness.ts +122 -0
- package/src/responses/parser.ts +1 -0
- package/src/responses/state.ts +83 -0
- package/src/router.ts +38 -23
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server.ts +130 -18
- package/src/service.ts +94 -32
- package/src/types.ts +24 -1
- package/src/update-job.ts +360 -0
- package/src/update.ts +73 -11
- package/src/usage-log.ts +3 -3
- package/src/usage-summary.ts +3 -2
- package/src/win-paths.ts +68 -0
- package/gui/dist/assets/index-DIBiVVC0.css +0 -1
- package/gui/dist/assets/index-DcnD944i.js +0 -9
package/src/router.ts
CHANGED
|
@@ -8,17 +8,26 @@ interface RouteResult {
|
|
|
8
8
|
modelId: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const MODEL_PROVIDER_PATTERNS:
|
|
12
|
-
|
|
11
|
+
const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
|
|
12
|
+
{
|
|
13
|
+
providerNames: ["anthropic"],
|
|
14
|
+
prefixes: [
|
|
13
15
|
"claude-", "claude-sonnet-", "claude-opus-", "claude-haiku-",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
providerNames: ["openai", "chatgpt", "openai-apikey"],
|
|
20
|
+
prefixes: [
|
|
16
21
|
"gpt-", "o1-", "o3-", "o4-",
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
providerNames: ["groq"],
|
|
26
|
+
prefixes: [
|
|
19
27
|
"llama-", "mixtral-", "gemma-",
|
|
20
|
-
|
|
21
|
-
}
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
22
31
|
|
|
23
32
|
// Merge registry-default effort maps under user values so persisted built-in provider configs
|
|
24
33
|
// that predate reasoningEffortMap/modelReasoningEffortMap still get correct wire translations
|
|
@@ -151,6 +160,9 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
const patternRoute = routeByKnownModelPattern(config, modelId);
|
|
164
|
+
if (patternRoute) return patternRoute;
|
|
165
|
+
|
|
154
166
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
155
167
|
if (prov.models && Array.isArray(prov.models) && (prov.models as string[]).includes(modelId)) {
|
|
156
168
|
return {
|
|
@@ -161,10 +173,24 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
161
173
|
}
|
|
162
174
|
}
|
|
163
175
|
|
|
164
|
-
|
|
176
|
+
if (hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
177
|
+
const defaultProv = config.providers[config.defaultProvider];
|
|
178
|
+
if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`);
|
|
179
|
+
return {
|
|
180
|
+
providerName: config.defaultProvider,
|
|
181
|
+
provider: routedProviderConfig(config.defaultProvider, defaultProv),
|
|
182
|
+
modelId,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw new Error(`No provider configured for model: ${modelId}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResult | undefined {
|
|
190
|
+
for (const { providerNames, prefixes } of MODEL_PROVIDER_PATTERNS) {
|
|
165
191
|
if (prefixes.some(prefix => modelId.startsWith(prefix))) {
|
|
166
|
-
const matchingProvider =
|
|
167
|
-
([name]) => name ===
|
|
192
|
+
const matchingProvider = Object.entries(config.providers).find(
|
|
193
|
+
([name]) => providerNames.some(providerName => name === providerName || name.startsWith(`${providerName}-`))
|
|
168
194
|
);
|
|
169
195
|
if (matchingProvider) {
|
|
170
196
|
const [provName, prov] = matchingProvider;
|
|
@@ -176,16 +202,5 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
176
202
|
}
|
|
177
203
|
}
|
|
178
204
|
}
|
|
179
|
-
|
|
180
|
-
if (hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
181
|
-
const defaultProv = config.providers[config.defaultProvider];
|
|
182
|
-
if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`);
|
|
183
|
-
return {
|
|
184
|
-
providerName: config.defaultProvider,
|
|
185
|
-
provider: routedProviderConfig(config.defaultProvider, defaultProv),
|
|
186
|
-
modelId,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
throw new Error(`No provider configured for model: ${modelId}`);
|
|
205
|
+
return undefined;
|
|
191
206
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAnthropicAdapter } from "../adapters/anthropic";
|
|
2
2
|
import { createAzureAdapter } from "../adapters/azure";
|
|
3
|
+
import { createCursorAdapter } from "../adapters/cursor";
|
|
3
4
|
import { createGoogleAdapter } from "../adapters/google";
|
|
4
5
|
import { createKiroAdapter } from "../adapters/kiro";
|
|
5
6
|
import { createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
@@ -37,6 +38,8 @@ export function resolveAdapter(providerConfig: OcxProviderConfig) {
|
|
|
37
38
|
case "azure":
|
|
38
39
|
case "azure-openai":
|
|
39
40
|
return createAzureAdapter(providerConfig);
|
|
41
|
+
case "cursor":
|
|
42
|
+
return createCursorAdapter(providerConfig);
|
|
40
43
|
default:
|
|
41
44
|
throw new Error(`Unknown adapter: ${providerConfig.adapter}`);
|
|
42
45
|
}
|
package/src/server.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Server, ServerWebSocket } from "bun";
|
|
|
15
15
|
import {
|
|
16
16
|
DEFAULT_SUBAGENT_MODELS,
|
|
17
17
|
codexAutoStartEnabled,
|
|
18
|
+
applyProxyEnv,
|
|
18
19
|
getConfigPath,
|
|
19
20
|
hasOwnProvider,
|
|
20
21
|
isValidProviderName,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
websocketsEnabled,
|
|
26
27
|
} from "./config";
|
|
27
28
|
import { parseRequest } from "./responses/parser";
|
|
29
|
+
import { expandPreviousResponseInput, previousResponseConversationId, rememberResponseState } from "./responses/state";
|
|
28
30
|
import { routeModel } from "./router";
|
|
29
31
|
import { namespacedToolName } from "./types";
|
|
30
32
|
import {
|
|
@@ -39,7 +41,8 @@ import { describeImagesInPlace, planVisionSidecar } from "./vision";
|
|
|
39
41
|
import { removeCredential } from "./oauth/store";
|
|
40
42
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "./oauth/key-providers";
|
|
41
43
|
import { deriveProviderPresets } from "./providers/derive";
|
|
42
|
-
import
|
|
44
|
+
import { createAdapterEventQueue } from "./adapters/run-turn-queue";
|
|
45
|
+
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig } from "./types";
|
|
43
46
|
import type { OcxUsage } from "./types";
|
|
44
47
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "./provider-context-cap";
|
|
45
48
|
import {
|
|
@@ -190,6 +193,22 @@ const VERSION = (() => {
|
|
|
190
193
|
|
|
191
194
|
// Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
|
|
192
195
|
|
|
196
|
+
function buildToolBridgeMaps(parsed: OcxParsedRequest): {
|
|
197
|
+
toolNsMap: Map<string, { namespace: string; name: string }>;
|
|
198
|
+
freeformToolNames: Set<string>;
|
|
199
|
+
toolSearchToolNames: Set<string>;
|
|
200
|
+
} {
|
|
201
|
+
const toolNsMap = new Map<string, { namespace: string; name: string }>();
|
|
202
|
+
const freeformToolNames = new Set<string>();
|
|
203
|
+
const toolSearchToolNames = new Set<string>();
|
|
204
|
+
for (const t of parsed.context.tools ?? []) {
|
|
205
|
+
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
206
|
+
if (t.freeform) freeformToolNames.add(t.name);
|
|
207
|
+
if (t.toolSearch) toolSearchToolNames.add(t.name);
|
|
208
|
+
}
|
|
209
|
+
return { toolNsMap, freeformToolNames, toolSearchToolNames };
|
|
210
|
+
}
|
|
211
|
+
|
|
193
212
|
function sidecarOutcomeRecorder(config: OcxConfig, authCtx: CodexAuthContext): ((outcome: CodexUpstreamOutcome) => void) | undefined {
|
|
194
213
|
return authCtx.kind === "pool" || authCtx.kind === "main-pool"
|
|
195
214
|
? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome)
|
|
@@ -239,10 +258,12 @@ async function handleResponses(
|
|
|
239
258
|
} catch {
|
|
240
259
|
return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
|
|
241
260
|
}
|
|
261
|
+
body = expandPreviousResponseInput(body);
|
|
242
262
|
|
|
243
263
|
let parsed;
|
|
244
264
|
try {
|
|
245
265
|
parsed = parseRequest(body);
|
|
266
|
+
parsed._cursorConversationId = previousResponseConversationId(parsed.previousResponseId);
|
|
246
267
|
} catch (err) {
|
|
247
268
|
return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
248
269
|
}
|
|
@@ -455,6 +476,62 @@ async function handleResponses(
|
|
|
455
476
|
});
|
|
456
477
|
}
|
|
457
478
|
|
|
479
|
+
if (adapter.runTurn) {
|
|
480
|
+
const runTurnAbort = new AbortController();
|
|
481
|
+
linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
482
|
+
const queue = createAdapterEventQueue();
|
|
483
|
+
const runTurn = async (): Promise<void> => {
|
|
484
|
+
try {
|
|
485
|
+
await adapter.runTurn?.(
|
|
486
|
+
parsed,
|
|
487
|
+
{ headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal },
|
|
488
|
+
queue.push,
|
|
489
|
+
);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
queue.push({
|
|
492
|
+
type: "error",
|
|
493
|
+
message: err instanceof Error ? err.message : String(err),
|
|
494
|
+
});
|
|
495
|
+
} finally {
|
|
496
|
+
queue.close();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
501
|
+
if (parsed.stream) {
|
|
502
|
+
void runTurn();
|
|
503
|
+
const sseStream = bridgeToResponsesSSE(
|
|
504
|
+
queue.stream(), parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
505
|
+
() => {
|
|
506
|
+
runTurnAbort.abort();
|
|
507
|
+
queue.close();
|
|
508
|
+
}, 2_000,
|
|
509
|
+
{
|
|
510
|
+
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
511
|
+
stallTimeoutSec: config.stallTimeoutSec,
|
|
512
|
+
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
513
|
+
onCompletedResponse: response => rememberResponseState(parsed._rawBody, response, parsed._cursorConversationId),
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
const bridgeTurnAc = new AbortController();
|
|
517
|
+
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc);
|
|
518
|
+
return new Response(trackedSse, {
|
|
519
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
await runTurn();
|
|
524
|
+
const events = await queue.collect();
|
|
525
|
+
const json = buildResponseJSON(events, parsed.modelId, {
|
|
526
|
+
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
527
|
+
toolNsMap,
|
|
528
|
+
freeformToolNames,
|
|
529
|
+
toolSearchToolNames,
|
|
530
|
+
});
|
|
531
|
+
rememberResponseState(parsed._rawBody, json, parsed._cursorConversationId);
|
|
532
|
+
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
533
|
+
}
|
|
534
|
+
|
|
458
535
|
// Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
|
|
459
536
|
// run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
|
|
460
537
|
// through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
|
|
@@ -506,14 +583,7 @@ async function handleResponses(
|
|
|
506
583
|
|
|
507
584
|
if (parsed.stream) {
|
|
508
585
|
const eventStream = adapter.parseStream(upstreamResponse);
|
|
509
|
-
const toolNsMap
|
|
510
|
-
const freeformToolNames = new Set<string>();
|
|
511
|
-
const toolSearchToolNames = new Set<string>();
|
|
512
|
-
for (const t of parsed.context.tools ?? []) {
|
|
513
|
-
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
514
|
-
if (t.freeform) freeformToolNames.add(t.name);
|
|
515
|
-
if (t.toolSearch) toolSearchToolNames.add(t.name);
|
|
516
|
-
}
|
|
586
|
+
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
517
587
|
const sseStream = bridgeToResponsesSSE(
|
|
518
588
|
eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
519
589
|
() => upstream.abort(), 2_000,
|
|
@@ -521,6 +591,7 @@ async function handleResponses(
|
|
|
521
591
|
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
522
592
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
523
593
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
594
|
+
onCompletedResponse: response => rememberResponseState(parsed._rawBody, response, parsed._cursorConversationId),
|
|
524
595
|
},
|
|
525
596
|
);
|
|
526
597
|
const bridgeTurnAc = new AbortController();
|
|
@@ -537,20 +608,14 @@ async function handleResponses(
|
|
|
537
608
|
} finally {
|
|
538
609
|
cleanupUpstreamAbort();
|
|
539
610
|
}
|
|
540
|
-
const toolNsMap
|
|
541
|
-
const freeformToolNames = new Set<string>();
|
|
542
|
-
const toolSearchToolNames = new Set<string>();
|
|
543
|
-
for (const t of parsed.context.tools ?? []) {
|
|
544
|
-
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
545
|
-
if (t.freeform) freeformToolNames.add(t.name);
|
|
546
|
-
if (t.toolSearch) toolSearchToolNames.add(t.name);
|
|
547
|
-
}
|
|
611
|
+
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
548
612
|
const json = buildResponseJSON(events, parsed.modelId, {
|
|
549
613
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
550
614
|
toolNsMap,
|
|
551
615
|
freeformToolNames,
|
|
552
616
|
toolSearchToolNames,
|
|
553
617
|
});
|
|
618
|
+
rememberResponseState(parsed._rawBody, json, parsed._cursorConversationId);
|
|
554
619
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
555
620
|
}
|
|
556
621
|
|
|
@@ -1542,6 +1607,51 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
|
|
|
1542
1607
|
return jsonResponse({ ok: true, codexAutoStart: codexAutoStartEnabled(config) });
|
|
1543
1608
|
}
|
|
1544
1609
|
|
|
1610
|
+
if (url.pathname === "/api/sync" && req.method === "POST") {
|
|
1611
|
+
const { syncModelsToCodex } = await import("./codex-sync");
|
|
1612
|
+
const result = await syncModelsToCodex(undefined, config, null);
|
|
1613
|
+
return jsonResponse({
|
|
1614
|
+
...result,
|
|
1615
|
+
staleAppServerHint: "If Codex App still shows an older model list, restart its long-lived app-server process after sync.",
|
|
1616
|
+
}, result.ok ? 200 : 500);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (url.pathname === "/api/update/check" && req.method === "GET") {
|
|
1620
|
+
const { checkForUpdate, normalizeUpdateChannel } = await import("./update-job");
|
|
1621
|
+
const rawTag = url.searchParams.get("tag");
|
|
1622
|
+
if (rawTag && rawTag !== "latest" && rawTag !== "preview") {
|
|
1623
|
+
return jsonResponse({ error: "tag must be latest or preview" }, 400);
|
|
1624
|
+
}
|
|
1625
|
+
return jsonResponse(checkForUpdate(normalizeUpdateChannel(rawTag)));
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (url.pathname === "/api/update/run" && req.method === "POST") {
|
|
1629
|
+
const { normalizeUpdateChannel, startUpdateJob, UpdateJobError } = await import("./update-job");
|
|
1630
|
+
let body: { tag?: unknown; restart?: unknown };
|
|
1631
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1632
|
+
if (body.tag !== undefined && body.tag !== "latest" && body.tag !== "preview") {
|
|
1633
|
+
return jsonResponse({ error: "tag must be latest or preview" }, 400);
|
|
1634
|
+
}
|
|
1635
|
+
if (body.restart !== undefined && typeof body.restart !== "boolean") {
|
|
1636
|
+
return jsonResponse({ error: "restart boolean is required" }, 400);
|
|
1637
|
+
}
|
|
1638
|
+
try {
|
|
1639
|
+
return jsonResponse({ ok: true, job: startUpdateJob(normalizeUpdateChannel(body.tag as string | undefined), body.restart !== false) });
|
|
1640
|
+
} catch (err) {
|
|
1641
|
+
if (err instanceof UpdateJobError) {
|
|
1642
|
+
return jsonResponse({ error: err.message, code: err.code }, err.status);
|
|
1643
|
+
}
|
|
1644
|
+
return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (url.pathname === "/api/update/status" && req.method === "GET") {
|
|
1649
|
+
const { readUpdateJob } = await import("./update-job");
|
|
1650
|
+
const job = readUpdateJob(url.searchParams.get("jobId"));
|
|
1651
|
+
if (!job) return jsonResponse({ error: "update job not found" }, 404);
|
|
1652
|
+
return jsonResponse({ ok: true, job });
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1545
1655
|
if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
|
|
1546
1656
|
const ws = config.webSearchSidecar ?? {};
|
|
1547
1657
|
const vs = config.visionSidecar ?? {};
|
|
@@ -1869,6 +1979,7 @@ async function fetchAllModels(config: OcxConfig): Promise<CatalogModel[]> {
|
|
|
1869
1979
|
|
|
1870
1980
|
export function startServer(port?: number) {
|
|
1871
1981
|
const config = loadConfig();
|
|
1982
|
+
applyProxyEnv(config);
|
|
1872
1983
|
assertServerAuthConfig(config);
|
|
1873
1984
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
1874
1985
|
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
@@ -1942,7 +2053,8 @@ export function startServer(port?: number) {
|
|
|
1942
2053
|
}
|
|
1943
2054
|
|
|
1944
2055
|
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
1945
|
-
|
|
2056
|
+
// service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
|
|
2057
|
+
return jsonResponse({ status: "ok", service: "opencodex", version: VERSION, uptime: process.uptime(), pid: process.pid, port: listenPort }, 200, req, config);
|
|
1946
2058
|
}
|
|
1947
2059
|
|
|
1948
2060
|
if (url.pathname.startsWith("/api/")) {
|
package/src/service.ts
CHANGED
|
@@ -9,12 +9,13 @@ import { execFileSync, execSync } from "node:child_process";
|
|
|
9
9
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { getConfigDir, readPid, removePid } from "./config";
|
|
12
|
+
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
|
|
13
13
|
import { loadConfig } from "./config";
|
|
14
14
|
import { restoreNativeCodex } from "./codex-inject";
|
|
15
15
|
import { durableBunPath, durableBunRuntime } from "./bun-runtime";
|
|
16
|
-
import { isProcessAlive,
|
|
16
|
+
import { isProcessAlive, stopProxy } from "./process-control";
|
|
17
17
|
import { serviceApiTokenFilePath } from "./service-secrets";
|
|
18
|
+
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./win-paths";
|
|
18
19
|
|
|
19
20
|
const LABEL = "com.opencodex.proxy";
|
|
20
21
|
const TASK = "opencodex-proxy";
|
|
@@ -62,11 +63,15 @@ function serviceStatePaths(): string[] {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function currentCodexHome(): string {
|
|
65
|
-
|
|
66
|
+
const raw = process.env.CODEX_HOME?.trim();
|
|
67
|
+
return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex");
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
function currentOpenCodexHome(): string {
|
|
69
|
-
|
|
71
|
+
// getConfigDir() already resolves OPENCODEX_HOME with ~ expansion; keep the
|
|
72
|
+
// install-state comparison on the same normalization or `~/...` values falsely
|
|
73
|
+
// fail the environment-match check depending on cwd.
|
|
74
|
+
return getConfigDir();
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
function normalizePathForCompare(path: string): string {
|
|
@@ -78,13 +83,19 @@ interface ServiceInstallState {
|
|
|
78
83
|
version: 1;
|
|
79
84
|
codexHome: string;
|
|
80
85
|
opencodexHome: string;
|
|
86
|
+
/** Baked at install; lets status flag paths gone stale after npm prefix/nvm moves. */
|
|
87
|
+
bunPath?: string;
|
|
88
|
+
cliPath?: string;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function writeServiceInstallState(): void {
|
|
92
|
+
const { bun, cli } = cliEntry();
|
|
84
93
|
const state: ServiceInstallState = {
|
|
85
94
|
version: 1,
|
|
86
95
|
codexHome: currentCodexHome(),
|
|
87
96
|
opencodexHome: currentOpenCodexHome(),
|
|
97
|
+
bunPath: bun,
|
|
98
|
+
cliPath: cli,
|
|
88
99
|
};
|
|
89
100
|
for (const path of serviceStatePaths()) {
|
|
90
101
|
const dir = dirname(path);
|
|
@@ -252,9 +263,15 @@ function windowsBatchValue(value: string): string {
|
|
|
252
263
|
.replace(/[\r\n]/g, "");
|
|
253
264
|
}
|
|
254
265
|
|
|
255
|
-
|
|
266
|
+
type WindowsBatchValueKind = "raw" | "path" | "pathList";
|
|
267
|
+
|
|
268
|
+
function windowsBatchSet(name: string, value: string | undefined, kind: WindowsBatchValueKind = "raw"): string | null {
|
|
256
269
|
if (!value) return null;
|
|
257
|
-
|
|
270
|
+
const rendered =
|
|
271
|
+
kind === "path" ? windowsEnvIndirectBatchValue(value, windowsBatchValue)
|
|
272
|
+
: kind === "pathList" ? windowsEnvIndirectBatchPathList(value, windowsBatchValue)
|
|
273
|
+
: windowsBatchValue(value);
|
|
274
|
+
return `set "${name}=${rendered}"`;
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
function taskXmlString(value: string): string {
|
|
@@ -273,14 +290,17 @@ export function buildWindowsServiceScript(entry = cliEntry()): string {
|
|
|
273
290
|
const lines = [
|
|
274
291
|
"@echo off",
|
|
275
292
|
"setlocal",
|
|
293
|
+
// The wrapper runs in its own hidden console, so switching that console to UTF-8 is
|
|
294
|
+
// safe (no leak into user shells) and lets cmd parse any UTF-8 remnants correctly.
|
|
295
|
+
"chcp 65001 >nul",
|
|
276
296
|
windowsBatchSet("OCX_SERVICE", "1"),
|
|
277
|
-
windowsBatchSet("PATH", path),
|
|
278
|
-
windowsBatchSet("CODEX_HOME", process.env.CODEX_HOME?.trim()),
|
|
279
|
-
windowsBatchSet("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim()),
|
|
280
|
-
windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath()),
|
|
281
|
-
windowsBatchSet("OCX_SERVICE_LOG", serviceLogPath()),
|
|
282
|
-
windowsBatchSet("OCX_BUN", bun),
|
|
283
|
-
windowsBatchSet("OCX_CLI", cli),
|
|
297
|
+
windowsBatchSet("PATH", path, "pathList"),
|
|
298
|
+
windowsBatchSet("CODEX_HOME", process.env.CODEX_HOME?.trim(), "path"),
|
|
299
|
+
windowsBatchSet("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim(), "path"),
|
|
300
|
+
windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath(), "path"),
|
|
301
|
+
windowsBatchSet("OCX_SERVICE_LOG", serviceLogPath(), "path"),
|
|
302
|
+
windowsBatchSet("OCX_BUN", bun, "path"),
|
|
303
|
+
windowsBatchSet("OCX_CLI", cli, "path"),
|
|
284
304
|
'if exist "%OCX_API_TOKEN_FILE%" (',
|
|
285
305
|
' set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"',
|
|
286
306
|
")",
|
|
@@ -295,7 +315,9 @@ export function buildWindowsServiceScript(entry = cliEntry()): string {
|
|
|
295
315
|
'"%OCX_BUN%" "%OCX_CLI%" start >>"%OCX_SERVICE_LOG%" 2>&1',
|
|
296
316
|
"if %ERRORLEVEL% NEQ 0 (",
|
|
297
317
|
' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] child exited with code %ERRORLEVEL%; restarting in 5s',
|
|
298
|
-
|
|
318
|
+
// `timeout` needs console stdin and dies with "Input redirection is not supported"
|
|
319
|
+
// under Task Scheduler, turning the 5s cooldown into a hot restart loop; ping doesn't.
|
|
320
|
+
" ping -n 6 127.0.0.1 >nul",
|
|
299
321
|
" goto loop",
|
|
300
322
|
")",
|
|
301
323
|
"endlocal",
|
|
@@ -374,13 +396,32 @@ function uninstallLaunchd(): void {
|
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
// ── Windows (Task Scheduler) ──
|
|
399
|
+
/**
|
|
400
|
+
* In-place service-asset write that tolerates the transient EBUSY/EPERM/EACCES Windows
|
|
401
|
+
* throws while the just-ended task's cmd.exe (or an AV scanner) still holds the file.
|
|
402
|
+
*/
|
|
403
|
+
function writeServiceAssetWithRetry(path: string, content: string, encoding: "utf8" | "utf16le"): void {
|
|
404
|
+
for (let attempt = 0; ; attempt++) {
|
|
405
|
+
try {
|
|
406
|
+
writeFileSync(path, content, encoding);
|
|
407
|
+
return;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
410
|
+
if (attempt >= 2 || (code !== "EBUSY" && code !== "EPERM" && code !== "EACCES")) throw err;
|
|
411
|
+
Bun.sleepSync(150);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
377
416
|
function installWindows(): void {
|
|
378
417
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
379
418
|
writeServiceApiTokenFile();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
writeFileSync(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
419
|
+
// End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
|
|
420
|
+
// script mid-rewrite runs a torn batch file, and its open handle can fail the write.
|
|
383
421
|
try { stopWindows(); } catch { /* not running */ }
|
|
422
|
+
const script = windowsServiceScriptPath();
|
|
423
|
+
writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
|
|
424
|
+
writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
384
425
|
schtasks(buildWindowsSchtasksCreateArgs(script));
|
|
385
426
|
schtasks(["/run", "/tn", TASK]);
|
|
386
427
|
writeServiceInstallState();
|
|
@@ -394,8 +435,22 @@ function uninstallWindows(): void {
|
|
|
394
435
|
if (existsSync(windowsTaskXmlPath())) unlinkSync(windowsTaskXmlPath());
|
|
395
436
|
}
|
|
396
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Warn when the paths baked into installed service assets no longer exist (npm prefix
|
|
440
|
+
* moved, nvm switch, reinstall) — the service manager would restart-loop on a dead path
|
|
441
|
+
* while `schtasks`/`launchctl` still report "installed".
|
|
442
|
+
*/
|
|
443
|
+
export function bakedServicePathsDiagnostic(): string | null {
|
|
444
|
+
const state = readServiceInstallState();
|
|
445
|
+
if (!state?.bunPath || !state?.cliPath) return null;
|
|
446
|
+
const missing = [state.bunPath, state.cliPath].filter(path => !existsSync(path));
|
|
447
|
+
if (missing.length === 0) return null;
|
|
448
|
+
return `STALE baked paths (missing: ${missing.join(", ")}) — run 'ocx service install' to re-bake`;
|
|
449
|
+
}
|
|
450
|
+
|
|
397
451
|
function serviceDiagnosticsSummary(): string {
|
|
398
|
-
|
|
452
|
+
const stale = bakedServicePathsDiagnostic();
|
|
453
|
+
return stale ? `${stale}; logs: ${serviceLogPath()}` : `logs: ${serviceLogPath()}`;
|
|
399
454
|
}
|
|
400
455
|
|
|
401
456
|
// ── Linux (systemd user unit) ──
|
|
@@ -489,21 +544,23 @@ function platformOps(): ServiceOps | null {
|
|
|
489
544
|
|
|
490
545
|
type TrackedProxyCleanupResult = "none" | "stale" | "stopped";
|
|
491
546
|
|
|
492
|
-
function stopTrackedProxyIfRunning(): TrackedProxyCleanupResult {
|
|
547
|
+
async function stopTrackedProxyIfRunning(): Promise<TrackedProxyCleanupResult> {
|
|
493
548
|
const pid = readPid();
|
|
494
549
|
if (!pid) return "none";
|
|
495
550
|
if (!isProcessAlive(pid)) {
|
|
496
551
|
removePid(pid);
|
|
552
|
+
removeRuntimePort(pid);
|
|
497
553
|
return "stale";
|
|
498
554
|
}
|
|
499
|
-
|
|
555
|
+
await stopProxy(pid);
|
|
500
556
|
removePid(pid);
|
|
557
|
+
removeRuntimePort(pid);
|
|
501
558
|
return "stopped";
|
|
502
559
|
}
|
|
503
560
|
|
|
504
|
-
function stopTrackedProxyForServiceCommand(): TrackedProxyCleanupResult {
|
|
561
|
+
async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyCleanupResult> {
|
|
505
562
|
try {
|
|
506
|
-
return stopTrackedProxyIfRunning();
|
|
563
|
+
return await stopTrackedProxyIfRunning();
|
|
507
564
|
} catch (err) {
|
|
508
565
|
console.error(`⚠️ Failed to stop proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
509
566
|
return "none";
|
|
@@ -531,6 +588,13 @@ export function stopServiceIfInstalled(): boolean {
|
|
|
531
588
|
return false;
|
|
532
589
|
}
|
|
533
590
|
|
|
591
|
+
/** Delete install-state files; stale state would make `ocx update` "reinstall" a service that no longer exists. */
|
|
592
|
+
function removeServiceInstallState(): void {
|
|
593
|
+
for (const path of serviceStatePaths()) {
|
|
594
|
+
try { if (existsSync(path)) unlinkSync(path); } catch { /* best-effort */ }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
534
598
|
/**
|
|
535
599
|
* Best-effort service removal for full uninstall. Unlike `ocx service uninstall`, this is quiet
|
|
536
600
|
* when no service exists and never exits the process just because the platform has no service
|
|
@@ -540,16 +604,16 @@ export function uninstallServiceIfInstalled(): boolean {
|
|
|
540
604
|
assertServiceEnvironmentMatchesInstall();
|
|
541
605
|
if (process.platform === "darwin") {
|
|
542
606
|
if (existsSync(plistPath())) {
|
|
543
|
-
try { uninstallLaunchd(); return true; } catch { return false; }
|
|
607
|
+
try { uninstallLaunchd(); removeServiceInstallState(); return true; } catch { return false; }
|
|
544
608
|
}
|
|
545
609
|
} else if (process.platform === "win32") {
|
|
546
610
|
try {
|
|
547
611
|
const q = schtasks(["/query", "/tn", TASK]);
|
|
548
|
-
if (q.includes(TASK)) { uninstallWindows(); return true; }
|
|
612
|
+
if (q.includes(TASK)) { uninstallWindows(); removeServiceInstallState(); return true; }
|
|
549
613
|
} catch { /* task not found */ }
|
|
550
614
|
} else if (process.platform === "linux" && existsSync(unitPath())) {
|
|
551
|
-
try { uninstallSystemd(); return true; } catch {
|
|
552
|
-
try { unlinkSync(unitPath()); return true; } catch { return false; }
|
|
615
|
+
try { uninstallSystemd(); removeServiceInstallState(); return true; } catch {
|
|
616
|
+
try { unlinkSync(unitPath()); removeServiceInstallState(); return true; } catch { return false; }
|
|
553
617
|
}
|
|
554
618
|
}
|
|
555
619
|
return false;
|
|
@@ -581,7 +645,7 @@ export function serviceStatusSummary(): string {
|
|
|
581
645
|
return `unsupported on ${process.platform}`;
|
|
582
646
|
}
|
|
583
647
|
|
|
584
|
-
export function serviceCommand(sub?: string): void {
|
|
648
|
+
export async function serviceCommand(sub?: string): Promise<void> {
|
|
585
649
|
const ops = platformOps();
|
|
586
650
|
if (!ops) {
|
|
587
651
|
console.error("ocx service supports macOS (launchd), Windows (Task Scheduler), and Linux (systemd).");
|
|
@@ -602,7 +666,7 @@ export function serviceCommand(sub?: string): void {
|
|
|
602
666
|
case "stop":
|
|
603
667
|
assertServiceEnvironmentMatchesInstall();
|
|
604
668
|
ops.stop();
|
|
605
|
-
stopTrackedProxyForServiceCommand();
|
|
669
|
+
await stopTrackedProxyForServiceCommand();
|
|
606
670
|
restoreNativeCodex();
|
|
607
671
|
console.log("✅ service stopped + native Codex restored.");
|
|
608
672
|
break;
|
|
@@ -616,12 +680,10 @@ export function serviceCommand(sub?: string): void {
|
|
|
616
680
|
case "remove":
|
|
617
681
|
assertServiceEnvironmentMatchesInstall();
|
|
618
682
|
ops.stop();
|
|
619
|
-
stopTrackedProxyForServiceCommand();
|
|
683
|
+
await stopTrackedProxyForServiceCommand();
|
|
620
684
|
ops.uninstall();
|
|
621
685
|
restoreNativeCodex();
|
|
622
|
-
|
|
623
|
-
try { if (existsSync(path)) unlinkSync(path); } catch { /* best-effort */ }
|
|
624
|
-
}
|
|
686
|
+
removeServiceInstallState();
|
|
625
687
|
try { if (existsSync(serviceApiTokenFilePath())) unlinkSync(serviceApiTokenFilePath()); } catch { /* best-effort */ }
|
|
626
688
|
console.log("✅ service uninstalled + native Codex restored.");
|
|
627
689
|
break;
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface OcxParsedRequest {
|
|
|
5
5
|
stream: boolean;
|
|
6
6
|
options: OcxRequestOptions;
|
|
7
7
|
_rawBody?: unknown;
|
|
8
|
+
/** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
|
|
9
|
+
_cursorConversationId?: string;
|
|
8
10
|
/**
|
|
9
11
|
* The hosted `{type:"web_search", ...}` tool config, stashed when Codex enables web search. Routed
|
|
10
12
|
* (non-OpenAI) providers can't run it server-side, so the proxy re-exposes it as a function tool and
|
|
@@ -166,6 +168,7 @@ export interface OcxRequestOptions {
|
|
|
166
168
|
topP?: number;
|
|
167
169
|
stopSequences?: string[];
|
|
168
170
|
toolChoice?: OcxToolChoice;
|
|
171
|
+
parallelToolCalls?: boolean;
|
|
169
172
|
reasoning?: string;
|
|
170
173
|
hideThinkingSummary?: boolean;
|
|
171
174
|
serviceTier?: string;
|
|
@@ -192,7 +195,9 @@ export type AdapterEvent =
|
|
|
192
195
|
| { type: "web_search_call_begin"; id: string }
|
|
193
196
|
| { type: "web_search_call_end"; id: string; queries: string[]; status?: "completed" | "failed"; sources?: OcxUrlCitation[] }
|
|
194
197
|
| { type: "done"; usage?: OcxUsage }
|
|
195
|
-
|
|
198
|
+
// `usage` carries best-effort partial consumption when a turn dies before a clean done
|
|
199
|
+
// (e.g. cursor upstream 502 mid-stream), so failed requests can log real token counts.
|
|
200
|
+
| { type: "error"; message: string; usage?: OcxUsage };
|
|
196
201
|
|
|
197
202
|
/**
|
|
198
203
|
* A web source backing a search answer. Surfaced on the search-end event and rendered by the bridge
|
|
@@ -232,6 +237,12 @@ export interface OcxConfig {
|
|
|
232
237
|
contextCapValue?: number;
|
|
233
238
|
/** Bind hostname. Default "127.0.0.1" (loopback only). Set "0.0.0.0" to expose on all interfaces. */
|
|
234
239
|
hostname?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Outbound HTTP(S) proxy URL for provider requests (e.g. "http://user:pass@proxy:8080", or
|
|
242
|
+
* "${HTTPS_PROXY}"-style env reference). Mirrored into HTTP_PROXY/HTTPS_PROXY at startup when
|
|
243
|
+
* those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
|
|
244
|
+
*/
|
|
245
|
+
proxy?: string;
|
|
235
246
|
/** Upstream stall timeout (seconds). After this many seconds of no upstream data, emits response.incomplete. Default 90. Min 1. */
|
|
236
247
|
stallTimeoutSec?: number;
|
|
237
248
|
/** Connect timeout (ms) for upstream fetch — covers DNS, TCP, TLS, and response header. Default 30000. */
|
|
@@ -359,6 +370,18 @@ export interface OcxProviderConfig {
|
|
|
359
370
|
project?: string;
|
|
360
371
|
/** Vertex AI location, e.g. "us-central1" or "global" (or GOOGLE_CLOUD_LOCATION env). */
|
|
361
372
|
location?: string;
|
|
373
|
+
/**
|
|
374
|
+
* Cursor adapter only: MCP servers opencodex starts/connects and exposes to the Cursor agent
|
|
375
|
+
* as callable tools. Each entry is spawned (stdio `command`) or connected (`url`) lazily per
|
|
376
|
+
* stream; their tools are advertised to the Cursor server and executed against the live server.
|
|
377
|
+
*/
|
|
378
|
+
mcpServers?: Record<string, import("./adapters/cursor/mcp-config").CursorMcpServerConfig>;
|
|
379
|
+
/**
|
|
380
|
+
* Cursor adapter only: opt-in external executor for computer-use / record-screen. opencodex is
|
|
381
|
+
* headless and cannot control a screen itself; provide commands here only when running on a host
|
|
382
|
+
* that can. With no executor, these tools honestly report "not supported".
|
|
383
|
+
*/
|
|
384
|
+
desktopExecutor?: import("./adapters/cursor/native-exec-desktop").DesktopExecutorConfig;
|
|
362
385
|
}
|
|
363
386
|
|
|
364
387
|
export interface CodexAccount {
|