@iobroker/assistant-satellite 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ioBroker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @iobroker/assistant-satellite
2
+
3
+ A standalone **voice satellite** for the [`ioBroker.assistant`](https://github.com/ioBroker/ioBroker.assistant)
4
+ adapter. It runs the wake word on the device, streams the microphone to the adapter, and plays the
5
+ spoken reply — over the same UDP protocol as the Hannah satellite. **No ioBroker install required**;
6
+ runs on a bare Raspberry Pi via `npx`.
7
+
8
+ > STT → LLM → TTS all run in the adapter. The satellite only does: wake word · mic capture · playback.
9
+
10
+ ## Requirements
11
+
12
+ - Node.js ≥ 18
13
+ - Audio backend (auto-selected by platform):
14
+ - **Linux**: ALSA tools — `sudo apt install alsa-utils` (`arecord`/`aplay`)
15
+ - **Windows / macOS**: **ffmpeg** (provides `ffmpeg`/`ffplay`) on the PATH
16
+ - A running `ioBroker.assistant` instance with the **Voice** server enabled
17
+
18
+ Wake-word inference uses `onnxruntime-node`, which ships prebuilt binaries for Linux (x64/arm64),
19
+ Windows (x64) and macOS (x64/arm64) — so the satellite runs on all three; only the audio backend differs.
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ # 1. First run writes a default config and exits:
25
+ npx @iobroker/assistant-satellite
26
+
27
+ # 2. Edit config.json — at least set "host" (the ioBroker host) and your ALSA devices:
28
+ # "host": "192.168.1.129", "micDevice": "plughw:2,0", "speakerDevice": "plughw:2,0"
29
+
30
+ # 3. Run:
31
+ npx @iobroker/assistant-satellite config.json
32
+ ```
33
+
34
+ On first run it downloads the OpenWakeWord models into `modelsDir`. Then say the wake word
35
+ (default **"hey jarvis"**) → speak → the answer is played back.
36
+
37
+ Find your ALSA device with `arecord -l` / `aplay -l` (→ `plughw:<card>,<device>`; the `plug` prefix
38
+ lets ALSA resample so 16 kHz capture works on any card).
39
+
40
+ ### As a bare command
41
+
42
+ The package exposes an `assistant-satellite` binary (like `mocha`, `eslint`, … — via the `bin` field),
43
+ so you don't have to type `node build/cli.js`. Get it onto your PATH by installing globally:
44
+
45
+ ```bash
46
+ npm i -g @iobroker/assistant-satellite # after publish; or `npm i -g .` / `npm link` from a clone
47
+ assistant-satellite check config.json
48
+ assistant-satellite config.json
49
+ sudo assistant-satellite install config.json
50
+ ```
51
+
52
+ `npx @iobroker/assistant-satellite …` works without a global install. All subcommands
53
+ (`check` / `install` / `uninstall`) accept the same forms.
54
+
55
+ ## Configuration (`config.json`)
56
+
57
+ | Key | Default | Meaning |
58
+ |----------------------------------|------------------|------------------------------------------------------|
59
+ | `logLevel` | `info` | `info` or `debug` (wake-word/mic diagnostics) |
60
+ | `device` / `room` | `satellite` / `` | identity reported to the adapter |
61
+ | `host` / `port` | `` / `7775` | adapter address (fixed → **no MQTT broker needed**) |
62
+ | `listenPort` | `7776` | UDP port the satellite receives TTS on |
63
+ | `mqttBroker` … | `` | optional discovery instead of a fixed `host` |
64
+ | `audioBackend` | `auto` | `auto` / `alsa` / `ffmpeg` |
65
+ | `micDevice` / `speakerDevice` | `default` | see per-platform notes below |
66
+ | `wakewordModel` | `hey_jarvis` | built-in name, URL, or local `.onnx` path |
67
+ | `wakewordThreshold` | `0.5` | detection sensitivity (0–1) |
68
+ | `silenceThreshold` / `silenceMs` | `300` / `800` | end-of-speech (VAD) |
69
+ | `minRecordMs` / `maxRecordMs` | `800` / `8000` | recording bounds |
70
+
71
+ Built-in wake words: `hey_jarvis`, `alexa`, `hey_mycroft`, `hey_rhasspy`.
72
+
73
+ ### Audio devices per platform
74
+
75
+ - **Linux (alsa):** `micDevice`/`speakerDevice` = ALSA names, e.g. `plughw:2,0`. List with `arecord -l` /
76
+ `aplay -l` (the `plug` prefix lets ALSA resample so 16 kHz capture works on any card).
77
+ - **Windows (ffmpeg):** `micDevice` = the DirectShow device **name**, e.g. `Microphone (Poly Sync 20)`.
78
+ List with `ffmpeg -hide_banner -list_devices true -f dshow -i dummy`. Playback uses the default output
79
+ (`speakerDevice` is ignored via ffplay).
80
+ - **macOS (ffmpeg):** `micDevice` = the avfoundation audio **index**, e.g. `0`. List with
81
+ `ffmpeg -hide_banner -f avfoundation -list_devices true -i ""`.
82
+
83
+ ## Run as a service (systemd)
84
+
85
+ The satellite already retries registration and re-registers automatically if the adapter restarts, so
86
+ `systemd` only needs to keep the process alive.
87
+
88
+ First verify everything is present (Node, systemd, root, audio tools, **live mic-in / speaker-out test**,
89
+ config) — `install` runs these automatically and aborts on failure:
90
+
91
+ ```bash
92
+ node build/cli.js check config.json # dry-run of the same checks
93
+ ```
94
+
95
+ Then install itself as a service (Linux, needs `sudo`):
96
+
97
+ ```bash
98
+ # from a clone (build first), pointing at your config:
99
+ npm run build
100
+ sudo node build/cli.js install config.json # add --force to install despite check failures
101
+ # …or, after `npm i -g @iobroker/assistant-satellite`:
102
+ sudo assistant-satellite install /path/to/config.json
103
+ ```
104
+
105
+ `install` writes `/etc/systemd/system/assistant-satellite.service` (running as **your** user, in the
106
+ `audio` group, with absolute paths to `node`, `cli.js` and the config), then `daemon-reload` +
107
+ `enable --now`. Manage it with:
108
+
109
+ ```bash
110
+ journalctl -u assistant-satellite -f # logs
111
+ sudo systemctl restart assistant-satellite
112
+ sudo node build/cli.js uninstall # stop + remove (or: sudo assistant-satellite uninstall)
113
+ ```
114
+
115
+ <details><summary>Prefer a handwritten unit file?</summary>
116
+
117
+ ```ini
118
+ [Unit]
119
+ Description=ioBroker assistant satellite
120
+ After=network-online.target sound.target
121
+ Wants=network-online.target
122
+
123
+ [Service]
124
+ WorkingDirectory=/opt/assistant-satellite
125
+ ExecStart=/usr/bin/node /opt/assistant-satellite/build/cli.js /opt/assistant-satellite/config.json
126
+ Restart=always
127
+ RestartSec=5
128
+ User=iob
129
+ SupplementaryGroups=audio
130
+
131
+ [Install]
132
+ WantedBy=multi-user.target
133
+ ```
134
+ </details>
135
+
136
+ ## Status
137
+
138
+ Early scaffold. The audio/UDP/registration/playback pipeline follows the working Hannah satellite;
139
+ the **OpenWakeWord** inference (`src/wakeword.ts`) is a from-scratch Node port and its frame math /
140
+ threshold should be validated on the target device.
141
+
142
+ ## Changelog
143
+ <!--
144
+ Placeholder for the next version (at the beginning of the line):
145
+ ### **WORK IN PROGRESS**
146
+ -->
147
+ ### **WORK IN PROGRESS**
148
+ * (@GermanBluefox) Initial commit
149
+
150
+
151
+ ## License
152
+
153
+ MIT License
154
+
155
+ Copyright (c) 2025-2026 Denis Haev <dogafox@gmail.com>
156
+
157
+ Permission is hereby granted, free of charge, to any person obtaining a copy
158
+ of this software and associated documentation files (the "Software"), to deal
159
+ in the Software without restriction, including without limitation the rights
160
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
161
+ copies of the Software, and to permit persons to whom the Software is
162
+ furnished to do so, subject to the following conditions:
163
+
164
+ The above copyright notice and this permission notice shall be included in all
165
+ copies or substantial portions of the Software.
166
+
167
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
168
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
169
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
170
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
171
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
172
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
173
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ import type { Logger } from './index';
2
+ export type AudioBackend = 'alsa' | 'ffmpeg';
3
+ /** Resolve 'auto' → alsa on Linux, ffmpeg elsewhere. */
4
+ export declare function resolveBackend(pref: string): AudioBackend;
5
+ /** ffmpeg capture input args per platform (device = dshow name / avfoundation index / ALSA name). */
6
+ export declare function ffmpegInput(device: string): string[];
7
+ /** Continuous microphone capture at 16 kHz mono 16-bit; emits raw PCM chunks. */
8
+ export declare class Mic {
9
+ private readonly backend;
10
+ private readonly device;
11
+ private readonly log;
12
+ private proc;
13
+ constructor(backend: AudioBackend, device: string, log: Logger);
14
+ start(onData: (pcm: Buffer) => void): void;
15
+ stop(): void;
16
+ }
17
+ /** Play raw mono 16-bit PCM at the given rate; resolves when playback finishes. */
18
+ export declare function playPcm(pcm: Buffer, sampleRate: number, backend: AudioBackend, device: string, log: Logger): Promise<void>;
19
+ /** A short rising "listening" beep (mono 16-bit @ 16 kHz), synthesised once. */
20
+ export declare function pling(): Buffer;
package/build/audio.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Mic = void 0;
4
+ exports.resolveBackend = resolveBackend;
5
+ exports.ffmpegInput = ffmpegInput;
6
+ exports.playPcm = playPcm;
7
+ exports.pling = pling;
8
+ /**
9
+ * Audio I/O with two backends:
10
+ * - 'alsa' : spawn `arecord`/`aplay` — robust on a Pi, no native build (Linux only).
11
+ * - 'ffmpeg': spawn `ffmpeg`/`ffplay` — cross-platform (Windows / macOS / Linux).
12
+ *
13
+ * Capture is always 16 kHz mono 16-bit; use a `plughw:CARD,DEV` device (ALSA) so it resamples.
14
+ */
15
+ const node_child_process_1 = require("node:child_process");
16
+ const protocol_1 = require("./protocol");
17
+ /** Resolve 'auto' → alsa on Linux, ffmpeg elsewhere. */
18
+ function resolveBackend(pref) {
19
+ if (pref === 'alsa' || pref === 'ffmpeg') {
20
+ return pref;
21
+ }
22
+ return process.platform === 'linux' ? 'alsa' : 'ffmpeg';
23
+ }
24
+ /** ffmpeg capture input args per platform (device = dshow name / avfoundation index / ALSA name). */
25
+ function ffmpegInput(device) {
26
+ if (process.platform === 'win32') {
27
+ return ['-f', 'dshow', '-i', `audio=${device || 'default'}`];
28
+ }
29
+ if (process.platform === 'darwin') {
30
+ return ['-f', 'avfoundation', '-i', `:${device || '0'}`];
31
+ }
32
+ return ['-f', 'alsa', '-i', device || 'default'];
33
+ }
34
+ /** Continuous microphone capture at 16 kHz mono 16-bit; emits raw PCM chunks. */
35
+ class Mic {
36
+ backend;
37
+ device;
38
+ log;
39
+ proc = null;
40
+ constructor(backend, device, log) {
41
+ this.backend = backend;
42
+ this.device = device;
43
+ this.log = log;
44
+ }
45
+ start(onData) {
46
+ const [cmd, args] = this.backend === 'ffmpeg'
47
+ ? [
48
+ 'ffmpeg',
49
+ [
50
+ '-hide_banner',
51
+ '-loglevel',
52
+ 'error',
53
+ ...ffmpegInput(this.device),
54
+ '-ac',
55
+ '1',
56
+ '-ar',
57
+ String(protocol_1.AUDIO_SAMPLE_RATE),
58
+ '-f',
59
+ 's16le',
60
+ '-',
61
+ ],
62
+ ]
63
+ : [
64
+ 'arecord',
65
+ [
66
+ '-q',
67
+ '-t',
68
+ 'raw',
69
+ '-f',
70
+ 'S16_LE',
71
+ '-c',
72
+ '1',
73
+ '-r',
74
+ String(protocol_1.AUDIO_SAMPLE_RATE),
75
+ ...(this.device && this.device !== 'default' ? ['-D', this.device] : []),
76
+ ],
77
+ ];
78
+ this.proc = (0, node_child_process_1.spawn)(cmd, args);
79
+ this.proc.stdout?.on('data', (d) => onData(d));
80
+ this.proc.stderr?.on('data', (d) => this.log.debug(`${cmd}: ${d.toString().trim()}`));
81
+ this.proc.on('error', e => this.log.error(`${cmd} failed: ${e.message} — is it installed? ` +
82
+ (this.backend === 'ffmpeg' ? '(install ffmpeg)' : "(sudo apt install alsa-utils)")));
83
+ this.log.info(`Microphone capture started (${this.backend}: ${this.device || 'default'} @ ${protocol_1.AUDIO_SAMPLE_RATE} Hz).`);
84
+ }
85
+ stop() {
86
+ this.proc?.kill();
87
+ this.proc = null;
88
+ }
89
+ }
90
+ exports.Mic = Mic;
91
+ /** Play raw mono 16-bit PCM at the given rate; resolves when playback finishes. */
92
+ function playPcm(pcm, sampleRate, backend, device, log) {
93
+ return new Promise(resolve => {
94
+ const [cmd, args] = backend === 'ffmpeg'
95
+ ? [
96
+ 'ffplay',
97
+ ['-hide_banner', '-loglevel', 'error', '-nodisp', '-autoexit', '-f', 's16le', '-ar', String(sampleRate), '-ch_layout', 'mono', '-i', '-'],
98
+ ]
99
+ : [
100
+ 'aplay',
101
+ ['-q', '-t', 'raw', '-f', 'S16_LE', '-c', '1', '-r', String(sampleRate), ...(device && device !== 'default' ? ['-D', device] : [])],
102
+ ];
103
+ const proc = (0, node_child_process_1.spawn)(cmd, args);
104
+ proc.on('close', () => resolve());
105
+ proc.on('error', e => {
106
+ log.error(`${cmd} failed: ${e.message}`);
107
+ resolve();
108
+ });
109
+ proc.stdin?.end(pcm);
110
+ });
111
+ }
112
+ /** A short rising "listening" beep (mono 16-bit @ 16 kHz), synthesised once. */
113
+ function pling() {
114
+ const rate = protocol_1.AUDIO_SAMPLE_RATE;
115
+ const dur = 0.18;
116
+ const n = Math.floor(rate * dur);
117
+ const buf = Buffer.alloc(n * 2);
118
+ for (let i = 0; i < n; i++) {
119
+ const t = i / rate;
120
+ const freq = 880 + (1320 - 880) * (i / n);
121
+ const fade = Math.sin((Math.PI * i) / n);
122
+ const val = Math.max(-32768, Math.min(32767, Math.round(32767 * 0.6 * fade * Math.sin(2 * Math.PI * freq * t))));
123
+ buf.writeInt16LE(val, i * 2);
124
+ }
125
+ return buf;
126
+ }
127
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { type AudioBackend } from './audio';
2
+ import type { SatelliteConfig } from './config';
3
+ import type { Logger } from './index';
4
+ export type CheckStatus = 'ok' | 'warn' | 'fail';
5
+ export interface CheckResult {
6
+ name: string;
7
+ status: CheckStatus;
8
+ detail: string;
9
+ }
10
+ /** Run all checks. `forService` adds the systemd/root prerequisites. */
11
+ export declare function runChecks(cfg: SatelliteConfig, backend: AudioBackend, forService: boolean): CheckResult[];
12
+ /** Print the results; returns false if any check failed. */
13
+ export declare function printChecks(results: CheckResult[], log: Logger): boolean;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runChecks = runChecks;
37
+ exports.printChecks = printChecks;
38
+ /**
39
+ * Preflight checks: run before installing the service (and via the `check` subcommand) to verify
40
+ * everything is present — Node, systemd, root rights, audio tools, and functional mic-in / speaker-out.
41
+ */
42
+ const node_child_process_1 = require("node:child_process");
43
+ const fs = __importStar(require("node:fs"));
44
+ const audio_1 = require("./audio");
45
+ const ok = (name, detail = '') => ({ name, status: 'ok', detail });
46
+ const warn = (name, detail) => ({ name, status: 'warn', detail });
47
+ const fail = (name, detail) => ({ name, status: 'fail', detail });
48
+ function which(cmd) {
49
+ try {
50
+ (0, node_child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'which', [cmd], { stdio: 'ignore' });
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ const firstLine = (s) => s.split('\n')[0].slice(0, 140);
58
+ /** Capture ~1 s from the mic and confirm real bytes come out. */
59
+ function checkCapture(cfg, backend) {
60
+ try {
61
+ let out;
62
+ if (backend === 'ffmpeg') {
63
+ out = (0, node_child_process_1.execFileSync)('ffmpeg', ['-hide_banner', '-loglevel', 'error', ...(0, audio_1.ffmpegInput)(cfg.micDevice), '-t', '1', '-ac', '1', '-ar', '16000', '-f', 's16le', '-'], { timeout: 8000, maxBuffer: 1 << 22 });
64
+ }
65
+ else {
66
+ const args = ['-q', '-f', 'S16_LE', '-r', '16000', '-c', '1', '-d', '1', '-t', 'raw'];
67
+ if (cfg.micDevice && cfg.micDevice !== 'default') {
68
+ args.push('-D', cfg.micDevice);
69
+ }
70
+ out = (0, node_child_process_1.execFileSync)('arecord', args, { timeout: 8000, maxBuffer: 1 << 22 });
71
+ }
72
+ return out.length > 1000
73
+ ? ok('audio:in', `captured ${out.length} bytes from '${cfg.micDevice}'`)
74
+ : fail('audio:in', `captured only ${out.length} bytes — wrong device or no signal`);
75
+ }
76
+ catch (e) {
77
+ return fail('audio:in', `capture failed (device busy/wrong?): ${firstLine(e.message)}`);
78
+ }
79
+ }
80
+ /** Play 0.2 s of silence to confirm the output device opens. */
81
+ function checkPlayback(cfg, backend) {
82
+ const silence = Buffer.alloc(Math.round((16000 * 2) / 5)); // 0.2 s @ 16 kHz mono 16-bit
83
+ try {
84
+ if (backend === 'ffmpeg') {
85
+ (0, node_child_process_1.execFileSync)('ffplay', ['-hide_banner', '-loglevel', 'error', '-nodisp', '-autoexit', '-f', 's16le', '-ar', '16000', '-ch_layout', 'mono', '-i', '-'], { input: silence, timeout: 8000 });
86
+ }
87
+ else {
88
+ const args = ['-q', '-f', 'S16_LE', '-r', '16000', '-c', '1'];
89
+ if (cfg.speakerDevice && cfg.speakerDevice !== 'default') {
90
+ args.push('-D', cfg.speakerDevice);
91
+ }
92
+ (0, node_child_process_1.execFileSync)('aplay', args, { input: silence, timeout: 8000 });
93
+ }
94
+ return ok('audio:out', `output device '${cfg.speakerDevice}' opened`);
95
+ }
96
+ catch (e) {
97
+ return fail('audio:out', `playback failed (device busy/wrong?): ${firstLine(e.message)}`);
98
+ }
99
+ }
100
+ /** Run all checks. `forService` adds the systemd/root prerequisites. */
101
+ function runChecks(cfg, backend, forService) {
102
+ const results = [];
103
+ const major = Number(process.versions.node.split('.')[0]);
104
+ results.push(major >= 18 ? ok('node', `v${process.versions.node}`) : fail('node', `v${process.versions.node} (need ≥ 18)`));
105
+ if (forService) {
106
+ results.push(process.platform === 'linux'
107
+ ? ok('platform', 'linux')
108
+ : fail('platform', `${process.platform} — systemd install is Linux-only`));
109
+ results.push(which('systemctl') ? ok('systemctl', 'found') : fail('systemctl', 'not found'));
110
+ const root = typeof process.getuid === 'function' && process.getuid() === 0;
111
+ results.push(root ? ok('root', 'running as root') : warn('root', 'not root — run install with sudo'));
112
+ }
113
+ for (const t of backend === 'ffmpeg' ? ['ffmpeg', 'ffplay'] : ['arecord', 'aplay']) {
114
+ results.push(which(t)
115
+ ? ok(`tool:${t}`, 'found')
116
+ : fail(`tool:${t}`, `not found — install ${backend === 'ffmpeg' ? 'ffmpeg' : 'alsa-utils'}`));
117
+ }
118
+ results.push(checkCapture(cfg, backend));
119
+ results.push(checkPlayback(cfg, backend));
120
+ results.push(cfg.host || cfg.mqttBroker
121
+ ? ok('config:server', cfg.host ? `host ${cfg.host}:${cfg.port}` : `discovery via ${cfg.mqttBroker}`)
122
+ : fail('config:server', 'set "host" (or "mqttBroker") so the satellite can reach the adapter'));
123
+ try {
124
+ fs.mkdirSync(cfg.modelsDir, { recursive: true });
125
+ fs.accessSync(cfg.modelsDir, fs.constants.W_OK);
126
+ results.push(ok('models', `'${cfg.modelsDir}' writable`));
127
+ }
128
+ catch {
129
+ results.push(warn('models', `'${cfg.modelsDir}' not writable — model download may fail`));
130
+ }
131
+ return results;
132
+ }
133
+ /** Print the results; returns false if any check failed. */
134
+ function printChecks(results, log) {
135
+ const label = { ok: 'OK ', warn: 'WARN', fail: 'FAIL' };
136
+ for (const c of results) {
137
+ const line = `[${label[c.status]}] ${c.name}${c.detail ? ` — ${c.detail}` : ''}`;
138
+ if (c.status === 'fail') {
139
+ log.error(line);
140
+ }
141
+ else if (c.status === 'warn') {
142
+ log.warn(line);
143
+ }
144
+ else {
145
+ log.info(line);
146
+ }
147
+ }
148
+ return !results.some(c => c.status === 'fail');
149
+ }
150
+ //# sourceMappingURL=checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"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"}
package/build/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/cli.js ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ /**
38
+ * CLI entry point:
39
+ * assistant-satellite [config.json] run the satellite
40
+ * assistant-satellite install [config.json] install + start a systemd service (Linux, needs sudo)
41
+ * assistant-satellite uninstall stop + remove the systemd service (needs sudo)
42
+ */
43
+ const fs = __importStar(require("node:fs"));
44
+ const index_1 = require("./index");
45
+ const service_1 = require("./service");
46
+ const checks_1 = require("./checks");
47
+ const audio_1 = require("./audio");
48
+ // Debug is off until we know the config's logLevel; the DEBUG env var forces it on regardless.
49
+ let debugEnabled = !!process.env.DEBUG;
50
+ const log = {
51
+ info: (m) => console.log(`[INFO] ${m}`),
52
+ warn: (m) => console.warn(`[WARN] ${m}`),
53
+ error: (m) => console.error(`[ERROR] ${m}`),
54
+ debug: (m) => {
55
+ if (debugEnabled) {
56
+ console.log(`[DEBUG] ${m}`);
57
+ }
58
+ },
59
+ };
60
+ const [command, ...rest] = process.argv.slice(2);
61
+ const configArg = rest.find(a => !a.startsWith('--')) || 'config.json';
62
+ // `check` — run preflight checks (rights, audio in/out, tools, config) and exit.
63
+ if (command === 'check') {
64
+ if (!fs.existsSync(configArg)) {
65
+ log.error(`config not found: ${configArg}`);
66
+ process.exit(1);
67
+ }
68
+ try {
69
+ const c = (0, index_1.loadConfig)(JSON.parse(fs.readFileSync(configArg, 'utf8')));
70
+ const passed = (0, checks_1.printChecks)((0, checks_1.runChecks)(c, (0, audio_1.resolveBackend)(c.audioBackend), true), log);
71
+ process.exit(passed ? 0 : 1);
72
+ }
73
+ catch (e) {
74
+ log.error(e.message);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ // Service management subcommands (Linux/systemd). `install` runs the checks first.
79
+ if (command === 'install' || command === '--install') {
80
+ try {
81
+ (0, service_1.installService)(configArg, log, rest.includes('--force'));
82
+ process.exit(0);
83
+ }
84
+ catch (e) {
85
+ log.error(e.message);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ if (command === 'uninstall' || command === '--uninstall') {
90
+ try {
91
+ (0, service_1.uninstallService)(log);
92
+ process.exit(0);
93
+ }
94
+ catch (e) {
95
+ log.error(e.message);
96
+ process.exit(1);
97
+ }
98
+ }
99
+ const configPath = command || 'config.json';
100
+ if (!fs.existsSync(configPath)) {
101
+ fs.writeFileSync(configPath, `${JSON.stringify(index_1.DEFAULT_CONFIG, null, 4)}\n`);
102
+ log.info(`No config found — wrote defaults to ${configPath}.`);
103
+ log.info('Edit it (at least set "host" and "device"), then run again.');
104
+ process.exit(0);
105
+ }
106
+ let cfg;
107
+ try {
108
+ cfg = (0, index_1.loadConfig)(JSON.parse(fs.readFileSync(configPath, 'utf8')));
109
+ }
110
+ catch (e) {
111
+ log.error(`Cannot read ${configPath}: ${e.message}`);
112
+ process.exit(1);
113
+ }
114
+ if (cfg.logLevel === 'debug') {
115
+ debugEnabled = true;
116
+ }
117
+ const satellite = new index_1.Satellite(cfg, { log, onStatus: s => log.debug(`status: ${s}`) });
118
+ satellite.start().catch(e => {
119
+ log.error(e.message);
120
+ process.exit(1);
121
+ });
122
+ let stopping = false;
123
+ async function shutdown() {
124
+ if (stopping) {
125
+ return;
126
+ }
127
+ stopping = true;
128
+ log.info('Shutting down …');
129
+ await satellite.stop();
130
+ process.exit(0);
131
+ }
132
+ process.on('SIGINT', () => void shutdown());
133
+ process.on('SIGTERM', () => void shutdown());
134
+ //# sourceMappingURL=cli.js.map