codeksei 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +661 -0
  2. package/README.en.md +215 -0
  3. package/README.md +259 -0
  4. package/bin/codeksei.js +10 -0
  5. package/bin/cyberboss.js +11 -0
  6. package/package.json +86 -0
  7. package/scripts/install-background-tasks.ps1 +135 -0
  8. package/scripts/open_shared_wechat_thread.sh +94 -0
  9. package/scripts/open_wechat_thread.sh +117 -0
  10. package/scripts/shared-common.js +791 -0
  11. package/scripts/shared-open.js +46 -0
  12. package/scripts/shared-start.js +41 -0
  13. package/scripts/shared-status.js +74 -0
  14. package/scripts/shared-supervisor.js +141 -0
  15. package/scripts/shared-task-runner.ps1 +87 -0
  16. package/scripts/shared-watchdog.js +290 -0
  17. package/scripts/show_shared_status.sh +53 -0
  18. package/scripts/start_shared_app_server.sh +65 -0
  19. package/scripts/start_shared_wechat.sh +108 -0
  20. package/scripts/timeline-screenshot.sh +15 -0
  21. package/scripts/uninstall-background-tasks.ps1 +23 -0
  22. package/src/adapters/channel/weixin/account-store.js +135 -0
  23. package/src/adapters/channel/weixin/api-v2.js +258 -0
  24. package/src/adapters/channel/weixin/api.js +180 -0
  25. package/src/adapters/channel/weixin/context-token-store.js +84 -0
  26. package/src/adapters/channel/weixin/index.js +605 -0
  27. package/src/adapters/channel/weixin/legacy.js +567 -0
  28. package/src/adapters/channel/weixin/login-common.js +63 -0
  29. package/src/adapters/channel/weixin/login-legacy.js +124 -0
  30. package/src/adapters/channel/weixin/login-v2.js +186 -0
  31. package/src/adapters/channel/weixin/media-mime.js +22 -0
  32. package/src/adapters/channel/weixin/media-receive.js +370 -0
  33. package/src/adapters/channel/weixin/media-send.js +331 -0
  34. package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
  35. package/src/adapters/channel/weixin/message-utils.js +199 -0
  36. package/src/adapters/channel/weixin/protocol.js +77 -0
  37. package/src/adapters/channel/weixin/redact.js +41 -0
  38. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
  39. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
  40. package/src/adapters/runtime/codex/events.js +252 -0
  41. package/src/adapters/runtime/codex/index.js +502 -0
  42. package/src/adapters/runtime/codex/message-utils.js +141 -0
  43. package/src/adapters/runtime/codex/model-catalog.js +106 -0
  44. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
  45. package/src/adapters/runtime/codex/rpc-client.js +443 -0
  46. package/src/adapters/runtime/codex/session-store.js +376 -0
  47. package/src/app/channel-send-file-cli.js +57 -0
  48. package/src/app/diary-write-cli.js +620 -0
  49. package/src/app/note-auto-cli.js +201 -0
  50. package/src/app/note-sync-cli.js +130 -0
  51. package/src/app/project-radar-cli.js +165 -0
  52. package/src/app/reminder-write-cli.js +210 -0
  53. package/src/app/review-cli.js +134 -0
  54. package/src/app/system-checkin-poller.js +100 -0
  55. package/src/app/system-send-cli.js +129 -0
  56. package/src/app/timeline-event-cli.js +273 -0
  57. package/src/app/timeline-screenshot-cli.js +109 -0
  58. package/src/core/app.js +1810 -0
  59. package/src/core/branding.js +167 -0
  60. package/src/core/command-registry.js +609 -0
  61. package/src/core/config.js +84 -0
  62. package/src/core/default-targets.js +163 -0
  63. package/src/core/durable-note-schema.js +325 -0
  64. package/src/core/instructions-template.js +31 -0
  65. package/src/core/note-sync.js +433 -0
  66. package/src/core/project-radar.js +402 -0
  67. package/src/core/review-semantic.js +524 -0
  68. package/src/core/review.js +1081 -0
  69. package/src/core/shared-bridge-heartbeat.js +140 -0
  70. package/src/core/stream-delivery.js +990 -0
  71. package/src/core/system-message-dispatcher.js +68 -0
  72. package/src/core/system-message-queue-store.js +128 -0
  73. package/src/core/thread-state-store.js +135 -0
  74. package/src/core/timeline-screenshot-queue-store.js +134 -0
  75. package/src/core/workspace-alias.js +163 -0
  76. package/src/core/workspace-bootstrap.js +338 -0
  77. package/src/index.js +270 -0
  78. package/src/integrations/timeline/index.js +191 -0
  79. package/templates/weixin-instructions.md +53 -0
  80. package/templates/weixin-operations.md +69 -0
