claude-code-tools 0.2.0__tar.gz → 0.2.2__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.

Potentially problematic release.


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

Files changed (22) hide show
  1. claude_code_tools-0.2.0/README.md → claude_code_tools-0.2.2/PKG-INFO +32 -3
  2. claude_code_tools-0.2.0/PKG-INFO → claude_code_tools-0.2.2/README.md +18 -17
  3. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/__init__.py +1 -1
  4. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/find_claude_session.py +178 -39
  5. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/find_codex_session.py +127 -26
  6. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/pyproject.toml +2 -2
  7. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/.gitignore +0 -0
  8. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/LICENSE +0 -0
  9. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/codex_bridge_mcp.py +0 -0
  10. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/dotenv_vault.py +0 -0
  11. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/env_safe.py +0 -0
  12. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/tmux_cli_controller.py +0 -0
  13. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/claude_code_tools/tmux_remote_controller.py +0 -0
  14. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/cc-codex-instructions.md +0 -0
  15. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/claude-code-chutes.md +0 -0
  16. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/claude-code-tmux-tutorials.md +0 -0
  17. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/dot-zshrc.md +0 -0
  18. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/find-claude-session.md +0 -0
  19. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/lmsh.md +0 -0
  20. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/reddit-post.md +0 -0
  21. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/tmux-cli-instructions.md +0 -0
  22. {claude_code_tools-0.2.0 → claude_code_tools-0.2.2}/docs/vault-documentation.md +0 -0
@@ -1,3 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-code-tools
3
+ Version: 0.2.2
4
+ Summary: Collection of tools for working with Claude Code
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.0.0
8
+ Requires-Dist: fire>=0.5.0
9
+ Requires-Dist: mcp>=1.13.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: commitizen>=3.0.0; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
1
15
  # claude-code-tools
2
16
 
3
17
  A collection of practical tools, hooks, and utilities for enhancing Claude Code
@@ -201,19 +215,25 @@ fcs "keywords" -g
201
215
 
202
216
  ### Features
203
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
204
222
  - Interactive session selection with previews
205
- - 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)
206
225
  - Automatic session resumption with `claude -r`
207
226
  - Persistent directory changes when resuming cross-project sessions
227
+ - Press Enter to cancel (no need for Ctrl+C)
208
228
 
209
229
  Note: You can also use `find-claude-session` directly, but directory changes
210
230
  won't persist after exiting Claude Code.
211
231
 
212
232
  For detailed documentation, see [docs/find-claude-session.md](docs/find-claude-session.md).
213
233
 
214
- Looks like this --
234
+ Looks like this --
215
235
 
216
- ![fcs.png](docs/fcs.png)
236
+ ![find-claude-session.png](demos/find-claude-session.png)
217
237
 
218
238
  <a id="find-codex-session"></a>
219
239
  ## 🔍 find-codex-session
@@ -245,6 +265,10 @@ find-codex-session "keywords" --codex-home /custom/path
245
265
 
246
266
  ### Features
247
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
248
272
  - **Project filtering**: Search current project only (default) or all projects with `-g`
249
273
  - Case-insensitive AND keyword search across all session content
250
274
  - Interactive session selection with Rich table display
@@ -253,6 +277,11 @@ find-codex-session "keywords" --codex-home /custom/path
253
277
  - Cross-project session support with directory change prompts
254
278
  - Reverse chronological ordering (most recent first)
255
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
+ ![find-codex-session.png](demos/find-codex-session.png)
256
285
 
257
286
  <a id="vault"></a>
258
287
  ## 🔐 vault
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: claude-code-tools
3
- Version: 0.2.0
4
- Summary: Collection of tools for working with Claude Code
5
- License-File: LICENSE
6
- Requires-Python: >=3.11
7
- Requires-Dist: click>=8.0.0
8
- Requires-Dist: fire>=0.5.0
9
- Requires-Dist: mcp>=1.13.0
10
- Requires-Dist: rich>=13.0.0
11
- Provides-Extra: dev
12
- Requires-Dist: commitizen>=3.0.0; extra == 'dev'
13
- Description-Content-Type: text/markdown
14
-
15
1
  # claude-code-tools
16
2
 
17
3
  A collection of practical tools, hooks, and utilities for enhancing Claude Code
@@ -215,19 +201,25 @@ fcs "keywords" -g
215
201
 
216
202
  ### Features
217
203
 
