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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  公開日は日本時間です。
4
4
 
5
+ ## 0.19.0 — 2026-11-01公開
6
+
7
+ - タスクIDと接続指定の不一致エラーに、修正用の`--connection`とJSONの`expected_connection`・`connection`を追加しました。Desktop Cloudには`desktop-cloud`、Web Cloudには`cloud`、ローカルには既定の`shared`を示します。拒否・終了コード・既存エラーコードを維持し、自動切り替えは行いません。
8
+ - `help`の全30トピックを同じ構成に揃えました。各トピックは使い所・使い方・オプション(既定値)・確認できること・エラーと対処・手順・例・補足・関連を持ち、`--json`でも同じ項目名で取得できます。記述は実装と照合し、実装と食い違っていた説明(接続ソケットのリンク、Cloudの版確認の範囲、`status`の`unregistered`など)を直しました。`help desktop-cloud`と`help environments`の「使い所: undefined」表示と、`checkpoint run`・`resource run`の`--json`を`--`の後ろに示していた使い方表示も直しました。トピック名は変わりません。
9
+ - 文言や内部実装に固定されていたテストを整理し、ヘルプの記述とエラーコード・オプションの対応を検査するテストを追加しました。
10
+ - Desktop Cloudで受付不明の送信・プロジェクト割り当てが残ると、以後の送信・`thread assign`が永久に拒否される問題を修正しました。Desktopのプロフィール状態で割り当てを確認できた記録は自動で確定します。確認できない記録は、ユーザーが`history mark <TASK> <MESSAGE-ID> --resolve-unknown --note TEXT`または`thread receipt <ATTEMPT> --resolve-unknown --note TEXT`で理由を残して解除できます(`--supervisor`付きは`SUPERVISOR_CONTROL_FORBIDDEN`)。記録がないことを未配送の証拠にはしません。解除した送信は、結び付いた指摘でも受付不明として扱いません。拒否エラーには解除対象の`existing_message_id`・`existing_attempt_id`を含めます。
11
+ - ローカルの`send`が、`--`より前の未知のオプション(`--kidn`などの打ち間違い)を本文として送っていた問題を修正し、`OPTIONS_INVALID`で拒否するようにしました。`--`より後ろは従来どおり本文です。
12
+ - ローカルの`resource`で、`--supervisor`付きの`acquire`・`renew`・`release`・`status`・`list`が監督状態を確認していなかった問題を修正しました。`--supervisor`付きは`--thread`が必須になり、停止済み・別セッションは拒否、一時停止中は`acquire`・`renew`・`run`を`SUPERVISOR_PAUSED`で拒否します。
13
+ - ローカルタスクへの`diff`が送信の省略形として解釈されていた問題を修正し、接続前に`OPTIONS_INVALID`で拒否します。
14
+ - コードを持たなかった利用者向けの失敗に、エラーコードを付けました(`OPTIONS_INVALID`、`MESSAGE_NOT_FOUND`、`CHECKPOINT_NOT_FOUND`、`INSTRUCTION_NOT_REPLACEABLE`、`RESOURCE_NOT_OWNED`、`TASK_IDLE`、`TASK_BUSY`、`DESKTOP_START_UNSUPPORTED`、`DESKTOP_ALREADY_RUNNING`、`DESKTOP_START_FAILED`、`DESKTOP_UI_NOT_READY`、`DESKTOP_UI_FAILED`、`CURSOR_TOO_LARGE`、`STORE_UNSAFE`など)。実行中のターンがない送信と、idleでないタスクへの`--new-turn`は、`NOT_SENT`ではなく`TASK_IDLE`・`TASK_BUSY`になります。中断したローカルの`checkpoint run`・`resource run`は`error_code: CANCELLED`を返します。
15
+
5
16
  ## 0.18.2 — 2026-11-01公開
6
17
 
7
18
  - 停止済みの監督IDが、別の監督への交代後に古い本文から再登録できる問題を修正しました。停止履歴を監督記録に保持し、登録前のdoctor・再登録・観測・送信で`SUPERVISOR_STOPPED`を返します。旧形式の停止済み記録も、交代時に履歴へ引き継ぎます。保存済み旧CLIを使う監督には、更新後のCLIでプロンプトを生成し直してください。
package/README.md CHANGED
@@ -48,6 +48,8 @@ npx -y codexteer@0.18.0 supervise <thread-id>
48
48
 
