@claude-sessions/core 0.3.7 → 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
@@ -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;
@@ -334,8 +337,8 @@ var findOrphanAgentsWithPaths = (projectName) => Effect.gen(function* () {
334
337
  }
335
338
  for (const entry of files) {
336
339
  const entryPath = path2.join(projectPath, entry);
337
- const stat3 = yield* Effect.tryPromise(() => fs2.stat(entryPath).catch(() => null));
338
- if (stat3?.isDirectory() && !entry.startsWith(".")) {
340
+ const stat4 = yield* Effect.tryPromise(() => fs2.stat(entryPath).catch(() => null));
341
+ if (stat4?.isDirectory() && !entry.startsWith(".")) {
339
342
  const subagentsPath = path2.join(entryPath, "subagents");
340
343
  const subagentsExists = yield* Effect.tryPromise(
341
344
  () => fs2.stat(subagentsPath).then(() => true).catch(() => false)
@@ -635,8 +638,8 @@ var deleteOrphanTodos = () => Effect2.gen(function* () {
635
638
  return { success: true, deletedCount };
636
639
  });
637
640
 
638
- // src/session.ts
639
- 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";
640
643
  import * as fs4 from "fs/promises";
641
644
  import * as path4 from "path";
642
645
  var listProjects = Effect3.gen(function* () {
@@ -666,15 +669,42 @@ var listProjects = Effect3.gen(function* () {
666
669
  );
667
670
  return projects;
668
671
  });
669
- var listSessions = (projectName) => Effect3.gen(function* () {
670
- const projectPath = path4.join(getSessionsDir(), projectName);
671
- 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));
672
702
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
673
- const sessions = yield* Effect3.all(
703
+ const sessions = yield* Effect4.all(
674
704
  sessionFiles.map(
675
- (file) => Effect3.gen(function* () {
676
- const filePath = path4.join(projectPath, file);
677
- 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"));
678
708
  const lines = content.trim().split("\n").filter(Boolean);
679
709
  const messages = lines.map((line) => JSON.parse(line));
680
710
  const sessionId = file.replace(".jsonl", "");
@@ -693,10 +723,25 @@ var listSessions = (projectName) => Effect3.gen(function* () {
693
723
  }),
694
724
  O.getOrElse(() => hasSummary ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`)
695
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
+ );
696
739
  return {
697
740
  id: sessionId,
698
741
  projectName,
699
742
  title,
743
+ customTitle,
744
+ currentSummary,
700
745
  // If session has summary but no user/assistant messages, count as 1
701
746
  messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : hasSummary ? 1 : 0,
702
747
  createdAt: firstMessage?.timestamp,
@@ -712,15 +757,15 @@ var listSessions = (projectName) => Effect3.gen(function* () {
712
757
  return dateB - dateA;
713
758
  });
714
759
  });
715
- var readSession = (projectName, sessionId) => Effect3.gen(function* () {
716
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
717
- 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"));
718
763
  const lines = content.trim().split("\n").filter(Boolean);
719
764
  return lines.map((line) => JSON.parse(line));
720
765
  });
721
- var deleteMessage = (projectName, sessionId, messageUuid) => Effect3.gen(function* () {
722
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
723
- 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"));
724
769
  const lines = content.trim().split("\n").filter(Boolean);
725
770
  const messages = lines.map((line) => JSON.parse(line));
726
771
  const targetIndex = messages.findIndex(
@@ -739,12 +784,12 @@ var deleteMessage = (projectName, sessionId, messageUuid) => Effect3.gen(functio
739
784
  }
740
785
  messages.splice(targetIndex, 1);
741
786
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
742
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
787
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
743
788
  return { success: true, deletedMessage: deletedMsg };
744
789
  });
745
- var restoreMessage = (projectName, sessionId, message, index) => Effect3.gen(function* () {
746
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
747
- 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"));
748
793
  const lines = content.trim().split("\n").filter(Boolean);
749
794
  const messages = lines.map((line) => JSON.parse(line));
750
795
  const msgUuid = message.uuid ?? message.messageId;
@@ -761,41 +806,41 @@ var restoreMessage = (projectName, sessionId, message, index) => Effect3.gen(fun
761
806
  const insertIndex = Math.min(index, messages.length);
762
807
  messages.splice(insertIndex, 0, message);
763
808
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
764
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
809
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
765
810
  return { success: true };
766
811
  });
767
- var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
812
+ var deleteSession = (projectName, sessionId) => Effect4.gen(function* () {
768
813
  const sessionsDir = getSessionsDir();
769
- const projectPath = path4.join(sessionsDir, projectName);
770
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
814
+ const projectPath = path5.join(sessionsDir, projectName);
815
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
771
816
  const linkedAgents = yield* findLinkedAgents(projectName, sessionId);
772
- const stat3 = yield* Effect3.tryPromise(() => fs4.stat(filePath));
773
- if (stat3.size === 0) {
774
- yield* Effect3.tryPromise(() => fs4.unlink(filePath));
775
- const agentBackupDir2 = path4.join(projectPath, ".bak");
776
- 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 }));
777
822
  for (const agentId of linkedAgents) {
778
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
779
- const agentBackupPath = path4.join(agentBackupDir2, `${agentId}.jsonl`);
780
- 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(() => {
781
826
  }));
782
827
  }
783
828
  yield* deleteLinkedTodos(sessionId, linkedAgents);
784
829
  return { success: true, deletedAgents: linkedAgents.length };
785
830
  }
786
- const backupDir = path4.join(sessionsDir, ".bak");
787
- yield* Effect3.tryPromise(() => fs4.mkdir(backupDir, { recursive: true }));
788
- const agentBackupDir = path4.join(projectPath, ".bak");
789
- 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 }));
790
835
  for (const agentId of linkedAgents) {
791
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
792
- const agentBackupPath = path4.join(agentBackupDir, `${agentId}.jsonl`);
793
- 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(() => {
794
839
  }));
795
840
  }
796
841
  const todosResult = yield* deleteLinkedTodos(sessionId, linkedAgents);
797
- const backupPath = path4.join(backupDir, `${projectName}_${sessionId}.jsonl`);
798
- yield* Effect3.tryPromise(() => fs4.rename(filePath, backupPath));
842
+ const backupPath = path5.join(backupDir, `${projectName}_${sessionId}.jsonl`);
843
+ yield* Effect4.tryPromise(() => fs5.rename(filePath, backupPath));
799
844
  return {
800
845
  success: true,
801
846
  backupPath,
@@ -803,10 +848,10 @@ var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
803
848
  deletedTodos: todosResult.deletedCount
804
849
  };
805
850
  });
806
- var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function* () {
807
- const projectPath = path4.join(getSessionsDir(), projectName);
808
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
809
- 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"));
810
855
  const lines = content.trim().split("\n").filter(Boolean);
811
856
  if (lines.length === 0) {
812
857
  return { success: false, error: "Empty session" };
@@ -830,14 +875,14 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
830
875
  messages.unshift(customTitleRecord);
831
876
  }
832
877
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
833
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
834
- 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));
835
880
  const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
836
881
  const summariesTargetingThis = [];
837
882
  for (const file of allJsonlFiles) {
838
- const otherFilePath = path4.join(projectPath, file);
883
+ const otherFilePath = path5.join(projectPath, file);
839
884
  try {
840
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
885
+ const otherContent = yield* Effect4.tryPromise(() => fs5.readFile(otherFilePath, "utf-8"));
841
886
  const otherLines = otherContent.trim().split("\n").filter(Boolean);
842
887
  const otherMessages = otherLines.map((l) => JSON.parse(l));
843
888
  for (let i = 0; i < otherMessages.length; i++) {
@@ -857,8 +902,8 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
857
902
  if (summariesTargetingThis.length > 0) {
858
903
  summariesTargetingThis.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
859
904
  const firstSummary = summariesTargetingThis[0];
860
- const summaryFilePath = path4.join(projectPath, firstSummary.file);
861
- 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"));
862
907
  const summaryLines = summaryContent.trim().split("\n").filter(Boolean);
863
908
  const summaryMessages = summaryLines.map((l) => JSON.parse(l));
864
909
  summaryMessages[firstSummary.idx] = {
@@ -866,9 +911,9 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
866
911
  summary: newTitle
867
912
  };
868
913
  const newSummaryContent = summaryMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
869
- yield* Effect3.tryPromise(() => fs4.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
914
+ yield* Effect4.tryPromise(() => fs5.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
870
915
  } else {
871
- const currentContent = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
916
+ const currentContent = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
872
917
  const currentLines = currentContent.trim().split("\n").filter(Boolean);
873
918
  const currentMessages = currentLines.map((l) => JSON.parse(l));
874
919
  const firstUserIdx = currentMessages.findIndex((m) => m.type === "user");
@@ -887,233 +932,50 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
887
932
 
888
933
  ${cleanedText}`;
889
934
  const updatedContent = currentMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
890
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, updatedContent, "utf-8"));
935
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, updatedContent, "utf-8"));
891
936
  }
892
937
  }
893
938
  }
894
939
  }
895
940
  return { success: true };
896
941
  });
897
- var getSessionFiles = (projectName, sessionId) => Effect3.gen(function* () {
898
- const messages = yield* readSession(projectName, sessionId);
899
- const fileChanges = [];
900
- const seenFiles = /* @__PURE__ */ new Set();
901
- for (const msg of messages) {
902
- if (msg.type === "file-history-snapshot") {
903
- const snapshot = msg;
904
- const backups = snapshot.snapshot?.trackedFileBackups;
905
- if (backups && typeof backups === "object") {
906
- for (const filePath of Object.keys(backups)) {
907
- if (!seenFiles.has(filePath)) {
908
- seenFiles.add(filePath);
909
- fileChanges.push({
910
- path: filePath,
911
- action: "modified",
912
- timestamp: snapshot.snapshot?.timestamp,
913
- messageUuid: snapshot.messageId ?? msg.uuid
914
- });
915
- }
916
- }
917
- }
918
- }
919
- if (msg.type === "assistant" && msg.message?.content) {
920
- const content = msg.message.content;
921
- if (Array.isArray(content)) {
922
- for (const item of content) {
923
- if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
924
- const toolUse = item;
925
- if ((toolUse.name === "Write" || toolUse.name === "Edit") && toolUse.input?.file_path) {
926
- const filePath = toolUse.input.file_path;
927
- if (!seenFiles.has(filePath)) {
928
- seenFiles.add(filePath);
929
- fileChanges.push({
930
- path: filePath,
931
- action: toolUse.name === "Write" ? "created" : "modified",
932
- timestamp: msg.timestamp,
933
- messageUuid: msg.uuid
934
- });
935
- }
936
- }
937
- }
938
- }
939
- }
940
- }
941
- }
942
- return {
943
- sessionId,
944
- projectName,
945
- files: fileChanges,
946
- totalChanges: fileChanges.length
947
- };
948
- });
949
- var analyzeSession = (projectName, sessionId) => Effect3.gen(function* () {
950
- const messages = yield* readSession(projectName, sessionId);
951
- let userMessages = 0;
952
- let assistantMessages = 0;
953
- let summaryCount = 0;
954
- let snapshotCount = 0;
955
- const toolUsageMap = /* @__PURE__ */ new Map();
956
- const filesChanged = /* @__PURE__ */ new Set();
957
- const patterns = [];
958
- const milestones = [];
959
- let firstTimestamp;
960
- let lastTimestamp;
961
- for (const msg of messages) {
962
- if (msg.timestamp) {
963
- if (!firstTimestamp) firstTimestamp = msg.timestamp;
964
- lastTimestamp = msg.timestamp;
965
- }
966
- if (msg.type === "user") {
967
- userMessages++;
968
- const content = typeof msg.content === "string" ? msg.content : "";
969
- if (content.toLowerCase().includes("commit") || content.toLowerCase().includes("\uC644\uB8CC")) {
970
- milestones.push({
971
- timestamp: msg.timestamp,
972
- description: `User checkpoint: ${content.slice(0, 50)}...`,
973
- messageUuid: msg.uuid
974
- });
975
- }
976
- } else if (msg.type === "assistant") {
977
- assistantMessages++;
978
- if (msg.message?.content && Array.isArray(msg.message.content)) {
979
- for (const item of msg.message.content) {
980
- if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
981
- const toolUse = item;
982
- const toolName = toolUse.name ?? "unknown";
983
- const existing = toolUsageMap.get(toolName) ?? { count: 0, errorCount: 0 };
984
- existing.count++;
985
- toolUsageMap.set(toolName, existing);
986
- if ((toolName === "Write" || toolName === "Edit") && toolUse.input?.file_path) {
987
- filesChanged.add(toolUse.input.file_path);
988
- }
989
- }
990
- }
991
- }
992
- } else if (msg.type === "summary") {
993
- summaryCount++;
994
- if (msg.summary) {
995
- milestones.push({
996
- timestamp: msg.timestamp,
997
- description: `Summary: ${msg.summary.slice(0, 100)}...`,
998
- messageUuid: msg.uuid
999
- });
1000
- }
1001
- } else if (msg.type === "file-history-snapshot") {
1002
- snapshotCount++;
1003
- const snapshot = msg;
1004
- if (snapshot.snapshot?.trackedFileBackups) {
1005
- for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1006
- filesChanged.add(filePath);
1007
- }
1008
- }
1009
- }
1010
- }
1011
- for (const msg of messages) {
1012
- if (msg.type === "user" && msg.content && Array.isArray(msg.content)) {
1013
- for (const item of msg.content) {
1014
- if (item && typeof item === "object" && "type" in item && item.type === "tool_result" && "is_error" in item && item.is_error) {
1015
- const toolResultItem = item;
1016
- const toolUseId = toolResultItem.tool_use_id;
1017
- if (toolUseId) {
1018
- for (const prevMsg of messages) {
1019
- if (prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
1020
- for (const prevItem of prevMsg.message.content) {
1021
- if (prevItem && typeof prevItem === "object" && "type" in prevItem && prevItem.type === "tool_use" && "id" in prevItem && prevItem.id === toolUseId) {
1022
- const toolName = prevItem.name ?? "unknown";
1023
- const existing = toolUsageMap.get(toolName);
1024
- if (existing) {
1025
- existing.errorCount++;
1026
- }
1027
- }
1028
- }
1029
- }
1030
- }
1031
- }
1032
- }
1033
- }
1034
- }
1035
- }
1036
- let durationMinutes = 0;
1037
- if (firstTimestamp && lastTimestamp) {
1038
- const first = new Date(firstTimestamp).getTime();
1039
- const last = new Date(lastTimestamp).getTime();
1040
- durationMinutes = Math.round((last - first) / 1e3 / 60);
1041
- }
1042
- const toolUsageArray = Array.from(toolUsageMap.entries()).map(([name, stats]) => ({
1043
- name,
1044
- count: stats.count,
1045
- errorCount: stats.errorCount
1046
- }));
1047
- for (const tool of toolUsageArray) {
1048
- if (tool.count >= 3 && tool.errorCount / tool.count > 0.3) {
1049
- patterns.push({
1050
- type: "high_error_rate",
1051
- description: `${tool.name} had ${tool.errorCount}/${tool.count} errors (${Math.round(tool.errorCount / tool.count * 100)}%)`,
1052
- count: tool.errorCount
1053
- });
1054
- }
1055
- }
1056
- if (snapshotCount > 10) {
1057
- patterns.push({
1058
- type: "many_snapshots",
1059
- description: `${snapshotCount} file-history-snapshots could be compressed`,
1060
- count: snapshotCount
1061
- });
1062
- }
1063
- return {
1064
- sessionId,
1065
- projectName,
1066
- durationMinutes,
1067
- stats: {
1068
- totalMessages: messages.length,
1069
- userMessages,
1070
- assistantMessages,
1071
- summaryCount,
1072
- snapshotCount
1073
- },
1074
- toolUsage: toolUsageArray.sort((a, b) => b.count - a.count),
1075
- filesChanged: Array.from(filesChanged),
1076
- patterns,
1077
- milestones
1078
- };
1079
- });
1080
- var moveSession = (sourceProject, sessionId, targetProject) => Effect3.gen(function* () {
942
+ var moveSession = (sourceProject, sessionId, targetProject) => Effect4.gen(function* () {
1081
943
  const sessionsDir = getSessionsDir();
1082
- const sourcePath = path4.join(sessionsDir, sourceProject);
1083
- const targetPath = path4.join(sessionsDir, targetProject);
1084
- const sourceFile = path4.join(sourcePath, `${sessionId}.jsonl`);
1085
- const targetFile = path4.join(targetPath, `${sessionId}.jsonl`);
1086
- const sourceExists = yield* Effect3.tryPromise(
1087
- () => 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)
1088
950
  );
1089
951
  if (!sourceExists) {
1090
952
  return { success: false, error: "Source session not found" };
1091
953
  }
1092
- const targetExists = yield* Effect3.tryPromise(
1093
- () => fs4.access(targetFile).then(() => true).catch(() => false)
954
+ const targetExists = yield* Effect4.tryPromise(
955
+ () => fs5.access(targetFile).then(() => true).catch(() => false)
1094
956
  );
1095
957
  if (targetExists) {
1096
958
  return { success: false, error: "Session already exists in target project" };
1097
959
  }
1098
- yield* Effect3.tryPromise(() => fs4.mkdir(targetPath, { recursive: true }));
960
+ yield* Effect4.tryPromise(() => fs5.mkdir(targetPath, { recursive: true }));
1099
961
  const linkedAgents = yield* findLinkedAgents(sourceProject, sessionId);
1100
- yield* Effect3.tryPromise(() => fs4.rename(sourceFile, targetFile));
962
+ yield* Effect4.tryPromise(() => fs5.rename(sourceFile, targetFile));
1101
963
  for (const agentId of linkedAgents) {
1102
- const sourceAgentFile = path4.join(sourcePath, `${agentId}.jsonl`);
1103
- const targetAgentFile = path4.join(targetPath, `${agentId}.jsonl`);
1104
- const agentExists = yield* Effect3.tryPromise(
1105
- () => 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)
1106
968
  );
1107
969
  if (agentExists) {
1108
- yield* Effect3.tryPromise(() => fs4.rename(sourceAgentFile, targetAgentFile));
970
+ yield* Effect4.tryPromise(() => fs5.rename(sourceAgentFile, targetAgentFile));
1109
971
  }
1110
972
  }
1111
973
  return { success: true };
1112
974
  });
1113
- var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(function* () {
1114
- const projectPath = path4.join(getSessionsDir(), projectName);
1115
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
1116
- 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"));
1117
979
  const lines = content.trim().split("\n").filter(Boolean);
1118
980
  const allMessages = lines.map((line) => JSON.parse(line));
1119
981
  const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
@@ -1161,15 +1023,15 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
1161
1023
  updatedMovedMessages.unshift(clonedSummary);
1162
1024
  }
1163
1025
  const keptContent = keptMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1164
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, keptContent, "utf-8"));
1165
- 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`);
1166
1028
  const newContent = updatedMovedMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1167
- yield* Effect3.tryPromise(() => fs4.writeFile(newFilePath, newContent, "utf-8"));
1168
- 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));
1169
1031
  const agentJsonlFiles = agentFiles.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
1170
1032
  for (const agentFile of agentJsonlFiles) {
1171
- const agentPath = path4.join(projectPath, agentFile);
1172
- 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"));
1173
1035
  const agentLines = agentContent.trim().split("\n").filter(Boolean);
1174
1036
  if (agentLines.length === 0) continue;
1175
1037
  const firstAgentMsg = JSON.parse(agentLines[0]);
@@ -1184,7 +1046,7 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
1184
1046
  return JSON.stringify({ ...msg, sessionId: newSessionId });
1185
1047
  });
1186
1048
  const updatedAgentContent = updatedAgentMessages.join("\n") + "\n";
1187
- yield* Effect3.tryPromise(() => fs4.writeFile(agentPath, updatedAgentContent, "utf-8"));
1049
+ yield* Effect4.tryPromise(() => fs5.writeFile(agentPath, updatedAgentContent, "utf-8"));
1188
1050
  }
1189
1051
  }
1190
1052
  }
@@ -1196,281 +1058,70 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
1196
1058
  duplicatedSummary: shouldDuplicate
1197
1059
  };
1198
1060
  });
1199
- var cleanInvalidMessages = (projectName, sessionId) => Effect3.gen(function* () {
1200
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1201
- 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"));
1202
1070
  const lines = content.trim().split("\n").filter(Boolean);
1203
- if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
1204
1071
  const messages = lines.map((line) => JSON.parse(line));
1205
- const invalidIndices = [];
1206
- messages.forEach((msg, idx) => {
1207
- if (isInvalidApiKeyMessage(msg)) {
1208
- 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
+ }
1086
+ }
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
+ }
1209
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 ?? "");
1210
1117
  });
1211
- if (invalidIndices.length === 0) {
1212
- const userAssistantCount = messages.filter(
1213
- (m) => m.type === "user" || m.type === "assistant"
1214
- ).length;
1215
- const hasSummary2 = messages.some((m) => m.type === "summary");
1216
- const remainingCount2 = userAssistantCount > 0 ? userAssistantCount : hasSummary2 ? 1 : 0;
1217
- return { removedCount: 0, remainingCount: remainingCount2 };
1218
- }
1219
- const filtered = [];
1220
- let lastValidUuid = null;
1221
- for (let i = 0; i < messages.length; i++) {
1222
- if (invalidIndices.includes(i)) {
1223
- continue;
1224
- }
1225
- const msg = messages[i];
1226
- if (msg.parentUuid && invalidIndices.some((idx) => messages[idx]?.uuid === msg.parentUuid)) {
1227
- msg.parentUuid = lastValidUuid;
1228
- }
1229
- filtered.push(msg);
1230
- lastValidUuid = msg.uuid;
1231
- }
1232
- const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
1233
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1234
- const remainingUserAssistant = filtered.filter(
1235
- (m) => m.type === "user" || m.type === "assistant"
1236
- ).length;
1237
- const hasSummary = filtered.some((m) => m.type === "summary");
1238
- const remainingCount = remainingUserAssistant > 0 ? remainingUserAssistant : hasSummary ? 1 : 0;
1239
- return { removedCount: invalidIndices.length, remainingCount };
1240
- });
1241
- var previewCleanup = (projectName) => Effect3.gen(function* () {
1242
- const projects = yield* listProjects;
1243
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1244
- const orphanTodos = yield* findOrphanTodos();
1245
- const orphanTodoCount = orphanTodos.length;
1246
- const results = yield* Effect3.all(
1247
- targetProjects.map(
1248
- (project) => Effect3.gen(function* () {
1249
- const sessions = yield* listSessions(project.name);
1250
- const emptySessions = sessions.filter((s) => s.messageCount === 0);
1251
- const invalidSessions = sessions.filter(
1252
- (s) => s.title?.includes("Invalid API key") || s.title?.includes("API key")
1253
- );
1254
- let emptyWithTodosCount = 0;
1255
- for (const session of emptySessions) {
1256
- const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1257
- const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1258
- if (hasTodos) {
1259
- emptyWithTodosCount++;
1260
- }
1261
- }
1262
- const orphanAgents = yield* findOrphanAgents(project.name);
1263
- return {
1264
- project: project.name,
1265
- emptySessions,
1266
- invalidSessions,
1267
- emptyWithTodosCount,
1268
- orphanAgentCount: orphanAgents.length,
1269
- orphanTodoCount: 0
1270
- // Will set for first project only
1271
- };
1272
- })
1273
- ),
1274
- { concurrency: 5 }
1275
- );
1276
- if (results.length > 0) {
1277
- results[0] = { ...results[0], orphanTodoCount };
1278
- }
1279
- return results;
1280
- });
1281
- var clearSessions = (options) => Effect3.gen(function* () {
1282
- const {
1283
- projectName,
1284
- clearEmpty = true,
1285
- clearInvalid = true,
1286
- skipWithTodos = true,
1287
- clearOrphanAgents = true,
1288
- clearOrphanTodos = false
1289
- } = options;
1290
- const projects = yield* listProjects;
1291
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1292
- let deletedSessionCount = 0;
1293
- let removedMessageCount = 0;
1294
- let deletedOrphanAgentCount = 0;
1295
- let deletedOrphanTodoCount = 0;
1296
- const sessionsToDelete = [];
1297
- if (clearInvalid) {
1298
- for (const project of targetProjects) {
1299
- const projectPath = path4.join(getSessionsDir(), project.name);
1300
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1301
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1302
- for (const file of sessionFiles) {
1303
- const sessionId = file.replace(".jsonl", "");
1304
- const result = yield* cleanInvalidMessages(project.name, sessionId);
1305
- removedMessageCount += result.removedCount;
1306
- if (result.remainingCount === 0) {
1307
- sessionsToDelete.push({ project: project.name, sessionId });
1308
- }
1309
- }
1310
- }
1311
- }
1312
- if (clearEmpty) {
1313
- for (const project of targetProjects) {
1314
- const sessions = yield* listSessions(project.name);
1315
- for (const session of sessions) {
1316
- if (session.messageCount === 0) {
1317
- const alreadyMarked = sessionsToDelete.some(
1318
- (s) => s.project === project.name && s.sessionId === session.id
1319
- );
1320
- if (!alreadyMarked) {
1321
- if (skipWithTodos) {
1322
- const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1323
- const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1324
- if (hasTodos) continue;
1325
- }
1326
- sessionsToDelete.push({ project: project.name, sessionId: session.id });
1327
- }
1328
- }
1329
- }
1330
- }
1331
- }
1332
- for (const { project, sessionId } of sessionsToDelete) {
1333
- yield* deleteSession(project, sessionId);
1334
- deletedSessionCount++;
1335
- }
1336
- if (clearOrphanAgents) {
1337
- for (const project of targetProjects) {
1338
- const result = yield* deleteOrphanAgents(project.name);
1339
- deletedOrphanAgentCount += result.count;
1340
- }
1341
- }
1342
- if (clearOrphanTodos) {
1343
- const result = yield* deleteOrphanTodos();
1344
- deletedOrphanTodoCount = result.deletedCount;
1345
- }
1346
- return {
1347
- success: true,
1348
- deletedCount: deletedSessionCount,
1349
- removedMessageCount,
1350
- deletedOrphanAgentCount,
1351
- deletedOrphanTodoCount
1352
- };
1353
- });
1354
- var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1355
- const { projectName, searchContent = false } = options;
1356
- const results = [];
1357
- const queryLower = query.toLowerCase();
1358
- const projects = yield* listProjects;
1359
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1360
- for (const project of targetProjects) {
1361
- const sessions = yield* listSessions(project.name);
1362
- for (const session of sessions) {
1363
- const titleLower = (session.title ?? "").toLowerCase();
1364
- if (titleLower.includes(queryLower)) {
1365
- results.push({
1366
- sessionId: session.id,
1367
- projectName: project.name,
1368
- title: session.title ?? "Untitled",
1369
- matchType: "title",
1370
- timestamp: session.updatedAt
1371
- });
1372
- }
1373
- }
1374
- }
1375
- if (searchContent) {
1376
- for (const project of targetProjects) {
1377
- const projectPath = path4.join(getSessionsDir(), project.name);
1378
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1379
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1380
- for (const file of sessionFiles) {
1381
- const sessionId = file.replace(".jsonl", "");
1382
- if (results.some((r) => r.sessionId === sessionId && r.projectName === project.name)) {
1383
- continue;
1384
- }
1385
- const filePath = path4.join(projectPath, file);
1386
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1387
- const lines = content.trim().split("\n").filter(Boolean);
1388
- for (const line of lines) {
1389
- try {
1390
- const msg = JSON.parse(line);
1391
- if (msg.type !== "user" && msg.type !== "assistant") continue;
1392
- const text = extractTextContent(msg.message);
1393
- const textLower = text.toLowerCase();
1394
- if (textLower.includes(queryLower)) {
1395
- const matchIndex = textLower.indexOf(queryLower);
1396
- const start = Math.max(0, matchIndex - 50);
1397
- const end = Math.min(text.length, matchIndex + query.length + 50);
1398
- const snippet = (start > 0 ? "..." : "") + text.slice(start, end).trim() + (end < text.length ? "..." : "");
1399
- results.push({
1400
- sessionId,
1401
- projectName: project.name,
1402
- title: extractTitle(extractTextContent(msg.message)) || `Session ${sessionId.slice(0, 8)}`,
1403
- matchType: "content",
1404
- snippet,
1405
- messageUuid: msg.uuid,
1406
- timestamp: msg.timestamp
1407
- });
1408
- break;
1409
- }
1410
- } catch {
1411
- }
1412
- }
1413
- }
1414
- }
1415
- }
1416
- return results.sort((a, b) => {
1417
- const dateA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
1418
- const dateB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
1419
- return dateB - dateA;
1420
- });
1421
- });
1422
- var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession) => Effect3.gen(function* () {
1423
- const projectPath = path4.join(getSessionsDir(), projectName);
1424
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
1425
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1426
- const lines = content.trim().split("\n").filter(Boolean);
1427
- const messages = lines.map((line) => JSON.parse(line));
1428
- let summaries;
1429
- if (summariesByTargetSession) {
1430
- summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort(
1431
- (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
1432
- );
1433
- } else {
1434
- summaries = [];
1435
- const sessionUuids = /* @__PURE__ */ new Set();
1436
- for (const msg of messages) {
1437
- if (msg.uuid && typeof msg.uuid === "string") {
1438
- sessionUuids.add(msg.uuid);
1439
- }
1440
- }
1441
- const projectFiles = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1442
- const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1443
- for (const file of allJsonlFiles) {
1444
- try {
1445
- const otherFilePath = path4.join(projectPath, file);
1446
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
1447
- const otherLines = otherContent.trim().split("\n").filter(Boolean);
1448
- for (const line of otherLines) {
1449
- try {
1450
- const msg = JSON.parse(line);
1451
- if (msg.type === "summary" && typeof msg.summary === "string" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
1452
- const targetMsg = messages.find((m) => m.uuid === msg.leafUuid);
1453
- summaries.push({
1454
- summary: msg.summary,
1455
- leafUuid: msg.leafUuid,
1456
- timestamp: targetMsg?.timestamp ?? msg.timestamp
1457
- });
1458
- }
1459
- } catch {
1460
- }
1461
- }
1462
- } catch {
1463
- }
1464
- }
1465
- }
1466
- summaries.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
1467
- let lastCompactBoundaryUuid;
1468
- for (let i = messages.length - 1; i >= 0; i--) {
1469
- const msg = messages[i];
1470
- if (msg.type === "system" && msg.subtype === "compact_boundary") {
1471
- lastCompactBoundaryUuid = msg.uuid;
1472
- break;
1473
- }
1118
+ let lastCompactBoundaryUuid;
1119
+ for (let i = messages.length - 1; i >= 0; i--) {
1120
+ const msg = messages[i];
1121
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
1122
+ lastCompactBoundaryUuid = msg.uuid;
1123
+ break;
1124
+ }
1474
1125
  }
1475
1126
  const firstUserMsg = messages.find((m) => m.type === "user");
1476
1127
  const customTitleMsg = messages.find((m) => m.type === "custom-title");
@@ -1484,9 +1135,9 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1484
1135
  const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1485
1136
  const agents = [];
1486
1137
  for (const agentId of linkedAgentIds) {
1487
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
1138
+ const agentPath = path6.join(projectPath, `${agentId}.jsonl`);
1488
1139
  try {
1489
- const agentContent = yield* Effect3.tryPromise(() => fs4.readFile(agentPath, "utf-8"));
1140
+ const agentContent = yield* Effect5.tryPromise(() => fs6.readFile(agentPath, "utf-8"));
1490
1141
  const agentLines = agentContent.trim().split("\n").filter(Boolean);
1491
1142
  const agentMsgs = agentLines.map((l) => JSON.parse(l));
1492
1143
  const agentUserAssistant = agentMsgs.filter(
@@ -1522,6 +1173,7 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1522
1173
  messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : summaries.length > 0 ? 1 : 0,
1523
1174
  createdAt: firstMessage?.timestamp ?? void 0,
1524
1175
  updatedAt: lastMessage?.timestamp ?? void 0,
1176
+ fileMtime,
1525
1177
  summaries,
1526
1178
  agents,
1527
1179
  todos,
@@ -1529,24 +1181,40 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1529
1181
  };
1530
1182
  });
1531
1183
  var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1532
- var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1184
+ var DEFAULT_SORT = { field: "summary", order: "desc" };
1185
+ var loadProjectTreeData = (projectName, sortOptions) => Effect5.gen(function* () {
1533
1186
  const project = (yield* listProjects).find((p) => p.name === projectName);
1534
1187
  if (!project) {
1535
1188
  return null;
1536
1189
  }
1537
- const projectPath = path4.join(getSessionsDir(), projectName);
1538
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1190
+ const sort = sortOptions ?? DEFAULT_SORT;
1191
+ const projectPath = path6.join(getSessionsDir(), projectName);
1192
+ const files = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1539
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
+ );
1540
1208
  const globalUuidMap = /* @__PURE__ */ new Map();
1541
1209
  const allSummaries = [];
1542
1210
  const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1543
- yield* Effect3.all(
1211
+ yield* Effect5.all(
1544
1212
  allJsonlFiles.map(
1545
- (file) => Effect3.gen(function* () {
1546
- const filePath = path4.join(projectPath, file);
1213
+ (file) => Effect5.gen(function* () {
1214
+ const filePath = path6.join(projectPath, file);
1547
1215
  const fileSessionId = file.replace(".jsonl", "");
1548
1216
  try {
1549
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1217
+ const content = yield* Effect5.tryPromise(() => fs6.readFile(filePath, "utf-8"));
1550
1218
  const lines = content.trim().split("\n").filter(Boolean);
1551
1219
  for (const line of lines) {
1552
1220
  try {
@@ -1567,7 +1235,8 @@ var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1567
1235
  allSummaries.push({
1568
1236
  summary: msg.summary,
1569
1237
  leafUuid: msg.leafUuid,
1570
- timestamp: msg.timestamp
1238
+ timestamp: msg.timestamp,
1239
+ sourceFile: file
1571
1240
  });
1572
1241
  }
1573
1242
  } catch {
@@ -1591,22 +1260,60 @@ var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1591
1260
  summariesByTargetSession.get(targetSessionId).push({
1592
1261
  summary: summaryData.summary,
1593
1262
  leafUuid: summaryData.leafUuid,
1594
- timestamp: targetInfo.timestamp ?? summaryData.timestamp
1263
+ // Use summary's own timestamp for sorting, not the target message's timestamp
1264
+ timestamp: summaryData.timestamp ?? targetInfo.timestamp,
1265
+ sourceFile: summaryData.sourceFile
1595
1266
  });
1596
1267
  }
1597
1268
  }
1598
1269
  }
1599
- const sessions = yield* Effect3.all(
1270
+ const sessions = yield* Effect5.all(
1600
1271
  sessionFiles.map((file) => {
1601
1272
  const sessionId = file.replace(".jsonl", "");
1602
- return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession);
1273
+ const mtime = fileMtimes.get(sessionId);
1274
+ return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1603
1275
  }),
1604
1276
  { concurrency: 10 }
1605
1277
  );
1606
1278
  const sortedSessions = sessions.sort((a, b) => {
1607
- const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
1608
- const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
1609
- return dateB - dateA;
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;
1610
1317
  });
1611
1318
  const filteredSessions = sortedSessions.filter((s) => {
1612
1319
  if (isErrorSessionTitle(s.title)) return false;
@@ -1622,31 +1329,146 @@ var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1622
1329
  sessions: filteredSessions
1623
1330
  };
1624
1331
  });
1625
- var updateSessionSummary = (projectName, sessionId, newSummary) => Effect3.gen(function* () {
1626
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1627
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1628
- const lines = content.trim().split("\n").filter(Boolean);
1629
- const messages = lines.map((line) => JSON.parse(line));
1630
- const summaryIdx = messages.findIndex((m) => m.type === "summary");
1631
- if (summaryIdx >= 0) {
1632
- messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
1633
- } else {
1634
- const firstUserMsg = messages.find((m) => m.type === "user");
1635
- const summaryMsg = {
1636
- type: "summary",
1637
- summary: newSummary,
1638
- leafUuid: firstUserMsg?.uuid ?? null
1639
- };
1640
- messages.unshift(summaryMsg);
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
+ }
1641
1398
  }
1642
- const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1643
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1644
- return { success: true };
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
+ };
1645
1467
  });
1646
- var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(function* () {
1468
+ var compressSession = (projectName, sessionId, options = {}) => Effect6.gen(function* () {
1647
1469
  const { keepSnapshots = "first_last", maxToolOutputLength = 5e3 } = options;
1648
- const filePath = path4.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1649
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1470
+ const filePath = path7.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1471
+ const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1650
1472
  const originalSize = Buffer.byteLength(content, "utf-8");
1651
1473
  const lines = content.trim().split("\n").filter(Boolean);
1652
1474
  const messages = lines.map((line) => JSON.parse(line));
@@ -1689,7 +1511,7 @@ var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(func
1689
1511
  }
1690
1512
  const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1691
1513
  const compressedSize = Buffer.byteLength(newContent, "utf-8");
1692
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1514
+ yield* Effect6.tryPromise(() => fs7.writeFile(filePath, newContent, "utf-8"));
1693
1515
  return {
1694
1516
  success: true,
1695
1517
  originalSize,
@@ -1698,12 +1520,12 @@ var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(func
1698
1520
  truncatedOutputs
1699
1521
  };
1700
1522
  });
1701
- var extractProjectKnowledge = (projectName, sessionIds) => Effect3.gen(function* () {
1523
+ var extractProjectKnowledge = (projectName, sessionIds) => Effect6.gen(function* () {
1702
1524
  const sessionsDir = getSessionsDir();
1703
- const projectDir = path4.join(sessionsDir, projectName);
1525
+ const projectDir = path7.join(sessionsDir, projectName);
1704
1526
  let targetSessionIds = sessionIds;
1705
1527
  if (!targetSessionIds) {
1706
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectDir));
1528
+ const files = yield* Effect6.tryPromise(() => fs7.readdir(projectDir));
1707
1529
  targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
1708
1530
  }
1709
1531
  const fileModifyCount = /* @__PURE__ */ new Map();
@@ -1770,7 +1592,14 @@ var extractProjectKnowledge = (projectName, sessionIds) => Effect3.gen(function*
1770
1592
  decisions: decisions.slice(0, 20)
1771
1593
  };
1772
1594
  });
1773
- var summarizeSession = (projectName, sessionId, options = {}) => Effect3.gen(function* () {
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* () {
1774
1603
  const { limit = 50, maxLength = 100 } = options;
1775
1604
  const messages = yield* readSession(projectName, sessionId);
1776
1605
  const lines = [];
@@ -1820,13 +1649,338 @@ var summarizeSession = (projectName, sessionId, options = {}) => Effect3.gen(fun
1820
1649
  formatted
1821
1650
  };
1822
1651
  });
1823
- function truncateText(text, maxLen) {
1824
- const cleaned = text.replace(/\n/g, " ");
1825
- if (cleaned.length > maxLen) {
1826
- return cleaned.slice(0, maxLen) + "...";
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 };
1827
1676
  }
1828
- return cleaned;
1829
- }
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
1747
+ } = options;
1748
+ const projects = yield* listProjects;
1749
+ const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1750
+ let deletedSessionCount = 0;
1751
+ let removedMessageCount = 0;
1752
+ let deletedOrphanAgentCount = 0;
1753
+ let deletedOrphanTodoCount = 0;
1754
+ const sessionsToDelete = [];
1755
+ if (clearInvalid) {
1756
+ for (const project of targetProjects) {
1757
+ const projectPath = path8.join(getSessionsDir(), project.name);
1758
+ const files = yield* Effect7.tryPromise(() => fs8.readdir(projectPath));
1759
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1760
+ for (const file of sessionFiles) {
1761
+ const sessionId = file.replace(".jsonl", "");
1762
+ const result = yield* cleanInvalidMessages(project.name, sessionId);
1763
+ removedMessageCount += result.removedCount;
1764
+ if (result.remainingCount === 0) {
1765
+ sessionsToDelete.push({ project: project.name, sessionId });
1766
+ }
1767
+ }
1768
+ }
1769
+ }
1770
+ if (clearEmpty) {
1771
+ for (const project of targetProjects) {
1772
+ const sessions = yield* listSessions(project.name);
1773
+ for (const session of sessions) {
1774
+ if (session.messageCount === 0) {
1775
+ const alreadyMarked = sessionsToDelete.some(
1776
+ (s) => s.project === project.name && s.sessionId === session.id
1777
+ );
1778
+ if (!alreadyMarked) {
1779
+ if (skipWithTodos) {
1780
+ const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1781
+ const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1782
+ if (hasTodos) continue;
1783
+ }
1784
+ sessionsToDelete.push({ project: project.name, sessionId: session.id });
1785
+ }
1786
+ }
1787
+ }
1788
+ }
1789
+ }
1790
+ for (const { project, sessionId } of sessionsToDelete) {
1791
+ yield* deleteSession(project, sessionId);
1792
+ deletedSessionCount++;
1793
+ }
1794
+ if (clearOrphanAgents) {
1795
+ for (const project of targetProjects) {
1796
+ const result = yield* deleteOrphanAgents(project.name);
1797
+ deletedOrphanAgentCount += result.count;
1798
+ }
1799
+ }
1800
+ if (clearOrphanTodos) {
1801
+ const result = yield* deleteOrphanTodos();
1802
+ deletedOrphanTodoCount = result.deletedCount;
1803
+ }
1804
+ return {
1805
+ success: true,
1806
+ deletedCount: deletedSessionCount,
1807
+ removedMessageCount,
1808
+ deletedOrphanAgentCount,
1809
+ deletedOrphanTodoCount
1810
+ };
1811
+ });
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* () {
1818
+ const { projectName, searchContent = false } = options;
1819
+ const results = [];
1820
+ const queryLower = query.toLowerCase();
1821
+ const projects = yield* listProjects;
1822
+ const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1823
+ for (const project of targetProjects) {
1824
+ const sessions = yield* listSessions(project.name);
1825
+ for (const session of sessions) {
1826
+ const titleLower = (session.title ?? "").toLowerCase();
1827
+ if (titleLower.includes(queryLower)) {
1828
+ results.push({
1829
+ sessionId: session.id,
1830
+ projectName: project.name,
1831
+ title: session.title ?? "Untitled",
1832
+ matchType: "title",
1833
+ timestamp: session.updatedAt
1834
+ });
1835
+ }
1836
+ }
1837
+ }
1838
+ if (searchContent) {
1839
+ for (const project of targetProjects) {
1840
+ const projectPath = path9.join(getSessionsDir(), project.name);
1841
+ const files = yield* Effect8.tryPromise(() => fs9.readdir(projectPath));
1842
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1843
+ for (const file of sessionFiles) {
1844
+ const sessionId = file.replace(".jsonl", "");
1845
+ if (results.some((r) => r.sessionId === sessionId && r.projectName === project.name)) {
1846
+ continue;
1847
+ }
1848
+ const filePath = path9.join(projectPath, file);
1849
+ const content = yield* Effect8.tryPromise(() => fs9.readFile(filePath, "utf-8"));
1850
+ const lines = content.trim().split("\n").filter(Boolean);
1851
+ for (const line of lines) {
1852
+ try {
1853
+ const msg = JSON.parse(line);
1854
+ if (msg.type !== "user" && msg.type !== "assistant") continue;
1855
+ const text = extractTextContent(msg.message);
1856
+ const textLower = text.toLowerCase();
1857
+ if (textLower.includes(queryLower)) {
1858
+ const matchIndex = textLower.indexOf(queryLower);
1859
+ const start = Math.max(0, matchIndex - 50);
1860
+ const end = Math.min(text.length, matchIndex + query.length + 50);
1861
+ const snippet = (start > 0 ? "..." : "") + text.slice(start, end).trim() + (end < text.length ? "..." : "");
1862
+ results.push({
1863
+ sessionId,
1864
+ projectName: project.name,
1865
+ title: extractTitle(extractTextContent(msg.message)) || `Session ${sessionId.slice(0, 8)}`,
1866
+ matchType: "content",
1867
+ snippet,
1868
+ messageUuid: msg.uuid,
1869
+ timestamp: msg.timestamp
1870
+ });
1871
+ break;
1872
+ }
1873
+ } catch {
1874
+ }
1875
+ }
1876
+ }
1877
+ }
1878
+ }
1879
+ return results.sort((a, b) => {
1880
+ const dateA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
1881
+ const dateB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
1882
+ return dateB - dateA;
1883
+ });
1884
+ });
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
+ });
1906
+ }
1907
+ }
1908
+ }
1909
+ }
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
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+ }
1933
+ return {
1934
+ sessionId,
1935
+ projectName,
1936
+ files: fileChanges,
1937
+ totalChanges: fileChanges.length
1938
+ };
1939
+ });
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 {
1952
+ return null;
1953
+ }
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;
1973
+ });
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));
1979
+ return true;
1980
+ } catch {
1981
+ return false;
1982
+ }
1983
+ });
1830
1984
  export {
1831
1985
  analyzeSession,
1832
1986
  clearSessions,
@@ -1849,11 +2003,14 @@ export {
1849
2003
  folderNameToDisplayPath,
1850
2004
  folderNameToPath,
1851
2005
  getDisplayTitle,
2006
+ getIndexEntryDisplayTitle,
1852
2007
  getLogger,
1853
2008
  getRealPathFromSession,
1854
2009
  getSessionFiles,
2010
+ getSessionSortTimestamp,
1855
2011
  getSessionsDir,
1856
2012
  getTodosDir,
2013
+ hasSessionsIndex,
1857
2014
  isContinuationSummary,
1858
2015
  isInvalidApiKeyMessage,
1859
2016
  listProjects,
@@ -1861,6 +2018,7 @@ export {
1861
2018
  loadAgentMessages,
1862
2019
  loadProjectTreeData,
1863
2020
  loadSessionTreeData,
2021
+ loadSessionsIndex,
1864
2022
  maskHomePath,
1865
2023
  moveSession,
1866
2024
  pathToFolderName,
@@ -1871,6 +2029,7 @@ export {
1871
2029
  searchSessions,
1872
2030
  sessionHasTodos,
1873
2031
  setLogger,
2032
+ sortIndexEntriesByModified,
1874
2033
  sortProjects,
1875
2034
  splitSession,
1876
2035
  summarizeSession,