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/README.md +29 -0
- package/dist/{chunk-APCRFDFH.js → chunk-CAHB75ZH.js} +2 -10
- package/dist/chunk-CAHB75ZH.js.map +1 -0
- package/dist/cli.js +35 -27
- package/dist/cli.js.map +1 -1
- package/dist/server.js +338 -151
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- 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-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
|
-
|
|
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",
|
|
1143
|
+
request.raw.on("close", () => {
|
|
1106
1144
|
if (!streamEnded) {
|
|
1107
1145
|
console.log("[ChatRoute] Client disconnected, aborting");
|
|
1108
1146
|
streamEnded = true;
|
|
1109
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
2886
|
+
} finally {
|
|
2887
|
+
process.exit(exitCode);
|
|
2716
2888
|
}
|
|
2717
2889
|
};
|
|
2718
|
-
process.on("SIGINT",
|
|
2719
|
-
|
|
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
|
-
|
|
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
|
};
|