infinicode 2.3.4 → 2.3.6

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 (45) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +5 -1
  2. package/.opencode/plugins/mesh-commands.tsx +4 -3
  3. package/.opencode/plugins/routing-mode-display.tsx +6 -2
  4. package/.opencode/tui.json +2 -1
  5. package/dist/cli.js +25 -1
  6. package/dist/commands/mesh-install.d.ts +10 -0
  7. package/dist/commands/mesh-install.js +160 -0
  8. package/dist/commands/run.js +196 -41
  9. package/dist/kernel/agents/backends/cli-backend.d.ts +37 -0
  10. package/dist/kernel/agents/backends/cli-backend.js +132 -0
  11. package/dist/kernel/agents/backends/detect.d.ts +5 -0
  12. package/dist/kernel/agents/backends/detect.js +40 -0
  13. package/dist/kernel/agents/backends/index.d.ts +11 -0
  14. package/dist/kernel/agents/backends/index.js +3 -0
  15. package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
  16. package/dist/kernel/agents/backends/parse-utils.js +72 -0
  17. package/dist/kernel/agents/backends/registry.d.ts +30 -0
  18. package/dist/kernel/agents/backends/registry.js +140 -0
  19. package/dist/kernel/agents/backends/types.d.ts +65 -0
  20. package/dist/kernel/agents/backends/types.js +1 -0
  21. package/dist/kernel/agents/index.d.ts +8 -0
  22. package/dist/kernel/agents/index.js +8 -0
  23. package/dist/kernel/federation/federation.d.ts +54 -1
  24. package/dist/kernel/federation/federation.js +180 -8
  25. package/dist/kernel/federation/types.d.ts +26 -1
  26. package/dist/kernel/free-providers.js +83 -0
  27. package/dist/kernel/gateway/index.d.ts +7 -0
  28. package/dist/kernel/gateway/index.js +7 -0
  29. package/dist/kernel/gateway/openai-gateway.d.ts +137 -0
  30. package/dist/kernel/gateway/openai-gateway.js +723 -0
  31. package/dist/kernel/index.d.ts +3 -1
  32. package/dist/kernel/index.js +5 -1
  33. package/dist/kernel/logger.d.ts +16 -3
  34. package/dist/kernel/logger.js +38 -0
  35. package/dist/kernel/mcp/mcp-server.js +47 -8
  36. package/dist/kernel/orchestrator.d.ts +4 -0
  37. package/dist/kernel/orchestrator.js +34 -0
  38. package/dist/kernel/provider-manager.d.ts +13 -0
  39. package/dist/kernel/provider-manager.js +58 -5
  40. package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
  41. package/dist/kernel/providers/openai-compatible-provider.js +22 -3
  42. package/dist/kernel/recovery-manager.js +55 -15
  43. package/dist/kernel/router.js +15 -2
  44. package/dist/kernel/types.d.ts +12 -2
  45. package/package.json +1 -1
@@ -109,7 +109,7 @@ function AnimatedLogo(props: { renderer?: any }) {
109
109
  );
110
110
  }
111
111
 
112
- export const tui = async (api: any) => {
112
+ const tui = async (api: any) => {
113
113
  api.slots.register({
114
114
  slots: {
115
115
  home_logo() {
@@ -118,3 +118,7 @@ export const tui = async (api: any) => {
118
118
  },
119
119
  });
120
120
  };
121
+
122
+ // The TUI plugin loader reads `mod.default` (strict mode) and requires an object
123
+ // with a tui() function — a bare `export const tui` is silently skipped.
124
+ export default { id: "infinicode-home-logo", tui };
@@ -16,7 +16,6 @@
16
16
  * /follow, /dispatch, /goal, /mission, /role) open a prompt first.
17
17
  */
18
18
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
19
- import { createBindingLookup } from "@opentui/keymap/extras"
20
19
 
21
20
  type CommandResult = { ok?: boolean; text?: string; data?: unknown }
22
21
 
@@ -112,8 +111,10 @@ const SPECS: Spec[] = [
112
111
 
113
112
  const tui: TuiPlugin = async (api, options) => {
114
113
  if (options?.enabled === false) return
115
- const keys = createBindingLookup({})
116
114
 
115
+ // No keybindings — these are palette-only slash commands. Empty bindings avoids
116
+ // any dependency on the keymap-extras runtime module (which may not resolve for
117
+ // an externally-loaded plugin) so registration can never fail to load.
117
118
  api.keymap.registerLayer({
118
119
  commands: SPECS.map((s) => ({
119
120
  name: s.name,
@@ -127,7 +128,7 @@ const tui: TuiPlugin = async (api, options) => {
127
128
  else runNow(api, s.title, cmd)
128
129
  },
129
130
  })),
130
- bindings: keys.gather("mesh.global", SPECS.map((s) => s.name)),
131
+ bindings: [],
131
132
  })
132
133
  }
133
134
 
@@ -85,7 +85,7 @@ function buildRoutingBadge(cfg: InfinicodeConfig | null, tuiModel?: string): Rou
85
85
 
86
86
  // ── Plugin ──────────────────────────────────────────────────────────────────
87
87
 
88
- export const tui = async (api: any) => {
88
+ const tui = async (api: any) => {
89
89
  const [cfg] = createSignal(loadConfig());
90
90
 
91
91
  const getTuiModel = (): string | undefined => {
@@ -132,4 +132,8 @@ export const tui = async (api: any) => {
132
132
  },
133
133
  },
134
134
  });
135
- };
135
+ };
136
+
137
+ // The TUI plugin loader reads `mod.default` (strict mode) and requires an object
138
+ // with a tui() function — a bare `export const tui` is silently skipped.
139
+ export default { id: "infinicode-routing-mode", tui };
@@ -3,6 +3,7 @@
3
3
  "theme": "infinibot-gold",
4
4
  "plugin": [
5
5
  "./plugins/home-logo-animation.tsx",
6
- "./plugins/routing-mode-display.tsx"
6
+ "./plugins/routing-mode-display.tsx",
7
+ "./plugins/mesh-commands.tsx"
7
8
  ]
8
9
  }
