claude-session-backup 0.4.2__tar.gz → 0.4.6__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.
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/PKG-INFO +2 -2
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/README.md +1 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/_version.py +2 -2
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/cli.py +61 -5
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/commands.py +100 -50
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/config.py +12 -5
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts5_db.py +9 -9
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts5_importer.py +1 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts5_index.py +7 -5
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts_paths.py +15 -16
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/git_ops.py +28 -17
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/index.py +2 -2
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/lockfile.py +4 -2
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/migrations.py +5 -6
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/pathkit.py +193 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/scanner.py +13 -10
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/search.py +1 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/sesslog_scanner.py +3 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/transcript_walker.py +2 -2
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup.egg-info/PKG-INFO +2 -2
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup.egg-info/SOURCES.txt +2 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_commands.py +4 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_fts_paths.py +10 -10
- claude_session_backup-0.4.6/tests/test_passthrough.py +152 -0
- claude_session_backup-0.4.6/tests/test_pathkit_layout.py +247 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_restore.py +44 -3
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_session_sources.py +1 -1
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_view.py +3 -3
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/LICENSE +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/__init__.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/__main__.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/distill.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts5_migrations.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/ids.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/metadata.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/search_render.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/sesslog_parser.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/timeline.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup.egg-info/dependency_links.txt +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup.egg-info/entry_points.txt +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup.egg-info/requires.txt +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup.egg-info/top_level.txt +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/pyproject.toml +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/setup.cfg +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_backup_hook.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_cli.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_config.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_distill.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_fts5_db.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_fts5_importer.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_fts5_index.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_fts5_migrations.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_hook.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_ids.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_index.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_lockfile.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_metadata.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_migrations.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_pathkit.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_scanner.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_search.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_search_render.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_sesslog_parser.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_sesslog_scanner.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_timeline.py +0 -0
- {claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-session-backup
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Git-backed Claude Code session backup with timeline view, folder analysis, deletion detection, and session restore.
|
|
5
5
|
Author-email: djdarcy <6962246+djdarcy@users.noreply.github.com>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -246,7 +246,7 @@ Like the project?
|
|
|
246
246
|
|
|
247
247
|
## Acknowledgements
|
|
248
248
|
|
|
249
|
-
- [claude-vault](https://github.com/kuroko1t/claude-vault) by [@kuroko1t](https://github.com/kuroko1t) --
|
|
249
|
+
- [claude-vault](https://github.com/kuroko1t/claude-vault) by [@kuroko1t](https://github.com/kuroko1t) -- Serendipitously started development on `csb` a week or so before [kuroko1t's blog post](https://dev.to/kuroko1t/i-built-a-tool-to-stop-losing-my-claude-code-conversation-history-5500) laying out the problem.
|
|
250
250
|
- [claude-code-history-viewer](https://github.com/jhlee0409/claude-code-history-viewer) by [@jhlee0409](https://github.com/jhlee0409) - GUI session reader that `csb view` launches.
|
|
251
251
|
|
|
252
252
|
## License
|
|
@@ -214,7 +214,7 @@ Like the project?
|
|
|
214
214
|
|
|
215
215
|
## Acknowledgements
|
|
216
216
|
|
|
217
|
-
- [claude-vault](https://github.com/kuroko1t/claude-vault) by [@kuroko1t](https://github.com/kuroko1t) --
|
|
217
|
+
- [claude-vault](https://github.com/kuroko1t/claude-vault) by [@kuroko1t](https://github.com/kuroko1t) -- Serendipitously started development on `csb` a week or so before [kuroko1t's blog post](https://dev.to/kuroko1t/i-built-a-tool-to-stop-losing-my-claude-code-conversation-history-5500) laying out the problem.
|
|
218
218
|
- [claude-code-history-viewer](https://github.com/jhlee0409/claude-code-history-viewer) by [@jhlee0409](https://github.com/jhlee0409) - GUI session reader that `csb view` launches.
|
|
219
219
|
|
|
220
220
|
## License
|
{claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/_version.py
RENAMED
|
@@ -15,13 +15,13 @@ To bump version: python scripts/sync-versions.py --bump patch
|
|
|
15
15
|
# Version components - edit these for version bumps
|
|
16
16
|
MAJOR = 0
|
|
17
17
|
MINOR = 4
|
|
18
|
-
PATCH =
|
|
18
|
+
PATCH = 6
|
|
19
19
|
PHASE = "" # Per-MINOR feature set: None, "alpha", "beta", "rc1", etc.
|
|
20
20
|
PRE_RELEASE_NUM = 1 # PEP 440 pre-release number (e.g., a1, b2)
|
|
21
21
|
PROJECT_PHASE = "alpha" # Project-wide: "prealpha", "alpha", "beta", "stable"
|
|
22
22
|
|
|
23
23
|
# Auto-updated by git hooks - do not edit manually
|
|
24
|
-
__version__ = "0.4.
|
|
24
|
+
__version__ = "0.4.6_main_56-20260614-54d2a56b"
|
|
25
25
|
__app_name__ = "claude-session-backup"
|
|
26
26
|
|
|
27
27
|
|
|
@@ -53,6 +53,12 @@ for flag, spec in _COMMON_FLAGS.items():
|
|
|
53
53
|
if "short" in spec:
|
|
54
54
|
_COMMON_FLAG_NAMES.add(spec["short"])
|
|
55
55
|
|
|
56
|
+
# Commands that launch a wrapped subtool and therefore accept `--` passthrough
|
|
57
|
+
# (#47): everything after a standalone `--` is forwarded verbatim to the child
|
|
58
|
+
# (resume -> claude, view -> the history viewer). Any other command rejects a
|
|
59
|
+
# passthrough rather than silently dropping it.
|
|
60
|
+
PASSTHROUGH_COMMANDS = {"resume", "view"}
|
|
61
|
+
|
|
56
62
|
|
|
57
63
|
def _add_common_flags(parser):
|
|
58
64
|
"""Add common flags to a subcommand parser."""
|
|
@@ -227,7 +233,19 @@ def build_parser():
|
|
|
227
233
|
)
|
|
228
234
|
|
|
229
235
|
# resume
|
|
230
|
-
p_resume = sub.add_parser(
|
|
236
|
+
p_resume = sub.add_parser(
|
|
237
|
+
"resume",
|
|
238
|
+
help="Launch claude --resume with full UUID",
|
|
239
|
+
description=(
|
|
240
|
+
"Resolve a session, cd to its start folder, and launch\n"
|
|
241
|
+
"`claude --resume <uuid>`. Forward extra args to claude after `--`:\n\n"
|
|
242
|
+
" csb resume <query> resume it\n"
|
|
243
|
+
" csb resume <query> -- --fork-session resume + forward --fork-session to claude\n\n"
|
|
244
|
+
"Everything after `--` is passed verbatim to claude (don't re-pass\n"
|
|
245
|
+
"--resume / -r -- csb already supplies it)."
|
|
246
|
+
),
|
|
247
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
248
|
+
)
|
|
231
249
|
_add_common_flags(p_resume)
|
|
232
250
|
p_resume.add_argument(
|
|
233
251
|
"session_id", metavar="query",
|
|
@@ -270,7 +288,9 @@ def build_parser():
|
|
|
270
288
|
"key (csb config viewer_path <path>), then platform install\n"
|
|
271
289
|
"locations. Without a viewer, prints the transcript path.\n"
|
|
272
290
|
"Pruned sessions offer restore-from-git first (same flags as\n"
|
|
273
|
-
"`csb resume`)
|
|
291
|
+
"`csb resume`).\n\n"
|
|
292
|
+
"Forward extra args to the viewer after `--`:\n"
|
|
293
|
+
" csb view <query> -- <viewer-args>"
|
|
274
294
|
),
|
|
275
295
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
276
296
|
)
|
|
@@ -612,9 +632,9 @@ def build_parser():
|
|
|
612
632
|
p_update_rebuild.add_argument(
|
|
613
633
|
"--include-fts5", action="store_true",
|
|
614
634
|
help=(
|
|
615
|
-
"Also
|
|
616
|
-
"
|
|
617
|
-
"
|
|
635
|
+
"Also force-rebuild the per-project FTS5 content indexes after "
|
|
636
|
+
"the index rebuild. Fails soft -- an FTS5 problem warns but never "
|
|
637
|
+
"fails the rebuild."
|
|
618
638
|
),
|
|
619
639
|
)
|
|
620
640
|
p_update_rebuild.add_argument(
|
|
@@ -732,21 +752,57 @@ def build_parser():
|
|
|
732
752
|
return parser
|
|
733
753
|
|
|
734
754
|
|
|
755
|
+
def _split_passthrough(argv):
|
|
756
|
+
"""Split argv at the first standalone ``--`` token (#47).
|
|
757
|
+
|
|
758
|
+
Returns ``(csb_argv, passthrough)`` -- everything before the ``--`` for
|
|
759
|
+
csb's own parsing, everything after it forwarded verbatim to a wrapped
|
|
760
|
+
subtool. No ``--`` -> ``(argv, [])``. Matches only the exact two-char
|
|
761
|
+
token, never ``--db``/``--force``. Must run BEFORE ``_hoist_common_flags``
|
|
762
|
+
so forwarded flags are never hoisted into csb's own options.
|
|
763
|
+
"""
|
|
764
|
+
if "--" in argv:
|
|
765
|
+
i = argv.index("--")
|
|
766
|
+
return list(argv[:i]), list(argv[i + 1:])
|
|
767
|
+
return list(argv), []
|
|
768
|
+
|
|
769
|
+
|
|
735
770
|
def main(argv=None):
|
|
736
771
|
"""Entry point for csb CLI."""
|
|
737
772
|
# Hoist common flags from before the subcommand to after it.
|
|
738
773
|
# This makes `csb --quiet backup` work the same as `csb backup --quiet`.
|
|
739
774
|
if argv is None:
|
|
740
775
|
argv = sys.argv[1:]
|
|
776
|
+
|
|
777
|
+
# `--` passthrough (#47): everything after the first standalone `--` token
|
|
778
|
+
# is forwarded verbatim to the wrapped subtool. Carve it off BEFORE
|
|
779
|
+
# flag-hoisting so a forwarded flag (e.g. `csb resume x -- --db /other`)
|
|
780
|
+
# is never mistaken for one of csb's own options. argparse never sees the
|
|
781
|
+
# `--` -- the tail is reattached to the namespace as `args.passthrough`.
|
|
782
|
+
argv, passthrough = _split_passthrough(argv)
|
|
783
|
+
|
|
741
784
|
argv = _hoist_common_flags(argv)
|
|
742
785
|
|
|
743
786
|
parser = build_parser()
|
|
744
787
|
args = parser.parse_args(argv)
|
|
788
|
+
args.passthrough = passthrough
|
|
745
789
|
|
|
746
790
|
if args.command is None:
|
|
747
791
|
parser.print_help()
|
|
748
792
|
return 0
|
|
749
793
|
|
|
794
|
+
# Only subtool-launchers can forward args; reject (never silently drop)
|
|
795
|
+
# a passthrough given to any other command.
|
|
796
|
+
if passthrough and args.command not in PASSTHROUGH_COMMANDS:
|
|
797
|
+
capable = ", ".join(sorted(PASSTHROUGH_COMMANDS))
|
|
798
|
+
print(
|
|
799
|
+
f"csb {args.command}: `--` passthrough is only supported by: "
|
|
800
|
+
f"{capable}. (Everything after `--` is forwarded to the wrapped "
|
|
801
|
+
f"tool, which `{args.command}` does not launch.)",
|
|
802
|
+
file=sys.stderr,
|
|
803
|
+
)
|
|
804
|
+
return 2
|
|
805
|
+
|
|
750
806
|
# Import handlers lazily to keep startup fast
|
|
751
807
|
if args.command == "backup":
|
|
752
808
|
from .commands import cmd_backup
|
{claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/commands.py
RENAMED
|
@@ -70,6 +70,7 @@ from .metadata import (
|
|
|
70
70
|
read_name_cache,
|
|
71
71
|
read_session_state,
|
|
72
72
|
)
|
|
73
|
+
from .pathkit import ClaudePaths
|
|
73
74
|
from .scanner import scan_projects
|
|
74
75
|
from .timeline import format_session_line, format_timeline, render_timeline_rich, HAS_RICH
|
|
75
76
|
|
|
@@ -192,8 +193,10 @@ def _cmd_backup_inner(args, config, claude_dir, quiet) -> int:
|
|
|
192
193
|
# Upsert into index. Store rel_path with forward slashes so it
|
|
193
194
|
# works directly with `git show <commit>:<path>` (which rejects
|
|
194
195
|
# backslash separators on Windows). Path operations downstream
|
|
195
|
-
# accept either separator.
|
|
196
|
-
|
|
196
|
+
# accept either separator. ClaudePaths.rel() also survives a
|
|
197
|
+
# junction/symlink claude_dir, where a resolved-vs-raw prefix
|
|
198
|
+
# mismatch made bare relative_to() raise ValueError (#46).
|
|
199
|
+
rel_path = ClaudePaths.from_dir(claude_dir).rel(sf.jsonl_path)
|
|
197
200
|
# Restore-verify gate (v0.3.16): only let a reappeared JSONL
|
|
198
201
|
# clear an existing deleted_at if it's a genuine transcript
|
|
199
202
|
# (>=1 parsed event). A stub / garbage file (event_count == 0,
|
|
@@ -454,9 +457,9 @@ def find_unbacked_sessions(conn, claude_dir, exclude=None):
|
|
|
454
457
|
aren't in the index at all) -- i.e. sessions with un-backed-up changes.
|
|
455
458
|
|
|
456
459
|
The single source of truth for "what isn't backed up", shared by the
|
|
457
|
-
SessionStart hook's `_check` and
|
|
458
|
-
|
|
459
|
-
skip (e.g. the currently-active session, whose JSONL is mid-write).
|
|
460
|
+
SessionStart hook's `_check` and ``csb status``'s un-backed-up section
|
|
461
|
+
(and, eventually, ``csb list``). ``exclude`` is a set of full session ids
|
|
462
|
+
to skip (e.g. the currently-active session, whose JSONL is mid-write).
|
|
460
463
|
"""
|
|
461
464
|
exclude = set(exclude or [])
|
|
462
465
|
stale = []
|
|
@@ -961,8 +964,9 @@ def cmd_restore(args) -> int:
|
|
|
961
964
|
#
|
|
962
965
|
# Single source of truth for the file-level restore policy. Used by:
|
|
963
966
|
# - cmd_restore -- the `csb restore <uuid>` command
|
|
964
|
-
# - cmd_resume --
|
|
965
|
-
# - cmd_view -- (
|
|
967
|
+
# - cmd_resume -- prompts-to-restore when the session is pruned
|
|
968
|
+
# - cmd_view -- same when viewing a pruned session (#14 / #34)
|
|
969
|
+
# - cmd_distill -- same when distilling a pruned session (#12)
|
|
966
970
|
#
|
|
967
971
|
# Callers are responsible for resolving the UUID, finding `jsonl_path` (DB
|
|
968
972
|
# row OR git-history fallback), and finding `commit` (parent-of-deletion).
|
|
@@ -1020,8 +1024,8 @@ def _restore_session(
|
|
|
1020
1024
|
from the DB row OR via `git_find_jsonl_by_uuid` fallback.
|
|
1021
1025
|
commit: commit-ish to restore from. Caller obtains via
|
|
1022
1026
|
`git_find_deleted_file(claude_dir, jsonl_path)`.
|
|
1023
|
-
jsonl_only: if True, restore only the main transcript
|
|
1024
|
-
|
|
1027
|
+
jsonl_only: if True, restore only the main transcript JSONL
|
|
1028
|
+
(skip the session subtree and logger sidecars).
|
|
1025
1029
|
force: if True, overwrite present on-disk files from git.
|
|
1026
1030
|
Default behavior preserves on-disk content (idempotent;
|
|
1027
1031
|
never clobbers local content with newer-than-git writes).
|
|
@@ -1211,8 +1215,11 @@ def _recreate_transcript_symlink(
|
|
|
1211
1215
|
from dazzle_filekit import create_symlink
|
|
1212
1216
|
except ImportError:
|
|
1213
1217
|
return False
|
|
1214
|
-
|
|
1215
|
-
|
|
1218
|
+
cp = ClaudePaths.from_dir(claude_dir)
|
|
1219
|
+
link_path = cp.abs_of(link_rel)
|
|
1220
|
+
# .resolve() stays: the is_symlink comparison below resolves the live
|
|
1221
|
+
# link's target, so both sides must share resolve semantics (#46).
|
|
1222
|
+
target_abs = cp.jsonl(slug, uuid).resolve()
|
|
1216
1223
|
# Skip work if a correct symlink already exists (idempotent, no churn).
|
|
1217
1224
|
try:
|
|
1218
1225
|
if link_path.is_symlink() and Path(os.readlink(link_path)).resolve() == target_abs:
|
|
@@ -1442,11 +1449,7 @@ def _extract_slug_from_jsonl_path(jsonl_path: str) -> str:
|
|
|
1442
1449
|
"""
|
|
1443
1450
|
if not jsonl_path:
|
|
1444
1451
|
return ""
|
|
1445
|
-
|
|
1446
|
-
parts = norm.split("/")
|
|
1447
|
-
if len(parts) >= 3 and parts[0] == "projects":
|
|
1448
|
-
return parts[1]
|
|
1449
|
-
return ""
|
|
1452
|
+
return ClaudePaths.parse_rel(jsonl_path).slug or ""
|
|
1450
1453
|
|
|
1451
1454
|
|
|
1452
1455
|
def _categorize_restored_paths(
|
|
@@ -1652,7 +1655,7 @@ def cmd_search(args) -> int:
|
|
|
1652
1655
|
|
|
1653
1656
|
|
|
1654
1657
|
def cmd_build_fts5(args) -> int:
|
|
1655
|
-
"""Build / refresh per-project FTS5 content indices
|
|
1658
|
+
"""Build / refresh per-project FTS5 content indices.
|
|
1656
1659
|
|
|
1657
1660
|
Idempotent: by default only re-indexes sessions whose JSONL mtime
|
|
1658
1661
|
has advanced past ``indexed_sessions.last_jsonl_mtime``. Use
|
|
@@ -1739,16 +1742,49 @@ def cmd_update(args) -> int:
|
|
|
1739
1742
|
|
|
1740
1743
|
|
|
1741
1744
|
def _maybe_refresh_fts5(args) -> None:
|
|
1742
|
-
"""
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1745
|
+
"""Wipe + rebuild the per-project FTS5 content indexes after a
|
|
1746
|
+
`csb update rebuild-index --include-fts5` (#3, the last open AC).
|
|
1747
|
+
|
|
1748
|
+
Force-rebuild on purpose: rebuild-index is the nuclear
|
|
1749
|
+
reconstruct-everything verb, so the content indexes are rebuilt
|
|
1750
|
+
unconditionally too rather than mtime-gated (that incremental path
|
|
1751
|
+
is `csb update build-fts5` without --force).
|
|
1752
|
+
|
|
1753
|
+
Fails SOFT in every case: by the time this seam runs the main index
|
|
1754
|
+
rebuild has already succeeded, so a missing-FTS5 SQLite build or an
|
|
1755
|
+
indexing error downgrades to a stderr warning with the manual
|
|
1756
|
+
command, never a non-zero rebuild exit.
|
|
1757
|
+
|
|
1758
|
+
(Backup-time incremental FTS5 indexing -- the other half of the
|
|
1759
|
+
original #3 Phase 2 spec -- was REJECTED by design: it would add
|
|
1760
|
+
latency inside the PreCompact/SessionEnd hooks, and v0.3.22's
|
|
1761
|
+
search-time freshness rescue makes it unnecessary.)
|
|
1750
1762
|
"""
|
|
1751
|
-
|
|
1763
|
+
from . import fts5_db, fts5_index
|
|
1764
|
+
|
|
1765
|
+
quiet = getattr(args, "quiet", False)
|
|
1766
|
+
if not fts5_db.fts5_available():
|
|
1767
|
+
print(
|
|
1768
|
+
"Warning: this Python's SQLite lacks FTS5; skipped the "
|
|
1769
|
+
"--include-fts5 refresh (main index rebuild is intact).",
|
|
1770
|
+
file=sys.stderr,
|
|
1771
|
+
)
|
|
1772
|
+
return
|
|
1773
|
+
config = _get_config(args)
|
|
1774
|
+
conn = open_db(config["index_path"])
|
|
1775
|
+
init_schema(conn, quiet=quiet)
|
|
1776
|
+
try:
|
|
1777
|
+
fts5_index.build_all(
|
|
1778
|
+
conn, Path(config["claude_dir"]), force=True, quiet=quiet,
|
|
1779
|
+
)
|
|
1780
|
+
except Exception as e: # noqa: BLE001 -- secondary refresh must not fail the rebuild
|
|
1781
|
+
print(
|
|
1782
|
+
f"Warning: FTS5 refresh failed ({e}); main index rebuild is "
|
|
1783
|
+
f"intact. Run `csb update build-fts5 --force` manually.",
|
|
1784
|
+
file=sys.stderr,
|
|
1785
|
+
)
|
|
1786
|
+
finally:
|
|
1787
|
+
conn.close()
|
|
1752
1788
|
|
|
1753
1789
|
|
|
1754
1790
|
def cmd_backfill_deleted(args) -> int:
|
|
@@ -1776,11 +1812,12 @@ def cmd_backfill_deleted(args) -> int:
|
|
|
1776
1812
|
|
|
1777
1813
|
Flags:
|
|
1778
1814
|
--dry-run -- preview without writing anything
|
|
1779
|
-
--full --
|
|
1780
|
-
|
|
1815
|
+
--full -- accepted but not yet differentiated from the default
|
|
1816
|
+
run (the last_refreshed_at marker is recorded but not
|
|
1817
|
+
yet used as an incremental-skip gate)
|
|
1781
1818
|
|
|
1782
|
-
Plan ref:
|
|
1783
|
-
|
|
1819
|
+
Plan ref: 2026-06-02__15-46-56__claude-plan__safe-update-umbrella-
|
|
1820
|
+
and-backfill-v0.3.11.md
|
|
1784
1821
|
"""
|
|
1785
1822
|
from .index import (
|
|
1786
1823
|
count_git_deleted_jsonls,
|
|
@@ -1907,8 +1944,7 @@ def cmd_backfill_deleted(args) -> int:
|
|
|
1907
1944
|
continue
|
|
1908
1945
|
|
|
1909
1946
|
# Derive the project slug from the path: projects/<slug>/<uuid>.jsonl
|
|
1910
|
-
|
|
1911
|
-
project = parts[1] if len(parts) >= 3 and parts[0] == "projects" else ""
|
|
1947
|
+
project = ClaudePaths.parse_rel(jp).slug or ""
|
|
1912
1948
|
|
|
1913
1949
|
meta = extract_metadata_from_bytes(blob, session_id=sid, project=project)
|
|
1914
1950
|
new_folder_count = len(meta.folder_usage)
|
|
@@ -2009,14 +2045,14 @@ def cmd_rebuild_index(args) -> int:
|
|
|
2009
2045
|
deleted-session rows back in (skipping any UUIDs the live
|
|
2010
2046
|
rescan already repopulated, which would mean the JSONL came
|
|
2011
2047
|
back somehow).
|
|
2012
|
-
7. Optionally runs ``_maybe_refresh_fts5`` (
|
|
2013
|
-
|
|
2048
|
+
7. Optionally runs ``_maybe_refresh_fts5`` (force wipe + rebuild
|
|
2049
|
+
of the per-project FTS5 DBs, fail-soft) if ``--include-fts5``.
|
|
2014
2050
|
8. Optionally chains ``cmd_backfill_deleted`` if
|
|
2015
2051
|
``--include-backfill-deleted`` is set.
|
|
2016
2052
|
9. Removes the ``.bak`` on full success.
|
|
2017
2053
|
|
|
2018
|
-
Plan ref:
|
|
2019
|
-
|
|
2054
|
+
Plan ref: 2026-06-02__15-46-56__claude-plan__safe-update-umbrella-
|
|
2055
|
+
and-backfill-v0.3.11.md
|
|
2020
2056
|
"""
|
|
2021
2057
|
config = _get_config(args)
|
|
2022
2058
|
claude_dir = config["claude_dir"]
|
|
@@ -2089,11 +2125,12 @@ def cmd_rebuild_index(args) -> int:
|
|
|
2089
2125
|
print(f"Preserved {restored} deleted-session {noun} "
|
|
2090
2126
|
f"across rebuild")
|
|
2091
2127
|
|
|
2092
|
-
# 5. Optional --include-fts5
|
|
2128
|
+
# 5. Optional --include-fts5: wipe + rebuild per-project FTS5 DBs
|
|
2129
|
+
# against the freshly rebuilt index (fails soft -- see the helper).
|
|
2093
2130
|
if getattr(args, "include_fts5", False):
|
|
2094
2131
|
_maybe_refresh_fts5(args)
|
|
2095
2132
|
|
|
2096
|
-
# 6. Optional --include-backfill-deleted
|
|
2133
|
+
# 6. Optional --include-backfill-deleted -- chain cmd_backfill_deleted
|
|
2097
2134
|
if getattr(args, "include_backfill_deleted", False):
|
|
2098
2135
|
cmd_backfill_deleted(args)
|
|
2099
2136
|
|
|
@@ -2448,19 +2485,27 @@ def _find_viewer(config) -> Optional[dict]:
|
|
|
2448
2485
|
return None
|
|
2449
2486
|
|
|
2450
2487
|
|
|
2451
|
-
def
|
|
2488
|
+
def _passthrough_args(args) -> list:
|
|
2489
|
+
"""The args after a standalone `--` (#47), forwarded verbatim to the
|
|
2490
|
+
wrapped subtool. Empty list when none were given."""
|
|
2491
|
+
return list(getattr(args, "passthrough", None) or [])
|
|
2492
|
+
|
|
2493
|
+
|
|
2494
|
+
def _launch_viewer(viewer: dict, session_value: str, passthrough: list = None) -> int:
|
|
2452
2495
|
"""Launch CCHV focused on ``session_value`` (a full session UUID).
|
|
2453
2496
|
|
|
2454
2497
|
Binary mode launches DETACHED so the viewer outlives this shell.
|
|
2455
2498
|
Dev mode runs ``pnpm tauri:dev`` in the foreground (build output
|
|
2456
|
-
visible; Ctrl-C stops it).
|
|
2499
|
+
visible; Ctrl-C stops it). ``passthrough`` (anything after `--`, #47)
|
|
2500
|
+
is appended verbatim to the viewer's argv.
|
|
2457
2501
|
"""
|
|
2458
2502
|
import platform as _platform
|
|
2459
2503
|
import subprocess
|
|
2460
2504
|
|
|
2505
|
+
extra = list(passthrough or [])
|
|
2461
2506
|
mode, path = viewer["mode"], viewer["path"]
|
|
2462
2507
|
if mode == "dev":
|
|
2463
|
-
cmd = ["pnpm", "tauri:dev", "--", "--", "--session", session_value]
|
|
2508
|
+
cmd = ["pnpm", "tauri:dev", "--", "--", "--session", session_value] + extra
|
|
2464
2509
|
print(f"Launching in dev mode from: {path}")
|
|
2465
2510
|
print(" (Vite + cargo run -- Ctrl-C to stop)")
|
|
2466
2511
|
try:
|
|
@@ -2470,7 +2515,7 @@ def _launch_viewer(viewer: dict, session_value: str) -> int:
|
|
|
2470
2515
|
print(" Is pnpm installed?", file=sys.stderr)
|
|
2471
2516
|
return 1
|
|
2472
2517
|
|
|
2473
|
-
cmd = [path, "--session", session_value]
|
|
2518
|
+
cmd = [path, "--session", session_value] + extra
|
|
2474
2519
|
try:
|
|
2475
2520
|
if _platform.system() == "Windows":
|
|
2476
2521
|
subprocess.Popen(
|
|
@@ -2696,7 +2741,7 @@ def cmd_view(args) -> int:
|
|
|
2696
2741
|
print(" - install: https://github.com/jhlee0409/claude-code-history-viewer")
|
|
2697
2742
|
return 0
|
|
2698
2743
|
|
|
2699
|
-
return _launch_viewer(viewer, full_id)
|
|
2744
|
+
return _launch_viewer(viewer, full_id, _passthrough_args(args))
|
|
2700
2745
|
|
|
2701
2746
|
|
|
2702
2747
|
# ── csb distill (#12): human-readable chat-log rendering ────────────────────
|
|
@@ -2714,7 +2759,7 @@ def _distill_canonical_path(claude_dir: str, session: dict) -> Path:
|
|
|
2714
2759
|
jsonl_rel = session.get("jsonl_path") or ""
|
|
2715
2760
|
slug = (Path(jsonl_rel).parent.name if jsonl_rel
|
|
2716
2761
|
else (session.get("project") or "unknown"))
|
|
2717
|
-
return
|
|
2762
|
+
return ClaudePaths.from_dir(claude_dir).distilled_md(slug, session["session_id"])
|
|
2718
2763
|
|
|
2719
2764
|
|
|
2720
2765
|
def _safe_stdout_write(text: str) -> None:
|
|
@@ -2740,7 +2785,7 @@ def _render_session_distill(
|
|
|
2740
2785
|
|
|
2741
2786
|
full_id = session["session_id"]
|
|
2742
2787
|
jsonl_rel = session.get("jsonl_path") or ""
|
|
2743
|
-
jsonl_abs =
|
|
2788
|
+
jsonl_abs = ClaudePaths.from_dir(claude_dir).abs_of(jsonl_rel) if jsonl_rel else None
|
|
2744
2789
|
convo_type, convo_path, tool_paths = pick_channels(
|
|
2745
2790
|
src_rows, jsonl_abs, source_override,
|
|
2746
2791
|
)
|
|
@@ -3132,11 +3177,16 @@ def cmd_resume(args) -> int:
|
|
|
3132
3177
|
if target is None:
|
|
3133
3178
|
target = session.get("start_folder")
|
|
3134
3179
|
|
|
3180
|
+
# Forward anything after `--` straight to claude (#47), e.g.
|
|
3181
|
+
# `csb resume <name> -- --fork-session`.
|
|
3182
|
+
claude_cmd = ["claude", "--resume", full_id] + _passthrough_args(args)
|
|
3183
|
+
launch_str = " ".join(claude_cmd)
|
|
3184
|
+
|
|
3135
3185
|
print(f"Resuming: {name}")
|
|
3136
3186
|
print(f" ID: {full_id}")
|
|
3137
3187
|
if target:
|
|
3138
3188
|
print(f" cd {target}")
|
|
3139
|
-
print(f"
|
|
3189
|
+
print(f" {launch_str}")
|
|
3140
3190
|
print()
|
|
3141
3191
|
|
|
3142
3192
|
# Launch claude --resume as a child process. We use subprocess.run with
|
|
@@ -3153,7 +3203,7 @@ def cmd_resume(args) -> int:
|
|
|
3153
3203
|
import subprocess
|
|
3154
3204
|
try:
|
|
3155
3205
|
result = subprocess.run(
|
|
3156
|
-
|
|
3206
|
+
claude_cmd,
|
|
3157
3207
|
cwd=target if target else None,
|
|
3158
3208
|
check=False,
|
|
3159
3209
|
)
|
|
@@ -3166,10 +3216,10 @@ def cmd_resume(args) -> int:
|
|
|
3166
3216
|
if target and not os.path.isdir(target):
|
|
3167
3217
|
print(f"Error: cannot cd to {target}: {e}", file=sys.stderr)
|
|
3168
3218
|
print("The folder may have been deleted. Run manually:", file=sys.stderr)
|
|
3169
|
-
print(f" cd <correct-folder> &&
|
|
3219
|
+
print(f" cd <correct-folder> && {launch_str}", file=sys.stderr)
|
|
3170
3220
|
return 1
|
|
3171
3221
|
print("Error: 'claude' command not found in PATH.", file=sys.stderr)
|
|
3172
|
-
print(f"Run manually:
|
|
3222
|
+
print(f"Run manually: {launch_str}", file=sys.stderr)
|
|
3173
3223
|
return 1
|
|
3174
3224
|
except NotADirectoryError as e:
|
|
3175
3225
|
# Edge case: target exists but isn't a directory (file with same name).
|
|
@@ -3701,7 +3751,7 @@ def _bulk_restore_jsonls(results, args, config, scope_label: str, quiet: bool) -
|
|
|
3701
3751
|
skipped = 0
|
|
3702
3752
|
failed = 0
|
|
3703
3753
|
for s, jsonl_rel in candidates:
|
|
3704
|
-
full_path =
|
|
3754
|
+
full_path = ClaudePaths.from_dir(claude_dir).abs_of(jsonl_rel)
|
|
3705
3755
|
if full_path.exists() and not force:
|
|
3706
3756
|
print(f" SKIP {s['session_id'][:8]} {jsonl_rel} "
|
|
3707
3757
|
f"(already exists; use --force to overwrite)")
|
|
@@ -8,6 +8,8 @@ import json
|
|
|
8
8
|
import os
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
from .pathkit import ClaudePaths
|
|
12
|
+
|
|
11
13
|
DEFAULT_CONFIG = {
|
|
12
14
|
"claude_dir": "~/.claude",
|
|
13
15
|
"index_path": "~/.claude/session-backup.db",
|
|
@@ -52,7 +54,7 @@ ENV_CLAUDE_DIR = "CLAUDE_DIR"
|
|
|
52
54
|
ENV_CLAUDE_CONFIG_DIR = "CLAUDE_CONFIG_DIR"
|
|
53
55
|
ENV_DB_PATH = "CLAUDE_SESSION_BACKUP_DB"
|
|
54
56
|
|
|
55
|
-
CONFIG_FILENAME =
|
|
57
|
+
CONFIG_FILENAME = ClaudePaths.CONFIG_FILE # canonical spelling lives in pathkit (#46)
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def _env_claude_dir():
|
|
@@ -90,7 +92,7 @@ def get_settings_path(claude_dir=None):
|
|
|
90
92
|
can edit the TTL through it.
|
|
91
93
|
"""
|
|
92
94
|
base = Path(claude_dir).expanduser() if claude_dir else _default_claude_dir()
|
|
93
|
-
return base /
|
|
95
|
+
return base / ClaudePaths.SETTINGS_FILE
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
def load_config(claude_dir=None):
|
|
@@ -132,10 +134,15 @@ def load_config(claude_dir=None):
|
|
|
132
134
|
# (#45): when index_path is still the stock default but claude_dir
|
|
133
135
|
# was relocated (flag / CLAUDE_DIR / CLAUDE_CONFIG_DIR / config),
|
|
134
136
|
# follow the relocation -- otherwise sessions would scan from the
|
|
135
|
-
# new dir while the index silently pinned to ~/.claude.
|
|
136
|
-
|
|
137
|
+
# new dir while the index silently pinned to ~/.claude. Compared
|
|
138
|
+
# Path-normalized (#46): a config file carrying the EXPANDED default
|
|
139
|
+
# spelling still counts as "stock default" and follows relocation.
|
|
140
|
+
stock_db = DEFAULT_CONFIG["index_path"]
|
|
141
|
+
if (config["index_path"] == stock_db
|
|
142
|
+
or Path(str(config["index_path"])).expanduser()
|
|
143
|
+
== Path(stock_db).expanduser()):
|
|
137
144
|
config["index_path"] = str(
|
|
138
|
-
Path(config["claude_dir"]).expanduser() /
|
|
145
|
+
Path(config["claude_dir"]).expanduser() / ClaudePaths.DEFAULT_DB
|
|
139
146
|
)
|
|
140
147
|
|
|
141
148
|
return config
|
{claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts5_db.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Per-project SQLite FTS5 content database
|
|
2
|
+
Per-project SQLite FTS5 content database.
|
|
3
3
|
|
|
4
4
|
One DB per project at ``~/.claude/csb-fts/<project>__<slug-hash>_<USER>.db``
|
|
5
5
|
(path convention locked in :mod:`fts_paths`). Each DB is self-contained --
|
|
@@ -9,12 +9,12 @@ main DB tracks WHICH sessions have been indexed via the already-reserved
|
|
|
9
9
|
per-project FTS5 DB independently tracks the same fact via its own
|
|
10
10
|
``indexed_sessions`` table (source of truth for "did this row land?").
|
|
11
11
|
|
|
12
|
-
Why per-project (
|
|
13
|
-
files, faster targeted queries, per-project archive/move/delete, no
|
|
14
|
-
contention when multiple projects refresh in parallel.
|
|
15
|
-
in :mod:`fts_paths`.
|
|
12
|
+
Why per-project (rather than one monolithic content store): smaller
|
|
13
|
+
files, faster targeted queries, per-project archive/move/delete, and no
|
|
14
|
+
contention when multiple projects refresh in parallel. The path
|
|
15
|
+
convention lives in :mod:`fts_paths`.
|
|
16
16
|
|
|
17
|
-
Schema
|
|
17
|
+
Schema::
|
|
18
18
|
|
|
19
19
|
messages(id PK, session_id, uuid, message_index, role,
|
|
20
20
|
role_subtype, content, timestamp,
|
|
@@ -91,9 +91,9 @@ CREATE TABLE IF NOT EXISTS indexed_sessions (
|
|
|
91
91
|
--
|
|
92
92
|
-- `strength` column (v0.3.1): per-row importance weight assigned at
|
|
93
93
|
-- import time. 3 = wrote/edited/notebook_edit (active modification),
|
|
94
|
-
-- 2 = read (passive view), 1 = searched (Grep probe). Used by
|
|
95
|
-
--
|
|
96
|
-
--
|
|
94
|
+
-- 2 = read (passive view), 1 = searched (Grep probe). Used by
|
|
95
|
+
-- `csb search -d/-D` directory-scope ranking. Default 2 so legacy
|
|
96
|
+
-- rows pre-migration land on the median.
|
|
97
97
|
CREATE TABLE IF NOT EXISTS file_operations (
|
|
98
98
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
99
99
|
session_id TEXT NOT NULL,
|
{claude_session_backup-0.4.2 → claude_session_backup-0.4.6}/claude_session_backup/fts5_index.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
FTS5 build orchestrator
|
|
2
|
+
FTS5 build orchestrator.
|
|
3
3
|
|
|
4
4
|
Walks the main DB's ``sessions`` + ``session_sources`` tables to
|
|
5
5
|
discover candidate sessions, opens the per-project FTS5 DB for each,
|
|
@@ -10,15 +10,17 @@ session import.
|
|
|
10
10
|
|
|
11
11
|
The orchestrator is intentionally explicit -- no hidden background
|
|
12
12
|
indexing, no triggers in the main DB. ``csb backup`` does NOT call
|
|
13
|
-
this (would slow the hook). Users opt in via ``csb update build-fts5
|
|
14
|
-
|
|
13
|
+
this (would slow the hook). Users opt in via ``csb update build-fts5``,
|
|
14
|
+
or ``csb update rebuild-index --include-fts5`` to refresh alongside an
|
|
15
|
+
index rebuild.
|
|
15
16
|
|
|
16
17
|
Source of truth (per design doc):
|
|
17
18
|
- Per-project FTS5 DB's ``indexed_sessions`` table: authoritative
|
|
18
19
|
"is this session in the index, at what mtime"
|
|
19
20
|
- Main DB's ``session_sources.fts5_indexed_at`` / ``content_hash``:
|
|
20
|
-
HINT columns
|
|
21
|
-
|
|
21
|
+
HINT columns kept in sync for inspection; the search dispatcher
|
|
22
|
+
checks freshness against the per-project DB's ``indexed_sessions``,
|
|
23
|
+
not these columns.
|
|
22
24
|
"""
|
|
23
25
|
|
|
24
26
|
from __future__ import annotations
|