infinicode 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,46 @@ infinicode mission status <id>
90
90
  infinicode mission list
91
91
  ```
92
92
 
93
+ ### As a device mesh (multi-device, new in v2)
94
+
95
+ Install infinicode on every machine — laptop, workstation, Raspberry&nbsp;Pi — and they become one fleet. Spawn AI agents on any device from any device (no SSH), stream activity/hardware back, and drive it all from an MCP host like Claude Code or InfiniBot.
96
+
97
+ ```bash
98
+ # install on each device
99
+ npm install -g infinicode
100
+
101
+ # same LAN — zero config: each node broadcasts a UDP beacon and auto-connects
102
+ infinicode serve --hub --lan # your main machine
103
+ infinicode serve --role satellite --lan # a Pi / other box
104
+
105
+ # across LAN + remote — auto-discover over Tailscale
106
+ infinicode serve --hub --tailscale
107
+ infinicode serve --role satellite --tailscale --tag tag:robopark
108
+
109
+ # or point at exact peers
110
+ infinicode serve --role satellite --seed http://192.168.1.20:47913
111
+
112
+ # verify the mesh
113
+ infinicode x /nodes
114
+ ```
115
+
116
+ Drive the fleet from an MCP host — register the control server (spawn / dispatch / follow / role / voice / mesh-map tools):
117
+
118
+ ```jsonc
119
+ // ~/.claude.json
120
+ { "mcpServers": { "infinicode": { "command": "infinicode", "args": ["mcp", "--lan"] } } }
121
+ ```
122
+
123
+ Link a node onto an **InfiniBot** neural-mesh map (no InfiniBot changes needed):
124
+
125
+ ```bash
126
+ infinicode serve --hub --gateway ws://<infinibot-host>:18789 --gateway-token <token>
127
+ ```
128
+
129
+ Discovery options: `--lan` (same subnet, zero-config UDP broadcast, port 47915), `--tailscale` (LAN+remote), `--seed <url>` (exact) — they combine. Full guide: `docs/user-manual.html`; design deep-dive: `docs/federation-architecture.html`.
130
+
131
+ > The Windows TUI binaries are not shipped in the npm package (they're large and platform-specific), so `infinicode run` needs a local TUI build; the mesh commands (`serve`, `mcp`, `mission`, `console`) run everywhere as pure Node.
132
+
93
133
  ### As a library
94
134
 
95
135
  ```ts
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.0.0';
18
+ const VERSION = '2.1.0';
19
19
  const config = new Conf({
20
20
  projectName: 'infinicode',
21
21
  defaults: DEFAULT_CONFIG,
@@ -287,7 +287,9 @@ program
287
287
  .option('--seed <url...>', 'peer base URL(s) to connect to on start')
288
288
  .option('--auto-update', 'accept auto-updates announced by a hub/relay')
289
289
  .option('--tailscale', 'auto-discover + connect peers over Tailscale')
290
- .option('--tag <tag>', 'only discover peers carrying this Tailscale ACL tag')
290
+ .option('--lan', 'auto-discover + connect peers on the same LAN via UDP broadcast (no seed/Tailscale needed)')
291
+ .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
292
+ .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
291
293
  .option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
292
294
  .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
293
295
  .option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
@@ -304,7 +306,9 @@ program
304
306
  .option('--token <token>', 'shared bearer token peers must present')
305
307
  .option('--seed <url...>', 'peer base URL(s) to connect to on start')
306
308
  .option('--tailscale', 'auto-discover + connect peers over Tailscale')
307
- .option('--tag <tag>', 'only discover peers carrying this Tailscale ACL tag')
309
+ .option('--lan', 'auto-discover + connect peers on the same LAN via UDP broadcast (no seed/Tailscale needed)')
310
+ .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
311
+ .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
308
312
  .action(async (opts) => {
309
313
  await mcpCommand(config, opts);
310
314
  });
@@ -8,5 +8,7 @@ export interface McpOptions {
8
8
  seed?: string[];
9
9
  tailscale?: boolean;
10
10
  tag?: string;
11
+ lan?: boolean;
12
+ lanPort?: string;
11
13
  }
12
14
  export declare function mcpCommand(config: Conf<InfinicodeConfig>, opts: McpOptions): Promise<void>;
@@ -81,6 +81,10 @@ export async function mcpCommand(config, opts) {
81
81
  const n = await federation.discoverAndConnect(opts.tag).catch(() => 0);
82
82
  stderrLogger.info(`tailscale discovery connected ${n} peer(s)`);
83
83
  }
84
+ if (opts.lan ?? fed.lan) {
85
+ federation.startLanDiscovery(opts.tag, opts.lanPort ? parseInt(opts.lanPort, 10) : fed.lanPort);
86
+ stderrLogger.info('LAN auto-discovery on (udp broadcast)');
87
+ }
84
88
  // Proactive autonomy: a goal loop the MCP host can populate (add_goal/…).
85
89
  const autonomy = new GoalLoop({ kernel, logger: stderrLogger });
86
90
  autonomy.start();
@@ -11,6 +11,8 @@ export interface ServeOptions {
11
11
  hub?: boolean;
12
12
  tailscale?: boolean;
13
13
  tag?: string;
14
+ lan?: boolean;
15
+ lanPort?: string;
14
16
  gateway?: string;
15
17
  gatewayToken?: string;
16
18
  gatewayPassword?: string;
@@ -95,6 +95,11 @@ export async function serve(config, opts) {
95
95
  const n = await federation.discoverAndConnect(opts.tag).catch(() => 0);
96
96
  console.log(chalk.green(` ✓ tailscale discovery connected ${n} peer(s)`));
97
97
  }
98
+ // Optional: zero-config LAN discovery — auto-connect peers on this subnet.
99
+ if (opts.lan ?? fed.lan) {
100
+ federation.startLanDiscovery(opts.tag, opts.lanPort ? parseInt(opts.lanPort, 10) : fed.lanPort);
101
+ console.log(chalk.green(` ✓ LAN auto-discovery on (udp broadcast) — peers on this subnet connect automatically`));
102
+ }
98
103
  // Optional: log this node into an InfiniBot gateway so it renders live on the
99
104
  // neural-mesh map + hardware panels (no InfiniBot changes needed).
100
105
  let connector;
@@ -37,6 +37,10 @@ export interface FederationConfig {
37
37
  heartbeatSec?: number;
38
38
  /** Accept + apply auto-updates announced by a hub/relay. */
39
39
  autoUpdate?: boolean;
40
+ /** Zero-config LAN discovery: broadcast + auto-connect peers on this subnet. */
41
+ lan?: boolean;
42
+ /** UDP discovery port for LAN discovery (default 47915). */
43
+ lanPort?: number;
40
44
  }
41
45
  export interface InfinicodeConfig {
42
46
  masterUrl: string;
@@ -54,6 +54,9 @@ export declare class Federation {
54
54
  private telemetryTimer?;
55
55
  private lastHw?;
56
56
  private unsub?;
57
+ private lan?;
58
+ /** nodeIds we're currently dialing via LAN discovery (dedup guard). */
59
+ private lanConnecting;
57
60
  readonly configSync: ConfigSync;
58
61
  readonly autoUpdate: AutoUpdate;
59
62
  readonly compute: ComputeRouter;
@@ -109,6 +112,12 @@ export declare class Federation {
109
112
  connect(url: string): Promise<import("./types.js").PeerInfo | null>;
110
113
  /** Auto-discover peers over Tailscale and connect to each (skips self/known). */
111
114
  discoverAndConnect(tag?: string): Promise<number>;
115
+ /**
116
+ * Start zero-config LAN discovery: broadcast a beacon on the local subnet and
117
+ * auto-connect to any peer we hear (no --seed / Tailscale needed). Continuous —
118
+ * new nodes that come online later are picked up too. Idempotent.
119
+ */
120
+ startLanDiscovery(tag?: string, discoveryPort?: number): void;
112
121
  /** Neutral node-status list (self + peers) — feeds the mesh map. */
113
122
  nodeStatuses(): NodeStatus[];
114
123
  peers(): import("./types.js").PeerInfo[];
@@ -9,6 +9,7 @@ import { buildManifest } from './node-identity.js';
9
9
  import { envelope, reply, frame, newId } from './protocol.js';
10
10
  import { loadRole, saveRole, rolePreamble } from './role-context.js';
11
11
  import { discoverTailscalePeers } from './discovery.js';
12
+ import { LanBeacon } from './lan-discovery.js';
12
13
  import { DEFAULT_MESH_PORT } from './types.js';
13
14
  export class Federation {
14
15
  deps;
@@ -20,6 +21,9 @@ export class Federation {
20
21
  telemetryTimer;
21
22
  lastHw;
22
23
  unsub;
24
+ lan;
25
+ /** nodeIds we're currently dialing via LAN discovery (dedup guard). */
26
+ lanConnecting = new Set();
23
27
  configSync;
24
28
  autoUpdate;
25
29
  compute = new ComputeRouter();
@@ -285,6 +289,44 @@ export class Federation {
285
289
  }
286
290
  return connected;
287
291
  }
292
+ /**
293
+ * Start zero-config LAN discovery: broadcast a beacon on the local subnet and
294
+ * auto-connect to any peer we hear (no --seed / Tailscale needed). Continuous —
295
+ * new nodes that come online later are picked up too. Idempotent.
296
+ */
297
+ startLanDiscovery(tag, discoveryPort) {
298
+ if (this.lan)
299
+ return;
300
+ const { identity, logger, options } = this.deps;
301
+ this.lan = new LanBeacon({
302
+ nodeId: identity.nodeId,
303
+ name: identity.displayName,
304
+ role: identity.role,
305
+ meshPort: options.port ?? DEFAULT_MESH_PORT,
306
+ tag,
307
+ auth: !!options.token,
308
+ discoveryPort,
309
+ logger,
310
+ onPeer: ({ nodeId, url, name }) => {
311
+ if (nodeId === identity.nodeId)
312
+ return; // self
313
+ if (this.mesh?.get(nodeId))
314
+ return; // already connected
315
+ if (this.lanConnecting.has(nodeId))
316
+ return; // dial in flight
317
+ this.lanConnecting.add(nodeId);
318
+ void this.mesh
319
+ ?.connect(url)
320
+ .then(p => {
321
+ if (p)
322
+ logger.info(`[federation] lan discovered ${p.displayName ?? name} @ ${url}`);
323
+ })
324
+ .catch(() => { })
325
+ .finally(() => this.lanConnecting.delete(nodeId));
326
+ },
327
+ });
328
+ this.lan.start();
329
+ }
288
330
  // ── Views ────────────────────────────────────────────────────────────────
289
331
  /** Neutral node-status list (self + peers) — feeds the mesh map. */
290
332
  nodeStatuses() {
@@ -327,6 +369,7 @@ export class Federation {
327
369
  async stop() {
328
370
  this.unsub?.();
329
371
  this.bridge?.stop();
372
+ this.lan?.stop();
330
373
  if (this.telemetryTimer)
331
374
  clearInterval(this.telemetryTimer);
332
375
  this.mesh?.stop();
@@ -25,6 +25,8 @@ export type { ComputeCandidate, ComputeNeed } from './compute-router.js';
25
25
  export { loadRole, saveRole, rolePreamble } from './role-context.js';
26
26
  export { discoverTailscalePeers, tailscaleAvailable } from './discovery.js';
27
27
  export type { DiscoverOptions } from './discovery.js';
28
+ export { LanBeacon, DEFAULT_LAN_DISCOVERY_PORT } from './lan-discovery.js';
29
+ export type { LanBeaconOptions, DiscoveredLanPeer } from './lan-discovery.js';
28
30
  export { envelope, reply, frame, frameToSSE, parseSSE, newId } from './protocol.js';
29
31
  export * as moltfed from './moltfed-adapter.js';
30
32
  export { MoltfedConnector } from './moltfed-connector.js';
@@ -18,6 +18,7 @@ export { AutoUpdate, compareVersions } from './auto-update.js';
18
18
  export { ComputeRouter } from './compute-router.js';
19
19
  export { loadRole, saveRole, rolePreamble } from './role-context.js';
20
20
  export { discoverTailscalePeers, tailscaleAvailable } from './discovery.js';
21
+ export { LanBeacon, DEFAULT_LAN_DISCOVERY_PORT } from './lan-discovery.js';
21
22
  export { envelope, reply, frame, frameToSSE, parseSSE, newId } from './protocol.js';
22
23
  export * as moltfed from './moltfed-adapter.js';
23
24
  export { MoltfedConnector } from './moltfed-connector.js';
@@ -0,0 +1,43 @@
1
+ import type { Logger } from '../types.js';
2
+ export declare const DEFAULT_LAN_DISCOVERY_PORT = 47915;
3
+ export interface DiscoveredLanPeer {
4
+ nodeId: string;
5
+ name: string;
6
+ role: string;
7
+ url: string;
8
+ address: string;
9
+ tag?: string;
10
+ }
11
+ export interface LanBeaconOptions {
12
+ nodeId: string;
13
+ name: string;
14
+ role: string;
15
+ /** The mesh (HTTP+SSE) port this node advertises. */
16
+ meshPort: number;
17
+ /** Only match peers carrying this tag (mirrors Tailscale --tag). */
18
+ tag?: string;
19
+ /** Whether this node's mesh requires a token (informational only). */
20
+ auth?: boolean;
21
+ /** UDP port to broadcast/listen on (default 47915). */
22
+ discoveryPort?: number;
23
+ /** Broadcast cadence (default 5s). */
24
+ intervalMs?: number;
25
+ logger?: Logger;
26
+ /** Called when a *different* node's beacon is heard. */
27
+ onPeer: (peer: DiscoveredLanPeer) => void;
28
+ }
29
+ /** Periodically broadcasts a discovery beacon and reports peers it hears. */
30
+ export declare class LanBeacon {
31
+ private opts;
32
+ private sock?;
33
+ private timer?;
34
+ private readonly port;
35
+ private readonly intervalMs;
36
+ private stopped;
37
+ constructor(opts: LanBeaconOptions);
38
+ start(): void;
39
+ private beaconPayload;
40
+ private broadcast;
41
+ private onMessage;
42
+ stop(): void;
43
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * OpenKernel — LAN Peer Discovery (UDP broadcast)
3
+ *
4
+ * Zero-config, dependency-free same-subnet discovery. Each node periodically
5
+ * broadcasts a tiny beacon (nodeId, name, role, mesh port, optional tag) over
6
+ * UDP; any node that hears a beacon learns a peer's mesh URL and connects. No
7
+ * --seed, no Tailscale needed on a shared LAN.
8
+ *
9
+ * Broadcast packets are NOT forwarded by routers, so this stays within the local
10
+ * subnet by design — discovery only ever reaches physically co-located, trusted
11
+ * machines. Actual mesh RPC is still gated by the shared --token if one is set.
12
+ */
13
+ import dgram from 'node:dgram';
14
+ import os from 'node:os';
15
+ export const DEFAULT_LAN_DISCOVERY_PORT = 47915;
16
+ const MAGIC = 'ICMESH1';
17
+ /** Periodically broadcasts a discovery beacon and reports peers it hears. */
18
+ export class LanBeacon {
19
+ opts;
20
+ sock;
21
+ timer;
22
+ port;
23
+ intervalMs;
24
+ stopped = false;
25
+ constructor(opts) {
26
+ this.opts = opts;
27
+ this.port = opts.discoveryPort ?? DEFAULT_LAN_DISCOVERY_PORT;
28
+ this.intervalMs = opts.intervalMs ?? 5000;
29
+ }
30
+ start() {
31
+ const sock = dgram.createSocket({ type: 'udp4', reuseAddr: true });
32
+ this.sock = sock;
33
+ sock.on('error', err => {
34
+ this.opts.logger?.debug?.(`[federation] lan discovery socket error: ${err.message}`);
35
+ });
36
+ sock.on('message', (buf, rinfo) => this.onMessage(buf, rinfo));
37
+ sock.bind(this.port, () => {
38
+ try {
39
+ sock.setBroadcast(true);
40
+ }
41
+ catch {
42
+ /* some platforms disallow; best-effort */
43
+ }
44
+ this.broadcast();
45
+ this.timer = setInterval(() => this.broadcast(), this.intervalMs);
46
+ this.opts.logger?.info?.(`[federation] lan discovery listening on udp/${this.port}`);
47
+ });
48
+ }
49
+ beaconPayload() {
50
+ const wire = {
51
+ t: MAGIC,
52
+ v: 1,
53
+ nodeId: this.opts.nodeId,
54
+ name: this.opts.name,
55
+ role: this.opts.role,
56
+ meshPort: this.opts.meshPort,
57
+ tag: this.opts.tag,
58
+ auth: this.opts.auth,
59
+ };
60
+ return Buffer.from(JSON.stringify(wire));
61
+ }
62
+ broadcast() {
63
+ if (this.stopped || !this.sock)
64
+ return;
65
+ const msg = this.beaconPayload();
66
+ for (const addr of broadcastAddresses()) {
67
+ this.sock.send(msg, this.port, addr, err => {
68
+ if (err)
69
+ this.opts.logger?.debug?.(`[federation] lan beacon send ${addr} failed: ${err.message}`);
70
+ });
71
+ }
72
+ }
73
+ onMessage(buf, rinfo) {
74
+ let data;
75
+ try {
76
+ data = JSON.parse(buf.toString('utf8'));
77
+ }
78
+ catch {
79
+ return;
80
+ }
81
+ if (!data || data.t !== MAGIC)
82
+ return;
83
+ if (typeof data.nodeId !== 'string' || typeof data.meshPort !== 'number')
84
+ return;
85
+ if (data.nodeId === this.opts.nodeId)
86
+ return; // ignore our own beacon
87
+ if (this.opts.tag && data.tag !== this.opts.tag)
88
+ return; // tag filter
89
+ const address = rinfo.address;
90
+ this.opts.onPeer({
91
+ nodeId: data.nodeId,
92
+ name: data.name ?? data.nodeId,
93
+ role: data.role ?? 'peer',
94
+ url: `http://${address}:${data.meshPort}`,
95
+ address,
96
+ tag: data.tag,
97
+ });
98
+ }
99
+ stop() {
100
+ this.stopped = true;
101
+ if (this.timer)
102
+ clearInterval(this.timer);
103
+ try {
104
+ this.sock?.close();
105
+ }
106
+ catch {
107
+ /* ignore */
108
+ }
109
+ this.sock = undefined;
110
+ }
111
+ }
112
+ /** Limited broadcast + each interface's directed broadcast address (IPv4). */
113
+ function broadcastAddresses() {
114
+ const addrs = new Set(['255.255.255.255']);
115
+ for (const list of Object.values(os.networkInterfaces())) {
116
+ for (const ni of list ?? []) {
117
+ // Node 20 reports family as 'IPv4'; guard the numeric form defensively.
118
+ if (ni.family !== 'IPv4' && ni.family !== 4)
119
+ continue;
120
+ if (ni.internal)
121
+ continue;
122
+ const b = directedBroadcast(ni.address, ni.netmask);
123
+ if (b)
124
+ addrs.add(b);
125
+ }
126
+ }
127
+ return [...addrs];
128
+ }
129
+ function directedBroadcast(ip, mask) {
130
+ const ipp = ip.split('.').map(Number);
131
+ const mp = mask.split('.').map(Number);
132
+ if (ipp.length !== 4 || mp.length !== 4 || [...ipp, ...mp].some(n => Number.isNaN(n)))
133
+ return null;
134
+ return ipp.map((o, i) => (o & mp[i]) | (~mp[i] & 0xff)).join('.');
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
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",