claude-code-tools 1.0.6__py3-none-any.whl → 1.4.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_code_tools/__init__.py +1 -1
- claude_code_tools/action_rpc.py +16 -10
- claude_code_tools/aichat.py +793 -51
- claude_code_tools/claude_continue.py +4 -0
- claude_code_tools/codex_continue.py +48 -0
- claude_code_tools/export_session.py +9 -5
- claude_code_tools/find_claude_session.py +36 -12
- claude_code_tools/find_codex_session.py +33 -18
- claude_code_tools/find_session.py +30 -16
- claude_code_tools/gdoc2md.py +220 -0
- claude_code_tools/md2gdoc.py +549 -0
- claude_code_tools/search_index.py +83 -9
- claude_code_tools/session_menu_cli.py +1 -1
- claude_code_tools/session_utils.py +3 -3
- claude_code_tools/smart_trim.py +18 -8
- claude_code_tools/smart_trim_core.py +4 -2
- claude_code_tools/tmux_cli_controller.py +35 -25
- claude_code_tools/trim_session.py +28 -2
- claude_code_tools-1.4.1.dist-info/METADATA +1113 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/RECORD +30 -24
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/entry_points.txt +2 -0
- docs/local-llm-setup.md +286 -0
- docs/reddit-aichat-resume-v2.md +80 -0
- docs/reddit-aichat-resume.md +29 -0
- docs/reddit-aichat.md +79 -0
- docs/rollover-details.md +67 -0
- node_ui/action_config.js +3 -3
- node_ui/menu.js +67 -113
- claude_code_tools/session_tui.py +0 -516
- claude_code_tools-1.0.6.dist-info/METADATA +0 -685
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/WHEEL +0 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/licenses/LICENSE +0 -0
claude_code_tools/aichat.py
CHANGED
|
@@ -32,8 +32,18 @@ class SessionIDGroup(click.Group):
|
|
|
32
32
|
|
|
33
33
|
@click.group(cls=SessionIDGroup, invoke_without_command=True)
|
|
34
34
|
@click.version_option()
|
|
35
|
+
@click.option(
|
|
36
|
+
'--claude-home',
|
|
37
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
38
|
+
help='Claude home directory (default: ~/.claude or CLAUDE_CONFIG_DIR)',
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
'--codex-home',
|
|
42
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
43
|
+
help='Codex home directory (default: ~/.codex or CODEX_HOME)',
|
|
44
|
+
)
|
|
35
45
|
@click.pass_context
|
|
36
|
-
def main(ctx):
|
|
46
|
+
def main(ctx, claude_home, codex_home):
|
|
37
47
|
"""
|
|
38
48
|
Session management tools for Claude Code and Codex.
|
|
39
49
|
|
|
@@ -56,7 +66,17 @@ def main(ctx):
|
|
|
56
66
|
aichat resume abc123-def456 # Resume specific session
|
|
57
67
|
aichat menu abc123-def456
|
|
58
68
|
aichat trim session-id.jsonl
|
|
69
|
+
|
|
70
|
+
\b
|
|
71
|
+
Environment variables:
|
|
72
|
+
CLAUDE_CONFIG_DIR - Default Claude home (overridden by --claude-home)
|
|
73
|
+
CODEX_HOME - Default Codex home (overridden by --codex-home)
|
|
59
74
|
"""
|
|
75
|
+
# Store home dirs in context for subcommands to access
|
|
76
|
+
ctx.ensure_object(dict)
|
|
77
|
+
ctx.obj['claude_home'] = claude_home
|
|
78
|
+
ctx.obj['codex_home'] = codex_home
|
|
79
|
+
|
|
60
80
|
# Auto-index sessions on every aichat command (incremental, fast if up-to-date)
|
|
61
81
|
# Skip for build-index/clear-index to avoid double-indexing or state conflicts
|
|
62
82
|
# In JSON mode (-j/--json), suppress all output for clean parsing
|
|
@@ -68,10 +88,10 @@ def main(ctx):
|
|
|
68
88
|
try:
|
|
69
89
|
from claude_code_tools.search_index import auto_index
|
|
70
90
|
from claude_code_tools.session_utils import get_claude_home, get_codex_home
|
|
71
|
-
# Respect
|
|
91
|
+
# Respect CLI args, then env vars, then defaults
|
|
72
92
|
auto_index(
|
|
73
|
-
claude_home=get_claude_home(),
|
|
74
|
-
codex_home=get_codex_home(),
|
|
93
|
+
claude_home=get_claude_home(cli_arg=claude_home),
|
|
94
|
+
codex_home=get_codex_home(cli_arg=codex_home),
|
|
75
95
|
verbose=False,
|
|
76
96
|
silent=json_mode,
|
|
77
97
|
)
|
|
@@ -157,7 +177,75 @@ def _find_and_run_session_ui(
|
|
|
157
177
|
from claude_code_tools.session_utils import default_export_path
|
|
158
178
|
|
|
159
179
|
if session_id:
|
|
160
|
-
|
|
180
|
+
if direct_action:
|
|
181
|
+
# Direct action mode: bypass session_menu_cli for proper context
|
|
182
|
+
# This allows direct_action to be passed to Node UI for correct
|
|
183
|
+
# escape behavior (exit to shell vs. go to resume menu)
|
|
184
|
+
from claude_code_tools.session_utils import (
|
|
185
|
+
find_session_file,
|
|
186
|
+
default_export_path,
|
|
187
|
+
detect_agent_from_path,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Find session file
|
|
191
|
+
input_path = Path(session_id).expanduser()
|
|
192
|
+
if input_path.exists() and input_path.is_file():
|
|
193
|
+
session_file = input_path
|
|
194
|
+
agent = detect_agent_from_path(session_file)
|
|
195
|
+
project_path = str(session_file.parent)
|
|
196
|
+
git_branch = None
|
|
197
|
+
else:
|
|
198
|
+
result = find_session_file(session_id)
|
|
199
|
+
if not result:
|
|
200
|
+
print(f"Error: Session not found: {session_id}", file=sys.stderr)
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
agent, session_file, project_path, git_branch = result
|
|
203
|
+
|
|
204
|
+
# Build session dict for Node UI
|
|
205
|
+
session_dict = {
|
|
206
|
+
"agent": agent,
|
|
207
|
+
"agent_display": "Claude" if agent == "claude" else "Codex",
|
|
208
|
+
"session_id": session_file.stem,
|
|
209
|
+
"mod_time": session_file.stat().st_mtime,
|
|
210
|
+
"create_time": session_file.stat().st_ctime,
|
|
211
|
+
"lines": 0, # Not needed for direct action
|
|
212
|
+
"project": Path(project_path).name,
|
|
213
|
+
"preview": "",
|
|
214
|
+
"cwd": project_path,
|
|
215
|
+
"branch": git_branch or "",
|
|
216
|
+
"file_path": str(session_file),
|
|
217
|
+
"default_export_path": str(default_export_path(session_file, agent)),
|
|
218
|
+
"is_trimmed": False,
|
|
219
|
+
"derivation_type": None,
|
|
220
|
+
"is_sidechain": False,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Handler for actions
|
|
224
|
+
def handler(sess, action, kwargs=None):
|
|
225
|
+
merged_kwargs = {**(action_kwargs or {}), **(kwargs or {})}
|
|
226
|
+
return execute_action(
|
|
227
|
+
action,
|
|
228
|
+
sess["agent"],
|
|
229
|
+
Path(sess["file_path"]),
|
|
230
|
+
sess["cwd"],
|
|
231
|
+
action_kwargs=merged_kwargs if merged_kwargs else None,
|
|
232
|
+
session_id=sess.get("session_id"),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
rpc_path = str(Path(__file__).parent / "action_rpc.py")
|
|
236
|
+
|
|
237
|
+
# Run Node UI directly with direct_action for proper escape behavior
|
|
238
|
+
run_node_menu_ui(
|
|
239
|
+
[session_dict],
|
|
240
|
+
[],
|
|
241
|
+
handler,
|
|
242
|
+
start_screen=start_screen,
|
|
243
|
+
rpc_path=rpc_path,
|
|
244
|
+
direct_action=direct_action,
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# Normal mode (no direct_action): route through session_menu_cli
|
|
161
249
|
sys.argv = [
|
|
162
250
|
sys.argv[0].replace('aichat', 'session-menu'),
|
|
163
251
|
'--start-screen', start_screen,
|
|
@@ -459,22 +547,71 @@ def menu(ctx):
|
|
|
459
547
|
|
|
460
548
|
@main.command("trim")
|
|
461
549
|
@click.argument("session", required=False)
|
|
462
|
-
@click.option("--
|
|
463
|
-
|
|
464
|
-
|
|
550
|
+
@click.option("--tools", "-t",
|
|
551
|
+
help="Comma-separated tools to trim (e.g., 'bash,read,edit'). "
|
|
552
|
+
"Default: all tools.")
|
|
553
|
+
@click.option("--len", "-l", "threshold", type=int, default=500,
|
|
554
|
+
help="Minimum length threshold in chars for trimming (default: 500)")
|
|
555
|
+
@click.option("--trim-assistant", "-a", "trim_assistant", type=int,
|
|
556
|
+
help="Trim assistant messages: positive N trims first N over threshold, "
|
|
557
|
+
"negative N trims all except last |N| over threshold")
|
|
558
|
+
@click.option("--output-dir", "-o",
|
|
559
|
+
help="Output directory (default: same as input file)")
|
|
560
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
561
|
+
help="Force agent type (auto-detected if not specified)")
|
|
562
|
+
@click.option("--claude-home", help="Path to Claude home directory")
|
|
563
|
+
@click.option("--simple-ui", is_flag=True,
|
|
564
|
+
help="Use simple CLI trim instead of Node UI (requires session ID)")
|
|
565
|
+
def trim(session, tools, threshold, trim_assistant, output_dir, agent, claude_home, simple_ui):
|
|
566
|
+
"""Trim session to reduce size by truncating large tool outputs.
|
|
567
|
+
|
|
568
|
+
Trims tool call results and optionally assistant messages that exceed
|
|
569
|
+
the length threshold. Creates a new trimmed session file with lineage
|
|
570
|
+
metadata linking back to the original.
|
|
571
|
+
|
|
572
|
+
If no session ID provided, finds latest session for current project/branch
|
|
573
|
+
and opens the interactive trim menu.
|
|
465
574
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
575
|
+
\b
|
|
576
|
+
Examples:
|
|
577
|
+
aichat trim # Interactive menu for latest session
|
|
578
|
+
aichat trim abc123 # Interactive menu for specific session
|
|
579
|
+
aichat trim abc123 --simple-ui # Direct CLI trim with defaults
|
|
580
|
+
aichat trim abc123 -t bash,read -l 1000 # Trim specific tools, custom threshold
|
|
581
|
+
aichat trim abc123 -a 5 # Also trim first 5 long assistant msgs
|
|
582
|
+
|
|
583
|
+
\b
|
|
584
|
+
Options:
|
|
585
|
+
--tools, -t Which tool outputs to trim (default: all)
|
|
586
|
+
--len, -l Character threshold for trimming (default: 500)
|
|
587
|
+
--trim-assistant, -a Trim assistant messages too
|
|
588
|
+
--output-dir, -o Where to write trimmed file
|
|
469
589
|
"""
|
|
470
590
|
import sys
|
|
471
591
|
|
|
472
|
-
if simple_ui:
|
|
592
|
+
if simple_ui or tools or trim_assistant or output_dir:
|
|
593
|
+
# Direct CLI mode - need a session
|
|
473
594
|
if not session:
|
|
474
|
-
print("Error:
|
|
595
|
+
print("Error: Direct trim options require a session ID", file=sys.stderr)
|
|
596
|
+
print("Use 'aichat trim' without options for interactive mode", file=sys.stderr)
|
|
475
597
|
sys.exit(1)
|
|
476
|
-
|
|
477
|
-
|
|
598
|
+
|
|
599
|
+
# Build args for trim-session CLI
|
|
600
|
+
args = [session]
|
|
601
|
+
if tools:
|
|
602
|
+
args.extend(["--tools", tools])
|
|
603
|
+
if threshold != 500:
|
|
604
|
+
args.extend(["--len", str(threshold)])
|
|
605
|
+
if trim_assistant is not None:
|
|
606
|
+
args.extend(["--trim-assistant-messages", str(trim_assistant)])
|
|
607
|
+
if output_dir:
|
|
608
|
+
args.extend(["--output-dir", output_dir])
|
|
609
|
+
if agent:
|
|
610
|
+
args.extend(["--agent", agent])
|
|
611
|
+
if claude_home:
|
|
612
|
+
args.extend(["--claude-home", claude_home])
|
|
613
|
+
|
|
614
|
+
sys.argv = [sys.argv[0].replace('aichat', 'trim-session')] + args
|
|
478
615
|
from claude_code_tools.trim_session import main as trim_main
|
|
479
616
|
trim_main()
|
|
480
617
|
return
|
|
@@ -488,32 +625,122 @@ def trim(session, simple_ui):
|
|
|
488
625
|
)
|
|
489
626
|
|
|
490
627
|
|
|
491
|
-
@main.command("smart-trim"
|
|
492
|
-
@click.
|
|
493
|
-
|
|
628
|
+
@main.command("smart-trim")
|
|
629
|
+
@click.argument("session", required=False)
|
|
630
|
+
@click.option("--instructions", "-i",
|
|
631
|
+
help="Custom instructions for what to prioritize when trimming")
|
|
632
|
+
@click.option("--exclude-types", "-e",
|
|
633
|
+
help="Comma-separated message types to never trim (default: user)")
|
|
634
|
+
@click.option("--preserve-recent", "-p", type=int, default=10,
|
|
635
|
+
help="Always preserve last N messages (default: 10)")
|
|
636
|
+
@click.option("--len", "-l", "content_threshold", type=int, default=200,
|
|
637
|
+
help="Minimum chars for content extraction (default: 200)")
|
|
638
|
+
@click.option("--output-dir", "-o",
|
|
639
|
+
help="Output directory (default: same as input)")
|
|
640
|
+
@click.option("--dry-run", "-n", is_flag=True,
|
|
641
|
+
help="Show what would be trimmed without doing it")
|
|
642
|
+
@click.option("--claude-home", help="Path to Claude home directory")
|
|
643
|
+
def smart_trim(session, instructions, exclude_types, preserve_recent, content_threshold,
|
|
644
|
+
output_dir, dry_run, claude_home):
|
|
494
645
|
"""Smart trim using Claude SDK agents (EXPERIMENTAL).
|
|
495
646
|
|
|
647
|
+
Uses an LLM to intelligently analyze the session and decide what can
|
|
648
|
+
be safely trimmed while preserving important context. Creates a new
|
|
649
|
+
trimmed session with lineage metadata.
|
|
650
|
+
|
|
496
651
|
If no session ID provided, finds latest session for current project/branch.
|
|
652
|
+
|
|
653
|
+
\b
|
|
654
|
+
Examples:
|
|
655
|
+
aichat smart-trim # Interactive for latest session
|
|
656
|
+
aichat smart-trim abc123 # Smart trim specific session
|
|
657
|
+
aichat smart-trim abc123 --dry-run # Preview what would be trimmed
|
|
658
|
+
aichat smart-trim abc123 -p 20 # Preserve last 20 messages
|
|
659
|
+
aichat smart-trim abc123 -e user,system # Never trim user or system msgs
|
|
660
|
+
aichat smart-trim abc123 -i "preserve auth-related messages"
|
|
661
|
+
|
|
662
|
+
\b
|
|
663
|
+
Options:
|
|
664
|
+
--instructions, -i Custom instructions for what to prioritize
|
|
665
|
+
--exclude-types, -e Message types to EXCLUDE from trimming
|
|
666
|
+
--preserve-recent, -p Keep last N messages untouched (default: 10)
|
|
667
|
+
--len, -l Min chars for content extraction (default: 200)
|
|
668
|
+
--dry-run, -n Preview only, don't actually trim
|
|
497
669
|
"""
|
|
498
670
|
import sys
|
|
499
|
-
|
|
500
|
-
|
|
671
|
+
from pathlib import Path
|
|
672
|
+
|
|
673
|
+
from claude_code_tools.session_utils import find_session_file, detect_agent_from_path
|
|
674
|
+
|
|
675
|
+
# If --instructions provided, use the handler function (same as TUI)
|
|
676
|
+
if instructions and session:
|
|
677
|
+
input_path = Path(session).expanduser()
|
|
678
|
+
if input_path.exists() and input_path.is_file():
|
|
679
|
+
session_file = input_path
|
|
680
|
+
detected_agent = detect_agent_from_path(session_file)
|
|
681
|
+
session_id = session_file.stem
|
|
682
|
+
project_path = str(session_file.parent)
|
|
683
|
+
else:
|
|
684
|
+
result = find_session_file(session)
|
|
685
|
+
if not result:
|
|
686
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
687
|
+
sys.exit(1)
|
|
688
|
+
detected_agent, session_file, project_path, _ = result
|
|
689
|
+
session_id = session_file.stem
|
|
690
|
+
|
|
691
|
+
# Use the handler function which supports custom_instructions
|
|
692
|
+
if detected_agent == "claude":
|
|
693
|
+
from claude_code_tools.find_claude_session import handle_smart_trim_resume_claude
|
|
694
|
+
handle_smart_trim_resume_claude(
|
|
695
|
+
session_id, project_path, claude_home,
|
|
696
|
+
custom_instructions=instructions,
|
|
697
|
+
)
|
|
698
|
+
else:
|
|
699
|
+
from claude_code_tools.find_codex_session import handle_smart_trim_resume_codex
|
|
700
|
+
from claude_code_tools.session_utils import get_codex_home
|
|
701
|
+
handle_smart_trim_resume_codex(
|
|
702
|
+
{"file_path": str(session_file), "cwd": project_path, "session_id": session_id},
|
|
703
|
+
Path(get_codex_home(cli_arg=None)),
|
|
704
|
+
custom_instructions=instructions,
|
|
705
|
+
)
|
|
706
|
+
return
|
|
707
|
+
|
|
708
|
+
# If any direct CLI options specified (but not instructions), use smart_trim.py
|
|
709
|
+
if exclude_types or preserve_recent != 10 or content_threshold != 200 or output_dir or dry_run:
|
|
710
|
+
if not session:
|
|
711
|
+
print("Error: Direct smart-trim options require a session ID", file=sys.stderr)
|
|
712
|
+
print("Use 'aichat smart-trim' without options for interactive mode", file=sys.stderr)
|
|
713
|
+
sys.exit(1)
|
|
714
|
+
|
|
715
|
+
# Build args for smart-trim CLI
|
|
716
|
+
args = [session]
|
|
717
|
+
if exclude_types:
|
|
718
|
+
args.extend(["--exclude-types", exclude_types])
|
|
719
|
+
if preserve_recent != 10:
|
|
720
|
+
args.extend(["--preserve-recent", str(preserve_recent)])
|
|
721
|
+
if content_threshold != 200:
|
|
722
|
+
args.extend(["--content-threshold", str(content_threshold)])
|
|
723
|
+
if output_dir:
|
|
724
|
+
args.extend(["--output-dir", output_dir])
|
|
725
|
+
if dry_run:
|
|
726
|
+
args.append("--dry-run")
|
|
727
|
+
if claude_home:
|
|
728
|
+
args.extend(["--claude-home", claude_home])
|
|
501
729
|
|
|
502
|
-
if session_id:
|
|
503
|
-
# Pass to existing smart_trim CLI
|
|
504
730
|
sys.argv = [sys.argv[0].replace('aichat', 'smart-trim')] + args
|
|
505
731
|
from claude_code_tools.smart_trim import main as smart_trim_main
|
|
506
732
|
smart_trim_main()
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
733
|
+
return
|
|
734
|
+
|
|
735
|
+
# Show interactive UI for entering instructions
|
|
736
|
+
# (whether session provided or not - find latest if not)
|
|
737
|
+
_find_and_run_session_ui(
|
|
738
|
+
session_id=session,
|
|
739
|
+
agent_constraint='both',
|
|
740
|
+
start_screen='smart_trim_form',
|
|
741
|
+
select_target='smart_trim_form',
|
|
742
|
+
results_title=' Which session to smart-trim? ',
|
|
743
|
+
)
|
|
517
744
|
|
|
518
745
|
|
|
519
746
|
@main.command("export", context_settings={"ignore_unknown_options": True, "allow_extra_args": True, "allow_interspersed_args": False})
|
|
@@ -660,15 +887,528 @@ def delete(ctx):
|
|
|
660
887
|
delete_main()
|
|
661
888
|
|
|
662
889
|
|
|
890
|
+
@main.command("info")
|
|
891
|
+
@click.argument("session", required=False)
|
|
892
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
893
|
+
help="Force agent type (auto-detected if not specified)")
|
|
894
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
895
|
+
def info(session, agent, json_output):
|
|
896
|
+
"""Show information about a session.
|
|
897
|
+
|
|
898
|
+
Displays session metadata including file path, agent type, project,
|
|
899
|
+
branch, timestamps, message counts, and lineage (parent sessions).
|
|
900
|
+
|
|
901
|
+
If no session ID provided, shows info for latest session in current
|
|
902
|
+
project/branch.
|
|
903
|
+
|
|
904
|
+
\b
|
|
905
|
+
Examples:
|
|
906
|
+
aichat info # Info for latest session
|
|
907
|
+
aichat info abc123-def456 # Info for specific session
|
|
908
|
+
aichat info --json abc123 # Output as JSON
|
|
909
|
+
"""
|
|
910
|
+
import json as json_lib
|
|
911
|
+
import sys
|
|
912
|
+
from pathlib import Path
|
|
913
|
+
from datetime import datetime
|
|
914
|
+
|
|
915
|
+
from claude_code_tools.session_utils import (
|
|
916
|
+
find_session_file,
|
|
917
|
+
detect_agent_from_path,
|
|
918
|
+
extract_cwd_from_session,
|
|
919
|
+
count_user_messages,
|
|
920
|
+
default_export_path,
|
|
921
|
+
)
|
|
922
|
+
from claude_code_tools.session_lineage import get_continuation_lineage
|
|
923
|
+
|
|
924
|
+
# Find session file
|
|
925
|
+
if session:
|
|
926
|
+
input_path = Path(session).expanduser()
|
|
927
|
+
if input_path.exists() and input_path.is_file():
|
|
928
|
+
session_file = input_path
|
|
929
|
+
detected_agent = agent or detect_agent_from_path(session_file)
|
|
930
|
+
else:
|
|
931
|
+
result = find_session_file(session)
|
|
932
|
+
if not result:
|
|
933
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
934
|
+
sys.exit(1)
|
|
935
|
+
detected_agent, session_file, _, _ = result
|
|
936
|
+
if agent:
|
|
937
|
+
detected_agent = agent
|
|
938
|
+
else:
|
|
939
|
+
# No session provided - find latest
|
|
940
|
+
_find_and_run_session_ui(
|
|
941
|
+
session_id=None,
|
|
942
|
+
agent_constraint='both',
|
|
943
|
+
start_screen='action',
|
|
944
|
+
direct_action='path', # Just show path for now
|
|
945
|
+
)
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
# Gather session info
|
|
949
|
+
session_id = session_file.stem
|
|
950
|
+
mod_time = datetime.fromtimestamp(session_file.stat().st_mtime)
|
|
951
|
+
create_time = datetime.fromtimestamp(session_file.stat().st_ctime)
|
|
952
|
+
file_size = session_file.stat().st_size
|
|
953
|
+
line_count = sum(1 for _ in open(session_file))
|
|
954
|
+
|
|
955
|
+
# Extract metadata from session
|
|
956
|
+
from claude_code_tools.export_session import extract_session_metadata
|
|
957
|
+
metadata = extract_session_metadata(session_file, detected_agent)
|
|
958
|
+
cwd = metadata.get("cwd") or extract_cwd_from_session(session_file)
|
|
959
|
+
project = Path(cwd).name if cwd else "unknown"
|
|
960
|
+
custom_title = metadata.get("customTitle", "")
|
|
961
|
+
user_msg_count = count_user_messages(session_file, detected_agent)
|
|
962
|
+
|
|
963
|
+
# Get lineage
|
|
964
|
+
lineage = get_continuation_lineage(session_file, export_missing=False)
|
|
965
|
+
lineage_info = []
|
|
966
|
+
for node in lineage:
|
|
967
|
+
lineage_info.append({
|
|
968
|
+
"file": str(node.session_file),
|
|
969
|
+
"type": node.derivation_type or "original",
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
info_data = {
|
|
973
|
+
"session_id": session_id,
|
|
974
|
+
"agent": detected_agent,
|
|
975
|
+
"custom_title": custom_title,
|
|
976
|
+
"file_path": str(session_file),
|
|
977
|
+
"project": project,
|
|
978
|
+
"cwd": cwd,
|
|
979
|
+
"created": create_time.isoformat(),
|
|
980
|
+
"modified": mod_time.isoformat(),
|
|
981
|
+
"file_size_bytes": file_size,
|
|
982
|
+
"line_count": line_count,
|
|
983
|
+
"user_message_count": user_msg_count,
|
|
984
|
+
"export_path": str(default_export_path(session_file, detected_agent)),
|
|
985
|
+
"lineage": lineage_info,
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if json_output:
|
|
989
|
+
print(json_lib.dumps(info_data, indent=2))
|
|
990
|
+
else:
|
|
991
|
+
print(f"\n{'='*60}")
|
|
992
|
+
print(f"Session: {session_id}")
|
|
993
|
+
if custom_title:
|
|
994
|
+
print(f"Title: {custom_title}")
|
|
995
|
+
print(f"{'='*60}")
|
|
996
|
+
print(f"Agent: {detected_agent}")
|
|
997
|
+
print(f"Project: {project}")
|
|
998
|
+
print(f"CWD: {cwd}")
|
|
999
|
+
print(f"File: {session_file}")
|
|
1000
|
+
print(f"Created: {create_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1001
|
+
print(f"Modified: {mod_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1002
|
+
print(f"Size: {file_size:,} bytes")
|
|
1003
|
+
print(f"Lines: {line_count:,}")
|
|
1004
|
+
print(f"User msgs: {user_msg_count}")
|
|
1005
|
+
if lineage_info:
|
|
1006
|
+
print(f"\nLineage ({len(lineage_info)} sessions):")
|
|
1007
|
+
for i, node in enumerate(lineage_info):
|
|
1008
|
+
prefix = " └─" if i == len(lineage_info) - 1 else " ├─"
|
|
1009
|
+
fname = Path(node["file"]).name
|
|
1010
|
+
print(f"{prefix} {fname} ({node['type']})")
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
@main.command("copy")
|
|
1014
|
+
@click.argument("session", required=False)
|
|
1015
|
+
@click.option("--dest", "-d", help="Destination path (default: prompted)")
|
|
1016
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
1017
|
+
help="Force agent type (auto-detected if not specified)")
|
|
1018
|
+
def copy_session(session, dest, agent):
|
|
1019
|
+
"""Copy a session file to a new location.
|
|
1020
|
+
|
|
1021
|
+
If no session ID provided, finds latest session for current project/branch.
|
|
1022
|
+
|
|
1023
|
+
\b
|
|
1024
|
+
Examples:
|
|
1025
|
+
aichat copy abc123-def456 # Copy with prompted destination
|
|
1026
|
+
aichat copy abc123 -d ~/backups/ # Copy to specific directory
|
|
1027
|
+
aichat copy abc123 -d ./my-session.jsonl # Copy with specific filename
|
|
1028
|
+
"""
|
|
1029
|
+
import sys
|
|
1030
|
+
from pathlib import Path
|
|
1031
|
+
|
|
1032
|
+
from claude_code_tools.session_utils import find_session_file, detect_agent_from_path
|
|
1033
|
+
|
|
1034
|
+
if not session:
|
|
1035
|
+
_find_and_run_session_ui(
|
|
1036
|
+
session_id=None,
|
|
1037
|
+
agent_constraint='both',
|
|
1038
|
+
start_screen='action',
|
|
1039
|
+
direct_action='copy',
|
|
1040
|
+
)
|
|
1041
|
+
return
|
|
1042
|
+
|
|
1043
|
+
# Find session file
|
|
1044
|
+
input_path = Path(session).expanduser()
|
|
1045
|
+
if input_path.exists() and input_path.is_file():
|
|
1046
|
+
session_file = input_path
|
|
1047
|
+
detected_agent = agent or detect_agent_from_path(session_file)
|
|
1048
|
+
else:
|
|
1049
|
+
result = find_session_file(session)
|
|
1050
|
+
if not result:
|
|
1051
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
1052
|
+
sys.exit(1)
|
|
1053
|
+
detected_agent, session_file, _, _ = result
|
|
1054
|
+
if agent:
|
|
1055
|
+
detected_agent = agent
|
|
1056
|
+
|
|
1057
|
+
# Import agent-specific copy function
|
|
1058
|
+
if detected_agent == "claude":
|
|
1059
|
+
from claude_code_tools.find_claude_session import copy_session_file
|
|
1060
|
+
else:
|
|
1061
|
+
from claude_code_tools.find_codex_session import copy_session_file
|
|
1062
|
+
|
|
1063
|
+
copy_session_file(str(session_file), dest)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
@main.command("query")
|
|
1067
|
+
@click.argument("session", required=False)
|
|
1068
|
+
@click.argument("question", required=False)
|
|
1069
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
1070
|
+
help="Force agent type (auto-detected if not specified)")
|
|
1071
|
+
def query_session(session, question, agent):
|
|
1072
|
+
"""Query a session with a question using an AI agent.
|
|
1073
|
+
|
|
1074
|
+
Exports the session and uses Claude to answer questions about its content.
|
|
1075
|
+
If no question provided, opens interactive query mode.
|
|
1076
|
+
|
|
1077
|
+
\b
|
|
1078
|
+
Examples:
|
|
1079
|
+
aichat query abc123 "What was the main bug fixed?"
|
|
1080
|
+
aichat query abc123 "Summarize the changes made"
|
|
1081
|
+
aichat query # Interactive mode for latest session
|
|
1082
|
+
"""
|
|
1083
|
+
import sys
|
|
1084
|
+
from pathlib import Path
|
|
1085
|
+
|
|
1086
|
+
from claude_code_tools.session_utils import find_session_file, detect_agent_from_path
|
|
1087
|
+
|
|
1088
|
+
if not session:
|
|
1089
|
+
_find_and_run_session_ui(
|
|
1090
|
+
session_id=None,
|
|
1091
|
+
agent_constraint='both',
|
|
1092
|
+
start_screen='query',
|
|
1093
|
+
direct_action='query',
|
|
1094
|
+
)
|
|
1095
|
+
return
|
|
1096
|
+
|
|
1097
|
+
# Find session file
|
|
1098
|
+
input_path = Path(session).expanduser()
|
|
1099
|
+
if input_path.exists() and input_path.is_file():
|
|
1100
|
+
session_file = input_path
|
|
1101
|
+
detected_agent = agent or detect_agent_from_path(session_file)
|
|
1102
|
+
else:
|
|
1103
|
+
result = find_session_file(session)
|
|
1104
|
+
if not result:
|
|
1105
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
1106
|
+
sys.exit(1)
|
|
1107
|
+
detected_agent, session_file, cwd, _ = result
|
|
1108
|
+
if agent:
|
|
1109
|
+
detected_agent = agent
|
|
1110
|
+
|
|
1111
|
+
# If no question, show interactive query UI
|
|
1112
|
+
if not question:
|
|
1113
|
+
from claude_code_tools.node_menu_ui import run_node_menu_ui
|
|
1114
|
+
from claude_code_tools.session_menu_cli import execute_action
|
|
1115
|
+
|
|
1116
|
+
rpc_path = str(Path(__file__).parent / "action_rpc.py")
|
|
1117
|
+
session_data = {
|
|
1118
|
+
"session_id": session_file.stem,
|
|
1119
|
+
"agent": detected_agent,
|
|
1120
|
+
"file_path": str(session_file),
|
|
1121
|
+
"cwd": str(session_file.parent),
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
def handler(sess, action, kwargs=None):
|
|
1125
|
+
return execute_action(
|
|
1126
|
+
action, sess["agent"], Path(sess["file_path"]),
|
|
1127
|
+
sess["cwd"], action_kwargs=kwargs
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
run_node_menu_ui(
|
|
1131
|
+
[session_data], [], handler,
|
|
1132
|
+
start_screen="query",
|
|
1133
|
+
rpc_path=rpc_path,
|
|
1134
|
+
)
|
|
1135
|
+
return
|
|
1136
|
+
|
|
1137
|
+
# Direct query with provided question
|
|
1138
|
+
from claude_code_tools.session_utils import default_export_path
|
|
1139
|
+
|
|
1140
|
+
# Export session first
|
|
1141
|
+
if detected_agent == "claude":
|
|
1142
|
+
from claude_code_tools.find_claude_session import handle_export_session
|
|
1143
|
+
else:
|
|
1144
|
+
from claude_code_tools.find_codex_session import handle_export_session
|
|
1145
|
+
|
|
1146
|
+
export_path = default_export_path(session_file, detected_agent)
|
|
1147
|
+
handle_export_session(str(session_file))
|
|
1148
|
+
|
|
1149
|
+
# Query using Claude
|
|
1150
|
+
import subprocess
|
|
1151
|
+
prompt = f"Read the session transcript at {export_path} and answer: {question}"
|
|
1152
|
+
subprocess.run(["claude", "-p", prompt])
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
@main.command("clone")
|
|
1156
|
+
@click.argument("session", required=False)
|
|
1157
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
1158
|
+
help="Force agent type (auto-detected if not specified)")
|
|
1159
|
+
def clone_session_cmd(session, agent):
|
|
1160
|
+
"""Clone a session and resume the clone.
|
|
1161
|
+
|
|
1162
|
+
Creates a copy of the session with a new ID and resumes it,
|
|
1163
|
+
leaving the original session untouched.
|
|
1164
|
+
|
|
1165
|
+
If no session ID provided, finds latest session for current project/branch.
|
|
1166
|
+
|
|
1167
|
+
\b
|
|
1168
|
+
Examples:
|
|
1169
|
+
aichat clone abc123-def456 # Clone specific session
|
|
1170
|
+
aichat clone # Clone latest session
|
|
1171
|
+
"""
|
|
1172
|
+
import sys
|
|
1173
|
+
from pathlib import Path
|
|
1174
|
+
|
|
1175
|
+
from claude_code_tools.session_utils import find_session_file, detect_agent_from_path
|
|
1176
|
+
|
|
1177
|
+
if not session:
|
|
1178
|
+
_find_and_run_session_ui(
|
|
1179
|
+
session_id=None,
|
|
1180
|
+
agent_constraint='both',
|
|
1181
|
+
start_screen='resume',
|
|
1182
|
+
direct_action='clone',
|
|
1183
|
+
)
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
# Find session file
|
|
1187
|
+
input_path = Path(session).expanduser()
|
|
1188
|
+
if input_path.exists() and input_path.is_file():
|
|
1189
|
+
session_file = input_path
|
|
1190
|
+
detected_agent = agent or detect_agent_from_path(session_file)
|
|
1191
|
+
cwd = str(session_file.parent)
|
|
1192
|
+
else:
|
|
1193
|
+
result = find_session_file(session)
|
|
1194
|
+
if not result:
|
|
1195
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
1196
|
+
sys.exit(1)
|
|
1197
|
+
detected_agent, session_file, cwd, _ = result
|
|
1198
|
+
if agent:
|
|
1199
|
+
detected_agent = agent
|
|
1200
|
+
|
|
1201
|
+
session_id = session_file.stem
|
|
1202
|
+
|
|
1203
|
+
# Execute clone
|
|
1204
|
+
if detected_agent == "claude":
|
|
1205
|
+
from claude_code_tools.find_claude_session import clone_session
|
|
1206
|
+
clone_session(session_id, cwd, shell_mode=False)
|
|
1207
|
+
else:
|
|
1208
|
+
from claude_code_tools.find_codex_session import clone_session
|
|
1209
|
+
clone_session(str(session_file), session_id, cwd, shell_mode=False)
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
@main.command("rollover")
|
|
1213
|
+
@click.argument("session", required=False)
|
|
1214
|
+
@click.option("--quick", is_flag=True,
|
|
1215
|
+
help="Quick rollover without context extraction (just preserve lineage)")
|
|
1216
|
+
@click.option("--prompt", "-p", help="Custom prompt for context extraction")
|
|
1217
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
1218
|
+
help="Force agent type (auto-detected if not specified)")
|
|
1219
|
+
def rollover(session, quick, prompt, agent):
|
|
1220
|
+
"""Rollover: hand off work to a fresh session with preserved lineage.
|
|
1221
|
+
|
|
1222
|
+
Creates a new session with a summary of the current work and links back
|
|
1223
|
+
to the parent session. The new session starts with full context available
|
|
1224
|
+
while the parent session is preserved intact.
|
|
1225
|
+
|
|
1226
|
+
\b
|
|
1227
|
+
Options:
|
|
1228
|
+
--quick Skip context extraction, just preserve lineage pointers
|
|
1229
|
+
--prompt Custom instructions for what context to extract
|
|
1230
|
+
|
|
1231
|
+
\b
|
|
1232
|
+
Examples:
|
|
1233
|
+
aichat rollover abc123 # Rollover with context extraction
|
|
1234
|
+
aichat rollover abc123 --quick # Quick rollover (lineage only)
|
|
1235
|
+
aichat rollover abc123 -p "Focus on the auth changes"
|
|
1236
|
+
aichat rollover # Rollover latest session
|
|
1237
|
+
"""
|
|
1238
|
+
import sys
|
|
1239
|
+
from pathlib import Path
|
|
1240
|
+
|
|
1241
|
+
from claude_code_tools.session_utils import (
|
|
1242
|
+
find_session_file,
|
|
1243
|
+
detect_agent_from_path,
|
|
1244
|
+
continue_with_options,
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
# If CLI options provided, use direct handler (backward compatible)
|
|
1248
|
+
if quick or prompt or agent:
|
|
1249
|
+
if not session:
|
|
1250
|
+
print("Error: --quick, --prompt, or --agent require a session ID",
|
|
1251
|
+
file=sys.stderr)
|
|
1252
|
+
print("Use 'aichat rollover' without options for interactive mode",
|
|
1253
|
+
file=sys.stderr)
|
|
1254
|
+
sys.exit(1)
|
|
1255
|
+
|
|
1256
|
+
# Find session file
|
|
1257
|
+
input_path = Path(session).expanduser()
|
|
1258
|
+
if input_path.exists() and input_path.is_file():
|
|
1259
|
+
session_file = input_path
|
|
1260
|
+
detected_agent = agent or detect_agent_from_path(session_file)
|
|
1261
|
+
else:
|
|
1262
|
+
result = find_session_file(session)
|
|
1263
|
+
if not result:
|
|
1264
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
1265
|
+
sys.exit(1)
|
|
1266
|
+
detected_agent, session_file, _, _ = result
|
|
1267
|
+
if agent:
|
|
1268
|
+
detected_agent = agent
|
|
1269
|
+
|
|
1270
|
+
# Execute rollover directly
|
|
1271
|
+
rollover_type = "quick" if quick else "context"
|
|
1272
|
+
continue_with_options(
|
|
1273
|
+
str(session_file),
|
|
1274
|
+
detected_agent,
|
|
1275
|
+
preset_prompt=prompt,
|
|
1276
|
+
rollover_type=rollover_type,
|
|
1277
|
+
)
|
|
1278
|
+
return
|
|
1279
|
+
|
|
1280
|
+
# Show interactive Node UI for rollover options
|
|
1281
|
+
# (whether session provided or not - find latest if not)
|
|
1282
|
+
# Start at lineage screen, with direct_action for proper escape behavior
|
|
1283
|
+
_find_and_run_session_ui(
|
|
1284
|
+
session_id=session,
|
|
1285
|
+
agent_constraint='both',
|
|
1286
|
+
start_screen='lineage',
|
|
1287
|
+
select_target='lineage',
|
|
1288
|
+
results_title=' Which session to rollover? ',
|
|
1289
|
+
direct_action='continue',
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
@main.command("lineage")
|
|
1294
|
+
@click.argument("session", required=False)
|
|
1295
|
+
@click.option("--agent", type=click.Choice(["claude", "codex"], case_sensitive=False),
|
|
1296
|
+
help="Force agent type (auto-detected if not specified)")
|
|
1297
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
1298
|
+
def lineage(session, agent, json_output):
|
|
1299
|
+
"""Show the parent lineage chain of a session.
|
|
1300
|
+
|
|
1301
|
+
Traces back through continue_metadata and trim_metadata to find
|
|
1302
|
+
all ancestor sessions, from the current session back to the original.
|
|
1303
|
+
|
|
1304
|
+
\b
|
|
1305
|
+
Examples:
|
|
1306
|
+
aichat lineage abc123-def456 # Show lineage for specific session
|
|
1307
|
+
aichat lineage # Show lineage for latest session
|
|
1308
|
+
aichat lineage abc123 --json # Output as JSON
|
|
1309
|
+
"""
|
|
1310
|
+
import json as json_lib
|
|
1311
|
+
import sys
|
|
1312
|
+
from pathlib import Path
|
|
1313
|
+
from datetime import datetime
|
|
1314
|
+
|
|
1315
|
+
from claude_code_tools.session_utils import find_session_file, detect_agent_from_path
|
|
1316
|
+
from claude_code_tools.session_lineage import get_full_lineage_chain
|
|
1317
|
+
|
|
1318
|
+
if not session:
|
|
1319
|
+
_find_and_run_session_ui(
|
|
1320
|
+
session_id=None,
|
|
1321
|
+
agent_constraint='both',
|
|
1322
|
+
start_screen='lineage',
|
|
1323
|
+
)
|
|
1324
|
+
return
|
|
1325
|
+
|
|
1326
|
+
# Find session file
|
|
1327
|
+
input_path = Path(session).expanduser()
|
|
1328
|
+
if input_path.exists() and input_path.is_file():
|
|
1329
|
+
session_file = input_path
|
|
1330
|
+
detected_agent = agent or detect_agent_from_path(session_file)
|
|
1331
|
+
else:
|
|
1332
|
+
result = find_session_file(session)
|
|
1333
|
+
if not result:
|
|
1334
|
+
print(f"Error: Session not found: {session}", file=sys.stderr)
|
|
1335
|
+
sys.exit(1)
|
|
1336
|
+
detected_agent, session_file, _, _ = result
|
|
1337
|
+
if agent:
|
|
1338
|
+
detected_agent = agent
|
|
1339
|
+
|
|
1340
|
+
# Get lineage (returns newest-first, ending with original)
|
|
1341
|
+
lineage_chain = get_full_lineage_chain(session_file)
|
|
1342
|
+
|
|
1343
|
+
# Check if this is an original session (only one item with type "original")
|
|
1344
|
+
if len(lineage_chain) == 1 and lineage_chain[0][1] == "original":
|
|
1345
|
+
print("No lineage found (this is an original session).")
|
|
1346
|
+
return
|
|
1347
|
+
|
|
1348
|
+
lineage_data = []
|
|
1349
|
+
for path, derivation_type in lineage_chain:
|
|
1350
|
+
mod_time = datetime.fromtimestamp(path.stat().st_mtime)
|
|
1351
|
+
lineage_data.append({
|
|
1352
|
+
"session_id": path.stem,
|
|
1353
|
+
"file_path": str(path),
|
|
1354
|
+
"derivation_type": derivation_type,
|
|
1355
|
+
"modified": mod_time.isoformat(),
|
|
1356
|
+
})
|
|
1357
|
+
|
|
1358
|
+
if json_output:
|
|
1359
|
+
print(json_lib.dumps(lineage_data, indent=2))
|
|
1360
|
+
else:
|
|
1361
|
+
print(f"\nLineage for: {session_file.stem}")
|
|
1362
|
+
print(f"{'='*60}")
|
|
1363
|
+
print(f"Chain has {len(lineage_data)} session(s):\n")
|
|
1364
|
+
|
|
1365
|
+
for i, node in enumerate(lineage_data):
|
|
1366
|
+
# Current session or ancestor
|
|
1367
|
+
if i == 0:
|
|
1368
|
+
marker = "► "
|
|
1369
|
+
else:
|
|
1370
|
+
marker = " "
|
|
1371
|
+
|
|
1372
|
+
dtype = node["derivation_type"]
|
|
1373
|
+
fname = Path(node["file_path"]).name
|
|
1374
|
+
mod = node["modified"][:10]
|
|
1375
|
+
|
|
1376
|
+
if i == len(lineage_data) - 1:
|
|
1377
|
+
prefix = f"{marker}└─"
|
|
1378
|
+
else:
|
|
1379
|
+
prefix = f"{marker}├─"
|
|
1380
|
+
|
|
1381
|
+
print(f"{prefix} [{dtype:10}] {fname} ({mod})")
|
|
1382
|
+
|
|
1383
|
+
|
|
663
1384
|
@main.command("resume", context_settings={"ignore_unknown_options": True, "allow_extra_args": True, "allow_interspersed_args": False})
|
|
1385
|
+
@click.option(
|
|
1386
|
+
'--claude-home',
|
|
1387
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
1388
|
+
help='Claude home directory (default: ~/.claude or CLAUDE_CONFIG_DIR)',
|
|
1389
|
+
)
|
|
1390
|
+
@click.option(
|
|
1391
|
+
'--codex-home',
|
|
1392
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
1393
|
+
help='Codex home directory (default: ~/.codex or CODEX_HOME)',
|
|
1394
|
+
)
|
|
664
1395
|
@click.pass_context
|
|
665
|
-
def resume_session(ctx):
|
|
1396
|
+
def resume_session(ctx, claude_home, codex_home):
|
|
666
1397
|
"""Resume a session with various options (resume, clone, trim, continue).
|
|
667
1398
|
|
|
668
1399
|
If no session ID provided, finds latest session for current project/branch.
|
|
669
1400
|
Shows resume menu with options: resume as-is, clone, trim+resume,
|
|
670
1401
|
smart-trim, or continue with context.
|
|
1402
|
+
|
|
1403
|
+
\b
|
|
1404
|
+
Environment variables:
|
|
1405
|
+
CLAUDE_CONFIG_DIR - Default Claude home (overridden by --claude-home)
|
|
1406
|
+
CODEX_HOME - Default Codex home (overridden by --codex-home)
|
|
671
1407
|
"""
|
|
1408
|
+
# Merge with parent context options (CLI arg takes precedence)
|
|
1409
|
+
claude_home = claude_home or (ctx.obj or {}).get('claude_home')
|
|
1410
|
+
codex_home = codex_home or (ctx.obj or {}).get('codex_home')
|
|
1411
|
+
|
|
672
1412
|
args = ctx.args
|
|
673
1413
|
session_id = args[0] if args and not args[0].startswith('-') else None
|
|
674
1414
|
_find_and_run_session_ui(
|
|
@@ -1202,10 +1942,10 @@ def index_stats(index, cwd, claude_home, codex_home):
|
|
|
1202
1942
|
help='Filter to specific git branch (only effective when not global)')
|
|
1203
1943
|
@click.option('-n', '--num-results', type=int, default=None,
|
|
1204
1944
|
help='Limit number of results displayed')
|
|
1205
|
-
@click.option('--original', is_flag=True, help='
|
|
1206
|
-
@click.option('--sub-agent', is_flag=True, help='Include sub-agent sessions')
|
|
1207
|
-
@click.option('--trimmed', is_flag=True, help='
|
|
1208
|
-
@click.option('--rollover', is_flag=True, help='
|
|
1945
|
+
@click.option('--no-original', is_flag=True, help='Exclude original sessions')
|
|
1946
|
+
@click.option('--sub-agent', is_flag=True, help='Include sub-agent sessions (additive)')
|
|
1947
|
+
@click.option('--no-trimmed', is_flag=True, help='Exclude trimmed sessions')
|
|
1948
|
+
@click.option('--no-rollover', is_flag=True, help='Exclude rollover sessions')
|
|
1209
1949
|
@click.option('--min-lines', type=int, default=None,
|
|
1210
1950
|
help='Only show sessions with at least N lines')
|
|
1211
1951
|
@click.option('--after', metavar='DATE',
|
|
@@ -1224,7 +1964,7 @@ def index_stats(index, cwd, claude_home, codex_home):
|
|
|
1224
1964
|
@click.argument('query', required=False)
|
|
1225
1965
|
def search(
|
|
1226
1966
|
claude_home_arg, codex_home_arg, global_search, filter_dir, filter_branch,
|
|
1227
|
-
num_results,
|
|
1967
|
+
num_results, no_original, sub_agent, no_trimmed, no_rollover, min_lines,
|
|
1228
1968
|
after, before, agent, json_output, by_time, query
|
|
1229
1969
|
):
|
|
1230
1970
|
"""Launch interactive TUI for full-text session search.
|
|
@@ -1317,14 +2057,14 @@ def search(
|
|
|
1317
2057
|
rust_args.extend(["--branch", filter_branch])
|
|
1318
2058
|
if num_results:
|
|
1319
2059
|
rust_args.extend(["--num-results", str(num_results)])
|
|
1320
|
-
if
|
|
1321
|
-
rust_args.append("--original")
|
|
2060
|
+
if no_original:
|
|
2061
|
+
rust_args.append("--no-original")
|
|
1322
2062
|
if sub_agent:
|
|
1323
2063
|
rust_args.append("--sub-agent")
|
|
1324
|
-
if
|
|
1325
|
-
rust_args.append("--trimmed")
|
|
1326
|
-
if
|
|
1327
|
-
rust_args.append("--rollover")
|
|
2064
|
+
if no_trimmed:
|
|
2065
|
+
rust_args.append("--no-trimmed")
|
|
2066
|
+
if no_rollover:
|
|
2067
|
+
rust_args.append("--no-rollover")
|
|
1328
2068
|
if min_lines:
|
|
1329
2069
|
rust_args.extend(["--min-lines", str(min_lines)])
|
|
1330
2070
|
if after:
|
|
@@ -1490,15 +2230,17 @@ def search(
|
|
|
1490
2230
|
if filter_state.get("filter_branch"):
|
|
1491
2231
|
rust_args.extend(["--branch", filter_state["filter_branch"]])
|
|
1492
2232
|
|
|
1493
|
-
# Session type filters
|
|
1494
|
-
|
|
1495
|
-
|
|
2233
|
+
# Session type filters
|
|
2234
|
+
# Subtractive: add --no-* when type is excluded
|
|
2235
|
+
# Additive: add --sub-agent when sub-agents are included
|
|
2236
|
+
if not filter_state.get("include_original", True):
|
|
2237
|
+
rust_args.append("--no-original")
|
|
1496
2238
|
if filter_state.get("include_sub"):
|
|
1497
2239
|
rust_args.append("--sub-agent")
|
|
1498
|
-
if filter_state.get("include_trimmed"):
|
|
1499
|
-
rust_args.append("--trimmed")
|
|
1500
|
-
if filter_state.get("include_continued"):
|
|
1501
|
-
rust_args.append("--rollover")
|
|
2240
|
+
if not filter_state.get("include_trimmed", True):
|
|
2241
|
+
rust_args.append("--no-trimmed")
|
|
2242
|
+
if not filter_state.get("include_continued", True):
|
|
2243
|
+
rust_args.append("--no-rollover")
|
|
1502
2244
|
|
|
1503
2245
|
# Other filters
|
|
1504
2246
|
if filter_state.get("filter_min_lines"):
|