hyperclaw 4.0.0 โ 4.0.1
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 +49 -17
- package/dist/api-keys-guide-Bzig1R5W.js +149 -0
- package/dist/connector-DRv1ahC_.js +343 -0
- package/dist/delivery-B-SJqXLn.js +95 -0
- package/dist/delivery-VgFeuu2J.js +5 -0
- package/dist/hyperclawbot-DfMGowZC.js +480 -0
- package/dist/onboard-3q20ZyHj.js +9 -0
- package/dist/onboard-DnegOHMh.js +3026 -0
- package/dist/run-main.js +93 -94
- package/dist/runner-Bu--_RXw.js +810 -0
- package/dist/sdk/index.js +2 -2
- package/dist/sdk/index.mjs +2 -2
- package/dist/server-CCI1hv45.js +1047 -0
- package/dist/server-RBqwE_GN.js +4 -0
- package/dist/voice-transcription-CbQBToY0.js +138 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<p align="center">
|
|
1
|
+
๏ปฟ<p align="center">
|
|
2
2
|
<img src="assets/icon.png" width="120" alt="HyperClaw">
|
|
3
3
|
<br>
|
|
4
4
|
<h1 align="center">๐ฆ
HyperClaw โ Personal AI Assistant</h1>
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="https://img.shields.io/badge/build-passing-brightgreen?style=flat-square" alt="build">
|
|
9
|
-
<img src="https://img.shields.io/badge/release-v4.0.
|
|
9
|
+
<img src="https://img.shields.io/badge/release-v4.0.1-blue?style=flat-square" alt="release">
|
|
10
10
|
<img src="https://img.shields.io/badge/node-%E2%89%A522-green?style=flat-square" alt="node">
|
|
11
11
|
<img src="https://img.shields.io/badge/license-MIT-gray?style=flat-square" alt="license">
|
|
12
12
|
<img src="https://img.shields.io/badge/typescript-5.4-3178c6?style=flat-square&logo=typescript&logoColor=white" alt="typescript">
|
|
13
|
+
<img src="https://img.shields.io/badge/security-ethical%20hacking-red?style=flat-square&logo=hackthebox&logoColor=white" alt="security">
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
16
|
<p align="center">
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
</p>
|
|
22
23
|
|
|
23
24
|
<p align="center">
|
|
24
|
-
<em>If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.</em>
|
|
25
|
+
<em>If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.</em><br>
|
|
26
|
+
<em>Built for developers, security researchers, and power users who want full control.</em>
|
|
25
27
|
</p>
|
|
26
28
|
|
|
27
29
|
<p align="center">
|
|
@@ -36,19 +38,34 @@
|
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
41
|
+
## Use cases
|
|
42
|
+
|
|
43
|
+
| Use case | How |
|
|
44
|
+
|----------|-----|
|
|
45
|
+
| **Personal assistant** | Chat via Telegram/Discord, voice on macOS/iOS, always-on daemon |
|
|
46
|
+
| **Bug bounty & OSINT** | HackerOne/Bugcrowd/Synack API keys, web-search skill, clipboard & screenshot tools |
|
|
47
|
+
| **Ethical hacking / pentest** | PC access tools (bash, file read/write), sandboxed execution, MCP tool servers |
|
|
48
|
+
| **Cybersecurity research** | Automate recon, triage findings, draft reports โ all from your phone via Telegram |
|
|
49
|
+
| **Developer productivity** | Code review, GitHub integration, local shell access, memory across sessions |
|
|
50
|
+
| **Home automation** | Cron skills, morning briefing, calendar events, device commands (macOS/Android) |
|
|
51
|
+
|
|
52
|
+
> HyperClaw runs **locally on your machine** โ your data, your keys, your control.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
39
56
|
## Install
|
|
40
57
|
|
|
41
58
|
Runtime: Node โฅ 22.
|
|
42
59
|
|
|
43
60
|
```bash
|
|
44
|
-
npm install -g github:hyperclaw-ai/hyperclaw
|
|
45
|
-
# ฮฎ
|
|
46
|
-
npm install -g https://github.com/hyperclaw-ai/hyperclaw
|
|
47
61
|
npm install -g hyperclaw@latest
|
|
48
62
|
# or: pnpm add -g hyperclaw@latest
|
|
49
|
-
hyperclaw onboard --install deamon
|
|
50
63
|
|
|
64
|
+
# First-time setup wizard
|
|
51
65
|
hyperclaw onboard
|
|
66
|
+
|
|
67
|
+
# Or install with daemon (auto-start on boot, full PC access)
|
|
68
|
+
hyperclaw onboard --install-daemon
|
|
52
69
|
```
|
|
53
70
|
|
|
54
71
|
The wizard guides you step by step โ provider, model, gateway, channels, and skills.
|
|
@@ -62,17 +79,20 @@ Works on **macOS, Linux, and Windows** (via WSL2 recommended). Compatible with n
|
|
|
62
79
|
# 1. Run the onboarding wizard (first time)
|
|
63
80
|
hyperclaw onboard
|
|
64
81
|
|
|
65
|
-
#
|
|
82
|
+
# 2a. Start the gateway in foreground
|
|
66
83
|
hyperclaw gateway --port 18789 --verbose
|
|
67
84
|
|
|
68
|
-
#
|
|
69
|
-
hyperclaw daemon
|
|
85
|
+
# 2b. Or run as a background daemon (auto-start on boot)
|
|
86
|
+
hyperclaw daemon start
|
|
70
87
|
|
|
71
|
-
#
|
|
88
|
+
# 3. Talk to your assistant
|
|
72
89
|
hyperclaw agent --message "What can you do?"
|
|
73
90
|
|
|
74
|
-
#
|
|
75
|
-
|
|
91
|
+
# 4. Security / bug bounty โ run recon from your phone
|
|
92
|
+
# Just message your Telegram bot: "search HackerOne for targets on acme.com"
|
|
93
|
+
|
|
94
|
+
# 5. Check status
|
|
95
|
+
hyperclaw doctor
|
|
76
96
|
```
|
|
77
97
|
|
|
78
98
|
Upgrading? Run `hyperclaw doctor` to check and migrate.
|
|
@@ -213,7 +233,7 @@ Full guide: [docs/security.md](docs/security.md)
|
|
|
213
233
|
## Features
|
|
214
234
|
|
|
215
235
|
- **Local-first Gateway** โ single control plane for sessions, channels, tools, and events
|
|
216
|
-
- **Multi-channel inbox** โ
|
|
236
|
+
- **Multi-channel inbox** โ 27+ channels, unified session model
|
|
217
237
|
- **Multi-agent routing** โ route channels/accounts to isolated agent workspaces
|
|
218
238
|
- **Extended thinking** โ Claude extended thinking with `/think high` in chat
|
|
219
239
|
- **Voice** โ Talk Mode with ElevenLabs TTS + system TTS fallback
|
|
@@ -312,10 +332,19 @@ docker build -f Dockerfile.sandbox -t hyperclaw:sandbox .
|
|
|
312
332
|
```
|
|
313
333
|
hyperclaw/
|
|
314
334
|
โโโ src/ # Core CLI, gateway, channels, tools
|
|
315
|
-
โ โโโ cli/ # CLI entry point +
|
|
335
|
+
โ โโโ cli/ # CLI entry point + onboarding wizard
|
|
316
336
|
โ โโโ gateway/ # Gateway server + manager (re-exports)
|
|
317
337
|
โ โโโ channels/ # Channel connectors + registry
|
|
318
|
-
โ
|
|
338
|
+
โ โโโ services/ # MCP, memory, heartbeat, cron
|
|
339
|
+
โ โโโ agent/ # Agent loop, orchestrator, tool dispatch
|
|
340
|
+
โ โโโ canvas/ # A2UI Canvas renderer
|
|
341
|
+
โ โโโ commands/ # CLI sub-commands (channels, pairingโฆ)
|
|
342
|
+
โ โโโ hooks/ # Lifecycle hooks (boot, cron, memory)
|
|
343
|
+
โ โโโ infra/ # Tool policy, destructive gate, secrets
|
|
344
|
+
โ โโโ media/ # Voice, TTS, STT, audio
|
|
345
|
+
โ โโโ routing/ # Session routing + multi-agent dispatch
|
|
346
|
+
โ โโโ security/ # Auth, sandboxing, DM policy
|
|
347
|
+
โ โโโ โฆ # (sdk, types, webhooks, logging, pluginsโฆ)
|
|
319
348
|
โโโ packages/
|
|
320
349
|
โ โโโ core/ # Inference engine, agent loop
|
|
321
350
|
โ โโโ gateway/ # Gateway package (standalone)
|
|
@@ -324,9 +353,12 @@ hyperclaw/
|
|
|
324
353
|
โ โโโ ios/ # iOS node app
|
|
325
354
|
โ โโโ android/ # Android node app
|
|
326
355
|
โ โโโ macos/ # macOS menu bar app
|
|
327
|
-
โ
|
|
356
|
+
โ โโโ macos-menubar/ # Tauri macOS menu bar
|
|
357
|
+
โ โโโ web/ # Web UI (React + Vite)
|
|
328
358
|
โโโ extensions/ # Channel connectors (Telegram, Discordโฆ)
|
|
329
359
|
โโโ skills/ # Bundled skills (reminders, translator)
|
|
360
|
+
โโโ workspace-templates/ # Agent config templates (AGENTS.md, SOUL.md, TOOLS.mdโฆ)
|
|
361
|
+
โโโ scripts/ # Build + utility scripts
|
|
330
362
|
โโโ tests/ # Vitest โ unit / integration / e2e
|
|
331
363
|
โโโ docs/ # Full documentation
|
|
332
364
|
```
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-jS-bbMI5.js');
|
|
2
|
+
|
|
3
|
+
//#region src/infra/api-keys-guide.ts
|
|
4
|
+
const API_KEYS_GUIDE = [
|
|
5
|
+
{
|
|
6
|
+
serviceId: "anthropic",
|
|
7
|
+
name: "Anthropic (Claude)",
|
|
8
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
9
|
+
url: "platform.anthropic.com",
|
|
10
|
+
setupSteps: [
|
|
11
|
+
"1. Go to platform.anthropic.com โ API Keys.",
|
|
12
|
+
"2. Sign up / sign in with an Anthropic account.",
|
|
13
|
+
"3. Create Key โ copy it (starts with sk-ant-). Not shown again!",
|
|
14
|
+
"",
|
|
15
|
+
" ๐ platform.anthropic.com/settings/keys"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
serviceId: "openai",
|
|
20
|
+
name: "OpenAI (GPT)",
|
|
21
|
+
envVar: "OPENAI_API_KEY",
|
|
22
|
+
url: "platform.openai.com",
|
|
23
|
+
setupSteps: [
|
|
24
|
+
"1. Go to platform.openai.com โ API keys.",
|
|
25
|
+
"2. Create new secret key โ copy it (starts with sk-). Not shown again!",
|
|
26
|
+
"3. You need billing enabled for production use.",
|
|
27
|
+
"",
|
|
28
|
+
" ๐ platform.openai.com/api-keys"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
serviceId: "openrouter",
|
|
33
|
+
name: "OpenRouter",
|
|
34
|
+
envVar: "OPENROUTER_API_KEY",
|
|
35
|
+
url: "openrouter.ai",
|
|
36
|
+
setupSteps: [
|
|
37
|
+
"1. Go to openrouter.ai โ Keys.",
|
|
38
|
+
"2. Sign in (Google/GitHub).",
|
|
39
|
+
"3. Create Key โ copy it. OpenRouter provides access to many models (Claude, GPT etc.).",
|
|
40
|
+
"",
|
|
41
|
+
" ๐ openrouter.ai/keys"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
serviceId: "tavily",
|
|
46
|
+
name: "Tavily (Web Search)",
|
|
47
|
+
envVar: "TAVILY_API_KEY",
|
|
48
|
+
url: "tavily.com",
|
|
49
|
+
setupSteps: [
|
|
50
|
+
"1. Go to tavily.com โ Sign up.",
|
|
51
|
+
"2. Dashboard โ API Keys โ Create API Key.",
|
|
52
|
+
"3. Copy the key. Used for the web-search skill.",
|
|
53
|
+
"",
|
|
54
|
+
" ๐ app.tavily.com"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
serviceId: "elevenlabs",
|
|
59
|
+
name: "ElevenLabs (TTS)",
|
|
60
|
+
envVar: "ELEVENLABS_API_KEY",
|
|
61
|
+
url: "elevenlabs.io",
|
|
62
|
+
setupSteps: [
|
|
63
|
+
"1. Go to elevenlabs.io โ Profile โ API Key.",
|
|
64
|
+
"2. Copy the API key (or create a new one).",
|
|
65
|
+
"3. Used for talk mode (voice responses).",
|
|
66
|
+
"",
|
|
67
|
+
" ๐ elevenlabs.io/app/settings/api-keys"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
serviceId: "deepl",
|
|
72
|
+
name: "DeepL (Translation)",
|
|
73
|
+
envVar: "DEEPL_API_KEY",
|
|
74
|
+
url: "deepl.com",
|
|
75
|
+
setupSteps: [
|
|
76
|
+
"1. Go to deepl.com/pro-api โ Get API key.",
|
|
77
|
+
"2. Sign up (free tier available).",
|
|
78
|
+
"3. Account โ API keys โ copy the Authentication Key.",
|
|
79
|
+
"",
|
|
80
|
+
" ๐ deepl.com/pro-api"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
serviceId: "github",
|
|
85
|
+
name: "GitHub (PAT)",
|
|
86
|
+
envVar: "GITHUB_TOKEN",
|
|
87
|
+
url: "github.com",
|
|
88
|
+
setupSteps: [
|
|
89
|
+
"1. GitHub โ Settings โ Developer settings โ Personal access tokens.",
|
|
90
|
+
"2. Generate new token (classic or fine-grained).",
|
|
91
|
+
"3. Select scopes: repo, read:user etc. depending on use case.",
|
|
92
|
+
"",
|
|
93
|
+
" ๐ github.com/settings/tokens"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
serviceId: "xai",
|
|
98
|
+
name: "xAI (Grok)",
|
|
99
|
+
envVar: "XAI_API_KEY",
|
|
100
|
+
url: "x.ai",
|
|
101
|
+
setupSteps: [
|
|
102
|
+
"1. Go to console.x.ai โ API keys.",
|
|
103
|
+
"2. Sign in and create a new key.",
|
|
104
|
+
"3. Copy the key.",
|
|
105
|
+
"",
|
|
106
|
+
" ๐ console.x.ai"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
serviceId: "google",
|
|
111
|
+
name: "Google AI (Gemini)",
|
|
112
|
+
envVar: "GOOGLE_AI_API_KEY",
|
|
113
|
+
url: "ai.google.dev",
|
|
114
|
+
setupSteps: [
|
|
115
|
+
"1. Go to aistudio.google.com/apikey.",
|
|
116
|
+
"2. Get API key or Create API key.",
|
|
117
|
+
"3. Copy the key.",
|
|
118
|
+
"",
|
|
119
|
+
" ๐ aistudio.google.com/apikey"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
const SERVICE_ID_MAP = new Map(API_KEYS_GUIDE.map((g) => [g.serviceId.toLowerCase(), g]));
|
|
124
|
+
/** Known name aliases (e.g. anthropic, claude -> anthropic) */
|
|
125
|
+
const ALIASES = {
|
|
126
|
+
claude: "anthropic",
|
|
127
|
+
gpt: "openai",
|
|
128
|
+
xai: "xai",
|
|
129
|
+
google: "google"
|
|
130
|
+
};
|
|
131
|
+
function getApiKeyGuide(serviceId) {
|
|
132
|
+
const id = serviceId.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
133
|
+
return SERVICE_ID_MAP.get(id) ?? SERVICE_ID_MAP.get(ALIASES[id] ?? "") ?? null;
|
|
134
|
+
}
|
|
135
|
+
/** For unknown services โ generic API key instructions */
|
|
136
|
+
const GENERIC_API_KEY_STEPS = [
|
|
137
|
+
"For an unknown service:",
|
|
138
|
+
"1. Go to the official service website (e.g. developers.xxx.com).",
|
|
139
|
+
"2. Sign up / sign in. An account is usually required.",
|
|
140
|
+
"3. Look for \"API Keys\", \"Credentials\", \"Developer\" or \"Integrations\" section.",
|
|
141
|
+
"4. Create a new API key or token. Copy it immediately โ many services do not show it again.",
|
|
142
|
+
"5. Keep it secret โ do not share it or commit it to a repo.",
|
|
143
|
+
"",
|
|
144
|
+
" ๐ก Known services: anthropic, openai, openrouter, tavily, elevenlabs, deepl, github, xai, google"
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
exports.GENERIC_API_KEY_STEPS = GENERIC_API_KEY_STEPS;
|
|
149
|
+
exports.getApiKeyGuide = getApiKeyGuide;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-jS-bbMI5.js');
|
|
2
|
+
const chalk = require_chunk.__toESM(require("chalk"));
|
|
3
|
+
const fs_extra = require_chunk.__toESM(require("fs-extra"));
|
|
4
|
+
const path = require_chunk.__toESM(require("path"));
|
|
5
|
+
const os = require_chunk.__toESM(require("os"));
|
|
6
|
+
const ws = require_chunk.__toESM(require("ws"));
|
|
7
|
+
const https = require_chunk.__toESM(require("https"));
|
|
8
|
+
const events = require_chunk.__toESM(require("events"));
|
|
9
|
+
|
|
10
|
+
//#region extensions/discord/src/connector.ts
|
|
11
|
+
const STATE_FILE = path.default.join(os.default.homedir(), ".hyperclaw", "discord-state.json");
|
|
12
|
+
const OPC = {
|
|
13
|
+
DISPATCH: 0,
|
|
14
|
+
HEARTBEAT: 1,
|
|
15
|
+
IDENTIFY: 2,
|
|
16
|
+
RESUME: 6,
|
|
17
|
+
RECONNECT: 7,
|
|
18
|
+
INVALID_SESSION: 9,
|
|
19
|
+
HELLO: 10,
|
|
20
|
+
HEARTBEAT_ACK: 11
|
|
21
|
+
};
|
|
22
|
+
const INTENTS = {
|
|
23
|
+
GUILDS: 1,
|
|
24
|
+
GUILD_MESSAGES: 512,
|
|
25
|
+
GUILD_MESSAGE_CONTENT: 32768,
|
|
26
|
+
DIRECT_MESSAGES: 4096,
|
|
27
|
+
DIRECT_MESSAGE_CONTENT: 16384
|
|
28
|
+
};
|
|
29
|
+
function discordRest(token, method, endpoint, body) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
32
|
+
const req = https.default.request({
|
|
33
|
+
hostname: "discord.com",
|
|
34
|
+
port: 443,
|
|
35
|
+
path: `/api/v10${endpoint}`,
|
|
36
|
+
method,
|
|
37
|
+
headers: {
|
|
38
|
+
"Authorization": `Bot ${token}`,
|
|
39
|
+
"User-Agent": "HyperClaw/4.0.1 (https://hyperclaw.ai)",
|
|
40
|
+
...payload ? {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
43
|
+
} : {}
|
|
44
|
+
}
|
|
45
|
+
}, (res) => {
|
|
46
|
+
let data = "";
|
|
47
|
+
res.on("data", (c) => data += c);
|
|
48
|
+
res.on("end", () => {
|
|
49
|
+
if (res.statusCode === 204) return resolve(null);
|
|
50
|
+
try {
|
|
51
|
+
const r = JSON.parse(data);
|
|
52
|
+
if (res.statusCode && res.statusCode >= 400) reject(new Error(`Discord API ${res.statusCode}: ${r.message || data}`));
|
|
53
|
+
else resolve(r);
|
|
54
|
+
} catch {
|
|
55
|
+
reject(new Error(`Invalid JSON (${res.statusCode})`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
req.on("error", reject);
|
|
60
|
+
if (payload) req.write(payload);
|
|
61
|
+
req.end();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
var DiscordConnector = class extends events.EventEmitter {
|
|
65
|
+
token;
|
|
66
|
+
config;
|
|
67
|
+
ws = null;
|
|
68
|
+
heartbeatInterval = null;
|
|
69
|
+
lastSequence = null;
|
|
70
|
+
sessionId = null;
|
|
71
|
+
resumeGatewayUrl = null;
|
|
72
|
+
running = false;
|
|
73
|
+
botUser = null;
|
|
74
|
+
constructor(token, config) {
|
|
75
|
+
super();
|
|
76
|
+
this.token = token;
|
|
77
|
+
this.config = {
|
|
78
|
+
token,
|
|
79
|
+
dmPolicy: "allowlist",
|
|
80
|
+
allowFrom: [],
|
|
81
|
+
approvedPairings: [],
|
|
82
|
+
pendingPairings: {},
|
|
83
|
+
listenGuildIds: [],
|
|
84
|
+
commandPrefix: "!",
|
|
85
|
+
...config
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async connect() {
|
|
89
|
+
const gateway = await discordRest(this.token, "GET", "/gateway/bot");
|
|
90
|
+
const wsUrl = (gateway.url || "wss://gateway.discord.gg") + "/?v=10&encoding=json";
|
|
91
|
+
this.botUser = await discordRest(this.token, "GET", "/users/@me");
|
|
92
|
+
console.log(chalk.default.green(` ๐ฆ
Discord: @${this.botUser?.username} connected`));
|
|
93
|
+
await this.loadState();
|
|
94
|
+
this.running = true;
|
|
95
|
+
await this.openWebSocket(wsUrl);
|
|
96
|
+
}
|
|
97
|
+
async disconnect() {
|
|
98
|
+
this.running = false;
|
|
99
|
+
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
100
|
+
this.ws?.close(1e3);
|
|
101
|
+
await this.saveState();
|
|
102
|
+
}
|
|
103
|
+
async openWebSocket(url) {
|
|
104
|
+
this.ws = new ws.WebSocket(url, { headers: { "User-Agent": "HyperClaw/4.0.1" } });
|
|
105
|
+
this.ws.on("message", async (data) => {
|
|
106
|
+
const payload = JSON.parse(data.toString());
|
|
107
|
+
await this.handlePayload(payload);
|
|
108
|
+
});
|
|
109
|
+
this.ws.on("close", (code) => {
|
|
110
|
+
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
111
|
+
if (!this.running) return;
|
|
112
|
+
const resumable = ![
|
|
113
|
+
1e3,
|
|
114
|
+
4004,
|
|
115
|
+
4010,
|
|
116
|
+
4011,
|
|
117
|
+
4012,
|
|
118
|
+
4013,
|
|
119
|
+
4014
|
|
120
|
+
].includes(code);
|
|
121
|
+
console.log(chalk.default.yellow(` โ Discord WS closed (${code}) โ ${resumable ? "resuming" : "reconnecting"} in 5s`));
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
if (this.running) {
|
|
124
|
+
const url$1 = resumable && this.resumeGatewayUrl ? this.resumeGatewayUrl + "/?v=10&encoding=json" : "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
125
|
+
this.openWebSocket(url$1);
|
|
126
|
+
}
|
|
127
|
+
}, 5e3);
|
|
128
|
+
});
|
|
129
|
+
this.ws.on("error", (e) => console.log(chalk.default.yellow(` โ Discord WS error: ${e.message}`)));
|
|
130
|
+
}
|
|
131
|
+
async handlePayload(payload) {
|
|
132
|
+
const { op, d, s, t } = payload;
|
|
133
|
+
if (s !== null && s !== void 0) this.lastSequence = s;
|
|
134
|
+
switch (op) {
|
|
135
|
+
case OPC.HELLO:
|
|
136
|
+
this.startHeartbeat(d.heartbeat_interval);
|
|
137
|
+
if (this.sessionId && this.lastSequence) this.resume();
|
|
138
|
+
else this.identify();
|
|
139
|
+
break;
|
|
140
|
+
case OPC.HEARTBEAT_ACK: break;
|
|
141
|
+
case OPC.HEARTBEAT:
|
|
142
|
+
this.sendWs({
|
|
143
|
+
op: OPC.HEARTBEAT,
|
|
144
|
+
d: this.lastSequence
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
case OPC.RECONNECT:
|
|
148
|
+
this.ws?.close(4e3);
|
|
149
|
+
break;
|
|
150
|
+
case OPC.INVALID_SESSION:
|
|
151
|
+
if (d) setTimeout(() => this.resume(), 2e3);
|
|
152
|
+
else {
|
|
153
|
+
this.sessionId = null;
|
|
154
|
+
this.lastSequence = null;
|
|
155
|
+
setTimeout(() => this.identify(), 2e3);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
case OPC.DISPATCH:
|
|
159
|
+
await this.handleEvent(t, d);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
identify() {
|
|
164
|
+
const intents = INTENTS.GUILDS | INTENTS.GUILD_MESSAGES | INTENTS.GUILD_MESSAGE_CONTENT | INTENTS.DIRECT_MESSAGES;
|
|
165
|
+
this.sendWs({
|
|
166
|
+
op: OPC.IDENTIFY,
|
|
167
|
+
d: {
|
|
168
|
+
token: this.token,
|
|
169
|
+
intents,
|
|
170
|
+
properties: {
|
|
171
|
+
os: process.platform,
|
|
172
|
+
browser: "HyperClaw",
|
|
173
|
+
device: "HyperClaw"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
resume() {
|
|
179
|
+
this.sendWs({
|
|
180
|
+
op: OPC.RESUME,
|
|
181
|
+
d: {
|
|
182
|
+
token: this.token,
|
|
183
|
+
session_id: this.sessionId,
|
|
184
|
+
seq: this.lastSequence
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
startHeartbeat(interval) {
|
|
189
|
+
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
190
|
+
this.heartbeatInterval = setInterval(() => {
|
|
191
|
+
this.sendWs({
|
|
192
|
+
op: OPC.HEARTBEAT,
|
|
193
|
+
d: this.lastSequence
|
|
194
|
+
});
|
|
195
|
+
}, interval);
|
|
196
|
+
}
|
|
197
|
+
sendWs(payload) {
|
|
198
|
+
if (this.ws?.readyState === ws.WebSocket.OPEN) this.ws.send(JSON.stringify(payload));
|
|
199
|
+
}
|
|
200
|
+
async handleEvent(type, data) {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case "READY":
|
|
203
|
+
this.sessionId = data.session_id;
|
|
204
|
+
this.resumeGatewayUrl = data.resume_gateway_url;
|
|
205
|
+
this.botUser = data.user;
|
|
206
|
+
await this.saveState();
|
|
207
|
+
this.emit("ready", data.user);
|
|
208
|
+
break;
|
|
209
|
+
case "RESUMED":
|
|
210
|
+
console.log(chalk.default.gray(" Discord session resumed"));
|
|
211
|
+
break;
|
|
212
|
+
case "MESSAGE_CREATE":
|
|
213
|
+
await this.handleMessage(data);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async handleMessage(msg) {
|
|
218
|
+
if (msg.author.id === this.botUser?.id) return;
|
|
219
|
+
if (msg.author.bot) return;
|
|
220
|
+
const isDM = !msg.channel_id.startsWith("0") && await this.isDirectMessage(msg.channel_id);
|
|
221
|
+
const userId = msg.author.id;
|
|
222
|
+
if (isDM) {
|
|
223
|
+
const allowed = await this.checkDMPolicy(userId, msg.channel_id, msg.content);
|
|
224
|
+
if (!allowed) return;
|
|
225
|
+
}
|
|
226
|
+
this.emit("message", {
|
|
227
|
+
id: msg.id,
|
|
228
|
+
channelId: "discord",
|
|
229
|
+
from: userId,
|
|
230
|
+
fromUsername: msg.author.global_name || msg.author.username,
|
|
231
|
+
chatId: msg.channel_id,
|
|
232
|
+
text: msg.content,
|
|
233
|
+
timestamp: msg.timestamp,
|
|
234
|
+
isDM
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
dmChannelCache = /* @__PURE__ */ new Set();
|
|
238
|
+
async isDirectMessage(channelId) {
|
|
239
|
+
if (this.dmChannelCache.has(channelId)) return true;
|
|
240
|
+
try {
|
|
241
|
+
const channel = await discordRest(this.token, "GET", `/channels/${channelId}`);
|
|
242
|
+
if (channel.type === 1) {
|
|
243
|
+
this.dmChannelCache.add(channelId);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
async checkDMPolicy(userId, channelId, text) {
|
|
250
|
+
if (this.config.dmPolicy === "none") return false;
|
|
251
|
+
if (this.config.dmPolicy === "open") return true;
|
|
252
|
+
if (this.config.dmPolicy === "allowlist") {
|
|
253
|
+
if (this.config.allowFrom.includes(userId)) return true;
|
|
254
|
+
await this.sendMessage(channelId, "๐ฆ
**HyperClaw**\n\nYou are not on the allowlist.");
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
if (this.config.dmPolicy === "pairing") {
|
|
258
|
+
if (this.config.approvedPairings.includes(userId)) return true;
|
|
259
|
+
const upper = text.trim().toUpperCase();
|
|
260
|
+
if (this.config.pendingPairings[upper]) {
|
|
261
|
+
this.config.approvedPairings.push(userId);
|
|
262
|
+
delete this.config.pendingPairings[upper];
|
|
263
|
+
await this.saveState();
|
|
264
|
+
await this.sendMessage(channelId, "๐ฆ
**Paired!** You can now send messages.");
|
|
265
|
+
this.emit("pairing:approved", {
|
|
266
|
+
userId,
|
|
267
|
+
channelId: "discord"
|
|
268
|
+
});
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const code = this.generateCode();
|
|
272
|
+
this.config.pendingPairings[code] = userId;
|
|
273
|
+
await this.saveState();
|
|
274
|
+
await this.sendMessage(channelId, `๐ฆ
**HyperClaw Pairing**\n\nSend the owner this code:\n\`${code}\`\n\nApprove with:\n\`hyperclaw pairing approve discord ${code}\``);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
async sendMessage(channelId, content) {
|
|
280
|
+
const chunks = content.match(/.{1,2000}/gs) || [content];
|
|
281
|
+
let last = null;
|
|
282
|
+
for (const chunk of chunks) last = await discordRest(this.token, "POST", `/channels/${channelId}/messages`, { content: chunk });
|
|
283
|
+
return last;
|
|
284
|
+
}
|
|
285
|
+
async sendTyping(channelId) {
|
|
286
|
+
await discordRest(this.token, "POST", `/channels/${channelId}/typing`).catch(() => {});
|
|
287
|
+
}
|
|
288
|
+
async createDMChannel(userId) {
|
|
289
|
+
const ch = await discordRest(this.token, "POST", "/users/@me/channels", { recipient_id: userId });
|
|
290
|
+
return ch.id;
|
|
291
|
+
}
|
|
292
|
+
async sendDM(userId, content) {
|
|
293
|
+
const channelId = await this.createDMChannel(userId);
|
|
294
|
+
await this.sendMessage(channelId, content);
|
|
295
|
+
}
|
|
296
|
+
generateCode() {
|
|
297
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
298
|
+
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
|
299
|
+
}
|
|
300
|
+
approvePairing(code) {
|
|
301
|
+
const upper = code.toUpperCase();
|
|
302
|
+
if (!this.config.pendingPairings[upper]) return false;
|
|
303
|
+
this.config.approvedPairings.push(this.config.pendingPairings[upper]);
|
|
304
|
+
delete this.config.pendingPairings[upper];
|
|
305
|
+
this.saveState();
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
addToAllowlist(userId) {
|
|
309
|
+
if (!this.config.allowFrom.includes(userId)) {
|
|
310
|
+
this.config.allowFrom.push(userId);
|
|
311
|
+
this.saveState();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async loadState() {
|
|
315
|
+
try {
|
|
316
|
+
const s = await fs_extra.default.readJson(STATE_FILE);
|
|
317
|
+
this.sessionId = s.sessionId || null;
|
|
318
|
+
this.lastSequence = s.lastSequence || null;
|
|
319
|
+
this.resumeGatewayUrl = s.resumeGatewayUrl || null;
|
|
320
|
+
if (s.pendingPairings) this.config.pendingPairings = s.pendingPairings;
|
|
321
|
+
if (s.approvedPairings) this.config.approvedPairings = s.approvedPairings;
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
324
|
+
async saveState() {
|
|
325
|
+
await fs_extra.default.ensureDir(path.default.dirname(STATE_FILE));
|
|
326
|
+
await fs_extra.default.writeJson(STATE_FILE, {
|
|
327
|
+
sessionId: this.sessionId,
|
|
328
|
+
lastSequence: this.lastSequence,
|
|
329
|
+
resumeGatewayUrl: this.resumeGatewayUrl,
|
|
330
|
+
pendingPairings: this.config.pendingPairings,
|
|
331
|
+
approvedPairings: this.config.approvedPairings
|
|
332
|
+
}, { spaces: 2 });
|
|
333
|
+
}
|
|
334
|
+
isRunning() {
|
|
335
|
+
return this.running;
|
|
336
|
+
}
|
|
337
|
+
getBotUser() {
|
|
338
|
+
return this.botUser;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
exports.DiscordConnector = DiscordConnector;
|