204
+ - **Action menu** after session selection:
205
+ - Resume session (default)
206
+ - Show session file path
207
+ - Copy session file to file (*.jsonl) or directory
218
208
  - Interactive session selection with previews
219
- - Cross-project search capabilities
209
+ - Cross-project search capabilities (local by default, `-g` for global)
210
+ - Shows last user message preview (filtered, multi-line wrapping)
220
211
  - Automatic session resumption with `claude -r`
221
212
  - Persistent directory changes when resuming cross-project sessions
213
+ - Press Enter to cancel (no need for Ctrl+C)
222
214
 
223
215
  Note: You can also use `find-claude-session` directly, but directory changes
224
216
  won't persist after exiting Claude Code.
225
217
 
226
218
  For detailed documentation, see [docs/find-claude-session.md](docs/find-claude-session.md).
227
219
 
228
- Looks like this --
220
+ Looks like this --
229
221
 
230
- ![fcs.png](docs/fcs.png)
222
+ ![find-claude-session.png](demos/find-claude-session.png)
231
223
 
232
224
  <a id="find-codex-session"></a>
233
225
  ## 🔍 find-codex-session
@@ -259,6 +251,10 @@ find-codex-session "keywords" --codex-home /custom/path
259
251
 
260
252
  ### Features
261
253
 
254
+ - **Action menu** after session selection:
255
+ - Resume session (default)
256
+ - Show session file path
257
+ - Copy session file to file (*.jsonl) or directory
262
258
  - **Project filtering**: Search current project only (default) or all projects with `-g`
263
259
  - Case-insensitive AND keyword search across all session content
264
260
  - Interactive session selection with Rich table display
@@ -267,6 +263,11 @@ find-codex-session "keywords" --codex-home /custom/path
267
263
  - Cross-project session support with directory change prompts
268
264
  - Reverse chronological ordering (most recent first)
269
265
  - Multi-line preview wrapping for better readability
266
+ - Press Enter to cancel (no need for Ctrl+C)
267
+
268
+ Looks like this --
269
+
270
+ ![find-codex-session.png](demos/find-codex-session.png)
270
271
 
271
272
  <a id="vault"></a>
272
273
  ## 🔐 vault
@@ -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.2"
@@ -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
- mod_time = jsonl_file.stat().st_mtime
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
- mod_time = jsonl_file.stat().st_mtime
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
- mod_time = jsonl_file.stat().st_mtime
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
- mod_date = datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M')
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
- mod_date,
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 Ctrl+C to cancel\n")
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
- choices=[str(i) for i in range(1, len(display_sessions) + 1)],
362
- show_choices=False,
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
- ui_console.print("[red]Invalid choice. Please try again.[/red]")
369
- continue
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 (session_info[0], session_info[5]) # Return (session_id, project_path)
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
- 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)
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
- mod_date = datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M:%S')
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} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
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} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
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
- # Parse timestamp
246
- timestamp_str = metadata["timestamp"]
247
- if timestamp_str:
248
- try:
249
- dt = datetime.fromisoformat(
250
- timestamp_str.replace("Z", "+00:00")
251
- )
252
- date_str = dt.strftime("%Y-%m-%d %H:%M")
253
- except ValueError:
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 date (reverse chronological) and limit
277
- matches.sort(key=lambda x: x["date"], reverse=True)
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[tuple[str, str]]:
280
+ ) -> Optional[dict]:
284
281
  """
285
282
  Display matches in interactive UI and get user selection.
286
283
 
287
- Returns: (session_id, cwd) or None if cancelled
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]["session_id"], matches[0]["cwd"]
328
+ return matches[0]
332
329
 
333
330
  try:
334
331
  choice = input(
335
- "\nEnter number to resume session (or Enter to cancel): "
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]["session_id"], matches[idx]["cwd"]
340
+ return matches[idx]
343
341
  else:
344
342
  print("Invalid selection.")
345
343
  return None
346
- except (ValueError, KeyboardInterrupt):
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
- result = display_interactive_ui(matches)
452
- if result:
453
- session_id, cwd = result
454
- resume_session(session_id, cwd, args.shell)
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
  [project]
2
2
  name = "claude-code-tools"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Collection of tools for working with Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -43,7 +43,7 @@ exclude = [
43
43
 
44
44
  [tool.commitizen]
45
45
  name = "cz_conventional_commits"
46
- version = "0.2.0"
46
+ version = "0.2.2"
47
47
  tag_format = "v$version"
48
48
  version_files = [
49
49
  "pyproject.toml:version",