infinicode 2.2.0 → 2.2.2
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/dist/cli.js +1 -1
- package/dist/commands/run.js +27 -16
- package/dist/kernel/federation/federation.d.ts +10 -0
- package/dist/kernel/federation/federation.js +47 -1
- package/dist/kernel/federation/peer-mesh.d.ts +2 -0
- package/dist/kernel/federation/peer-mesh.js +6 -1
- package/dist/kernel/federation/transport-http.d.ts +2 -0
- package/dist/kernel/federation/transport-http.js +7 -3
- package/package.json +1 -1
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.2.
|
|
18
|
+
const VERSION = '2.2.2';
|
|
19
19
|
const config = new Conf({
|
|
20
20
|
projectName: 'infinicode',
|
|
21
21
|
defaults: DEFAULT_CONFIG,
|
package/dist/commands/run.js
CHANGED
|
@@ -26,10 +26,21 @@ import { SilentLogger } from '../kernel/index.js';
|
|
|
26
26
|
import { openAICompatBaseURL } from '../kernel/provider-url.js';
|
|
27
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const PROMPT_ANIMATION_MAX_FRAMES = Math.min(ASCII_VIDEO_FRAMES.length, ASCII_VIDEO_FPS * 3);
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Locate the bundled TUI binary, or null if it isn't shipped in this install.
|
|
31
|
+
* IMPORTANT: never fall back to spawning `infinicode` itself — that re-enters
|
|
32
|
+
* this default command and creates an infinite launch loop (the npm package
|
|
33
|
+
* ships the CLI/kernel only, without the large platform-specific TUI binary).
|
|
34
|
+
*/
|
|
35
|
+
function resolveTuiBinary() {
|
|
30
36
|
const rootDir = resolve(__dirname, '..', '..');
|
|
31
|
-
const
|
|
32
|
-
|
|
37
|
+
const names = process.platform === 'win32' ? ['infinicode-tui.exe'] : ['infinicode-tui'];
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const p = join(rootDir, 'bin', name);
|
|
40
|
+
if (existsSync(p))
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
33
44
|
}
|
|
34
45
|
function fitPromptFrameToTerminal(frame) {
|
|
35
46
|
const columns = Math.max(1, (process.stdout.columns ?? 120) - 1);
|
|
@@ -345,27 +356,27 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
345
356
|
}
|
|
346
357
|
console.log(chalk.dim('-'.repeat(40)));
|
|
347
358
|
await renderPromptAnimation();
|
|
359
|
+
const tuiBinary = resolveTuiBinary();
|
|
360
|
+
if (!tuiBinary) {
|
|
361
|
+
console.log(chalk.yellow('\ninfinicode TUI is not bundled in this install.'));
|
|
362
|
+
console.log(chalk.dim('The npm package ships the kernel + device-mesh CLI only — the TUI binary is'));
|
|
363
|
+
console.log(chalk.dim('large and platform-specific, so it is not published. Use these instead:'));
|
|
364
|
+
console.log(chalk.cyan(' infinicode serve --hub ') + chalk.dim('# run this device as a mesh node'));
|
|
365
|
+
console.log(chalk.cyan(' infinicode mcp ') + chalk.dim('# MCP control server for a harness'));
|
|
366
|
+
console.log(chalk.cyan(' infinicode mission run ... ') + chalk.dim('# headless mission execution'));
|
|
367
|
+
console.log(chalk.cyan(' infinicode console ') + chalk.dim('# slash-command console'));
|
|
368
|
+
console.log(chalk.dim('\nTo use the TUI, build it from source and place it in bin/ (infinicode-tui[.exe]).'));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
348
371
|
const { execa } = await import('execa');
|
|
349
|
-
const infinicodeCommand = resolveInfinicodeCommand();
|
|
350
372
|
try {
|
|
351
|
-
|
|
352
|
-
await execa('which', ['infinicode']).catch(() => execa('where', ['infinicode']));
|
|
353
|
-
}
|
|
354
|
-
await execa(infinicodeCommand, [], {
|
|
373
|
+
await execa(tuiBinary, [], {
|
|
355
374
|
stdio: 'inherit',
|
|
356
375
|
env: process.env,
|
|
357
376
|
});
|
|
358
377
|
}
|
|
359
378
|
catch (e) {
|
|
360
379
|
const err = e;
|
|
361
|
-
if (err.code === 'ENOENT') {
|
|
362
|
-
console.log(chalk.yellow('\ninfinicode TUI binary not found.'));
|
|
363
|
-
console.log(chalk.dim('\nThe TUI requires the infinicode binary in bin/ or on PATH.'));
|
|
364
|
-
console.log(chalk.dim('Build from source:'));
|
|
365
|
-
console.log(chalk.cyan(' pnpm build'));
|
|
366
|
-
console.log();
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
380
|
if (err.exitCode) {
|
|
370
381
|
process.exit(err.exitCode);
|
|
371
382
|
}
|
|
@@ -61,6 +61,8 @@ export declare class Federation {
|
|
|
61
61
|
private lan?;
|
|
62
62
|
/** nodeIds we're currently dialing via LAN discovery (dedup guard). */
|
|
63
63
|
private lanConnecting;
|
|
64
|
+
/** nodeIds we're currently dialing back after an inbound heartbeat/hello. */
|
|
65
|
+
private inboundConnecting;
|
|
64
66
|
private configPullTimer?;
|
|
65
67
|
readonly configSync: ConfigSync;
|
|
66
68
|
readonly autoUpdate: AutoUpdate;
|
|
@@ -115,6 +117,14 @@ export declare class Federation {
|
|
|
115
117
|
/** Satellite: pull shared config from a peer (usually the hub). */
|
|
116
118
|
pullConfig(nodeId: string): Promise<boolean>;
|
|
117
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;
|
|
118
128
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
119
129
|
private maybeAutoPull;
|
|
120
130
|
/** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
|
|
@@ -24,6 +24,8 @@ 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();
|
|
27
29
|
configPullTimer;
|
|
28
30
|
configSync;
|
|
29
31
|
autoUpdate;
|
|
@@ -68,6 +70,7 @@ export class Federation {
|
|
|
68
70
|
logger,
|
|
69
71
|
heartbeatMs: options.heartbeatMs,
|
|
70
72
|
getLoad: () => loadFromSnapshot(this.lastHw),
|
|
73
|
+
selfPort: port,
|
|
71
74
|
onFrame: (peerId, f) => this.ingestFrame(peerId, f),
|
|
72
75
|
onPeerChange: () => this.emitLocal('nd', frame('nd', identity.nodeId, this.nodeStatuses())),
|
|
73
76
|
});
|
|
@@ -95,12 +98,14 @@ export class Federation {
|
|
|
95
98
|
logger.info(`[federation] node ${identity.displayName} (${identity.role}) online on :${port}`);
|
|
96
99
|
}
|
|
97
100
|
// ── Inbound RPC ────────────────────────────────────────────────────────────
|
|
98
|
-
async onRpc(env,
|
|
101
|
+
async onRpc(env, remote) {
|
|
99
102
|
const self = this.deps.identity.nodeId;
|
|
100
103
|
switch (env.kind) {
|
|
101
104
|
case 'heartbeat':
|
|
105
|
+
this.learnInboundPeer(env, remote);
|
|
102
106
|
return reply(env, 'ack', self, { load: loadFromSnapshot(this.lastHw) });
|
|
103
107
|
case 'hello':
|
|
108
|
+
this.learnInboundPeer(env, remote);
|
|
104
109
|
return reply(env, 'welcome', self, buildManifest(this.deps.identity, this.deps.describe()));
|
|
105
110
|
case 'task.dispatch': {
|
|
106
111
|
// Non-blocking handoff: register the run, ack with a runId immediately,
|
|
@@ -287,6 +292,34 @@ export class Federation {
|
|
|
287
292
|
await this.maybeAutoPull(peer);
|
|
288
293
|
return peer;
|
|
289
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
|
+
}
|
|
290
323
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
291
324
|
async maybeAutoPull(peer) {
|
|
292
325
|
if (!this.deps.options.autoPullConfig)
|
|
@@ -403,3 +436,16 @@ export class Federation {
|
|
|
403
436
|
await this.server?.stop();
|
|
404
437
|
}
|
|
405
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) {
|
|
@@ -36,6 +36,8 @@ export declare class MeshClient {
|
|
|
36
36
|
private opts;
|
|
37
37
|
constructor(opts?: MeshClientOptions);
|
|
38
38
|
private headers;
|
|
39
|
+
/** Normalize a base URL so a trailing slash doesn't produce `//fed/...` (→ 404). */
|
|
40
|
+
private base;
|
|
39
41
|
fetchManifest(baseUrl: string): Promise<NodeManifest>;
|
|
40
42
|
rpc(baseUrl: string, env: FederationEnvelope): Promise<FederationEnvelope>;
|
|
41
43
|
/**
|
|
@@ -152,8 +152,12 @@ export class MeshClient {
|
|
|
152
152
|
h['authorization'] = `Bearer ${this.opts.token}`;
|
|
153
153
|
return h;
|
|
154
154
|
}
|
|
155
|
+
/** Normalize a base URL so a trailing slash doesn't produce `//fed/...` (→ 404). */
|
|
156
|
+
base(baseUrl) {
|
|
157
|
+
return baseUrl.replace(/\/+$/, '');
|
|
158
|
+
}
|
|
155
159
|
async fetchManifest(baseUrl) {
|
|
156
|
-
const res = await fetch(`${baseUrl}/fed/manifest`, {
|
|
160
|
+
const res = await fetch(`${this.base(baseUrl)}/fed/manifest`, {
|
|
157
161
|
headers: this.headers(),
|
|
158
162
|
signal: AbortSignal.timeout(this.opts.timeoutMs ?? 5000),
|
|
159
163
|
});
|
|
@@ -162,7 +166,7 @@ export class MeshClient {
|
|
|
162
166
|
return (await res.json());
|
|
163
167
|
}
|
|
164
168
|
async rpc(baseUrl, env) {
|
|
165
|
-
const res = await fetch(`${baseUrl}/fed/rpc`, {
|
|
169
|
+
const res = await fetch(`${this.base(baseUrl)}/fed/rpc`, {
|
|
166
170
|
method: 'POST',
|
|
167
171
|
headers: this.headers(),
|
|
168
172
|
body: JSON.stringify(env),
|
|
@@ -180,7 +184,7 @@ export class MeshClient {
|
|
|
180
184
|
const ctrl = new AbortController();
|
|
181
185
|
(async () => {
|
|
182
186
|
try {
|
|
183
|
-
const res = await fetch(`${baseUrl}/fed/stream`, {
|
|
187
|
+
const res = await fetch(`${this.base(baseUrl)}/fed/stream`, {
|
|
184
188
|
headers: this.headers(),
|
|
185
189
|
signal: ctrl.signal,
|
|
186
190
|
});
|
package/package.json
CHANGED