kalshi-trading-bot-cli 2.1.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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/assets/kalshi-flow-light.png +0 -0
  4. package/assets/screenshot.png +0 -0
  5. package/env.example +43 -0
  6. package/kalshi-flow-light.png +0 -0
  7. package/package.json +66 -0
  8. package/src/agent/agent.ts +249 -0
  9. package/src/agent/channels.ts +53 -0
  10. package/src/agent/index.ts +29 -0
  11. package/src/agent/prompts.ts +171 -0
  12. package/src/agent/run-context.ts +23 -0
  13. package/src/agent/scratchpad.ts +465 -0
  14. package/src/agent/token-counter.ts +33 -0
  15. package/src/agent/tool-executor.ts +166 -0
  16. package/src/agent/types.ts +221 -0
  17. package/src/audit/index.ts +25 -0
  18. package/src/audit/reader.ts +43 -0
  19. package/src/audit/trail.ts +29 -0
  20. package/src/audit/types.ts +133 -0
  21. package/src/backtest/discovery.ts +170 -0
  22. package/src/backtest/fetcher.ts +247 -0
  23. package/src/backtest/metrics.ts +165 -0
  24. package/src/backtest/renderer.ts +196 -0
  25. package/src/backtest/types.ts +45 -0
  26. package/src/cli.ts +943 -0
  27. package/src/commands/alerts.ts +48 -0
  28. package/src/commands/analyze.ts +662 -0
  29. package/src/commands/backtest.ts +276 -0
  30. package/src/commands/clear-cache.ts +24 -0
  31. package/src/commands/config.ts +107 -0
  32. package/src/commands/dispatch.ts +473 -0
  33. package/src/commands/edge.ts +62 -0
  34. package/src/commands/formatters.ts +339 -0
  35. package/src/commands/help.ts +263 -0
  36. package/src/commands/helpers.ts +48 -0
  37. package/src/commands/index.ts +287 -0
  38. package/src/commands/json.ts +43 -0
  39. package/src/commands/parse-args.ts +229 -0
  40. package/src/commands/portfolio.ts +236 -0
  41. package/src/commands/review.ts +176 -0
  42. package/src/commands/scan-formatters.ts +98 -0
  43. package/src/commands/scan.ts +38 -0
  44. package/src/commands/search-edge.ts +139 -0
  45. package/src/commands/status.ts +70 -0
  46. package/src/commands/themes.ts +117 -0
  47. package/src/commands/watch.ts +295 -0
  48. package/src/components/answer-box.ts +57 -0
  49. package/src/components/approval-prompt.ts +34 -0
  50. package/src/components/browse-list.ts +134 -0
  51. package/src/components/chat-log.ts +291 -0
  52. package/src/components/custom-editor.ts +18 -0
  53. package/src/components/debug-panel.ts +52 -0
  54. package/src/components/index.ts +17 -0
  55. package/src/components/intro.ts +92 -0
  56. package/src/components/select-list.ts +155 -0
  57. package/src/components/tool-event.ts +127 -0
  58. package/src/components/user-query.ts +18 -0
  59. package/src/components/working-indicator.ts +87 -0
  60. package/src/controllers/agent-runner.ts +283 -0
  61. package/src/controllers/browse.ts +1013 -0
  62. package/src/controllers/index.ts +7 -0
  63. package/src/controllers/input-history.ts +76 -0
  64. package/src/controllers/model-selection.ts +244 -0
  65. package/src/db/alerts.ts +77 -0
  66. package/src/db/edge.ts +105 -0
  67. package/src/db/event-index.ts +323 -0
  68. package/src/db/events.ts +41 -0
  69. package/src/db/index.ts +60 -0
  70. package/src/db/octagon-cache.ts +118 -0
  71. package/src/db/positions.ts +71 -0
  72. package/src/db/risk.ts +51 -0
  73. package/src/db/schema.ts +227 -0
  74. package/src/db/themes.ts +34 -0
  75. package/src/db/trades.ts +50 -0
  76. package/src/eval/brier.ts +90 -0
  77. package/src/eval/index.ts +4 -0
  78. package/src/eval/performance.ts +87 -0
  79. package/src/gateway/access-control.ts +253 -0
  80. package/src/gateway/agent-runner.ts +75 -0
  81. package/src/gateway/alerts/formatter.ts +90 -0
  82. package/src/gateway/alerts/index.ts +4 -0
  83. package/src/gateway/alerts/router.ts +32 -0
  84. package/src/gateway/alerts/terminal.ts +16 -0
  85. package/src/gateway/alerts/types.ts +13 -0
  86. package/src/gateway/channels/index.ts +9 -0
  87. package/src/gateway/channels/manager.ts +153 -0
  88. package/src/gateway/channels/types.ts +48 -0
  89. package/src/gateway/channels/whatsapp/README.md +234 -0
  90. package/src/gateway/channels/whatsapp/auth-store.ts +140 -0
  91. package/src/gateway/channels/whatsapp/dedupe.ts +60 -0
  92. package/src/gateway/channels/whatsapp/error.ts +122 -0
  93. package/src/gateway/channels/whatsapp/inbound.ts +326 -0
  94. package/src/gateway/channels/whatsapp/index.ts +5 -0
  95. package/src/gateway/channels/whatsapp/lid.ts +56 -0
  96. package/src/gateway/channels/whatsapp/logger.ts +25 -0
  97. package/src/gateway/channels/whatsapp/login.ts +94 -0
  98. package/src/gateway/channels/whatsapp/outbound.ts +119 -0
  99. package/src/gateway/channels/whatsapp/plugin.ts +54 -0
  100. package/src/gateway/channels/whatsapp/reconnect.ts +40 -0
  101. package/src/gateway/channels/whatsapp/runtime.ts +122 -0
  102. package/src/gateway/channels/whatsapp/session.ts +89 -0
  103. package/src/gateway/channels/whatsapp/types.ts +32 -0
  104. package/src/gateway/commands/handler.ts +64 -0
  105. package/src/gateway/commands/index.ts +7 -0
  106. package/src/gateway/commands/parser.ts +29 -0
  107. package/src/gateway/commands/wa-formatters.ts +92 -0
  108. package/src/gateway/config.ts +244 -0
  109. package/src/gateway/extension-points.ts +17 -0
  110. package/src/gateway/gateway.ts +301 -0
  111. package/src/gateway/group/history-buffer.ts +75 -0
  112. package/src/gateway/group/index.ts +8 -0
  113. package/src/gateway/group/member-tracker.ts +60 -0
  114. package/src/gateway/group/mention-detection.ts +42 -0
  115. package/src/gateway/heartbeat/index.ts +8 -0
  116. package/src/gateway/heartbeat/prompt.ts +73 -0
  117. package/src/gateway/heartbeat/runner.ts +200 -0
  118. package/src/gateway/heartbeat/suppression.ts +74 -0
  119. package/src/gateway/index.ts +138 -0
  120. package/src/gateway/routing/resolve-route.ts +119 -0
  121. package/src/gateway/sessions/store.ts +65 -0
  122. package/src/gateway/types.ts +11 -0
  123. package/src/gateway/utils.ts +82 -0
  124. package/src/index.tsx +30 -0
  125. package/src/model/llm.ts +247 -0
  126. package/src/providers.ts +94 -0
  127. package/src/risk/circuit-breaker.ts +113 -0
  128. package/src/risk/correlation.ts +40 -0
  129. package/src/risk/gate.ts +125 -0
  130. package/src/risk/index.ts +10 -0
  131. package/src/risk/kelly.ts +230 -0
  132. package/src/scan/alerter.ts +64 -0
  133. package/src/scan/edge-computer.ts +164 -0
  134. package/src/scan/invoker.ts +199 -0
  135. package/src/scan/loop.ts +184 -0
  136. package/src/scan/octagon-client.ts +627 -0
  137. package/src/scan/octagon-events-api.ts +105 -0
  138. package/src/scan/octagon-prefetch.ts +172 -0
  139. package/src/scan/theme-resolver.ts +179 -0
  140. package/src/scan/types.ts +62 -0
  141. package/src/scan/watchdog.ts +126 -0
  142. package/src/setup/wizard.ts +659 -0
  143. package/src/theme.ts +67 -0
  144. package/src/tools/fetch/cache.ts +95 -0
  145. package/src/tools/fetch/external-content.ts +200 -0
  146. package/src/tools/fetch/index.ts +1 -0
  147. package/src/tools/fetch/web-fetch-utils.ts +122 -0
  148. package/src/tools/fetch/web-fetch.ts +419 -0
  149. package/src/tools/index.ts +10 -0
  150. package/src/tools/kalshi/api.ts +251 -0
  151. package/src/tools/kalshi/dlq.ts +35 -0
  152. package/src/tools/kalshi/events.ts +84 -0
  153. package/src/tools/kalshi/exchange.ts +24 -0
  154. package/src/tools/kalshi/historical.ts +89 -0
  155. package/src/tools/kalshi/index.ts +11 -0
  156. package/src/tools/kalshi/kalshi-search.ts +437 -0
  157. package/src/tools/kalshi/kalshi-trade.ts +102 -0
  158. package/src/tools/kalshi/markets.ts +76 -0
  159. package/src/tools/kalshi/portfolio.ts +100 -0
  160. package/src/tools/kalshi/search-index.ts +198 -0
  161. package/src/tools/kalshi/series.ts +16 -0
  162. package/src/tools/kalshi/trading.ts +115 -0
  163. package/src/tools/kalshi/types.ts +199 -0
  164. package/src/tools/registry.ts +160 -0
  165. package/src/tools/search/index.ts +25 -0
  166. package/src/tools/search/tavily.ts +35 -0
  167. package/src/tools/types.ts +53 -0
  168. package/src/tools/v2/edge-query.ts +135 -0
  169. package/src/tools/v2/octagon-report.ts +112 -0
  170. package/src/tools/v2/portfolio-query.ts +79 -0
  171. package/src/tools/v2/portfolio-review.ts +59 -0
  172. package/src/tools/v2/risk-status.ts +94 -0
  173. package/src/tools/v2/scan.ts +78 -0
  174. package/src/types/qrcode-terminal.d.ts +7 -0
  175. package/src/types/whiskeysockets-baileys.d.ts +41 -0
  176. package/src/types.ts +22 -0
  177. package/src/utils/ai-message.ts +26 -0
  178. package/src/utils/bot-config.ts +219 -0
  179. package/src/utils/cache.ts +195 -0
  180. package/src/utils/config.ts +113 -0
  181. package/src/utils/env.ts +111 -0
  182. package/src/utils/errors.ts +313 -0
  183. package/src/utils/history-context.ts +32 -0
  184. package/src/utils/in-memory-chat-history.ts +268 -0
  185. package/src/utils/index.ts +28 -0
  186. package/src/utils/input-key-handlers.ts +64 -0
  187. package/src/utils/logger.ts +67 -0
  188. package/src/utils/long-term-chat-history.ts +138 -0
  189. package/src/utils/markdown-table.ts +227 -0
  190. package/src/utils/model.ts +70 -0
  191. package/src/utils/ollama.ts +37 -0
  192. package/src/utils/paths.ts +12 -0
  193. package/src/utils/progress-channel.ts +84 -0
  194. package/src/utils/telemetry.ts +103 -0
  195. package/src/utils/text-navigation.ts +81 -0
  196. package/src/utils/thinking-verbs.ts +18 -0
  197. package/src/utils/tokens.ts +36 -0
  198. package/src/utils/tool-description.ts +61 -0
