devmentorai-server 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.
- package/README.md +141 -0
- package/dist/chunk-QFZAYHDT.js +241 -0
- package/dist/chunk-QFZAYHDT.js.map +1 -0
- package/dist/cli.js +452 -0
- package/dist/cli.js.map +1 -0
- package/dist/server.js +2704 -0
- package/dist/server.js.map +1 -0
- package/package.json +81 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,2704 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONFIG,
|
|
3
|
+
IMAGES_DIR,
|
|
4
|
+
SESSION_TYPE_CONFIGS,
|
|
5
|
+
deleteDir,
|
|
6
|
+
ensureDir,
|
|
7
|
+
fileExists,
|
|
8
|
+
formatDate,
|
|
9
|
+
generateMessageId,
|
|
10
|
+
generateSessionId,
|
|
11
|
+
getAgentConfig,
|
|
12
|
+
getDefaultModel,
|
|
13
|
+
getMessageImagesDir,
|
|
14
|
+
getSessionImagesDir,
|
|
15
|
+
getThumbnailPath,
|
|
16
|
+
toImageRelativePath,
|
|
17
|
+
toRelativePath,
|
|
18
|
+
toUrlPath
|
|
19
|
+
} from "./chunk-QFZAYHDT.js";
|
|
20
|
+
|
|
21
|
+
// src/server.ts
|
|
22
|
+
import Fastify from "fastify";
|
|
23
|
+
import cors from "@fastify/cors";
|
|
24
|
+
|
|
25
|
+
// src/routes/health.ts
|
|
26
|
+
var startTime = Date.now();
|
|
27
|
+
async function healthRoutes(fastify) {
|
|
28
|
+
fastify.get("/health", async (_request, reply) => {
|
|
29
|
+
const copilotService = fastify.copilotService;
|
|
30
|
+
const healthData = {
|
|
31
|
+
status: copilotService.isReady() ? "healthy" : "degraded",
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
copilotConnected: copilotService.isReady() && !copilotService.isMockMode(),
|
|
34
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
35
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
36
|
+
};
|
|
37
|
+
return reply.send({
|
|
38
|
+
success: true,
|
|
39
|
+
data: healthData
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/routes/sessions.ts
|
|
45
|
+
import { z } from "zod";
|
|
46
|
+
|
|
47
|
+
// src/services/thumbnail-service.ts
|
|
48
|
+
import sharp from "sharp";
|
|
49
|
+
import fs from "fs";
|
|
50
|
+
import path from "path";
|
|
51
|
+
var THUMBNAIL_CONFIG = {
|
|
52
|
+
maxDimension: 200,
|
|
53
|
+
quality: 60,
|
|
54
|
+
format: "jpeg"
|
|
55
|
+
};
|
|
56
|
+
function parseDataUrl(dataUrl) {
|
|
57
|
+
const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const [, mimeType, base64Data] = match;
|
|
60
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
61
|
+
return { buffer, mimeType };
|
|
62
|
+
}
|
|
63
|
+
async function getImageDimensions(buffer) {
|
|
64
|
+
const metadata = await sharp(buffer).metadata();
|
|
65
|
+
return {
|
|
66
|
+
width: metadata.width || 0,
|
|
67
|
+
height: metadata.height || 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function generateThumbnail(buffer) {
|
|
71
|
+
return sharp(buffer).resize(THUMBNAIL_CONFIG.maxDimension, THUMBNAIL_CONFIG.maxDimension, {
|
|
72
|
+
fit: "inside",
|
|
73
|
+
withoutEnlargement: true
|
|
74
|
+
}).jpeg({ quality: THUMBNAIL_CONFIG.quality }).toBuffer();
|
|
75
|
+
}
|
|
76
|
+
function getExtensionForMimeType(mimeType) {
|
|
77
|
+
const extensions = {
|
|
78
|
+
"image/jpeg": "jpg",
|
|
79
|
+
"image/png": "png",
|
|
80
|
+
"image/webp": "webp",
|
|
81
|
+
"image/gif": "gif"
|
|
82
|
+
};
|
|
83
|
+
return extensions[mimeType] || "jpg";
|
|
84
|
+
}
|
|
85
|
+
function getFullImagePath(sessionId, messageId, index, extension) {
|
|
86
|
+
const messageDir = getMessageImagesDir(sessionId, messageId);
|
|
87
|
+
return path.join(messageDir, `image_${index}.${extension}`);
|
|
88
|
+
}
|
|
89
|
+
async function processMessageImages(sessionId, messageId, images, backendUrl) {
|
|
90
|
+
if (!images || images.length === 0) return [];
|
|
91
|
+
const messageDir = getMessageImagesDir(sessionId, messageId);
|
|
92
|
+
ensureDir(messageDir);
|
|
93
|
+
const processedImages = [];
|
|
94
|
+
for (let index = 0; index < images.length; index++) {
|
|
95
|
+
const image = images[index];
|
|
96
|
+
try {
|
|
97
|
+
const parsed = parseDataUrl(image.dataUrl);
|
|
98
|
+
if (!parsed) {
|
|
99
|
+
console.error(`[ThumbnailService] Failed to parse data URL for image ${image.id}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const { buffer, mimeType } = parsed;
|
|
103
|
+
const dimensions = await getImageDimensions(buffer);
|
|
104
|
+
const thumbnailBuffer = await generateThumbnail(buffer);
|
|
105
|
+
const thumbnailAbsPath = getThumbnailPath(sessionId, messageId, index);
|
|
106
|
+
fs.writeFileSync(thumbnailAbsPath, thumbnailBuffer);
|
|
107
|
+
const extension = getExtensionForMimeType(mimeType);
|
|
108
|
+
const fullImageAbsPath = getFullImagePath(sessionId, messageId, index, extension);
|
|
109
|
+
fs.writeFileSync(fullImageAbsPath, buffer);
|
|
110
|
+
console.log(`[ThumbnailService] Saved full image to ${fullImageAbsPath}`);
|
|
111
|
+
const thumbnailRelativePath = toRelativePath(thumbnailAbsPath);
|
|
112
|
+
const thumbnailUrlPath = toUrlPath(toImageRelativePath(thumbnailAbsPath));
|
|
113
|
+
const fullImageUrlPath = toUrlPath(toImageRelativePath(fullImageAbsPath));
|
|
114
|
+
processedImages.push({
|
|
115
|
+
id: image.id,
|
|
116
|
+
source: image.source,
|
|
117
|
+
mimeType: image.mimeType,
|
|
118
|
+
dimensions,
|
|
119
|
+
fileSize: buffer.length,
|
|
120
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
121
|
+
thumbnailUrl: `${backendUrl}/api/images/${thumbnailUrlPath}`,
|
|
122
|
+
thumbnailPath: thumbnailRelativePath,
|
|
123
|
+
fullImagePath: fullImageAbsPath,
|
|
124
|
+
fullImageUrl: `${backendUrl}/api/images/${fullImageUrlPath}`
|
|
125
|
+
});
|
|
126
|
+
console.log(`[ThumbnailService] Processed image ${index + 1}/${images.length} for message ${messageId}`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`[ThumbnailService] Failed to process image ${image.id}:`, error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return processedImages;
|
|
132
|
+
}
|
|
133
|
+
function getThumbnailFilePath(sessionId, messageId, index) {
|
|
134
|
+
const thumbnailPath = getThumbnailPath(sessionId, messageId, index);
|
|
135
|
+
return fileExists(thumbnailPath) ? thumbnailPath : null;
|
|
136
|
+
}
|
|
137
|
+
function deleteSessionImages(sessionId) {
|
|
138
|
+
const sessionDir = getSessionImagesDir(sessionId);
|
|
139
|
+
deleteDir(sessionDir);
|
|
140
|
+
console.log(`[ThumbnailService] Deleted images for session ${sessionId}`);
|
|
141
|
+
}
|
|
142
|
+
function toImageAttachments(processedImages) {
|
|
143
|
+
return processedImages.map((img) => ({
|
|
144
|
+
id: img.id,
|
|
145
|
+
source: img.source,
|
|
146
|
+
mimeType: img.mimeType,
|
|
147
|
+
dimensions: img.dimensions,
|
|
148
|
+
fileSize: img.fileSize,
|
|
149
|
+
timestamp: img.timestamp,
|
|
150
|
+
thumbnailUrl: img.thumbnailUrl,
|
|
151
|
+
fullImageUrl: img.fullImageUrl
|
|
152
|
+
// Note: dataUrl is NOT included - it's only used during processing
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/routes/sessions.ts
|
|
157
|
+
var createSessionSchema = z.object({
|
|
158
|
+
name: z.string().min(1).max(100),
|
|
159
|
+
type: z.enum(["devops", "writing", "development", "general"]),
|
|
160
|
+
model: z.string().optional(),
|
|
161
|
+
systemPrompt: z.string().optional()
|
|
162
|
+
});
|
|
163
|
+
var updateSessionSchema = z.object({
|
|
164
|
+
name: z.string().min(1).max(100).optional(),
|
|
165
|
+
status: z.enum(["active", "paused", "closed"]).optional()
|
|
166
|
+
});
|
|
167
|
+
async function sessionRoutes(fastify) {
|
|
168
|
+
fastify.get("/sessions", async (request, reply) => {
|
|
169
|
+
const page = parseInt(request.query.page || "1", 10);
|
|
170
|
+
const pageSize = parseInt(request.query.pageSize || "50", 10);
|
|
171
|
+
const sessions = fastify.sessionService.listSessions(page, pageSize);
|
|
172
|
+
return reply.send({
|
|
173
|
+
success: true,
|
|
174
|
+
data: sessions
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
fastify.post("/sessions", async (request, reply) => {
|
|
178
|
+
try {
|
|
179
|
+
const body = createSessionSchema.parse(request.body);
|
|
180
|
+
console.log("[sessionRoutes] Creating session with body:", body);
|
|
181
|
+
const session = fastify.sessionService.createSession(body);
|
|
182
|
+
await fastify.copilotService.createCopilotSession(
|
|
183
|
+
session.id,
|
|
184
|
+
session.type,
|
|
185
|
+
session.model,
|
|
186
|
+
session.systemPrompt
|
|
187
|
+
);
|
|
188
|
+
return reply.code(201).send({
|
|
189
|
+
success: true,
|
|
190
|
+
data: session
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof z.ZodError) {
|
|
194
|
+
return reply.code(400).send({
|
|
195
|
+
success: false,
|
|
196
|
+
error: {
|
|
197
|
+
code: "VALIDATION_ERROR",
|
|
198
|
+
message: "Invalid request body",
|
|
199
|
+
details: { errors: error.errors }
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
fastify.get("/sessions/:id", async (request, reply) => {
|
|
207
|
+
const session = fastify.sessionService.getSession(request.params.id);
|
|
208
|
+
if (!session) {
|
|
209
|
+
return reply.code(404).send({
|
|
210
|
+
success: false,
|
|
211
|
+
error: {
|
|
212
|
+
code: "NOT_FOUND",
|
|
213
|
+
message: "Session not found"
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return reply.send({
|
|
218
|
+
success: true,
|
|
219
|
+
data: session
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
fastify.patch("/sessions/:id", async (request, reply) => {
|
|
223
|
+
try {
|
|
224
|
+
const body = updateSessionSchema.parse(request.body);
|
|
225
|
+
const session = fastify.sessionService.updateSession(request.params.id, body);
|
|
226
|
+
if (!session) {
|
|
227
|
+
return reply.code(404).send({
|
|
228
|
+
success: false,
|
|
229
|
+
error: {
|
|
230
|
+
code: "NOT_FOUND",
|
|
231
|
+
message: "Session not found"
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return reply.send({
|
|
236
|
+
success: true,
|
|
237
|
+
data: session
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error instanceof z.ZodError) {
|
|
241
|
+
return reply.code(400).send({
|
|
242
|
+
success: false,
|
|
243
|
+
error: {
|
|
244
|
+
code: "VALIDATION_ERROR",
|
|
245
|
+
message: "Invalid request body",
|
|
246
|
+
details: { errors: error.errors }
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
fastify.delete("/sessions/:id", async (request, reply) => {
|
|
254
|
+
const sessionId = request.params.id;
|
|
255
|
+
console.log(`[SessionRoute] Deleting session: ${sessionId}`);
|
|
256
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
257
|
+
if (!session) {
|
|
258
|
+
return reply.code(404).send({
|
|
259
|
+
success: false,
|
|
260
|
+
error: {
|
|
261
|
+
code: "NOT_FOUND",
|
|
262
|
+
message: "Session not found"
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
await fastify.copilotService.destroySession(sessionId);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(`[SessionRoute] Error destroying Copilot session:`, error);
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
deleteSessionImages(sessionId);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(`[SessionRoute] Error deleting session images:`, error);
|
|
275
|
+
}
|
|
276
|
+
const deleted = fastify.sessionService.deleteSession(sessionId);
|
|
277
|
+
console.log(`[SessionRoute] Session ${sessionId} deleted: ${deleted}`);
|
|
278
|
+
return reply.code(200).send({
|
|
279
|
+
success: true,
|
|
280
|
+
data: { deleted, sessionId }
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
fastify.post("/sessions/:id/resume", async (request, reply) => {
|
|
284
|
+
const sessionId = request.params.id;
|
|
285
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
286
|
+
if (!session) {
|
|
287
|
+
return reply.code(404).send({
|
|
288
|
+
success: false,
|
|
289
|
+
error: {
|
|
290
|
+
code: "NOT_FOUND",
|
|
291
|
+
message: "Session not found"
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const resumed = await fastify.copilotService.resumeCopilotSession(sessionId);
|
|
296
|
+
if (!resumed) {
|
|
297
|
+
await fastify.copilotService.createCopilotSession(
|
|
298
|
+
session.id,
|
|
299
|
+
session.type,
|
|
300
|
+
session.model,
|
|
301
|
+
session.systemPrompt
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const updatedSession = fastify.sessionService.updateSession(sessionId, { status: "active" });
|
|
305
|
+
return reply.send({
|
|
306
|
+
success: true,
|
|
307
|
+
data: updatedSession
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
fastify.post("/sessions/:id/abort", async (request, reply) => {
|
|
311
|
+
await fastify.copilotService.abortRequest(request.params.id);
|
|
312
|
+
return reply.send({
|
|
313
|
+
success: true
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
fastify.get("/sessions/:id/messages", async (request, reply) => {
|
|
317
|
+
const session = fastify.sessionService.getSession(request.params.id);
|
|
318
|
+
if (!session) {
|
|
319
|
+
return reply.code(404).send({
|
|
320
|
+
success: false,
|
|
321
|
+
error: {
|
|
322
|
+
code: "NOT_FOUND",
|
|
323
|
+
message: "Session not found"
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const page = parseInt(request.query.page || "1", 10);
|
|
328
|
+
const pageSize = parseInt(request.query.pageSize || "100", 10);
|
|
329
|
+
const messages = fastify.sessionService.listMessages(request.params.id, page, pageSize);
|
|
330
|
+
return reply.send({
|
|
331
|
+
success: true,
|
|
332
|
+
data: messages
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/routes/chat.ts
|
|
338
|
+
import { z as z2 } from "zod";
|
|
339
|
+
|
|
340
|
+
// src/services/context-prompt-builder.ts
|
|
341
|
+
var PLATFORM_CONTEXT_NOTES = {
|
|
342
|
+
azure: `**Platform:** Azure Portal
|
|
343
|
+
Common diagnostic locations: Activity Log, Diagnose and solve problems, Resource health`,
|
|
344
|
+
aws: `**Platform:** AWS Console
|
|
345
|
+
Common diagnostic locations: CloudTrail, CloudWatch Logs, IAM policies`,
|
|
346
|
+
gcp: `**Platform:** Google Cloud Console
|
|
347
|
+
Common diagnostic locations: Cloud Logging, Error Reporting, IAM bindings`,
|
|
348
|
+
github: `**Platform:** GitHub
|
|
349
|
+
Common diagnostic locations: Actions tab, Issues, Pull Requests`,
|
|
350
|
+
gitlab: `**Platform:** GitLab
|
|
351
|
+
Common diagnostic locations: Pipelines, Merge Requests, Project settings`,
|
|
352
|
+
kubernetes: `**Platform:** Kubernetes Dashboard
|
|
353
|
+
Common diagnostic commands: kubectl logs, kubectl describe, kubectl get events`,
|
|
354
|
+
jenkins: `**Platform:** Jenkins
|
|
355
|
+
Common diagnostic locations: Console output, Pipeline syntax, Agent status`,
|
|
356
|
+
datadog: `**Platform:** Datadog
|
|
357
|
+
Common diagnostic locations: Metric graphs, Alert conditions, Integration status`,
|
|
358
|
+
grafana: `**Platform:** Grafana
|
|
359
|
+
Common diagnostic locations: Query editor, Data source config, Alert rules`,
|
|
360
|
+
generic: ``
|
|
361
|
+
};
|
|
362
|
+
function formatPageContext(page) {
|
|
363
|
+
const { platform } = page;
|
|
364
|
+
const platformLabel = platform.specificProduct || platform.type.toUpperCase();
|
|
365
|
+
let section = `## Page Context
|
|
366
|
+
`;
|
|
367
|
+
section += `- **Platform:** ${platformLabel}`;
|
|
368
|
+
if (platform.confidence < 0.8) {
|
|
369
|
+
section += ` (confidence: ${Math.round(platform.confidence * 100)}%)`;
|
|
370
|
+
}
|
|
371
|
+
section += "\n";
|
|
372
|
+
section += `- **URL:** ${page.url}
|
|
373
|
+
`;
|
|
374
|
+
section += `- **Title:** ${page.title}
|
|
375
|
+
`;
|
|
376
|
+
return section;
|
|
377
|
+
}
|
|
378
|
+
function formatErrors(errors) {
|
|
379
|
+
if (errors.length === 0) return "";
|
|
380
|
+
let section = `## Errors and Alerts on Page
|
|
381
|
+
`;
|
|
382
|
+
section += `${errors.length} issue(s) detected:
|
|
383
|
+
|
|
384
|
+
`;
|
|
385
|
+
for (const error of errors) {
|
|
386
|
+
section += `- **[${error.severity.toUpperCase()}]** ${error.message}
|
|
387
|
+
`;
|
|
388
|
+
if (error.source) section += ` Source: ${error.source}
|
|
389
|
+
`;
|
|
390
|
+
if (error.context) section += ` Context: ${error.context}
|
|
391
|
+
`;
|
|
392
|
+
if (error.stackTrace) section += ` Stack: ${error.stackTrace.slice(0, 200)}...
|
|
393
|
+
`;
|
|
394
|
+
section += "\n";
|
|
395
|
+
}
|
|
396
|
+
return section;
|
|
397
|
+
}
|
|
398
|
+
function formatHeadings(headings) {
|
|
399
|
+
if (headings.length === 0) return "";
|
|
400
|
+
let section = `## Page Structure (Headings)
|
|
401
|
+
`;
|
|
402
|
+
for (const heading of headings.slice(0, 10)) {
|
|
403
|
+
const indent = " ".repeat(heading.level - 1);
|
|
404
|
+
section += `${indent}- ${heading.text}
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
return section + "\n";
|
|
408
|
+
}
|
|
409
|
+
function formatRelevantSections(sections) {
|
|
410
|
+
if (sections.length === 0) return "";
|
|
411
|
+
let section = `## Relevant UI Elements
|
|
412
|
+
`;
|
|
413
|
+
for (const s of sections.slice(0, 5)) {
|
|
414
|
+
section += `### ${s.purpose.replace("-", " ").toUpperCase()}
|
|
415
|
+
`;
|
|
416
|
+
section += `\`\`\`
|
|
417
|
+
${s.textContent.slice(0, 300)}
|
|
418
|
+
\`\`\`
|
|
419
|
+
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
422
|
+
return section;
|
|
423
|
+
}
|
|
424
|
+
function formatCodeBlocks(codeBlocks) {
|
|
425
|
+
if (!codeBlocks || codeBlocks.length === 0) return "";
|
|
426
|
+
let section = `## Code Snippets Found on Page
|
|
427
|
+
`;
|
|
428
|
+
for (const block of codeBlocks.slice(0, 3)) {
|
|
429
|
+
const lang = block.attributes.detectedLanguage || "text";
|
|
430
|
+
section += `### ${lang.toUpperCase()}
|
|
431
|
+
`;
|
|
432
|
+
section += `\`\`\`${lang}
|
|
433
|
+
${block.textContent.slice(0, 500)}
|
|
434
|
+
\`\`\`
|
|
435
|
+
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
return section;
|
|
439
|
+
}
|
|
440
|
+
function formatTables(tables) {
|
|
441
|
+
if (!tables || tables.length === 0) return "";
|
|
442
|
+
let section = `## Data Tables
|
|
443
|
+
`;
|
|
444
|
+
for (const table of tables.slice(0, 2)) {
|
|
445
|
+
const rows = table.attributes.rowCount || "?";
|
|
446
|
+
const cols = table.attributes.columnCount || "?";
|
|
447
|
+
section += `### Table (${rows} rows \xD7 ${cols} cols)
|
|
448
|
+
`;
|
|
449
|
+
section += `\`\`\`
|
|
450
|
+
${table.textContent.slice(0, 400)}
|
|
451
|
+
\`\`\`
|
|
452
|
+
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
return section;
|
|
456
|
+
}
|
|
457
|
+
function formatConsoleLogs(logs) {
|
|
458
|
+
if (!logs || logs.length === 0) return "";
|
|
459
|
+
const errors = logs.filter((l) => l.level === "error" || l.level === "warn");
|
|
460
|
+
if (errors.length === 0) return "";
|
|
461
|
+
let section = `## Browser Console Logs
|
|
462
|
+
`;
|
|
463
|
+
section += `${errors.length} error/warning message(s):
|
|
464
|
+
|
|
465
|
+
`;
|
|
466
|
+
for (const log of errors.slice(0, 5)) {
|
|
467
|
+
section += `- **${log.level.toUpperCase()}:** ${log.message.slice(0, 200)}
|
|
468
|
+
`;
|
|
469
|
+
if (log.stackTrace) {
|
|
470
|
+
section += ` Stack: ${log.stackTrace.slice(0, 150)}...
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
section += "\n";
|
|
474
|
+
}
|
|
475
|
+
return section;
|
|
476
|
+
}
|
|
477
|
+
function formatNetworkErrors(errors) {
|
|
478
|
+
if (!errors || errors.length === 0) return "";
|
|
479
|
+
let section = `## Network Requests Failed
|
|
480
|
+
`;
|
|
481
|
+
section += `${errors.length} failed request(s):
|
|
482
|
+
|
|
483
|
+
`;
|
|
484
|
+
for (const err of errors.slice(0, 5)) {
|
|
485
|
+
const status = err.status ? `HTTP ${err.status}` : err.errorMessage || "Failed";
|
|
486
|
+
section += `- **${err.method}** ${err.url.slice(0, 80)}${err.url.length > 80 ? "..." : ""}
|
|
487
|
+
`;
|
|
488
|
+
section += ` Status: ${status}
|
|
489
|
+
|
|
490
|
+
`;
|
|
491
|
+
}
|
|
492
|
+
return section;
|
|
493
|
+
}
|
|
494
|
+
function formatModalContent(modal) {
|
|
495
|
+
if (!modal) return "";
|
|
496
|
+
let section = `## Active Modal/Dialog
|
|
497
|
+
`;
|
|
498
|
+
const title = modal.attributes.title || "Modal";
|
|
499
|
+
section += `### ${title}
|
|
500
|
+
`;
|
|
501
|
+
section += `\`\`\`
|
|
502
|
+
${modal.textContent.slice(0, 400)}
|
|
503
|
+
\`\`\`
|
|
504
|
+
|
|
505
|
+
`;
|
|
506
|
+
return section;
|
|
507
|
+
}
|
|
508
|
+
function formatPlatformSpecificContext(platformContext) {
|
|
509
|
+
if (!platformContext || Object.keys(platformContext).length === 0) return "";
|
|
510
|
+
let section = `## Platform-Specific Details
|
|
511
|
+
`;
|
|
512
|
+
for (const [key, value] of Object.entries(platformContext)) {
|
|
513
|
+
if (value !== void 0 && value !== null) {
|
|
514
|
+
section += `- **${key}:** ${String(value)}
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return section + "\n";
|
|
519
|
+
}
|
|
520
|
+
function formatUIState(uiState) {
|
|
521
|
+
let section = `## UI State
|
|
522
|
+
`;
|
|
523
|
+
section += `- **Page State:** ${uiState.pageState}
|
|
524
|
+
`;
|
|
525
|
+
const issues = [];
|
|
526
|
+
if (uiState.loadingIndicators > 0) issues.push(`${uiState.loadingIndicators} loading indicator(s)`);
|
|
527
|
+
if (uiState.errorStates > 0) issues.push(`${uiState.errorStates} error state(s)`);
|
|
528
|
+
if (uiState.emptyStates > 0) issues.push(`${uiState.emptyStates} empty state(s)`);
|
|
529
|
+
if (uiState.toastNotifications > 0) issues.push(`${uiState.toastNotifications} notification(s)`);
|
|
530
|
+
if (uiState.formValidationErrors > 0) issues.push(`${uiState.formValidationErrors} form validation error(s)`);
|
|
531
|
+
if (uiState.disabledButtons > 0) issues.push(`${uiState.disabledButtons} disabled button(s)`);
|
|
532
|
+
if (uiState.modalOpen) issues.push(`modal/dialog open`);
|
|
533
|
+
if (issues.length > 0) {
|
|
534
|
+
section += `- **UI Issues:** ${issues.join(", ")}
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
return section + "\n";
|
|
538
|
+
}
|
|
539
|
+
function formatRuntimeErrors(errors) {
|
|
540
|
+
if (errors.length === 0) return "";
|
|
541
|
+
let section = `## JavaScript Runtime Errors
|
|
542
|
+
`;
|
|
543
|
+
section += `${errors.length} runtime error(s) detected:
|
|
544
|
+
|
|
545
|
+
`;
|
|
546
|
+
for (const err of errors.slice(0, 5)) {
|
|
547
|
+
const errType = err.type === "unhandledrejection" ? "Promise Rejection" : "JS Error";
|
|
548
|
+
section += `- **${errType}:** ${err.message.slice(0, 200)}
|
|
549
|
+
`;
|
|
550
|
+
if (err.source) {
|
|
551
|
+
section += ` Source: ${err.source}${err.lineno ? `:${err.lineno}` : ""}${err.colno ? `:${err.colno}` : ""}
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
if (err.stack) {
|
|
555
|
+
section += ` Stack: ${err.stack.slice(0, 150)}...
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
section += "\n";
|
|
559
|
+
}
|
|
560
|
+
return section;
|
|
561
|
+
}
|
|
562
|
+
function formatSelectedText(selectedText) {
|
|
563
|
+
if (!selectedText) return "";
|
|
564
|
+
return `## User Selected Text
|
|
565
|
+
The user has highlighted this text on the page:
|
|
566
|
+
\`\`\`
|
|
567
|
+
${selectedText}
|
|
568
|
+
\`\`\`
|
|
569
|
+
|
|
570
|
+
`;
|
|
571
|
+
}
|
|
572
|
+
function formatSessionHistory(messages) {
|
|
573
|
+
if (messages.count === 0 || messages.lastN.length === 0) return "";
|
|
574
|
+
let section = `## Recent Conversation (${messages.count} messages total)
|
|
575
|
+
`;
|
|
576
|
+
for (const msg of messages.lastN) {
|
|
577
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
578
|
+
section += `**${role}:** ${msg.content.slice(0, 200)}${msg.content.length > 200 ? "..." : ""}
|
|
579
|
+
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
582
|
+
return section;
|
|
583
|
+
}
|
|
584
|
+
var DEFAULT_OPTIONS = {
|
|
585
|
+
includePlatformNotes: true,
|
|
586
|
+
includeErrors: true,
|
|
587
|
+
includeStructure: true,
|
|
588
|
+
// Phase 2 defaults
|
|
589
|
+
includeCodeBlocks: true,
|
|
590
|
+
includeTables: true,
|
|
591
|
+
includeConsoleLogs: true,
|
|
592
|
+
includeNetworkErrors: true,
|
|
593
|
+
includeModal: true,
|
|
594
|
+
includePlatformSpecific: true,
|
|
595
|
+
maxContextLength: 1e4
|
|
596
|
+
// Increased for Phase 2
|
|
597
|
+
};
|
|
598
|
+
function buildContextAwarePrompt(context, userMessage, options = {}) {
|
|
599
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
600
|
+
let enrichedPrompt = "";
|
|
601
|
+
enrichedPrompt += `# Browser Context (from user's authenticated session)
|
|
602
|
+
|
|
603
|
+
`;
|
|
604
|
+
enrichedPrompt += `The following context was extracted from the user's browser. `;
|
|
605
|
+
enrichedPrompt += `This may include private or authenticated content that is not publicly accessible. `;
|
|
606
|
+
enrichedPrompt += `Use this context to answer the user's question - DO NOT attempt to fetch URLs externally.
|
|
607
|
+
|
|
608
|
+
`;
|
|
609
|
+
enrichedPrompt += formatPageContext(context.page);
|
|
610
|
+
if (context.page.uiState) {
|
|
611
|
+
enrichedPrompt += formatUIState(context.page.uiState);
|
|
612
|
+
}
|
|
613
|
+
if (opts.includePlatformNotes) {
|
|
614
|
+
const platformNotes = PLATFORM_CONTEXT_NOTES[context.page.platform.type] || PLATFORM_CONTEXT_NOTES.generic;
|
|
615
|
+
if (platformNotes) {
|
|
616
|
+
enrichedPrompt += platformNotes + "\n\n";
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (opts.includePlatformSpecific && context.page.platform.specificContext) {
|
|
620
|
+
enrichedPrompt += formatPlatformSpecificContext(context.page.platform.specificContext);
|
|
621
|
+
}
|
|
622
|
+
if (opts.includeErrors) {
|
|
623
|
+
enrichedPrompt += formatErrors(context.text.errors);
|
|
624
|
+
}
|
|
625
|
+
if (opts.includeConsoleLogs && context.text.consoleLogs) {
|
|
626
|
+
enrichedPrompt += formatConsoleLogs(context.text.consoleLogs);
|
|
627
|
+
}
|
|
628
|
+
if (opts.includeNetworkErrors && context.text.networkErrors) {
|
|
629
|
+
enrichedPrompt += formatNetworkErrors(context.text.networkErrors);
|
|
630
|
+
}
|
|
631
|
+
if (context.text.runtimeErrors?.length > 0) {
|
|
632
|
+
enrichedPrompt += formatRuntimeErrors(context.text.runtimeErrors);
|
|
633
|
+
}
|
|
634
|
+
enrichedPrompt += formatSelectedText(context.text.selectedText);
|
|
635
|
+
if (opts.includeModal && context.structure.modal) {
|
|
636
|
+
enrichedPrompt += formatModalContent(context.structure.modal);
|
|
637
|
+
}
|
|
638
|
+
if (opts.includeStructure) {
|
|
639
|
+
enrichedPrompt += formatHeadings(context.text.headings);
|
|
640
|
+
enrichedPrompt += formatRelevantSections(context.structure.relevantSections);
|
|
641
|
+
}
|
|
642
|
+
if (opts.includeCodeBlocks && context.structure.codeBlocks) {
|
|
643
|
+
enrichedPrompt += formatCodeBlocks(context.structure.codeBlocks);
|
|
644
|
+
}
|
|
645
|
+
if (opts.includeTables && context.structure.tables) {
|
|
646
|
+
enrichedPrompt += formatTables(context.structure.tables);
|
|
647
|
+
}
|
|
648
|
+
enrichedPrompt += formatSessionHistory(context.session.previousMessages);
|
|
649
|
+
if (context.privacy?.privacyMaskingApplied) {
|
|
650
|
+
enrichedPrompt += `
|
|
651
|
+
**Note:** Some sensitive data (${context.privacy.sensitiveDataTypes?.join(", ") || "various types"}) has been redacted for privacy.
|
|
652
|
+
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
enrichedPrompt += `---
|
|
656
|
+
|
|
657
|
+
## User Message
|
|
658
|
+
${userMessage}
|
|
659
|
+
`;
|
|
660
|
+
if (enrichedPrompt.length > (opts.maxContextLength || 1e4)) {
|
|
661
|
+
enrichedPrompt = truncatePrompt(enrichedPrompt, opts.maxContextLength || 1e4, userMessage);
|
|
662
|
+
}
|
|
663
|
+
return { systemPrompt: null, userPrompt: enrichedPrompt };
|
|
664
|
+
}
|
|
665
|
+
function truncatePrompt(prompt, maxLength, userMessage) {
|
|
666
|
+
const userMessageSection = `## User Message
|
|
667
|
+
${userMessage}
|
|
668
|
+
`;
|
|
669
|
+
const reservedLength = userMessageSection.length + 500;
|
|
670
|
+
const availableLength = maxLength - reservedLength;
|
|
671
|
+
if (availableLength < 500) {
|
|
672
|
+
return userMessageSection;
|
|
673
|
+
}
|
|
674
|
+
const truncatedContext = prompt.slice(0, availableLength);
|
|
675
|
+
const lastNewline = truncatedContext.lastIndexOf("\n");
|
|
676
|
+
return truncatedContext.slice(0, lastNewline) + "\n\n[Context truncated for length]\n\n" + userMessageSection;
|
|
677
|
+
}
|
|
678
|
+
function buildSimplePrompt(userMessage, pageUrl, pageTitle, selectedText) {
|
|
679
|
+
let userPrompt = "";
|
|
680
|
+
if (pageUrl || pageTitle || selectedText) {
|
|
681
|
+
userPrompt += `**Context:**
|
|
682
|
+
`;
|
|
683
|
+
if (pageUrl) userPrompt += `- Page URL: ${pageUrl}
|
|
684
|
+
`;
|
|
685
|
+
if (pageTitle) userPrompt += `- Page Title: ${pageTitle}
|
|
686
|
+
`;
|
|
687
|
+
if (selectedText) userPrompt += `- Selected Text: "${selectedText.slice(0, 500)}${selectedText.length > 500 ? "..." : ""}"
|
|
688
|
+
`;
|
|
689
|
+
userPrompt += "\n**Question:** ";
|
|
690
|
+
}
|
|
691
|
+
userPrompt += userMessage;
|
|
692
|
+
return { systemPrompt: null, userPrompt };
|
|
693
|
+
}
|
|
694
|
+
function validateContext(context) {
|
|
695
|
+
if (!context || typeof context !== "object") return false;
|
|
696
|
+
const c = context;
|
|
697
|
+
if (!c.metadata || typeof c.metadata !== "object") return false;
|
|
698
|
+
if (!c.page || typeof c.page !== "object") return false;
|
|
699
|
+
if (!c.text || typeof c.text !== "object") return false;
|
|
700
|
+
if (!c.session || typeof c.session !== "object") return false;
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
function sanitizeContext(context) {
|
|
704
|
+
const sanitized = JSON.parse(JSON.stringify(context));
|
|
705
|
+
const sensitivePatterns = [
|
|
706
|
+
/Bearer\s+[A-Za-z0-9\-_]+/gi,
|
|
707
|
+
/api[_-]?key[=:]\s*[A-Za-z0-9\-_]+/gi,
|
|
708
|
+
/password[=:]\s*\S+/gi,
|
|
709
|
+
/token[=:]\s*[A-Za-z0-9\-_]+/gi
|
|
710
|
+
];
|
|
711
|
+
let visibleText = sanitized.text.visibleText;
|
|
712
|
+
for (const pattern of sensitivePatterns) {
|
|
713
|
+
visibleText = visibleText.replace(pattern, "[REDACTED]");
|
|
714
|
+
}
|
|
715
|
+
sanitized.text.visibleText = visibleText;
|
|
716
|
+
const url = new URL(sanitized.page.url);
|
|
717
|
+
const sensitiveParams = ["token", "key", "secret", "password", "auth"];
|
|
718
|
+
for (const param of sensitiveParams) {
|
|
719
|
+
if (url.searchParams.has(param)) {
|
|
720
|
+
url.searchParams.set(param, "[REDACTED]");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
sanitized.page.url = url.toString();
|
|
724
|
+
sanitized.page.urlParsed.search = url.search;
|
|
725
|
+
return sanitized;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/routes/chat.ts
|
|
729
|
+
var simpleContextSchema = z2.object({
|
|
730
|
+
pageUrl: z2.string().optional(),
|
|
731
|
+
pageTitle: z2.string().optional(),
|
|
732
|
+
selectedText: z2.string().optional(),
|
|
733
|
+
action: z2.enum([
|
|
734
|
+
"explain",
|
|
735
|
+
"translate",
|
|
736
|
+
"rewrite",
|
|
737
|
+
"fix_grammar",
|
|
738
|
+
"summarize",
|
|
739
|
+
"expand",
|
|
740
|
+
"analyze_config",
|
|
741
|
+
"diagnose_error"
|
|
742
|
+
]).optional()
|
|
743
|
+
});
|
|
744
|
+
var fullContextSchema = z2.object({
|
|
745
|
+
metadata: z2.object({
|
|
746
|
+
captureTimestamp: z2.string(),
|
|
747
|
+
captureMode: z2.enum(["auto", "manual"]),
|
|
748
|
+
browserInfo: z2.object({
|
|
749
|
+
userAgent: z2.string(),
|
|
750
|
+
viewport: z2.object({ width: z2.number(), height: z2.number() }),
|
|
751
|
+
language: z2.string()
|
|
752
|
+
})
|
|
753
|
+
}),
|
|
754
|
+
page: z2.object({
|
|
755
|
+
url: z2.string(),
|
|
756
|
+
urlParsed: z2.object({
|
|
757
|
+
protocol: z2.string(),
|
|
758
|
+
hostname: z2.string(),
|
|
759
|
+
pathname: z2.string(),
|
|
760
|
+
search: z2.string(),
|
|
761
|
+
hash: z2.string()
|
|
762
|
+
}),
|
|
763
|
+
title: z2.string(),
|
|
764
|
+
platform: z2.object({
|
|
765
|
+
type: z2.string(),
|
|
766
|
+
confidence: z2.number(),
|
|
767
|
+
indicators: z2.array(z2.string()),
|
|
768
|
+
specificProduct: z2.string().optional()
|
|
769
|
+
})
|
|
770
|
+
}),
|
|
771
|
+
text: z2.object({
|
|
772
|
+
selectedText: z2.string().optional(),
|
|
773
|
+
visibleText: z2.string(),
|
|
774
|
+
headings: z2.array(z2.object({
|
|
775
|
+
level: z2.number(),
|
|
776
|
+
text: z2.string()
|
|
777
|
+
})),
|
|
778
|
+
errors: z2.array(z2.object({
|
|
779
|
+
type: z2.enum(["error", "warning", "info"]),
|
|
780
|
+
message: z2.string(),
|
|
781
|
+
source: z2.enum(["console", "ui", "network", "dom"]).optional(),
|
|
782
|
+
severity: z2.enum(["critical", "high", "medium", "low"]),
|
|
783
|
+
context: z2.string().optional()
|
|
784
|
+
})),
|
|
785
|
+
metadata: z2.object({
|
|
786
|
+
totalLength: z2.number(),
|
|
787
|
+
truncated: z2.boolean()
|
|
788
|
+
})
|
|
789
|
+
}),
|
|
790
|
+
structure: z2.object({
|
|
791
|
+
relevantSections: z2.array(z2.any()),
|
|
792
|
+
errorContainers: z2.array(z2.any()),
|
|
793
|
+
activeElements: z2.any(),
|
|
794
|
+
metadata: z2.any()
|
|
795
|
+
}),
|
|
796
|
+
session: z2.object({
|
|
797
|
+
sessionId: z2.string(),
|
|
798
|
+
sessionType: z2.string(),
|
|
799
|
+
intent: z2.object({
|
|
800
|
+
primary: z2.string(),
|
|
801
|
+
keywords: z2.array(z2.string()),
|
|
802
|
+
implicitSignals: z2.array(z2.string()),
|
|
803
|
+
explicitGoal: z2.string().optional()
|
|
804
|
+
}),
|
|
805
|
+
previousMessages: z2.object({
|
|
806
|
+
count: z2.number(),
|
|
807
|
+
lastN: z2.array(z2.any())
|
|
808
|
+
})
|
|
809
|
+
}),
|
|
810
|
+
privacy: z2.object({
|
|
811
|
+
redactedFields: z2.array(z2.string()),
|
|
812
|
+
sensitiveDataDetected: z2.boolean(),
|
|
813
|
+
consentGiven: z2.boolean(),
|
|
814
|
+
dataRetention: z2.enum(["session", "none"])
|
|
815
|
+
})
|
|
816
|
+
}).passthrough();
|
|
817
|
+
var imagePayloadSchema = z2.object({
|
|
818
|
+
id: z2.string(),
|
|
819
|
+
dataUrl: z2.string(),
|
|
820
|
+
mimeType: z2.enum(["image/png", "image/jpeg", "image/webp"]),
|
|
821
|
+
source: z2.enum(["screenshot", "paste", "drop"])
|
|
822
|
+
});
|
|
823
|
+
var sendMessageSchema = z2.object({
|
|
824
|
+
prompt: z2.string().min(1),
|
|
825
|
+
context: simpleContextSchema.optional(),
|
|
826
|
+
fullContext: fullContextSchema.optional(),
|
|
827
|
+
useContextAwareMode: z2.boolean().optional(),
|
|
828
|
+
images: z2.array(imagePayloadSchema).max(5).optional()
|
|
829
|
+
});
|
|
830
|
+
async function chatRoutes(fastify) {
|
|
831
|
+
const buildPrompt = (body) => {
|
|
832
|
+
if (body.fullContext && body.useContextAwareMode !== false) {
|
|
833
|
+
if (validateContext(body.fullContext)) {
|
|
834
|
+
const sanitizedContext = sanitizeContext(body.fullContext);
|
|
835
|
+
const { userPrompt: userPrompt2 } = buildContextAwarePrompt(sanitizedContext, body.prompt);
|
|
836
|
+
return { userPrompt: userPrompt2, promptType: "context-aware" };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const { userPrompt } = buildSimplePrompt(
|
|
840
|
+
body.prompt,
|
|
841
|
+
body.context?.pageUrl,
|
|
842
|
+
body.context?.pageTitle,
|
|
843
|
+
body.context?.selectedText
|
|
844
|
+
);
|
|
845
|
+
return { userPrompt, promptType: "simple" };
|
|
846
|
+
};
|
|
847
|
+
fastify.post("/sessions/:id/chat", async (request, reply) => {
|
|
848
|
+
try {
|
|
849
|
+
const body = sendMessageSchema.parse(request.body);
|
|
850
|
+
const sessionId = request.params.id;
|
|
851
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
852
|
+
if (!session) {
|
|
853
|
+
return reply.code(404).send({
|
|
854
|
+
success: false,
|
|
855
|
+
error: {
|
|
856
|
+
code: "NOT_FOUND",
|
|
857
|
+
message: "Session not found"
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
const { userPrompt, promptType } = buildPrompt(body);
|
|
862
|
+
console.log(`[ChatRoute] Using ${promptType} prompt`);
|
|
863
|
+
const userMessage = fastify.sessionService.addMessage(
|
|
864
|
+
sessionId,
|
|
865
|
+
"user",
|
|
866
|
+
body.prompt,
|
|
867
|
+
body.context ? {
|
|
868
|
+
pageUrl: body.context.pageUrl,
|
|
869
|
+
selectedText: body.context.selectedText,
|
|
870
|
+
action: body.context.action
|
|
871
|
+
} : void 0
|
|
872
|
+
);
|
|
873
|
+
if (body.fullContext) {
|
|
874
|
+
try {
|
|
875
|
+
fastify.sessionService.saveContext(
|
|
876
|
+
sessionId,
|
|
877
|
+
JSON.stringify(body.fullContext),
|
|
878
|
+
userMessage.id,
|
|
879
|
+
body.fullContext.page?.url,
|
|
880
|
+
body.fullContext.page?.title,
|
|
881
|
+
body.fullContext.page?.platform?.type
|
|
882
|
+
);
|
|
883
|
+
console.log(`[ChatRoute] Context saved for message ${userMessage.id}`);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.error("[ChatRoute] Failed to persist context:", err);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const response = await fastify.copilotService.sendMessage(
|
|
889
|
+
sessionId,
|
|
890
|
+
userPrompt,
|
|
891
|
+
body.context
|
|
892
|
+
// NOT passing systemPrompt - preserves Copilot's intelligence
|
|
893
|
+
);
|
|
894
|
+
const assistantMessage = fastify.sessionService.addMessage(
|
|
895
|
+
sessionId,
|
|
896
|
+
"assistant",
|
|
897
|
+
response
|
|
898
|
+
);
|
|
899
|
+
return reply.send({
|
|
900
|
+
success: true,
|
|
901
|
+
data: assistantMessage
|
|
902
|
+
});
|
|
903
|
+
} catch (error) {
|
|
904
|
+
if (error instanceof z2.ZodError) {
|
|
905
|
+
return reply.code(400).send({
|
|
906
|
+
success: false,
|
|
907
|
+
error: {
|
|
908
|
+
code: "VALIDATION_ERROR",
|
|
909
|
+
message: "Invalid request body",
|
|
910
|
+
details: { errors: error.errors }
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
fastify.post("/sessions/:id/chat/stream", async (request, reply) => {
|
|
918
|
+
try {
|
|
919
|
+
const body = sendMessageSchema.parse(request.body);
|
|
920
|
+
const sessionId = request.params.id;
|
|
921
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
922
|
+
if (!session) {
|
|
923
|
+
return reply.code(404).send({
|
|
924
|
+
success: false,
|
|
925
|
+
error: {
|
|
926
|
+
code: "NOT_FOUND",
|
|
927
|
+
message: "Session not found"
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
const { userPrompt, promptType } = buildPrompt(body);
|
|
932
|
+
console.log(`[ChatRoute] Using ${promptType} prompt for streaming`);
|
|
933
|
+
let processedImagesRaw = [];
|
|
934
|
+
let processedImages = [];
|
|
935
|
+
const host = request.headers.host || `${request.hostname}:3847`;
|
|
936
|
+
const backendUrl = `http://${host}`;
|
|
937
|
+
const userMessage = fastify.sessionService.addMessage(
|
|
938
|
+
sessionId,
|
|
939
|
+
"user",
|
|
940
|
+
body.prompt,
|
|
941
|
+
body.context ? {
|
|
942
|
+
pageUrl: body.context.pageUrl,
|
|
943
|
+
selectedText: body.context.selectedText,
|
|
944
|
+
action: body.context.action,
|
|
945
|
+
contextAware: body.useContextAwareMode
|
|
946
|
+
} : { contextAware: body.useContextAwareMode }
|
|
947
|
+
);
|
|
948
|
+
if (body.images && body.images.length > 0) {
|
|
949
|
+
try {
|
|
950
|
+
console.log(`[ChatRoute] Processing ${body.images.length} images for message ${userMessage.id}`);
|
|
951
|
+
processedImagesRaw = await processMessageImages(
|
|
952
|
+
sessionId,
|
|
953
|
+
userMessage.id,
|
|
954
|
+
body.images,
|
|
955
|
+
backendUrl
|
|
956
|
+
);
|
|
957
|
+
processedImages = toImageAttachments(processedImagesRaw);
|
|
958
|
+
fastify.sessionService.updateMessageMetadata(userMessage.id, {
|
|
959
|
+
...userMessage.metadata,
|
|
960
|
+
images: processedImages
|
|
961
|
+
});
|
|
962
|
+
console.log(`[ChatRoute] Processed ${processedImages.length} images successfully`);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
console.error("[ChatRoute] Failed to process images:", err);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const copilotAttachments = processedImagesRaw.map((img, index) => ({
|
|
968
|
+
type: "file",
|
|
969
|
+
path: img.fullImagePath,
|
|
970
|
+
displayName: `image_${index + 1}.${img.mimeType.split("/")[1] || "jpg"}`
|
|
971
|
+
}));
|
|
972
|
+
if (copilotAttachments.length > 0) {
|
|
973
|
+
console.log(`[ChatRoute] Built ${copilotAttachments.length} attachments for Copilot:`, copilotAttachments);
|
|
974
|
+
}
|
|
975
|
+
if (body.fullContext) {
|
|
976
|
+
try {
|
|
977
|
+
fastify.sessionService.saveContext(
|
|
978
|
+
sessionId,
|
|
979
|
+
JSON.stringify(body.fullContext),
|
|
980
|
+
userMessage.id,
|
|
981
|
+
body.fullContext.page?.url,
|
|
982
|
+
body.fullContext.page?.title,
|
|
983
|
+
body.fullContext.page?.platform?.type
|
|
984
|
+
);
|
|
985
|
+
console.log(`[ChatRoute] Context saved for message ${userMessage.id}`);
|
|
986
|
+
} catch (err) {
|
|
987
|
+
console.error("[ChatRoute] Failed to persist context:", err);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
991
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
992
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
993
|
+
reply.raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
994
|
+
let fullContent = "";
|
|
995
|
+
let assistantMessageId = null;
|
|
996
|
+
let streamEnded = false;
|
|
997
|
+
let lastActivityTime = Date.now();
|
|
998
|
+
const STREAM_TIMEOUT_MS = 12e4;
|
|
999
|
+
const IDLE_TIMEOUT_MS = 3e4;
|
|
1000
|
+
const sendSSE = (event) => {
|
|
1001
|
+
if (!streamEnded) {
|
|
1002
|
+
reply.raw.write(`data: ${JSON.stringify(event)}
|
|
1003
|
+
|
|
1004
|
+
`);
|
|
1005
|
+
lastActivityTime = Date.now();
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
const endStream = (reason = "completed") => {
|
|
1009
|
+
if (streamEnded) return;
|
|
1010
|
+
streamEnded = true;
|
|
1011
|
+
console.log(`[ChatRoute] Stream ending: ${reason}. Content length: ${fullContent.length}`);
|
|
1012
|
+
if (fullContent && !assistantMessageId) {
|
|
1013
|
+
const message = fastify.sessionService.addMessage(
|
|
1014
|
+
sessionId,
|
|
1015
|
+
"assistant",
|
|
1016
|
+
fullContent
|
|
1017
|
+
);
|
|
1018
|
+
assistantMessageId = message.id;
|
|
1019
|
+
}
|
|
1020
|
+
sendSSE({ type: "done", data: { messageId: assistantMessageId || void 0, reason } });
|
|
1021
|
+
reply.raw.write("data: [DONE]\n\n");
|
|
1022
|
+
reply.raw.end();
|
|
1023
|
+
};
|
|
1024
|
+
const streamComplete = new Promise((resolve2) => {
|
|
1025
|
+
const globalTimeout = setTimeout(() => {
|
|
1026
|
+
console.warn("[ChatRoute] Global stream timeout reached");
|
|
1027
|
+
endStream("timeout");
|
|
1028
|
+
fastify.copilotService.abortRequest(sessionId).catch(() => {
|
|
1029
|
+
});
|
|
1030
|
+
resolve2();
|
|
1031
|
+
}, STREAM_TIMEOUT_MS);
|
|
1032
|
+
const idleCheckInterval = setInterval(() => {
|
|
1033
|
+
const idleTime = Date.now() - lastActivityTime;
|
|
1034
|
+
if (idleTime > IDLE_TIMEOUT_MS && !streamEnded) {
|
|
1035
|
+
console.warn(`[ChatRoute] Stream idle for ${idleTime}ms, ending`);
|
|
1036
|
+
endStream("idle_timeout");
|
|
1037
|
+
fastify.copilotService.abortRequest(sessionId).catch(() => {
|
|
1038
|
+
});
|
|
1039
|
+
resolve2();
|
|
1040
|
+
}
|
|
1041
|
+
}, 5e3);
|
|
1042
|
+
const handleEvent = (event) => {
|
|
1043
|
+
if (streamEnded) return;
|
|
1044
|
+
console.log("[ChatRoute] Received event:", event.type);
|
|
1045
|
+
lastActivityTime = Date.now();
|
|
1046
|
+
switch (event.type) {
|
|
1047
|
+
case "assistant.message_delta":
|
|
1048
|
+
fullContent += event.data.deltaContent || "";
|
|
1049
|
+
sendSSE({
|
|
1050
|
+
type: "message_delta",
|
|
1051
|
+
data: { deltaContent: event.data.deltaContent }
|
|
1052
|
+
});
|
|
1053
|
+
break;
|
|
1054
|
+
case "assistant.message":
|
|
1055
|
+
fullContent = event.data.content || fullContent;
|
|
1056
|
+
sendSSE({
|
|
1057
|
+
type: "message_complete",
|
|
1058
|
+
data: { content: fullContent }
|
|
1059
|
+
});
|
|
1060
|
+
break;
|
|
1061
|
+
case "tool.execution_start":
|
|
1062
|
+
sendSSE({
|
|
1063
|
+
type: "tool_start",
|
|
1064
|
+
data: {
|
|
1065
|
+
toolName: event.data.toolName,
|
|
1066
|
+
toolCallId: event.data.toolCallId
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
break;
|
|
1070
|
+
case "tool.execution_complete":
|
|
1071
|
+
sendSSE({
|
|
1072
|
+
type: "tool_complete",
|
|
1073
|
+
data: { toolCallId: event.data.toolCallId }
|
|
1074
|
+
});
|
|
1075
|
+
break;
|
|
1076
|
+
case "session.idle":
|
|
1077
|
+
clearTimeout(globalTimeout);
|
|
1078
|
+
clearInterval(idleCheckInterval);
|
|
1079
|
+
endStream("completed");
|
|
1080
|
+
resolve2();
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
fastify.copilotService.streamMessage(
|
|
1085
|
+
sessionId,
|
|
1086
|
+
userPrompt,
|
|
1087
|
+
body.context,
|
|
1088
|
+
handleEvent,
|
|
1089
|
+
copilotAttachments.length > 0 ? copilotAttachments : void 0
|
|
1090
|
+
);
|
|
1091
|
+
});
|
|
1092
|
+
request.raw.on("close", async () => {
|
|
1093
|
+
if (!streamEnded) {
|
|
1094
|
+
console.log("[ChatRoute] Client disconnected, aborting");
|
|
1095
|
+
streamEnded = true;
|
|
1096
|
+
await fastify.copilotService.abortRequest(sessionId);
|
|
1097
|
+
reply.raw.end();
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
await streamComplete;
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (error instanceof z2.ZodError) {
|
|
1103
|
+
return reply.code(400).send({
|
|
1104
|
+
success: false,
|
|
1105
|
+
error: {
|
|
1106
|
+
code: "VALIDATION_ERROR",
|
|
1107
|
+
message: "Invalid request body",
|
|
1108
|
+
details: error.errors
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const errorEvent = {
|
|
1113
|
+
type: "error",
|
|
1114
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
1115
|
+
};
|
|
1116
|
+
reply.raw.write(`data: ${JSON.stringify(errorEvent)}
|
|
1117
|
+
|
|
1118
|
+
`);
|
|
1119
|
+
reply.raw.write("data: [DONE]\n\n");
|
|
1120
|
+
reply.raw.end();
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
fastify.get("/sessions/:id/context", async (request, reply) => {
|
|
1124
|
+
const sessionId = request.params.id;
|
|
1125
|
+
const limit = request.query.limit || 10;
|
|
1126
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
1127
|
+
if (!session) {
|
|
1128
|
+
return reply.code(404).send({
|
|
1129
|
+
success: false,
|
|
1130
|
+
error: {
|
|
1131
|
+
code: "NOT_FOUND",
|
|
1132
|
+
message: "Session not found"
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const latestContext = fastify.sessionService.getLatestContext(sessionId);
|
|
1137
|
+
const contextHistory = fastify.sessionService.getContextHistory(sessionId, limit);
|
|
1138
|
+
const contextCount = fastify.sessionService.getContextCount(sessionId);
|
|
1139
|
+
return reply.send({
|
|
1140
|
+
success: true,
|
|
1141
|
+
data: {
|
|
1142
|
+
latest: latestContext,
|
|
1143
|
+
history: contextHistory,
|
|
1144
|
+
totalCount: contextCount
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
fastify.get("/sessions/:id/context/:contextId", async (request, reply) => {
|
|
1149
|
+
const { id: sessionId, contextId } = request.params;
|
|
1150
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
1151
|
+
if (!session) {
|
|
1152
|
+
return reply.code(404).send({
|
|
1153
|
+
success: false,
|
|
1154
|
+
error: {
|
|
1155
|
+
code: "NOT_FOUND",
|
|
1156
|
+
message: "Session not found"
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
const context = fastify.sessionService.getContext(contextId);
|
|
1161
|
+
if (!context || context.sessionId !== sessionId) {
|
|
1162
|
+
return reply.code(404).send({
|
|
1163
|
+
success: false,
|
|
1164
|
+
error: {
|
|
1165
|
+
code: "NOT_FOUND",
|
|
1166
|
+
message: "Context not found"
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
return reply.send({
|
|
1171
|
+
success: true,
|
|
1172
|
+
data: {
|
|
1173
|
+
...context,
|
|
1174
|
+
contextData: JSON.parse(context.contextJson)
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
fastify.post("/sessions/:id/context/cleanup", async (request, reply) => {
|
|
1179
|
+
const sessionId = request.params.id;
|
|
1180
|
+
const keepCount = request.body?.keepCount || 20;
|
|
1181
|
+
const session = fastify.sessionService.getSession(sessionId);
|
|
1182
|
+
if (!session) {
|
|
1183
|
+
return reply.code(404).send({
|
|
1184
|
+
success: false,
|
|
1185
|
+
error: {
|
|
1186
|
+
code: "NOT_FOUND",
|
|
1187
|
+
message: "Session not found"
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
const deletedCount = fastify.sessionService.cleanupOldContexts(sessionId, keepCount);
|
|
1192
|
+
return reply.send({
|
|
1193
|
+
success: true,
|
|
1194
|
+
data: {
|
|
1195
|
+
deletedCount,
|
|
1196
|
+
remainingCount: fastify.sessionService.getContextCount(sessionId)
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/routes/models.ts
|
|
1203
|
+
var AVAILABLE_MODELS = [
|
|
1204
|
+
// Free tier (0x)
|
|
1205
|
+
{
|
|
1206
|
+
id: "gpt-4.1",
|
|
1207
|
+
name: "GPT-4.1",
|
|
1208
|
+
description: "Fast and capable model for most tasks",
|
|
1209
|
+
provider: "openai",
|
|
1210
|
+
isDefault: true,
|
|
1211
|
+
pricingTier: "free",
|
|
1212
|
+
pricingMultiplier: 0
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
id: "gpt-4o",
|
|
1216
|
+
name: "GPT-4o",
|
|
1217
|
+
description: "Multimodal model with vision capabilities",
|
|
1218
|
+
provider: "openai",
|
|
1219
|
+
isDefault: false,
|
|
1220
|
+
pricingTier: "free",
|
|
1221
|
+
pricingMultiplier: 0
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: "gpt-5-mini",
|
|
1225
|
+
name: "GPT-5 Mini",
|
|
1226
|
+
description: "Fast, lightweight model for simple tasks",
|
|
1227
|
+
provider: "openai",
|
|
1228
|
+
isDefault: false,
|
|
1229
|
+
pricingTier: "free",
|
|
1230
|
+
pricingMultiplier: 0
|
|
1231
|
+
},
|
|
1232
|
+
// Cheap tier (0.33x)
|
|
1233
|
+
{
|
|
1234
|
+
id: "claude-haiku-4.5",
|
|
1235
|
+
name: "Claude Haiku 4.5",
|
|
1236
|
+
description: "Fast, efficient model for quick tasks",
|
|
1237
|
+
provider: "anthropic",
|
|
1238
|
+
isDefault: false,
|
|
1239
|
+
pricingTier: "cheap",
|
|
1240
|
+
pricingMultiplier: 0.33
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
id: "gpt-5.1-codex-mini",
|
|
1244
|
+
name: "GPT-5.1 Codex Mini",
|
|
1245
|
+
description: "Compact coding model",
|
|
1246
|
+
provider: "openai",
|
|
1247
|
+
isDefault: false,
|
|
1248
|
+
pricingTier: "cheap",
|
|
1249
|
+
pricingMultiplier: 0.33
|
|
1250
|
+
},
|
|
1251
|
+
// Standard tier (1x)
|
|
1252
|
+
{
|
|
1253
|
+
id: "gpt-5",
|
|
1254
|
+
name: "GPT-5",
|
|
1255
|
+
description: "Most capable model for complex reasoning",
|
|
1256
|
+
provider: "openai",
|
|
1257
|
+
isDefault: false,
|
|
1258
|
+
pricingTier: "standard",
|
|
1259
|
+
pricingMultiplier: 1
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
id: "gpt-5.1",
|
|
1263
|
+
name: "GPT-5.1",
|
|
1264
|
+
description: "Enhanced reasoning and analysis",
|
|
1265
|
+
provider: "openai",
|
|
1266
|
+
isDefault: false,
|
|
1267
|
+
pricingTier: "standard",
|
|
1268
|
+
pricingMultiplier: 1
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
id: "gpt-5.1-codex",
|
|
1272
|
+
name: "GPT-5.1 Codex",
|
|
1273
|
+
description: "Specialized for code generation",
|
|
1274
|
+
provider: "openai",
|
|
1275
|
+
isDefault: false,
|
|
1276
|
+
pricingTier: "standard",
|
|
1277
|
+
pricingMultiplier: 1
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: "gpt-5.2",
|
|
1281
|
+
name: "GPT-5.2",
|
|
1282
|
+
description: "Latest generation model",
|
|
1283
|
+
provider: "openai",
|
|
1284
|
+
isDefault: false,
|
|
1285
|
+
pricingTier: "standard",
|
|
1286
|
+
pricingMultiplier: 1
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
id: "claude-sonnet-4",
|
|
1290
|
+
name: "Claude Sonnet 4",
|
|
1291
|
+
description: "Balanced model for general use",
|
|
1292
|
+
provider: "anthropic",
|
|
1293
|
+
isDefault: false,
|
|
1294
|
+
pricingTier: "standard",
|
|
1295
|
+
pricingMultiplier: 1
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
id: "claude-sonnet-4.5",
|
|
1299
|
+
name: "Claude Sonnet 4.5",
|
|
1300
|
+
description: "Enhanced balanced model",
|
|
1301
|
+
provider: "anthropic",
|
|
1302
|
+
isDefault: false,
|
|
1303
|
+
pricingTier: "standard",
|
|
1304
|
+
pricingMultiplier: 1
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
id: "gemini-3-pro-preview",
|
|
1308
|
+
name: "Gemini 3 Pro (Preview)",
|
|
1309
|
+
description: "Google's latest model",
|
|
1310
|
+
provider: "google",
|
|
1311
|
+
isDefault: false,
|
|
1312
|
+
pricingTier: "standard",
|
|
1313
|
+
pricingMultiplier: 1
|
|
1314
|
+
},
|
|
1315
|
+
// Premium tier (3x)
|
|
1316
|
+
{
|
|
1317
|
+
id: "claude-opus-4.5",
|
|
1318
|
+
name: "Claude Opus 4.5",
|
|
1319
|
+
description: "Premium model for complex analysis",
|
|
1320
|
+
provider: "anthropic",
|
|
1321
|
+
isDefault: false,
|
|
1322
|
+
pricingTier: "premium",
|
|
1323
|
+
pricingMultiplier: 3
|
|
1324
|
+
}
|
|
1325
|
+
];
|
|
1326
|
+
async function modelsRoutes(fastify) {
|
|
1327
|
+
fastify.get("/models", async (_request, reply) => {
|
|
1328
|
+
const defaultModel = AVAILABLE_MODELS.find((m) => m.isDefault);
|
|
1329
|
+
return reply.send({
|
|
1330
|
+
success: true,
|
|
1331
|
+
data: {
|
|
1332
|
+
models: AVAILABLE_MODELS,
|
|
1333
|
+
default: defaultModel?.id || "gpt-4.1"
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1337
|
+
fastify.get("/models/:id", async (request, reply) => {
|
|
1338
|
+
const modelId = request.params.id;
|
|
1339
|
+
const model = AVAILABLE_MODELS.find((m) => m.id === modelId);
|
|
1340
|
+
if (!model) {
|
|
1341
|
+
return reply.code(404).send({
|
|
1342
|
+
success: false,
|
|
1343
|
+
error: {
|
|
1344
|
+
code: "NOT_FOUND",
|
|
1345
|
+
message: `Model '${modelId}' not found`
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
return reply.send({
|
|
1350
|
+
success: true,
|
|
1351
|
+
data: model
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/routes/images.ts
|
|
1357
|
+
import fs2 from "fs";
|
|
1358
|
+
import path2 from "path";
|
|
1359
|
+
var MIME_TYPES = {
|
|
1360
|
+
"jpg": "image/jpeg",
|
|
1361
|
+
"jpeg": "image/jpeg",
|
|
1362
|
+
"png": "image/png",
|
|
1363
|
+
"webp": "image/webp",
|
|
1364
|
+
"gif": "image/gif"
|
|
1365
|
+
};
|
|
1366
|
+
async function imagesRoutes(fastify) {
|
|
1367
|
+
fastify.get(
|
|
1368
|
+
"/:sessionId/:messageId/:filename",
|
|
1369
|
+
async (request, reply) => {
|
|
1370
|
+
const { sessionId, messageId, filename } = request.params;
|
|
1371
|
+
if (!sessionId?.match(/^session_[a-z0-9]+$/) || !messageId?.match(/^msg_[a-z0-9]+$/)) {
|
|
1372
|
+
return reply.code(400).send({ error: "Invalid session or message ID format" });
|
|
1373
|
+
}
|
|
1374
|
+
let filePath = null;
|
|
1375
|
+
let mimeType = "image/jpeg";
|
|
1376
|
+
const thumbMatch = filename.match(/^thumb_(\d+)\.jpg$/);
|
|
1377
|
+
if (thumbMatch) {
|
|
1378
|
+
const imageIndex = parseInt(thumbMatch[1], 10);
|
|
1379
|
+
filePath = getThumbnailFilePath(sessionId, messageId, imageIndex);
|
|
1380
|
+
mimeType = "image/jpeg";
|
|
1381
|
+
}
|
|
1382
|
+
const imageMatch = filename.match(/^image_(\d+)\.(jpg|jpeg|png|webp|gif)$/i);
|
|
1383
|
+
if (imageMatch) {
|
|
1384
|
+
const ext = imageMatch[2].toLowerCase();
|
|
1385
|
+
mimeType = MIME_TYPES[ext] || "image/jpeg";
|
|
1386
|
+
filePath = path2.join(IMAGES_DIR, sessionId, messageId, filename);
|
|
1387
|
+
}
|
|
1388
|
+
if (!filePath) {
|
|
1389
|
+
return reply.code(400).send({ error: "Invalid filename format" });
|
|
1390
|
+
}
|
|
1391
|
+
if (!fs2.existsSync(filePath)) {
|
|
1392
|
+
console.log(`[ImagesRoute] File not found: ${filePath}`);
|
|
1393
|
+
return reply.code(404).send({ error: "Image not found" });
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
const fileBuffer = fs2.readFileSync(filePath);
|
|
1397
|
+
return reply.code(200).header("Content-Type", mimeType).header("Cache-Control", "public, max-age=31536000, immutable").header("Content-Length", fileBuffer.length).send(fileBuffer);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
console.error("[ImagesRoute] Failed to read image file:", error);
|
|
1400
|
+
return reply.code(500).send({ error: "Failed to read image" });
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/routes/tools.ts
|
|
1407
|
+
function registerToolsRoutes(app, copilotService) {
|
|
1408
|
+
app.get("/api/tools", async (request, reply) => {
|
|
1409
|
+
const sessionType = request.query.type || "devops";
|
|
1410
|
+
const tools = copilotService.getAvailableTools(sessionType);
|
|
1411
|
+
return reply.send({
|
|
1412
|
+
success: true,
|
|
1413
|
+
data: tools
|
|
1414
|
+
});
|
|
1415
|
+
});
|
|
1416
|
+
app.post("/api/tools/execute", async (request, reply) => {
|
|
1417
|
+
const { toolName, params } = request.body;
|
|
1418
|
+
if (!toolName) {
|
|
1419
|
+
return reply.status(400).send({
|
|
1420
|
+
success: false,
|
|
1421
|
+
error: "toolName is required"
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
const result = await copilotService.executeTool(toolName, params || {});
|
|
1425
|
+
if (!result.success) {
|
|
1426
|
+
return reply.status(400).send({
|
|
1427
|
+
success: false,
|
|
1428
|
+
error: result.error
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return reply.send({
|
|
1432
|
+
success: true,
|
|
1433
|
+
data: {
|
|
1434
|
+
toolName,
|
|
1435
|
+
result: result.result
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
app.post("/api/tools/analyze-config", async (request, reply) => {
|
|
1440
|
+
const { content, type } = request.body;
|
|
1441
|
+
if (!content) {
|
|
1442
|
+
return reply.status(400).send({
|
|
1443
|
+
success: false,
|
|
1444
|
+
error: "content is required"
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
const result = await copilotService.executeTool("analyze_config", {
|
|
1448
|
+
content,
|
|
1449
|
+
type: type || "auto"
|
|
1450
|
+
});
|
|
1451
|
+
return reply.send({
|
|
1452
|
+
success: result.success,
|
|
1453
|
+
data: result.result,
|
|
1454
|
+
error: result.error
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
app.post("/api/tools/analyze-error", async (request, reply) => {
|
|
1458
|
+
const { error, context } = request.body;
|
|
1459
|
+
if (!error) {
|
|
1460
|
+
return reply.status(400).send({
|
|
1461
|
+
success: false,
|
|
1462
|
+
error: "error is required"
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
const result = await copilotService.executeTool("analyze_error", {
|
|
1466
|
+
error,
|
|
1467
|
+
context: context || "general"
|
|
1468
|
+
});
|
|
1469
|
+
return reply.send({
|
|
1470
|
+
success: result.success,
|
|
1471
|
+
data: result.result,
|
|
1472
|
+
error: result.error
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/services/copilot.service.ts
|
|
1478
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
1479
|
+
|
|
1480
|
+
// src/tools/devops-tools.ts
|
|
1481
|
+
import * as fs3 from "fs/promises";
|
|
1482
|
+
import * as path3 from "path";
|
|
1483
|
+
var ALLOWED_DIRECTORIES = [
|
|
1484
|
+
process.env.HOME || "/home",
|
|
1485
|
+
"/tmp"
|
|
1486
|
+
];
|
|
1487
|
+
function isPathAllowed(filePath) {
|
|
1488
|
+
const resolved = path3.resolve(filePath);
|
|
1489
|
+
return ALLOWED_DIRECTORIES.some((dir) => resolved.startsWith(dir));
|
|
1490
|
+
}
|
|
1491
|
+
var readFileTool = {
|
|
1492
|
+
name: "read_file",
|
|
1493
|
+
description: "Read the contents of a local file. Use this to analyze configuration files, logs, or code.",
|
|
1494
|
+
parameters: {
|
|
1495
|
+
type: "object",
|
|
1496
|
+
properties: {
|
|
1497
|
+
path: {
|
|
1498
|
+
type: "string",
|
|
1499
|
+
description: "Absolute or relative path to the file to read"
|
|
1500
|
+
},
|
|
1501
|
+
maxLines: {
|
|
1502
|
+
type: "number",
|
|
1503
|
+
description: "Maximum number of lines to read (default: 500)"
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
required: ["path"]
|
|
1507
|
+
},
|
|
1508
|
+
handler: async (params) => {
|
|
1509
|
+
const filePath = params.path;
|
|
1510
|
+
const maxLines = params.maxLines || 500;
|
|
1511
|
+
if (!isPathAllowed(filePath)) {
|
|
1512
|
+
return `Error: Access denied. Path "${filePath}" is outside allowed directories.`;
|
|
1513
|
+
}
|
|
1514
|
+
try {
|
|
1515
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
1516
|
+
const lines = content.split("\n");
|
|
1517
|
+
if (lines.length > maxLines) {
|
|
1518
|
+
return lines.slice(0, maxLines).join("\n") + `
|
|
1519
|
+
|
|
1520
|
+
[Truncated: ${lines.length - maxLines} more lines]`;
|
|
1521
|
+
}
|
|
1522
|
+
return content;
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
if (error.code === "ENOENT") {
|
|
1525
|
+
return `Error: File not found: ${filePath}`;
|
|
1526
|
+
}
|
|
1527
|
+
return `Error reading file: ${error}`;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
var listDirectoryTool = {
|
|
1532
|
+
name: "list_directory",
|
|
1533
|
+
description: "List contents of a directory. Useful for exploring project structure.",
|
|
1534
|
+
parameters: {
|
|
1535
|
+
type: "object",
|
|
1536
|
+
properties: {
|
|
1537
|
+
path: {
|
|
1538
|
+
type: "string",
|
|
1539
|
+
description: "Path to the directory to list"
|
|
1540
|
+
},
|
|
1541
|
+
recursive: {
|
|
1542
|
+
type: "boolean",
|
|
1543
|
+
description: "Whether to list recursively (default: false, max depth: 3)"
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
required: ["path"]
|
|
1547
|
+
},
|
|
1548
|
+
handler: async (params) => {
|
|
1549
|
+
const dirPath = params.path;
|
|
1550
|
+
const recursive = params.recursive || false;
|
|
1551
|
+
if (!isPathAllowed(dirPath)) {
|
|
1552
|
+
return `Error: Access denied. Path "${dirPath}" is outside allowed directories.`;
|
|
1553
|
+
}
|
|
1554
|
+
async function listDir(dir, depth = 0) {
|
|
1555
|
+
if (depth > 3) return [];
|
|
1556
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1557
|
+
const results = [];
|
|
1558
|
+
for (const entry of entries) {
|
|
1559
|
+
const prefix = " ".repeat(depth);
|
|
1560
|
+
const fullPath = path3.join(dir, entry.name);
|
|
1561
|
+
if (entry.isDirectory()) {
|
|
1562
|
+
results.push(`${prefix}\u{1F4C1} ${entry.name}/`);
|
|
1563
|
+
if (recursive) {
|
|
1564
|
+
results.push(...await listDir(fullPath, depth + 1));
|
|
1565
|
+
}
|
|
1566
|
+
} else {
|
|
1567
|
+
const stats = await fs3.stat(fullPath);
|
|
1568
|
+
const size = formatSize(stats.size);
|
|
1569
|
+
results.push(`${prefix}\u{1F4C4} ${entry.name} (${size})`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return results;
|
|
1573
|
+
}
|
|
1574
|
+
try {
|
|
1575
|
+
const contents = await listDir(dirPath);
|
|
1576
|
+
return contents.join("\n");
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
return `Error listing directory: ${error}`;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
function formatSize(bytes) {
|
|
1583
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
1584
|
+
let size = bytes;
|
|
1585
|
+
let unitIndex = 0;
|
|
1586
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
1587
|
+
size /= 1024;
|
|
1588
|
+
unitIndex++;
|
|
1589
|
+
}
|
|
1590
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
1591
|
+
}
|
|
1592
|
+
var analyzeConfigTool = {
|
|
1593
|
+
name: "analyze_config",
|
|
1594
|
+
description: "Analyze a configuration file for DevOps best practices. Supports Kubernetes, Docker, Terraform, and CloudFormation.",
|
|
1595
|
+
parameters: {
|
|
1596
|
+
type: "object",
|
|
1597
|
+
properties: {
|
|
1598
|
+
content: {
|
|
1599
|
+
type: "string",
|
|
1600
|
+
description: "The configuration file content to analyze"
|
|
1601
|
+
},
|
|
1602
|
+
type: {
|
|
1603
|
+
type: "string",
|
|
1604
|
+
enum: ["kubernetes", "docker", "terraform", "cloudformation", "github-actions", "auto"],
|
|
1605
|
+
description: "Type of configuration (auto-detect if not specified)"
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
required: ["content"]
|
|
1609
|
+
},
|
|
1610
|
+
handler: async (params) => {
|
|
1611
|
+
const content = params.content;
|
|
1612
|
+
let configType = params.type || "auto";
|
|
1613
|
+
if (configType === "auto") {
|
|
1614
|
+
configType = detectConfigType(content);
|
|
1615
|
+
}
|
|
1616
|
+
const issues = [];
|
|
1617
|
+
const suggestions = [];
|
|
1618
|
+
switch (configType) {
|
|
1619
|
+
case "kubernetes":
|
|
1620
|
+
analyzeKubernetes(content, issues, suggestions);
|
|
1621
|
+
break;
|
|
1622
|
+
case "docker":
|
|
1623
|
+
analyzeDocker(content, issues, suggestions);
|
|
1624
|
+
break;
|
|
1625
|
+
case "terraform":
|
|
1626
|
+
analyzeTerraform(content, issues, suggestions);
|
|
1627
|
+
break;
|
|
1628
|
+
case "cloudformation":
|
|
1629
|
+
analyzeCloudFormation(content, issues, suggestions);
|
|
1630
|
+
break;
|
|
1631
|
+
case "github-actions":
|
|
1632
|
+
analyzeGitHubActions(content, issues, suggestions);
|
|
1633
|
+
break;
|
|
1634
|
+
default:
|
|
1635
|
+
return `Could not determine configuration type. Please specify the type parameter.`;
|
|
1636
|
+
}
|
|
1637
|
+
let result = `## Configuration Analysis (${configType})
|
|
1638
|
+
|
|
1639
|
+
`;
|
|
1640
|
+
if (issues.length > 0) {
|
|
1641
|
+
result += `### \u26A0\uFE0F Issues Found
|
|
1642
|
+
`;
|
|
1643
|
+
issues.forEach((issue, i) => {
|
|
1644
|
+
result += `${i + 1}. ${issue}
|
|
1645
|
+
`;
|
|
1646
|
+
});
|
|
1647
|
+
result += "\n";
|
|
1648
|
+
} else {
|
|
1649
|
+
result += `### \u2705 No Critical Issues Found
|
|
1650
|
+
|
|
1651
|
+
`;
|
|
1652
|
+
}
|
|
1653
|
+
if (suggestions.length > 0) {
|
|
1654
|
+
result += `### \u{1F4A1} Suggestions
|
|
1655
|
+
`;
|
|
1656
|
+
suggestions.forEach((suggestion, i) => {
|
|
1657
|
+
result += `${i + 1}. ${suggestion}
|
|
1658
|
+
`;
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
return result;
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
function detectConfigType(content) {
|
|
1665
|
+
if (content.includes("apiVersion:") && content.includes("kind:")) return "kubernetes";
|
|
1666
|
+
if (content.includes("FROM ") || content.includes("COPY ") || content.includes("RUN ")) return "docker";
|
|
1667
|
+
if (content.includes('resource "') || content.includes('provider "')) return "terraform";
|
|
1668
|
+
if (content.includes("AWSTemplateFormatVersion") || content.includes("Resources:")) return "cloudformation";
|
|
1669
|
+
if (content.includes("jobs:") && content.includes("runs-on:")) return "github-actions";
|
|
1670
|
+
return "unknown";
|
|
1671
|
+
}
|
|
1672
|
+
function analyzeKubernetes(content, issues, suggestions) {
|
|
1673
|
+
if (!content.includes("resources:") || !content.includes("limits:")) {
|
|
1674
|
+
issues.push("Missing resource limits. Pods without limits can consume excessive cluster resources.");
|
|
1675
|
+
}
|
|
1676
|
+
if (!content.includes("securityContext:")) {
|
|
1677
|
+
suggestions.push("Consider adding securityContext to restrict container privileges.");
|
|
1678
|
+
}
|
|
1679
|
+
if (content.includes(":latest")) {
|
|
1680
|
+
issues.push("Using :latest tag. Pin specific image versions for reproducible deployments.");
|
|
1681
|
+
}
|
|
1682
|
+
if (!content.includes("livenessProbe:") && !content.includes("readinessProbe:")) {
|
|
1683
|
+
suggestions.push("Add health probes (livenessProbe/readinessProbe) for better reliability.");
|
|
1684
|
+
}
|
|
1685
|
+
if (!content.includes("namespace:")) {
|
|
1686
|
+
suggestions.push("Explicitly specify namespace to avoid deploying to default namespace.");
|
|
1687
|
+
}
|
|
1688
|
+
if (content.includes("runAsRoot: true") || content.includes("privileged: true")) {
|
|
1689
|
+
issues.push("Container configured to run as root or privileged. This is a security risk.");
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function analyzeDocker(content, issues, suggestions) {
|
|
1693
|
+
const lines = content.split("\n");
|
|
1694
|
+
const fromLine = lines.find((l) => l.trim().startsWith("FROM "));
|
|
1695
|
+
if (fromLine?.includes(":latest")) {
|
|
1696
|
+
issues.push("Using :latest base image. Pin a specific version for reproducible builds.");
|
|
1697
|
+
}
|
|
1698
|
+
const runCount = lines.filter((l) => l.trim().startsWith("RUN ")).length;
|
|
1699
|
+
if (runCount > 5) {
|
|
1700
|
+
suggestions.push(`${runCount} separate RUN commands. Consider combining them to reduce image layers.`);
|
|
1701
|
+
}
|
|
1702
|
+
if (content.includes("ADD ") && !content.includes(".tar") && !content.includes("http")) {
|
|
1703
|
+
suggestions.push("Use COPY instead of ADD for simple file copying. ADD has extra features that may be unnecessary.");
|
|
1704
|
+
}
|
|
1705
|
+
if (content.includes("COPY . ") || content.includes("COPY ./ ")) {
|
|
1706
|
+
suggestions.push("Copying entire context. Ensure .dockerignore is configured to exclude unnecessary files.");
|
|
1707
|
+
}
|
|
1708
|
+
if (!content.includes("USER ")) {
|
|
1709
|
+
suggestions.push("No USER directive. Consider running as non-root user for security.");
|
|
1710
|
+
}
|
|
1711
|
+
if (!content.includes("AS ") && content.includes("npm install") || content.includes("go build")) {
|
|
1712
|
+
suggestions.push("Consider multi-stage builds to reduce final image size.");
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function analyzeTerraform(content, issues, suggestions) {
|
|
1716
|
+
if (!content.includes("required_version") && !content.includes("required_providers")) {
|
|
1717
|
+
issues.push("Missing version constraints. Pin Terraform and provider versions for reproducibility.");
|
|
1718
|
+
}
|
|
1719
|
+
const hardcodedPatterns = [
|
|
1720
|
+
/ami-[a-z0-9]+/,
|
|
1721
|
+
/subnet-[a-z0-9]+/,
|
|
1722
|
+
/sg-[a-z0-9]+/,
|
|
1723
|
+
/vpc-[a-z0-9]+/
|
|
1724
|
+
];
|
|
1725
|
+
for (const pattern of hardcodedPatterns) {
|
|
1726
|
+
if (pattern.test(content)) {
|
|
1727
|
+
suggestions.push("Hardcoded AWS resource IDs detected. Consider using data sources or variables.");
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (!content.includes('backend "')) {
|
|
1732
|
+
suggestions.push("No remote backend configured. Use S3/GCS/Azure for team collaboration.");
|
|
1733
|
+
}
|
|
1734
|
+
if (content.includes("password") || content.includes("secret") || content.includes("api_key")) {
|
|
1735
|
+
if (!content.includes("sensitive = true")) {
|
|
1736
|
+
issues.push("Potentially sensitive variables without sensitive = true flag.");
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (content.includes("aws_s3_bucket") && !content.includes("server_side_encryption")) {
|
|
1740
|
+
suggestions.push("S3 bucket without explicit encryption configuration.");
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
function analyzeCloudFormation(content, issues, suggestions) {
|
|
1744
|
+
if (content.includes("AWS::RDS::") || content.includes("AWS::S3::Bucket")) {
|
|
1745
|
+
if (!content.includes("DeletionPolicy")) {
|
|
1746
|
+
issues.push("Stateful resources without DeletionPolicy. Data may be lost on stack deletion.");
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (!content.includes("UpdateReplacePolicy")) {
|
|
1750
|
+
suggestions.push("Consider adding UpdateReplacePolicy for stateful resources.");
|
|
1751
|
+
}
|
|
1752
|
+
suggestions.push("Run `aws cloudformation detect-stack-drift` regularly to catch manual changes.");
|
|
1753
|
+
if (content.includes("Parameters:") && !content.includes("AllowedValues")) {
|
|
1754
|
+
suggestions.push("Consider adding AllowedValues constraints to parameters.");
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function analyzeGitHubActions(content, issues, suggestions) {
|
|
1758
|
+
if (content.includes("uses: ") && !content.includes("@v") && !content.includes("@sha")) {
|
|
1759
|
+
issues.push("Actions without version pinning. Pin to specific versions or SHA for security.");
|
|
1760
|
+
}
|
|
1761
|
+
if (content.includes("${{ secrets.") && content.includes("echo ")) {
|
|
1762
|
+
issues.push("Potential secret exposure in echo/print commands.");
|
|
1763
|
+
}
|
|
1764
|
+
if (!content.includes("permissions:")) {
|
|
1765
|
+
suggestions.push("Explicitly define job permissions for better security (least privilege).");
|
|
1766
|
+
}
|
|
1767
|
+
if ((content.includes("npm ") || content.includes("pip ") || content.includes("go ")) && !content.includes("actions/cache")) {
|
|
1768
|
+
suggestions.push("Consider adding caching to speed up workflows.");
|
|
1769
|
+
}
|
|
1770
|
+
if (!content.includes("timeout-minutes:")) {
|
|
1771
|
+
suggestions.push("Add timeout-minutes to prevent stuck workflows from running indefinitely.");
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
var analyzeErrorTool = {
|
|
1775
|
+
name: "analyze_error",
|
|
1776
|
+
description: "Analyze error messages or logs to diagnose issues and suggest solutions.",
|
|
1777
|
+
parameters: {
|
|
1778
|
+
type: "object",
|
|
1779
|
+
properties: {
|
|
1780
|
+
error: {
|
|
1781
|
+
type: "string",
|
|
1782
|
+
description: "The error message or log content to analyze"
|
|
1783
|
+
},
|
|
1784
|
+
context: {
|
|
1785
|
+
type: "string",
|
|
1786
|
+
enum: ["kubernetes", "docker", "terraform", "aws", "linux", "nodejs", "python", "general"],
|
|
1787
|
+
description: "Context/environment where the error occurred"
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
required: ["error"]
|
|
1791
|
+
},
|
|
1792
|
+
handler: async (params) => {
|
|
1793
|
+
const error = params.error;
|
|
1794
|
+
const context = params.context || "general";
|
|
1795
|
+
const analysis = [];
|
|
1796
|
+
const possibleCauses = [];
|
|
1797
|
+
const solutions = [];
|
|
1798
|
+
if (error.includes("permission denied") || error.includes("EACCES")) {
|
|
1799
|
+
possibleCauses.push("Insufficient file system permissions");
|
|
1800
|
+
solutions.push("Check file/directory permissions with `ls -la`");
|
|
1801
|
+
solutions.push("Try running with appropriate user or `sudo` if necessary");
|
|
1802
|
+
}
|
|
1803
|
+
if (error.includes("connection refused") || error.includes("ECONNREFUSED")) {
|
|
1804
|
+
possibleCauses.push("Target service is not running or not listening on expected port");
|
|
1805
|
+
solutions.push("Verify the service is running: `systemctl status <service>`");
|
|
1806
|
+
solutions.push("Check if the port is open: `netstat -tlnp | grep <port>`");
|
|
1807
|
+
}
|
|
1808
|
+
if (error.includes("out of memory") || error.includes("OOMKilled")) {
|
|
1809
|
+
possibleCauses.push("Application exceeded memory limits");
|
|
1810
|
+
solutions.push("Increase memory limits if possible");
|
|
1811
|
+
solutions.push("Profile memory usage to find leaks");
|
|
1812
|
+
solutions.push("Consider horizontal scaling");
|
|
1813
|
+
}
|
|
1814
|
+
if (error.includes("timeout") || error.includes("ETIMEDOUT")) {
|
|
1815
|
+
possibleCauses.push("Network latency or unreachable endpoint");
|
|
1816
|
+
possibleCauses.push("Slow database queries or API responses");
|
|
1817
|
+
solutions.push("Check network connectivity: `ping`, `traceroute`");
|
|
1818
|
+
solutions.push("Review and optimize slow queries");
|
|
1819
|
+
solutions.push("Consider increasing timeout values");
|
|
1820
|
+
}
|
|
1821
|
+
if (context === "kubernetes") {
|
|
1822
|
+
if (error.includes("CrashLoopBackOff")) {
|
|
1823
|
+
possibleCauses.push("Container fails to start or crashes immediately");
|
|
1824
|
+
solutions.push("Check container logs: `kubectl logs <pod> --previous`");
|
|
1825
|
+
solutions.push("Verify image exists and is pullable");
|
|
1826
|
+
solutions.push("Check resource limits and requests");
|
|
1827
|
+
}
|
|
1828
|
+
if (error.includes("ImagePullBackOff")) {
|
|
1829
|
+
possibleCauses.push("Cannot pull container image");
|
|
1830
|
+
solutions.push("Verify image name and tag");
|
|
1831
|
+
solutions.push("Check image registry credentials (imagePullSecrets)");
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (context === "docker") {
|
|
1835
|
+
if (error.includes("no space left on device")) {
|
|
1836
|
+
possibleCauses.push("Docker disk space exhausted");
|
|
1837
|
+
solutions.push("Clean up: `docker system prune -a`");
|
|
1838
|
+
solutions.push("Remove unused images: `docker image prune`");
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (possibleCauses.length === 0) {
|
|
1842
|
+
analysis.push("No specific pattern matched. Consider:");
|
|
1843
|
+
analysis.push("- Searching the error message in documentation");
|
|
1844
|
+
analysis.push("- Checking application logs for more context");
|
|
1845
|
+
analysis.push("- Verifying configuration and environment variables");
|
|
1846
|
+
}
|
|
1847
|
+
let result = `## Error Analysis
|
|
1848
|
+
|
|
1849
|
+
`;
|
|
1850
|
+
result += `**Context:** ${context}
|
|
1851
|
+
|
|
1852
|
+
`;
|
|
1853
|
+
if (possibleCauses.length > 0) {
|
|
1854
|
+
result += `### Possible Causes
|
|
1855
|
+
`;
|
|
1856
|
+
possibleCauses.forEach((cause) => result += `- ${cause}
|
|
1857
|
+
`);
|
|
1858
|
+
result += "\n";
|
|
1859
|
+
}
|
|
1860
|
+
if (solutions.length > 0) {
|
|
1861
|
+
result += `### Suggested Solutions
|
|
1862
|
+
`;
|
|
1863
|
+
solutions.forEach((solution, i) => result += `${i + 1}. ${solution}
|
|
1864
|
+
`);
|
|
1865
|
+
result += "\n";
|
|
1866
|
+
}
|
|
1867
|
+
if (analysis.length > 0) {
|
|
1868
|
+
result += `### Notes
|
|
1869
|
+
`;
|
|
1870
|
+
analysis.forEach((note) => result += `${note}
|
|
1871
|
+
`);
|
|
1872
|
+
}
|
|
1873
|
+
return result;
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
var devopsTools = [
|
|
1877
|
+
readFileTool,
|
|
1878
|
+
listDirectoryTool,
|
|
1879
|
+
analyzeConfigTool,
|
|
1880
|
+
analyzeErrorTool
|
|
1881
|
+
];
|
|
1882
|
+
function getToolByName(name) {
|
|
1883
|
+
return devopsTools.find((tool) => tool.name === name);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// src/services/copilot.service.ts
|
|
1887
|
+
var MCP_SERVERS = {
|
|
1888
|
+
github: {
|
|
1889
|
+
type: "http",
|
|
1890
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
1891
|
+
tools: ["*"]
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
var CopilotService = class {
|
|
1895
|
+
constructor(sessionService) {
|
|
1896
|
+
this.sessionService = sessionService;
|
|
1897
|
+
}
|
|
1898
|
+
client = null;
|
|
1899
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1900
|
+
initialized = false;
|
|
1901
|
+
mockMode = false;
|
|
1902
|
+
async initialize() {
|
|
1903
|
+
try {
|
|
1904
|
+
this.client = new CopilotClient();
|
|
1905
|
+
await this.client.start();
|
|
1906
|
+
this.initialized = true;
|
|
1907
|
+
console.log("[CopilotService] Initialized successfully");
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
console.error("[CopilotService] Failed to initialize:", error);
|
|
1910
|
+
this.mockMode = true;
|
|
1911
|
+
this.initialized = true;
|
|
1912
|
+
console.warn("[CopilotService] Running in mock mode");
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
isReady() {
|
|
1916
|
+
return this.initialized;
|
|
1917
|
+
}
|
|
1918
|
+
isMockMode() {
|
|
1919
|
+
return this.mockMode;
|
|
1920
|
+
}
|
|
1921
|
+
async createCopilotSession(sessionId, type, model, systemPrompt, enableMcp = false) {
|
|
1922
|
+
if (this.mockMode || !this.client) {
|
|
1923
|
+
this.sessions.set(sessionId, {
|
|
1924
|
+
sessionId,
|
|
1925
|
+
session: null,
|
|
1926
|
+
type
|
|
1927
|
+
});
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const agentConfig = getAgentConfig(type);
|
|
1931
|
+
const typeConfig = SESSION_TYPE_CONFIGS[type];
|
|
1932
|
+
const tools = type === "devops" ? this.buildSdkTools() : void 0;
|
|
1933
|
+
const mcpServers = enableMcp ? MCP_SERVERS : void 0;
|
|
1934
|
+
const session = await this.client.createSession({
|
|
1935
|
+
sessionId,
|
|
1936
|
+
model: model || typeConfig.defaultModel,
|
|
1937
|
+
streaming: true,
|
|
1938
|
+
customAgents: agentConfig ? [agentConfig] : void 0,
|
|
1939
|
+
systemMessage: systemPrompt ? { content: systemPrompt } : void 0,
|
|
1940
|
+
tools,
|
|
1941
|
+
mcpServers
|
|
1942
|
+
});
|
|
1943
|
+
this.sessions.set(sessionId, { sessionId, session, type });
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Convert our Tool definitions to Copilot SDK Tool format.
|
|
1947
|
+
* The SDK expects tools with a `handler` function — it calls the handler
|
|
1948
|
+
* directly and uses the return value as the tool result (no sendToolResult needed).
|
|
1949
|
+
*/
|
|
1950
|
+
buildSdkTools() {
|
|
1951
|
+
return devopsTools.map((tool) => ({
|
|
1952
|
+
name: tool.name,
|
|
1953
|
+
description: tool.description,
|
|
1954
|
+
parameters: tool.parameters,
|
|
1955
|
+
handler: async (args) => {
|
|
1956
|
+
console.log(`[CopilotService] Executing tool ${tool.name}`);
|
|
1957
|
+
try {
|
|
1958
|
+
return await tool.handler(args);
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
console.error(`[CopilotService] Tool ${tool.name} failed:`, error);
|
|
1961
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}));
|
|
1965
|
+
}
|
|
1966
|
+
async resumeCopilotSession(sessionId) {
|
|
1967
|
+
if (this.mockMode || !this.client) {
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
try {
|
|
1971
|
+
const session = await this.client.resumeSession(sessionId);
|
|
1972
|
+
const dbSession = this.sessionService.getSession(sessionId);
|
|
1973
|
+
this.sessions.set(sessionId, { sessionId, session, type: dbSession?.type || "general" });
|
|
1974
|
+
console.log(`[CopilotService] Session ${sessionId} resumed from disk`);
|
|
1975
|
+
return true;
|
|
1976
|
+
} catch (resumeError) {
|
|
1977
|
+
console.log(`[CopilotService] Could not resume session ${sessionId}, will try to create new`);
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
const dbSession = this.sessionService.getSession(sessionId);
|
|
1981
|
+
if (!dbSession) {
|
|
1982
|
+
console.error(`[CopilotService] Session ${sessionId} not found in database`);
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
await this.createCopilotSession(
|
|
1986
|
+
sessionId,
|
|
1987
|
+
dbSession.type,
|
|
1988
|
+
dbSession.model,
|
|
1989
|
+
dbSession.systemPrompt || void 0,
|
|
1990
|
+
false
|
|
1991
|
+
// MCP disabled by default on recreate
|
|
1992
|
+
);
|
|
1993
|
+
console.log(`[CopilotService] Session ${sessionId} recreated successfully`);
|
|
1994
|
+
return true;
|
|
1995
|
+
} catch (createError) {
|
|
1996
|
+
console.error(`[CopilotService] Failed to create session ${sessionId}:`, createError);
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
async sendMessage(sessionId, prompt, context, onEvent) {
|
|
2001
|
+
return this.withRetry(async () => {
|
|
2002
|
+
const copilotSession = this.sessions.get(sessionId);
|
|
2003
|
+
let fullPrompt = prompt;
|
|
2004
|
+
if (context?.selectedText && !prompt.includes(context.selectedText)) {
|
|
2005
|
+
fullPrompt = `Context (selected text):
|
|
2006
|
+
${context.selectedText}
|
|
2007
|
+
|
|
2008
|
+
User request: ${prompt}`;
|
|
2009
|
+
}
|
|
2010
|
+
if (context?.pageUrl && !prompt.includes(context.pageUrl)) {
|
|
2011
|
+
fullPrompt = `Page: ${context.pageUrl}
|
|
2012
|
+
${fullPrompt}`;
|
|
2013
|
+
}
|
|
2014
|
+
if (this.mockMode || !copilotSession?.session) {
|
|
2015
|
+
return this.generateMockResponse(prompt, context);
|
|
2016
|
+
}
|
|
2017
|
+
let responseContent = "";
|
|
2018
|
+
if (onEvent) {
|
|
2019
|
+
copilotSession.session.on(onEvent);
|
|
2020
|
+
}
|
|
2021
|
+
copilotSession.session.on((event) => {
|
|
2022
|
+
if (event.type === "assistant.message") {
|
|
2023
|
+
responseContent = event.data.content || "";
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
const response = await copilotSession.session.sendAndWait({ prompt: fullPrompt });
|
|
2027
|
+
console.log(`[CopilotService] Received response for session ${sessionId}`);
|
|
2028
|
+
console.log(`[CopilotService] Response: ${response?.data}...`);
|
|
2029
|
+
console.log(`[CopilotService] responseContent: ${responseContent}...`);
|
|
2030
|
+
return response?.data.content || responseContent;
|
|
2031
|
+
}, 3, 1e3);
|
|
2032
|
+
}
|
|
2033
|
+
async streamMessage(sessionId, prompt, context, onEvent, attachments) {
|
|
2034
|
+
let copilotSession = this.sessions.get(sessionId);
|
|
2035
|
+
if (!copilotSession?.session && !this.mockMode) {
|
|
2036
|
+
console.log(`[CopilotService] Session ${sessionId} not in memory, attempting auto-resume...`);
|
|
2037
|
+
const resumed = await this.resumeCopilotSession(sessionId);
|
|
2038
|
+
if (resumed) {
|
|
2039
|
+
copilotSession = this.sessions.get(sessionId);
|
|
2040
|
+
console.log(`[CopilotService] Session ${sessionId} auto-resumed successfully`);
|
|
2041
|
+
} else {
|
|
2042
|
+
console.warn(`[CopilotService] Failed to auto-resume session ${sessionId}, falling back to mock`);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
let fullPrompt = prompt;
|
|
2046
|
+
if (context?.selectedText && !prompt.includes(context.selectedText)) {
|
|
2047
|
+
fullPrompt = `Context (selected text):
|
|
2048
|
+
${context.selectedText}
|
|
2049
|
+
|
|
2050
|
+
User request: ${prompt}`;
|
|
2051
|
+
}
|
|
2052
|
+
if (this.mockMode || !copilotSession?.session) {
|
|
2053
|
+
console.log("[CopilotService] Streaming mock response");
|
|
2054
|
+
await this.streamMockResponse(prompt, context, onEvent);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
console.log("[CopilotService] Starting real stream for session", sessionId);
|
|
2058
|
+
if (attachments && attachments.length > 0) {
|
|
2059
|
+
console.log(`[CopilotService] Sending ${attachments.length} attachments:`, attachments.map((a) => a.path));
|
|
2060
|
+
}
|
|
2061
|
+
if (onEvent) {
|
|
2062
|
+
copilotSession.session.on(onEvent);
|
|
2063
|
+
}
|
|
2064
|
+
copilotSession.session.send({
|
|
2065
|
+
prompt: fullPrompt,
|
|
2066
|
+
attachments
|
|
2067
|
+
});
|
|
2068
|
+
console.log("[CopilotService] Message sent, waiting for events...");
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Retry logic for transient errors
|
|
2072
|
+
*/
|
|
2073
|
+
async withRetry(operation, maxRetries = 3, delayMs = 1e3) {
|
|
2074
|
+
let lastError;
|
|
2075
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2076
|
+
try {
|
|
2077
|
+
return await operation();
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2080
|
+
const nonRetryableErrors = ["authentication", "invalid_session", "rate_limit"];
|
|
2081
|
+
if (nonRetryableErrors.some((e) => lastError.message.toLowerCase().includes(e))) {
|
|
2082
|
+
throw lastError;
|
|
2083
|
+
}
|
|
2084
|
+
if (attempt < maxRetries) {
|
|
2085
|
+
console.warn(`[CopilotService] Attempt ${attempt} failed, retrying in ${delayMs}ms...`);
|
|
2086
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
2087
|
+
delayMs *= 2;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
throw lastError;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Get available tools for a session type
|
|
2095
|
+
*/
|
|
2096
|
+
getAvailableTools(type) {
|
|
2097
|
+
if (type === "devops") {
|
|
2098
|
+
return devopsTools.map((t) => ({ name: t.name, description: t.description }));
|
|
2099
|
+
}
|
|
2100
|
+
return [];
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Execute a tool directly (for testing or standalone use)
|
|
2104
|
+
*/
|
|
2105
|
+
async executeTool(toolName, params) {
|
|
2106
|
+
const tool = getToolByName(toolName);
|
|
2107
|
+
if (!tool) {
|
|
2108
|
+
return { success: false, error: `Unknown tool: ${toolName}` };
|
|
2109
|
+
}
|
|
2110
|
+
try {
|
|
2111
|
+
const result = await tool.handler(params);
|
|
2112
|
+
return { success: true, result };
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
return {
|
|
2115
|
+
success: false,
|
|
2116
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
async abortRequest(sessionId) {
|
|
2121
|
+
const copilotSession = this.sessions.get(sessionId);
|
|
2122
|
+
if (copilotSession?.session && !this.mockMode) {
|
|
2123
|
+
await copilotSession.session.abort();
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
async destroySession(sessionId) {
|
|
2127
|
+
const copilotSession = this.sessions.get(sessionId);
|
|
2128
|
+
if (copilotSession?.session && !this.mockMode) {
|
|
2129
|
+
try {
|
|
2130
|
+
await copilotSession.session.abort().catch(() => {
|
|
2131
|
+
});
|
|
2132
|
+
await copilotSession.session.destroy();
|
|
2133
|
+
console.log(`[CopilotService] Session ${sessionId} destroyed successfully`);
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
console.error(`[CopilotService] Error destroying session ${sessionId}:`, error);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (this.client && !this.mockMode) {
|
|
2139
|
+
try {
|
|
2140
|
+
await this.client.deleteSession(sessionId);
|
|
2141
|
+
console.log(`[CopilotService] Session ${sessionId} files deleted from disk`);
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
console.log(`[CopilotService] Could not delete session files (may not exist): ${sessionId}`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
this.sessions.delete(sessionId);
|
|
2147
|
+
console.log(`[CopilotService] Session ${sessionId} removed from memory`);
|
|
2148
|
+
}
|
|
2149
|
+
async shutdown() {
|
|
2150
|
+
for (const [sessionId, copilotSession] of this.sessions) {
|
|
2151
|
+
try {
|
|
2152
|
+
if (copilotSession.session && !this.mockMode) {
|
|
2153
|
+
await copilotSession.session.destroy();
|
|
2154
|
+
}
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
console.error(`[CopilotService] Failed to destroy session ${sessionId}:`, error);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
this.sessions.clear();
|
|
2160
|
+
if (this.client && !this.mockMode) {
|
|
2161
|
+
try {
|
|
2162
|
+
await this.client.stop();
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
console.error("[CopilotService] Failed to stop client:", error);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
this.initialized = false;
|
|
2168
|
+
}
|
|
2169
|
+
// Mock implementations for development without Copilot CLI
|
|
2170
|
+
generateMockResponse(prompt, context) {
|
|
2171
|
+
const action = context?.action;
|
|
2172
|
+
if (action === "explain") {
|
|
2173
|
+
return `**Explanation:**
|
|
2174
|
+
|
|
2175
|
+
This appears to be ${context?.selectedText?.slice(0, 50)}...
|
|
2176
|
+
|
|
2177
|
+
In essence, this code/text demonstrates a common pattern used in software development. The key points are:
|
|
2178
|
+
|
|
2179
|
+
1. It handles a specific use case
|
|
2180
|
+
2. It follows best practices
|
|
2181
|
+
3. It can be extended for additional functionality`;
|
|
2182
|
+
}
|
|
2183
|
+
if (action === "translate") {
|
|
2184
|
+
return `**Translation:**
|
|
2185
|
+
|
|
2186
|
+
${context?.selectedText || "No text provided"}`;
|
|
2187
|
+
}
|
|
2188
|
+
if (action === "rewrite") {
|
|
2189
|
+
return `**Rewritten:**
|
|
2190
|
+
|
|
2191
|
+
${context?.selectedText || prompt}`;
|
|
2192
|
+
}
|
|
2193
|
+
if (action === "fix_grammar") {
|
|
2194
|
+
return `**Corrected:**
|
|
2195
|
+
|
|
2196
|
+
${context?.selectedText || prompt}`;
|
|
2197
|
+
}
|
|
2198
|
+
return `**Mock Response:**
|
|
2199
|
+
|
|
2200
|
+
I understand you're asking about: "${prompt.slice(0, 100)}..."
|
|
2201
|
+
|
|
2202
|
+
This is a mock response because the Copilot SDK is not available. In production, you would receive intelligent responses powered by GitHub Copilot.
|
|
2203
|
+
|
|
2204
|
+
To enable real responses:
|
|
2205
|
+
1. Install GitHub Copilot CLI
|
|
2206
|
+
2. Authenticate with your GitHub account
|
|
2207
|
+
3. Restart the DevMentorAI backend`;
|
|
2208
|
+
}
|
|
2209
|
+
async streamMockResponse(prompt, context, onEvent) {
|
|
2210
|
+
const response = this.generateMockResponse(prompt, context);
|
|
2211
|
+
const words = response.split(" ");
|
|
2212
|
+
for (let i = 0; i < words.length; i++) {
|
|
2213
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
2214
|
+
onEvent?.({
|
|
2215
|
+
type: "assistant.message_delta",
|
|
2216
|
+
data: { deltaContent: words[i] + (i < words.length - 1 ? " " : "") }
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
onEvent?.({
|
|
2220
|
+
type: "assistant.message",
|
|
2221
|
+
data: { content: response }
|
|
2222
|
+
});
|
|
2223
|
+
onEvent?.({
|
|
2224
|
+
type: "session.idle",
|
|
2225
|
+
data: {}
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
// src/services/session.service.ts
|
|
2231
|
+
var SessionService = class {
|
|
2232
|
+
constructor(db) {
|
|
2233
|
+
this.db = db;
|
|
2234
|
+
}
|
|
2235
|
+
// Sessions
|
|
2236
|
+
listSessions(page = 1, pageSize = 50) {
|
|
2237
|
+
const offset = (page - 1) * pageSize;
|
|
2238
|
+
const countStmt = this.db.prepare("SELECT COUNT(*) as count FROM sessions");
|
|
2239
|
+
const count = countStmt.get().count;
|
|
2240
|
+
const stmt = this.db.prepare(`
|
|
2241
|
+
SELECT * FROM sessions
|
|
2242
|
+
ORDER BY updated_at DESC
|
|
2243
|
+
LIMIT ? OFFSET ?
|
|
2244
|
+
`);
|
|
2245
|
+
const rows = stmt.all(pageSize, offset);
|
|
2246
|
+
return {
|
|
2247
|
+
items: rows.map(this.mapDbSession),
|
|
2248
|
+
total: count,
|
|
2249
|
+
page,
|
|
2250
|
+
pageSize,
|
|
2251
|
+
hasMore: offset + rows.length < count
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
getSession(id) {
|
|
2255
|
+
const stmt = this.db.prepare("SELECT * FROM sessions WHERE id = ?");
|
|
2256
|
+
const row = stmt.get(id);
|
|
2257
|
+
return row ? this.mapDbSession(row) : null;
|
|
2258
|
+
}
|
|
2259
|
+
createSession(request) {
|
|
2260
|
+
const id = generateSessionId();
|
|
2261
|
+
const now = formatDate();
|
|
2262
|
+
const agentConfig = getAgentConfig(request.type);
|
|
2263
|
+
const model = request.model || getDefaultModel(request.type);
|
|
2264
|
+
const stmt = this.db.prepare(`
|
|
2265
|
+
INSERT INTO sessions (id, name, type, model, system_prompt, custom_agent, created_at, updated_at)
|
|
2266
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2267
|
+
`);
|
|
2268
|
+
stmt.run(
|
|
2269
|
+
id,
|
|
2270
|
+
request.name,
|
|
2271
|
+
request.type,
|
|
2272
|
+
model,
|
|
2273
|
+
request.systemPrompt || agentConfig?.prompt || null,
|
|
2274
|
+
agentConfig?.name || null,
|
|
2275
|
+
now,
|
|
2276
|
+
now
|
|
2277
|
+
);
|
|
2278
|
+
return this.getSession(id);
|
|
2279
|
+
}
|
|
2280
|
+
updateSession(id, request) {
|
|
2281
|
+
const session = this.getSession(id);
|
|
2282
|
+
if (!session) return null;
|
|
2283
|
+
const updates = [];
|
|
2284
|
+
const values = [];
|
|
2285
|
+
if (request.name !== void 0) {
|
|
2286
|
+
updates.push("name = ?");
|
|
2287
|
+
values.push(request.name);
|
|
2288
|
+
}
|
|
2289
|
+
if (request.status !== void 0) {
|
|
2290
|
+
updates.push("status = ?");
|
|
2291
|
+
values.push(request.status);
|
|
2292
|
+
}
|
|
2293
|
+
if (updates.length === 0) return session;
|
|
2294
|
+
updates.push("updated_at = ?");
|
|
2295
|
+
values.push(formatDate());
|
|
2296
|
+
values.push(id);
|
|
2297
|
+
const stmt = this.db.prepare(`
|
|
2298
|
+
UPDATE sessions SET ${updates.join(", ")} WHERE id = ?
|
|
2299
|
+
`);
|
|
2300
|
+
stmt.run(...values);
|
|
2301
|
+
return this.getSession(id);
|
|
2302
|
+
}
|
|
2303
|
+
deleteSession(id) {
|
|
2304
|
+
const stmt = this.db.prepare("DELETE FROM sessions WHERE id = ?");
|
|
2305
|
+
const result = stmt.run(id);
|
|
2306
|
+
return result.changes > 0;
|
|
2307
|
+
}
|
|
2308
|
+
incrementMessageCount(sessionId) {
|
|
2309
|
+
const stmt = this.db.prepare(`
|
|
2310
|
+
UPDATE sessions
|
|
2311
|
+
SET message_count = message_count + 1, updated_at = ?
|
|
2312
|
+
WHERE id = ?
|
|
2313
|
+
`);
|
|
2314
|
+
stmt.run(formatDate(), sessionId);
|
|
2315
|
+
}
|
|
2316
|
+
// Messages
|
|
2317
|
+
listMessages(sessionId, page = 1, pageSize = 100) {
|
|
2318
|
+
const offset = (page - 1) * pageSize;
|
|
2319
|
+
const countStmt = this.db.prepare("SELECT COUNT(*) as count FROM messages WHERE session_id = ?");
|
|
2320
|
+
const count = countStmt.get(sessionId).count;
|
|
2321
|
+
const stmt = this.db.prepare(`
|
|
2322
|
+
SELECT * FROM messages
|
|
2323
|
+
WHERE session_id = ?
|
|
2324
|
+
ORDER BY timestamp ASC
|
|
2325
|
+
LIMIT ? OFFSET ?
|
|
2326
|
+
`);
|
|
2327
|
+
const rows = stmt.all(sessionId, pageSize, offset);
|
|
2328
|
+
return {
|
|
2329
|
+
items: rows.map((row) => this.mapDbMessage(row)),
|
|
2330
|
+
total: count,
|
|
2331
|
+
page,
|
|
2332
|
+
pageSize,
|
|
2333
|
+
hasMore: offset + rows.length < count
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
addMessage(sessionId, role, content, metadata) {
|
|
2337
|
+
const id = generateMessageId();
|
|
2338
|
+
const timestamp = formatDate();
|
|
2339
|
+
const stmt = this.db.prepare(`
|
|
2340
|
+
INSERT INTO messages (id, session_id, role, content, timestamp, metadata)
|
|
2341
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2342
|
+
`);
|
|
2343
|
+
stmt.run(
|
|
2344
|
+
id,
|
|
2345
|
+
sessionId,
|
|
2346
|
+
role,
|
|
2347
|
+
content,
|
|
2348
|
+
timestamp,
|
|
2349
|
+
metadata ? JSON.stringify(metadata) : null
|
|
2350
|
+
);
|
|
2351
|
+
this.incrementMessageCount(sessionId);
|
|
2352
|
+
return {
|
|
2353
|
+
id,
|
|
2354
|
+
sessionId,
|
|
2355
|
+
role,
|
|
2356
|
+
content,
|
|
2357
|
+
timestamp,
|
|
2358
|
+
metadata
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
updateMessageContent(messageId, content) {
|
|
2362
|
+
const stmt = this.db.prepare("UPDATE messages SET content = ? WHERE id = ?");
|
|
2363
|
+
stmt.run(content, messageId);
|
|
2364
|
+
}
|
|
2365
|
+
updateMessageMetadata(messageId, metadata) {
|
|
2366
|
+
const stmt = this.db.prepare("UPDATE messages SET metadata = ? WHERE id = ?");
|
|
2367
|
+
stmt.run(JSON.stringify(metadata), messageId);
|
|
2368
|
+
}
|
|
2369
|
+
// ============================================================================
|
|
2370
|
+
// Context Persistence (Phase 5)
|
|
2371
|
+
// ============================================================================
|
|
2372
|
+
/**
|
|
2373
|
+
* Save context for a session (associated with a message)
|
|
2374
|
+
*/
|
|
2375
|
+
saveContext(sessionId, contextJson, messageId, pageUrl, pageTitle, platform) {
|
|
2376
|
+
const id = `ctx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2377
|
+
const now = formatDate();
|
|
2378
|
+
const stmt = this.db.prepare(`
|
|
2379
|
+
INSERT INTO session_contexts (id, session_id, message_id, context_json, page_url, page_title, platform, extracted_at)
|
|
2380
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2381
|
+
`);
|
|
2382
|
+
stmt.run(id, sessionId, messageId || null, contextJson, pageUrl || null, pageTitle || null, platform || null, now);
|
|
2383
|
+
return id;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Get the most recent context for a session
|
|
2387
|
+
*/
|
|
2388
|
+
getLatestContext(sessionId) {
|
|
2389
|
+
const stmt = this.db.prepare(`
|
|
2390
|
+
SELECT id, context_json, page_url, platform, extracted_at
|
|
2391
|
+
FROM session_contexts
|
|
2392
|
+
WHERE session_id = ?
|
|
2393
|
+
ORDER BY extracted_at DESC
|
|
2394
|
+
LIMIT 1
|
|
2395
|
+
`);
|
|
2396
|
+
const row = stmt.get(sessionId);
|
|
2397
|
+
if (!row) return null;
|
|
2398
|
+
return {
|
|
2399
|
+
id: row.id,
|
|
2400
|
+
contextJson: row.context_json,
|
|
2401
|
+
pageUrl: row.page_url || void 0,
|
|
2402
|
+
platform: row.platform || void 0,
|
|
2403
|
+
extractedAt: row.extracted_at
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Get context history for a session
|
|
2408
|
+
*/
|
|
2409
|
+
getContextHistory(sessionId, limit = 10) {
|
|
2410
|
+
const stmt = this.db.prepare(`
|
|
2411
|
+
SELECT id, message_id, page_url, page_title, platform, extracted_at
|
|
2412
|
+
FROM session_contexts
|
|
2413
|
+
WHERE session_id = ?
|
|
2414
|
+
ORDER BY extracted_at DESC
|
|
2415
|
+
LIMIT ?
|
|
2416
|
+
`);
|
|
2417
|
+
const rows = stmt.all(sessionId, limit);
|
|
2418
|
+
return rows.map((row) => ({
|
|
2419
|
+
id: row.id,
|
|
2420
|
+
messageId: row.message_id || void 0,
|
|
2421
|
+
pageUrl: row.page_url || void 0,
|
|
2422
|
+
pageTitle: row.page_title || void 0,
|
|
2423
|
+
platform: row.platform || void 0,
|
|
2424
|
+
extractedAt: row.extracted_at
|
|
2425
|
+
}));
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Get a specific context by ID
|
|
2429
|
+
*/
|
|
2430
|
+
getContext(contextId) {
|
|
2431
|
+
const stmt = this.db.prepare(`
|
|
2432
|
+
SELECT id, session_id, context_json, extracted_at
|
|
2433
|
+
FROM session_contexts
|
|
2434
|
+
WHERE id = ?
|
|
2435
|
+
`);
|
|
2436
|
+
const row = stmt.get(contextId);
|
|
2437
|
+
if (!row) return null;
|
|
2438
|
+
return {
|
|
2439
|
+
id: row.id,
|
|
2440
|
+
sessionId: row.session_id,
|
|
2441
|
+
contextJson: row.context_json,
|
|
2442
|
+
extractedAt: row.extracted_at
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Clean up old contexts for a session (keep only last N)
|
|
2447
|
+
*/
|
|
2448
|
+
cleanupOldContexts(sessionId, keepCount = 20) {
|
|
2449
|
+
const keepStmt = this.db.prepare(`
|
|
2450
|
+
SELECT id FROM session_contexts
|
|
2451
|
+
WHERE session_id = ?
|
|
2452
|
+
ORDER BY extracted_at DESC
|
|
2453
|
+
LIMIT ?
|
|
2454
|
+
`);
|
|
2455
|
+
const idsToKeep = keepStmt.all(sessionId, keepCount).map((r) => r.id);
|
|
2456
|
+
if (idsToKeep.length === 0) return 0;
|
|
2457
|
+
const deleteStmt = this.db.prepare(`
|
|
2458
|
+
DELETE FROM session_contexts
|
|
2459
|
+
WHERE session_id = ?
|
|
2460
|
+
AND id NOT IN (${idsToKeep.map(() => "?").join(",")})
|
|
2461
|
+
`);
|
|
2462
|
+
const result = deleteStmt.run(sessionId, ...idsToKeep);
|
|
2463
|
+
return result.changes;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Get context count for a session
|
|
2467
|
+
*/
|
|
2468
|
+
getContextCount(sessionId) {
|
|
2469
|
+
const stmt = this.db.prepare("SELECT COUNT(*) as count FROM session_contexts WHERE session_id = ?");
|
|
2470
|
+
const result = stmt.get(sessionId);
|
|
2471
|
+
return result.count;
|
|
2472
|
+
}
|
|
2473
|
+
mapDbSession(row) {
|
|
2474
|
+
return {
|
|
2475
|
+
id: row.id,
|
|
2476
|
+
name: row.name,
|
|
2477
|
+
type: row.type,
|
|
2478
|
+
status: row.status,
|
|
2479
|
+
model: row.model,
|
|
2480
|
+
systemPrompt: row.system_prompt || void 0,
|
|
2481
|
+
customAgent: row.custom_agent || void 0,
|
|
2482
|
+
messageCount: row.message_count,
|
|
2483
|
+
createdAt: row.created_at,
|
|
2484
|
+
updatedAt: row.updated_at
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
mapDbMessage(row) {
|
|
2488
|
+
let metadata = row.metadata ? JSON.parse(row.metadata) : void 0;
|
|
2489
|
+
if (metadata?.images) {
|
|
2490
|
+
metadata.images = metadata.images.map((img) => ({
|
|
2491
|
+
...img,
|
|
2492
|
+
thumbnailUrl: this.fixImageUrl(img.thumbnailUrl),
|
|
2493
|
+
fullImageUrl: this.fixImageUrl(img.fullImageUrl)
|
|
2494
|
+
}));
|
|
2495
|
+
}
|
|
2496
|
+
return {
|
|
2497
|
+
id: row.id,
|
|
2498
|
+
sessionId: row.session_id,
|
|
2499
|
+
role: row.role,
|
|
2500
|
+
content: row.content,
|
|
2501
|
+
timestamp: row.timestamp,
|
|
2502
|
+
metadata
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Fix legacy image URLs that have incorrect format
|
|
2507
|
+
* - Adds missing port (localhost -> localhost:3847)
|
|
2508
|
+
* - Removes duplicate "images/images/" path
|
|
2509
|
+
*/
|
|
2510
|
+
fixImageUrl(url) {
|
|
2511
|
+
if (!url) return url;
|
|
2512
|
+
let fixed = url;
|
|
2513
|
+
if (fixed.includes("http://localhost/api")) {
|
|
2514
|
+
fixed = fixed.replace("http://localhost/api", "http://localhost:3847/api");
|
|
2515
|
+
}
|
|
2516
|
+
if (fixed.includes("/api/images/images/")) {
|
|
2517
|
+
fixed = fixed.replace("/api/images/images/", "/api/images/");
|
|
2518
|
+
}
|
|
2519
|
+
return fixed;
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
// src/db/index.ts
|
|
2524
|
+
import Database from "better-sqlite3";
|
|
2525
|
+
import path4 from "path";
|
|
2526
|
+
import os from "os";
|
|
2527
|
+
import fs4 from "fs";
|
|
2528
|
+
var DB_DIR = path4.join(os.homedir(), ".devmentorai");
|
|
2529
|
+
var DB_PATH = path4.join(DB_DIR, "devmentorai.db");
|
|
2530
|
+
console.log(`Database path: ${DB_PATH}`);
|
|
2531
|
+
function initDatabase() {
|
|
2532
|
+
if (!fs4.existsSync(DB_DIR)) {
|
|
2533
|
+
fs4.mkdirSync(DB_DIR, { recursive: true });
|
|
2534
|
+
}
|
|
2535
|
+
const db = new Database(DB_PATH);
|
|
2536
|
+
db.pragma("journal_mode = WAL");
|
|
2537
|
+
db.exec(`
|
|
2538
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
2539
|
+
id TEXT PRIMARY KEY,
|
|
2540
|
+
name TEXT NOT NULL,
|
|
2541
|
+
type TEXT NOT NULL CHECK (type IN ('devops', 'writing', 'development', 'general')),
|
|
2542
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused', 'closed')),
|
|
2543
|
+
model TEXT NOT NULL DEFAULT 'gpt-4.1',
|
|
2544
|
+
system_prompt TEXT,
|
|
2545
|
+
custom_agent TEXT,
|
|
2546
|
+
message_count INTEGER NOT NULL DEFAULT 0,
|
|
2547
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2548
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2549
|
+
);
|
|
2550
|
+
|
|
2551
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
2552
|
+
id TEXT PRIMARY KEY,
|
|
2553
|
+
session_id TEXT NOT NULL,
|
|
2554
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
|
2555
|
+
content TEXT NOT NULL,
|
|
2556
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2557
|
+
metadata TEXT,
|
|
2558
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
2559
|
+
);
|
|
2560
|
+
|
|
2561
|
+
-- New table for session context persistence (Phase 5)
|
|
2562
|
+
CREATE TABLE IF NOT EXISTS session_contexts (
|
|
2563
|
+
id TEXT PRIMARY KEY,
|
|
2564
|
+
session_id TEXT NOT NULL,
|
|
2565
|
+
message_id TEXT,
|
|
2566
|
+
context_json TEXT NOT NULL,
|
|
2567
|
+
page_url TEXT,
|
|
2568
|
+
page_title TEXT,
|
|
2569
|
+
platform TEXT,
|
|
2570
|
+
extracted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2571
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
2572
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
2573
|
+
);
|
|
2574
|
+
|
|
2575
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
|
|
2576
|
+
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
|
|
2577
|
+
CREATE INDEX IF NOT EXISTS idx_session_contexts_session_id ON session_contexts(session_id);
|
|
2578
|
+
CREATE INDEX IF NOT EXISTS idx_session_contexts_extracted_at ON session_contexts(extracted_at);
|
|
2579
|
+
`);
|
|
2580
|
+
return db;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/server.ts
|
|
2584
|
+
var PORT = parseInt(process.env.DEVMENTORAI_PORT || "", 10) || DEFAULT_CONFIG.DEFAULT_PORT;
|
|
2585
|
+
var HOST = "0.0.0.0";
|
|
2586
|
+
var DEBUG_MODE = true;
|
|
2587
|
+
function truncate(str, maxLen = 500) {
|
|
2588
|
+
if (!str) return "";
|
|
2589
|
+
if (str.length <= maxLen) return str;
|
|
2590
|
+
return str.slice(0, maxLen) + `... [truncated ${str.length - maxLen} chars]`;
|
|
2591
|
+
}
|
|
2592
|
+
async function createServer() {
|
|
2593
|
+
const fastify = Fastify({
|
|
2594
|
+
logger: {
|
|
2595
|
+
level: DEBUG_MODE ? "debug" : "info",
|
|
2596
|
+
transport: {
|
|
2597
|
+
target: "pino-pretty",
|
|
2598
|
+
options: {
|
|
2599
|
+
colorize: true,
|
|
2600
|
+
translateTime: "HH:MM:ss Z",
|
|
2601
|
+
ignore: "pid,hostname"
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
});
|
|
2606
|
+
if (DEBUG_MODE) {
|
|
2607
|
+
fastify.log.info("\u{1F50D} Debug mode enabled - logging all requests and responses");
|
|
2608
|
+
fastify.addHook("preHandler", async (request) => {
|
|
2609
|
+
const body = request.body ? truncate(JSON.stringify(request.body)) : null;
|
|
2610
|
+
fastify.log.debug({
|
|
2611
|
+
type: "\u2192 REQUEST",
|
|
2612
|
+
method: request.method,
|
|
2613
|
+
url: request.url,
|
|
2614
|
+
headers: {
|
|
2615
|
+
"content-type": request.headers["content-type"],
|
|
2616
|
+
"user-agent": request.headers["user-agent"]
|
|
2617
|
+
},
|
|
2618
|
+
body
|
|
2619
|
+
});
|
|
2620
|
+
});
|
|
2621
|
+
fastify.addHook("onSend", async (request, reply, payload) => {
|
|
2622
|
+
const statusCode = reply.statusCode;
|
|
2623
|
+
let responseBody = null;
|
|
2624
|
+
if (reply.getHeader("content-type") === "text/event-stream") {
|
|
2625
|
+
responseBody = "[SSE Stream]";
|
|
2626
|
+
} else if (typeof payload === "string") {
|
|
2627
|
+
responseBody = truncate(payload);
|
|
2628
|
+
} else if (Buffer.isBuffer(payload)) {
|
|
2629
|
+
responseBody = truncate(payload.toString());
|
|
2630
|
+
}
|
|
2631
|
+
fastify.log.debug({
|
|
2632
|
+
type: "\u2190 RESPONSE",
|
|
2633
|
+
method: request.method,
|
|
2634
|
+
url: request.url,
|
|
2635
|
+
statusCode,
|
|
2636
|
+
body: responseBody
|
|
2637
|
+
});
|
|
2638
|
+
return payload;
|
|
2639
|
+
});
|
|
2640
|
+
fastify.addHook("onError", async (request, reply, error) => {
|
|
2641
|
+
fastify.log.error({
|
|
2642
|
+
type: "\u2717 ERROR",
|
|
2643
|
+
method: request.method,
|
|
2644
|
+
url: request.url,
|
|
2645
|
+
error: error.message,
|
|
2646
|
+
stack: error.stack
|
|
2647
|
+
});
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
const db = initDatabase();
|
|
2651
|
+
fastify.log.info("Database initialized");
|
|
2652
|
+
const sessionService = new SessionService(db);
|
|
2653
|
+
const copilotService = new CopilotService(sessionService);
|
|
2654
|
+
try {
|
|
2655
|
+
await copilotService.initialize();
|
|
2656
|
+
fastify.log.info("CopilotService initialized");
|
|
2657
|
+
} catch (err) {
|
|
2658
|
+
fastify.log.error({ err }, "Failed to initialize CopilotService");
|
|
2659
|
+
fastify.log.warn("Running in mock mode - Copilot features will be simulated");
|
|
2660
|
+
}
|
|
2661
|
+
fastify.decorate("sessionService", sessionService);
|
|
2662
|
+
fastify.decorate("copilotService", copilotService);
|
|
2663
|
+
await fastify.register(cors, {
|
|
2664
|
+
origin: true,
|
|
2665
|
+
// Allow all origins in development
|
|
2666
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
2667
|
+
allowedHeaders: ["Content-Type", "Authorization"]
|
|
2668
|
+
});
|
|
2669
|
+
await fastify.register(healthRoutes, { prefix: "/api" });
|
|
2670
|
+
await fastify.register(sessionRoutes, { prefix: "/api" });
|
|
2671
|
+
await fastify.register(chatRoutes, { prefix: "/api" });
|
|
2672
|
+
await fastify.register(modelsRoutes, { prefix: "/api" });
|
|
2673
|
+
await fastify.register(imagesRoutes, { prefix: "/api/images" });
|
|
2674
|
+
registerToolsRoutes(fastify, copilotService);
|
|
2675
|
+
return fastify;
|
|
2676
|
+
}
|
|
2677
|
+
async function main() {
|
|
2678
|
+
const fastify = await createServer();
|
|
2679
|
+
const shutdown = async () => {
|
|
2680
|
+
fastify.log.info("Shutting down...");
|
|
2681
|
+
try {
|
|
2682
|
+
await fastify.copilotService.shutdown();
|
|
2683
|
+
await fastify.close();
|
|
2684
|
+
process.exit(0);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
fastify.log.error({ err }, "Error during shutdown");
|
|
2687
|
+
process.exit(1);
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
process.on("SIGINT", shutdown);
|
|
2691
|
+
process.on("SIGTERM", shutdown);
|
|
2692
|
+
try {
|
|
2693
|
+
await fastify.listen({ port: PORT, host: HOST });
|
|
2694
|
+
fastify.log.info(`\u{1F680} DevMentorAI backend running at http://${HOST}:${PORT}`);
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
fastify.log.error(error);
|
|
2697
|
+
process.exit(1);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
main();
|
|
2701
|
+
export {
|
|
2702
|
+
createServer
|
|
2703
|
+
};
|
|
2704
|
+
//# sourceMappingURL=server.js.map
|