@connectorx/n8n-nodes-cortex 0.1.11 → 0.1.13

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 ADDED
@@ -0,0 +1,35 @@
1
+ # n8n-nodes-cortex
2
+
3
+ This is an n8n Community Node for interacting with the **Cortex API**. It allows you to send messages, manage conversations, and interact with Kanban resources within your Cortex environment.
4
+
5
+ ## Features
6
+
7
+ - **Authentication**: Supports standard Cortex User Tokens and API Tokens.
8
+ - **Dynamic Tenant Selection**: Automatically fetches accessible tenants.
9
+ - **User Tokens**: Lists all tenants the user has access to.
10
+ - **API Tokens**: Automatically detects the single tenant associated with the token.
11
+ - **Resources**:
12
+ - **Message**: Send text, media, notes, and reactions.
13
+ - **Conversation**: Update conversation status, owner, and column.
14
+
15
+ ## Installation
16
+
17
+ You can install this node directly in n8n via **Settings > Community Nodes**.
18
+
19
+ **Package Name**: `@connectorx/n8n-nodes-cortex`
20
+
21
+ ## Credentials
22
+
23
+ 1. **Base URL**: The URL to your Cortex API functions (e.g., `https://your-project.supabase.co/functions/v1/api`).
24
+ 2. **Access Token**: Your Cortex JWT or API Token (`cortex_...`).
25
+
26
+ ## Usage
27
+
28
+ ### Tenant Selection
29
+ The node requires you to select a **Tenant** to interact with.
30
+ - If you use a **User Token**, the dropdown will populate with all your tenants.
31
+ - If you use an **API Token** (`cortex_...`), the dropdown will automatically show the specific tenant bound to that token.
32
+
33
+ ### Operations
34
+ - **Send Message**: Supports various types (Text, Image, Video, Audio, Document, Voice, Note, Reaction).
35
+ - **Update Conversation**: Move cards between columns, change owners, or update status (Open/Closed/Snoozed).
@@ -11,11 +11,11 @@ class CortexApi {
11
11
  name: 'apiBaseUrl',
12
12
  type: 'string',
13
13
  default: 'https://api.your-cortex-instance.com',
14
- description: 'The base URL of the Cortex API (e.g. https://your-project.supabase.co/functions/v1/api) - Do not include trailing slash',
14
+ description: 'The base URL of the Cortex Integration API (e.g. https://your-project.supabase.co/functions/v1/api/integrations)',
15
15
  required: true,
16
16
  },
