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
@@ -4,14 +4,17 @@ import path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { fingerprintFile, collectEvidence, verifyEvidence } from "./evidence.mjs";
6
6
  import { digest } from "./observe.mjs";
7
- import { normalizeThreadId } from "./thread-id.mjs";
7
+ import { normalizeTaskId } from "./task-id.mjs";
8
8
  import { readRecord, writeRecord, listRecords, withStoreLock } from "./store.mjs";
9
9
  import { runCommand } from "./runner.mjs";
10
+ import { targetHost } from "./target.mjs";
11
+ import { remoteCaptureInputs, remoteCheckpointScope, remoteWatchInputs } from "./remote-io.mjs";
10
12
 
11
13
  const within = (file, root) => file === root || file.startsWith(root + path.sep);
12
14
  function excluded(file, excludes) { return file.split(path.sep).includes(".git") || excludes.some(x => within(file, x)); }
13
15
 
14
16
  export async function captureInputs(roots, excludes = [], { allowMissing = false } = {}) {
17
+ if (targetHost() !== "local") return remoteCaptureInputs(roots, excludes, { allowMissing });
15
18
  const records = new Map();
16
19
  async function visit(file) {
17
20
  if (excluded(file, excludes) || records.has(file)) return;
@@ -38,18 +41,20 @@ function differences(before, after) {
38
41
  }
39
42
 
40
43
  export async function getCheckpoint(threadInput, id, storage = {}) {
41
- const threadId = normalizeThreadId(threadInput), entry = await readRecord("checkpoints", id, storage);
44
+ const threadId = normalizeTaskId(threadInput), entry = await readRecord("checkpoints", id, storage);
42
45
  if (!entry || entry.thread_id !== threadId || entry.id !== id) throw new Error("Checkpoint not found for this task.");
43
46
  return entry;
44
47
  }
45
48
 
46
49
  export async function createCheckpoint(threadInput, name, { paths, excludes = [], cwd = process.cwd(), ...storage }) {
47
- const threadId = normalizeThreadId(threadInput);
50
+ const threadId = normalizeTaskId(threadInput);
48
51
  if (!name?.trim() || !paths?.length) throw new Error("capture requires a name and at least one --path.");
49
- const roots = [...new Set(paths.map(p => path.resolve(cwd, p)))], omit = excludes.map(p => path.resolve(cwd, p));
52
+ const scope = targetHost() === "local" ? { cwd: path.resolve(cwd), roots: [...new Set(paths.map(p => path.resolve(cwd, p)))], excludes: excludes.map(p => path.resolve(cwd, p)) }
53
+ : await remoteCheckpointScope(threadId, paths, excludes);
54
+ const { roots, excludes: omit } = scope;
50
55
  const snapshot = await captureInputs(roots, omit);
51
56
  if (!snapshot.files.some(f => f.type === "file")) throw new Error("No input files were selected.");
52
- const entry = { schema: 1, id: randomUUID(), thread_id: threadId, name, cwd: path.resolve(cwd), created_at: new Date().toISOString(), snapshot, runs: [], artifacts: [] };
57
+ const entry = { schema: 1, id: randomUUID(), thread_id: threadId, name, cwd: scope.cwd, created_at: new Date().toISOString(), snapshot, runs: [], artifacts: [] };
53
58
  await writeRecord("checkpoints", entry.id, entry, storage);
54
59
  return checkpointSummary(entry);
55
60
  }
@@ -72,39 +77,70 @@ export async function verifyCheckpoint(threadInput, id, storage = {}) {
72
77
  }
73
78
 
74
79
  function watchInputs(snapshot) {
75
- const touched = new Set(), watchers = []; let error = null;
80
+ const touched = new Set(), watchers = []; let error = null, pending = Promise.resolve(), dirty = false, scanning = false;
81
+ // macOS can deliver queued events for files created before watch startup,
82
+ // including parent notifications for excluded outputs. Confirm an event
83
+ // against the input snapshot rather than treating its path alone as a write.
84
+ const scan = () => {
85
+ dirty = true;
86
+ if (scanning) return;
87
+ scanning = true;
88
+ pending = pending.then(async () => {
89
+ while (dirty) {
90
+ dirty = false;
91
+ try {
92
+ const current = await captureInputs(snapshot.roots, snapshot.excludes, { allowMissing: true }), changes = differences(snapshot, current);
93
+ for (const file of [...changes.added, ...changes.removed, ...changes.changed]) if (touched.size < 100) touched.add(file);
94
+ } catch (e) { error = e.message; }
95
+ }
96
+ }).finally(() => { scanning = false; });
97
+ };
76
98
  const files = new Map(snapshot.files.map(f => [f.ref, f]));
77
99
  const dirs = [...new Set(snapshot.roots.map(r => files.get(r)?.type === "directory" ? r : path.dirname(r)))];
78
100
  try {
79
101
  for (const dir of dirs) {
80
- const watcher = watch(dir, { recursive: true }, (_, filename) => {
102
+ const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
81
103
  const file = filename ? path.resolve(dir, filename.toString()) : dir;
82
- if (!excluded(file, snapshot.excludes) && (!filename || snapshot.roots.some(r => within(file, r)))) { if (touched.size < 100) touched.add(file); }
104
+ if (!excluded(file, snapshot.excludes) && (!filename || snapshot.roots.some(r => within(file, r)))) {
105
+ // A content-change event on an existing input remains evidence even
106
+ // if it was restored within the filesystem timestamp resolution.
107
+ if (eventType === "change" && files.get(file)?.type === "file" && touched.size < 100) touched.add(file);
108
+ scan();
109
+ }
83
110
  });
84
111
  watcher.on("error", e => { error = e.message; }); watchers.push(watcher);
85
112
  }
86
113
  } catch (e) { for (const w of watchers) w.close(); throw e; }
87
- return { touched, get error() { return error; }, close() { for (const w of watchers) w.close(); } };
114
+ return { touched, get error() { return error; }, settle: () => pending, async close() { for (const w of watchers) w.close(); await pending; } };
88
115
  }
89
116
 
90
- export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, ...storage } = {}) {
117
+ export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, execBackend, remoteShell, supervisor, connection, ...storage } = {}) {
91
118
  if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw new Error("A command and timeout-ms between 0 and 86400000 are required.");
92
- const threadId = normalizeThreadId(threadInput);
119
+ const threadId = normalizeTaskId(threadInput);
93
120
  return withStoreLock(`checkpoint-${id}`, async () => {
94
- const entry = await getCheckpoint(threadId, id, storage), watcher = watchInputs(entry.snapshot);
121
+ const entry = await getCheckpoint(threadId, id, storage), watcher = targetHost() === "local" ? watchInputs(entry.snapshot) : await remoteWatchInputs(entry.snapshot);
95
122
  try {
96
123
  const before = await captureInputs(entry.snapshot.roots, entry.snapshot.excludes, { allowMissing: true });
97
124
  if (before.digest !== entry.snapshot.digest) throw new Error("Inputs changed since capture. Capture a new checkpoint before running.");
98
125
  const run = { id: randomUUID(), argv, status: "running", started_at: new Date().toISOString() };
99
126
  entry.runs.push(run); await writeRecord("checkpoints", id, entry, storage);
100
- const result = await runCommand(argv, { cwd: entry.cwd, timeoutMs, onStart: async pid => { run.pid = pid; await writeRecord("checkpoints", id, entry, storage); } });
127
+ let result;
128
+ try { result = await runCommand(argv, { cwd: entry.cwd, timeoutMs, execBackend, remoteShell, supervisor, connection, threadId, onStart: async (pid, info) => { Object.assign(run, { pid }, info); await writeRecord("checkpoints", id, entry, storage); } }); }
129
+ catch (error) {
130
+ Object.assign(run, { status: error.uncertain ? "unknown" : "failed", error_code: error.code, error: error.message, inputs_unchanged: false,
131
+ remote_process_id: error.remote_process_id, touched_paths: [...watcher.touched], observation_error: watcher.error, completed_at: new Date().toISOString() });
132
+ await writeRecord("checkpoints", id, entry, storage); throw error;
133
+ }
101
134
  let after, snapshotError;
102
135
  try { after = await captureInputs(entry.snapshot.roots, entry.snapshot.excludes, { allowMissing: true }); } catch (e) { snapshotError = e.message; }
136
+ await watcher.settle?.();
137
+ const changedInputs = after ? differences(entry.snapshot, after) : null;
138
+ const touchedPaths = [...new Set([...watcher.touched, ...(changedInputs ? [...changedInputs.added, ...changedInputs.removed, ...changedInputs.changed] : [])])].slice(0, 100);
103
139
  Object.assign(run, result, { status: "completed", inputs_unchanged: after?.digest === entry.snapshot.digest && !watcher.touched.size && !watcher.error,
104
- touched_paths: [...watcher.touched], observation_error: watcher.error ?? snapshotError ?? null, differences: after ? differences(entry.snapshot, after) : null });
140
+ touched_paths: touchedPaths, observed_touched_paths: [...watcher.touched], observation_error: watcher.error ?? snapshotError ?? null, differences: changedInputs });
105
141
  await writeRecord("checkpoints", id, entry, storage);
106
142
  return { checkpoint_id: id, ...run, valid: run.exit_code === 0 && !run.error && !run.timed_out && run.inputs_unchanged };
107
- } finally { watcher.close(); }
143
+ } finally { await watcher.close(); }
108
144
  }, storage);
