claude-code-tools 1.19.7__tar.gz → 1.19.8__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 (126) hide show
  1. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/PKG-INFO +1 -1
  2. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/__init__.py +1 -1
  3. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/cli.py +25 -0
  4. claude_code_tools-1.19.8/claude_code_tools/voice_type/setup_wizard.py +357 -0
  5. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/pyproject.toml +2 -2
  6. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/.gitignore +0 -0
  7. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/LICENSE +0 -0
  8. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/README.md +0 -0
  9. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/action_rpc.py +0 -0
  10. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  11. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/backends.py +0 -0
  12. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/cli.py +0 -0
  13. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/config.py +0 -0
  14. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/convert.py +0 -0
  15. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  16. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/locking.py +0 -0
  17. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/paths.py +0 -0
  18. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/registry.py +0 -0
  19. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/session.py +0 -0
  20. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/store.py +0 -0
  21. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  22. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/agent_tunnel/trust.py +0 -0
  23. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/aichat.py +0 -0
  24. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/claude_continue.py +0 -0
  25. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_continue.py +0 -0
  26. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server.py +0 -0
  27. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_cli.py +0 -0
  28. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_fingerprint.py +0 -0
  29. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_generation.py +0 -0
  30. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_legacy.py +0 -0
  31. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_models.py +0 -0
  32. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_process.py +0 -0
  33. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_reservation.py +0 -0
  34. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_retry.py +0 -0
  35. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_reuse.py +0 -0
  36. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_state.py +0 -0
  37. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_supervisor.py +0 -0
  38. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/codex_server_worker.py +0 -0
  39. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/config.py +0 -0
  40. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/csv2gsheet.py +0 -0
  41. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/delete_session.py +0 -0
  42. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/dotenv_vault.py +0 -0
  43. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/env_safe.py +0 -0
  44. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/export_all.py +0 -0
  45. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/export_claude_session.py +0 -0
  46. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/export_codex_session.py +0 -0
  47. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/export_session.py +0 -0
  48. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/find_claude_session.py +0 -0
  49. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/find_codex_session.py +0 -0
  50. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/find_original_session.py +0 -0
  51. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/find_session.py +0 -0
  52. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/find_trimmed_sessions.py +0 -0
  53. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/fix_session.py +0 -0
  54. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/gdoc2docx.py +0 -0
  55. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/gdoc2md.py +0 -0
  56. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/gsheet2csv.py +0 -0
  57. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/md2gdoc.py +0 -0
  58. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/__init__.py +0 -0
  59. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/cli.py +0 -0
  60. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/hooks.py +0 -0
  61. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/models.py +0 -0
  62. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/prompt_detect.py +0 -0
  63. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/store.py +0 -0
  64. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/msg/watcher.py +0 -0
  65. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/node_menu_ui.py +0 -0
  66. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/port_claude_noise.py +0 -0
  67. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/port_claude_to_codex.py +0 -0
  68. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/port_codex_flatten.py +0 -0
  69. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/port_codex_to_claude.py +0 -0
  70. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/port_service.py +0 -0
  71. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/resolve_session.py +0 -0
  72. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/resolve_session_names.py +0 -0
  73. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/resolve_session_render.py +0 -0
  74. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/search_index.py +0 -0
  75. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/session_lineage.py +0 -0
  76. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/session_menu.py +0 -0
  77. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/session_menu_cli.py +0 -0
  78. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/session_utils.py +0 -0
  79. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/smart_trim.py +0 -0
  80. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/smart_trim_core.py +0 -0
  81. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/tmux_cli_controller.py +0 -0
  82. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/tmux_execution_helpers.py +0 -0
  83. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/tmux_remote_controller.py +0 -0
  84. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/trim_in_place.py +0 -0
  85. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/trim_session.py +0 -0
  86. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/trim_session_claude.py +0 -0
  87. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/trim_session_codex.py +0 -0
  88. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/__init__.py +0 -0
  89. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/app.py +0 -0
  90. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/config.py +0 -0
  91. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/engine_parakeet.py +0 -0
  92. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/engine_parakeet_mlx.py +0 -0
  93. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/engines.py +0 -0
  94. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/hotkey.py +0 -0
  95. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/inject.py +0 -0
  96. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/logic.py +0 -0
  97. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/voice_type/overlay.py +0 -0
  98. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli.py +0 -0
  99. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_contract.py +0 -0
  100. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_formatting.py +0 -0
  101. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  102. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_manifest.py +0 -0
  103. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_projection.py +0 -0
  104. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_rendering.py +0 -0
  105. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_snapshots.py +0 -0
  106. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  107. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_processes.py +0 -0
  108. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_runs.py +0 -0
  109. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_store_io.py +0 -0
  110. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/claude_code_tools/workflow_validation.py +0 -0
  111. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/agent-tunnel-spec.md +0 -0
  112. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/claude-code-tmux-tutorials.md +0 -0
  113. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/dot-zshrc.md +0 -0
  114. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/find-claude-session.md +0 -0
  115. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/intercom-spec.md +0 -0
  116. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/lmsh.md +0 -0
  117. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/local-llm-setup.md +0 -0
  118. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/msg-phase-a-plan.md +0 -0
  119. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/msg-plan-v2.md +0 -0
  120. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/tmux-cli-instructions.md +0 -0
  121. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/docs/vault-documentation.md +0 -0
  122. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/hatch_build.py +0 -0
  123. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/node_ui/action_config.js +0 -0
  124. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/node_ui/menu.js +0 -0
  125. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/node_ui/package-lock.json +0 -0
  126. {claude_code_tools-1.19.7 → claude_code_tools-1.19.8}/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.7
