claude-code-tools 0.2.5__tar.gz → 0.2.7__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 (23) hide show
  1. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/PKG-INFO +134 -8
  2. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/README.md +133 -7
  3. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/__init__.py +1 -1
  4. claude_code_tools-0.2.7/claude_code_tools/find_session.py +447 -0
  5. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/pyproject.toml +3 -2
  6. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/.gitignore +0 -0
  7. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/LICENSE +0 -0
  8. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/codex_bridge_mcp.py +0 -0
  9. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/dotenv_vault.py +0 -0
  10. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/env_safe.py +0 -0
  11. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/find_claude_session.py +0 -0
  12. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/find_codex_session.py +0 -0
  13. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/tmux_cli_controller.py +0 -0
  14. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/claude_code_tools/tmux_remote_controller.py +0 -0
  15. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/cc-codex-instructions.md +0 -0
  16. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/claude-code-chutes.md +0 -0
  17. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/claude-code-tmux-tutorials.md +0 -0
  18. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/dot-zshrc.md +0 -0
  19. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/find-claude-session.md +0 -0
  20. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/lmsh.md +0 -0
  21. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/reddit-post.md +0 -0
  22. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/tmux-cli-instructions.md +0 -0
  23. {claude_code_tools-0.2.5 → claude_code_tools-0.2.7}/docs/vault-documentation.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -23,6 +23,7 @@ and other CLI coding agents.