17
17
  {
18
- displayName: 'Access Token',
18
+ displayName: 'API Token',
19
19
  name: 'accessToken',
20
20
  type: 'string',
21
21
  typeOptions: {
@@ -29,7 +29,7 @@ class CortexApi {
29
29
  this.test = {
30
30
  request: {
31
31
  baseURL: '={{$credentials.apiBaseUrl.replace(/\\/$/, "")}}',
32
- url: '/functions/v1/api/auth/me',
32
+ url: '/validate',
33
33
  method: 'GET',
34
34
  headers: {
35
35
  Authorization: '={{ "Bearer " + $credentials.accessToken }}',
@@ -4,7 +4,7 @@ exports.Cortex = void 0;
4
4
  class Cortex {
5
5
  constructor() {
6
6
  this.description = {
7
- displayName: 'Cortex v1.2',
7
+ displayName: 'Cortex',
8
8
  name: 'cortex',
9
9
  icon: 'file:cortex.png',
10
10
  group: ['input'],
@@ -37,20 +37,13 @@ class Cortex {
37
37
  name: 'Conversation',
38
38
  value: 'conversation',
39
39
  },
40
+ {
41
+ name: 'Contact',
42
+ value: 'contact',
43
+ },
40
44
  ],
41
45
  default: 'message',
42
46
  },
43
- {
44
- displayName: 'Tenant Name',
45
- name: 'tenantId',
46
- type: 'options',
47
- typeOptions: {
48
- loadOptionsMethod: 'getTenants',
49
- },
50
- default: '',
51
- required: true,
52
- description: 'The Tenant to interact with',
53
- },
54
47
  {
55
48
  displayName: 'Operation',
56
49
  name: 'operation',
@@ -58,21 +51,19 @@ class Cortex {
58
51
  noDataExpression: true,
59
52
  displayOptions: {
60
53
  show: {
61
- resource: [
62
- 'message',
63
- ],
54
+ resource: ['message'],
64
55
  },
65
56
  },
66
57
  options: [
67
58
  {
68
59
  name: 'Send',
69
60
  value: 'send',
70
- description: 'Send a message (Text, Media, Note, Reaction)',
61
+ description: 'Send a message (Text, Media, Template, etc.)',
71
62
  },
72
63
  {
73
64
  name: 'Send Typing',
74
65
  value: 'sendTyping',
75
- description: 'Send a typing indicator',
66
+ description: 'Send a typing/recording indicator',
76
67
  },
77
68
  ],
78
69
  default: 'send',
@@ -84,98 +75,90 @@ class Cortex {
84
75
  noDataExpression: true,
85
76
  displayOptions: {
86
77
  show: {
87
- resource: [
88
- 'conversation',
89
- ],
78
+ resource: ['conversation'],
90
79
  },
91
80
  },
92
81
  options: [
93
82
  {
94
83
  name: 'Update',
95
84
  value: 'update',
96
- description: 'Update conversation (Column, Owner, Status)',
85
+ description: 'Move column or assign owner',
86
+ },
87
+ {
88
+ name: 'Get History',
89
+ value: 'getHistory',
90
+ description: 'Get conversation message history',
97
91
  },
98
92
  ],
99
93
  default: 'update',
100
94
  },
101
95
  {
102
- displayName: 'Conversation ID',
103
- name: 'conversation_id',
104
- type: 'string',
105
- default: '',
106
- required: true,
107
- description: 'The Conversation ID',
108
- },
109
- {
110
- displayName: 'Message Type',
111
- name: 'msg_type',
96
+ displayName: 'Operation',
97
+ name: 'operation',
112
98
  type: 'options',
99
+ noDataExpression: true,
113
100
  displayOptions: {
114
101
  show: {
115
- resource: ['message'],
116
- operation: ['send'],
102
+ resource: ['contact'],
117
103
  },
118
104
  },
119
105
  options: [
120
- { name: 'Text', value: 'text' },
121
- { name: 'Image', value: 'image' },
122
- { name: 'Video', value: 'video' },
123
- { name: 'Audio', value: 'audio' },
124
- { name: 'Document', value: 'document' },
125
- { name: 'Voice', value: 'voice' },
126
- { name: 'Internal Note', value: 'note' },
127
- { name: 'Reaction', value: 'reaction' },
106
+ {
107
+ name: 'Get',
108
+ value: 'get',
109
+ description: 'Get contact details',
110
+ },
111
+ {
112
+ name: 'Update',
113
+ value: 'update',
114
+ description: 'Update contact fields (name, email, tags, custom_data)',
115
+ },
128
116
  ],
129
- default: 'text',
117
+ default: 'get',
130
118
  },
131
119
  {
132
- displayName: 'Content (URL or Text)',
133
- name: 'content',
120
+ displayName: 'Conversation ID',
121
+ name: 'conversationId',
134
122
  type: 'string',
123
+ required: true,
135
124
  displayOptions: {
136
125
  show: {
137
- resource: ['message'],
138
- operation: ['send'],
139
- },
140
- hide: {
141
- msg_type: ['reaction'],
126
+ resource: ['message', 'conversation'],
142
127
  },
143
128
  },
144
129
  default: '',
145
- description: 'Text content or Media URL. For reactions, use the "Emoji" field if separate, or put emoji here.',
130
+ description: 'The UUID of the conversation',
146
131
  },
147
132
  {
148
- displayName: 'React to Message ID',
149
- name: 'react_to_message_id',
133
+ displayName: 'Contact ID',
134
+ name: 'contactId',
150
135
  type: 'string',
136
+ required: true,
151
137
  displayOptions: {
152
138
  show: {
153
- resource: ['message'],
154
- operation: ['send'],
155
- msg_type: ['reaction'],
139
+ resource: ['contact'],
156
140
  },
157
141
  },
158
142
  default: '',
159
- required: true,
160
- description: 'ID of the message to react to',
143
+ description: 'The UUID of the contact',
161
144
  },
162
145
  {
163
- displayName: 'Emoji',
146
+ displayName: 'Content (Text, URL, or Template JSON)',
164
147
  name: 'content',
165
148
  type: 'string',
149
+ required: true,
166
150
  displayOptions: {
167
151
  show: {
168
152
  resource: ['message'],
169
153
  operation: ['send'],
170
- msg_type: ['reaction'],
171
154
  },
172
155
  },
173
156
  default: '',
174
- description: 'Emoji to react with',
157
+ description: 'Message text, media URL, or JSON string for templates',
175
158
  },
176
159
  {
177
- displayName: 'Sender Type',
178
- name: 'sender_type',
160
+ displayName: 'Message Type',
161
+ name: 'msg_type',
179
162
  type: 'options',
180
163
  displayOptions: {
181
164
  show: {
@@ -184,10 +167,17 @@ class Cortex {
184
167
  },
185
168
  },
186
169
  options: [
187
- { name: 'Human Agent', value: 'human_agent' },
188
- { name: 'AI Agent', value: 'ai_agent' },
170
+ { name: 'Text', value: 'text' },
171
+ { name: 'Image', value: 'image' },
172
+ { name: 'Video', value: 'video' },
173
+ { name: 'Audio', value: 'audio' },
174
+ { name: 'Document', value: 'document' },
175
+ { name: 'Voice', value: 'voice' },
176
+ { name: 'Internal Note', value: 'internal_note' },
177
+ { name: 'Template', value: 'template' },
178
+ { name: 'Reaction', value: 'reaction' },
189
179
  ],
190
- default: 'human_agent',
180
+ default: 'text',
191
181
  },
192
182
  {
193
183
  displayName: 'Sender ID',
@@ -200,7 +190,21 @@ class Cortex {
200
190
  },
201
191
  },
202
192
  default: '',
203
- description: 'ID of the sender (e.g. system, user_email). Optional.',
193
+ description: 'Optional sender identifier (defaults to "agent")',
194
+ },
195
+ {
196
+ displayName: 'React to Message ID',
197
+ name: 'react_to_message_id',
198
+ type: 'string',
199
+ displayOptions: {
200
+ show: {
201
+ resource: ['message'],
202
+ operation: ['send'],
203
+ msg_type: ['reaction'],
204
+ },
205
+ },
206
+ default: '',
207
+ description: 'UUID of the message to react to',
204
208
  },
205
209
  {
206
210
  displayName: 'Reply To Message ID',
@@ -213,10 +217,10 @@ class Cortex {
213
217
  },
214
218
  hide: {
215
219
  msg_type: ['reaction'],
216
- }
220
+ },
217
221
  },
218
222
  default: '',
219
- description: 'ID of the message to reply to',
223
+ description: 'UUID of the message to reply to',
220
224
  },
221
225
  {
222
226
  displayName: 'Status',
@@ -230,7 +234,8 @@ class Cortex {
230
234
  },
231
235
  options: [
232
236
  { name: 'Typing', value: 'typing' },
233
- { name: 'Stop', value: 'stop' },
237
+ { name: 'Recording', value: 'recording' },
238
+ { name: 'Stopped', value: 'stopped' },
234
239
  ],
235
240
  default: 'typing',
236
241
  },
@@ -240,7 +245,6 @@ class Cortex {
240
245
  type: 'options',
241
246
  typeOptions: {
242
247
  loadOptionsMethod: 'getColumns',
243
- loadOptionsDependsOn: ['tenantId'],
244
248
  },
245
249
  displayOptions: {
246
250
  show: {
@@ -257,7 +261,6 @@ class Cortex {
257
261
  type: 'options',
258
262
  typeOptions: {
259
263
  loadOptionsMethod: 'getUsers',
260
- loadOptionsDependsOn: ['tenantId'],
261
264
  },
262
265
  displayOptions: {
263
266
  show: {
@@ -269,71 +272,109 @@ class Cortex {
269
272
  description: 'New Owner ID. Leave empty to keep current.',
270
273
  },
271
274
  {
272
- displayName: 'Status',
273
- name: 'status',
274
- type: 'options',
275
+ displayName: 'Limit',
276
+ name: 'limit',
277
+ type: 'number',
275
278
  displayOptions: {
276
279
  show: {
277
280
  resource: ['conversation'],
281
+ operation: ['getHistory'],
282
+ },
283
+ },
284
+ typeOptions: {
285
+ minValue: 1,
286
+ maxValue: 100,
287
+ },
288
+ default: 20,
289
+ description: 'How many messages to retrieve',
290
+ },
291
+ {
292
+ displayName: 'Before (ISO Timestamp)',
293
+ name: 'before',
294
+ type: 'dateTime',
295
+ displayOptions: {
296
+ show: {
297
+ resource: ['conversation'],
298
+ operation: ['getHistory'],
299
+ },
300
+ },
301
+ default: '',
302
+ description: 'Timestamp for pagination',
303
+ },
304
+ {
305
+ displayName: 'Name',
306
+ name: 'contactName',
307
+ type: 'string',
308
+ displayOptions: {
309
+ show: {
310
+ resource: ['contact'],
278
311
  operation: ['update'],
279
312
  },
280
313
  },
281
- options: [
282
- { name: 'No Change', value: '' },
283
- { name: 'Open', value: 'open' },
284
- { name: 'Closed', value: 'closed' },
285
- { name: 'Snoozed', value: 'snoozed' },
286
- ],
287
314
  default: '',
288
- description: 'Update conversation status',
315
+ description: 'Updated contact name',
316
+ },
317
+ {
318
+ displayName: 'Email',
319
+ name: 'contactEmail',
320
+ type: 'string',
321
+ displayOptions: {
322
+ show: {
323
+ resource: ['contact'],
324
+ operation: ['update'],
325
+ },
326
+ },
327
+ default: '',
328
+ description: 'Updated contact email',
329
+ },
330
+ {
331
+ displayName: 'Tags (Comma Separated)',
332
+ name: 'contactTags',
333
+ type: 'string',
334
+ displayOptions: {
335
+ show: {
336
+ resource: ['contact'],
337
+ operation: ['update'],
338
+ },
339
+ },
340
+ default: '',
341
+ description: 'List of tags separated by commas',
342
+ },
343
+ {
344
+ displayName: 'Custom Data (JSON)',
345
+ name: 'contactCustomData',
346
+ type: 'string',
347
+ displayOptions: {
348
+ show: {
349
+ resource: ['contact'],
350
+ operation: ['update'],
351
+ },
352
+ },
353
+ default: '',
354
+ description: 'Custom properties in JSON format',
289
355
  },
290
356
  ],
291
357
  };
292
358
  this.methods = {
293
359
  loadOptions: {
294
- async getTenants() {
295
- const credentials = await this.getCredentials('cortexApi');
296
- const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
297
- const options = {
298
- headers: {
299
- 'Content-Type': 'application/json',
300
- 'Authorization': `Bearer ${credentials.accessToken}`,
301
- },
302
- method: 'GET',
303
- uri: `${baseUrl}/user-tenants`,
304
- json: true,
305
- };
306
- try {
307
- const response = await this.helpers.request(options);
308
- return response.map((t) => ({
309
- name: t.name || t.tenant_id,
310
- value: t.tenant_id
311
- }));
312
- }
313
- catch (error) {
314
- console.error('getTenants error:', error);
315
- return [];
316
- }
317
- },
318
360
  async getColumns() {
319
361
  const credentials = await this.getCredentials('cortexApi');
320
362
  const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
321
- const tenantId = this.getNodeParameter('tenantId');
322
- if (!tenantId) {
323
- return [];
324
- }
325
363
  const options = {
326
364
  headers: {
327
365
  'Content-Type': 'application/json',
328
366
  'Authorization': `Bearer ${credentials.accessToken}`,
329
367
  },
330
368
  method: 'GET',
331
- uri: `${baseUrl}/kanban/${tenantId}/columns`,
369
+ uri: `${baseUrl}/columns`,
332
370
  json: true,
333
371
  };
334
372
  try {
335
373
  const response = await this.helpers.request(options);
336
- return response;
374
+ return response.map((c) => ({
375
+ name: c.name || c.id,
376
+ value: c.id
377
+ }));
337
378
  }
338
379
  catch (error) {
339
380
  return [];
@@ -342,24 +383,20 @@ class Cortex {
342
383
  async getUsers() {
343
384
  const credentials = await this.getCredentials('cortexApi');
344
385
  const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
345
- const tenantId = this.getNodeParameter('tenantId');
346
- if (!tenantId) {
347
- return [];
348
- }
349
386
  const options = {
350
387
  headers: {
351
388
  'Content-Type': 'application/json',
352
389
  'Authorization': `Bearer ${credentials.accessToken}`,
353
390
  },
354
391
  method: 'GET',
355
- uri: `${baseUrl}/kanban/${tenantId}/users`,
392
+ uri: `${baseUrl}/users`,
356
393
  json: true,
357
394
  };
358
395
  try {
359
396
  const response = await this.helpers.request(options);
360
397
  return response.map((u) => ({
361
- name: u.name || u.user_id,
362
- value: u.user_id
398
+ name: u.name || u.email || u.id,
399
+ value: u.id
363
400
  }));
364
401
  }
365
402
  catch (error) {
@@ -378,9 +415,6 @@ class Cortex {
378
415
  const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
379
416
  for (let i = 0; i < items.length; i++) {
380
417
  try {
381
- const tenantId = this.getNodeParameter('tenantId', i);
382
- const conversationId = this.getNodeParameter('conversation_id', i);
383
- let responseData;
384
418
  const options = {
385
419
  headers: {
386
420
  'Content-Type': 'application/json',
@@ -388,60 +422,100 @@ class Cortex {
388
422
  },
389
423
  method: 'POST',
390
424
  uri: '',
391
- body: {},
392
425
  json: true,
393
426
  };
394
427
  if (resource === 'message') {
428
+ const conversationId = this.getNodeParameter('conversationId', i);
395
429
  if (operation === 'send') {
396
- options.method = 'POST';
397
430
  options.uri = `${baseUrl}/messages/send`;
398
- const msgType = this.getNodeParameter('msg_type', i);
399
431
  const content = this.getNodeParameter('content', i);
400
- const senderType = this.getNodeParameter('sender_type', i);
432
+ const msgType = this.getNodeParameter('msg_type', i);
401
433
  const senderId = this.getNodeParameter('sender_id', i);
402
434
  const replyTo = this.getNodeParameter('reply_to_message_id', i);
403
435
  const reactTo = this.getNodeParameter('react_to_message_id', i);
404
436
  const body = {
405
437
  conversation_id: conversationId,
406
- tenant_id: tenantId,
438
+ content: content,
407
439
  type: msgType,
408
- sender_type: senderType,
409
440
  };
410
- if (content)
411
- body.content = content;
412
441
  if (senderId)
413
442
  body.sender_id = senderId;
414
- if (replyTo && msgType !== 'reaction')
443
+ if (replyTo)
415
444
  body.reply_to_message_id = replyTo;
416
445
  if (msgType === 'reaction' && reactTo)
417
446
  body.react_to_message_id = reactTo;
418
447
  options.body = body;
419
448
  }
420
449
  else if (operation === 'sendTyping') {
421
- options.method = 'POST';
422
450
  options.uri = `${baseUrl}/messages/typing`;
423
451
  const status = this.getNodeParameter('typing_status', i);
424
452
  options.body = {
425
453
  conversation_id: conversationId,
426
- tenant_id: tenantId,
427
454
  status: status,
428
455
  };
429
456
  }
430
457
  }
431
458
  else if (resource === 'conversation') {
459
+ const conversationId = this.getNodeParameter('conversationId', i);
432
460
  if (operation === 'update') {
433
461
  options.method = 'PATCH';
434
- options.uri = `${baseUrl}/kanban/${tenantId}/conversations/${conversationId}`;
462
+ options.uri = `${baseUrl}/conversations/${conversationId}`;
435
463
  const columnId = this.getNodeParameter('column_id', i);
436
464
  const ownerId = this.getNodeParameter('owner_id', i);
437
- const status = this.getNodeParameter('status', i);
438
465
  const body = {};
439
466
  if (columnId)
440
467
  body.column_id = columnId;
441
468
  if (ownerId)
442
469
  body.owner_id = ownerId;
443
- if (status)
444
- body.status = status;
470
+ if (Object.keys(body).length > 0) {
471
+ options.body = body;
472
+ }
473
+ else {
474
+ returnData.push({ json: { status: 'no_changes' } });
475
+ continue;
476
+ }
477
+ }
478
+ else if (operation === 'getHistory') {
479
+ options.method = 'GET';
480
+ const limit = this.getNodeParameter('limit', i);
481
+ const before = this.getNodeParameter('before', i);
482
+ options.uri = `${baseUrl}/conversations/${conversationId}/messages`;
483
+ options.qs = {
484
+ limit: limit,
485
+ };
486
+ if (before)
487
+ options.qs.before = before;
488
+ }
489
+ }
490
+ else if (resource === 'contact') {
491
+ const contactId = this.getNodeParameter('contactId', i);
492
+ if (operation === 'get') {
493
+ options.method = 'GET';
494
+ options.uri = `${baseUrl}/contacts/${contactId}`;
495
+ }
496
+ else if (operation === 'update') {
497
+ options.method = 'PATCH';
498
+ options.uri = `${baseUrl}/contacts/${contactId}`;
499
+ const name = this.getNodeParameter('contactName', i);
500
+ const email = this.getNodeParameter('contactEmail', i);
501
+ const tagsStr = this.getNodeParameter('contactTags', i);
502
+ const customDataStr = this.getNodeParameter('contactCustomData', i);
503
+ const body = {};
504
+ if (name)
505
+ body.name = name;
506
+ if (email)
507
+ body.email = email;
508
+ if (tagsStr) {
509
+ body.tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
510
+ }
511
+ if (customDataStr) {
512
+ try {
513
+ body.custom_data = JSON.parse(customDataStr);
514
+ }
515
+ catch (e) {
516
+ throw new Error('Custom Data must be a valid JSON string');
517
+ }
518
+ }
445
519
  if (Object.keys(body).length > 0) {
446
520
  options.body = body;
447
521
  }
@@ -451,8 +525,8 @@ class Cortex {
451
525
  }
452
526
  }
453
527
  }
454
- responseData = await this.helpers.request(options);
455
- returnData.push(responseData);
528
+ const responseData = await this.helpers.request(options);
529
+ returnData.push(Array.isArray(responseData) ? { json: responseData } : responseData);
456
530
  }
457
531
  catch (error) {
458
532
  if (this.continueOnFail()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectorx/n8n-nodes-cortex",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "n8n nodes for Cortex API",
5
5
  "keywords": [
6
6
  "n8n-community-node-package"
@@ -44,4 +44,4 @@
44
44
  "peerDependencies": {
45
45
  "n8n-workflow": "*"
46
46
  }
47
- }
47
+ }