vocalize-cli 0.7.0__tar.gz → 0.8.0__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 (46) hide show
  1. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/CHANGELOG.md +27 -0
  2. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/PKG-INFO +17 -7
  3. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/README.md +16 -6
  4. vocalize_cli-0.8.0/hooks/install_quick_action.py +135 -0
  5. vocalize_cli-0.8.0/hooks/quick_actions/Speak Latest Plan.workflow/Contents/Info.plist +26 -0
  6. vocalize_cli-0.8.0/hooks/quick_actions/Speak Latest Plan.workflow/Contents/Resources/document.wflow +212 -0
  7. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/quick_actions/Speak with Vocalize.workflow/Contents/Resources/document.wflow +6 -3
  8. vocalize_cli-0.8.0/hooks/speak_options.py +245 -0
  9. vocalize_cli-0.8.0/tests/test_install_quick_action.py +264 -0
  10. vocalize_cli-0.8.0/tests/test_speak_options.py +348 -0
  11. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/__init__.py +1 -1
  12. vocalize_cli-0.7.0/hooks/install_quick_action.py +0 -82
  13. vocalize_cli-0.7.0/tests/test_install_quick_action.py +0 -158
  14. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/.env.example +0 -0
  15. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/.github/workflows/ci.yml +0 -0
  16. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/.gitignore +0 -0
  17. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/LICENSE +0 -0
  18. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/claude_stop_hook.py +0 -0
  19. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/install_hook.py +0 -0
  20. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/quick_actions/Speak with Vocalize.workflow/Contents/Info.plist +0 -0
  21. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/quick_actions/Stop Vocalize.workflow/Contents/Info.plist +0 -0
  22. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/quick_actions/Stop Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  23. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/hooks/speak_url_gate.py +0 -0
  24. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/pyproject.toml +0 -0
  25. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/conftest.py +0 -0
  26. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_audio.py +0 -0
  27. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_auth.py +0 -0
  28. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_claude_stop_hook.py +0 -0
  29. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_cli.py +0 -0
  30. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_clipboard.py +0 -0
  31. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_config.py +0 -0
  32. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_install_hook.py +0 -0
  33. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_preprocess.py +0 -0
  34. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_speak_url_gate.py +0 -0
  35. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_tts.py +0 -0
  36. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/tests/test_wizard.py +0 -0
  37. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/__main__.py +0 -0
  38. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/audio.py +0 -0
  39. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/auth.py +0 -0
  40. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/cli.py +0 -0
  41. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/clipboard.py +0 -0
  42. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/config.py +0 -0
  43. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/exceptions.py +0 -0
  44. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/preprocess.py +0 -0
  45. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/tts.py +0 -0
  46. {vocalize_cli-0.7.0 → vocalize_cli-0.8.0}/vocalize/wizard.py +0 -0
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## 0.8.0 - 2026-08-31
7
+
8
+ ### Added
9
+
10
+ - The "Speak with Vocalize" and "Speak Latest Plan" Quick Actions now show
11
+ a picker when input is over the cap and overflow is `ask`: speak all,
12
+ three summary depths — light (~25s), medium (~1 min), detailed (~2.5 min)
13
+ — or truncate. Summaries are generated by piping the text through
14
+ `claude -p --model haiku` (tools denied); each is spoken with a hard
15
+ ceiling so an over-long summary can't reintroduce a long read. New
16
+ `hooks/speak_options.py` is the picker/summarizer front-end.
17
+ - `hooks/install_quick_action.py` now also resolves and bakes a `claude`
18
+ binary path (plus the PATH additions it needs under a bare Services
19
+ environment) and the helper's location. `claude` is optional — without
20
+ it, the picker just omits the three summary depths. Re-run the installer
21
+ to pick up a newly installed `claude`. Picking a summary has a few
22
+ seconds of silent cold-start before audio begins.
23
+
24
+ ## 0.7.1 - 2026-08-31
25
+
26
+ ### Added
27
+
28
+ - "Speak Latest Plan" Quick Action — a no-input Service that speaks the
29
+ newest plan under `~/.claude/plans/` on demand (dialog-asks when over
30
+ the cap). For hearing a Claude Code plan proposal before accepting it;
31
+ installed by the same `hooks/install_quick_action.py`.
32
+
6
33
  ## 0.7.0 - 2026-08-31
