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.
- package/CHANGELOG.md +37 -2
- package/README.md +154 -16
- 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 +21 -9
- 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 +90 -0
- package/package.json +13 -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 +233 -19
- 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 +23 -5
- package/src/creation-verification.mjs +36 -0
- 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 +6 -6
- package/src/evidence.mjs +3 -0
- package/src/findings.mjs +7 -7
- package/src/help.mjs +155 -26
- package/src/journal.mjs +37 -9
- package/src/launcher.mjs +16 -9
- package/src/monitor.mjs +18 -6
- package/src/native-command.mjs +100 -0
- package/src/native-rpc.mjs +116 -0
- package/src/native-worker.mjs +25 -0
- 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 +104 -0
- package/src/prompt-command.mjs +25 -0
- package/src/prompt.mjs +35 -11
- 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/runtime.mjs +4 -2
- package/src/socket-endpoint.mjs +40 -0
- package/src/standalone-create.mjs +135 -0
- package/src/store.mjs +6 -3
- package/src/supervise.mjs +32 -55
- package/src/supervision.mjs +41 -19
- package/src/target.mjs +25 -0
- package/src/task-id.mjs +23 -0
- package/src/thread-create.mjs +128 -0
- 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/src/wrapper.mjs +68 -5
package/src/checkpoint.mjs
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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 }, (
|
|
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)))) {
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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");
|