109
145
  }
110
146
 
@@ -123,6 +159,6 @@ export async function attachArtifacts(threadInput, id, refs, storage = {}) {
123
159
  }
124
160
 
125
161
  export async function listCheckpoints(threadInput, storage = {}) {
126
- const threadId = normalizeThreadId(threadInput);
162
+ const threadId = normalizeTaskId(threadInput);
127
163
  return (await listRecords("checkpoints", storage)).filter(c => c?.thread_id === threadId).sort((a, b) => a.created_at.localeCompare(b.created_at)).map(entry => checkpointSummary(entry));
128
164
  }
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,28 +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";
21
+ import { repairTurns } from "./turn-repair.mjs";
22
+ import { listProjects, registeredRemoteProjects } from "./projects.mjs";
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";
18
28
 
19
29
  const DEFAULT_SEND_BACKEND = "app-server";
20
30
 
21
31
  function success(command, data, json) {
32
+ if (data && !Array.isArray(data)) Object.assign(data, hostFields());
22
33
  if (json) console.log(JSON.stringify({ ok: true, command, data }));
23
34
  return data;
24
35
  }
25
36
 
26
- function fail(error, json) {
37
+ export function fail(error, json) {
27
38
  const message = error instanceof Error ? error.message : String(error);
28
39
  if (json) console.log(JSON.stringify({ ok: false, ...(error.watch ? { command: "watch", data: error.watch } : {}), error: {
29
- message, code: error.code, delivery_status: error.delivery_status,
40
+ message, code: error.code, ...hostFields(), delivery_status: error.delivery_status,
30
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
+ } : {}),
31
48
  sent: error.sent, thread_id: error.thread_id, rpc_code: error.rpc_code ?? error.rpcCode,
32
49
  message_id: error.message_id, client_message_id: error.client_message_id, journal_update_required: error.journal_update_required,
33
50
  existing_message_id: error.existing_message_id,
51
+ existing_attempt_id: error.existing_attempt_id, existing_receipt_path: error.existing_receipt_path,
34
52
  supervision_update_required: error.supervision_update_required,
53
+ history_changed: error.history_changed, backup_path: error.backup_path, receipt_path: error.receipt_path,
54
+ repair_stat: error.repair_stat,
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,
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,
35
62
  } }));
