infinicode 1.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.
Files changed (159) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +120 -0
  2. package/.opencode/plugins/routing-mode-display.tsx +135 -0
  3. package/.opencode/themes/infinibot-gold.json +222 -0
  4. package/.opencode/tui.json +8 -0
  5. package/README.md +564 -72
  6. package/dist/ascii-video-animation.d.ts +2 -0
  7. package/dist/ascii-video-animation.js +243 -0
  8. package/dist/cli.js +199 -50
  9. package/dist/commands/console.d.ts +6 -0
  10. package/dist/commands/console.js +111 -0
  11. package/dist/commands/kernel-setup.d.ts +3 -0
  12. package/dist/commands/kernel-setup.js +303 -0
  13. package/dist/commands/mcp.d.ts +14 -0
  14. package/dist/commands/mcp.js +100 -0
  15. package/dist/commands/mission.d.ts +16 -0
  16. package/dist/commands/mission.js +301 -0
  17. package/dist/commands/models.d.ts +3 -1
  18. package/dist/commands/models.js +111 -55
  19. package/dist/commands/providers.d.ts +6 -0
  20. package/dist/commands/providers.js +95 -0
  21. package/dist/commands/run.d.ts +2 -1
  22. package/dist/commands/run.js +349 -59
  23. package/dist/commands/serve.d.ts +20 -0
  24. package/dist/commands/serve.js +132 -0
  25. package/dist/commands/setup.d.ts +1 -1
  26. package/dist/commands/setup.js +77 -44
  27. package/dist/commands/status.d.ts +2 -1
  28. package/dist/commands/status.js +46 -30
  29. package/dist/commands/workers.d.ts +5 -0
  30. package/dist/commands/workers.js +103 -0
  31. package/dist/kernel/autonomy/goal-loop.d.ts +49 -0
  32. package/dist/kernel/autonomy/goal-loop.js +113 -0
  33. package/dist/kernel/autonomy/index.d.ts +8 -0
  34. package/dist/kernel/autonomy/index.js +7 -0
  35. package/dist/kernel/browser/browser-controller.d.ts +57 -0
  36. package/dist/kernel/browser/browser-controller.js +175 -0
  37. package/dist/kernel/checkpoint-engine.d.ts +17 -0
  38. package/dist/kernel/checkpoint-engine.js +128 -0
  39. package/dist/kernel/command-system.d.ts +37 -0
  40. package/dist/kernel/command-system.js +239 -0
  41. package/dist/kernel/config-schema.d.ts +57 -0
  42. package/dist/kernel/config-schema.js +36 -0
  43. package/dist/kernel/event-bus.d.ts +19 -0
  44. package/dist/kernel/event-bus.js +65 -0
  45. package/dist/kernel/federation/auto-update.d.ts +36 -0
  46. package/dist/kernel/federation/auto-update.js +57 -0
  47. package/dist/kernel/federation/compute-router.d.ts +27 -0
  48. package/dist/kernel/federation/compute-router.js +44 -0
  49. package/dist/kernel/federation/config-sync.d.ts +33 -0
  50. package/dist/kernel/federation/config-sync.js +37 -0
  51. package/dist/kernel/federation/discovery.d.ts +11 -0
  52. package/dist/kernel/federation/discovery.js +44 -0
  53. package/dist/kernel/federation/event-bridge.d.ts +30 -0
  54. package/dist/kernel/federation/event-bridge.js +61 -0
  55. package/dist/kernel/federation/federation-plugin.d.ts +24 -0
  56. package/dist/kernel/federation/federation-plugin.js +35 -0
  57. package/dist/kernel/federation/federation.d.ts +135 -0
  58. package/dist/kernel/federation/federation.js +378 -0
  59. package/dist/kernel/federation/index.d.ts +33 -0
  60. package/dist/kernel/federation/index.js +24 -0
  61. package/dist/kernel/federation/lan-discovery.d.ts +43 -0
  62. package/dist/kernel/federation/lan-discovery.js +135 -0
  63. package/dist/kernel/federation/moltfed-adapter.d.ts +30 -0
  64. package/dist/kernel/federation/moltfed-adapter.js +52 -0
  65. package/dist/kernel/federation/moltfed-connector.d.ts +98 -0
  66. package/dist/kernel/federation/moltfed-connector.js +193 -0
  67. package/dist/kernel/federation/node-identity.d.ts +19 -0
  68. package/dist/kernel/federation/node-identity.js +86 -0
  69. package/dist/kernel/federation/peer-mesh.d.ts +46 -0
  70. package/dist/kernel/federation/peer-mesh.js +117 -0
  71. package/dist/kernel/federation/protocol.d.ts +20 -0
  72. package/dist/kernel/federation/protocol.js +61 -0
  73. package/dist/kernel/federation/role-context.d.ts +5 -0
  74. package/dist/kernel/federation/role-context.js +45 -0
  75. package/dist/kernel/federation/telemetry.d.ts +21 -0
  76. package/dist/kernel/federation/telemetry.js +138 -0
  77. package/dist/kernel/federation/transport-http.d.ts +44 -0
  78. package/dist/kernel/federation/transport-http.js +207 -0
  79. package/dist/kernel/federation/types.d.ts +212 -0
  80. package/dist/kernel/federation/types.js +3 -0
  81. package/dist/kernel/free-providers.d.ts +28 -0
  82. package/dist/kernel/free-providers.js +162 -0
  83. package/dist/kernel/frontend-scoring.d.ts +21 -0
  84. package/dist/kernel/frontend-scoring.js +125 -0
  85. package/dist/kernel/index.d.ts +63 -0
  86. package/dist/kernel/index.js +45 -0
  87. package/dist/kernel/kernel.d.ts +75 -0
  88. package/dist/kernel/kernel.js +223 -0
  89. package/dist/kernel/logger.d.ts +18 -0
  90. package/dist/kernel/logger.js +26 -0
  91. package/dist/kernel/mcp/index.d.ts +9 -0
  92. package/dist/kernel/mcp/index.js +8 -0
  93. package/dist/kernel/mcp/mcp-server.d.ts +27 -0
  94. package/dist/kernel/mcp/mcp-server.js +178 -0
  95. package/dist/kernel/mission-engine.d.ts +42 -0
  96. package/dist/kernel/mission-engine.js +160 -0
  97. package/dist/kernel/orchestrator.d.ts +51 -0
  98. package/dist/kernel/orchestrator.js +226 -0
  99. package/dist/kernel/plugin-manager.d.ts +28 -0
  100. package/dist/kernel/plugin-manager.js +76 -0
  101. package/dist/kernel/plugins/browser-plugin.d.ts +22 -0
  102. package/dist/kernel/plugins/browser-plugin.js +34 -0
  103. package/dist/kernel/plugins/dashboard-plugin.d.ts +17 -0
  104. package/dist/kernel/plugins/dashboard-plugin.js +72 -0
  105. package/dist/kernel/plugins/discord-plugin.d.ts +11 -0
  106. package/dist/kernel/plugins/discord-plugin.js +35 -0
  107. package/dist/kernel/plugins/metrics-plugin.d.ts +33 -0
  108. package/dist/kernel/plugins/metrics-plugin.js +86 -0
  109. package/dist/kernel/plugins/search-plugin.d.ts +17 -0
  110. package/dist/kernel/plugins/search-plugin.js +20 -0
  111. package/dist/kernel/plugins/slack-plugin.d.ts +11 -0
  112. package/dist/kernel/plugins/slack-plugin.js +35 -0
  113. package/dist/kernel/plugins/telegram-plugin.d.ts +20 -0
  114. package/dist/kernel/plugins/telegram-plugin.js +122 -0
  115. package/dist/kernel/policies.d.ts +33 -0
  116. package/dist/kernel/policies.js +105 -0
  117. package/dist/kernel/provider-discovery.d.ts +41 -0
  118. package/dist/kernel/provider-discovery.js +179 -0
  119. package/dist/kernel/provider-manager.d.ts +38 -0
  120. package/dist/kernel/provider-manager.js +206 -0
  121. package/dist/kernel/provider-url.d.ts +18 -0
  122. package/dist/kernel/provider-url.js +45 -0
  123. package/dist/kernel/providers/gemini-provider.d.ts +43 -0
  124. package/dist/kernel/providers/gemini-provider.js +203 -0
  125. package/dist/kernel/providers/ollama-provider.d.ts +44 -0
  126. package/dist/kernel/providers/ollama-provider.js +241 -0
  127. package/dist/kernel/providers/openai-compatible-provider.d.ts +43 -0
  128. package/dist/kernel/providers/openai-compatible-provider.js +233 -0
  129. package/dist/kernel/recovery-manager.d.ts +38 -0
  130. package/dist/kernel/recovery-manager.js +156 -0
  131. package/dist/kernel/router.d.ts +44 -0
  132. package/dist/kernel/router.js +222 -0
  133. package/dist/kernel/sample-missions.d.ts +11 -0
  134. package/dist/kernel/sample-missions.js +132 -0
  135. package/dist/kernel/scheduler.d.ts +30 -0
  136. package/dist/kernel/scheduler.js +147 -0
  137. package/dist/kernel/sdk/harness-sdk.d.ts +37 -0
  138. package/dist/kernel/sdk/harness-sdk.js +48 -0
  139. package/dist/kernel/sdk/plugin-sdk.d.ts +27 -0
  140. package/dist/kernel/sdk/plugin-sdk.js +40 -0
  141. package/dist/kernel/search/search-controller.d.ts +32 -0
  142. package/dist/kernel/search/search-controller.js +141 -0
  143. package/dist/kernel/setup.d.ts +13 -0
  144. package/dist/kernel/setup.js +59 -0
  145. package/dist/kernel/types.d.ts +479 -0
  146. package/dist/kernel/types.js +7 -0
  147. package/dist/kernel/verification-engine.d.ts +33 -0
  148. package/dist/kernel/verification-engine.js +287 -0
  149. package/dist/kernel/worker-runtime.d.ts +66 -0
  150. package/dist/kernel/worker-runtime.js +261 -0
  151. package/dist/kernel/workers/browser-worker.d.ts +6 -0
  152. package/dist/kernel/workers/browser-worker.js +92 -0
  153. package/dist/kernel/workers/builtin-workers.d.ts +20 -0
  154. package/dist/kernel/workers/builtin-workers.js +120 -0
  155. package/dist/kernel/workers/research-worker.d.ts +5 -0
  156. package/dist/kernel/workers/research-worker.js +90 -0
  157. package/dist/prompt-ascii-video-animation.d.ts +2 -0
  158. package/dist/prompt-ascii-video-animation.js +243 -0
  159. package/package.json +43 -9
