n8n-nodes-engram 0.2.3 → 0.2.5

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.
@@ -14,7 +14,7 @@ class EngramAdmin {
14
14
  name: 'engramAdmin',
15
15
  icon: 'file:engram-admin.png',
16
16
  group: ['transform'],
17
- version: 1,
17
+ version: [1, 2],
18
18
  description: 'Manage and administer the Engram knowledge graph',
19
19
  defaults: {
20
20
  name: 'Engram Admin',
@@ -52,79 +52,167 @@ class EngramAdmin {
52
52
  ],
53
53
  default: 'embedded',
54
54
  },
55
+ {
56
+ displayName: 'Resource',
57
+ name: 'resource',
58
+ type: 'options',
59
+ noDataExpression: true,
60
+ options: [
61
+ {
62
+ name: 'Monitoring',
63
+ value: 'monitoring',
64
+ description: 'Graph statistics and group monitoring',
65
+ },
66
+ {
67
+ name: 'Lifecycle',
68
+ value: 'lifecycle',
69
+ description: 'Data retention, clearing groups, and cleanup',
70
+ },
71
+ {
72
+ name: 'Hygiene',
73
+ value: 'hygiene',
74
+ description: 'Find orphaned entities, duplicates, and stale edges',
75
+ },
76
+ {
77
+ name: 'Portability',
78
+ value: 'portability',
79
+ description: 'Export and import graph data',
80
+ },
81
+ {
82
+ name: 'Analysis',
83
+ value: 'analysis',
84
+ description: 'Community detection and graph analysis',
85
+ },
86
+ ],
87
+ default: 'monitoring',
88
+ },
55
89
  {
56
90
  displayName: 'Operation',
57
91
  name: 'operation',
58
92
  type: 'options',
59
93
  noDataExpression: true,
94
+ displayOptions: { show: { resource: ['monitoring'] } },
60
95
  options: [
61
96
  {
62
97
  name: 'Stats',
63
98
  value: 'stats',
64
99
  description: 'Get graph statistics with enhanced metrics',
100
+ action: 'Get graph statistics',
65
101
  },
66
102
  {
67
103
  name: 'List Groups',
68
104
  value: 'listGroups',
69
105
  description: 'List all groups/sessions with per-group statistics',
106
+ action: 'List all groups',
70
107
  },
71
108
  {
72
109
  name: 'Group Stats',
73
110
  value: 'groupStats',
74
111
  description: 'Get detailed statistics for a specific group/session',
112
+ action: 'Get group statistics',
75
113
  },
114
+ ],
115
+ default: 'stats',
116
+ },
117
+ {
118
+ displayName: 'Operation',
119
+ name: 'operation',
120
+ type: 'options',
121
+ noDataExpression: true,
122
+ displayOptions: { show: { resource: ['lifecycle'] } },
123
+ options: [
76
124
  {
77
125
  name: 'Apply Retention',
78
126
  value: 'applyRetention',
79
127
  description: 'Remove old episodes based on a retention policy',
128
+ action: 'Apply retention policy',
80
129
  },
81
130
  {
82
131
  name: 'Clear Group',
83
132
  value: 'clearGroup',
84
133
  description: 'Clear all data for a specific group/session',
134
+ action: 'Clear a group',
85
135
  },
86
136
  {
87
137
  name: 'Bulk Clear Groups',
88
138
  value: 'bulkClearGroups',
89
139
  description: 'Clear multiple groups/sessions at once',
140
+ action: 'Bulk clear groups',
90
141
  },
91
142
  {
92
143
  name: 'Clear All',
93
144
  value: 'clearAll',
94
145
  description: 'Clear ALL data from the graph (destructive!)',
146
+ action: 'Clear all data',
95
147
  },
148
+ ],
149
+ default: 'applyRetention',
150
+ },
151
+ {
152
+ displayName: 'Operation',
153
+ name: 'operation',
154
+ type: 'options',
155
+ noDataExpression: true,
156
+ displayOptions: { show: { resource: ['hygiene'] } },
157
+ options: [
96
158
  {
97
159
  name: 'Orphaned Entities',
98
160
  value: 'orphanedEntities',
99
161
  description: 'Find or remove entities with no relationships',
162
+ action: 'Find orphaned entities',
100
163
  },
101
164
  {
102
165
  name: 'Duplicate Entities',
103
166
  value: 'duplicateEntities',
104
167
  description: 'Find entities with duplicate or similar names',
168
+ action: 'Find duplicate entities',
105
169
  },
106
170
  {
107
171
  name: 'Expire Stale Edges',
108
172
  value: 'expireStaleEdges',
109
173
  description: 'Find and expire edges with broken references or past validity',
174
+ action: 'Expire stale edges',
110
175
  },
176
+ ],
177
+ default: 'orphanedEntities',
178
+ },
179
+ {
180
+ displayName: 'Operation',
181
+ name: 'operation',
182
+ type: 'options',
183
+ noDataExpression: true,
184
+ displayOptions: { show: { resource: ['portability'] } },
185
+ options: [
111
186
  {
112
187
  name: 'Export',
113
188
  value: 'export',
114
189
  description: 'Export graph data as JSON',
190
+ action: 'Export graph data',
115
191
  },
116
192
  {
117
193
  name: 'Import',
118
194
  value: 'import',
119
195
  description: 'Import graph data from JSON',
196
+ action: 'Import graph data',
120
197
  },
198
+ ],
199
+ default: 'export',
200
+ },
201
+ {
202
+ displayName: 'Operation',
203
+ name: 'operation',
204
+ type: 'options',
205
+ noDataExpression: true,
206
+ displayOptions: { show: { resource: ['analysis'] } },
207
+ options: [
121
208
  {
122
209
  name: 'Detect Communities',
123
210
  value: 'detectCommunities',
124
211
  description: 'Detect entity communities/clusters using label propagation',
212
+ action: 'Detect communities',
125
213
  },
126
214
  ],
127
- default: 'stats',
215
+ default: 'detectCommunities',
128
216
  },
129
217
  {
130
218
  displayName: 'Group ID',
@@ -354,6 +442,7 @@ class EngramAdmin {
354
442
  var _a;
355
443
  const items = this.getInputData();
356
444
  const backend = this.getNodeParameter('backend', 0, 'embedded');
445
+ const resource = this.getNodeParameter('resource', 0, 'monitoring');
357
446
  const operation = this.getNodeParameter('operation', 0);
358
447
  let storage;
359
448
  if (backend === 'neo4j') {
@@ -375,414 +464,43 @@ class EngramAdmin {
375
464
  }
376
465
  await storage.initialize();
377
466
  const returnData = [];
467
+ const ctx = this;
378
468
  try {
379
469
  for (let i = 0; i < items.length; i++) {
380
470
  try {
381
- switch (operation) {
382
- case 'stats': {
383
- const groupId = this.getNodeParameter('groupIdFilter', i, '');
384
- const baseStats = await storage.getStats(groupId || undefined);
385
- const data = await storage.exportGraph(groupId || undefined);
386
- const activeEdges = data.edges.filter((e) => e.expired_at === null).length;
387
- const expiredEdges = data.edges.filter((e) => e.expired_at !== null).length;
388
- const groupCount = baseStats.group_ids.length;
389
- const enhancedStats = {
390
- ...baseStats,
391
- group_count: groupCount,
392
- active_edge_count: activeEdges,
393
- expired_edge_count: expiredEdges,
394
- avg_edges_per_entity: baseStats.entity_count > 0
395
- ? Math.round((baseStats.edge_count / baseStats.entity_count) * 100) / 100
396
- : 0,
397
- avg_episodes_per_group: groupCount > 0
398
- ? Math.round((baseStats.episode_count / groupCount) * 100) / 100
399
- : 0,
400
- storage_backend: backend,
401
- data_span_days: baseStats.oldest_episode && baseStats.newest_episode
402
- ? Math.round(((new Date(baseStats.newest_episode).getTime() -
403
- new Date(baseStats.oldest_episode).getTime()) /
404
- (1000 * 60 * 60 * 24)) *
405
- 10) / 10
406
- : null,
407
- };
408
- returnData.push({ json: enhancedStats });
409
- break;
410
- }
411
- case 'listGroups': {
412
- const includeStats = this.getNodeParameter('includeStats', i, true);
413
- const limit = this.getNodeParameter('limit', i, 100);
414
- const globalStats = await storage.getStats();
415
- const allGroupIds = globalStats.group_ids.slice(0, limit);
416
- if (includeStats) {
417
- for (const gid of allGroupIds) {
418
- const gStats = await storage.getStats(gid);
419
- returnData.push({
420
- json: {
421
- group_id: gid,
422
- entity_count: gStats.entity_count,
423
- edge_count: gStats.edge_count,
424
- episode_count: gStats.episode_count,
425
- entity_types: gStats.entity_types,
426
- oldest_episode: gStats.oldest_episode,
427
- newest_episode: gStats.newest_episode,
428
- },
429
- });
430
- }
431
- }
432
- else {
433
- for (const gid of allGroupIds) {
434
- returnData.push({ json: { group_id: gid } });
435
- }
436
- }
437
- if (returnData.length === 0) {
438
- returnData.push({
439
- json: {
440
- message: 'No groups found',
441
- total_groups: 0,
442
- },
443
- });
444
- }
445
- break;
446
- }
447
- case 'groupStats': {
448
- const groupId = this.getNodeParameter('groupId', i);
449
- if (!groupId) {
450
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Group Stats operation', { itemIndex: i });
451
- }
452
- const gStats = await storage.getStats(groupId);
453
- const gData = await storage.exportGraph(groupId);
454
- const activeEdges = gData.edges.filter((e) => e.expired_at === null).length;
455
- const expiredEdges = gData.edges.filter((e) => e.expired_at !== null).length;
456
- const relationshipTypes = {};
457
- for (const edge of gData.edges) {
458
- relationshipTypes[edge.name] = (relationshipTypes[edge.name] || 0) + 1;
459
- }
460
- returnData.push({
461
- json: {
462
- group_id: groupId,
463
- entity_count: gStats.entity_count,
464
- edge_count: gStats.edge_count,
465
- episode_count: gStats.episode_count,
466
- entity_types: gStats.entity_types,
467
- relationship_types: relationshipTypes,
468
- active_edge_count: activeEdges,
469
- expired_edge_count: expiredEdges,
470
- avg_edges_per_entity: gStats.entity_count > 0
471
- ? Math.round((gStats.edge_count / gStats.entity_count) * 100) / 100
472
- : 0,
473
- oldest_episode: gStats.oldest_episode,
474
- newest_episode: gStats.newest_episode,
475
- data_span_days: gStats.oldest_episode && gStats.newest_episode
476
- ? Math.round(((new Date(gStats.newest_episode).getTime() -
477
- new Date(gStats.oldest_episode).getTime()) /
478
- (1000 * 60 * 60 * 24)) *
479
- 10) / 10
480
- : null,
481
- },
482
- });
483
- break;
484
- }
485
- case 'applyRetention': {
486
- const groupId = this.getNodeParameter('groupId', i);
487
- if (!groupId) {
488
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Apply Retention operation', { itemIndex: i });
489
- }
490
- const retentionType = this.getNodeParameter('retentionType', i);
491
- const retentionValue = this.getNodeParameter('retentionValue', i);
492
- const removed = await storage.applyRetention(groupId, {
493
- type: retentionType,
494
- value: retentionValue,
495
- });
496
- returnData.push({
497
- json: {
498
- success: true,
499
- operation: 'applyRetention',
500
- group_id: groupId,
501
- policy: { type: retentionType, value: retentionValue },
502
- episodes_removed: removed,
503
- },
504
- });
505
- break;
506
- }
507
- case 'clearGroup': {
508
- const groupId = this.getNodeParameter('groupId', i);
509
- if (!groupId) {
510
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Clear Group operation', {
511
- itemIndex: i,
512
- description: 'Provide the session/group ID whose data you want to delete.',
513
- });
514
- }
515
- await storage.clearGroup(groupId);
516
- returnData.push({
517
- json: { success: true, operation: 'clearGroup', group_id: groupId },
518
- });
519
- break;
520
- }
521
- case 'bulkClearGroups': {
522
- const confirmDestructive = this.getNodeParameter('confirmDestructive', i, false);
523
- if (!confirmDestructive) {
524
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Confirm Destructive must be enabled to proceed with Bulk Clear Groups', {
525
- itemIndex: i,
526
- description: 'This is a safety measure. Enable "Confirm Destructive" to proceed.',
527
- });
528
- }
529
- const groupIdsRaw = this.getNodeParameter('groupIds', i);
530
- let groupIds;
531
- if (Array.isArray(groupIdsRaw)) {
532
- groupIds = groupIdsRaw.filter((id) => typeof id === 'string' && id.length > 0);
533
- }
534
- else {
535
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group IDs must be a JSON array of strings', { itemIndex: i });
536
- }
537
- if (groupIds.length === 0) {
538
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group IDs array is empty', {
539
- itemIndex: i,
540
- });
541
- }
542
- const cleared = [];
543
- const failed = [];
544
- for (const gid of groupIds) {
545
- try {
546
- await storage.clearGroup(gid);
547
- cleared.push(gid);
548
- }
549
- catch (err) {
550
- failed.push({ group_id: gid, error: err.message });
551
- }
552
- }
553
- returnData.push({
554
- json: {
555
- success: failed.length === 0,
556
- operation: 'bulkClearGroups',
557
- cleared,
558
- failed,
559
- total_cleared: cleared.length,
560
- total_failed: failed.length,
561
- },
562
- });
563
- break;
564
- }
565
- case 'clearAll': {
566
- await storage.clearAll();
567
- returnData.push({
568
- json: { success: true, operation: 'clearAll' },
569
- });
570
- break;
571
- }
572
- case 'orphanedEntities': {
573
- const groupId = this.getNodeParameter('groupId', i);
574
- if (!groupId) {
575
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Orphaned Entities operation', { itemIndex: i });
576
- }
577
- const deleteOrphans = this.getNodeParameter('deleteOrphans', i, false);
578
- const entities = await storage.listEntities(groupId, { limit: 10000 });
579
- const orphaned = [];
580
- for (const entity of entities) {
581
- const edges = await storage.getEdgesForEntity(entity.uuid);
582
- if (edges.length === 0) {
583
- orphaned.push({
584
- uuid: entity.uuid,
585
- name: entity.name,
586
- entity_type: entity.entity_type,
587
- summary: entity.summary,
588
- created_at: entity.created_at,
589
- });
590
- }
591
- }
592
- if (deleteOrphans) {
593
- for (const o of orphaned) {
594
- await storage.deleteEntity(o.uuid);
595
- }
596
- }
597
- returnData.push({
598
- json: {
599
- operation: 'orphanedEntities',
600
- group_id: groupId,
601
- orphaned,
602
- total_orphaned: orphaned.length,
603
- deleted: deleteOrphans,
604
- },
605
- });
606
- break;
607
- }
608
- case 'duplicateEntities': {
609
- const groupId = this.getNodeParameter('groupId', i);
610
- if (!groupId) {
611
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Duplicate Entities operation', { itemIndex: i });
612
- }
613
- const entities = await storage.listEntities(groupId, { limit: 10000 });
614
- const nameMap = new Map();
615
- for (const entity of entities) {
616
- const key = entity.name.toLowerCase().trim();
617
- if (!nameMap.has(key))
618
- nameMap.set(key, []);
619
- nameMap.get(key).push(entity);
620
- }
621
- const duplicateGroups = [];
622
- for (const [canonicalName, group] of nameMap) {
623
- if (group.length > 1) {
624
- const enriched = [];
625
- for (const e of group) {
626
- const edges = await storage.getEdgesForEntity(e.uuid);
627
- enriched.push({
628
- uuid: e.uuid,
629
- name: e.name,
630
- entity_type: e.entity_type,
631
- edge_count: edges.length,
632
- created_at: e.created_at,
633
- });
634
- }
635
- duplicateGroups.push({
636
- canonical_name: canonicalName,
637
- count: group.length,
638
- entities: enriched,
639
- });
640
- }
641
- }
642
- returnData.push({
643
- json: {
644
- operation: 'duplicateEntities',
645
- group_id: groupId,
646
- duplicate_groups: duplicateGroups,
647
- total_duplicate_groups: duplicateGroups.length,
648
- },
649
- });
650
- break;
651
- }
652
- case 'expireStaleEdges': {
653
- const groupId = this.getNodeParameter('groupId', i);
654
- if (!groupId) {
655
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Expire Stale Edges operation', { itemIndex: i });
656
- }
657
- const dryRun = this.getNodeParameter('dryRun', i, true);
658
- const data = await storage.exportGraph(groupId);
659
- const entityUuids = new Set(data.entities.map((e) => e.uuid));
660
- const staleEdges = [];
661
- for (const edge of data.edges) {
662
- if (edge.expired_at)
663
- continue;
664
- if (!entityUuids.has(edge.source_node_uuid) ||
665
- !entityUuids.has(edge.target_node_uuid)) {
666
- staleEdges.push({
667
- uuid: edge.uuid,
668
- name: edge.name,
669
- fact: edge.fact,
670
- reason: 'dangling_reference',
671
- });
672
- }
673
- else if (edge.invalid_at && new Date(edge.invalid_at) < new Date()) {
674
- staleEdges.push({
675
- uuid: edge.uuid,
676
- name: edge.name,
677
- fact: edge.fact,
678
- reason: 'past_invalid_at',
679
- });
680
- }
681
- }
682
- if (!dryRun) {
683
- for (const edge of staleEdges) {
684
- await storage.updateEdge(edge.uuid, { expired_at: (0, temporal_1.nowIso)() });
685
- }
686
- }
687
- returnData.push({
688
- json: {
689
- operation: 'expireStaleEdges',
690
- group_id: groupId,
691
- dry_run: dryRun,
692
- stale_edges: staleEdges,
693
- total_stale: staleEdges.length,
694
- expired: !dryRun,
695
- },
696
- });
697
- break;
698
- }
699
- case 'detectCommunities': {
700
- const groupId = this.getNodeParameter('groupId', i);
701
- if (!groupId) {
702
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Group ID is required for Detect Communities operation', { itemIndex: i });
703
- }
704
- const minCommunitySize = this.getNodeParameter('minCommunitySize', i, 2);
705
- const generateSummaries = this.getNodeParameter('generateSummaries', i, false);
706
- const detector = new CommunityDetector_1.CommunityDetector(storage);
707
- let result = await detector.detect(groupId, { minCommunitySize });
708
- if (generateSummaries) {
709
- const extractionCreds = await this.getCredentials('engramExtractionApi');
710
- const model = this.getNodeParameter('summaryModel', i, '');
711
- const concurrency = this.getNodeParameter('summaryConcurrency', i, 3);
712
- if (!model) {
713
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Summary Model is required when Generate Summaries is enabled', { itemIndex: i });
714
- }
715
- const llm = new LlmClient_1.LlmClient({
716
- apiKey: extractionCreds.apiKey,
717
- baseUrl: extractionCreds.baseUrl,
718
- model,
719
- });
720
- const summarizer = new CommunitySummarizer_1.CommunitySummarizer(llm);
721
- result = await summarizer.summarizeAll(result, concurrency);
722
- }
723
- returnData.push({
724
- json: {
725
- total_entities: result.total_entities,
726
- unclustered_entities: result.unclustered_entities,
727
- detection_method: result.detection_method,
728
- community_count: result.communities.length,
729
- communities: result.communities.map((c) => ({
730
- id: c.id,
731
- label: c.label,
732
- summary: c.summary,
733
- entity_count: c.entity_count,
734
- edge_count: c.edge_count,
735
- key_entities: c.key_entities,
736
- members: c.members.map((m) => ({
737
- uuid: m.entity.uuid,
738
- name: m.entity.name,
739
- entity_type: m.entity.entity_type,
740
- edge_count: m.edges.length,
741
- })),
742
- })),
743
- },
744
- });
745
- break;
471
+ const beforeLength = returnData.length;
472
+ if (resource === 'monitoring') {
473
+ await executeMonitoring(ctx, storage, operation, i, returnData, backend);
474
+ }
475
+ else if (resource === 'lifecycle') {
476
+ await executeLifecycle(ctx, storage, operation, i, returnData);
477
+ }
478
+ else if (resource === 'hygiene') {
479
+ await executeHygiene(ctx, storage, operation, i, returnData);
480
+ }
481
+ else if (resource === 'portability') {
482
+ await executePortability(ctx, storage, operation, i, returnData);
483
+ }
484
+ else if (resource === 'analysis') {
485
+ await executeAnalysis(ctx, storage, operation, i, returnData);
486
+ }
487
+ if (returnData.length === beforeLength) {
488
+ await executeLifecycle(ctx, storage, operation, i, returnData);
489
+ if (returnData.length === beforeLength) {
490
+ await executeHygiene(ctx, storage, operation, i, returnData);
746
491
  }
747
- case 'export': {
748
- const groupId = this.getNodeParameter('groupIdFilter', i, '');
749
- const data = await storage.exportGraph(groupId || undefined);
750
- returnData.push({ json: data });
751
- break;
492
+ if (returnData.length === beforeLength) {
493
+ await executePortability(ctx, storage, operation, i, returnData);
752
494
  }
753
- case 'import': {
754
- const importData = this.getNodeParameter('importData', i);
755
- if (!importData ||
756
- typeof importData !== 'object' ||
757
- !Array.isArray(importData.entities) ||
758
- !Array.isArray(importData.edges) ||
759
- !Array.isArray(importData.episodes)) {
760
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid import data format', {
761
- itemIndex: i,
762
- description: 'Import data must be a JSON object with "entities", "edges", and "episodes" arrays. Use data from a previous Export operation.',
763
- });
764
- }
765
- await storage.importGraph(importData);
766
- const graphData = importData;
767
- returnData.push({
768
- json: {
769
- success: true,
770
- operation: 'import',
771
- imported: {
772
- entities: graphData.entities.length,
773
- edges: graphData.edges.length,
774
- episodes: graphData.episodes.length,
775
- },
776
- },
777
- });
778
- break;
495
+ if (returnData.length === beforeLength) {
496
+ await executeAnalysis(ctx, storage, operation, i, returnData);
779
497
  }
780
498
  }
781
499
  }
782
500
  catch (error) {
783
501
  if (error instanceof n8n_workflow_1.NodeOperationError)
784
502
  throw error;
785
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Engram Admin error: ${error.message}`, { itemIndex: i });
503
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Engram Admin error: ${error.message}`, { itemIndex: i });
786
504
  }
787
505
  }
