claude-code-tools 0.2.4__tar.gz → 0.2.6__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.
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/PKG-INFO +98 -2
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/README.md +97 -1
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/find_claude_session.py +41 -21
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/find_codex_session.py +43 -8
- claude_code_tools-0.2.6/claude_code_tools/find_session.py +438 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/pyproject.toml +4 -2
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/.gitignore +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/LICENSE +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/codex_bridge_mcp.py +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/tmux_cli_controller.py +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/cc-codex-instructions.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/claude-code-chutes.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/find-claude-session.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/lmsh.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/reddit-post.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/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.
|
|
3
|
+
Version: 0.2.6
|
|
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,86 @@ 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
|
+
> **⚠️ Note about the `fs` command**: For convenience, this tool is available as both `find-session` and `fs`. The short `fs` alias may conflict with existing commands or aliases in your shell. If you have a conflict, you can:
|
|
186
|
+
> - Use the full `find-session` command instead
|
|
187
|
+
> - Create your own alias: `alias mysearch='find-session'`
|
|
188
|
+
> - Uninstall and reinstall without the `fs` entry point (edit `pyproject.toml`)
|
|
189
|
+
|
|
190
|
+
### Usage
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Search all agents in current project
|
|
194
|
+
fs "keyword1,keyword2"
|
|
195
|
+
|
|
196
|
+
# Show all sessions across all agents in current project
|
|
197
|
+
fs
|
|
198
|
+
|
|
199
|
+
# Search across all projects (Claude + Codex)
|
|
200
|
+
fs "keywords" -g
|
|
201
|
+
|
|
202
|
+
# Show all sessions across all projects
|
|
203
|
+
fs -g
|
|
204
|
+
|
|
205
|
+
# Search only specific agent(s)
|
|
206
|
+
fs "bug,fix" --agents claude
|
|
207
|
+
fs "error" --agents codex
|
|
208
|
+
|
|
209
|
+
# Limit number of results
|
|
210
|
+
fs "keywords" -n 15
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Features
|
|
214
|
+
|
|
215
|
+
- **Multi-agent search**: Searches both Claude Code and Codex sessions simultaneously
|
|
216
|
+
- **Unified display**: Single table showing sessions from all agents with agent column
|
|
217
|
+
- **Smart resume**: Automatically uses correct CLI tool (`claude` or `codex`) based on selected session
|
|
218
|
+
- **Optional keyword search**: Keywords are optional—omit them to show all sessions
|
|
219
|
+
- **Action menu** after session selection:
|
|
220
|
+
- Resume session (default)
|
|
221
|
+
- Show session file path
|
|
222
|
+
- Copy session file to file (*.jsonl) or directory
|
|
223
|
+
- **Project filtering**: Search current project only (default) or all projects with `-g`
|
|
224
|
+
- **Agent filtering**: Use `--agents claude codex` to search specific agents only
|
|
225
|
+
- **Configurable**: Optional config file at `~/.config/find-session/config.json` for customizing agents
|
|
226
|
+
- Interactive session selection with Rich table display
|
|
227
|
+
- Shows agent, project, git branch, date, line count, and preview
|
|
228
|
+
- Reverse chronological ordering (most recent first)
|
|
229
|
+
- Press Enter to cancel (no need for Ctrl+C)
|
|
230
|
+
|
|
231
|
+
### Configuration (Optional)
|
|
232
|
+
|
|
233
|
+
Create `~/.config/find-session/config.json` to customize agent settings:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"agents": [
|
|
238
|
+
{
|
|
239
|
+
"name": "claude",
|
|
240
|
+
"display_name": "Claude",
|
|
241
|
+
"home_dir": "~/.claude",
|
|
242
|
+
"enabled": true
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"name": "codex",
|
|
246
|
+
"display_name": "Codex",
|
|
247
|
+
"home_dir": "~/.codex",
|
|
248
|
+
"enabled": true
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
This allows you to:
|
|
255
|
+
- Enable/disable specific agents
|
|
256
|
+
- Override default home directories
|
|
257
|
+
- Customize display names
|
|
258
|
+
- Prepare for future agent additions
|
|
259
|
+
|
|
178
260
|
<a id="find-claude-session"></a>
|
|
179
261
|
## 🔍 find-claude-session
|
|
180
262
|
|
|
@@ -208,13 +290,20 @@ source /path/to/claude-code-tools/scripts/fcs-function.sh
|
|
|
208
290
|
# Search in current project
|
|
209
291
|
fcs "keyword1,keyword2,keyword3"
|
|
210
292
|
|
|
211
|
-
#
|
|
293
|
+
# Show all sessions in current project (no keyword filtering)
|
|
294
|
+
fcs
|
|
295
|
+
|
|
296
|
+
# Search across all Claude projects
|
|
212
297
|
fcs "keywords" --global
|
|
213
298
|
fcs "keywords" -g
|
|
299
|
+
|
|
300
|
+
# Show all sessions across all projects
|
|
301
|
+
fcs -g
|
|
214
302
|
```
|
|
215
303
|
|
|
216
304
|
### Features
|
|
217
305
|
|
|
306
|
+
- **Optional keyword search**: Keywords are optional—omit them to show all sessions
|
|
218
307
|
- **Action menu** after session selection:
|
|
219
308
|
- Resume session (default)
|
|
220
309
|
- Show session file path
|
|
@@ -252,10 +341,16 @@ Search and resume Codex sessions by keywords. Usage is similar to `find-claude-s
|
|
|
252
341
|
# Search in current project only (default)
|
|
253
342
|
find-codex-session "keyword1,keyword2"
|
|
254
343
|
|
|
344
|
+
# Show all sessions in current project (no keyword filtering)
|
|
345
|
+
find-codex-session
|
|
346
|
+
|
|
255
347
|
# Search across all projects
|
|
256
348
|
find-codex-session "keywords" -g
|
|
257
349
|
find-codex-session "keywords" --global
|
|
258
350
|
|
|
351
|
+
# Show all sessions across all projects
|
|
352
|
+
find-codex-session -g
|
|
353
|
+
|
|
259
354
|
# Limit number of results
|
|
260
355
|
find-codex-session "keywords" -n 5
|
|
261
356
|
|
|
@@ -265,6 +360,7 @@ find-codex-session "keywords" --codex-home /custom/path
|
|
|
265
360
|
|
|
266
361
|
### Features
|
|
267
362
|
|
|
363
|
+
- **Optional keyword search**: Keywords are optional—omit them to show all sessions
|
|
268
364
|
- **Action menu** after session selection:
|
|
269
365
|
- Resume session (default)
|
|
270
366
|
- Show session file path
|
|
@@ -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,86 @@ 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
|
+
> **⚠️ Note about the `fs` command**: For convenience, this tool is available as both `find-session` and `fs`. The short `fs` alias may conflict with existing commands or aliases in your shell. If you have a conflict, you can:
|
|
172
|
+
> - Use the full `find-session` command instead
|
|
173
|
+
> - Create your own alias: `alias mysearch='find-session'`
|
|
174
|
+
> - Uninstall and reinstall without the `fs` entry point (edit `pyproject.toml`)
|
|
175
|
+
|
|
176
|
+
### Usage
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Search all agents in current project
|
|
180
|
+
fs "keyword1,keyword2"
|
|
181
|
+
|
|
182
|
+
# Show all sessions across all agents in current project
|
|
183
|
+
fs
|
|
184
|
+
|
|
185
|
+
# Search across all projects (Claude + Codex)
|
|
186
|
+
fs "keywords" -g
|
|
187
|
+
|
|
188
|
+
# Show all sessions across all projects
|
|
189
|
+
fs -g
|
|
190
|
+
|
|
191
|
+
# Search only specific agent(s)
|
|
192
|
+
fs "bug,fix" --agents claude
|
|
193
|
+
fs "error" --agents codex
|
|
194
|
+
|
|
195
|
+
# Limit number of results
|
|
196
|
+
fs "keywords" -n 15
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Features
|
|
200
|
+
|
|
201
|
+
- **Multi-agent search**: Searches both Claude Code and Codex sessions simultaneously
|
|
202
|
+
- **Unified display**: Single table showing sessions from all agents with agent column
|
|
203
|
+
- **Smart resume**: Automatically uses correct CLI tool (`claude` or `codex`) based on selected session
|
|
204
|
+
- **Optional keyword search**: Keywords are optional—omit them to show all sessions
|
|
205
|
+
- **Action menu** after session selection:
|
|
206
|
+
- Resume session (default)
|
|
207
|
+
- Show session file path
|
|
208
|
+
- Copy session file to file (*.jsonl) or directory
|
|
209
|
+
- **Project filtering**: Search current project only (default) or all projects with `-g`
|
|
210
|
+
- **Agent filtering**: Use `--agents claude codex` to search specific agents only
|
|
211
|
+
- **Configurable**: Optional config file at `~/.config/find-session/config.json` for customizing agents
|
|
212
|
+
- Interactive session selection with Rich table display
|
|
213
|
+
- Shows agent, project, git branch, date, line count, and preview
|
|
214
|
+
- Reverse chronological ordering (most recent first)
|
|
215
|
+
- Press Enter to cancel (no need for Ctrl+C)
|
|
216
|
+
|
|
217
|
+
### Configuration (Optional)
|
|
218
|
+
|
|
219
|
+
Create `~/.config/find-session/config.json` to customize agent settings:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"agents": [
|
|
224
|
+
{
|
|
225
|
+
"name": "claude",
|
|
226
|
+
"display_name": "Claude",
|
|
227
|
+
"home_dir": "~/.claude",
|
|
228
|
+
"enabled": true
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "codex",
|
|
232
|
+
"display_name": "Codex",
|
|
233
|
+
"home_dir": "~/.codex",
|
|
234
|
+
"enabled": true
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
This allows you to:
|
|
241
|
+
- Enable/disable specific agents
|
|
242
|
+
- Override default home directories
|
|
243
|
+
- Customize display names
|
|
244
|
+
- Prepare for future agent additions
|
|
245
|
+
|
|
164
246
|
<a id="find-claude-session"></a>
|
|
165
247
|
## 🔍 find-claude-session
|
|
166
248
|
|
|
@@ -194,13 +276,20 @@ source /path/to/claude-code-tools/scripts/fcs-function.sh
|
|
|
194
276
|
# Search in current project
|
|
195
277
|
fcs "keyword1,keyword2,keyword3"
|
|
196
278
|
|
|
197
|
-
#
|
|
279
|
+
# Show all sessions in current project (no keyword filtering)
|
|
280
|
+
fcs
|
|
281
|
+
|
|
282
|
+
# Search across all Claude projects
|
|
198
283
|
fcs "keywords" --global
|
|
199
284
|
fcs "keywords" -g
|
|
285
|
+
|
|
286
|
+
# Show all sessions across all projects
|
|
287
|
+
fcs -g
|
|
200
288
|
```
|
|
201
289
|
|
|
202
290
|
### Features
|
|
203
291
|
|
|
292
|
+
- **Optional keyword search**: Keywords are optional—omit them to show all sessions
|
|
204
293
|
- **Action menu** after session selection:
|
|
205
294
|
- Resume session (default)
|
|
206
295
|
- Show session file path
|
|
@@ -238,10 +327,16 @@ Search and resume Codex sessions by keywords. Usage is similar to `find-claude-s
|
|
|
238
327
|
# Search in current project only (default)
|
|
239
328
|
find-codex-session "keyword1,keyword2"
|
|
240
329
|
|
|
330
|
+
# Show all sessions in current project (no keyword filtering)
|
|
331
|
+
find-codex-session
|
|
332
|
+
|
|
241
333
|
# Search across all projects
|
|
242
334
|
find-codex-session "keywords" -g
|
|
243
335
|
find-codex-session "keywords" --global
|
|
244
336
|
|
|
337
|
+
# Show all sessions across all projects
|
|
338
|
+
find-codex-session -g
|
|
339
|
+
|
|
245
340
|
# Limit number of results
|
|
246
341
|
find-codex-session "keywords" -n 5
|
|
247
342
|
|
|
@@ -251,6 +346,7 @@ find-codex-session "keywords" --codex-home /custom/path
|
|
|
251
346
|
|
|
252
347
|
### Features
|
|
253
348
|
|
|
349
|
+
- **Optional keyword search**: Keywords are optional—omit them to show all sessions
|
|
254
350
|
- **Action menu** after session selection:
|
|
255
351
|
- Resume session (default)
|
|
256
352
|
- Show session file path
|
{claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/find_claude_session.py
RENAMED
|
@@ -124,29 +124,49 @@ def extract_project_name(original_path: str) -> str:
|
|
|
124
124
|
def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool, int, Optional[str]]:
|
|
125
125
|
"""
|
|
126
126
|
Check if all keywords are present in the JSONL file, count lines, and extract git branch.
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
Args:
|
|
129
129
|
filepath: Path to the JSONL file
|
|
130
|
-
keywords: List of keywords to search for (case-insensitive)
|
|
131
|
-
|
|
130
|
+
keywords: List of keywords to search for (case-insensitive). Empty list matches all files.
|
|
131
|
+
|
|
132
132
|
Returns:
|
|
133
133
|
Tuple of (matches: bool, line_count: int, git_branch: Optional[str])
|
|
134
|
-
- matches: True if ALL keywords are found in the file
|
|
134
|
+
- matches: True if ALL keywords are found in the file (or True if no keywords)
|
|
135
135
|
- line_count: Total number of lines in the file
|
|
136
136
|
- git_branch: Git branch name from the first message that has it, or None
|
|
137
137
|
"""
|
|
138
|
+
# If no keywords, match all files
|
|
139
|
+
if not keywords:
|
|
140
|
+
line_count = 0
|
|
141
|
+
git_branch = None
|
|
142
|
+
try:
|
|
143
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
144
|
+
for line in f:
|
|
145
|
+
line_count += 1
|
|
146
|
+
# Extract git branch from JSON if not already found
|
|
147
|
+
if git_branch is None:
|
|
148
|
+
try:
|
|
149
|
+
data = json.loads(line.strip())
|
|
150
|
+
if 'gitBranch' in data and data['gitBranch']:
|
|
151
|
+
git_branch = data['gitBranch']
|
|
152
|
+
except (json.JSONDecodeError, KeyError):
|
|
153
|
+
pass
|
|
154
|
+
except Exception:
|
|
155
|
+
return False, 0, None
|
|
156
|
+
return True, line_count, git_branch
|
|
157
|
+
|
|
138
158
|
# Convert keywords to lowercase for case-insensitive search
|
|
139
159
|
keywords_lower = [k.lower() for k in keywords]
|
|
140
160
|
found_keywords = set()
|
|
141
161
|
line_count = 0
|
|
142
162
|
git_branch = None
|
|
143
|
-
|
|
163
|
+
|
|
144
164
|
try:
|
|
145
165
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
146
166
|
for line in f:
|
|
147
167
|
line_count += 1
|
|
148
168
|
line_lower = line.lower()
|
|
149
|
-
|
|
169
|
+
|
|
150
170
|
# Extract git branch from JSON if not already found
|
|
151
171
|
if git_branch is None:
|
|
152
172
|
try:
|
|
@@ -155,7 +175,7 @@ def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool,
|
|
|
155
175
|
git_branch = data['gitBranch']
|
|
156
176
|
except (json.JSONDecodeError, KeyError):
|
|
157
177
|
pass
|
|
158
|
-
|
|
178
|
+
|
|
159
179
|
# Check which keywords are in this line
|
|
160
180
|
for keyword in keywords_lower:
|
|
161
181
|
if keyword in line_lower:
|
|
@@ -163,7 +183,7 @@ def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool,
|
|
|
163
183
|
except Exception:
|
|
164
184
|
# Skip files that can't be read
|
|
165
185
|
return False, 0, None
|
|
166
|
-
|
|
186
|
+
|
|
167
187
|
matches = len(found_keywords) == len(keywords_lower)
|
|
168
188
|
return matches, line_count, git_branch
|
|
169
189
|
|
|
@@ -310,22 +330,23 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str,
|
|
|
310
330
|
"""Display interactive UI for session selection."""
|
|
311
331
|
if not RICH_AVAILABLE:
|
|
312
332
|
return None
|
|
313
|
-
|
|
333
|
+
|
|
314
334
|
# Use stderr console if in stderr mode
|
|
315
335
|
ui_console = Console(file=sys.stderr) if stderr_mode else console
|
|
316
336
|
if not ui_console:
|
|
317
337
|
return None
|
|
318
|
-
|
|
338
|
+
|
|
319
339
|
# Limit to specified number of sessions
|
|
320
340
|
display_sessions = sessions[:num_matches]
|
|
321
|
-
|
|
341
|
+
|
|
322
342
|
if not display_sessions:
|
|
323
343
|
ui_console.print("[red]No sessions found[/red]")
|
|
324
344
|
return None
|
|
325
|
-
|
|
345
|
+
|
|
326
346
|
# Create table
|
|
347
|
+
title = f"Sessions matching: {', '.join(keywords)}" if keywords else "All sessions"
|
|
327
348
|
table = Table(
|
|
328
|
-
title=
|
|
349
|
+
title=title,
|
|
329
350
|
box=box.ROUNDED,
|
|
330
351
|
show_header=True,
|
|
331
352
|
header_style="bold cyan"
|
|
@@ -609,7 +630,9 @@ To persist directory changes when resuming sessions:
|
|
|
609
630
|
)
|
|
610
631
|
parser.add_argument(
|
|
611
632
|
"keywords",
|
|
612
|
-
|
|
633
|
+
nargs='?',
|
|
634
|
+
default="",
|
|
635
|
+
help="Comma-separated keywords to search for (case-insensitive). If omitted, shows all sessions."
|
|
613
636
|
)
|
|
614
637
|
parser.add_argument(
|
|
615
638
|
"-g", "--global",
|
|
@@ -636,11 +659,7 @@ To persist directory changes when resuming sessions:
|
|
|
636
659
|
args = parser.parse_args()
|
|
637
660
|
|
|
638
661
|
# Parse keywords
|
|
639
|
-
keywords = [k.strip() for k in args.keywords.split(",") if k.strip()]
|
|
640
|
-
|
|
641
|
-
if not keywords:
|
|
642
|
-
print("Error: No keywords provided", file=sys.stderr)
|
|
643
|
-
sys.exit(1)
|
|
662
|
+
keywords = [k.strip() for k in args.keywords.split(",") if k.strip()] if args.keywords else []
|
|
644
663
|
|
|
645
664
|
# Check if searching current project only
|
|
646
665
|
if not getattr(args, 'global'):
|
|
@@ -656,10 +675,11 @@ To persist directory changes when resuming sessions:
|
|
|
656
675
|
|
|
657
676
|
if not matching_sessions:
|
|
658
677
|
scope = "all projects" if getattr(args, 'global') else "current project"
|
|
678
|
+
keyword_msg = f" containing all keywords: {', '.join(keywords)}" if keywords else ""
|
|
659
679
|
if RICH_AVAILABLE and console and not args.shell:
|
|
660
|
-
console.print(f"[yellow]No sessions found
|
|
680
|
+
console.print(f"[yellow]No sessions found{keyword_msg} in {scope}[/yellow]")
|
|
661
681
|
else:
|
|
662
|
-
print(f"No sessions found
|
|
682
|
+
print(f"No sessions found{keyword_msg} in {scope}", file=sys.stderr)
|
|
663
683
|
sys.exit(0)
|
|
664
684
|
|
|
665
685
|
# If we have rich and there are results, show interactive UI
|
|
@@ -112,10 +112,44 @@ def search_keywords_in_file(
|
|
|
112
112
|
Search for keywords in a Codex session file.
|
|
113
113
|
|
|
114
114
|
Returns: (found, line_count, preview)
|
|
115
|
-
- found: True if all keywords found (case-insensitive AND logic)
|
|
115
|
+
- found: True if all keywords found (case-insensitive AND logic), or True if no keywords
|
|
116
116
|
- line_count: total lines in file
|
|
117
117
|
- preview: best user message content (skips system messages)
|
|
118
118
|
"""
|
|
119
|
+
# If no keywords, match all files
|
|
120
|
+
if not keywords:
|
|
121
|
+
line_count = 0
|
|
122
|
+
last_user_message = None
|
|
123
|
+
try:
|
|
124
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
125
|
+
for line in f:
|
|
126
|
+
line_count += 1
|
|
127
|
+
if not line.strip():
|
|
128
|
+
continue
|
|
129
|
+
try:
|
|
130
|
+
entry = json.loads(line)
|
|
131
|
+
# Extract user messages (skip system messages)
|
|
132
|
+
if (
|
|
133
|
+
entry.get("type") == "response_item"
|
|
134
|
+
and entry.get("payload", {}).get("role") == "user"
|
|
135
|
+
):
|
|
136
|
+
content = entry.get("payload", {}).get("content", [])
|
|
137
|
+
if isinstance(content, list) and len(content) > 0:
|
|
138
|
+
first_item = content[0]
|
|
139
|
+
if isinstance(first_item, dict):
|
|
140
|
+
text = first_item.get("text", "")
|
|
141
|
+
if text and not is_system_message(text):
|
|
142
|
+
cleaned = text[:400].replace("\n", " ").strip()
|
|
143
|
+
if len(cleaned) > 20:
|
|
144
|
+
last_user_message = cleaned
|
|
145
|
+
elif last_user_message is None:
|
|
146
|
+
last_user_message = cleaned
|
|
147
|
+
except json.JSONDecodeError:
|
|
148
|
+
continue
|
|
149
|
+
return True, line_count, last_user_message
|
|
150
|
+
except (OSError, IOError):
|
|
151
|
+
return False, 0, None
|
|
152
|
+
|
|
119
153
|
keywords_lower = [k.lower() for k in keywords]
|
|
120
154
|
found_keywords = set()
|
|
121
155
|
line_count = 0
|
|
@@ -277,6 +311,7 @@ def find_sessions(
|
|
|
277
311
|
|
|
278
312
|
def display_interactive_ui(
|
|
279
313
|
matches: list[dict],
|
|
314
|
+
keywords: list[str] = None,
|
|
280
315
|
) -> Optional[dict]:
|
|
281
316
|
"""
|
|
282
317
|
Display matches in interactive UI and get user selection.
|
|
@@ -289,7 +324,8 @@ def display_interactive_ui(
|
|
|
289
324
|
|
|
290
325
|
if RICH_AVAILABLE:
|
|
291
326
|
console = Console()
|
|
292
|
-
|
|
327
|
+
title = f"Codex Sessions matching: {', '.join(keywords)}" if keywords else "All Codex Sessions"
|
|
328
|
+
table = Table(title=title, show_header=True)
|
|
293
329
|
table.add_column("#", style="cyan", justify="right")
|
|
294
330
|
table.add_column("Session ID", style="yellow", no_wrap=True)
|
|
295
331
|
table.add_column("Project", style="green")
|
|
@@ -486,7 +522,9 @@ Examples:
|
|
|
486
522
|
|
|
487
523
|
parser.add_argument(
|
|
488
524
|
"keywords",
|
|
489
|
-
|
|
525
|
+
nargs='?',
|
|
526
|
+
default="",
|
|
527
|
+
help="Comma-separated keywords to search (AND logic). If omitted, shows all sessions.",
|
|
490
528
|
)
|
|
491
529
|
parser.add_argument(
|
|
492
530
|
"-g",
|
|
@@ -515,10 +553,7 @@ Examples:
|
|
|
515
553
|
args = parser.parse_args()
|
|
516
554
|
|
|
517
555
|
# Parse keywords
|
|
518
|
-
keywords = [k.strip() for k in args.keywords.split(",") if k.strip()]
|
|
519
|
-
if not keywords:
|
|
520
|
-
print("Error: No keywords provided", file=sys.stderr)
|
|
521
|
-
sys.exit(1)
|
|
556
|
+
keywords = [k.strip() for k in args.keywords.split(",") if k.strip()] if args.keywords else []
|
|
522
557
|
|
|
523
558
|
# Get Codex home
|
|
524
559
|
codex_home = get_codex_home(args.codex_home)
|
|
@@ -532,7 +567,7 @@ Examples:
|
|
|
532
567
|
)
|
|
533
568
|
|
|
534
569
|
# Display and get selection
|
|
535
|
-
selected_match = display_interactive_ui(matches)
|
|
570
|
+
selected_match = display_interactive_ui(matches, keywords)
|
|
536
571
|
if not selected_match:
|
|
537
572
|
return
|
|
538
573
|
|
|
@@ -0,0 +1,438 @@
|
|
|
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) -> Optional[str]:
|
|
266
|
+
"""Show action menu for selected session."""
|
|
267
|
+
print(f"\n=== Session: {session['session_id'][:16]}... ===")
|
|
268
|
+
print(f"Agent: {session['agent_display']}")
|
|
269
|
+
print(f"Project: {session['project']}")
|
|
270
|
+
if session.get("branch"):
|
|
271
|
+
print(f"Branch: {session['branch']}")
|
|
272
|
+
print(f"\nWhat would you like to do?")
|
|
273
|
+
print("1. Resume session (default)")
|
|
274
|
+
print("2. Show session file path")
|
|
275
|
+
print("3. Copy session file to file (*.jsonl) or directory")
|
|
276
|
+
print()
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
choice = input("Enter choice [1-3] (or Enter for 1): ").strip()
|
|
280
|
+
if not choice or choice == "1":
|
|
281
|
+
return "resume"
|
|
282
|
+
elif choice == "2":
|
|
283
|
+
return "path"
|
|
284
|
+
elif choice == "3":
|
|
285
|
+
return "copy"
|
|
286
|
+
else:
|
|
287
|
+
print("Invalid choice.")
|
|
288
|
+
return None
|
|
289
|
+
except KeyboardInterrupt:
|
|
290
|
+
print("\nCancelled.")
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def handle_action(session: dict, action: str, shell_mode: bool = False) -> None:
|
|
295
|
+
"""Handle the selected action based on agent type."""
|
|
296
|
+
agent = session["agent"]
|
|
297
|
+
|
|
298
|
+
if action == "resume":
|
|
299
|
+
if agent == "claude":
|
|
300
|
+
resume_claude_session(
|
|
301
|
+
session["session_id"],
|
|
302
|
+
session["cwd"],
|
|
303
|
+
shell_mode=shell_mode,
|
|
304
|
+
claude_home=session.get("claude_home"),
|
|
305
|
+
)
|
|
306
|
+
elif agent == "codex":
|
|
307
|
+
resume_codex_session(
|
|
308
|
+
session["session_id"], session["cwd"], shell_mode=shell_mode
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
elif action == "path":
|
|
312
|
+
if agent == "claude":
|
|
313
|
+
file_path = get_claude_session_file_path(
|
|
314
|
+
session["session_id"],
|
|
315
|
+
session["cwd"],
|
|
316
|
+
claude_home=session.get("claude_home"),
|
|
317
|
+
)
|
|
318
|
+
print(f"\nSession file path:")
|
|
319
|
+
print(file_path)
|
|
320
|
+
elif agent == "codex":
|
|
321
|
+
print(f"\nSession file path:")
|
|
322
|
+
print(session.get("file_path", "Unknown"))
|
|
323
|
+
|
|
324
|
+
elif action == "copy":
|
|
325
|
+
if agent == "claude":
|
|
326
|
+
file_path = get_claude_session_file_path(
|
|
327
|
+
session["session_id"],
|
|
328
|
+
session["cwd"],
|
|
329
|
+
claude_home=session.get("claude_home"),
|
|
330
|
+
)
|
|
331
|
+
copy_claude_session_file(file_path)
|
|
332
|
+
elif agent == "codex":
|
|
333
|
+
copy_codex_session_file(session.get("file_path", ""))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main():
|
|
337
|
+
parser = argparse.ArgumentParser(
|
|
338
|
+
description="Unified session finder - search across multiple coding agents",
|
|
339
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
340
|
+
epilog="""
|
|
341
|
+
Examples:
|
|
342
|
+
find-session "langroid,MCP" # Search all agents in current project
|
|
343
|
+
find-session -g # Show all sessions across all projects
|
|
344
|
+
find-session "bug" --agents claude # Search only Claude sessions
|
|
345
|
+
find-session "error" --agents codex # Search only Codex sessions
|
|
346
|
+
""",
|
|
347
|
+
)
|
|
348
|
+
parser.add_argument(
|
|
349
|
+
"keywords",
|
|
350
|
+
nargs="?",
|
|
351
|
+
default="",
|
|
352
|
+
help="Comma-separated keywords to search (AND logic). If omitted, shows all sessions.",
|
|
353
|
+
)
|
|
354
|
+
parser.add_argument(
|
|
355
|
+
"-g",
|
|
356
|
+
"--global",
|
|
357
|
+
dest="global_search",
|
|
358
|
+
action="store_true",
|
|
359
|
+
help="Search across all projects, not just the current one",
|
|
360
|
+
)
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"-n",
|
|
363
|
+
"--num-matches",
|
|
364
|
+
type=int,
|
|
365
|
+
default=10,
|
|
366
|
+
help="Number of matching sessions to display (default: 10)",
|
|
367
|
+
)
|
|
368
|
+
parser.add_argument(
|
|
369
|
+
"--agents",
|
|
370
|
+
nargs="+",
|
|
371
|
+
choices=["claude", "codex"],
|
|
372
|
+
help="Limit search to specific agents (default: all)",
|
|
373
|
+
)
|
|
374
|
+
parser.add_argument(
|
|
375
|
+
"--shell",
|
|
376
|
+
action="store_true",
|
|
377
|
+
help="Output shell commands for evaluation (for use with shell function)",
|
|
378
|
+
)
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
"--claude-home", type=str, help="Path to Claude home directory (default: ~/.claude)"
|
|
381
|
+
)
|
|
382
|
+
parser.add_argument(
|
|
383
|
+
"--codex-home", type=str, help="Path to Codex home directory (default: ~/.codex)"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
args = parser.parse_args()
|
|
387
|
+
|
|
388
|
+
# Parse keywords
|
|
389
|
+
keywords = (
|
|
390
|
+
[k.strip() for k in args.keywords.split(",") if k.strip()]
|
|
391
|
+
if args.keywords
|
|
392
|
+
else []
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Search all agents
|
|
396
|
+
matching_sessions = search_all_agents(
|
|
397
|
+
keywords,
|
|
398
|
+
global_search=args.global_search,
|
|
399
|
+
num_matches=args.num_matches,
|
|
400
|
+
agents=args.agents,
|
|
401
|
+
claude_home=args.claude_home,
|
|
402
|
+
codex_home=args.codex_home,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if not matching_sessions:
|
|
406
|
+
scope = "all projects" if args.global_search else "current project"
|
|
407
|
+
keyword_msg = (
|
|
408
|
+
f" containing all keywords: {', '.join(keywords)}" if keywords else ""
|
|
409
|
+
)
|
|
410
|
+
if RICH_AVAILABLE:
|
|
411
|
+
console = Console()
|
|
412
|
+
console.print(f"[yellow]No sessions found{keyword_msg} in {scope}[/yellow]")
|
|
413
|
+
else:
|
|
414
|
+
print(f"No sessions found{keyword_msg} in {scope}", file=sys.stderr)
|
|
415
|
+
sys.exit(0)
|
|
416
|
+
|
|
417
|
+
# Display interactive UI
|
|
418
|
+
if RICH_AVAILABLE:
|
|
419
|
+
selected_session = display_interactive_ui(
|
|
420
|
+
matching_sessions, keywords, stderr_mode=args.shell, num_matches=args.num_matches
|
|
421
|
+
)
|
|
422
|
+
if selected_session:
|
|
423
|
+
# Show action menu
|
|
424
|
+
action = show_action_menu(selected_session)
|
|
425
|
+
if action:
|
|
426
|
+
handle_action(selected_session, action, shell_mode=args.shell)
|
|
427
|
+
else:
|
|
428
|
+
# Fallback without rich
|
|
429
|
+
print("\nMatching sessions:")
|
|
430
|
+
for idx, session in enumerate(matching_sessions[: args.num_matches], 1):
|
|
431
|
+
print(
|
|
432
|
+
f"{idx}. [{session['agent_display']}] {session['session_id'][:16]}... | "
|
|
433
|
+
f"{session['project']} | {session.get('branch', 'N/A')}"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
if __name__ == "__main__":
|
|
438
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-code-tools"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.6"
|
|
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,8 @@ 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"
|
|
21
|
+
fs = "claude_code_tools.find_session:main"
|
|
20
22
|
vault = "claude_code_tools.dotenv_vault:main"
|
|
21
23
|
tmux-cli = "claude_code_tools.tmux_cli_controller:main"
|
|
22
24
|
env-safe = "claude_code_tools.env_safe:main"
|
|
@@ -43,7 +45,7 @@ exclude = [
|
|
|
43
45
|
|
|
44
46
|
[tool.commitizen]
|
|
45
47
|
name = "cz_conventional_commits"
|
|
46
|
-
version = "0.2.
|
|
48
|
+
version = "0.2.6"
|
|
47
49
|
tag_format = "v$version"
|
|
48
50
|
version_files = [
|
|
49
51
|
"pyproject.toml:version",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/tmux_cli_controller.py
RENAMED
|
File without changes
|
{claude_code_tools-0.2.4 → claude_code_tools-0.2.6}/claude_code_tools/tmux_remote_controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|