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.
@@ -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,6 @@
1
+ """Allow running claude-session-backup as a module: python -m claude_session_backup"""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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)