infinicode 2.8.113 → 2.8.114

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
@@ -130,7 +130,25 @@ infinicode serve --hub --gateway ws://<infinibot-host>:18789 --gateway-token <to
130
130
 
131
131
  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`.
132
132
 
133
- > 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.
133
+ ### One-link MCP mesh connection
134
+
135
+ On the first Tailscale device, start MCP with a shared token and generate its direct link:
136
+
137
+ ```bash
138
+ infinicode mesh link --token YOUR_SECRET
139
+ infinicode mcp --tailscale --token YOUR_SECRET
140
+ ```
141
+
142
+ Give the single generated `http://.../fed/join?token=...` URL to the agent on the second device. It can connect immediately with either:
143
+
144
+ ```bash
145
+ infinicode mcp --connect "PASTE_LINK_HERE"
146
+ infinicode mesh join "PASTE_LINK_HERE" --host opencode
147
+ ```
148
+
149
+ The first form runs an MCP instance directly. The second persists the same MCP connection in OpenCode or Claude (`--host all` configures both). The link is a credential: share it only inside the tailnet and rotate the mesh token if it leaks.
150
+
151
+ > 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.
134
152
 
135
153
  ### As a library
136
154
 
@@ -621,4 +639,4 @@ MIT
621
639
 
622
640
  ---
623
641
 
624
- ⚡ Built for sovereign computing. Your code, your models, your hardware.
642
+ ⚡ Built for sovereign computing. Your code, your models, your hardware.
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ import { serve } from './commands/serve.js';
16
16
  import { mcpCommand } from './commands/mcp.js';
17
17
  import { maintain } from './commands/maintain.js';
18
18
  import { meshInstall, meshUninstall } from './commands/mesh-install.js';
19
+ import { meshLink } from './commands/mesh-link.js';
19
20
  import { installTui } from './commands/install-tui.js';
20
21
  import { attachRoboparkSubcommands } from './commands/robopark.js';
21
22
  import { DEFAULT_CONFIG } from './kernel/config-schema.js';
@@ -351,12 +352,33 @@ meshCmd
351
352
  .option('--host <host>', 'target host: claude | opencode | all', 'claude')
352
353
  .option('--token <token>', 'shared bearer token every device must present (recommended off-LAN)')
353
354
  .option('--name <name>', 'friendly node name for this device')
355
+ .option('--link <url>', 'direct mesh join URL generated on the other device')
354
356
  .option('--tailscale', 'discover peers over Tailscale instead of / as well as the LAN')
355
357
  .option('--no-lan', 'disable zero-config LAN auto-discovery')
356
358
  .option('--print', 'dry run — show what would be written without changing anything')
357
359
  .action(async (opts) => {
358
360
  await meshInstall(opts);
359
361
  });
362
+ meshCmd
363
+ .command('link')
364
+ .description('Generate one direct Tailscale URL another MCP agent can use to join this mesh')
365
+ .option('--host <host>', 'Tailscale MagicDNS name or 100.x IP (auto-detected by default)')
366
+ .option('--port <port>', 'MCP mesh port on this device (default: serve port + 1)')
367
+ .option('--token <token>', 'shared mesh token (defaults to saved federation token)')
368
+ .option('--https', 'generate an https:// link for a TLS-enabled mesh node')
369
+ .option('--quiet', 'print only the URL')
370
+ .action(async (opts) => {
371
+ await meshLink(config, opts);
372
+ });
373
+ meshCmd
374
+ .command('join <link>')
375
+ .description('Install a direct mesh URL into Claude Code and/or OpenCode')
376
+ .option('--host <host>', 'target host: claude | opencode | all', 'all')
377
+ .option('--name <name>', 'friendly node name for this device')
378
+ .option('--print', 'dry run - show what would be written')
379
+ .action(async (link, opts) => {
380
+ await meshInstall({ ...opts, link, lan: false });
381
+ });
360
382
  meshCmd
361
383
  .command('uninstall')
362
384
  .description('Remove the infinicode MCP server from your coding host')
@@ -387,6 +409,7 @@ program
387
409
  .option('--lan', 'auto-discover + connect peers on the same LAN via UDP broadcast (no seed/Tailscale needed)')
388
410
  .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
389
411
  .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
412
+ .option('--connect <url>', 'single direct mesh join URL generated by `infinicode mesh link`')
390
413
  .action(async (opts) => {
391
414
  await mcpCommand(config, opts);
392
415
  });
@@ -10,5 +10,7 @@ export interface McpOptions {
10
10
  tag?: string;
11
11
  lan?: boolean;
12
12
  lanPort?: string;
13
+ /** Canonical URL generated by `infinicode mesh link`. */
14
+ connect?: string;
13
15
  }
