pi-undo-redo 0.1.1

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/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # pi-undo-redo
2
+
3
+ Undo/redo for Pi conversations and workspace changes with hybrid git/non-git behavior.
4
+
5
+ In git repositories this extension keeps file snapshots in a shadow git repository and records per-message patch metadata in the Pi session. `/undo` navigates the conversation tree back to the previous user message and reverts only the files changed by the undone message(s). `/redo` restores the files and conversation leaf captured before undo.
6
+
7
+ In non-git directories it does **not** scan or snapshot the whole workspace. It snapshots only explicit file paths touched by Pi file tools that provide a path (`write` and `edit`). Shell commands and custom tools without explicit path metadata are not parsed for paths and are not promised to be undo-restorable.
8
+
9
+ A dirty guard blocks `/undo`, `/redo`, and `/tree` when unsnapshotted workspace changes would be overwritten.
10
+
11
+ ## Commands
12
+
13
+ - `/undo` - undo the latest checkpointed user message and restore changed files
14
+ - `/redo` - restore the last undone conversation/file state
15
+ - `/undo-cleanup` - conservatively prune old protected snapshot refs while keeping snapshots referenced by the current session state
16
+
17
+ ## Configuration
18
+
19
+ Configure in `~/.pi/agent/settings.json` or `.pi/settings.json`:
20
+
21
+ ```json
22
+ {
23
+ "undoRedo": {
24
+ "storageDir": "E:/pi-undo-redo",
25
+ "largeFileLimitBytes": 2097152,
26
+ "gitTimeoutMs": 30000,
27
+ "enabled": true
28
+ }
29
+ }
30
+ ```
31
+
32
+ Defaults:
33
+
34
+ - `storageDir`: `~/.pi/agent/state/pi-undo-redo`
35
+ - `largeFileLimitBytes`: `2097152` (2 MiB; large explicit non-git files are skipped/fail closed with a warning instead of silently claiming undo support)
36
+ - `gitTimeoutMs`: `30000`
37
+ - `enabled`: `true`
38
+
39
+ ## Hybrid behavior
40
+
41
+ ### Git repositories
42
+
43
+ - Uses the existing shadow git snapshot behavior.
44
+ - Catches tracked modifications and ordinary non-ignored untracked files, including files created by shell/filesystem commands.
45
+ - Respects `.gitignore` and `.git/info/exclude` from the source repository.
46
+ - Maintains a shadow snapshot of source tracked files plus changed ordinary non-ignored untracked files; ignored files and large untracked files are not copied into the shadow repository.
47
+ - `/tree` restores the workspace snapshot that matches the selected conversation node.
48
+
49
+ ### Non-git directories
50
+
51
+ - Does not root-wide scan/snapshot the cwd, home directory, or project root.
52
+ - Before-agent snapshots are empty and cheap; when a Pi `write` or `edit` tool is about to run, the extension snapshots only that explicit path's current state.
53
+ - Finalization snapshots after-state for those touched files only, supporting create/modify/delete for normal files where possible.
54
+ - `/undo` and `/redo` restore only `checkpoint.files` from the session checkpoint.
55
+ - `/tree` restores only the cumulative explicit checkpoint files along the branch up to the target node; it never restores or diffs a whole cwd snapshot.
56
+ - Dirty guard checks only affected checkpoint/touched paths, so unrelated sentinel files are not included.
57
+ - Shell-created files in non-git mode are not parsed or restored. The extension warns once per agent turn when a bash tool runs in non-git mode.
58
+
59
+ ## Path safety in non-git mode
60
+
61
+ Explicit paths are normalized relative to the workspace root. Relative paths resolve against Pi's current `ctx.cwd`; absolute paths are accepted only when they remain inside the workspace root. Paths containing NUL, escaping via `..`, or targeting dangerous workspace directories such as `.git`, `.pi`, or `node_modules` are rejected. Windows-style paths normalize to `/`-separated relative paths in checkpoint metadata.
62
+
63
+ ## Notes
64
+
65
+ - Snapshots are protected with internal shadow-git refs so stored tree objects are not immediately lost to Git GC.
66
+ - Restore avoids recursive directory deletion for file/dir conflicts; unsafe conflicts fail closed instead of deleting unknown directory contents.
67
+ - Non-git mode intentionally trades shell/filesystem auto-discovery for safety and bounded scope. Use a git repository when full OpenCode-style shell/file-system change capture is required.
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "pi-undo-redo",
3
+ "version": "0.1.1",
4
+ "description": "Undo/redo for Pi conversations and workspace changes with hybrid git and non-git snapshots.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Yueby/pi-undo-redo.git"
10
+ },
11
+ "homepage": "https://github.com/Yueby/pi-undo-redo#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Yueby/pi-undo-redo/issues"
14
+ },
15
+ "main": "./src/extension.ts",
16
+ "files": [
17
+ "src/",
18
+ "README.md",
19
+ "package.json",
20
+ "scripts/publish.mjs"
21
+ ],
22
+ "keywords": [
23
+ "pi-package",
24
+ "pi-extension",
25
+ "pi",
26
+ "pi-coding-agent",
27
+ "opencode",
28
+ "undo",
29
+ "redo",
30
+ "snapshot",
31
+ "rollback"
32
+ ],
33
+ "pi": {
34
+ "extensions": [
35
+ "./src/extension.ts"
36
+ ]
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "node scripts/self-test.mjs",
42
+ "release": "node scripts/publish.mjs"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.15.21",
46
+ "typescript": "^5.8.3"
47
+ },
48
+ "peerDependencies": {
49
+ "@earendil-works/pi-coding-agent": "*"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@earendil-works/pi-coding-agent": {
53
+ "optional": true
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ process.chdir(root);
9
+
10
+ const registry = "https://registry.npmjs.org/";
11
+
12
+ if (!existsSync("package.json")) {
13
+ console.error("package.json not found; script root resolution failed");
14
+ process.exit(1);
15
+ }
16
+
17
+ run("npm", ["run", "typecheck"]);
18
+ run("npm", ["test"]);
19
+ ensureNpmLogin();
20
+ run("npm", ["publish", "--access", "public", "--registry", registry, "--auth-type", "web"]);
21
+
22
+ function ensureNpmLogin() {
23
+ const whoami = run("npm", ["whoami", "--registry", registry], { exitOnError: false });
24
+ if (whoami.status === 0) return;
25
+ console.log("\nNot logged in to npm. Starting web login...");
26
+ run("npm", ["login", "--registry", registry, "--auth-type", "web"]);
27
+ }
28
+
29
+ function run(command, args, options = {}) {
30
+ const result = spawnSync(command, args, { stdio: "inherit", shell: process.platform === "win32" });
31
+ if (result.error) throw result.error;
32
+ if (result.status !== 0 && options.exitOnError !== false) process.exit(result.status ?? 1);
33
+ return result;
34
+ }