pi-rewind-hook 1.1.1 → 1.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/CHANGELOG.md +26 -0
- package/README.md +36 -17
- package/index.ts +151 -48
- package/install.js +51 -24
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-01-05
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
- Requires pi v0.35.0+ (unified extensions system)
|
|
9
|
+
- Install location changed from `hooks/rewind` to `extensions/rewind`
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Migrated from hooks to unified extensions system
|
|
13
|
+
- Settings key changed from `hooks` to `extensions`
|
|
14
|
+
- Install script now migrates old hooks config and cleans up old directory
|
|
15
|
+
- Renamed "Hook" to "Extension" throughout codebase and docs
|
|
16
|
+
|
|
17
|
+
## [1.2.0] - 2025-01-03
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Tree navigation support (`session_before_tree`) - restore files when navigating session tree
|
|
21
|
+
- Entry-based checkpoint mapping (uses entry IDs instead of turn indices)
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Migrated to granular session events API (pi-coding-agent v0.31+)
|
|
25
|
+
- Use `pi.exec` instead of `ctx.exec` per updated hooks API
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Removed `agent_end` handler that was clearing checkpoints after each turn
|
|
29
|
+
- "Undo last file rewind" now cancels branch instead of creating unwanted branch
|
|
30
|
+
|
|
5
31
|
## [1.1.1] - 2024-12-27
|
|
6
32
|
|
|
7
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Rewind
|
|
1
|
+
# Rewind Extension
|
|
2
2
|
|
|
3
|
-
A Pi agent
|
|
3
|
+
A Pi agent extension 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
4
|
|
|
5
5
|
## Screenshots
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ A Pi agent hook that enables rewinding file changes during coding sessions. Crea
|
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
|
-
- Pi agent v0.
|
|
13
|
+
- Pi agent v0.35.0+ (unified extensions system)
|
|
14
14
|
- Node.js (for installation)
|
|
15
15
|
- Git repository (checkpoints are stored as git refs)
|
|
16
16
|
|
|
@@ -21,9 +21,11 @@ npx pi-rewind-hook
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
This will:
|
|
24
|
-
1. Create `~/.pi/agent/
|
|
25
|
-
2. Download the
|
|
26
|
-
3. Add the
|
|
24
|
+
1. Create `~/.pi/agent/extensions/rewind/`
|
|
25
|
+
2. Download the extension files
|
|
26
|
+
3. Add the extension to your `~/.pi/agent/settings.json`
|
|
27
|
+
4. Migrate any existing hooks config to extensions (if upgrading from v1.2.0)
|
|
28
|
+
5. Clean up old `hooks/rewind` directory (if present)
|
|
27
29
|
|
|
28
30
|
### Alternative Installation
|
|
29
31
|
|
|
@@ -36,14 +38,14 @@ curl -fsSL https://raw.githubusercontent.com/nicobailon/pi-rewind-hook/main/inst
|
|
|
36
38
|
Or clone the repo and configure manually:
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
git clone https://github.com/nicobailon/pi-rewind-hook ~/.pi/agent/
|
|
41
|
+
git clone https://github.com/nicobailon/pi-rewind-hook ~/.pi/agent/extensions/rewind
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
Then add to `~/.pi/agent/settings.json`:
|
|
43
45
|
|
|
44
46
|
```json
|
|
45
47
|
{
|
|
46
|
-
"
|
|
48
|
+
"extensions": ["~/.pi/agent/extensions/rewind/index.ts"]
|
|
47
49
|
}
|
|
48
50
|
```
|
|
49
51
|
|
|
@@ -55,11 +57,20 @@ Then add to `~/.pi/agent/settings.json`:
|
|
|
55
57
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/nicobailon/pi-rewind-hook/main/install.js" -OutFile install.js; node install.js; Remove-Item install.js
|
|
56
58
|
```
|
|
57
59
|
|
|
60
|
+
### Upgrading from v1.2.0
|
|
61
|
+
|
|
62
|
+
If you're upgrading from pi-rewind-hook v1.2.0 (which used the hooks system), simply run `npx pi-rewind-hook` again. The installer will:
|
|
63
|
+
- Move the extension from `hooks/rewind` to `extensions/rewind`
|
|
64
|
+
- Migrate your settings.json from `hooks` to `extensions`
|
|
65
|
+
- Clean up the old hooks directory
|
|
66
|
+
|
|
67
|
+
**Note:** v1.3.0+ requires pi v0.35.0 or later. If you're on an older version of pi, stay on pi-rewind-hook v1.2.0.
|
|
68
|
+
|
|
58
69
|
## How It Works
|
|
59
70
|
|
|
60
71
|
### Checkpoints
|
|
61
72
|
|
|
62
|
-
The
|
|
73
|
+
The extension creates git refs at two points:
|
|
63
74
|
|
|
64
75
|
1. **Session start** - When pi starts, creates a "resume checkpoint" of the current file state
|
|
65
76
|
2. **Each turn** - Before the agent processes each message, creates a checkpoint
|
|
@@ -68,11 +79,17 @@ Checkpoints are stored as git refs under `refs/pi-checkpoints/` and are pruned t
|
|
|
68
79
|
|
|
69
80
|
### Rewinding
|
|
70
81
|
|
|
71
|
-
To rewind
|
|
82
|
+
To rewind via `/branch`:
|
|
72
83
|
|
|
73
84
|
1. Type `/branch` in pi
|
|
74
85
|
2. Select a message to branch from
|
|
75
|
-
3. Choose a restore option
|
|
86
|
+
3. Choose a restore option
|
|
87
|
+
|
|
88
|
+
To rewind via tree navigation:
|
|
89
|
+
|
|
90
|
+
1. Press `Tab` to open the session tree
|
|
91
|
+
2. Navigate to a different node
|
|
92
|
+
3. Choose a restore option
|
|
76
93
|
|
|
77
94
|
**For messages from the current session:**
|
|
78
95
|
|
|
@@ -81,6 +98,7 @@ To rewind:
|
|
|
81
98
|
| **Restore all (files + conversation)** | Restored | Reset to that point |
|
|
82
99
|
| **Conversation only (keep current files)** | Unchanged | Reset to that point |
|
|
83
100
|
| **Code only (restore files, keep conversation)** | Restored | Unchanged |
|
|
101
|
+
| **Undo last file rewind** | Restored to before last rewind | Unchanged |
|
|
84
102
|
|
|
85
103
|
**For messages from before the current session (uses resume checkpoint):**
|
|
86
104
|
|
|
@@ -89,10 +107,11 @@ To rewind:
|
|
|
89
107
|
| **Restore to session start (files + conversation)** | Restored to session start | Reset to that point |
|
|
90
108
|
| **Conversation only (keep current files)** | Unchanged | Reset to that point |
|
|
91
109
|
| **Restore to session start (files only, keep conversation)** | Restored to session start | Unchanged |
|
|
110
|
+
| **Undo last file rewind** | Restored to before last rewind | Unchanged |
|
|
92
111
|
|
|
93
112
|
### Resumed Sessions
|
|
94
113
|
|
|
95
|
-
When you resume a session (`pi --resume`), the
|
|
114
|
+
When you resume a session (`pi --resume`), the extension 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).
|
|
96
115
|
|
|
97
116
|
## Examples
|
|
98
117
|
|
|
@@ -157,15 +176,15 @@ git for-each-ref --format='%(refname)' refs/pi-checkpoints/ | xargs -n1 git upda
|
|
|
157
176
|
|
|
158
177
|
## Uninstalling
|
|
159
178
|
|
|
160
|
-
1. Remove the
|
|
179
|
+
1. Remove the extension directory:
|
|
161
180
|
```bash
|
|
162
|
-
rm -rf ~/.pi/agent/
|
|
181
|
+
rm -rf ~/.pi/agent/extensions/rewind
|
|
163
182
|
```
|
|
164
|
-
On Windows (PowerShell): `Remove-Item -Recurse -Force ~/.pi/agent/
|
|
183
|
+
On Windows (PowerShell): `Remove-Item -Recurse -Force ~/.pi/agent/extensions/rewind`
|
|
165
184
|
|
|
166
|
-
2. Remove the
|
|
185
|
+
2. Remove the extension from `~/.pi/agent/settings.json` (delete the line with `rewind/index.ts` from the `extensions` array)
|
|
167
186
|
|
|
168
|
-
3. Optionally, clean up git refs in each repo where you used the
|
|
187
|
+
3. Optionally, clean up git refs in each repo where you used the extension:
|
|
169
188
|
```bash
|
|
170
189
|
git for-each-ref --format='%(refname)' refs/pi-checkpoints/ | xargs -n1 git update-ref -d
|
|
171
190
|
```
|
package/index.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Rewind Extension - Git-based file restoration for pi branching
|
|
3
|
+
*
|
|
4
|
+
* Creates worktree snapshots at each turn so /branch can restore code state.
|
|
5
|
+
* Supports: restore files + conversation, files only, conversation only, undo last restore.
|
|
6
|
+
*
|
|
7
|
+
* Updated for pi-coding-agent v0.35.0+ (unified extensions system)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
11
|
import { exec as execCb } from "child_process";
|
|
3
12
|
import { mkdtemp, rm } from "fs/promises";
|
|
4
13
|
import { tmpdir } from "os";
|
|
@@ -13,13 +22,14 @@ const MAX_CHECKPOINTS = 100;
|
|
|
13
22
|
|
|
14
23
|
type ExecFn = (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
15
24
|
|
|
16
|
-
export default function (pi:
|
|
17
|
-
const checkpoints = new Map<
|
|
25
|
+
export default function (pi: ExtensionAPI) {
|
|
26
|
+
const checkpoints = new Map<string, string>();
|
|
27
|
+
let currentEntryId: string | undefined;
|
|
18
28
|
let resumeCheckpoint: string | null = null;
|
|
19
29
|
let repoRoot: string | null = null;
|
|
20
30
|
let isGitRepo = false;
|
|
21
31
|
|
|
22
|
-
console.error(`[rewind]
|
|
32
|
+
console.error(`[rewind] Extension loaded`);
|
|
23
33
|
|
|
24
34
|
async function findBeforeRestoreRef(exec: ExecFn): Promise<{ refName: string; commitSha: string } | null> {
|
|
25
35
|
try {
|
|
@@ -71,7 +81,7 @@ export default function (pi: HookAPI) {
|
|
|
71
81
|
async function restoreWithBackup(
|
|
72
82
|
exec: ExecFn,
|
|
73
83
|
targetRef: string,
|
|
74
|
-
notify: (msg: string, level: "
|
|
84
|
+
notify: (msg: string, level: "info" | "warning" | "error") => void
|
|
75
85
|
): Promise<boolean> {
|
|
76
86
|
try {
|
|
77
87
|
const existingBackup = await findBeforeRestoreRef(exec);
|
|
@@ -113,12 +123,38 @@ export default function (pi: HookAPI) {
|
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
async function pruneCheckpoints(exec: ExecFn) {
|
|
127
|
+
try {
|
|
128
|
+
const result = await exec("git", [
|
|
129
|
+
"for-each-ref",
|
|
130
|
+
"--sort=creatordate",
|
|
131
|
+
"--format=%(refname)",
|
|
132
|
+
REF_PREFIX,
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const refs = result.stdout.trim().split("\n").filter(Boolean);
|
|
136
|
+
const currentResumeRef = resumeCheckpoint ? `${REF_PREFIX}${resumeCheckpoint}` : null;
|
|
137
|
+
const checkpointRefs = refs.filter(r =>
|
|
138
|
+
!r.includes(BEFORE_RESTORE_PREFIX) && r !== currentResumeRef
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (checkpointRefs.length > MAX_CHECKPOINTS) {
|
|
142
|
+
const toDelete = checkpointRefs.slice(0, checkpointRefs.length - MAX_CHECKPOINTS);
|
|
143
|
+
for (const ref of toDelete) {
|
|
144
|
+
await exec("git", ["update-ref", "-d", ref]);
|
|
145
|
+
console.error(`[rewind] Pruned old checkpoint: ${ref}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`[rewind] Failed to prune checkpoints: ${err}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
118
154
|
if (!ctx.hasUI) return;
|
|
119
155
|
|
|
120
156
|
try {
|
|
121
|
-
const result = await
|
|
157
|
+
const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
122
158
|
isGitRepo = result.stdout.trim() === "true";
|
|
123
159
|
} catch {
|
|
124
160
|
isGitRepo = false;
|
|
@@ -129,7 +165,7 @@ export default function (pi: HookAPI) {
|
|
|
129
165
|
const checkpointId = `checkpoint-resume-${Date.now()}`;
|
|
130
166
|
|
|
131
167
|
try {
|
|
132
|
-
const success = await createCheckpointFromWorktree(
|
|
168
|
+
const success = await createCheckpointFromWorktree(pi.exec, checkpointId);
|
|
133
169
|
if (success) {
|
|
134
170
|
resumeCheckpoint = checkpointId;
|
|
135
171
|
console.error(`[rewind] Created resume checkpoint: ${checkpointId}`);
|
|
@@ -139,6 +175,11 @@ export default function (pi: HookAPI) {
|
|
|
139
175
|
}
|
|
140
176
|
});
|
|
141
177
|
|
|
178
|
+
pi.on("tool_result", async (_event, ctx) => {
|
|
179
|
+
const leaf = ctx.sessionManager.getLeafEntry();
|
|
180
|
+
if (leaf) currentEntryId = leaf.id;
|
|
181
|
+
});
|
|
182
|
+
|
|
142
183
|
pi.on("turn_start", async (event, ctx) => {
|
|
143
184
|
if (!ctx.hasUI) return;
|
|
144
185
|
if (!isGitRepo) return;
|
|
@@ -146,25 +187,30 @@ export default function (pi: HookAPI) {
|
|
|
146
187
|
const checkpointId = `checkpoint-${event.timestamp}`;
|
|
147
188
|
|
|
148
189
|
try {
|
|
149
|
-
const success = await createCheckpointFromWorktree(
|
|
150
|
-
if (success) {
|
|
151
|
-
checkpoints.set(
|
|
190
|
+
const success = await createCheckpointFromWorktree(pi.exec, checkpointId);
|
|
191
|
+
if (success && currentEntryId) {
|
|
192
|
+
checkpoints.set(currentEntryId, checkpointId);
|
|
152
193
|
console.error(
|
|
153
|
-
`[rewind] Created checkpoint ${checkpointId} for
|
|
194
|
+
`[rewind] Created checkpoint ${checkpointId} for entry ${currentEntryId}`
|
|
154
195
|
);
|
|
155
|
-
await pruneCheckpoints(
|
|
196
|
+
await pruneCheckpoints(pi.exec);
|
|
156
197
|
}
|
|
157
198
|
} catch (err) {
|
|
158
199
|
console.error(`[rewind] Failed to create checkpoint: ${err}`);
|
|
159
200
|
}
|
|
160
201
|
});
|
|
161
202
|
|
|
162
|
-
pi.on("
|
|
163
|
-
if (event.reason !== "before_branch") return;
|
|
203
|
+
pi.on("session_before_branch", async (event, ctx) => {
|
|
164
204
|
if (!ctx.hasUI) return;
|
|
165
|
-
if (!isGitRepo) return;
|
|
166
205
|
|
|
167
|
-
|
|
206
|
+
try {
|
|
207
|
+
const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
208
|
+
if (result.stdout.trim() !== "true") return;
|
|
209
|
+
} catch {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let checkpointId = checkpoints.get(event.entryId);
|
|
168
214
|
let usingResumeCheckpoint = false;
|
|
169
215
|
|
|
170
216
|
if (!checkpointId && resumeCheckpoint) {
|
|
@@ -172,7 +218,7 @@ export default function (pi: HookAPI) {
|
|
|
172
218
|
usingResumeCheckpoint = true;
|
|
173
219
|
}
|
|
174
220
|
|
|
175
|
-
const beforeRestoreRef = await findBeforeRestoreRef(
|
|
221
|
+
const beforeRestoreRef = await findBeforeRestoreRef(pi.exec);
|
|
176
222
|
const hasUndo = !!beforeRestoreRef;
|
|
177
223
|
|
|
178
224
|
if (!checkpointId && !hasUndo) {
|
|
@@ -184,7 +230,7 @@ export default function (pi: HookAPI) {
|
|
|
184
230
|
}
|
|
185
231
|
|
|
186
232
|
const options: string[] = [];
|
|
187
|
-
|
|
233
|
+
|
|
188
234
|
if (checkpointId) {
|
|
189
235
|
if (usingResumeCheckpoint) {
|
|
190
236
|
options.push("Restore to session start (files + conversation)");
|
|
@@ -217,28 +263,28 @@ export default function (pi: HookAPI) {
|
|
|
217
263
|
|
|
218
264
|
if (choice === "Undo last file rewind") {
|
|
219
265
|
const success = await restoreWithBackup(
|
|
220
|
-
|
|
266
|
+
pi.exec,
|
|
221
267
|
beforeRestoreRef!.commitSha,
|
|
222
268
|
ctx.ui.notify.bind(ctx.ui)
|
|
223
269
|
);
|
|
224
270
|
if (success) {
|
|
225
|
-
ctx.ui.notify("Files restored to before last rewind", "
|
|
271
|
+
ctx.ui.notify("Files restored to before last rewind", "info");
|
|
226
272
|
}
|
|
227
|
-
return {
|
|
273
|
+
return { cancel: true };
|
|
228
274
|
}
|
|
229
275
|
|
|
230
276
|
const ref = `${REF_PREFIX}${checkpointId}`;
|
|
231
277
|
const success = await restoreWithBackup(
|
|
232
|
-
|
|
278
|
+
pi.exec,
|
|
233
279
|
ref,
|
|
234
280
|
ctx.ui.notify.bind(ctx.ui)
|
|
235
281
|
);
|
|
236
282
|
if (success) {
|
|
237
283
|
ctx.ui.notify(
|
|
238
|
-
usingResumeCheckpoint
|
|
239
|
-
? "Files restored to session start"
|
|
284
|
+
usingResumeCheckpoint
|
|
285
|
+
? "Files restored to session start"
|
|
240
286
|
: "Files restored from checkpoint",
|
|
241
|
-
"
|
|
287
|
+
"info"
|
|
242
288
|
);
|
|
243
289
|
}
|
|
244
290
|
|
|
@@ -247,30 +293,87 @@ export default function (pi: HookAPI) {
|
|
|
247
293
|
}
|
|
248
294
|
});
|
|
249
295
|
|
|
250
|
-
async
|
|
296
|
+
pi.on("session_before_tree", async (event, ctx) => {
|
|
297
|
+
if (!ctx.hasUI) return;
|
|
298
|
+
|
|
251
299
|
try {
|
|
252
|
-
const result = await exec("git", [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
]);
|
|
300
|
+
const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
301
|
+
if (result.stdout.trim() !== "true") return;
|
|
302
|
+
} catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
258
305
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
!r.includes(BEFORE_RESTORE_PREFIX) && r !== currentResumeRef
|
|
263
|
-
);
|
|
306
|
+
const targetId = event.preparation.targetId;
|
|
307
|
+
let checkpointId = checkpoints.get(targetId);
|
|
308
|
+
let usingResumeCheckpoint = false;
|
|
264
309
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
310
|
+
if (!checkpointId && resumeCheckpoint) {
|
|
311
|
+
checkpointId = resumeCheckpoint;
|
|
312
|
+
usingResumeCheckpoint = true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const beforeRestoreRef = await findBeforeRestoreRef(pi.exec);
|
|
316
|
+
const hasUndo = !!beforeRestoreRef;
|
|
317
|
+
|
|
318
|
+
if (!checkpointId && !hasUndo) {
|
|
319
|
+
ctx.ui.notify("No checkpoint available for this message", "info");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const options: string[] = [];
|
|
324
|
+
|
|
325
|
+
if (checkpointId) {
|
|
326
|
+
if (usingResumeCheckpoint) {
|
|
327
|
+
options.push("Restore files to session start");
|
|
328
|
+
} else {
|
|
329
|
+
options.push("Restore files to that point");
|
|
271
330
|
}
|
|
272
|
-
|
|
273
|
-
console.error(`[rewind] Failed to prune checkpoints: ${err}`);
|
|
331
|
+
options.push("Keep current files");
|
|
274
332
|
}
|
|
275
|
-
|
|
333
|
+
|
|
334
|
+
if (hasUndo) {
|
|
335
|
+
options.push("Undo last file rewind");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
options.push("Cancel navigation");
|
|
339
|
+
|
|
340
|
+
const choice = await ctx.ui.select("Restore Options", options);
|
|
341
|
+
|
|
342
|
+
if (!choice || choice === "Cancel navigation") {
|
|
343
|
+
ctx.ui.notify("Navigation cancelled", "info");
|
|
344
|
+
return { cancel: true };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (choice === "Keep current files") {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (choice === "Undo last file rewind") {
|
|
352
|
+
const success = await restoreWithBackup(
|
|
353
|
+
pi.exec,
|
|
354
|
+
beforeRestoreRef!.commitSha,
|
|
355
|
+
ctx.ui.notify.bind(ctx.ui)
|
|
356
|
+
);
|
|
357
|
+
if (success) {
|
|
358
|
+
ctx.ui.notify("Files restored to before last rewind", "info");
|
|
359
|
+
}
|
|
360
|
+
return { cancel: true };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const ref = `${REF_PREFIX}${checkpointId}`;
|
|
364
|
+
const success = await restoreWithBackup(
|
|
365
|
+
pi.exec,
|
|
366
|
+
ref,
|
|
367
|
+
ctx.ui.notify.bind(ctx.ui)
|
|
368
|
+
);
|
|
369
|
+
if (success) {
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
usingResumeCheckpoint
|
|
372
|
+
? "Files restored to session start"
|
|
373
|
+
: "Files restored to checkpoint",
|
|
374
|
+
"info"
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
276
379
|
}
|
package/install.js
CHANGED
|
@@ -6,9 +6,10 @@ const https = require("https");
|
|
|
6
6
|
const os = require("os");
|
|
7
7
|
|
|
8
8
|
const REPO_URL = "https://raw.githubusercontent.com/nicobailon/pi-rewind-hook/main";
|
|
9
|
-
const
|
|
9
|
+
const EXT_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "rewind");
|
|
10
|
+
const OLD_HOOK_DIR = path.join(os.homedir(), ".pi", "agent", "hooks", "rewind");
|
|
10
11
|
const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
11
|
-
const
|
|
12
|
+
const EXT_PATH = "~/.pi/agent/extensions/rewind/index.ts";
|
|
12
13
|
|
|
13
14
|
function download(url) {
|
|
14
15
|
return new Promise((resolve, reject) => {
|
|
@@ -28,22 +29,19 @@ function download(url) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
async function main() {
|
|
31
|
-
console.log("Installing pi-rewind-hook...\n");
|
|
32
|
+
console.log("Installing pi-rewind-hook (Rewind Extension)...\n");
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
console.log(`
|
|
35
|
-
fs.mkdirSync(HOOK_DIR, { recursive: true });
|
|
34
|
+
fs.mkdirSync(EXT_DIR, { recursive: true });
|
|
35
|
+
console.log(`Created directory: ${EXT_DIR}`);
|
|
36
36
|
|
|
37
|
-
// Download hook files
|
|
38
37
|
console.log("Downloading index.ts...");
|
|
39
|
-
const
|
|
40
|
-
fs.writeFileSync(path.join(
|
|
38
|
+
const extContent = await download(`${REPO_URL}/index.ts`);
|
|
39
|
+
fs.writeFileSync(path.join(EXT_DIR, "index.ts"), extContent);
|
|
41
40
|
|
|
42
41
|
console.log("Downloading README.md...");
|
|
43
42
|
const readmeContent = await download(`${REPO_URL}/README.md`);
|
|
44
|
-
fs.writeFileSync(path.join(
|
|
43
|
+
fs.writeFileSync(path.join(EXT_DIR, "README.md"), readmeContent);
|
|
45
44
|
|
|
46
|
-
// Update settings.json
|
|
47
45
|
console.log(`\nUpdating settings: ${SETTINGS_FILE}`);
|
|
48
46
|
|
|
49
47
|
let settings = {};
|
|
@@ -56,25 +54,54 @@ async function main() {
|
|
|
56
54
|
}
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
settings.
|
|
57
|
+
if (settings.hooks && Array.isArray(settings.hooks) && settings.hooks.length > 0) {
|
|
58
|
+
console.log("\nMigrating hooks to extensions...");
|
|
59
|
+
if (!Array.isArray(settings.extensions)) {
|
|
60
|
+
settings.extensions = [];
|
|
61
|
+
}
|
|
62
|
+
for (const entry of settings.hooks) {
|
|
63
|
+
if (entry.includes("/hooks/rewind")) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const newPath = entry.replace("/hooks/", "/extensions/");
|
|
67
|
+
if (!settings.extensions.includes(newPath)) {
|
|
68
|
+
settings.extensions.push(newPath);
|
|
69
|
+
console.log(` Migrated: ${entry} -> ${newPath}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
delete settings.hooks;
|
|
73
|
+
console.log("Removed old 'hooks' key from settings");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!Array.isArray(settings.extensions)) {
|
|
77
|
+
settings.extensions = [];
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const EXT_PATH_ALT = "~/.pi/agent/extensions/rewind";
|
|
81
|
+
const hasRewindExt = settings.extensions.some(p =>
|
|
82
|
+
p === EXT_PATH || p === EXT_PATH_ALT ||
|
|
83
|
+
p.includes("/extensions/rewind/index.ts") ||
|
|
84
|
+
p.endsWith("/extensions/rewind")
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!hasRewindExt) {
|
|
88
|
+
settings.extensions.push(EXT_PATH);
|
|
89
|
+
console.log(`Added "${EXT_PATH}" to extensions array`);
|
|
72
90
|
} else {
|
|
73
|
-
console.log("
|
|
91
|
+
console.log("Extension already configured in settings.json");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
|
|
95
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(OLD_HOOK_DIR)) {
|
|
98
|
+
console.log(`\nCleaning up old hooks directory: ${OLD_HOOK_DIR}`);
|
|
99
|
+
fs.rmSync(OLD_HOOK_DIR, { recursive: true, force: true });
|
|
100
|
+
console.log("Removed old hooks/rewind directory");
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
console.log("\nInstallation complete!");
|
|
77
|
-
console.log("\nThe rewind
|
|
104
|
+
console.log("\nThe rewind extension will load automatically when you start pi.");
|
|
78
105
|
console.log("Use /branch to rewind to a previous checkpoint.");
|
|
79
106
|
}
|
|
80
107
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-rewind-hook",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Rewind
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Rewind extension for Pi agent - automatic git checkpoints with file/conversation restore",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pi-rewind-hook": "./install.js"
|
|
7
7
|
},
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
"keywords": [
|
|
13
13
|
"pi",
|
|
14
14
|
"pi-agent",
|
|
15
|
-
"
|
|
15
|
+
"extension",
|
|
16
16
|
"checkpoint",
|
|
17
17
|
"rewind",
|
|
18
18
|
"git"
|
|
19
19
|
],
|
|
20
20
|
"author": "nicobailon",
|
|
21
|
-
"license": "MIT"
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": ["./index.ts"]
|
|
24
|
+
}
|
|
22
25
|
}
|