788
506
  }
@@ -795,4 +513,398 @@ class EngramAdmin {
795
513
  }
796
514
  }
797
515
  exports.EngramAdmin = EngramAdmin;
516
+ async function executeMonitoring(ctx, storage, operation, i, returnData, backend) {
517
+ if (operation === 'stats') {
518
+ const groupId = ctx.getNodeParameter('groupIdFilter', i, '').trim();
519
+ const baseStats = await storage.getStats(groupId || undefined);
520
+ const data = await storage.exportGraph(groupId || undefined);
521
+ const activeEdges = data.edges.filter((e) => e.expired_at === null).length;
522
+ const expiredEdges = data.edges.filter((e) => e.expired_at !== null).length;
523
+ const groupCount = baseStats.group_ids.length;
524
+ const enhancedStats = {
525
+ ...baseStats,
526
+ group_count: groupCount,
527
+ active_edge_count: activeEdges,
528
+ expired_edge_count: expiredEdges,
529
+ avg_edges_per_entity: baseStats.entity_count > 0
530
+ ? Math.round((baseStats.edge_count / baseStats.entity_count) * 100) / 100
531
+ : 0,
532
+ avg_episodes_per_group: groupCount > 0 ? Math.round((baseStats.episode_count / groupCount) * 100) / 100 : 0,
533
+ storage_backend: backend,
534
+ data_span_days: baseStats.oldest_episode && baseStats.newest_episode
535
+ ? Math.round(((new Date(baseStats.newest_episode).getTime() -
536
+ new Date(baseStats.oldest_episode).getTime()) /
537
+ (1000 * 60 * 60 * 24)) *
538
+ 10) / 10
539
+ : null,
540
+ };
541
+ returnData.push({ json: enhancedStats });
542
+ }
543
+ else if (operation === 'listGroups') {
544
+ const includeStats = ctx.getNodeParameter('includeStats', i, true);
545
+ const limit = ctx.getNodeParameter('limit', i, 100);
546
+ const globalStats = await storage.getStats();
547
+ const allGroupIds = globalStats.group_ids.slice(0, limit);
548
+ if (includeStats) {
549
+ for (const gid of allGroupIds) {
550
+ const gStats = await storage.getStats(gid);
551
+ returnData.push({
552
+ json: {
553
+ group_id: gid,
554
+ entity_count: gStats.entity_count,
555
+ edge_count: gStats.edge_count,
556
+ episode_count: gStats.episode_count,
557
+ entity_types: gStats.entity_types,
558
+ oldest_episode: gStats.oldest_episode,
559
+ newest_episode: gStats.newest_episode,
560
+ },
561
+ });
562
+ }
563
+ }
564
+ else {
565
+ for (const gid of allGroupIds) {
566
+ returnData.push({ json: { group_id: gid } });
567
+ }
568
+ }
569
+ if (returnData.length === 0) {
570
+ returnData.push({
571
+ json: {
572
+ message: 'No groups found',
573
+ total_groups: 0,
574
+ },
575
+ });
576
+ }
577
+ }
578
+ else if (operation === 'groupStats') {
579
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
580
+ if (!groupId) {
581
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Group Stats operation', { itemIndex: i });
582
+ }
583
+ const gStats = await storage.getStats(groupId);
584
+ const gData = await storage.exportGraph(groupId);
585
+ const activeEdges = gData.edges.filter((e) => e.expired_at === null).length;
586
+ const expiredEdges = gData.edges.filter((e) => e.expired_at !== null).length;
587
+ const relationshipTypes = {};
588
+ for (const edge of gData.edges) {
589
+ relationshipTypes[edge.name] = (relationshipTypes[edge.name] || 0) + 1;
590
+ }
591
+ returnData.push({
592
+ json: {
593
+ group_id: groupId,
594
+ entity_count: gStats.entity_count,
595
+ edge_count: gStats.edge_count,
596
+ episode_count: gStats.episode_count,
597
+ entity_types: gStats.entity_types,
598
+ relationship_types: relationshipTypes,
599
+ active_edge_count: activeEdges,
600
+ expired_edge_count: expiredEdges,
601
+ avg_edges_per_entity: gStats.entity_count > 0
602
+ ? Math.round((gStats.edge_count / gStats.entity_count) * 100) / 100
603
+ : 0,
604
+ oldest_episode: gStats.oldest_episode,
605
+ newest_episode: gStats.newest_episode,
606
+ data_span_days: gStats.oldest_episode && gStats.newest_episode
607
+ ? Math.round(((new Date(gStats.newest_episode).getTime() -
608
+ new Date(gStats.oldest_episode).getTime()) /
609
+ (1000 * 60 * 60 * 24)) *
610
+ 10) / 10
611
+ : null,
612
+ },
613
+ });
614
+ }
615
+ }
616
+ async function executeLifecycle(ctx, storage, operation, i, returnData) {
617
+ if (operation === 'applyRetention') {
618
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
619
+ if (!groupId) {
620
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Apply Retention operation', { itemIndex: i });
621
+ }
622
+ const retentionType = ctx.getNodeParameter('retentionType', i);
623
+ const retentionValue = ctx.getNodeParameter('retentionValue', i);
624
+ const removed = await storage.applyRetention(groupId, {
625
+ type: retentionType,
626
+ value: retentionValue,
627
+ });
628
+ returnData.push({
629
+ json: {
630
+ success: true,
631
+ operation: 'applyRetention',
632
+ group_id: groupId,
633
+ policy: { type: retentionType, value: retentionValue },
634
+ episodes_removed: removed,
635
+ },
636
+ });
637
+ }
638
+ else if (operation === 'clearGroup') {
639
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
640
+ if (!groupId) {
641
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Clear Group operation', {
642
+ itemIndex: i,
643
+ description: 'Provide the session/group ID whose data you want to delete.',
644
+ });
645
+ }
646
+ await storage.clearGroup(groupId);
647
+ returnData.push({
648
+ json: { success: true, operation: 'clearGroup', group_id: groupId },
649
+ });
650
+ }
651
+ else if (operation === 'bulkClearGroups') {
652
+ const confirmDestructive = ctx.getNodeParameter('confirmDestructive', i, false);
653
+ if (!confirmDestructive) {
654
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Confirm Destructive must be enabled to proceed with Bulk Clear Groups', {
655
+ itemIndex: i,
656
+ description: 'This is a safety measure. Enable "Confirm Destructive" to proceed.',
657
+ });
658
+ }
659
+ const groupIdsRaw = ctx.getNodeParameter('groupIds', i);
660
+ let groupIds;
661
+ if (Array.isArray(groupIdsRaw)) {
662
+ groupIds = groupIdsRaw.filter((id) => typeof id === 'string' && id.length > 0);
663
+ }
664
+ else {
665
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group IDs must be a JSON array of strings', {
666
+ itemIndex: i,
667
+ });
668
+ }
669
+ if (groupIds.length === 0) {
670
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group IDs array is empty', {
671
+ itemIndex: i,
672
+ });
673
+ }
674
+ const cleared = [];
675
+ const failed = [];
676
+ for (const gid of groupIds) {
677
+ try {
678
+ await storage.clearGroup(gid);
679
+ cleared.push(gid);
680
+ }
681
+ catch (err) {
682
+ failed.push({ group_id: gid, error: err.message });
683
+ }
684
+ }
685
+ returnData.push({
686
+ json: {
687
+ success: failed.length === 0,
688
+ operation: 'bulkClearGroups',
689
+ cleared,
690
+ failed,
691
+ total_cleared: cleared.length,
692
+ total_failed: failed.length,
693
+ },
694
+ });
695
+ }
696
+ else if (operation === 'clearAll') {
697
+ await storage.clearAll();
698
+ returnData.push({
699
+ json: { success: true, operation: 'clearAll' },
700
+ });
701
+ }
702
+ }
703
+ async function executeHygiene(ctx, storage, operation, i, returnData) {
704
+ if (operation === 'orphanedEntities') {
705
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
706
+ if (!groupId) {
707
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Orphaned Entities operation', { itemIndex: i });
708
+ }
709
+ const deleteOrphans = ctx.getNodeParameter('deleteOrphans', i, false);
710
+ const entities = await storage.listEntities(groupId, { limit: 10000 });
711
+ const orphaned = [];
712
+ for (const entity of entities) {
713
+ const edges = await storage.getEdgesForEntity(entity.uuid);
714
+ if (edges.length === 0) {
715
+ orphaned.push({
716
+ uuid: entity.uuid,
717
+ name: entity.name,
718
+ entity_type: entity.entity_type,
719
+ summary: entity.summary,
720
+ created_at: entity.created_at,
721
+ });
722
+ }
723
+ }
724
+ if (deleteOrphans) {
725
+ for (const o of orphaned) {
726
+ await storage.deleteEntity(o.uuid);
727
+ }
728
+ }
729
+ returnData.push({
730
+ json: {
731
+ operation: 'orphanedEntities',
732
+ group_id: groupId,
733
+ orphaned,
734
+ total_orphaned: orphaned.length,
735
+ deleted: deleteOrphans,
736
+ },
737
+ });
738
+ }
739
+ else if (operation === 'duplicateEntities') {
740
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
741
+ if (!groupId) {
742
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Duplicate Entities operation', { itemIndex: i });
743
+ }
744
+ const entities = await storage.listEntities(groupId, { limit: 10000 });
745
+ const nameMap = new Map();
746
+ for (const entity of entities) {
747
+ const key = entity.name.toLowerCase().trim();
748
+ if (!nameMap.has(key))
749
+ nameMap.set(key, []);
750
+ nameMap.get(key).push(entity);
751
+ }
752
+ const duplicateGroups = [];
753
+ for (const [canonicalName, group] of nameMap) {
754
+ if (group.length > 1) {
755
+ const enriched = [];
756
+ for (const e of group) {
757
+ const edges = await storage.getEdgesForEntity(e.uuid);
758
+ enriched.push({
759
+ uuid: e.uuid,
760
+ name: e.name,
761
+ entity_type: e.entity_type,
762
+ edge_count: edges.length,
763
+ created_at: e.created_at,
764
+ });
765
+ }
766
+ duplicateGroups.push({
767
+ canonical_name: canonicalName,
768
+ count: group.length,
769
+ entities: enriched,
770
+ });
771
+ }
772
+ }
773
+ returnData.push({
774
+ json: {
775
+ operation: 'duplicateEntities',
776
+ group_id: groupId,
777
+ duplicate_groups: duplicateGroups,
778
+ total_duplicate_groups: duplicateGroups.length,
779
+ },
780
+ });
781
+ }
782
+ else if (operation === 'expireStaleEdges') {
783
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
784
+ if (!groupId) {
785
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Expire Stale Edges operation', { itemIndex: i });
786
+ }
787
+ const dryRun = ctx.getNodeParameter('dryRun', i, true);
788
+ const data = await storage.exportGraph(groupId);
789
+ const entityUuids = new Set(data.entities.map((e) => e.uuid));
790
+ const staleEdges = [];
791
+ for (const edge of data.edges) {
792
+ if (edge.expired_at)
793
+ continue;
794
+ if (!entityUuids.has(edge.source_node_uuid) || !entityUuids.has(edge.target_node_uuid)) {
795
+ staleEdges.push({
796
+ uuid: edge.uuid,
797
+ name: edge.name,
798
+ fact: edge.fact,
799
+ reason: 'dangling_reference',
800
+ });
801
+ }
802
+ else if (edge.invalid_at && new Date(edge.invalid_at) < new Date()) {
803
+ staleEdges.push({
804
+ uuid: edge.uuid,
805
+ name: edge.name,
806
+ fact: edge.fact,
807
+ reason: 'past_invalid_at',
808
+ });
809
+ }
810
+ }
811
+ if (!dryRun) {
812
+ for (const edge of staleEdges) {
813
+ await storage.updateEdge(edge.uuid, { expired_at: (0, temporal_1.nowIso)() });
814
+ }
815
+ }
816
+ returnData.push({
817
+ json: {
818
+ operation: 'expireStaleEdges',
819
+ group_id: groupId,
820
+ dry_run: dryRun,
821
+ stale_edges: staleEdges,
822
+ total_stale: staleEdges.length,
823
+ expired: !dryRun,
824
+ },
825
+ });
826
+ }
827
+ }
828
+ async function executePortability(ctx, storage, operation, i, returnData) {
829
+ if (operation === 'export') {
830
+ const groupId = ctx.getNodeParameter('groupIdFilter', i, '').trim();
831
+ const data = await storage.exportGraph(groupId || undefined);
832
+ returnData.push({ json: data });
833
+ }
834
+ else if (operation === 'import') {
835
+ const importData = ctx.getNodeParameter('importData', i);
836
+ if (!importData ||
837
+ typeof importData !== 'object' ||
838
+ !Array.isArray(importData.entities) ||
839
+ !Array.isArray(importData.edges) ||
840
+ !Array.isArray(importData.episodes)) {
841
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Invalid import data format', {
842
+ itemIndex: i,
843
+ description: 'Import data must be a JSON object with "entities", "edges", and "episodes" arrays. Use data from a previous Export operation.',
844
+ });
845
+ }
846
+ await storage.importGraph(importData);
847
+ const graphData = importData;
848
+ returnData.push({
849
+ json: {
850
+ success: true,
851
+ operation: 'import',
852
+ imported: {
853
+ entities: graphData.entities.length,
854
+ edges: graphData.edges.length,
855
+ episodes: graphData.episodes.length,
856
+ },
857
+ },
858
+ });
859
+ }
860
+ }
861
+ async function executeAnalysis(ctx, storage, operation, i, returnData) {
862
+ if (operation === 'detectCommunities') {
863
+ const groupId = ctx.getNodeParameter('groupId', i).trim();
864
+ if (!groupId) {
865
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Group ID is required for Detect Communities operation', { itemIndex: i });
866
+ }
867
+ const minCommunitySize = ctx.getNodeParameter('minCommunitySize', i, 2);
868
+ const generateSummaries = ctx.getNodeParameter('generateSummaries', i, false);
869
+ const detector = new CommunityDetector_1.CommunityDetector(storage);
870
+ let result = await detector.detect(groupId, { minCommunitySize });
871
+ if (generateSummaries) {
872
+ const extractionCreds = await ctx.getCredentials('engramExtractionApi');
873
+ const model = ctx.getNodeParameter('summaryModel', i, '');
874
+ const concurrency = ctx.getNodeParameter('summaryConcurrency', i, 3);
875
+ if (!model) {
876
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Summary Model is required when Generate Summaries is enabled', { itemIndex: i });
877
+ }
878
+ const llm = new LlmClient_1.LlmClient({
879
+ apiKey: extractionCreds.apiKey,
880
+ baseUrl: extractionCreds.baseUrl,
881
+ model,
882
+ });
883
+ const summarizer = new CommunitySummarizer_1.CommunitySummarizer(llm);
884
+ result = await summarizer.summarizeAll(result, concurrency);
885
+ }
886
+ returnData.push({
887
+ json: {
888
+ total_entities: result.total_entities,
889
+ unclustered_entities: result.unclustered_entities,
890
+ detection_method: result.detection_method,
891
+ community_count: result.communities.length,
892
+ communities: result.communities.map((c) => ({
893
+ id: c.id,
894
+ label: c.label,
895
+ summary: c.summary,
896
+ entity_count: c.entity_count,
897
+ edge_count: c.edge_count,
898
+ key_entities: c.key_entities,
899
+ members: c.members.map((m) => ({
900
+ uuid: m.entity.uuid,
901
+ name: m.entity.name,
902
+ entity_type: m.entity.entity_type,
903
+ edge_count: m.edges.length,
904
+ })),
905
+ })),
906
+ },
907
+ });
908
+ }
909
+ }
798
910
  //# sourceMappingURL=EngramAdmin.node.js.map