claude-code-tools 0.1.17__py3-none-any.whl → 0.1.18__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.17"
3
+ __version__ = "0.1.18"
@@ -1,69 +1,229 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Remote Tmux Controller - Stub implementation
4
- This is a minimal stub to prevent import errors when tmux-cli is used outside tmux.
3
+ Remote Tmux Controller
4
+
5
+ Enables tmux-cli to work when run outside of tmux by:
6
+ - Auto-creating a detached tmux session on first use
7
+ - Managing commands in separate tmux windows (not panes)
8
+ - Providing an API compatible with the local (pane) controller
5
9
  """
6
10
 
7
11
  import subprocess
8
- from typing import Optional, List, Dict
12
+ import time
13
+ import hashlib
14
+ from typing import Optional, List, Dict, Tuple, Union
9
15
 
10
16
 
11
17
  class RemoteTmuxController:
12
- """Stub implementation of RemoteTmuxController to prevent import errors."""
18
+ """Remote controller that manages a dedicated tmux session and windows."""
13
19
 
14
20
  def __init__(self, session_name: str = "remote-cli-session"):
15
- """Initialize with session name."""
21
+ """Initialize with session name and ensure the session exists."""
16
22
  self.session_name = session_name
17
- print(f"Warning: RemoteTmuxController is not fully implemented.")
18
- print(f"Remote mode functionality is currently unavailable.")
19
- print(f"Please use tmux-cli from inside a tmux session for full functionality.")
23
+ self.target_window: Optional[str] = None # e.g., "session:0" (active pane in that window)
24
+ print(f"Note: tmux-cli is running outside tmux. Managing windows in session '{session_name}'.")
25
+ print("For better integration, consider running from inside a tmux session.")
26
+ print("Use 'tmux-cli attach' to view the remote session.")
27
+ self._ensure_session()
28
+
29
+ # ----------------------------
30
+ # Internal utilities
31
+ # ----------------------------
32
+ def _run_tmux(self, args: List[str]) -> Tuple[str, int]:
33
+ result = subprocess.run(
34
+ ['tmux'] + args,
35
+ capture_output=True,
36
+ text=True
37
+ )
38
+ return result.stdout.strip(), result.returncode
39
+
40
+ def _ensure_session(self) -> None:
41
+ """Create the session if it doesn't exist (detached)."""
42
+ _, code = self._run_tmux(['has-session', '-t', self.session_name])
43
+ if code != 0:
44
+ # Create a detached session using user's default shell
45
+ # Return the session name just to force creation
46
+ self._run_tmux([
47
+ 'new-session', '-d', '-s', self.session_name, '-P', '-F', '#{session_name}'
48
+ ])
49
+ # Remember first window as default target
50
+ self.target_window = f"{self.session_name}:0"
51
+ else:
52
+ # If already exists and we don't have a target, set to active window
53
+ if not self.target_window:
54
+ win, code2 = self._run_tmux(['display-message', '-p', '-t', self.session_name, '#{session_name}:#{window_index}'])
55
+ if code2 == 0 and win:
56
+ self.target_window = win
57
+
58
+ def _window_target(self, pane: Optional[str]) -> str:
59
+ """Resolve user-provided pane/window hint to a tmux target.
60
+ Accepts:
61
+ - None -> use last target window if set else active window in session
62
+ - digits (e.g., "1") -> session:index
63
+ - full tmux target (e.g., "name:1" or "name:1.0" or "%12") -> pass-through
64
+ """
65
+ self._ensure_session()
66
+ if pane is None:
67
+ if self.target_window:
68
+ return self.target_window
69
+ # Fallback to active window in session
70
+ win, code = self._run_tmux(['display-message', '-p', '-t', self.session_name, '#{session_name}:#{window_index}'])
71
+ if code == 0 and win:
72
+ self.target_window = win
73
+ return win
74
+ # Final fallback: session:0
75
+ return f"{self.session_name}:0"
76
+ # If user supplied a simple index
77
+ if isinstance(pane, str) and pane.isdigit():
78
+ return f"{self.session_name}:{pane}"
79
+ # Otherwise assume user provided a pane/window target or pane id
80
+ return pane
81
+
82
+ def _active_pane_in_window(self, window_target: str) -> str:
83
+ """Return a target that tmux can use to address the active pane of a window.
84
+ For tmux commands that accept pane targets, a window target resolves to its
85
+ active pane, so we can pass the window target directly.
86
+ Still, normalize to make intent clear.
87
+ """
88
+ return window_target
20
89
 
21
90
  def list_panes(self) -> List[Dict[str, str]]:
