@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.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/cli/init.js +387 -0
- package/dist/cli.js +184 -0
- package/dist/clients/telegram.js +148 -0
- package/dist/core/agent/factory.js +67 -0
- package/dist/core/agent/index.js +32 -0
- package/dist/core/agent/providers/claude-code.js +158 -0
- package/dist/core/agent/types.js +9 -0
- package/dist/core/approval.js +192 -0
- package/dist/core/executor.js +237 -0
- package/dist/core/logger.js +76 -0
- package/dist/core/platform.js +130 -0
- package/dist/gateway/gateway.js +435 -0
- package/dist/gateway/index.js +9 -0
- package/dist/gateway/security.js +35 -0
- package/dist/gateway/session.js +195 -0
- package/dist/gateway/types.js +8 -0
- package/dist/index.js +130 -0
- package/dist/mcp/server.js +156 -0
- package/dist/telegram/bot.js +333 -0
- package/install.sh +817 -0
- package/package.json +73 -0
- package/uninstall.sh +121 -0
|
@@ -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;
|