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/cli.mjs CHANGED
@@ -39,6 +39,7 @@ export function fail(error, json) {
39
39
  if (json) console.log(JSON.stringify({ ok: false, ...(error.watch ? { command: "watch", data: error.watch } : {}), error: {
40
40
  message, code: error.code, ...hostFields(), delivery_status: error.delivery_status,
41
41
  operation: error.operation, capability: error.capability,
42
+ expected_connection: error.expected_connection, connection: error.connection,
42
43
  ...((error.execution_environment === "cloud" || (typeof error.code === "string" && error.code.startsWith("CLOUD_"))) ? {
43
44
  execution_environment: error.execution_environment, task_id: error.task_id, url: error.url,
44
45
  submission_status: error.submission_status, uncertain: error.uncertain,
@@ -65,7 +66,7 @@ export function fail(error, json) {
65
66
  if (error.execution_environment === "cloud" || (typeof error.code === "string" && error.code.startsWith("DESKTOP_CLOUD_"))) {
66
67
  // Recovery metadata must survive an uncertain submission or a stale
67
68
  // 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
+ for (const key of ["creation_status", "submission_status", "delivery_status", "attempt_id", "receipt_path", "existing_message_id", "existing_attempt_id", "existing_receipt_path", "task_id", "thread_id", "url"]) {
69
70
  if (typeof error[key] === "string") console.error(`${key}: ${JSON.stringify(error[key])}`);
70
71
  }
71
72
  if (error.receipt_update_required) console.error("receipt_update_required: true");
@@ -76,11 +77,13 @@ export function fail(error, json) {
76
77
  process.exitCode = 1;
77
78
  }
78
79
 
80
+ const usage = message => Object.assign(new Error(message), { code: "OPTIONS_INVALID" });
81
+
79
82
  function takeOption(args, name, defaultValue) {
80
83
  const index = args.indexOf(name);
81
84
  if (index === -1 || (args.includes("--") && index > args.indexOf("--"))) return defaultValue;
82
85
  const value = args[index + 1];
83
- if (value == null || value.startsWith("--")) throw new Error(`${name} requires a value.`);
86
+ if (value == null || value.startsWith("--")) throw usage(`${name} requires a value.`);
84
87
  args.splice(index, 2);
85
88
  return value;
86
89
  }
@@ -123,7 +126,7 @@ function readMessage(parts) {
123
126
 
124
127
  function backendOption(args, fallback) {
125
128
  const backend = takeOption(args, "--backend", fallback);
126
- if (backend != null && !["app-server", "ui"].includes(backend)) throw new Error("--backend must be app-server or ui.");
129
+ if (backend != null && !["app-server", "ui"].includes(backend)) throw usage("--backend must be app-server or ui.");
127
130
  return backend;
128
131
  }
129
132
 
@@ -161,7 +164,7 @@ export async function main(argv) {
161
164
  try {
162
165
  const transport = validateRemoteTransport(takeOption(args, "--remote-transport", "native"));
163
166
  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.");
167
+ if (args.slice(0, args.includes("--") ? args.indexOf("--") : args.length).includes("--host")) throw usage("Specify --host only once.");
165
168
  if (explicitHost !== undefined) validateHostId(explicitHost);
166
169
  const probe = [...args];
167
170
  takeFlag(probe, "--json");
@@ -196,7 +199,7 @@ async function mainOnTarget(argv) {
196
199
  const connectionDependencies = observationConnection(connection);
197
200
  const requiredNode = takeOption(args, "--require-node-version", undefined);
198
201
  if (requiredNode !== undefined) {
199
- if (!/^v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(requiredNode)) throw new Error("--require-node-version requires a Node version such as v25.1.0.");
202
+ if (!/^v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(requiredNode)) throw usage("--require-node-version requires a Node version such as v25.1.0.");
200
203
  if (requiredNode !== process.version) throw Object.assign(new Error(`This supervision command requires Node ${requiredNode}, but is running ${process.version}. Regenerate the supervisor prompt with the intended Node runtime.`), { code: "NODE_VERSION_MISMATCH" });
201
204
  }
202
205
  if (takeFlag(args, "--version")) {
@@ -216,6 +219,7 @@ async function mainOnTarget(argv) {
216
219
  }
217
220
 
218
221
  let command = args.shift();
222
+ if (command === "diff") throw usage("diff reads a Web Cloud task's recorded patch and is not available for local or remote Desktop tasks. Use read --include-output to inspect file changes.");
219
223
  const control = isControlTransport();
220
224
  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
225
  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)
@@ -229,14 +233,14 @@ async function mainOnTarget(argv) {
229
233
  if (command === "remote") {
230
234
  if (supervisor) throw Object.assign(new Error("A bound supervisor cannot enroll, pair or manage account credentials."), { code: "SUPERVISOR_MISMATCH" });
231
235
  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.");
236
+ if (provider !== undefined && action !== "prepare") throw usage("--key-provider is only available for remote prepare.");
237
+ if (!["prepare", "recover-key", "enroll", "status", "identity", "pair", "verify"].includes(action) || args.length || (codeStdin && action !== "pair") || (action === "pair" && !codeStdin)) throw usage("Expected: remote prepare|recover-key|enroll|status|identity|verify, or --host HOST remote pair --code-stdin.");
238
+ if (["prepare", "enroll"].includes(action) && targetHost() !== "local") throw usage("remote prepare/enroll registers this local controller; omit --host.");
235
239
  // 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_...");
240
+ if (action === "pair" && !/^remote-control:env_[A-Za-z0-9_-]+$/.test(targetHost())) throw Object.assign(new Error("remote pair requires --host remote-control:env_..."), { code: "REMOTE_PAIR_INPUT_INVALID" });
237
241
  if (action === "pair" && process.stdin.isTTY) console.error("codexteer: Enter the selected host's fresh pairing code (hidden), then press Enter.");
238
242
  const code = action === "pair" ? await readPairingCode() : undefined;
239
- if (code && code.length > 128) throw new Error("Invalid pairing code input.");
243
+ if (code && code.length > 128) throw Object.assign(new Error("Invalid pairing code input."), { code: "REMOTE_PAIR_INPUT_INVALID" });
240
244
  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
245
  : action === "pair" ? pairDevice(context, targetHost(), code)
242
246
  : action === "verify" ? verifyDevice(context) : action === "identity" ? deviceIdentityInfo(context) : readDevice(context.home, context.identity).then(deviceStatus));
@@ -244,12 +248,13 @@ async function mainOnTarget(argv) {
244
248
  }
245
249
  if (command === "supervise" && ["register", "status", "list", "pause", "resume", "stop"].includes(args[0])) {
246
250
  const action = args.shift(), owner = action === "register" ? takeOption(args, "--owner", "external") : undefined;
247
- if (args.length !== (action === "list" ? 0 : 1)) throw new Error(`Expected: supervise ${action}${action === "list" ? "" : " <THREAD>"}.`);
251
+ if (args.length !== (action === "list" ? 0 : 1)) throw usage(`Expected: supervise ${action}${action === "list" ? "" : " <THREAD>"}.`);
248
252
  let data;
249
253
  if (action === "register") {
250
- if (!supervisor) throw new Error("supervise register requires the --supervisor ID from a generated prompt.");
254
+ if (!supervisor) throw usage("supervise register requires the --supervisor ID from a generated prompt.");
251
255
  data = await registerSupervisor(args[0], supervisor, { owner, connection });
252
256
  } else {
257
+ if (supervisor && action === "list") throw Object.assign(new Error("A bound supervisor cannot list other supervisor sessions. Use supervise status <THREAD> for its own task, or run supervise list from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
253
258
  if (supervisor && action !== "status") throw Object.assign(new Error("Supervisors cannot change their own control state. Run this control command from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
254
259
  if (supervisor) {
255
260
  const record = await getSupervisor(args[0]);
@@ -280,7 +285,7 @@ async function mainOnTarget(argv) {
280
285
  else if (action === "list") options = { all: takeFlag(args, "--all"), limit: Number(takeOption(args, "--limit", "50")) };
281
286
  const handlers = { create: createFinding, show: findingStatus, list: listFindings, resolve: resolveFinding, dismiss: dismissFinding, reopen: reopenFinding, evaluate: evaluateFinding, stats: findingStats };
282
287
  const single = ["create", "list", "stats"].includes(action);
283
- if (!Object.hasOwn(handlers, action) || args.length !== (single ? 1 : 2) || args.some(arg => arg.startsWith("--"))) throw new Error("See codexteer help findings.");
288
+ if (!Object.hasOwn(handlers, action) || args.length !== (single ? 1 : 2) || args.some(arg => arg.startsWith("--"))) throw usage("See codexteer help findings.");
284
289
  if (supervisor) await assertSupervisor(args[0], supervisor, { connection });
285
290
  data = single ? await handlers[action](args[0], options) : await handlers[action](args[0], args[1], options);
286
291
  success(`findings.${action}`, data, json);
@@ -291,7 +296,7 @@ async function mainOnTarget(argv) {
291
296
  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
297
  const promptOnly = args[0] === "prompt";
293
298
  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.");
299
+ else if (json) throw usage("orchestrate inherits agent output and does not support --json. Use help orchestrate --json or orchestrate prompt --project ID|NAME <GOAL> --json.");
295
300
  const separator = promptOnly ? -1 : args.indexOf("--");
296
301
  const ownArgs = separator < 0 ? args : args.slice(0, separator);
297
302
  const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
@@ -299,8 +304,8 @@ async function mainOnTarget(argv) {
299
304
  const agent = takeOption(ownArgs, "--agent", promptOnly ? undefined : "claude");
300
305
  const usePathCli = takeFlag(ownArgs, "--use-path-cli");
301
306
  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.");
307
+ else if (ownArgs.some(arg => arg.startsWith("--"))) throw usage("Unknown orchestrate option. See help orchestrate; agent arguments belong after --.");
308
+ if (ownArgs.length !== 1) throw usage("Expected: orchestrate [prompt] --project <ID-or-exact-name> <GOAL>. Quote the goal as one argument, or use - for stdin.");
304
309
  const goal = ownArgs[0] === "-" ? readFileSync(0, "utf8") : ownArgs[0];
305
310
  if (promptOnly) {
306
311
  const data = await prepareOrchestrationPrompt(project, goal, { agent, connection, usePathCli });
@@ -313,47 +318,49 @@ async function mainOnTarget(argv) {
313
318
  args.shift();
314
319
  const agent = takeOption(args, "--agent", "claude");
315
320
  const usePathCli = takeFlag(args, "--use-path-cli");
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.");
317
- if (supervisor) throw new Error("A supervisor cannot generate a replacement session.");
321
+ if (args.length < 1 || args.length > 2 || args.some(arg => arg.startsWith("--"))) throw usage("Expected: supervise prompt <THREAD> [MESSAGE]. See codexteer help supervise prompt.");
322
+ if (supervisor) throw Object.assign(new Error("A supervisor cannot generate a replacement session."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
318
323
  const data = await prepareSupervisorPrompt(args[0], args[1], { agent, connection, usePathCli });
319
324
  success("supervise.prompt", data, json);
320
325
  if (!json) console.log(data.prompt);
321
326
  return;
322
327
  }
323
328
  if (command === "supervise") {
324
- if (supervisor) throw new Error("A supervisor cannot start a replacement session.");
325
- if (json) throw new Error("supervise inherits agent output and does not support --json. Use help supervise --json or supervise prompt <THREAD> --json.");
329
+ if (supervisor) throw Object.assign(new Error("A supervisor cannot start a replacement session."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
330
+ if (json) throw usage("supervise inherits agent output and does not support --json. Use help supervise --json or supervise prompt <THREAD> --json.");
326
331
  const separator = args.indexOf("--");
327
332
  const ownArgs = separator < 0 ? args : args.slice(0, separator);
328
333
  const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
329
334
  const agent = takeOption(ownArgs, "--agent", "claude");
330
335
  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...>].");
336
+ if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(arg => arg.startsWith("--"))) throw usage("Expected: supervise <THREAD> [MESSAGE] [--agent claude|codex] [--use-path-cli] [-- <AGENT-ARGS...>].");
332
337
  process.exitCode = await superviseAgent(ownArgs[0], { agent, agentArgs, policy: ownArgs[1], connection, usePathCli });
333
338
  return;
334
339
  }
335
340
  if (command === "resource") {
336
- const action = args.shift(); let data;
341
+ const threadId = takeOption(args, "--thread", undefined), action = args.shift(); let data;
342
+ if (supervisor && threadId === undefined) throw Object.assign(new Error("A supervisor resource command requires its --thread target."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
343
+ // Like read/watch, inspecting leases and giving one up stay available while intervention is paused.
344
+ if (supervisor) await assertSupervisor(threadId, supervisor, { connection, sending: ["acquire", "renew", "run"].includes(action) });
337
345
  if (["acquire", "run"].includes(action)) {
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) };
346
+ const options = { owner: takeOption(args, "--owner", undefined), ttlMs: Number(takeOption(args, "--ttl-ms", "600000")), reason: takeOption(args, "--reason", ""), condition: takeOption(args, "--condition", ""), threadId, cwd: takeOption(args, "--cwd", undefined) };
339
347
  if (action === "run") {
340
348
  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" });
342
- if (args[1] !== "--" || args.length < 3) throw new Error("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
349
+ if (args[1] !== "--" || args.length < 3) throw usage("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
343
350
  data = await runWithResource(args[0], args.slice(2), { ...options, timeoutMs, execBackend, remoteShell, supervisor, connection });
344
351
  if (!includeOutput) delete data.output_tail;
345
352
  if (!data.valid) process.exitCode = 1;
346
353
  } else {
347
- if (args.length !== 1) throw new Error("Expected: resource acquire <NAME> --owner NAME.");
354
+ if (args.length !== 1) throw usage("Expected: resource acquire <NAME> --owner NAME.");
348
355
  data = await acquireResource(args[0], options);
349
356
  }
350
357
  } else if (["renew", "release"].includes(action)) {
351
358
  const token = takeOption(args, "--token", undefined), ttlMs = action === "renew" ? Number(takeOption(args, "--ttl-ms", "600000")) : undefined;
352
- if (args.length !== 1) throw new Error(`Expected: resource ${action} <NAME> --token TOKEN.`);
359
+ if (args.length !== 1) throw usage(`Expected: resource ${action} <NAME> --token TOKEN.`);
353
360
  data = action === "renew" ? await renewResource(args[0], token, { ttlMs }) : await releaseResource(args[0], token);
354
361
  } else if (action === "status" && args.length === 1) data = await resourceStatus(args[0]);
355
362
  else if (action === "list" && args.length === 0) data = await listResources();
356
- else throw new Error("See codexteer help resource.");
363
+ else throw usage("See codexteer help resource.");
357
364
  success(`resource.${action}`, data, json);
358
365
  if (!json) console.log(JSON.stringify(data, null, 2));
359
366
  return;
@@ -364,27 +371,27 @@ async function mainOnTarget(argv) {
364
371
  let data;
365
372
  if (action === "capture") {
366
373
  const paths = takeOptions(args, "--path"), excludes = takeOptions(args, "--exclude");
367
- if (args.length !== 2) throw new Error("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
374
+ if (args.length !== 2) throw usage("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
368
375
  data = await createCheckpoint(args[0], args[1], { paths, excludes });
369
376
  } else if (action === "run") {
370
377
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), execBackend = takeOption(args, "--exec-backend", "app-server"), remoteShell = takeOption(args, "--shell", undefined);
371
378
  if (supervisor && execBackend === "thread-shell") await assertSupervisor(args[0], supervisor, { connection, sending: true });
372
- if (args[2] !== "--" || args.length < 4) throw new Error("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
379
+ if (args[2] !== "--" || args.length < 4) throw usage("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
373
380
  data = await runCheckpoint(args[0], args[1], args.slice(3), { timeoutMs, execBackend, remoteShell, supervisor, connection });
374
381
  if (!includeOutput) delete data.output_tail;
375
382
  if (!data.valid) process.exitCode = 1;
376
383
  } else if (action === "attach") {
377
384
  const refs = takeOptions(args, "--artifact");
378
- if (args.length !== 2) throw new Error("Expected: checkpoint attach <THREAD> <ID> --artifact FILE ...");
385
+ if (args.length !== 2) throw usage("Expected: checkpoint attach <THREAD> <ID> --artifact FILE ...");
379
386
  data = await attachArtifacts(args[0], args[1], refs);
380
387
  } else if (action === "list") {
381
- if (args.length !== 1) throw new Error("Expected: checkpoint list <THREAD>.");
388
+ if (args.length !== 1) throw usage("Expected: checkpoint list <THREAD>.");
382
389
  data = await listCheckpoints(args[0]);
383
390
  } else if (["show", "verify"].includes(action)) {
384
- if (args.length !== 2) throw new Error(`Expected: checkpoint ${action} <THREAD> <ID>.`);
391
+ if (args.length !== 2) throw usage(`Expected: checkpoint ${action} <THREAD> <ID>.`);
385
392
  data = action === "verify" ? await verifyCheckpoint(args[0], args[1]) : checkpointSummary(await getCheckpoint(args[0], args[1]), includeOutput);
386
393
  if (action === "verify" && !data.valid) process.exitCode = 1;
387
- } else throw new Error("See codexteer help checkpoint.");
394
+ } else throw usage("See codexteer help checkpoint.");
388
395
  success(`checkpoint.${action}`, data, json);
389
396
  if (!json) console.log(JSON.stringify(data, null, 2));
390
397
  return;
@@ -393,18 +400,18 @@ async function mainOnTarget(argv) {
393
400
  const action = args.shift();
394
401
  if (action === "list") {
395
402
  const all = takeFlag(args, "--all");
396
- if (args.length !== 1) throw new Error("Expected: instructions list <THREAD> [--all].");
403
+ if (args.length !== 1) throw usage("Expected: instructions list <THREAD> [--all].");
397
404
  if (supervisor) await assertSupervisor(args[0], supervisor, { connection });
398
405
  const data = await listInstructions(args[0], { all });
399
406
  success("instructions.list", data, json);
400
407
  if (!json) { for (const entry of data.instructions) console.log(`${entry.id} ${entry.instruction_status} ${entry.metadata?.source ?? "unspecified"}\n${entry.body}`); if (data.pending_changes.length) console.log(`配送未確認: ${data.pending_changes.map(e => e.id).join(", ")}`); }
401
408
  return;
402
409
  }
403
- if (action !== "retract") throw new Error("See codexteer help instructions.");
410
+ if (action !== "retract") throw usage("See codexteer help instructions.");
404
411
  if (connection === "desktop") throw desktopReadOnly();
405
412
  const reason = takeOption(args, "--reason", undefined), source = takeOption(args, "--source", undefined), basedOn = takeOption(args, "--based-on", undefined);
406
413
  const dryRun = takeFlag(args, "--dry-run"), newTurn = takeFlag(args, "--new-turn");
407
- if (args.length !== 2 || !reason?.trim()) throw new Error("Expected: instructions retract <THREAD> <MESSAGE-ID> --reason TEXT.");
414
+ if (args.length !== 2 || !reason?.trim()) throw usage("Expected: instructions retract <THREAD> <MESSAGE-ID> --reason TEXT.");
408
415
  const data = await sendTrackedMessage(args[0], reason, { retracts: args[1], source, basedOn, dryRun, newTurn, supervisor, connection });
409
416
  success("instructions.retract", data, json);
410
417
  if (!json) console.log(dryRun ? "撤回の送信プレビューです。" : `撤回を受け付けました。message_id: ${data.message_id}`);
@@ -416,7 +423,7 @@ async function mainOnTarget(argv) {
416
423
  const pending = takeFlag(args, "--pending");
417
424
  const report = action === "mark" ? { status: takeOption(args, "--status", undefined), note: takeOption(args, "--note", undefined), evidence: takeOptions(args, "--evidence"), by: takeOption(args, "--by", "local") } : {};
418
425
  const [threadId, id] = args;
419
- if (!threadId || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !id) || (action === "list" && id)) throw new Error("See codexteer help history.");
426
+ if (!threadId || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !id) || (action === "list" && id)) throw usage("See codexteer help history.");
420
427
  if (supervisor) await assertSupervisor(threadId, supervisor, { connection });
421
428
  const data = action === "list" ? await listMessages(threadId, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(threadId, id), includeText) : action === "check" ? await reconcileMessages(threadId, id, { includeText }, connectionDependencies) : await markMessage(threadId, id, report);
422
429
  success(`history.${action}`, data, json);
@@ -437,14 +444,14 @@ async function mainOnTarget(argv) {
437
444
  if (stream) json = true;
438
445
  if (command === "watch") {
439
446
  const until = takeOption(args, "--until", undefined), timeout = takeOption(args, "--timeout-ms", undefined);
440
- if (stream && (until !== undefined || timeout !== undefined)) throw new Error("--stream runs until cancelled; do not combine it with --until or --timeout-ms. See codexteer help monitor.");
447
+ if (stream && (until !== undefined || timeout !== undefined)) throw usage("--stream runs until cancelled; do not combine it with --until or --timeout-ms. See codexteer help monitor.");
441
448
  const notify = takeOption(args, "--notify", stream ? "digest" : "all"), settle = takeOption(args, "--settle-ms", undefined), maxHold = takeOption(args, "--max-hold-ms", undefined);
442
- if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw new Error("--notify digest, --settle-ms and --max-hold-ms require --stream.");
443
- if (notify !== "digest" && (settle !== undefined || maxHold !== undefined)) throw new Error("--settle-ms and --max-hold-ms require --notify digest.");
449
+ if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw usage("--notify digest, --settle-ms and --max-hold-ms require --stream.");
450
+ if (notify !== "digest" && (settle !== undefined || maxHold !== undefined)) throw usage("--settle-ms and --max-hold-ms require --notify digest.");
444
451
  Object.assign(options, notificationOptions({ notify, ...(settle === undefined ? {} : { settleMs: Number(settle) }), ...(maxHold === undefined ? {} : { maxHoldMs: Number(maxHold) }) }));
445
452
  Object.assign(options, { watch: true, until: until ?? "change", timeoutMs: Number(timeout ?? "30000"), pollMs: Number(takeOption(args, "--poll-ms", "1000")) });
446
453
  }
447
- if (args.length !== 1) throw new Error(`Expected: ${command} <THREAD>. See codexteer help ${command}.`);
454
+ if (args.length !== 1) throw usage(`Expected: ${command} <THREAD>. See codexteer help ${command}.`);
448
455
  if (stream) { await monitorCommand(args[0], options, connectionDependencies); return; }
449
456
  const data = await observeThread(args[0], options, connectionDependencies);
450
457
  data.connection_mode = connection;
@@ -457,10 +464,10 @@ async function mainOnTarget(argv) {
457
464
  const backend = backendOption(args, "app-server");
458
465
  if (connection === "desktop" && backend === "ui") throw desktopReadOnly();
459
466
  const target = takeOption(args, "--thread", undefined);
460
- if (target !== undefined && backend !== "app-server") throw new Error("doctor --thread requires --backend app-server.");
467
+ if (target !== undefined && backend !== "app-server") throw usage("doctor --thread requires --backend app-server.");
461
468
  const threadId = target === undefined ? undefined : normalizeThreadId(target);
462
469
  if (supervisor && threadId) await assertSupervisorRegistration(threadId, supervisor, { connection });
463
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
470
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
464
471
  if (targetHost() !== "local" && backend === "ui") throw remoteReadOnly();
465
472
  const report = backend === "ui" ? desktopDoctor() : await appServerDoctor({ threadId, ...connectionDependencies });
466
473
  if (report.connection) Object.assign(report.connection, { mode: connection, observation_only: connection === "desktop" || (targetHost() !== "local" && !control), ...hostFields() });
@@ -492,7 +499,7 @@ async function mainOnTarget(argv) {
492
499
  if (command === "desktop") {
493
500
  const action = args.shift();
494
501
  if (action === "update-status") {
495
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
502
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
496
503
  const data = await desktopUpdateStatus();
497
504
  success("desktop.update-status", data, json);
498
505
  if (!json) console.log(JSON.stringify(data, null, 2));
@@ -502,15 +509,15 @@ async function mainOnTarget(argv) {
502
509
  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" });
503
510
  const dryRun = takeFlag(args, "--dry-run");
504
511
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "600000"));
505
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
512
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
506
513
  const data = await launchDesktopUpdate({ dryRun, timeoutMs });
507
514
  success("desktop.update", data, json);
508
515
  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}`);
509
516
  return;
510
517
  }
511
- if (action !== "start") throw new Error("Expected: codexteer desktop start|update|update-status");
518
+ if (action !== "start") throw usage("Expected: codexteer desktop start|update|update-status");
512
519
  const dryRun = takeFlag(args, "--dry-run");
513
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
520
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
514
521
  const data = await startDesktop({ dryRun });
515
522
  success("desktop.start", data, json);
516
523
  if (!json) console.log(dryRun ? "Dry run: would launch Desktop with the shared App Server wrapper." : "Desktop shared App Server is ready.");
@@ -519,9 +526,9 @@ async function mainOnTarget(argv) {
519
526
 
520
527
  if (command === "projects") {
521
528
  if (connection === "desktop") throw desktopReadOnly();
522
- if (args.shift() !== "list") throw new Error("Expected: codexteer projects list [--remote | --all] [--host HOST-ID]");
529
+ if (args.shift() !== "list") throw usage("Expected: codexteer projects list [--remote | --all] [--host HOST-ID]");
523
530
  const remote = takeFlag(args, "--remote"), all = takeFlag(args, "--all");
524
- if (args.length || (remote && all) || (all && targetHost() !== "local")) throw new Error("Use projects list [--remote | --all] or projects list --host HOST-ID.");
531
+ if (args.length || (remote && all) || (all && targetHost() !== "local")) throw usage("Use projects list [--remote | --all] or projects list --host HOST-ID.");
525
532
  const saved = targetHost() !== "local" && remoteTransport() === "paired" ? await listRemoteProjects()
526
533
  : remote || all || targetHost() !== "local" ? await registeredRemoteProjects() : [];
527
534
  const projects = [...(!remote && (all || targetHost() === "local") ? await listProjects() : []),
@@ -532,15 +539,15 @@ async function mainOnTarget(argv) {
532
539
  }
533
540
 
534
541
  if (command === "threads") {
535
- if (args.shift() !== "list") throw new Error("Expected: codexteer threads list");
542
+ if (args.shift() !== "list") throw usage("Expected: codexteer threads list");
536
543
  const limit = Number.parseInt(takeOption(args, "--limit", "20"), 10);
537
544
  const desktopOnly = takeFlag(args, "--desktop-only");
538
545
  const cursor = takeOption(args, "--cursor", undefined), projectId = takeOption(args, "--project", undefined), search = takeOption(args, "--search", undefined);
539
- if (args.length > 0) throw new Error(`Unexpected argument: ${args[0]}`);
546
+ if (args.length > 0) throw usage(`Unexpected argument: ${args[0]}`);
540
547
  if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
541
- throw new Error("--limit must be an integer between 1 and 500.");
548
+ throw usage("--limit must be an integer between 1 and 500.");
542
549
  }
543
- if (targetHost() === "local" && [cursor, projectId, search].some(value => value !== undefined)) throw new Error("--cursor/--project/--search currently require a paired remote host.");
550
+ if (targetHost() === "local" && [cursor, projectId, search].some(value => value !== undefined)) throw usage("--cursor/--project/--search currently require a paired remote host.");
544
551
  const data = targetHost() === "local" ? { threads: await listLocalThreads({ limit, desktopOnly }) } : await listRemoteThreads({ limit, desktopOnly, cursor, projectId, search });
545
552
  const threads = data.threads;
546
553
  success("threads.list", { ...data, count: threads.length }, json);
@@ -554,7 +561,7 @@ async function mainOnTarget(argv) {
554
561
  if (connection === "desktop") throw desktopReadOnly();
555
562
  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" });
556
563
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "120000"));
557
- if (args.length !== 1 || args[0].startsWith("--")) throw new Error("Expected: thread verify <ATTEMPT-ID> [--timeout-ms N].");
564
+ if (args.length !== 1 || args[0].startsWith("--")) throw usage("Expected: thread verify <ATTEMPT-ID> [--timeout-ms N].");
558
565
  const data = await (targetHost() === "local" ? verifyProjectThread : verifyRemoteProjectThread)(args[0], { timeoutMs });
559
566
  success("thread.verify", data, json);
560
567
  if (!json) console.log(`Verified existing ${data.deep_link}; creation and initial instruction were not repeated.`);
@@ -572,7 +579,7 @@ async function mainOnTarget(argv) {
572
579
  options.timeoutMs = Number(options.timeoutMs);
573
580
  }
574
581
  const literal = args.indexOf("--");
575
- 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 --.");
582
+ if (args.slice(0, literal < 0 ? args.length : literal).some(arg => arg.startsWith("--"))) throw usage("Unknown thread create option. Use -- before an instruction starting with --.");
576
583
  const message = readMessage(args);
577
584
  const data = await (targetHost() === "local" ? createProjectThread : createRemoteProjectThread)(selector, message, options);
578
585
  success("thread.create", data, json);
@@ -581,7 +588,7 @@ async function mainOnTarget(argv) {
581
588
  }
582
589
  if (action === "repair") {
583
590
  const apply = takeFlag(args, "--apply"), dryRun = takeFlag(args, "--dry-run");
584
- if (args.length !== 1 || args[0].startsWith("--") || (apply && dryRun)) throw new Error("Expected: thread repair <THREAD> [--dry-run | --apply].");
591
+ if (args.length !== 1 || args[0].startsWith("--") || (apply && dryRun)) throw usage("Expected: thread repair <THREAD> [--dry-run | --apply].");
585
592
  if (connection === "desktop") throw desktopReadOnly();
586
593
  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" });
587
594
  const data = await repairTurns(args[0], { apply });
@@ -589,8 +596,8 @@ async function mainOnTarget(argv) {
589
596
  if (!json) console.log(JSON.stringify(data, null, 2));
590
597
  return;
591
598
  }
592
- if (action !== "resolve") throw new Error("Expected: codexteer thread create|verify|resolve|repair. See help thread.");
593
- if (args.length !== 1) throw new Error("thread resolve requires exactly one thread ID or URL.");
599
+ if (action !== "resolve") throw usage("Expected: codexteer thread create|verify|resolve|repair. See help thread.");
600
+ if (args.length !== 1) throw usage("thread resolve requires exactly one thread ID or URL.");
594
601
  const threadId = normalizeThreadId(args[0]);
595
602
  const data = { thread_id: threadId, deep_link: threadDeepLink(threadId) };
596
603
  success("thread.resolve", data, json);
@@ -599,7 +606,7 @@ async function mainOnTarget(argv) {
599
606
  }
600
607
 
601
608
  if (command === "open") {
602
- if (args.length !== 1) throw new Error("open requires exactly one thread ID or URL.");
609
+ if (args.length !== 1) throw usage("open requires exactly one thread ID or URL.");
603
610
  const threadId = normalizeThreadId(args[0]);
604
611
  const data = targetHost() === "local" ? openDesktopThread(threadId) : await ensureRemoteDesktopSubscription(threadId);
605
612
  success("open", data, json);
@@ -609,7 +616,7 @@ async function mainOnTarget(argv) {
609
616
 
610
617
  if (command === "debug-ui") {
611
618
  const waitMs = Number.parseInt(takeOption(args, "--wait-ms", "1500"), 10);
612
- if (args.length !== 1) throw new Error("debug-ui requires exactly one thread ID or URL.");
619
+ if (args.length !== 1) throw usage("debug-ui requires exactly one thread ID or URL.");
613
620
  const threadId = normalizeThreadId(args[0]);
614
621
  const data = inspectDesktopUi(threadId, { waitMs });
615
622
  success("debug-ui", data, json);
@@ -628,22 +635,23 @@ async function mainOnTarget(argv) {
628
635
  if (remoteTransport() !== "paired") throw remoteReadOnly();
629
636
  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" });
630
637
  }
631
- if (supervisor && backend !== "app-server") throw new Error("Managed supervision requires the app-server backend.");
638
+ if (supervisor && backend !== "app-server") throw usage("Managed supervision requires the app-server backend.");
632
639
  const explicitSound = takeFlag(args, "--sound");
633
640
  const noSound = takeFlag(args, "--no-sound");
634
- if (explicitSound && noSound) throw new Error("--sound and --no-sound cannot be combined.");
635
- if (explicitSound && backend !== "app-server") throw new Error("--sound requires --backend app-server so receipt can be confirmed.");
641
+ if (explicitSound && noSound) throw usage("--sound and --no-sound cannot be combined.");
642
+ if (explicitSound && backend !== "app-server") throw usage("--sound requires --backend app-server so receipt can be confirmed.");
636
643
  const newTurn = takeFlag(args, "--new-turn");
637
644
  const directive = { source: takeOption(args, "--source", undefined), kind: takeOption(args, "--kind", undefined), evidence: takeOptions(args, "--evidence"), basedOn: takeOption(args, "--based-on", undefined), supersedes: takeOption(args, "--supersedes", undefined), expiresAt: takeOption(args, "--expires-at", undefined), checkpoint: takeOption(args, "--checkpoint", undefined), finding: takeOption(args, "--finding", undefined) };
638
- if (backend === "ui" && (directive.source || directive.kind || directive.evidence.length || directive.basedOn || directive.supersedes || directive.expiresAt || directive.checkpoint || directive.finding)) throw new Error("Directive metadata requires --backend app-server.");
645
+ if (backend === "ui" && (directive.source || directive.kind || directive.evidence.length || directive.basedOn || directive.supersedes || directive.expiresAt || directive.checkpoint || directive.finding)) throw usage("Directive metadata requires --backend app-server.");
639
646
  const keepFocus = takeFlag(args, "--keep-focus");
640
647
  const waitInput = takeOption(args, "--wait-ms", undefined);
641
- if (backend !== "ui" && (keepFocus || waitInput != null)) throw new Error("--keep-focus and --wait-ms require --backend ui.");
648
+ if (backend !== "ui" && (keepFocus || waitInput != null)) throw usage("--keep-focus and --wait-ms require --backend ui.");
642
649
  const waitMs = Number(waitInput ?? "1500");
650
+ if (args.slice(0, args.includes("--") ? args.indexOf("--") : args.length).some(arg => arg.startsWith("--"))) throw usage("Unknown send option. Use -- before instruction text starting with --.");
643
651
  const threadInput = args.shift();
644
- if (!threadInput) throw new Error("send requires a thread ID or codex://threads URL.");
652
+ if (!threadInput) throw usage("send requires a thread ID or codex://threads URL.");
645
653
  const message = readMessage(args);
646
- if (message.trim() === "") throw new Error("send requires a non-empty message.");
654
+ if (message.trim() === "") throw usage("send requires a non-empty message.");
647
655
  const threadId = normalizeThreadId(threadInput);
648
656
  const data = backend === "app-server"
649
657
  ? await sendTrackedMessage(threadId, message, { dryRun, newTurn, ...directive, supervisor, connection })
package/src/cloud-cli.mjs CHANGED
@@ -1,10 +1,12 @@
1
- import { desktopCloudProjectDoctor, assignDesktopCloudProject, desktopCloudProjectReceipt } from "./desktop-cloud-project.mjs";
1
+ import { desktopCloudProjectDoctor, assignDesktopCloudProject, desktopCloudProjectReceipt, resolveDesktopCloudProjectAttempt } from "./desktop-cloud-project.mjs";
2
2
  import { listProjects as listDesktopProjects } from "./projects.mjs";
3
3
  import { isDesktopCloudTask, desktopCloudId, desktopCloudLink } from "./desktop-cloud-id.mjs";
4
4
  import { desktopCloudConnection, desktopCloudEnvironments } from "./desktop-cloud-api.mjs";
5
5
  import { createDesktopCloudTask, listDesktopCloudTasks, desktopCloudReceipt } from "./desktop-cloud.mjs";
6
6
  import { isCloudTask } from "./task-id.mjs";
7
7
  import { cloudTaskId } from "./cloud-id.mjs";
8
+ import { normalizeThreadId } from "./thread-id.mjs";
9
+ import { connectionMismatch } from "./connection-error.mjs";
8
10
  import { cloudConnection } from "./cloud-connection.mjs";
9
11
  import { observeThread, printObservation } from "./observe.mjs";
10
12
  import { monitorCommand } from "./monitor.mjs";
@@ -12,7 +14,7 @@ import { notificationOptions } from "./notify.mjs";
12
14
  import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
13
15
  import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, assertSupervisorRegistration, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
14
16
  import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
15
- import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
17
+ import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, resolveUnknownDelivery, listInstructions } from "./journal.mjs";
16
18
  import { createCloudTask, cloudTaskDiff, listCloudTasks, cloudCreationReceipt } from "./cloud.mjs";
17
19
  import { cloudSendReceipt, reconcileCloudSend } from "./cloud-observe.mjs";
18
20
  import { cloudEnvironments } from "./cloud-api.mjs";
@@ -67,8 +69,18 @@ export const cloudInvocation = argv => !!selectedCloudInvocation(argv);
67
69
  export async function mainCloud(argv) {
68
70
  const args = [...argv], json = flag(args, "--json"), connection = option(args, "--connection", selectedCloudInvocation(argv) || "cloud");
69
71
  const desktopCloud = connection === "desktop-cloud";
70
- const targetId = desktopCloud ? desktopCloudId : cloudTaskId;
71
- if (!["cloud", "desktop-cloud"].includes(connection)) throw failure("A cloud task requires the cloud connection.", "CLOUD_TARGET_CONFLICT");
72
+ const targetId = desktopCloud ? desktopCloudId : value => {
73
+ try { return cloudTaskId(value); }
74
+ catch (error) {
75
+ // Preserve the Web Cloud ID validator's existing error code.
76
+ if (isDesktopCloudTask(value)) throw connectionMismatch("desktop-cloud", connection, error.code);
77
+ let local = false;
78
+ try { normalizeThreadId(value); local = true; } catch {}
79
+ if (local) throw connectionMismatch("shared", connection, error.code);
80
+ throw error;
81
+ }
82
+ };
83
+ if (!["cloud", "desktop-cloud"].includes(connection)) throw Object.assign(connectionMismatch(selectedCloudInvocation(argv) || "cloud", connection), { execution_environment: "cloud" });
72
84
  const own = args.slice(0, args.includes("--") ? args.indexOf("--") : args.length);
73
85
  if (own.some(value => ["--host", "--remote-transport", "--context-thread"].includes(value))) throw failure("Desktop host routes cannot select a cloud task.", "CLOUD_TARGET_CONFLICT");
74
86
  const requiredNode = option(args, "--require-node-version");
@@ -159,11 +171,12 @@ export async function mainCloud(argv) {
159
171
  const settings = { taskId: id };
160
172
  output(`history.${action}`, action === "show" ? await cloudSendReceipt(attempt, settings) : await reconcileCloudSend(attempt, settings)); return;
161
173
  }
162
- const includeText = flag(args, "--include-text"), pending = flag(args, "--pending");
163
- const report = action === "mark" ? { status: option(args, "--status"), note: option(args, "--note"), evidence: repeated(args, "--evidence"), by: option(args, "--by", "local") } : {};
174
+ const includeText = flag(args, "--include-text"), pending = flag(args, "--pending"), resolveUnknown = desktopCloud && action === "mark" && flag(args, "--resolve-unknown");
175
+ const report = resolveUnknown ? { note: option(args, "--note") } : action === "mark" ? { status: option(args, "--status"), note: option(args, "--note"), evidence: repeated(args, "--evidence"), by: option(args, "--by", "local") } : {};
164
176
  if (!args[0] || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !args[1]) || (action === "list" && args[1])) throw failure("Invalid history arguments.");
177
+ if (resolveUnknown && supervisor) throw failure("A supervisor cannot release its own unknown delivery. The user must run this without --supervisor.", "SUPERVISOR_CONTROL_FORBIDDEN");
165
178
  const id = await bound(args[0]); const data = action === "list" ? await listMessages(id, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(id, args[1]), includeText)
166
- : action === "check" ? await reconcileMessages(id, args[1], { includeText }, dependencies) : await markMessage(id, args[1], report);
179
+ : action === "check" ? await reconcileMessages(id, args[1], { includeText }, dependencies) : resolveUnknown ? await resolveUnknownDelivery(id, args[1], report) : await markMessage(id, args[1], report);
167
180
  output(`history.${action}`, data); return;
168
181
  }
169
182
  if (command === "instructions") {
@@ -300,7 +313,8 @@ async function desktopCloudCommands(command, args, { supervisor, output, bound,
300
313
  output("thread.create", await createDesktopCloudTask(environment, message(args), { project, title, dryRun })); return true;
301
314
  }
302
315
  if (action === "assign") { const project = option(args, "--project"); exact(args, 1); const data = await assignDesktopCloudProject(await bound(args[0]), project); output("thread.assign", data); if (data.verification_status !== "verified") process.exitCode = 1; return true; }
303
- exact(args, 1);
316
+ const resolveUnknown = action === "receipt" && flag(args, "--resolve-unknown"), note = resolveUnknown ? option(args, "--note") : undefined; exact(args, 1);
317
+ if (resolveUnknown) { output("thread.receipt", await resolveDesktopCloudProjectAttempt(args[0], { note })); return true; }
304
318
  if (action === "receipt") { let data; try { data = await desktopCloudReceipt(args[0]); } catch (error) { if (error.code !== "DESKTOP_CLOUD_CREATION_RECEIPT_NOT_FOUND") throw error; data = await desktopCloudProjectReceipt(args[0]); } output("thread.receipt", data); return true; }
305
319
  if (action === "resolve") { const id = await bound(args[0]); output("thread.resolve", { thread_id: id, deep_link: desktopCloudLink(id) }); return true; }
306
320
  throw failure("Expected thread create|assign|receipt|resolve.");
@@ -0,0 +1,7 @@
1
+ // Diagnostics only: callers retain their existing routing and rejection rules.
2
+ export function connectionMismatch(expected, selected, code = "CLOUD_TARGET_CONFLICT") {
3
+ const message = expected === "shared"
4
+ ? `For a local task, use --connection shared (the default local connection; got ${selected}).`
5
+ : `This task requires --connection ${expected} (got ${selected}).`;
6
+ return Object.assign(new Error(message), { code, expected_connection: expected, connection: selected });
7
+ }
@@ -1,11 +1,19 @@
1
1
  import { normalizeThreadId } from "./thread-id.mjs";
2
+ import { isCloudTask } from "./cloud-id.mjs";
3
+ import { connectionMismatch } from "./connection-error.mjs";
2
4
  export function isDesktopCloudTask(value) {
3
5
  if (typeof value !== "string") return false;
4
6
  if (value.startsWith("durable_")) return true;
5
7
  try { const u = new URL(value); return u.protocol === "codex:" && u.hostname === "threads" && u.searchParams.getAll("hostId").includes("durable"); } catch { return false; }
6
8
  }
7
9
  export function desktopCloudId(value) {
8
- if (!isDesktopCloudTask(value)) throw Object.assign(new Error("Desktop Cloud requires a durable_ ID or a link with hostId=durable."), { code: "CLOUD_TARGET_CONFLICT" });
10
+ if (!isDesktopCloudTask(value)) {
11
+ if (isCloudTask(value)) throw connectionMismatch("cloud", "desktop-cloud");
12
+ let local = false;
13
+ try { normalizeThreadId(value); local = true; } catch {}
14
+ if (local) throw connectionMismatch("shared", "desktop-cloud");
15
+ throw Object.assign(new Error("Desktop Cloud requires a durable_ ID or a link with hostId=durable."), { code: "CLOUD_TARGET_CONFLICT" });
16
+ }
9
17
  let uuid = value.slice(8);
10
18
  if (!value.startsWith("durable_")) {
11
19
  const url = new URL(value);