infinicode 2.8.8 → 2.8.10

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.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * RoboPark — `robopark setup-livekit`.
3
+ *
4
+ * Dispatches an opencode agent over the mesh to install and start a LiveKit
5
+ * server on the fleet hub via Docker. After the agent reports success, the
6
+ * scheduler is told about the new LiveKit server.
7
+ *
8
+ * Usage:
9
+ * robopark setup-livekit --hub-url http://HUB_IP:47913 --token <mesh_token>
10
+ *
11
+ * If --hub-url is omitted, the hub is discovered over Tailscale.
12
+ */
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import { homedir, hostname } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import chalk from 'chalk';
17
+ import { discoverContext } from './discovery.js';
18
+ function loadMeshToken() {
19
+ const paths = [
20
+ join(homedir(), '.robopark', 'mesh.token'),
21
+ join(homedir(), '.config', 'infinicode-nodejs', 'config.json'),
22
+ join(homedir(), '.infinicode-nodejs', 'config.json'),
23
+ join(homedir(), 'AppData', 'Roaming', 'infinicode-nodejs', 'Config', 'config.json'),
24
+ ];
25
+ for (const p of paths) {
26
+ if (!existsSync(p))
27
+ continue;
28
+ try {
29
+ if (p.endsWith('mesh.token'))
30
+ return readFileSync(p, 'utf8').trim();
31
+ const cfg = JSON.parse(readFileSync(p, 'utf8'));
32
+ if (cfg.federation?.token)
33
+ return cfg.federation.token;
34
+ }
35
+ catch { /* ignore */ }
36
+ }
37
+ return process.env.ROBOPARK_MESH_TOKEN;
38
+ }
39
+ function randId() {
40
+ return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
41
+ }
42
+ function envelope(kind, from, data, opts = {}) {
43
+ return { v: 1, id: randId(), kind, from, to: opts.to, ts: Date.now(), auth: opts.auth, data };
44
+ }
45
+ async function meshRpc(targetUrl, token, env) {
46
+ const url = `${targetUrl.replace(/\/$/, '')}/fed/rpc?token=${encodeURIComponent(token)}`;
47
+ const res = await fetch(url, {
48
+ method: 'POST',
49
+ headers: { 'content-type': 'application/json' },
50
+ body: JSON.stringify(env),
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => '');
54
+ throw new Error(`mesh RPC ${res.status}: ${text}`);
55
+ }
56
+ return (await res.json());
57
+ }
58
+ async function getHubNodeId(hubUrl, token) {
59
+ try {
60
+ const url = `${hubUrl.replace(/\/$/, '')}/fed/manifest?token=${encodeURIComponent(token)}`;
61
+ const res = await fetch(url, { headers: { accept: 'application/json' } });
62
+ if (!res.ok)
63
+ return null;
64
+ const manifest = (await res.json());
65
+ return manifest.nodeId || manifest.id || null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ const LIVEKIT_SETUP_PROMPT = `You are the RoboPark infrastructure agent running on the fleet hub.
72
+
73
+ GOAL: Install and start a LiveKit server in Docker on this machine so the RoboPark scheduler can mint tokens and the robot preview agent can publish camera/audio.
74
+
75
+ DO NOT ask the user for confirmation. Execute the steps directly. If a step is already done, skip it and report it.
76
+
77
+ Steps:
78
+ 1. Check if Docker is installed and running. If not, install Docker (apt-get install docker.io on Debian/Ubuntu, or dnf install docker on Fedora, or use the official convenience script if needed) and start it with systemctl.
79
+ 2. Pull the official LiveKit server image: docker pull livekit/livekit-server:latest
80
+ 3. Create a directory /opt/livekit if it does not exist.
81
+ 4. Generate API credentials: run the LiveKit CLI container to create a key/secret and save them to /opt/livekit/keys.yaml:
82
+ docker run --rm livekit/livekit-server:latest generate-keys
83
+ Parse the output and write it as a YAML file with fields api_key and api_secret.
84
+ 5. Start the LiveKit server container with host networking on port 7880:
85
+ docker run -d --name livekit-server --restart unless-stopped --network host \
86
+ -v /opt/livekit/keys.yaml:/etc/livekit/keys.yaml:ro \
87
+ livekit/livekit-server:latest \
88
+ --config /etc/livekit/keys.yaml --bind 0.0.0.0 --port 7880
89
+ 6. Wait up to 30 seconds and verify it is healthy: curl -sf http://127.0.0.1:7880
90
+ 7. Report back the WebSocket URL (ws://THIS_MACHINE_TAILSCALE_IP:7880 or ws://127.0.0.1:7880), the api_key, the api_secret, and whether the container is running.
91
+
92
+ IMPORTANT: this is a headless fleet node. Do not open a browser. Only run shell commands and report the final LiveKit URL + credentials in a JSON code block like:
93
+ \`\`\`json
94
+ {
95
+ "url": "ws://127.0.0.1:7880",
96
+ "api_key": "...",
97
+ "api_secret": "...",
98
+ "running": true
99
+ }
100
+ \`\`\`
101
+ `;
102
+ function extractJson(text) {
103
+ const m = text.match(/\`\`\`json\s*([\s\S]*?)\s*\`\`\`/);
104
+ if (!m)
105
+ return null;
106
+ try {
107
+ return JSON.parse(m[1]);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ async function registerLiveKitServer(schedulerUrl, info) {
114
+ const url = `${schedulerUrl.replace(/\/$/, '')}/api/servers`;
115
+ const body = {
116
+ id: 'livekit-hub-docker',
117
+ name: 'LiveKit Hub Docker',
118
+ url: info.url,
119
+ webhook_url: info.url.replace(/^wss?/, 'http'),
120
+ api_key: info.api_key,
121
+ api_secret: info.api_secret,
122
+ gpu_name: 'none',
123
+ gpu_vram_mb: 0,
124
+ max_sessions: 8,
125
+ status: 'online',
126
+ };
127
+ const res = await fetch(url, {
128
+ method: 'POST',
129
+ headers: { 'content-type': 'application/json' },
130
+ body: JSON.stringify(body),
131
+ });
132
+ if (!res.ok) {
133
+ const text = await res.text().catch(() => '');
134
+ throw new Error(`register server failed ${res.status}: ${text}`);
135
+ }
136
+ }
137
+ export async function roboparkSetupLivekit(opts) {
138
+ const ctx = await discoverContext();
139
+ const hubUrl = opts.hubUrl ?? (ctx.hub ? `http://${ctx.hub.ip}:${ctx.meshPort ?? 47913}` : undefined);
140
+ const token = opts.token ?? ctx.meshToken ?? loadMeshToken();
141
+ const schedulerUrl = opts.schedulerUrl ?? (hubUrl ? hubUrl.replace(/:\d+$/, ':8080') : undefined);
142
+ const timeoutMs = (opts.timeout ? parseInt(opts.timeout, 10) : 180) * 1000;
143
+ if (!hubUrl) {
144
+ console.log(chalk.red(' ✗ no hub discovered. Pass --hub-url http://HUB_IP:47913'));
145
+ process.exit(1);
146
+ }
147
+ if (!token) {
148
+ console.log(chalk.red(' ✗ no mesh token. Pass --token or run robopark setup first.'));
149
+ process.exit(1);
150
+ }
151
+ console.log(chalk.bold('\n robopark setup-livekit'));
152
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
153
+ console.log(` hub: ${chalk.cyan(hubUrl)}`);
154
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl ?? 'not provided')}`);
155
+ console.log();
156
+ if (opts.dryRun) {
157
+ console.log(chalk.yellow(' dry run — prompt that would be sent:'));
158
+ console.log(chalk.dim(LIVEKIT_SETUP_PROMPT.split('\n').map(l => ' ' + l).join('\n')));
159
+ return;
160
+ }
161
+ const from = hostname().split('.')[0];
162
+ const nodeId = await getHubNodeId(hubUrl, token);
163
+ if (!nodeId) {
164
+ console.log(chalk.red(' ✗ hub manifest not reachable. Is infinicode serve running on the hub?'));
165
+ process.exit(1);
166
+ }
167
+ console.log(chalk.dim(` hub node id: ${nodeId}`));
168
+ const dispatchEnv = envelope('task.dispatch', from, {
169
+ description: 'Install LiveKit server on hub via Docker',
170
+ capabilities: ['shell', 'docker', 'reasoning'],
171
+ prompt: LIVEKIT_SETUP_PROMPT,
172
+ agent: 'opencode',
173
+ context: { role: 'hub', site: 'fleet-hub' },
174
+ }, { to: nodeId, auth: token });
175
+ console.log(chalk.dim(' dispatching opencode agent to hub…'));
176
+ const accepted = await meshRpc(hubUrl, token, dispatchEnv);
177
+ if (!accepted || accepted.kind !== 'task.accepted') {
178
+ const err = accepted?.data && typeof accepted.data === 'object' ? JSON.stringify(accepted.data) : 'unknown';
179
+ console.log(chalk.red(` ✗ dispatch failed: ${err}`));
180
+ process.exit(1);
181
+ }
182
+ const { runId } = accepted.data;
183
+ console.log(chalk.dim(` run id: ${runId}`));
184
+ const start = Date.now();
185
+ let lastStatus = null;
186
+ let result = null;
187
+ while (Date.now() - start < timeoutMs) {
188
+ await new Promise(r => setTimeout(r, 3000));
189
+ const statusEnv = envelope('task.status', from, { runId }, { to: nodeId, auth: token });
190
+ result = await meshRpc(hubUrl, token, statusEnv);
191
+ const rec = (result?.data ?? {});
192
+ if (rec.status !== lastStatus) {
193
+ lastStatus = rec.status ?? null;
194
+ console.log(` status: ${chalk.cyan(rec.status ?? 'unknown')}${rec.error ? chalk.red(` — ${rec.error}`) : ''}`);
195
+ }
196
+ if (rec.status === 'completed' || rec.status === 'failed')
197
+ break;
198
+ }
199
+ if (!result) {
200
+ console.log(chalk.red(' ✗ no response from hub'));
201
+ process.exit(1);
202
+ }
203
+ const rec = result.data;
204
+ if (rec.status !== 'completed') {
205
+ console.log(chalk.red(` ✗ LiveKit setup failed: ${rec.error || 'unknown'}`));
206
+ if (rec.outputs)
207
+ console.log(chalk.dim(JSON.stringify(rec.outputs, null, 2)));
208
+ process.exit(1);
209
+ }
210
+ const content = rec.outputs?.map(o => o.content).filter(Boolean).join('\n') || '';
211
+ const parsed = extractJson(content);
212
+ if (!parsed || !parsed.url || !parsed.api_key || !parsed.api_secret) {
213
+ console.log(chalk.yellow(' ⚠ agent finished but did not return LiveKit credentials in a JSON block'));
214
+ console.log(chalk.dim(content));
215
+ process.exit(1);
216
+ }
217
+ console.log(chalk.green(' ✓ LiveKit server reported running'));
218
+ console.log(` url: ${chalk.cyan(String(parsed.url))}`);
219
+ console.log(` key: ${chalk.cyan(String(parsed.api_key))}`);
220
+ if (schedulerUrl) {
221
+ try {
222
+ await registerLiveKitServer(schedulerUrl, {
223
+ url: String(parsed.url),
224
+ api_key: String(parsed.api_key),
225
+ api_secret: String(parsed.api_secret),
226
+ });
227
+ console.log(chalk.green(` ✓ registered LiveKit server with scheduler`));
228
+ }
229
+ catch (e) {
230
+ console.log(chalk.yellow(` ⚠ could not register with scheduler: ${e.message}`));
231
+ }
232
+ }
233
+ }
@@ -13,6 +13,9 @@ export interface SetupOptions {
13
13
  tailscaleAuth?: string;
14
14
  tag?: string;
15
15
  schedulerPort?: string;
16
+ enrollmentToken?: string;
17
+ videoDevice?: string;
18
+ audioDevice?: string;
16
19
  start?: boolean;
17
20
  autoStart?: boolean;
18
21
  yes?: boolean;
@@ -160,13 +160,37 @@ async function setupRobot(config, opts, ctx, internal) {
160
160
  '--auto-update',
161
161
  '--supervised',
162
162
  ];
163
+ const schedulerUrl = `http://${ctx.hub?.ip ?? '127.0.0.1'}:${opts.schedulerPort ? parseInt(opts.schedulerPort, 10) : DEFAULT_SCHEDULER_PORT}`;
164
+ const previewAgentArgs = [
165
+ 'preview-agent',
166
+ '--scheduler-url', schedulerUrl,
167
+ '--robot-id', internal.name,
168
+ '--video-device', opts.videoDevice ?? 'auto',
169
+ '--audio-device', opts.audioDevice ?? 'default',
170
+ '--save-config',
171
+ ];
172
+ if (opts.enrollmentToken)
173
+ previewAgentArgs.push('--enrollment-token', opts.enrollmentToken);
163
174
  console.log(chalk.dim(' robot command:'));
164
175
  console.log(' ' + chalk.cyan(`infinicode ${args.join(' ')}`));
176
+ console.log(' ' + chalk.cyan(`robopark ${previewAgentArgs.join(' ')}`));
165
177
  console.log();
166
178
  if (opts.start) {
167
179
  const robotArgv = await infinicodeArgv(args);
168
180
  console.log(chalk.dim(' starting robot satellite node…'));
169
181
  runDetached(robotArgv[0], robotArgv.slice(1));
182
+ if (opts.enrollmentToken) {
183
+ console.log(chalk.dim(' enrolling robot with scheduler…'));
184
+ const enrollArgv = [process.execPath, process.argv[1], 'enroll',
185
+ '--scheduler-url', schedulerUrl,
186
+ '--robot-id', internal.name,
187
+ '--enrollment-token', opts.enrollmentToken,
188
+ '--save-config'];
189
+ runDetached(enrollArgv[0], enrollArgv.slice(1));
190
+ }
191
+ console.log(chalk.dim(' starting preview agent…'));
192
+ const previewArgv = [process.execPath, process.argv[1], ...previewAgentArgs];
193
+ runDetached(previewArgv[0], previewArgv.slice(1));
170
194
  }
171
195
  if (opts.autoStart) {
172
196
  const robotArgv = await infinicodeArgv(args);
@@ -177,6 +201,14 @@ async function setupRobot(config, opts, ctx, internal) {
177
201
  args: robotArgv.slice(1),
178
202
  });
179
203
  console.log(reg.ok ? chalk.green(` ✓ ${reg.message}`) : chalk.yellow(` ⚠ ${reg.message}`));
204
+ const previewArgv = [process.execPath, process.argv[1], ...previewAgentArgs, '--foreground'];
205
+ const reg2 = await registerAutoStart({
206
+ role: 'robot-preview-agent',
207
+ name: internal.name,
208
+ command: previewArgv[0],
209
+ args: previewArgv.slice(1),
210
+ });
211
+ console.log(reg2.ok ? chalk.green(` ✓ ${reg2.message}`) : chalk.yellow(` ⚠ ${reg2.message}`));
180
212
  }
181
213
  }
