omniwire 3.1.2 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
- "lastCheck": 1774750607929,
2
+ "lastCheck": 1774756238937,
3
3
  "lastVersion": "3.0.1",
4
- "autoUpdateEnabled": false,
4
+ "autoUpdateEnabled": true,
5
5
  "source": "auto",
6
6
  "checkIntervalMs": 3600000
7
7
  }
@@ -6,6 +6,8 @@
6
6
  // authenticated, encrypted SSH channels. The "exec" references below are SSH2 methods.
7
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
  import { z } from 'zod';
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
9
11
  import { ShellManager, kernelExec } from '../nodes/shell.js';
10
12
  import { RealtimeChannel } from '../nodes/realtime.js';
11
13
  import { TunnelManager } from '../nodes/tunnel.js';
@@ -199,8 +201,11 @@ function cbRecordFail(err) { cbFailCount++; cbLastError = err; }
199
201
  function sqlEscape(val) {
200
202
  return val.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\0/g, '');
201
203
  }
202
- /** Fire-and-forget write to CyberBase. Never blocks, never throws. */
204
+ /** Fire-and-forget write to CyberBase + Obsidian vault + Canvas. Never blocks, never throws. */
203
205
  function cb(category, key, value) {
206
+ // Sync to Obsidian vault + Canvas mindmap (local, synchronous, best-effort)
207
+ syncVault(category, key, value);
208
+ // Sync to CyberBase PostgreSQL (remote, async, queued)
204
209
  if (!cbManager || cbCircuitOpen())
205
210
  return;
206
211
  const valEsc = sqlEscape(value).slice(0, 50000);
@@ -239,6 +244,199 @@ async function drainCb() {
239
244
  else
240
245
  cbDraining = false;
241
246
  }
247
+ // -- Obsidian + Canvas auto-sync ------------------------------------------------
248
+ // Mirrors CyberBase writes to local Obsidian vault + Canvas mindmap.
249
+ // Vault path is resolved at startup; if it doesn't exist, sync is silently skipped.
250
+ const VAULT_ROOT = join(process.env.USERPROFILE ?? process.env.HOME ?? '', 'Documents', 'BuisnessProjects', 'CyberBase');
251
+ const CANVAS_PATH = join(VAULT_ROOT, 'CyberBase MindMap.canvas');
252
+ const vaultExists = existsSync(VAULT_ROOT);
253
+ /** Map CyberBase category → Obsidian vault subfolder */
254
+ function vaultFolder(category) {
255
+ const cat = category.toLowerCase();
256
+ if (cat.startsWith('project'))
257
+ return 'projects';
258
+ if (cat.startsWith('infra') || cat.startsWith('tool') || cat.startsWith('mesh'))
259
+ return 'infrastructure';
260
+ if (cat.startsWith('vuln') || cat.startsWith('security') || cat.startsWith('cve'))
261
+ return 'knowledge/security-kb';
262
+ if (cat.startsWith('cred'))
263
+ return 'credentials';
264
+ if (cat.startsWith('system') || cat.startsWith('rule'))
265
+ return 'system';
266
+ if (cat.startsWith('log'))
267
+ return 'logs';
268
+ if (cat.startsWith('sync'))
269
+ return 'sync';
270
+ if (cat.startsWith('note') || cat.startsWith('memo'))
271
+ return 'memory';
272
+ return 'knowledge';
273
+ }
274
+ /** Sanitize a key into a valid filename */
275
+ function sanitizeFilename(key) {
276
+ return key.replace(/[<>:"/\\|?*]/g, '-').replace(/^\.+/, '').slice(0, 120);
277
+ }
278
+ /** Auto-sync a knowledge entry to Obsidian vault as a .md file */
279
+ function syncObsidian(category, key, value) {
280
+ if (!vaultExists)
281
+ return;
282
+ try {
283
+ const folder = join(VAULT_ROOT, vaultFolder(category));
284
+ if (!existsSync(folder))
285
+ mkdirSync(folder, { recursive: true });
286
+ const filename = sanitizeFilename(key) + '.md';
287
+ const filepath = join(folder, filename);
288
+ const frontmatter = `---\nsource: omniwire\ncategory: ${category}\nkey: ${key}\nupdated: ${new Date().toISOString()}\n---\n\n`;
289
+ // If value looks like markdown, write as-is; otherwise wrap in code block
290
+ const body = value.includes('\n') && (value.includes('#') || value.includes('|') || value.includes('- '))
291
+ ? value
292
+ : `\`\`\`\n${value}\n\`\`\``;
293
+ writeFileSync(filepath, frontmatter + body, 'utf-8');
294
+ }
295
+ catch { /* vault sync is best-effort */ }
296
+ }
297
+ /** Find a non-overlapping position for a new canvas node using grid placement */
298
+ function findFreeCanvasPosition(existingNodes, width, height) {
299
+ const GRID_X = 500; // horizontal spacing
300
+ const GRID_Y = 400; // vertical spacing
301
+ const PADDING = 80; // minimum gap between nodes
302
+ const MAX_COLS = 6;
303
+ // Check if a position collides with any existing node
304
+ const collides = (x, y) => {
305
+ for (const n of existingNodes) {
306
+ const overlap = x < n.x + n.w + PADDING &&
307
+ x + width + PADDING > n.x &&
308
+ y < n.y + n.h + PADDING &&
309
+ y + height + PADDING > n.y;
310
+ if (overlap)
311
+ return true;
312
+ }
313
+ return false;
314
+ };
315
+ // Find center of existing nodes to place new ones nearby
316
+ let cx = 0;
317
+ let cy = 0;
318
+ if (existingNodes.length > 0) {
319
+ for (const n of existingNodes) {
320
+ cx += n.x;
321
+ cy += n.y;
322
+ }
323
+ cx = Math.round(cx / existingNodes.length);
324
+ cy = Math.round(cy / existingNodes.length);
325
+ }
326
+ // Spiral outward from center to find free spot
327
+ for (let ring = 0; ring < 20; ring++) {
328
+ for (let col = -ring; col <= ring; col++) {
329
+ for (let row = -ring; row <= ring; row++) {
330
+ if (Math.abs(col) !== ring && Math.abs(row) !== ring)
331
+ continue; // only edges of ring
332
+ const x = cx + col * GRID_X;
333
+ const y = cy + row * GRID_Y;
334
+ if (!collides(x, y))
335
+ return { x, y };
336
+ }
337
+ }
338
+ }
339
+ // Fallback: far right of canvas
340
+ const maxX = existingNodes.reduce((m, n) => Math.max(m, n.x + n.w), 0);
341
+ return { x: maxX + GRID_X, y: 0 };
342
+ }
343
+ /** Map a CyberBase category to a canvas node color (Obsidian canvas colors 1-6) */
344
+ function canvasColor(category) {
345
+ const cat = category.toLowerCase();
346
+ if (cat.startsWith('project'))
347
+ return '2'; // green
348
+ if (cat.startsWith('infra') || cat.startsWith('tool') || cat.startsWith('mesh'))
349
+ return '4'; // purple
350
+ if (cat.startsWith('vuln') || cat.startsWith('security'))
351
+ return '5'; // cyan
352
+ if (cat.startsWith('rule') || cat.startsWith('system'))
353
+ return '1'; // red
354
+ if (cat.startsWith('cred'))
355
+ return '3'; // yellow
356
+ return '6'; // default
357
+ }
358
+ /** Auto-sync a knowledge entry to the Canvas mindmap — adds or updates a node */
359
+ function syncCanvas(category, key, value) {
360
+ if (!vaultExists || !existsSync(CANVAS_PATH))
361
+ return;
362
+ try {
363
+ const raw = readFileSync(CANVAS_PATH, 'utf-8');
364
+ const canvas = JSON.parse(raw);
365
+ const nodeId = `auto_${sanitizeFilename(category)}_${sanitizeFilename(key)}`.slice(0, 60);
366
+ const title = `## ${category}: ${key}`;
367
+ const textContent = `${title}\n${value.slice(0, 500)}`;
368
+ const nodeWidth = 280;
369
+ const nodeHeight = Math.min(180, 80 + Math.ceil(value.length / 50) * 18);
370
+ const color = canvasColor(category);
371
+ // Find existing node by id
372
+ const existingIdx = canvas.nodes.findIndex(n => n.id === nodeId);
373
+ if (existingIdx >= 0) {
374
+ // Update in place — keep position
375
+ canvas.nodes[existingIdx] = {
376
+ ...canvas.nodes[existingIdx],
377
+ text: textContent,
378
+ height: nodeHeight,
379
+ color,
380
+ };
381
+ }
382
+ else {
383
+ // Find free position
384
+ const boxes = canvas.nodes.map(n => ({
385
+ x: n.x, y: n.y, w: n.width, h: n.height,
386
+ }));
387
+ const pos = findFreeCanvasPosition(boxes, nodeWidth, nodeHeight);
388
+ canvas.nodes.push({
389
+ id: nodeId,
390
+ type: 'text',
391
+ text: textContent,
392
+ x: pos.x,
393
+ y: pos.y,
394
+ width: nodeWidth,
395
+ height: nodeHeight,
396
+ color,
397
+ });
398
+ // Auto-connect to relevant parent node
399
+ const parentId = findCanvasParent(category, canvas.nodes);
400
+ if (parentId) {
401
+ canvas.edges.push({
402
+ id: `e_auto_${nodeId}`,
403
+ fromNode: parentId,
404
+ fromSide: 'bottom',
405
+ toNode: nodeId,
406
+ toSide: 'top',
407
+ label: category,
408
+ });
409
+ }
410
+ }
411
+ writeFileSync(CANVAS_PATH, JSON.stringify(canvas, null, '\t'), 'utf-8');
412
+ }
413
+ catch { /* canvas sync is best-effort */ }
414
+ }
415
+ /** Find the best parent node in the canvas to connect a new entry to */
416
+ function findCanvasParent(category, nodes) {
417
+ const cat = category.toLowerCase();
418
+ // Map categories to known canvas node IDs
419
+ if (cat.startsWith('project'))
420
+ return nodes.find(n => n.id === 'core')?.id ?? null;
421
+ if (cat.startsWith('infra') || cat.startsWith('mesh') || cat.startsWith('tool'))
422
+ return nodes.find(n => n.id === 'omniwire' || n.id === 'infra')?.id ?? null;
423
+ if (cat.startsWith('vuln') || cat.startsWith('security') || cat.startsWith('cve'))
424
+ return nodes.find(n => n.id === 'securitykb')?.id ?? null;
425
+ if (cat.startsWith('cred'))
426
+ return nodes.find(n => n.id === '1password' || n.id === 'db')?.id ?? null;
427
+ if (cat.startsWith('rule') || cat.startsWith('system'))
428
+ return nodes.find(n => n.id === 'rules')?.id ?? null;
429
+ if (cat.startsWith('note') || cat.startsWith('memo'))
430
+ return nodes.find(n => n.id === 'vault')?.id ?? null;
431
+ return nodes.find(n => n.id === 'core')?.id ?? null;
432
+ }
433
+ /** Sync entry to both Obsidian + Canvas (fire-and-forget, called from cb()) */
434
+ function syncVault(category, key, value) {
435
+ syncObsidian(category, key, value);
436
+ // Only add significant entries to canvas (skip tiny store values)
437
+ if (value.length > 50)
438
+ syncCanvas(category, key, value);
439
+ }
242
440
  /** Get CyberBase health status */
243
441
  function getCbHealth() {
244
442
  return { healthy: cbHealthy, failCount: cbFailCount, lastError: cbLastError, queueSize: CB_QUEUE.length };
@@ -1766,73 +1964,177 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
1766
1964
  return fail('Invalid action or missing params');
1767
1965
  });
1768
1966
  // --- Tool 33: omniwire_cdp ---
1769
- 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.', {
1770
- 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'),
1967
+ // Uses the persistent cdp-browser Docker container (puppeteer-core) for all operations.
1968
+ // Falls back to direct Chrome CLI for nodes without the container.
1969
+ const cdpScript = (js) => `docker exec cdp-browser node -e ${JSON.stringify(`const puppeteer=require('puppeteer-core');(async()=>{` +
1970
+ `const r=await fetch('http://127.0.0.1:9222/json/version');const{webSocketDebuggerUrl:ws}=await r.json();` +
1971
+ `const browser=await puppeteer.connect({browserWSEndpoint:ws});` +
1972
+ js +
1973
+ `})().catch(e=>{console.error('ERR:',e.message);process.exit(1)});`)} 2>&1`;
1974
+ server.tool('omniwire_cdp', 'Chrome DevTools Protocol — persistent headless browser via Docker container. Navigate, screenshot, HTML, PDF, cookies, evaluate JS, click, type, wait, network intercept, set-cookies, clear. Reuses pages across calls for speed.', {
1975
+ action: z.enum([
1976
+ 'navigate', 'screenshot', 'html', 'text', 'pdf', 'cookies', 'set-cookies', 'clear-cookies',
1977
+ 'tabs', 'close-tab', 'evaluate', 'click', 'type', 'wait', 'select',
1978
+ 'network', 'status', 'viewport',
1979
+ ]).describe('navigate=open URL, screenshot=capture PNG, html=DOM dump, text=innerText, pdf=save PDF, ' +
1980
+ 'cookies=get all, set-cookies=inject cookies, clear-cookies=wipe, tabs=list pages, close-tab=close page, ' +
1981
+ 'evaluate=run JS in page, click=click selector, type=type into selector, wait=wait for selector, ' +
1982
+ 'select=querySelector extract, network=recent requests, status=container health, viewport=set size'),
1771
1983
  node: z.string().optional().describe('Node (default: contabo)'),
1772
- url: z.string().optional().describe('URL for navigate/screenshot/html/pdf'),
1773
- file: z.string().optional().describe('Output path for screenshot/pdf'),
1774
- session_id: z.string().optional().describe('Session ID from launch'),
1775
- }, async ({ action, node, url, file: outFile, session_id }) => {
1984
+ url: z.string().optional().describe('URL for navigate'),
1985
+ selector: z.string().optional().describe('CSS selector for click/type/wait/select'),
1986
+ value: z.string().optional().describe('Text for type action, JS for evaluate, cookies JSON for set-cookies'),
1987
+ file: z.string().optional().describe('Output path for screenshot/pdf (default: /tmp/cdp-*)'),
1988
+ tab: z.number().optional().describe('Tab index (0-based, default: 0 = most recent)'),
1989
+ width: z.number().optional().describe('Viewport width for viewport action (default: 1920)'),
1990
+ height: z.number().optional().describe('Viewport height for viewport action (default: 1080)'),
1991
+ wait_ms: z.number().optional().describe('Wait timeout in ms (default: 10000)'),
1992
+ full_page: z.boolean().optional().describe('Full page screenshot (default: true)'),
1993
+ }, async ({ action, node, url, selector, value, file: outFile, tab, width, height, wait_ms, full_page }) => {
1776
1994
  const nodeId = node ?? 'contabo';
1777
- const sdir = '/tmp/.omniwire-cdp';
1778
- if (action === 'launch') {
1779
- const id = `cdp-${Date.now().toString(36)}`;
1780
- const port = 9222 + Math.floor(Math.random() * 100);
1781
- const cmd = `mkdir -p ${sdir}; command -v google-chrome >/dev/null || command -v chromium-browser >/dev/null || { echo "chrome not installed"; exit 1; }; ` +
1782
- `CHROME=$(command -v google-chrome || command -v chromium-browser); ` +
1783
- `$CHROME --headless --disable-gpu --no-sandbox --remote-debugging-port=${port} --user-data-dir=${sdir}/${id} ${url ? `"${url}"` : 'about:blank'} &>/dev/null & ` +
1784
- `echo $! > ${sdir}/${id}.pid; echo ${port} > ${sdir}/${id}.port; sleep 1; ` +
1785
- `echo "session=${id} port=${port} pid=$(cat ${sdir}/${id}.pid)"`;
1786
- const r = await manager.exec(nodeId, cmd);
1787
- if (r.code === 0)
1788
- resultStore.set('cdp_session', id);
1789
- return ok(nodeId, r.durationMs, r.stdout, 'chrome launch');
1790
- }
1791
- const sid = session_id ?? resultStore.get('cdp_session') ?? '';
1792
- if (action === 'navigate' && url) {
1793
- 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}"`;
1794
- const r = await manager.exec(nodeId, cmd);
1795
- return ok(nodeId, r.durationMs, r.stdout, `navigate`);
1995
+ const tabIdx = tab ?? 0;
1996
+ const timeout = wait_ms ?? 10000;
1997
+ const getPage = `const pages=await browser.pages();const page=pages[${tabIdx}]||pages[0];if(!page){console.log('no pages open');process.exit(0);}`;
1998
+ if (action === 'status') {
1999
+ const r = await manager.exec(nodeId, `docker inspect cdp-browser --format '{{.State.Status}} uptime={{.State.StartedAt}}' 2>/dev/null; ` +
2000
+ `curl -sf http://127.0.0.1:9222/json/version 2>/dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);print(f\\"chrome={d.get('Browser','')} proto={d.get('Protocol-Version','')}\\")" 2>/dev/null; ` +
2001
+ `curl -sf http://127.0.0.1:9222/json/list 2>/dev/null | python3 -c "import json,sys;tabs=json.load(sys.stdin);print(f\\"{len(tabs)} tabs open\\");[print(f\\" {t['id'][:8]} {t.get('url','')[:80]}\\") for t in tabs[:10]]" 2>/dev/null`);
2002
+ return ok(nodeId, r.durationMs, r.stdout, 'cdp status');
2003
+ }
2004
+ if (action === 'navigate') {
2005
+ if (!url)
2006
+ return fail('url required');
2007
+ const u = url.replace(/'/g, "\\'");
2008
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}` +
2009
+ `await page.goto('${u}',{waitUntil:'networkidle2',timeout:${timeout}});` +
2010
+ `console.log('url='+page.url());console.log('title='+await page.title());`));
2011
+ return ok(nodeId, r.durationMs, r.stdout, 'navigate');
1796
2012
  }
1797
2013
  if (action === 'screenshot') {
1798
- const output = outFile ?? `/tmp/screenshot-${Date.now()}.png`;
1799
- 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');
1800
- 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))"`;
1801
- const r = await manager.exec(nodeId, cmd);
2014
+ const out = outFile ?? `/tmp/cdp-screenshot-${Date.now()}.png`;
2015
+ const fp = full_page !== false;
2016
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}` +
2017
+ `await page.screenshot({path:'${out}',fullPage:${fp}});` +
2018
+ `const fs=require('fs');const sz=fs.statSync('${out}').size;` +
2019
+ `console.log('saved: ${out} ('+Math.round(sz/1024)+'KB) '+page.url());`));
1802
2020
  return ok(nodeId, r.durationMs, r.stdout, 'screenshot');
1803
2021
  }
1804
2022
  if (action === 'html') {
1805
- const target = url ?? 'about:blank';
1806
- 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`;
1807
- const r = await manager.exec(nodeId, cmd);
2023
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}` +
2024
+ `const html=await page.content();` +
2025
+ `console.log(html.substring(0,${url ? '50000' : '10000'}));`));
1808
2026
  return ok(nodeId, r.durationMs, r.stdout, 'html');
1809
2027
  }
2028
+ if (action === 'text') {
2029
+ const sel = selector ? `.replace(/'/g,"\\\\'")` : '';
2030
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}` +
2031
+ (selector
2032
+ ? `const el=await page.$('${selector.replace(/'/g, "\\'")}');const t=el?await page.evaluate(e=>e.innerText,el):'(not found)';console.log(t.substring(0,20000));`
2033
+ : `const t=await page.evaluate(()=>document.body.innerText);console.log(t.substring(0,20000));`)));
2034
+ return ok(nodeId, r.durationMs, r.stdout, 'text');
2035
+ }
1810
2036
  if (action === 'pdf') {
1811
- const output = outFile ?? `/tmp/page-${Date.now()}.pdf`;
1812
- const target = url ?? 'about:blank';
1813
- 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))"`;
1814
- const r = await manager.exec(nodeId, cmd);
2037
+ const out = outFile ?? `/tmp/cdp-page-${Date.now()}.pdf`;
2038
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}` +
2039
+ `await page.pdf({path:'${out}',format:'A4',printBackground:true});` +
2040
+ `const fs=require('fs');const sz=fs.statSync('${out}').size;` +
2041
+ `console.log('saved: ${out} ('+Math.round(sz/1024)+'KB)');`));
1815
2042
  return ok(nodeId, r.durationMs, r.stdout, 'pdf');
1816
2043
  }
1817
2044
  if (action === 'cookies') {
1818
- 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"`;
1819
- const r = await manager.exec(nodeId, cmd);
1820
- return ok(nodeId, r.durationMs, r.stdout, 'cdp cookies');
2045
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const cookies=await page.cookies();` +
2046
+ `cookies.forEach(c=>console.log(c.domain+'\\t'+c.name+'='+c.value.substring(0,60)+(c.value.length>60?'...':'')));` +
2047
+ `console.log('--- '+cookies.length+' cookies ---');`));
2048
+ return ok(nodeId, r.durationMs, r.stdout, 'cookies');
2049
+ }
2050
+ if (action === 'set-cookies') {
2051
+ if (!value)
2052
+ return fail('value required (JSON array of cookie objects)');
2053
+ const v = value.replace(/'/g, "\\'").replace(/\n/g, '');
2054
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const cookies=JSON.parse('${v}');` +
2055
+ `await page.setCookie(...cookies);console.log('set '+cookies.length+' cookies');`));
2056
+ return ok(nodeId, r.durationMs, r.stdout, 'set-cookies');
2057
+ }
2058
+ if (action === 'clear-cookies') {
2059
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const client=await page.createCDPSession();` +
2060
+ `await client.send('Network.clearBrowserCookies');console.log('cookies cleared');`));
2061
+ return ok(nodeId, r.durationMs, r.stdout, 'clear-cookies');
1821
2062
  }
1822
2063
  if (action === 'tabs') {
1823
- 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"`;
1824
- const r = await manager.exec(nodeId, cmd);
2064
+ const r = await manager.exec(nodeId, cdpScript(`const pages=await browser.pages();` +
2065
+ `pages.forEach((p,i)=>console.log(i+' '+p.url().substring(0,100)));` +
2066
+ `console.log('--- '+pages.length+' tabs ---');`));
1825
2067
  return ok(nodeId, r.durationMs, r.stdout, 'tabs');
1826
2068
  }
