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