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 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.0-blue?style=flat-square" alt="release">
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
- # 2. Start the gateway (stays running in foreground)
82
+ # 2a. Start the gateway in foreground
66
83
  hyperclaw gateway --port 18789 --verbose
67
84
 
68
- # 3. Or install as a background daemon (launchd / systemd)
69
- hyperclaw daemon install && hyperclaw daemon start
85
+ # 2b. Or run as a background daemon (auto-start on boot)
86
+ hyperclaw daemon start
70
87
 
71
- # 4. Talk to your assistant
88
+ # 3. Talk to your assistant
72
89
  hyperclaw agent --message "What can you do?"
73
90
 
74
- # 5. Send a message to a connected channel
75
- hyperclaw message send --to +1234567890 --message "Hello from HyperClaw"
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** โ€” 14+ channels, unified session model
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 + commands
335
+ โ”‚ โ”œโ”€โ”€ cli/ # CLI entry point + onboarding wizard
316
336
  โ”‚ โ”œโ”€โ”€ gateway/ # Gateway server + manager (re-exports)
317
337
  โ”‚ โ”œโ”€โ”€ channels/ # Channel connectors + registry
318
- โ”‚ โ””โ”€โ”€ services/ # MCP, memory, heartbeat, cron
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
- โ”‚ โ””โ”€โ”€ macos-menubar/ # Tauri macOS menu bar
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;