minecodex 0.1.14 → 0.1.16
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 +2 -0
- package/features/model-slider/codex-feature.json +1 -0
- package/package.json +1 -1
- package/packages/cli/src/chatgpt-launch-coordinator.mjs +77 -0
- package/packages/cli/src/control-server.mjs +2 -0
- package/packages/cli/src/platform.mjs +220 -29
- package/packages/cli/src/runtime-manager.mjs +178 -100
- package/packages/runtime-host/src/codex-runtime.mjs +180 -109
- package/packages/runtime-host/src/main.mjs +1 -3
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ mcx install
|
|
|
26
26
|
|
|
27
27
|
运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
|
|
28
28
|
|
|
29
|
+
MineCodex 只以后台辅助服务运行,前台应用始终是 OpenAI 签名的官方 `ChatGPT.app`,并继续使用官方默认用户数据。通过 Dock、Finder、Spotlight、登录项或会话恢复打开 ChatGPT 时,后台服务会验证官方应用与对应进程;若该进程尚未开放兼容的本机调试端口,则确认其完整退出后再通过 LaunchServices 重新打开官方应用并注入功能。MineCodex 不修改 `ChatGPT.app`、`app.asar`、Dock 项目或默认打开方式。
|
|
30
|
+
|
|
29
31
|
打开本地控制台:
|
|
30
32
|
|
|
31
33
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function hasExactArgument(command, argument) {
|
|
2
|
+
const value = String(command);
|
|
3
|
+
let offset = value.indexOf(argument);
|
|
4
|
+
while (offset >= 0) {
|
|
5
|
+
const before = offset === 0 || /\s/.test(value[offset - 1]);
|
|
6
|
+
const end = offset + argument.length;
|
|
7
|
+
const after = end === value.length || /\s/.test(value[end]);
|
|
8
|
+
if (before && after) return true;
|
|
9
|
+
offset = value.indexOf(argument, offset + 1);
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasLoopbackCdpArguments(command, cdpPort) {
|
|
15
|
+
return hasExactArgument(command, "--remote-debugging-address=127.0.0.1")
|
|
16
|
+
&& hasExactArgument(command, `--remote-debugging-port=${cdpPort}`)
|
|
17
|
+
&& hasExactArgument(command, `--remote-allow-origins=http://127.0.0.1:${cdpPort}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requireProcess(processInfo, context) {
|
|
21
|
+
if (!processInfo || !Number.isInteger(processInfo.pid) || processInfo.pid <= 1) {
|
|
22
|
+
throw new Error(`MineCodex could not verify the ${context} ChatGPT process.`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof processInfo.command !== "string" || !processInfo.command.trim()) {
|
|
25
|
+
throw new Error(`MineCodex could not verify the ${context} ChatGPT command line.`);
|
|
26
|
+
}
|
|
27
|
+
if (processInfo.verified === false) {
|
|
28
|
+
throw new Error(`MineCodex rejected an unverified ${context} ChatGPT process.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ChatGPTLaunchCoordinator {
|
|
33
|
+
constructor({ platform, cdpPort }) {
|
|
34
|
+
this.platform = platform;
|
|
35
|
+
this.cdpPort = cdpPort;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async attach(processInfo, owned, { startRuntime, waitForHealthy }) {
|
|
39
|
+
requireProcess(processInfo, owned ? "LaunchServices-created" : "observed");
|
|
40
|
+
await this.platform.assertNativeCodexCdp(processInfo, this.cdpPort);
|
|
41
|
+
const status = await startRuntime();
|
|
42
|
+
const healthy = await waitForHealthy(status);
|
|
43
|
+
await this.platform.assertNativeCodexCdp(processInfo, this.cdpPort);
|
|
44
|
+
return {
|
|
45
|
+
...healthy,
|
|
46
|
+
codex: { pid: processInfo.pid, owned, command: processInfo.command ?? null },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async ensureObservedLaunch(processInfo, { stopRuntime, startRuntime, waitForHealthy }) {
|
|
51
|
+
requireProcess(processInfo, "observed");
|
|
52
|
+
if (typeof this.platform?.terminateNativeCodex !== "function"
|
|
53
|
+
|| typeof this.platform?.openNativeCodex !== "function"
|
|
54
|
+
|| typeof this.platform?.assertNativeCodexCdp !== "function") {
|
|
55
|
+
throw new Error("MineCodex platform adapter cannot coordinate an official ChatGPT launch.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await stopRuntime();
|
|
59
|
+
if (hasLoopbackCdpArguments(processInfo.command, this.cdpPort)) {
|
|
60
|
+
return this.attach(processInfo, false, { startRuntime, waitForHealthy });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const terminated = await this.platform.terminateNativeCodex(processInfo.pid, processInfo.command);
|
|
64
|
+
if (terminated !== 1) {
|
|
65
|
+
throw new Error(`MineCodex refused to replace ChatGPT pid ${processInfo.pid} after identity changed.`);
|
|
66
|
+
}
|
|
67
|
+
const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
|
|
68
|
+
return this.attach(launched, true, { startRuntime, waitForHealthy });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async relaunchOwned({ stopRuntime, terminateOwned, startRuntime, waitForHealthy }) {
|
|
72
|
+
await stopRuntime();
|
|
73
|
+
await terminateOwned();
|
|
74
|
+
const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
|
|
75
|
+
return this.attach(launched, true, { startRuntime, waitForHealthy });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -290,11 +290,13 @@ export async function createControlServer({
|
|
|
290
290
|
service: { running: true },
|
|
291
291
|
runtime: {
|
|
292
292
|
running: runtime.running,
|
|
293
|
+
healthy: runtime.healthy === true,
|
|
293
294
|
pid: runtime.pid ?? null,
|
|
294
295
|
runtimeSessionId: runtime.runtimeSessionId ?? null,
|
|
295
296
|
ready: runtime.ready ?? null,
|
|
296
297
|
plugins: runtime.plugins ?? runtime.pluginStates ?? [],
|
|
297
298
|
renderer: runtime.renderer ?? null,
|
|
299
|
+
codex: runtime.codex ?? null,
|
|
298
300
|
failures: runtime.failures ?? [],
|
|
299
301
|
},
|
|
300
302
|
runtimeSessionId: runtime.runtimeSessionId ?? null,
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/pr
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { hasLoopbackCdpArguments } from "./chatgpt-launch-coordinator.mjs";
|
|
6
7
|
|
|
7
8
|
const defaultExecFile = promisify(execFileCallback);
|
|
8
9
|
const defaultFileSystem = Object.freeze({ mkdir, readFile, rename, rm, stat, unlink, writeFile });
|
|
@@ -11,6 +12,8 @@ export const SERVICE_LABEL = "com.vontean.minecodex.background";
|
|
|
11
12
|
export const LEGACY_SERVICE_LABEL = "com.vontean.minecodex";
|
|
12
13
|
export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
|
|
13
14
|
export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
|
|
15
|
+
export const CODEX_BUNDLE_IDENTIFIER = "com.openai.codex";
|
|
16
|
+
export const CODEX_TEAM_IDENTIFIER = "2DC432GLL2";
|
|
14
17
|
export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
|
|
15
18
|
export const MIN_MACOS_VERSION = Object.freeze({ major: 13, darwinMajor: 22 });
|
|
16
19
|
|
|
@@ -90,6 +93,40 @@ export async function resolveCodexInstallation({
|
|
|
90
93
|
);
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
export async function verifyCodexInstallation({ installation, execFile = defaultExecFile } = {}) {
|
|
97
|
+
if (!installation?.appPath || !installation?.executable) {
|
|
98
|
+
throw new Error("Codex Desktop installation is incomplete.");
|
|
99
|
+
}
|
|
100
|
+
const infoPath = path.join(installation.appPath, "Contents", "Info.plist");
|
|
101
|
+
const { stdout: bundleIdentifier } = await execFile("/usr/bin/plutil", [
|
|
102
|
+
"-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPath,
|
|
103
|
+
]);
|
|
104
|
+
if (String(bundleIdentifier).trim() !== CODEX_BUNDLE_IDENTIFIER) {
|
|
105
|
+
throw new Error(`Refusing non-official ChatGPT bundle: ${String(bundleIdentifier).trim() || "unknown"}`);
|
|
106
|
+
}
|
|
107
|
+
await execFile("/usr/bin/codesign", ["--verify", "--deep", "--strict", installation.appPath]);
|
|
108
|
+
const { stderr: details } = await execFile("/usr/bin/codesign", [
|
|
109
|
+
"-dv", "--verbose=4", installation.appPath,
|
|
110
|
+
]);
|
|
111
|
+
const authority = String(details).match(/^Authority=(.+)$/m)?.[1] ?? "";
|
|
112
|
+
const identifier = String(details).match(/^Identifier=(.+)$/m)?.[1] ?? "";
|
|
113
|
+
const teamIdentifier = String(details).match(/^TeamIdentifier=(.+)$/m)?.[1] ?? "";
|
|
114
|
+
if (
|
|
115
|
+
identifier !== CODEX_BUNDLE_IDENTIFIER
|
|
116
|
+
|| teamIdentifier !== CODEX_TEAM_IDENTIFIER
|
|
117
|
+
|| !/\bOpenAI\b/i.test(authority)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("Refusing ChatGPT with an unexpected signing identity.");
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
...installation,
|
|
123
|
+
bundleIdentifier: CODEX_BUNDLE_IDENTIFIER,
|
|
124
|
+
signingIdentity: authority,
|
|
125
|
+
teamIdentifier,
|
|
126
|
+
verified: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
93
130
|
export function parseProcessList(output) {
|
|
94
131
|
return String(output).split("\n").flatMap((line) => {
|
|
95
132
|
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
@@ -116,6 +153,22 @@ function hasManagedProfile(command, profileDir) {
|
|
|
116
153
|
return false;
|
|
117
154
|
}
|
|
118
155
|
|
|
156
|
+
function extractCdpPort(command) {
|
|
157
|
+
const match = String(command).match(/(?:^|\s)--remote-debugging-port=(\d+)(?:\s|$)/);
|
|
158
|
+
return match ? Number(match[1]) : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseCodexOwnerMarker(content) {
|
|
162
|
+
try {
|
|
163
|
+
const marker = JSON.parse(String(content));
|
|
164
|
+
if (marker?.version !== 1 || !Number.isInteger(marker.pid) || marker.pid <= 1) return null;
|
|
165
|
+
if (typeof marker.command !== "string" || !marker.command.trim()) return null;
|
|
166
|
+
return marker;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
119
172
|
export function serviceIsAbsent(error) {
|
|
120
173
|
return /could not find service|service not found|no such process|domain does not support specified action/i.test(
|
|
121
174
|
`${error?.stderr ?? ""}\n${error?.message ?? ""}`,
|
|
@@ -136,6 +189,48 @@ async function waitForProcessExit(pid, { processApi, timeoutMs = 8_000, sleep =
|
|
|
136
189
|
throw new Error(`Process ${pid} did not exit in time`);
|
|
137
190
|
}
|
|
138
191
|
|
|
192
|
+
async function waitForProcessesExit(
|
|
193
|
+
pids,
|
|
194
|
+
{ processApi, timeoutMs = 8_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) },
|
|
195
|
+
) {
|
|
196
|
+
const pending = new Set(pids);
|
|
197
|
+
const deadline = Date.now() + timeoutMs;
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
for (const pid of pending) {
|
|
200
|
+
try {
|
|
201
|
+
processApi.kill(pid, 0);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.code === "ESRCH") pending.delete(pid);
|
|
204
|
+
else throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (pending.size === 0) return;
|
|
208
|
+
await sleep(100);
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`ChatGPT process tree did not exit in time: ${[...pending].join(", ")}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function descendantPids(output, rootPid) {
|
|
214
|
+
const childrenByParent = new Map();
|
|
215
|
+
for (const line of String(output).split("\n")) {
|
|
216
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+/);
|
|
217
|
+
if (!match) continue;
|
|
218
|
+
const pid = Number(match[1]);
|
|
219
|
+
const parentPid = Number(match[2]);
|
|
220
|
+
const children = childrenByParent.get(parentPid) ?? [];
|
|
221
|
+
children.push(pid);
|
|
222
|
+
childrenByParent.set(parentPid, children);
|
|
223
|
+
}
|
|
224
|
+
const descendants = [];
|
|
225
|
+
const pending = [...(childrenByParent.get(rootPid) ?? [])];
|
|
226
|
+
while (pending.length) {
|
|
227
|
+
const pid = pending.shift();
|
|
228
|
+
descendants.push(pid);
|
|
229
|
+
pending.push(...(childrenByParent.get(pid) ?? []));
|
|
230
|
+
}
|
|
231
|
+
return descendants;
|
|
232
|
+
}
|
|
233
|
+
|
|
139
234
|
function serviceTarget(uid) {
|
|
140
235
|
return `gui/${uid}/${SERVICE_LABEL}`;
|
|
141
236
|
}
|
|
@@ -169,7 +264,7 @@ export class MacPlatformAdapter {
|
|
|
169
264
|
execFile = defaultExecFile,
|
|
170
265
|
fileSystem = defaultFileSystem,
|
|
171
266
|
processApi = process,
|
|
172
|
-
sleep,
|
|
267
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
173
268
|
codexInstallation,
|
|
174
269
|
} = {}) {
|
|
175
270
|
this.uid = uid;
|
|
@@ -181,6 +276,8 @@ export class MacPlatformAdapter {
|
|
|
181
276
|
this.processApi = processApi;
|
|
182
277
|
this.sleep = sleep;
|
|
183
278
|
this.codexInstallation = codexInstallation;
|
|
279
|
+
this.codexInstallationFingerprint = null;
|
|
280
|
+
this.codexInstallationInjected = Boolean(codexInstallation);
|
|
184
281
|
}
|
|
185
282
|
|
|
186
283
|
assertInstallSupported() {
|
|
@@ -194,7 +291,7 @@ export class MacPlatformAdapter {
|
|
|
194
291
|
}
|
|
195
292
|
|
|
196
293
|
async assertCodexInstalled() {
|
|
197
|
-
|
|
294
|
+
const installation = await resolveCodexInstallation({
|
|
198
295
|
homeDir: this.homeDir,
|
|
199
296
|
fileExists: async (filePath) => {
|
|
200
297
|
try {
|
|
@@ -206,11 +303,36 @@ export class MacPlatformAdapter {
|
|
|
206
303
|
}
|
|
207
304
|
},
|
|
208
305
|
});
|
|
306
|
+
this.codexInstallation = await verifyCodexInstallation({ installation, execFile: this.execFile });
|
|
307
|
+
this.codexInstallationFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
|
|
308
|
+
this.codexInstallationInjected = false;
|
|
209
309
|
return this.codexInstallation;
|
|
210
310
|
}
|
|
211
311
|
|
|
212
312
|
async codex() {
|
|
213
|
-
|
|
313
|
+
if (!this.codexInstallation) return this.assertCodexInstalled();
|
|
314
|
+
if (this.codexInstallationInjected) return this.codexInstallation;
|
|
315
|
+
const currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
|
|
316
|
+
if (this.codexInstallationFingerprint == null) {
|
|
317
|
+
this.codexInstallationFingerprint = currentFingerprint;
|
|
318
|
+
} else if (currentFingerprint !== this.codexInstallationFingerprint) {
|
|
319
|
+
return this.assertCodexInstalled();
|
|
320
|
+
}
|
|
321
|
+
return this.codexInstallation;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async readCodexInstallationFingerprint(installation) {
|
|
325
|
+
if (typeof this.fileSystem.stat !== "function") return null;
|
|
326
|
+
const paths = [
|
|
327
|
+
installation.executable,
|
|
328
|
+
path.join(installation.appPath, "Contents", "Info.plist"),
|
|
329
|
+
path.join(installation.appPath, "Contents", "_CodeSignature", "CodeResources"),
|
|
330
|
+
];
|
|
331
|
+
const records = await Promise.all(paths.map(async (filePath) => {
|
|
332
|
+
const info = await this.fileSystem.stat(filePath);
|
|
333
|
+
return [filePath, info.dev ?? null, info.ino ?? null, info.size ?? null, info.mtimeMs ?? null];
|
|
334
|
+
}));
|
|
335
|
+
return JSON.stringify(records);
|
|
214
336
|
}
|
|
215
337
|
|
|
216
338
|
async isLaunchdServiceRunning(target) {
|
|
@@ -428,12 +550,34 @@ export class MacPlatformAdapter {
|
|
|
428
550
|
async listCodexProcesses() {
|
|
429
551
|
const installation = await this.codex();
|
|
430
552
|
const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,command="]);
|
|
431
|
-
return parseProcessList(stdout)
|
|
553
|
+
return parseProcessList(stdout)
|
|
554
|
+
.filter(({ command }) => isExactCodexProcess(command, installation))
|
|
555
|
+
.map((processInfo) => ({
|
|
556
|
+
...processInfo,
|
|
557
|
+
appPath: installation.appPath,
|
|
558
|
+
executable: installation.executable,
|
|
559
|
+
bundleIdentifier: installation.bundleIdentifier ?? CODEX_BUNDLE_IDENTIFIER,
|
|
560
|
+
signingIdentity: installation.signingIdentity ?? null,
|
|
561
|
+
verified: installation.verified !== false,
|
|
562
|
+
}));
|
|
432
563
|
}
|
|
433
564
|
|
|
434
565
|
async ownedCodexProcesses(paths) {
|
|
435
566
|
const processes = await this.listCodexProcesses();
|
|
436
|
-
|
|
567
|
+
let marker = null;
|
|
568
|
+
try {
|
|
569
|
+
marker = parseCodexOwnerMarker(await this.fileSystem.readFile(paths.codexPidPath, "utf8"));
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error.code !== "ENOENT") throw error;
|
|
572
|
+
}
|
|
573
|
+
return processes.filter(({ pid, command, verified }) => verified === true && (
|
|
574
|
+
hasManagedProfile(command, paths.profileDir)
|
|
575
|
+
|| (
|
|
576
|
+
pid === marker?.pid
|
|
577
|
+
&& command === marker.command
|
|
578
|
+
&& hasLoopbackCdpArguments(command, extractCdpPort(command))
|
|
579
|
+
)
|
|
580
|
+
));
|
|
437
581
|
}
|
|
438
582
|
|
|
439
583
|
async assertOwnedCodexStopped(paths) {
|
|
@@ -457,45 +601,92 @@ export class MacPlatformAdapter {
|
|
|
457
601
|
}
|
|
458
602
|
|
|
459
603
|
async terminateOwnedCodex(paths) {
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
pid = Number((await this.fileSystem.readFile(paths.codexPidPath, "utf8")).trim());
|
|
463
|
-
} catch (error) {
|
|
464
|
-
if (error.code === "ENOENT") {
|
|
465
|
-
await this.assertOwnedCodexStopped(paths);
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
throw error;
|
|
469
|
-
}
|
|
470
|
-
if (!Number.isInteger(pid) || pid <= 1) {
|
|
471
|
-
await this.assertOwnedCodexStopped(paths);
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
const processInfo = (await this.ownedCodexProcesses(paths)).find((candidate) => candidate.pid === pid);
|
|
604
|
+
const processInfo = (await this.ownedCodexProcesses(paths))[0];
|
|
475
605
|
if (!processInfo) {
|
|
476
606
|
await this.assertOwnedCodexStopped(paths);
|
|
477
607
|
return false;
|
|
478
608
|
}
|
|
479
|
-
this.processApi.kill(pid, "SIGTERM");
|
|
480
|
-
await waitForProcessExit(pid, { processApi: this.processApi, sleep: this.sleep });
|
|
609
|
+
this.processApi.kill(processInfo.pid, "SIGTERM");
|
|
610
|
+
await waitForProcessExit(processInfo.pid, { processApi: this.processApi, sleep: this.sleep });
|
|
481
611
|
await this.fileSystem.unlink(paths.codexPidPath).catch(() => {});
|
|
482
612
|
await this.assertOwnedCodexStopped(paths);
|
|
483
613
|
return true;
|
|
484
614
|
}
|
|
485
615
|
|
|
486
|
-
async terminateNativeCodex() {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
616
|
+
async terminateNativeCodex(pid, expectedCommand = null) {
|
|
617
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
618
|
+
throw new Error("MineCodex requires an exact ChatGPT PID to terminate.");
|
|
619
|
+
}
|
|
620
|
+
const processInfo = (await this.listCodexProcesses()).find((candidate) => (
|
|
621
|
+
candidate.pid === pid
|
|
622
|
+
&& candidate.verified === true
|
|
623
|
+
&& !candidate.command.includes("--user-data-dir=")
|
|
624
|
+
&& (!expectedCommand || candidate.command === expectedCommand)
|
|
625
|
+
));
|
|
626
|
+
if (!processInfo) return 0;
|
|
627
|
+
const { stdout: processTree } = await this.execFile("/bin/ps", ["-axo", "pid=,ppid=,command="]);
|
|
628
|
+
const ownedProcessTree = [pid, ...descendantPids(processTree, pid)];
|
|
629
|
+
this.processApi.kill(pid, "SIGTERM");
|
|
630
|
+
await waitForProcessesExit(ownedProcessTree, { processApi: this.processApi, sleep: this.sleep });
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async assertNativeCodexCdp(processInfo, cdpPort) {
|
|
635
|
+
if (!processInfo?.verified || !Number.isInteger(processInfo.pid) || !hasLoopbackCdpArguments(processInfo.command, cdpPort)) {
|
|
636
|
+
throw new Error("MineCodex could not bind the loopback CDP endpoint to a verified ChatGPT process.");
|
|
637
|
+
}
|
|
638
|
+
const { stdout } = await this.execFile("/usr/sbin/lsof", [
|
|
639
|
+
"-nP", "-a", "-p", String(processInfo.pid), `-iTCP:${cdpPort}`, "-sTCP:LISTEN", "-Fn",
|
|
640
|
+
]);
|
|
641
|
+
const fields = String(stdout).split("\n");
|
|
642
|
+
if (!fields.includes(`p${processInfo.pid}`) || !fields.includes(`n127.0.0.1:${cdpPort}`)) {
|
|
643
|
+
throw new Error(`ChatGPT pid ${processInfo.pid} does not own loopback CDP port ${cdpPort}.`);
|
|
644
|
+
}
|
|
645
|
+
return true;
|
|
491
646
|
}
|
|
492
647
|
|
|
493
648
|
async openBrowser(url) {
|
|
494
649
|
await this.execFile("/usr/bin/open", [url]);
|
|
495
650
|
}
|
|
496
651
|
|
|
497
|
-
async openNativeCodex() {
|
|
652
|
+
async openNativeCodex({ cdpPort } = {}) {
|
|
498
653
|
const installation = await this.codex();
|
|
499
|
-
|
|
654
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
|
|
655
|
+
await this.execFile("/usr/bin/open", ["-a", installation.appPath]);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const existingPids = new Set((await this.listCodexProcesses()).map(({ pid }) => pid));
|
|
659
|
+
await this.execFile("/usr/bin/open", [
|
|
660
|
+
"-n",
|
|
661
|
+
"-a",
|
|
662
|
+
installation.appPath,
|
|
663
|
+
"--args",
|
|
664
|
+
"--remote-debugging-address=127.0.0.1",
|
|
665
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
666
|
+
`--remote-allow-origins=http://127.0.0.1:${cdpPort}`,
|
|
667
|
+
]);
|
|
668
|
+
return this.waitForNativeCodex({ cdpPort, excludedPids: existingPids });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async waitForNativeCodex({ cdpPort, excludedPids = new Set(), timeoutMs = 15_000 } = {}) {
|
|
672
|
+
const deadline = Date.now() + timeoutMs;
|
|
673
|
+
while (Date.now() < deadline) {
|
|
674
|
+
const candidates = (await this.listCodexProcesses()).filter(({ pid, command, verified }) => (
|
|
675
|
+
verified === true
|
|
676
|
+
&& !excludedPids.has(pid)
|
|
677
|
+
&& !command.includes("--user-data-dir=")
|
|
678
|
+
&& hasLoopbackCdpArguments(command, cdpPort)
|
|
679
|
+
));
|
|
680
|
+
if (candidates.length > 1) {
|
|
681
|
+
throw new Error(`LaunchServices exposed multiple new ChatGPT processes on CDP port ${cdpPort}.`);
|
|
682
|
+
}
|
|
683
|
+
const processInfo = candidates[0];
|
|
684
|
+
if (processInfo) {
|
|
685
|
+
await this.assertNativeCodexCdp(processInfo, cdpPort);
|
|
686
|
+
return processInfo;
|
|
687
|
+
}
|
|
688
|
+
await this.sleep?.(50);
|
|
689
|
+
}
|
|
690
|
+
throw new Error(`LaunchServices did not expose a loopback CDP ChatGPT process on port ${cdpPort}.`);
|
|
500
691
|
}
|
|
501
692
|
}
|