@ciphore/radiocli 0.2.3 → 0.2.4
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.
- package/CHANGELOG.md +77 -0
- package/CONTRIBUTING.md +36 -6
- package/README.md +54 -10
- package/dist/agent/headless-host.js +39 -17
- package/dist/agent/launcher.js +6 -67
- package/dist/agent/mcp-install.js +15 -15
- package/dist/agent/service.js +3 -3
- package/dist/agent/session.js +13 -29
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/guard.js +79 -36
- package/dist/alarms/inhibitor.js +27 -22
- package/dist/alarms/power-guard-store.js +2 -10
- package/dist/alarms/runner.js +56 -25
- package/dist/alarms/schedule.js +9 -2
- package/dist/alarms/scheduler.js +194 -52
- package/dist/alarms/setup-verification.js +1 -2
- package/dist/alarms/system-volume-ownership.js +267 -0
- package/dist/alarms/system-volume.js +155 -14
- package/dist/alarms/terminal-launcher.js +76 -136
- package/dist/alarms/tui-presence.js +2 -4
- package/dist/cli.js +103 -38
- package/dist/platform/capabilities.js +53 -0
- package/dist/platform/desktop.js +36 -0
- package/dist/{player/command.js → platform/executables.js} +43 -9
- package/dist/platform/ipc.js +14 -0
- package/dist/platform/launch-command.js +135 -0
- package/dist/platform/loopback.js +44 -0
- package/dist/platform/network.js +312 -0
- package/dist/platform/packages.js +216 -0
- package/dist/platform/paths.js +40 -0
- package/dist/platform/runtime.js +67 -0
- package/dist/platform/shell.js +24 -0
- package/dist/platform/storage.js +48 -0
- package/dist/platform/support.js +214 -0
- package/dist/platform/terminal.js +63 -0
- package/dist/platform/terminals.js +203 -0
- package/dist/player/airplay-discovery.js +4 -2
- package/dist/player/backend-install.js +13 -94
- package/dist/player/command-diagnostics.js +2 -2
- package/dist/player/mpv-ipc-client.js +2 -1
- package/dist/player/player-controller.js +203 -33
- package/dist/providers/cache.js +4 -26
- package/dist/providers/radio-browser.js +152 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +79 -151
- package/dist/storage/store.js +105 -52
- package/dist/streams/import-stream.js +163 -0
- package/dist/ui/AdaptiveContent.js +33 -24
- package/dist/ui/App.js +173 -86
- package/dist/ui/AppContent.js +3 -3
- package/dist/ui/app-state.js +8 -1
- package/dist/ui/ascii.js +11 -2
- package/dist/ui/components/AdaptiveMarquee.js +6 -3
- package/dist/ui/components/Logo.js +5 -2
- package/dist/ui/components/Menu.js +2 -2
- package/dist/ui/components/ScreenHeader.js +1 -1
- package/dist/ui/components/StationList.js +6 -4
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/display-context.js +8 -11
- package/dist/ui/help-content.js +5 -5
- package/dist/ui/layout.js +5 -2
- package/dist/ui/page-footer.js +3 -3
- package/dist/ui/screen-items.js +2 -2
- package/dist/ui/screen-meta.js +1 -1
- package/dist/ui/screens/AirPlayCodeScreen.js +6 -2
- package/dist/ui/screens/AirPlaySettingsScreen.js +7 -3
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +5 -1
- package/dist/ui/screens/MapScreen.js +1 -1
- package/dist/ui/screens/NowPlayingScreen.js +4 -4
- package/dist/ui/screens/SearchScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +14 -7
- package/dist/ui/screens/StatsScreen.js +3 -1
- package/dist/ui/system-actions.js +80 -52
- package/dist/ui/terminal-renderer.js +16 -0
- package/dist/ui/use-alarm-tui.js +36 -21
- package/dist/ui/use-app-input.js +42 -22
- package/dist/ui/use-command-executor.js +31 -7
- package/dist/update-check.js +8 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,82 @@ All notable changes to RadioCLI are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.4] - 2026-09-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added a shared platform layer for runtime eligibility, capabilities, executable
|
|
13
|
+
discovery, package installation, storage paths, networking, IPC, desktop
|
|
14
|
+
helpers, and terminal launching.
|
|
15
|
+
- Added first-class Node.js 22 and 24 CI coverage for macOS Apple Silicon and
|
|
16
|
+
Intel, Ubuntu x64 and arm64, Windows x64, and Windows 11 arm64, including
|
|
17
|
+
packed installation and native mpv IPC playback checks.
|
|
18
|
+
- Added repeatable packed CLI and mpv playback checks in full FreeBSD, OpenBSD,
|
|
19
|
+
and NetBSD guest kernels, plus explicit installation and capability guidance
|
|
20
|
+
for additional Unix, WSL, Termux, Haiku, illumos, and AIX environments.
|
|
21
|
+
- Added offline, low-bandwidth, screen-reader, reduced-motion, ASCII, limited-
|
|
22
|
+
color, and noninteractive terminal modes with deterministic full, compact,
|
|
23
|
+
and micro rendering coverage.
|
|
24
|
+
- Added automatic HTTP/ICY metadata discovery when importing a direct station
|
|
25
|
+
URL from the CLI or the TUI command palette.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Centralized native package recipes and guided setup so dry runs and executed
|
|
30
|
+
commands use the same platform-specific definitions.
|
|
31
|
+
- Expanded `radiocli doctor --json` with host, runtime, network, storage,
|
|
32
|
+
terminal, playback, desktop, scheduling, and optional-feature capabilities.
|
|
33
|
+
- Reorganized installation, platform, architecture, reliability, privacy, and
|
|
34
|
+
contributor documentation as stable reference material with explicit support
|
|
35
|
+
boundaries and troubleshooting paths.
|
|
36
|
+
- Changed TUI `:import` to add custom stream URLs, renamed full-library recovery
|
|
37
|
+
to `:restore`, and consolidated shell URL additions under `radiocli import`.
|
|
38
|
+
- Placed newest imported stations after favorites and before recently played
|
|
39
|
+
stations in the Library.
|
|
40
|
+
- Changed resume for live radio to reload the active stream at its current live
|
|
41
|
+
edge instead of replaying audio accumulated in mpv's paused buffer.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Kept playback and the TUI usable when optional history, presence, cache, or
|
|
46
|
+
library writes fail, while preserving recoverable data after interrupted or
|
|
47
|
+
permission-denied writes.
|
|
48
|
+
- Hardened alarm deletion, native scheduler cleanup, terminal reopening, Alarm
|
|
49
|
+
Guard ownership, system-volume restoration, and forced-process teardown.
|
|
50
|
+
- Preserved exact arguments, data locations, and supported environment selectors
|
|
51
|
+
across macOS, Linux, BSD, and Windows child processes without forwarding
|
|
52
|
+
unrelated shell state.
|
|
53
|
+
- Improved player discovery, mpv readiness and IPC handling, provider timeouts,
|
|
54
|
+
mirror fallback, offline cache behavior, and degraded terminal rendering.
|
|
55
|
+
- Recovered ARM Linux playback from mpv audio-service initialization failures
|
|
56
|
+
by retrying once with ALSA first, retained bounded player diagnostics, and
|
|
57
|
+
stopped system-wide backend failures from skipping through every station.
|
|
58
|
+
- Replaced stale Radio Browser hosts with live DNS service discovery and added
|
|
59
|
+
a TLS-verified resolver fallback for temporary hostname failures without
|
|
60
|
+
bypassing configured proxies.
|
|
61
|
+
- Preserved full RGB theme colors when a terminal explicitly advertises
|
|
62
|
+
truecolor, even when its Node.js stream reports a conservative 256-color
|
|
63
|
+
depth; explicit 256-color, 16-color, and no-color modes remain unchanged.
|
|
64
|
+
- Kept retained Search results playable after clearing the submitted query and
|
|
65
|
+
added `Ctrl+F` to favorite the selected result while query input is active,
|
|
66
|
+
without intercepting an ordinary `f` typed into the search text.
|
|
67
|
+
- Dismissed added and removed favorite confirmations after 4.5 seconds on
|
|
68
|
+
Overview, Now Playing, Library, Explore, Search, Nearby, and country station
|
|
69
|
+
views.
|
|
70
|
+
- Marked AirPlay controls as macOS-only, hid the unsupported Settings shortcut,
|
|
71
|
+
and kept non-macOS users on their current screen with a direct platform notice.
|
|
72
|
+
- Corrected Windows terminal launches to create independent interactive console
|
|
73
|
+
handles and retain the intended user data location.
|
|
74
|
+
|
|
75
|
+
### Security
|
|
76
|
+
|
|
77
|
+
- Updated documentation-site dependencies to patched releases.
|
|
78
|
+
- Limited child-process environments to the settings required for RadioCLI data
|
|
79
|
+
identity and native integrations, excluding unrelated credentials and proxy
|
|
80
|
+
secrets.
|
|
81
|
+
- Made native cleanup and ownership checks fail closed when a job, helper,
|
|
82
|
+
process, or saved state cannot be identified safely.
|
|
83
|
+
|
|
8
84
|
## [0.2.3] - 2026-09-07
|
|
9
85
|
|
|
10
86
|
### Added
|
|
@@ -397,6 +473,7 @@ Initial public release.
|
|
|
397
473
|
[0.2.1]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.1
|
|
398
474
|
[0.2.2]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.2
|
|
399
475
|
[0.2.3]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.3
|
|
476
|
+
[0.2.4]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.4
|
|
400
477
|
[0.1.4]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.1.4
|
|
401
478
|
[0.1.3]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.1.3
|
|
402
479
|
[0.1.2]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.1.2
|
package/CONTRIBUTING.md
CHANGED
|
@@ -12,19 +12,22 @@ npm run build
|
|
|
12
12
|
npm run test
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Install `mpv` for playback controls
|
|
16
|
-
|
|
15
|
+
Install `mpv` for playback controls and metadata polling. On a macOS
|
|
16
|
+
configuration supported by Homebrew:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
brew install mpv
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
On Linux, use your package manager's `mpv` package. On Windows, use:
|
|
22
|
+
On Linux, use your package manager's `mpv` package. On Windows x64, use:
|
|
23
23
|
|
|
24
24
|
```powershell
|
|
25
25
|
winget install --id shinchiro.mpv -e
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
Use the [platform installation routes](apps/docs/content/docs/platforms.mdx#installation-and-playback-prerequisites)
|
|
29
|
+
for Windows arm64 and other package managers.
|
|
30
|
+
|
|
28
31
|
## Verification
|
|
29
32
|
|
|
30
33
|
Run these before opening a PR:
|
|
@@ -34,11 +37,19 @@ npm run check
|
|
|
34
37
|
npm run lint
|
|
35
38
|
npm run test
|
|
36
39
|
npm run build
|
|
40
|
+
npm run pack:check
|
|
41
|
+
npm run fresh:check -- --require-mpv
|
|
42
|
+
npm run check:package
|
|
43
|
+
npm run smoke:mcp
|
|
44
|
+
npm run docs:check
|
|
45
|
+
npm run docs:build
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
Install documentation dependencies with `npm --prefix apps/docs ci` first.
|
|
49
|
+
Run `npm run smoke:playback` when changing playback code. Its default fixture is
|
|
50
|
+
a local WAV file; mpv uses null audio in deterministic package
|
|
51
|
+
checks. These checks exercise the player process and controls, and do not prove
|
|
52
|
+
speaker output. `npm run smoke:data` intentionally contacts public providers.
|
|
42
53
|
|
|
43
54
|
Live provider checks are useful before releases, but ordinary pull requests
|
|
44
55
|
should stay deterministic. Run `npm run verify:release` for a full maintainer
|
|
@@ -52,9 +63,28 @@ checks.
|
|
|
52
63
|
- Never make location lookup implicit.
|
|
53
64
|
- Prefer clear terminal text over decorative filler.
|
|
54
65
|
- Keep command-line and TUI behavior aligned.
|
|
66
|
+
- Put OS decisions in `src/platform`; keep capability availability separate from
|
|
67
|
+
runtime eligibility and recorded platform verification.
|
|
55
68
|
- Add tests for parsing, storage, layout, or provider transformations when the
|
|
56
69
|
behavior can be checked without live audio.
|
|
57
70
|
|
|
71
|
+
## Platform verification
|
|
72
|
+
|
|
73
|
+
The required Node 22/24 CI matrix tests x64 and arm64 installations and real mpv
|
|
74
|
+
control. Weekly or manually triggered BSD checks execute the packed JavaScript
|
|
75
|
+
in guest kernels. Changes to script harnesses
|
|
76
|
+
also require `node --test scripts/packed-smoke.test-node.mjs scripts/install-smoke-mpv.test-node.mjs`.
|
|
77
|
+
|
|
78
|
+
Record OS release, CPU, endianness, libc, Node/npm versions, artifact hash, and
|
|
79
|
+
whether execution was native, virtualized, emulated, or mocked. An installation
|
|
80
|
+
test, native command mock, or foreign binary inspection alone cannot promote a
|
|
81
|
+
platform to supported. Follow the [support tiers](apps/docs/content/docs/platforms.mdx).
|
|
82
|
+
|
|
83
|
+
The deterministic visual fixtures cover full, compact, and micro layouts, all
|
|
84
|
+
major screens, and receiver styles. Preserve Unicode captures. When rendering
|
|
85
|
+
changes intentionally, describe the visible effect in the pull request and do
|
|
86
|
+
not update snapshots merely to hide failures.
|
|
87
|
+
|
|
58
88
|
## Issue Triage
|
|
59
89
|
|
|
60
90
|
Playback issues should include station name, country, backend, and
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Built with [Ink](https://github.com/vadimdemedes/ink),
|
|
|
26
26
|
|
|
27
27
|
## Quick start
|
|
28
28
|
|
|
29
|
-
macOS
|
|
29
|
+
On macOS configurations supported by Homebrew:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
brew install ciphore/tap/radiocli
|
|
@@ -44,11 +44,43 @@ radiocli
|
|
|
44
44
|
RadioCLI requires Node.js 22 or newer. The Homebrew formula installs `mpv` and
|
|
45
45
|
FFmpeg. After an npm install, `radiocli setup` detects the operating system and
|
|
46
46
|
package manager, lets you choose `mpv`, FFmpeg, and VLC, installs the selected
|
|
47
|
-
native tools with branded progress feedback, and
|
|
47
|
+
native tools with branded progress feedback, and checks that executables are
|
|
48
|
+
available. Play a station to verify the actual audio path. Intel or older macOS
|
|
49
|
+
installations should consult the platform matrix for alternative package routes.
|
|
48
50
|
|
|
49
51
|
See the [installation guide](apps/docs/content/docs/getting-started/install.mdx)
|
|
50
52
|
for Windows, Linux distributions, AirPlay prerequisites, and fallback players.
|
|
51
53
|
|
|
54
|
+
## Platform coverage
|
|
55
|
+
|
|
56
|
+
RadioCLI separates core playback, storage, terminal rendering, desktop helpers,
|
|
57
|
+
and native alarm services. A missing scheduler or clipboard helper leaves the
|
|
58
|
+
other features available. `radiocli doctor --json` reports each capability,
|
|
59
|
+
terminal and network policy, and runtime eligibility separately.
|
|
60
|
+
|
|
61
|
+
The [platform matrix](apps/docs/content/docs/platforms.mdx) distinguishes required
|
|
62
|
+
CI targets from verified installations, experimental community paths, and known
|
|
63
|
+
runtime blockers. Node 22 and 24 are the reference test lines. BSD package plans,
|
|
64
|
+
Termux, Haiku and illumos guidance do not imply that every OS/CPU combination has
|
|
65
|
+
been exercised. AIX playback and Haiku's packaged Node 20 remain explicit gaps.
|
|
66
|
+
Linux without a usable systemd user session can still run the player and TUI;
|
|
67
|
+
reliable background alarms require a verified native adapter.
|
|
68
|
+
|
|
69
|
+
Invocation preferences do not change saved settings:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
RADIOCLI_ASCII=1 radiocli # ASCII decoration; station names preserved
|
|
73
|
+
NO_COLOR=1 radiocli # no color or background fills
|
|
74
|
+
RADIOCLI_SCREEN_READER=1 radiocli # readable controls; no visualizer animation
|
|
75
|
+
RADIOCLI_OFFLINE=1 radiocli # cached directories and saved stations
|
|
76
|
+
RADIOCLI_LOW_BANDWIDTH=1 radiocli # cached atlas; no automatic receiver scan
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Offline mode disables directory, location, update, vote and receiver-discovery
|
|
80
|
+
requests. An external player still needs a connection to listen to a live stream.
|
|
81
|
+
For proxy, SSH, read-only storage and custom-player paths, see
|
|
82
|
+
[troubleshooting](apps/docs/content/docs/troubleshooting.mdx).
|
|
83
|
+
|
|
52
84
|
## Visual tour
|
|
53
85
|
|
|
54
86
|
These recordings come from the built TUI. Generate them locally with
|
|
@@ -81,10 +113,10 @@ These recordings come from the built TUI. Generate them locally with
|
|
|
81
113
|
| `←` / `→` or `Tab` / `Shift+Tab` | Switch screens |
|
|
82
114
|
| `↑` / `↓` or `n` / `p` | Move the selection |
|
|
83
115
|
| `Enter` | Open or tune the selection |
|
|
84
|
-
| `space` or `F8` | Pause
|
|
116
|
+
| `space` or `F8` | Pause; resume reconnects at the live edge with `mpv` |
|
|
85
117
|
| `,` / `.` or `F7` / `F9` | Previous or next station |
|
|
86
118
|
| `+` / `-` | Change volume |
|
|
87
|
-
| `f` | Save or remove a favorite |
|
|
119
|
+
| `f` (`Ctrl+F` while typing in Search) | Save or remove a favorite |
|
|
88
120
|
| `?` | Open all shortcuts and commands |
|
|
89
121
|
| `q` or `Ctrl+C` twice | Quit cleanly |
|
|
90
122
|
|
|
@@ -118,9 +150,9 @@ radiocli check # Check providers, playback tools, and the local store
|
|
|
118
150
|
radiocli doctor --json # Create a redacted support report
|
|
119
151
|
radiocli search "japan hits"
|
|
120
152
|
radiocli countries
|
|
153
|
+
radiocli import https://example.com/live.mp3
|
|
121
154
|
radiocli import stations.m3u
|
|
122
155
|
radiocli export favorites.m3u
|
|
123
|
-
radiocli add-url <stream-url> [station name]
|
|
124
156
|
radiocli alarm list
|
|
125
157
|
radiocli alarm doctor
|
|
126
158
|
radiocli update --install # Upgrade and repair enabled MCP registrations
|
|
@@ -137,7 +169,15 @@ fully quit and reopen agent clients after enabling, repairing, or upgrading.
|
|
|
137
169
|
Until that restart, even a newly created task in an already-running client will
|
|
138
170
|
not have RadioCLI's tools and may incorrectly fall back to browser playback.
|
|
139
171
|
|
|
140
|
-
RadioCLI imports `.m3u`, `.pls`, and
|
|
172
|
+
RadioCLI imports direct HTTP(S) station streams as well as `.m3u`, `.pls`, and
|
|
173
|
+
`.xspf` playlists. Direct import is useful when a station cannot be found through
|
|
174
|
+
an open radio directory—for example, an iHeartMedia station that is available
|
|
175
|
+
only through its direct stream. Find the station's direct stream URL and run
|
|
176
|
+
`radiocli import <stream-url> [station name]`, or enter
|
|
177
|
+
`:import <stream-url> [station name]` in the TUI. RadioCLI reads available
|
|
178
|
+
HTTP/ICY metadata to discover the name, codec, bitrate, and homepage; the name
|
|
179
|
+
argument is optional and overrides the published name. Imported stations appear
|
|
180
|
+
after favorites and before recently played stations. RadioCLI exports favorites
|
|
141
181
|
and imported streams as `.m3u`.
|
|
142
182
|
|
|
143
183
|
Alarms are experimental beta functionality and live under **Overview**,
|
|
@@ -214,6 +254,8 @@ complete data-flow description.
|
|
|
214
254
|
- `src/ui` — screens, input, layout, and terminal rendering
|
|
215
255
|
- `src/providers` — station directories, resolution, and caches
|
|
216
256
|
- `src/player` — playback backends, metadata, and AirPlay
|
|
257
|
+
- `src/platform` — host identity, independent capabilities, native command plans,
|
|
258
|
+
paths, terminal policy, networking and runtime eligibility
|
|
217
259
|
- `src/alarms` — schedules, native registration, Alarm Guard, and active controls
|
|
218
260
|
- `src/storage` — local library persistence and migration
|
|
219
261
|
- `apps/docs` — documentation website and manual
|
|
@@ -231,9 +273,10 @@ npm run verify
|
|
|
231
273
|
npm run dev
|
|
232
274
|
```
|
|
233
275
|
|
|
234
|
-
`npm run verify` checks types, lint, tests, the production build, and
|
|
235
|
-
|
|
236
|
-
|
|
276
|
+
`npm run verify` checks types, lint, tests, the production build, and live provider
|
|
277
|
+
data. `npm run fresh:check -- --require-mpv` tests packed installs with and without
|
|
278
|
+
optional dependencies using a local WAV, real mpv IPC controls and MCP. See
|
|
279
|
+
[contribution checks](CONTRIBUTING.md) for documentation and packaging validation.
|
|
237
280
|
|
|
238
281
|
## Contributing
|
|
239
282
|
|
|
@@ -244,9 +287,10 @@ include `radiocli check` output with playback reports.
|
|
|
244
287
|
|
|
245
288
|
- [Getting started](apps/docs/content/docs/index.mdx)
|
|
246
289
|
- [Installation](apps/docs/content/docs/getting-started/install.mdx)
|
|
290
|
+
- [Platform matrix](apps/docs/content/docs/platforms.mdx)
|
|
291
|
+
- [Troubleshooting](apps/docs/content/docs/troubleshooting.mdx)
|
|
247
292
|
- [Controls](apps/docs/content/docs/getting-started/tui-controls.mdx)
|
|
248
293
|
- [Architecture](apps/docs/content/docs/architecture.mdx)
|
|
249
|
-
- [Roadmap](apps/docs/content/docs/roadmap.mdx)
|
|
250
294
|
- [Release packaging](apps/docs/content/docs/release-packaging.mdx)
|
|
251
295
|
|
|
252
296
|
Run the documentation site locally with `npm run docs:dev`.
|
|
@@ -2,7 +2,7 @@ import { PlayerController } from '../player/player-controller.js';
|
|
|
2
2
|
import { ProviderManager } from '../providers/provider-manager.js';
|
|
3
3
|
import { JsonLibraryStore, stationKey } from '../storage/store.js';
|
|
4
4
|
import { startRadioSession } from './session.js';
|
|
5
|
-
import { detectPlaybackBackends } from '../player/backend-install.js';
|
|
5
|
+
import { airPlayMacOSOnlyMessage, detectPlaybackBackends, isAirPlayPlatformSupported } from '../player/backend-install.js';
|
|
6
6
|
export async function runHeadlessAgentHost() {
|
|
7
7
|
const store = new JsonLibraryStore();
|
|
8
8
|
const providers = new ProviderManager();
|
|
@@ -10,23 +10,43 @@ export async function runHeadlessAgentHost() {
|
|
|
10
10
|
player.refreshDetectedBackends();
|
|
11
11
|
let station = null;
|
|
12
12
|
let queue = [];
|
|
13
|
-
|
|
13
|
+
let persistenceWarning;
|
|
14
|
+
const persistHistory = (write) => {
|
|
15
|
+
try {
|
|
16
|
+
write();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const code = error?.code;
|
|
20
|
+
const detail = typeof code === 'string' && /^[A-Z][A-Z0-9_]+$/.test(code) ? ` (${code})` : '';
|
|
21
|
+
persistenceWarning = `Listening history was not saved${detail}. Check storage permissions or set RADIOCLI_HOME to a private writable directory.`;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const saveLibrary = (write) => {
|
|
25
|
+
write();
|
|
26
|
+
// Optional writes can be no-ops, so only an explicit successful save clears
|
|
27
|
+
// a prior warning. Explicit failures still reject the control request.
|
|
28
|
+
persistenceWarning = undefined;
|
|
29
|
+
};
|
|
30
|
+
const status = () => ({ owner: 'headless', playback: player.getState(), station, queue, persistenceWarning, output: {
|
|
14
31
|
preferredBackend: store.snapshot().settings.preferredBackend,
|
|
15
32
|
preferredAirPlayDevice: store.snapshot().settings.preferredAirPlayDevice
|
|
16
33
|
} });
|
|
17
34
|
const result = (message, ok = true, data) => ({ ok, message, status: status(), ...(data ? { data } : {}) });
|
|
18
35
|
const play = async (next, nextQueue = [next]) => {
|
|
19
|
-
store.finishActiveListeningSession();
|
|
36
|
+
persistHistory(() => store.finishActiveListeningSession());
|
|
20
37
|
await player.stop();
|
|
21
38
|
const resolved = await providers.resolve(next);
|
|
22
39
|
await player.play(next, resolved.url);
|
|
23
40
|
station = next;
|
|
24
41
|
queue = nextQueue.length ? nextQueue : [next];
|
|
25
|
-
store.addRecent(next);
|
|
26
|
-
store.startListeningSession(next);
|
|
42
|
+
persistHistory(() => store.addRecent(next));
|
|
43
|
+
persistHistory(() => store.startListeningSession(next));
|
|
27
44
|
return result(`Playing ${next.name}.`);
|
|
28
45
|
};
|
|
29
46
|
const handle = async (command) => {
|
|
47
|
+
if (['airplay-list', 'airplay-select', 'airplay-passcode'].includes(command.type) && !isAirPlayPlatformSupported()) {
|
|
48
|
+
return result(airPlayMacOSOnlyMessage, false, command.type === 'airplay-list' ? [] : undefined);
|
|
49
|
+
}
|
|
30
50
|
if (command.type === 'status')
|
|
31
51
|
return result(station ? `${player.getState().state}: ${station.name}` : 'RadioCLI is idle.');
|
|
32
52
|
if (command.type === 'play') {
|
|
@@ -37,17 +57,17 @@ export async function runHeadlessAgentHost() {
|
|
|
37
57
|
if (command.type === 'pause') {
|
|
38
58
|
const control = await player.pause();
|
|
39
59
|
if (control.ok)
|
|
40
|
-
store.finishActiveListeningSession();
|
|
60
|
+
persistHistory(() => store.finishActiveListeningSession());
|
|
41
61
|
return result(control.message ?? 'Paused.', control.ok);
|
|
42
62
|
}
|
|
43
63
|
if (command.type === 'resume') {
|
|
44
64
|
const control = await player.resume();
|
|
45
65
|
if (control.ok && station)
|
|
46
|
-
store.startListeningSession(station);
|
|
66
|
+
persistHistory(() => store.startListeningSession(station));
|
|
47
67
|
return result(control.message ?? 'Resumed.', control.ok);
|
|
48
68
|
}
|
|
49
69
|
if (command.type === 'stop') {
|
|
50
|
-
store.finishActiveListeningSession();
|
|
70
|
+
persistHistory(() => store.finishActiveListeningSession());
|
|
51
71
|
await player.stop();
|
|
52
72
|
station = null;
|
|
53
73
|
queue = [];
|
|
@@ -55,7 +75,7 @@ export async function runHeadlessAgentHost() {
|
|
|
55
75
|
return result('Stopped RadioCLI.');
|
|
56
76
|
}
|
|
57
77
|
if (command.type === 'alarm-preempt') {
|
|
58
|
-
store.finishActiveListeningSession();
|
|
78
|
+
persistHistory(() => store.finishActiveListeningSession());
|
|
59
79
|
await player.stop();
|
|
60
80
|
station = null;
|
|
61
81
|
queue = [];
|
|
@@ -64,7 +84,7 @@ export async function runHeadlessAgentHost() {
|
|
|
64
84
|
if (command.type === 'set-volume') {
|
|
65
85
|
const control = await player.setVolume(command.volume);
|
|
66
86
|
if (control.ok)
|
|
67
|
-
store.updateSettings({ volume: player.getState().volume });
|
|
87
|
+
saveLibrary(() => store.updateSettings({ volume: player.getState().volume }));
|
|
68
88
|
return result(control.message ?? `Volume ${player.getState().volume}.`, control.ok);
|
|
69
89
|
}
|
|
70
90
|
if (command.type === 'set-muted') {
|
|
@@ -77,7 +97,7 @@ export async function runHeadlessAgentHost() {
|
|
|
77
97
|
return result('No active station to favorite.', false);
|
|
78
98
|
const current = store.isFavorite(target);
|
|
79
99
|
if (current !== command.favorite)
|
|
80
|
-
store.toggleFavorite(target);
|
|
100
|
+
saveLibrary(() => store.toggleFavorite(target));
|
|
81
101
|
if (command.favorite && !current && store.snapshot().settings.shareDirectoryVotes)
|
|
82
102
|
void providers.vote(target);
|
|
83
103
|
return result(`${command.favorite ? 'Favorited' : 'Removed favorite'}: ${target.name}.`);
|
|
@@ -95,8 +115,10 @@ export async function runHeadlessAgentHost() {
|
|
|
95
115
|
return switchToLocal();
|
|
96
116
|
if (!detectPlaybackBackends().includes('airplay'))
|
|
97
117
|
return result('AirPlay playback is unavailable; run radiocli doctor.', false);
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
saveLibrary(() => {
|
|
119
|
+
store.updateSettings({ preferredAirPlayDevice: device.id });
|
|
120
|
+
store.updateSettings({ preferredBackend: 'airplay' });
|
|
121
|
+
});
|
|
100
122
|
if (station && ['playing', 'paused', 'loading'].includes(player.getState().state))
|
|
101
123
|
return play(station, queue);
|
|
102
124
|
return result(`Audio output set to AirPlay receiver ${device.name}.`);
|
|
@@ -108,7 +130,7 @@ export async function runHeadlessAgentHost() {
|
|
|
108
130
|
return result(control.message ?? 'AirPlay code sent.', control.ok);
|
|
109
131
|
}
|
|
110
132
|
if (command.type === 'update-settings') {
|
|
111
|
-
store.updateSettings(command.settings);
|
|
133
|
+
saveLibrary(() => store.updateSettings(command.settings));
|
|
112
134
|
return result('RadioCLI settings updated.');
|
|
113
135
|
}
|
|
114
136
|
const currentIndex = station ? queue.findIndex(item => stationKey(item) === stationKey(station)) : -1;
|
|
@@ -121,7 +143,7 @@ export async function runHeadlessAgentHost() {
|
|
|
121
143
|
const backend = backends.includes('mpv') ? 'mpv' : backends.includes('ffplay') ? 'ffplay' : backends.includes('vlc') ? 'vlc' : null;
|
|
122
144
|
if (!backend)
|
|
123
145
|
return result('No local playback backend is available. Run radiocli setup to install mpv.', false);
|
|
124
|
-
store.updateSettings({ preferredBackend: backend });
|
|
146
|
+
saveLibrary(() => store.updateSettings({ preferredBackend: backend }));
|
|
125
147
|
if (station && ['playing', 'paused', 'loading'].includes(player.getState().state))
|
|
126
148
|
return play(station, queue);
|
|
127
149
|
return result(`Audio output set to this device (${backend}).`);
|
|
@@ -129,11 +151,11 @@ export async function runHeadlessAgentHost() {
|
|
|
129
151
|
const session = await startRadioSession(handle);
|
|
130
152
|
const checkpoint = setInterval(() => {
|
|
131
153
|
if (player.getState().state === 'playing')
|
|
132
|
-
store.checkpointActiveListeningSession();
|
|
154
|
+
persistHistory(() => store.checkpointActiveListeningSession());
|
|
133
155
|
}, 30_000);
|
|
134
156
|
const shutdown = async () => {
|
|
135
157
|
clearInterval(checkpoint);
|
|
136
|
-
store.finishActiveListeningSession();
|
|
158
|
+
persistHistory(() => store.finishActiveListeningSession());
|
|
137
159
|
await player.stop();
|
|
138
160
|
await session.close();
|
|
139
161
|
};
|
package/dist/agent/launcher.js
CHANGED
|
@@ -1,71 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const terminal = detectAlarmTerminal();
|
|
8
|
-
const direct = environmentCommand(nodePath, cliPath, ['agent-ui', encodedCommand]);
|
|
9
|
-
const command = `${direct.map(shellQuote).join(' ')}`;
|
|
10
|
-
if (terminal === 'darwin:apple-terminal')
|
|
11
|
-
await launched(spawnDetached('/usr/bin/osascript', appleTerminalScript(command)));
|
|
12
|
-
else if (terminal === 'darwin:iterm')
|
|
13
|
-
await launched(spawnDetached('/usr/bin/osascript', iTermScript(command)));
|
|
14
|
-
else if (terminal === 'darwin:wezterm')
|
|
15
|
-
await launched(spawnDetached('/usr/bin/open', ['-na', 'WezTerm', '--args', 'start', '--always-new-process', '--', ...direct]));
|
|
16
|
-
else if (terminal === 'darwin:ghostty')
|
|
17
|
-
await launched(spawnDetached('/usr/bin/open', ['-na', 'Ghostty', '--args', '-e', ...direct]));
|
|
18
|
-
else if (terminal === 'darwin:kitty')
|
|
19
|
-
await launched(spawnDetached('/usr/bin/open', ['-na', 'kitty', '--args', '--detach', ...direct]));
|
|
20
|
-
else if (terminal === 'win32:windows-terminal')
|
|
21
|
-
await launched(spawnDetached('wt.exe', ['-w', 'new', 'new-tab', '--title', 'RadioCLI', ...direct]));
|
|
22
|
-
else if (terminal === 'win32:console')
|
|
23
|
-
await launched(spawnDetached('cmd.exe', ['/d', '/c', 'start', 'RadioCLI', 'cmd.exe', '/k', windowsCommand(direct)]));
|
|
24
|
-
else if (terminal.startsWith('linux:')) {
|
|
25
|
-
const executable = terminal.slice('linux:'.length);
|
|
26
|
-
if (!linuxTerminals.has(basename(executable)))
|
|
27
|
-
throw new Error('Saved Linux terminal is not supported.');
|
|
28
|
-
const name = basename(executable);
|
|
29
|
-
const prefix = name === 'gnome-terminal' || name === 'mate-terminal' || name === 'xfce4-terminal'
|
|
30
|
-
? ['--']
|
|
31
|
-
: name === 'wezterm' ? ['start', '--always-new-process', '--'] : ['-e'];
|
|
32
|
-
await launched(spawnDetached(executable, [...prefix, ...direct]));
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
throw new Error('No supported graphical terminal was found. Set agentControl.openUiOnPlay to false or open radiocli manually.');
|
|
36
|
-
}
|
|
2
|
+
import { launchTerminalCommand } from '../platform/terminals.js';
|
|
3
|
+
import { waitForLaunch } from '../platform/launch-command.js';
|
|
4
|
+
/** The agent service verifies the authenticated session after this request. */
|
|
5
|
+
export async function launchRadioTui(nodePath, cliPath, encodedCommand, options = {}) {
|
|
6
|
+
await launchTerminalCommand({ ...options, nodePath, args: [cliPath, 'agent-ui', encodedCommand], title: 'RadioCLI' });
|
|
37
7
|
}
|
|
38
8
|
export async function launchHeadlessHost(nodePath, cliPath) {
|
|
39
|
-
await
|
|
40
|
-
}
|
|
41
|
-
function environmentCommand(nodePath, cliPath, args) {
|
|
42
|
-
const command = [nodePath, cliPath, ...args];
|
|
43
|
-
const radioCliHome = process.env.RADIOCLI_HOME;
|
|
44
|
-
if (!radioCliHome)
|
|
45
|
-
return command;
|
|
46
|
-
if (process.platform === 'win32') {
|
|
47
|
-
return ['cmd.exe', '/d', '/c', `set "RADIOCLI_HOME=${cmdEscape(radioCliHome)}" && ${windowsCommand(command)}`];
|
|
48
|
-
}
|
|
49
|
-
return ['/usr/bin/env', `RADIOCLI_HOME=${radioCliHome}`, ...command];
|
|
50
|
-
}
|
|
51
|
-
function shellQuote(value) { return `'${value.replaceAll("'", `'\\''`)}'`; }
|
|
52
|
-
function windowsCommand(values) { return values.map(value => `"${value.replaceAll('"', '""')}"`).join(' '); }
|
|
53
|
-
function cmdEscape(value) { return value.replaceAll('%', '%%').replaceAll('"', '""').replaceAll('^', '^^').replaceAll('&', '^&').replaceAll('|', '^|').replaceAll('<', '^<').replaceAll('>', '^>'); }
|
|
54
|
-
function appleTerminalScript(command) { return ['-e', 'on run argv', '-e', 'tell application "Terminal"', '-e', 'activate', '-e', 'do script (item 1 of argv)', '-e', 'end tell', '-e', 'end run', command]; }
|
|
55
|
-
function iTermScript(command) { return ['-e', 'on run argv', '-e', 'tell application "iTerm"', '-e', 'activate', '-e', 'set w to (create window with default profile)', '-e', 'tell current session of w to write text (item 1 of argv)', '-e', 'end tell', '-e', 'end run', command]; }
|
|
56
|
-
function spawnDetached(command, args) { return spawn(command, [...args], { detached: true, stdio: 'ignore', windowsHide: false }); }
|
|
57
|
-
function launched(child) { return new Promise((resolve, reject) => { child.once('error', reject); child.once('spawn', () => { child.unref(); resolve(); }); }); }
|
|
58
|
-
export function resolveExecutable(input, env = process.env, platform = process.platform) {
|
|
59
|
-
if ((input.includes('/') || input.includes('\\')) && existsSync(input))
|
|
60
|
-
return input;
|
|
61
|
-
const suffixes = platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
62
|
-
const pathDelimiter = platform === 'win32' ? ';' : ':';
|
|
63
|
-
for (const directory of (env.PATH ?? '').split(pathDelimiter)) {
|
|
64
|
-
for (const suffix of suffixes) {
|
|
65
|
-
const path = join(directory, `${input}${suffix}`);
|
|
66
|
-
if (existsSync(path))
|
|
67
|
-
return path;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
9
|
+
await waitForLaunch(spawn(nodePath, [cliPath, 'agent-host'], { detached: true, stdio: 'ignore', windowsHide: false }));
|
|
71
10
|
}
|
|
@@ -5,7 +5,7 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
6
6
|
import { JsonLibraryStore } from '../storage/store.js';
|
|
7
7
|
import { defaultAgentControlSettings } from '../types.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveCommandDetails } from '../platform/executables.js';
|
|
9
9
|
const formatting = { insertSpaces: true, tabSize: 2, eol: '\n' };
|
|
10
10
|
export async function configureMcpIntegrations(enabled, runtime, output = process.stdout) {
|
|
11
11
|
const store = new JsonLibraryStore();
|
|
@@ -31,7 +31,7 @@ export async function configureMcpIntegrations(enabled, runtime, output = proces
|
|
|
31
31
|
}
|
|
32
32
|
else
|
|
33
33
|
results.push(notFound('Codex'));
|
|
34
|
-
const claude =
|
|
34
|
+
const claude = resolveCommandDetails('claude', { env: process.env }).path;
|
|
35
35
|
if (claude) {
|
|
36
36
|
results.push(await configureCliClient('Claude Code', claude, enabled
|
|
37
37
|
? ['mcp', 'add', '--scope', 'user', 'radiocli', '--', ...command]
|
|
@@ -40,7 +40,7 @@ export async function configureMcpIntegrations(enabled, runtime, output = proces
|
|
|
40
40
|
else
|
|
41
41
|
results.push(notFound('Claude Code'));
|
|
42
42
|
const openCodePath = openCodeConfigPath();
|
|
43
|
-
if (
|
|
43
|
+
if (resolveCommandDetails('opencode', { env: process.env }).path || existsSync(openCodePath)) {
|
|
44
44
|
try {
|
|
45
45
|
updateOpenCodeConfig(openCodePath, enabled, command);
|
|
46
46
|
results.push({ client: 'OpenCode', status: enabled ? 'configured' : 'removed', detail: openCodePath });
|
|
@@ -54,13 +54,13 @@ export async function configureMcpIntegrations(enabled, runtime, output = proces
|
|
|
54
54
|
configureJsonClient(results, 'Cursor', cursorDetected(), join(homedir(), '.cursor', 'mcp.json'), ['mcpServers', 'radiocli'], enabled, {
|
|
55
55
|
command: command[0], args: command.slice(1)
|
|
56
56
|
});
|
|
57
|
-
configureJsonClient(results, 'Gemini CLI', Boolean(
|
|
57
|
+
configureJsonClient(results, 'Gemini CLI', Boolean(resolveCommandDetails('gemini', { env: process.env }).path), join(homedir(), '.gemini', 'settings.json'), ['mcpServers', 'radiocli'], enabled, {
|
|
58
58
|
command: command[0], args: command.slice(1)
|
|
59
59
|
});
|
|
60
60
|
configureJsonClient(results, 'VS Code / Copilot Agent Host', vsCodeDetected(), join(homedir(), '.copilot', 'mcp-config.json'), ['servers', 'radiocli'], enabled, {
|
|
61
61
|
type: 'stdio', command: command[0], args: command.slice(1)
|
|
62
62
|
});
|
|
63
|
-
if (
|
|
63
|
+
if (resolveCommandDetails('orca', { env: process.env }).path) {
|
|
64
64
|
results.push({ client: 'Orca', status: 'inherited', detail: 'Orca exposes MCP integrations through its configured Codex/Claude agent runtimes.' });
|
|
65
65
|
}
|
|
66
66
|
else
|
|
@@ -182,7 +182,7 @@ export function probeMcpServer(command, timeoutMs = 5_000) {
|
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
function matchingRadioCliShim(runtime, platform) {
|
|
185
|
-
const candidate =
|
|
185
|
+
const candidate = resolveCommandDetails('radiocli', { env: process.env, platform }).path;
|
|
186
186
|
if (!candidate)
|
|
187
187
|
return undefined;
|
|
188
188
|
if (!usableDirectLauncher(candidate, platform))
|
|
@@ -204,16 +204,16 @@ async function mcpClientStates(command) {
|
|
|
204
204
|
const vscodePath = join(homedir(), '.copilot', 'mcp-config.json');
|
|
205
205
|
return {
|
|
206
206
|
codex: await codexClientState(resolveCodexExecutable(), command),
|
|
207
|
-
claude: await claudeClientState(
|
|
208
|
-
opencode: jsonClientState(Boolean(
|
|
207
|
+
claude: await claudeClientState(resolveCommandDetails('claude', { env: process.env }).path, command),
|
|
208
|
+
opencode: jsonClientState(Boolean(resolveCommandDetails('opencode', { env: process.env }).path || existsSync(openCodePath)), openCodePath, [
|
|
209
209
|
['mcp', 'servers', 'radiocli'], ['mcp', 'radiocli']
|
|
210
210
|
], command),
|
|
211
211
|
cursor: jsonClientState(cursorDetected(), cursorPath, [['mcpServers', 'radiocli']], command),
|
|
212
|
-
gemini: jsonClientState(Boolean(
|
|
212
|
+
gemini: jsonClientState(Boolean(resolveCommandDetails('gemini', { env: process.env }).path), geminiPath, [['mcpServers', 'radiocli']], command),
|
|
213
213
|
vscode: jsonClientState(vsCodeDetected(), vscodePath, [['servers', 'radiocli']], command),
|
|
214
214
|
orca: {
|
|
215
|
-
detected: Boolean(
|
|
216
|
-
state:
|
|
215
|
+
detected: Boolean(resolveCommandDetails('orca', { env: process.env }).path),
|
|
216
|
+
state: resolveCommandDetails('orca', { env: process.env }).path ? 'managed-by-client' : 'not-found',
|
|
217
217
|
detail: 'inherits MCP configuration from its Codex or Claude runtime'
|
|
218
218
|
},
|
|
219
219
|
portable: jsonClientState(true, portableConfigPath(), [['mcpServers', 'radiocli']], command)
|
|
@@ -226,7 +226,7 @@ export function resolveCodexExecutable(env = process.env, platform = process.pla
|
|
|
226
226
|
const configured = env.CODEX_CLI_PATH;
|
|
227
227
|
if (configured && existsSync(configured))
|
|
228
228
|
return configured;
|
|
229
|
-
const pathExecutable =
|
|
229
|
+
const pathExecutable = resolveCommandDetails('codex', { env, platform, home }).path;
|
|
230
230
|
if (pathExecutable)
|
|
231
231
|
return pathExecutable;
|
|
232
232
|
const candidates = platform === 'darwin'
|
|
@@ -243,14 +243,14 @@ export function resolveCodexExecutable(env = process.env, platform = process.pla
|
|
|
243
243
|
return candidates.find(candidate => existsSync(candidate));
|
|
244
244
|
}
|
|
245
245
|
function cursorDetected() {
|
|
246
|
-
return Boolean(
|
|
247
|
-
|
|
246
|
+
return Boolean(resolveCommandDetails('cursor', { env: process.env }).path ||
|
|
247
|
+
resolveCommandDetails('cursor-agent', { env: process.env }).path ||
|
|
248
248
|
existsSync(join(homedir(), '.cursor')) ||
|
|
249
249
|
(process.platform === 'darwin' && existsSync('/Applications/Cursor.app')) ||
|
|
250
250
|
(process.platform === 'win32' && existsSync(join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'Programs', 'cursor', 'Cursor.exe'))));
|
|
251
251
|
}
|
|
252
252
|
function vsCodeDetected() {
|
|
253
|
-
return Boolean(
|
|
253
|
+
return Boolean(resolveCommandDetails('code', { env: process.env }).path ||
|
|
254
254
|
(process.platform === 'darwin' && existsSync('/Applications/Visual Studio Code.app')) ||
|
|
255
255
|
(process.platform === 'win32' && existsSync(join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'Programs', 'Microsoft VS Code', 'Code.exe'))));
|
|
256
256
|
}
|
package/dist/agent/service.js
CHANGED
|
@@ -7,7 +7,7 @@ import { connectActiveAlarms } from '../alarms/active-session.js';
|
|
|
7
7
|
import { connectRadioSession, ensureRadioSession } from './session.js';
|
|
8
8
|
import { launchHeadlessHost, launchRadioTui } from './launcher.js';
|
|
9
9
|
import { PlayerController } from '../player/player-controller.js';
|
|
10
|
-
import { detectPlaybackBackends } from '../player/backend-install.js';
|
|
10
|
+
import { airPlayMacOSOnlyMessage, detectPlaybackBackends, isAirPlayPlatformSupported } from '../player/backend-install.js';
|
|
11
11
|
import { AgentAlarmService } from './alarm-service.js';
|
|
12
12
|
import { appVersion } from '../version.js';
|
|
13
13
|
import { checkForUpdate, updateCommandForInstall } from '../update-check.js';
|
|
@@ -317,8 +317,8 @@ export class AgentRadioService {
|
|
|
317
317
|
throw new Error('Agent control is disabled. Run radiocli mcp enable or enable it in Settings.');
|
|
318
318
|
}
|
|
319
319
|
assertAirPlayPlatform() {
|
|
320
|
-
if (
|
|
321
|
-
throw new Error(
|
|
320
|
+
if (!isAirPlayPlatformSupported())
|
|
321
|
+
throw new Error(airPlayMacOSOnlyMessage);
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
function opaqueStationId(station) { return `${station.provider}:${station.id}`; }
|