@gamalan/pi-gateway 1.0.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 (75) hide show
  1. package/README.md +366 -0
  2. package/config/config.default.json +51 -0
  3. package/dist/adapters/base.d.ts +85 -0
  4. package/dist/adapters/base.js +34 -0
  5. package/dist/adapters/base.js.map +1 -0
  6. package/dist/adapters/discord.d.ts +53 -0
  7. package/dist/adapters/discord.js +224 -0
  8. package/dist/adapters/discord.js.map +1 -0
  9. package/dist/adapters/index.d.ts +13 -0
  10. package/dist/adapters/index.js +8 -0
  11. package/dist/adapters/index.js.map +1 -0
  12. package/dist/adapters/slack.d.ts +49 -0
  13. package/dist/adapters/slack.js +231 -0
  14. package/dist/adapters/slack.js.map +1 -0
  15. package/dist/adapters/telegram.d.ts +64 -0
  16. package/dist/adapters/telegram.js +274 -0
  17. package/dist/adapters/telegram.js.map +1 -0
  18. package/dist/adapters/twitch.d.ts +75 -0
  19. package/dist/adapters/twitch.js +222 -0
  20. package/dist/adapters/twitch.js.map +1 -0
  21. package/dist/adapters/websocket.d.ts +30 -0
  22. package/dist/adapters/websocket.js +132 -0
  23. package/dist/adapters/websocket.js.map +1 -0
  24. package/dist/adapters/whatsapp.d.ts +49 -0
  25. package/dist/adapters/whatsapp.js +251 -0
  26. package/dist/adapters/whatsapp.js.map +1 -0
  27. package/dist/background/index.d.ts +1 -0
  28. package/dist/background/index.js +2 -0
  29. package/dist/background/index.js.map +1 -0
  30. package/dist/background/manager.d.ts +70 -0
  31. package/dist/background/manager.js +291 -0
  32. package/dist/background/manager.js.map +1 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.js +1275 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/logger.d.ts +12 -0
  37. package/dist/logger.js +39 -0
  38. package/dist/logger.js.map +1 -0
  39. package/dist/paths.d.ts +14 -0
  40. package/dist/paths.js +32 -0
  41. package/dist/paths.js.map +1 -0
  42. package/dist/security/auth.d.ts +91 -0
  43. package/dist/security/auth.js +316 -0
  44. package/dist/security/auth.js.map +1 -0
  45. package/dist/security/index.d.ts +1 -0
  46. package/dist/security/index.js +2 -0
  47. package/dist/security/index.js.map +1 -0
  48. package/dist/security/tool-policy.d.ts +66 -0
  49. package/dist/security/tool-policy.js +467 -0
  50. package/dist/security/tool-policy.js.map +1 -0
  51. package/dist/sessions/index.d.ts +1 -0
  52. package/dist/sessions/index.js +2 -0
  53. package/dist/sessions/index.js.map +1 -0
  54. package/dist/sessions/store.d.ts +64 -0
  55. package/dist/sessions/store.js +247 -0
  56. package/dist/sessions/store.js.map +1 -0
  57. package/package.json +57 -0
  58. package/src/adapters/base.ts +124 -0
  59. package/src/adapters/discord.ts +270 -0
  60. package/src/adapters/index.ts +13 -0
  61. package/src/adapters/slack.ts +313 -0
  62. package/src/adapters/telegram.ts +394 -0
  63. package/src/adapters/twitch.ts +316 -0
  64. package/src/adapters/websocket.ts +158 -0
  65. package/src/adapters/whatsapp.ts +296 -0
  66. package/src/background/index.ts +1 -0
  67. package/src/background/manager.ts +395 -0
  68. package/src/index.ts +1665 -0
  69. package/src/logger.ts +43 -0
  70. package/src/paths.ts +40 -0
  71. package/src/security/auth.ts +458 -0
  72. package/src/security/index.ts +1 -0
  73. package/src/security/tool-policy.ts +568 -0
  74. package/src/sessions/index.ts +1 -0
  75. package/src/sessions/store.ts +360 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Discord Adapter - Hermes-style Discord platform adapter
