@origintrail-official/dkg-node-ui 0.0.1-dev.1773506972.23bf9c0

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.
Files changed (84) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +49 -0
  3. package/dist/api.d.ts +30 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +805 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/chat-assistant.d.ts +68 -0
  8. package/dist/chat-assistant.d.ts.map +1 -0
  9. package/dist/chat-assistant.js +663 -0
  10. package/dist/chat-assistant.js.map +1 -0
  11. package/dist/chat-memory.d.ts +171 -0
  12. package/dist/chat-memory.d.ts.map +1 -0
  13. package/dist/chat-memory.js +985 -0
  14. package/dist/chat-memory.js.map +1 -0
  15. package/dist/chat-persistence-queue.d.ts +67 -0
  16. package/dist/chat-persistence-queue.d.ts.map +1 -0
  17. package/dist/chat-persistence-queue.js +245 -0
  18. package/dist/chat-persistence-queue.js.map +1 -0
  19. package/dist/db.d.ts +402 -0
  20. package/dist/db.d.ts.map +1 -0
  21. package/dist/db.js +887 -0
  22. package/dist/db.js.map +1 -0
  23. package/dist/gelf-push-worker.d.ts +67 -0
  24. package/dist/gelf-push-worker.d.ts.map +1 -0
  25. package/dist/gelf-push-worker.js +147 -0
  26. package/dist/gelf-push-worker.js.map +1 -0
  27. package/dist/index.d.ts +20 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +12 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/llm/capability-resolver.d.ts +3 -0
  32. package/dist/llm/capability-resolver.d.ts.map +1 -0
  33. package/dist/llm/capability-resolver.js +21 -0
  34. package/dist/llm/capability-resolver.js.map +1 -0
  35. package/dist/llm/client.d.ts +23 -0
  36. package/dist/llm/client.d.ts.map +1 -0
  37. package/dist/llm/client.js +91 -0
  38. package/dist/llm/client.js.map +1 -0
  39. package/dist/llm/provider-adapter.d.ts +16 -0
  40. package/dist/llm/provider-adapter.d.ts.map +1 -0
  41. package/dist/llm/provider-adapter.js +199 -0
  42. package/dist/llm/provider-adapter.js.map +1 -0
  43. package/dist/llm/types.d.ts +64 -0
  44. package/dist/llm/types.d.ts.map +1 -0
  45. package/dist/llm/types.js +2 -0
  46. package/dist/llm/types.js.map +1 -0
  47. package/dist/metrics-collector.d.ts +36 -0
  48. package/dist/metrics-collector.d.ts.map +1 -0
  49. package/dist/metrics-collector.js +155 -0
  50. package/dist/metrics-collector.js.map +1 -0
  51. package/dist/operation-tracker.d.ts +43 -0
  52. package/dist/operation-tracker.d.ts.map +1 -0
  53. package/dist/operation-tracker.js +195 -0
  54. package/dist/operation-tracker.js.map +1 -0
  55. package/dist/structured-logger.d.ts +16 -0
  56. package/dist/structured-logger.d.ts.map +1 -0
  57. package/dist/structured-logger.js +41 -0
  58. package/dist/structured-logger.js.map +1 -0
  59. package/dist/telemetry.d.ts +35 -0
  60. package/dist/telemetry.d.ts.map +1 -0
  61. package/dist/telemetry.js +45 -0
  62. package/dist/telemetry.js.map +1 -0
  63. package/dist-ui/assets/3d-force-graph-nMUNmvtB.js +964 -0
  64. package/dist-ui/assets/AgentHub-XKCM9uYQ.js +65 -0
  65. package/dist-ui/assets/AppHost-DoLIi89g.js +1 -0
  66. package/dist-ui/assets/Apps-Cc8HfqfD.js +1 -0
  67. package/dist-ui/assets/Dashboard-D5q6MK78.js +2 -0
  68. package/dist-ui/assets/Explorer-B80RVksc.js +64 -0
  69. package/dist-ui/assets/N3Parser-Q_-1ZY5E.js +7 -0
  70. package/dist-ui/assets/Settings-CG7-7GM-.js +71 -0
  71. package/dist-ui/assets/hooks-BLTFNmyP.js +1 -0
  72. package/dist-ui/assets/index-8_35CUX2.js +192 -0
  73. package/dist-ui/assets/index-CKZq_ZB-.css +1 -0
  74. package/dist-ui/assets/index-DH-l6lM0.js +76 -0
  75. package/dist-ui/assets/jsonld-32FQRO67-DhbO8O6B.js +2 -0
  76. package/dist-ui/assets/jsonld-BFI4wECl.js +62 -0
  77. package/dist-ui/assets/ntriples-ZWBY2WET-nIpilpjf.js +1 -0
  78. package/dist-ui/assets/ordinal-DIohFSkg.js +1 -0
  79. package/dist-ui/assets/renderer-3d-2EVDZII7-DsxBsJvs.js +2 -0
  80. package/dist-ui/assets/three.module-uCjFke6H.js +4019 -0
  81. package/dist-ui/assets/turtle-JJPK7LJ5-zezDJZEp.js +1 -0
  82. package/dist-ui/favicon.png +0 -0
  83. package/dist-ui/index.html +14 -0
  84. package/package.json +58 -0