22
- """Return empty list."""
23
- return []
91
+ """In remote mode, list windows in the managed session.
92
+ Returns a list shaped similarly to local list_panes, with keys:
93
+ id (window target), index, title (window name), active (bool), size (N/A)
94
+ """
95
+ self._ensure_session()
96
+ out, code = self._run_tmux([
97
+ 'list-windows', '-t', self.session_name,
98
+ '-F', '#{window_index}|#{window_name}|#{window_active}|#{window_width}x#{window_height}'
99
+ ])
100
+ if code != 0 or not out:
101
+ return []
102
+ windows: List[Dict[str, str]] = []
103
+ for line in out.split('\n'):
104
+ if not line:
105
+ continue
106
+ idx, name, active, size = line.split('|')
107
+ windows.append({
108
+ 'id': f"{self.session_name}:{idx}",
109
+ 'index': idx,
110
+ 'title': name,
111
+ 'active': active == '1',
112
+ 'size': size
113
+ })
114
+ return windows
24
115
 
25
116
  def launch_cli(self, command: str, name: Optional[str] = None) -> Optional[str]:
26
- """Not implemented."""
27
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
117
+ """Launch a command in a new window within the managed session.
118
+ Returns the window target (e.g., "session:1").
119
+ """
120
+ self._ensure_session()
121
+ args = ['new-window', '-t', self.session_name, '-P', '-F', '#{session_name}:#{window_index}']
122
+ if name:
123
+ args.extend(['-n', name])
124
+ if command:
125
+ args.append(command)
126
+ out, code = self._run_tmux(args)
127
+ if code == 0 and out:
128
+ self.target_window = out
129
+ return out
130
+ return None
28
131
 
29
132
  def send_keys(self, text: str, pane_id: Optional[str] = None, enter: bool = True,
30
- delay_enter: bool = True):
31
- """Not implemented."""
32
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
133
+ delay_enter: Union[bool, float] = True):
134
+ """Send keys to the active pane of a given window (or last target)."""
135
+ if not text:
136
+ return
137
+ target = self._active_pane_in_window(self._window_target(pane_id))
138
+ if enter and delay_enter:
139
+ # First send text (no Enter)
140
+ self._run_tmux(['send-keys', '-t', target, text])
141
+ # Delay
142
+ delay = 1.0 if isinstance(delay_enter, bool) else float(delay_enter)
143
+ time.sleep(delay)
144
+ # Then Enter
145
+ self._run_tmux(['send-keys', '-t', target, 'Enter'])
146
+ else:
147
+ args = ['send-keys', '-t', target, text]
148
+ if enter:
149
+ args.append('Enter')
150
+ self._run_tmux(args)
33
151
 
34
152
  def capture_pane(self, pane_id: Optional[str] = None, lines: Optional[int] = None) -> str:
35
- """Not implemented."""
36
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
153
+ """Capture output from the active pane of a window."""
154
+ target = self._active_pane_in_window(self._window_target(pane_id))
155
+ args = ['capture-pane', '-t', target, '-p']
156
+ if lines:
157
+ args.extend(['-S', f'-{lines}'])
158
+ out, _ = self._run_tmux(args)
159
+ return out
37
160
 
38
161
  def wait_for_idle(self, pane_id: Optional[str] = None, idle_time: float = 2.0,
39
162
  check_interval: float = 0.5, timeout: Optional[int] = None) -> bool:
40
- """Not implemented."""
41
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
163
+ """Wait until captured output is unchanged for idle_time seconds."""
164
+ target = self._active_pane_in_window(self._window_target(pane_id))
165
+ start_time = time.time()
166
+ last_change = time.time()
167
+ last_hash = ""
168
+ while True:
169
+ if timeout is not None and (time.time() - start_time) > timeout:
170
+ return False
171
+ content, _ = self._run_tmux(['capture-pane', '-t', target, '-p'])
172
+ h = hashlib.md5(content.encode()).hexdigest()
173
+ if h != last_hash:
174
+ last_hash = h
175
+ last_change = time.time()
176
+ else:
177
+ if (time.time() - last_change) >= idle_time:
178
+ return True
179
+ time.sleep(check_interval)
42
180
 
43
181
  def send_interrupt(self, pane_id: Optional[str] = None):
44
- """Not implemented."""
45
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
182
+ target = self._active_pane_in_window(self._window_target(pane_id))
183
+ self._run_tmux(['send-keys', '-t', target, 'C-c'])
46
184
 
47
185
  def send_escape(self, pane_id: Optional[str] = None):
