infinicode 2.8.8 → 2.8.9

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.
@@ -1,5 +1,5 @@
1
1
  export interface ServiceArgs {
2
- role: 'hub' | 'robot' | 'control' | 'hub-scheduler';
2
+ role: 'hub' | 'robot' | 'control' | 'hub-scheduler' | 'robot-preview-agent';
3
3
  name: string;
4
4
  command: string;
5
5
  args: string[];
@@ -0,0 +1,7 @@
1
+ export interface EnrollOptions {
2
+ schedulerUrl: string;
3
+ robotId?: string;
4
+ enrollmentToken?: string;
5
+ saveConfig?: boolean;
6
+ }
7
+ export declare function roboparkEnroll(opts: EnrollOptions): Promise<void>;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * RoboPark — `robopark enroll`.
3
+ *
4
+ * Enrolls this machine as a scheduler device. Either uses an explicit
5
+ * enrollment token, or reads it from ~/.robopark/enrollment_token.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { homedir, hostname } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import chalk from 'chalk';
12
+ const ROBOPARK_DIR = join(homedir(), '.robopark');
13
+ const ENROLLMENT_FILE = join(ROBOPARK_DIR, 'enrollment_token');
14
+ function ensureDir() {
15
+ mkdirSync(ROBOPARK_DIR, { recursive: true });
16
+ }
17
+ function readEnrollmentToken() {
18
+ if (existsSync(ENROLLMENT_FILE)) {
19
+ return readFileSync(ENROLLMENT_FILE, 'utf8').trim();
20
+ }
21
+ return undefined;
22
+ }
23
+ function saveEnrollmentToken(token) {
24
+ ensureDir();
25
+ writeFileSync(ENROLLMENT_FILE, token, { mode: 0o600 });
26
+ }
27
+ async function findSchedulerPath() {
28
+ const candidates = [
29
+ join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
30
+ join(process.cwd(), 'scheduler', 'preview_agent.py'),
31
+ ];
32
+ for (const p of candidates) {
33
+ if (existsSync(p))
34
+ return p;
35
+ }
36
+ try {
37
+ const { createRequire } = await import('node:module');
38
+ const require = createRequire(import.meta.url);
39
+ const pkgMain = require.resolve('infinicode/package.json');
40
+ const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
41
+ if (existsSync(p))
42
+ return p;
43
+ }
44
+ catch { /* ignore */ }
45
+ return null;
46
+ }
47
+ function findPython() {
48
+ const names = process.platform === 'win32'
49
+ ? ['python', 'python3', 'py']
50
+ : ['python3', 'python'];
51
+ for (const name of names) {
52
+ try {
53
+ const { execSync } = require('node:child_process');
54
+ execSync(`${name} --version`, { stdio: 'ignore' });
55
+ return name;
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ }
61
+ return process.platform === 'win32' ? 'python' : 'python3';
62
+ }
63
+ export async function roboparkEnroll(opts) {
64
+ const token = opts.enrollmentToken ?? readEnrollmentToken();
65
+ if (!token) {
66
+ console.log(chalk.red(' ✗ pass --enrollment-token <token> or write it to ~/.robopark/enrollment_token'));
67
+ process.exit(1);
68
+ }
69
+ saveEnrollmentToken(token);
70
+ const robotId = opts.robotId ?? hostname().split('.')[0];
71
+ const schedulerUrl = opts.schedulerUrl;
72
+ const script = await findSchedulerPath();
73
+ if (!script) {
74
+ console.log(chalk.red(' ✗ could not find preview_agent.py'));
75
+ process.exit(1);
76
+ }
77
+ const python = findPython();
78
+ const args = [
79
+ script,
80
+ '--scheduler-url', schedulerUrl,
81
+ '--robot-id', robotId,
82
+ '--enrollment-token', token,
83
+ ];
84
+ if (opts.saveConfig)
85
+ args.push('--save-config');
86
+ console.log(chalk.bold('\n robopark enroll'));
87
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
88
+ console.log(` robot: ${chalk.cyan(robotId)}`);
89
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
90
+ console.log();
91
+ const proc = spawn(python, args, { stdio: 'inherit' });
92
+ await new Promise((resolve, reject) => {
93
+ proc.on('error', reject);
94
+ proc.on('close', (code) => {
95
+ if (code === 0)
96
+ resolve();
97
+ else
98
+ reject(new Error(`enrollment exited ${code ?? ''}`));
99
+ });
100
+ });
101
+ }
@@ -0,0 +1,14 @@
1
+ export interface PreviewAgentOptions {
2
+ schedulerUrl: string;
3
+ robotId?: string;
4
+ deviceToken?: string;
5
+ enrollmentToken?: string;
6
+ videoDevice: string;
7
+ audioDevice: string;
8
+ width: string;
9
+ height: string;
10
+ fps: string;
11
+ saveConfig?: boolean;
12
+ foreground?: boolean;
13
+ }
14
+ export declare function roboparkPreviewAgent(opts: PreviewAgentOptions): Promise<void>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * RoboPark — `robopark preview-agent` launcher.
3
+ *
4
+ * Starts preview_agent.py with the right Python interpreter, auto-discovers
5
+ * the scheduler URL from mesh/hub, and detaches unless --foreground.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { existsSync } from 'node:fs';
9
+ import { hostname } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import chalk from 'chalk';
12
+ import { discoverContext } from './discovery.js';
13
+ const DEFAULT_SCHEDULER_PORT = 8080;
14
+ async function findSchedulerPath() {
15
+ const candidates = [
16
+ join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
17
+ join(process.cwd(), 'scheduler', 'preview_agent.py'),
18
+ ];
19
+ for (const p of candidates) {
20
+ if (existsSync(p))
21
+ return p;
22
+ }
23
+ try {
24
+ const { createRequire } = await import('node:module');
25
+ const require = createRequire(import.meta.url);
26
+ const pkgMain = require.resolve('infinicode/package.json');
27
+ const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
28
+ if (existsSync(p))
29
+ return p;
30
+ }
31
+ catch { /* ignore */ }
32
+ return null;
33
+ }
34
+ function findPython() {
35
+ const names = process.platform === 'win32'
36
+ ? ['python', 'python3', 'py']
37
+ : ['python3', 'python'];
38
+ for (const name of names) {
39
+ try {
40
+ const { execSync } = require('node:child_process');
41
+ execSync(`${name} --version`, { stdio: 'ignore' });
42
+ return name;
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ }
48
+ return process.platform === 'win32' ? 'python' : 'python3';
49
+ }
50
+ export async function roboparkPreviewAgent(opts) {
51
+ const ctx = await discoverContext();
52
+ const port = DEFAULT_SCHEDULER_PORT;
53
+ const schedulerUrl = opts.schedulerUrl !== 'http://localhost:8080'
54
+ ? opts.schedulerUrl
55
+ : ctx.hub
56
+ ? `http://${ctx.hub.ip}:${port}`
57
+ : 'http://localhost:8080';
58
+ const robotId = opts.robotId ?? hostname().split('.')[0];
59
+ const script = await findSchedulerPath();
60
+ if (!script) {
61
+ console.log(chalk.red(' ✗ could not find preview_agent.py'));
62
+ process.exit(1);
63
+ }
64
+ const python = findPython();
65
+ const args = [
66
+ script,
67
+ '--scheduler-url', schedulerUrl,
68
+ '--robot-id', robotId,
69
+ '--video-device', opts.videoDevice,
70
+ '--audio-device', opts.audioDevice,
71
+ '--width', opts.width,
72
+ '--height', opts.height,
73
+ '--fps', opts.fps,
74
+ ];
75
+ if (opts.deviceToken)
76
+ args.push('--device-token', opts.deviceToken);
77
+ if (opts.enrollmentToken)
78
+ args.push('--enrollment-token', opts.enrollmentToken);
79
+ if (opts.saveConfig)
80
+ args.push('--save-config');
81
+ console.log(chalk.bold('\n robopark preview-agent'));
82
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
83
+ console.log(` robot: ${chalk.cyan(robotId)}`);
84
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
85
+ console.log(` video: ${chalk.cyan(opts.videoDevice)}`);
86
+ console.log(` audio: ${chalk.cyan(opts.audioDevice)}`);
87
+ console.log();
88
+ if (opts.foreground) {
89
+ const proc = spawn(python, args, { stdio: 'inherit' });
90
+ await new Promise((resolve) => {
91
+ proc.on('close', () => resolve());
92
+ });
93
+ return;
94
+ }
95
+ const proc = spawn(python, args, {
96
+ stdio: 'ignore',
97
+ detached: true,
98
+ env: { ...process.env, ROBOPARK_PREVIEW_AGENT: '1' },
99
+ });
100
+ proc.unref();
101
+ console.log(chalk.green(' ✓ preview agent started in background'));
102
+ }
@@ -0,0 +1,7 @@
1
+ export interface ProbeOptions {
2
+ token?: string;
3
+ timeout?: string;
4
+ output?: string;
5
+ local?: boolean;
6
+ }
7
+ export declare function roboparkProbe(opts: ProbeOptions): Promise<void>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * RoboPark — `robopark probe`.
3
+ *
4
+ * Spawn opencode agents (locally or over mesh) to build an accurate
5
+ * infrastructure map. Discovers peers via Tailscale, then runs a structured
6
+ * prompt through the locally installed `opencode` CLI for each node.
7
+ *
8
+ * The agent is asked only to report facts — it does not mutate anything.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { hostname, networkInterfaces } from 'node:os';
12
+ import chalk from 'chalk';
13
+ import { discoverContext } from './discovery.js';
14
+ function getLanIps() {
15
+ const out = [];
16
+ for (const [name, addrs] of Object.entries(networkInterfaces())) {
17
+ if (!addrs)
18
+ continue;
19
+ for (const a of addrs) {
20
+ if (a.family === 'IPv4' && !a.internal)
21
+ out.push(a.address);
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ function findOpencode() {
27
+ const names = process.platform === 'win32' ? ['opencode.exe', 'opencode'] : ['opencode'];
28
+ for (const name of names) {
29
+ try {
30
+ const { execSync } = require('node:child_process');
31
+ execSync(`${name} --version`, { stdio: 'ignore' });
32
+ return name;
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ function runOpencodePrompt(prompt, timeoutMs) {
41
+ const bin = findOpencode();
42
+ if (!bin)
43
+ return Promise.resolve('opencode CLI not found; install opencode to run probes');
44
+ return new Promise((resolve) => {
45
+ const proc = spawn(bin, ['--no-project', '--non-interactive'], {
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ env: { ...process.env, OPENCODE_QUIET: '1' },
48
+ });
49
+ let stdout = '';
50
+ let stderr = '';
51
+ let killed = false;
52
+ const timer = setTimeout(() => {
53
+ killed = true;
54
+ proc.kill('SIGTERM');
55
+ }, timeoutMs);
56
+ proc.stdin?.write(prompt);
57
+ proc.stdin?.end();
58
+ proc.stdout?.on('data', (d) => { stdout += d.toString(); });
59
+ proc.stderr?.on('data', (d) => { stderr += d.toString(); });
60
+ proc.on('close', () => {
61
+ clearTimeout(timer);
62
+ if (killed)
63
+ resolve(stdout + '\n[probe timed out]');
64
+ else
65
+ resolve(stdout || stderr || '(no output)');
66
+ });
67
+ proc.on('error', (err) => {
68
+ clearTimeout(timer);
69
+ resolve(`opencode error: ${err.message}`);
70
+ });
71
+ });
72
+ }
73
+ function probePrompt(target, token) {
74
+ const tokenHint = token ? `mesh token is "${token}"` : 'no mesh token provided';
75
+ return `You are an infrastructure probe for the RoboPark fleet.
76
+ Target node: ${target.name} (${target.ip}) via ${target.source}.
77
+ ${tokenHint}.
78
+
79
+ Do NOT modify anything. Only report facts. Keep your answer under 600 words.
80
+
81
+ Please report:
82
+ 1. Hostname, OS, architecture.
83
+ 2. LAN IPs and Tailscale IP if available.
84
+ 3. Whether these processes are running: infinicode node, robopark scheduler, robopark preview_agent.
85
+ 4. If this node is the hub, whether the scheduler at http://localhost:8080/api/settings responds and what production_mode is.
86
+ 5. If this node is a robot/satellite, whether it has enrolled to the scheduler and whether ~/.robopark/device_token exists.
87
+ 6. Any recent errors in journalctl/systemctl for infinicode or robopark services (last 10 lines).
88
+ 7. Whether a camera (/dev/video0) or microphone is detected.
89
+
90
+ Answer as concise structured Markdown bullet list.`;
91
+ }
92
+ async function probeTarget(target, opts) {
93
+ const timeoutMs = opts.timeout ? parseInt(opts.timeout, 10) * 1000 : 60000;
94
+ try {
95
+ const summary = await runOpencodePrompt(probePrompt(target, opts.token), timeoutMs);
96
+ return { target, ok: true, summary, timestamp: new Date().toISOString() };
97
+ }
98
+ catch (e) {
99
+ return { target, ok: false, error: e.message, summary: '', timestamp: new Date().toISOString() };
100
+ }
101
+ }
102
+ export async function roboparkProbe(opts) {
103
+ const ctx = await discoverContext();
104
+ const self = { name: hostname().split('.')[0], ip: getLanIps()[0] || '127.0.0.1', source: 'self' };
105
+ const rawTargets = opts.local ? [self] : [self, ...(ctx.robots || []), ...(ctx.hub ? [ctx.hub] : [])];
106
+ const targets = rawTargets.map((t) => ({
107
+ name: t.name,
108
+ ip: t.ip,
109
+ source: t === self ? 'self' : 'tailscale',
110
+ }));
111
+ console.log(chalk.bold('\n robopark probe'));
112
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
113
+ console.log(` targets: ${chalk.cyan(targets.length)}`);
114
+ console.log();
115
+ const results = [];
116
+ for (const t of targets) {
117
+ process.stdout.write(chalk.dim(` probing ${t.name}… `));
118
+ const r = await probeTarget(t, opts);
119
+ results.push(r);
120
+ console.log(r.ok ? chalk.green('ok') : chalk.red('failed'));
121
+ }
122
+ console.log();
123
+ console.log(chalk.bold(' Probe results'));
124
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
125
+ for (const r of results) {
126
+ console.log(`\n ${chalk.cyan(r.target.name)} (${r.target.ip})`);
127
+ if (!r.ok) {
128
+ console.log(chalk.red(` error: ${r.error}`));
129
+ continue;
130
+ }
131
+ console.log(r.summary.split('\n').map(l => ' ' + l).join('\n'));
132
+ }
133
+ if (opts.output) {
134
+ const { writeFileSync } = await import('node:fs');
135
+ writeFileSync(opts.output, JSON.stringify(results, null, 2), 'utf8');
136
+ console.log(`\n ${chalk.green('✓')} wrote ${opts.output}`);
137
+ }
138
+ }
@@ -108,6 +108,19 @@ export async function roboparkServe(opts) {
108
108
  const port = opts.port ? parseInt(opts.port, 10) : 8080;
109
109
  const host = opts.host ?? '0.0.0.0';
110
110
  const python = findPython();
111
+ const schedulerDir = dirname(schedulerPath);
112
+ const requirements = join(schedulerDir, 'requirements.txt');
113
+ // Ensure scheduler Python dependencies are installed before starting.
114
+ if (existsSync(requirements)) {
115
+ console.log(chalk.dim(' ensuring scheduler Python deps…'));
116
+ try {
117
+ execSync(`${python} -m pip install -r "${requirements}" --user --quiet`, { stdio: 'pipe' });
118
+ }
119
+ catch (err) {
120
+ console.log(chalk.yellow(` ⚠ pip install failed: ${err instanceof Error ? err.message : String(err)}`));
121
+ console.log(chalk.dim(' continuing; scheduler may fail if deps are missing.'));
122
+ }
123
+ }
111
124
  const env = {
112
125
  ...process.env,
113
126
  SCHEDULER_HOST: host,
@@ -0,0 +1,8 @@
1
+ export interface SetupLivekitOptions {
2
+ hubUrl?: string;
3
+ token?: string;
4
+ schedulerUrl?: string;
5
+ timeout?: string;
6
+ dryRun?: boolean;
7
+ }
8
+ export declare function roboparkSetupLivekit(opts: SetupLivekitOptions): Promise<void>;
@@ -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;