@sarkar-ai/deskmate 0.2.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.
@@ -0,0 +1,435 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Gateway = void 0;
37
+ const logger_1 = require("../core/logger");
38
+ const executor_1 = require("../core/executor");
39
+ const approval_1 = require("../core/approval");
40
+ const agent_1 = require("../core/agent");
41
+ const platform_1 = require("../core/platform");
42
+ const security_1 = require("./security");
43
+ const session_1 = require("./session");
44
+ const log = (0, logger_1.createLogger)("Gateway");
45
+ const SCREENSHOT_DIR = process.env.TMPDIR
46
+ ? `${process.env.TMPDIR}deskmate-screenshots`
47
+ : "/tmp/deskmate-screenshots";
48
+ class Gateway {
49
+ clients = new Map();
50
+ security;
51
+ sessions;
52
+ executor;
53
+ agentProvider;
54
+ config;
55
+ systemPrompt;
56
+ constructor(config) {
57
+ this.config = config;
58
+ this.security = new security_1.SecurityManager(config.allowedUsers);
59
+ this.sessions = new session_1.SessionManager({
60
+ storagePath: config.storagePath,
61
+ });
62
+ this.executor = new executor_1.Executor(config.workingDir);
63
+ this.agentProvider = (0, agent_1.createAgentProvider)();
64
+ this.systemPrompt =
65
+ config.systemPrompt ||
66
+ `You are a local machine assistant named ${config.botName}. Users will ask you to perform tasks on their computer.
67
+
68
+ You have access to tools to execute commands, read/write files, and explore the filesystem. Use them to help users accomplish their tasks.
69
+
70
+ SCREENSHOT CAPABILITY:
71
+ When the user asks to see the screen, take a screenshot, or wants visual feedback, use this command:
72
+ ${(0, platform_1.getScreenshotHint)(SCREENSHOT_DIR)}
73
+ The screenshot will automatically be sent to the user after your response.
74
+
75
+ IMPORTANT RULES:
76
+ - Be concise in your responses
77
+ - Use the available tools to accomplish tasks
78
+ - For dangerous operations, explain what you're about to do before doing it
79
+ - Never use sudo unless explicitly asked
80
+ - Keep responses under 4000 characters
81
+ - When asked for screenshots, always use the screenshot command above`;
82
+ }
83
+ registerClient(client) {
84
+ if (this.clients.has(client.clientType)) {
85
+ throw new Error(`Client type "${client.clientType}" is already registered`);
86
+ }
87
+ this.clients.set(client.clientType, client);
88
+ log.info("Client registered", { clientType: client.clientType });
89
+ }
90
+ async start() {
91
+ if (this.clients.size === 0) {
92
+ throw new Error("No clients registered. Register at least one MessagingClient before starting.");
93
+ }
94
+ // Verify agent provider
95
+ log.info("Using agent provider", {
96
+ name: this.agentProvider.name,
97
+ version: this.agentProvider.version,
98
+ });
99
+ const available = await this.agentProvider.isAvailable();
100
+ if (!available) {
101
+ log.warn("Agent provider may not be fully available", { provider: this.agentProvider.name });
102
+ }
103
+ // Register approval notifier that broadcasts to clients with recent activity
104
+ approval_1.approvalManager.addNotifier(async (action) => {
105
+ await this.broadcastApproval(action);
106
+ });
107
+ // Start all clients
108
+ for (const client of this.clients.values()) {
109
+ await client.start(this);
110
+ log.info("Client started", { clientType: client.clientType });
111
+ }
112
+ log.info("Gateway started", { clients: Array.from(this.clients.keys()) });
113
+ }
114
+ async stop() {
115
+ for (const client of this.clients.values()) {
116
+ await client.stop();
117
+ }
118
+ this.sessions.stop();
119
+ if (this.agentProvider.cleanup) {
120
+ await this.agentProvider.cleanup();
121
+ }
122
+ log.info("Gateway stopped");
123
+ }
124
+ // ── MessageHandler implementation ───────────────────────────────────
125
+ async handleMessage(msg) {
126
+ // Auth check
127
+ if (!this.security.isAuthorized(msg.clientType, msg.userId)) {
128
+ log.debug("Unauthorized message dropped", {
129
+ clientType: msg.clientType,
130
+ userId: msg.userId,
131
+ });
132
+ return;
133
+ }
134
+ if (msg.command) {
135
+ await this.handleCommand(msg);
136
+ }
137
+ else {
138
+ await this.handleAgentQuery(msg);
139
+ }
140
+ }
141
+ async handleApproval(response, channelId) {
142
+ const { actionId, approved } = response;
143
+ let success;
144
+ if (approved) {
145
+ success = approval_1.approvalManager.approve(actionId);
146
+ }
147
+ else {
148
+ success = approval_1.approvalManager.reject(actionId);
149
+ }
150
+ // Update approval UI on all clients that have the channel
151
+ for (const client of this.clients.values()) {
152
+ try {
153
+ await client.updateApprovalStatus(channelId, actionId, approved);
154
+ }
155
+ catch {
156
+ // Client may not own this channel — ignore
157
+ }
158
+ }
159
+ if (!success) {
160
+ log.warn("Approval action not found", { actionId });
161
+ }
162
+ }
163
+ // ── Commands ────────────────────────────────────────────────────────
164
+ async handleCommand(msg) {
165
+ const client = this.clients.get(msg.clientType);
166
+ if (!client)
167
+ return;
168
+ switch (msg.command) {
169
+ case "start":
170
+ await client.sendMessage({
171
+ channelId: msg.channelId,
172
+ text: `*${this.config.botName} Ready*\n\n` +
173
+ "Send me any task and I'll execute it on your local machine.\n\n" +
174
+ "I remember our conversation, so you can ask follow-up questions!\n\n" +
175
+ "*Examples:*\n" +
176
+ "- `list all docker containers`\n" +
177
+ "- `what's using port 3000?`\n" +
178
+ "- `show disk usage`\n" +
179
+ "- `take a screenshot`\n\n" +
180
+ "*Commands:*\n" +
181
+ "- /screenshot - Take a screenshot\n" +
182
+ "- /status - System info\n" +
183
+ "- /reset - Clear memory & start fresh",
184
+ parseMode: "markdown",
185
+ });
186
+ break;
187
+ case "screenshot":
188
+ await this.handleScreenshotCommand(msg, client);
189
+ break;
190
+ case "status":
191
+ await this.handleStatusCommand(msg, client);
192
+ break;
193
+ case "reset":
194
+ await this.handleResetCommand(msg, client);
195
+ break;
196
+ default:
197
+ log.debug("Unknown command", { command: msg.command });
198
+ }
199
+ }
200
+ async handleScreenshotCommand(msg, client) {
201
+ await client.sendMessage({ channelId: msg.channelId, text: "Taking screenshot..." });
202
+ try {
203
+ const filepath = await this.executor.takeScreenshot();
204
+ if (filepath) {
205
+ await client.sendMessage({
206
+ channelId: msg.channelId,
207
+ image: filepath,
208
+ imageCaption: "Screenshot",
209
+ });
210
+ // Clean up
211
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
212
+ await fs.unlink(filepath).catch(() => { });
213
+ }
214
+ else {
215
+ await client.sendMessage({
216
+ channelId: msg.channelId,
217
+ text: "Screenshot failed.",
218
+ });
219
+ }
220
+ }
221
+ catch (error) {
222
+ log.error("Screenshot command failed", { error: error.message });
223
+ await client.sendMessage({
224
+ channelId: msg.channelId,
225
+ text: `Screenshot failed: ${error.message}`,
226
+ });
227
+ }
228
+ }
229
+ async handleStatusCommand(msg, client) {
230
+ const pending = approval_1.approvalManager.getPendingActions();
231
+ const info = await this.executor.getSystemInfo();
232
+ const hasSession = this.sessions.has(msg.clientType, msg.channelId);
233
+ await client.sendMessage({
234
+ channelId: msg.channelId,
235
+ text: `*System Status*\n\n` +
236
+ `- Host: ${info.hostname || "unknown"}\n` +
237
+ `- Platform: ${info.platform}\n` +
238
+ `- Agent: ${this.agentProvider.name} v${this.agentProvider.version}\n` +
239
+ `- Pending approvals: ${pending.length}\n` +
240
+ `- Working dir: \`${this.executor.getWorkingDir()}\`\n` +
241
+ `- Session active: ${hasSession ? "Yes" : "No"}`,
242
+ parseMode: "markdown",
243
+ });
244
+ }
245
+ async handleResetCommand(msg, client) {
246
+ const had = this.sessions.delete(msg.clientType, msg.channelId);
247
+ log.info("Session reset", { clientType: msg.clientType, channelId: msg.channelId, had });
248
+ await client.sendMessage({
249
+ channelId: msg.channelId,
250
+ text: had
251
+ ? "Session cleared! Starting fresh conversation."
252
+ : "No active session to clear.",
253
+ });
254
+ }
255
+ // ── Agent query loop ────────────────────────────────────────────────
256
+ async handleAgentQuery(msg) {
257
+ const client = this.clients.get(msg.clientType);
258
+ if (!client)
259
+ return;
260
+ // Send "Thinking..." placeholder
261
+ const thinkingId = await client.sendMessage({
262
+ channelId: msg.channelId,
263
+ text: "Thinking...",
264
+ });
265
+ const executionStartTime = new Date();
266
+ const existingSessionId = this.sessions.get(msg.clientType, msg.channelId);
267
+ log.info("Received message", {
268
+ clientType: msg.clientType,
269
+ userId: msg.userId,
270
+ channelId: msg.channelId,
271
+ hasSession: !!existingSessionId,
272
+ message: msg.text.slice(0, 100),
273
+ });
274
+ try {
275
+ let result = "";
276
+ let lastUpdate = Date.now();
277
+ let newSessionId;
278
+ for await (const event of this.agentProvider.queryStream(msg.text, {
279
+ systemPrompt: this.systemPrompt,
280
+ workingDir: this.config.workingDir,
281
+ sessionId: existingSessionId,
282
+ maxTurns: this.config.maxTurns ?? 10,
283
+ })) {
284
+ switch (event.type) {
285
+ case "text":
286
+ if (event.text)
287
+ result = event.text;
288
+ break;
289
+ case "tool_use":
290
+ log.debug("Tool use", { tool: event.toolName });
291
+ break;
292
+ case "done":
293
+ if (event.response) {
294
+ result = event.response.text;
295
+ newSessionId = event.response.sessionId;
296
+ }
297
+ break;
298
+ case "error":
299
+ throw new Error(event.error || "Unknown agent error");
300
+ }
301
+ // Periodic progress update
302
+ if (thinkingId && Date.now() - lastUpdate > 3000) {
303
+ try {
304
+ await client.sendMessage({
305
+ channelId: msg.channelId,
306
+ text: "Working...",
307
+ editMessageId: thinkingId,
308
+ });
309
+ lastUpdate = Date.now();
310
+ }
311
+ catch {
312
+ // Ignore edit errors
313
+ }
314
+ }
315
+ }
316
+ // Store session
317
+ if (newSessionId) {
318
+ this.sessions.set(msg.clientType, msg.channelId, newSessionId);
319
+ log.info("Session stored", { channelId: msg.channelId, sessionId: newSessionId });
320
+ }
321
+ // Send final result
322
+ const finalMessage = result || "Task completed (no output)";
323
+ const truncated = finalMessage.slice(0, 4000);
324
+ log.info("Agent completed", { resultLength: finalMessage.length, hasSession: !!newSessionId });
325
+ // Try markdown first, fall back to plain
326
+ try {
327
+ await client.sendMessage({
328
+ channelId: msg.channelId,
329
+ text: truncated,
330
+ editMessageId: thinkingId,
331
+ parseMode: "markdown",
332
+ });
333
+ }
334
+ catch {
335
+ await client.sendMessage({
336
+ channelId: msg.channelId,
337
+ text: truncated,
338
+ editMessageId: thinkingId,
339
+ parseMode: "plain",
340
+ });
341
+ }
342
+ // Send any screenshots taken during execution
343
+ await this.sendScreenshots(client, msg.channelId, executionStartTime);
344
+ }
345
+ catch (error) {
346
+ log.error("Agent error", { error: error.message });
347
+ if (error.message?.includes("session")) {
348
+ this.sessions.delete(msg.clientType, msg.channelId);
349
+ log.warn("Cleared invalid session", { channelId: msg.channelId });
350
+ }
351
+ await client.sendMessage({
352
+ channelId: msg.channelId,
353
+ text: `Error: ${error.message}`,
354
+ editMessageId: thinkingId,
355
+ });
356
+ }
357
+ }
358
+ // ── Screenshots ─────────────────────────────────────────────────────
359
+ async sendScreenshots(client, channelId, since) {
360
+ try {
361
+ const screenshots = await this.executor.getRecentScreenshots(since);
362
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
363
+ for (const filepath of screenshots) {
364
+ await client.sendMessage({
365
+ channelId,
366
+ image: filepath,
367
+ imageCaption: "Screenshot",
368
+ });
369
+ await fs.unlink(filepath).catch(() => { });
370
+ }
371
+ if (screenshots.length > 0) {
372
+ log.info("Screenshots sent", { count: screenshots.length });
373
+ }
374
+ }
375
+ catch (error) {
376
+ log.error("Failed to send screenshots", { error: error.message });
377
+ }
378
+ }
379
+ // ── Approval broadcasting ───────────────────────────────────────────
380
+ async broadcastApproval(action) {
381
+ const timeLeft = Math.ceil((action.expiresAt.getTime() - Date.now()) / 1000);
382
+ let details = "";
383
+ switch (action.type) {
384
+ case "command":
385
+ details = `Command: \`${action.details.command}\``;
386
+ break;
387
+ case "write_file":
388
+ details = `Path: \`${action.details.path}\`\nPreview: ${(action.details.contentPreview || "").slice(0, 100)}...`;
389
+ break;
390
+ case "folder_access":
391
+ details = `Folder: \`${action.details.baseFolder}\`\nFile: \`${action.details.path}\``;
392
+ break;
393
+ case "read_file":
394
+ details = `Path: \`${action.details.path}\``;
395
+ break;
396
+ default:
397
+ details = JSON.stringify(action.details, null, 2);
398
+ }
399
+ // Send to all clients that have recently-active channels
400
+ const recentChannels = this.sessions.getRecentChannels(30 * 60 * 1000);
401
+ // Group by clientType
402
+ const channelsByClient = new Map();
403
+ for (const { clientType, channelId } of recentChannels) {
404
+ if (!channelsByClient.has(clientType)) {
405
+ channelsByClient.set(clientType, []);
406
+ }
407
+ channelsByClient.get(clientType).push(channelId);
408
+ }
409
+ for (const [clientType, channelIds] of channelsByClient) {
410
+ const client = this.clients.get(clientType);
411
+ if (!client)
412
+ continue;
413
+ for (const channelId of channelIds) {
414
+ try {
415
+ await client.sendApprovalPrompt({
416
+ channelId,
417
+ actionId: action.id,
418
+ type: action.type,
419
+ description: action.description,
420
+ details,
421
+ expiresInSeconds: timeLeft,
422
+ });
423
+ }
424
+ catch (error) {
425
+ log.error("Failed to send approval prompt", {
426
+ clientType,
427
+ channelId,
428
+ error: error.message,
429
+ });
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ exports.Gateway = Gateway;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionManager = exports.SecurityManager = exports.Gateway = void 0;
4
+ var gateway_1 = require("./gateway");
5
+ Object.defineProperty(exports, "Gateway", { enumerable: true, get: function () { return gateway_1.Gateway; } });
6
+ var security_1 = require("./security");
7
+ Object.defineProperty(exports, "SecurityManager", { enumerable: true, get: function () { return security_1.SecurityManager; } });
8
+ var session_1 = require("./session");
9
+ Object.defineProperty(exports, "SessionManager", { enumerable: true, get: function () { return session_1.SessionManager; } });
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SecurityManager = void 0;
4
+ const logger_1 = require("../core/logger");
5
+ const log = (0, logger_1.createLogger)("SecurityManager");
6
+ class SecurityManager {
7
+ /** Map<clientType, Set<platformUserId>> — wildcard "*" matches any user */
8
+ allowList = new Map();
9
+ constructor(allowedUsers) {
10
+ for (const u of allowedUsers) {
11
+ this.addUser(u);
12
+ }
13
+ log.info("SecurityManager initialized", { userCount: allowedUsers.length });
14
+ }
15
+ addUser(identity) {
16
+ const { clientType, platformUserId } = identity;
17
+ if (!this.allowList.has(clientType)) {
18
+ this.allowList.set(clientType, new Set());
19
+ }
20
+ this.allowList.get(clientType).add(platformUserId);
21
+ log.debug("User added to allowlist", { clientType, platformUserId });
22
+ }
23
+ isAuthorized(clientType, platformUserId) {
24
+ // Check wildcard first
25
+ const wildcardSet = this.allowList.get("*");
26
+ if (wildcardSet?.has("*") || wildcardSet?.has(platformUserId)) {
27
+ return true;
28
+ }
29
+ const clientSet = this.allowList.get(clientType);
30
+ if (!clientSet)
31
+ return false;
32
+ return clientSet.has("*") || clientSet.has(platformUserId);
33
+ }
34
+ }
35
+ exports.SecurityManager = SecurityManager;
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SessionManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const logger_1 = require("../core/logger");
40
+ const log = (0, logger_1.createLogger)("SessionManager");
41
+ class SessionManager {
42
+ /** Composite key: `clientType:channelId` -> session */
43
+ sessions = new Map();
44
+ idleTimeoutMs;
45
+ pruneInterval = null;
46
+ storagePath;
47
+ saveTimer = null;
48
+ static DEBOUNCE_MS = 500;
49
+ constructor(options) {
50
+ const idleTimeoutMs = options?.idleTimeoutMs ?? 30 * 60 * 1000;
51
+ this.idleTimeoutMs = idleTimeoutMs;
52
+ this.storagePath = options?.storagePath ?? null;
53
+ if (this.storagePath) {
54
+ this.loadFromDisk();
55
+ }
56
+ this.pruneInterval = setInterval(() => this.pruneIdle(), 60_000);
57
+ log.info("SessionManager initialized", { idleTimeoutMs, storagePath: this.storagePath });
58
+ }
59
+ key(clientType, channelId) {
60
+ return `${clientType}:${channelId}`;
61
+ }
62
+ get(clientType, channelId) {
63
+ const entry = this.sessions.get(this.key(clientType, channelId));
64
+ if (entry) {
65
+ entry.lastActivity = new Date();
66
+ this.scheduleSave();
67
+ return entry.agentSessionId;
68
+ }
69
+ return undefined;
70
+ }
71
+ set(clientType, channelId, agentSessionId) {
72
+ const k = this.key(clientType, channelId);
73
+ this.sessions.set(k, {
74
+ agentSessionId,
75
+ createdAt: new Date(),
76
+ lastActivity: new Date(),
77
+ });
78
+ log.debug("Session stored", { key: k, agentSessionId });
79
+ this.scheduleSave();
80
+ }
81
+ delete(clientType, channelId) {
82
+ const k = this.key(clientType, channelId);
83
+ const had = this.sessions.has(k);
84
+ this.sessions.delete(k);
85
+ if (had) {
86
+ log.debug("Session deleted", { key: k });
87
+ this.scheduleSave();
88
+ }
89
+ return had;
90
+ }
91
+ has(clientType, channelId) {
92
+ return this.sessions.has(this.key(clientType, channelId));
93
+ }
94
+ /** Remove sessions that have been idle longer than idleTimeoutMs */
95
+ pruneIdle() {
96
+ const now = Date.now();
97
+ let pruned = 0;
98
+ for (const [k, entry] of this.sessions) {
99
+ if (now - entry.lastActivity.getTime() > this.idleTimeoutMs) {
100
+ this.sessions.delete(k);
101
+ pruned++;
102
+ }
103
+ }
104
+ if (pruned > 0) {
105
+ log.info("Pruned idle sessions", { pruned, remaining: this.sessions.size });
106
+ this.scheduleSave();
107
+ }
108
+ }
109
+ /** Get all channel IDs with recent activity (within the last N ms) */
110
+ getRecentChannels(withinMs) {
111
+ const cutoff = Date.now() - withinMs;
112
+ const result = [];
113
+ for (const [k, entry] of this.sessions) {
114
+ if (entry.lastActivity.getTime() >= cutoff) {
115
+ const [clientType, ...rest] = k.split(":");
116
+ result.push({ clientType, channelId: rest.join(":") });
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+ stop() {
122
+ if (this.pruneInterval) {
123
+ clearInterval(this.pruneInterval);
124
+ this.pruneInterval = null;
125
+ }
126
+ if (this.saveTimer) {
127
+ clearTimeout(this.saveTimer);
128
+ this.saveTimer = null;
129
+ }
130
+ // Flush any pending save synchronously on stop
131
+ if (this.storagePath) {
132
+ this.saveToDiskSync();
133
+ }
134
+ }
135
+ // ── Persistence ──────────────────────────────────────────────
136
+ loadFromDisk() {
137
+ if (!this.storagePath)
138
+ return;
139
+ try {
140
+ const raw = fs.readFileSync(this.storagePath, "utf-8");
141
+ const data = JSON.parse(raw);
142
+ for (const [key, entry] of Object.entries(data)) {
143
+ this.sessions.set(key, {
144
+ agentSessionId: entry.agentSessionId,
145
+ createdAt: new Date(entry.createdAt),
146
+ lastActivity: new Date(entry.lastActivity),
147
+ });
148
+ }
149
+ log.info("Loaded sessions from disk", { count: this.sessions.size, path: this.storagePath });
150
+ }
151
+ catch (err) {
152
+ if (err.code === "ENOENT") {
153
+ log.info("No existing session file, starting fresh", { path: this.storagePath });
154
+ }
155
+ else {
156
+ log.warn("Failed to load sessions from disk, starting fresh", { error: err.message, path: this.storagePath });
157
+ }
158
+ }
159
+ }
160
+ serialize() {
161
+ const data = {};
162
+ for (const [key, entry] of this.sessions) {
163
+ data[key] = {
164
+ agentSessionId: entry.agentSessionId,
165
+ createdAt: entry.createdAt.toISOString(),
166
+ lastActivity: entry.lastActivity.toISOString(),
167
+ };
168
+ }
169
+ return data;
170
+ }
171
+ saveToDiskSync() {
172
+ if (!this.storagePath)
173
+ return;
174
+ try {
175
+ const dir = path.dirname(this.storagePath);
176
+ fs.mkdirSync(dir, { recursive: true });
177
+ fs.writeFileSync(this.storagePath, JSON.stringify(this.serialize(), null, 2));
178
+ }
179
+ catch (err) {
180
+ log.error("Failed to save sessions to disk", { error: err.message, path: this.storagePath });
181
+ }
182
+ }
183
+ scheduleSave() {
184
+ if (!this.storagePath)
185
+ return;
186
+ if (this.saveTimer) {
187
+ clearTimeout(this.saveTimer);
188
+ }
189
+ this.saveTimer = setTimeout(() => {
190
+ this.saveTimer = null;
191
+ this.saveToDiskSync();
192
+ }, SessionManager.DEBOUNCE_MS);
193
+ }
194
+ }
195
+ exports.SessionManager = SessionManager;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Gateway Types
4
+ *
5
+ * Shared interfaces for the multi-client gateway architecture.
6
+ * Platform adapters implement MessagingClient; the Gateway implements MessageHandler.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });