claude-session-backup 0.2.1__py3-none-any.whl
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.
- claude_session_backup/__init__.py +29 -0
- claude_session_backup/__main__.py +6 -0
- claude_session_backup/_version.py +83 -0
- claude_session_backup/cli.py +252 -0
- claude_session_backup/commands.py +526 -0
- claude_session_backup/config.py +108 -0
- claude_session_backup/git_ops.py +262 -0
- claude_session_backup/index.py +364 -0
- claude_session_backup/lockfile.py +74 -0
- claude_session_backup/metadata.py +157 -0
- claude_session_backup/scanner.py +251 -0
- claude_session_backup/timeline.py +342 -0
- claude_session_backup-0.2.1.dist-info/METADATA +228 -0
- claude_session_backup-0.2.1.dist-info/RECORD +18 -0
- claude_session_backup-0.2.1.dist-info/WHEEL +5 -0
- claude_session_backup-0.2.1.dist-info/entry_points.txt +2 -0
- claude_session_backup-0.2.1.dist-info/licenses/LICENSE +674 -0
- claude_session_backup-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
claude-session-backup - Git-backed Claude Code session backup tool.
|
|
3
|
+
|
|
4
|
+
Provides automated backup of Claude Code sessions with:
|
|
5
|
+
- Full session data preservation via git commits
|
|
6
|
+
- SQLite metadata index for fast timeline/search queries
|
|
7
|
+
- Deletion detection when Claude Code removes sessions
|
|
8
|
+
- Session restore from git history
|
|
9
|
+
- Working directory analysis per session
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
csb backup # scan, index, git commit
|
|
13
|
+
csb list # timeline view sorted by last-used
|
|
14
|
+
csb status # summary of sessions, deletions, git state
|
|
15
|
+
csb show <session-id> # detailed session info
|
|
16
|
+
csb restore <session-id> # restore deleted session from git
|
|
17
|
+
csb search "query" # search session metadata
|
|
18
|
+
csb rebuild-index # reconstruct SQLite from git history
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._version import __version__, get_version, get_base_version, VERSION, BASE_VERSION
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"get_version",
|
|
26
|
+
"get_base_version",
|
|
27
|
+
"VERSION",
|
|
28
|
+
"BASE_VERSION",
|
|
29
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version information for claude-session-backup.
|
|
3
|
+
|
|
4
|
+
This file is the canonical source for version numbers.
|
|
5
|
+
The __version__ string is automatically updated by git hooks
|
|
6
|
+
with build metadata (branch, build number, date, commit hash).
|
|
7
|
+
|
|
8
|
+
Format: MAJOR.MINOR.PATCH[-PHASE]_BRANCH_BUILD-YYYYMMDD-COMMITHASH
|
|
9
|
+
Example: 0.1.0_main_1-20260323-a1b2c3d4
|
|
10
|
+
|
|
11
|
+
To sync versions: python scripts/sync-versions.py
|
|
12
|
+
To bump version: python scripts/sync-versions.py --bump patch
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Version components - edit these for version bumps
|
|
16
|
+
MAJOR = 0
|
|
17
|
+
MINOR = 2
|
|
18
|
+
PATCH = 1
|
|
19
|
+
PHASE = "" # Per-MINOR feature set: None, "alpha", "beta", "rc1", etc.
|
|
20
|
+
PRE_RELEASE_NUM = 1 # PEP 440 pre-release number (e.g., a1, b2)
|
|
21
|
+
PROJECT_PHASE = "prealpha" # Project-wide: "prealpha", "alpha", "beta", "stable"
|
|
22
|
+
|
|
23
|
+
# Auto-updated by git hooks - do not edit manually
|
|
24
|
+
__version__ = "0.2.1"
|
|
25
|
+
__app_name__ = "claude-session-backup"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_version():
|
|
29
|
+
"""Return the full version string including branch and build info."""
|
|
30
|
+
return __version__
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_display_version():
|
|
34
|
+
"""Return a human-friendly version string with project phase."""
|
|
35
|
+
base = get_base_version()
|
|
36
|
+
if PROJECT_PHASE and PROJECT_PHASE != "stable":
|
|
37
|
+
return f"{PROJECT_PHASE.upper()} {base}"
|
|
38
|
+
return base
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_base_version():
|
|
42
|
+
"""Return the semantic version string (MAJOR.MINOR.PATCH[-PHASE])."""
|
|
43
|
+
if "_" in __version__:
|
|
44
|
+
return __version__.split("_")[0]
|
|
45
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
46
|
+
if PHASE:
|
|
47
|
+
base = f"{base}-{PHASE}"
|
|
48
|
+
return base
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_pip_version():
|
|
52
|
+
"""
|
|
53
|
+
Return PEP 440 compliant version for pip/setuptools.
|
|
54
|
+
|
|
55
|
+
Converts our version format to PEP 440:
|
|
56
|
+
- Main branch: 0.1.0-alpha_main_6-20260321-hash -> 0.1.0a1
|
|
57
|
+
- Dev branch: 0.1.0-alpha_dev_6-20260321-hash -> 0.1.0a1.dev6
|
|
58
|
+
"""
|
|
59
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
60
|
+
|
|
61
|
+
phase_map = {"alpha": f"a{PRE_RELEASE_NUM}", "beta": f"b{PRE_RELEASE_NUM}"}
|
|
62
|
+
if PHASE:
|
|
63
|
+
base += phase_map.get(PHASE, PHASE)
|
|
64
|
+
|
|
65
|
+
if "_" not in __version__:
|
|
66
|
+
return base
|
|
67
|
+
|
|
68
|
+
parts = __version__.split("_")
|
|
69
|
+
branch = parts[1] if len(parts) > 1 else "unknown"
|
|
70
|
+
|
|
71
|
+
if branch == "main":
|
|
72
|
+
return base
|
|
73
|
+
else:
|
|
74
|
+
build_info = "_".join(parts[2:]) if len(parts) > 2 else ""
|
|
75
|
+
build_num = build_info.split("-")[0] if "-" in build_info else "0"
|
|
76
|
+
return f"{base}.dev{build_num}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# For convenience in imports
|
|
80
|
+
VERSION = get_version()
|
|
81
|
+
BASE_VERSION = get_base_version()
|
|
82
|
+
PIP_VERSION = get_pip_version()
|
|
83
|
+
DISPLAY_VERSION = get_display_version()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for claude-session-backup.
|
|
3
|
+
|
|
4
|
+
Git-backed Claude Code session backup with timeline view, folder analysis,
|
|
5
|
+
deletion detection, and session restore.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
csb backup # scan, index, git commit
|
|
9
|
+
csb list [-n 20] [--deleted] # timeline view sorted by last-used
|
|
10
|
+
csb status # summary of sessions, deletions, git state
|
|
11
|
+
csb show <session-id> # detailed session info with folder analysis
|
|
12
|
+
csb restore <session-id> # restore deleted session from git history
|
|
13
|
+
csb resume <session-id> # launch claude --resume with full UUID
|
|
14
|
+
csb scan [path] # find sessions in current dir and children
|
|
15
|
+
csb search "query" # search session metadata
|
|
16
|
+
csb rebuild-index # reconstruct SQLite from git history
|
|
17
|
+
csb config [key] [value] # view/edit configuration
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from ._version import DISPLAY_VERSION
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Common flags ────────────────────────────────────────────────────
|
|
27
|
+
# Flags like --quiet, --claude-dir, --db work in either position:
|
|
28
|
+
# csb --quiet backup (before subcommand)
|
|
29
|
+
# csb backup --quiet (after subcommand)
|
|
30
|
+
#
|
|
31
|
+
# Implementation: only define on subcommand parsers. In main(), do a
|
|
32
|
+
# pre-parse of the raw argv to extract any global-position flags and
|
|
33
|
+
# inject them into the subcommand's argv before full parsing.
|
|
34
|
+
|
|
35
|
+
_COMMON_FLAGS = {
|
|
36
|
+
"--quiet": {"short": "-q", "action": "store_true", "default": False,
|
|
37
|
+
"help": "Suppress non-error output (for cron)"},
|
|
38
|
+
"--claude-dir": {"default": None,
|
|
39
|
+
"help": "Path to Claude Code directory (default: ~/.claude or $CLAUDE_DIR)"},
|
|
40
|
+
"--db": {"default": None,
|
|
41
|
+
"help": "Path to SQLite index database (default: ~/.claude/session-backup.db or $CLAUDE_SESSION_BACKUP_DB)"},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# All flag strings that are common (for pre-parse extraction)
|
|
45
|
+
_COMMON_FLAG_NAMES = set()
|
|
46
|
+
for flag, spec in _COMMON_FLAGS.items():
|
|
47
|
+
_COMMON_FLAG_NAMES.add(flag)
|
|
48
|
+
if "short" in spec:
|
|
49
|
+
_COMMON_FLAG_NAMES.add(spec["short"])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _add_common_flags(parser):
|
|
53
|
+
"""Add common flags to a subcommand parser."""
|
|
54
|
+
for flag, spec in _COMMON_FLAGS.items():
|
|
55
|
+
kwargs = {k: v for k, v in spec.items() if k != "short"}
|
|
56
|
+
args = [flag]
|
|
57
|
+
if "short" in spec:
|
|
58
|
+
args.append(spec["short"])
|
|
59
|
+
parser.add_argument(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _hoist_common_flags(argv):
|
|
63
|
+
"""
|
|
64
|
+
Move common flags from before the subcommand to after it.
|
|
65
|
+
|
|
66
|
+
Turns: ['--quiet', '--claude-dir', '/foo', 'backup', '--no-commit']
|
|
67
|
+
Into: ['backup', '--quiet', '--claude-dir', '/foo', '--no-commit']
|
|
68
|
+
|
|
69
|
+
This lets argparse handle everything via subcommand parsers only.
|
|
70
|
+
"""
|
|
71
|
+
if argv is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
hoisted = []
|
|
75
|
+
remainder = []
|
|
76
|
+
i = 0
|
|
77
|
+
found_subcommand = False
|
|
78
|
+
|
|
79
|
+
while i < len(argv):
|
|
80
|
+
arg = argv[i]
|
|
81
|
+
|
|
82
|
+
if found_subcommand:
|
|
83
|
+
remainder.append(arg)
|
|
84
|
+
i += 1
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if arg in _COMMON_FLAG_NAMES:
|
|
88
|
+
# Check if this flag takes a value (not store_true)
|
|
89
|
+
flag_key = arg if arg.startswith("--") else None
|
|
90
|
+
if flag_key is None:
|
|
91
|
+
# Short flag like -q -- find its long form
|
|
92
|
+
for long_flag, spec in _COMMON_FLAGS.items():
|
|
93
|
+
if spec.get("short") == arg:
|
|
94
|
+
flag_key = long_flag
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
takes_value = _COMMON_FLAGS.get(flag_key, {}).get("action") != "store_true"
|
|
98
|
+
|
|
99
|
+
hoisted.append(arg)
|
|
100
|
+
i += 1
|
|
101
|
+
if takes_value and i < len(argv):
|
|
102
|
+
hoisted.append(argv[i])
|
|
103
|
+
i += 1
|
|
104
|
+
elif not arg.startswith("-"):
|
|
105
|
+
# This is the subcommand
|
|
106
|
+
found_subcommand = True
|
|
107
|
+
remainder.append(arg)
|
|
108
|
+
i += 1
|
|
109
|
+
else:
|
|
110
|
+
# Unknown flag before subcommand (like --version)
|
|
111
|
+
remainder.append(arg)
|
|
112
|
+
i += 1
|
|
113
|
+
|
|
114
|
+
return remainder + hoisted
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_parser():
|
|
118
|
+
"""Build the argument parser."""
|
|
119
|
+
parser = argparse.ArgumentParser(
|
|
120
|
+
prog="csb",
|
|
121
|
+
description="Git-backed Claude Code session backup tool.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--version", action="version", version=f"%(prog)s {DISPLAY_VERSION}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
sub = parser.add_subparsers(dest="command", help="Available commands")
|
|
128
|
+
|
|
129
|
+
# backup
|
|
130
|
+
p_backup = sub.add_parser("backup", help="Scan sessions, update index, git commit")
|
|
131
|
+
_add_common_flags(p_backup)
|
|
132
|
+
p_backup.add_argument(
|
|
133
|
+
"--no-commit",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="Update index but skip git commit",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# list
|
|
139
|
+
p_list = sub.add_parser("list", help="Timeline view (default sort: last-used)")
|
|
140
|
+
_add_common_flags(p_list)
|
|
141
|
+
p_list.add_argument("filter", nargs="?", default=None, help="Filter by keyword in session name, project, or folder paths (case-insensitive)")
|
|
142
|
+
p_list.add_argument("-n", type=int, default=20, help="Number of sessions to show")
|
|
143
|
+
p_list.add_argument(
|
|
144
|
+
"--sort",
|
|
145
|
+
choices=["last-used", "expiration", "started", "oldest", "messages", "size"],
|
|
146
|
+
default="last-used",
|
|
147
|
+
help="Sort order: last-used (default), expiration (soonest purge first), "
|
|
148
|
+
"started (newest first), oldest (oldest first), messages, size",
|
|
149
|
+
)
|
|
150
|
+
p_list.add_argument("--deleted", action="store_true", help="Show only deleted sessions")
|
|
151
|
+
p_list.add_argument("--all", action="store_true", help="Show all sessions including deleted")
|
|
152
|
+
p_list.add_argument("--json", action="store_true", help="Output as JSON")
|
|
153
|
+
|
|
154
|
+
# status
|
|
155
|
+
p_status = sub.add_parser("status", help="Summary of sessions, deletions, git state")
|
|
156
|
+
_add_common_flags(p_status)
|
|
157
|
+
|
|
158
|
+
# show
|
|
159
|
+
p_show = sub.add_parser("show", help="Detailed session info with folder analysis")
|
|
160
|
+
_add_common_flags(p_show)
|
|
161
|
+
p_show.add_argument("session_id", help="Session ID (prefix match supported)")
|
|
162
|
+
|
|
163
|
+
# restore
|
|
164
|
+
p_restore = sub.add_parser("restore", help="Restore deleted session from git history")
|
|
165
|
+
_add_common_flags(p_restore)
|
|
166
|
+
p_restore.add_argument("session_id", help="Session ID to restore")
|
|
167
|
+
p_restore.add_argument("--dry-run", action="store_true", help="Show what would be restored")
|
|
168
|
+
|
|
169
|
+
# resume
|
|
170
|
+
p_resume = sub.add_parser("resume", help="Launch claude --resume with full UUID")
|
|
171
|
+
_add_common_flags(p_resume)
|
|
172
|
+
p_resume.add_argument("session_id", help="Session ID (prefix match supported)")
|
|
173
|
+
|
|
174
|
+
# scan
|
|
175
|
+
p_scan = sub.add_parser("scan", help="Find sessions in current directory and children")
|
|
176
|
+
_add_common_flags(p_scan)
|
|
177
|
+
p_scan.add_argument("path", nargs="?", default=".", help="Root path to scan (default: current directory)")
|
|
178
|
+
p_scan.add_argument("-n", type=int, default=20, help="Number of sessions to show")
|
|
179
|
+
p_scan.add_argument("--no-usage", "-NU", action="store_true",
|
|
180
|
+
help="Only match by project start folder, skip folder usage search")
|
|
181
|
+
|
|
182
|
+
# search
|
|
183
|
+
p_search = sub.add_parser("search", help="Search session metadata")
|
|
184
|
+
_add_common_flags(p_search)
|
|
185
|
+
p_search.add_argument("query", help="Search query")
|
|
186
|
+
p_search.add_argument("-n", type=int, default=10, help="Max results")
|
|
187
|
+
|
|
188
|
+
# rebuild-index
|
|
189
|
+
p_rebuild = sub.add_parser("rebuild-index", help="Reconstruct SQLite index from git history")
|
|
190
|
+
_add_common_flags(p_rebuild)
|
|
191
|
+
|
|
192
|
+
# config
|
|
193
|
+
p_config = sub.add_parser("config", help="View/edit configuration")
|
|
194
|
+
_add_common_flags(p_config)
|
|
195
|
+
p_config.add_argument("key", nargs="?", help="Config key to get/set")
|
|
196
|
+
p_config.add_argument("value", nargs="?", help="Value to set")
|
|
197
|
+
|
|
198
|
+
return parser
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main(argv=None):
|
|
202
|
+
"""Entry point for csb CLI."""
|
|
203
|
+
# Hoist common flags from before the subcommand to after it.
|
|
204
|
+
# This makes `csb --quiet backup` work the same as `csb backup --quiet`.
|
|
205
|
+
if argv is None:
|
|
206
|
+
argv = sys.argv[1:]
|
|
207
|
+
argv = _hoist_common_flags(argv)
|
|
208
|
+
|
|
209
|
+
parser = build_parser()
|
|
210
|
+
args = parser.parse_args(argv)
|
|
211
|
+
|
|
212
|
+
if args.command is None:
|
|
213
|
+
parser.print_help()
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
# Import handlers lazily to keep startup fast
|
|
217
|
+
if args.command == "backup":
|
|
218
|
+
from .commands import cmd_backup
|
|
219
|
+
return cmd_backup(args)
|
|
220
|
+
elif args.command == "list":
|
|
221
|
+
from .commands import cmd_list
|
|
222
|
+
return cmd_list(args)
|
|
223
|
+
elif args.command == "status":
|
|
224
|
+
from .commands import cmd_status
|
|
225
|
+
return cmd_status(args)
|
|
226
|
+
elif args.command == "show":
|
|
227
|
+
from .commands import cmd_show
|
|
228
|
+
return cmd_show(args)
|
|
229
|
+
elif args.command == "restore":
|
|
230
|
+
from .commands import cmd_restore
|
|
231
|
+
return cmd_restore(args)
|
|
232
|
+
elif args.command == "resume":
|
|
233
|
+
from .commands import cmd_resume
|
|
234
|
+
return cmd_resume(args)
|
|
235
|
+
elif args.command == "scan":
|
|
236
|
+
from .commands import cmd_scan
|
|
237
|
+
return cmd_scan(args)
|
|
238
|
+
elif args.command == "search":
|
|
239
|
+
from .commands import cmd_search
|
|
240
|
+
return cmd_search(args)
|
|
241
|
+
elif args.command == "rebuild-index":
|
|
242
|
+
from .commands import cmd_rebuild_index
|
|
243
|
+
return cmd_rebuild_index(args)
|
|
244
|
+
elif args.command == "config":
|
|
245
|
+
from .commands import cmd_config
|
|
246
|
+
return cmd_config(args)
|
|
247
|
+
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
sys.exit(main() or 0)
|