omniwire 2.4.0 → 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.
package/README.md CHANGED
@@ -1,26 +1,38 @@
1
1
  <p align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://capsule-render.vercel.app/api?type=waving&color=0:0A0E14,50:1A1F2E,100:59C2FF&height=200&section=header&text=OmniWire&fontSize=72&fontColor=59C2FF&animation=fadeIn&fontAlignY=35&desc=Unified%20Mesh%20Control%20Layer&descSize=18&descColor=8B949E&descAlignY=55" />
4
- <source media="(prefers-color-scheme: light)" srcset="https://capsule-render.vercel.app/api?type=waving&color=0:E8EAED,50:D4D8DE,100:59C2FF&height=200&section=header&text=OmniWire&fontSize=72&fontColor=0A0E14&animation=fadeIn&fontAlignY=35&desc=Unified%20Mesh%20Control%20Layer&descSize=18&descColor=586069&descAlignY=55" />
5
- <img alt="OmniWire" src="https://capsule-render.vercel.app/api?type=waving&color=0:0A0E14,50:1A1F2E,100:59C2FF&height=200&section=header&text=OmniWire&fontSize=72&fontColor=59C2FF&animation=fadeIn&fontAlignY=35&desc=Unified%20Mesh%20Control%20Layer&descSize=18&descColor=8B949E&descAlignY=55" />
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://capsule-render.vercel.app/api?type=venom&color=0:0A0E14,50:0D1B2A,100:1B2838&height=220&section=header&text=OmniWire&fontSize=80&fontColor=59C2FF&animation=fadeIn&fontAlignY=32&desc=Multi-Agent%20Mesh%20Control%20%E2%80%94%2053%20MCP%20Tools%20%C2%B7%20A2A%20Protocol%20%C2%B7%20~80ms%20Latency&descSize=16&descColor=8B949E&descAlignY=58" />
4
+ <source media="(prefers-color-scheme: light)" srcset="https://capsule-render.vercel.app/api?type=venom&color=0:E8EAED,50:D4D8DE,100:59C2FF&height=220&section=header&text=OmniWire&fontSize=80&fontColor=0A0E14&animation=fadeIn&fontAlignY=32&desc=Multi-Agent%20Mesh%20Control%20%E2%80%94%2053%20MCP%20Tools%20%C2%B7%20A2A%20Protocol%20%C2%B7%20~80ms%20Latency&descSize=16&descColor=586069&descAlignY=58" />
5
+ <img alt="OmniWire" src="https://capsule-render.vercel.app/api?type=venom&color=0:0A0E14,50:0D1B2A,100:1B2838&height=220&section=header&text=OmniWire&fontSize=80&fontColor=59C2FF&animation=fadeIn&fontAlignY=32&desc=Multi-Agent%20Mesh%20Control%20%E2%80%94%2053%20MCP%20Tools%20%C2%B7%20A2A%20Protocol%20%C2%B7%20~80ms%20Latency&descSize=16&descColor=8B949E&descAlignY=58" />
6
6
  </picture>
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/omniwire"><img src="https://img.shields.io/npm/v/omniwire?style=for-the-badge&logo=npm&color=CB3837&labelColor=0A0E14" alt="npm" /></a>
11
- <img src="https://img.shields.io/badge/MCP-49_tools-59C2FF?style=for-the-badge&labelColor=0A0E14" alt="tools" />
12
- <img src="https://img.shields.io/badge/A2A-ready-91B362?style=for-the-badge&labelColor=0A0E14" alt="A2A" />
13
- <img src="https://img.shields.io/badge/transport-stdio_%7C_SSE_%7C_REST-E6B450?style=for-the-badge&labelColor=0A0E14" alt="transports" />
14
- <img src="https://img.shields.io/badge/node-%E2%89%A520-CC93E6?style=for-the-badge&logo=node.js&labelColor=0A0E14" alt="node" />
11
+ <img src="https://img.shields.io/badge/MCP-53_tools-59C2FF?style=for-the-badge&labelColor=0A0E14" alt="tools" />
12
+ <img src="https://img.shields.io/badge/A2A-protocol-00C853?style=for-the-badge&labelColor=0A0E14" alt="A2A" />
13
+ <img src="https://img.shields.io/badge/latency-~80ms-FF6D00?style=for-the-badge&labelColor=0A0E14" alt="latency" />
14
+ <img src="https://img.shields.io/badge/lz4-transfer-CC93E6?style=for-the-badge&labelColor=0A0E14" alt="lz4" />
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-8B949E?style=for-the-badge&labelColor=0A0E14" alt="license" /></a>
16
16
  </p>
