claude-code-tools 0.2.5__py3-none-any.whl → 0.2.7__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_session.py +447 -0
- {claude_code_tools-0.2.5.dist-info → claude_code_tools-0.2.7.dist-info}/METADATA +134 -8
- {claude_code_tools-0.2.5.dist-info → claude_code_tools-0.2.7.dist-info}/RECORD +7 -6
- {claude_code_tools-0.2.5.dist-info → claude_code_tools-0.2.7.dist-info}/entry_points.txt +1 -0
- {claude_code_tools-0.2.5.dist-info → claude_code_tools-0.2.7.dist-info}/WHEEL +0 -0
- {claude_code_tools-0.2.5.dist-info → claude_code_tools-0.2.7.dist-info}/licenses/LICENSE +0 -0
claude_code_tools/__init__.py
CHANGED
|
@@ -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
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-tools
|
|
3
|
-
Version: 0.2.
|
|
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
|
-
|
|
384
|
+
fcs-codex "keyword1,keyword2"
|
|
261
385
|
|
|
262
386
|
# Show all sessions in current project (no keyword filtering)
|
|
263
|
-
|
|
387
|
+
fcs-codex
|
|
264
388
|
|
|
265
389
|
# Search across all projects
|
|
266
|
-
|
|
267
|
-
|
|
390
|
+
fcs-codex "keywords" -g
|
|
391
|
+
fcs-codex "keywords" --global
|
|
268
392
|
|
|
269
393
|
# Show all sessions across all projects
|
|
270
|
-
|
|
394
|
+
fcs-codex -g
|
|
271
395
|
|
|
272
396
|
# Limit number of results
|
|
273
|
-
|
|
397
|
+
fcs-codex "keywords" -n 5
|
|
274
398
|
|
|
275
399
|
# Custom Codex home directory
|
|
276
|
-
|
|
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
|

|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
claude_code_tools/__init__.py,sha256=
|
|
1
|
+
claude_code_tools/__init__.py,sha256=GaBBOYwJZjFc1syMk9qvI2rExHTcUBiiS132gH0E4V0,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
5
|
claude_code_tools/find_claude_session.py,sha256=Mc75CnAdp8mgO9y2s1_oG1rH9svZAUAdqOg_7ezkRg4,31482
|
|
6
6
|
claude_code_tools/find_codex_session.py,sha256=oRSHux_0kwSxKZe5m_Nzjx2aV0b--cVzPJ-w6RXLh0c,20791
|
|
7
|
+
claude_code_tools/find_session.py,sha256=IjvJSaeRxmGvdL6_GUVUqxQnTK7zfNECSP___QHX1V4,15443
|
|
7
8
|
claude_code_tools/tmux_cli_controller.py,sha256=5QDrDlv3oabIghRHuP8jMhUfxPeyYZxizNWW5sVuJIg,34607
|
|
8
9
|
claude_code_tools/tmux_remote_controller.py,sha256=eY1ouLtUzJ40Ik4nqUBvc3Gl1Rx0_L4TFW4j708lgvI,9942
|
|
9
10
|
docs/cc-codex-instructions.md,sha256=5E9QotkrcVYIE5VrvJGi-sg7tdyITDrsbhaqBKr4MUk,1109
|
|
@@ -15,8 +16,8 @@ docs/lmsh.md,sha256=o2TNP1Yfl3zW23GzEqK8Bx6z1hQof_lplaeEucuHNRU,1335
|
|
|
15
16
|
docs/reddit-post.md,sha256=ZA7kPoJNi06t6F9JQMBiIOv039ADC9lM8YXFt8UA_Jg,2345
|
|
16
17
|
docs/tmux-cli-instructions.md,sha256=hKGOdaPdBlb5XFzHfi0Mm7CVlysBuJUAfop3GHreyuw,5008
|
|
17
18
|
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.
|
|
19
|
+
claude_code_tools-0.2.7.dist-info/METADATA,sha256=JXVeXw2xHveoenlfljQO1iBbdooO-22c9ruYxiwA54M,21823
|
|
20
|
+
claude_code_tools-0.2.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
claude_code_tools-0.2.7.dist-info/entry_points.txt,sha256=4H8xD_1zxiannjRRB-vMyCpPOyaRG8d1jqsSJota4F4,338
|
|
22
|
+
claude_code_tools-0.2.7.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
|
|
23
|
+
claude_code_tools-0.2.7.dist-info/RECORD,,
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
env-safe = claude_code_tools.env_safe:main
|
|
3
3
|
find-claude-session = claude_code_tools.find_claude_session:main
|
|
4
4
|
find-codex-session = claude_code_tools.find_codex_session:main
|
|
5
|
+
find-session = claude_code_tools.find_session:main
|
|
5
6
|
tmux-cli = claude_code_tools.tmux_cli_controller:main
|
|
6
7
|
vault = claude_code_tools.dotenv_vault:main
|
|
File without changes
|
|
File without changes
|