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.
Files changed (68) hide show
  1. package/README.md +88 -63
  2. package/bin/robopark.js +7 -17
  3. package/conversation/elevenlabs_agent.py +1985 -0
  4. package/conversation/requirements.txt +3 -0
  5. package/conversation/supervisor_store.py +189 -0
  6. package/dist/kernel/config-schema.js +37 -0
  7. package/dist/kernel/types.js +7 -0
  8. package/dist/robopark/access.js +99 -0
  9. package/dist/robopark/add-robot.js +188 -0
  10. package/dist/robopark/agent-ctl.js +305 -0
  11. package/dist/robopark/auto-start.js +289 -0
  12. package/dist/robopark/conversation.js +505 -0
  13. package/dist/robopark/deployment-commands.js +47 -0
  14. package/dist/robopark/discovery.js +180 -0
  15. package/dist/robopark/doctor.js +175 -0
  16. package/dist/robopark/enroll.js +68 -0
  17. package/dist/robopark/llm-set.js +87 -0
  18. package/dist/robopark/motor-control.js +195 -0
  19. package/dist/robopark/preview-agent-launcher.js +77 -0
  20. package/dist/robopark/probe.js +138 -0
  21. package/dist/robopark/profile.js +69 -0
  22. package/dist/robopark/python-env.js +162 -0
  23. package/dist/robopark/robot-runtime.js +489 -0
  24. package/dist/robopark/scan.js +97 -0
  25. package/dist/robopark/screen-control.js +55 -0
  26. package/dist/robopark/secrets.js +41 -0
  27. package/dist/robopark/serve.js +285 -0
  28. package/dist/robopark/server-add.js +114 -0
  29. package/dist/robopark/setup-livekit.js +300 -0
  30. package/dist/robopark/setup.js +286 -0
  31. package/dist/robopark/standalone.js +466 -0
  32. package/dist/robopark/stop-all.js +141 -0
  33. package/dist/robopark/verify.js +192 -0
  34. package/dist/robopark/vision-agent-launcher.js +98 -0
  35. package/dist/robopark/vision-control.js +81 -0
  36. package/dist/robopark-cli.js +799 -0
  37. package/package.json +21 -5
  38. package/pi-client/_install_steps.sh +29 -29
  39. package/pi-client/client.py +61 -2
  40. package/pi-client/install.sh +40 -40
  41. package/pi-client/join_convo.sh +54 -54
  42. package/pi-client/livekit_bridge.py +16 -7
  43. package/pi-client/motor_bridge.py +6 -3
  44. package/scheduler/fleet_config.json +75 -0
  45. package/scheduler/main.py +4505 -135
  46. package/scheduler/media_lock.py +57 -0
  47. package/scheduler/preview_agent.py +1465 -87
  48. package/scheduler/production_config.json +139 -0
  49. package/scheduler/robot_supervisor.py +1705 -0
  50. package/scheduler/scripts/install-robot-supervisor-linux.sh +33 -0
  51. package/scheduler/scripts/install-robot-supervisor-windows.ps1 +49 -0
  52. package/scheduler/scripts/robopark-supervisor.service +20 -0
  53. package/scheduler/scripts/start-scheduler-local.ps1 +50 -0
  54. package/scheduler/supervisor.example.json +26 -0
  55. package/scheduler/vision_motion_trigger.py +101 -0
  56. package/screen/screen_runtime.py +75 -0
  57. package/vision/app_pi_clean.py +253 -16
  58. package/vision/audio_server_pi.py +19 -0
  59. package/vision/install.sh +34 -34
  60. package/vision/motor_server.py +224 -61
  61. package/vision/requirements_camera.txt +6 -0
  62. package/vision/requirements_motor.txt +4 -0
  63. package/vision/requirements_pi_unified.txt +1 -0
  64. package/vision/requirements_vision_agent.txt +19 -0
  65. package/vision/run.sh +244 -244
  66. package/vision/services/services.sh +12 -12
  67. package/scheduler/__pycache__/main.cpython-312.pyc +0 -0
  68. package/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * RoboPark — `robopark serve`.
