claude-codex-pair 0.2.0 → 0.3.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/README.md CHANGED
@@ -167,6 +167,7 @@ pair init
167
167
  pair uninit # undo pair init in this worktree (keeps history; see "Uninstall")
168
168
  pair register | unregister # run by Codex in its shell: let pair_listen notify this session
169
169
  pair mcp # stdio MCP server for Codex (see "Let pair wake Codex")
170
+ pair ps # list running pair mcp and pair wait processes, where they run, and what started them
170
171
  ```
171
172
 
172
173
  `pair status` shows the channel, worktree root, installed package version, and each lane's turn.
@@ -175,7 +176,7 @@ It also checks the generated instructions on disk:
175
176
  ```
176
177
  channel: /Users/you/.pair/your-repo-12345678
177
178
  worktree: /Users/you/your-repo
178
- version: claude-codex-pair 0.1.0
179
+ version: claude-codex-pair 0.2.0
179
180
  claude-to-codex: idle
180
181
  codex-to-claude: idle
181
182
  instructions on disk (does not verify what running agents loaded):
package/dist/pair.js CHANGED
@@ -1059,6 +1059,81 @@ function uninit(channel, root) {
1059
1059
  const summary = removed.length > 0 ? removed.map((item) => `removed: ${item}`) : [`nothing from pair init found in ${root}`];
1060
1060
  return [...summary, ...notes].join("\n");
1061
1061
  }
1062
+ const PS_LINE = /^\s*(\d+)\s+(\d+)\s+(\w{3} \w{3}\s+\d+ [\d:]+ \d{4})\s+(.*)$/; // lstart is always 5 fields
1063
+ // node itself runs the script, so a wrapper whose command line mentions it (`zsh -c "… pair.js wait"`) doesn't match
1064
+ const PAIR_PROCESS = /^(?:\S*\/)?node(?: -\S+)* \S*\/(?:bin\/pair|dist\/pair\.js) ((?:mcp|wait)\b.*)$/;
1065
+ const SHELL = /^(?:\S*\/)?(?:zsh|bash|sh|dash|fish)(?: |$)/;
1066
+ function listProcesses() {
1067
+ let out;
1068
+ try {
1069
+ out = execFileSync("ps", ["-axo", "pid=,ppid=,lstart=,command="], { encoding: "utf8", env: { ...process.env, LC_ALL: "C" } });
1070
+ }
1071
+ catch (error) {
1072
+ throw new PairError(`can't list processes (${error instanceof Error ? error.message : String(error)}); agent sandboxes often block ps, so run pair ps in your own terminal`);
1073
+ }
1074
+ return out.split("\n").flatMap((line) => {
1075
+ const match = PS_LINE.exec(line);
1076
+ return match ? [{ pid: Number(match[1]), ppid: Number(match[2]), started: match[3], command: match[4] }] : [];
1077
+ });
1078
+ }
1079
+ /** Working directories by pid: /proc on Linux, lsof elsewhere. Unknown ones are left out. */
1080
+ function workingDirs(pids) {
1081
+ const dirs = new Map();
1082
+ if (existsSync("/proc/self/cwd")) {
1083
+ for (const pid of pids) {
1084
+ try {
1085
+ dirs.set(pid, realpathSync(`/proc/${pid}/cwd`));
1086
+ }
1087
+ catch {
1088
+ // gone, or not ours
1089
+ }
1090
+ }
1091
+ return dirs;
1092
+ }
1093
+ let out = "";
1094
+ try {
1095
+ out = execFileSync("lsof", ["-a", "-d", "cwd", "-Fpn", "-p", pids.join(",")], { encoding: "utf8" });
1096
+ }
1097
+ catch (error) {
1098
+ // lsof exits 1 when one of the pids is gone, but still prints the rest
1099
+ out = error instanceof Error && "stdout" in error ? String(error.stdout) : "";
1100
+ }
1101
+ let pid = 0;
1102
+ for (const line of out.split("\n")) {
1103
+ if (line.startsWith("p")) {
1104
+ pid = Number(line.slice(1));
1105
+ }
1106
+ else if (line.startsWith("n")) {
1107
+ dirs.set(pid, line.slice(1));
1108
+ }
1109
+ }
1110
+ return dirs;
1111
+ }
1112
+ /** Every running `pair mcp` and `pair wait` on this machine, with what started it. */
1113
+ function ps() {
1114
+ const all = listProcesses();
1115
+ const byPid = new Map(all.map((proc) => [proc.pid, proc]));
1116
+ const ours = all.flatMap((proc) => {
1117
+ const match = PAIR_PROCESS.exec(proc.command);
1118
+ return match ? [{ ...proc, args: match[1] }] : [];
1119
+ });
1120
+ if (ours.length === 0) {
1121
+ return "no pair mcp or pair wait is running";
1122
+ }
1123
+ const dirs = workingDirs(ours.map((proc) => proc.pid));
1124
+ return ours
1125
+ .flatMap((proc) => {
1126
+ let host = byPid.get(proc.ppid);
1127
+ while (host && SHELL.test(host.command)) {
1128
+ host = byPid.get(host.ppid); // an agent runs pair wait through a shell; show the agent
1129
+ }
1130
+ return [
1131
+ `pair ${proc.args} pid ${proc.pid} since ${proc.started} in ${dirs.get(proc.pid) ?? "?"}`,
1132
+ ` started by ${host ? `${host.pid}: ${host.command.slice(0, 160)}` : `${proc.ppid} (gone)`}`,
1133
+ ];
1134
+ })
1135
+ .join("\n");
1136
+ }
1062
1137
  function readBody(source) {
1063
1138
  if (!source) {
1064
1139
  throw new PairError("send needs a message file, or - for stdin");
@@ -1087,7 +1162,8 @@ const USAGE = `usage:
1087
1162
  pair init
1088
1163
  pair uninit (remove what pair init wrote in this worktree; keeps history)
1089
1164
  pair register | unregister (in Codex's shell: let pair_listen notify this session)
1090
- pair mcp (stdio MCP server for Codex, providing pair_listen)`;
1165
+ pair mcp (stdio MCP server for Codex, providing pair_listen)
1166
+ pair ps (list running pair mcp and pair wait processes)`;
1091
1167
  async function main(argv) {
1092
1168
  const { values, positionals } = parseArgs({
1093
1169
  args: argv,
@@ -1143,6 +1219,9 @@ async function main(argv) {
1143
1219
  case "mcp":
1144
1220
  await serveMcp();
1145
1221
  return 0;
1222
+ case "ps":
1223
+ console.log(ps());
1224
+ return 0;
1146
1225
  default:
1147
1226
  console.error(USAGE);
1148
1227
  return 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-pair",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",