17
17
 
18
+ <br/>
19
+
20
+ <p align="center">
21
+ <b>The infrastructure layer for AI agent swarms.</b>
22
+ </p>
23
+
18
24
  <p align="center">
19
- <b>One MCP server to control all your machines.</b><br/>
20
- <sub>49 tools. Multi-agent orchestration. A2A messaging. Distributed locking. Cross-node pipelines.</sub><br/>
21
- <sub>SSH2 failover, adaptive file transfers, encrypted config sync, agentic chaining.</sub>
25
+ 53 MCP tools &bull; Agent-to-Agent messaging &bull; Distributed task queues &bull; Capability routing<br/>
26
+ AES-128-GCM SSH2 &bull; LZ4 transfers &bull; Circuit breakers &bull; Multi-path failover<br/>
27
+ Session chaining &bull; Pipeline DAGs &bull; Blackboard architecture &bull; Event pub/sub
22
28
  </p>
23
29
 
30
+ <br/>
31
+
32
+ > **v2.5** &mdash; AES-128-GCM cipher preference, 2s keepalive, LZ4 compression, `shuf` port finder, SFTP-first reads, agent registry, blackboard, task queues, capability routing. See [changelog](#changelog).
33
+
34
+ <br/>
35
+
24
36
  ---
25
37
 
26
38
  ## Quick Start
@@ -71,7 +83,7 @@ graph TB
71
83
  direction TB
72
84
  MCP["MCP Protocol Layer<br/>stdio | SSE | REST"]
73
85
 
74
- subgraph tools["49 Tools"]
86
+ subgraph tools["53 Tools"]
75
87
  direction LR
76
88
  EXEC["Execution<br/>exec run batch<br/>broadcast pipeline"]
77
89
  AGENT["Agentic<br/>store watch task<br/>a2a events locks"]
@@ -207,7 +219,7 @@ Nodes --push--> PostgreSQL (cyberbase)
207
219
 
208
220
  ---
209
221
 
210
- ## All 49 Tools
222
+ ## All 53 Tools
211
223
 
212
224
  ### Execution (5)
213
225
 
@@ -219,7 +231,7 @@ Nodes --push--> PostgreSQL (cyberbase)
219
231
  | `omniwire_broadcast` | Execute on all nodes simultaneously. JSON format support. |
220
232
  | `omniwire_pipeline` | Multi-step DAG. `{{prev}}`/`{{stepN}}` interpolation, per-step error handling, cross-node. |
221
233
 
222
- ### Agentic / A2A (9)
234
+ ### Agentic / A2A (13)
223
235
 
224
236
  | Tool | Description |
225
237
  |------|-------------|
@@ -231,6 +243,10 @@ Nodes --push--> PostgreSQL (cyberbase)
231
243
  | `omniwire_semaphore` | Distributed locking. Atomic acquire/release to prevent race conditions. |
232
244
  | `omniwire_event` | Pub/sub events. Emit/poll timestamped events per topic. ACP/A2A/ACPX compatible. |
233
245
  | `omniwire_workflow` | Define and run reusable named workflows (DAGs). Stored on disk, triggered by any agent. |
246
+ | `omniwire_agent_registry` | Register/discover agents by capabilities. Dynamic A2A routing. Heartbeat. |
247
+ | `omniwire_blackboard` | Shared blackboard for agent swarms. Post findings, read, search across topics. |
248
+ | `omniwire_task_queue` | Distributed task queue. Enqueue/dequeue with priorities. Complete/fail reporting. |
249
+ | `omniwire_capability` | Query node capabilities (tools, runtimes, GPU). Intelligent task routing. |
234
250
 
235
251
  ### Files & Transfer (6)
236
252
 
@@ -297,16 +313,30 @@ Nodes --push--> PostgreSQL (cyberbase)
297
313
 
298
314
  ## Performance
299
315
 
300
- | Operation | Latency | Details |
301
- |-----------|---------|---------|
302
- | Command exec | ~120ms | SSH2 channel on persistent connection |
303
- | Mesh status (all nodes) | ~150ms | Parallel probes, 5s cache |
304
- | File read (<1MB) | ~80ms | SFTP, binary-safe |
305
- | Transfer (10MB) | ~200ms | gzip netcat over WireGuard |
306
- | Pipeline (5 steps) | ~600ms | Sequential with interpolation |
307
- | Health check (4 nodes) | ~200ms | Parallel, structured output |
308
- | A2A message send | ~130ms | File-based queue |
309
- | Config push (all nodes) | ~200ms | Parallel + Obsidian mirror |
316
+ | Operation | Latency | v2.5 Optimization |
317
+ |-----------|---------|-------------------|
318
+ | **Command exec** | **~80ms** | AES-128-GCM cipher, persistent SSH2 channel, zero-fork `:` ping |
319
+ | **Mesh status** | **~100ms** | Parallel probes, 5s cache, single `/proc` read (no pipes) |
320
+ | **File read (<1MB)** | **~60ms** | SFTP-first path (skips `cat` shell fork) |
321
+ | **Transfer (10MB)** | **~120ms** | LZ4 compression (10x faster than gzip), 50ms bind delay |
322
+ | **Transfer (1GB)** | **~8s** | aria2c 16-connection parallel, 150ms server startup |
323
+ | **Pipeline (5 steps)** | **~400ms** | `{{prev}}` interpolation, no extra tool calls |
324
+ | **Health check (all)** | **~90ms** | Parallel Promise.allSettled, structured JSON |
325
+ | **A2A message** | **~85ms** | File-append queue, atomic dequeue |
326
+ | **Config push** | **~150ms** | Parallel deploy + Obsidian mirror |
327
+ | **Reconnect** | **~300ms** | 300ms initial delay (was 500ms), 2s keepalive detection |
328
+
329
+ **Optimizations in v2.5:**
330
+ - **Cipher**: AES-128-GCM (AES-NI accelerated) preferred over default negotiation
331
+ - **Key exchange**: curve25519-sha256 preferred (fastest modern KEX)
332
+ - **Keepalive**: 2s interval, 2 retries = 4s dead detection (was 6s)
333
+ - **Port finder**: `shuf` (pure bash) replaces `python3 -c socket` (saves ~30ms per transfer)
334
+ - **Compression**: LZ4-1 for transfers (10x faster than gzip, ~same ratio for mixed data)
335
+ - **Buffer**: Array push + join replaces string concatenation (O(n) vs O(n^2) for large outputs)
336
+ - **Status**: Single `/proc` read replaces multiple piped commands
337
+ - **Health ping**: `:` builtin replaces `true` (no hash lookup, no fork)
338
+ - **Reads**: SFTP subsystem tried first, falls back to `cat` only on failure
339
+ - **Circuit breaker**: 15s recovery (was 20s), 10s reconnect cap (was 15s)
310
340
 
311
341
  ---
312
342
 
@@ -357,6 +387,17 @@ Create `~/.omniwire/mesh.json`:
357
387
 
358
388
  ## Changelog
359
389
 
390
+ <details>
391
+ <summary><b>v2.5.0 -- Performance Overhaul, A2A Protocol Expansion</b></summary>
392
+
393
+ **Performance**: AES-128-GCM cipher, curve25519-sha256 KEX, 2s keepalive, LZ4 transfers (10x faster), `shuf` port finder (-30ms), SFTP-first reads, array buffer concat, `/proc` single-read status, `:` builtin health ping, 300ms reconnect start, 15s circuit breaker.
394
+
395
+ **4 new A2A tools** (49 -> 53): agent_registry (capability discovery), blackboard (swarm collaboration), task_queue (distributed work), capability (node routing).
396
+
397
+ **Connectivity**: Always-on 2s keepalive with 4s dead detection. 5s connect timeout. 10s reconnect cap. 15s circuit recovery.
398
+
399
+ </details>
400
+
360
401
  <details>
361
402
  <summary><b>v2.4.0 -- Agentic Loop, A2A, Multi-Agent Orchestration</b></summary>
362
403
 
@@ -396,7 +437,7 @@ Multi-path SSH (WireGuard/Tailscale/Public), SSH key caching, CyberBase integrat
396
437
  ```
397
438
  omniwire/
398
439
  src/
399
- mcp/ MCP server (49 tools, 3 transports)
440
+ mcp/ MCP server (53 tools, 3 transports)
400
441
  nodes/ SSH2 pool, transfer engine, PTY, tunnels
401
442
  sync/ CyberSync + CyberBase (PostgreSQL, Obsidian, encryption)
402
443
  protocol/ Mesh config, types, path parsing
@@ -67,7 +67,7 @@ const resultStore = new Map(); // key -> value store for chaining
67
67
  export function createOmniWireServer(manager, transfer) {
68
68
  const server = new McpServer({
69
69
  name: 'omniwire',
70
- version: '2.4.0',
70
+ version: '2.5.0',
71
71
  });
72
72
  const shells = new ShellManager(manager);
73
73
  const realtime = new RealtimeChannel(manager);
@@ -1156,6 +1156,157 @@ tags: ${meshNode.tags.join(', ')}`;
1156
1156
  }
