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 +2 -1
- package/slipstream-core.js +8 -0
- package/vektor-licence.js +216 -99
- package/visualize.js +235 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vektor-slipstream",
|
|
3
|
-
"version": "1.0.
|
|
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/",
|
package/slipstream-core.js
CHANGED
|
@@ -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
|
|
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.
|
|
10
|
-
* 3.
|
|
11
|
-
* 4.
|
|
12
|
-
* 5.
|
|
9
|
+
* 2. Check ~/.vektor/licence.json for cached activation (30-day TTL)
|
|
10
|
+
* 3. Cache hit + activation_id present → proceed silently
|
|
11
|
+
* 4. Cache miss → call Polar /activate (counts against 3-machine limit)
|
|
12
|
+
* 5. Success → cache activation_id + proceed
|
|
13
|
+
* 6. Limit reached → clear error with deactivation instructions
|
|
14
|
+
* 7. Invalid key → throw with purchase link
|
|
13
15
|
*
|
|
14
|
-
*
|
|
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
|
|
19
|
-
const path
|
|
20
|
-
const 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
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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;
|
|
65
|
+
if (Date.now() - entry.cached_at > CACHE_TTL_MS) return null;
|
|
62
66
|
return entry;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
const body = JSON.stringify({
|
|
69
|
-
key: licenceKey,
|
|
70
|
-
organization_id: POLAR_ORG_ID,
|
|
71
|
-
});
|
|
77
|
+
// ── Polar API calls ───────────────────────────────────────────────────────────
|
|
72
78
|
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
if (
|
|
95
|
-
return { valid: false, reason: `
|
|
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:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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().
|
|
112
|
-
*
|
|
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
|
|
206
|
+
* @param {string} licenceKey
|
|
115
207
|
* @returns {Promise<void>}
|
|
116
|
-
* @throws {Error} if key
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 —
|
|
223
|
+
// Cache miss or expired — activate this machine
|
|
146
224
|
let result;
|
|
147
225
|
try {
|
|
148
|
-
result = await
|
|
226
|
+
result = await _activateMachine(key);
|
|
149
227
|
} catch(e) {
|
|
150
|
-
// Network failure —
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
console.warn('[
|
|
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
|
-
'[
|
|
158
|
-
'Check your internet connection
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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('[
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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 — ${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 };
|