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 +13 -1
- package/README.md +20 -3
- package/dist/pair.js +210 -65
- package/package.json +1 -1
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
|
|
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 `
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
965
|
-
|
|
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
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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) => {
|