omniwire 2.7.0 → 3.0.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.
@@ -17,7 +17,25 @@ function t(ms) {
17
17
  return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
18
18
  }
19
19
  function trim(s) {
20
- return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + '\n...(truncated)' : s;
20
+ if (s.length <= MAX_OUTPUT)
21
+ return s;
22
+ // Detect JSON — show item count on truncation
23
+ if (s.trimStart().startsWith('[') || s.trimStart().startsWith('{')) {
24
+ try {
25
+ const parsed = JSON.parse(s);
26
+ const count = Array.isArray(parsed) ? parsed.length : Object.keys(parsed).length;
27
+ return s.slice(0, MAX_OUTPUT - 50) + `\n...(truncated, ${count} items total)`;
28
+ }
29
+ catch { }
30
+ }
31
+ // Detect table-like output — show header + tail
32
+ const lines = s.split('\n');
33
+ if (lines.length > 30) {
34
+ const header = lines.slice(0, 2).join('\n');
35
+ const lastLines = lines.slice(-5).join('\n');
36
+ return header + '\n...\n' + lastLines + `\n(${lines.length} lines total)`;
37
+ }
38
+ return s.slice(0, MAX_OUTPUT) + '\n...(truncated)';
21
39
  }
22
40
  function ok(node, ms, body, label) {
23
41
  const hdr = label ? `${node} > ${label}` : node;
@@ -116,8 +134,29 @@ function buildVpnWrappedCmd(vpnSpec, innerCmd) {
116
134
  // Fallback: just run the command (no VPN wrapping)
117
135
  return innerCmd;
118
136
  }
137
+ // -- Command safety -- block patterns that could cause irreversible system damage
138
+ const BLOCKED_PATTERNS = [
139
+ /rm\s+-rf\s+\/(?!\w)/, // rm -rf / (but allow rm -rf /tmp/something)
140
+ /dd\s+if=\/dev\/zero.*of=\/dev\/sd/, // dd zeroing disk
141
+ /mkfs\./, // format filesystem
142
+ /:(){ :\|:& };:/, // fork bomb
143
+ />\s*\/dev\/sd/, // redirect to disk device
144
+ /chmod\s+-R\s+777\s+\//, // chmod 777 /
145
+ ];
146
+ function checkCommandSafety(cmd) {
147
+ for (const pattern of BLOCKED_PATTERNS) {
148
+ if (pattern.test(cmd))
149
+ return `Blocked dangerous pattern: ${pattern.source}`;
150
+ }
151
+ return null;
152
+ }
119
153
  // -- Agentic state -- shared across tool calls in the same MCP session --------
154
+ // Session-scoped: all keys are cleared on process restart (not persisted to disk).
120
155
  const resultStore = new Map(); // key -> value store for chaining
156
+ const auditLog = [];
157
+ // -- Alias store -- in-memory command shortcuts ------------------------------
158
+ const aliasStore = new Map(); // name -> command template
159
+ const traceStore = new Map();
121
160
  const bgTasks = new Map();
122
161
  function bgId() { return `bg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`; }
123
162
  function dispatchBg(node, label, fn) {
@@ -127,6 +166,114 @@ function dispatchBg(node, label, fn) {
127
166
  bgTasks.set(id, task);
128
167
  return okBrief(`BACKGROUND ${id} dispatched on ${node}: ${label}`);
129
168
  }
169
+ // -- CyberBase persistence layer -- fire-and-forget writes to PostgreSQL ------
170
+ // All tool data auto-persists to CyberBase without blocking responses.
171
+ // Uses the SSH connection to contabo (where PostgreSQL runs).
172
+ // Category-based key-value store in sync_items table.
173
+ let cbManager = null;
174
+ const CB_QUEUE = [];
175
+ let cbDraining = false;
176
+ function cbInit(mgr) { cbManager = mgr; }
177
+ /** Fire-and-forget write to CyberBase knowledge table. Never blocks, never throws. */
178
+ function cb(category, key, value) {
179
+ if (!cbManager)
180
+ return;
181
+ const valEsc = value.replace(/'/g, "''").slice(0, 50000);
182
+ const keyEsc = `${category}:${key}`.replace(/'/g, "''");
183
+ // Upsert: delete old + insert new (no unique constraint on source_tool+key)
184
+ const sql = `DELETE FROM knowledge WHERE source_tool='omniwire' AND key='${keyEsc}'; INSERT INTO knowledge (source_tool, key, value, updated_at) VALUES ('omniwire', '${keyEsc}', jsonb_build_object('data', '${valEsc}'), NOW());`;
185
+ CB_QUEUE.push(sql);
186
+ if (!cbDraining)
187
+ drainCb();
188
+ }
189
+ /** Batch-drain CyberBase write queue (max 10 per flush) */
190
+ async function drainCb() {
191
+ if (!cbManager || CB_QUEUE.length === 0) {
192
+ cbDraining = false;
193
+ return;
194
+ }
195
+ cbDraining = true;
196
+ const batch = CB_QUEUE.splice(0, 10);
197
+ const combined = batch.join(' ');
198
+ try {
199
+ await cbManager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -c "${combined}" 2>/dev/null`);
200
+ }
201
+ catch { /* never fail */ }
202
+ if (CB_QUEUE.length > 0)
203
+ setTimeout(drainCb, 100);
204
+ else
205
+ cbDraining = false;
206
+ }
207
+ /** Read from CyberBase knowledge table. Returns value or null. */
208
+ async function cbGet(category, key) {
209
+ if (!cbManager)
210
+ return null;
211
+ const fullKey = `${category}:${key}`.replace(/'/g, "''");
212
+ try {
213
+ const r = await cbManager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -t -c "SELECT value->>'data' FROM knowledge WHERE source_tool='omniwire' AND key='${fullKey}';" 2>/dev/null`);
214
+ const val = r.stdout.trim();
215
+ return val || null;
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ /** List keys in a CyberBase category (from knowledge table) */
222
+ async function cbList(category) {
223
+ if (!cbManager)
224
+ return [];
225
+ const prefix = `${category}:`.replace(/'/g, "''");
226
+ try {
227
+ const r = await cbManager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -t -c "SELECT replace(key, '${prefix}', '') FROM knowledge WHERE source_tool='omniwire' AND key LIKE '${prefix}%' ORDER BY updated_at DESC LIMIT 100;" 2>/dev/null`);
228
+ return r.stdout.trim().split('\n').map(s => s.trim()).filter(Boolean);
229
+ }
230
+ catch {
231
+ return [];
232
+ }
233
+ }
234
+ /** Full-text search across CyberBase knowledge */
235
+ async function cbSearch(query, sourceFilter) {
236
+ if (!cbManager)
237
+ return '';
238
+ const srcFilter = sourceFilter ? `AND source_tool='${sourceFilter}'` : '';
239
+ const escaped = query.replace(/'/g, "''");
240
+ try {
241
+ const r = await cbManager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -t -c "SELECT source_tool, key, substring(value::text,1,200) FROM knowledge WHERE (value::text ILIKE '%${escaped}%' OR key ILIKE '%${escaped}%') ${srcFilter} ORDER BY updated_at DESC LIMIT 20;" 2>/dev/null`);
242
+ return r.stdout.trim();
243
+ }
244
+ catch {
245
+ return '';
246
+ }
247
+ }
248
+ /** Semantic search via pgvector (if embeddings populated) or full-text fallback */
249
+ async function cbSemanticSearch(query, limit = 10) {
250
+ if (!cbManager)
251
+ return '';
252
+ const escaped = query.replace(/'/g, "''");
253
+ try {
254
+ // Try pgvector cosine similarity first
255
+ const r = await cbManager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -t -c "
256
+ SELECT source_tool, key, substring(value::text,1,300)
257
+ FROM knowledge
258
+ WHERE embedding IS NOT NULL
259
+ ORDER BY embedding <=> (SELECT embedding FROM knowledge WHERE key ILIKE '%${escaped}%' LIMIT 1)
260
+ LIMIT ${limit};
261
+ " 2>/dev/null`);
262
+ if (r.stdout.trim())
263
+ return r.stdout.trim();
264
+ // Fallback: ILIKE full-text
265
+ const r2 = await cbManager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -t -c "
266
+ SELECT source_tool, key, substring(value::text,1,300)
267
+ FROM knowledge
268
+ WHERE value::text ILIKE '%${escaped}%' OR key ILIKE '%${escaped}%'
269
+ ORDER BY updated_at DESC LIMIT ${limit};
270
+ " 2>/dev/null`);
271
+ return r2.stdout.trim();
272
+ }
273
+ catch {
274
+ return '';
275
+ }
276
+ }
130
277
  // -----------------------------------------------------------------------------
131
278
  export function createOmniWireServer(manager, transfer) {
132
279
  const server = new McpServer({
@@ -151,6 +298,7 @@ export function createOmniWireServer(manager, transfer) {
151
298
  return origTool(name, desc, augSchema, wrappedHandler);
152
299
  };
153
300
  // ---------------------------------------------------------------------------
301
+ cbInit(manager); // Initialize CyberBase persistence layer
154
302
  const shells = new ShellManager(manager);
155
303
  const realtime = new RealtimeChannel(manager);
156
304
  const tunnels = new TunnelManager(manager);
@@ -225,13 +373,26 @@ export function createOmniWireServer(manager, transfer) {
225
373
  if (via_vpn) {
226
374
  effectiveCmd = buildVpnWrappedCmd(via_vpn, effectiveCmd);
227
375
  }
376
+ const blocked = checkCommandSafety(effectiveCmd);
377
+ if (blocked)
378
+ return fail(blocked);
228
379
  let result = await manager.exec(nodeId, effectiveCmd);
380
+ auditLog.push({ ts: Date.now(), tool: 'exec', node: nodeId, command: resolvedCmd ?? 'script', code: result.code, durationMs: result.durationMs });
381
+ if (auditLog.length > 1000)
382
+ auditLog.shift();
383
+ // Persist audit to CyberBase every 10 execs
384
+ if (auditLog.length % 10 === 0)
385
+ cb('audit', `batch-${Date.now()}`, JSON.stringify(auditLog.slice(-10)));
229
386
  for (let attempt = 0; attempt < maxRetries && result.code !== 0; attempt++) {
230
387
  await new Promise((r) => setTimeout(r, 1000));
231
388
  result = await manager.exec(nodeId, effectiveCmd);
389
+ auditLog.push({ ts: Date.now(), tool: 'exec', node: nodeId, command: resolvedCmd ?? 'script', code: result.code, durationMs: result.durationMs });
390
+ if (auditLog.length > 1000)
391
+ auditLog.shift();
232
392
  }
233
393
  if (store_as && result.code === 0) {
234
394
  resultStore.set(store_as, result.stdout.trim());
395
+ cb('store', store_as, result.stdout.trim()); // persist to CyberBase
235
396
  }
236
397
  if (assertPattern && result.code === 0) {
237
398
  const regex = new RegExp(assertPattern);
@@ -1360,7 +1521,745 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1360
1521
  }
1361
1522
  return fail('Invalid action or missing params');
1362
1523
  });
1363
- // --- Tool 32: omniwire_clipboard ---
1524
+ // --- Tool 32: omniwire_cookies ---
1525
+ server.tool('omniwire_cookies', 'Cookie management across mesh nodes. Import/export/convert between JSON, Header string, and Netscape cookies.txt formats. Sync cookies between nodes. Extract from Chrome/Firefox.', {
1526
+ action: z.enum(['get', 'set', 'export', 'import', 'convert', 'sync', 'extract', 'clear', 'list', 'cyberbase-get', 'cyberbase-set', '1password-get', '1password-set']).describe('get/set/export/import/convert/sync/extract/clear/list + cyberbase-get/set (PostgreSQL), 1password-get/set (op CLI)'),
1527
+ node: z.string().optional().describe('Node (default: contabo)'),
1528
+ domain: z.string().optional().describe('Domain filter (e.g. "github.com")'),
1529
+ format: z.enum(['json', 'header', 'netscape']).optional().describe('json=[{name,value,domain,...}], header="name=val; name2=val2", netscape=cookies.txt (curl/wget)'),
1530
+ cookies: z.string().optional().describe('Cookie data for set/import/convert. Format auto-detected.'),
1531
+ file: z.string().optional().describe('File path (default: /tmp/.omniwire-cookies/<domain>.txt)'),
1532
+ target_format: z.enum(['json', 'header', 'netscape']).optional().describe('Target format for convert'),
1533
+ dst_nodes: z.array(z.string()).optional().describe('Destination nodes for sync'),
1534
+ }, async ({ action, node, domain, format: fmt, cookies: cookieData, file, target_format, dst_nodes }) => {
1535
+ const nodeId = node ?? 'contabo';
1536
+ const cookieDir = '/tmp/.omniwire-cookies';
1537
+ const cookieFile = file ?? (domain ? `${cookieDir}/${domain.replace(/\./g, '_')}.txt` : `${cookieDir}/all.txt`);
1538
+ const jsonToHeader = `python3 -c "import json,sys;c=json.load(sys.stdin);c=c if isinstance(c,list) else [c];print('; '.join(f\\"{x['name']}={x['value']}\\" for x in c))"`;
1539
+ const jsonToNetscape = `python3 -c "import json,sys;c=json.load(sys.stdin);c=c if isinstance(c,list) else [c];print('# Netscape HTTP Cookie File');[print(f\\"{x.get('domain','.')}\t{'TRUE' if x.get('domain','').startswith('.') else 'FALSE'}\t{x.get('path','/')}\t{'TRUE' if x.get('secure') else 'FALSE'}\t{x.get('expires',0)}\t{x['name']}\t{x['value']}\\") for x in c]"`;
1540
+ const headerToJson = `python3 -c "import json,sys;h=sys.stdin.read().strip();print(json.dumps([{'name':p.split('=',1)[0].strip(),'value':p.split('=',1)[1].strip(),'domain':'','path':'/','secure':False} for p in h.split(';') if '=' in p],indent=2))"`;
1541
+ const netscapeToJson = `python3 -c "import json,sys;cookies=[];[cookies.append({'domain':p[0],'path':p[2],'secure':p[3]=='TRUE','expires':int(p[4]) if p[4].isdigit() else 0,'name':p[5],'value':p[6]}) for line in sys.stdin if not line.startswith('#') and line.strip() for p in [line.strip().split('\t')] if len(p)>=7];print(json.dumps(cookies,indent=2))"`;
1542
+ const detectFmt = (d) => {
1543
+ const t = d.trim();
1544
+ if (t.startsWith('[') || t.startsWith('{'))
1545
+ return 'json';
1546
+ if (t.includes('\t') && (t.includes('TRUE') || t.includes('FALSE')))
1547
+ return 'netscape';
1548
+ return 'header';
1549
+ };
1550
+ if (action === 'list') {
1551
+ const r = await manager.exec(nodeId, `mkdir -p ${cookieDir}; ls -la ${cookieDir}/ 2>/dev/null | tail -n +2 || echo "(empty)"`);
1552
+ return ok(nodeId, r.durationMs, r.stdout, 'cookie files');
1553
+ }
1554
+ if (action === 'get') {
1555
+ const r = await manager.exec(nodeId, `cat "${cookieFile}" 2>/dev/null || echo "no cookies at ${cookieFile}"`);
1556
+ return ok(nodeId, r.durationMs, r.stdout, `cookies ${domain ?? 'all'}`);
1557
+ }
1558
+ if (action === 'set' && cookieData) {
1559
+ const inFmt = fmt ?? detectFmt(cookieData);
1560
+ const esc = cookieData.replace(/'/g, "'\\''");
1561
+ const cmd = inFmt === 'json'
1562
+ ? `mkdir -p ${cookieDir}; echo '${esc}' > "${cookieFile}"`
1563
+ : inFmt === 'header'
1564
+ ? `mkdir -p ${cookieDir}; echo '${esc}' | ${headerToJson} > "${cookieFile}"`
1565
+ : `mkdir -p ${cookieDir}; echo '${esc}' | ${netscapeToJson} > "${cookieFile}"`;
1566
+ const r = await manager.exec(nodeId, cmd);
1567
+ return r.code === 0 ? okBrief(`Cookies saved to ${nodeId}:${cookieFile} (from ${inFmt})`) : fail(r.stderr);
1568
+ }
1569
+ if (action === 'convert' && cookieData && target_format) {
1570
+ const inFmt = fmt ?? detectFmt(cookieData);
1571
+ if (inFmt === target_format)
1572
+ return okBrief(cookieData);
1573
+ const esc = cookieData.replace(/'/g, "'\\''");
1574
+ const toJson = inFmt === 'header' ? `echo '${esc}' | ${headerToJson}` : inFmt === 'netscape' ? `echo '${esc}' | ${netscapeToJson}` : `echo '${esc}'`;
1575
+ const cmd = target_format === 'json' ? toJson : target_format === 'header' ? `${toJson} | ${jsonToHeader}` : `${toJson} | ${jsonToNetscape}`;
1576
+ const r = await manager.exec(nodeId, cmd);
1577
+ return ok(nodeId, r.durationMs, r.stdout, `${inFmt} → ${target_format}`);
1578
+ }
1579
+ if (action === 'export') {
1580
+ const outFmt = fmt ?? 'json';
1581
+ const cmd = outFmt === 'header' ? `cat "${cookieFile}" | ${jsonToHeader}` : outFmt === 'netscape' ? `cat "${cookieFile}" | ${jsonToNetscape}` : `cat "${cookieFile}"`;
1582
+ const r = await manager.exec(nodeId, cmd);
1583
+ return ok(nodeId, r.durationMs, r.stdout, `export ${outFmt}`);
1584
+ }
1585
+ if (action === 'import' && cookieData) {
1586
+ const inFmt = fmt ?? detectFmt(cookieData);
1587
+ const esc = cookieData.replace(/'/g, "'\\''");
1588
+ const cmd = `mkdir -p ${cookieDir}; echo '${esc}' ${inFmt !== 'json' ? `| ${inFmt === 'header' ? headerToJson : netscapeToJson}` : ''} > "${cookieFile}"`;
1589
+ const r = await manager.exec(nodeId, cmd);
1590
+ return r.code === 0 ? okBrief(`Imported to ${nodeId}:${cookieFile}`) : fail(r.stderr);
1591
+ }
1592
+ if (action === 'extract') {
1593
+ const domFilter = domain ? `WHERE host_key LIKE '%${domain}%'` : '';
1594
+ const domFilter2 = domain ? `WHERE host LIKE '%${domain}%'` : '';
1595
+ const cmd = `FOUND=0; for db in ~/.config/google-chrome/Default/Cookies ~/.config/chromium/Default/Cookies; do [ -f "$db" ] && { echo "--- Chrome: $db ---"; sqlite3 "$db" "SELECT host_key,name,value,path,expires_utc,is_secure FROM cookies ${domFilter} LIMIT 100;" 2>/dev/null; FOUND=1; }; done; for db in ~/.mozilla/firefox/*/cookies.sqlite; do [ -f "$db" ] && { echo "--- Firefox: $db ---"; sqlite3 "$db" "SELECT host,name,value,path,expiry,isSecure FROM moz_cookies ${domFilter2} LIMIT 100;" 2>/dev/null; FOUND=1; }; done; [ $FOUND -eq 0 ] && echo "No browser DBs found"`;
1596
+ const r = await manager.exec(nodeId, cmd);
1597
+ return ok(nodeId, r.durationMs, r.stdout, `extract ${domain ?? 'all'}`);
1598
+ }
1599
+ if (action === 'sync') {
1600
+ const targets = dst_nodes ?? manager.getOnlineNodes().filter(n => n !== nodeId && n !== 'windows');
1601
+ const src = await manager.exec(nodeId, `cat "${cookieFile}" 2>/dev/null`);
1602
+ if (src.code !== 0)
1603
+ return fail(`No cookies at ${cookieFile}`);
1604
+ const b64 = Buffer.from(src.stdout).toString('base64');
1605
+ // 1. Sync to mesh nodes
1606
+ const nodeResults = await Promise.all(targets.map(async (dst) => {
1607
+ const r = await manager.exec(dst, `mkdir -p ${cookieDir}; echo '${b64}' | base64 -d > "${cookieFile}"`);
1608
+ return { ...r, nodeId: dst };
1609
+ }));
1610
+ // 2. Sync to CyberBase (PostgreSQL on contabo)
1611
+ const domainKey = domain ?? 'all';
1612
+ const pgEscaped = src.stdout.replace(/'/g, "''");
1613
+ const cyberbaseResult = await manager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -c "INSERT INTO sync_items (category, key, value, updated_at) VALUES ('cookies', '${domainKey}', '${pgEscaped}', NOW()) ON CONFLICT (category, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();" 2>/dev/null && echo "cyberbase: synced" || echo "cyberbase: skipped (no DB)"`);
1614
+ // 3. Sync to 1Password (if op CLI available)
1615
+ const opResult = await manager.exec(nodeId, `command -v op >/dev/null 2>&1 && { ` +
1616
+ `op item get "OmniWire Cookies - ${domainKey}" --vault "CyberBase" >/dev/null 2>&1 && ` +
1617
+ `op item edit "OmniWire Cookies - ${domainKey}" --vault "CyberBase" "notesPlain=$(cat "${cookieFile}")" 2>/dev/null || ` +
1618
+ `op item create --category=SecureNote --vault="CyberBase" --title="OmniWire Cookies - ${domainKey}" "notesPlain=$(cat "${cookieFile}")" 2>/dev/null; ` +
1619
+ `echo "1password: synced"; } || echo "1password: op not available"`);
1620
+ const parts = nodeResults.map(r => `${r.nodeId}: ${r.code === 0 ? 'ok' : 'fail'}`);
1621
+ parts.push(cyberbaseResult.stdout.trim());
1622
+ parts.push(opResult.stdout.trim());
1623
+ return okBrief(`Cookie sync: ${parts.join(' | ')}`);
1624
+ }
1625
+ if (action === 'clear') {
1626
+ const r = await manager.exec(nodeId, `rm -f ${domain ? cookieFile : `${cookieDir}/*`} 2>/dev/null; echo "cleared"`);
1627
+ return ok(nodeId, r.durationMs, r.stdout, 'clear cookies');
1628
+ }
1629
+ if (action === 'cyberbase-get') {
1630
+ const domainKey = domain ?? 'all';
1631
+ const r = await manager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -t -c "SELECT value FROM sync_items WHERE category='cookies' AND key='${domainKey}';" 2>/dev/null`);
1632
+ if (!r.stdout.trim())
1633
+ return fail(`No cookies for '${domainKey}' in CyberBase`);
1634
+ return ok('contabo', r.durationMs, r.stdout.trim(), `cyberbase cookies: ${domainKey}`);
1635
+ }
1636
+ if (action === 'cyberbase-set' && cookieData) {
1637
+ const domainKey = domain ?? 'all';
1638
+ const pgEsc = cookieData.replace(/'/g, "''");
1639
+ const r = await manager.exec('contabo', `psql -h 127.0.0.1 -U cyberbase -d cyberbase -c "INSERT INTO sync_items (category, key, value, updated_at) VALUES ('cookies', '${domainKey}', '${pgEsc}', NOW()) ON CONFLICT (category, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();" 2>/dev/null`);
1640
+ return r.code === 0 ? okBrief(`Cookies stored in CyberBase: ${domainKey}`) : fail(r.stderr);
1641
+ }
1642
+ if (action === '1password-get') {
1643
+ const domainKey = domain ?? 'all';
1644
+ const r = await manager.exec(nodeId, `op item get "OmniWire Cookies - ${domainKey}" --vault "CyberBase" --fields notesPlain 2>/dev/null || echo "not found in 1Password"`);
1645
+ return ok(nodeId, r.durationMs, r.stdout, `1password cookies: ${domainKey}`);
1646
+ }
1647
+ if (action === '1password-set' && cookieData) {
1648
+ const domainKey = domain ?? 'all';
1649
+ const esc = cookieData.replace(/'/g, "'\\''");
1650
+ const r = await manager.exec(nodeId, `command -v op >/dev/null || { echo "op CLI not installed"; exit 1; }; ` +
1651
+ `op item get "OmniWire Cookies - ${domainKey}" --vault "CyberBase" >/dev/null 2>&1 && ` +
1652
+ `op item edit "OmniWire Cookies - ${domainKey}" --vault "CyberBase" "notesPlain=${esc}" 2>/dev/null || ` +
1653
+ `op item create --category=SecureNote --vault="CyberBase" --title="OmniWire Cookies - ${domainKey}" "notesPlain=${esc}" 2>/dev/null`);
1654
+ return r.code === 0 ? okBrief(`Cookies stored in 1Password: ${domainKey}`) : fail(r.stderr || r.stdout);
1655
+ }
1656
+ return fail('Invalid action or missing params');
1657
+ });
1658
+ // --- Tool 33: omniwire_cdp ---
1659
+ server.tool('omniwire_cdp', 'Chrome DevTools Protocol browser control on mesh nodes. Launch headless Chrome, navigate, screenshot, extract DOM/cookies, save PDF. For scraping, testing, recon.', {
1660
+ action: z.enum(['launch', 'navigate', 'screenshot', 'html', 'pdf', 'cookies', 'tabs', 'close', 'list']).describe('launch=start Chrome, navigate=open URL, screenshot=capture, html=dump DOM, pdf=save PDF, cookies=extract, tabs=list tabs, close=kill, list=sessions'),
1661
+ node: z.string().optional().describe('Node (default: contabo)'),
1662
+ url: z.string().optional().describe('URL for navigate/screenshot/html/pdf'),
1663
+ file: z.string().optional().describe('Output path for screenshot/pdf'),
1664
+ session_id: z.string().optional().describe('Session ID from launch'),
1665
+ }, async ({ action, node, url, file: outFile, session_id }) => {
1666
+ const nodeId = node ?? 'contabo';
1667
+ const sdir = '/tmp/.omniwire-cdp';
1668
+ if (action === 'launch') {
1669
+ const id = `cdp-${Date.now().toString(36)}`;
1670
+ const port = 9222 + Math.floor(Math.random() * 100);
1671
+ const cmd = `mkdir -p ${sdir}; command -v google-chrome >/dev/null || command -v chromium-browser >/dev/null || { echo "chrome not installed"; exit 1; }; ` +
1672
+ `CHROME=$(command -v google-chrome || command -v chromium-browser); ` +
1673
+ `$CHROME --headless --disable-gpu --no-sandbox --remote-debugging-port=${port} --user-data-dir=${sdir}/${id} ${url ? `"${url}"` : 'about:blank'} &>/dev/null & ` +
1674
+ `echo $! > ${sdir}/${id}.pid; echo ${port} > ${sdir}/${id}.port; sleep 1; ` +
1675
+ `echo "session=${id} port=${port} pid=$(cat ${sdir}/${id}.pid)"`;
1676
+ const r = await manager.exec(nodeId, cmd);
1677
+ if (r.code === 0)
1678
+ resultStore.set('cdp_session', id);
1679
+ return ok(nodeId, r.durationMs, r.stdout, 'chrome launch');
1680
+ }
1681
+ const sid = session_id ?? resultStore.get('cdp_session') ?? '';
1682
+ if (action === 'navigate' && url) {
1683
+ const cmd = `PORT=$(cat ${sdir}/${sid}.port 2>/dev/null); curl -sf "http://localhost:$PORT/json/new?${encodeURIComponent(url)}" 2>/dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);print(f\\"tab={d.get('id','')} url={d.get('url','')}\\")" 2>/dev/null || echo "opened ${url}"`;
1684
+ const r = await manager.exec(nodeId, cmd);
1685
+ return ok(nodeId, r.durationMs, r.stdout, `navigate`);
1686
+ }
1687
+ if (action === 'screenshot') {
1688
+ const output = outFile ?? `/tmp/screenshot-${Date.now()}.png`;
1689
+ const target = url ?? (sid ? `$(curl -sf http://localhost:$(cat ${sdir}/${sid}.port)/json/list 2>/dev/null | python3 -c "import json,sys;print(json.load(sys.stdin)[0]['url'])" 2>/dev/null)` : 'about:blank');
1690
+ const cmd = `CHROME=$(command -v google-chrome || command -v chromium-browser); $CHROME --headless --no-sandbox --disable-gpu --screenshot="${output}" --window-size=1920,1080 "${target}" 2>/dev/null && echo "saved: ${output} ($(du -h "${output}" | cut -f1))"`;
1691
+ const r = await manager.exec(nodeId, cmd);
1692
+ return ok(nodeId, r.durationMs, r.stdout, 'screenshot');
1693
+ }
1694
+ if (action === 'html') {
1695
+ const target = url ?? 'about:blank';
1696
+ const cmd = `CHROME=$(command -v google-chrome || command -v chromium-browser); $CHROME --headless --no-sandbox --disable-gpu --dump-dom "${target}" 2>/dev/null | head -200`;
1697
+ const r = await manager.exec(nodeId, cmd);
1698
+ return ok(nodeId, r.durationMs, r.stdout, 'html');
1699
+ }
1700
+ if (action === 'pdf') {
1701
+ const output = outFile ?? `/tmp/page-${Date.now()}.pdf`;
1702
+ const target = url ?? 'about:blank';
1703
+ const cmd = `CHROME=$(command -v google-chrome || command -v chromium-browser); $CHROME --headless --no-sandbox --disable-gpu --print-to-pdf="${output}" --no-pdf-header-footer "${target}" 2>/dev/null && echo "saved: ${output} ($(du -h "${output}" | cut -f1))"`;
1704
+ const r = await manager.exec(nodeId, cmd);
1705
+ return ok(nodeId, r.durationMs, r.stdout, 'pdf');
1706
+ }
1707
+ if (action === 'cookies') {
1708
+ const cmd = `find ${sdir}/${sid}/ -name "Cookies" 2>/dev/null | head -1 | xargs -I{} sqlite3 "{}" "SELECT host_key,name,value,path,expires_utc FROM cookies LIMIT 50;" 2>/dev/null || echo "no cookies DB"`;
1709
+ const r = await manager.exec(nodeId, cmd);
1710
+ return ok(nodeId, r.durationMs, r.stdout, 'cdp cookies');
1711
+ }
1712
+ if (action === 'tabs') {
1713
+ const cmd = `PORT=$(cat ${sdir}/${sid}.port 2>/dev/null); curl -sf http://localhost:$PORT/json/list 2>/dev/null | python3 -c "import json,sys;[print(f\\"{t['id'][:8]} {t.get('url','')}\\" ) for t in json.load(sys.stdin)]" 2>/dev/null || echo "no tabs"`;
1714
+ const r = await manager.exec(nodeId, cmd);
1715
+ return ok(nodeId, r.durationMs, r.stdout, 'tabs');
1716
+ }
1717
+ if (action === 'close') {
1718
+ const cmd = `PID=$(cat ${sdir}/${sid}.pid 2>/dev/null); [ -n "$PID" ] && kill $PID 2>/dev/null && rm -rf ${sdir}/${sid}* && echo "closed ${sid}" || echo "not found"`;
1719
+ const r = await manager.exec(nodeId, cmd);
1720
+ return ok(nodeId, r.durationMs, r.stdout, 'close');
1721
+ }
1722
+ if (action === 'list') {
1723
+ const cmd = `ls ${sdir}/*.pid 2>/dev/null | while read f; do id=$(basename "$f" .pid); port=$(cat ${sdir}/$id.port 2>/dev/null); pid=$(cat "$f"); ps -p $pid >/dev/null 2>&1 && s="running" || s="dead"; echo "$id port=$port pid=$pid $s"; done || echo "no sessions"`;
1724
+ const r = await manager.exec(nodeId, cmd);
1725
+ return ok(nodeId, r.durationMs, r.stdout, 'cdp sessions');
1726
+ }
1727
+ return fail('Invalid action or missing params');
1728
+ });
1729
+ // --- Tool 34: omniwire_proxy ---
1730
+ server.tool('omniwire_proxy', 'HTTP/SOCKS proxy management on mesh nodes. Start HTTP proxies, SOCKS tunnels via SSH -D, or socat TCP forwarders. Actions: start, stop, status, list.', {
1731
+ action: z.enum(['start', 'stop', 'status', 'list']).describe('Action'),
1732
+ node: z.string().optional().describe('Target node (default: contabo)'),
1733
+ type: z.enum(['http', 'socks', 'forward']).optional().describe('http=python http.server, socks=SSH -D SOCKS5, forward=socat TCP forwarder'),
1734
+ port: z.number().optional().describe('Local port to listen on'),
1735
+ target: z.string().optional().describe('Target for forward type (host:port)'),
1736
+ }, async ({ action, node, type, port, target }) => {
1737
+ const nodeId = node ?? 'contabo';
1738
+ const pd = '/tmp/.omniwire-proxies';
1739
+ if (action === 'list' || action === 'status') {
1740
+ const result = await manager.exec(nodeId, `mkdir -p ${pd}; ls ${pd}/*.pid 2>/dev/null | while read f; do id=$(basename "$f" .pid); pid=$(cat "$f" 2>/dev/null); ptype=$(cat "${pd}/$id.type" 2>/dev/null); pport=$(cat "${pd}/$id.port" 2>/dev/null); alive=$(kill -0 "$pid" 2>/dev/null && echo running || echo dead); echo "$id $ptype :$pport pid=$pid $alive"; done || echo "(none)"; ss -tlnp 2>/dev/null | grep LISTEN | head -20`);
1741
+ return ok(nodeId, result.durationMs, result.stdout, 'proxy status');
1742
+ }
1743
+ if (action === 'start') {
1744
+ const proxyType = type ?? 'http';
1745
+ const listenPort = port ?? (proxyType === 'http' ? 8888 : proxyType === 'socks' ? 1080 : 9090);
1746
+ const id = `ow-proxy-${proxyType}-${listenPort}`;
1747
+ let startCmd;
1748
+ if (proxyType === 'http') {
1749
+ startCmd = `cd /tmp && python3 -m http.server ${listenPort} --bind 0.0.0.0 >/tmp/${id}.log 2>&1 & echo $! > ${pd}/${id}.pid`;
1750
+ }
1751
+ else if (proxyType === 'socks') {
1752
+ startCmd = `ssh -D 0.0.0.0:${listenPort} -N -f -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes localhost 2>/dev/null; pgrep -f "ssh -D.*${listenPort}" | head -1 > ${pd}/${id}.pid`;
1753
+ }
1754
+ else {
1755
+ if (!target)
1756
+ return fail('target (host:port) required for forward type');
1757
+ const et = target.replace(/'/g, "'\\''");
1758
+ startCmd = `socat TCP-LISTEN:${listenPort},fork,reuseaddr TCP:${et} >/tmp/${id}.log 2>&1 & echo $! > ${pd}/${id}.pid`;
1759
+ }
1760
+ const result = await manager.exec(nodeId, `mkdir -p ${pd}; ${startCmd}; echo "${proxyType}" > ${pd}/${id}.type; echo "${listenPort}" > ${pd}/${id}.port; sleep 0.5; pid=$(cat ${pd}/${id}.pid 2>/dev/null); kill -0 "$pid" 2>/dev/null && echo "started: ${id} on :${listenPort} pid=$pid" || echo "WARN: check /tmp/${id}.log"`);
1761
+ return ok(nodeId, result.durationMs, result.stdout, `proxy start ${proxyType}:${listenPort}`);
1762
+ }
1763
+ if (action === 'stop') {
1764
+ const listenPort = port ?? 0;
1765
+ const stopCmd = listenPort
1766
+ ? `for f in ${pd}/*.port; do p=$(cat "$f" 2>/dev/null); if [ "$p" = "${listenPort}" ]; then id=$(basename "$f" .port); pid=$(cat "${pd}/$id.pid" 2>/dev/null); kill "$pid" 2>/dev/null; rm -f "${pd}/$id."*; echo "stopped $id"; fi; done`
1767
+ : `for f in ${pd}/*.pid; do pid=$(cat "$f" 2>/dev/null); kill "$pid" 2>/dev/null; done; rm -f ${pd}/*.pid ${pd}/*.type ${pd}/*.port; echo "all proxies stopped"`;
1768
+ const result = await manager.exec(nodeId, `mkdir -p ${pd}; ${stopCmd}`);
1769
+ return ok(nodeId, result.durationMs, result.stdout, 'proxy stop');
1770
+ }
1771
+ return fail('invalid action');
1772
+ });
1773
+ // --- Tool 35: omniwire_dns ---
1774
+ server.tool('omniwire_dns', 'DNS management on mesh nodes. Resolve hostnames, switch DNS servers, flush caches, manage /etc/hosts entries.', {
1775
+ action: z.enum(['resolve', 'set-server', 'flush-cache', 'zone-add', 'zone-list', 'block-domain']).describe('Action'),
1776
+ node: z.string().optional().describe('Target node (default: contabo)'),
1777
+ domain: z.string().optional().describe('Domain to resolve or block'),
1778
+ server: z.string().optional().describe('DNS server IP for set-server (e.g., 1.1.1.1)'),
1779
+ ip: z.string().optional().describe('IP for zone-add or block-domain override (default: 0.0.0.0)'),
1780
+ }, async ({ action, node, domain, server, ip }) => {
1781
+ const nodeId = node ?? 'contabo';
1782
+ if (action === 'resolve') {
1783
+ if (!domain)
1784
+ return fail('domain required');
1785
+ const d = domain.replace(/'/g, "'\\''");
1786
+ const result = await manager.exec(nodeId, `dig +short '${d}' 2>/dev/null || nslookup '${d}' 2>&1 | grep -E 'Address:|Name:' | tail -5`);
1787
+ return ok(nodeId, result.durationMs, result.stdout || '(no result)', `resolve ${domain}`);
1788
+ }
1789
+ if (action === 'set-server') {
1790
+ if (!server)
1791
+ return fail('server IP required');
1792
+ const result = await manager.exec(nodeId, `printf 'nameserver ${server}\nsearch %s\n' "$(hostname -d 2>/dev/null || echo local)" | tee /etc/resolv.conf; echo "DNS set to ${server}"; dig +short google.com @${server} 2>/dev/null | head -3`);
1793
+ return ok(nodeId, result.durationMs, result.stdout, `dns set-server ${server}`);
1794
+ }
1795
+ if (action === 'flush-cache') {
1796
+ const result = await manager.exec(nodeId, 'systemd-resolve --flush-caches 2>/dev/null && echo "flushed via systemd-resolve" || resolvectl flush-caches 2>/dev/null && echo "flushed via resolvectl" || (service nscd restart 2>/dev/null && echo "nscd restarted") || echo "no cache daemon found"');
1797
+ return ok(nodeId, result.durationMs, result.stdout, 'dns flush-cache');
1798
+ }
1799
+ if (action === 'block-domain') {
1800
+ if (!domain)
1801
+ return fail('domain required');
1802
+ const blockIp = ip ?? '0.0.0.0';
1803
+ const d = domain.replace(/'/g, "'\\''");
1804
+ const result = await manager.exec(nodeId, `grep -qF '${d}' /etc/hosts 2>/dev/null && echo "already in /etc/hosts" || (echo "${blockIp} ${d}" | tee -a /etc/hosts && echo "blocked ${domain} -> ${blockIp}")`);
1805
+ return ok(nodeId, result.durationMs, result.stdout, `dns block ${domain}`);
1806
+ }
1807
+ if (action === 'zone-add') {
1808
+ if (!domain || !ip)
1809
+ return fail('domain and ip required for zone-add');
1810
+ const d = domain.replace(/'/g, "'\\''");
1811
+ const result = await manager.exec(nodeId, `grep -qF '${d}' /etc/hosts 2>/dev/null && sed -i "s/.*${d}.*/${ip} ${d}/" /etc/hosts && echo "updated ${domain}" || (echo "${ip} ${d}" >> /etc/hosts && echo "added ${domain} -> ${ip}")`);
1812
+ return ok(nodeId, result.durationMs, result.stdout, `dns zone-add ${domain}`);
1813
+ }
1814
+ if (action === 'zone-list') {
1815
+ const result = await manager.exec(nodeId, "grep -v '^#' /etc/hosts | grep -v '^$' | sort");
1816
+ return ok(nodeId, result.durationMs, result.stdout, 'dns zone-list');
1817
+ }
1818
+ return fail('invalid action');
1819
+ });
1820
+ // --- Tool 36: omniwire_backup ---
1821
+ server.tool('omniwire_backup', 'Snapshot and restore paths on mesh nodes. Creates timestamped tarballs in /var/backups/omniwire/. Actions: snapshot, restore, list, diff, cleanup.', {
1822
+ action: z.enum(['snapshot', 'restore', 'list', 'diff', 'cleanup']).describe('Action'),
1823
+ node: z.string().optional().describe('Target node (default: contabo)'),
1824
+ path: z.string().optional().describe('Path to snapshot or restore to'),
1825
+ backup_id: z.string().optional().describe('Backup filename (for restore/diff). Use list to find IDs.'),
1826
+ retention_days: z.number().optional().describe('Days to keep for cleanup (default: 30)'),
1827
+ }, async ({ action, node, path, backup_id, retention_days }) => {
1828
+ const nodeId = node ?? 'contabo';
1829
+ const bd = '/var/backups/omniwire';
1830
+ if (action === 'snapshot') {
1831
+ if (!path)
1832
+ return fail('path required');
1833
+ const p = path.replace(/'/g, "'\\''");
1834
+ const result = await manager.exec(nodeId, `mkdir -p ${bd}; ts=\$(date +%s); host=\$(hostname); out="${bd}/\${host}-\${ts}.tar.gz"; tar czf "$out" '${p}' 2>&1 && ls -lh "$out" && echo "snapshot: $out" || echo "snapshot failed"`);
1835
+ return ok(nodeId, result.durationMs, result.stdout, `backup snapshot ${path}`);
1836
+ }
1837
+ if (action === 'list') {
1838
+ const result = await manager.exec(nodeId, `ls -lht ${bd}/ 2>/dev/null | head -30 || echo "(no backups)"`);
1839
+ return ok(nodeId, result.durationMs, result.stdout, 'backup list');
1840
+ }
1841
+ if (action === 'restore') {
1842
+ if (!backup_id || !path)
1843
+ return fail('backup_id and path required');
1844
+ const p = path.replace(/'/g, "'\\''");
1845
+ const bid = backup_id.replace(/'/g, "'\\''");
1846
+ const result = await manager.exec(nodeId, `mkdir -p '${p}'; tar xzf '${bd}/${bid}' -C '${p}' 2>&1 && echo "restored to ${path}" || echo "restore failed"`);
1847
+ return ok(nodeId, result.durationMs, result.stdout, `backup restore ${backup_id}`);
1848
+ }
1849
+ if (action === 'diff') {
1850
+ if (!backup_id || !path)
1851
+ return fail('backup_id and path required for diff');
1852
+ const p = path.replace(/'/g, "'\\''");
1853
+ const bid = backup_id.replace(/'/g, "'\\''");
1854
+ const tmpDir = `/tmp/.ow-diff-${Date.now().toString(36)}`;
1855
+ const result = await manager.exec(nodeId, `mkdir -p ${tmpDir}; tar xzf '${bd}/${bid}' -C ${tmpDir} 2>/dev/null; diff -rq '${p}' ${tmpDir} 2>&1 | head -40; rm -rf ${tmpDir}`);
1856
+ return ok(nodeId, result.durationMs, result.stdout || '(no differences)', `backup diff ${backup_id}`);
1857
+ }
1858
+ if (action === 'cleanup') {
1859
+ const days = retention_days ?? 30;
1860
+ const result = await manager.exec(nodeId, `find ${bd}/ -name '*.tar.gz' -mtime +${days} -print -delete 2>/dev/null | wc -l | xargs -I{} echo "removed {} backups older than ${days} days"`);
1861
+ return ok(nodeId, result.durationMs, result.stdout, `backup cleanup >${days}d`);
1862
+ }
1863
+ return fail('invalid action');
1864
+ });
1865
+ // --- Tool 37: omniwire_container ---
1866
+ server.tool('omniwire_container', 'Full Docker container lifecycle management. Actions: compose-up, compose-down, build, push, logs, ps, prune, stats, inspect.', {
1867
+ action: z.enum(['compose-up', 'compose-down', 'build', 'push', 'logs', 'ps', 'prune', 'stats', 'inspect']).describe('Action'),
1868
+ node: z.string().optional().describe('Target node (default: contabo)'),
1869
+ container: z.string().optional().describe('Container name or ID (for logs, inspect)'),
1870
+ file: z.string().optional().describe('docker-compose file path'),
1871
+ tag: z.string().optional().describe('Image tag for build/push'),
1872
+ context: z.string().optional().describe('Build context path (default: .)'),
1873
+ tail_lines: z.number().optional().describe('Log lines to tail (default: 50)'),
1874
+ }, async ({ action, node, container, file, tag, context, tail_lines }) => {
1875
+ const nodeId = node ?? 'contabo';
1876
+ if (action === 'compose-up') {
1877
+ const cf = file ? `-f '${file.replace(/'/g, "'\\''")}'` : '';
1878
+ const result = await manager.exec(nodeId, `docker compose ${cf} up -d 2>&1`);
1879
+ return ok(nodeId, result.durationMs, result.stdout + result.stderr, 'compose up');
1880
+ }
1881
+ if (action === 'compose-down') {
1882
+ const cf = file ? `-f '${file.replace(/'/g, "'\\''")}'` : '';
1883
+ const result = await manager.exec(nodeId, `docker compose ${cf} down 2>&1`);
1884
+ return ok(nodeId, result.durationMs, result.stdout + result.stderr, 'compose down');
1885
+ }
1886
+ if (action === 'build') {
1887
+ if (!tag)
1888
+ return fail('tag required for build');
1889
+ const ctx = context ?? '.';
1890
+ const result = await manager.exec(nodeId, `docker build -t '${tag.replace(/'/g, "'\\''")}' '${ctx.replace(/'/g, "'\\''")}' 2>&1 | tail -20`);
1891
+ return ok(nodeId, result.durationMs, result.stdout, `docker build ${tag}`);
1892
+ }
1893
+ if (action === 'push') {
1894
+ if (!tag)
1895
+ return fail('tag required for push');
1896
+ const result = await manager.exec(nodeId, `docker push '${tag.replace(/'/g, "'\\''")}' 2>&1`);
1897
+ return ok(nodeId, result.durationMs, result.stdout + result.stderr, `docker push ${tag}`);
1898
+ }
1899
+ if (action === 'logs') {
1900
+ if (!container)
1901
+ return fail('container required for logs');
1902
+ const lines = tail_lines ?? 50;
1903
+ const result = await manager.exec(nodeId, `docker logs --tail ${lines} '${container.replace(/'/g, "'\\''")}' 2>&1`);
1904
+ return ok(nodeId, result.durationMs, result.stdout + result.stderr, `docker logs ${container}`);
1905
+ }
1906
+ if (action === 'ps') {
1907
+ const result = await manager.exec(nodeId, `docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>&1`);
1908
+ return ok(nodeId, result.durationMs, result.stdout, 'docker ps');
1909
+ }
1910
+ if (action === 'prune') {
1911
+ const result = await manager.exec(nodeId, 'docker system prune -af --volumes 2>&1 | tail -10');
1912
+ return ok(nodeId, result.durationMs, result.stdout, 'docker prune');
1913
+ }
1914
+ if (action === 'stats') {
1915
+ const result = await manager.exec(nodeId, `docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" 2>&1`);
1916
+ return ok(nodeId, result.durationMs, result.stdout, 'docker stats');
1917
+ }
1918
+ if (action === 'inspect') {
1919
+ if (!container)
1920
+ return fail('container required for inspect');
1921
+ const result = await manager.exec(nodeId, `docker inspect '${container.replace(/'/g, "'\\''")}' 2>&1 | head -60`);
1922
+ return ok(nodeId, result.durationMs, result.stdout, `docker inspect ${container}`);
1923
+ }
1924
+ return fail('invalid action');
1925
+ });
1926
+ // --- Tool 38: omniwire_cert ---
1927
+ server.tool('omniwire_cert', 'TLS certificate management. List, issue via certbot, renew, check expiry, inspect cert details, or generate self-signed certs.', {
1928
+ action: z.enum(['list', 'issue', 'renew', 'check-expiry', 'info', 'generate-self-signed']).describe('Action'),
1929
+ node: z.string().optional().describe('Target node (default: contabo)'),
1930
+ domain: z.string().optional().describe('Domain name'),
1931
+ email: z.string().optional().describe('Email for certbot ACME registration'),
1932
+ path: z.string().optional().describe('Certificate file path (for info action)'),
1933
+ }, async ({ action, node, domain, email, path }) => {
1934
+ const nodeId = node ?? 'contabo';
1935
+ if (action === 'list') {
1936
+ const result = await manager.exec(nodeId, "ls /etc/letsencrypt/live/ 2>/dev/null && echo '---pem---' && ls /etc/ssl/certs/*.pem 2>/dev/null | head -20 || echo '(no certs found)'");
1937
+ return ok(nodeId, result.durationMs, result.stdout, 'cert list');
1938
+ }
1939
+ if (action === 'issue') {
1940
+ if (!domain)
1941
+ return fail('domain required');
1942
+ if (!email)
1943
+ return fail('email required for certbot');
1944
+ const d = domain.replace(/'/g, "'\\''");
1945
+ const e = email.replace(/'/g, "'\\''");
1946
+ const result = await manager.exec(nodeId, `certbot certonly --standalone -d '${d}' --non-interactive --agree-tos -m '${e}' 2>&1 | tail -20`);
1947
+ return ok(nodeId, result.durationMs, result.stdout, `cert issue ${domain}`);
1948
+ }
1949
+ if (action === 'renew') {
1950
+ const result = await manager.exec(nodeId, 'certbot renew 2>&1 | tail -20');
1951
+ return ok(nodeId, result.durationMs, result.stdout, 'cert renew');
1952
+ }
1953
+ if (action === 'check-expiry') {
1954
+ if (!domain)
1955
+ return fail('domain required');
1956
+ const d = domain.replace(/'/g, "'\\''");
1957
+ const result = await manager.exec(nodeId, `echo | openssl s_client -connect '${d}':443 -servername '${d}' 2>/dev/null | openssl x509 -noout -dates 2>/dev/null || echo "could not connect to ${domain}:443"`);
1958
+ return ok(nodeId, result.durationMs, result.stdout, `cert expiry ${domain}`);
1959
+ }
1960
+ if (action === 'info') {
1961
+ if (!path)
1962
+ return fail('path required for info');
1963
+ const p = path.replace(/'/g, "'\\''");
1964
+ const result = await manager.exec(nodeId, `openssl x509 -in '${p}' -noout -text 2>&1 | head -30`);
1965
+ return ok(nodeId, result.durationMs, result.stdout, `cert info ${path}`);
1966
+ }
1967
+ if (action === 'generate-self-signed') {
1968
+ if (!domain)
1969
+ return fail('domain required');
1970
+ const d = domain.replace(/'/g, "'\\''");
1971
+ const outDir = '/etc/ssl/omniwire';
1972
+ const result = await manager.exec(nodeId, `mkdir -p ${outDir}; openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ${outDir}/${d}.key -out ${outDir}/${d}.crt -subj "/CN=${d}/O=OmniWire/C=NO" 2>&1 && echo "generated: ${outDir}/${d}.crt + .key" && openssl x509 -in ${outDir}/${d}.crt -noout -dates`);
1973
+ return ok(nodeId, result.durationMs, result.stdout, `cert self-signed ${domain}`);
1974
+ }
1975
+ return fail('invalid action');
1976
+ });
1977
+ // --- Tool 39: omniwire_user ---
1978
+ server.tool('omniwire_user', 'User and SSH key management on mesh nodes. Actions: list, add, remove, add-key, remove-key, sudo-add, sudo-remove, passwd.', {
1979
+ action: z.enum(['list', 'add', 'remove', 'add-key', 'remove-key', 'sudo-add', 'sudo-remove', 'passwd']).describe('Action'),
1980
+ node: z.string().optional().describe('Target node (default: contabo)'),
1981
+ username: z.string().optional().describe('Username to operate on'),
1982
+ ssh_key: z.string().optional().describe('SSH public key string (for add-key/remove-key)'),
1983
+ password: z.string().optional().describe('Password for passwd action'),
1984
+ }, async ({ action, node, username, ssh_key, password }) => {
1985
+ const nodeId = node ?? 'contabo';
1986
+ if (action === 'list') {
1987
+ const result = await manager.exec(nodeId, "getent passwd | grep -v '/sbin/nologin\|/bin/false\|/usr/sbin/nologin' | awk -F: '{print $1, $3, $6}' | column -t");
1988
+ return ok(nodeId, result.durationMs, result.stdout, 'user list');
1989
+ }
1990
+ if (action === 'add') {
1991
+ if (!username)
1992
+ return fail('username required');
1993
+ const u = username.replace(/'/g, "'\\''");
1994
+ const result = await manager.exec(nodeId, `useradd -m -s /bin/bash '${u}' 2>&1 && echo "user ${username} created" || echo "user may already exist"`);
1995
+ return ok(nodeId, result.durationMs, result.stdout, `user add ${username}`);
1996
+ }
1997
+ if (action === 'remove') {
1998
+ if (!username)
1999
+ return fail('username required');
2000
+ const u = username.replace(/'/g, "'\\''");
2001
+ const result = await manager.exec(nodeId, `userdel -r '${u}' 2>&1 && echo "user ${username} removed" || echo "failed to remove user"`);
2002
+ return ok(nodeId, result.durationMs, result.stdout, `user remove ${username}`);
2003
+ }
2004
+ if (action === 'add-key') {
2005
+ if (!username || !ssh_key)
2006
+ return fail('username and ssh_key required');
2007
+ const u = username.replace(/'/g, "'\\''");
2008
+ const k = ssh_key.replace(/'/g, "'\\''");
2009
+ const result = await manager.exec(nodeId, `homedir=$(getent passwd '${u}' | cut -d: -f6); mkdir -p "$homedir/.ssh"; chmod 700 "$homedir/.ssh"; grep -qF '${k}' "$homedir/.ssh/authorized_keys" 2>/dev/null && echo "key already present" || (echo '${k}' >> "$homedir/.ssh/authorized_keys" && chmod 600 "$homedir/.ssh/authorized_keys" && chown -R '${u}' "$homedir/.ssh" && echo "key added for ${username}")`);
2010
+ return ok(nodeId, result.durationMs, result.stdout, `user add-key ${username}`);
2011
+ }
2012
+ if (action === 'remove-key') {
2013
+ if (!username || !ssh_key)
2014
+ return fail('username and ssh_key required');
2015
+ const u = username.replace(/'/g, "'\\''");
2016
+ const k = ssh_key.replace(/'/g, "'\\''");
2017
+ const result = await manager.exec(nodeId, `homedir=$(getent passwd '${u}' | cut -d: -f6); grep -vF '${k}' "$homedir/.ssh/authorized_keys" 2>/dev/null > /tmp/.ow-keys.tmp && mv /tmp/.ow-keys.tmp "$homedir/.ssh/authorized_keys" && echo "key removed for ${username}" || echo "failed"`);
2018
+ return ok(nodeId, result.durationMs, result.stdout, `user remove-key ${username}`);
2019
+ }
2020
+ if (action === 'sudo-add') {
2021
+ if (!username)
2022
+ return fail('username required');
2023
+ const u = username.replace(/'/g, "'\\''");
2024
+ const result = await manager.exec(nodeId, `echo '${u} ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/${u} && chmod 440 /etc/sudoers.d/${u} && echo "sudo added for ${username}"`);
2025
+ return ok(nodeId, result.durationMs, result.stdout, `user sudo-add ${username}`);
2026
+ }
2027
+ if (action === 'sudo-remove') {
2028
+ if (!username)
2029
+ return fail('username required');
2030
+ const u = username.replace(/'/g, "'\\''");
2031
+ const result = await manager.exec(nodeId, `rm -f '/etc/sudoers.d/${u}' && echo "sudo removed for ${username}"`);
2032
+ return ok(nodeId, result.durationMs, result.stdout, `user sudo-remove ${username}`);
2033
+ }
2034
+ if (action === 'passwd') {
2035
+ if (!username || !password)
2036
+ return fail('username and password required');
2037
+ const u = username.replace(/'/g, "'\\''");
2038
+ const pw = password.replace(/'/g, "'\\''");
2039
+ const result = await manager.exec(nodeId, `echo '${u}:${pw}' | chpasswd 2>&1 && echo "password updated for ${username}" || echo "chpasswd failed"`);
2040
+ return ok(nodeId, result.durationMs, result.stdout, `user passwd ${username}`);
2041
+ }
2042
+ return fail('invalid action');
2043
+ });
2044
+ // --- Tool 40: omniwire_schedule ---
2045
+ server.tool('omniwire_schedule', 'Distributed cron scheduling with failover. Stores schedule JSON, writes crontab entries on preferred node, supports fallback nodes. Actions: add, remove, list, run-now, history.', {
2046
+ action: z.enum(['add', 'remove', 'list', 'run-now', 'history']).describe('Action'),
2047
+ node: z.string().optional().describe('Preferred node for execution (default: contabo)'),
2048
+ schedule_id: z.string().optional().describe('Schedule identifier'),
2049
+ command: z.string().optional().describe('Command to schedule (for add)'),
2050
+ cron_expr: z.string().optional().describe('Cron expression (e.g. "0 */6 * * *")'),
2051
+ fallback_nodes: z.array(z.string()).optional().describe('Fallback nodes if primary is offline'),
2052
+ }, async ({ action, node, schedule_id, command, cron_expr, fallback_nodes }) => {
2053
+ const nodeId = node ?? 'contabo';
2054
+ const sd = '/etc/omniwire/schedules';
2055
+ if (action === 'list') {
2056
+ const result = await manager.exec(nodeId, `mkdir -p ${sd}; ls ${sd}/*.json 2>/dev/null | while read f; do id=$(basename "$f" .json); echo "--- $id ---"; cat "$f" 2>/dev/null | grep -E '"command"|"cron_expr"|"node"'; done || echo "(no schedules)"; echo "=== crontab ==="; crontab -l 2>/dev/null | grep omniwire || echo "(none)"`);
2057
+ return ok(nodeId, result.durationMs, result.stdout, 'schedule list');
2058
+ }
2059
+ if (action === 'add') {
2060
+ if (!schedule_id || !command || !cron_expr)
2061
+ return fail('schedule_id, command, and cron_expr required');
2062
+ const sid = schedule_id.replace(/'/g, "'\\''");
2063
+ const cmd = command.replace(/'/g, "'\\''");
2064
+ const meta = JSON.stringify({ id: schedule_id, command, cron_expr, node: nodeId, fallback_nodes: fallback_nodes ?? [], created: Date.now() });
2065
+ const metaEsc = meta.replace(/'/g, "'\\''");
2066
+ const result = await manager.exec(nodeId, `mkdir -p ${sd}; echo '${metaEsc}' > ${sd}/${sid}.json; (crontab -l 2>/dev/null; echo '# omniwire:${sid}'; echo '${cron_expr} ${cmd}') | crontab -; echo "schedule ${schedule_id} added: ${cron_expr}"`);
2067
+ return ok(nodeId, result.durationMs, result.stdout, `schedule add ${schedule_id}`);
2068
+ }
2069
+ if (action === 'remove') {
2070
+ if (!schedule_id)
2071
+ return fail('schedule_id required');
2072
+ const sid = schedule_id.replace(/'/g, "'\\''");
2073
+ const result = await manager.exec(nodeId, `rm -f ${sd}/${sid}.json; crontab -l 2>/dev/null | grep -v 'omniwire:${sid}' | crontab -; echo "schedule ${schedule_id} removed"`);
2074
+ return ok(nodeId, result.durationMs, result.stdout, `schedule remove ${schedule_id}`);
2075
+ }
2076
+ if (action === 'run-now') {
2077
+ if (!schedule_id)
2078
+ return fail('schedule_id required');
2079
+ const sid = schedule_id.replace(/'/g, "'\\''");
2080
+ const result = await manager.exec(nodeId, `cmd=$(python3 -c "import json,sys; d=json.load(open('${sd}/${sid}.json')); print(d['command'])" 2>/dev/null); if [ -z "$cmd" ]; then echo "schedule ${schedule_id} not found"; exit 1; fi; echo "running: $cmd"; bash -c "$cmd" 2>&1`);
2081
+ return ok(nodeId, result.durationMs, result.stdout, `schedule run-now ${schedule_id}`);
2082
+ }
2083
+ if (action === 'history') {
2084
+ if (!schedule_id)
2085
+ return fail('schedule_id required');
2086
+ const sid = schedule_id.replace(/'/g, "'\\''");
2087
+ const result = await manager.exec(nodeId, `journalctl --no-pager -n 20 --grep='${sid}' 2>/dev/null || grep '${sid}' /var/log/syslog 2>/dev/null | tail -20 || echo "(no history found)"`);
2088
+ return ok(nodeId, result.durationMs, result.stdout, `schedule history ${schedule_id}`);
2089
+ }
2090
+ return fail('invalid action');
2091
+ });
2092
+ // --- Tool 41: omniwire_alert ---
2093
+ server.tool('omniwire_alert', 'Threshold alerting for mesh nodes. Fire when disk/mem/load exceeds threshold or service goes down. Destinations: webhook or local log. Actions: set, remove, list, test, history.', {
2094
+ action: z.enum(['set', 'remove', 'list', 'test', 'history']).describe('Action'),
2095
+ node: z.string().optional().describe('Node to monitor (default: contabo)'),
2096
+ alert_id: z.string().optional().describe('Alert rule identifier'),
2097
+ metric: z.enum(['disk', 'mem', 'load', 'offline', 'service']).optional().describe('Metric to monitor'),
2098
+ threshold: z.number().optional().describe('Threshold value (disk/mem: %, load: float)'),
2099
+ webhook_url: z.string().optional().describe('Webhook URL to POST alert payload to'),
2100
+ }, async ({ action, node, alert_id, metric, threshold, webhook_url }) => {
2101
+ const nodeId = node ?? 'contabo';
2102
+ const ad = '/tmp/.omniwire-alerts';
2103
+ const fd = `${ad}/fired`;
2104
+ if (action === 'list') {
2105
+ const result = await manager.exec(nodeId, `mkdir -p ${ad}; ls ${ad}/*.json 2>/dev/null | while read f; do echo "$(basename $f .json):"; cat "$f" | grep -E '"metric"|"threshold"|"webhook"'; done || echo "(no alerts configured)"`);
2106
+ return ok(nodeId, result.durationMs, result.stdout, 'alert list');
2107
+ }
2108
+ if (action === 'set') {
2109
+ if (!alert_id || !metric)
2110
+ return fail('alert_id and metric required');
2111
+ const aid = alert_id.replace(/'/g, "'\\''");
2112
+ const thresh = threshold ?? (metric === 'load' ? 4 : 90);
2113
+ const rule = JSON.stringify({ id: alert_id, node: nodeId, metric, threshold: thresh, webhook_url: webhook_url ?? null, created: Date.now() });
2114
+ const ruleEsc = rule.replace(/'/g, "'\\''");
2115
+ const checkScript = metric === 'disk'
2116
+ ? `val=$(df / --output=pcent | tail -1 | tr -d ' %'); [ "$val" -gt '${thresh}' ] && echo ALERT`
2117
+ : metric === 'mem'
2118
+ ? `val=$(free | awk '/Mem:/{printf "%.0f", $3/$2*100}'); [ "$val" -gt '${thresh}' ] && echo ALERT`
2119
+ : metric === 'load'
2120
+ ? `val=$(awk '{print $1}' /proc/loadavg); awk "BEGIN{exit ($val > ${thresh}) ? 0 : 1}" && echo ALERT`
2121
+ : metric === 'service'
2122
+ ? `systemctl is-active '${aid}' >/dev/null 2>&1 || echo ALERT`
2123
+ : `ping -c1 -W2 127.0.0.1 >/dev/null 2>&1 || echo ALERT`;
2124
+ const wh = webhook_url ? webhook_url.replace(/'/g, "'\\''") : '';
2125
+ const fireCmd = webhook_url
2126
+ ? `curl -s -X POST '${wh}' -H 'Content-Type: application/json' -d '{"alert":"${aid}","node":"${nodeId}","metric":"${metric}"}' 2>/dev/null`
2127
+ : `mkdir -p ${fd}; echo "$(date -Iseconds) ${aid} ${nodeId} ${metric}" >> ${fd}/events.log`;
2128
+ const cronLine = `* * * * * bash -c '${checkScript.replace(/'/g, "'\\''")}' 2>/dev/null | grep -q ALERT && ${fireCmd}`;
2129
+ const result = await manager.exec(nodeId, `mkdir -p ${ad}; echo '${ruleEsc}' > ${ad}/${aid}.json; (crontab -l 2>/dev/null | grep -v 'omniwire-alert:${aid}'; echo '# omniwire-alert:${aid}'; echo '${cronLine.replace(/'/g, "'\\''")}') | crontab -; echo "alert ${alert_id} set (${metric} threshold=${thresh})"`);
2130
+ return ok(nodeId, result.durationMs, result.stdout, `alert set ${alert_id}`);
2131
+ }
2132
+ if (action === 'remove') {
2133
+ if (!alert_id)
2134
+ return fail('alert_id required');
2135
+ const aid = alert_id.replace(/'/g, "'\\''");
2136
+ const result = await manager.exec(nodeId, `rm -f ${ad}/${aid}.json; crontab -l 2>/dev/null | grep -v 'omniwire-alert:${aid}' | crontab -; echo "alert ${alert_id} removed"`);
2137
+ return ok(nodeId, result.durationMs, result.stdout, `alert remove ${alert_id}`);
2138
+ }
2139
+ if (action === 'test') {
2140
+ if (!alert_id)
2141
+ return fail('alert_id required');
2142
+ const aid = alert_id.replace(/'/g, "'\\''");
2143
+ const ruleResult = await manager.exec(nodeId, `cat ${ad}/${aid}.json 2>/dev/null`);
2144
+ let fireCmd;
2145
+ try {
2146
+ const parsed = JSON.parse(ruleResult.stdout);
2147
+ fireCmd = parsed.webhook_url
2148
+ ? `curl -s -X POST '${parsed.webhook_url}' -H 'Content-Type: application/json' -d '{"alert":"${aid}","node":"${nodeId}","test":true}' && echo "test alert sent"`
2149
+ : `mkdir -p ${fd}; echo "$(date -Iseconds) TEST ${aid} ${nodeId}" >> ${fd}/events.log && echo "test alert written to events.log"`;
2150
+ }
2151
+ catch {
2152
+ fireCmd = `mkdir -p ${fd}; echo "$(date -Iseconds) TEST ${aid} ${nodeId}" >> ${fd}/events.log && echo "test alert written to events.log"`;
2153
+ }
2154
+ const result = await manager.exec(nodeId, fireCmd);
2155
+ return ok(nodeId, result.durationMs, result.stdout, `alert test ${alert_id}`);
2156
+ }
2157
+ if (action === 'history') {
2158
+ const filterPart = alert_id ? `| grep '${alert_id.replace(/'/g, "'\\''")}'` : '';
2159
+ const result = await manager.exec(nodeId, `cat ${fd}/events.log 2>/dev/null ${filterPart} | tail -30 || echo "(no fired alerts)"`);
2160
+ return ok(nodeId, result.durationMs, result.stdout, 'alert history');
2161
+ }
2162
+ return fail('invalid action');
2163
+ });
2164
+ // --- Tool 42: omniwire_log_aggregate ---
2165
+ server.tool('omniwire_log_aggregate', 'Cross-node log search and aggregation. Run grep/journalctl across all nodes in parallel, merge results with node prefix. Actions: search, tail, count.', {
2166
+ action: z.enum(['search', 'tail', 'count']).describe('search=grep pattern, tail=last N lines, count=count matches per node'),
2167
+ pattern: z.string().optional().describe('Search/grep pattern'),
2168
+ nodes: z.array(z.string()).optional().describe('Nodes to search (default: all online)'),
2169
+ source: z.enum(['journalctl', 'syslog', 'file']).optional().describe('Log source (default: journalctl)'),
2170
+ file_path: z.string().optional().describe('Log file path (required when source=file)'),
2171
+ limit: z.number().optional().describe('Max lines per node (default: 50)'),
2172
+ }, async ({ action, pattern, nodes: targetNodes, source, file_path, limit }) => {
2173
+ const logLimit = limit ?? 50;
2174
+ const logSource = source ?? 'journalctl';
2175
+ const nodeIds = targetNodes ?? manager.getOnlineNodes();
2176
+ function buildCmd(act) {
2177
+ if (logSource === 'journalctl') {
2178
+ const gp = pattern ? `--grep='${pattern.replace(/'/g, "'\\''")}' ` : '';
2179
+ if (act === 'count')
2180
+ return `journalctl --no-pager ${gp}-q 2>/dev/null | wc -l`;
2181
+ return `journalctl --no-pager -n ${logLimit} ${gp}2>/dev/null`;
2182
+ }
2183
+ if (logSource === 'syslog') {
2184
+ const lf = '/var/log/syslog';
2185
+ if (act === 'count')
2186
+ return pattern
2187
+ ? `grep -cE '${pattern.replace(/'/g, "'\\''")}' ${lf} 2>/dev/null || echo 0`
2188
+ : `wc -l < ${lf} 2>/dev/null || echo 0`;
2189
+ return pattern
2190
+ ? `grep -E '${pattern.replace(/'/g, "'\\''")}' ${lf} 2>/dev/null | tail -${logLimit}`
2191
+ : `tail -${logLimit} ${lf} 2>/dev/null`;
2192
+ }
2193
+ if (!file_path)
2194
+ return "echo '(file_path required for source=file)'";
2195
+ const fp = file_path.replace(/'/g, "'\\''");
2196
+ if (act === 'count')
2197
+ return pattern
2198
+ ? `grep -cE '${pattern.replace(/'/g, "'\\''")}' '${fp}' 2>/dev/null || echo 0`
2199
+ : `wc -l < '${fp}' 2>/dev/null || echo 0`;
2200
+ return pattern
2201
+ ? `grep -E '${pattern.replace(/'/g, "'\\''")}' '${fp}' 2>/dev/null | tail -${logLimit}`
2202
+ : `tail -${logLimit} '${fp}' 2>/dev/null`;
2203
+ }
2204
+ const cmd = buildCmd(action);
2205
+ const results = await Promise.all(nodeIds.map(async (id) => ({ ...await manager.exec(id, cmd), nodeId: id })));
2206
+ if (action === 'count') {
2207
+ const lines = results.map((r) => `${r.nodeId.padEnd(12)} ${r.stdout.trim() || '0'}`);
2208
+ return okBrief(lines.join('\n'));
2209
+ }
2210
+ return multiResult(results);
2211
+ });
2212
+ // --- Tool 43: omniwire_benchmark ---
2213
+ server.tool('omniwire_benchmark', 'Node performance benchmarking. CPU, memory, disk I/O, and network throughput. Actions: cpu, memory, disk, network, all.', {
2214
+ action: z.enum(['cpu', 'memory', 'disk', 'network', 'all']).describe('"all" runs cpu+memory+disk and returns comparison table across nodes'),
2215
+ node: z.string().optional().describe('Target node (default: all online for cpu/mem/disk/all)'),
2216
+ target_node: z.string().optional().describe('Second node for network test (required for action=network)'),
2217
+ }, async ({ action, node, target_node }) => {
2218
+ const cpuCmd = `sysbench cpu --time=5 run 2>&1 | grep 'events per second' || (dd if=/dev/zero bs=1M count=500 2>/dev/null | md5sum | awk '{print "md5-throughput OK"}')`;
2219
+ const memCmd = "sysbench memory --time=5 run 2>&1 | grep transferred || (dd if=/dev/zero of=/dev/null bs=1M count=1000 2>&1 | grep -E 'MB/s|GB/s|copied')";
2220
+ const diskCmd = "dd if=/dev/zero of=/tmp/.ow-bench bs=1M count=100 oflag=direct 2>&1 | grep -E 'MB/s|GB/s|copied'; rm -f /tmp/.ow-bench";
2221
+ if (action === 'network') {
2222
+ if (!target_node)
2223
+ return fail('target_node required for network benchmark');
2224
+ const srcNode = node ?? 'contabo';
2225
+ const bport = 19876;
2226
+ await manager.exec(target_node, `nc -l -p ${bport} > /dev/null &`);
2227
+ await new Promise((r) => setTimeout(r, 500));
2228
+ const targetInfo = await manager.exec(target_node, "hostname -I | awk '{print $1}'");
2229
+ const targetIp = targetInfo.stdout.trim();
2230
+ if (!targetIp)
2231
+ return fail(`could not resolve IP for ${target_node}`);
2232
+ const sendResult = await manager.exec(srcNode, `dd if=/dev/zero bs=1M count=100 2>/dev/null | nc -w 5 ${targetIp} ${bport} 2>&1 | grep -E 'MB/s|GB/s|copied'; echo "network test: ${srcNode} -> ${target_node}"`);
2233
+ await manager.exec(target_node, `pkill -f 'nc -l -p ${bport}' 2>/dev/null; true`);
2234
+ return ok(srcNode, sendResult.durationMs, sendResult.stdout, `network bench ${srcNode}->${target_node}`);
2235
+ }
2236
+ const targetNodes = node ? [node] : manager.getOnlineNodes();
2237
+ if (action === 'cpu') {
2238
+ const results = await Promise.all(targetNodes.map(async (id) => ({ ...await manager.exec(id, cpuCmd), nodeId: id })));
2239
+ return multiResult(results);
2240
+ }
2241
+ if (action === 'memory') {
2242
+ const results = await Promise.all(targetNodes.map(async (id) => ({ ...await manager.exec(id, memCmd), nodeId: id })));
2243
+ return multiResult(results);
2244
+ }
2245
+ if (action === 'disk') {
2246
+ const results = await Promise.all(targetNodes.map(async (id) => ({ ...await manager.exec(id, diskCmd), nodeId: id })));
2247
+ return multiResult(results);
2248
+ }
2249
+ const allResults = await Promise.all(targetNodes.map(async (id) => {
2250
+ const [cpuR, memR, diskR] = await Promise.all([
2251
+ manager.exec(id, cpuCmd),
2252
+ manager.exec(id, memCmd),
2253
+ manager.exec(id, diskCmd),
2254
+ ]);
2255
+ const cpuVal = cpuR.stdout.match(/[\d.]+ events per second/)?.[0] ?? cpuR.stdout.split('\n')[0]?.slice(0, 35) ?? '--';
2256
+ const memVal = memR.stdout.match(/[\d.]+ \w+B transferred/)?.[0] ?? memR.stdout.split('\n')[0]?.slice(0, 35) ?? '--';
2257
+ const diskVal = diskR.stdout.match(/[\d.]+ \w+B\/s/)?.[0] ?? diskR.stdout.split('\n')[0]?.slice(0, 25) ?? '--';
2258
+ return `${id.padEnd(12)} cpu: ${cpuVal.padEnd(35)} mem: ${memVal.padEnd(35)} disk: ${diskVal}`;
2259
+ }));
2260
+ return okBrief(`benchmark results (cpu / mem / disk)\n${allResults.join('\n')}`);
2261
+ });
2262
+ // --- Tool 34: omniwire_clipboard ---
1364
2263
  server.tool('omniwire_clipboard', 'Copy text between nodes via a shared clipboard buffer.', {
1365
2264
  action: z.enum(['copy', 'paste', 'clear']).describe('Action'),
1366
2265
  content: z.string().optional().describe('Text to copy (for copy action)'),
@@ -1423,7 +2322,7 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1423
2322
  // AGENTIC / A2A / MULTI-AGENT TOOLS
1424
2323
  // =========================================================================
1425
2324
  // --- Tool 33: omniwire_store ---
1426
- server.tool('omniwire_store', 'Key-value store for chaining results across tool calls in the same session. Agents can store intermediate results and retrieve them later. Keys persist until session ends.', {
2325
+ server.tool('omniwire_store', 'Key-value store for chaining results. Auto-persists to CyberBase. On get, checks memory first then CyberBase fallback. Keys survive across sessions via CyberBase.', {
1427
2326
  action: z.enum(['get', 'set', 'delete', 'list', 'clear']).describe('Action'),
1428
2327
  key: z.string().optional().describe('Key name (required for get/set/delete)'),
1429
2328
  value: z.string().optional().describe('Value to store (for set)'),
@@ -1432,24 +2331,37 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1432
2331
  case 'get':
1433
2332
  if (!key)
1434
2333
  return fail('key required');
1435
- return okBrief(resultStore.get(key) ?? '(not found)');
2334
+ // Memory first, CyberBase fallback
2335
+ let val = resultStore.get(key);
2336
+ if (!val) {
2337
+ val = await cbGet('store', key) ?? undefined;
2338
+ if (val)
2339
+ resultStore.set(key, val);
2340
+ }
2341
+ return okBrief(val ?? '(not found)');
1436
2342
  case 'set':
1437
2343
  if (!key || value === undefined)
1438
2344
  return fail('key and value required');
1439
2345
  resultStore.set(key, value);
1440
- return okBrief(`stored ${key} (${value.length} chars)`);
2346
+ cb('store', key, value); // persist to CyberBase
2347
+ return okBrief(`stored ${key} (${value.length} chars) [memory + cyberbase]`);
1441
2348
  case 'delete':
1442
2349
  if (!key)
1443
2350
  return fail('key required');
1444
2351
  resultStore.delete(key);
2352
+ cb('store', key, ''); // mark deleted in CyberBase
1445
2353
  return okBrief(`deleted ${key}`);
1446
- case 'list':
1447
- if (resultStore.size === 0)
1448
- return okBrief('(empty store)');
1449
- return okBrief([...resultStore.entries()].map(([k, v]) => `${k} = ${v.slice(0, 80)}${v.length > 80 ? '...' : ''}`).join('\n'));
2354
+ case 'list': {
2355
+ // Merge memory + CyberBase keys
2356
+ const memKeys = [...resultStore.entries()].map(([k, v]) => `${k} = ${v.slice(0, 80)}${v.length > 80 ? '...' : ''}`);
2357
+ const cbKeys = await cbList('store');
2358
+ const extra = cbKeys.filter(k => !resultStore.has(k)).map(k => `${k} (cyberbase)`);
2359
+ const all = [...memKeys, ...extra];
2360
+ return okBrief(all.length > 0 ? all.join('\n') : '(empty store)');
2361
+ }
1450
2362
  case 'clear':
1451
2363
  resultStore.clear();
1452
- return okBrief('store cleared');
2364
+ return okBrief('memory store cleared (CyberBase entries preserved)');
1453
2365
  }
1454
2366
  });
1455
2367
  // --- Tool 34: omniwire_pipeline ---
@@ -1578,7 +2490,7 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1578
2490
  });
1579
2491
  // --- Tool 37: omniwire_agent_task ---
1580
2492
  server.tool('omniwire_agent_task', 'Dispatch a task to a specific node for background execution and retrieve results later. Creates a task file on the node, runs it in background, and provides a task ID for polling. Designed for A2A (agent-to-agent) workflows where one agent dispatches work and another retrieves results.', {
1581
- action: z.enum(['dispatch', 'status', 'result', 'list', 'cancel']).describe('Action'),
2493
+ action: z.enum(['dispatch', 'status', 'result', 'list', 'cancel', 'dlq']).describe('Action. dlq=list tasks that failed (non-zero exit).'),
1582
2494
  node: z.string().optional().describe('Node (default: contabo)'),
1583
2495
  command: z.string().optional().describe('Command to dispatch (for dispatch action)'),
1584
2496
  task_id: z.string().optional().describe('Task ID (for status/result/cancel)'),
@@ -1591,7 +2503,9 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1591
2503
  return fail('command required');
1592
2504
  const id = `ow-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1593
2505
  const escaped = command.replace(/'/g, "'\\''");
1594
- const script = `mkdir -p ${taskDir} && echo 'running' > ${taskDir}/${id}.status && echo '${label ?? command.slice(0, 60)}' > ${taskDir}/${id}.label && (bash -c '${escaped}' > ${taskDir}/${id}.stdout 2> ${taskDir}/${id}.stderr; echo $? > ${taskDir}/${id}.exit; echo 'done' > ${taskDir}/${id}.status) &`;
2506
+ // On non-zero exit, copy task files to DLQ for later inspection
2507
+ const dlqCmd = `mkdir -p ${taskDir}/dlq && cp ${taskDir}/${id}.* ${taskDir}/dlq/ 2>/dev/null`;
2508
+ const script = `mkdir -p ${taskDir} && echo 'running' > ${taskDir}/${id}.status && echo '${label ?? command.slice(0, 60)}' > ${taskDir}/${id}.label && (bash -c '${escaped}' > ${taskDir}/${id}.stdout 2> ${taskDir}/${id}.stderr; _rc=$?; echo $_rc > ${taskDir}/${id}.exit; if [ $_rc -ne 0 ]; then ${dlqCmd}; fi; echo 'done' > ${taskDir}/${id}.status) &`;
1595
2509
  const result = await manager.exec(nodeId, script);
1596
2510
  return result.code === 0
1597
2511
  ? okBrief(`${nodeId} task dispatched: ${id}`)
@@ -1613,6 +2527,10 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1613
2527
  await manager.exec(nodeId, `echo 'cancelled' > ${taskDir}/${task_id}.status`);
1614
2528
  return okBrief(`${nodeId} ${task_id} cancelled`);
1615
2529
  }
2530
+ if (action === 'dlq') {
2531
+ const result = await manager.exec(nodeId, `for f in ${taskDir}/dlq/*.status 2>/dev/null; do [ -f "$f" ] || continue; id=$(basename "$f" .status); rc=$(cat ${taskDir}/dlq/$id.exit 2>/dev/null || echo '?'); lbl=$(cat ${taskDir}/dlq/$id.label 2>/dev/null); echo "$id exit=$rc $lbl"; done 2>/dev/null | tail -20 || echo '(empty DLQ)'`);
2532
+ return ok(nodeId, result.durationMs, result.stdout || '(empty DLQ)', 'task DLQ');
2533
+ }
1616
2534
  return fail('invalid action/params');
1617
2535
  });
1618
2536
  // --- Tool 38: omniwire_a2a_message ---
@@ -1623,15 +2541,25 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1623
2541
  message: z.string().optional().describe('Message content (for send). Can be JSON.'),
1624
2542
  sender: z.string().optional().describe('Sender agent name (for send)'),
1625
2543
  count: z.number().optional().describe('Number of messages to receive (default: 1). Messages are dequeued on receive.'),
1626
- }, async ({ action, channel, node, message, sender, count }) => {
2544
+ schema: z.enum(['text', 'json', 'any']).optional().describe('Message format validation. json=must be valid JSON, text=plain string, any=no validation (default).'),
2545
+ }, async ({ action, channel, node, message, sender, count, schema }) => {
1627
2546
  const nodeId = node ?? 'contabo';
1628
2547
  const queueDir = '/tmp/.omniwire-a2a';
1629
2548
  if (action === 'send') {
1630
2549
  if (!channel || !message)
1631
2550
  return fail('channel and message required');
2551
+ // Schema validation
2552
+ if (schema === 'json') {
2553
+ try {
2554
+ JSON.parse(message);
2555
+ }
2556
+ catch {
2557
+ return fail('schema=json but message is not valid JSON');
2558
+ }
2559
+ }
1632
2560
  const ts = Date.now();
1633
2561
  const id = `${ts}-${Math.random().toString(36).slice(2, 6)}`;
1634
- const payload = JSON.stringify({ id, ts, sender: sender ?? 'unknown', message });
2562
+ const payload = JSON.stringify({ id, ts, sender: sender ?? 'unknown', schema: schema ?? 'any', message });
1635
2563
  const escaped = payload.replace(/'/g, "'\\''");
1636
2564
  const result = await manager.exec(nodeId, `mkdir -p ${queueDir}/${channel} && echo '${escaped}' >> ${queueDir}/${channel}/queue`);
1637
2565
  return result.code === 0
@@ -1722,7 +2650,8 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1722
2650
  source: z.string().optional().describe('Source agent name (for emit)'),
1723
2651
  since: z.string().optional().describe('Only return events after this timestamp (epoch ms) for poll'),
1724
2652
  limit: z.number().optional().describe('Max events to return (default: 10)'),
1725
- }, async ({ action, topic, node, data, source, since, limit }) => {
2653
+ filter: z.string().optional().describe('Regex filter applied to event data during poll. Only matching events returned.'),
2654
+ }, async ({ action, topic, node, data, source, since, limit, filter }) => {
1726
2655
  const nodeId = node ?? 'contabo';
1727
2656
  const eventDir = '/tmp/.omniwire-events';
1728
2657
  const n = limit ?? 10;
@@ -1748,6 +2677,11 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1748
2677
  else {
1749
2678
  cmd = `tail -${n} ${eventDir}/events.log 2>/dev/null || echo '(no events)'`;
1750
2679
  }
2680
+ // Apply regex filter on event data if specified
2681
+ if (filter) {
2682
+ const escapedFilter = filter.replace(/'/g, "'\\''");
2683
+ cmd = `(${cmd}) | grep -E '${escapedFilter}' 2>/dev/null`;
2684
+ }
1751
2685
  const result = await manager.exec(nodeId, cmd);
1752
2686
  return ok(nodeId, result.durationMs, result.stdout || '(no events)', `events ${topic ?? 'all'}`);
1753
2687
  }
@@ -1892,7 +2826,8 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1892
2826
  const entry = JSON.stringify({ ts: Date.now(), author: author ?? 'agent', content });
1893
2827
  const escaped = entry.replace(/'/g, "'\\''");
1894
2828
  await manager.exec(nodeId, `mkdir -p ${bbDir} && echo '${escaped}' >> ${bbDir}/${topic}.log`);
1895
- return okBrief(`posted to ${topic} (${content.length} chars)`);
2829
+ cb('blackboard', `${topic}:${Date.now()}`, entry); // persist to CyberBase
2830
+ return okBrief(`posted to ${topic} (${content.length} chars) [node + cyberbase]`);
1896
2831
  }
1897
2832
  if (action === 'read') {
1898
2833
  if (!topic)
@@ -1983,6 +2918,334 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1983
2918
  const results = await manager.execAll(cmd);
1984
2919
  return multiResult(results);
1985
2920
  });
2921
+ // --- Tool 46: omniwire_snippet ---
2922
+ server.tool('omniwire_snippet', 'Saved command templates on a node. Save reusable snippets with {{var}} placeholders, then run them with variable substitution.', {
2923
+ action: z.enum(['save', 'run', 'list', 'delete']).describe('save=store template, run=execute with var substitution, list=show all, delete=remove'),
2924
+ node: z.string().describe('Target node'),
2925
+ name: z.string().optional().describe('Snippet name (required for save/run/delete)'),
2926
+ command: z.string().optional().describe('Command template for save. Use {{var}} for placeholders.'),
2927
+ vars: z.string().optional().describe('Key=value pairs for run substitution, space-separated. E.g. "host=1.2.3.4 port=8080"'),
2928
+ }, async ({ action, node, name, command, vars }) => {
2929
+ const snippetDir = '/tmp/.omniwire-snippets';
2930
+ if (action === 'list') {
2931
+ const result = await manager.exec(node, `mkdir -p ${snippetDir} && ls ${snippetDir}/ 2>/dev/null || echo '(no snippets)'`);
2932
+ return ok(node, result.durationMs, result.stdout, 'snippets');
2933
+ }
2934
+ if (action === 'save') {
2935
+ if (!name || !command)
2936
+ return fail('name and command required for save');
2937
+ const escaped = command.replace(/'/g, "'\\''");
2938
+ const result = await manager.exec(node, `mkdir -p ${snippetDir} && echo '${escaped}' > ${snippetDir}/${name}.sh && chmod +x ${snippetDir}/${name}.sh`);
2939
+ return result.code === 0
2940
+ ? okBrief(`${node} snippet saved: ${name}`)
2941
+ : fail(`${node} snippet save: ${result.stderr}`);
2942
+ }
2943
+ if (action === 'run') {
2944
+ if (!name)
2945
+ return fail('name required for run');
2946
+ const readResult = await manager.exec(node, `cat ${snippetDir}/${name}.sh 2>/dev/null`);
2947
+ if (readResult.code !== 0)
2948
+ return fail(`snippet ${name} not found`);
2949
+ let template = readResult.stdout.trim();
2950
+ if (vars) {
2951
+ for (const pair of vars.split(/\s+/)) {
2952
+ const eqIdx = pair.indexOf('=');
2953
+ if (eqIdx < 0)
2954
+ continue;
2955
+ const k = pair.slice(0, eqIdx);
2956
+ const v = pair.slice(eqIdx + 1);
2957
+ template = template.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), v);
2958
+ }
2959
+ }
2960
+ const result = await manager.exec(node, template);
2961
+ return ok(node, result.durationMs, fmtExecOutput(result, 30), `snippet:${name}`);
2962
+ }
2963
+ if (action === 'delete') {
2964
+ if (!name)
2965
+ return fail('name required for delete');
2966
+ const result = await manager.exec(node, `rm -f ${snippetDir}/${name}.sh`);
2967
+ return result.code === 0
2968
+ ? okBrief(`${node} snippet deleted: ${name}`)
2969
+ : fail(`${node} snippet delete: ${result.stderr}`);
2970
+ }
2971
+ return fail('invalid action');
2972
+ });
2973
+ // --- Tool 47: omniwire_alias ---
2974
+ server.tool('omniwire_alias', 'In-session command shortcuts. Set short aliases for long commands, then run them by alias name on any node.', {
2975
+ action: z.enum(['set', 'get', 'list', 'delete', 'run']).describe('set=define alias, get=show command, list=all aliases, delete=remove, run=execute alias on node'),
2976
+ name: z.string().optional().describe('Alias name (required for set/get/delete/run)'),
2977
+ command: z.string().optional().describe('Command to alias (for set). Supports {{key}} interpolation.'),
2978
+ node: z.string().optional().describe('Node to run alias on (for run action). Default: contabo.'),
2979
+ }, async ({ action, name, command, node }) => {
2980
+ if (action === 'list') {
2981
+ if (aliasStore.size === 0)
2982
+ return okBrief('(no aliases)');
2983
+ return okBrief([...aliasStore.entries()].map(([k, v]) => `${k} = ${v}`).join('\n'));
2984
+ }
2985
+ if (action === 'set') {
2986
+ if (!name || !command)
2987
+ return fail('name and command required');
2988
+ aliasStore.set(name, command);
2989
+ return okBrief(`alias set: ${name} = ${command.slice(0, 80)}`);
2990
+ }
2991
+ if (action === 'get') {
2992
+ if (!name)
2993
+ return fail('name required');
2994
+ const cmd = aliasStore.get(name);
2995
+ return cmd ? okBrief(`${name} = ${cmd}`) : fail(`alias ${name} not found`);
2996
+ }
2997
+ if (action === 'delete') {
2998
+ if (!name)
2999
+ return fail('name required');
3000
+ aliasStore.delete(name);
3001
+ return okBrief(`alias ${name} deleted`);
3002
+ }
3003
+ if (action === 'run') {
3004
+ if (!name)
3005
+ return fail('name required');
3006
+ const template = aliasStore.get(name);
3007
+ if (!template)
3008
+ return fail(`alias ${name} not found`);
3009
+ const nodeId = node ?? 'contabo';
3010
+ const resolved = template.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
3011
+ const result = await manager.exec(nodeId, resolved);
3012
+ return ok(nodeId, result.durationMs, fmtExecOutput(result, 30), `alias:${name}`);
3013
+ }
3014
+ return fail('invalid action');
3015
+ });
3016
+ // --- Tool 48: omniwire_trace ---
3017
+ server.tool('omniwire_trace', 'Distributed tracing across mesh nodes. Start a trace, record spans with timing, view a waterfall breakdown of where time was spent.', {
3018
+ action: z.enum(['start', 'stop', 'view']).describe('start=create trace, stop=mark complete, view=show all spans with waterfall'),
3019
+ trace_id: z.string().optional().describe('Trace ID (required for stop/view). Returned by start.'),
3020
+ node: z.string().optional().describe('Node where the span ran (for start)'),
3021
+ command: z.string().optional().describe('Command/label for this trace (for start)'),
3022
+ }, async ({ action, trace_id, node, command }) => {
3023
+ if (action === 'start') {
3024
+ const id = `tr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
3025
+ const nodeId = node ?? 'local';
3026
+ const startMs = Date.now();
3027
+ const span = { node: nodeId, command: command ?? 'trace-start', startMs, endMs: startMs, result: 'started' };
3028
+ traceStore.set(id, { spans: [span], startMs, done: false });
3029
+ return okBrief(`trace started: ${id}`);
3030
+ }
3031
+ if (action === 'stop') {
3032
+ if (!trace_id)
3033
+ return fail('trace_id required');
3034
+ const trace = traceStore.get(trace_id);
3035
+ if (!trace)
3036
+ return fail(`trace ${trace_id} not found`);
3037
+ traceStore.set(trace_id, { ...trace, done: true });
3038
+ return okBrief(`trace ${trace_id} stopped (${t(Date.now() - trace.startMs)} total)`);
3039
+ }
3040
+ if (action === 'view') {
3041
+ if (!trace_id) {
3042
+ if (traceStore.size === 0)
3043
+ return okBrief('(no traces)');
3044
+ const traceList = [...traceStore.entries()].map(([id, tr]) => `${id} ${tr.done ? 'done' : 'running'} ${tr.spans.length} spans ${t(Date.now() - tr.startMs)}`);
3045
+ return okBrief(traceList.join('\n'));
3046
+ }
3047
+ const trace = traceStore.get(trace_id);
3048
+ if (!trace)
3049
+ return fail(`trace ${trace_id} not found`);
3050
+ const lastSpan = trace.spans[trace.spans.length - 1];
3051
+ const totalMs = (trace.done && lastSpan ? lastSpan.endMs : Date.now()) - trace.startMs || 1;
3052
+ const lines = [`trace ${trace_id} ${trace.done ? 'done' : 'running'} total: ${t(totalMs)}`, ''];
3053
+ for (const span of trace.spans) {
3054
+ const spanMs = span.endMs - span.startMs;
3055
+ const startOffset = span.startMs - trace.startMs;
3056
+ const barWidth = Math.max(1, Math.round((spanMs / totalMs) * 40));
3057
+ const padLeft = Math.round((startOffset / totalMs) * 40);
3058
+ const bar = ' '.repeat(padLeft) + '='.repeat(barWidth);
3059
+ lines.push(`${span.node.padEnd(12)} ${t(spanMs).padStart(7)} |${bar}| ${span.command.slice(0, 50)}`);
3060
+ }
3061
+ return okBrief(lines.join('\n'));
3062
+ }
3063
+ return fail('invalid action');
3064
+ });
3065
+ // --- Tool 49: omniwire_doctor ---
3066
+ server.tool('omniwire_doctor', 'Full health diagnostic for mesh nodes. Checks SSH connectivity, disk, memory, load, Docker, nftables, WireGuard, required tools, OmniWire version, and CyberBase reachability.', {
3067
+ node: z.string().optional().describe('Target node id. Omit to check all online nodes.'),
3068
+ }, async ({ node }) => {
3069
+ const diagnosticCmd = [
3070
+ `disk=$(df / --output=pcent | tail -1 | tr -d ' %'); [ "\${disk}" -lt 80 ] && echo "PASS disk \${disk}%" || { [ "\${disk}" -lt 90 ] && echo "WARN disk \${disk}%" || echo "FAIL disk \${disk}%"; }`,
3071
+ `mem=$(free | awk '/Mem:/{printf "%.0f", $3/$2*100}'); [ "\${mem}" -lt 85 ] && echo "PASS mem \${mem}%" || { [ "\${mem}" -lt 95 ] && echo "WARN mem \${mem}%" || echo "FAIL mem \${mem}%"; }`,
3072
+ `load=$(cat /proc/loadavg | awk '{print $1}'); norm=$(echo "$load" | awk '{printf "%.1f", $1}'); echo "$norm" | awk '{if($1<1.5) print "PASS load " $1; else if($1<4) print "WARN load " $1; else print "FAIL load " $1}'`,
3073
+ `docker info >/dev/null 2>&1 && echo "PASS docker running" || echo "WARN docker not running"`,
3074
+ `nft list tables >/dev/null 2>&1 && echo "PASS nftables loaded" || echo "WARN nftables not loaded"`,
3075
+ `ip link show wg0 >/dev/null 2>&1 && echo "PASS wireguard wg0 up" || echo "WARN wireguard wg0 not found"`,
3076
+ `for tool in curl tar gzip lz4 nc; do command -v $tool >/dev/null 2>&1 && echo "PASS tool:$tool" || echo "FAIL tool:$tool missing"; done`,
3077
+ `omniwire --version >/dev/null 2>&1 && echo "PASS omniwire installed" || echo "WARN omniwire binary not in PATH"`,
3078
+ `timeout 3 bash -c 'echo "" | nc -w2 10.10.0.1 5432' 2>/dev/null && echo "PASS cyberbase reachable" || echo "WARN cyberbase 10.10.0.1:5432 unreachable"`,
3079
+ ].join('; ');
3080
+ const targetNodes = node ? [node] : manager.getOnlineNodes();
3081
+ const results = await Promise.all(targetNodes.map(async (n) => {
3082
+ const r = await manager.exec(n, diagnosticCmd);
3083
+ return { ...r, nodeId: n };
3084
+ }));
3085
+ const parts = results.map((r) => {
3086
+ if (r.code === -1)
3087
+ return `-- ${r.nodeId}\nFAIL ssh connectivity`;
3088
+ const lines = r.stdout.split('\n').filter(Boolean);
3089
+ const pass = lines.filter((l) => l.startsWith('PASS')).length;
3090
+ const warn = lines.filter((l) => l.startsWith('WARN')).length;
3091
+ const failCount = lines.filter((l) => l.startsWith('FAIL')).length;
3092
+ return `-- ${r.nodeId} pass=${pass} warn=${warn} fail=${failCount} ${t(r.durationMs)}\n${lines.join('\n')}`;
3093
+ });
3094
+ return okBrief(trim(parts.join('\n\n')));
3095
+ });
3096
+ // --- Tool 50: omniwire_metrics ---
3097
+ server.tool('omniwire_metrics', 'Collect and export Prometheus-compatible metrics from mesh nodes. Scrape returns current values; export formats as Prometheus text exposition.', {
3098
+ action: z.enum(['scrape', 'export']).describe('scrape=collect current metrics, export=Prometheus text format'),
3099
+ node: z.string().optional().describe('Specific node to scrape (default: all online nodes)'),
3100
+ }, async ({ action, node }) => {
3101
+ const metricsCmd = [
3102
+ `echo "uptime_seconds=$(cat /proc/uptime | awk '{print int($1)}')"`,
3103
+ `echo "mem_used_pct=$(free | awk '/Mem:/{printf \"%.1f\", $3/$2*100}')"`,
3104
+ `echo "disk_used_pct=$(df / --output=pcent | tail -1 | tr -d ' %')"`,
3105
+ `echo "load_1m=$(cat /proc/loadavg | awk '{print $1}')"`,
3106
+ `echo "tcp_connections=$(ss -s | awk '/TCP:/{print $2}')"`,
3107
+ `echo "docker_containers=$(docker ps -q 2>/dev/null | wc -l | tr -d ' ')"`,
3108
+ ].join('; ');
3109
+ const parseMetrics = (raw) => Object.fromEntries(raw.split('\n').filter(Boolean).map((l) => {
3110
+ const idx = l.indexOf('=');
3111
+ return [l.slice(0, idx), l.slice(idx + 1)];
3112
+ }));
3113
+ const targetNodes = node ? [node] : manager.getOnlineNodes();
3114
+ const nodeResults = await Promise.all(targetNodes.map(async (n) => {
3115
+ const start = Date.now();
3116
+ const r = await manager.exec(n, metricsCmd);
3117
+ return { nodeId: n, raw: r.stdout, latencyMs: Date.now() - start, ok: r.code === 0 };
3118
+ }));
3119
+ if (action === 'scrape') {
3120
+ const parts = nodeResults.map((r) => {
3121
+ if (!r.ok)
3122
+ return `-- ${r.nodeId} OFFLINE`;
3123
+ const m = parseMetrics(r.raw);
3124
+ return `-- ${r.nodeId} lat=${r.latencyMs}ms\n` +
3125
+ ` mem=${m['mem_used_pct'] ?? '--'}% disk=${m['disk_used_pct'] ?? '--'}% ` +
3126
+ `load=${m['load_1m'] ?? '--'} tcp=${m['tcp_connections'] ?? '--'} ` +
3127
+ `docker=${m['docker_containers'] ?? '--'} uptime=${m['uptime_seconds'] ?? '--'}s`;
3128
+ });
3129
+ return okBrief(parts.join('\n'));
3130
+ }
3131
+ // Prometheus text exposition format
3132
+ const promLines = [
3133
+ '# HELP omniwire_node_latency_ms SSH round-trip latency in milliseconds',
3134
+ '# TYPE omniwire_node_latency_ms gauge',
3135
+ ...nodeResults.map((r) => `omniwire_node_latency_ms{node="${r.nodeId}"} ${r.latencyMs}`),
3136
+ ];
3137
+ const metricDefs = [
3138
+ { key: 'mem_used_pct', name: 'omniwire_node_mem_used_pct', help: 'Memory used percentage', metricType: 'gauge' },
3139
+ { key: 'disk_used_pct', name: 'omniwire_node_disk_used_pct', help: 'Disk used percentage on /', metricType: 'gauge' },
3140
+ { key: 'load_1m', name: 'omniwire_node_load_1m', help: '1-minute load average', metricType: 'gauge' },
3141
+ { key: 'tcp_connections', name: 'omniwire_node_tcp_connections', help: 'Total TCP connections', metricType: 'gauge' },
3142
+ { key: 'docker_containers', name: 'omniwire_node_docker_containers', help: 'Running Docker containers', metricType: 'gauge' },
3143
+ { key: 'uptime_seconds', name: 'omniwire_node_uptime_seconds', help: 'Node uptime in seconds', metricType: 'counter' },
3144
+ ];
3145
+ for (const def of metricDefs) {
3146
+ promLines.push(`# HELP ${def.name} ${def.help}`, `# TYPE ${def.name} ${def.metricType}`);
3147
+ for (const r of nodeResults) {
3148
+ if (!r.ok)
3149
+ continue;
3150
+ const val = parseMetrics(r.raw)[def.key];
3151
+ if (val !== undefined)
3152
+ promLines.push(`${def.name}{node="${r.nodeId}"} ${val}`);
3153
+ }
3154
+ }
3155
+ return okBrief(promLines.join('\n'));
3156
+ });
3157
+ // --- Tool 51: omniwire_audit ---
3158
+ server.tool('omniwire_audit', 'View and search the command audit log. All omniwire_exec calls are automatically logged. Supports viewing recent entries, filtering, and computing stats.', {
3159
+ action: z.enum(['view', 'search', 'clear', 'stats']).describe('view=last N entries, search=filter by node/tool/pattern, clear=wipe log, stats=count/duration/error rate'),
3160
+ limit: z.number().optional().describe('Number of entries to show (default 50)'),
3161
+ node_filter: z.string().optional().describe('Filter by node name'),
3162
+ pattern: z.string().optional().describe('Regex pattern to match against command string'),
3163
+ }, async ({ action, limit, node_filter, pattern }) => {
3164
+ if (action === 'clear') {
3165
+ auditLog.length = 0;
3166
+ return okBrief('audit log cleared');
3167
+ }
3168
+ if (action === 'stats') {
3169
+ if (auditLog.length === 0)
3170
+ return okBrief('audit log empty');
3171
+ const byNode = new Map();
3172
+ for (const entry of auditLog) {
3173
+ const s = byNode.get(entry.node) ?? { count: 0, errors: 0, totalMs: 0 };
3174
+ s.count++;
3175
+ if (entry.code !== 0)
3176
+ s.errors++;
3177
+ s.totalMs += entry.durationMs;
3178
+ byNode.set(entry.node, s);
3179
+ }
3180
+ const statLines = [`audit log: ${auditLog.length} entries`, ''];
3181
+ for (const [n, s] of [...byNode.entries()].sort((a, b) => b[1].count - a[1].count)) {
3182
+ const errRate = ((s.errors / s.count) * 100).toFixed(1);
3183
+ const avgMs = (s.totalMs / s.count).toFixed(0);
3184
+ statLines.push(`${n.padEnd(12)} calls=${s.count} errors=${s.errors} (${errRate}%) avg=${avgMs}ms`);
3185
+ }
3186
+ return okBrief(statLines.join('\n'));
3187
+ }
3188
+ let entries = [...auditLog];
3189
+ if (node_filter)
3190
+ entries = entries.filter((e) => e.node === node_filter);
3191
+ if (pattern) {
3192
+ const regex = new RegExp(pattern, 'i');
3193
+ entries = entries.filter((e) => regex.test(e.command));
3194
+ }
3195
+ const n = limit ?? 50;
3196
+ const slice = entries.slice(-n);
3197
+ if (slice.length === 0)
3198
+ return okBrief('(no entries)');
3199
+ const entryLines = slice.map((e) => {
3200
+ const ts = new Date(e.ts).toISOString().slice(11, 19);
3201
+ const status = e.code === 0 ? 'ok' : `exit ${e.code}`;
3202
+ return `${ts} ${e.node.padEnd(10)} ${e.tool.padEnd(6)} ${status.padEnd(8)} ${t(e.durationMs).padStart(6)} ${e.command.slice(0, 60)}`;
3203
+ });
3204
+ return okBrief(entryLines.join('\n'));
3205
+ });
3206
+ // --- Tool 52: omniwire_plugin ---
3207
+ server.tool('omniwire_plugin', 'Plugin system loader. Scan and inspect JS plugin files in /etc/omniwire/plugins/ or ~/.omniwire/plugins/ on any node.', {
3208
+ action: z.enum(['list', 'load', 'unload', 'info']).describe('list=scan plugin dirs, load=mark plugin active (future), unload=mark inactive, info=show plugin header'),
3209
+ node: z.string().optional().describe('Target node (default: contabo)'),
3210
+ plugin_name: z.string().optional().describe('Plugin file name without .js extension (for load/unload/info)'),
3211
+ }, async ({ action, node, plugin_name }) => {
3212
+ const nodeId = node ?? 'contabo';
3213
+ const pluginDirs = ['/etc/omniwire/plugins', '~/.omniwire/plugins'];
3214
+ if (action === 'list') {
3215
+ const scanCmd = pluginDirs
3216
+ .map((dir) => `[ -d "${dir}" ] && for f in "${dir}"/*.js; do [ -f "$f" ] || continue; name=$(basename "$f" .js); desc=$(head -1 "$f" | sed 's|^// *||;s|^/\\* *||;s| *\\*/$||'); echo "$name $dir $desc"; done`)
3217
+ .join('; ');
3218
+ const result = await manager.exec(nodeId, `(${scanCmd}) 2>/dev/null || echo "(no plugins found)"`);
3219
+ return ok(nodeId, result.durationMs, result.stdout || '(no plugins found)', 'plugins');
3220
+ }
3221
+ if (action === 'info') {
3222
+ if (!plugin_name)
3223
+ return fail('plugin_name required');
3224
+ const findCmd = pluginDirs.map((dir) => `[ -f "${dir}/${plugin_name}.js" ] && echo "${dir}/${plugin_name}.js"`).join('; ');
3225
+ const pathResult = await manager.exec(nodeId, `(${findCmd}) 2>/dev/null | head -1`);
3226
+ const pluginPath = pathResult.stdout.trim();
3227
+ if (!pluginPath)
3228
+ return fail(`plugin ${plugin_name} not found in plugin dirs`);
3229
+ const result = await manager.exec(nodeId, `head -20 "${pluginPath}"`);
3230
+ return ok(nodeId, result.durationMs, result.stdout, `plugin:${plugin_name}`);
3231
+ }
3232
+ if (action === 'load') {
3233
+ if (!plugin_name)
3234
+ return fail('plugin_name required');
3235
+ const findCmd = pluginDirs.map((dir) => `[ -f "${dir}/${plugin_name}.js" ] && echo "${dir}/${plugin_name}.js"`).join('; ');
3236
+ const pathResult = await manager.exec(nodeId, `(${findCmd}) 2>/dev/null | head -1`);
3237
+ const pluginPath = pathResult.stdout.trim();
3238
+ if (!pluginPath)
3239
+ return fail(`plugin ${plugin_name} not found`);
3240
+ return okBrief(`plugin ${plugin_name} located at ${pluginPath} — dynamic load pending runtime support`);
3241
+ }
3242
+ if (action === 'unload') {
3243
+ if (!plugin_name)
3244
+ return fail('plugin_name required');
3245
+ return okBrief(`plugin ${plugin_name} unloaded (in-memory only — no persistent state)`);
3246
+ }
3247
+ return fail('invalid action');
3248
+ });
1986
3249
  return server;
1987
3250
  }
1988
3251
  //# sourceMappingURL=server.js.map