codexteer 0.17.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.
- package/CHANGELOG.md +28 -1
- package/README.md +127 -14
- package/assets/remote-device-key.swift +94 -0
- package/bin/codexteer-update.mjs +3 -0
- package/bin/codexteer.mjs +6 -2
- package/docs/cloud.md +113 -0
- package/docs/desktop-cloud.md +36 -0
- package/docs/distribution.md +11 -19
- package/docs/orchestration.md +120 -0
- package/docs/remote-cli-validation.json +139 -0
- package/docs/remote-control.md +65 -0
- package/docs/remote-device-enrollment.md +91 -0
- package/docs/remote-observation.md +57 -0
- package/docs/remote-operations.md +89 -0
- package/docs/thread-create.md +20 -45
- package/package.json +11 -2
- package/scripts/update-desktop.applescript +51 -0
- package/src/agent-launch.mjs +27 -0
- package/src/app-server.mjs +34 -10
- package/src/checkpoint.mjs +52 -16
- package/src/cli.mjs +201 -23
- package/src/cloud-api.mjs +118 -0
- package/src/cloud-cli.mjs +319 -0
- package/src/cloud-connection.mjs +64 -0
- package/src/cloud-id.mjs +16 -0
- package/src/cloud-observe.mjs +178 -0
- package/src/cloud-send.mjs +33 -0
- package/src/cloud.mjs +172 -0
- package/src/connection.mjs +12 -1
- package/src/creation-verification.mjs +30 -10
- package/src/desktop-cloud-api.mjs +119 -0
- package/src/desktop-cloud-id.mjs +18 -0
- package/src/desktop-cloud-project.mjs +148 -0
- package/src/desktop-cloud.mjs +98 -0
- package/src/desktop.mjs +3 -1
- package/src/directive.mjs +5 -2
- package/src/distribution.mjs +2 -2
- package/src/evidence.mjs +3 -0
- package/src/findings.mjs +7 -7
- package/src/help.mjs +140 -28
- package/src/journal.mjs +37 -9
- package/src/launcher.mjs +16 -9
- package/src/monitor.mjs +18 -6
- package/src/native-command.mjs +10 -2
- package/src/native-rpc.mjs +15 -2
- package/src/observe.mjs +21 -7
- package/src/orchestrate-prompt.mjs +96 -0
- package/src/orchestrate.mjs +29 -0
- package/src/paged-read.mjs +7 -1
- package/src/projects.mjs +27 -2
- package/src/prompt-command.mjs +25 -0
- package/src/prompt.mjs +25 -8
- package/src/remote-client-info.mjs +16 -0
- package/src/remote-control-auth.mjs +109 -0
- package/src/remote-control-socket.mjs +137 -0
- package/src/remote-control.mjs +134 -0
- package/src/remote-device-key.mjs +41 -0
- package/src/remote-device-login.mjs +76 -0
- package/src/remote-device-protocol.mjs +72 -0
- package/src/remote-device-store.mjs +71 -0
- package/src/remote-device-worker.mjs +48 -0
- package/src/remote-device.mjs +202 -0
- package/src/remote-io.mjs +163 -0
- package/src/remote-key-access.mjs +76 -0
- package/src/remote-key-worker.mjs +38 -0
- package/src/remote-keychain.mjs +72 -0
- package/src/remote-management.mjs +234 -0
- package/src/remote.mjs +132 -0
- package/src/resource.mjs +8 -6
- package/src/runner.mjs +6 -1
- package/src/standalone-create.mjs +4 -3
- package/src/store.mjs +6 -3
- package/src/supervise.mjs +25 -53
- package/src/supervision.mjs +41 -19
- package/src/target.mjs +25 -0
- package/src/task-id.mjs +23 -0
- package/src/thread-create.mjs +4 -3
- package/src/thread-id.mjs +21 -6
- package/src/thread-verify.mjs +108 -0
- package/src/turn-repair.mjs +228 -0
- package/src/update.mjs +158 -0
- package/docs/thread-create-native-investigation.md +0 -136
package/src/cli.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { desktopDoctor, inspectDesktopUi, openDesktopThread, sendDesktopMessage } from "./desktop.mjs";
|
|
3
|
-
import { normalizeThreadId, threadDeepLink } from "./thread-id.mjs";
|
|
3
|
+
import { normalizeThreadId, threadDeepLink, parseThreadTarget } from "./thread-id.mjs";
|
|
4
|
+
import { targetHost, targetContext, withTarget, validateHostId, hostFields, remoteReadOnly, remoteTransport, validateRemoteTransport, isControlTransport } from "./target.mjs";
|
|
4
5
|
import { listLocalThreads } from "./thread-store.mjs";
|
|
5
6
|
import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
|
|
7
|
+
import { launchDesktopUpdate, desktopUpdateStatus } from "./update.mjs";
|
|
6
8
|
import { appServerDoctor, startDesktop } from "./launcher.mjs";
|
|
7
9
|
import { VERSION, helpData, renderHelp } from "./help.mjs";
|
|
8
10
|
import { observeThread, printObservation } from "./observe.mjs";
|
|
@@ -12,33 +14,65 @@ import { createCheckpoint, listCheckpoints, getCheckpoint, checkpointSummary, ve
|
|
|
12
14
|
import { acquireResource, resourceStatus, renewResource, releaseResource, listResources, runWithResource } from "./resource.mjs";
|
|
13
15
|
import { playSendSound } from "./sound.mjs";
|
|
14
16
|
import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
|
|
17
|
+
import { prepareOrchestrationPrompt, orchestrateAgent } from "./orchestrate.mjs";
|
|
15
18
|
import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
|
|
16
19
|
import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
|
|
17
20
|
import { connectionMode, observationConnection, desktopReadOnly } from "./connection.mjs";
|
|
18
|
-
import {
|
|
21
|
+
import { repairTurns } from "./turn-repair.mjs";
|
|
22
|
+
import { listProjects, registeredRemoteProjects } from "./projects.mjs";
|
|
19
23
|
import { createProjectThread } from "./thread-create.mjs";
|
|
24
|
+
import { verifyProjectThread } from "./thread-verify.mjs";
|
|
25
|
+
import { withDeviceAccount, prepareDevice, recoverDeviceKey, enrollDevice, pairDevice, verifyDevice, deviceStatus, deviceIdentityInfo, readPairingCode } from "./remote-device.mjs";
|
|
26
|
+
import { readDevice } from "./remote-device-store.mjs";
|
|
27
|
+
import { listRemoteProjects, listRemoteThreads, createRemoteProjectThread, verifyRemoteProjectThread, ensureRemoteDesktopSubscription } from "./remote-management.mjs";
|
|
20
28
|
|
|
21
29
|
const DEFAULT_SEND_BACKEND = "app-server";
|
|
22
30
|
|
|
23
31
|
function success(command, data, json) {
|
|
32
|
+
if (data && !Array.isArray(data)) Object.assign(data, hostFields());
|
|
24
33
|
if (json) console.log(JSON.stringify({ ok: true, command, data }));
|
|
25
34
|
return data;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
function fail(error, json) {
|
|
37
|
+
export function fail(error, json) {
|
|
29
38
|
const message = error instanceof Error ? error.message : String(error);
|
|
30
39
|
if (json) console.log(JSON.stringify({ ok: false, ...(error.watch ? { command: "watch", data: error.watch } : {}), error: {
|
|
31
|
-
message, code: error.code, delivery_status: error.delivery_status,
|
|
40
|
+
message, code: error.code, ...hostFields(), delivery_status: error.delivery_status,
|
|
32
41
|
operation: error.operation, capability: error.capability,
|
|
42
|
+
...((error.execution_environment === "cloud" || (typeof error.code === "string" && error.code.startsWith("CLOUD_"))) ? {
|
|
43
|
+
execution_environment: error.execution_environment, task_id: error.task_id, url: error.url,
|
|
44
|
+
submission_status: error.submission_status, uncertain: error.uncertain,
|
|
45
|
+
cli_exit_code: error.cli_exit_code, cli_signal: error.cli_signal,
|
|
46
|
+
http_status: error.http_status, retry_after_ms: error.retry_after_ms,
|
|
47
|
+
} : {}),
|
|
33
48
|
sent: error.sent, thread_id: error.thread_id, rpc_code: error.rpc_code ?? error.rpcCode,
|
|
34
49
|
message_id: error.message_id, client_message_id: error.client_message_id, journal_update_required: error.journal_update_required,
|
|
35
50
|
existing_message_id: error.existing_message_id,
|
|
51
|
+
existing_attempt_id: error.existing_attempt_id, existing_receipt_path: error.existing_receipt_path,
|
|
36
52
|
supervision_update_required: error.supervision_update_required,
|
|
37
|
-
receipt_path: error.receipt_path,
|
|
53
|
+
history_changed: error.history_changed, backup_path: error.backup_path, receipt_path: error.receipt_path,
|
|
54
|
+
repair_stat: error.repair_stat,
|
|
38
55
|
creation_status: error.creation_status, attempt_id: error.attempt_id, receipt_update_required: error.receipt_update_required,
|
|
56
|
+
reservation_cleanup_required: error.reservation_cleanup_required,
|
|
39
57
|
native_call_id: error.native_call_id,
|
|
58
|
+
uncertain: error.uncertain, resource: error.resource, lease_retained: error.lease_retained, remote_execution_status: error.remote_execution_status,
|
|
59
|
+
remote_process_id: error.remote_process_id,
|
|
60
|
+
verification_checks: error.verification_checks,
|
|
61
|
+
remote_control_auth: error.remote_control_auth,
|
|
40
62
|
} }));
|
|
41
|
-
else
|
|
63
|
+
else {
|
|
64
|
+
console.error(`codexteer: ${message}`);
|
|
65
|
+
if (error.execution_environment === "cloud" || (typeof error.code === "string" && error.code.startsWith("DESKTOP_CLOUD_"))) {
|
|
66
|
+
// Recovery metadata must survive an uncertain submission or a stale
|
|
67
|
+
// on-disk receipt. Never print the child error, prompt, or diagnostics.
|
|
68
|
+
for (const key of ["creation_status", "submission_status", "delivery_status", "attempt_id", "receipt_path", "existing_attempt_id", "existing_receipt_path", "task_id", "thread_id", "url"]) {
|
|
69
|
+
if (typeof error[key] === "string") console.error(`${key}: ${JSON.stringify(error[key])}`);
|
|
70
|
+
}
|
|
71
|
+
if (error.receipt_update_required) console.error("receipt_update_required: true");
|
|
72
|
+
if (error.reservation_cleanup_required) console.error("reservation_cleanup_required: true");
|
|
73
|
+
if (Number.isSafeInteger(error.retry_after_ms)) console.error(`retry_after_ms: ${error.retry_after_ms}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
42
76
|
process.exitCode = 1;
|
|
43
77
|
}
|
|
44
78
|
|
|
@@ -64,6 +98,23 @@ function takeOptions(args, name) {
|
|
|
64
98
|
return values;
|
|
65
99
|
}
|
|
66
100
|
|
|
101
|
+
function routingThread(probe) {
|
|
102
|
+
const valued = new Set(["--backend", "--source", "--kind", "--evidence", "--based-on", "--supersedes", "--expires-at", "--checkpoint", "--finding",
|
|
103
|
+
"--wait-ms", "--limit", "--max-chars", "--since", "--until", "--timeout-ms", "--poll-ms", "--notify", "--settle-ms", "--max-hold-ms",
|
|
104
|
+
"--owner", "--agent", "--status", "--note", "--by", "--reason", "--condition", "--ttl-ms", "--token", "--cwd", "--exec-backend", "--shell", "--key", "--title", "--rating", "--path", "--exclude", "--artifact"]);
|
|
105
|
+
const positional = [];
|
|
106
|
+
for (let i = 0; i < probe.length; i++) {
|
|
107
|
+
if (probe[i] === "--") { positional.push(...probe.slice(i + 1)); break; }
|
|
108
|
+
if (probe[i].startsWith("--")) { if (valued.has(probe[i])) i++; continue; }
|
|
109
|
+
positional.push(probe[i]);
|
|
110
|
+
}
|
|
111
|
+
const [command, action] = positional;
|
|
112
|
+
if (/^(codex:\/\/|[0-9a-f]{8}-)/i.test(command ?? "")) return command;
|
|
113
|
+
const nested = ["history", "instructions", "findings", "checkpoint", "thread"].includes(command)
|
|
114
|
+
|| (command === "supervise" && ["prompt", "register", "status", "pause", "resume", "stop", "list"].includes(action));
|
|
115
|
+
return positional[nested ? 2 : 1];
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
function readMessage(parts) {
|
|
68
119
|
if (parts[0] === "--") parts.shift();
|
|
69
120
|
if (parts.length === 1 && parts[0] === "-") return readFileSync(0, "utf8");
|
|
@@ -106,6 +157,35 @@ function printSupervisor(data) {
|
|
|
106
157
|
}
|
|
107
158
|
|
|
108
159
|
export async function main(argv) {
|
|
160
|
+
const args = [...argv];
|
|
161
|
+
try {
|
|
162
|
+
const transport = validateRemoteTransport(takeOption(args, "--remote-transport", "native"));
|
|
163
|
+
const explicitHost = takeOption(args, "--host", undefined);
|
|
164
|
+
if (args.slice(0, args.includes("--") ? args.indexOf("--") : args.length).includes("--host")) throw new Error("Specify --host only once.");
|
|
165
|
+
if (explicitHost !== undefined) validateHostId(explicitHost);
|
|
166
|
+
const probe = [...args];
|
|
167
|
+
takeFlag(probe, "--json");
|
|
168
|
+
for (const flag of ["--connection", "--supervisor", "--require-node-version"]) takeOption(probe, flag, undefined);
|
|
169
|
+
takeOption(probe, "--context-thread", undefined);
|
|
170
|
+
const command = probe[0];
|
|
171
|
+
const contextThread = takeOption(args, "--context-thread", undefined);
|
|
172
|
+
let candidate;
|
|
173
|
+
if (command === "doctor") candidate = takeOption(probe, "--thread", undefined);
|
|
174
|
+
else if (command === "resource") candidate = takeOption(probe, "--thread", undefined);
|
|
175
|
+
else if (["read", "status", "watch", "send", "open", "debug-ui", "supervise", "history", "instructions", "findings", "checkpoint"].includes(command)
|
|
176
|
+
|| (command === "thread" && ["resolve", "repair"].includes(probe[1])) || /^(codex:\/\/|[0-9a-f]{8}-)/i.test(command ?? "")) {
|
|
177
|
+
candidate = routingThread(probe);
|
|
178
|
+
}
|
|
179
|
+
const linkHost = candidate === undefined ? undefined : parseThreadTarget(candidate).hostId;
|
|
180
|
+
if (explicitHost !== undefined && linkHost !== undefined && explicitHost !== linkHost) throw Object.assign(new Error("--host conflicts with the thread link hostId."), { code: "HOST_MISMATCH" });
|
|
181
|
+
const host = linkHost ?? explicitHost ?? "local";
|
|
182
|
+
const ownArguments = probe.slice(0, probe.includes("--") ? probe.indexOf("--") : probe.length);
|
|
183
|
+
if (isControlTransport(transport) && host === "local" && command !== "remote" && !["help", "--help", "--version"].some(value => ownArguments.includes(value))) throw Object.assign(new Error("--remote-transport control requires a remote hostId link or --host."), { code: "REMOTE_HOST_REQUIRED" });
|
|
184
|
+
return await withTarget(host, contextThread, () => mainOnTarget(args), { transport });
|
|
185
|
+
} catch (error) { fail(error, argv.includes("--json")); }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function mainOnTarget(argv) {
|
|
109
189
|
const args = [...argv];
|
|
110
190
|
let json = takeFlag(args, "--json");
|
|
111
191
|
|
|
@@ -127,7 +207,7 @@ export async function main(argv) {
|
|
|
127
207
|
if (args.length === 0 || takeFlag(args, "--help") || args[0] === "help") {
|
|
128
208
|
if (args[0] === "help") args.shift();
|
|
129
209
|
const candidate = args[0] ?? "overview";
|
|
130
|
-
const topic =
|
|
210
|
+
const topic = ["supervise", "orchestrate"].includes(candidate) && args[1] === "prompt" ? `${candidate} prompt`
|
|
131
211
|
: /^(codex:\/\/|[0-9a-f]{8}-)/i.test(candidate) ? "send" : candidate;
|
|
132
212
|
const data = helpData(topic);
|
|
133
213
|
success("help", data, json);
|
|
@@ -136,9 +216,32 @@ export async function main(argv) {
|
|
|
136
216
|
}
|
|
137
217
|
|
|
138
218
|
let command = args.shift();
|
|
219
|
+
const control = isControlTransport();
|
|
220
|
+
if (targetHost() !== "local" && ["desktop", "debug-ui"].includes(command)) throw Object.assign(new Error("Desktop process/update and Accessibility diagnostics cannot target the remote host. No local operation was run."), { code: "REMOTE_HOST_OPERATION_UNSUPPORTED" });
|
|
221
|
+
if (targetHost() !== "local" && !["remote", "projects", "read", "status", "watch", "doctor", "supervise", "findings", "history", "instructions", "thread", "open", ...(control ? ["send"] : []), ...(remoteTransport() === "paired" ? ["threads", "resource", "checkpoint", "orchestrate"] : [])].includes(command)
|
|
222
|
+
&& !(control && /^(codex:\/\/|[0-9a-f]{8}-)/i.test(command))) throw remoteReadOnly();
|
|
223
|
+
if (targetHost() !== "local" && command === "thread" && args[0] !== "resolve" && remoteTransport() !== "paired") throw remoteReadOnly();
|
|
224
|
+
if (targetHost() !== "local" && command === "thread" && args[0] === "repair") throw Object.assign(new Error("Offline rollout repair is supported only for validated local macOS/CLI versions. The remote history was not modified."), { code: "REMOTE_REPAIR_UNSUPPORTED" });
|
|
225
|
+
if (targetHost() !== "local" && command === "instructions" && args[0] !== "list" && !control) throw remoteReadOnly();
|
|
139
226
|
// An explicit observation endpoint can never silently route a mutation to
|
|
140
227
|
// the shared wrapper or UI. Local records/help remain available.
|
|
141
228
|
if (connection === "desktop" && !["supervise", "findings", "history", "instructions", "checkpoint", "resource", "read", "status", "watch", "doctor", "threads", "thread"].includes(command)) throw desktopReadOnly();
|
|
229
|
+
if (command === "remote") {
|
|
230
|
+
if (supervisor) throw Object.assign(new Error("A bound supervisor cannot enroll, pair or manage account credentials."), { code: "SUPERVISOR_MISMATCH" });
|
|
231
|
+
const action = args.shift(), codeStdin = takeFlag(args, "--code-stdin"), provider = takeOption(args, "--key-provider", undefined);
|
|
232
|
+
if (provider !== undefined && action !== "prepare") throw new Error("--key-provider is only available for remote prepare.");
|
|
233
|
+
if (!["prepare", "recover-key", "enroll", "status", "identity", "pair", "verify"].includes(action) || args.length || (codeStdin && action !== "pair") || (action === "pair" && !codeStdin)) throw new Error("Expected: remote prepare|recover-key|enroll|status|identity|verify, or --host HOST remote pair --code-stdin.");
|
|
234
|
+
if (["prepare", "enroll"].includes(action) && targetHost() !== "local") throw new Error("remote prepare/enroll registers this local controller; omit --host.");
|
|
235
|
+
// Code input is never an argv option or a chat message.
|
|
236
|
+
if (action === "pair" && !/^remote-control:env_[A-Za-z0-9_-]+$/.test(targetHost())) throw new Error("remote pair requires --host remote-control:env_...");
|
|
237
|
+
if (action === "pair" && process.stdin.isTTY) console.error("codexteer: Enter the selected host's fresh pairing code (hidden), then press Enter.");
|
|
238
|
+
const code = action === "pair" ? await readPairingCode() : undefined;
|
|
239
|
+
if (code && code.length > 128) throw new Error("Invalid pairing code input.");
|
|
240
|
+
const data = await withDeviceAccount(context => action === "prepare" ? prepareDevice(context, { ...(provider ? { provider } : {}) }) : action === "recover-key" ? recoverDeviceKey(context) : action === "enroll" ? enrollDevice(context, { progress: text => console.error(`codexteer: ${text}`) })
|
|
241
|
+
: action === "pair" ? pairDevice(context, targetHost(), code)
|
|
242
|
+
: action === "verify" ? verifyDevice(context) : action === "identity" ? deviceIdentityInfo(context) : readDevice(context.home, context.identity).then(deviceStatus));
|
|
243
|
+
success(`remote.${action}`, data, json); if (!json) console.log(JSON.stringify(data, null, 2)); return;
|
|
244
|
+
}
|
|
142
245
|
if (command === "supervise" && ["register", "status", "list", "pause", "resume", "stop"].includes(args[0])) {
|
|
143
246
|
const action = args.shift(), owner = action === "register" ? takeOption(args, "--owner", "external") : undefined;
|
|
144
247
|
if (args.length !== (action === "list" ? 0 : 1)) throw new Error(`Expected: supervise ${action}${action === "list" ? "" : " <THREAD>"}.`);
|
|
@@ -184,6 +287,28 @@ export async function main(argv) {
|
|
|
184
287
|
if (!json) console.log(JSON.stringify(data, null, 2));
|
|
185
288
|
return;
|
|
186
289
|
}
|
|
290
|
+
if (command === "orchestrate") {
|
|
291
|
+
if (supervisor) throw Object.assign(new Error("Managed supervision is bound to its existing task and cannot orchestrate a project."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
|
|
292
|
+
const promptOnly = args[0] === "prompt";
|
|
293
|
+
if (promptOnly) args.shift();
|
|
294
|
+
else if (json) throw new Error("orchestrate inherits agent output and does not support --json. Use help orchestrate --json or orchestrate prompt --project ID|NAME <GOAL> --json.");
|
|
295
|
+
const separator = promptOnly ? -1 : args.indexOf("--");
|
|
296
|
+
const ownArgs = separator < 0 ? args : args.slice(0, separator);
|
|
297
|
+
const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
|
|
298
|
+
const project = takeOption(ownArgs, "--project", undefined);
|
|
299
|
+
const agent = takeOption(ownArgs, "--agent", promptOnly ? undefined : "claude");
|
|
300
|
+
const usePathCli = takeFlag(ownArgs, "--use-path-cli");
|
|
301
|
+
if (promptOnly && ownArgs[0] === "--") ownArgs.shift();
|
|
302
|
+
else if (ownArgs.some(arg => arg.startsWith("--"))) throw new Error("Unknown orchestrate option. See help orchestrate; agent arguments belong after --.");
|
|
303
|
+
if (ownArgs.length !== 1) throw new Error("Expected: orchestrate [prompt] --project <ID-or-exact-name> <GOAL>. Quote the goal as one argument, or use - for stdin.");
|
|
304
|
+
const goal = ownArgs[0] === "-" ? readFileSync(0, "utf8") : ownArgs[0];
|
|
305
|
+
if (promptOnly) {
|
|
306
|
+
const data = await prepareOrchestrationPrompt(project, goal, { agent, connection, usePathCli });
|
|
307
|
+
success("orchestrate.prompt", data, json);
|
|
308
|
+
if (!json) console.log(data.prompt);
|
|
309
|
+
} else process.exitCode = await orchestrateAgent(project, goal, { agent, agentArgs, connection, usePathCli });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
187
312
|
if (command === "supervise" && args[0] === "prompt") {
|
|
188
313
|
args.shift();
|
|
189
314
|
const agent = takeOption(args, "--agent", "claude");
|
|
@@ -210,11 +335,12 @@ export async function main(argv) {
|
|
|
210
335
|
if (command === "resource") {
|
|
211
336
|
const action = args.shift(); let data;
|
|
212
337
|
if (["acquire", "run"].includes(action)) {
|
|
213
|
-
const options = { owner: takeOption(args, "--owner", undefined), ttlMs: Number(takeOption(args, "--ttl-ms", "600000")), reason: takeOption(args, "--reason", ""), condition: takeOption(args, "--condition", ""), threadId: takeOption(args, "--thread", undefined) };
|
|
338
|
+
const options = { owner: takeOption(args, "--owner", undefined), ttlMs: Number(takeOption(args, "--ttl-ms", "600000")), reason: takeOption(args, "--reason", ""), condition: takeOption(args, "--condition", ""), threadId: takeOption(args, "--thread", undefined), cwd: takeOption(args, "--cwd", undefined) };
|
|
214
339
|
if (action === "run") {
|
|
215
|
-
const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), includeOutput = takeFlag(args, "--include-output");
|
|
340
|
+
const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), includeOutput = takeFlag(args, "--include-output"), execBackend = takeOption(args, "--exec-backend", "app-server"), remoteShell = takeOption(args, "--shell", undefined);
|
|
341
|
+
if (supervisor) await assertSupervisor(options.threadId, supervisor, { connection, sending: execBackend === "thread-shell" });
|
|
216
342
|
if (args[1] !== "--" || args.length < 3) throw new Error("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
|
|
217
|
-
data = await runWithResource(args[0], args.slice(2), { ...options, timeoutMs });
|
|
343
|
+
data = await runWithResource(args[0], args.slice(2), { ...options, timeoutMs, execBackend, remoteShell, supervisor, connection });
|
|
218
344
|
if (!includeOutput) delete data.output_tail;
|
|
219
345
|
if (!data.valid) process.exitCode = 1;
|
|
220
346
|
} else {
|
|
@@ -241,9 +367,10 @@ export async function main(argv) {
|
|
|
241
367
|
if (args.length !== 2) throw new Error("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
|
|
242
368
|
data = await createCheckpoint(args[0], args[1], { paths, excludes });
|
|
243
369
|
} else if (action === "run") {
|
|
244
|
-
const timeoutMs = Number(takeOption(args, "--timeout-ms", "0"));
|
|
370
|
+
const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), execBackend = takeOption(args, "--exec-backend", "app-server"), remoteShell = takeOption(args, "--shell", undefined);
|
|
371
|
+
if (supervisor && execBackend === "thread-shell") await assertSupervisor(args[0], supervisor, { connection, sending: true });
|
|
245
372
|
if (args[2] !== "--" || args.length < 4) throw new Error("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
|
|
246
|
-
data = await runCheckpoint(args[0], args[1], args.slice(3), { timeoutMs });
|
|
373
|
+
data = await runCheckpoint(args[0], args[1], args.slice(3), { timeoutMs, execBackend, remoteShell, supervisor, connection });
|
|
247
374
|
if (!includeOutput) delete data.output_tail;
|
|
248
375
|
if (!data.valid) process.exitCode = 1;
|
|
249
376
|
} else if (action === "attach") {
|
|
@@ -338,8 +465,9 @@ export async function main(argv) {
|
|
|
338
465
|
if (record?.session_id === supervisor) await assertSupervisor(threadId, supervisor, { connection });
|
|
339
466
|
}
|
|
340
467
|
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
468
|
+
if (targetHost() !== "local" && backend === "ui") throw remoteReadOnly();
|
|
341
469
|
const report = backend === "ui" ? desktopDoctor() : await appServerDoctor({ threadId, ...connectionDependencies });
|
|
342
|
-
if (report.connection) Object.assign(report.connection, { mode: connection, observation_only: connection === "desktop" });
|
|
470
|
+
if (report.connection) Object.assign(report.connection, { mode: connection, observation_only: connection === "desktop" || (targetHost() !== "local" && !control), ...hostFields() });
|
|
343
471
|
report.default_send_backend = DEFAULT_SEND_BACKEND;
|
|
344
472
|
report.rollout_status = "enabled";
|
|
345
473
|
success("doctor", report, json);
|
|
@@ -366,7 +494,25 @@ export async function main(argv) {
|
|
|
366
494
|
}
|
|
367
495
|
|
|
368
496
|
if (command === "desktop") {
|
|
369
|
-
|
|
497
|
+
const action = args.shift();
|
|
498
|
+
if (action === "update-status") {
|
|
499
|
+
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
500
|
+
const data = await desktopUpdateStatus();
|
|
501
|
+
success("desktop.update-status", data, json);
|
|
502
|
+
if (!json) console.log(JSON.stringify(data, null, 2));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (action === "update") {
|
|
506
|
+
if (supervisor) throw Object.assign(new Error("A supervisor cannot update or restart Desktop. Run this command from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
|
|
507
|
+
const dryRun = takeFlag(args, "--dry-run");
|
|
508
|
+
const timeoutMs = Number(takeOption(args, "--timeout-ms", "600000"));
|
|
509
|
+
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
510
|
+
const data = await launchDesktopUpdate({ dryRun, timeoutMs });
|
|
511
|
+
success("desktop.update", data, json);
|
|
512
|
+
if (!json) console.log(dryRun ? "Dry run: official update, relaunch, normal quit, wrapper startup, and verification." : `Update worker started (${data.worker_pid}). Check: ${data.status_command}`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (action !== "start") throw new Error("Expected: codexteer desktop start|update|update-status");
|
|
370
516
|
const dryRun = takeFlag(args, "--dry-run");
|
|
371
517
|
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
372
518
|
const data = await startDesktop({ dryRun });
|
|
@@ -377,10 +523,15 @@ export async function main(argv) {
|
|
|
377
523
|
|
|
378
524
|
if (command === "projects") {
|
|
379
525
|
if (connection === "desktop") throw desktopReadOnly();
|
|
380
|
-
if (args.shift() !== "list"
|
|
381
|
-
const
|
|
526
|
+
if (args.shift() !== "list") throw new Error("Expected: codexteer projects list [--remote | --all] [--host HOST-ID]");
|
|
527
|
+
const remote = takeFlag(args, "--remote"), all = takeFlag(args, "--all");
|
|
528
|
+
if (args.length || (remote && all) || (all && targetHost() !== "local")) throw new Error("Use projects list [--remote | --all] or projects list --host HOST-ID.");
|
|
529
|
+
const saved = targetHost() !== "local" && remoteTransport() === "paired" ? await listRemoteProjects()
|
|
530
|
+
: remote || all || targetHost() !== "local" ? await registeredRemoteProjects() : [];
|
|
531
|
+
const projects = [...(!remote && (all || targetHost() === "local") ? await listProjects() : []),
|
|
532
|
+
...saved.filter(project => targetHost() === "local" || project.host_id === targetHost())];
|
|
382
533
|
success("projects.list", { projects, count: projects.length }, json);
|
|
383
|
-
if (!json) for (const project of projects) console.log(`${project.project_id} ${project.name}\n${project.roots.map(root => ` ${root}`).join("\n")}`);
|
|
534
|
+
if (!json) for (const project of projects) console.log(`${project.project_id} ${project.name}${project.host_id ? ` [${project.host_id}; availability: ${project.availability}]` : ""}\n${project.roots.map(root => ` ${root}`).join("\n")}`);
|
|
384
535
|
return;
|
|
385
536
|
}
|
|
386
537
|
|
|
@@ -388,18 +539,31 @@ export async function main(argv) {
|
|
|
388
539
|
if (args.shift() !== "list") throw new Error("Expected: codexteer threads list");
|
|
389
540
|
const limit = Number.parseInt(takeOption(args, "--limit", "20"), 10);
|
|
390
541
|
const desktopOnly = takeFlag(args, "--desktop-only");
|
|
542
|
+
const cursor = takeOption(args, "--cursor", undefined), projectId = takeOption(args, "--project", undefined), search = takeOption(args, "--search", undefined);
|
|
391
543
|
if (args.length > 0) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
392
544
|
if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
|
|
393
545
|
throw new Error("--limit must be an integer between 1 and 500.");
|
|
394
546
|
}
|
|
395
|
-
|
|
396
|
-
|
|
547
|
+
if (targetHost() === "local" && [cursor, projectId, search].some(value => value !== undefined)) throw new Error("--cursor/--project/--search currently require a paired remote host.");
|
|
548
|
+
const data = targetHost() === "local" ? { threads: await listLocalThreads({ limit, desktopOnly }) } : await listRemoteThreads({ limit, desktopOnly, cursor, projectId, search });
|
|
549
|
+
const threads = data.threads;
|
|
550
|
+
success("threads.list", { ...data, count: threads.length }, json);
|
|
397
551
|
if (!json) printThreads(threads);
|
|
398
552
|
return;
|
|
399
553
|
}
|
|
400
554
|
|
|
401
555
|
if (command === "thread") {
|
|
402
556
|
const action = args.shift();
|
|
557
|
+
if (action === "verify") {
|
|
558
|
+
if (connection === "desktop") throw desktopReadOnly();
|
|
559
|
+
if (supervisor) throw Object.assign(new Error("Managed supervision is bound to its existing task. Verify a project creation receipt without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
|
|
560
|
+
const timeoutMs = Number(takeOption(args, "--timeout-ms", "120000"));
|
|
561
|
+
if (args.length !== 1 || args[0].startsWith("--")) throw new Error("Expected: thread verify <ATTEMPT-ID> [--timeout-ms N].");
|
|
562
|
+
const data = await (targetHost() === "local" ? verifyProjectThread : verifyRemoteProjectThread)(args[0], { timeoutMs });
|
|
563
|
+
success("thread.verify", data, json);
|
|
564
|
+
if (!json) console.log(`Verified existing ${data.deep_link}; creation and initial instruction were not repeated.`);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
403
567
|
if (action === "create") {
|
|
404
568
|
if (connection === "desktop") throw desktopReadOnly();
|
|
405
569
|
if (supervisor) throw Object.assign(new Error("Managed supervision is bound to its existing task. Run thread create without --supervisor under the user's delegated creation scope."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
|
|
@@ -407,19 +571,29 @@ export async function main(argv) {
|
|
|
407
571
|
const options = { title: takeOption(args, "--title", undefined), cwd: takeOption(args, "--cwd", undefined), model: takeOption(args, "--model", undefined),
|
|
408
572
|
permissions: takeOption(args, "--permissions", undefined), sandbox: takeOption(args, "--sandbox", undefined),
|
|
409
573
|
approvalPolicy: takeOption(args, "--approval-policy", undefined), approvalsReviewer: takeOption(args, "--approvals-reviewer", undefined), dryRun: takeFlag(args, "--dry-run"),
|
|
410
|
-
contextThread:
|
|
574
|
+
contextThread: targetContext(), thinking: takeOption(args, "--thinking", undefined), timeoutMs: takeOption(args, "--timeout-ms", undefined) };
|
|
411
575
|
if (options.timeoutMs !== undefined) {
|
|
412
576
|
options.timeoutMs = Number(options.timeoutMs);
|
|
413
577
|
}
|
|
414
578
|
const literal = args.indexOf("--");
|
|
415
579
|
if (args.slice(0, literal < 0 ? args.length : literal).some(arg => arg.startsWith("--"))) throw new Error("Unknown thread create option. Use -- before an instruction starting with --.");
|
|
416
580
|
const message = readMessage(args);
|
|
417
|
-
const data = await createProjectThread(selector, message, options);
|
|
581
|
+
const data = await (targetHost() === "local" ? createProjectThread : createRemoteProjectThread)(selector, message, options);
|
|
418
582
|
success("thread.create", data, json);
|
|
419
583
|
if (!json) console.log(options.dryRun ? `Dry run: ${data.project_name} (${data.cwd}), ${data.message_characters} characters.` : `Created ${data.deep_link} in ${data.project_name}; initial instruction accepted${data.turn_id ? ` (turn ${data.turn_id})` : ""}.`);
|
|
420
584
|
return;
|
|
421
585
|
}
|
|
422
|
-
if (action
|
|
586
|
+
if (action === "repair") {
|
|
587
|
+
const apply = takeFlag(args, "--apply"), dryRun = takeFlag(args, "--dry-run");
|
|
588
|
+
if (args.length !== 1 || args[0].startsWith("--") || (apply && dryRun)) throw new Error("Expected: thread repair <THREAD> [--dry-run | --apply].");
|
|
589
|
+
if (connection === "desktop") throw desktopReadOnly();
|
|
590
|
+
if (supervisor) throw Object.assign(new Error("A supervisor cannot repair task history. Run this command from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
|
|
591
|
+
const data = await repairTurns(args[0], { apply });
|
|
592
|
+
success("thread.repair", data, json);
|
|
593
|
+
if (!json) console.log(JSON.stringify(data, null, 2));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (action !== "resolve") throw new Error("Expected: codexteer thread create|verify|resolve|repair. See help thread.");
|
|
423
597
|
if (args.length !== 1) throw new Error("thread resolve requires exactly one thread ID or URL.");
|
|
424
598
|
const threadId = normalizeThreadId(args[0]);
|
|
425
599
|
const data = { thread_id: threadId, deep_link: threadDeepLink(threadId) };
|
|
@@ -431,7 +605,7 @@ export async function main(argv) {
|
|
|
431
605
|
if (command === "open") {
|
|
432
606
|
if (args.length !== 1) throw new Error("open requires exactly one thread ID or URL.");
|
|
433
607
|
const threadId = normalizeThreadId(args[0]);
|
|
434
|
-
const data = openDesktopThread(threadId);
|
|
608
|
+
const data = targetHost() === "local" ? openDesktopThread(threadId) : await ensureRemoteDesktopSubscription(threadId);
|
|
435
609
|
success("open", data, json);
|
|
436
610
|
if (!json) console.log(`Opened ${data.deep_link}`);
|
|
437
611
|
return;
|
|
@@ -454,6 +628,10 @@ export async function main(argv) {
|
|
|
454
628
|
|
|
455
629
|
const dryRun = takeFlag(args, "--dry-run");
|
|
456
630
|
const backend = backendOption(args, DEFAULT_SEND_BACKEND);
|
|
631
|
+
if (targetHost() !== "local" && backend === "ui") {
|
|
632
|
+
if (remoteTransport() !== "paired") throw remoteReadOnly();
|
|
633
|
+
throw Object.assign(new Error("UI sending on the remote host is unsupported. No UI or other delivery route was used."), { code: "REMOTE_UI_UNSUPPORTED", sent: false, delivery_status: "not_sent" });
|
|
634
|
+
}
|
|
457
635
|
if (supervisor && backend !== "app-server") throw new Error("Managed supervision requires the app-server backend.");
|
|
458
636
|
const explicitSound = takeFlag(args, "--sound");
|
|
459
637
|
const noSound = takeFlag(args, "--no-sound");
|
|
@@ -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
|
+
}
|