codexteer 0.18.2 → 0.19.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 +11 -0
- package/README.md +2 -0
- package/docs/distribution.md +2 -0
- package/package.json +1 -1
- package/src/app-server.mjs +4 -4
- package/src/checkpoint.mjs +10 -10
- package/src/cli.mjs +76 -68
- package/src/cloud-cli.mjs +22 -8
- package/src/connection-error.mjs +7 -0
- package/src/desktop-cloud-id.mjs +9 -1
- package/src/desktop-cloud-project.mjs +37 -4
- package/src/desktop-cloud.mjs +10 -2
- package/src/desktop.mjs +9 -9
- package/src/directive.mjs +4 -4
- package/src/evidence.mjs +3 -3
- package/src/findings.mjs +6 -3
- package/src/help-cloud-remote.mjs +399 -0
- package/src/help-errors.mjs +10 -0
- package/src/help-intervention.mjs +492 -0
- package/src/help-local.mjs +508 -0
- package/src/help-observation.mjs +200 -0
- package/src/help-supervision.mjs +253 -0
- package/src/help.mjs +58 -304
- package/src/journal.mjs +22 -7
- package/src/launcher.mjs +6 -6
- package/src/monitor.mjs +2 -2
- package/src/notify.mjs +3 -3
- package/src/observe.mjs +4 -4
- package/src/paged-read.mjs +2 -2
- package/src/resource.mjs +6 -6
- package/src/runner.mjs +5 -5
- package/src/store.mjs +2 -2
- package/src/supervise.mjs +1 -1
- package/src/task-id.mjs +5 -4
- package/src/thread-id.mjs +5 -5
- package/src/wrapper.mjs +2 -2
package/src/launcher.mjs
CHANGED
|
@@ -121,19 +121,19 @@ export function desktopLaunchArgs(home, wrapper) {
|
|
|
121
121
|
export async function startDesktop({ dryRun = false, run = spawnSync, prepare = prepareDeployment, discover = discoverRuntime, connect = RpcClient.connect } = {}) {
|
|
122
122
|
const home = codexHome();
|
|
123
123
|
if (dryRun) return { backend: "app-server", dry_run: true, started: false, command: "desktop.start" };
|
|
124
|
-
if (process.platform !== "darwin") throw new Error("Desktop startup requires macOS.");
|
|
125
|
-
if (process.env.CODEX_APP_SERVER_WS_URL?.trim()) throw new Error("An existing CODEX_APP_SERVER_WS_URL override conflicts with shared Desktop startup. Remove it from this launch environment first.");
|
|
124
|
+
if (process.platform !== "darwin") throw Object.assign(new Error("Desktop startup requires macOS."), { code: "DESKTOP_START_UNSUPPORTED" });
|
|
125
|
+
if (process.env.CODEX_APP_SERVER_WS_URL?.trim()) throw Object.assign(new Error("An existing CODEX_APP_SERVER_WS_URL override conflicts with shared Desktop startup. Remove it from this launch environment first."), { code: "DESKTOP_START_UNSUPPORTED" });
|
|
126
126
|
await access(BUNDLED_CLI, constants.X_OK);
|
|
127
127
|
await access(BUNDLED_NODE, constants.X_OK);
|
|
128
128
|
// No Accessibility APIs, focus changes, process termination, or -n (new instance).
|
|
129
129
|
const processes = run("/bin/ps", ["-axo", "comm="], { encoding: "utf8", timeout: 3000 });
|
|
130
|
-
if (processes.status !== 0) throw new Error("Cannot check whether Desktop is running. Run desktop start from your terminal.");
|
|
130
|
+
if (processes.status !== 0) throw Object.assign(new Error("Cannot check whether Desktop is running. Run desktop start from your terminal."), { code: "DESKTOP_START_FAILED" });
|
|
131
131
|
if (processes.stdout.split("\n").some(line => line.trim() === `${APP_PATH}/Contents/MacOS/ChatGPT`)) {
|
|
132
|
-
throw new Error("Desktop is already running. Finish current tasks and quit Desktop, then run: codexteer desktop start");
|
|
132
|
+
throw Object.assign(new Error("Desktop is already running. Finish current tasks and quit Desktop, then run: codexteer desktop start"), { code: "DESKTOP_ALREADY_RUNNING" });
|
|
133
133
|
}
|
|
134
134
|
const deployment = await prepare(home);
|
|
135
135
|
const launched = run("/usr/bin/open", desktopLaunchArgs(home, deployment.wrapper_path), { encoding: "utf8", timeout: 5000 });
|
|
136
|
-
if (launched.status !== 0) throw new Error("Desktop launch failed. Normal startup from the Dock is still available.");
|
|
136
|
+
if (launched.status !== 0) throw Object.assign(new Error("Desktop launch failed. Normal startup from the Dock is still available."), { code: "DESKTOP_START_FAILED" });
|
|
137
137
|
let lastError;
|
|
138
138
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
139
139
|
try {
|
|
@@ -149,5 +149,5 @@ export async function startDesktop({ dryRun = false, run = spawnSync, prepare =
|
|
|
149
149
|
}
|
|
150
150
|
await delay(200);
|
|
151
151
|
}
|
|
152
|
-
throw new Error(`Desktop started, but its shared App Server is not ready. ${lastError?.message ?? ""}`);
|
|
152
|
+
throw Object.assign(new Error(`Desktop started, but its shared App Server is not ready. ${lastError?.message ?? ""}`), { code: "RUNTIME_NOT_READY" });
|
|
153
153
|
}
|
package/src/monitor.mjs
CHANGED
|
@@ -24,8 +24,8 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
24
24
|
if (discover === discoverRuntime) discover = cloud.discover;
|
|
25
25
|
if (connect === RpcClient.connect) connect = cloud.connect;
|
|
26
26
|
}
|
|
27
|
-
if (!Number.isInteger(pollMs) || pollMs < 250 || pollMs > 10000) throw new Error("--poll-ms must be between 250 and 10000.");
|
|
28
|
-
if (!Number.isInteger(reconnectTimeoutMs) || reconnectTimeoutMs < 1 || reconnectTimeoutMs > 60000) throw new Error("Invalid reconnect timeout.");
|
|
27
|
+
if (!Number.isInteger(pollMs) || pollMs < 250 || pollMs > 10000) throw Object.assign(new Error("--poll-ms must be between 250 and 10000."), { code: "OPTIONS_INVALID" });
|
|
28
|
+
if (!Number.isInteger(reconnectTimeoutMs) || reconnectTimeoutMs < 1 || reconnectTimeoutMs > 60000) throw Object.assign(new Error("Invalid reconnect timeout."), { code: "OPTIONS_INVALID" });
|
|
29
29
|
const notification = notificationOptions(options);
|
|
30
30
|
const buffer = notification.notify === "digest" ? new NotificationBuffer(notification) : null;
|
|
31
31
|
if (signal?.aborted) return;
|
package/src/notify.mjs
CHANGED
|
@@ -5,9 +5,9 @@ const key = event => JSON.stringify([event.turn_id, event.id]);
|
|
|
5
5
|
const commandState = ({ change, output, ...event }) => event;
|
|
6
6
|
|
|
7
7
|
export function notificationOptions({ notify = "digest", settleMs = 20000, maxHoldMs = 600000 } = {}) {
|
|
8
|
-
if (!["all", "digest"].includes(notify)) throw new Error("--notify must be all or digest.");
|
|
9
|
-
if (!Number.isInteger(settleMs) || settleMs < 1000 || settleMs > 120000) throw new Error("--settle-ms must be between 1000 and 120000.");
|
|
10
|
-
if (!Number.isInteger(maxHoldMs) || maxHoldMs < 10000 || maxHoldMs > 1800000) throw new Error("--max-hold-ms must be between 10000 and 1800000.");
|
|
8
|
+
if (!["all", "digest"].includes(notify)) throw Object.assign(new Error("--notify must be all or digest."), { code: "OPTIONS_INVALID" });
|
|
9
|
+
if (!Number.isInteger(settleMs) || settleMs < 1000 || settleMs > 120000) throw Object.assign(new Error("--settle-ms must be between 1000 and 120000."), { code: "OPTIONS_INVALID" });
|
|
10
|
+
if (!Number.isInteger(maxHoldMs) || maxHoldMs < 10000 || maxHoldMs > 1800000) throw Object.assign(new Error("--max-hold-ms must be between 10000 and 1800000."), { code: "OPTIONS_INVALID" });
|
|
11
11
|
return { notify, settleMs, maxHoldMs };
|
|
12
12
|
}
|
|
13
13
|
|
package/src/observe.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export function trimEvent(event, maxChars, includeOutput) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export function readSnapshot(thread, { since, limit = 50, maxChars = 2000, includeOutput = false } = {}) {
|
|
98
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 1000 || !Number.isInteger(maxChars) || maxChars < 100 || maxChars > 20000) throw new Error("Invalid read limit or max-chars.");
|
|
98
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1000 || !Number.isInteger(maxChars) || maxChars < 100 || maxChars > 20000) throw Object.assign(new Error("Invalid read limit or max-chars."), { code: "OPTIONS_INVALID" });
|
|
99
99
|
const s = threadSnapshot(thread), old = since ? decodeCursor(since, thread.id) : null;
|
|
100
100
|
if (old && old.v !== 1) throw invalidCursor();
|
|
101
101
|
if (old && (old.count > s.events.length || prefixHash(s.hashes, old.count, old.open) !== old.prefix)) throw invalidCursor();
|
|
@@ -137,7 +137,7 @@ export async function fetchThread(client, threadId, metadata) {
|
|
|
137
137
|
|
|
138
138
|
async function readAttempt(client, threadId, options) {
|
|
139
139
|
const old = options.since ? decodeCursor(options.since, threadId) : null;
|
|
140
|
-
if (options.fullHistory && old?.v === 2) throw new Error("A v2 cursor uses paged history. Omit --full-history, or read --full-history without --since to start a full check.");
|
|
140
|
+
if (options.fullHistory && old?.v === 2) throw Object.assign(new Error("A v2 cursor uses paged history. Omit --full-history, or read --full-history without --since to start a full check."), { code: "OPTIONS_INVALID" });
|
|
141
141
|
const { thread } = await client.request("thread/read", { threadId, includeTurns: false });
|
|
142
142
|
if (thread?.id !== threadId || !Array.isArray(thread.turns)) throw protocolFailure("App Server returned invalid task metadata.");
|
|
143
143
|
if (thread.historyMode === "paginated" && !options.fullHistory) {
|
|
@@ -169,6 +169,8 @@ export async function observeThread(threadInput, options = {}, { discover = disc
|
|
|
169
169
|
}
|
|
170
170
|
if (options.supervisor) await assertSupervisor(threadId, options.supervisor, { connection: options.connection });
|
|
171
171
|
if (options.since) decodeCursor(options.since, threadId);
|
|
172
|
+
const { until = "change", timeoutMs = 30000, pollMs = 1000 } = options;
|
|
173
|
+
if (options.watch && (!["change", "idle", "attention"].includes(until) || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 60000 || !Number.isInteger(pollMs) || pollMs < 250 || pollMs > 10000)) throw Object.assign(new Error("Invalid watch condition, timeout-ms or poll-ms."), { code: "OPTIONS_INVALID" });
|
|
172
174
|
let client;
|
|
173
175
|
try {
|
|
174
176
|
const { paths, state } = await discover();
|
|
@@ -179,8 +181,6 @@ export async function observeThread(threadInput, options = {}, { discover = disc
|
|
|
179
181
|
if (options.supervisor) await recordSupervisorObservation(threadId, options.supervisor, result);
|
|
180
182
|
return result;
|
|
181
183
|
}
|
|
182
|
-
const { until = "change", timeoutMs = 30000, pollMs = 1000 } = options;
|
|
183
|
-
if (!["change", "idle", "attention"].includes(until) || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 60000 || !Number.isInteger(pollMs) || pollMs < 250 || pollMs > 10000) throw new Error("Invalid watch condition, timeout-ms or poll-ms.");
|
|
184
184
|
const deadline = now() + timeoutMs;
|
|
185
185
|
let baseline = options.since;
|
|
186
186
|
while (true) {
|
package/src/paged-read.mjs
CHANGED
|
@@ -75,13 +75,13 @@ function finish(state, cursor, events, commands, old, pending, omitted) {
|
|
|
75
75
|
Object.assign(cursor, hostFields());
|
|
76
76
|
cursor.state = digest(state); cursor.active_turn_id = state.active_turn_id; cursor.pending = pending;
|
|
77
77
|
const encoded = Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
78
|
-
if (encoded.length > 1024 * 1024) throw new Error("Too many in-progress items for a fast cursor. Use --full-history.");
|
|
78
|
+
if (encoded.length > 1024 * 1024) throw Object.assign(new Error("Too many in-progress items for a fast cursor. Use --full-history."), { code: "CURSOR_TOO_LARGE" });
|
|
79
79
|
return { ...state, events, running_commands: [...commands.values()], cursor: encoded, changed: !old || events.length > 0 || old.state !== cursor.state,
|
|
80
80
|
has_more: pending, omitted_older_events: omitted, history_scope: "tail-and-tracked-items", running_commands_complete: !pending, observed_at: new Date().toISOString() };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export async function readPaged(client, thread, { since, limit = 50, maxChars = 2000, includeOutput = false } = {}) {
|
|
84
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 1000 || !Number.isInteger(maxChars) || maxChars < 100 || maxChars > 20000) throw new Error("Invalid read limit or max-chars.");
|
|
84
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1000 || !Number.isInteger(maxChars) || maxChars < 100 || maxChars > 20000) throw Object.assign(new Error("Invalid read limit or max-chars."), { code: "OPTIONS_INVALID" });
|
|
85
85
|
const old = since ? decodeCursor(since, thread.id) : null;
|
|
86
86
|
if (old && old.v !== 2) throw invalidCursor();
|
|
87
87
|
const turns = await turnMetadata(client, thread.id), state = stateOf(thread, turns), commands = new Map();
|
package/src/resource.mjs
CHANGED
|
@@ -6,10 +6,10 @@ import { runCommand } from "./runner.mjs";
|
|
|
6
6
|
import { targetHost } from "./target.mjs";
|
|
7
7
|
|
|
8
8
|
function resourceKey(name) {
|
|
9
|
-
if (!name?.trim() || name.length > 200 || /[\r\n\x00-\x1f]/.test(name)) throw new Error("Resource name must be a non-empty single line (up to 200 characters).");
|
|
9
|
+
if (!name?.trim() || name.length > 200 || /[\r\n\x00-\x1f]/.test(name)) throw Object.assign(new Error("Resource name must be a non-empty single line (up to 200 characters)."), { code: "OPTIONS_INVALID" });
|
|
10
10
|
return digest(name).slice(0, 40);
|
|
11
11
|
}
|
|
12
|
-
function validTtl(ttlMs) { if (!Number.isInteger(ttlMs) || ttlMs < 1000 || ttlMs > 86400000) throw new Error("--ttl-ms must be between 1000 and 86400000."); }
|
|
12
|
+
function validTtl(ttlMs) { if (!Number.isInteger(ttlMs) || ttlMs < 1000 || ttlMs > 86400000) throw Object.assign(new Error("--ttl-ms must be between 1000 and 86400000."), { code: "OPTIONS_INVALID" }); }
|
|
13
13
|
function leaseState(entry, now = Date.now()) { return !entry ? "available" : entry.released_at ? "released" : Date.parse(entry.expires_at) <= now ? "expired" : "held"; }
|
|
14
14
|
|
|
15
15
|
export async function resourceStatus(name, { now = Date.now, ...storage } = {}) {
|
|
@@ -19,7 +19,7 @@ export async function resourceStatus(name, { now = Date.now, ...storage } = {})
|
|
|
19
19
|
|
|
20
20
|
export async function acquireResource(name, { owner, ttlMs = 600000, reason = "", condition = "", threadId, now = Date.now, ...storage }) {
|
|
21
21
|
const key = resourceKey(name); validTtl(ttlMs);
|
|
22
|
-
if (!owner?.trim()) throw new Error("--owner is required (for example claude-code or codex).");
|
|
22
|
+
if (!owner?.trim()) throw Object.assign(new Error("--owner is required (for example claude-code or codex)."), { code: "OPTIONS_INVALID" });
|
|
23
23
|
if (threadId) threadId = normalizeTaskId(threadId);
|
|
24
24
|
return withStoreLock(`resource-${key}`, async () => {
|
|
25
25
|
const current = await readRecord("resources", key, storage);
|
|
@@ -34,7 +34,7 @@ export async function renewResource(name, token, { ttlMs = 600000, now = Date.no
|
|
|
34
34
|
const key = resourceKey(name); validTtl(ttlMs);
|
|
35
35
|
return withStoreLock(`resource-${key}`, async () => {
|
|
36
36
|
const entry = await readRecord("resources", key, storage);
|
|
37
|
-
if (!token || entry?.token !== token || leaseState(entry, now()) !== "held") throw new Error("Lease is no longer owned by this token. Acquire it again after checking status.");
|
|
37
|
+
if (!token || entry?.token !== token || leaseState(entry, now()) !== "held") throw Object.assign(new Error("Lease is no longer owned by this token. Acquire it again after checking status."), { code: "RESOURCE_NOT_OWNED" });
|
|
38
38
|
entry.expires_at = new Date(now() + ttlMs).toISOString();
|
|
39
39
|
await writeRecord("resources", key, entry, storage); return entry;
|
|
40
40
|
}, storage);
|
|
@@ -44,7 +44,7 @@ export async function releaseResource(name, token, { now = Date.now, ...storage
|
|
|
44
44
|
const key = resourceKey(name);
|
|
45
45
|
return withStoreLock(`resource-${key}`, async () => {
|
|
46
46
|
const entry = await readRecord("resources", key, storage);
|
|
47
|
-
if (!token || entry?.token !== token) throw new Error("Lease token does not match. Another owner's lease was not released.");
|
|
47
|
+
if (!token || entry?.token !== token) throw Object.assign(new Error("Lease token does not match. Another owner's lease was not released."), { code: "RESOURCE_NOT_OWNED" });
|
|
48
48
|
entry.released_at ??= new Date(now()).toISOString();
|
|
49
49
|
await writeRecord("resources", key, entry, storage); return entry;
|
|
50
50
|
}, storage);
|
|
@@ -55,7 +55,7 @@ export async function listResources({ now = Date.now, ...storage } = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function runWithResource(name, argv, { timeoutMs = 0, cwd = targetHost() === "local" ? process.cwd() : undefined, ttlMs = 600000, execBackend, remoteShell, supervisor, connection, ...options }, { execute = runCommand } = {}) {
|
|
58
|
-
if (!argv.length || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw new Error("A command and valid --timeout-ms are required.");
|
|
58
|
+
if (!argv.length || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw Object.assign(new Error("A command and valid --timeout-ms are required."), { code: "OPTIONS_INVALID" });
|
|
59
59
|
const lease = await acquireResource(name, { ...options, ttlMs }), controller = new AbortController();
|
|
60
60
|
let timer, renewal = Promise.resolve(), stopped = false, leaseError = null, released = false, result, uncertain = false;
|
|
61
61
|
const schedule = () => { timer = setTimeout(() => {
|
package/src/runner.mjs
CHANGED
|
@@ -3,10 +3,10 @@ import { targetHost } from "./target.mjs";
|
|
|
3
3
|
import { runRemoteCommand, runRemoteShellCommand } from "./remote-io.mjs";
|
|
4
4
|
|
|
5
5
|
export function runCommand(argv, { cwd, timeoutMs = 0, onStart = async () => {}, signal: abortSignal, execBackend = "app-server", threadId, remoteShell, supervisor, connection } = {}) {
|
|
6
|
-
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.");
|
|
7
|
-
if (!["app-server", "thread-shell"].includes(execBackend)) throw new Error("--exec-backend must be app-server or thread-shell.");
|
|
6
|
+
if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw Object.assign(new Error("A command and timeout-ms between 0 and 86400000 are required."), { code: "OPTIONS_INVALID" });
|
|
7
|
+
if (!["app-server", "thread-shell"].includes(execBackend)) throw Object.assign(new Error("--exec-backend must be app-server or thread-shell."), { code: "OPTIONS_INVALID" });
|
|
8
8
|
if (targetHost() !== "local") return (execBackend === "thread-shell" ? runRemoteShellCommand : runRemoteCommand)(argv, { cwd, timeoutMs, onStart, signal: abortSignal, threadId, shell: remoteShell, supervisor, connection });
|
|
9
|
-
if (execBackend !== "app-server") throw new Error("thread-shell is an explicit remote execution backend.");
|
|
9
|
+
if (execBackend !== "app-server") throw Object.assign(new Error("thread-shell is an explicit remote execution backend."), { code: "OPTIONS_INVALID" });
|
|
10
10
|
return new Promise((resolve, reject) => {
|
|
11
11
|
const started = new Date().toISOString();
|
|
12
12
|
let output = "", truncated = false, timedOut = false, failure, killer, timer, startUpdate = Promise.resolve(), closed = false;
|
|
@@ -16,7 +16,7 @@ export function runCommand(argv, { cwd, timeoutMs = 0, onStart = async () => {},
|
|
|
16
16
|
try { process.kill(-child.pid, signal); } catch (e) { if (e.code !== "ESRCH") failure ??= e; }
|
|
17
17
|
};
|
|
18
18
|
const stop = () => { terminate("SIGTERM"); killer ??= setTimeout(() => terminate("SIGKILL"), 1000); };
|
|
19
|
-
const interrupt = () => { failure ??= new Error("Command interrupted."); stop(); };
|
|
19
|
+
const interrupt = () => { failure ??= Object.assign(new Error("Command interrupted."), { code: "CANCELLED" }); stop(); };
|
|
20
20
|
process.on("SIGINT", interrupt); process.on("SIGTERM", interrupt);
|
|
21
21
|
abortSignal?.addEventListener("abort", interrupt, { once: true });
|
|
22
22
|
for (const stream of [child.stdout, child.stderr]) {
|
|
@@ -30,7 +30,7 @@ export function runCommand(argv, { cwd, timeoutMs = 0, onStart = async () => {},
|
|
|
30
30
|
clearTimeout(timer); clearTimeout(killer); process.off("SIGINT", interrupt); process.off("SIGTERM", interrupt);
|
|
31
31
|
abortSignal?.removeEventListener("abort", interrupt);
|
|
32
32
|
await startUpdate;
|
|
33
|
-
const result = { pid: child.pid ?? null, exit_code: code, signal, timed_out: timedOut, started_at: started, completed_at: new Date().toISOString(), output_tail: output, output_truncated: truncated, error: failure?.message ?? null };
|
|
33
|
+
const result = { pid: child.pid ?? null, exit_code: code, signal, timed_out: timedOut, started_at: started, completed_at: new Date().toISOString(), output_tail: output, output_truncated: truncated, error: failure?.message ?? null, error_code: failure ? failure.code ?? null : null };
|
|
34
34
|
resolve(result);
|
|
35
35
|
});
|
|
36
36
|
});
|
package/src/store.mjs
CHANGED
|
@@ -7,14 +7,14 @@ import { hostNamespace, hostFields } from "./target.mjs";
|
|
|
7
7
|
const valid = value => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,150}$/.test(value);
|
|
8
8
|
// The persisted namespace predates the codexteer rename; keep existing records.
|
|
9
9
|
export function storePath(bucket, id, home = codexHome()) {
|
|
10
|
-
if (!valid(bucket) || (id != null && !valid(id))) throw new Error("Invalid local record identifier.");
|
|
10
|
+
if (!valid(bucket) || (id != null && !valid(id))) throw Object.assign(new Error("Invalid local record identifier."), { code: "OPTIONS_INVALID" });
|
|
11
11
|
const namespace = hostNamespace();
|
|
12
12
|
return path.join(home, "codex-steer", ...(namespace ? ["hosts", namespace] : []), bucket, ...(id == null ? [] : [`${id}.json`]));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async function owned(file, directory = false) {
|
|
16
16
|
const s = await lstat(file);
|
|
17
|
-
if (s.uid !== process.getuid() || s.isSymbolicLink() || (s.mode & 0o077) || (directory ? !s.isDirectory() : !s.isFile())) throw new Error("Unsafe local state ownership, permissions, or file type.");
|
|
17
|
+
if (s.uid !== process.getuid() || s.isSymbolicLink() || (s.mode & 0o077) || (directory ? !s.isDirectory() : !s.isFile())) throw Object.assign(new Error("Unsafe local state ownership, permissions, or file type."), { code: "STORE_UNSAFE" });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async function directory(bucket, home, create = false) {
|
package/src/supervise.mjs
CHANGED
|
@@ -19,7 +19,7 @@ export async function prepareSupervisorPrompt(threadInput, policy, { agent = "cl
|
|
|
19
19
|
connection = taskConnection(threadId, connection);
|
|
20
20
|
validateSupervisionPolicy(policy);
|
|
21
21
|
connectionMode(connection);
|
|
22
|
-
if (!SUPERVISOR_AGENTS.includes(agent)) throw new Error("--agent must be claude or codex.");
|
|
22
|
+
if (!SUPERVISOR_AGENTS.includes(agent)) throw Object.assign(new Error("--agent must be claude or codex."), { code: "OPTIONS_INVALID" });
|
|
23
23
|
const supervisor = { session_id: randomUUID(), owner: agent, connection, ...hostFields() };
|
|
24
24
|
let context = targetContext() ?? process.env.CODEX_THREAD_ID;
|
|
25
25
|
if (targetHost() !== "local" && remoteTransport() === "native" && !context) throw Object.assign(new Error("Remote supervision requires --context-thread <LOCAL-THREAD-ID> or CODEX_THREAD_ID."), { code: "REMOTE_CONTEXT_REQUIRED" });
|
package/src/task-id.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDesktopCloudTask, desktopCloudId } from "./desktop-cloud-id.mjs";
|
|
2
2
|
import { normalizeThreadId } from "./thread-id.mjs";
|
|
3
3
|
import { isCloudTask, cloudTaskId } from "./cloud-id.mjs";
|
|
4
|
+
import { connectionMismatch } from "./connection-error.mjs";
|
|
4
5
|
|
|
5
6
|
export { isCloudTask } from "./cloud-id.mjs";
|
|
6
7
|
|
|
@@ -12,12 +13,12 @@ export function normalizeTaskId(value) {
|
|
|
12
13
|
|
|
13
14
|
export function taskConnection(value, selected = "shared") {
|
|
14
15
|
if (isDesktopCloudTask(value)) {
|
|
15
|
-
if (!["shared", "desktop-cloud"].includes(selected)) throw
|
|
16
|
+
if (!["shared", "desktop-cloud"].includes(selected)) throw connectionMismatch("desktop-cloud", selected);
|
|
16
17
|
return "desktop-cloud";
|
|
17
18
|
}
|
|
18
|
-
if (selected === "desktop-cloud") throw
|
|
19
|
+
if (selected === "desktop-cloud") throw connectionMismatch(isCloudTask(value) ? "cloud" : "shared", selected);
|
|
19
20
|
const cloud = isCloudTask(value);
|
|
20
|
-
if (cloud && selected === "desktop") throw
|
|
21
|
-
if (!cloud && selected === "cloud") throw
|
|
21
|
+
if (cloud && selected === "desktop") throw connectionMismatch("cloud", selected);
|
|
22
|
+
if (!cloud && selected === "cloud") throw connectionMismatch("shared", selected);
|
|
22
23
|
return cloud ? "cloud" : selected;
|
|
23
24
|
}
|
package/src/thread-id.mjs
CHANGED
|
@@ -3,7 +3,7 @@ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{
|
|
|
3
3
|
|
|
4
4
|
export function parseThreadTarget(input) {
|
|
5
5
|
if (typeof input !== "string" || input.trim() === "") {
|
|
6
|
-
throw new Error("Thread ID is required.");
|
|
6
|
+
throw Object.assign(new Error("Thread ID is required."), { code: "OPTIONS_INVALID" });
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
let value = input.trim(), hostId;
|
|
@@ -12,19 +12,19 @@ export function parseThreadTarget(input) {
|
|
|
12
12
|
try {
|
|
13
13
|
url = new URL(value);
|
|
14
14
|
} catch {
|
|
15
|
-
throw new Error(`Invalid Codex thread URL: ${input}`);
|
|
15
|
+
throw Object.assign(new Error(`Invalid Codex thread URL: ${input}`), { code: "OPTIONS_INVALID" });
|
|
16
16
|
}
|
|
17
17
|
if (url.hostname !== "threads") {
|
|
18
|
-
throw new Error(`Expected codex://threads/<thread-id>: ${input}`);
|
|
18
|
+
throw Object.assign(new Error(`Expected codex://threads/<thread-id>: ${input}`), { code: "OPTIONS_INVALID" });
|
|
19
19
|
}
|
|
20
20
|
const hosts = url.searchParams.getAll("hostId");
|
|
21
|
-
if (hosts.length > 1) throw new Error("A thread link must have only one hostId.");
|
|
21
|
+
if (hosts.length > 1) throw Object.assign(new Error("A thread link must have only one hostId."), { code: "OPTIONS_INVALID" });
|
|
22
22
|
if (hosts.length) hostId = validateHostId(hosts[0]);
|
|
23
23
|
value = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (!UUID_PATTERN.test(value)) {
|
|
27
|
-
throw new Error(`Invalid local Codex thread ID: ${input}`);
|
|
27
|
+
throw Object.assign(new Error(`Invalid local Codex thread ID: ${input}`), { code: "OPTIONS_INVALID" });
|
|
28
28
|
}
|
|
29
29
|
return { threadId: value.toLowerCase(), hostId };
|
|
30
30
|
}
|
package/src/wrapper.mjs
CHANGED
|
@@ -104,12 +104,12 @@ export function serverArguments(args, socketPath) {
|
|
|
104
104
|
if (arg === "--stdio") continue;
|
|
105
105
|
if (arg === "--listen" || arg.startsWith("--listen=")) {
|
|
106
106
|
const value = arg === "--listen" ? args[++i] : arg.slice(9);
|
|
107
|
-
if (value !== "stdio://") throw new Error("Desktop requested a conflicting App Server transport.");
|
|
107
|
+
if (value !== "stdio://") throw Object.assign(new Error("Desktop requested a conflicting App Server transport."), { code: "OPTIONS_INVALID" });
|
|
108
108
|
continue;
|
|
109
109
|
}
|
|
110
110
|
transformed.push(arg);
|
|
111
111
|
if (values.has(arg) || ["--code-mode-host", "--ws-auth", "--ws-token-file", "--ws-token-sha256", "--ws-shared-secret-file", "--ws-issuer", "--ws-audience", "--ws-max-clock-skew-seconds"].includes(arg)) {
|
|
112
|
-
if (args[i + 1] == null) throw new Error("Incomplete App Server startup arguments.");
|
|
112
|
+
if (args[i + 1] == null) throw Object.assign(new Error("Incomplete App Server startup arguments."), { code: "OPTIONS_INVALID" });
|
|
113
113
|
transformed.push(args[++i]);
|
|
114
114
|
}
|
|
115
115
|
}
|