@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,360 @@
1
+ /**
2
+ * Session Store - Hermes-style per-chat session management
3
+ *
4
+ * Features:
5
+ * - Per-chat sessions with unique IDs
6
+ * - Reset policies: daily (hour-based) and idle (minutes-based)
7
+ * - Session persistence across restarts
8
+ * - Background session isolation
9
+ */
10
+
11
+ import Database from "better-sqlite3";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+ import { existsSync, mkdirSync } from "fs";
15
+ import { logger } from "../logger.js";
16
+
17
+ export type ResetPolicy = "daily" | "idle" | "both";
18
+
19
+ export interface SessionConfig {
20
+ id: string;
21
+ platform: string;
22
+ channelId: string;
23
+ userId: string;
24
+ resetPolicy: ResetPolicy;
25
+ dailyHour: number; // Hour (0-23) for daily reset
26
+ idleMinutes: number; // Minutes for idle reset
27
+ lastActivity: number; // Timestamp of last activity
28
+ createdAt: number;
29
+ isBackground: boolean;
30
+ parentSessionId?: string; // For background task tracking
31
+ }
32
+
33
+ interface SessionRow {
34
+ id: string;
35
+ platform: string;
36
+ channel_id: string;
37
+ user_id: string;
38
+ reset_policy: string;
39
+ daily_hour: number;
40
+ idle_minutes: number;
41
+ last_activity: number;
42
+ created_at: number;
43
+ is_background: number;
44
+ parent_session_id: string | null;
45
+ }
46
+
47
+ const GATEWAY_DIR = join(homedir(), ".pi");
48
+ const SESSIONS_DB = join(GATEWAY_DIR, "gateway-sessions.db");
49
+
50
+ let db: Database.Database | null = null;
51
+
52
+ /**
53
+ * Initialize session database
54
+ */
55
+ export function initSessionStore(): Database.Database {
56
+ if (db) return db;
57
+
58
+ if (!existsSync(GATEWAY_DIR)) {
59
+ mkdirSync(GATEWAY_DIR, { recursive: true });
60
+ }
61
+
62
+ db = new Database(SESSIONS_DB);
63
+ db.exec("PRAGMA journal_mode = WAL;");
64
+
65
+ // Sessions table
66
+ db.exec(`
67
+ CREATE TABLE IF NOT EXISTS sessions (
68
+ id TEXT PRIMARY KEY,
69
+ platform TEXT NOT NULL,
70
+ channel_id TEXT NOT NULL,
71
+ user_id TEXT NOT NULL,
72
+ reset_policy TEXT NOT NULL DEFAULT 'idle',
73
+ daily_hour INTEGER NOT NULL DEFAULT 4,
74
+ idle_minutes INTEGER NOT NULL DEFAULT 1440,
75
+ last_activity INTEGER NOT NULL,
76
+ created_at INTEGER NOT NULL,
77
+ is_background INTEGER NOT NULL DEFAULT 0,
78
+ parent_session_id TEXT,
79
+ FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
80
+ )
81
+ `);
82
+
83
+ // Indexes for fast lookups
84
+ db.exec(
85
+ `CREATE INDEX IF NOT EXISTS idx_sessions_platform_channel ON sessions(platform, channel_id)`,
86
+ );
87
+ db.exec(
88
+ `CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)`,
89
+ );
90
+ db.exec(
91
+ `CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity)`,
92
+ );
93
+
94
+ logger.info("[SessionStore] Database initialized");
95
+ return db;
96
+ }
97
+
98
+ /**
99
+ * Generate unique session ID
100
+ */
101
+ export function generateSessionId(): string {
102
+ return `sess-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
103
+ }
104
+
105
+ /**
106
+ * Get or create session for a platform/channel
107
+ */
108
+ export function getOrCreateSession(
109
+ platform: string,
110
+ channelId: string,
111
+ userId: string,
112
+ config?: Partial<SessionConfig>,
113
+ ): SessionConfig {
114
+ const database = initSessionStore();
115
+
116
+ // Try to find existing active session
117
+ const existing = database
118
+ .prepare(`
119
+ SELECT * FROM sessions
120
+ WHERE platform = ? AND channel_id = ? AND is_background = 0
121
+ ORDER BY last_activity DESC
122
+ LIMIT 1
123
+ `)
124
+ .get(platform, channelId) as SessionRow | undefined;
125
+
126
+ if (existing) {
127
+ // Check if session needs reset
128
+ if (shouldResetSession(existing)) {
129
+ // Delete old session, create fresh one
130
+ database.prepare("DELETE FROM sessions WHERE id = ?").run(existing.id);
131
+ } else {
132
+ // Update last activity and return
133
+ database
134
+ .prepare("UPDATE sessions SET last_activity = ? WHERE id = ?")
135
+ .run(Date.now(), existing.id);
136
+ return rowToSession(existing);
137
+ }
138
+ }
139
+
140
+ // Create new session
141
+ const id = generateSessionId();
142
+ const now = Date.now();
143
+
144
+ const session: SessionConfig = {
145
+ id,
146
+ platform,
147
+ channelId,
148
+ userId,
149
+ resetPolicy: config?.resetPolicy ?? "idle",
150
+ dailyHour: config?.dailyHour ?? 4,
151
+ idleMinutes: config?.idleMinutes ?? 1440,
152
+ lastActivity: now,
153
+ createdAt: now,
154
+ isBackground: false,
155
+ ...config,
156
+ };
157
+
158
+ database
159
+ .prepare(`
160
+ INSERT INTO sessions (id, platform, channel_id, user_id, reset_policy, daily_hour, idle_minutes, last_activity, created_at, is_background, parent_session_id)
161
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
162
+ `)
163
+ .run(
164
+ session.id,
165
+ session.platform,
166
+ session.channelId,
167
+ session.userId,
168
+ session.resetPolicy,
169
+ session.dailyHour,
170
+ session.idleMinutes,
171
+ session.lastActivity,
172
+ session.createdAt,
173
+ session.isBackground ? 1 : 0,
174
+ session.parentSessionId ?? null,
175
+ );
176
+
177
+ logger.info(
178
+ `[SessionStore] Created session ${id.slice(0, 12)}... for ${platform}/${channelId}`,
179
+ );
180
+ return session;
181
+ }
182
+
183
+ /**
184
+ * Create a background session (isolated from parent)
185
+ */
186
+ export function createBackgroundSession(
187
+ platform: string,
188
+ channelId: string,
189
+ userId: string,
190
+ parentSessionId?: string,
191
+ ): SessionConfig {
192
+ const database = initSessionStore();
193
+
194
+ const id = generateSessionId();
195
+ const now = Date.now();
196
+
197
+ const session: SessionConfig = {
198
+ id,
199
+ platform,
200
+ channelId,
201
+ userId,
202
+ resetPolicy: "idle",
203
+ dailyHour: 4,
204
+ idleMinutes: 1440,
205
+ lastActivity: now,
206
+ createdAt: now,
207
+ isBackground: true,
208
+ parentSessionId,
209
+ };
210
+
211
+ database
212
+ .prepare(`
213
+ INSERT INTO sessions (id, platform, channel_id, user_id, reset_policy, daily_hour, idle_minutes, last_activity, created_at, is_background, parent_session_id)
214
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
215
+ `)
216
+ .run(
217
+ session.id,
218
+ session.platform,
219
+ session.channelId,
220
+ session.userId,
221
+ session.resetPolicy,
222
+ session.dailyHour,
223
+ session.idleMinutes,
224
+ session.lastActivity,
225
+ session.createdAt,
226
+ 1, // is_background
227
+ session.parentSessionId ?? null,
228
+ );
229
+
230
+ logger.info(
231
+ `[SessionStore] Created background session ${id.slice(0, 12)}...`,
232
+ );
233
+ return session;
234
+ }
235
+
236
+ /**
237
+ * Check if session should be reset
238
+ */
239
+ function shouldResetSession(row: SessionRow): boolean {
240
+ const now = Date.now();
241
+
242
+ // Check idle timeout
243
+ const idleMs = row.idle_minutes * 60 * 1000;
244
+ if (now - row.last_activity > idleMs) {
245
+ logger.info(
246
+ `[SessionStore] Session ${row.id.slice(0, 8)} reset: idle timeout`,
247
+ );
248
+ return true;
249
+ }
250
+
251
+ // Check daily reset
252
+ if (row.reset_policy === "daily" || row.reset_policy === "both") {
253
+ const lastActivity = new Date(row.last_activity);
254
+ const nowDate = new Date(now);
255
+
256
+ // Check if we crossed the daily reset hour since last activity
257
+ if (
258
+ lastActivity.getHours() < row.daily_hour &&
259
+ nowDate.getHours() >= row.daily_hour
260
+ ) {
261
+ logger.info(
262
+ `[SessionStore] Session ${row.id.slice(0, 8)} reset: daily at ${row.daily_hour}:00`,
263
+ );
264
+ return true;
265
+ }
266
+ }
267
+
268
+ return false;
269
+ }
270
+
271
+ /**
272
+ * Update session last activity
273
+ */
274
+ export function touchSession(sessionId: string): void {
275
+ const database = initSessionStore();
276
+ database
277
+ .prepare("UPDATE sessions SET last_activity = ? WHERE id = ?")
278
+ .run(Date.now(), sessionId);
279
+ }
280
+
281
+ /**
282
+ * Get session by ID
283
+ */
284
+ export function getSession(sessionId: string): SessionConfig | null {
285
+ const database = initSessionStore();
286
+ const row = database
287
+ .prepare("SELECT * FROM sessions WHERE id = ?")
288
+ .get(sessionId) as SessionRow | undefined;
289
+ return row ? rowToSession(row) : null;
290
+ }
291
+
292
+ /**
293
+ * Delete session
294
+ */
295
+ export function deleteSession(sessionId: string): void {
296
+ const database = initSessionStore();
297
+ database.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
298
+ }
299
+
300
+ /**
301
+ * List sessions by platform
302
+ */
303
+ export function listSessions(platform?: string): SessionConfig[] {
304
+ const database = initSessionStore();
305
+ const query = platform
306
+ ? "SELECT * FROM sessions WHERE platform = ? AND is_background = 0 ORDER BY last_activity DESC"
307
+ : "SELECT * FROM sessions WHERE is_background = 0 ORDER BY last_activity DESC";
308
+
309
+ const rows = platform
310
+ ? (database.prepare(query).all(platform) as SessionRow[])
311
+ : (database.prepare(query).all() as SessionRow[]);
312
+
313
+ return rows.map(rowToSession);
314
+ }
315
+
316
+ /**
317
+ * Clean up stale sessions
318
+ */
319
+ export function cleanupStaleSessions(): number {
320
+ const database = initSessionStore();
321
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days
322
+ const result = database
323
+ .prepare("DELETE FROM sessions WHERE last_activity < ?")
324
+ .run(cutoff);
325
+ return result.changes;
326
+ }
327
+
328
+ /**
329
+ * Get background sessions for delivery
330
+ */
331
+ export function getPendingBackgroundResults(): SessionConfig[] {
332
+ const database = initSessionStore();
333
+ // Background sessions that exist but parent still needs delivery
334
+ const rows = database
335
+ .prepare(`
336
+ SELECT s.* FROM sessions s
337
+ WHERE s.is_background = 1
338
+ ORDER BY s.created_at ASC
339
+ `)
340
+ .all() as SessionRow[];
341
+
342
+ return rows.map(rowToSession);
343
+ }
344
+
345
+ // Helper to convert DB row to SessionConfig
346
+ function rowToSession(row: SessionRow): SessionConfig {
347
+ return {
348
+ id: row.id,
349
+ platform: row.platform,
350
+ channelId: row.channel_id,
351
+ userId: row.user_id,
352
+ resetPolicy: row.reset_policy as ResetPolicy,
353
+ dailyHour: row.daily_hour,
354
+ idleMinutes: row.idle_minutes,
355
+ lastActivity: row.last_activity,
356
+ createdAt: row.created_at,
357
+ isBackground: row.is_background === 1,
358
+ parentSessionId: row.parent_session_id ?? undefined,
359
+ };
360
+ }