@connectorx/n8n-nodes-cortex 0.1.12 → 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/integrations/validate',
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,83 +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',
278
+ displayOptions: {
279
+ show: {
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',
275
295
  displayOptions: {
276
296
  show: {
277
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'],
311
+ operation: ['update'],
312
+ },
313
+ },
314
+ default: '',
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'],
278
337
  operation: ['update'],
279
338
  },
280
339
  },
281
- options: [
282
- { name: 'No Change', value: '' },
283
- { name: 'Open', value: 'open' },
284
- { name: 'Closed', value: 'closed' },
285
- { name: 'Snoozed', value: 'snoozed' },
286
- ],
287
340
  default: '',
288
- description: 'Update conversation status',
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
- try {
315
- options.uri = `${baseUrl}/integrations/validate`;
316
- const validateResponse = await this.helpers.request(options);
317
- if (validateResponse.valid && validateResponse.tenant_id) {
318
- return [{
319
- name: `Current Tenant (${validateResponse.tenant_id.slice(0, 8)}...)`,
320
- value: validateResponse.tenant_id
321
- }];
322
- }
323
- }
324
- catch (e) {
325
- }
326
- console.error('getTenants error: Failed to fetch tenants via list or validate', error);
327
- return [];
328
- }
329
- },
330
360
  async getColumns() {
331
361
  const credentials = await this.getCredentials('cortexApi');
332
362
  const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
333
- const tenantId = this.getNodeParameter('tenantId');
334
- if (!tenantId) {
335
- return [];
336
- }
337
363
  const options = {
338
364
  headers: {
339
365
  'Content-Type': 'application/json',
340
366
  'Authorization': `Bearer ${credentials.accessToken}`,
341
367
  },
342
368
  method: 'GET',
343
- uri: `${baseUrl}/kanban/${tenantId}/columns`,
369
+ uri: `${baseUrl}/columns`,
344
370
  json: true,
345
371
  };
346
372
  try {
347
373
  const response = await this.helpers.request(options);
348
- return response;
374
+ return response.map((c) => ({
375
+ name: c.name || c.id,
376
+ value: c.id
377
+ }));
349
378
  }
350
379
  catch (error) {
351
380
  return [];
@@ -354,24 +383,20 @@ class Cortex {
354
383
  async getUsers() {
355
384
  const credentials = await this.getCredentials('cortexApi');
356
385
  const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
357
- const tenantId = this.getNodeParameter('tenantId');
358
- if (!tenantId) {
359
- return [];
360
- }
361
386
  const options = {
362
387
  headers: {
363
388
  'Content-Type': 'application/json',
364
389
  'Authorization': `Bearer ${credentials.accessToken}`,
365
390
  },
366
391
  method: 'GET',
367
- uri: `${baseUrl}/kanban/${tenantId}/users`,
392
+ uri: `${baseUrl}/users`,
368
393
  json: true,
369
394
  };
370
395
  try {
371
396
  const response = await this.helpers.request(options);
372
397
  return response.map((u) => ({
373
- name: u.name || u.user_id,
374
- value: u.user_id
398
+ name: u.name || u.email || u.id,
399
+ value: u.id
375
400
  }));
376
401
  }
377
402
  catch (error) {
@@ -390,9 +415,6 @@ class Cortex {
390
415
  const baseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
391
416
  for (let i = 0; i < items.length; i++) {
392
417
  try {
393
- const tenantId = this.getNodeParameter('tenantId', i);
394
- const conversationId = this.getNodeParameter('conversation_id', i);
395
- let responseData;
396
418
  const options = {
397
419
  headers: {
398
420
  'Content-Type': 'application/json',
@@ -400,60 +422,100 @@ class Cortex {
400
422
  },
401
423
  method: 'POST',
402
424
  uri: '',
403
- body: {},
404
425
  json: true,
405
426
  };
406
427
  if (resource === 'message') {
428
+ const conversationId = this.getNodeParameter('conversationId', i);
407
429
  if (operation === 'send') {
408
- options.method = 'POST';
409
430
  options.uri = `${baseUrl}/messages/send`;
410
- const msgType = this.getNodeParameter('msg_type', i);
411
431
  const content = this.getNodeParameter('content', i);
412
- const senderType = this.getNodeParameter('sender_type', i);
432
+ const msgType = this.getNodeParameter('msg_type', i);
413
433
  const senderId = this.getNodeParameter('sender_id', i);
414
434
  const replyTo = this.getNodeParameter('reply_to_message_id', i);
415
435
  const reactTo = this.getNodeParameter('react_to_message_id', i);
416
436
  const body = {
417
437
  conversation_id: conversationId,
418
- tenant_id: tenantId,
438
+ content: content,
419
439
  type: msgType,
420
- sender_type: senderType,
421
440
  };
422
- if (content)
423
- body.content = content;
424
441
  if (senderId)
425
442
  body.sender_id = senderId;
426
- if (replyTo && msgType !== 'reaction')
443
+ if (replyTo)
427
444
  body.reply_to_message_id = replyTo;
428
445
  if (msgType === 'reaction' && reactTo)
429
446
  body.react_to_message_id = reactTo;
430
447
  options.body = body;
431
448
  }
432
449
  else if (operation === 'sendTyping') {
433
- options.method = 'POST';
434
450
  options.uri = `${baseUrl}/messages/typing`;
435
451
  const status = this.getNodeParameter('typing_status', i);
436
452
  options.body = {
437
453
  conversation_id: conversationId,
438
- tenant_id: tenantId,
439
454
  status: status,
440
455
  };
441
456
  }
442
457
  }
443
458
  else if (resource === 'conversation') {
459
+ const conversationId = this.getNodeParameter('conversationId', i);
444
460
  if (operation === 'update') {
445
461
  options.method = 'PATCH';
446
- options.uri = `${baseUrl}/kanban/${tenantId}/conversations/${conversationId}`;
462
+ options.uri = `${baseUrl}/conversations/${conversationId}`;
447
463
  const columnId = this.getNodeParameter('column_id', i);
448
464
  const ownerId = this.getNodeParameter('owner_id', i);
449
- const status = this.getNodeParameter('status', i);
450
465
  const body = {};
451
466
  if (columnId)
452
467
  body.column_id = columnId;
453
468
  if (ownerId)
454
469
  body.owner_id = ownerId;
455
- if (status)
456
- 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
+ }
457
519
  if (Object.keys(body).length > 0) {
458
520
  options.body = body;
459
521
  }
@@ -463,8 +525,8 @@ class Cortex {
463
525
  }
464
526
  }
465
527
  }
466
- responseData = await this.helpers.request(options);
467
- returnData.push(responseData);
528
+ const responseData = await this.helpers.request(options);
529
+ returnData.push(Array.isArray(responseData) ? { json: responseData } : responseData);
468
530
  }
469
531
  catch (error) {
470
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.12",
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
+ }