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.
@@ -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 port = env.data?.port;
581
- const ip = normalizeIp(remote);
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.connect(url)
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.connect(url)
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;
@@ -352,6 +352,9 @@ export class MeshClient {
352
352
  constructor(opts = {}) {
353
353
  this.opts = opts;
354
354
  }
355
+ get token() {
356
+ return this.opts.token;
357
+ }
355
358
  headers() {
356
359
  const h = { 'content-type': 'application/json' };
357
360
  if (this.opts.token)
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')}</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')}</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');
@@ -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.26",
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/chosen-to-build/infinicode"
70
+ "url": "https://github.com/MrYungCEO/infinicode"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=20"