claude-code-tools 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-code-tools might be problematic. Click here for more details.

@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -346,8 +346,8 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str,
346
346
  ui_console.print(table)
347
347
  ui_console.print("\n[bold]Select a session:[/bold]")
348
348
  ui_console.print(f" • Enter number (1-{len(display_sessions)}) to select")
349
- ui_console.print(" • Press Ctrl+C to cancel\n")
350
-
349
+ ui_console.print(" • Press Enter to cancel\n")
350
+
351
351
  while True:
352
352
  try:
353
353
  # In stderr mode, we need to ensure nothing goes to stdout
@@ -355,39 +355,148 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str,
355
355
  # Temporarily redirect stdout to devnull
356
356
  old_stdout = sys.stdout
357
357
  sys.stdout = open(os.devnull, 'w')
358
-
358
+
359
359
  choice = Prompt.ask(
360
360
  "Your choice",
361
- choices=[str(i) for i in range(1, len(display_sessions) + 1)],
362
- show_choices=False,
361
+ default="",
362
+ show_default=False,
363
363
  console=ui_console
364
364
  )
365
-
366
- # Handle empty input
365
+
366
+ # Handle empty input - cancel
367
367
  if not choice or not choice.strip():
368
- ui_console.print("[red]Invalid choice. Please try again.[/red]")
369
- continue
370
-
368
+ # Restore stdout first
369
+ if stderr_mode:
370
+ sys.stdout.close()
371
+ sys.stdout = old_stdout
372
+ ui_console.print("[yellow]Cancelled[/yellow]")
373
+ return None
374
+
371
375
  # Restore stdout
372
376
  if stderr_mode:
373
377
  sys.stdout.close()
374
378
  sys.stdout = old_stdout
375
-
379
+
376
380
  idx = int(choice) - 1
377
381
  if 0 <= idx < len(display_sessions):
378
382
  session_info = display_sessions[idx]
379
- return (session_info[0], session_info[5]) # Return (session_id, project_path)
380
-
383
+ return session_info # Return full session tuple
384
+ else:
385
+ ui_console.print("[red]Invalid choice. Please try again.[/red]")
386
+
381
387
  except KeyboardInterrupt:
388
+ # Restore stdout if needed
389
+ if stderr_mode and sys.stdout != old_stdout:
390
+ sys.stdout.close()
391
+ sys.stdout = old_stdout
382
392
  ui_console.print("\n[yellow]Cancelled[/yellow]")
383
393
  return None
384
394
  except EOFError:
395
+ # Restore stdout if needed
396
+ if stderr_mode and sys.stdout != old_stdout:
397
+ sys.stdout.close()
398
+ sys.stdout = old_stdout
385
399
  ui_console.print("\n[yellow]Cancelled (EOF)[/yellow]")
386
400
  return None
387
401
  except ValueError:
388
402
  ui_console.print("[red]Invalid choice. Please try again.[/red]")
389
403
 
390
404
 
