claude-code-tools 0.1.14__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.
- claude_code_tools/__init__.py +1 -1
- claude_code_tools/env_safe.py +235 -0
- claude_code_tools/tmux_cli_controller.py +20 -0
- claude_code_tools/tmux_remote_controller.py +193 -33
- {claude_code_tools-0.1.14.dist-info → claude_code_tools-0.1.18.dist-info}/METADATA +51 -10
- claude_code_tools-0.1.18.dist-info/RECORD +18 -0
- {claude_code_tools-0.1.14.dist-info → claude_code_tools-0.1.18.dist-info}/entry_points.txt +1 -0
- claude_code_tools-0.1.18.dist-info/licenses/LICENSE +21 -0
- docs/dot-zshrc.md +307 -0
- claude_code_tools-0.1.14.dist-info/RECORD +0 -15
- {claude_code_tools-0.1.14.dist-info → claude_code_tools-0.1.18.dist-info}/WHEEL +0 -0
claude_code_tools/__init__.py
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
env-safe: A safe way to inspect .env files without exposing sensitive values.
|
|
4
|
+
|
|
5
|
+
This tool allows Claude Code and other automated tools to:
|
|
6
|
+
- List environment variable keys without showing values
|
|
7
|
+
- Check if specific keys exist
|
|
8
|
+
- Count the number of variables defined
|
|
9
|
+
- Validate .env file syntax
|
|
10
|
+
|
|
11
|
+
It specifically avoids displaying actual values to prevent accidental exposure
|
|
12
|
+
of secrets, API keys, and other sensitive information.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Tuple, Optional
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_env_file(filepath: Path) -> List[Tuple[str, bool]]:
|
|
24
|
+
"""
|
|
25
|
+
Parse a .env file and extract variable names.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of tuples: (variable_name, has_value)
|
|
29
|
+
"""
|
|
30
|
+
if not filepath.exists():
|
|
31
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
32
|
+
|
|
33
|
+
variables = []
|
|
34
|
+
|
|
35
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
36
|
+
for line_num, line in enumerate(f, 1):
|
|
37
|
+
# Skip empty lines and comments
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if not line or line.startswith('#'):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# Match KEY=value pattern
|
|
43
|
+
match = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$', line)
|
|
44
|
+
if match:
|
|
45
|
+
key = match.group(1)
|
|
46
|
+
value = match.group(2).strip()
|
|
47
|
+
has_value = bool(value and value != '""' and value != "''")
|
|
48
|
+
variables.append((key, has_value))
|
|
49
|
+
elif '=' in line:
|
|
50
|
+
# Malformed line - has = but doesn't match pattern
|
|
51
|
+
print(f"Warning: Line {line_num} appears malformed: {line[:50]}...",
|
|
52
|
+
file=sys.stderr)
|
|
53
|
+
|
|
54
|
+
return variables
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def list_keys(filepath: Path, show_status: bool = False) -> None:
|
|
58
|
+
"""List all environment variable keys in the file."""
|
|
59
|
+
try:
|
|
60
|
+
variables = parse_env_file(filepath)
|
|
61
|
+
|
|
62
|
+
if not variables:
|
|
63
|
+
print("No environment variables found in file.")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if show_status:
|
|
67
|
+
print(f"{'KEY':<30} {'STATUS':<10}")
|
|
68
|
+
print("-" * 40)
|
|
69
|
+
for key, has_value in sorted(variables):
|
|
70
|
+
status = "defined" if has_value else "empty"
|
|
71
|
+
print(f"{key:<30} {status:<10}")
|
|
72
|
+
else:
|
|
73
|
+
for key, _ in sorted(variables):
|
|
74
|
+
print(key)
|
|
75
|
+
|
|
76
|
+
except FileNotFoundError as e:
|
|
77
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_key(filepath: Path, key_name: str) -> None:
|
|
85
|
+
"""Check if a specific key exists in the .env file."""
|
|
86
|
+
try:
|
|
87
|
+
variables = parse_env_file(filepath)
|
|
88
|
+
|
|
89
|
+
for key, has_value in variables:
|
|
90
|
+
if key == key_name:
|
|
91
|
+
if has_value:
|
|
92
|
+
print(f"✓ {key_name} is defined with a value")
|
|
93
|
+
else:
|
|
94
|
+
print(f"⚠ {key_name} is defined but empty")
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
print(f"✗ {key_name} is not defined")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
except FileNotFoundError as e:
|
|
101
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def count_variables(filepath: Path) -> None:
|
|
109
|
+
"""Count the number of variables defined."""
|
|
110
|
+
try:
|
|
111
|
+
variables = parse_env_file(filepath)
|
|
112
|
+
total = len(variables)
|
|
113
|
+
with_values = sum(1 for _, has_value in variables if has_value)
|
|
114
|
+
empty = total - with_values
|
|
115
|
+
|
|
116
|
+
print(f"Total variables: {total}")
|
|
117
|
+
if total > 0:
|
|
118
|
+
print(f" With values: {with_values}")
|
|
119
|
+
print(f" Empty: {empty}")
|
|
120
|
+
|
|
121
|
+
except FileNotFoundError as e:
|
|
122
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_syntax(filepath: Path) -> None:
|
|
130
|
+
"""Validate the syntax of the .env file."""
|
|
131
|
+
try:
|
|
132
|
+
if not filepath.exists():
|
|
133
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
issues = []
|
|
137
|
+
valid_lines = 0
|
|
138
|
+
|
|
139
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
140
|
+
for line_num, line in enumerate(f, 1):
|
|
141
|
+
original_line = line
|
|
142
|
+
line = line.strip()
|
|
143
|
+
|
|
144
|
+
# Skip empty lines and comments
|
|
145
|
+
if not line or line.startswith('#'):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Check for valid KEY=value pattern
|
|
149
|
+
if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*\s*=.*$', line):
|
|
150
|
+
if '=' in line:
|
|
151
|
+
issues.append(f"Line {line_num}: Invalid key format")
|
|
152
|
+
else:
|
|
153
|
+
issues.append(f"Line {line_num}: Missing '=' separator")
|
|
154
|
+
else:
|
|
155
|
+
valid_lines += 1
|
|
156
|
+
|
|
157
|
+
if issues:
|
|
158
|
+
print(f"✗ Found {len(issues)} syntax issue(s):")
|
|
159
|
+
for issue in issues[:10]: # Show first 10 issues
|
|
160
|
+
print(f" {issue}")
|
|
161
|
+
if len(issues) > 10:
|
|
162
|
+
print(f" ... and {len(issues) - 10} more")
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
else:
|
|
165
|
+
print(f"✓ Syntax valid ({valid_lines} variables defined)")
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
parser = argparse.ArgumentParser(
|
|
174
|
+
description="Safely inspect .env files without exposing sensitive values",
|
|
175
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
176
|
+
epilog="""
|
|
177
|
+
Examples:
|
|
178
|
+
env-safe list # List all keys
|
|
179
|
+
env-safe list --status # List keys with defined/empty status
|
|
180
|
+
env-safe check API_KEY # Check if API_KEY exists
|
|
181
|
+
env-safe count # Count variables
|
|
182
|
+
env-safe validate # Check syntax
|
|
183
|
+
env-safe list --file config.env # Use different file
|
|
184
|
+
|
|
185
|
+
This tool is designed to be safe for automated tools like Claude Code,
|
|
186
|
+
preventing accidental exposure of sensitive environment values.
|
|
187
|
+
"""
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
'--file', '-f',
|
|
192
|
+
default='.env',
|
|
193
|
+
help='Path to env file (default: .env)'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
|
197
|
+
|
|
198
|
+
# List command
|
|
199
|
+
list_parser = subparsers.add_parser('list', help='List all environment keys')
|
|
200
|
+
list_parser.add_argument(
|
|
201
|
+
'--status', '-s',
|
|
202
|
+
action='store_true',
|
|
203
|
+
help='Show whether each key has a value'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Check command
|
|
207
|
+
check_parser = subparsers.add_parser('check', help='Check if a key exists')
|
|
208
|
+
check_parser.add_argument('key', help='The key name to check')
|
|
209
|
+
|
|
210
|
+
# Count command
|
|
211
|
+
count_parser = subparsers.add_parser('count', help='Count variables')
|
|
212
|
+
|
|
213
|
+
# Validate command
|
|
214
|
+
validate_parser = subparsers.add_parser('validate', help='Validate syntax')
|
|
215
|
+
|
|
216
|
+
args = parser.parse_args()
|
|
217
|
+
|
|
218
|
+
if not args.command:
|
|
219
|
+
parser.print_help()
|
|
220
|
+
sys.exit(1)
|
|
221
|
+
|
|
222
|
+
filepath = Path(args.file)
|
|
223
|
+
|
|
224
|
+
if args.command == 'list':
|
|
225
|
+
list_keys(filepath, args.status)
|
|
226
|
+
elif args.command == 'check':
|
|
227
|
+
check_key(filepath, args.key)
|
|
228
|
+
elif args.command == 'count':
|
|
229
|
+
count_variables(filepath)
|
|
230
|
+
elif args.command == 'validate':
|
|
231
|
+
validate_syntax(filepath)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == '__main__':
|
|
235
|
+
main()
|
|
@@ -128,6 +128,19 @@ class TmuxCLIController:
|
|
|
128
128
|
output, code = self._run_tmux_command(['display-message', '-p', '#{pane_id}'])
|
|
129
129
|
return output if code == 0 else None
|
|
130
130
|
|
|
131
|
+
def get_current_window_id(self) -> Optional[str]:
|
|
132
|
+
"""Get the ID of the current tmux window."""
|
|
133
|
+
# Use TMUX_PANE environment variable to get the pane we're running in
|
|
134
|
+
import os
|
|
135
|
+
current_pane = os.environ.get('TMUX_PANE')
|
|
136
|
+
if current_pane:
|
|
137
|
+
# Get the window ID for this specific pane
|
|
138
|
+
output, code = self._run_tmux_command(['display-message', '-t', current_pane, '-p', '#{window_id}'])
|
|
139
|
+
return output if code == 0 else None
|
|
140
|
+
# Fallback to current window
|
|
141
|
+
output, code = self._run_tmux_command(['display-message', '-p', '#{window_id}'])
|
|
142
|
+
return output if code == 0 else None
|
|
143
|
+
|
|
131
144
|
def list_panes(self) -> List[Dict[str, str]]:
|
|
132
145
|
"""
|
|
133
146
|
List all panes in the current window.
|
|
@@ -175,8 +188,15 @@ class TmuxCLIController:
|
|
|
175
188
|
Returns:
|
|
176
189
|
Pane ID of the created pane
|
|
177
190
|
"""
|
|
191
|
+
# Get the current window ID to ensure pane is created in this window
|
|
192
|
+
current_window_id = self.get_current_window_id()
|
|
193
|
+
|
|
178
194
|
cmd = ['split-window']
|
|
179
195
|
|
|
196
|
+
# Target the specific window where tmux-cli was called from
|
|
197
|
+
if current_window_id:
|
|
198
|
+
cmd.extend(['-t', current_window_id])
|
|
199
|
+
|
|
180
200
|
if vertical:
|
|
181
201
|
cmd.append('-h')
|
|
182
202
|
else:
|
|
@@ -1,69 +1,229 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Remote Tmux Controller
|
|
4
|
-
|
|
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
|
-
|
|
12
|
+
import time
|
|
13
|
+
import hashlib
|
|
14
|
+
from typing import Optional, List, Dict, Tuple, Union
|
|
9
15
|
|
|
10
16
|
|
|
11
17
|
class RemoteTmuxController:
|
|
12
|
-
"""
|
|
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
|
-
|
|
18
|
-
print(f"
|
|
19
|
-
print(
|
|
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
|
-
"""
|
|
23
|
-
|
|
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
|
-
"""
|
|
27
|
-
|
|
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
|
-
"""
|
|
32
|
-
|
|
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
|
-
"""
|
|
36
|
-
|
|
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
|
-
"""
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
"""
|
|
65
|
-
|
|
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
|
-
"""
|
|
69
|
-
|
|
228
|
+
"""Resolve user-provided identifier to a tmux target string for remote ops."""
|
|
229
|
+
return self._window_target(pane)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-tools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.18
|
|
4
4
|
Summary: Collection of tools for working with Claude Code
|
|
5
|
+
License-File: LICENSE
|
|
5
6
|
Requires-Python: >=3.11
|
|
6
7
|
Requires-Dist: click>=8.0.0
|
|
7
8
|
Requires-Dist: fire>=0.5.0
|
|
@@ -62,6 +63,7 @@ This gives you:
|
|
|
62
63
|
- `tmux-cli` - The interactive CLI controller we just covered
|
|
63
64
|
- `find-claude-session` - Search and resume Claude Code sessions by keywords
|
|
64
65
|
- `vault` - Encrypted backup for your .env files
|
|
66
|
+
- `env-safe` - Safely inspect .env files without exposing values
|
|
65
67
|
|
|
66
68
|
## 🎮 tmux-cli Deep Dive
|
|
67
69
|
|
|
@@ -106,6 +108,12 @@ Example uses:
|
|
|
106
108
|
- Launch web apps and test them with browser automation MCP tools like Puppeteer
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
Incidentally, installing the Puppeteer MCP tool is easy:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
claude mcp add puppeteer -- npx -y @modelcontextprotocol/server-puppeteer
|
|
115
|
+
```
|
|
116
|
+
|
|
109
117
|
For detailed instructions, see [docs/tmux-cli-instructions.md](docs/tmux-cli-instructions.md).
|
|
110
118
|
|
|
111
119
|
## 🔍 find-claude-session
|
|
@@ -182,6 +190,31 @@ vault status # Check sync status for current project
|
|
|
182
190
|
|
|
183
191
|
For detailed documentation, see [docs/vault-documentation.md](docs/vault-documentation.md).
|
|
184
192
|
|
|
193
|
+
## 🔍 env-safe
|
|
194
|
+
|
|
195
|
+
Safely inspect .env files without exposing sensitive values. Designed for Claude Code and other automated tools that need to work with environment files without accidentally leaking secrets.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
env-safe list # List all environment variable keys
|
|
199
|
+
env-safe list --status # Show keys with defined/empty status
|
|
200
|
+
env-safe check API_KEY # Check if a specific key exists
|
|
201
|
+
env-safe count # Count total, defined, and empty variables
|
|
202
|
+
env-safe validate # Validate .env file syntax
|
|
203
|
+
env-safe --help # See all options
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Key Features
|
|
207
|
+
|
|
208
|
+
- **No Value Exposure** - Never displays actual environment values
|
|
209
|
+
- **Safe Inspection** - Check which keys exist without security risks
|
|
210
|
+
- **Syntax Validation** - Verify .env file format is correct
|
|
211
|
+
- **Status Checking** - See which variables are defined vs empty
|
|
212
|
+
- **Claude Code Integration** - Works with protection hooks to provide safe alternative
|
|
213
|
+
|
|
214
|
+
### Why env-safe?
|
|
215
|
+
|
|
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
|
+
|
|
185
218
|
## 🛡️ Claude Code Safety Hooks
|
|
186
219
|
|
|
187
220
|
This repository includes a comprehensive set of safety hooks that enhance Claude
|
|
@@ -193,6 +226,8 @@ Code's behavior and prevent dangerous operations.
|
|
|
193
226
|
pattern
|
|
194
227
|
- **Git Safety** - Prevents dangerous `git add -A`, unsafe checkouts, and
|
|
195
228
|
accidental data loss
|
|
229
|
+
- **Environment Security** - Blocks all .env file operations (read/write/edit),
|
|
230
|
+
suggests `env-safe` command for safe inspection
|
|
196
231
|
- **Context Management** - Blocks reading files >500 lines to prevent context
|
|
197
232
|
bloat
|
|
198
233
|
- **Command Enhancement** - Enforces ripgrep (`rg`) over grep for better
|
|
@@ -200,20 +235,26 @@ Code's behavior and prevent dangerous operations.
|
|
|
200
235
|
|
|
201
236
|
### Quick Setup
|
|
202
237
|
|
|
203
|
-
1. Copy the
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
252
|
```
|
|
213
253
|
|
|
214
254
|
### Available Hooks
|
|
215
255
|
|
|
216
256
|
- `bash_hook.py` - Comprehensive bash command safety checks
|
|
257
|
+
- `env_file_protection_hook.py` - Blocks all .env file operations
|
|
217
258
|
- `file_size_conditional_hook.py` - Prevents reading huge files
|
|
218
259
|
- `grep_block_hook.py` - Enforces ripgrep usage
|
|
219
260
|
- `notification_hook.sh` - Sends ntfy.sh notifications
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
claude_code_tools/__init__.py,sha256=l3LA3WuWQ4uiYiBmrFu5kJhLChWwGUFgAm6V_K3XBhU,90
|
|
2
|
+
claude_code_tools/dotenv_vault.py,sha256=KPI9NDFu5HE6FfhQUYw6RhdR-miN0ScJHsBg0OVG61k,9617
|
|
3
|
+
claude_code_tools/env_safe.py,sha256=TSSkOjEpzBwNgbeSR-0tR1-pAW_qmbZNmn3fiAsHJ4w,7659
|
|
4
|
+
claude_code_tools/find_claude_session.py,sha256=TfQWW2zMDJAnfLREt_P23BB6e9Qb-XS22SSEU80K-4Y,23524
|
|
5
|
+
claude_code_tools/tmux_cli_controller.py,sha256=Af9XPPfKxB8FsT-wIxJBzhUUqTo2IpnCH6XzJDTnRo0,28454
|
|
6
|
+
claude_code_tools/tmux_remote_controller.py,sha256=eY1ouLtUzJ40Ik4nqUBvc3Gl1Rx0_L4TFW4j708lgvI,9942
|
|
7
|
+
docs/claude-code-chutes.md,sha256=jCnYAAHZm32NGHE0CzGGl3vpO_zlF_xdmr23YxuCjPg,8098
|
|
8
|
+
docs/claude-code-tmux-tutorials.md,sha256=S-9U3a1AaPEBPo3oKpWuyOfKK7yPFOIu21P_LDfGUJk,7558
|
|
9
|
+
docs/dot-zshrc.md,sha256=DC2fOiGrUlIzol6N_47CW53a4BsnMEvCnhlRRVxFCTc,7160
|
|
10
|
+
docs/find-claude-session.md,sha256=fACbQP0Bj5jqIpNWk0lGDOQQaji-K9Va3gUv2RA47VQ,4284
|
|
11
|
+
docs/reddit-post.md,sha256=ZA7kPoJNi06t6F9JQMBiIOv039ADC9lM8YXFt8UA_Jg,2345
|
|
12
|
+
docs/tmux-cli-instructions.md,sha256=lQqKTI-uhH-EdU9P4To4GC10WJjj3VllAW4cxd8jfj8,4167
|
|
13
|
+
docs/vault-documentation.md,sha256=5XzNpHyhGU38JU2hKEWEL1gdPq3rC2zBg8yotK4eNF4,3600
|
|
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,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Prasad Chalasani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
docs/dot-zshrc.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Zsh Shell Setup Guide
|
|
2
|
+
|
|
3
|
+
This guide covers the essential setup for a productive Zsh environment with
|
|
4
|
+
Oh My Zsh, plugins, and Starship prompt.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
1. **Install Homebrew** (macOS package manager)
|
|
9
|
+
```bash
|
|
10
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. **Install Zsh** (usually pre-installed on macOS)
|
|
14
|
+
```bash
|
|
15
|
+
brew install zsh
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Oh My Zsh Installation
|
|
19
|
+
|
|
20
|
+
Install Oh My Zsh framework for managing Zsh configuration:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This creates `~/.oh-my-zsh` directory and a basic `~/.zshrc` file.
|
|
27
|
+
|
|
28
|
+
## Essential Plugins
|
|
29
|
+
|
|
30
|
+
### 1. Zsh Autosuggestions
|
|
31
|
+
|
|
32
|
+
Suggests commands as you type based on history and completions:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Zsh Syntax Highlighting
|
|
39
|
+
|
|
40
|
+
Provides syntax highlighting for the shell:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Configure Plugins in .zshrc
|
|
47
|
+
|
|
48
|
+
Add to your `~/.zshrc`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Oh My Zsh path
|
|
52
|
+
export ZSH="$HOME/.oh-my-zsh"
|
|
53
|
+
|
|
54
|
+
# Plugins configuration
|
|
55
|
+
plugins=(
|
|
56
|
+
git
|
|
57
|
+
zsh-autosuggestions
|
|
58
|
+
zsh-syntax-highlighting
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Source Oh My Zsh (commented out if using Starship)
|
|
62
|
+
# source $ZSH/oh-my-zsh.sh
|
|
63
|
+
|
|
64
|
+
# Source autosuggestions manually if needed
|
|
65
|
+
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Starship Prompt
|
|
69
|
+
|
|
70
|
+
Starship is a minimal, blazing-fast, and customizable prompt for any shell.
|
|
71
|
+
|
|
72
|
+
### Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -sS https://starship.rs/install.sh | sh
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Configuration
|
|
79
|
+
|
|
80
|
+
Add to the end of your `~/.zshrc`:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
eval "$(starship init zsh)"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Starship Configuration File
|
|
87
|
+
|
|
88
|
+
You can customize starship by creating/editing `~/.config/starship.toml`.
|
|
89
|
+
|
|
90
|
+
I recommend this `catppuccin` preset:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
starship preset catppuccin-powerline -o ~/.config/starship.toml
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
But if you want to tweak it, see the starship config docs.
|
|
97
|
+
https://starship.rs/guide/
|
|
98
|
+
|
|
99
|
+
## Enhanced Directory Listings with eza
|
|
100
|
+
|
|
101
|
+
Eza is a modern replacement for `ls` with colors, icons, and Git integration.
|
|
102
|
+
|
|
103
|
+
### Installation
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
brew install eza
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Configuration
|
|
110
|
+
|
|
111
|
+
Add these aliases to your `~/.zshrc` to replace default `ls` commands:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Replace ls with eza for beautiful directory listings
|
|
115
|
+
alias ls='eza --icons --group-directories-first'
|
|
116
|
+
alias ll='eza -l --icons --group-directories-first --header'
|
|
117
|
+
alias la='eza -la --icons --group-directories-first --header'
|
|
118
|
+
alias lt='eza --tree --icons --level=2'
|
|
119
|
+
alias ltd='eza --tree --icons --level=2 --only-dirs'
|
|
120
|
+
|
|
121
|
+
# Extended eza aliases
|
|
122
|
+
alias l='eza -lbF --git --icons' # list with git status
|
|
123
|
+
alias llm='eza -lbGd --git --sort=modified' # long list, modified date sort
|
|
124
|
+
alias lls='eza -lbhHigmuSa --time-style=long-iso --git --color-scale' # full details
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Features
|
|
128
|
+
|
|
129
|
+
- **Icons**: Shows file type icons (requires a Nerd Font)
|
|
130
|
+
- **Colors**: Color-codes files by type and permissions
|
|
131
|
+
- **Git Integration**: Shows git status in listings
|
|
132
|
+
- **Tree View**: Built-in tree view with `--tree` flag
|
|
133
|
+
- **Sorting**: Groups directories first by default
|
|
134
|
+
|
|
135
|
+
### Usage Examples
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
ls # Basic listing with icons
|
|
139
|
+
ll # Long format with details
|
|
140
|
+
la # Show hidden files
|
|
141
|
+
lt # Tree view (2 levels)
|
|
142
|
+
l # List with git status indicators
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Terminal Title Management
|
|
146
|
+
|
|
147
|
+
Add custom terminal title that shows current directory:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Disable auto-setting terminal title
|
|
151
|
+
DISABLE_AUTO_TITLE="true"
|
|
152
|
+
|
|
153
|
+
# Set terminal title to current directory
|
|
154
|
+
function precmd () {
|
|
155
|
+
echo -ne "\033]0;$(print -rD $PWD)\007"
|
|
156
|
+
}
|
|
157
|
+
precmd
|
|
158
|
+
|
|
159
|
+
# Show command being executed in title with rocket emojis
|
|
160
|
+
function preexec () {
|
|
161
|
+
print -Pn "\e]0;🚀 $(print -rD $PWD) $1 🚀\a"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## History Search Enhancement
|
|
166
|
+
|
|
167
|
+
### Option 1: Atuin (Modern Shell History)
|
|
168
|
+
|
|
169
|
+
Install Atuin for enhanced history search:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
brew install atuin
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Configure in `~/.zshrc`:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
export ATUIN_NOBIND="true"
|
|
179
|
+
eval "$(atuin init zsh)"
|
|
180
|
+
# Bind to up arrow keys (depends on terminal)
|
|
181
|
+
bindkey '^[[A' _atuin_search_widget
|
|
182
|
+
bindkey '^[OA' _atuin_search_widget
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Option 2: HSTR (Simple History Search)
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
brew install hstr
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Configure in `~/.zshrc`:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
alias hh=hstr
|
|
195
|
+
setopt histignorespace # skip cmds w/ leading space from history
|
|
196
|
+
export HSTR_CONFIG=hicolor # get more colors
|
|
197
|
+
bindkey -s "\C-r" "\C-a hstr -- \C-j" # bind hstr to Ctrl-r
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Useful Aliases
|
|
201
|
+
|
|
202
|
+
Add these productivity aliases to your `~/.zshrc`:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Git aliases
|
|
206
|
+
alias gsuno="git status -uno" # git status without untracked files
|
|
207
|
+
alias gspu="git stash push -m" # stash with message
|
|
208
|
+
alias gspop="git stash pop" # pop stash
|
|
209
|
+
alias gsl="git stash list" # list stashes
|
|
210
|
+
|
|
211
|
+
# Quick git commit amend and push
|
|
212
|
+
function gcpq() {
|
|
213
|
+
ga -u
|
|
214
|
+
git commit --amend --no-edit
|
|
215
|
+
git push origin main --force-with-lease
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Tmux aliases
|
|
219
|
+
alias tmnew="tmux new -s "
|
|
220
|
+
alias tmls="tmux ls"
|
|
221
|
+
alias tma="tmux a -t "
|
|
222
|
+
alias tmk="tmux kill-session -t "
|
|
223
|
+
alias tmd="tmux detach"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Completion System
|
|
227
|
+
|
|
228
|
+
Enable advanced tab completion:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
autoload -Uz compinit
|
|
232
|
+
zstyle ':completion:*' menu select
|
|
233
|
+
fpath+=~/.zfunc
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Environment Variables
|
|
237
|
+
|
|
238
|
+
Set common environment variables:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
export EDITOR="nano" # or "vim", "code", etc.
|
|
242
|
+
export TERM=xterm-256color
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Final .zshrc Structure
|
|
246
|
+
|
|
247
|
+
Your `~/.zshrc` should follow this general structure:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# 1. Oh My Zsh configuration
|
|
251
|
+
export ZSH="$HOME/.oh-my-zsh"
|
|
252
|
+
plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
|
|
253
|
+
|
|
254
|
+
# 2. Source additional files
|
|
255
|
+
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
|
|
256
|
+
|
|
257
|
+
# 3. Environment variables
|
|
258
|
+
export EDITOR="nano"
|
|
259
|
+
export TERM=xterm-256color
|
|
260
|
+
|
|
261
|
+
# 4. Aliases and functions
|
|
262
|
+
alias gsuno="git status -uno"
|
|
263
|
+
# ... more aliases
|
|
264
|
+
|
|
265
|
+
# 5. Terminal title customization
|
|
266
|
+
DISABLE_AUTO_TITLE="true"
|
|
267
|
+
function precmd () { ... }
|
|
268
|
+
function preexec () { ... }
|
|
269
|
+
|
|
270
|
+
# 6. History search tool (Atuin or HSTR)
|
|
271
|
+
eval "$(atuin init zsh)"
|
|
272
|
+
|
|
273
|
+
# 7. Completion system
|
|
274
|
+
autoload -Uz compinit
|
|
275
|
+
zstyle ':completion:*' menu select
|
|
276
|
+
|
|
277
|
+
# 8. Starship prompt (at the very end)
|
|
278
|
+
eval "$(starship init zsh)"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Verification
|
|
282
|
+
|
|
283
|
+
After setup, verify everything works:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# Reload shell configuration
|
|
287
|
+
source ~/.zshrc
|
|
288
|
+
|
|
289
|
+
# Test autosuggestions (type a partial command and see suggestions)
|
|
290
|
+
# Test syntax highlighting (commands should be colored)
|
|
291
|
+
# Test Starship prompt (should see a styled prompt)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Troubleshooting
|
|
295
|
+
|
|
296
|
+
1. **Slow shell startup**: Comment out unused plugins and features
|
|
297
|
+
2. **Autosuggestions not working**: Ensure the plugin is properly cloned and
|
|
298
|
+
sourced
|
|
299
|
+
3. **Starship not showing**: Make sure it's the last line in `.zshrc`
|
|
300
|
+
4. **Terminal title not updating**: Check if `DISABLE_AUTO_TITLE="true"` is set
|
|
301
|
+
|
|
302
|
+
## Additional Resources
|
|
303
|
+
|
|
304
|
+
- [Oh My Zsh Documentation](https://github.com/ohmyzsh/ohmyzsh/wiki)
|
|
305
|
+
- [Starship Documentation](https://starship.rs/config/)
|
|
306
|
+
- [Zsh Autosuggestions](https://github.com/zsh-users/zsh-autosuggestions)
|
|
307
|
+
- [Atuin Documentation](https://github.com/atuinsh/atuin)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
claude_code_tools/__init__.py,sha256=_-QQFZ18-bFEW2GEAgme8FhI4Ejyk7Fxq8o0zPNtT9U,90
|
|
2
|
-
claude_code_tools/dotenv_vault.py,sha256=KPI9NDFu5HE6FfhQUYw6RhdR-miN0ScJHsBg0OVG61k,9617
|
|
3
|
-
claude_code_tools/find_claude_session.py,sha256=TfQWW2zMDJAnfLREt_P23BB6e9Qb-XS22SSEU80K-4Y,23524
|
|
4
|
-
claude_code_tools/tmux_cli_controller.py,sha256=0PqWBhITw74TtOKAfiUJMyHEFVOlLp4sEYmSlDHyznc,27482
|
|
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
|
|
7
|
-
docs/claude-code-tmux-tutorials.md,sha256=S-9U3a1AaPEBPo3oKpWuyOfKK7yPFOIu21P_LDfGUJk,7558
|
|
8
|
-
docs/find-claude-session.md,sha256=fACbQP0Bj5jqIpNWk0lGDOQQaji-K9Va3gUv2RA47VQ,4284
|
|
9
|
-
docs/reddit-post.md,sha256=ZA7kPoJNi06t6F9JQMBiIOv039ADC9lM8YXFt8UA_Jg,2345
|
|
10
|
-
docs/tmux-cli-instructions.md,sha256=lQqKTI-uhH-EdU9P4To4GC10WJjj3VllAW4cxd8jfj8,4167
|
|
11
|
-
docs/vault-documentation.md,sha256=5XzNpHyhGU38JU2hKEWEL1gdPq3rC2zBg8yotK4eNF4,3600
|
|
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,,
|
|
File without changes
|