@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,395 @@
1
+ /**
2
+ * Background Task Manager - Hermes-style background session handling
3
+ *
4
+ * Features:
5
+ * - Isolated sessions for background tasks
6
+ * - Result delivery to parent session
7
+ * - Progress notifications
8
+ * - Timeout handling
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 { spawn, type ChildProcess } from "node:child_process";
16
+ import { createBackgroundSession } from "../sessions/store.js";
17
+ import { randomBytes } from "node:crypto";
18
+ import { logger } from "../logger.js";
19
+
20
+ export type BackgroundStatus =
21
+ | "running"
22
+ | "completed"
23
+ | "failed"
24
+ | "timeout"
25
+ | "delivered";
26
+
27
+ export interface BackgroundTask {
28
+ id: string;
29
+ sessionId: string;
30
+ parentSessionId: string;
31
+ command: string;
32
+ status: BackgroundStatus;
33
+ progress: number;
34
+ progressMessage?: string;
35
+ result?: unknown;
36
+ error?: string;
37
+ createdAt: number;
38
+ startedAt?: number;
39
+ completedAt?: number;
40
+ deliveredAt?: number;
41
+ }
42
+
43
+ interface TaskRow {
44
+ id: string;
45
+ session_id: string;
46
+ parent_session_id: string;
47
+ command: string;
48
+ status: string;
49
+ progress: number;
50
+ progress_message: string | null;
51
+ result: string | null;
52
+ error: string | null;
53
+ created_at: number;
54
+ started_at: number | null;
55
+ completed_at: number | null;
56
+ delivered_at: number | null;
57
+ }
58
+
59
+ const GATEWAY_DIR = join(homedir(), ".pi");
60
+ const TASKS_DB = join(GATEWAY_DIR, "gateway-background-tasks.db");
61
+
62
+ let db: Database.Database | null = null;
63
+ const runningProcesses: Map<string, ChildProcess> = new Map();
64
+ const progressCallbacks: Map<string, (task: BackgroundTask) => void> =
65
+ new Map();
66
+
67
+ /**
68
+ * Initialize background tasks database
69
+ */
70
+ export function initBackgroundTasks(): Database.Database {
71
+ if (db) return db;
72
+
73
+ if (!existsSync(GATEWAY_DIR)) {
74
+ mkdirSync(GATEWAY_DIR, { recursive: true });
75
+ }
76
+
77
+ db = new Database(TASKS_DB);
78
+ db.exec("PRAGMA journal_mode = WAL;");
79
+
80
+ // Tasks table
81
+ db.exec(`
82
+ CREATE TABLE IF NOT EXISTS background_tasks (
83
+ id TEXT PRIMARY KEY,
84
+ session_id TEXT NOT NULL,
85
+ parent_session_id TEXT NOT NULL,
86
+ command TEXT NOT NULL,
87
+ status TEXT NOT NULL DEFAULT 'running',
88
+ progress INTEGER NOT NULL DEFAULT 0,
89
+ progress_message TEXT,
90
+ result TEXT,
91
+ error TEXT,
92
+ created_at INTEGER NOT NULL,
93
+ started_at INTEGER,
94
+ completed_at INTEGER,
95
+ delivered_at INTEGER
96
+ )
97
+ `);
98
+
99
+ // Indexes
100
+ db.exec(
101
+ `CREATE INDEX IF NOT EXISTS idx_tasks_parent ON background_tasks(parent_session_id)`,
102
+ );
103
+ db.exec(
104
+ `CREATE INDEX IF NOT EXISTS idx_tasks_status ON background_tasks(status)`,
105
+ );
106
+
107
+ logger.info("[BackgroundTasks] Database initialized");
108
+ return db;
109
+ }
110
+
111
+ /**
112
+ * Start a background task
113
+ */
114
+ export function startBackgroundTask(
115
+ parentSessionId: string,
116
+ command: string,
117
+ onProgress?: (task: BackgroundTask) => void,
118
+ ): BackgroundTask {
119
+ const database = initBackgroundTasks();
120
+
121
+ const id = `bg-${Date.now()}-${randomBytes(4).toString("hex")}`;
122
+ const now = Date.now();
123
+
124
+ // Create background session
125
+ const session = createBackgroundSession(
126
+ "background",
127
+ id,
128
+ "system",
129
+ parentSessionId,
130
+ );
131
+
132
+ // Create task record
133
+ const task: BackgroundTask = {
134
+ id,
135
+ sessionId: session.id,
136
+ parentSessionId,
137
+ command,
138
+ status: "running",
139
+ progress: 0,
140
+ createdAt: now,
141
+ };
142
+
143
+ database
144
+ .prepare(`
145
+ INSERT INTO background_tasks
146
+ (id, session_id, parent_session_id, command, status, progress, created_at)
147
+ VALUES (?, ?, ?, ?, ?, ?, ?)
148
+ `)
149
+ .run(id, session.id, parentSessionId, command, "running", 0, now);
150
+
151
+ // Register progress callback
152
+ if (onProgress) {
153
+ progressCallbacks.set(id, onProgress);
154
+ }
155
+
156
+ // Start the actual process
157
+ const proc = spawn("pi", ["--mode", "json", "--print", command], {
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ env: { ...process.env },
160
+ });
161
+
162
+ runningProcesses.set(id, proc);
163
+
164
+ let stdout = "";
165
+ let stderr = "";
166
+
167
+ proc.stdout?.on("data", (data: Buffer) => {
168
+ stdout += data.toString();
169
+
170
+ // Parse progress updates
171
+ try {
172
+ const lines = stdout.split("\n").filter(Boolean);
173
+ for (const line of lines) {
174
+ const parsed = JSON.parse(line);
175
+ if (parsed.progress !== undefined) {
176
+ updateTaskProgress(id, parsed.progress, parsed.message);
177
+ }
178
+ }
179
+ } catch {
180
+ // Not JSON yet
181
+ }
182
+ });
183
+
184
+ proc.stderr?.on("data", (data: Buffer) => {
185
+ stderr += data.toString();
186
+ });
187
+
188
+ proc.on("close", (code) => {
189
+ runningProcesses.delete(id);
190
+
191
+ if (code === 0) {
192
+ try {
193
+ const result = stdout.trim() ? JSON.parse(stdout) : { success: true };
194
+ completeTask(id, result);
195
+ } catch {
196
+ completeTask(id, { success: true, output: stdout });
197
+ }
198
+ } else {
199
+ failTask(id, stderr || `Process exited with code ${code}`);
200
+ }
201
+ });
202
+
203
+ proc.on("error", (err) => {
204
+ runningProcesses.delete(id);
205
+ failTask(id, err.message);
206
+ });
207
+
208
+ logger.info(`[BackgroundTasks] Started task ${id.slice(0, 12)}...`);
209
+ return task;
210
+ }
211
+
212
+ /**
213
+ * Update task progress
214
+ */
215
+ export function updateTaskProgress(
216
+ taskId: string,
217
+ progress: number,
218
+ message?: string,
219
+ ): void {
220
+ const database = initBackgroundTasks();
221
+
222
+ database
223
+ .prepare(`
224
+ UPDATE background_tasks SET progress = ?, progress_message = ?
225
+ WHERE id = ?
226
+ `)
227
+ .run(progress, message ?? null, taskId);
228
+
229
+ // Notify via callback
230
+ const callback = progressCallbacks.get(taskId);
231
+ if (callback) {
232
+ const task = getTask(taskId);
233
+ if (task) callback(task);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Complete a task successfully
239
+ */
240
+ export function completeTask(taskId: string, result: unknown): void {
241
+ const database = initBackgroundTasks();
242
+ const now = Date.now();
243
+
244
+ database
245
+ .prepare(`
246
+ UPDATE background_tasks
247
+ SET status = 'completed', progress = 100, result = ?, completed_at = ?
248
+ WHERE id = ?
249
+ `)
250
+ .run(JSON.stringify(result), now, taskId);
251
+
252
+ logger.info(`[BackgroundTasks] Task ${taskId.slice(0, 12)}... completed`);
253
+ }
254
+
255
+ /**
256
+ * Fail a task
257
+ */
258
+ export function failTask(taskId: string, error: string): void {
259
+ const database = initBackgroundTasks();
260
+ const now = Date.now();
261
+
262
+ database
263
+ .prepare(`
264
+ UPDATE background_tasks
265
+ SET status = 'failed', error = ?, completed_at = ?
266
+ WHERE id = ?
267
+ `)
268
+ .run(error, now, taskId);
269
+
270
+ logger.info(
271
+ `[BackgroundTasks] Task ${taskId.slice(0, 12)}... failed: ${error}`,
272
+ );
273
+ }
274
+
275
+ /**
276
+ * Mark task as delivered to user
277
+ */
278
+ export function markTaskDelivered(taskId: string): void {
279
+ const database = initBackgroundTasks();
280
+ database
281
+ .prepare(`
282
+ UPDATE background_tasks SET status = 'delivered', delivered_at = ?
283
+ WHERE id = ?
284
+ `)
285
+ .run(Date.now(), taskId);
286
+ }
287
+
288
+ /**
289
+ * Get task by ID
290
+ */
291
+ export function getTask(taskId: string): BackgroundTask | null {
292
+ const database = initBackgroundTasks();
293
+ const row = database
294
+ .prepare("SELECT * FROM background_tasks WHERE id = ?")
295
+ .get(taskId) as TaskRow | undefined;
296
+ return row ? rowToTask(row) : null;
297
+ }
298
+
299
+ /**
300
+ * Get pending results for parent session
301
+ */
302
+ export function getPendingResultsForSession(
303
+ parentSessionId: string,
304
+ ): BackgroundTask[] {
305
+ const database = initBackgroundTasks();
306
+
307
+ const rows = database
308
+ .prepare(`
309
+ SELECT * FROM background_tasks
310
+ WHERE parent_session_id = ? AND status IN ('completed', 'failed')
311
+ AND delivered_at IS NULL
312
+ ORDER BY created_at ASC
313
+ `)
314
+ .all(parentSessionId) as TaskRow[];
315
+
316
+ return rows.map(rowToTask);
317
+ }
318
+
319
+ /**
320
+ * List all tasks
321
+ */
322
+ export function listTasks(status?: BackgroundStatus): BackgroundTask[] {
323
+ const database = initBackgroundTasks();
324
+
325
+ const query = status
326
+ ? "SELECT * FROM background_tasks WHERE status = ? ORDER BY created_at DESC"
327
+ : "SELECT * FROM background_tasks ORDER BY created_at DESC";
328
+
329
+ const rows = status
330
+ ? (database.prepare(query).all(status) as TaskRow[])
331
+ : (database.prepare(query).all() as TaskRow[]);
332
+
333
+ return rows.map(rowToTask);
334
+ }
335
+
336
+ /**
337
+ * Cancel a running task
338
+ */
339
+ export function cancelTask(taskId: string): boolean {
340
+ const proc = runningProcesses.get(taskId);
341
+ if (proc) {
342
+ proc.kill("SIGTERM");
343
+ runningProcesses.delete(taskId);
344
+
345
+ const database = initBackgroundTasks();
346
+ database
347
+ .prepare(`
348
+ UPDATE background_tasks SET status = 'failed', error = ?, completed_at = ?
349
+ WHERE id = ?
350
+ `)
351
+ .run("Cancelled by user", Date.now(), taskId);
352
+
353
+ return true;
354
+ }
355
+ return false;
356
+ }
357
+
358
+ /**
359
+ * Clean up old tasks
360
+ */
361
+ export function cleanupOldTasks(
362
+ maxAgeMs: number = 7 * 24 * 60 * 60 * 1000,
363
+ ): number {
364
+ const database = initBackgroundTasks();
365
+ const cutoff = Date.now() - maxAgeMs;
366
+ const result = database
367
+ .prepare("DELETE FROM background_tasks WHERE created_at < ?")
368
+ .run(cutoff);
369
+ return result.changes;
370
+ }
371
+
372
+ // Helper to convert DB row to BackgroundTask
373
+ function rowToTask(row: TaskRow): BackgroundTask {
374
+ return {
375
+ id: row.id,
376
+ sessionId: row.session_id,
377
+ parentSessionId: row.parent_session_id,
378
+ command: row.command,
379
+ status: row.status as BackgroundStatus,
380
+ progress: row.progress,
381
+ progressMessage: row.progress_message ?? undefined,
382
+ result: (() => {
383
+ try {
384
+ return row.result ? JSON.parse(row.result) : undefined;
385
+ } catch {
386
+ return undefined;
387
+ }
388
+ })(),
389
+ error: row.error ?? undefined,
390
+ createdAt: row.created_at,
391
+ startedAt: row.started_at ?? undefined,
392
+ completedAt: row.completed_at ?? undefined,
393
+ deliveredAt: row.delivered_at ?? undefined,
394
+ };
395
+ }