36
- 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
+ }
37
76
  process.exitCode = 1;
38
77
  }
39
78
 
@@ -59,6 +98,23 @@ function takeOptions(args, name) {
59
98
  return values;
60
99
  }
61
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
+
62
118
  function readMessage(parts) {
63
119
  if (parts[0] === "--") parts.shift();
64
120
  if (parts.length === 1 && parts[0] === "-") return readFileSync(0, "utf8");
@@ -101,6 +157,35 @@ function printSupervisor(data) {
101
157
  }
102
158
 
103
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) {
104
189
  const args = [...argv];
105
190
  let json = takeFlag(args, "--json");
106
191
 
@@ -122,7 +207,7 @@ export async function main(argv) {
122
207
  if (args.length === 0 || takeFlag(args, "--help") || args[0] === "help") {
123
208
  if (args[0] === "help") args.shift();
124
209
  const candidate = args[0] ?? "overview";
125
- const topic = candidate === "supervise" && args[1] === "prompt" ? "supervise prompt"
210
+ const topic = ["supervise", "orchestrate"].includes(candidate) && args[1] === "prompt" ? `${candidate} prompt`
126
211
  : /^(codex:\/\/|[0-9a-f]{8}-)/i.test(candidate) ? "send" : candidate;
127
212
  const data = helpData(topic);
128
213
  success("help", data, json);
@@ -131,9 +216,32 @@ export async function main(argv) {
131
216
  }
132
217
 
133
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();
134
226
  // An explicit observation endpoint can never silently route a mutation to
135
227
  // the shared wrapper or UI. Local records/help remain available.
136
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
+ }
137
245
  if (command === "supervise" && ["register", "status", "list", "pause", "resume", "stop"].includes(args[0])) {
138
246
  const action = args.shift(), owner = action === "register" ? takeOption(args, "--owner", "external") : undefined;
139
247
  if (args.length !== (action === "list" ? 0 : 1)) throw new Error(`Expected: supervise ${action}${action === "list" ? "" : " <THREAD>"}.`);
@@ -179,12 +287,35 @@ export async function main(argv) {
179
287
  if (!json) console.log(JSON.stringify(data, null, 2));
180
288
  return;
181
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
+ }
182
312
  if (command === "supervise" && args[0] === "prompt") {
183
313
  args.shift();
184
314
  const agent = takeOption(args, "--agent", "claude");
315
+ const usePathCli = takeFlag(args, "--use-path-cli");
185
316
  if (args.length < 1 || args.length > 2 || args.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise prompt <THREAD> [MESSAGE]. See codexteer help supervise prompt.");
186
317
  if (supervisor) throw new Error("A supervisor cannot generate a replacement session.");
187
- const data = await prepareSupervisorPrompt(args[0], args[1], { agent, connection });
318
+ const data = await prepareSupervisorPrompt(args[0], args[1], { agent, connection, usePathCli });
188
319
  success("supervise.prompt", data, json);
189
320
  if (!json) console.log(data.prompt);
190
321
  return;
@@ -196,18 +327,20 @@ export async function main(argv) {
196
327
  const ownArgs = separator < 0 ? args : args.slice(0, separator);
197
328
  const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
198
329
  const agent = takeOption(ownArgs, "--agent", "claude");
199
- if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise <THREAD> [MESSAGE] [--agent claude|codex] [-- <AGENT-ARGS...>].");
200
- process.exitCode = await superviseAgent(ownArgs[0], { agent, agentArgs, policy: ownArgs[1], connection });
330
+ const usePathCli = takeFlag(ownArgs, "--use-path-cli");
331
+ if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise <THREAD> [MESSAGE] [--agent claude|codex] [--use-path-cli] [-- <AGENT-ARGS...>].");
332
+ process.exitCode = await superviseAgent(ownArgs[0], { agent, agentArgs, policy: ownArgs[1], connection, usePathCli });
201
333
  return;
202
334
  }
203
335
  if (command === "resource") {
204
336
  const action = args.shift(); let data;
205
337
  if (["acquire", "run"].includes(action)) {
206
- 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) };
207
339
  if (action === "run") {
208
- 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" });
209
342
  if (args[1] !== "--" || args.length < 3) throw new Error("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
210
- 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 });
211
344
  if (!includeOutput) delete data.output_tail;
212
345
  if (!data.valid) process.exitCode = 1;
213
346
  } else {
@@ -234,9 +367,10 @@ export async function main(argv) {
234
367
  if (args.length !== 2) throw new Error("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
235
368
  data = await createCheckpoint(args[0], args[1], { paths, excludes });
236
369
  } else if (action === "run") {
237
- 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 });
238
372
  if (args[2] !== "--" || args.length < 4) throw new Error("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
239
- 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 });
240
374
  if (!includeOutput) delete data.output_tail;
241
375
  if (!data.valid) process.exitCode = 1;
242
376
  } else if (action === "attach") {
@@ -331,8 +465,9 @@ export async function main(argv) {
331
465
  if (record?.session_id === supervisor) await assertSupervisor(threadId, supervisor, { connection });
332
466
  }
333
467
  if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
468
+ if (targetHost() !== "local" && backend === "ui") throw remoteReadOnly();
334
469
  const report = backend === "ui" ? desktopDoctor() : await appServerDoctor({ threadId, ...connectionDependencies });
335
- 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() });
336
471
  report.default_send_backend = DEFAULT_SEND_BACKEND;
337
472
  report.rollout_status = "enabled";
338
473
  success("doctor", report, json);
@@ -359,7 +494,25 @@ export async function main(argv) {
359
494
  }
360
495
 
361
496
  if (command === "desktop") {
362
- 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");
363
516
  const dryRun = takeFlag(args, "--dry-run");
364
517
  if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
365
518
  const data = await startDesktop({ dryRun });
@@ -368,22 +521,79 @@ export async function main(argv) {
368
521
  return;
369
522
  }
370
523
 
524
+ if (command === "projects") {
525
+ if (connection === "desktop") throw desktopReadOnly();
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())];
533
+ success("projects.list", { projects, count: projects.length }, json);
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")}`);
535
+ return;
536
+ }
537
+
371
538
  if (command === "threads") {
372
539
  if (args.shift() !== "list") throw new Error("Expected: codexteer threads list");
373
540
  const limit = Number.parseInt(takeOption(args, "--limit", "20"), 10);
374
541
  const desktopOnly = takeFlag(args, "--desktop-only");
542
+ const cursor = takeOption(args, "--cursor", undefined), projectId = takeOption(args, "--project", undefined), search = takeOption(args, "--search", undefined);
375
543
  if (args.length > 0) throw new Error(`Unexpected argument: ${args[0]}`);
376
544
  if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
377
545
  throw new Error("--limit must be an integer between 1 and 500.");
378
546
  }
379
- const threads = await listLocalThreads({ limit, desktopOnly });
380
- 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);
381
551
  if (!json) printThreads(threads);
