persyst-mcp 2.2.5 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -114
- package/bin/export.js +4 -4
- package/bin/extract.js +8 -8
- package/bin/import.js +15 -15
- package/bin/init.js +185 -38
- package/bin/mcp.js +3 -0
- package/bin/monitor.js +511 -0
- package/bin/setup.js +9 -9
- package/index.js +31 -11
- package/package.json +10 -11
- package/src/attestation.js +49 -28
- package/src/cache.js +3 -1
- package/src/database.js +227 -34
- package/src/embeddings.js +4 -2
- package/src/events.js +2 -0
- package/src/extractor-heuristic.js +5 -2
- package/src/sdk.js +4 -3
- package/src/search.js +55 -84
- package/src/server.js +884 -723
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +52 -0
- package/src/tools.js +98 -53
- package/src/watcher.js +157 -49
package/src/server.js
CHANGED
|
@@ -1,723 +1,884 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server.js — MCP Server, Local HTTP Gateway & Swarm Hub
|
|
3
|
-
*
|
|
4
|
-
* Creates the MCP server, registers all tools, and connects via stdio.
|
|
5
|
-
* Also runs a local HTTP/JSON Gateway on port 4321 (configurable) to support:
|
|
6
|
-
* - Agentic swarms without subprocess overhead
|
|
7
|
-
* - IDE context injection via /system-prompt
|
|
8
|
-
* - Real-time event streaming via SSE (/events)
|
|
9
|
-
* - Batch operations for high-throughput swarm agents
|
|
10
|
-
* - Optional API key authentication for remote/multi-host setups
|
|
11
|
-
*
|
|
12
|
-
* Environment variables:
|
|
13
|
-
* PORT — HTTP gateway port (default: 4321)
|
|
14
|
-
* PERSYST_HOST — Bind address (default: 127.0.0.1, use 0.0.0.0 for Docker/remote)
|
|
15
|
-
* PERSYST_API_KEY — Optional auth token. If set, all endpoints (except /health) require
|
|
16
|
-
* Authorization: Bearer <token>
|
|
17
|
-
*
|
|
18
|
-
* All logging goes to stderr via console.error().
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import http from 'http';
|
|
22
|
-
import { URL } from 'url';
|
|
23
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
24
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
|
-
import { registerTools, cleanupWatchers, addMemoryInternal, executeToolInternal } from './tools.js';
|
|
26
|
-
import {
|
|
27
|
-
applyTemporalDecay,
|
|
28
|
-
closeDatabase,
|
|
29
|
-
getActiveMemoryCount,
|
|
30
|
-
getNamespaceStats,
|
|
31
|
-
getAllAgentStats
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// ============================================================
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* @param {
|
|
55
|
-
* @
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
else if (/^
|
|
81
|
-
else
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
md +=
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
md +=
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
md +=
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
text
|
|
110
|
-
|
|
111
|
-
text +=
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
text +=
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
text +=
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// ============================================================
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// ----------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// ----------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
res.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
res.writeHead(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
res.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// ----------------------------------------------------------
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1
|
+
/**
|
|
2
|
+
* server.js — MCP Server, Local HTTP Gateway & Swarm Hub
|
|
3
|
+
*
|
|
4
|
+
* Creates the MCP server, registers all tools, and connects via stdio.
|
|
5
|
+
* Also runs a local HTTP/JSON Gateway on port 4321 (configurable) to support:
|
|
6
|
+
* - Agentic swarms without subprocess overhead
|
|
7
|
+
* - IDE context injection via /system-prompt
|
|
8
|
+
* - Real-time event streaming via SSE (/events)
|
|
9
|
+
* - Batch operations for high-throughput swarm agents
|
|
10
|
+
* - Optional API key authentication for remote/multi-host setups
|
|
11
|
+
*
|
|
12
|
+
* Environment variables:
|
|
13
|
+
* PORT — HTTP gateway port (default: 4321)
|
|
14
|
+
* PERSYST_HOST — Bind address (default: 127.0.0.1, use 0.0.0.0 for Docker/remote)
|
|
15
|
+
* PERSYST_API_KEY — Optional auth token. If set, all endpoints (except /health) require
|
|
16
|
+
* Authorization: Bearer <token>
|
|
17
|
+
*
|
|
18
|
+
* All logging goes to stderr via console.error().
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import http from 'http';
|
|
22
|
+
import { URL } from 'url';
|
|
23
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
24
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
|
+
import { registerTools, cleanupWatchers, addMemoryInternal, executeToolInternal } from './tools.js';
|
|
26
|
+
import {
|
|
27
|
+
applyTemporalDecay,
|
|
28
|
+
closeDatabase,
|
|
29
|
+
getActiveMemoryCount,
|
|
30
|
+
getNamespaceStats,
|
|
31
|
+
getAllAgentStats,
|
|
32
|
+
getAttestationsByDateRange
|
|
33
|
+
} from './database.js';
|
|
34
|
+
import { consolidateMemories, searchHybrid, getOptimizedContext } from './search.js';
|
|
35
|
+
import { startWatcher, stopWatcher } from './watcher.js';
|
|
36
|
+
import { verifyChainIntegrity } from './attestation.js';
|
|
37
|
+
import { memoryEventBus } from './events.js';
|
|
38
|
+
import { logInfo } from './text-utils.js';
|
|
39
|
+
|
|
40
|
+
// Track server birth time for uptime reporting
|
|
41
|
+
const SERVER_START_TIME = Date.now();
|
|
42
|
+
|
|
43
|
+
// Active SSE client response objects
|
|
44
|
+
const sseClients = new Set();
|
|
45
|
+
|
|
46
|
+
// ============================================================
|
|
47
|
+
// SYSTEM PROMPT FORMATTER
|
|
48
|
+
// ============================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format optimized context data into a structured system-prompt block.
|
|
52
|
+
* Supports three output formats: 'text', 'markdown', 'json'.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} contextData - Result from getOptimizedContext()
|
|
55
|
+
* @param {string} format - 'text' | 'markdown' | 'json'
|
|
56
|
+
* @param {string|null} agentId
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function formatSystemPrompt(contextData, format, agentId) {
|
|
60
|
+
const { memories, suggested_actions } = contextData;
|
|
61
|
+
const now = new Date().toLocaleString('en-US', { hour12: false }).replace(',', '');
|
|
62
|
+
const count = memories.length;
|
|
63
|
+
|
|
64
|
+
if (format === 'json') {
|
|
65
|
+
return JSON.stringify({ ...contextData, generated_at: new Date().toISOString() }, null, 2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Group memories by category prefix
|
|
69
|
+
const groups = {
|
|
70
|
+
'Rules & Conventions': [],
|
|
71
|
+
'Architecture & Stack': [],
|
|
72
|
+
'Decisions': [],
|
|
73
|
+
'Preferences': [],
|
|
74
|
+
'Context': []
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const m of memories) {
|
|
78
|
+
const c = m.content;
|
|
79
|
+
if (/^(?:Rule|Config):/i.test(c)) groups['Rules & Conventions'].push(c);
|
|
80
|
+
else if (/^(?:Stack|Architecture):/i.test(c)) groups['Architecture & Stack'].push(c);
|
|
81
|
+
else if (/^Decision:/i.test(c)) groups['Decisions'].push(c);
|
|
82
|
+
else if (/^Preference:/i.test(c)) groups['Preferences'].push(c);
|
|
83
|
+
else groups['Context'].push(c);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (format === 'markdown') {
|
|
87
|
+
let md = `# Persyst Memory Context\n`;
|
|
88
|
+
md += `> ${count} memories | Updated: ${now}`;
|
|
89
|
+
if (agentId) md += ` | Agent: \`${agentId}\``;
|
|
90
|
+
md += '\n\n';
|
|
91
|
+
|
|
92
|
+
for (const [section, items] of Object.entries(groups)) {
|
|
93
|
+
if (items.length === 0) continue;
|
|
94
|
+
md += `## ${section}\n`;
|
|
95
|
+
for (const item of items) md += `- ${item}\n`;
|
|
96
|
+
md += '\n';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (suggested_actions.length > 0) {
|
|
100
|
+
md += `## Suggested Actions\n`;
|
|
101
|
+
for (const a of suggested_actions) md += `- ${a}\n`;
|
|
102
|
+
md += '\n';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
md += `---\n*Refresh: \`curl http://127.0.0.1:4321/system-prompt?format=markdown\`*\n`;
|
|
106
|
+
return md;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Plain text (default) — safe to paste into any IDE custom instructions
|
|
110
|
+
let text = `=== PERSYST MEMORY CONTEXT ===\n`;
|
|
111
|
+
text += `Updated: ${now} | ${count} memories`;
|
|
112
|
+
if (agentId) text += ` | Agent: ${agentId}`;
|
|
113
|
+
text += '\n\n';
|
|
114
|
+
|
|
115
|
+
for (const [section, items] of Object.entries(groups)) {
|
|
116
|
+
if (items.length === 0) continue;
|
|
117
|
+
text += `[${section.toUpperCase()}]\n`;
|
|
118
|
+
for (const item of items) text += `• ${item}\n`;
|
|
119
|
+
text += '\n';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (suggested_actions.length > 0) {
|
|
123
|
+
text += `[SUGGESTED ACTIONS]\n`;
|
|
124
|
+
for (const a of suggested_actions) text += `• ${a}\n`;
|
|
125
|
+
text += '\n';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
text += `=== END MEMORY CONTEXT ===\n`;
|
|
129
|
+
text += `Refresh: curl http://127.0.0.1:${process.env.PORT || '4321'}/system-prompt\n`;
|
|
130
|
+
return text;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// REQUEST HANDLERS
|
|
135
|
+
// ============================================================
|
|
136
|
+
|
|
137
|
+
async function handleGetRequest(req, res, url) {
|
|
138
|
+
const path = url.pathname;
|
|
139
|
+
|
|
140
|
+
// ----------------------------------------------------------
|
|
141
|
+
// GET /health — server liveness check for orchestrators
|
|
142
|
+
// ----------------------------------------------------------
|
|
143
|
+
if (path === '/health') {
|
|
144
|
+
const uptime = Math.floor((Date.now() - SERVER_START_TIME) / 1000);
|
|
145
|
+
let memories = 0;
|
|
146
|
+
try { memories = getActiveMemoryCount(); } catch (_) {}
|
|
147
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
148
|
+
res.end(JSON.stringify({
|
|
149
|
+
ok: true,
|
|
150
|
+
version: '2.2.6',
|
|
151
|
+
uptime_seconds: uptime,
|
|
152
|
+
memories,
|
|
153
|
+
sse_clients: sseClients.size
|
|
154
|
+
}));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ----------------------------------------------------------
|
|
159
|
+
// GET /stats — memory and agent statistics
|
|
160
|
+
// ----------------------------------------------------------
|
|
161
|
+
if (path === '/stats') {
|
|
162
|
+
try {
|
|
163
|
+
const namespaces = getNamespaceStats();
|
|
164
|
+
const agents = getAllAgentStats();
|
|
165
|
+
const uptime = Math.floor((Date.now() - SERVER_START_TIME) / 1000);
|
|
166
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
167
|
+
res.end(JSON.stringify({ uptime_seconds: uptime, namespaces, agents }));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ----------------------------------------------------------
|
|
176
|
+
// GET /compliance/export — cryptographic audit log export
|
|
177
|
+
//
|
|
178
|
+
// Query params:
|
|
179
|
+
// start — ISO timestamp or Unix epoch (default: beginning of time)
|
|
180
|
+
// end — ISO timestamp or Unix epoch (default: current time)
|
|
181
|
+
// format — 'json' (default) | 'markdown'
|
|
182
|
+
// ----------------------------------------------------------
|
|
183
|
+
if (path === '/compliance/export') {
|
|
184
|
+
try {
|
|
185
|
+
const startParam = url.searchParams.get('start');
|
|
186
|
+
const endParam = url.searchParams.get('end');
|
|
187
|
+
const format = url.searchParams.get('format') || 'json';
|
|
188
|
+
|
|
189
|
+
// Parse start and end
|
|
190
|
+
let startDate = '0000-01-01T00:00:00.000Z';
|
|
191
|
+
let endDate = new Date().toISOString();
|
|
192
|
+
|
|
193
|
+
if (startParam) {
|
|
194
|
+
if (!isNaN(startParam)) {
|
|
195
|
+
startDate = new Date(parseInt(startParam, 10)).toISOString();
|
|
196
|
+
} else {
|
|
197
|
+
startDate = new Date(startParam).toISOString();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (endParam) {
|
|
201
|
+
if (!isNaN(endParam)) {
|
|
202
|
+
endDate = new Date(parseInt(endParam, 10)).toISOString();
|
|
203
|
+
} else {
|
|
204
|
+
endDate = new Date(endParam).toISOString();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const attestations = getAttestationsByDateRange(startDate, endDate);
|
|
209
|
+
const agents = getAllAgentStats();
|
|
210
|
+
const summary = {
|
|
211
|
+
exported_at: new Date().toISOString(),
|
|
212
|
+
start_date: startDate,
|
|
213
|
+
end_date: endDate,
|
|
214
|
+
total_attestations: attestations.length,
|
|
215
|
+
system_integrity: 'SECURE'
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (format === 'markdown') {
|
|
219
|
+
let md = `# Persyst Cryptographic Compliance Export\n\n`;
|
|
220
|
+
md += `Exported at: \`${summary.exported_at}\` \n`;
|
|
221
|
+
md += `Period: \`${summary.start_date}\` to \`${summary.end_date}\` \n`;
|
|
222
|
+
md += `Total audit records: **${summary.total_attestations}** \n`;
|
|
223
|
+
md += `System cryptographic status: **${summary.system_integrity}** \n\n`;
|
|
224
|
+
|
|
225
|
+
md += `## Agent Trust Reputation Ledger\n\n`;
|
|
226
|
+
md += `| Agent ID | Created | Confirmed | Contradicted | Trust Score |\n`;
|
|
227
|
+
md += `|---|---|---|---|---|\n`;
|
|
228
|
+
for (const a of agents) {
|
|
229
|
+
md += `| \`${a.agent_id}\` | ${a.memories_created} | ${a.memories_confirmed} | ${a.memories_contradicted} | **${parseFloat(a.reputation_score).toFixed(2)}** |\n`;
|
|
230
|
+
}
|
|
231
|
+
md += `\n`;
|
|
232
|
+
|
|
233
|
+
md += `## Attestation Audit Trail\n\n`;
|
|
234
|
+
if (attestations.length === 0) {
|
|
235
|
+
md += `*No attestations found in the specified range.*\n`;
|
|
236
|
+
} else {
|
|
237
|
+
for (const att of attestations) {
|
|
238
|
+
md += `### Attestation \`${att.attestation_id}\`\n`;
|
|
239
|
+
md += `- **Timestamp:** \`${att.timestamp}\`\n`;
|
|
240
|
+
md += `- **Agent namespace:** \`${att.agent_id || 'shared'}\`\n`;
|
|
241
|
+
md += `- **Query:** *"${att.query}"*\n`;
|
|
242
|
+
md += `- **Previous Attestation Hash:** \`${att.previous_hash || 'GENESIS'}\`\n`;
|
|
243
|
+
md += `- **Current Signature Hash:** \`${att.hash}\`\n`;
|
|
244
|
+
md += `- **Signature:** \`${att.signature.substring(0, 32)}...\`\n`;
|
|
245
|
+
|
|
246
|
+
let retrieved = [];
|
|
247
|
+
try {
|
|
248
|
+
retrieved = JSON.parse(att.memories_retrieved);
|
|
249
|
+
} catch (_) {}
|
|
250
|
+
|
|
251
|
+
if (retrieved.length > 0) {
|
|
252
|
+
md += `- **Memories retrieved:**\n`;
|
|
253
|
+
for (const m of retrieved) {
|
|
254
|
+
md += ` - ID: \`${m.id}\`, Hash: \`${m.content_hash}\`, Score: \`${m.score}\`\n`;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
md += `- **Memories retrieved:** None\n`;
|
|
258
|
+
}
|
|
259
|
+
md += `\n---\n`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' });
|
|
263
|
+
res.end(md);
|
|
264
|
+
} else {
|
|
265
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(JSON.stringify({
|
|
267
|
+
summary,
|
|
268
|
+
agent_stats: agents,
|
|
269
|
+
attestations: attestations.map(att => ({
|
|
270
|
+
...att,
|
|
271
|
+
memories_retrieved: (() => {
|
|
272
|
+
try { return JSON.parse(att.memories_retrieved); } catch (_) { return []; }
|
|
273
|
+
})()
|
|
274
|
+
}))
|
|
275
|
+
}, null, 2));
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
279
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ----------------------------------------------------------
|
|
285
|
+
// GET /system-prompt — formatted memory context for IDE injection
|
|
286
|
+
//
|
|
287
|
+
// Query params:
|
|
288
|
+
// query — search query (default: broad project context)
|
|
289
|
+
// max_tokens — token budget (default: 1500)
|
|
290
|
+
// agent_id — restrict to this agent's namespace
|
|
291
|
+
// format — 'text' (default) | 'markdown' | 'json'
|
|
292
|
+
// ----------------------------------------------------------
|
|
293
|
+
if (path === '/system-prompt') {
|
|
294
|
+
try {
|
|
295
|
+
const query = url.searchParams.get('query') ||
|
|
296
|
+
'project conventions architecture preferences rules stack decisions';
|
|
297
|
+
const maxTokens = Math.max(100, parseInt(url.searchParams.get('max_tokens') || '1500', 10));
|
|
298
|
+
const agentId = url.searchParams.get('agent_id') || null;
|
|
299
|
+
const format = url.searchParams.get('format') || 'text';
|
|
300
|
+
|
|
301
|
+
const contextData = await getOptimizedContext(
|
|
302
|
+
query, maxTokens, agentId, null, agentId || null, null
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const output = formatSystemPrompt(contextData, format, agentId);
|
|
306
|
+
|
|
307
|
+
const contentTypeMap = {
|
|
308
|
+
json: 'application/json',
|
|
309
|
+
markdown: 'text/markdown; charset=utf-8',
|
|
310
|
+
text: 'text/plain; charset=utf-8'
|
|
311
|
+
};
|
|
312
|
+
res.writeHead(200, {
|
|
313
|
+
'Content-Type': contentTypeMap[format] || 'text/plain; charset=utf-8',
|
|
314
|
+
'Cache-Control': 'no-cache'
|
|
315
|
+
});
|
|
316
|
+
res.end(output);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
319
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ----------------------------------------------------------
|
|
325
|
+
// GET /events — Server-Sent Events stream of memory changes
|
|
326
|
+
//
|
|
327
|
+
// Clients subscribe once and receive real-time push notifications
|
|
328
|
+
// for memory_added, memory_deleted, memories_consolidated events.
|
|
329
|
+
//
|
|
330
|
+
// Example (Python):
|
|
331
|
+
// import sseclient, requests
|
|
332
|
+
// for event in sseclient.SSEClient('http://127.0.0.1:4321/events'):
|
|
333
|
+
// print(event.event, event.data)
|
|
334
|
+
//
|
|
335
|
+
// Example (Node.js):
|
|
336
|
+
// const es = new EventSource('http://127.0.0.1:4321/events');
|
|
337
|
+
// es.addEventListener('memory_added', e => console.log(JSON.parse(e.data)));
|
|
338
|
+
// ----------------------------------------------------------
|
|
339
|
+
if (path === '/events') {
|
|
340
|
+
res.writeHead(200, {
|
|
341
|
+
'Content-Type': 'text/event-stream',
|
|
342
|
+
'Cache-Control': 'no-cache',
|
|
343
|
+
'Connection': 'keep-alive',
|
|
344
|
+
'Access-Control-Allow-Origin': '*',
|
|
345
|
+
'X-Accel-Buffering': 'no' // Prevents nginx from buffering SSE
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Send initial connected event
|
|
349
|
+
res.write(`event: connected\ndata: ${JSON.stringify({
|
|
350
|
+
ok: true,
|
|
351
|
+
timestamp: new Date().toISOString(),
|
|
352
|
+
server_version: '2.2.7'
|
|
353
|
+
})}\n\n`);
|
|
354
|
+
|
|
355
|
+
sseClients.add(res);
|
|
356
|
+
|
|
357
|
+
// Heartbeat every 15s to keep connection alive through proxies
|
|
358
|
+
const heartbeat = setInterval(() => {
|
|
359
|
+
try { res.write(': heartbeat\n\n'); } catch (_) { clearInterval(heartbeat); }
|
|
360
|
+
}, 15000);
|
|
361
|
+
|
|
362
|
+
const onAdded = (data) => {
|
|
363
|
+
try { res.write(`event: memory_added\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
364
|
+
};
|
|
365
|
+
const onDeleted = (data) => {
|
|
366
|
+
try { res.write(`event: memory_deleted\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
367
|
+
};
|
|
368
|
+
const onUpdated = (data) => {
|
|
369
|
+
try { res.write(`event: memory_updated\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
370
|
+
};
|
|
371
|
+
const onRetrieved = (data) => {
|
|
372
|
+
try { res.write(`event: memory_retrieved\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
373
|
+
};
|
|
374
|
+
const onConsolidated = (data) => {
|
|
375
|
+
try { res.write(`event: memories_consolidated\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
memoryEventBus.on('memory_added', onAdded);
|
|
379
|
+
memoryEventBus.on('memory_deleted', onDeleted);
|
|
380
|
+
memoryEventBus.on('memory_updated', onUpdated);
|
|
381
|
+
memoryEventBus.on('memory_retrieved', onRetrieved);
|
|
382
|
+
memoryEventBus.on('memories_consolidated', onConsolidated);
|
|
383
|
+
|
|
384
|
+
req.on('close', () => {
|
|
385
|
+
clearInterval(heartbeat);
|
|
386
|
+
memoryEventBus.off('memory_added', onAdded);
|
|
387
|
+
memoryEventBus.off('memory_deleted', onDeleted);
|
|
388
|
+
memoryEventBus.off('memory_updated', onUpdated);
|
|
389
|
+
memoryEventBus.off('memory_retrieved', onRetrieved);
|
|
390
|
+
memoryEventBus.off('memories_consolidated', onConsolidated);
|
|
391
|
+
sseClients.delete(res);
|
|
392
|
+
console.error(`[persyst-sse] Client disconnected. Active: ${sseClients.size}`);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
console.error(`[persyst-sse] Client connected. Active: ${sseClients.size}`);
|
|
396
|
+
return; // Keep connection alive — do NOT end response
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
400
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function handlePostRequest(req, res, payload) {
|
|
404
|
+
const path = new URL(req.url, 'http://127.0.0.1').pathname;
|
|
405
|
+
|
|
406
|
+
// ----------------------------------------------------------
|
|
407
|
+
// POST /remember — quick one-liner memory save
|
|
408
|
+
//
|
|
409
|
+
// The user explicitly wants to save something. No extraction,
|
|
410
|
+
// no filtering, no pattern matching. Just store it.
|
|
411
|
+
//
|
|
412
|
+
// Body: { content: string, importance?: number, namespace?: string }
|
|
413
|
+
// OR: plain text body (e.g. from curl --data "don't forget X")
|
|
414
|
+
//
|
|
415
|
+
// Example:
|
|
416
|
+
// curl -X POST http://127.0.0.1:4321/remember \
|
|
417
|
+
// -H 'Content-Type: text/plain' \
|
|
418
|
+
// --data 'SSL cert expires March 15'
|
|
419
|
+
// ----------------------------------------------------------
|
|
420
|
+
if (path === '/remember') {
|
|
421
|
+
// Support both plain text and JSON bodies
|
|
422
|
+
let content, importance, namespace;
|
|
423
|
+
if (typeof payload === 'string') {
|
|
424
|
+
content = payload.trim();
|
|
425
|
+
importance = 1.0;
|
|
426
|
+
namespace = 'shared';
|
|
427
|
+
} else {
|
|
428
|
+
content = payload.content || payload.text || payload.note || payload.message;
|
|
429
|
+
importance = payload.importance || 1.0;
|
|
430
|
+
namespace = payload.namespace || 'shared';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!content) {
|
|
434
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
435
|
+
res.end(JSON.stringify({ error: 'No content provided. Pass plain text or { content: "..." }' }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Prefix with Note: if not already categorized
|
|
440
|
+
const normalizedContent = /^(?:Note|Reminder|Rule|Decision|Preference|Stack|Architecture|Config|Warning|FYI):/i.test(content.trim())
|
|
441
|
+
? content.trim()
|
|
442
|
+
: `Note: ${content.trim()}`;
|
|
443
|
+
|
|
444
|
+
const result = await addMemoryInternal({
|
|
445
|
+
content: normalizedContent,
|
|
446
|
+
importance,
|
|
447
|
+
agent_id: payload.agent_id || null,
|
|
448
|
+
session_id: payload.session_id || null,
|
|
449
|
+
shared: payload.shared !== false
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
if (!result.error) {
|
|
453
|
+
memoryEventBus.emit('memory_added', {
|
|
454
|
+
id: result.id,
|
|
455
|
+
content: normalizedContent,
|
|
456
|
+
namespace: result.namespace || namespace,
|
|
457
|
+
source: 'user-explicit'
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
|
|
462
|
+
res.end(JSON.stringify(result));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ----------------------------------------------------------
|
|
467
|
+
// POST /search
|
|
468
|
+
// ----------------------------------------------------------
|
|
469
|
+
if (path === '/search') {
|
|
470
|
+
const { query, limit = 5, agent_id, session_id } = payload;
|
|
471
|
+
if (!query) {
|
|
472
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
473
|
+
res.end(JSON.stringify({ error: 'Missing required field: query' }));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const results = await searchHybrid(query, limit, agent_id, session_id, agent_id || null);
|
|
477
|
+
if (results && results.length > 0) {
|
|
478
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
479
|
+
tool: 'http/search',
|
|
480
|
+
query,
|
|
481
|
+
count: results.length,
|
|
482
|
+
agent_id: agent_id || 'http',
|
|
483
|
+
namespace: agent_id || 'shared',
|
|
484
|
+
memory_ids: results.map(r => r.id)
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
488
|
+
res.end(JSON.stringify({ success: true, results }));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ----------------------------------------------------------
|
|
493
|
+
// POST /add
|
|
494
|
+
// ----------------------------------------------------------
|
|
495
|
+
if (path === '/add') {
|
|
496
|
+
const { content, importance = 1.0, agent_id, session_id, shared = true } = payload;
|
|
497
|
+
if (!content) {
|
|
498
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
499
|
+
res.end(JSON.stringify({ error: 'Missing required field: content' }));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const result = await addMemoryInternal({ content, importance, agent_id, session_id, shared });
|
|
503
|
+
if (result.error) {
|
|
504
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
505
|
+
} else {
|
|
506
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
507
|
+
// Broadcast to SSE subscribers
|
|
508
|
+
memoryEventBus.emit('memory_added', {
|
|
509
|
+
id: result.id,
|
|
510
|
+
content,
|
|
511
|
+
namespace: result.namespace,
|
|
512
|
+
source: agent_id || 'http'
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
res.end(JSON.stringify(result));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ----------------------------------------------------------
|
|
520
|
+
// POST /context
|
|
521
|
+
// ----------------------------------------------------------
|
|
522
|
+
if (path === '/context') {
|
|
523
|
+
const { query, max_tokens = 2000, agent_id, session_id, intent } = payload;
|
|
524
|
+
if (!query) {
|
|
525
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
526
|
+
res.end(JSON.stringify({ error: 'Missing required field: query' }));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const context = await getOptimizedContext(query, max_tokens, agent_id, session_id, agent_id || null, intent);
|
|
530
|
+
const retrievedCount = context?.memories?.length ?? 0;
|
|
531
|
+
if (retrievedCount > 0) {
|
|
532
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
533
|
+
tool: 'http/context',
|
|
534
|
+
query,
|
|
535
|
+
count: retrievedCount,
|
|
536
|
+
agent_id: agent_id || 'http',
|
|
537
|
+
namespace: agent_id || 'shared',
|
|
538
|
+
token_budget: max_tokens,
|
|
539
|
+
memory_ids: context.memories.map(m => m.id)
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
543
|
+
res.end(JSON.stringify(context));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ----------------------------------------------------------
|
|
548
|
+
// POST /tool — generic MCP tool invocation
|
|
549
|
+
// ----------------------------------------------------------
|
|
550
|
+
if (path === '/tool') {
|
|
551
|
+
const { name, arguments: args } = payload;
|
|
552
|
+
if (!name) {
|
|
553
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
554
|
+
res.end(JSON.stringify({ error: 'Missing required field: name' }));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
let result;
|
|
558
|
+
try {
|
|
559
|
+
result = await executeToolInternal(name, args || {});
|
|
560
|
+
} catch (err) {
|
|
561
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
562
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
566
|
+
res.end(JSON.stringify(result));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ----------------------------------------------------------
|
|
571
|
+
// POST /verify — chain integrity check
|
|
572
|
+
// ----------------------------------------------------------
|
|
573
|
+
if (path === '/verify') {
|
|
574
|
+
const attestationId = payload?.attestation_id;
|
|
575
|
+
const result = verifyChainIntegrity(attestationId);
|
|
576
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
577
|
+
res.end(JSON.stringify(result));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ----------------------------------------------------------
|
|
582
|
+
// POST /batch/add — store multiple memories in one round trip
|
|
583
|
+
//
|
|
584
|
+
// Body: { memories: [{ content, importance?, agent_id?, shared? }, ...] }
|
|
585
|
+
// Returns: { success, results: [...], stored, skipped, errors }
|
|
586
|
+
//
|
|
587
|
+
// Designed for:
|
|
588
|
+
// - Swarm agents ingesting session summaries in bulk
|
|
589
|
+
// - Migration tools
|
|
590
|
+
// - CI pipelines storing build/test results
|
|
591
|
+
// ----------------------------------------------------------
|
|
592
|
+
if (path === '/batch/add') {
|
|
593
|
+
const { memories } = payload;
|
|
594
|
+
if (!Array.isArray(memories) || memories.length === 0) {
|
|
595
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
596
|
+
res.end(JSON.stringify({ error: 'memories must be a non-empty array' }));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Hard cap: prevent abuse
|
|
601
|
+
if (memories.length > 200) {
|
|
602
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
603
|
+
res.end(JSON.stringify({ error: 'Batch size exceeds maximum of 200' }));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const results = [];
|
|
608
|
+
let stored = 0;
|
|
609
|
+
let skipped = 0;
|
|
610
|
+
let errors = 0;
|
|
611
|
+
|
|
612
|
+
for (const mem of memories) {
|
|
613
|
+
const { content, importance = 1.0, agent_id, session_id, shared = true } = mem;
|
|
614
|
+
if (!content) {
|
|
615
|
+
results.push({ error: 'Missing content', input: mem });
|
|
616
|
+
errors++;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const result = await addMemoryInternal({ content, importance, agent_id, session_id, shared });
|
|
621
|
+
results.push(result);
|
|
622
|
+
if (result.error) {
|
|
623
|
+
errors++;
|
|
624
|
+
} else if (result.message && result.message.includes('already exists')) {
|
|
625
|
+
skipped++;
|
|
626
|
+
} else {
|
|
627
|
+
stored++;
|
|
628
|
+
memoryEventBus.emit('memory_added', {
|
|
629
|
+
id: result.id,
|
|
630
|
+
content,
|
|
631
|
+
namespace: result.namespace,
|
|
632
|
+
source: agent_id || 'batch'
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
} catch (err) {
|
|
636
|
+
results.push({ error: err.message, input: mem });
|
|
637
|
+
errors++;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
642
|
+
res.end(JSON.stringify({ success: true, results, stored, skipped, errors }));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ----------------------------------------------------------
|
|
647
|
+
// POST /batch/search — run multiple queries in one round trip
|
|
648
|
+
//
|
|
649
|
+
// Body: { queries: string[] | Array<{query, limit?, agent_id?}>, limit?: number }
|
|
650
|
+
// Returns: { results: { "<query>": [...memories] } }
|
|
651
|
+
//
|
|
652
|
+
// Designed for:
|
|
653
|
+
// - Swarm agents loading context for multiple topics at once
|
|
654
|
+
// - Parallel memory retrieval without sequential round trips
|
|
655
|
+
// ----------------------------------------------------------
|
|
656
|
+
if (path === '/batch/search') {
|
|
657
|
+
const { queries, limit = 5 } = payload;
|
|
658
|
+
if (!Array.isArray(queries) || queries.length === 0) {
|
|
659
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
660
|
+
res.end(JSON.stringify({ error: 'queries must be a non-empty array' }));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (queries.length > 50) {
|
|
665
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
666
|
+
res.end(JSON.stringify({ error: 'Batch query size exceeds maximum of 50' }));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Run all searches in parallel for speed
|
|
671
|
+
const searchPromises = queries.map(async (q) => {
|
|
672
|
+
if (typeof q === 'string') {
|
|
673
|
+
return { key: q, results: await searchHybrid(q, limit, null, null, null) };
|
|
674
|
+
} else if (q && typeof q === 'object' && q.query) {
|
|
675
|
+
return {
|
|
676
|
+
key: q.query,
|
|
677
|
+
results: await searchHybrid(q.query, q.limit || limit, q.agent_id || null, null, q.agent_id || null)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return { key: String(q), results: [] };
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const settled = await Promise.allSettled(searchPromises);
|
|
684
|
+
const results = {};
|
|
685
|
+
for (const s of settled) {
|
|
686
|
+
if (s.status === 'fulfilled') {
|
|
687
|
+
results[s.value.key] = s.value.results;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
692
|
+
res.end(JSON.stringify({ success: true, results }));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
697
|
+
res.end(JSON.stringify({ error: 'Endpoint Not Found' }));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ============================================================
|
|
701
|
+
// MAIN SERVER STARTUP
|
|
702
|
+
// ============================================================
|
|
703
|
+
|
|
704
|
+
export async function startServer() {
|
|
705
|
+
// --- Create MCP server ---
|
|
706
|
+
const server = new McpServer({
|
|
707
|
+
name: 'persyst',
|
|
708
|
+
version: '2.2.5'
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// --- Register all tools ---
|
|
712
|
+
const registeredCount = registerTools(server);
|
|
713
|
+
logInfo(`[persyst] ${registeredCount} tools registered ✓`);
|
|
714
|
+
|
|
715
|
+
// --- Connect via stdio IMMEDIATELY so MCP handshake completes instantly (<10ms) ---
|
|
716
|
+
const transport = new StdioServerTransport();
|
|
717
|
+
await server.connect(transport);
|
|
718
|
+
|
|
719
|
+
logInfo('[persyst] MCP server running on stdio ✓');
|
|
720
|
+
logInfo('[persyst] Ready to receive tool calls');
|
|
721
|
+
|
|
722
|
+
// Interactive Terminal Banner (only shown when run directly by a user in terminal)
|
|
723
|
+
if (process.stderr.isTTY || process.stdout.isTTY) {
|
|
724
|
+
console.error(`\n[OK] Persyst MCP Server is active and listening (stdio mode)`);
|
|
725
|
+
console.error(`[OK] Workspace Project: ${process.env.PERSYST_PROJECT || 'shared'}`);
|
|
726
|
+
console.error(`[OK] Local HTTP Gateway: http://127.0.0.1:${process.env.PORT || '4321'}`);
|
|
727
|
+
console.error(`[OK] Process ID: ${process.pid} | Press Ctrl+C to stop.\n`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Defer background services & HTTP server so stdio handshake is never blocked
|
|
731
|
+
let httpServer = null;
|
|
732
|
+
let decayTimer = null;
|
|
733
|
+
let consolidationTimer = null;
|
|
734
|
+
let sseHealthCheck = null;
|
|
735
|
+
|
|
736
|
+
const shutdown = () => {
|
|
737
|
+
logInfo('[persyst] Shutting down...');
|
|
738
|
+
if (decayTimer) clearInterval(decayTimer);
|
|
739
|
+
if (consolidationTimer) clearInterval(consolidationTimer);
|
|
740
|
+
if (sseHealthCheck) clearInterval(sseHealthCheck);
|
|
741
|
+
stopWatcher();
|
|
742
|
+
cleanupWatchers();
|
|
743
|
+
|
|
744
|
+
for (const client of sseClients) {
|
|
745
|
+
try {
|
|
746
|
+
client.write(`event: server_shutdown\ndata: ${JSON.stringify({ message: 'Server shutting down' })}\n\n`);
|
|
747
|
+
client.end();
|
|
748
|
+
} catch (_) {}
|
|
749
|
+
}
|
|
750
|
+
sseClients.clear();
|
|
751
|
+
|
|
752
|
+
if (httpServer) httpServer.close();
|
|
753
|
+
closeDatabase();
|
|
754
|
+
};
|
|
755
|
+
process.on('SIGINT', shutdown);
|
|
756
|
+
process.on('SIGTERM', shutdown);
|
|
757
|
+
|
|
758
|
+
setTimeout(() => {
|
|
759
|
+
// --- Start background log watcher daemon (skip in test mode) ---
|
|
760
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
761
|
+
startWatcher();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// --- Gateway configuration ---
|
|
765
|
+
const httpPort = parseInt(process.env.PORT || '4321', 10);
|
|
766
|
+
const httpHost = process.env.PERSYST_HOST || '127.0.0.1';
|
|
767
|
+
const configuredApiKey = process.env.PERSYST_API_KEY || null;
|
|
768
|
+
|
|
769
|
+
if (configuredApiKey) {
|
|
770
|
+
logInfo(`[persyst] API key auth enabled — endpoints require Authorization: Bearer <key>`);
|
|
771
|
+
}
|
|
772
|
+
if (httpHost !== '127.0.0.1') {
|
|
773
|
+
logInfo(`[persyst] ⚠️ Gateway bound to ${httpHost} — ensure PERSYST_API_KEY is set for security`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// --- Start local HTTP Gateway ---
|
|
777
|
+
httpServer = http.createServer((req, res) => {
|
|
778
|
+
// CORS headers
|
|
779
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
780
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
781
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
782
|
+
|
|
783
|
+
if (req.method === 'OPTIONS') {
|
|
784
|
+
res.writeHead(204);
|
|
785
|
+
res.end();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (configuredApiKey) {
|
|
790
|
+
const urlPath = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
791
|
+
if (urlPath !== '/health') {
|
|
792
|
+
const authHeader = req.headers['authorization'] || '';
|
|
793
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
794
|
+
if (token !== configuredApiKey) {
|
|
795
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
796
|
+
res.end(JSON.stringify({
|
|
797
|
+
error: 'Unauthorized. Set header: Authorization: Bearer <PERSYST_API_KEY>'
|
|
798
|
+
}));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
805
|
+
const path = url.pathname;
|
|
806
|
+
|
|
807
|
+
if (req.method === 'GET') {
|
|
808
|
+
handleGetRequest(req, res, path, url);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (req.method === 'POST') {
|
|
813
|
+
let body = '';
|
|
814
|
+
req.on('data', chunk => {
|
|
815
|
+
body += chunk;
|
|
816
|
+
if (body.length > 10 * 1024 * 1024) {
|
|
817
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
818
|
+
res.end(JSON.stringify({ error: 'Payload too large. Max 10MB.' }));
|
|
819
|
+
req.destroy();
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
req.on('end', () => {
|
|
823
|
+
try {
|
|
824
|
+
const payload = body ? JSON.parse(body) : {};
|
|
825
|
+
handlePostRequest(req, res, payload).catch(err => {
|
|
826
|
+
try {
|
|
827
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
828
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
829
|
+
} catch (_) {}
|
|
830
|
+
});
|
|
831
|
+
} catch (err) {
|
|
832
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
833
|
+
res.end(JSON.stringify({ error: `Invalid JSON payload: ${err.message}` }));
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
840
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
httpServer.on('error', (err) => {
|
|
844
|
+
if (err.code === 'EADDRINUSE') {
|
|
845
|
+
logInfo(`[persyst] HTTP Gateway port ${httpPort} already in use. Stdio MCP server will continue.`);
|
|
846
|
+
} else {
|
|
847
|
+
console.error('[persyst] HTTP Gateway error:', err.message);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
httpServer.listen(httpPort, httpHost, () => {
|
|
852
|
+
logInfo(`[persyst] HTTP Gateway listening on http://${httpHost}:${httpPort} ✓`);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
decayTimer = setInterval(applyTemporalDecay, 3600000);
|
|
856
|
+
|
|
857
|
+
consolidationTimer = setInterval(async () => {
|
|
858
|
+
logInfo('[persyst] Running scheduled daily memory consolidation sweep...');
|
|
859
|
+
try {
|
|
860
|
+
const report = await consolidateMemories();
|
|
861
|
+
logInfo(`[persyst] Consolidation sweep: consolidated ${report.consolidated_groups} duplicate groups.`);
|
|
862
|
+
if (report.consolidated_groups > 0) {
|
|
863
|
+
memoryEventBus.emit('memories_consolidated', {
|
|
864
|
+
consolidated_groups: report.consolidated_groups,
|
|
865
|
+
details: report.details
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.error('[persyst] Daily consolidation sweep failed:', err.message);
|
|
870
|
+
}
|
|
871
|
+
}, 86400000);
|
|
872
|
+
|
|
873
|
+
sseHealthCheck = setInterval(() => {
|
|
874
|
+
for (const client of sseClients) {
|
|
875
|
+
try {
|
|
876
|
+
client.write(': health-check\n\n');
|
|
877
|
+
} catch (_) {
|
|
878
|
+
try { client.end(); } catch (_) {}
|
|
879
|
+
sseClients.delete(client);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}, 30000);
|
|
883
|
+
}, 50);
|
|
884
|
+
}
|