vektor-slipstream 1.0.5 → 1.0.6

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.

Potentially problematic release.


This version of vektor-slipstream might be problematic. Click here for more details.

package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vektor-slipstream",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Hardware-accelerated persistent memory for AI agents. Local-first, zero cloud dependency, $0 embedding cost.",
5
5
  "main": "slipstream-core.js",
6
6
  "exports": {
@@ -51,6 +51,7 @@
51
51
  "detect-hardware.js",
52
52
  "vektor-licence.js",
53
53
  "sovereign.js",
54
+ "visualize.js",
54
55
  "TENETS.md",
55
56
  "models/model_quantized.onnx",
56
57
  "examples/",
@@ -23,6 +23,7 @@
23
23
  const path = require('path');
24
24
  const fs = require('fs');
25
25
  const { validateLicence } = require('./vektor-licence');
26
+ const { startVisualizer } = require('./visualize');
26
27
 
27
28
  // ─── SQLite loader — better-sqlite3 with clear error ─────────────────────────
28
29
  function loadSQLite(dbPath) {
@@ -623,6 +624,13 @@ class SlipstreamMemory {
623
624
  remove(id) {
624
625
  try { this.db.prepare("DELETE FROM memories WHERE id=? AND agent_id=?").run(id, this.agentId); } catch (_) {}
625
626
  }
627
+
628
+ // ── visualize(opts) ───────────────────────────────────────────────────────
629
+ // Opens a local D3 graph in the browser showing the agent's memory graph.
630
+ // Returns the server instance so the caller can close it programmatically.
631
+ async visualize(opts = {}) {
632
+ return startVisualizer(this, opts);
633
+ }
626
634
  }
627
635
 
628
636
  // ─── Boot Banner ──────────────────────────────────────────────────────────────
package/vektor-licence.js CHANGED
@@ -2,39 +2,43 @@
2
2
  /**
3
3
  * vektor-licence.js — Polar Licence Enforcement
4
4
  * ─────────────────────────────────────────────────────────────────────────────
5
- * Validates a Polar licence key before allowing createMemory() to proceed.
5
+ * Validates a Polar licence key and enforces 3-machine activation limit.
6
6
  *
7
7
  * Flow:
8
8
  * 1. Customer calls createMemory({ licenceKey: 'VEKTOR-XXXX-...' })
9
- * 2. We check ~/.vektor/licence.json for a cached validation (30-day TTL)
10
- * 3. If cache miss or expiredcall Polar validate API
11
- * 4. Validcache result, proceed
12
- * 5. Invalid/missingthrow with purchase link
9
+ * 2. Check ~/.vektor/licence.json for cached activation (30-day TTL)
10
+ * 3. Cache hit + activation_id presentproceed silently
11
+ * 4. Cache miss call Polar /activate (counts against 3-machine limit)
12
+ * 5. Successcache activation_id + proceed
13
+ * 6. Limit reached → clear error with deactivation instructions
14
+ * 7. Invalid key → throw with purchase link
13
15
  *
14
- * Polar validate endpoint requires no auth token — safe for client-side use.
16
+ * Machine deactivation (free up a slot):
17
+ * node -e "require('vektor-slipstream/vektor-licence').deactivateMachine('VEKTOR-XXXX')"
18
+ * Or customer manages activations at: https://polar.sh
15
19
  * ─────────────────────────────────────────────────────────────────────────────
16
20
  */
17
21
 
18
- const fs = require('fs');
19
- const path = require('path');
20
- const os = require('os');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const os = require('os');
21
25
  const crypto = require('crypto');
22
26
 
23
27
  // ── Config ────────────────────────────────────────────────────────────────────
24
- // Replace POLAR_ORG_ID with your actual Polar organisation ID from
25
- // polar.sh → Settings → Organisation ID
26
28
 
27
- const POLAR_ORG_ID = 'a922049c-3049-41e8-9b20-b18890576b6f';
28
- const POLAR_API = 'https://api.polar.sh/v1/customer-portal/license-keys/validate';
29
- const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
30
- const CACHE_DIR = path.join(os.homedir(), '.vektor');
31
- const CACHE_FILE = path.join(CACHE_DIR, 'licence.json');
32
- const PURCHASE_URL = 'https://vektormemory.com/#pricing';
29
+ const POLAR_ORG_ID = 'a922049c-3049-41e8-9b20-b18890576b6f';
30
+ const POLAR_VALIDATE = 'https://api.polar.sh/v1/customer-portal/license-keys/validate';
31
+ const POLAR_ACTIVATE = 'https://api.polar.sh/v1/customer-portal/license-keys/activate';
32
+ const POLAR_DEACTIVATE = 'https://api.polar.sh/v1/customer-portal/license-keys/deactivate';
33
+ const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
34
+ const CACHE_DIR = path.join(os.homedir(), '.vektor');
35
+ const CACHE_FILE = path.join(CACHE_DIR, 'licence.json');
36
+ const PURCHASE_URL = 'https://vektormemory.com/#pricing';
37
+ const MACHINE_LABEL = `${os.hostname()} (${os.platform()}/${os.arch()})`;
33
38
 
34
39
  // ── Cache helpers ─────────────────────────────────────────────────────────────
35
40
 
36
41
  function _cacheKey(licenceKey) {
37
- // Hash the key so we don't store it in plaintext
38
42
  return crypto.createHash('sha256').update(licenceKey).digest('hex').slice(0, 16);
39
43
  }
40
44
 
@@ -58,134 +62,247 @@ function _getCached(licenceKey) {
58
62
  const cache = _readCache();
59
63
  const entry = cache[_cacheKey(licenceKey)];
60
64
  if (!entry) return null;
61
- if (Date.now() - entry.cached_at > CACHE_TTL_MS) return null; // expired
65
+ if (Date.now() - entry.cached_at > CACHE_TTL_MS) return null;
62
66
  return entry;
63
67
  }
64
68
 
65
- // ── Polar validation ──────────────────────────────────────────────────────────
69
+ function _clearCache(licenceKey) {
70
+ try {
71
+ const cache = _readCache();
72
+ delete cache[_cacheKey(licenceKey)];
73
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
74
+ } catch(_) {}
75
+ }
66
76
 
67
- async function _validateWithPolar(licenceKey) {
68
- const body = JSON.stringify({
69
- key: licenceKey,
70
- organization_id: POLAR_ORG_ID,
71
- });
77
+ // ── Polar API calls ───────────────────────────────────────────────────────────
72
78
 
73
- const res = await fetch(POLAR_API, {
79
+ async function _polarPost(url, body) {
80
+ const res = await fetch(url, {
74
81
  method: 'POST',
75
82
  headers: { 'Content-Type': 'application/json' },
76
- body,
83
+ body: JSON.stringify(body),
77
84
  signal: AbortSignal.timeout(10000),
78
85
  });
86
+ return { status: res.status, data: res.ok ? await res.json() : null };
87
+ }
79
88
 
80
- if (res.status === 404) {
81
- return { valid: false, reason: 'Key not found' };
82
- }
83
- if (res.status === 422) {
84
- return { valid: false, reason: 'Invalid key format' };
85
- }
86
- if (!res.ok) {
87
- // Network/server error — allow offline grace period
88
- return { valid: null, reason: `Polar API error: ${res.status}` };
89
- }
89
+ async function _activateMachine(licenceKey) {
90
+ const { status, data } = await _polarPost(POLAR_ACTIVATE, {
91
+ key: licenceKey,
92
+ organization_id: POLAR_ORG_ID,
93
+ label: MACHINE_LABEL,
94
+ });
90
95
 
91
- const data = await res.json();
96
+ if (status === 403) return { valid: false, reason: 'LIMIT_REACHED' };
97
+ if (status === 404) return { valid: false, reason: 'KEY_NOT_FOUND' };
98
+ if (status === 422) return { valid: false, reason: 'INVALID_FORMAT' };
99
+ if (!data) return { valid: null, reason: `API_ERROR_${status}` };
92
100
 
93
- // Polar returns status: 'granted' | 'revoked' | 'disabled'
94
- if (data.status !== 'granted') {
95
- return { valid: false, reason: `Licence ${data.status}` };
101
+ const lk = data.license_key || data;
102
+ if (lk.status !== 'granted') {
103
+ return { valid: false, reason: `LICENCE_${(lk.status || 'UNKNOWN').toUpperCase()}` };
96
104
  }
97
105
 
98
106
  return {
99
- valid: true,
100
- status: data.status,
101
- expires_at: data.expires_at || null,
102
- key_id: data.id,
107
+ valid: true,
108
+ activation_id: data.id,
109
+ key_id: lk.id,
110
+ expires_at: lk.expires_at || null,
111
+ usage: lk.usage,
112
+ limit: lk.limit_activations,
103
113
  };
104
114
  }
105
115
 
106
- // ── Main export ───────────────────────────────────────────────────────────────
116
+ async function _validateKey(licenceKey) {
117
+ const { status, data } = await _polarPost(POLAR_VALIDATE, {
118
+ key: licenceKey,
119
+ organization_id: POLAR_ORG_ID,
120
+ });
121
+
122
+ if (status === 404) return { valid: false, reason: 'KEY_NOT_FOUND' };
123
+ if (status === 422) return { valid: false, reason: 'INVALID_FORMAT' };
124
+ if (!data) return { valid: null, reason: `API_ERROR_${status}` };
125
+ if (data.status !== 'granted') {
126
+ return { valid: false, reason: `LICENCE_${(data.status || 'UNKNOWN').toUpperCase()}` };
127
+ }
128
+ return { valid: true };
129
+ }
130
+
131
+ async function _deactivateMachine(licenceKey, activationId) {
132
+ const { status } = await _polarPost(POLAR_DEACTIVATE, {
133
+ key: licenceKey,
134
+ organization_id: POLAR_ORG_ID,
135
+ activation_id: activationId,
136
+ });
137
+ return status === 200 || status === 204;
138
+ }
139
+
140
+ // ── Error messages ────────────────────────────────────────────────────────────
141
+
142
+ function _missingKeyError() {
143
+ return new Error(
144
+ '\n' +
145
+ ' ╔══════════════════════════════════════════════════════╗\n' +
146
+ ' ║ VEKTOR SLIPSTREAM — LICENCE REQUIRED ║\n' +
147
+ ' ╚══════════════════════════════════════════════════════╝\n' +
148
+ '\n' +
149
+ ' A valid licence key is required to use Vektor Slipstream.\n' +
150
+ '\n' +
151
+ ' Purchase at: ' + PURCHASE_URL + '\n' +
152
+ '\n' +
153
+ ' Usage:\n' +
154
+ ' const memory = await createMemory({\n' +
155
+ ' agentId: \'my-agent\',\n' +
156
+ ' licenceKey: \'VEKTOR-XXXX-XXXX-XXXX\',\n' +
157
+ ' });\n'
158
+ );
159
+ }
160
+
161
+ function _invalidKeyError(reason) {
162
+ return new Error(
163
+ '\n' +
164
+ ' ╔══════════════════════════════════════════════════════╗\n' +
165
+ ' ║ VEKTOR SLIPSTREAM — LICENCE INVALID ║\n' +
166
+ ' ╚══════════════════════════════════════════════════════╝\n' +
167
+ '\n' +
168
+ ' Reason: ' + reason + '\n' +
169
+ '\n' +
170
+ ' Purchase a valid licence at: ' + PURCHASE_URL + '\n' +
171
+ ' Already purchased? Check your email for the key.\n'
172
+ );
173
+ }
174
+
175
+ function _limitReachedError() {
176
+ return new Error(
177
+ '\n' +
178
+ ' ╔══════════════════════════════════════════════════════╗\n' +
179
+ ' ║ VEKTOR SLIPSTREAM — ACTIVATION LIMIT REACHED ║\n' +
180
+ ' ╚══════════════════════════════════════════════════════╝\n' +
181
+ '\n' +
182
+ ' Your licence is active on 3 machines (the maximum).\n' +
183
+ '\n' +
184
+ ' To activate on this machine, deactivate another first:\n' +
185
+ '\n' +
186
+ ' Option A — run on the machine you want to remove:\n' +
187
+ ' node -e "require(\'vektor-slipstream/vektor-licence\')\n' +
188
+ ' .deactivateMachine(\'YOUR_KEY\')"\n' +
189
+ '\n' +
190
+ ' Option B — manage activations at:\n' +
191
+ ' https://polar.sh (your customer portal)\n' +
192
+ '\n' +
193
+ ' Need more machines? Enterprise (10 seats) coming soon.\n' +
194
+ ' Contact: hello@vektormemory.com\n'
195
+ );
196
+ }
197
+
198
+ // ── Public API ────────────────────────────────────────────────────────────────
107
199
 
108
200
  /**
109
201
  * validateLicence(licenceKey)
110
202
  *
111
- * Call before createMemory(). Throws if the key is invalid.
112
- * Caches valid keys for 30 days so it doesn't call Polar on every run.
203
+ * Call before createMemory(). Activates this machine on first run,
204
+ * validates on subsequent runs. Enforces 3-machine limit via Polar.
113
205
  *
114
- * @param {string} licenceKey — the customer's Polar licence key
206
+ * @param {string} licenceKey
115
207
  * @returns {Promise<void>}
116
- * @throws {Error} if key is invalid, revoked, or missing
208
+ * @throws {Error} if key invalid, revoked, missing, or limit reached
117
209
  */
118
210
  async function validateLicence(licenceKey) {
119
211
  if (!licenceKey || typeof licenceKey !== 'string' || licenceKey.trim().length < 8) {
120
- throw new Error(
121
- '\n' +
122
- ' ╔══════════════════════════════════════════════════════╗\n' +
123
- ' ║ VEKTOR SLIPSTREAM — LICENCE REQUIRED ║\n' +
124
- ' ╚══════════════════════════════════════════════════════╝\n' +
125
- '\n' +
126
- ' A valid licence key is required to use Vektor Slipstream.\n' +
127
- '\n' +
128
- ' Purchase at: ' + PURCHASE_URL + '\n' +
129
- '\n' +
130
- ' Usage:\n' +
131
- ' const memory = await createMemory({\n' +
132
- ' agentId: \'my-agent\',\n' +
133
- ' licenceKey: \'VEKTOR-XXXX-XXXX-XXXX\',\n' +
134
- ' });\n'
135
- );
212
+ throw _missingKeyError();
136
213
  }
137
214
 
138
- // Check cache first
139
- const cached = _getCached(licenceKey);
140
- if (cached?.valid === true) {
141
- // Cache hit — valid, proceed silently
142
- return;
215
+ const key = licenceKey.trim();
216
+
217
+ // Cache hit this machine is already activated
218
+ const cached = _getCached(key);
219
+ if (cached?.valid === true && cached?.activation_id) {
220
+ return; // proceed silently
143
221
  }
144
222
 
145
- // Cache miss or expired — validate with Polar
223
+ // Cache miss or expired — activate this machine
146
224
  let result;
147
225
  try {
148
- result = await _validateWithPolar(licenceKey.trim());
226
+ result = await _activateMachine(key);
149
227
  } catch(e) {
150
- // Network failure — check if we have any cached entry (even expired) as grace period
151
- const staleCache = _readCache()[_cacheKey(licenceKey)];
152
- if (staleCache?.valid === true) {
153
- console.warn('[SLIPSTREAM] Could not reach licence server — using cached validation (grace period).');
228
+ // Network failure — grace period using stale cache
229
+ const stale = _readCache()[_cacheKey(key)];
230
+ if (stale?.valid === true) {
231
+ console.warn('[vektor] Could not reach licence server — using cached activation (grace period).');
154
232
  return;
155
233
  }
156
234
  throw new Error(
157
- '[SLIPSTREAM] Licence validation failed — could not reach Polar.\n' +
158
- 'Check your internet connection and try again.\n' +
159
- 'Error: ' + e.message
235
+ '[vektor] Licence validation failed — could not reach Polar.\n' +
236
+ 'Check your internet connection.\nError: ' + e.message
160
237
  );
161
238
  }
162
239
 
163
240
  if (result.valid === null) {
164
- // Polar API error — use stale cache as grace period
165
- const staleCache = _readCache()[_cacheKey(licenceKey)];
166
- if (staleCache?.valid === true) {
167
- console.warn('[SLIPSTREAM] Licence server error — using cached validation (grace period).');
241
+ const stale = _readCache()[_cacheKey(key)];
242
+ if (stale?.valid === true) {
243
+ console.warn('[vektor] Licence server error — using cached activation (grace period).');
168
244
  return;
169
245
  }
170
- throw new Error('[SLIPSTREAM] Licence server temporarily unavailable. Try again in a moment.');
246
+ throw new Error('[vektor] Licence server temporarily unavailable. Try again in a moment.');
171
247
  }
172
248
 
173
249
  if (!result.valid) {
174
- throw new Error(
175
- '\n' +
176
- ' ╔══════════════════════════════════════════════════════╗\n' +
177
- ' ║ VEKTOR SLIPSTREAM — LICENCE INVALID ║\n' +
178
- ' ╚══════════════════════════════════════════════════════╝\n' +
179
- '\n' +
180
- ' Reason: ' + result.reason + '\n' +
181
- '\n' +
182
- ' Purchase a valid licence at: ' + PURCHASE_URL + '\n' +
183
- ' Already purchased? Check your email for the key.\n'
184
- );
250
+ if (result.reason === 'LIMIT_REACHED') throw _limitReachedError();
251
+ throw _invalidKeyError(result.reason);
252
+ }
253
+
254
+ // Cache the activation
255
+ _writeCache(key, {
256
+ valid: true,
257
+ activation_id: result.activation_id,
258
+ key_id: result.key_id,
259
+ expires_at: result.expires_at,
260
+ machine: MACHINE_LABEL,
261
+ });
262
+
263
+ console.log(`[vektor] Licence activated — ${MACHINE_LABEL} (${result.usage}/${result.limit} seats used)`);
264
+ }
265
+
266
+ /**
267
+ * deactivateMachine(licenceKey)
268
+ *
269
+ * Frees this machine's activation slot. Call before switching machines
270
+ * or uninstalling. Removes one of the 3 used slots from Polar.
271
+ *
272
+ * CLI usage:
273
+ * node -e "require('vektor-slipstream/vektor-licence').deactivateMachine('VEKTOR-XXXX')"
274
+ *
275
+ * @param {string} licenceKey
276
+ * @returns {Promise<void>}
277
+ */
278
+ async function deactivateMachine(licenceKey) {
279
+ if (!licenceKey || licenceKey.trim().length < 8) {
280
+ console.error('[vektor] No licence key provided.');
281
+ return;
282
+ }
283
+
284
+ const key = licenceKey.trim();
285
+ const cached = _readCache()[_cacheKey(key)];
286
+
287
+ if (!cached?.activation_id) {
288
+ console.log('[vektor] No active activation found on this machine.');
289
+ _clearCache(key);
290
+ return;
185
291
  }
186
292
 
187
- // Valid — cache it
188
- _writeCache(licenceKey, result);
293
+ console.log(`[vektor] Deactivating: ${MACHINE_LABEL}`);
294
+
295
+ try {
296
+ const ok = await _deactivateMachine(key, cached.activation_id);
297
+ if (ok) {
298
+ _clearCache(key);
299
+ console.log('[vektor] Deactivation successful — slot is now free for another machine.');
300
+ } else {
301
+ console.warn('[vektor] Deactivation failed. Manage activations at: https://polar.sh');
302
+ }
303
+ } catch(e) {
304
+ console.error('[vektor] Deactivation error:', e.message);
305
+ }
189
306
  }
190
307
 
191
- module.exports = { validateLicence };
308
+ module.exports = { validateLicence, deactivateMachine };
package/visualize.js ADDED
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+ /**
3
+ * visualize.js — memory.visualize()
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ * Opens a local HTTP server serving a D3 force graph of the agent's memory.
6
+ * Launched by memory.visualize() — opens browser automatically.
7
+ * Runs until the user closes the browser tab or calls server.close().
8
+ *
9
+ * No external dependencies beyond what's already in the SDK.
10
+ * Pure Node.js http + inline HTML/CSS/JS with D3 from CDN.
11
+ * ─────────────────────────────────────────────────────────────────────────────
12
+ */
13
+
14
+ const http = require('http');
15
+ const os = require('os');
16
+
17
+ // ── D3 Graph HTML ─────────────────────────────────────────────────────────────
18
+
19
+ function buildHTML(agentId) {
20
+ return `<!DOCTYPE html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="UTF-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
25
+ <title>Vektor Memory Graph — ${agentId}</title>
26
+ <style>
27
+ * { box-sizing: border-box; margin: 0; padding: 0; }
28
+ body { background: #0c0d0e; color: #e2e8f0; font-family: 'IBM Plex Mono', monospace; overflow: hidden; }
29
+ #header { position: absolute; top: 0; left: 0; right: 0; padding: 12px 20px; display: flex; align-items: center; justify-content: space-between; background: rgba(12,13,14,0.9); border-bottom: 1px solid rgba(255,255,255,0.08); z-index: 10; }
30
+ #header h1 { font-size: 13px; font-weight: 500; letter-spacing: 0.06em; color: rgba(242,242,242,0.7); }
31
+ #stats { font-size: 11px; color: rgba(242,242,242,0.38); letter-spacing: 0.04em; }
32
+ #graph { width: 100vw; height: 100vh; }
33
+ .node circle { stroke-width: 1.5; cursor: pointer; }
34
+ .node text { font-size: 10px; fill: rgba(242,242,242,0.6); pointer-events: none; }
35
+ .link { stroke: rgba(255,255,255,0.12); stroke-width: 1; }
36
+ .link.semantic { stroke: rgba(99,153,34,0.4); }
37
+ .link.causal { stroke: rgba(255,107,0,0.4); }
38
+ .link.temporal { stroke: rgba(59,139,212,0.4); }
39
+ .link.entity { stroke: rgba(127,119,221,0.4); }
40
+ #tooltip { position: absolute; background: rgba(12,13,14,0.95); border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; padding: 10px 14px; font-size: 11px; max-width: 300px; line-height: 1.6; pointer-events: none; display: none; z-index: 20; }
41
+ #tooltip .t-content { color: rgba(242,242,242,0.8); margin-bottom: 4px; }
42
+ #tooltip .t-meta { color: rgba(242,242,242,0.38); font-size: 10px; }
43
+ #legend { position: absolute; bottom: 16px; left: 20px; font-size: 10px; color: rgba(242,242,242,0.38); line-height: 2; }
44
+ .leg { display: flex; align-items: center; gap: 6px; }
45
+ .leg-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
46
+ #controls { position: absolute; bottom: 16px; right: 20px; display: flex; gap: 8px; }
47
+ #controls button { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: rgba(242,242,242,0.6); font-size: 11px; font-family: inherit; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
48
+ #controls button:hover { background: rgba(255,255,255,0.1); }
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <div id="header">
53
+ <h1>VEKTOR MEMORY GRAPH &mdash; ${agentId}</h1>
54
+ <span id="stats">loading...</span>
55
+ </div>
56
+ <svg id="graph"></svg>
57
+ <div id="tooltip"><div class="t-content" id="tt-content"></div><div class="t-meta" id="tt-meta"></div></div>
58
+ <div id="legend">
59
+ <div class="leg"><div class="leg-dot" style="background:#639922"></div> semantic</div>
60
+ <div class="leg"><div class="leg-dot" style="background:#ff6b00"></div> causal</div>
61
+ <div class="leg"><div class="leg-dot" style="background:#378add"></div> temporal</div>
62
+ <div class="leg"><div class="leg-dot" style="background:#7f77dd"></div> entity</div>
63
+ </div>
64
+ <div id="controls">
65
+ <button onclick="restart()">Reset</button>
66
+ <button onclick="window.location.reload()">Refresh</button>
67
+ </div>
68
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
69
+ <script>
70
+ const W = window.innerWidth, H = window.innerHeight;
71
+ const svg = d3.select('#graph').attr('width', W).attr('height', H);
72
+ const g = svg.append('g');
73
+ const tooltip = document.getElementById('tooltip');
74
+
75
+ svg.call(d3.zoom().scaleExtent([0.1, 4]).on('zoom', e => g.attr('transform', e.transform)));
76
+
77
+ const COLOR = { semantic: '#639922', causal: '#ff6b00', temporal: '#378add', entity: '#7f77dd', default: '#888780' };
78
+
79
+ async function load() {
80
+ const data = await fetch('/data').then(r => r.json());
81
+ const nodes = data.nodes;
82
+ const links = data.edges.map(e => ({ ...e, source: e.source_id, target: e.target_id }));
83
+
84
+ document.getElementById('stats').textContent =
85
+ nodes.length + ' nodes · ' + links.length + ' edges · agent: ${agentId}';
86
+
87
+ g.selectAll('*').remove();
88
+
89
+ const sim = d3.forceSimulation(nodes)
90
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80))
91
+ .force('charge', d3.forceManyBody().strength(-120))
92
+ .force('center', d3.forceCenter(W / 2, H / 2))
93
+ .force('collision', d3.forceCollide(20));
94
+
95
+ const link = g.append('g').selectAll('line')
96
+ .data(links).join('line')
97
+ .attr('class', d => 'link ' + (d.edge_type || 'semantic'));
98
+
99
+ const node = g.append('g').selectAll('g')
100
+ .data(nodes).join('g').attr('class', 'node')
101
+ .call(d3.drag()
102
+ .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
103
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
104
+ .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }));
105
+
106
+ node.append('circle')
107
+ .attr('r', d => 4 + Math.min((d.importance || 1) * 2, 10))
108
+ .attr('fill', d => COLOR[d.edge_type] || COLOR.default)
109
+ .attr('fill-opacity', 0.8)
110
+ .attr('stroke', d => COLOR[d.edge_type] || COLOR.default)
111
+ .on('mouseover', (e, d) => {
112
+ document.getElementById('tt-content').textContent = d.content ? d.content.slice(0, 200) : d.id;
113
+ document.getElementById('tt-meta').textContent =
114
+ 'importance: ' + (d.importance || 1) + ' · type: ' + (d.edge_type || 'semantic') + ' · id: ' + d.id;
115
+ tooltip.style.display = 'block';
116
+ tooltip.style.left = (e.pageX + 12) + 'px';
117
+ tooltip.style.top = (e.pageY + 12) + 'px';
118
+ })
119
+ .on('mousemove', e => {
120
+ tooltip.style.left = (e.pageX + 12) + 'px';
121
+ tooltip.style.top = (e.pageY + 12) + 'px';
122
+ })
123
+ .on('mouseout', () => tooltip.style.display = 'none');
124
+
125
+ node.append('text')
126
+ .attr('dx', 10).attr('dy', 4)
127
+ .text(d => d.content ? d.content.slice(0, 30) + (d.content.length > 30 ? '…' : '') : '');
128
+
129
+ sim.on('tick', () => {
130
+ link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
131
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
132
+ node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
133
+ });
134
+
135
+ window._sim = sim;
136
+ }
137
+
138
+ function restart() { if (window._sim) { window._sim.alpha(1).restart(); } }
139
+
140
+ load().catch(e => {
141
+ document.getElementById('stats').textContent = 'Error loading graph: ' + e.message;
142
+ });
143
+ </script>
144
+ </body>
145
+ </html>`;
146
+ }
147
+
148
+ // ── HTTP Server ───────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * startVisualizer(memory, opts)
152
+ *
153
+ * Starts a local HTTP server serving a D3 memory graph.
154
+ * Opens the browser automatically.
155
+ *
156
+ * @param {SlipstreamMemory} memory — the memory instance to visualize
157
+ * @param {object} opts
158
+ * @param {number} opts.port — port (default: random available)
159
+ * @param {boolean} opts.open — auto-open browser (default: true)
160
+ * @returns {Promise<http.Server>}
161
+ */
162
+ async function startVisualizer(memory, opts = {}) {
163
+ const { open = true } = opts;
164
+ const agentId = memory.agentId || 'agent';
165
+
166
+ const server = http.createServer(async (req, res) => {
167
+ const cors = { 'Access-Control-Allow-Origin': '*' };
168
+
169
+ if (req.url === '/') {
170
+ res.writeHead(200, { 'Content-Type': 'text/html', ...cors });
171
+ res.end(buildHTML(agentId));
172
+ return;
173
+ }
174
+
175
+ if (req.url === '/data') {
176
+ try {
177
+ // Pull all memories as nodes
178
+ const nodes = memory.db.prepare(
179
+ 'SELECT id, content, edge_type, importance FROM memories WHERE agent_id = ? ORDER BY importance DESC LIMIT 500'
180
+ ).all(agentId);
181
+
182
+ // Pull edges
183
+ const edges = memory.db.prepare(
184
+ 'SELECT source_id, target_id, edge_type, weight FROM memory_edges WHERE agent_id = ? LIMIT 2000'
185
+ ).all(agentId);
186
+
187
+ res.writeHead(200, { 'Content-Type': 'application/json', ...cors });
188
+ res.end(JSON.stringify({ nodes, edges }));
189
+ } catch(e) {
190
+ res.writeHead(500, { 'Content-Type': 'application/json', ...cors });
191
+ res.end(JSON.stringify({ error: e.message }));
192
+ }
193
+ return;
194
+ }
195
+
196
+ res.writeHead(404); res.end();
197
+ });
198
+
199
+ // Find an available port
200
+ await new Promise((resolve, reject) => {
201
+ const port = opts.port || 0; // 0 = OS picks available port
202
+ server.listen(port, '127.0.0.1', () => resolve());
203
+ server.on('error', reject);
204
+ });
205
+
206
+ const { port } = server.address();
207
+ const url = `http://localhost:${port}`;
208
+
209
+ console.log('');
210
+ console.log(' ╔══════════════════════════════════════════════════════╗');
211
+ console.log(' ║ VEKTOR MEMORY GRAPH — VISUALIZER ║');
212
+ console.log(' ╚══════════════════════════════════════════════════════╝');
213
+ console.log('');
214
+ console.log(` Agent: ${agentId}`);
215
+ console.log(` URL: ${url}`);
216
+ console.log('');
217
+ console.log(' Close this process (Ctrl+C) to stop the visualizer.');
218
+ console.log('');
219
+
220
+ // Auto-open browser
221
+ if (open) {
222
+ const platform = os.platform();
223
+ const { exec } = require('child_process');
224
+ const cmd = platform === 'win32' ? `start ${url}`
225
+ : platform === 'darwin' ? `open ${url}`
226
+ : `xdg-open ${url}`;
227
+ exec(cmd, err => {
228
+ if (err) console.log(` Open manually: ${url}`);
229
+ });
230
+ }
231
+
232
+ return server;
233
+ }
234
+
235
+ module.exports = { startVisualizer };