@@ -0,0 +1,234 @@
1
+ # WhatsApp Gateway
2
+
3
+ Chat with the bot through WhatsApp by linking your phone to the gateway. Messages you send to yourself (self-chat) are processed by the bot and responses are sent back to the same chat.
4
+
5
+ ## Table of Contents
6
+
7
+ - [✅ Prerequisites](#-prerequisites)
8
+ - [🔗 How to Link WhatsApp](#-how-to-link-whatsapp)
9
+ - [🚀 How to Run](#-how-to-run)
10
+ - [💬 How to Chat](#-how-to-chat)
11
+ - [⚙️ Configuration](#️-configuration)
12
+ - [👥 Group Chat](#-group-chat)
13
+ - [🔄 How to Relink](#-how-to-relink)
14
+ - [🐛 Troubleshooting](#-troubleshooting)
15
+ - [🔧 Full Reset](#-full-reset)
16
+
17
+ ## ✅ Prerequisites
18
+
19
+ - The bot installed and working (see main [README](../../../../README.md))
20
+ - WhatsApp installed on your phone
21
+ - Your phone connected to the internet
22
+
23
+ ## 🔗 How to Link WhatsApp
24
+
25
+ Link your WhatsApp account by scanning a QR code:
26
+
27
+ ```bash
28
+ bun run gateway:login
29
+ ```
30
+
31
+ This will:
32
+ 1. Display a QR code in your terminal
33
+ 2. Open WhatsApp on your phone
34
+ 3. Go to **Settings > Linked Devices > Link a Device**
35
+ 4. Scan the QR code
36
+
37
+ After linking, you'll be asked how you want to use the bot:
38
+
39
+ ### Option 1: Self-chat (personal phone)
40
+
41
+ Use your own WhatsApp to talk to the bot by messaging yourself. The linked phone number is added to `allowFrom` and self-chat mode is activated automatically.
42
+
43
+ ### Option 2: Dedicated bot phone
44
+
45
+ If the bot has its own phone number (e.g. a separate SIM), choose this option and enter the phone number(s) allowed to message it. The gateway will be configured with `dmPolicy: "allowlist"` so other people can DM the bot.
46
+
47
+ Credentials are saved to `.kalshi-trading-bot-cli/credentials/whatsapp/default/`.
48
+
49
+ ## 🚀 How to Run
50
+
51
+ Start the gateway to begin receiving messages:
52
+
53
+ ```bash
54
+ bun run gateway
55
+ ```
56
+
57
+ You should see:
58
+ ```
59
+ [whatsapp] Connected
60
+ Gateway running. Press Ctrl+C to stop.
61
+ ```
62
+
63
+ The gateway will now listen for incoming WhatsApp messages and respond using the bot.
64
+
65
+ ## 💬 How to Chat
66
+
67
+ Once the gateway is running:
68
+
69
+ 1. Open WhatsApp on your phone
70
+ 2. Go to your own chat (message yourself)
71
+ 3. Send a message like "What is Apple's revenue?"
72
+ 4. You'll see a typing indicator while the bot processes
73
+ 5. The bot's response will appear in the chat
74
+
75
+ **Example conversation:**
76
+ ```
77
+ You: What was NVIDIA's revenue in 2024?
78
+ Bot: NVIDIA's revenue for fiscal year 2024 was $60.9 billion...
79
+ ```
80
+
81
+ ## ⚙️ Configuration
82
+
83
+ The gateway configuration is stored at `.kalshi-trading-bot-cli/gateway.json`. It's auto-created when you run `gateway:login`.
84
+
85
+ **Self-chat configuration** (personal phone, message yourself):
86
+ ```json
87
+ {
88
+ "gateway": {
89
+ "accountId": "default",
90
+ "logLevel": "info"
91
+ },
92
+ "channels": {
93
+ "whatsapp": {
94
+ "enabled": true,
95
+ "allowFrom": ["+1234567890"]
96
+ }
97
+ },
98
+ "bindings": []
99
+ }
100
+ ```
101
+
102
+ **Bot phone configuration** (dedicated bot phone, others message it):
103
+ ```json
104
+ {
105
+ "gateway": {
106
+ "accountId": "default",
107
+ "logLevel": "info"
108
+ },
109
+ "channels": {
110
+ "whatsapp": {
111
+ "enabled": true,
112
+ "accounts": {
113
+ "default": {
114
+ "dmPolicy": "allowlist",
115
+ "allowFrom": ["+1555YOURNUM"],
116
+ "groupPolicy": "disabled",
117
+ "groupAllowFrom": []
118
+ }
119
+ },
120
+ "allowFrom": ["+1555YOURNUM"]
121
+ }
122
+ },
123
+ "bindings": []
124
+ }
125
+ ```
126
+
127
+ **Key settings:**
128
+
129
+ | Setting | Description |
130
+ |---------|-------------|
131
+ | `channels.whatsapp.allowFrom` | Phone numbers allowed to message the bot (E.164 format) |
132
+ | `channels.whatsapp.enabled` | Enable/disable the WhatsApp channel |
133
+ | `accounts.<id>.dmPolicy` | DM access policy: `pairing` (default), `allowlist`, `open`, or `disabled` |
134
+ | `accounts.<id>.allowFrom` | Per-account allowed senders (overrides top-level `allowFrom`) |
135
+ | `gateway.logLevel` | Log verbosity: `silent`, `error`, `info`, `debug` |
136
+
137
+ ## 👥 Group Chat
138
+
139
+ The bot can participate in WhatsApp group chats, responding only when @-mentioned.
140
+
141
+ ### Setup
142
+
143
+ Add group policy to your account in `.kalshi-trading-bot-cli/gateway.json`:
144
+
145
+ ```jsonc
146
+ {
147
+ "channels": {
148
+ "whatsapp": {
149
+ "enabled": true,
150
+ "accounts": {
151
+ "default": {
152
+ "groupPolicy": "open", // "open", "allowlist", or "disabled"
153
+ "groupAllowFrom": ["*"] // no need to list individual group members
154
+ }
155
+ },
156
+ "allowFrom": ["+1234567890"] // existing DM allowlist (unrelated to groups)
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ | Setting | Description |
163
+ |---------|-------------|
164
+ | `groupPolicy` | `"open"` (any group), `"allowlist"` (restricted), or `"disabled"` (default) |
165
+ | `groupAllowFrom` | Which groups the bot can participate in (`["*"]` for any) |
166
+
167
+ You don't need to list individual group members — when `groupPolicy` is `"open"`, the bot will respond to @-mentions from anyone in any group it's added to.
168
+
169
+ ### Usage
170
+
171
+ 1. Add the bot's WhatsApp number to a group
172
+ 2. Send messages normally — the bot stays silent
173
+ 3. @-mention the bot (tap `@` and select from the picker) to get a response
174
+ 4. The bot sees recent group messages for context, so it can follow the conversation
175
+
176
+ **Note:** You must use WhatsApp's @-mention picker (tap `@` then select the contact) — typing a phone number manually won't trigger a response.
177
+
178
+ ## 🔄 How to Relink
179
+
180
+ If you need to relink your WhatsApp (e.g., after logging out or switching phones):
181
+
182
+ 1. Stop the gateway (Ctrl+C)
183
+ 2. Delete the credentials:
184
+ ```bash
185
+ rm -rf .kalshi-trading-bot-cli/credentials/whatsapp/default
186
+ ```
187
+ 3. Run login again:
188
+ ```bash
189
+ bun run gateway:login
190
+ ```
191
+ 4. Scan the new QR code
192
+
193
+ ## 🐛 Troubleshooting
194
+
195
+ **Gateway shows "Disconnected":**
196
+ - Check your internet connection
197
+ - Try relinking (see above)
198
+
199
+ **Messages not being received:**
200
+ - Verify your phone number is in `allowFrom` in `.kalshi-trading-bot-cli/gateway.json`
201
+ - Make sure you're messaging yourself (self-chat mode)
202
+
203
+ **Debug logs:**
204
+ - Check `.kalshi-trading-bot-cli/gateway-debug.log` for detailed logs
205
+
206
+ ## 🔧 Full Reset
207
+
208
+ If you're experiencing persistent issues (connection problems, encryption errors, messages not sending), perform a full reset:
209
+
210
+ 1. **Stop the gateway** (Ctrl+C if running)
211
+
212
+ 2. **Unlink from WhatsApp:**
213
+ - Open WhatsApp on your phone
214
+ - Go to **Settings > Linked Devices**
215
+ - Tap on the device and select **Log Out**
216
+
217
+ 3. **Clear all local data:**
218
+ ```bash
219
+ rm -rf .kalshi-trading-bot-cli/credentials/whatsapp/default
220
+ rm -rf .kalshi-trading-bot-cli/gateway.json
221
+ rm -rf .kalshi-trading-bot-cli/gateway-debug.log
222
+ ```
223
+
224
+ 4. **Relink and start fresh:**
225
+ ```bash
226
+ bun run gateway:login
227
+ ```
228
+
229
+ 5. **Scan the QR code** and start the gateway:
230
+ ```bash
231
+ bun run gateway
232
+ ```
233
+
234
+ This clears all cached credentials and encryption sessions, which resolves most connection issues.
@@ -0,0 +1,140 @@
1
+ import { existsSync, statSync, readFileSync, copyFileSync, rmSync } from 'node:fs';
2
+ import { rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ export function resolveCredsPath(authDir: string): string {
6
+ return join(authDir, 'creds.json');
7
+ }
8
+
9
+ export function resolveCredsBackupPath(authDir: string): string {
10
+ return join(authDir, 'creds.json.bak');
11
+ }
12
+
13
+ export function hasCredsSync(authDir: string): boolean {
14
+ try {
15
+ const stats = statSync(resolveCredsPath(authDir));
16
+ return stats.isFile() && stats.size > 1;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function readCredsJsonRaw(filePath: string): string | null {
23
+ try {
24
+ if (!existsSync(filePath)) {
25
+ return null;
26
+ }
27
+ const stats = statSync(filePath);
28
+ if (!stats.isFile() || stats.size <= 1) {
29
+ return null;
30
+ }
31
+ return readFileSync(filePath, 'utf-8');
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * If creds.json is missing or corrupted, restore from backup if available.
39
+ */
40
+ export function maybeRestoreCredsFromBackup(authDir: string): void {
41
+ try {
42
+ const credsPath = resolveCredsPath(authDir);
43
+ const backupPath = resolveCredsBackupPath(authDir);
44
+ const raw = readCredsJsonRaw(credsPath);
45
+ if (raw) {
46
+ // Validate that creds.json is parseable
47
+ JSON.parse(raw);
48
+ return;
49
+ }
50
+
51
+ const backupRaw = readCredsJsonRaw(backupPath);
52
+ if (!backupRaw) {
53
+ return;
54
+ }
55
+
56
+ // Ensure backup is parseable before restoring
57
+ JSON.parse(backupRaw);
58
+ copyFileSync(backupPath, credsPath);
59
+ console.log('Restored WhatsApp creds.json from backup');
60
+ } catch {
61
+ // ignore
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Back up creds.json before saving new credentials.
67
+ */
68
+ export function backupCredsBeforeSave(authDir: string): void {
69
+ try {
70
+ const credsPath = resolveCredsPath(authDir);
71
+ const backupPath = resolveCredsBackupPath(authDir);
72
+ const raw = readCredsJsonRaw(credsPath);
73
+ if (raw) {
74
+ // Validate before backing up
75
+ JSON.parse(raw);
76
+ copyFileSync(credsPath, backupPath);
77
+ }
78
+ } catch {
79
+ // ignore backup failures
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Check if valid WhatsApp credentials exist.
85
+ */
86
+ export async function authExists(authDir: string): Promise<boolean> {
87
+ maybeRestoreCredsFromBackup(authDir);
88
+ const credsPath = resolveCredsPath(authDir);
89
+ try {
90
+ const stats = statSync(credsPath);
91
+ if (!stats.isFile() || stats.size <= 1) {
92
+ return false;
93
+ }
94
+ const raw = readFileSync(credsPath, 'utf-8');
95
+ JSON.parse(raw);
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Extract the linked phone number from stored credentials.
104
+ * Returns E.164 format (e.g., "+1234567890") and raw JID.
105
+ */
106
+ export function readSelfId(authDir: string): { e164: string | null; jid: string | null } {
107
+ try {
108
+ const credsPath = resolveCredsPath(authDir);
109
+ if (!existsSync(credsPath)) {
110
+ return { e164: null, jid: null };
111
+ }
112
+ const raw = readFileSync(credsPath, 'utf-8');
113
+ const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
114
+ const jid = parsed?.me?.id ?? null;
115
+ // JID format: "1234567890:123@s.whatsapp.net" -> "+1234567890"
116
+ const e164 = jid ? jidToE164(jid) : null;
117
+ return { e164, jid };
118
+ } catch {
119
+ return { e164: null, jid: null };
120
+ }
121
+ }
122
+
123
+ function jidToE164(jid: string): string | null {
124
+ const match = jid.match(/^(\d+):/);
125
+ return match ? `+${match[1]}` : null;
126
+ }
127
+
128
+ /**
129
+ * Clear WhatsApp credentials (logout).
130
+ */
131
+ export async function logout(authDir: string): Promise<boolean> {
132
+ const exists = await authExists(authDir);
133
+ if (!exists) {
134
+ console.log('No WhatsApp session found; nothing to delete.');
135
+ return false;
136
+ }
137
+ await rm(authDir, { recursive: true, force: true });
138
+ console.log('Cleared WhatsApp credentials.');
139
+ return true;
140
+ }
@@ -0,0 +1,60 @@
1
+ const RECENT_MESSAGE_TTL_MS = 20 * 60_000; // 20 minutes
2
+ const RECENT_MESSAGE_MAX = 5000;
3
+
4
+ type CacheEntry = {
5
+ key: string;
6
+ timestamp: number;
7
+ };
8
+
9
+ const cache = new Map<string, CacheEntry>();
10
+ const insertionOrder: string[] = [];
11
+
12
+ function pruneExpired(): void {
13
+ const now = Date.now();
14
+ const cutoff = now - RECENT_MESSAGE_TTL_MS;
15
+
16
+ // Remove expired entries from the front of insertion order
17
+ while (insertionOrder.length > 0) {
18
+ const oldestKey = insertionOrder[0];
19
+ const entry = cache.get(oldestKey);
20
+ if (entry && entry.timestamp < cutoff) {
21
+ cache.delete(oldestKey);
22
+ insertionOrder.shift();
23
+ } else {
24
+ break;
25
+ }
26
+ }
27
+
28
+ // Enforce max size
29
+ while (cache.size > RECENT_MESSAGE_MAX && insertionOrder.length > 0) {
30
+ const oldestKey = insertionOrder.shift();
31
+ if (oldestKey) {
32
+ cache.delete(oldestKey);
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if a message ID was recently seen.
39
+ * Returns true if it's a duplicate (already seen), false if new.
40
+ * Automatically adds the key to the cache if not seen before.
41
+ */
42
+ export function isRecentInboundMessage(key: string): boolean {
43
+ pruneExpired();
44
+
45
+ if (cache.has(key)) {
46
+ return true;
47
+ }
48
+
49
+ cache.set(key, { key, timestamp: Date.now() });
50
+ insertionOrder.push(key);
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Clear the deduplication cache (useful for testing).
56
+ */
57
+ export function resetInboundDedupe(): void {
58
+ cache.clear();
59
+ insertionOrder.length = 0;
60
+ }
@@ -0,0 +1,122 @@
1
+ import { getStatusCode } from './session.js';
2
+
3
+ function safeStringify(value: unknown, limit = 800): string {
4
+ try {
5
+ const seen = new WeakSet();
6
+ const raw = JSON.stringify(
7
+ value,
8
+ (_key, v) => {
9
+ if (typeof v === 'bigint') {
10
+ return v.toString();
11
+ }
12
+ if (typeof v === 'function') {
13
+ const maybeName = (v as { name?: unknown }).name;
14
+ const name =
15
+ typeof maybeName === 'string' && maybeName.length > 0 ? maybeName : 'anonymous';
16
+ return `[Function ${name}]`;
17
+ }
18
+ if (typeof v === 'object' && v) {
19
+ if (seen.has(v)) {
20
+ return '[Circular]';
21
+ }
22
+ seen.add(v);
23
+ }
24
+ return v;
25
+ },
26
+ 2,
27
+ );
28
+ if (!raw) {
29
+ return String(value);
30
+ }
31
+ return raw.length > limit ? `${raw.slice(0, limit)}…` : raw;
32
+ } catch {
33
+ return String(value);
34
+ }
35
+ }
36
+
37
+ function extractBoomDetails(err: unknown): {
38
+ statusCode?: number;
39
+ error?: string;
40
+ message?: string;
41
+ } | null {
42
+ if (!err || typeof err !== 'object') {
43
+ return null;
44
+ }
45
+ const output = (err as { output?: unknown })?.output as
46
+ | { statusCode?: unknown; payload?: unknown }
47
+ | undefined;
48
+ if (!output || typeof output !== 'object') {
49
+ return null;
50
+ }
51
+ const payload = (output as { payload?: unknown }).payload as
52
+ | { error?: unknown; message?: unknown; statusCode?: unknown }
53
+ | undefined;
54
+ const statusCode =
55
+ typeof (output as { statusCode?: unknown }).statusCode === 'number'
56
+ ? ((output as { statusCode?: unknown }).statusCode as number)
57
+ : typeof payload?.statusCode === 'number'
58
+ ? payload.statusCode
59
+ : undefined;
60
+ const error = typeof payload?.error === 'string' ? payload.error : undefined;
61
+ const message = typeof payload?.message === 'string' ? payload.message : undefined;
62
+ if (!statusCode && !error && !message) {
63
+ return null;
64
+ }
65
+ return { statusCode, error, message };
66
+ }
67
+
68
+ /**
69
+ * Format Baileys errors into human-readable messages.
70
+ * Handles nested Boom error structures.
71
+ */
72
+ export function formatError(err: unknown): string {
73
+ if (err instanceof Error) {
74
+ return err.message;
75
+ }
76
+ if (typeof err === 'string') {
77
+ return err;
78
+ }
79
+ if (!err || typeof err !== 'object') {
80
+ return String(err);
81
+ }
82
+
83
+ // Baileys frequently wraps errors under `error` with a Boom-like shape.
84
+ const boom =
85
+ extractBoomDetails(err) ??
86
+ extractBoomDetails((err as { error?: unknown })?.error) ??
87
+ extractBoomDetails((err as { lastDisconnect?: { error?: unknown } })?.lastDisconnect?.error);
88
+
89
+ const status = boom?.statusCode ?? getStatusCode(err);
90
+ const code = (err as { code?: unknown })?.code;
91
+ const codeText = typeof code === 'string' || typeof code === 'number' ? String(code) : undefined;
92
+
93
+ const messageCandidates = [
94
+ boom?.message,
95
+ typeof (err as { message?: unknown })?.message === 'string'
96
+ ? ((err as { message?: unknown }).message as string)
97
+ : undefined,
98
+ typeof (err as { error?: { message?: unknown } })?.error?.message === 'string'
99
+ ? ((err as { error?: { message?: unknown } }).error?.message as string)
100
+ : undefined,
101
+ ].filter((v): v is string => Boolean(v && v.trim().length > 0));
102
+ const message = messageCandidates[0];
103
+
104
+ const pieces: string[] = [];
105
+ if (typeof status === 'number') {
106
+ pieces.push(`status=${status}`);
107
+ }
108
+ if (boom?.error) {
109
+ pieces.push(boom.error);
110
+ }
111
+ if (message) {
112
+ pieces.push(message);
113
+ }
114
+ if (codeText) {
115
+ pieces.push(`code=${codeText}`);
116
+ }
117
+
118
+ if (pieces.length > 0) {
119
+ return pieces.join(' ');
120
+ }
121
+ return safeStringify(err);
122
+ }