robopark 2.8.35 → 3.0.0
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/README.md +88 -63
- package/bin/robopark.js +7 -17
- package/conversation/elevenlabs_agent.py +1985 -0
- package/conversation/requirements.txt +3 -0
- package/conversation/supervisor_store.py +189 -0
- package/dist/kernel/config-schema.js +37 -0
- package/dist/kernel/types.js +7 -0
- package/dist/robopark/access.js +99 -0
- package/dist/robopark/add-robot.js +188 -0
- package/dist/robopark/agent-ctl.js +305 -0
- package/dist/robopark/auto-start.js +289 -0
- package/dist/robopark/conversation.js +505 -0
- package/dist/robopark/deployment-commands.js +47 -0
- package/dist/robopark/discovery.js +180 -0
- package/dist/robopark/doctor.js +175 -0
- package/dist/robopark/enroll.js +68 -0
- package/dist/robopark/llm-set.js +87 -0
- package/dist/robopark/motor-control.js +195 -0
- package/dist/robopark/preview-agent-launcher.js +77 -0
- package/dist/robopark/probe.js +138 -0
- package/dist/robopark/profile.js +69 -0
- package/dist/robopark/python-env.js +162 -0
- package/dist/robopark/robot-runtime.js +489 -0
- package/dist/robopark/scan.js +97 -0
- package/dist/robopark/screen-control.js +55 -0
- package/dist/robopark/secrets.js +41 -0
- package/dist/robopark/serve.js +285 -0
- package/dist/robopark/server-add.js +114 -0
- package/dist/robopark/setup-livekit.js +300 -0
- package/dist/robopark/setup.js +286 -0
- package/dist/robopark/standalone.js +466 -0
- package/dist/robopark/stop-all.js +141 -0
- package/dist/robopark/verify.js +192 -0
- package/dist/robopark/vision-agent-launcher.js +98 -0
- package/dist/robopark/vision-control.js +81 -0
- package/dist/robopark-cli.js +799 -0
- package/package.json +21 -5
- package/pi-client/_install_steps.sh +29 -29
- package/pi-client/client.py +61 -2
- package/pi-client/install.sh +40 -40
- package/pi-client/join_convo.sh +54 -54
- package/pi-client/livekit_bridge.py +16 -7
- package/pi-client/motor_bridge.py +6 -3
- package/scheduler/fleet_config.json +75 -0
- package/scheduler/main.py +4505 -135
- package/scheduler/media_lock.py +57 -0
- package/scheduler/preview_agent.py +1465 -87
- package/scheduler/production_config.json +139 -0
- package/scheduler/robot_supervisor.py +1705 -0
- package/scheduler/scripts/install-robot-supervisor-linux.sh +33 -0
- package/scheduler/scripts/install-robot-supervisor-windows.ps1 +49 -0
- package/scheduler/scripts/robopark-supervisor.service +20 -0
- package/scheduler/scripts/start-scheduler-local.ps1 +50 -0
- package/scheduler/supervisor.example.json +26 -0
- package/scheduler/vision_motion_trigger.py +101 -0
- package/screen/screen_runtime.py +75 -0
- package/vision/app_pi_clean.py +253 -16
- package/vision/audio_server_pi.py +19 -0
- package/vision/install.sh +34 -34
- package/vision/motor_server.py +224 -61
- package/vision/requirements_camera.txt +6 -0
- package/vision/requirements_motor.txt +4 -0
- package/vision/requirements_pi_unified.txt +1 -0
- package/vision/requirements_vision_agent.txt +19 -0
- package/vision/run.sh +244 -244
- package/vision/services/services.sh +12 -12
- package/scheduler/__pycache__/main.cpython-312.pyc +0 -0
- package/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark CLI entry point (shipped as `infinicode/robopark` and `robopark`).
|
|
3
|
+
*
|
|
4
|
+
* A thin operator layer over the infinicode mesh. Keeps product concerns
|
|
5
|
+
* (scheduler, fleet UI, video sessions, InfiniBot map) separate from the
|
|
6
|
+
* execution kernel.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import Conf from 'conf';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { roboparkScan } from './robopark/scan.js';
|
|
15
|
+
import { roboparkServe } from './robopark/serve.js';
|
|
16
|
+
import { roboparkSetup } from './robopark/setup.js';
|
|
17
|
+
import { stopAll } from './robopark/stop-all.js';
|
|
18
|
+
import { roboparkDoctor } from './robopark/doctor.js';
|
|
19
|
+
import { roboparkAddRobot } from './robopark/add-robot.js';
|
|
20
|
+
import { saveHubProfile, clearHubProfile } from './robopark/profile.js';
|
|
21
|
+
import { roboparkAgentUp, roboparkAgentDown, roboparkAgentLogs } from './robopark/agent-ctl.js';
|
|
22
|
+
import { roboparkSecrets } from './robopark/secrets.js';
|
|
23
|
+
import { roboparkRobotRuntime } from './robopark/robot-runtime.js';
|
|
24
|
+
import { roboparkVisionDown, roboparkVisionRestart, roboparkVisionStatus, roboparkVisionUp, } from './robopark/vision-control.js';
|
|
25
|
+
import { roboparkConversationDown, roboparkConversationInstall, roboparkConversationRestart, roboparkConversationStart, roboparkConversationStop, roboparkConversationUp, } from './robopark/conversation.js';
|
|
26
|
+
import { roboparkScreenDown, roboparkScreenInstall, roboparkScreenRestart, roboparkScreenUp } from './robopark/screen-control.js';
|
|
27
|
+
import { roboparkMotorDiscover, roboparkMotorDown, roboparkMotorRestart, roboparkMotorStatus, roboparkMotorStop, roboparkMotorTest, roboparkMotorUp, } from './robopark/motor-control.js';
|
|
28
|
+
import { connectDevice, gatewayStart, printRuntimeStatus, runGatewayService, startDevice, superviseDevice, } from './robopark/standalone.js';
|
|
29
|
+
import { requirePackageAccess } from './robopark/access.js';
|
|
30
|
+
import { deploymentCommandSheet } from './robopark/deployment-commands.js';
|
|
31
|
+
// Read the real version from package.json so `--version` never drifts from
|
|
32
|
+
// what is actually installed (this was a hardcoded constant that went stale
|
|
33
|
+
// across several releases — see the identical fix + comment in cli.ts).
|
|
34
|
+
function pkgVersion() {
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
for (const rel of ['../package.json', '../../package.json']) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(join(here, rel), 'utf8')).version ?? '0.0.0';
|
|
39
|
+
}
|
|
40
|
+
catch { /* try next */ }
|
|
41
|
+
}
|
|
42
|
+
return '0.0.0';
|
|
43
|
+
}
|
|
44
|
+
const config = new Conf({
|
|
45
|
+
// Preserve the deployed config location so packaging never forks an
|
|
46
|
+
// existing robot into a fresh identity/config namespace.
|
|
47
|
+
projectName: 'infinicode',
|
|
48
|
+
...(process.env.ROBOPARK_CONFIG_DIR ? { cwd: process.env.ROBOPARK_CONFIG_DIR } : {}),
|
|
49
|
+
});
|
|
50
|
+
const program = new Command('robopark')
|
|
51
|
+
.description('RoboPark fleet control CLI — set up, watch, and drive talking robots')
|
|
52
|
+
.version(pkgVersion())
|
|
53
|
+
.option('--password <password>', 'one-time package activation password (prefer ROBOPARK_PACKAGE_PASSWORD)');
|
|
54
|
+
program.hook('preAction', (_command, actionCommand) => {
|
|
55
|
+
// A valid gateway pairing token is itself the device bootstrap capability.
|
|
56
|
+
// connectDevice() records activation only after enrollment succeeds.
|
|
57
|
+
if (actionCommand.name() === 'connect' || actionCommand.name() === 'commands')
|
|
58
|
+
return;
|
|
59
|
+
requirePackageAccess(program.opts().password);
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command('commands')
|
|
63
|
+
.description('Print the exact gateway and persistent per-character deployment commands')
|
|
64
|
+
.option('--token <token>', 'include a gateway pairing token in the device command')
|
|
65
|
+
.option('--public-url <url>', 'include the gateway public/Tailscale URL')
|
|
66
|
+
.action((opts) => {
|
|
67
|
+
try {
|
|
68
|
+
console.log(deploymentCommandSheet({ version: pkgVersion(), ...opts }));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error(chalk.red(` ✗ command sheet failed: ${err?.message ?? err}`));
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const gateway = program
|
|
76
|
+
.command('gateway')
|
|
77
|
+
.description('Run the standalone RoboPark dashboard, scheduler, and device gateway');
|
|
78
|
+
gateway
|
|
79
|
+
.command('start')
|
|
80
|
+
.description('Start the dashboard/gateway and print a one-line device connection token')
|
|
81
|
+
.option('--port <port>', 'gateway HTTP port', '8080')
|
|
82
|
+
.option('--host <host>', 'gateway bind host', '0.0.0.0')
|
|
83
|
+
.option('--public-url <url>', 'URL devices use to reach this gateway')
|
|
84
|
+
.option('--data-dir <dir>', 'durable gateway data directory')
|
|
85
|
+
.option('--no-open', 'do not open the dashboard')
|
|
86
|
+
.option('--foreground', 'keep the gateway attached to this terminal')
|
|
87
|
+
.action(async (opts) => {
|
|
88
|
+
try {
|
|
89
|
+
await gatewayStart(opts);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.error(chalk.red(` ✗ gateway start failed: ${err?.message ?? err}`));
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
program
|
|
97
|
+
.command('connect <token>')
|
|
98
|
+
.description('Register this device with a gateway using one copy/paste token')
|
|
99
|
+
.option('--name <name>', 'device name (defaults to hostname)')
|
|
100
|
+
.action(async (token, opts) => {
|
|
101
|
+
try {
|
|
102
|
+
await connectDevice(token, opts);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error(chalk.red(` ✗ connect failed: ${err?.message ?? err}`));
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
program
|
|
110
|
+
.command('start')
|
|
111
|
+
.description('Persist and supervise this device camera/voice runtime')
|
|
112
|
+
.option('--voice', 'stream microphone audio and accept supervised voice sessions')
|
|
113
|
+
.option('--video', 'stream the selected camera')
|
|
114
|
+
.option('--character <id>', 'gateway character preset, e.g. vixen')
|
|
115
|
+
.option('--video-device <device>', 'camera selection', 'auto')
|
|
116
|
+
.option('--audio-device <device>', 'microphone selection', 'default')
|
|
117
|
+
.option('--width <px>', 'video width', '640')
|
|
118
|
+
.option('--height <px>', 'video height', '480')
|
|
119
|
+
.option('--fps <n>', 'video frame rate', '15')
|
|
120
|
+
.option('--foreground', 'run the supervisor in this terminal')
|
|
121
|
+
.action(async (opts) => {
|
|
122
|
+
try {
|
|
123
|
+
await startDevice(opts);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.error(chalk.red(` ✗ start failed: ${err?.message ?? err}`));
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
program
|
|
131
|
+
.command('status')
|
|
132
|
+
.description('Show the local supervisor state')
|
|
133
|
+
.action(() => {
|
|
134
|
+
try {
|
|
135
|
+
printRuntimeStatus();
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
console.error(chalk.red(` ✗ status failed: ${err?.message ?? err}`));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
program
|
|
143
|
+
.command('_gateway', { hidden: true })
|
|
144
|
+
.option('--port <port>', 'gateway HTTP port', '8080')
|
|
145
|
+
.option('--host <host>', 'gateway bind host', '0.0.0.0')
|
|
146
|
+
.option('--data-dir <dir>', 'durable gateway data directory')
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
try {
|
|
149
|
+
await runGatewayService({ ...opts, open: false, foreground: true });
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
console.error(chalk.red(` gateway service fatal: ${err?.message ?? err}`));
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
program
|
|
157
|
+
.command('_supervise', { hidden: true })
|
|
158
|
+
.requiredOption('--config <path>')
|
|
159
|
+
.action(async (opts) => {
|
|
160
|
+
try {
|
|
161
|
+
await superviseDevice(opts.config);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.error(chalk.red(` supervisor fatal: ${err?.message ?? err}`));
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const conversation = program
|
|
169
|
+
.command('conversation')
|
|
170
|
+
.description('Run a direct motion/voice-triggered ElevenLabs agent on robot hardware');
|
|
171
|
+
conversation
|
|
172
|
+
.command('install')
|
|
173
|
+
.description('Idempotently install and verify Pi audio/Python dependencies')
|
|
174
|
+
.option('--with-vision', 'also install and verify RoboVision dependencies')
|
|
175
|
+
.option('--skip-apt', 'skip apt and only repair/verify the RoboPark Python environment')
|
|
176
|
+
.action(async (opts) => {
|
|
177
|
+
try {
|
|
178
|
+
await roboparkConversationInstall(opts);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(chalk.red(` ✗ conversation install failed: ${err?.message ?? err}`));
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
conversation
|
|
186
|
+
.command('up')
|
|
187
|
+
.description('Arm motion and local voice triggers, then join the configured ElevenLabs agent')
|
|
188
|
+
.requiredOption('--agent-id <id>', 'ElevenLabs agent id')
|
|
189
|
+
.option('--branch-id <id>', 'pin conversations to an ElevenLabs agent branch')
|
|
190
|
+
.option('--api-key <key>', 'ElevenLabs API key (prefer ELEVENLABS_API_KEY)')
|
|
191
|
+
.option('--public-agent', 'agent does not require an API key')
|
|
192
|
+
.option('--always-on', 'join immediately and reconnect forever without waiting for a trigger')
|
|
193
|
+
.option('--name <name>', 'robot name (defaults to hostname)')
|
|
194
|
+
.option('--audio-input <device>', 'stable USB microphone name or ALSA/PyAudio device', 'Usb Audio Device: USB Audio')
|
|
195
|
+
.option('--audio-output <device>', 'stable USB speaker name or ALSA/PyAudio device', 'USB Audio Device: -')
|
|
196
|
+
.option('--no-motion', 'disable the local motion webhook')
|
|
197
|
+
.option('--no-voice-trigger', 'disable the local sound-level trigger')
|
|
198
|
+
.option('--motion-host <host>', 'motion webhook bind host', '127.0.0.1')
|
|
199
|
+
.option('--motion-port <port>', 'motion webhook port', '5057')
|
|
200
|
+
.option('--vision-url <url>', 'running RoboVision server to configure for motion', 'http://127.0.0.1:5000')
|
|
201
|
+
.option('--no-manage-vision', 'do not start and supervise the local RoboVision camera process')
|
|
202
|
+
.option('--camera-device <device>', 'RoboVision camera device (auto prefers /dev/robopark-camera)', 'auto')
|
|
203
|
+
.option('--voice-threshold <rms>', 'idle microphone RMS required to trigger', '900')
|
|
204
|
+
.option('--idle-timeout <sec>', 'end after this many seconds without a transcript; 0 disables', '120')
|
|
205
|
+
.option('--max-session <sec>', 'hard maximum conversation duration; 0 disables', '540')
|
|
206
|
+
.option('--cooldown <sec>', 'minimum delay before another session', '2')
|
|
207
|
+
.option('--motor <pin-name>', 'register motor/relay, e.g. 17-head (repeatable)', (value, previous) => [...previous, value], [])
|
|
208
|
+
.option('--motor-active-high', 'registered motor relays activate HIGH (default: active-low)')
|
|
209
|
+
.option('--motor-pulse-ms <ms>', 'default bounded motor pulse duration', '300')
|
|
210
|
+
.option('--no-provision-agent-tools', 'skip automatic ElevenLabs robotMotor tool creation/attachment')
|
|
211
|
+
.option('--auto-start', 'install and start a production systemd service')
|
|
212
|
+
.action(async (opts) => {
|
|
213
|
+
try {
|
|
214
|
+
await roboparkConversationUp(opts);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.error(chalk.red(` ✗ conversation failed: ${err?.message ?? err}`));
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
conversation
|
|
222
|
+
.command('stop')
|
|
223
|
+
.description('Stop local conversation workers without deleting their configuration')
|
|
224
|
+
.option('--name <name>', 'explicit service override (normally unnecessary)')
|
|
225
|
+
.action((opts) => {
|
|
226
|
+
try {
|
|
227
|
+
roboparkConversationStop(opts.name);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
console.error(chalk.red(` ✗ conversation stop failed: ${err?.message ?? err}`));
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
conversation
|
|
235
|
+
.command('start')
|
|
236
|
+
.description('Start the selected local conversation worker and wait for real readiness')
|
|
237
|
+
.option('--name <name>', 'explicit service override (normally unnecessary)')
|
|
238
|
+
.option('--trigger', 'trigger one conversation after readiness')
|
|
239
|
+
.action(async (opts) => {
|
|
240
|
+
try {
|
|
241
|
+
await roboparkConversationStart(opts.name, Boolean(opts.trigger));
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.error(chalk.red(` ✗ conversation start failed: ${err?.message ?? err}`));
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
conversation
|
|
249
|
+
.command('down')
|
|
250
|
+
.description('Stop and remove the direct conversation auto-start service')
|
|
251
|
+
.option('--name <name>', 'robot name (defaults to hostname)')
|
|
252
|
+
.action((opts) => {
|
|
253
|
+
try {
|
|
254
|
+
roboparkConversationDown(opts.name);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error(chalk.red(` ✗ conversation stop failed: ${err?.message ?? err}`));
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
conversation
|
|
262
|
+
.command('restart')
|
|
263
|
+
.description('Cleanly restart a production conversation worker and wait until it is ready')
|
|
264
|
+
.option('--name <name>', 'explicit service override (normally unnecessary)')
|
|
265
|
+
.option('--trigger', 'trigger one conversation after readiness')
|
|
266
|
+
.action(async (opts) => {
|
|
267
|
+
try {
|
|
268
|
+
await roboparkConversationRestart(opts.name, Boolean(opts.trigger));
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
console.error(chalk.red(` ✗ conversation restart failed: ${err?.message ?? err}`));
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
program
|
|
276
|
+
.command('scan')
|
|
277
|
+
.description('Auto-discover the fleet and print ready-to-run setup commands')
|
|
278
|
+
.option('--token <token>', 'mesh auth token to use')
|
|
279
|
+
.option('--output <dir>', 'write install scripts to this directory')
|
|
280
|
+
.option('--no-gateway', 'skip InfiniBot gateway in generated commands')
|
|
281
|
+
.action(async (opts) => {
|
|
282
|
+
await roboparkScan({
|
|
283
|
+
token: opts.token,
|
|
284
|
+
output: opts.output,
|
|
285
|
+
gateway: opts.gateway,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
program
|
|
289
|
+
.command('probe')
|
|
290
|
+
.description('Dispatch opencode agents across the mesh to map infrastructure')
|
|
291
|
+
.option('--token <token>', 'mesh auth token (optional, for context)')
|
|
292
|
+
.option('--timeout <sec>', 'seconds per node', '60')
|
|
293
|
+
.option('--output <file>', 'write JSON results to this file')
|
|
294
|
+
.option('--local', 'probe only this machine')
|
|
295
|
+
.action(async (opts) => {
|
|
296
|
+
const { roboparkProbe } = await import('./robopark/probe.js');
|
|
297
|
+
await roboparkProbe(opts);
|
|
298
|
+
});
|
|
299
|
+
program
|
|
300
|
+
.command('setup-livekit')
|
|
301
|
+
.description('Dispatch an opencode agent on the hub to install LiveKit server via Docker')
|
|
302
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
303
|
+
.option('--token <token>', 'mesh auth token')
|
|
304
|
+
.option('--scheduler-url <url>', 'scheduler URL to register the new server, e.g. http://100.64.1.2:8080')
|
|
305
|
+
.option('--timeout <sec>', 'max seconds to wait for the agent', '180')
|
|
306
|
+
.option('--dry-run', 'print the prompt instead of dispatching')
|
|
307
|
+
.action(async (opts) => {
|
|
308
|
+
const { roboparkSetupLivekit } = await import('./robopark/setup-livekit.js');
|
|
309
|
+
await roboparkSetupLivekit(opts);
|
|
310
|
+
});
|
|
311
|
+
program
|
|
312
|
+
.command('verify')
|
|
313
|
+
.description('Verify the RoboPark fleet is ready for live preview')
|
|
314
|
+
.option('--scheduler-url <url>', 'scheduler base URL, e.g. http://livekit-1:8080')
|
|
315
|
+
.option('--robot-id <id>', 'robot name to verify (defaults to hostname)')
|
|
316
|
+
.action(async (opts) => {
|
|
317
|
+
const { roboparkVerify } = await import('./robopark/verify.js');
|
|
318
|
+
await roboparkVerify(opts);
|
|
319
|
+
});
|
|
320
|
+
program
|
|
321
|
+
.command('serve')
|
|
322
|
+
.description('Start the RoboPark scheduler + web UI')
|
|
323
|
+
.option('--port <port>', 'scheduler HTTP port', '8080')
|
|
324
|
+
.option('--host <host>', 'scheduler bind host', '0.0.0.0')
|
|
325
|
+
.option('--data-dir <dir>', 'scheduler data directory')
|
|
326
|
+
.option('--gateway <url>', 'InfiniBot gateway ws:// URL')
|
|
327
|
+
.option('--gateway-token <token>', 'InfiniBot gateway token')
|
|
328
|
+
.option('--gateway-password <pw>', 'InfiniBot gateway password')
|
|
329
|
+
.option('--no-open', 'do not open the RoboPark Control Center in a browser')
|
|
330
|
+
.option('--foreground', 'stay in foreground; do not detach')
|
|
331
|
+
.action(async (opts) => {
|
|
332
|
+
await roboparkServe(opts);
|
|
333
|
+
});
|
|
334
|
+
program
|
|
335
|
+
.command('secrets')
|
|
336
|
+
.description('Provision the scheduler-managed ROBOVOICE authentication secret')
|
|
337
|
+
.option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
|
|
338
|
+
.option('--worker-env <path>', 'ROBOVOICE worker .env file', '.env')
|
|
339
|
+
.option('--rotate', 'rotate the shared secret before synchronizing it')
|
|
340
|
+
.action(async (opts) => {
|
|
341
|
+
try {
|
|
342
|
+
await roboparkSecrets(opts);
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
console.error(chalk.red(` ✗ secret provisioning failed: ${err?.message ?? err}`));
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
program
|
|
350
|
+
.command('enroll')
|
|
351
|
+
.description('Enroll this robot/satellite with the RoboPark scheduler')
|
|
352
|
+
.option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
|
|
353
|
+
.option('--robot-id <id>', 'robot/device name (defaults to hostname)')
|
|
354
|
+
.option('--enrollment-token <token>', 'one-time enrollment token (or ENV ENROLLMENT_TOKEN)')
|
|
355
|
+
.option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
|
|
356
|
+
.action(async (opts) => {
|
|
357
|
+
const { roboparkEnroll } = await import('./robopark/enroll.js');
|
|
358
|
+
await roboparkEnroll(opts);
|
|
359
|
+
});
|
|
360
|
+
program
|
|
361
|
+
.command('preview-agent')
|
|
362
|
+
.description('Start the robot-side preview agent (camera + mic to scheduler)')
|
|
363
|
+
.option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
|
|
364
|
+
.option('--robot-id <id>', 'robot/device name (defaults to hostname)')
|
|
365
|
+
.option('--device-token <token>', 'long-lived device token')
|
|
366
|
+
.option('--enrollment-token <token>', 'one-time enrollment token')
|
|
367
|
+
.option('--video-device <dev>', 'camera: auto, /dev/video0, 0, picamera', 'auto')
|
|
368
|
+
.option('--audio-device <dev>', 'microphone: default, none, or name/index', 'default')
|
|
369
|
+
.option('--robovision-url <url>', 'RoboVision base URL used for camera streaming and hardware inventory')
|
|
370
|
+
.option('--width <px>', 'video width', '640')
|
|
371
|
+
.option('--height <px>', 'video height', '480')
|
|
372
|
+
.option('--fps <n>', 'video fps', '15')
|
|
373
|
+
.option('--vision-webhook-port <port>', 'local port for RoboVisionAI_PI\'s motion webhook (0 disables)', '5057')
|
|
374
|
+
.option('--vision-trigger-cooldown <sec>', 'min seconds between motion-triggered sessions', '20')
|
|
375
|
+
.option('--vision-session-seconds <sec>', 'how long a motion-triggered session holds the publisher', '90')
|
|
376
|
+
.option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
|
|
377
|
+
.option('--foreground', 'stay in foreground; do not detach')
|
|
378
|
+
.action(async (opts) => {
|
|
379
|
+
const { roboparkPreviewAgent } = await import('./robopark/preview-agent-launcher.js');
|
|
380
|
+
await roboparkPreviewAgent(opts);
|
|
381
|
+
});
|
|
382
|
+
program
|
|
383
|
+
.command('vision-agent')
|
|
384
|
+
.description('Start the robot-side camera/motion detector (RoboVisionAI_PI) — self-arms and wires to preview-agent\'s vision webhook')
|
|
385
|
+
.option('--port <port>', 'vision server port', '5000')
|
|
386
|
+
.option('--motion-webhook-url <url>', 'where to POST on motion (defaults to the local preview-agent\'s vision webhook, http://127.0.0.1:5057/)')
|
|
387
|
+
.option('--no-motion-active', 'do not arm motion detection on startup (armed by default)')
|
|
388
|
+
.option('--foreground', 'stay in foreground; do not detach')
|
|
389
|
+
.action(async (opts) => {
|
|
390
|
+
const { roboparkVisionAgent } = await import('./robopark/vision-agent-launcher.js');
|
|
391
|
+
await roboparkVisionAgent(opts);
|
|
392
|
+
});
|
|
393
|
+
const visionProgram = program
|
|
394
|
+
.command('vision')
|
|
395
|
+
.description('Register and operate an always-on robot camera feed independently of voice calls');
|
|
396
|
+
const collectOption = (value, previous) => previous.concat(value);
|
|
397
|
+
const motorProgram = program
|
|
398
|
+
.command('motor')
|
|
399
|
+
.description('Register and operate a persistent robot-local GPIO motor server');
|
|
400
|
+
motorProgram.command('up <name>')
|
|
401
|
+
.description('Start the motor server and persist its safe relay registry')
|
|
402
|
+
.option('--token <token>', 'motor API token (defaults to the saved mesh token)')
|
|
403
|
+
.option('--port <port>', 'loopback motor API port', '8001')
|
|
404
|
+
.option('--motor <gpio-name>', 'register a relay, e.g. 17-head or 27-eyes:low', collectOption, [])
|
|
405
|
+
.option('--no-auto-start', 'run without registering the service at boot')
|
|
406
|
+
.action(async (name, opts) => {
|
|
407
|
+
try {
|
|
408
|
+
await roboparkMotorUp(name, opts);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
console.error(chalk.red(` ✗ motor up failed: ${err?.message ?? err}`));
|
|
412
|
+
process.exitCode = 1;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
motorProgram.command('status <name>')
|
|
416
|
+
.description('Show motor service, GPIO, and registry health')
|
|
417
|
+
.action(async (name) => {
|
|
418
|
+
try {
|
|
419
|
+
await roboparkMotorStatus(name);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
console.error(chalk.red(` ✗ motor status failed: ${err?.message ?? err}`));
|
|
423
|
+
process.exitCode = 1;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
motorProgram.command('discover <name>')
|
|
427
|
+
.description('Read the robot-local persisted motor registry')
|
|
428
|
+
.action(async (name) => {
|
|
429
|
+
try {
|
|
430
|
+
await roboparkMotorDiscover(name);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
console.error(chalk.red(` ✗ motor discovery failed: ${err?.message ?? err}`));
|
|
434
|
+
process.exitCode = 1;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
motorProgram.command('test <name> [motor]')
|
|
438
|
+
.description('Pulse one registered motor, or safely test all registered relays')
|
|
439
|
+
.option('--seconds <seconds>', 'pulse duration', '0.3')
|
|
440
|
+
.action(async (name, motor, opts) => {
|
|
441
|
+
try {
|
|
442
|
+
await roboparkMotorTest(name, motor, Number(opts.seconds));
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
console.error(chalk.red(` ✗ motor test failed: ${err?.message ?? err}`));
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
motorProgram.command('stop <name>')
|
|
450
|
+
.description('Immediately de-energize all registered relays')
|
|
451
|
+
.action(async (name) => {
|
|
452
|
+
try {
|
|
453
|
+
await roboparkMotorStop(name);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
console.error(chalk.red(` ✗ motor stop failed: ${err?.message ?? err}`));
|
|
457
|
+
process.exitCode = 1;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
motorProgram.command('restart <name>')
|
|
461
|
+
.description('Cleanly restart the persistent motor server')
|
|
462
|
+
.action((name) => {
|
|
463
|
+
try {
|
|
464
|
+
roboparkMotorRestart(name);
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
console.error(chalk.red(` ✗ motor restart failed: ${err?.message ?? err}`));
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
motorProgram.command('down <name>')
|
|
472
|
+
.description('Stop and remove the persistent motor server')
|
|
473
|
+
.action((name) => {
|
|
474
|
+
try {
|
|
475
|
+
roboparkMotorDown(name);
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
console.error(chalk.red(` ✗ motor down failed: ${err?.message ?? err}`));
|
|
479
|
+
process.exitCode = 1;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
const screenProgram = program
|
|
483
|
+
.command('screen')
|
|
484
|
+
.description('Operate an authenticated internal noVNC desktop relay over the Infinicode mesh');
|
|
485
|
+
screenProgram.command('install')
|
|
486
|
+
.description('Install Wayland/X11 VNC and noVNC relay dependencies on a Pi')
|
|
487
|
+
.action(() => { try {
|
|
488
|
+
roboparkScreenInstall();
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
console.error(chalk.red(` ✗ screen install failed: ${err?.message ?? err}`));
|
|
492
|
+
process.exitCode = 1;
|
|
493
|
+
} });
|
|
494
|
+
screenProgram.command('up <name>')
|
|
495
|
+
.description('Start and persist a loopback-only desktop relay for a robot')
|
|
496
|
+
.option('--desktop-user <user>', 'Linux desktop account to share')
|
|
497
|
+
.action(async (name, opts) => { try {
|
|
498
|
+
await roboparkScreenUp(name, opts.desktopUser);
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
console.error(chalk.red(` ✗ screen up failed: ${err?.message ?? err}`));
|
|
502
|
+
process.exitCode = 1;
|
|
503
|
+
} });
|
|
504
|
+
screenProgram.command('restart <name>')
|
|
505
|
+
.description('Restart a robot desktop relay')
|
|
506
|
+
.action((name) => { try {
|
|
507
|
+
roboparkScreenRestart(name);
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
console.error(chalk.red(` ✗ screen restart failed: ${err?.message ?? err}`));
|
|
511
|
+
process.exitCode = 1;
|
|
512
|
+
} });
|
|
513
|
+
screenProgram.command('down <name>')
|
|
514
|
+
.description('Stop and remove a robot desktop relay')
|
|
515
|
+
.action((name) => { try {
|
|
516
|
+
roboparkScreenDown(name);
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
console.error(chalk.red(` ✗ screen down failed: ${err?.message ?? err}`));
|
|
520
|
+
process.exitCode = 1;
|
|
521
|
+
} });
|
|
522
|
+
visionProgram
|
|
523
|
+
.command('up <name>')
|
|
524
|
+
.description('Start and persist a named camera relay using saved hub credentials')
|
|
525
|
+
.option('--hub-url <url>', 'hub mesh URL (defaults to `robopark hub-use` profile)')
|
|
526
|
+
.option('--token <token>', 'mesh token (defaults to `robopark hub-use` profile)')
|
|
527
|
+
.option('--character <id>', 'optional character preset associated with this robot')
|
|
528
|
+
.option('--video-device <dev>', 'camera selection', 'auto')
|
|
529
|
+
.option('--port <port>', 'robot mesh port', '47913')
|
|
530
|
+
.option('--lan', 'use LAN transport (default)')
|
|
531
|
+
.option('--tailscale', 'use Tailscale transport')
|
|
532
|
+
.option('--no-auto-start', 'run now without registering the feed at boot')
|
|
533
|
+
.action(async (name, opts) => {
|
|
534
|
+
try {
|
|
535
|
+
await roboparkVisionUp(config, name, opts);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
console.error(chalk.red(` ✗ vision up failed: ${err?.message ?? err}`));
|
|
539
|
+
process.exitCode = 1;
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
visionProgram
|
|
543
|
+
.command('status <name>')
|
|
544
|
+
.description('Show the camera runtime and local feed status')
|
|
545
|
+
.action(async (name) => {
|
|
546
|
+
try {
|
|
547
|
+
await roboparkVisionStatus(name);
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
console.error(chalk.red(` ✗ vision status failed: ${err?.message ?? err}`));
|
|
551
|
+
process.exitCode = 1;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
visionProgram
|
|
555
|
+
.command('restart <name>')
|
|
556
|
+
.description('Cleanly restart the persistent camera relay')
|
|
557
|
+
.action((name) => {
|
|
558
|
+
try {
|
|
559
|
+
roboparkVisionRestart(name);
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
console.error(chalk.red(` ✗ vision restart failed: ${err?.message ?? err}`));
|
|
563
|
+
process.exitCode = 1;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
visionProgram
|
|
567
|
+
.command('down <name>')
|
|
568
|
+
.description('Stop and remove the persistent camera relay')
|
|
569
|
+
.action((name) => {
|
|
570
|
+
try {
|
|
571
|
+
roboparkVisionDown(name);
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
console.error(chalk.red(` ✗ vision down failed: ${err?.message ?? err}`));
|
|
575
|
+
process.exitCode = 1;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
const robotProgram = program
|
|
579
|
+
.command('robot')
|
|
580
|
+
.description('Run the complete robot runtime as one supervised service');
|
|
581
|
+
robotProgram
|
|
582
|
+
.command('up')
|
|
583
|
+
.description('Start mesh, RoboVision, audio, motion webhook, and preview agent in dependency order')
|
|
584
|
+
.requiredOption('--name <name>', 'robot name')
|
|
585
|
+
.requiredOption('--hub-url <url>', 'hub mesh URL, e.g. http://192.168.1.12:47913')
|
|
586
|
+
.requiredOption('--token <token>', 'shared mesh token')
|
|
587
|
+
.option('--scheduler-url <url>', 'scheduler URL (derived from --hub-url when omitted)')
|
|
588
|
+
.option('--enrollment-token <token>', 'one-time scheduler enrollment token for a fresh device')
|
|
589
|
+
.option('--character <id>', 'character preset assigned during canonical device bootstrap')
|
|
590
|
+
.option('--port <port>', 'robot mesh port', '47913')
|
|
591
|
+
.option('--lan', 'use LAN transport (default)')
|
|
592
|
+
.option('--tailscale', 'use Tailscale transport')
|
|
593
|
+
.option('--video-device <dev>', 'camera selection', 'auto')
|
|
594
|
+
.option('--audio-device <dev>', 'microphone selection', 'default')
|
|
595
|
+
.option('--device-role <role>', 'hardware role: combined | voice_vision | motor', 'combined')
|
|
596
|
+
.option('--no-vision', 'do not start RoboVision camera/audio/motion services')
|
|
597
|
+
.option('--foreground', 'keep logs in this terminal instead of ~/.robopark/logs')
|
|
598
|
+
.action(async (opts) => {
|
|
599
|
+
if (opts.lan && opts.tailscale)
|
|
600
|
+
throw new Error('choose exactly one of --lan or --tailscale');
|
|
601
|
+
if (!['combined', 'voice_vision', 'motor'].includes(opts.deviceRole))
|
|
602
|
+
throw new Error('device role must be combined, voice_vision, or motor');
|
|
603
|
+
await roboparkRobotRuntime({
|
|
604
|
+
name: opts.name,
|
|
605
|
+
hubUrl: opts.hubUrl,
|
|
606
|
+
schedulerUrl: opts.schedulerUrl ?? `${opts.hubUrl.replace(/\/+$/, '')}/robopark`,
|
|
607
|
+
token: opts.token,
|
|
608
|
+
port: opts.port,
|
|
609
|
+
network: opts.tailscale ? 'tailscale' : 'lan',
|
|
610
|
+
enrollmentToken: opts.enrollmentToken,
|
|
611
|
+
character: opts.character,
|
|
612
|
+
videoDevice: opts.videoDevice,
|
|
613
|
+
audioDevice: opts.audioDevice,
|
|
614
|
+
deviceRole: opts.deviceRole,
|
|
615
|
+
vision: opts.vision,
|
|
616
|
+
foreground: opts.foreground,
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
program
|
|
620
|
+
.command('stop')
|
|
621
|
+
.description('Stop every infinicode/robopark process on this device AND remove any supervisor (systemd/Task Scheduler/launchd) that would respawn it')
|
|
622
|
+
.action(async () => {
|
|
623
|
+
console.log(chalk.bold('\n robopark stop\n ' + '─'.repeat(48)));
|
|
624
|
+
try {
|
|
625
|
+
const result = await stopAll();
|
|
626
|
+
if (result.killedPids.length)
|
|
627
|
+
console.log(chalk.green(` ✓ killed ${result.killedPids.length} process(es): ${result.killedPids.join(', ')}`));
|
|
628
|
+
else
|
|
629
|
+
console.log(chalk.dim(' no matching processes were running'));
|
|
630
|
+
if (result.removedUnits.length)
|
|
631
|
+
console.log(chalk.green(` ✓ removed ${result.removedUnits.length} supervisor unit(s): ${result.removedUnits.join(', ')}`));
|
|
632
|
+
else
|
|
633
|
+
console.log(chalk.dim(' no supervisor units registered'));
|
|
634
|
+
for (const err of result.errors)
|
|
635
|
+
console.log(chalk.yellow(` ! ${err}`));
|
|
636
|
+
console.log('');
|
|
637
|
+
}
|
|
638
|
+
catch (e) {
|
|
639
|
+
console.error('stop failed:', e instanceof Error ? (e.stack ?? e.message) : e);
|
|
640
|
+
process.exitCode = 1;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
program
|
|
644
|
+
.command('doctor')
|
|
645
|
+
.description('Run a full fleet health check: hub, scheduler, LiveKit servers, device heartbeats, production mode')
|
|
646
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
647
|
+
.option('--token <token>', 'mesh auth token')
|
|
648
|
+
.option('--scheduler-url <url>', 'scheduler base URL, e.g. http://100.64.1.2:8080')
|
|
649
|
+
.action(async (opts) => {
|
|
650
|
+
await roboparkDoctor(opts);
|
|
651
|
+
});
|
|
652
|
+
program
|
|
653
|
+
.command('server-add')
|
|
654
|
+
.description('Register a LiveKit server with the RoboPark scheduler')
|
|
655
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
656
|
+
.option('--scheduler-url <url>', 'scheduler URL, e.g. http://100.64.1.2:8080 (derived from --hub-url if omitted)')
|
|
657
|
+
.option('--token <token>', 'mesh auth token (optional)')
|
|
658
|
+
.requiredOption('--id <id>', 'unique server id, e.g. livekit-hub-docker')
|
|
659
|
+
.option('--name <name>', 'display name (defaults to --id)')
|
|
660
|
+
.requiredOption('--url <url>', 'LiveKit ws:// URL, e.g. ws://100.64.1.2:7880')
|
|
661
|
+
.option('--webhook-url <url>', 'internal http URL for health/metrics (defaults to --url with ws->http)')
|
|
662
|
+
.option('--api-key <key>', "LiveKit API key (defaults to 'devkey')")
|
|
663
|
+
.option('--api-secret <secret>', "LiveKit API secret (defaults to 'secret')")
|
|
664
|
+
.option('--max-sessions <n>', 'max concurrent sessions', '8')
|
|
665
|
+
.action(async (opts) => {
|
|
666
|
+
const { roboparkServerAdd } = await import('./robopark/server-add.js');
|
|
667
|
+
await roboparkServerAdd(opts);
|
|
668
|
+
});
|
|
669
|
+
program
|
|
670
|
+
.command('llm-set')
|
|
671
|
+
.description('Upsert LLM_PROVIDER/OLLAMA_* keys into a ROBOVOICE .env file')
|
|
672
|
+
.requiredOption('--env-path <path>', 'path to the target .env file (e.g. a ROBOVOICE checkout)')
|
|
673
|
+
.requiredOption('--provider <name>', 'ollama | ollama-cloud | groq')
|
|
674
|
+
.option('--model <name>', 'model name (ollama providers only)')
|
|
675
|
+
.option('--host <url>', 'Ollama server URL (ollama providers only)')
|
|
676
|
+
.option('--api-key <key>', 'API key for cloud auth (ollama-cloud; currently unused by ROBOVOICE — see CLI warning)')
|
|
677
|
+
.action(async (opts) => {
|
|
678
|
+
const { roboparkLlmSet } = await import('./robopark/llm-set.js');
|
|
679
|
+
await roboparkLlmSet(opts);
|
|
680
|
+
});
|
|
681
|
+
program
|
|
682
|
+
.command('hub-use')
|
|
683
|
+
.description('Save hub/scheduler/token/site as defaults so other commands stop needing them repeated')
|
|
684
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
685
|
+
.option('--token <token>', 'shared mesh token')
|
|
686
|
+
.option('--scheduler-url <url>', 'scheduler base URL, e.g. http://100.64.1.2:8080')
|
|
687
|
+
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
688
|
+
.option('--clear', 'wipe the saved hub profile')
|
|
689
|
+
.action(async (opts) => {
|
|
690
|
+
if (opts.clear) {
|
|
691
|
+
clearHubProfile();
|
|
692
|
+
console.log(chalk.green(' ✓ cleared saved hub profile'));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
saveHubProfile({ hubUrl: opts.hubUrl, token: opts.token, schedulerUrl: opts.schedulerUrl, site: opts.site });
|
|
696
|
+
console.log(chalk.green(' ✓ saved hub profile to ~/.robopark/hub-profile.json'));
|
|
697
|
+
});
|
|
698
|
+
program
|
|
699
|
+
.command('add-robot <name>')
|
|
700
|
+
.description('Mint a scheduler enrollment token and print (or run) the robot setup one-liner')
|
|
701
|
+
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
702
|
+
.option('--hub-url <url>', 'hub federation URL (defaults to saved hub profile / auto-discovery)')
|
|
703
|
+
.option('--token <token>', 'shared mesh token (defaults to saved hub profile / auto-discovery)')
|
|
704
|
+
.option('--scheduler-url <url>', 'scheduler base URL (defaults to saved hub profile / auto-discovery)')
|
|
705
|
+
.option('--character <id>', 'force a specific park-map pad/character preset id (default: fuzzy-matched from the robot name)')
|
|
706
|
+
.option('--lan', 'generate a robot command for the same local network (default)')
|
|
707
|
+
.option('--tailscale', 'generate a robot command for a Tailscale-reachable hub')
|
|
708
|
+
.option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
|
|
709
|
+
.option('--auto-start', 'register the robot runtime to start on boot')
|
|
710
|
+
.option('--yes', 'non-interactive production setup')
|
|
711
|
+
.action(async (name, opts) => {
|
|
712
|
+
await roboparkAddRobot(config, { name, ...opts });
|
|
713
|
+
});
|
|
714
|
+
program
|
|
715
|
+
.command('agent-up')
|
|
716
|
+
.description('Start the ROBOVOICE docker stack and register its LiveKit server with the scheduler')
|
|
717
|
+
.requiredOption('--path <dir>', 'path to the ROBOVOICE checkout (contains docker-compose.yaml)')
|
|
718
|
+
.option('--scheduler-url <url>', 'scheduler URL (defaults to resolved hub context)')
|
|
719
|
+
.option('--hub-url <url>', 'hub URL (defaults to resolved hub context)')
|
|
720
|
+
.option('--token <token>', 'mesh token')
|
|
721
|
+
.option('--server-id <id>', "server id to register (default: 'robovoice-<hostname>')")
|
|
722
|
+
.option('--server-name <name>', 'server display name')
|
|
723
|
+
.option('--no-register', 'bring up docker only; skip scheduler registration')
|
|
724
|
+
.action(roboparkAgentUp);
|
|
725
|
+
program
|
|
726
|
+
.command('agent-down')
|
|
727
|
+
.description('Stop the ROBOVOICE docker stack')
|
|
728
|
+
.requiredOption('--path <dir>', 'path to the ROBOVOICE checkout')
|
|
729
|
+
.action(roboparkAgentDown);
|
|
730
|
+
program
|
|
731
|
+
.command('agent-logs')
|
|
732
|
+
.description('Tail ROBOVOICE docker compose logs')
|
|
733
|
+
.requiredOption('--path <dir>', 'path to the ROBOVOICE checkout')
|
|
734
|
+
.option('-f, --follow', 'follow log output')
|
|
735
|
+
.option('--service <name>', 'scope logs to one compose service (e.g. agent, livekit)')
|
|
736
|
+
.action(roboparkAgentLogs);
|
|
737
|
+
program
|
|
738
|
+
.command('setup')
|
|
739
|
+
.description('Set up this device in one pass')
|
|
740
|
+
.option('--hub', 'this device is the on-site hub')
|
|
741
|
+
.option('--robot', 'this device is a robot')
|
|
742
|
+
.option('--control', 'this device is a control station')
|
|
743
|
+
.option('--voice-vision', 'this Windows device owns voice and vision')
|
|
744
|
+
.option('--motor-node', 'this Pi owns motors only')
|
|
745
|
+
.option('--role <role>', 'explicit role: hub | robot | voice-vision | motor | control')
|
|
746
|
+
.option('--name <name>', 'device name (defaults to hostname)')
|
|
747
|
+
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
748
|
+
.option('--hub-url <url>', 'hub base URL for robots/control')
|
|
749
|
+
.option('--token <token>', 'shared mesh token')
|
|
750
|
+
.option('--port <port>', 'mesh listen port', '47913')
|
|
751
|
+
.option('--gateway <url>', 'InfiniBot gateway ws:// URL')
|
|
752
|
+
.option('--gateway-token <token>', 'InfiniBot gateway token')
|
|
753
|
+
.option('--scheduler-port <port>', 'scheduler port on hub', '8080')
|
|
754
|
+
.option('--tailscale-auth <token>', 'Tailscale auth key for the hub')
|
|
755
|
+
.option('--tag <tag>', 'Tailscale tag filter')
|
|
756
|
+
.option('--enrollment-token <token>', 'one-time scheduler enrollment token for robots')
|
|
757
|
+
.option('--character <id>', 'character preset assigned during canonical device bootstrap')
|
|
758
|
+
.option('--lan', 'use LAN discovery and LAN hub/scheduler addresses (default for robots)')
|
|
759
|
+
.option('--tailscale', 'use Tailscale discovery and Tailscale hub/scheduler addresses')
|
|
760
|
+
.option('--video-device <dev>', 'default camera for robot preview agent', 'auto')
|
|
761
|
+
.option('--audio-device <dev>', 'default microphone for robot preview agent', 'default')
|
|
762
|
+
.option('--no-vision', 'do not start the vision (camera/motion trigger) agent for a robot — armed by default')
|
|
763
|
+
.option('--start', 'launch the node now')
|
|
764
|
+
.option('--auto-start', 'register to start on boot')
|
|
765
|
+
.option('--yes', 'non-interactive mode')
|
|
766
|
+
.action(async (opts) => {
|
|
767
|
+
const role = opts.role ?? (opts.hub ? 'hub' : opts.robot ? 'robot' : opts.voiceVision ? 'voice-vision' : opts.motorNode ? 'motor' : opts.control ? 'control' : undefined);
|
|
768
|
+
if (!role) {
|
|
769
|
+
console.log(chalk.red(' pass a role: --hub | --robot | --voice-vision | --motor-node | --control'));
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
if (!['hub', 'robot', 'voice-vision', 'motor', 'control'].includes(role))
|
|
773
|
+
throw new Error('invalid setup role');
|
|
774
|
+
await roboparkSetup(config, {
|
|
775
|
+
role,
|
|
776
|
+
name: opts.name,
|
|
777
|
+
site: opts.site,
|
|
778
|
+
hub: opts.hubUrl,
|
|
779
|
+
token: opts.token,
|
|
780
|
+
port: opts.port,
|
|
781
|
+
gateway: opts.gateway,
|
|
782
|
+
gatewayToken: opts.gatewayToken,
|
|
783
|
+
schedulerPort: opts.schedulerPort,
|
|
784
|
+
tailscaleAuth: opts.tailscaleAuth,
|
|
785
|
+
tag: opts.tag,
|
|
786
|
+
enrollmentToken: opts.enrollmentToken,
|
|
787
|
+
character: opts.character,
|
|
788
|
+
videoDevice: opts.videoDevice,
|
|
789
|
+
audioDevice: opts.audioDevice,
|
|
790
|
+
vision: opts.vision,
|
|
791
|
+
start: opts.start,
|
|
792
|
+
autoStart: opts.autoStart,
|
|
793
|
+
yes: opts.yes,
|
|
794
|
+
lan: opts.lan,
|
|
795
|
+
tailscale: opts.tailscale,
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
// Keep legacy fleet/run/apply/dashboard commands available while migrating.
|
|
799
|
+
await program.parseAsync(process.argv);
|