infinicode 1.0.0 → 2.0.0

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.
Files changed (157) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +120 -0
  2. package/.opencode/plugins/routing-mode-display.tsx +135 -0
  3. package/.opencode/themes/infinibot-gold.json +222 -0
  4. package/.opencode/tui.json +8 -0
  5. package/README.md +527 -75
  6. package/dist/ascii-video-animation.d.ts +2 -0
  7. package/dist/ascii-video-animation.js +243 -0
  8. package/dist/cli.js +195 -50
  9. package/dist/commands/console.d.ts +6 -0
  10. package/dist/commands/console.js +111 -0
  11. package/dist/commands/kernel-setup.d.ts +3 -0
  12. package/dist/commands/kernel-setup.js +303 -0
  13. package/dist/commands/mcp.d.ts +12 -0
  14. package/dist/commands/mcp.js +96 -0
  15. package/dist/commands/mission.d.ts +16 -0
  16. package/dist/commands/mission.js +301 -0
  17. package/dist/commands/models.d.ts +3 -1
  18. package/dist/commands/models.js +111 -55
  19. package/dist/commands/providers.d.ts +6 -0
  20. package/dist/commands/providers.js +95 -0
  21. package/dist/commands/run.d.ts +2 -1
  22. package/dist/commands/run.js +349 -59
  23. package/dist/commands/serve.d.ts +18 -0
  24. package/dist/commands/serve.js +127 -0
  25. package/dist/commands/setup.d.ts +1 -1
  26. package/dist/commands/setup.js +77 -44
  27. package/dist/commands/status.d.ts +2 -1
  28. package/dist/commands/status.js +46 -30
  29. package/dist/commands/workers.d.ts +5 -0
  30. package/dist/commands/workers.js +103 -0
  31. package/dist/kernel/autonomy/goal-loop.d.ts +49 -0
  32. package/dist/kernel/autonomy/goal-loop.js +113 -0
  33. package/dist/kernel/autonomy/index.d.ts +8 -0
  34. package/dist/kernel/autonomy/index.js +7 -0
  35. package/dist/kernel/browser/browser-controller.d.ts +57 -0
  36. package/dist/kernel/browser/browser-controller.js +175 -0
  37. package/dist/kernel/checkpoint-engine.d.ts +17 -0
  38. package/dist/kernel/checkpoint-engine.js +128 -0
  39. package/dist/kernel/command-system.d.ts +37 -0
  40. package/dist/kernel/command-system.js +239 -0
  41. package/dist/kernel/config-schema.d.ts +53 -0
  42. package/dist/kernel/config-schema.js +36 -0
  43. package/dist/kernel/event-bus.d.ts +19 -0
  44. package/dist/kernel/event-bus.js +65 -0
  45. package/dist/kernel/federation/auto-update.d.ts +36 -0
  46. package/dist/kernel/federation/auto-update.js +57 -0
  47. package/dist/kernel/federation/compute-router.d.ts +27 -0
  48. package/dist/kernel/federation/compute-router.js +44 -0
  49. package/dist/kernel/federation/config-sync.d.ts +33 -0
  50. package/dist/kernel/federation/config-sync.js +37 -0
  51. package/dist/kernel/federation/discovery.d.ts +11 -0
  52. package/dist/kernel/federation/discovery.js +44 -0
  53. package/dist/kernel/federation/event-bridge.d.ts +30 -0
  54. package/dist/kernel/federation/event-bridge.js +61 -0
  55. package/dist/kernel/federation/federation-plugin.d.ts +24 -0
  56. package/dist/kernel/federation/federation-plugin.js +35 -0
  57. package/dist/kernel/federation/federation.d.ts +126 -0
  58. package/dist/kernel/federation/federation.js +335 -0
  59. package/dist/kernel/federation/index.d.ts +31 -0
  60. package/dist/kernel/federation/index.js +23 -0
  61. package/dist/kernel/federation/moltfed-adapter.d.ts +30 -0
  62. package/dist/kernel/federation/moltfed-adapter.js +52 -0
  63. package/dist/kernel/federation/moltfed-connector.d.ts +98 -0
  64. package/dist/kernel/federation/moltfed-connector.js +193 -0
  65. package/dist/kernel/federation/node-identity.d.ts +19 -0
  66. package/dist/kernel/federation/node-identity.js +86 -0
  67. package/dist/kernel/federation/peer-mesh.d.ts +46 -0
  68. package/dist/kernel/federation/peer-mesh.js +117 -0
  69. package/dist/kernel/federation/protocol.d.ts +20 -0
  70. package/dist/kernel/federation/protocol.js +61 -0
  71. package/dist/kernel/federation/role-context.d.ts +5 -0
  72. package/dist/kernel/federation/role-context.js +45 -0
  73. package/dist/kernel/federation/telemetry.d.ts +21 -0
  74. package/dist/kernel/federation/telemetry.js +138 -0
  75. package/dist/kernel/federation/transport-http.d.ts +44 -0
  76. package/dist/kernel/federation/transport-http.js +207 -0
  77. package/dist/kernel/federation/types.d.ts +212 -0
  78. package/dist/kernel/federation/types.js +3 -0
  79. package/dist/kernel/free-providers.d.ts +28 -0
  80. package/dist/kernel/free-providers.js +162 -0
  81. package/dist/kernel/frontend-scoring.d.ts +21 -0
  82. package/dist/kernel/frontend-scoring.js +125 -0
  83. package/dist/kernel/index.d.ts +63 -0
  84. package/dist/kernel/index.js +45 -0
  85. package/dist/kernel/kernel.d.ts +75 -0
  86. package/dist/kernel/kernel.js +223 -0
  87. package/dist/kernel/logger.d.ts +18 -0
  88. package/dist/kernel/logger.js +26 -0
  89. package/dist/kernel/mcp/index.d.ts +9 -0
  90. package/dist/kernel/mcp/index.js +8 -0
  91. package/dist/kernel/mcp/mcp-server.d.ts +27 -0
  92. package/dist/kernel/mcp/mcp-server.js +178 -0
  93. package/dist/kernel/mission-engine.d.ts +42 -0
  94. package/dist/kernel/mission-engine.js +160 -0
  95. package/dist/kernel/orchestrator.d.ts +51 -0
  96. package/dist/kernel/orchestrator.js +226 -0
  97. package/dist/kernel/plugin-manager.d.ts +28 -0
  98. package/dist/kernel/plugin-manager.js +76 -0
  99. package/dist/kernel/plugins/browser-plugin.d.ts +22 -0
  100. package/dist/kernel/plugins/browser-plugin.js +34 -0
  101. package/dist/kernel/plugins/dashboard-plugin.d.ts +17 -0
  102. package/dist/kernel/plugins/dashboard-plugin.js +72 -0
  103. package/dist/kernel/plugins/discord-plugin.d.ts +11 -0
  104. package/dist/kernel/plugins/discord-plugin.js +35 -0
  105. package/dist/kernel/plugins/metrics-plugin.d.ts +33 -0
  106. package/dist/kernel/plugins/metrics-plugin.js +86 -0
  107. package/dist/kernel/plugins/search-plugin.d.ts +17 -0
  108. package/dist/kernel/plugins/search-plugin.js +20 -0
  109. package/dist/kernel/plugins/slack-plugin.d.ts +11 -0
  110. package/dist/kernel/plugins/slack-plugin.js +35 -0
  111. package/dist/kernel/plugins/telegram-plugin.d.ts +20 -0
  112. package/dist/kernel/plugins/telegram-plugin.js +122 -0
  113. package/dist/kernel/policies.d.ts +33 -0
  114. package/dist/kernel/policies.js +105 -0
  115. package/dist/kernel/provider-discovery.d.ts +41 -0
  116. package/dist/kernel/provider-discovery.js +179 -0
  117. package/dist/kernel/provider-manager.d.ts +38 -0
  118. package/dist/kernel/provider-manager.js +206 -0
  119. package/dist/kernel/provider-url.d.ts +18 -0
  120. package/dist/kernel/provider-url.js +45 -0
  121. package/dist/kernel/providers/gemini-provider.d.ts +43 -0
  122. package/dist/kernel/providers/gemini-provider.js +203 -0
  123. package/dist/kernel/providers/ollama-provider.d.ts +44 -0
  124. package/dist/kernel/providers/ollama-provider.js +241 -0
  125. package/dist/kernel/providers/openai-compatible-provider.d.ts +43 -0
  126. package/dist/kernel/providers/openai-compatible-provider.js +233 -0
  127. package/dist/kernel/recovery-manager.d.ts +38 -0
  128. package/dist/kernel/recovery-manager.js +156 -0
  129. package/dist/kernel/router.d.ts +44 -0
  130. package/dist/kernel/router.js +222 -0
  131. package/dist/kernel/sample-missions.d.ts +11 -0
  132. package/dist/kernel/sample-missions.js +132 -0
  133. package/dist/kernel/scheduler.d.ts +30 -0
  134. package/dist/kernel/scheduler.js +147 -0
  135. package/dist/kernel/sdk/harness-sdk.d.ts +37 -0
  136. package/dist/kernel/sdk/harness-sdk.js +48 -0
  137. package/dist/kernel/sdk/plugin-sdk.d.ts +27 -0
  138. package/dist/kernel/sdk/plugin-sdk.js +40 -0
  139. package/dist/kernel/search/search-controller.d.ts +32 -0
  140. package/dist/kernel/search/search-controller.js +141 -0
  141. package/dist/kernel/setup.d.ts +13 -0
  142. package/dist/kernel/setup.js +59 -0
  143. package/dist/kernel/types.d.ts +479 -0
  144. package/dist/kernel/types.js +7 -0
  145. package/dist/kernel/verification-engine.d.ts +33 -0
  146. package/dist/kernel/verification-engine.js +287 -0
  147. package/dist/kernel/worker-runtime.d.ts +66 -0
  148. package/dist/kernel/worker-runtime.js +261 -0
  149. package/dist/kernel/workers/browser-worker.d.ts +6 -0
  150. package/dist/kernel/workers/browser-worker.js +92 -0
  151. package/dist/kernel/workers/builtin-workers.d.ts +20 -0
  152. package/dist/kernel/workers/builtin-workers.js +120 -0
  153. package/dist/kernel/workers/research-worker.d.ts +5 -0
  154. package/dist/kernel/workers/research-worker.js +90 -0
  155. package/dist/prompt-ascii-video-animation.d.ts +2 -0
  156. package/dist/prompt-ascii-video-animation.js +243 -0
  157. package/package.json +43 -9
