@ciphore/radiocli 0.2.2 → 0.2.3
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 +47 -1
- package/README.md +30 -0
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +143 -0
- package/dist/agent/launcher.js +71 -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 +248 -0
- package/dist/alarms/cli.js +4 -1
- package/dist/alarms/runner.js +43 -26
- package/dist/cli.js +60 -3
- package/dist/player/player-controller.js +19 -0
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +4 -0
- package/dist/setup.js +71 -2
- package/dist/storage/store.js +33 -1
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +24 -9
- package/dist/ui/App.js +299 -18
- package/dist/ui/AppContent.js +4 -4
- package/dist/ui/components/StationList.js +3 -5
- package/dist/ui/components/VersionIndicator.js +19 -0
- package/dist/ui/page-footer.js +4 -2
- package/dist/ui/screen-items.js +40 -9
- package/dist/ui/screens/AlarmsScreen.js +2 -1
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/HomeScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +70 -53
- package/dist/ui/use-alarm-tui.js +4 -0
- package/dist/ui/use-app-input.js +29 -3
- 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 +26 -7
- package/package.json +4 -1
package/dist/setup.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
import { clearCommandCache, commandExists } from './player/command.js';
|
|
5
5
|
import { detectPlaybackBackends, playbackBackendStatusLines } from './player/backend-install.js';
|
|
6
|
+
import { configureMcpIntegrations } from './agent/mcp-install.js';
|
|
7
|
+
import { JsonLibraryStore } from './storage/store.js';
|
|
8
|
+
import { defaultAgentControlSettings } from './types.js';
|
|
6
9
|
const components = ['mpv', 'ffmpeg', 'vlc'];
|
|
7
10
|
const packageManagers = ['brew', 'winget', 'scoop', 'choco', 'apt', 'dnf', 'pacman', 'apk', 'zypper'];
|
|
8
11
|
export async function runSetup(options = {}) {
|
|
@@ -21,23 +24,37 @@ export async function runSetup(options = {}) {
|
|
|
21
24
|
if (!parsed.yes && !parsed.only && isInteractive(input, output)) {
|
|
22
25
|
selected = await promptForComponents({ platform, installed, input, output });
|
|
23
26
|
}
|
|
27
|
+
let mcp = parsed.mcp;
|
|
28
|
+
if (mcp === null && !parsed.yes && isInteractive(input, output)) {
|
|
29
|
+
mcp = await promptYesNo(input, output, ' Agent control via MCP (detected coding agents)', true);
|
|
30
|
+
}
|
|
31
|
+
let agentUi = parsed.agentUi;
|
|
32
|
+
if (mcp === true && agentUi === null && !parsed.yes && isInteractive(input, output)) {
|
|
33
|
+
const note = platform === 'darwin'
|
|
34
|
+
? ' (macOS will ask the agent app to control Terminal on first use)'
|
|
35
|
+
: ' (opens a separate terminal window)';
|
|
36
|
+
agentUi = await promptYesNo(input, output, ` Open the RadioCLI TUI for agent playback${note}`, true);
|
|
37
|
+
}
|
|
24
38
|
const plan = createSetupPlan({ platform, osRelease, packageManager, installed, selected });
|
|
25
39
|
printPlan(plan, output);
|
|
26
40
|
const missing = plan.selected.filter(component => !plan.installed[component]);
|
|
27
41
|
if (missing.length === 0) {
|
|
28
42
|
output.write(plan.selected.length === 0 ? '\nNo components selected.\n' : '\nEverything selected is already installed.\n');
|
|
43
|
+
await finishMcpSetup(mcp, agentUi, parsed.dryRun, output);
|
|
29
44
|
printVerification(output);
|
|
30
45
|
return;
|
|
31
46
|
}
|
|
32
47
|
if (!plan.packageManager) {
|
|
33
48
|
if (parsed.dryRun) {
|
|
34
49
|
output.write('\nDry run complete. Install the missing components manually; no system packages were changed.\n');
|
|
50
|
+
await finishMcpSetup(mcp, agentUi, true, output);
|
|
35
51
|
return;
|
|
36
52
|
}
|
|
37
53
|
throw new Error('No supported system package manager was found. Install mpv manually, then run radiocli doctor.');
|
|
38
54
|
}
|
|
39
55
|
if (parsed.dryRun) {
|
|
40
56
|
output.write('\nDry run complete. No system packages were changed.\n');
|
|
57
|
+
await finishMcpSetup(mcp, agentUi, true, output);
|
|
41
58
|
return;
|
|
42
59
|
}
|
|
43
60
|
if (parsed.packageManager && !hasCommand(parsed.packageManager)) {
|
|
@@ -64,6 +81,7 @@ export async function runSetup(options = {}) {
|
|
|
64
81
|
}
|
|
65
82
|
clearCommandCache();
|
|
66
83
|
output.write('\nSetup complete.\n');
|
|
84
|
+
await finishMcpSetup(mcp, agentUi, false, output);
|
|
67
85
|
printVerification(output);
|
|
68
86
|
}
|
|
69
87
|
export function createSetupPlan({ platform, osRelease = '', packageManager, installed, selected }) {
|
|
@@ -115,6 +133,8 @@ export function parseSetupArgs(args) {
|
|
|
115
133
|
let yes = false;
|
|
116
134
|
let only = null;
|
|
117
135
|
let packageManager = null;
|
|
136
|
+
let mcp = null;
|
|
137
|
+
let agentUi = null;
|
|
118
138
|
for (let index = 0; index < args.length; index += 1) {
|
|
119
139
|
const arg = args[index];
|
|
120
140
|
if (arg === '--all')
|
|
@@ -123,6 +143,26 @@ export function parseSetupArgs(args) {
|
|
|
123
143
|
dryRun = true;
|
|
124
144
|
else if (arg === '--yes' || arg === '-y')
|
|
125
145
|
yes = true;
|
|
146
|
+
else if (arg === '--mcp') {
|
|
147
|
+
if (mcp === false)
|
|
148
|
+
throw new Error('Use either --mcp or --no-mcp, not both.');
|
|
149
|
+
mcp = true;
|
|
150
|
+
}
|
|
151
|
+
else if (arg === '--no-mcp') {
|
|
152
|
+
if (mcp === true)
|
|
153
|
+
throw new Error('Use either --mcp or --no-mcp, not both.');
|
|
154
|
+
mcp = false;
|
|
155
|
+
}
|
|
156
|
+
else if (arg === '--agent-ui') {
|
|
157
|
+
if (agentUi === false)
|
|
158
|
+
throw new Error('Use either --agent-ui or --headless-agent, not both.');
|
|
159
|
+
agentUi = true;
|
|
160
|
+
}
|
|
161
|
+
else if (arg === '--headless-agent') {
|
|
162
|
+
if (agentUi === true)
|
|
163
|
+
throw new Error('Use either --agent-ui or --headless-agent, not both.');
|
|
164
|
+
agentUi = false;
|
|
165
|
+
}
|
|
126
166
|
else if (arg === '--only')
|
|
127
167
|
only = parseComponents(args[++index]);
|
|
128
168
|
else if (arg.startsWith('--only='))
|
|
@@ -136,7 +176,36 @@ export function parseSetupArgs(args) {
|
|
|
136
176
|
}
|
|
137
177
|
if (all && only)
|
|
138
178
|
throw new Error('Use either --all or --only, not both.');
|
|
139
|
-
|
|
179
|
+
if (agentUi !== null && mcp !== true)
|
|
180
|
+
throw new Error('--agent-ui and --headless-agent require --mcp.');
|
|
181
|
+
return { all, dryRun, yes, only, packageManager, mcp, agentUi };
|
|
182
|
+
}
|
|
183
|
+
async function finishMcpSetup(enabled, agentUi, dryRun, output) {
|
|
184
|
+
if (enabled === null)
|
|
185
|
+
return;
|
|
186
|
+
if (dryRun) {
|
|
187
|
+
output.write(`\nAgent integration: would be ${enabled ? 'enabled and installed for detected MCP clients' : 'disabled and removed from detected MCP clients'}.\n`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const entry = process.argv[1];
|
|
191
|
+
if (!entry)
|
|
192
|
+
throw new Error('Could not locate the RadioCLI executable for MCP setup.');
|
|
193
|
+
const results = await configureMcpIntegrations(enabled, { nodePath: process.execPath, cliPath: realpathSync(entry) }, output);
|
|
194
|
+
const failed = results.filter(result => result.status === 'failed');
|
|
195
|
+
if (failed.length > 0) {
|
|
196
|
+
throw new Error(`Playback setup finished, but ${failed.length} agent integration${failed.length === 1 ? '' : 's'} failed: ${failed.map(result => result.client).join(', ')}. Run radiocli mcp status for details.`);
|
|
197
|
+
}
|
|
198
|
+
if (enabled) {
|
|
199
|
+
const store = new JsonLibraryStore();
|
|
200
|
+
const current = store.snapshot().settings.agentControl ?? defaultAgentControlSettings;
|
|
201
|
+
const openUiOnPlay = agentUi ?? current.openUiOnPlay;
|
|
202
|
+
if (openUiOnPlay !== current.openUiOnPlay) {
|
|
203
|
+
store.updateSettings({ agentControl: { ...current, openUiOnPlay } });
|
|
204
|
+
}
|
|
205
|
+
output.write(openUiOnPlay
|
|
206
|
+
? '\nAgent playback: terminal TUI enabled (default). The host OS may request app-control permission on first use.\n'
|
|
207
|
+
: '\nAgent playback: headless; no separate terminal window or app-control permission is needed.\n');
|
|
208
|
+
}
|
|
140
209
|
}
|
|
141
210
|
function parseComponents(value) {
|
|
142
211
|
const values = value?.split(',').map(item => item.trim().toLowerCase()).filter(Boolean) ?? [];
|
package/dist/storage/store.js
CHANGED
|
@@ -61,7 +61,25 @@ const settingsSchema = z.object({
|
|
|
61
61
|
transparentBackground: z.boolean().default(false),
|
|
62
62
|
asciiMode: z.boolean().default(false),
|
|
63
63
|
reduceMotion: z.boolean().default(false),
|
|
64
|
-
mouseSupport: z.boolean().default(true)
|
|
64
|
+
mouseSupport: z.boolean().default(true),
|
|
65
|
+
automaticUpdateChecks: z.boolean().default(true),
|
|
66
|
+
agentControl: z.object({
|
|
67
|
+
enabled: z.boolean().default(false),
|
|
68
|
+
openUiOnPlay: z.boolean().default(true),
|
|
69
|
+
focusNowPlaying: z.boolean().default(true),
|
|
70
|
+
completionPreset: z.object({
|
|
71
|
+
action: z.enum(['play', 'pause', 'resume', 'stop']).default('play'),
|
|
72
|
+
source: z.enum(['recent', 'favorite', 'popular', 'country']).default('recent'),
|
|
73
|
+
countryCode: z.string().length(2).optional(),
|
|
74
|
+
ifPlaying: z.enum(['keep', 'replace']).default('keep'),
|
|
75
|
+
openUi: z.boolean().optional()
|
|
76
|
+
}).default({ action: 'play', source: 'recent', ifPlaying: 'keep' })
|
|
77
|
+
}).default({
|
|
78
|
+
enabled: false,
|
|
79
|
+
openUiOnPlay: true,
|
|
80
|
+
focusNowPlaying: true,
|
|
81
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
82
|
+
})
|
|
65
83
|
});
|
|
66
84
|
const absoluteInstantSchema = z.string().refine(value => /(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value)), { message: 'Expected an absolute ISO-8601 instant with a timezone offset.' });
|
|
67
85
|
const alarmMinuteInstantSchema = absoluteInstantSchema.refine(value => new Date(value).getUTCSeconds() === 0 && new Date(value).getUTCMilliseconds() === 0, { message: 'Alarm occurrences use minute precision; seconds must be zero.' });
|
|
@@ -194,6 +212,13 @@ const librarySchema = z.object({
|
|
|
194
212
|
tuneTimeoutSeconds: 12,
|
|
195
213
|
skipBrokenStreams: true,
|
|
196
214
|
mouseSupport: true,
|
|
215
|
+
automaticUpdateChecks: true,
|
|
216
|
+
agentControl: {
|
|
217
|
+
enabled: false,
|
|
218
|
+
openUiOnPlay: true,
|
|
219
|
+
focusNowPlaying: true,
|
|
220
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
221
|
+
},
|
|
197
222
|
mediaKeys: defaultMediaKeys
|
|
198
223
|
})
|
|
199
224
|
});
|
|
@@ -636,6 +661,13 @@ function defaultState() {
|
|
|
636
661
|
tuneTimeoutSeconds: 12,
|
|
637
662
|
skipBrokenStreams: true,
|
|
638
663
|
mouseSupport: true,
|
|
664
|
+
automaticUpdateChecks: true,
|
|
665
|
+
agentControl: {
|
|
666
|
+
enabled: false,
|
|
667
|
+
openUiOnPlay: true,
|
|
668
|
+
focusNowPlaying: true,
|
|
669
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
670
|
+
},
|
|
639
671
|
mediaKeys: defaultMediaKeys
|
|
640
672
|
}
|
|
641
673
|
};
|
package/dist/types.js
CHANGED
|
@@ -2,3 +2,9 @@ import { defaultReceiverStyle, receiverStyleNames } from './ui/visualizers/recei
|
|
|
2
2
|
export { defaultReceiverStyle, receiverStyleNames };
|
|
3
3
|
const providerIds = ['radio-browser', 'radio-garden', 'playlist'];
|
|
4
4
|
export const themeNames = ['green', 'amber', 'blue', 'ruby', 'ice', 'teal', 'violet', 'copper', 'cyan', 'lime', 'coral', 'rose', 'slate', 'mono'];
|
|
5
|
+
export const defaultAgentControlSettings = {
|
|
6
|
+
enabled: false,
|
|
7
|
+
openUiOnPlay: true,
|
|
8
|
+
focusNowPlaying: true,
|
|
9
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
10
|
+
};
|
|
@@ -5,7 +5,7 @@ import { computeListeningStats } from '../activity/stats.js';
|
|
|
5
5
|
import { playbackBackendLabel } from '../player/backend-install.js';
|
|
6
6
|
import { visibleWindow } from './list-window.js';
|
|
7
7
|
import { displayWidth, padDisplayEnd, stationLocation, stationTags, stationTech, truncate } from './format.js';
|
|
8
|
-
import { homeItems,
|
|
8
|
+
import { homeItems, settingsGroup, settingsGroups, settingsItemsForPage } from './screen-items.js';
|
|
9
9
|
import { screenTitle } from './screen-meta.js';
|
|
10
10
|
import { keyHelpSections, commandHelp } from './help-content.js';
|
|
11
11
|
import { settingLabel, settingValue } from './screens/SettingsScreen.js';
|
|
@@ -23,7 +23,7 @@ export function AdaptiveContent(props) {
|
|
|
23
23
|
return _jsx(AdaptiveContentBody, { ...props });
|
|
24
24
|
}
|
|
25
25
|
function AdaptiveContentBody(props) {
|
|
26
|
-
const { mode, screen, selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, exploreCursor, library, diagnostics, backends, updateCheck, favoriteKeys, stationTitle, } = props;
|
|
26
|
+
const { mode, screen, settingsPage = 'root', selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, exploreCursor, library, diagnostics, backends, updateCheck, appVersion, favoriteKeys, stationTitle, } = props;
|
|
27
27
|
const accent = themeAccent(theme);
|
|
28
28
|
const { ascii } = useDisplay();
|
|
29
29
|
const { bodyRows } = adaptiveFrameMetrics(mode, height);
|
|
@@ -41,6 +41,7 @@ function AdaptiveContentBody(props) {
|
|
|
41
41
|
if (screen === 'search') {
|
|
42
42
|
const rows = adaptiveRows({
|
|
43
43
|
screen,
|
|
44
|
+
settingsPage,
|
|
44
45
|
stations,
|
|
45
46
|
countries,
|
|
46
47
|
airPlayDevices,
|
|
@@ -48,6 +49,7 @@ function AdaptiveContentBody(props) {
|
|
|
48
49
|
diagnostics,
|
|
49
50
|
backends,
|
|
50
51
|
updateCheck,
|
|
52
|
+
appVersion,
|
|
51
53
|
favoriteKeys,
|
|
52
54
|
selected,
|
|
53
55
|
width,
|
|
@@ -88,6 +90,7 @@ function AdaptiveContentBody(props) {
|
|
|
88
90
|
}
|
|
89
91
|
const rows = adaptiveRows({
|
|
90
92
|
screen,
|
|
93
|
+
settingsPage,
|
|
91
94
|
stations,
|
|
92
95
|
countries,
|
|
93
96
|
airPlayDevices,
|
|
@@ -95,6 +98,7 @@ function AdaptiveContentBody(props) {
|
|
|
95
98
|
diagnostics,
|
|
96
99
|
backends,
|
|
97
100
|
updateCheck,
|
|
101
|
+
appVersion,
|
|
98
102
|
favoriteKeys,
|
|
99
103
|
selected,
|
|
100
104
|
width,
|
|
@@ -255,16 +259,20 @@ function adaptiveRows(input) {
|
|
|
255
259
|
return homeItems.map(item => ({
|
|
256
260
|
key: item.screen,
|
|
257
261
|
label: item.label,
|
|
258
|
-
detail: mode === 'compact' && width >= 52 ? item.detail : undefined
|
|
262
|
+
detail: mode === 'compact' && width >= 52 ? item.detail : undefined,
|
|
263
|
+
separator: ' '
|
|
259
264
|
}));
|
|
260
265
|
}
|
|
261
266
|
if (screen === 'settings') {
|
|
262
|
-
const
|
|
267
|
+
const pageItems = settingsItemsForPage(input.settingsPage);
|
|
268
|
+
const labels = pageItems.map(item => settingLabel(item, updateCheck, input.appVersion));
|
|
263
269
|
const labelWidth = pairedColumnWidth(labels, width, mode);
|
|
264
|
-
return
|
|
270
|
+
return pageItems.map((item, index) => ({
|
|
265
271
|
key: item,
|
|
266
|
-
label: padDisplayEnd(truncate(settingLabel(item, updateCheck), labelWidth), labelWidth),
|
|
267
|
-
detail:
|
|
272
|
+
label: padDisplayEnd(truncate(settingLabel(item, updateCheck, input.appVersion), labelWidth), labelWidth),
|
|
273
|
+
detail: input.settingsPage === 'root'
|
|
274
|
+
? adaptiveSettingsRootValue(item)
|
|
275
|
+
: settingValue(item, library.settings, diagnostics, backends, airPlayDevices, updateCheck, input.appVersion),
|
|
268
276
|
separator: ' ',
|
|
269
277
|
index
|
|
270
278
|
}));
|
|
@@ -303,6 +311,10 @@ function adaptiveRows(input) {
|
|
|
303
311
|
}
|
|
304
312
|
return stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii);
|
|
305
313
|
}
|
|
314
|
+
function adaptiveSettingsRootValue(item) {
|
|
315
|
+
const group = settingsGroups.find(candidate => candidate.label === item);
|
|
316
|
+
return group ? `${group.items.length} settings ›` : undefined;
|
|
317
|
+
}
|
|
306
318
|
function stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii) {
|
|
307
319
|
return stations.map((station, index) => {
|
|
308
320
|
const favorite = favoriteKeys.has(`${station.provider}:${station.id}`);
|
|
@@ -362,7 +374,7 @@ function adaptiveEmptyState(input) {
|
|
|
362
374
|
return [{ key: 'empty', label: `No stations in ${stationTitle.toLowerCase()}.` }, { key: 'hint', label: 'Try another view or clear filters.' }];
|
|
363
375
|
}
|
|
364
376
|
function adaptiveStatus(props) {
|
|
365
|
-
const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel
|
|
377
|
+
const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel } = props;
|
|
366
378
|
if (screen === 'home') {
|
|
367
379
|
return `${library.favorites.length} favorites · ${library.recent.length} recent · ${library.imported.length} imported`;
|
|
368
380
|
}
|
|
@@ -370,7 +382,10 @@ function adaptiveStatus(props) {
|
|
|
370
382
|
return `${playbackBackendLabel(playback.backend)} · ${playback.state} · vol ${playback.volume}`;
|
|
371
383
|
}
|
|
372
384
|
if (screen === 'settings') {
|
|
373
|
-
|
|
385
|
+
const group = settingsGroup(props.settingsPage ?? 'root');
|
|
386
|
+
return group
|
|
387
|
+
? `${group.label} · Enter changes setting · b categories`
|
|
388
|
+
: 'Choose a category · Updates are available here';
|
|
374
389
|
}
|
|
375
390
|
if (screen === 'search') {
|
|
376
391
|
const query = searchQuery || (editingSearch ? 'type to search' : 'press / to search');
|