182
214
  async function setupControl(config, opts, ctx, internal) {
@@ -0,0 +1,6 @@
1
+ export interface VerifyOptions {
2
+ schedulerUrl?: string;
3
+ robotId?: string;
4
+ token?: string;
5
+ }
6
+ export declare function roboparkVerify(opts: VerifyOptions): Promise<void>;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * RoboPark — `robopark verify`.
3
+ *
4
+ * Checks the fleet end-to-end and reports what's working vs. missing:
5
+ * - hub / scheduler reachable
6
+ * - LiveKit configured and server registered
7
+ * - production mode enabled
8
+ * - robots/devices enrolled
9
+ * - preview agent running on robots
10
+ * - preview room can be started
11
+ *
12
+ * Usage:
13
+ * robopark verify
14
+ * robopark verify --scheduler-url http://livekit-1:8080 --robot-id robobmw
15
+ */
16
+ import { existsSync } from 'node:fs';
17
+ import { homedir, hostname } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import chalk from 'chalk';
20
+ import { discoverContext } from './discovery.js';
21
+ function loadDeviceToken(robotId) {
22
+ const paths = [
23
+ join(homedir(), '.robopark', 'device_token'),
24
+ ];
25
+ for (const p of paths) {
26
+ if (existsSync(p))
27
+ return readFile(p);
28
+ }
29
+ return undefined;
30
+ }
31
+ function readFile(p) {
32
+ const { readFileSync } = require('node:fs');
33
+ return readFileSync(p, 'utf8').trim();
34
+ }
35
+ async function fetchJson(url, init) {
36
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(5000) });
37
+ if (!res.ok)
38
+ throw new Error(`${res.status} ${res.statusText}`);
39
+ return res.json();
40
+ }
41
+ export async function roboparkVerify(opts) {
42
+ const ctx = await discoverContext();
43
+ const schedulerUrl = (opts.schedulerUrl ?? (ctx.hub ? `http://${ctx.hub.ip}:8080` : 'http://localhost:8080')).replace(/\/$/, '');
44
+ const robotId = opts.robotId ?? hostname().split('.')[0];
45
+ console.log(chalk.bold('\n robopark verify'));
46
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
47
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
48
+ console.log(` robot: ${chalk.cyan(robotId)}`);
49
+ console.log();
50
+ const checks = [];
51
+ // 1. Scheduler reachable
52
+ try {
53
+ const settings = (await fetchJson(`${schedulerUrl}/api/settings`));
54
+ checks.push({ name: 'scheduler reachable', ok: true, message: `production_mode=${settings.production_mode ?? false}` });
55
+ if (!settings.production_mode) {
56
+ checks.push({ name: 'production mode', ok: false, message: 'Production mode is OFF. Toggle it in the dashboard or PUT /api/settings', detail: 'Pi preview/session enrollment is blocked until production mode is ON.' });
57
+ }
58
+ else {
59
+ checks.push({ name: 'production mode', ok: true, message: 'ON' });
60
+ }
61
+ }
62
+ catch (e) {
63
+ checks.push({ name: 'scheduler reachable', ok: false, message: `could not reach scheduler: ${e.message}`, detail: 'Is robopark serve running on the hub? Is the scheduler port open?' });
64
+ }
65
+ // 2. LiveKit configured
66
+ try {
67
+ const lk = (await fetchJson(`${schedulerUrl}/api/livekit/config`));
68
+ if (lk.url && lk.has_secret) {
69
+ checks.push({ name: 'livekit configured', ok: true, message: `url=${lk.url}` });
70
+ }
71
+ else {
72
+ checks.push({ name: 'livekit configured', ok: false, message: 'LiveKit URL or API secret missing', detail: 'Set LiveKit config in the dashboard or via robopark setup-livekit.' });
73
+ }
74
+ }
75
+ catch (e) {
76
+ checks.push({ name: 'livekit configured', ok: false, message: `error: ${e.message}` });
77
+ }
78
+ // 3. LiveKit server registered
79
+ try {
80
+ const servers = (await fetchJson(`${schedulerUrl}/api/servers`));
81
+ const online = servers.filter(s => s.status === 'online');
82
+ if (online.length > 0) {
83
+ checks.push({ name: 'livekit server registered', ok: true, message: `${online.length} online server(s)` });
84
+ }
85
+ else if (servers.length > 0) {
86
+ checks.push({ name: 'livekit server registered', ok: false, message: `${servers.length} server(s) but none online`, detail: 'Check that the LiveKit container/process is running and reachable from the hub.' });
87
+ }
88
+ else {
89
+ checks.push({ name: 'livekit server registered', ok: false, message: 'no servers in scheduler', detail: 'Add a LiveKit server in the dashboard or run robopark setup-livekit.' });
90
+ }
91
+ }
92
+ catch (e) {
93
+ checks.push({ name: 'livekit server registered', ok: false, message: `error: ${e.message}` });
94
+ }
95
+ // 4. Device enrolled
96
+ let deviceToken;
97
+ let deviceId;
98
+ try {
99
+ const devices = (await fetchJson(`${schedulerUrl}/api/devices`));
100
+ const device = devices.find(d => d.name === robotId || d.id === robotId);
101
+ if (device) {
102
+ deviceId = device.id;
103
+ checks.push({ name: 'device enrolled', ok: true, message: `${device.id} (${device.status})` });
104
+ }
105
+ else {
106
+ checks.push({ name: 'device enrolled', ok: false, message: `${robotId} not found in scheduler`, detail: 'Run robopark enroll on the robot with the scheduler enrollment token.' });
107
+ }
108
+ }
109
+ catch (e) {
110
+ checks.push({ name: 'device enrolled', ok: false, message: `error: ${e.message}` });
111
+ }
112
+ // 5. Local device token
113
+ deviceToken = loadDeviceToken(robotId);
114
+ if (deviceToken) {
115
+ checks.push({ name: 'local device token', ok: true, message: `found ~/.robopark/device_token` });
116
+ }
117
+ else {
118
+ checks.push({ name: 'local device token', ok: false, message: 'no ~/.robopark/device_token', detail: 'Run robopark enroll on this machine first.' });
119
+ }
120
+ // 6. Robot row exists
121
+ try {
122
+ const robots = (await fetchJson(`${schedulerUrl}/api/robots`));
123
+ const robot = robots.find(r => r.id === (deviceId || robotId) || r.name === robotId);
124
+ if (robot) {
125
+ checks.push({ name: 'robot registered', ok: true, message: `${robot.id} (${robot.status})` });
126
+ }
127
+ else {
128
+ checks.push({ name: 'robot registered', ok: false, message: `${robotId} not in robots table`, detail: 'The device should auto-create a robot row on enrollment. Re-run enroll or check scheduler logs.' });
129
+ }
130
+ }
131
+ catch (e) {
132
+ checks.push({ name: 'robot registered', ok: false, message: `error: ${e.message}` });
133
+ }
134
+ // 7. Preview agent polling
135
+ if (deviceToken) {
136
+ try {
137
+ const agentRes = (await fetchJson(`${schedulerUrl}/api/robots/${robotId}/preview/agent`, {
138
+ headers: { Authorization: `Bearer ${deviceToken}` },
139
+ }));
140
+ if (agentRes.active) {
141
+ checks.push({ name: 'preview agent polling', ok: true, message: 'preview currently active' });
142
+ }
143
+ else {
144
+ checks.push({ name: 'preview agent polling', ok: true, message: `no active preview (reason=${agentRes.reason || 'none'})`, detail: 'This is normal when the dashboard is not watching. Start a preview from the dashboard.' });
145
+ }
146
+ }
147
+ catch (e) {
148
+ checks.push({ name: 'preview agent polling', ok: false, message: `error: ${e.message}`, detail: 'Is robopark preview-agent running on the robot?' });
149
+ }
150
+ }
151
+ else {
152
+ checks.push({ name: 'preview agent polling', ok: false, message: 'skipped (no device token)' });
153
+ }
154
+ // 8. Try starting a preview from the operator side
155
+ try {
156
+ const preview = (await fetchJson(`${schedulerUrl}/api/robots/${robotId}/preview/start`, { method: 'POST' }));
157
+ if (preview.active) {
158
+ checks.push({ name: 'operator preview start', ok: true, message: `room assigned at ${preview.url}` });
159
+ // stop it so we don't leave a stale preview running
160
+ await fetch(`${schedulerUrl}/api/robots/${robotId}/preview/stop`, { method: 'POST' });
161
+ }
162
+ else {
163
+ checks.push({ name: 'operator preview start', ok: false, message: `could not start: ${preview.reason || 'unknown'}`, detail: 'Enable production mode, add a LiveKit server, and ensure the robot is not in a session.' });
164
+ }
165
+ }
166
+ catch (e) {
167
+ checks.push({ name: 'operator preview start', ok: false, message: `error: ${e.message}` });
168
+ }
169
+ // Print results
170
+ let pass = 0;
171
+ let fail = 0;
172
+ for (const c of checks) {
173
+ const icon = c.ok ? chalk.green('✓') : chalk.red('✗');
174
+ const color = c.ok ? chalk.green : chalk.red;
175
+ console.log(` ${icon} ${color(c.name)} — ${c.message}`);
176
+ if (c.detail) {
177
+ console.log(chalk.dim(` → ${c.detail}`));
178
+ }
179
+ if (c.ok)
180
+ pass++;
181
+ else
182
+ fail++;
183
+ }
184
+ console.log();
185
+ if (fail === 0) {
186
+ console.log(chalk.bold.green(` ${pass}/${checks.length} checks passed — fleet is ready for preview.`));
187
+ }
188
+ else {
189
+ console.log(chalk.bold.yellow(` ${pass}/${checks.length} passed, ${fail} failed — fix the items above.`));
190
+ process.exitCode = 1;
191
+ }
192
+ }
@@ -17,7 +17,7 @@ const config = new Conf({
17
17
  });
