@c0sc0s/codex-tags 0.5.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 (48) hide show
  1. package/.codex-plugin/plugin.json +24 -0
  2. package/AGENTS.md +44 -0
  3. package/CHANGELOG.md +75 -0
  4. package/README.md +87 -0
  5. package/README.zh-CN.md +87 -0
  6. package/assets/README.md +19 -0
  7. package/assets/banner.png +0 -0
  8. package/assets/icon.icns +0 -0
  9. package/assets/logo.png +0 -0
  10. package/bin/codex-tags.mjs +89 -0
  11. package/docs/architecture.md +56 -0
  12. package/docs/compatibility.md +47 -0
  13. package/docs/development.md +84 -0
  14. package/docs/distribution.md +65 -0
  15. package/docs/protocol.md +89 -0
  16. package/docs/roadmap.md +40 -0
  17. package/hooks/hooks.json +40 -0
  18. package/hooks/session-naming.mjs +107 -0
  19. package/package.json +65 -0
  20. package/runtime/dist/injected.js +3161 -0
  21. package/runtime/src/cdp-client.mjs +100 -0
  22. package/runtime/src/codex-process.mjs +115 -0
  23. package/runtime/src/content-index.mjs +138 -0
  24. package/runtime/src/controller-router.mjs +84 -0
  25. package/runtime/src/controller-state.mjs +17 -0
  26. package/runtime/src/controller.mjs +290 -0
  27. package/runtime/src/inject-expression.mjs +49 -0
  28. package/runtime/src/protocol.d.mts +31 -0
  29. package/runtime/src/protocol.mjs +43 -0
  30. package/runtime/src/runtime-target-registry.mjs +92 -0
  31. package/runtime/src/search-index.mjs +191 -0
  32. package/runtime/src/session-catalog.mjs +52 -0
  33. package/runtime/src/settings-repository.mjs +58 -0
  34. package/runtime/src/tag-settings.d.mts +18 -0
  35. package/runtime/src/tag-settings.mjs +65 -0
  36. package/runtime/src/title-format.d.mts +11 -0
  37. package/runtime/src/title-format.mjs +33 -0
  38. package/scripts/cli-options.mjs +17 -0
  39. package/scripts/health.mjs +20 -0
  40. package/scripts/lifecycle-lock.mjs +21 -0
  41. package/scripts/manage.mjs +19 -0
  42. package/scripts/manager-core.mjs +463 -0
  43. package/skills/doctor/SKILL.md +18 -0
  44. package/skills/doctor/agents/openai.yaml +4 -0
  45. package/skills/initial/SKILL.md +22 -0
  46. package/skills/initial/agents/openai.yaml +4 -0
  47. package/skills/rename/SKILL.md +20 -0
  48. package/skills/rename/agents/openai.yaml +4 -0
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { closeSync, openSync } from "node:fs";
4
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+
9
+ import { CdpClient } from "./cdp-client.mjs";
10
+ import { CodexProcess, findCodexApp } from "./codex-process.mjs";
11
+ import { ControllerRouter } from "./controller-router.mjs";
12
+ import { buildRuntimeMessageExpression, RUNTIME_BINDING, RUNTIME_VERSION } from "./inject-expression.mjs";
13
+ import { isOwnedControllerCommand, parseControllerPid } from "./controller-state.mjs";
14
+ import { createRuntimeMessage, RuntimeMessageType } from "./protocol.mjs";
15
+ import { SessionSearchIndex } from "./search-index.mjs";
16
+ import { SessionCatalog } from "./session-catalog.mjs";
17
+ import { SettingsRepository } from "./settings-repository.mjs";
18
+ import { RuntimeTargetRegistry } from "./runtime-target-registry.mjs";
19
+
20
+ process.umask(0o077);
21
+
22
+ const run = promisify(execFile);
23
+ const configuredPort = Number(process.env.CODEX_TAGS_CDP_PORT ?? 9341);
24
+ if (!Number.isSafeInteger(configuredPort) || configuredPort < 1024 || configuredPort > 65535) throw new Error("CODEX_TAGS_CDP_PORT must be an integer between 1024 and 65535");
25
+ const PORT = configuredPort;
26
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
27
+ const STATE_DIR = process.env.CODEX_TAGS_STATE_DIR ?? dirname(SCRIPT_PATH);
28
+ const PID_PATH = join(STATE_DIR, "controller.pid");
29
+ const LOG_PATH = join(STATE_DIR, "controller.log");
30
+ const SEARCH_DATABASE_PATH = join(STATE_DIR, "search.sqlite");
31
+ const SETTINGS_PATH = join(STATE_DIR, "settings.json");
32
+ const INDEX_REFRESH_INTERVAL_MS = 30_000;
33
+ const settingsRepository = new SettingsRepository(SETTINGS_PATH);
34
+ const codexProcess = new CodexProcess({ port: PORT });
35
+ const targetRegistry = new RuntimeTargetRegistry({
36
+ port: PORT,
37
+ ownsEndpoint: () => codexProcess.ownsCdpEndpoint(),
38
+ settingsRepository,
39
+ });
40
+
41
+ const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
42
+
43
+ async function apply() {
44
+ const { results } = await targetRegistry.ensureInjected(await settingsRepository.read());
45
+ return results;
46
+ }
47
+
48
+ async function readControllerPid() {
49
+ try {
50
+ return parseControllerPid(await readFile(PID_PATH, "utf8"));
51
+ } catch { return null; }
52
+ }
53
+
54
+ async function isOwnedController(pid) {
55
+ try {
56
+ const { stdout } = await run("/bin/ps", ["-p", String(pid), "-o", "command="]);
57
+ return isOwnedControllerCommand(stdout, SCRIPT_PATH);
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ async function stopController() {
64
+ const pid = await readControllerPid();
65
+ if (pid !== null && await isOwnedController(pid)) {
66
+ try { process.kill(pid, "SIGTERM"); } catch {}
67
+ const deadline = Date.now() + 3000;
68
+ while (Date.now() < deadline && await isOwnedController(pid)) await wait(50);
69
+ if (await isOwnedController(pid)) throw new Error("The controller did not stop. No files were removed; retry after it exits.");
70
+ }
71
+ await rm(PID_PATH, { force: true });
72
+ }
73
+
74
+ async function spawnController() {
75
+ await stopController();
76
+ const output = openSync(LOG_PATH, "a");
77
+ const child = spawn(process.execPath, [SCRIPT_PATH, "watch"], {
78
+ detached: true,
79
+ stdio: ["ignore", output, output],
80
+ });
81
+ child.unref();
82
+ closeSync(output);
83
+ }
84
+
85
+ async function start() {
86
+ if (!findCodexApp()) throw new Error("Install the official Codex desktop app before activating Tags.");
87
+ await mkdir(STATE_DIR, { recursive: true });
88
+ await stopController();
89
+ if (!(await codexProcess.cdpIsReady(() => targetRegistry.discover()))) {
90
+ if (await codexProcess.ownsCdpEndpoint() || await codexProcess.hasCdpLaunchArguments()) {
91
+ await codexProcess.waitForCdp(() => targetRegistry.discover());
92
+ } else {
93
+ if (await codexProcess.portIsListening()) throw new Error(`端口 ${PORT} 已被其他进程占用;未退出或修改 Codex`);
94
+ await codexProcess.launchWithCdp(() => targetRegistry.discover());
95
+ }
96
+ }
97
+ const results = await apply();
98
+ await spawnController();
99
+ console.log(`Codex Sidebar Tags ${RUNTIME_VERSION} 已启用`, results);
100
+ }
101
+
102
+ async function hotApply() {
103
+ await mkdir(STATE_DIR, { recursive: true });
104
+ await stopController();
105
+ const results = await apply();
106
+ await spawnController();
107
+ return results;
108
+ }
109
+
110
+ async function watch() {
111
+ await mkdir(STATE_DIR, { recursive: true });
112
+ const nextPidPath = `${PID_PATH}.next-${process.pid}`;
113
+ await writeFile(nextPidPath, `${JSON.stringify({ pid: process.pid, scriptPath: SCRIPT_PATH, startedAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600 });
114
+ await rename(nextPidPath, PID_PATH);
115
+ console.log(`${new Date().toISOString()} watching ${RUNTIME_VERSION}`);
116
+ let misses = 0;
117
+ const searchIndex = new SessionSearchIndex(SEARCH_DATABASE_PATH);
118
+ const catalog = new SessionCatalog();
119
+ let catalogState = await catalog.read();
120
+ let catalogSignature = JSON.stringify(catalogState);
121
+ let nextCatalogRefreshAt = 0;
122
+ const runtimeClients = new Map();
123
+ let indexStatus = { phase: "indexing", completed: 0, total: 0, changed: 0 };
124
+ let indexRefresh = Promise.resolve();
125
+ let nextIndexRefreshAt = 0;
126
+ let settingsState = await settingsRepository.read();
127
+ if (settingsState.error) console.error(`${new Date().toISOString()} ${settingsState.error}`);
128
+ const syncTagSettings = async (target) => {
129
+ if (settingsState.exists || settingsState.error) return;
130
+ const definitions = await targetRegistry.evaluate(target, "window.__codexSidebarTags?.tagDefinitions?.() ?? null");
131
+ if (!Array.isArray(definitions)) return;
132
+ const result = await settingsRepository.write(definitions);
133
+ settingsState = { settings: result.settings, exists: true, error: null };
134
+ };
135
+ const scheduleIndexRefresh = () => {
136
+ indexRefresh = indexRefresh
137
+ .then(() => searchIndex.refresh((status) => { indexStatus = status; }))
138
+ .catch((error) => {
139
+ indexStatus = { phase: "error", message: error.message };
140
+ console.error(`${new Date().toISOString()} search indexing failed: ${error.message}`);
141
+ });
142
+ nextIndexRefreshAt = Date.now() + INDEX_REFRESH_INTERVAL_MS;
143
+ };
144
+ scheduleIndexRefresh();
145
+
146
+ const sendRuntimeMessage = (client, message, executionContextId) => client.evaluate(
147
+ buildRuntimeMessageExpression(message),
148
+ executionContextId,
149
+ );
150
+
151
+ const broadcastSettings = async (settings) => {
152
+ const message = createRuntimeMessage(RuntimeMessageType.settingsSnapshot, { settings });
153
+ await Promise.allSettled([...runtimeClients.values()].map((client) => sendRuntimeMessage(client, message)));
154
+ };
155
+
156
+ const router = new ControllerRouter({
157
+ searchIndex,
158
+ settingsRepository,
159
+ getSettings: () => settingsState.settings,
160
+ onSettingsChanged: async (settings) => {
161
+ settingsState = { settings, exists: true, error: null };
162
+ await broadcastSettings(settings);
163
+ },
164
+ waitForIndex: () => indexRefresh,
165
+ getIndexStatus: () => indexStatus,
166
+ send: sendRuntimeMessage,
167
+ openSession: async (threadId) => {
168
+ if (catalogState.items.some((item) => item.threadId === threadId)) await run("/usr/bin/open", [`codex://threads/${threadId}`]);
169
+ },
170
+ });
171
+
172
+ const ensureRuntimeClient = async (target) => {
173
+ const existing = runtimeClients.get(target.id);
174
+ if (existing?.connected) return existing;
175
+ existing?.close();
176
+ const client = await CdpClient.connect(target);
177
+ await client.addBinding(RUNTIME_BINDING, (params) => router.handle(client, params));
178
+ runtimeClients.set(target.id, client);
179
+ await router.sendSettingsSnapshot(client);
180
+ await sendRuntimeMessage(client, createRuntimeMessage(RuntimeMessageType.catalogSnapshot, catalogState));
181
+ return client;
182
+ };
183
+
184
+ let cleanupPromise = null;
185
+ const clean = () => {
186
+ cleanupPromise ??= (async () => {
187
+ for (const client of runtimeClients.values()) client.close();
188
+ runtimeClients.clear();
189
+ searchIndex.close();
190
+ if ((await readControllerPid()) === process.pid) await rm(PID_PATH, { force: true });
191
+ })();
192
+ return cleanupPromise;
193
+ };
194
+ process.once("SIGTERM", () => { clean().finally(() => process.exit(0)); });
195
+ process.once("SIGINT", () => { clean().finally(() => process.exit(0)); });
196
+
197
+ try {
198
+ while (true) {
199
+ try {
200
+ const { targets } = await targetRegistry.ensureInjected(settingsState);
201
+ const currentTargetIds = new Set(targets.map(({ id }) => id));
202
+ for (const [targetId, client] of runtimeClients) {
203
+ if (currentTargetIds.has(targetId)) continue;
204
+ client.close();
205
+ runtimeClients.delete(targetId);
206
+ router.forgetTarget(targetId);
207
+ }
208
+ for (const target of targets) {
209
+ await ensureRuntimeClient(target);
210
+ await syncTagSettings(target);
211
+ }
212
+ if (Date.now() >= nextIndexRefreshAt) scheduleIndexRefresh();
213
+ if (Date.now() >= nextCatalogRefreshAt) {
214
+ catalogState = await catalog.read();
215
+ const signature = JSON.stringify(catalogState);
216
+ if (signature !== catalogSignature) {
217
+ catalogSignature = signature;
218
+ await Promise.allSettled([...runtimeClients.values()].map((client) => sendRuntimeMessage(client, createRuntimeMessage(RuntimeMessageType.catalogSnapshot, catalogState))));
219
+ }
220
+ nextCatalogRefreshAt = Date.now() + 5000;
221
+ }
222
+ misses = 0;
223
+ } catch (error) {
224
+ misses += 1;
225
+ if (misses === 1 || misses % 10 === 0) console.error(`${new Date().toISOString()} sync failed (${misses}): ${error.message}`);
226
+ if (misses >= 10 && !(await codexProcess.isRunning())) return;
227
+ }
228
+ await wait(1000);
229
+ }
230
+ } finally {
231
+ await clean();
232
+ }
233
+ }
234
+
235
+ async function restore() {
236
+ await stopController();
237
+ if (await codexProcess.cdpIsReady(() => targetRegistry.discover())) await targetRegistry.removeInjection();
238
+ console.log("侧栏标题增强已移除;Codex 安装包没有被修改。");
239
+ }
240
+
241
+ async function purge() {
242
+ await stopController();
243
+ if (await codexProcess.cdpIsReady(() => targetRegistry.discover())) {
244
+ for (const target of await targetRegistry.discover()) {
245
+ await targetRegistry.evaluate(target, `
246
+ localStorage.removeItem("codex-sidebar-tags-config-v1");
247
+ localStorage.removeItem("codex-sidebar-tags-index-v1");
248
+ `);
249
+ }
250
+ await targetRegistry.removeInjection();
251
+ }
252
+ console.log("侧栏增强及其浏览器缓存已移除;Codex 会话没有被修改。");
253
+ }
254
+
255
+ async function status() {
256
+ const ready = await codexProcess.cdpIsReady(() => targetRegistry.discover());
257
+ const storedPid = await readControllerPid();
258
+ const controllerPid = storedPid !== null && await isOwnedController(storedPid) ? storedPid : null;
259
+ const codexRunning = await codexProcess.isRunning();
260
+ const activeVersions = [];
261
+ const activeWindows = [];
262
+ if (ready) {
263
+ for (const target of await targetRegistry.discover()) {
264
+ activeVersions.push(await targetRegistry.evaluate(target, "window.__codexSidebarTags?.version ?? null"));
265
+ activeWindows.push(await targetRegistry.evaluate(target, "window.__codexSidebarTags?.status?.() ?? null"));
266
+ }
267
+ }
268
+ let searchIndex = null;
269
+ try {
270
+ const index = new SessionSearchIndex(SEARCH_DATABASE_PATH, { readOnly: true });
271
+ searchIndex = index.status();
272
+ index.close();
273
+ } catch (error) {
274
+ searchIndex = { error: error.message };
275
+ }
276
+ const tagSettingsState = await settingsRepository.read();
277
+ const tagSettings = tagSettingsState.exists ? tagSettingsState.settings : null;
278
+ const catalogState = await new SessionCatalog().read();
279
+ const catalog = { complete: catalogState.complete, count: catalogState.items.length, error: catalogState.error };
280
+ console.log(JSON.stringify({ codexRunning, cdp: ready, controllerPid, sourceVersion: RUNTIME_VERSION, activeVersions, activeWindows, searchIndex, catalog, tagSettings, tagSettingsError: tagSettingsState.error }, null, 2));
281
+ }
282
+
283
+ const command = process.argv[2] ?? "start";
284
+ if (command === "start") await start();
285
+ else if (command === "apply") console.log(await hotApply());
286
+ else if (command === "restore") await restore();
287
+ else if (command === "purge") await purge();
288
+ else if (command === "status") await status();
289
+ else if (command === "watch") await watch();
290
+ else throw new Error(`未知命令:${command}`);
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { DEFAULT_TAG_DEFINITIONS, LEGACY_TONE_COLORS, TAG_COLOR_PRESETS } from "./tag-settings.mjs";
6
+ import { createRuntimeMessage, RUNTIME_PROTOCOL_VERSION, RuntimeMessageType } from "./protocol.mjs";
7
+
8
+ export const RUNTIME_VERSION = "6.0.12";
9
+ export const RUNTIME_BINDING = "__codexTagsRequest";
10
+ export const SEARCH_BINDING = RUNTIME_BINDING;
11
+
12
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
13
+ const bundleCandidates = [
14
+ join(moduleDirectory, "dist", "injected.js"),
15
+ join(moduleDirectory, "..", "dist", "injected.js"),
16
+ ];
17
+ const bundlePath = bundleCandidates.find(existsSync);
18
+
19
+ if (!bundlePath) {
20
+ throw new Error("Codex Tags runtime bundle is missing. Run `npm run build` before installing.");
21
+ }
22
+
23
+ const injectedBundle = readFileSync(bundlePath, "utf8");
24
+
25
+ export function buildInjectionExpression(options = {}) {
26
+ const config = {
27
+ version: RUNTIME_VERSION,
28
+ protocolVersion: RUNTIME_PROTOCOL_VERSION,
29
+ tagDefinitions: options.tagDefinitions ?? DEFAULT_TAG_DEFINITIONS,
30
+ settingsSource: options.settingsSource ?? "defaults",
31
+ colorPresets: TAG_COLOR_PRESETS,
32
+ legacyToneColors: LEGACY_TONE_COLORS,
33
+ requestBinding: RUNTIME_BINDING,
34
+ };
35
+ return `(() => { ${injectedBundle}\nreturn CodexTagsInjected.installRuntime(${JSON.stringify(config)}); })()`;
36
+ }
37
+
38
+ export function buildRuntimeMessageExpression(message) {
39
+ return `window.__codexSidebarTags?.handleMessage?.(${JSON.stringify(message)}) ?? false`;
40
+ }
41
+
42
+ export function buildSearchResultExpression(result) {
43
+ const { type: _legacyType, requestId, ...payload } = result;
44
+ return buildRuntimeMessageExpression(createRuntimeMessage(RuntimeMessageType.searchResult, payload, requestId));
45
+ }
46
+
47
+ export function buildRemovalExpression() {
48
+ return "(() => { try { return window.__codexSidebarTags?.dispose?.() ?? false; } catch { return false; } })()";
49
+ }
@@ -0,0 +1,31 @@
1
+ export const RUNTIME_PROTOCOL_VERSION: 1;
2
+
3
+ export const RuntimeMessageType: Readonly<{
4
+ hello: "hello";
5
+ searchRequest: "search.request";
6
+ searchResult: "search.result";
7
+ settingsGet: "settings.get";
8
+ settingsSnapshot: "settings.snapshot";
9
+ settingsUpdate: "settings.update";
10
+ settingsError: "settings.error";
11
+ runtimeStatus: "runtime.status";
12
+ catalogSnapshot: "catalog.snapshot";
13
+ navigationOpen: "navigation.open";
14
+ }>;
15
+
16
+ export interface RuntimeMessage<TType extends string = string, TPayload extends Record<string, unknown> = Record<string, unknown>> {
17
+ protocolVersion: 1;
18
+ type: TType;
19
+ requestId?: number;
20
+ payload: TPayload;
21
+ }
22
+
23
+ export function createRuntimeMessage<TType extends string, TPayload extends Record<string, unknown>>(
24
+ type: TType,
25
+ payload?: TPayload,
26
+ requestId?: number,
27
+ ): RuntimeMessage<TType, TPayload>;
28
+
29
+ export function parseRuntimeMessage(value: unknown):
30
+ | { ok: true; message: RuntimeMessage }
31
+ | { ok: false; reason: "invalid-json" | "invalid-envelope" | "unsupported-version" | "invalid-type" | "invalid-payload" | "invalid-request-id" };
@@ -0,0 +1,43 @@
1
+ export const RUNTIME_PROTOCOL_VERSION = 1;
2
+
3
+ export const RuntimeMessageType = Object.freeze({
4
+ hello: "hello",
5
+ searchRequest: "search.request",
6
+ searchResult: "search.result",
7
+ settingsGet: "settings.get",
8
+ settingsSnapshot: "settings.snapshot",
9
+ settingsUpdate: "settings.update",
10
+ settingsError: "settings.error",
11
+ runtimeStatus: "runtime.status",
12
+ catalogSnapshot: "catalog.snapshot",
13
+ navigationOpen: "navigation.open",
14
+ });
15
+
16
+ export function createRuntimeMessage(type, payload = {}, requestId) {
17
+ if (typeof type !== "string" || !type) throw new TypeError("Runtime message type must be a non-empty string");
18
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new TypeError("Runtime message payload must be an object");
19
+ if (requestId !== undefined && !Number.isSafeInteger(requestId)) throw new TypeError("Runtime message requestId must be a safe integer");
20
+ return {
21
+ protocolVersion: RUNTIME_PROTOCOL_VERSION,
22
+ type,
23
+ ...(requestId === undefined ? {} : { requestId }),
24
+ payload,
25
+ };
26
+ }
27
+
28
+ export function parseRuntimeMessage(value) {
29
+ let candidate = value;
30
+ if (typeof candidate === "string") {
31
+ try {
32
+ candidate = JSON.parse(candidate);
33
+ } catch {
34
+ return { ok: false, reason: "invalid-json" };
35
+ }
36
+ }
37
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return { ok: false, reason: "invalid-envelope" };
38
+ if (candidate.protocolVersion !== RUNTIME_PROTOCOL_VERSION) return { ok: false, reason: "unsupported-version" };
39
+ if (typeof candidate.type !== "string" || !candidate.type) return { ok: false, reason: "invalid-type" };
40
+ if (candidate.payload === null || typeof candidate.payload !== "object" || Array.isArray(candidate.payload)) return { ok: false, reason: "invalid-payload" };
41
+ if (candidate.requestId !== undefined && !Number.isSafeInteger(candidate.requestId)) return { ok: false, reason: "invalid-request-id" };
42
+ return { ok: true, message: candidate };
43
+ }
@@ -0,0 +1,92 @@
1
+ import { buildInjectionExpression, buildRemovalExpression, RUNTIME_VERSION } from "./inject-expression.mjs";
2
+
3
+ export function isCodexRendererTarget(target) {
4
+ return target?.type === "page"
5
+ && typeof target.url === "string"
6
+ && target.url.startsWith("app://-")
7
+ && typeof target.webSocketDebuggerUrl === "string";
8
+ }
9
+
10
+ export class RuntimeTargetRegistry {
11
+ constructor(options) {
12
+ this.port = options.port;
13
+ this.ownsEndpoint = options.ownsEndpoint;
14
+ this.settingsRepository = options.settingsRepository;
15
+ }
16
+
17
+ async discover() {
18
+ const response = await fetch(`http://127.0.0.1:${this.port}/json/list`, {
19
+ redirect: "error",
20
+ signal: AbortSignal.timeout(800),
21
+ });
22
+ if (!response.ok) throw new Error(`CDP discovery failed: HTTP ${response.status}`);
23
+ const targets = await response.json();
24
+ return targets.filter(isCodexRendererTarget);
25
+ }
26
+
27
+ async evaluate(target, expression) {
28
+ const socket = new WebSocket(target.webSocketDebuggerUrl);
29
+ await new Promise((resolve, reject) => {
30
+ const timer = setTimeout(() => {
31
+ socket.close();
32
+ reject(new Error(`CDP websocket open timed out for target ${target.id}`));
33
+ }, 3000);
34
+ socket.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true });
35
+ socket.addEventListener("error", () => { clearTimeout(timer); reject(new Error(`CDP websocket open failed for target ${target.id}`)); }, { once: true });
36
+ });
37
+
38
+ try {
39
+ return await new Promise((resolve, reject) => {
40
+ const id = 1;
41
+ const timer = setTimeout(() => reject(new Error(`Runtime.evaluate timed out for target ${target.id}`)), 15_000);
42
+ socket.addEventListener("message", (event) => {
43
+ let message;
44
+ try {
45
+ message = JSON.parse(event.data);
46
+ } catch {
47
+ return;
48
+ }
49
+ if (message.id !== id) return;
50
+ clearTimeout(timer);
51
+ if (message.error) reject(new Error(message.error.message ?? "Runtime.evaluate failed"));
52
+ else if (message.result?.exceptionDetails) reject(new Error(message.result.exceptionDetails.text ?? "injected script failed"));
53
+ else resolve(message.result?.result?.value);
54
+ });
55
+ socket.send(JSON.stringify({
56
+ id,
57
+ method: "Runtime.evaluate",
58
+ params: { expression, awaitPromise: true, returnByValue: true },
59
+ }));
60
+ });
61
+ } finally {
62
+ socket.close();
63
+ }
64
+ }
65
+
66
+ async ensureInjected(settingsState) {
67
+ if (!(await this.ownsEndpoint())) throw new Error(`拒绝连接:127.0.0.1:${this.port} 不属于 Codex`);
68
+ const resolvedSettings = settingsState ?? await this.settingsRepository.read();
69
+ const targets = await this.discover();
70
+ if (targets.length === 0) throw new Error("未找到 Codex 主窗口 renderer");
71
+ const results = [];
72
+ const injectedTargetIds = new Set();
73
+ for (const target of targets) {
74
+ const activeVersion = await this.evaluate(target, "window.__codexSidebarTags?.version ?? null");
75
+ if (activeVersion === RUNTIME_VERSION) {
76
+ results.push(await this.evaluate(target, "window.__codexSidebarTags.status()"));
77
+ continue;
78
+ }
79
+ results.push(await this.evaluate(target, buildInjectionExpression({
80
+ tagDefinitions: resolvedSettings.settings.tags,
81
+ settingsSource: resolvedSettings.exists ? "repository" : "defaults",
82
+ })));
83
+ injectedTargetIds.add(target.id);
84
+ }
85
+ return { targets, results, injectedTargetIds };
86
+ }
87
+
88
+ async removeInjection() {
89
+ if (!(await this.ownsEndpoint())) throw new Error(`拒绝连接:127.0.0.1:${this.port} 不属于 Codex`);
90
+ for (const target of await this.discover()) await this.evaluate(target, buildRemovalExpression());
91
+ }
92
+ }