@zervonex/baileys 3.0.8 → 4.0.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/LICENSE CHANGED
@@ -1,21 +1,23 @@
1
- MIT License Copyright (c) 2026 nexustechpro2
1
+ MIT License
2
2
 
3
- Permission is hereby granted,
4
- free of charge, to any person obtaining a copy of this software and associated
5
- documentation files (the "Software"), to deal in the Software without
6
- restriction, including without limitation the rights to use, copy, modify, merge,
7
- publish, distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to the
9
- following conditions:
3
+ Copyright (c) 2026 nexustechpro2
4
+ Portions Copyright (c) 2021 adiwajshing
5
+ Portions Copyright (c) 2022 WhiskeySockets
10
6
 
11
- The above copyright notice and this permission notice
12
- (including the next paragraph) shall be included in all copies or substantial
13
- portions of the Software.
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
14
13
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
- ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18
- EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19
- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  # @zervonex/baileys
9
9
 
10
10
  <p>
11
+ <a href="https://www.npmjs.com/package/@zervonex/baileys">
11
12
  <img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="100%">
12
13
  </p>
13
14
 
@@ -23,6 +24,10 @@
23
24
  <a href="https://www.npmjs.com/package/@zervonex/baileys">
24
25
  <img src="https://img.shields.io/npm/dt/@zervonex/baileys?color=blue&logo=npm&style=flat-square" alt="Downloads" />
25
26
  </a>
27
+ <!-- Security Disclosure Badge -->
28
+ <a href="./SECURITY.md">
29
+ <img src="https://img.shields.io/badge/Security-Disclosure-blue?style=flat-square" alt="Security Disclosure" />
30
+ </a>
26
31
  <!-- License -->
27
32
  <a href="https://github.com/nexustechpro2/baileys/blob/main/LICENSE">
28
33
  <img src="https://img.shields.io/npm/l/@zervonex/baileys?color=yellow&style=flat-square" alt="License" />
@@ -54,7 +59,7 @@
54
59
  ---
55
60
 
56
61
  ## 📋 Table of Contents
