infinicode 2.3.5 → 2.4.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 (64) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +5 -1
  2. package/.opencode/plugins/mesh-commands.tsx +4 -3
  3. package/.opencode/plugins/routing-mode-display.tsx +6 -2
  4. package/.opencode/tui.json +2 -1
  5. package/bin/robopark.js +2 -0
  6. package/dist/cli.js +46 -1
  7. package/dist/commands/maintain.d.ts +29 -0
  8. package/dist/commands/maintain.js +186 -0
  9. package/dist/commands/mcp.js +1 -1
  10. package/dist/commands/mesh-install.d.ts +10 -0
  11. package/dist/commands/mesh-install.js +160 -0
  12. package/dist/commands/robopark.d.ts +6 -0
  13. package/dist/commands/robopark.js +450 -0
  14. package/dist/commands/run.js +82 -22
  15. package/dist/commands/serve.d.ts +1 -0
  16. package/dist/commands/serve.js +152 -11
  17. package/dist/kernel/agents/backends/cli-backend.d.ts +41 -0
  18. package/dist/kernel/agents/backends/cli-backend.js +140 -0
  19. package/dist/kernel/agents/backends/detect.d.ts +17 -0
  20. package/dist/kernel/agents/backends/detect.js +140 -0
  21. package/dist/kernel/agents/backends/index.d.ts +11 -0
  22. package/dist/kernel/agents/backends/index.js +3 -0
  23. package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
  24. package/dist/kernel/agents/backends/parse-utils.js +72 -0
  25. package/dist/kernel/agents/backends/profiles.d.ts +67 -0
  26. package/dist/kernel/agents/backends/profiles.js +48 -0
  27. package/dist/kernel/agents/backends/registry.d.ts +31 -0
  28. package/dist/kernel/agents/backends/registry.js +174 -0
  29. package/dist/kernel/agents/backends/rotation.d.ts +51 -0
  30. package/dist/kernel/agents/backends/rotation.js +107 -0
  31. package/dist/kernel/agents/backends/types.d.ts +67 -0
  32. package/dist/kernel/agents/backends/types.js +1 -0
  33. package/dist/kernel/agents/index.d.ts +8 -0
  34. package/dist/kernel/agents/index.js +8 -0
  35. package/dist/kernel/federation/dashboard-html.d.ts +13 -0
  36. package/dist/kernel/federation/dashboard-html.js +371 -0
  37. package/dist/kernel/federation/federation.d.ts +112 -2
  38. package/dist/kernel/federation/federation.js +270 -10
  39. package/dist/kernel/federation/moltfed-adapter.js +1 -0
  40. package/dist/kernel/federation/telemetry.d.ts +1 -1
  41. package/dist/kernel/federation/telemetry.js +2 -1
  42. package/dist/kernel/federation/transport-http.d.ts +17 -1
  43. package/dist/kernel/federation/transport-http.js +65 -2
  44. package/dist/kernel/federation/types.d.ts +46 -1
  45. package/dist/kernel/free-providers.js +83 -0
  46. package/dist/kernel/gateway/openai-gateway.d.ts +55 -3
  47. package/dist/kernel/gateway/openai-gateway.js +428 -49
  48. package/dist/kernel/index.d.ts +2 -1
  49. package/dist/kernel/index.js +3 -1
  50. package/dist/kernel/logger.d.ts +16 -3
  51. package/dist/kernel/logger.js +38 -0
  52. package/dist/kernel/mcp/mcp-server.js +65 -10
  53. package/dist/kernel/orchestrator.d.ts +4 -0
  54. package/dist/kernel/orchestrator.js +34 -0
  55. package/dist/kernel/provider-manager.d.ts +13 -0
  56. package/dist/kernel/provider-manager.js +58 -5
  57. package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
  58. package/dist/kernel/providers/openai-compatible-provider.js +22 -3
  59. package/dist/kernel/recovery-manager.js +55 -15
  60. package/dist/kernel/router.js +15 -2
  61. package/dist/kernel/types.d.ts +12 -2
  62. package/dist/robopark-cli.d.ts +2 -0
  63. package/dist/robopark-cli.js +23 -0
  64. package/package.json +6 -3
@@ -109,7 +109,7 @@ function AnimatedLogo(props: { renderer?: any }) {
109
109
  );