3
+ Version: 1.19.8
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.7"
3
+ __version__ = "1.19.8"
@@ -157,6 +157,12 @@ def _cmd_hotkey() -> int:
157
157
  return 0
158
158
 
159
159
 
160
+ def _cmd_setup(args: argparse.Namespace) -> int:
161
+ from .setup_wizard import run_setup
162
+
163
+ return run_setup(config_path=args.config, force=args.force)
164
+
165
+
160
166
  def _cmd_init(args: argparse.Namespace) -> int:
161
167
  try:
162
168
  path = write_sample_config(args.config, force=args.force)
@@ -164,6 +170,7 @@ def _cmd_init(args: argparse.Namespace) -> int:
164
170
  print(f"voice-type: {e}", file=sys.stderr)
165
171
  return 1
166
172
  print(f"wrote {path}")
173
+ print("(tip: `voice-type setup` walks you through it interactively)")
167
174
  return 0
168
175
 
169
176
 
@@ -179,6 +186,7 @@ examples:
179
186
  voice-type --engine parakeet-mlx --segmentation hold
180
187
  # best accuracy: whole-take dictation
181
188
  voice-type --mode wake # hands-free with wake word "claude"
189
+ voice-type setup # interactive walkthrough -> config
182
190
  voice-type init # write a commented sample config
183
191
  voice-type hotkey # record a chord, print the config line
184
192
 
@@ -218,12 +226,29 @@ Accessibility (to type), Input Monitoring (global hotkeys).
218
226
  "hotkey",
219
227
  help="press a key combo and print the matching config line",
220
228
  )
229
+ p_setup = sub.add_parser(
230
+ "setup",
231
+ help="interactive walkthrough that writes your config",
232
+ )
233
+ p_setup.add_argument(
234
+ "--config",
235
+ type=Path,
236
+ # SUPPRESS so a --config given BEFORE the subcommand is not
237
+ # clobbered by the subparser's default (see the init parser).
238
+ default=argparse.SUPPRESS,
239
+ help="destination path",
240
+ )
241
+ p_setup.add_argument(
242
+ "--force", action="store_true", help="overwrite existing config"
243
+ )
221
244
 
222
245
  args = parser.parse_args()
223
246
  if args.command == "init":
224
247
  return _cmd_init(args)
225
248
  if args.command == "hotkey":
226
249
  return _cmd_hotkey()
250
+ if args.command == "setup":
251
+ return _cmd_setup(args)
227
252
  return _cmd_run(args)
228
253
 
229
254
 
