@bitkyc08/opencodex 2.5.6 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +17 -6
- package/README.md +19 -7
- package/README.zh-CN.md +12 -3
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +88 -2
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BS4X1QDi.js +9 -0
- package/gui/dist/assets/{index-CKqUwc02.css → index-BwvDb198.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +20 -6
- package/src/adapters/anthropic.ts +16 -5
- package/src/adapters/google.ts +9 -2
- package/src/adapters/openai-chat.ts +13 -5
- package/src/bun-runtime.ts +22 -1
- package/src/cli-help.ts +111 -0
- package/src/cli-status.ts +164 -0
- package/src/cli.ts +77 -186
- package/src/codex-account-store.ts +47 -8
- package/src/codex-auth-api.ts +111 -54
- package/src/codex-auth-collision.ts +5 -0
- package/src/codex-catalog.ts +24 -12
- package/src/codex-history-provider.ts +29 -13
- package/src/codex-inject.ts +46 -29
- package/src/codex-journal.ts +77 -13
- package/src/codex-quota.ts +11 -3
- package/src/codex-routing.ts +14 -4
- package/src/codex-shim.ts +71 -24
- package/src/codex-websocket-registry.ts +20 -4
- package/src/config.ts +138 -4
- package/src/init.ts +7 -2
- package/src/oauth/callback-server.ts +22 -15
- package/src/oauth/index.ts +18 -4
- package/src/oauth/login-cli.ts +8 -1
- package/src/oauth/store.ts +2 -1
- package/src/process-control.ts +36 -0
- package/src/provider-label.ts +8 -0
- package/src/responses/parser.ts +18 -1
- package/src/router.ts +61 -5
- package/src/server.ts +878 -94
- package/src/service-secrets.ts +6 -0
- package/src/service.ts +293 -28
- package/src/types.ts +26 -1
- package/src/update.ts +16 -9
- package/src/usage-debug.ts +65 -0
- package/src/usage-log.ts +62 -0
- package/src/usage-summary.ts +0 -0
- package/src/ws-bridge.ts +2 -2
- package/gui/README.md +0 -73
- package/gui/dist/assets/index-CSUvRNAX.js +0 -9
package/src/codex-journal.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { atomicWriteFile } from "./config";
|
|
@@ -9,11 +10,26 @@ interface Journal {
|
|
|
9
10
|
version: 1;
|
|
10
11
|
originalConfig: string;
|
|
11
12
|
originalProfile: string | null;
|
|
13
|
+
injectedConfigHash?: string;
|
|
14
|
+
injectedProfileHash?: string | null;
|
|
12
15
|
pid: number;
|
|
13
16
|
timestamp: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
interface RestoreJournalResult {
|
|
20
|
+
configRestored: boolean;
|
|
21
|
+
profileRestored: boolean;
|
|
22
|
+
configChanged: boolean;
|
|
23
|
+
profileChanged: boolean;
|
|
24
|
+
complete: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sha256(content: string | null): string | null {
|
|
28
|
+
return content === null ? null : createHash("sha256").update(content).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export function writeJournal(): void {
|
|
32
|
+
if (existsSync(JOURNAL_PATH) && readJournal()) return;
|
|
17
33
|
if (!existsSync(CODEX_CONFIG_PATH)) return;
|
|
18
34
|
const config = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
19
35
|
const profile = existsSync(CODEX_PROFILE_PATH)
|
|
@@ -29,20 +45,73 @@ export function writeJournal(): void {
|
|
|
29
45
|
atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
|
|
30
46
|
}
|
|
31
47
|
|
|
48
|
+
export function markJournalInjectedState(config: string, profile: string | null): void {
|
|
49
|
+
const journal = readJournal();
|
|
50
|
+
if (!journal) return;
|
|
51
|
+
if (journal.injectedConfigHash) return;
|
|
52
|
+
journal.injectedConfigHash = sha256(config) ?? undefined;
|
|
53
|
+
journal.injectedProfileHash = sha256(profile);
|
|
54
|
+
atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
export function removeJournal(): void {
|
|
33
58
|
try { unlinkSync(JOURNAL_PATH); } catch { /* ignore */ }
|
|
34
59
|
}
|
|
35
60
|
|
|
36
|
-
|
|
37
|
-
if (!existsSync(JOURNAL_PATH)) return
|
|
38
|
-
let journal: Journal;
|
|
61
|
+
function readJournal(): Journal | null {
|
|
62
|
+
if (!existsSync(JOURNAL_PATH)) return null;
|
|
39
63
|
try {
|
|
40
|
-
journal = JSON.parse(readFileSync(JOURNAL_PATH, "utf-8"));
|
|
64
|
+
const journal = JSON.parse(readFileSync(JOURNAL_PATH, "utf-8")) as Journal;
|
|
41
65
|
if (journal.version !== 1) throw new Error("unknown version");
|
|
66
|
+
return journal;
|
|
42
67
|
} catch {
|
|
43
68
|
removeJournal();
|
|
44
|
-
return
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function restoreJournalState(): RestoreJournalResult {
|
|
74
|
+
const journal = readJournal();
|
|
75
|
+
if (!journal) {
|
|
76
|
+
return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false, complete: false };
|
|
45
77
|
}
|
|
78
|
+
const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") : "";
|
|
79
|
+
const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
|
|
80
|
+
const configUnchanged = !journal.injectedConfigHash || sha256(currentConfig) === journal.injectedConfigHash;
|
|
81
|
+
const profileUnchanged = journal.injectedProfileHash === undefined || sha256(currentProfile) === (journal.injectedProfileHash ?? null);
|
|
82
|
+
|
|
83
|
+
let configRestored = false;
|
|
84
|
+
let profileRestored = false;
|
|
85
|
+
if (configUnchanged) {
|
|
86
|
+
atomicWriteFile(CODEX_CONFIG_PATH, Buffer.from(journal.originalConfig, "base64").toString("utf-8"));
|
|
87
|
+
configRestored = true;
|
|
88
|
+
}
|
|
89
|
+
if (profileUnchanged) {
|
|
90
|
+
if (journal.originalProfile !== null) {
|
|
91
|
+
atomicWriteFile(CODEX_PROFILE_PATH, Buffer.from(journal.originalProfile, "base64").toString("utf-8"));
|
|
92
|
+
} else if (existsSync(CODEX_PROFILE_PATH)) {
|
|
93
|
+
try { unlinkSync(CODEX_PROFILE_PATH); } catch { /* ignore */ }
|
|
94
|
+
}
|
|
95
|
+
profileRestored = true;
|
|
96
|
+
}
|
|
97
|
+
const complete = configRestored && profileRestored;
|
|
98
|
+
if (complete) removeJournal();
|
|
99
|
+
return {
|
|
100
|
+
configRestored,
|
|
101
|
+
profileRestored,
|
|
102
|
+
configChanged: !configUnchanged,
|
|
103
|
+
profileChanged: !profileUnchanged,
|
|
104
|
+
complete,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function restoreJournal(): boolean {
|
|
109
|
+
return restoreJournalState().complete;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function reconcileJournal(): boolean {
|
|
113
|
+
const journal = readJournal();
|
|
114
|
+
if (!journal) return false;
|
|
46
115
|
try {
|
|
47
116
|
process.kill(journal.pid, 0);
|
|
48
117
|
return false;
|
|
@@ -51,13 +120,8 @@ export function reconcileJournal(): boolean {
|
|
|
51
120
|
return false;
|
|
52
121
|
}
|
|
53
122
|
}
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
} else if (existsSync(CODEX_PROFILE_PATH)) {
|
|
58
|
-
try { unlinkSync(CODEX_PROFILE_PATH); } catch { /* ignore */ }
|
|
59
|
-
}
|
|
60
|
-
removeJournal();
|
|
61
|
-
console.error(`⚠️ Previous session (PID ${journal.pid}) did not shut down cleanly. Codex config restored from journal.`);
|
|
123
|
+
const restored = restoreJournalState();
|
|
124
|
+
if (!restored.configRestored && !restored.profileRestored) return false;
|
|
125
|
+
console.error(`⚠️ Previous session (PID ${journal.pid}) did not shut down cleanly. Codex state restored from journal.`);
|
|
62
126
|
return true;
|
|
63
127
|
}
|
package/src/codex-quota.ts
CHANGED
|
@@ -121,21 +121,29 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const quota: Omit<StoredAccountQuota, "updatedAt"> = {};
|
|
124
|
+
const thirtyDayOnly = data.plan_type?.trim().toLowerCase() === "go" || data.plan_type?.trim().toLowerCase() === "free";
|
|
124
125
|
const weeklyPercent = normalizeUsagePercent(data.rate_limit.secondary_window?.used_percent);
|
|
125
126
|
const fiveHourPercent = normalizeUsagePercent(data.rate_limit.primary_window?.used_percent);
|
|
126
127
|
const monthlyPercent = normalizeUsagePercent(data.rate_limit.tertiary_window?.used_percent);
|
|
127
128
|
const weeklyResetAt = normalizeResetAt(data.rate_limit.secondary_window?.reset_at);
|
|
128
129
|
const fiveHourResetAt = normalizeResetAt(data.rate_limit.primary_window?.reset_at);
|
|
129
130
|
const monthlyResetAt = normalizeResetAt(data.rate_limit.tertiary_window?.reset_at);
|
|
130
|
-
if (
|
|
131
|
+
if (thirtyDayOnly) {
|
|
132
|
+
const goMonthlyPercent = monthlyPercent ?? fiveHourPercent;
|
|
133
|
+
const goMonthlyResetAt = monthlyResetAt ?? fiveHourResetAt;
|
|
134
|
+
if (goMonthlyPercent !== undefined) {
|
|
135
|
+
quota.monthlyPercent = goMonthlyPercent;
|
|
136
|
+
if (goMonthlyResetAt !== undefined) quota.monthlyResetAt = goMonthlyResetAt;
|
|
137
|
+
}
|
|
138
|
+
} else if (weeklyPercent !== undefined) {
|
|
131
139
|
quota.weeklyPercent = weeklyPercent;
|
|
132
140
|
if (weeklyResetAt !== undefined) quota.weeklyResetAt = weeklyResetAt;
|
|
133
141
|
}
|
|
134
|
-
if (fiveHourPercent !== undefined) {
|
|
142
|
+
if (!thirtyDayOnly && fiveHourPercent !== undefined) {
|
|
135
143
|
quota.fiveHourPercent = fiveHourPercent;
|
|
136
144
|
if (fiveHourResetAt !== undefined) quota.fiveHourResetAt = fiveHourResetAt;
|
|
137
145
|
}
|
|
138
|
-
if (monthlyPercent !== undefined) {
|
|
146
|
+
if (!thirtyDayOnly && monthlyPercent !== undefined) {
|
|
139
147
|
quota.monthlyPercent = monthlyPercent;
|
|
140
148
|
if (monthlyResetAt !== undefined) quota.monthlyResetAt = monthlyResetAt;
|
|
141
149
|
}
|
package/src/codex-routing.ts
CHANGED
|
@@ -74,8 +74,14 @@ export function computeCodexUsageScore(quota: {
|
|
|
74
74
|
weeklyPercent?: number;
|
|
75
75
|
fiveHourPercent?: number;
|
|
76
76
|
monthlyPercent?: number;
|
|
77
|
-
} | null): number {
|
|
77
|
+
} | null, plan?: string | null): number {
|
|
78
78
|
if (!quota) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
79
|
+
const normalizedPlan = plan?.trim().toLowerCase();
|
|
80
|
+
if (normalizedPlan === "go" || normalizedPlan === "free") {
|
|
81
|
+
return typeof quota.monthlyPercent === "number" && Number.isFinite(quota.monthlyPercent)
|
|
82
|
+
? quota.monthlyPercent
|
|
83
|
+
: CODEX_UNKNOWN_USAGE_SCORE;
|
|
84
|
+
}
|
|
79
85
|
const values = [quota.weeklyPercent, quota.fiveHourPercent, quota.monthlyPercent]
|
|
80
86
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
81
87
|
return values.length > 0 ? Math.max(...values) : CODEX_UNKNOWN_USAGE_SCORE;
|
|
@@ -204,11 +210,15 @@ function getEligiblePoolAccounts(config: OcxConfig, excludeId?: string, now = Da
|
|
|
204
210
|
.map(account => account.id);
|
|
205
211
|
}
|
|
206
212
|
|
|
213
|
+
function getPoolAccountPlan(config: OcxConfig, accountId: string): string | undefined {
|
|
214
|
+
return (config.codexAccounts ?? []).find(account => !account.isMain && account.id === accountId)?.plan;
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
function pickLowerUsageAccount(config: OcxConfig, active: string, activeUsage: number, now: number): string {
|
|
208
218
|
let best = active;
|
|
209
219
|
let bestUsage = activeUsage;
|
|
210
220
|
for (const id of getEligiblePoolAccounts(config, active, now)) {
|
|
211
|
-
const usage = computeCodexUsageScore(getAccountQuota(id));
|
|
221
|
+
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
212
222
|
if (usage < bestUsage) {
|
|
213
223
|
best = id;
|
|
214
224
|
bestUsage = usage;
|
|
@@ -221,7 +231,7 @@ export function pickLowestUsageCodexAccount(config: OcxConfig, excludeId?: strin
|
|
|
221
231
|
let best: string | null = null;
|
|
222
232
|
let bestUsage = Number.POSITIVE_INFINITY;
|
|
223
233
|
for (const id of getEligiblePoolAccounts(config, excludeId, now)) {
|
|
224
|
-
const usage = computeCodexUsageScore(getAccountQuota(id));
|
|
234
|
+
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
225
235
|
if (usage < bestUsage) {
|
|
226
236
|
best = id;
|
|
227
237
|
bestUsage = usage;
|
|
@@ -240,7 +250,7 @@ function applyQuotaAutoSwitch(config: OcxConfig, active: string, now: number): s
|
|
|
240
250
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
241
251
|
if (threshold <= 0) return active;
|
|
242
252
|
const quota = getAccountQuota(active);
|
|
243
|
-
const activeUsage = computeCodexUsageScore(quota);
|
|
253
|
+
const activeUsage = computeCodexUsageScore(quota, getPoolAccountPlan(config, active));
|
|
244
254
|
if (activeUsage < threshold) return active;
|
|
245
255
|
const best = pickLowerUsageAccount(config, active, activeUsage, now);
|
|
246
256
|
if (best !== active) setActiveCodexAccount(config, best);
|
package/src/codex-shim.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { delimiter, dirname, extname, join } from "node:path";
|
|
|
2
2
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { getConfigDir } from "./config";
|
|
4
4
|
import { durableBunPath } from "./bun-runtime";
|
|
5
|
+
import { serviceApiTokenFilePath } from "./service-secrets";
|
|
5
6
|
|
|
6
7
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
8
|
+
let lastShimDiscoveryError: string | null = null;
|
|
7
9
|
const CODEX_INTERNAL_COMMANDS = [
|
|
8
10
|
"app-server",
|
|
9
11
|
"archive",
|
|
@@ -13,7 +15,6 @@ const CODEX_INTERNAL_COMMANDS = [
|
|
|
13
15
|
"debug",
|
|
14
16
|
"delete",
|
|
15
17
|
"doctor",
|
|
16
|
-
"exec",
|
|
17
18
|
"exec-server",
|
|
18
19
|
"features",
|
|
19
20
|
"fork",
|
|
@@ -22,8 +23,6 @@ const CODEX_INTERNAL_COMMANDS = [
|
|
|
22
23
|
"logout",
|
|
23
24
|
"mcp",
|
|
24
25
|
"plugin",
|
|
25
|
-
"resume",
|
|
26
|
-
"review",
|
|
27
26
|
"sandbox",
|
|
28
27
|
"unarchive",
|
|
29
28
|
"update",
|
|
@@ -41,6 +40,8 @@ interface ShimFileState {
|
|
|
41
40
|
wrapperPath: string;
|
|
42
41
|
originalPath: string;
|
|
43
42
|
backupPath: string;
|
|
43
|
+
realPath?: string;
|
|
44
|
+
preserveOnly?: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function cliEntry(): { bun: string; cli: string } {
|
|
@@ -79,7 +80,20 @@ function findCodexOnPath(): string | null {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
83
|
+
lastShimDiscoveryError = null;
|
|
82
84
|
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
85
|
+
const exe = join(dir, "codex.exe");
|
|
86
|
+
if (existsSync(exe) && !isShim(exe)) {
|
|
87
|
+
try {
|
|
88
|
+
if (!lstatSync(exe).isDirectory()) {
|
|
89
|
+
lastShimDiscoveryError =
|
|
90
|
+
`Found codex.exe at ${exe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
|
|
91
|
+
"install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart.";
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
} catch { /* keep scanning */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
const cmd = join(dir, "codex.cmd");
|
|
84
98
|
const ps1 = join(dir, "codex.ps1");
|
|
85
99
|
const targets: ShimFileState[] = [];
|
|
@@ -92,15 +106,6 @@ function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
|
92
106
|
} catch { /* keep scanning */ }
|
|
93
107
|
}
|
|
94
108
|
if (targets.length > 0) return targets;
|
|
95
|
-
|
|
96
|
-
const exe = join(dir, "codex.exe");
|
|
97
|
-
if (!existsSync(exe) || isShim(exe)) continue;
|
|
98
|
-
try {
|
|
99
|
-
if (lstatSync(exe).isDirectory()) continue;
|
|
100
|
-
const wrapperPath = join(dir, "codex.cmd");
|
|
101
|
-
if (existsSync(wrapperPath) && !isShim(wrapperPath)) continue;
|
|
102
|
-
return [{ wrapperPath, originalPath: exe, backupPath: backupPathFor(exe) }];
|
|
103
|
-
} catch { /* keep scanning */ }
|
|
104
109
|
}
|
|
105
110
|
return null;
|
|
106
111
|
}
|
|
@@ -110,36 +115,62 @@ function backupPathFor(path: string): string {
|
|
|
110
115
|
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
function shQuote(value: string): string {
|
|
119
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
114
123
|
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
124
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
115
125
|
return `#!/usr/bin/env sh
|
|
116
126
|
# ${SHIM_MARKER}
|
|
127
|
+
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
128
|
+
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
129
|
+
export OPENCODEX_API_AUTH_TOKEN
|
|
130
|
+
fi
|
|
117
131
|
case "$1" in
|
|
118
132
|
${internalCommands}|--help|-h|--version|-V)
|
|
119
133
|
;;
|
|
120
134
|
*)
|
|
121
135
|
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
122
|
-
|
|
136
|
+
${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
123
137
|
fi
|
|
124
138
|
;;
|
|
125
139
|
esac
|
|
126
|
-
exec
|
|
140
|
+
exec ${shQuote(realCodexPath)} "$@"
|
|
127
141
|
`;
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
function windowsBatchValue(value: string): string {
|
|
145
|
+
return value
|
|
146
|
+
.replace(/%/g, "%%")
|
|
147
|
+
.replace(/\^/g, "^^")
|
|
148
|
+
.replace(/"/g, "")
|
|
149
|
+
.replace(/[\r\n]/g, "");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function windowsBatchSet(name: string, value: string): string {
|
|
153
|
+
return `set "${name}=${windowsBatchValue(value)}"`;
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
131
157
|
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
132
158
|
return `@echo off\r
|
|
133
159
|
rem ${SHIM_MARKER}\r
|
|
160
|
+
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
|
|
161
|
+
${windowsBatchSet("OCX_BUN", bunPath)}\r
|
|
162
|
+
${windowsBatchSet("OCX_CLI", cliPath)}\r
|
|
163
|
+
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
|
|
164
|
+
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
|
|
134
165
|
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
135
166
|
${internalCommandChecks}\r
|
|
136
167
|
if /I "%~1"=="--help" goto run_codex\r
|
|
137
168
|
if /I "%~1"=="-h" goto run_codex\r
|
|
138
169
|
if /I "%~1"=="--version" goto run_codex\r
|
|
139
170
|
if /I "%~1"=="-V" goto run_codex\r
|
|
140
|
-
"
|
|
171
|
+
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
141
172
|
:run_codex\r
|
|
142
|
-
"
|
|
173
|
+
"%OCX_REAL_CODEX%" %*\r
|
|
143
174
|
`;
|
|
144
175
|
}
|
|
145
176
|
|
|
@@ -149,8 +180,12 @@ function psString(value: string): string {
|
|
|
149
180
|
|
|
150
181
|
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
151
182
|
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
183
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
152
184
|
return `#!/usr/bin/env pwsh
|
|
153
185
|
# ${SHIM_MARKER}
|
|
186
|
+
if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
|
|
187
|
+
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
|
|
188
|
+
}
|
|
154
189
|
$internalCommands = @(${internalCommands})
|
|
155
190
|
$firstArg = if ($args.Count -gt 0) { [string]$args[0] } else { "" }
|
|
156
191
|
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $firstArg -or @("--help", "-h", "--version", "-V") -contains $firstArg
|
|
@@ -218,19 +253,26 @@ function replaceOwnedBackup(sourcePath: string, backupPath: string): void {
|
|
|
218
253
|
}
|
|
219
254
|
|
|
220
255
|
function refreshShimFile(file: ShimFileState): boolean {
|
|
256
|
+
if (file.preserveOnly) {
|
|
257
|
+
if (existsSync(file.originalPath) && !isShim(file.originalPath)) {
|
|
258
|
+
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
221
263
|
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
222
264
|
if (file.wrapperPath !== file.originalPath) return false;
|
|
223
265
|
replaceOwnedBackup(file.wrapperPath, file.backupPath);
|
|
224
|
-
writeShim(file.wrapperPath, file.backupPath);
|
|
266
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
225
267
|
return true;
|
|
226
268
|
}
|
|
227
269
|
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
228
|
-
writeShim(file.wrapperPath, file.backupPath);
|
|
270
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
229
271
|
return true;
|
|
230
272
|
}
|
|
231
273
|
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
232
274
|
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
233
|
-
writeShim(file.wrapperPath, file.backupPath);
|
|
275
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
234
276
|
return true;
|
|
235
277
|
}
|
|
236
278
|
return false;
|
|
@@ -242,7 +284,11 @@ export function installCodexShim(): { installed: boolean; message: string } {
|
|
|
242
284
|
const files = stateFiles(existing);
|
|
243
285
|
let refreshed = false;
|
|
244
286
|
for (const file of files) refreshed = refreshShimFile(file) || refreshed;
|
|
245
|
-
const allInstalled = files.every(file =>
|
|
287
|
+
const allInstalled = files.every(file => file.preserveOnly
|
|
288
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
289
|
+
: existsSync(file.wrapperPath)
|
|
290
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
291
|
+
&& isShim(file.wrapperPath));
|
|
246
292
|
if (refreshed || allInstalled) {
|
|
247
293
|
writeState(primaryState(files));
|
|
248
294
|
if (refreshed) {
|
|
@@ -258,20 +304,20 @@ export function installCodexShim(): { installed: boolean; message: string } {
|
|
|
258
304
|
}
|
|
259
305
|
}
|
|
260
306
|
|
|
261
|
-
const targets = process.platform === "win32"
|
|
307
|
+
const targets: ShimFileState[] | null = process.platform === "win32"
|
|
262
308
|
? findWindowsCodexTargets()
|
|
263
309
|
: (() => {
|
|
264
310
|
const originalPath = findCodexOnPath();
|
|
265
311
|
return originalPath ? [{ wrapperPath: originalPath, originalPath, backupPath: backupPathFor(originalPath) }] : null;
|
|
266
312
|
})();
|
|
267
|
-
if (!targets) return { installed: false, message: "Could not find a codex executable on PATH." };
|
|
313
|
+
if (!targets) return { installed: false, message: lastShimDiscoveryError ?? "Could not find a codex executable on PATH." };
|
|
268
314
|
|
|
269
315
|
for (const target of targets) {
|
|
270
316
|
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
271
317
|
}
|
|
272
318
|
for (const target of targets) {
|
|
273
|
-
renameSync(target.originalPath, target.backupPath);
|
|
274
|
-
writeShim(target.wrapperPath, target.backupPath);
|
|
319
|
+
if (existsSync(target.originalPath)) renameSync(target.originalPath, target.backupPath);
|
|
320
|
+
if (!target.preserveOnly) writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
|
|
275
321
|
}
|
|
276
322
|
writeState(primaryState(targets));
|
|
277
323
|
return {
|
|
@@ -285,6 +331,7 @@ export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
|
285
331
|
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
|
|
286
332
|
const files = stateFiles(state);
|
|
287
333
|
for (const file of files) {
|
|
334
|
+
if (file.preserveOnly) continue;
|
|
288
335
|
if (existsSync(file.wrapperPath) && isShim(file.wrapperPath)) unlinkSync(file.wrapperPath);
|
|
289
336
|
}
|
|
290
337
|
for (const file of files) {
|
|
@@ -8,8 +8,7 @@ function trackedAccountId(ws: ServerWebSocket<WsData>): string | null {
|
|
|
8
8
|
return ctx?.kind === "pool" ? ctx.accountId : null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const accountId = trackedAccountId(ws);
|
|
11
|
+
function addSocketForAccount(accountId: string | null, ws: ServerWebSocket<WsData>): void {
|
|
13
12
|
if (!accountId) return;
|
|
14
13
|
let sockets = socketsByAccount.get(accountId);
|
|
15
14
|
if (!sockets) {
|
|
@@ -19,8 +18,7 @@ export function registerCodexWebSocket(ws: ServerWebSocket<WsData>): void {
|
|
|
19
18
|
sockets.add(ws);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
const accountId = trackedAccountId(ws);
|
|
21
|
+
function removeSocketForAccount(accountId: string | null, ws: ServerWebSocket<WsData>): void {
|
|
24
22
|
if (!accountId) return;
|
|
25
23
|
const sockets = socketsByAccount.get(accountId);
|
|
26
24
|
if (!sockets) return;
|
|
@@ -28,6 +26,24 @@ export function unregisterCodexWebSocket(ws: ServerWebSocket<WsData>): void {
|
|
|
28
26
|
if (sockets.size === 0) socketsByAccount.delete(accountId);
|
|
29
27
|
}
|
|
30
28
|
|
|
29
|
+
export function registerCodexWebSocket(ws: ServerWebSocket<WsData>): void {
|
|
30
|
+
addSocketForAccount(trackedAccountId(ws), ws);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function unregisterCodexWebSocket(ws: ServerWebSocket<WsData>): void {
|
|
34
|
+
removeSocketForAccount(trackedAccountId(ws), ws);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function updateCodexWebSocketAuthContext(
|
|
38
|
+
ws: ServerWebSocket<WsData>,
|
|
39
|
+
authContext: WsData["authContext"],
|
|
40
|
+
): void {
|
|
41
|
+
const before = trackedAccountId(ws);
|
|
42
|
+
removeSocketForAccount(before, ws);
|
|
43
|
+
ws.data.authContext = authContext;
|
|
44
|
+
addSocketForAccount(trackedAccountId(ws), ws);
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
export function invalidateCodexWebSocketsForAccount(accountId: string): number {
|
|
32
48
|
const sockets = socketsByAccount.get(accountId);
|
|
33
49
|
if (!sockets) return 0;
|