49
49
  DesktopのCloud環境構成は、`codexteer --connection desktop-cloud environments list --json`で取得します。新規作成は`thread create --environment <ID_OR_EXACT_NAME>`、継続・監視・監督は`durable_`付きIDまたは`hostId=durable`付きリンクで共通コマンドを使います。所属情報と登録APIの可否は`doctor --thread ... --project ...`で区別します。公式の署名付き`thread assign`は対象がそのDesktopの実行環境を使う場合に限り利用でき、通常のCloud環境では`--project`付き作成を現在も事前拒否します。[操作・確認済み範囲と制限](docs/desktop-cloud.md)
50
50
 
51
+ タスクと明示した接続が合わない場合は拒否し、エラー文に修正用の`--connection`、JSONの`error.expected_connection`に修正候補、`error.connection`に今回の指定値を返します。`durable_`付きID・`hostId=durable`付きリンクには`--connection desktop-cloud`、Web Cloudの`task_`付きID・公式リンクには`--connection cloud`を使います。ローカルUUIDには既定の`--connection shared`を候補として示します。ローカルの観測には`--connection desktop`も利用できますが、送信には使えません。明示指定を自動で切り替えることはありません。
52
+
51
53
  ### Codex Cloudを使う
52
54
 
53
55
  CloudタスクのID・公式リンクで、同じ`supervise`・`read/watch`・`send`・`history/instructions/findings`を使えます。監督の`pause/resume/stop`や保存済みCLIも共通です。接続先をまだ選んでいない一覧と作成は`--connection cloud projects list`・`threads list`・`thread create --project <ENV_ID>`を使います。実行中のCloud送信は同じタスクの元ターンを中断して後続ターンを開始する方式です。[操作とサービス上の違い](docs/cloud.md)
@@ -7,6 +7,8 @@
7
7
 
8
8
  `0.18.2`は、監督が交代した後も停止済みIDを履歴に保持し、古い本文からの再登録・観測・送信を`SUPERVISOR_STOPPED`で拒否します。登録前の`doctor`にも同じ判定を適用します。同じIDの別タスクへの登録を拒否し、旧版で重複したIDを持つ記録も`supervise list --json`で全件と衝突先を返します。該当タスクはそれぞれ`supervise stop`で停止できます。更新前の停止済み記録は交代時に引き継ぎますが、すでに上書きされて失われた停止履歴は復元できません。保存済み旧CLIには修正が反映されないため、更新後のCLIで監督を起動し直すか、プロンプトを生成し直してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は必要ありません。
9
9
 
10
+ `0.19.0`は、`help`の全トピックを同じ構成に揃えて実装と照合し、Desktop Cloudで受付不明の記録が残ると以後の送信・割り当てが止まる問題、ローカル`send`の未知オプションの送信、`--supervisor`付きローカル`resource`の監督状態の未確認を修正します。利用者向けの失敗にはエラーコードを付けました。実行中のターンがない送信の`NOT_SENT`は`TASK_IDLE`・`TASK_BUSY`に変わるため、コードで分岐している呼び出し側は更新してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は必要ありません。保存済み旧CLIで動く監督には反映されないため、更新後のCLIで監督を起動し直すか、プロンプトを生成し直してください。
11
+
10
12
  `package.json` は `private` を持たず、`publishConfig` でnpm公式レジストリへのpublic公開を指定しています。公開可能な設定であることと、実際に公開済みであることは別です。
11
13
 
12
14
  公開後の基本的な実行形式は次のとおりです。通常はバージョン指定なしで使えます。監督開始後は保存したCLIを使い続けるため、監督中の一貫性を保つために版を明示する必要はありません。起動側と操作側の製品バージョンも、必要な通信仕様が対応していれば異なっていても使えます。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexteer",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Send steering messages to running Codex Desktop threads from a terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,7 +74,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
74
74
  const input = [{ type: "text", text: message }];
75
75
  if (!newTurn) {
76
76
  const turn = activeTurn(thread);
77
- if (!turn) throw new Error("The task has no eligible active turn. Read it again before sending; use --new-turn only for an idle task.");
77
+ if (!turn) throw Object.assign(new Error("The task has no eligible active turn. Read it again before sending; use --new-turn only for an idle task."), { code: "TASK_IDLE" });
78
78
  await beforeSend?.(thread, client);
79
79
  // Desktop uses clientId on the resulting userMessage to render external
80
80
  // steering as a user bubble instead of just an anonymous "steered" marker.
@@ -84,7 +84,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
84
84
  if (accepted?.turnId !== turn.id) throw new RpcFailure("App Server returned an unexpected turn ID. Do not retry automatically.", { code: "PROTOCOL_ERROR", uncertain: true });
85
85
  return { turn_id: accepted.turnId, client_message_id: clientUserMessageId };
86
86
  }