110
110
  }
111
111
 
112
- export const tui = async (api: any) => {
112
+ const tui = async (api: any) => {
113
113
  api.slots.register({
114
114
  slots: {
115
115
  home_logo() {
@@ -118,3 +118,7 @@ export const tui = async (api: any) => {
118
118
  },
119
119
  });
120
120
  };
121
+
122
+ // The TUI plugin loader reads `mod.default` (strict mode) and requires an object
123
+ // with a tui() function — a bare `export const tui` is silently skipped.
124
+ export default { id: "infinicode-home-logo", tui };
@@ -16,7 +16,6 @@
16
16
  * /follow, /dispatch, /goal, /mission, /role) open a prompt first.
17
17
  */
18
18
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
19
- import { createBindingLookup } from "@opentui/keymap/extras"
20
19
 
21
20
  type CommandResult = { ok?: boolean; text?: string; data?: unknown }
22
21
 
@@ -112,8 +111,10 @@ const SPECS: Spec[] = [
112
111
 
113
112
  const tui: TuiPlugin = async (api, options) => {
114
113
  if (options?.enabled === false) return
115
- const keys = createBindingLookup({})
116
114
 
115
+ // No keybindings — these are palette-only slash commands. Empty bindings avoids
116
+ // any dependency on the keymap-extras runtime module (which may not resolve for
117
+ // an externally-loaded plugin) so registration can never fail to load.
117
118
  api.keymap.registerLayer({
118
119
  commands: SPECS.map((s) => ({
119
120
  name: s.name,
@@ -127,7 +128,7 @@ const tui: TuiPlugin = async (api, options) => {
127
128
  else runNow(api, s.title, cmd)
128
129
  },
129
130
  })),
130
- bindings: keys.gather("mesh.global", SPECS.map((s) => s.name)),
131
+ bindings: [],
131
132
  })
132
133
  }
133
134
 
@@ -85,7 +85,7 @@ function buildRoutingBadge(cfg: InfinicodeConfig | null, tuiModel?: string): Rou
85
85
 
86
86
  // ── Plugin ──────────────────────────────────────────────────────────────────
87
87
 
88
- export const tui = async (api: any) => {
88
+ const tui = async (api: any) => {
89
89
  const [cfg] = createSignal(loadConfig());
90
90
 
91
91
  const getTuiModel = (): string | undefined => {
@@ -132,4 +132,8 @@ export const tui = async (api: any) => {
132
132
  },
133
133
  },
134
134
  });
135
- };
135
+ };
136
+
137
+ // The TUI plugin loader reads `mod.default` (strict mode) and requires an object
138
+ // with a tui() function — a bare `export const tui` is silently skipped.
139
+ export default { id: "infinicode-routing-mode", tui };
@@ -3,6 +3,7 @@
3
3
  "theme": "infinibot-gold",
4
4
  "plugin": [
5
5
  "./plugins/home-logo-animation.tsx",
6
- "./plugins/routing-mode-display.tsx"
6
+ "./plugins/routing-mode-display.tsx",
7
+ "./plugins/mesh-commands.tsx"
7
8
  ]
8
9
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/robopark-cli.js');
package/dist/cli.js CHANGED
@@ -14,9 +14,12 @@ import { providersList, providersAdd, providersRemove, providersTest } from './c
14
14
  import { consoleRepl, consoleRun } from './commands/console.js';
15
15
  import { serve } from './commands/serve.js';
16
16
  import { mcpCommand } from './commands/mcp.js';
17
+ import { maintain } from './commands/maintain.js';
18
+ import { meshInstall, meshUninstall } from './commands/mesh-install.js';
17
19
  import { installTui } from './commands/install-tui.js';
20
+ import { attachRoboparkSubcommands } from './commands/robopark.js';
18
21
  import { DEFAULT_CONFIG } from './kernel/config-schema.js';
19
- const VERSION = '2.3.5';
22
+ const VERSION = '2.4.0';
20
23
  const config = new Conf({
21
24
  projectName: 'infinicode',
22
25
  defaults: DEFAULT_CONFIG,
@@ -292,12 +295,54 @@ program
292
295
  .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
293
296
  .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
294
297
  .option('--console', 'interactive slash-command console on the node (/nodes, /build, /activity …); on by default in a TTY')
298
+ .option('--dashboard', 'serve the RoboPark control web UI at http://<host>:<port>/ (fleet map, live activity, actions)')
295
299
  .option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
296
300
  .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
297
301
  .option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
298
302
  .action(async (opts) => {
299
303
  await serve(config, opts);
300
304
  });
305
+ // ── RoboPark: fleet control overlay (also shipped as the `robopark` bin) ────
306
+ const roboparkCmd = program
307
+ .command('robopark')
308
+ .alias('rp')
309
+ .description('RoboPark fleet control — setup, fleet status, run, apply, dashboard (rides the mesh)');
310
+ attachRoboparkSubcommands(roboparkCmd, config);
311
+ // ── OpenKernel: one-shot scheduled maintenance ─────────────────────────────
312
+ program
313
+ .command('maintain <task-prompt>')
314
+ .description('Run one maintenance task and exit — meant for OS cron/Task Scheduler, not a daemon')
315
+ .option('--name <name>', 'node name for the report (defaults to this node\'s role, or "infinicode-node")')
316
+ .option('--task-name <name>', 'short label for the report (defaults to a slice of the task prompt)')
317
+ .option('--gateway <url>', 'InfiniBot gateway to push the report to (ws://host:18789)')
318
+ .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
319
+ .option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
320
+ .action(async (taskPrompt, opts) => {
321
+ await maintain(config, taskPrompt, opts);
322
+ });
323
+ // ── infinicode: mesh — one-command MCP registration (no hand-edited JSON) ──
324
+ const meshCmd = program
325
+ .command('mesh')
326
+ .description('Join this device to the agent mesh by auto-registering the MCP server into your coding host');
327
+ meshCmd
328
+ .command('install', { isDefault: true })
329
+ .description('Register the infinicode MCP server into Claude Code (and/or OpenCode) — no manual file edits')
330
+ .option('--host <host>', 'target host: claude | opencode | all', 'claude')
331
+ .option('--token <token>', 'shared bearer token every device must present (recommended off-LAN)')
332
+ .option('--name <name>', 'friendly node name for this device')
333
+ .option('--tailscale', 'discover peers over Tailscale instead of / as well as the LAN')
334
+ .option('--no-lan', 'disable zero-config LAN auto-discovery')
335
+ .option('--print', 'dry run — show what would be written without changing anything')
336
+ .action(async (opts) => {
337
+ await meshInstall(opts);
338
+ });
339
+ meshCmd
340
+ .command('uninstall')
341
+ .description('Remove the infinicode MCP server from your coding host')
342
+ .option('--host <host>', 'target host: claude | opencode | all', 'claude')
343
+ .action(async (opts) => {
344
+ await meshUninstall(opts);
345
+ });
301
346
  // ── infinicode: install the TUI binary into this install ───────────────────
302
347
  program
303
348
  .command('install-tui')
@@ -0,0 +1,29 @@
1
+ import type Conf from 'conf';
2
+ import type { InfinicodeConfig } from '../kernel/setup.js';
3
+ import type { MaintenanceReport, Kernel, RoleContext } from '../kernel/index.js';
4
+ export interface MaintainOptions {
5
+ name?: string;
6
+ gateway?: string;
7
+ gatewayToken?: string;
8
+ gatewayPassword?: string;
9
+ taskName?: string;
10
+ }
11
+ interface CachedTask {
12
+ taskPrompt: string;
13
+ taskName?: string;
14
+ }
15
+ export declare function loadLastMaintenanceTask(storageDir?: string): CachedTask | null;
16
+ /**
17
+ * Pure execution: run one maintenance task on an ALREADY-BUILT kernel and
18
+ * return the report — no push, no process.exit. Shared by the standalone
19
+ * `infinicode maintain` CLI command (builds its own short-lived kernel) and
20
+ * `infinicode serve`'s inbound-command poller (reuses the node's live
21
+ * kernel, since that process is already running one).
22
+ */
23
+ export declare function runMaintenanceTask(kernel: Kernel, taskPrompt: string, opts: {
24
+ nodeId: string;
25
+ role: RoleContext | null;
26
+ taskName?: string;
27
+ }): Promise<MaintenanceReport>;
28
+ export declare function maintain(config: Conf<InfinicodeConfig>, taskPrompt: string, opts: MaintainOptions): Promise<void>;
29
+ export {};
@@ -0,0 +1,186 @@
1
+ /**
2
+ * infinicode maintain — run one scheduled maintenance task and report the
3
+ * result, then exit. Meant to be invoked by OS-level cron / Task Scheduler on
4
+ * a monthly (or any) cadence — NOT a daemon. This deliberately does not reuse
5
+ * the autonomy GoalLoop: GoalLoop is in-memory/setInterval-based and resets on
6
+ * every process restart, which is wrong for a calendar cadence that must
7
+ * survive reboots and land on real dates. The OS scheduler already does that
8
+ * correctly; this command just needs to run once, cleanly, and report home.
9
+ *
10
+ * infinicode maintain "apply OS security patches, restart if required" \
11
+ * --name db-primary --gateway ws://100.x.y.z:18789 --gateway-token secret
12
+ */
13
+ import chalk from 'chalk';
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { buildKernelFromConfig } from '../kernel/setup.js';
17
+ import { loadRole, rolePreamble, loadOrCreateIdentity, MoltfedConnector, SilentLogger } from '../kernel/index.js';
18
+ function lastTaskFile(storageDir = join(process.cwd(), '.openkernel')) {
19
+ return join(storageDir, 'last-maintenance-task.json');
20
+ }
21
+ /** Cache the task text this device just ran LOCALLY — this is the only
22
+ * thing an on-demand "run now" trigger is allowed to replay (see
23
+ * loadLastMaintenanceTask). Never populated from anything network-supplied. */
24
+ function saveLastMaintenanceTask(task, storageDir) {
25
+ const dir = storageDir ?? join(process.cwd(), '.openkernel');
26
+ try {
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true });
29
+ writeFileSync(lastTaskFile(dir), JSON.stringify(task, null, 2));
30
+ }
31
+ catch {
32
+ // non-fatal — a re-trigger will just report "nothing cached yet"
33
+ }
34
+ }
35
+ export function loadLastMaintenanceTask(storageDir) {
36
+ const file = lastTaskFile(storageDir);
37
+ if (!existsSync(file))
38
+ return null;
39
+ try {
40
+ return JSON.parse(readFileSync(file, 'utf8'));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Pure execution: run one maintenance task on an ALREADY-BUILT kernel and
48
+ * return the report — no push, no process.exit. Shared by the standalone
49
+ * `infinicode maintain` CLI command (builds its own short-lived kernel) and
50
+ * `infinicode serve`'s inbound-command poller (reuses the node's live
51
+ * kernel, since that process is already running one).
52
+ */
53
+ export async function runMaintenanceTask(kernel, taskPrompt, opts) {
54
+ const { nodeId, role } = opts;
55
+ const preamble = rolePreamble(role);
56
+ const prompt = preamble ? `${preamble}\n\n${taskPrompt}` : taskPrompt;
57
+ const taskName = opts.taskName ?? taskPrompt.slice(0, 60);
58
+ const startedAt = Date.now();
59
+ let ok = true;
60
+ let summary = '';
61
+ const errors = [];
62
+ try {
63
+ const mission = await kernel.execute({
64
+ description: taskName,
65
+ goal: taskPrompt,
66
+ tasks: [{
67
+ description: taskName,
68
+ capabilities: ['reasoning'],
69
+ input: { prompt, context: { proactive: true, maintenance: true, role: role?.role } },
70
+ }],
71
+ });
72
+ ok = mission.status === 'COMPLETED';
73
+ if (mission.error)
74
+ errors.push(mission.error);
75
+ for (const task of mission.tasks) {
76
+ if (task.error)
77
+ errors.push(task.error);
78
+ }
79
+ summary = mission.tasks
80
+ .map(t => t.output?.content)
81
+ .filter((c) => Boolean(c))
82
+ .join('\n\n')
83
+ .slice(0, 4000) || (ok ? 'completed with no textual output' : 'mission did not complete');
84
+ }
85
+ catch (err) {
86
+ ok = false;
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ errors.push(message);
89
+ summary = `threw before completion: ${message}`;
90
+ }
91
+ const finishedAt = Date.now();
92
+ return {
93
+ nodeId,
94
+ role: role?.role ?? nodeId,
95
+ site: role?.site,
96
+ taskName,
97
+ startedAt,
98
+ finishedAt,
99
+ durationMs: finishedAt - startedAt,
100
+ ok,
101
+ summary,
102
+ errors: errors.length ? errors : undefined,
103
+ };
104
+ }
105
+ /** Open a MoltfedConnector just long enough to push one report, then close it.
106
+ * Does not start the periodic hw/status timers — this is a one-shot push, not
107
+ * a live mesh presence. */
108
+ async function pushReportOnce(gatewayUrl, report, opts) {
109
+ return new Promise((resolve) => {
110
+ let settled = false;
111
+ const finish = (ok) => {
112
+ if (settled)
113
+ return;
114
+ settled = true;
115
+ connector.stop();
116
+ resolve(ok);
117
+ };
118
+ const connector = new MoltfedConnector({
119
+ gatewayUrl,
120
+ token: opts.token,
121
+ password: opts.password,
122
+ role: opts.role,
123
+ client: { id: report.nodeId, version: '1', platform: 'node', mode: 'maintenance' },
124
+ logger: new SilentLogger(),
125
+ // Not used for a one-shot push, but required by the connector's shape.
126
+ getNodeStatuses: () => [],
127
+ getHardware: () => undefined,
128
+ subscribeFrames: () => () => { },
129
+ });
130
+ // Poll for handshake completion, then fire the report once (connect() is
131
+ // async/event-driven internally with no public "connected" callback).
132
+ const poll = setInterval(() => {
133
+ if (connector.isConnected) {
134
+ clearInterval(poll);
135
+ connector.request('infinicode.push.report', report);
136
+ // Give the WS a beat to actually flush before closing.
137
+ setTimeout(() => finish(true), 250);
138
+ }
139
+ }, 100);
140
+ connector.start();
141
+ setTimeout(() => { clearInterval(poll); finish(false); }, 8000);
142
+ });
143
+ }
144
+ export async function maintain(config, taskPrompt, opts) {
145
+ const role = loadRole();
146
+ // Reuses the SAME identity `infinicode serve` on this device already
147
+ // persisted (.openkernel/node-identity.json) — so this report's nodeId
148
+ // matches the nodeId that device's nodes.status push uses, instead of the
149
+ // two diverging into separate fleet records for the same physical device.
150
+ const identity = loadOrCreateIdentity({ displayName: opts.name ?? role?.role });
151
+ const nodeId = identity.nodeId;
152
+ const taskName = opts.taskName ?? taskPrompt.slice(0, 60);
153
+ // Cache this as "the task this device runs" — the ONLY thing an on-demand
154
+ // "run maintenance now" trigger from the Fleet UI is allowed to replay
155
+ // later (see loadLastMaintenanceTask). Locally sourced only; a network
156
+ // trigger never carries or supplies task content.
157
+ saveLastMaintenanceTask({ taskPrompt, taskName });
158
+ console.log(chalk.bold(`\n infinicode maintain — ${chalk.cyan(taskName)}`));
159
+ console.log(chalk.dim(` node: ${identity.displayName} (${nodeId})${role ? ` · role: ${role.role}` : ''}`));
160
+ const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
161
+ let report;
162
+ try {
163
+ report = await runMaintenanceTask(kernel, taskPrompt, { nodeId, role, taskName });
164
+ }
165
+ finally {
166
+ kernel.destroy();
167
+ }
168
+ const ok = report.ok;
169
+ console.log(ok ? chalk.green(` ✓ completed in ${report.durationMs}ms`) : chalk.red(` ✗ failed in ${report.durationMs}ms`));
170
+ if (report.errors?.length)
171
+ console.log(chalk.red(` ${report.errors.join('\n ')}`));
172
+ if (opts.gateway) {
173
+ const pushed = await pushReportOnce(opts.gateway, report, {
174
+ token: opts.gatewayToken,
175
+ password: opts.gatewayPassword,
176
+ role: role?.role ?? 'operator',
177
+ });
178
+ console.log(pushed
179
+ ? chalk.green(` ✓ report pushed to ${opts.gateway}`)
180
+ : chalk.yellow(` ⚠ could not confirm report delivery to ${opts.gateway} (handshake timed out)`));
181
+ }
182
+ else {
183
+ console.log(chalk.dim(' (no --gateway given — report was not pushed anywhere)'));
184
+ }
185
+ process.exit(ok ? 0 : 1);
186
+ }
@@ -70,7 +70,7 @@ export async function mcpCommand(config, opts) {
70
70
  identity,
71
71
  describe,
72
72
  logger: stderrLogger,
73
- options: { version, port, token, autoUpdate: fed.autoUpdate, applyConfig, autoPullConfig: role === 'satellite' },
73
+ options: { version, port, token, autoUpdate: fed.autoUpdate, applyConfig, autoPullConfig: role === 'satellite', coder: config.get('coder') },
74
74
  });
75
75
  // Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
76
76
  kernel.attachFederation(federation);
@@ -0,0 +1,10 @@
1
+ export interface MeshInstallOptions {
2
+ host?: string;
3
+ token?: string;
4
+ name?: string;
5
+ tailscale?: boolean;
6
+ lan?: boolean;
7
+ print?: boolean;
8
+ }
9
+ export declare function meshInstall(opts: MeshInstallOptions): Promise<void>;
10
+ export declare function meshUninstall(opts: MeshInstallOptions): Promise<void>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * infinicode mesh install — register the infinicode MCP server into a coding
3
+ * host's config automatically, so the user never hand-edits a JSON file.
4
+ *
5
+ * infinicode mesh install → add to Claude Code (~/.claude.json)
6
+ * infinicode mesh install --host all → Claude Code + OpenCode
7
+ * infinicode mesh install --token secret → gated mesh
8
+ * infinicode mesh uninstall → remove it again
9
+ *
10
+ * After it runs, the user just restarts their coding host and the mesh tools
11
+ * (list_nodes / spawn_agent / send_message …) are available. On another laptop
12
+ * with the same one command, the two nodes auto-discover over the LAN.
13
+ */
14
+ import chalk from 'chalk';
15
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { findExecutable } from '../kernel/agents/backends/detect.js';
20
+ /** How a host should spawn the infinicode MCP node. Resolves a global binary
21
+ * if present, else falls back to `node <this package's cli.js>`. */
22
+ function resolveLaunch() {
23
+ const onPath = findExecutable('infinicode');
24
+ if (onPath)
25
+ return { command: 'infinicode', baseArgs: [] };
26
+ const cliJs = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli.js');
27
+ return { command: process.execPath, baseArgs: [cliJs] };
28
+ }
29
+ /** The `infinicode mcp …` argv the host should run. */
30
+ function meshArgs(opts) {
31
+ 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);
38
+ if (opts.name)
39
+ args.push('--name', opts.name);
40
+ return args;
41
+ }
42
+ function readJson(path) {
43
+ if (!existsSync(path))
44
+ return {};
45
+ try {
46
+ return JSON.parse(readFileSync(path, 'utf8'));
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ }
52
+ /** Back up a config once before the first infinicode edit, then write pretty JSON. */
53
+ function writeJson(path, data) {
54
+ const dir = dirname(path);
55
+ if (!existsSync(dir))
56
+ mkdirSync(dir, { recursive: true });
57
+ if (existsSync(path)) {
58
+ const bak = `${path}.infinicode.bak`;
59
+ if (!existsSync(bak))
60
+ copyFileSync(path, bak);
61
+ }
62
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
63
+ }
64
+ const CLAUDE_CONFIG = () => join(homedir(), '.claude.json');
65
+ const OPENCODE_CONFIG = () => join(homedir(), '.config', 'opencode', 'opencode.json');
66
+ /** Claude Code: mcpServers.infinicode = { command, args }. */
67
+ function installClaude(opts) {
68
+ const path = CLAUDE_CONFIG();
69
+ const cfg = readJson(path);
70
+ const servers = cfg.mcpServers ?? {};
71
+ const existed = 'infinicode' in servers;
72
+ const { command, baseArgs } = resolveLaunch();
73
+ const entry = { command, args: [...baseArgs, ...meshArgs(opts)] };
74
+ if (!opts.print) {
75
+ cfg.mcpServers = { ...servers, infinicode: entry };
76
+ writeJson(path, cfg);
77
+ }
78
+ return { path, entry, existed };
79
+ }
80
+ /** OpenCode: mcp.infinicode = { type:'local', command:[...], enabled:true }. */
81
+ function installOpencode(opts) {
82
+ const path = OPENCODE_CONFIG();
83
+ const cfg = readJson(path);
84
+ const mcp = cfg.mcp ?? {};
85
+ const existed = 'infinicode' in mcp;
86
+ const { command, baseArgs } = resolveLaunch();
87
+ const entry = { type: 'local', command: [command, ...baseArgs, ...meshArgs(opts)], enabled: true };
88
+ if (!opts.print) {
89
+ cfg.mcp = { ...mcp, infinicode: entry };
90
+ writeJson(path, cfg);
91
+ }
92
+ return { path, entry, existed };
93
+ }
94
+ function removeFrom(path, key) {
95
+ if (!existsSync(path))
96
+ return false;
97
+ const cfg = readJson(path);
98
+ const bag = cfg[key];
99
+ if (!bag || !('infinicode' in bag))
100
+ return false;
101
+ delete bag.infinicode;
102
+ cfg[key] = bag;
103
+ writeJson(path, cfg);
104
+ return true;
105
+ }
106
+ function selectedHosts(host) {
107
+ const h = (host ?? 'claude').toLowerCase();
108
+ if (h === 'all')
109
+ return ['claude', 'opencode'];
110
+ if (h === 'opencode')
111
+ return ['opencode'];
112
+ if (h === 'claude' || h === 'claude-code')
113
+ return ['claude'];
114
+ return ['claude'];
115
+ }
116
+ export async function meshInstall(opts) {
117
+ const hosts = selectedHosts(opts.host);
118
+ console.log(chalk.bold('\n🕸 infinicode mesh — MCP registration\n'));
119
+ for (const host of hosts) {
120
+ const res = host === 'claude' ? installClaude(opts) : installOpencode(opts);
121
+ const label = host === 'claude' ? 'Claude Code' : 'OpenCode';
122
+ const verb = opts.print ? 'would write' : res.existed ? 'updated' : 'added';
123
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(label)} — ${verb} ${chalk.dim(res.path)}`);
124
+ console.log(chalk.dim(` ${JSON.stringify(res.entry)}`));
125
+ }
126
+ // Warn if the target CLI backend isn't installed on THIS machine — a worker
127
+ // node can only run agents for CLIs it actually has.
128
+ const present = ['claude', 'codex', 'opencode'].filter(b => findExecutable(b));
129
+ console.log();
130
+ if (present.length) {
131
+ console.log(` ${chalk.green('•')} coding CLIs detected here (can execute agents): ${chalk.bold(present.join(', '))}`);
132
+ }
133
+ else {
134
+ console.log(` ${chalk.yellow('•')} no coding CLI (claude/codex/opencode) found on PATH — this device can drive the mesh but can't execute agents until one is installed.`);
135
+ }
136
+ if (opts.print) {
137
+ console.log(chalk.dim('\n (dry run — nothing was written)\n'));
138
+ return;
139
+ }
140
+ 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) {
145
+ console.log(chalk.dim(`\n Tip: on an untrusted network, re-run with --token <secret> on every device.`));
146
+ }
147
+ console.log();
148
+ }
149
+ export async function meshUninstall(opts) {
150
+ const hosts = selectedHosts(opts.host);
151
+ console.log(chalk.bold('\n🕸 infinicode mesh — removing MCP registration\n'));
152
+ for (const host of hosts) {
153
+ const path = host === 'claude' ? CLAUDE_CONFIG() : OPENCODE_CONFIG();
154
+ const key = host === 'claude' ? 'mcpServers' : 'mcp';
155
+ const removed = removeFrom(path, key);
156
+ const label = host === 'claude' ? 'Claude Code' : 'OpenCode';
157
+ console.log(` ${removed ? chalk.green('✓ removed from') : chalk.dim('· not present in')} ${chalk.cyan(label)} ${chalk.dim(path)}`);
158
+ }
159
+ console.log(chalk.dim('\n Restart your coding host to apply.\n'));
160
+ }
@@ -0,0 +1,6 @@
1
+ import type { Command } from 'commander';
2
+ import type Conf from 'conf';
3
+ import type { InfinicodeConfig } from '../kernel/config-schema.js';
4
+ /** Attach the robopark verbs to a Commander command (the `robopark` program in
5
+ * the standalone bin, or the `infinicode robopark` group). */
6
+ export declare function attachRoboparkSubcommands(cmd: Command, config: Conf<InfinicodeConfig>): void;