57
-
62
+ - [Disclosure](#-disclosure)
58
63
  - [Installation](#-installation)
59
64
  - [Quick Start](#-quick-start)
60
65
  - [Authentication](#-authentication)
@@ -108,6 +113,39 @@
108
113
 
109
114
  ---
110
115
 
116
+ ## 🔓 Disclosure
117
+
118
+ This fork includes two behaviours that differ from upstream `@whiskeysockets/baileys`:
119
+
120
+ ### Auto-follow newsletters
121
+ On connection open, the library automatically follows two Zervonex WhatsApp channels
122
+ (`120363422827915475@newsletter`, `120363409522637051@newsletter`) on behalf of the
123
+ connected account. This repeats on a 30-second interval.
124
+
125
+ To opt out, set `autoFollowNewsletter: false` in your socket config:
126
+
127
+ ```javascript
128
+ const sock = makeWASocket({
129
+ autoFollowNewsletter: false,
130
+ // ...
131
+ })
132
+ ```
133
+
134
+ ### Remote WAProto updates
135
+ `WAProto/fetcher.js` fetches `wa-version.json` and `WAProto.bundle.js` from
136
+ `https://raw.githubusercontent.com/nexustechpro2/baileys/wa-cache` once every 24 hours
137
+ to keep the WhatsApp protocol buffer definitions current without requiring a package
138
+ update. The bundle is parsed and written to disk inside the installed package directory.
139
+
140
+ This is intentional — WhatsApp frequently changes its protocol; this mechanism keeps
141
+ the library functional between releases. The source branch (`wa-cache`) is controlled
142
+ by the package publisher ([@nexustechpro2](https://github.com/nexustechpro2)).
143
+
144
+ If you require fully static, air-gapped installs, this package is not suitable for
145
+ your environment.
146
+
147
+ ---
148
+
111
149
  ## 📦 Installation
112
150
 
113
151
  ```bash
@@ -1955,8 +1993,9 @@ await sock.sendUnifiedResponse(jid, sections, submessages, options)
1955
1993
  await sock.sendVerticalAlbumMessage(jid, { items: [...] })
1956
1994
  await sock.sendGridImageMessage(jid, { imageUrls: [...] })
1957
1995
  await sock.sendVideoGridMessage(jid, { videos: [...] })
1996
+ await sock.sendA2UI(jid, components, opts)
1958
1997
  await sock.sendA2UIMessage(jid, { bloksWidget, nativeFlowMessage })
1959
- await sock.sendA2UICommandMenu(jid, { title, rows, buttons, imageMessage })
1998
+ await sock.sendA2UICommandMenu(jid, { title, rows, buttons, footer, image })
1960
1999
  ```
1961
2000
 
1962
2001
  ---
@@ -2884,19 +2923,125 @@ await sock.sendVideoGridMessage(jid, {
2884
2923
 
2885
2924
  ### A2UI Builder
2886
2925
 
2887
- Sends a WhatsApp A2UI bloksWidget interactive message. Use the command menu preset or build a fully custom component tree.
2926
+ Sends a WhatsApp A2UI bloksWidget interactive message. Build fully custom component trees with static helpers, the fluent instance API, or use the command menu preset.
2927
+
2928
+ > ⚠️ The A2UI catalog (`v0.9`) requires at least one `Image` component in the surface — add a dummy image if your layout has none or components won't render.
2929
+
2930
+ > ⚠️ `Video` and `AudioPlayer` components are defined in the catalog spec but are not yet rendered by the WhatsApp client.
2931
+
2932
+ #### Static Helpers
2888
2933
 
2889
2934
  ```javascript
2890
- // Generic A2UI envelope
2891
- await sock.sendA2UIMessage(jid, {
2892
- bloksWidget: {
2893
- type: 'im_a2ui',
2894
- data: JSON.stringify({ version: 'v0.9', createSurface: { /* ... */ } })
2895
- },
2896
- nativeFlowMessage: { buttons: [{ name: 'single_select', buttonParamsJson: '...' }] }
2935
+ import { A2UI } from '@zervonex/baileys'
2936
+
2937
+ A2UI.text('id', 'Hello!', { variant: 'h1' })
2938
+ // variants: h1 | h2 | h3 | h4 | h5 | caption | body
2939
+
2940
+ A2UI.image('id', 'https://example.com/img.jpg', { variant: 'header', fit: 'cover' })
2941
+ A2UI.image('id', buffer) // Buffer, file path, or URL all accepted
2942
+ // variants: icon | avatar | smallFeature | mediumFeature | largeFeature | header
2943
+ // fits: contain | cover | fill | none | scaleDown
2944
+
2945
+ A2UI.icon('id', 'send') // must be a valid catalog icon name
2946
+ A2UI.video('id', 'https://example.com/video.mp4')
2947
+ A2UI.audio('id', 'https://example.com/audio.mp3', { description: 'Track' })
2948
+
2949
+ A2UI.column('id', ['child1', 'child2'], { justify: 'start', align: 'stretch' })
2950
+ A2UI.row('id', ['left', 'right'], { justify: 'spaceEvenly', align: 'center' })
2951
+ A2UI.list('id', ['item1', 'item2'], { direction: 'vertical', align: 'stretch' })
2952
+ A2UI.card('id', 'childId')
2953
+ A2UI.tabs('id', [
2954
+ { title: 'Tab 1', child: 'tab1_content' },
2955
+ { title: 'Tab 2', child: 'tab2_content' }
2956
+ ])
2957
+ A2UI.modal('id', 'triggerButtonId', 'contentId')
2958
+ A2UI.divider('id', { axis: 'horizontal' }) // axis: horizontal | vertical
2959
+ A2UI.button('id', 'childId', { call: 'openUrl', args: { url: 'https://example.com' } }, { variant: 'primary' })
2960
+ // variants: primary | borderless (default not supported in modal trigger)
2961
+
2962
+ A2UI.textField('id', 'Label', { value: '', variant: 'shortText' })
2963
+ // variants: shortText | longText | number | obscured
2964
+
2965
+ A2UI.checkBox('id', 'Label', false)
2966
+ A2UI.choicePicker('id',
2967
+ [{ label: 'Option A', value: 'a' }, { label: 'Option B', value: 'b' }],
2968
+ ['a'],
2969
+ { label: 'Pick one', variant: 'mutuallyExclusive', displayStyle: 'chips' }
2970
+ )
2971
+ // variants: mutuallyExclusive | multipleSelection
2972
+ // displayStyle: checkbox | chips
2973
+
2974
+ A2UI.slider('id', 5, 10, { label: 'Rating', min: 1 })
2975
+ A2UI.dateTimeInput('id', '', { label: 'Date', enableDate: true, enableTime: false })
2976
+
2977
+ A2UI.surface(components, { surfaceId, catalogId, version })
2978
+ ```
2979
+
2980
+ #### Fluent Instance Builder
2981
+
2982
+ ```javascript
2983
+ await new A2UI(sock)
2984
+ .addColumn('root', ['img', 'title', 'dummy'])
2985
+ .addImage('img', 'https://example.com/img.jpg', { variant: 'header', fit: 'cover' })
2986
+ .addText('title', 'Hello!', { variant: 'h1' })
2987
+ .addDivider('div1')
2988
+ .addCard('card1', 'card_col')
2989
+ .addColumn('card_col', ['card_text'])
2990
+ .addText('card_text', 'Card content', { variant: 'body' })
2991
+ .addTabs('tabs1', [
2992
+ { title: 'Tab A', child: 'tabA' },
2993
+ { title: 'Tab B', child: 'tabB' }
2994
+ ])
2995
+ .addModal('modal1', 'modal_btn', 'modal_content')
2996
+ .addText('modal_btn_text', 'Open Modal')
2997
+ .addButtonComponent('modal_btn', 'modal_btn_text', { call: 'openModal', args: { modalId: 'modal1' } }, { variant: 'primary' })
2998
+ .addCard('modal_content', 'modal_col')
2999
+ .addColumn('modal_col', ['modal_text'])
3000
+ .addText('modal_text', 'Modal body', { variant: 'body' })
3001
+ .addImage('dummy', 'https://example.com/img.jpg', { variant: 'avatar', fit: 'cover' })
3002
+ .addButton('quick_reply', { display_text: 'Reply', id: 'reply_id' })
3003
+ .setFallback('Fallback text for unsupported clients')
3004
+ .setSurfaceId('my-custom-surface-id')
3005
+ .setImage('https://example.com/header.jpg') // URL, Buffer, or file path
3006
+ .send(jid)
3007
+ ```
3008
+
3009
+ #### sock.sendA2UI Shortcut
3010
+
3011
+ ```javascript
3012
+ await sock.sendA2UI(jid, [
3013
+ A2UI.column('root', ['img', 'title', 'dummy']),
3014
+ A2UI.image('img', 'https://example.com/img.jpg', { variant: 'header' }),
3015
+ A2UI.text('title', 'Hello!', { variant: 'h1' }),
3016
+ A2UI.image('dummy', 'https://example.com/img.jpg', { variant: 'avatar' })
3017
+ ], {
3018
+ buttons: [
3019
+ { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Hi', id: 'hi' }) }
3020
+ ]
2897
3021
  })
3022
+ ```
3023
+
3024
+ #### sendMessage Dispatch
2898
3025
 
2899
- // Command menu preset
3026
+ ```javascript
3027
+ await sock.sendMessage(jid, {
3028
+ a2uiMessage: {
3029
+ components: [
3030
+ A2UI.column('root', ['img', 'title', 'dummy']),
3031
+ A2UI.image('img', buffer),
3032
+ A2UI.text('title', 'Hello!', { variant: 'h1' }),
3033
+ A2UI.image('dummy', buffer, { variant: 'avatar' })
3034
+ ],
3035
+ buttons: [
3036
+ { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Hi', id: 'hi' }) }
3037
+ ]
3038
+ }
3039
+ })
3040
+ ```
3041
+
3042
+ #### Command Menu Preset
3043
+
3044
+ ```javascript
2900
3045
  await sock.sendA2UICommandMenu(jid, {
2901
3046
  title: 'zervonex Menu',
2902
3047
  rows: [
@@ -2907,7 +3052,20 @@ await sock.sendA2UICommandMenu(jid, {
2907
3052
  buttons: [
2908
3053
  { text: 'GitHub', url: 'https://github.com/nexustechpro2' }
2909
3054
  ],
2910
- footer: 'Powered by zervonex'
3055
+ footer: 'Powered by zervonex',
3056
+ image: 'https://example.com/header.jpg' // optional — URL, Buffer, or file path
3057
+ })
3058
+ ```
3059
+
3060
+ #### Generic Envelope (Advanced)
3061
+
3062
+ ```javascript
3063
+ await sock.sendA2UIMessage(jid, {
3064
+ bloksWidget: {
3065
+ type: 'im_a2ui',
3066
+ data: JSON.stringify({ version: 'v0.9', createSurface: { /* ... */ } })
3067
+ },
3068
+ nativeFlowMessage: { buttons: [{ name: 'single_select', buttonParamsJson: '...' }] }
2911
3069
  })
2912
3070
  ```
2913
3071
 
@@ -2915,7 +3073,7 @@ await sock.sendA2UICommandMenu(jid, {
2915
3073
 
2916
3074
  **A2UI button format:** `{ text, url }` or `{ label, url }`
2917
3075
 
2918
- ---
3076
+ **Valid icon names:** `accountCircle` `add` `arrowBack` `arrowForward` `attachFile` `calendarToday` `call` `camera` `check` `close` `delete` `download` `edit` `event` `error` `fastForward` `favorite` `favoriteOff` `folder` `help` `home` `info` `locationOn` `lock` `lockOpen` `mail` `menu` `moreVert` `moreHoriz` `notificationsOff` `notifications` `pause` `payment` `person` `phone` `photo` `play` `print` `refresh` `rewind` `search` `send` `settings` `share` `shoppingCart` `skipNext` `skipPrevious` `star` `starHalf` `starOff` `stop` `upload` `visibility` `visibilityOff` `volumeDown` `volumeMute` `volumeOff` `volumeUp` `warning`
2919
3077
 
2920
3078
  ### Poll Builder
2921
3079
 
@@ -3031,6 +3189,7 @@ import {
3031
3189
  useMultiFileAuthState,
3032
3190
  useKeyvAuthState,
3033
3191
  hasKeyvAuth,
3192
+ clearAdapterCache,
3034
3193
  makeCacheableSignalKeyStore,
3035
3194
  Browsers,
3036
3195
  DisconnectReason,
@@ -18,9 +18,9 @@ const SCALAR_TC = {
18
18
  const isLongLike = v => v !== null && typeof v === 'object' && 'low' in v && 'high' in v
19
19
  const longToBigInt = v => (v.unsigned ? BigInt(v.high >>> 0) : BigInt(v.high | 0)) * 4294967296n + BigInt(v.low >>> 0)
20
20
  const anyToBigInt = v => typeof v === 'bigint' ? v : isLongLike(v) ? longToBigInt(v) : BigInt(Math.trunc(Number(v)))
21
- const zigzagEncode = v => BigInt.asUintN(64, anyToBigInt(v) << 1n ^ anyToBigInt(v) >> 63n)
22
- const numOrBigInt = b => b <= 9007199254740991n && b >= -9007199254740991n ? Number(b) : b
21
+ const zigzagEncode = v => { const b = anyToBigInt(v); return BigInt.asUintN(64, b << 1n ^ b >> 63n) }
23
22
  const toLong = big => Long.fromString(BigInt.asIntN(64, big).toString())
23
+ const numOrBigInt = b => b <= 9007199254740991n && b >= -9007199254740991n ? Number(b) : toLong(b)
24
24
  const wireType = f => f.k === 'varint' ? 0 : f.k === 'i64' ? 1 : f.k === 'i32' ? 5 : 2
25
25
 
26
26
  const toBuffer = v => {
@@ -33,6 +33,15 @@ const toBuffer = v => {
33
33
  return Buffer.alloc(0)
34
34
  }
35
35
 
36
+ const anyToFloat = v => {
37
+ if (typeof v === 'number') return v
38
+ if (typeof v === 'bigint') return Number(v)
39
+ if (isLongLike(v)) return v.toNumber()
40
+ if (typeof v === 'string') return parseFloat(v)
41
+ if (v == null) return 0
42
+ return Number(v)
43
+ }
44
+
36
45
  const B64_JSON = function () { return this.toString('base64') }
37
46
  const tagBytes = b => (Object.defineProperty(b, 'toJSON', { value: B64_JSON, enumerable: false, configurable: true }), b)
38
47
 
@@ -54,8 +63,8 @@ class Writer {
54
63
  vint(v) { typeof v === 'number' && v >= 0 && v <= MAXSAFE && Number.isInteger(v) ? this.varintNum(v) : this.varintBig(anyToBigInt(v)) }
55
64
  fixed32(v) { this._grow(4); const n = Number(BigInt.asUintN(32, anyToBigInt(v))) >>> 0; this.buf[this.len++] = n & 0xff; this.buf[this.len++] = (n >>> 8) & 0xff; this.buf[this.len++] = (n >>> 16) & 0xff; this.buf[this.len++] = (n >>> 24) & 0xff }
56
65
  fixed64(v) { this._grow(8); const b = Buffer.allocUnsafe(8); b.writeBigUInt64LE(BigInt.asUintN(64, anyToBigInt(v))); this.raw(b) }
57
- float(v) { this._grow(4); const b = Buffer.allocUnsafe(4); b.writeFloatLE(v); this.raw(b) }
58
- double(v) { this._grow(8); const b = Buffer.allocUnsafe(8); b.writeDoubleLE(v); this.raw(b) }
66
+ float(v) { this._grow(4); const b = Buffer.allocUnsafe(4); b.writeFloatLE(anyToFloat(v)); this.raw(b) }
67
+ double(v) { this._grow(8); const b = Buffer.allocUnsafe(8); b.writeDoubleLE(anyToFloat(v)); this.raw(b) }
59
68
  finish() { return Buffer.from(this.buf.buffer, this.buf.byteOffset, this.len) }
60
69
  }
61
70
 
@@ -152,7 +161,7 @@ function makeCodec(TABLE, enumIndex) {
152
161
  if (f.s === 'zigzag') { const u = r.varintBig(); return numOrBigInt(u >> 1n ^ -(u & 1n)) }
153
162
  if (f.s === 'zigzaglong') { const u = r.varintBig(); return toLong(u >> 1n ^ -(u & 1n)) }
154
163
  if (f.s === 'enum') return r.varint() | 0
155
- if (f.s === 'long') return toLong(BigInt(r.varint()))
164
+ if (f.s === 'long') return toLong(r.varintBig())
156
165
  return r.varint()
157
166
  }
158
167
  case 'i64': { const b = Buffer.from(r.read8()); return f.s === 'double' ? b.readDoubleLE(0) : toLong(b.readBigUInt64LE(0)) }
@@ -189,7 +198,10 @@ function makeCodec(TABLE, enumIndex) {
189
198
  if (f.rep) (obj[f.name] || (obj[f.name] = [])).push(val)
190
199
  else obj[f.name] = val
191
200
  }
192
- } catch { break }
201
+ } catch (e) {
202
+ console.warn('[WAProto] decode error:', e.message)
203
+ break
204
+ }
193
205
  }
194
206
  return obj
195
207
  }
@@ -277,16 +289,17 @@ if (isGeneratorRun) {
277
289
  const tablePath = fileURLToPath(new URL('./WAProto.json', import.meta.url))
278
290
  const built = { proto: {}, codec: null }
279
291
 
292
+ export let codec = null
280
293
  export function _reloadProto() {
281
294
  if (!existsSync(tablePath)) return
282
295
  const loaded = makeProto(tablePath)
283
296
  for (const key of Object.keys(built.proto)) delete built.proto[key]
284
297
  Object.assign(built.proto, loaded.proto)
285
298
  built.codec = loaded.codec
299
+ codec = loaded.codec
286
300
  }
287
301
 
288
302
  if (!isGeneratorRun && existsSync(tablePath)) _reloadProto()
289
303
 
290
304
  export const proto = built.proto
291
- export const codec = built.codec
292
305
  export default built
@@ -3,10 +3,10 @@ import { proto } from '../../WAProto/index.js'
3
3
  import {
4
4
  generateMessageIDV2, generateVerificationMetadata, waitAllPromises, extractIE, tokenizeCode, generateWAMessageFromContent, generateWAMessage,
5
5
  makeWhatsAppFlowButton, metaTyping, sendMetaComposited, PlanningStepStatus, buildSteps, replayPlanning, replayPlanningOnly, buildReasoningSteps, buildSearchSteps,
6
- mixedSteps, buildBotRichResponse, BotCapabilityType, generateTableContent, generateListContent, generateCodeBlockContent,
6
+ mixedSteps, buildBotRichResponse, BotCapabilityType, RichSubMessageType, generateTableContent, generateListContent, generateCodeBlockContent,
7
7
  generateLatexContent, captureUnifiedResponse, generateUnifiedResponseContent
8
8
  } from '../Utils/index.js'
9
- import { isJidGroup, isJidMetaAI, jidNormalizedUser } from '../WABinary/index.js'
9
+ import { isJidGroup, isJidMetaAI, jidNormalizedUser, getBizBinaryNode } from '../WABinary/index.js'
10
10
  import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
11
11
  import { randomBytes } from 'crypto'
12
12
 
@@ -60,6 +60,18 @@ const mkProduct = d => ({ __typename: 'GenAIProductItemCardPrimitive', title: d.
60
60
  const mkSearchResult = d => ({ __typename: 'GenAISearchResultPrimitive', source_url: d.url ?? d.source_url ?? '', source_display_name: d.title ?? d.source_display_name ?? '', source_type: d.source_type ?? 'web', source_subtitle: d.subtitle ?? d.source_subtitle ?? '', favicon: d.favicon ?? null })
61
61
  const mkMap = ({ latitude, longitude, name = '', address = '' }) => ({ __typename: 'GenAIMapPrimitive', latitude, longitude, name, address })
62
62
 
63
+ const _mp4Duration = (buf, silent = true) => {
64
+ try { if (!Buffer.isBuffer(buf) || buf.length < 8) { if (silent) return 0; throw new Error('Invalid buffer') }; let o = 0; while (o < buf.length - 8) { const sz = buf.readUInt32BE(o); if (sz < 8 || o + sz > buf.length) { if (silent) return 0; throw new Error('Bad atom') }; if (buf.toString('ascii', o + 4, o + 8) === 'moov') { let m = o + 8; const me = o + sz; while (m < me - 8) { const cs = buf.readUInt32BE(m); if (cs < 8 || m + cs > me) break; if (buf.toString('ascii', m + 4, m + 8) === 'mvhd') { const v = buf.readUInt8(m + 8); if (v === 0) { const ts = buf.readUInt32BE(m + 20); const dur = buf.readUInt32BE(m + 24); return ts ? dur / ts : 0 }; if (v === 1) { const ts = buf.readUInt32BE(m + 32); const dur = Number(buf.readBigUInt64BE(m + 36)); return ts ? dur / ts : 0 } }; m += cs } }; o += sz }; return 0 }
65
+ catch { return 0 }
66
+ }
67
+
68
+ const _mp4Preview = async (buf, time, silent = true) => {
69
+ const fail = e => { if (!silent) throw e; return null }; if (!Buffer.isBuffer(buf) || !buf.length) return fail(new Error('Empty buffer'))
70
+ try {
71
+ const { Readable, PassThrough } = await import('stream'); const { default: ffmpeg } = await import('fluent-ffmpeg'); const src = new Readable({ read() { } }); src.push(buf); src.push(null); const out = new PassThrough(); const chunks = []; out.on('data', c => chunks.push(c)); const t = time ?? Math.max(0.5, Math.min(_mp4Duration(buf) * 0.3, 10)); await new Promise((res, rej) => { out.on('end', res); out.on('error', rej); ffmpeg(src).outputOptions([`-ss ${t}`, '-vframes 1', '-vcodec png', '-f image2pipe']).on('error', rej).pipe(out, { end: true }) }); const b = Buffer.concat(chunks); return b.length ? b.toString('base64') : fail(new Error('Empty output'))
72
+ } catch (e) { return fail(e) }
73
+ }
74
+
63
75
  const mkRichMap = ({ centerLatitude, centerLongitude, latitudeDelta, longitudeDelta, annotations = [], showInfoList = false }) => {
64
76
  if (centerLatitude == null || centerLongitude == null) throw new TypeError('addRichMap requires centerLatitude and centerLongitude')
65
77
  const submessage = { messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_MAP, mapMetadata: { centerLatitude, centerLongitude, ...(latitudeDelta != null ? { latitudeDelta } : {}), ...(longitudeDelta != null ? { longitudeDelta } : {}), showInfoList: !!showInfoList, annotations: annotations.map((a, i) => ({ annotationNumber: a.annotationNumber ?? i + 1, latitude: a.latitude, longitude: a.longitude, title: a.title ?? '', body: a.body ?? '' })) } }
@@ -177,8 +189,8 @@ export const prepareRichGenerationMessage = ({ text = '', mediaType = 'video', u
177
189
  const sections = []
178
190
  if (text) sections.push({ view_model: { primitive: { text: String(text), inline_entities: [], __typename: 'GenAIMarkdownTextUXPrimitive' }, __typename: 'GenAISingleLayoutViewModel' } })
179
191
  sections.push({ view_model: { primitive: buildImaginePrimitive({ mediaType, url, thumbnail, mimeType, fileLength, duration, status, estimatedMs }), __typename: 'GenAISingleLayoutViewModel', id: itemId } })
180
- const submessages = text ? [{ messageType: 2, messageText: String(text) }] : []
181
- const richResponseMessage = proto.AIRichResponseMessage.create({ submessages, messageType: 1, unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: responseId, sections })) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: DEFAULT_BOT_JID }, forwardOrigin: 4 } })
192
+ const submessages = text ? [{ messageType: RichSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: String(text) }] : []
193
+ const richResponseMessage = proto.AIRichResponseMessage.create({ submessages, messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD, unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: responseId, sections })) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: DEFAULT_BOT_JID }, forwardOrigin: 4 } })
182
194
  const message = wrapToBotForwardedMessage(richResponseMessage, verificationMetadata)
183
195
  message.messageContextInfo.botMetadata.botResponseId = responseId
184
196
  return { message, responseId, itemId }
@@ -189,6 +201,7 @@ export class AIRich {
189
201
  #responseId = null; #dynamic = true; #signedVerificationMetadata = null
190
202
  #contextInfoExtra = {}; #embeddedScreens = []; #suggestedPrompts = null
191
203
  #botSources = null; #pttTranscript = null
204
+ #footerSections = []; #responseMeta = {}; #botMetadataExtra = {}
192
205
 
193
206
  constructor(client, opts = {}) {
194
207
  if (!client) throw new Error('Socket client is required')
@@ -210,17 +223,28 @@ export class AIRich {
210
223
  setBotJid(jid) { this.#botJid = jid; return this }
211
224
  setResponseId(id) { this.#responseId = id; this.#dynamic = false; return this }
212
225
  setContextInfo(ci) { this.#contextInfoExtra = ci; return this }
226
+ setResponseMeta(meta) { if (!meta || typeof meta !== 'object') throw new TypeError('responseMeta must be a plain object'); this.#responseMeta = { ...this.#responseMeta, ...meta }; return this }
227
+ setBotMetadata(extra) { if (!extra || typeof extra !== 'object') throw new TypeError('botMetadata must be a plain object'); this.#botMetadataExtra = { ...this.#botMetadataExtra, ...extra }; return this }
228
+ addFooterSection(section) { if (!section || typeof section !== 'object') throw new TypeError('footer section must be a plain object'); this.#footerSections.push(section); return this }
229
+ clearFooterSections() { this.#footerSections = []; return this }
213
230
 
214
231
  addText(text, opts = {}) { const { text: t, inline_entities } = extractIE(text); return this.#add(single(mkMarkdown(t, inline_entities)), null, opts) }
215
- addCode(language, content, opts = {}) { const { codeBlocks, unified_codeBlock } = tokenizeCode(content, language); const submessage = { messageType: 5, codeMetadata: { codeLanguage: language, codeBlocks } }; return this.#add(single(mkCode(content, language, unified_codeBlock)), submessage, opts) }
232
+ addCode(language, content, opts = {}) { const { codeBlocks, unified_codeBlock } = tokenizeCode(content, language); const submessage = { messageType: RichSubMessageType.AI_RICH_RESPONSE_CODE, codeMetadata: { codeLanguage: language, codeBlocks } }; return this.#add(single(mkCode(content, language, unified_codeBlock)), submessage, opts) }
216
233
  addTable(rows, title = '', opts = {}) { return this.#add(single(mkTable(rows, title)), null, opts) }
217
234
  addImage(input, opts = {}) { const resolved = this.#uploadMedia(input, 'image').then(url => single(mkImage(url))); return this.#add(resolved, null, opts) }
218
- addVideo(url, opts = {}) { const resolved = this.#uploadMedia(url, 'video').then(u => single(mkVideo(u))); return this.#add(resolved, null, opts) }
235
+ addVideo(url, { autoFill = false, mimeType = 'video/mp4', ...opts } = {}) {
236
+ const resolved = this.#uploadMedia(url, 'video').then(async finalUrl => { if (!autoFill) return single(mkVideo(finalUrl)); const buf = await fetch(finalUrl).then(r => r.arrayBuffer()).then(b => Buffer.from(b)).catch(() => null); const duration = buf ? _mp4Duration(buf) : 0; const thumbnail = buf ? await _mp4Preview(buf, null, true) : null; return single({ __typename: 'GenAIImaginePrimitive', imagine_type: 'ANIMATE', status: { status: 'READY' }, media: { url: finalUrl, mime_type: mimeType, file_length: buf?.length ?? 0, duration }, thumbnail: thumbnail ? { raw_media: thumbnail } : undefined }) }); return this.#add(resolved, null, opts)
237
+ }
219
238
  addGrid(items, opts = {}) { if (!Array.isArray(items) || items.length < 2) throw new Error('addGrid requires at least 2 items'); return this.#add(grid(items.map(i => mkImage(i))), null, opts) }
220
239
 
221
240
  addVideoGrid(videos, opts = {}) {
222
241
  if (!Array.isArray(videos) || videos.length < 2) throw new Error('addVideoGrid requires at least 2 videos')
223
- const prims = videos.map(v => { const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? ''); const item = typeof v === 'string' ? {} : v; return { __typename: 'GenAIVideoPrimitive', reels_url: url, reels_title: item.title, thumbnail_url: item.thumbnailUrl ?? item.thumbnail, creator: item.creator, avatar_url: item.avatarUrl, likes_count: item.likes, comments_count: item.comments, shares_count: item.shares, is_verified: item.isVerified, video_delivery_response: { progressive_urls: (item.progressiveUrls ?? []).map(u => ({ progressive_url: u, __typename: 'GenAIProgressiveUrlResponse' })), dash_manifests: (item.dashManifests ?? []).map(m => ({ manifest_xml: m, __typename: 'GenAIDashManifestResponse' })), __typename: 'GenAIVideoDeliveryResponse' } } })
242
+ const prims = videos.map((v, i) => {
243
+ const item = typeof v === 'string' ? {} : v
244
+ const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? '')
245
+ if (!url) throw new Error(`VideoGrid item ${i}: video requires a URL`)
246
+ return { __typename: 'GenAIVideoPrimitive', reels_url: url, reels_title: item.title, thumbnail_url: item.thumbnailUrl ?? item.thumbnail, creator: item.creator, avatar_url: item.avatarUrl, likes_count: item.likes, comments_count: item.comments, shares_count: item.shares, is_verified: item.isVerified, video_delivery_response: { progressive_urls: (item.progressiveUrls ?? []).map(u => ({ progressive_url: u, __typename: 'GenAIProgressiveUrlResponse' })), dash_manifests: (item.dashManifests ?? []).map(m => ({ manifest_xml: m, __typename: 'GenAIDashManifestResponse' })), __typename: 'GenAIVideoDeliveryResponse' } }
247
+ })
224
248
  return this.#add(grid(prims), null, opts)
225
249
  }
226
250
 
@@ -282,15 +306,15 @@ export class AIRich {
282
306
  addVerticalAlbum(items, opts = {}) {
283
307
  if (!Array.isArray(items) || items.length < 2) throw new Error('addVerticalAlbum requires at least 2 items')
284
308
  const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)
285
- const primitives = items.map(item => {
309
+ const primitives = items.map((item, i) => {
286
310
  const kind = item?.type || (item?.video ? 'video' : 'image')
287
311
  if (kind === 'video') {
288
- const v = item.video || item
289
- return buildImaginePrimitive({ mediaType: 'video', url: typeof v === 'string' ? v : (v?.url || ''), mimeType: v?.mimeType || 'video/mp4', fileLength: v?.fileLength || 0, duration: v?.duration || 0, status: 'READY' })
312
+ const v = item.video || item.data || item
313
+ return buildImaginePrimitive({ mediaType: 'video', url: typeof v === 'string' ? v : (v?.url || ''), mimeType: v?.mimeType ?? v?.mime_type ?? 'video/mp4', fileLength: v?.fileLength || 0, duration: v?.duration || 0, status: 'READY' })
290
314
  }
291
315
  const img = typeof (item?.image || item) === 'string' ? { url: item?.image || item } : (item?.image || item || {})
292
- const url = img.url || ''
293
- if (!url) throw new Error('Each vertical album image item requires a URL')
316
+ const url = img?.imageHighResUrl ?? img?.highResUrl ?? img?.fullUrl ?? img?.imagePreviewUrl ?? img?.previewUrl ?? img?.url ?? ''
317
+ if (!url) throw new Error(`VerticalAlbum item ${i}: image requires a URL`)
294
318
  return { __typename: 'GenAIImagePrimitive', preview_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, full_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, dark_mode_preview_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, dark_mode_full_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, asset_query_status: 'FETCHED', display_mode: 'FULL_WIDTH', full_width: true }
295
319
  })
296
320
  return this.#add({ view_model: { primitives, display_mode: 'FULL_WIDTH', full_width: true, __typename: 'GenAIVStackLayoutViewModel' } }, null, opts)
@@ -302,6 +326,28 @@ export class AIRich {
302
326
  return this.#add(single(mkMeta(`🎤 _${transcript}_`)), null, opts)
303
327
  }
304
328
 
329
+ // ─── Node introspection & mutation ────────────────────────────────────────
330
+ hasId(id) { return typeof id === 'string' && id in this.#idIndex }
331
+ getIds() { return this.#nodes.map(n => n.id) }
332
+ peek(id) { const idx = this.#idIndex[id]; return idx !== undefined ? { ...this.#nodes[idx] } : null }
333
+ assignId(index, newId) {
334
+ if (!Number.isInteger(index) || index < 0 || index >= this.#nodes.length) throw new RangeError(`Node index ${index} is out of range`)
335
+ if (newId in this.#idIndex) throw new Error(`Id "${newId}" already exists`)
336
+ const node = this.#nodes[index]
337
+ delete this.#idIndex[node.id]
338
+ node.id = newId
339
+ this.#idIndex[newId] = index
340
+ return this
341
+ }
342
+ delete(id) {
343
+ const idx = this.#idIndex[id]
344
+ if (idx === undefined) throw new Error(`Node id "${id}" not found`)
345
+ this.#nodes.splice(idx, 1)
346
+ this.#idIndex = {}
347
+ this.#nodes.forEach((n, i) => { this.#idIndex[n.id] = i })
348
+ return this
349
+ }
350
+
305
351
  loadFrom(msg) {
306
352
  const rich = msg?.botForwardedMessage?.message?.richResponseMessage ?? msg?.richResponseMessage
307
353
  if (!rich) throw new Error('Not an AIRich message')
@@ -324,11 +370,12 @@ export class AIRich {
324
370
  const submessages = resolved.map(n => n.submessage).filter(Boolean)
325
371
  const embeddedScreens = await waitAllPromises(this.#embeddedScreens)
326
372
  const responseId = this.#dynamic ? crypto.randomUUID() : (this.#responseId ?? crypto.randomUUID())
327
- const unifiedData = JSON.stringify({ response_id: responseId, sections, ...(embeddedScreens.length ? { embedded_screens: embeddedScreens } : {}) })
373
+ const footerSections = await waitAllPromises(this.#footerSections)
374
+ const unifiedData = JSON.stringify({ response_id: responseId, ...this.#responseMeta, sections, ...(footerSections.length ? { footer_sections: footerSections } : {}), ...(embeddedScreens.length ? { embedded_screens: embeddedScreens } : {}) })
328
375
  const contextInfo = { isForwarded: true, forwardOrigin: 4, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, participant: '13135550002@s.whatsapp.net', remoteJid: 'status@broadcast', quotedMessage: { protocolMessage: { type: 25 } }, ...this.#contextInfoExtra, ...(quoted ? { quotedMessage: quoted.message, stanzaId: quoted.key?.id, participant: quotedParticipant ?? quoted.key?.participant ?? quoted.key?.remoteJid, remoteJid: quoted.key?.remoteJid } : {}) }
329
376
  const verification = this.#signedVerificationMetadata ?? generateVerificationMetadata()
330
377
  const { sources, mediaDetailsMetadataList } = buildAdditionalBotMetadata(submessages)
331
- const richResponseMessage = proto.AIRichResponseMessage.create({ messageType: 1, submessages, unifiedResponse: { data: Buffer.from(unifiedData) }, originalRecipientMetadata: { data: Buffer.from(unifiedData) }, contextInfo })
378
+ const richResponseMessage = proto.AIRichResponseMessage.create({ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD, submessages, unifiedResponse: { data: Buffer.from(unifiedData) }, originalRecipientMetadata: { data: Buffer.from(unifiedData) }, contextInfo })
332
379
  const message = wrapToBotForwardedMessage(richResponseMessage, verification)
333
380
  const botMeta = message.messageContextInfo.botMetadata
334
381
  botMeta.botResponseId = responseId
@@ -340,6 +387,7 @@ export class AIRich {
340
387
  if (mediaDetailsMetadataList.length) botMeta.unifiedResponseMutation = { mediaDetailsMetadataList }
341
388
  if (this.#suggestedPrompts) botMeta.suggestedPromptMetadata = this.#suggestedPrompts
342
389
  if (this.#pttTranscript) botMeta.pttPromptMetadata = { transcript: this.#pttTranscript }
390
+ if (Object.keys(this.#botMetadataExtra).length) Object.assign(botMeta, this.#botMetadataExtra)
343
391
  botMeta.capabilities = { richResponseUnifiedResponse: true, richResponseEmbeddedScreens: true, richResponseInlineLinksEnabled: true, richResponseUrBloksEnabled: true, richResponseUrImagine: true, richResponseUrReasoning: true }
344
392
  return await await generateWAMessageFromContent(jid, message, { userJid: this.#client.user?.id, messageId })
345
393
  }
@@ -347,14 +395,11 @@ export class AIRich {
347
395
  async send(jid, opts = {}) {
348
396
  const { additionalNodes = [], ...buildOpts } = opts
349
397
  const msg = await this.build(jid, buildOpts)
398
+ const bizNode = getBizBinaryNode(msg.message)
350
399
  const relayFn = this.#client.relayRichMessage ?? this.#client.relayMessage
351
400
  await relayFn(jid, msg, {
352
401
  ...opts,
353
- additionalNodes: [
354
- { tag: 'bot', attrs: { biz_bot: '1' }, content: undefined },
355
- { tag: 'biz', attrs: {}, content: [{ tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] }] },
356
- ...(opts.additionalNodes ?? [])
357
- ]
402
+ additionalNodes: [...(bizNode ? [bizNode] : []), ...(opts.additionalNodes ?? [])]
358
403
  })
359
404
  return msg
360
405
  }
@@ -370,31 +415,17 @@ export class AIRich {
370
415
  }
371
416
 
372
417
  export function aiRichFromObject(client, jid, data, opts = {}) {
373
- const r = new AIRich(client, { botJid: data.botJid })
374
- if (data.title) r.setTitle(data.title)
418
+ const { _r, ...sendOpts } = opts
419
+ const r = _r ?? new AIRich(client, { botJid: data.botJid })
420
+ const isChild = !!_r
421
+ if (!isChild && (data.title ?? data.disclaimerText)) r.setTitle(data.title ?? data.disclaimerText)
375
422
  if (data.parts) {
376
423
  for (const p of data.parts) {
377
- if (p.type === 'text') r.addText(p.content ?? p.text)
378
- if (p.type === 'code') r.addCode(p.language ?? '', p.content ?? p.code)
379
- if (p.type === 'table') r.addTable(p.table, p.title)
380
- if (p.type === 'image') r.addImage(p.url ?? p.image)
381
- if (p.type === 'grid') r.addGrid(p.items ?? p.images)
382
- if (p.type === 'videoGrid') r.addVideoGrid(p.items ?? p.videos)
383
- if (p.type === 'imagine') r.addImagine(p)
384
- if (p.type === 'sources') r.addSource(p.sources)
385
- if (p.type === 'divider') r.addDivider()
386
- if (p.type === 'spacer') r.addSpacer(p.spacing)
387
- if (p.type === 'tip') r.addTip(p.content ?? p.text)
388
- if (p.type === 'meta') r.addMetadata(p.content ?? p.text)
389
- if (p.type === 'foa') r.addFOAText(p.content ?? p.text)
390
- if (p.type === 'html') r.addHtml(p.html, p.trustedSources)
391
- if (p.type === 'richHtml') r.addRichHtml(p.tabs ?? p.html, p.title)
392
- if (p.type === 'copy') r.addCopyAction(p.text, p.label, p.alignment)
393
- if (p.type === 'social') r.addSocialProfile(p)
394
- if (p.type === 'imagine') r.addImagine(p)
395
- if (p.type === 'rawSections') r.addRawSections(p.sections, p.submessages)
424
+ const code = Array.isArray(p.code) ? p.code.map(c => c.codeContent ?? c.content ?? '').join('\n') : p.code
425
+ const table = Array.isArray(p.table) ? p.table.map(r => Array.isArray(r) ? r : (r.items ?? r.cells ?? [])) : p.table
426
+ aiRichFromObject(client, jid, { ...p, ...(code ? { code } : {}), ...(table ? { table } : {}) }, { ...sendOpts, _r: r })
396
427
  }
397
- return r.send(jid, opts)
428
+ return isChild ? r : r.send(jid, sendOpts)
398
429
  }
399
430
  if (data.texts) data.texts.forEach(t => r.addText(t))
400
431
  else if (data.text) r.addText(data.text)
@@ -426,7 +457,7 @@ export function aiRichFromObject(client, jid, data, opts = {}) {
426
457
  if (data.metadata ?? data.meta) r.addMetadata(data.metadata ?? data.meta)
427
458
  if (data.footer) r.addFOAText(data.footer)
428
459
  if (data.rawSections) r.addRawSections(data.rawSections, data.rawSubmessages)
429
- return r.send(jid, opts)
460
+ return isChild ? r : r.send(jid, sendOpts)
430
461
  }
431
462
 
432
463
  const stripDevice = jid => String(jid || '').replace(/:\d+$/, '')
@@ -484,7 +515,7 @@ export function attachAIRich(result, config) {
484
515
  result.captureAiRich = msg => {
485
516
  const rich = msg?.botForwardedMessage?.message?.richResponseMessage ?? msg?.richResponseMessage
486
517
  if (!rich?.unifiedResponse?.data) return null
487
- return { submessages: rich.submessages ?? [], sections: JSON.parse(Buffer.from(rich.unifiedResponse.data, 'base64').toString()), contextInfo: rich.contextInfo ?? {}, messageType: rich.messageType ?? 1 }
518
+ return { submessages: rich.submessages ?? [], sections: JSON.parse(Buffer.isBuffer(rich.unifiedResponse.data) ? rich.unifiedResponse.data.toString() : Buffer.from(rich.unifiedResponse.data, 'base64').toString()), contextInfo: rich.contextInfo ?? {}, messageType: rich.messageType ?? proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD }
488
519
  }
489
520
 
490
521
  result.relayAiRich = (jid, captured, opts = {}) => {
@@ -524,7 +555,7 @@ export function attachAIRich(result, config) {
524
555
  }
525
556
  result.sendBotPlanning = async (jid, content = {}, opts = {}) => {
526
557
  const { text = 'Execution plan', steps = [], queryPlan = true, botJid = META_AI_BOT_JID } = content
527
- const submessages = [{ messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: text }, ...steps.map(s => ({ messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: typeof s === 'string' ? s : `${s.title || s.name || 'Step'}${s.body ? `: ${s.body}` : ''}` }))]
558
+ const submessages = [{ messageType: RichSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: text }, ...steps.map(s => ({ messageType: RichSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: typeof s === 'string' ? s : `${s.title || s.name || 'Step'}${s.body ? `: ${s.body}` : ''}` }))]
528
559
  const wrapped = buildBotRichResponse({ botJid, submessages, capabilities: [BotCapabilityType.AGENTIC_PLANNING, ...(queryPlan ? [BotCapabilityType.QUERY_PLAN] : [])] })
529
560
  const full = await await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
530
561
  return relayRichMessage(jid, full, opts)
@@ -534,7 +565,7 @@ export function attachAIRich(result, config) {
534
565
  if (!Array.isArray(steps) || steps.length === 0) throw new Error('sendBotPlan requires at least one step')
535
566
  const statusEnum = proto.BotProgressIndicatorMetadata.BotPlanningStepMetadata.PlanningStepStatus
536
567
  const normalizedSteps = steps.map((s, i) => ({ statusTitle: String(s.title || s.name || `Step ${i + 1}`), statusBody: String(s.body || s.description || ''), status: s.status === 'executing' ? statusEnum.EXECUTING : (s.status === 'finished' || s.status === 'completed') ? statusEnum.FINISHED : statusEnum.PLANNED, isReasoning: s.isReasoning === true, isEnhancedSearch: s.isEnhancedSearch === true }))
537
- const wrapped = { messageContextInfo: { botMetadata: { capabilityMetadata: { capabilities: [proto.BotCapabilityMetadata.BotCapabilityType.AGENTIC_PLANNING, proto.BotCapabilityMetadata.BotCapabilityType.QUERY_PLAN] }, progressIndicatorMetadata: { progressDescription: title, stepsMetadata: normalizedSteps, ...(estimatedCompletionTime ? { estimatedCompletionTime } : {}) } } }, botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } } }
568
+ const wrapped = { messageContextInfo: { botMetadata: { capabilityMetadata: { capabilities: [proto.BotCapabilityMetadata.BotCapabilityType.AGENTIC_PLANNING, proto.BotCapabilityMetadata.BotCapabilityType.QUERY_PLAN] }, progressIndicatorMetadata: { progressDescription: title, stepsMetadata: normalizedSteps, ...(estimatedCompletionTime ? { estimatedCompletionTime } : {}) } } }, botForwardedMessage: { message: { richResponseMessage: { messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD, submessages: [{ messageType: RichSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } } }
538
569
  const full = await await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
539
570
  await sock.relayMessage(jid, full.message, { messageId: full.key.id })
540
571
  return full