23
23
  - [🚀 Quick Start](#quick-start)
24
24
  - [🎮 tmux-cli Deep Dive](#tmux-cli-deep-dive)
25
25
  - [🚀 lmsh (Experimental) — natural language to shell commands](#lmsh-experimental)
26
+ - [🔍 find-session — unified search across Claude & Codex sessions](#find-session)
26
27
  - [🔍 find-claude-session — search and resume Claude sessions](#find-claude-session)
27
28
  - [🔍 find-codex-session — search and resume Codex sessions](#find-codex-session)
28
29
  - [🔐 vault — encrypted .env backup & sync](#vault)
@@ -81,6 +82,7 @@ uv tool install git+https://github.com/pchalasani/claude-code-tools
81
82
 
82
83
  This gives you:
83
84
  - `tmux-cli` - The interactive CLI controller we just covered
85
+ - `find-session` - Unified search across Claude Code and Codex sessions
84
86
  - `find-claude-session` - Search and resume Claude Code sessions by keywords
85
87
  - `find-codex-session` - Search and resume Codex sessions by keywords
86
88
  - `vault` - Encrypted backup for your .env files
@@ -175,6 +177,107 @@ cp target/release/lmsh ~/.cargo/bin/
175
177
 
176
178
  See [docs/lmsh.md](docs/lmsh.md) for details.
177
179
 
180
+ <a id="find-session"></a>
181
+ ## 🔍 find-session
182
+
183
+ **Unified session finder** - Search across both Claude Code and Codex sessions simultaneously.
184
+
185
+ ### Setup (Recommended)
186
+
187
+ Add this function to your shell config (.bashrc/.zshrc) for persistent directory changes:
188
+
189
+ ```bash
190
+ fs() {
191
+ # Check if user is asking for help
192
+ if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
193
+ find-session --help
194
+ return
195
+ fi
196
+ # Run find-session in shell mode and evaluate the output
197
+ eval "$(find-session --shell "$@" | sed '/^$/d')"
198
+ }
199
+ ```
200
+
201
+ Or source the provided function:
202
+ ```bash
203
+ source /path/to/claude-code-tools/scripts/fs-function.sh
204
+ ```
205
+
206
+ **Why use the shell wrapper?** When you resume a session from a different directory, the wrapper ensures the directory change persists after the session exits. Without it, you'll be back in your original directory after exiting the session (though the session itself runs in the correct directory).
207
+
208
+ ### Usage
209
+
210
+ ```bash
211
+ # Search all agents in current project
212
+ fs "keyword1,keyword2"
213
+
214
+ # Show all sessions across all agents in current project
215
+ fs
216
+
217
+ # Search across all projects (Claude + Codex)
218
+ fs "keywords" -g
219
+
220
+ # Show all sessions across all projects
221
+ fs -g
222
+
223
+ # Search only specific agent(s)
224
+ fs "bug,fix" --agents claude
225
+ fs "error" --agents codex
226
+
227
+ # Limit number of results
228
+ fs "keywords" -n 15
229
+ ```
230
+
231
+ ### Features
232
+
233
+ - **Multi-agent search**: Searches both Claude Code and Codex sessions simultaneously
234
+ - **Unified display**: Single table showing sessions from all agents with agent column
235
+ - **Smart resume**: Automatically uses correct CLI tool (`claude` or `codex`) based on selected session
236
+ - **Persistent directory changes**: Using the `fs` wrapper ensures you stay in the session's directory after exit
237
+ - **Optional keyword search**: Keywords are optional—omit them to show all sessions
238
+ - **Action menu** after session selection:
239
+ - Resume session (default)
240
+ - Show session file path
241
+ - Copy session file to file (*.jsonl) or directory
242
+ - **Project filtering**: Search current project only (default) or all projects with `-g`
243
+ - **Agent filtering**: Use `--agents claude codex` to search specific agents only
244
+ - **Configurable**: Optional config file at `~/.config/find-session/config.json` for customizing agents
245
+ - Interactive session selection with Rich table display
246
+ - Shows agent, project, git branch, date, line count, and preview
247
+ - Reverse chronological ordering (most recent first)
248
+ - Press Enter to cancel (no need for Ctrl+C)
249
+
250
+ Note: You can also use `find-session` directly, but directory changes won't persist after exiting sessions.
251
+
252
+ ### Configuration (Optional)
253
+
254
+ Create `~/.config/find-session/config.json` to customize agent settings:
255
+
256
+ ```json
257
+ {
258
+ "agents": [
259
+ {
260
+ "name": "claude",
261
+ "display_name": "Claude",
262
+ "home_dir": "~/.claude",
263
+ "enabled": true
264
+ },
265
+ {
266
+ "name": "codex",
267
+ "display_name": "Codex",
268
+ "home_dir": "~/.codex",
269
+ "enabled": true
270
+ }
271
+ ]
272
+ }
273
+ ```
274
+
275
+ This allows you to:
276
+ - Enable/disable specific agents
277
+ - Override default home directories
278
+ - Customize display names
279
+ - Prepare for future agent additions
280
+
178
281
  <a id="find-claude-session"></a>
179
282
  ## 🔍 find-claude-session
180
283
 
@@ -253,27 +356,48 @@ Search and resume Codex sessions by keywords. Usage is similar to `find-claude-s
253
356
  - Extracts metadata from `session_meta` entries in Codex JSONL files
254
357
  - Resumes sessions with `codex resume <session-id>`
255
358
 
359
+ ### Setup (Recommended)
360
+
361
+ Add this function to your shell config (.bashrc/.zshrc) for persistent directory changes:
362
+
363
+ ```bash
364
+ fcs-codex() {
365
+ # Check if user is asking for help
366
+ if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
367
+ find-codex-session --help
368
+ return
369
+ fi
370
+ # Run find-codex-session in shell mode and evaluate the output
371
+ eval "$(find-codex-session --shell "$@" | sed '/^$/d')"
372
+ }
373
+ ```
374
+
375
+ Or source the provided function:
376
+ ```bash
377
+ source /path/to/claude-code-tools/scripts/fcs-codex-function.sh
378
+ ```
379
+
256
380
  ### Usage
257
381
 
258
382
  ```bash
259
383
  # Search in current project only (default)
260
- find-codex-session "keyword1,keyword2"
384
+ fcs-codex "keyword1,keyword2"
261
385
 
262
386
  # Show all sessions in current project (no keyword filtering)
263
- find-codex-session
387
+ fcs-codex
264
388
 
265
389
  # Search across all projects
266
- find-codex-session "keywords" -g
267
- find-codex-session "keywords" --global
390
+ fcs-codex "keywords" -g
391
+ fcs-codex "keywords" --global
268
392
 
269
393
  # Show all sessions across all projects
270
- find-codex-session -g
394
+ fcs-codex -g
271
395
 
272
396
  # Limit number of results
273
- find-codex-session "keywords" -n 5
397
+ fcs-codex "keywords" -n 5
274
398
 
275
399
  # Custom Codex home directory
276
- find-codex-session "keywords" --codex-home /custom/path
400
+ fcs-codex "keywords" --codex-home /custom/path
277
401
  ```
278
402
 
279
403
  ### Features
@@ -293,6 +417,8 @@ find-codex-session "keywords" --codex-home /custom/path
293
417
  - Multi-line preview wrapping for better readability
294
418
  - Press Enter to cancel (no need for Ctrl+C)
295
419
 
420
+ Note: You can also use `find-codex-session` directly, but directory changes won't persist after exiting Codex.
421
+
296
422
  Looks like this --
297
423
 
298
424
  ![find-codex-session.png](demos/find-codex-session.png)
@@ -9,6 +9,7 @@ and other CLI coding agents.
9
9
  - [🚀 Quick Start](#quick-start)
10
10
  - [🎮 tmux-cli Deep Dive](#tmux-cli-deep-dive)
11
11
  - [🚀 lmsh (Experimental) — natural language to shell commands](#lmsh-experimental)
12
+ - [🔍 find-session — unified search across Claude & Codex sessions](#find-session)
12
13
  - [🔍 find-claude-session — search and resume Claude sessions](#find-claude-session)
13
14
  - [🔍 find-codex-session — search and resume Codex sessions](#find-codex-session)
14
15
  - [🔐 vault — encrypted .env backup & sync](#vault)
@@ -67,6 +68,7 @@ uv tool install git+https://github.com/pchalasani/claude-code-tools
67
68
 
68
69
  This gives you:
69
70
  - `tmux-cli` - The interactive CLI controller we just covered
71
+ - `find-session` - Unified search across Claude Code and Codex sessions
70
72
  - `find-claude-session` - Search and resume Claude Code sessions by keywords
71
73
  - `find-codex-session` - Search and resume Codex sessions by keywords
72
74
  - `vault` - Encrypted backup for your .env files
@@ -161,6 +163,107 @@ cp target/release/lmsh ~/.cargo/bin/
161
163
 
162
164
  See [docs/lmsh.md](docs/lmsh.md) for details.
163
165
 
166
+ <a id="find-session"></a>
167
+ ## 🔍 find-session
168
+
169
+ **Unified session finder** - Search across both Claude Code and Codex sessions simultaneously.
170
+
171
+ ### Setup (Recommended)
172
+
173
+ Add this function to your shell config (.bashrc/.zshrc) for persistent directory changes:
174
+
175
+ ```bash
176
+ fs() {
177
+ # Check if user is asking for help
178
+ if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
179
+ find-session --help
180
+ return
181
+ fi
182
+ # Run find-session in shell mode and evaluate the output
183
+ eval "$(find-session --shell "$@" | sed '/^$/d')"
184
+ }
185
+ ```
186
+
187
+ Or source the provided function:
188
+ ```bash
189
+ source /path/to/claude-code-tools/scripts/fs-function.sh
190
+ ```
191
+
192
+ **Why use the shell wrapper?** When you resume a session from a different directory, the wrapper ensures the directory change persists after the session exits. Without it, you'll be back in your original directory after exiting the session (though the session itself runs in the correct directory).
193
+
194
+ ### Usage
195
+
196
+ ```bash
197
+ # Search all agents in current project
198
+ fs "keyword1,keyword2"
199
+
200
+ # Show all sessions across all agents in current project
201
+ fs
202
+
203
+ # Search across all projects (Claude + Codex)
204
+ fs "keywords" -g
205
+
206
+ # Show all sessions across all projects
207
+ fs -g
208
+
209
+ # Search only specific agent(s)
210
+ fs "bug,fix" --agents claude
211
+ fs "error" --agents codex
212
+
213
+ # Limit number of results
214
+ fs "keywords" -n 15
215
+ ```
216
+
217
+ ### Features
218
+
219
+ - **Multi-agent search**: Searches both Claude Code and Codex sessions simultaneously
220
+ - **Unified display**: Single table showing sessions from all agents with agent column
221
+ - **Smart resume**: Automatically uses correct CLI tool (`claude` or `codex`) based on selected session
222
+ - **Persistent directory changes**: Using the `fs` wrapper ensures you stay in the session's directory after exit
223
+ - **Optional keyword search**: Keywords are optional—omit them to show all sessions
224
+ - **Action menu** after session selection:
225
+ - Resume session (default)
226
+ - Show session file path
227
+ - Copy session file to file (*.jsonl) or directory
228
+ - **Project filtering**: Search current project only (default) or all projects with `-g`
229
+ - **Agent filtering**: Use `--agents claude codex` to search specific agents only
230
+ - **Configurable**: Optional config file at `~/.config/find-session/config.json` for customizing agents
231
+ - Interactive session selection with Rich table display
232
+ - Shows agent, project, git branch, date, line count, and preview
233
+ - Reverse chronological ordering (most recent first)
234
+ - Press Enter to cancel (no need for Ctrl+C)
235
+
236
+ Note: You can also use `find-session` directly, but directory changes won't persist after exiting sessions.
237
+
238
+ ### Configuration (Optional)
239
+
240
+ Create `~/.config/find-session/config.json` to customize agent settings:
241
+
242
+ ```json
243
+ {
244
+ "agents": [
245
+ {
246
+ "name": "claude",
247
+ "display_name": "Claude",
248
+ "home_dir": "~/.claude",
249
+ "enabled": true
250
+ },
251
+ {
252
+ "name": "codex",
253
+ "display_name": "Codex",
254
+ "home_dir": "~/.codex",
255
+ "enabled": true
256
+ }
257
+ ]
258
+ }
259
+ ```
260
+
261
+ This allows you to:
262
+ - Enable/disable specific agents
263
+ - Override default home directories
264
+ - Customize display names
265
+ - Prepare for future agent additions
266
+
164
267
  <a id="find-claude-session"></a>
165
268
  ## 🔍 find-claude-session
166
269
 
@@ -239,27 +342,48 @@ Search and resume Codex sessions by keywords. Usage is similar to `find-claude-s
239
342
  - Extracts metadata from `session_meta` entries in Codex JSONL files
240
343
  - Resumes sessions with `codex resume <session-id>`
241
344
 
345
+ ### Setup (Recommended)
346
+
347
+ Add this function to your shell config (.bashrc/.zshrc) for persistent directory changes:
348
+
349
+ ```bash
350
+ fcs-codex() {
351
+ # Check if user is asking for help
352
+ if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
353
+ find-codex-session --help
354
+ return
355
+ fi
356
+ # Run find-codex-session in shell mode and evaluate the output
357
+ eval "$(find-codex-session --shell "$@" | sed '/^$/d')"
358
+ }
359
+ ```
360
+
361
+ Or source the provided function:
362
+ ```bash
363
+ source /path/to/claude-code-tools/scripts/fcs-codex-function.sh
364
+ ```
365
+
242
366
  ### Usage
243
367
 
244
368
  ```bash
245
369
  # Search in current project only (default)
246
- find-codex-session "keyword1,keyword2"
370
+ fcs-codex "keyword1,keyword2"
247
371
 
248
372
  # Show all sessions in current project (no keyword filtering)
249
- find-codex-session
373
+ fcs-codex
250
374
 
251
375
  # Search across all projects
252
- find-codex-session "keywords" -g
253
- find-codex-session "keywords" --global
376
+ fcs-codex "keywords" -g
377
+ fcs-codex "keywords" --global
254
378
 
255
379
  # Show all sessions across all projects
256
- find-codex-session -g
380
+ fcs-codex -g
257
381
 
258
382
  # Limit number of results
259
- find-codex-session "keywords" -n 5
383
+ fcs-codex "keywords" -n 5
260
384
 
261
385
  # Custom Codex home directory
262
- find-codex-session "keywords" --codex-home /custom/path
386
+ fcs-codex "keywords" --codex-home /custom/path
263
387
  ```
264
388
 
265
389
  ### Features
@@ -279,6 +403,8 @@ find-codex-session "keywords" --codex-home /custom/path
279
403
  - Multi-line preview wrapping for better readability
280
404
  - Press Enter to cancel (no need for Ctrl+C)
281
405
 
406
+ Note: You can also use `find-codex-session` directly, but directory changes won't persist after exiting Codex.
407
+
282
408
  Looks like this --
283
409
 
284
410
  ![find-codex-session.png](demos/find-codex-session.png)
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "0.2.5"
3
+ __version__ = "0.2.7"
@@ -0,0 +1,447 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified session finder - search across multiple coding agents (Claude Code, Codex, etc.)
4
+
5
+ Usage:
6
+ find-session [keywords] [OPTIONS]
7
+ fs [keywords] [OPTIONS] # via shell wrapper
8
+
9
+ Examples:
10
+ find-session "langroid,MCP" # Search all agents in current project
11
+ find-session -g # Show all sessions across all projects
12
+ find-session "bug" --agents claude # Search only Claude sessions
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import List, Optional
22
+
23
+ # Import search functions from existing tools
24
+ from claude_code_tools.find_claude_session import (
25
+ find_sessions as find_claude_sessions,
26
+ resume_session as resume_claude_session,
27
+ get_session_file_path as get_claude_session_file_path,
28
+ copy_session_file as copy_claude_session_file,
29
+ )
30
+ from claude_code_tools.find_codex_session import (
31
+ find_sessions as find_codex_sessions,
32
+ resume_session as resume_codex_session,
33
+ get_codex_home,
34
+ copy_session_file as copy_codex_session_file,
35
+ )
36
+
37
+ try:
38
+ from rich.console import Console
39
+ from rich.table import Table
40
+ from rich import box
41
+
42
+ RICH_AVAILABLE = True
43
+ except ImportError:
44
+ RICH_AVAILABLE = False
45
+
46
+
47
+ @dataclass
48
+ class AgentConfig:
49
+ """Configuration for a coding agent."""
50
+
51
+ name: str # Internal name (e.g., "claude", "codex")
52
+ display_name: str # Display name (e.g., "Claude", "Codex")
53
+ home_dir: Optional[str] # Custom home directory (None = default)
54
+ enabled: bool = True
55
+
56
+
57
+ def get_default_agents() -> List[AgentConfig]:
58
+ """Get default agent configurations."""
59
+ return [
60
+ AgentConfig(name="claude", display_name="Claude", home_dir=None),
61
+ AgentConfig(name="codex", display_name="Codex", home_dir=None),
62
+ ]
63
+
64
+
65
+ def load_config() -> List[AgentConfig]:
66
+ """Load agent configuration from config file or use defaults."""
67
+ config_path = Path.home() / ".config" / "find-session" / "config.json"
68
+
69
+ if config_path.exists():
70
+ try:
71
+ with open(config_path, "r") as f:
72
+ data = json.load(f)
73
+ agents = []
74
+ for agent_data in data.get("agents", []):
75
+ agents.append(
76
+ AgentConfig(
77
+ name=agent_data["name"],
78
+ display_name=agent_data.get(
79
+ "display_name", agent_data["name"].title()
80
+ ),
81
+ home_dir=agent_data.get("home_dir"),
82
+ enabled=agent_data.get("enabled", True),
83
+ )
84
+ )
85
+ return agents
86
+ except (json.JSONDecodeError, KeyError, IOError):
87
+ pass
88
+
89
+ # Return defaults if config doesn't exist or is invalid
90
+ return get_default_agents()
91
+
92
+
93
+ def search_all_agents(
94
+ keywords: List[str],
95
+ global_search: bool = False,
96
+ num_matches: int = 10,
97
+ agents: Optional[List[str]] = None,
98
+ claude_home: Optional[str] = None,
99
+ codex_home: Optional[str] = None,
100
+ ) -> List[dict]:
101
+ """
102
+ Search sessions across all enabled agents.
103
+
104
+ Returns list of dicts with agent metadata added.
105
+ """
106
+ agent_configs = load_config()
107
+
108
+ # Filter by requested agents if specified
109
+ if agents:
110
+ agent_configs = [a for a in agent_configs if a.name in agents]
111
+
112
+ # Filter by enabled agents
113
+ agent_configs = [a for a in agent_configs if a.enabled]
114
+
115
+ all_sessions = []
116
+
117
+ for agent_config in agent_configs:
118
+ if agent_config.name == "claude":
119
+ # Search Claude sessions
120
+ home = claude_home or agent_config.home_dir
121
+ sessions = find_claude_sessions(
122
+ keywords, global_search=global_search, claude_home=home
123
+ )
124
+
125
+ # Add agent metadata to each session
126
+ for session in sessions:
127
+ session_dict = {
128
+ "agent": "claude",
129
+ "agent_display": agent_config.display_name,
130
+ "session_id": session[0],
131
+ "mod_time": session[1],
132
+ "create_time": session[2],
133
+ "lines": session[3],
134
+ "project": session[4],
135
+ "preview": session[5],
136
+ "cwd": session[6],
137
+ "branch": session[7] if len(session) > 7 else "",
138
+ "claude_home": home,
139
+ }
140
+ all_sessions.append(session_dict)
141
+
142
+ elif agent_config.name == "codex":
143
+ # Search Codex sessions
144
+ home = codex_home or agent_config.home_dir
145
+ codex_home_path = get_codex_home(home)
146
+
147
+ if codex_home_path.exists():
148
+ sessions = find_codex_sessions(
149
+ codex_home_path,
150
+ keywords,
151
+ num_matches=num_matches * 2, # Get more for merging
152
+ global_search=global_search,
153
+ )
154
+
155
+ # Add agent metadata to each session
156
+ for session in sessions:
157
+ session_dict = {
158
+ "agent": "codex",
159
+ "agent_display": agent_config.display_name,
160
+ "session_id": session["session_id"],
161
+ "mod_time": session["mod_time"],
162
+ "create_time": session.get("mod_time"), # Codex doesn't separate these
163
+ "lines": session["lines"],
164
+ "project": session["project"],
165
+ "preview": session["preview"],
166
+ "cwd": session["cwd"],
167
+ "branch": session.get("branch", ""),
168
+ "file_path": session.get("file_path", ""),
169
+ }
170
+ all_sessions.append(session_dict)
171
+
172
+ # Sort by modification time (newest first) and limit
173
+ all_sessions.sort(key=lambda x: x["mod_time"], reverse=True)
174
+ return all_sessions[:num_matches]
175
+
176
+
177
+ def display_interactive_ui(
178
+ sessions: List[dict], keywords: List[str], stderr_mode: bool = False, num_matches: int = 10
179
+ ) -> Optional[dict]:
180
+ """Display unified session selection UI."""
181
+ if not RICH_AVAILABLE:
182
+ return None
183
+
184
+ # Use stderr console if in stderr mode
185
+ ui_console = Console(file=sys.stderr) if stderr_mode else Console()
186
+
187
+ # Limit to specified number of sessions
188
+ display_sessions = sessions[:num_matches]
189
+
190
+ if not display_sessions:
191
+ ui_console.print("[red]No sessions found[/red]")
192
+ return None
193
+
194
+ # Create table
195
+ title = (
196
+ f"Sessions matching: {', '.join(keywords)}" if keywords else "All sessions"
197
+ )
198
+ table = Table(
199
+ title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan"
200
+ )
201
+
202
+ table.add_column("#", style="bold yellow", width=3)
203
+ table.add_column("Agent", style="magenta", width=6)
204
+ table.add_column("Session ID", style="dim", width=10)
205
+ table.add_column("Project", style="green")
206
+ table.add_column("Branch", style="cyan")
207
+ table.add_column("Date", style="blue")
208
+ table.add_column("Lines", style="cyan", justify="right", width=6)
209
+ table.add_column("Last User Message", style="white", max_width=50, overflow="fold")
210
+
211
+ for idx, session in enumerate(display_sessions, 1):
212
+ # Format date from mod_time
213
+ from datetime import datetime
214
+
215
+ mod_time = session["mod_time"]
216
+ date_str = datetime.fromtimestamp(mod_time).strftime("%m/%d %H:%M")
217
+
218
+ branch_display = session.get("branch", "") or "N/A"
219
+
220
+ table.add_row(
221
+ str(idx),
222
+ session["agent_display"],
223
+ session["session_id"][:8] + "...",
224
+ session["project"],
225
+ branch_display,
226
+ date_str,
227
+ str(session["lines"]),
228
+ session["preview"],
229
+ )
230
+
231
+ ui_console.print(table)
232
+ ui_console.print("\n[bold]Select a session:[/bold]")
233
+ ui_console.print(f" • Enter number (1-{len(display_sessions)}) to select")
234
+ ui_console.print(" • Press Enter to cancel\n")
235
+
236
+ while True:
237
+ try:
238
+ from rich.prompt import Prompt
239
+
240
+ choice = Prompt.ask(
241
+ "Your choice", default="", show_default=False, console=ui_console
242
+ )
243
+
244
+ # Handle empty input - cancel
245
+ if not choice or not choice.strip():
246
+ ui_console.print("[yellow]Cancelled[/yellow]")
247
+ return None
248
+
249
+ idx = int(choice) - 1
250
+ if 0 <= idx < len(display_sessions):
251
+ return display_sessions[idx]
252
+ else:
253
+ ui_console.print("[red]Invalid choice. Please try again.[/red]")
254
+
255
+ except KeyboardInterrupt:
256
+ ui_console.print("\n[yellow]Cancelled[/yellow]")
257
+ return None
258
+ except EOFError:
259
+ ui_console.print("\n[yellow]Cancelled (EOF)[/yellow]")
260
+ return None
261
+ except ValueError:
262
+ ui_console.print("[red]Invalid choice. Please try again.[/red]")
263
+
264
+
265
+ def show_action_menu(session: dict, stderr_mode: bool = False) -> Optional[str]:
266
+ """Show action menu for selected session."""
267
+ output = sys.stderr if stderr_mode else sys.stdout
268
+
269
+ print(f"\n=== Session: {session['session_id'][:16]}... ===", file=output)
270
+ print(f"Agent: {session['agent_display']}", file=output)
271
+ print(f"Project: {session['project']}", file=output)
272
+ if session.get("branch"):
273
+ print(f"Branch: {session['branch']}", file=output)
274
+ print(f"\nWhat would you like to do?", file=output)
275
+ print("1. Resume session (default)", file=output)
276
+ print("2. Show session file path", file=output)
277
+ print("3. Copy session file to file (*.jsonl) or directory", file=output)
278
+ print(file=output)
279
+
280
+ try:
281
+ if stderr_mode:
282
+ # In stderr mode, prompt to stderr so it's visible
283
+ sys.stderr.write("Enter choice [1-3] (or Enter for 1): ")
284
+ sys.stderr.flush()
285
+ choice = sys.stdin.readline().strip()
286
+ else:
287
+ choice = input("Enter choice [1-3] (or Enter for 1): ").strip()
288
+
289
+ if not choice or choice == "1":
290
+ return "resume"
291
+ elif choice == "2":
292
+ return "path"
293
+ elif choice == "3":
294
+ return "copy"
295
+ else:
296
+ print("Invalid choice.", file=output)
297
+ return None
298
+ except KeyboardInterrupt:
299
+ print("\nCancelled.", file=output)
300
+ return None
301
+
302
+
303
+ def handle_action(session: dict, action: str, shell_mode: bool = False) -> None:
304
+ """Handle the selected action based on agent type."""
305
+ agent = session["agent"]
306
+
307
+ if action == "resume":
308
+ if agent == "claude":
309
+ resume_claude_session(
310
+ session["session_id"],
311
+ session["cwd"],
312
+ shell_mode=shell_mode,
313
+ claude_home=session.get("claude_home"),
314
+ )
315
+ elif agent == "codex":
316
+ resume_codex_session(
317
+ session["session_id"], session["cwd"], shell_mode=shell_mode
318
+ )
319
+
320
+ elif action == "path":
321
+ if agent == "claude":
322
+ file_path = get_claude_session_file_path(
323
+ session["session_id"],
324
+ session["cwd"],
325
+ claude_home=session.get("claude_home"),
326
+ )
327
+ print(f"\nSession file path:")
328
+ print(file_path)
329
+ elif agent == "codex":
330
+ print(f"\nSession file path:")
331
+ print(session.get("file_path", "Unknown"))
332
+
333
+ elif action == "copy":
334
+ if agent == "claude":
335
+ file_path = get_claude_session_file_path(
336
+ session["session_id"],
337
+ session["cwd"],
338
+ claude_home=session.get("claude_home"),
339
+ )
340
+ copy_claude_session_file(file_path)
341
+ elif agent == "codex":
342
+ copy_codex_session_file(session.get("file_path", ""))
343
+
344
+
345
+ def main():
346
+ parser = argparse.ArgumentParser(
347
+ description="Unified session finder - search across multiple coding agents",
348
+ formatter_class=argparse.RawDescriptionHelpFormatter,
349
+ epilog="""
350
+ Examples:
351
+ find-session "langroid,MCP" # Search all agents in current project
352
+ find-session -g # Show all sessions across all projects
353
+ find-session "bug" --agents claude # Search only Claude sessions
354
+ find-session "error" --agents codex # Search only Codex sessions
355
+ """,
356
+ )
357
+ parser.add_argument(
358
+ "keywords",
359
+ nargs="?",
360
+ default="",
361
+ help="Comma-separated keywords to search (AND logic). If omitted, shows all sessions.",
362
+ )
363
+ parser.add_argument(
364
+ "-g",
365
+ "--global",
366
+ dest="global_search",
367
+ action="store_true",
368
+ help="Search across all projects, not just the current one",
369
+ )
370
+ parser.add_argument(
371
+ "-n",
372
+ "--num-matches",
373
+ type=int,
374
+ default=10,
375
+ help="Number of matching sessions to display (default: 10)",
376
+ )
377
+ parser.add_argument(
378
+ "--agents",
379
+ nargs="+",
380
+ choices=["claude", "codex"],
381
+ help="Limit search to specific agents (default: all)",
382
+ )
383
+ parser.add_argument(
384
+ "--shell",
385
+ action="store_true",
386
+ help="Output shell commands for evaluation (for use with shell function)",
387
+ )
388
+ parser.add_argument(
389
+ "--claude-home", type=str, help="Path to Claude home directory (default: ~/.claude)"
390
+ )
391
+ parser.add_argument(
392
+ "--codex-home", type=str, help="Path to Codex home directory (default: ~/.codex)"
393
+ )
394
+
395
+ args = parser.parse_args()
396
+
397
+ # Parse keywords
398
+ keywords = (
399
+ [k.strip() for k in args.keywords.split(",") if k.strip()]
400
+ if args.keywords
401
+ else []
402
+ )
403
+
404
+ # Search all agents
405
+ matching_sessions = search_all_agents(
406
+ keywords,
407
+ global_search=args.global_search,
408
+ num_matches=args.num_matches,
409
+ agents=args.agents,
410
+ claude_home=args.claude_home,
411
+ codex_home=args.codex_home,
412
+ )
413
+
414
+ if not matching_sessions:
415
+ scope = "all projects" if args.global_search else "current project"
416
+ keyword_msg = (
417
+ f" containing all keywords: {', '.join(keywords)}" if keywords else ""
418
+ )
419
+ if RICH_AVAILABLE:
420
+ console = Console()
421
+ console.print(f"[yellow]No sessions found{keyword_msg} in {scope}[/yellow]")
422
+ else:
423
+ print(f"No sessions found{keyword_msg} in {scope}", file=sys.stderr)
424
+ sys.exit(0)
425
+
426
+ # Display interactive UI
427
+ if RICH_AVAILABLE:
428
+ selected_session = display_interactive_ui(
429
+ matching_sessions, keywords, stderr_mode=args.shell, num_matches=args.num_matches
430
+ )
431
+ if selected_session:
432
+ # Show action menu
433
+ action = show_action_menu(selected_session, stderr_mode=args.shell)
434
+ if action:
435
+ handle_action(selected_session, action, shell_mode=args.shell)
436
+ else:
437
+ # Fallback without rich
438
+ print("\nMatching sessions:")
439
+ for idx, session in enumerate(matching_sessions[: args.num_matches], 1):
440
+ print(
441
+ f"{idx}. [{session['agent_display']}] {session['session_id'][:16]}... | "
442
+ f"{session['project']} | {session.get('branch', 'N/A')}"
443
+ )
444
+
445
+
446
+ if __name__ == "__main__":
447
+ main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "0.2.5"
3
+ version = "0.2.7"
4
4
  description = "Collection of tools for working with Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -17,6 +17,7 @@ dev = ["commitizen>=3.0.0"]
17
17
  [project.scripts]
18
18
  find-claude-session = "claude_code_tools.find_claude_session:main"
19
19
  find-codex-session = "claude_code_tools.find_codex_session:main"
20
+ find-session = "claude_code_tools.find_session:main"
20
21
  vault = "claude_code_tools.dotenv_vault:main"
21
22
  tmux-cli = "claude_code_tools.tmux_cli_controller:main"
22
23
  env-safe = "claude_code_tools.env_safe:main"
@@ -43,7 +44,7 @@ exclude = [
43
44
 
44
45
  [tool.commitizen]
45
46
  name = "cz_conventional_commits"
46
- version = "0.2.5"
47
+ version = "0.2.7"
47
48
  tag_format = "v$version"
48
49
  version_files = [
49
50
  "pyproject.toml:version",