@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { themeAccent, themeContributionColors } from '../theme.js';
|
|
2
|
+
import { retroReceiverBuilders } from './retro-receivers.js';
|
|
2
3
|
import { receiverStyleMetadata } from './receiver-style-registry.js';
|
|
3
4
|
const receiverStyleBuilders = {
|
|
5
|
+
...retroReceiverBuilders,
|
|
4
6
|
ultracode: buildUltracode,
|
|
5
7
|
'motion-contour': buildMotionContour,
|
|
6
8
|
leds: buildLeds,
|
|
@@ -89,6 +91,7 @@ function buildMicroVisualizer(style, pulse, width, height, theme) {
|
|
|
89
91
|
// the circular braille rings) while fixed-size and minimum-size scenes use the
|
|
90
92
|
// supersampled fit path below.
|
|
91
93
|
const microNativeScaleStyles = new Set([
|
|
94
|
+
...Object.keys(retroReceiverBuilders),
|
|
92
95
|
'pulse-grid',
|
|
93
96
|
'hex-pulse',
|
|
94
97
|
'galaxy',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { buildGallopFine } from './gallop.js';
|
|
2
|
+
import { panelBackground, themeAccent, themeContributionColors } from '../theme.js';
|
|
3
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
4
|
+
function blend(a, b, t) {
|
|
5
|
+
return '#' + [1, 3, 5].map(offset => Math.round(parseInt(a.slice(offset, offset + 2), 16) * (1 - t) + parseInt(b.slice(offset, offset + 2), 16) * t).toString(16).padStart(2, '0')).join('');
|
|
6
|
+
}
|
|
7
|
+
/** A cell canvas, not a bitmap: glyphs, whole-cell color and intentional empty space. */
|
|
8
|
+
function canvas(width, height, theme) {
|
|
9
|
+
const w = Math.max(1, Math.floor(width));
|
|
10
|
+
const h = Math.max(1, Math.floor(height));
|
|
11
|
+
const accent = themeAccent(theme);
|
|
12
|
+
const contributions = themeContributionColors(theme);
|
|
13
|
+
const ramp = [panelBackground, blend(panelBackground, contributions[1], 0.55),
|
|
14
|
+
...contributions.slice(1), blend(accent, '#ffffff', 0.36), '#ffffff'];
|
|
15
|
+
const cells = Array.from({ length: h }, () => Array.from({ length: w }, () => ({ text: ' ', color: accent })));
|
|
16
|
+
const paint = (x, y, text, level, background = false, color) => {
|
|
17
|
+
const row = Math.round(y);
|
|
18
|
+
const col = Math.round(x);
|
|
19
|
+
if (row < 0 || row >= h || col < 0 || col >= w)
|
|
20
|
+
return;
|
|
21
|
+
const ink = color ?? ramp[Math.max(0, Math.min(ramp.length - 1, Math.round(level)))];
|
|
22
|
+
cells[row][col] = background ? { text: ' ', color: ink, backgroundColor: ink } : { text, color: ink };
|
|
23
|
+
};
|
|
24
|
+
const finish = () => cells.map(row => {
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (const cell of row) {
|
|
27
|
+
const last = segments.at(-1);
|
|
28
|
+
if (last && last.color === cell.color && last.backgroundColor === cell.backgroundColor)
|
|
29
|
+
last.text += cell.text;
|
|
30
|
+
else
|
|
31
|
+
segments.push({ ...cell });
|
|
32
|
+
}
|
|
33
|
+
return { text: row.map(c => c.text).join(''), color: accent, segments };
|
|
34
|
+
});
|
|
35
|
+
return { w, h, ramp, paint, finish };
|
|
36
|
+
}
|
|
37
|
+
// Interleaved wavefronts, folded through each other. Entire cells are the light source.
|
|
38
|
+
const crossfade = (pulse, width, height, theme) => {
|
|
39
|
+
const c = canvas(width, height, theme);
|
|
40
|
+
const t = pulse * 0.08;
|
|
41
|
+
const shades = Array.from({ length: 18 }, (_, i) => blend(c.ramp[2], c.ramp[5], i / 17));
|
|
42
|
+
for (let y = 0; y < c.h; y++) {
|
|
43
|
+
for (let x = 0; x < c.w; x++) {
|
|
44
|
+
const u = x / c.w * 2 - 1;
|
|
45
|
+
const v = y / c.h * 2 - 1;
|
|
46
|
+
const a = u * 8.5 + v * 3 + Math.sin(v * 3.4 - t * 0.7) * 2 - t * 1.1;
|
|
47
|
+
const b = v * 5 - u * 2 + Math.sin(u * 3 + t * 0.5) * 1.5 + t * 0.75;
|
|
48
|
+
const value = clamp(0.5 + 0.28 * Math.cos(a) + 0.24 * Math.cos(b));
|
|
49
|
+
c.paint(x, y, ' ', 0, true, shades[Math.round(value * 17)]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return c.finish();
|
|
53
|
+
};
|
|
54
|
+
export const terminalReceiverBuilders = {
|
|
55
|
+
crossfade,
|
|
56
|
+
'gallop-fine': buildGallopFine
|
|
57
|
+
};
|
package/dist/update-check.js
CHANGED
|
@@ -40,17 +40,26 @@ export function shouldCheckForUpdate(updateCheck, now = Date.now()) {
|
|
|
40
40
|
const checkedAt = Date.parse(updateCheck.checkedAt);
|
|
41
41
|
return !Number.isFinite(checkedAt) || now - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
|
|
42
42
|
}
|
|
43
|
-
export function
|
|
43
|
+
export function automaticUpdateChecksAllowed(enabled = true) {
|
|
44
|
+
return enabled && process.env.RADIOCLI_DISABLE_UPDATE_CHECK !== '1' && process.env.CI !== 'true';
|
|
45
|
+
}
|
|
46
|
+
export function updateAvailableForVersion(updateCheck, currentVersion = updateCheck?.currentVersion) {
|
|
47
|
+
return Boolean(updateCheck?.updateAvailable &&
|
|
48
|
+
updateCheck.latestVersion &&
|
|
49
|
+
currentVersion &&
|
|
50
|
+
compareSemver(updateCheck.latestVersion, currentVersion) > 0);
|
|
51
|
+
}
|
|
52
|
+
export function updateStatusText(updateCheck, currentVersion) {
|
|
44
53
|
if (!updateCheck) {
|
|
45
54
|
return 'not checked yet';
|
|
46
55
|
}
|
|
47
|
-
if (updateCheck
|
|
56
|
+
if (updateAvailableForVersion(updateCheck, currentVersion) && updateCheck.latestVersion) {
|
|
48
57
|
return `v${updateCheck.latestVersion} available`;
|
|
49
58
|
}
|
|
50
59
|
if (updateCheck.error) {
|
|
51
60
|
return `check failed: ${updateCheck.error}`;
|
|
52
61
|
}
|
|
53
|
-
return updateCheck.latestVersion ? `current at v${updateCheck.latestVersion}` : 'not checked yet';
|
|
62
|
+
return updateCheck.latestVersion ? `current at v${currentVersion ?? updateCheck.latestVersion}` : 'not checked yet';
|
|
54
63
|
}
|
|
55
64
|
export function updateCommandForInstall(entryPath = process.argv[1]) {
|
|
56
65
|
const resolved = resolvePath(entryPath);
|
|
@@ -58,16 +67,21 @@ export function updateCommandForInstall(entryPath = process.argv[1]) {
|
|
|
58
67
|
if (/\/(?:opt\/homebrew|usr\/local)\/(?:Cellar|Homebrew)\//.test(haystack) || /\/\.linuxbrew\/(?:Cellar|Homebrew)\//.test(haystack)) {
|
|
59
68
|
return { method: 'homebrew', command: 'brew update && brew upgrade radiocli' };
|
|
60
69
|
}
|
|
61
|
-
if (
|
|
70
|
+
if (/[\\/](?:pnpm|\.pnpm-global)[\\/]/.test(haystack)) {
|
|
71
|
+
return { method: 'pnpm', command: 'pnpm add -g @ciphore/radiocli@latest' };
|
|
72
|
+
}
|
|
73
|
+
if (/[\\/]\.bun[\\/]install[\\/]global[\\/]/.test(haystack)) {
|
|
74
|
+
return { method: 'bun', command: 'bun add -g @ciphore/radiocli@latest' };
|
|
75
|
+
}
|
|
76
|
+
if (/[\\/]node_modules[\\/]@ciphore[\\/]radiocli[\\/]/.test(haystack)) {
|
|
62
77
|
return { method: 'npm', command: 'npm install -g @ciphore/radiocli@latest' };
|
|
63
78
|
}
|
|
64
79
|
return { method: 'unknown', command: 'npm install -g @ciphore/radiocli@latest' };
|
|
65
80
|
}
|
|
66
81
|
export function installUpdate(command = updateCommandForInstall().command) {
|
|
67
82
|
return new Promise(resolve => {
|
|
68
|
-
const shell = process.platform
|
|
69
|
-
const
|
|
70
|
-
const child = spawn(shell, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
83
|
+
const shell = updateShellForPlatform(process.platform, command);
|
|
84
|
+
const child = spawn(shell.command, shell.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
71
85
|
const chunks = [];
|
|
72
86
|
child.stdout.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
|
73
87
|
child.stderr.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
|
@@ -80,6 +94,11 @@ export function installUpdate(command = updateCommandForInstall().command) {
|
|
|
80
94
|
});
|
|
81
95
|
});
|
|
82
96
|
}
|
|
97
|
+
export function updateShellForPlatform(platform, command) {
|
|
98
|
+
return platform === 'win32'
|
|
99
|
+
? { command: 'cmd.exe', args: ['/d', '/s', '/c', command] }
|
|
100
|
+
: { command: 'sh', args: ['-lc', command] };
|
|
101
|
+
}
|
|
83
102
|
export function compareSemver(left, right) {
|
|
84
103
|
const leftParts = semverParts(left);
|
|
85
104
|
const rightParts = semverParts(right);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ciphore/radiocli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A terminal-first world radio receiver built with Ink, mpv, and resilient public-radio providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,13 +61,16 @@
|
|
|
61
61
|
"prepublishOnly": "npm run check && npm run lint && npm run test && npm run smoke:data && npm run fresh:check && npm run check:package",
|
|
62
62
|
"pack:check": "npm pack --dry-run",
|
|
63
63
|
"smoke:data": "tsx src/smoke/data-smoke.ts",
|
|
64
|
+
"smoke:mcp": "node scripts/mcp-smoke.mjs",
|
|
64
65
|
"smoke:playback": "tsx src/smoke/playback-smoke.ts",
|
|
65
66
|
"demo:script": "node scripts/demo-script.mjs",
|
|
66
67
|
"demo:assets": "node scripts/capture-demo-assets.mjs",
|
|
67
68
|
"fresh:check": "node scripts/fresh-check.mjs"
|
|
68
69
|
},
|
|
69
70
|
"dependencies": {
|
|
71
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
70
72
|
"ink": "^7.0.5",
|
|
73
|
+
"jsonc-parser": "^3.3.1",
|
|
71
74
|
"react": "^19.2.6",
|
|
72
75
|
"zod": "^4.4.3"
|
|
73
76
|
},
|