claude-code-tools 0.1.12__py3-none-any.whl → 0.1.14__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_claude_session.py +76 -29
- {claude_code_tools-0.1.12.dist-info → claude_code_tools-0.1.14.dist-info}/METADATA +25 -6
- {claude_code_tools-0.1.12.dist-info → claude_code_tools-0.1.14.dist-info}/RECORD +8 -6
- docs/claude-code-chutes.md +138 -0
- docs/reddit-post.md +55 -0
- {claude_code_tools-0.1.12.dist-info → claude_code_tools-0.1.14.dist-info}/WHEEL +0 -0
- {claude_code_tools-0.1.12.dist-info → claude_code_tools-0.1.14.dist-info}/entry_points.txt +0 -0
claude_code_tools/__init__.py
CHANGED
|
@@ -38,19 +38,31 @@ except ImportError:
|
|
|
38
38
|
console = Console() if RICH_AVAILABLE else None
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def get_claude_project_dir() -> Path:
|
|
41
|
+
def get_claude_project_dir(claude_home: Optional[str] = None) -> Path:
|
|
42
42
|
"""Convert current working directory to Claude project directory path."""
|
|
43
43
|
cwd = os.getcwd()
|
|
44
|
+
|
|
45
|
+
# Use provided claude_home or default to ~/.claude
|
|
46
|
+
if claude_home:
|
|
47
|
+
base_dir = Path(claude_home).expanduser()
|
|
48
|
+
else:
|
|
49
|
+
base_dir = Path.home() / ".claude"
|
|
44
50
|
|
|
45
51
|
# Replace / with - to match Claude's directory naming convention
|
|
46
52
|
project_path = cwd.replace("/", "-")
|
|
47
|
-
claude_dir =
|
|
53
|
+
claude_dir = base_dir / "projects" / project_path
|
|
48
54
|
return claude_dir
|
|
49
55
|
|
|
50
56
|
|
|
51
|
-
def get_all_claude_projects() -> List[Tuple[Path, str]]:
|
|
57
|
+
def get_all_claude_projects(claude_home: Optional[str] = None) -> List[Tuple[Path, str]]:
|
|
52
58
|
"""Get all Claude project directories with their original paths."""
|
|
53
|
-
|
|
59
|
+
# Use provided claude_home or default to ~/.claude
|
|
60
|
+
if claude_home:
|
|
61
|
+
base_dir = Path(claude_home).expanduser()
|
|
62
|
+
else:
|
|
63
|
+
base_dir = Path.home() / ".claude"
|
|
64
|
+
|
|
65
|
+
projects_dir = base_dir / "projects"
|
|
54
66
|
|
|
55
67
|
if not projects_dir.exists():
|
|
56
68
|
return []
|
|
@@ -109,39 +121,51 @@ def extract_project_name(original_path: str) -> str:
|
|
|
109
121
|
return parts[-1] if parts else "unknown"
|
|
110
122
|
|
|
111
123
|
|
|
112
|
-
def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool, int]:
|
|
124
|
+
def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool, int, Optional[str]]:
|
|
113
125
|
"""
|
|
114
|
-
Check if all keywords are present in the JSONL file and
|
|
126
|
+
Check if all keywords are present in the JSONL file, count lines, and extract git branch.
|
|
115
127
|
|
|
116
128
|
Args:
|
|
117
129
|
filepath: Path to the JSONL file
|
|
118
130
|
keywords: List of keywords to search for (case-insensitive)
|
|
119
131
|
|
|
120
132
|
Returns:
|
|
121
|
-
Tuple of (matches: bool, line_count: int)
|
|
133
|
+
Tuple of (matches: bool, line_count: int, git_branch: Optional[str])
|
|
122
134
|
- matches: True if ALL keywords are found in the file
|
|
123
135
|
- line_count: Total number of lines in the file
|
|
136
|
+
- git_branch: Git branch name from the first message that has it, or None
|
|
124
137
|
"""
|
|
125
138
|
# Convert keywords to lowercase for case-insensitive search
|
|
126
139
|
keywords_lower = [k.lower() for k in keywords]
|
|
127
140
|
found_keywords = set()
|
|
128
141
|
line_count = 0
|
|
142
|
+
git_branch = None
|
|
129
143
|
|
|
130
144
|
try:
|
|
131
145
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
132
146
|
for line in f:
|
|
133
147
|
line_count += 1
|
|
134
148
|
line_lower = line.lower()
|
|
149
|
+
|
|
150
|
+
# Extract git branch from JSON if not already found
|
|
151
|
+
if git_branch is None:
|
|
152
|
+
try:
|
|
153
|
+
data = json.loads(line.strip())
|
|
154
|
+
if 'gitBranch' in data and data['gitBranch']:
|
|
155
|
+
git_branch = data['gitBranch']
|
|
156
|
+
except (json.JSONDecodeError, KeyError):
|
|
157
|
+
pass
|
|
158
|
+
|
|
135
159
|
# Check which keywords are in this line
|
|
136
160
|
for keyword in keywords_lower:
|
|
137
161
|
if keyword in line_lower:
|
|
138
162
|
found_keywords.add(keyword)
|
|
139
163
|
except Exception:
|
|
140
164
|
# Skip files that can't be read
|
|
141
|
-
return False, 0
|
|
165
|
+
return False, 0, None
|
|
142
166
|
|
|
143
167
|
matches = len(found_keywords) == len(keywords_lower)
|
|
144
|
-
return matches, line_count
|
|
168
|
+
return matches, line_count, git_branch
|
|
145
169
|
|
|
146
170
|
|
|
147
171
|
def get_session_preview(filepath: Path) -> str:
|
|
@@ -175,22 +199,23 @@ def get_session_preview(filepath: Path) -> str:
|
|
|
175
199
|
return "No preview available"
|
|
176
200
|
|
|
177
201
|
|
|
178
|
-
def find_sessions(keywords: List[str], global_search: bool = False) -> List[Tuple[str, float, int, str, str, str]]:
|
|
202
|
+
def find_sessions(keywords: List[str], global_search: bool = False, claude_home: Optional[str] = None) -> List[Tuple[str, float, int, str, str, str, Optional[str]]]:
|
|
179
203
|
"""
|
|
180
204
|
Find all Claude Code sessions containing the specified keywords.
|
|
181
205
|
|
|
182
206
|
Args:
|
|
183
207
|
keywords: List of keywords to search for
|
|
184
208
|
global_search: If True, search all projects; if False, search current project only
|
|
209
|
+
claude_home: Optional custom Claude home directory (defaults to ~/.claude)
|
|
185
210
|
|
|
186
211
|
Returns:
|
|
187
|
-
List of tuples (session_id, modification_time, line_count, project_name, preview, project_path) sorted by modification time
|
|
212
|
+
List of tuples (session_id, modification_time, line_count, project_name, preview, project_path, git_branch) sorted by modification time
|
|
188
213
|
"""
|
|
189
214
|
matching_sessions = []
|
|
190
215
|
|
|
191
216
|
if global_search:
|
|
192
217
|
# Search all projects
|
|
193
|
-
projects = get_all_claude_projects()
|
|
218
|
+
projects = get_all_claude_projects(claude_home)
|
|
194
219
|
|
|
195
220
|
if RICH_AVAILABLE and console:
|
|
196
221
|
with Progress(
|
|
@@ -207,12 +232,12 @@ def find_sessions(keywords: List[str], global_search: bool = False) -> List[Tupl
|
|
|
207
232
|
|
|
208
233
|
# Search all JSONL files in this project directory
|
|
209
234
|
for jsonl_file in project_dir.glob("*.jsonl"):
|
|
210
|
-
matches, line_count = search_keywords_in_file(jsonl_file, keywords)
|
|
235
|
+
matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
|
|
211
236
|
if matches:
|
|
212
237
|
session_id = jsonl_file.stem
|
|
213
238
|
mod_time = jsonl_file.stat().st_mtime
|
|
214
239
|
preview = get_session_preview(jsonl_file)
|
|
215
|
-
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path))
|
|
240
|
+
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path, git_branch))
|
|
216
241
|
|
|
217
242
|
progress.advance(task)
|
|
218
243
|
else:
|
|
@@ -221,15 +246,15 @@ def find_sessions(keywords: List[str], global_search: bool = False) -> List[Tupl
|
|
|
221
246
|
project_name = extract_project_name(original_path)
|
|
222
247
|
|
|
223
248
|
for jsonl_file in project_dir.glob("*.jsonl"):
|
|
224
|
-
matches, line_count = search_keywords_in_file(jsonl_file, keywords)
|
|
249
|
+
matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
|
|
225
250
|
if matches:
|
|
226
251
|
session_id = jsonl_file.stem
|
|
227
252
|
mod_time = jsonl_file.stat().st_mtime
|
|
228
253
|
preview = get_session_preview(jsonl_file)
|
|
229
|
-
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path))
|
|
254
|
+
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path, git_branch))
|
|
230
255
|
else:
|
|
231
256
|
# Search current project only
|
|
232
|
-
claude_dir = get_claude_project_dir()
|
|
257
|
+
claude_dir = get_claude_project_dir(claude_home)
|
|
233
258
|
|
|
234
259
|
if not claude_dir.exists():
|
|
235
260
|
return []
|
|
@@ -238,12 +263,12 @@ def find_sessions(keywords: List[str], global_search: bool = False) -> List[Tupl
|
|
|
238
263
|
|
|
239
264
|
# Search all JSONL files in the directory
|
|
240
265
|
for jsonl_file in claude_dir.glob("*.jsonl"):
|
|
241
|
-
matches, line_count = search_keywords_in_file(jsonl_file, keywords)
|
|
266
|
+
matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
|
|
242
267
|
if matches:
|
|
243
268
|
session_id = jsonl_file.stem
|
|
244
269
|
mod_time = jsonl_file.stat().st_mtime
|
|
245
270
|
preview = get_session_preview(jsonl_file)
|
|
246
|
-
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, os.getcwd()))
|
|
271
|
+
matching_sessions.append((session_id, mod_time, line_count, project_name, preview, os.getcwd(), git_branch))
|
|
247
272
|
|
|
248
273
|
# Sort by modification time (newest first)
|
|
249
274
|
matching_sessions.sort(key=lambda x: x[1], reverse=True)
|
|
@@ -251,7 +276,7 @@ def find_sessions(keywords: List[str], global_search: bool = False) -> List[Tupl
|
|
|
251
276
|
return matching_sessions
|
|
252
277
|
|
|
253
278
|
|
|
254
|
-
def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]], keywords: List[str], stderr_mode: bool = False, num_matches: int = 10) -> Optional[Tuple[str, str]]:
|
|
279
|
+
def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str, Optional[str]]], keywords: List[str], stderr_mode: bool = False, num_matches: int = 10) -> Optional[Tuple[str, str]]:
|
|
255
280
|
"""Display interactive UI for session selection."""
|
|
256
281
|
if not RICH_AVAILABLE:
|
|
257
282
|
return None
|
|
@@ -279,16 +304,19 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]]
|
|
|
279
304
|
table.add_column("#", style="bold yellow", width=3)
|
|
280
305
|
table.add_column("Session ID", style="dim")
|
|
281
306
|
table.add_column("Project", style="green")
|
|
307
|
+
table.add_column("Branch", style="magenta")
|
|
282
308
|
table.add_column("Date", style="blue")
|
|
283
309
|
table.add_column("Lines", style="cyan", justify="right")
|
|
284
310
|
table.add_column("Preview", style="white", overflow="fold")
|
|
285
311
|
|
|
286
|
-
for idx, (session_id, mod_time, line_count, project_name, preview, _) in enumerate(display_sessions, 1):
|
|
312
|
+
for idx, (session_id, mod_time, line_count, project_name, preview, _, git_branch) in enumerate(display_sessions, 1):
|
|
287
313
|
mod_date = datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M')
|
|
314
|
+
branch_display = git_branch if git_branch else "N/A"
|
|
288
315
|
table.add_row(
|
|
289
316
|
str(idx),
|
|
290
317
|
session_id[:8] + "...",
|
|
291
318
|
project_name,
|
|
319
|
+
branch_display,
|
|
292
320
|
mod_date,
|
|
293
321
|
str(line_count),
|
|
294
322
|
preview
|
|
@@ -314,6 +342,11 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]]
|
|
|
314
342
|
console=ui_console
|
|
315
343
|
)
|
|
316
344
|
|
|
345
|
+
# Handle empty input
|
|
346
|
+
if not choice or not choice.strip():
|
|
347
|
+
ui_console.print("[red]Invalid choice. Please try again.[/red]")
|
|
348
|
+
continue
|
|
349
|
+
|
|
317
350
|
# Restore stdout
|
|
318
351
|
if stderr_mode:
|
|
319
352
|
sys.stdout.close()
|
|
@@ -327,7 +360,10 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]]
|
|
|
327
360
|
except KeyboardInterrupt:
|
|
328
361
|
ui_console.print("\n[yellow]Cancelled[/yellow]")
|
|
329
362
|
return None
|
|
330
|
-
except
|
|
363
|
+
except EOFError:
|
|
364
|
+
ui_console.print("\n[yellow]Cancelled (EOF)[/yellow]")
|
|
365
|
+
return None
|
|
366
|
+
except ValueError:
|
|
331
367
|
ui_console.print("[red]Invalid choice. Please try again.[/red]")
|
|
332
368
|
|
|
333
369
|
|
|
@@ -438,6 +474,11 @@ To persist directory changes when resuming sessions:
|
|
|
438
474
|
action="store_true",
|
|
439
475
|
help="Output shell commands for evaluation (for use with shell function)"
|
|
440
476
|
)
|
|
477
|
+
parser.add_argument(
|
|
478
|
+
"--claude-home",
|
|
479
|
+
type=str,
|
|
480
|
+
help="Path to Claude home directory (default: ~/.claude)"
|
|
481
|
+
)
|
|
441
482
|
|
|
442
483
|
args = parser.parse_args()
|
|
443
484
|
|
|
@@ -450,7 +491,7 @@ To persist directory changes when resuming sessions:
|
|
|
450
491
|
|
|
451
492
|
# Check if searching current project only
|
|
452
493
|
if not getattr(args, 'global'):
|
|
453
|
-
claude_dir = get_claude_project_dir()
|
|
494
|
+
claude_dir = get_claude_project_dir(args.claude_home)
|
|
454
495
|
|
|
455
496
|
if not claude_dir.exists():
|
|
456
497
|
print(f"No Claude project directory found for: {os.getcwd()}", file=sys.stderr)
|
|
@@ -458,7 +499,7 @@ To persist directory changes when resuming sessions:
|
|
|
458
499
|
sys.exit(1)
|
|
459
500
|
|
|
460
501
|
# Find matching sessions
|
|
461
|
-
matching_sessions = find_sessions(keywords, global_search=getattr(args, 'global'))
|
|
502
|
+
matching_sessions = find_sessions(keywords, global_search=getattr(args, 'global'), claude_home=args.claude_home)
|
|
462
503
|
|
|
463
504
|
if not matching_sessions:
|
|
464
505
|
scope = "all projects" if getattr(args, 'global') else "current project"
|
|
@@ -478,12 +519,13 @@ To persist directory changes when resuming sessions:
|
|
|
478
519
|
# Fallback: print session IDs as before
|
|
479
520
|
if not args.shell:
|
|
480
521
|
print("\nMatching sessions:")
|
|
481
|
-
for idx, (session_id, mod_time, line_count, project_name, preview, project_path) in enumerate(matching_sessions[:args.num_matches], 1):
|
|
522
|
+
for idx, (session_id, mod_time, line_count, project_name, preview, project_path, git_branch) in enumerate(matching_sessions[:args.num_matches], 1):
|
|
482
523
|
mod_date = datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M:%S')
|
|
524
|
+
branch_display = git_branch if git_branch else "N/A"
|
|
483
525
|
if getattr(args, 'global'):
|
|
484
|
-
print(f"{idx}. {session_id} | {project_name} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
|
|
526
|
+
print(f"{idx}. {session_id} | {project_name} | {branch_display} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
|
|
485
527
|
else:
|
|
486
|
-
print(f"{idx}. {session_id} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
|
|
528
|
+
print(f"{idx}. {session_id} | {branch_display} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
|
|
487
529
|
|
|
488
530
|
if len(matching_sessions) > args.num_matches:
|
|
489
531
|
print(f"\n... and {len(matching_sessions) - args.num_matches} more sessions", file=sys.stderr if args.shell else sys.stdout)
|
|
@@ -492,7 +534,7 @@ To persist directory changes when resuming sessions:
|
|
|
492
534
|
if len(matching_sessions) == 1:
|
|
493
535
|
if not args.shell:
|
|
494
536
|
print("\nOnly one match found. Resuming automatically...")
|
|
495
|
-
session_id, _, _, _, _, project_path = matching_sessions[0]
|
|
537
|
+
session_id, _, _, _, _, project_path, _ = matching_sessions[0]
|
|
496
538
|
resume_session(session_id, project_path, shell_mode=args.shell)
|
|
497
539
|
else:
|
|
498
540
|
try:
|
|
@@ -503,10 +545,15 @@ To persist directory changes when resuming sessions:
|
|
|
503
545
|
choice = sys.stdin.readline().strip()
|
|
504
546
|
else:
|
|
505
547
|
choice = input("\nEnter number to resume session (or Ctrl+C to cancel): ")
|
|
548
|
+
|
|
549
|
+
# Handle empty input or EOF
|
|
550
|
+
if not choice:
|
|
551
|
+
print("Cancelled (EOF)", file=sys.stderr)
|
|
552
|
+
sys.exit(0)
|
|
506
553
|
|
|
507
554
|
idx = int(choice) - 1
|
|
508
555
|
if 0 <= idx < min(args.num_matches, len(matching_sessions)):
|
|
509
|
-
session_id, _, _, _, _, project_path = matching_sessions[idx]
|
|
556
|
+
session_id, _, _, _, _, project_path, _ = matching_sessions[idx]
|
|
510
557
|
resume_session(session_id, project_path, shell_mode=args.shell)
|
|
511
558
|
else:
|
|
512
559
|
print("Invalid choice", file=sys.stderr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-tools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
4
4
|
Summary: Collection of tools for working with Claude Code
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: click>=8.0.0
|
|
@@ -12,10 +12,13 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
|
|
13
13
|
# claude-code-tools
|
|
14
14
|
|
|
15
|
-
A collection of practical tools, hooks, and utilities for enhancing Claude Code
|
|
15
|
+
A collection of practical tools, hooks, and utilities for enhancing Claude Code
|
|
16
|
+
and other CLI coding agents.
|
|
16
17
|
|
|
17
18
|
## 🎮 tmux-cli: Bridging Claude Code and Interactive CLIs
|
|
18
19
|
|
|
20
|
+
> **Note**: While the description below focuses on Claude Code, tmux-cli works with any CLI coding agent.
|
|
21
|
+
|
|
19
22
|

|
|
20
23
|
|
|
21
24
|
Consider these scenarios:
|
|
@@ -25,8 +28,9 @@ for user input, but CC can't respond to prompts.
|
|
|
25
28
|
|
|
26
29
|
You want Claude Code to debug using pdb, stepping through code line by line.
|
|
27
30
|
|
|
28
|
-
You need
|
|
29
|
-
|
|
31
|
+
You need your CLI code agent to launch another instance of the same OR different
|
|
32
|
+
CLI code agent, AND interact with it, not as a hidden sub-agent,
|
|
33
|
+
but as a visible session you can monitor (as shown in gif above).
|
|
30
34
|
|
|
31
35
|
**tmux-cli enables these workflows** by giving Claude Code programmatic control
|
|
32
36
|
over terminal applications.
|
|
@@ -47,7 +51,10 @@ use tmux-cli behind the scenes.
|
|
|
47
51
|
## 🚀 Quick Start
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
# Install
|
|
54
|
+
# Install from PyPI (recommended)
|
|
55
|
+
uv tool install claude-code-tools
|
|
56
|
+
|
|
57
|
+
# Or install the latest development version from GitHub
|
|
51
58
|
uv tool install git+https://github.com/pchalasani/claude-code-tools
|
|
52
59
|
```
|
|
53
60
|
|
|
@@ -111,7 +118,15 @@ Add this function to your shell config (.bashrc/.zshrc) for persistent directory
|
|
|
111
118
|
changes:
|
|
112
119
|
|
|
113
120
|
```bash
|
|
114
|
-
fcs() {
|
|
121
|
+
fcs() {
|
|
122
|
+
# Check if user is asking for help
|
|
123
|
+
if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
|
|
124
|
+
find-claude-session --help
|
|
125
|
+
return
|
|
126
|
+
fi
|
|
127
|
+
# Run find-claude-session in shell mode and evaluate the output
|
|
128
|
+
eval "$(find-claude-session --shell "$@" | sed '/^$/d')"
|
|
129
|
+
}
|
|
115
130
|
```
|
|
116
131
|
|
|
117
132
|
Or source the provided function:
|
|
@@ -142,6 +157,10 @@ won't persist after exiting Claude Code.
|
|
|
142
157
|
|
|
143
158
|
For detailed documentation, see [docs/find-claude-session.md](docs/find-claude-session.md).
|
|
144
159
|
|
|
160
|
+
Looks like this --
|
|
161
|
+
|
|
162
|
+

|
|
163
|
+
|
|
145
164
|
## 🔐 vault
|
|
146
165
|
|
|
147
166
|
Centralized encrypted backup for .env files across all your projects using SOPS.
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
claude_code_tools/__init__.py,sha256=
|
|
1
|
+
claude_code_tools/__init__.py,sha256=_-QQFZ18-bFEW2GEAgme8FhI4Ejyk7Fxq8o0zPNtT9U,90
|
|
2
2
|
claude_code_tools/dotenv_vault.py,sha256=KPI9NDFu5HE6FfhQUYw6RhdR-miN0ScJHsBg0OVG61k,9617
|
|
3
|
-
claude_code_tools/find_claude_session.py,sha256=
|
|
3
|
+
claude_code_tools/find_claude_session.py,sha256=TfQWW2zMDJAnfLREt_P23BB6e9Qb-XS22SSEU80K-4Y,23524
|
|
4
4
|
claude_code_tools/tmux_cli_controller.py,sha256=0PqWBhITw74TtOKAfiUJMyHEFVOlLp4sEYmSlDHyznc,27482
|
|
5
5
|
claude_code_tools/tmux_remote_controller.py,sha256=uK9lJKrNz7_NeV1_V3BM-q0r6sRVmYfOR8H3zo5hfH8,3220
|
|
6
|
+
docs/claude-code-chutes.md,sha256=jCnYAAHZm32NGHE0CzGGl3vpO_zlF_xdmr23YxuCjPg,8098
|
|
6
7
|
docs/claude-code-tmux-tutorials.md,sha256=S-9U3a1AaPEBPo3oKpWuyOfKK7yPFOIu21P_LDfGUJk,7558
|
|
7
8
|
docs/find-claude-session.md,sha256=fACbQP0Bj5jqIpNWk0lGDOQQaji-K9Va3gUv2RA47VQ,4284
|
|
9
|
+
docs/reddit-post.md,sha256=ZA7kPoJNi06t6F9JQMBiIOv039ADC9lM8YXFt8UA_Jg,2345
|
|
8
10
|
docs/tmux-cli-instructions.md,sha256=lQqKTI-uhH-EdU9P4To4GC10WJjj3VllAW4cxd8jfj8,4167
|
|
9
11
|
docs/vault-documentation.md,sha256=5XzNpHyhGU38JU2hKEWEL1gdPq3rC2zBg8yotK4eNF4,3600
|
|
10
|
-
claude_code_tools-0.1.
|
|
11
|
-
claude_code_tools-0.1.
|
|
12
|
-
claude_code_tools-0.1.
|
|
13
|
-
claude_code_tools-0.1.
|
|
12
|
+
claude_code_tools-0.1.14.dist-info/METADATA,sha256=mkXcHoHrAhUHp-4rYvAKD9qDA94C5hsPj9OSs1j2i9I,10313
|
|
13
|
+
claude_code_tools-0.1.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
claude_code_tools-0.1.14.dist-info/entry_points.txt,sha256=yUTZlZ2jteoUZ9bGPvZKI9tjmohPDhRk1fovu5pZACM,181
|
|
15
|
+
claude_code_tools-0.1.14.dist-info/RECORD,,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Here’s what I found (forums + docs) and the cleanest ways to run **Claude Code** against **Chutes (OpenAI-compatible) instead of Anthropic models**—including whether you need an extra router.
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
* **You can’t point Claude Code directly at Chutes** because Claude Code speaks the **Anthropic `/v1/messages`** format, while Chutes exposes an **OpenAI-compatible `/v1/chat/completions`** API. You need a **translator/gateway** in the middle. ([Anthropic][1])
|
|
8
|
+
* Two working choices seen in the community and in Anthropic’s docs:
|
|
9
|
+
|
|
10
|
+
1. **Claude Code Router (CCR)** — purpose-built local proxy that converts Claude Code → OpenAI format; works with OpenRouter, DeepSeek, Groq, etc., and also with custom OpenAI-compatible bases like **Chutes**. ([GitHub][2], [npm][3])
|
|
11
|
+
2. **LiteLLM “Anthropic unified endpoint”** — run LiteLLM as a gateway that **exposes an Anthropic-style endpoint** to Claude Code, while LiteLLM calls Chutes behind the scenes using its **OpenAI-compatible** route. ([Anthropic][1], [LiteLLM][4], [LiteLLM][5])
|
|
12
|
+
|
|
13
|
+
Reddit users repeatedly report running **Claude Code + Chutes** successfully (often with GLM-4.5, DeepSeek, or Kimi K2) by using a router/gateway. ([Reddit][6], [Reddit][7], [Reddit][8])
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Option A — Claude Code Router (simplest)
|
|
18
|
+
|
|
19
|
+
**When to choose:** quick setup focused on Claude Code; easy model switching with `/model`.
|
|
20
|
+
|
|
21
|
+
1. **Install**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i -g @anthropic-ai/claude-code @musistudio/claude-code-router
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
CCR runs a local server and translates requests for you. ([GitHub][2])
|
|
28
|
+
|
|
29
|
+
2. **Configure CCR for Chutes**
|
|
30
|
+
Create `~/.claude-code-router/config.json` like:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"Providers": [
|
|
35
|
+
{
|
|
36
|
+
"name": "chutes",
|
|
37
|
+
"api_base_url": "https://llm.chutes.ai/v1/chat/completions",
|
|
38
|
+
"api_keys": ["YOUR_CHUTES_API_KEY"],
|
|
39
|
+
"models": ["deepseek-ai/DeepSeek-V3-0324"],
|
|
40
|
+
"transformer": { "use": ["openrouter"] }
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"Router": { "default": "chutes,deepseek-ai/DeepSeek-V3-0324" }
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Notes:
|
|
48
|
+
|
|
49
|
+
* Chutes’ **base URL** is OpenAI-style: `https://llm.chutes.ai/v1/chat/completions`. ([Reddit][9])
|
|
50
|
+
* Model IDs vary (e.g., `deepseek-ai/DeepSeek-V3-0324`, `Qwen/Qwen3-235B-A22B`, `ZhipuAI/glm-4.5`)—grab the exact string from Chutes’ model page. ([Reddit][9])
|
|
51
|
+
|
|
52
|
+
3. **Start CCR and point Claude Code at it**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ccr start
|
|
56
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
57
|
+
claude
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
CCR exposes an Anthropic-compatible endpoint; setting `ANTHROPIC_BASE_URL` makes Claude Code use it. In the Claude Code REPL you can switch models with `/model provider,model`. ([npm][3])
|
|
61
|
+
|
|
62
|
+
**Community confirmation:** multiple Reddit threads mention using Claude Code with GLM-4.5/DeepSeek **via Chutes** (often preferring CCR). ([Reddit][6], [Reddit][7])
|
|
63
|
+
**Video walkthroughs:** several YouTube explainers demo **Claude Code Router**, setup, and model routing. ([YouTube][10], [YouTube][11])
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Option B — LiteLLM “Anthropic unified endpoint” (more general)
|
|
68
|
+
|
|
69
|
+
**When to choose:** you want observability, spend controls, or to route many providers with one gateway.
|
|
70
|
+
|
|
71
|
+
1. **Define a LiteLLM model that points to Chutes**
|
|
72
|
+
Create `litellm_config.yaml`:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
model_list:
|
|
76
|
+
- model_name: chutes-deepseek-v3
|
|
77
|
+
litellm_params:
|
|
78
|
+
model: openai/deepseek-ai/DeepSeek-V3-0324 # tell LiteLLM it's OpenAI-compatible
|
|
79
|
+
api_base: https://llm.chutes.ai/v1 # base should include /v1
|
|
80
|
+
api_key: YOUR_CHUTES_API_KEY
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
LiteLLM’s OpenAI-compatible docs show the `openai/` prefix and the need for `/v1` in `api_base`. ([LiteLLM][5])
|
|
84
|
+
|
|
85
|
+
2. **Run LiteLLM and expose an Anthropic-style endpoint**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
litellm --config /path/to/litellm_config.yaml
|
|
89
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:4000
|
|
90
|
+
claude
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Anthropic’s **LLM Gateway** guide explicitly supports setting `ANTHROPIC_BASE_URL` to a LiteLLM endpoint that speaks the **Anthropic messages** format. ([Anthropic][1], [LiteLLM][4])
|
|
94
|
+
|
|
95
|
+
**Working example calling Chutes through LiteLLM (code snippet/gist):** shows `api_base="https://llm.chutes.ai/v1"` and an `openai/...` model. ([Gist][12])
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Do you need “Claude Code Router”?
|
|
100
|
+
|
|
101
|
+
* **You need some router/gateway.** Claude Code won’t talk directly to Chutes’ OpenAI endpoint. You can use **CCR** (purpose-built) **or** **LiteLLM** (general gateway). Anthropic’s own docs describe the LiteLLM route with `ANTHROPIC_BASE_URL`, which is often the most “officially documented” path. ([Anthropic][1])
|
|
102
|
+
* If you already run LiteLLM for other projects, use **Option B**. If you just want the fastest path specifically for Claude Code, **Option A (CCR)** is very popular on Reddit/YouTube. ([Reddit][7], [YouTube][10])
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Model tips & quirks from forums
|
|
107
|
+
|
|
108
|
+
* **GLM-4.5** and **DeepSeek** models are frequently called out as working well with Claude Code via Chutes (router required). ([Reddit][6])
|
|
109
|
+
* Some users report **tool-call robustness varies by model** (e.g., mixed results for Qwen Coder vs GLM-4.5). Test your target model for tool use. ([Reddit][13])
|
|
110
|
+
* Chutes availability/rate-limit errors happen occasionally; regenerating keys or retrial often resolves. Base URL must be exact. ([Reddit][9])
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Sources
|
|
115
|
+
|
|
116
|
+
* Anthropic docs — **LLM Gateway configuration** (LiteLLM, `ANTHROPIC_BASE_URL`, Anthropic-format endpoint). ([Anthropic][1])
|
|
117
|
+
* LiteLLM docs — **Anthropic unified endpoint** and **OpenAI-compatible** config (`openai/` model prefix, `/v1` base). ([LiteLLM][4], [LiteLLM][5])
|
|
118
|
+
* **Claude Code Router** repo/features and usage, plus fork with added key rotation & commands. ([GitHub][2], [npm][3])
|
|
119
|
+
* Reddit confirmations & how-tos (Chutes base URL, examples, and user reports running Claude Code + Chutes): ([Reddit][9], [Reddit][6], [Reddit][7])
|
|
120
|
+
* YouTube demos of **Claude Code Router** setup/routing. ([YouTube][10], [YouTube][11])
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
If you’d like, tell me your target model on Chutes (e.g., DeepSeek V3, GLM-4.5, Kimi K2), and I’ll hand you an exact `config.json` (CCR) or `litellm_config.yaml` that you can paste in and run.
|
|
125
|
+
|
|
126
|
+
[1]: https://docs.anthropic.com/en/docs/claude-code/llm-gateway "LLM gateway configuration - Anthropic"
|
|
127
|
+
[2]: https://github.com/musistudio/claude-code-router "GitHub - musistudio/claude-code-router: Use Claude Code as the foundation for coding infrastructure, allowing you to decide how to interact with the model while enjoying updates from Anthropic."
|
|
128
|
+
[3]: https://www.npmjs.com/package/%40tellerlin/claude-code-router?activeTab=code "@tellerlin/claude-code-router - npm"
|
|
129
|
+
[4]: https://docs.litellm.ai/docs/anthropic_unified?utm_source=chatgpt.com "v1/messages"
|
|
130
|
+
[5]: https://docs.litellm.ai/docs/providers/openai_compatible?utm_source=chatgpt.com "OpenAI-Compatible Endpoints"
|
|
131
|
+
[6]: https://www.reddit.com/r/ChatGPTCoding/comments/1mcgm9s/psa_zaiglm45_is_absolutely_crushing_it_for_coding/?utm_source=chatgpt.com "zai/glm-4.5 is absolutely crushing it for coding - way better ..."
|
|
132
|
+
[7]: https://www.reddit.com/r/LocalLLaMA/comments/1mchsyd/tutorial_use_glm_45_or_any_llm_with_claude_code/?utm_source=chatgpt.com "[tutorial] Use GLM 4.5 (or any LLM) with Claude Code"
|
|
133
|
+
[8]: https://www.reddit.com/r/ClaudeAI/comments/1m3nyrn/who_is_using_claude_code_with_kimi_k2_thoughts/?utm_source=chatgpt.com "Who is using Claude Code with kimi k2? Thoughts? Tips?"
|
|
134
|
+
[9]: https://www.reddit.com/r/JanitorAI_Official/comments/1ju5vih/visual_guide_for_deepseek_users_via_chutesai_full/ "Visual Guide for DeepSeek Users (via Chutes.ai) – Full Credit to u/r3dux1337! : r/JanitorAI_Official"
|
|
135
|
+
[10]: https://www.youtube.com/watch?pp=0gcJCfwAo7VqN5tD&v=sAuCUAZnXAE&utm_source=chatgpt.com "Claude Code Router + Gemini 2.5 Pro FREE API: RIP Gemini ..."
|
|
136
|
+
[11]: https://www.youtube.com/watch?v=df-Fu2n7SLM&utm_source=chatgpt.com "The Claude Code HACK Anthropic Does Not Want You To Use"
|
|
137
|
+
[12]: https://gist.github.com/aquan9/58f4a77414a74703157bf79ea1bf009f?utm_source=chatgpt.com "Using litellm with chutes.ai from the bit-tensor chutes subnet."
|
|
138
|
+
[13]: https://www.reddit.com/r/LocalLLaMA/comments/1mf8la7/qwen3coder_is_bad_at_tool_call_while_glm45_is/?utm_source=chatgpt.com "Qwen3-Coder is bad at tool call while glm-4.5 is ..."
|
docs/reddit-post.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Made a tool that lets Claude Code control terminal apps - thought some of you might find it useful
|
|
2
|
+
|
|
3
|
+
Hey everyone! I've been using Claude Code a lot lately and kept running into the same frustration - it couldn't interact with CLI applications that need user input. So I built something to fix that.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
It's basically like Playwright/Puppeteer but for the terminal. Claude Code can now:
|
|
8
|
+
- Run interactive scripts and respond to prompts
|
|
9
|
+
- Use debuggers like pdb to step through code
|
|
10
|
+
- Launch and control other CLI apps
|
|
11
|
+
- Even spin up another Claude Code instance to have it work as a sub-agent --
|
|
12
|
+
and unlike the built-in sub-agents, you can clearly see what's going on.
|
|
13
|
+
|
|
14
|
+
[GIF PLACEHOLDER - Shows tmux-cli in action]
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
The magic happens through tmux (terminal multiplexer). I created a tool called `tmux-cli` that gives Claude Code the ability to:
|
|
19
|
+
- Launch apps in separate tmux panes
|
|
20
|
+
- Send keystrokes to them
|
|
21
|
+
- Capture their output
|
|
22
|
+
- Wait for them to idle
|
|
23
|
+
|
|
24
|
+
You don't need to know tmux commands - Claude Code handles everything. Just tell it what you want and it figures out the tmux stuff.
|
|
25
|
+
|
|
26
|
+
## Real use cases I've found helpful
|
|
27
|
+
|
|
28
|
+
- **Debugging**: Claude can now use pdb to step through Python code, examine variables, and help me understand program flow
|
|
29
|
+
- **Testing interactive scripts**: No more manually entering test inputs - Claude handles it
|
|
30
|
+
- **Spawn other Claudes**: I can have Claude launch another Claude instance to work on
|
|
31
|
+
a task and interact with it. And unlike the built-in sub-agents, it's fully visible.
|
|
32
|
+
|
|
33
|
+
## Getting it
|
|
34
|
+
|
|
35
|
+
If you want to try it:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Install from PyPI
|
|
39
|
+
uv tool install claude-code-tools
|
|
40
|
+
|
|
41
|
+
# Or get latest from GitHub
|
|
42
|
+
uv tool install git+https://github.com/pchalasani/claude-code-tools
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then add a snippet to your `~/.claude/CLAUDE.md` to let Claude Code know about tmux-cli.
|
|
46
|
+
|
|
47
|
+
The repo also includes some other tools like encrypted .env backup and a Claude session finder, but tmux-cli is the main thing I wanted to share.
|
|
48
|
+
|
|
49
|
+
## Not trying to oversell
|
|
50
|
+
|
|
51
|
+
This isn't revolutionary or anything - it's just a practical tool that solved a real problem I kept hitting. If you work with Claude Code and interactive CLIs, you might find it useful too.
|
|
52
|
+
|
|
53
|
+
Happy to answer questions or hear if anyone has similar tools they've built!
|
|
54
|
+
|
|
55
|
+
Repo: https://github.com/pchalasani/claude-code-tools
|
|
File without changes
|
|
File without changes
|