n8n-nodes-bgos 1.3.2 → 1.4.0

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
@@ -16,6 +16,37 @@ Perform operations on the BGOS platform:
16
16
  | **User** | Get or Create, Update |
17
17
  | **Scheduled Task** | Get, Get User Tasks, Delete |
18
18
 
19
+ #### Render Mode — Inline Buttons vs. Modal Pop-Under
20
+
21
+ Two ways the BGOS app renders buttons that come with a message:
22
+
23
+ | Mode | Looks Like | When It Makes Sense |
24
+ |---|---|---|
25
+ | **Inline** (default on `sendMessage`) | Telegram-style chips inside a warm off-white card below the message. Skip (top-left), "✍ Custom reply" footer link. Stays clickable forever — user can answer now, tomorrow, or never. | The message is **not** a direct reply to something the user just typed: cron, scheduler, external webhook, unprompted check-in. Single question, up to 6 labeled choices. |
26
+ | **Modal** (default on `askUserInput`) | Bottom sheet / pop-under that demands attention. Multi-question carousel, free-text fallback, skip-all. | Active back-and-forth conversation — user just sent a message and you need their answer to continue. Also use for multi-question flows. |
27
+
28
+ **Default to inline unless you know the user is actively in the chat.** Modals interrupt. Inline never does.
29
+
30
+ ##### Decision tree for AI agents
31
+
32
+ ```
33
+ Is this message triggered by a user message they sent in the last ~2 minutes?
34
+ ├─ YES → modal OK (but inline also works)
35
+ └─ NO → inline. Always.
36
+
37
+ Do you need multi-question carousel, free-text, OR skip-all semantics?
38
+ ├─ YES → must use modal (askUserInput op with askRenderMode=modal)
39
+ └─ NO → prefer inline
40
+ ```
41
+
42
+ ##### Agent-facing contract
43
+
44
+ - **Inline** chips ≤ 6 (backend rejects more). Labels ≤ 24 chars render cleanly; longer wraps.
45
+ - When the user clicks a chip, your `BGOS Trigger` receives `button_clicked` with `callback_data` = the option's `value`.
46
+ - When the user taps **Skip**, `button_clicked` fires with `callback_data: "__skip__"` and `option_id: null`.
47
+ - When the user taps **Custom reply** and submits free text, they get **two** events: a normal user `message` AND a `button_clicked` with `callback_data: "__custom__"` + `custom_text: <what they typed>`. Correlate by `message_id`.
48
+ - Respond to button clicks with the **Answer a Callback** operation so the chip animation completes.
49
+
19
50
  #### Ask User Input
20
51
 
21
52
  Pops the BGOS app's polished modal/sheet to ask the user 1–4 multiple-choice questions in a single carousel. Each question accepts: option list (label + value), optional free-text fallback, optional skip. The node POSTs each question to `/api/v1/messages` with `messageType=ask_user_input`, sharing one `askId` so they group into one carousel.
