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.
- package/.opencode/plugins/home-logo-animation.tsx +5 -1
- package/.opencode/plugins/mesh-commands.tsx +4 -3
- package/.opencode/plugins/routing-mode-display.tsx +6 -2
- package/.opencode/tui.json +2 -1
- package/dist/cli.js +25 -1
- package/dist/commands/mesh-install.d.ts +10 -0
- package/dist/commands/mesh-install.js +160 -0
- package/dist/commands/run.js +196 -41
- package/dist/kernel/agents/backends/cli-backend.d.ts +37 -0
- package/dist/kernel/agents/backends/cli-backend.js +132 -0
- package/dist/kernel/agents/backends/detect.d.ts +5 -0
- package/dist/kernel/agents/backends/detect.js +40 -0
- package/dist/kernel/agents/backends/index.d.ts +11 -0
- package/dist/kernel/agents/backends/index.js +3 -0
- package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
- package/dist/kernel/agents/backends/parse-utils.js +72 -0
- package/dist/kernel/agents/backends/registry.d.ts +30 -0
- package/dist/kernel/agents/backends/registry.js +140 -0
- package/dist/kernel/agents/backends/types.d.ts +65 -0
- package/dist/kernel/agents/backends/types.js +1 -0
- package/dist/kernel/agents/index.d.ts +8 -0
- package/dist/kernel/agents/index.js +8 -0
- package/dist/kernel/federation/federation.d.ts +54 -1
- package/dist/kernel/federation/federation.js +180 -8
- package/dist/kernel/federation/types.d.ts +26 -1
- package/dist/kernel/free-providers.js +83 -0
- package/dist/kernel/gateway/index.d.ts +7 -0
- package/dist/kernel/gateway/index.js +7 -0
- package/dist/kernel/gateway/openai-gateway.d.ts +137 -0
- package/dist/kernel/gateway/openai-gateway.js +723 -0
- package/dist/kernel/index.d.ts +3 -1
- package/dist/kernel/index.js +5 -1
- package/dist/kernel/logger.d.ts +16 -3
- package/dist/kernel/logger.js +38 -0
- package/dist/kernel/mcp/mcp-server.js +47 -8
- package/dist/kernel/orchestrator.d.ts +4 -0
- package/dist/kernel/orchestrator.js +34 -0
- package/dist/kernel/provider-manager.d.ts +13 -0
- package/dist/kernel/provider-manager.js +58 -5
- package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
- package/dist/kernel/providers/openai-compatible-provider.js +22 -3
- package/dist/kernel/recovery-manager.js +55 -15
- package/dist/kernel/router.js +15 -2
- package/dist/kernel/types.d.ts +12 -2
- package/package.json +1 -1
|
@@ -109,7 +109,7 @@ function AnimatedLogo(props: { renderer?: any }) {
|
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
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:
|
|
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
|
-
|
|
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 };
|
package/.opencode/tui.json
CHANGED
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.
|
|
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
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
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 =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
652
|
+
await cleanup();
|
|
498
653
|
}
|