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.
Files changed (82) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +127 -14
  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 +11 -19
  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 +20 -45
  16. package/package.json +11 -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 +201 -23
  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 +12 -1
  30. package/src/creation-verification.mjs +30 -10
  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 +2 -2
  38. package/src/evidence.mjs +3 -0
  39. package/src/findings.mjs +7 -7
  40. package/src/help.mjs +140 -28
  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 +10 -2
  45. package/src/native-rpc.mjs +15 -2
  46. package/src/observe.mjs +21 -7
  47. package/src/orchestrate-prompt.mjs +96 -0
  48. package/src/orchestrate.mjs +29 -0
  49. package/src/paged-read.mjs +7 -1
  50. package/src/projects.mjs +27 -2
  51. package/src/prompt-command.mjs +25 -0
  52. package/src/prompt.mjs +25 -8
  53. package/src/remote-client-info.mjs +16 -0
  54. package/src/remote-control-auth.mjs +109 -0
  55. package/src/remote-control-socket.mjs +137 -0
  56. package/src/remote-control.mjs +134 -0
  57. package/src/remote-device-key.mjs +41 -0
  58. package/src/remote-device-login.mjs +76 -0
  59. package/src/remote-device-protocol.mjs +72 -0
  60. package/src/remote-device-store.mjs +71 -0
  61. package/src/remote-device-worker.mjs +48 -0
  62. package/src/remote-device.mjs +202 -0
  63. package/src/remote-io.mjs +163 -0
  64. package/src/remote-key-access.mjs +76 -0
  65. package/src/remote-key-worker.mjs +38 -0
  66. package/src/remote-keychain.mjs +72 -0
  67. package/src/remote-management.mjs +234 -0
  68. package/src/remote.mjs +132 -0
  69. package/src/resource.mjs +8 -6
  70. package/src/runner.mjs +6 -1
  71. package/src/standalone-create.mjs +4 -3
  72. package/src/store.mjs +6 -3
  73. package/src/supervise.mjs +25 -53
  74. package/src/supervision.mjs +41 -19
  75. package/src/target.mjs +25 -0
  76. package/src/task-id.mjs +23 -0
  77. package/src/thread-create.mjs +4 -3
  78. package/src/thread-id.mjs +21 -6
  79. package/src/thread-verify.mjs +108 -0
  80. package/src/turn-repair.mjs +228 -0
  81. package/src/update.mjs +158 -0
  82. 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 { listProjects } from "./projects.mjs";
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 console.error(`codexteer: ${message}`);
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 = candidate === "supervise" && args[1] === "prompt" ? "supervise prompt"
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
- if (args.shift() !== "start") throw new Error("Expected: codexteer desktop start");
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" || args.length) throw new Error("Expected: codexteer projects list");
381
- const projects = await listProjects();
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
- const threads = await listLocalThreads({ limit, desktopOnly });
396
- success("threads.list", { threads, count: threads.length }, json);
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: takeOption(args, "--context-thread", undefined), thinking: takeOption(args, "--thinking", undefined), timeoutMs: takeOption(args, "--timeout-ms", undefined) };
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 !== "resolve") throw new Error("Expected: codexteer thread create|resolve. See help thread.");
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
+ }