405
+ def show_action_menu(session_info: Tuple[str, float, int, str, str, str, Optional[str]]) -> Optional[str]:
406
+ """
407
+ Show action menu for selected session.
408
+
409
+ Returns: action choice ('resume', 'path', 'copy') or None if cancelled
410
+ """
411
+ session_id, _, _, project_name, _, project_path, git_branch = session_info
412
+
413
+ print(f"\n=== Session: {session_id[:8]}... ===")
414
+ print(f"Project: {project_name}")
415
+ if git_branch:
416
+ print(f"Branch: {git_branch}")
417
+ print(f"\nWhat would you like to do?")
418
+ print("1. Resume session (default)")
419
+ print("2. Show session file path")
420
+ print("3. Copy session file to file (*.jsonl) or directory")
421
+ print()
422
+
423
+ try:
424
+ choice = input("Enter choice [1-3] (or Enter for 1): ").strip()
425
+ if not choice or choice == "1":
426
+ return "resume"
427
+ elif choice == "2":
428
+ return "path"
429
+ elif choice == "3":
430
+ return "copy"
431
+ else:
432
+ print("Invalid choice.")
433
+ return None
434
+ except KeyboardInterrupt:
435
+ print("\nCancelled.")
436
+ return None
437
+
438
+
439
+ def get_session_file_path(session_id: str, project_path: str, claude_home: Optional[str] = None) -> str:
440
+ """Get the full file path for a session."""
441
+ # Convert project path to Claude directory format
442
+ base_dir = Path(claude_home).expanduser() if claude_home else Path.home() / ".claude"
443
+ encoded_path = project_path.replace("/", "-")
444
+ claude_project_dir = base_dir / "projects" / encoded_path
445
+ return str(claude_project_dir / f"{session_id}.jsonl")
446
+
447
+
448
+ def copy_session_file(session_file_path: str) -> None:
449
+ """Copy session file to user-specified file or directory."""
450
+ try:
451
+ dest = input("\nEnter destination file or directory path: ").strip()
452
+ if not dest:
453
+ print("Cancelled.")
454
+ return
455
+
456
+ dest_path = Path(dest).expanduser()
457
+ source = Path(session_file_path)
458
+
459
+ # Determine if destination is a directory or file
460
+ if dest_path.exists():
461
+ if dest_path.is_dir():
462
+ # Copy into directory with original filename
463
+ dest_file = dest_path / source.name
464
+ else:
465
+ # Copy to specified file
466
+ dest_file = dest_path
467
+ else:
468
+ # Destination doesn't exist - check if it looks like a directory
469
+ if dest.endswith('/') or dest.endswith(os.sep):
470
+ # Treat as directory - create it
471
+ create = input(f"Directory {dest_path} does not exist. Create it? [y/N]: ").strip().lower()
472
+ if create in ('y', 'yes'):
473
+ dest_path.mkdir(parents=True, exist_ok=True)
474
+ dest_file = dest_path / source.name
475
+ else:
476
+ print("Cancelled.")
477
+ return
478
+ else:
479
+ # Treat as file - create parent directory if needed
480
+ parent = dest_path.parent
481
+ if not parent.exists():
482
+ create = input(f"Parent directory {parent} does not exist. Create it? [y/N]: ").strip().lower()
483
+ if create in ('y', 'yes'):
484
+ parent.mkdir(parents=True, exist_ok=True)
485
+ else:
486
+ print("Cancelled.")
487
+ return
488
+ dest_file = dest_path
489
+
490
+ import shutil
491
+ shutil.copy2(source, dest_file)
492
+ print(f"\nCopied to: {dest_file}")
493
+
494
+ except KeyboardInterrupt:
495
+ print("\nCancelled.")
496
+ except Exception as e:
497
+ print(f"\nError copying file: {e}")
498
+
499
+
391
500
  def resume_session(session_id: str, project_path: str, shell_mode: bool = False):
392
501
  """Resume a Claude session using claude -r command."""
393
502
  current_dir = os.getcwd()
@@ -532,10 +641,26 @@ To persist directory changes when resuming sessions:
532
641
 
533
642
  # If we have rich and there are results, show interactive UI
534
643
  if RICH_AVAILABLE and console:
535
- result = display_interactive_ui(matching_sessions, keywords, stderr_mode=args.shell, num_matches=args.num_matches)
536
- if result:
537
- session_id, project_path = result
538
- resume_session(session_id, project_path, shell_mode=args.shell)
644
+ selected_session = display_interactive_ui(matching_sessions, keywords, stderr_mode=args.shell, num_matches=args.num_matches)
645
+ if selected_session:
646
+ # Show action menu
647
+ action = show_action_menu(selected_session)
648
+ if not action:
649
+ return
650
+
651
+ session_id = selected_session[0]
652
+ project_path = selected_session[5]
653
+
654
+ # Perform selected action
655
+ if action == "resume":
656
+ resume_session(session_id, project_path, shell_mode=args.shell)
657
+ elif action == "path":
658
+ session_file_path = get_session_file_path(session_id, project_path, args.claude_home)
659
+ print(f"\nSession file path:")
660
+ print(session_file_path)
661
+ elif action == "copy":
662
+ session_file_path = get_session_file_path(session_id, project_path, args.claude_home)
663
+ copy_session_file(session_file_path)
539
664
  else:
540
665
  # Fallback: print session IDs as before
541
666
  if not args.shell:
@@ -280,11 +280,11 @@ def find_sessions(
280
280
 
281
281
  def display_interactive_ui(
282
282
  matches: list[dict],
283
- ) -> Optional[tuple[str, str]]:
283
+ ) -> Optional[dict]:
284
284
  """
285
285
  Display matches in interactive UI and get user selection.
286
286
 
287
- Returns: (session_id, cwd) or None if cancelled
287
+ Returns: selected match dict or None if cancelled
288
288
  """
