claude-codex-pair 0.3.1 → 0.4.1

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/DESIGN.md CHANGED
@@ -74,7 +74,21 @@ active threads.
74
74
  freshness to the channel and lane states. Freshness compares the marked pair section in
75
75
  `AGENTS.md` and the generated Claude skill against the same templates used by `init`. It is
76
76
  read-only and describes files on disk, not instructions already loaded into a running session.
77
- The check runs only on `status`, not during polling or message delivery.
77
+ The comparison ignores the `<!-- pair x.y.z -->` stamp that records which version wrote a file.
78
+
79
+ `send`, `wait`, `history`, `clean`, `register` and `unregister` run the same check once at startup,
80
+ not on each poll, and rewrite stale files. They skip files stamped by a newer pair, so worktrees
81
+ shared by two versions don't flip back and forth. A write failure is a warning on stderr, so a
82
+ read-only sandbox still delivers messages. `status` stays read-only, and `mcp` never refreshes:
83
+ it runs unsandboxed and only reads. `status` and `init` also report a newer npm version. They check
84
+ at most once a day, cache the answer under the state directory, and stay quiet on failure.
85
+ `init` also keeps files a newer pair wrote; deleting them is the way to downgrade. The refresh note goes
86
+ to stderr after the command's own output, so `pair wait`'s first line is still the delivery or
87
+ timeout line agents read. `pair upgrade` installs with npm, then runs the `init` of the package it
88
+ just installed (found with `npm root --global`), not the first `pair` on PATH, because both this
89
+ process and an older copy on PATH would write the old templates. It asks `npm view` first, so it uses
90
+ the same registry as the install. Already on the latest version, it only runs the in-process
91
+ refresh. If the check fails, it goes ahead with the install, which reports the actual npm error.
78
92
 
79
93
  Channel operations report `EACCES` and `EPERM` with the affected path and the sandbox
80
94
  configuration printed by `init`. Either ordinary permissions or sandbox restrictions can
package/README.md CHANGED
@@ -158,7 +158,7 @@ lines. Pair needs no extra sandbox config, because every channel lives under `~/
158
158
 
159
159
  ## Commands
160
160
 
161
- The agents run `send` and `wait` themselves. You'll mostly use `init`, `status`, `history` and `clean`.
161
+ The agents run `send` and `wait` themselves. You'll mostly use `init`, `status`, `history`, `clean` and `upgrade`.
162
162
 
163
163
  ```
164
164
  pair send <request|review|response> --as <claude|codex> [--verdict approve|changes] <file|->
@@ -167,6 +167,7 @@ pair status
167
167
  pair history [thread-id] # list finished threads (latest first), or print one (any unique part of its id)
168
168
  pair clean --keep N # delete all but the N newest finished threads
169
169
  pair init
170
+ pair upgrade # install the latest pair from npm, then refresh this worktree's instructions
170
171
  pair uninit # undo pair init in this worktree (keeps history; see "Uninstall")
171
172
  pair register | unregister # run by Codex in its shell: let pair_listen notify this session
172
173
  pair mcp # stdio MCP server for Codex (see "Let pair wake Codex")
@@ -189,11 +190,28 @@ codex notifications: session 01a13922-c08f-79e3-a41f-6780876fd9c5, registered 20
189
190
  last listener event: 2026-10-14T08:05:12.345Z notified 01a13922-… about /Users/you/.pair/…/001-request.md
