claude-code-tools 0.2.0__py3-none-any.whl → 0.2.2__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.
Potentially problematic release.
This version of claude-code-tools might be problematic. Click here for more details.
- claude_code_tools/__init__.py +1 -1
- claude_code_tools/find_claude_session.py +178 -39
- claude_code_tools/find_codex_session.py +127 -26
- {claude_code_tools-0.2.0.dist-info → claude_code_tools-0.2.2.dist-info}/METADATA +19 -4
- {claude_code_tools-0.2.0.dist-info → claude_code_tools-0.2.2.dist-info}/RECORD +8 -8
- {claude_code_tools-0.2.0.dist-info → claude_code_tools-0.2.2.dist-info}/WHEEL +0 -0
- {claude_code_tools-0.2.0.dist-info → claude_code_tools-0.2.2.dist-info}/entry_points.txt +0 -0
- {claude_code_tools-0.2.0.dist-info → claude_code_tools-0.2.2.dist-info}/licenses/LICENSE +0 -0
claude_code_tools/__init__.py
CHANGED
|
@@ -220,17 +220,17 @@ def get_session_preview(filepath: Path) -> str:
|
|
|
220
220
|
return last_user_message if last_user_message else "No preview available"
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
def find_sessions(keywords: List[str], global_search: bool = False, claude_home: Optional[str] = None) -> List[Tuple[str, float, int, str, str, str, Optional[str]]]:
|
|
223
|
+
def find_sessions(keywords: List[str], global_search: bool = False, claude_home: Optional[str] = None) -> List[Tuple[str, float, float, int, str, str, str, Optional[str]]]:
|
|
224
224
|
"""
|
|
225
225
|
Find all Claude Code sessions containing the specified keywords.
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
Args:
|
|
228
228
|
keywords: List of keywords to search for
|
|
229
229
|
global_search: If True, search all projects; if False, search current project only
|
|
230
230
|
claude_home: Optional custom Claude home directory (defaults to ~/.claude)
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
Returns:
|
|
233
|
-
List of tuples (session_id, modification_time, line_count, project_name, preview, project_path, git_branch) sorted by modification time
|
|
233
|
+
List of tuples (session_id, modification_time, creation_time, line_count, project_name, preview, project_path, git_branch) sorted by modification time
|
|
234
234
|
"""
|
|
235
235
|
matching_sessions = []
|
|
236
236
|
|
|
@@ -256,23 +256,29 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
|
|
|
256
256
|
matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
|
|
257
257
|
if matches:
|
|
258
258
|
session_id = jsonl_file.stem
|
|
259
|
-
|
|
259
|
+
stat = jsonl_file.stat()
|
|
260
|
+
mod_time = stat.st_mtime
|
|
261
|
+
# Get creation time (birthtime on macOS, ctime elsewhere)
|
|
262
|
+
create_time = getattr(stat, 'st_birthtime', stat.st_ctime)
|
|
260
263
|
preview = get_session_preview(jsonl_file)
|
|
261
|
-
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path, git_branch))
|
|
264
|
+
matching_sessions.append((session_id, mod_time, create_time, line_count, project_name, preview, original_path, git_branch))
|
|
262
265
|
|
|
263
266
|
progress.advance(task)
|
|
264
267
|
else:
|
|
265
268
|
# Fallback without rich
|
|
266
269
|
for project_dir, original_path in projects:
|
|
267
270
|
project_name = extract_project_name(original_path)
|
|
268
|
-
|
|
271
|
+
|
|
269
272
|
for jsonl_file in project_dir.glob("*.jsonl"):
|
|
270
273
|
matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
|
|
271
274
|
if matches:
|
|
272
275
|
session_id = jsonl_file.stem
|
|
273
|
-
|
|
276
|
+
stat = jsonl_file.stat()
|
|
277
|
+
mod_time = stat.st_mtime
|
|
278
|
+
# Get creation time (birthtime on macOS, ctime elsewhere)
|
|
279
|
+
create_time = getattr(stat, 'st_birthtime', stat.st_ctime)
|
|
274
280
|
preview = get_session_preview(jsonl_file)
|
|
275
|
-
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path, git_branch))
|
|
281
|
+
matching_sessions.append((session_id, mod_time, create_time, line_count, project_name, preview, original_path, git_branch))
|
|
276
282
|
else:
|
|
277
283
|
# Search current project only
|
|
278
284
|
claude_dir = get_claude_project_dir(claude_home)
|
|
@@ -287,9 +293,12 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
|
|
|
287
293
|
matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
|
|
288
294
|
if matches:
|
|
289
295
|
session_id = jsonl_file.stem
|
|
290
|
-
|
|
296
|
+
stat = jsonl_file.stat()
|
|
297
|
+
mod_time = stat.st_mtime
|
|
298
|
+
# Get creation time (birthtime on macOS, ctime elsewhere)
|
|
299
|
+
create_time = getattr(stat, 'st_birthtime', stat.st_ctime)
|
|
291
300
|
preview = get_session_preview(jsonl_file)
|
|
292
|
-
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, os.getcwd(), git_branch))
|
|
301
|
+
matching_sessions.append((session_id, mod_time, create_time, line_count, project_name, preview, os.getcwd(), git_branch))
|
|
293
302
|
|
|
294
303
|
# Sort by modification time (newest first)
|
|
295
304
|
matching_sessions.sort(key=lambda x: x[1], reverse=True)
|
|
@@ -326,19 +335,22 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str,
|
|
|
326
335
|
table.add_column("Session ID", style="dim")
|
|
327
336
|
table.add_column("Project", style="green")
|
|
328
337
|
table.add_column("Branch", style="magenta")
|
|
329
|
-
table.add_column("Date", style="blue")
|
|
338
|
+
table.add_column("Date-Range", style="blue")
|
|
330
339
|
table.add_column("Lines", style="cyan", justify="right")
|
|
331
340
|
table.add_column("Preview", style="white", max_width=60, overflow="fold")
|
|
332
341
|
|
|
333
|
-
for idx, (session_id, mod_time, line_count, project_name, preview, _, git_branch) in enumerate(display_sessions, 1):
|
|
334
|
-
|
|
342
|
+
for idx, (session_id, mod_time, create_time, line_count, project_name, preview, _, git_branch) in enumerate(display_sessions, 1):
|
|
343
|
+
# Format: "10/04 - 10/09 13:45"
|
|
344
|
+
create_date = datetime.fromtimestamp(create_time).strftime('%m/%d')
|
|
345
|
+
mod_date = datetime.fromtimestamp(mod_time).strftime('%m/%d %H:%M')
|
|
346
|
+
date_display = f"{create_date} - {mod_date}"
|
|
335
347
|
branch_display = git_branch if git_branch else "N/A"
|
|
336
348
|
table.add_row(
|
|
337
349
|
str(idx),
|
|
338
350
|
session_id[:8] + "...",
|
|
339
351
|
project_name,
|
|
340
352
|
branch_display,
|
|
341
|
-
|
|
353
|
+
date_display,
|
|
342
354
|
str(line_count),
|
|
343
355
|
preview
|
|
344
356
|
)
|
|
@@ -346,8 +358,8 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str,
|
|
|
346
358
|
ui_console.print(table)
|
|
347
359
|
ui_console.print("\n[bold]Select a session:[/bold]")
|
|
348
360
|
ui_console.print(f" • Enter number (1-{len(display_sessions)}) to select")
|
|
349
|
-
ui_console.print(" • Press
|
|
350
|
-
|
|
361
|
+
ui_console.print(" • Press Enter to cancel\n")
|
|
362
|
+
|
|
351
363
|
while True:
|
|
352
364
|
try:
|
|
353
365
|
# In stderr mode, we need to ensure nothing goes to stdout
|
|
@@ -355,39 +367,148 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str,
|
|
|
355
367
|
# Temporarily redirect stdout to devnull
|
|
356
368
|
old_stdout = sys.stdout
|
|
357
369
|
sys.stdout = open(os.devnull, 'w')
|
|
358
|
-
|
|
370
|
+
|
|
359
371
|
choice = Prompt.ask(
|
|
360
372
|
"Your choice",
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
default="",
|
|
374
|
+
show_default=False,
|
|
363
375
|
console=ui_console
|
|
364
376
|
)
|
|
365
|
-
|
|
366
|
-
# Handle empty input
|
|
377
|
+
|
|
378
|
+
# Handle empty input - cancel
|
|
367
379
|
if not choice or not choice.strip():
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
# Restore stdout first
|
|
381
|
+
if stderr_mode:
|
|
382
|
+
sys.stdout.close()
|
|
383
|
+
sys.stdout = old_stdout
|
|
384
|
+
ui_console.print("[yellow]Cancelled[/yellow]")
|
|
385
|
+
return None
|
|
386
|
+
|
|
371
387
|
# Restore stdout
|
|
372
388
|
if stderr_mode:
|
|
373
389
|
sys.stdout.close()
|
|
374
390
|
sys.stdout = old_stdout
|
|
375
|
-
|
|
391
|
+
|
|
376
392
|
idx = int(choice) - 1
|
|
377
393
|
if 0 <= idx < len(display_sessions):
|
|
378
394
|
session_info = display_sessions[idx]
|
|
379
|
-
return
|
|
380
|
-
|
|
395
|
+
return session_info # Return full session tuple
|
|
396
|
+
else:
|
|
397
|
+
ui_console.print("[red]Invalid choice. Please try again.[/red]")
|
|
398
|
+
|
|
381
399
|
except KeyboardInterrupt:
|
|
400
|
+
# Restore stdout if needed
|
|
401
|
+
if stderr_mode and sys.stdout != old_stdout:
|
|
402
|
+
sys.stdout.close()
|
|
403
|
+
sys.stdout = old_stdout
|
|
382
404
|
ui_console.print("\n[yellow]Cancelled[/yellow]")
|
|
383
405
|
return None
|
|
384
406
|
except EOFError:
|
|
407
|
+
# Restore stdout if needed
|
|
408
|
+
if stderr_mode and sys.stdout != old_stdout:
|
|
409
|
+
sys.stdout.close()
|
|
410
|
+
sys.stdout = old_stdout
|
|
385
411
|
ui_console.print("\n[yellow]Cancelled (EOF)[/yellow]")
|
|
386
412
|
return None
|
|
387
413
|
except ValueError:
|
|
388
414
|
ui_console.print("[red]Invalid choice. Please try again.[/red]")
|
|
389
415
|
|
|
390
416
|
|
|
417
|
+
def show_action_menu(session_info: Tuple[str, float, float, int, str, str, str, Optional[str]]) -> Optional[str]:
|
|
418
|
+
"""
|
|
419
|
+
Show action menu for selected session.
|
|
420
|
+
|
|
421
|
+
Returns: action choice ('resume', 'path', 'copy') or None if cancelled
|
|
422
|
+
"""
|
|
423
|
+
session_id, _, _, _, project_name, _, project_path, git_branch = session_info
|
|
424
|
+
|
|
425
|
+
print(f"\n=== Session: {session_id[:8]}... ===")
|
|
426
|
+
print(f"Project: {project_name}")
|
|
427
|
+
if git_branch:
|
|
428
|
+
print(f"Branch: {git_branch}")
|
|
429
|
+
print(f"\nWhat would you like to do?")
|
|
430
|
+
print("1. Resume session (default)")
|
|
431
|
+
print("2. Show session file path")
|
|
432
|
+
print("3. Copy session file to file (*.jsonl) or directory")
|
|
433
|
+
print()
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
choice = input("Enter choice [1-3] (or Enter for 1): ").strip()
|
|
437
|
+
if not choice or choice == "1":
|
|
438
|
+
return "resume"
|
|
439
|
+
elif choice == "2":
|
|
440
|
+
return "path"
|
|
441
|
+
elif choice == "3":
|
|
442
|
+
return "copy"
|
|
443
|
+
else:
|
|
444
|
+
print("Invalid choice.")
|
|
445
|
+
return None
|
|
446
|
+
except KeyboardInterrupt:
|
|
447
|
+
print("\nCancelled.")
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def get_session_file_path(session_id: str, project_path: str, claude_home: Optional[str] = None) -> str:
|
|
452
|
+
"""Get the full file path for a session."""
|
|
453
|
+
# Convert project path to Claude directory format
|
|
454
|
+
base_dir = Path(claude_home).expanduser() if claude_home else Path.home() / ".claude"
|
|
455
|
+
encoded_path = project_path.replace("/", "-")
|
|
456
|
+
claude_project_dir = base_dir / "projects" / encoded_path
|
|
457
|
+
return str(claude_project_dir / f"{session_id}.jsonl")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def copy_session_file(session_file_path: str) -> None:
|
|
461
|
+
"""Copy session file to user-specified file or directory."""
|
|
462
|
+
try:
|
|
463
|
+
dest = input("\nEnter destination file or directory path: ").strip()
|
|
464
|
+
if not dest:
|
|
465
|
+
print("Cancelled.")
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
dest_path = Path(dest).expanduser()
|
|
469
|
+
source = Path(session_file_path)
|
|
470
|
+
|
|
471
|
+
# Determine if destination is a directory or file
|
|
472
|
+
if dest_path.exists():
|
|
473
|
+
if dest_path.is_dir():
|
|
474
|
+
# Copy into directory with original filename
|
|
475
|
+
dest_file = dest_path / source.name
|
|
476
|
+
else:
|
|
477
|
+
# Copy to specified file
|
|
478
|
+
dest_file = dest_path
|
|
479
|
+
else:
|
|
480
|
+
# Destination doesn't exist - check if it looks like a directory
|
|
481
|
+
if dest.endswith('/') or dest.endswith(os.sep):
|
|
482
|
+
# Treat as directory - create it
|
|
483
|
+
create = input(f"Directory {dest_path} does not exist. Create it? [y/N]: ").strip().lower()
|
|
484
|
+
if create in ('y', 'yes'):
|
|
485
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
dest_file = dest_path / source.name
|
|
487
|
+
else:
|
|
488
|
+
print("Cancelled.")
|
|
489
|
+
return
|
|
490
|
+
else:
|
|
491
|
+
# Treat as file - create parent directory if needed
|
|
492
|
+
parent = dest_path.parent
|
|
493
|
+
if not parent.exists():
|
|
494
|
+
create = input(f"Parent directory {parent} does not exist. Create it? [y/N]: ").strip().lower()
|
|
495
|
+
if create in ('y', 'yes'):
|
|
496
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
else:
|
|
498
|
+
print("Cancelled.")
|
|
499
|
+
return
|
|
500
|
+
dest_file = dest_path
|
|
501
|
+
|
|
502
|
+
import shutil
|
|
503
|
+
shutil.copy2(source, dest_file)
|
|
504
|
+
print(f"\nCopied to: {dest_file}")
|
|
505
|
+
|
|
506
|
+
except KeyboardInterrupt:
|
|
507
|
+
print("\nCancelled.")
|
|
508
|
+
except Exception as e:
|
|
509
|
+
print(f"\nError copying file: {e}")
|
|
510
|
+
|
|
511
|
+
|
|
391
512
|
def resume_session(session_id: str, project_path: str, shell_mode: bool = False):
|
|
392
513
|
"""Resume a Claude session using claude -r command."""
|
|
393
514
|
current_dir = os.getcwd()
|
|
@@ -532,30 +653,48 @@ To persist directory changes when resuming sessions:
|
|
|
532
653
|
|
|
533
654
|
# If we have rich and there are results, show interactive UI
|
|
534
655
|
if RICH_AVAILABLE and console:
|
|
535
|
-
|
|
536
|
-
if
|
|
537
|
-
|
|
538
|
-
|
|
656
|
+
selected_session = display_interactive_ui(matching_sessions, keywords, stderr_mode=args.shell, num_matches=args.num_matches)
|
|
657
|
+
if selected_session:
|
|
658
|
+
# Show action menu
|
|
659
|
+
action = show_action_menu(selected_session)
|
|
660
|
+
if not action:
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
session_id = selected_session[0]
|
|
664
|
+
project_path = selected_session[6] # Updated index after adding creation_time
|
|
665
|
+
|
|
666
|
+
# Perform selected action
|
|
667
|
+
if action == "resume":
|
|
668
|
+
resume_session(session_id, project_path, shell_mode=args.shell)
|
|
669
|
+
elif action == "path":
|
|
670
|
+
session_file_path = get_session_file_path(session_id, project_path, args.claude_home)
|
|
671
|
+
print(f"\nSession file path:")
|
|
672
|
+
print(session_file_path)
|
|
673
|
+
elif action == "copy":
|
|
674
|
+
session_file_path = get_session_file_path(session_id, project_path, args.claude_home)
|
|
675
|
+
copy_session_file(session_file_path)
|
|
539
676
|
else:
|
|
540
677
|
# Fallback: print session IDs as before
|
|
541
678
|
if not args.shell:
|
|
542
679
|
print("\nMatching sessions:")
|
|
543
|
-
for idx, (session_id, mod_time, line_count, project_name, preview, project_path, git_branch) in enumerate(matching_sessions[:args.num_matches], 1):
|
|
544
|
-
|
|
680
|
+
for idx, (session_id, mod_time, create_time, line_count, project_name, preview, project_path, git_branch) in enumerate(matching_sessions[:args.num_matches], 1):
|
|
681
|
+
create_date = datetime.fromtimestamp(create_time).strftime('%m/%d')
|
|
682
|
+
mod_date = datetime.fromtimestamp(mod_time).strftime('%m/%d %H:%M')
|
|
683
|
+
date_display = f"{create_date} - {mod_date}"
|
|
545
684
|
branch_display = git_branch if git_branch else "N/A"
|
|
546
685
|
if getattr(args, 'global'):
|
|
547
|
-
print(f"{idx}. {session_id} | {project_name} | {branch_display} | {
|
|
686
|
+
print(f"{idx}. {session_id} | {project_name} | {branch_display} | {date_display} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
|
|
548
687
|
else:
|
|
549
|
-
print(f"{idx}. {session_id} | {branch_display} | {
|
|
550
|
-
|
|
688
|
+
print(f"{idx}. {session_id} | {branch_display} | {date_display} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
|
|
689
|
+
|
|
551
690
|
if len(matching_sessions) > args.num_matches:
|
|
552
691
|
print(f"\n... and {len(matching_sessions) - args.num_matches} more sessions", file=sys.stderr if args.shell else sys.stdout)
|
|
553
|
-
|
|
692
|
+
|
|
554
693
|
# Simple selection without rich
|
|
555
694
|
if len(matching_sessions) == 1:
|
|
556
695
|
if not args.shell:
|
|
557
696
|
print("\nOnly one match found. Resuming automatically...")
|
|
558
|
-
session_id, _, _, _, _, project_path, _ = matching_sessions[0]
|
|
697
|
+
session_id, _, _, _, _, _, project_path, _ = matching_sessions[0]
|
|
559
698
|
resume_session(session_id, project_path, shell_mode=args.shell)
|
|
560
699
|
else:
|
|
561
700
|
try:
|
|
@@ -242,19 +242,15 @@ def find_sessions(
|
|
|
242
242
|
if current_cwd and metadata["cwd"] != current_cwd:
|
|
243
243
|
continue
|
|
244
244
|
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
date_str = timestamp_str[:16]
|
|
255
|
-
else:
|
|
256
|
-
# Fallback to directory date
|
|
257
|
-
date_str = f"{year_dir.name}-{month_dir.name}-{day_dir.name}"
|
|
245
|
+
# Get file stats for timestamps
|
|
246
|
+
stat = session_file.stat()
|
|
247
|
+
mod_time = stat.st_mtime
|
|
248
|
+
create_time = getattr(stat, 'st_birthtime', stat.st_ctime)
|
|
249
|
+
|
|
250
|
+
# Format dates: "10/04 - 10/09 13:45"
|
|
251
|
+
create_date = datetime.fromtimestamp(create_time).strftime("%m/%d")
|
|
252
|
+
mod_date = datetime.fromtimestamp(mod_time).strftime("%m/%d %H:%M")
|
|
253
|
+
date_str = f"{create_date} - {mod_date}"
|
|
258
254
|
|
|
259
255
|
matches.append(
|
|
260
256
|
{
|
|
@@ -262,6 +258,7 @@ def find_sessions(
|
|
|
262
258
|
"project": get_project_name(metadata["cwd"]),
|
|
263
259
|
"branch": metadata["branch"] or "",
|
|
264
260
|
"date": date_str,
|
|
261
|
+
"mod_time": mod_time, # For sorting
|
|
265
262
|
"lines": line_count,
|
|
266
263
|
"preview": preview or "No preview",
|
|
267
264
|
"cwd": metadata["cwd"],
|
|
@@ -273,18 +270,18 @@ def find_sessions(
|
|
|
273
270
|
if len(matches) >= num_matches * 3:
|
|
274
271
|
break
|
|
275
272
|
|
|
276
|
-
# Sort by
|
|
277
|
-
matches.sort(key=lambda x: x["
|
|
273
|
+
# Sort by modification time (newest first) and limit
|
|
274
|
+
matches.sort(key=lambda x: x["mod_time"], reverse=True)
|
|
278
275
|
return matches[:num_matches]
|
|
279
276
|
|
|
280
277
|
|
|
281
278
|
def display_interactive_ui(
|
|
282
279
|
matches: list[dict],
|
|
283
|
-
) -> Optional[
|
|
280
|
+
) -> Optional[dict]:
|
|
284
281
|
"""
|
|
285
282
|
Display matches in interactive UI and get user selection.
|
|
286
283
|
|
|
287
|
-
Returns:
|
|
284
|
+
Returns: selected match dict or None if cancelled
|
|
288
285
|
"""
|
|
289
286
|
if not matches:
|
|
290
287
|
print("No matching sessions found.")
|
|
@@ -297,7 +294,7 @@ def display_interactive_ui(
|
|
|
297
294
|
table.add_column("Session ID", style="yellow", no_wrap=True)
|
|
298
295
|
table.add_column("Project", style="green")
|
|
299
296
|
table.add_column("Branch", style="magenta")
|
|
300
|
-
table.add_column("Date", style="blue")
|
|
297
|
+
table.add_column("Date-Range", style="blue")
|
|
301
298
|
table.add_column("Lines", justify="right")
|
|
302
299
|
table.add_column("Preview", style="dim", max_width=60, overflow="fold")
|
|
303
300
|
|
|
@@ -328,26 +325,113 @@ def display_interactive_ui(
|
|
|
328
325
|
# Get user selection
|
|
329
326
|
if len(matches) == 1:
|
|
330
327
|
print(f"\nAuto-selecting only match: {matches[0]['session_id'][:16]}...")
|
|
331
|
-
return matches[0]
|
|
328
|
+
return matches[0]
|
|
332
329
|
|
|
333
330
|
try:
|
|
334
331
|
choice = input(
|
|
335
|
-
"\nEnter number to
|
|
332
|
+
"\nEnter number to select session (or Enter to cancel): "
|
|
336
333
|
).strip()
|
|
337
334
|
if not choice:
|
|
335
|
+
print("Cancelled.")
|
|
338
336
|
return None
|
|
339
337
|
|
|
340
338
|
idx = int(choice) - 1
|
|
341
339
|
if 0 <= idx < len(matches):
|
|
342
|
-
return matches[idx]
|
|
340
|
+
return matches[idx]
|
|
343
341
|
else:
|
|
344
342
|
print("Invalid selection.")
|
|
345
343
|
return None
|
|
346
|
-
except
|
|
344
|
+
except ValueError:
|
|
345
|
+
print("Invalid input.")
|
|
346
|
+
return None
|
|
347
|
+
except KeyboardInterrupt:
|
|
347
348
|
print("\nCancelled.")
|
|
348
349
|
return None
|
|
349
350
|
|
|
350
351
|
|
|
352
|
+
def show_action_menu(match: dict) -> Optional[str]:
|
|
353
|
+
"""
|
|
354
|
+
Show action menu for selected session.
|
|
355
|
+
|
|
356
|
+
Returns: action choice ('resume', 'path', 'copy') or None if cancelled
|
|
357
|
+
"""
|
|
358
|
+
print(f"\n=== Session: {match['session_id'][:16]}... ===")
|
|
359
|
+
print(f"Project: {match['project']}")
|
|
360
|
+
print(f"Branch: {match['branch']}")
|
|
361
|
+
print(f"\nWhat would you like to do?")
|
|
362
|
+
print("1. Resume session (default)")
|
|
363
|
+
print("2. Show session file path")
|
|
364
|
+
print("3. Copy session file to file (*.jsonl) or directory")
|
|
365
|
+
print()
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
choice = input("Enter choice [1-3] (or Enter for 1): ").strip()
|
|
369
|
+
if not choice or choice == "1":
|
|
370
|
+
return "resume"
|
|
371
|
+
elif choice == "2":
|
|
372
|
+
return "path"
|
|
373
|
+
elif choice == "3":
|
|
374
|
+
return "copy"
|
|
375
|
+
else:
|
|
376
|
+
print("Invalid choice.")
|
|
377
|
+
return None
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
print("\nCancelled.")
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def copy_session_file(file_path: str) -> None:
|
|
384
|
+
"""Copy session file to user-specified file or directory."""
|
|
385
|
+
try:
|
|
386
|
+
dest = input("\nEnter destination file or directory path: ").strip()
|
|
387
|
+
if not dest:
|
|
388
|
+
print("Cancelled.")
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
dest_path = Path(dest).expanduser()
|
|
392
|
+
source = Path(file_path)
|
|
393
|
+
|
|
394
|
+
# Determine if destination is a directory or file
|
|
395
|
+
if dest_path.exists():
|
|
396
|
+
if dest_path.is_dir():
|
|
397
|
+
# Copy into directory with original filename
|
|
398
|
+
dest_file = dest_path / source.name
|
|
399
|
+
else:
|
|
400
|
+
# Copy to specified file
|
|
401
|
+
dest_file = dest_path
|
|
402
|
+
else:
|
|
403
|
+
# Destination doesn't exist - check if it looks like a directory
|
|
404
|
+
if dest.endswith('/') or dest.endswith(os.sep):
|
|
405
|
+
# Treat as directory - create it
|
|
406
|
+
create = input(f"Directory {dest_path} does not exist. Create it? [y/N]: ").strip().lower()
|
|
407
|
+
if create in ('y', 'yes'):
|
|
408
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
dest_file = dest_path / source.name
|
|
410
|
+
else:
|
|
411
|
+
print("Cancelled.")
|
|
412
|
+
return
|
|
413
|
+
else:
|
|
414
|
+
# Treat as file - create parent directory if needed
|
|
415
|
+
parent = dest_path.parent
|
|
416
|
+
if not parent.exists():
|
|
417
|
+
create = input(f"Parent directory {parent} does not exist. Create it? [y/N]: ").strip().lower()
|
|
418
|
+
if create in ('y', 'yes'):
|
|
419
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
420
|
+
else:
|
|
421
|
+
print("Cancelled.")
|
|
422
|
+
return
|
|
423
|
+
dest_file = dest_path
|
|
424
|
+
|
|
425
|
+
import shutil
|
|
426
|
+
shutil.copy2(source, dest_file)
|
|
427
|
+
print(f"\nCopied to: {dest_file}")
|
|
428
|
+
|
|
429
|
+
except KeyboardInterrupt:
|
|
430
|
+
print("\nCancelled.")
|
|
431
|
+
except Exception as e:
|
|
432
|
+
print(f"\nError copying file: {e}")
|
|
433
|
+
|
|
434
|
+
|
|
351
435
|
def resume_session(
|
|
352
436
|
session_id: str, cwd: str, shell_mode: bool = False
|
|
353
437
|
) -> None:
|
|
@@ -448,10 +532,27 @@ Examples:
|
|
|
448
532
|
)
|
|
449
533
|
|
|
450
534
|
# Display and get selection
|
|
451
|
-
|
|
452
|
-
if
|
|
453
|
-
|
|
454
|
-
|
|
535
|
+
selected_match = display_interactive_ui(matches)
|
|
536
|
+
if not selected_match:
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
# Show action menu
|
|
540
|
+
action = show_action_menu(selected_match)
|
|
541
|
+
if not action:
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# Perform selected action
|
|
545
|
+
if action == "resume":
|
|
546
|
+
resume_session(
|
|
547
|
+
selected_match["session_id"],
|
|
548
|
+
selected_match["cwd"],
|
|
549
|
+
args.shell
|
|
550
|
+
)
|
|
551
|
+
elif action == "path":
|
|
552
|
+
print(f"\nSession file path:")
|
|
553
|
+
print(selected_match["file_path"])
|
|
554
|
+
elif action == "copy":
|
|
555
|
+
copy_session_file(selected_match["file_path"])
|
|
455
556
|
|
|
456
557
|
|
|
457
558
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-tools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Collection of tools for working with Claude Code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -215,19 +215,25 @@ fcs "keywords" -g
|
|
|
215
215
|
|
|
216
216
|
### Features
|
|
217
217
|
|
|
218
|
+
- **Action menu** after session selection:
|
|
219
|
+
- Resume session (default)
|
|
220
|
+
- Show session file path
|
|
221
|
+
- Copy session file to file (*.jsonl) or directory
|
|
218
222
|
- Interactive session selection with previews
|
|
219
|
-
- Cross-project search capabilities
|
|
223
|
+
- Cross-project search capabilities (local by default, `-g` for global)
|
|
224
|
+
- Shows last user message preview (filtered, multi-line wrapping)
|
|
220
225
|
- Automatic session resumption with `claude -r`
|
|
221
226
|
- Persistent directory changes when resuming cross-project sessions
|
|
227
|
+
- Press Enter to cancel (no need for Ctrl+C)
|
|
222
228
|
|
|
223
229
|
Note: You can also use `find-claude-session` directly, but directory changes
|
|
224
230
|
won't persist after exiting Claude Code.
|
|
225
231
|
|
|
226
232
|
For detailed documentation, see [docs/find-claude-session.md](docs/find-claude-session.md).
|
|
227
233
|
|
|
228
|
-
Looks like this --
|
|
234
|
+
Looks like this --
|
|
229
235
|
|
|
230
|
-

|
|
231
237
|
|
|
232
238
|
<a id="find-codex-session"></a>
|
|
233
239
|
## 🔍 find-codex-session
|
|
@@ -259,6 +265,10 @@ find-codex-session "keywords" --codex-home /custom/path
|
|
|
259
265
|
|
|
260
266
|
### Features
|
|
261
267
|
|
|
268
|
+
- **Action menu** after session selection:
|
|
269
|
+
- Resume session (default)
|
|
270
|
+
- Show session file path
|
|
271
|
+
- Copy session file to file (*.jsonl) or directory
|
|
262
272
|
- **Project filtering**: Search current project only (default) or all projects with `-g`
|
|
263
273
|
- Case-insensitive AND keyword search across all session content
|
|
264
274
|
- Interactive session selection with Rich table display
|
|
@@ -267,6 +277,11 @@ find-codex-session "keywords" --codex-home /custom/path
|
|
|
267
277
|
- Cross-project session support with directory change prompts
|
|
268
278
|
- Reverse chronological ordering (most recent first)
|
|
269
279
|
- Multi-line preview wrapping for better readability
|
|
280
|
+
- Press Enter to cancel (no need for Ctrl+C)
|
|
281
|
+
|
|
282
|
+
Looks like this --
|
|
283
|
+
|
|
284
|
+

|
|
270
285
|
|
|
271
286
|
<a id="vault"></a>
|
|
272
287
|
## 🔐 vault
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
claude_code_tools/__init__.py,sha256=
|
|
1
|
+
claude_code_tools/__init__.py,sha256=YHQZkBgVfpPN0Z0W8p9p6AdlvW5nzD1Rd79wUlJ6AdU,89
|
|
2
2
|
claude_code_tools/codex_bridge_mcp.py,sha256=0roYm3YgEFB6y2MvGovzHyY7avKtire4qBtz3kVaYoY,12596
|
|
3
3
|
claude_code_tools/dotenv_vault.py,sha256=KPI9NDFu5HE6FfhQUYw6RhdR-miN0ScJHsBg0OVG61k,9617
|
|
4
4
|
claude_code_tools/env_safe.py,sha256=TSSkOjEpzBwNgbeSR-0tR1-pAW_qmbZNmn3fiAsHJ4w,7659
|
|
5
|
-
claude_code_tools/find_claude_session.py,sha256=
|
|
6
|
-
claude_code_tools/find_codex_session.py,sha256=
|
|
5
|
+
claude_code_tools/find_claude_session.py,sha256=pXutZWDM51IaR1oxQeFXEzTwzMKyDLVv8HM6ONr3Iws,29935
|
|
6
|
+
claude_code_tools/find_codex_session.py,sha256=llTo8_QtGsxxRzCRm1pYXNq70kcZJRZySUnCCfqExZA,18947
|
|
7
7
|
claude_code_tools/tmux_cli_controller.py,sha256=5QDrDlv3oabIghRHuP8jMhUfxPeyYZxizNWW5sVuJIg,34607
|
|
8
8
|
claude_code_tools/tmux_remote_controller.py,sha256=eY1ouLtUzJ40Ik4nqUBvc3Gl1Rx0_L4TFW4j708lgvI,9942
|
|
9
9
|
docs/cc-codex-instructions.md,sha256=5E9QotkrcVYIE5VrvJGi-sg7tdyITDrsbhaqBKr4MUk,1109
|
|
@@ -15,8 +15,8 @@ docs/lmsh.md,sha256=o2TNP1Yfl3zW23GzEqK8Bx6z1hQof_lplaeEucuHNRU,1335
|
|
|
15
15
|
docs/reddit-post.md,sha256=ZA7kPoJNi06t6F9JQMBiIOv039ADC9lM8YXFt8UA_Jg,2345
|
|
16
16
|
docs/tmux-cli-instructions.md,sha256=hKGOdaPdBlb5XFzHfi0Mm7CVlysBuJUAfop3GHreyuw,5008
|
|
17
17
|
docs/vault-documentation.md,sha256=5XzNpHyhGU38JU2hKEWEL1gdPq3rC2zBg8yotK4eNF4,3600
|
|
18
|
-
claude_code_tools-0.2.
|
|
19
|
-
claude_code_tools-0.2.
|
|
20
|
-
claude_code_tools-0.2.
|
|
21
|
-
claude_code_tools-0.2.
|
|
22
|
-
claude_code_tools-0.2.
|
|
18
|
+
claude_code_tools-0.2.2.dist-info/METADATA,sha256=9BYCRDr8fE1LK3TzC7Zu58PZXQ_REGehQczPmT-au2w,17530
|
|
19
|
+
claude_code_tools-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
20
|
+
claude_code_tools-0.2.2.dist-info/entry_points.txt,sha256=rAHzNUN7b_HIRbFlvpYwK38FG6jREYWaO0ssnhAVPrg,287
|
|
21
|
+
claude_code_tools-0.2.2.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
|
|
22
|
+
claude_code_tools-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|