18
18
  const program = new Command('robopark')
19
19
  .description('RoboPark fleet control CLI — set up, watch, and drive talking robots')
20
- .version('2.8.8');
20
+ .version('2.8.9');
21
21
  program
22
22
  .command('scan')
23
23
  .description('Auto-discover the fleet and print ready-to-run setup commands')
@@ -31,6 +31,38 @@ program
31
31
  gateway: opts.gateway,
32
32
  });
33
33
  });
34
+ program
35
+ .command('probe')
36
+ .description('Dispatch opencode agents across the mesh to map infrastructure')
37
+ .option('--token <token>', 'mesh auth token (optional, for context)')
38
+ .option('--timeout <sec>', 'seconds per node', '60')
39
+ .option('--output <file>', 'write JSON results to this file')
40
+ .option('--local', 'probe only this machine')
41
+ .action(async (opts) => {
42
+ const { roboparkProbe } = await import('./robopark/probe.js');
43
+ await roboparkProbe(opts);
44
+ });
45
+ program
46
+ .command('setup-livekit')
47
+ .description('Dispatch an opencode agent on the hub to install LiveKit server via Docker')
48
+ .option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
49
+ .option('--token <token>', 'mesh auth token')
50
+ .option('--scheduler-url <url>', 'scheduler URL to register the new server, e.g. http://100.64.1.2:8080')
51
+ .option('--timeout <sec>', 'max seconds to wait for the agent', '180')
52
+ .option('--dry-run', 'print the prompt instead of dispatching')
53
+ .action(async (opts) => {
54
+ const { roboparkSetupLivekit } = await import('./robopark/setup-livekit.js');
55
+ await roboparkSetupLivekit(opts);
56
+ });
57
+ program
58
+ .command('verify')
59
+ .description('Verify the RoboPark fleet is ready for live preview')
60
+ .option('--scheduler-url <url>', 'scheduler base URL, e.g. http://livekit-1:8080')
61
+ .option('--robot-id <id>', 'robot name to verify (defaults to hostname)')
62
+ .action(async (opts) => {
63
+ const { roboparkVerify } = await import('./robopark/verify.js');
64
+ await roboparkVerify(opts);
65
+ });
34
66
  program
