infinicode 2.1.0 → 2.2.1

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/README.md CHANGED
@@ -109,10 +109,12 @@ infinicode serve --role satellite --tailscale --tag tag:robopark
109
109
  # or point at exact peers
110
110
  infinicode serve --role satellite --seed http://192.168.1.20:47913
111
111
 
112
- # verify the mesh
113
- infinicode x /nodes
112
+ # verify the mesh — ask any running node for its peers (self + connected)
113
+ curl http://localhost:47913/fed/nodes
114
114
  ```
115
115
 
116
+ Set your cloud provider keys + policy **once on the hub**; every satellite auto-sources them (over `--seed`/`--lan`/`--tailscale`) and registers the providers + models live, no restart.
117
+
116
118
  Drive the fleet from an MCP host — register the control server (spawn / dispatch / follow / role / voice / mesh-map tools):
117
119
 
118
120
  ```jsonc
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import { consoleRepl, consoleRun } from './commands/console.js';
15
15
  import { serve } from './commands/serve.js';
16
16
  import { mcpCommand } from './commands/mcp.js';
17
17
  import { DEFAULT_CONFIG } from './kernel/config-schema.js';
18
- const VERSION = '2.1.0';
18
+ const VERSION = '2.2.1';
19
19
  const config = new Conf({
20
20
  projectName: 'infinicode',
21
21
  defaults: DEFAULT_CONFIG,
@@ -12,7 +12,7 @@
12
12
  import { readFileSync } from 'node:fs';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { dirname, join } from 'node:path';
15
- import { buildKernelFromConfig } from '../kernel/setup.js';
15
+ import { buildKernelFromConfig, registerCloudProviders } from '../kernel/setup.js';
16
16
  import { Federation, GoalLoop, loadOrCreateIdentity, serveMcpStdio, SilentLogger, } from '../kernel/index.js';
17
17
  function pkgVersion() {
18
18
  try {
@@ -51,21 +51,39 @@ export async function mcpCommand(config, opts) {
51
51
  };
52
52
  };
53
53
  const applyConfig = (cfg) => {
54
- if (cfg.cloudProviders)
55
- config.set('cloudProviders', cfg.cloudProviders);
56
- if (cfg.policy)
54
+ let live = 0;
55
+ if (cfg.cloudProviders) {
56
+ const providers = cfg.cloudProviders;
57
+ config.set('cloudProviders', providers);
58
+ live = registerCloudProviders(kernel, providers); // live → models available now
59
+ }
60
+ if (cfg.policy) {
57
61
  config.set('policy', cfg.policy);
62
+ kernel.setDefaultPolicy(cfg.policy);
63
+ }
58
64
  if (cfg.defaultModel)
59
65
  config.set('defaultModel', cfg.defaultModel);
60
- stderrLogger.info(`applied shared config (rev ${cfg.revision})`);
66
+ stderrLogger.info(`sourced shared config from hub (rev ${cfg.revision}) — ${live} provider(s) live`);
61
67
  };
62
68
  const federation = new Federation({
63
69
  kernel,
64
70
  identity,
65
71
  describe,
66
72
  logger: stderrLogger,
67
- options: { version, port, token, autoUpdate: fed.autoUpdate, applyConfig },
73
+ options: { version, port, token, autoUpdate: fed.autoUpdate, applyConfig, autoPullConfig: role === 'satellite' },
68
74
  });
75
+ // Hub/peer/relay: publish local providers + policy for satellites to source.
76
+ const sharedFromConf = () => ({
77
+ cloudProviders: config.get('cloudProviders'),
78
+ policy: config.get('policy'),
79
+ defaultModel: config.get('defaultModel'),
80
+ });
81
+ if (role !== 'satellite') {
82
+ federation.publishConfig(sharedFromConf());
83
+ for (const key of ['cloudProviders', 'policy', 'defaultModel']) {
84
+ config.onDidChange(key, () => federation.publishConfig(sharedFromConf()));
85
+ }
86
+ }
69
87
  try {
70
88
  await federation.start();
71
89
  }
@@ -13,7 +13,7 @@ import chalk from 'chalk';
13
13
  import { readFileSync } from 'node:fs';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { dirname, join } from 'node:path';
16
- import { buildKernelFromConfig } from '../kernel/setup.js';
16
+ import { buildKernelFromConfig, registerCloudProviders } from '../kernel/setup.js';
17
17
  import { Federation, MoltfedConnector, loadOrCreateIdentity } from '../kernel/index.js';
18
18
  function pkgVersion() {
19
19
  try {
@@ -44,15 +44,22 @@ export async function serve(config, opts) {
44
44
  providers: kernel.providerManager.listInfos().map(p => p.id),
45
45
  };
46
46
  };
47
- // Satellite: persist shared config locally so providers/keys survive a restart.
47
+ // Satellite: persist shared config AND hot-apply it to the running kernel so
48
+ // the hub's providers + their models are available immediately (no restart).
48
49
  const applyConfig = (cfg) => {
49
- if (cfg.cloudProviders)
50
- config.set('cloudProviders', cfg.cloudProviders);
51
- if (cfg.policy)
50
+ let live = 0;
51
+ if (cfg.cloudProviders) {
52
+ const providers = cfg.cloudProviders;
53
+ config.set('cloudProviders', providers);
54
+ live = registerCloudProviders(kernel, providers); // live registration → models appear now
55
+ }
56
+ if (cfg.policy) {
52
57
  config.set('policy', cfg.policy);
58
+ kernel.setDefaultPolicy(cfg.policy);
59
+ }
53
60
  if (cfg.defaultModel)
54
61
  config.set('defaultModel', cfg.defaultModel);
55
- console.log(chalk.green(` ✓ applied shared config (rev ${cfg.revision}) — restart to load new providers`));
62
+ console.log(chalk.green(` ✓ sourced shared config from hub (rev ${cfg.revision}) — ${live} provider(s) live`));
56
63
  };
57
64
  const federation = new Federation({
58
65
  kernel,
@@ -67,8 +74,22 @@ export async function serve(config, opts) {
67
74
  heartbeatMs: (fed.heartbeatSec ?? 30) * 1000,
68
75
  autoUpdate: opts.autoUpdate ?? fed.autoUpdate,
69
76
  applyConfig,
77
+ autoPullConfig: role === 'satellite',
70
78
  },
71
79
  });
80
+ // Hub/peer/relay: publish this machine's providers + policy so satellites can
81
+ // auto-source them. Re-publish when the local config changes (live).
82
+ const sharedFromConf = () => ({
83
+ cloudProviders: config.get('cloudProviders'),
84
+ policy: config.get('policy'),
85
+ defaultModel: config.get('defaultModel'),
86
+ });
87
+ if (role !== 'satellite') {
88
+ federation.publishConfig(sharedFromConf());
89
+ for (const key of ['cloudProviders', 'policy', 'defaultModel']) {
90
+ config.onDidChange(key, () => federation.publishConfig(sharedFromConf()));
91
+ }
92
+ }
72
93
  console.log(chalk.bold(`\n infinicode mesh node`));
73
94
  console.log(chalk.dim(' ' + '-'.repeat(46)));
74
95
  console.log(` node: ${chalk.cyan(identity.displayName)} ${chalk.dim('(' + identity.nodeId + ')')}`);
@@ -78,17 +99,12 @@ export async function serve(config, opts) {
78
99
  console.log(` auth: ${chalk.green('token required')}`);
79
100
  console.log();
80
101
  await federation.start();
81
- // Connect to seeds and, as a satellite, pull shared config from the first hub.
102
+ // Connect to seeds. Satellites auto-source shared config from any hub/relay
103
+ // they connect to (handled inside federation.connect()).
82
104
  for (const url of seeds) {
83
105
  const peer = await federation.connect(url);
84
- if (peer) {
106
+ if (peer)
85
107
  console.log(chalk.green(` ✓ connected to ${peer.displayName} @ ${url}`));
86
- if (role === 'satellite') {
87
- const applied = await federation.pullConfig(peer.nodeId).catch(() => false);
88
- if (applied)
89
- console.log(chalk.green(` ✓ pulled shared config from ${peer.displayName}`));
90
- }
91
- }
92
108
  }
93
109
  // Optional: auto-discover peers over Tailscale (no --seed needed).
94
110
  if (opts.tailscale) {
@@ -11,7 +11,7 @@
11
11
  * (heartbeat / task.dispatch / command / config / update).
12
12
  */
13
13
  import type { Capability, KernelLike, Logger } from '../types.js';
14
- import type { HardwareSnapshot, NodeIdentity, NodeStatus, SharedConfig, StreamFrame } from './types.js';
14
+ import type { HardwareSnapshot, NodeIdentity, NodeStatus, PeerInfo, SharedConfig, StreamFrame } from './types.js';
15
15
  import { ConfigSync } from './config-sync.js';
16
16
  import { AutoUpdate } from './auto-update.js';
17
17
  import { ComputeRouter, type ComputeNeed } from './compute-router.js';
@@ -36,6 +36,10 @@ export interface FederationOptions {
36
36
  autoUpdate?: boolean;
37
37
  /** Satellite: how to apply an incoming shared config (register providers etc.). */
38
38
  applyConfig?: (cfg: SharedConfig) => void | Promise<void>;
39
+ /** Satellite: auto-pull shared config from any hub/relay we connect to. */
40
+ autoPullConfig?: boolean;
41
+ /** Satellite: how often to re-pull shared config (ms, default 30000; 0 disables). */
42
+ configPullMs?: number;
39
43
  }
40
44
  export interface FederationDeps {
41
45
  kernel: KernelLike;
@@ -57,6 +61,9 @@ export declare class Federation {
57
61
  private lan?;
58
62
  /** nodeIds we're currently dialing via LAN discovery (dedup guard). */
59
63
  private lanConnecting;
64
+ /** nodeIds we're currently dialing back after an inbound heartbeat/hello. */
65
+ private inboundConnecting;
66
+ private configPullTimer?;
60
67
  readonly configSync: ConfigSync;
61
68
  readonly autoUpdate: AutoUpdate;
62
69
  readonly compute: ComputeRouter;
@@ -109,7 +116,19 @@ export declare class Federation {
109
116
  publishConfig(cfg: Omit<SharedConfig, 'revision' | 'updatedAt'>): SharedConfig;
110
117
  /** Satellite: pull shared config from a peer (usually the hub). */
111
118
  pullConfig(nodeId: string): Promise<boolean>;
112
- connect(url: string): Promise<import("./types.js").PeerInfo | null>;
119
+ connect(url: string): Promise<PeerInfo | null>;
120
+ /**
121
+ * Register an inbound peer that dialed US (learned from its heartbeat/hello),
122
+ * so dispatch works regardless of who connected first. The hub dials the
123
+ * sender back (→ full manifest + stream), making a satellite that only
124
+ * `--seed`s the hub visible + dispatchable. Gated: a satellite never dials
125
+ * back (accept-only, never dials home).
126
+ */
127
+ private learnInboundPeer;
128
+ /** Satellite: pull shared config from a freshly-connected hub/relay. */
129
+ private maybeAutoPull;
130
+ /** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
131
+ private autoPullFromHub;
113
132
  /** Auto-discover peers over Tailscale and connect to each (skips self/known). */
114
133
  discoverAndConnect(tag?: string): Promise<number>;
115
134
  /**
@@ -120,7 +139,7 @@ export declare class Federation {
120
139
  startLanDiscovery(tag?: string, discoveryPort?: number): void;
121
140
  /** Neutral node-status list (self + peers) — feeds the mesh map. */
122
141
  nodeStatuses(): NodeStatus[];
123
- peers(): import("./types.js").PeerInfo[];
142
+ peers(): PeerInfo[];
124
143
  /** Latest local hardware snapshot (for the MOLTFED connector / panels). */
125
144
  latestHardware(): HardwareSnapshot | undefined;
126
145
  /** Subscribe to the merged mesh frame stream (local + peer frames). */
@@ -24,6 +24,9 @@ export class Federation {
24
24
  lan;
25
25
  /** nodeIds we're currently dialing via LAN discovery (dedup guard). */
26
26
  lanConnecting = new Set();
27
+ /** nodeIds we're currently dialing back after an inbound heartbeat/hello. */
28
+ inboundConnecting = new Set();
29
+ configPullTimer;
27
30
  configSync;
28
31
  autoUpdate;
29
32
  compute = new ComputeRouter();
@@ -57,6 +60,7 @@ export class Federation {
57
60
  token: options.token,
58
61
  logger,
59
62
  getManifest: () => buildManifest(identity, this.deps.describe()),
63
+ getNodes: () => this.nodeStatuses(),
60
64
  onRpc: (env, remote) => this.onRpc(env, remote),
61
65
  });
62
66
  await this.server.start();
@@ -66,6 +70,7 @@ export class Federation {
66
70
  logger,
67
71
  heartbeatMs: options.heartbeatMs,
68
72
  getLoad: () => loadFromSnapshot(this.lastHw),
73
+ selfPort: port,
69
74
  onFrame: (peerId, f) => this.ingestFrame(peerId, f),
70
75
  onPeerChange: () => this.emitLocal('nd', frame('nd', identity.nodeId, this.nodeStatuses())),
71
76
  });
@@ -82,18 +87,25 @@ export class Federation {
82
87
  this.lastHw = this.telemetry.collect();
83
88
  this.server?.broadcast(frame('hw', identity.nodeId, this.lastHw));
84
89
  }, telMs);
85
- // Connect to seed peers.
90
+ // Connect to seed peers (via this.connect so satellites auto-pull config).
86
91
  for (const url of options.seeds ?? [])
87
- void this.mesh.connect(url);
92
+ void this.connect(url);
93
+ // Satellite: periodically re-pull shared config so hub changes (new
94
+ // providers/keys/policy) propagate without a reconnect.
95
+ if (options.autoPullConfig && options.configPullMs !== 0) {
96
+ this.configPullTimer = setInterval(() => void this.autoPullFromHub(), options.configPullMs ?? 30_000);
97
+ }
88
98
  logger.info(`[federation] node ${identity.displayName} (${identity.role}) online on :${port}`);
89
99
  }
90
100
  // ── Inbound RPC ────────────────────────────────────────────────────────────
91
- async onRpc(env, _remote) {
101
+ async onRpc(env, remote) {
92
102
  const self = this.deps.identity.nodeId;
93
103
  switch (env.kind) {
94
104
  case 'heartbeat':
105
+ this.learnInboundPeer(env, remote);
95
106
  return reply(env, 'ack', self, { load: loadFromSnapshot(this.lastHw) });
96
107
  case 'hello':
108
+ this.learnInboundPeer(env, remote);
97
109
  return reply(env, 'welcome', self, buildManifest(this.deps.identity, this.deps.describe()));
98
110
  case 'task.dispatch': {
99
111
  // Non-blocking handoff: register the run, ack with a runId immediately,
@@ -275,7 +287,54 @@ export class Federation {
275
287
  return false;
276
288
  }
277
289
  async connect(url) {
278
- return this.mesh?.connect(url) ?? null;
290
+ const peer = (await this.mesh?.connect(url)) ?? null;
291
+ if (peer)
292
+ await this.maybeAutoPull(peer);
293
+ return peer;
294
+ }
295
+ /**
296
+ * Register an inbound peer that dialed US (learned from its heartbeat/hello),
297
+ * so dispatch works regardless of who connected first. The hub dials the
298
+ * sender back (→ full manifest + stream), making a satellite that only
299
+ * `--seed`s the hub visible + dispatchable. Gated: a satellite never dials
300
+ * back (accept-only, never dials home).
301
+ */
302
+ learnInboundPeer(env, remote) {
303
+ if (this.deps.identity.role === 'satellite')
304
+ return; // accept-only
305
+ const from = env.from;
306
+ if (!from || from === this.deps.identity.nodeId)
307
+ return;
308
+ if (this.mesh?.get(from))
309
+ return; // already tracked
310
+ if (this.inboundConnecting.has(from))
311
+ return; // dial in flight
312
+ const port = env.data?.port;
313
+ const ip = normalizeIp(remote);
314
+ if (!port || !ip)
315
+ return;
316
+ const url = `http://${ip}:${port}`;
317
+ this.inboundConnecting.add(from);
318
+ this.deps.logger.info(`[federation] learned inbound peer ${from} → dialing back ${url}`);
319
+ void this.connect(url)
320
+ .catch(() => { })
321
+ .finally(() => this.inboundConnecting.delete(from));
322
+ }
323
+ /** Satellite: pull shared config from a freshly-connected hub/relay. */
324
+ async maybeAutoPull(peer) {
325
+ if (!this.deps.options.autoPullConfig)
326
+ return;
327
+ if (peer.role !== 'hub' && peer.role !== 'relay')
328
+ return;
329
+ const applied = await this.pullConfig(peer.nodeId).catch(() => false);
330
+ if (applied)
331
+ this.deps.logger.info(`[federation] pulled shared config from ${peer.displayName}`);
332
+ }
333
+ /** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
334
+ async autoPullFromHub() {
335
+ const hub = (this.mesh?.connected() ?? []).find(p => p.role === 'hub' || p.role === 'relay');
336
+ if (hub)
337
+ await this.maybeAutoPull(hub);
279
338
  }
280
339
  /** Auto-discover peers over Tailscale and connect to each (skips self/known). */
281
340
  async discoverAndConnect(tag) {
@@ -283,7 +342,7 @@ export class Federation {
283
342
  const urls = await discoverTailscalePeers({ port, tag, logger: this.deps.logger });
284
343
  let connected = 0;
285
344
  for (const url of urls) {
286
- const peer = await this.mesh?.connect(url);
345
+ const peer = await this.connect(url);
287
346
  if (peer && peer.nodeId !== this.deps.identity.nodeId)
288
347
  connected++;
289
348
  }
@@ -315,8 +374,7 @@ export class Federation {
315
374
  if (this.lanConnecting.has(nodeId))
316
375
  return; // dial in flight
317
376
  this.lanConnecting.add(nodeId);
318
- void this.mesh
319
- ?.connect(url)
377
+ void this.connect(url)
320
378
  .then(p => {
321
379
  if (p)
322
380
  logger.info(`[federation] lan discovered ${p.displayName ?? name} @ ${url}`);
@@ -370,9 +428,24 @@ export class Federation {
370
428
  this.unsub?.();
371
429
  this.bridge?.stop();
372
430
  this.lan?.stop();
431
+ if (this.configPullTimer)
432
+ clearInterval(this.configPullTimer);
373
433
  if (this.telemetryTimer)
374
434
  clearInterval(this.telemetryTimer);
375
435
  this.mesh?.stop();
376
436
  await this.server?.stop();
377
437
  }
378
438
  }
439
+ /** Strip IPv6-mapped prefixes so an inbound remoteAddress becomes a diallable IPv4 host. */
440
+ function normalizeIp(remote) {
441
+ if (!remote)
442
+ return null;
443
+ let ip = remote.trim();
444
+ if (ip === '::1')
445
+ return '127.0.0.1';
446
+ if (ip.startsWith('::ffff:'))
447
+ ip = ip.slice(7);
448
+ if (ip.includes(':'))
449
+ return null; // raw IPv6 — skip (use --lan/--tailscale to dial proactively)
450
+ return ip;
451
+ }
@@ -23,6 +23,8 @@ export interface PeerMeshOptions {
23
23
  onPeerChange?: () => void;
24
24
  /** Current local load 0–100, sent with heartbeats. */
25
25
  getLoad: () => number;
26
+ /** Our own mesh port — advertised in heartbeats so hubs can dial us back. */
27
+ selfPort?: number;
26
28
  }
27
29
  export declare class PeerMesh {
28
30
  private opts;
@@ -34,6 +34,11 @@ export class PeerMesh {
34
34
  };
35
35
  this.peers.set(peer.nodeId, peer);
36
36
  this.openStream(peer);
37
+ // Immediate heartbeat so the peer learns us (nodeId + port) right away —
38
+ // lets a hub register + dial us back without waiting a full heartbeat cycle.
39
+ void this.opts.client
40
+ .rpc(url, envelope('heartbeat', this.opts.self.nodeId, { load: this.opts.getLoad(), port: this.opts.selfPort }, { to: peer.nodeId }))
41
+ .catch(() => { });
37
42
  this.opts.logger.info(`[federation] connected to ${peer.displayName} (${peer.nodeId}) @ ${url}`);
38
43
  this.opts.onPeerChange?.();
39
44
  return peer;
@@ -67,7 +72,7 @@ export class PeerMesh {
67
72
  await Promise.all(this.list().map(async (peer) => {
68
73
  const t0 = Date.now();
69
74
  try {
70
- await this.opts.client.rpc(peer.url, envelope('heartbeat', this.opts.self.nodeId, { load: this.opts.getLoad() }, { to: peer.nodeId }));
75
+ await this.opts.client.rpc(peer.url, envelope('heartbeat', this.opts.self.nodeId, { load: this.opts.getLoad(), port: this.opts.selfPort }, { to: peer.nodeId }));
71
76
  peer.latencyMs = Date.now() - t0;
72
77
  peer.lastSeenAt = Date.now();
73
78
  if (!peer.connected) {
@@ -7,6 +7,8 @@ export interface MeshServerOptions {
7
7
  /** Optional shared token; requests must present it as Bearer auth. */
8
8
  token?: string;
9
9
  getManifest: () => NodeManifest;
10
+ /** Optional: full mesh view (self + peers) for the GET /fed/nodes probe. */
11
+ getNodes?: () => unknown;
10
12
  onRpc: RpcHandler;
11
13
  logger: Logger;
12
14
  }
@@ -69,6 +69,11 @@ export class MeshServer {
69
69
  res.end(JSON.stringify(this.opts.getManifest()));
70
70
  return;
71
71
  }
72
+ if (req.method === 'GET' && url.startsWith('/fed/nodes')) {
73
+ res.writeHead(200, { 'content-type': 'application/json' });
74
+ res.end(JSON.stringify(this.opts.getNodes?.() ?? []));
75
+ return;
76
+ }
72
77
  if (req.method === 'GET' && url.startsWith('/fed/stream')) {
73
78
  this.openStream(req, res);
74
79
  return;
@@ -6,8 +6,15 @@
6
6
  import type Conf from 'conf';
7
7
  import { Kernel } from '../kernel/index.js';
8
8
  import type { Logger } from '../kernel/index.js';
9
- import type { InfinicodeConfig } from './config-schema.js';
9
+ import type { InfinicodeConfig, CloudProviderConfig } from './config-schema.js';
10
10
  export type { InfinicodeConfig, CloudProviderConfig, WorkerModelPref } from './config-schema.js';
11
+ /**
12
+ * Register (or re-register) cloud providers on a live kernel from config.
13
+ * Used at build time and for satellite hot-apply of hub-shared config, so new
14
+ * providers + their models become available without a restart. Returns the
15
+ * count of enabled providers registered.
16
+ */
17
+ export declare function registerCloudProviders(kernel: Kernel, cloudProviders: CloudProviderConfig[]): number;
11
18
  export declare function buildKernelFromConfig(config: Conf<InfinicodeConfig>, options?: {
12
19
  logger?: Logger;
13
20
  }): Kernel;
@@ -1,20 +1,13 @@
1
1
  import { Kernel, OllamaProvider, OpenAICompatibleProvider, GeminiProvider } from '../kernel/index.js';
2
2
  import { getProviderPreset } from './free-providers.js';
3
- export function buildKernelFromConfig(config, options) {
4
- const masterUrl = config.get('masterUrl');
5
- const tailscaleMasterUrl = config.get('tailscaleMasterUrl');
6
- const defaultModel = config.get('defaultModel');
7
- const ollamaOptions = {
8
- id: 'ollama',
9
- name: 'Ollama',
10
- baseURL: masterUrl,
11
- fallbackURL: tailscaleMasterUrl,
12
- defaultModel,
13
- };
14
- const kernel = new Kernel({ loadBuiltinWorkers: true, logger: options?.logger });
15
- kernel.registerProvider('ollama', new OllamaProvider(ollamaOptions));
16
- // Register cloud providers
17
- const cloudProviders = config.get('cloudProviders') ?? [];
3
+ /**
4
+ * Register (or re-register) cloud providers on a live kernel from config.
5
+ * Used at build time and for satellite hot-apply of hub-shared config, so new
6
+ * providers + their models become available without a restart. Returns the
7
+ * count of enabled providers registered.
8
+ */
9
+ export function registerCloudProviders(kernel, cloudProviders) {
10
+ let n = 0;
18
11
  for (const cfg of cloudProviders) {
19
12
  if (!cfg.enabled)
20
13
  continue;
@@ -28,6 +21,7 @@ export function buildKernelFromConfig(config, options) {
28
21
  knownModels: preset?.knownModels ?? [],
29
22
  };
30
23
  kernel.registerProvider(cfg.id, new GeminiProvider(geminiOptions));
24
+ n++;
31
25
  continue;
32
26
  }
33
27
  const options = {
@@ -39,7 +33,25 @@ export function buildKernelFromConfig(config, options) {
39
33
  knownModels: preset?.knownModels ?? [],
40
34
  };
41
35
  kernel.registerProvider(cfg.id, new OpenAICompatibleProvider(options));
36
+ n++;
42
37
  }
38
+ return n;
39
+ }
40
+ export function buildKernelFromConfig(config, options) {
41
+ const masterUrl = config.get('masterUrl');
42
+ const tailscaleMasterUrl = config.get('tailscaleMasterUrl');
43
+ const defaultModel = config.get('defaultModel');
44
+ const ollamaOptions = {
45
+ id: 'ollama',
46
+ name: 'Ollama',
47
+ baseURL: masterUrl,
48
+ fallbackURL: tailscaleMasterUrl,
49
+ defaultModel,
50
+ };
51
+ const kernel = new Kernel({ loadBuiltinWorkers: true, logger: options?.logger });
52
+ kernel.registerProvider('ollama', new OllamaProvider(ollamaOptions));
53
+ // Register cloud providers
54
+ registerCloudProviders(kernel, config.get('cloudProviders') ?? []);
43
55
  // Apply per-worker model pins — the worker runtime honors them when routing.
44
56
  const workerModels = config.get('workerModels') ?? {};
45
57
  if (workerModels && Object.keys(workerModels).length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",