@@ -0,0 +1,357 @@
1
+ """Interactive setup wizard for voice-type (``voice-type setup``).
2
+
3
+ Walks a new user through the handful of choices that matter — engine,
4
+ activation mode, segmentation, hotkey, wake word, and a few niceties —
5
+ explaining each, then writes a valid ``config.toml`` and prints the
6
+ command to run. Unlike ``voice-type init`` (which drops a fully
7
+ commented sample file to edit by hand), this asks questions.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import tempfile
14
+ import tomllib
15
+ from pathlib import Path
16
+
17
+ from .config import (
18
+ DEFAULT_CONFIG_PATH,
19
+ VALID_MODEL_ARCHS,
20
+ Config,
21
+ )
22
+
23
+
24
+ class _Cancelled(Exception):
25
+ """Raised when the user aborts a prompt (Ctrl-C).
26
+
27
+ questionary 2.x turns Ctrl-C (and Ctrl-Q) into the ``None`` answer
28
+ that ``_ask`` maps to this exception; Escape is not a cancellation
29
+ key inside questionary prompts, so it is not advertised here.
30
+ """
31
+
32
+
33
+ def _ask(prompt): # noqa: ANN001, ANN202
34
+ """Return a prompt's answer, or raise ``_Cancelled`` if aborted.
35
+
36
+ questionary returns ``None`` when a prompt is cancelled (Ctrl-C);
37
+ every answer flows through here so an abort — at a required OR an
38
+ optional prompt — always cancels the whole wizard instead of being
39
+ mistaken for a "no"/default and writing a config anyway.
40
+ """
41
+ value = prompt.ask()
42
+ if value is None:
43
+ raise _Cancelled
44
+ return value
45
+
46
+
47
+ # TOML basic-string escapes required by the spec; every other control
48
+ # character (U+0000–U+001F and U+007F) is emitted as a \uXXXX escape so
49
+ # a prompt answer with a newline/tab/etc. never produces invalid TOML.
50
+ _TOML_STR_ESCAPES = {
51
+ "\\": "\\\\",
52
+ '"': '\\"',
53
+ "\b": "\\b",
54
+ "\t": "\\t",
55
+ "\n": "\\n",
56
+ "\f": "\\f",
57
+ "\r": "\\r",
58
+ }
59
+
60
+
61
+ def _toml_string(s: str) -> str:
62
+ out: list[str] = []
63
+ for ch in s:
64
+ escaped = _TOML_STR_ESCAPES.get(ch)
65
+ if escaped is not None:
66
+ out.append(escaped)
67
+ elif ord(ch) < 0x20 or ord(ch) == 0x7F:
68
+ out.append(f"\\u{ord(ch):04X}")
69
+ else:
70
+ out.append(ch)
71
+ return '"' + "".join(out) + '"'
72
+
73
+
74
+ def _toml_value(v: object) -> str:
75
+ if isinstance(v, bool):
76
+ return "true" if v else "false"
77
+ if isinstance(v, str):
78
+ return _toml_string(v)
79
+ if isinstance(v, (int, float)):
80
+ return repr(v)
81
+ if isinstance(v, list):
82
+ return "[" + ", ".join(_toml_value(x) for x in v) + "]"
83
+ raise TypeError(f"cannot serialize {v!r} to TOML")
84
+
85
+
86
+ def _atomic_write(path: Path, text: str, overwrite: bool) -> None:
87
+ """Write ``text`` to ``path`` atomically.
88
+
89
+ Writes to a temporary file in the destination directory, flushes and
90
+ fsyncs it, then publishes it over the destination so an I/O failure or
91
+ interruption can never truncate or half-write an existing config. The
92
+ temporary file is removed on any failure.
93
+
94
+ When ``overwrite`` is true the publish is an unconditional
95
+ ``os.replace``. When it is false — the caller never authorized
96
+ clobbering an existing config — the publish is an atomic no-clobber
97
+ ``os.link``, which raises ``FileExistsError`` if the destination
98
+ appeared (e.g. another process created it) after the wizard's initial
99
+ existence check, so a config is never silently destroyed.
100
+ """
101
+ path.parent.mkdir(parents=True, exist_ok=True)
102
+ fd, tmp = tempfile.mkstemp(
103
+ dir=str(path.parent), prefix=".config-", suffix=".toml.tmp"
104
+ )
105
+ try:
106
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
107
+ f.write(text)
108
+ f.flush()
109
+ os.fsync(f.fileno())
110
+ if overwrite:
111
+ os.replace(tmp, path)
112
+ else:
113
+ # Atomic create-or-fail: link succeeds only if path is absent.
114
+ os.link(tmp, path)
115
+ try:
116
+ os.unlink(tmp)
117
+ except OSError:
118
+ pass
119
+ except BaseException:
120
+ try:
121
+ os.unlink(tmp)
122
+ except OSError:
123
+ pass
124
+ raise
125
+
126
+
127
+ def _write_config(path: Path, answers: dict, overwrite: bool) -> None:
128
+ lines = [
129
+ "# voice-type configuration — written by `voice-type setup`.",
130
+ "# Run `voice-type init --force` for a fully commented sample.",
131
+ "",
132
+ ]
133
+ for key, value in answers.items():
134
+ lines.append(f"{key} = {_toml_value(value)}")
135
+ text = "\n".join(lines) + "\n"
136
+ # Guard: never replace the destination with a document tomllib can't
137
+ # read back (the loader that voice-type itself uses).
138
+ tomllib.loads(text)
139
+ _atomic_write(path, text, overwrite)
140
+
141
+
142
+ def _manual_hotkey(q, default: str): # noqa: ANN001, ANN202
143
+ """Prompt for a hotkey typed by hand, validated by ``parse_hotkey``."""
144
+ from .hotkey import parse_hotkey
145
+
146
+ def _valid(text: str): # noqa: ANN202
147
+ try:
148
+ parse_hotkey(text)
149
+ return True
150
+ except ValueError as e:
151
+ return str(e)
152
+
153
+ return _ask(q.text(
154
+ 'Hotkey (e.g. "<ctrl>+;" or "ctrl+;"):',
155
+ default=default,
156
+ validate=_valid,
157
+ ))
158
+
159
+
160
+ def _ask_hotkey(q, label: str, default: str): # noqa: ANN001, ANN202
161
+ """Return a hotkey string via keep-default / record / type."""
162
+ how = _ask(q.select(
163
+ f"{label} hotkey:",
164
+ choices=[
165
+ f"Keep default ({default})",
166
+ "Record one now (press the combo)",
167
+ "Type it manually",
168
+ ],
169
+ ))
170
+ if how.startswith("Keep"):
171
+ return default
172
+ if how.startswith("Record"):
173
+ try:
174
+ from .hotkey import record_hotkey
175
+
176
+ print(" Press the key combo now (15s)...")
177
+ chord = record_hotkey()
178
+ except ImportError:
179
+ # Recording needs the optional voice dependency (pynput). Say
180
+ # so — rather than the ambiguous "nothing recorded" — and fall
181
+ # through to manual entry instead of silently keeping default.
182
+ print(
183
+ " recording needs the voice dependency (pynput); "
184
+ "type the hotkey manually instead."
185
+ )
186
+ return _manual_hotkey(q, default)
187
+ except KeyboardInterrupt:
188
+ # Ctrl-C during the record window cancels the whole wizard,
189
+ # matching every other prompt — nothing gets written.
190
+ raise _Cancelled
191
+ if not chord:
192
+ print(" nothing recorded; keeping default.")
193
+ return default
194
+ # A recorded chord can contain keys the hotkey listener can't
195
+ # bind (e.g. <caps_lock>, media keys). Validate it the same way
196
+ # a manually typed one is validated, so the wizard never writes
197
+ # a config that later fails when the listeners start.
198
+ from .hotkey import parse_hotkey
199
+
200
+ try:
201
+ parse_hotkey(chord)
202
+ except ValueError as e:
203
+ print(
204
+ f" recorded {chord!r} can't be used as a hotkey ({e}); "
205
+ "type one manually instead."
206
+ )
207
+ else:
208
+ print(f" recorded: {chord}")
209
+ return chord
210
+ # fall through to manual entry (which retries / keeps the default)
211
+ return _manual_hotkey(q, default)
212
+
213
+
214
+ def run_setup(config_path: Path | None = None, force: bool = False) -> int:
215
+ """Run the interactive wizard; returns a process exit code."""
216
+ try:
217
+ import questionary as q
218
+ except ImportError as e: # pragma: no cover - questionary is a base dep
219
+ print(f"voice-type: {e}; setup needs questionary")
220
+ return 1
221
+
222
+ path = config_path or DEFAULT_CONFIG_PATH
223
+ ans: dict = {}
224
+ # Whether clobbering an existing config was authorized: --force, or the
225
+ # user confirming the overwrite prompt below. If neither, the final
226
+ # write uses an atomic no-clobber publish so a config created by another
227
+ # process while the wizard runs is never silently overwritten.
228
+ overwrite_ok = force
229
+ try:
230
+ if path.exists() and not force:
231
+ if not _ask(
232
+ q.confirm(f"{path} exists — overwrite it?", default=False)
233
+ ):
234
+ print("setup cancelled.")
235
+ return 1
236
+ overwrite_ok = True
237
+
238
+ print("voice-type setup — a few questions, then you're ready.\n")
239
+
240
+ engine = _ask(q.select(
241
+ "Transcription engine:",
242
+ choices=[
243
+ q.Choice(
244
+ "parakeet-mlx — best accuracy + speed (Apple GPU; "
245
+ "needs the voice-mlx extra)",
246
+ value="parakeet-mlx",
247
+ ),
248
+ q.Choice(
249
+ "parakeet — Parakeet on CPU (needs voice-parakeet)",
250
+ value="parakeet",
251
+ ),
252
+ q.Choice(
253
+ "moonshine — small streaming models (needs voice)",
254
+ value="moonshine",
255
+ ),
256
+ ],
257
+ ))
258
+ ans["engine"] = engine
259
+ is_parakeet = engine in ("parakeet", "parakeet-mlx")
260
+
261
+ if engine == "parakeet":
262
+ ans["parakeet_model"] = _ask(q.select(
263
+ "Parakeet build:",
264
+ choices=[
265
+ q.Choice("v3-int8 — multilingual, ~490 MB", "v3-int8"),
266
+ q.Choice(
267
+ "v2-fp16 — English, higher precision", "v2-fp16"
268
+ ),
269
+ ],
270
+ ))
271
+ elif engine == "moonshine":
272
+ ans["model_arch"] = _ask(q.select(
273
+ "Moonshine model:",
274
+ choices=list(VALID_MODEL_ARCHS),
275
+ default="medium-streaming",
276
+ ))
277
+
278
+ mode = _ask(q.select(
279
+ "How should dictation activate?",
280
+ choices=[
281
+ q.Choice("toggle — a hotkey starts/stops", "toggle"),
282
+ q.Choice("wake — say a wake word (hands-free)", "wake"),
283
+ q.Choice("vad — always on while running", "vad"),
284
+ ],
285
+ ))
286
+ ans["mode"] = mode
287
+
288
+ if mode == "toggle" and is_parakeet:
289
+ ans["segmentation"] = _ask(q.select(
290
+ "When should the text appear?",
291
+ choices=[
292
+ q.Choice(
293
+ "hold — whole take on toggle-off (most accurate)",
294
+ "hold",
295
+ ),
296
+ q.Choice("vad — each utterance when you pause", "vad"),
297
+ ],
298
+ ))
299
+
300
+ ans["hotkey"] = _ask_hotkey(q, "Toggle-recording", "<ctrl>+;")
301
+
302
+ if mode == "wake":
303
+ word = _ask(q.text("Wake word:", default="claude"))
304
+ ans["wake_word"] = word.strip() or "claude"
305
+ aliases = _ask(q.text(
306
+ "Wake-word aliases the model mishears "
307
+ "(comma-separated, optional):",
308
+ default="",
309
+ ))
310
+ if aliases.strip():
311
+ ans["wake_word_aliases"] = [
312
+ a.strip() for a in aliases.split(",") if a.strip()
313
+ ]
314
+
315
+ if _ask(q.confirm(
316
+ "Configure extras (sounds, clipboard rescue, ghost)?",
317
+ default=False,
318
+ )):
319
+ ans["sounds"] = _ask(
320
+ q.confirm("Play start/stop chimes?", default=True)
321
+ )
322
+ if _ask(q.confirm(
323
+ "Add a hotkey to re-type the last transcript "
324
+ "(wrong-window rescue)?",
325
+ default=False,
326
+ )):
327
+ ans["paste_hotkey"] = _ask_hotkey(
328
+ q, "Paste-again", "<cmd>+<ctrl>+v"
329
+ )
330
+ ans["copy_to_clipboard"] = True
331
+ ans["overlay"] = _ask(q.confirm(
332
+ "Show the floating ghost while recording?", default=True
333
+ ))
334
+ except _Cancelled:
335
+ print("\nsetup cancelled — nothing written.")
336
+ return 1
337
+
338
+ # Validate the combination before writing.
339
+ try:
340
+ Config(**ans).validate()
341
+ except (ValueError, TypeError) as e:
342
+ print(f"\nvoice-type: those choices don't combine ({e}).")
343
+ return 1
344
+
345
+ try:
346
+ _write_config(path, ans, overwrite_ok)
347
+ except FileExistsError:
348
+ # A config appeared after the initial existence check and the user
349
+ # never authorized an overwrite; refuse rather than clobber it.
350
+ print(
351
+ f"\nvoice-type: {path} was created while setup was running; "
352
+ "not overwriting it. Re-run with --force to replace it."
353
+ )
354
+ return 1
355
+ print(f"\n✓ wrote {path}")
356
+ print("\nStart dictating with:\n voice-type")
357
+ return 0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "1.19.7"
3
+ version = "1.19.8"
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.7"
134
+ version = "1.19.8"
135
135
  tag_format = "v$version"
136
136
  version_files = [
137
137
  "pyproject.toml:version",