35
67
  .command('serve')
36
68
  .description('Start the RoboPark scheduler + web UI')
@@ -44,6 +76,35 @@ program
44
76
  .action(async (opts) => {
45
77
  await roboparkServe(opts);
46
78
  });
79
+ program
80
+ .command('enroll')
81
+ .description('Enroll this robot/satellite with the RoboPark scheduler')
82
+ .option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
83
+ .option('--robot-id <id>', 'robot/device name (defaults to hostname)')
84
+ .option('--enrollment-token <token>', 'one-time enrollment token (or ENV ENROLLMENT_TOKEN)')
85
+ .option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
86
+ .action(async (opts) => {
87
+ const { roboparkEnroll } = await import('./robopark/enroll.js');
88
+ await roboparkEnroll(opts);
89
+ });
90
+ program
91
+ .command('preview-agent')
92
+ .description('Start the robot-side preview agent (camera + mic to scheduler)')
93
+ .option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
94
+ .option('--robot-id <id>', 'robot/device name (defaults to hostname)')
95
+ .option('--device-token <token>', 'long-lived device token')
96
+ .option('--enrollment-token <token>', 'one-time enrollment token')
97
+ .option('--video-device <dev>', 'camera: auto, /dev/video0, 0, picamera', 'auto')
98
+ .option('--audio-device <dev>', 'microphone: default, none, or name/index', 'default')
99
+ .option('--width <px>', 'video width', '640')
100
+ .option('--height <px>', 'video height', '480')
101
+ .option('--fps <n>', 'video fps', '15')
102
+ .option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
103
+ .option('--foreground', 'stay in foreground; do not detach')
104
+ .action(async (opts) => {
105
+ const { roboparkPreviewAgent } = await import('./robopark/preview-agent-launcher.js');
106
+ await roboparkPreviewAgent(opts);
107
+ });
47
108
  program
