pi-rewind-hook 1.0.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 (4) hide show
  1. package/README.md +172 -0
  2. package/index.ts +141 -0
  3. package/install.js +84 -0
  4. package/package.json +22 -0
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # Rewind Hook
2
+
3
+ A Pi agent hook that enables rewinding file changes during coding sessions. Creates automatic checkpoints using git refs, allowing you to restore files to previous states while optionally preserving conversation history.
4
+
5
+ ## Requirements
6
+
7
+ - Pi agent v0.18.0+
8
+ - Node.js (for installation)
9
+ - Git repository (checkpoints are stored as git refs)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npx pi-rewind-hook
15
+ ```
16
+
17
+ This will:
18
+ 1. Create `~/.pi/agent/hooks/rewind/`
19
+ 2. Download the hook files
20
+ 3. Add the hook to your `~/.pi/agent/settings.json`
21
+
22
+ ### Alternative Installation
23
+
24
+ Using curl:
25
+
26
+ ```bash
27
+ curl -fsSL https://raw.githubusercontent.com/nicobailon/pi-rewind-hook/main/install.js | node
28
+ ```
29
+
30
+ Or clone the repo and configure manually:
31
+
32
+ ```bash
33
+ git clone https://github.com/nicobailon/pi-rewind-hook ~/.pi/agent/hooks/rewind
34
+ ```
35
+
36
+ Then add to `~/.pi/agent/settings.json`:
37
+
38
+ ```json
39
+ {
40
+ "hooks": ["~/.pi/agent/hooks/rewind/index.ts"]
41
+ }
42
+ ```
43
+
44
+ ### Platform Notes
45
+
46
+ **Windows:** The `npx` command works in PowerShell, Command Prompt, and WSL. If you prefer curl on Windows without WSL:
47
+
48
+ ```powershell
49
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/nicobailon/pi-rewind-hook/main/install.js" -OutFile install.js; node install.js; Remove-Item install.js
50
+ ```
51
+
52
+ ## How It Works
53
+
54
+ ### Checkpoints
55
+
56
+ The hook creates git refs at two points:
57
+
58
+ 1. **Session start** - When pi starts, creates a "resume checkpoint" of the current file state
59
+ 2. **Each turn** - Before the agent processes each message, creates a checkpoint
60
+
61
+ Checkpoints are stored as git refs under `refs/pi-checkpoints/` and are pruned to keep the last 100.
62
+
63
+ ### Rewinding
64
+
65
+ To rewind:
66
+
67
+ 1. Type `/branch` in pi
68
+ 2. Select a message to branch from
69
+ 3. Choose a restore option:
70
+
71
+ **For messages from the current session:**
72
+
73
+ | Option | Files | Conversation |
74
+ |--------|-------|--------------|
75
+ | **Restore all (files + conversation)** | Restored | Reset to that point |
76
+ | **Conversation only (keep current files)** | Unchanged | Reset to that point |
77
+ | **Code only (restore files, keep conversation)** | Restored | Unchanged |
78
+
79
+ **For messages from before the current session (uses resume checkpoint):**
80
+
81
+ | Option | Files | Conversation |
82
+ |--------|-------|--------------|
83
+ | **Restore to session start (files + conversation)** | Restored to session start | Reset to that point |
84
+ | **Conversation only (keep current files)** | Unchanged | Reset to that point |
85
+ | **Restore to session start (files only, keep conversation)** | Restored to session start | Unchanged |
86
+
87
+ ### Resumed Sessions
88
+
89
+ When you resume a session (`pi --resume`), the hook creates a resume checkpoint. If you branch to a message from before the current session, you can restore files to the state when you resumed (not per-message granularity, but a safety net).
90
+
91
+ ## Examples
92
+
93
+ ### Undo a bad refactor
94
+
95
+ ```
96
+ You: refactor the auth module to use JWT
97
+ Agent: [makes changes you don't like]
98
+
99
+ You: /branch
100
+ → Select "refactor the auth module to use JWT"
101
+ → Select "Code only (restore files, keep conversation)"
102
+
103
+ Result: Files restored, conversation intact. Try a different approach.
104
+ ```
105
+
106
+ ### Start fresh from a checkpoint
107
+
108
+ ```
109
+ You: /branch
110
+ → Select an earlier message
111
+ → Select "Restore all (files + conversation)"
112
+
113
+ Result: Both files and conversation reset to that point.
114
+ ```
115
+
116
+ ### Recover after resuming
117
+
118
+ ```bash
119
+ pi --resume # resume old session
120
+ ```
121
+
122
+ ```
123
+ Agent: [immediately breaks something]
124
+
125
+ You: /branch
126
+ → Select any old message
127
+ → Select "Restore to session start (files only, keep conversation)"
128
+
129
+ Result: Files restored to state when you resumed.
130
+ ```
131
+
132
+ ## Viewing Checkpoints
133
+
134
+ List all checkpoint refs:
135
+
136
+ ```bash
137
+ git for-each-ref refs/pi-checkpoints/
138
+ ```
139
+
140
+ Manually restore to a checkpoint:
141
+
142
+ ```bash
143
+ git checkout refs/pi-checkpoints/checkpoint-1234567890 -- .
144
+ ```
145
+
146
+ Delete all checkpoints:
147
+
148
+ ```bash
149
+ git for-each-ref --format='%(refname)' refs/pi-checkpoints/ | xargs -n1 git update-ref -d
150
+ ```
151
+
152
+ ## Uninstalling
153
+
154
+ 1. Remove the hook directory:
155
+ ```bash
156
+ rm -rf ~/.pi/agent/hooks/rewind
157
+ ```
158
+ On Windows (PowerShell): `Remove-Item -Recurse -Force ~/.pi/agent/hooks/rewind`
159
+
160
+ 2. Remove the hook from `~/.pi/agent/settings.json` (delete the line with `rewind/index.ts` from the `hooks` array)
161
+
162
+ 3. Optionally, clean up git refs in each repo where you used the hook:
163
+ ```bash
164
+ git for-each-ref --format='%(refname)' refs/pi-checkpoints/ | xargs -n1 git update-ref -d
165
+ ```
166
+
167
+ ## Limitations
168
+
169
+ - Only works in git repositories
170
+ - Checkpoints are per-process (in-memory map), not persisted across restarts
171
+ - Resumed sessions only have a single resume checkpoint for pre-session messages
172
+ - Tracks working directory changes only (not staged/committed changes)
package/index.ts ADDED
@@ -0,0 +1,141 @@
1
+ import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
2
+
3
+ const REF_PREFIX = "refs/pi-checkpoints/";
4
+ const MAX_CHECKPOINTS = 100;
5
+
6
+ export default function (pi: HookAPI) {
7
+ const checkpoints = new Map<number, string>();
8
+ let resumeCheckpoint: string | null = null;
9
+
10
+ console.error(`[rewind] Hook loaded`);
11
+
12
+ pi.on("session_start", async (event, ctx) => {
13
+ if (!ctx.hasUI) return;
14
+
15
+ const checkpointId = `checkpoint-resume-${Date.now()}`;
16
+
17
+ try {
18
+ await ctx.exec("git", [
19
+ "update-ref",
20
+ `${REF_PREFIX}${checkpointId}`,
21
+ "HEAD",
22
+ ]);
23
+
24
+ resumeCheckpoint = checkpointId;
25
+ console.error(`[rewind] Created resume checkpoint: ${checkpointId}`);
26
+ } catch (err) {
27
+ console.error(`[rewind] Failed to create resume checkpoint: ${err}`);
28
+ }
29
+ });
30
+
31
+ pi.on("turn_start", async (event, ctx) => {
32
+ if (!ctx.hasUI) return;
33
+
34
+ const checkpointId = `checkpoint-${event.timestamp}`;
35
+
36
+ try {
37
+ await ctx.exec("git", [
38
+ "update-ref",
39
+ `${REF_PREFIX}${checkpointId}`,
40
+ "HEAD",
41
+ ]);
42
+
43
+ checkpoints.set(event.turnIndex, checkpointId);
44
+ console.error(
45
+ `[rewind] Created checkpoint ${checkpointId} for turn ${event.turnIndex}`
46
+ );
47
+
48
+ await pruneCheckpoints(ctx);
49
+ } catch (err) {
50
+ console.error(`[rewind] Failed to create checkpoint: ${err}`);
51
+ }
52
+ });
53
+
54
+ pi.on("branch", async (event, ctx) => {
55
+ if (!ctx.hasUI) return;
56
+
57
+ let checkpointId = checkpoints.get(event.targetTurnIndex);
58
+ let usingResumeCheckpoint = false;
59
+
60
+ if (!checkpointId && resumeCheckpoint) {
61
+ checkpointId = resumeCheckpoint;
62
+ usingResumeCheckpoint = true;
63
+ }
64
+
65
+ if (!checkpointId) {
66
+ ctx.ui.notify(
67
+ "No checkpoint available for this message",
68
+ "info"
69
+ );
70
+ return undefined;
71
+ }
72
+
73
+ const options = usingResumeCheckpoint
74
+ ? [
75
+ "Restore to session start (files + conversation)",
76
+ "Conversation only (keep current files)",
77
+ "Restore to session start (files only, keep conversation)",
78
+ ]
79
+ : [
80
+ "Restore all (files + conversation)",
81
+ "Conversation only (keep current files)",
82
+ "Code only (restore files, keep conversation)",
83
+ ];
84
+
85
+ const choice = await ctx.ui.select("Restore Options", options);
86
+
87
+ if (!choice) {
88
+ ctx.ui.notify("Rewind cancelled", "info");
89
+ return { skipConversationRestore: true };
90
+ }
91
+
92
+ if (choice.startsWith("Conversation only")) {
93
+ return undefined;
94
+ }
95
+
96
+ try {
97
+ const ref = `${REF_PREFIX}${checkpointId}`;
98
+ await ctx.exec("git", ["checkout", ref, "--", "."]);
99
+ console.error(`[rewind] Restored files from ${checkpointId}`);
100
+ ctx.ui.notify(
101
+ usingResumeCheckpoint
102
+ ? "Files restored to session start"
103
+ : "Files restored from checkpoint",
104
+ "success"
105
+ );
106
+ } catch (err) {
107
+ console.error(`[rewind] Failed to restore: ${err}`);
108
+ ctx.ui.notify(`Failed to restore files: ${err}`, "error");
109
+ return { skipConversationRestore: true };
110
+ }
111
+
112
+ if (choice.includes("files only") || choice.startsWith("Code only")) {
113
+ return { skipConversationRestore: true };
114
+ }
115
+
116
+ return undefined;
117
+ });
118
+
119
+ async function pruneCheckpoints(ctx: { exec: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string; code: number }> }) {
120
+ try {
121
+ const result = await ctx.exec("git", [
122
+ "for-each-ref",
123
+ "--sort=creatordate",
124
+ "--format=%(refname)",
125
+ REF_PREFIX,
126
+ ]);
127
+
128
+ const refs = result.stdout.trim().split("\n").filter(Boolean);
129
+
130
+ if (refs.length > MAX_CHECKPOINTS) {
131
+ const toDelete = refs.slice(0, refs.length - MAX_CHECKPOINTS);
132
+ for (const ref of toDelete) {
133
+ await ctx.exec("git", ["update-ref", "-d", ref]);
134
+ console.error(`[rewind] Pruned old checkpoint: ${ref}`);
135
+ }
136
+ }
137
+ } catch (err) {
138
+ console.error(`[rewind] Failed to prune checkpoints: ${err}`);
139
+ }
140
+ }
141
+ }
package/install.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const https = require("https");
6
+ const os = require("os");
7
+
8
+ const REPO_URL = "https://raw.githubusercontent.com/nicobailon/pi-rewind-hook/main";
9
+ const HOOK_DIR = path.join(os.homedir(), ".pi", "agent", "hooks", "rewind");
10
+ const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
11
+ const HOOK_PATH = "~/.pi/agent/hooks/rewind/index.ts";
12
+
13
+ function download(url) {
14
+ return new Promise((resolve, reject) => {
15
+ https.get(url, (res) => {
16
+ if (res.statusCode === 301 || res.statusCode === 302) {
17
+ return download(res.headers.location).then(resolve).catch(reject);
18
+ }
19
+ if (res.statusCode !== 200) {
20
+ return reject(new Error(`Failed to download ${url}: ${res.statusCode}`));
21
+ }
22
+ let data = "";
23
+ res.on("data", (chunk) => (data += chunk));
24
+ res.on("end", () => resolve(data));
25
+ res.on("error", reject);
26
+ }).on("error", reject);
27
+ });
28
+ }
29
+
30
+ async function main() {
31
+ console.log("Installing pi-rewind-hook...\n");
32
+
33
+ // Create hook directory
34
+ console.log(`Creating directory: ${HOOK_DIR}`);
35
+ fs.mkdirSync(HOOK_DIR, { recursive: true });
36
+
37
+ // Download hook files
38
+ console.log("Downloading index.ts...");
39
+ const hookContent = await download(`${REPO_URL}/index.ts`);
40
+ fs.writeFileSync(path.join(HOOK_DIR, "index.ts"), hookContent);
41
+
42
+ console.log("Downloading README.md...");
43
+ const readmeContent = await download(`${REPO_URL}/README.md`);
44
+ fs.writeFileSync(path.join(HOOK_DIR, "README.md"), readmeContent);
45
+
46
+ // Update settings.json
47
+ console.log(`\nUpdating settings: ${SETTINGS_FILE}`);
48
+
49
+ let settings = {};
50
+ if (fs.existsSync(SETTINGS_FILE)) {
51
+ try {
52
+ settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
53
+ } catch (err) {
54
+ console.error(`Warning: Could not parse existing settings.json: ${err.message}`);
55
+ console.error("Creating new settings file...");
56
+ }
57
+ }
58
+
59
+ // Ensure hooks array exists
60
+ if (!Array.isArray(settings.hooks)) {
61
+ settings.hooks = [];
62
+ }
63
+
64
+ // Add hook if not already present
65
+ if (!settings.hooks.includes(HOOK_PATH)) {
66
+ settings.hooks.push(HOOK_PATH);
67
+
68
+ // Ensure parent directory exists
69
+ fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
70
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
71
+ console.log(`Added "${HOOK_PATH}" to hooks array`);
72
+ } else {
73
+ console.log("Hook already configured in settings.json");
74
+ }
75
+
76
+ console.log("\nInstallation complete!");
77
+ console.log("\nThe rewind hook will load automatically when you start pi.");
78
+ console.log("Use /branch to rewind to a previous checkpoint.");
79
+ }
80
+
81
+ main().catch((err) => {
82
+ console.error(`\nInstallation failed: ${err.message}`);
83
+ process.exit(1);
84
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "pi-rewind-hook",
3
+ "version": "1.0.0",
4
+ "description": "Rewind hook for Pi agent - automatic git checkpoints with file/conversation restore",
5
+ "bin": {
6
+ "pi-rewind-hook": "./install.js"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/nicobailon/pi-rewind-hook"
11
+ },
12
+ "keywords": [
13
+ "pi",
14
+ "pi-agent",
15
+ "hook",
16
+ "checkpoint",
17
+ "rewind",
18
+ "git"
19
+ ],
20
+ "author": "nicobailon",
21
+ "license": "MIT"
22
+ }