3
+ *
4
+ * Starts the RoboPark scheduler (Python FastAPI) and the web UI in one pass.
5
+ * The scheduler lives next to this package in `scheduler/main.py`.
6
+ *
7
+ * robopark serve --port 8080
8
+ * robopark serve --gateway ws://infinibot:18789 --gateway-token <token>
9
+ */
10
+ import chalk from 'chalk';
11
+ import { spawn, execSync } from 'node:child_process';
12
+ import { cpSync, existsSync, mkdirSync, statSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ function pkgDir() {
17
+ try {
18
+ // dist/robopark/serve.js -> dist/robopark -> dist -> project root
19
+ return dirname(dirname(dirname(fileURLToPath(import.meta.url))));
20
+ }
21
+ catch {
22
+ return process.cwd();
23
+ }
24
+ }
25
+ function findScheduler() {
26
+ const candidates = [
27
+ // When robopark serve is invoked via the robopark wrapper, the wrapper
28
+ // imports infinicode/robopark, so this code runs from inside the
29
+ // infinicode package. The scheduler Python files are shipped there.
30
+ join(pkgDir(), 'packages', 'robopark', 'scheduler', 'main.py'),
31
+ // Fallbacks for local checkout / when robopark is installed standalone.
32
+ join(pkgDir(), 'scheduler', 'main.py'),
33
+ join(process.cwd(), 'packages', 'robopark', 'scheduler', 'main.py'),
34
+ join(process.cwd(), 'scheduler', 'main.py'),
35
+ ];
36
+ for (const p of candidates) {
37
+ if (existsSync(p))
38
+ return p;
39
+ }
40
+ // Last resort: resolve the installed infinicode package and look next to it.
41
+ try {
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const req = require('module').createRequire(fileURLToPath(import.meta.url));
44
+ const pkgMain = req.resolve('infinicode/package.json');
45
+ const p = join(dirname(pkgMain), 'packages', 'robopark', 'scheduler', 'main.py');
46
+ if (existsSync(p))
47
+ return p;
48
+ }
49
+ catch { /* ignore */ }
50
+ return null;
51
+ }
52
+ function findPython() {
53
+ // On Windows the canonical interpreter is usually `python` (from the Microsoft
54
+ // Store / python.org installer); `python3` is common on Linux/macOS. Prefer the
55
+ // platform-specific name first, then fall back to the other conventions.
56
+ const names = process.platform === 'win32'
57
+ ? ['python', 'python3', 'python3.12', 'python3.11', 'python3.10', 'py']
58
+ : ['python3.12', 'python3.11', 'python3.10', 'python3', 'python'];
59
+ for (const name of names) {
60
+ try {
61
+ execSync(`${name} --version`, { stdio: 'ignore' });
62
+ return name;
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ }
68
+ // Nothing verified; return the most likely name so the user sees the actual
69
+ // ENOENT/"not found" error from the OS rather than a silent fallback.
70
+ return process.platform === 'win32' ? 'python' : 'python3';
71
+ }
72
+ function resolveSchedulerDataDir(explicit) {
73
+ if (explicit)
74
+ return explicit;
75
+ if (process.env.SCHEDULER_DATA_DIR)
76
+ return process.env.SCHEDULER_DATA_DIR;
77
+ const stable = join(homedir(), '.robopark', 'scheduler');
78
+ const stableDb = join(stable, 'scheduler.db');
79
+ if (existsSync(stableDb))
80
+ return stable;
81
+ // Older releases stored data beside whichever npm installation launched
82
+ // the scheduler. Local, global and npx installs therefore created separate
83
+ // databases. Migrate the largest existing DB once into stable user storage.
84
+ const legacyCandidates = [join(pkgDir(), '..', 'data')];
85
+ if (process.platform === 'win32' && process.env.APPDATA) {
86
+ legacyCandidates.push(join(process.env.APPDATA, 'npm', 'node_modules', 'data'));
87
+ }
88
+ const legacy = legacyCandidates
89
+ .filter((candidate, index, all) => all.indexOf(candidate) === index)
90
+ .filter(candidate => existsSync(join(candidate, 'scheduler.db')))
91
+ .sort((a, b) => statSync(join(b, 'scheduler.db')).size - statSync(join(a, 'scheduler.db')).size)[0];
92
+ mkdirSync(stable, { recursive: true });
93
+ if (legacy) {
94
+ cpSync(legacy, stable, { recursive: true, force: false, errorOnExist: false });
95
+ console.log(chalk.yellow(` migrated scheduler data: ${legacy} -> ${stable}`));
96
+ }
97
+ return stable;
98
+ }
99
+ /** Resolve the actual infinicode CLI entry point so `robopark setup --start`
100
+ * works even when global bins are not on PATH.
101
+ *
102
+ * Returns the node executable path and the path to `bin/infinicode.js` inside
103
+ * the installed `infinicode` package. We avoid spawning the bare `infinicode`
104
+ * command because npm's `.cmd` shim is not reliably resolvable by Node's
105
+ * `spawn` without `shell: true` on Windows, and shell concatenation triggers
106
+ * a deprecation warning when args are passed.
107
+ */
108
+ export async function resolveInfinicodeBin() {
109
+ try {
110
+ // When running from the infinicode package itself, the CLI entry is nearby.
111
+ const local = join(pkgDir(), 'bin', 'infinicode.js');
112
+ if (existsSync(local))
113
+ return { node: process.execPath, script: local };
114
+ }
115
+ catch { /* ignore */ }
116
+ try {
117
+ // When running from the robopark wrapper, resolve infinicode from npm.
118
+ const { createRequire } = await import('node:module');
119
+ // createRequire expects a file path, not a file:// URL.
120
+ const require = createRequire(fileURLToPath(import.meta.url));
121
+ const pkgMain = require.resolve('infinicode/package.json');
122
+ const candidate = join(dirname(pkgMain), 'bin', 'infinicode.js');
123
+ if (existsSync(candidate))
124
+ return { node: process.execPath, script: candidate };
125
+ }
126
+ catch { /* ignore */ }
127
+ return null;
128
+ }
129
+ export async function roboparkServe(opts) {
130
+ const schedulerPath = findScheduler();
131
+ if (!schedulerPath) {
132
+ console.log(chalk.red(' ✗ could not find scheduler/main.py'));
133
+ console.log(chalk.dim(' make sure robopark is installed from npm, not a partial checkout.'));
134
+ process.exit(1);
135
+ }
136
+ const port = opts.port ? parseInt(opts.port, 10) : 8080;
137
+ const host = opts.host ?? '0.0.0.0';
138
+ const python = findPython();
139
+ const schedulerDir = dirname(schedulerPath);
140
+ const requirements = join(schedulerDir, 'requirements.txt');
141
+ const dataDir = resolveSchedulerDataDir(opts.dataDir);
142
+ // Ensure scheduler Python dependencies are installed before starting.
143
+ if (existsSync(requirements)) {
144
+ console.log(chalk.dim(' ensuring scheduler Python deps…'));
145
+ try {
146
+ execSync(`${python} -m pip install -r "${requirements}" --user --quiet`, { stdio: 'pipe' });
147
+ }
148
+ catch (err) {
149
+ console.log(chalk.yellow(` ⚠ pip install failed: ${err instanceof Error ? err.message : String(err)}`));
150
+ console.log(chalk.dim(' continuing; scheduler may fail if deps are missing.'));
151
+ }
152
+ }
153
+ const env = {
154
+ ...process.env,
155
+ SCHEDULER_HOST: host,
156
+ SCHEDULER_PORT: String(port),
157
+ SCHEDULER_DATA_DIR: dataDir,
158
+ };
159
+ if (opts.gateway)
160
+ env.INFINIBOT_GATEWAY_URL = opts.gateway;
161
+ if (opts.gatewayToken)
162
+ env.INFINIBOT_GATEWAY_TOKEN = opts.gatewayToken;
163
+ if (opts.gatewayPassword)
164
+ env.INFINIBOT_GATEWAY_PASSWORD = opts.gatewayPassword;
165
+ console.log(chalk.bold('\n robopark serve'));
166
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
167
+ console.log(` scheduler: ${chalk.cyan(schedulerPath)}`);
168
+ console.log(` python: ${chalk.cyan(python)}`);
169
+ console.log(` listen: ${chalk.cyan(`${host}:${port}`)}`);
170
+ console.log(` data: ${chalk.cyan(dataDir)}`);
171
+ if (opts.gateway)
172
+ console.log(` gateway: ${chalk.cyan(opts.gateway)}`);
173
+ console.log();
174
+ const proc = spawn(python, ['main.py'], {
175
+ cwd: dirname(schedulerPath),
176
+ env,
177
+ stdio: opts.foreground ? 'pipe' : 'ignore',
178
+ detached: !opts.foreground,
179
+ });
180
+ let ready = false;
181
+ proc.stdout?.on('data', (d) => {
182
+ const line = d.toString().trim();
183
+ if (line)
184
+ console.log(chalk.dim(' [scheduler] ' + line));
185
+ if (/Uvicorn running|Application startup complete/i.test(line)) {
186
+ ready = true;
187
+ printUrl(port);
188
+ }
189
+ });
190
+ proc.stderr?.on('data', (d) => {
191
+ const line = d.toString().trim();
192
+ if (line)
193
+ console.log(chalk.yellow(' [scheduler] ' + line));
194
+ });
195
+ proc.on('error', (err) => {
196
+ console.log(chalk.red(` ✗ scheduler failed to start: ${err.message}`));
197
+ process.exit(1);
198
+ });
199
+ proc.on('exit', (code) => {
200
+ if (!ready && code !== 0) {
201
+ console.log(chalk.red(` ✗ scheduler exited ${code ?? ''}`));
202
+ process.exit(code ?? 1);
203
+ }
204
+ });
205
+ if (opts.foreground) {
206
+ process.on('SIGINT', () => {
207
+ console.log(chalk.dim('\n stopping scheduler…'));
208
+ try {
209
+ proc.kill('SIGINT');
210
+ }
211
+ catch { /* ignore */ }
212
+ });
213
+ proc.on('exit', (code, signal) => {
214
+ if (signal)
215
+ process.exit(1);
216
+ process.exit(code ?? 1);
217
+ });
218
+ if (opts.open !== false) {
219
+ await openDashboard(host, port);
220
+ }
221
+ }
222
+ else {
223
+ // Detached mode: let the user walk away. The process keeps running.
224
+ proc.unref();
225
+ if (opts.open !== false) {
226
+ await openDashboard(host, port);
227
+ }
228
+ printUrl(port);
229
+ console.log(chalk.dim(' running in background. Use Task Manager / systemctl / launchctl to stop.'));
230
+ }
231
+ }
232
+ function printUrl(port) {
233
+ const url = `http://localhost:${port}`;
234
+ console.log(chalk.bold(` dashboard: ${chalk.cyan(url)}`));
235
+ }
236
+ /** Quick health check: is the scheduler responding? */
237
+ export async function schedulerHealthy(url, timeoutMs = 3000) {
238
+ try {
239
+ const res = await fetch(`${url.replace(/\/$/, '')}/api/settings`, { signal: AbortSignal.timeout(timeoutMs) });
240
+ return res.ok;
241
+ }
242
+ catch {
243
+ return false;
244
+ }
245
+ }
246
+ function dashboardUrl(host, port) {
247
+ // 0.0.0.0/:: are bind addresses, not useful browser destinations.
248
+ const browserHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
249
+ const formattedHost = browserHost.includes(':') && !browserHost.startsWith('[')
250
+ ? `[${browserHost}]`
251
+ : browserHost;
252
+ return `http://${formattedHost}:${port}/`;
253
+ }
254
+ function openBrowser(url) {
255
+ // Use detached platform-native launchers so robopark serve remains usable
256
+ // from terminals and does not inherit browser stdio or lifecycle.
257
+ const command = process.platform === 'win32'
258
+ ? 'cmd.exe'
259
+ : process.platform === 'darwin'
260
+ ? 'open'
261
+ : 'xdg-open';
262
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
263
+ try {
264
+ spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
265
+ }
266
+ catch (err) {
267
+ console.log(chalk.yellow(` ⚠ could not open browser: ${err instanceof Error ? err.message : String(err)}`));
268
+ }
269
+ }
270
+ async function openDashboard(host, port) {
271
+ const url = dashboardUrl(host, port);
272
+ const deadline = Date.now() + 10_000;
273
+ let healthy = false;
274
+ while (Date.now() < deadline) {
275
+ if (await schedulerHealthy(url, 500)) {
276
+ healthy = true;
277
+ break;
278
+ }
279
+ await new Promise((resolve) => setTimeout(resolve, 250));
280
+ }
281
+ if (!healthy) {
282
+ console.log(chalk.yellow(' ⚠ scheduler did not answer within 10s; opening the dashboard anyway.'));
283
+ }
284
+ openBrowser(url);
285
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * RoboPark — `robopark server add`.
3
+ *
4
+ * Registers a LiveKit server with the RoboPark scheduler via
5
+ * `POST /api/servers`, wrapping the curl call operators previously had to
6
+ * hand-type.
7
+ *
8
+ * Usage:
9
+ * robopark server add --id livekit-hub-docker --url ws://100.x.y.z:7880
10
+ * robopark server add --id livekit-hub-docker --url ws://100.x.y.z:7880 \
11
+ * --api-key devkey --api-secret secret --scheduler-url http://100.x.y.z:8080
12
+ *
13
+ * If --scheduler-url is omitted, it is derived from --hub-url (or discovery)
14
+ * the same way `robopark setup-livekit` does: hub host on port 8080.
15
+ */
16
+ import { readFileSync, existsSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import chalk from 'chalk';
20
+ import { discoverContext } from './discovery.js';
21
+ const DEFAULT_SCHEDULER_PORT = 8080;
22
+ const DEV_API_KEY = 'devkey';
23
+ const DEV_API_SECRET = 'secret';
24
+ function loadMeshToken() {
25
+ const paths = [
26
+ join(homedir(), '.robopark', 'mesh.token'),
27
+ join(homedir(), '.config', 'infinicode-nodejs', 'config.json'),
28
+ join(homedir(), '.infinicode-nodejs', 'config.json'),
29
+ join(homedir(), 'AppData', 'Roaming', 'infinicode-nodejs', 'Config', 'config.json'),
30
+ ];
31
+ for (const p of paths) {
32
+ if (!existsSync(p))
33
+ continue;
34
+ try {
35
+ if (p.endsWith('mesh.token'))
36
+ return readFileSync(p, 'utf8').trim();
37
+ const cfg = JSON.parse(readFileSync(p, 'utf8'));
38
+ if (cfg.federation?.token)
39
+ return cfg.federation.token;
40
+ }
41
+ catch { /* ignore */ }
42
+ }
43
+ return process.env.ROBOPARK_MESH_TOKEN;
44
+ }
45
+ async function postServer(schedulerUrl, body) {
46
+ const url = `${schedulerUrl.replace(/\/$/, '')}/api/servers`;
47
+ const res = await fetch(url, {
48
+ method: 'POST',
49
+ headers: { 'content-type': 'application/json' },
50
+ body: JSON.stringify(body),
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => '');
54
+ throw new Error(`${res.status}: ${text || res.statusText}`);
55
+ }
56
+ }
57
+ export async function roboparkServerAdd(opts) {
58
+ const ctx = await discoverContext();
59
+ const hubUrl = opts.hubUrl ?? (ctx.hub ? `http://${ctx.hub.ip}:${ctx.meshPort ?? 47913}` : undefined);
60
+ // Kept for parity with setup-livekit's discovery flow, even though the
61
+ // scheduler's /api/servers endpoint itself is unauthenticated today.
62
+ void (opts.token ?? ctx.meshToken ?? loadMeshToken());
63
+ const schedulerUrl = opts.schedulerUrl ?? (hubUrl ? hubUrl.replace(/:\d+$/, `:${DEFAULT_SCHEDULER_PORT}`) : undefined);
64
+ console.log(chalk.bold('\n robopark server add'));
65
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
66
+ if (!schedulerUrl) {
67
+ console.log(chalk.red(' ✗ no scheduler URL. Pass --scheduler-url or --hub-url, or run robopark setup first.'));
68
+ process.exit(1);
69
+ }
70
+ if (!opts.id) {
71
+ console.log(chalk.red(' ✗ --id is required (unique server id, e.g. livekit-hub-docker).'));
72
+ process.exit(1);
73
+ }
74
+ if (!opts.url) {
75
+ console.log(chalk.red(' ✗ --url is required (LiveKit ws:// URL, e.g. ws://100.x.y.z:7880).'));
76
+ process.exit(1);
77
+ }
78
+ let apiKey = opts.apiKey;
79
+ let apiSecret = opts.apiSecret;
80
+ if (!apiKey || !apiSecret) {
81
+ console.log(chalk.yellow(` ⚠ --api-key/--api-secret not provided; defaulting to LiveKit --dev credentials ('${DEV_API_KEY}'/'${DEV_API_SECRET}')`));
82
+ apiKey = apiKey ?? DEV_API_KEY;
83
+ apiSecret = apiSecret ?? DEV_API_SECRET;
84
+ }
85
+ const body = {
86
+ id: opts.id,
87
+ name: opts.name ?? opts.id,
88
+ url: opts.url,
89
+ webhook_url: opts.webhookUrl ?? opts.url.replace(/^wss?/, 'http'),
90
+ api_key: apiKey,
91
+ api_secret: apiSecret,
92
+ gpu_name: 'none',
93
+ gpu_vram_mb: 0,
94
+ max_sessions: opts.maxSessions ? parseInt(opts.maxSessions, 10) : 8,
95
+ status: 'online',
96
+ };
97
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
98
+ console.log(` id: ${chalk.cyan(body.id)}`);
99
+ console.log(` url: ${chalk.cyan(body.url)}`);
100
+ console.log();
101
+ try {
102
+ await postServer(schedulerUrl, body);
103
+ console.log(chalk.green(` ✓ registered LiveKit server '${body.id}' with scheduler`));
104
+ console.log(chalk.dim(` name: ${body.name}`));
105
+ console.log(chalk.dim(` url: ${body.url}`));
106
+ console.log(chalk.dim(` webhook_url: ${body.webhook_url}`));
107
+ console.log(chalk.dim(` api_key: ${body.api_key}`));
108
+ console.log(chalk.dim(` max_sessions: ${body.max_sessions}`));
109
+ }
110
+ catch (e) {
111
+ console.log(chalk.red(` ✗ failed to register server: ${e.message}`));
112
+ process.exit(1);
113
+ }
114
+ }