@@ -1,83 +1,373 @@
1
+ /**
2
+ * infinicode run — launch the infinicode TUI with the full kernel provider
3
+ * pool injected.
4
+ *
5
+ * The TUI is infinicode's SolidJS terminal UI (themes, markdown rendering,
6
+ * diff view, plugin slots, the infinicode gold theme + animated logo).
7
+ * infinicode builds the provider config from the saved kernel config
8
+ * (Ollama master + all cloud providers: Groq, OpenRouter, GitHub, NVIDIA,
9
+ * HF, Gemini) and injects it via OPENCODE_CONFIG_CONTENT so the TUI can
10
+ * use every provider — not just a single Ollama master.
11
+ *
12
+ * The kernel's router/recovery/verification do not run inside the TUI;
13
+ * the TUI is the Harness (prompts, workflows, personas). The kernel owns
14
+ * execution. For headless mission execution with full kernel routing,
15
+ * use `infinicode mission run`.
16
+ */
1
17
  import chalk from 'chalk';
2
18
  import ora from 'ora';
3
- export async function runAgent(masterUrl, model, useTui, config) {
4
- // Check connection first
5
- const spinner = ora('Connecting to Ollama...').start();
19
+ import { existsSync } from 'node:fs';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from '../ascii-video-animation.js';
23
+ import { getProviderPreset } from '../kernel/free-providers.js';
24
+ import { buildKernelFromConfig } from '../kernel/setup.js';
25
+ import { SilentLogger } from '../kernel/index.js';
26
+ import { openAICompatBaseURL } from '../kernel/provider-url.js';
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const PROMPT_ANIMATION_MAX_FRAMES = Math.min(ASCII_VIDEO_FRAMES.length, ASCII_VIDEO_FPS * 3);
29
+ function resolveInfinicodeCommand() {
30
+ const rootDir = resolve(__dirname, '..', '..');
31
+ const bundled = join(rootDir, 'bin', 'infinicode-tui.exe');
32
+ return existsSync(bundled) ? bundled : 'infinicode';
33
+ }
34
+ function fitPromptFrameToTerminal(frame) {
35
+ const columns = Math.max(1, (process.stdout.columns ?? 120) - 1);
36
+ const rows = Math.max(1, Math.min((process.stdout.rows ?? 40) - 8, 24));
37
+ const lines = frame.split('\n');
38
+ const maxWidth = Math.max(...lines.map(line => line.length), 0);
39
+ const startColumn = Math.max(0, Math.floor((maxWidth - columns) / 2));
40
+ const startRow = Math.max(0, Math.floor((lines.length - rows) / 2));
41
+ return lines
42
+ .slice(startRow, startRow + rows)
43
+ .map(line => line.slice(startColumn, startColumn + columns).padEnd(columns, ' '))
44
+ .join('\n');
45
+ }
46
+ function colorPromptFrame(frame) {
47
+ return chalk.hex('#fbbf24')(fitPromptFrameToTerminal(frame));
48
+ }
49
+ async function renderPromptAnimation() {
50
+ const firstFrame = ASCII_VIDEO_FRAMES[0] ?? '';
51
+ if (!firstFrame)
52
+ return;
53
+ if (!process.stdout.isTTY || process.env.CI) {
54
+ console.log(colorPromptFrame(firstFrame));
55
+ return;
56
+ }
57
+ const frameDelayMs = Math.round(1000 / ASCII_VIDEO_FPS);
58
+ let renderedRows = 0;
59
+ process.stdout.write('\x1b[?25l\x1b[?7l');
60
+ try {
61
+ for (let index = 0; index < PROMPT_ANIMATION_MAX_FRAMES; index++) {
62
+ const output = colorPromptFrame(ASCII_VIDEO_FRAMES[index] ?? firstFrame);
63
+ const rows = output.split('\n').length;
64
+ if (renderedRows > 0) {
65
+ process.stdout.write(`\x1b[${renderedRows}F`);
66
+ }
67
+ process.stdout.write(`${output}\n`);
68
+ renderedRows = rows;
69
+ await new Promise(resolve => setTimeout(resolve, frameDelayMs));
70
+ }
71
+ }
72
+ finally {
73
+ if (renderedRows > 0) {
74
+ process.stdout.write(`\x1b[${renderedRows}F\x1b[J`);
75
+ }
76
+ process.stdout.write('\x1b[0m\x1b[?7h\x1b[?25h');
77
+ }
78
+ }
79
+ /**
80
+ * Check if Ollama is reachable at the given URL.
81
+ */
82
+ async function isOllamaReachable(url) {
6
83
  try {
7
- const response = await fetch(`${masterUrl}/api/tags`, {
8
- signal: AbortSignal.timeout(5000)
84
+ const response = await fetch(`${url}/api/tags`, {
85
+ signal: AbortSignal.timeout(5000),
9
86
  });
10
- if (!response.ok) {
11
- throw new Error(`HTTP ${response.status}`);
87
+ return response.ok;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Resolve the best reachable Ollama master URL (LAN → Tailscale fallback).
95
+ */
96
+ async function resolveConfiguredMasterUrl(config) {
97
+ const masterUrl = config.get('masterUrl');
98
+ const tailscaleMasterUrl = config.get('tailscaleMasterUrl');
99
+ const candidates = [
100
+ masterUrl,
101
+ ...(tailscaleMasterUrl && tailscaleMasterUrl !== masterUrl ? [tailscaleMasterUrl] : []),
102
+ ];
103
+ for (const candidate of candidates) {
104
+ try {
105
+ const response = await fetch(`${candidate}/api/tags`, {
106
+ signal: AbortSignal.timeout(5000),
107
+ });
108
+ if (response.ok) {
109
+ if (candidate !== masterUrl) {
110
+ console.log(chalk.yellow(`LAN unavailable; using Tailscale master ${candidate}`));
111
+ }
112
+ return candidate;
113
+ }
114
+ }
115
+ catch {
116
+ // try next
117
+ }
118
+ }
119
+ return masterUrl;
120
+ }
121
+ /** Build one opencode model entry, carrying the real context window so the TUI
122
+ * knows each model's limits (helps compaction). */
123
+ function modelEntry(m) {
124
+ const entry = { name: m.name ?? m.id };
125
+ // Only set a limit when we have a real context value (> the 8192 default) —
126
+ // a wrong-small limit would make opencode compact large models too early.
127
+ if (m.contextLength && m.contextLength > 8192) {
128
+ entry.limit = { context: m.contextLength, output: Math.min(16384, Math.max(2048, Math.floor(m.contextLength / 16))) };
129
+ }
130
+ return entry;
131
+ }
132
+ function modelMap(models) {
133
+ return Object.fromEntries(models.map(m => [m.id, modelEntry(m)]));
134
+ }
135
+ function buildProviderConfig(masterUrl, defaultModel, cloudProviders, masterReachable, modelsByProvider = {}) {
136
+ const providers = {};
137
+ // Prefer the provider's full fetched catalog; fall back to the curated preset.
138
+ const catalog = (id, fallback) => {
139
+ const fetched = modelsByProvider[id];
140
+ return fetched && fetched.length ? fetched : fallback;
141
+ };
142
+ // Only inject Ollama if the master is reachable
143
+ if (masterReachable) {
144
+ providers['ollama'] = {
145
+ npm: '@ai-sdk/openai-compatible',
146
+ name: 'Ollama',
147
+ options: {
148
+ baseURL: `${masterUrl}/v1`,
149
+ },
150
+ models: modelMap(catalog('ollama', [{ id: defaultModel, name: defaultModel, contextLength: 8192 }])),
151
+ };
152
+ }
153
+ for (const cfg of cloudProviders) {
154
+ if (!cfg.enabled || !cfg.apiKey)
155
+ continue;
156
+ const preset = getProviderPreset(cfg.id);
157
+ const fallback = (preset?.knownModels ?? []).map(m => ({ id: m.id, name: m.name, contextLength: m.contextLength }));
158
+ const models = modelMap(catalog(cfg.id, fallback));
159
+ if (cfg.id === 'gemini') {
160
+ providers[cfg.id] = {
161
+ npm: '@ai-sdk/google',
162
+ name: cfg.name,
163
+ options: {
164
+ apiKey: cfg.apiKey,
165
+ },
166
+ models,
167
+ };
168
+ }
169
+ else {
170
+ providers[cfg.id] = {
171
+ npm: '@ai-sdk/openai-compatible',
172
+ name: cfg.name,
173
+ options: {
174
+ // Normalize so the AI SDK's `<base>/chat/completions` resolves
175
+ // correctly for every provider (e.g. Groq needs the /v1 segment).
176
+ baseURL: openAICompatBaseURL(cfg.baseURL),
177
+ apiKey: cfg.apiKey,
178
+ headers: cfg.headerName && cfg.headerName !== 'Authorization'
179
+ ? { [cfg.headerName]: cfg.apiKey }
180
+ : undefined,
181
+ },
182
+ models,
183
+ };
12
184
  }
13
- const data = await response.json();
14
- const models = data.models || [];
15
- const hasModel = models.some(m => m.name === model || m.name.startsWith(model.split(':')[0]));
16
- if (!hasModel) {
17
- spinner.warn(`Model '${model}' not found`);
18
- if (models.length > 0) {
19
- console.log(chalk.dim(`\nAvailable: ${models.slice(0, 5).map(m => m.name).join(', ')}`));
185
+ }
186
+ return providers;
187
+ }
188
+ /**
189
+ * Pick the best default model: the pinned coding-worker model if set
190
+ * and its provider is healthy, otherwise the first cloud provider's
191
+ * recommended coding model, otherwise the Ollama default.
192
+ */
193
+ async function pickDefaultModel(config, masterReachable) {
194
+ const workerModels = config.get('workerModels') ?? {};
195
+ const codingPin = workerModels['coding'];
196
+ // 1. Honor the coding-worker pin if its provider is available
197
+ if (codingPin) {
198
+ const cloudProviders = config.get('cloudProviders') ?? [];
199
+ const isCloud = cloudProviders.some(p => p.id === codingPin.providerId && p.enabled);
200
+ if (codingPin.providerId === 'ollama' && masterReachable) {
201
+ return { provider: codingPin.providerId, model: codingPin.modelId };
202
+ }
203
+ if (isCloud) {
204
+ return { provider: codingPin.providerId, model: codingPin.modelId };
205
+ }
206
+ }
207
+ // 2. If Ollama is reachable, use the Ollama default model
208
+ if (masterReachable) {
209
+ return { provider: 'ollama', model: config.get('defaultModel') };
210
+ }
211
+ // 3. Ollama down — pick the first enabled cloud provider's recommended coding model
212
+ const cloudProviders = config.get('cloudProviders') ?? [];
213
+ for (const cp of cloudProviders) {
214
+ if (!cp.enabled || !cp.apiKey)
215
+ continue;
216
+ const preset = getProviderPreset(cp.id);
217
+ const recs = preset?.recommended?.['coding'] ?? [];
218
+ if (recs.length > 0) {
219
+ return { provider: cp.id, model: recs[0] };
220
+ }
221
+ // fallback to first known model
222
+ const first = preset?.knownModels?.[0];
223
+ if (first) {
224
+ return { provider: cp.id, model: first.id };
225
+ }
226
+ }
227
+ // 4. Nothing reachable — return Ollama default anyway (TUI will show error)
228
+ return { provider: 'ollama', model: config.get('defaultModel') };
229
+ }
230
+ /**
231
+ * One kernel pass for the TUI launch: fetch each provider's FULL model catalog
232
+ * in parallel (so all models are exposed, incl. large ones like GLM-5.2 on HF
233
+ * and Nemotron-Ultra on NVIDIA), then route the default model under the policy.
234
+ */
235
+ async function prepareRouting(config, policyName) {
236
+ const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
237
+ const modelsByProvider = {};
238
+ try {
239
+ // Let background token-verified health checks run first.
240
+ await new Promise(resolve => setTimeout(resolve, 3000));
241
+ // Fetch every provider's full catalog concurrently.
242
+ await Promise.all(kernel.providerManager.list().map(async (provider) => {
243
+ try {
244
+ const caps = await provider.capabilities();
245
+ modelsByProvider[provider.info.id] = caps.models;
20
246
  }
21
- console.log(chalk.yellow(`\nPull the model first:`));
22
- console.log(chalk.cyan(` ollama pull ${model}\n`));
23
- process.exit(1);
247
+ catch {
248
+ modelsByProvider[provider.info.id] = [];
249
+ }
250
+ }));
251
+ const providers = (await kernel.providerManager.getHealthyProviders()).map(p => p.info);
252
+ const models = await kernel.providerManager.getAvailableModels();
253
+ let routed = null;
254
+ if (providers.length > 0 && models.length > 0) {
255
+ const ranked = kernel.router.rank({
256
+ capabilities: ['coding', 'reasoning'],
257
+ preferences: { reasoning: 95, speed: 80, contextLength: 32_000 },
258
+ policy: kernel.policies.get(policyName),
259
+ availableProviders: providers,
260
+ availableModels: models,
261
+ // Interactive sessions grow large; prefer a model that can accept a
262
+ // sizeable request so we don't default to a low-TPM free tier (e.g. Groq
263
+ // free ≈ 12k) that rejects big prompts mid-session.
264
+ estimatedRequestTokens: 16_000,
265
+ });
266
+ if (ranked[0])
267
+ routed = { providerId: ranked[0].providerId, modelId: ranked[0].modelId };
24
268
  }
25
- spinner.succeed('Ollama connected');
269
+ return { routed, modelsByProvider };
270
+ }
271
+ catch {
272
+ return { routed: null, modelsByProvider };
273
+ }
274
+ finally {
275
+ kernel.destroy();
276
+ }
277
+ }
278
+ /** Ensure the routed model exists in the TUI's injected provider config. */
279
+ function ensureModelInjected(providers, providerId, modelId) {
280
+ const p = providers[providerId];
281
+ if (!p)
282
+ return;
283
+ p.models = p.models ?? {};
284
+ if (!p.models[modelId])
285
+ p.models[modelId] = { name: modelId };
286
+ }
287
+ export async function runAgent(_masterUrl, _model, _useTui, config) {
288
+ const masterUrl = await resolveConfiguredMasterUrl(config);
289
+ const masterReachable = await isOllamaReachable(masterUrl);
290
+ const defaultModel = config.get('defaultModel');
291
+ const cloudProviders = config.get('cloudProviders') ?? [];
292
+ const enabledCloud = cloudProviders.filter(p => p.enabled);
293
+ const policyName = config.get('policy') ?? 'balanced';
294
+ // One kernel pass: load every provider's full model catalog (in parallel) and
295
+ // route the default model under the policy.
296
+ const routeSpinner = ora('loading model catalog & routing...').start();
297
+ const { routed, modelsByProvider } = await prepareRouting(config, policyName);
298
+ routeSpinner.stop();
299
+ const providers = buildProviderConfig(masterUrl, defaultModel, cloudProviders, masterReachable, modelsByProvider);
300
+ let defaultProvider;
301
+ let pickedModel;
302
+ if (routed && providers[routed.providerId]) {
303
+ defaultProvider = routed.providerId;
304
+ pickedModel = routed.modelId;
305
+ ensureModelInjected(providers, routed.providerId, routed.modelId);
26
306
  }
27
- catch (error) {
28
- spinner.fail('Connection failed');
29
- console.log(chalk.red(`\n✗ Could not connect to ${masterUrl}`));
30
- console.log(chalk.dim('\nRun: infinicode connect <master-ip>\n'));
31
- process.exit(1);
307
+ else {
308
+ const fb = await pickDefaultModel(config, masterReachable);
309
+ defaultProvider = fb.provider;
310
+ pickedModel = fb.model;
32
311
  }
33
- // Set environment for opencode
34
- process.env.LOCAL_ENDPOINT = `${masterUrl}/v1`;
312
+ const modelRef = `${defaultProvider}/${pickedModel}`;
313
+ const providerCount = Object.keys(providers).length;
314
+ const modelCount = Object.values(providers).reduce((n, p) => n + Object.keys(p.models ?? {}).length, 0);
315
+ const policy = config.get('policy') ?? 'local-first';
316
+ const pinnedCount = Object.keys(config.get('workerModels') ?? {}).length;
317
+ const routingInfo = `AUTO ${policy} ${providerCount}P ${pinnedCount}pins`;
35
318
  process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
36
- providers: {
37
- anthropic: { disabled: true },
38
- openai: { disabled: true },
39
- gemini: { disabled: true },
40
- groq: { disabled: true },
41
- openrouter: { disabled: true },
42
- xai: { disabled: true },
43
- azure: { disabled: true },
44
- bedrock: { disabled: true },
45
- vertexai: { disabled: true },
46
- copilot: { disabled: true },
47
- local: { apiKey: 'not-needed' }
319
+ $schema: 'https://opencode.ai/config.json',
320
+ provider: providers,
321
+ model: modelRef,
322
+ small_model: modelRef,
323
+ enabled_providers: Object.keys(providers),
324
+ agent: {
325
+ build: {
326
+ model: modelRef,
327
+ description: `◆ ${routingInfo}`,
328
+ color: 'primary',
329
+ },
330
+ plan: {
331
+ model: modelRef,
332
+ description: `◆ ${routingInfo}`,
333
+ color: 'secondary',
334
+ },
48
335
  },
49
- agents: {
50
- coder: { model: `local.${model}` }
51
- },
52
- tui: {
53
- theme: config.get('theme') || 'infini'
54
- }
55
336
  });
56
- console.log(chalk.dim(`\nStarting with ${chalk.cyan(model)}...`));
57
- console.log(chalk.dim('─'.repeat(40)));
58
- // Try to run opencode
337
+ console.log(chalk.dim(`\ninfinicode — launching TUI with ${chalk.cyan(String(providerCount))} provider(s), ${chalk.cyan(String(modelCount))} model(s)`));
338
+ console.log(chalk.dim(` default model: ${chalk.cyan(modelRef)}`));
339
+ console.log(chalk.dim(` policy: ${chalk.cyan(config.get('policy') ?? 'local-first')}`));
340
+ if (enabledCloud.length > 0) {
341
+ console.log(chalk.dim(` cloud: ${enabledCloud.map(p => p.id).join(', ')}`));
342
+ }
343
+ if (!masterReachable) {
344
+ console.log(chalk.yellow(` ⚠ Ollama not reachable — using cloud providers only`));
345
+ }
346
+ console.log(chalk.dim('-'.repeat(40)));
347
+ await renderPromptAnimation();
59
348
  const { execa } = await import('execa');
349
+ const infinicodeCommand = resolveInfinicodeCommand();
60
350
  try {
61
- // Check if opencode is installed
62
- await execa('which', ['opencode']).catch(() => execa('where', ['opencode']));
63
- // Run opencode
64
- await execa('opencode', [], {
351
+ if (infinicodeCommand === 'infinicode') {
352
+ await execa('which', ['infinicode']).catch(() => execa('where', ['infinicode']));
353
+ }
354
+ await execa(infinicodeCommand, [], {
65
355
  stdio: 'inherit',
66
- env: process.env
356
+ env: process.env,
67
357
  });
68
358
  }
69
359
  catch (e) {
70
- if (e.code === 'ENOENT' || e.exitCode === 1) {
71
- console.log(chalk.yellow('\n⚠️ OpenCode not found.'));
72
- console.log(chalk.dim('\nInstall it with:'));
73
- console.log(chalk.cyan(' npm install -g opencode'));
74
- console.log(chalk.dim('\nOr use infinicode with the Go binary:'));
75
- console.log(chalk.cyan(' cd /path/to/opencode-infini && go build -o infini && ./infini'));
360
+ const err = e;
361
+ if (err.code === 'ENOENT') {
362
+ console.log(chalk.yellow('\ninfinicode TUI binary not found.'));
363
+ console.log(chalk.dim('\nThe TUI requires the infinicode binary in bin/ or on PATH.'));
364
+ console.log(chalk.dim('Build from source:'));
365
+ console.log(chalk.cyan(' pnpm build'));
76
366
  console.log();
77
367
  process.exit(1);
78
368
  }
79
- if (e.exitCode) {
80
- process.exit(e.exitCode);
369
+ if (err.exitCode) {
370
+ process.exit(err.exitCode);
81
371
  }
82
372
  throw e;
83
373
  }
@@ -0,0 +1,18 @@
1
+ import type Conf from 'conf';
2
+ import type { InfinicodeConfig } from '../kernel/setup.js';
3
+ export interface ServeOptions {
4
+ role?: string;
5
+ name?: string;
6
+ port?: string;
7
+ host?: string;
8
+ token?: string;
9
+ seed?: string[];
10
+ autoUpdate?: boolean;
11
+ hub?: boolean;
12
+ tailscale?: boolean;
13
+ tag?: string;
14
+ gateway?: string;
15
+ gatewayToken?: string;
16
+ gatewayPassword?: string;
17
+ }
18
+ export declare function serve(config: Conf<InfinicodeConfig>, opts: ServeOptions): Promise<void>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * infinicode serve — run this device as a federation mesh node.
3
+ *
4
+ * infinicode serve start the mesh (uses config.federation)
5
+ * infinicode serve --role satellite --seed http://100.x.y.z:47913
6
+ * infinicode serve --hub --port 47913 --name workstation
7
+ *
8
+ * The node exposes an HTTP+SSE mesh endpoint, streams its activity + hardware
9
+ * telemetry to connected peers/harnesses, dispatches tasks across the mesh, and
10
+ * (as a satellite) inherits shared API/provider config from its hub.
11
+ */
12
+ import chalk from 'chalk';
13
+ import { readFileSync } from 'node:fs';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dirname, join } from 'node:path';
16
+ import { buildKernelFromConfig } from '../kernel/setup.js';
17
+ import { Federation, MoltfedConnector, loadOrCreateIdentity } from '../kernel/index.js';
18
+ function pkgVersion() {
19
+ try {
20
+ const here = dirname(fileURLToPath(import.meta.url));
21
+ return JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8')).version ?? '0.0.0';
22
+ }
23
+ catch {
24
+ return '0.0.0';
25
+ }
26
+ }
27
+ export async function serve(config, opts) {
28
+ const fed = config.get('federation') ?? {};
29
+ const role = (opts.hub ? 'hub' : opts.role ?? fed.role ?? 'peer');
30
+ const port = opts.port ? parseInt(opts.port, 10) : fed.port ?? 47913;
31
+ const seeds = [...(fed.seeds ?? []), ...(opts.seed ?? [])];
32
+ const token = opts.token ?? fed.token;
33
+ const kernel = buildKernelFromConfig(config);
34
+ const identity = loadOrCreateIdentity({ role, displayName: opts.name ?? fed.displayName });
35
+ // Manifest inputs are read live from the kernel so the map shows real caps.
36
+ const describe = () => {
37
+ const defs = kernel.workerRuntime.listRegisteredDefinitions();
38
+ const caps = [...new Set(defs.flatMap(d => d.capabilities))];
39
+ return {
40
+ version: pkgVersion(),
41
+ capabilities: caps,
42
+ workers: defs.map(d => d.type),
43
+ commands: kernel.commands.list().map(c => c.name),
44
+ providers: kernel.providerManager.listInfos().map(p => p.id),
45
+ };
46
+ };
47
+ // Satellite: persist shared config locally so providers/keys survive a restart.
48
+ const applyConfig = (cfg) => {
49
+ if (cfg.cloudProviders)
50
+ config.set('cloudProviders', cfg.cloudProviders);
51
+ if (cfg.policy)
52
+ config.set('policy', cfg.policy);
53
+ if (cfg.defaultModel)
54
+ config.set('defaultModel', cfg.defaultModel);
55
+ console.log(chalk.green(` ✓ applied shared config (rev ${cfg.revision}) — restart to load new providers`));
56
+ };
57
+ const federation = new Federation({
58
+ kernel,
59
+ identity,
60
+ describe,
61
+ logger: kernel.logger,
62
+ options: {
63
+ version: pkgVersion(),
64
+ port,
65
+ host: opts.host ?? fed.host,
66
+ token,
67
+ heartbeatMs: (fed.heartbeatSec ?? 30) * 1000,
68
+ autoUpdate: opts.autoUpdate ?? fed.autoUpdate,
69
+ applyConfig,
70
+ },
71
+ });
72
+ console.log(chalk.bold(`\n infinicode mesh node`));
73
+ console.log(chalk.dim(' ' + '-'.repeat(46)));
74
+ console.log(` node: ${chalk.cyan(identity.displayName)} ${chalk.dim('(' + identity.nodeId + ')')}`);
75
+ console.log(` role: ${chalk.yellow(role)}`);
76
+ console.log(` listen: ${chalk.cyan((opts.host ?? fed.host ?? '0.0.0.0') + ':' + port)}`);
77
+ if (token)
78
+ console.log(` auth: ${chalk.green('token required')}`);
79
+ console.log();
80
+ await federation.start();
81
+ // Connect to seeds and, as a satellite, pull shared config from the first hub.
82
+ for (const url of seeds) {
83
+ const peer = await federation.connect(url);
84
+ if (peer) {
85
+ console.log(chalk.green(` ✓ connected to ${peer.displayName} @ ${url}`));
86
+ if (role === 'satellite') {
87
+ const applied = await federation.pullConfig(peer.nodeId).catch(() => false);
88
+ if (applied)
89
+ console.log(chalk.green(` ✓ pulled shared config from ${peer.displayName}`));
90
+ }
91
+ }
92
+ }
93
+ // Optional: auto-discover peers over Tailscale (no --seed needed).
94
+ if (opts.tailscale) {
95
+ const n = await federation.discoverAndConnect(opts.tag).catch(() => 0);
96
+ console.log(chalk.green(` ✓ tailscale discovery connected ${n} peer(s)`));
97
+ }
98
+ // Optional: log this node into an InfiniBot gateway so it renders live on the
99
+ // neural-mesh map + hardware panels (no InfiniBot changes needed).
100
+ let connector;
101
+ if (opts.gateway) {
102
+ connector = new MoltfedConnector({
103
+ gatewayUrl: opts.gateway,
104
+ token: opts.gatewayToken,
105
+ password: opts.gatewayPassword,
106
+ client: { id: identity.displayName, version: pkgVersion(), platform: 'node', mode: 'backend' },
107
+ logger: kernel.logger,
108
+ getNodeStatuses: () => federation.nodeStatuses(),
109
+ getHardware: () => federation.latestHardware(),
110
+ subscribeFrames: (cb) => federation.onFrames(cb),
111
+ });
112
+ connector.start();
113
+ console.log(chalk.green(` ✓ linking to InfiniBot gateway ${opts.gateway}`));
114
+ }
115
+ console.log(chalk.dim(`\n mesh online — Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
116
+ const shutdown = async () => {
117
+ console.log(chalk.dim('\n stopping mesh…'));
118
+ connector?.stop();
119
+ await federation.stop();
120
+ kernel.destroy();
121
+ process.exit(0);
122
+ };
123
+ process.on('SIGINT', () => void shutdown());
124
+ process.on('SIGTERM', () => void shutdown());
125
+ // Keep the process alive.
126
+ await new Promise(() => { });
127
+ }
@@ -1,2 +1,2 @@
1
1
  import type Conf from 'conf';
2
- export declare function setupMaster(masterIp: string, port: string, model: string, config: Conf<any>): Promise<void>;
2
+ export declare function setupMaster(masterIp: string, port: string, model: string, config: Conf<any>, tailscaleMasterIp?: string, tailscalePort?: string): Promise<void>;