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.

@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "0.1.12"
3
+ __version__ = "0.1.14"
@@ -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 = Path.home() / ".claude" / "projects" / project_path
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
- projects_dir = Path.home() / ".claude" / "projects"
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 count lines.
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 (ValueError, EOFError):
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.12
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
  ![tmux-cli demo](demos/tmux-cli-demo-short.gif)
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 CC to launch another Claude Code instance for code review - not as a
29
- hidden sub-agent, but as a visible session you can monitor (as shown in gif above).
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 all the command-line tools
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() { eval "$(find-claude-session --shell "$@" | sed '/^$/d')"; }
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
+ ![fcs.png](docs/fcs.png)
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=5y73ossjGhW7mbIf6P5lH6vZCjSHBixWmUA09OTZUQI,90
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=k7K-lwHY2aEWEgUw92T_NT3ds6cGql9eq1CKQikig68,21295
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.12.dist-info/METADATA,sha256=OB9c9watxAHAekzai5JteE9epGn3NK1nI5S6aMmn8KA,9767
11
- claude_code_tools-0.1.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- claude_code_tools-0.1.12.dist-info/entry_points.txt,sha256=yUTZlZ2jteoUZ9bGPvZKI9tjmohPDhRk1fovu5pZACM,181
13
- claude_code_tools-0.1.12.dist-info/RECORD,,
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