289
289
  if not matches:
290
290
  print("No matching sessions found.")
@@ -328,26 +328,113 @@ def display_interactive_ui(
328
328
  # Get user selection
329
329
  if len(matches) == 1:
330
330
  print(f"\nAuto-selecting only match: {matches[0]['session_id'][:16]}...")
331
- return matches[0]["session_id"], matches[0]["cwd"]
331
+ return matches[0]
332
332
 
333
333
  try:
334
334
  choice = input(
335
- "\nEnter number to resume session (or Enter to cancel): "
335
+ "\nEnter number to select session (or Enter to cancel): "
336
336
  ).strip()
337
337
  if not choice:
338
+ print("Cancelled.")
338
339
  return None
339
340
 
340
341
  idx = int(choice) - 1
341
342
  if 0 <= idx < len(matches):
342
- return matches[idx]["session_id"], matches[idx]["cwd"]
343
+ return matches[idx]
343
344
  else:
344
345
  print("Invalid selection.")
345
346
  return None
346
- except (ValueError, KeyboardInterrupt):
347
+ except ValueError:
348
+ print("Invalid input.")
349
+ return None
350
+ except KeyboardInterrupt:
347
351
  print("\nCancelled.")
348
352
  return None
349
353
 
350
354
 
355
+ def show_action_menu(match: dict) -> Optional[str]:
356
+ """
357
+ Show action menu for selected session.
358
+
359
+ Returns: action choice ('resume', 'path', 'copy') or None if cancelled
360
+ """
361
+ print(f"\n=== Session: {match['session_id'][:16]}... ===")
362
+ print(f"Project: {match['project']}")
363
+ print(f"Branch: {match['branch']}")
364
+ print(f"\nWhat would you like to do?")
365
+ print("1. Resume session (default)")
366
+ print("2. Show session file path")
367
+ print("3. Copy session file to file (*.jsonl) or directory")
368
+ print()
369
+
370
+ try:
371
+ choice = input("Enter choice [1-3] (or Enter for 1): ").strip()
372
+ if not choice or choice == "1":
373
+ return "resume"
374
+ elif choice == "2":
375
+ return "path"
376
+ elif choice == "3":
377
+ return "copy"
378
+ else:
379
+ print("Invalid choice.")
380
+ return None
381
+ except KeyboardInterrupt:
382
+ print("\nCancelled.")
383
+ return None
384
+
385
+
386
+ def copy_session_file(file_path: str) -> None:
387
+ """Copy session file to user-specified file or directory."""
388
+ try:
389
+ dest = input("\nEnter destination file or directory path: ").strip()
390
+ if not dest:
391
+ print("Cancelled.")
392
+ return
393
+
394
+ dest_path = Path(dest).expanduser()
395
+ source = Path(file_path)
396
+
397
+ # Determine if destination is a directory or file
398
+ if dest_path.exists():
399
+ if dest_path.is_dir():
400
+ # Copy into directory with original filename
401
+ dest_file = dest_path / source.name
402
+ else:
403
+ # Copy to specified file
404
+ dest_file = dest_path
405
+ else:
406
+ # Destination doesn't exist - check if it looks like a directory
407
+ if dest.endswith('/') or dest.endswith(os.sep):
408
+ # Treat as directory - create it
409
+ create = input(f"Directory {dest_path} does not exist. Create it? [y/N]: ").strip().lower()
410
+ if create in ('y', 'yes'):
411
+ dest_path.mkdir(parents=True, exist_ok=True)
412
+ dest_file = dest_path / source.name
413
+ else:
414
+ print("Cancelled.")
415
+ return
416
+ else:
417
+ # Treat as file - create parent directory if needed
418
+ parent = dest_path.parent
419
+ if not parent.exists():
420
+ create = input(f"Parent directory {parent} does not exist. Create it? [y/N]: ").strip().lower()
421
+ if create in ('y', 'yes'):
422
+ parent.mkdir(parents=True, exist_ok=True)
423
+ else:
424
+ print("Cancelled.")
425
+ return
426
+ dest_file = dest_path
427
+
428
+ import shutil
429
+ shutil.copy2(source, dest_file)
430
+ print(f"\nCopied to: {dest_file}")
431
+
432
+ except KeyboardInterrupt:
433
+ print("\nCancelled.")
434
+ except Exception as e:
435
+ print(f"\nError copying file: {e}")
436
+
437
+
351
438
  def resume_session(
352
439
  session_id: str, cwd: str, shell_mode: bool = False
353
440
  ) -> None:
@@ -448,10 +535,27 @@ Examples:
448
535
  )