3
+ *
4
+ * Features:
5
+ * - DM and guild channel support
6
+ * - Slash command registration
7
+ * - Typing indicators
8
+ * - Message editing/deletion
9
+ * - Rate limit handling
10
+ */
11
+
12
+ import { BaseAdapter, type PlatformMessage, type PlatformConfig } from "./base.js";
13
+ import { logger } from "../logger.js";
14
+
15
+ export interface DiscordConfig extends PlatformConfig {
16
+ platform: "discord";
17
+ botToken: string;
18
+ guildId?: string;
19
+ allowedChannels?: string[]; // Whitelist specific channels
20
+ allowedRoles?: string[]; // Whitelist roles
21
+ requireMention?: boolean; // Require @mention in guilds
22
+ }
23
+
24
+ export class DiscordAdapter extends BaseAdapter {
25
+ readonly platform = "discord" as const;
26
+ config: DiscordConfig;
27
+ private httpClient: typeof fetch | null = null;
28
+ private wsConnection: WebSocket | null = null;
29
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
30
+ private sequence: number | null = null;
31
+ private sessionId: string | null = null;
32
+ private intents: number = 0;
33
+
34
+ constructor(config: DiscordConfig) {
35
+ super();
36
+ this.config = config;
37
+
38
+ // Intents: GUILD_MESSAGES (1<<9) + DIRECT_MESSAGES (1<<12) + MESSAGE_CONTENT (1<<15)
39
+ this.intents = 1 << 9 | 1 << 12 | 1 << 15;
40
+ }
41
+
42
+ async initialize(): Promise<void> {
43
+ // Test bot token
44
+ const response = await this.apiRequest("/users/@me");
45
+ const data: any = await response.json();
46
+ if (!response.ok) {
47
+ throw new Error(`Discord authentication failed: ${response.status}`);
48
+ }
49
+ logger.info(`[Discord] Bot initialized: ${data.username}`);
50
+ }
51
+
52
+ private async apiRequest(endpoint: string, options: RequestInit = {}): Promise<Response> {
53
+ const url = `https://discord.com/api/v10${endpoint}`;
54
+ return fetch(url, {
55
+ ...options,
56
+ headers: {
57
+ "Authorization": `Bot ${this.config.botToken}`,
58
+ "Content-Type": "application/json",
59
+ ...options.headers,
60
+ },
61
+ });
62
+ }
63
+
64
+ async start(callbacks): Promise<void> {
65
+ await super.start(callbacks);
66
+
67
+ // Connect to Gateway
68
+ const gatewayResponse = await this.apiRequest("/gateway");
69
+ const gatewayData = (await gatewayResponse.json()) as { url: string };
70
+ const gatewayUrl = `${gatewayData.url}?v=10&encoding=json&intents=${this.intents}`;
71
+
72
+ this.wsConnection = new WebSocket(gatewayUrl);
73
+
74
+ this.wsConnection.onopen = () => {
75
+ logger.info("[Discord] WebSocket connected");
76
+ };
77
+
78
+ this.wsConnection.onmessage = async (event) => {
79
+ const data: any = JSON.parse(event.data);
80
+ await this.handleGatewayMessage(data);
81
+ };
82
+
83
+ this.wsConnection.onclose = () => {
84
+ logger.info("[Discord] WebSocket closed");
85
+ this.callbacks?.onDisconnect?.();
86
+ // Attempt reconnect after 5 seconds
87
+ setTimeout(() => this.start(callbacks), 5000);
88
+ };
89
+ }
90
+
91
+ private async handleGatewayMessage(data: any): Promise<void> {
92
+ switch (data.op) {
93
+ case 0: // Dispatch
94
+ this.sequence = data.s;
95
+ await this.handleDispatch(data.t, data.d);
96
+ break;
97
+
98
+ case 10: // Hello
99
+ this.startHeartbeat(data.d.heartbeat_interval);
100
+ this.identify();
101
+ break;
102
+
103
+ case 11: // Heartbeat ACK
104
+ // Heartbeat acknowledged
105
+ break;
106
+ }
107
+ }
108
+
109
+ private startHeartbeat(interval: number): void {
110
+ this.heartbeatInterval = setInterval(() => {
111
+ if (this.wsConnection?.readyState === WebSocket.OPEN) {
112
+ this.wsConnection.send(JSON.stringify({
113
+ op: 1,
114
+ d: this.sequence,
115
+ }));
116
+ }
117
+ }, interval);
118
+ }
119
+
120
+ private async identify(): Promise<void> {
121
+ const identifyPayload = {
122
+ op: 2,
123
+ d: {
124
+ token: this.config.botToken,
125
+ intents: this.intents,
126
+ properties: {
127
+ os: "linux",
128
+ browser: "pi-gateway",
129
+ device: "pi-gateway",
130
+ },
131
+ },
132
+ };
133
+
134
+ this.wsConnection?.send(JSON.stringify(identifyPayload));
135
+ }
136
+
137
+ private async handleDispatch(type: string, data: any): Promise<void> {
138
+ switch (type) {
139
+ case "READY":
140
+ this.sessionId = data.session_id;
141
+ logger.info(`[Discord] Logged in as ${data.user.username}`);
142
+ break;
143
+
144
+ case "MESSAGE_CREATE":
145
+ await this.handleMessage(data);
146
+ break;
147
+
148
+ case "MESSAGE_UPDATE":
149
+ // Handle edits if needed
150
+ break;
151
+ }
152
+ }
153
+
154
+ private async handleMessage(data: any): Promise<void> {
155
+ // Ignore bots
156
+ if (data.author.bot && data.author.id !== this.getBotId()) return;
157
+
158
+ // Check if DM or allowed channel
159
+ const isDM = !data.guild_id;
160
+ if (!isDM && this.config.allowedChannels?.length) {
161
+ if (!this.config.allowedChannels.includes(data.channel_id)) return;
162
+ }
163
+
164
+ // Check mention requirement in guilds
165
+ if (!isDM && this.config.requireMention) {
166
+ const mentioned = data.content.includes(`<@${this.getBotId()}>`);
167
+ if (!mentioned) return;
168
+ }
169
+
170
+ const message: PlatformMessage = {
171
+ id: data.id,
172
+ platform: this.platform,
173
+ channelId: data.channel_id,
174
+ userId: data.author.id,
175
+ content: data.content,
176
+ timestamp: new Date(data.timestamp).getTime(),
177
+ metadata: {
178
+ guildId: data.guild_id,
179
+ username: data.author.username,
180
+ discriminator: data.author.discriminator,
181
+ isDM,
182
+ },
183
+ };
184
+
185
+ await this.callbacks?.onMessage(message);
186
+ }
187
+
188
+ private getBotId(): string {
189
+ // Extract from token - this is approximate
190
+ return this.config.botToken.split(".")[0];
191
+ }
192
+
193
+ async sendMessage(channelId: string, content: string): Promise<string> {
194
+ const response = await this.apiRequest(`/channels/${channelId}/messages`, {
195
+ method: "POST",
196
+ body: JSON.stringify({ content }),
197
+ });
198
+
199
+ if (!response.ok) {
200
+ const error = await response.text();
201
+ throw new Error(`Failed to send message: ${error}`);
202
+ }
203
+
204
+ const data = (await response.json()) as { id: string };
205
+ return data.id;
206
+ }
207
+
208
+ async editMessage(channelId: string, messageId: string, content: string): Promise<void> {
209
+ await this.apiRequest(`/channels/${channelId}/messages/${messageId}`, {
210
+ method: "PATCH",
211
+ body: JSON.stringify({ content }),
212
+ });
213
+ }
214
+
215
+ async deleteMessage(channelId: string, messageId: string): Promise<void> {
216
+ await this.apiRequest(`/channels/${channelId}/messages/${messageId}`, {
217
+ method: "DELETE",
218
+ });
219
+ }
220
+
221
+ async setTyping(channelId: string, isTyping: boolean): Promise<void> {
222
+ if (!isTyping) return; // Discord doesn't have a "stop typing" API
223
+
224
+ await this.apiRequest(`/channels/${channelId}/typing`, {
225
+ method: "POST",
226
+ });
227
+ }
228
+
229
+ async getStatus(): Promise<{ connected: boolean; latency?: number }> {
230
+ try {
231
+ const response = await this.apiRequest("/gateway/bot");
232
+ const data: any = await response.json();
233
+ return {
234
+ connected: true,
235
+ latency: data.session_start_limit?.remaining ?? undefined,
236
+ };
237
+ } catch {
238
+ return { connected: false };
239
+ }
240
+ }
241
+
242
+ async stop(): Promise<void> {
243
+ if (this.heartbeatInterval) {
244
+ clearInterval(this.heartbeatInterval);
245
+ }
246
+ if (this.wsConnection) {
247
+ this.wsConnection.close();
248
+ }
249
+ await super.stop();
250
+ }
251
+
252
+ // Helper to register slash commands
253
+ async registerSlashCommands(commands: Array<{
254
+ name: string;
255
+ description: string;
256
+ options?: any[];
257
+ }>): Promise<void> {
258
+ if (!this.config.guildId) {
259
+ logger.warn("[Discord] guildId required for slash commands");
260
+ return;
261
+ }
262
+
263
+ await this.apiRequest(`/applications/${this.getBotId()}/guilds/${this.config.guildId}/commands`, {
264
+ method: "PUT",
265
+ body: JSON.stringify(commands),
266
+ });
267
+
268
+ logger.info(`[Discord] Registered ${commands.length} slash commands`);
269
+ }
270
+ }
@@ -0,0 +1,13 @@
1
+ export { BaseAdapter, type PlatformMessage, type PlatformConfig, type AdapterCallbacks, type PlatformAdapter } from "./base.js";
2
+ export { DiscordAdapter } from "./discord.js";
3
+ export type { DiscordConfig } from "./discord.js";
4
+ export { TwitchAdapter } from "./twitch.js";
5
+ export type { TwitchConfig } from "./twitch.js";
6
+ export { TelegramAdapter } from "./telegram.js";
7
+ export type { TelegramConfig } from "./telegram.js";
8
+ export { SlackAdapter } from "./slack.js";
9
+ export type { SlackConfig } from "./slack.js";
10
+ export { WhatsAppAdapter } from "./whatsapp.js";
11
+ export type { WhatsAppConfig } from "./whatsapp.js";
12
+ export { WebSocketAdapter } from "./websocket.js";
13
+ export type { WebSocketConfig } from "./websocket.js";
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Slack Adapter - Hermes-style Slack platform adapter
3
+ *
4
+ * Features:
5
+ * - Incoming webhooks (outbound only)
6
+ * - Web API (with bot token)
7
+ * - Slash command support
8
+ * - Block Kit support
9
+ */
10
+
11
+ import { BaseAdapter, type PlatformMessage, type PlatformConfig } from "./base.js";
12
+ import { logger } from "../logger.js";
13
+
14
+ export interface SlackConfig extends PlatformConfig {
15
+ platform: "slack";
16
+ webhookUrl?: string;
17
+ botToken?: string;
18
+ signingSecret?: string;
19
+ teamId?: string;
20
+ defaultChannel?: string;
21
+ }
22
+
23
+ interface SlackMessage {
24
+ type: string;
25
+ channel?: string;
26
+ user?: string;
27
+ username?: string;
28
+ text: string;
29
+ ts: string;
30
+ thread_ts?: string;
31
+ }
32
+
33
+ export class SlackAdapter extends BaseAdapter {
34
+ readonly platform = "slack" as const;
35
+ config: SlackConfig;
36
+
37
+ private connected = false;
38
+
39
+ constructor(config: SlackConfig) {
40
+ super();
41
+ this.config = {
42
+ enabled: true,
43
+ platform: "slack",
44
+ ...config,
45
+ };
46
+ }
47
+
48
+ async initialize(): Promise<void> {
49
+ if (this.config.botToken) {
50
+ // Test bot token
51
+ const response = await this.apiRequest("auth.test", { method: "POST" });
52
+ const data = await response.json() as { ok: boolean; team_id?: string };
53
+
54
+ if (!data.ok) {
55
+ throw new Error(`Slack auth failed: ${data}`);
56
+ }
57
+
58
+ logger.info(`[Slack] Bot initialized for team: ${data.team_id}`);
59
+ } else if (this.config.webhookUrl) {
60
+ logger.info("[Slack] Using webhook mode (outbound only)");
61
+ } else {
62
+ throw new Error("Slack requires either botToken or webhookUrl");
63
+ }
64
+ }
65
+
66
+ private async apiRequest(endpoint: string, options: RequestInit = {}): Promise<Response> {
67
+ const url = `https://slack.com/api/${endpoint}`;
68
+ return fetch(url, {
69
+ ...options,
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ "Authorization": `Bearer ${this.config.botToken}`,
73
+ ...options.headers,
74
+ },
75
+ });
76
+ }
77
+
78
+ async start(callbacks): Promise<void> {
79
+ await super.start(callbacks);
80
+ this.connected = true;
81
+
82
+ if (!this.config.botToken && !this.config.webhookUrl) {
83
+ logger.warn("[Slack] No credentials configured - adapter is outbound-only");
84
+ }
85
+ }
86
+
87
+ async stop(): Promise<void> {
88
+ this.connected = false;
89
+ await super.stop();
90
+ }
91
+
92
+ async sendMessage(channelId: string, content: string): Promise<string> {
93
+ // Try webhook first if available
94
+ if (this.config.webhookUrl) {
95
+ const payload = {
96
+ text: content,
97
+ ...(channelId && channelId.startsWith("#") ? { channel: channelId } : {}),
98
+ };
99
+
100
+ const response = await fetch(this.config.webhookUrl, {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify(payload),
104
+ });
105
+
106
+ if (!response.ok) {
107
+ throw new Error(`Slack webhook failed: ${response.status}`);
108
+ }
109
+
110
+ return `webhook-${Date.now()}`;
111
+ }
112
+
113
+ // Use Web API if we have a bot token
114
+ if (this.config.botToken) {
115
+ const response = await this.apiRequest("chat.postMessage", {
116
+ method: "POST",
117
+ body: JSON.stringify({
118
+ channel: channelId,
119
+ text: content,
120
+ }),
121
+ });
122
+
123
+ const data = await response.json() as { ok: boolean; ts?: string; error?: string };
124
+
125
+ if (!data.ok) {
126
+ throw new Error(`Slack API error: ${data.error}`);
127
+ }
128
+
129
+ return data.ts || String(Date.now());
130
+ }
131
+
132
+ throw new Error("No Slack credentials configured");
133
+ }
134
+
135
+ async postMessage(channelId: string, content: string, blocks?: any[]): Promise<string> {
136
+ if (!this.config.botToken) {
137
+ throw new Error("Bot token required for rich messages");
138
+ }
139
+
140
+ const body: Record<string, any> = {
141
+ channel: channelId,
142
+ text: content,
143
+ };
144
+
145
+ if (blocks) {
146
+ body.blocks = blocks;
147
+ }
148
+
149
+ const response = await this.apiRequest("chat.postMessage", {
150
+ method: "POST",
151
+ body: JSON.stringify(body),
152
+ });
153
+
154
+ const data = await response.json() as { ok: boolean; ts?: string; error?: string };
155
+
156
+ if (!data.ok) {
157
+ throw new Error(`Slack API error: ${data.error}`);
158
+ }
159
+
160
+ return data.ts || String(Date.now());
161
+ }
162
+
163
+ async replyToThread(channelId: string, threadTs: string, content: string): Promise<string> {
164
+ if (!this.config.botToken) {
165
+ throw new Error("Bot token required for threaded replies");
166
+ }
167
+
168
+ const response = await this.apiRequest("chat.postMessage", {
169
+ method: "POST",
170
+ body: JSON.stringify({
171
+ channel: channelId,
172
+ text: content,
173
+ thread_ts: threadTs,
174
+ }),
175
+ });
176
+
177
+ const data = await response.json() as { ok: boolean; ts?: string; error?: string };
178
+
179
+ if (!data.ok) {
180
+ throw new Error(`Slack API error: ${data.error}`);
181
+ }
182
+
183
+ return data.ts || String(Date.now());
184
+ }
185
+
186
+ async editMessage(channelId: string, messageTs: string, content: string): Promise<void> {
187
+ if (!this.config.botToken) {
188
+ throw new Error("Bot token required for editing");
189
+ }
190
+
191
+ await this.apiRequest("chat.update", {
192
+ method: "POST",
193
+ body: JSON.stringify({
194
+ channel: channelId,
195
+ ts: messageTs,
196
+ text: content,
197
+ }),
198
+ });
199
+ }
200
+
201
+ async deleteMessage(channelId: string, messageTs: string): Promise<void> {
202
+ if (!this.config.botToken) {
203
+ throw new Error("Bot token required for deletion");
204
+ }
205
+
206
+ await this.apiRequest("chat.delete", {
207
+ method: "POST",
208
+ body: JSON.stringify({
209
+ channel: channelId,
210
+ ts: messageTs,
211
+ }),
212
+ });
213
+ }
214
+
215
+ async setTyping(channelId: string, isTyping: boolean): Promise<void> {
216
+ if (!this.config.botToken) return;
217
+
218
+ await this.apiRequest("chat.postEphemeral", {
219
+ method: "POST",
220
+ body: JSON.stringify({
221
+ channel: channelId,
222
+ text: isTyping ? "_typing_" : "",
223
+ }),
224
+ });
225
+ }
226
+
227
+ async getChannelInfo(channelId: string): Promise<{
228
+ id: string;
229
+ name: string;
230
+ numMembers: number;
231
+ topic: string;
232
+ } | null> {
233
+ if (!this.config.botToken) {
234
+ throw new Error("Bot token required");
235
+ }
236
+
237
+ const response = await this.apiRequest("conversations.info", {
238
+ method: "POST",
239
+ body: JSON.stringify({ channel: channelId }),
240
+ });
241
+
242
+ const data = await response.json() as {
243
+ ok: boolean;
244
+ channel?: {
245
+ id: string;
246
+ name: string;
247
+ num_members: number;
248
+ topic?: { value: string };
249
+ };
250
+ };
251
+
252
+ if (!data.ok || !data.channel) {
253
+ return null;
254
+ }
255
+
256
+ return {
257
+ id: data.channel.id,
258
+ name: data.channel.name,
259
+ numMembers: data.channel.num_members,
260
+ topic: data.channel.topic?.value || "",
261
+ };
262
+ }
263
+
264
+ async listChannels(): Promise<Array<{ id: string; name: string }>> {
265
+ if (!this.config.botToken) {
266
+ throw new Error("Bot token required");
267
+ }
268
+
269
+ const response = await this.apiRequest("conversations.list", {
270
+ method: "POST",
271
+ body: JSON.stringify({ types: "public_channel,private_channel" }),
272
+ });
273
+
274
+ const data = await response.json() as {
275
+ ok: boolean;
276
+ channels?: Array<{ id: string; name: string }>;
277
+ };
278
+
279
+ if (!data.ok) {
280
+ return [];
281
+ }
282
+
283
+ return (data.channels || []).map(c => ({ id: c.id, name: c.name }));
284
+ }
285
+
286
+ async getStatus(): Promise<{ connected: boolean; latency?: number }> {
287
+ return {
288
+ connected: this.connected,
289
+ ...(this.config.botToken ? { mode: "api" } : { mode: "webhook" })
290
+ };
291
+ }
292
+
293
+ // Handle incoming events (for WebSocket or Socket Mode)
294
+ async handleIncomingEvent(event: any): Promise<void> {
295
+ if (event.type === "message" && !event.subtype) {
296
+ const message: PlatformMessage = {
297
+ id: this.generateMessageId(),
298
+ platform: "slack",
299
+ channelId: event.channel,
300
+ userId: event.user,
301
+ content: event.text,
302
+ timestamp: parseFloat(event.ts) * 1000,
303
+ metadata: {
304
+ team: event.team,
305
+ threadTs: event.thread_ts,
306
+ username: event.username,
307
+ },
308
+ };
309
+
310
+ this.emitMessage(message);
311
+ }
312
+ }
313
+ }