48
109
  .command('setup')
49
110
  .description('Set up this device in one pass')
@@ -61,6 +122,9 @@ program
61
122
  .option('--scheduler-port <port>', 'scheduler port on hub', '8080')
62
123
  .option('--tailscale-auth <token>', 'Tailscale auth key for the hub')
63
124
  .option('--tag <tag>', 'Tailscale tag filter')
125
+ .option('--enrollment-token <token>', 'one-time scheduler enrollment token for robots')
126
+ .option('--video-device <dev>', 'default camera for robot preview agent', 'auto')
127
+ .option('--audio-device <dev>', 'default microphone for robot preview agent', 'default')
64
128
  .option('--start', 'launch the node now')
65
129
  .option('--auto-start', 'register to start on boot')
66
130
  .option('--yes', 'non-interactive mode')
@@ -82,6 +146,9 @@ program
82
146
  schedulerPort: opts.schedulerPort,
83
147
  tailscaleAuth: opts.tailscaleAuth,
84
148
  tag: opts.tag,
149
+ enrollmentToken: opts.enrollmentToken,
150
+ videoDevice: opts.videoDevice,
151
+ audioDevice: opts.audioDevice,
85
152
  start: opts.start,
86
153
  autoStart: opts.autoStart,
87
154
  yes: opts.yes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.8",
3
+ "version": "2.8.10",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -3,7 +3,12 @@ FROM python:3.11-slim
3
3
 
4
4
  WORKDIR /app
5
5
 
6
- # Install dependencies
6
+ # Install build dependencies for pyaudio / opencv wheels, then Python packages.
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ gcc g++ libportaudio2 portaudio19-dev \
9
+ libgl1 libglib2.0-0 libsm6 libxext6 libxrender-dev \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
7
12
  COPY requirements.txt .
8
13
  RUN pip install --no-cache-dir -r requirements.txt
9
14