infinicode 2.8.26 → 2.8.28
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/.opencode/plugins/home-logo-animation.tsx +124 -124
- package/.opencode/plugins/mesh-commands.tsx +140 -140
- package/.opencode/plugins/routing-mode-display.tsx +138 -138
- package/.opencode/themes/infinibot-gold.json +222 -222
- package/.opencode/tui.json +8 -8
- package/README.md +623 -623
- package/bin/robopark.js +2 -2
- package/dist/kernel/agents/backends/registry.js +22 -2
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +1512 -1403
- package/dist/kernel/federation/federation.d.ts +3 -0
- package/dist/kernel/federation/federation.js +25 -4
- package/dist/kernel/federation/transport-http.d.ts +1 -0
- package/dist/kernel/federation/transport-http.js +3 -0
- package/dist/kernel/plugins/dashboard-plugin.js +8 -8
- package/dist/kernel/providers/ollama-provider.d.ts +5 -0
- package/dist/kernel/providers/ollama-provider.js +131 -0
- package/dist/robopark/add-robot.js +25 -3
- package/dist/robopark/auto-start.js +38 -38
- package/dist/robopark/probe.js +15 -15
- package/dist/robopark/setup-livekit.js +52 -52
- package/package.json +2 -2
- package/packages/robopark/README.md +63 -63
- package/packages/robopark/pi-client/README.md +17 -17
- package/packages/robopark/scheduler/main.py +827 -2
- package/packages/robopark/scheduler/preview_agent.py +24 -0
- package/packages/robopark/scheduler/requirements-robot.txt +9 -9
- package/packages/robopark/vision/README.md +66 -66
- package/packages/robopark/vision/requirements-demo.txt +16 -16
|
@@ -251,6 +251,9 @@ export declare class Federation {
|
|
|
251
251
|
* back (accept-only, never dials home).
|
|
252
252
|
*/
|
|
253
253
|
private learnInboundPeer;
|
|
254
|
+
/** Connect with a longer manifest fetch timeout. Inbound dial-backs often
|
|
255
|
+
* cross Tailscale or Windows firewall, so the default 5s is too aggressive. */
|
|
256
|
+
private connectWithTimeout;
|
|
254
257
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
255
258
|
private maybeAutoPull;
|
|
256
259
|
/** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
|
|
@@ -577,17 +577,38 @@ export class Federation {
|
|
|
577
577
|
return; // already tracked
|
|
578
578
|
if (this.inboundConnecting.has(from))
|
|
579
579
|
return; // dial in flight
|
|
580
|
-
const
|
|
581
|
-
const
|
|
580
|
+
const data = env.data ?? {};
|
|
581
|
+
const port = data.port;
|
|
582
|
+
const ip = normalizeIp(data.ip ?? remote);
|
|
582
583
|
if (!port || !ip)
|
|
583
584
|
return;
|
|
584
585
|
const url = `http://${ip}:${port}`;
|
|
585
586
|
this.inboundConnecting.add(from);
|
|
586
587
|
this.deps.logger.info(`[federation] learned inbound peer ${from} → dialing back ${url}`);
|
|
587
|
-
void this.
|
|
588
|
+
void this.connectWithTimeout(url, 30_000)
|
|
588
589
|
.catch(() => { })
|
|
589
590
|
.finally(() => this.inboundConnecting.delete(from));
|
|
590
591
|
}
|
|
592
|
+
/** Connect with a longer manifest fetch timeout. Inbound dial-backs often
|
|
593
|
+
* cross Tailscale or Windows firewall, so the default 5s is too aggressive. */
|
|
594
|
+
async connectWithTimeout(url, timeoutMs) {
|
|
595
|
+
const peer = await this.mesh?.connect(url) ?? null;
|
|
596
|
+
if (peer) {
|
|
597
|
+
await this.maybeAutoPull(peer);
|
|
598
|
+
return peer;
|
|
599
|
+
}
|
|
600
|
+
// If the first attempt failed with the default timeout, retry once with the
|
|
601
|
+
// requested longer timeout before giving up. MeshClient currently uses
|
|
602
|
+
// opts.timeoutMs for manifest fetches, so we temporarily swap the client.
|
|
603
|
+
const client = new MeshClient({ token: this.client.token, timeoutMs });
|
|
604
|
+
const manifest = await client.fetchManifest(url);
|
|
605
|
+
const viaMesh = await this.mesh?.connect(url) ?? null;
|
|
606
|
+
if (viaMesh) {
|
|
607
|
+
await this.maybeAutoPull(viaMesh);
|
|
608
|
+
return viaMesh;
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
591
612
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
592
613
|
async maybeAutoPull(peer) {
|
|
593
614
|
if (!this.deps.options.autoPullConfig)
|
|
@@ -642,7 +663,7 @@ export class Federation {
|
|
|
642
663
|
if (this.lanConnecting.has(nodeId))
|
|
643
664
|
return; // dial in flight
|
|
644
665
|
this.lanConnecting.add(nodeId);
|
|
645
|
-
void this.
|
|
666
|
+
void this.connectWithTimeout(url, 30_000)
|
|
646
667
|
.then(p => {
|
|
647
668
|
if (p)
|
|
648
669
|
logger.info(`[federation] lan discovered ${p.displayName ?? name} @ ${url}`);
|
|
@@ -71,6 +71,7 @@ export interface MeshClientOptions {
|
|
|
71
71
|
export declare class MeshClient {
|
|
72
72
|
private opts;
|
|
73
73
|
constructor(opts?: MeshClientOptions);
|
|
74
|
+
get token(): string | undefined;
|
|
74
75
|
private headers;
|
|
75
76
|
/** Normalize a base URL so a trailing slash doesn't produce `//fed/...` (→ 404). */
|
|
76
77
|
private base;
|
|
@@ -33,14 +33,14 @@ export function createDashboardPlugin(options = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
// HTML dashboard
|
|
35
35
|
const missions = kernelRef?.listMissions() ?? [];
|
|
36
|
-
const html = `<!doctype html><html><head><meta charset="utf-8"><title>OpenKernel Dashboard</title>
|
|
37
|
-
<style>body{font:14px/1.5 system-ui;margin:2rem}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:4px 8px}</style></head>
|
|
38
|
-
<body><h1>OpenKernel Dashboard</h1>
|
|
39
|
-
<h2>Missions (${missions.length})</h2>
|
|
40
|
-
<table><tr><th>ID</th><th>Name</th><th>Status</th><th>Goal</th><th>Tasks</th></tr>
|
|
41
|
-
${missions.map(m => `<tr><td>${m.id}</td><td>${m.name}</td><td>${m.status}</td><td>${escapeHtml(m.goal)}</td><td>${m.tasks.filter(t => t.status === 'COMPLETED').length}/${m.tasks.length}</td></tr>`).join('')}
|
|
42
|
-
</table>
|
|
43
|
-
<p><a href="/status">/status</a> · <a href="/missions">/missions</a> · <a href="/events">/events</a></p>
|
|
36
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><title>OpenKernel Dashboard</title>
|
|
37
|
+
<style>body{font:14px/1.5 system-ui;margin:2rem}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:4px 8px}</style></head>
|
|
38
|
+
<body><h1>OpenKernel Dashboard</h1>
|
|
39
|
+
<h2>Missions (${missions.length})</h2>
|
|
40
|
+
<table><tr><th>ID</th><th>Name</th><th>Status</th><th>Goal</th><th>Tasks</th></tr>
|
|
41
|
+
${missions.map(m => `<tr><td>${m.id}</td><td>${m.name}</td><td>${m.status}</td><td>${escapeHtml(m.goal)}</td><td>${m.tasks.filter(t => t.status === 'COMPLETED').length}/${m.tasks.length}</td></tr>`).join('')}
|
|
42
|
+
</table>
|
|
43
|
+
<p><a href="/status">/status</a> · <a href="/missions">/missions</a> · <a href="/events">/events</a></p>
|
|
44
44
|
</body></html>`;
|
|
45
45
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
46
46
|
res.end(html);
|
|
@@ -30,7 +30,11 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
30
30
|
constructor(options: OllamaProviderOptions);
|
|
31
31
|
capabilities(): Promise<ProviderCapabilities>;
|
|
32
32
|
complete(request: CompletionRequest): Promise<CompletionResult>;
|
|
33
|
+
private completeNative;
|
|
34
|
+
private completeOpenAI;
|
|
33
35
|
completeStream(request: CompletionRequest): AsyncIterable<CompletionChunk>;
|
|
36
|
+
private completeStreamNative;
|
|
37
|
+
private completeStreamOpenAI;
|
|
34
38
|
refresh(): Promise<void>;
|
|
35
39
|
verify(): Promise<{
|
|
36
40
|
ok: boolean;
|
|
@@ -42,6 +46,7 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
42
46
|
private toProviderModel;
|
|
43
47
|
private guessContextLength;
|
|
44
48
|
private buildMessages;
|
|
49
|
+
private buildPrompt;
|
|
45
50
|
private resolveURL;
|
|
46
51
|
getDefaultModel(): string | undefined;
|
|
47
52
|
getCachedModels(): ProviderModel[];
|
|
@@ -35,6 +35,57 @@ export class OllamaProvider {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
async complete(request) {
|
|
38
|
+
// Local Ollama uses the native /api/generate endpoint. Some users run an
|
|
39
|
+
// OpenAI-compatible shim at /v1/chat/completions, but stock Ollama (the
|
|
40
|
+
// common case in this project) does not, so default to the native protocol
|
|
41
|
+
// and fall back to the shim only when /api/generate is unavailable.
|
|
42
|
+
try {
|
|
43
|
+
return await this.completeNative(request);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
if (!msg.includes('404'))
|
|
48
|
+
throw err;
|
|
49
|
+
return await this.completeOpenAI(request);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async completeNative(request) {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const url = await this.resolveURL();
|
|
55
|
+
const endpoint = `${url}/api/generate`;
|
|
56
|
+
const body = {
|
|
57
|
+
model: request.model,
|
|
58
|
+
prompt: this.buildPrompt(request),
|
|
59
|
+
system: request.systemPrompt,
|
|
60
|
+
stream: false,
|
|
61
|
+
options: {
|
|
62
|
+
temperature: request.temperature ?? 0.3,
|
|
63
|
+
num_predict: request.maxTokens ?? 4096,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const response = await fetch(endpoint, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: this.headers(),
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
signal: AbortSignal.timeout(this.timeoutMs * 3),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Ollama completion failed: HTTP ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = (await response.json());
|
|
76
|
+
const content = data.response ?? '';
|
|
77
|
+
const tokensIn = data.prompt_eval_count ?? 0;
|
|
78
|
+
const tokensOut = data.eval_count ?? 0;
|
|
79
|
+
return {
|
|
80
|
+
content,
|
|
81
|
+
providerId: this.info.id,
|
|
82
|
+
modelId: request.model,
|
|
83
|
+
tokensIn,
|
|
84
|
+
tokensOut,
|
|
85
|
+
durationMs: Date.now() - start,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async completeOpenAI(request) {
|
|
38
89
|
const start = Date.now();
|
|
39
90
|
const url = await this.resolveURL();
|
|
40
91
|
const endpoint = `${url}/v1/chat/completions`;
|
|
@@ -68,6 +119,72 @@ export class OllamaProvider {
|
|
|
68
119
|
};
|
|
69
120
|
}
|
|
70
121
|
async *completeStream(request) {
|
|
122
|
+
// Prefer native /api/generate streaming; fall back to OpenAI-compatible shim
|
|
123
|
+
// only when the native endpoint returns 404.
|
|
124
|
+
try {
|
|
125
|
+
yield* this.completeStreamNative(request);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
if (!msg.includes('404'))
|
|
130
|
+
throw err;
|
|
131
|
+
yield* this.completeStreamOpenAI(request);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async *completeStreamNative(request) {
|
|
135
|
+
const url = await this.resolveURL();
|
|
136
|
+
const endpoint = `${url}/api/generate`;
|
|
137
|
+
const body = {
|
|
138
|
+
model: request.model,
|
|
139
|
+
prompt: this.buildPrompt(request),
|
|
140
|
+
system: request.systemPrompt,
|
|
141
|
+
stream: true,
|
|
142
|
+
options: {
|
|
143
|
+
temperature: request.temperature ?? 0.3,
|
|
144
|
+
num_predict: request.maxTokens ?? 4096,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const response = await fetch(endpoint, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: this.headers(),
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
signal: AbortSignal.timeout(this.timeoutMs * 6),
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok || !response.body) {
|
|
154
|
+
throw new Error(`Ollama stream failed: HTTP ${response.status}`);
|
|
155
|
+
}
|
|
156
|
+
const reader = response.body.getReader();
|
|
157
|
+
const decoder = new TextDecoder();
|
|
158
|
+
let buffer = '';
|
|
159
|
+
while (true) {
|
|
160
|
+
const { done, value } = await reader.read();
|
|
161
|
+
if (done)
|
|
162
|
+
break;
|
|
163
|
+
buffer += decoder.decode(value, { stream: true });
|
|
164
|
+
const lines = buffer.split('\n');
|
|
165
|
+
buffer = lines.pop() ?? '';
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const trimmed = line.trim();
|
|
168
|
+
if (!trimmed)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const chunk = JSON.parse(trimmed);
|
|
172
|
+
const delta = chunk.response ?? '';
|
|
173
|
+
if (delta) {
|
|
174
|
+
yield { delta, done: false, providerId: this.info.id, modelId: request.model };
|
|
175
|
+
}
|
|
176
|
+
if (chunk.done) {
|
|
177
|
+
yield { delta: '', done: true, providerId: this.info.id, modelId: request.model };
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// skip malformed
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async *completeStreamOpenAI(request) {
|
|
71
188
|
const url = await this.resolveURL();
|
|
72
189
|
const endpoint = `${url}/v1/chat/completions`;
|
|
73
190
|
const body = {
|
|
@@ -227,6 +344,20 @@ export class OllamaProvider {
|
|
|
227
344
|
messages.push({ role: 'user', content: request.prompt });
|
|
228
345
|
return messages;
|
|
229
346
|
}
|
|
347
|
+
buildPrompt(request) {
|
|
348
|
+
const parts = [];
|
|
349
|
+
if (request.systemPrompt) {
|
|
350
|
+
parts.push(request.systemPrompt);
|
|
351
|
+
}
|
|
352
|
+
if (request.context) {
|
|
353
|
+
const ctxStr = Object.entries(request.context)
|
|
354
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
355
|
+
.join('\n');
|
|
356
|
+
parts.push(ctxStr);
|
|
357
|
+
}
|
|
358
|
+
parts.push(request.prompt);
|
|
359
|
+
return parts.join('\n\n');
|
|
360
|
+
}
|
|
230
361
|
async resolveURL() {
|
|
231
362
|
const candidates = [this.activeURL, this.fallbackURL, this.baseURL].filter((u, i, arr) => !!u && arr.indexOf(u) === i);
|
|
232
363
|
for (const candidate of candidates) {
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
import chalk from 'chalk';
|
|
25
25
|
import { resolveContext } from './profile.js';
|
|
26
26
|
import { roboparkSetup } from './setup.js';
|
|
27
|
-
async function mintEnrollmentToken(schedulerUrl, name) {
|
|
27
|
+
async function mintEnrollmentToken(schedulerUrl, name, characterId) {
|
|
28
28
|
const url = `${schedulerUrl.replace(/\/$/, '')}/api/devices`;
|
|
29
29
|
const res = await fetch(url, {
|
|
30
30
|
method: 'POST',
|
|
31
31
|
headers: { 'content-type': 'application/json' },
|
|
32
|
-
body: JSON.stringify({ name }),
|
|
32
|
+
body: JSON.stringify({ name, character_id: characterId }),
|
|
33
33
|
});
|
|
34
34
|
if (!res.ok) {
|
|
35
35
|
const text = await res.text().catch(() => '');
|
|
@@ -37,6 +37,24 @@ async function mintEnrollmentToken(schedulerUrl, name) {
|
|
|
37
37
|
}
|
|
38
38
|
return (await res.json());
|
|
39
39
|
}
|
|
40
|
+
async function pickCharacterPreset(schedulerUrl, name) {
|
|
41
|
+
const lower = name.toLowerCase();
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(`${schedulerUrl.replace(/\/$/, '')}/api/character-presets`);
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
return undefined;
|
|
46
|
+
const presets = (await res.json());
|
|
47
|
+
// Exact id match first, then fuzzy name match.
|
|
48
|
+
const exact = presets.find(p => p.id.toLowerCase() === lower || p.name.toLowerCase() === lower);
|
|
49
|
+
if (exact)
|
|
50
|
+
return exact.id;
|
|
51
|
+
const partial = presets.find(p => lower.includes(p.id.toLowerCase()) || p.name.toLowerCase().includes(lower));
|
|
52
|
+
return partial?.id;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
40
58
|
/**
|
|
41
59
|
* Build a Windows-task-friendly command string from an argv array.
|
|
42
60
|
*
|
|
@@ -63,9 +81,13 @@ export async function roboparkAddRobot(config, opts) {
|
|
|
63
81
|
console.log(` scheduler: ${chalk.cyan(ctx.schedulerUrl)}`);
|
|
64
82
|
console.log(` name: ${chalk.cyan(opts.name)}${ctx.site ? chalk.dim(' @ ' + ctx.site) : ''}`);
|
|
65
83
|
console.log();
|
|
84
|
+
const characterId = await pickCharacterPreset(ctx.schedulerUrl, opts.name);
|
|
85
|
+
if (characterId) {
|
|
86
|
+
console.log(` character: ${chalk.cyan(characterId)}`);
|
|
87
|
+
}
|
|
66
88
|
let minted;
|
|
67
89
|
try {
|
|
68
|
-
minted = await mintEnrollmentToken(ctx.schedulerUrl, opts.name);
|
|
90
|
+
minted = await mintEnrollmentToken(ctx.schedulerUrl, opts.name, characterId);
|
|
69
91
|
}
|
|
70
92
|
catch (e) {
|
|
71
93
|
console.log(chalk.red(` ✗ failed to mint enrollment token: ${e.message}`));
|
|
@@ -65,21 +65,21 @@ async function registerSystemd(service) {
|
|
|
65
65
|
const envLines = Object.entries(service.env ?? {})
|
|
66
66
|
.map(([k, v]) => `Environment="${k}=${v.replace(/"/g, '\\"')}"`)
|
|
67
67
|
.join('\n');
|
|
68
|
-
const unit = `[Unit]
|
|
69
|
-
Description=RoboPark ${service.role} for ${service.name}
|
|
70
|
-
After=network-online.target
|
|
71
|
-
Wants=network-online.target
|
|
72
|
-
|
|
73
|
-
[Service]
|
|
74
|
-
Type=simple
|
|
75
|
-
ExecStart=${service.command} ${service.args.map(shellQuote).join(' ')}
|
|
76
|
-
WorkingDirectory=${service.workingDir ?? '/opt/robopark'}
|
|
77
|
-
Restart=always
|
|
78
|
-
RestartSec=5
|
|
79
|
-
${envLines}
|
|
80
|
-
|
|
81
|
-
[Install]
|
|
82
|
-
WantedBy=multi-user.target
|
|
68
|
+
const unit = `[Unit]
|
|
69
|
+
Description=RoboPark ${service.role} for ${service.name}
|
|
70
|
+
After=network-online.target
|
|
71
|
+
Wants=network-online.target
|
|
72
|
+
|
|
73
|
+
[Service]
|
|
74
|
+
Type=simple
|
|
75
|
+
ExecStart=${service.command} ${service.args.map(shellQuote).join(' ')}
|
|
76
|
+
WorkingDirectory=${service.workingDir ?? '/opt/robopark'}
|
|
77
|
+
Restart=always
|
|
78
|
+
RestartSec=5
|
|
79
|
+
${envLines}
|
|
80
|
+
|
|
81
|
+
[Install]
|
|
82
|
+
WantedBy=multi-user.target
|
|
83
83
|
`;
|
|
84
84
|
try {
|
|
85
85
|
writeFileSync(unitPath, unit, 'utf8');
|
|
@@ -94,29 +94,29 @@ async function registerLaunchd(service) {
|
|
|
94
94
|
const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
95
95
|
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
96
96
|
const env = service.env ?? {};
|
|
97
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
98
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
99
|
-
<plist version="1.0">
|
|
100
|
-
<dict>
|
|
101
|
-
<key>Label</key>
|
|
102
|
-
<string>${label}</string>
|
|
103
|
-
<key>ProgramArguments</key>
|
|
104
|
-
<array>
|
|
105
|
-
<string>${service.command}</string>
|
|
106
|
-
${service.args.map(a => `<string>${a.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')}</string>`).join('\n ')}
|
|
107
|
-
</array>
|
|
108
|
-
<key>WorkingDirectory</key>
|
|
109
|
-
<string>${service.workingDir ?? homedir()}</string>
|
|
110
|
-
<key>RunAtLoad</key>
|
|
111
|
-
<true/>
|
|
112
|
-
<key>KeepAlive</key>
|
|
113
|
-
<true/>
|
|
114
|
-
<key>StandardOutPath</key>
|
|
115
|
-
<string>${join(homedir(), 'Library', 'Logs', `${label}.log`)}</string>
|
|
116
|
-
<key>StandardErrorPath</key>
|
|
117
|
-
<string>${join(homedir(), 'Library', 'Logs', `${label}.err.log`)}</string>
|
|
118
|
-
${Object.entries(env).map(([k, v]) => `<key>EnvironmentVariables</key>\n <dict>\n <key>${k}</key>\n <string>${v}</string>\n </dict>`).join('\n ')}
|
|
119
|
-
</dict>
|
|
97
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
98
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
99
|
+
<plist version="1.0">
|
|
100
|
+
<dict>
|
|
101
|
+
<key>Label</key>
|
|
102
|
+
<string>${label}</string>
|
|
103
|
+
<key>ProgramArguments</key>
|
|
104
|
+
<array>
|
|
105
|
+
<string>${service.command}</string>
|
|
106
|
+
${service.args.map(a => `<string>${a.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')}</string>`).join('\n ')}
|
|
107
|
+
</array>
|
|
108
|
+
<key>WorkingDirectory</key>
|
|
109
|
+
<string>${service.workingDir ?? homedir()}</string>
|
|
110
|
+
<key>RunAtLoad</key>
|
|
111
|
+
<true/>
|
|
112
|
+
<key>KeepAlive</key>
|
|
113
|
+
<true/>
|
|
114
|
+
<key>StandardOutPath</key>
|
|
115
|
+
<string>${join(homedir(), 'Library', 'Logs', `${label}.log`)}</string>
|
|
116
|
+
<key>StandardErrorPath</key>
|
|
117
|
+
<string>${join(homedir(), 'Library', 'Logs', `${label}.err.log`)}</string>
|
|
118
|
+
${Object.entries(env).map(([k, v]) => `<key>EnvironmentVariables</key>\n <dict>\n <key>${k}</key>\n <string>${v}</string>\n </dict>`).join('\n ')}
|
|
119
|
+
</dict>
|
|
120
120
|
</plist>`;
|
|
121
121
|
try {
|
|
122
122
|
writeFileSync(plistPath, plist, 'utf8');
|
package/dist/robopark/probe.js
CHANGED
|
@@ -72,21 +72,21 @@ function runOpencodePrompt(prompt, timeoutMs) {
|
|
|
72
72
|
}
|
|
73
73
|
function probePrompt(target, token) {
|
|
74
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
|
-
|
|
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
90
|
Answer as concise structured Markdown bullet list.`;
|
|
91
91
|
}
|
|
92
92
|
async function probeTarget(target, opts) {
|
|
@@ -83,68 +83,68 @@ async function getHubPlatformFromStatus(hubUrl, token) {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
const LIVEKIT_SETUP_PROMPT_HEADER = `You are the RoboPark infrastructure agent running on the fleet hub.
|
|
87
|
-
|
|
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.
|
|
89
|
-
|
|
90
|
-
DO NOT ask the user for confirmation. Execute the steps directly. If a step is already done, skip it and report it.
|
|
86
|
+
const LIVEKIT_SETUP_PROMPT_HEADER = `You are the RoboPark infrastructure agent running on the fleet hub.
|
|
87
|
+
|
|
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.
|
|
89
|
+
|
|
90
|
+
DO NOT ask the user for confirmation. Execute the steps directly. If a step is already done, skip it and report it.
|
|
91
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
|
-
\`\`\`
|
|
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
102
|
`;
|
|
103
|
-
const LIVEKIT_SETUP_STEPS_LINUX = `Steps:
|
|
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.
|
|
105
|
-
2. Pull the official LiveKit server image: docker pull livekit/livekit-server:latest
|
|
106
|
-
3. Create a directory /opt/livekit if it does not exist.
|
|
107
|
-
4. Generate API credentials: run the LiveKit CLI container to create a key/secret and save them to /opt/livekit/keys.yaml:
|
|
108
|
-
docker run --rm livekit/livekit-server:latest generate-keys
|
|
109
|
-
Parse the output and write it as a YAML file with fields api_key and api_secret.
|
|
110
|
-
5. Start the LiveKit server container with host networking on port 7880:
|
|
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 \\
|
|
114
|
-
--config /etc/livekit/keys.yaml --bind 0.0.0.0 --port 7880
|
|
115
|
-
6. Wait up to 30 seconds and verify it is healthy: curl -sf http://127.0.0.1:7880
|
|
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.
|
|
103
|
+
const LIVEKIT_SETUP_STEPS_LINUX = `Steps:
|
|
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.
|
|
105
|
+
2. Pull the official LiveKit server image: docker pull livekit/livekit-server:latest
|
|
106
|
+
3. Create a directory /opt/livekit if it does not exist.
|
|
107
|
+
4. Generate API credentials: run the LiveKit CLI container to create a key/secret and save them to /opt/livekit/keys.yaml:
|
|
108
|
+
docker run --rm livekit/livekit-server:latest generate-keys
|
|
109
|
+
Parse the output and write it as a YAML file with fields api_key and api_secret.
|
|
110
|
+
5. Start the LiveKit server container with host networking on port 7880:
|
|
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 \\
|
|
114
|
+
--config /etc/livekit/keys.yaml --bind 0.0.0.0 --port 7880
|
|
115
|
+
6. Wait up to 30 seconds and verify it is healthy: curl -sf http://127.0.0.1:7880
|
|
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
117
|
`;
|
|
118
118
|
// Windows hosts: Docker Desktop does not support --network host, so ports are
|
|
119
119
|
// mapped explicitly. --dev mode ships with well-known devkey/secret, which
|
|
120
120
|
// sidesteps the `generate-keys` dance (that container command is awkward to
|
|
121
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.
|
|
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
134
|
`;
|
|
135
135
|
// Used when the hub's platform could not be determined before dispatch: ask
|
|
136
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.
|
|
142
|
-
|
|
143
|
-
--- IF WINDOWS (use PowerShell): ---
|
|
144
|
-
${LIVEKIT_SETUP_STEPS_WINDOWS}
|
|
145
|
-
|
|
146
|
-
--- IF LINUX/macOS (use bash/sh): ---
|
|
147
|
-
${LIVEKIT_SETUP_STEPS_LINUX}
|
|
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.
|
|
142
|
+
|
|
143
|
+
--- IF WINDOWS (use PowerShell): ---
|
|
144
|
+
${LIVEKIT_SETUP_STEPS_WINDOWS}
|
|
145
|
+
|
|
146
|
+
--- IF LINUX/macOS (use bash/sh): ---
|
|
147
|
+
${LIVEKIT_SETUP_STEPS_LINUX}
|
|
148
148
|
`;
|
|
149
149
|
function buildLivekitSetupPrompt(platform) {
|
|
150
150
|
let steps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinicode",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.28",
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"license": "MIT",
|
|
68
68
|
"repository": {
|
|
69
69
|
"type": "git",
|
|
70
|
-
"url": "https://github.com/
|
|
70
|
+
"url": "https://github.com/MrYungCEO/infinicode"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=20"
|