@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +30 -0
  3. package/dist/agent/alarm-service.js +210 -0
  4. package/dist/agent/cli.js +193 -0
  5. package/dist/agent/headless-host.js +143 -0
  6. package/dist/agent/launcher.js +71 -0
  7. package/dist/agent/mcp-install.js +467 -0
  8. package/dist/agent/mcp-server.js +139 -0
  9. package/dist/agent/service.js +347 -0
  10. package/dist/agent/session.js +248 -0
  11. package/dist/alarms/cli.js +4 -1
  12. package/dist/alarms/runner.js +43 -26
  13. package/dist/cli.js +60 -3
  14. package/dist/player/player-controller.js +19 -0
  15. package/dist/providers/provider-manager.js +5 -0
  16. package/dist/providers/radio-browser.js +4 -0
  17. package/dist/setup.js +71 -2
  18. package/dist/storage/store.js +33 -1
  19. package/dist/types.js +6 -0
  20. package/dist/ui/AdaptiveContent.js +24 -9
  21. package/dist/ui/App.js +299 -18
  22. package/dist/ui/AppContent.js +4 -4
  23. package/dist/ui/components/StationList.js +3 -5
  24. package/dist/ui/components/VersionIndicator.js +19 -0
  25. package/dist/ui/page-footer.js +4 -2
  26. package/dist/ui/screen-items.js +40 -9
  27. package/dist/ui/screens/AlarmsScreen.js +2 -1
  28. package/dist/ui/screens/CountriesScreen.js +8 -5
  29. package/dist/ui/screens/HomeScreen.js +3 -1
  30. package/dist/ui/screens/SettingsScreen.js +70 -53
  31. package/dist/ui/use-alarm-tui.js +4 -0
  32. package/dist/ui/use-app-input.js +29 -3
  33. package/dist/ui/visualizers/gallop.js +118 -0
  34. package/dist/ui/visualizers/horse-stride.js +20 -0
  35. package/dist/ui/visualizers/receiver-style-registry.js +12 -2
  36. package/dist/ui/visualizers/receiver-visualizers.js +3 -0
  37. package/dist/ui/visualizers/retro-receivers.js +4 -0
  38. package/dist/ui/visualizers/terminal-receivers.js +57 -0
  39. package/dist/update-check.js +26 -7
  40. 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,4 @@
1
+ import { terminalReceiverBuilders } from './terminal-receivers.js';
2
+ export const retroReceiverBuilders = {
3
+ ...terminalReceiverBuilders
4
+ };
@@ -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
+ };
@@ -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 updateStatusText(updateCheck) {
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.updateAvailable && updateCheck.latestVersion) {
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 (/\/node_modules\/@ciphore\/radiocli\//.test(haystack)) {
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 === 'win32' ? 'cmd.exe' : 'sh';
69
- const args = process.platform === 'win32' ? ['/d', '/s', '/c', command] : ['-lc', command];
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.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
  },