claude-codex-pair 0.3.1 → 0.4.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/DESIGN.md CHANGED
@@ -74,7 +74,19 @@ 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.
78
90
 
79
91
  Channel operations report `EACCES` and `EPERM` with the affected path and the sandbox
80
92
  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,26 @@ 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. It runs `npm install -g claude-codex-pair@latest`, then the new `pair init` in the
207
+ current worktree if pair is set up there. Other worktrees refresh their instructions on the next
208
+ `pair send`, `wait`, `history` or `clean`. pair owns these files, so edits inside them are
209
+ overwritten. pair, including `pair init`, never rewrites instructions that a newer pair wrote, so
210
+ worktrees shared by different versions don't flip back and forth. To downgrade, delete them first. Restart Claude Code and Codex afterwards. Running
211
+ sessions keep the instructions they loaded at start, and Codex keeps its old `pair mcp` until
212
+ it restarts.
197
213
 
198
214
  If a channel read or write fails with `EACCES` or `EPERM`, the command names the path and prints
199
215
  sandbox configuration snippets using your actual channel directory. Check ordinary filesystem
@@ -206,6 +222,7 @@ so a cleanup problem doesn't prevent delivering a review or starting the next th
206
222
  | `PAIR_AGENT` | (none) | Default for `--as` |
207
223
  | `PAIR_MAX_ROUNDS` | `3` | Number of review rounds before escalation |
208
224
  | `PAIR_KEEP` | `100` | Finished threads to keep, by finish time. Older ones are deleted whenever a thread finishes. `all` keeps everything, `0` keeps none |
225
+ | `PAIR_NO_UPDATE_CHECK` | (none) | Any value turns off the daily npm version check |
209
226
 
210
227
  ## Uninstall
211
228
 
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,113 @@ 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
+ const skillPath = join(root, ".claude", "skills", "pair", "SKILL.md");
896
+ const agentsPath = join(root, "AGENTS.md");
897
+ const refreshed = [];
898
+ try {
899
+ if (instructionState(skillPath, claudeSkill()) === "stale") {
900
+ writeFileSync(skillPath, claudeSkill());
901
+ refreshed.push(skillPath);
902
+ }
903
+ if (instructionState(agentsPath, codexSection(), true) === "stale") {
904
+ writeFileSync(agentsPath, readFileSync(agentsPath, "utf8").replace(PAIR_SECTION, () => codexSection()));
905
+ refreshed.push(`${agentsPath} (pair section)`);
906
+ }
907
+ }
908
+ catch (error) {
909
+ // A read-only sandbox: message delivery must still work, so report and go on.
910
+ const reason = error instanceof Error ? error.message : String(error);
911
+ return `pair: couldn't refresh instructions (${reason}); run \`pair init\` in your own terminal`;
912
+ }
913
+ return refreshed.length > 0
914
+ ? `pair: refreshed ${refreshed.join(", ")} to pair ${PACKAGE.version}; reload agent sessions to load them`
915
+ : undefined;
916
+ }
917
+ function unstamped(text) {
918
+ return text.replace(STAMP, "");
919
+ }
920
+ function isNewer(version, than) {
921
+ return version.localeCompare(than, "en", { numeric: true }) > 0;
922
+ }
923
+ /** The latest version on npm, checked at most once a day; undefined when opted out or the check fails. */
924
+ async function latestVersion() {
925
+ if (process.env.PAIR_NO_UPDATE_CHECK) {
926
+ return undefined;
927
+ }
928
+ const cache = join(stateHome(), "latest.json");
929
+ try {
930
+ const cached = JSON.parse(readFileSync(cache, "utf8"));
931
+ if (Date.now() - cached.checkedAt < UPDATE_CHECK_MS && typeof cached.version === "string") {
932
+ return cached.version;
933
+ }
934
+ }
935
+ catch {
936
+ // no cache yet, or a corrupt one: check again
937
+ }
938
+ let version;
939
+ try {
940
+ // ponytail: the public registry, not npm's configured one; read `npm config get registry` if mirrors matter
941
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE.name}/latest`, { signal: AbortSignal.timeout(2000) });
942
+ version = (await response.json()).version;
943
+ }
944
+ catch {
945
+ return undefined; // offline or sandboxed: the notice is best-effort
946
+ }
947
+ if (typeof version !== "string") {
948
+ return undefined;
949
+ }
950
+ try {
951
+ mkdirSync(dirname(cache), { recursive: true });
952
+ writeFileSync(cache, JSON.stringify({ checkedAt: Date.now(), version }));
953
+ }
954
+ catch {
955
+ // a read-only state dir only means checking again next time
956
+ }
957
+ return version;
958
+ }
959
+ async function updateNotice() {
960
+ const latest = await latestVersion();
961
+ return latest && isNewer(latest, PACKAGE.version)
962
+ ? [`update available: ${PACKAGE.name} ${PACKAGE.version} → ${latest}. Run \`pair upgrade\` in your own terminal.`]
963
+ : [];
964
+ }
965
+ /** Installs the latest pair, then lets the new version refresh this worktree's instructions. */
966
+ function upgrade(root) {
967
+ const install = spawnSync("npm", ["install", "--global", `${PACKAGE.name}@latest`], { stdio: "inherit" });
968
+ if (install.error) {
969
+ throw new PairError(`couldn't run npm: ${install.error.message}`);
970
+ }
971
+ if (install.status !== 0) {
972
+ return install.status ?? 1;
973
+ }
974
+ const states = [
975
+ instructionState(join(root, ".claude", "skills", "pair", "SKILL.md"), claudeSkill()),
976
+ instructionState(join(root, "AGENTS.md"), codexSection(), true),
977
+ ];
978
+ if (states.some((state) => state !== "missing")) {
979
+ // The package just installed, not whatever `pair` comes first on PATH, which may be an older copy.
980
+ const globalRoot = spawnSync("npm", ["root", "--global"], { encoding: "utf8" });
981
+ if (globalRoot.error || globalRoot.status !== 0) {
982
+ throw new PairError("installed, but `npm root --global` failed; run `pair init` yourself");
983
+ }
984
+ const installed = join(globalRoot.stdout.trim(), PACKAGE.name, "dist", "pair.js");
985
+ const init = spawnSync(process.execPath, [installed, "init"], { cwd: root, stdio: "inherit" });
986
+ if (init.error || init.status !== 0) {
987
+ throw new PairError(`installed, but the new \`pair init\` failed${init.error ? `: ${init.error.message}` : ""}; run it yourself`);
988
+ }
989
+ }
990
+ console.log("Restart Claude Code and Codex sessions to load the new version. Other worktrees refresh on their next pair send or wait.");
991
+ return 0;
992
+ }
876
993
  /** Finished threads, most recently finished first. Entries that aren't archived threads are ignored. */
