codexteer 0.16.0 → 0.18.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 (85) hide show
  1. package/CHANGELOG.md +37 -2
  2. package/README.md +154 -16
  3. package/assets/remote-device-key.swift +94 -0
  4. package/bin/codexteer-update.mjs +3 -0
  5. package/bin/codexteer.mjs +6 -2
  6. package/docs/cloud.md +113 -0
  7. package/docs/desktop-cloud.md +36 -0
  8. package/docs/distribution.md +21 -9
  9. package/docs/orchestration.md +120 -0
  10. package/docs/remote-cli-validation.json +139 -0
  11. package/docs/remote-control.md +65 -0
  12. package/docs/remote-device-enrollment.md +91 -0
  13. package/docs/remote-observation.md +57 -0
  14. package/docs/remote-operations.md +89 -0
  15. package/docs/thread-create.md +90 -0
  16. package/package.json +13 -2
  17. package/scripts/update-desktop.applescript +51 -0
  18. package/src/agent-launch.mjs +27 -0
  19. package/src/app-server.mjs +34 -10
  20. package/src/checkpoint.mjs +52 -16
  21. package/src/cli.mjs +233 -19
  22. package/src/cloud-api.mjs +118 -0
  23. package/src/cloud-cli.mjs +319 -0
  24. package/src/cloud-connection.mjs +64 -0
  25. package/src/cloud-id.mjs +16 -0
  26. package/src/cloud-observe.mjs +178 -0
  27. package/src/cloud-send.mjs +33 -0
  28. package/src/cloud.mjs +172 -0
  29. package/src/connection.mjs +23 -5
  30. package/src/creation-verification.mjs +36 -0
  31. package/src/desktop-cloud-api.mjs +119 -0
  32. package/src/desktop-cloud-id.mjs +18 -0
  33. package/src/desktop-cloud-project.mjs +148 -0
  34. package/src/desktop-cloud.mjs +98 -0
  35. package/src/desktop.mjs +3 -1
  36. package/src/directive.mjs +5 -2
  37. package/src/distribution.mjs +6 -6
  38. package/src/evidence.mjs +3 -0
  39. package/src/findings.mjs +7 -7
  40. package/src/help.mjs +155 -26
  41. package/src/journal.mjs +37 -9
  42. package/src/launcher.mjs +16 -9
  43. package/src/monitor.mjs +18 -6
  44. package/src/native-command.mjs +100 -0
  45. package/src/native-rpc.mjs +116 -0
  46. package/src/native-worker.mjs +25 -0
  47. package/src/observe.mjs +21 -7
  48. package/src/orchestrate-prompt.mjs +96 -0
  49. package/src/orchestrate.mjs +29 -0
  50. package/src/paged-read.mjs +7 -1
  51. package/src/projects.mjs +104 -0
  52. package/src/prompt-command.mjs +25 -0
  53. package/src/prompt.mjs +35 -11
  54. package/src/remote-client-info.mjs +16 -0
  55. package/src/remote-control-auth.mjs +109 -0
  56. package/src/remote-control-socket.mjs +137 -0
  57. package/src/remote-control.mjs +134 -0
  58. package/src/remote-device-key.mjs +41 -0
  59. package/src/remote-device-login.mjs +76 -0
  60. package/src/remote-device-protocol.mjs +72 -0
  61. package/src/remote-device-store.mjs +71 -0
  62. package/src/remote-device-worker.mjs +48 -0
  63. package/src/remote-device.mjs +202 -0
  64. package/src/remote-io.mjs +163 -0
  65. package/src/remote-key-access.mjs +76 -0
  66. package/src/remote-key-worker.mjs +38 -0
  67. package/src/remote-keychain.mjs +72 -0
  68. package/src/remote-management.mjs +234 -0
  69. package/src/remote.mjs +132 -0
  70. package/src/resource.mjs +8 -6
  71. package/src/runner.mjs +6 -1
  72. package/src/runtime.mjs +4 -2
  73. package/src/socket-endpoint.mjs +40 -0
  74. package/src/standalone-create.mjs +135 -0
  75. package/src/store.mjs +6 -3
  76. package/src/supervise.mjs +32 -55
  77. package/src/supervision.mjs +41 -19
  78. package/src/target.mjs +25 -0
  79. package/src/task-id.mjs +23 -0
  80. package/src/thread-create.mjs +128 -0
  81. package/src/thread-id.mjs +21 -6
  82. package/src/thread-verify.mjs +108 -0
  83. package/src/turn-repair.mjs +228 -0
  84. package/src/update.mjs +158 -0
  85. package/src/wrapper.mjs +68 -5
