codexteer 0.14.1

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +519 -0
  3. package/assets/send-pururu.wav +0 -0
  4. package/bin/codexteer-wrapper.mjs +10 -0
  5. package/bin/codexteer.mjs +5 -0
  6. package/docs/distribution.md +186 -0
  7. package/node_modules/ws/LICENSE +20 -0
  8. package/node_modules/ws/README.md +548 -0
  9. package/node_modules/ws/browser.js +8 -0
  10. package/node_modules/ws/index.js +22 -0
  11. package/node_modules/ws/lib/buffer-util.js +131 -0
  12. package/node_modules/ws/lib/constants.js +19 -0
  13. package/node_modules/ws/lib/event-target.js +292 -0
  14. package/node_modules/ws/lib/extension.js +203 -0
  15. package/node_modules/ws/lib/limiter.js +55 -0
  16. package/node_modules/ws/lib/permessage-deflate.js +530 -0
  17. package/node_modules/ws/lib/receiver.js +743 -0
  18. package/node_modules/ws/lib/sender.js +607 -0
  19. package/node_modules/ws/lib/stream.js +161 -0
  20. package/node_modules/ws/lib/subprotocol.js +62 -0
  21. package/node_modules/ws/lib/validation.js +152 -0
  22. package/node_modules/ws/lib/websocket-server.js +562 -0
  23. package/node_modules/ws/lib/websocket.js +1407 -0
  24. package/node_modules/ws/package.json +74 -0
  25. package/node_modules/ws/wrapper.mjs +21 -0
  26. package/package.json +41 -0
  27. package/scripts/accessibility.applescript +43 -0
  28. package/scripts/inspect.applescript +72 -0
  29. package/scripts/send.applescript +45 -0
  30. package/src/app-server.mjs +117 -0
  31. package/src/bridge.mjs +79 -0
  32. package/src/checkpoint.mjs +128 -0
  33. package/src/cli.mjs +358 -0
  34. package/src/compatibility.mjs +115 -0
  35. package/src/desktop.mjs +207 -0
  36. package/src/directive.mjs +39 -0
  37. package/src/distribution.mjs +192 -0
  38. package/src/evidence.mjs +33 -0
  39. package/src/help.mjs +145 -0
  40. package/src/journal.mjs +120 -0
  41. package/src/launcher.mjs +145 -0
  42. package/src/monitor.mjs +123 -0
  43. package/src/observe.mjs +185 -0
  44. package/src/paged-read.mjs +228 -0
  45. package/src/prompt.mjs +51 -0
  46. package/src/resource.mjs +70 -0
  47. package/src/rpc.mjs +162 -0
  48. package/src/runner.mjs +32 -0
  49. package/src/runtime.mjs +130 -0
  50. package/src/settings.mjs +49 -0
  51. package/src/sound.mjs +16 -0
  52. package/src/store.mjs +78 -0
  53. package/src/subscription.mjs +118 -0
  54. package/src/supervise.mjs +57 -0
  55. package/src/thread-id.mjs +35 -0
  56. package/src/thread-store.mjs +72 -0
  57. package/src/version.mjs +19 -0
  58. package/src/wrapper.mjs +149 -0
