@bitkyc08/opencodex 2.5.5-preview.1 → 2.5.5
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-CKX3MGK9.js → index-qkvcDJZw.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli.ts +54 -11
- package/src/codex-catalog.ts +196 -26
- package/src/codex-history-provider.ts +35 -6
- package/src/codex-shim.ts +159 -42
- package/src/config.ts +61 -7
- package/src/open-url.ts +9 -1
- package/src/server.ts +51 -3
- package/src/service.ts +46 -9
package/src/codex-shim.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync,
|
|
|
3
3
|
import { getConfigDir } from "./config";
|
|
4
4
|
|
|
5
5
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
6
|
-
const STATE_PATH = join(getConfigDir(), "codex-shim.json");
|
|
7
6
|
const CODEX_INTERNAL_COMMANDS = [
|
|
8
7
|
"app-server",
|
|
9
8
|
"archive",
|
|
@@ -34,6 +33,13 @@ interface ShimState {
|
|
|
34
33
|
wrapperPath: string;
|
|
35
34
|
originalPath: string;
|
|
36
35
|
backupPath: string;
|
|
36
|
+
wrappers?: ShimFileState[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ShimFileState {
|
|
40
|
+
wrapperPath: string;
|
|
41
|
+
originalPath: string;
|
|
42
|
+
backupPath: string;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
function cliEntry(): { bun: string; cli: string } {
|
|
@@ -69,6 +75,33 @@ function findCodexOnPath(): string | null {
|
|
|
69
75
|
return null;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
79
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
80
|
+
const cmd = join(dir, "codex.cmd");
|
|
81
|
+
const ps1 = join(dir, "codex.ps1");
|
|
82
|
+
const targets: ShimFileState[] = [];
|
|
83
|
+
for (const path of [cmd, ps1]) {
|
|
84
|
+
if (!existsSync(path) || isShim(path)) continue;
|
|
85
|
+
try {
|
|
86
|
+
if (!lstatSync(path).isDirectory()) {
|
|
87
|
+
targets.push({ wrapperPath: path, originalPath: path, backupPath: backupPathFor(path) });
|
|
88
|
+
}
|
|
89
|
+
} catch { /* keep scanning */ }
|
|
90
|
+
}
|
|
91
|
+
if (targets.length > 0) return targets;
|
|
92
|
+
|
|
93
|
+
const exe = join(dir, "codex.exe");
|
|
94
|
+
if (!existsSync(exe) || isShim(exe)) continue;
|
|
95
|
+
try {
|
|
96
|
+
if (lstatSync(exe).isDirectory()) continue;
|
|
97
|
+
const wrapperPath = join(dir, "codex.cmd");
|
|
98
|
+
if (existsSync(wrapperPath) && !isShim(wrapperPath)) continue;
|
|
99
|
+
return [{ wrapperPath, originalPath: exe, backupPath: backupPathFor(exe) }];
|
|
100
|
+
} catch { /* keep scanning */ }
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
72
105
|
function backupPathFor(path: string): string {
|
|
73
106
|
const ext = extname(path);
|
|
74
107
|
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
@@ -107,83 +140,167 @@ if /I "%~1"=="-V" goto run_codex\r
|
|
|
107
140
|
`;
|
|
108
141
|
}
|
|
109
142
|
|
|
143
|
+
function psString(value: string): string {
|
|
144
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
148
|
+
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
149
|
+
return `#!/usr/bin/env pwsh
|
|
150
|
+
# ${SHIM_MARKER}
|
|
151
|
+
$internalCommands = @(${internalCommands})
|
|
152
|
+
$firstArg = if ($args.Count -gt 0) { [string]$args[0] } else { "" }
|
|
153
|
+
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $firstArg -or @("--help", "-h", "--version", "-V") -contains $firstArg
|
|
154
|
+
if (-not $skipEnsure) {
|
|
155
|
+
& ${psString(bunPath)} ${psString(cliPath)} ensure *> $null
|
|
156
|
+
}
|
|
157
|
+
& ${psString(realCodexPath)} @args
|
|
158
|
+
exit $LASTEXITCODE
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
110
162
|
function readState(): ShimState | null {
|
|
111
163
|
try {
|
|
112
|
-
return JSON.parse(readFileSync(
|
|
164
|
+
return JSON.parse(readFileSync(statePath(), "utf8")) as ShimState;
|
|
113
165
|
} catch {
|
|
114
166
|
return null;
|
|
115
167
|
}
|
|
116
168
|
}
|
|
117
169
|
|
|
170
|
+
function statePath(): string {
|
|
171
|
+
return join(getConfigDir(), "codex-shim.json");
|
|
172
|
+
}
|
|
173
|
+
|
|
118
174
|
function writeState(state: ShimState): void {
|
|
119
175
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
120
|
-
writeFileSync(
|
|
176
|
+
writeFileSync(statePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
121
177
|
}
|
|
122
178
|
|
|
123
179
|
function writeShim(wrapperPath: string, realCodexPath: string): void {
|
|
124
180
|
const { bun, cli } = cliEntry();
|
|
125
181
|
if (process.platform === "win32") {
|
|
126
|
-
|
|
182
|
+
if (wrapperPath.toLowerCase().endsWith(".ps1")) {
|
|
183
|
+
writeFileSync(wrapperPath, buildWindowsPowerShellCodexShim(realCodexPath, bun, cli), "utf8");
|
|
184
|
+
} else {
|
|
185
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
|
|
186
|
+
}
|
|
127
187
|
} else {
|
|
128
188
|
writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
|
|
129
189
|
chmodSync(wrapperPath, 0o755);
|
|
130
190
|
}
|
|
131
191
|
}
|
|
132
192
|
|
|
193
|
+
function stateFiles(state: ShimState): ShimFileState[] {
|
|
194
|
+
return state.wrappers?.length
|
|
195
|
+
? state.wrappers
|
|
196
|
+
: [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function primaryState(files: ShimFileState[]): ShimState {
|
|
200
|
+
const first = files[0];
|
|
201
|
+
return { platform: process.platform, ...first, wrappers: files };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function replaceOwnedBackup(sourcePath: string, backupPath: string): void {
|
|
205
|
+
const oldBackupPath = `${backupPath}.old-${process.pid}`;
|
|
206
|
+
if (existsSync(oldBackupPath)) unlinkSync(oldBackupPath);
|
|
207
|
+
if (existsSync(backupPath)) renameSync(backupPath, oldBackupPath);
|
|
208
|
+
try {
|
|
209
|
+
renameSync(sourcePath, backupPath);
|
|
210
|
+
if (existsSync(oldBackupPath)) unlinkSync(oldBackupPath);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (!existsSync(backupPath) && existsSync(oldBackupPath)) renameSync(oldBackupPath, backupPath);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function refreshShimFile(file: ShimFileState): boolean {
|
|
218
|
+
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
219
|
+
if (file.wrapperPath !== file.originalPath) return false;
|
|
220
|
+
replaceOwnedBackup(file.wrapperPath, file.backupPath);
|
|
221
|
+
writeShim(file.wrapperPath, file.backupPath);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
225
|
+
writeShim(file.wrapperPath, file.backupPath);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
229
|
+
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
230
|
+
writeShim(file.wrapperPath, file.backupPath);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
133
236
|
export function installCodexShim(): { installed: boolean; message: string } {
|
|
134
237
|
const existing = readState();
|
|
135
|
-
if (existing
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
238
|
+
if (existing) {
|
|
239
|
+
const files = stateFiles(existing);
|
|
240
|
+
let refreshed = false;
|
|
241
|
+
for (const file of files) refreshed = refreshShimFile(file) || refreshed;
|
|
242
|
+
const allInstalled = files.every(file => existsSync(file.wrapperPath) && existsSync(file.backupPath) && isShim(file.wrapperPath));
|
|
243
|
+
if (refreshed || allInstalled) {
|
|
244
|
+
writeState(primaryState(files));
|
|
245
|
+
if (refreshed) {
|
|
246
|
+
return {
|
|
247
|
+
installed: true,
|
|
248
|
+
message: `Codex update detected. Backed up new launcher and refreshed shim at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
140
251
|
return {
|
|
141
|
-
installed:
|
|
142
|
-
message: `Codex
|
|
252
|
+
installed: false,
|
|
253
|
+
message: `Codex autostart shim already installed at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
143
254
|
};
|
|
144
255
|
}
|
|
145
|
-
return { installed: false, message: `Codex autostart shim already installed at ${existing.wrapperPath}.` };
|
|
146
|
-
}
|
|
147
|
-
if (existing && existsSync(existing.backupPath) && (!existsSync(existing.wrapperPath) || !isShim(existing.wrapperPath))) {
|
|
148
|
-
if (existsSync(existing.wrapperPath)) unlinkSync(existing.wrapperPath);
|
|
149
|
-
writeShim(existing.wrapperPath, existing.backupPath);
|
|
150
|
-
writeState({ ...existing, platform: process.platform });
|
|
151
|
-
return {
|
|
152
|
-
installed: true,
|
|
153
|
-
message: `Codex autostart shim repaired at ${existing.wrapperPath}. Original remains at ${existing.backupPath}.`,
|
|
154
|
-
};
|
|
155
256
|
}
|
|
156
257
|
|
|
157
|
-
const
|
|
158
|
-
|
|
258
|
+
const targets = process.platform === "win32"
|
|
259
|
+
? findWindowsCodexTargets()
|
|
260
|
+
: (() => {
|
|
261
|
+
const originalPath = findCodexOnPath();
|
|
262
|
+
return originalPath ? [{ wrapperPath: originalPath, originalPath, backupPath: backupPathFor(originalPath) }] : null;
|
|
263
|
+
})();
|
|
264
|
+
if (!targets) return { installed: false, message: "Could not find a codex executable on PATH." };
|
|
159
265
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
266
|
+
for (const target of targets) {
|
|
267
|
+
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
268
|
+
}
|
|
269
|
+
for (const target of targets) {
|
|
270
|
+
renameSync(target.originalPath, target.backupPath);
|
|
271
|
+
writeShim(target.wrapperPath, target.backupPath);
|
|
272
|
+
}
|
|
273
|
+
writeState(primaryState(targets));
|
|
274
|
+
return {
|
|
275
|
+
installed: true,
|
|
276
|
+
message: `Codex autostart shim installed at ${targets.map(t => t.wrapperPath).join(", ")}. Original saved at ${targets.map(t => t.backupPath).join(", ")}.`,
|
|
277
|
+
};
|
|
168
278
|
}
|
|
169
279
|
|
|
170
280
|
export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
171
281
|
const state = readState();
|
|
172
282
|
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
283
|
+
const files = stateFiles(state);
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
if (existsSync(file.wrapperPath) && isShim(file.wrapperPath)) unlinkSync(file.wrapperPath);
|
|
286
|
+
}
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
if (existsSync(file.backupPath) && !existsSync(file.originalPath)) renameSync(file.backupPath, file.originalPath);
|
|
289
|
+
}
|
|
290
|
+
if (existsSync(statePath())) unlinkSync(statePath());
|
|
291
|
+
return { removed: true, message: `Codex autostart shim removed. Restored ${files.map(f => f.originalPath).join(", ")}.` };
|
|
177
292
|
}
|
|
178
293
|
|
|
179
294
|
export function codexShimStatus(): string {
|
|
180
295
|
const state = readState();
|
|
181
296
|
if (!state) return "Codex autostart shim is not installed.";
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
?
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
297
|
+
return stateFiles(state).map(file => {
|
|
298
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
299
|
+
? isShim(file.wrapperPath)
|
|
300
|
+
? "shim present"
|
|
301
|
+
: "present but not an opencodex shim"
|
|
302
|
+
: "missing";
|
|
303
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
304
|
+
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
305
|
+
}).join("\n");
|
|
189
306
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, chmodSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -92,7 +93,7 @@ export function loadConfig(): OcxConfig {
|
|
|
92
93
|
return getDefaultConfig();
|
|
93
94
|
}
|
|
94
95
|
try {
|
|
95
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
96
|
+
const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
|
|
96
97
|
const parsed = JSON.parse(raw);
|
|
97
98
|
const result = configSchema.safeParse(parsed);
|
|
98
99
|
if (result.success) return result.data as OcxConfig;
|
|
@@ -172,7 +173,7 @@ export function writePid(pid: number): void {
|
|
|
172
173
|
} else {
|
|
173
174
|
hardenConfigDir();
|
|
174
175
|
}
|
|
175
|
-
|
|
176
|
+
atomicWriteFile(getPidPath(), String(pid));
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
export function readPid(): number | null {
|
|
@@ -180,13 +181,15 @@ export function readPid(): number | null {
|
|
|
180
181
|
if (!existsSync(pidPath)) return null;
|
|
181
182
|
try {
|
|
182
183
|
const raw = readFileSync(pidPath, "utf-8").trim();
|
|
183
|
-
const pid =
|
|
184
|
-
if (
|
|
184
|
+
const pid = parsePidFile(raw);
|
|
185
|
+
if (pid === null) return null;
|
|
185
186
|
try {
|
|
186
187
|
process.kill(pid, 0);
|
|
187
|
-
return pid;
|
|
188
|
+
return isLikelyOcxStartProcess(pid) ? pid : null;
|
|
188
189
|
} catch (e: unknown) {
|
|
189
|
-
if ((e as NodeJS.ErrnoException).code === "EPERM")
|
|
190
|
+
if ((e as NodeJS.ErrnoException).code === "EPERM") {
|
|
191
|
+
return isLikelyOcxStartProcess(pid) ? pid : null;
|
|
192
|
+
}
|
|
190
193
|
return null;
|
|
191
194
|
}
|
|
192
195
|
} catch {
|
|
@@ -194,7 +197,8 @@ export function readPid(): number | null {
|
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
export function removePid(): void {
|
|
200
|
+
export function removePid(expectedPid?: number): void {
|
|
201
|
+
if (expectedPid !== undefined && readPidFileValue() !== expectedPid) return;
|
|
198
202
|
try {
|
|
199
203
|
unlinkSync(getPidPath());
|
|
200
204
|
} catch { /* ignore */ }
|
|
@@ -207,6 +211,56 @@ function warnConfigRepaired(configPath: string, error: z.ZodError): void {
|
|
|
207
211
|
console.error(`opencodex config at ${configPath}: repaired missing field(s) [${fields}] with defaults. Your providers and accounts are preserved.`);
|
|
208
212
|
}
|
|
209
213
|
|
|
214
|
+
function readPidFileValue(): number | null {
|
|
215
|
+
try {
|
|
216
|
+
return parsePidFile(readFileSync(getPidPath(), "utf-8"));
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function parsePidFile(raw: string): number | null {
|
|
223
|
+
const trimmed = raw.trim();
|
|
224
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
225
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
226
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isOcxStartCommandLine(commandLine: string): boolean {
|
|
230
|
+
const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
|
|
231
|
+
const hasOcxEntrypoint = normalized.includes("src/cli.ts")
|
|
232
|
+
|| normalized.includes("@bitkyc08/opencodex")
|
|
233
|
+
|| /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/.test(normalized);
|
|
234
|
+
return hasOcxEntrypoint && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isLikelyOcxStartProcess(pid: number): boolean {
|
|
238
|
+
const commandLine = readProcessCommandLine(pid);
|
|
239
|
+
if (commandLine === undefined) return false;
|
|
240
|
+
return isOcxStartCommandLine(commandLine);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function readProcessCommandLine(pid: number): string | undefined {
|
|
244
|
+
try {
|
|
245
|
+
if (process.platform === "win32") {
|
|
246
|
+
const output = execFileSync("powershell.exe", [
|
|
247
|
+
"-NoProfile",
|
|
248
|
+
"-Command",
|
|
249
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
250
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 });
|
|
251
|
+
return output.trim() || undefined;
|
|
252
|
+
}
|
|
253
|
+
const output = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
254
|
+
encoding: "utf-8",
|
|
255
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
256
|
+
timeout: 1000,
|
|
257
|
+
});
|
|
258
|
+
return output.trim() || undefined;
|
|
259
|
+
} catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
210
264
|
function warnAndBackupInvalidConfig(configPath: string, error: unknown): void {
|
|
211
265
|
if (warnedConfigFallbacks.has(configPath)) return;
|
|
212
266
|
warnedConfigFallbacks.add(configPath);
|
package/src/open-url.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function windowsRundll32(): string {
|
|
6
|
+
const windowsRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
|
|
7
|
+
const candidate = join(windowsRoot, "System32", "rundll32.exe");
|
|
8
|
+
return existsSync(candidate) ? candidate : "rundll32";
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
export function openUrl(url: string): void {
|
|
4
12
|
if (!/^https?:\/\//i.test(url)) return;
|
|
5
13
|
const cmd =
|
|
6
14
|
process.platform === "darwin" ? "open"
|
|
7
|
-
: process.platform === "win32" ?
|
|
15
|
+
: process.platform === "win32" ? windowsRundll32()
|
|
8
16
|
: "xdg-open";
|
|
9
17
|
const args = process.platform === "win32"
|
|
10
18
|
? ["url.dll,FileProtocolHandler", url]
|
package/src/server.ts
CHANGED
|
@@ -522,14 +522,58 @@ async function fetchWithHeaderTimeout(
|
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
|
|
525
|
+
export interface RequestLogEntry {
|
|
526
|
+
requestId: string;
|
|
527
|
+
timestamp: number;
|
|
528
|
+
model: string;
|
|
529
|
+
provider: string;
|
|
530
|
+
status: number;
|
|
531
|
+
durationMs: number;
|
|
532
|
+
errorCode?: string;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const requestLog: RequestLogEntry[] = [];
|
|
526
536
|
const MAX_LOG_SIZE = 200;
|
|
537
|
+
let requestLogSeq = 0;
|
|
527
538
|
|
|
528
|
-
function addRequestLog(entry:
|
|
539
|
+
function addRequestLog(entry: RequestLogEntry) {
|
|
529
540
|
requestLog.push(entry);
|
|
530
541
|
if (requestLog.length > MAX_LOG_SIZE) requestLog.shift();
|
|
531
542
|
}
|
|
532
543
|
|
|
544
|
+
export function nextRequestLogId(timestamp = Date.now()): string {
|
|
545
|
+
requestLogSeq = (requestLogSeq % 1_000_000) + 1;
|
|
546
|
+
return `ocx-${timestamp.toString(36)}-${requestLogSeq.toString(36)}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function requestLogErrorCode(status: number): string | undefined {
|
|
550
|
+
if (status >= 200 && status < 400) return undefined;
|
|
551
|
+
if (status === 400 || status === 409) return "invalid_request_error";
|
|
552
|
+
if (status === 401 || status === 403) return "invalid_api_key";
|
|
553
|
+
if (status === 429) return "rate_limit_exceeded";
|
|
554
|
+
if (status === 503) return "server_is_overloaded";
|
|
555
|
+
if (status >= 500) return "upstream_server_error";
|
|
556
|
+
return `http_${status}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchParams): RequestLogEntry[] {
|
|
560
|
+
let filtered = logs;
|
|
561
|
+
const provider = params.get("provider")?.trim();
|
|
562
|
+
if (provider) filtered = filtered.filter(entry => entry.provider === provider);
|
|
563
|
+
const status = params.get("status")?.trim().toLowerCase();
|
|
564
|
+
if (status) {
|
|
565
|
+
filtered = /^[1-5]xx$/.test(status)
|
|
566
|
+
? filtered.filter(entry => Math.floor(entry.status / 100) === Number(status[0]))
|
|
567
|
+
: filtered.filter(entry => String(entry.status) === status);
|
|
568
|
+
}
|
|
569
|
+
const tailRaw = params.get("tail")?.trim();
|
|
570
|
+
if (tailRaw) {
|
|
571
|
+
const tail = Number.parseInt(tailRaw, 10);
|
|
572
|
+
if (Number.isFinite(tail) && tail > 0) filtered = filtered.slice(-Math.min(tail, MAX_LOG_SIZE));
|
|
573
|
+
}
|
|
574
|
+
return filtered;
|
|
575
|
+
}
|
|
576
|
+
|
|
533
577
|
/**
|
|
534
578
|
* Relay an upstream body verbatim while wiring client-cancel -> upstream.abort(). A body returned
|
|
535
579
|
* directly from fetch does NOT propagate the consumer's cancel to a signalled fetch, so a client
|
|
@@ -941,7 +985,7 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
|
|
|
941
985
|
}
|
|
942
986
|
|
|
943
987
|
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
944
|
-
return jsonResponse(requestLog);
|
|
988
|
+
return jsonResponse(filterRequestLogs(requestLog, url.searchParams));
|
|
945
989
|
}
|
|
946
990
|
|
|
947
991
|
if (url.pathname === "/api/providers" && req.method === "GET") {
|
|
@@ -1228,14 +1272,18 @@ export function startServer(port?: number) {
|
|
|
1228
1272
|
return formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked");
|
|
1229
1273
|
}
|
|
1230
1274
|
const start = Date.now();
|
|
1275
|
+
const requestId = nextRequestLogId(start);
|
|
1231
1276
|
const logCtx = { model: "unknown", provider: "unknown" };
|
|
1232
1277
|
const response = await handleResponses(req, config, logCtx);
|
|
1278
|
+
const errorCode = requestLogErrorCode(response.status);
|
|
1233
1279
|
addRequestLog({
|
|
1280
|
+
requestId,
|
|
1234
1281
|
timestamp: start,
|
|
1235
1282
|
model: logCtx.model,
|
|
1236
1283
|
provider: logCtx.provider,
|
|
1237
1284
|
status: response.status,
|
|
1238
1285
|
durationMs: Date.now() - start,
|
|
1286
|
+
...(errorCode ? { errorCode } : {}),
|
|
1239
1287
|
});
|
|
1240
1288
|
return response;
|
|
1241
1289
|
}
|
package/src/service.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Codex on a service-managed restart (the restarted instance re-injects); explicit stop/uninstall
|
|
6
6
|
* restore it via the command.
|
|
7
7
|
*/
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
9
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
@@ -95,6 +95,19 @@ function sh(cmd: string): string {
|
|
|
95
95
|
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function runFile(file: string, args: string[]): string {
|
|
99
|
+
return execFileSync(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function windowsSchtasks(): string {
|
|
103
|
+
const candidate = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "schtasks.exe");
|
|
104
|
+
return existsSync(candidate) ? candidate : "schtasks.exe";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function schtasks(args: string[]): string {
|
|
108
|
+
return runFile(windowsSchtasks(), args);
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
function windowsBatchValue(value: string): string {
|
|
99
112
|
return value.replace(/%/g, "%%").replace(/[\r\n]/g, "");
|
|
100
113
|
}
|
|
@@ -124,6 +137,10 @@ export function buildWindowsServiceScript(): string {
|
|
|
124
137
|
return `${lines.join("\r\n")}\r\n`;
|
|
125
138
|
}
|
|
126
139
|
|
|
140
|
+
export function buildWindowsSchtasksCreateArgs(script = windowsServiceScriptPath()): string[] {
|
|
141
|
+
return ["/create", "/tn", TASK, "/tr", `"${script}"`, "/sc", "onlogon", "/rl", "highest", "/f"];
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
// ── macOS (launchd) ──
|
|
128
145
|
function installLaunchd(): void {
|
|
129
146
|
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
@@ -148,14 +165,14 @@ function installWindows(): void {
|
|
|
148
165
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
149
166
|
const script = windowsServiceScriptPath();
|
|
150
167
|
writeFileSync(script, buildWindowsServiceScript(), "utf8");
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
schtasks(buildWindowsSchtasksCreateArgs(script));
|
|
169
|
+
schtasks(["/run", "/tn", TASK]);
|
|
153
170
|
}
|
|
154
|
-
function startWindows(): void {
|
|
155
|
-
function stopWindows(): void { try {
|
|
156
|
-
function statusWindows(): string { try { return
|
|
171
|
+
function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
|
|
172
|
+
function stopWindows(): void { try { schtasks(["/end", "/tn", TASK]); } catch { /* not running */ } }
|
|
173
|
+
function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
|
|
157
174
|
function uninstallWindows(): void {
|
|
158
|
-
try {
|
|
175
|
+
try { schtasks(["/delete", "/tn", TASK, "/f"]); } catch { /* absent */ }
|
|
159
176
|
if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
|
|
160
177
|
}
|
|
161
178
|
|
|
@@ -253,7 +270,7 @@ export function stopServiceIfInstalled(): boolean {
|
|
|
253
270
|
}
|
|
254
271
|
} else if (process.platform === "win32") {
|
|
255
272
|
try {
|
|
256
|
-
const q =
|
|
273
|
+
const q = schtasks(["/query", "/tn", TASK]);
|
|
257
274
|
if (q.includes(TASK)) { stopWindows(); return true; }
|
|
258
275
|
} catch { /* task not found */ }
|
|
259
276
|
} else if (process.platform === "linux" && isSystemd() && existsSync(unitPath())) {
|
|
@@ -274,7 +291,7 @@ export function uninstallServiceIfInstalled(): boolean {
|
|
|
274
291
|
}
|
|
275
292
|
} else if (process.platform === "win32") {
|
|
276
293
|
try {
|
|
277
|
-
const q =
|
|
294
|
+
const q = schtasks(["/query", "/tn", TASK]);
|
|
278
295
|
if (q.includes(TASK)) { uninstallWindows(); return true; }
|
|
279
296
|
} catch { /* task not found */ }
|
|
280
297
|
} else if (process.platform === "linux" && isSystemd() && existsSync(unitPath())) {
|
|
@@ -283,6 +300,26 @@ export function uninstallServiceIfInstalled(): boolean {
|
|
|
283
300
|
return false;
|
|
284
301
|
}
|
|
285
302
|
|
|
303
|
+
export function serviceStatusSummary(): string {
|
|
304
|
+
if (process.platform === "darwin") {
|
|
305
|
+
if (!existsSync(plistPath())) return "not installed";
|
|
306
|
+
const status = statusLaunchd();
|
|
307
|
+
return status ? "installed (launchd)" : "installed, not loaded";
|
|
308
|
+
}
|
|
309
|
+
if (process.platform === "win32") {
|
|
310
|
+
const status = statusWindows();
|
|
311
|
+
return status ? "installed (Task Scheduler)" : "not installed";
|
|
312
|
+
}
|
|
313
|
+
if (process.platform === "linux") {
|
|
314
|
+
if (existsSync("/.dockerenv")) return "unsupported in Docker";
|
|
315
|
+
if (!isSystemd()) return "unsupported: systemd not found";
|
|
316
|
+
if (!existsSync(unitPath())) return "not installed";
|
|
317
|
+
const status = statusSystemd();
|
|
318
|
+
return status ? "installed (systemd user)" : "installed, not running";
|
|
319
|
+
}
|
|
320
|
+
return `unsupported on ${process.platform}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
286
323
|
export function serviceCommand(sub?: string): void {
|
|
287
324
|
const ops = platformOps();
|
|
288
325
|
if (!ops) {
|