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.
- package/.opencode/plugins/home-logo-animation.tsx +5 -1
- package/.opencode/plugins/mesh-commands.tsx +4 -3
- package/.opencode/plugins/routing-mode-display.tsx +6 -2
- package/.opencode/tui.json +2 -1
- package/bin/robopark.js +2 -0
- package/dist/cli.js +46 -1
- package/dist/commands/maintain.d.ts +29 -0
- package/dist/commands/maintain.js +186 -0
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mesh-install.d.ts +10 -0
- package/dist/commands/mesh-install.js +160 -0
- package/dist/commands/robopark.d.ts +6 -0
- package/dist/commands/robopark.js +450 -0
- package/dist/commands/run.js +82 -22
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +152 -11
- package/dist/kernel/agents/backends/cli-backend.d.ts +41 -0
- package/dist/kernel/agents/backends/cli-backend.js +140 -0
- package/dist/kernel/agents/backends/detect.d.ts +17 -0
- package/dist/kernel/agents/backends/detect.js +140 -0
- package/dist/kernel/agents/backends/index.d.ts +11 -0
- package/dist/kernel/agents/backends/index.js +3 -0
- package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
- package/dist/kernel/agents/backends/parse-utils.js +72 -0
- package/dist/kernel/agents/backends/profiles.d.ts +67 -0
- package/dist/kernel/agents/backends/profiles.js +48 -0
- package/dist/kernel/agents/backends/registry.d.ts +31 -0
- package/dist/kernel/agents/backends/registry.js +174 -0
- package/dist/kernel/agents/backends/rotation.d.ts +51 -0
- package/dist/kernel/agents/backends/rotation.js +107 -0
- package/dist/kernel/agents/backends/types.d.ts +67 -0
- package/dist/kernel/agents/backends/types.js +1 -0
- package/dist/kernel/agents/index.d.ts +8 -0
- package/dist/kernel/agents/index.js +8 -0
- package/dist/kernel/federation/dashboard-html.d.ts +13 -0
- package/dist/kernel/federation/dashboard-html.js +371 -0
- package/dist/kernel/federation/federation.d.ts +112 -2
- package/dist/kernel/federation/federation.js +270 -10
- package/dist/kernel/federation/moltfed-adapter.js +1 -0
- package/dist/kernel/federation/telemetry.d.ts +1 -1
- package/dist/kernel/federation/telemetry.js +2 -1
- package/dist/kernel/federation/transport-http.d.ts +17 -1
- package/dist/kernel/federation/transport-http.js +65 -2
- package/dist/kernel/federation/types.d.ts +46 -1
- package/dist/kernel/free-providers.js +83 -0
- package/dist/kernel/gateway/openai-gateway.d.ts +55 -3
- package/dist/kernel/gateway/openai-gateway.js +428 -49
- package/dist/kernel/index.d.ts +2 -1
- package/dist/kernel/index.js +3 -1
- package/dist/kernel/logger.d.ts +16 -3
- package/dist/kernel/logger.js +38 -0
- package/dist/kernel/mcp/mcp-server.js +65 -10
- package/dist/kernel/orchestrator.d.ts +4 -0
- package/dist/kernel/orchestrator.js +34 -0
- package/dist/kernel/provider-manager.d.ts +13 -0
- package/dist/kernel/provider-manager.js +58 -5
- package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
- package/dist/kernel/providers/openai-compatible-provider.js +22 -3
- package/dist/kernel/recovery-manager.js +55 -15
- package/dist/kernel/router.js +15 -2
- package/dist/kernel/types.d.ts +12 -2
- package/dist/robopark-cli.d.ts +2 -0
- package/dist/robopark-cli.js +23 -0
- package/package.json +6 -3
|
@@ -109,7 +109,7 @@ function AnimatedLogo(props: { renderer?: any }) {
|
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
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:
|
|
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
|
-
|
|
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 };
|
package/.opencode/tui.json
CHANGED
package/bin/robopark.js
ADDED
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.
|
|
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
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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;
|