@stackflo-labs/n8n-nodes-retainr 0.2.7 → 0.3.1

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 CHANGED
@@ -72,6 +72,40 @@ Use the **Dedup Threshold** field (e.g., `0.95`) when storing. If a sufficiently
72
72
 
73
73
  This node has `usableAsTool` enabled — you can use it directly as a tool inside n8n's **AI Agent** node, letting the agent decide when to store or recall memories autonomously.
74
74
 
75
+ ## Demo Video
76
+
77
+ A recorded demo covering all n8n Creator Portal requirements is at:
78
+
79
+ ```
80
+ products/retainr/web/public/demo/n8n-demo.mp4
81
+ ```
82
+
83
+ Full disk path (Windows): `D:\dev\datadir\stackflo\products\retainr\web\public\demo\n8n-demo.mp4`
84
+
85
+ **To re-record** (e.g. after a node update):
86
+
87
+ ```bash
88
+ # 1. Start n8n with the node pre-installed
89
+ cd packages/n8n-node
90
+ docker compose -f e2e/docker-compose.yml up --build -d
91
+
92
+ # 2. Record — no API keys needed, the script handles everything
93
+ cd ../../products/retainr/web
94
+ N8N_BASE_URL=http://127.0.0.1:5678 \
95
+ node scripts/record-n8n-demo.mjs
96
+ # → public/demo/n8n-demo-raw.webm
97
+
98
+ # 3. Post-process to MP4
99
+ bash ../../packages/n8n-node/demo/post-process.sh public/demo/n8n-demo-raw.webm
100
+ # → public/demo/n8n-demo.mp4
101
+
102
+ # 4. Tear down
103
+ cd ../../packages/n8n-node
104
+ docker compose -f e2e/docker-compose.yml down
105
+ ```
106
+
107
+ See `demo/README.md` for full details.
108
+
75
109
  ## API Documentation
76
110
 