@@ -0,0 +1,118 @@
1
+ import { spawn } from "node:child_process";
2
+ import { VERSION } from "./version.mjs";
3
+
4
+ const failure = (code, message) => Object.assign(new Error(message), { code, execution_environment: "cloud" });
5
+ const LIMIT = 16 * 1024 * 1024;
6
+
7
+ // Credentials stay in memory and come from the official process, never auth.json.
8
+ // No model session is started and no Desktop connection is used.
9
+ export function cloudCredentials({ spawnProcess = spawn, timeoutMs = 15000, signal } = {}) {
10
+ return new Promise((resolve, reject) => {
11
+ if (signal?.aborted) { reject(failure("CLOUD_CANCELLED", "Cloud authentication was cancelled.")); return; }
12
+ let child, buffer = "", bytes = 0, settled = false, killTimer, initialized = false;
13
+ const finish = (error, result) => {
14
+ if (settled) return;
15
+ settled = true; clearTimeout(timer); signal?.removeEventListener("abort", cancel);
16
+ child?.stdin.destroy(); child?.kill();
17
+ killTimer = setTimeout(() => child?.kill("SIGKILL"), 500); killTimer.unref();
18
+ buffer = "";
19
+ if (error) reject(error); else resolve(result);
20
+ };
21
+ const cancel = () => finish(failure("CLOUD_CANCELLED", "Cloud authentication was cancelled."));
22
+ const timer = setTimeout(() => finish(failure("CLOUD_AUTH_TIMEOUT", "Official CLI authentication timed out.")), timeoutMs);
23
+ signal?.addEventListener("abort", cancel, { once: true });
24
+ try { child = spawnProcess("codex", ["app-server", "--listen", "stdio://"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); }
25
+ catch { finish(failure("CLOUD_AUTH_UNAVAILABLE", "Cannot start the official Codex App Server.")); return; }
26
+ child.on("error", () => finish(failure("CLOUD_AUTH_UNAVAILABLE", "Cannot start the official Codex App Server.")));
27
+ child.on("close", () => { clearTimeout(killTimer); finish(failure("CLOUD_AUTH_FAILED", "Official CLI authentication closed before completion.")); });
28
+ const count = chunk => { bytes += Buffer.byteLength(chunk); if (bytes > LIMIT) finish(failure("CLOUD_OUTPUT_LIMIT", "Official authentication exceeded the response limit.")); };
29
+ child.stderr.on("data", count);
30
+ child.stdin.on("error", () => finish(failure("CLOUD_AUTH_FAILED", "Official CLI authentication failed.")));
31
+ const send = value => child.stdin.write(JSON.stringify(value) + "\n");
32
+ child.stdout.setEncoding("utf8");
33
+ child.stdout.on("data", chunk => {
34
+ count(chunk); if (settled) return;
35
+ buffer += chunk;
36
+ let newline;
37
+ while (!settled && (newline = buffer.indexOf("\n")) >= 0) {
38
+ const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
39
+ let message;
40
+ try { message = JSON.parse(line); } catch { finish(failure("CLOUD_AUTH_PROTOCOL", "Invalid official authentication response.")); return; }
41
+ if (message.id === 1) {
42
+ if (initialized || !message.result || message.error) { finish(failure("CLOUD_AUTH_PROTOCOL", "Official authentication initialization failed.")); return; }
43
+ initialized = true;
44
+ send({ method: "initialized" });
45
+ send({ id: 2, method: "getAuthStatus", params: { includeToken: true, refreshToken: false } });
46
+ } else if (message.id === 2) {
47
+ const result = message.result;
48
+ if (!initialized || message.error || result?.authMethod !== "chatgpt" || typeof result.authToken !== "string" || !result.authToken || /[\s\x00-\x1f\x7f]/u.test(result.authToken)) {
49
+ finish(failure("CLOUD_LOGIN_REQUIRED", "Sign in to the official Codex CLI with ChatGPT to list cloud environments.")); return;
50
+ }
51
+ let accountId;
52
+ try { accountId = JSON.parse(Buffer.from(result.authToken.split(".")[1], "base64url").toString("utf8"))["https://api.openai.com/auth"]?.chatgpt_account_id; } catch {}
53
+ if (typeof accountId !== "string" || !/^[A-Za-z0-9_-]{1,200}$/.test(accountId)) { finish(failure("CLOUD_AUTH_PROTOCOL", "Official authentication did not identify a ChatGPT account.")); return; }
54
+ finish(null, { token: result.authToken, accountId });
55
+ }
56
+ }
57
+ });
58
+ send({ id: 1, method: "initialize", params: { clientInfo: { name: "codexteer", version: VERSION } } });
59
+ });
60
+ }
61
+
62
+ // Fixed official origin, no redirects, URL overrides, raw diagnostics or retries.
63
+ export async function cloudApi(route, { timeoutMs = 120000, credentials = cloudCredentials, fetchRequest = fetch, body, signal } = {}) {
64
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600000) throw failure("CLOUD_OPTIONS_INVALID", "Invalid cloud request deadline.");
65
+ if (!(body === undefined ? /^(?:environments|tasks\/task_[A-Za-z0-9_-]{1,200}(?:\/turns)?)$/.test(route) : route === "tasks")) throw failure("CLOUD_OPTIONS_INVALID", "Unsupported cloud API route.");
66
+ if (signal?.aborted) throw failure("CLOUD_CANCELLED", "Cloud request was cancelled.");
67
+ const started = Date.now();
68
+ let auth;
69
+ try { auth = await credentials({ timeoutMs: Math.min(timeoutMs, 15000), signal }); }
70
+ catch (error) {
71
+ if (error.code === "CLOUD_AUTH_TIMEOUT" && Date.now() - started >= timeoutMs) throw failure("CLOUD_TIMEOUT", "Cloud authentication exceeded the request deadline.");
72
+ throw error;
73
+ }
74
+ const remaining = timeoutMs - (Date.now() - started);
75
+ if (remaining <= 0) throw failure("CLOUD_TIMEOUT", "Cloud authentication exceeded the request deadline.");
76
+ const controller = new AbortController(), timer = setTimeout(() => controller.abort(), remaining);
77
+ const cancel = () => controller.abort(); signal?.addEventListener("abort", cancel, { once: true });
78
+ if (signal?.aborted) controller.abort();
79
+ try {
80
+ const response = await fetchRequest(`https://chatgpt.com/backend-api/wham/${route}`, {
81
+ method: body === undefined ? "GET" : "POST", redirect: "error", signal: controller.signal,
82
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
83
+ headers: { Authorization: `Bearer ${auth.token}`, "ChatGPT-Account-ID": auth.accountId, Accept: "application/json", "Content-Type": "application/json", "User-Agent": `codexteer/${VERSION}` },
84
+ });
85
+ if (!response.ok) {
86
+ await response.body?.cancel();
87
+ if (response.status === 429) {
88
+ const header = response.headers.get("retry-after"), seconds = header != null && /^\d+(?:\.\d+)?$/.test(header) ? Number(header) : null;
89
+ const after = seconds !== null ? Math.min(Number.MAX_SAFE_INTEGER, seconds * 1000) : header == null ? NaN : Date.parse(header) - Date.now();
90
+ throw Object.assign(failure("CLOUD_RATE_LIMITED", "Cloud requests are rate limited. Respect retry_after_ms before another read; no request was retried."), {
91
+ http_status: 429, retry_after_ms: Number.isFinite(after) ? Math.max(1000, Math.min(Number.MAX_SAFE_INTEGER, Math.ceil(after))) : 15000,
92
+ });
93
+ }
94
+ throw failure(response.status === 401 ? "CLOUD_LOGIN_REQUIRED" : response.status === 403 ? "CLOUD_ACCESS_DENIED" : "CLOUD_HTTP_FAILED", `Cloud request failed (HTTP ${response.status}). No response body or credentials were disclosed.`);
95
+ }
96
+ let bytes = 0; const chunks = [];
97
+ for await (const chunk of response.body) {
98
+ bytes += chunk.length; if (bytes > LIMIT) { controller.abort(); throw failure("CLOUD_OUTPUT_LIMIT", "Cloud response exceeded the 16 MiB limit."); }
99
+ chunks.push(chunk);
100
+ }
101
+ try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); }
102
+ catch { throw failure("CLOUD_PROTOCOL_UNVERIFIED", "Cloud returned an unrecognized response. No raw payload was disclosed."); }
103
+ } catch (error) {
104
+ if (typeof error.code === "string" && error.code.startsWith("CLOUD_")) throw error;
105
+ throw failure(signal?.aborted ? "CLOUD_CANCELLED" : controller.signal.aborted ? "CLOUD_TIMEOUT" : "CLOUD_CONNECTION_FAILED", "Cloud request did not complete. No credentials or raw diagnostics were disclosed.");
106
+ } finally { clearTimeout(timer); signal?.removeEventListener("abort", cancel); }
107
+ }
108
+
109
+ export async function cloudEnvironments(settings = {}, { request = cloudApi } = {}) {
110
+ if (settings.timeoutMs !== undefined && (!Number.isSafeInteger(settings.timeoutMs) || settings.timeoutMs < 1000 || settings.timeoutMs > 600000)) throw failure("CLOUD_OPTIONS_INVALID", "--timeout-ms must be 1000..600000.");
111
+ const rows = await request("environments", settings);
112
+ if (!Array.isArray(rows) || rows.length > 10000 || rows.some(row => !row || typeof row.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/.test(row.id)
113
+ || (row.label != null && (typeof row.label !== "string" || row.label.length > 1000)))) throw failure("CLOUD_PROTOCOL_UNVERIFIED", "Cloud returned an unrecognized environment inventory.");
114
+ return { backend: "codex-api", execution_environment: "cloud", environments: rows.map(row => ({ id: row.id, name: row.label ?? null, pinned: row.is_pinned === true,
115
+ repositories: Object.values(row.repo_map ?? {}).flatMap(repo => typeof repo?.repository_full_name === "string" && repo.repository_full_name.length < 1000
116
+ ? [{ name: repo.repository_full_name, default_branch: typeof repo.default_branch === "string" && repo.default_branch.length < 256 ? repo.default_branch : null }] : []),
117
+ })), source: "accessible-task-environments", availability: "listed-access-only; execution-not-verified" };
118
+ }
@@ -0,0 +1,319 @@
1
+ import { desktopCloudProjectDoctor, assignDesktopCloudProject, desktopCloudProjectReceipt } from "./desktop-cloud-project.mjs";
2
+ import { listProjects as listDesktopProjects } from "./projects.mjs";
3
+ import { isDesktopCloudTask, desktopCloudId, desktopCloudLink } from "./desktop-cloud-id.mjs";
4
+ import { desktopCloudConnection, desktopCloudEnvironments } from "./desktop-cloud-api.mjs";
5
+ import { createDesktopCloudTask, listDesktopCloudTasks, desktopCloudReceipt } from "./desktop-cloud.mjs";
6
+ import { isCloudTask } from "./task-id.mjs";
7
+ import { cloudTaskId } from "./cloud-id.mjs";
8
+ import { cloudConnection } from "./cloud-connection.mjs";
9
+ import { observeThread, printObservation } from "./observe.mjs";
10
+ import { monitorCommand } from "./monitor.mjs";
11
+ import { notificationOptions } from "./notify.mjs";
12
+ import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
13
+ import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
14
+ import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
15
+ import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
16
+ import { createCloudTask, cloudTaskDiff, listCloudTasks, cloudCreationReceipt } from "./cloud.mjs";
17
+ import { cloudSendReceipt, reconcileCloudSend } from "./cloud-observe.mjs";
18
+ import { cloudEnvironments } from "./cloud-api.mjs";
19
+ import { readFileSync } from "node:fs";
20
+ import { spawnSync } from "node:child_process";
21
+ import { playSendSound } from "./sound.mjs";
22
+ import { VERSION } from "./version.mjs";
23
+ import { createCheckpoint, listCheckpoints, getCheckpoint, checkpointSummary, verifyCheckpoint, runCheckpoint, attachArtifacts } from "./checkpoint.mjs";
24
+ import { acquireResource, resourceStatus, renewResource, releaseResource, listResources, runWithResource } from "./resource.mjs";
25
+
26
+ const failure = (message, code = "CLOUD_OPTIONS_INVALID") => Object.assign(new Error(message), { code, execution_environment: "cloud" });
27
+ const values = new Set("--environment --connection --supervisor --require-node-version --host --remote-transport --context-thread --thread --backend --source --kind --evidence --based-on --finding --expires-at --supersedes --checkpoint --limit --max-chars --since --poll-ms --timeout-ms --until --notify --settle-ms --max-hold-ms --owner --key --condition --note --rating --reason --root --path --exclude --command --cwd --artifact --status --by --token --ttl-ms --agent --project --env --branch --title --model --thinking --cursor --attempt".split(" "));
28
+ function option(args, name, fallback) {
29
+ const index = args.indexOf(name), stop = args.indexOf("--");
30
+ if (index < 0 || (stop >= 0 && index > stop)) return fallback;
31
+ const value = args[index + 1]; if (value == null || value.startsWith("--")) throw failure(`${name} requires a value.`);
32
+ args.splice(index, 2); return value;
33
+ }
34
+ function flag(args, name) {
35
+ const index = args.indexOf(name), stop = args.indexOf("--");
36
+ if (index < 0 || (stop >= 0 && index > stop)) return false;
37
+ args.splice(index, 1); return true;
38
+ }
39
+ const repeated = (args, name) => { const result = []; let value; while ((value = option(args, name)) !== undefined) result.push(value); return result; };
40
+ const message = args => { if (args[0] === "--") args.shift(); return args.length === 1 && args[0] === "-" ? readFileSync(0, "utf8") : args.join(" "); };
41
+ function exact(args, count) { if (args.length !== count || args.some(arg => arg.startsWith("--"))) throw failure("Unexpected arguments. See the command's help."); }
42
+
43
+ function selectedCloudInvocation(argv) {
44
+ const end = argv.indexOf("--"), own = end < 0 ? argv : argv.slice(0, end);
45
+ const connection = own.indexOf("--connection");
46
+ const positional = [];
47
+ for (let i = 0; i < own.length; i++) {
48
+ if (values.has(own[i])) { i++; continue; }
49
+ if (!own[i].startsWith("--")) positional.push(own[i]);
50
+ }
51
+ const [command, action] = positional;
52
+ if (["help", "--help", "--version"].includes(command) || own.includes("--help") || own.includes("--version")) return false;
53
+ if (connection >= 0 && ["cloud", "desktop-cloud"].includes(own[connection + 1])) return own[connection + 1];
54
+ const thread = own.indexOf("--thread");
55
+ if (["doctor", "resource"].includes(command) && thread >= 0) return isDesktopCloudTask(own[thread + 1]) ? "desktop-cloud" : isCloudTask(own[thread + 1]) ? "cloud" : false;
56
+ if (command === "thread" && action === "create") return false;
57
+ const nested = ["history", "instructions", "findings", "checkpoint", "thread"].includes(command)
58
+ || (command === "supervise" && ["prompt", "register", "status", "pause", "resume", "stop", "list"].includes(action));
59
+ const target = isCloudTask(command) || isDesktopCloudTask(command) ? command : positional[nested ? 2 : 1];
60
+ return isDesktopCloudTask(target) ? "desktop-cloud" : isCloudTask(target) ? "cloud" : false;
61
+ }
62
+
63
+ export const cloudInvocation = argv => !!selectedCloudInvocation(argv);
64
+
65
+ // Only parsing/routing is backend-specific. Supervision, monitoring, directives,
66
+ // findings, journaling and saved-agent launch use the same local implementation.
67
+ export async function mainCloud(argv) {
68
+ const args = [...argv], json = flag(args, "--json"), connection = option(args, "--connection", selectedCloudInvocation(argv) || "cloud");
69
+ const desktopCloud = connection === "desktop-cloud";
70
+ const targetId = desktopCloud ? desktopCloudId : cloudTaskId;
71
+ if (!["cloud", "desktop-cloud"].includes(connection)) throw failure("A cloud task requires the cloud connection.", "CLOUD_TARGET_CONFLICT");
72
+ const own = args.slice(0, args.includes("--") ? args.indexOf("--") : args.length);
73
+ if (own.some(value => ["--host", "--remote-transport", "--context-thread"].includes(value))) throw failure("Desktop host routes cannot select a cloud task.", "CLOUD_TARGET_CONFLICT");
74
+ const requiredNode = option(args, "--require-node-version");
75
+ if (requiredNode !== undefined && requiredNode !== process.version) throw failure("The saved supervisor requires its original Node version. Regenerate its prompt.", "NODE_VERSION_MISMATCH");
76
+ const session = option(args, "--supervisor"), supervisor = session === undefined ? undefined : supervisorId(session);
77
+ let command = args.shift();
78
+ if (isCloudTask(command) || isDesktopCloudTask(command)) { args.unshift(command); command = "send"; }
79
+ const output = (name, data) => {
80
+ if (json) console.log(JSON.stringify({ ok: true, command: name, data }));
81
+ else console.log(JSON.stringify(data, null, 2));
82
+ return data;
83
+ };
84
+ const bound = async (target, { allowUnregistered = false } = {}) => {
85
+ const id = targetId(target);
86
+ if (supervisor) {
87
+ const record = allowUnregistered ? await getSupervisor(id) : null;
88
+ if (!allowUnregistered || record?.session_id === supervisor) await assertSupervisor(id, supervisor, { connection });
89
+ else if (record && record.state !== "stopped") throw failure("Another supervisor owns this cloud task.", "SUPERVISOR_CONFLICT");
90
+ }
91
+ return id;
92
+ };
93
+ const dependencies = desktopCloud ? desktopCloudConnection() : cloudConnection();
94
+ if (desktopCloud && await desktopCloudCommands(command, args, { supervisor, output, bound, dependencies })) return;
95
+
96
+ if (["read", "status", "watch"].includes(command)) {
97
+ const options = { supervisor, connection, fullHistory: flag(args, "--full-history"), since: option(args, "--since"),
98
+ limit: Number(option(args, "--limit", "50")), maxChars: Number(option(args, "--max-chars", "2000")), includeOutput: flag(args, "--include-output") };
99
+ const stream = command === "watch" && flag(args, "--stream");
100
+ if (command === "watch") {
101
+ const until = option(args, "--until"), timeout = option(args, "--timeout-ms");
102
+ if (stream && (until !== undefined || timeout !== undefined)) throw failure("--stream runs until cancelled; do not combine it with --until or --timeout-ms.");
103
+ Object.assign(options, { watch: true, until: until ?? "change", timeoutMs: Number(timeout ?? "30000"), pollMs: Number(option(args, "--poll-ms", "5000")) });
104
+ const notify = option(args, "--notify", stream ? "digest" : "all"), settle = option(args, "--settle-ms"), maxHold = option(args, "--max-hold-ms");
105
+ if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw failure("Digest options require --stream.");
106
+ Object.assign(options, notificationOptions({ notify, ...(settle === undefined ? {} : { settleMs: Number(settle) }), ...(maxHold === undefined ? {} : { maxHoldMs: Number(maxHold) }) }));
107
+ }
108
+ exact(args, 1); const id = await bound(args[0]);
109
+ if (stream) return monitorCommand(id, options, dependencies);
110
+ const data = await observeThread(id, options, dependencies); data.connection_mode = connection;
111
+ if (command === "status") delete data.events;
112
+ if (json) output(command, data); else printObservation(data, command === "status");
113
+ return;
114
+ }
115
+ if (command === "supervise") {
116
+ const action = args[0];
117
+ if (["register", "status", "list", "pause", "resume", "stop"].includes(action)) {
118
+ args.shift(); const owner = action === "register" ? option(args, "--owner", "external") : undefined;
119
+ exact(args, action === "list" ? 0 : 1);
120
+ const id = action === "list" ? null : targetId(args[0]); let data;
121
+ if (action === "register") {
122
+ if (!supervisor) throw failure("supervise register requires the generated --supervisor ID.");
123
+ data = await registerSupervisor(id, supervisor, { owner, connection });
124
+ } else {
125
+ if (supervisor && action !== "status") throw failure("A supervisor cannot change its own control state.", "SUPERVISOR_CONTROL_FORBIDDEN");
126
+ if (supervisor && id && (await getSupervisor(id))?.session_id !== supervisor) throw failure("This command belongs to another supervisor.", "SUPERVISOR_MISMATCH");
127
+ data = action === "list" ? (await listSupervisors()).filter(record => record.connection === connection) : action === "status" ? await supervisorStatus(id) : await controlSupervisor(id, action);
128
+ if (action === "status") { const pending = await listFindings(id, { limit: 20 }); data.unresolved_findings = { total: pending.total, has_more: pending.has_more, findings: pending.findings.map(f => ({ id: f.id, title: f.title, status: f.status, revision: f.revision })) }; }
129
+ }
130
+ output(`supervise.${action}`, data); return;
131
+ }
132
+ if (supervisor) throw failure("A supervisor cannot start a replacement session.", "SUPERVISOR_CONTROL_FORBIDDEN");
133
+ const promptOnly = action === "prompt"; if (promptOnly) args.shift();
134
+ const separator = args.indexOf("--"), ownArgs = separator < 0 ? args : args.slice(0, separator), forwarded = separator < 0 ? [] : args.slice(separator + 1);
135
+ const agent = option(ownArgs, "--agent", "claude"), usePathCli = flag(ownArgs, "--use-path-cli");
136
+ if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(a => a.startsWith("--"))) throw failure("Expected: supervise [prompt] <TASK> [POLICY] [--agent claude|codex].");
137
+ const id = targetId(ownArgs[0]);
138
+ if (promptOnly) { const data = await prepareSupervisorPrompt(id, ownArgs[1], { agent, usePathCli, connection }); if (json) output("supervise.prompt", data); else console.log(data.prompt); }
139
+ else { if (json) throw failure("supervise inherits agent output and does not support --json."); process.exitCode = await superviseAgent(id, { policy: ownArgs[1], agent, usePathCli, agentArgs: forwarded, connection }); }
140
+ return;
141
+ }
142
+ if (command === "findings") {
143
+ const action = args.shift(), options = {};
144
+ const names = { key: "--key", title: "--title", condition: "--condition", basedOn: "--based-on", checkpoint: "--checkpoint", note: "--note", reason: "--reason", rating: "--rating" };
145
+ const allowed = { create: ["key", "title", "condition", "basedOn"], resolve: ["checkpoint", "note"], dismiss: ["reason"], reopen: ["reason", "condition", "basedOn"], evaluate: ["rating", "reason"] }[action] ?? [];
146
+ for (const key of allowed) { const value = option(args, names[key]); if (value !== undefined) options[key] = value; }
147
+ if (["create", "reopen"].includes(action)) { const evidence = repeated(args, "--evidence"); if (evidence.length) options.evidence = evidence; }
148
+ if (action === "list") { options.all = flag(args, "--all"); options.limit = Number(option(args, "--limit", "50")); }
149
+ const handlers = { create: createFinding, show: findingStatus, list: listFindings, resolve: resolveFinding, dismiss: dismissFinding, reopen: reopenFinding, evaluate: evaluateFinding, stats: findingStats };
150
+ if (!Object.hasOwn(handlers, action)) throw failure("Unknown findings operation.");
151
+ const single = ["create", "list", "stats"].includes(action); exact(args, single ? 1 : 2); const id = await bound(args[0]);
152
+ output(`findings.${action}`, single ? await handlers[action](id, options) : await handlers[action](id, args[1], options)); return;
153
+ }
154
+ if (command === "history") {
155
+ const action = args.shift(), attempt = option(args, "--attempt");
156
+ if (attempt !== undefined) {
157
+ if (desktopCloud) throw failure("Use the common history check with a message ID for Desktop Cloud.");
158
+ if (!["show", "check"].includes(action)) throw failure("--attempt requires history show or history check.");
159
+ exact(args, 1); const id = await bound(args[0]);
160
+ const settings = { taskId: id };
161
+ output(`history.${action}`, action === "show" ? await cloudSendReceipt(attempt, settings) : await reconcileCloudSend(attempt, settings)); return;
162
+ }
163
+ const includeText = flag(args, "--include-text"), pending = flag(args, "--pending");
164
+ const report = action === "mark" ? { status: option(args, "--status"), note: option(args, "--note"), evidence: repeated(args, "--evidence"), by: option(args, "--by", "local") } : {};
165
+ if (!args[0] || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !args[1]) || (action === "list" && args[1])) throw failure("Invalid history arguments.");
166
+ const id = await bound(args[0]); const data = action === "list" ? await listMessages(id, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(id, args[1]), includeText)
167
+ : action === "check" ? await reconcileMessages(id, args[1], { includeText }, dependencies) : await markMessage(id, args[1], report);
168
+ output(`history.${action}`, data); return;
169
+ }
170
+ if (command === "instructions") {
171
+ const action = args.shift();
172
+ if (action === "list") { const all = flag(args, "--all"); exact(args, 1); output("instructions.list", await listInstructions(await bound(args[0]), { all })); return; }
173
+ if (action !== "retract") throw failure("Expected instructions list|retract.");
174
+ const source = option(args, "--source", "external"), reason = option(args, "--reason"), basedOn = option(args, "--based-on"), dryRun = flag(args, "--dry-run"), newTurn = flag(args, "--new-turn");
175
+ exact(args, 2); if (!reason) throw failure("Retraction requires --reason.");
176
+ output("instructions.retract", await sendTrackedMessage(await bound(args[0]), reason, { source, retracts: args[1], basedOn, dryRun, newTurn, supervisor, connection })); return;
177
+ }
178
+ if (command === "checkpoint") {
179
+ const action = args.shift(), includeOutput = flag(args, "--include-output"); let data;
180
+ if (action === "capture") {
181
+ const paths = repeated(args, "--path"), excludes = repeated(args, "--exclude"); exact(args, 2);
182
+ data = await createCheckpoint(await bound(args[0]), args[1], { paths, excludes });
183
+ } else if (action === "run") {
184
+ const timeoutMs = Number(option(args, "--timeout-ms", "0"));
185
+ if (args[2] !== "--" || args.length < 4) throw failure("Expected checkpoint run <TASK> <ID> -- <COMMAND> [ARGS...].");
186
+ data = await runCheckpoint(await bound(args[0]), args[1], args.slice(3), { timeoutMs });
187
+ if (!includeOutput) delete data.output_tail; if (!data.valid) process.exitCode = 1;
188
+ } else if (action === "attach") { const refs = repeated(args, "--artifact"); exact(args, 2); data = await attachArtifacts(await bound(args[0]), args[1], refs); }
189
+ else if (action === "list") { exact(args, 1); data = await listCheckpoints(await bound(args[0])); }
190
+ else if (["show", "verify"].includes(action)) { exact(args, 2); const id = await bound(args[0]); data = action === "verify" ? await verifyCheckpoint(id, args[1]) : checkpointSummary(await getCheckpoint(id, args[1]), includeOutput); if (action === "verify" && !data.valid) process.exitCode = 1; }
191
+ else throw failure("Unknown checkpoint operation.");
192
+ output(`checkpoint.${action}`, Array.isArray(data) ? data.map(item => ({ ...item, execution_environment: "local" })) : { ...data, execution_environment: "local", verification_target: "explicit local input/artifact paths; cloud files are not mounted" }); return;
193
+ }
194
+ if (command === "resource") {
195
+ const action = args.shift(), threadInput = option(args, "--thread");
196
+ const threadId = threadInput === undefined ? undefined : await bound(threadInput);
197
+ if (supervisor && !threadId) throw failure("A supervisor resource command requires its --thread target.", "SUPERVISOR_CONTROL_FORBIDDEN");
198
+ let data;
199
+ if (["acquire", "run"].includes(action)) {
200
+ const settings = { owner: option(args, "--owner"), ttlMs: Number(option(args, "--ttl-ms", "600000")), reason: option(args, "--reason", ""), condition: option(args, "--condition", ""), threadId };
201
+ if (action === "run") {
202
+ const timeoutMs = Number(option(args, "--timeout-ms", "0")), includeOutput = flag(args, "--include-output");
203
+ if (args[1] !== "--" || args.length < 3) throw failure("Expected resource run <NAME> --owner NAME -- <COMMAND> [ARGS...].");
204
+ data = await runWithResource(args[0], args.slice(2), { ...settings, timeoutMs }); if (!includeOutput) delete data.output_tail; if (!data.valid) process.exitCode = 1;
205
+ } else { exact(args, 1); data = await acquireResource(args[0], settings); }
206
+ } else if (["renew", "release"].includes(action)) {
207
+ const token = option(args, "--token"), ttlMs = Number(option(args, "--ttl-ms", "600000")); exact(args, 1);
208
+ data = action === "renew" ? await renewResource(args[0], token, { ttlMs }) : await releaseResource(args[0], token);
209
+ } else if (action === "status") { exact(args, 1); data = await resourceStatus(args[0]); }
210
+ else if (action === "list") { exact(args, 0); data = await listResources(); }
211
+ else throw failure("Unknown resource operation.");
212
+ output(`resource.${action}`, Array.isArray(data) ? data.map(item => ({ ...item, execution_environment: "local" })) : { ...data, execution_environment: "local", scope: "local coordination and explicitly supplied local commands" }); return;
213
+ }
214
+ if (command === "threads") {
215
+ if (args.shift() !== "list") throw failure("Expected threads list.");
216
+ const limit = Number(option(args, "--limit", "20")), cursor = option(args, "--cursor"), environment = option(args, "--env"); exact(args, 0);
217
+ if (supervisor) throw failure("A bound supervisor cannot enumerate unrelated cloud tasks.", "SUPERVISOR_CONTROL_FORBIDDEN");
218
+ output("threads.list", await listCloudTasks({ limit, cursor, environment })); return;
219
+ }
220
+ if (command === "thread" && args[0] === "receipt") {
221
+ args.shift(); exact(args, 1);
222
+ if (supervisor) throw failure("A bound supervisor cannot inspect unrelated creation attempts.", "SUPERVISOR_CONTROL_FORBIDDEN");
223
+ output("thread.receipt", await cloudCreationReceipt(args[0])); return;
224
+ }
225
+ if (command === "thread" && args[0] === "create") {
226
+ args.shift(); if (supervisor) throw failure("A bound supervisor cannot create another task.", "SUPERVISOR_CONTROL_FORBIDDEN");
227
+ const project = option(args, "--project"), requestedBranch = option(args, "--branch"), dryRun = flag(args, "--dry-run");
228
+ if (project === undefined) throw failure("Cloud creation requires --project with an environment ID or exact name.");
229
+ const end = args.includes("--") ? args.indexOf("--") : args.length; if (args.slice(0, end).some(arg => arg.startsWith("--"))) throw failure("Unknown cloud creation option; explicit model/title/permission overrides are not implemented yet.");
230
+ const body = message(args); if (!body.trim()) throw failure("A non-empty initial instruction is required.");
231
+ const rows = (await cloudEnvironments()).environments, selector = project, matches = rows.filter(e => e.id === selector || e.name === selector);
232
+ if (matches.length !== 1) throw failure("The cloud environment selector must match one accessible ID or exact name.");
233
+ const selected = matches[0], branch = requestedBranch ?? (selected.repositories.length === 1 ? selected.repositories[0].default_branch : null);
234
+ if (!branch) throw failure("Specify --branch: this environment does not identify one repository default branch.");
235
+ const data = await createCloudTask(selected.id, branch, body, { dryRun });
236
+ output("thread.create", { ...data, thread_id: data.task_id ?? null, project_id: selected.id, project_name: selected.name, deep_link: data.url ?? null,
237
+ branch_source: requestedBranch === undefined ? "environment-default" : "explicit" }); return;
238
+ }
239
+ if (command === "projects") { exact(args, 1); if (args[0] !== "list") throw failure("Expected projects list."); if (supervisor) throw failure("A bound supervisor cannot enumerate unrelated cloud environments.", "SUPERVISOR_CONTROL_FORBIDDEN"); const data = await cloudEnvironments(); output("projects.list", { projects: data.environments.map(e => ({ ...e, project_id: e.id, execution_environment: "cloud" })) }); return; }
240
+ if (command === "doctor") {
241
+ const target = option(args, "--thread"), backend = option(args, "--backend", "app-server"); exact(args, 0);
242
+ if (backend !== "app-server") throw failure("Cloud cannot use Desktop UI.", "CLOUD_TARGET_CONFLICT");
243
+ if (target) {
244
+ const id = await bound(target, { allowUnregistered: true });
245
+ await observeThread(id, { connection, limit: 1 }, dependencies);
246
+ } else { if (supervisor) throw failure("A bound supervisor doctor requires its target task.", "SUPERVISOR_CONTROL_FORBIDDEN"); await cloudEnvironments(); }
247
+ output("doctor", { ready: true, backend: "codex-api", execution_environment: "cloud", version: VERSION,
248
+ codex_steer_compatibility: { status: "compatible" }, connection: { status: "connected", mode: "cloud" },
249
+ observation_compatibility: { status: target ? "verified" : "unverified" },
250
+ runtime_compatibility: { operations: Object.fromEntries(["read", "watch", "monitor", "history_check", "send", "send_new_turn"].map(name => [name, { status: "supported" }])) },
251
+ active_send_semantics: "interrupt-and-follow-up; same task, new turn", unverified_features: ["approval_roundtrip", "atomic_server_freshness"] }); return;
252
+ }
253
+ if (command === "thread" && args[0] === "resolve") { args.shift(); exact(args, 1); const id = targetId(args[0]); output("thread.resolve", { thread_id: id, task_id: id, deep_link: `https://chatgpt.com/codex/tasks/${id}`, execution_environment: "cloud" }); return; }
254
+ if (command === "open") {
255
+ exact(args, 1); const id = await bound(args[0]), url = `https://chatgpt.com/codex/tasks/${id}`;
256
+ if (process.platform !== "darwin") throw failure("Open this task URL in your browser; native browser launch is only implemented on macOS.", "CLOUD_CAPABILITY_UNSUPPORTED");
257
+ const result = spawnSync("/usr/bin/open", [url], { encoding: "utf8", timeout: 5000 }); if (result.status !== 0) throw failure("Could not open the cloud task in the browser.", "CLOUD_OPEN_FAILED");
258
+ output("open", { thread_id: id, deep_link: url, execution_environment: "cloud" }); return;
259
+ }
260
+ if (command === "diff") {
261
+ const attempt = option(args, "--attempt"), timeout = option(args, "--timeout-ms"); exact(args, 1);
262
+ const data = await cloudTaskDiff(await bound(args[0]), { ...(attempt === undefined ? {} : { attempt: Number(attempt) }), ...(timeout === undefined ? {} : { timeoutMs: Number(timeout) }) });
263
+ if (json) output("diff", data);
264
+ else if (data.diff === null) console.error("No server-recorded patch is available. This does not prove the cloud working copy has no changes; inspect read command evidence.");
265
+ else process.stdout.write(data.diff);
266
+ return;
267
+ }
268
+ if (command !== "send") throw failure("This cloud operation is not implemented yet. No Desktop fallback was attempted.", "CLOUD_CAPABILITY_UNSUPPORTED");
269
+ const dryRun = flag(args, "--dry-run"), newTurn = flag(args, "--new-turn"), backend = option(args, "--backend", "app-server");
270
+ const noSound = flag(args, "--no-sound"), explicitSound = flag(args, "--sound"); if (noSound && explicitSound) throw failure("--sound and --no-sound cannot be combined.");
271
+ if (backend !== "app-server") throw failure("Cloud cannot send through Desktop UI.", "CLOUD_TARGET_CONFLICT");
272
+ const options = { dryRun, newTurn, supervisor, connection, evidence: repeated(args, "--evidence") };
273
+ for (const [key, name] of Object.entries({ source: "--source", kind: "--kind", basedOn: "--based-on", supersedes: "--supersedes", expiresAt: "--expires-at", checkpoint: "--checkpoint", finding: "--finding" })) { const value = option(args, name); if (value !== undefined) options[key] = value; }
274
+ const id = targetId(args.shift()), end = args.includes("--") ? args.indexOf("--") : args.length;
275
+ if (args.slice(0, end).some(arg => arg.startsWith("--"))) throw failure("Unknown send option. Use -- before instruction text starting with --.");
276
+ const body = message(args); if (!body.trim()) throw failure("send requires a non-empty message.");
277
+ const receipt = await sendTrackedMessage(id, body, options);
278
+ receipt.sound = noSound ? { played: false, reason: "disabled" } : playSendSound(receipt);
279
+ output("send", receipt);
280
+ }
281
+
282
+ async function desktopCloudCommands(command, args, { supervisor, output, bound, dependencies }) {
283
+ if (!["projects", "environments", "threads", "thread", "doctor", "diff", "open"].includes(command)) return false;
284
+ if (supervisor && ["projects", "environments", "threads", "thread"].includes(command)) throw failure("A supervisor cannot enumerate or create other tasks.", "SUPERVISOR_CONTROL_FORBIDDEN");
285
+ if (["projects", "environments"].includes(command)) {
286
+ exact(args, 1); if (args[0] !== "list") throw failure("Expected list.");
287
+ if (command === "projects") { output("projects.list", { projects: (await listDesktopProjects()).flatMap(row => row.desktop_project_ids.map(id => ({ project_id: id, canonical_project_id: row.project_id, name: row.name, roots: row.roots }))), catalog_source: "desktop-app-server", assignment_requires_exact_desktop_executor: true }); return true; }
288
+ output("environments.list", await desktopCloudEnvironments()); return true;
289
+ }
290
+ if (command === "threads") {
291
+ if (args.shift() !== "list") throw failure("Expected threads list.");
292
+ const limit = Number(option(args, "--limit", "20")), cursor = option(args, "--cursor"); exact(args, 0);
293
+ output("threads.list", await listDesktopCloudTasks({ limit, cursor })); return true;
294
+ }
295
+ if (command === "thread") {
296
+ const action = args.shift();
297
+ if (action === "create") {
298
+ const environment = option(args, "--environment"), project = option(args, "--project"), title = option(args, "--title"), dryRun = flag(args, "--dry-run");
299
+ const end = args.includes("--") ? args.indexOf("--") : args.length;
300
+ if (args.slice(0, end).some(arg => arg.startsWith("--"))) throw failure("Unknown Desktop Cloud creation option.");
301
+ output("thread.create", await createDesktopCloudTask(environment, message(args), { project, title, dryRun })); return true;
302
+ }
303
+ if (action === "assign") { const project = option(args, "--project"); exact(args, 1); const data = await assignDesktopCloudProject(await bound(args[0]), project); output("thread.assign", data); if (data.verification_status !== "verified") process.exitCode = 1; return true; }
304
+ exact(args, 1);
305
+ if (action === "receipt") { let data; try { data = await desktopCloudReceipt(args[0]); } catch (error) { if (error.code !== "DESKTOP_CLOUD_CREATION_RECEIPT_NOT_FOUND") throw error; data = await desktopCloudProjectReceipt(args[0]); } output("thread.receipt", data); return true; }
306
+ if (action === "resolve") { const id = await bound(args[0]); output("thread.resolve", { thread_id: id, deep_link: desktopCloudLink(id) }); return true; }
307
+ throw failure("Expected thread create|assign|receipt|resolve.");
308
+ }
309
+ if (command === "doctor") {
310
+ const target = option(args, "--thread"), project = option(args, "--project"); exact(args, 0);
311
+ if (project !== undefined && !target) throw failure("--project diagnostics require --thread.");
312
+ const id = target ? await bound(target, { allowUnregistered: true }) : null;
313
+ if (id) await observeThread(id, { connection: "desktop-cloud", limit: 1 }, dependencies);
314
+ else { if (supervisor) throw failure("A supervisor doctor needs its target."); await desktopCloudEnvironments(); }
315
+ output("doctor", { ready: true, connection_mode: "desktop-cloud", observation: target ? "verified" : "unverified", project_membership: id ? await desktopCloudProjectDoctor(id, project) : { membership: { status: "unverified" }, assignment_api: { status: "unverified" } }, approval_roundtrip: "unverified", atomic_server_freshness: "unverified" }); return true;
316
+ }
317
+ if (command === "open") { exact(args, 1); output("open", { deep_link: desktopCloudLink(await bound(args[0])) }); return true; }
318
+ throw failure("Desktop Cloud diff retrieval is not verified. Inspect file-change events with read --include-output.", "CAPABILITY_UNVERIFIED");
319
+ }
@@ -0,0 +1,64 @@
1
+ import { cloudTaskId } from "./cloud-id.mjs";
2
+ import { readCloudTask } from "./cloud-observe.mjs";
3
+ import { codexHome } from "./runtime.mjs";
4
+
5
+ const status = value => ["in_progress", "pending", "running", "queued"].includes(value) ? "inProgress"
6
+ : value === "cancelled" ? "interrupted" : value === "failed" || value === "error" ? "failed" : value === "completed" ? "completed" : "unknown";
7
+
8
+ // Adapt public cloud history into the same observation model used by the local
9
+ // monitor, freshness checks and directive journal. Keep backend IDs unchanged.
10
+ export function cloudThread(snapshot) {
11
+ const taskId = cloudTaskId(snapshot.task_id);
12
+ const turns = snapshot.turns.map(turn => ({ id: turn.turn_id, status: turn.role === "user" ? "completed" : status(turn.status), items: [
13
+ ...turn.commands.map(command => ({ id: command.id, type: "commandExecution", command: command.command, cwd: null,
14
+ status: status(command.status === "inProgress" ? "in_progress" : command.status), exitCode: command.exit_code, aggregatedOutput: command.output ?? "" })),
15
+ ...(turn.progress ?? []).map((message, index) => ({ id: `${turn.turn_id}:progress:${message.id === undefined ? `index:${index}` : `id:${message.id}`}`, type: "agentMessage",
16
+ phase: "commentary", status: status(turn.status), text: message.text })),
17
+ ...turn.messages.map((message, index) => turn.role === "user"
18
+ ? { id: `${turn.turn_id}:message:${index}`, type: "userMessage", clientId: null, cloudParentTurnId: turn.parent_turn_id,
19
+ content: [{ type: "text", text: message.text }] }
20
+ : { id: `${turn.turn_id}:message:${index}`, type: "agentMessage", phase: "final_answer", status: status(turn.status), text: message.text }),
21
+ ] }));
22
+ const active = turns.filter(turn => turn.status === "inProgress");
23
+ return { id: taskId, name: snapshot.title ?? null, cwd: null, historyMode: "aggregate", canAcceptDirectInput: true,
24
+ status: { type: active.length ? "active" : status(snapshot.status) === "unknown" ? "systemError" : "idle", activeFlags: [] },
25
+ turns, cloud_snapshot: snapshot };
26
+ }
27
+
28
+ export class CloudReadClient {
29
+ constructor({ read = readCloudTask, timeoutMs = 30000, signal } = {}) {
30
+ this.read = read; this.timeoutMs = timeoutMs; this.controller = new AbortController();
31
+ this.cancel = () => this.close(); signal?.addEventListener("abort", this.cancel, { once: true });
32
+ this.signal = signal; if (signal?.aborted) this.close();
33
+ }
34
+ async request(method, params = {}, options = {}) {
35
+ if (this.controller.signal.aborted) throw Object.assign(new Error("Cloud observation was cancelled."), { code: "CANCELLED" });
36
+ if (method !== "thread/read" || options.mutation) throw Object.assign(new Error("Unsupported cloud observation method."), { code: "CAPABILITY_UNSUPPORTED" });
37
+ const id = cloudTaskId(params.threadId);
38
+ try {
39
+ if (!params.includeTurns || this.cached?.task_id !== id) this.cached = await this.read(id, { timeoutMs: this.timeoutMs, signal: this.controller.signal });
40
+ const thread = cloudThread(this.cached);
41
+ if (params.includeTurns) this.cached = null;
42
+ return { thread };
43
+ } catch (error) {
44
+ const mapped = { CLOUD_CONNECTION_FAILED: "CONNECTION_FAILED", CLOUD_RATE_LIMITED: "CONNECTION_FAILED", CLOUD_TIMEOUT: "TIMEOUT", CLOUD_AUTH_TIMEOUT: "TIMEOUT", CLOUD_STATE_CHANGED: "OBSERVATION_CHANGED", CLOUD_CANCELLED: "CANCELLED" }[error.code];
45
+ if (mapped) throw Object.assign(new Error(error.message), { code: mapped, cause_code: error.code,
46
+ ...(error.retry_after_ms === undefined ? {} : { retry_after_ms: error.retry_after_ms }) });
47
+ throw error;
48
+ }
49
+ }
50
+ close() { this.controller.abort(); this.signal?.removeEventListener("abort", this.cancel); this.cached = null; }
51
+ }
52
+
53
+ export function cloudConnection(dependencies = {}) {
54
+ return {
55
+ async discover(home = codexHome()) {
56
+ return { paths: { home, socket: "codex-cloud" }, state: { codex_home: home, cloud_connection: true,
57
+ codex_steer_protocol: 1, codex_steer_capabilities: { thread_read: [1], history_pagination: [], turn_steer: [], turn_start: [], desktop_subscribe: [] } } };
58
+ },
59
+ async connect(endpoint, options = {}) {
60
+ if (endpoint !== "codex-cloud") throw Object.assign(new Error("Invalid cloud observation endpoint."), { code: "CLOUD_TARGET_CONFLICT" });
61
+ return new CloudReadClient({ ...dependencies, ...options });
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,16 @@
1
+ const pattern = /^task_[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/;
2
+ const failure = () => Object.assign(new Error("Invalid Codex Cloud task. Use a task ID or an official chatgpt.com/codex/tasks link."), { code: "CLOUD_OPTIONS_INVALID" });
3
+
4
+ export function isCloudTask(value) {
5
+ return typeof value === "string" && (value.startsWith("task_") || /^https:\/\/chatgpt\.com\/codex\/tasks\//.test(value));
6
+ }
7
+
8
+ export function cloudTaskId(value) {
9
+ if (typeof value !== "string") throw failure();
10
+ if (pattern.test(value)) return value;
11
+ try {
12
+ const url = new URL(value), id = url.pathname.slice("/codex/tasks/".length);
13
+ if (url.origin === "https://chatgpt.com" && !url.username && !url.password && url.pathname.startsWith("/codex/tasks/") && pattern.test(id)) return id;
14
+ } catch {}
15
+ throw failure();
16
+ }