382
552
  return;
383
553
  }
384
554
 
385
555
  if (command === "thread") {
386
- if (args.shift() !== "resolve") throw new Error("Expected: codexteer thread resolve <THREAD>");
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
+ }
567
+ if (action === "create") {
568
+ if (connection === "desktop") throw desktopReadOnly();
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" });
570
+ const selector = takeOption(args, "--project", undefined);
571
+ const options = { title: takeOption(args, "--title", undefined), cwd: takeOption(args, "--cwd", undefined), model: takeOption(args, "--model", undefined),
572
+ permissions: takeOption(args, "--permissions", undefined), sandbox: takeOption(args, "--sandbox", undefined),
573
+ approvalPolicy: takeOption(args, "--approval-policy", undefined), approvalsReviewer: takeOption(args, "--approvals-reviewer", undefined), dryRun: takeFlag(args, "--dry-run"),
574
+ contextThread: targetContext(), thinking: takeOption(args, "--thinking", undefined), timeoutMs: takeOption(args, "--timeout-ms", undefined) };
575
+ if (options.timeoutMs !== undefined) {
576
+ options.timeoutMs = Number(options.timeoutMs);
577
+ }
578
+ const literal = args.indexOf("--");
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 --.");
580
+ const message = readMessage(args);
581
+ const data = await (targetHost() === "local" ? createProjectThread : createRemoteProjectThread)(selector, message, options);
582
+ success("thread.create", data, json);
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})` : ""}.`);
584
+ return;
585
+ }
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.");
387
597
  if (args.length !== 1) throw new Error("thread resolve requires exactly one thread ID or URL.");
388
598
  const threadId = normalizeThreadId(args[0]);
389
599
  const data = { thread_id: threadId, deep_link: threadDeepLink(threadId) };
@@ -395,7 +605,7 @@ export async function main(argv) {
395
605
  if (command === "open") {
396
606
  if (args.length !== 1) throw new Error("open requires exactly one thread ID or URL.");
397
607
  const threadId = normalizeThreadId(args[0]);
398
- const data = openDesktopThread(threadId);
608
+ const data = targetHost() === "local" ? openDesktopThread(threadId) : await ensureRemoteDesktopSubscription(threadId);
399
609
  success("open", data, json);
400
610
  if (!json) console.log(`Opened ${data.deep_link}`);
401
611
  return;
@@ -418,6 +628,10 @@ export async function main(argv) {
418
628
 
419
629
  const dryRun = takeFlag(args, "--dry-run");
420
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
+ }
421
635
  if (supervisor && backend !== "app-server") throw new Error("Managed supervision requires the app-server backend.");
422
636
  const explicitSound = takeFlag(args, "--sound");
423
637
  const noSound = takeFlag(args, "--no-sound");