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 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 Hook
1
+ # Rewind Extension
2
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.
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.18.0+
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/hooks/rewind/`
25
- 2. Download the hook files
26
- 3. Add the hook to your `~/.pi/agent/settings.json`
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/hooks/rewind
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
- "hooks": ["~/.pi/agent/hooks/rewind/index.ts"]
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 hook creates git refs at two points:
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 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).
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 hook directory:
179
+ 1. Remove the extension directory:
161
180
  ```bash
162
- rm -rf ~/.pi/agent/hooks/rewind
181
+ rm -rf ~/.pi/agent/extensions/rewind
163
182
  ```
164
- On Windows (PowerShell): `Remove-Item -Recurse -Force ~/.pi/agent/hooks/rewind`
183
+ On Windows (PowerShell): `Remove-Item -Recurse -Force ~/.pi/agent/extensions/rewind`
165
184
 
166
- 2. Remove the hook from `~/.pi/agent/settings.json` (delete the line with `rewind/index.ts` from the `hooks` array)
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 hook:
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
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
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: HookAPI) {
17
- const checkpoints = new Map<number, string>();
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] Hook loaded`);
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: "success" | "error" | "info") => void
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
- pi.on("session", async (event, ctx) => {
117
- if (event.reason !== "start") return;
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 ctx.exec("git", ["rev-parse", "--is-inside-work-tree"]);
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(ctx.exec, checkpointId);
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(ctx.exec, checkpointId);
150
- if (success) {
151
- checkpoints.set(event.turnIndex, checkpointId);
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 turn ${event.turnIndex}`
194
+ `[rewind] Created checkpoint ${checkpointId} for entry ${currentEntryId}`
154
195
  );
155
- await pruneCheckpoints(ctx.exec);
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("session", async (event, ctx) => {
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
- let checkpointId = checkpoints.get(event.targetTurnIndex);
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(ctx.exec);
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
- ctx.exec,
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", "success");
271
+ ctx.ui.notify("Files restored to before last rewind", "info");
226
272
  }
227
- return { skipConversationRestore: true };
273
+ return { cancel: true };
228
274
  }
229
275
 
230
276
  const ref = `${REF_PREFIX}${checkpointId}`;
231
277
  const success = await restoreWithBackup(
232
- ctx.exec,
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
- "success"
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 function pruneCheckpoints(exec: ExecFn) {
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
- "for-each-ref",
254
- "--sort=creatordate",
255
- "--format=%(refname)",
256
- REF_PREFIX,
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
- const refs = result.stdout.trim().split("\n").filter(Boolean);
260
- const currentResumeRef = resumeCheckpoint ? `${REF_PREFIX}${resumeCheckpoint}` : null;
261
- const checkpointRefs = refs.filter(r =>
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
- if (checkpointRefs.length > MAX_CHECKPOINTS) {
266
- const toDelete = checkpointRefs.slice(0, checkpointRefs.length - MAX_CHECKPOINTS);
267
- for (const ref of toDelete) {
268
- await exec("git", ["update-ref", "-d", ref]);
269
- console.error(`[rewind] Pruned old checkpoint: ${ref}`);
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
- } catch (err) {
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 HOOK_DIR = path.join(os.homedir(), ".pi", "agent", "hooks", "rewind");
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 HOOK_PATH = "~/.pi/agent/hooks/rewind/index.ts";
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
- // Create hook directory
34
- console.log(`Creating directory: ${HOOK_DIR}`);
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 hookContent = await download(`${REPO_URL}/index.ts`);
40
- fs.writeFileSync(path.join(HOOK_DIR, "index.ts"), hookContent);
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(HOOK_DIR, "README.md"), readmeContent);
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
- // Ensure hooks array exists
60
- if (!Array.isArray(settings.hooks)) {
61
- settings.hooks = [];
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
- // 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`);
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("Hook already configured in settings.json");
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 hook will load automatically when you start pi.");
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.1.1",
4
- "description": "Rewind hook for Pi agent - automatic git checkpoints with file/conversation restore",
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
- "hook",
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
  }