infinicode 2.3.6 → 2.5.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 (34) hide show
  1. package/bin/robopark.js +2 -0
  2. package/dist/cli.js +26 -1
  3. package/dist/commands/maintain.d.ts +29 -0
  4. package/dist/commands/maintain.js +186 -0
  5. package/dist/commands/mcp.js +1 -1
  6. package/dist/commands/robopark.d.ts +6 -0
  7. package/dist/commands/robopark.js +450 -0
  8. package/dist/commands/serve.d.ts +5 -0
  9. package/dist/commands/serve.js +170 -11
  10. package/dist/kernel/agents/backends/cli-backend.d.ts +4 -0
  11. package/dist/kernel/agents/backends/cli-backend.js +13 -5
  12. package/dist/kernel/agents/backends/detect.d.ts +12 -0
  13. package/dist/kernel/agents/backends/detect.js +102 -2
  14. package/dist/kernel/agents/backends/profiles.d.ts +67 -0
  15. package/dist/kernel/agents/backends/profiles.js +48 -0
  16. package/dist/kernel/agents/backends/registry.d.ts +2 -1
  17. package/dist/kernel/agents/backends/registry.js +39 -5
  18. package/dist/kernel/agents/backends/rotation.d.ts +51 -0
  19. package/dist/kernel/agents/backends/rotation.js +107 -0
  20. package/dist/kernel/agents/backends/types.d.ts +3 -1
  21. package/dist/kernel/federation/dashboard-html.d.ts +13 -0
  22. package/dist/kernel/federation/dashboard-html.js +561 -0
  23. package/dist/kernel/federation/federation.d.ts +75 -1
  24. package/dist/kernel/federation/federation.js +119 -23
  25. package/dist/kernel/federation/moltfed-adapter.js +1 -0
  26. package/dist/kernel/federation/telemetry.d.ts +1 -1
  27. package/dist/kernel/federation/telemetry.js +2 -1
  28. package/dist/kernel/federation/transport-http.d.ts +34 -1
  29. package/dist/kernel/federation/transport-http.js +117 -4
  30. package/dist/kernel/federation/types.d.ts +20 -0
  31. package/dist/kernel/mcp/mcp-server.js +23 -7
  32. package/dist/robopark-cli.d.ts +2 -0
  33. package/dist/robopark-cli.js +23 -0
  34. package/package.json +7 -3
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/robopark-cli.js');
package/dist/cli.js CHANGED
@@ -14,10 +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';
17
18
  import { meshInstall, meshUninstall } from './commands/mesh-install.js';
18
19
  import { installTui } from './commands/install-tui.js';
20
+ import { attachRoboparkSubcommands } from './commands/robopark.js';
19
21
  import { DEFAULT_CONFIG } from './kernel/config-schema.js';
20
- const VERSION = '2.3.6';
22
+ const VERSION = '2.5.0';
21
23
  const config = new Conf({
22
24
  projectName: 'infinicode',
23
25
  defaults: DEFAULT_CONFIG,
@@ -293,12 +295,35 @@ program
293
295
  .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
294
296
  .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
295
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)')
299
+ .option('--scheduler-url <url>', 'link a RoboPark scheduler so the dashboard shows sessions + telemetry (e.g. http://localhost:8080)')
300
+ .option('--scheduler-token <token>', 'bearer token to present to the scheduler on proxied requests')
301
+ .option('--tls-cert <path>', 'PEM cert file — serve the dashboard/mesh over HTTPS')
302
+ .option('--tls-key <path>', 'PEM private key file for --tls-cert')
296
303
  .option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
297
304
  .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
298
305
  .option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
299
306
  .action(async (opts) => {
300
307
  await serve(config, opts);
301
308
  });
309
+ // ── RoboPark: fleet control overlay (also shipped as the `robopark` bin) ────
310
+ const roboparkCmd = program
311
+ .command('robopark')
312
+ .alias('rp')
313
+ .description('RoboPark fleet control — setup, fleet status, run, apply, dashboard (rides the mesh)');
314
+ attachRoboparkSubcommands(roboparkCmd, config);
315
+ // ── OpenKernel: one-shot scheduled maintenance ─────────────────────────────
316
+ program
317
+ .command('maintain <task-prompt>')
318
+ .description('Run one maintenance task and exit — meant for OS cron/Task Scheduler, not a daemon')
319
+ .option('--name <name>', 'node name for the report (defaults to this node\'s role, or "infinicode-node")')
320
+ .option('--task-name <name>', 'short label for the report (defaults to a slice of the task prompt)')
321
+ .option('--gateway <url>', 'InfiniBot gateway to push the report to (ws://host:18789)')
322
+ .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
323
+ .option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
324
+ .action(async (taskPrompt, opts) => {
325
+ await maintain(config, taskPrompt, opts);
326
+ });
302
327
  // ── infinicode: mesh — one-command MCP registration (no hand-edited JSON) ──
303
328
  const meshCmd = program
304
329
  .command('mesh')
@@ -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,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;