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/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) {
@@ -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 Object.assign(new Error("Desktop Cloud cannot use another connection."), { code: "CLOUD_TARGET_CONFLICT" });
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 Object.assign(new Error("Use a durable_ ID or hostId=durable link for Desktop Cloud."), { code: "CLOUD_TARGET_CONFLICT" });
19
+ if (selected === "desktop-cloud") throw connectionMismatch(isCloudTask(value) ? "cloud" : "shared", selected);
19
20
  const cloud = isCloudTask(value);
20
- if (cloud && selected === "desktop") throw Object.assign(new Error("A cloud task cannot use a Desktop observation endpoint."), { code: "CLOUD_TARGET_CONFLICT" });
21
- if (!cloud && selected === "cloud") throw Object.assign(new Error("The cloud connection requires a cloud task ID or link."), { code: "CLOUD_TARGET_CONFLICT" });
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
  }