pair-mode 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/.claude-plugin/plugin.json +12 -0
- package/.codex-plugin/plugin.json +29 -0
- package/README.md +24 -0
- package/assets/duck.png +0 -0
- package/assets/favicon.png +0 -0
- package/commands/pair.md +18 -0
- package/dist/cli.js +42 -9
- package/hooks/codex.json +17 -0
- package/hooks/hooks.json +16 -0
- package/package.json +6 -1
- package/skills/pair/SKILL.md +20 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pair-mode",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Hold every proposed edit, annotate the diff line by line, and hand the notes back to the agent.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "ofrusch",
|
|
7
|
+
"email": "owenc343@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/oFrusch/pair-mode#readme",
|
|
10
|
+
"repository": "https://github.com/oFrusch/pair-mode.git",
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pair-mode",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Hold every proposed edit, annotate the diff line by line, and hand the notes back to the agent.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "ofrusch",
|
|
7
|
+
"url": "https://github.com/oFrusch"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/oFrusch/pair-mode#readme",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"code-review",
|
|
13
|
+
"diff",
|
|
14
|
+
"hooks",
|
|
15
|
+
"pair-programming",
|
|
16
|
+
"terminal"
|
|
17
|
+
],
|
|
18
|
+
"hooks": "./hooks/codex.json",
|
|
19
|
+
"skills": "./skills/",
|
|
20
|
+
"interface": {
|
|
21
|
+
"displayName": "Pair Mode",
|
|
22
|
+
"shortDescription": "Review each proposed edit before it lands",
|
|
23
|
+
"longDescription": "Pair mode intercepts every proposed edit. It renders the diff in a terminal pane or a browser page, collects notes anchored to line numbers, then holds the edit and returns those notes to the agent. A review you close with no notes applies the edit as proposed.",
|
|
24
|
+
"developerName": "ofrusch",
|
|
25
|
+
"category": "Engineering",
|
|
26
|
+
"websiteURL": "https://github.com/oFrusch/pair-mode",
|
|
27
|
+
"logo": "./assets/duck.png"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/oFrusch/pair-mode/main/docs/images/duck.png" alt="The pair-mode duck" width="120" align="right">
|
|
2
|
+
|
|
1
3
|
# pair mode
|
|
2
4
|
|
|
3
5
|
#### For when your agent needs a rubber ducky.
|
|
@@ -19,6 +21,28 @@ npm install -g pair-mode
|
|
|
19
21
|
pair-mode setup
|
|
20
22
|
```
|
|
21
23
|
|
|
24
|
+
### Or install it as a plugin
|
|
25
|
+
|
|
26
|
+
A plugin resolves its own path at runtime, so it needs no global install and no setup step.
|
|
27
|
+
|
|
28
|
+
Claude Code:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/plugin marketplace add oFrusch/pair-mode
|
|
32
|
+
/plugin install pair-mode
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Codex:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
codex plugin marketplace add oFrusch/pair-mode
|
|
39
|
+
codex plugin add pair-mode@pair-mode
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The plugin registers the hook and the `/pair` command for that one CLI. Run
|
|
43
|
+
`npm install -g pair-mode` as well if you want the `pair-mode` command on your PATH, which
|
|
44
|
+
`pair-mode watch` and `pair-mode config` need.
|
|
45
|
+
|
|
22
46
|
Install pair-mode globally before you run setup. Setup writes the install path into each CLI's config as an absolute path. `npx pair-mode setup` runs from a package cache that npm later prunes, which would leave every hook pointing at a deleted file. Setup detects that case and stops.
|
|
23
47
|
|
|
24
48
|
The setup command detects the CLIs and multiplexers on your machine, registers the
|
package/assets/duck.png
ADDED
|
Binary file
|
|
Binary file
|
package/commands/pair.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Toggle pair mode. Every proposed edit opens in the pair review pane for line annotation.
|
|
3
|
+
allowed-tools: Bash(node:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" $ARGUMENTS` and report the resulting state in one line.
|
|
7
|
+
|
|
8
|
+
With no action, run it with `status`.
|
|
9
|
+
|
|
10
|
+
Pair mode intercepts your Write, Edit, and MultiEdit calls. The user reads the proposed diff in the
|
|
11
|
+
pair review pane, selects lines, and attaches notes. Pair mode then holds the edit and
|
|
12
|
+
returns those notes as questions anchored to line numbers. Answer every question. Do not
|
|
13
|
+
re-attempt the edit until the user asks for it. An edit the user closes with no notes
|
|
14
|
+
applies as proposed.
|
|
15
|
+
|
|
16
|
+
Pair mode keys the flag by the real directory path, and the hook walks up from the edited
|
|
17
|
+
file. A flag on a parent directory therefore covers every repo beneath it. If pair mode
|
|
18
|
+
stays on after an `off`, check each parent with `status <dir>`.
|
package/dist/cli.js
CHANGED
|
@@ -7361,7 +7361,7 @@ var require_dist = __commonJS({
|
|
|
7361
7361
|
});
|
|
7362
7362
|
|
|
7363
7363
|
// src/cli/index.ts
|
|
7364
|
-
import { readFileSync as
|
|
7364
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
7365
7365
|
import { join as join13, resolve as resolve2 } from "node:path";
|
|
7366
7366
|
|
|
7367
7367
|
// src/cli/setup/setup.ts
|
|
@@ -12156,6 +12156,7 @@ function runTui(options, io, abort) {
|
|
|
12156
12156
|
var TITLE = "pair-mode watch";
|
|
12157
12157
|
var HINT = "waiting for an edit \xB7 q quits";
|
|
12158
12158
|
var LABEL_WIDTH = 10;
|
|
12159
|
+
var DUCK = [" __", " <(o )___", " ( ._> /", " `---'"];
|
|
12159
12160
|
function line(label, value, truecolor) {
|
|
12160
12161
|
return fg(theme.fold, truecolor) + label.padEnd(LABEL_WIDTH) + RESET + value;
|
|
12161
12162
|
}
|
|
@@ -12167,9 +12168,12 @@ function queueText(waiting) {
|
|
|
12167
12168
|
}
|
|
12168
12169
|
function renderIdle(status, width, truecolor) {
|
|
12169
12170
|
const heading = bg(theme.chrome, truecolor) + fg(theme.statusText, truecolor) + TITLE.padEnd(width).slice(0, width) + RESET;
|
|
12171
|
+
const duck = DUCK.map((row) => fg(theme.chrome, truecolor) + row + RESET);
|
|
12170
12172
|
return [
|
|
12171
12173
|
heading,
|
|
12172
12174
|
"",
|
|
12175
|
+
...duck,
|
|
12176
|
+
"",
|
|
12173
12177
|
line("session", status.directory, truecolor),
|
|
12174
12178
|
line("socket", status.socketPath, truecolor),
|
|
12175
12179
|
line("clients", plural(status.clients, "client"), truecolor),
|
|
@@ -12597,6 +12601,8 @@ function runConfig(args, path) {
|
|
|
12597
12601
|
// src/web/server.ts
|
|
12598
12602
|
import { createServer as createServer2 } from "node:http";
|
|
12599
12603
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
12604
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
12605
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
12600
12606
|
|
|
12601
12607
|
// src/web/client/bundle.ts
|
|
12602
12608
|
var CLIENT_BUNDLE = `"use strict";
|
|
@@ -12946,7 +12952,8 @@ header {
|
|
|
12946
12952
|
}
|
|
12947
12953
|
|
|
12948
12954
|
header .seg { padding: 6px 13px; white-space: nowrap; }
|
|
12949
|
-
header .mode {
|
|
12955
|
+
header .mode { display: flex; align-items: center; gap: 7px; color: var(--amber); font-weight: 700; letter-spacing: 0.1em; }
|
|
12956
|
+
header .mark { border-radius: 4px; }
|
|
12950
12957
|
header .path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
|
|
12951
12958
|
header .plus { color: var(--add-bar); }
|
|
12952
12959
|
header .minus { color: var(--del-bar); }
|
|
@@ -13170,7 +13177,15 @@ textarea {
|
|
|
13170
13177
|
|
|
13171
13178
|
.hint { color: var(--dim); font-size: 11px; }
|
|
13172
13179
|
|
|
13173
|
-
#idle { padding:
|
|
13180
|
+
#idle { padding: 72px 24px; color: var(--dim); text-align: center; }
|
|
13181
|
+
#idle img { border-radius: 14px; opacity: 0.75; animation: bob 3.2s ease-in-out infinite; }
|
|
13182
|
+
#idle p { margin: 16px 0 0; }
|
|
13183
|
+
|
|
13184
|
+
@keyframes bob { 50% { transform: translateY(-6px); } }
|
|
13185
|
+
|
|
13186
|
+
@media (prefers-reduced-motion: reduce) {
|
|
13187
|
+
#idle img { animation: none; }
|
|
13188
|
+
}
|
|
13174
13189
|
|
|
13175
13190
|
@media (max-width: 900px) {
|
|
13176
13191
|
main { flex-wrap: wrap; }
|
|
@@ -13181,6 +13196,7 @@ textarea {
|
|
|
13181
13196
|
`;
|
|
13182
13197
|
var WIRING = String.raw`
|
|
13183
13198
|
const C = PairClient;
|
|
13199
|
+
const assets = document.body.dataset.assets ?? "";
|
|
13184
13200
|
const token = location.pathname.split("/")[2];
|
|
13185
13201
|
const main = document.querySelector("main");
|
|
13186
13202
|
const diff = document.getElementById("diff");
|
|
@@ -13291,7 +13307,8 @@ function renderMargin() {
|
|
|
13291
13307
|
|
|
13292
13308
|
function render() {
|
|
13293
13309
|
if (review === null) {
|
|
13294
|
-
diff.innerHTML =
|
|
13310
|
+
diff.innerHTML =
|
|
13311
|
+
'<div id="idle"><img src="' + assets + '/duck.png" alt="" width="88" height="88"><p>waiting for an edit</p></div>';
|
|
13295
13312
|
pathLabel.textContent = "no review open";
|
|
13296
13313
|
plusLabel.textContent = "";
|
|
13297
13314
|
minusLabel.textContent = "";
|
|
@@ -13563,18 +13580,19 @@ ${WIRING}`);
|
|
|
13563
13580
|
function pressed(layout, wanted) {
|
|
13564
13581
|
return layout === wanted ? "true" : "false";
|
|
13565
13582
|
}
|
|
13566
|
-
function renderPage(layout = "split") {
|
|
13583
|
+
function renderPage(layout = "split", assetBase = "") {
|
|
13567
13584
|
return `<!doctype html>
|
|
13568
13585
|
<html lang="en">
|
|
13569
13586
|
<head>
|
|
13570
13587
|
<meta charset="utf-8">
|
|
13571
13588
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13589
|
+
<link rel="icon" type="image/png" href="${assetBase}/favicon.png">
|
|
13572
13590
|
<title>pair mode</title>
|
|
13573
13591
|
<style>${STYLE}</style>
|
|
13574
13592
|
</head>
|
|
13575
|
-
<body data-layout="${layout}">
|
|
13593
|
+
<body data-layout="${layout}" data-assets="${assetBase}">
|
|
13576
13594
|
<header>
|
|
13577
|
-
<span class="seg mode">
|
|
13595
|
+
<span class="seg mode"><img class="mark" src="${assetBase}/duck.png" alt="" width="18" height="18">pair</span>
|
|
13578
13596
|
<span class="seg path" id="path">no review open</span>
|
|
13579
13597
|
<span class="seg plus" id="plus"></span>
|
|
13580
13598
|
<span class="seg minus" id="minus"></span>
|
|
@@ -13647,6 +13665,15 @@ function webNotesToQuestions(review, notes) {
|
|
|
13647
13665
|
var HOST = "127.0.0.1";
|
|
13648
13666
|
var TOKEN_BYTES = 16;
|
|
13649
13667
|
var MAX_BODY_BYTES = 1e6;
|
|
13668
|
+
function readAsset(name) {
|
|
13669
|
+
const bundled = fileURLToPath4(new URL(`../assets/${name}`, import.meta.url));
|
|
13670
|
+
const source = fileURLToPath4(new URL(`../../assets/${name}`, import.meta.url));
|
|
13671
|
+
return readFileSync7(existsSync10(bundled) ? bundled : source);
|
|
13672
|
+
}
|
|
13673
|
+
var IMAGES = {
|
|
13674
|
+
"favicon.png": readAsset("favicon.png"),
|
|
13675
|
+
"duck.png": readAsset("duck.png")
|
|
13676
|
+
};
|
|
13650
13677
|
var NOT_FOUND = 404;
|
|
13651
13678
|
var OK = 200;
|
|
13652
13679
|
var BAD_REQUEST = 400;
|
|
@@ -13778,7 +13805,13 @@ data: ${data}
|
|
|
13778
13805
|
const url = request.url ?? "";
|
|
13779
13806
|
if (url === base && request.method === "GET") {
|
|
13780
13807
|
response.writeHead(OK, { "content-type": "text/html; charset=utf-8" });
|
|
13781
|
-
response.end(renderPage(options.layout));
|
|
13808
|
+
response.end(renderPage(options.layout, base));
|
|
13809
|
+
return;
|
|
13810
|
+
}
|
|
13811
|
+
const image = url.startsWith(`${base}/`) ? IMAGES[url.slice(base.length + 1)] : void 0;
|
|
13812
|
+
if (image !== void 0 && request.method === "GET") {
|
|
13813
|
+
response.writeHead(OK, { "content-type": "image/png" });
|
|
13814
|
+
response.end(image);
|
|
13782
13815
|
return;
|
|
13783
13816
|
}
|
|
13784
13817
|
if (url === `${base}/events` && request.method === "GET") {
|
|
@@ -13947,7 +13980,7 @@ Commands:
|
|
|
13947
13980
|
function readVersion() {
|
|
13948
13981
|
const pkgPath = join13(installRoot(), "package.json");
|
|
13949
13982
|
try {
|
|
13950
|
-
const raw = JSON.parse(
|
|
13983
|
+
const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
13951
13984
|
if (isRecord(raw) && typeof raw["version"] === "string") {
|
|
13952
13985
|
return raw["version"];
|
|
13953
13986
|
}
|
package/hooks/codex.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Pair mode holds a proposed edit so the user can annotate the diff line by line.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "apply_patch|Edit|Write",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node \"${PLUGIN_ROOT}/dist/codex.js\"",
|
|
11
|
+
"timeout": 600
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
package/hooks/hooks.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Annotate a coding agent's proposed edits line by line, in your terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
33
|
"assets",
|
|
34
|
+
".claude-plugin/plugin.json",
|
|
35
|
+
".codex-plugin/plugin.json",
|
|
36
|
+
"hooks",
|
|
37
|
+
"commands",
|
|
38
|
+
"skills",
|
|
34
39
|
"README.md"
|
|
35
40
|
],
|
|
36
41
|
"type": "module",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pair
|
|
3
|
+
description: Toggle pair mode. Every proposed edit opens in the pair review pane for line annotation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read the action from the user's message, then run `pair-mode <action>` and report the resulting state in one line.
|
|
7
|
+
|
|
8
|
+
With no action, run `pair-mode status`.
|
|
9
|
+
|
|
10
|
+
Pair mode intercepts your apply_patch, Write, and Edit calls. The user reads the proposed diff in the
|
|
11
|
+
pair review pane, selects lines, and attaches notes. Pair mode then holds the edit and
|
|
12
|
+
returns those notes as questions anchored to line numbers. Answer every question. Do not
|
|
13
|
+
re-attempt the edit until the user asks for it. An edit the user closes with no notes
|
|
14
|
+
applies as proposed.
|
|
15
|
+
|
|
16
|
+
Pair mode keys the flag by the real directory path, and the hook walks up from the edited
|
|
17
|
+
file. A flag on a parent directory therefore covers every repo beneath it. If pair mode
|
|
18
|
+
stays on after an `off`, check each parent with `pair-mode status <dir>`.
|
|
19
|
+
|
|
20
|
+
The `pair-mode` command must resolve on PATH. Install it with `npm install -g pair-mode`.
|