claude-code-tools 1.19.4__tar.gz → 1.19.6__tar.gz

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.
Files changed (138) hide show
  1. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/PKG-INFO +1 -1
  2. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/__init__.py +1 -1
  3. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/app.py +5 -2
  4. claude_code_tools-1.19.6/claude_code_tools/voice_type/inject.py +167 -0
  5. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/overlay.py +81 -43
  6. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/pyproject.toml +2 -2
  7. claude_code_tools-1.19.4/claude_code_tools/voice_type/inject.py +0 -69
  8. claude_code_tools-1.19.4/docs/ai-agent-writing-skills.md +0 -500
  9. claude_code_tools-1.19.4/docs/cc-codex-instructions.md +0 -37
  10. claude_code_tools-1.19.4/docs/claude-code-chutes.md +0 -138
  11. claude_code_tools-1.19.4/docs/ensemble-ideas.md +0 -56
  12. claude_code_tools-1.19.4/docs/example-with-images.md +0 -366
  13. claude_code_tools-1.19.4/docs/linked-in-20260102.md +0 -33
  14. claude_code_tools-1.19.4/docs/paper-writing-style-transfer-tools.md +0 -397
  15. claude_code_tools-1.19.4/docs/reddit-aichat-resume-v2.md +0 -80
  16. claude_code_tools-1.19.4/docs/reddit-aichat-resume.md +0 -29
  17. claude_code_tools-1.19.4/docs/reddit-aichat.md +0 -79
  18. claude_code_tools-1.19.4/docs/reddit-post.md +0 -55
  19. claude_code_tools-1.19.4/docs/rollover-details.md +0 -67
  20. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/.gitignore +0 -0
  21. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/LICENSE +0 -0
  22. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/README.md +0 -0
  23. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/action_rpc.py +0 -0
  24. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  25. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/backends.py +0 -0
  26. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/cli.py +0 -0
  27. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/config.py +0 -0
  28. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/convert.py +0 -0
  29. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  30. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/locking.py +0 -0
  31. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/paths.py +0 -0
  32. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/registry.py +0 -0
  33. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/session.py +0 -0
  34. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/store.py +0 -0
  35. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  36. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/agent_tunnel/trust.py +0 -0
  37. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/aichat.py +0 -0
  38. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/claude_continue.py +0 -0
  39. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_continue.py +0 -0
  40. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server.py +0 -0
  41. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_cli.py +0 -0
  42. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_fingerprint.py +0 -0
  43. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_generation.py +0 -0
  44. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_legacy.py +0 -0
  45. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_models.py +0 -0
  46. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_process.py +0 -0
  47. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_reservation.py +0 -0
  48. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_retry.py +0 -0
  49. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_reuse.py +0 -0
  50. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_state.py +0 -0
  51. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_supervisor.py +0 -0
  52. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/codex_server_worker.py +0 -0
  53. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/config.py +0 -0
  54. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/csv2gsheet.py +0 -0
  55. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/delete_session.py +0 -0
  56. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/dotenv_vault.py +0 -0
  57. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/env_safe.py +0 -0
  58. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/export_all.py +0 -0
  59. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/export_claude_session.py +0 -0
  60. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/export_codex_session.py +0 -0
  61. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/export_session.py +0 -0
  62. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/find_claude_session.py +0 -0
  63. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/find_codex_session.py +0 -0
  64. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/find_original_session.py +0 -0
  65. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/find_session.py +0 -0
  66. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/find_trimmed_sessions.py +0 -0
  67. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/fix_session.py +0 -0
  68. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/gdoc2docx.py +0 -0
  69. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/gdoc2md.py +0 -0
  70. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/gsheet2csv.py +0 -0
  71. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/md2gdoc.py +0 -0
  72. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/__init__.py +0 -0
  73. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/cli.py +0 -0
  74. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/hooks.py +0 -0
  75. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/models.py +0 -0
  76. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/prompt_detect.py +0 -0
  77. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/store.py +0 -0
  78. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/msg/watcher.py +0 -0
  79. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/node_menu_ui.py +0 -0
  80. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/port_claude_noise.py +0 -0
  81. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/port_claude_to_codex.py +0 -0
  82. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/port_codex_flatten.py +0 -0
  83. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/port_codex_to_claude.py +0 -0
  84. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/port_service.py +0 -0
  85. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/resolve_session.py +0 -0
  86. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/resolve_session_names.py +0 -0
  87. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/resolve_session_render.py +0 -0
  88. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/search_index.py +0 -0
  89. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/session_lineage.py +0 -0
  90. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/session_menu.py +0 -0
  91. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/session_menu_cli.py +0 -0
  92. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/session_utils.py +0 -0
  93. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/smart_trim.py +0 -0
  94. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/smart_trim_core.py +0 -0
  95. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/tmux_cli_controller.py +0 -0
  96. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/tmux_execution_helpers.py +0 -0
  97. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/tmux_remote_controller.py +0 -0
  98. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/trim_in_place.py +0 -0
  99. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/trim_session.py +0 -0
  100. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/trim_session_claude.py +0 -0
  101. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/trim_session_codex.py +0 -0
  102. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/__init__.py +0 -0
  103. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/cli.py +0 -0
  104. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/config.py +0 -0
  105. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/engine_parakeet.py +0 -0
  106. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/engine_parakeet_mlx.py +0 -0
  107. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/engines.py +0 -0
  108. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/hotkey.py +0 -0
  109. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/voice_type/logic.py +0 -0
  110. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli.py +0 -0
  111. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_contract.py +0 -0
  112. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_formatting.py +0 -0
  113. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  114. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_manifest.py +0 -0
  115. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_projection.py +0 -0
  116. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_rendering.py +0 -0
  117. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_snapshots.py +0 -0
  118. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  119. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_processes.py +0 -0
  120. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_runs.py +0 -0
  121. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_store_io.py +0 -0
  122. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/claude_code_tools/workflow_validation.py +0 -0
  123. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/agent-tunnel-spec.md +0 -0
  124. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/claude-code-tmux-tutorials.md +0 -0
  125. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/dot-zshrc.md +0 -0
  126. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/find-claude-session.md +0 -0
  127. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/intercom-spec.md +0 -0
  128. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/lmsh.md +0 -0
  129. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/local-llm-setup.md +0 -0
  130. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/msg-phase-a-plan.md +0 -0
  131. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/msg-plan-v2.md +0 -0
  132. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/tmux-cli-instructions.md +0 -0
  133. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/docs/vault-documentation.md +0 -0
  134. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/hatch_build.py +0 -0
  135. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/node_ui/action_config.js +0 -0
  136. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/node_ui/menu.js +0 -0
  137. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/node_ui/package-lock.json +0 -0
  138. {claude_code_tools-1.19.4 → claude_code_tools-1.19.6}/node_ui/package.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 1.19.4