14
16
  export declare function mcpCommand(config: Conf<InfinicodeConfig>, opts: McpOptions): Promise<void>;
@@ -14,6 +14,7 @@ import { fileURLToPath } from 'node:url';
14
14
  import { dirname, join } from 'node:path';
15
15
  import { buildKernelFromConfig, registerCloudProviders } from '../kernel/setup.js';
16
16
  import { Federation, GoalLoop, loadOrCreateIdentity, serveMcpStdio, SilentLogger, } from '../kernel/index.js';
17
+ import { parseMeshJoinLink, redactMeshJoinLink } from '../kernel/federation/mesh-link.js';
17
18
  function pkgVersion() {
18
19
  try {
19
20
  const here = dirname(fileURLToPath(import.meta.url));
@@ -32,10 +33,14 @@ const stderrLogger = {
32
33
  };
33
34
  export async function mcpCommand(config, opts) {
34
35
  const fed = config.get('federation') ?? {};
36
+ const direct = opts.connect ? parseMeshJoinLink(opts.connect) : undefined;
35
37
  const role = (opts.role ?? fed.role ?? 'peer');
36
38
  // Default to 47914 so a co-located `infinicode serve` (47913) doesn't clash.
37
39
  const port = opts.meshPort ? parseInt(opts.meshPort, 10) : (fed.port ?? 47913) + 1;
38
- const token = opts.token ?? fed.token;
40
+ if (opts.token && direct?.token && opts.token !== direct.token) {
41
+ throw new Error('the explicit --token does not match the direct mesh link token');
42
+ }
43
+ const token = opts.token ?? direct?.token ?? fed.token;
39
44
  const version = pkgVersion();
40
45
  const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
41
46
  const identity = loadOrCreateIdentity({ role, displayName: opts.name ?? fed.displayName });
@@ -94,9 +99,15 @@ export async function mcpCommand(config, opts) {
94
99
  // Continue serving MCP tools; dispatch will report "no peers" until fixed.
95
100
  }
96
101
  // Join the mesh: static seeds + optional Tailscale discovery.
97
- const seeds = [...(fed.seeds ?? []), ...(opts.seed ?? [])];
98
- for (const url of seeds)
99
- await federation.connect(url).catch(() => null);
102
+ const seeds = [...(fed.seeds ?? []), ...(opts.seed ?? []), ...(direct ? [direct.seed] : [])];
103
+ for (const url of [...new Set(seeds)]) {
104
+ const peer = await federation.connect(url).catch(() => null);
105
+ if (direct && url === direct.seed) {
106
+ if (!peer)
107
+ throw new Error(`direct mesh connection failed: ${redactMeshJoinLink(direct.link)}`);
108
+ stderrLogger.info(`direct mesh connected to ${peer.displayName} @ ${url}`);
109
+ }
110
+ }
100
111
  if (opts.tailscale) {
101
112
  const n = await federation.discoverAndConnect(opts.tag).catch(() => 0);
102
113
  stderrLogger.info(`tailscale discovery connected ${n} peer(s)`);
@@ -3,6 +3,7 @@ export interface MeshInstallOptions {
3
3
  token?: string;
4
4
  name?: string;
5
5
  tailscale?: boolean;
6
+ link?: string;
6
7
  lan?: boolean;
7
8
  print?: boolean;
8
9
  }
@@ -17,6 +17,7 @@ import { homedir } from 'node:os';
17
17
  import { join, dirname } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { findExecutable } from '../kernel/agents/backends/detect.js';
20
+ import { parseMeshJoinLink, redactMeshJoinLink } from '../kernel/federation/mesh-link.js';
20
21
  /** How a host should spawn the infinicode MCP node. Resolves a global binary
21
22
  * if present, else falls back to `node <this package's cli.js>`. */
22
23
  function resolveLaunch() {
@@ -29,12 +30,18 @@ function resolveLaunch() {
29
30
  /** The `infinicode mcp …` argv the host should run. */
30
31
  function meshArgs(opts) {
31
32
  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);
33
+ if (opts.link) {
34
+ parseMeshJoinLink(opts.link);
35
+ args.push('--connect', opts.link);
36
+ }
37
+ else {
38
+ if (opts.lan !== false)
39
+ args.push('--lan');
40
+ if (opts.tailscale)
41
+ args.push('--tailscale');
42
+ if (opts.token)
43
+ args.push('--token', opts.token);
44
+ }
38
45
  if (opts.name)
39
46
  args.push('--name', opts.name);
40
47
  return args;
@@ -114,6 +121,7 @@ function selectedHosts(host) {
114
121
  return ['claude'];
115
122
  }
116
123
  export async function meshInstall(opts) {
124
+ const direct = opts.link ? parseMeshJoinLink(opts.link) : undefined;
117
125
  const hosts = selectedHosts(opts.host);
118
126
  console.log(chalk.bold('\n🕸 infinicode mesh — MCP registration\n'));
119
127
  for (const host of hosts) {
@@ -121,7 +129,10 @@ export async function meshInstall(opts) {
121
129
  const label = host === 'claude' ? 'Claude Code' : 'OpenCode';
122
130
  const verb = opts.print ? 'would write' : res.existed ? 'updated' : 'added';
123
131
  console.log(` ${chalk.green('✓')} ${chalk.cyan(label)} — ${verb} ${chalk.dim(res.path)}`);
124
- console.log(chalk.dim(` ${JSON.stringify(res.entry)}`));
132
+ const shown = opts.link
133
+ ? JSON.stringify(res.entry).split(opts.link).join(redactMeshJoinLink(opts.link))
134
+ : JSON.stringify(res.entry);
135
+ console.log(chalk.dim(` ${shown}`));
125
136
  }
126
137
  // Warn if the target CLI backend isn't installed on THIS machine — a worker
127
138
  // node can only run agents for CLIs it actually has.
@@ -138,10 +149,16 @@ export async function meshInstall(opts) {
138
149
  return;
139
150
  }
140
151
  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) {
152
+ if (direct) {
153
+ console.log(` 1. Restart the coding host on this device.`);
154
+ console.log(` 2. Ask the agent to call ${chalk.italic('"list_nodes"')}.`);
155
+ }
156
+ else {
157
+ console.log(` 1. Run the SAME command on the other laptop.`);
158
+ console.log(` 2. Restart your coding host (Claude Code) on both.`);
159
+ console.log(` 3. Ask it: ${chalk.italic('"list_nodes"')} then ${chalk.italic('"spawn_agent on <laptop> with agent claude to …"')}`);
160
+ }
161
+ if (!opts.token && !direct?.token) {
145
162
  console.log(chalk.dim(`\n Tip: on an untrusted network, re-run with --token <secret> on every device.`));
146
163
  }
147
164
  console.log();
@@ -0,0 +1,10 @@
1
+ import type Conf from 'conf';
2
+ import type { InfinicodeConfig } from '../kernel/config-schema.js';
3
+ export interface MeshLinkOptions {
4
+ host?: string;
5
+ port?: string;
6
+ token?: string;
7
+ https?: boolean;
8
+ quiet?: boolean;
9
+ }
10
+ export declare function meshLink(config: Conf<InfinicodeConfig>, opts: MeshLinkOptions): Promise<string>;
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+ import { execa } from 'execa';
3
+ import { buildMeshJoinLink } from '../kernel/federation/mesh-link.js';
4
+ async function tailscaleHost() {
5
+ try {
6
+ const { stdout } = await execa('tailscale', ['status', '--json'], { timeout: 5000 });
7
+ const self = JSON.parse(stdout).Self;
8
+ const dns = self?.DNSName?.replace(/\.$/, '');
9
+ if (dns)
10
+ return dns;
11
+ return self?.TailscaleIPs?.find(ip => ip.includes('.'));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ export async function meshLink(config, opts) {
18
+ const fed = config.get('federation') ?? {};
19
+ const host = opts.host?.trim() || await tailscaleHost();
20
+ if (!host)
21
+ throw new Error('Tailscale address unavailable; run tailscale up or pass --host <MagicDNS-name-or-100.x-IP>');
22
+ const rawPort = opts.port ?? String((fed.port ?? 47913) + 1);
23
+ const port = Number.parseInt(rawPort, 10);
24
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
25
+ throw new Error('mesh link port must be between 1 and 65535');
26
+ const token = opts.token ?? fed.token;
27
+ const bracketedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
28
+ const link = buildMeshJoinLink(`${opts.https ? 'https' : 'http'}://${bracketedHost}:${port}`, token);
29
+ if (opts.quiet) {
30
+ console.log(link);
31
+ return link;
32
+ }
33
+ console.log(chalk.bold('\n Infinicode direct mesh link'));
34
+ console.log(chalk.cyan(` ${link}`));
35
+ console.log(chalk.dim('\n Give this one URL to the agent on the other device.'));
36
+ console.log(` Direct MCP: ${chalk.cyan(`infinicode mcp --connect "${link}"`)}`);
37
+ console.log(` OpenCode/Claude: ${chalk.cyan(`infinicode mesh join "${link}" --host all`)}`);
38
+ if (token)
39
+ console.log(chalk.yellow('\n This link grants mesh access. Treat it like a password and rotate the token if shared accidentally.'));
40
+ else
41
+ console.log(chalk.yellow('\n This mesh has no token. Add --token <secret> on both devices for production use.'));
42
+ console.log();
43
+ return link;
44
+ }