sessionsteward 0.1.0__tar.gz
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.
- sessionsteward-0.1.0/LICENSE +21 -0
- sessionsteward-0.1.0/PKG-INFO +130 -0
- sessionsteward-0.1.0/README.md +117 -0
- sessionsteward-0.1.0/pyproject.toml +20 -0
- sessionsteward-0.1.0/sessionsteward/__init__.py +0 -0
- sessionsteward-0.1.0/sessionsteward/__main__.py +4 -0
- sessionsteward-0.1.0/sessionsteward/cli.py +244 -0
- sessionsteward-0.1.0/sessionsteward/core.py +468 -0
- sessionsteward-0.1.0/sessionsteward/diet.py +86 -0
- sessionsteward-0.1.0/sessionsteward.egg-info/PKG-INFO +130 -0
- sessionsteward-0.1.0/sessionsteward.egg-info/SOURCES.txt +14 -0
- sessionsteward-0.1.0/sessionsteward.egg-info/dependency_links.txt +1 -0
- sessionsteward-0.1.0/sessionsteward.egg-info/entry_points.txt +2 -0
- sessionsteward-0.1.0/sessionsteward.egg-info/top_level.txt +1 -0
- sessionsteward-0.1.0/setup.cfg +4 -0
- sessionsteward-0.1.0/tests/test_steward.py +452 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Calas Automations Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sessionsteward
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Versioned snapshots for agent working state: instant undo for CLAUDE.md and friends.
|
|
5
|
+
Author: Calas Automations Inc.
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/calasdan-sketch/sessionsteward
|
|
8
|
+
Project-URL: Issues, https://github.com/calasdan-sketch/sessionsteward/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Session Steward (`sessionsteward`)
|
|
15
|
+
|
|
16
|
+
Versioned snapshots for agent working state. Coding agents silently
|
|
17
|
+
degrade: compaction drops constraints, and one "remember this" can
|
|
18
|
+
overwrite your 30-line CLAUDE.md with a single bullet — no warning, no
|
|
19
|
+
undo. sessionsteward git-versions your agent config + memory files on
|
|
20
|
+
every change: instant undo, pre-compaction snapshots, and a context-diet
|
|
21
|
+
report showing what's eating the window.
|
|
22
|
+
|
|
23
|
+
**Scope:** session *ops* (snapshots, undo, compaction guard) — not memory
|
|
24
|
+
*storage*. That boundary stays clean.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Zero dependencies — Python 3.10+ stdlib only (uses your system's git).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone <repo> && cd sessionsteward
|
|
32
|
+
# no-install alias (run from the repo root):
|
|
33
|
+
alias sessionsteward="python3 -m sessionsteward"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or install it properly (console script `sessionsteward`):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
sessionsteward init # set up the snapshots repo
|
|
46
|
+
sessionsteward watch --add NOTES.md ~/x.md # watch extra paths
|
|
47
|
+
|
|
48
|
+
sessionsteward snap -m "before refactor" # snapshot now
|
|
49
|
+
sessionsteward precompact # snapshot before compaction
|
|
50
|
+
sessionsteward watch # daemon: auto-snapshot on change
|
|
51
|
+
|
|
52
|
+
sessionsteward log # list snapshots (this project only)
|
|
53
|
+
sessionsteward diff # what changed (e.g. what compaction dropped)
|
|
54
|
+
sessionsteward undo # restore the previous snapshot
|
|
55
|
+
sessionsteward restore <sha> # restore a specific snapshot
|
|
56
|
+
sessionsteward diet # what's eating the context window
|
|
57
|
+
sessionsteward install-hooks # print Claude Code hooks JSON
|
|
58
|
+
sessionsteward install-hooks --write # merge it into ~/.claude/settings.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The horror-story scenario, fixed:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
$ sessionsteward snap -m "before letting the agent edit memory"
|
|
65
|
+
$ # agent overwrites your 30-line CLAUDE.md with one bullet
|
|
66
|
+
$ sessionsteward undo
|
|
67
|
+
undone 1 step(s) -> a3f9c21
|
|
68
|
+
/home/you/proj/CLAUDE.md
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Safety rules: empty snaps are skipped (no noise commits); `undo`/`restore`
|
|
72
|
+
never delete — files that didn't exist in the target snapshot are moved to
|
|
73
|
+
`~/.sessionsteward/trash/` instead, **directories included**.
|
|
74
|
+
`log`/`diff`/`undo`/`restore` are scoped to the current project (`--root`,
|
|
75
|
+
default: cwd): snapshots record their project root, and a `restore` of a
|
|
76
|
+
snapshot from another project is refused rather than clobbering your files.
|
|
77
|
+
Undoing (or restoring) with unsaved changes first snapshots the current
|
|
78
|
+
state as `pre-undo`/`pre-restore`, so every undo is reversible — redo is
|
|
79
|
+
`restore` of that snapshot.
|
|
80
|
+
|
|
81
|
+
Ignored on every snap and by the watch daemon: `*.log`, `*transcript*`,
|
|
82
|
+
`__pycache__`, `.git`, `node_modules` (transcripts would otherwise balloon
|
|
83
|
+
the snapshots repo).
|
|
84
|
+
|
|
85
|
+
## Claude Code hooks
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
sessionsteward install-hooks # print the hooks JSON + where it goes
|
|
89
|
+
sessionsteward install-hooks --write # merge into ~/.claude/settings.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This wires **PreCompact** → `sessionsteward precompact` and **SessionStart**
|
|
93
|
+
→ `sessionsteward snap -m "session start"`. `--write` merges into the
|
|
94
|
+
existing settings file without clobbering your other hooks (re-running is
|
|
95
|
+
a no-op).
|
|
96
|
+
|
|
97
|
+
## Context diet
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
# Context diet
|
|
101
|
+
|
|
102
|
+
Watched files: 3, total ~12,400 chars (~3,100 tokens)
|
|
103
|
+
|
|
104
|
+
## Biggest files
|
|
105
|
+
| file | chars | lines |
|
|
106
|
+
...
|
|
107
|
+
## Duplicated content (4 lines in 2+ files)
|
|
108
|
+
- _Be helpful and concise in all responses_ …
|
|
109
|
+
- `/proj/CLAUDE.md`
|
|
110
|
+
- `/proj/.claude/settings.json`
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Suggested hook wiring
|
|
114
|
+
|
|
115
|
+
- **Pre-compaction:** run `sessionsteward precompact` (or add it to your
|
|
116
|
+
agent's pre-compact hook) — then `sessionsteward diff` afterwards to see
|
|
117
|
+
exactly what the compaction dropped.
|
|
118
|
+
- **Session start:** `sessionsteward snap -m "session start"`.
|
|
119
|
+
- **Memory writes:** point the steward at the same files your agent's
|
|
120
|
+
memory hook writes.
|
|
121
|
+
|
|
122
|
+
## Tests
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python3 -m unittest discover -s tests # 24 tests, temp dirs only
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
TBD
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Session Steward (`sessionsteward`)
|
|
2
|
+
|
|
3
|
+
Versioned snapshots for agent working state. Coding agents silently
|
|
4
|
+
degrade: compaction drops constraints, and one "remember this" can
|
|
5
|
+
overwrite your 30-line CLAUDE.md with a single bullet — no warning, no
|
|
6
|
+
undo. sessionsteward git-versions your agent config + memory files on
|
|
7
|
+
every change: instant undo, pre-compaction snapshots, and a context-diet
|
|
8
|
+
report showing what's eating the window.
|
|
9
|
+
|
|
10
|
+
**Scope:** session *ops* (snapshots, undo, compaction guard) — not memory
|
|
11
|
+
*storage*. That boundary stays clean.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Zero dependencies — Python 3.10+ stdlib only (uses your system's git).
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone <repo> && cd sessionsteward
|
|
19
|
+
# no-install alias (run from the repo root):
|
|
20
|
+
alias sessionsteward="python3 -m sessionsteward"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install it properly (console script `sessionsteward`):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
sessionsteward init # set up the snapshots repo
|
|
33
|
+
sessionsteward watch --add NOTES.md ~/x.md # watch extra paths
|
|
34
|
+
|
|
35
|
+
sessionsteward snap -m "before refactor" # snapshot now
|
|
36
|
+
sessionsteward precompact # snapshot before compaction
|
|
37
|
+
sessionsteward watch # daemon: auto-snapshot on change
|
|
38
|
+
|
|
39
|
+
sessionsteward log # list snapshots (this project only)
|
|
40
|
+
sessionsteward diff # what changed (e.g. what compaction dropped)
|
|
41
|
+
sessionsteward undo # restore the previous snapshot
|
|
42
|
+
sessionsteward restore <sha> # restore a specific snapshot
|
|
43
|
+
sessionsteward diet # what's eating the context window
|
|
44
|
+
sessionsteward install-hooks # print Claude Code hooks JSON
|
|
45
|
+
sessionsteward install-hooks --write # merge it into ~/.claude/settings.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The horror-story scenario, fixed:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
$ sessionsteward snap -m "before letting the agent edit memory"
|
|
52
|
+
$ # agent overwrites your 30-line CLAUDE.md with one bullet
|
|
53
|
+
$ sessionsteward undo
|
|
54
|
+
undone 1 step(s) -> a3f9c21
|
|
55
|
+
/home/you/proj/CLAUDE.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Safety rules: empty snaps are skipped (no noise commits); `undo`/`restore`
|
|
59
|
+
never delete — files that didn't exist in the target snapshot are moved to
|
|
60
|
+
`~/.sessionsteward/trash/` instead, **directories included**.
|
|
61
|
+
`log`/`diff`/`undo`/`restore` are scoped to the current project (`--root`,
|
|
62
|
+
default: cwd): snapshots record their project root, and a `restore` of a
|
|
63
|
+
snapshot from another project is refused rather than clobbering your files.
|
|
64
|
+
Undoing (or restoring) with unsaved changes first snapshots the current
|
|
65
|
+
state as `pre-undo`/`pre-restore`, so every undo is reversible — redo is
|
|
66
|
+
`restore` of that snapshot.
|
|
67
|
+
|
|
68
|
+
Ignored on every snap and by the watch daemon: `*.log`, `*transcript*`,
|
|
69
|
+
`__pycache__`, `.git`, `node_modules` (transcripts would otherwise balloon
|
|
70
|
+
the snapshots repo).
|
|
71
|
+
|
|
72
|
+
## Claude Code hooks
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
sessionsteward install-hooks # print the hooks JSON + where it goes
|
|
76
|
+
sessionsteward install-hooks --write # merge into ~/.claude/settings.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This wires **PreCompact** → `sessionsteward precompact` and **SessionStart**
|
|
80
|
+
→ `sessionsteward snap -m "session start"`. `--write` merges into the
|
|
81
|
+
existing settings file without clobbering your other hooks (re-running is
|
|
82
|
+
a no-op).
|
|
83
|
+
|
|
84
|
+
## Context diet
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
# Context diet
|
|
88
|
+
|
|
89
|
+
Watched files: 3, total ~12,400 chars (~3,100 tokens)
|
|
90
|
+
|
|
91
|
+
## Biggest files
|
|
92
|
+
| file | chars | lines |
|
|
93
|
+
...
|
|
94
|
+
## Duplicated content (4 lines in 2+ files)
|
|
95
|
+
- _Be helpful and concise in all responses_ …
|
|
96
|
+
- `/proj/CLAUDE.md`
|
|
97
|
+
- `/proj/.claude/settings.json`
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Suggested hook wiring
|
|
101
|
+
|
|
102
|
+
- **Pre-compaction:** run `sessionsteward precompact` (or add it to your
|
|
103
|
+
agent's pre-compact hook) — then `sessionsteward diff` afterwards to see
|
|
104
|
+
exactly what the compaction dropped.
|
|
105
|
+
- **Session start:** `sessionsteward snap -m "session start"`.
|
|
106
|
+
- **Memory writes:** point the steward at the same files your agent's
|
|
107
|
+
memory hook writes.
|
|
108
|
+
|
|
109
|
+
## Tests
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python3 -m unittest discover -s tests # 24 tests, temp dirs only
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
TBD
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sessionsteward"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Versioned snapshots for agent working state: instant undo for CLAUDE.md and friends."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{name = "Calas Automations Inc."}]
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/calasdan-sketch/sessionsteward"
|
|
17
|
+
Issues = "https://github.com/calasdan-sketch/sessionsteward/issues"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
sessionsteward = "sessionsteward.cli:main"
|
|
File without changes
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Session steward: versioned snapshots for agent working state.
|
|
2
|
+
|
|
3
|
+
Coding agents silently degrade: compaction drops constraints, and one
|
|
4
|
+
"remember this" can overwrite your 30-line CLAUDE.md with a single
|
|
5
|
+
bullet — no warning, no undo. sessionsteward git-versions your agent
|
|
6
|
+
config + memory files on every change. Instant undo, pre-compaction
|
|
7
|
+
snapshots, and a context-diet report.
|
|
8
|
+
|
|
9
|
+
Scope note: this is *session ops* (snapshots, undo, compaction guard).
|
|
10
|
+
Memory *storage* is OneMemory's job — the boundary stays clean.
|
|
11
|
+
|
|
12
|
+
sessionsteward init # set up the snapshots repo
|
|
13
|
+
sessionsteward watch --add CLAUDE.md # watch extra paths (also: ~/..., /abs/...)
|
|
14
|
+
sessionsteward snap -m "before refactor"
|
|
15
|
+
sessionsteward precompact # snapshot before compaction
|
|
16
|
+
sessionsteward log # list snapshots
|
|
17
|
+
sessionsteward diff [sha1] [sha2] # what changed (e.g. what compaction dropped)
|
|
18
|
+
sessionsteward undo # restore the previous snapshot
|
|
19
|
+
sessionsteward restore <sha> # restore a specific snapshot
|
|
20
|
+
sessionsteward diet # what's eating the context window
|
|
21
|
+
sessionsteward install-hooks [--write] # Claude Code hooks JSON
|
|
22
|
+
sessionsteward watch # daemon: auto-snapshot on change
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
|
|
33
|
+
from . import core as core_mod
|
|
34
|
+
from . import diet as diet_mod
|
|
35
|
+
|
|
36
|
+
VERSION = "0.1.0"
|
|
37
|
+
|
|
38
|
+
CLAUDE_SETTINGS = os.path.expanduser("~/.claude/settings.json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_init(args) -> int:
|
|
42
|
+
repo = core_mod.init_repo()
|
|
43
|
+
print(f"snapshots repo: {repo}")
|
|
44
|
+
print("watching (defaults): " + ", ".join(core_mod.DEFAULT_WATCH))
|
|
45
|
+
print("add more: sessionsteward watch --add <path>")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_watch_add(args) -> int:
|
|
50
|
+
core_mod.init_repo()
|
|
51
|
+
cfg = core_mod.load_config()
|
|
52
|
+
for p in args.add:
|
|
53
|
+
if p not in cfg["watch"]:
|
|
54
|
+
cfg["watch"].append(p)
|
|
55
|
+
core_mod.save_config(cfg)
|
|
56
|
+
print("watching: " + ", ".join(cfg["watch"] or core_mod.DEFAULT_WATCH))
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_snap(args) -> int:
|
|
61
|
+
sha = core_mod.snap(args.root, args.message or "manual snapshot")
|
|
62
|
+
print(f"snapshot {sha[:7]}")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_precompact(args) -> int:
|
|
67
|
+
sha = core_mod.snap(args.root, "pre-compaction snapshot")
|
|
68
|
+
print(f"pre-compaction snapshot {sha[:7]} — "
|
|
69
|
+
f"`sessionsteward diff {sha[:7]}` after, to see what was dropped")
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_log(args) -> int:
|
|
74
|
+
if not core_mod.repo_ready():
|
|
75
|
+
print("no snapshots yet — run `sessionsteward init` first")
|
|
76
|
+
return 0
|
|
77
|
+
for s in core_mod.log(args.limit, project_root=args.root):
|
|
78
|
+
print(f"{s['sha'][:7]} {s['date'][:16]} {s['message']}")
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_diff(args) -> int:
|
|
83
|
+
if not core_mod.repo_ready():
|
|
84
|
+
print("no snapshots yet")
|
|
85
|
+
return 2
|
|
86
|
+
snaps = core_mod.log(50, project_root=args.root)
|
|
87
|
+
sha1 = args.sha1 or (snaps[1]["sha"] if len(snaps) > 1 else None)
|
|
88
|
+
sha2 = args.sha2 or "HEAD"
|
|
89
|
+
if not sha1:
|
|
90
|
+
print("only one snapshot — nothing to diff")
|
|
91
|
+
return 0
|
|
92
|
+
out = core_mod.diff(sha1, sha2)
|
|
93
|
+
kept = [ln for ln in out.splitlines() if ln[:1] in "+-"
|
|
94
|
+
and not ln.startswith(("+++", "---"))]
|
|
95
|
+
sys.stdout.write("\n".join(kept[:200]) + ("\n" if kept else "(no changes)\n"))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_undo(args) -> int:
|
|
100
|
+
if not core_mod.repo_ready():
|
|
101
|
+
print("no snapshots yet")
|
|
102
|
+
return 2
|
|
103
|
+
snaps = core_mod.log(50, project_root=args.root)
|
|
104
|
+
if not snaps:
|
|
105
|
+
print("nothing to restore — no snapshots yet")
|
|
106
|
+
return 1
|
|
107
|
+
if core_mod.worktree_matches_snapshot(snaps[0]["sha"], args.root):
|
|
108
|
+
# Clean state: the current state is already snapshotted, so undo
|
|
109
|
+
# needs no pre-snap and redo is just `restore` of snaps[0].
|
|
110
|
+
if len(snaps) < 2:
|
|
111
|
+
print("only one snapshot — nothing to undo to")
|
|
112
|
+
return 0
|
|
113
|
+
target = snaps[min(args.steps, len(snaps) - 1)]["sha"]
|
|
114
|
+
else:
|
|
115
|
+
# Dirty state: snapshot it first ("pre-undo") so undo is
|
|
116
|
+
# reversible — redo is `restore` of the pre-undo snapshot.
|
|
117
|
+
core_mod.snap(args.root, "pre-undo")
|
|
118
|
+
snaps = core_mod.log(50, project_root=args.root)
|
|
119
|
+
target = snaps[min(args.steps, len(snaps) - 1)]["sha"]
|
|
120
|
+
try:
|
|
121
|
+
restored = core_mod.restore(target, project_root=args.root)
|
|
122
|
+
except core_mod.NothingToRestoreError as e:
|
|
123
|
+
print(e)
|
|
124
|
+
return 1
|
|
125
|
+
except core_mod.CrossProjectError as e:
|
|
126
|
+
print(f"error: {e}")
|
|
127
|
+
return 1
|
|
128
|
+
print(f"undone {args.steps} step(s) -> {target[:7]}")
|
|
129
|
+
for r in restored:
|
|
130
|
+
print(f" {r}")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_restore(args) -> int:
|
|
135
|
+
if not core_mod.repo_ready():
|
|
136
|
+
print("no snapshots yet")
|
|
137
|
+
return 2
|
|
138
|
+
# Validate before the pre-restore snap so a bad sha doesn't pollute log.
|
|
139
|
+
try:
|
|
140
|
+
core_mod.check_restore(args.sha, args.root)
|
|
141
|
+
except core_mod.NothingToRestoreError as e:
|
|
142
|
+
print(e)
|
|
143
|
+
return 1
|
|
144
|
+
except core_mod.CrossProjectError as e:
|
|
145
|
+
print(f"error: {e}")
|
|
146
|
+
return 1
|
|
147
|
+
# Redo support: if the current state isn't snapshotted yet, snapshot
|
|
148
|
+
# it first ("pre-restore") so the restore is reversible.
|
|
149
|
+
snaps = core_mod.log(50, project_root=args.root)
|
|
150
|
+
if snaps and not core_mod.worktree_matches_snapshot(snaps[0]["sha"],
|
|
151
|
+
args.root):
|
|
152
|
+
core_mod.snap(args.root, "pre-restore")
|
|
153
|
+
restored = core_mod.restore(args.sha, project_root=args.root)
|
|
154
|
+
print(f"restored {args.sha[:7]}")
|
|
155
|
+
for r in restored:
|
|
156
|
+
print(f" {r}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_diet(args) -> int:
|
|
161
|
+
print(diet_mod.render(diet_mod.analyze(args.root)))
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_install_hooks(args) -> int:
|
|
166
|
+
payload = core_mod.build_hooks_json()
|
|
167
|
+
print(json.dumps(payload, indent=2))
|
|
168
|
+
print()
|
|
169
|
+
print("Copy the JSON above into the \"hooks\" section of "
|
|
170
|
+
"~/.claude/settings.json, or re-run with --write to merge it "
|
|
171
|
+
"automatically (existing settings are preserved).")
|
|
172
|
+
print("Wiring: PreCompact -> `sessionsteward precompact`, "
|
|
173
|
+
"SessionStart -> `sessionsteward snap -m \"session start\"`.")
|
|
174
|
+
if args.write:
|
|
175
|
+
path = core_mod.write_hooks_file(CLAUDE_SETTINGS)
|
|
176
|
+
print(f"merged hooks into {path}")
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def cmd_watch(args) -> int:
|
|
181
|
+
core_mod.init_repo()
|
|
182
|
+
print(f"watching for changes (every {args.interval}s, Ctrl-C to stop)…")
|
|
183
|
+
mtimes = core_mod.file_mtimes(core_mod.watched_paths(args.root))
|
|
184
|
+
try:
|
|
185
|
+
while True:
|
|
186
|
+
time.sleep(args.interval)
|
|
187
|
+
now = core_mod.file_mtimes(core_mod.watched_paths(args.root))
|
|
188
|
+
if now != mtimes:
|
|
189
|
+
sha = core_mod.snap(args.root, "auto: files changed")
|
|
190
|
+
print(f"change detected -> snapshot {sha[:7]}")
|
|
191
|
+
mtimes = now
|
|
192
|
+
except KeyboardInterrupt:
|
|
193
|
+
print("\nstopped.")
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
198
|
+
p = argparse.ArgumentParser(prog="sessionsteward", description=__doc__)
|
|
199
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {VERSION}")
|
|
200
|
+
p.add_argument("--root", default=os.getcwd(),
|
|
201
|
+
help="Project root for relative watch paths.")
|
|
202
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
203
|
+
|
|
204
|
+
sub.add_parser("init", help="Set up the snapshots repo.")
|
|
205
|
+
wa = sub.add_parser("watch", help="Watch daemon / manage watch list.")
|
|
206
|
+
wa.add_argument("--add", nargs="+", default=None,
|
|
207
|
+
help="Add paths to the watch list (then exit).")
|
|
208
|
+
wa.add_argument("--interval", type=float, default=5.0,
|
|
209
|
+
help="Poll seconds for the daemon.")
|
|
210
|
+
sn = sub.add_parser("snap", help="Snapshot now.")
|
|
211
|
+
sn.add_argument("-m", "--message", default=None)
|
|
212
|
+
sub.add_parser("precompact", help="Snapshot before compaction.")
|
|
213
|
+
lg = sub.add_parser("log", help="List snapshots.")
|
|
214
|
+
lg.add_argument("--limit", type=int, default=20)
|
|
215
|
+
df = sub.add_parser("diff", help="Diff two snapshots.")
|
|
216
|
+
df.add_argument("sha1", nargs="?")
|
|
217
|
+
df.add_argument("sha2", nargs="?", default="HEAD")
|
|
218
|
+
un = sub.add_parser("undo", help="Restore the previous snapshot.")
|
|
219
|
+
un.add_argument("--steps", type=int, default=1)
|
|
220
|
+
rs = sub.add_parser("restore", help="Restore a specific snapshot.")
|
|
221
|
+
rs.add_argument("sha")
|
|
222
|
+
sub.add_parser("diet", help="Context-diet report.")
|
|
223
|
+
ih = sub.add_parser("install-hooks",
|
|
224
|
+
help="Print (or --write) Claude Code hooks JSON.")
|
|
225
|
+
ih.add_argument("--write", action="store_true",
|
|
226
|
+
help="Merge the hooks into ~/.claude/settings.json.")
|
|
227
|
+
return p
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main(argv=None) -> int:
|
|
231
|
+
args = build_parser().parse_args(argv)
|
|
232
|
+
if args.cmd == "watch" and args.add:
|
|
233
|
+
return cmd_watch_add(args)
|
|
234
|
+
cmds = {
|
|
235
|
+
"init": cmd_init, "watch": cmd_watch, "snap": cmd_snap,
|
|
236
|
+
"precompact": cmd_precompact, "log": cmd_log, "diff": cmd_diff,
|
|
237
|
+
"undo": cmd_undo, "restore": cmd_restore, "diet": cmd_diet,
|
|
238
|
+
"install-hooks": cmd_install_hooks,
|
|
239
|
+
}
|
|
240
|
+
return cmds[args.cmd](args)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
sys.exit(main())
|