3
+ Version: 1.19.6
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "1.19.4"
3
+ __version__ = "1.19.6"
@@ -15,7 +15,7 @@ from enum import Enum
15
15
  from typing import Callable
16
16
 
17
17
  from .config import Config
18
- from .inject import Typist, copy_to_clipboard, play_sound
18
+ from .inject import SoundPlayer, Typist, copy_to_clipboard
19
19
  from .logic import (
20
20
  collapse_repeats,
21
21
  contains_phrase,
@@ -44,6 +44,9 @@ class VoiceTypeApp:
44
44
  def __init__(self, cfg: Config) -> None:
45
45
  self.cfg = cfg
46
46
  self.typist = Typist()
47
+ # Preload the chimes so the first one is instant and in sync
48
+ # with the overlay (afplay would lag ~100 ms behind the pill).
49
+ self._sound = SoundPlayer(cfg.sound_start, cfg.sound_stop)
47
50
  self._lock = threading.Lock()
48
51
  self._state = (
49
52
  State.ACTIVE
@@ -140,7 +143,7 @@ class VoiceTypeApp:
140
143
  if pre_msg is not None:
141
144
  self._status(pre_msg)
142
145
  if self.cfg.sounds and State.ACTIVE in (old, new):
143
- play_sound(
146
+ self._sound.play(
144
147
  self.cfg.sound_start
145
148
  if new == State.ACTIVE
146
149
  else self.cfg.sound_stop
@@ -0,0 +1,167 @@
1
+ """Keystroke injection and audio feedback.
2
+
3
+ Typing goes through pynput's ``keyboard.Controller``, which synthesizes
4
+ key events into whatever application currently has focus. On macOS the
5
+ process running voice-type (your terminal) needs Accessibility permission
6
+ (System Settings > Privacy & Security > Accessibility).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ _SYSTEM_SOUNDS = Path("/System/Library/Sounds")
16
+
17
+
18
+ class Typist:
19
+ """Types text into the focused application via synthetic key events."""
20
+
21
+ def __init__(self) -> None:
22
+ from pynput.keyboard import Controller
23
+
24
+ self._keyboard = Controller()
25
+
26
+ def type_text(self, text: str) -> None:
27
+ """Type ``text`` at the current cursor position."""
28
+ self._keyboard.type(text)
29
+
30
+ def press_enter(self) -> None:
31
+ """Press Enter in the focused application."""
32
+ from pynput.keyboard import Key
33
+
34
+ self._keyboard.tap(Key.enter)
35
+
36
+
37
+ def _resolve_sound(name: str) -> Path | None:
38
+ """Map a sound name or path to an existing file, or None."""
39
+ if not name:
40
+ return None
41
+ p = Path(name) if "/" in name else _SYSTEM_SOUNDS / f"{name}.aiff"
42
+ return p if p.exists() else None
43
+
44
+
45
+ def play_sound(name: str) -> None:
46
+ """Play a sound by spawning ``afplay`` (the fallback path).
47
+
48
+ Higher-latency than :class:`SoundPlayer` because afplay's own
49
+ CoreAudio startup delays audio-out ~100 ms; used only when the
50
+ preloaded AudioServices path is unavailable. No-ops off macOS / on
51
+ missing files.
52
+ """
53
+ sound = _resolve_sound(name) if sys.platform == "darwin" else None
54
+ if sound is None:
55
+ return
56
+ subprocess.Popen(
57
+ ["afplay", str(sound)],
58
+ stdout=subprocess.DEVNULL,
59
+ stderr=subprocess.DEVNULL,
60
+ )
61
+
62
+
63
+ class SoundPlayer:
64
+ """Instant start/stop sounds via CoreAudio ``AudioServices``.
65
+
66
+ Spawning ``afplay`` per sound delays audio-out ~100 ms behind the
67
+ in-process overlay, so the chime lands visibly after the waveform
68
+ pill. AudioServices plays a *preloaded* system-sound id in well
69
+ under a millisecond with no run loop, keeping sound and pill in
70
+ sync. Sounds are registered once at construction; playback is
71
+ thread-safe and asynchronous. Falls back to ``afplay`` when the
72
+ framework can't be loaded, and no-ops off macOS.
73
+ """
74
+
75
+ def __init__(self, *names: str) -> None:
76
+ self._ids: dict[str, int] = {}
77
+ self._at = None
78
+ self._cf = None
79
+ self._ctypes = None
80
+ if sys.platform == "darwin":
81
+ try:
82
+ import ctypes
83
+ import ctypes.util
84
+
85
+ self._ctypes = ctypes
86
+ at = ctypes.CDLL(ctypes.util.find_library("AudioToolbox"))
87
+ cf = ctypes.CDLL(
88
+ ctypes.util.find_library("CoreFoundation")
89
+ )
90
+ cf.CFURLCreateFromFileSystemRepresentation.restype = (
91
+ ctypes.c_void_p
92
+ )
93
+ cf.CFURLCreateFromFileSystemRepresentation.argtypes = [
94
+ ctypes.c_void_p,
95
+ ctypes.c_char_p,
96
+ ctypes.c_long,
97
+ ctypes.c_bool,
98
+ ]
99
+ cf.CFRelease.argtypes = [ctypes.c_void_p]
100
+ at.AudioServicesCreateSystemSoundID.restype = (
101
+ ctypes.c_int32
102
+ )
103
+ at.AudioServicesCreateSystemSoundID.argtypes = [
104
+ ctypes.c_void_p,
105
+ ctypes.POINTER(ctypes.c_uint32),
106
+ ]
107
+ at.AudioServicesPlaySystemSound.argtypes = [
108
+ ctypes.c_uint32
109
+ ]
110
+ self._at, self._cf = at, cf
111
+ except Exception:
112
+ self._at = None
113
+ for name in names:
114
+ self._sid(name) # preload so the first play is instant
115
+
116
+ def _sid(self, name: str) -> int | None:
117
+ """Return the (cached) system-sound id for ``name``, or None."""
118
+ if not name or self._at is None:
119
+ return None
120
+ if name in self._ids:
121
+ return self._ids[name]
122
+ path = _resolve_sound(name)
123
+ if path is None:
124
+ return None
125
+ try:
126
+ raw = str(path).encode()
127
+ url = self._cf.CFURLCreateFromFileSystemRepresentation(
128
+ None, raw, len(raw), False
129
+ )
130
+ if not url:
131
+ return None
132
+ sid = self._ctypes.c_uint32(0)
133
+ err = self._at.AudioServicesCreateSystemSoundID(
134
+ url, self._ctypes.byref(sid)
135
+ )
136
+ self._cf.CFRelease(url)
137
+ if err != 0 or sid.value == 0:
138
+ return None
139
+ self._ids[name] = sid.value
140
+ return sid.value
141
+ except Exception:
142
+ return None
143
+
144
+ def play(self, name: str) -> None:
145
+ """Play ``name`` instantly; fall back to afplay if needed."""
146
+ if not name or sys.platform != "darwin":
147
+ return
148
+ sid = self._sid(name)
149
+ if sid is not None:
150
+ try:
151
+ self._at.AudioServicesPlaySystemSound(sid)
152
+ return
153
+ except Exception:
154
+ pass
155
+ play_sound(name)
156
+
157
+
158
+ def copy_to_clipboard(text: str) -> None:
159
+ """Put ``text`` on the macOS clipboard (no-op elsewhere/on failure)."""
160
+ if sys.platform != "darwin" or not text:
161
+ return
162
+ try:
163
+ subprocess.run(
164
+ ["pbcopy"], input=text.encode(), check=False, timeout=5
165
+ )
166
+ except Exception:
167
+ pass
@@ -21,11 +21,12 @@ from typing import Callable
21
21
  SampleFn = Callable[[], tuple[str, bool, float]]
22
22
  TickFn = Callable[[], None]
23
23
 
24
- _WIDTH = 280.0
25
- _HEIGHT = 46.0
26
- _MARGIN_BOTTOM = 96.0
27
- _BARS = 64
28
- _TICK_SECONDS = 0.05
24
+ _WIDTH = 200.0
25
+ _HEIGHT = 150.0
26
+ _MARGIN_BOTTOM = 120.0
27
+ # Points around the blob's rim (smoothness) and frame rate.
28
+ _BLOB_POINTS = 108
29
+ _TICK_SECONDS = 0.033 # ~30 fps, smooth wobble
29
30
 
30
31
 
31
32
  def overlay_available() -> bool:
@@ -59,6 +60,7 @@ def run_overlay(sample: SampleFn, tick: TickFn, stopped: Callable[[], bool]) ->
59
60
  NSBackingStoreBuffered,
60
61
  NSBezierPath,
61
62
  NSColor,
63
+ NSGradient,
62
64
  NSMakeRect,
63
65
  NSPanel,
64
66
  NSScreen,
@@ -66,60 +68,96 @@ def run_overlay(sample: SampleFn, tick: TickFn, stopped: Callable[[], bool]) ->
66
68
  )
67
69
 
68
70
  class WaveView(AppKit.NSView): # noqa: D401
69
- """Draws the pill background, state dot, and scrolling bars."""
71
+ """A glowing blue blob that wobbles and swells with the voice.
72
+
73
+ A soft orb sits at the bottom of the screen: it breathes gently
74
+ in silence and, as the live gain-adjusted mic level rises, its
75
+ rim deforms (organic multi-harmonic wobble) and the whole blob
76
+ swells — so its shape reflects the actual audio. Bluish, with a
77
+ radial gradient core, a rim highlight, and an outer halo for
78
+ presence.
79
+ """
70
80
 
71
81
  def initWithFrame_(self, frame): # noqa: ANN001, ANN201, N802
72
82
  self = objc.super(WaveView, self).initWithFrame_(frame)
73
83
  if self is None:
74
84
  return None
75
- self.levels = [0.0] * _BARS
85
+ self.amp = 0.0 # smoothed audio level driving the wobble
86
+ self.phase = 0.0
76
87
  self.recording = False
77
- self.label = ""
78
88
  return self
79
89
 
80
90
  def push_(self, sample): # noqa: ANN001, ANN201, N802
81
- label, recording, level = sample
82
- self.levels = self.levels[1:] + [max(0.0, min(1.0, level))]
91
+ _label, recording, level = sample
92
+ level = max(0.0, min(1.0, float(level)))
93
+ # Fast attack, slow release: pops on speech, settles gently.
94
+ k = 0.55 if level > self.amp else 0.15
95
+ self.amp += k * (level - self.amp)
96
+ self.phase += 0.16
83
97
  self.recording = bool(recording)
84
- self.label = str(label)
85
98
  self.setNeedsDisplay_(True)
86
99
 
100
+ def _blob_path(self, cx, cy, base_r): # noqa: ANN001, ANN202
101
+ import math
102
+
103
+ path = NSBezierPath.bezierPath()
104
+ wob = 0.09 + 0.42 * self.amp # rim deformation depth
105
+ breathe = 0.05 * math.sin(self.phase * 0.5)
106
+ swell = 1.0 + 0.24 * self.amp # overall growth with volume
107
+ for i in range(_BLOB_POINTS + 1):
108
+ a = 2.0 * math.pi * i / _BLOB_POINTS
109
+ shape = (
110
+ math.sin(3 * a + self.phase)
111
+ + 0.6 * math.sin(5 * a - 1.3 * self.phase)
112
+ + 0.3 * math.sin(7 * a + 1.7 * self.phase)
113
+ ) / 1.9
114
+ r = base_r * swell * (1.0 + breathe + wob * shape)
115
+ x = cx + r * math.cos(a)
116
+ y = cy + r * math.sin(a)
117
+ if i == 0:
118
+ path.moveToPoint_((x, y))
119
+ else:
120
+ path.lineToPoint_((x, y))
121
+ path.closePath()
122
+ return path
123
+
87
124
  def drawRect_(self, rect): # noqa: ANN001, ANN201, N802
88
125
  b = self.bounds()
89
- pill = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
90
- b, b.size.height / 2, b.size.height / 2
126
+ cx, cy = b.size.width / 2, b.size.height / 2
127
+ base_r = min(b.size.width, b.size.height) * 0.26
128
+
129
+ # outer halo for presence
130
+ halo = self._blob_path(cx, cy, base_r * 1.28)
131
+ NSColor.colorWithCalibratedRed_green_blue_alpha_(
132
+ 0.30, 0.62, 1.0, 0.20
133
+ ).setFill()
134
+ halo.fill()
135
+
136
+ # the blob: radial gradient core -> deeper blue rim
137
+ blob = self._blob_path(cx, cy, base_r)
138
+ core = NSColor.colorWithCalibratedRed_green_blue_alpha_(
139
+ 0.60, 0.85, 1.0, 0.85
91
140
  )
92
- NSColor.colorWithCalibratedWhite_alpha_(0.08, 0.82).setFill()
93
- pill.fill()
94
- if self.recording:
95
- bar_color = NSColor.colorWithCalibratedRed_green_blue_alpha_(
96
- 1.0, 0.27, 0.23, 1.0
97
- )
98
- else:
99
- bar_color = NSColor.colorWithCalibratedWhite_alpha_(
100
- 0.62, 0.9
101
- )
102
- bar_color.setFill()
103
- # state dot on the left
104
- dot = 8.0
105
- dot_rect = NSMakeRect(
106
- 14.0, (b.size.height - dot) / 2, dot, dot
141
+ edge = NSColor.colorWithCalibratedRed_green_blue_alpha_(
142
+ 0.16, 0.44, 0.98, 0.70
143
+ )
144
+ grad = NSGradient.alloc().initWithStartingColor_endingColor_(
145
+ core, edge
107
146
  )
108
- NSBezierPath.bezierPathWithOvalInRect_(dot_rect).fill()
109
- # scrolling waveform bars, mirrored around the centerline
110
- left = 30.0
111
- right = 14.0
112
- span = b.size.width - left - right
113
- bw = span / _BARS
114
- mid = b.size.height / 2
115
- max_half = b.size.height / 2 - 7.0
116
- for i, level in enumerate(self.levels):
117
- half = max(1.0, level * max_half)
118
- x = left + i * bw
119
- bar = NSMakeRect(x, mid - half, max(1.0, bw - 2.0), 2 * half)
120
- NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
121
- bar, 1.0, 1.0
122
- ).fill()
147
+ if grad is not None:
148
+ grad.drawInBezierPath_relativeCenterPosition_(
149
+ blob, (0.0, 0.28)
150
+ )
151
+ else: # pragma: no cover - gradient unavailable
152
+ edge.setFill()
153
+ blob.fill()
154
+
155
+ # bright rim
156
+ NSColor.colorWithCalibratedRed_green_blue_alpha_(
157
+ 0.75, 0.92, 1.0, 0.9
158
+ ).setStroke()
159
+ blob.setLineWidth_(2.0)
160
+ blob.stroke()
123
161
 
124
162
  class Driver(AppKit.NSObject):
125
163
  """NSTimer target: samples audio, ticks the app, ends the loop.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "1.19.4"
3
+ version = "1.19.6"
4
4
  description = "Collection of tools for working with Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -131,7 +131,7 @@ markers = [
131
131
 
132
132
  [tool.commitizen]
133
133
  name = "cz_conventional_commits"
134
- version = "1.19.4"
134
+ version = "1.19.6"
135
135
  tag_format = "v$version"
136
136
  version_files = [
137
137
  "pyproject.toml:version",
@@ -1,69 +0,0 @@
1
- """Keystroke injection and audio feedback.
2
-
3
- Typing goes through pynput's ``keyboard.Controller``, which synthesizes
4
- key events into whatever application currently has focus. On macOS the
5
- process running voice-type (your terminal) needs Accessibility permission
6
- (System Settings > Privacy & Security > Accessibility).
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import subprocess
12
- import sys
13
- from pathlib import Path
14
-
15
- _SYSTEM_SOUNDS = Path("/System/Library/Sounds")
16
-
17
-
18
- class Typist:
19
- """Types text into the focused application via synthetic key events."""
20
-
21
- def __init__(self) -> None:
22
- from pynput.keyboard import Controller
23
-
24
- self._keyboard = Controller()
25
-
26
- def type_text(self, text: str) -> None:
27
- """Type ``text`` at the current cursor position."""
28
- self._keyboard.type(text)
29
-
30
- def press_enter(self) -> None:
31
- """Press Enter in the focused application."""
32
- from pynput.keyboard import Key
33
-
34
- self._keyboard.tap(Key.enter)
35
-
36
-
37
- def play_sound(name: str) -> None:
38
- """Play a named macOS system sound or a sound file path.
39
-
40
- ``name`` is either a system sound name (e.g. "Glass", "Bottle" —
41
- see /System/Library/Sounds) or an absolute path to an audio file.
42
- Silently no-ops off macOS, on empty names, and on missing files.
43
- """
44
- if sys.platform != "darwin" or not name:
45
- return
46
- sound = (
47
- Path(name)
48
- if "/" in name
49
- else _SYSTEM_SOUNDS / f"{name}.aiff"
50
- )
51
- if not sound.exists():
52
- return
53
- subprocess.Popen(
54
- ["afplay", str(sound)],
55
- stdout=subprocess.DEVNULL,
56
- stderr=subprocess.DEVNULL,
57
- )
58
-
59
-
60
- def copy_to_clipboard(text: str) -> None:
61
- """Put ``text`` on the macOS clipboard (no-op elsewhere/on failure)."""
62
- if sys.platform != "darwin" or not text:
63
- return
64
- try:
65
- subprocess.run(
66
- ["pbcopy"], input=text.encode(), check=False, timeout=5
67
- )
68
- except Exception:
69
- pass