7
34
 
8
35
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vocalize-cli
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: A CLI that turns text, markdown, or piped stdin into speech via the ElevenLabs API, with markdown-table-aware preprocessing.
5
5
  Project-URL: Homepage, https://github.com/matthager12-collab/vocalize
6
6
  Project-URL: Repository, https://github.com/matthager12-collab/vocalize
@@ -259,10 +259,18 @@ Two Services let you use vocalize from any app without a terminal:
259
259
  - **Speak with Vocalize** — highlight text anywhere, right-click →
260
260
  Services → Speak with Vocalize. It stops whatever was already playing
261
261
  and reads the selection. If the selection is over your cap and your
262
- overflow mode is `ask`, a native dialog asks Truncate / Speak all /
263
- Cancel (that's the `--ask-dialog` flag at work).
262
+ overflow mode is `ask`, a picker offers **Speak all**, three summary
263
+ depths (**light** ~25s, **medium** ~1 min, **detailed** ~2.5 min), or
264
+ **Truncate**. Summaries are produced by `claude -p --model haiku` (with
265
+ tools denied) — picking one has a few seconds of silent cold-start
266
+ before audio begins. If no `claude` binary was found when you ran the
267
+ installer, the picker simply omits the three summary depths.
264
268
  - **Stop Vocalize** — appears in every app's application menu → Services;
265
269
  silences playback from anywhere.
270
+ - **Speak Latest Plan** — reads the newest Claude Code plan file
271
+ (`~/.claude/plans/`) aloud on demand. Made for the plan-approval moment:
272
+ the proposal card is up, you press your shortcut, hear the plan, then
273
+ accept or reject. Nothing reads unless you trigger it.
266
274
 
267
275
  Install both:
268
276
 
@@ -271,10 +279,12 @@ python3 hooks/install_quick_action.py
271
279
  ```
272
280
 
273
281
  This copies the bundles from `hooks/quick_actions/` into
274
- `~/Library/Services/` with this machine's absolute `vocalize` path baked
275
- in, then refreshes the Services registry. To trigger them from the
276
- keyboard, assign shortcuts under System Settings Keyboard Keyboard
277
- Shortcuts Services Text.
282
+ `~/Library/Services/` with this machine's absolute `vocalize`, `claude`,
283
+ and helper paths baked in, then refreshes the Services registry. Run it
284
+ from a normal terminal its PATH is what gets captured. To trigger the
285
+ actions from the keyboard, assign shortcuts under System Settings
286
+ Keyboard → Keyboard Shortcuts → Services (Stop Vocalize is worth a
287
+ shortcut of its own so you can silence a read from anywhere).
278
288
 
279
289
  Some Electron apps (Claude Code desktop among them) don't expose the
280
290
  Services menu for text selected in their own window. There, copy the
@@ -225,10 +225,18 @@ Two Services let you use vocalize from any app without a terminal:
225
225
  - **Speak with Vocalize** — highlight text anywhere, right-click →
226
226
  Services → Speak with Vocalize. It stops whatever was already playing
227
227
  and reads the selection. If the selection is over your cap and your
228
- overflow mode is `ask`, a native dialog asks Truncate / Speak all /
229
- Cancel (that's the `--ask-dialog` flag at work).
228
+ overflow mode is `ask`, a picker offers **Speak all**, three summary
229
+ depths (**light** ~25s, **medium** ~1 min, **detailed** ~2.5 min), or
230
+ **Truncate**. Summaries are produced by `claude -p --model haiku` (with
231
+ tools denied) — picking one has a few seconds of silent cold-start
232
+ before audio begins. If no `claude` binary was found when you ran the
233
+ installer, the picker simply omits the three summary depths.
230
234
  - **Stop Vocalize** — appears in every app's application menu → Services;
231
235
  silences playback from anywhere.
236
+ - **Speak Latest Plan** — reads the newest Claude Code plan file
237
+ (`~/.claude/plans/`) aloud on demand. Made for the plan-approval moment:
238
+ the proposal card is up, you press your shortcut, hear the plan, then
239
+ accept or reject. Nothing reads unless you trigger it.
232
240
 
233
241
  Install both:
234
242
 
@@ -237,10 +245,12 @@ python3 hooks/install_quick_action.py
237
245
  ```
238
246
 
239
247
  This copies the bundles from `hooks/quick_actions/` into
240
- `~/Library/Services/` with this machine's absolute `vocalize` path baked
241
- in, then refreshes the Services registry. To trigger them from the
242
- keyboard, assign shortcuts under System Settings Keyboard Keyboard
243
- Shortcuts Services Text.
248
+ `~/Library/Services/` with this machine's absolute `vocalize`, `claude`,
249
+ and helper paths baked in, then refreshes the Services registry. Run it
250
+ from a normal terminal its PATH is what gets captured. To trigger the
251
+ actions from the keyboard, assign shortcuts under System Settings
252
+ Keyboard → Keyboard Shortcuts → Services (Stop Vocalize is worth a
253
+ shortcut of its own so you can silence a read from anywhere).
244
254
 
245
255
  Some Electron apps (Claude Code desktop among them) don't expose the
246
256
  Services menu for text selected in their own window. There, copy the
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """Copy the vocalize Quick Actions into ~/Library/Services/.
3
+
4
+ Unlike install_hook.py there is no shared settings file to merge into —
5
+ each Service is a self-contained bundle, so "install" is: copy the bundle
6
+ with this machine's absolute vocalize path baked in. Re-running simply
7
+ overwrites the same two bundles.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ from xml.sax.saxutils import escape as xml_escape
18
+
19
+ TEMPLATES_DIR = Path(__file__).resolve().parent / "quick_actions"
20
+ SERVICES_DIR = Path.home() / "Library" / "Services"
21
+ BUNDLE_NAMES = (
22
+ "Speak with Vocalize.workflow",
23
+ "Stop Vocalize.workflow",
24
+ "Speak Latest Plan.workflow",
25
+ )
26
+ PLACEHOLDER = "__VOCALIZE_BIN__"
27
+ CLAUDE_PLACEHOLDER = "__CLAUDE_BIN__"
28
+ CLAUDE_EXTRA_PATH_PLACEHOLDER = "__CLAUDE_EXTRA_PATH__"
29
+ HELPER_PLACEHOLDER = "__HELPER__"
30
+ PBS = "/System/Library/CoreServices/pbs"
31
+
32
+ # Baked values land inside "..." in the Quick Action's zsh script. These
33
+ # characters could escape those quotes, so refuse rather than try to quote.
34
+ _UNSAFE_PATH_CHARS = set('"\\`$')
35
+
36
+
37
+ def _resolve_vocalize_bin() -> Path:
38
+ repo_venv = Path(__file__).resolve().parent.parent / ".venv" / "bin" / "vocalize"
39
+ if repo_venv.is_file():
40
+ return repo_venv
41
+ on_path = shutil.which("vocalize")
42
+ if on_path:
43
+ return Path(on_path).resolve()
44
+ print(
45
+ "Could not find a vocalize binary (checked .venv/bin/vocalize next to "
46
+ "this repo, then PATH). Install vocalize first.",
47
+ file=sys.stderr,
48
+ )
49
+ sys.exit(1)
50
+
51
+
52
+ def _resolve_helper() -> Path:
53
+ helper = Path(__file__).resolve().parent / "speak_options.py"
54
+ if not helper.is_file():
55
+ print(f"Could not find the picker helper at {helper}.", file=sys.stderr)
56
+ sys.exit(1)
57
+ return helper
58
+
59
+
60
+ def _resolve_claude() -> tuple[str, str]:
61
+ """Return (claude_path, extra_PATH). Empty strings when claude is absent.
62
+
63
+ claude.exe may need `node` resolved via PATH even when invoked by its
64
+ absolute path, and a bare Services environment has neither on PATH, so
65
+ bake claude's and node's directories for the helper to prepend.
66
+ """
67
+ found = shutil.which("claude")
68
+ if not found:
69
+ return "", ""
70
+ claude_path = Path(found).resolve()
71
+ dirs = [str(claude_path.parent)]
72
+ node = shutil.which("node")
73
+ if node:
74
+ node_dir = str(Path(node).resolve().parent)
75
+ if node_dir not in dirs:
76
+ dirs.append(node_dir)
77
+ return str(claude_path), os.pathsep.join(dirs)
78
+
79
+
80
+ def _install_one(name: str, substitutions: dict) -> None:
81
+ src = TEMPLATES_DIR / name
82
+ dest = SERVICES_DIR / name
83
+ if dest.exists():
84
+ shutil.rmtree(dest)
85
+ shutil.copytree(src, dest)
86
+
87
+ wflow = dest / "Contents" / "Resources" / "document.wflow"
88
+ text = wflow.read_text(encoding="utf-8")
89
+ for placeholder, value in substitutions.items():
90
+ text = text.replace(placeholder, xml_escape(value))
91
+ wflow.write_text(text, encoding="utf-8")
92
+
93
+
94
+ def main() -> int:
95
+ bin_path = str(_resolve_vocalize_bin())
96
+ helper_path = str(_resolve_helper())
97
+ claude_path, claude_extra_path = _resolve_claude()
98
+
99
+ substitutions = {
100
+ PLACEHOLDER: bin_path,
101
+ HELPER_PLACEHOLDER: helper_path,
102
+ CLAUDE_PLACEHOLDER: claude_path,
103
+ CLAUDE_EXTRA_PATH_PLACEHOLDER: claude_extra_path,
104
+ }
105
+ for value in substitutions.values():
106
+ if _UNSAFE_PATH_CHARS & set(value):
107
+ print(
108
+ f"Refusing to install: a resolved path contains characters that "
109
+ f"would break the Quick Action script: {value!r}",
110
+ file=sys.stderr,
111
+ )
112
+ return 1
113
+
114
+ SERVICES_DIR.mkdir(parents=True, exist_ok=True)
115
+ for name in BUNDLE_NAMES:
116
+ _install_one(name, substitutions)
117
+ print(f"Installed {name} -> {SERVICES_DIR / name}")
118
+
119
+ # Nudge the Services registry so the new entries appear without a logout.
120
+ subprocess.run([PBS, "-update"], check=False)
121
+
122
+ print(f"\nUsing vocalize at: {bin_path}")
123
+ if claude_path:
124
+ print(f"Summaries via claude at: {claude_path}")
125
+ else:
126
+ print("claude not found; Quick Actions will offer speak-all / truncate only.")
127
+ print('Highlight text in any app, then right-click -> Services -> "Speak with Vocalize".')
128
+ print("Keyboard shortcuts: System Settings -> Keyboard -> Keyboard Shortcuts -> Services.")
129
+ print("If the actions don't appear, re-open the Services submenu once, or log out and in.")
130
+ print("Run this installer from a normal terminal (its PATH is what gets baked in).")
131
+ return 0
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSServices</key>
6
+ <array>
7
+ <dict>
8
+ <key>NSMenuItem</key>
9
+ <dict>
10
+ <key>default</key>
11
+ <string>Speak Latest Plan</string>
12
+ </dict>
13
+ <key>NSMessage</key>
14
+ <string>runWorkflowAsService</string>
15
+ </dict>
16
+ </array>
17
+ <key>CFBundleDevelopmentRegion</key>
18
+ <string>en_US</string>
19
+ <key>CFBundleIdentifier</key>
20
+ <string>cards.arda.vocalize.speakplan</string>
21
+ <key>CFBundleName</key>
22
+ <string>Speak Latest Plan</string>
23
+ <key>CFBundleShortVersionString</key>
24
+ <string>1.0</string>
25
+ </dict>
26
+ </plist>
@@ -0,0 +1,212 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>AMApplicationBuild</key>
6
+ <string>346</string>
7
+ <key>AMApplicationVersion</key>
8
+ <string>2.3</string>
9
+ <key>AMDocumentVersion</key>
10
+ <string>2</string>
11
+ <key>actions</key>
12
+ <array>
13
+ <dict>
14
+ <key>action</key>
15
+ <dict>
16
+ <key>AMAccepts</key>
17
+ <dict>
18
+ <key>Container</key>
19
+ <string>List</string>
20
+ <key>Optional</key>
21
+ <true/>
22
+ <key>Types</key>
23
+ <array>
24
+ <string>com.apple.cocoa.string</string>
25
+ </array>
26
+ </dict>
27
+ <key>AMActionVersion</key>
28
+ <string>2.0.3</string>
29
+ <key>AMApplication</key>
30
+ <array>
31
+ <string>Automator</string>
32
+ </array>
33
+ <key>AMParameterProperties</key>
34
+ <dict>
35
+ <key>COMMAND_STRING</key>
36
+ <dict/>
37
+ <key>CheckedForUserDefaultShell</key>
38
+ <dict/>
39
+ <key>inputMethod</key>
40
+ <dict/>
41
+ <key>shell</key>
42
+ <dict/>
43
+ <key>source</key>
44
+ <dict/>
45
+ </dict>
46
+ <key>AMProvides</key>
47
+ <dict>
48
+ <key>Container</key>
49
+ <string>List</string>
50
+ <key>Types</key>
51
+ <array>
52
+ <string>com.apple.cocoa.string</string>
53
+ </array>
54
+ </dict>
55
+ <key>ActionBundlePath</key>
56
+ <string>/System/Library/Automator/Run Shell Script.action</string>
57
+ <key>ActionName</key>
58
+ <string>Run Shell Script</string>
59
+ <key>ActionParameters</key>
60
+ <dict>
61
+ <key>COMMAND_STRING</key>
62
+ <string># No input: speak the newest Claude Code plan file on demand.
63
+ BIN="__VOCALIZE_BIN__"
64
+ if [[ ! -x "$BIN" ]]; then
65
+ /usr/bin/osascript -e 'display notification "vocalize not found — re-run hooks/install_quick_action.py" with title "Vocalize"'
66
+ exit 1
67
+ fi
68
+ PLAN=$(ls -t "$HOME/.claude/plans"/*.md 2&gt;/dev/null | head -1)
69
+ if [[ -z "$PLAN" ]]; then
70
+ /usr/bin/osascript -e 'display notification "No plan files found in ~/.claude/plans." with title "Vocalize"'
71
+ exit 0
72
+ fi
73
+ "$BIN" stop &gt;/dev/null 2&gt;&amp;1
74
+ export VOCALIZE_BIN="$BIN"
75
+ export CLAUDE_BIN="__CLAUDE_BIN__"
76
+ export CLAUDE_EXTRA_PATH="__CLAUDE_EXTRA_PATH__"
77
+ exec /usr/bin/python3 "__HELPER__" &lt; "$PLAN"</string>
78
+ <key>CheckedForUserDefaultShell</key>
79
+ <false/>
80
+ <key>inputMethod</key>
81
+ <integer>1</integer>
82
+ <key>shell</key>
83
+ <string>/bin/zsh</string>
84
+ <key>source</key>
85
+ <string></string>
86
+ </dict>
87
+ <key>BundleIdentifier</key>
88
+ <string>com.apple.RunShellScript</string>
89
+ <key>CFBundleVersion</key>
90
+ <string>2.0.3</string>
91
+ <key>CanShowSelectedItemsWhenRun</key>
92
+ <false/>
93
+ <key>CanShowWhenRun</key>
94
+ <true/>
95
+ <key>Category</key>
96
+ <array>
97
+ <string>AMCategoryUtilities</string>
98
+ </array>
99
+ <key>Class Name</key>
100
+ <string>RunShellScriptAction</string>
101
+ <key>InputUUID</key>
102
+ <string>2C7F9E10-55D2-4B31-9A6E-8E0D4C71F301</string>
103
+ <key>Keywords</key>
104
+ <array>
105
+ <string>Shell</string>
106
+ <string>Script</string>
107
+ <string>Command</string>
108
+ <string>Run</string>
109
+ <string>Unix</string>
110
+ </array>
111
+ <key>OutputUUID</key>
112
+ <string>2C7F9E10-55D2-4B31-9A6E-8E0D4C71F302</string>
113
+ <key>UUID</key>
114
+ <string>2C7F9E10-55D2-4B31-9A6E-8E0D4C71F303</string>
115
+ <key>UnlocalizedApplications</key>
116
+ <array>
117
+ <string>Automator</string>
118
+ </array>
119
+ <key>arguments</key>
120
+ <dict>
121
+ <key>0</key>
122
+ <dict>
123
+ <key>default value</key>
124
+ <integer>0</integer>
125
+ <key>name</key>
126
+ <string>inputMethod</string>
127
+ <key>required</key>
128
+ <string>0</string>
129
+ <key>type</key>
130
+ <string>0</string>
131
+ <key>uuid</key>
132
+ <string>0</string>
133
+ </dict>
134
+ <key>1</key>
135
+ <dict>
136
+ <key>default value</key>
137
+ <string></string>
138
+ <key>name</key>
139
+ <string>source</string>
140
+ <key>required</key>
141
+ <string>0</string>
142
+ <key>type</key>
143
+ <string>0</string>
144
+ <key>uuid</key>
145
+ <string>1</string>
146
+ </dict>
147
+ <key>2</key>
148
+ <dict>
149
+ <key>default value</key>
150
+ <false/>
151
+ <key>name</key>
152
+ <string>CheckedForUserDefaultShell</string>
153
+ <key>required</key>
154
+ <string>0</string>
155
+ <key>type</key>
156
+ <string>0</string>
157
+ <key>uuid</key>
158
+ <string>2</string>
159
+ </dict>
160
+ <key>3</key>
161
+ <dict>
162
+ <key>default value</key>
163
+ <string></string>
164
+ <key>name</key>
165
+ <string>COMMAND_STRING</string>
166
+ <key>required</key>
167
+ <string>0</string>
168
+ <key>type</key>
169
+ <string>0</string>
170
+ <key>uuid</key>
171
+ <string>3</string>
172
+ </dict>
173
+ <key>4</key>
174
+ <dict>
175
+ <key>default value</key>
176
+ <string>/bin/sh</string>
177
+ <key>name</key>
178
+ <string>shell</string>
179
+ <key>required</key>
180
+ <string>0</string>
181
+ <key>type</key>
182
+ <string>0</string>
183
+ <key>uuid</key>
184
+ <string>4</string>
185
+ </dict>
186
+ </dict>
187
+ <key>isViewVisible</key>
188
+ <true/>
189
+ </dict>
190
+ <key>isViewVisible</key>
191
+ <true/>
192
+ </dict>
193
+ </array>
194
+ <key>connectors</key>
195
+ <dict/>
196
+ <key>workflowMetaData</key>
197
+ <dict>
198
+ <key>serviceApplicationBundleID</key>
199
+ <string></string>
200
+ <key>serviceApplicationPath</key>
201
+ <string></string>
202
+ <key>serviceInputTypeIdentifier</key>
203
+ <string>com.apple.Automator.nothing</string>
204
+ <key>serviceOutputTypeIdentifier</key>
205
+ <string>com.apple.Automator.nothing</string>
206
+ <key>serviceProcessesInput</key>
207
+ <integer>0</integer>
208
+ <key>workflowTypeIdentifier</key>
209
+ <string>com.apple.Automator.servicesMenu</string>
210
+ </dict>
211
+ </dict>
212
+ </plist>
@@ -59,15 +59,18 @@
59
59
  <key>ActionParameters</key>
60
60
  <dict>
61
61
  <key>COMMAND_STRING</key>
62
- <string># Selected text arrives on stdin. The only interpolated value is the
63
- # vocalize path, baked in by hooks/install_quick_action.py.
62
+ <string># Selected text arrives on stdin. Interpolated values (vocalize/claude
63
+ # paths, helper location) are baked in by hooks/install_quick_action.py.
64
64
  BIN="__VOCALIZE_BIN__"
65
65
  if [[ ! -x "$BIN" ]]; then
66
66
  /usr/bin/osascript -e 'display notification "vocalize not found — re-run hooks/install_quick_action.py" with title "Vocalize"'
67
67
  exit 1
68
68
  fi
69
69
  "$BIN" stop &gt;/dev/null 2&gt;&amp;1
70
- exec "$BIN" speak-file - --play --ask-dialog</string>
70
+ export VOCALIZE_BIN="$BIN"
71
+ export CLAUDE_BIN="__CLAUDE_BIN__"
72
+ export CLAUDE_EXTRA_PATH="__CLAUDE_EXTRA_PATH__"
73
+ exec /usr/bin/python3 "__HELPER__"</string>
71
74
  <key>CheckedForUserDefaultShell</key>
72
75
  <false/>
73
76
  <key>inputMethod</key>