infinicode 2.3.5 → 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 +82 -22
- 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/openai-gateway.d.ts +55 -3
- package/dist/kernel/gateway/openai-gateway.js +428 -49
- package/dist/kernel/index.d.ts +2 -1
- package/dist/kernel/index.js +3 -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, KernelGateway } 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
|
}
|
|
@@ -442,6 +446,30 @@ function buildGatewayTuiConfig(gatewayUrl, providers) {
|
|
|
442
446
|
},
|
|
443
447
|
};
|
|
444
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
|
+
}
|
|
445
473
|
export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
446
474
|
const masterUrl = await resolveConfiguredMasterUrl(config);
|
|
447
475
|
const masterReachable = await isOllamaReachable(masterUrl);
|
|
@@ -457,11 +485,13 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
457
485
|
// (/nodes, /build, /activity, …) have a local node to talk to. Reuses a
|
|
458
486
|
// running `infinicode serve` if one is up, else starts an in-process node.
|
|
459
487
|
const mesh = await startLocalMeshNode(config, sessionKernel);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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;
|
|
465
495
|
// One kernel pass: load every provider's full model catalog (in parallel) and
|
|
466
496
|
// route the default model under the policy.
|
|
467
497
|
const routeSpinner = ora('loading model catalog & routing...').start();
|
|
@@ -471,21 +501,52 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
471
501
|
// single provider, so interactive chat gains full kernel parity: auto-routing,
|
|
472
502
|
// escalate-on-error, provider rotation, health/quota, live telemetry.
|
|
473
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');
|
|
474
507
|
let gateway;
|
|
475
508
|
if (!process.env.INFINICODE_NO_GATEWAY && gwProviders.length > 0) {
|
|
476
509
|
try {
|
|
477
|
-
gateway = new KernelGateway({
|
|
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
|
+
});
|
|
478
518
|
await gateway.start();
|
|
519
|
+
console.log(chalk.green(` ⚡ kernel gateway on ${gateway.url}`) + chalk.dim(` · log: ${gatewayLogFile}`));
|
|
479
520
|
}
|
|
480
|
-
catch {
|
|
481
|
-
|
|
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;
|
|
482
529
|
}
|
|
483
530
|
}
|
|
484
531
|
let providers;
|
|
485
532
|
let modelRef;
|
|
486
533
|
if (gateway) {
|
|
487
534
|
providers = buildGatewayTuiConfig(gateway.url, gwProviders);
|
|
488
|
-
|
|
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
|
+
}
|
|
489
550
|
}
|
|
490
551
|
else {
|
|
491
552
|
// Fallback: talk to providers directly (no kernel in the loop).
|
|
@@ -511,15 +572,15 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
511
572
|
const routingInfo = gateway
|
|
512
573
|
? `KERNEL ${policy} ${providerCount}P ${pinnedCount}pins`
|
|
513
574
|
: `AUTO ${policy} ${providerCount}P ${pinnedCount}pins`;
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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;
|
|
523
584
|
}
|
|
524
585
|
process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
525
586
|
$schema: 'https://opencode.ai/config.json',
|
|
@@ -527,7 +588,6 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
527
588
|
model: modelRef,
|
|
528
589
|
small_model: modelRef,
|
|
529
590
|
enabled_providers: Object.keys(providers),
|
|
530
|
-
...(pluginPaths.length > 0 ? { plugin: pluginPaths } : {}),
|
|
531
591
|
agent: {
|
|
532
592
|
build: {
|
|
533
593
|
model: modelRef,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AgentBackend, BackendId, BackendRunContext, BackendRunResult, BackendTask } from './types.js';
|
|
2
|
+
/** Accumulator threaded through per-line parsing. */
|
|
3
|
+
export interface CliParsed {
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
content?: string;
|
|
6
|
+
isError?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface CliBackendSpec {
|
|
9
|
+
id: BackendId;
|
|
10
|
+
displayName: string;
|
|
11
|
+
/** Binary name looked up on PATH (overridable via `binEnv`). */
|
|
12
|
+
bin: string;
|
|
13
|
+
/** argv for a fresh run. `prompt` already includes any role preamble. */
|
|
14
|
+
runArgs(prompt: string, task: BackendTask): string[];
|
|
15
|
+
/** argv to continue an existing session with a new message. */
|
|
16
|
+
resumeArgs?(sessionId: string, message: string, task: BackendTask): string[];
|
|
17
|
+
/** Parse one output line (or the whole raw output, as a fallback) into `acc`. */
|
|
18
|
+
parseLine(line: string, acc: CliParsed): void;
|
|
19
|
+
/** Extra process env. */
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
/** Env var overriding the binary path (e.g. INFINICODE_CLAUDE_BIN). */
|
|
22
|
+
binEnv?: string;
|
|
23
|
+
/** Env var of extra whitespace-split args appended to every invocation. */
|
|
24
|
+
extraArgsEnv?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class CliAgentBackend implements AgentBackend {
|
|
27
|
+
private spec;
|
|
28
|
+
readonly id: BackendId;
|
|
29
|
+
readonly displayName: string;
|
|
30
|
+
constructor(spec: CliBackendSpec);
|
|
31
|
+
private resolveBin;
|
|
32
|
+
available(): boolean;
|
|
33
|
+
private extraArgs;
|
|
34
|
+
run(task: BackendTask, ctx: BackendRunContext): Promise<BackendRunResult>;
|
|
35
|
+
send(sessionId: string, message: string, ctx: BackendRunContext): Promise<BackendRunResult>;
|
|
36
|
+
private exec;
|
|
37
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Generic CLI agent backend
|
|
3
|
+
*
|
|
4
|
+
* Runs a third-party coding CLI (Claude Code / Codex / OpenCode) as the executor
|
|
5
|
+
* for a dispatched mesh task. Spawns the process, streams every stdout/stderr
|
|
6
|
+
* line up the mesh as a frame, and parses the CLI's output into a session id +
|
|
7
|
+
* final text so the run can be followed and (later) messaged again.
|
|
8
|
+
*
|
|
9
|
+
* A `CliBackendSpec` captures the per-CLI differences (binary, argv, parsing);
|
|
10
|
+
* everything else — spawning, streaming, cancellation — is shared here.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { findExecutable } from './detect.js';
|
|
14
|
+
export class CliAgentBackend {
|
|
15
|
+
spec;
|
|
16
|
+
id;
|
|
17
|
+
displayName;
|
|
18
|
+
constructor(spec) {
|
|
19
|
+
this.spec = spec;
|
|
20
|
+
this.id = spec.id;
|
|
21
|
+
this.displayName = spec.displayName;
|
|
22
|
+
}
|
|
23
|
+
resolveBin() {
|
|
24
|
+
const override = this.spec.binEnv ? process.env[this.spec.binEnv] : undefined;
|
|
25
|
+
if (override)
|
|
26
|
+
return override;
|
|
27
|
+
return findExecutable(this.spec.bin);
|
|
28
|
+
}
|
|
29
|
+
available() {
|
|
30
|
+
return this.resolveBin() !== null;
|
|
31
|
+
}
|
|
32
|
+
extraArgs() {
|
|
33
|
+
const raw = this.spec.extraArgsEnv ? process.env[this.spec.extraArgsEnv] : undefined;
|
|
34
|
+
return raw ? raw.split(/\s+/).filter(Boolean) : [];
|
|
35
|
+
}
|
|
36
|
+
async run(task, ctx) {
|
|
37
|
+
const prompt = task.prompt && ctx.rolePreamble ? `${ctx.rolePreamble}\n\n${task.prompt}` : task.prompt;
|
|
38
|
+
const argv = task.sessionId && this.spec.resumeArgs
|
|
39
|
+
? this.spec.resumeArgs(task.sessionId, prompt, task)
|
|
40
|
+
: this.spec.runArgs(prompt, task);
|
|
41
|
+
return this.exec([...argv, ...this.extraArgs()], ctx);
|
|
42
|
+
}
|
|
43
|
+
async send(sessionId, message, ctx) {
|
|
44
|
+
const msg = ctx.rolePreamble ? `${ctx.rolePreamble}\n\n${message}` : message;
|
|
45
|
+
const task = { prompt: msg, sessionId };
|
|
46
|
+
const argv = this.spec.resumeArgs
|
|
47
|
+
? this.spec.resumeArgs(sessionId, msg, task)
|
|
48
|
+
: this.spec.runArgs(msg, task);
|
|
49
|
+
return this.exec([...argv, ...this.extraArgs()], ctx);
|
|
50
|
+
}
|
|
51
|
+
exec(argv, ctx) {
|
|
52
|
+
return new Promise(resolve => {
|
|
53
|
+
const bin = this.resolveBin();
|
|
54
|
+
if (!bin) {
|
|
55
|
+
resolve({ status: 'failed', error: `${this.spec.bin} not found on PATH` });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const child = spawn(bin, argv, {
|
|
59
|
+
cwd: ctx.cwd ?? process.cwd(),
|
|
60
|
+
env: { ...process.env, ...this.spec.env },
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
});
|
|
63
|
+
const acc = {};
|
|
64
|
+
let stdoutBuf = '';
|
|
65
|
+
let rawAll = '';
|
|
66
|
+
let stderrTail = '';
|
|
67
|
+
let settled = false;
|
|
68
|
+
const onAbort = () => {
|
|
69
|
+
try {
|
|
70
|
+
child.kill();
|
|
71
|
+
}
|
|
72
|
+
catch { /* already gone */ }
|
|
73
|
+
};
|
|
74
|
+
ctx.signal?.addEventListener('abort', onAbort, { once: true });
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
ctx.signal?.removeEventListener('abort', onAbort);
|
|
80
|
+
resolve(result);
|
|
81
|
+
};
|
|
82
|
+
child.stdout.setEncoding('utf8');
|
|
83
|
+
child.stdout.on('data', (chunk) => {
|
|
84
|
+
rawAll += chunk;
|
|
85
|
+
stdoutBuf += chunk;
|
|
86
|
+
let nl;
|
|
87
|
+
while ((nl = stdoutBuf.indexOf('\n')) >= 0) {
|
|
88
|
+
const line = stdoutBuf.slice(0, nl).replace(/\r$/, '');
|
|
89
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
90
|
+
if (!line.trim())
|
|
91
|
+
continue;
|
|
92
|
+
try {
|
|
93
|
+
this.spec.parseLine(line, acc);
|
|
94
|
+
}
|
|
95
|
+
catch { /* tolerant parsing */ }
|
|
96
|
+
ctx.onFrame({ kind: 'log', text: line });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
child.stderr.setEncoding('utf8');
|
|
100
|
+
child.stderr.on('data', (chunk) => {
|
|
101
|
+
stderrTail = (stderrTail + chunk).slice(-2000);
|
|
102
|
+
const trimmed = chunk.replace(/\s+$/, '');
|
|
103
|
+
if (trimmed)
|
|
104
|
+
ctx.onFrame({ kind: 'log', text: trimmed });
|
|
105
|
+
});
|
|
106
|
+
child.on('error', err => finish({ status: 'failed', error: err.message }));
|
|
107
|
+
child.on('close', code => {
|
|
108
|
+
// Flush a trailing partial line.
|
|
109
|
+
if (stdoutBuf.trim()) {
|
|
110
|
+
try {
|
|
111
|
+
this.spec.parseLine(stdoutBuf.trim(), acc);
|
|
112
|
+
}
|
|
113
|
+
catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
// Fallback for CLIs that pretty-print one multi-line JSON object.
|
|
116
|
+
if (!acc.content && !acc.sessionId && rawAll.trim()) {
|
|
117
|
+
try {
|
|
118
|
+
this.spec.parseLine(rawAll.trim(), acc);
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
const ok = !acc.isError && (code === 0 || !!acc.content);
|
|
123
|
+
finish({
|
|
124
|
+
status: ok ? 'completed' : 'failed',
|
|
125
|
+
sessionId: acc.sessionId,
|
|
126
|
+
content: acc.content,
|
|
127
|
+
error: ok ? undefined : (stderrTail.trim() || acc.content || `${this.spec.bin} exited with code ${code}`),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Absolute path to `bin` on PATH, or null if not found. */
|
|
2
|
+
export declare function findExecutable(bin: string): string | null;
|
|
3
|
+
export declare function hasExecutable(bin: string): boolean;
|
|
4
|
+
/** Test hook — forget cached lookups. */
|
|
5
|
+
export declare function clearExecutableCache(): void;
|