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.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/assets/kalshi-flow-light.png +0 -0
- package/assets/screenshot.png +0 -0
- package/env.example +43 -0
- package/kalshi-flow-light.png +0 -0
- package/package.json +66 -0
- package/src/agent/agent.ts +249 -0
- package/src/agent/channels.ts +53 -0
- package/src/agent/index.ts +29 -0
- package/src/agent/prompts.ts +171 -0
- package/src/agent/run-context.ts +23 -0
- package/src/agent/scratchpad.ts +465 -0
- package/src/agent/token-counter.ts +33 -0
- package/src/agent/tool-executor.ts +166 -0
- package/src/agent/types.ts +221 -0
- package/src/audit/index.ts +25 -0
- package/src/audit/reader.ts +43 -0
- package/src/audit/trail.ts +29 -0
- package/src/audit/types.ts +133 -0
- package/src/backtest/discovery.ts +170 -0
- package/src/backtest/fetcher.ts +247 -0
- package/src/backtest/metrics.ts +165 -0
- package/src/backtest/renderer.ts +196 -0
- package/src/backtest/types.ts +45 -0
- package/src/cli.ts +943 -0
- package/src/commands/alerts.ts +48 -0
- package/src/commands/analyze.ts +662 -0
- package/src/commands/backtest.ts +276 -0
- package/src/commands/clear-cache.ts +24 -0
- package/src/commands/config.ts +107 -0
- package/src/commands/dispatch.ts +473 -0
- package/src/commands/edge.ts +62 -0
- package/src/commands/formatters.ts +339 -0
- package/src/commands/help.ts +263 -0
- package/src/commands/helpers.ts +48 -0
- package/src/commands/index.ts +287 -0
- package/src/commands/json.ts +43 -0
- package/src/commands/parse-args.ts +229 -0
- package/src/commands/portfolio.ts +236 -0
- package/src/commands/review.ts +176 -0
- package/src/commands/scan-formatters.ts +98 -0
- package/src/commands/scan.ts +38 -0
- package/src/commands/search-edge.ts +139 -0
- package/src/commands/status.ts +70 -0
- package/src/commands/themes.ts +117 -0
- package/src/commands/watch.ts +295 -0
- package/src/components/answer-box.ts +57 -0
- package/src/components/approval-prompt.ts +34 -0
- package/src/components/browse-list.ts +134 -0
- package/src/components/chat-log.ts +291 -0
- package/src/components/custom-editor.ts +18 -0
- package/src/components/debug-panel.ts +52 -0
- package/src/components/index.ts +17 -0
- package/src/components/intro.ts +92 -0
- package/src/components/select-list.ts +155 -0
- package/src/components/tool-event.ts +127 -0
- package/src/components/user-query.ts +18 -0
- package/src/components/working-indicator.ts +87 -0
- package/src/controllers/agent-runner.ts +283 -0
- package/src/controllers/browse.ts +1013 -0
- package/src/controllers/index.ts +7 -0
- package/src/controllers/input-history.ts +76 -0
- package/src/controllers/model-selection.ts +244 -0
- package/src/db/alerts.ts +77 -0
- package/src/db/edge.ts +105 -0
- package/src/db/event-index.ts +323 -0
- package/src/db/events.ts +41 -0
- package/src/db/index.ts +60 -0
- package/src/db/octagon-cache.ts +118 -0
- package/src/db/positions.ts +71 -0
- package/src/db/risk.ts +51 -0
- package/src/db/schema.ts +227 -0
- package/src/db/themes.ts +34 -0
- package/src/db/trades.ts +50 -0
- package/src/eval/brier.ts +90 -0
- package/src/eval/index.ts +4 -0
- package/src/eval/performance.ts +87 -0
- package/src/gateway/access-control.ts +253 -0
- package/src/gateway/agent-runner.ts +75 -0
- package/src/gateway/alerts/formatter.ts +90 -0
- package/src/gateway/alerts/index.ts +4 -0
- package/src/gateway/alerts/router.ts +32 -0
- package/src/gateway/alerts/terminal.ts +16 -0
- package/src/gateway/alerts/types.ts +13 -0
- package/src/gateway/channels/index.ts +9 -0
- package/src/gateway/channels/manager.ts +153 -0
- package/src/gateway/channels/types.ts +48 -0
- package/src/gateway/channels/whatsapp/README.md +234 -0
- package/src/gateway/channels/whatsapp/auth-store.ts +140 -0
- package/src/gateway/channels/whatsapp/dedupe.ts +60 -0
- package/src/gateway/channels/whatsapp/error.ts +122 -0
- package/src/gateway/channels/whatsapp/inbound.ts +326 -0
- package/src/gateway/channels/whatsapp/index.ts +5 -0
- package/src/gateway/channels/whatsapp/lid.ts +56 -0
- package/src/gateway/channels/whatsapp/logger.ts +25 -0
- package/src/gateway/channels/whatsapp/login.ts +94 -0
- package/src/gateway/channels/whatsapp/outbound.ts +119 -0
- package/src/gateway/channels/whatsapp/plugin.ts +54 -0
- package/src/gateway/channels/whatsapp/reconnect.ts +40 -0
- package/src/gateway/channels/whatsapp/runtime.ts +122 -0
- package/src/gateway/channels/whatsapp/session.ts +89 -0
- package/src/gateway/channels/whatsapp/types.ts +32 -0
- package/src/gateway/commands/handler.ts +64 -0
- package/src/gateway/commands/index.ts +7 -0
- package/src/gateway/commands/parser.ts +29 -0
- package/src/gateway/commands/wa-formatters.ts +92 -0
- package/src/gateway/config.ts +244 -0
- package/src/gateway/extension-points.ts +17 -0
- package/src/gateway/gateway.ts +301 -0
- package/src/gateway/group/history-buffer.ts +75 -0
- package/src/gateway/group/index.ts +8 -0
- package/src/gateway/group/member-tracker.ts +60 -0
- package/src/gateway/group/mention-detection.ts +42 -0
- package/src/gateway/heartbeat/index.ts +8 -0
- package/src/gateway/heartbeat/prompt.ts +73 -0
- package/src/gateway/heartbeat/runner.ts +200 -0
- package/src/gateway/heartbeat/suppression.ts +74 -0
- package/src/gateway/index.ts +138 -0
- package/src/gateway/routing/resolve-route.ts +119 -0
- package/src/gateway/sessions/store.ts +65 -0
- package/src/gateway/types.ts +11 -0
- package/src/gateway/utils.ts +82 -0
- package/src/index.tsx +30 -0
- package/src/model/llm.ts +247 -0
- package/src/providers.ts +94 -0
- package/src/risk/circuit-breaker.ts +113 -0
- package/src/risk/correlation.ts +40 -0
- package/src/risk/gate.ts +125 -0
- package/src/risk/index.ts +10 -0
- package/src/risk/kelly.ts +230 -0
- package/src/scan/alerter.ts +64 -0
- package/src/scan/edge-computer.ts +164 -0
- package/src/scan/invoker.ts +199 -0
- package/src/scan/loop.ts +184 -0
- package/src/scan/octagon-client.ts +627 -0
- package/src/scan/octagon-events-api.ts +105 -0
- package/src/scan/octagon-prefetch.ts +172 -0
- package/src/scan/theme-resolver.ts +179 -0
- package/src/scan/types.ts +62 -0
- package/src/scan/watchdog.ts +126 -0
- package/src/setup/wizard.ts +659 -0
- package/src/theme.ts +67 -0
- package/src/tools/fetch/cache.ts +95 -0
- package/src/tools/fetch/external-content.ts +200 -0
- package/src/tools/fetch/index.ts +1 -0
- package/src/tools/fetch/web-fetch-utils.ts +122 -0
- package/src/tools/fetch/web-fetch.ts +419 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/kalshi/api.ts +251 -0
- package/src/tools/kalshi/dlq.ts +35 -0
- package/src/tools/kalshi/events.ts +84 -0
- package/src/tools/kalshi/exchange.ts +24 -0
- package/src/tools/kalshi/historical.ts +89 -0
- package/src/tools/kalshi/index.ts +11 -0
- package/src/tools/kalshi/kalshi-search.ts +437 -0
- package/src/tools/kalshi/kalshi-trade.ts +102 -0
- package/src/tools/kalshi/markets.ts +76 -0
- package/src/tools/kalshi/portfolio.ts +100 -0
- package/src/tools/kalshi/search-index.ts +198 -0
- package/src/tools/kalshi/series.ts +16 -0
- package/src/tools/kalshi/trading.ts +115 -0
- package/src/tools/kalshi/types.ts +199 -0
- package/src/tools/registry.ts +160 -0
- package/src/tools/search/index.ts +25 -0
- package/src/tools/search/tavily.ts +35 -0
- package/src/tools/types.ts +53 -0
- package/src/tools/v2/edge-query.ts +135 -0
- package/src/tools/v2/octagon-report.ts +112 -0
- package/src/tools/v2/portfolio-query.ts +79 -0
- package/src/tools/v2/portfolio-review.ts +59 -0
- package/src/tools/v2/risk-status.ts +94 -0
- package/src/tools/v2/scan.ts +78 -0
- package/src/types/qrcode-terminal.d.ts +7 -0
- package/src/types/whiskeysockets-baileys.d.ts +41 -0
- package/src/types.ts +22 -0
- package/src/utils/ai-message.ts +26 -0
- package/src/utils/bot-config.ts +219 -0
- package/src/utils/cache.ts +195 -0
- package/src/utils/config.ts +113 -0
- package/src/utils/env.ts +111 -0
- package/src/utils/errors.ts +313 -0
- package/src/utils/history-context.ts +32 -0
- package/src/utils/in-memory-chat-history.ts +268 -0
- package/src/utils/index.ts +28 -0
- package/src/utils/input-key-handlers.ts +64 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/long-term-chat-history.ts +138 -0
- package/src/utils/markdown-table.ts +227 -0
- package/src/utils/model.ts +70 -0
- package/src/utils/ollama.ts +37 -0
- package/src/utils/paths.ts +12 -0
- package/src/utils/progress-channel.ts +84 -0
- package/src/utils/telemetry.ts +103 -0
- package/src/utils/text-navigation.ts +81 -0
- package/src/utils/thinking-verbs.ts +18 -0
- package/src/utils/tokens.ts +36 -0
- 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
|
+
}
|