pair-mode 0.3.3 → 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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -8
- package/README.md +5 -3
- package/assets/duck-appalled-banner-sprite.png +0 -0
- package/assets/duck-appalled-banner-v2-sprite.png +0 -0
- package/assets/duck-appalled-banner-v2.gif +0 -0
- package/assets/duck-appalled-sprite.png +0 -0
- package/assets/duck-appalled.gif +0 -0
- package/assets/duck-before-main.png +0 -0
- package/assets/duck.png +0 -0
- package/assets/favicon.png +0 -0
- package/dist/claude-code.js +2 -1
- package/dist/cli.js +83 -22
- package/dist/codex.js +353 -248
- package/dist/opencode.js +2 -1
- package/dist/pair-tui.js +5 -1
- package/dist/pi.js +2 -1
- package/package.json +15 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Hold every proposed edit, annotate the diff line by line, and hand the notes back to the agent.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ofrusch",
|
|
@@ -8,13 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://github.com/oFrusch/pair-mode#readme",
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"keywords": [
|
|
12
|
-
"code-review",
|
|
13
|
-
"diff",
|
|
14
|
-
"hooks",
|
|
15
|
-
"pair-programming",
|
|
16
|
-
"terminal"
|
|
17
|
-
],
|
|
11
|
+
"keywords": ["code-review", "diff", "hooks", "pair-programming", "terminal"],
|
|
18
12
|
"hooks": "./hooks/codex.json",
|
|
19
13
|
"skills": "./skills/",
|
|
20
14
|
"interface": {
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
<
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/oFrusch/pair-mode/main/assets/duck-appalled-banner-v2.gif" alt="A duck reacting to a diff full of deletions">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">pair mode</h1>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center"><b>For when your agent needs a rubber ducky.</b></p>
|
|
6
8
|
|
|
7
9
|
`pair-mode` is a tool that allows you to sit somewhere between being an approval monkey for your agent running in manual mode and just totally vibe coding in auto mode.
|
|
8
10
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/duck.png
CHANGED
|
Binary file
|
package/assets/favicon.png
CHANGED
|
Binary file
|
package/dist/claude-code.js
CHANGED
|
@@ -9169,13 +9169,14 @@ function detect(preference, adapters = {}) {
|
|
|
9169
9169
|
|
|
9170
9170
|
// src/transports/pane/pane.ts
|
|
9171
9171
|
var NAME_BYTES2 = 6;
|
|
9172
|
+
var OWNER_ONLY_FILE = 384;
|
|
9172
9173
|
function splitCommand(value) {
|
|
9173
9174
|
return value.trim().split(/\s+/).filter((part) => part !== "");
|
|
9174
9175
|
}
|
|
9175
9176
|
function tempFile(prefix, suffix, content) {
|
|
9176
9177
|
const name = `${prefix}${randomBytes3(NAME_BYTES2).toString("hex")}${suffix}`;
|
|
9177
9178
|
const path = join8(tmpdir2(), name);
|
|
9178
|
-
writeFileSync5(path, content, "utf-8");
|
|
9179
|
+
writeFileSync5(path, content, { encoding: "utf-8", mode: OWNER_ONLY_FILE, flag: "wx" });
|
|
9179
9180
|
return path;
|
|
9180
9181
|
}
|
|
9181
9182
|
function removeTreeQuietly(path) {
|
package/dist/cli.js
CHANGED
|
@@ -8579,7 +8579,14 @@ function connect(socketPath) {
|
|
|
8579
8579
|
});
|
|
8580
8580
|
}
|
|
8581
8581
|
function emptyHandlers() {
|
|
8582
|
-
return {
|
|
8582
|
+
return {
|
|
8583
|
+
review: [],
|
|
8584
|
+
cancel: [],
|
|
8585
|
+
change: [],
|
|
8586
|
+
close: [],
|
|
8587
|
+
bufferedReviews: [],
|
|
8588
|
+
bufferedCancels: []
|
|
8589
|
+
};
|
|
8583
8590
|
}
|
|
8584
8591
|
function addReviewHandler(handlers, handler) {
|
|
8585
8592
|
handlers.review.push(handler);
|
|
@@ -8592,6 +8599,7 @@ function addCancelHandler(handlers, handler) {
|
|
|
8592
8599
|
async function attach(socketPath, client, handlers, onState) {
|
|
8593
8600
|
const socket = await connect(socketPath);
|
|
8594
8601
|
const readLines = createLineReader();
|
|
8602
|
+
socket.on("error", () => socket.destroy());
|
|
8595
8603
|
socket.on("data", (chunk) => {
|
|
8596
8604
|
readLines(chunk).forEach((line2) => {
|
|
8597
8605
|
const message = decodeLine(line2);
|
|
@@ -8650,6 +8658,9 @@ async function ownerHost(options) {
|
|
|
8650
8658
|
onChange(handler) {
|
|
8651
8659
|
handlers.change.push(handler);
|
|
8652
8660
|
},
|
|
8661
|
+
// The owner's socket only closes when the server it owns closes, so a handler here would never fire.
|
|
8662
|
+
onClose() {
|
|
8663
|
+
},
|
|
8653
8664
|
async close() {
|
|
8654
8665
|
socket.destroy();
|
|
8655
8666
|
await server.close();
|
|
@@ -8663,6 +8674,11 @@ async function viewerHost(options) {
|
|
|
8663
8674
|
remote = state;
|
|
8664
8675
|
handlers.change.forEach((handler) => handler());
|
|
8665
8676
|
});
|
|
8677
|
+
socket.on("close", () => {
|
|
8678
|
+
remote = null;
|
|
8679
|
+
handlers.change.forEach((handler) => handler());
|
|
8680
|
+
handlers.close.forEach((handler) => handler());
|
|
8681
|
+
});
|
|
8666
8682
|
return {
|
|
8667
8683
|
socketPath: options.socketPath,
|
|
8668
8684
|
owns: false,
|
|
@@ -8670,10 +8686,14 @@ async function viewerHost(options) {
|
|
|
8670
8686
|
return { clients: remote?.clientCount ?? 0, waiting: remote?.waitingDepth ?? 0 };
|
|
8671
8687
|
},
|
|
8672
8688
|
refreshCounts() {
|
|
8673
|
-
socket.
|
|
8689
|
+
if (!socket.destroyed) {
|
|
8690
|
+
socket.write(encode({ type: "status" }));
|
|
8691
|
+
}
|
|
8674
8692
|
},
|
|
8675
8693
|
verdict(id, questions) {
|
|
8676
|
-
socket.
|
|
8694
|
+
if (!socket.destroyed) {
|
|
8695
|
+
socket.write(encode({ type: "verdict", id, questions }));
|
|
8696
|
+
}
|
|
8677
8697
|
},
|
|
8678
8698
|
onReview(handler) {
|
|
8679
8699
|
addReviewHandler(handlers, handler);
|
|
@@ -8684,6 +8704,9 @@ async function viewerHost(options) {
|
|
|
8684
8704
|
onChange(handler) {
|
|
8685
8705
|
handlers.change.push(handler);
|
|
8686
8706
|
},
|
|
8707
|
+
onClose(handler) {
|
|
8708
|
+
handlers.close.push(handler);
|
|
8709
|
+
},
|
|
8687
8710
|
close() {
|
|
8688
8711
|
socket.destroy();
|
|
8689
8712
|
return Promise.resolve();
|
|
@@ -9825,6 +9848,13 @@ function detect(preference, adapters = {}) {
|
|
|
9825
9848
|
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, copyFileSync } from "node:fs";
|
|
9826
9849
|
import { basename as basename5, dirname as dirname7, join as join12, sep as sep2 } from "node:path";
|
|
9827
9850
|
var HOOK_TIMEOUT_SECONDS = 1800;
|
|
9851
|
+
function hookCommand(installRoot2, file) {
|
|
9852
|
+
const path = join12(installRoot2, "dist", file);
|
|
9853
|
+
if (/^[A-Za-z0-9_./-]+$/.test(path)) {
|
|
9854
|
+
return path;
|
|
9855
|
+
}
|
|
9856
|
+
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
9857
|
+
}
|
|
9828
9858
|
function isHookEntry(value) {
|
|
9829
9859
|
if (!isRecord(value)) {
|
|
9830
9860
|
return false;
|
|
@@ -9915,7 +9945,7 @@ function hasCommand(groups, command) {
|
|
|
9915
9945
|
}
|
|
9916
9946
|
function matchesOurCommand(command) {
|
|
9917
9947
|
const suffix = sep2 + join12("dist", basename5(command));
|
|
9918
|
-
return (entry) => entry.command === command || entry.command.endsWith(suffix);
|
|
9948
|
+
return (entry) => entry.command === command || entry.command.endsWith(suffix) || entry.command.endsWith(suffix + "'");
|
|
9919
9949
|
}
|
|
9920
9950
|
function upsertHookGroup(groups, matcher, command, timeout) {
|
|
9921
9951
|
const matches = matchesOurCommand(command);
|
|
@@ -9984,7 +10014,7 @@ function claudeCodeSettingsPath(homeDir) {
|
|
|
9984
10014
|
}
|
|
9985
10015
|
function registerClaudeCode(homeDir, installRoot2) {
|
|
9986
10016
|
const path = claudeCodeSettingsPath(homeDir);
|
|
9987
|
-
const command =
|
|
10017
|
+
const command = hookCommand(installRoot2, "claude-code.js");
|
|
9988
10018
|
return registerPreToolUseHook(path, "Write|Edit|MultiEdit", command, HOOK_TIMEOUT_SECONDS);
|
|
9989
10019
|
}
|
|
9990
10020
|
function codexHooksPath(homeDir) {
|
|
@@ -9992,7 +10022,7 @@ function codexHooksPath(homeDir) {
|
|
|
9992
10022
|
}
|
|
9993
10023
|
function registerCodex(homeDir, installRoot2) {
|
|
9994
10024
|
const path = codexHooksPath(homeDir);
|
|
9995
|
-
const command =
|
|
10025
|
+
const command = hookCommand(installRoot2, "codex.js");
|
|
9996
10026
|
return registerPreToolUseHook(path, "apply_patch|Edit|Write", command, HOOK_TIMEOUT_SECONDS);
|
|
9997
10027
|
}
|
|
9998
10028
|
function findMultiEditMatchers(homeDir) {
|
|
@@ -10231,20 +10261,22 @@ function checkCommandOnPath(resolves) {
|
|
|
10231
10261
|
};
|
|
10232
10262
|
}
|
|
10233
10263
|
function checkClis(home, root) {
|
|
10234
|
-
const claudeCommand =
|
|
10235
|
-
const codexCommand =
|
|
10264
|
+
const claudeCommand = hookCommand(root, "claude-code.js");
|
|
10265
|
+
const codexCommand = hookCommand(root, "codex.js");
|
|
10266
|
+
const claudeTarget = join14(root, "dist", "claude-code.js");
|
|
10267
|
+
const codexTarget = join14(root, "dist", "codex.js");
|
|
10236
10268
|
const opencodeTarget = join14(root, "dist", "opencode.js");
|
|
10237
10269
|
const piTarget = join14(root, "dist", "pi.js");
|
|
10238
10270
|
const specs = [
|
|
10239
10271
|
{
|
|
10240
10272
|
cli: "claude-code",
|
|
10241
10273
|
registered: isPreToolUseRegistered(claudeCodeSettingsPath(home), claudeCommand),
|
|
10242
|
-
targetExists: existsSync10(
|
|
10274
|
+
targetExists: existsSync10(claudeTarget)
|
|
10243
10275
|
},
|
|
10244
10276
|
{
|
|
10245
10277
|
cli: "codex",
|
|
10246
10278
|
registered: isPreToolUseRegistered(codexHooksPath(home), codexCommand),
|
|
10247
|
-
targetExists: existsSync10(
|
|
10279
|
+
targetExists: existsSync10(codexTarget)
|
|
10248
10280
|
},
|
|
10249
10281
|
{
|
|
10250
10282
|
cli: "opencode",
|
|
@@ -12585,6 +12617,7 @@ function splitInput(chunk) {
|
|
|
12585
12617
|
|
|
12586
12618
|
// src/tui/notes/notes.ts
|
|
12587
12619
|
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
12620
|
+
var OWNER_ONLY_FILE2 = 384;
|
|
12588
12621
|
function rangeOf(selection) {
|
|
12589
12622
|
const reversed = selection.anchorRow > selection.headRow || selection.anchorRow === selection.headRow && selection.anchorColumn > selection.headColumn;
|
|
12590
12623
|
if (!reversed) {
|
|
@@ -12655,7 +12688,10 @@ function toQuestions(notes) {
|
|
|
12655
12688
|
}
|
|
12656
12689
|
function writeResult(path, notes) {
|
|
12657
12690
|
try {
|
|
12658
|
-
writeFileSync7(path, JSON.stringify({ questions: toQuestions(notes) }, null, 2),
|
|
12691
|
+
writeFileSync7(path, JSON.stringify({ questions: toQuestions(notes) }, null, 2), {
|
|
12692
|
+
encoding: "utf-8",
|
|
12693
|
+
mode: OWNER_ONLY_FILE2
|
|
12694
|
+
});
|
|
12659
12695
|
} catch {
|
|
12660
12696
|
return;
|
|
12661
12697
|
}
|
|
@@ -13231,15 +13267,29 @@ async function runWatch(options, config) {
|
|
|
13231
13267
|
nudge();
|
|
13232
13268
|
});
|
|
13233
13269
|
host.onCancel((id) => {
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13270
|
+
const queuedIndex = pending.findIndex((review) => review.id === id);
|
|
13271
|
+
if (queuedIndex !== -1) {
|
|
13272
|
+
pending.splice(queuedIndex, 1);
|
|
13273
|
+
nudge();
|
|
13274
|
+
return;
|
|
13275
|
+
}
|
|
13276
|
+
if (aborts.has(id)) {
|
|
13277
|
+
cancelled.add(id);
|
|
13278
|
+
aborts.get(id)?.abort();
|
|
13279
|
+
nudge();
|
|
13280
|
+
}
|
|
13237
13281
|
});
|
|
13238
13282
|
host.onChange(() => {
|
|
13239
13283
|
if (!busy && !quitting) {
|
|
13240
13284
|
paintIdle(io, status(), truecolor);
|
|
13241
13285
|
}
|
|
13242
13286
|
});
|
|
13287
|
+
host.onClose(() => {
|
|
13288
|
+
quitting = true;
|
|
13289
|
+
aborts.forEach((abort) => abort.abort());
|
|
13290
|
+
errors = [...errors, new Error("the session owner exited")];
|
|
13291
|
+
nudge();
|
|
13292
|
+
});
|
|
13243
13293
|
while (!quitting) {
|
|
13244
13294
|
const review = pending.shift();
|
|
13245
13295
|
if (review === void 0) {
|
|
@@ -14437,12 +14487,17 @@ function post(payload) {
|
|
|
14437
14487
|
.then((response) => {
|
|
14438
14488
|
// A refused verdict means another client already answered, so the notes typed here can never land.
|
|
14439
14489
|
if (!response.ok) {
|
|
14440
|
-
|
|
14490
|
+
if (review !== null && review.id === payload.id) {
|
|
14491
|
+
clearReview();
|
|
14492
|
+
}
|
|
14441
14493
|
warn("this review was already answered elsewhere - your notes were not sent");
|
|
14442
14494
|
return;
|
|
14443
14495
|
}
|
|
14444
14496
|
|
|
14445
|
-
|
|
14497
|
+
// Only clear if the server answered this review, not one that arrived via SSE meanwhile.
|
|
14498
|
+
if (review !== null && review.id === payload.id) {
|
|
14499
|
+
clearReview();
|
|
14500
|
+
}
|
|
14446
14501
|
})
|
|
14447
14502
|
.catch(() => {
|
|
14448
14503
|
sendButton.disabled = notes.length === 0;
|
|
@@ -14457,10 +14512,16 @@ approveButton.addEventListener("click", () => post({ id: review.id, notes: [] })
|
|
|
14457
14512
|
const events = new EventSource("/r/" + token + "/events");
|
|
14458
14513
|
|
|
14459
14514
|
events.addEventListener("review", (event) => {
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14515
|
+
const incoming = JSON.parse(event.data);
|
|
14516
|
+
|
|
14517
|
+
// Only reset if it is a different review.
|
|
14518
|
+
if (review === null || review.id !== incoming.id) {
|
|
14519
|
+
notes = [];
|
|
14520
|
+
expanded = new Set();
|
|
14521
|
+
hidePopup();
|
|
14522
|
+
}
|
|
14523
|
+
|
|
14524
|
+
review = incoming;
|
|
14464
14525
|
render();
|
|
14465
14526
|
});
|
|
14466
14527
|
|
|
@@ -14811,12 +14872,12 @@ async function toWebReview(review, config) {
|
|
|
14811
14872
|
|
|
14812
14873
|
// src/web/watch.ts
|
|
14813
14874
|
var OWNER_ONLY_DIR3 = 448;
|
|
14814
|
-
var
|
|
14875
|
+
var OWNER_ONLY_FILE3 = 384;
|
|
14815
14876
|
function publishUrl(path, url) {
|
|
14816
14877
|
mkdirSync7(dirname8(path), { recursive: true, mode: OWNER_ONLY_DIR3 });
|
|
14817
14878
|
writeFileSync8(path, JSON.stringify({ url, pid: process.pid }) + "\n", {
|
|
14818
14879
|
encoding: "utf-8",
|
|
14819
|
-
mode:
|
|
14880
|
+
mode: OWNER_ONLY_FILE3
|
|
14820
14881
|
});
|
|
14821
14882
|
}
|
|
14822
14883
|
function linkPid(path) {
|