infinicode 2.3.2 → 2.3.4
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/mesh-commands.tsx +139 -0
- package/dist/cli.js +2 -1
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/run.js +100 -3
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +36 -1
- package/dist/kernel/capability-map.d.ts +18 -0
- package/dist/kernel/capability-map.js +84 -0
- package/dist/kernel/checkpoint-engine.js +9 -0
- package/dist/kernel/command-system.d.ts +3 -0
- package/dist/kernel/command-system.js +315 -10
- package/dist/kernel/config-schema.d.ts +11 -0
- package/dist/kernel/config-schema.js +1 -0
- package/dist/kernel/federation/event-bridge.js +3 -1
- package/dist/kernel/federation/federation.d.ts +10 -0
- package/dist/kernel/federation/federation.js +29 -9
- package/dist/kernel/federation/transport-http.d.ts +3 -0
- package/dist/kernel/federation/transport-http.js +25 -0
- package/dist/kernel/free-providers.d.ts +23 -0
- package/dist/kernel/free-providers.js +73 -1
- package/dist/kernel/index.d.ts +4 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/kernel.d.ts +12 -0
- package/dist/kernel/kernel.js +29 -2
- package/dist/kernel/mission-engine.js +14 -0
- package/dist/kernel/orchestrator.d.ts +3 -0
- package/dist/kernel/orchestrator.js +74 -2
- package/dist/kernel/planner.d.ts +42 -0
- package/dist/kernel/planner.js +174 -0
- package/dist/kernel/recovery-manager.d.ts +9 -0
- package/dist/kernel/recovery-manager.js +64 -3
- package/dist/kernel/router.d.ts +12 -1
- package/dist/kernel/router.js +36 -1
- package/dist/kernel/scheduler.d.ts +17 -0
- package/dist/kernel/scheduler.js +97 -1
- package/dist/kernel/setup.d.ts +9 -0
- package/dist/kernel/setup.js +38 -0
- package/dist/kernel/types.d.ts +65 -2
- package/dist/kernel/verification-engine.js +83 -27
- package/dist/kernel/verification-exec.d.ts +14 -0
- package/dist/kernel/verification-exec.js +65 -0
- package/dist/kernel/worker-runtime.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* infinicode — mesh command palette
|
|
4
|
+
*
|
|
5
|
+
* Adds the OpenKernel mesh/kernel slash-commands natively to the TUI's `/`
|
|
6
|
+
* command palette. Each command is dispatched to a LOCAL mesh node over HTTP
|
|
7
|
+
* (POST /fed/command → kernel.command()), so the same command surface exposed by
|
|
8
|
+
* `infinicode serve`'s console and the CLI is available right inside the TUI.
|
|
9
|
+
*
|
|
10
|
+
* The local node is started (or reused) by `infinicode run`, which injects:
|
|
11
|
+
* INFINICODE_MESH_URL e.g. http://127.0.0.1:47913
|
|
12
|
+
* INFINICODE_MESH_TOKEN optional bearer token
|
|
13
|
+
*
|
|
14
|
+
* Argless commands (/nodes, /running, /activity, /whoami, /mesh-help) run
|
|
15
|
+
* immediately and show output in an alert. Arg-taking commands (/node, /build,
|
|
16
|
+
* /follow, /dispatch, /goal, /mission, /role) open a prompt first.
|
|
17
|
+
*/
|
|
18
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
19
|
+
import { createBindingLookup } from "@opentui/keymap/extras"
|
|
20
|
+
|
|
21
|
+
type CommandResult = { ok?: boolean; text?: string; data?: unknown }
|
|
22
|
+
|
|
23
|
+
const meshUrl = () => (process.env.INFINICODE_MESH_URL ?? "http://127.0.0.1:47913").replace(/\/+$/, "")
|
|
24
|
+
const meshToken = () => process.env.INFINICODE_MESH_TOKEN
|
|
25
|
+
|
|
26
|
+
/** POST a slash-command to the local mesh node and return its CommandResult. */
|
|
27
|
+
async function send(input: string): Promise<CommandResult> {
|
|
28
|
+
const token = meshToken()
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`${meshUrl()}/fed/command`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({ input }),
|
|
37
|
+
signal: AbortSignal.timeout(30_000),
|
|
38
|
+
})
|
|
39
|
+
if (!res.ok) return { ok: false, text: `mesh error ${res.status} — is a node running? (infinicode serve)` }
|
|
40
|
+
return (await res.json()) as CommandResult
|
|
41
|
+
} catch (e) {
|
|
42
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
43
|
+
return { ok: false, text: `no local mesh node reachable at ${meshUrl()} (${msg})` }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render a CommandResult in a scrollable alert dialog. */
|
|
48
|
+
function show(api: TuiPluginApi, title: string, res: CommandResult): void {
|
|
49
|
+
const DialogAlert = api.ui.DialogAlert
|
|
50
|
+
const message = (res.text && res.text.trim()) || (res.ok ? "(no output)" : "command failed")
|
|
51
|
+
api.ui.dialog.setSize("large")
|
|
52
|
+
api.ui.dialog.replace(() => (
|
|
53
|
+
<DialogAlert title={title} message={message} onConfirm={() => api.ui.dialog.clear()} />
|
|
54
|
+
))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Run a fixed slash-command now and show the result. */
|
|
58
|
+
function runNow(api: TuiPluginApi, title: string, input: string): void {
|
|
59
|
+
api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
|
|
60
|
+
void send(input).then((res) => show(api, title, res))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Open a prompt, then dispatch `<slash> <entered args>`. */
|
|
64
|
+
function runWithArgs(
|
|
65
|
+
api: TuiPluginApi,
|
|
66
|
+
title: string,
|
|
67
|
+
slash: string,
|
|
68
|
+
placeholder: string,
|
|
69
|
+
): void {
|
|
70
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
71
|
+
api.ui.dialog.setSize("medium")
|
|
72
|
+
api.ui.dialog.replace(() => (
|
|
73
|
+
<DialogPrompt
|
|
74
|
+
title={title}
|
|
75
|
+
value=""
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
onConfirm={(args: string) => {
|
|
78
|
+
api.ui.dialog.clear()
|
|
79
|
+
const input = args && args.trim() ? `${slash} ${args.trim()}` : slash
|
|
80
|
+
api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
|
|
81
|
+
void send(input).then((res) => show(api, title, res))
|
|
82
|
+
}}
|
|
83
|
+
onCancel={() => api.ui.dialog.clear()}
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface Spec {
|
|
89
|
+
name: string
|
|
90
|
+
slashName: string
|
|
91
|
+
title: string
|
|
92
|
+
/** if set, prompt for args first with this placeholder; else run immediately */
|
|
93
|
+
arg?: string
|
|
94
|
+
/** the underlying kernel slash-command (defaults to `/${slashName}`) */
|
|
95
|
+
cmd?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SPECS: Spec[] = [
|
|
99
|
+
{ name: "mesh_nodes", slashName: "nodes", title: "Mesh nodes" },
|
|
100
|
+
{ name: "mesh_running", slashName: "running", title: "Running tasks" },
|
|
101
|
+
{ name: "mesh_activity", slashName: "activity", title: "Mesh activity" },
|
|
102
|
+
{ name: "mesh_whoami", slashName: "whoami", title: "This node" },
|
|
103
|
+
{ name: "mesh_help", slashName: "mesh-help", title: "Mesh commands", cmd: "/help" },
|
|
104
|
+
{ name: "mesh_node", slashName: "node", title: "Node control", arg: "<node> <message>", cmd: "/node" },
|
|
105
|
+
{ name: "mesh_build", slashName: "build", title: "Phased build", arg: "<node> <goal>", cmd: "/build" },
|
|
106
|
+
{ name: "mesh_dispatch", slashName: "dispatch", title: "Dispatch task", arg: "<node> <task>", cmd: "/dispatch" },
|
|
107
|
+
{ name: "mesh_follow", slashName: "follow", title: "Follow run", arg: "<run-id>", cmd: "/follow" },
|
|
108
|
+
{ name: "mesh_goal", slashName: "goal", title: "New goal", arg: "<goal description>", cmd: "/goal" },
|
|
109
|
+
{ name: "mesh_mission", slashName: "mission", title: "Mission", arg: "<mission description>", cmd: "/mission" },
|
|
110
|
+
{ name: "mesh_role", slashName: "role", title: "Set role", arg: "<role>", cmd: "/role" },
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
const tui: TuiPlugin = async (api, options) => {
|
|
114
|
+
if (options?.enabled === false) return
|
|
115
|
+
const keys = createBindingLookup({})
|
|
116
|
+
|
|
117
|
+
api.keymap.registerLayer({
|
|
118
|
+
commands: SPECS.map((s) => ({
|
|
119
|
+
name: s.name,
|
|
120
|
+
title: s.title,
|
|
121
|
+
category: "Mesh",
|
|
122
|
+
namespace: "palette" as const,
|
|
123
|
+
slashName: s.slashName,
|
|
124
|
+
run() {
|
|
125
|
+
const cmd = s.cmd ?? `/${s.slashName}`
|
|
126
|
+
if (s.arg) runWithArgs(api, s.title, cmd, s.arg)
|
|
127
|
+
else runNow(api, s.title, cmd)
|
|
128
|
+
},
|
|
129
|
+
})),
|
|
130
|
+
bindings: keys.gather("mesh.global", SPECS.map((s) => s.name)),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
135
|
+
id: "infinicode-mesh-commands",
|
|
136
|
+
tui,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default plugin
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { serve } from './commands/serve.js';
|
|
|
16
16
|
import { mcpCommand } from './commands/mcp.js';
|
|
17
17
|
import { installTui } from './commands/install-tui.js';
|
|
18
18
|
import { DEFAULT_CONFIG } from './kernel/config-schema.js';
|
|
19
|
-
const VERSION = '2.3.
|
|
19
|
+
const VERSION = '2.3.4';
|
|
20
20
|
const config = new Conf({
|
|
21
21
|
projectName: 'infinicode',
|
|
22
22
|
defaults: DEFAULT_CONFIG,
|
|
@@ -291,6 +291,7 @@ program
|
|
|
291
291
|
.option('--lan', 'auto-discover + connect peers on the same LAN via UDP broadcast (no seed/Tailscale needed)')
|
|
292
292
|
.option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
|
|
293
293
|
.option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
|
|
294
|
+
.option('--console', 'interactive slash-command console on the node (/nodes, /build, /activity …); on by default in a TTY')
|
|
294
295
|
.option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
|
|
295
296
|
.option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
|
|
296
297
|
.option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
|
package/dist/commands/mcp.js
CHANGED
|
@@ -72,6 +72,8 @@ export async function mcpCommand(config, opts) {
|
|
|
72
72
|
logger: stderrLogger,
|
|
73
73
|
options: { version, port, token, autoUpdate: fed.autoUpdate, applyConfig, autoPullConfig: role === 'satellite' },
|
|
74
74
|
});
|
|
75
|
+
// Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
|
|
76
|
+
kernel.attachFederation(federation);
|
|
75
77
|
// Hub/peer/relay: publish local providers + policy for satellites to source.
|
|
76
78
|
const sharedFromConf = () => ({
|
|
77
79
|
cloudProviders: config.get('cloudProviders'),
|
package/dist/commands/run.js
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import ora from 'ora';
|
|
19
|
-
import { existsSync } from 'node:fs';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { dirname, join, resolve } from 'node:path';
|
|
21
|
-
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
23
|
import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from '../ascii-video-animation.js';
|
|
24
24
|
import { getProviderPreset } from '../kernel/free-providers.js';
|
|
25
25
|
import { buildKernelFromConfig } from '../kernel/setup.js';
|
|
26
|
-
import { SilentLogger } from '../kernel/index.js';
|
|
26
|
+
import { SilentLogger, Federation, loadOrCreateIdentity } from '../kernel/index.js';
|
|
27
27
|
import { openAICompatBaseURL } from '../kernel/provider-url.js';
|
|
28
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
29
|
const PROMPT_ANIMATION_MAX_FRAMES = Math.min(ASCII_VIDEO_FRAMES.length, ASCII_VIDEO_FPS * 3);
|
|
@@ -309,6 +309,81 @@ function ensureModelInjected(providers, providerId, modelId) {
|
|
|
309
309
|
if (!p.models[modelId])
|
|
310
310
|
p.models[modelId] = { name: modelId };
|
|
311
311
|
}
|
|
312
|
+
function runPkgVersion() {
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(readFileSync(join(resolve(__dirname, '..', '..'), 'package.json'), 'utf8')).version ?? '0.0.0';
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return '0.0.0';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Is a mesh node already listening at this URL? (e.g. a separate `infinicode serve`). */
|
|
321
|
+
async function meshAlive(url, token) {
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch(`${url}/fed/manifest`, {
|
|
324
|
+
headers: token ? { authorization: `Bearer ${token}` } : undefined,
|
|
325
|
+
signal: AbortSignal.timeout(1500),
|
|
326
|
+
});
|
|
327
|
+
return res.ok;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Make the TUI a mesh participant so the in-TUI slash commands (/nodes, /build,
|
|
335
|
+
* /activity …) have a local node to talk to. If a node is already running on the
|
|
336
|
+
* configured port (a separate `infinicode serve`), point the TUI at THAT one;
|
|
337
|
+
* otherwise start an in-process node for the session. Best-effort: on any
|
|
338
|
+
* failure the TUI still launches, just without the live mesh palette.
|
|
339
|
+
*/
|
|
340
|
+
async function startLocalMeshNode(config) {
|
|
341
|
+
if (process.env.INFINICODE_NO_MESH)
|
|
342
|
+
return null;
|
|
343
|
+
const fed = config.get('federation') ?? {};
|
|
344
|
+
const port = fed.port ?? 47913;
|
|
345
|
+
const token = fed.token;
|
|
346
|
+
const url = `http://127.0.0.1:${port}`;
|
|
347
|
+
try {
|
|
348
|
+
// Reuse an already-running node (don't double-bind the port).
|
|
349
|
+
if (await meshAlive(url, token))
|
|
350
|
+
return { url, token, stop: async () => { } };
|
|
351
|
+
const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
|
|
352
|
+
const role = (fed.role ?? 'peer');
|
|
353
|
+
const identity = loadOrCreateIdentity({ role, displayName: fed.displayName });
|
|
354
|
+
const version = runPkgVersion();
|
|
355
|
+
const describe = () => {
|
|
356
|
+
const defs = kernel.workerRuntime.listRegisteredDefinitions();
|
|
357
|
+
return {
|
|
358
|
+
version,
|
|
359
|
+
capabilities: [...new Set(defs.flatMap(d => d.capabilities))],
|
|
360
|
+
workers: defs.map(d => d.type),
|
|
361
|
+
commands: kernel.commands.list().map(c => c.name),
|
|
362
|
+
providers: kernel.providerManager.listInfos().map(p => p.id),
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const federation = new Federation({
|
|
366
|
+
kernel, identity, describe, logger: new SilentLogger(),
|
|
367
|
+
options: {
|
|
368
|
+
version, port, host: fed.host, token, seeds: fed.seeds,
|
|
369
|
+
autoPullConfig: role === 'satellite',
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
kernel.attachFederation(federation);
|
|
373
|
+
await federation.start();
|
|
374
|
+
if (fed.lan)
|
|
375
|
+
federation.startLanDiscovery(undefined, fed.lanPort);
|
|
376
|
+
for (const s of fed.seeds ?? [])
|
|
377
|
+
await federation.connect(s).catch(() => undefined);
|
|
378
|
+
return {
|
|
379
|
+
url, token,
|
|
380
|
+
stop: async () => { await federation.stop(); kernel.destroy(); },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
312
387
|
export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
313
388
|
const masterUrl = await resolveConfiguredMasterUrl(config);
|
|
314
389
|
const masterReachable = await isOllamaReachable(masterUrl);
|
|
@@ -316,6 +391,15 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
316
391
|
const cloudProviders = config.get('cloudProviders') ?? [];
|
|
317
392
|
const enabledCloud = cloudProviders.filter(p => p.enabled);
|
|
318
393
|
const policyName = config.get('policy') ?? 'balanced';
|
|
394
|
+
// Make this TUI session a mesh participant so the in-TUI slash commands
|
|
395
|
+
// (/nodes, /build, /activity, …) have a local node to talk to. Reuses a
|
|
396
|
+
// running `infinicode serve` if one is up, else starts an in-process node.
|
|
397
|
+
const mesh = await startLocalMeshNode(config);
|
|
398
|
+
if (mesh) {
|
|
399
|
+
process.env.INFINICODE_MESH_URL = mesh.url;
|
|
400
|
+
if (mesh.token)
|
|
401
|
+
process.env.INFINICODE_MESH_TOKEN = mesh.token;
|
|
402
|
+
}
|
|
319
403
|
// One kernel pass: load every provider's full model catalog (in parallel) and
|
|
320
404
|
// route the default model under the policy.
|
|
321
405
|
const routeSpinner = ora('loading model catalog & routing...').start();
|
|
@@ -340,12 +424,23 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
340
424
|
const policy = config.get('policy') ?? 'local-first';
|
|
341
425
|
const pinnedCount = Object.keys(config.get('workerModels') ?? {}).length;
|
|
342
426
|
const routingInfo = `AUTO ${policy} ${providerCount}P ${pinnedCount}pins`;
|
|
427
|
+
// Runtime TUI plugin that adds the mesh slash-commands (/nodes, /build, …)
|
|
428
|
+
// to the `/` palette. Shipped in the package at .opencode/plugins; loaded via
|
|
429
|
+
// a file:// URL so it resolves regardless of the TUI's cwd. Only injected when
|
|
430
|
+
// a local mesh node is available (else the commands would have nothing to hit).
|
|
431
|
+
const pluginPaths = [];
|
|
432
|
+
if (mesh) {
|
|
433
|
+
const pluginFile = join(resolve(__dirname, '..', '..'), '.opencode', 'plugins', 'mesh-commands.tsx');
|
|
434
|
+
if (existsSync(pluginFile))
|
|
435
|
+
pluginPaths.push(pathToFileURL(pluginFile).href);
|
|
436
|
+
}
|
|
343
437
|
process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
344
438
|
$schema: 'https://opencode.ai/config.json',
|
|
345
439
|
provider: providers,
|
|
346
440
|
model: modelRef,
|
|
347
441
|
small_model: modelRef,
|
|
348
442
|
enabled_providers: Object.keys(providers),
|
|
443
|
+
...(pluginPaths.length > 0 ? { plugin: pluginPaths } : {}),
|
|
349
444
|
agent: {
|
|
350
445
|
build: {
|
|
351
446
|
model: modelRef,
|
|
@@ -392,10 +487,12 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
|
|
|
392
487
|
});
|
|
393
488
|
}
|
|
394
489
|
catch (e) {
|
|
490
|
+
await mesh?.stop().catch(() => undefined);
|
|
395
491
|
const err = e;
|
|
396
492
|
if (err.exitCode) {
|
|
397
493
|
process.exit(err.exitCode);
|
|
398
494
|
}
|
|
399
495
|
throw e;
|
|
400
496
|
}
|
|
497
|
+
await mesh?.stop().catch(() => undefined);
|
|
401
498
|
}
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -77,6 +77,8 @@ export async function serve(config, opts) {
|
|
|
77
77
|
autoPullConfig: role === 'satellite',
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
+
// Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
|
|
81
|
+
kernel.attachFederation(federation);
|
|
80
82
|
// Hub/peer/relay: publish this machine's providers + policy so satellites can
|
|
81
83
|
// auto-source them. Re-publish when the local config changes (live).
|
|
82
84
|
const sharedFromConf = () => ({
|
|
@@ -133,7 +135,6 @@ export async function serve(config, opts) {
|
|
|
133
135
|
connector.start();
|
|
134
136
|
console.log(chalk.green(` ✓ linking to InfiniBot gateway ${opts.gateway}`));
|
|
135
137
|
}
|
|
136
|
-
console.log(chalk.dim(`\n mesh online — Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
|
|
137
138
|
const shutdown = async () => {
|
|
138
139
|
console.log(chalk.dim('\n stopping mesh…'));
|
|
139
140
|
connector?.stop();
|
|
@@ -143,6 +144,40 @@ export async function serve(config, opts) {
|
|
|
143
144
|
};
|
|
144
145
|
process.on('SIGINT', () => void shutdown());
|
|
145
146
|
process.on('SIGTERM', () => void shutdown());
|
|
147
|
+
// Interactive slash-command console on the live mesh node: type /nodes,
|
|
148
|
+
// /build <node> <goal>, /activity, /running, /follow <id>, etc. Enabled with
|
|
149
|
+
// --console (or automatically when stdin is a TTY).
|
|
150
|
+
if (opts.console ?? process.stdin.isTTY) {
|
|
151
|
+
const { createInterface } = await import('node:readline');
|
|
152
|
+
console.log(chalk.dim(`\n mesh online — slash-command console ready. Type /help or /nodes. Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
|
|
153
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: chalk.cyan('mesh> ') });
|
|
154
|
+
rl.prompt();
|
|
155
|
+
rl.on('line', async (line) => {
|
|
156
|
+
const input = line.trim();
|
|
157
|
+
if (!input) {
|
|
158
|
+
rl.prompt();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (input === 'exit' || input === 'quit') {
|
|
162
|
+
await shutdown();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
rl.pause();
|
|
166
|
+
try {
|
|
167
|
+
const result = await kernel.command(input.startsWith('/') ? input : '/' + input);
|
|
168
|
+
console.log((result.ok ? chalk.reset : chalk.yellow)(result.text));
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
console.log(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
172
|
+
}
|
|
173
|
+
rl.resume();
|
|
174
|
+
rl.prompt();
|
|
175
|
+
});
|
|
176
|
+
rl.on('close', () => void shutdown());
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log(chalk.dim(`\n mesh online — Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
|
|
180
|
+
}
|
|
146
181
|
// Keep the process alive.
|
|
147
182
|
await new Promise(() => { });
|
|
148
183
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Capability Normalization
|
|
3
|
+
*
|
|
4
|
+
* Harness-friendly capability passing. A phase (from a harness like InfiniBot,
|
|
5
|
+
* or from the LLM planner) can name what it needs in natural terms — "backend",
|
|
6
|
+
* "ui", "testing", "devops" — and this maps them onto the kernel's canonical
|
|
7
|
+
* capability vocabulary so the router/worker-matcher understand them. Unknown
|
|
8
|
+
* terms fall back to a fuzzy canonical match, then to `reasoning`.
|
|
9
|
+
*/
|
|
10
|
+
import type { Capability } from './types.js';
|
|
11
|
+
/** The kernel's canonical capability vocabulary (mirrors the Capability union). */
|
|
12
|
+
export declare const CANONICAL_CAPABILITIES: Capability[];
|
|
13
|
+
/**
|
|
14
|
+
* Normalize natural capability names to canonical capabilities. Accepts canonical
|
|
15
|
+
* names as-is, maps known synonyms, fuzzy-matches by substring, and guarantees a
|
|
16
|
+
* non-empty result (falls back to `['reasoning']`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeCapabilities(input?: string[]): Capability[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** The kernel's canonical capability vocabulary (mirrors the Capability union). */
|
|
2
|
+
export const CANONICAL_CAPABILITIES = [
|
|
3
|
+
'coding', 'browser', 'vision', 'terminal', 'planning', 'reasoning', 'filesystem',
|
|
4
|
+
'search', 'crawl', 'ocr', 'review', 'summarize', 'citations', 'translation',
|
|
5
|
+
'documentation', 'architecture', 'verification', 'frontend', 'design',
|
|
6
|
+
'accessibility', 'dataviz', 'animation',
|
|
7
|
+
];
|
|
8
|
+
/** Natural-language / domain terms → canonical capabilities. */
|
|
9
|
+
const SYNONYMS = {
|
|
10
|
+
// engineering domains
|
|
11
|
+
backend: ['coding', 'terminal', 'filesystem', 'reasoning'],
|
|
12
|
+
'back-end': ['coding', 'terminal', 'filesystem', 'reasoning'],
|
|
13
|
+
server: ['coding', 'terminal', 'filesystem'],
|
|
14
|
+
api: ['coding', 'reasoning'],
|
|
15
|
+
ui: ['frontend', 'design'],
|
|
16
|
+
ux: ['frontend', 'design', 'accessibility'],
|
|
17
|
+
'front-end': ['frontend', 'design'],
|
|
18
|
+
web: ['frontend', 'coding'],
|
|
19
|
+
fullstack: ['coding', 'frontend', 'filesystem'],
|
|
20
|
+
'full-stack': ['coding', 'frontend', 'filesystem'],
|
|
21
|
+
mobile: ['frontend', 'coding'],
|
|
22
|
+
// testing / quality
|
|
23
|
+
test: ['verification', 'review'],
|
|
24
|
+
tests: ['verification', 'review'],
|
|
25
|
+
testing: ['verification', 'review'],
|
|
26
|
+
qa: ['verification', 'review'],
|
|
27
|
+
security: ['review', 'reasoning'],
|
|
28
|
+
audit: ['review', 'reasoning'],
|
|
29
|
+
// planning / design
|
|
30
|
+
architect: ['architecture', 'planning', 'reasoning'],
|
|
31
|
+
plan: ['planning', 'reasoning'],
|
|
32
|
+
spec: ['planning', 'documentation'],
|
|
33
|
+
// data / research
|
|
34
|
+
research: ['search', 'crawl', 'summarize'],
|
|
35
|
+
data: ['dataviz', 'reasoning'],
|
|
36
|
+
db: ['coding', 'filesystem'],
|
|
37
|
+
database: ['coding', 'filesystem'],
|
|
38
|
+
ml: ['reasoning', 'coding'],
|
|
39
|
+
ai: ['reasoning', 'coding'],
|
|
40
|
+
// ops / misc
|
|
41
|
+
refactor: ['coding', 'review'],
|
|
42
|
+
debug: ['coding', 'reasoning', 'terminal'],
|
|
43
|
+
deploy: ['terminal', 'filesystem'],
|
|
44
|
+
devops: ['terminal', 'filesystem', 'coding'],
|
|
45
|
+
infra: ['terminal', 'filesystem'],
|
|
46
|
+
docs: ['documentation', 'summarize'],
|
|
47
|
+
translate: ['translation'],
|
|
48
|
+
a11y: ['accessibility'],
|
|
49
|
+
chart: ['dataviz'],
|
|
50
|
+
charts: ['dataviz'],
|
|
51
|
+
animate: ['animation'],
|
|
52
|
+
scrape: ['crawl', 'search'],
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Normalize natural capability names to canonical capabilities. Accepts canonical
|
|
56
|
+
* names as-is, maps known synonyms, fuzzy-matches by substring, and guarantees a
|
|
57
|
+
* non-empty result (falls back to `['reasoning']`).
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeCapabilities(input) {
|
|
60
|
+
const out = new Set();
|
|
61
|
+
const canon = new Set(CANONICAL_CAPABILITIES);
|
|
62
|
+
for (const raw of input ?? []) {
|
|
63
|
+
const key = String(raw).trim().toLowerCase();
|
|
64
|
+
if (!key)
|
|
65
|
+
continue;
|
|
66
|
+
if (canon.has(key)) {
|
|
67
|
+
out.add(key);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const mapped = SYNONYMS[key];
|
|
71
|
+
if (mapped) {
|
|
72
|
+
for (const c of mapped)
|
|
73
|
+
out.add(c);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// fuzzy: the term contains (or is contained by) a canonical token
|
|
77
|
+
const hit = CANONICAL_CAPABILITIES.find(c => key.includes(c) || c.includes(key));
|
|
78
|
+
if (hit)
|
|
79
|
+
out.add(hit);
|
|
80
|
+
}
|
|
81
|
+
if (out.size === 0)
|
|
82
|
+
out.add('reasoning');
|
|
83
|
+
return [...out];
|
|
84
|
+
}
|
|
@@ -28,6 +28,13 @@ export class CheckpointEngine {
|
|
|
28
28
|
// cap per-mission checkpoint files to prevent disk exhaustion
|
|
29
29
|
this.pruneMission(mission.id);
|
|
30
30
|
const id = nanoid(12);
|
|
31
|
+
// Persist the mission's recent event log + working memory so a resumed or
|
|
32
|
+
// crash-recovered long-running mission keeps its activity trail and context.
|
|
33
|
+
const logs = this.eventBus
|
|
34
|
+
.getHistory()
|
|
35
|
+
.filter(e => e.missionId === mission.id)
|
|
36
|
+
.slice(-200);
|
|
37
|
+
const memory = mission.metadata ? structuredClone(mission.metadata) : undefined;
|
|
31
38
|
const checkpoint = {
|
|
32
39
|
id,
|
|
33
40
|
missionId: mission.id,
|
|
@@ -35,6 +42,8 @@ export class CheckpointEngine {
|
|
|
35
42
|
workerStates,
|
|
36
43
|
timestamp: Date.now(),
|
|
37
44
|
reason,
|
|
45
|
+
logs,
|
|
46
|
+
memory,
|
|
38
47
|
};
|
|
39
48
|
const file = join(this.dir, `${mission.id}_${id}.json`);
|
|
40
49
|
writeFileSync(file, JSON.stringify(checkpoint, null, 2), 'utf-8');
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Kernel } from './kernel.js';
|
|
14
14
|
import type { CommandResult } from './types.js';
|
|
15
|
+
import type { Federation } from './federation/federation.js';
|
|
15
16
|
export interface CommandContext {
|
|
16
17
|
kernel: Kernel;
|
|
18
|
+
/** The mesh node, when the kernel is running under `serve`/`mcp`. */
|
|
19
|
+
federation?: Federation;
|
|
17
20
|
args: string[];
|
|
18
21
|
raw: string;
|
|
19
22
|
}
|