@bitkyc08/opencodex 2.10.2 → 2.11.0-preview.20260808
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-B1P60C4o.js +70 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { atomicWriteFile } from "../config";
|
|
@@ -103,6 +103,45 @@ interface Desktop3pMetadata {
|
|
|
103
103
|
[key: string]: unknown;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export type Desktop3pLibraryKind =
|
|
107
|
+
| "not_installed"
|
|
108
|
+
| "standard"
|
|
109
|
+
| "gateway_ours"
|
|
110
|
+
| "gateway_drifted"
|
|
111
|
+
| "foreign"
|
|
112
|
+
| "no_owned_state"
|
|
113
|
+
| "broken"
|
|
114
|
+
| "unsafe";
|
|
115
|
+
|
|
116
|
+
export interface Desktop3pLibraryInspection {
|
|
117
|
+
kind: Desktop3pLibraryKind;
|
|
118
|
+
libraryPath: string;
|
|
119
|
+
selectedProfilePath: string | null;
|
|
120
|
+
appliedId: string | null;
|
|
121
|
+
/** Paths of opencodex-owned rows that are not selected by Desktop. */
|
|
122
|
+
residualPaths: string[];
|
|
123
|
+
/** Bounded reason code; never includes metadata or profile contents. */
|
|
124
|
+
reason?: "metadata_unreadable" | "unsafe_applied_id" | "invalid_owned_profile";
|
|
125
|
+
fingerprint?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Whether Desktop's applied selection is our owned entry, by ID match alone.
|
|
128
|
+
* `null` = undeterminable (no metadata, unreadable metadata, or no appliedId);
|
|
129
|
+
* a readable appliedId with no owned entry is a KNOWN false, not unknown.
|
|
130
|
+
* Deliberately independent of profile-file health: the status contract
|
|
131
|
+
* predates this inspector and callers render tri-state.
|
|
132
|
+
*/
|
|
133
|
+
ownedProfileActive: boolean | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface Desktop3pRemovalResult {
|
|
137
|
+
ok: boolean;
|
|
138
|
+
changed: boolean;
|
|
139
|
+
kind: "removed" | "noop" | "cleanup_incomplete" | "unsafe" | "write_failed";
|
|
140
|
+
libraryPath: string;
|
|
141
|
+
residualPaths?: string[];
|
|
142
|
+
reason?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
let desktop3pRegistry = new Map<string, string>();
|
|
107
146
|
let desktop3pAliasesByRoute = new Map<string, string>();
|
|
108
147
|
|
|
@@ -327,6 +366,186 @@ function parseMetadata(path: string): Desktop3pMetadata {
|
|
|
327
366
|
return { ...parsed, entries: parsed.entries };
|
|
328
367
|
}
|
|
329
368
|
|
|
369
|
+
const SAFE_DESKTOP_PROFILE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
370
|
+
|
|
371
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function isOwnedDesktopEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
|
|
376
|
+
return entry?.name === "opencodex" || entry?.name === "opencodex-standard";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** A gateway row is removable; the selected standard row must always remain. */
|
|
380
|
+
function isOwnedDesktopGatewayEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
|
|
381
|
+
return entry?.name === "opencodex";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function profilePath(libraryPath: string, id: string): string {
|
|
385
|
+
return join(libraryPath, `${id}.json`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Read Desktop's selected config without changing its library.
|
|
390
|
+
*
|
|
391
|
+
* This is intentionally separate from the eager writer below: status probes must
|
|
392
|
+
* never manufacture a config-library directory on a machine without Desktop.
|
|
393
|
+
*/
|
|
394
|
+
export function inspectDesktop3pConfigLibrary(
|
|
395
|
+
options: Desktop3pConfigLibraryOptions & { appliedFingerprint?: string | null } = {},
|
|
396
|
+
): Desktop3pLibraryInspection {
|
|
397
|
+
const libraryPath = resolveDesktop3pConfigLibraryPath(options);
|
|
398
|
+
if (!existsSync(libraryPath)) {
|
|
399
|
+
return { kind: "not_installed", libraryPath, selectedProfilePath: null, appliedId: null, residualPaths: [], ownedProfileActive: null };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const metadataPath = join(libraryPath, "_meta.json");
|
|
403
|
+
if (!existsSync(metadataPath)) {
|
|
404
|
+
return { kind: "no_owned_state", libraryPath, selectedProfilePath: null, appliedId: null, residualPaths: [], ownedProfileActive: null };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let metadata: Desktop3pMetadata;
|
|
408
|
+
try {
|
|
409
|
+
metadata = parseMetadata(metadataPath);
|
|
410
|
+
} catch {
|
|
411
|
+
return {
|
|
412
|
+
kind: "unsafe", libraryPath, selectedProfilePath: null, appliedId: null, residualPaths: [], reason: "metadata_unreadable", ownedProfileActive: null,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const appliedId = typeof metadata.appliedId === "string" ? metadata.appliedId : null;
|
|
416
|
+
if (appliedId === null) {
|
|
417
|
+
return { kind: "no_owned_state", libraryPath, selectedProfilePath: null, appliedId: null, residualPaths: [], ownedProfileActive: null };
|
|
418
|
+
}
|
|
419
|
+
const selected = metadata.entries.find(entry => entry?.id === appliedId);
|
|
420
|
+
// A readable appliedId with no owned entry is a KNOWN false, not unknown.
|
|
421
|
+
const ownedProfileActive = isOwnedDesktopEntry(selected);
|
|
422
|
+
if (!SAFE_DESKTOP_PROFILE_ID.test(appliedId)) {
|
|
423
|
+
return {
|
|
424
|
+
kind: "unsafe", libraryPath, selectedProfilePath: null, appliedId, residualPaths: [], reason: "unsafe_applied_id", ownedProfileActive,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const selectedProfilePath = profilePath(libraryPath, appliedId);
|
|
429
|
+
const residualPaths = metadata.entries
|
|
430
|
+
.filter(entry => isOwnedDesktopGatewayEntry(entry) && entry.id !== appliedId && SAFE_DESKTOP_PROFILE_ID.test(entry.id))
|
|
431
|
+
.flatMap(entry => [profilePath(libraryPath, entry.id), `${profilePath(libraryPath, entry.id)}.bak`])
|
|
432
|
+
.filter(existsSync);
|
|
433
|
+
if (!existsSync(selectedProfilePath)) {
|
|
434
|
+
return { kind: "broken", libraryPath, selectedProfilePath, appliedId, residualPaths, ownedProfileActive };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let profile: Record<string, unknown>;
|
|
438
|
+
let fingerprint: string;
|
|
439
|
+
try {
|
|
440
|
+
const source = readFileSync(selectedProfilePath, "utf8");
|
|
441
|
+
const parsed = JSON.parse(source) as unknown;
|
|
442
|
+
if (!isRecord(parsed)) return { kind: "broken", libraryPath, selectedProfilePath, appliedId, residualPaths, ownedProfileActive };
|
|
443
|
+
profile = parsed;
|
|
444
|
+
fingerprint = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
445
|
+
} catch {
|
|
446
|
+
return { kind: "broken", libraryPath, selectedProfilePath, appliedId, residualPaths, ownedProfileActive };
|
|
447
|
+
}
|
|
448
|
+
if (!isOwnedDesktopEntry(selected)) {
|
|
449
|
+
return { kind: "foreign", libraryPath, selectedProfilePath, appliedId, residualPaths, fingerprint, ownedProfileActive };
|
|
450
|
+
}
|
|
451
|
+
if (profile.inferenceProvider === undefined) {
|
|
452
|
+
return { kind: "standard", libraryPath, selectedProfilePath, appliedId, residualPaths, fingerprint, ownedProfileActive };
|
|
453
|
+
}
|
|
454
|
+
const validGateway = profile.inferenceProvider === "gateway"
|
|
455
|
+
&& profile.inferenceCredentialKind === "static"
|
|
456
|
+
&& typeof profile.inferenceGatewayBaseUrl === "string"
|
|
457
|
+
&& typeof profile.inferenceGatewayApiKey === "string";
|
|
458
|
+
if (!validGateway) {
|
|
459
|
+
return {
|
|
460
|
+
kind: "unsafe", libraryPath, selectedProfilePath, appliedId, residualPaths, fingerprint, reason: "invalid_owned_profile", ownedProfileActive,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
kind: options.appliedFingerprint && options.appliedFingerprint === fingerprint ? "gateway_ours" : "gateway_drifted",
|
|
465
|
+
libraryPath, selectedProfilePath, appliedId, residualPaths, fingerprint, ownedProfileActive,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Select a credential-free standard profile before deleting an owned gateway.
|
|
471
|
+
* The old metadata row remains as a retry locator only until both its profile
|
|
472
|
+
* and backup are absent; successful cleanup removes it in the same operation.
|
|
473
|
+
*/
|
|
474
|
+
export function removeDesktop3pStandardPivot(
|
|
475
|
+
options: Desktop3pConfigLibraryOptions & {
|
|
476
|
+
appliedFingerprint?: string | null;
|
|
477
|
+
unlink?: (path: string) => void;
|
|
478
|
+
} = {},
|
|
479
|
+
): Desktop3pRemovalResult {
|
|
480
|
+
const inspected = inspectDesktop3pConfigLibrary(options);
|
|
481
|
+
if (inspected.kind === "not_installed" || inspected.kind === "no_owned_state") {
|
|
482
|
+
return { ok: true, changed: false, kind: "noop", libraryPath: inspected.libraryPath };
|
|
483
|
+
}
|
|
484
|
+
if (inspected.kind === "broken" || inspected.kind === "unsafe" || inspected.kind === "gateway_drifted") {
|
|
485
|
+
return { ok: false, changed: false, kind: "unsafe", libraryPath: inspected.libraryPath, reason: inspected.reason };
|
|
486
|
+
}
|
|
487
|
+
if (!inspected.appliedId || !SAFE_DESKTOP_PROFILE_ID.test(inspected.appliedId)) {
|
|
488
|
+
return { ok: false, changed: false, kind: "unsafe", libraryPath: inspected.libraryPath, reason: "unsafe_applied_id" };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const metadataPath = join(inspected.libraryPath, "_meta.json");
|
|
492
|
+
try {
|
|
493
|
+
const metadata = parseMetadata(metadataPath);
|
|
494
|
+
const selectedId = inspected.appliedId;
|
|
495
|
+
// When Desktop is actively using our gateway, pivot only that selected row
|
|
496
|
+
// first. Any second owned row is residue for a later standard-mode retry;
|
|
497
|
+
// this preserves the selected-row preference after an interrupted cleanup.
|
|
498
|
+
const targetIds = inspected.kind === "gateway_ours"
|
|
499
|
+
? [selectedId]
|
|
500
|
+
: metadata.entries
|
|
501
|
+
.filter(isOwnedDesktopGatewayEntry)
|
|
502
|
+
.map(entry => entry.id)
|
|
503
|
+
.filter(id => SAFE_DESKTOP_PROFILE_ID.test(id));
|
|
504
|
+
if (targetIds.length === 0) return { ok: true, changed: false, kind: "noop", libraryPath: inspected.libraryPath };
|
|
505
|
+
|
|
506
|
+
let metadataAfterPivot = metadata;
|
|
507
|
+
if (inspected.kind === "gateway_ours") {
|
|
508
|
+
const standardId = randomUUID();
|
|
509
|
+
const standardPath = profilePath(inspected.libraryPath, standardId);
|
|
510
|
+
atomicWriteFile(standardPath, "{}\n");
|
|
511
|
+
const standardEntry: Desktop3pMetadataEntry = { id: standardId, name: "opencodex-standard" };
|
|
512
|
+
metadataAfterPivot = { ...metadata, appliedId: standardId, entries: [...metadata.entries, standardEntry] };
|
|
513
|
+
atomicWriteFile(metadataPath, JSON.stringify(metadataAfterPivot, null, 2) + "\n");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const residualPaths: string[] = [];
|
|
517
|
+
for (const id of targetIds) {
|
|
518
|
+
for (const candidate of [profilePath(inspected.libraryPath, id), `${profilePath(inspected.libraryPath, id)}.bak`]) {
|
|
519
|
+
try {
|
|
520
|
+
if (existsSync(candidate)) (options.unlink ?? unlinkSync)(candidate);
|
|
521
|
+
} catch {
|
|
522
|
+
// Only the path is allowed to leave this credential-bearing cleanup boundary.
|
|
523
|
+
}
|
|
524
|
+
if (existsSync(candidate)) residualPaths.push(candidate);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const ownedResiduePaths = metadataAfterPivot.entries
|
|
528
|
+
.filter(entry => isOwnedDesktopGatewayEntry(entry) && !targetIds.includes(entry.id) && SAFE_DESKTOP_PROFILE_ID.test(entry.id))
|
|
529
|
+
.flatMap(entry => [profilePath(inspected.libraryPath, entry.id), `${profilePath(inspected.libraryPath, entry.id)}.bak`])
|
|
530
|
+
.filter(existsSync);
|
|
531
|
+
if (residualPaths.length > 0 || ownedResiduePaths.length > 0) {
|
|
532
|
+
return {
|
|
533
|
+
ok: false, changed: true, kind: "cleanup_incomplete", libraryPath: inspected.libraryPath,
|
|
534
|
+
residualPaths: [...new Set([...residualPaths, ...ownedResiduePaths])],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
// Do not leave a metadata row pointing at a deleted profile. For a foreign
|
|
538
|
+
// selection this only removes proven opencodex residues; appliedId is kept.
|
|
539
|
+
atomicWriteFile(
|
|
540
|
+
metadataPath,
|
|
541
|
+
JSON.stringify({ ...metadataAfterPivot, entries: metadataAfterPivot.entries.filter(entry => !targetIds.includes(entry.id)) }, null, 2) + "\n",
|
|
542
|
+
);
|
|
543
|
+
return { ok: true, changed: true, kind: "removed", libraryPath: inspected.libraryPath };
|
|
544
|
+
} catch {
|
|
545
|
+
return { ok: false, changed: false, kind: "write_failed", libraryPath: inspected.libraryPath };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
330
549
|
/** Write and apply the opencodex config in Claude Desktop 3P's config library. */
|
|
331
550
|
export function writeDesktop3pConfig(
|
|
332
551
|
port: number,
|
|
@@ -343,7 +562,8 @@ export function writeDesktop3pConfig(
|
|
|
343
562
|
try {
|
|
344
563
|
mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
|
|
345
564
|
const metadata = parseMetadata(metadataPath);
|
|
346
|
-
const
|
|
565
|
+
const selected = metadata.entries.find(entry => entry?.id === metadata.appliedId && isOwnedDesktopGatewayEntry(entry));
|
|
566
|
+
const existing = selected ?? metadata.entries.find(entry => isOwnedDesktopGatewayEntry(entry) && typeof entry.id === "string");
|
|
347
567
|
const id = existing?.id ?? randomUUID();
|
|
348
568
|
configPath = join(libraryPath, `${id}.json`);
|
|
349
569
|
const entry: Desktop3pMetadataEntry = existing ? { ...existing, id, name: "opencodex" } : { id, name: "opencodex" };
|
package/src/claude/outbound.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
TranslatorBudgetExceededError,
|
|
17
17
|
type TranslatorBudget,
|
|
18
18
|
} from "../lib/translator-budget";
|
|
19
|
+
import { sseFieldOffset, sseFieldValue } from "../lib/sse-decoder";
|
|
19
20
|
|
|
20
21
|
type Rec = Record<string, unknown>;
|
|
21
22
|
|
|
@@ -588,10 +589,16 @@ export function responsesSseToAnthropicSse(
|
|
|
588
589
|
while (lineStart <= rawFrame.length) {
|
|
589
590
|
const newline = rawFrame.indexOf("\n", lineStart);
|
|
590
591
|
const lineEnd = newline === -1 ? rawFrame.length : newline;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
592
|
+
// The space after the colon is optional in text/event-stream (#1170);
|
|
593
|
+
// compute the value offset the same way sseFieldValue does, without
|
|
594
|
+
// slicing the line first — the byte accounting below is keyed to
|
|
595
|
+
// offsets into rawFrame.
|
|
596
|
+
const eventOffset = sseFieldOffset(rawFrame, lineStart, lineEnd, "event");
|
|
597
|
+
const dataOffset = sseFieldOffset(rawFrame, lineStart, lineEnd, "data");
|
|
598
|
+
if (eventOffset !== -1) {
|
|
599
|
+
eventName = rawFrame.slice(eventOffset, lineEnd).trim();
|
|
600
|
+
} else if (dataOffset !== -1) {
|
|
601
|
+
const fragmentStart = dataOffset;
|
|
595
602
|
const fragmentBytes = utf8SliceBytes(rawFrame, fragmentStart, lineEnd);
|
|
596
603
|
const fragmentReservation = translatorBudget.reserveTransient(fragmentBytes, { kind: "live_transient" });
|
|
597
604
|
let fragmentCommitted = false;
|
|
@@ -861,8 +868,10 @@ export async function collectAnthropicMessage(
|
|
|
861
868
|
let eventName = "";
|
|
862
869
|
let dataLine = "";
|
|
863
870
|
for (const line of rawFrame.split("\n")) {
|
|
864
|
-
|
|
865
|
-
|
|
871
|
+
const eventValue = sseFieldValue(line, "event");
|
|
872
|
+
if (eventValue !== null) { eventName = eventValue.trim(); continue; }
|
|
873
|
+
const dataValue = sseFieldValue(line, "data");
|
|
874
|
+
if (dataValue !== null) dataLine += dataValue;
|
|
866
875
|
}
|
|
867
876
|
if (!eventName || !dataLine) continue;
|
|
868
877
|
let data: unknown;
|
package/src/cli/account-api.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface AccountRow {
|
|
|
21
21
|
masked?: string;
|
|
22
22
|
active: boolean;
|
|
23
23
|
needsReauth?: boolean;
|
|
24
|
+
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
25
|
+
priority?: number;
|
|
24
26
|
quota?: CodexQuotaDto | null;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -172,6 +174,7 @@ interface CodexAccountDto {
|
|
|
172
174
|
plan?: string;
|
|
173
175
|
isMain?: boolean;
|
|
174
176
|
needsReauth?: boolean;
|
|
177
|
+
priority?: number;
|
|
175
178
|
quota?: CodexQuotaDto | null;
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -219,6 +222,7 @@ export async function fetchCodexRows(
|
|
|
219
222
|
plan: a.plan,
|
|
220
223
|
active: a.id === activeId,
|
|
221
224
|
needsReauth: a.needsReauth,
|
|
225
|
+
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
222
226
|
...(forceRefresh ? { quota: projectQuota(a.quota) } : {}),
|
|
223
227
|
}));
|
|
224
228
|
return { rows, activeId, autoSwitchThreshold, status: 200 };
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from "../config";
|
|
2
|
+
import {
|
|
3
|
+
MAX_ACCOUNT_PRIORITY,
|
|
4
|
+
MIN_ACCOUNT_PRIORITY,
|
|
5
|
+
normalizeAccountPriority,
|
|
6
|
+
parseAccountPriority,
|
|
7
|
+
} from "../codex/pool-rotation";
|
|
2
8
|
import {
|
|
3
9
|
apiError,
|
|
4
10
|
apiJson,
|
|
@@ -18,6 +24,7 @@ const EXTENDED_USAGE = `Usage:
|
|
|
18
24
|
ocx account refresh <provider> [--json]
|
|
19
25
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
20
26
|
ocx account alias <provider> <id|main> <display-name|-> [--json]
|
|
27
|
+
ocx account priority <provider> <id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
21
28
|
ocx account remove <provider> <id|main> --yes [--json]
|
|
22
29
|
ocx account clear-cooldown <provider> <id|main> [--json]
|
|
23
30
|
ocx account add-key <provider> [--label <label>] [--json]`;
|
|
@@ -316,6 +323,111 @@ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promi
|
|
|
316
323
|
return 0;
|
|
317
324
|
}
|
|
318
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Named selection orders. The words convey sequence rather than rank because the
|
|
328
|
+
* pool moves down the list only when everything above it is drained — "high
|
|
329
|
+
* priority" would suggest the account gets more traffic, which is not what
|
|
330
|
+
* ordering does.
|
|
331
|
+
*/
|
|
332
|
+
const PRIORITY_PRESETS: Record<string, number> = {
|
|
333
|
+
first: 2,
|
|
334
|
+
earlier: 1,
|
|
335
|
+
normal: 0,
|
|
336
|
+
later: -1,
|
|
337
|
+
last: -2,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
function priorityPresetName(priority: number): string | null {
|
|
341
|
+
return Object.entries(PRIORITY_PRESETS).find(([, value]) => value === priority)?.[0] ?? null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function formatPriority(priority: number): string {
|
|
345
|
+
const preset = priorityPresetName(priority);
|
|
346
|
+
const signed = priority > 0 ? `+${priority}` : String(priority);
|
|
347
|
+
return preset ? `${signed} (${preset})` : signed;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** `null` = reset to the default; `undefined` = unparseable. */
|
|
351
|
+
function parsePriorityArgument(raw: string): number | null | undefined {
|
|
352
|
+
const word = raw.trim().toLowerCase();
|
|
353
|
+
if (word === "reset") return null;
|
|
354
|
+
// Own keys only: `in` also matches "constructor", "__proto__", and friends.
|
|
355
|
+
if (Object.hasOwn(PRIORITY_PRESETS, word)) return PRIORITY_PRESETS[word];
|
|
356
|
+
// The regex only rules out shapes Number() would coerce ("1e2", " 1 ", ""); the range
|
|
357
|
+
// itself comes from the core parser so the CLI cannot drift from what the API accepts.
|
|
358
|
+
if (!/^[+-]?\d+$/.test(word)) return undefined;
|
|
359
|
+
return parseAccountPriority(Number(word)) ?? undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function cmdPriority(args: string[], deps: AccountDeps): Promise<number> {
|
|
363
|
+
const wantsJson = flag(args, "--json");
|
|
364
|
+
const name = args.shift();
|
|
365
|
+
const requestedId = args.shift();
|
|
366
|
+
const requestedPriority = args.shift();
|
|
367
|
+
if (!name || !requestedId || args.length) return usage();
|
|
368
|
+
const classified = configAndType(deps, name);
|
|
369
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
370
|
+
if (classified.type !== "codex") {
|
|
371
|
+
return usage("Error: selection order only applies to the openai Codex account pool");
|
|
372
|
+
}
|
|
373
|
+
const id = requestedId === "main" ? MAIN_ID : requestedId;
|
|
374
|
+
|
|
375
|
+
// Validate before touching the network so a typo never reaches the proxy.
|
|
376
|
+
let priority: number | null | undefined;
|
|
377
|
+
if (requestedPriority !== undefined) {
|
|
378
|
+
priority = parsePriorityArgument(requestedPriority);
|
|
379
|
+
if (priority === undefined) {
|
|
380
|
+
return usage(`Error: selection order must be an integer ${MIN_ACCOUNT_PRIORITY}..${MAX_ACCOUNT_PRIORITY}, one of ${Object.keys(PRIORITY_PRESETS).join("/")}, or reset`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
385
|
+
if (!baseUrl) return proxyUnreachable();
|
|
386
|
+
|
|
387
|
+
// No value means "show" — a read must not rewrite what it is reporting.
|
|
388
|
+
if (priority === undefined) {
|
|
389
|
+
const result = await fetchCodexRows(deps, baseUrl);
|
|
390
|
+
const failed = familyFailure(result, `failed to read ${name} accounts`);
|
|
391
|
+
if (failed !== null) return failed;
|
|
392
|
+
const row = result.rows.find(candidate => candidate.id === id);
|
|
393
|
+
if (!row) return usage(`Error: no ${name} account ${requestedId}`);
|
|
394
|
+
const current = normalizeAccountPriority(row.priority);
|
|
395
|
+
if (wantsJson) {
|
|
396
|
+
console.log(JSON.stringify(
|
|
397
|
+
{ ok: true, provider: name, id, priority: current, preset: priorityPresetName(current) },
|
|
398
|
+
null,
|
|
399
|
+
2,
|
|
400
|
+
));
|
|
401
|
+
} else {
|
|
402
|
+
console.log(`${name}: ${requestedId} selection order is ${formatPriority(current)}`);
|
|
403
|
+
}
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/priority", { id, priority });
|
|
408
|
+
if (response.status === 0) return proxyUnreachable();
|
|
409
|
+
if (response.status !== 200) return apiError(response.json, `failed to set selection order for ${requestedId}`);
|
|
410
|
+
const applied = typeof response.json.priority === "number" ? response.json.priority : (priority ?? 0);
|
|
411
|
+
if (wantsJson) {
|
|
412
|
+
console.log(JSON.stringify(
|
|
413
|
+
{ ok: true, provider: name, id, priority: applied, preset: priorityPresetName(applied) },
|
|
414
|
+
null,
|
|
415
|
+
2,
|
|
416
|
+
));
|
|
417
|
+
} else {
|
|
418
|
+
console.log(`${name}: ${requestedId} selection order is now ${formatPriority(applied)}`);
|
|
419
|
+
}
|
|
420
|
+
// Not cmdUse's note: re-ordering takes effect on the next unbound request rather than
|
|
421
|
+
// only on new sessions, because preemption moves those requests up immediately.
|
|
422
|
+
console.error("Takes effect from the next unbound request; running threads keep their current account until drained.");
|
|
423
|
+
// The release is not optional and not conditional on the value changing, so it has to be
|
|
424
|
+
// stated: this route is the only way to clear a pin without immediately setting another,
|
|
425
|
+
// which means a write storing the order an account already had still releases it. Without
|
|
426
|
+
// this line that is a silent side effect of a command that looks purely declarative.
|
|
427
|
+
console.error('Also releases any manual "use this account now" pin, on any account.');
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
319
431
|
export async function cmdAlias(args: string[], deps: AccountDeps): Promise<number> {
|
|
320
432
|
const wantsJson = flag(args, "--json");
|
|
321
433
|
const name = args.shift();
|
package/src/cli/account.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { loadConfig } from "../config";
|
|
3
3
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
4
4
|
import type { OcxConfig } from "../types";
|
|
5
|
-
import { cmdAddKey, cmdAlias, cmdAutoSwitch, cmdClearCooldown, cmdRefresh, cmdRemove } from "./account-extended";
|
|
5
|
+
import { cmdAddKey, cmdAlias, cmdAutoSwitch, cmdClearCooldown, cmdPriority, cmdRefresh, cmdRemove } from "./account-extended";
|
|
6
6
|
import { apiError, apiJson, classifyAccount, fetchRows, proxyUnreachable, resolveBaseUrl, type AccountDeps, type AccountRow, type AccountType, type ApiResult }
|
|
7
7
|
from "./account-api";
|
|
8
8
|
|
|
@@ -22,6 +22,7 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
22
22
|
ocx account refresh <provider> [--json]
|
|
23
23
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
24
24
|
ocx account alias <provider> <account-or-key-id> <display-name|-> [--json]
|
|
25
|
+
ocx account priority <provider> <account-id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
25
26
|
ocx account remove <provider> <account-or-key-id|main> --yes [--json]
|
|
26
27
|
ocx account clear-cooldown <provider> <account-id|main> [--json]
|
|
27
28
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
@@ -67,11 +68,24 @@ function statusText(row: AccountRow): string {
|
|
|
67
68
|
return parts.join(" ");
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Signed so the sort direction reads off the column; "-" where ordering does not apply. */
|
|
72
|
+
function priorityText(row: AccountRow): string {
|
|
73
|
+
if (row.priority === undefined) return "-";
|
|
74
|
+
return row.priority > 0 ? `+${row.priority}` : String(row.priority);
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
export function formatAccountTable(rows: AccountRow[]): string {
|
|
71
|
-
const header = ["PROVIDER", "TYPE", "ID", "PLAN/LABEL", "STATUS"];
|
|
78
|
+
const header = ["PROVIDER", "TYPE", "ID", "PLAN/LABEL", "PRIORITY", "STATUS"];
|
|
72
79
|
const data = rows.map(r => {
|
|
73
80
|
const keyLabel = r.masked && r.label !== r.masked ? `${r.masked} (${r.label})` : r.masked;
|
|
74
|
-
return [
|
|
81
|
+
return [
|
|
82
|
+
r.provider,
|
|
83
|
+
r.type,
|
|
84
|
+
displayId(r.id),
|
|
85
|
+
r.type === "api-key" ? keyLabel ?? "-" : r.label ?? "-",
|
|
86
|
+
priorityText(r),
|
|
87
|
+
statusText(r),
|
|
88
|
+
];
|
|
75
89
|
});
|
|
76
90
|
const widths = header.map((h, i) => Math.max(h.length, ...data.map(d => d[i]!.length)));
|
|
77
91
|
const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
|
|
@@ -244,9 +258,11 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
244
258
|
if (wantsJson) console.log(JSON.stringify({ ok: true, provider: name, type: c.type, activeId }, null, 2));
|
|
245
259
|
else console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
|
|
246
260
|
if (c.type === "codex") {
|
|
247
|
-
console.error("
|
|
248
|
-
|
|
249
|
-
|
|
261
|
+
console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
|
|
262
|
+
const active = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/active");
|
|
263
|
+
if (active.status === 200 && typeof active.json.autoSwitchThreshold === "number" && active.json.autoSwitchThreshold > 0) {
|
|
264
|
+
console.error(`Note: auto-switch (threshold ${active.json.autoSwitchThreshold}%) may override this pin.`);
|
|
265
|
+
}
|
|
250
266
|
}
|
|
251
267
|
return 0;
|
|
252
268
|
}
|
|
@@ -260,6 +276,7 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
|
|
|
260
276
|
if (sub === "refresh") return await cmdRefresh(rest, deps);
|
|
261
277
|
if (sub === "auto-switch") return await cmdAutoSwitch(rest, deps);
|
|
262
278
|
if (sub === "alias" || sub === "rename") return await cmdAlias(rest, deps);
|
|
279
|
+
if (sub === "priority") return await cmdPriority(rest, deps);
|
|
263
280
|
if (sub === "remove") return await cmdRemove(rest, deps);
|
|
264
281
|
if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
|
|
265
282
|
if (sub === "add-key") return await cmdAddKey(rest, deps);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { loadConfig, saveConfigPreservingClaudeCode } from "../config";
|
|
4
|
+
import { setIntegrationEnabled } from "../codex/desired-state";
|
|
4
5
|
import {
|
|
5
6
|
DESKTOP_FAMILIES,
|
|
6
7
|
moveDesktopRoute,
|
|
@@ -41,7 +42,11 @@ export async function applyProfile(
|
|
|
41
42
|
profile: DesktopProfile,
|
|
42
43
|
mode: Desktop3pConfigMode,
|
|
43
44
|
deps: ApplyProfileDeps = {},
|
|
44
|
-
): Promise<{ ok: boolean; path: string; reason?: string }> {
|
|
45
|
+
): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
|
|
46
|
+
// Explicit apply is an enable action. Persist intent before any Desktop write
|
|
47
|
+
// so a process crash cannot leave a gateway profile that startup immediately removes.
|
|
48
|
+
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
49
|
+
if (!desired.ok) return { ok: false, path: "", reason: desired.message };
|
|
45
50
|
const config = loadConfig();
|
|
46
51
|
const state = await buildClaudeDesktopState(config, profile);
|
|
47
52
|
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
|
|
@@ -52,7 +57,7 @@ export async function applyProfile(
|
|
|
52
57
|
// serving process installs the map there; a local-only write leaves the
|
|
53
58
|
// daemon unable to decode aliases, and the provider rejects them (400).
|
|
54
59
|
const post = deps.postApplyImpl ?? (async (m: Desktop3pConfigMode, p: DesktopProfile) =>
|
|
55
|
-
runtimeRequest<{ ok?: boolean; path?: string; error?: string }>(
|
|
60
|
+
runtimeRequest<{ ok?: boolean; path?: string; error?: string; saved?: boolean; warning?: string }>(
|
|
56
61
|
"/api/claude-desktop/apply",
|
|
57
62
|
// The daemon's config may be older than what we just saved, so the
|
|
58
63
|
// profile travels with the request instead of being re-read there.
|
|
@@ -61,12 +66,26 @@ export async function applyProfile(
|
|
|
61
66
|
try {
|
|
62
67
|
const applied = await post(mode, state.profile);
|
|
63
68
|
if (applied.ok === false) return { ok: false, path: applied.path ?? "", reason: applied.error ?? "daemon apply failed" };
|
|
64
|
-
|
|
69
|
+
// Partial success: Desktop was written but the applied marker was not
|
|
70
|
+
// persisted. Pass the degradation up instead of reporting a clean apply.
|
|
71
|
+
const partial = (applied as { saved?: boolean; warning?: string }).saved === false;
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
path: applied.path ?? "",
|
|
75
|
+
...(partial ? { warning: (applied as { warning?: string }).warning ?? "applied marker was not saved" } : {}),
|
|
76
|
+
};
|
|
65
77
|
} catch (error) {
|
|
66
78
|
return { ok: false, path: "", reason: error instanceof Error ? error.message : String(error) };
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
const allModels = await fetchAllModels(config);
|
|
82
|
+
// The toggle can persist OFF while fetchAllModels was awaiting (same race the
|
|
83
|
+
// management writers fence). Re-read persisted intent immediately before the
|
|
84
|
+
// writer; a lost race is a discriminated skip, not a write.
|
|
85
|
+
const { claudeDesktopIntegrationEnabledNow } = await import("../codex/desired-state");
|
|
86
|
+
if (!claudeDesktopIntegrationEnabledNow()) {
|
|
87
|
+
return { ok: false, path: "", reason: "desired_state_changed" };
|
|
88
|
+
}
|
|
70
89
|
const routed = filterCatalogVisibleModels(allModels, config).map(model => ({
|
|
71
90
|
provider: model.provider,
|
|
72
91
|
id: model.id,
|
|
@@ -111,6 +130,9 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
111
130
|
return 1;
|
|
112
131
|
}
|
|
113
132
|
console.log(`Claude Desktop 설정을 적용했습니다: ${result.path}`);
|
|
133
|
+
// The write landed; only the bookkeeping marker did not. Saying nothing
|
|
134
|
+
// would leave the saved-vs-applied display wrong with no explanation.
|
|
135
|
+
if (result.warning) console.warn(`⚠️ ${result.warning}`);
|
|
114
136
|
console.log("Claude Desktop을 완전히 종료한 뒤 다시 열어 주세요.");
|
|
115
137
|
return 0;
|
|
116
138
|
} catch (error) {
|
|
@@ -175,6 +197,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
175
197
|
if (flags.includes("--apply")) {
|
|
176
198
|
const result = await applyProfile(reconciled, "static", deps);
|
|
177
199
|
if (!result.ok) { console.error(`프로필은 저장했지만 Desktop 적용에 실패했습니다: ${result.reason ?? "unknown error"}`); return 1; }
|
|
200
|
+
if (result.warning) console.warn(`⚠️ ${result.warning}`);
|
|
178
201
|
}
|
|
179
202
|
console.log("Claude Desktop 프로필을 가져왔습니다.");
|
|
180
203
|
return 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { clearCodexAccountPin } from "../codex/account-priority";
|
|
2
3
|
import { getConfigPath, readConfigDiagnostics, saveConfig, validateConfigCandidate } from "../config";
|
|
3
4
|
import type { OcxConfig } from "../types";
|
|
4
5
|
import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
|
|
@@ -102,6 +103,14 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
102
103
|
setPath(candidate, path, raw === undefined ? undefined : parseValue(raw), action === "unset");
|
|
103
104
|
const config = validate(candidate);
|
|
104
105
|
const savedValue = action === "unset" ? null : getPath(config, path);
|
|
106
|
+
// Setting the order here is the operator restating it, exactly as through
|
|
107
|
+
// `ocx account priority` or the management route, so it releases the manual pin
|
|
108
|
+
// for the same reason those do: a pin made before any order existed would
|
|
109
|
+
// otherwise outrank every order set afterwards, capping the pool at the pinned
|
|
110
|
+
// account's tier with nothing on any surface explaining why. `import` is
|
|
111
|
+
// deliberately not covered — that file supplies its own pin, so there is no
|
|
112
|
+
// stale one to release.
|
|
113
|
+
if (pathSegments(path)[0] === "codexAccountPriorities") clearCodexAccountPin(config);
|
|
105
114
|
saveConfig(config);
|
|
106
115
|
printData({ ok: true, path, value: redact(savedValue, path.split(".").at(-1)) }, wantsJson,
|
|
107
116
|
[`${action === "unset" ? "Unset" : "Set"} ${path}.`]);
|