package/dist/cli.js CHANGED
@@ -14,9 +14,10 @@ import { providersList, providersAdd, providersRemove, providersTest } from './c
14
14
  import { consoleRepl, consoleRun } from './commands/console.js';
15
15
  import { serve } from './commands/serve.js';
16
16
  import { mcpCommand } from './commands/mcp.js';
17
+ import { meshInstall, meshUninstall } from './commands/mesh-install.js';
17
18
  import { installTui } from './commands/install-tui.js';
18
19
  import { DEFAULT_CONFIG } from './kernel/config-schema.js';
19
- const VERSION = '2.3.4';
20
+ const VERSION = '2.3.6';
20
21
  const config = new Conf({
21
22
  projectName: 'infinicode',
22
23
  defaults: DEFAULT_CONFIG,
@@ -298,6 +299,29 @@ program
298
299
  .action(async (opts) => {
299
300
  await serve(config, opts);
300
301
  });
302
+ // ── infinicode: mesh — one-command MCP registration (no hand-edited JSON) ──
303
+ const meshCmd = program
304
+ .command('mesh')
305
+ .description('Join this device to the agent mesh by auto-registering the MCP server into your coding host');
306
+ meshCmd
307
+ .command('install', { isDefault: true })
308
+ .description('Register the infinicode MCP server into Claude Code (and/or OpenCode) — no manual file edits')
309
+ .option('--host <host>', 'target host: claude | opencode | all', 'claude')
310
+ .option('--token <token>', 'shared bearer token every device must present (recommended off-LAN)')
311
+ .option('--name <name>', 'friendly node name for this device')
312
+ .option('--tailscale', 'discover peers over Tailscale instead of / as well as the LAN')
313
+ .option('--no-lan', 'disable zero-config LAN auto-discovery')
314
+ .option('--print', 'dry run — show what would be written without changing anything')
315
+ .action(async (opts) => {
316
+ await meshInstall(opts);
317
+ });
318
+ meshCmd
319
+ .command('uninstall')
320
+ .description('Remove the infinicode MCP server from your coding host')
321
+ .option('--host <host>', 'target host: claude | opencode | all', 'claude')
322
+ .action(async (opts) => {
323
+ await meshUninstall(opts);
324
+ });
301
325
  // ── infinicode: install the TUI binary into this install ───────────────────
302
326
  program
303
327
  .command('install-tui')
@@ -0,0 +1,10 @@
1
+ export interface MeshInstallOptions {
2
+ host?: string;
3
+ token?: string;
4
+ name?: string;
5
+ tailscale?: boolean;
6
+ lan?: boolean;
7
+ print?: boolean;
8
+ }
9
+ export declare function meshInstall(opts: MeshInstallOptions): Promise<void>;
10
+ export declare function meshUninstall(opts: MeshInstallOptions): Promise<void>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * infinicode mesh install — register the infinicode MCP server into a coding
3
+ * host's config automatically, so the user never hand-edits a JSON file.
4
+ *
5
+ * infinicode mesh install → add to Claude Code (~/.claude.json)
6
+ * infinicode mesh install --host all → Claude Code + OpenCode
7
+ * infinicode mesh install --token secret → gated mesh
8
+ * infinicode mesh uninstall → remove it again
9
+ *
10
+ * After it runs, the user just restarts their coding host and the mesh tools
11
+ * (list_nodes / spawn_agent / send_message …) are available. On another laptop
12
+ * with the same one command, the two nodes auto-discover over the LAN.
13
+ */
14
+ import chalk from 'chalk';
15
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { findExecutable } from '../kernel/agents/backends/detect.js';
20
+ /** How a host should spawn the infinicode MCP node. Resolves a global binary
21
+ * if present, else falls back to `node <this package's cli.js>`. */
22
+ function resolveLaunch() {
23
+ const onPath = findExecutable('infinicode');
24
+ if (onPath)
25
+ return { command: 'infinicode', baseArgs: [] };
26
+ const cliJs = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli.js');
27
+ return { command: process.execPath, baseArgs: [cliJs] };
28
+ }
29
+ /** The `infinicode mcp …` argv the host should run. */
30
+ function meshArgs(opts) {
31
+ const args = ['mcp'];
32
+ if (opts.lan !== false)
33
+ args.push('--lan');
34
+ if (opts.tailscale)
35
+ args.push('--tailscale');
36
+ if (opts.token)
37
+ args.push('--token', opts.token);
38
+ if (opts.name)
39
+ args.push('--name', opts.name);
40
+ return args;
41
+ }
42
+ function readJson(path) {
43
+ if (!existsSync(path))
44
+ return {};
45
+ try {
46
+ return JSON.parse(readFileSync(path, 'utf8'));
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ }
52
+ /** Back up a config once before the first infinicode edit, then write pretty JSON. */
53
+ function writeJson(path, data) {
54
+ const dir = dirname(path);
55
+ if (!existsSync(dir))
56
+ mkdirSync(dir, { recursive: true });
57
+ if (existsSync(path)) {
58
+ const bak = `${path}.infinicode.bak`;
59
+ if (!existsSync(bak))
60
+ copyFileSync(path, bak);
61
+ }
62
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
63
+ }
64
+ const CLAUDE_CONFIG = () => join(homedir(), '.claude.json');
65
+ const OPENCODE_CONFIG = () => join(homedir(), '.config', 'opencode', 'opencode.json');
66
+ /** Claude Code: mcpServers.infinicode = { command, args }. */
67
+ function installClaude(opts) {
68
+ const path = CLAUDE_CONFIG();
69
+ const cfg = readJson(path);
70
+ const servers = cfg.mcpServers ?? {};
71
+ const existed = 'infinicode' in servers;
72
+ const { command, baseArgs } = resolveLaunch();
73
+ const entry = { command, args: [...baseArgs, ...meshArgs(opts)] };
74
+ if (!opts.print) {
75
+ cfg.mcpServers = { ...servers, infinicode: entry };
76
+ writeJson(path, cfg);
77
+ }
78
+ return { path, entry, existed };
79
+ }
80
+ /** OpenCode: mcp.infinicode = { type:'local', command:[...], enabled:true }. */
81
+ function installOpencode(opts) {
82
+ const path = OPENCODE_CONFIG();
83
+ const cfg = readJson(path);
84
+ const mcp = cfg.mcp ?? {};
85
+ const existed = 'infinicode' in mcp;
86
+ const { command, baseArgs } = resolveLaunch();
87
+ const entry = { type: 'local', command: [command, ...baseArgs, ...meshArgs(opts)], enabled: true };
88
+ if (!opts.print) {
89
+ cfg.mcp = { ...mcp, infinicode: entry };
90
+ writeJson(path, cfg);
91
+ }
92
+ return { path, entry, existed };
93
+ }
94
+ function removeFrom(path, key) {
95
+ if (!existsSync(path))
96
+ return false;
97
+ const cfg = readJson(path);
98
+ const bag = cfg[key];
99
+ if (!bag || !('infinicode' in bag))
100
+ return false;
101
+ delete bag.infinicode;
102
+ cfg[key] = bag;
103
+ writeJson(path, cfg);
104
+ return true;
105
+ }
106
+ function selectedHosts(host) {
107
+ const h = (host ?? 'claude').toLowerCase();
108
+ if (h === 'all')
109
+ return ['claude', 'opencode'];
110
+ if (h === 'opencode')
111
+ return ['opencode'];
112
+ if (h === 'claude' || h === 'claude-code')
113
+ return ['claude'];
114
+ return ['claude'];
115
+ }
116
+ export async function meshInstall(opts) {
117
+ const hosts = selectedHosts(opts.host);
118
+ console.log(chalk.bold('\n🕸 infinicode mesh — MCP registration\n'));
119
+ for (const host of hosts) {
120
+ const res = host === 'claude' ? installClaude(opts) : installOpencode(opts);
121
+ const label = host === 'claude' ? 'Claude Code' : 'OpenCode';
122
+ const verb = opts.print ? 'would write' : res.existed ? 'updated' : 'added';
123
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(label)} — ${verb} ${chalk.dim(res.path)}`);
124
+ console.log(chalk.dim(` ${JSON.stringify(res.entry)}`));
125
+ }
126
+ // Warn if the target CLI backend isn't installed on THIS machine — a worker
127
+ // node can only run agents for CLIs it actually has.
128
+ const present = ['claude', 'codex', 'opencode'].filter(b => findExecutable(b));
129
+ console.log();
130
+ if (present.length) {
131
+ console.log(` ${chalk.green('•')} coding CLIs detected here (can execute agents): ${chalk.bold(present.join(', '))}`);
132
+ }
133
+ else {
134
+ console.log(` ${chalk.yellow('•')} no coding CLI (claude/codex/opencode) found on PATH — this device can drive the mesh but can't execute agents until one is installed.`);
135
+ }
136
+ if (opts.print) {
137
+ console.log(chalk.dim('\n (dry run — nothing was written)\n'));
138
+ return;
139
+ }
140
+ console.log(chalk.bold('\n Next:'));
141
+ console.log(` 1. Run the SAME command on the other laptop.`);
142
+ console.log(` 2. Restart your coding host (Claude Code) on both.`);
143
+ console.log(` 3. Ask it: ${chalk.italic('"list_nodes"')} then ${chalk.italic('"spawn_agent on <laptop> with agent claude to …"')}`);
144
+ if (!opts.token) {
145
+ console.log(chalk.dim(`\n Tip: on an untrusted network, re-run with --token <secret> on every device.`));
146
+ }
147
+ console.log();
148
+ }
149
+ export async function meshUninstall(opts) {
150
+ const hosts = selectedHosts(opts.host);
151
+ console.log(chalk.bold('\n🕸 infinicode mesh — removing MCP registration\n'));
152
+ for (const host of hosts) {
153
+ const path = host === 'claude' ? CLAUDE_CONFIG() : OPENCODE_CONFIG();
154
+ const key = host === 'claude' ? 'mcpServers' : 'mcp';
155
+ const removed = removeFrom(path, key);
156
+ const label = host === 'claude' ? 'Claude Code' : 'OpenCode';
157
+ console.log(` ${removed ? chalk.green('✓ removed from') : chalk.dim('· not present in')} ${chalk.cyan(label)} ${chalk.dim(path)}`);
158
+ }
159
+ console.log(chalk.dim('\n Restart your coding host to apply.\n'));
160
+ }
@@ -18,12 +18,12 @@ import chalk from 'chalk';
18
18
  import ora from 'ora';
19
19
  import { existsSync, readFileSync } from 'node:fs';
20
20
  import { dirname, join, resolve } from 'node:path';
21
- import { fileURLToPath, pathToFileURL } from 'node:url';
21
+ import { fileURLToPath } from 'node:url';
22
22
  import { createRequire } from 'node:module';
23
23
  import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from '../ascii-video-animation.js';
24
24
  import { getProviderPreset } from '../kernel/free-providers.js';
25
25
  import { buildKernelFromConfig } from '../kernel/setup.js';
26
- import { SilentLogger, Federation, loadOrCreateIdentity } from '../kernel/index.js';
26
+ import { SilentLogger, FileLogger, Federation, loadOrCreateIdentity, KernelGateway, phaseBenchmarkFloor } from '../kernel/index.js';
27
27
  import { openAICompatBaseURL } from '../kernel/provider-url.js';
28
28
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
29
  const PROMPT_ANIMATION_MAX_FRAMES = Math.min(ASCII_VIDEO_FRAMES.length, ASCII_VIDEO_FPS * 3);
@@ -150,7 +150,11 @@ function modelEntry(m) {
150
150
  // Only set a limit when we have a real context value (> the 8192 default) —
151
151
  // a wrong-small limit would make opencode compact large models too early.
152
152
  if (m.contextLength && m.contextLength > 8192) {
153
- entry.limit = { context: m.contextLength, output: Math.min(16384, Math.max(2048, Math.floor(m.contextLength / 16))) };
153
+ // Generous output headroom so a large single-file generation completes in one
154
+ // turn instead of truncating at the cap and stalling ("continue"). Frontier
155
+ // models support 16–32k output; a too-high ask that a model rejects is caught
156
+ // by the gateway's swap-on-error, so we bias toward completing big writes.
157
+ entry.limit = { context: m.contextLength, output: Math.min(32768, Math.max(4096, Math.floor(m.contextLength / 8))) };
154
158
  }
155
159
  return entry;
156
160
  }
@@ -257,8 +261,11 @@ async function pickDefaultModel(config, masterReachable) {
257
261
  * in parallel (so all models are exposed, incl. large ones like GLM-5.2 on HF
258
262
  * and Nemotron-Ultra on NVIDIA), then route the default model under the policy.
259
263
  */
260
- async function prepareRouting(config, policyName) {
261
- const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
264
+ async function prepareRouting(config, policyName, sharedKernel) {
265
+ // Reuse the caller's session kernel when provided (so the gateway routes
266
+ // against the same warmed-up provider pool) — only destroy a kernel we own.
267
+ const kernel = sharedKernel ?? buildKernelFromConfig(config, { logger: new SilentLogger() });
268
+ const ownsKernel = !sharedKernel;
262
269
  const modelsByProvider = {};
263
270
  try {
264
271
  // Let background token-verified health checks run first.
@@ -297,7 +304,8 @@ async function prepareRouting(config, policyName) {
297
304
  return { routed: null, modelsByProvider };
298
305
  }
299
306
  finally {
300
- kernel.destroy();
307
+ if (ownsKernel)
308
+ kernel.destroy();
301
309
  }
302
310
  }
303
311
  /** Ensure the routed model exists in the TUI's injected provider config. */
@@ -337,7 +345,7 @@ async function meshAlive(url, token) {
337
345
  * otherwise start an in-process node for the session. Best-effort: on any
338
346
  * failure the TUI still launches, just without the live mesh palette.
339
347
  */
340
- async function startLocalMeshNode(config) {
348
+ async function startLocalMeshNode(config, kernel) {
341
349
  if (process.env.INFINICODE_NO_MESH)
342
350
  return null;
343
351
  const fed = config.get('federation') ?? {};
@@ -348,7 +356,6 @@ async function startLocalMeshNode(config) {
348
356
  // Reuse an already-running node (don't double-bind the port).
349
357
  if (await meshAlive(url, token))
350
358
  return { url, token, stop: async () => { } };
351
- const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
352
359
  const role = (fed.role ?? 'peer');
353
360
  const identity = loadOrCreateIdentity({ role, displayName: fed.displayName });
354
361
  const version = runPkgVersion();
@@ -375,15 +382,94 @@ async function startLocalMeshNode(config) {
375
382
  federation.startLanDiscovery(undefined, fed.lanPort);
376
383
  for (const s of fed.seeds ?? [])
377
384
  await federation.connect(s).catch(() => undefined);
385
+ // The kernel is owned by runAgent — the mesh only stops the federation.
378
386
  return {
379
387
  url, token,
380
- stop: async () => { await federation.stop(); kernel.destroy(); },
388
+ stop: async () => { await federation.stop(); },
381
389
  };
382
390
  }
383
391
  catch {
384
392
  return null;
385
393
  }
386
394
  }
395
+ /** Build the gateway's provider table (baseURL/apiKey/models) from config. */
396
+ function buildGatewayProviders(config, masterUrl, masterReachable, defaultModel, modelsByProvider) {
397
+ const out = [];
398
+ const catalog = (id, fallback) => {
399
+ const fetched = modelsByProvider[id];
400
+ return fetched && fetched.length ? fetched : fallback;
401
+ };
402
+ const shape = (models) => models.map(m => ({ id: m.id, name: m.name, contextLength: m.contextLength }));
403
+ if (masterReachable) {
404
+ out.push({
405
+ id: 'ollama',
406
+ baseURL: `${masterUrl}/v1`,
407
+ models: shape(catalog('ollama', [{ id: defaultModel, name: defaultModel, contextLength: 8192 }])),
408
+ });
409
+ }
410
+ for (const cfg of config.get('cloudProviders') ?? []) {
411
+ if (!cfg.enabled || !cfg.apiKey)
412
+ continue;
413
+ const preset = getProviderPreset(cfg.id);
414
+ const fallback = (preset?.knownModels ?? []).map(m => ({ id: m.id, name: m.name, contextLength: m.contextLength }));
415
+ const models = shape(catalog(cfg.id, fallback));
416
+ if (cfg.id === 'gemini') {
417
+ // Google's OpenAI-compatibility endpoint keeps the relay uniform.
418
+ out.push({ id: 'gemini', baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: cfg.apiKey, models });
419
+ }
420
+ else {
421
+ const headers = cfg.headerName && cfg.headerName !== 'Authorization' ? { [cfg.headerName]: cfg.apiKey } : undefined;
422
+ out.push({ id: cfg.id, baseURL: openAICompatBaseURL(cfg.baseURL), apiKey: cfg.apiKey, headers, models });
423
+ }
424
+ }
425
+ return out;
426
+ }
427
+ /**
428
+ * Build the TUI's provider config as a SINGLE `infinicode` provider pointing at
429
+ * the local kernel gateway. Real API keys stay server-side (the TUI only gets a
430
+ * dummy token); every model in the pool is exposed as `providerId/modelId`, plus
431
+ * an `auto` entry that lets the kernel router pick per request.
432
+ */
433
+ function buildGatewayTuiConfig(gatewayUrl, providers) {
434
+ const models = { auto: { name: 'AUTO — kernel routes best model' } };
435
+ for (const p of providers) {
436
+ for (const m of p.models) {
437
+ models[`${p.id}/${m.id}`] = modelEntry({ id: `${p.id}/${m.id}`, name: m.name ?? m.id, contextLength: m.contextLength });
438
+ }
439
+ }
440
+ return {
441
+ infinicode: {
442
+ npm: '@ai-sdk/openai-compatible',
443
+ name: 'infinicode (kernel)',
444
+ options: { baseURL: gatewayUrl, apiKey: 'infinicode-gateway' },
445
+ models,
446
+ },
447
+ };
448
+ }
449
+ /**
450
+ * Pick the strongest coding-capable model across the FULL fetched catalog (not
451
+ * just health-verified providers). Groq health-checks fastest, so ranking over
452
+ * only-healthy providers at startup hands the default to Groq/Llama before a
453
+ * frontier provider (NVIDIA/HF/Chutes) finishes verifying. Ranking the whole
454
+ * catalog by benchmark lets GLM/Kimi/DeepSeek/Qwen3 win; the gateway swaps on
455
+ * failure anyway, so choosing a not-yet-verified provider is safe.
456
+ */
457
+ function pickStrongestModel(kernel, modelsByProvider) {
458
+ let best = null;
459
+ let bestBench = -1;
460
+ for (const [providerId, models] of Object.entries(modelsByProvider)) {
461
+ for (const m of models) {
462
+ if (!m.capabilities?.includes('coding'))
463
+ continue;
464
+ const b = kernel.router.benchmarkFor(m);
465
+ if (b > bestBench) {
466
+ bestBench = b;
467
+ best = { providerId, modelId: m.id };
468
+ }
469
+ }
470
+ }
471
+ return best;
472
+ }
387
473
  export async function runAgent(_masterUrl, _model, _useTui, config) {
388
474
  const masterUrl = await resolveConfiguredMasterUrl(config);
389
475
  const masterReachable = await isOllamaReachable(masterUrl);
@@ -391,48 +477,110 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
391
477
  const cloudProviders = config.get('cloudProviders') ?? [];
392
478
  const enabledCloud = cloudProviders.filter(p => p.enabled);
393
479
  const policyName = config.get('policy') ?? 'balanced';
480
+ // Session kernel — lives for the whole TUI session. It is the routing/recovery
481
+ // brain the gateway and the mesh node both share, so every chat turn the TUI
482
+ // makes flows through the same warmed-up provider pool + event bus.
483
+ const sessionKernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
394
484
  // Make this TUI session a mesh participant so the in-TUI slash commands
395
485
  // (/nodes, /build, /activity, …) have a local node to talk to. Reuses a
396
486
  // running `infinicode serve` if one is up, else starts an in-process node.
397
- const mesh = await startLocalMeshNode(config);
398
- if (mesh) {
399
- process.env.INFINICODE_MESH_URL = mesh.url;
400
- if (mesh.token)
401
- process.env.INFINICODE_MESH_TOKEN = mesh.token;
402
- }
487
+ const mesh = await startLocalMeshNode(config, sessionKernel);
488
+ // Always give the in-TUI mesh commands a target: the node we started, or the
489
+ // default local port (a separate `infinicode serve` may be there / come up).
490
+ const fedCfg = config.get('federation') ?? {};
491
+ process.env.INFINICODE_MESH_URL = mesh?.url ?? `http://127.0.0.1:${fedCfg.port ?? 47913}`;
492
+ const meshToken = mesh?.token ?? fedCfg.token;
493
+ if (meshToken)
494
+ process.env.INFINICODE_MESH_TOKEN = meshToken;
403
495
  // One kernel pass: load every provider's full model catalog (in parallel) and
404
496
  // route the default model under the policy.
405
497
  const routeSpinner = ora('loading model catalog & routing...').start();
406
- const { routed, modelsByProvider } = await prepareRouting(config, policyName);
498
+ const { routed, modelsByProvider } = await prepareRouting(config, policyName, sessionKernel);
407
499
  routeSpinner.stop();
408
- const providers = buildProviderConfig(masterUrl, defaultModel, cloudProviders, masterReachable, modelsByProvider);
409
- let defaultProvider;
410
- let pickedModel;
411
- if (routed && providers[routed.providerId]) {
412
- defaultProvider = routed.providerId;
413
- pickedModel = routed.modelId;
414
- ensureModelInjected(providers, routed.providerId, routed.modelId);
500
+ // Stand up the OpenAI-compatible kernel gateway. The TUI points at THIS as its
501
+ // single provider, so interactive chat gains full kernel parity: auto-routing,
502
+ // escalate-on-error, provider rotation, health/quota, live telemetry.
503
+ const gwProviders = buildGatewayProviders(config, masterUrl, masterReachable, defaultModel, modelsByProvider);
504
+ // Durable trace of routing/swap/truncation/continuation decisions — the gateway's
505
+ // console belongs to the TUI, so without this a mid-task death is invisible.
506
+ const gatewayLogFile = join(dirname(config.path), 'gateway.log');
507
+ let gateway;
508
+ if (!process.env.INFINICODE_NO_GATEWAY && gwProviders.length > 0) {
509
+ try {
510
+ gateway = new KernelGateway({
511
+ kernel: sessionKernel,
512
+ providers: gwProviders,
513
+ policyName,
514
+ logger: new FileLogger(gatewayLogFile),
515
+ // Bias interactive coding toward capable models (soft floor, not a pin).
516
+ minBenchmark: phaseBenchmarkFloor(['coding', 'frontend']),
517
+ });
518
+ await gateway.start();
519
+ console.log(chalk.green(` ⚡ kernel gateway on ${gateway.url}`) + chalk.dim(` · log: ${gatewayLogFile}`));
520
+ }
521
+ catch (err) {
522
+ // Surface WHY the kernel gateway couldn't start — without it there's no
523
+ // routing/reliability, so a silent fallback to direct-Llama is exactly the
524
+ // "died / still using llama" symptom. Make it loud.
525
+ console.log(chalk.yellow(`\n ⚠ kernel gateway failed to start — falling back to direct routing (no auto-swap/reliability).`));
526
+ console.log(chalk.dim(` reason: ${err instanceof Error ? err.message : String(err)}`));
527
+ gateway?.stop().catch(() => undefined);
528
+ gateway = undefined;
529
+ }
530
+ }
531
+ let providers;
532
+ let modelRef;
533
+ if (gateway) {
534
+ providers = buildGatewayTuiConfig(gateway.url, gwProviders);
535
+ // Default to the CONCRETE strongest model (not "auto") so the footer shows a
536
+ // real, capable model and the session stays consistent — but pick it from the
537
+ // FULL catalog by benchmark so it's a frontier model, not fast-but-mid Groq/
538
+ // Llama. Gateway still swaps on failure; "auto" stays selectable in the palette.
539
+ const best = pickStrongestModel(sessionKernel, modelsByProvider) ?? routed;
540
+ if (best) {
541
+ const key = `${best.providerId}/${best.modelId}`;
542
+ const gwModels = providers['infinicode'].models;
543
+ if (!gwModels[key])
544
+ gwModels[key] = modelEntry({ id: key, name: key });
545
+ modelRef = `infinicode/${key}`;
546
+ }
547
+ else {
548
+ modelRef = 'infinicode/auto';
549
+ }
415
550
  }
416
551
  else {
417
- const fb = await pickDefaultModel(config, masterReachable);
418
- defaultProvider = fb.provider;
419
- pickedModel = fb.model;
552
+ // Fallback: talk to providers directly (no kernel in the loop).
553
+ providers = buildProviderConfig(masterUrl, defaultModel, cloudProviders, masterReachable, modelsByProvider);
554
+ let defaultProvider;
555
+ let pickedModel;
556
+ if (routed && providers[routed.providerId]) {
557
+ defaultProvider = routed.providerId;
558
+ pickedModel = routed.modelId;
559
+ ensureModelInjected(providers, routed.providerId, routed.modelId);
560
+ }
561
+ else {
562
+ const fb = await pickDefaultModel(config, masterReachable);
563
+ defaultProvider = fb.provider;
564
+ pickedModel = fb.model;
565
+ }
566
+ modelRef = `${defaultProvider}/${pickedModel}`;
420
567
  }
421
- const modelRef = `${defaultProvider}/${pickedModel}`;
422
- const providerCount = Object.keys(providers).length;
568
+ const providerCount = gateway ? gwProviders.length : Object.keys(providers).length;
423
569
  const modelCount = Object.values(providers).reduce((n, p) => n + Object.keys(p.models ?? {}).length, 0);
424
570
  const policy = config.get('policy') ?? 'local-first';
425
571
  const pinnedCount = Object.keys(config.get('workerModels') ?? {}).length;
426
- const routingInfo = `AUTO ${policy} ${providerCount}P ${pinnedCount}pins`;
427
- // Runtime TUI plugin that adds the mesh slash-commands (/nodes, /build, …)
428
- // to the `/` palette. Shipped in the package at .opencode/plugins; loaded via
429
- // a file:// URL so it resolves regardless of the TUI's cwd. Only injected when
430
- // a local mesh node is available (else the commands would have nothing to hit).
431
- const pluginPaths = [];
432
- if (mesh) {
433
- const pluginFile = join(resolve(__dirname, '..', '..'), '.opencode', 'plugins', 'mesh-commands.tsx');
434
- if (existsSync(pluginFile))
435
- pluginPaths.push(pathToFileURL(pluginFile).href);
572
+ const routingInfo = gateway
573
+ ? `KERNEL ${policy} ${providerCount}P ${pinnedCount}pins`
574
+ : `AUTO ${policy} ${providerCount}P ${pinnedCount}pins`;
575
+ // TUI plugins (theme, animated logo, routing display, AND the mesh slash-command
576
+ // palette) load ONLY from a `tui` config file NOT from OPENCODE_CONFIG_CONTENT
577
+ // (that's the main/server config; a TUI plugin there never has its `tui` export
578
+ // invoked, so its /commands never register). Point the TUI at the package's
579
+ // tui.json via OPENCODE_TUI_CONFIG so all three plugins load regardless of cwd.
580
+ // Its plugin paths are relative and resolve next to tui.json (../plugins/*.tsx).
581
+ const tuiConfigFile = join(resolve(__dirname, '..', '..'), '.opencode', 'tui.json');
582
+ if (existsSync(tuiConfigFile) && !process.env.OPENCODE_TUI_CONFIG) {
583
+ process.env.OPENCODE_TUI_CONFIG = tuiConfigFile;
436
584
  }
437
585
  process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
438
586
  $schema: 'https://opencode.ai/config.json',
@@ -440,7 +588,6 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
440
588
  model: modelRef,
441
589
  small_model: modelRef,
442
590
  enabled_providers: Object.keys(providers),
443
- ...(pluginPaths.length > 0 ? { plugin: pluginPaths } : {}),
444
591
  agent: {
445
592
  build: {
446
593
  model: modelRef,
@@ -455,6 +602,9 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
455
602
  },
456
603
  });
457
604
  console.log(chalk.dim(`\ninfinicode — launching TUI with ${chalk.cyan(String(providerCount))} provider(s), ${chalk.cyan(String(modelCount))} model(s)`));
605
+ if (gateway) {
606
+ console.log(chalk.dim(` kernel gateway: ${chalk.green(gateway.url)} ${chalk.dim('(auto-routing + escalate-on-error)')}`));
607
+ }
458
608
  console.log(chalk.dim(` default model: ${chalk.cyan(modelRef)}`));
459
609
  console.log(chalk.dim(` policy: ${chalk.cyan(config.get('policy') ?? 'local-first')}`));
460
610
  if (enabledCloud.length > 0) {
@@ -479,6 +629,11 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
479
629
  console.log(chalk.cyan(' infinicode install-tui --url <url>') + chalk.dim(' # or download from a hosted binary'));
480
630
  process.exit(1);
481
631
  }
632
+ const cleanup = async () => {
633
+ await gateway?.stop().catch(() => undefined);
634
+ await mesh?.stop().catch(() => undefined);
635
+ sessionKernel.destroy();
636
+ };
482
637
  const { execa } = await import('execa');
483
638
  try {
484
639
  await execa(tuiBinary, [], {
@@ -487,12 +642,12 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
487
642
  });
488
643
  }
489
644
  catch (e) {
490
- await mesh?.stop().catch(() => undefined);
645
+ await cleanup();
491
646
  const err = e;
492
647
  if (err.exitCode) {
493
648
  process.exit(err.exitCode);
494
649
  }
495
650
  throw e;
496
651
  }
497
- await mesh?.stop().catch(() => undefined);
652
+ await cleanup();
498
653
  }