@@ -0,0 +1,35 @@
1
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+
3
+ export function normalizeThreadId(input) {
4
+ if (typeof input !== "string" || input.trim() === "") {
5
+ throw new Error("Thread ID is required.");
6
+ }
7
+
8
+ let value = input.trim();
9
+ if (value.startsWith("codex://")) {
10
+ let url;
11
+ try {
12
+ url = new URL(value);
13
+ } catch {
14
+ throw new Error(`Invalid Codex thread URL: ${input}`);
15
+ }
16
+ if (url.hostname !== "threads") {
17
+ throw new Error(`Expected codex://threads/<thread-id>: ${input}`);
18
+ }
19
+ value = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
20
+ }
21
+
22
+ if (!UUID_PATTERN.test(value)) {
23
+ throw new Error(`Invalid local Codex thread ID: ${input}`);
24
+ }
25
+ return value.toLowerCase();
26
+ }
27
+
28
+ export function threadDeepLink(threadId) {
29
+ return `codex://threads/${normalizeThreadId(threadId)}`;
30
+ }
31
+
32
+ export function threadComposerDeepLink(threadId, prompt = "") {
33
+ const query = new URLSearchParams({ prompt });
34
+ return `${threadDeepLink(threadId)}?${query}`;
35
+ }
@@ -0,0 +1,72 @@
1
+ import { open, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { codexHome } from "./runtime.mjs";
4
+
5
+ const UUID_IN_FILENAME = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
6
+
7
+ async function walk(directory, output) {
8
+ let entries;
9
+ try {
10
+ entries = await readdir(directory, { withFileTypes: true });
11
+ } catch (error) {
12
+ if (error.code === "ENOENT") return;
13
+ throw error;
14
+ }
15
+
16
+ await Promise.all(entries.map(async (entry) => {
17
+ const entryPath = path.join(directory, entry.name);
18
+ if (entry.isDirectory()) await walk(entryPath, output);
19
+ else if (entry.isFile() && UUID_IN_FILENAME.test(entry.name)) output.push(entryPath);
20
+ }));
21
+ }
22
+
23
+ async function readSessionMetadata(filePath) {
24
+ const handle = await open(filePath, "r");
25
+ try {
26
+ const buffer = Buffer.alloc(128 * 1024);
27
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
28
+ const firstLine = buffer.subarray(0, bytesRead).toString("utf8").split("\n", 1)[0];
29
+ const record = JSON.parse(firstLine);
30
+ return record.type === "session_meta" ? record.payload : {};
31
+ } finally {
32
+ await handle.close();
33
+ }
34
+ }
35
+
36
+ export async function listLocalThreads({
37
+ limit = 20,
38
+ desktopOnly = false,
39
+ sessionsRoot = path.join(codexHome(), "sessions"),
40
+ } = {}) {
41
+ const files = [];
42
+ await walk(sessionsRoot, files);
43
+ const recent = await Promise.all(files.map(async (filePath) => ({
44
+ filePath,
45
+ info: await stat(filePath),
46
+ })));
47
+ recent.sort((a, b) => b.info.mtimeMs - a.info.mtimeMs);
48
+
49
+ const threads = [];
50
+ for (const item of recent) {
51
+ if (threads.length >= limit) break;
52
+ let metadata;
53
+ try {
54
+ metadata = await readSessionMetadata(item.filePath);
55
+ } catch {
56
+ continue;
57
+ }
58
+ if (desktopOnly && metadata.originator !== "Codex Desktop") continue;
59
+ const filenameId = UUID_IN_FILENAME.exec(item.filePath)?.[1];
60
+ const threadId = metadata.id ?? metadata.session_id ?? filenameId;
61
+ if (!threadId) continue;
62
+ threads.push({
63
+ thread_id: threadId,
64
+ updated_at: item.info.mtime.toISOString(),
65
+ created_at: metadata.timestamp ?? null,
66
+ cwd: metadata.cwd ?? null,
67
+ originator: metadata.originator ?? null,
68
+ deep_link: `codex://threads/${threadId}`,
69
+ });
70
+ }
71
+ return threads;
72
+ }
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const metadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4
+ export const VERSION = metadata.version;
5
+ export const PACKAGE_NAME = metadata.name;
6
+ const safeVersion = value => typeof value === "string" && value.length <= 100 && /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(value);
7
+ if (!safeVersion(VERSION)) throw new Error("Invalid package version.");
8
+
9
+ export function versionCompatibility(state) {
10
+ const runningVersion = safeVersion(state?.codex_steer_version) ? state.codex_steer_version : null;
11
+ const runningPackage = typeof state?.codex_steer_package === "string" && /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(state.codex_steer_package) && state.codex_steer_package.length <= 214 ? state.codex_steer_package : null;
12
+ const unknown = !runningVersion || !runningPackage;
13
+ const matches = runningVersion === VERSION && runningPackage === PACKAGE_NAME;
14
+ return {
15
+ status: unknown ? "unverified" : matches ? "matched" : "mismatch",
16
+ client: { package: PACKAGE_NAME, version: VERSION },
17
+ runtime: { package: runningPackage, version: runningVersion },
18
+ };
19
+ }
@@ -0,0 +1,149 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { chmod, stat } from "node:fs/promises";
3
+ import { once } from "node:events";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { claimRuntime } from "./runtime.mjs";
6
+ import { connectSocket } from "./rpc.mjs";
7
+ import { relay } from "./bridge.mjs";
8
+ import { DesktopSubscriptions, serveSubscriptions } from "./subscription.mjs";
9
+ import { VERSION, PACKAGE_NAME } from "./version.mjs";
10
+ import { RUNTIME_PROTOCOL, RUNTIME_CAPABILITIES } from "./compatibility.mjs";
11
+ import { describeDistribution } from "./distribution.mjs";
12
+
13
+ export const BUNDLED_CLI = "/Applications/ChatGPT.app/Contents/Resources/codex";
14
+ export const BUNDLED_NODE = "/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node";
15
+ const DESKTOP_EXECUTABLE = "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT";
16
+
17
+ export function isDesktopProcess({ parentPid = process.ppid, run = spawnSync } = {}) {
18
+ const parent = run("/bin/ps", ["-p", String(parentPid), "-o", "comm="], { encoding: "utf8", timeout: 3000 });
19
+ return parent.status === 0 && parent.stdout.trim() === DESKTOP_EXECUTABLE;
20
+ }
21
+
22
+ async function forwardInvocation(executable, args, env) {
23
+ const child = spawn(executable, args, { env, stdio: "inherit" });
24
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
25
+ const handlers = signals.map(signal => () => child.kill(signal));
26
+ signals.forEach((signal, index) => process.on(signal, handlers[index]));
27
+ try {
28
+ const [code, signal] = await once(child, "exit");
29
+ return code ?? (signal ? 1 : 0);
30
+ } finally {
31
+ signals.forEach((signal, index) => process.off(signal, handlers[index]));
32
+ }
33
+ }
34
+
35
+ export function serverArguments(args, socketPath) {
36
+ // Parse CLI/global options, not substrings of TOML values or user prompts.
37
+ const values = new Set(["-c", "--config", "--enable", "--disable"]);
38
+ let index = 0;
39
+ while (index < args.length && args[index].startsWith("-")) {
40
+ if (values.has(args[index])) index += 2;
41
+ else if (/^--(?:config|enable|disable)=/.test(args[index])) index++;
42
+ else return null;
43
+ }
44
+ if (args[index] !== "app-server") return null;
45
+ const transformed = args.slice(0, index + 1);
46
+ for (let i = index + 1; i < args.length; i++) {
47
+ const arg = args[i];
48
+ if (["--help", "-h", "--version", "-V"].includes(arg)) return null;
49
+ if (!arg.startsWith("-")) return null; // daemon, proxy, schema generation, help
50
+ if (arg === "--stdio") continue;
51
+ if (arg === "--listen" || arg.startsWith("--listen=")) {
52
+ const value = arg === "--listen" ? args[++i] : arg.slice(9);
53
+ if (value !== "stdio://") throw new Error("Desktop requested a conflicting App Server transport.");
54
+ continue;
55
+ }
56
+ transformed.push(arg);
57
+ if (values.has(arg) || ["--code-mode-host", "--ws-auth", "--ws-token-file", "--ws-token-sha256", "--ws-shared-secret-file", "--ws-issuer", "--ws-audience", "--ws-max-clock-skew-seconds"].includes(arg)) {
58
+ if (args[i + 1] == null) throw new Error("Incomplete App Server startup arguments.");
59
+ transformed.push(args[++i]);
60
+ }
61
+ }
62
+ return [...transformed, "--listen", `unix://${socketPath}`];
63
+ }
64
+
65
+ export async function stopChild(child) {
66
+ if (!child?.pid) return;
67
+ const signal = name => {
68
+ try { process.kill(-child.pid, name); } catch (error) { if (error.code !== "ESRCH") throw error; }
69
+ };
70
+ signal("SIGTERM");
71
+ if (child.exitCode == null && child.signalCode == null) {
72
+ await Promise.race([once(child, "exit").catch(() => {}), delay(2000)]);
73
+ }
74
+ // Include only the process group created for this server, including its hosts.
75
+ signal("SIGKILL");
76
+ }
77
+
78
+ export async function runWrapper(args, { executable = BUNDLED_CLI, env = process.env, input = process.stdin, output = process.stdout, desktopProcess } = {}) {
79
+ // CODEX_CLI_PATH is inherited by MCP/Computer Use helpers which launch their
80
+ // own stdio servers. Only Desktop's direct child owns the shared runtime.
81
+ // Check origin before parsing/changing transports so helper calls remain exact.
82
+ if (!(desktopProcess ?? isDesktopProcess()) || !serverArguments(args, "/unused")) {
83
+ return forwardInvocation(executable, args, env);
84
+ }
85
+ // Desktop's app-tools peer authorization checks the process ancestry.
86
+ if (process.execPath !== BUNDLED_NODE) {
87
+ throw new Error("Run the Desktop wrapper directly so it uses Desktop's bundled Node runtime.");
88
+ }
89
+ const version = spawnSync(executable, ["--version"], { encoding: "utf8", timeout: 5000, env });
90
+ const cliVersion = version.stdout?.match(/codex-cli ([\w.+-]+)/)?.[1];
91
+ if (version.status !== 0 || !cliVersion) throw new Error("Could not verify the bundled Codex CLI.");
92
+ const distribution = await describeDistribution();
93
+ const lease = await claimRuntime(env.CODEX_HOME, {
94
+ cli_version: cliVersion, node_path: process.execPath, node_version: process.versions.node,
95
+ codex_steer_package: PACKAGE_NAME, codex_steer_version: VERSION, codex_steer_protocol: RUNTIME_PROTOCOL,
96
+ codex_steer_capabilities: RUNTIME_CAPABILITIES,
97
+ distribution_sha256: distribution.sha256,
98
+ });
99
+ let child;
100
+ let socket;
101
+ let stopSubscriptions;
102
+ let subscriptions;
103
+ let interrupted;
104
+ const signal = () => { interrupted = true; socket?.terminate(); input.destroy(); };
105
+ process.on("SIGTERM", signal);
106
+ process.on("SIGINT", signal);
107
+ try {
108
+ child = spawn(executable, serverArguments(args, lease.paths.socket), {
109
+ env, detached: true, stdio: ["ignore", "ignore", "inherit"],
110
+ });
111
+ let spawnError;
112
+ child.on("error", error => { spawnError = error; });
113
+ for (let i = 0; i < 100; i++) {
114
+ if (interrupted || spawnError || child.exitCode != null || child.signalCode != null) throw new Error("App Server exited during startup.");
115
+ if (await stat(lease.paths.socket).catch(() => null)) break;
116
+ await delay(100);
117
+ }
118
+ await chmod(lease.paths.socket, 0o600);
119
+ await lease.update({ server_pid: child.pid });
120
+ socket = await connectSocket(lease.paths.socket);
121
+ subscriptions = new DesktopSubscriptions(socket);
122
+ stopSubscriptions = await serveSubscriptions(lease.paths.control, subscriptions);
123
+ let initId;
124
+ await relay(input, output, socket, {
125
+ onDesktopMessage(message) {
126
+ if (message.method === "initialize") initId = message.id;
127
+ },
128
+ onServerMessage(message) {
129
+ // Desktop 26.903.71938 sends initialize and considers its response the
130
+ // handshake completion; it does not emit the optional initialized
131
+ // notification shown in the public client example. Forward either form.
132
+ if (!message.method && initId != null && message.id === initId && message.result) {
133
+ subscriptions.ready = true;
134
+ lease.update({ desktop_connected: true }).catch(() => socket.terminate());
135
+ }
136
+ return subscriptions.consume(message);
137
+ },
138
+ });
139
+ return 0;
140
+ } finally {
141
+ process.off("SIGTERM", signal);
142
+ process.off("SIGINT", signal);
143
+ await lease.update({ desktop_connected: false }).catch(() => {});
144
+ socket?.terminate();
145
+ await stopSubscriptions?.();
146
+ await stopChild(child);
147
+ await lease.release();
148
+ }
149
+ }