package/dist/api.js ADDED
@@ -0,0 +1,805 @@
1
+ import { join } from 'node:path';
2
+ import { createReadStream, existsSync } from 'node:fs';
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import { IMPORT_SOURCES } from './chat-memory.js';
5
+ import { ChatPersistenceQueue } from './chat-persistence-queue.js';
6
+ const MIME = {
7
+ '.html': 'text/html',
8
+ '.js': 'application/javascript',
9
+ '.css': 'text/css',
10
+ '.json': 'application/json',
11
+ '.svg': 'image/svg+xml',
12
+ '.png': 'image/png',
13
+ '.ico': 'image/x-icon',
14
+ '.woff2': 'font/woff2',
15
+ '.woff': 'font/woff',
16
+ };
17
+ const chatPersistenceQueues = new WeakMap();
18
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
19
+ const PERIOD_UNITS = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
20
+ function parsePeriodMs(input) {
21
+ const num = parseInt(input, 10);
22
+ if (!isNaN(num) && num > 0 && /^\d+$/.test(input))
23
+ return num;
24
+ const match = input.match(/^(\d+)\s*([mhdw])$/i);
25
+ if (match) {
26
+ const val = parseInt(match[1], 10);
27
+ const unit = match[2].toLowerCase();
28
+ if (val > 0 && PERIOD_UNITS[unit])
29
+ return val * PERIOD_UNITS[unit];
30
+ }
31
+ return 86_400_000;
32
+ }
33
+ function autoBucketMs(periodMs) {
34
+ if (periodMs <= 15 * 60_000)
35
+ return 60_000; // <=15min → 1-min buckets
36
+ if (periodMs <= 60 * 60_000)
37
+ return 5 * 60_000; // <=1h → 5-min buckets
38
+ if (periodMs <= 6 * 3_600_000)
39
+ return 15 * 60_000; // <=6h → 15-min buckets
40
+ if (periodMs <= 24 * 3_600_000)
41
+ return 3_600_000; // <=24h → 1-hour buckets
42
+ if (periodMs <= 7 * 86_400_000)
43
+ return 6 * 3_600_000; // <=7d → 6-hour buckets
44
+ return 86_400_000; // >7d → daily buckets
45
+ }
46
+ function getChatPersistenceQueue(db, memoryManager) {
47
+ let queue = chatPersistenceQueues.get(db);
48
+ if (!queue) {
49
+ queue = new ChatPersistenceQueue(db, memoryManager);
50
+ chatPersistenceQueues.set(db, queue);
51
+ }
52
+ return queue;
53
+ }
54
+ function normalizeSessionId(raw) {
55
+ if (typeof raw !== 'string')
56
+ return null;
57
+ const value = raw.trim();
58
+ if (!value)
59
+ return null;
60
+ return SESSION_ID_PATTERN.test(value) ? value : null;
61
+ }
62
+ function normalizeTurnId(raw) {
63
+ return normalizeSessionId(raw);
64
+ }
65
+ /**
66
+ * Handles all /api/metrics, /api/operations, /api/logs, /api/query-history,
67
+ * /api/saved-queries, and /ui routes. Returns true if the request was handled.
68
+ */
69
+ export async function handleNodeUIRequest(req, res, url, db, staticDir, chatAssistant, metricsCollector, authToken, memoryManager, llmSettings, telemetrySettings) {
70
+ const path = url.pathname;
71
+ // --- Metrics ---
72
+ if (req.method === 'GET' && path === '/api/metrics') {
73
+ if (metricsCollector) {
74
+ try {
75
+ const live = await metricsCollector.collect();
76
+ return json(res, 200, live);
77
+ }
78
+ catch {
79
+ const snap = db.getLatestSnapshot();
80
+ return json(res, 200, snap ?? {});
81
+ }
82
+ }
83
+ const snap = db.getLatestSnapshot();
84
+ return json(res, 200, snap ?? {});
85
+ }
86
+ if (req.method === 'GET' && path === '/api/metrics/history') {
87
+ const from = parseInt(url.searchParams.get('from') ?? '0', 10) || (Date.now() - 86_400_000);
88
+ const to = parseInt(url.searchParams.get('to') ?? '0', 10) || Date.now();
89
+ const maxPoints = parseInt(url.searchParams.get('maxPoints') ?? '500', 10);
90
+ const snapshots = db.getSnapshotHistory(from, to, maxPoints);
91
+ return json(res, 200, { snapshots });
92
+ }
93
+ // --- Prometheus metrics ---
94
+ // TODO: Prometheus /metrics endpoint — implementation in progress, hidden until ready
95
+ // if (req.method === 'GET' && path === '/metrics') {
96
+ // if (metricsCollector) {
97
+ // try {
98
+ // const m = await metricsCollector.collect();
99
+ // const lines = [
100
+ // `# HELP dkg_uptime_seconds Node uptime in seconds`,
101
+ // `# TYPE dkg_uptime_seconds gauge`,
102
+ // `dkg_uptime_seconds ${m.uptime_seconds ?? 0}`,
103
+ // `# HELP dkg_cpu_percent CPU usage percentage`,
104
+ // `# TYPE dkg_cpu_percent gauge`,
105
+ // `dkg_cpu_percent ${m.cpu_percent ?? 0}`,
106
+ // `# HELP dkg_memory_bytes Memory usage in bytes`,
107
+ // `# TYPE dkg_memory_bytes gauge`,
108
+ // `dkg_memory_bytes{type="heap"} ${m.heap_used_bytes ?? 0}`,
109
+ // `dkg_memory_bytes{type="system"} ${m.mem_used_bytes ?? 0}`,
110
+ // `# HELP dkg_peers_total Number of connected peers`,
111
+ // `# TYPE dkg_peers_total gauge`,
112
+ // `dkg_peers_total{type="direct"} ${m.direct_peers ?? 0}`,
113
+ // `dkg_peers_total{type="relayed"} ${m.relayed_peers ?? 0}`,
114
+ // `dkg_peers_total{type="mesh"} ${m.mesh_peers ?? 0}`,
115
+ // `# HELP dkg_triples_total Total triples in the store`,
116
+ // `# TYPE dkg_triples_total gauge`,
117
+ // `dkg_triples_total ${m.total_triples ?? 0}`,
118
+ // `# HELP dkg_kcs_total Knowledge collections`,
119
+ // `# TYPE dkg_kcs_total gauge`,
120
+ // `dkg_kcs_total{status="confirmed"} ${m.confirmed_kcs ?? 0}`,
121
+ // `dkg_kcs_total{status="tentative"} ${m.tentative_kcs ?? 0}`,
122
+ // `# HELP dkg_kas_total Knowledge assets`,
123
+ // `# TYPE dkg_kas_total gauge`,
124
+ // `dkg_kas_total ${m.total_kas ?? 0}`,
125
+ // `# HELP dkg_store_bytes Triple store size in bytes`,
126
+ // `# TYPE dkg_store_bytes gauge`,
127
+ // `dkg_store_bytes ${m.store_bytes ?? 0}`,
128
+ // `# HELP dkg_rpc_latency_ms RPC latency in milliseconds`,
129
+ // `# TYPE dkg_rpc_latency_ms gauge`,
130
+ // `dkg_rpc_latency_ms ${m.rpc_latency_ms ?? 0}`,
131
+ // `# HELP dkg_rpc_healthy RPC health status (1=healthy, 0=unhealthy)`,
132
+ // `# TYPE dkg_rpc_healthy gauge`,
133
+ // `dkg_rpc_healthy ${m.rpc_healthy ?? 0}`,
134
+ // '',
135
+ // ];
136
+ // res.writeHead(200, {
137
+ // 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
138
+ // 'Cache-Control': 'no-cache',
139
+ // });
140
+ // res.end(lines.join('\n'));
141
+ // return true;
142
+ // } catch {
143
+ // res.writeHead(503, { 'Content-Type': 'text/plain' });
144
+ // res.end('# metrics temporarily unavailable\n');
145
+ // return true;
146
+ // }
147
+ // }
148
+ // res.writeHead(503, { 'Content-Type': 'text/plain' });
149
+ // res.end('# metrics collector not initialized\n');
150
+ // return true;
151
+ // }
152
+ // --- Error hotspots ---
153
+ if (req.method === 'GET' && path === '/api/error-hotspots') {
154
+ const periodMs = parseInt(url.searchParams.get('periodMs') ?? String(7 * 86_400_000), 10);
155
+ const hotspots = db.getErrorHotspots(periodMs);
156
+ return json(res, 200, { hotspots });
157
+ }
158
+ if (req.method === 'GET' && path === '/api/failed-operations') {
159
+ const phase = url.searchParams.get('phase') ?? undefined;
160
+ const operationName = url.searchParams.get('operationName') ?? undefined;
161
+ const periodMs = parseInt(url.searchParams.get('periodMs') ?? String(7 * 86_400_000), 10);
162
+ const q = url.searchParams.get('q') ?? undefined;
163
+ const limit = parseInt(url.searchParams.get('limit') ?? '50', 10);
164
+ const result = db.getFailedOperations({ phase, operationName, periodMs, q, limit });
165
+ return json(res, 200, result);
166
+ }
167
+ // --- Operations ---
168
+ if (req.method === 'GET' && path === '/api/operations') {
169
+ const name = url.searchParams.get('name') ?? undefined;
170
+ const status = url.searchParams.get('status') ?? undefined;
171
+ const operationId = url.searchParams.get('operationId') ?? undefined;
172
+ const from = url.searchParams.get('from') ? parseInt(url.searchParams.get('from'), 10) : undefined;
173
+ const to = url.searchParams.get('to') ? parseInt(url.searchParams.get('to'), 10) : undefined;
174
+ const limit = parseInt(url.searchParams.get('limit') ?? '100', 10);
175
+ const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
176
+ const includePhases = url.searchParams.get('phases') === '1';
177
+ if (includePhases) {
178
+ const result = db.getOperationsWithPhases({ name, status, operationId, from, to, limit, offset });
179
+ return json(res, 200, result);
180
+ }
181
+ const result = db.getOperations({ name, status, operationId, from, to, limit, offset });
182
+ return json(res, 200, result);
183
+ }
184
+ if (req.method === 'GET' && path.startsWith('/api/operations/')) {
185
+ const opId = path.slice('/api/operations/'.length);
186
+ if (!opId)
187
+ return json(res, 400, { error: 'Missing operation ID' });
188
+ const result = db.getOperation(opId);
189
+ if (!result.operation)
190
+ return json(res, 404, { error: 'Operation not found' });
191
+ return json(res, 200, result);
192
+ }
193
+ // --- Operation stats ---
194
+ if (req.method === 'GET' && path === '/api/operation-stats') {
195
+ const name = url.searchParams.get('name') ?? undefined;
196
+ const periodMs = parsePeriodMs(url.searchParams.get('periodMs') ?? url.searchParams.get('period') ?? '24h');
197
+ const bucketMs = autoBucketMs(periodMs);
198
+ const result = db.getOperationStats({ name, periodMs, bucketMs });
199
+ return json(res, 200, result);
200
+ }
201
+ // --- Success rates by operation type ---
202
+ if (req.method === 'GET' && path === '/api/success-rates') {
203
+ const periodMs = parsePeriodMs(url.searchParams.get('periodMs') ?? url.searchParams.get('period') ?? '7d');
204
+ const rates = db.getSuccessRatesByType(periodMs);
205
+ return json(res, 200, { rates });
206
+ }
207
+ // --- Per-type time series ---
208
+ if (req.method === 'GET' && path === '/api/per-type-stats') {
209
+ const periodMs = parsePeriodMs(url.searchParams.get('periodMs') ?? url.searchParams.get('period') ?? '7d');
210
+ const rawBucket = url.searchParams.get('bucketMs');
211
+ const bucketMs = rawBucket ? parseInt(rawBucket, 10) : autoBucketMs(periodMs);
212
+ const result = db.getPerTypeTimeSeries({ periodMs, bucketMs });
213
+ return json(res, 200, result);
214
+ }
215
+ // --- Spending / Economics ---
216
+ if (req.method === 'GET' && path === '/api/economics') {
217
+ const spending = db.getSpendingSummary();
218
+ return json(res, 200, spending);
219
+ }
220
+ // --- Logs ---
221
+ if (req.method === 'GET' && path === '/api/logs') {
222
+ const q = url.searchParams.get('q') ?? undefined;
223
+ const operationId = url.searchParams.get('operationId') ?? undefined;
224
+ const level = url.searchParams.get('level') ?? undefined;
225
+ const module = url.searchParams.get('module') ?? undefined;
226
+ const from = url.searchParams.get('from') ? parseInt(url.searchParams.get('from'), 10) : undefined;
227
+ const to = url.searchParams.get('to') ? parseInt(url.searchParams.get('to'), 10) : undefined;
228
+ const limit = parseInt(url.searchParams.get('limit') ?? '200', 10);
229
+ const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
230
+ const result = db.searchLogs({ q, operationId, level, module, from, to, limit, offset });
231
+ return json(res, 200, result);
232
+ }
233
+ // --- Node log (daemon.log file) ---
234
+ if (req.method === 'GET' && path === '/api/node-log') {
235
+ const logFilePath = join(db.dataDir, 'daemon.log');
236
+ const rawLines = parseInt(url.searchParams.get('lines') ?? '500', 10);
237
+ const tailLines = Number.isFinite(rawLines) && rawLines > 0 ? Math.min(rawLines, 5000) : 500;
238
+ const search = url.searchParams.get('q') ?? '';
239
+ try {
240
+ const fileStat = await stat(logFilePath);
241
+ const TAIL_BYTES = Math.min(tailLines * 300, fileStat.size);
242
+ const { createReadStream } = await import('node:fs');
243
+ const start = Math.max(0, fileStat.size - TAIL_BYTES);
244
+ const chunk = await new Promise((resolve, reject) => {
245
+ const parts = [];
246
+ createReadStream(logFilePath, { start, encoding: 'utf-8' })
247
+ .on('data', (d) => parts.push(typeof d === 'string' ? d : d.toString('utf-8')))
248
+ .on('end', () => resolve(parts.join('')))
249
+ .on('error', reject);
250
+ });
251
+ let lines = chunk.split('\n');
252
+ if (start > 0)
253
+ lines = lines.slice(1);
254
+ lines = lines.slice(-tailLines);
255
+ if (search) {
256
+ const lower = search.toLowerCase();
257
+ lines = lines.filter(l => l.toLowerCase().includes(lower));
258
+ }
259
+ return json(res, 200, { lines, totalSize: fileStat.size });
260
+ }
261
+ catch {
262
+ return json(res, 200, { lines: [], totalSize: 0 });
263
+ }
264
+ }
265
+ // --- Query history ---
266
+ if (req.method === 'GET' && path === '/api/query-history') {
267
+ const limit = parseInt(url.searchParams.get('limit') ?? '50', 10);
268
+ const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
269
+ const history = db.getQueryHistory(limit, offset);
270
+ return json(res, 200, { history });
271
+ }
272
+ // --- Saved queries ---
273
+ if (req.method === 'GET' && path === '/api/saved-queries') {
274
+ const queries = db.getSavedQueries();
275
+ return json(res, 200, { queries });
276
+ }
277
+ if (req.method === 'POST' && path === '/api/saved-queries') {
278
+ const body = await readBody(req);
279
+ const { name, description, sparql } = JSON.parse(body);
280
+ if (!name || !sparql)
281
+ return json(res, 400, { error: 'Missing "name" or "sparql"' });
282
+ const id = db.insertSavedQuery({ name, description, sparql });
283
+ return json(res, 201, { id });
284
+ }
285
+ if (req.method === 'PUT' && path.startsWith('/api/saved-queries/')) {
286
+ const id = parseInt(path.slice('/api/saved-queries/'.length), 10);
287
+ if (isNaN(id))
288
+ return json(res, 400, { error: 'Invalid ID' });
289
+ const body = await readBody(req);
290
+ const updates = JSON.parse(body);
291
+ db.updateSavedQuery(id, updates);
292
+ return json(res, 200, { ok: true });
293
+ }
294
+ if (req.method === 'DELETE' && path.startsWith('/api/saved-queries/')) {
295
+ const id = parseInt(path.slice('/api/saved-queries/'.length), 10);
296
+ if (isNaN(id))
297
+ return json(res, 400, { error: 'Invalid ID' });
298
+ db.deleteSavedQuery(id);
299
+ return json(res, 200, { ok: true });
300
+ }
301
+ // --- Data retention settings ---
302
+ if (req.method === 'GET' && path === '/api/settings/retention') {
303
+ return json(res, 200, { retentionDays: db.getRetentionDays() });
304
+ }
305
+ if (req.method === 'PUT' && path === '/api/settings/retention') {
306
+ const body = await readBody(req);
307
+ const payload = JSON.parse(body ?? '{}');
308
+ const days = payload.retentionDays;
309
+ if (days == null || !Number.isInteger(days) || days < 1 || days > 365) {
310
+ return json(res, 400, { error: 'retentionDays must be an integer 1-365' });
311
+ }
312
+ db.setRetentionDays(days);
313
+ db.prune();
314
+ return json(res, 200, { ok: true, retentionDays: db.getRetentionDays() });
315
+ }
316
+ // --- Telemetry settings ---
317
+ if (req.method === 'GET' && path === '/api/settings/telemetry') {
318
+ const enabled = telemetrySettings?.getTelemetryEnabled() ?? false;
319
+ return json(res, 200, { enabled });
320
+ }
321
+ if (req.method === 'PUT' && path === '/api/settings/telemetry') {
322
+ if (!telemetrySettings)
323
+ return json(res, 501, { error: 'Telemetry not available' });
324
+ const body = await readBody(req);
325
+ const payload = JSON.parse(body ?? '{}');
326
+ if (typeof payload.enabled !== 'boolean') {
327
+ return json(res, 400, { error: 'enabled must be a boolean' });
328
+ }
329
+ const result = await telemetrySettings.setTelemetryEnabled(payload.enabled);
330
+ if (!result.ok)
331
+ return json(res, 422, { error: result.error });
332
+ return json(res, 200, { ok: true, enabled: payload.enabled });
333
+ }
334
+ // --- LLM settings ---
335
+ if (req.method === 'GET' && path === '/api/settings/llm') {
336
+ if (chatAssistant) {
337
+ const info = chatAssistant.getLlmConfig();
338
+ return json(res, 200, info);
339
+ }
340
+ return json(res, 200, { configured: false });
341
+ }
342
+ if (req.method === 'PUT' && path === '/api/settings/llm' && llmSettings) {
343
+ const body = await readBody(req);
344
+ const payload = JSON.parse(body ?? '{}');
345
+ const incomingApiKey = typeof payload.apiKey === 'string' ? payload.apiKey : undefined;
346
+ const incomingModel = typeof payload.model === 'string' ? payload.model.trim() : undefined;
347
+ const incomingBaseURL = typeof payload.baseURL === 'string' ? payload.baseURL.trim() : undefined;
348
+ const clearRequested = payload.clear === true;
349
+ // Backward compatibility: old clients send { apiKey: "" } to clear.
350
+ const legacyClearRequested = incomingApiKey === '' &&
351
+ payload.model === undefined &&
352
+ payload.baseURL === undefined;
353
+ let llm;
354
+ if (clearRequested || legacyClearRequested) {
355
+ llm = null;
356
+ }
357
+ else {
358
+ const current = llmSettings.getLlm();
359
+ const currentApiKey = current?.apiKey?.trim();
360
+ const nextApiKey = incomingApiKey?.trim() || currentApiKey;
361
+ if (!nextApiKey) {
362
+ return json(res, 400, {
363
+ error: 'Missing API key. Provide "apiKey" or clear the config explicitly.',
364
+ });
365
+ }
366
+ llm = {
367
+ apiKey: nextApiKey,
368
+ model: payload.model !== undefined ? (incomingModel || undefined) : current?.model,
369
+ baseURL: payload.baseURL !== undefined ? (incomingBaseURL || undefined) : current?.baseURL,
370
+ };
371
+ }
372
+ try {
373
+ await llmSettings.setLlm(llm);
374
+ const info = chatAssistant?.getLlmConfig() ?? { configured: !!llm };
375
+ return json(res, 200, { ok: true, ...info });
376
+ }
377
+ catch (err) {
378
+ return json(res, 500, { error: err.message ?? 'Failed to save LLM config' });
379
+ }
380
+ }
381
+ // --- Chat assistant ---
382
+ if (req.method === 'GET' && path === '/api/chat-assistant/persistence/events' && memoryManager) {
383
+ const queue = getChatPersistenceQueue(db, memoryManager);
384
+ beginSse(res);
385
+ const now = Date.now();
386
+ sendSse(res, {
387
+ type: 'persist_health',
388
+ ts: now,
389
+ ...queue.getHealthSnapshot(now),
390
+ });
391
+ const unsubscribe = queue.subscribe((event) => {
392
+ sendSse(res, event);
393
+ });
394
+ const keepAlive = setInterval(() => {
395
+ if (res.writableEnded || res.destroyed)
396
+ return;
397
+ res.write(': ping\n\n');
398
+ }, 15_000);
399
+ const cleanup = () => {
400
+ clearInterval(keepAlive);
401
+ unsubscribe();
402
+ if (!res.writableEnded) {
403
+ res.end();
404
+ }
405
+ };
406
+ req.on('close', cleanup);
407
+ req.on('error', cleanup);
408
+ return true;
409
+ }
410
+ if (req.method === 'GET' && path === '/api/chat-assistant/persistence/health' && memoryManager) {
411
+ const queue = getChatPersistenceQueue(db, memoryManager);
412
+ const now = Date.now();
413
+ return json(res, 200, {
414
+ ts: now,
415
+ ...queue.getHealthSnapshot(now),
416
+ });
417
+ }
418
+ if (req.method === 'POST' && path === '/api/chat-assistant' && chatAssistant) {
419
+ const body = await readBody(req);
420
+ let payload;
421
+ try {
422
+ payload = JSON.parse(body ?? '{}');
423
+ }
424
+ catch {
425
+ return json(res, 400, { error: 'Invalid JSON payload' });
426
+ }
427
+ const message = typeof payload.message === 'string' ? payload.message.trim() : '';
428
+ const rawSessionId = payload.sessionId;
429
+ const acceptHeader = Array.isArray(req.headers.accept) ? req.headers.accept.join(', ') : (req.headers.accept ?? '');
430
+ const streamRequested = payload.stream === true || acceptHeader.includes('text/event-stream');
431
+ if (!message)
432
+ return json(res, 400, { error: 'Missing "message"' });
433
+ const providedSessionId = rawSessionId === undefined ? null : normalizeSessionId(rawSessionId);
434
+ if (rawSessionId !== undefined && !providedSessionId) {
435
+ return json(res, 400, { error: 'Invalid "sessionId" format' });
436
+ }
437
+ const sessionId = providedSessionId ?? crypto.randomUUID();
438
+ const turnId = crypto.randomUUID();
439
+ if (streamRequested) {
440
+ beginSse(res);
441
+ sendSse(res, { type: 'meta', sessionId });
442
+ const startedAt = Date.now();
443
+ const llmStartedAt = Date.now();
444
+ let finalReply;
445
+ try {
446
+ for await (const event of chatAssistant.answerStream({ message })) {
447
+ if (event.type === 'text_delta') {
448
+ sendSse(res, event);
449
+ continue;
450
+ }
451
+ if (event.type === 'final') {
452
+ finalReply = event.response;
453
+ }
454
+ }
455
+ if (!finalReply)
456
+ throw new Error('Chat stream ended without a final response');
457
+ const llmMs = Date.now() - llmStartedAt;
458
+ const persisted = enqueueTurnPersistence(db, memoryManager, {
459
+ turnId,
460
+ sessionId,
461
+ userMessage: message,
462
+ assistantReply: finalReply.reply,
463
+ toolCalls: finalReply.toolCalls,
464
+ });
465
+ const totalMs = Date.now() - startedAt;
466
+ const responseMode = finalReply.responseMode ?? 'rule-based';
467
+ sendSse(res, {
468
+ type: 'final',
469
+ ...finalReply,
470
+ responseMode,
471
+ sessionId,
472
+ turnId,
473
+ persistStatus: persisted.persistStatus,
474
+ persistError: persisted.persistError,
475
+ timings: {
476
+ llm_ms: llmMs,
477
+ store_ms: persisted.storeMs,
478
+ total_ms: totalMs,
479
+ },
480
+ });
481
+ }
482
+ catch (err) {
483
+ const messageText = err instanceof Error ? err.message : String(err);
484
+ const diagnostics = finalReply?.llmDiagnostics;
485
+ console.error('[chat-assistant] Streaming error:', messageText);
486
+ sendSse(res, {
487
+ type: 'error',
488
+ error: messageText,
489
+ llmDiagnostics: diagnostics,
490
+ });
491
+ }
492
+ finally {
493
+ res.end();
494
+ }
495
+ return true;
496
+ }
497
+ try {
498
+ const startedAt = Date.now();
499
+ const llmStartedAt = Date.now();
500
+ const reply = await chatAssistant.answer({ message });
501
+ const llmMs = Date.now() - llmStartedAt;
502
+ const persisted = enqueueTurnPersistence(db, memoryManager, {
503
+ turnId,
504
+ sessionId,
505
+ userMessage: message,
506
+ assistantReply: reply.reply,
507
+ toolCalls: reply.toolCalls,
508
+ });
509
+ const totalMs = Date.now() - startedAt;
510
+ const responseMode = reply.responseMode ?? 'rule-based';
511
+ return json(res, 200, {
512
+ ...reply,
513
+ responseMode,
514
+ sessionId,
515
+ turnId,
516
+ persistStatus: persisted.persistStatus,
517
+ persistError: persisted.persistError,
518
+ timings: {
519
+ llm_ms: llmMs,
520
+ store_ms: persisted.storeMs,
521
+ total_ms: totalMs,
522
+ },
523
+ });
524
+ }
525
+ catch (err) {
526
+ return json(res, 500, { error: err.message });
527
+ }
528
+ }
529
+ // --- Memory (chat history stored in DKG) ---
530
+ if (req.method === 'GET' && path === '/api/memory/sessions' && memoryManager) {
531
+ const rawLimit = parseInt(url.searchParams.get('limit') ?? '20', 10);
532
+ const limit = Math.max(1, Math.min(isNaN(rawLimit) ? 20 : rawLimit, 100));
533
+ try {
534
+ const sessions = await memoryManager.getRecentChats(limit);
535
+ return json(res, 200, { sessions });
536
+ }
537
+ catch (err) {
538
+ return json(res, 500, { error: err.message ?? 'Failed to fetch sessions' });
539
+ }
540
+ }
541
+ if (req.method === 'GET' && path.startsWith('/api/memory/sessions/') && path.endsWith('/graph-delta') && memoryManager) {
542
+ const sessionId = normalizeSessionId(decodeURIComponent(path.slice('/api/memory/sessions/'.length, -'/graph-delta'.length)));
543
+ if (!sessionId)
544
+ return json(res, 400, { error: 'Invalid session ID' });
545
+ const turnId = normalizeTurnId(url.searchParams.get('turnId'));
546
+ if (!turnId)
547
+ return json(res, 400, { error: 'Missing or invalid "turnId"' });
548
+ const rawBaseTurnId = url.searchParams.get('baseTurnId');
549
+ const baseTurnId = rawBaseTurnId == null || rawBaseTurnId === ''
550
+ ? null
551
+ : normalizeTurnId(rawBaseTurnId);
552
+ if (rawBaseTurnId != null && rawBaseTurnId !== '' && !baseTurnId) {
553
+ return json(res, 400, { error: 'Invalid "baseTurnId" format' });
554
+ }
555
+ try {
556
+ const delta = await memoryManager.getSessionGraphDelta(sessionId, turnId, { baseTurnId });
557
+ return json(res, 200, delta);
558
+ }
559
+ catch (err) {
560
+ return json(res, 500, { error: err.message ?? 'Failed to fetch session graph delta' });
561
+ }
562
+ }
563
+ if (req.method === 'GET' &&
564
+ path.startsWith('/api/memory/sessions/') &&
565
+ !path.endsWith('/graph-delta') &&
566
+ !path.endsWith('/publication') &&
567
+ !path.endsWith('/publish') &&
568
+ memoryManager) {
569
+ const sessionId = normalizeSessionId(decodeURIComponent(path.slice('/api/memory/sessions/'.length)));
570
+ if (!sessionId)
571
+ return json(res, 400, { error: 'Invalid session ID' });
572
+ try {
573
+ const session = await memoryManager.getSession(sessionId);
574
+ if (!session)
575
+ return json(res, 404, { error: 'Session not found' });
576
+ return json(res, 200, session);
577
+ }
578
+ catch (err) {
579
+ return json(res, 500, { error: err.message ?? 'Failed to fetch session' });
580
+ }
581
+ }
582
+ if (req.method === 'GET' && path.startsWith('/api/memory/sessions/') && path.endsWith('/publication') && memoryManager) {
583
+ const sessionId = normalizeSessionId(decodeURIComponent(path.slice('/api/memory/sessions/'.length, -'/publication'.length)));
584
+ if (!sessionId)
585
+ return json(res, 400, { error: 'Invalid session ID' });
586
+ try {
587
+ const status = await memoryManager.getSessionPublicationStatus(sessionId);
588
+ return json(res, 200, status);
589
+ }
590
+ catch (err) {
591
+ return json(res, 500, { error: err.message ?? 'Failed to fetch session publication status' });
592
+ }
593
+ }
594
+ if (req.method === 'POST' && path.startsWith('/api/memory/sessions/') && path.endsWith('/publish') && memoryManager) {
595
+ const sessionId = normalizeSessionId(decodeURIComponent(path.slice('/api/memory/sessions/'.length, -'/publish'.length)));
596
+ if (!sessionId)
597
+ return json(res, 400, { error: 'Invalid session ID' });
598
+ const body = await readBody(req);
599
+ let payload;
600
+ try {
601
+ payload = JSON.parse(body || '{}');
602
+ }
603
+ catch {
604
+ return json(res, 400, { error: 'Invalid JSON payload' });
605
+ }
606
+ const rootEntities = Array.isArray(payload.rootEntities)
607
+ ? payload.rootEntities.filter((r) => typeof r === 'string')
608
+ : undefined;
609
+ const clearWorkspaceAfter = payload.clearAfter === true;
610
+ try {
611
+ const result = await memoryManager.publishSession(sessionId, {
612
+ rootEntities,
613
+ clearWorkspaceAfter,
614
+ });
615
+ return json(res, 200, result);
616
+ }
617
+ catch (err) {
618
+ const message = err?.message ?? 'Failed to publish session';
619
+ const status = /No workspace entities found|Selected root entities/.test(message) ? 400 : 500;
620
+ return json(res, status, { error: message });
621
+ }
622
+ }
623
+ if (req.method === 'POST' && path === '/api/memory/import' && memoryManager) {
624
+ let body;
625
+ try {
626
+ body = await readBody(req, IMPORT_MAX_BYTES);
627
+ }
628
+ catch (err) {
629
+ if (err instanceof PayloadTooLargeError)
630
+ return json(res, 413, { error: 'Payload too large' });
631
+ throw err;
632
+ }
633
+ let parsed;
634
+ try {
635
+ parsed = JSON.parse(body);
636
+ }
637
+ catch {
638
+ return json(res, 400, { error: 'Invalid JSON body' });
639
+ }
640
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
641
+ return json(res, 400, { error: 'Request body must be a JSON object' });
642
+ }
643
+ const { text, source, useLlm } = parsed;
644
+ if (!text || typeof text !== 'string' || text.trim().length === 0) {
645
+ return json(res, 400, { error: 'Missing or empty "text" field' });
646
+ }
647
+ const importSource = IMPORT_SOURCES.includes(source) ? source : 'other';
648
+ try {
649
+ const result = await memoryManager.importMemories(text.trim(), importSource, { useLlm: useLlm === true });
650
+ return json(res, 200, result);
651
+ }
652
+ catch (err) {
653
+ console.error('[node-ui] Import memories failed:', err);
654
+ return json(res, 500, { error: 'Failed to import memories' });
655
+ }
656
+ }
657
+ if (req.method === 'GET' && path === '/api/memory/stats' && memoryManager) {
658
+ try {
659
+ const stats = await memoryManager.getStats();
660
+ return json(res, 200, stats);
661
+ }
662
+ catch (err) {
663
+ return json(res, 200, { paranetId: 'agent-memory', initialized: false, messageCount: 0, knowledgeTriples: 0, totalTriples: 0, sessionCount: 0, entityCount: 0 });
664
+ }
665
+ }
666
+ // --- Notifications ---
667
+ if (req.method === 'GET' && path === '/api/notifications') {
668
+ const since = url.searchParams.get('since');
669
+ const limit = url.searchParams.get('limit');
670
+ const data = db.getNotifications({
671
+ since: since ? Number(since) : undefined,
672
+ limit: limit ? Number(limit) : undefined,
673
+ });
674
+ return json(res, 200, data);
675
+ }
676
+ if (req.method === 'POST' && path === '/api/notifications/read') {
677
+ const body = await readBody(req);
678
+ let ids;
679
+ try {
680
+ const parsed = JSON.parse(body);
681
+ if (Array.isArray(parsed.ids))
682
+ ids = parsed.ids.map(Number);
683
+ }
684
+ catch { /* mark all */ }
685
+ const count = db.markNotificationsRead(ids);
686
+ return json(res, 200, { marked: count });
687
+ }
688
+ // --- Static UI files ---
689
+ if (path === '/ui' || path.startsWith('/ui/')) {
690
+ return serveStatic(res, staticDir, path, authToken);
691
+ }
692
+ return false;
693
+ }
694
+ function enqueueTurnPersistence(db, memoryManager, job) {
695
+ if (!memoryManager) {
696
+ return { persistStatus: 'skipped', storeMs: 0 };
697
+ }
698
+ try {
699
+ const queue = getChatPersistenceQueue(db, memoryManager);
700
+ const snapshot = queue.enqueue(job);
701
+ return {
702
+ persistStatus: snapshot.status,
703
+ persistError: snapshot.error,
704
+ storeMs: snapshot.storeMs ?? 0,
705
+ };
706
+ }
707
+ catch (err) {
708
+ const message = err instanceof Error ? err.message : String(err);
709
+ console.error('[chat-persistence] enqueue failed:', message);
710
+ return { persistStatus: 'failed', persistError: message, storeMs: 0 };
711
+ }
712
+ }
713
+ function beginSse(res) {
714
+ res.writeHead(200, {
715
+ 'Content-Type': 'text/event-stream; charset=utf-8',
716
+ 'Cache-Control': 'no-cache, no-transform',
717
+ 'Connection': 'keep-alive',
718
+ 'X-Accel-Buffering': 'no',
719
+ 'Access-Control-Allow-Origin': '*',
720
+ });
721
+ }
722
+ function sendSse(res, data) {
723
+ if (res.writableEnded || res.destroyed)
724
+ return;
725
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
726
+ }
727
+ async function serveStatic(res, staticDir, urlPath, authToken) {
728
+ let filePath = urlPath === '/ui' || urlPath === '/ui/'
729
+ ? join(staticDir, 'index.html')
730
+ : join(staticDir, urlPath.slice('/ui/'.length));
731
+ // SPA fallback: if not a file with extension, serve index.html
732
+ const ext = filePath.slice(filePath.lastIndexOf('.'));
733
+ if (!MIME[ext]) {
734
+ filePath = join(staticDir, 'index.html');
735
+ }
736
+ if (!existsSync(filePath)) {
737
+ filePath = join(staticDir, 'index.html');
738
+ }
739
+ const mimeExt = filePath.slice(filePath.lastIndexOf('.'));
740
+ const isHtml = mimeExt === '.html';
741
+ try {
742
+ if (isHtml && authToken) {
743
+ const html = await readFile(filePath, 'utf-8');
744
+ const injection = `<script>window.__DKG_TOKEN__=${JSON.stringify(authToken)}</script>`;
745
+ const injected = html.replace('</head>', `${injection}</head>`);
746
+ const buf = Buffer.from(injected, 'utf-8');
747
+ res.writeHead(200, {
748
+ 'Content-Type': 'text/html',
749
+ 'Content-Length': buf.byteLength,
750
+ 'Cache-Control': 'no-cache',
751
+ });
752
+ res.end(buf);
753
+ }
754
+ else {
755
+ const s = await stat(filePath);
756
+ const contentType = MIME[mimeExt] ?? 'application/octet-stream';
757
+ res.writeHead(200, {
758
+ 'Content-Type': contentType,
759
+ 'Content-Length': s.size,
760
+ 'Cache-Control': isHtml ? 'no-cache' : 'public, max-age=31536000, immutable',
761
+ });
762
+ createReadStream(filePath).pipe(res);
763
+ }
764
+ }
765
+ catch {
766
+ res.writeHead(200, { 'Content-Type': 'text/html' });
767
+ res.end('<!DOCTYPE html><html><body><h1>Node UI not built</h1><p>Run <code>pnpm build:ui</code> in @origintrail-official/dkg-node-ui</p></body></html>');
768
+ }
769
+ return true;
770
+ }
771
+ function json(res, status, data) {
772
+ res.writeHead(status, {
773
+ 'Content-Type': 'application/json',
774
+ 'Access-Control-Allow-Origin': '*',
775
+ });
776
+ res.end(JSON.stringify(data));
777
+ return true;
778
+ }
779
+ function readBody(req, maxBytes) {
780
+ return new Promise((resolve, reject) => {
781
+ const chunks = [];
782
+ let totalBytes = 0;
783
+ let rejected = false;
784
+ req.on('data', (c) => {
785
+ if (rejected)
786
+ return;
787
+ totalBytes += c.length;
788
+ if (maxBytes != null && totalBytes > maxBytes) {
789
+ rejected = true;
790
+ reject(new PayloadTooLargeError());
791
+ req.resume();
792
+ return;
793
+ }
794
+ chunks.push(c);
795
+ });
796
+ req.on('end', () => { if (!rejected)
797
+ resolve(Buffer.concat(chunks).toString()); });
798
+ req.on('error', reject);
799
+ });
800
+ }
801
+ const IMPORT_MAX_BYTES = 2 * 1024 * 1024; // 2 MB
802
+ class PayloadTooLargeError extends Error {
803
+ constructor() { super('Payload too large'); }
804
+ }
805
+ //# sourceMappingURL=api.js.map