devmentorai-server 1.2.2 → 1.2.3

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-CAHB75ZH.js";
22
22
 
23
23
  // src/server.ts
24
24
  import Fastify from "fastify";
@@ -175,12 +175,13 @@ var createSessionSchema = z.object({
175
175
  });
176
176
  var updateSessionSchema = z.object({
177
177
  name: z.string().min(1).max(100).optional(),
178
- status: z.enum(["active", "paused", "closed"]).optional()
178
+ status: z.enum(["active", "paused", "closed"]).optional(),
179
+ model: z.string().min(1).optional()
179
180
  });
180
181
  async function sessionRoutes(fastify) {
181
182
  fastify.get("/sessions", async (request, reply) => {
182
- const page = parseInt(request.query.page || "1", 10);
183
- const pageSize = parseInt(request.query.pageSize || "50", 10);
183
+ const page = Number.parseInt(request.query.page || "1", 10);
184
+ const pageSize = Number.parseInt(request.query.pageSize || "50", 10);
184
185
  const sessions = fastify.sessionService.listSessions(page, pageSize);
185
186
  return reply.send({
186
187
  success: true,
@@ -235,6 +236,24 @@ async function sessionRoutes(fastify) {
235
236
  fastify.patch("/sessions/:id", async (request, reply) => {
236
237
  try {
237
238
  const body = updateSessionSchema.parse(request.body);
239
+ const currentSession = fastify.sessionService.getSession(request.params.id);
240
+ if (!currentSession) {
241
+ return reply.code(404).send({
242
+ success: false,
243
+ error: {
244
+ code: "NOT_FOUND",
245
+ message: "Session not found"
246
+ }
247
+ });
248
+ }
249
+ if (body.model && body.model !== currentSession.model) {
250
+ await fastify.copilotService.switchSessionModel(
251
+ currentSession.id,
252
+ currentSession.type,
253
+ body.model,
254
+ currentSession.systemPrompt
255
+ );
256
+ }
238
257
  const session = fastify.sessionService.updateSession(request.params.id, body);
239
258
  if (!session) {
240
259
  return reply.code(404).send({
@@ -337,8 +356,8 @@ async function sessionRoutes(fastify) {
337
356
  }
338
357
  });
339
358
  }
340
- const page = parseInt(request.query.page || "1", 10);
341
- const pageSize = parseInt(request.query.pageSize || "100", 10);
359
+ const page = Number.parseInt(request.query.page || "1", 10);
360
+ const pageSize = Number.parseInt(request.query.pageSize || "100", 10);
342
361
  const messages = fastify.sessionService.listMessages(request.params.id, page, pageSize);
343
362
  return reply.send({
344
363
  success: true,
@@ -1035,8 +1054,13 @@ async function chatRoutes(fastify) {
1035
1054
  reply.raw.end();
1036
1055
  };
1037
1056
  const streamComplete = new Promise((resolve2) => {
1057
+ const cleanupTimers = () => {
1058
+ clearTimeout(globalTimeout);
1059
+ clearInterval(idleCheckInterval);
1060
+ };
1038
1061
  const globalTimeout = setTimeout(() => {
1039
1062
  console.warn("[ChatRoute] Global stream timeout reached");
1063
+ cleanupTimers();
1040
1064
  endStream("timeout");
1041
1065
  fastify.copilotService.abortRequest(sessionId).catch(() => {
1042
1066
  });
@@ -1046,6 +1070,7 @@ async function chatRoutes(fastify) {
1046
1070
  const idleTime = Date.now() - lastActivityTime;
1047
1071
  if (idleTime > IDLE_TIMEOUT_MS && !streamEnded) {
1048
1072
  console.warn(`[ChatRoute] Stream idle for ${idleTime}ms, ending`);
1073
+ cleanupTimers();
1049
1074
  endStream("idle_timeout");
1050
1075
  fastify.copilotService.abortRequest(sessionId).catch(() => {
1051
1076
  });
@@ -1087,26 +1112,41 @@ async function chatRoutes(fastify) {
1087
1112
  });
1088
1113
  break;
1089
1114
  case "session.idle":
1090
- clearTimeout(globalTimeout);
1091
- clearInterval(idleCheckInterval);
1115
+ cleanupTimers();
1092
1116
  endStream("completed");
1093
1117
  resolve2();
1094
1118
  break;
1095
1119
  }
1096
1120
  };
1097
- fastify.copilotService.streamMessage(
1121
+ const streamStart = fastify.copilotService.streamMessage(
1098
1122
  sessionId,
1099
1123
  userPrompt,
1100
1124
  body.context,
1101
1125
  handleEvent,
1102
1126
  copilotAttachments.length > 0 ? copilotAttachments : void 0
1103
1127
  );
1128
+ streamStart.catch((streamError) => {
1129
+ console.error("[ChatRoute] Failed to start Copilot stream:", streamError);
1130
+ cleanupTimers();
1131
+ if (!streamEnded) {
1132
+ sendSSE({
1133
+ type: "error",
1134
+ data: {
1135
+ error: streamError instanceof Error ? streamError.message : "Failed to start stream"
1136
+ }
1137
+ });
1138
+ endStream("copilot_error");
1139
+ }
1140
+ resolve2();
1141
+ });
1104
1142
  });
1105
- request.raw.on("close", async () => {
1143
+ request.raw.on("close", () => {
1106
1144
  if (!streamEnded) {
1107
1145
  console.log("[ChatRoute] Client disconnected, aborting");
1108
1146
  streamEnded = true;
1109
- await fastify.copilotService.abortRequest(sessionId);
1147
+ fastify.copilotService.abortRequest(sessionId).catch((abortError) => {
1148
+ console.error("[ChatRoute] Failed to abort request after disconnect:", abortError);
1149
+ });
1110
1150
  reply.raw.end();
1111
1151
  }
1112
1152
  });
@@ -1213,143 +1253,56 @@ async function chatRoutes(fastify) {
1213
1253
  }
1214
1254
 
1215
1255
  // 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
1256
+ var TIER_ORDER = ["free", "cheap", "standard", "premium"];
1257
+ var FALLBACK_MODEL = {
1258
+ id: "gpt-4.1",
1259
+ name: "GPT-4.1",
1260
+ description: "Recommended baseline model for DevMentorAI sessions",
1261
+ provider: "openai",
1262
+ available: true,
1263
+ isDefault: true,
1264
+ pricingTier: "free",
1265
+ pricingMultiplier: 0
1266
+ };
1267
+ function sortModelsByTierAndName(models) {
1268
+ return [...models].sort((a, b) => {
1269
+ const aTier = a.pricingTier || "standard";
1270
+ const bTier = b.pricingTier || "standard";
1271
+ const tierDiff = TIER_ORDER.indexOf(aTier) - TIER_ORDER.indexOf(bTier);
1272
+ if (tierDiff !== 0) return tierDiff;
1273
+ return a.name.localeCompare(b.name);
1274
+ });
1275
+ }
1276
+ async function getModelsPayload(fastify) {
1277
+ const response = await fastify.copilotService.listModels();
1278
+ if (!response.models || response.models.length === 0) {
1279
+ return {
1280
+ models: [FALLBACK_MODEL],
1281
+ default: FALLBACK_MODEL.id
1282
+ };
1337
1283
  }
1338
- ];
1284
+ const sortedModels = sortModelsByTierAndName(response.models);
1285
+ const defaultModel = sortedModels.find((model) => model.id === response.default)?.id || sortedModels.find((model) => model.isDefault)?.id || FALLBACK_MODEL.id;
1286
+ return {
1287
+ models: sortedModels.map((model) => ({
1288
+ ...model,
1289
+ isDefault: model.id === defaultModel
1290
+ })),
1291
+ default: defaultModel
1292
+ };
1293
+ }
1339
1294
  async function modelsRoutes(fastify) {
1340
1295
  fastify.get("/models", async (_request, reply) => {
1341
- const defaultModel = AVAILABLE_MODELS.find((m) => m.isDefault);
1296
+ const payload = await getModelsPayload(fastify);
1342
1297
  return reply.send({
1343
1298
  success: true,
1344
- data: {
1345
- models: AVAILABLE_MODELS,
1346
- default: defaultModel?.id || "gpt-4.1"
1347
- }
1299
+ data: payload
1348
1300
  });
1349
1301
  });
1350
1302
  fastify.get("/models/:id", async (request, reply) => {
1351
1303
  const modelId = request.params.id;
1352
- const model = AVAILABLE_MODELS.find((m) => m.id === modelId);
1304
+ const payload = await getModelsPayload(fastify);
1305
+ const model = payload.models.find((m) => m.id === modelId);
1353
1306
  if (!model) {
1354
1307
  return reply.code(404).send({
1355
1308
  success: false,
@@ -1366,6 +1319,24 @@ async function modelsRoutes(fastify) {
1366
1319
  });
1367
1320
  }
1368
1321
 
1322
+ // src/routes/account.ts
1323
+ async function accountRoutes(fastify) {
1324
+ fastify.get("/account/auth", async (_request, reply) => {
1325
+ const auth = await fastify.copilotService.getAuthStatus();
1326
+ return reply.send({
1327
+ success: true,
1328
+ data: auth
1329
+ });
1330
+ });
1331
+ fastify.get("/account/quota", async (_request, reply) => {
1332
+ const quota = await fastify.copilotService.getQuota();
1333
+ return reply.send({
1334
+ success: true,
1335
+ data: quota
1336
+ });
1337
+ });
1338
+ }
1339
+
1369
1340
  // src/routes/images.ts
1370
1341
  import fs2 from "fs";
1371
1342
  import path2 from "path";
@@ -1918,6 +1889,7 @@ var MCP_SERVERS = {
1918
1889
  tools: ["*"]
1919
1890
  }
1920
1891
  };
1892
+ var RECOMMENDED_DEFAULT_MODEL = "gpt-4.1";
1921
1893
  var CopilotService = class {
1922
1894
  constructor(sessionService) {
1923
1895
  this.sessionService = sessionService;
@@ -1945,6 +1917,190 @@ var CopilotService = class {
1945
1917
  isMockMode() {
1946
1918
  return this.mockMode;
1947
1919
  }
1920
+ async getAuthStatus() {
1921
+ if (this.mockMode || !this.client) {
1922
+ return {
1923
+ isAuthenticated: false,
1924
+ login: null,
1925
+ reason: "Copilot SDK unavailable (mock mode)"
1926
+ };
1927
+ }
1928
+ try {
1929
+ const client = this.client;
1930
+ if (!client.getAuthStatus) {
1931
+ return {
1932
+ isAuthenticated: false,
1933
+ login: null,
1934
+ reason: "Copilot auth API not available in this SDK version"
1935
+ };
1936
+ }
1937
+ const auth = await client.getAuthStatus();
1938
+ return {
1939
+ isAuthenticated: Boolean(auth?.isAuthenticated),
1940
+ login: typeof auth?.login === "string" ? auth.login : null
1941
+ };
1942
+ } catch (error) {
1943
+ return {
1944
+ isAuthenticated: false,
1945
+ login: null,
1946
+ reason: error instanceof Error ? error.message : "Failed to get auth status"
1947
+ };
1948
+ }
1949
+ }
1950
+ async listModels() {
1951
+ if (this.mockMode || !this.client) {
1952
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
1953
+ }
1954
+ try {
1955
+ const client = this.client;
1956
+ if (!client.listModels) {
1957
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
1958
+ }
1959
+ const rawModels = await client.listModels();
1960
+ if (!Array.isArray(rawModels)) {
1961
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
1962
+ }
1963
+ const models = rawModels.map((raw) => this.normalizeModel(raw)).filter((model) => Boolean(model?.id));
1964
+ const recommendedAvailable = models.some((model) => model.id === RECOMMENDED_DEFAULT_MODEL);
1965
+ const sdkDefault = models.find((model) => model.isDefault)?.id;
1966
+ const fallbackFirst = models[0]?.id;
1967
+ const defaultModel = recommendedAvailable && RECOMMENDED_DEFAULT_MODEL || sdkDefault || fallbackFirst || RECOMMENDED_DEFAULT_MODEL;
1968
+ const modelsWithDefaultFlag = models.map((model) => ({
1969
+ ...model,
1970
+ isDefault: model.id === defaultModel
1971
+ }));
1972
+ return { models: modelsWithDefaultFlag, default: defaultModel };
1973
+ } catch (error) {
1974
+ console.error("[CopilotService] Failed to list models:", error);
1975
+ return { models: [], default: RECOMMENDED_DEFAULT_MODEL };
1976
+ }
1977
+ }
1978
+ async getQuota() {
1979
+ if (this.mockMode || !this.client) {
1980
+ return {
1981
+ used: null,
1982
+ included: null,
1983
+ remaining: null,
1984
+ percentageUsed: null,
1985
+ percentageRemaining: null,
1986
+ raw: {}
1987
+ };
1988
+ }
1989
+ try {
1990
+ const client = this.client;
1991
+ const quotaData = await client.rpc?.account?.getQuota?.();
1992
+ if (!quotaData || typeof quotaData !== "object") {
1993
+ return {
1994
+ used: null,
1995
+ included: null,
1996
+ remaining: null,
1997
+ percentageUsed: null,
1998
+ percentageRemaining: null,
1999
+ raw: {}
2000
+ };
2001
+ }
2002
+ return this.normalizeQuota(quotaData);
2003
+ } catch (error) {
2004
+ return {
2005
+ used: null,
2006
+ included: null,
2007
+ remaining: null,
2008
+ percentageUsed: null,
2009
+ percentageRemaining: null,
2010
+ raw: {
2011
+ error: error instanceof Error ? error.message : "Failed to get quota"
2012
+ }
2013
+ };
2014
+ }
2015
+ }
2016
+ async switchSessionModel(sessionId, type, model, systemPrompt) {
2017
+ const existing = this.sessions.get(sessionId);
2018
+ if (existing?.session && !this.mockMode) {
2019
+ try {
2020
+ await existing.session.abort().catch(() => void 0);
2021
+ await existing.session.destroy();
2022
+ } catch (error) {
2023
+ console.warn(`[CopilotService] Failed to destroy previous session before model switch: ${sessionId}`, error);
2024
+ }
2025
+ }
2026
+ this.sessions.delete(sessionId);
2027
+ if (this.client && !this.mockMode) {
2028
+ try {
2029
+ await this.client.deleteSession(sessionId);
2030
+ } catch {
2031
+ }
2032
+ }
2033
+ await this.createCopilotSession(sessionId, type, model, systemPrompt);
2034
+ }
2035
+ normalizeModel(raw) {
2036
+ const id = typeof raw.id === "string" ? raw.id : "";
2037
+ const name = typeof raw.name === "string" ? raw.name : id;
2038
+ const description = typeof raw.description === "string" && raw.description.trim().length > 0 ? raw.description : `AI model ${id}`;
2039
+ const provider = typeof raw.provider === "string" && raw.provider.trim().length > 0 ? raw.provider : this.inferProviderFromModelId(id);
2040
+ const multiplier = typeof raw.billing?.multiplier === "number" && Number.isFinite(raw.billing.multiplier) ? raw.billing.multiplier : void 0;
2041
+ const supportedReasoningEfforts = Array.isArray(raw.supportedReasoningEfforts) ? raw.supportedReasoningEfforts.filter((effort) => typeof effort === "string") : void 0;
2042
+ return {
2043
+ id,
2044
+ name,
2045
+ description,
2046
+ provider,
2047
+ available: true,
2048
+ isDefault: Boolean(raw.isDefault),
2049
+ pricingMultiplier: multiplier,
2050
+ pricingTier: this.mapPricingTier(multiplier),
2051
+ supportedReasoningEfforts
2052
+ };
2053
+ }
2054
+ inferProviderFromModelId(modelId) {
2055
+ const normalized = modelId.toLowerCase();
2056
+ if (normalized.startsWith("gpt")) return "openai";
2057
+ if (normalized.startsWith("claude")) return "anthropic";
2058
+ if (normalized.startsWith("gemini")) return "google";
2059
+ return "unknown";
2060
+ }
2061
+ mapPricingTier(multiplier) {
2062
+ if (multiplier === 0) return "free";
2063
+ if (typeof multiplier === "number" && multiplier > 0 && multiplier < 1) return "cheap";
2064
+ if (typeof multiplier === "number" && multiplier > 1) return "premium";
2065
+ return "standard";
2066
+ }
2067
+ normalizeQuota(raw) {
2068
+ const used = this.readNumber(raw, ["used", "consumed", "usage", "quotaUsed", "totalUsed"]);
2069
+ const included = this.readNumber(raw, ["included", "limit", "quota", "total", "quotaTotal", "allowed"]);
2070
+ const computedRemaining = typeof included === "number" && typeof used === "number" ? Math.max(included - used, 0) : null;
2071
+ const remaining = this.readNumber(raw, ["remaining", "left", "available"]) ?? computedRemaining;
2072
+ const computedPercentageUsed = typeof included === "number" && included > 0 && typeof used === "number" ? Math.min(100, used / included * 100) : null;
2073
+ const percentageUsed = this.readNumber(raw, ["percentageUsed", "usedPercent", "percentUsed"]) ?? computedPercentageUsed;
2074
+ const percentageRemaining = this.readNumber(raw, ["percentageRemaining", "remainingPercent", "percentRemaining"]) ?? (typeof percentageUsed === "number" ? Math.max(0, 100 - percentageUsed) : null);
2075
+ return {
2076
+ used,
2077
+ included,
2078
+ remaining,
2079
+ percentageUsed,
2080
+ percentageRemaining,
2081
+ periodStart: this.readString(raw, ["periodStart", "startAt", "windowStart"]),
2082
+ periodEnd: this.readString(raw, ["periodEnd", "endAt", "windowEnd"]),
2083
+ raw
2084
+ };
2085
+ }
2086
+ readNumber(source, keys) {
2087
+ for (const key of keys) {
2088
+ const value = source[key];
2089
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2090
+ if (typeof value === "string") {
2091
+ const parsed = Number(value);
2092
+ if (Number.isFinite(parsed)) return parsed;
2093
+ }
2094
+ }
2095
+ return null;
2096
+ }
2097
+ readString(source, keys) {
2098
+ for (const key of keys) {
2099
+ const value = source[key];
2100
+ if (typeof value === "string" && value.trim().length > 0) return value;
2101
+ }
2102
+ return null;
2103
+ }
1948
2104
  async createCopilotSession(sessionId, type, model, systemPrompt, enableMcp = false) {
1949
2105
  if (this.mockMode || !this.client) {
1950
2106
  this.sessions.set(sessionId, {
@@ -2002,6 +2158,7 @@ var CopilotService = class {
2002
2158
  return true;
2003
2159
  } catch (resumeError) {
2004
2160
  console.log(`[CopilotService] Could not resume session ${sessionId}, will try to create new`);
2161
+ console.log("[CopilotService] Resume error:", resumeError);
2005
2162
  }
2006
2163
  try {
2007
2164
  const dbSession = this.sessionService.getSession(sessionId);
@@ -2052,7 +2209,7 @@ ${fullPrompt}`;
2052
2209
  });
2053
2210
  const response = await copilotSession.session.sendAndWait({ prompt: fullPrompt });
2054
2211
  console.log(`[CopilotService] Received response for session ${sessionId}`);
2055
- console.log(`[CopilotService] Response: ${response?.data}...`);
2212
+ console.log("[CopilotService] Response payload:", response?.data);
2056
2213
  console.log(`[CopilotService] responseContent: ${responseContent}...`);
2057
2214
  return response?.data.content || responseContent;
2058
2215
  }, 3, 1e3);
@@ -2088,7 +2245,7 @@ User request: ${prompt}`;
2088
2245
  if (onEvent) {
2089
2246
  copilotSession.session.on(onEvent);
2090
2247
  }
2091
- copilotSession.session.send({
2248
+ await copilotSession.session.send({
2092
2249
  prompt: fullPrompt,
2093
2250
  attachments
2094
2251
  });
@@ -2105,7 +2262,8 @@ User request: ${prompt}`;
2105
2262
  } catch (error) {
2106
2263
  lastError = error instanceof Error ? error : new Error(String(error));
2107
2264
  const nonRetryableErrors = ["authentication", "invalid_session", "rate_limit"];
2108
- if (nonRetryableErrors.some((e) => lastError.message.toLowerCase().includes(e))) {
2265
+ const errorMessage = (lastError?.message || "").toLowerCase();
2266
+ if (nonRetryableErrors.some((e) => errorMessage.includes(e))) {
2109
2267
  throw lastError;
2110
2268
  }
2111
2269
  if (attempt < maxRetries) {
@@ -2167,6 +2325,7 @@ User request: ${prompt}`;
2167
2325
  await this.client.deleteSession(sessionId);
2168
2326
  console.log(`[CopilotService] Session ${sessionId} files deleted from disk`);
2169
2327
  } catch (error) {
2328
+ console.log("[CopilotService] deleteSession error:", error);
2170
2329
  console.log(`[CopilotService] Could not delete session files (may not exist): ${sessionId}`);
2171
2330
  }
2172
2331
  }
@@ -2317,6 +2476,10 @@ var SessionService = class {
2317
2476
  updates.push("status = ?");
2318
2477
  values.push(request.status);
2319
2478
  }
2479
+ if (request.model !== void 0) {
2480
+ updates.push("model = ?");
2481
+ values.push(request.model);
2482
+ }
2320
2483
  if (updates.length === 0) return session;
2321
2484
  updates.push("updated_at = ?");
2322
2485
  values.push(formatDate());
@@ -2400,7 +2563,7 @@ var SessionService = class {
2400
2563
  * Save context for a session (associated with a message)
2401
2564
  */
2402
2565
  saveContext(sessionId, contextJson, messageId, pageUrl, pageTitle, platform) {
2403
- const id = `ctx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2566
+ const id = `ctx_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
2404
2567
  const now = formatDate();
2405
2568
  const stmt = this.db.prepare(`
2406
2569
  INSERT INTO session_contexts (id, session_id, message_id, context_json, page_url, page_title, platform, extracted_at)
@@ -2608,7 +2771,7 @@ function initDatabase() {
2608
2771
  }
2609
2772
 
2610
2773
  // src/server.ts
2611
- var PORT = parseInt(process.env.DEVMENTORAI_PORT || "", 10) || DEFAULT_CONFIG.DEFAULT_PORT;
2774
+ var PORT = Number.parseInt(process.env.DEVMENTORAI_PORT || "", 10) || DEFAULT_CONFIG.DEFAULT_PORT;
2612
2775
  var HOST = "0.0.0.0";
2613
2776
  var DEBUG_MODE = true;
2614
2777
  function truncate(str, maxLen = 500) {
@@ -2697,6 +2860,7 @@ async function createServer() {
2697
2860
  await fastify.register(sessionRoutes, { prefix: "/api" });
2698
2861
  await fastify.register(chatRoutes, { prefix: "/api" });
2699
2862
  await fastify.register(modelsRoutes, { prefix: "/api" });
2863
+ await fastify.register(accountRoutes, { prefix: "/api" });
2700
2864
  await fastify.register(updatesRoutes, { prefix: "/api" });
2701
2865
  await fastify.register(imagesRoutes, { prefix: "/api/images" });
2702
2866
  registerToolsRoutes(fastify, copilotService);
@@ -2704,19 +2868,37 @@ async function createServer() {
2704
2868
  }
2705
2869
  async function main() {
2706
2870
  const fastify = await createServer();
2707
- const shutdown = async () => {
2708
- fastify.log.info("Shutting down...");
2871
+ let shuttingDown = false;
2872
+ const shutdown = async (reason, error) => {
2873
+ if (shuttingDown) return;
2874
+ shuttingDown = true;
2875
+ fastify.log.warn({ reason }, "Shutting down...");
2876
+ if (error) {
2877
+ fastify.log.error({ err: error }, "Fatal process error");
2878
+ }
2879
+ let exitCode = 0;
2709
2880
  try {
2710
2881
  await fastify.copilotService.shutdown();
2711
2882
  await fastify.close();
2712
- process.exit(0);
2713
2883
  } catch (err) {
2884
+ exitCode = 1;
2714
2885
  fastify.log.error({ err }, "Error during shutdown");
2715
- process.exit(1);
2886
+ } finally {
2887
+ process.exit(exitCode);
2716
2888
  }
2717
2889
  };
2718
- process.on("SIGINT", shutdown);
2719
- process.on("SIGTERM", shutdown);
2890
+ process.on("SIGINT", () => {
2891
+ void shutdown("SIGINT");
2892
+ });
2893
+ process.on("SIGTERM", () => {
2894
+ void shutdown("SIGTERM");
2895
+ });
2896
+ process.on("unhandledRejection", (reason) => {
2897
+ fastify.log.error({ err: reason }, "Unhandled promise rejection");
2898
+ });
2899
+ process.on("uncaughtException", (error) => {
2900
+ void shutdown("UNCAUGHT_EXCEPTION", error);
2901
+ });
2720
2902
  try {
2721
2903
  await fastify.listen({ port: PORT, host: HOST });
2722
2904
  fastify.log.info(`\u{1F680} DevMentorAI backend running at http://${HOST}:${PORT}`);
@@ -2725,7 +2907,12 @@ async function main() {
2725
2907
  process.exit(1);
2726
2908
  }
2727
2909
  }
2728
- main();
2910
+ try {
2911
+ await main();
2912
+ } catch (error) {
2913
+ console.error("[DevMentorAI] Fatal startup error:", error);
2914
+ process.exit(1);
2915
+ }
2729
2916
  export {
2730
2917
  createServer
2731
2918
  };