190
191
  ```
191
192
 
192
- Instruction states are `current`, `stale`, `missing`, or `unreadable`. The check compares only
193
+ Instruction states are `current`, `stale`, `newer`, `missing`, or `unreadable`. The check compares only
193
194
  the marked pair section in `AGENTS.md`, so your other instructions don't affect it. Status
194
195
  doesn't change files. Run `pair init` to refresh missing or stale instructions, then reload
195
196
  agent sessions; fix access to unreadable files first. Current files on disk don't guarantee
196
- that a running agent has loaded them.
197
+ that a running agent has loaded them. `newer` means a newer pair wrote them: run `pair upgrade`.
198
+
199
+ `pair status` and `pair init` also print a line when npm has a newer version. They check at most
200
+ once a day and cache the answer in `~/.local/state/pair/latest.json`. The check stays quiet when
201
+ offline or sandboxed.
202
+
203
+ ## Upgrading
204
+
205
+ Run `pair upgrade` in your own terminal, not through an agent: agent sandboxes block the network and
206
+ global installs. If npm has a newer version, it runs `npm install -g claude-codex-pair@latest`, then
207
+ the new `pair init` in the current worktree if pair is set up there, and prints only the files that
208
+ `init` wrote or kept. Already on the latest version, it installs nothing and only refreshes this
209
+ worktree's stale instructions. Other worktrees refresh their instructions on the next
210
+ `pair send`, `wait`, `history` or `clean`. pair owns these files, so edits inside them are
211
+ overwritten. pair, including `pair init`, never rewrites instructions that a newer pair wrote, so
212
+ worktrees shared by different versions don't flip back and forth. To downgrade, delete them first. Restart Claude Code and Codex afterwards. Running
213
+ sessions keep the instructions they loaded at start, and Codex keeps its old `pair mcp` until
214
+ it restarts.
197
215
 
198
216
  If a channel read or write fails with `EACCES` or `EPERM`, the command names the path and prints
199
217
  sandbox configuration snippets using your actual channel directory. Check ordinary filesystem
@@ -206,6 +224,7 @@ so a cleanup problem doesn't prevent delivering a review or starting the next th
206
224
  | `PAIR_AGENT` | (none) | Default for `--as` |
207
225
  | `PAIR_MAX_ROUNDS` | `3` | Number of review rounds before escalation |
208
226
  | `PAIR_KEEP` | `100` | Finished threads to keep, by finish time. Older ones are deleted whenever a thread finishes. `all` keeps everything, `0` keeps none |
227
+ | `PAIR_NO_UPDATE_CHECK` | (none) | Any value turns off the daily npm version check |
209
228
 
210
229
  ## Uninstall
211
230
 
package/dist/pair.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execFile, execFileSync } from "node:child_process";
2
+ import { execFile, execFileSync, spawnSync } from "node:child_process";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { accessSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
@@ -24,6 +24,12 @@ const PAIR_SECTIONS = /<!-- pair:start -->[\s\S]*?<!-- pair:end -->(?:\r?\n)?/g;
24
24
  // SKILL.md as released versions wrote it (0.1.0 and 0.1.1), so `uninit` can tell it from an edited one.
25
25
  const RELEASED_SKILL_SHA256 = ["e9afb64c53d5e90f935bcb66406601a31a91b37758467cf867713e245f8af288"];
26
26
  const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
27
+ const PACKAGE = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
28
+ // Which pair wrote an instruction file, so an older pair never rolls back what a newer one wrote.
29
+ const STAMP = /<!-- pair (\S+) -->\n/;
30
+ // Commands that refresh stale instructions. Not status (a read-only diagnosis) or mcp (unsandboxed, so it only reads).
31
+ const REFRESHING_COMMANDS = ["send", "wait", "history", "clean", "register", "unregister"];
32
+ const UPDATE_CHECK_MS = 24 * 60 * 60 * 1000;
27
33
  const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; // any UUID version; Codex uses v7
28
34
  const SESSION_FILE = "codex.session"; // the Codex session that pair_listen may notify, per channel
29
35
  const LISTEN_LOG = "codex.listen.log";
@@ -506,9 +512,12 @@ function rotateLog(path) {
506
512
  * any check and the write that follows, so the unsandboxed server only ever reads channels.
507
513
  */
508
514
  function stateDir(channel) {
515
+ return join(stateHome(), basename(channel));
516
+ }
517
+ function stateHome() {
509
518
  const configured = process.env.XDG_STATE_HOME;
510
519
  const root = configured && isAbsolute(configured) ? configured : join(homedir(), ".local", "state");
511
- return join(resolve(root), "pair", basename(channel));
520
+ return join(resolve(root), "pair");
512
521
  }
513
522
  function logListen(channel, text) {
514
523
  try {
@@ -817,7 +826,11 @@ function instructionState(path, expected, sectionOnly = false) {
817
826
  if (actual === undefined) {
818
827
  return "missing"; // AGENTS.md exists but has no complete pair section
819
828
  }
820
- return actual === expected ? "current" : "stale";
829
+ if (unstamped(actual) === unstamped(expected)) {
830
+ return "current";
831
+ }
832
+ const writer = STAMP.exec(actual)?.[1];
833
+ return writer && isNewer(writer, PACKAGE.version) ? `newer (pair ${writer})` : "stale";
821
834
  }
822
835
  catch (error) {
823
836
  if (isMissing(error)) {
@@ -855,13 +868,12 @@ function notificationStatus(channel) {
855
868
  }
856
869
  function status(channel, root) {
857
870
  const lanes = channelAccess(channel, () => AGENTS.map((agent) => describe(channel, agent)));
858
- const { name, version } = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
859
871
  const agents = instructionState(join(root, "AGENTS.md"), codexSection(), true);
860
872
  const skill = instructionState(join(root, ".claude", "skills", "pair", "SKILL.md"), claudeSkill());
861
873
  return [
862
874
  `channel: ${channel}`,
863
875
  `worktree: ${root}`,
864
- `version: ${name} ${version}`,
876
+ `version: ${PACKAGE.name} ${PACKAGE.version}`,
865
877
  ...lanes,
866
878
  "instructions on disk (does not verify what running agents loaded):",
867
879
  ` AGENTS.md (pair section): ${agents}`,
@@ -871,8 +883,137 @@ function status(channel, root) {
871
883
  ? ["Run `pair init` to refresh missing or stale instructions, then reload agent sessions."] : []),
872
884
  ...(agents.startsWith("unreadable") || skill.startsWith("unreadable")
873
885
  ? ["Check permissions on the unreadable instruction paths before refreshing them."] : []),
886
+ ...(agents.startsWith("newer") || skill.startsWith("newer")
887
+ ? ["A newer pair wrote these instructions: run `pair upgrade` in your own terminal."] : []),
874
888
  ].join("\n");
875
889
  }
890
+ /**
891
+ * Rewrites instructions an older pair wrote; leaves missing, unreadable and newer ones for `pair status` to report.
892
+ * Returns a note for after the command's output: agents read `pair wait`'s first line, with stderr merged.
893
+ */
894
+ function refreshInstructions(root) {
895
+ try {
896
+ return refreshedNote(refreshStale(root));
897
+ }
898
+ catch (error) {
899
+ // A read-only sandbox: message delivery must still work, so report and go on.
900
+ const reason = error instanceof Error ? error.message : String(error);
901
+ return `pair: couldn't refresh instructions (${reason}); run \`pair init\` in your own terminal`;
902
+ }
903
+ }
904
+ /** The stale instruction files it rewrote. A write failure throws. */
905
+ function refreshStale(root) {
906
+ const skillPath = join(root, ".claude", "skills", "pair", "SKILL.md");
907
+ const agentsPath = join(root, "AGENTS.md");
908
+ const refreshed = [];
909
+ if (instructionState(skillPath, claudeSkill()) === "stale") {
910
+ writeFileSync(skillPath, claudeSkill());
911
+ refreshed.push(skillPath);
912
+ }
913
+ if (instructionState(agentsPath, codexSection(), true) === "stale") {
914
+ writeFileSync(agentsPath, readFileSync(agentsPath, "utf8").replace(PAIR_SECTION, () => codexSection()));
915
+ refreshed.push(`${agentsPath} (pair section)`);
916
+ }
917
+ return refreshed;
918
+ }
919
+ function refreshedNote(refreshed) {
920
+ return refreshed.length > 0
921
+ ? `pair: refreshed ${refreshed.join(", ")} to pair ${PACKAGE.version}; reload agent sessions to load them`
922
+ : undefined;
923
+ }
924
+ function unstamped(text) {
925
+ return text.replace(STAMP, "");
926
+ }
927
+ function isNewer(version, than) {
928
+ return version.localeCompare(than, "en", { numeric: true }) > 0;
929
+ }
930
+ /** The latest version on npm, checked at most once a day; undefined when opted out or the check fails. */
931
+ async function latestVersion() {
932
+ if (process.env.PAIR_NO_UPDATE_CHECK) {
933
+ return undefined;
934
+ }
935
+ const cache = join(stateHome(), "latest.json");
936
+ try {
937
+ const cached = JSON.parse(readFileSync(cache, "utf8"));
938
+ if (Date.now() - cached.checkedAt < UPDATE_CHECK_MS && typeof cached.version === "string") {
939
+ return cached.version;
940
+ }
941
+ }
942
+ catch {
943
+ // no cache yet, or a corrupt one: check again
944
+ }
945
+ let version;
946
+ try {
947
+ // ponytail: the public registry, not npm's configured one; read `npm config get registry` if mirrors matter
948
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE.name}/latest`, { signal: AbortSignal.timeout(2000) });
949
+ version = (await response.json()).version;
950
+ }
951
+ catch {
952
+ return undefined; // offline or sandboxed: the notice is best-effort
953
+ }
954
+ if (typeof version !== "string") {
955
+ return undefined;
956
+ }
957
+ try {
958
+ mkdirSync(dirname(cache), { recursive: true });
959
+ writeFileSync(cache, JSON.stringify({ checkedAt: Date.now(), version }));
960
+ }
961
+ catch {
962
+ // a read-only state dir only means checking again next time
963
+ }
964
+ return version;
965
+ }
966
+ async function updateNotice() {
967
+ const latest = await latestVersion();
968
+ return latest && isNewer(latest, PACKAGE.version)
969
+ ? [`update available: ${PACKAGE.name} ${PACKAGE.version} → ${latest}. Run \`pair upgrade\` in your own terminal.`]
970
+ : [];
971
+ }
972
+ /** Installs the latest pair, then lets the new version refresh this worktree's instructions. */
973
+ function upgrade(root) {
974
+ // npm's own answer, from the registry it installs from; if it fails, the install reports why.
975
+ const view = spawnSync("npm", ["view", PACKAGE.name, "version"], { encoding: "utf8" });
976
+ const latest = view.status === 0 ? view.stdout.trim() : undefined;
977
+ if (latest && !isNewer(latest, PACKAGE.version)) {
978
+ let note;
979
+ try {
980
+ note = refreshedNote(refreshStale(root)); // refreshing is all that is left to do, so a failure fails upgrade
981
+ }
982
+ catch (error) {
983
+ const reason = error instanceof Error ? error.message : String(error);
984
+ throw new PairError(`already up to date, but couldn't refresh instructions (${reason})`);
985
+ }
986
+ console.log(`already up to date: ${PACKAGE.name} ${PACKAGE.version}${note ? `\n${note}` : ""}`);
987
+ return 0;
988
+ }
989
+ const install = spawnSync("npm", ["install", "--global", `${PACKAGE.name}@latest`], { stdio: "inherit" });
990
+ if (install.error) {
991
+ throw new PairError(`couldn't run npm: ${install.error.message}`);
992
+ }
993
+ if (install.status !== 0) {
994
+ return install.status ?? 1;
995
+ }
996
+ const states = [
997
+ instructionState(join(root, ".claude", "skills", "pair", "SKILL.md"), claudeSkill()),
998
+ instructionState(join(root, "AGENTS.md"), codexSection(), true),
999
+ ];
1000
+ if (states.some((state) => state !== "missing")) {
1001
+ // The package just installed, not whatever `pair` comes first on PATH, which may be an older copy.
1002
+ const globalRoot = spawnSync("npm", ["root", "--global"], { encoding: "utf8" });
1003
+ if (globalRoot.error || globalRoot.status !== 0) {
1004
+ throw new PairError("installed, but `npm root --global` failed; run `pair init` yourself");
1005
+ }
1006
+ const installed = join(globalRoot.stdout.trim(), PACKAGE.name, "dist", "pair.js");
1007
+ const init = spawnSync(process.execPath, [installed, "init"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
1008
+ if (init.error || init.status !== 0) {
1009
+ throw new PairError(`installed, but the new \`pair init\` failed${init.error ? `: ${init.error.message}` : ""}; run it yourself`);
1010
+ }
1011
+ // Only what it did to the files: the sandbox and `codex mcp add` setup it also prints is already done.
1012
+ console.log(init.stdout.split("\n").filter((line) => /^(?:wrote|kept): /.test(line)).join("\n"));
1013
+ }
1014
+ console.log(`upgraded: ${PACKAGE.name} ${PACKAGE.version} → ${latest ?? "latest"}. Restart Claude Code and Codex sessions to load it; other worktrees refresh on their next pair send or wait.`);
1015
+ return 0;
1016
+ }
876
1017
  /** Finished threads, most recently finished first. Entries that aren't archived threads are ignored. */
877
1018
  function listArchived(channel) {
878
1019
  const archiveDir = join(channel, "archive");
@@ -953,25 +1094,42 @@ function renderPrompt(agent) {
953
1094
  }
954
1095
  function claudeSkill() {
955
1096
  const header = `---\nname: pair\ndescription: Request a code review from codex after finishing a task, or act as codex's reviewer ("be my reviewer", "listen"), via the pair CLI.\n---\n\n`;
956
- return header + renderPrompt("claude");
1097
+ return `${header}<!-- pair ${PACKAGE.version} -->\n${renderPrompt("claude")}`;
957
1098
  }
958
1099
  function codexSection() {
959
- return `<!-- pair:start -->\n${renderPrompt("codex").trimEnd()}\n<!-- pair:end -->\n`;
1100
+ return `<!-- pair:start -->\n<!-- pair ${PACKAGE.version} -->\n${renderPrompt("codex").trimEnd()}\n<!-- pair:end -->\n`;
960
1101
  }
961
1102
  function init(channel, root) {
962
1103
  channelAccess(channel, () => mkdirSync(channel, { recursive: true }));
1104
+ // Current files keep their stamp too: a newer pair may have written the same text.
1105
+ const kept = (state) => state === "current" ? " (current)" : state.startsWith("newer") ? " (a newer pair wrote it; run `pair upgrade`, or delete it to downgrade)" : undefined;
1106
+ const done = [];
963
1107
  const skillDir = join(root, ".claude", "skills", "pair");
964
- mkdirSync(skillDir, { recursive: true });
965
- writeFileSync(join(skillDir, "SKILL.md"), claudeSkill());
1108
+ const skillPath = join(skillDir, "SKILL.md");
1109
+ const skillKept = kept(instructionState(skillPath, claudeSkill()));
1110
+ if (skillKept) {
1111
+ done.push(`kept: ${skillPath}${skillKept}`);
1112
+ }
1113
+ else {
1114
+ mkdirSync(skillDir, { recursive: true });
1115
+ writeFileSync(skillPath, claudeSkill());
1116
+ done.push(`wrote: ${skillPath}`);
1117
+ }
966
1118
  const agentsPath = join(root, "AGENTS.md");
967
1119
  const section = codexSection();
968
- const existing = existsSync(agentsPath) ? readFileSync(agentsPath, "utf8") : "";
969
- const updated = PAIR_SECTION.test(existing) ? existing.replace(PAIR_SECTION, () => section) : `${existing}${existing ? "\n" : ""}${section}`;
970
- writeFileSync(agentsPath, updated);
1120
+ const agentsKept = kept(instructionState(agentsPath, section, true));
1121
+ if (agentsKept) {
1122
+ done.push(`kept: ${agentsPath} (pair section)${agentsKept}`);
1123
+ }
1124
+ else {
1125
+ const existing = existsSync(agentsPath) ? readFileSync(agentsPath, "utf8") : "";
1126
+ const updated = PAIR_SECTION.test(existing) ? existing.replace(PAIR_SECTION, () => section) : `${existing}${existing ? "\n" : ""}${section}`;
1127
+ writeFileSync(agentsPath, updated);
1128
+ done.push(`wrote: ${agentsPath} (pair section)`);
1129
+ }
971
1130
  return [
972
1131
  `channel: ${channel}`,
973
- `wrote: ${join(skillDir, "SKILL.md")}`,
974
- `wrote: ${agentsPath} (pair section)`,
1132
+ ...done,
975
1133
  "",
976
1134
  sandboxConfig(channel),
977
1135
  "",
@@ -1017,7 +1175,7 @@ function uninit(channel, root) {
1017
1175
  const skillPath = join(skillDir, "SKILL.md");
1018
1176
  if (existsSync(skillPath)) {
1019
1177
  const skill = readFileSync(skillPath, "utf8");
1020
- if (skill === claudeSkill() || RELEASED_SKILL_SHA256.includes(createHash("sha256").update(skill).digest("hex"))) {
1178
+ if (unstamped(skill) === unstamped(claudeSkill()) || RELEASED_SKILL_SHA256.includes(createHash("sha256").update(skill).digest("hex"))) {
1021
1179
  rmSync(skillPath);
1022
1180
  removed.push(skillPath);
1023
1181
  // Drop the directories init created, up to the first one that holds something else.
@@ -1240,6 +1398,7 @@ const USAGE = `usage:
1240
1398
  pair history [thread-id] (list finished threads, or show one)
1241
1399
  pair clean --keep N (delete all but the N newest finished threads)
1242
1400
  pair init
1401
+ pair upgrade (install the latest pair from npm, then refresh this worktree)
1243
1402
  pair uninit (remove what pair init wrote in this worktree; keeps history)
1244
1403
  pair register | unregister (in Codex's shell: let pair_listen notify this session)
1245
1404
  pair mcp (stdio MCP server for Codex, providing pair_listen)
@@ -1254,57 +1413,67 @@ async function main(argv) {
1254
1413
  const root = worktreeRoot();
1255
1414
  const channel = channelDir(root);
1256
1415
  const agent = () => oneOf(values.as ?? process.env.PAIR_AGENT, AGENTS, "--as (or PAIR_AGENT)");
1257
- switch (command) {
1258
- case "send": {
1259
- autoKeep(); // reject a bad PAIR_KEEP before touching any thread
1260
- const type = oneOf(rest[0], MESSAGE_TYPES, "message type");
1261
- const verdict = values.verdict === undefined ? undefined : oneOf(values.verdict, VERDICTS, "--verdict");
1262
- const me = agent();
1263
- const body = readBody(rest[1]);
1264
- console.log(`sent: ${channelAccess(channel, () => send(channel, me, type, body, verdict))}`);
1265
- return 0;
1266
- }
1267
- case "wait": {
1268
- autoKeep(); // reject a bad PAIR_KEEP before touching any thread
1269
- const me = agent();
1270
- const timeout = positiveNumber(values.timeout ?? String(DEFAULT_WAIT_SECONDS), "--timeout");
1271
- if (!(await wait(channel, me, timeout))) {
1272
- // Exit 0: a timeout is routine, and agent UIs show any non-zero exit as a failure. No "run it
1273
- // again" here: whether to wait again differs by agent (Codex must not poll), so the prompt says.
1274
- console.log(`pair: no message for ${me} yet.`);
1416
+ const refreshNote = REFRESHING_COMMANDS.includes(command) ? refreshInstructions(root) : undefined;
1417
+ try {
1418
+ switch (command) {
1419
+ case "send": {
1420
+ autoKeep(); // reject a bad PAIR_KEEP before touching any thread
1421
+ const type = oneOf(rest[0], MESSAGE_TYPES, "message type");
1422
+ const verdict = values.verdict === undefined ? undefined : oneOf(values.verdict, VERDICTS, "--verdict");
1423
+ const me = agent();
1424
+ const body = readBody(rest[1]);
1425
+ console.log(`sent: ${channelAccess(channel, () => send(channel, me, type, body, verdict))}`);
1426
+ return 0;
1275
1427
  }
1276
- return 0;
1277
- }
1278
- case "status":
1279
- console.log(status(channel, root));
1280
- return 0;
1281
- case "history":
1282
- console.log(channelAccess(channel, () => history(channel, rest[0])));
1283
- return 0;
1284
- case "clean":
1285
- console.log(channelAccess(channel, () => clean(channel, values.keep)));
1286
- return 0;
1287
- case "init":
1288
- console.log(init(channel, root));
1289
- return 0;
1290
- case "uninit":
1291
- console.log(uninit(channel, root));
1292
- return 0;
1293
- case "register":
1294
- console.log(channelAccess(channel, () => register(channel, root)));
1295
- return 0;
1296
- case "unregister":
1297
- console.log(channelAccess(channel, () => unregister(channel)));
1298
- return 0;
1299
- case "mcp":
1300
- await serveMcp();
1301
- return 0;
1302
- case "ps":
1303
- console.log(ps(values.verbose ?? false));
1304
- return 0;
1305
- default:
1306
- console.error(USAGE);
1307
- return 1;
1428
+ case "wait": {
1429
+ autoKeep(); // reject a bad PAIR_KEEP before touching any thread
1430
+ const me = agent();
1431
+ const timeout = positiveNumber(values.timeout ?? String(DEFAULT_WAIT_SECONDS), "--timeout");
1432
+ if (!(await wait(channel, me, timeout))) {
1433
+ // Exit 0: a timeout is routine, and agent UIs show any non-zero exit as a failure. No "run it
1434
+ // again" here: whether to wait again differs by agent (Codex must not poll), so the prompt says.
1435
+ console.log(`pair: no message for ${me} yet.`);
1436
+ }
1437
+ return 0;
1438
+ }
1439
+ case "status":
1440
+ console.log([status(channel, root), ...(await updateNotice())].join("\n"));
1441
+ return 0;
1442
+ case "history":
1443
+ console.log(channelAccess(channel, () => history(channel, rest[0])));
1444
+ return 0;
1445
+ case "clean":
1446
+ console.log(channelAccess(channel, () => clean(channel, values.keep)));
1447
+ return 0;
1448
+ case "init":
1449
+ console.log([init(channel, root), ...(await updateNotice())].join("\n"));
1450
+ return 0;
1451
+ case "upgrade":
1452
+ return upgrade(root);
1453
+ case "uninit":
1454
+ console.log(uninit(channel, root));
1455
+ return 0;
1456
+ case "register":
1457
+ console.log(channelAccess(channel, () => register(channel, root)));
1458
+ return 0;
1459
+ case "unregister":
1460
+ console.log(channelAccess(channel, () => unregister(channel)));
1461
+ return 0;
1462
+ case "mcp":
1463
+ await serveMcp();
1464
+ return 0;
1465
+ case "ps":
1466
+ console.log(ps(values.verbose ?? false));
1467
+ return 0;
1468
+ default:
1469
+ console.error(USAGE);
1470
+ return 1;
1471
+ }
1472
+ }
1473
+ finally {
1474
+ if (refreshNote) {
1475
+ console.error(refreshNote);
1476
+ }
1308
1477
  }
1309
1478
  }
1310
1479
  main(process.argv.slice(2)).then((code) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-pair",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Claude Code and Codex review each other's work over a file-system chat channel",
5
5
  "keywords": [
6
6
  "claude-code",