@ganakailabs/cloudeval-cli 0.19.2 → 0.19.4

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.
@@ -0,0 +1,1014 @@
1
+ // src/frontendLinks.ts
2
+ var trimTrailingSlash = (value) => value.replace(/\/+$/, "");
3
+ var resolveFrontendBaseUrl = ({
4
+ frontendUrl,
5
+ apiBaseUrl,
6
+ env = process.env
7
+ } = {}) => {
8
+ const explicit = frontendUrl?.trim() || env.CLOUDEVAL_FRONTEND_URL || env.CLOUDEVAL_WEB_URL;
9
+ if (explicit) {
10
+ return trimTrailingSlash(explicit);
11
+ }
12
+ try {
13
+ const api = apiBaseUrl ? new URL(apiBaseUrl) : void 0;
14
+ if (api && ["localhost", "127.0.0.1", "::1"].includes(api.hostname)) {
15
+ return "http://localhost:3000";
16
+ }
17
+ } catch {
18
+ }
19
+ return "https://cloudeval.ai";
20
+ };
21
+ var appUrl = (baseUrl, path3) => new URL(
22
+ `/app${path3.startsWith("/") ? path3 : `/${path3}`}`,
23
+ `${trimTrailingSlash(baseUrl)}/`
24
+ );
25
+ var setParam = (url, key, value) => {
26
+ if (value !== void 0 && value !== "" && value !== false) {
27
+ url.searchParams.set(key, String(value));
28
+ }
29
+ };
30
+ var setArrayParam = (url, key, value) => {
31
+ if (!value) {
32
+ return;
33
+ }
34
+ url.searchParams.set(key, Array.isArray(value) ? value.join(",") : value);
35
+ };
36
+ var buildFrontendUrl = (options) => {
37
+ let url;
38
+ switch (options.target) {
39
+ case "overview":
40
+ url = appUrl(options.baseUrl, "/overview");
41
+ break;
42
+ case "chat":
43
+ url = appUrl(options.baseUrl, "/chat");
44
+ setParam(url, "threadId", options.threadId);
45
+ break;
46
+ case "projects":
47
+ url = appUrl(options.baseUrl, "/projects");
48
+ if (options.quick) {
49
+ setParam(url, "dialog", "quick");
50
+ setParam(url, "template_url", options.templateUrl);
51
+ setParam(url, "name", options.name);
52
+ setParam(url, "description", options.description);
53
+ setParam(url, "provider", options.provider);
54
+ setParam(url, "auto_submit", options.autoSubmit ? "true" : void 0);
55
+ }
56
+ break;
57
+ case "project":
58
+ if (!options.projectId) {
59
+ throw new Error("projectId is required for project frontend links.");
60
+ }
61
+ url = appUrl(
62
+ options.baseUrl,
63
+ `/projects/${encodeURIComponent(options.projectId)}`
64
+ );
65
+ setParam(url, "view", options.view);
66
+ setParam(url, "layout", options.layout);
67
+ setArrayParam(url, "node", options.node);
68
+ setParam(url, "resource", options.resource);
69
+ setParam(url, "tab", options.tab);
70
+ setParam(url, "file", options.file);
71
+ setArrayParam(url, "files", options.files);
72
+ setParam(url, "cursor", options.cursor);
73
+ setParam(url, "selection", options.selection);
74
+ setParam(
75
+ url,
76
+ "workspaceFocus",
77
+ options.workspaceFocus ? "true" : void 0
78
+ );
79
+ setParam(url, "mode", options.presentation ? "presentation" : void 0);
80
+ break;
81
+ case "connections":
82
+ url = appUrl(options.baseUrl, "/connections");
83
+ setParam(url, "dialog", options.dialog);
84
+ break;
85
+ case "connection":
86
+ if (!options.connectionId) {
87
+ throw new Error(
88
+ "connectionId is required for connection frontend links."
89
+ );
90
+ }
91
+ url = appUrl(
92
+ options.baseUrl,
93
+ `/connections/${encodeURIComponent(options.connectionId)}`
94
+ );
95
+ break;
96
+ case "reports":
97
+ url = options.projectId ? appUrl(
98
+ options.baseUrl,
99
+ `/reports/${encodeURIComponent(options.projectId)}`
100
+ ) : appUrl(options.baseUrl, "/reports");
101
+ setParam(url, "tab", options.tab);
102
+ setParam(url, "reportType", options.reportType);
103
+ setParam(url, "timeRange", options.timeRange);
104
+ setParam(url, "persona", options.persona);
105
+ setParam(url, "cadence", options.cadence);
106
+ setParam(url, "issuesQuery", options.issuesQuery);
107
+ setParam(
108
+ url,
109
+ "issuesFullscreen",
110
+ options.issuesFullscreen ? "1" : void 0
111
+ );
112
+ setParam(url, "issuesView", options.issuesView);
113
+ setParam(url, "downloadPdf", options.downloadPdf ? "1" : void 0);
114
+ setParam(url, "pdfVerbosity", options.pdfVerbosity);
115
+ setParam(url, "downloadReport", options.downloadReport);
116
+ setParam(url, "reportVerbosity", options.reportVerbosity);
117
+ break;
118
+ case "billing":
119
+ url = appUrl(options.baseUrl, "/subscription");
120
+ setParam(url, "tab", options.tab);
121
+ break;
122
+ default:
123
+ throw new Error(
124
+ `Unsupported frontend target '${String(options.target)}'.`
125
+ );
126
+ }
127
+ return url.toString();
128
+ };
129
+ var openExternalUrl = async (url) => {
130
+ const { spawn } = await import("child_process");
131
+ const platform = process.platform;
132
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
133
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
134
+ const child = spawn(command, args, {
135
+ detached: true,
136
+ stdio: "ignore"
137
+ });
138
+ child.unref();
139
+ };
140
+
141
+ // src/cliConfig.ts
142
+ import fs from "fs/promises";
143
+ import os from "os";
144
+ import path from "path";
145
+ var CONFIG_PROFILE_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
146
+ var SETTINGS_FILE = "settings.json";
147
+ var normalizeConfigProfile = (profile) => {
148
+ const normalized = (profile || process.env.CLOUDEVAL_PROFILE || "default").trim() || "default";
149
+ if (!CONFIG_PROFILE_PATTERN.test(normalized)) {
150
+ throw new Error(
151
+ `Invalid profile '${normalized}'. Use letters, numbers, dashes, or underscores.`
152
+ );
153
+ }
154
+ return normalized;
155
+ };
156
+ var getActiveConfigProfile = (command) => {
157
+ const opts = typeof command?.optsWithGlobals === "function" ? command.optsWithGlobals() : command?.opts();
158
+ return normalizeConfigProfile(opts?.profile);
159
+ };
160
+ var getCloudevalConfigDir = () => path.join(os.homedir(), ".config", "cloudeval");
161
+ var getCliConfigPath = (profile) => {
162
+ const normalized = normalizeConfigProfile(profile);
163
+ if (normalized === "default") {
164
+ return path.join(getCloudevalConfigDir(), SETTINGS_FILE);
165
+ }
166
+ return path.join(getCloudevalConfigDir(), "profiles", normalized, SETTINGS_FILE);
167
+ };
168
+ var ensureConfigParent = async (filePath) => {
169
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
170
+ };
171
+ var loadCliConfig = async (profile) => {
172
+ const filePath = getCliConfigPath(profile);
173
+ try {
174
+ const raw = await fs.readFile(filePath, "utf8");
175
+ const parsed = JSON.parse(raw);
176
+ return parsed && typeof parsed === "object" ? parsed : {};
177
+ } catch (error) {
178
+ if (error?.code === "ENOENT") {
179
+ return {};
180
+ }
181
+ throw error;
182
+ }
183
+ };
184
+ var saveCliConfig = async (config, profile) => {
185
+ const filePath = getCliConfigPath(profile);
186
+ await ensureConfigParent(filePath);
187
+ const tempPath = `${filePath}.${process.pid}.tmp`;
188
+ await fs.writeFile(tempPath, `${JSON.stringify(config, null, 2)}
189
+ `, {
190
+ encoding: "utf8",
191
+ mode: 384
192
+ });
193
+ await fs.rename(tempPath, filePath);
194
+ return filePath;
195
+ };
196
+ var listCliConfigProfiles = async () => {
197
+ const profiles = /* @__PURE__ */ new Set(["default"]);
198
+ const profilesDir = path.join(getCloudevalConfigDir(), "profiles");
199
+ try {
200
+ const entries = await fs.readdir(profilesDir, { withFileTypes: true });
201
+ for (const entry of entries) {
202
+ if (entry.isDirectory() && CONFIG_PROFILE_PATTERN.test(entry.name)) {
203
+ profiles.add(entry.name);
204
+ }
205
+ }
206
+ } catch (error) {
207
+ if (error?.code !== "ENOENT") {
208
+ throw error;
209
+ }
210
+ }
211
+ return [...profiles].sort();
212
+ };
213
+ var keyAliases = {
214
+ project: "defaultProjectId",
215
+ projectId: "defaultProjectId",
216
+ defaultProject: "defaultProjectId",
217
+ defaultProjectId: "defaultProjectId",
218
+ model: "model",
219
+ mode: "mode",
220
+ chatMode: "mode",
221
+ defaultMode: "mode",
222
+ baseUrl: "baseUrl",
223
+ frontendUrl: "frontendUrl",
224
+ outputFormat: "outputFormat",
225
+ format: "outputFormat"
226
+ };
227
+ var normalizeCliMode = (value) => {
228
+ const normalized = value?.trim().toLowerCase();
229
+ if (!normalized) {
230
+ return void 0;
231
+ }
232
+ if (normalized === "ask" || normalized === "agent") {
233
+ return normalized;
234
+ }
235
+ throw new Error("mode must be one of: ask, agent");
236
+ };
237
+ var normalizeConfigKey = (key) => {
238
+ const normalized = key.trim();
239
+ const mapped = keyAliases[normalized];
240
+ if (!mapped) {
241
+ throw new Error(
242
+ `Unsupported config key '${key}'. Supported keys: baseUrl, frontendUrl, defaultProjectId, model, mode, outputFormat.`
243
+ );
244
+ }
245
+ return mapped;
246
+ };
247
+ var readCliConfigValue = (config, key) => {
248
+ const normalized = normalizeConfigKey(key);
249
+ const value = config[normalized];
250
+ return typeof value === "string" ? value : void 0;
251
+ };
252
+ var writeCliConfigValue = (config, key, value) => {
253
+ const normalized = normalizeConfigKey(key);
254
+ const normalizedValue = normalized === "mode" ? normalizeCliMode(value) : value;
255
+ return {
256
+ ...config,
257
+ [normalized]: normalizedValue
258
+ };
259
+ };
260
+ var unsetCliConfigValue = (config, key) => {
261
+ const normalized = normalizeConfigKey(key);
262
+ const next = { ...config };
263
+ delete next[normalized];
264
+ return next;
265
+ };
266
+
267
+ // src/ui/userDisplayName.ts
268
+ var toTitleCase = (value) => {
269
+ const normalized = value.trim();
270
+ if (!normalized) {
271
+ return "";
272
+ }
273
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
274
+ };
275
+ var firstToken = (value) => {
276
+ const token = value?.trim().split(/\s+/)[0]?.replace(/^[^\p{L}]+|[^\p{L}]+$/gu, "");
277
+ return token ? toTitleCase(token) : void 0;
278
+ };
279
+ var firstNameFromEmail = (email) => {
280
+ const localPart = email?.split("@")[0];
281
+ if (!localPart) {
282
+ return void 0;
283
+ }
284
+ const token = localPart.split(/[._-]+/).find((part) => /[a-z]/i.test(part))?.replace(/\d+/g, "");
285
+ return token ? toTitleCase(token) : void 0;
286
+ };
287
+ var getFirstNameForDisplay = (user, fallback = "You") => {
288
+ const fromName = firstToken(user?.full_name ?? user?.fullName ?? user?.name);
289
+ if (fromName) {
290
+ return fromName;
291
+ }
292
+ return firstNameFromEmail(user?.email) ?? fallback;
293
+ };
294
+
295
+ // src/sessionsStore.ts
296
+ import fsSync from "fs";
297
+ import fs2 from "fs/promises";
298
+ import path2 from "path";
299
+ import { fileURLToPath } from "url";
300
+ var SQLJS_WASM_ENV_VAR = "CLOUDEVAL_SQLJS_WASM";
301
+ var legacySessionsDir = (profile) => {
302
+ const normalized = normalizeConfigProfile(profile);
303
+ if (normalized === "default") {
304
+ return path2.join(getCloudevalConfigDir(), "sessions");
305
+ }
306
+ return path2.join(getCloudevalConfigDir(), "profiles", normalized, "sessions");
307
+ };
308
+ var sessionsDatabasePath = (profile) => {
309
+ const normalized = normalizeConfigProfile(profile);
310
+ if (normalized === "default") {
311
+ return path2.join(getCloudevalConfigDir(), "sessions.sqlite");
312
+ }
313
+ return path2.join(getCloudevalConfigDir(), "profiles", normalized, "sessions.sqlite");
314
+ };
315
+ var legacySessionPath = (threadId, profile) => path2.join(legacySessionsDir(profile), `${sanitizeThreadId(threadId)}.json`);
316
+ var sanitizeThreadId = (threadId) => threadId.replace(/[^a-zA-Z0-9_.-]/g, "_");
317
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
318
+ var titleFromQuestion = (question) => {
319
+ const singleLine = question.replace(/[^\p{L}\p{N}\s'-]/gu, " ").replace(/\s+/g, " ").trim().replace(/^(can you|could you|please|help me|show me|tell me|what is|what are|how do i)\s+/i, "").replace(/^(review|investigate|triage|summarize|explain|analyze)\s+the\s+/i, "$1 ");
320
+ if (!singleLine) {
321
+ return "Untitled CloudEval session";
322
+ }
323
+ const words = singleLine.split(/\s+/).slice(0, 7);
324
+ const joined = words.join(" ");
325
+ const title = joined.charAt(0).toUpperCase() + joined.slice(1);
326
+ return title.length > 80 ? `${title.slice(0, 77)}...` : title;
327
+ };
328
+ var sanitizeTitle = (title) => {
329
+ const cleaned = title.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/[\u200b-\u200f\u202a-\u202e]/g, "").replace(/\s+/g, " ").trim();
330
+ if (!cleaned) {
331
+ throw new Error("Session title cannot be empty.");
332
+ }
333
+ return cleaned.length > 100 ? cleaned.slice(0, 100) : cleaned;
334
+ };
335
+ var hasFile = (candidate) => {
336
+ try {
337
+ return fsSync.statSync(candidate).isFile();
338
+ } catch {
339
+ return false;
340
+ }
341
+ };
342
+ var findSqlJsWasmInNodeModules = (dir) => {
343
+ const nodeModulesDir = path2.join(dir, "node_modules");
344
+ const direct = path2.join(nodeModulesDir, "sql.js", "dist", "sql-wasm.wasm");
345
+ if (hasFile(direct)) {
346
+ return direct;
347
+ }
348
+ const pnpmRoot = path2.join(nodeModulesDir, ".pnpm");
349
+ if (!fsSync.existsSync(pnpmRoot)) {
350
+ return void 0;
351
+ }
352
+ for (const entry of fsSync.readdirSync(pnpmRoot)) {
353
+ if (!entry.startsWith("sql.js@")) {
354
+ continue;
355
+ }
356
+ const candidate = path2.join(
357
+ pnpmRoot,
358
+ entry,
359
+ "node_modules",
360
+ "sql.js",
361
+ "dist",
362
+ "sql-wasm.wasm"
363
+ );
364
+ if (hasFile(candidate)) {
365
+ return candidate;
366
+ }
367
+ }
368
+ return void 0;
369
+ };
370
+ var searchSqlJsWasmFrom = (start) => {
371
+ let current = path2.resolve(start);
372
+ while (true) {
373
+ const localCandidates = [
374
+ path2.join(current, "sql-wasm.wasm"),
375
+ path2.join(current, "dist", "sql-wasm.wasm")
376
+ ];
377
+ for (const candidate of localCandidates) {
378
+ if (hasFile(candidate)) {
379
+ return candidate;
380
+ }
381
+ }
382
+ const nodeModulesMatch = findSqlJsWasmInNodeModules(current);
383
+ if (nodeModulesMatch) {
384
+ return nodeModulesMatch;
385
+ }
386
+ const parent = path2.dirname(current);
387
+ if (parent === current) {
388
+ return void 0;
389
+ }
390
+ current = parent;
391
+ }
392
+ };
393
+ var resolveSqlJsWasmPath = () => {
394
+ if (process.env[SQLJS_WASM_ENV_VAR]) {
395
+ return process.env[SQLJS_WASM_ENV_VAR];
396
+ }
397
+ const moduleDir = path2.dirname(fileURLToPath(import.meta.url));
398
+ const seen = /* @__PURE__ */ new Set();
399
+ const roots = [process.cwd(), moduleDir, path2.dirname(process.execPath)];
400
+ for (const root of roots) {
401
+ const resolvedRoot = path2.resolve(root);
402
+ if (seen.has(resolvedRoot)) {
403
+ continue;
404
+ }
405
+ seen.add(resolvedRoot);
406
+ const match = searchSqlJsWasmFrom(resolvedRoot);
407
+ if (match) {
408
+ return match;
409
+ }
410
+ }
411
+ return void 0;
412
+ };
413
+ var isBunRuntime = () => typeof process.versions.bun === "string";
414
+ var dynamicImport = new Function("specifier", "return import(specifier)");
415
+ var openBunDatabase = async (dbPath) => {
416
+ if (!isBunRuntime()) {
417
+ return null;
418
+ }
419
+ try {
420
+ const { Database } = await dynamicImport("bun:sqlite");
421
+ await fs2.mkdir(path2.dirname(dbPath), { recursive: true, mode: 448 });
422
+ const db = new Database(dbPath, { create: true });
423
+ db.exec("PRAGMA foreign_keys = ON");
424
+ return {
425
+ rows(sql, params = []) {
426
+ const statement = db.query(sql);
427
+ return statement.all(...params);
428
+ },
429
+ run(sql, params = []) {
430
+ const statement = db.query(sql);
431
+ statement.run(...params);
432
+ },
433
+ async persist() {
434
+ },
435
+ close() {
436
+ db.close();
437
+ }
438
+ };
439
+ } catch {
440
+ return null;
441
+ }
442
+ };
443
+ var sqlJsFactoryPromise = null;
444
+ var loadSqlJsFactory = async () => {
445
+ if (!sqlJsFactoryPromise) {
446
+ sqlJsFactoryPromise = (async () => {
447
+ const initSqlJs = (await import("sql.js")).default;
448
+ const wasmPath = resolveSqlJsWasmPath();
449
+ return initSqlJs({
450
+ locateFile: (file) => {
451
+ if (file === "sql-wasm.wasm" && wasmPath) {
452
+ return wasmPath;
453
+ }
454
+ return file;
455
+ }
456
+ });
457
+ })();
458
+ }
459
+ return sqlJsFactoryPromise;
460
+ };
461
+ var openSqlJsDatabase = async (dbPath) => {
462
+ await fs2.mkdir(path2.dirname(dbPath), { recursive: true, mode: 448 });
463
+ const SQL = await loadSqlJsFactory();
464
+ let bytes;
465
+ try {
466
+ bytes = await fs2.readFile(dbPath);
467
+ } catch (error) {
468
+ if (error?.code !== "ENOENT") {
469
+ throw error;
470
+ }
471
+ }
472
+ const db = bytes ? new SQL.Database(bytes) : new SQL.Database();
473
+ db.run("PRAGMA foreign_keys = ON");
474
+ let dirty = false;
475
+ return {
476
+ rows(sql, params = []) {
477
+ const statement = db.prepare(sql, params);
478
+ const rows = [];
479
+ try {
480
+ while (statement.step()) {
481
+ rows.push(statement.getAsObject());
482
+ }
483
+ } finally {
484
+ statement.free();
485
+ }
486
+ return rows;
487
+ },
488
+ run(sql, params = []) {
489
+ db.run(sql, params);
490
+ dirty = true;
491
+ },
492
+ async persist() {
493
+ if (!dirty) {
494
+ return;
495
+ }
496
+ const tempPath = `${dbPath}.${process.pid}.tmp`;
497
+ await fs2.writeFile(tempPath, Buffer.from(db.export()), { mode: 384 });
498
+ await fs2.rename(tempPath, dbPath);
499
+ dirty = false;
500
+ },
501
+ close() {
502
+ db.close();
503
+ }
504
+ };
505
+ };
506
+ var openSessionDatabase = async (profile) => {
507
+ const dbPath = sessionsDatabasePath(profile);
508
+ return await openBunDatabase(dbPath) ?? openSqlJsDatabase(dbPath);
509
+ };
510
+ var ensureSchema = (db) => {
511
+ db.run(`
512
+ CREATE TABLE IF NOT EXISTS sessions (
513
+ thread_id TEXT PRIMARY KEY,
514
+ title TEXT NOT NULL,
515
+ project_id TEXT,
516
+ project_name TEXT,
517
+ model TEXT,
518
+ profile TEXT NOT NULL,
519
+ created_at TEXT NOT NULL,
520
+ updated_at TEXT NOT NULL,
521
+ message_count INTEGER NOT NULL DEFAULT 0
522
+ )
523
+ `);
524
+ db.run(`
525
+ CREATE TABLE IF NOT EXISTS session_messages (
526
+ session_thread_id TEXT NOT NULL,
527
+ message_index INTEGER NOT NULL,
528
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
529
+ content TEXT NOT NULL,
530
+ created_at TEXT NOT NULL,
531
+ PRIMARY KEY (session_thread_id, message_index),
532
+ FOREIGN KEY (session_thread_id) REFERENCES sessions(thread_id) ON DELETE CASCADE
533
+ )
534
+ `);
535
+ db.run("CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at DESC)");
536
+ db.run(
537
+ "CREATE INDEX IF NOT EXISTS idx_session_messages_thread ON session_messages(session_thread_id, message_index)"
538
+ );
539
+ ensureFtsSchema(db);
540
+ };
541
+ var ensureFtsSchema = (db) => {
542
+ try {
543
+ db.run(`
544
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_messages_fts USING fts5(
545
+ thread_id UNINDEXED,
546
+ title,
547
+ project,
548
+ content
549
+ )
550
+ `);
551
+ return true;
552
+ } catch {
553
+ return false;
554
+ }
555
+ };
556
+ var optionalString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
557
+ var messageFromRow = (row) => {
558
+ const role = row.role;
559
+ const content = row.content;
560
+ const createdAt = row.created_at;
561
+ if (role !== "user" && role !== "assistant" || typeof content !== "string") {
562
+ return null;
563
+ }
564
+ return {
565
+ role,
566
+ content,
567
+ createdAt: typeof createdAt === "string" ? createdAt : nowIso()
568
+ };
569
+ };
570
+ var sessionFromRow = (db, row) => {
571
+ const threadId = row.thread_id;
572
+ const title = row.title;
573
+ const createdAt = row.created_at;
574
+ const updatedAt = row.updated_at;
575
+ if (typeof threadId !== "string" || typeof title !== "string" || typeof createdAt !== "string" || typeof updatedAt !== "string") {
576
+ return null;
577
+ }
578
+ const messages = db.rows(
579
+ `SELECT role, content, created_at
580
+ FROM session_messages
581
+ WHERE session_thread_id = ?
582
+ ORDER BY message_index ASC`,
583
+ [threadId]
584
+ ).map(messageFromRow).filter((message) => Boolean(message));
585
+ return {
586
+ threadId,
587
+ title,
588
+ projectId: optionalString(row.project_id),
589
+ projectName: optionalString(row.project_name),
590
+ model: optionalString(row.model),
591
+ profile: optionalString(row.profile),
592
+ createdAt,
593
+ updatedAt,
594
+ messageCount: Number(row.message_count) || messages.length,
595
+ messages
596
+ };
597
+ };
598
+ var getSessionFromDb = (db, threadId) => {
599
+ const row = db.rows("SELECT * FROM sessions WHERE thread_id = ?", [threadId])[0];
600
+ return row ? sessionFromRow(db, row) : null;
601
+ };
602
+ var runTransaction = (db, fn) => {
603
+ db.run("BEGIN IMMEDIATE");
604
+ try {
605
+ const result = fn();
606
+ db.run("COMMIT");
607
+ return result;
608
+ } catch (error) {
609
+ try {
610
+ db.run("ROLLBACK");
611
+ } catch {
612
+ }
613
+ throw error;
614
+ }
615
+ };
616
+ var upsertSession = (db, session) => {
617
+ runTransaction(db, () => {
618
+ db.run(
619
+ `INSERT INTO sessions (
620
+ thread_id,
621
+ title,
622
+ project_id,
623
+ project_name,
624
+ model,
625
+ profile,
626
+ created_at,
627
+ updated_at,
628
+ message_count
629
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
630
+ ON CONFLICT(thread_id) DO UPDATE SET
631
+ title = excluded.title,
632
+ project_id = excluded.project_id,
633
+ project_name = excluded.project_name,
634
+ model = excluded.model,
635
+ profile = excluded.profile,
636
+ created_at = excluded.created_at,
637
+ updated_at = excluded.updated_at,
638
+ message_count = excluded.message_count`,
639
+ [
640
+ session.threadId,
641
+ session.title,
642
+ session.projectId ?? null,
643
+ session.projectName ?? null,
644
+ session.model ?? null,
645
+ session.profile ?? "default",
646
+ session.createdAt,
647
+ session.updatedAt,
648
+ session.messages.length
649
+ ]
650
+ );
651
+ db.run("DELETE FROM session_messages WHERE session_thread_id = ?", [session.threadId]);
652
+ session.messages.forEach((message, index) => {
653
+ db.run(
654
+ `INSERT INTO session_messages (
655
+ session_thread_id,
656
+ message_index,
657
+ role,
658
+ content,
659
+ created_at
660
+ ) VALUES (?, ?, ?, ?, ?)`,
661
+ [session.threadId, index, message.role, message.content, message.createdAt]
662
+ );
663
+ });
664
+ upsertSessionFts(db, session);
665
+ });
666
+ };
667
+ var upsertSessionFts = (db, session) => {
668
+ if (!ensureFtsSchema(db)) {
669
+ return;
670
+ }
671
+ try {
672
+ db.run("DELETE FROM session_messages_fts WHERE thread_id = ?", [session.threadId]);
673
+ db.run(
674
+ `INSERT INTO session_messages_fts (thread_id, title, project, content)
675
+ VALUES (?, ?, ?, ?)`,
676
+ [
677
+ session.threadId,
678
+ session.title,
679
+ [session.projectId, session.projectName].filter(Boolean).join(" "),
680
+ session.messages.map((message) => message.content).join("\n")
681
+ ]
682
+ );
683
+ } catch {
684
+ }
685
+ };
686
+ var deleteSessionFts = (db, threadId) => {
687
+ if (!ensureFtsSchema(db)) {
688
+ return;
689
+ }
690
+ try {
691
+ db.run("DELETE FROM session_messages_fts WHERE thread_id = ?", [threadId]);
692
+ } catch {
693
+ }
694
+ };
695
+ var readLegacySessionFile = async (filePath) => {
696
+ try {
697
+ const raw = await fs2.readFile(filePath, "utf8");
698
+ const parsed = JSON.parse(raw);
699
+ return parsed && typeof parsed === "object" ? normalizeLegacySession(parsed) : null;
700
+ } catch (error) {
701
+ if (error?.code === "ENOENT") {
702
+ return null;
703
+ }
704
+ throw error;
705
+ }
706
+ };
707
+ var normalizeLegacySession = (value) => {
708
+ if (!value || typeof value !== "object" || typeof value.threadId !== "string") {
709
+ return null;
710
+ }
711
+ const timestamp = nowIso();
712
+ const messages = Array.isArray(value.messages) ? value.messages.map((message) => {
713
+ if (!message || message.role !== "user" && message.role !== "assistant" || typeof message.content !== "string") {
714
+ return null;
715
+ }
716
+ return {
717
+ role: message.role,
718
+ content: message.content,
719
+ createdAt: typeof message.createdAt === "string" ? message.createdAt : timestamp
720
+ };
721
+ }).filter(
722
+ (message) => Boolean(message)
723
+ ) : [];
724
+ return {
725
+ threadId: value.threadId,
726
+ title: typeof value.title === "string" && value.title.trim() ? sanitizeTitle(value.title) : "Untitled CloudEval session",
727
+ projectId: optionalString(value.projectId),
728
+ projectName: optionalString(value.projectName),
729
+ model: optionalString(value.model),
730
+ profile: optionalString(value.profile),
731
+ createdAt: typeof value.createdAt === "string" ? value.createdAt : timestamp,
732
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : timestamp,
733
+ messageCount: messages.length,
734
+ messages
735
+ };
736
+ };
737
+ var readLegacySessions = async (profile) => {
738
+ try {
739
+ const dir = legacySessionsDir(profile);
740
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
741
+ const sessions = await Promise.all(
742
+ entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => readLegacySessionFile(path2.join(dir, entry.name)))
743
+ );
744
+ return sessions.filter((session) => Boolean(session));
745
+ } catch (error) {
746
+ if (error?.code === "ENOENT") {
747
+ return [];
748
+ }
749
+ throw error;
750
+ }
751
+ };
752
+ var migrateLegacyJsonSessions = async (db, profile) => {
753
+ const normalizedProfile = normalizeConfigProfile(profile);
754
+ const legacySessions = await readLegacySessions(normalizedProfile);
755
+ for (const session of legacySessions) {
756
+ if (getSessionFromDb(db, session.threadId)) {
757
+ continue;
758
+ }
759
+ upsertSession(db, {
760
+ ...session,
761
+ profile: session.profile ?? normalizedProfile,
762
+ messageCount: session.messages.length
763
+ });
764
+ }
765
+ };
766
+ var withSessionDatabase = async (profile, fn) => {
767
+ const normalizedProfile = normalizeConfigProfile(profile);
768
+ const db = await openSessionDatabase(normalizedProfile);
769
+ try {
770
+ ensureSchema(db);
771
+ await migrateLegacyJsonSessions(db, normalizedProfile);
772
+ const result = await fn(db, normalizedProfile);
773
+ await db.persist();
774
+ return result;
775
+ } finally {
776
+ db.close();
777
+ }
778
+ };
779
+ var recordSessionTurn = async ({
780
+ threadId,
781
+ question,
782
+ response,
783
+ project,
784
+ model,
785
+ profile
786
+ }) => {
787
+ if (!threadId || !question.trim() && !response.trim()) {
788
+ return;
789
+ }
790
+ await withSessionDatabase(profile, (db, normalizedProfile) => {
791
+ const existing = getSessionFromDb(db, threadId);
792
+ const timestamp = nowIso();
793
+ const messages = [
794
+ ...existing?.messages ?? [],
795
+ ...question.trim() ? [{ role: "user", content: question.trim(), createdAt: timestamp }] : [],
796
+ ...response.trim() ? [{ role: "assistant", content: response.trim(), createdAt: timestamp }] : []
797
+ ];
798
+ upsertSession(db, {
799
+ threadId,
800
+ title: existing?.title ?? titleFromQuestion(question),
801
+ projectId: project?.id ?? existing?.projectId,
802
+ projectName: project?.name ?? existing?.projectName,
803
+ model: model ?? existing?.model,
804
+ profile: normalizedProfile,
805
+ createdAt: existing?.createdAt ?? timestamp,
806
+ updatedAt: timestamp,
807
+ messageCount: messages.length,
808
+ messages
809
+ });
810
+ });
811
+ };
812
+ var listSessions = async (limit = 20, profile) => withSessionDatabase(profile, (db) => {
813
+ const rows = db.rows(
814
+ `SELECT *
815
+ FROM sessions
816
+ ORDER BY updated_at DESC
817
+ LIMIT ?`,
818
+ [Math.max(1, limit)]
819
+ );
820
+ return rows.map((row) => sessionFromRow(db, row)).filter((session) => Boolean(session));
821
+ });
822
+ var listAllSessionsFromDb = (db) => {
823
+ const rows = db.rows(
824
+ `SELECT *
825
+ FROM sessions
826
+ ORDER BY updated_at DESC`
827
+ );
828
+ return rows.map((row) => sessionFromRow(db, row)).filter((session) => Boolean(session));
829
+ };
830
+ var getSession = async (threadId, profile) => withSessionDatabase(profile, (db) => getSessionFromDb(db, threadId));
831
+ var renameSession = async (threadId, title, profile) => {
832
+ return withSessionDatabase(profile, (db) => {
833
+ const session = getSessionFromDb(db, threadId);
834
+ if (!session) {
835
+ return null;
836
+ }
837
+ const updated = {
838
+ ...session,
839
+ title: sanitizeTitle(title),
840
+ updatedAt: nowIso()
841
+ };
842
+ upsertSession(db, updated);
843
+ return updated;
844
+ });
845
+ };
846
+ var searchTerms = (query) => query.toLowerCase().split(/[^a-z0-9]+/i).map((term) => term.trim()).filter((term) => term.length > 1);
847
+ var countTerm = (text, term) => {
848
+ if (!text || !term) return 0;
849
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
850
+ return text.match(new RegExp(escaped, "gi"))?.length ?? 0;
851
+ };
852
+ var previewFor = (session, terms) => {
853
+ const matched = session.messages.map((message) => ({
854
+ message,
855
+ score: terms.reduce((total, term) => total + countTerm(message.content, term), 0)
856
+ })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score)[0]?.message;
857
+ const value = matched?.content ?? session.messages.at(-1)?.content ?? session.title;
858
+ const singleLine = value.replace(/\s+/g, " ").trim();
859
+ return singleLine.length > 180 ? `${singleLine.slice(0, 177)}...` : singleLine;
860
+ };
861
+ var scoreSession = (session, terms) => {
862
+ const title = session.title.toLowerCase();
863
+ const project = `${session.projectId ?? ""} ${session.projectName ?? ""}`.toLowerCase();
864
+ const messages = session.messages.map((message) => message.content).join("\n").toLowerCase();
865
+ return terms.reduce(
866
+ (score, term) => score + countTerm(title, term) * 8 + countTerm(project, term) * 4 + countTerm(messages, term),
867
+ 0
868
+ );
869
+ };
870
+ var toSearchResult = (session, score, terms) => ({
871
+ threadId: session.threadId,
872
+ title: session.title,
873
+ projectId: session.projectId,
874
+ projectName: session.projectName,
875
+ model: session.model,
876
+ profile: session.profile,
877
+ createdAt: session.createdAt,
878
+ updatedAt: session.updatedAt,
879
+ messageCount: session.messageCount,
880
+ score,
881
+ preview: previewFor(session, terms)
882
+ });
883
+ var searchSessions = async (query, options = {}) => {
884
+ const terms = searchTerms(query);
885
+ const limit = Math.max(1, options.limit ?? 20);
886
+ return withSessionDatabase(options.profile, (db) => {
887
+ const sessions = listAllSessionsFromDb(db);
888
+ if (!terms.length) {
889
+ return sessions.slice(0, limit).map((session) => toSearchResult(session, 0, []));
890
+ }
891
+ const ftsMatches = searchSessionsWithFts(db, terms, limit * 4);
892
+ const candidates = ftsMatches?.length ? ftsMatches : sessions;
893
+ return candidates.map((session) => ({
894
+ session,
895
+ score: scoreSession(session, terms)
896
+ })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || b.session.updatedAt.localeCompare(a.session.updatedAt)).slice(0, limit).map((entry) => toSearchResult(entry.session, entry.score, terms));
897
+ });
898
+ };
899
+ var ftsQueryForTerms = (terms) => terms.map((term) => term.replace(/"/g, "")).filter(Boolean).map((term) => `${term}*`).join(" OR ");
900
+ var rebuildFtsIndex = (db) => {
901
+ if (!ensureFtsSchema(db)) {
902
+ return;
903
+ }
904
+ try {
905
+ const sessionCount = Number(db.rows("SELECT COUNT(*) AS count FROM sessions")[0]?.count ?? 0);
906
+ const ftsCount = Number(db.rows("SELECT COUNT(*) AS count FROM session_messages_fts")[0]?.count ?? 0);
907
+ if (ftsCount >= sessionCount) {
908
+ return;
909
+ }
910
+ db.run("DELETE FROM session_messages_fts");
911
+ for (const session of listAllSessionsFromDb(db)) {
912
+ upsertSessionFts(db, session);
913
+ }
914
+ } catch {
915
+ }
916
+ };
917
+ var searchSessionsWithFts = (db, terms, limit) => {
918
+ if (!ensureFtsSchema(db)) {
919
+ return null;
920
+ }
921
+ const query = ftsQueryForTerms(terms);
922
+ if (!query) {
923
+ return null;
924
+ }
925
+ try {
926
+ rebuildFtsIndex(db);
927
+ const rows = db.rows(
928
+ `SELECT sessions.*
929
+ FROM session_messages_fts
930
+ JOIN sessions ON sessions.thread_id = session_messages_fts.thread_id
931
+ WHERE session_messages_fts MATCH ?
932
+ ORDER BY bm25(session_messages_fts) ASC, sessions.updated_at DESC
933
+ LIMIT ?`,
934
+ [query, limit]
935
+ );
936
+ return rows.map((row) => sessionFromRow(db, row)).filter((session) => Boolean(session));
937
+ } catch {
938
+ return null;
939
+ }
940
+ };
941
+ var resolveSessionReference = async (reference, profile) => {
942
+ const trimmed = reference.trim();
943
+ if (!trimmed) {
944
+ return null;
945
+ }
946
+ const exact = await getSession(trimmed, profile);
947
+ if (exact) {
948
+ return exact;
949
+ }
950
+ const sessions = await exportSessions(profile);
951
+ const lower = trimmed.toLowerCase();
952
+ return sessions.find((session) => session.title.toLowerCase() === lower) ?? sessions.find((session) => session.threadId.startsWith(trimmed)) ?? sessions.find((session) => session.title.toLowerCase().includes(lower)) ?? null;
953
+ };
954
+ var deleteSession = async (threadId, profile) => {
955
+ const deleted = await withSessionDatabase(profile, (db) => {
956
+ const existing = getSessionFromDb(db, threadId);
957
+ if (!existing) {
958
+ return false;
959
+ }
960
+ deleteSessionFts(db, threadId);
961
+ db.run("DELETE FROM sessions WHERE thread_id = ?", [threadId]);
962
+ return true;
963
+ });
964
+ try {
965
+ await fs2.unlink(legacySessionPath(threadId, profile));
966
+ } catch (error) {
967
+ if (error?.code !== "ENOENT") {
968
+ throw error;
969
+ }
970
+ }
971
+ return deleted;
972
+ };
973
+ var exportSessions = async (profile) => listSessions(Number.MAX_SAFE_INTEGER, profile);
974
+ var pruneSessions = async (olderThanDays, profile) => {
975
+ const cutoff = Date.now() - Math.max(1, olderThanDays) * 24 * 60 * 60 * 1e3;
976
+ const sessions = await exportSessions(profile);
977
+ let deleted = 0;
978
+ for (const session of sessions) {
979
+ const updatedAt = Date.parse(session.updatedAt);
980
+ if (Number.isFinite(updatedAt) && updatedAt < cutoff) {
981
+ if (await deleteSession(session.threadId, profile)) {
982
+ deleted++;
983
+ }
984
+ }
985
+ }
986
+ return deleted;
987
+ };
988
+
989
+ export {
990
+ resolveFrontendBaseUrl,
991
+ buildFrontendUrl,
992
+ openExternalUrl,
993
+ normalizeConfigProfile,
994
+ getActiveConfigProfile,
995
+ getCloudevalConfigDir,
996
+ getCliConfigPath,
997
+ loadCliConfig,
998
+ saveCliConfig,
999
+ listCliConfigProfiles,
1000
+ normalizeCliMode,
1001
+ readCliConfigValue,
1002
+ writeCliConfigValue,
1003
+ unsetCliConfigValue,
1004
+ getFirstNameForDisplay,
1005
+ recordSessionTurn,
1006
+ listSessions,
1007
+ getSession,
1008
+ renameSession,
1009
+ searchSessions,
1010
+ resolveSessionReference,
1011
+ deleteSession,
1012
+ exportSessions,
1013
+ pruneSessions
1014
+ };