48
- """Not implemented."""
49
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
186
+ target = self._active_pane_in_window(self._window_target(pane_id))
187
+ self._run_tmux(['send-keys', '-t', target, 'Escape'])
50
188
 
51
189
  def kill_window(self, window_id: Optional[str] = None):
52
- """Not implemented."""
53
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
190
+ target = self._window_target(window_id)
191
+ # Ensure the target refers to a window (not a %pane id)
192
+ # If user passed a pane id like %12, tmux can still resolve to its window
193
+ self._run_tmux(['kill-window', '-t', target])
194
+ if self.target_window == target:
195
+ self.target_window = None
54
196
 
55
197
  def attach_session(self):
56
- """Not implemented."""
57
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
198
+ self._ensure_session()
199
+ # Attach will replace the current terminal view until the user detaches
200
+ subprocess.run(['tmux', 'attach-session', '-t', self.session_name])
58
201
 
59
202
  def cleanup_session(self):
60
- """Not implemented."""
61
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
203
+ self._run_tmux(['kill-session', '-t', self.session_name])
204
+ self.target_window = None
62
205
 
63
206
  def list_windows(self) -> List[Dict[str, str]]:
64
- """Not implemented."""
65
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
207
+ """List all windows in the managed session with basic info."""
208
+ self._ensure_session()
209
+ out, code = self._run_tmux(['list-windows', '-t', self.session_name, '-F', '#{window_index}|#{window_name}|#{window_active}'])
210
+ if code != 0 or not out:
211
+ return []
212
+ windows: List[Dict[str, str]] = []
213
+ for line in out.split('\n'):
214
+ if not line:
215
+ continue
216
+ idx, name, active = line.split('|')
217
+ # Try to get active pane id for each window (best effort)
218
+ pane_out, _ = self._run_tmux(['display-message', '-p', '-t', f'{self.session_name}:{idx}', '#{pane_id}'])
219
+ windows.append({
220
+ 'index': idx,
221
+ 'name': name,
222
+ 'active': active == '1',
223
+ 'pane_id': pane_out or ''
224
+ })
225
+ return windows
66
226
 
67
227
  def _resolve_pane_id(self, pane: Optional[str]) -> Optional[str]:
68
- """Not implemented."""
69
- raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
228
+ """Resolve user-provided identifier to a tmux target string for remote ops."""
229
+ return self._window_target(pane)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 0.1.17
3
+ Version: 0.1.18
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -213,7 +213,7 @@ env-safe --help # See all options
213
213
 
214
214
  ### Why env-safe?
215
215
 
216
- When Claude Code attempts to read .env files directly (via cat, grep, etc.), safety hooks block the operation to prevent accidental exposure of API keys and secrets. The `env-safe` command provides a secure alternative that lets Claude Code inspect environment configuration without security risks.
216
+ Claude Code is completely blocked from directly accessing .env files - no reading, writing, or editing allowed. This prevents both accidental exposure of API keys and unintended modifications. The `env-safe` command provides the only approved way for Claude Code to inspect environment configuration safely, while any modifications must be done manually outside of Claude Code.
217
217
 
218
218
  ## 🛡️ Claude Code Safety Hooks
219
219
 
@@ -226,8 +226,8 @@ Code's behavior and prevent dangerous operations.
226
226
  pattern
227
227
  - **Git Safety** - Prevents dangerous `git add -A`, unsafe checkouts, and
228
228
  accidental data loss
229
- - **Environment Security** - Blocks direct .env file access, suggests `env-safe`
230
- command instead
229
+ - **Environment Security** - Blocks all .env file operations (read/write/edit),
230
+ suggests `env-safe` command for safe inspection
231
231
  - **Context Management** - Blocks reading files >500 lines to prevent context
232
232
  bloat
233
233
  - **Command Enhancement** - Enforces ripgrep (`rg`) over grep for better
@@ -235,20 +235,26 @@ Code's behavior and prevent dangerous operations.
235
235
 
236
236
  ### Quick Setup
237
237
 
