-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcli.py
More file actions
271 lines (214 loc) · 8.49 KB
/
cli.py
File metadata and controls
271 lines (214 loc) · 8.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#!/usr/bin/env python3
"""
CCNotify CLI - Simplified single-command installer for ccnotify
"""
import argparse
import sys
import warnings
from rich.console import Console
# Suppress pydub's regex warnings
warnings.filterwarnings("ignore", message=r".*invalid escape sequence.*", category=SyntaxWarning)
from .installer import InstallationDetector, FirstTimeFlow, UpdateFlow
from .installer.welcome import display_error_message
from .version import get_package_version
console = Console()
def main():
"""Main CLI entry point - simplified single install command"""
parser = argparse.ArgumentParser(
description="CCNotify - Voice Notification System for Claude Code",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
uvx ccnotify install # Install or update CCNotify intelligently
uvx ccnotify install --force # Force complete reinstallation
uvx ccnotify install --config-only # Only update configuration
uvx ccnotify install --quiet # Minimal output mode
""",
)
# Simplified argument structure
parser.add_argument(
"command",
nargs="?",
default="install",
choices=["install"],
help="Command to execute (default: install)",
)
parser.add_argument("--force", action="store_true", help="Force complete reinstallation")
parser.add_argument(
"--config-only", action="store_true", help="Only update configuration, skip script updates"
)
parser.add_argument("--quiet", action="store_true", help="Minimal output mode")
parser.add_argument(
"--logging", action="store_true", help="Enable logging to file (off by default)"
)
parser.add_argument("--version", action="version", version=f"CCNotify {get_package_version()}")
args = parser.parse_args()
# Always execute install command with intelligent detection
success = execute_install_command(args.force, args.config_only, args.quiet, args.logging)
if not success:
sys.exit(1)
def execute_install_command(
force: bool = False, config_only: bool = False, quiet: bool = False, logging: bool = False
) -> bool:
"""Execute the intelligent install command with detection logic."""
# Validate parameters
if not isinstance(logging, bool):
raise TypeError("logging parameter must be a boolean")
try:
# Detect existing installation
detector = InstallationDetector()
status = detector.check_existing_installation()
if status.exists and not force:
# Existing installation - run update flow
update_flow = UpdateFlow()
return update_flow.run(config_only=config_only, quiet=quiet, logging=logging)
else:
# No installation or force requested - run first-time flow
first_time_flow = FirstTimeFlow()
return first_time_flow.run(force=force, quiet=quiet, logging=logging)
except KeyboardInterrupt:
if not quiet:
display_error_message("Installation cancelled by user")
return False
except Exception as e:
if not quiet:
display_error_message("Installation failed", str(e))
return False
def get_notify_template() -> str:
"""Get the ccnotify.py template content for legacy compatibility."""
# Import the actual notify.py content and embed version
from .notify import main as notify_main
from .version import get_package_version
import inspect
# Get the complete notify.py file content
from pathlib import Path
notify_file = Path(__file__).parent / "notify.py"
if notify_file.exists():
content = notify_file.read_text()
# Embed version in the generated script
from .version import embed_version_in_script
return embed_version_in_script(content, get_package_version())
# Fallback minimal template
version = get_package_version()
return f'''#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "pync",
# "requests",
# "kokoro-onnx",
# "pydub",
# "soundfile",
# "tqdm",
# "rich"
# ]
# ///
"""
CCNotify - Voice Notification System for Claude Code
Generated by ccnotify installer v{version}
"""
__version__ = "{version}"
# Minimal notification handler
import sys
import json
from pathlib import Path
def load_config():
config_file = Path.home() / ".claude" / "ccnotify" / "config.json"
if config_file.exists():
try:
with open(config_file, 'r') as f:
return json.load(f)
except Exception:
pass
return {"tts_provider": "none"}
def main():
config = load_config()
message = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Claude Code notification"
try:
if sys.platform == "darwin":
import pync
pync.notify(message, title="Claude Code")
else:
print(f"Notification: {message}")
except ImportError:
print(f"Notification: {message}")
if __name__ == "__main__":
main()
'''
def update_claude_settings(script_path: str, logging: bool = False) -> bool:
"""Update Claude settings.json to configure ccnotify hooks."""
import json
import shutil
from pathlib import Path
claude_dir = Path.home() / ".claude"
settings_file = claude_dir / "settings.json"
try:
if settings_file.exists():
# Create backup first
backup_file = settings_file.with_suffix(".json.ccnotify.bak")
shutil.copy2(settings_file, backup_file)
with open(settings_file, "r") as f:
settings = json.load(f)
else:
settings = {}
# Add our hook configuration
if "hooks" not in settings:
settings["hooks"] = {}
# Configure ccnotify hook for relevant events
# Add --logging flag to command if logging is enabled
command = f"uv run {script_path}"
if logging:
command += " --logging"
hook_config = {"type": "command", "command": command}
events_to_hook = ["PreToolUse", "PostToolUse", "Stop", "SubagentStop", "Notification"]
hooks_added = False
for event in events_to_hook:
if event not in settings["hooks"]:
settings["hooks"][event] = []
# Check if our hook is already configured and update if needed
# Hook structure: {"matcher": ".*", "hooks": [{"type": "command", "command": "..."}]}
hook_updated = False
hook_exists = False
for i, entry in enumerate(settings["hooks"][event]):
if not isinstance(entry, dict):
continue
hooks_list = entry.get("hooks", [])
if not isinstance(hooks_list, list):
continue
for j, hook in enumerate(hooks_list):
if not isinstance(hook, dict):
continue
existing_command = hook.get("command", "")
# Check if this is our ccnotify hook
if "ccnotify.py" in existing_command or str(script_path) in existing_command:
hook_exists = True
# Update the command if it's different (e.g., logging flag changed)
if existing_command != command:
try:
settings["hooks"][event][i]["hooks"][j]["command"] = command
hook_updated = True
hooks_added = True
except (KeyError, IndexError) as e:
# Log error but continue processing
print(
f"Warning: Could not update hook for {event}: {e}",
file=sys.stderr,
)
break
if hook_exists:
break
if not hook_exists:
settings["hooks"][event].append({"matcher": ".*", "hooks": [hook_config]})
hooks_added = True
# Enable hooks if not already enabled
if not settings.get("hooksEnabled", False):
settings["hooksEnabled"] = True
hooks_added = True
if hooks_added:
with open(settings_file, "w") as f:
json.dump(settings, f, indent=2)
return True
except Exception:
return False
if __name__ == "__main__":
main()