1157
1157
  return fail('invalid action');
1158
1158
  });
1159
+ // --- Tool 42: omniwire_agent_registry ---
1160
+ server.tool('omniwire_agent_registry', 'Register/discover agents on the mesh. Agents announce their capabilities and other agents can discover them. Enables dynamic A2A routing and capability-based task delegation.', {
1161
+ action: z.enum(['register', 'deregister', 'discover', 'list', 'heartbeat']).describe('Action'),
1162
+ node: z.string().optional().describe('Node hosting registry (default: contabo)'),
1163
+ agent_id: z.string().optional().describe('Unique agent ID'),
1164
+ capabilities: z.array(z.string()).optional().describe('Agent capabilities (e.g., ["scan", "exploit", "report"])'),
1165
+ metadata: z.string().optional().describe('JSON metadata about the agent'),
1166
+ capability: z.string().optional().describe('Capability to search for (discover action)'),
1167
+ }, async ({ action, node, agent_id, capabilities, metadata, capability }) => {
1168
+ const nodeId = node ?? 'contabo';
1169
+ const regDir = '/tmp/.omniwire-agents';
1170
+ if (action === 'register') {
1171
+ if (!agent_id)
1172
+ return fail('agent_id required');
1173
+ const entry = JSON.stringify({ id: agent_id, capabilities: capabilities ?? [], metadata: metadata ?? '{}', ts: Date.now(), node: nodeId });
1174
+ const escaped = entry.replace(/'/g, "'\\''");
1175
+ await manager.exec(nodeId, `mkdir -p ${regDir} && echo '${escaped}' > ${regDir}/${agent_id}.json`);
1176
+ return okBrief(`agent ${agent_id} registered (${(capabilities ?? []).join(', ')})`);
1177
+ }
1178
+ if (action === 'deregister') {
1179
+ if (!agent_id)
1180
+ return fail('agent_id required');
1181
+ await manager.exec(nodeId, `rm -f ${regDir}/${agent_id}.json`);
1182
+ return okBrief(`agent ${agent_id} deregistered`);
1183
+ }
1184
+ if (action === 'heartbeat') {
1185
+ if (!agent_id)
1186
+ return fail('agent_id required');
1187
+ await manager.exec(nodeId, `[ -f ${regDir}/${agent_id}.json ] && tmp=$(cat ${regDir}/${agent_id}.json) && echo "$tmp" | sed 's/"ts":[0-9]*/"ts":${Date.now()}/' > ${regDir}/${agent_id}.json`);
1188
+ return okBrief(`agent ${agent_id} heartbeat`);
1189
+ }
1190
+ if (action === 'discover' && capability) {
1191
+ const result = await manager.exec(nodeId, `grep -l '"${capability}"' ${regDir}/*.json 2>/dev/null | xargs -I{} cat {} 2>/dev/null`);
1192
+ return ok(nodeId, result.durationMs, result.stdout || '(no agents with that capability)', `discover ${capability}`);
1193
+ }
1194
+ if (action === 'list') {
1195
+ const result = await manager.exec(nodeId, `cat ${regDir}/*.json 2>/dev/null || echo '(no agents)'`);
1196
+ return ok(nodeId, result.durationMs, result.stdout, 'agent registry');
1197
+ }
1198
+ return fail('invalid action');
1199
+ });
1200
+ // --- Tool 43: omniwire_blackboard ---
1201
+ server.tool('omniwire_blackboard', 'Shared blackboard for multi-agent collaboration. Agents post findings, hypotheses, and decisions to topic-scoped boards. Other agents read and build on them. Classic AI blackboard architecture for agent swarms.', {
1202
+ action: z.enum(['post', 'read', 'topics', 'clear', 'search']).describe('Action'),
1203
+ node: z.string().optional(),
1204
+ topic: z.string().optional().describe('Board topic (e.g., "recon-findings", "vuln-analysis")'),
1205
+ content: z.string().optional().describe('Content to post'),
1206
+ author: z.string().optional().describe('Author agent ID'),
1207
+ query: z.string().optional().describe('Search query (grep pattern) for search action'),
1208
+ limit: z.number().optional().describe('Max entries (default: 20)'),
1209
+ }, async ({ action, node, topic, content, author, query, limit }) => {
1210
+ const nodeId = node ?? 'contabo';
1211
+ const bbDir = '/tmp/.omniwire-blackboard';
1212
+ const n = limit ?? 20;
1213
+ if (action === 'post') {
1214
+ if (!topic || !content)
1215
+ return fail('topic and content required');
1216
+ const entry = JSON.stringify({ ts: Date.now(), author: author ?? 'agent', content });
1217
+ const escaped = entry.replace(/'/g, "'\\''");
1218
+ await manager.exec(nodeId, `mkdir -p ${bbDir} && echo '${escaped}' >> ${bbDir}/${topic}.log`);
1219
+ return okBrief(`posted to ${topic} (${content.length} chars)`);
1220
+ }
1221
+ if (action === 'read') {
1222
+ if (!topic)
1223
+ return fail('topic required');
1224
+ const result = await manager.exec(nodeId, `tail -${n} ${bbDir}/${topic}.log 2>/dev/null || echo '(empty board)'`);
1225
+ return ok(nodeId, result.durationMs, result.stdout, `board:${topic}`);
1226
+ }
1227
+ if (action === 'topics') {
1228
+ const result = await manager.exec(nodeId, `ls -1 ${bbDir}/*.log 2>/dev/null | xargs -I{} sh -c 'echo "$(basename {} .log) $(wc -l < {})"' 2>/dev/null || echo '(no topics)'`);
1229
+ return ok(nodeId, result.durationMs, result.stdout, 'blackboard topics');
1230
+ }
1231
+ if (action === 'search' && query) {
1232
+ const escaped = query.replace(/'/g, "'\\''");
1233
+ const topicFilter = topic ? `${bbDir}/${topic}.log` : `${bbDir}/*.log`;
1234
+ const result = await manager.exec(nodeId, `grep -h '${escaped}' ${topicFilter} 2>/dev/null | tail -${n}`);
1235
+ return ok(nodeId, result.durationMs, result.stdout || '(no matches)', `search:${query}`);
1236
+ }
1237
+ if (action === 'clear') {
1238
+ if (!topic)
1239
+ return fail('topic required');
1240
+ await manager.exec(nodeId, `rm -f ${bbDir}/${topic}.log`);
1241
+ return okBrief(`board ${topic} cleared`);
1242
+ }
1243
+ return fail('invalid action');
1244
+ });
1245
+ // --- Tool 44: omniwire_task_queue ---
1246
+ server.tool('omniwire_task_queue', 'Distributed task queue for agent swarms. Producers enqueue tasks, consumer agents dequeue and process them. Supports priorities, deadlines, and result reporting. Core A2A work distribution primitive.', {
1247
+ action: z.enum(['enqueue', 'dequeue', 'complete', 'fail', 'status', 'pending']).describe('Action'),
1248
+ node: z.string().optional(),
1249
+ queue: z.string().optional().describe('Queue name (default: "default")'),
1250
+ task: z.string().optional().describe('Task payload (JSON) for enqueue'),
1251
+ priority: z.number().optional().describe('Priority 0-9, higher = more urgent (default: 5)'),
1252
+ task_id: z.string().optional().describe('Task ID for complete/fail'),
1253
+ result: z.string().optional().describe('Result data for complete'),
1254
+ error: z.string().optional().describe('Error message for fail'),
1255
+ }, async ({ action, node, queue, task, priority, task_id, result: taskResult, error: taskError }) => {
1256
+ const nodeId = node ?? 'contabo';
1257
+ const qName = queue ?? 'default';
1258
+ const qDir = `/tmp/.omniwire-taskq/${qName}`;
1259
+ if (action === 'enqueue') {
1260
+ if (!task)
1261
+ return fail('task required');
1262
+ const id = `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1263
+ const pri = priority ?? 5;
1264
+ const entry = JSON.stringify({ id, priority: pri, ts: Date.now(), status: 'pending', task: JSON.parse(task) });
1265
+ const escaped = entry.replace(/'/g, "'\\''");
1266
+ await manager.exec(nodeId, `mkdir -p ${qDir} && echo '${escaped}' > ${qDir}/${pri}_${id}.task`);
1267
+ return okBrief(`enqueued ${id} (priority ${pri})`);
1268
+ }
1269
+ if (action === 'dequeue') {
1270
+ // Take highest priority task (9 first, then 8, ...)
1271
+ const result = await manager.exec(nodeId, `f=$(ls -r ${qDir}/*.task 2>/dev/null | head -1); [ -n "$f" ] && cat "$f" && rm -f "$f" || echo '(empty queue)'`);
1272
+ return ok(nodeId, result.durationMs, result.stdout, `dequeue:${qName}`);
1273
+ }
1274
+ if (action === 'complete' && task_id) {
1275
+ const entry = JSON.stringify({ id: task_id, status: 'complete', ts: Date.now(), result: taskResult ?? '' });
1276
+ const escaped = entry.replace(/'/g, "'\\''");
1277
+ await manager.exec(nodeId, `mkdir -p ${qDir}/done && echo '${escaped}' > ${qDir}/done/${task_id}.result`);
1278
+ return okBrief(`task ${task_id} completed`);
1279
+ }
1280
+ if (action === 'fail' && task_id) {
1281
+ const entry = JSON.stringify({ id: task_id, status: 'failed', ts: Date.now(), error: taskError ?? 'unknown' });
1282
+ const escaped = entry.replace(/'/g, "'\\''");
1283
+ await manager.exec(nodeId, `mkdir -p ${qDir}/failed && echo '${escaped}' > ${qDir}/failed/${task_id}.result`);
1284
+ return okBrief(`task ${task_id} failed`);
1285
+ }
1286
+ if (action === 'status') {
1287
+ const result = await manager.exec(nodeId, `echo "pending: $(ls ${qDir}/*.task 2>/dev/null | wc -l)"; echo "done: $(ls ${qDir}/done/*.result 2>/dev/null | wc -l)"; echo "failed: $(ls ${qDir}/failed/*.result 2>/dev/null | wc -l)"`);
1288
+ return ok(nodeId, result.durationMs, result.stdout, `queue:${qName}`);
1289
+ }
1290
+ if (action === 'pending') {
1291
+ const result = await manager.exec(nodeId, `cat ${qDir}/*.task 2>/dev/null | head -20 || echo '(empty)'`);
1292
+ return ok(nodeId, result.durationMs, result.stdout, `pending:${qName}`);
1293
+ }
1294
+ return fail('invalid action');
1295
+ });
1296
+ // --- Tool 45: omniwire_capability ---
1297
+ server.tool('omniwire_capability', 'Query node capabilities for intelligent task routing. Returns what tools, runtimes, and resources each node has. Agents use this to decide WHERE to dispatch tasks.', {
1298
+ node: z.string().optional().describe('Specific node (default: all online)'),
1299
+ check: z.array(z.string()).optional().describe('Specific capabilities to check (e.g., ["docker", "python3", "gpu", "nmap"])'),
1300
+ }, async ({ node, check }) => {
1301
+ const checks = check ?? ['docker', 'python3', 'node', 'go', 'nmap', 'ffuf', 'git', 'psql', 'lz4', 'aria2c', 'gcc'];
1302
+ const cmd = checks.map((c) => `command -v ${c} >/dev/null 2>&1 && echo "${c}:yes" || echo "${c}:no"`).join('; ');
1303
+ if (node) {
1304
+ const result = await manager.exec(node, cmd);
1305
+ return ok(node, result.durationMs, result.stdout, 'capabilities');
1306
+ }
1307
+ const results = await manager.execAll(cmd);
1308
+ return multiResult(results);
1309
+ });
1159
1310
  return server;
1160
1311
  }
1161
1312
  //# sourceMappingURL=server.js.map