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/README.md +29 -0
- package/dist/{chunk-APCRFDFH.js → chunk-X4NL4LWR.js} +9 -13
- package/dist/chunk-X4NL4LWR.js.map +1 -0
- package/dist/cli.js +35 -27
- package/dist/cli.js.map +1 -1
- package/dist/server.js +525 -174
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-APCRFDFH.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
toImageRelativePath,
|
|
19
19
|
toRelativePath,
|
|
20
20
|
toUrlPath
|
|
21
|
-
} from "./chunk-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
122
|
+
const fullImageBuffer = await generateFullImage(buffer);
|
|
123
|
+
const extension = "jpg";
|
|
121
124
|
const fullImageAbsPath = getFullImagePath(sessionId, messageId, index, extension);
|
|
122
|
-
fs.writeFileSync(fullImageAbsPath,
|
|
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.
|
|
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
|
-
|
|
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",
|
|
1185
|
+
request.raw.on("close", () => {
|
|
1106
1186
|
if (!streamEnded) {
|
|
1107
1187
|
console.log("[ChatRoute] Client disconnected, aborting");
|
|
1108
1188
|
streamEnded = true;
|
|
1109
|
-
|
|
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
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
3050
|
+
} finally {
|
|
3051
|
+
process.exit(exitCode);
|
|
2716
3052
|
}
|
|
2717
3053
|
};
|
|
2718
|
-
process.on("SIGINT",
|
|
2719
|
-
|
|
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
|
-
|
|
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
|
};
|