@bitkyc08/opencodex 2.8.0 → 2.8.2-preview.20260731
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/gui/dist/assets/{index-BDjpkcRN.js → index-GC0Vlu1Z.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -7
- package/src/adapters/openai-chat.ts +55 -4
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/init.ts +129 -102
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog.ts +2 -2
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/winsw.ts +6 -0
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +13 -2
- package/src/providers/free-directory.ts +4 -1
- package/src/server/index.ts +3 -3
- package/src/server/management/agent-settings-routes.ts +4 -4
- package/src/server/management/config-routes.ts +5 -0
- package/src/server/management/model-routes.ts +15 -1
- package/src/server/management/shared.ts +16 -3
- package/src/server/proxy-liveness.ts +9 -2
- package/src/service.ts +159 -13
- package/src/tray/windows.ts +54 -9
- package/src/types.ts +5 -0
package/src/oauth/kiro.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
persistKiroCliSessionRecovery,
|
|
19
19
|
readImportedKiroCredential,
|
|
20
20
|
readKiroCliSqliteCredential,
|
|
21
|
+
resolveKiroCliExecutable,
|
|
21
22
|
restoreKiroCliSession,
|
|
22
23
|
restoreStaleKiroCliSessionRecovery,
|
|
23
24
|
requireKiroRegion,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
type KiroCliSessionSnapshot,
|
|
26
27
|
type KiroImportDiagnostic,
|
|
27
28
|
} from "./kiro-credentials";
|
|
29
|
+
import { homedir } from "node:os";
|
|
28
30
|
import { getAccountSet, saveAccountCredential } from "./store";
|
|
29
31
|
|
|
30
32
|
const DEFAULT_REGION = "us-east-1";
|
|
@@ -72,9 +74,18 @@ const pendingKiroLoginTransactions = new WeakMap<OAuthCredentials, KiroCliSessio
|
|
|
72
74
|
/** Forced logins that started with no native CLI DB must logout on persistence failure. */
|
|
73
75
|
const pendingKiroEmptyPriorSessions = new WeakSet<OAuthCredentials>();
|
|
74
76
|
|
|
77
|
+
|
|
78
|
+
function resolveRuntimeKiroCliExecutable(): string {
|
|
79
|
+
return resolveKiroCliExecutable({
|
|
80
|
+
env: process.env,
|
|
81
|
+
platform: process.platform,
|
|
82
|
+
home: process.platform === "win32" ? homedir() : (process.env.HOME || homedir()),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
function logoutKiroCliBestEffort(): void {
|
|
76
87
|
try {
|
|
77
|
-
Bun.spawnSync([
|
|
88
|
+
Bun.spawnSync([resolveRuntimeKiroCliExecutable(), "logout"], {
|
|
78
89
|
stdin: "ignore",
|
|
79
90
|
stdout: "ignore",
|
|
80
91
|
stderr: "ignore",
|
|
@@ -128,7 +139,7 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
|
|
|
128
139
|
throwIfKiroLoginCancelled(signal);
|
|
129
140
|
let child: ReturnType<typeof Bun.spawn>;
|
|
130
141
|
try {
|
|
131
|
-
child = Bun.spawn([
|
|
142
|
+
child = Bun.spawn([resolveRuntimeKiroCliExecutable(), ...args], {
|
|
132
143
|
stdin: "ignore",
|
|
133
144
|
stdout: "pipe",
|
|
134
145
|
stderr: "ignore",
|
|
@@ -18,7 +18,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = {
|
|
|
18
18
|
],
|
|
19
19
|
"recurring-credit": ["bytez", "nous-research"],
|
|
20
20
|
"signup-credit": [
|
|
21
|
-
"agentrouter", "ai21", "baichuan", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
21
|
+
"agentrouter", "ai21", "baichuan", "baseten", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
22
22
|
"hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder",
|
|
23
23
|
"scaleway", "sensenova", "stepfun", "together", "vertex",
|
|
24
24
|
],
|
|
@@ -119,6 +119,9 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
|
119
119
|
agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
|
|
120
120
|
ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
|
|
121
121
|
baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
|
|
122
|
+
// Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
|
|
123
|
+
// and a chat completion against moonshotai/Kimi-K3 returned a standard chat.completion payload.
|
|
124
|
+
baseten: openAi("https://inference.baseten.co/v1", "https://app.baseten.co/settings/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.baseten.co/inference/model-apis/overview", modelsUrl: "https://inference.baseten.co/v1/models", lastVerified: "2026-07-30" }),
|
|
122
125
|
deepinfra: openAi("https://api.deepinfra.com/v1/openai", "https://deepinfra.com/dash/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://deepinfra.com/docs/openai_api" }),
|
|
123
126
|
deepseek: openAi("https://api.deepseek.com", "https://platform.deepseek.com/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://api-docs.deepseek.com/api/list-models" }),
|
|
124
127
|
doubao: openAi("https://ark.cn-beijing.volces.com/api/v3", "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", { verification: "official" }),
|
package/src/server/index.ts
CHANGED
|
@@ -406,7 +406,7 @@ export function startServer(port?: number) {
|
|
|
406
406
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
407
407
|
}
|
|
408
408
|
const goModels = await fetchAllModels(config);
|
|
409
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } = await import("../codex/catalog");
|
|
409
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
410
410
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
411
411
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
412
412
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -424,7 +424,7 @@ export function startServer(port?: number) {
|
|
|
424
424
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config);
|
|
425
425
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
426
426
|
buildDesktop3pRegistry(
|
|
427
|
-
[...
|
|
427
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
428
428
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
429
429
|
config.claudeCode?.desktopProfile,
|
|
430
430
|
);
|
|
@@ -441,7 +441,7 @@ export function startServer(port?: number) {
|
|
|
441
441
|
: idsParam === "desktop"
|
|
442
442
|
? "desktop3p" as const
|
|
443
443
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
444
|
-
const data = buildAnthropicModelInfos([...
|
|
444
|
+
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
|
|
445
445
|
return jsonResponse({ data }, 200, req, config);
|
|
446
446
|
}
|
|
447
447
|
if (url.searchParams.has("client_version")) {
|
|
@@ -80,12 +80,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
80
80
|
if (config.claudeCode?.desktopAutoApply === false) return;
|
|
81
81
|
if (!config.claudeCode?.desktopProfile) return;
|
|
82
82
|
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
83
|
-
const {
|
|
83
|
+
const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
84
84
|
const allModels = await fetchAllModels(config);
|
|
85
85
|
const routed = filterCatalogVisibleModels(allModels, config).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
|
|
86
86
|
const result = writeDesktop3pConfig(
|
|
87
87
|
config.port ?? 10100,
|
|
88
|
-
[...
|
|
88
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
89
89
|
routed,
|
|
90
90
|
config.apiKeys?.[0]?.key,
|
|
91
91
|
"static",
|
|
@@ -607,7 +607,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
607
607
|
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
|
|
608
608
|
saveConfigPreservingClaudeCode(config);
|
|
609
609
|
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
610
|
-
const {
|
|
610
|
+
const { desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
611
611
|
const routed = state.models
|
|
612
612
|
.filter(model => model.available && !model.route.startsWith("native/"))
|
|
613
613
|
.map(model => {
|
|
@@ -616,7 +616,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
616
616
|
});
|
|
617
617
|
const result = writeDesktop3pConfig(
|
|
618
618
|
Number(url.port) || config.port,
|
|
619
|
-
[...
|
|
619
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
620
620
|
routed,
|
|
621
621
|
config.apiKeys?.[0]?.key,
|
|
622
622
|
"static",
|
|
@@ -111,6 +111,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
return jsonResponse({
|
|
114
|
+
// The dashboard renders request-log timestamps. Without this it formats them in the
|
|
115
|
+
// BROWSER's zone, so a KST proxy viewed from a UTC browser reports every request nine
|
|
116
|
+
// hours off (#725). Carried on settings rather than /api/logs because that route's
|
|
117
|
+
// array response has four consumers that would have to change with it.
|
|
118
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
114
119
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
115
120
|
port: config.port,
|
|
116
121
|
hostname: config.hostname ?? "127.0.0.1",
|
|
@@ -83,7 +83,7 @@ import { drainAndShutdown } from "../lifecycle";
|
|
|
83
83
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
84
84
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
85
85
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
86
|
-
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
86
|
+
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
|
|
87
87
|
import { applySystemEnvToggle } from "../system-env";
|
|
88
88
|
|
|
89
89
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
@@ -98,6 +98,20 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
98
98
|
// ~/.opencodex/config.json with the `existing-uuid` test fixture.
|
|
99
99
|
const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
|
|
100
100
|
|
|
101
|
+
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
102
|
+
const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
|
|
103
|
+
const catalog = readCatalog(readCodexCatalogPath());
|
|
104
|
+
if (!catalog) return jsonResponse({ error: "catalog not found" }, 404, req, config);
|
|
105
|
+
const headers: Record<string, string> = {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
...corsHeaders(req, config),
|
|
108
|
+
};
|
|
109
|
+
const { loadPersistedCodexRuntime } = await import("../../codex/runtime");
|
|
110
|
+
const version = loadPersistedCodexRuntime()?.selectedVersion;
|
|
111
|
+
if (version) headers["x-opencodex-codex-version"] = version;
|
|
112
|
+
return new Response(JSON.stringify(catalog), { status: 200, headers });
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
102
116
|
const models = await fetchAllModels(config);
|
|
103
117
|
const disabled = new Set(config.disabledModels ?? []);
|
|
@@ -214,14 +214,14 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid
|
|
|
214
214
|
|
|
215
215
|
/** Shared Desktop profile DTO builder for the management API and CLI. */
|
|
216
216
|
export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) {
|
|
217
|
-
const { filterCatalogVisibleModels, nativeOpenAiContextWindow,
|
|
217
|
+
const { filterCatalogVisibleModels, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
218
218
|
const { DESKTOP_SUPPORTS_1M_THRESHOLD } = await import("../../claude/desktop-3p");
|
|
219
219
|
const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile");
|
|
220
220
|
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
221
221
|
const profileModels: DesktopProfileModel[] = [
|
|
222
222
|
// Native rows carry their real context window from the same accessor the Grok sync
|
|
223
223
|
// uses — otherwise Sol's 372k and gpt-5.5's 272k render as blank on Desktop.
|
|
224
|
-
...
|
|
224
|
+
...desktopVisibleNativeSlugs(config).map(id => {
|
|
225
225
|
const contextWindow = nativeOpenAiContextWindow(id);
|
|
226
226
|
return { route: `native/${id}`, label: `${id} (native)`,
|
|
227
227
|
...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
@@ -233,6 +233,19 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
|
|
|
233
233
|
})),
|
|
234
234
|
];
|
|
235
235
|
const profile = reconcileDesktopProfile(stored ?? config.claudeCode?.desktopProfile, profileModels);
|
|
236
|
+
if (config.claudeCode?.desktopNativeModels === false) {
|
|
237
|
+
for (const route of Object.keys(profile.assignments)) {
|
|
238
|
+
if (route.startsWith("native/")) delete profile.assignments[route];
|
|
239
|
+
}
|
|
240
|
+
for (const family of ["opus", "fable", "sonnet", "haiku"] as const) {
|
|
241
|
+
const current = profile.defaults[family];
|
|
242
|
+
if (current?.startsWith("native/")) {
|
|
243
|
+
profile.defaults[family] = Object.keys(profile.assignments)
|
|
244
|
+
.filter(route => profile.assignments[route]?.family === family)
|
|
245
|
+
.sort()[0] ?? null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
236
249
|
const available = new Set(profileModels.map(model => model.route));
|
|
237
250
|
const modelByRoute = new Map(profileModels.map(model => [model.route, model]));
|
|
238
251
|
// Effort support: routed models with a non-empty reasoningEfforts ladder support effort;
|
|
@@ -241,7 +254,7 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
|
|
|
241
254
|
for (const m of routed) {
|
|
242
255
|
effortByRoute.set(`${m.provider}/${m.id}`, Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0);
|
|
243
256
|
}
|
|
244
|
-
for (const id of
|
|
257
|
+
for (const id of desktopVisibleNativeSlugs(config)) {
|
|
245
258
|
effortByRoute.set(`native/${id}`, true);
|
|
246
259
|
}
|
|
247
260
|
const models = Object.keys(profile.assignments).sort().map(route => ({
|
|
@@ -109,6 +109,13 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
109
109
|
return verified === candidate ? verified : null;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
const verifiedReportedPid = (reported: number | null): number | null => {
|
|
113
|
+
if (reported === null) return null;
|
|
114
|
+
if (!Number.isSafeInteger(reported) || reported <= 0) return null;
|
|
115
|
+
const verified = verifyPidFn(reported);
|
|
116
|
+
return verified === reported ? verified : null;
|
|
117
|
+
};
|
|
118
|
+
|
|
112
119
|
const pid = readPidFn();
|
|
113
120
|
let probedPort: number | null = null;
|
|
114
121
|
if (pid) {
|
|
@@ -136,7 +143,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
136
143
|
// (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
|
|
137
144
|
// would hand destructive callers (stopProxy → kill fallback) a reusable pid.
|
|
138
145
|
if (identity) {
|
|
139
|
-
return { pid: identity.pid
|
|
146
|
+
return { pid: verifiedReportedPid(identity.pid), port: record.port, hostname: record.hostname, source: "runtime" };
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -145,7 +152,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
145
152
|
const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
|
|
146
153
|
if (identity) {
|
|
147
154
|
return {
|
|
148
|
-
pid: identity.pid ?? killablePid(pid),
|
|
155
|
+
pid: verifiedReportedPid(identity.pid) ?? killablePid(pid),
|
|
149
156
|
port,
|
|
150
157
|
hostname: config.hostname,
|
|
151
158
|
source: "config",
|
package/src/service.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* restore it via the command.
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
import { findLiveProxy } from "./server/proxy-liveness";
|
|
9
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
|
|
13
|
+
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
|
|
13
14
|
import { loadConfig } from "./config";
|
|
14
15
|
import { restoreNativeCodex } from "./codex/inject";
|
|
15
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
@@ -17,7 +18,6 @@ import { isWslRuntime } from "./codex/home";
|
|
|
17
18
|
import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
|
|
18
19
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
19
20
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
20
|
-
import { findLiveProxy } from "./server/proxy-liveness";
|
|
21
21
|
import { randomUUID } from "node:crypto";
|
|
22
22
|
import {
|
|
23
23
|
ELEVATION_REQUEST_TIMEOUT_MS,
|
|
@@ -1428,7 +1428,38 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
1428
1428
|
* scheduler backend first; on failure the machine is left with NO service (explicitly
|
|
1429
1429
|
* reported) — never a silent fallback to the scheduler.
|
|
1430
1430
|
*/
|
|
1431
|
+
/** Refuse WinSW when the interactive user is a Microsoft account (SCM cannot authenticate it). */
|
|
1432
|
+
export function assertWindowsNativeServiceAccountSupported(): void {
|
|
1433
|
+
if (process.platform !== "win32") return;
|
|
1434
|
+
const source = readWindowsPrincipalSource();
|
|
1435
|
+
if (source?.toLowerCase() === "microsoftaccount") {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
"The native (WinSW) service backend cannot run under a Microsoft-account Windows login. "
|
|
1438
|
+
+ "Keep the Task Scheduler backend (`ocx service install`) or sign in with a local/domain account before `ocx service install --native`.",
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function readWindowsPrincipalSource(): string | null {
|
|
1444
|
+
if (process.platform !== "win32") return null;
|
|
1445
|
+
const ps = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
1446
|
+
if (!existsSync(ps)) return null;
|
|
1447
|
+
try {
|
|
1448
|
+
const out = execFileSync(ps, [
|
|
1449
|
+
"-NoLogo",
|
|
1450
|
+
"-NoProfile",
|
|
1451
|
+
"-NonInteractive",
|
|
1452
|
+
"-Command",
|
|
1453
|
+
"(Get-LocalUser -Name $env:USERNAME -ErrorAction SilentlyContinue).PrincipalSource",
|
|
1454
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
|
|
1455
|
+
return out || null;
|
|
1456
|
+
} catch {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1431
1461
|
async function installWindowsNative(): Promise<void> {
|
|
1462
|
+
assertWindowsNativeServiceAccountSupported();
|
|
1432
1463
|
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1433
1464
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1434
1465
|
writeServiceApiTokenFile();
|
|
@@ -1463,11 +1494,49 @@ async function installWindowsNative(): Promise<void> {
|
|
|
1463
1494
|
writeServiceInstallState("native");
|
|
1464
1495
|
}
|
|
1465
1496
|
function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
|
|
1466
|
-
|
|
1497
|
+
|
|
1498
|
+
export function isWindowsSchedulerEndBenign(error: unknown): boolean {
|
|
1499
|
+
const detail = schtasksErrorDetail(error).toLowerCase();
|
|
1500
|
+
return detail.includes("no running instance")
|
|
1501
|
+
|| detail.includes("not currently running")
|
|
1502
|
+
|| detail.includes("0x41330");
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* End the scheduler task. "Already stopped" is success; other `/end` failures are
|
|
1507
|
+
* swallowed so callers can still run tracked-proxy + live-proxy cleanup.
|
|
1508
|
+
*
|
|
1509
|
+
* Do not key a restart-window wait on `/end` failure: the #764 case is an `/end`
|
|
1510
|
+
* that *succeeds* while the wrapper survives and respawns. That verification lives
|
|
1511
|
+
* on the stop-verification path (poll across the restart window), not here.
|
|
1512
|
+
*/
|
|
1513
|
+
export function stopWindows(): void {
|
|
1514
|
+
try {
|
|
1515
|
+
schtasks(["/end", "/tn", TASK]);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (isWindowsSchedulerEndBenign(error)) return;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1467
1520
|
function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
|
|
1468
1521
|
function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
|
|
1469
1522
|
function uninstallWindows(): void {
|
|
1470
|
-
|
|
1523
|
+
const probe = probeWindowsSchedulerTask(TASK);
|
|
1524
|
+
if (probe.status === "present") {
|
|
1525
|
+
try {
|
|
1526
|
+
schtasks(["/delete", "/tn", TASK, "/f"]);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
throw new Error(`Failed to delete Task Scheduler task ${TASK}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1529
|
+
}
|
|
1530
|
+
const afterDelete = probeWindowsSchedulerTask(TASK);
|
|
1531
|
+
if (afterDelete.status === "present") {
|
|
1532
|
+
throw new Error(`Task Scheduler task ${TASK} is still present after delete — refusing to remove service assets. Retry from an elevated shell.`);
|
|
1533
|
+
}
|
|
1534
|
+
if (afterDelete.status === "unknown") {
|
|
1535
|
+
throw new Error(`Task Scheduler task ${TASK} presence could not be verified after delete — refusing to remove service assets.`);
|
|
1536
|
+
}
|
|
1537
|
+
} else if (probe.status === "unknown") {
|
|
1538
|
+
throw new Error(`Task Scheduler task ${TASK} presence could not be verified — refusing to remove service assets.`);
|
|
1539
|
+
}
|
|
1471
1540
|
if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
|
|
1472
1541
|
if (existsSync(windowsLauncherVbsPath())) unlinkSync(windowsLauncherVbsPath());
|
|
1473
1542
|
if (existsSync(windowsTaskXmlPath())) unlinkSync(windowsTaskXmlPath());
|
|
@@ -1626,18 +1695,77 @@ function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
|
1626
1695
|
|
|
1627
1696
|
type TrackedProxyCleanupResult = "none" | "stale" | "stopped";
|
|
1628
1697
|
|
|
1698
|
+
function verifiedKillTarget(pid: number | null | undefined): number | null {
|
|
1699
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
1700
|
+
const verified = verifyPidIdentity(pid);
|
|
1701
|
+
return verified === pid ? verified : null;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Whether a proxy is still answering after the service manager claimed to stop it.
|
|
1706
|
+
*
|
|
1707
|
+
* `ops.stop()` reports the outcome of the STOP COMMAND, not of the process. A Windows scheduler
|
|
1708
|
+
* task whose wrapper survives `schtasks /end` respawns its child a few seconds later, so a stop
|
|
1709
|
+
* that returned success can still leave a live proxy — and `ocx service stop` then restored
|
|
1710
|
+
* native Codex on top of a running one (#764). The tracked-pid cleanup does not catch it either:
|
|
1711
|
+
* the respawned child writes a different pid, or none this process knows about.
|
|
1712
|
+
*
|
|
1713
|
+
* Probed rather than assumed, and bounded. The respawn risk is specific to a supervisor that can
|
|
1714
|
+
* restart its child — the Windows scheduler wrapper — so only that case pays the restart window.
|
|
1715
|
+
* Everywhere else a single probe answers the question, because nothing is going to bring the
|
|
1716
|
+
* proxy back after `launchctl unload` or `systemctl stop`. Making every platform wait 7s on a
|
|
1717
|
+
* stop that already succeeded would trade one bug for a worse everyday one.
|
|
1718
|
+
*/
|
|
1719
|
+
export async function proxyStillLiveAfterStop(deps: {
|
|
1720
|
+
findProxy?: () => Promise<{ port: number } | null>;
|
|
1721
|
+
sleep?: (ms: number) => Promise<void>;
|
|
1722
|
+
now?: () => number;
|
|
1723
|
+
/** Whether the stopped supervisor can respawn its child; only then is polling worth the wait. */
|
|
1724
|
+
canRespawn?: boolean;
|
|
1725
|
+
} = {}): Promise<{ port: number } | null> {
|
|
1726
|
+
const findProxy = deps.findProxy ?? findLiveProxy;
|
|
1727
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
1728
|
+
const now = deps.now ?? Date.now;
|
|
1729
|
+
const canRespawn = deps.canRespawn ?? process.platform === "win32";
|
|
1730
|
+
const deadline = now() + (canRespawn ? 7000 : 0);
|
|
1731
|
+
for (;;) {
|
|
1732
|
+
try {
|
|
1733
|
+
const live = await findProxy();
|
|
1734
|
+
if (live) return live;
|
|
1735
|
+
} catch {
|
|
1736
|
+
// A probe failure is not proof the proxy is gone; keep polling until the deadline.
|
|
1737
|
+
}
|
|
1738
|
+
if (now() >= deadline) return null;
|
|
1739
|
+
await sleep(1000);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1629
1743
|
async function stopTrackedProxyIfRunning(): Promise<TrackedProxyCleanupResult> {
|
|
1744
|
+
let stopped = false;
|
|
1630
1745
|
const pid = readPid();
|
|
1631
|
-
|
|
1632
|
-
if (
|
|
1746
|
+
const trackedKillPid = verifiedKillTarget(pid);
|
|
1747
|
+
if (trackedKillPid !== null && isProcessAlive(trackedKillPid)) {
|
|
1748
|
+
await stopProxy(trackedKillPid);
|
|
1749
|
+
removePid(trackedKillPid);
|
|
1750
|
+
removeRuntimePort(trackedKillPid);
|
|
1751
|
+
stopped = true;
|
|
1752
|
+
} else if (pid) {
|
|
1633
1753
|
removePid(pid);
|
|
1634
1754
|
removeRuntimePort(pid);
|
|
1635
|
-
return "stale";
|
|
1636
1755
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1756
|
+
// Orphan recovery: the pid file can be missing/stale while the service wrapper keeps
|
|
1757
|
+
// a live proxy running — mirror `ocx stop`'s identity-checked findLiveProxy fallback.
|
|
1758
|
+
const live = await findLiveProxy({ timeoutMs: 1500 });
|
|
1759
|
+
const liveKillPid = verifiedKillTarget(live?.pid);
|
|
1760
|
+
if (liveKillPid !== null) {
|
|
1761
|
+
await stopProxy(liveKillPid);
|
|
1762
|
+
removePid(liveKillPid);
|
|
1763
|
+
removeRuntimePort(liveKillPid);
|
|
1764
|
+
stopped = true;
|
|
1765
|
+
}
|
|
1766
|
+
if (stopped) return "stopped";
|
|
1767
|
+
if (pid) return "stale";
|
|
1768
|
+
return "none";
|
|
1641
1769
|
}
|
|
1642
1770
|
|
|
1643
1771
|
async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyCleanupResult> {
|
|
@@ -1945,13 +2073,29 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1945
2073
|
ops.start();
|
|
1946
2074
|
console.log("✅ service started.");
|
|
1947
2075
|
break;
|
|
1948
|
-
case "stop":
|
|
2076
|
+
case "stop": {
|
|
1949
2077
|
assertServiceEnvironmentMatchesInstall();
|
|
1950
2078
|
// Only stop what is actually installed. The unguarded call ran a real `launchctl unload`
|
|
1951
2079
|
// (and its Windows/Linux twins) even with nothing installed.
|
|
1952
|
-
if (ops.status() !== null || isServiceInstalled())
|
|
2080
|
+
if (ops.status() !== null || isServiceInstalled()) {
|
|
2081
|
+
ops.stop();
|
|
2082
|
+
}
|
|
1953
2083
|
await stopTrackedProxyForServiceCommand();
|
|
1954
2084
|
{
|
|
2085
|
+
// Verify rather than trust the stop command: a surviving wrapper respawns its child
|
|
2086
|
+
// seconds later, and restoring native Codex on top of a live proxy is the failure #764
|
|
2087
|
+
// reports as "stop reports success without stopping the proxy".
|
|
2088
|
+
const survivor = await proxyStillLiveAfterStop();
|
|
2089
|
+
if (survivor) {
|
|
2090
|
+
console.error(
|
|
2091
|
+
`❌ service stop did not take effect: a proxy is still listening on port ${survivor.port}.`
|
|
2092
|
+
+ "\nNative Codex was NOT restored, because doing so while the proxy is running leaves"
|
|
2093
|
+
+ " both pointing at each other. Check for a second service backend (`ocx service status`)"
|
|
2094
|
+
+ " or a manually started proxy, then re-run `ocx service stop`.",
|
|
2095
|
+
);
|
|
2096
|
+
process.exitCode = 1;
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
1955
2099
|
const restore = restoreNativeCodex();
|
|
1956
2100
|
if (restore.success) console.log("✅ service stopped + native Codex restored.");
|
|
1957
2101
|
else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
|
|
@@ -1962,6 +2106,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1962
2106
|
else if (!grok.ok) console.error(`⚠️ ${grok.message}`);
|
|
1963
2107
|
}
|
|
1964
2108
|
break;
|
|
2109
|
+
}
|
|
1965
2110
|
case "status": {
|
|
1966
2111
|
if (process.platform === "win32" && backend === "scheduler") {
|
|
1967
2112
|
console.log(await inspectWindowsSchedulerServiceStatus());
|
|
@@ -2007,3 +2152,4 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2007
2152
|
process.exit(1);
|
|
2008
2153
|
}
|
|
2009
2154
|
}
|
|
2155
|
+
|
package/src/tray/windows.ts
CHANGED
|
@@ -150,8 +150,16 @@ function quoteRunValue(value: string): string {
|
|
|
150
150
|
return `\"${value}\"`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
function installedTrayLauncherPath(): string {
|
|
154
|
+
return join(getConfigDir(), "opencodex-tray.vbs");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function quoteVbsPath(value: string): string {
|
|
158
|
+
return value.replace(/"/g, '""');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Full PowerShell invocation used by the owned VBS launcher (not written to HKCU Run). */
|
|
162
|
+
export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
|
|
155
163
|
return [
|
|
156
164
|
quoteRunValue(powershell),
|
|
157
165
|
"-NoLogo",
|
|
@@ -169,6 +177,27 @@ export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry, powershell =
|
|
|
169
177
|
].join(" ");
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
/** Short HKCU Run command (must stay ≤260 chars under long Windows user/npm paths). */
|
|
181
|
+
export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry & { launcherPath: string }): string {
|
|
182
|
+
const wscript = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
|
|
183
|
+
return `${quoteRunValue(wscript)} //B //NoLogo ${quoteRunValue(entry.launcherPath)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function buildWindowsTrayLauncherScript(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
|
|
187
|
+
const command = buildWindowsTrayPowerShellCommand(entry, powershell);
|
|
188
|
+
// VBS CreateObject("WScript.Shell").Run command, 0, False — hidden, non-blocking.
|
|
189
|
+
return [
|
|
190
|
+
"' OpenCodex owned tray launcher — do not edit by hand.",
|
|
191
|
+
`CreateObject("WScript.Shell").Run "${quoteVbsPath(command)}", 0, False`,
|
|
192
|
+
"",
|
|
193
|
+
].join("\r\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @deprecated Prefer buildWindowsTrayPowerShellCommand; kept for callers that still expect the long form. */
|
|
197
|
+
export function buildWindowsTrayLegacyRunCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
|
|
198
|
+
return buildWindowsTrayPowerShellCommand(entry, powershell);
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
function readState(): WindowsTrayState | null {
|
|
173
202
|
try {
|
|
174
203
|
const state = JSON.parse(readFileSync(trayStatePath(), "utf8")) as Partial<WindowsTrayState>;
|
|
@@ -204,7 +233,11 @@ function replaceOwnedFile(path: string, contents: string | Buffer): void {
|
|
|
204
233
|
}
|
|
205
234
|
}
|
|
206
235
|
|
|
207
|
-
function writeState(
|
|
236
|
+
function writeState(
|
|
237
|
+
entry: WindowsTrayEntry & { launcherPath: string },
|
|
238
|
+
runValue: string,
|
|
239
|
+
runCommand: string,
|
|
240
|
+
): void {
|
|
208
241
|
const path = trayStatePath();
|
|
209
242
|
replaceOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
|
|
210
243
|
}
|
|
@@ -376,7 +409,8 @@ function trayStatusFrom(registered: string | null): WindowsTrayStatus {
|
|
|
376
409
|
const running = heartbeatProcessAlive(heartbeat);
|
|
377
410
|
const registrationOwned = state !== null
|
|
378
411
|
&& registered === state.runCommand
|
|
379
|
-
&& [state.bun, state.cli, state.script, ...
|
|
412
|
+
&& [state.bun, state.cli, state.script, ...(state.launcherPath ? [state.launcherPath] : []), ...installedTrayIconPaths()]
|
|
413
|
+
.every(path => existsSync(path));
|
|
380
414
|
const stale = windowsTrayRegistrationIsStale({
|
|
381
415
|
registered: registered !== null,
|
|
382
416
|
registrationOwned,
|
|
@@ -507,7 +541,12 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
507
541
|
}
|
|
508
542
|
recordOwnedConfigPath(getConfigDir(), trayStatePath());
|
|
509
543
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
510
|
-
const
|
|
544
|
+
const launcherPath = installedTrayLauncherPath();
|
|
545
|
+
const entryWithLauncher = { ...entry, launcherPath };
|
|
546
|
+
const runCommand = buildWindowsTrayRunCommand(entryWithLauncher);
|
|
547
|
+
if (runCommand.length > 260) {
|
|
548
|
+
throw new Error(`Tray Run command exceeds the Windows 260-character limit (${runCommand.length} chars).`);
|
|
549
|
+
}
|
|
511
550
|
const runValue = windowsTrayRunValue(entry.opencodexHome);
|
|
512
551
|
const existing = readOwnedRunValue(runValue);
|
|
513
552
|
const state = readState();
|
|
@@ -517,6 +556,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
517
556
|
if (existsSync(entry.script) && (!state || resolve(state.script) !== resolve(entry.script))) {
|
|
518
557
|
throw new Error(`Refusing to overwrite an unowned tray script at ${entry.script}.`);
|
|
519
558
|
}
|
|
559
|
+
if (existsSync(launcherPath) && (!state?.launcherPath || resolve(state.launcherPath) !== resolve(launcherPath))) {
|
|
560
|
+
throw new Error(`Refusing to overwrite an unowned tray launcher at ${launcherPath}.`);
|
|
561
|
+
}
|
|
520
562
|
if (!state && iconPairs.some(pair => existsSync(pair.installed))) {
|
|
521
563
|
throw new Error("Refusing to overwrite unowned Windows tray icon assets.");
|
|
522
564
|
}
|
|
@@ -531,6 +573,7 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
531
573
|
|
|
532
574
|
const previousStateBytes = existsSync(trayStatePath()) ? readFileSync(trayStatePath()) : null;
|
|
533
575
|
const previousScriptBytes = existsSync(entry.script) ? readFileSync(entry.script) : null;
|
|
576
|
+
const previousLauncherBytes = existsSync(launcherPath) ? readFileSync(launcherPath) : null;
|
|
534
577
|
const previousIconBytes = new Map(iconPairs.map(pair => [
|
|
535
578
|
pair.installed,
|
|
536
579
|
existsSync(pair.installed) ? readFileSync(pair.installed) : null,
|
|
@@ -540,6 +583,10 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
540
583
|
if (previousScriptBytes) replaceOwnedFile(entry.script, previousScriptBytes);
|
|
541
584
|
else if (existsSync(entry.script)) unlinkSync(entry.script);
|
|
542
585
|
} catch { /* rollback best-effort */ }
|
|
586
|
+
try {
|
|
587
|
+
if (previousLauncherBytes) replaceOwnedFile(launcherPath, previousLauncherBytes);
|
|
588
|
+
else if (existsSync(launcherPath)) unlinkSync(launcherPath);
|
|
589
|
+
} catch { /* rollback best-effort */ }
|
|
543
590
|
for (const [path, contents] of previousIconBytes) {
|
|
544
591
|
try {
|
|
545
592
|
if (contents) replaceOwnedFile(path, contents);
|
|
@@ -567,8 +614,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
567
614
|
if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence.");
|
|
568
615
|
replaceOwnedFile(entry.script, readFileSync(sourceScript));
|
|
569
616
|
for (const pair of iconPairs) replaceOwnedFile(pair.installed, readFileSync(pair.source));
|
|
617
|
+
replaceOwnedFile(launcherPath, Buffer.from("\uFEFF" + buildWindowsTrayLauncherScript(entry), "utf16le"));
|
|
570
618
|
runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]);
|
|
571
|
-
writeState(
|
|
619
|
+
writeState(entryWithLauncher, runValue, runCommand);
|
|
572
620
|
} catch (error) {
|
|
573
621
|
restorePreviousInstall();
|
|
574
622
|
throw error;
|
|
@@ -578,9 +626,6 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
578
626
|
restorePreviousInstall();
|
|
579
627
|
throw new Error("The tray startup registration was installed, but the tray process did not become healthy.");
|
|
580
628
|
}
|
|
581
|
-
if (state?.launcherPath && existsSync(state.launcherPath)) {
|
|
582
|
-
try { unlinkSync(state.launcherPath); } catch { /* old owned VBS is inert after a committed Run replacement */ }
|
|
583
|
-
}
|
|
584
629
|
return getWindowsTrayStatus();
|
|
585
630
|
}
|
|
586
631
|
|
package/src/types.ts
CHANGED
|
@@ -455,6 +455,11 @@ export interface OcxClaudeCodeConfig {
|
|
|
455
455
|
desktopProfile?: OcxClaudeDesktopProfile;
|
|
456
456
|
/** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */
|
|
457
457
|
desktopAutoApply?: boolean;
|
|
458
|
+
/**
|
|
459
|
+
* When false, omit `native/*` rows from Claude Desktop show/export/apply. Default: enabled.
|
|
460
|
+
* Routing-sidecar alias decoding is unchanged — only the Desktop model list writer.
|
|
461
|
+
*/
|
|
462
|
+
desktopNativeModels?: boolean;
|
|
458
463
|
}
|
|
459
464
|
|
|
460
465
|
export type OcxClaudeDesktopFamily = "opus" | "fable" | "sonnet" | "haiku";
|