449
536
 
450
537
  # Display and get selection
451
- result = display_interactive_ui(matches)
452
- if result:
453
- session_id, cwd = result
454
- resume_session(session_id, cwd, args.shell)
538
+ selected_match = display_interactive_ui(matches)
539
+ if not selected_match:
540
+ return
541
+
542
+ # Show action menu
543
+ action = show_action_menu(selected_match)
544
+ if not action:
545
+ return
546
+
547
+ # Perform selected action
548
+ if action == "resume":
549
+ resume_session(
550
+ selected_match["session_id"],
551
+ selected_match["cwd"],
552
+ args.shell
553
+ )
554
+ elif action == "path":
555
+ print(f"\nSession file path:")
556
+ print(selected_match["file_path"])
557
+ elif action == "copy":
558
+ copy_session_file(selected_match["file_path"])
455
559
 
456
560
 
457
561
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -225,9 +225,9 @@ won't persist after exiting Claude Code.
225
225
 
226
226
  For detailed documentation, see [docs/find-claude-session.md](docs/find-claude-session.md).
227
227
 
228
- Looks like this --
228
+ Looks like this --
229
229
 
230
- ![fcs.png](docs/fcs.png)
230
+ ![find-claude-session.png](demos/find-claude-session.png)
231
231
 
232
232
  <a id="find-codex-session"></a>
233
233
  ## 🔍 find-codex-session
@@ -268,6 +268,10 @@ find-codex-session "keywords" --codex-home /custom/path
268
268
  - Reverse chronological ordering (most recent first)
269
269
  - Multi-line preview wrapping for better readability
270
270
 
271
+ Looks like this --
272
+
273
+ ![find-codex-session.png](demos/find-codex-session.png)
274
+
271
275
  <a id="vault"></a>
272
276
  ## 🔐 vault
273
277
 
@@ -1,9 +1,9 @@
1
- claude_code_tools/__init__.py,sha256=CT_2W9O4WI9FzVRSv2aWi3GUcT26f2XEaWEPlJzZmhc,89
1
+ claude_code_tools/__init__.py,sha256=AzSPIlHglKw2AVy9dWwJmnRBYKIN4vLOFYIfPXvjtqE,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=zHPIGcq8Af1IRAGd0BTfjLWQngxy7zMzYuSeWfnsVR0,24201
6
- claude_code_tools/find_codex_session.py,sha256=gx2QK001QHrVNRpV8_-wjCSGwpK-YAosyRMO5ltaXyU,15583
5
+ claude_code_tools/find_claude_session.py,sha256=06xGZCG5kGN2IxSKf0oCVKl_CrC8JYbInl8c7OKwUm8,28929
6
+ claude_code_tools/find_codex_session.py,sha256=NvFATapn8aGTXOJmhu8tis9PA12ZpKz7cWYRsZ4vMSQ,19003
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.0.dist-info/METADATA,sha256=5brLd-Ya8_bGL6fm_Ee35I6eFvr3trzm-HRGK4V4ed8,16925
19
- claude_code_tools-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- claude_code_tools-0.2.0.dist-info/entry_points.txt,sha256=rAHzNUN7b_HIRbFlvpYwK38FG6jREYWaO0ssnhAVPrg,287
21
- claude_code_tools-0.2.0.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
22
- claude_code_tools-0.2.0.dist-info/RECORD,,
18
+ claude_code_tools-0.2.1.dist-info/METADATA,sha256=hRnH4OIKTQL9oTuoG0XEMPn-LPT78EJkaLYiA9fAbqg,17034
19
+ claude_code_tools-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ claude_code_tools-0.2.1.dist-info/entry_points.txt,sha256=rAHzNUN7b_HIRbFlvpYwK38FG6jREYWaO0ssnhAVPrg,287
21
+ claude_code_tools-0.2.1.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
22
+ claude_code_tools-0.2.1.dist-info/RECORD,,