devmentorai-server 1.2.2 → 1.3.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 CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  toImageRelativePath,
19
19
  toRelativePath,
20
20
  toUrlPath
21
- } from "./chunk-APCRFDFH.js";
21
+ } from "./chunk-X4NL4LWR.js";
22
22
 
23
23
  // src/server.ts
24
24
  import Fastify from "fastify";
@@ -66,6 +66,11 @@ var THUMBNAIL_CONFIG = {
66
66
  quality: 60,
67
67
  format: "jpeg"
68
68
  };
69
+ var FULL_IMAGE_CONFIG = {
70
+ maxDimension: 2e3,
71
+ quality: 80,
72
+ format: "jpeg"
73
+ };
69
74
  function parseDataUrl(dataUrl) {
70
75
  const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/);
71
76
  if (!match) return null;
@@ -86,14 +91,11 @@ async function generateThumbnail(buffer) {
86
91
  withoutEnlargement: true
87
92
  }).jpeg({ quality: THUMBNAIL_CONFIG.quality }).toBuffer();
88
93
  }
89
- function getExtensionForMimeType(mimeType) {
90
- const extensions = {
91
- "image/jpeg": "jpg",
92
- "image/png": "png",
93
- "image/webp": "webp",
94
- "image/gif": "gif"
95
- };
96
- return extensions[mimeType] || "jpg";
94
+ async function generateFullImage(buffer) {
95
+ return sharp(buffer).resize(FULL_IMAGE_CONFIG.maxDimension, FULL_IMAGE_CONFIG.maxDimension, {
96
+ fit: "inside",
97
+ withoutEnlargement: true
98
+ }).jpeg({ quality: FULL_IMAGE_CONFIG.quality }).toBuffer();
97
99
  }
98
100
  function getFullImagePath(sessionId, messageId, index, extension) {
99
101
  const messageDir = getMessageImagesDir(sessionId, messageId);
@@ -117,10 +119,11 @@ async function processMessageImages(sessionId, messageId, images, backendUrl) {
117
119
  const thumbnailBuffer = await generateThumbnail(buffer);
118
120
  const thumbnailAbsPath = getThumbnailPath(sessionId, messageId, index);
119
121
  fs.writeFileSync(thumbnailAbsPath, thumbnailBuffer);
120
- const extension = getExtensionForMimeType(mimeType);
122
+ const fullImageBuffer = await generateFullImage(buffer);
123
+ const extension = "jpg";
121
124
  const fullImageAbsPath = getFullImagePath(sessionId, messageId, index, extension);
122
- fs.writeFileSync(fullImageAbsPath, buffer);
123
- console.log(`[ThumbnailService] Saved full image to ${fullImageAbsPath}`);
125
+ fs.writeFileSync(fullImageAbsPath, fullImageBuffer);
126
+ console.log(`[ThumbnailService] Saved compressed full image to ${fullImageAbsPath}`);
124
127
  const thumbnailRelativePath = toRelativePath(thumbnailAbsPath);
125
128
  const thumbnailUrlPath = toUrlPath(toImageRelativePath(thumbnailAbsPath));
126
129
  const fullImageUrlPath = toUrlPath(toImageRelativePath(fullImageAbsPath));
@@ -175,12 +178,13 @@ var createSessionSchema = z.object({
175
178
  });
176
179
  var updateSessionSchema = z.object({
177
180
  name: z.string().min(1).max(100).optional(),
178
- status: z.enum(["active", "paused", "closed"]).optional()
181
+ status: z.enum(["active", "paused", "closed"]).optional(),
182
+ model: z.string().min(1).optional()
179
183
  });
180
184
  async function sessionRoutes(fastify) {
181
185
  fastify.get("/sessions", async (request, reply) => {
182
- const page = parseInt(request.query.page || "1", 10);
183
- const pageSize = parseInt(request.query.pageSize || "50", 10);
186
+ const page = Number.parseInt(request.query.page || "1", 10);
187
+ const pageSize = Number.parseInt(request.query.pageSize || "50", 10);
184
188
  const sessions = fastify.sessionService.listSessions(page, pageSize);
185
189
  return reply.send({
186
190
  success: true,
@@ -235,6 +239,24 @@ async function sessionRoutes(fastify) {
235
239
  fastify.patch("/sessions/:id", async (request, reply) => {
236
240
  try {
237
241
  const body = updateSessionSchema.parse(request.body);
242
+ const currentSession = fastify.sessionService.getSession(request.params.id);
243
+ if (!currentSession) {
244
+ return reply.code(404).send({
245
+ success: false,
246
+ error: {
247
+ code: "NOT_FOUND",
248
+ message: "Session not found"
249
+ }
250
+ });
251
+ }
252
+ if (body.model && body.model !== currentSession.model) {
253
+ await fastify.copilotService.switchSessionModel(
254
+ currentSession.id,
255
+ currentSession.type,
256
+ body.model,
257
+ currentSession.systemPrompt
258
+ );
259
+ }
238
260
  const session = fastify.sessionService.updateSession(request.params.id, body);
239
261
  if (!session) {
240
262
  return reply.code(404).send({
@@ -337,8 +359,8 @@ async function sessionRoutes(fastify) {
337
359
  }
338
360
  });
339
361
  }
340
- const page = parseInt(request.query.page || "1", 10);
341
- const pageSize = parseInt(request.query.pageSize || "100", 10);
362
+ const page = Number.parseInt(request.query.page || "1", 10);
363
+ const pageSize = Number.parseInt(request.query.pageSize || "100", 10);
342
364
  const messages = fastify.sessionService.listMessages(request.params.id, page, pageSize);
343
365
  return reply.send({
344
366
  success: true,
@@ -833,12 +855,23 @@ var imagePayloadSchema = z2.object({
833
855
  mimeType: z2.enum(["image/png", "image/jpeg", "image/webp"]),
834
856
  source: z2.enum(["screenshot", "paste", "drop"])
835
857
  });
858
+ var preUploadedImageSchema = z2.object({
859
+ id: z2.string(),
860
+ fullImagePath: z2.string(),
861
+ mimeType: z2.string(),
862
+ thumbnailUrl: z2.string().optional(),
863
+ fullImageUrl: z2.string().optional(),
864
+ dimensions: z2.object({ width: z2.number(), height: z2.number() }).optional(),
865
+ fileSize: z2.number().optional()
866
+ });
836
867
  var sendMessageSchema = z2.object({
837
868
  prompt: z2.string().min(1),
838
869
  context: simpleContextSchema.optional(),
839
870
  fullContext: fullContextSchema.optional(),
840
871
  useContextAwareMode: z2.boolean().optional(),
841
- images: z2.array(imagePayloadSchema).max(5).optional()
872
+ images: z2.array(imagePayloadSchema).max(5).optional(),
873
+ /** Pre-uploaded image references (already processed on disk) */
874
+ preUploadedImages: z2.array(preUploadedImageSchema).max(5).optional()
842
875
  });
843
876
  async function chatRoutes(fastify) {
844
877
  const buildPrompt = (body) => {
@@ -945,6 +978,7 @@ async function chatRoutes(fastify) {
945
978
  console.log(`[ChatRoute] Using ${promptType} prompt for streaming`);
946
979
  let processedImagesRaw = [];
947
980
  let processedImages = [];
981
+ let copilotAttachments = [];
948
982
  const host = request.headers.host || `${request.hostname}:3847`;
949
983
  const backendUrl = `http://${host}`;
950
984
  const userMessage = fastify.sessionService.addMessage(
@@ -958,9 +992,30 @@ async function chatRoutes(fastify) {
958
992
  contextAware: body.useContextAwareMode
959
993
  } : { contextAware: body.useContextAwareMode }
960
994
  );
961
- if (body.images && body.images.length > 0) {
995
+ if (body.preUploadedImages && body.preUploadedImages.length > 0) {
996
+ console.log(`[ChatRoute] Using ${body.preUploadedImages.length} pre-uploaded images`);
997
+ copilotAttachments = body.preUploadedImages.map((img, index) => ({
998
+ type: "file",
999
+ path: img.fullImagePath,
1000
+ displayName: `image_${index + 1}.${(img.mimeType || "image/jpeg").split("/")[1] || "jpg"}`
1001
+ }));
1002
+ processedImages = body.preUploadedImages.map((img) => ({
1003
+ id: img.id,
1004
+ source: "screenshot",
1005
+ mimeType: img.mimeType || "image/jpeg",
1006
+ dimensions: img.dimensions || { width: 0, height: 0 },
1007
+ fileSize: img.fileSize || 0,
1008
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1009
+ thumbnailUrl: img.thumbnailUrl,
1010
+ fullImageUrl: img.fullImageUrl
1011
+ }));
1012
+ fastify.sessionService.updateMessageMetadata(userMessage.id, {
1013
+ ...userMessage.metadata,
1014
+ images: processedImages
1015
+ });
1016
+ } else if (body.images && body.images.length > 0) {
962
1017
  try {
963
- console.log(`[ChatRoute] Processing ${body.images.length} images for message ${userMessage.id}`);
1018
+ console.log(`[ChatRoute] Processing ${body.images.length} inline images for message ${userMessage.id}`);
964
1019
  processedImagesRaw = await processMessageImages(
965
1020
  sessionId,
966
1021
  userMessage.id,
@@ -972,16 +1027,16 @@ async function chatRoutes(fastify) {
972
1027
  ...userMessage.metadata,
973
1028
  images: processedImages
974
1029
  });
1030
+ copilotAttachments = processedImagesRaw.map((img, index) => ({
1031
+ type: "file",
1032
+ path: img.fullImagePath,
1033
+ displayName: `image_${index + 1}.${img.mimeType.split("/")[1] || "jpg"}`
1034
+ }));
975
1035
  console.log(`[ChatRoute] Processed ${processedImages.length} images successfully`);
976
1036
  } catch (err) {
977
1037
  console.error("[ChatRoute] Failed to process images:", err);
978
1038
  }
979
1039
  }
980
- const copilotAttachments = processedImagesRaw.map((img, index) => ({
981
- type: "file",
982
- path: img.fullImagePath,
983
- displayName: `image_${index + 1}.${img.mimeType.split("/")[1] || "jpg"}`
984
- }));
985
1040
  if (copilotAttachments.length > 0) {
986
1041
  console.log(`[ChatRoute] Built ${copilotAttachments.length} attachments for Copilot:`, copilotAttachments);
987
1042
  }
@@ -1035,8 +1090,13 @@ async function chatRoutes(fastify) {
1035
1090
  reply.raw.end();
1036
1091
  };
1037
1092
  const streamComplete = new Promise((resolve2) => {
1093
+ const cleanupTimers = () => {
1094
+ clearTimeout(globalTimeout);
1095
+ clearInterval(idleCheckInterval);
1096
+ };
1038
1097
  const globalTimeout = setTimeout(() => {
1039
1098
  console.warn("[ChatRoute] Global stream timeout reached");
1099
+ cleanupTimers();
1040
1100
  endStream("timeout");
1041
1101
  fastify.copilotService.abortRequest(sessionId).catch(() => {
1042
1102
  });
@@ -1046,6 +1106,7 @@ async function chatRoutes(fastify) {
1046
1106
  const idleTime = Date.now() - lastActivityTime;
1047
1107
  if (idleTime > IDLE_TIMEOUT_MS && !streamEnded) {
1048
1108
  console.warn(`[ChatRoute] Stream idle for ${idleTime}ms, ending`);
1109
+ cleanupTimers();
1049
1110
  endStream("idle_timeout");
1050
1111
  fastify.copilotService.abortRequest(sessionId).catch(() => {
1051
1112
  });
@@ -1087,26 +1148,47 @@ async function chatRoutes(fastify) {
1087
1148
  });
1088
1149
  break;
1089
1150
  case "session.idle":
1090
- clearTimeout(globalTimeout);
1091
- clearInterval(idleCheckInterval);
1151
+ cleanupTimers();
1092
1152
  endStream("completed");
1093
1153
  resolve2();
1094
1154
  break;
1095
1155
  }
1096
1156
  };
1097
- fastify.copilotService.streamMessage(
1157
+ const streamStart = fastify.copilotService.streamMessage(
1098
1158
  sessionId,
1099
1159
  userPrompt,
1100
1160
  body.context,
1101
1161
  handleEvent,
1102
1162
  copilotAttachments.length > 0 ? copilotAttachments : void 0
1103
1163
  );
1164
+ streamStart.catch((streamError) => {
1165
+ console.error("[ChatRoute] Failed to start Copilot stream:", streamError);
1166
+ cleanupTimers();
1167
+ if (!streamEnded) {
1168
+ if (!reply.raw.headersSent) {
1169
+ reply.raw.setHeader("Content-Type", "text/event-stream");
1170
+ reply.raw.setHeader("Cache-Control", "no-cache");
1171
+ reply.raw.setHeader("Connection", "keep-alive");
1172
+ reply.raw.setHeader("Access-Control-Allow-Origin", "*");
1173
+ }
1174
+ sendSSE({
1175
+ type: "error",
1176
+ data: {
1177
+ error: streamError instanceof Error ? streamError.message : "Failed to start stream"
1178
+ }
1179
+ });
1180
+ endStream("copilot_error");
1181
+ }
1182
+ resolve2();
1183
+ });
1104
1184
  });
1105
- request.raw.on("close", async () => {
1185
+ request.raw.on("close", () => {
1106
1186
  if (!streamEnded) {
1107
1187
  console.log("[ChatRoute] Client disconnected, aborting");
1108
1188
  streamEnded = true;
1109
- await fastify.copilotService.abortRequest(sessionId);
1189
+ fastify.copilotService.abortRequest(sessionId).catch((abortError) => {
1190
+ console.error("[ChatRoute] Failed to abort request after disconnect:", abortError);
1191
+ });
1110
1192
  reply.raw.end();
1111
1193
  }
1112
1194
  });
@@ -1122,6 +1204,12 @@ async function chatRoutes(fastify) {
1122
1204
  }
1123
1205
  });
1124
1206
  }
1207
+ if (!reply.raw.headersSent) {
1208
+ reply.raw.setHeader("Content-Type", "text/event-stream");
1209
+ reply.raw.setHeader("Cache-Control", "no-cache");
1210
+ reply.raw.setHeader("Connection", "keep-alive");
1211
+ reply.raw.setHeader("Access-Control-Allow-Origin", "*");
1212
+ }
1125
1213
  const errorEvent = {
1126
1214
  type: "error",
1127
1215
  data: { error: error instanceof Error ? error.message : "Unknown error" }
@@ -1213,143 +1301,56 @@ async function chatRoutes(fastify) {
1213
1301
  }
1214
1302
 
1215
1303
  // src/routes/models.ts
1216
- var AVAILABLE_MODELS = [
1217
- // Free tier (0x)
1218
- {
1219
- id: "gpt-4.1",
1220
- name: "GPT-4.1",
1221
- description: "Fast and capable model for most tasks",
1222
- provider: "openai",
1223
- isDefault: true,
1224
- pricingTier: "free",
1225
- pricingMultiplier: 0
1226
- },
1227
- {
1228
- id: "gpt-4o",
1229
- name: "GPT-4o",
1230
- description: "Multimodal model with vision capabilities",
1231
- provider: "openai",
1232
- isDefault: false,
1233
- pricingTier: "free",
1234
- pricingMultiplier: 0
1235
- },
1236
- {
1237
- id: "gpt-5-mini",
1238
- name: "GPT-5 Mini",
1239
- description: "Fast, lightweight model for simple tasks",
1240
- provider: "openai",
1241
- isDefault: false,
1242
- pricingTier: "free",
1243
- pricingMultiplier: 0
1244
- },
1245
- // Cheap tier (0.33x)
1246
- {
1247
- id: "claude-haiku-4.5",
1248
- name: "Claude Haiku 4.5",
1249
- description: "Fast, efficient model for quick tasks",
1250
- provider: "anthropic",
1251
- isDefault: false,
1252
- pricingTier: "cheap",
1253
- pricingMultiplier: 0.33
1254
- },
1255
- {
1256
- id: "gpt-5.1-codex-mini",
1257
- name: "GPT-5.1 Codex Mini",
1258
- description: "Compact coding model",
1259
- provider: "openai",
1260
- isDefault: false,
1261
- pricingTier: "cheap",
1262
- pricingMultiplier: 0.33
1263
- },
1264
- // Standard tier (1x)
1265
- {
1266
- id: "gpt-5",
1267
- name: "GPT-5",
1268
- description: "Most capable model for complex reasoning",
1269
- provider: "openai",
1270
- isDefault: false,
1271
- pricingTier: "standard",
1272
- pricingMultiplier: 1
1273
- },
1274
- {
1275
- id: "gpt-5.1",
1276
- name: "GPT-5.1",
1277
- description: "Enhanced reasoning and analysis",
1278
- provider: "openai",
1279
- isDefault: false,
1280
- pricingTier: "standard",
1281
- pricingMultiplier: 1
1282
- },
1283
- {
1284
- id: "gpt-5.1-codex",
1285
- name: "GPT-5.1 Codex",
1286
- description: "Specialized for code generation",
1287
- provider: "openai",
1288
- isDefault: false,
1289
- pricingTier: "standard",
1290
- pricingMultiplier: 1
1291
- },
1292
- {
1293
- id: "gpt-5.2",
1294
- name: "GPT-5.2",
1295
- description: "Latest generation model",
1296
- provider: "openai",
1297
- isDefault: false,
1298
- pricingTier: "standard",
1299
- pricingMultiplier: 1
1300
- },
1301
- {
1302
- id: "claude-sonnet-4",
1303
- name: "Claude Sonnet 4",
1304
- description: "Balanced model for general use",
1305
- provider: "anthropic",
1306
- isDefault: false,
1307
- pricingTier: "standard",
1308
- pricingMultiplier: 1
1309
- },
1310
- {
1311
- id: "claude-sonnet-4.5",
1312
- name: "Claude Sonnet 4.5",
1313
- description: "Enhanced balanced model",
1314
- provider: "anthropic",
1315
- isDefault: false,
1316
- pricingTier: "standard",
1317
- pricingMultiplier: 1
1318
- },
1319
- {
1320
- id: "gemini-3-pro-preview",
1321
- name: "Gemini 3 Pro (Preview)",
1322
- description: "Google's latest model",
1323
- provider: "google",
1324
- isDefault: false,
1325
- pricingTier: "standard",
1326
- pricingMultiplier: 1
1327
- },
1328
- // Premium tier (3x)
1329
- {
1330
- id: "claude-opus-4.5",
1331
- name: "Claude Opus 4.5",
1332
- description: "Premium model for complex analysis",
1333
- provider: "anthropic",
1334
- isDefault: false,
1335
- pricingTier: "premium",
1336
- pricingMultiplier: 3
1304
+ var TIER_ORDER = ["free", "cheap", "standard", "premium"];
1305
+ var FALLBACK_MODEL = {
1306
+ id: "gpt-4.1",
1307
+ name: "GPT-4.1",
1308
+ description: "Recommended baseline model for DevMentorAI sessions",
1309
+ provider: "openai",
1310
+ available: true,
1311
+ isDefault: true,
1312
+ pricingTier: "free",
1313
+ pricingMultiplier: 0
1314
+ };
1315
+ function sortModelsByTierAndName(models) {
1316
+ return [...models].sort((a, b) => {
1317
+ const aTier = a.pricingTier || "standard";
1318
+ const bTier = b.pricingTier || "standard";
1319
+ const tierDiff = TIER_ORDER.indexOf(aTier) - TIER_ORDER.indexOf(bTier);
1320
+ if (tierDiff !== 0) return tierDiff;
1321
+ return a.name.localeCompare(b.name);
1322
+ });
1323
+ }
1324
+ async function getModelsPayload(fastify) {
1325
+ const response = await fastify.copilotService.listModels();
1326
+ if (!response.models || response.models.length === 0) {
1327
+ return {
1328
+ models: [FALLBACK_MODEL],
1329
+ default: FALLBACK_MODEL.id
1330
+ };
1337
1331
  }
1338
- ];
1332
+ const sortedModels = sortModelsByTierAndName(response.models);
1333
+ const defaultModel = sortedModels.find((model) => model.id === response.default)?.id || sortedModels.find((model) => model.isDefault)?.id || FALLBACK_MODEL.id;
1334
+ return {
1335
+ models: sortedModels.map((model) => ({
1336
+ ...model,
1337
+ isDefault: model.id === defaultModel
1338
+ })),
1339
+ default: defaultModel
1340
+ };
1341
+ }
1339
1342
  async function modelsRoutes(fastify) {
1340
1343
  fastify.get("/models", async (_request, reply) => {
1341
- const defaultModel = AVAILABLE_MODELS.find((m) => m.isDefault);
1344
+ const payload = await getModelsPayload(fastify);
1342
1345
  return reply.send({
1343
1346
  success: true,
1344
- data: {
1345
- models: AVAILABLE_MODELS,
1346
- default: defaultModel?.id || "gpt-4.1"
1347
- }
1347
+ data: payload
1348
1348
  });
1349
1349
  });
1350
1350
  fastify.get("/models/:id", async (request, reply) => {
1351
1351
  const modelId = request.params.id;
1352
- const model = AVAILABLE_MODELS.find((m) => m.id === modelId);
1352
+ const payload = await getModelsPayload(fastify);
1353
+ const model = payload.models.find((m) => m.id === modelId);
1353
1354
  if (!model) {
1354
1355
  return reply.code(404).send({
1355
1356
  success: false,
@@ -1366,7 +1367,26 @@ async function modelsRoutes(fastify) {
1366
1367
  });
1367
1368
  }
1368
1369
 
1370
+ // src/routes/account.ts
1371
+ async function accountRoutes(fastify) {
1372
+ fastify.get("/account/auth", async (_request, reply) => {
1373
+ const auth = await fastify.copilotService.getAuthStatus();
1374
+ return reply.send({
1375
+ success: true,
1376
+ data: auth
1377
+ });
1378
+ });
1379
+ fastify.get("/account/quota", async (_request, reply) => {
1380
+ const quota = await fastify.copilotService.getQuota();
1381
+ return reply.send({
1382
+ success: true,
1383
+ data: quota
1384
+ });
1385
+ });
1386
+ }
1387
+
1369
1388
  // src/routes/images.ts
1389
+ import { z as z3 } from "zod";
1370
1390
  import fs2 from "fs";
1371
1391
  import path2 from "path";
1372
1392
  var MIME_TYPES = {
@@ -1376,7 +1396,68 @@ var MIME_TYPES = {
1376
1396
  "webp": "image/webp",
1377
1397
  "gif": "image/gif"
1378
1398
  };
1399
+ var uploadImageSchema = z3.object({
1400
+ id: z3.string(),
1401
+ dataUrl: z3.string(),
1402
+ mimeType: z3.enum(["image/png", "image/jpeg", "image/webp"]),
1403
+ source: z3.enum(["screenshot", "paste", "drop"])
1404
+ });
1405
+ var uploadBodySchema = z3.object({
1406
+ images: z3.array(uploadImageSchema).min(1).max(5)
1407
+ });
1379
1408
  async function imagesRoutes(fastify) {
1409
+ fastify.post(
1410
+ "/upload/:sessionId/:messageId",
1411
+ async (request, reply) => {
1412
+ try {
1413
+ const { sessionId, messageId } = request.params;
1414
+ const body = uploadBodySchema.parse(request.body);
1415
+ const host = request.headers.host || `${request.hostname}:3847`;
1416
+ const backendUrl = `http://${host}`;
1417
+ console.log(`[ImagesRoute] Pre-uploading ${body.images.length} images for ${sessionId}/${messageId}`);
1418
+ const processedImagesRaw = await processMessageImages(
1419
+ sessionId,
1420
+ messageId,
1421
+ body.images,
1422
+ backendUrl
1423
+ );
1424
+ const processedImages = toImageAttachments(processedImagesRaw);
1425
+ const responseImages = processedImagesRaw.map((img, i) => ({
1426
+ id: img.id,
1427
+ thumbnailUrl: img.thumbnailUrl,
1428
+ fullImageUrl: img.fullImageUrl,
1429
+ fullImagePath: img.fullImagePath,
1430
+ mimeType: img.mimeType,
1431
+ dimensions: img.dimensions,
1432
+ fileSize: img.fileSize
1433
+ }));
1434
+ console.log(`[ImagesRoute] Pre-upload complete: ${responseImages.length} images processed`);
1435
+ return reply.send({
1436
+ success: true,
1437
+ data: { images: responseImages }
1438
+ });
1439
+ } catch (error) {
1440
+ if (error instanceof z3.ZodError) {
1441
+ return reply.code(400).send({
1442
+ success: false,
1443
+ error: {
1444
+ code: "VALIDATION_ERROR",
1445
+ message: "Invalid upload body",
1446
+ details: error.errors
1447
+ }
1448
+ });
1449
+ }
1450
+ console.error("[ImagesRoute] Upload failed:", error);
1451
+ return reply.code(500).send({
1452
+ success: false,
1453
+ error: {
1454
+ code: "UPLOAD_ERROR",
1455
+ message: error instanceof Error ? error.message : "Failed to process images"
1456
+ }
1457
+ });
1458
+ }
1459
+ }
1460
+ );
1380
1461
  fastify.get(
1381
1462
  "/:sessionId/:messageId/:filename",
1382
1463
  async (request, reply) => {
@@ -1502,7 +1583,7 @@ function registerToolsRoutes(app, copilotService) {
1502
1583
  }
1503
1584
 
1504
1585
  // src/services/copilot.service.ts
1505
- import { CopilotClient } from "@github/copilot-sdk";
1586
+ import { CopilotClient, approveAll } from "@github/copilot-sdk";
1506
1587
 
1507
1588
  // src/tools/devops-tools.ts
1508
1589
  import * as fs3 from "fs/promises";
@@ -1900,11 +1981,61 @@ var analyzeErrorTool = {
1900
1981
  return result;
1901
1982
  }
1902
1983
  };
1984
+ var fetchUrlTool = {
1985
+ name: "fetch_url",
1986
+ description: "Fetch the text content of a public URL. Use this to read documentation, github issues, or other public web pages when the user shares a URL.",
1987
+ parameters: {
1988
+ type: "object",
1989
+ properties: {
1990
+ url: {
1991
+ type: "string",
1992
+ description: "The HTTP or HTTPS URL to fetch"
1993
+ }
1994
+ },
1995
+ required: ["url"]
1996
+ },
1997
+ handler: async (params) => {
1998
+ const targetUrl = params.url;
1999
+ if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
2000
+ return `Error: Only HTTP/HTTPS URLs are supported, got "${targetUrl}"`;
2001
+ }
2002
+ try {
2003
+ const controller = new AbortController();
2004
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
2005
+ const response = await fetch(targetUrl, {
2006
+ signal: controller.signal,
2007
+ headers: {
2008
+ "User-Agent": "DevMentorAI/1.0",
2009
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8"
2010
+ }
2011
+ });
2012
+ clearTimeout(timeoutId);
2013
+ if (!response.ok) {
2014
+ return `Error: Server responded with status ${response.status} ${response.statusText}`;
2015
+ }
2016
+ const contentType = response.headers.get("content-type") || "";
2017
+ const text = await response.text();
2018
+ const MAX_LENGTH = 15e3;
2019
+ if (text.length > MAX_LENGTH) {
2020
+ return text.substring(0, MAX_LENGTH) + `
2021
+
2022
+ [Content truncated at ${MAX_LENGTH} characters]`;
2023
+ }
2024
+ return text;
2025
+ } catch (error) {
2026
+ if (error.name === "AbortError") {
2027
+ return `Error: Request to ${targetUrl} timed out after 10 seconds.`;
2028
+ }
2029
+ return `Error fetching URL: ${error instanceof Error ? error.message : String(error)}`;
2030
+ }
2031
+ }
2032
+ };
1903
2033
  var devopsTools = [
1904
2034
  readFileTool,
1905
2035
  listDirectoryTool,
1906
2036
  analyzeConfigTool,
1907
- analyzeErrorTool
2037
+ analyzeErrorTool,
2038
+ fetchUrlTool
1908
2039
  ];
1909
2040
  function getToolByName(name) {
1910
2041
  return devopsTools.find((tool) => tool.name === name);
@@ -1918,6 +2049,7 @@ var MCP_SERVERS = {
1918
2049
  tools: ["*"]
1919
2050
  }
1920
2051
  };
2052
+ var RECOMMENDED_DEFAULT_MODEL = "gpt-4.1";
1921
2053
  var CopilotService = class {
1922
2054
  constructor(sessionService) {
1923
2055
  this.sessionService = sessionService;
@@ -1945,6 +2077,190 @@ var CopilotService = class {
1945
2077
  isMockMode() {
1946
2078
  return this.mockMode;
1947
2079
  }
2080
+ async getAuthStatus() {
2081
+ if (this.mockMode || !this.client) {
2082
+ return {
2083
+ isAuthenticated: false,
2084
+ login: null,
2085
+ reason: "Copilot SDK unavailable (mock mode)"
2086
+ };
2087
+ }
2088
+ try {
2089
+ const client = this.client;
2090
+ if (!client.getAuthStatus) {
2091
+ return {
2092
+ isAuthenticated: false,
2093
+ login: null,
2094
+ reason: "Copilot auth API not available in this SDK version"
2095
+ };
2096
+ }
2097
+ const auth = await client.getAuthStatus();
2098
+ return {
2099
+ isAuthenticated: Boolean(auth?.isAuthenticated),
2100
+ login: typeof auth?.login === "string" ? auth.login : null
2101
+ };
2102
+ } catch (error) {
2103
+ return {
2104
+ isAuthenticated: false,
2105
+ login: null,
2106
+ reason: error instanceof Error ? error.message : "Failed to get auth status"
2107
+ };
2108
+ }
2109
+ }
2110
+ async listModels() {
2111
+ if (this.mockMode || !this.client) {
2112
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
2113
+ }
2114
+ try {
2115
+ const client = this.client;
2116
+ if (!client.listModels) {
2117
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
2118
+ }
2119
+ const rawModels = await client.listModels();
2120
+ if (!Array.isArray(rawModels)) {
2121
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
2122
+ }
2123
+ const models = rawModels.map((raw) => this.normalizeModel(raw)).filter((model) => Boolean(model?.id));
2124
+ const recommendedAvailable = models.some((model) => model.id === RECOMMENDED_DEFAULT_MODEL);
2125
+ const sdkDefault = models.find((model) => model.isDefault)?.id;
2126
+ const fallbackFirst = models[0]?.id;
2127
+ const defaultModel = recommendedAvailable && RECOMMENDED_DEFAULT_MODEL || sdkDefault || fallbackFirst || RECOMMENDED_DEFAULT_MODEL;
2128
+ const modelsWithDefaultFlag = models.map((model) => ({
2129
+ ...model,
2130
+ isDefault: model.id === defaultModel
2131
+ }));
2132
+ return { models: modelsWithDefaultFlag, default: defaultModel };
2133
+ } catch (error) {
2134
+ console.error("[CopilotService] Failed to list models:", error);
2135
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
2136
+ }
2137
+ }
2138
+ async getQuota() {
2139
+ if (this.mockMode || !this.client) {
2140
+ return {
2141
+ used: null,
2142
+ included: null,
2143
+ remaining: null,
2144
+ percentageUsed: null,
2145
+ percentageRemaining: null,
2146
+ raw: {}
2147
+ };
2148
+ }
2149
+ try {
2150
+ const client = this.client;
2151
+ const quotaData = await client.rpc?.account?.getQuota?.();
2152
+ if (!quotaData || typeof quotaData !== "object") {
2153
+ return {
2154
+ used: null,
2155
+ included: null,
2156
+ remaining: null,
2157
+ percentageUsed: null,
2158
+ percentageRemaining: null,
2159
+ raw: {}
2160
+ };
2161
+ }
2162
+ return this.normalizeQuota(quotaData);
2163
+ } catch (error) {
2164
+ return {
2165
+ used: null,
2166
+ included: null,
2167
+ remaining: null,
2168
+ percentageUsed: null,
2169
+ percentageRemaining: null,
2170
+ raw: {
2171
+ error: error instanceof Error ? error.message : "Failed to get quota"
2172
+ }
2173
+ };
2174
+ }
2175
+ }
2176
+ async switchSessionModel(sessionId, type, model, systemPrompt) {
2177
+ const existing = this.sessions.get(sessionId);
2178
+ if (existing?.session && !this.mockMode) {
2179
+ try {
2180
+ await existing.session.abort().catch(() => void 0);
2181
+ await existing.session.destroy();
2182
+ } catch (error) {
2183
+ console.warn(`[CopilotService] Failed to destroy previous session before model switch: ${sessionId}`, error);
2184
+ }
2185
+ }
2186
+ this.sessions.delete(sessionId);
2187
+ if (this.client && !this.mockMode) {
2188
+ try {
2189
+ await this.client.deleteSession(sessionId);
2190
+ } catch {
2191
+ }
2192
+ }
2193
+ await this.createCopilotSession(sessionId, type, model, systemPrompt);
2194
+ }
2195
+ normalizeModel(raw) {
2196
+ const id = typeof raw.id === "string" ? raw.id : "";
2197
+ const name = typeof raw.name === "string" ? raw.name : id;
2198
+ const description = typeof raw.description === "string" && raw.description.trim().length > 0 ? raw.description : `AI model ${id}`;
2199
+ const provider = typeof raw.provider === "string" && raw.provider.trim().length > 0 ? raw.provider : this.inferProviderFromModelId(id);
2200
+ const multiplier = typeof raw.billing?.multiplier === "number" && Number.isFinite(raw.billing.multiplier) ? raw.billing.multiplier : void 0;
2201
+ const supportedReasoningEfforts = Array.isArray(raw.supportedReasoningEfforts) ? raw.supportedReasoningEfforts.filter((effort) => typeof effort === "string") : void 0;
2202
+ return {
2203
+ id,
2204
+ name,
2205
+ description,
2206
+ provider,
2207
+ available: true,
2208
+ isDefault: Boolean(raw.isDefault),
2209
+ pricingMultiplier: multiplier,
2210
+ pricingTier: this.mapPricingTier(multiplier),
2211
+ supportedReasoningEfforts
2212
+ };
2213
+ }
2214
+ inferProviderFromModelId(modelId) {
2215
+ const normalized = modelId.toLowerCase();
2216
+ if (normalized.startsWith("gpt")) return "openai";
2217
+ if (normalized.startsWith("claude")) return "anthropic";
2218
+ if (normalized.startsWith("gemini")) return "google";
2219
+ return "unknown";
2220
+ }
2221
+ mapPricingTier(multiplier) {
2222
+ if (multiplier === 0) return "free";
2223
+ if (typeof multiplier === "number" && multiplier > 0 && multiplier < 1) return "cheap";
2224
+ if (typeof multiplier === "number" && multiplier > 1) return "premium";
2225
+ return "standard";
2226
+ }
2227
+ normalizeQuota(raw) {
2228
+ const used = this.readNumber(raw, ["used", "consumed", "usage", "quotaUsed", "totalUsed"]);
2229
+ const included = this.readNumber(raw, ["included", "limit", "quota", "total", "quotaTotal", "allowed"]);
2230
+ const computedRemaining = typeof included === "number" && typeof used === "number" ? Math.max(included - used, 0) : null;
2231
+ const remaining = this.readNumber(raw, ["remaining", "left", "available"]) ?? computedRemaining;
2232
+ const computedPercentageUsed = typeof included === "number" && included > 0 && typeof used === "number" ? Math.min(100, used / included * 100) : null;
2233
+ const percentageUsed = this.readNumber(raw, ["percentageUsed", "usedPercent", "percentUsed"]) ?? computedPercentageUsed;
2234
+ const percentageRemaining = this.readNumber(raw, ["percentageRemaining", "remainingPercent", "percentRemaining"]) ?? (typeof percentageUsed === "number" ? Math.max(0, 100 - percentageUsed) : null);
2235
+ return {
2236
+ used,
2237
+ included,
2238
+ remaining,
2239
+ percentageUsed,
2240
+ percentageRemaining,
2241
+ periodStart: this.readString(raw, ["periodStart", "startAt", "windowStart"]),
2242
+ periodEnd: this.readString(raw, ["periodEnd", "endAt", "windowEnd"]),
2243
+ raw
2244
+ };
2245
+ }
2246
+ readNumber(source, keys) {
2247
+ for (const key of keys) {
2248
+ const value = source[key];
2249
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2250
+ if (typeof value === "string") {
2251
+ const parsed = Number(value);
2252
+ if (Number.isFinite(parsed)) return parsed;
2253
+ }
2254
+ }
2255
+ return null;
2256
+ }
2257
+ readString(source, keys) {
2258
+ for (const key of keys) {
2259
+ const value = source[key];
2260
+ if (typeof value === "string" && value.trim().length > 0) return value;
2261
+ }
2262
+ return null;
2263
+ }
1948
2264
  async createCopilotSession(sessionId, type, model, systemPrompt, enableMcp = false) {
1949
2265
  if (this.mockMode || !this.client) {
1950
2266
  this.sessions.set(sessionId, {
@@ -1965,7 +2281,8 @@ var CopilotService = class {
1965
2281
  customAgents: agentConfig ? [agentConfig] : void 0,
1966
2282
  systemMessage: systemPrompt ? { content: systemPrompt } : void 0,
1967
2283
  tools,
1968
- mcpServers
2284
+ mcpServers,
2285
+ onPermissionRequest: approveAll
1969
2286
  });
1970
2287
  this.sessions.set(sessionId, { sessionId, session, type });
1971
2288
  }
@@ -1995,13 +2312,14 @@ var CopilotService = class {
1995
2312
  return true;
1996
2313
  }
1997
2314
  try {
1998
- const session = await this.client.resumeSession(sessionId);
2315
+ const session = await this.client.resumeSession(sessionId, { onPermissionRequest: approveAll });
1999
2316
  const dbSession = this.sessionService.getSession(sessionId);
2000
2317
  this.sessions.set(sessionId, { sessionId, session, type: dbSession?.type || "general" });
2001
2318
  console.log(`[CopilotService] Session ${sessionId} resumed from disk`);
2002
2319
  return true;
2003
2320
  } catch (resumeError) {
2004
2321
  console.log(`[CopilotService] Could not resume session ${sessionId}, will try to create new`);
2322
+ console.log("[CopilotService] Resume error:", resumeError);
2005
2323
  }
2006
2324
  try {
2007
2325
  const dbSession = this.sessionService.getSession(sessionId);
@@ -2052,7 +2370,7 @@ ${fullPrompt}`;
2052
2370
  });
2053
2371
  const response = await copilotSession.session.sendAndWait({ prompt: fullPrompt });
2054
2372
  console.log(`[CopilotService] Received response for session ${sessionId}`);
2055
- console.log(`[CopilotService] Response: ${response?.data}...`);
2373
+ console.log("[CopilotService] Response payload:", response?.data);
2056
2374
  console.log(`[CopilotService] responseContent: ${responseContent}...`);
2057
2375
  return response?.data.content || responseContent;
2058
2376
  }, 3, 1e3);
@@ -2088,7 +2406,7 @@ User request: ${prompt}`;
2088
2406
  if (onEvent) {
2089
2407
  copilotSession.session.on(onEvent);
2090
2408
  }
2091
- copilotSession.session.send({
2409
+ await copilotSession.session.send({
2092
2410
  prompt: fullPrompt,
2093
2411
  attachments
2094
2412
  });
@@ -2105,7 +2423,8 @@ User request: ${prompt}`;
2105
2423
  } catch (error) {
2106
2424
  lastError = error instanceof Error ? error : new Error(String(error));
2107
2425
  const nonRetryableErrors = ["authentication", "invalid_session", "rate_limit"];
2108
- if (nonRetryableErrors.some((e) => lastError.message.toLowerCase().includes(e))) {
2426
+ const errorMessage = (lastError?.message || "").toLowerCase();
2427
+ if (nonRetryableErrors.some((e) => errorMessage.includes(e))) {
2109
2428
  throw lastError;
2110
2429
  }
2111
2430
  if (attempt < maxRetries) {
@@ -2167,6 +2486,7 @@ User request: ${prompt}`;
2167
2486
  await this.client.deleteSession(sessionId);
2168
2487
  console.log(`[CopilotService] Session ${sessionId} files deleted from disk`);
2169
2488
  } catch (error) {
2489
+ console.log("[CopilotService] deleteSession error:", error);
2170
2490
  console.log(`[CopilotService] Could not delete session files (may not exist): ${sessionId}`);
2171
2491
  }
2172
2492
  }
@@ -2317,6 +2637,10 @@ var SessionService = class {
2317
2637
  updates.push("status = ?");
2318
2638
  values.push(request.status);
2319
2639
  }
2640
+ if (request.model !== void 0) {
2641
+ updates.push("model = ?");
2642
+ values.push(request.model);
2643
+ }
2320
2644
  if (updates.length === 0) return session;
2321
2645
  updates.push("updated_at = ?");
2322
2646
  values.push(formatDate());
@@ -2400,7 +2724,7 @@ var SessionService = class {
2400
2724
  * Save context for a session (associated with a message)
2401
2725
  */
2402
2726
  saveContext(sessionId, contextJson, messageId, pageUrl, pageTitle, platform) {
2403
- const id = `ctx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2727
+ const id = `ctx_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
2404
2728
  const now = formatDate();
2405
2729
  const stmt = this.db.prepare(`
2406
2730
  INSERT INTO session_contexts (id, session_id, message_id, context_json, page_url, page_title, platform, extracted_at)
@@ -2608,7 +2932,7 @@ function initDatabase() {
2608
2932
  }
2609
2933
 
2610
2934
  // src/server.ts
2611
- var PORT = parseInt(process.env.DEVMENTORAI_PORT || "", 10) || DEFAULT_CONFIG.DEFAULT_PORT;
2935
+ var PORT = Number.parseInt(process.env.DEVMENTORAI_PORT || "", 10) || DEFAULT_CONFIG.DEFAULT_PORT;
2612
2936
  var HOST = "0.0.0.0";
2613
2937
  var DEBUG_MODE = true;
2614
2938
  function truncate(str, maxLen = 500) {
@@ -2618,6 +2942,9 @@ function truncate(str, maxLen = 500) {
2618
2942
  }
2619
2943
  async function createServer() {
2620
2944
  const fastify = Fastify({
2945
+ // Allow large payloads for image uploads (data URLs can be 10-30MB for full-page screenshots)
2946
+ bodyLimit: 50 * 1024 * 1024,
2947
+ // 50MB
2621
2948
  logger: {
2622
2949
  level: DEBUG_MODE ? "debug" : "info",
2623
2950
  transport: {
@@ -2697,6 +3024,7 @@ async function createServer() {
2697
3024
  await fastify.register(sessionRoutes, { prefix: "/api" });
2698
3025
  await fastify.register(chatRoutes, { prefix: "/api" });
2699
3026
  await fastify.register(modelsRoutes, { prefix: "/api" });
3027
+ await fastify.register(accountRoutes, { prefix: "/api" });
2700
3028
  await fastify.register(updatesRoutes, { prefix: "/api" });
2701
3029
  await fastify.register(imagesRoutes, { prefix: "/api/images" });
2702
3030
  registerToolsRoutes(fastify, copilotService);
@@ -2704,19 +3032,37 @@ async function createServer() {
2704
3032
  }
2705
3033
  async function main() {
2706
3034
  const fastify = await createServer();
2707
- const shutdown = async () => {
2708
- fastify.log.info("Shutting down...");
3035
+ let shuttingDown = false;
3036
+ const shutdown = async (reason, error) => {
3037
+ if (shuttingDown) return;
3038
+ shuttingDown = true;
3039
+ fastify.log.warn({ reason }, "Shutting down...");
3040
+ if (error) {
3041
+ fastify.log.error({ err: error }, "Fatal process error");
3042
+ }
3043
+ let exitCode = 0;
2709
3044
  try {
2710
3045
  await fastify.copilotService.shutdown();
2711
3046
  await fastify.close();
2712
- process.exit(0);
2713
3047
  } catch (err) {
3048
+ exitCode = 1;
2714
3049
  fastify.log.error({ err }, "Error during shutdown");
2715
- process.exit(1);
3050
+ } finally {
3051
+ process.exit(exitCode);
2716
3052
  }
2717
3053
  };
2718
- process.on("SIGINT", shutdown);
2719
- process.on("SIGTERM", shutdown);
3054
+ process.on("SIGINT", () => {
3055
+ void shutdown("SIGINT");
3056
+ });
3057
+ process.on("SIGTERM", () => {
3058
+ void shutdown("SIGTERM");
3059
+ });
3060
+ process.on("unhandledRejection", (reason) => {
3061
+ fastify.log.error({ err: reason }, "Unhandled promise rejection");
3062
+ });
3063
+ process.on("uncaughtException", (error) => {
3064
+ void shutdown("UNCAUGHT_EXCEPTION", error);
3065
+ });
2720
3066
  try {
2721
3067
  await fastify.listen({ port: PORT, host: HOST });
2722
3068
  fastify.log.info(`\u{1F680} DevMentorAI backend running at http://${HOST}:${PORT}`);
@@ -2725,7 +3071,12 @@ async function main() {
2725
3071
  process.exit(1);
2726
3072
  }
2727
3073
  }
2728
- main();
3074
+ try {
3075
+ await main();
3076
+ } catch (error) {
3077
+ console.error("[DevMentorAI] Fatal startup error:", error);
3078
+ process.exit(1);
3079
+ }
2729
3080
  export {
2730
3081
  createServer
2731
3082
  };