87
- if (!["idle", "notLoaded"].includes(thread.status?.type) || thread.turns.some(turn => turn.status === "inProgress")) throw new Error("--new-turn requires an idle task. Nothing was sent.");
87
+ if (!["idle", "notLoaded"].includes(thread.status?.type) || thread.turns.some(turn => turn.status === "inProgress")) throw Object.assign(new Error("--new-turn requires an idle task. Nothing was sent."), { code: "TASK_BUSY" });
88
88
  // Resume through Desktop's long-lived connection so approval ownership
89
89
  // survives the short-lived sender. Never resume only on the sender connection.
90
90
  if (!subscribe) throw new Error("Desktop subscription support is required for --new-turn.");
@@ -93,7 +93,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
93
93
  await beforeSend?.(thread, client);
94
94
  await subscribe(threadId);
95
95
  const resumed = await readSendState(client, threadId);
96
- if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw new Error("Task became active while resuming. Nothing was sent.");
96
+ if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw Object.assign(new Error("Task became active while resuming. Nothing was sent."), { code: "TASK_BUSY" });
97
97
  await beforeSend?.(resumed, client);
98
98
  const clientUserMessageId = clientMessageId;
99
99
  const accepted = await client.request("turn/start", { threadId, input, clientUserMessageId }, { mutation: true });