877
994
  function listArchived(channel) {
878
995
  const archiveDir = join(channel, "archive");
@@ -953,25 +1070,42 @@ function renderPrompt(agent) {
953
1070
  }
954
1071
  function claudeSkill() {
955
1072
  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");
1073
+ return `${header}<!-- pair ${PACKAGE.version} -->\n${renderPrompt("claude")}`;
957
1074
  }
958
1075
  function codexSection() {
959
- return `<!-- pair:start -->\n${renderPrompt("codex").trimEnd()}\n<!-- pair:end -->\n`;
1076
+ return `<!-- pair:start -->\n<!-- pair ${PACKAGE.version} -->\n${renderPrompt("codex").trimEnd()}\n<!-- pair:end -->\n`;
960
1077
  }
961
1078
  function init(channel, root) {
962
1079
  channelAccess(channel, () => mkdirSync(channel, { recursive: true }));
1080
+ // Current files keep their stamp too: a newer pair may have written the same text.
1081
+ const kept = (state) => state === "current" ? " (current)" : state.startsWith("newer") ? " (a newer pair wrote it; run `pair upgrade`, or delete it to downgrade)" : undefined;
1082
+ const done = [];
963
1083
  const skillDir = join(root, ".claude", "skills", "pair");
964
- mkdirSync(skillDir, { recursive: true });
965
- writeFileSync(join(skillDir, "SKILL.md"), claudeSkill());
1084
+ const skillPath = join(skillDir, "SKILL.md");
1085
+ const skillKept = kept(instructionState(skillPath, claudeSkill()));
1086
+ if (skillKept) {
1087
+ done.push(`kept: ${skillPath}${skillKept}`);
1088
+ }
1089
+ else {
1090
+ mkdirSync(skillDir, { recursive: true });
1091
+ writeFileSync(skillPath, claudeSkill());
1092
+ done.push(`wrote: ${skillPath}`);
1093
+ }
966
1094
  const agentsPath = join(root, "AGENTS.md");
967
1095
  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);
1096
+ const agentsKept = kept(instructionState(agentsPath, section, true));
1097
+ if (agentsKept) {
1098
+ done.push(`kept: ${agentsPath} (pair section)${agentsKept}`);
1099
+ }
1100
+ else {
1101
+ const existing = existsSync(agentsPath) ? readFileSync(agentsPath, "utf8") : "";
1102
+ const updated = PAIR_SECTION.test(existing) ? existing.replace(PAIR_SECTION, () => section) : `${existing}${existing ? "\n" : ""}${section}`;
1103
+ writeFileSync(agentsPath, updated);
1104
+ done.push(`wrote: ${agentsPath} (pair section)`);
1105
+ }
971
1106
  return [
972
1107
  `channel: ${channel}`,
973
- `wrote: ${join(skillDir, "SKILL.md")}`,
974
- `wrote: ${agentsPath} (pair section)`,
1108
+ ...done,
975
1109
  "",
976
1110
  sandboxConfig(channel),
977
1111
  "",
@@ -1017,7 +1151,7 @@ function uninit(channel, root) {
1017
1151
  const skillPath = join(skillDir, "SKILL.md");
1018
1152
  if (existsSync(skillPath)) {
1019
1153
  const skill = readFileSync(skillPath, "utf8");
1020
- if (skill === claudeSkill() || RELEASED_SKILL_SHA256.includes(createHash("sha256").update(skill).digest("hex"))) {
1154
+ if (unstamped(skill) === unstamped(claudeSkill()) || RELEASED_SKILL_SHA256.includes(createHash("sha256").update(skill).digest("hex"))) {
1021
1155
  rmSync(skillPath);
1022
1156
  removed.push(skillPath);
1023
1157
  // Drop the directories init created, up to the first one that holds something else.
@@ -1240,6 +1374,7 @@ const USAGE = `usage:
1240
1374
  pair history [thread-id] (list finished threads, or show one)
1241
1375
  pair clean --keep N (delete all but the N newest finished threads)
1242
1376
  pair init
1377
+ pair upgrade (install the latest pair from npm, then refresh this worktree)
1243
1378
  pair uninit (remove what pair init wrote in this worktree; keeps history)
1244
1379
  pair register | unregister (in Codex's shell: let pair_listen notify this session)
1245
1380
  pair mcp (stdio MCP server for Codex, providing pair_listen)
@@ -1254,57 +1389,67 @@ async function main(argv) {
1254
1389
  const root = worktreeRoot();
1255
1390
  const channel = channelDir(root);
1256
1391
  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.`);
1392
+ const refreshNote = REFRESHING_COMMANDS.includes(command) ? refreshInstructions(root) : undefined;
1393
+ try {
1394
+ switch (command) {
1395
+ case "send": {
1396
+ autoKeep(); // reject a bad PAIR_KEEP before touching any thread
1397
+ const type = oneOf(rest[0], MESSAGE_TYPES, "message type");
1398
+ const verdict = values.verdict === undefined ? undefined : oneOf(values.verdict, VERDICTS, "--verdict");
1399
+ const me = agent();
1400
+ const body = readBody(rest[1]);
1401
+ console.log(`sent: ${channelAccess(channel, () => send(channel, me, type, body, verdict))}`);
1402
+ return 0;
1275
1403
  }
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;
1404
+ case "wait": {
1405
+ autoKeep(); // reject a bad PAIR_KEEP before touching any thread
1406
+ const me = agent();
1407
+ const timeout = positiveNumber(values.timeout ?? String(DEFAULT_WAIT_SECONDS), "--timeout");
1408
+ if (!(await wait(channel, me, timeout))) {
1409
+ // Exit 0: a timeout is routine, and agent UIs show any non-zero exit as a failure. No "run it
1410
+ // again" here: whether to wait again differs by agent (Codex must not poll), so the prompt says.
1411
+ console.log(`pair: no message for ${me} yet.`);
1412
+ }
1413
+ return 0;
1414
+ }
1415
+ case "status":
1416
+ console.log([status(channel, root), ...(await updateNotice())].join("\n"));
1417
+ return 0;
1418
+ case "history":
1419
+ console.log(channelAccess(channel, () => history(channel, rest[0])));
1420
+ return 0;
1421
+ case "clean":
1422
+ console.log(channelAccess(channel, () => clean(channel, values.keep)));
1423
+ return 0;
1424
+ case "init":
1425
+ console.log([init(channel, root), ...(await updateNotice())].join("\n"));
1426
+ return 0;
1427
+ case "upgrade":
1428
+ return upgrade(root);
1429
+ case "uninit":
1430
+ console.log(uninit(channel, root));
1431
+ return 0;
1432
+ case "register":
1433
+ console.log(channelAccess(channel, () => register(channel, root)));
1434
+ return 0;
1435
+ case "unregister":
1436
+ console.log(channelAccess(channel, () => unregister(channel)));
1437
+ return 0;
1438
+ case "mcp":
1439
+ await serveMcp();
1440
+ return 0;
1441
+ case "ps":
1442
+ console.log(ps(values.verbose ?? false));
1443
+ return 0;
1444
+ default:
1445
+ console.error(USAGE);
1446
+ return 1;
1447
+ }
1448
+ }
1449
+ finally {
1450
+ if (refreshNote) {
1451
+ console.error(refreshNote);
1452
+ }
1308
1453
  }
1309
1454
  }
1310
1455
  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.0",
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",