77
111
  Full API reference: [retainr.dev/docs/api](https://retainr.dev/docs/api)
@@ -14,7 +14,7 @@ class RetainrApi {
14
14
  typeOptions: { password: true },
15
15
  default: '',
16
16
  placeholder: 'rec_live_...',
17
- description: 'Your retainr.dev API key. Create one at https://retainr.dev/dashboard.',
17
+ description: 'Your retainr.dev API key. Each key is scoped to one workspace. Create keys at https://retainr.dev/dashboard.',
18
18
  },
19
19
  {
20
20
  displayName: 'Base URL',
@@ -10,7 +10,7 @@ class Retainr {
10
10
  icon: 'file:retainr.svg',
11
11
  group: ['transform'],
12
12
  version: 1,
13
- subtitle: '={{$parameter["operation"] + " " + $parameter["resource"]}}',
13
+ subtitle: '={{"getContext":"Get Context","store":"Store Memory","search":"Search Memories","list":"List Memories","delete":"Delete Memories","getUsage":"Get Usage"}[$parameter["operation"]] ?? $parameter["operation"]}}',
14
14
  description: 'Store, search, and retrieve AI agent memories.',
15
15
  defaults: {
16
16
  name: 'Retainr',
@@ -217,6 +217,21 @@ class Retainr {
217
217
  },
218
218
  },
219
219
  },
220
+ {
221
+ displayName: 'Namespace',
222
+ name: 'namespace',
223
+ type: 'string',
224
+ required: false,
225
+ default: '',
226
+ placeholder: 'sarah-chen-001',
227
+ description: 'Customer or tenant identifier within this workspace. Workspace is set by the API key; namespace groups memories per customer.',
228
+ displayOptions: {
229
+ show: {
230
+ resource: ['memory'],
231
+ operation: ['store'],
232
+ },
233
+ },
234
+ },
220
235
  {
221
236
  displayName: 'Additional Fields',
222
237
  name: 'storeAdditionalFields',
@@ -248,13 +263,6 @@ class Retainr {
248
263
  default: '{}',
249
264
  description: 'Arbitrary key-value pairs as JSON object.',
250
265
  },
251
- {
252
- displayName: 'Namespace',
253
- name: 'namespace',
254
- type: 'string',
255
- default: '',
256
- description: 'Free-form grouping label for organizing memories.',
257
- },
258
266
  {
259
267
  displayName: 'Tags',
260
268
  name: 'tags',
@@ -295,6 +303,21 @@ class Retainr {
295
303
  },
296
304
  },
297
305
  },
306
+ {
307
+ displayName: 'Namespace',
308
+ name: 'namespace',
309
+ type: 'string',
310
+ required: false,
311
+ default: '',
312
+ placeholder: 'sarah-chen-001',
313
+ description: 'Filter by customer/tenant namespace within this workspace.',
314
+ displayOptions: {
315
+ show: {
316
+ resource: ['memory'],
317
+ operation: ['search'],
318
+ },
319
+ },
320
+ },
298
321
  {
299
322
  displayName: 'Additional Fields',
300
323
  name: 'searchAdditionalFields',
@@ -325,13 +348,6 @@ class Retainr {
325
348
  default: 50,
326
349
  description: 'Max number of results to return.',
327
350
  },
328
- {
329
- displayName: 'Namespace',
330
- name: 'namespace',
331
- type: 'string',
332
- default: '',
333
- description: 'Filter by namespace.',
334
- },
335
351
  {
336
352
  displayName: 'Scope',
337
353
  name: 'scope',
@@ -430,6 +446,21 @@ class Retainr {
430
446
  },
431
447
  },
432
448
  },
449
+ {
450
+ displayName: 'Namespace',
451
+ name: 'namespace',
452
+ type: 'string',
453
+ required: false,
454
+ default: '',
455
+ placeholder: 'sarah-chen-001',
456
+ description: 'Filter by customer/tenant namespace within this workspace.',
457
+ displayOptions: {
458
+ show: {
459
+ resource: ['memory'],
460
+ operation: ['getContext'],
461
+ },
462
+ },
463
+ },
433
464
  {
434
465
  displayName: 'Additional Fields',
435
466
  name: 'contextAdditionalFields',
@@ -460,13 +491,6 @@ class Retainr {
460
491
  default: 5,
461
492
  description: 'Maximum number of memories to include in the context (API max 20).',
462
493
  },
463
- {
464
- displayName: 'Namespace',
465
- name: 'namespace',
466
- type: 'string',
467
- default: '',
468
- description: 'Filter by namespace.',
469
- },
470
494
  {
471
495
  displayName: 'Scope',
472
496
  name: 'scope',
@@ -517,6 +541,21 @@ class Retainr {
517
541
  // ==================================================================
518
542
  // Fields — Memory > List
519
543
  // ==================================================================
544
+ {
545
+ displayName: 'Namespace',
546
+ name: 'namespace',
547
+ type: 'string',
548
+ required: false,
549
+ default: '',
550
+ placeholder: 'sarah-chen-001',
551
+ description: 'Filter by customer/tenant namespace within this workspace.',
552
+ displayOptions: {
553
+ show: {
554
+ resource: ['memory'],
555
+ operation: ['list'],
556
+ },
557
+ },
558
+ },
520
559
  {
521
560
  displayName: 'Additional Fields',
522
561
  name: 'listAdditionalFields',
@@ -547,13 +586,6 @@ class Retainr {
547
586
  default: 50,
548
587
  description: 'Max number of results to return.',
549
588
  },
550
- {
551
- displayName: 'Namespace',
552
- name: 'namespace',
553
- type: 'string',
554
- default: '',
555
- description: 'Filter by namespace.',
556
- },
557
589
  {
558
590
  displayName: 'Offset',
559
591
  name: 'offset',
@@ -622,6 +654,21 @@ class Retainr {
622
654
  },
623
655
  },
624
656
  },
657
+ {
658
+ displayName: 'Namespace',
659
+ name: 'namespace',
660
+ type: 'string',
661
+ required: false,
662
+ default: '',
663
+ placeholder: 'sarah-chen-001',
664
+ description: 'Filter by customer/tenant namespace within this workspace.',
665
+ displayOptions: {
666
+ show: {
667
+ resource: ['memory'],
668
+ operation: ['delete'],
669
+ },
670
+ },
671
+ },
625
672
  {
626
673
  displayName: 'Additional Fields',
627
674
  name: 'deleteAdditionalFields',
@@ -642,13 +689,6 @@ class Retainr {
642
689
  default: '',
643
690
  description: 'Filter by agent ID.',
644
691
  },
645
- {
646
- displayName: 'Namespace',
647
- name: 'namespace',
648
- type: 'string',
649
- default: '',
650
- description: 'Filter by namespace.',
651
- },
652
692
  {
653
693
  displayName: 'Session ID',
654
694
  name: 'sessionId',
@@ -768,6 +808,7 @@ function parseTags(raw) {
768
808
  async function storeMemory(i, baseUrl) {
769
809
  const content = this.getNodeParameter('content', i);
770
810
  const scope = this.getNodeParameter('scope', i);
811
+ const namespace = this.getNodeParameter('namespace', i, '');
771
812
  const additional = this.getNodeParameter('storeAdditionalFields', i);
772
813
  const body = { content, scope };
773
814
  // Scope-specific IDs
@@ -780,9 +821,13 @@ async function storeMemory(i, baseUrl) {
780
821
  else if (scope === 'agent') {
781
822
  body.agent_id = this.getNodeParameter('agentId', i);
782
823
  }
783
- // Optional fields
784
- if (additional.namespace)
824
+ // Namespace — top-level parameter takes priority, fall back to additional fields
825
+ if (namespace) {
826
+ body.namespace = namespace;
827
+ }
828
+ else if (additional.namespace) {
785
829
  body.namespace = additional.namespace;
830
+ }
786
831
  if (additional.tags)
787
832
  body.tags = parseTags(additional.tags);
788
833
  if (additional.ttlSeconds)
@@ -800,6 +845,7 @@ async function storeMemory(i, baseUrl) {
800
845
  }
801
846
  async function searchMemories(i, baseUrl) {
802
847
  const query = this.getNodeParameter('query', i);
848
+ const namespace = this.getNodeParameter('namespace', i, '');
803
849
  const additional = this.getNodeParameter('searchAdditionalFields', i);
804
850
  const body = { query };
805
851
  if (additional.scope)
@@ -810,8 +856,13 @@ async function searchMemories(i, baseUrl) {
810
856
  body.user_id = additional.userId;
811
857
  if (additional.agentId)
812
858
  body.agent_id = additional.agentId;
813
- if (additional.namespace)
859
+ // Namespace — top-level parameter takes priority, fall back to additional fields
860
+ if (namespace) {
861
+ body.namespace = namespace;
862
+ }
863
+ else if (additional.namespace) {
814
864
  body.namespace = additional.namespace;
865
+ }
815
866
  if (additional.tags)
816
867
  body.tags = parseTags(additional.tags);
817
868
  if (additional.limit)
@@ -823,6 +874,7 @@ async function searchMemories(i, baseUrl) {
823
874
  async function getContext(i, baseUrl) {
824
875
  const query = this.getNodeParameter('query', i);
825
876
  const format = this.getNodeParameter('format', i);
877
+ const namespace = this.getNodeParameter('namespace', i, '');
826
878
  const additional = this.getNodeParameter('contextAdditionalFields', i);
827
879
  const body = { query, format };
828
880
  if (additional.scope)
@@ -833,8 +885,13 @@ async function getContext(i, baseUrl) {
833
885
  body.user_id = additional.userId;
834
886
  if (additional.agentId)
835
887
  body.agent_id = additional.agentId;
836
- if (additional.namespace)
888
+ // Namespace — top-level parameter takes priority, fall back to additional fields
889
+ if (namespace) {
890
+ body.namespace = namespace;
891
+ }
892
+ else if (additional.namespace) {
837
893
  body.namespace = additional.namespace;
894
+ }
838
895
  if (additional.tags)
839
896
  body.tags = parseTags(additional.tags);
840
897
  if (additional.maxMemories)
@@ -844,6 +901,7 @@ async function getContext(i, baseUrl) {
844
901
  return apiRequest.call(this, 'POST', baseUrl, '/v1/memories/context', body);
845
902
  }
846
903
  async function listMemories(i, baseUrl) {
904
+ const namespace = this.getNodeParameter('namespace', i, '');
847
905
  const additional = this.getNodeParameter('listAdditionalFields', i);
848
906
  const qs = {};
849
907
  if (additional.scope)
@@ -854,8 +912,13 @@ async function listMemories(i, baseUrl) {
854
912
  qs.user_id = additional.userId;
855
913
  if (additional.agentId)
856
914
  qs.agent_id = additional.agentId;
857
- if (additional.namespace)
915
+ // Namespace — top-level parameter takes priority, fall back to additional fields
916
+ if (namespace) {
917
+ qs.namespace = namespace;
918
+ }
919
+ else if (additional.namespace) {
858
920
  qs.namespace = additional.namespace;
921
+ }
859
922
  if (additional.tags)
860
923
  qs.tags = additional.tags;
861
924
  if (additional.limit)
@@ -866,6 +929,7 @@ async function listMemories(i, baseUrl) {
866
929
  }
867
930
  async function deleteMemories(i, baseUrl) {
868
931
  const scope = this.getNodeParameter('deleteScope', i);
932
+ const namespace = this.getNodeParameter('namespace', i, '');
869
933
  const additional = this.getNodeParameter('deleteAdditionalFields', i);
870
934
  const body = { scope };
871
935
  if (additional.sessionId)
@@ -874,8 +938,13 @@ async function deleteMemories(i, baseUrl) {
874
938
  body.user_id = additional.userId;
875
939
  if (additional.agentId)
876
940
  body.agent_id = additional.agentId;
877
- if (additional.namespace)
941
+ // Namespace — top-level parameter takes priority, fall back to additional fields
942
+ if (namespace) {
943
+ body.namespace = namespace;
944
+ }
945
+ else if (additional.namespace) {
878
946
  body.namespace = additional.namespace;
947
+ }
879
948
  return apiRequest.call(this, 'DELETE', baseUrl, '/v1/memories', body);
880
949
  }
881
950
  async function getWorkspaceInfo(baseUrl) {
@@ -1,9 +1,4 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60" fill="none">
2
- <rect width="60" height="60" rx="14" fill="#6366F1"/>
3
- <g stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none">
4
- <ellipse cx="30" cy="19" rx="12" ry="5"/>
5
- <path d="M18 19v9c0 2.8 5.4 5 12 5s12-2.2 12-5v-9"/>
6
- <path d="M18 28v9c0 2.8 5.4 5 12 5s12-2.2 12-5v-9"/>
7
- </g>
8
- <circle cx="30" cy="28" r="2.5" fill="#fff"/>
2
+ <path d="M30 13 A17 17 0 1 1 13 30" stroke="#7C3AED" stroke-width="6.5" stroke-linecap="round" fill="none"/>
3
+ <circle cx="13" cy="30" r="6.5" fill="#7C3AED"/>
9
4
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackflo-labs/n8n-nodes-retainr",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/stackflo-labs/n8n-nodes-retainr.git"
@@ -39,7 +39,10 @@
39
39
  "e2e": "node e2e/integration-test.mjs",
40
40
  "e2e:up": "docker compose -f e2e/docker-compose.yml up --build -d",
41
41
  "e2e:down": "docker compose -f e2e/docker-compose.yml down",
42
- "e2e:run": "npm run e2e:up && npm run e2e -- && npm run e2e:down"
42
+ "e2e:run": "npm run e2e:up && npm run e2e -- && npm run e2e:down",
43
+ "demo:up": "docker compose -f demo/docker-compose.demo.yml up --build -d",
44
+ "demo:down": "docker compose -f demo/docker-compose.demo.yml down",
45
+ "demo:record": "npm run demo:up && cd ../../products/retainr/web && node scripts/record-n8n-demo.mjs"
43
46
  },
44
47
  "publishConfig": {
45
48
  "access": "public"