@@ -110,7 +110,7 @@ export async function sendAppServerMessage(threadInput, message, { dryRun = fals
110
110
  const route = remote ? remoteControlConnection() : {};
111
111
  connect ??= route.connect ?? RpcClient.connect; discover ??= route.discover ?? discoverRuntime;
112
112
  const threadId = normalizeThreadId(threadInput);
113
- if (typeof message !== "string" || !message.trim()) throw new Error("Message must not be empty.");
113
+ if (typeof message !== "string" || !message.trim()) throw Object.assign(new Error("Message must not be empty."), { code: "OPTIONS_INVALID" });
114
114
  const plan = { thread_id: threadId, ...hostFields(), backend: "app-server", delivery_action: newTurn ? "new-turn" : "steer", message_characters: [...message].length, dry_run: dryRun };
115
115
  if (dryRun) return { ...plan, sent: false, delivery_status: "not_sent" };
116
116
  let client;
@@ -22,13 +22,13 @@ export async function captureInputs(roots, excludes = [], { allowMissing = false
22
22
  try { info = await lstat(file); } catch (e) { if (e.code === "ENOENT" && allowMissing) return; throw e; }
23
23
  if (info.isSymbolicLink()) {
24
24
  const target = await realpath(file), targetInfo = await lstat(target);
25
- if (targetInfo.isDirectory()) throw new Error("Directory symlinks are not snapshot inputs. Specify the real directory explicitly.");
25
+ if (targetInfo.isDirectory()) throw Object.assign(new Error("Directory symlinks are not snapshot inputs. Specify the real directory explicitly."), { code: "OPTIONS_INVALID" });
26
26
  records.set(file, { ...await fingerprintFile(file), link: await readlink(file), mtime_ms: targetInfo.mtimeMs, ctime_ms: targetInfo.ctimeMs });
27
27
  } else if (info.isDirectory()) {
28
28
  records.set(file, { type: "directory", ref: file, mode: info.mode & 0o777 });
29
29
  for (const name of (await readdir(file)).sort()) await visit(path.join(file, name));
30
30
  } else if (info.isFile()) records.set(file, { ...await fingerprintFile(file), mtime_ms: info.mtimeMs, ctime_ms: info.ctimeMs });
31
- else throw new Error("Snapshot inputs must be files or directories.");
31
+ else throw Object.assign(new Error("Snapshot inputs must be files or directories."), { code: "OPTIONS_INVALID" });
32
32
  }
33
33
  for (const root of roots) await visit(root);
34
34
  const files = [...records.values()].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -42,18 +42,18 @@ function differences(before, after) {
42
42
 
43
43
  export async function getCheckpoint(threadInput, id, storage = {}) {
44
44
  const threadId = normalizeTaskId(threadInput), entry = await readRecord("checkpoints", id, storage);
45
- if (!entry || entry.thread_id !== threadId || entry.id !== id) throw new Error("Checkpoint not found for this task.");
45
+ if (!entry || entry.thread_id !== threadId || entry.id !== id) throw Object.assign(new Error("Checkpoint not found for this task."), { code: "CHECKPOINT_NOT_FOUND" });
46
46
  return entry;
47
47
  }
48
48
 
49
49
  export async function createCheckpoint(threadInput, name, { paths, excludes = [], cwd = process.cwd(), ...storage }) {
50
50
  const threadId = normalizeTaskId(threadInput);
51
- if (!name?.trim() || !paths?.length) throw new Error("capture requires a name and at least one --path.");
51
+ if (!name?.trim() || !paths?.length) throw Object.assign(new Error("capture requires a name and at least one --path."), { code: "OPTIONS_INVALID" });
52
52
  const scope = targetHost() === "local" ? { cwd: path.resolve(cwd), roots: [...new Set(paths.map(p => path.resolve(cwd, p)))], excludes: excludes.map(p => path.resolve(cwd, p)) }
53
53
  : await remoteCheckpointScope(threadId, paths, excludes);
54
54
  const { roots, excludes: omit } = scope;
55
55
  const snapshot = await captureInputs(roots, omit);
56
- if (!snapshot.files.some(f => f.type === "file")) throw new Error("No input files were selected.");
56
+ if (!snapshot.files.some(f => f.type === "file")) throw Object.assign(new Error("No input files were selected."), { code: "OPTIONS_INVALID" });
57
57
  const entry = { schema: 1, id: randomUUID(), thread_id: threadId, name, cwd: scope.cwd, created_at: new Date().toISOString(), snapshot, runs: [], artifacts: [] };
58
58
  await writeRecord("checkpoints", entry.id, entry, storage);
59
59
  return checkpointSummary(entry);
@@ -115,13 +115,13 @@ function watchInputs(snapshot) {
115
115
  }
116
116
 
117
117
  export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, execBackend, remoteShell, supervisor, connection, ...storage } = {}) {
118
- if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw new Error("A command and timeout-ms between 0 and 86400000 are required.");
118
+ 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" });
119
119
  const threadId = normalizeTaskId(threadInput);
120
120
  return withStoreLock(`checkpoint-${id}`, async () => {
121
121
  const entry = await getCheckpoint(threadId, id, storage), watcher = targetHost() === "local" ? watchInputs(entry.snapshot) : await remoteWatchInputs(entry.snapshot);
122
122
  try {
123
123
  const before = await captureInputs(entry.snapshot.roots, entry.snapshot.excludes, { allowMissing: true });
124
- if (before.digest !== entry.snapshot.digest) throw new Error("Inputs changed since capture. Capture a new checkpoint before running.");
124
+ if (before.digest !== entry.snapshot.digest) throw Object.assign(new Error("Inputs changed since capture. Capture a new checkpoint before running."), { code: "STALE_CHECKPOINT" });
125
125
  const run = { id: randomUUID(), argv, status: "running", started_at: new Date().toISOString() };
126
126
  entry.runs.push(run); await writeRecord("checkpoints", id, entry, storage);
127
127
  let result;
@@ -145,12 +145,12 @@ export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, exec
145
145
  }
146
146
 
147
147
  export async function attachArtifacts(threadInput, id, refs, storage = {}) {
148
- if (!refs.length) throw new Error("At least one --artifact file is required.");
148
+ if (!refs.length) throw Object.assign(new Error("At least one --artifact file is required."), { code: "OPTIONS_INVALID" });
149
149
  return withStoreLock(`checkpoint-${id}`, async () => {
150
150
  const verified = await verifyCheckpoint(threadInput, id, storage);
151
- if (!verified.valid) throw new Error("A successful run against unchanged inputs is required before attaching artifacts.");
151
+ if (!verified.valid) throw Object.assign(new Error("A successful run against unchanged inputs is required before attaching artifacts."), { code: "STALE_CHECKPOINT" });
152
152
  const artifacts = await collectEvidence(refs);
153
- if (artifacts.some(a => a.type !== "file")) throw new Error("Artifacts must be local files.");
153
+ if (artifacts.some(a => a.type !== "file")) throw Object.assign(new Error("Artifacts must be local files."), { code: "OPTIONS_INVALID" });
154
154
  const entry = await getCheckpoint(threadInput, id, storage);
155
155
  entry.artifacts.push(...artifacts.map(a => ({ ...a, run_id: entry.runs.at(-1).id, attached_at: new Date().toISOString() })));
156
156
  await writeRecord("checkpoints", id, entry, storage);