238
- 1. Copy the sample hooks configuration:
239
- ```bash
240
- cp hooks/settings.sample.json hooks/settings.json
241
- export CLAUDE_CODE_TOOLS_PATH=/path/to/claude-code-tools
242
- ```
243
-
244
- 2. Reference in your Claude Code settings or use `--hooks` flag:
245
- ```bash
246
- claude --hooks /path/to/hooks/settings.json
238
+ 1. Copy the hooks configuration from `hooks/settings.sample.json`
239
+
240
+ 2. Add the hooks to your global Claude settings at `~/.claude/settings.json`:
241
+ - If the file doesn't exist, create it
242
+ - Copy the "hooks" section from settings.sample.json
243
+ - Replace `/path/to/claude-code-tools` with your actual path to this repository
244
+
245
+ Example ~/.claude/settings.json:
246
+ ```json
247
+ {
248
+ "hooks": {
249
+ // ... hooks configuration from settings.sample.json ...
250
+ }
251
+ }
247
252
  ```
248
253
 
249
254
  ### Available Hooks
250
255
 
251
256
  - `bash_hook.py` - Comprehensive bash command safety checks
257
+ - `env_file_protection_hook.py` - Blocks all .env file operations
252
258
  - `file_size_conditional_hook.py` - Prevents reading huge files
253
259
  - `grep_block_hook.py` - Enforces ripgrep usage
254
260
  - `notification_hook.sh` - Sends ntfy.sh notifications
@@ -1,9 +1,9 @@
1
- claude_code_tools/__init__.py,sha256=jq4ZpdUjyb15CwnvQCfrpZGCdByRKWiuSxc7ERjTOG4,90
1
+ claude_code_tools/__init__.py,sha256=l3LA3WuWQ4uiYiBmrFu5kJhLChWwGUFgAm6V_K3XBhU,90
2
2
  claude_code_tools/dotenv_vault.py,sha256=KPI9NDFu5HE6FfhQUYw6RhdR-miN0ScJHsBg0OVG61k,9617
3
3
  claude_code_tools/env_safe.py,sha256=TSSkOjEpzBwNgbeSR-0tR1-pAW_qmbZNmn3fiAsHJ4w,7659
4
4
  claude_code_tools/find_claude_session.py,sha256=TfQWW2zMDJAnfLREt_P23BB6e9Qb-XS22SSEU80K-4Y,23524
5
5
  claude_code_tools/tmux_cli_controller.py,sha256=Af9XPPfKxB8FsT-wIxJBzhUUqTo2IpnCH6XzJDTnRo0,28454
6
- claude_code_tools/tmux_remote_controller.py,sha256=uK9lJKrNz7_NeV1_V3BM-q0r6sRVmYfOR8H3zo5hfH8,3220
6
+ claude_code_tools/tmux_remote_controller.py,sha256=eY1ouLtUzJ40Ik4nqUBvc3Gl1Rx0_L4TFW4j708lgvI,9942
7
7
  docs/claude-code-chutes.md,sha256=jCnYAAHZm32NGHE0CzGGl3vpO_zlF_xdmr23YxuCjPg,8098
8
8
  docs/claude-code-tmux-tutorials.md,sha256=S-9U3a1AaPEBPo3oKpWuyOfKK7yPFOIu21P_LDfGUJk,7558
9
9
  docs/dot-zshrc.md,sha256=DC2fOiGrUlIzol6N_47CW53a4BsnMEvCnhlRRVxFCTc,7160
@@ -11,8 +11,8 @@ docs/find-claude-session.md,sha256=fACbQP0Bj5jqIpNWk0lGDOQQaji-K9Va3gUv2RA47VQ,4
11
11
  docs/reddit-post.md,sha256=ZA7kPoJNi06t6F9JQMBiIOv039ADC9lM8YXFt8UA_Jg,2345
12
12
  docs/tmux-cli-instructions.md,sha256=lQqKTI-uhH-EdU9P4To4GC10WJjj3VllAW4cxd8jfj8,4167
13
13
  docs/vault-documentation.md,sha256=5XzNpHyhGU38JU2hKEWEL1gdPq3rC2zBg8yotK4eNF4,3600
14
- claude_code_tools-0.1.17.dist-info/METADATA,sha256=j26z4BD4451XoUZIpSn4e7CNVYbkHxcZCQa5IC-pkQk,11954
15
- claude_code_tools-0.1.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- claude_code_tools-0.1.17.dist-info/entry_points.txt,sha256=AdJXTNrrAbUp0EhSRQuA0IBjFLBUXdqh7nuBYVFEAig,224
17
- claude_code_tools-0.1.17.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
18
- claude_code_tools-0.1.17.dist-info/RECORD,,
14
+ claude_code_tools-0.1.18.dist-info/METADATA,sha256=NLlm1gDQL74h7IXXtvBqVX8TC6GBuTL4wydxt8SKsO4,12310
15
+ claude_code_tools-0.1.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ claude_code_tools-0.1.18.dist-info/entry_points.txt,sha256=AdJXTNrrAbUp0EhSRQuA0IBjFLBUXdqh7nuBYVFEAig,224
17
+ claude_code_tools-0.1.18.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
18
+ claude_code_tools-0.1.18.dist-info/RECORD,,