@@ -0,0 +1,791 @@
1
+ const fs = require("fs");
2
+ const http = require("http");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const { execFileSync, spawn } = require("child_process");
6
+ const dotenv = require("dotenv");
7
+ const {
8
+ APP_NAME,
9
+ PACKAGE_NAME,
10
+ ensureStateDirectory,
11
+ listEnvFileCandidates,
12
+ readPrefixedBoolEnv,
13
+ readPrefixedEnv,
14
+ resolveStateDir,
15
+ } = require("../src/core/branding");
16
+ const {
17
+ DEFAULT_SHARED_BRIDGE_HEARTBEAT_MAX_AGE_MS,
18
+ classifySharedBridgeHeartbeat,
19
+ readSharedBridgeHeartbeat,
20
+ } = require("../src/core/shared-bridge-heartbeat");
21
+
22
+ const rootDir = path.resolve(__dirname, "..");
23
+ loadSharedEnv();
24
+ const port = String(readPrefixedEnv(process.env, "SHARED_PORT") || "8765");
25
+ const listenUrl = `ws://127.0.0.1:${port}`;
26
+ const stateDir = resolveStateDir({ env: process.env });
27
+ const logDir = path.join(stateDir, "logs");
28
+ const appServerPidFile = path.join(logDir, "shared-app-server.pid");
29
+ const bridgePidFile = path.join(logDir, "shared-wechat.pid");
30
+ const supervisorPidFile = path.join(logDir, "shared-supervisor.pid");
31
+ const appServerLogFile = path.join(logDir, "shared-app-server.log");
32
+ const bridgeLogFile = path.join(logDir, "shared-wechat.log");
33
+ const supervisorLogFile = path.join(logDir, "shared-supervisor.log");
34
+ const bridgeHeartbeatFile = path.join(logDir, "shared-wechat-heartbeat.json");
35
+ const watchdogStateFile = path.join(logDir, "shared-watchdog-state.json");
36
+ const accountsDir = path.join(stateDir, "accounts");
37
+ const sessionFile = readPrefixedEnv(process.env, "SESSIONS_FILE") || path.join(stateDir, "sessions.json");
38
+ const WINDOWS_CMD_SUFFIX_RE = /\.(cmd|bat)$/i;
39
+ const WINDOWS_EXE_SUFFIX_RE = /\.(exe|com)$/i;
40
+ const BRIDGE_HEARTBEAT_MAX_AGE_MS = Number.parseInt(
41
+ String(readPrefixedEnv(process.env, "SHARED_BRIDGE_HEARTBEAT_MAX_AGE_MS") || ""),
42
+ 10
43
+ ) || DEFAULT_SHARED_BRIDGE_HEARTBEAT_MAX_AGE_MS;
44
+ const SHARED_USE_BUNDLED_CODEX_BINARY = readPrefixedBoolEnv(
45
+ process.env,
46
+ "SHARED_USE_BUNDLED_CODEX_BINARY",
47
+ true
48
+ );
49
+ const SHARED_DISABLE_PLUGINS = readPrefixedBoolEnv(
50
+ process.env,
51
+ "SHARED_DISABLE_PLUGINS",
52
+ false
53
+ );
54
+ const SHARED_DISABLE_SHELL_SNAPSHOT = readPrefixedBoolEnv(
55
+ process.env,
56
+ "SHARED_DISABLE_SHELL_SNAPSHOT",
57
+ false
58
+ );
59
+
60
+ function loadSharedEnv() {
61
+ ensureStateDirectory({ env: process.env });
62
+ const candidates = listEnvFileCandidates({ cwd: rootDir, env: process.env });
63
+ for (const envPath of candidates) {
64
+ if (!fs.existsSync(envPath)) {
65
+ continue;
66
+ }
67
+ dotenv.config({ path: envPath });
68
+ return;
69
+ }
70
+ }
71
+
72
+ function ensureLogDir() {
73
+ fs.mkdirSync(logDir, { recursive: true });
74
+ }
75
+
76
+ function isPidAlive(pid) {
77
+ const numeric = Number(pid);
78
+ if (!Number.isInteger(numeric) || numeric <= 0) {
79
+ return false;
80
+ }
81
+ try {
82
+ process.kill(numeric, 0);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ function readPidFile(filePath) {
90
+ try {
91
+ const raw = fs.readFileSync(filePath, "utf8").trim();
92
+ return raw ? Number.parseInt(raw, 10) : 0;
93
+ } catch {
94
+ return 0;
95
+ }
96
+ }
97
+
98
+ function writePidFile(filePath, pid) {
99
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
100
+ fs.writeFileSync(filePath, `${pid}\n`, "utf8");
101
+ }
102
+
103
+ function removePidFileIfMatches(filePath, pid) {
104
+ const current = readPidFile(filePath);
105
+ if (current && current === pid) {
106
+ fs.rmSync(filePath, { force: true });
107
+ }
108
+ }
109
+
110
+ function readJsonFile(filePath) {
111
+ try {
112
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function writeJsonFile(filePath, payload) {
119
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
120
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
121
+ }
122
+
123
+ function findListeningPidByPort(targetPort) {
124
+ const normalizedPort = Number(targetPort);
125
+ if (!Number.isInteger(normalizedPort) || normalizedPort <= 0) {
126
+ return 0;
127
+ }
128
+ try {
129
+ if (process.platform === "win32") {
130
+ const output = execFileSync("netstat", ["-ano", "-p", "tcp"], {
131
+ encoding: "utf8",
132
+ stdio: ["ignore", "pipe", "ignore"],
133
+ windowsHide: true,
134
+ });
135
+ for (const rawLine of output.split(/\r?\n/)) {
136
+ const line = rawLine.trim();
137
+ if (!line || !line.startsWith("TCP")) {
138
+ continue;
139
+ }
140
+ const parts = line.split(/\s+/);
141
+ if (parts.length < 5) {
142
+ continue;
143
+ }
144
+ const [, localAddress, , state, pidText] = parts;
145
+ if (state.toUpperCase() !== "LISTENING") {
146
+ continue;
147
+ }
148
+ if (!localAddress.endsWith(`:${normalizedPort}`)) {
149
+ continue;
150
+ }
151
+ const pid = Number.parseInt(pidText, 10);
152
+ return isPidAlive(pid) ? pid : 0;
153
+ }
154
+ return 0;
155
+ }
156
+
157
+ const output = execFileSync("lsof", [`-nPiTCP:${normalizedPort}`, "-sTCP:LISTEN", "-t"], {
158
+ encoding: "utf8",
159
+ stdio: ["ignore", "pipe", "ignore"],
160
+ });
161
+ const pid = Number.parseInt(output.trim().split(/\r?\n/)[0] || "", 10);
162
+ return isPidAlive(pid) ? pid : 0;
163
+ } catch {
164
+ return 0;
165
+ }
166
+ }
167
+
168
+ function checkReadyz() {
169
+ return new Promise((resolve) => {
170
+ const req = http.get(
171
+ {
172
+ hostname: "127.0.0.1",
173
+ port: Number(port),
174
+ path: "/readyz",
175
+ timeout: 500,
176
+ },
177
+ (res) => {
178
+ res.resume();
179
+ resolve(res.statusCode >= 200 && res.statusCode < 300);
180
+ }
181
+ );
182
+ req.on("error", () => resolve(false));
183
+ req.on("timeout", () => {
184
+ req.destroy();
185
+ resolve(false);
186
+ });
187
+ });
188
+ }
189
+
190
+ async function resolveReadyAppServerPid() {
191
+ if (!(await checkReadyz())) {
192
+ return 0;
193
+ }
194
+
195
+ const pidFromFile = readPidFile(appServerPidFile);
196
+ // On Windows a detached shell launch can leave the pid file pointing at the
197
+ // wrapper cmd.exe instead of the real codex.exe listener. Prefer the actual
198
+ // listening pid whenever readyz is healthy, then backfill the pid file.
199
+ const listenerPid = findListeningPidByPort(port);
200
+ if (listenerPid) {
201
+ if (listenerPid !== pidFromFile) {
202
+ writePidFile(appServerPidFile, listenerPid);
203
+ }
204
+ return listenerPid;
205
+ }
206
+
207
+ return pidFromFile && isPidAlive(pidFromFile) ? pidFromFile : 0;
208
+ }
209
+
210
+ async function waitForReadyz({ attempts = 10, delayMs = 300 } = {}) {
211
+ for (let index = 0; index < attempts; index += 1) {
212
+ if (await checkReadyz()) {
213
+ return true;
214
+ }
215
+ await sleep(delayMs);
216
+ }
217
+ return false;
218
+ }
219
+
220
+ function openLogFile(filePath) {
221
+ return fs.openSync(filePath, "a");
222
+ }
223
+
224
+ function resolveSpawnCommand(command) {
225
+ const normalized = normalizeText(command);
226
+ if (!normalized || process.platform !== "win32") {
227
+ return normalized || command;
228
+ }
229
+ if (path.isAbsolute(normalized) && fs.existsSync(normalized)) {
230
+ return normalized;
231
+ }
232
+ try {
233
+ const output = execFileSync("where.exe", [normalized], {
234
+ encoding: "utf8",
235
+ stdio: ["ignore", "pipe", "ignore"],
236
+ windowsHide: true,
237
+ });
238
+ const candidates = output
239
+ .split(/\r?\n/)
240
+ .map((line) => line.trim())
241
+ .filter(Boolean);
242
+ const preferred = candidates.find(isPreferredWindowsCmdShim)
243
+ || candidates.find((candidate) => WINDOWS_EXE_SUFFIX_RE.test(candidate) && !isWindowsAppsPath(candidate))
244
+ || candidates.find((candidate) => WINDOWS_CMD_SUFFIX_RE.test(candidate))
245
+ || candidates.find((candidate) => WINDOWS_EXE_SUFFIX_RE.test(candidate))
246
+ || candidates[0];
247
+ return preferred || normalized;
248
+ } catch {
249
+ return normalized;
250
+ }
251
+ }
252
+
253
+ function isPreferredWindowsCmdShim(candidate) {
254
+ const normalized = String(candidate || "").trim().toLowerCase();
255
+ return WINDOWS_CMD_SUFFIX_RE.test(normalized) && normalized.includes("\\appdata\\roaming\\npm\\");
256
+ }
257
+
258
+ function isWindowsAppsPath(candidate) {
259
+ return String(candidate || "").trim().toLowerCase().includes("\\windowsapps\\");
260
+ }
261
+
262
+ function resolveCodexTargetTriple() {
263
+ if (process.platform !== "win32") {
264
+ return "";
265
+ }
266
+ if (process.arch === "x64") {
267
+ return "x86_64-pc-windows-msvc";
268
+ }
269
+ if (process.arch === "arm64") {
270
+ return "aarch64-pc-windows-msvc";
271
+ }
272
+ return "";
273
+ }
274
+
275
+ function resolveCodexPlatformPackageName() {
276
+ if (process.platform !== "win32") {
277
+ return "";
278
+ }
279
+ if (process.arch === "x64") {
280
+ return "codex-win32-x64";
281
+ }
282
+ if (process.arch === "arm64") {
283
+ return "codex-win32-arm64";
284
+ }
285
+ return "";
286
+ }
287
+
288
+ function resolveBundledCodexBinary(command) {
289
+ if (process.platform !== "win32") {
290
+ return "";
291
+ }
292
+ const resolvedCommand = resolveSpawnCommand(command);
293
+ const normalizedResolved = normalizeText(resolvedCommand).toLowerCase();
294
+ if (
295
+ !normalizedResolved
296
+ || (!normalizedResolved.endsWith("\\codex.cmd")
297
+ && !normalizedResolved.endsWith("\\codex")
298
+ && normalizedResolved !== "codex")
299
+ ) {
300
+ return "";
301
+ }
302
+
303
+ const npmRoot = normalizedResolved.includes("\\appdata\\roaming\\npm\\")
304
+ ? path.dirname(resolvedCommand)
305
+ : "";
306
+ const targetTriple = resolveCodexTargetTriple();
307
+ const platformPackage = resolveCodexPlatformPackageName();
308
+ if (!npmRoot || !targetTriple || !platformPackage) {
309
+ return "";
310
+ }
311
+
312
+ const candidate = path.join(
313
+ npmRoot,
314
+ "node_modules",
315
+ "@openai",
316
+ "codex",
317
+ "node_modules",
318
+ "@openai",
319
+ platformPackage,
320
+ "vendor",
321
+ targetTriple,
322
+ "codex",
323
+ "codex.exe"
324
+ );
325
+ return fs.existsSync(candidate) ? candidate : "";
326
+ }
327
+
328
+ function quoteWindowsCmdArg(value) {
329
+ const text = String(value ?? "");
330
+ if (!text.length) {
331
+ return "\"\"";
332
+ }
333
+ if (!/[\s"]/u.test(text)) {
334
+ return text;
335
+ }
336
+ const escaped = text.replace(/(\\*)"/g, "$1$1\\\"");
337
+ return `"${escaped.replace(/(\\+)$/g, "$1$1")}"`;
338
+ }
339
+
340
+ function buildSpawnInvocation(command, args = []) {
341
+ const resolvedCommand = resolveSpawnCommand(command);
342
+ if (process.platform === "win32" && WINDOWS_CMD_SUFFIX_RE.test(resolvedCommand)) {
343
+ return {
344
+ command: "cmd.exe",
345
+ args: ["/d", "/s", "/c", [resolvedCommand, ...args].map(quoteWindowsCmdArg).join(" ")],
346
+ };
347
+ }
348
+ return {
349
+ command: resolvedCommand,
350
+ args,
351
+ };
352
+ }
353
+
354
+ function spawnDetachedCommand(command, args, { logFile, cwd = rootDir, env = {} } = {}) {
355
+ const stdoutFd = openLogFile(logFile);
356
+ const stderrFd = openLogFile(logFile);
357
+ const spawnSpec = buildSpawnInvocation(command, args);
358
+ const child = spawn(spawnSpec.command, spawnSpec.args, {
359
+ cwd,
360
+ env: { ...process.env, ...env },
361
+ detached: true,
362
+ stdio: ["ignore", stdoutFd, stderrFd],
363
+ shell: false,
364
+ windowsHide: true,
365
+ });
366
+ child.unref();
367
+ return child.pid;
368
+ }
369
+
370
+ function readProcessCommandLine(pid) {
371
+ const numeric = Number(pid);
372
+ if (!Number.isInteger(numeric) || numeric <= 0) {
373
+ return "";
374
+ }
375
+ try {
376
+ if (process.platform === "win32") {
377
+ return execFileSync("powershell.exe", [
378
+ "-NoProfile",
379
+ "-Command",
380
+ `Get-CimInstance Win32_Process -Filter "ProcessId = ${numeric}" | Select-Object -ExpandProperty CommandLine`,
381
+ ], {
382
+ encoding: "utf8",
383
+ stdio: ["ignore", "pipe", "ignore"],
384
+ windowsHide: true,
385
+ }).trim();
386
+ }
387
+ return execFileSync("ps", ["-p", String(numeric), "-o", "command="], {
388
+ encoding: "utf8",
389
+ stdio: ["ignore", "pipe", "ignore"],
390
+ }).trim();
391
+ } catch {
392
+ return "";
393
+ }
394
+ }
395
+
396
+ function commandLineMatchesAll(commandLine, expectedSubstrings = []) {
397
+ const normalizedCommandLine = normalizeText(commandLine).toLowerCase();
398
+ const normalizedPatterns = expectedSubstrings
399
+ .map((value) => normalizeText(value).toLowerCase())
400
+ .filter(Boolean);
401
+ if (!normalizedPatterns.length) {
402
+ return true;
403
+ }
404
+ if (!normalizedCommandLine) {
405
+ return false;
406
+ }
407
+ return normalizedPatterns.every((pattern) => normalizedCommandLine.includes(pattern));
408
+ }
409
+
410
+ async function stopManagedProcess(filePath, { expectedSubstrings = [], label = "managed process" } = {}) {
411
+ const pid = readPidFile(filePath);
412
+ if (!pid) {
413
+ return { pid: 0, status: "missing", commandLine: "" };
414
+ }
415
+ if (!isPidAlive(pid)) {
416
+ fs.rmSync(filePath, { force: true });
417
+ return { pid, status: "stale", commandLine: "" };
418
+ }
419
+
420
+ const commandLine = readProcessCommandLine(pid);
421
+ // Only terminate processes we can positively attribute to this shared stack.
422
+ // PID reuse on a long-lived desktop session is real, so a naked "kill pidfile"
423
+ // would eventually hit an unrelated process.
424
+ if (!commandLineMatchesAll(commandLine, expectedSubstrings)) {
425
+ return { pid, status: "unexpected_command", commandLine };
426
+ }
427
+
428
+ try {
429
+ process.kill(pid);
430
+ } catch {}
431
+
432
+ for (let index = 0; index < 10; index += 1) {
433
+ if (!isPidAlive(pid)) {
434
+ fs.rmSync(filePath, { force: true });
435
+ return { pid, status: "terminated", commandLine };
436
+ }
437
+ await sleep(200);
438
+ }
439
+
440
+ try {
441
+ if (process.platform === "win32") {
442
+ execFileSync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
443
+ stdio: ["ignore", "ignore", "ignore"],
444
+ windowsHide: true,
445
+ });
446
+ } else {
447
+ process.kill(pid, "SIGKILL");
448
+ }
449
+ } catch {}
450
+
451
+ for (let index = 0; index < 10; index += 1) {
452
+ if (!isPidAlive(pid)) {
453
+ fs.rmSync(filePath, { force: true });
454
+ return { pid, status: "terminated", commandLine };
455
+ }
456
+ await sleep(200);
457
+ }
458
+
459
+ throw new Error(`failed to stop ${label} pid=${pid}`);
460
+ }
461
+
462
+ function readSharedBridgeHealth() {
463
+ const pid = readPidFile(bridgePidFile);
464
+ const alive = pid ? isPidAlive(pid) : false;
465
+ const heartbeat = readSharedBridgeHeartbeat(bridgeHeartbeatFile);
466
+ const classification = classifySharedBridgeHeartbeat(heartbeat, {
467
+ expectedPid: alive ? pid : 0,
468
+ maxAgeMs: BRIDGE_HEARTBEAT_MAX_AGE_MS,
469
+ });
470
+ return {
471
+ pid,
472
+ alive,
473
+ heartbeat,
474
+ classification,
475
+ healthy: Boolean(pid && alive && classification.healthy),
476
+ };
477
+ }
478
+
479
+ async function waitForSharedBridgeHealthy({ attempts = 30, delayMs = 1000 } = {}) {
480
+ for (let index = 0; index < attempts; index += 1) {
481
+ const health = readSharedBridgeHealth();
482
+ if (health.healthy) {
483
+ return health;
484
+ }
485
+ await sleep(delayMs);
486
+ }
487
+ return null;
488
+ }
489
+
490
+ function startSharedBridge() {
491
+ const pid = spawnDetachedCommand(process.execPath, ["./bin/codeksei.js", "start", "--checkin"], {
492
+ logFile: bridgeLogFile,
493
+ cwd: rootDir,
494
+ env: {
495
+ CODEKSEI_CODEX_ENDPOINT: listenUrl,
496
+ CYBERBOSS_CODEX_ENDPOINT: listenUrl,
497
+ },
498
+ });
499
+ writePidFile(bridgePidFile, pid);
500
+ return pid;
501
+ }
502
+
503
+ function startSharedSupervisor({ intervalMinutes = 5 } = {}) {
504
+ const args = ["./scripts/shared-supervisor.js", `--interval-minutes=${intervalMinutes}`];
505
+ const pid = spawnDetachedCommand(process.execPath, args, {
506
+ logFile: supervisorLogFile,
507
+ cwd: rootDir,
508
+ });
509
+ writePidFile(supervisorPidFile, pid);
510
+ return pid;
511
+ }
512
+
513
+ async function ensureSharedAppServer() {
514
+ ensureLogDir();
515
+ const readyPid = await resolveReadyAppServerPid();
516
+ if (readyPid) {
517
+ return { pid: readyPid, status: "already_running" };
518
+ }
519
+
520
+ const env = {
521
+ CODEKSEI_STATE_DIR: stateDir,
522
+ CYBERBOSS_STATE_DIR: stateDir,
523
+ TIMELINE_FOR_AGENT_STATE_DIR: stateDir,
524
+ };
525
+ if (!process.env.TIMELINE_FOR_AGENT_CHROME_PATH) {
526
+ env.TIMELINE_FOR_AGENT_CHROME_PATH =
527
+ readPrefixedEnv(process.env, "SCREENSHOT_CHROME_PATH")
528
+ || (process.platform === "darwin"
529
+ ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
530
+ : "");
531
+ }
532
+
533
+ const command = readPrefixedEnv(process.env, "CODEX_COMMAND") || "codex";
534
+ // Some Windows setups behave better when the shared app-server is launched
535
+ // through the npm codex.cmd shim, because later shell_command children can
536
+ // inherit that console environment instead of spawning a fresh visible one.
537
+ // Keep the direct codex.exe path as an opt-in/opt-out switch so we can A/B
538
+ // detached startup behavior without rewriting the shared lifecycle.
539
+ const detachedCommand = SHARED_USE_BUNDLED_CODEX_BINARY
540
+ ? (resolveBundledCodexBinary(command) || command)
541
+ : command;
542
+ const appServerArgs = ["app-server", "--listen", listenUrl];
543
+ // The shared WeChat bridge does not rely on Desktop-only curated plugins, so
544
+ // allow callers to disable plugin loading for this detached runtime. That
545
+ // avoids noisy startup sync/cache warnings without changing the user's global
546
+ // Codex Desktop plugin setup.
547
+ if (SHARED_DISABLE_PLUGINS) {
548
+ appServerArgs.push("--disable", "plugins");
549
+ }
550
+ // PowerShell shell snapshots are not supported upstream today. Disabling the
551
+ // feature for the shared runtime only suppresses the startup warning; normal
552
+ // shell command execution still works.
553
+ if (SHARED_DISABLE_SHELL_SNAPSHOT) {
554
+ appServerArgs.push("--disable", "shell_snapshot");
555
+ }
556
+
557
+ const pid = spawnDetachedCommand(detachedCommand, appServerArgs, {
558
+ logFile: appServerLogFile,
559
+ env,
560
+ });
561
+ writePidFile(appServerPidFile, pid);
562
+
563
+ const ready = await waitForReadyz();
564
+ if (!ready) {
565
+ throw new Error(`failed to start shared app-server; check ${appServerLogFile}`);
566
+ }
567
+
568
+ const adoptedPid = await resolveReadyAppServerPid();
569
+ return {
570
+ pid: adoptedPid || pid,
571
+ status: adoptedPid && adoptedPid !== pid ? "started_adopted" : "started",
572
+ };
573
+ }
574
+
575
+ async function ensureManagedAppServer({ restartUnhealthy = false } = {}) {
576
+ const readyPid = await resolveReadyAppServerPid();
577
+ if (readyPid) {
578
+ return { pid: readyPid, status: "already_running" };
579
+ }
580
+
581
+ let recovered = false;
582
+ if (restartUnhealthy && readPidFile(appServerPidFile)) {
583
+ const stopped = await stopManagedProcess(appServerPidFile, {
584
+ expectedSubstrings: ["codex", "app-server"],
585
+ label: "shared app-server",
586
+ });
587
+ if (stopped.status === "unexpected_command") {
588
+ throw new Error(`refusing to stop shared app-server pid=${stopped.pid}: unexpected command line`);
589
+ }
590
+ recovered = stopped.status === "terminated";
591
+ }
592
+
593
+ const started = await ensureSharedAppServer();
594
+ return {
595
+ ...started,
596
+ status: recovered ? "restarted" : started.status,
597
+ };
598
+ }
599
+
600
+ function ensureBridgeNotRunning() {
601
+ const pidFromFile = readPidFile(bridgePidFile);
602
+ if (pidFromFile && isPidAlive(pidFromFile)) {
603
+ return pidFromFile;
604
+ }
605
+ if (pidFromFile) {
606
+ fs.rmSync(bridgePidFile, { force: true });
607
+ }
608
+ return 0;
609
+ }
610
+
611
+ async function ensureManagedBridge({ restartUnhealthy = false } = {}) {
612
+ ensureLogDir();
613
+ const health = readSharedBridgeHealth();
614
+ if (health.healthy) {
615
+ return { pid: health.pid, status: "already_running", health };
616
+ }
617
+
618
+ if (health.pid && health.alive) {
619
+ const warmed = await waitForSharedBridgeHealthy({ attempts: 5, delayMs: 1000 });
620
+ if (warmed) {
621
+ return { pid: warmed.pid, status: "already_running", health: warmed };
622
+ }
623
+ }
624
+
625
+ let recovered = false;
626
+ if (restartUnhealthy && health.pid) {
627
+ const stopped = await stopManagedProcess(bridgePidFile, {
628
+ expectedSubstrings: ["start", "checkin"],
629
+ label: "shared codeksei bridge",
630
+ });
631
+ if (stopped.status === "unexpected_command") {
632
+ throw new Error(`refusing to stop shared codeksei bridge pid=${stopped.pid}: unexpected command line`);
633
+ }
634
+ recovered = stopped.status === "terminated";
635
+ } else if (health.pid && !health.alive) {
636
+ fs.rmSync(bridgePidFile, { force: true });
637
+ }
638
+
639
+ const pid = startSharedBridge();
640
+ const readyHealth = await waitForSharedBridgeHealthy();
641
+ if (!readyHealth) {
642
+ throw new Error(`failed to start shared codeksei bridge; check ${bridgeLogFile}`);
643
+ }
644
+ return {
645
+ pid: readyHealth.pid || pid,
646
+ status: recovered ? "restarted" : "started",
647
+ health: readyHealth,
648
+ };
649
+ }
650
+
651
+ async function ensureManagedSupervisor({ intervalMinutes = 5 } = {}) {
652
+ ensureLogDir();
653
+ const pid = readPidFile(supervisorPidFile);
654
+ if (pid && isPidAlive(pid)) {
655
+ const commandLine = readProcessCommandLine(pid);
656
+ if (!commandLineMatchesAll(commandLine, ["shared-supervisor.js"])) {
657
+ throw new Error(`refusing to adopt shared supervisor pid=${pid}: unexpected command line`);
658
+ }
659
+ return { pid, status: "already_running" };
660
+ }
661
+ if (pid) {
662
+ fs.rmSync(supervisorPidFile, { force: true });
663
+ }
664
+
665
+ const startedPid = startSharedSupervisor({ intervalMinutes });
666
+ await sleep(300);
667
+ return {
668
+ pid: readPidFile(supervisorPidFile) || startedPid,
669
+ status: "started",
670
+ };
671
+ }
672
+
673
+ function resolveCurrentAccountId() {
674
+ if (!fs.existsSync(accountsDir)) {
675
+ return "";
676
+ }
677
+ const entries = fs.readdirSync(accountsDir)
678
+ .filter((name) => name.endsWith(".json") && !name.endsWith(".context-tokens.json"))
679
+ .map((name) => {
680
+ const fullPath = path.join(accountsDir, name);
681
+ try {
682
+ const parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
683
+ return {
684
+ accountId: normalizeText(parsed?.accountId),
685
+ savedAt: parseTimestamp(parsed?.savedAt),
686
+ };
687
+ } catch {
688
+ return null;
689
+ }
690
+ })
691
+ .filter(Boolean)
692
+ .filter((entry) => entry.accountId);
693
+ entries.sort((left, right) => right.savedAt - left.savedAt);
694
+ return entries[0]?.accountId || "";
695
+ }
696
+
697
+ function resolveBoundThread(workspaceRoot) {
698
+ if (!fs.existsSync(sessionFile)) {
699
+ throw new Error(`session file not found: ${sessionFile}`);
700
+ }
701
+ const data = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
702
+ const currentAccountId = resolveCurrentAccountId();
703
+ const bindings = Object.values(data.bindings || {})
704
+ .filter((binding) => !currentAccountId || normalizeText(binding?.accountId) === currentAccountId)
705
+ .sort((left, right) => parseTimestamp(right?.updatedAt) - parseTimestamp(left?.updatedAt));
706
+
707
+ const normalizedWorkspaceRoot = normalizeText(workspaceRoot);
708
+ const exact = bindings.find((binding) => getThreadId(binding, normalizedWorkspaceRoot));
709
+ if (exact) {
710
+ return {
711
+ threadId: getThreadId(exact, normalizedWorkspaceRoot),
712
+ workspaceRoot: normalizedWorkspaceRoot,
713
+ };
714
+ }
715
+
716
+ const active = bindings.find((binding) => {
717
+ const activeWorkspaceRoot = normalizeText(binding?.activeWorkspaceRoot);
718
+ return activeWorkspaceRoot && getThreadId(binding, activeWorkspaceRoot);
719
+ });
720
+ if (active) {
721
+ const activeWorkspaceRoot = normalizeText(active.activeWorkspaceRoot);
722
+ return {
723
+ threadId: getThreadId(active, activeWorkspaceRoot),
724
+ workspaceRoot: activeWorkspaceRoot,
725
+ };
726
+ }
727
+
728
+ throw new Error(`no bound WeChat thread found for workspace: ${workspaceRoot}`);
729
+ }
730
+
731
+ function getThreadId(binding, workspaceRoot) {
732
+ if (!workspaceRoot) {
733
+ return "";
734
+ }
735
+ const map = binding && typeof binding.threadIdByWorkspaceRoot === "object"
736
+ ? binding.threadIdByWorkspaceRoot
737
+ : {};
738
+ return normalizeText(map[workspaceRoot]);
739
+ }
740
+
741
+ function parseTimestamp(value) {
742
+ const parsed = Date.parse(normalizeText(value));
743
+ return Number.isFinite(parsed) ? parsed : 0;
744
+ }
745
+
746
+ function normalizeText(value) {
747
+ return typeof value === "string" ? value.trim() : "";
748
+ }
749
+
750
+ function sleep(ms) {
751
+ return new Promise((resolve) => setTimeout(resolve, ms));
752
+ }
753
+
754
+ module.exports = {
755
+ rootDir,
756
+ port,
757
+ listenUrl,
758
+ stateDir,
759
+ logDir,
760
+ appServerPidFile,
761
+ bridgePidFile,
762
+ supervisorPidFile,
763
+ appServerLogFile,
764
+ bridgeLogFile,
765
+ supervisorLogFile,
766
+ bridgeHeartbeatFile,
767
+ watchdogStateFile,
768
+ BRIDGE_HEARTBEAT_MAX_AGE_MS,
769
+ ensureLogDir,
770
+ isPidAlive,
771
+ readPidFile,
772
+ writePidFile,
773
+ readJsonFile,
774
+ writeJsonFile,
775
+ removePidFileIfMatches,
776
+ buildSpawnInvocation,
777
+ spawnDetachedCommand,
778
+ readProcessCommandLine,
779
+ stopManagedProcess,
780
+ readSharedBridgeHealth,
781
+ waitForSharedBridgeHealthy,
782
+ startSharedBridge,
783
+ startSharedSupervisor,
784
+ resolveReadyAppServerPid,
785
+ ensureSharedAppServer,
786
+ ensureManagedAppServer,
787
+ ensureBridgeNotRunning,
788
+ ensureManagedBridge,
789
+ ensureManagedSupervisor,
790
+ resolveBoundThread,
791
+ };