@ciphore/radiocli 0.2.2 → 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 +124 -1
- package/CONTRIBUTING.md +36 -6
- package/README.md +84 -10
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +165 -0
- package/dist/agent/launcher.js +10 -0
- package/dist/agent/mcp-install.js +467 -0
- package/dist/agent/mcp-server.js +139 -0
- package/dist/agent/service.js +347 -0
- package/dist/agent/session.js +232 -0
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/cli.js +4 -1
- 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 +96 -48
- 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 +157 -35
- 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 +220 -31
- package/dist/providers/cache.js +4 -26
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +156 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +149 -152
- package/dist/storage/store.js +138 -53
- package/dist/streams/import-stream.js +163 -0
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +54 -30
- package/dist/ui/App.js +466 -98
- package/dist/ui/AppContent.js +4 -4
- 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 +8 -8
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/components/VersionIndicator.js +19 -0
- 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 +5 -3
- package/dist/ui/screen-items.js +42 -11
- 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/AlarmsScreen.js +2 -1
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +7 -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 +80 -56
- 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 +40 -21
- package/dist/ui/use-app-input.js +69 -23
- package/dist/ui/use-command-executor.js +31 -7
- package/dist/ui/visualizers/gallop.js +118 -0
- package/dist/ui/visualizers/horse-stride.js +20 -0
- package/dist/ui/visualizers/receiver-style-registry.js +12 -2
- package/dist/ui/visualizers/receiver-visualizers.js +3 -0
- package/dist/ui/visualizers/retro-receivers.js +4 -0
- package/dist/ui/visualizers/terminal-receivers.js +57 -0
- package/dist/update-check.js +33 -23
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,128 @@ 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
|
-
## [
|
|
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
|
+
|
|
84
|
+
## [0.2.3] - 2026-09-07
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
- Added opt-in local coding-agent control through both a typed stdio MCP server
|
|
89
|
+
and `radiocli agent` shell commands, covering discovery, playback, favorites,
|
|
90
|
+
listening stats, appearance, and configurable task-completion actions.
|
|
91
|
+
- Added automatic MCP setup for detected Codex, Claude Code, OpenCode, Cursor,
|
|
92
|
+
Gemini CLI, VS Code/Copilot Agent Host, and Orca-backed runtimes, plus a
|
|
93
|
+
portable configuration for other MCP clients.
|
|
94
|
+
- Added typed MCP and voice-accessible alarm management, active ringing
|
|
95
|
+
controls, and macOS AirPlay discovery, receiver selection, local-output
|
|
96
|
+
switching, and ephemeral passcode submission.
|
|
97
|
+
- Added `radiocli update --install`, package-manager-aware upgrade commands,
|
|
98
|
+
an in-app update indicator, read-only MCP update status, and
|
|
99
|
+
`radiocli mcp repair`.
|
|
100
|
+
- Added Agent settings for installing or repairing MCP integrations and choosing
|
|
101
|
+
between a visible terminal player and headless playback.
|
|
102
|
+
- Added the animated Gallop receiver as the new default and a color-field
|
|
103
|
+
Crossfade receiver.
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- Agent playback continues to open the terminal TUI by default on every platform,
|
|
108
|
+
while interactive setup now explains the macOS Automation prompt and offers an
|
|
109
|
+
explicit headless opt-out. MCP guidance prevents ordinary task-completion radio
|
|
110
|
+
actions from creating implicit goals or monitors.
|
|
111
|
+
- MCP registrations now use upgrade-stable, directly executable launchers,
|
|
112
|
+
including a shell-free Node command on Windows, and enabled registrations are
|
|
113
|
+
repaired after an in-app or CLI update.
|
|
114
|
+
- Active interactive or agent playback now yields cleanly when an alarm begins,
|
|
115
|
+
preventing overlapping streams while preserving the alarm if TUI handoff fails.
|
|
116
|
+
- Kept the Countries station-count column fixed while scrolling, added focused
|
|
117
|
+
marquee reading for overlong country and station names across Explore,
|
|
118
|
+
Library, and Nearby, and aligned Overview descriptions behind a compact
|
|
119
|
+
separator-free title column.
|
|
120
|
+
|
|
121
|
+
### Security
|
|
122
|
+
|
|
123
|
+
- Agent playback uses a single-owner, authenticated loopback session with
|
|
124
|
+
serialized commands, private discovery files, and strict isolation from
|
|
125
|
+
active alarm sessions. Agent tools accept saved or RadioCLI-discovered station
|
|
126
|
+
IDs rather than arbitrary stream URLs.
|
|
127
|
+
- Alarm removal requires an exact ID and explicit confirmation. AirPlay receiver
|
|
128
|
+
IDs must come from local discovery, and receiver passcodes are never persisted
|
|
129
|
+
or echoed by agent responses.
|
|
9
130
|
|
|
10
131
|
## [0.2.2] - 2026-09-04
|
|
11
132
|
|
|
@@ -351,6 +472,8 @@ Initial public release.
|
|
|
351
472
|
[0.2.0]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.0
|
|
352
473
|
[0.2.1]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.1
|
|
353
474
|
[0.2.2]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.2.2
|
|
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
|
|
354
477
|
[0.1.4]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.1.4
|
|
355
478
|
[0.1.3]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.1.3
|
|
356
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
|
|
|
@@ -94,20 +126,58 @@ covers filters, playback, media-key learning, AirPlay, and every command.
|
|
|
94
126
|
|
|
95
127
|
## CLI
|
|
96
128
|
|
|
129
|
+
RadioCLI can also be controlled by local coding agents. Run `radiocli setup`
|
|
130
|
+
and opt into MCP, or use `radiocli setup --mcp`, to configure detected Codex,
|
|
131
|
+
Claude, OpenCode, Cursor, Gemini, VS Code/Copilot, and Orca-backed runtimes
|
|
132
|
+
automatically. From the TUI, open **Settings → Agent control & MCP** and turn on
|
|
133
|
+
**Allow local agent control** for the same one-step setup—no terminal command is
|
|
134
|
+
required. The same actions are
|
|
135
|
+
available to non-MCP harnesses through `radiocli agent` and `radiocli alarm`:
|
|
136
|
+
search and play stations, browse recents/favorites/nearby stations, favorite the
|
|
137
|
+
current station, inspect listening stats, create and control alarms, control
|
|
138
|
+
macOS AirPlay, change the receiver look, or run a user-configured completion
|
|
139
|
+
preset. Codex Voice can invoke those same typed MCP tools. Agent playback opens
|
|
140
|
+
the normal RadioCLI TUI in a terminal window by default on macOS, Windows, and
|
|
141
|
+
Linux. Interactive setup explains that macOS may require the agent host to approve
|
|
142
|
+
a one-time Automation prompt; users can explicitly opt into headless playback.
|
|
143
|
+
Scheduled alarms remain isolated and always use local output rather than
|
|
144
|
+
unattended AirPlay. See the
|
|
145
|
+
[CLI guide](apps/docs/content/docs/getting-started/cli.mdx#local-agent-and-mcp-control).
|
|
146
|
+
|
|
97
147
|
```bash
|
|
98
148
|
radiocli # Start the TUI
|
|
99
149
|
radiocli check # Check providers, playback tools, and the local store
|
|
100
150
|
radiocli doctor --json # Create a redacted support report
|
|
101
151
|
radiocli search "japan hits"
|
|
102
152
|
radiocli countries
|
|
153
|
+
radiocli import https://example.com/live.mp3
|
|
103
154
|
radiocli import stations.m3u
|
|
104
155
|
radiocli export favorites.m3u
|
|
105
|
-
radiocli add-url <stream-url> [station name]
|
|
106
156
|
radiocli alarm list
|
|
107
157
|
radiocli alarm doctor
|
|
158
|
+
radiocli update --install # Upgrade and repair enabled MCP registrations
|
|
108
159
|
```
|
|
109
160
|
|
|
110
|
-
|
|
161
|
+
Existing installations keep their library and preferences when upgraded. Use
|
|
162
|
+
`radiocli update` to inspect the detected Homebrew, npm, pnpm, or Bun command,
|
|
163
|
+
or `radiocli update --install` to run it. MCP registrations use an
|
|
164
|
+
upgrade-stable launcher so versioned package paths do not break them. On
|
|
165
|
+
Windows this is a shell-free Node command; on Unix it uses the `radiocli` shim.
|
|
166
|
+
Run `radiocli mcp status` to inspect registrations and `radiocli mcp repair`
|
|
167
|
+
after moving an installation or when an agent client reports a stale server;
|
|
168
|
+
fully quit and reopen agent clients after enabling, repairing, or upgrading.
|
|
169
|
+
Until that restart, even a newly created task in an already-running client will
|
|
170
|
+
not have RadioCLI's tools and may incorrectly fall back to browser playback.
|
|
171
|
+
|
|
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
|
|
111
181
|
and imported streams as `.m3u`.
|
|
112
182
|
|
|
113
183
|
Alarms are experimental beta functionality and live under **Overview**,
|
|
@@ -184,6 +254,8 @@ complete data-flow description.
|
|
|
184
254
|
- `src/ui` — screens, input, layout, and terminal rendering
|
|
185
255
|
- `src/providers` — station directories, resolution, and caches
|
|
186
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
|
|
187
259
|
- `src/alarms` — schedules, native registration, Alarm Guard, and active controls
|
|
188
260
|
- `src/storage` — local library persistence and migration
|
|
189
261
|
- `apps/docs` — documentation website and manual
|
|
@@ -201,9 +273,10 @@ npm run verify
|
|
|
201
273
|
npm run dev
|
|
202
274
|
```
|
|
203
275
|
|
|
204
|
-
`npm run verify` checks types, lint, tests, the production build, and
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
207
280
|
|
|
208
281
|
## Contributing
|
|
209
282
|
|
|
@@ -214,9 +287,10 @@ include `radiocli check` output with playback reports.
|
|
|
214
287
|
|
|
215
288
|
- [Getting started](apps/docs/content/docs/index.mdx)
|
|
216
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)
|
|
217
292
|
- [Controls](apps/docs/content/docs/getting-started/tui-controls.mdx)
|
|
218
293
|
- [Architecture](apps/docs/content/docs/architecture.mdx)
|
|
219
|
-
- [Roadmap](apps/docs/content/docs/roadmap.mdx)
|
|
220
294
|
- [Release packaging](apps/docs/content/docs/release-packaging.mdx)
|
|
221
295
|
|
|
222
296
|
Run the documentation site locally with `npm run docs:dev`.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { canonicalizeAlarmTime, canonicalizeIsoWeekdays, canonicalizeTimeZone, nextOccurrenceForAlarm } from '../alarms/schedule.js';
|
|
2
|
+
import { connectActiveAlarms } from '../alarms/active-session.js';
|
|
3
|
+
import { createSchedulerService } from '../alarms/scheduler.js';
|
|
4
|
+
export class AgentAlarmService {
|
|
5
|
+
store;
|
|
6
|
+
resolveStation;
|
|
7
|
+
scheduler;
|
|
8
|
+
handoffToInteractive;
|
|
9
|
+
constructor(store, resolveStation, scheduler = createSchedulerService(), handoffToInteractive) {
|
|
10
|
+
this.store = store;
|
|
11
|
+
this.resolveStation = resolveStation;
|
|
12
|
+
this.scheduler = scheduler;
|
|
13
|
+
this.handoffToInteractive = handoffToInteractive;
|
|
14
|
+
}
|
|
15
|
+
async list() {
|
|
16
|
+
return Promise.all(this.store.listAlarms().map(alarm => this.describe(alarm)));
|
|
17
|
+
}
|
|
18
|
+
async status() {
|
|
19
|
+
const alarms = this.store.listAlarms();
|
|
20
|
+
return {
|
|
21
|
+
active: await this.activeStatuses(),
|
|
22
|
+
scheduler: await this.scheduler.runtimeStatus(alarms),
|
|
23
|
+
alarms: await Promise.all(alarms.map(alarm => this.describe(alarm)))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async create(input) {
|
|
27
|
+
const station = await this.requiredStation(input.stationId);
|
|
28
|
+
const fallbackStation = input.fallbackStationId
|
|
29
|
+
? await this.requiredStation(input.fallbackStationId)
|
|
30
|
+
: undefined;
|
|
31
|
+
const alarmInput = {
|
|
32
|
+
label: cleanLabel(input.label ?? station.name),
|
|
33
|
+
enabled: input.enabled ?? true,
|
|
34
|
+
station,
|
|
35
|
+
schedule: normalizeSchedule(input.schedule),
|
|
36
|
+
playback: {
|
|
37
|
+
volume: boundedInteger(input.volume ?? 40, 'volume', 0, 100),
|
|
38
|
+
fadeSeconds: boundedInteger(input.fadeSeconds ?? 0, 'fadeSeconds', 0, 3600),
|
|
39
|
+
stopAfterMinutes: boundedInteger(input.stopAfterMinutes ?? 60, 'stopAfterMinutes', 1, 10080),
|
|
40
|
+
...(fallbackStation ? { fallbackStation } : {})
|
|
41
|
+
},
|
|
42
|
+
reliability: {
|
|
43
|
+
missedRunGraceMinutes: boundedInteger(input.missedRunGraceMinutes ?? 10, 'missedRunGraceMinutes', 0, 10080),
|
|
44
|
+
wakeIfSupported: input.wakeIfSupported ?? false,
|
|
45
|
+
keepAwakeUntilAlarm: input.keepAwakeUntilAlarm ?? false
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const alarm = this.store.addAlarm(alarmInput);
|
|
49
|
+
await this.syncSaved(alarm);
|
|
50
|
+
return this.describe(alarm);
|
|
51
|
+
}
|
|
52
|
+
async update(id, input) {
|
|
53
|
+
const alarm = this.requiredAlarm(id);
|
|
54
|
+
const station = input.stationId ? await this.requiredStation(input.stationId) : alarm.station;
|
|
55
|
+
const fallbackStation = input.clearFallback
|
|
56
|
+
? undefined
|
|
57
|
+
: input.fallbackStationId ? await this.requiredStation(input.fallbackStationId) : alarm.playback.fallbackStation;
|
|
58
|
+
const updated = this.store.updateAlarm(alarm.id, {
|
|
59
|
+
label: input.label === undefined ? alarm.label : cleanLabel(input.label),
|
|
60
|
+
enabled: input.enabled ?? alarm.enabled,
|
|
61
|
+
station,
|
|
62
|
+
schedule: input.schedule ? normalizeSchedule(input.schedule) : alarm.schedule,
|
|
63
|
+
playback: {
|
|
64
|
+
volume: boundedInteger(input.volume ?? alarm.playback.volume, 'volume', 0, 100),
|
|
65
|
+
fadeSeconds: boundedInteger(input.fadeSeconds ?? alarm.playback.fadeSeconds, 'fadeSeconds', 0, 3600),
|
|
66
|
+
stopAfterMinutes: boundedInteger(input.stopAfterMinutes ?? alarm.playback.stopAfterMinutes, 'stopAfterMinutes', 1, 10080),
|
|
67
|
+
...(fallbackStation ? { fallbackStation } : {})
|
|
68
|
+
},
|
|
69
|
+
reliability: {
|
|
70
|
+
missedRunGraceMinutes: boundedInteger(input.missedRunGraceMinutes ?? alarm.reliability.missedRunGraceMinutes, 'missedRunGraceMinutes', 0, 10080),
|
|
71
|
+
wakeIfSupported: input.wakeIfSupported ?? alarm.reliability.wakeIfSupported,
|
|
72
|
+
keepAwakeUntilAlarm: input.keepAwakeUntilAlarm ?? alarm.reliability.keepAwakeUntilAlarm
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
await this.syncSaved(updated);
|
|
76
|
+
return this.describe(updated);
|
|
77
|
+
}
|
|
78
|
+
async setEnabled(id, enabled) {
|
|
79
|
+
const alarm = this.store.toggleAlarm(this.requiredAlarm(id).id, enabled);
|
|
80
|
+
await this.syncSaved(alarm);
|
|
81
|
+
return this.describe(alarm);
|
|
82
|
+
}
|
|
83
|
+
async remove(id, confirm) {
|
|
84
|
+
if (!confirm)
|
|
85
|
+
throw new Error('Alarm removal requires confirm=true. List alarms first and confirm the exact alarm ID with the user.');
|
|
86
|
+
const alarm = this.requiredAlarm(id);
|
|
87
|
+
await this.scheduler.remove(alarm.id);
|
|
88
|
+
if (!this.store.removeAlarm(alarm.id))
|
|
89
|
+
throw new Error(`Alarm not found: ${id}`);
|
|
90
|
+
return { ok: true, removed: alarm.id };
|
|
91
|
+
}
|
|
92
|
+
async sync() {
|
|
93
|
+
return this.scheduler.syncAll(this.store.listAlarms());
|
|
94
|
+
}
|
|
95
|
+
async controlActive(input) {
|
|
96
|
+
const { client, status } = await this.selectActive(input.alarmId, input.occurrenceAt);
|
|
97
|
+
if (input.action === 'dismiss')
|
|
98
|
+
await client.dismiss();
|
|
99
|
+
else if (input.action === 'keep-playing')
|
|
100
|
+
await client.keepPlaying();
|
|
101
|
+
else if (input.action === 'handoff') {
|
|
102
|
+
if (status.state !== 'playing')
|
|
103
|
+
throw new Error('Alarm playback is still starting.');
|
|
104
|
+
if (!this.handoffToInteractive)
|
|
105
|
+
throw new Error('Interactive playback handoff is unavailable.');
|
|
106
|
+
await this.handoffToInteractive(status);
|
|
107
|
+
await client.handoff();
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
await client.snooze(boundedInteger(input.snoozeMinutes ?? 10, 'snoozeMinutes', 1, 1440));
|
|
111
|
+
return { ok: true, action: input.action, alarm: activeStatusForAgent(status) };
|
|
112
|
+
}
|
|
113
|
+
async describe(alarm) {
|
|
114
|
+
return {
|
|
115
|
+
id: alarm.id,
|
|
116
|
+
label: alarm.label,
|
|
117
|
+
enabled: alarm.enabled,
|
|
118
|
+
station: { id: `${alarm.station.provider}:${alarm.station.id}`, name: alarm.station.name },
|
|
119
|
+
schedule: alarm.schedule,
|
|
120
|
+
nextOccurrence: nextOccurrenceForAlarm(alarm, new Date())?.toISOString() ?? null,
|
|
121
|
+
playback: {
|
|
122
|
+
volume: alarm.playback.volume,
|
|
123
|
+
fadeSeconds: alarm.playback.fadeSeconds,
|
|
124
|
+
stopAfterMinutes: alarm.playback.stopAfterMinutes,
|
|
125
|
+
...(alarm.playback.fallbackStation ? { fallbackStation: {
|
|
126
|
+
id: `${alarm.playback.fallbackStation.provider}:${alarm.playback.fallbackStation.id}`,
|
|
127
|
+
name: alarm.playback.fallbackStation.name
|
|
128
|
+
} } : {})
|
|
129
|
+
},
|
|
130
|
+
reliability: alarm.reliability,
|
|
131
|
+
lastRun: alarm.lastRun,
|
|
132
|
+
snoozedUntil: alarm.nextOverride?.at
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async activeStatuses() {
|
|
136
|
+
return (await Promise.all((await connectActiveAlarms()).map(client => client.status()))).map(activeStatusForAgent);
|
|
137
|
+
}
|
|
138
|
+
async selectActive(alarmId, occurrenceAt) {
|
|
139
|
+
const matches = [];
|
|
140
|
+
for (const client of await connectActiveAlarms()) {
|
|
141
|
+
const status = await client.status();
|
|
142
|
+
if ((!alarmId || status.alarmId === alarmId) && (!occurrenceAt || status.scheduledAt === occurrenceAt))
|
|
143
|
+
matches.push({ client, status });
|
|
144
|
+
}
|
|
145
|
+
if (!matches.length)
|
|
146
|
+
throw new Error('No matching alarm is currently ringing.');
|
|
147
|
+
if (matches.length > 1)
|
|
148
|
+
throw new Error('Multiple alarms are ringing. List alarm status, then provide alarm_id and optionally occurrence_at.');
|
|
149
|
+
return matches[0];
|
|
150
|
+
}
|
|
151
|
+
requiredAlarm(id) {
|
|
152
|
+
const alarm = this.store.getAlarm(id);
|
|
153
|
+
if (!alarm)
|
|
154
|
+
throw new Error(`Alarm not found: ${id}`);
|
|
155
|
+
return alarm;
|
|
156
|
+
}
|
|
157
|
+
async requiredStation(id) {
|
|
158
|
+
const station = await this.resolveStation(id);
|
|
159
|
+
if (!station)
|
|
160
|
+
throw new Error(`Unknown station ID: ${id}. Search or browse first.`);
|
|
161
|
+
return station;
|
|
162
|
+
}
|
|
163
|
+
async syncSaved(alarm) {
|
|
164
|
+
try {
|
|
165
|
+
await this.scheduler.sync(alarm);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw new Error(`Alarm ${alarm.id} was saved, but scheduler setup is degraded: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function normalizeSchedule(schedule) {
|
|
173
|
+
if (schedule.type === 'once') {
|
|
174
|
+
const instant = new Date(schedule.at);
|
|
175
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(schedule.at) || !Number.isFinite(instant.getTime()) || instant.getUTCSeconds() !== 0 || instant.getUTCMilliseconds() !== 0) {
|
|
176
|
+
throw new Error('One-time alarms require an absolute ISO-8601 minute with an offset or Z and zero seconds.');
|
|
177
|
+
}
|
|
178
|
+
if (instant.getTime() <= Date.now())
|
|
179
|
+
throw new Error('One-time alarms must be scheduled in the future.');
|
|
180
|
+
return { type: 'once', at: instant.toISOString() };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
type: 'recurring',
|
|
184
|
+
time: canonicalizeAlarmTime(schedule.time),
|
|
185
|
+
weekdays: canonicalizeIsoWeekdays(schedule.weekdays),
|
|
186
|
+
timezone: canonicalizeTimeZone(schedule.timezone)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function boundedInteger(value, label, min, max) {
|
|
190
|
+
if (!Number.isInteger(value) || value < min || value > max)
|
|
191
|
+
throw new Error(`${label} must be an integer from ${min} to ${max}.`);
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
function cleanLabel(value) {
|
|
195
|
+
const label = value.trim();
|
|
196
|
+
if (!label || label.length > 120 || /[\u0000-\u001F\u007F-\u009F]/.test(label))
|
|
197
|
+
throw new Error('Alarm label must be 1-120 printable characters.');
|
|
198
|
+
return label;
|
|
199
|
+
}
|
|
200
|
+
function activeStatusForAgent(status) {
|
|
201
|
+
return {
|
|
202
|
+
alarmId: status.alarmId,
|
|
203
|
+
scheduledAt: status.scheduledAt,
|
|
204
|
+
stationName: status.stationName,
|
|
205
|
+
...(status.station ? { station: { id: `${status.station.provider}:${status.station.id}`, name: status.station.name } } : {}),
|
|
206
|
+
startedAt: status.startedAt,
|
|
207
|
+
state: status.state,
|
|
208
|
+
keepPlaying: status.keepPlaying
|
|
209
|
+
};
|
|
210
|
+
}
|