whalibmob 5.5.93 → 5.6.3

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
@@ -36,7 +36,8 @@ Usdc ethereum network.
36
36
  > This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or affiliates. "WhatsApp" and related names are registered trademarks of their respective owners. Use at your own discretion.
37
37
 
38
38
  - whalibmob does not require a browser, Selenium, or any other external runtime — it communicates directly with WhatsApp using a **TCP socket** and the **Noise Protocol** handshake.
39
- - The library operates as a real **iOS mobile device**, not as WhatsApp Web. It uses the Mobile API endpoint, which behaves differently from the Web API.
39
+ - The library operates as a real **iOS mobile device**, using the Mobile API endpoint, which behaves differently from the Web API.
40
+ - It **also speaks WhatsApp Web over a WebSocket**. When a number cannot receive an SMS, or is already in use on a phone, whalibmob can link itself to that existing account with an **8-character pairing code** and run as one of its linked devices — with full message history and the account's address book. See [Linking to an Existing Account](#linking-to-an-existing-account-pairing-code). The API is identical in both modes.
40
41
  - Signal Protocol encryption is **fully inlined** in pure JavaScript — no native binaries, no node-gyp, runs anywhere Node.js runs.
41
42
 
42
43
  ## Install
@@ -57,6 +58,7 @@ npm install -g whalibmob
57
58
  - [Install the CLI](#install-the-cli)
58
59
  - [First-Time Setup: Register a Number](#first-time-setup-register-a-number)
59
60
  - [Connect](#cli-connect)
61
+ - [Pairing Code](#cli-pairing-code)
60
62
  - [Listen Mode](#listen-mode)
61
63
  - [CLI — Interactive Shell Commands](#cli--interactive-shell-commands)
62
64
  - [Messaging Commands](#messaging-commands)
@@ -128,6 +130,26 @@ npm install -g whalibmob
128
130
  - [Register a New Number](#register-a-new-number)
129
131
  - [Device Attestation with Frida (optional)](#device-attestation-with-frida-optional)
130
132
  - [Connect](#connect)
133
+ - [Linking to an Existing Account (Pairing Code)](#linking-to-an-existing-account-pairing-code)
134
+ - [Requesting a Pairing Code](#requesting-a-pairing-code)
135
+ - [Reconnecting a Linked Session](#reconnecting-a-linked-session)
136
+ - [Choosing Your Own Code](#choosing-your-own-code)
137
+ - [Options](#options)
138
+ - [Events Specific to Linking](#events-specific-to-linking)
139
+ - [Getting the History and the Address Book](#getting-the-history-and-the-address-book)
140
+ - [Session Files](#session-files)
141
+ - [Media in Companion Mode](#media-in-companion-mode)
142
+ - [Device Identity](#device-identity)
143
+ - [How the Code Protects the Link](#how-the-code-protects-the-link)
144
+ - [Companion Mode — Node.js API](#companion-mode--nodejs-api)
145
+ - [Complete Working Example](#complete-working-example)
146
+ - [Methods](#methods)
147
+ - [Events](#events)
148
+ - [Reading What the Phone Sent](#reading-what-the-phone-sent)
149
+ - [Sending](#sending)
150
+ - [Handling Reconnects](#handling-reconnects)
151
+ - [Knowing Which Mode You Are In](#knowing-which-mode-you-are-in)
152
+ - [Two Sessions on One Number](#two-sessions-on-one-number)
131
153
  - [Saving & Restoring Sessions](#saving--restoring-sessions)
132
154
  - [Signal Store Utilities](#signal-store-utilities)
133
155
  - [makeCacheableSignalKeyStore](#makecacheablesignalkeystore)
@@ -400,6 +422,390 @@ client.on('connected', () => {
400
422
  await client.init('919634847671')
401
423
  ```
402
424
 
425
+ ## Linking to an Existing Account (Pairing Code)
426
+
427
+ Registering a number over SMS makes whalibmob that number's **own device**. Sometimes that is not what you want — the number is already in use on a phone, or the verification SMS never arrives. For those cases whalibmob can instead connect over a **WebSocket** and link itself to an account that already exists, exactly the way the WhatsApp Web and desktop clients do.
428
+
429
+ You get an 8-character pairing code, the account owner types it into their phone, and from then on whalibmob is one of the account's linked devices. The whole library works the same afterwards — same client, same methods, same events.
430
+
431
+ > [!IMPORTANT]
432
+ > The two modes are independent. SMS registration is unchanged and still the default; nothing about it is affected by linking. A single number can even have both a registered session and a linked session — they are stored in separate files and never share state.
433
+
434
+ ### Requesting a Pairing Code
435
+
436
+ Connect first, then ask for the code: the request travels over the encrypted channel, so the channel has to exist before there can be a code.
437
+
438
+ ```js
439
+ const { WhalibmobClient } = require('whalibmob')
440
+ const path = require('path')
441
+
442
+ const client = new WhalibmobClient({
443
+ sessionDir: path.join(process.env.HOME, '.waSession')
444
+ })
445
+
446
+ client.on('paired', (p) => {
447
+ console.log('linked as', p.jid) // 919634847671:7@s.whatsapp.net
448
+ console.log('lid ', p.lid) // 112713111982325:7@lid
449
+ console.log('slot ', p.deviceIndex) // 7
450
+ })
451
+
452
+ client.on('connected', () => {
453
+ console.log('ready')
454
+ })
455
+
456
+ // open the WebSocket connection as a companion
457
+ await client.connectWeb('919634847671', { syncFullHistory: true })
458
+
459
+ // ask for the code — returns immediately, the link completes later
460
+ const code = await client.requestPairingCode('919634847671')
461
+ console.log('enter this on the phone:', code) // e.g. "K7M2QX4B"
462
+ ```
463
+
464
+ On the phone that owns the number:
465
+
466
+ **WhatsApp → Settings → Linked Devices → Link a device → Link with phone number instead**, then type the code.
467
+
468
+ A few seconds after the code is accepted you will see `paired`, the server restarts the stream, and `connected` fires on the new connection. From that point on everything else in this document applies unchanged:
469
+
470
+ ```js
471
+ await client.sendText('919876543210@s.whatsapp.net', 'sent from a linked device')
472
+ ```
473
+
474
+ ### Reconnecting a Linked Session
475
+
476
+ The link is persisted. On every later run, `connectWeb()` alone is enough — do **not** request a new code:
477
+
478
+ ```js
479
+ const client = new WhalibmobClient({ sessionDir })
480
+
481
+ client.on('connected', () => console.log('reconnected'))
482
+
483
+ await client.connectWeb('919634847671')
484
+ ```
485
+
486
+ `requestPairingCode()` throws if the session is already linked, so it is safe to guard on that.
487
+
488
+ ### Choosing Your Own Code
489
+
490
+ If you would rather show the user a code you picked, pass it as the second argument. It must be exactly 8 characters:
491
+
492
+ ```js
493
+ const code = await client.requestPairingCode('919634847671', 'MYCODE12')
494
+ ```
495
+
496
+ ### Options
497
+
498
+ ```js
499
+ await client.connectWeb(phone, {
500
+ syncFullHistory: true, // ask the phone for full history (default true)
501
+ browser: ['Ubuntu', 'Chrome', '120.0.0.0'] // what the owner sees in Linked Devices
502
+ })
503
+ ```
504
+
505
+ `browser` is `[os, client, version]`. The second element decides the device icon shown on the phone — `Chrome`, `Firefox`, `Safari`, `Edge`, `Opera`, `Desktop` are all recognised.
506
+
507
+ ### Events Specific to Linking
508
+
509
+ | Event | Fires when |
510
+ |---|---|
511
+ | `pairing_code` | a code has been requested — `{ code, phoneNumber }` |
512
+ | `paired` | the owner accepted the code — `{ jid, lid, deviceIndex, platform }` |
513
+ | `restart_required` | the server is restarting the stream after pairing (normal; the reconnect is automatic) |
514
+ | `pair_device` | the QR path produced reference strings — `{ refs }` |
515
+ | `history_sync` | a chunk of history arrived from the phone |
516
+
517
+ ### Getting the History and the Address Book
518
+
519
+ This is the part a registered number can never do. A device registered over SMS **is** the account's primary, and a primary has nobody to receive history from — it starts with an empty contact list and an empty chat list, and only learns about people who message it.
520
+
521
+ A linked device is different: the account's phone ships its chats, its contacts and its push names over as soon as the link is established. whalibmob decrypts and stores those automatically, and emits them as they arrive:
522
+
523
+ ```js
524
+ client.on('history_sync', (r) => {
525
+ console.log(r.syncTypeName) // INITIAL_BOOTSTRAP, RECENT, FULL, PUSH_NAME
526
+ console.log('chats ', r.chats.length)
527
+ console.log('contacts', r.contacts.length)
528
+
529
+ for (const c of r.contacts.slice(0, 5)) {
530
+ console.log(c.jid, c.name || c.notify)
531
+ }
532
+ })
533
+
534
+ await client.connectWeb(phone, { syncFullHistory: true })
535
+ ```
536
+
537
+ History arrives in chunks over the first minute or so after linking, largest first. Everything is merged into `<phone>.web.history.json` in your session directory, so it survives restarts and you can read it directly.
538
+
539
+ > [!NOTE]
540
+ > `syncFullHistory: false` asks only for recent messages, which links noticeably faster on accounts with years of history.
541
+
542
+ ### Session Files
543
+
544
+ | File | Holds |
545
+ |---|---|
546
+ | `<phone>.web.json` | link state — keys, `advSecretKey`, the device slot you were given |
547
+ | `<phone>.web.signal.json` | Signal sessions and pre-keys for the linked device |
548
+ | `<phone>.web.sk.json` | group SenderKeys |
549
+ | `<phone>.web.tctoken.json` | privacy tokens |
550
+ | `<phone>.web.history.json` | synced chats, contacts, push names, LID↔PN mappings |
551
+ | `<phone>.web.messages.json` | flat map of message id → message metadata |
552
+
553
+ The `.web.` prefix keeps a linked session entirely separate from an SMS-registered one for the same number.
554
+
555
+ ### Media in Companion Mode
556
+
557
+ Uploads and downloads go to the CDN endpoints the web client uses, not the mobile ones. This is handled for you — `sendImage`, `sendVideo`, `sendSticker` and the rest take the same arguments in both modes — but it is worth knowing why the distinction exists.
558
+
559
+ The web client has no endpoint per media type. A sticker is uploaded to the image endpoint and a GIF to the video one; what makes them a sticker or a GIF lives in the message itself, not in the URL. Uploading a sticker to `/mms/sticker` as a companion gets a 404. The CDN also expects a browser `Origin` on both the upload and the download, and a mobile WhatsApp user agent against a web-issued auth token is a mismatch it can reject.
560
+
561
+ | Media | Primary endpoint | Companion endpoint |
562
+ |---|---|---|
563
+ | image | `/mms/image` | `/mms/image` |
564
+ | video | `/mms/video` | `/mms/video` |
565
+ | audio | `/mms/audio` | `/mms/audio` |
566
+ | document | `/mms/document` | `/mms/document` |
567
+ | sticker | `/mms/sticker` | `/mms/image` |
568
+ | gif | `/mms/gif` | `/mms/video` |
569
+ | voice note | `/mms/ptt` | `/mms/audio` |
570
+
571
+ The same applies to history sync blobs and profile pictures.
572
+
573
+ ### Device Identity
574
+
575
+ Every message a companion sends that opens a new Signal session carries a `device-identity` node: the signed record the account's primary device issued during pairing, including the account signature key. That is how the recipient's client knows a message from device 7 of an account genuinely belongs to that account rather than to someone who merely knows the number.
576
+
577
+ whalibmob attaches it automatically whenever a `pkmsg` is in the stanza, in direct messages and in groups alike. A device registered over SMS has no such record — nothing issued one to it — and correctly sends nothing.
578
+
579
+ ### How the Code Protects the Link
580
+
581
+ The pairing code is a password, not an identifier. It is never sent to the server in the clear. Both sides run it through PBKDF2-SHA256 (131,072 iterations) to derive a key that wraps the ephemeral public keys they exchange, then combine two Diffie-Hellman results into `adv_secret` — the key every later proof of account membership is authenticated under.
582
+
583
+ whalibmob verifies three things before accepting a link, and refuses it outright if any fails: the HMAC over the signed device identity, the account signature over its own identity key, and the device slot it was issued. A wrong code cannot produce a working link, and a tampered response cannot either.
584
+
585
+ ## Companion Mode — Node.js API
586
+
587
+ Everything below is the full surface for running whalibmob as a linked device. If you have used the SMS primary API, none of it will surprise you: the client is the same class, the methods take the same arguments, and the events carry the same shapes. Only the way in differs.
588
+
589
+ ### Complete Working Example
590
+
591
+ A bot that links itself on first run, reconnects silently on every run after that, and replies to messages.
592
+
593
+ ```js
594
+ const { WhalibmobClient } = require('whalibmob')
595
+ const path = require('path')
596
+ const fs = require('fs')
597
+
598
+ const PHONE = '919634847671' // no '+', no spaces
599
+ const SESS_DIR = path.join(process.env.HOME, '.waSession')
600
+
601
+ // A session is already linked when this file exists and carries a device JID.
602
+ function isLinked() {
603
+ const f = path.join(SESS_DIR, PHONE + '.web.json')
604
+ if (!fs.existsSync(f)) return false
605
+ try {
606
+ const j = JSON.parse(fs.readFileSync(f, 'utf8'))
607
+ return !!(j.registered && j.me && j.me.id)
608
+ } catch { return false }
609
+ }
610
+
611
+ async function start() {
612
+ const client = new WhalibmobClient({ sessionDir: SESS_DIR })
613
+
614
+ client.on('pairing_code', ({ code }) => {
615
+ console.log('\n pairing code:', code.slice(0, 4) + '-' + code.slice(4))
616
+ console.log(' phone → Settings → Linked Devices → Link a device')
617
+ console.log(' → Link with phone number instead\n')
618
+ })
619
+
620
+ client.on('paired', ({ jid, lid, deviceIndex, platform }) => {
621
+ console.log('linked as', jid, '· slot', deviceIndex, '· primary is', platform)
622
+ })
623
+
624
+ // The server restarts the stream right after pairing. This is normal and the
625
+ // reconnect is automatic — there is nothing to do but log it.
626
+ client.on('restart_required', () => console.log('restarting stream...'))
627
+
628
+ client.on('connected', () => console.log('connected'))
629
+
630
+ client.on('history_sync', (r) => {
631
+ console.log(`history ${r.syncTypeName}: ${r.chats.length} chats, ${r.contacts.length} contacts`)
632
+ })
633
+
634
+ client.on('message', async (msg) => {
635
+ const d = msg.decoded
636
+ if (!d) return
637
+ console.log(msg.from, d.type, d.text || '')
638
+
639
+ if (d.type === 'text' && d.text === 'ping') {
640
+ await client.sendText(msg.from, 'pong')
641
+ }
642
+ })
643
+
644
+ client.on('error', (e) => console.error('error:', e.message))
645
+
646
+ await client.connectWeb(PHONE, { syncFullHistory: true })
647
+
648
+ if (!isLinked()) {
649
+ await client.requestPairingCode(PHONE)
650
+ }
651
+ }
652
+
653
+ start()
654
+ ```
655
+
656
+ Run it once, type the code into the phone, and it is linked. Run it again and it connects straight away.
657
+
658
+ ### Methods
659
+
660
+ | Method | Description |
661
+ |---|---|
662
+ | `client.connectWeb(phone, opts?)` | Open the companion connection. Resolves as soon as the encrypted channel is up when unlinked, or after `<success>` when already linked. |
663
+ | `client.requestPairingCode(phone?, customCode?)` | Ask for an 8-character code. Returns it immediately; the link completes later. Throws if the session is already linked. |
664
+ | `client.disconnect()` | Close the connection. The link survives — reconnect with `connectWeb()`. |
665
+
666
+ `connectWeb(phone, opts)` options:
667
+
668
+ | Option | Default | Description |
669
+ |---|---|---|
670
+ | `syncFullHistory` | `true` | Ask the phone for the full archive. `false` requests recent messages only and links noticeably faster. |
671
+ | `browser` | `['Ubuntu', 'Chrome', '120.0.0.0']` | `[os, client, version]`. The second element picks the icon shown under Linked Devices: `Chrome`, `Firefox`, `Safari`, `Edge`, `Opera`, `Desktop`. |
672
+ | `version` | fetched live | Web client revision to announce, e.g. `[2, 3000, 1035194821]`. Set it to pin one. |
673
+ | `fetchVersion` | `true` | Set `false` to skip the live lookup and use the pinned fallback. |
674
+
675
+ #### The Announced Version
676
+
677
+ The web endpoint checks the client revision during the handshake and refuses an unrecognised one with `<failure reason="405">` — before any stanza is exchanged, and with nothing in the failure to say the version was the problem. The mobile endpoint is far more forgiving; this one is not.
678
+
679
+ whalibmob therefore reads the live revision from WhatsApp Web's own service worker on each `connectWeb()`, and falls back to a pinned value if that lookup fails. You should not have to think about it, but you can:
680
+
681
+ ```js
682
+ const { fetchWaWebVersion } = require('whalibmob')
683
+
684
+ const { version, isLatest } = await fetchWaWebVersion()
685
+ console.log(version, isLatest) // [2, 3000, 1035194821] true
686
+
687
+ // pin it yourself
688
+ await client.connectWeb(phone, { version: [2, 3000, 1035194821] })
689
+
690
+ // or skip the lookup entirely
691
+ await client.connectWeb(phone, { fetchVersion: false })
692
+ ```
693
+
694
+ If you ever see `405`, this is what it means. It is not a revoked session and there is nothing to re-pair — reconnect to pick up the current revision.
695
+
696
+ `requestPairingCode(phone, customCode)`:
697
+
698
+ - `phone` — optional; defaults to the number given to `connectWeb`.
699
+ - `customCode` — optional; must be **exactly 8 characters** or it throws. Use it to show a code you generated yourself.
700
+
701
+ ### Events
702
+
703
+ Every event from the SMS primary API fires here too — `message`, `receipt`, `presence`, `group_update`, `call`, `blocklist`, `privacy_settings`, and the rest. These are the ones only companion mode produces:
704
+
705
+ | Event | Payload | Fires when |
706
+ |---|---|---|
707
+ | `pairing_code` | `{ code, phoneNumber }` | a code was requested |
708
+ | `paired` | `{ jid, lid, deviceIndex, platform }` | the owner accepted the code |
709
+ | `restart_required` | `{ reason }` | the server is restarting the stream after pairing — the reconnect is automatic |
710
+ | `pair_device` | `{ refs }` | the QR path produced reference strings |
711
+ | `history_sync` | `{ syncTypeName, chats, contacts, pushNames, merged }` | a chunk of history arrived |
712
+ | `history_sync_error` | `{ err, notification }` | a chunk could not be fetched or decrypted |
713
+ | `client_rejected` | `{ reason, location, message }` | the server refused the client itself, not the session — `405` means the announced version is not accepted. Distinct from `auth_failure`, and there is nothing to re-pair. |
714
+
715
+ ### Reading What the Phone Sent
716
+
717
+ ```js
718
+ client.on('history_sync', (r) => {
719
+ // r.syncTypeName — INITIAL_BOOTSTRAP | RECENT | FULL | PUSH_NAME | ...
720
+ for (const chat of r.chats) {
721
+ // { id, name, unreadCount, lastMsgTimestamp, messageCount }
722
+ console.log(chat.id, chat.name, chat.unreadCount, chat.messageCount)
723
+ }
724
+ for (const contact of r.contacts) {
725
+ // { id, name, username, pnJid, lidJid }
726
+ console.log(contact.id, contact.name, contact.pnJid, contact.lidJid)
727
+ }
728
+ for (const p of r.pushNames || []) {
729
+ console.log(p.id, p.pushname)
730
+ }
731
+ })
732
+ ```
733
+
734
+ Chunks arrive over the first minute or so after linking, largest first. Everything is merged into `<phone>.web.history.json`, so you can also read it straight off disk once and skip the event:
735
+
736
+ ```js
737
+ const hist = JSON.parse(
738
+ fs.readFileSync(path.join(SESS_DIR, PHONE + '.web.history.json'), 'utf8')
739
+ )
740
+ console.log(Object.keys(hist.chats || {}).length, 'chats on disk')
741
+ ```
742
+
743
+ ### Sending
744
+
745
+ Identical to primary mode — same methods, same arguments, same return shapes:
746
+
747
+ ```js
748
+ await client.sendText(jid, 'hello')
749
+ await client.sendImage(jid, './photo.jpg', 'a caption')
750
+ await client.sendVideo(jid, './clip.mp4', 'watch this')
751
+ await client.sendAudio(jid, './voice.ogg', { ptt: true })
752
+ await client.sendDocument(jid, './report.pdf', 'report.pdf')
753
+ await client.sendSticker(jid, './sticker.webp')
754
+ await client.sendLocation(jid, 44.4268, 26.1025, 'Bucharest')
755
+ await client.sendContact(jid, 'Ana', vcard)
756
+ await client.sendPoll(jid, 'Lunch?', ['Pizza', 'Sushi'], 1)
757
+ await client.sendReaction(jid, msgId, '👍')
758
+ await client.sendStatus({ image: './photo.jpg', caption: 'hi' })
759
+ ```
760
+
761
+ Full signatures for each of these are in [Sending Messages](#sending-messages) and [Media Messages](#media-messages); nothing about them changes in companion mode.
762
+
763
+ Groups, blocking, privacy settings and profile changes work the same way too.
764
+
765
+ > [!NOTE]
766
+ > Media uploads go to the endpoints the web client uses, which are not the same as the mobile ones for stickers, GIFs and voice notes. whalibmob switches automatically — see [Media in Companion Mode](#media-in-companion-mode).
767
+
768
+ ### Handling Reconnects
769
+
770
+ The library reconnects on its own with backoff. What you should handle is the difference between a transient drop and a revoked link:
771
+
772
+ ```js
773
+ client.on('disconnected', () => console.log('dropped'))
774
+ client.on('reconnecting', ({ delay }) => console.log('retry in', delay / 1000, 's'))
775
+ client.on('reconnected', () => console.log('back'))
776
+
777
+ // The owner removed this device under Linked Devices. The session is dead —
778
+ // delete it and pair again.
779
+ client.on('auth_failure', ({ reason }) => {
780
+ console.error('link revoked:', reason)
781
+ fs.rmSync(path.join(SESS_DIR, PHONE + '.web.json'), { force: true })
782
+ process.exit(1)
783
+ })
784
+ ```
785
+
786
+ ### Knowing Which Mode You Are In
787
+
788
+ ```js
789
+ console.log(client._mode) // 'web' or 'mobile'
790
+ console.log(client.store.me.id) // 919634847671:7@s.whatsapp.net
791
+ console.log(client.store.me.lid) // 112713111982325:7@lid
792
+ console.log(client.store.deviceIndex) // 7 — which linked-device slot
793
+ console.log(client.store.platform) // 'android' — what the primary runs
794
+ ```
795
+
796
+ `deviceIndex` is what makes a companion a companion. A primary is device 0 and its JID is the bare number; a companion occupies a numbered slot, and the account's own phone becomes a peer it encrypts to like any other device.
797
+
798
+ ### Two Sessions on One Number
799
+
800
+ A number may be SMS-registered and separately linked as a companion. They never share state — separate files, separate Signal sessions, separate history. Which one you get is decided by the method you call:
801
+
802
+ ```js
803
+ await client.init(PHONE) // mobile / primary, over TCP
804
+ await client.connectWeb(PHONE) // web / companion, over WebSocket
805
+ ```
806
+
807
+ Use two `WhalibmobClient` instances if you want both at once.
808
+
403
809
  ## Saving & Restoring Sessions
404
810
 
405
811
  Sessions are automatically persisted to disk as JSON files under the `sessionDir` you provide. The file is named `<phone>.json`. On the next `client.init()` call the session is restored and no re-registration is needed.
@@ -1906,6 +2312,69 @@ Use a custom session directory with `--session`:
1906
2312
  wa connect 919634847671 --session /data/my-sessions
1907
2313
  ```
1908
2314
 
2315
+ ### CLI Pairing Code
2316
+
2317
+ If the number is already in use on a phone, or the verification SMS never arrives, link to the existing account instead. When it is not obvious which way you mean, `wa connect` asks:
2318
+
2319
+ ```
2320
+ how do you want to connect?
2321
+ 1) sms register this number as its own device
2322
+ 2) pairing code link to an existing WhatsApp account (8-digit code)
2323
+ sms or pairing code? [1/2] 2
2324
+ ```
2325
+
2326
+ The question is skipped when only one kind of session exists on disk, and when stdin is not a terminal. Force either one:
2327
+
2328
+ ```sh
2329
+ wa connect 919634847671 --sms
2330
+ wa connect 919634847671 --pair
2331
+ ```
2332
+
2333
+ Or go straight to linking:
2334
+
2335
+ ```sh
2336
+ wa pair 919634847671
2337
+ ```
2338
+
2339
+ ```
2340
+ linking +919634847671 to an existing WhatsApp account...
2341
+ ────────────────────────────────────────────────────────
2342
+ pairing code K7M2-QX4B
2343
+ ────────────────────────────────────────────────────────
2344
+ on the phone that owns +919634847671:
2345
+ WhatsApp → Settings → Linked Devices → Link a device
2346
+ → Link with phone number instead → enter the code above
2347
+
2348
+ the code is valid for a few minutes; waiting...
2349
+
2350
+ linked as 919634847671:7@s.whatsapp.net (112713111982325:7@lid)
2351
+ device slot 7 · primary is android
2352
+ finishing handshake...
2353
+ connected as +919634847671 (web / companion)
2354
+ history INITIAL_BOOTSTRAP chats=214 contacts=486
2355
+ wa +919634847671>
2356
+ ```
2357
+
2358
+ Once linked, plain `wa connect` reconnects without asking for a new code. To pick your own code, pass it as a second argument — exactly 8 characters:
2359
+
2360
+ ```sh
2361
+ wa pair 919634847671 MYCODE12
2362
+ ```
2363
+
2364
+ The debug prompt works the same in both modes. Answer `y` at startup, or pass `--debug`, to see every stanza of the pairing exchange:
2365
+
2366
+ ```sh
2367
+ wa pair 919634847671 --debug
2368
+ ```
2369
+
2370
+ From inside the shell:
2371
+
2372
+ ```sh
2373
+ wa> /pair 919634847671
2374
+ wa> /connect 919634847671 pair
2375
+ wa> /connect 919634847671 sms
2376
+ ```
2377
+
1909
2378
  ### Listen Mode
1910
2379
 
1911
2380
  Connect and print all incoming events to the terminal. The process stays alive indefinitely until you press Ctrl+C:
@@ -2623,8 +3092,19 @@ now run: /connect 919634847671
2623
3092
 
2624
3093
  ```sh
2625
3094
  # connect to a number (while already in the shell)
3095
+ # asks sms or pairing code when both are possible
2626
3096
  wa> /connect 919634847671
2627
3097
 
3098
+ # force one or the other
3099
+ wa> /connect 919634847671 sms
3100
+ wa> /connect 919634847671 pair
3101
+
3102
+ # link to an existing account by 8-digit pairing code
3103
+ wa> /pair 919634847671
3104
+
3105
+ # with a code you chose yourself (exactly 8 characters)
3106
+ wa> /pair 919634847671 MYCODE12
3107
+
2628
3108
  # disconnect
2629
3109
  wa> /disconnect
2630
3110
 
@@ -2739,7 +3219,8 @@ wa> /quit
2739
3219
  | `/reg code <phone> [method]` | Request verification code |
2740
3220
  | `/reg confirm <phone> <code>` | Complete registration |
2741
3221
  | **Connection** | |
2742
- | `/connect <phone>` | Connect to WhatsApp |
3222
+ | `/connect <phone> [sms\|pair]` | Connect to WhatsApp — asks which method when unset |
3223
+ | `/pair <phone> [code]` | Link to an existing account by 8-digit pairing code |
2743
3224
  | `/disconnect` | Disconnect current session |
2744
3225
  | `/reconnect` | Force reconnection |
2745
3226
  | `/session` | Show session info |