@cookielab.io/klovi 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -223,13 +223,23 @@ class PluginRegistry {
223
223
  getAllPlugins() {
224
224
  return [...this.plugins.values()].map((entry) => entry.plugin);
225
225
  }
226
- discoverAllProjects() {
226
+ discoverPluginStates(includeSessions) {
227
227
  return Effect2.gen(this, function* () {
228
- const allProjects = [];
229
- for (const { plugin, configLayer } of this.plugins.values()) {
230
- const projects = yield* plugin.discoverProjects.pipe(Effect2.provide(configLayer), Effect2.catchAll(() => Effect2.succeed([])));
231
- allProjects.push(...projects);
228
+ const states = [];
229
+ for (const entry of this.plugins.values()) {
230
+ const discoveredIndex = includeSessions && entry.plugin.discoverIndex ? yield* entry.plugin.discoverIndex.pipe(Effect2.provide(entry.configLayer), Effect2.catchAll(() => Effect2.succeed(undefined))) : undefined;
231
+ const projects = discoveredIndex?.projects ?? (yield* entry.plugin.discoverProjects.pipe(Effect2.provide(entry.configLayer), Effect2.catchAll(() => Effect2.succeed([]))));
232
+ states.push({
233
+ entry,
234
+ projects,
235
+ ...discoveredIndex ? { sessionsByNativeId: discoveredIndex.sessionsByNativeId } : {}
236
+ });
232
237
  }
238
+ return states;
239
+ });
240
+ }
241
+ mergeProjects(allProjects) {
242
+ return Effect2.gen(function* () {
233
243
  yield* resolveT3CodePaths(allProjects);
234
244
  const projectsByPath = new Map;
235
245
  for (const project of allProjects) {
@@ -259,6 +269,50 @@ class PluginRegistry {
259
269
  return merged;
260
270
  });
261
271
  }
272
+ encodeSessions(pluginId, sessions) {
273
+ return sessions.map((session) => ({
274
+ ...session,
275
+ sessionId: this.sessionIdEncoder(pluginId, session.sessionId),
276
+ pluginId
277
+ }));
278
+ }
279
+ loadSourceSessions(state, source) {
280
+ const discoveredSessions = state.sessionsByNativeId?.get(source.nativeId);
281
+ if (discoveredSessions) {
282
+ return Effect2.succeed(this.encodeSessions(source.pluginId, discoveredSessions));
283
+ }
284
+ return state.entry.plugin.listSessions(source.nativeId).pipe(Effect2.provide(state.entry.configLayer), Effect2.catchAll(() => Effect2.succeed([])), Effect2.map((sessions) => this.encodeSessions(source.pluginId, sessions)));
285
+ }
286
+ discoverAllProjects() {
287
+ return Effect2.gen(this, function* () {
288
+ const states = yield* this.discoverPluginStates(false);
289
+ return yield* this.mergeProjects(states.flatMap((state) => state.projects));
290
+ });
291
+ }
292
+ discoverAllProjectsWithSessions() {
293
+ return Effect2.gen(this, function* () {
294
+ const states = yield* this.discoverPluginStates(true);
295
+ const mergedProjects = yield* this.mergeProjects(states.flatMap((state) => state.projects));
296
+ const statesByPluginId = new Map(states.map((state) => [state.entry.plugin.id, state]));
297
+ const sessionsByEncodedPath = new Map;
298
+ for (const project of mergedProjects) {
299
+ const allSessions = [];
300
+ for (const source of project.sources) {
301
+ const state = statesByPluginId.get(source.pluginId);
302
+ if (!state) {
303
+ continue;
304
+ }
305
+ allSessions.push(...yield* this.loadSourceSessions(state, source));
306
+ }
307
+ sortByIsoDesc(allSessions, (session) => session.timestamp);
308
+ sessionsByEncodedPath.set(project.encodedPath, allSessions);
309
+ }
310
+ return {
311
+ projects: mergedProjects,
312
+ sessionsByEncodedPath
313
+ };
314
+ });
315
+ }
262
316
  listAllSessions(project) {
263
317
  return Effect2.gen(this, function* () {
264
318
  const allSessions = [];
@@ -267,12 +321,7 @@ class PluginRegistry {
267
321
  if (!entry) {
268
322
  continue;
269
323
  }
270
- const sessions = yield* entry.plugin.listSessions(source.nativeId).pipe(Effect2.provide(entry.configLayer), Effect2.catchAll(() => Effect2.succeed([])));
271
- allSessions.push(...sessions.map((session) => ({
272
- ...session,
273
- sessionId: this.sessionIdEncoder(source.pluginId, session.sessionId),
274
- pluginId: source.pluginId
275
- })));
324
+ allSessions.push(...yield* entry.plugin.listSessions(source.nativeId).pipe(Effect2.provide(entry.configLayer), Effect2.catchAll(() => Effect2.succeed([])), Effect2.map((sessions) => this.encodeSessions(source.pluginId, sessions))));
276
325
  }
277
326
  sortByIsoDesc(allSessions, (session) => session.timestamp);
278
327
  return allSessions;
@@ -880,7 +929,7 @@ var init_platform_node = __esm(() => {
880
929
  import { execFile } from "node:child_process";
881
930
 
882
931
  // ../../packages/server/src/effect/bootstrap.ts
883
- import { join as join14 } from "node:path";
932
+ import { join as join15 } from "node:path";
884
933
  import { HttpServer } from "@effect/platform";
885
934
  import { Cause, Effect as Effect32, Fiber, Layer as Layer8 } from "effect";
886
935
 
@@ -2773,6 +2822,15 @@ function extractFirstUserTextFromHeaders(db, composerId, headers) {
2773
2822
  }
2774
2823
  return "";
2775
2824
  }
2825
+ function resolveComposerFallbackMessage(composer) {
2826
+ if (isNonEmptyString(composer.name)) {
2827
+ return composer.name.trim();
2828
+ }
2829
+ if (isNonEmptyString(composer.subtitle)) {
2830
+ return composer.subtitle.trim();
2831
+ }
2832
+ return "Cursor session";
2833
+ }
2776
2834
  function resolveComposerFirstMessage(globalDb, composer, composerId) {
2777
2835
  if (globalDb) {
2778
2836
  const rawComposerData = queryKeyValueRow(globalDb, `composerData:${composerId}`);
@@ -2786,13 +2844,7 @@ function resolveComposerFirstMessage(globalDb, composer, composerId) {
2786
2844
  return truncate(fromHeaders, SESSION_PREVIEW_MAX_LENGTH);
2787
2845
  }
2788
2846
  }
2789
- if (isNonEmptyString(composer.name)) {
2790
- return composer.name.trim();
2791
- }
2792
- if (isNonEmptyString(composer.subtitle)) {
2793
- return composer.subtitle.trim();
2794
- }
2795
- return "Cursor session";
2847
+ return resolveComposerFallbackMessage(composer);
2796
2848
  }
2797
2849
  function resolveComposerSessionType(unifiedMode) {
2798
2850
  if (unifiedMode === "plan") {
@@ -2803,7 +2855,13 @@ function resolveComposerSessionType(unifiedMode) {
2803
2855
  }
2804
2856
  return;
2805
2857
  }
2806
- function createComposerSummary(projectPath, workspaceDbPath, composer, globalDb) {
2858
+ function createComposerSummary({
2859
+ projectPath,
2860
+ workspaceDbPath,
2861
+ composer,
2862
+ globalDb,
2863
+ options = { resolveFirstMessage: true }
2864
+ }) {
2807
2865
  if (!isNonEmptyString(composer.composerId)) {
2808
2866
  return null;
2809
2867
  }
@@ -2813,7 +2871,7 @@ function createComposerSummary(projectPath, workspaceDbPath, composer, globalDb)
2813
2871
  }
2814
2872
  const { composerId } = composer;
2815
2873
  const unifiedMode = composer.unifiedMode ?? composer.forceMode ?? "chat";
2816
- const firstMessage = resolveComposerFirstMessage(globalDb, composer, composerId);
2874
+ const firstMessage = options.resolveFirstMessage ? resolveComposerFirstMessage(globalDb, composer, composerId) : resolveComposerFallbackMessage(composer);
2817
2875
  return {
2818
2876
  kind: "composer",
2819
2877
  rawSessionId: `composer:${composerId}`,
@@ -2840,6 +2898,31 @@ function parseProjectPathFromWorkspaceJson(content) {
2840
2898
  }
2841
2899
  return fileUrlToPath(parsed.folder);
2842
2900
  }
2901
+ function discoverWorkspaceDescriptors(targetProjectPath) {
2902
+ return Effect21.gen(function* () {
2903
+ const workspaceStorageDir = yield* getCursorWorkspaceStorageDirEffect();
2904
+ const workspaceEntries = yield* readDirEntriesSafe2(workspaceStorageDir);
2905
+ const workspaceDescriptors = [];
2906
+ for (const entry of workspaceEntries) {
2907
+ if (!entry.isDirectory) {
2908
+ continue;
2909
+ }
2910
+ const workspaceDir = join12(workspaceStorageDir, entry.name);
2911
+ const workspaceJsonPath = join12(workspaceDir, "workspace.json");
2912
+ const workspaceDbPath = join12(workspaceDir, "state.vscdb");
2913
+ const exists = yield* fileExists3(workspaceJsonPath);
2914
+ if (!exists) {
2915
+ continue;
2916
+ }
2917
+ const projectPath = yield* readFileText3(workspaceJsonPath).pipe(Effect21.map(parseProjectPathFromWorkspaceJson), Effect21.catchAll(() => Effect21.succeed(null)));
2918
+ if (!projectPath || targetProjectPath && projectPath !== targetProjectPath) {
2919
+ continue;
2920
+ }
2921
+ workspaceDescriptors.push({ projectPath, workspaceDbPath });
2922
+ }
2923
+ return workspaceDescriptors;
2924
+ });
2925
+ }
2843
2926
  function findProjectSessions(sessionsByProject, projectPath) {
2844
2927
  const existing = sessionsByProject.get(projectPath);
2845
2928
  if (existing) {
@@ -2852,6 +2935,46 @@ function findProjectSessions(sessionsByProject, projectPath) {
2852
2935
  function pushSession(sessionsByProject, session) {
2853
2936
  findProjectSessions(sessionsByProject, session.projectPath).push(session);
2854
2937
  }
2938
+ function pushSessions(sessionsByProject, sessions) {
2939
+ for (const session of sessions) {
2940
+ pushSession(sessionsByProject, session);
2941
+ }
2942
+ }
2943
+ function collectComposerSessions({
2944
+ workspaceDescriptors,
2945
+ globalDb,
2946
+ sessionsByProject,
2947
+ composersById,
2948
+ options
2949
+ }) {
2950
+ return Effect21.gen(function* () {
2951
+ for (const descriptor of workspaceDescriptors) {
2952
+ const workspaceDb = yield* openCursorDbIfExists(descriptor.workspaceDbPath);
2953
+ if (!workspaceDb) {
2954
+ continue;
2955
+ }
2956
+ try {
2957
+ const composers = readWorkspaceComposerEntries(workspaceDb);
2958
+ for (const composer of composers) {
2959
+ const summary = createComposerSummary({
2960
+ projectPath: descriptor.projectPath,
2961
+ workspaceDbPath: descriptor.workspaceDbPath,
2962
+ composer,
2963
+ globalDb,
2964
+ options
2965
+ });
2966
+ if (!summary) {
2967
+ continue;
2968
+ }
2969
+ composersById.set(summary.composerId, summary);
2970
+ pushSession(sessionsByProject, summary);
2971
+ }
2972
+ } finally {
2973
+ workspaceDb.close();
2974
+ }
2975
+ }
2976
+ });
2977
+ }
2855
2978
  function readFirstTranscriptUserMessage(text) {
2856
2979
  let firstMessage = "";
2857
2980
  iterateJsonl3(text, ({ parsed }) => {
@@ -2895,45 +3018,50 @@ function listTranscriptFiles(agentTranscriptsDir) {
2895
3018
  return files;
2896
3019
  });
2897
3020
  }
2898
- function discoverBackgroundAgents(workspaceProjectPaths) {
3021
+ function createBackgroundAgentSummary(projectPath, transcript, firstMessage) {
3022
+ return {
3023
+ kind: "agent",
3024
+ rawSessionId: `agent:${transcript.agentId}`,
3025
+ projectPath,
3026
+ agentId: transcript.agentId,
3027
+ filePath: transcript.filePath,
3028
+ timestamp: transcript.mtimeIso,
3029
+ timestampMs: new Date(transcript.mtimeIso).getTime(),
3030
+ firstMessage,
3031
+ slug: transcript.agentId,
3032
+ model: "unknown",
3033
+ gitBranch: "",
3034
+ sessionType: "implementation"
3035
+ };
3036
+ }
3037
+ function discoverBackgroundAgentsForProject(projectPath, options = { readPreviewText: true }) {
2899
3038
  return Effect21.gen(function* () {
2900
3039
  const config = yield* PluginConfig;
2901
- const projectsDir = join12(config.dataDir, "projects");
2902
- const entries = yield* readDirEntriesSafe2(projectsDir);
2903
- const encodedProjectPaths = new Map(workspaceProjectPaths.map((projectPath) => [encodeCursorProjectPath(projectPath), projectPath]));
3040
+ const agentTranscriptsDir = join12(config.dataDir, "projects", encodeCursorProjectPath(projectPath), "agent-transcripts");
2904
3041
  const agentsById = new Map;
2905
- for (const entry of entries) {
2906
- if (!entry.isDirectory) {
2907
- continue;
2908
- }
2909
- const projectPath = encodedProjectPaths.get(entry.name);
2910
- if (!projectPath) {
2911
- continue;
2912
- }
2913
- const agentTranscriptsDir = join12(projectsDir, entry.name, "agent-transcripts");
2914
- const transcriptDirExists = yield* fileExists3(agentTranscriptsDir);
2915
- if (!transcriptDirExists) {
2916
- continue;
2917
- }
2918
- const transcripts = yield* listTranscriptFiles(agentTranscriptsDir);
2919
- for (const transcript of transcripts) {
3042
+ const transcriptDirExists = yield* fileExists3(agentTranscriptsDir);
3043
+ if (!transcriptDirExists) {
3044
+ return agentsById;
3045
+ }
3046
+ const transcripts = yield* listTranscriptFiles(agentTranscriptsDir);
3047
+ for (const transcript of transcripts) {
3048
+ let firstMessage = "Cursor background agent";
3049
+ if (options.readPreviewText) {
2920
3050
  const text = yield* readFileText3(transcript.filePath).pipe(Effect21.catchAll(() => Effect21.succeed("")));
2921
- const firstMessage = readFirstTranscriptUserMessage(text) || "Cursor background agent";
2922
- const timestampMs = new Date(transcript.mtimeIso).getTime();
2923
- agentsById.set(transcript.agentId, {
2924
- kind: "agent",
2925
- rawSessionId: `agent:${transcript.agentId}`,
2926
- projectPath,
2927
- agentId: transcript.agentId,
2928
- filePath: transcript.filePath,
2929
- timestamp: transcript.mtimeIso,
2930
- timestampMs,
2931
- firstMessage,
2932
- slug: transcript.agentId,
2933
- model: "unknown",
2934
- gitBranch: "",
2935
- sessionType: "implementation"
2936
- });
3051
+ firstMessage = readFirstTranscriptUserMessage(text) || firstMessage;
3052
+ }
3053
+ agentsById.set(transcript.agentId, createBackgroundAgentSummary(projectPath, transcript, firstMessage));
3054
+ }
3055
+ return agentsById;
3056
+ });
3057
+ }
3058
+ function discoverBackgroundAgents(projectPaths, options = { readPreviewText: true }) {
3059
+ return Effect21.gen(function* () {
3060
+ const agentsById = new Map;
3061
+ for (const projectPath of new Set(projectPaths)) {
3062
+ const projectAgents = yield* discoverBackgroundAgentsForProject(projectPath, options);
3063
+ for (const agent of projectAgents.values()) {
3064
+ agentsById.set(agent.agentId, agent);
2937
3065
  }
2938
3066
  }
2939
3067
  return agentsById;
@@ -2970,7 +3098,7 @@ function createPlanSummary(entry, projectPath, filePath, displayName) {
2970
3098
  sessionType: "plan"
2971
3099
  };
2972
3100
  }
2973
- function discoverMappedPlans(globalDb, composersById, agentsById) {
3101
+ function discoverMappedPlans(globalDb, composersById, agentsById, options = { loadDisplayName: true }) {
2974
3102
  return Effect21.gen(function* () {
2975
3103
  const plansById = new Map;
2976
3104
  if (!globalDb) {
@@ -2992,8 +3120,8 @@ function discoverMappedPlans(globalDb, composersById, agentsById) {
2992
3120
  if (!exists) {
2993
3121
  continue;
2994
3122
  }
2995
- const displayName = yield* readPlanDisplayName(filePath).pipe(Effect21.catchAll(() => Effect21.succeed("")));
2996
- const summary = createPlanSummary(entry, projectPath, filePath, displayName || entry.name?.trim() || "");
3123
+ const displayName = options.loadDisplayName ? yield* readPlanDisplayName(filePath).pipe(Effect21.catchAll(() => Effect21.succeed(""))) : "";
3124
+ const summary = createPlanSummary(entry, projectPath, filePath, displayName || entry.name?.trim() || "Cursor plan");
2997
3125
  if (!summary) {
2998
3126
  continue;
2999
3127
  }
@@ -3024,63 +3152,57 @@ function buildProjects(sessionsByProject) {
3024
3152
  sortByIsoDesc(projects, (project) => project.lastActivity);
3025
3153
  return projects;
3026
3154
  }
3155
+ function buildCursorProjectIndex(nativeId) {
3156
+ return Effect21.gen(function* () {
3157
+ const globalDb = yield* openCursorGlobalDb();
3158
+ const sessionsByProject = new Map;
3159
+ const composersById = new Map;
3160
+ try {
3161
+ const workspaceDescriptors = yield* discoverWorkspaceDescriptors(nativeId);
3162
+ yield* collectComposerSessions({
3163
+ workspaceDescriptors,
3164
+ globalDb,
3165
+ sessionsByProject,
3166
+ composersById,
3167
+ options: { resolveFirstMessage: true }
3168
+ });
3169
+ const agentsById = yield* discoverBackgroundAgents([nativeId], { readPreviewText: true });
3170
+ pushSessions(sessionsByProject, agentsById.values());
3171
+ const plansById = yield* discoverMappedPlans(globalDb, composersById, agentsById, { loadDisplayName: true });
3172
+ pushSessions(sessionsByProject, plansById.values());
3173
+ return {
3174
+ projects: buildProjects(sessionsByProject),
3175
+ sessionsByProject,
3176
+ composersById,
3177
+ agentsById,
3178
+ plansById
3179
+ };
3180
+ } finally {
3181
+ globalDb?.close();
3182
+ }
3183
+ });
3184
+ }
3027
3185
  function buildCursorIndex() {
3028
3186
  return Effect21.gen(function* () {
3029
- const workspaceStorageDir = yield* getCursorWorkspaceStorageDirEffect();
3030
- const workspaceEntries = yield* readDirEntriesSafe2(workspaceStorageDir);
3031
3187
  const globalDb = yield* openCursorGlobalDb();
3032
3188
  const sessionsByProject = new Map;
3033
3189
  const composersById = new Map;
3034
3190
  try {
3035
- const workspaceDescriptors = [];
3036
- for (const entry of workspaceEntries) {
3037
- if (!entry.isDirectory) {
3038
- continue;
3039
- }
3040
- const workspaceDir = join12(workspaceStorageDir, entry.name);
3041
- const workspaceJsonPath = join12(workspaceDir, "workspace.json");
3042
- const workspaceDbPath = join12(workspaceDir, "state.vscdb");
3043
- const exists = yield* fileExists3(workspaceJsonPath);
3044
- if (!exists) {
3045
- continue;
3046
- }
3047
- const projectPath = yield* readFileText3(workspaceJsonPath).pipe(Effect21.map(parseProjectPathFromWorkspaceJson), Effect21.catchAll(() => Effect21.succeed(null)));
3048
- if (!projectPath) {
3049
- continue;
3050
- }
3051
- workspaceDescriptors.push({ projectPath, workspaceDbPath });
3052
- }
3191
+ const workspaceDescriptors = yield* discoverWorkspaceDescriptors();
3053
3192
  const workspaceProjectPaths = [...new Set(workspaceDescriptors.map((descriptor) => descriptor.projectPath))];
3054
- for (const descriptor of workspaceDescriptors) {
3055
- const workspaceDb = yield* openCursorDbIfExists(descriptor.workspaceDbPath);
3056
- if (!workspaceDb) {
3057
- continue;
3058
- }
3059
- try {
3060
- const composers = readWorkspaceComposerEntries(workspaceDb);
3061
- for (const composer of composers) {
3062
- const summary = createComposerSummary(descriptor.projectPath, descriptor.workspaceDbPath, composer, globalDb);
3063
- if (!summary) {
3064
- continue;
3065
- }
3066
- composersById.set(summary.composerId, summary);
3067
- pushSession(sessionsByProject, summary);
3068
- }
3069
- } finally {
3070
- workspaceDb.close();
3071
- }
3072
- }
3193
+ yield* collectComposerSessions({
3194
+ workspaceDescriptors,
3195
+ globalDb,
3196
+ sessionsByProject,
3197
+ composersById,
3198
+ options: { resolveFirstMessage: true }
3199
+ });
3073
3200
  const agentsById = yield* discoverBackgroundAgents(workspaceProjectPaths);
3074
- for (const agent of agentsById.values()) {
3075
- pushSession(sessionsByProject, agent);
3076
- }
3201
+ pushSessions(sessionsByProject, agentsById.values());
3077
3202
  const plansById = yield* discoverMappedPlans(globalDb, composersById, agentsById);
3078
- for (const plan of plansById.values()) {
3079
- pushSession(sessionsByProject, plan);
3080
- }
3081
- const projects = buildProjects(sessionsByProject);
3203
+ pushSessions(sessionsByProject, plansById.values());
3082
3204
  return {
3083
- projects,
3205
+ projects: buildProjects(sessionsByProject),
3084
3206
  sessionsByProject,
3085
3207
  composersById,
3086
3208
  agentsById,
@@ -3103,11 +3225,44 @@ function toSessionSummary(session) {
3103
3225
  sessionType: session.sessionType
3104
3226
  };
3105
3227
  }
3228
+ function buildCursorDiscoveryIndex() {
3229
+ return buildCursorIndex().pipe(Effect21.map((index) => ({
3230
+ projects: index.projects,
3231
+ sessionsByNativeId: new Map([...index.sessionsByProject.entries()].map(([projectPath, sessions]) => [
3232
+ projectPath,
3233
+ sessions.map(toSessionSummary)
3234
+ ]))
3235
+ })));
3236
+ }
3106
3237
  function discoverCursorProjects() {
3107
- return buildCursorIndex().pipe(Effect21.map((index) => index.projects));
3238
+ return Effect21.gen(function* () {
3239
+ const globalDb = yield* openCursorGlobalDb();
3240
+ const sessionsByProject = new Map;
3241
+ const composersById = new Map;
3242
+ try {
3243
+ const workspaceDescriptors = yield* discoverWorkspaceDescriptors();
3244
+ const workspaceProjectPaths = [...new Set(workspaceDescriptors.map((descriptor) => descriptor.projectPath))];
3245
+ yield* collectComposerSessions({
3246
+ workspaceDescriptors,
3247
+ globalDb,
3248
+ sessionsByProject,
3249
+ composersById,
3250
+ options: { resolveFirstMessage: false }
3251
+ });
3252
+ const agentsById = yield* discoverBackgroundAgents(workspaceProjectPaths, { readPreviewText: false });
3253
+ pushSessions(sessionsByProject, agentsById.values());
3254
+ const plansById = yield* discoverMappedPlans(globalDb, composersById, agentsById, {
3255
+ loadDisplayName: false
3256
+ });
3257
+ pushSessions(sessionsByProject, plansById.values());
3258
+ return buildProjects(sessionsByProject);
3259
+ } finally {
3260
+ globalDb?.close();
3261
+ }
3262
+ });
3108
3263
  }
3109
3264
  function listCursorSessions(nativeId) {
3110
- return buildCursorIndex().pipe(Effect21.map((index) => {
3265
+ return buildCursorProjectIndex(nativeId).pipe(Effect21.map((index) => {
3111
3266
  const sessions = index.sessionsByProject.get(nativeId) ?? [];
3112
3267
  sortByIsoDesc(sessions, (session) => session.timestamp);
3113
3268
  return sessions.map(toSessionSummary);
@@ -3341,7 +3496,7 @@ function loadCursorAgentSession(summary) {
3341
3496
  }
3342
3497
  function loadCursorSession(nativeId, sessionId) {
3343
3498
  return Effect22.gen(function* () {
3344
- const index = yield* buildCursorIndex();
3499
+ const index = yield* buildCursorProjectIndex(nativeId);
3345
3500
  const session = index.composersById.get(sessionId.replace(COMPOSER_PREFIX_REGEX, "")) ?? index.agentsById.get(sessionId.replace(AGENT_PREFIX_REGEX, "")) ?? index.plansById.get(sessionId.replace(PLAN_PREFIX_REGEX, ""));
3346
3501
  if (!session || session.projectPath !== nativeId) {
3347
3502
  return yield* Effect22.fail(new Error(`Cursor session not found: ${sessionId}`));
@@ -3379,6 +3534,12 @@ var cursorPlugin = {
3379
3534
  message: String(err),
3380
3535
  cause: err
3381
3536
  })))),
3537
+ discoverIndex: buildCursorDiscoveryIndex().pipe(Effect23.catchAll((err) => Effect23.fail(new PluginError({
3538
+ pluginId: "cursor",
3539
+ operation: "discoverIndex",
3540
+ message: String(err),
3541
+ cause: err
3542
+ })))),
3382
3543
  listSessions: (nativeId) => listCursorSessions(nativeId).pipe(Effect23.catchAll((err) => Effect23.fail(new PluginError({
3383
3544
  pluginId: "cursor",
3384
3545
  operation: "listSessions",
@@ -3569,13 +3730,11 @@ function getProjects(registry) {
3569
3730
  }
3570
3731
  function getSessions(registry, params) {
3571
3732
  return Effect27.gen(function* () {
3572
- const projects = yield* registry.discoverAllProjects();
3573
- const project = projects.find((p) => p.encodedPath === params.encodedPath);
3574
- if (!project) {
3733
+ const discovered = yield* registry.discoverAllProjectsWithSessions();
3734
+ if (!discovered.projects.find((project) => project.encodedPath === params.encodedPath)) {
3575
3735
  return { sessions: [] };
3576
3736
  }
3577
- const sessions = yield* registry.listAllSessions(project);
3578
- return { sessions };
3737
+ return { sessions: discovered.sessionsByEncodedPath.get(params.encodedPath) ?? [] };
3579
3738
  });
3580
3739
  }
3581
3740
  function getSession(registry, params) {
@@ -3633,10 +3792,10 @@ function projectNameFromPath(fullPath) {
3633
3792
  }
3634
3793
  function searchSessions(registry) {
3635
3794
  return Effect27.gen(function* () {
3636
- const projects = yield* registry.discoverAllProjects();
3637
- const perProject = yield* Effect27.forEach(projects, (project) => registry.listAllSessions(project).pipe(Effect27.map((sessions) => ({ project, sessions }))), { concurrency: "unbounded" });
3795
+ const discovered = yield* registry.discoverAllProjectsWithSessions();
3638
3796
  const allSessions = [];
3639
- for (const { project, sessions } of perProject) {
3797
+ for (const project of discovered.projects) {
3798
+ const sessions = discovered.sessionsByEncodedPath.get(project.encodedPath) ?? [];
3640
3799
  const projectName = projectNameFromPath(project.name);
3641
3800
  for (const session of sessions) {
3642
3801
  allSessions.push({
@@ -3755,6 +3914,8 @@ function updateUpdateSettings(settingsPath, params) {
3755
3914
  }
3756
3915
 
3757
3916
  // ../../packages/server/src/services/stats-service.ts
3917
+ import { dirname as dirname2, join as join14 } from "node:path";
3918
+ import { FileSystem as FileSystem11 } from "@effect/platform";
3758
3919
  import { Effect as Effect30 } from "effect";
3759
3920
 
3760
3921
  // ../../packages/server/src/services/stats.ts
@@ -3820,11 +3981,11 @@ function countVisibleMessages(turns) {
3820
3981
  }
3821
3982
  function collectSessionsWithProjects(registry, stats) {
3822
3983
  return Effect29.gen(function* () {
3823
- const projects = yield* registry.discoverAllProjects();
3824
- stats.projects = projects.length;
3984
+ const discovered = yield* registry.discoverAllProjectsWithSessions();
3985
+ stats.projects = discovered.projects.length;
3825
3986
  const sessionsWithProject = [];
3826
- for (const project of projects) {
3827
- const sessions = yield* registry.listAllSessions(project);
3987
+ for (const project of discovered.projects) {
3988
+ const sessions = discovered.sessionsByEncodedPath.get(project.encodedPath) ?? [];
3828
3989
  stats.sessions += sessions.length;
3829
3990
  for (const session of sessions) {
3830
3991
  sessionsWithProject.push({ project, session });
@@ -3865,6 +4026,9 @@ function applyUsageStats(stats, modelUsage, usage) {
3865
4026
  modelUsage.cacheReadTokens += usage.cacheReadTokens ?? 0;
3866
4027
  modelUsage.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
3867
4028
  }
4029
+ function totalUsageTokens(usage) {
4030
+ return usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheCreationTokens ?? 0);
4031
+ }
3868
4032
  function applyTurnStats(stats, turns, fallbackModel) {
3869
4033
  stats.messages += countVisibleMessages(turns);
3870
4034
  for (const turn of turns) {
@@ -3872,10 +4036,10 @@ function applyTurnStats(stats, turns, fallbackModel) {
3872
4036
  continue;
3873
4037
  }
3874
4038
  stats.toolCalls += turn.contentBlocks.filter((block) => block.type === "tool_call").length;
3875
- const modelUsage = ensureModelUsage(stats.models, turn.model || fallbackModel || "unknown");
3876
- if (!turn.usage) {
4039
+ if (!turn.usage || totalUsageTokens(turn.usage) <= 0) {
3877
4040
  continue;
3878
4041
  }
4042
+ const modelUsage = ensureModelUsage(stats.models, turn.model || fallbackModel || "unknown");
3879
4043
  applyUsageStats(stats, modelUsage, turn.usage);
3880
4044
  }
3881
4045
  }
@@ -3899,10 +4063,163 @@ function scanStats(registry) {
3899
4063
  }
3900
4064
 
3901
4065
  // ../../packages/server/src/services/stats-service.ts
3902
- function getStats(registry) {
4066
+ var STATS_CACHE_FILENAME = "stats-cache.json";
4067
+ var refreshBootTimes = new Map;
4068
+ var refreshEpochs = new Map;
4069
+ var refreshingCachePaths = new Set;
4070
+ function getStatsCachePath(settingsPath) {
4071
+ return join14(dirname2(settingsPath), STATS_CACHE_FILENAME);
4072
+ }
4073
+ function getRefreshBootTime(cachePath) {
4074
+ const existing = refreshBootTimes.get(cachePath);
4075
+ if (existing) {
4076
+ return existing;
4077
+ }
4078
+ const bootTime = new Date().toISOString();
4079
+ refreshBootTimes.set(cachePath, bootTime);
4080
+ return bootTime;
4081
+ }
4082
+ function getRefreshEpoch(cachePath) {
4083
+ return refreshEpochs.get(cachePath) ?? 0;
4084
+ }
4085
+ function bumpRefreshEpoch(cachePath) {
4086
+ const nextEpoch = getRefreshEpoch(cachePath) + 1;
4087
+ refreshEpochs.set(cachePath, nextEpoch);
4088
+ return nextEpoch;
4089
+ }
4090
+ function isDashboardStats(value) {
4091
+ if (typeof value !== "object" || value === null) {
4092
+ return false;
4093
+ }
4094
+ const candidate = value;
4095
+ const requiredNumberFields = [
4096
+ "projects",
4097
+ "sessions",
4098
+ "messages",
4099
+ "todaySessions",
4100
+ "thisWeekSessions",
4101
+ "inputTokens",
4102
+ "outputTokens",
4103
+ "cacheReadTokens",
4104
+ "cacheCreationTokens",
4105
+ "toolCalls"
4106
+ ];
4107
+ for (const field of requiredNumberFields) {
4108
+ if (typeof candidate[field] !== "number") {
4109
+ return false;
4110
+ }
4111
+ }
4112
+ return typeof candidate["models"] === "object" && candidate["models"] !== null;
4113
+ }
4114
+ function isStatsCacheFile(value) {
4115
+ if (typeof value !== "object" || value === null) {
4116
+ return false;
4117
+ }
4118
+ const candidate = value;
4119
+ return candidate["version"] === 1 && typeof candidate["cachedAt"] === "string" && isDashboardStats(candidate["stats"]);
4120
+ }
4121
+ function loadStatsCache(settingsPath) {
4122
+ return Effect30.gen(function* () {
4123
+ const fs = yield* FileSystem11.FileSystem;
4124
+ const cachePath = getStatsCachePath(settingsPath);
4125
+ const raw = yield* fs.readFileString(cachePath).pipe(Effect30.catchAll(() => Effect30.succeed(null)));
4126
+ if (raw === null) {
4127
+ return null;
4128
+ }
4129
+ const parsed = yield* Effect30.try({
4130
+ try: () => JSON.parse(raw),
4131
+ catch: () => null
4132
+ }).pipe(Effect30.catchAll(() => Effect30.succeed(null)));
4133
+ return parsed !== null && isStatsCacheFile(parsed) ? parsed : null;
4134
+ });
4135
+ }
4136
+ function persistStatsCache(settingsPath, stats, expectedEpoch) {
4137
+ return Effect30.gen(function* () {
4138
+ const fs = yield* FileSystem11.FileSystem;
4139
+ const cachePath = getStatsCachePath(settingsPath);
4140
+ const cacheDir = dirname2(cachePath);
4141
+ const cachedAt = new Date().toISOString();
4142
+ yield* fs.makeDirectory(cacheDir, { recursive: true }).pipe(Effect30.catchAll(() => Effect30.void));
4143
+ if (getRefreshEpoch(cachePath) !== expectedEpoch) {
4144
+ return null;
4145
+ }
4146
+ const tmpPath = join14(cacheDir, `.stats-cache-${Date.now()}.tmp`);
4147
+ const payload = {
4148
+ version: 1,
4149
+ cachedAt,
4150
+ stats
4151
+ };
4152
+ const wroteTempFile = yield* fs.writeFileString(tmpPath, JSON.stringify(payload, null, 2)).pipe(Effect30.map(() => true), Effect30.catchAll(() => Effect30.succeed(false)));
4153
+ if (!wroteTempFile) {
4154
+ return null;
4155
+ }
4156
+ if (getRefreshEpoch(cachePath) !== expectedEpoch) {
4157
+ yield* fs.remove(tmpPath).pipe(Effect30.catchAll(() => Effect30.void));
4158
+ return null;
4159
+ }
4160
+ const renamed = yield* fs.rename(tmpPath, cachePath).pipe(Effect30.map(() => true), Effect30.catchAll(() => fs.remove(tmpPath).pipe(Effect30.catchAll(() => Effect30.void), Effect30.map(() => false))));
4161
+ if (!renamed) {
4162
+ return null;
4163
+ }
4164
+ return getRefreshEpoch(cachePath) === expectedEpoch ? cachedAt : null;
4165
+ });
4166
+ }
4167
+ function computeAndPersistStats(settingsPath, registry, expectedEpoch) {
3903
4168
  return Effect30.gen(function* () {
3904
4169
  const stats = yield* scanStats(registry);
3905
- return { stats };
4170
+ const cachedAt = yield* persistStatsCache(settingsPath, stats, expectedEpoch);
4171
+ return { stats, ...cachedAt ? { cachedAt } : {} };
4172
+ });
4173
+ }
4174
+ function refreshStats(settingsPath, registry) {
4175
+ return Effect30.gen(function* () {
4176
+ const cachePath = getStatsCachePath(settingsPath);
4177
+ const expectedEpoch = getRefreshEpoch(cachePath);
4178
+ refreshingCachePaths.add(cachePath);
4179
+ const result = yield* computeAndPersistStats(settingsPath, registry, expectedEpoch);
4180
+ return { ...result, refreshing: false };
4181
+ }).pipe(Effect30.ensuring(Effect30.sync(() => {
4182
+ refreshingCachePaths.delete(getStatsCachePath(settingsPath));
4183
+ })));
4184
+ }
4185
+ function scheduleRefresh(settingsPath, registry) {
4186
+ return Effect30.gen(function* () {
4187
+ const cachePath = getStatsCachePath(settingsPath);
4188
+ if (refreshingCachePaths.has(cachePath)) {
4189
+ return;
4190
+ }
4191
+ const expectedEpoch = getRefreshEpoch(cachePath);
4192
+ refreshingCachePaths.add(cachePath);
4193
+ yield* computeAndPersistStats(settingsPath, registry, expectedEpoch).pipe(Effect30.catchAllCause(() => Effect30.void), Effect30.ensuring(Effect30.sync(() => {
4194
+ refreshingCachePaths.delete(cachePath);
4195
+ })), Effect30.forkDaemon);
4196
+ });
4197
+ }
4198
+ function getStats(settingsPath, registry) {
4199
+ return Effect30.gen(function* () {
4200
+ const cachePath = getStatsCachePath(settingsPath);
4201
+ const cached = yield* loadStatsCache(settingsPath);
4202
+ if (!cached) {
4203
+ return yield* refreshStats(settingsPath, registry);
4204
+ }
4205
+ const bootTime = getRefreshBootTime(cachePath);
4206
+ const shouldRefresh = cached.cachedAt < bootTime;
4207
+ if (shouldRefresh) {
4208
+ yield* scheduleRefresh(settingsPath, registry);
4209
+ }
4210
+ return {
4211
+ stats: cached.stats,
4212
+ cachedAt: cached.cachedAt,
4213
+ refreshing: shouldRefresh || refreshingCachePaths.has(cachePath)
4214
+ };
4215
+ });
4216
+ }
4217
+ function invalidateStatsCache(settingsPath) {
4218
+ return Effect30.gen(function* () {
4219
+ const fs = yield* FileSystem11.FileSystem;
4220
+ const cachePath = getStatsCachePath(settingsPath);
4221
+ bumpRefreshEpoch(cachePath);
4222
+ yield* fs.remove(cachePath).pipe(Effect30.catchAll(() => Effect30.void));
3906
4223
  });
3907
4224
  }
3908
4225
 
@@ -3931,7 +4248,7 @@ var KloviServicesLive = Layer6.effect(KloviServices, Effect31.gen(function* () {
3931
4248
  return {
3932
4249
  acceptRisks: () => completeOnboarding(settingsPath),
3933
4250
  getVersion: () => Effect31.succeed(getVersion(versionState)),
3934
- getStats: () => getStats(registry),
4251
+ getStats: () => getStats(settingsPath, registry),
3935
4252
  getProjects: () => getProjects(registry),
3936
4253
  getSessions: (params) => getSessions(registry, params),
3937
4254
  getSession: (params) => getSession(registry, params),
@@ -3941,6 +4258,7 @@ var KloviServicesLive = Layer6.effect(KloviServices, Effect31.gen(function* () {
3941
4258
  updatePluginSetting: (params) => Effect31.gen(function* () {
3942
4259
  const result = yield* updatePluginSetting(settingsPath, params);
3943
4260
  yield* refreshRegistry();
4261
+ yield* invalidateStatsCache(settingsPath);
3944
4262
  return result;
3945
4263
  }),
3946
4264
  getGeneralSettings: () => getGeneralSettings(settingsPath),
@@ -3949,6 +4267,7 @@ var KloviServicesLive = Layer6.effect(KloviServices, Effect31.gen(function* () {
3949
4267
  resetSettings: () => Effect31.gen(function* () {
3950
4268
  const result = yield* resetSettings(settingsPath);
3951
4269
  yield* refreshRegistry();
4270
+ yield* invalidateStatsCache(settingsPath);
3952
4271
  return result;
3953
4272
  }),
3954
4273
  getUpdateSettings: () => getUpdateSettings(settingsPath),
@@ -3961,7 +4280,7 @@ var KloviServicesLive = Layer6.effect(KloviServices, Effect31.gen(function* () {
3961
4280
  // ../../packages/server/src/effect/bootstrap.ts
3962
4281
  function getDefaultSettingsPath() {
3963
4282
  const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
3964
- return join14(home, ".klovi", "settings.json");
4283
+ return join15(home, ".klovi", "settings.json");
3965
4284
  }
3966
4285
  function detectRuntime(requested = "auto") {
3967
4286
  if (requested !== "auto") {
@@ -4100,11 +4419,16 @@ import { Effect as Effect35 } from "effect";
4100
4419
  // src/static-handler.ts
4101
4420
  import { HttpServerRequest as HttpServerRequest2, HttpServerResponse as HttpServerResponse2 } from "@effect/platform";
4102
4421
  import { Effect as Effect34 } from "effect";
4422
+ var notFound = HttpServerResponse2.unsafeJson({ error: "Not found" }, { status: 404 });
4423
+ var isNavigationRequest = (pathname) => {
4424
+ const lastSegment2 = pathname.split("/").pop() ?? "";
4425
+ return !lastSegment2.includes(".");
4426
+ };
4103
4427
  var makeStaticHandler = (staticDir) => Effect34.gen(function* () {
4104
4428
  const req = yield* HttpServerRequest2.HttpServerRequest;
4105
4429
  const url = new URL(req.url, "http://localhost");
4106
4430
  const filePath = url.pathname === "/" ? "/index.html" : url.pathname;
4107
- return yield* HttpServerResponse2.file(`${staticDir}${filePath}`).pipe(Effect34.orElse(() => HttpServerResponse2.file(`${staticDir}/index.html`)), Effect34.orElse(() => Effect34.succeed(HttpServerResponse2.unsafeJson({ error: "Not found" }, { status: 404 }))));
4431
+ return yield* HttpServerResponse2.file(`${staticDir}${filePath}`).pipe(Effect34.orElse(() => isNavigationRequest(url.pathname) ? HttpServerResponse2.file(`${staticDir}/index.html`).pipe(Effect34.orElse(() => Effect34.succeed(notFound))) : Effect34.succeed(notFound)));
4108
4432
  });
4109
4433
 
4110
4434
  // src/http-app.ts