infinicode 2.8.22 → 2.8.25
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/dist/kernel/federation/peer-mesh.js +15 -1
- package/dist/kernel/providers/ollama-provider.d.ts +4 -0
- package/dist/kernel/providers/ollama-provider.js +16 -2
- package/dist/kernel/setup.js +13 -0
- package/dist/robopark/add-robot.d.ts +8 -0
- package/dist/robopark/add-robot.js +113 -0
- package/dist/robopark/agent-ctl.d.ts +20 -0
- package/dist/robopark/agent-ctl.js +237 -0
- package/dist/robopark/doctor.d.ts +6 -0
- package/dist/robopark/doctor.js +175 -0
- package/dist/robopark/llm-set.d.ts +8 -0
- package/dist/robopark/llm-set.js +87 -0
- package/dist/robopark/profile.d.ts +40 -0
- package/dist/robopark/profile.js +69 -0
- package/dist/robopark/server-add.d.ts +13 -0
- package/dist/robopark/server-add.js +114 -0
- package/dist/robopark/setup-livekit.js +89 -22
- package/dist/robopark/setup.js +52 -8
- package/dist/robopark/stop-all.d.ts +6 -0
- package/dist/robopark/stop-all.js +125 -0
- package/dist/robopark-cli.js +119 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +107 -10
- package/packages/robopark/scheduler/preview_agent.py +1011 -782
- package/packages/robopark/vision/app_pi_clean.py +81 -1
|
@@ -55,26 +55,52 @@ async function meshRpc(targetUrl, token, env) {
|
|
|
55
55
|
}
|
|
56
56
|
return (await res.json());
|
|
57
57
|
}
|
|
58
|
-
async function
|
|
58
|
+
async function getHubManifest(hubUrl, token) {
|
|
59
59
|
try {
|
|
60
60
|
const url = `${hubUrl.replace(/\/$/, '')}/fed/manifest?token=${encodeURIComponent(token)}`;
|
|
61
61
|
const res = await fetch(url, { headers: { accept: 'application/json' } });
|
|
62
62
|
if (!res.ok)
|
|
63
|
-
return null;
|
|
63
|
+
return { nodeId: null, platform: null };
|
|
64
64
|
const manifest = (await res.json());
|
|
65
|
-
return manifest.nodeId || manifest.id || null;
|
|
65
|
+
return { nodeId: manifest.nodeId || manifest.id || null, platform: manifest.platform ?? null };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { nodeId: null, platform: null };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Fallback: GET /fed/status returns the hub's own NodeManifest under `self`,
|
|
72
|
+
* including `platform`. Used when /fed/manifest doesn't carry it. */
|
|
73
|
+
async function getHubPlatformFromStatus(hubUrl, token) {
|
|
74
|
+
try {
|
|
75
|
+
const url = `${hubUrl.replace(/\/$/, '')}/fed/status?token=${encodeURIComponent(token)}`;
|
|
76
|
+
const res = await fetch(url, { headers: { accept: 'application/json' } });
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
return null;
|
|
79
|
+
const status = (await res.json());
|
|
80
|
+
return status.self?.platform ?? null;
|
|
66
81
|
}
|
|
67
82
|
catch {
|
|
68
83
|
return null;
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
|
-
const
|
|
86
|
+
const LIVEKIT_SETUP_PROMPT_HEADER = `You are the RoboPark infrastructure agent running on the fleet hub.
|
|
72
87
|
|
|
73
88
|
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
89
|
|
|
75
90
|
DO NOT ask the user for confirmation. Execute the steps directly. If a step is already done, skip it and report it.
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
`;
|
|
92
|
+
const LIVEKIT_SETUP_PROMPT_FOOTER = `
|
|
93
|
+
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:
|
|
94
|
+
\`\`\`json
|
|
95
|
+
{
|
|
96
|
+
"url": "ws://127.0.0.1:7880",
|
|
97
|
+
"api_key": "...",
|
|
98
|
+
"api_secret": "...",
|
|
99
|
+
"running": true
|
|
100
|
+
}
|
|
101
|
+
\`\`\`
|
|
102
|
+
`;
|
|
103
|
+
const LIVEKIT_SETUP_STEPS_LINUX = `Steps:
|
|
78
104
|
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
105
|
2. Pull the official LiveKit server image: docker pull livekit/livekit-server:latest
|
|
80
106
|
3. Create a directory /opt/livekit if it does not exist.
|
|
@@ -82,23 +108,58 @@ Steps:
|
|
|
82
108
|
docker run --rm livekit/livekit-server:latest generate-keys
|
|
83
109
|
Parse the output and write it as a YAML file with fields api_key and api_secret.
|
|
84
110
|
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
|
|
111
|
+
docker run -d --name livekit-server --restart unless-stopped --network host \\
|
|
112
|
+
-v /opt/livekit/keys.yaml:/etc/livekit/keys.yaml:ro \\
|
|
113
|
+
livekit/livekit-server:latest \\
|
|
88
114
|
--config /etc/livekit/keys.yaml --bind 0.0.0.0 --port 7880
|
|
89
115
|
6. Wait up to 30 seconds and verify it is healthy: curl -sf http://127.0.0.1:7880
|
|
90
116
|
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.
|
|
117
|
+
`;
|
|
118
|
+
// Windows hosts: Docker Desktop does not support --network host, so ports are
|
|
119
|
+
// mapped explicitly. --dev mode ships with well-known devkey/secret, which
|
|
120
|
+
// sidesteps the `generate-keys` dance (that container command is awkward to
|
|
121
|
+
// run/parse consistently from PowerShell).
|
|
122
|
+
const LIVEKIT_SETUP_STEPS_WINDOWS = `Steps (Windows host — use PowerShell, NOT bash):
|
|
123
|
+
1. Check if Docker Desktop is installed and running: docker info
|
|
124
|
+
If it is not installed, stop and report that Docker Desktop must be installed manually (winget install Docker.DockerDesktop), since it requires a GUI/WSL2 backend and cannot be silently installed headlessly.
|
|
125
|
+
2. Pull the official LiveKit server image: docker pull livekit/livekit-server:latest
|
|
126
|
+
3. Start the LiveKit server container in --dev mode (uses the well-known devkey/secret credentials, no key generation needed). Docker Desktop on Windows does not support --network host, so map ports explicitly:
|
|
127
|
+
docker run -d --name livekit-server --restart unless-stopped \`
|
|
128
|
+
-p 7880:7880 -p 7881:7881 -p 7881:7881/udp -p 50000-50100:50000-50100/udp \`
|
|
129
|
+
livekit/livekit-server:latest --dev --bind 0.0.0.0
|
|
130
|
+
(If a container named livekit-server already exists, remove it first with: docker rm -f livekit-server)
|
|
131
|
+
4. Wait up to 30 seconds and verify it is healthy: curl.exe -sf http://127.0.0.1:7880 (or Invoke-WebRequest)
|
|
132
|
+
5. The credentials in --dev mode are fixed: api_key = devkey, api_secret = secret.
|
|
133
|
+
6. Report back the WebSocket URL (ws://THIS_MACHINE_TAILSCALE_IP:7880 or ws://127.0.0.1:7880), api_key "devkey", api_secret "secret", and whether the container is running.
|
|
134
|
+
`;
|
|
135
|
+
// Used when the hub's platform could not be determined before dispatch: ask
|
|
136
|
+
// the agent to detect its own OS first, then follow the matching branch.
|
|
137
|
+
const LIVEKIT_SETUP_STEPS_AUTODETECT = `Steps:
|
|
138
|
+
0. FIRST, detect your own OS before running anything else:
|
|
139
|
+
- On Windows, $env:OS will be "Windows_NT" (PowerShell) — or the presence of \`Get-Command\` with no \`uname\` binary.
|
|
140
|
+
- On Linux/macOS, \`uname\` will succeed and print Linux/Darwin.
|
|
141
|
+
Branch into ONE of the two command sets below based on that detection. Do not run both.
|
|
91
142
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"api_secret": "...",
|
|
98
|
-
"running": true
|
|
99
|
-
}
|
|
100
|
-
\`\`\`
|
|
143
|
+
--- IF WINDOWS (use PowerShell): ---
|
|
144
|
+
${LIVEKIT_SETUP_STEPS_WINDOWS}
|
|
145
|
+
|
|
146
|
+
--- IF LINUX/macOS (use bash/sh): ---
|
|
147
|
+
${LIVEKIT_SETUP_STEPS_LINUX}
|
|
101
148
|
`;
|
|
149
|
+
function buildLivekitSetupPrompt(platform) {
|
|
150
|
+
let steps;
|
|
151
|
+
if (platform === 'win32') {
|
|
152
|
+
steps = LIVEKIT_SETUP_STEPS_WINDOWS;
|
|
153
|
+
}
|
|
154
|
+
else if (platform === 'linux' || platform === 'darwin') {
|
|
155
|
+
steps = LIVEKIT_SETUP_STEPS_LINUX;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Platform unknown ahead of dispatch — let the agent detect itself.
|
|
159
|
+
steps = LIVEKIT_SETUP_STEPS_AUTODETECT;
|
|
160
|
+
}
|
|
161
|
+
return LIVEKIT_SETUP_PROMPT_HEADER + '\n' + steps + LIVEKIT_SETUP_PROMPT_FOOTER;
|
|
162
|
+
}
|
|
102
163
|
function extractJson(text) {
|
|
103
164
|
const m = text.match(/\`\`\`json\s*([\s\S]*?)\s*\`\`\`/);
|
|
104
165
|
if (!m)
|
|
@@ -152,14 +213,20 @@ export async function roboparkSetupLivekit(opts) {
|
|
|
152
213
|
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
153
214
|
console.log(` hub: ${chalk.cyan(hubUrl)}`);
|
|
154
215
|
console.log(` scheduler: ${chalk.cyan(schedulerUrl ?? 'not provided')}`);
|
|
216
|
+
const from = hostname().split('.')[0];
|
|
217
|
+
const manifest = await getHubManifest(hubUrl, token);
|
|
218
|
+
let platform = manifest.platform;
|
|
219
|
+
if (!platform)
|
|
220
|
+
platform = await getHubPlatformFromStatus(hubUrl, token);
|
|
221
|
+
console.log(` hub platform: ${chalk.cyan(platform ?? 'unknown (agent will self-detect)')}`);
|
|
155
222
|
console.log();
|
|
223
|
+
const prompt = buildLivekitSetupPrompt(platform);
|
|
156
224
|
if (opts.dryRun) {
|
|
157
225
|
console.log(chalk.yellow(' dry run — prompt that would be sent:'));
|
|
158
|
-
console.log(chalk.dim(
|
|
226
|
+
console.log(chalk.dim(prompt.split('\n').map(l => ' ' + l).join('\n')));
|
|
159
227
|
return;
|
|
160
228
|
}
|
|
161
|
-
const
|
|
162
|
-
const nodeId = await getHubNodeId(hubUrl, token);
|
|
229
|
+
const nodeId = manifest.nodeId;
|
|
163
230
|
if (!nodeId) {
|
|
164
231
|
console.log(chalk.red(' ✗ hub manifest not reachable. Is infinicode serve running on the hub?'));
|
|
165
232
|
process.exit(1);
|
|
@@ -168,7 +235,7 @@ export async function roboparkSetupLivekit(opts) {
|
|
|
168
235
|
const dispatchEnv = envelope('task.dispatch', from, {
|
|
169
236
|
description: 'Install LiveKit server on hub via Docker',
|
|
170
237
|
capabilities: ['shell', 'docker', 'reasoning'],
|
|
171
|
-
prompt
|
|
238
|
+
prompt,
|
|
172
239
|
agent: 'opencode',
|
|
173
240
|
context: { role: 'hub', site: 'fleet-hub' },
|
|
174
241
|
}, { to: nodeId, auth: token });
|
package/dist/robopark/setup.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* robopark setup --control --start
|
|
10
10
|
*/
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
@@ -23,6 +23,43 @@ function saveToken(token) {
|
|
|
23
23
|
mkdirSync(dir, { recursive: true });
|
|
24
24
|
writeFileSync(join(dir, 'mesh.token'), token, 'utf8');
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Clear a stale device identity before a fresh enrollment.
|
|
28
|
+
*
|
|
29
|
+
* preview_agent.py only re-enrolls (POSTs /api/devices/enroll) when it has
|
|
30
|
+
* NO device_token. If ~/.robopark/device_token and ~/.robopark/preview_agent.json
|
|
31
|
+
* survive from a previous enrollment (e.g. against a different scheduler),
|
|
32
|
+
* PreviewAgent.__init__ loads that old token/device_id and skips enrollment
|
|
33
|
+
* entirely — silently reusing a stale identity, which then 401s against the
|
|
34
|
+
* new target scheduler. An explicit --enrollment-token means the caller
|
|
35
|
+
* wants a fresh enrollment, not to reuse a stale identity from a previous
|
|
36
|
+
* target scheduler, so wipe the persisted device_token/device_id (and the
|
|
37
|
+
* token file) whenever one is passed.
|
|
38
|
+
*/
|
|
39
|
+
function resetStaleEnrollment() {
|
|
40
|
+
const dir = join(homedir(), '.robopark');
|
|
41
|
+
const configPath = join(dir, 'preview_agent.json');
|
|
42
|
+
const tokenPath = join(dir, 'device_token');
|
|
43
|
+
if (existsSync(tokenPath)) {
|
|
44
|
+
try {
|
|
45
|
+
rmSync(tokenPath);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.log(chalk.yellow(` ⚠ could not clear stale device token: ${err.message}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (existsSync(configPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
54
|
+
delete cfg.device_id;
|
|
55
|
+
delete cfg.device_token;
|
|
56
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.log(chalk.yellow(` ⚠ could not clear stale enrollment config: ${err.message}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
26
63
|
/** Run an external command and detach unless foreground is requested. */
|
|
27
64
|
function runDetached(cmd, args, env) {
|
|
28
65
|
const proc = spawn(cmd, args, {
|
|
@@ -193,14 +230,21 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
193
230
|
const robotArgv = await infinicodeArgv(args);
|
|
194
231
|
console.log(chalk.dim(' starting robot satellite node…'));
|
|
195
232
|
runDetached(robotArgv[0], robotArgv.slice(1));
|
|
233
|
+
// NOTE: there is deliberately no separate `robopark enroll` spawn here.
|
|
234
|
+
// preview_agent.py's own startup (PreviewAgent.start(), see
|
|
235
|
+
// packages/robopark/scheduler/preview_agent.py) already performs
|
|
236
|
+
// enrollment itself whenever it has an --enrollment-token and no
|
|
237
|
+
// device_token yet. Spawning `robopark enroll` as well used to launch a
|
|
238
|
+
// SECOND full preview_agent.py process (enroll.ts runs the whole script,
|
|
239
|
+
// not a one-shot enroll-and-exit) with a different arg set (no
|
|
240
|
+
// --video-device/--audio-device/vision flags), which raced the "real"
|
|
241
|
+
// preview-agent process below for the same camera/mic and the vision
|
|
242
|
+
// webhook port (5057). --enrollment-token is already forwarded via
|
|
243
|
+
// previewAgentArgs, so one process below covers both enrollment and the
|
|
244
|
+
// long-running agent.
|
|
196
245
|
if (opts.enrollmentToken) {
|
|
197
|
-
console.log(chalk.dim('
|
|
198
|
-
|
|
199
|
-
'--scheduler-url', schedulerUrl,
|
|
200
|
-
'--robot-id', internal.name,
|
|
201
|
-
'--enrollment-token', opts.enrollmentToken,
|
|
202
|
-
'--save-config'];
|
|
203
|
-
runDetached(enrollArgv[0], enrollArgv.slice(1));
|
|
246
|
+
console.log(chalk.dim(' fresh --enrollment-token given — clearing any stale device identity…'));
|
|
247
|
+
resetStaleEnrollment();
|
|
204
248
|
}
|
|
205
249
|
console.log(chalk.dim(' starting preview agent…'));
|
|
206
250
|
const previewArgv = [process.execPath, process.argv[1], ...previewAgentArgs];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark / infinicode — stop everything on this machine, including whatever
|
|
3
|
+
* would respawn it.
|
|
4
|
+
*
|
|
5
|
+
* `--supervised` nodes (systemd/Task Scheduler/launchd, `Restart=always`) come
|
|
6
|
+
* back on their own after a plain kill — that's the class of bug that leaves a
|
|
7
|
+
* stale hub process answering on a port after someone thought they killed it.
|
|
8
|
+
* This kills every matching PID directly (never a blanket `killall node`) AND
|
|
9
|
+
* disables/removes the supervisor unit so it does not restart.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
// Long-running daemons only — never a short-lived CLI invocation like
|
|
13
|
+
// `robopark setup` or `robopark stop` itself, which would otherwise match
|
|
14
|
+
// its own (or an ancestor shell's) command line and self-terminate mid-run.
|
|
15
|
+
const PATTERNS = [
|
|
16
|
+
'infinicode serve',
|
|
17
|
+
'infinicode.js serve',
|
|
18
|
+
'cli.js serve', // source/dev invocation: `node dist/cli.js serve ...`
|
|
19
|
+
'robopark serve',
|
|
20
|
+
'robopark-cli.js serve',
|
|
21
|
+
'preview_agent.py',
|
|
22
|
+
'app_pi_clean.py',
|
|
23
|
+
];
|
|
24
|
+
export async function stopAll() {
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
if (platform === 'win32')
|
|
27
|
+
return stopAllWindows();
|
|
28
|
+
return stopAllUnix();
|
|
29
|
+
}
|
|
30
|
+
function stopAllWindows() {
|
|
31
|
+
const killedPids = [];
|
|
32
|
+
const removedUnits = [];
|
|
33
|
+
const errors = [];
|
|
34
|
+
// Find every process whose command line matches, by PID — not by image
|
|
35
|
+
// name, so we never touch an unrelated node.exe (e.g. an MCP server).
|
|
36
|
+
const ps = spawnSync('powershell.exe', [
|
|
37
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
38
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -match '${PATTERNS.join('|').replace(/'/g, "''")}') } | Select-Object -ExpandProperty ProcessId`,
|
|
39
|
+
], { encoding: 'utf8' });
|
|
40
|
+
const pids = (ps.stdout ?? '').split(/\r?\n/).map(s => s.trim()).filter(Boolean).map(Number).filter(Number.isFinite);
|
|
41
|
+
const self = process.pid;
|
|
42
|
+
for (const pid of pids) {
|
|
43
|
+
if (pid === self)
|
|
44
|
+
continue; // never kill the process running this command
|
|
45
|
+
const kill = spawnSync('taskkill', ['/PID', String(pid), '/F', '/T'], { encoding: 'utf8' });
|
|
46
|
+
if (kill.status === 0)
|
|
47
|
+
killedPids.push(pid);
|
|
48
|
+
else
|
|
49
|
+
errors.push(`taskkill ${pid}: ${(kill.stderr || kill.stdout || '').trim()}`);
|
|
50
|
+
}
|
|
51
|
+
// Remove any Scheduled Tasks registered by `robopark setup --auto-start`
|
|
52
|
+
// (auto-start.ts names them "RoboPark-<role>-<name>") so they don't relaunch
|
|
53
|
+
// on next login/boot.
|
|
54
|
+
const query = spawnSync('schtasks', ['/Query', '/FO', 'CSV', '/NH'], { encoding: 'utf8' });
|
|
55
|
+
const taskNames = (query.stdout ?? '')
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.map(line => line.split(',')[0]?.replace(/^"|"$/g, ''))
|
|
58
|
+
.filter((n) => !!n && n.startsWith('\\RoboPark-'));
|
|
59
|
+
for (const name of taskNames) {
|
|
60
|
+
const del = spawnSync('schtasks', ['/Delete', '/TN', name, '/F'], { encoding: 'utf8' });
|
|
61
|
+
if (del.status === 0)
|
|
62
|
+
removedUnits.push(name);
|
|
63
|
+
else
|
|
64
|
+
errors.push(`schtasks /Delete ${name}: ${(del.stderr || del.stdout || '').trim()}`);
|
|
65
|
+
}
|
|
66
|
+
return { killedPids, removedUnits, errors };
|
|
67
|
+
}
|
|
68
|
+
function stopAllUnix() {
|
|
69
|
+
const killedPids = [];
|
|
70
|
+
const removedUnits = [];
|
|
71
|
+
const errors = [];
|
|
72
|
+
const self = process.pid;
|
|
73
|
+
const ps = spawnSync('ps', ['ax', '-o', 'pid=,command='], { encoding: 'utf8' });
|
|
74
|
+
const lines = (ps.stdout ?? '').split('\n');
|
|
75
|
+
const re = new RegExp(PATTERNS.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'));
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const m = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
78
|
+
if (!m)
|
|
79
|
+
continue;
|
|
80
|
+
const pid = Number(m[1]);
|
|
81
|
+
const cmd = m[2];
|
|
82
|
+
if (pid === self || !re.test(cmd))
|
|
83
|
+
continue;
|
|
84
|
+
const kill = spawnSync('kill', ['-9', String(pid)], { encoding: 'utf8' });
|
|
85
|
+
if (kill.status === 0)
|
|
86
|
+
killedPids.push(pid);
|
|
87
|
+
else
|
|
88
|
+
errors.push(`kill -9 ${pid}: ${(kill.stderr || '').trim()}`);
|
|
89
|
+
}
|
|
90
|
+
// Also free the well-known RoboPark ports directly — belt and suspenders
|
|
91
|
+
// against a supervised process whose command line didn't match a pattern.
|
|
92
|
+
for (const port of [47913, 47921, 47922, 8080, 5000, 5057]) {
|
|
93
|
+
const lsof = spawnSync('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
|
|
94
|
+
const pids = (lsof.stdout ?? '').split('\n').map(s => s.trim()).filter(Boolean).map(Number);
|
|
95
|
+
for (const pid of pids) {
|
|
96
|
+
if (pid === self || killedPids.includes(pid))
|
|
97
|
+
continue;
|
|
98
|
+
const kill = spawnSync('kill', ['-9', String(pid)], { encoding: 'utf8' });
|
|
99
|
+
if (kill.status === 0)
|
|
100
|
+
killedPids.push(pid);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (process.platform === 'linux') {
|
|
104
|
+
// Broad match, not just "robopark-*" — known unit names in the wild
|
|
105
|
+
// include a bare "infinicode.service" (see pi-client install scripts),
|
|
106
|
+
// which a narrower glob would silently leave running (and respawning
|
|
107
|
+
// whatever we just killed, if it has Restart=always).
|
|
108
|
+
const list = spawnSync('systemctl', ['list-units', '--all', '--plain', '--no-legend'], { encoding: 'utf8' });
|
|
109
|
+
const units = (list.stdout ?? '')
|
|
110
|
+
.split('\n')
|
|
111
|
+
.map(l => l.trim().split(/\s+/)[0])
|
|
112
|
+
.filter((u) => !!u && u.endsWith('.service') && /infinicode|robopark/i.test(u));
|
|
113
|
+
for (const unit of units) {
|
|
114
|
+
spawnSync('systemctl', ['stop', unit], { encoding: 'utf8' });
|
|
115
|
+
const disable = spawnSync('systemctl', ['disable', unit], { encoding: 'utf8' });
|
|
116
|
+
if (disable.status === 0)
|
|
117
|
+
removedUnits.push(unit);
|
|
118
|
+
else
|
|
119
|
+
errors.push(`systemctl disable ${unit}: ${(disable.stderr || '').trim()}`);
|
|
120
|
+
}
|
|
121
|
+
if (units.length)
|
|
122
|
+
spawnSync('systemctl', ['daemon-reload'], { encoding: 'utf8' });
|
|
123
|
+
}
|
|
124
|
+
return { killedPids, removedUnits, errors };
|
|
125
|
+
}
|
package/dist/robopark-cli.js
CHANGED
|
@@ -15,6 +15,11 @@ import { attachRoboparkSubcommands as attachLegacy } from './commands/robopark.j
|
|
|
15
15
|
import { roboparkScan } from './robopark/scan.js';
|
|
16
16
|
import { roboparkServe } from './robopark/serve.js';
|
|
17
17
|
import { roboparkSetup } from './robopark/setup.js';
|
|
18
|
+
import { stopAll } from './robopark/stop-all.js';
|
|
19
|
+
import { roboparkDoctor } from './robopark/doctor.js';
|
|
20
|
+
import { roboparkAddRobot } from './robopark/add-robot.js';
|
|
21
|
+
import { saveHubProfile, clearHubProfile } from './robopark/profile.js';
|
|
22
|
+
import { roboparkAgentUp, roboparkAgentDown, roboparkAgentLogs } from './robopark/agent-ctl.js';
|
|
18
23
|
// Read the real version from package.json so `--version` never drifts from
|
|
19
24
|
// what is actually installed (this was a hardcoded constant that went stale
|
|
20
25
|
// across several releases — see the identical fix + comment in cli.ts).
|
|
@@ -135,6 +140,120 @@ program
|
|
|
135
140
|
const { roboparkVisionAgent } = await import('./robopark/vision-agent-launcher.js');
|
|
136
141
|
await roboparkVisionAgent(opts);
|
|
137
142
|
});
|
|
143
|
+
program
|
|
144
|
+
.command('stop')
|
|
145
|
+
.description('Stop every infinicode/robopark process on this device AND remove any supervisor (systemd/Task Scheduler/launchd) that would respawn it')
|
|
146
|
+
.action(async () => {
|
|
147
|
+
console.log(chalk.bold('\n robopark stop\n ' + '─'.repeat(48)));
|
|
148
|
+
try {
|
|
149
|
+
const result = await stopAll();
|
|
150
|
+
if (result.killedPids.length)
|
|
151
|
+
console.log(chalk.green(` ✓ killed ${result.killedPids.length} process(es): ${result.killedPids.join(', ')}`));
|
|
152
|
+
else
|
|
153
|
+
console.log(chalk.dim(' no matching processes were running'));
|
|
154
|
+
if (result.removedUnits.length)
|
|
155
|
+
console.log(chalk.green(` ✓ removed ${result.removedUnits.length} supervisor unit(s): ${result.removedUnits.join(', ')}`));
|
|
156
|
+
else
|
|
157
|
+
console.log(chalk.dim(' no supervisor units registered'));
|
|
158
|
+
for (const err of result.errors)
|
|
159
|
+
console.log(chalk.yellow(` ! ${err}`));
|
|
160
|
+
console.log('');
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
console.error('stop failed:', e instanceof Error ? (e.stack ?? e.message) : e);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
program
|
|
168
|
+
.command('doctor')
|
|
169
|
+
.description('Run a full fleet health check: hub, scheduler, LiveKit servers, device heartbeats, production mode')
|
|
170
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
171
|
+
.option('--token <token>', 'mesh auth token')
|
|
172
|
+
.option('--scheduler-url <url>', 'scheduler base URL, e.g. http://100.64.1.2:8080')
|
|
173
|
+
.action(async (opts) => {
|
|
174
|
+
await roboparkDoctor(opts);
|
|
175
|
+
});
|
|
176
|
+
program
|
|
177
|
+
.command('server-add')
|
|
178
|
+
.description('Register a LiveKit server with the RoboPark scheduler')
|
|
179
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
180
|
+
.option('--scheduler-url <url>', 'scheduler URL, e.g. http://100.64.1.2:8080 (derived from --hub-url if omitted)')
|
|
181
|
+
.option('--token <token>', 'mesh auth token (optional)')
|
|
182
|
+
.requiredOption('--id <id>', 'unique server id, e.g. livekit-hub-docker')
|
|
183
|
+
.option('--name <name>', 'display name (defaults to --id)')
|
|
184
|
+
.requiredOption('--url <url>', 'LiveKit ws:// URL, e.g. ws://100.64.1.2:7880')
|
|
185
|
+
.option('--webhook-url <url>', 'internal http URL for health/metrics (defaults to --url with ws->http)')
|
|
186
|
+
.option('--api-key <key>', "LiveKit API key (defaults to 'devkey')")
|
|
187
|
+
.option('--api-secret <secret>', "LiveKit API secret (defaults to 'secret')")
|
|
188
|
+
.option('--max-sessions <n>', 'max concurrent sessions', '8')
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
const { roboparkServerAdd } = await import('./robopark/server-add.js');
|
|
191
|
+
await roboparkServerAdd(opts);
|
|
192
|
+
});
|
|
193
|
+
program
|
|
194
|
+
.command('llm-set')
|
|
195
|
+
.description('Upsert LLM_PROVIDER/OLLAMA_* keys into a ROBOVOICE .env file')
|
|
196
|
+
.requiredOption('--env-path <path>', 'path to the target .env file (e.g. a ROBOVOICE checkout)')
|
|
197
|
+
.requiredOption('--provider <name>', 'ollama | ollama-cloud | groq')
|
|
198
|
+
.option('--model <name>', 'model name (ollama providers only)')
|
|
199
|
+
.option('--host <url>', 'Ollama server URL (ollama providers only)')
|
|
200
|
+
.option('--api-key <key>', 'API key for cloud auth (ollama-cloud; currently unused by ROBOVOICE — see CLI warning)')
|
|
201
|
+
.action(async (opts) => {
|
|
202
|
+
const { roboparkLlmSet } = await import('./robopark/llm-set.js');
|
|
203
|
+
await roboparkLlmSet(opts);
|
|
204
|
+
});
|
|
205
|
+
program
|
|
206
|
+
.command('hub-use')
|
|
207
|
+
.description('Save hub/scheduler/token/site as defaults so other commands stop needing them repeated')
|
|
208
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
209
|
+
.option('--token <token>', 'shared mesh token')
|
|
210
|
+
.option('--scheduler-url <url>', 'scheduler base URL, e.g. http://100.64.1.2:8080')
|
|
211
|
+
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
212
|
+
.option('--clear', 'wipe the saved hub profile')
|
|
213
|
+
.action(async (opts) => {
|
|
214
|
+
if (opts.clear) {
|
|
215
|
+
clearHubProfile();
|
|
216
|
+
console.log(chalk.green(' ✓ cleared saved hub profile'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
saveHubProfile({ hubUrl: opts.hubUrl, token: opts.token, schedulerUrl: opts.schedulerUrl, site: opts.site });
|
|
220
|
+
console.log(chalk.green(' ✓ saved hub profile to ~/.robopark/hub-profile.json'));
|
|
221
|
+
});
|
|
222
|
+
program
|
|
223
|
+
.command('add-robot')
|
|
224
|
+
.description('Mint a scheduler enrollment token and print (or run) the robot setup one-liner')
|
|
225
|
+
.requiredOption('--name <name>', 'robot name, e.g. robobmw')
|
|
226
|
+
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
227
|
+
.option('--hub-url <url>', 'hub federation URL (defaults to saved hub profile / auto-discovery)')
|
|
228
|
+
.option('--token <token>', 'shared mesh token (defaults to saved hub profile / auto-discovery)')
|
|
229
|
+
.option('--scheduler-url <url>', 'scheduler base URL (defaults to saved hub profile / auto-discovery)')
|
|
230
|
+
.option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
|
|
231
|
+
.action(async (opts) => {
|
|
232
|
+
await roboparkAddRobot(config, opts);
|
|
233
|
+
});
|
|
234
|
+
program
|
|
235
|
+
.command('agent-up')
|
|
236
|
+
.description('Start the ROBOVOICE docker stack and register its LiveKit server with the scheduler')
|
|
237
|
+
.requiredOption('--path <dir>', 'path to the ROBOVOICE checkout (contains docker-compose.yaml)')
|
|
238
|
+
.option('--scheduler-url <url>', 'scheduler URL (defaults to resolved hub context)')
|
|
239
|
+
.option('--hub-url <url>', 'hub URL (defaults to resolved hub context)')
|
|
240
|
+
.option('--token <token>', 'mesh token')
|
|
241
|
+
.option('--server-id <id>', "server id to register (default: 'robovoice-<hostname>')")
|
|
242
|
+
.option('--server-name <name>', 'server display name')
|
|
243
|
+
.option('--no-register', 'bring up docker only; skip scheduler registration')
|
|
244
|
+
.action(roboparkAgentUp);
|
|
245
|
+
program
|
|
246
|
+
.command('agent-down')
|
|
247
|
+
.description('Stop the ROBOVOICE docker stack')
|
|
248
|
+
.requiredOption('--path <dir>', 'path to the ROBOVOICE checkout')
|
|
249
|
+
.action(roboparkAgentDown);
|
|
250
|
+
program
|
|
251
|
+
.command('agent-logs')
|
|
252
|
+
.description('Tail ROBOVOICE docker compose logs')
|
|
253
|
+
.requiredOption('--path <dir>', 'path to the ROBOVOICE checkout')
|
|
254
|
+
.option('-f, --follow', 'follow log output')
|
|
255
|
+
.option('--service <name>', 'scope logs to one compose service (e.g. agent, livekit)')
|
|
256
|
+
.action(roboparkAgentLogs);
|
|
138
257
|
program
|
|
139
258
|
.command('setup')
|
|
140
259
|
.description('Set up this device in one pass')
|
package/package.json
CHANGED