@claude-sessions/core 0.3.6 → 0.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/index.js CHANGED
@@ -24,7 +24,7 @@ var createLogger = (namespace) => ({
24
24
 
25
25
  // src/paths.ts
26
26
  var log = createLogger("paths");
27
- var getSessionsDir = () => path.join(os.homedir(), ".claude", "projects");
27
+ var getSessionsDir = () => process.env.CLAUDE_SESSIONS_DIR || path.join(os.homedir(), ".claude", "projects");
28
28
  var getTodosDir = () => path.join(os.homedir(), ".claude", "todos");
29
29
  var extractCwdFromContent = (content) => {
30
30
  const lines = content.split("\n").filter((l) => l.trim());
@@ -80,22 +80,22 @@ var pathToFolderName = (absolutePath) => {
80
80
  return convertNonAscii(absolutePath).replace(/^\//g, "-").replace(/\/\./g, "--").replace(/\//g, "-").replace(/\./g, "-");
81
81
  };
82
82
  var tryGetCwdFromFile = (filePath, fileSystem = fs, logger2 = log) => {
83
- const basename2 = path.basename(filePath);
83
+ const basename3 = path.basename(filePath);
84
84
  try {
85
85
  const content = fileSystem.readFileSync(filePath, "utf-8");
86
86
  const cwd = extractCwdFromContent(content);
87
87
  if (cwd === null) {
88
88
  const lines = content.split("\n").filter((l) => l.trim());
89
89
  if (lines.length === 0) {
90
- logger2.debug(`tryGetCwdFromFile: ${basename2} -> empty file`);
90
+ logger2.debug(`tryGetCwdFromFile: ${basename3} -> empty file`);
91
91
  } else {
92
- logger2.debug(`tryGetCwdFromFile: ${basename2} -> no cwd found in ${lines.length} lines`);
92
+ logger2.debug(`tryGetCwdFromFile: ${basename3} -> no cwd found in ${lines.length} lines`);
93
93
  }
94
94
  return null;
95
95
  }
96
96
  return cwd;
97
97
  } catch (e) {
98
- logger2.warn(`tryGetCwdFromFile: ${basename2} -> read error: ${e}`);
98
+ logger2.warn(`tryGetCwdFromFile: ${basename3} -> read error: ${e}`);
99
99
  return null;
100
100
  }
101
101
  };
@@ -254,6 +254,9 @@ var maskHomePath = (text, homeDir) => {
254
254
  const regex = new RegExp(`${escapedHome}(?=[/\\\\]|$)`, "g");
255
255
  return text.replace(regex, "~");
256
256
  };
257
+ var getSessionSortTimestamp = (session) => {
258
+ return session.summaries?.[0]?.timestamp ?? session.createdAt;
259
+ };
257
260
  var sortProjects = (projects, options = {}) => {
258
261
  const { currentProjectName, homeDir, filterEmpty = true } = options;
259
262
  const filtered = filterEmpty ? projects.filter((p) => p.sessionCount > 0) : projects;
@@ -298,46 +301,127 @@ var findLinkedAgents = (projectName, sessionId) => Effect.gen(function* () {
298
301
  }
299
302
  return linkedAgents;
300
303
  });
301
- var findOrphanAgents = (projectName) => Effect.gen(function* () {
304
+ var findOrphanAgentsWithPaths = (projectName) => Effect.gen(function* () {
302
305
  const projectPath = path2.join(getSessionsDir(), projectName);
303
306
  const files = yield* Effect.tryPromise(() => fs2.readdir(projectPath));
304
307
  const sessionIds = new Set(
305
308
  files.filter((f) => !f.startsWith("agent-") && f.endsWith(".jsonl")).map((f) => f.replace(".jsonl", ""))
306
309
  );
307
- const agentFiles = files.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
308
310
  const orphanAgents = [];
309
- for (const agentFile of agentFiles) {
311
+ const checkAgentFile = async (filePath) => {
312
+ try {
313
+ const content = await fs2.readFile(filePath, "utf-8");
314
+ const lines = content.split("\n").filter((l) => l.trim());
315
+ const firstLine = lines[0];
316
+ if (!firstLine) return null;
317
+ const parsed = JSON.parse(firstLine);
318
+ if (parsed.sessionId && !sessionIds.has(parsed.sessionId)) {
319
+ const fileName = path2.basename(filePath);
320
+ return {
321
+ agentId: fileName.replace(".jsonl", ""),
322
+ sessionId: parsed.sessionId,
323
+ lineCount: lines.length
324
+ };
325
+ }
326
+ } catch {
327
+ }
328
+ return null;
329
+ };
330
+ const rootAgentFiles = files.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
331
+ for (const agentFile of rootAgentFiles) {
310
332
  const filePath = path2.join(projectPath, agentFile);
311
- const content = yield* Effect.tryPromise(() => fs2.readFile(filePath, "utf-8"));
312
- const firstLine = content.split("\n")[0];
313
- if (firstLine) {
314
- try {
315
- const parsed = JSON.parse(firstLine);
316
- if (parsed.sessionId && !sessionIds.has(parsed.sessionId)) {
317
- orphanAgents.push({
318
- agentId: agentFile.replace(".jsonl", ""),
319
- sessionId: parsed.sessionId
320
- });
333
+ const orphan = yield* Effect.tryPromise(() => checkAgentFile(filePath));
334
+ if (orphan) {
335
+ orphanAgents.push({ ...orphan, filePath });
336
+ }
337
+ }
338
+ for (const entry of files) {
339
+ const entryPath = path2.join(projectPath, entry);
340
+ const stat4 = yield* Effect.tryPromise(() => fs2.stat(entryPath).catch(() => null));
341
+ if (stat4?.isDirectory() && !entry.startsWith(".")) {
342
+ const subagentsPath = path2.join(entryPath, "subagents");
343
+ const subagentsExists = yield* Effect.tryPromise(
344
+ () => fs2.stat(subagentsPath).then(() => true).catch(() => false)
345
+ );
346
+ if (subagentsExists) {
347
+ const subagentFiles = yield* Effect.tryPromise(
348
+ () => fs2.readdir(subagentsPath).catch(() => [])
349
+ );
350
+ for (const subagentFile of subagentFiles) {
351
+ if (subagentFile.startsWith("agent-") && subagentFile.endsWith(".jsonl")) {
352
+ const filePath = path2.join(subagentsPath, subagentFile);
353
+ const orphan = yield* Effect.tryPromise(() => checkAgentFile(filePath));
354
+ if (orphan) {
355
+ orphanAgents.push({ ...orphan, filePath });
356
+ }
357
+ }
321
358
  }
322
- } catch {
323
359
  }
324
360
  }
325
361
  }
326
362
  return orphanAgents;
327
363
  });
364
+ var findOrphanAgents = (projectName) => Effect.gen(function* () {
365
+ const orphans = yield* findOrphanAgentsWithPaths(projectName);
366
+ return orphans.map(({ agentId, sessionId }) => ({ agentId, sessionId }));
367
+ });
328
368
  var deleteOrphanAgents = (projectName) => Effect.gen(function* () {
329
369
  const projectPath = path2.join(getSessionsDir(), projectName);
330
- const orphans = yield* findOrphanAgents(projectName);
331
- const backupDir = path2.join(projectPath, ".bak");
332
- yield* Effect.tryPromise(() => fs2.mkdir(backupDir, { recursive: true }));
370
+ const orphans = yield* findOrphanAgentsWithPaths(projectName);
333
371
  const deletedAgents = [];
372
+ const backedUpAgents = [];
373
+ const cleanedFolders = [];
374
+ let backupDirCreated = false;
375
+ const foldersToCheck = /* @__PURE__ */ new Set();
334
376
  for (const orphan of orphans) {
335
- const agentPath = path2.join(projectPath, `${orphan.agentId}.jsonl`);
336
- const agentBackupPath = path2.join(backupDir, `${orphan.agentId}.jsonl`);
337
- yield* Effect.tryPromise(() => fs2.rename(agentPath, agentBackupPath));
338
- deletedAgents.push(orphan.agentId);
377
+ const parentDir = path2.dirname(orphan.filePath);
378
+ if (parentDir.endsWith("/subagents") || parentDir.endsWith("\\subagents")) {
379
+ foldersToCheck.add(parentDir);
380
+ }
381
+ if (orphan.lineCount <= 2) {
382
+ yield* Effect.tryPromise(() => fs2.unlink(orphan.filePath));
383
+ deletedAgents.push(orphan.agentId);
384
+ } else {
385
+ if (!backupDirCreated) {
386
+ const backupDir2 = path2.join(projectPath, ".bak");
387
+ yield* Effect.tryPromise(() => fs2.mkdir(backupDir2, { recursive: true }));
388
+ backupDirCreated = true;
389
+ }
390
+ const backupDir = path2.join(projectPath, ".bak");
391
+ const agentBackupPath = path2.join(backupDir, `${orphan.agentId}.jsonl`);
392
+ yield* Effect.tryPromise(() => fs2.rename(orphan.filePath, agentBackupPath));
393
+ backedUpAgents.push(orphan.agentId);
394
+ }
339
395
  }
340
- return { success: true, deletedAgents, count: deletedAgents.length };
396
+ for (const subagentsDir of foldersToCheck) {
397
+ const isEmpty = yield* Effect.tryPromise(async () => {
398
+ const files = await fs2.readdir(subagentsDir);
399
+ return files.length === 0;
400
+ });
401
+ if (isEmpty) {
402
+ yield* Effect.tryPromise(() => fs2.rmdir(subagentsDir));
403
+ cleanedFolders.push(subagentsDir);
404
+ const sessionDir = path2.dirname(subagentsDir);
405
+ const sessionDirEmpty = yield* Effect.tryPromise(async () => {
406
+ const files = await fs2.readdir(sessionDir);
407
+ return files.length === 0;
408
+ });
409
+ if (sessionDirEmpty) {
410
+ yield* Effect.tryPromise(() => fs2.rmdir(sessionDir));
411
+ cleanedFolders.push(sessionDir);
412
+ }
413
+ }
414
+ }
415
+ return {
416
+ success: true,
417
+ deletedAgents,
418
+ backedUpAgents,
419
+ cleanedFolders,
420
+ deletedCount: deletedAgents.length,
421
+ backedUpCount: backedUpAgents.length,
422
+ cleanedFolderCount: cleanedFolders.length,
423
+ count: deletedAgents.length + backedUpAgents.length
424
+ };
341
425
  });
342
426
  var loadAgentMessages = (projectName, _sessionId, agentId) => Effect.gen(function* () {
343
427
  const projectPath = path2.join(getSessionsDir(), projectName);
@@ -554,8 +638,8 @@ var deleteOrphanTodos = () => Effect2.gen(function* () {
554
638
  return { success: true, deletedCount };
555
639
  });
556
640
 
557
- // src/session.ts
558
- import { Effect as Effect3, pipe, Array as A, Option as O } from "effect";
641
+ // src/session/projects.ts
642
+ import { Effect as Effect3 } from "effect";
559
643
  import * as fs4 from "fs/promises";
560
644
  import * as path4 from "path";
561
645
  var listProjects = Effect3.gen(function* () {
@@ -585,15 +669,42 @@ var listProjects = Effect3.gen(function* () {
585
669
  );
586
670
  return projects;
587
671
  });
588
- var listSessions = (projectName) => Effect3.gen(function* () {
589
- const projectPath = path4.join(getSessionsDir(), projectName);
590
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
672
+
673
+ // src/session/crud.ts
674
+ import { Effect as Effect4, pipe, Array as A, Option as O } from "effect";
675
+ import * as fs5 from "fs/promises";
676
+ import * as path5 from "path";
677
+ import * as crypto from "crypto";
678
+ var updateSessionSummary = (projectName, sessionId, newSummary) => Effect4.gen(function* () {
679
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
680
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
681
+ const lines = content.trim().split("\n").filter(Boolean);
682
+ const messages = lines.map((line) => JSON.parse(line));
683
+ const summaryIdx = messages.findIndex((m) => m.type === "summary");
684
+ if (summaryIdx >= 0) {
685
+ messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
686
+ } else {
687
+ const firstUserMsg = messages.find((m) => m.type === "user");
688
+ const summaryMsg = {
689
+ type: "summary",
690
+ summary: newSummary,
691
+ leafUuid: firstUserMsg?.uuid ?? null
692
+ };
693
+ messages.unshift(summaryMsg);
694
+ }
695
+ const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
696
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
697
+ return { success: true };
698
+ });
699
+ var listSessions = (projectName) => Effect4.gen(function* () {
700
+ const projectPath = path5.join(getSessionsDir(), projectName);
701
+ const files = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
591
702
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
592
- const sessions = yield* Effect3.all(
703
+ const sessions = yield* Effect4.all(
593
704
  sessionFiles.map(
594
- (file) => Effect3.gen(function* () {
595
- const filePath = path4.join(projectPath, file);
596
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
705
+ (file) => Effect4.gen(function* () {
706
+ const filePath = path5.join(projectPath, file);
707
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
597
708
  const lines = content.trim().split("\n").filter(Boolean);
598
709
  const messages = lines.map((line) => JSON.parse(line));
599
710
  const sessionId = file.replace(".jsonl", "");
@@ -612,10 +723,25 @@ var listSessions = (projectName) => Effect3.gen(function* () {
612
723
  }),
613
724
  O.getOrElse(() => hasSummary ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`)
614
725
  );
726
+ const currentSummary = pipe(
727
+ messages,
728
+ A.findFirst((m) => m.type === "summary"),
729
+ O.map((m) => m.summary),
730
+ O.getOrUndefined
731
+ );
732
+ const customTitle = pipe(
733
+ messages,
734
+ A.findFirst((m) => m.type === "custom-title"),
735
+ O.map((m) => m.customTitle),
736
+ O.flatMap(O.fromNullable),
737
+ O.getOrUndefined
738
+ );
615
739
  return {
616
740
  id: sessionId,
617
741
  projectName,
618
742
  title,
743
+ customTitle,
744
+ currentSummary,
619
745
  // If session has summary but no user/assistant messages, count as 1
620
746
  messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : hasSummary ? 1 : 0,
621
747
  createdAt: firstMessage?.timestamp,
@@ -631,15 +757,15 @@ var listSessions = (projectName) => Effect3.gen(function* () {
631
757
  return dateB - dateA;
632
758
  });
633
759
  });
634
- var readSession = (projectName, sessionId) => Effect3.gen(function* () {
635
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
636
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
760
+ var readSession = (projectName, sessionId) => Effect4.gen(function* () {
761
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
762
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
637
763
  const lines = content.trim().split("\n").filter(Boolean);
638
764
  return lines.map((line) => JSON.parse(line));
639
765
  });
640
- var deleteMessage = (projectName, sessionId, messageUuid) => Effect3.gen(function* () {
641
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
642
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
766
+ var deleteMessage = (projectName, sessionId, messageUuid) => Effect4.gen(function* () {
767
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
768
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
643
769
  const lines = content.trim().split("\n").filter(Boolean);
644
770
  const messages = lines.map((line) => JSON.parse(line));
645
771
  const targetIndex = messages.findIndex(
@@ -658,12 +784,12 @@ var deleteMessage = (projectName, sessionId, messageUuid) => Effect3.gen(functio
658
784
  }
659
785
  messages.splice(targetIndex, 1);
660
786
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
661
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
787
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
662
788
  return { success: true, deletedMessage: deletedMsg };
663
789
  });
664
- var restoreMessage = (projectName, sessionId, message, index) => Effect3.gen(function* () {
665
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
666
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
790
+ var restoreMessage = (projectName, sessionId, message, index) => Effect4.gen(function* () {
791
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
792
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
667
793
  const lines = content.trim().split("\n").filter(Boolean);
668
794
  const messages = lines.map((line) => JSON.parse(line));
669
795
  const msgUuid = message.uuid ?? message.messageId;
@@ -680,41 +806,41 @@ var restoreMessage = (projectName, sessionId, message, index) => Effect3.gen(fun
680
806
  const insertIndex = Math.min(index, messages.length);
681
807
  messages.splice(insertIndex, 0, message);
682
808
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
683
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
809
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
684
810
  return { success: true };
685
811
  });
686
- var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
812
+ var deleteSession = (projectName, sessionId) => Effect4.gen(function* () {
687
813
  const sessionsDir = getSessionsDir();
688
- const projectPath = path4.join(sessionsDir, projectName);
689
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
814
+ const projectPath = path5.join(sessionsDir, projectName);
815
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
690
816
  const linkedAgents = yield* findLinkedAgents(projectName, sessionId);
691
- const stat2 = yield* Effect3.tryPromise(() => fs4.stat(filePath));
692
- if (stat2.size === 0) {
693
- yield* Effect3.tryPromise(() => fs4.unlink(filePath));
694
- const agentBackupDir2 = path4.join(projectPath, ".bak");
695
- yield* Effect3.tryPromise(() => fs4.mkdir(agentBackupDir2, { recursive: true }));
817
+ const stat4 = yield* Effect4.tryPromise(() => fs5.stat(filePath));
818
+ if (stat4.size === 0) {
819
+ yield* Effect4.tryPromise(() => fs5.unlink(filePath));
820
+ const agentBackupDir2 = path5.join(projectPath, ".bak");
821
+ yield* Effect4.tryPromise(() => fs5.mkdir(agentBackupDir2, { recursive: true }));
696
822
  for (const agentId of linkedAgents) {
697
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
698
- const agentBackupPath = path4.join(agentBackupDir2, `${agentId}.jsonl`);
699
- yield* Effect3.tryPromise(() => fs4.rename(agentPath, agentBackupPath).catch(() => {
823
+ const agentPath = path5.join(projectPath, `${agentId}.jsonl`);
824
+ const agentBackupPath = path5.join(agentBackupDir2, `${agentId}.jsonl`);
825
+ yield* Effect4.tryPromise(() => fs5.rename(agentPath, agentBackupPath).catch(() => {
700
826
  }));
701
827
  }
702
828
  yield* deleteLinkedTodos(sessionId, linkedAgents);
703
829
  return { success: true, deletedAgents: linkedAgents.length };
704
830
  }
705
- const backupDir = path4.join(sessionsDir, ".bak");
706
- yield* Effect3.tryPromise(() => fs4.mkdir(backupDir, { recursive: true }));
707
- const agentBackupDir = path4.join(projectPath, ".bak");
708
- yield* Effect3.tryPromise(() => fs4.mkdir(agentBackupDir, { recursive: true }));
831
+ const backupDir = path5.join(sessionsDir, ".bak");
832
+ yield* Effect4.tryPromise(() => fs5.mkdir(backupDir, { recursive: true }));
833
+ const agentBackupDir = path5.join(projectPath, ".bak");
834
+ yield* Effect4.tryPromise(() => fs5.mkdir(agentBackupDir, { recursive: true }));
709
835
  for (const agentId of linkedAgents) {
710
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
711
- const agentBackupPath = path4.join(agentBackupDir, `${agentId}.jsonl`);
712
- yield* Effect3.tryPromise(() => fs4.rename(agentPath, agentBackupPath).catch(() => {
836
+ const agentPath = path5.join(projectPath, `${agentId}.jsonl`);
837
+ const agentBackupPath = path5.join(agentBackupDir, `${agentId}.jsonl`);
838
+ yield* Effect4.tryPromise(() => fs5.rename(agentPath, agentBackupPath).catch(() => {
713
839
  }));
714
840
  }
715
841
  const todosResult = yield* deleteLinkedTodos(sessionId, linkedAgents);
716
- const backupPath = path4.join(backupDir, `${projectName}_${sessionId}.jsonl`);
717
- yield* Effect3.tryPromise(() => fs4.rename(filePath, backupPath));
842
+ const backupPath = path5.join(backupDir, `${projectName}_${sessionId}.jsonl`);
843
+ yield* Effect4.tryPromise(() => fs5.rename(filePath, backupPath));
718
844
  return {
719
845
  success: true,
720
846
  backupPath,
@@ -722,10 +848,10 @@ var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
722
848
  deletedTodos: todosResult.deletedCount
723
849
  };
724
850
  });
725
- var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function* () {
726
- const projectPath = path4.join(getSessionsDir(), projectName);
727
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
728
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
851
+ var renameSession = (projectName, sessionId, newTitle) => Effect4.gen(function* () {
852
+ const projectPath = path5.join(getSessionsDir(), projectName);
853
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
854
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
729
855
  const lines = content.trim().split("\n").filter(Boolean);
730
856
  if (lines.length === 0) {
731
857
  return { success: false, error: "Empty session" };
@@ -749,14 +875,14 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
749
875
  messages.unshift(customTitleRecord);
750
876
  }
751
877
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
752
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
753
- const projectFiles = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
878
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
879
+ const projectFiles = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
754
880
  const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
755
881
  const summariesTargetingThis = [];
756
882
  for (const file of allJsonlFiles) {
757
- const otherFilePath = path4.join(projectPath, file);
883
+ const otherFilePath = path5.join(projectPath, file);
758
884
  try {
759
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
885
+ const otherContent = yield* Effect4.tryPromise(() => fs5.readFile(otherFilePath, "utf-8"));
760
886
  const otherLines = otherContent.trim().split("\n").filter(Boolean);
761
887
  const otherMessages = otherLines.map((l) => JSON.parse(l));
762
888
  for (let i = 0; i < otherMessages.length; i++) {
@@ -776,8 +902,8 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
776
902
  if (summariesTargetingThis.length > 0) {
777
903
  summariesTargetingThis.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
778
904
  const firstSummary = summariesTargetingThis[0];
779
- const summaryFilePath = path4.join(projectPath, firstSummary.file);
780
- const summaryContent = yield* Effect3.tryPromise(() => fs4.readFile(summaryFilePath, "utf-8"));
905
+ const summaryFilePath = path5.join(projectPath, firstSummary.file);
906
+ const summaryContent = yield* Effect4.tryPromise(() => fs5.readFile(summaryFilePath, "utf-8"));
781
907
  const summaryLines = summaryContent.trim().split("\n").filter(Boolean);
782
908
  const summaryMessages = summaryLines.map((l) => JSON.parse(l));
783
909
  summaryMessages[firstSummary.idx] = {
@@ -785,9 +911,9 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
785
911
  summary: newTitle
786
912
  };
787
913
  const newSummaryContent = summaryMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
788
- yield* Effect3.tryPromise(() => fs4.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
914
+ yield* Effect4.tryPromise(() => fs5.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
789
915
  } else {
790
- const currentContent = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
916
+ const currentContent = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
791
917
  const currentLines = currentContent.trim().split("\n").filter(Boolean);
792
918
  const currentMessages = currentLines.map((l) => JSON.parse(l));
793
919
  const firstUserIdx = currentMessages.findIndex((m) => m.type === "user");
@@ -806,102 +932,50 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
806
932
 
807
933
  ${cleanedText}`;
808
934
  const updatedContent = currentMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
809
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, updatedContent, "utf-8"));
935
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, updatedContent, "utf-8"));
810
936
  }
811
937
  }
812
938
  }
813
939
  }
814
940
  return { success: true };
815
941
  });
816
- var getSessionFiles = (projectName, sessionId) => Effect3.gen(function* () {
817
- const messages = yield* readSession(projectName, sessionId);
818
- const fileChanges = [];
819
- const seenFiles = /* @__PURE__ */ new Set();
820
- for (const msg of messages) {
821
- if (msg.type === "file-history-snapshot") {
822
- const snapshot = msg;
823
- const backups = snapshot.snapshot?.trackedFileBackups;
824
- if (backups && typeof backups === "object") {
825
- for (const filePath of Object.keys(backups)) {
826
- if (!seenFiles.has(filePath)) {
827
- seenFiles.add(filePath);
828
- fileChanges.push({
829
- path: filePath,
830
- action: "modified",
831
- timestamp: snapshot.snapshot?.timestamp,
832
- messageUuid: snapshot.messageId ?? msg.uuid
833
- });
834
- }
835
- }
836
- }
837
- }
838
- if (msg.type === "assistant" && msg.message?.content) {
839
- const content = msg.message.content;
840
- if (Array.isArray(content)) {
841
- for (const item of content) {
842
- if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
843
- const toolUse = item;
844
- if ((toolUse.name === "Write" || toolUse.name === "Edit") && toolUse.input?.file_path) {
845
- const filePath = toolUse.input.file_path;
846
- if (!seenFiles.has(filePath)) {
847
- seenFiles.add(filePath);
848
- fileChanges.push({
849
- path: filePath,
850
- action: toolUse.name === "Write" ? "created" : "modified",
851
- timestamp: msg.timestamp,
852
- messageUuid: msg.uuid
853
- });
854
- }
855
- }
856
- }
857
- }
858
- }
859
- }
860
- }
861
- return {
862
- sessionId,
863
- projectName,
864
- files: fileChanges,
865
- totalChanges: fileChanges.length
866
- };
867
- });
868
- var moveSession = (sourceProject, sessionId, targetProject) => Effect3.gen(function* () {
942
+ var moveSession = (sourceProject, sessionId, targetProject) => Effect4.gen(function* () {
869
943
  const sessionsDir = getSessionsDir();
870
- const sourcePath = path4.join(sessionsDir, sourceProject);
871
- const targetPath = path4.join(sessionsDir, targetProject);
872
- const sourceFile = path4.join(sourcePath, `${sessionId}.jsonl`);
873
- const targetFile = path4.join(targetPath, `${sessionId}.jsonl`);
874
- const sourceExists = yield* Effect3.tryPromise(
875
- () => fs4.access(sourceFile).then(() => true).catch(() => false)
944
+ const sourcePath = path5.join(sessionsDir, sourceProject);
945
+ const targetPath = path5.join(sessionsDir, targetProject);
946
+ const sourceFile = path5.join(sourcePath, `${sessionId}.jsonl`);
947
+ const targetFile = path5.join(targetPath, `${sessionId}.jsonl`);
948
+ const sourceExists = yield* Effect4.tryPromise(
949
+ () => fs5.access(sourceFile).then(() => true).catch(() => false)
876
950
  );
877
951
  if (!sourceExists) {
878
952
  return { success: false, error: "Source session not found" };
879
953
  }
880
- const targetExists = yield* Effect3.tryPromise(
881
- () => fs4.access(targetFile).then(() => true).catch(() => false)
954
+ const targetExists = yield* Effect4.tryPromise(
955
+ () => fs5.access(targetFile).then(() => true).catch(() => false)
882
956
  );
883
957
  if (targetExists) {
884
958
  return { success: false, error: "Session already exists in target project" };
885
959
  }
886
- yield* Effect3.tryPromise(() => fs4.mkdir(targetPath, { recursive: true }));
960
+ yield* Effect4.tryPromise(() => fs5.mkdir(targetPath, { recursive: true }));
887
961
  const linkedAgents = yield* findLinkedAgents(sourceProject, sessionId);
888
- yield* Effect3.tryPromise(() => fs4.rename(sourceFile, targetFile));
962
+ yield* Effect4.tryPromise(() => fs5.rename(sourceFile, targetFile));
889
963
  for (const agentId of linkedAgents) {
890
- const sourceAgentFile = path4.join(sourcePath, `${agentId}.jsonl`);
891
- const targetAgentFile = path4.join(targetPath, `${agentId}.jsonl`);
892
- const agentExists = yield* Effect3.tryPromise(
893
- () => fs4.access(sourceAgentFile).then(() => true).catch(() => false)
964
+ const sourceAgentFile = path5.join(sourcePath, `${agentId}.jsonl`);
965
+ const targetAgentFile = path5.join(targetPath, `${agentId}.jsonl`);
966
+ const agentExists = yield* Effect4.tryPromise(
967
+ () => fs5.access(sourceAgentFile).then(() => true).catch(() => false)
894
968
  );
895
969
  if (agentExists) {
896
- yield* Effect3.tryPromise(() => fs4.rename(sourceAgentFile, targetAgentFile));
970
+ yield* Effect4.tryPromise(() => fs5.rename(sourceAgentFile, targetAgentFile));
897
971
  }
898
972
  }
899
973
  return { success: true };
900
974
  });
901
- var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(function* () {
902
- const projectPath = path4.join(getSessionsDir(), projectName);
903
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
904
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
975
+ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect4.gen(function* () {
976
+ const projectPath = path5.join(getSessionsDir(), projectName);
977
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
978
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
905
979
  const lines = content.trim().split("\n").filter(Boolean);
906
980
  const allMessages = lines.map((line) => JSON.parse(line));
907
981
  const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
@@ -949,15 +1023,15 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
949
1023
  updatedMovedMessages.unshift(clonedSummary);
950
1024
  }
951
1025
  const keptContent = keptMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
952
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, keptContent, "utf-8"));
953
- const newFilePath = path4.join(projectPath, `${newSessionId}.jsonl`);
1026
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, keptContent, "utf-8"));
1027
+ const newFilePath = path5.join(projectPath, `${newSessionId}.jsonl`);
954
1028
  const newContent = updatedMovedMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
955
- yield* Effect3.tryPromise(() => fs4.writeFile(newFilePath, newContent, "utf-8"));
956
- const agentFiles = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1029
+ yield* Effect4.tryPromise(() => fs5.writeFile(newFilePath, newContent, "utf-8"));
1030
+ const agentFiles = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
957
1031
  const agentJsonlFiles = agentFiles.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
958
1032
  for (const agentFile of agentJsonlFiles) {
959
- const agentPath = path4.join(projectPath, agentFile);
960
- const agentContent = yield* Effect3.tryPromise(() => fs4.readFile(agentPath, "utf-8"));
1033
+ const agentPath = path5.join(projectPath, agentFile);
1034
+ const agentContent = yield* Effect4.tryPromise(() => fs5.readFile(agentPath, "utf-8"));
961
1035
  const agentLines = agentContent.trim().split("\n").filter(Boolean);
962
1036
  if (agentLines.length === 0) continue;
963
1037
  const firstAgentMsg = JSON.parse(agentLines[0]);
@@ -972,7 +1046,7 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
972
1046
  return JSON.stringify({ ...msg, sessionId: newSessionId });
973
1047
  });
974
1048
  const updatedAgentContent = updatedAgentMessages.join("\n") + "\n";
975
- yield* Effect3.tryPromise(() => fs4.writeFile(agentPath, updatedAgentContent, "utf-8"));
1049
+ yield* Effect4.tryPromise(() => fs5.writeFile(agentPath, updatedAgentContent, "utf-8"));
976
1050
  }
977
1051
  }
978
1052
  }
@@ -984,96 +1058,692 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
984
1058
  duplicatedSummary: shouldDuplicate
985
1059
  };
986
1060
  });
987
- var cleanInvalidMessages = (projectName, sessionId) => Effect3.gen(function* () {
988
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
989
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1061
+
1062
+ // src/session/tree.ts
1063
+ import { Effect as Effect5 } from "effect";
1064
+ import * as fs6 from "fs/promises";
1065
+ import * as path6 from "path";
1066
+ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession, fileMtime) => Effect5.gen(function* () {
1067
+ const projectPath = path6.join(getSessionsDir(), projectName);
1068
+ const filePath = path6.join(projectPath, `${sessionId}.jsonl`);
1069
+ const content = yield* Effect5.tryPromise(() => fs6.readFile(filePath, "utf-8"));
990
1070
  const lines = content.trim().split("\n").filter(Boolean);
991
- if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
992
1071
  const messages = lines.map((line) => JSON.parse(line));
993
- const invalidIndices = [];
994
- messages.forEach((msg, idx) => {
995
- if (isInvalidApiKeyMessage(msg)) {
996
- invalidIndices.push(idx);
1072
+ let summaries;
1073
+ if (summariesByTargetSession) {
1074
+ summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort((a, b) => {
1075
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1076
+ if (timestampCmp !== 0) return timestampCmp;
1077
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1078
+ });
1079
+ } else {
1080
+ summaries = [];
1081
+ const sessionUuids = /* @__PURE__ */ new Set();
1082
+ for (const msg of messages) {
1083
+ if (msg.uuid && typeof msg.uuid === "string") {
1084
+ sessionUuids.add(msg.uuid);
1085
+ }
997
1086
  }
998
- });
999
- if (invalidIndices.length === 0) {
1000
- const userAssistantCount = messages.filter(
1001
- (m) => m.type === "user" || m.type === "assistant"
1002
- ).length;
1003
- const hasSummary2 = messages.some((m) => m.type === "summary");
1004
- const remainingCount2 = userAssistantCount > 0 ? userAssistantCount : hasSummary2 ? 1 : 0;
1005
- return { removedCount: 0, remainingCount: remainingCount2 };
1006
- }
1007
- const filtered = [];
1008
- let lastValidUuid = null;
1009
- for (let i = 0; i < messages.length; i++) {
1010
- if (invalidIndices.includes(i)) {
1011
- continue;
1087
+ const projectFiles = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1088
+ const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1089
+ for (const file of allJsonlFiles) {
1090
+ try {
1091
+ const otherFilePath = path6.join(projectPath, file);
1092
+ const otherContent = yield* Effect5.tryPromise(() => fs6.readFile(otherFilePath, "utf-8"));
1093
+ const otherLines = otherContent.trim().split("\n").filter(Boolean);
1094
+ for (const line of otherLines) {
1095
+ try {
1096
+ const msg = JSON.parse(line);
1097
+ if (msg.type === "summary" && typeof msg.summary === "string" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
1098
+ const targetMsg = messages.find((m) => m.uuid === msg.leafUuid);
1099
+ summaries.push({
1100
+ summary: msg.summary,
1101
+ leafUuid: msg.leafUuid,
1102
+ timestamp: targetMsg?.timestamp ?? msg.timestamp,
1103
+ sourceFile: file
1104
+ });
1105
+ }
1106
+ } catch {
1107
+ }
1108
+ }
1109
+ } catch {
1110
+ }
1012
1111
  }
1112
+ }
1113
+ summaries.sort((a, b) => {
1114
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1115
+ if (timestampCmp !== 0) return timestampCmp;
1116
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1117
+ });
1118
+ let lastCompactBoundaryUuid;
1119
+ for (let i = messages.length - 1; i >= 0; i--) {
1013
1120
  const msg = messages[i];
1014
- if (msg.parentUuid && invalidIndices.some((idx) => messages[idx]?.uuid === msg.parentUuid)) {
1015
- msg.parentUuid = lastValidUuid;
1121
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
1122
+ lastCompactBoundaryUuid = msg.uuid;
1123
+ break;
1016
1124
  }
1017
- filtered.push(msg);
1018
- lastValidUuid = msg.uuid;
1019
1125
  }
1020
- const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
1021
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1022
- const remainingUserAssistant = filtered.filter(
1126
+ const firstUserMsg = messages.find((m) => m.type === "user");
1127
+ const customTitleMsg = messages.find((m) => m.type === "custom-title");
1128
+ const customTitle = customTitleMsg?.customTitle;
1129
+ const title = firstUserMsg ? extractTitle(extractTextContent(firstUserMsg.message)) : summaries.length > 0 ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`;
1130
+ const userAssistantMessages = messages.filter(
1023
1131
  (m) => m.type === "user" || m.type === "assistant"
1024
- ).length;
1025
- const hasSummary = filtered.some((m) => m.type === "summary");
1026
- const remainingCount = remainingUserAssistant > 0 ? remainingUserAssistant : hasSummary ? 1 : 0;
1027
- return { removedCount: invalidIndices.length, remainingCount };
1028
- });
1029
- var previewCleanup = (projectName) => Effect3.gen(function* () {
1030
- const projects = yield* listProjects;
1031
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1032
- const orphanTodos = yield* findOrphanTodos();
1033
- const orphanTodoCount = orphanTodos.length;
1034
- const results = yield* Effect3.all(
1035
- targetProjects.map(
1036
- (project) => Effect3.gen(function* () {
1037
- const sessions = yield* listSessions(project.name);
1038
- const emptySessions = sessions.filter((s) => s.messageCount === 0);
1039
- const invalidSessions = sessions.filter(
1040
- (s) => s.title?.includes("Invalid API key") || s.title?.includes("API key")
1041
- );
1042
- let emptyWithTodosCount = 0;
1043
- for (const session of emptySessions) {
1044
- const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1045
- const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1046
- if (hasTodos) {
1047
- emptyWithTodosCount++;
1048
- }
1049
- }
1050
- const orphanAgents = yield* findOrphanAgents(project.name);
1051
- return {
1052
- project: project.name,
1053
- emptySessions,
1054
- invalidSessions,
1055
- emptyWithTodosCount,
1056
- orphanAgentCount: orphanAgents.length,
1057
- orphanTodoCount: 0
1058
- // Will set for first project only
1059
- };
1060
- })
1061
- ),
1062
- { concurrency: 5 }
1063
1132
  );
1064
- if (results.length > 0) {
1065
- results[0] = { ...results[0], orphanTodoCount };
1133
+ const firstMessage = userAssistantMessages[0];
1134
+ const lastMessage = userAssistantMessages[userAssistantMessages.length - 1];
1135
+ const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1136
+ const agents = [];
1137
+ for (const agentId of linkedAgentIds) {
1138
+ const agentPath = path6.join(projectPath, `${agentId}.jsonl`);
1139
+ try {
1140
+ const agentContent = yield* Effect5.tryPromise(() => fs6.readFile(agentPath, "utf-8"));
1141
+ const agentLines = agentContent.trim().split("\n").filter(Boolean);
1142
+ const agentMsgs = agentLines.map((l) => JSON.parse(l));
1143
+ const agentUserAssistant = agentMsgs.filter(
1144
+ (m) => m.type === "user" || m.type === "assistant"
1145
+ );
1146
+ let agentName;
1147
+ const firstAgentMsg = agentMsgs.find((m) => m.type === "user");
1148
+ if (firstAgentMsg) {
1149
+ const text = extractTextContent(firstAgentMsg.message);
1150
+ if (text) {
1151
+ agentName = extractTitle(text);
1152
+ }
1153
+ }
1154
+ agents.push({
1155
+ id: agentId,
1156
+ name: agentName,
1157
+ messageCount: agentUserAssistant.length
1158
+ });
1159
+ } catch {
1160
+ agents.push({
1161
+ id: agentId,
1162
+ messageCount: 0
1163
+ });
1164
+ }
1066
1165
  }
1067
- return results;
1068
- });
1069
- var clearSessions = (options) => Effect3.gen(function* () {
1070
- const {
1166
+ const todos = yield* findLinkedTodos(sessionId, linkedAgentIds);
1167
+ return {
1168
+ id: sessionId,
1071
1169
  projectName,
1072
- clearEmpty = true,
1073
- clearInvalid = true,
1074
- skipWithTodos = true,
1075
- clearOrphanAgents = false,
1076
- clearOrphanTodos = false
1170
+ title,
1171
+ customTitle,
1172
+ currentSummary: summaries[0]?.summary,
1173
+ messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : summaries.length > 0 ? 1 : 0,
1174
+ createdAt: firstMessage?.timestamp ?? void 0,
1175
+ updatedAt: lastMessage?.timestamp ?? void 0,
1176
+ fileMtime,
1177
+ summaries,
1178
+ agents,
1179
+ todos,
1180
+ lastCompactBoundaryUuid
1181
+ };
1182
+ });
1183
+ var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1184
+ var DEFAULT_SORT = { field: "summary", order: "desc" };
1185
+ var loadProjectTreeData = (projectName, sortOptions) => Effect5.gen(function* () {
1186
+ const project = (yield* listProjects).find((p) => p.name === projectName);
1187
+ if (!project) {
1188
+ return null;
1189
+ }
1190
+ const sort = sortOptions ?? DEFAULT_SORT;
1191
+ const projectPath = path6.join(getSessionsDir(), projectName);
1192
+ const files = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1193
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1194
+ const fileMtimes = /* @__PURE__ */ new Map();
1195
+ yield* Effect5.all(
1196
+ sessionFiles.map(
1197
+ (file) => Effect5.gen(function* () {
1198
+ const filePath = path6.join(projectPath, file);
1199
+ try {
1200
+ const stat4 = yield* Effect5.tryPromise(() => fs6.stat(filePath));
1201
+ fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
1202
+ } catch {
1203
+ }
1204
+ })
1205
+ ),
1206
+ { concurrency: 20 }
1207
+ );
1208
+ const globalUuidMap = /* @__PURE__ */ new Map();
1209
+ const allSummaries = [];
1210
+ const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1211
+ yield* Effect5.all(
1212
+ allJsonlFiles.map(
1213
+ (file) => Effect5.gen(function* () {
1214
+ const filePath = path6.join(projectPath, file);
1215
+ const fileSessionId = file.replace(".jsonl", "");
1216
+ try {
1217
+ const content = yield* Effect5.tryPromise(() => fs6.readFile(filePath, "utf-8"));
1218
+ const lines = content.trim().split("\n").filter(Boolean);
1219
+ for (const line of lines) {
1220
+ try {
1221
+ const msg = JSON.parse(line);
1222
+ if (msg.uuid && typeof msg.uuid === "string") {
1223
+ globalUuidMap.set(msg.uuid, {
1224
+ sessionId: fileSessionId,
1225
+ timestamp: msg.timestamp
1226
+ });
1227
+ }
1228
+ if (msg.messageId && typeof msg.messageId === "string") {
1229
+ globalUuidMap.set(msg.messageId, {
1230
+ sessionId: fileSessionId,
1231
+ timestamp: msg.snapshot?.timestamp
1232
+ });
1233
+ }
1234
+ if (msg.type === "summary" && typeof msg.summary === "string") {
1235
+ allSummaries.push({
1236
+ summary: msg.summary,
1237
+ leafUuid: msg.leafUuid,
1238
+ timestamp: msg.timestamp,
1239
+ sourceFile: file
1240
+ });
1241
+ }
1242
+ } catch {
1243
+ }
1244
+ }
1245
+ } catch {
1246
+ }
1247
+ })
1248
+ ),
1249
+ { concurrency: 20 }
1250
+ );
1251
+ const summariesByTargetSession = /* @__PURE__ */ new Map();
1252
+ for (const summaryData of allSummaries) {
1253
+ if (summaryData.leafUuid) {
1254
+ const targetInfo = globalUuidMap.get(summaryData.leafUuid);
1255
+ if (targetInfo) {
1256
+ const targetSessionId = targetInfo.sessionId;
1257
+ if (!summariesByTargetSession.has(targetSessionId)) {
1258
+ summariesByTargetSession.set(targetSessionId, []);
1259
+ }
1260
+ summariesByTargetSession.get(targetSessionId).push({
1261
+ summary: summaryData.summary,
1262
+ leafUuid: summaryData.leafUuid,
1263
+ // Use summary's own timestamp for sorting, not the target message's timestamp
1264
+ timestamp: summaryData.timestamp ?? targetInfo.timestamp,
1265
+ sourceFile: summaryData.sourceFile
1266
+ });
1267
+ }
1268
+ }
1269
+ }
1270
+ const sessions = yield* Effect5.all(
1271
+ sessionFiles.map((file) => {
1272
+ const sessionId = file.replace(".jsonl", "");
1273
+ const mtime = fileMtimes.get(sessionId);
1274
+ return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1275
+ }),
1276
+ { concurrency: 10 }
1277
+ );
1278
+ const sortedSessions = sessions.sort((a, b) => {
1279
+ let comparison = 0;
1280
+ switch (sort.field) {
1281
+ case "summary": {
1282
+ const timeA = getSessionSortTimestamp(a);
1283
+ const timeB = getSessionSortTimestamp(b);
1284
+ const dateA = timeA ? new Date(timeA).getTime() : a.fileMtime ?? 0;
1285
+ const dateB = timeB ? new Date(timeB).getTime() : b.fileMtime ?? 0;
1286
+ comparison = dateA - dateB;
1287
+ break;
1288
+ }
1289
+ case "modified": {
1290
+ comparison = (a.fileMtime ?? 0) - (b.fileMtime ?? 0);
1291
+ break;
1292
+ }
1293
+ case "created": {
1294
+ const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
1295
+ const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
1296
+ comparison = createdA - createdB;
1297
+ break;
1298
+ }
1299
+ case "updated": {
1300
+ const updatedA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
1301
+ const updatedB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
1302
+ comparison = updatedA - updatedB;
1303
+ break;
1304
+ }
1305
+ case "messageCount": {
1306
+ comparison = a.messageCount - b.messageCount;
1307
+ break;
1308
+ }
1309
+ case "title": {
1310
+ const titleA = a.customTitle ?? a.currentSummary ?? a.title;
1311
+ const titleB = b.customTitle ?? b.currentSummary ?? b.title;
1312
+ comparison = titleA.localeCompare(titleB);
1313
+ break;
1314
+ }
1315
+ }
1316
+ return sort.order === "desc" ? -comparison : comparison;
1317
+ });
1318
+ const filteredSessions = sortedSessions.filter((s) => {
1319
+ if (isErrorSessionTitle(s.title)) return false;
1320
+ if (isErrorSessionTitle(s.customTitle)) return false;
1321
+ if (isErrorSessionTitle(s.currentSummary)) return false;
1322
+ return true;
1323
+ });
1324
+ return {
1325
+ name: project.name,
1326
+ displayName: project.displayName,
1327
+ path: project.path,
1328
+ sessionCount: filteredSessions.length,
1329
+ sessions: filteredSessions
1330
+ };
1331
+ });
1332
+
1333
+ // src/session/analysis.ts
1334
+ import { Effect as Effect6 } from "effect";
1335
+ import * as fs7 from "fs/promises";
1336
+ import * as path7 from "path";
1337
+ var analyzeSession = (projectName, sessionId) => Effect6.gen(function* () {
1338
+ const messages = yield* readSession(projectName, sessionId);
1339
+ let userMessages = 0;
1340
+ let assistantMessages = 0;
1341
+ let summaryCount = 0;
1342
+ let snapshotCount = 0;
1343
+ const toolUsageMap = /* @__PURE__ */ new Map();
1344
+ const filesChanged = /* @__PURE__ */ new Set();
1345
+ const patterns = [];
1346
+ const milestones = [];
1347
+ let firstTimestamp;
1348
+ let lastTimestamp;
1349
+ for (const msg of messages) {
1350
+ if (msg.timestamp) {
1351
+ if (!firstTimestamp) firstTimestamp = msg.timestamp;
1352
+ lastTimestamp = msg.timestamp;
1353
+ }
1354
+ if (msg.type === "user") {
1355
+ userMessages++;
1356
+ const content = typeof msg.content === "string" ? msg.content : "";
1357
+ if (content.toLowerCase().includes("commit") || content.toLowerCase().includes("\uC644\uB8CC")) {
1358
+ milestones.push({
1359
+ timestamp: msg.timestamp,
1360
+ description: `User checkpoint: ${content.slice(0, 50)}...`,
1361
+ messageUuid: msg.uuid
1362
+ });
1363
+ }
1364
+ } else if (msg.type === "assistant") {
1365
+ assistantMessages++;
1366
+ if (msg.message?.content && Array.isArray(msg.message.content)) {
1367
+ for (const item of msg.message.content) {
1368
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
1369
+ const toolUse = item;
1370
+ const toolName = toolUse.name ?? "unknown";
1371
+ const existing = toolUsageMap.get(toolName) ?? { count: 0, errorCount: 0 };
1372
+ existing.count++;
1373
+ toolUsageMap.set(toolName, existing);
1374
+ if ((toolName === "Write" || toolName === "Edit") && toolUse.input?.file_path) {
1375
+ filesChanged.add(toolUse.input.file_path);
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ } else if (msg.type === "summary") {
1381
+ summaryCount++;
1382
+ if (msg.summary) {
1383
+ milestones.push({
1384
+ timestamp: msg.timestamp,
1385
+ description: `Summary: ${msg.summary.slice(0, 100)}...`,
1386
+ messageUuid: msg.uuid
1387
+ });
1388
+ }
1389
+ } else if (msg.type === "file-history-snapshot") {
1390
+ snapshotCount++;
1391
+ const snapshot = msg;
1392
+ if (snapshot.snapshot?.trackedFileBackups) {
1393
+ for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1394
+ filesChanged.add(filePath);
1395
+ }
1396
+ }
1397
+ }
1398
+ }
1399
+ for (const msg of messages) {
1400
+ if (msg.type === "user" && msg.content && Array.isArray(msg.content)) {
1401
+ for (const item of msg.content) {
1402
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_result" && "is_error" in item && item.is_error) {
1403
+ const toolResultItem = item;
1404
+ const toolUseId = toolResultItem.tool_use_id;
1405
+ if (toolUseId) {
1406
+ for (const prevMsg of messages) {
1407
+ if (prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
1408
+ for (const prevItem of prevMsg.message.content) {
1409
+ if (prevItem && typeof prevItem === "object" && "type" in prevItem && prevItem.type === "tool_use" && "id" in prevItem && prevItem.id === toolUseId) {
1410
+ const toolName = prevItem.name ?? "unknown";
1411
+ const existing = toolUsageMap.get(toolName);
1412
+ if (existing) {
1413
+ existing.errorCount++;
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ let durationMinutes = 0;
1425
+ if (firstTimestamp && lastTimestamp) {
1426
+ const first = new Date(firstTimestamp).getTime();
1427
+ const last = new Date(lastTimestamp).getTime();
1428
+ durationMinutes = Math.round((last - first) / 1e3 / 60);
1429
+ }
1430
+ const toolUsageArray = Array.from(toolUsageMap.entries()).map(([name, stats]) => ({
1431
+ name,
1432
+ count: stats.count,
1433
+ errorCount: stats.errorCount
1434
+ }));
1435
+ for (const tool of toolUsageArray) {
1436
+ if (tool.count >= 3 && tool.errorCount / tool.count > 0.3) {
1437
+ patterns.push({
1438
+ type: "high_error_rate",
1439
+ description: `${tool.name} had ${tool.errorCount}/${tool.count} errors (${Math.round(tool.errorCount / tool.count * 100)}%)`,
1440
+ count: tool.errorCount
1441
+ });
1442
+ }
1443
+ }
1444
+ if (snapshotCount > 10) {
1445
+ patterns.push({
1446
+ type: "many_snapshots",
1447
+ description: `${snapshotCount} file-history-snapshots could be compressed`,
1448
+ count: snapshotCount
1449
+ });
1450
+ }
1451
+ return {
1452
+ sessionId,
1453
+ projectName,
1454
+ durationMinutes,
1455
+ stats: {
1456
+ totalMessages: messages.length,
1457
+ userMessages,
1458
+ assistantMessages,
1459
+ summaryCount,
1460
+ snapshotCount
1461
+ },
1462
+ toolUsage: toolUsageArray.sort((a, b) => b.count - a.count),
1463
+ filesChanged: Array.from(filesChanged),
1464
+ patterns,
1465
+ milestones
1466
+ };
1467
+ });
1468
+ var compressSession = (projectName, sessionId, options = {}) => Effect6.gen(function* () {
1469
+ const { keepSnapshots = "first_last", maxToolOutputLength = 5e3 } = options;
1470
+ const filePath = path7.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1471
+ const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1472
+ const originalSize = Buffer.byteLength(content, "utf-8");
1473
+ const lines = content.trim().split("\n").filter(Boolean);
1474
+ const messages = lines.map((line) => JSON.parse(line));
1475
+ let removedSnapshots = 0;
1476
+ let truncatedOutputs = 0;
1477
+ const snapshotIndices = [];
1478
+ messages.forEach((msg, idx) => {
1479
+ if (msg.type === "file-history-snapshot") {
1480
+ snapshotIndices.push(idx);
1481
+ }
1482
+ });
1483
+ const filteredMessages = messages.filter((msg, idx) => {
1484
+ if (msg.type === "file-history-snapshot") {
1485
+ if (keepSnapshots === "none") {
1486
+ removedSnapshots++;
1487
+ return false;
1488
+ }
1489
+ if (keepSnapshots === "first_last") {
1490
+ const isFirst = idx === snapshotIndices[0];
1491
+ const isLast = idx === snapshotIndices[snapshotIndices.length - 1];
1492
+ if (!isFirst && !isLast) {
1493
+ removedSnapshots++;
1494
+ return false;
1495
+ }
1496
+ }
1497
+ }
1498
+ return true;
1499
+ });
1500
+ for (const msg of filteredMessages) {
1501
+ if (msg.type === "user" && Array.isArray(msg.content)) {
1502
+ for (const item of msg.content) {
1503
+ if (item.type === "tool_result" && typeof item.content === "string") {
1504
+ if (maxToolOutputLength > 0 && item.content.length > maxToolOutputLength) {
1505
+ item.content = item.content.slice(0, maxToolOutputLength) + "\n... [truncated]";
1506
+ truncatedOutputs++;
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+ const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1513
+ const compressedSize = Buffer.byteLength(newContent, "utf-8");
1514
+ yield* Effect6.tryPromise(() => fs7.writeFile(filePath, newContent, "utf-8"));
1515
+ return {
1516
+ success: true,
1517
+ originalSize,
1518
+ compressedSize,
1519
+ removedSnapshots,
1520
+ truncatedOutputs
1521
+ };
1522
+ });
1523
+ var extractProjectKnowledge = (projectName, sessionIds) => Effect6.gen(function* () {
1524
+ const sessionsDir = getSessionsDir();
1525
+ const projectDir = path7.join(sessionsDir, projectName);
1526
+ let targetSessionIds = sessionIds;
1527
+ if (!targetSessionIds) {
1528
+ const files = yield* Effect6.tryPromise(() => fs7.readdir(projectDir));
1529
+ targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
1530
+ }
1531
+ const fileModifyCount = /* @__PURE__ */ new Map();
1532
+ const toolSequences = [];
1533
+ const decisions = [];
1534
+ for (const sessionId of targetSessionIds) {
1535
+ try {
1536
+ const messages = yield* readSession(projectName, sessionId);
1537
+ for (const msg of messages) {
1538
+ if (msg.type === "file-history-snapshot") {
1539
+ const snapshot = msg;
1540
+ if (snapshot.snapshot?.trackedFileBackups) {
1541
+ for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1542
+ const existing = fileModifyCount.get(filePath) ?? { count: 0 };
1543
+ existing.count++;
1544
+ existing.lastModified = snapshot.snapshot.timestamp;
1545
+ fileModifyCount.set(filePath, existing);
1546
+ }
1547
+ }
1548
+ }
1549
+ if (msg.type === "assistant" && msg.message?.content && Array.isArray(msg.message.content)) {
1550
+ const tools = [];
1551
+ for (const item of msg.message.content) {
1552
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
1553
+ const toolUse = item;
1554
+ if (toolUse.name) tools.push(toolUse.name);
1555
+ }
1556
+ }
1557
+ if (tools.length > 1) {
1558
+ toolSequences.push(tools);
1559
+ }
1560
+ }
1561
+ if (msg.type === "summary" && msg.summary) {
1562
+ decisions.push({
1563
+ context: "Session summary",
1564
+ decision: msg.summary.slice(0, 200),
1565
+ sessionId
1566
+ });
1567
+ }
1568
+ }
1569
+ } catch {
1570
+ continue;
1571
+ }
1572
+ }
1573
+ const hotFiles = Array.from(fileModifyCount.entries()).map(([filePath, data]) => ({
1574
+ path: filePath,
1575
+ modifyCount: data.count,
1576
+ lastModified: data.lastModified
1577
+ })).sort((a, b) => b.modifyCount - a.modifyCount).slice(0, 20);
1578
+ const workflowMap = /* @__PURE__ */ new Map();
1579
+ for (const seq of toolSequences) {
1580
+ const key = seq.join(" -> ");
1581
+ workflowMap.set(key, (workflowMap.get(key) ?? 0) + 1);
1582
+ }
1583
+ const workflows = Array.from(workflowMap.entries()).filter(([, count]) => count >= 2).map(([sequence, count]) => ({
1584
+ sequence: sequence.split(" -> "),
1585
+ count
1586
+ })).sort((a, b) => b.count - a.count).slice(0, 10);
1587
+ return {
1588
+ projectName,
1589
+ patterns: [],
1590
+ hotFiles,
1591
+ workflows,
1592
+ decisions: decisions.slice(0, 20)
1593
+ };
1594
+ });
1595
+ function truncateText(text, maxLen) {
1596
+ const cleaned = text.replace(/\n/g, " ");
1597
+ if (cleaned.length > maxLen) {
1598
+ return cleaned.slice(0, maxLen) + "...";
1599
+ }
1600
+ return cleaned;
1601
+ }
1602
+ var summarizeSession = (projectName, sessionId, options = {}) => Effect6.gen(function* () {
1603
+ const { limit = 50, maxLength = 100 } = options;
1604
+ const messages = yield* readSession(projectName, sessionId);
1605
+ const lines = [];
1606
+ let count = 0;
1607
+ for (const msg of messages) {
1608
+ if (count >= limit) break;
1609
+ if (msg.type === "user" || msg.type === "human") {
1610
+ let timeStr;
1611
+ if (msg.timestamp) {
1612
+ try {
1613
+ const dt = new Date(msg.timestamp);
1614
+ timeStr = dt.toLocaleString("ko-KR", {
1615
+ month: "2-digit",
1616
+ day: "2-digit",
1617
+ hour: "2-digit",
1618
+ minute: "2-digit",
1619
+ hour12: false
1620
+ });
1621
+ } catch {
1622
+ }
1623
+ }
1624
+ const text = extractTextContent(msg.message);
1625
+ if (text) {
1626
+ const truncated = truncateText(text, maxLength);
1627
+ lines.push({ role: "user", content: truncated, timestamp: timeStr });
1628
+ count++;
1629
+ }
1630
+ } else if (msg.type === "assistant") {
1631
+ const text = extractTextContent(msg.message);
1632
+ if (text) {
1633
+ const truncated = truncateText(text, maxLength);
1634
+ lines.push({ role: "assistant", content: truncated });
1635
+ count++;
1636
+ }
1637
+ }
1638
+ }
1639
+ const formatted = lines.map((line) => {
1640
+ if (line.role === "user") {
1641
+ return line.timestamp ? `user [${line.timestamp}]: ${line.content}` : `user: ${line.content}`;
1642
+ }
1643
+ return `assistant: ${line.content}`;
1644
+ }).join("\n");
1645
+ return {
1646
+ sessionId,
1647
+ projectName,
1648
+ lines,
1649
+ formatted
1650
+ };
1651
+ });
1652
+
1653
+ // src/session/cleanup.ts
1654
+ import { Effect as Effect7 } from "effect";
1655
+ import * as fs8 from "fs/promises";
1656
+ import * as path8 from "path";
1657
+ var cleanInvalidMessages = (projectName, sessionId) => Effect7.gen(function* () {
1658
+ const filePath = path8.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1659
+ const content = yield* Effect7.tryPromise(() => fs8.readFile(filePath, "utf-8"));
1660
+ const lines = content.trim().split("\n").filter(Boolean);
1661
+ if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
1662
+ const messages = lines.map((line) => JSON.parse(line));
1663
+ const invalidIndices = [];
1664
+ messages.forEach((msg, idx) => {
1665
+ if (isInvalidApiKeyMessage(msg)) {
1666
+ invalidIndices.push(idx);
1667
+ }
1668
+ });
1669
+ if (invalidIndices.length === 0) {
1670
+ const userAssistantCount = messages.filter(
1671
+ (m) => m.type === "user" || m.type === "assistant"
1672
+ ).length;
1673
+ const hasSummary2 = messages.some((m) => m.type === "summary");
1674
+ const remainingCount2 = userAssistantCount > 0 ? userAssistantCount : hasSummary2 ? 1 : 0;
1675
+ return { removedCount: 0, remainingCount: remainingCount2 };
1676
+ }
1677
+ const filtered = [];
1678
+ let lastValidUuid = null;
1679
+ for (let i = 0; i < messages.length; i++) {
1680
+ if (invalidIndices.includes(i)) {
1681
+ continue;
1682
+ }
1683
+ const msg = messages[i];
1684
+ if (msg.parentUuid && invalidIndices.some((idx) => messages[idx]?.uuid === msg.parentUuid)) {
1685
+ msg.parentUuid = lastValidUuid;
1686
+ }
1687
+ filtered.push(msg);
1688
+ lastValidUuid = msg.uuid;
1689
+ }
1690
+ const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
1691
+ yield* Effect7.tryPromise(() => fs8.writeFile(filePath, newContent, "utf-8"));
1692
+ const remainingUserAssistant = filtered.filter(
1693
+ (m) => m.type === "user" || m.type === "assistant"
1694
+ ).length;
1695
+ const hasSummary = filtered.some((m) => m.type === "summary");
1696
+ const remainingCount = remainingUserAssistant > 0 ? remainingUserAssistant : hasSummary ? 1 : 0;
1697
+ return { removedCount: invalidIndices.length, remainingCount };
1698
+ });
1699
+ var previewCleanup = (projectName) => Effect7.gen(function* () {
1700
+ const projects = yield* listProjects;
1701
+ const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1702
+ const orphanTodos = yield* findOrphanTodos();
1703
+ const orphanTodoCount = orphanTodos.length;
1704
+ const results = yield* Effect7.all(
1705
+ targetProjects.map(
1706
+ (project) => Effect7.gen(function* () {
1707
+ const sessions = yield* listSessions(project.name);
1708
+ const emptySessions = sessions.filter((s) => s.messageCount === 0);
1709
+ const invalidSessions = sessions.filter(
1710
+ (s) => s.title?.includes("Invalid API key") || s.title?.includes("API key")
1711
+ );
1712
+ let emptyWithTodosCount = 0;
1713
+ for (const session of emptySessions) {
1714
+ const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1715
+ const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1716
+ if (hasTodos) {
1717
+ emptyWithTodosCount++;
1718
+ }
1719
+ }
1720
+ const orphanAgents = yield* findOrphanAgents(project.name);
1721
+ return {
1722
+ project: project.name,
1723
+ emptySessions,
1724
+ invalidSessions,
1725
+ emptyWithTodosCount,
1726
+ orphanAgentCount: orphanAgents.length,
1727
+ orphanTodoCount: 0
1728
+ // Will set for first project only
1729
+ };
1730
+ })
1731
+ ),
1732
+ { concurrency: 5 }
1733
+ );
1734
+ if (results.length > 0) {
1735
+ results[0] = { ...results[0], orphanTodoCount };
1736
+ }
1737
+ return results;
1738
+ });
1739
+ var clearSessions = (options) => Effect7.gen(function* () {
1740
+ const {
1741
+ projectName,
1742
+ clearEmpty = true,
1743
+ clearInvalid = true,
1744
+ skipWithTodos = true,
1745
+ clearOrphanAgents = true,
1746
+ clearOrphanTodos = false
1077
1747
  } = options;
1078
1748
  const projects = yield* listProjects;
1079
1749
  const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
@@ -1084,8 +1754,8 @@ var clearSessions = (options) => Effect3.gen(function* () {
1084
1754
  const sessionsToDelete = [];
1085
1755
  if (clearInvalid) {
1086
1756
  for (const project of targetProjects) {
1087
- const projectPath = path4.join(getSessionsDir(), project.name);
1088
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1757
+ const projectPath = path8.join(getSessionsDir(), project.name);
1758
+ const files = yield* Effect7.tryPromise(() => fs8.readdir(projectPath));
1089
1759
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1090
1760
  for (const file of sessionFiles) {
1091
1761
  const sessionId = file.replace(".jsonl", "");
@@ -1139,7 +1809,12 @@ var clearSessions = (options) => Effect3.gen(function* () {
1139
1809
  deletedOrphanTodoCount
1140
1810
  };
1141
1811
  });
1142
- var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1812
+
1813
+ // src/session/search.ts
1814
+ import { Effect as Effect8 } from "effect";
1815
+ import * as fs9 from "fs/promises";
1816
+ import * as path9 from "path";
1817
+ var searchSessions = (query, options = {}) => Effect8.gen(function* () {
1143
1818
  const { projectName, searchContent = false } = options;
1144
1819
  const results = [];
1145
1820
  const queryLower = query.toLowerCase();
@@ -1162,16 +1837,16 @@ var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1162
1837
  }
1163
1838
  if (searchContent) {
1164
1839
  for (const project of targetProjects) {
1165
- const projectPath = path4.join(getSessionsDir(), project.name);
1166
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1840
+ const projectPath = path9.join(getSessionsDir(), project.name);
1841
+ const files = yield* Effect8.tryPromise(() => fs9.readdir(projectPath));
1167
1842
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1168
1843
  for (const file of sessionFiles) {
1169
1844
  const sessionId = file.replace(".jsonl", "");
1170
1845
  if (results.some((r) => r.sessionId === sessionId && r.projectName === project.name)) {
1171
1846
  continue;
1172
1847
  }
1173
- const filePath = path4.join(projectPath, file);
1174
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1848
+ const filePath = path9.join(projectPath, file);
1849
+ const content = yield* Effect8.tryPromise(() => fs9.readFile(filePath, "utf-8"));
1175
1850
  const lines = content.trim().split("\n").filter(Boolean);
1176
1851
  for (const line of lines) {
1177
1852
  try {
@@ -1207,232 +1882,109 @@ var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1207
1882
  return dateB - dateA;
1208
1883
  });
1209
1884
  });
1210
- var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession) => Effect3.gen(function* () {
1211
- const projectPath = path4.join(getSessionsDir(), projectName);
1212
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
1213
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1214
- const lines = content.trim().split("\n").filter(Boolean);
1215
- const messages = lines.map((line) => JSON.parse(line));
1216
- let summaries;
1217
- if (summariesByTargetSession) {
1218
- summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort(
1219
- (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
1220
- );
1221
- } else {
1222
- summaries = [];
1223
- const sessionUuids = /* @__PURE__ */ new Set();
1224
- for (const msg of messages) {
1225
- if (msg.uuid && typeof msg.uuid === "string") {
1226
- sessionUuids.add(msg.uuid);
1227
- }
1228
- }
1229
- const projectFiles = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1230
- const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1231
- for (const file of allJsonlFiles) {
1232
- try {
1233
- const otherFilePath = path4.join(projectPath, file);
1234
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
1235
- const otherLines = otherContent.trim().split("\n").filter(Boolean);
1236
- for (const line of otherLines) {
1237
- try {
1238
- const msg = JSON.parse(line);
1239
- if (msg.type === "summary" && typeof msg.summary === "string" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
1240
- const targetMsg = messages.find((m) => m.uuid === msg.leafUuid);
1241
- summaries.push({
1242
- summary: msg.summary,
1243
- leafUuid: msg.leafUuid,
1244
- timestamp: targetMsg?.timestamp ?? msg.timestamp
1245
- });
1246
- }
1247
- } catch {
1885
+
1886
+ // src/session/files.ts
1887
+ import { Effect as Effect9 } from "effect";
1888
+ var getSessionFiles = (projectName, sessionId) => Effect9.gen(function* () {
1889
+ const messages = yield* readSession(projectName, sessionId);
1890
+ const fileChanges = [];
1891
+ const seenFiles = /* @__PURE__ */ new Set();
1892
+ for (const msg of messages) {
1893
+ if (msg.type === "file-history-snapshot") {
1894
+ const snapshot = msg;
1895
+ const backups = snapshot.snapshot?.trackedFileBackups;
1896
+ if (backups && typeof backups === "object") {
1897
+ for (const filePath of Object.keys(backups)) {
1898
+ if (!seenFiles.has(filePath)) {
1899
+ seenFiles.add(filePath);
1900
+ fileChanges.push({
1901
+ path: filePath,
1902
+ action: "modified",
1903
+ timestamp: snapshot.snapshot?.timestamp,
1904
+ messageUuid: snapshot.messageId ?? msg.uuid
1905
+ });
1248
1906
  }
1249
1907
  }
1250
- } catch {
1251
1908
  }
1252
1909
  }
1253
- }
1254
- summaries.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
1255
- let lastCompactBoundaryUuid;
1256
- for (let i = messages.length - 1; i >= 0; i--) {
1257
- const msg = messages[i];
1258
- if (msg.type === "system" && msg.subtype === "compact_boundary") {
1259
- lastCompactBoundaryUuid = msg.uuid;
1260
- break;
1261
- }
1262
- }
1263
- const firstUserMsg = messages.find((m) => m.type === "user");
1264
- const customTitleMsg = messages.find((m) => m.type === "custom-title");
1265
- const customTitle = customTitleMsg?.customTitle;
1266
- const title = firstUserMsg ? extractTitle(extractTextContent(firstUserMsg.message)) : summaries.length > 0 ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`;
1267
- const userAssistantMessages = messages.filter(
1268
- (m) => m.type === "user" || m.type === "assistant"
1269
- );
1270
- const firstMessage = userAssistantMessages[0];
1271
- const lastMessage = userAssistantMessages[userAssistantMessages.length - 1];
1272
- const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1273
- const agents = [];
1274
- for (const agentId of linkedAgentIds) {
1275
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
1276
- try {
1277
- const agentContent = yield* Effect3.tryPromise(() => fs4.readFile(agentPath, "utf-8"));
1278
- const agentLines = agentContent.trim().split("\n").filter(Boolean);
1279
- const agentMsgs = agentLines.map((l) => JSON.parse(l));
1280
- const agentUserAssistant = agentMsgs.filter(
1281
- (m) => m.type === "user" || m.type === "assistant"
1282
- );
1283
- let agentName;
1284
- const firstAgentMsg = agentMsgs.find((m) => m.type === "user");
1285
- if (firstAgentMsg) {
1286
- const text = extractTextContent(firstAgentMsg.message);
1287
- if (text) {
1288
- agentName = extractTitle(text);
1910
+ if (msg.type === "assistant" && msg.message?.content) {
1911
+ const content = msg.message.content;
1912
+ if (Array.isArray(content)) {
1913
+ for (const item of content) {
1914
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
1915
+ const toolUse = item;
1916
+ if ((toolUse.name === "Write" || toolUse.name === "Edit") && toolUse.input?.file_path) {
1917
+ const filePath = toolUse.input.file_path;
1918
+ if (!seenFiles.has(filePath)) {
1919
+ seenFiles.add(filePath);
1920
+ fileChanges.push({
1921
+ path: filePath,
1922
+ action: toolUse.name === "Write" ? "created" : "modified",
1923
+ timestamp: msg.timestamp,
1924
+ messageUuid: msg.uuid
1925
+ });
1926
+ }
1927
+ }
1928
+ }
1289
1929
  }
1290
1930
  }
1291
- agents.push({
1292
- id: agentId,
1293
- name: agentName,
1294
- messageCount: agentUserAssistant.length
1295
- });
1296
- } catch {
1297
- agents.push({
1298
- id: agentId,
1299
- messageCount: 0
1300
- });
1301
1931
  }
1302
1932
  }
1303
- const todos = yield* findLinkedTodos(sessionId, linkedAgentIds);
1304
1933
  return {
1305
- id: sessionId,
1934
+ sessionId,
1306
1935
  projectName,
1307
- title,
1308
- customTitle,
1309
- currentSummary: summaries[0]?.summary,
1310
- messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : summaries.length > 0 ? 1 : 0,
1311
- createdAt: firstMessage?.timestamp ?? void 0,
1312
- updatedAt: lastMessage?.timestamp ?? void 0,
1313
- summaries,
1314
- agents,
1315
- todos,
1316
- lastCompactBoundaryUuid
1936
+ files: fileChanges,
1937
+ totalChanges: fileChanges.length
1317
1938
  };
1318
1939
  });
1319
- var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1320
- var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1321
- const project = (yield* listProjects).find((p) => p.name === projectName);
1322
- if (!project) {
1940
+
1941
+ // src/session/index-file.ts
1942
+ import { Effect as Effect10 } from "effect";
1943
+ import * as fs10 from "fs/promises";
1944
+ import * as path10 from "path";
1945
+ var loadSessionsIndex = (projectName) => Effect10.gen(function* () {
1946
+ const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
1947
+ try {
1948
+ const content = yield* Effect10.tryPromise(() => fs10.readFile(indexPath, "utf-8"));
1949
+ const index = JSON.parse(content);
1950
+ return index;
1951
+ } catch {
1323
1952
  return null;
1324
1953
  }
1325
- const projectPath = path4.join(getSessionsDir(), projectName);
1326
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1327
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1328
- const globalUuidMap = /* @__PURE__ */ new Map();
1329
- const allSummaries = [];
1330
- const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1331
- yield* Effect3.all(
1332
- allJsonlFiles.map(
1333
- (file) => Effect3.gen(function* () {
1334
- const filePath = path4.join(projectPath, file);
1335
- const fileSessionId = file.replace(".jsonl", "");
1336
- try {
1337
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1338
- const lines = content.trim().split("\n").filter(Boolean);
1339
- for (const line of lines) {
1340
- try {
1341
- const msg = JSON.parse(line);
1342
- if (msg.uuid && typeof msg.uuid === "string") {
1343
- globalUuidMap.set(msg.uuid, {
1344
- sessionId: fileSessionId,
1345
- timestamp: msg.timestamp
1346
- });
1347
- }
1348
- if (msg.messageId && typeof msg.messageId === "string") {
1349
- globalUuidMap.set(msg.messageId, {
1350
- sessionId: fileSessionId,
1351
- timestamp: msg.snapshot?.timestamp
1352
- });
1353
- }
1354
- if (msg.type === "summary" && typeof msg.summary === "string") {
1355
- allSummaries.push({
1356
- summary: msg.summary,
1357
- leafUuid: msg.leafUuid,
1358
- timestamp: msg.timestamp
1359
- });
1360
- }
1361
- } catch {
1362
- }
1363
- }
1364
- } catch {
1365
- }
1366
- })
1367
- ),
1368
- { concurrency: 20 }
1369
- );
1370
- const summariesByTargetSession = /* @__PURE__ */ new Map();
1371
- for (const summaryData of allSummaries) {
1372
- if (summaryData.leafUuid) {
1373
- const targetInfo = globalUuidMap.get(summaryData.leafUuid);
1374
- if (targetInfo) {
1375
- const targetSessionId = targetInfo.sessionId;
1376
- if (!summariesByTargetSession.has(targetSessionId)) {
1377
- summariesByTargetSession.set(targetSessionId, []);
1378
- }
1379
- summariesByTargetSession.get(targetSessionId).push({
1380
- summary: summaryData.summary,
1381
- leafUuid: summaryData.leafUuid,
1382
- timestamp: targetInfo.timestamp ?? summaryData.timestamp
1383
- });
1384
- }
1385
- }
1386
- }
1387
- const sessions = yield* Effect3.all(
1388
- sessionFiles.map((file) => {
1389
- const sessionId = file.replace(".jsonl", "");
1390
- return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession);
1391
- }),
1392
- { concurrency: 10 }
1393
- );
1394
- const sortedSessions = sessions.sort((a, b) => {
1395
- const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
1396
- const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
1397
- return dateB - dateA;
1954
+ });
1955
+ var getIndexEntryDisplayTitle = (entry) => {
1956
+ if (entry.customTitle) return entry.customTitle;
1957
+ if (entry.summary) return entry.summary;
1958
+ let prompt = entry.firstPrompt;
1959
+ if (prompt === "No prompt") return "Untitled";
1960
+ if (prompt.startsWith("[Request interrupted")) return "Untitled";
1961
+ prompt = prompt.replace(/<ide_[^>]*>[^<]*<\/ide_[^>]*>/g, "").trim();
1962
+ if (!prompt) return "Untitled";
1963
+ if (prompt.length > 60) {
1964
+ return prompt.slice(0, 57) + "...";
1965
+ }
1966
+ return prompt;
1967
+ };
1968
+ var sortIndexEntriesByModified = (entries) => {
1969
+ return [...entries].sort((a, b) => {
1970
+ const modA = new Date(a.modified).getTime();
1971
+ const modB = new Date(b.modified).getTime();
1972
+ return modB - modA;
1398
1973
  });
1399
- const filteredSessions = sortedSessions.filter((s) => {
1400
- if (isErrorSessionTitle(s.title)) return false;
1401
- if (isErrorSessionTitle(s.customTitle)) return false;
1402
- if (isErrorSessionTitle(s.currentSummary)) return false;
1974
+ };
1975
+ var hasSessionsIndex = (projectName) => Effect10.gen(function* () {
1976
+ const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
1977
+ try {
1978
+ yield* Effect10.tryPromise(() => fs10.access(indexPath));
1403
1979
  return true;
1404
- });
1405
- return {
1406
- name: project.name,
1407
- displayName: project.displayName,
1408
- path: project.path,
1409
- sessionCount: filteredSessions.length,
1410
- sessions: filteredSessions
1411
- };
1412
- });
1413
- var updateSessionSummary = (projectName, sessionId, newSummary) => Effect3.gen(function* () {
1414
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1415
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1416
- const lines = content.trim().split("\n").filter(Boolean);
1417
- const messages = lines.map((line) => JSON.parse(line));
1418
- const summaryIdx = messages.findIndex((m) => m.type === "summary");
1419
- if (summaryIdx >= 0) {
1420
- messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
1421
- } else {
1422
- const firstUserMsg = messages.find((m) => m.type === "user");
1423
- const summaryMsg = {
1424
- type: "summary",
1425
- summary: newSummary,
1426
- leafUuid: firstUserMsg?.uuid ?? null
1427
- };
1428
- messages.unshift(summaryMsg);
1980
+ } catch {
1981
+ return false;
1429
1982
  }
1430
- const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1431
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1432
- return { success: true };
1433
1983
  });
1434
1984
  export {
1985
+ analyzeSession,
1435
1986
  clearSessions,
1987
+ compressSession,
1436
1988
  createLogger,
1437
1989
  deleteLinkedTodos,
1438
1990
  deleteMessage,
@@ -1440,6 +1992,7 @@ export {
1440
1992
  deleteOrphanTodos,
1441
1993
  deleteSession,
1442
1994
  displayPathToFolderName,
1995
+ extractProjectKnowledge,
1443
1996
  extractTextContent,
1444
1997
  extractTitle,
1445
1998
  findLinkedAgents,
@@ -1450,11 +2003,14 @@ export {
1450
2003
  folderNameToDisplayPath,
1451
2004
  folderNameToPath,
1452
2005
  getDisplayTitle,
2006
+ getIndexEntryDisplayTitle,
1453
2007
  getLogger,
1454
2008
  getRealPathFromSession,
1455
2009
  getSessionFiles,
2010
+ getSessionSortTimestamp,
1456
2011
  getSessionsDir,
1457
2012
  getTodosDir,
2013
+ hasSessionsIndex,
1458
2014
  isContinuationSummary,
1459
2015
  isInvalidApiKeyMessage,
1460
2016
  listProjects,
@@ -1462,6 +2018,7 @@ export {
1462
2018
  loadAgentMessages,
1463
2019
  loadProjectTreeData,
1464
2020
  loadSessionTreeData,
2021
+ loadSessionsIndex,
1465
2022
  maskHomePath,
1466
2023
  moveSession,
1467
2024
  pathToFolderName,
@@ -1472,8 +2029,10 @@ export {
1472
2029
  searchSessions,
1473
2030
  sessionHasTodos,
1474
2031
  setLogger,
2032
+ sortIndexEntriesByModified,
1475
2033
  sortProjects,
1476
2034
  splitSession,
2035
+ summarizeSession,
1477
2036
  updateSessionSummary
1478
2037
  };
1479
2038
  //# sourceMappingURL=index.js.map