@@ -11,8 +11,9 @@ class BGOSAction {
11
11
  group: ['transform'],
12
12
  version: 1,
13
13
  subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
14
- description: 'Interact with the BGOS platform',
14
+ description: 'Interact with the BGOS chat platform. Send/edit/pin/delete messages, ask the user a question, send a message and wait for the reply, fetch chat metadata, manage assistants and files. Usable as an AI agent tool.',
15
15
  defaults: { name: 'BGOS Action' },
16
+ usableAsTool: true,
16
17
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
17
18
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
18
19
  credentials: [
@@ -45,22 +46,40 @@ class BGOSAction {
45
46
  noDataExpression: true,
46
47
  displayOptions: { show: { resource: ['message'] } },
47
48
  options: [
48
- {
49
- name: 'Delete a Message',
50
- value: 'deleteMessage',
51
- description: 'Delete a message from a chat',
52
- action: 'Delete a message',
53
- },
54
49
  {
55
50
  name: 'Send a Message',
56
51
  value: 'sendMessage',
57
- description: 'Send an assistant reply to the user with optional file attachment',
52
+ description: 'Post a new message to a chat. Use for proactive notifications, assistant replies, status updates, or anything you want to say to the user or another agent. Supports text, file attachments, and tappable inline/modal buttons.',
58
53
  action: 'Send a message',
59
54
  },
55
+ {
56
+ name: 'Send and Wait for Reply',
57
+ value: 'sendAndWaitForReply',
58
+ description: 'Post a message and BLOCK until the next reply arrives in the chat (default 10 min timeout). Use this when an AI agent calls another agent (or the user) and needs the response back as the tool result. Returns { sentMessageId, replyMessageId, replyText }.',
59
+ action: 'Send a message and wait for reply',
60
+ },
61
+ {
62
+ name: 'Edit a Message',
63
+ value: 'editMessage',
64
+ description: 'Replace the text of a previously-sent message in place. Use to update a status message ("Working..." → "Done") instead of spamming new messages. Requires Message ID and User ID.',
65
+ action: 'Edit a message',
66
+ },
67
+ {
68
+ name: 'Pin or Unpin a Message',
69
+ value: 'pinMessage',
70
+ description: 'Toggle the pinned state of a message. Pinned messages appear in the chat\'s pin bar. Set "Is Pinned" to true to pin or false to unpin.',
71
+ action: 'Pin or unpin a message',
72
+ },
73
+ {
74
+ name: 'Delete a Message',
75
+ value: 'deleteMessage',
76
+ description: 'Permanently remove a message from a chat. Use sparingly — most workflows should edit the text rather than delete + resend.',
77
+ action: 'Delete a message',
78
+ },
60
79
  {
61
80
  name: 'Ask User Input',
62
81
  value: 'askUserInput',
63
- description: 'Ask the user 1–4 multiple-choice questions through the polished modal/sheet in the BGOS app',
82
+ description: 'Ask the user 1–4 multiple-choice questions through a polished modal/sheet (or inline carousel). BLOCKS until answered. Use ONLY when the user is actively in conversation and you need their immediate choice; for async/scheduled prompts use Send a Message with buttons instead.',
64
83
  action: 'Ask user input',
65
84
  },
66
85
  ],
@@ -76,25 +95,31 @@ class BGOSAction {
76
95
  displayOptions: { show: { resource: ['chat'] } },
77
96
  options: [
78
97
  {
79
- name: 'Delete a Chat',
80
- value: 'deleteChat',
81
- description: 'Delete a chat',
82
- action: 'Delete a chat',
98
+ name: 'Get a Chat',
99
+ value: 'getChat',
100
+ description: 'Fetch chat metadata (title, assistant, ownership, timestamps) by chat ID. Use to gather context before deciding how to respond — e.g. checking the assigned assistant or the chat title.',
101
+ action: 'Get a chat',
102
+ },
103
+ {
104
+ name: 'Set Title on a Chat',
105
+ value: 'renameChat',
106
+ description: 'Rename a chat. Use after a meaningful event (e.g. summarising the topic of a conversation, or marking a chat as resolved).',
107
+ action: 'Set title on a chat',
83
108
  },
84
109
  {
85
110
  name: 'Save Chat History',
86
111
  value: 'saveChatHistory',
87
- description: 'Bulk-save messages to a chat',
112
+ description: 'Bulk-insert a list of messages into a chat. Use for migrations or for replaying conversation history; not for normal sending — use "Send a Message" for that.',
88
113
  action: 'Save chat history',
89
114
  },
90
115
  {
91
- name: 'Set Title on a Chat',
92
- value: 'renameChat',
93
- description: 'Rename a chat',
94
- action: 'Set title on a chat',
116
+ name: 'Delete a Chat',
117
+ value: 'deleteChat',
118
+ description: 'Permanently delete a chat and its messages. Destructive — use only for explicit cleanup tasks.',
119
+ action: 'Delete a chat',
95
120
  },
96
121
  ],
97
- default: 'renameChat',
122
+ default: 'getChat',
98
123
  required: true,
99
124
  },
100
125
  // ── Assistant operations ──────────────────────────────────────────────
@@ -228,6 +253,17 @@ class BGOSAction {
228
253
  placeholder: '[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]',
229
254
  description: 'JSON array of button options to display with the message',
230
255
  },
256
+ {
257
+ displayName: 'Render Mode',
258
+ name: 'renderMode',
259
+ type: 'options',
260
+ default: 'inline',
261
+ options: [
262
+ { name: 'Inline buttons (Telegram-style, asynchronous)', value: 'inline' },
263
+ { name: 'Modal pop-under (interactive, blocks UI)', value: 'modal' },
264
+ ],
265
+ description: 'Rule: If the message is NOT a direct reply to something the user just typed — cron, scheduler, external webhook, unprompted check-in — use INLINE. If the user just sent a message within the last ~2 min and you want their full attention, MODAL is acceptable. When in doubt, pick INLINE — it never interrupts. Max 6 options for inline.',
266
+ },
231
267
  ],
232
268
  },
233
269
  // ── Delete a Message ──────────────────────────────────────────────────
@@ -240,6 +276,115 @@ class BGOSAction {
240
276
  description: 'ID of the message to delete. Falls back to message_id / messageId from input data.',
241
277
  displayOptions: { show: { resource: ['message'], operation: ['deleteMessage'] } },
242
278
  },
279
+ // ── Edit a Message ────────────────────────────────────────────────────
280
+ {
281
+ displayName: 'Message ID',
282
+ name: 'messageId',
283
+ type: 'string',
284
+ default: '',
285
+ placeholder: '456',
286
+ description: 'ID of the message to edit. Falls back to message_id / messageId from input data.',
287
+ displayOptions: { show: { resource: ['message'], operation: ['editMessage'] } },
288
+ },
289
+ {
290
+ displayName: 'New Text',
291
+ name: 'messageText',
292
+ type: 'string',
293
+ typeOptions: { rows: 4 },
294
+ default: '',
295
+ placeholder: 'Done — here\'s the result:',
296
+ description: 'The new message text. Replaces the existing text entirely. Falls back to the input data\'s text field.',
297
+ displayOptions: { show: { resource: ['message'], operation: ['editMessage'] } },
298
+ },
299
+ {
300
+ displayName: 'User ID',
301
+ name: 'userId',
302
+ type: 'string',
303
+ default: '',
304
+ placeholder: 'user_xxx',
305
+ description: 'User ID of the message owner (for ownership validation). Falls back to user_id from input data.',
306
+ displayOptions: { show: { resource: ['message'], operation: ['editMessage'] } },
307
+ },
308
+ // ── Pin or Unpin a Message ────────────────────────────────────────────
309
+ {
310
+ displayName: 'Message ID',
311
+ name: 'messageId',
312
+ type: 'string',
313
+ default: '',
314
+ placeholder: '456',
315
+ description: 'ID of the message to pin or unpin. Falls back to message_id / messageId from input data.',
316
+ displayOptions: { show: { resource: ['message'], operation: ['pinMessage'] } },
317
+ },
318
+ {
319
+ displayName: 'Is Pinned',
320
+ name: 'isPinned',
321
+ type: 'boolean',
322
+ default: true,
323
+ description: 'Whether the message should be pinned (true) or unpinned (false). Pinned messages appear in the chat\'s pin bar.',
324
+ displayOptions: { show: { resource: ['message'], operation: ['pinMessage'] } },
325
+ },
326
+ // ── Send and Wait for Reply ───────────────────────────────────────────
327
+ {
328
+ displayName: 'Assistant ID',
329
+ name: 'assistantId',
330
+ type: 'string',
331
+ default: '',
332
+ placeholder: '42',
333
+ description: 'ID of the assistant sending the message. Falls back to assistantId / assistant_id from input data.',
334
+ displayOptions: { show: { resource: ['message'], operation: ['sendAndWaitForReply'] } },
335
+ },
336
+ {
337
+ displayName: 'Chat ID',
338
+ name: 'chatId',
339
+ type: 'string',
340
+ default: '',
341
+ placeholder: '123',
342
+ description: 'ID of the chat to post into and listen on. Falls back to chatId / chat_id from input data.',
343
+ displayOptions: { show: { resource: ['message'], operation: ['sendAndWaitForReply'] } },
344
+ },
345
+ {
346
+ displayName: 'Text',
347
+ name: 'messageText',
348
+ type: 'string',
349
+ typeOptions: { rows: 4 },
350
+ default: '',
351
+ placeholder: 'Hey Ava, can you check on the latest sales numbers?',
352
+ description: 'Message text to post. The next reply in the chat will be returned as the tool result.',
353
+ displayOptions: { show: { resource: ['message'], operation: ['sendAndWaitForReply'] } },
354
+ },
355
+ {
356
+ displayName: 'User ID',
357
+ name: 'userId',
358
+ type: 'string',
359
+ default: '',
360
+ placeholder: 'user_xxx',
361
+ description: 'User ID who owns the chat — needed to poll its messages. Falls back to user_id / userId from input data.',
362
+ displayOptions: { show: { resource: ['message'], operation: ['sendAndWaitForReply'] } },
363
+ },
364
+ {
365
+ displayName: 'Additional Fields',
366
+ name: 'replyAdditionalFields',
367
+ type: 'collection',
368
+ placeholder: 'Add Field',
369
+ default: {},
370
+ displayOptions: { show: { resource: ['message'], operation: ['sendAndWaitForReply'] } },
371
+ options: [
372
+ {
373
+ displayName: 'Timeout (Seconds)',
374
+ name: 'replyTimeoutSeconds',
375
+ type: 'number',
376
+ default: 600,
377
+ description: 'How long to wait for a reply before failing the node (default 10 minutes).',
378
+ },
379
+ {
380
+ displayName: 'Poll Interval (Ms)',
381
+ name: 'replyPollIntervalMs',
382
+ type: 'number',
383
+ default: 2000,
384
+ description: 'How often to check for new messages while waiting (default 2 seconds; minimum 1000).',
385
+ },
386
+ ],
387
+ },
243
388
  // ── Ask User Input ────────────────────────────────────────────────────
244
389
  {
245
390
  displayName: 'Chat ID',
@@ -340,6 +485,18 @@ class BGOSAction {
340
485
  description: 'Whether the node should block until every question is answered (or the timeout expires) and return structured answers. When disabled, the node returns immediately after posting; subscribe to the Ask Response event in a BGOS Trigger to receive each answer.',
341
486
  displayOptions: { show: { resource: ['message'], operation: ['askUserInput'] } },
342
487
  },
488
+ {
489
+ displayName: 'Render Mode',
490
+ name: 'askRenderMode',
491
+ type: 'options',
492
+ default: 'modal',
493
+ options: [
494
+ { name: 'Modal pop-under (interactive, blocks UI)', value: 'modal' },
495
+ { name: 'Inline buttons (Telegram-style, asynchronous)', value: 'inline' },
496
+ ],
497
+ description: 'MODAL is the default because askUserInput is typically used mid-conversation. Switch to INLINE if this ask is triggered by cron/schedule/external webhook, or if you do NOT want to interrupt the user. Note: only MODAL supports multi-question carousel, free-text, and skip-all. INLINE renders 1 question with up to 6 chips plus a "Custom reply" / "Skip" footer.',
498
+ displayOptions: { show: { resource: ['message'], operation: ['askUserInput'] } },
499
+ },
343
500
  {
344
501
  displayName: 'Additional Fields',
345
502
  name: 'askAdditionalFields',
@@ -372,7 +529,7 @@ class BGOSAction {
372
529
  default: '',
373
530
  placeholder: '123',
374
531
  description: 'ID of the chat. Falls back to chat_id / chat.id from input data.',
375
- displayOptions: { show: { resource: ['chat'], operation: ['renameChat', 'deleteChat', 'saveChatHistory'] } },
532
+ displayOptions: { show: { resource: ['chat'], operation: ['renameChat', 'deleteChat', 'saveChatHistory', 'getChat'] } },
376
533
  },
377
534
  {
378
535
  displayName: 'New Title',
@@ -534,7 +691,7 @@ class BGOSAction {
534
691
  type: 'string',
535
692
  default: '',
536
693
  placeholder: 'user_xxx',
537
- description: 'Override the user ID. If empty, falls back to user_id from the input data.',
694
+ description: 'Override the user ID. If empty, falls back to user_id from the input data. Optional for "Get a Chat".',
538
695
  displayOptions: {
539
696
  show: {
540
697
  resource: ['chat', 'assistant', 'callback', 'file'],
@@ -569,14 +726,40 @@ class BGOSAction {
569
726
  nodeParams.fileId = String(additionalFields.fileId ?? '');
570
727
  nodeParams.sender = String(additionalFields.sender ?? 'assistant');
571
728
  nodeParams.options = String(additionalFields.options ?? '');
729
+ nodeParams.renderMode = additionalFields.renderMode
730
+ ? String(additionalFields.renderMode)
731
+ : 'inline';
572
732
  }
573
733
  if (operation === 'deleteMessage') {
574
734
  nodeParams.deleteMessageId = String(this.getNodeParameter('deleteMessageId', i, '') ?? '');
575
735
  }
736
+ if (operation === 'editMessage') {
737
+ nodeParams.messageId = String(this.getNodeParameter('messageId', i, '') ?? '');
738
+ nodeParams.messageText = String(this.getNodeParameter('messageText', i, '') ?? '');
739
+ nodeParams.userId = String(this.getNodeParameter('userId', i, '') ?? '');
740
+ }
741
+ if (operation === 'pinMessage') {
742
+ nodeParams.messageId = String(this.getNodeParameter('messageId', i, '') ?? '');
743
+ nodeParams.isPinned = Boolean(this.getNodeParameter('isPinned', i, true));
744
+ }
745
+ if (operation === 'sendAndWaitForReply') {
746
+ nodeParams.assistantId = String(this.getNodeParameter('assistantId', i, '') ?? '');
747
+ nodeParams.chatId = String(this.getNodeParameter('chatId', i, '') ?? '');
748
+ nodeParams.messageText = String(this.getNodeParameter('messageText', i, '') ?? '');
749
+ nodeParams.userId = String(this.getNodeParameter('userId', i, '') ?? '');
750
+ const replyAdditional = this.getNodeParameter('replyAdditionalFields', i, {});
751
+ nodeParams.replyTimeoutSeconds = Number(replyAdditional.replyTimeoutSeconds ?? 600);
752
+ nodeParams.replyPollIntervalMs = Number(replyAdditional.replyPollIntervalMs ?? 2000);
753
+ }
754
+ if (operation === 'getChat') {
755
+ nodeParams.chatId = String(this.getNodeParameter('chatId', i, '') ?? '');
756
+ nodeParams.userId = String(this.getNodeParameter('userId', i, '') ?? '');
757
+ }
576
758
  if (operation === 'askUserInput') {
577
759
  nodeParams.chatId = String(this.getNodeParameter('chatId', i, '') ?? '');
578
760
  nodeParams.userId = String(this.getNodeParameter('askUserId', i, '') ?? '');
579
761
  nodeParams.waitForAnswers = Boolean(this.getNodeParameter('waitForAnswers', i, true));
762
+ nodeParams.renderMode = String(this.getNodeParameter('askRenderMode', i, 'modal') ?? 'modal');
580
763
  const askQuestionsRaw = this.getNodeParameter('askQuestions', i, {});
581
764
  const questions = (askQuestionsRaw.question ?? []).map((q) => ({
582
765
  text: String(q.text ?? '').trim(),
@@ -37,6 +37,10 @@ type NodeParams = {
37
37
  waitForAnswers?: boolean;
38
38
  askTimeoutSeconds?: number;
39
39
  askPollIntervalMs?: number;
40
+ renderMode?: string;
41
+ isPinned?: boolean;
42
+ replyTimeoutSeconds?: number;
43
+ replyPollIntervalMs?: number;
40
44
  [key: string]: unknown;
41
45
  };
42
46
  export declare function handleEventByType(this: IExecuteFunctions, eventType: string, eventData: Record<string, unknown>, nodeParams: NodeParams): Promise<unknown>;
@@ -50,6 +50,10 @@ async function handleEventByType(eventType, eventData, nodeParams) {
50
50
  else if (Array.isArray(eventData.options)) {
51
51
  buttonOptions = eventData.options;
52
52
  }
53
+ const renderMode = (nodeParams.renderMode
54
+ ?? eventData.renderMode
55
+ ?? eventData.render_mode
56
+ ?? 'inline');
53
57
  return await techWebhook_1.sendMessageToBackend.call(this, apiOptions, {
54
58
  assistantId: assistantId,
55
59
  chatId: chatId,
@@ -57,6 +61,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
57
61
  text,
58
62
  options: buttonOptions,
59
63
  files,
64
+ renderMode,
60
65
  isCode: (eventData.isCode ?? message?.isCode ?? null),
61
66
  artifactCode: (eventData.artifactCode ?? message?.artifactCode ?? null),
62
67
  isArticle: (eventData.isArticle ?? message?.isArticle ?? null),
@@ -76,6 +81,80 @@ async function handleEventByType(eventType, eventData, nodeParams) {
76
81
  }
77
82
  return await techWebhook_1.deleteMessage.call(this, apiOptions, String(messageId));
78
83
  }
84
+ case 'editMessage': {
85
+ const apiOptions = getApiOptions(nodeParams);
86
+ const messageId = nodeParams.messageId || (eventData.message_id ?? eventData.messageId ?? message?.id);
87
+ const userId = (nodeParams.userId ?? eventData.user_id ?? eventData.userId);
88
+ const newText = (nodeParams.messageText ?? '').trim() || (eventData.text ?? message?.text ?? '');
89
+ if (!messageId || !userId || !newText) {
90
+ throw new Error('Message ID, User ID, and Text are required for Edit a Message. Set the fields or make sure input data contains message_id, user_id, and text.');
91
+ }
92
+ return await techWebhook_1.editMessageText.call(this, apiOptions, userId, String(messageId), String(newText));
93
+ }
94
+ case 'pinMessage': {
95
+ const apiOptions = getApiOptions(nodeParams);
96
+ const messageId = nodeParams.messageId || (eventData.message_id ?? eventData.messageId ?? message?.id);
97
+ if (!messageId) {
98
+ throw new Error('Message ID is required for Pin/Unpin a Message. Set the field or make sure input data contains message_id.');
99
+ }
100
+ // pinState may come from the node param or input data; default to true (pin)
101
+ const pinStateRaw = nodeParams.isPinned ?? eventData.isPinned ?? eventData.is_pinned ?? true;
102
+ const isPinned = pinStateRaw === true || pinStateRaw === 'true';
103
+ return await techWebhook_1.setMessagePinned.call(this, apiOptions, String(messageId), isPinned);
104
+ }
105
+ case 'sendAndWaitForReply': {
106
+ const apiOptions = getApiOptions(nodeParams);
107
+ const assistantId = nodeParams.assistantId || (eventData.assistantId ?? eventData.assistant_id ?? assistant?.id);
108
+ const chatId = nodeParams.chatId || (eventData.chatId ?? eventData.chat_id ?? chat?.id ?? message?.chatId);
109
+ const userId = (nodeParams.userId ?? eventData.user_id ?? eventData.userId);
110
+ const text = (nodeParams.messageText ?? '').trim() || (eventData.text ?? message?.text ?? '');
111
+ if (!assistantId || !chatId || !userId || !text) {
112
+ throw new Error('Assistant ID, Chat ID, User ID, and Text are required for Send and Wait for Reply.');
113
+ }
114
+ // Step 1: post the outgoing message and capture its id
115
+ const sendResult = (await techWebhook_1.sendMessageToBackend.call(this, apiOptions, {
116
+ assistantId: assistantId,
117
+ chatId: chatId,
118
+ sender: 'assistant',
119
+ text,
120
+ }));
121
+ const sentMessageId = Number(sendResult?.id
122
+ ?? sendResult?.message?.id
123
+ ?? 0);
124
+ if (!sentMessageId) {
125
+ throw new Error('Could not determine sent message ID from BGOS response — cannot wait for reply.');
126
+ }
127
+ // Step 2: poll the chat for any new message with id > sentMessageId
128
+ const timeoutSeconds = Number(nodeParams.replyTimeoutSeconds ?? 600);
129
+ const pollIntervalMs = Math.max(1000, Number(nodeParams.replyPollIntervalMs ?? 2000));
130
+ const deadline = Date.now() + timeoutSeconds * 1000;
131
+ while (Date.now() < deadline) {
132
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
133
+ try {
134
+ const data = await techWebhook_1.getChatMessages.call(this, apiOptions, Number(chatId), userId);
135
+ // Cast to a permissive shape — backend returns extra fields beyond ChatMessageEntry
136
+ const msgs = (data?.messages ?? []);
137
+ for (const entry of msgs) {
138
+ const m = (entry.message ?? entry);
139
+ const id = Number(m.id ?? 0);
140
+ const replyText = m.text;
141
+ if (id > sentMessageId && replyText) {
142
+ return {
143
+ success: true,
144
+ sentMessageId,
145
+ replyMessageId: id,
146
+ replyText,
147
+ raw: entry,
148
+ };
149
+ }
150
+ }
151
+ }
152
+ catch {
153
+ // Network blip — keep polling.
154
+ }
155
+ }
156
+ throw new Error(`Timed out after ${timeoutSeconds}s waiting for a reply to message ${sentMessageId} in chat ${chatId}.`);
157
+ }
79
158
  case 'askUserInput': {
80
159
  const apiOptions = getApiOptions(nodeParams);
81
160
  const chatId = nodeParams.chatId || (eventData.chatId ?? eventData.chat_id ?? chat?.id ?? message?.chatId);
@@ -108,6 +187,10 @@ async function handleEventByType(eventType, eventData, nodeParams) {
108
187
  if (!q.options.length)
109
188
  throw new Error(`Question ${idx + 1}: at least one option is required.`);
110
189
  }
190
+ const renderMode = (nodeParams.renderMode
191
+ ?? eventData.renderMode
192
+ ?? eventData.render_mode
193
+ ?? 'modal');
111
194
  // Post each question. The first call's response carries the
112
195
  // backend-generated askId; reuse it for the rest so they group into
113
196
  // one carousel/sheet on the frontend.
@@ -124,6 +207,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
124
207
  allowFreeText: q.allowFreeText,
125
208
  allowSkip: q.allowSkip,
126
209
  options: q.options,
210
+ renderMode,
127
211
  });
128
212
  postedIds.push(posted.id);
129
213
  if (!askId && posted.askId)
@@ -233,6 +317,14 @@ async function handleEventByType(eventType, eventData, nodeParams) {
233
317
  throw new Error('Chat ID is required for Delete a Chat.');
234
318
  return await techWebhook_1.deleteChat.call(this, getApiOptions(nodeParams), userId, String(chatId));
235
319
  }
320
+ case 'getChat': {
321
+ const apiOptions = getApiOptions(nodeParams);
322
+ const chatId = nodeParams.chatId || (eventData.chat_id ?? eventData.chatId ?? chat?.id);
323
+ if (!chatId)
324
+ throw new Error('Chat ID is required for Get a Chat.');
325
+ const userId = (nodeParams.userId ?? eventData.user_id ?? eventData.userId);
326
+ return await techWebhook_1.getChat.call(this, apiOptions, String(chatId), userId);
327
+ }
236
328
  case 'saveChatHistory': {
237
329
  const userId = (nodeParams.userId ?? eventData.user_id ?? eventData.userId);
238
330
  if (!userId)
@@ -18,6 +18,24 @@ export declare function saveChatHistory(this: IExecuteFunctions, options: BgosAp
18
18
  export declare function deleteMessage(this: IExecuteFunctions, options: BgosApiOptions, messageId: string | number): Promise<{
19
19
  success: boolean;
20
20
  }>;
21
+ /**
22
+ * PATCH /api/v1/messages/:id — update the text of an existing message.
23
+ * Useful for status-updates-in-place (e.g. "Working..." → "Done"), replacing
24
+ * the prior approach of sending a new message every time.
25
+ */
26
+ export declare function editMessageText(this: IExecuteFunctions, options: BgosApiOptions, userId: string, messageId: string | number, text: string): Promise<unknown>;
27
+ /**
28
+ * PATCH /api/v1/messages/:id/pin — toggle the pin state of a message.
29
+ * Body { isPinned: boolean }. Backend stamps pinned_at when true,
30
+ * clears it when false. Emits a message_pinned WebSocket event.
31
+ */
32
+ export declare function setMessagePinned(this: IExecuteFunctions, options: BgosApiOptions, messageId: string | number, isPinned: boolean): Promise<unknown>;
33
+ /**
34
+ * GET /api/v1/chats/:chatId?userId=... — fetch chat metadata (title,
35
+ * assistantId, ownership). Useful for AI agents that need context before
36
+ * deciding how to respond.
37
+ */
38
+ export declare function getChat(this: IExecuteFunctions, options: BgosApiOptions, chatId: string | number, userId?: string): Promise<unknown>;
21
39
  export declare function sendMessageToBackend(this: IExecuteFunctions, options: BgosApiOptions, payload: {
22
40
  assistantId: string | number;
23
41
  chatId: string | number;
@@ -25,6 +43,7 @@ export declare function sendMessageToBackend(this: IExecuteFunctions, options: B
25
43
  text?: string | null;
26
44
  options?: unknown[];
27
45
  files?: unknown[];
46
+ renderMode?: string | null;
28
47
  isCode?: boolean | null;
29
48
  artifactCode?: string | null;
30
49
  isArticle?: boolean | null;
@@ -67,6 +86,7 @@ export declare function postAskQuestion(this: IExecuteFunctions, options: BgosAp
67
86
  label: string;
68
87
  value: string;
69
88
  }>;
89
+ renderMode?: string | null;
70
90
  }): Promise<AskQuestionPosted>;
71
91
  export interface ChatMessageEntry {
72
92
  message: {
@@ -7,6 +7,9 @@ exports.renameChat = renameChat;
7
7
  exports.deleteChat = deleteChat;
8
8
  exports.saveChatHistory = saveChatHistory;
9
9
  exports.deleteMessage = deleteMessage;
10
+ exports.editMessageText = editMessageText;
11
+ exports.setMessagePinned = setMessagePinned;
12
+ exports.getChat = getChat;
10
13
  exports.sendMessageToBackend = sendMessageToBackend;
11
14
  exports.postAskQuestion = postAskQuestion;
12
15
  exports.getChatMessages = getChatMessages;
@@ -97,6 +100,51 @@ async function deleteMessage(options, messageId) {
97
100
  });
98
101
  return { success: true };
99
102
  }
103
+ /**
104
+ * PATCH /api/v1/messages/:id — update the text of an existing message.
105
+ * Useful for status-updates-in-place (e.g. "Working..." → "Done"), replacing
106
+ * the prior approach of sending a new message every time.
107
+ */
108
+ async function editMessageText(options, userId, messageId, text) {
109
+ const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/messages/${messageId}`;
110
+ return this.helpers.httpRequest({
111
+ method: 'PATCH',
112
+ url,
113
+ headers: buildHeaders(options.apiKey),
114
+ body: { userId, text },
115
+ json: true,
116
+ });
117
+ }
118
+ /**
119
+ * PATCH /api/v1/messages/:id/pin — toggle the pin state of a message.
120
+ * Body { isPinned: boolean }. Backend stamps pinned_at when true,
121
+ * clears it when false. Emits a message_pinned WebSocket event.
122
+ */
123
+ async function setMessagePinned(options, messageId, isPinned) {
124
+ const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/messages/${messageId}/pin`;
125
+ return this.helpers.httpRequest({
126
+ method: 'PATCH',
127
+ url,
128
+ headers: buildHeaders(options.apiKey),
129
+ body: { isPinned },
130
+ json: true,
131
+ });
132
+ }
133
+ /**
134
+ * GET /api/v1/chats/:chatId?userId=... — fetch chat metadata (title,
135
+ * assistantId, ownership). Useful for AI agents that need context before
136
+ * deciding how to respond.
137
+ */
138
+ async function getChat(options, chatId, userId) {
139
+ const qs = userId ? `?userId=${encodeURIComponent(userId)}` : '';
140
+ const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/chats/${chatId}${qs}`;
141
+ return this.helpers.httpRequest({
142
+ method: 'GET',
143
+ url,
144
+ headers: buildHeaders(options.apiKey),
145
+ json: true,
146
+ });
147
+ }
100
148
  async function sendMessageToBackend(options, payload) {
101
149
  const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/send-message`;
102
150
  const body = {
@@ -117,6 +165,8 @@ async function sendMessageToBackend(options, payload) {
117
165
  options: payload.options ?? [],
118
166
  files: payload.files ?? [],
119
167
  };
168
+ if (payload.renderMode)
169
+ body.renderMode = payload.renderMode;
120
170
  return this.helpers.httpRequest({
121
171
  method: 'POST',
122
172
  url,
@@ -148,6 +198,8 @@ async function postAskQuestion(options, payload) {
148
198
  };
149
199
  if (payload.askId)
150
200
  body.askId = payload.askId;
201
+ if (payload.renderMode)
202
+ body.renderMode = payload.renderMode;
151
203
  const result = (await this.helpers.httpRequest({
152
204
  method: 'POST',
153
205
  url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-bgos",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "n8n community nodes for BGOS (Brand Growth OS) - AI assistant chat platform",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",