omniwire 3.1.3 → 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.
package/dist/mcp/server.js
CHANGED
|
@@ -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 };
|
|
@@ -3461,8 +3659,8 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
|
|
|
3461
3659
|
return fail('invalid action');
|
|
3462
3660
|
});
|
|
3463
3661
|
// --- Tool 53: omniwire_knowledge ---
|
|
3464
|
-
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
|
|
3465
|
-
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'),
|
|
3466
3664
|
category: z.string().optional().describe('Knowledge category (e.g., tools, vulns, infra, notes)'),
|
|
3467
3665
|
key: z.string().optional().describe('Knowledge key (for get/set/delete)'),
|
|
3468
3666
|
value: z.string().optional().describe('Value to store (for set)'),
|
|
@@ -3560,6 +3758,25 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
|
|
|
3560
3758
|
const r = await cbManager.exec('contabo', pgExec("DELETE FROM knowledge WHERE value IS NULL OR value::text = 'null' OR key = ''; VACUUM ANALYZE knowledge;"));
|
|
3561
3759
|
return okBrief(`vacuum complete:\n${r.stdout.trim()}`);
|
|
3562
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
|
+
}
|
|
3563
3780
|
return fail('invalid action');
|
|
3564
3781
|
});
|
|
3565
3782
|
// --- Tool 54: omniwire_omnimesh ---
|