1827
- if (action === 'close') {
1828
- 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"`;
1829
- const r = await manager.exec(nodeId, cmd);
1830
- return ok(nodeId, r.durationMs, r.stdout, 'close');
2069
+ if (action === 'close-tab') {
2070
+ const r = await manager.exec(nodeId, cdpScript(`const pages=await browser.pages();` +
2071
+ `if(pages.length<=${tabIdx}){console.log('tab ${tabIdx} not found');process.exit(0);}` +
2072
+ `const url=pages[${tabIdx}].url();await pages[${tabIdx}].close();` +
2073
+ `console.log('closed tab ${tabIdx}: '+url);console.log((pages.length-1)+' tabs remaining');`));
2074
+ return ok(nodeId, r.durationMs, r.stdout, 'close-tab');
2075
+ }
2076
+ if (action === 'evaluate') {
2077
+ if (!value)
2078
+ return fail('value required (JavaScript to evaluate in page context)');
2079
+ const js = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
2080
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const result=await page.evaluate(()=>{${js}});` +
2081
+ `console.log(typeof result==='object'?JSON.stringify(result,null,2):String(result));`));
2082
+ return ok(nodeId, r.durationMs, r.stdout, 'evaluate');
2083
+ }
2084
+ if (action === 'click') {
2085
+ if (!selector)
2086
+ return fail('selector required');
2087
+ const sel = selector.replace(/'/g, "\\'");
2088
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}await page.waitForSelector('${sel}',{timeout:${timeout}});` +
2089
+ `await page.click('${sel}');console.log('clicked: ${sel}');` +
2090
+ `await new Promise(r=>setTimeout(r,500));console.log('url='+page.url());`));
2091
+ return ok(nodeId, r.durationMs, r.stdout, 'click');
2092
+ }
2093
+ if (action === 'type') {
2094
+ if (!selector || !value)
2095
+ return fail('selector and value required');
2096
+ const sel = selector.replace(/'/g, "\\'");
2097
+ const val = value.replace(/'/g, "\\'");
2098
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}await page.waitForSelector('${sel}',{timeout:${timeout}});` +
2099
+ `await page.type('${sel}','${val}');console.log('typed ${value.length} chars into ${sel}');`));
2100
+ return ok(nodeId, r.durationMs, r.stdout, 'type');
2101
+ }
2102
+ if (action === 'wait') {
2103
+ if (!selector)
2104
+ return fail('selector required');
2105
+ const sel = selector.replace(/'/g, "\\'");
2106
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const el=await page.waitForSelector('${sel}',{timeout:${timeout}});` +
2107
+ `const tag=await page.evaluate(e=>e.tagName+' '+e.className,el);` +
2108
+ `console.log('found: ${sel} → '+tag);`));
2109
+ return ok(nodeId, r.durationMs, r.stdout, 'wait');
2110
+ }
2111
+ if (action === 'select') {
2112
+ if (!selector)
2113
+ return fail('selector required');
2114
+ const sel = selector.replace(/'/g, "\\'");
2115
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const els=await page.$$('${sel}');` +
2116
+ `const results=[];for(const el of els.slice(0,20)){` +
2117
+ `const d=await page.evaluate(e=>({tag:e.tagName,text:e.innerText?.substring(0,200),href:e.href||'',src:e.src||''}),el);` +
2118
+ `results.push(d);}` +
2119
+ `console.log(JSON.stringify(results,null,2));console.log('--- '+els.length+' matches ---');`));
2120
+ return ok(nodeId, r.durationMs, r.stdout, 'select');
1831
2121
  }
1832
- if (action === 'list') {
1833
- 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"`;
1834
- const r = await manager.exec(nodeId, cmd);
1835
- return ok(nodeId, r.durationMs, r.stdout, 'cdp sessions');
2122
+ if (action === 'network') {
2123
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}const client=await page.createCDPSession();` +
2124
+ `const entries=[];client.on('Network.responseReceived',e=>{entries.push({url:e.response.url.substring(0,100),status:e.response.status,type:e.type});});` +
2125
+ `await client.send('Network.enable');` +
2126
+ `await page.reload({waitUntil:'networkidle2',timeout:${timeout}});` +
2127
+ `await client.send('Network.disable');` +
2128
+ `entries.slice(0,30).forEach(e=>console.log(e.status+' '+e.type.padEnd(12)+' '+e.url));` +
2129
+ `console.log('--- '+entries.length+' requests ---');`));
2130
+ return ok(nodeId, r.durationMs, r.stdout, 'network');
2131
+ }
2132
+ if (action === 'viewport') {
2133
+ const w = width ?? 1920;
2134
+ const h = height ?? 1080;
2135
+ const r = await manager.exec(nodeId, cdpScript(`${getPage}await page.setViewport({width:${w},height:${h}});` +
2136
+ `console.log('viewport set to ${w}x${h}');`));
2137
+ return ok(nodeId, r.durationMs, r.stdout, 'viewport');
1836
2138
  }
1837
2139
  return fail('Invalid action or missing params');
1838
2140
  });
@@ -3357,8 +3659,8 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
3357
3659
  return fail('invalid action');
3358
3660
  });
3359
3661
  // --- Tool 53: omniwire_knowledge ---
3360
- server.tool('omniwire_knowledge', 'CyberBase knowledge base — CRUD, search, and health management for the unified PostgreSQL knowledge store. Supports text search, semantic/vector search, categories, and bulk operations.', {
3361
- action: z.enum(['get', 'set', 'delete', 'search', 'semantic-search', 'list', 'stats', 'health', 'categories', 'bulk-set', 'export', 'vacuum']).describe('Action'),
3662
+ server.tool('omniwire_knowledge', 'CyberBase knowledge base — CRUD, search, and health management for the unified PostgreSQL knowledge store. Auto-syncs all writes to Obsidian vault + Canvas mindmap. Supports text search, semantic/vector search, categories, bulk operations, and explicit sync-obsidian/sync-canvas actions.', {
3663
+ action: z.enum(['get', 'set', 'delete', 'search', 'semantic-search', 'list', 'stats', 'health', 'categories', 'bulk-set', 'export', 'vacuum', 'sync-obsidian', 'sync-canvas']).describe('Action'),
3362
3664
  category: z.string().optional().describe('Knowledge category (e.g., tools, vulns, infra, notes)'),
3363
3665
  key: z.string().optional().describe('Knowledge key (for get/set/delete)'),
3364
3666
  value: z.string().optional().describe('Value to store (for set)'),
@@ -3456,6 +3758,25 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
3456
3758
  const r = await cbManager.exec('contabo', pgExec("DELETE FROM knowledge WHERE value IS NULL OR value::text = 'null' OR key = ''; VACUUM ANALYZE knowledge;"));
3457
3759
  return okBrief(`vacuum complete:\n${r.stdout.trim()}`);
3458
3760
  }
3761
+ if (action === 'sync-obsidian') {
3762
+ if (!key || !value)
3763
+ return fail('key and value required');
3764
+ if (!vaultExists)
3765
+ return fail(`Obsidian vault not found at ${VAULT_ROOT}`);
3766
+ const cat = category ?? 'general';
3767
+ syncObsidian(cat, key, value);
3768
+ const folder = vaultFolder(cat);
3769
+ return okBrief(`synced to Obsidian: ${folder}/${sanitizeFilename(key)}.md (${value.length} chars)`);
3770
+ }
3771
+ if (action === 'sync-canvas') {
3772
+ if (!key || !value)
3773
+ return fail('key and value required');
3774
+ if (!vaultExists || !existsSync(CANVAS_PATH))
3775
+ return fail(`Canvas not found at ${CANVAS_PATH}`);
3776
+ const cat = category ?? 'general';
3777
+ syncCanvas(cat, key, value);
3778
+ return okBrief(`synced to Canvas: node auto_${sanitizeFilename(cat)}_${sanitizeFilename(key)} added/updated`);
3779
+ }
3459
3780
  return fail('invalid action');
3460
3781
  });
3461
3782
  // --- Tool 54: omniwire_omnimesh ---