@@ -0,0 +1,378 @@
1
+ import { MeshServer, MeshClient } from './transport-http.js';
2
+ import { PeerMesh } from './peer-mesh.js';
3
+ import { EventBridge } from './event-bridge.js';
4
+ import { ConfigSync } from './config-sync.js';
5
+ import { AutoUpdate } from './auto-update.js';
6
+ import { ComputeRouter } from './compute-router.js';
7
+ import { TelemetryCollector, loadFromSnapshot, selfNodeStatus, nodeStatusFromPeer } from './telemetry.js';
8
+ import { buildManifest } from './node-identity.js';
9
+ import { envelope, reply, frame, newId } from './protocol.js';
10
+ import { loadRole, saveRole, rolePreamble } from './role-context.js';
11
+ import { discoverTailscalePeers } from './discovery.js';
12
+ import { LanBeacon } from './lan-discovery.js';
13
+ import { DEFAULT_MESH_PORT } from './types.js';
14
+ export class Federation {
15
+ deps;
16
+ server;
17
+ client;
18
+ mesh;
19
+ bridge;
20
+ telemetry = new TelemetryCollector();
21
+ telemetryTimer;
22
+ lastHw;
23
+ unsub;
24
+ lan;
25
+ /** nodeIds we're currently dialing via LAN discovery (dedup guard). */
26
+ lanConnecting = new Set();
27
+ configSync;
28
+ autoUpdate;
29
+ compute = new ComputeRouter();
30
+ /** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */
31
+ frameSubs = new Set();
32
+ /** Recent frames for catch-up. */
33
+ frameHistory = [];
34
+ /** Runs received + executing on THIS node (async handoff). */
35
+ runs = new Map();
36
+ /** Runs WE dispatched → which peer holds them (for follow_task). */
37
+ dispatchedRuns = new Map();
38
+ constructor(deps) {
39
+ this.deps = deps;
40
+ this.client = new MeshClient({ token: deps.options.token });
41
+ this.configSync = new ConfigSync({ logger: deps.logger, apply: deps.options.applyConfig });
42
+ this.autoUpdate = new AutoUpdate({
43
+ version: deps.options.version,
44
+ logger: deps.logger,
45
+ enabled: deps.options.autoUpdate,
46
+ });
47
+ }
48
+ get identity() {
49
+ return this.deps.identity;
50
+ }
51
+ async start() {
52
+ const { logger, options, identity } = this.deps;
53
+ const port = options.port ?? DEFAULT_MESH_PORT;
54
+ this.server = new MeshServer({
55
+ port,
56
+ host: options.host,
57
+ token: options.token,
58
+ logger,
59
+ getManifest: () => buildManifest(identity, this.deps.describe()),
60
+ onRpc: (env, remote) => this.onRpc(env, remote),
61
+ });
62
+ await this.server.start();
63
+ this.mesh = new PeerMesh({
64
+ self: identity,
65
+ client: this.client,
66
+ logger,
67
+ heartbeatMs: options.heartbeatMs,
68
+ getLoad: () => loadFromSnapshot(this.lastHw),
69
+ onFrame: (peerId, f) => this.ingestFrame(peerId, f),
70
+ onPeerChange: () => this.emitLocal('nd', frame('nd', identity.nodeId, this.nodeStatuses())),
71
+ });
72
+ this.mesh.start();
73
+ // Bridge local kernel events → mesh stream (coalesced, animation-friendly).
74
+ this.bridge = new EventBridge({
75
+ nodeId: identity.nodeId,
76
+ emit: f => this.server?.broadcast(f),
77
+ });
78
+ this.unsub = this.deps.kernel.subscribeAll(e => this.bridge?.handle(e));
79
+ // Telemetry heartbeat → stream a HardwareSnapshot on a fixed cadence.
80
+ const telMs = options.telemetryMs ?? 1000;
81
+ this.telemetryTimer = setInterval(() => {
82
+ this.lastHw = this.telemetry.collect();
83
+ this.server?.broadcast(frame('hw', identity.nodeId, this.lastHw));
84
+ }, telMs);
85
+ // Connect to seed peers.
86
+ for (const url of options.seeds ?? [])
87
+ void this.mesh.connect(url);
88
+ logger.info(`[federation] node ${identity.displayName} (${identity.role}) online on :${port}`);
89
+ }
90
+ // ── Inbound RPC ────────────────────────────────────────────────────────────
91
+ async onRpc(env, _remote) {
92
+ const self = this.deps.identity.nodeId;
93
+ switch (env.kind) {
94
+ case 'heartbeat':
95
+ return reply(env, 'ack', self, { load: loadFromSnapshot(this.lastHw) });
96
+ case 'hello':
97
+ return reply(env, 'welcome', self, buildManifest(this.deps.identity, this.deps.describe()));
98
+ case 'task.dispatch': {
99
+ // Non-blocking handoff: register the run, ack with a runId immediately,
100
+ // execute in the background, and let the caller follow it.
101
+ const task = env.data;
102
+ const runId = newId();
103
+ this.runs.set(runId, { runId, status: 'running', description: task.description, startedAt: Date.now() });
104
+ void this.runTaskAsync(runId, task);
105
+ return reply(env, 'task.accepted', self, { runId });
106
+ }
107
+ case 'task.status': {
108
+ const runId = env.data.runId;
109
+ const rec = this.runs.get(runId);
110
+ return reply(env, 'task.result', self, rec ?? { runId, status: 'failed', description: '', error: 'unknown run', startedAt: 0 });
111
+ }
112
+ case 'command': {
113
+ const input = env.data?.input ?? '';
114
+ const result = await this.deps.kernel.command(input);
115
+ return reply(env, 'command.result', self, result);
116
+ }
117
+ case 'config.request':
118
+ return reply(env, 'config.offer', self, this.configSync.offer());
119
+ case 'config.offer':
120
+ await this.configSync.receive(env.data);
121
+ return reply(env, 'ack', self);
122
+ case 'update.announce': {
123
+ const peer = this.mesh?.get(env.from);
124
+ await this.autoUpdate.onAnnounce(env.data, peer?.role ?? 'peer');
125
+ return reply(env, 'ack', self);
126
+ }
127
+ case 'role.get':
128
+ return reply(env, 'ack', self, this.getRole());
129
+ case 'role.set': {
130
+ const r = env.data;
131
+ const saved = saveRole(r);
132
+ return reply(env, 'ack', self, saved);
133
+ }
134
+ default:
135
+ return reply(env, 'ack', self);
136
+ }
137
+ }
138
+ // ── Role context ───────────────────────────────────────────────────────────
139
+ getRole() {
140
+ return loadRole();
141
+ }
142
+ setRole(role) {
143
+ return saveRole(role);
144
+ }
145
+ async getRoleOf(nodeId) {
146
+ const peer = this.mesh?.get(nodeId);
147
+ if (!peer)
148
+ throw new Error(`unknown peer: ${nodeId}`);
149
+ const res = await this.client.rpc(peer.url, envelope('role.get', this.deps.identity.nodeId, {}, { to: nodeId }));
150
+ return res.data ?? null;
151
+ }
152
+ async setRoleOf(nodeId, role) {
153
+ const peer = this.mesh?.get(nodeId);
154
+ if (!peer)
155
+ throw new Error(`unknown peer: ${nodeId}`);
156
+ const res = await this.client.rpc(peer.url, envelope('role.set', this.deps.identity.nodeId, role, { to: nodeId }));
157
+ return res.data;
158
+ }
159
+ /** Execute a dispatched task on THIS node via the local kernel. */
160
+ async runTask(task) {
161
+ // Inject this device's persistent role so the spawned agent is context-aware.
162
+ const localRole = loadRole();
163
+ const preamble = rolePreamble(localRole);
164
+ const prompt = preamble ? `${preamble}\n\n${task.prompt}` : task.prompt;
165
+ const mission = await this.deps.kernel.execute({
166
+ description: task.description,
167
+ goal: task.description,
168
+ tasks: [{
169
+ description: task.description,
170
+ capabilities: task.capabilities.length ? task.capabilities : ['reasoning'],
171
+ input: { prompt, context: { role: task.role ?? localRole?.role, architecture: localRole?.architecture, ...task.context } },
172
+ }],
173
+ });
174
+ return {
175
+ missionId: mission.id,
176
+ status: mission.status,
177
+ outputs: mission.tasks.map(t => ({ status: t.status, content: t.output?.content, error: t.error })),
178
+ };
179
+ }
180
+ /** Run a dispatched task in the background, updating its RunRecord + streaming. */
181
+ async runTaskAsync(runId, task) {
182
+ const rec = this.runs.get(runId);
183
+ if (!rec)
184
+ return;
185
+ const nodeId = this.deps.identity.nodeId;
186
+ this.server?.broadcast(frame('ev', nodeId, { k: 'RUN_STARTED', run: runId, desc: task.description }));
187
+ try {
188
+ const result = await this.runTask(task);
189
+ rec.status = result.status === 'COMPLETED' ? 'completed' : 'failed';
190
+ rec.missionId = result.missionId;
191
+ rec.outputs = result.outputs;
192
+ rec.completedAt = Date.now();
193
+ }
194
+ catch (err) {
195
+ rec.status = 'failed';
196
+ rec.error = err instanceof Error ? err.message : String(err);
197
+ rec.completedAt = Date.now();
198
+ }
199
+ this.server?.broadcast(frame('ev', nodeId, { k: 'RUN_COMPLETED', run: runId, status: rec.status }));
200
+ }
201
+ // ── Outbound operations ──────────────────────────────────────────────────
202
+ /**
203
+ * Hand a task to the best-fit connected peer (compute-routed) or a named one.
204
+ * Non-blocking by default — returns a runId to follow. Pass wait to block
205
+ * until the run finishes (short tasks).
206
+ */
207
+ async dispatch(task, need, opts) {
208
+ if (!this.mesh)
209
+ return null;
210
+ const peers = this.mesh.connected();
211
+ const target = this.compute.best(peers, {
212
+ capabilities: task.capabilities,
213
+ preferNodeId: need?.preferNodeId,
214
+ preferName: need?.preferName,
215
+ });
216
+ if (!target) {
217
+ this.deps.logger.warn('[federation] no capable peer for dispatch');
218
+ return null;
219
+ }
220
+ const res = await this.client.rpc(target.url, envelope('task.dispatch', this.deps.identity.nodeId, task, { to: target.nodeId }));
221
+ const runId = res.data.runId;
222
+ this.dispatchedRuns.set(runId, target.nodeId);
223
+ let record = { runId, status: 'running', description: task.description, startedAt: Date.now() };
224
+ if (opts?.wait) {
225
+ record = await this.awaitRun(runId, opts.timeoutMs ?? 120_000);
226
+ }
227
+ return { peerId: target.nodeId, runId, record };
228
+ }
229
+ /** Query a dispatched run's current record (from whichever peer holds it). */
230
+ async followRun(runId) {
231
+ const local = this.runs.get(runId);
232
+ if (local)
233
+ return { record: local, frames: this.recentFrames(50) };
234
+ const peerId = this.dispatchedRuns.get(runId);
235
+ const peer = peerId ? this.mesh?.get(peerId) : undefined;
236
+ if (!peer)
237
+ return null;
238
+ const res = await this.client.rpc(peer.url, envelope('task.status', this.deps.identity.nodeId, { runId }, { to: peer.nodeId }));
239
+ return { record: res.data, frames: this.recentFrames(50) };
240
+ }
241
+ /** Poll a run until it finishes or the timeout elapses. */
242
+ async awaitRun(runId, timeoutMs = 120_000) {
243
+ const start = Date.now();
244
+ for (;;) {
245
+ const followed = await this.followRun(runId);
246
+ const rec = followed?.record;
247
+ if (rec && rec.status !== 'running')
248
+ return rec;
249
+ if (Date.now() - start > timeoutMs) {
250
+ return rec ?? { runId, status: 'failed', description: '', error: 'follow timeout', startedAt: start };
251
+ }
252
+ await new Promise(r => setTimeout(r, 500));
253
+ }
254
+ }
255
+ /** Run a slash-command on a specific peer. */
256
+ async runCommandOn(nodeId, input) {
257
+ const peer = this.mesh?.get(nodeId);
258
+ if (!peer)
259
+ throw new Error(`unknown peer: ${nodeId}`);
260
+ const res = await this.client.rpc(peer.url, envelope('command', this.deps.identity.nodeId, { input }, { to: nodeId }));
261
+ return res.data;
262
+ }
263
+ /** Hub: publish shared config (providers/API keys/policy) to the fleet. */
264
+ publishConfig(cfg) {
265
+ return this.configSync.publish(cfg);
266
+ }
267
+ /** Satellite: pull shared config from a peer (usually the hub). */
268
+ async pullConfig(nodeId) {
269
+ const peer = this.mesh?.get(nodeId);
270
+ if (!peer)
271
+ return false;
272
+ const res = await this.client.rpc(peer.url, envelope('config.request', this.deps.identity.nodeId, {}, { to: nodeId }));
273
+ if (res.kind === 'config.offer')
274
+ return this.configSync.receive(res.data);
275
+ return false;
276
+ }
277
+ async connect(url) {
278
+ return this.mesh?.connect(url) ?? null;
279
+ }
280
+ /** Auto-discover peers over Tailscale and connect to each (skips self/known). */
281
+ async discoverAndConnect(tag) {
282
+ const port = this.deps.options.port ?? DEFAULT_MESH_PORT;
283
+ const urls = await discoverTailscalePeers({ port, tag, logger: this.deps.logger });
284
+ let connected = 0;
285
+ for (const url of urls) {
286
+ const peer = await this.mesh?.connect(url);
287
+ if (peer && peer.nodeId !== this.deps.identity.nodeId)
288
+ connected++;
289
+ }
290
+ return connected;
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
+ }
330
+ // ── Views ────────────────────────────────────────────────────────────────
331
+ /** Neutral node-status list (self + peers) — feeds the mesh map. */
332
+ nodeStatuses() {
333
+ const d = this.deps.describe();
334
+ const self = selfNodeStatus(this.deps.identity, this.lastHw ?? this.telemetry.collect(), {
335
+ version: d.version,
336
+ capabilities: d.capabilities,
337
+ commands: d.commands,
338
+ });
339
+ const peers = (this.mesh?.list() ?? []).map(nodeStatusFromPeer);
340
+ return [self, ...peers];
341
+ }
342
+ peers() {
343
+ return this.mesh?.list() ?? [];
344
+ }
345
+ /** Latest local hardware snapshot (for the MOLTFED connector / panels). */
346
+ latestHardware() {
347
+ return this.lastHw;
348
+ }
349
+ /** Subscribe to the merged mesh frame stream (local + peer frames). */
350
+ onFrames(sub) {
351
+ this.frameSubs.add(sub);
352
+ return () => this.frameSubs.delete(sub);
353
+ }
354
+ recentFrames(n = 100) {
355
+ return this.frameHistory.slice(-n);
356
+ }
357
+ ingestFrame(peerId, f) {
358
+ this.frameHistory.push({ peerId, f });
359
+ if (this.frameHistory.length > 500)
360
+ this.frameHistory.shift();
361
+ for (const s of this.frameSubs)
362
+ s(peerId, f);
363
+ }
364
+ emitLocal(_t, f) {
365
+ // local status changes are also streamed to connected harnesses
366
+ this.server?.broadcast(f);
367
+ this.ingestFrame(this.deps.identity.nodeId, f);
368
+ }
369
+ async stop() {
370
+ this.unsub?.();
371
+ this.bridge?.stop();
372
+ this.lan?.stop();
373
+ if (this.telemetryTimer)
374
+ clearInterval(this.telemetryTimer);
375
+ this.mesh?.stop();
376
+ await this.server?.stop();
377
+ }
378
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * OpenKernel — Federation public API
3
+ *
4
+ * Neutral, transport-agnostic device mesh: peer discovery + heartbeats, remote
5
+ * task dispatch, live activity/telemetry streaming, API/config sharing, auto
6
+ * update, and a MOLTFED adapter for zero-change InfiniBot interop.
7
+ */
8
+ export * from './types.js';
9
+ export { Federation } from './federation.js';
10
+ export type { FederationOptions, FederationDeps, DispatchTask } from './federation.js';
11
+ export { createFederationPlugin } from './federation-plugin.js';
12
+ export type { FederationPlugin, FederationPluginOptions } from './federation-plugin.js';
13
+ export { loadOrCreateIdentity, buildManifest, totalMemoryBytes } from './node-identity.js';
14
+ export type { IdentityOptions, ManifestInputs } from './node-identity.js';
15
+ export { TelemetryCollector, loadFromSnapshot, nodeStatusFromPeer, selfNodeStatus } from './telemetry.js';
16
+ export { MeshServer, MeshClient } from './transport-http.js';
17
+ export type { MeshServerOptions, MeshClientOptions, RpcHandler } from './transport-http.js';
18
+ export { PeerMesh } from './peer-mesh.js';
19
+ export type { PeerMeshOptions } from './peer-mesh.js';
20
+ export { EventBridge } from './event-bridge.js';
21
+ export { ConfigSync } from './config-sync.js';
22
+ export { AutoUpdate, compareVersions } from './auto-update.js';
23
+ export { ComputeRouter } from './compute-router.js';
24
+ export type { ComputeCandidate, ComputeNeed } from './compute-router.js';
25
+ export { loadRole, saveRole, rolePreamble } from './role-context.js';
26
+ export { discoverTailscalePeers, tailscaleAvailable } from './discovery.js';
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';
30
+ export { envelope, reply, frame, frameToSSE, parseSSE, newId } from './protocol.js';
31
+ export * as moltfed from './moltfed-adapter.js';
32
+ export { MoltfedConnector } from './moltfed-connector.js';
33
+ export type { MoltfedConnectorOptions, WsLike, WsFactory, DeviceAuth } from './moltfed-connector.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OpenKernel — Federation public API
3
+ *
4
+ * Neutral, transport-agnostic device mesh: peer discovery + heartbeats, remote
5
+ * task dispatch, live activity/telemetry streaming, API/config sharing, auto
6
+ * update, and a MOLTFED adapter for zero-change InfiniBot interop.
7
+ */
8
+ export * from './types.js';
9
+ export { Federation } from './federation.js';
10
+ export { createFederationPlugin } from './federation-plugin.js';
11
+ export { loadOrCreateIdentity, buildManifest, totalMemoryBytes } from './node-identity.js';
12
+ export { TelemetryCollector, loadFromSnapshot, nodeStatusFromPeer, selfNodeStatus } from './telemetry.js';
13
+ export { MeshServer, MeshClient } from './transport-http.js';
14
+ export { PeerMesh } from './peer-mesh.js';
15
+ export { EventBridge } from './event-bridge.js';
16
+ export { ConfigSync } from './config-sync.js';
17
+ export { AutoUpdate, compareVersions } from './auto-update.js';
18
+ export { ComputeRouter } from './compute-router.js';
19
+ export { loadRole, saveRole, rolePreamble } from './role-context.js';
20
+ export { discoverTailscalePeers, tailscaleAvailable } from './discovery.js';
21
+ export { LanBeacon, DEFAULT_LAN_DISCOVERY_PORT } from './lan-discovery.js';
22
+ export { envelope, reply, frame, frameToSSE, parseSSE, newId } from './protocol.js';
23
+ export * as moltfed from './moltfed-adapter.js';
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
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * OpenKernel — MOLTFED Adapter (InfiniBot interop)
3
+ *
4
+ * Translates infinicode's NEUTRAL federation shapes into the message shapes
5
+ * InfiniBot already consumes, so an infinicode node lights up InfiniBot's
6
+ * neural-mesh map + hardware panels with ZERO InfiniBot changes:
7
+ *
8
+ * NodeStatus[] → nodes.status { ts, nodes:[…] }
9
+ * HardwareSnapshot → hardware.snapshot (same shape — passthrough)
10
+ * StreamFrame (ev) → agent broadcast event (activity panel / map animation)
11
+ *
12
+ * This module only builds payloads. The actual WebSocket connector that logs
13
+ * into an InfiniBot gateway and ships these is added in the integration step;
14
+ * keeping the mapping here means that connector is a thin transport shim.
15
+ */
16
+ import type { HardwareSnapshot, NodeStatus, StreamFrame } from './types.js';
17
+ /** InfiniBot `nodes.status` / `node.list` response shape. */
18
+ export declare function toNodesStatus(statuses: NodeStatus[]): {
19
+ ts: number;
20
+ nodes: Array<Record<string, unknown>>;
21
+ };
22
+ /** InfiniBot `hardware.snapshot` payload (shape already matches). */
23
+ export declare function toHardwareSnapshot(nodeId: string, hw: HardwareSnapshot): Record<string, unknown>;
24
+ /** Map a compact activity/routing frame to an InfiniBot-style `agent` event. */
25
+ export declare function toAgentEvent(f: StreamFrame): Record<string, unknown> | null;
26
+ /** Build the full set of InfiniBot-bound messages for a periodic push. */
27
+ export declare function buildMoltfedBatch(statuses: NodeStatus[], selfId: string, hw?: HardwareSnapshot): Array<{
28
+ method: string;
29
+ params: unknown;
30
+ }>;