@jnsahaj/pi-lumen-diff 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.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/index.ts +125 -0
  3. package/package.json +25 -0
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @jnsahaj/pi-lumen-diff
2
+
3
+ A [Pi coding agent](https://github.com/earendil-works/pi) extension that
4
+ hooks lumen into the review loop:
5
+
6
+ ```
7
+ agent finishes turn → lumen opens on the diff → annotate inline
8
+ → press `s` → annotations injected as next user message → agent fixes them
9
+ ```
10
+
11
+ The agent never invokes lumen. Pi runs it from the `agent_end` event,
12
+ suspends its own TUI while lumen owns the terminal, then injects the
13
+ annotations via `pi.sendUserMessage()` so they appear as if the user
14
+ typed them.
15
+
16
+ ## Install
17
+
18
+ Requires lumen ≥ 2.25 on `$PATH` (or set `LUMEN_BIN`).
19
+
20
+ **From source (recommended while iterating):**
21
+
22
+ ```bash
23
+ git clone https://github.com/jnsahaj/lumen.git
24
+ mkdir -p ~/.pi/agent/extensions
25
+ ln -s "$(pwd)/lumen/integrations/pi/extension" ~/.pi/agent/extensions/lumen
26
+ ```
27
+
28
+ **Once-off try without installing:**
29
+
30
+ ```bash
31
+ pi -e $(pwd)/lumen/integrations/pi/extension/index.ts
32
+ ```
33
+
34
+ **From npm (post-publish):**
35
+
36
+ ```bash
37
+ pi install npm:@jnsahaj/pi-lumen-diff
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ Run `/lumen-diff` whenever you want to review the working-tree diff.
43
+ Annotate, press `s` → `Enter`. The agent gets your feedback as its next
44
+ prompt.
45
+
46
+ ```
47
+ /lumen-diff
48
+ /lumen-diff HEAD~1
49
+ /lumen-diff main..-
50
+ /lumen-diff --file src/auth.rs
51
+ ```
52
+
53
+ To also pop lumen up automatically after every agent turn, set
54
+ `LUMEN_AUTO_REVIEW=1`.
55
+
56
+ ## Config
57
+
58
+ | Env var | Default | Meaning |
59
+ |----------------------|----------|------------------------------------------------------------------|
60
+ | `LUMEN_BIN` | `lumen` | Path to the lumen binary (use absolute if not on `$PATH`). |
61
+ | `LUMEN_AUTO_REVIEW` | `0` | Set to `1` to pop lumen up after every `agent_end`. |
package/index.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * lumen extension for the Pi coding agent.
3
+ *
4
+ * Runs on `/lumen-diff`, and optionally after every agent turn. When the
5
+ * user submits annotations, injects them as the next user message so the
6
+ * agent reacts as if the user had typed them. The agent never invokes
7
+ * lumen — Pi does.
8
+ *
9
+ * Enable the auto-trigger by setting LUMEN_AUTO_REVIEW=1 in the env.
10
+ */
11
+
12
+ import { spawnSync } from "node:child_process";
13
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
+
15
+ const LUMEN_BIN = process.env.LUMEN_BIN ?? "lumen";
16
+ const AUTO_REVIEW = process.env.LUMEN_AUTO_REVIEW === "1";
17
+
18
+ function hasUncommittedChanges(cwd: string): boolean {
19
+ const result = spawnSync("git", ["diff", "--quiet", "HEAD", "--"], {
20
+ cwd,
21
+ stdio: "ignore",
22
+ });
23
+ // `git diff --quiet` exits 1 when there are changes, 0 when clean.
24
+ return result.status === 1;
25
+ }
26
+
27
+ type LumenRun = { status: number | null; output: string; error: string | null };
28
+
29
+ function runLumenReview(cwd: string, args: string[]): LumenRun {
30
+ const result = spawnSync(LUMEN_BIN, ["diff", ...args], {
31
+ cwd,
32
+ // stdin: inherit so keystrokes flow; stdout: pipe so we capture
33
+ // annotations; stderr: inherit so any lumen errors are visible.
34
+ // lumen auto-routes its TUI to /dev/tty when stdout is captured.
35
+ stdio: ["inherit", "pipe", "inherit"],
36
+ env: process.env,
37
+ encoding: "utf8",
38
+ });
39
+
40
+ if (result.error) {
41
+ return { status: null, output: "", error: result.error.message };
42
+ }
43
+ return { status: result.status, output: (result.stdout ?? "").trim(), error: null };
44
+ }
45
+
46
+ async function reviewAndInject(
47
+ pi: ExtensionAPI,
48
+ ctx: ExtensionContext,
49
+ args: string[],
50
+ { silentOnClean }: { silentOnClean: boolean },
51
+ ): Promise<void> {
52
+ if (!ctx.hasUI) return;
53
+
54
+ if (!hasUncommittedChanges(ctx.cwd)) {
55
+ if (!silentOnClean) {
56
+ ctx.ui.notify("lumen: no uncommitted changes to review", "info");
57
+ }
58
+ return;
59
+ }
60
+
61
+ // Suspend Pi's TUI, hand the terminal to lumen, restart Pi's TUI.
62
+ // The `ctx.ui.custom` pattern is the same one Pi's own interactive-shell
63
+ // example uses for vim/htop/etc.
64
+ const result = await ctx.ui.custom<LumenRun>((tui, _theme, _kb, done) => {
65
+ tui.stop();
66
+ process.stdout.write("\x1b[2J\x1b[H");
67
+
68
+ const run = runLumenReview(ctx.cwd, args);
69
+
70
+ tui.start();
71
+ tui.requestRender(true);
72
+ done(run);
73
+
74
+ return { render: () => [], invalidate: () => {} };
75
+ });
76
+
77
+ if (result.error) {
78
+ ctx.ui.notify(`lumen: ${result.error}`, "error");
79
+ return;
80
+ }
81
+
82
+ if (result.status !== 0) {
83
+ ctx.ui.notify(`lumen exited with status ${result.status}`, "warning");
84
+ return;
85
+ }
86
+
87
+ if (!result.output) {
88
+ // User pressed `q` instead of `s` — they looked but didn't send feedback.
89
+ return;
90
+ }
91
+
92
+ // If the agent is still streaming (e.g. another turn kicked off while
93
+ // lumen was open), queue our feedback as a follow-up instead of throwing.
94
+ await pi.sendUserMessage(result.output, { deliverAs: "followUp" });
95
+ }
96
+
97
+ export default function (pi: ExtensionAPI) {
98
+ if (AUTO_REVIEW) {
99
+ pi.on("agent_end", async (_event, ctx) => {
100
+ try {
101
+ await reviewAndInject(pi, ctx, [], { silentOnClean: true });
102
+ } catch (err) {
103
+ ctx.ui.notify(
104
+ `lumen review failed: ${err instanceof Error ? err.message : String(err)}`,
105
+ "error",
106
+ );
107
+ }
108
+ });
109
+ }
110
+
111
+ pi.registerCommand("lumen-diff", {
112
+ description: "Open lumen on the diff; annotations get sent back to the agent",
113
+ handler: async (args, ctx) => {
114
+ const argv = (args ?? "").trim().length > 0 ? (args as string).trim().split(/\s+/) : [];
115
+ try {
116
+ await reviewAndInject(pi, ctx, argv, { silentOnClean: false });
117
+ } catch (err) {
118
+ ctx.ui.notify(
119
+ `lumen review failed: ${err instanceof Error ? err.message : String(err)}`,
120
+ "error",
121
+ );
122
+ }
123
+ },
124
+ });
125
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@jnsahaj/pi-lumen-diff",
3
+ "version": "0.3.0",
4
+ "description": "Pi extension that opens lumen on the diff after each agent turn and feeds annotations back as the next user message",
5
+ "type": "module",
6
+ "license": "MIT OR Apache-2.0",
7
+ "homepage": "https://github.com/jnsahaj/lumen",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/jnsahaj/lumen.git",
11
+ "directory": "integrations/pi/extension"
12
+ },
13
+ "keywords": ["pi-package", "lumen", "code-review", "diff", "coding-agent"],
14
+ "pi": {
15
+ "extensions": ["./"]
16
+ },
17
+ "files": ["index.ts", "README.md"],
18
+ "peerDependencies": {
19
+ "@earendil-works/pi-coding-agent": ">=0.74.0"
20
+ },
21
+ "devDependencies": {
22
+ "@earendil-works/pi-coding-agent": ">=0.74.0",
23
+ "@types/node": ">=20"
24
+ }
25
+ }