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/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