@claude-sessions/core 0.3.7 → 0.4.1

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
@@ -178,14 +178,19 @@ var extractTextContent = (message) => {
178
178
  }
179
179
  return "";
180
180
  };
181
+ var parseCommandMessage = (content) => {
182
+ const name = content?.match(/<command-name>([^<]+)<\/command-name>/)?.[1] ?? "";
183
+ const message = content?.match(/<command-message>([^<]+)<\/command-message>/)?.[1] ?? "";
184
+ return { name, message };
185
+ };
181
186
  var extractTitle = (text) => {
182
187
  if (!text) return "Untitled";
188
+ const { name } = parseCommandMessage(text);
189
+ if (name) return name;
183
190
  let cleaned = text.replace(/<ide_[^>]*>[\s\S]*?<\/ide_[^>]*>/g, "").trim();
184
191
  if (!cleaned) return "Untitled";
185
192
  if (cleaned.includes("\n\n")) {
186
193
  cleaned = cleaned.split("\n\n")[0];
187
- } else if (cleaned.includes("\n")) {
188
- cleaned = cleaned.split("\n")[0];
189
194
  }
190
195
  if (cleaned.length > 100) {
191
196
  return cleaned.slice(0, 100) + "...";
@@ -218,7 +223,13 @@ var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallb
218
223
  if (currentSummary) {
219
224
  return currentSummary.length > maxLength ? currentSummary.slice(0, maxLength - 3) + "..." : currentSummary;
220
225
  }
221
- if (title && title !== "Untitled") return title;
226
+ if (title && title !== "Untitled") {
227
+ if (title.includes("<command-name>")) {
228
+ const { name } = parseCommandMessage(title);
229
+ if (name) return name;
230
+ }
231
+ return title;
232
+ }
222
233
  return fallback;
223
234
  };
224
235
  var replaceMessageContent = (msg, text) => ({
@@ -272,6 +283,10 @@ var sortProjects = (projects, options = {}) => {
272
283
  return a.displayName.localeCompare(b.displayName);
273
284
  });
274
285
  };
286
+ var getSessionSortTimestamp = (session) => {
287
+ const timestampStr = session.summaries?.[0]?.timestamp ?? session.createdAt;
288
+ return timestampStr ? new Date(timestampStr).getTime() : 0;
289
+ };
275
290
 
276
291
  // src/agents.ts
277
292
  import { Effect } from "effect";
@@ -334,8 +349,8 @@ var findOrphanAgentsWithPaths = (projectName) => Effect.gen(function* () {
334
349
  }
335
350
  for (const entry of files) {
336
351
  const entryPath = path2.join(projectPath, entry);
337
- const stat3 = yield* Effect.tryPromise(() => fs2.stat(entryPath).catch(() => null));
338
- if (stat3?.isDirectory() && !entry.startsWith(".")) {
352
+ const stat4 = yield* Effect.tryPromise(() => fs2.stat(entryPath).catch(() => null));
353
+ if (stat4?.isDirectory() && !entry.startsWith(".")) {
339
354
  const subagentsPath = path2.join(entryPath, "subagents");
340
355
  const subagentsExists = yield* Effect.tryPromise(
341
356
  () => fs2.stat(subagentsPath).then(() => true).catch(() => false)
@@ -449,12 +464,7 @@ var findLinkedTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
449
464
  () => fs3.access(todosDir).then(() => true).catch(() => false)
450
465
  );
451
466
  if (!exists) {
452
- return {
453
- sessionId,
454
- sessionTodos: [],
455
- agentTodos: [],
456
- hasTodos: false
457
- };
467
+ return void 0;
458
468
  }
459
469
  const sessionTodoPath = path3.join(todosDir, `${sessionId}.json`);
460
470
  let sessionTodos = [];
@@ -635,8 +645,8 @@ var deleteOrphanTodos = () => Effect2.gen(function* () {
635
645
  return { success: true, deletedCount };
636
646
  });
637
647
 
638
- // src/session.ts
639
- import { Effect as Effect3, pipe, Array as A, Option as O } from "effect";
648
+ // src/session/projects.ts
649
+ import { Effect as Effect3 } from "effect";
640
650
  import * as fs4 from "fs/promises";
641
651
  import * as path4 from "path";
642
652
  var listProjects = Effect3.gen(function* () {
@@ -666,15 +676,213 @@ var listProjects = Effect3.gen(function* () {
666
676
  );
667
677
  return projects;
668
678
  });
669
- var listSessions = (projectName) => Effect3.gen(function* () {
670
- const projectPath = path4.join(getSessionsDir(), projectName);
671
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
679
+
680
+ // src/session/crud.ts
681
+ import { Effect as Effect4, pipe, Array as A, Option as O } from "effect";
682
+ import * as fs5 from "fs/promises";
683
+ import * as path5 from "path";
684
+ import * as crypto from "crypto";
685
+
686
+ // src/session/validation.ts
687
+ function validateChain(messages) {
688
+ const errors = [];
689
+ const uuids = /* @__PURE__ */ new Set();
690
+ for (const msg of messages) {
691
+ if (msg.uuid) {
692
+ uuids.add(msg.uuid);
693
+ }
694
+ }
695
+ let foundFirstMessage = false;
696
+ for (let i = 0; i < messages.length; i++) {
697
+ const msg = messages[i];
698
+ if (msg.type === "file-history-snapshot") {
699
+ continue;
700
+ }
701
+ if (!msg.uuid) {
702
+ continue;
703
+ }
704
+ if (!foundFirstMessage) {
705
+ foundFirstMessage = true;
706
+ if (msg.parentUuid === null) {
707
+ continue;
708
+ }
709
+ if (msg.parentUuid === void 0) {
710
+ errors.push({
711
+ type: "broken_chain",
712
+ uuid: msg.uuid,
713
+ line: i + 1,
714
+ parentUuid: null
715
+ });
716
+ continue;
717
+ }
718
+ }
719
+ if (msg.parentUuid === null || msg.parentUuid === void 0) {
720
+ errors.push({
721
+ type: "broken_chain",
722
+ uuid: msg.uuid,
723
+ line: i + 1,
724
+ parentUuid: null
725
+ });
726
+ continue;
727
+ }
728
+ if (!uuids.has(msg.parentUuid)) {
729
+ errors.push({
730
+ type: "orphan_parent",
731
+ uuid: msg.uuid,
732
+ line: i + 1,
733
+ parentUuid: msg.parentUuid
734
+ });
735
+ }
736
+ }
737
+ return {
738
+ valid: errors.length === 0,
739
+ errors
740
+ };
741
+ }
742
+ function validateToolUseResult(messages) {
743
+ const errors = [];
744
+ const toolUseIds = /* @__PURE__ */ new Set();
745
+ for (const msg of messages) {
746
+ const content = msg.message?.content;
747
+ if (Array.isArray(content)) {
748
+ for (const item of content) {
749
+ if (item.type === "tool_use" && item.id) {
750
+ toolUseIds.add(item.id);
751
+ }
752
+ }
753
+ }
754
+ }
755
+ for (let i = 0; i < messages.length; i++) {
756
+ const msg = messages[i];
757
+ const content = msg.message?.content;
758
+ if (!Array.isArray(content)) {
759
+ continue;
760
+ }
761
+ for (const item of content) {
762
+ if (item.type === "tool_result" && item.tool_use_id) {
763
+ if (!toolUseIds.has(item.tool_use_id)) {
764
+ errors.push({
765
+ type: "orphan_tool_result",
766
+ uuid: msg.uuid || "",
767
+ line: i + 1,
768
+ toolUseId: item.tool_use_id
769
+ });
770
+ }
771
+ }
772
+ }
773
+ }
774
+ return {
775
+ valid: errors.length === 0,
776
+ errors
777
+ };
778
+ }
779
+ function deleteMessageWithChainRepair(messages, targetId, targetType) {
780
+ let targetIndex = -1;
781
+ if (targetType === "file-history-snapshot") {
782
+ targetIndex = messages.findIndex(
783
+ (m) => m.type === "file-history-snapshot" && m.messageId === targetId
784
+ );
785
+ } else if (targetType === "summary") {
786
+ targetIndex = messages.findIndex(
787
+ (m) => m.leafUuid === targetId
788
+ );
789
+ } else {
790
+ targetIndex = messages.findIndex((m) => m.uuid === targetId);
791
+ if (targetIndex === -1) {
792
+ targetIndex = messages.findIndex(
793
+ (m) => m.leafUuid === targetId
794
+ );
795
+ }
796
+ if (targetIndex === -1) {
797
+ targetIndex = messages.findIndex(
798
+ (m) => m.type === "file-history-snapshot" && m.messageId === targetId
799
+ );
800
+ }
801
+ }
802
+ if (targetIndex === -1) {
803
+ return { deleted: null, alsoDeleted: [] };
804
+ }
805
+ const deletedMsg = messages[targetIndex];
806
+ const toolUseIds = [];
807
+ if (deletedMsg.type === "assistant") {
808
+ const content = deletedMsg.message?.content;
809
+ if (Array.isArray(content)) {
810
+ for (const item of content) {
811
+ if (item.type === "tool_use" && item.id) {
812
+ toolUseIds.push(item.id);
813
+ }
814
+ }
815
+ }
816
+ }
817
+ const toolResultIndices = [];
818
+ if (toolUseIds.length > 0) {
819
+ for (let i = 0; i < messages.length; i++) {
820
+ const msg = messages[i];
821
+ if (msg.type === "user") {
822
+ const content = msg.message?.content;
823
+ if (Array.isArray(content)) {
824
+ for (const item of content) {
825
+ if (item.type === "tool_result" && item.tool_use_id && toolUseIds.includes(item.tool_use_id)) {
826
+ toolResultIndices.push(i);
827
+ break;
828
+ }
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+ const indicesToDelete = [targetIndex, ...toolResultIndices].sort((a, b) => b - a);
835
+ for (const idx of indicesToDelete) {
836
+ const msg = messages[idx];
837
+ const isInParentChain = msg.type !== "file-history-snapshot" && msg.uuid;
838
+ if (isInParentChain) {
839
+ const deletedUuid = msg.uuid;
840
+ const parentUuid = msg.parentUuid;
841
+ for (const m of messages) {
842
+ if (m.parentUuid === deletedUuid) {
843
+ m.parentUuid = parentUuid;
844
+ }
845
+ }
846
+ }
847
+ }
848
+ const alsoDeleted = toolResultIndices.map((i) => messages[i]);
849
+ for (const idx of indicesToDelete) {
850
+ messages.splice(idx, 1);
851
+ }
852
+ return { deleted: deletedMsg, alsoDeleted };
853
+ }
854
+
855
+ // src/session/crud.ts
856
+ var updateSessionSummary = (projectName, sessionId, newSummary) => Effect4.gen(function* () {
857
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
858
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
859
+ const lines = content.trim().split("\n").filter(Boolean);
860
+ const messages = lines.map((line) => JSON.parse(line));
861
+ const summaryIdx = messages.findIndex((m) => m.type === "summary");
862
+ if (summaryIdx >= 0) {
863
+ messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
864
+ } else {
865
+ const firstUserMsg = messages.find((m) => m.type === "user");
866
+ const summaryMsg = {
867
+ type: "summary",
868
+ summary: newSummary,
869
+ leafUuid: firstUserMsg?.uuid ?? null
870
+ };
871
+ messages.unshift(summaryMsg);
872
+ }
873
+ const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
874
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
875
+ return { success: true };
876
+ });
877
+ var listSessions = (projectName) => Effect4.gen(function* () {
878
+ const projectPath = path5.join(getSessionsDir(), projectName);
879
+ const files = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
672
880
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
673
- const sessions = yield* Effect3.all(
881
+ const sessions = yield* Effect4.all(
674
882
  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"));
883
+ (file) => Effect4.gen(function* () {
884
+ const filePath = path5.join(projectPath, file);
885
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
678
886
  const lines = content.trim().split("\n").filter(Boolean);
679
887
  const messages = lines.map((line) => JSON.parse(line));
680
888
  const sessionId = file.replace(".jsonl", "");
@@ -693,10 +901,25 @@ var listSessions = (projectName) => Effect3.gen(function* () {
693
901
  }),
694
902
  O.getOrElse(() => hasSummary ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`)
695
903
  );
904
+ const currentSummary = pipe(
905
+ messages,
906
+ A.findFirst((m) => m.type === "summary"),
907
+ O.map((m) => m.summary),
908
+ O.getOrUndefined
909
+ );
910
+ const customTitle = pipe(
911
+ messages,
912
+ A.findFirst((m) => m.type === "custom-title"),
913
+ O.map((m) => m.customTitle),
914
+ O.flatMap(O.fromNullable),
915
+ O.getOrUndefined
916
+ );
696
917
  return {
697
918
  id: sessionId,
698
919
  projectName,
699
920
  title,
921
+ customTitle,
922
+ currentSummary,
700
923
  // If session has summary but no user/assistant messages, count as 1
701
924
  messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : hasSummary ? 1 : 0,
702
925
  createdAt: firstMessage?.timestamp,
@@ -712,39 +935,28 @@ var listSessions = (projectName) => Effect3.gen(function* () {
712
935
  return dateB - dateA;
713
936
  });
714
937
  });
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"));
938
+ var readSession = (projectName, sessionId) => Effect4.gen(function* () {
939
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
940
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
718
941
  const lines = content.trim().split("\n").filter(Boolean);
719
942
  return lines.map((line) => JSON.parse(line));
720
943
  });
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"));
944
+ var deleteMessage = (projectName, sessionId, messageUuid, targetType) => Effect4.gen(function* () {
945
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
946
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
724
947
  const lines = content.trim().split("\n").filter(Boolean);
725
948
  const messages = lines.map((line) => JSON.parse(line));
726
- const targetIndex = messages.findIndex(
727
- (m) => m.uuid === messageUuid || m.messageId === messageUuid || m.leafUuid === messageUuid
728
- );
729
- if (targetIndex === -1) {
949
+ const result = deleteMessageWithChainRepair(messages, messageUuid, targetType);
950
+ if (!result.deleted) {
730
951
  return { success: false, error: "Message not found" };
731
952
  }
732
- const deletedMsg = messages[targetIndex];
733
- const deletedUuid = deletedMsg?.uuid ?? deletedMsg?.messageId;
734
- const parentUuid = deletedMsg?.parentUuid;
735
- for (const msg of messages) {
736
- if (msg.parentUuid === deletedUuid) {
737
- msg.parentUuid = parentUuid;
738
- }
739
- }
740
- messages.splice(targetIndex, 1);
741
953
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
742
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
743
- return { success: true, deletedMessage: deletedMsg };
954
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
955
+ return { success: true, deletedMessage: result.deleted };
744
956
  });
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"));
957
+ var restoreMessage = (projectName, sessionId, message, index) => Effect4.gen(function* () {
958
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
959
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
748
960
  const lines = content.trim().split("\n").filter(Boolean);
749
961
  const messages = lines.map((line) => JSON.parse(line));
750
962
  const msgUuid = message.uuid ?? message.messageId;
@@ -761,41 +973,41 @@ var restoreMessage = (projectName, sessionId, message, index) => Effect3.gen(fun
761
973
  const insertIndex = Math.min(index, messages.length);
762
974
  messages.splice(insertIndex, 0, message);
763
975
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
764
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
976
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
765
977
  return { success: true };
766
978
  });
767
- var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
979
+ var deleteSession = (projectName, sessionId) => Effect4.gen(function* () {
768
980
  const sessionsDir = getSessionsDir();
769
- const projectPath = path4.join(sessionsDir, projectName);
770
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
981
+ const projectPath = path5.join(sessionsDir, projectName);
982
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
771
983
  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 }));
984
+ const stat4 = yield* Effect4.tryPromise(() => fs5.stat(filePath));
985
+ if (stat4.size === 0) {
986
+ yield* Effect4.tryPromise(() => fs5.unlink(filePath));
987
+ const agentBackupDir2 = path5.join(projectPath, ".bak");
988
+ yield* Effect4.tryPromise(() => fs5.mkdir(agentBackupDir2, { recursive: true }));
777
989
  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(() => {
990
+ const agentPath = path5.join(projectPath, `${agentId}.jsonl`);
991
+ const agentBackupPath = path5.join(agentBackupDir2, `${agentId}.jsonl`);
992
+ yield* Effect4.tryPromise(() => fs5.rename(agentPath, agentBackupPath).catch(() => {
781
993
  }));
782
994
  }
783
995
  yield* deleteLinkedTodos(sessionId, linkedAgents);
784
996
  return { success: true, deletedAgents: linkedAgents.length };
785
997
  }
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 }));
998
+ const backupDir = path5.join(sessionsDir, ".bak");
999
+ yield* Effect4.tryPromise(() => fs5.mkdir(backupDir, { recursive: true }));
1000
+ const agentBackupDir = path5.join(projectPath, ".bak");
1001
+ yield* Effect4.tryPromise(() => fs5.mkdir(agentBackupDir, { recursive: true }));
790
1002
  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(() => {
1003
+ const agentPath = path5.join(projectPath, `${agentId}.jsonl`);
1004
+ const agentBackupPath = path5.join(agentBackupDir, `${agentId}.jsonl`);
1005
+ yield* Effect4.tryPromise(() => fs5.rename(agentPath, agentBackupPath).catch(() => {
794
1006
  }));
795
1007
  }
796
1008
  const todosResult = yield* deleteLinkedTodos(sessionId, linkedAgents);
797
- const backupPath = path4.join(backupDir, `${projectName}_${sessionId}.jsonl`);
798
- yield* Effect3.tryPromise(() => fs4.rename(filePath, backupPath));
1009
+ const backupPath = path5.join(backupDir, `${projectName}_${sessionId}.jsonl`);
1010
+ yield* Effect4.tryPromise(() => fs5.rename(filePath, backupPath));
799
1011
  return {
800
1012
  success: true,
801
1013
  backupPath,
@@ -803,10 +1015,10 @@ var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
803
1015
  deletedTodos: todosResult.deletedCount
804
1016
  };
805
1017
  });
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"));
1018
+ var renameSession = (projectName, sessionId, newTitle) => Effect4.gen(function* () {
1019
+ const projectPath = path5.join(getSessionsDir(), projectName);
1020
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
1021
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
810
1022
  const lines = content.trim().split("\n").filter(Boolean);
811
1023
  if (lines.length === 0) {
812
1024
  return { success: false, error: "Empty session" };
@@ -830,14 +1042,14 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
830
1042
  messages.unshift(customTitleRecord);
831
1043
  }
832
1044
  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));
1045
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, newContent, "utf-8"));
1046
+ const projectFiles = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
835
1047
  const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
836
1048
  const summariesTargetingThis = [];
837
1049
  for (const file of allJsonlFiles) {
838
- const otherFilePath = path4.join(projectPath, file);
1050
+ const otherFilePath = path5.join(projectPath, file);
839
1051
  try {
840
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
1052
+ const otherContent = yield* Effect4.tryPromise(() => fs5.readFile(otherFilePath, "utf-8"));
841
1053
  const otherLines = otherContent.trim().split("\n").filter(Boolean);
842
1054
  const otherMessages = otherLines.map((l) => JSON.parse(l));
843
1055
  for (let i = 0; i < otherMessages.length; i++) {
@@ -857,8 +1069,8 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
857
1069
  if (summariesTargetingThis.length > 0) {
858
1070
  summariesTargetingThis.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
859
1071
  const firstSummary = summariesTargetingThis[0];
860
- const summaryFilePath = path4.join(projectPath, firstSummary.file);
861
- const summaryContent = yield* Effect3.tryPromise(() => fs4.readFile(summaryFilePath, "utf-8"));
1072
+ const summaryFilePath = path5.join(projectPath, firstSummary.file);
1073
+ const summaryContent = yield* Effect4.tryPromise(() => fs5.readFile(summaryFilePath, "utf-8"));
862
1074
  const summaryLines = summaryContent.trim().split("\n").filter(Boolean);
863
1075
  const summaryMessages = summaryLines.map((l) => JSON.parse(l));
864
1076
  summaryMessages[firstSummary.idx] = {
@@ -866,9 +1078,9 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
866
1078
  summary: newTitle
867
1079
  };
868
1080
  const newSummaryContent = summaryMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
869
- yield* Effect3.tryPromise(() => fs4.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
1081
+ yield* Effect4.tryPromise(() => fs5.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
870
1082
  } else {
871
- const currentContent = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1083
+ const currentContent = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
872
1084
  const currentLines = currentContent.trim().split("\n").filter(Boolean);
873
1085
  const currentMessages = currentLines.map((l) => JSON.parse(l));
874
1086
  const firstUserIdx = currentMessages.findIndex((m) => m.type === "user");
@@ -887,88 +1099,433 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
887
1099
 
888
1100
  ${cleanedText}`;
889
1101
  const updatedContent = currentMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
890
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, updatedContent, "utf-8"));
1102
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, updatedContent, "utf-8"));
891
1103
  }
892
1104
  }
893
1105
  }
894
1106
  }
895
1107
  return { success: true };
896
1108
  });
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
- }
1109
+ var moveSession = (sourceProject, sessionId, targetProject) => Effect4.gen(function* () {
1110
+ const sessionsDir = getSessionsDir();
1111
+ const sourcePath = path5.join(sessionsDir, sourceProject);
1112
+ const targetPath = path5.join(sessionsDir, targetProject);
1113
+ const sourceFile = path5.join(sourcePath, `${sessionId}.jsonl`);
1114
+ const targetFile = path5.join(targetPath, `${sessionId}.jsonl`);
1115
+ const sourceExists = yield* Effect4.tryPromise(
1116
+ () => fs5.access(sourceFile).then(() => true).catch(() => false)
1117
+ );
1118
+ if (!sourceExists) {
1119
+ return { success: false, error: "Source session not found" };
1120
+ }
1121
+ const targetExists = yield* Effect4.tryPromise(
1122
+ () => fs5.access(targetFile).then(() => true).catch(() => false)
1123
+ );
1124
+ if (targetExists) {
1125
+ return { success: false, error: "Session already exists in target project" };
1126
+ }
1127
+ yield* Effect4.tryPromise(() => fs5.mkdir(targetPath, { recursive: true }));
1128
+ const linkedAgents = yield* findLinkedAgents(sourceProject, sessionId);
1129
+ yield* Effect4.tryPromise(() => fs5.rename(sourceFile, targetFile));
1130
+ for (const agentId of linkedAgents) {
1131
+ const sourceAgentFile = path5.join(sourcePath, `${agentId}.jsonl`);
1132
+ const targetAgentFile = path5.join(targetPath, `${agentId}.jsonl`);
1133
+ const agentExists = yield* Effect4.tryPromise(
1134
+ () => fs5.access(sourceAgentFile).then(() => true).catch(() => false)
1135
+ );
1136
+ if (agentExists) {
1137
+ yield* Effect4.tryPromise(() => fs5.rename(sourceAgentFile, targetAgentFile));
918
1138
  }
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
- }
1139
+ }
1140
+ return { success: true };
1141
+ });
1142
+ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect4.gen(function* () {
1143
+ const projectPath = path5.join(getSessionsDir(), projectName);
1144
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
1145
+ const content = yield* Effect4.tryPromise(() => fs5.readFile(filePath, "utf-8"));
1146
+ const lines = content.trim().split("\n").filter(Boolean);
1147
+ const allMessages = lines.map((line) => JSON.parse(line));
1148
+ const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
1149
+ if (splitIndex === -1) {
1150
+ return { success: false, error: "Message not found" };
1151
+ }
1152
+ if (splitIndex === 0) {
1153
+ return { success: false, error: "Cannot split at first message" };
1154
+ }
1155
+ const newSessionId = crypto.randomUUID();
1156
+ const summaryMessages = allMessages.filter((m) => m.type === "summary");
1157
+ const summaryMessage = summaryMessages.length > 0 ? summaryMessages[summaryMessages.length - 1] : null;
1158
+ const splitMessage = allMessages[splitIndex];
1159
+ const shouldDuplicate = isContinuationSummary(splitMessage);
1160
+ let keptMessages = allMessages.slice(splitIndex);
1161
+ let movedMessages;
1162
+ if (shouldDuplicate) {
1163
+ const duplicatedMessage = {
1164
+ ...splitMessage,
1165
+ uuid: crypto.randomUUID(),
1166
+ sessionId: newSessionId
1167
+ };
1168
+ movedMessages = [...allMessages.slice(0, splitIndex), duplicatedMessage];
1169
+ } else {
1170
+ movedMessages = allMessages.slice(0, splitIndex);
1171
+ }
1172
+ keptMessages = keptMessages.map((msg, index) => {
1173
+ let updated = { ...msg };
1174
+ if (index === 0) {
1175
+ updated.parentUuid = null;
1176
+ updated = cleanupSplitFirstMessage(updated);
1177
+ }
1178
+ return updated;
1179
+ });
1180
+ const updatedMovedMessages = movedMessages.map((msg) => ({
1181
+ ...msg,
1182
+ sessionId: newSessionId
1183
+ }));
1184
+ if (summaryMessage) {
1185
+ const clonedSummary = {
1186
+ ...summaryMessage,
1187
+ sessionId: newSessionId,
1188
+ leafUuid: updatedMovedMessages[0]?.uuid ?? null
1189
+ };
1190
+ updatedMovedMessages.unshift(clonedSummary);
1191
+ }
1192
+ const keptContent = keptMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1193
+ yield* Effect4.tryPromise(() => fs5.writeFile(filePath, keptContent, "utf-8"));
1194
+ const newFilePath = path5.join(projectPath, `${newSessionId}.jsonl`);
1195
+ const newContent = updatedMovedMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1196
+ yield* Effect4.tryPromise(() => fs5.writeFile(newFilePath, newContent, "utf-8"));
1197
+ const agentFiles = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
1198
+ const agentJsonlFiles = agentFiles.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
1199
+ for (const agentFile of agentJsonlFiles) {
1200
+ const agentPath = path5.join(projectPath, agentFile);
1201
+ const agentContent = yield* Effect4.tryPromise(() => fs5.readFile(agentPath, "utf-8"));
1202
+ const agentLines = agentContent.trim().split("\n").filter(Boolean);
1203
+ if (agentLines.length === 0) continue;
1204
+ const firstAgentMsg = JSON.parse(agentLines[0]);
1205
+ if (firstAgentMsg.sessionId === sessionId) {
1206
+ const agentId = agentFile.replace("agent-", "").replace(".jsonl", "");
1207
+ const isRelatedToMoved = movedMessages.some(
1208
+ (msg) => msg.agentId === agentId
1209
+ );
1210
+ if (isRelatedToMoved) {
1211
+ const updatedAgentMessages = agentLines.map((line) => {
1212
+ const msg = JSON.parse(line);
1213
+ return JSON.stringify({ ...msg, sessionId: newSessionId });
1214
+ });
1215
+ const updatedAgentContent = updatedAgentMessages.join("\n") + "\n";
1216
+ yield* Effect4.tryPromise(() => fs5.writeFile(agentPath, updatedAgentContent, "utf-8"));
939
1217
  }
940
1218
  }
941
1219
  }
942
1220
  return {
943
- sessionId,
944
- projectName,
945
- files: fileChanges,
946
- totalChanges: fileChanges.length
1221
+ success: true,
1222
+ newSessionId,
1223
+ newSessionPath: newFilePath,
1224
+ movedMessageCount: movedMessages.length,
1225
+ duplicatedSummary: shouldDuplicate
947
1226
  };
948
1227
  });
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,
1228
+
1229
+ // src/session/tree.ts
1230
+ import { Effect as Effect5 } from "effect";
1231
+ import * as fs6 from "fs/promises";
1232
+ import * as path6 from "path";
1233
+ var sortSessions = (sessions, sort) => {
1234
+ return sessions.sort((a, b) => {
1235
+ let comparison = 0;
1236
+ switch (sort.field) {
1237
+ case "summary": {
1238
+ comparison = a.sortTimestamp - b.sortTimestamp;
1239
+ break;
1240
+ }
1241
+ case "modified": {
1242
+ comparison = (a.fileMtime ?? 0) - (b.fileMtime ?? 0);
1243
+ break;
1244
+ }
1245
+ case "created": {
1246
+ const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
1247
+ const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
1248
+ comparison = createdA - createdB;
1249
+ break;
1250
+ }
1251
+ case "updated": {
1252
+ const updatedA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
1253
+ const updatedB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
1254
+ comparison = updatedA - updatedB;
1255
+ break;
1256
+ }
1257
+ case "messageCount": {
1258
+ comparison = a.messageCount - b.messageCount;
1259
+ break;
1260
+ }
1261
+ case "title": {
1262
+ const titleA = a.customTitle ?? a.currentSummary ?? a.title;
1263
+ const titleB = b.customTitle ?? b.currentSummary ?? b.title;
1264
+ comparison = titleA.localeCompare(titleB);
1265
+ break;
1266
+ }
1267
+ }
1268
+ return sort.order === "desc" ? -comparison : comparison;
1269
+ });
1270
+ };
1271
+ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession, fileMtime) => Effect5.gen(function* () {
1272
+ const projectPath = path6.join(getSessionsDir(), projectName);
1273
+ const filePath = path6.join(projectPath, `${sessionId}.jsonl`);
1274
+ const content = yield* Effect5.tryPromise(() => fs6.readFile(filePath, "utf-8"));
1275
+ const lines = content.trim().split("\n").filter(Boolean);
1276
+ const messages = lines.map((line) => JSON.parse(line));
1277
+ let summaries;
1278
+ if (summariesByTargetSession) {
1279
+ summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort((a, b) => {
1280
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1281
+ if (timestampCmp !== 0) return timestampCmp;
1282
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1283
+ });
1284
+ } else {
1285
+ summaries = [];
1286
+ const sessionUuids = /* @__PURE__ */ new Set();
1287
+ for (const msg of messages) {
1288
+ if (msg.uuid && typeof msg.uuid === "string") {
1289
+ sessionUuids.add(msg.uuid);
1290
+ }
1291
+ }
1292
+ const projectFiles = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1293
+ const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1294
+ for (const file of allJsonlFiles) {
1295
+ try {
1296
+ const otherFilePath = path6.join(projectPath, file);
1297
+ const otherContent = yield* Effect5.tryPromise(() => fs6.readFile(otherFilePath, "utf-8"));
1298
+ const otherLines = otherContent.trim().split("\n").filter(Boolean);
1299
+ for (const line of otherLines) {
1300
+ try {
1301
+ const msg = JSON.parse(line);
1302
+ if (msg.type === "summary" && typeof msg.summary === "string" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
1303
+ const targetMsg = messages.find((m) => m.uuid === msg.leafUuid);
1304
+ summaries.push({
1305
+ summary: msg.summary,
1306
+ leafUuid: msg.leafUuid,
1307
+ timestamp: targetMsg?.timestamp ?? msg.timestamp,
1308
+ sourceFile: file
1309
+ });
1310
+ }
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ } catch {
1315
+ }
1316
+ }
1317
+ }
1318
+ summaries.sort((a, b) => {
1319
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1320
+ if (timestampCmp !== 0) return timestampCmp;
1321
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1322
+ });
1323
+ let lastCompactBoundaryUuid;
1324
+ for (let i = messages.length - 1; i >= 0; i--) {
1325
+ const msg = messages[i];
1326
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
1327
+ lastCompactBoundaryUuid = msg.uuid;
1328
+ break;
1329
+ }
1330
+ }
1331
+ const firstUserMsg = messages.find((m) => m.type === "user");
1332
+ const customTitleMsg = messages.find((m) => m.type === "custom-title");
1333
+ const customTitle = customTitleMsg?.customTitle;
1334
+ const title = firstUserMsg ? extractTitle(extractTextContent(firstUserMsg.message)) : summaries.length > 0 ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`;
1335
+ const userAssistantMessages = messages.filter(
1336
+ (m) => m.type === "user" || m.type === "assistant"
1337
+ );
1338
+ const firstMessage = userAssistantMessages[0];
1339
+ const lastMessage = userAssistantMessages[userAssistantMessages.length - 1];
1340
+ const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1341
+ const agents = [];
1342
+ for (const agentId of linkedAgentIds) {
1343
+ const agentPath = path6.join(projectPath, `${agentId}.jsonl`);
1344
+ try {
1345
+ const agentContent = yield* Effect5.tryPromise(() => fs6.readFile(agentPath, "utf-8"));
1346
+ const agentLines = agentContent.trim().split("\n").filter(Boolean);
1347
+ const agentMsgs = agentLines.map((l) => JSON.parse(l));
1348
+ const agentUserAssistant = agentMsgs.filter(
1349
+ (m) => m.type === "user" || m.type === "assistant"
1350
+ );
1351
+ let agentName;
1352
+ const firstAgentMsg = agentMsgs.find((m) => m.type === "user");
1353
+ if (firstAgentMsg) {
1354
+ const text = extractTextContent(firstAgentMsg.message);
1355
+ if (text) {
1356
+ agentName = extractTitle(text);
1357
+ }
1358
+ }
1359
+ agents.push({
1360
+ id: agentId,
1361
+ name: agentName,
1362
+ messageCount: agentUserAssistant.length
1363
+ });
1364
+ } catch {
1365
+ agents.push({
1366
+ id: agentId,
1367
+ messageCount: 0
1368
+ });
1369
+ }
1370
+ }
1371
+ const todos = yield* findLinkedTodos(sessionId, linkedAgentIds);
1372
+ const createdAt = firstMessage?.timestamp ?? void 0;
1373
+ const sortTimestamp = getSessionSortTimestamp({ summaries, createdAt });
1374
+ return {
1375
+ id: sessionId,
1376
+ projectName,
1377
+ title,
1378
+ customTitle,
1379
+ currentSummary: summaries[0]?.summary,
1380
+ messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : summaries.length > 0 ? 1 : 0,
1381
+ createdAt,
1382
+ updatedAt: lastMessage?.timestamp ?? void 0,
1383
+ fileMtime,
1384
+ sortTimestamp,
1385
+ summaries,
1386
+ agents,
1387
+ todos,
1388
+ lastCompactBoundaryUuid
1389
+ };
1390
+ });
1391
+ var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1392
+ var DEFAULT_SORT = { field: "summary", order: "desc" };
1393
+ var loadProjectTreeData = (projectName, sortOptions) => Effect5.gen(function* () {
1394
+ const project = (yield* listProjects).find((p) => p.name === projectName);
1395
+ if (!project) {
1396
+ return null;
1397
+ }
1398
+ const sort = sortOptions ?? DEFAULT_SORT;
1399
+ const projectPath = path6.join(getSessionsDir(), projectName);
1400
+ const files = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1401
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1402
+ const fileMtimes = /* @__PURE__ */ new Map();
1403
+ yield* Effect5.all(
1404
+ sessionFiles.map(
1405
+ (file) => Effect5.gen(function* () {
1406
+ const filePath = path6.join(projectPath, file);
1407
+ try {
1408
+ const stat4 = yield* Effect5.tryPromise(() => fs6.stat(filePath));
1409
+ fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
1410
+ } catch {
1411
+ }
1412
+ })
1413
+ ),
1414
+ { concurrency: 20 }
1415
+ );
1416
+ const globalUuidMap = /* @__PURE__ */ new Map();
1417
+ const allSummaries = [];
1418
+ const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1419
+ yield* Effect5.all(
1420
+ allJsonlFiles.map(
1421
+ (file) => Effect5.gen(function* () {
1422
+ const filePath = path6.join(projectPath, file);
1423
+ const fileSessionId = file.replace(".jsonl", "");
1424
+ try {
1425
+ const content = yield* Effect5.tryPromise(() => fs6.readFile(filePath, "utf-8"));
1426
+ const lines = content.trim().split("\n").filter(Boolean);
1427
+ for (const line of lines) {
1428
+ try {
1429
+ const msg = JSON.parse(line);
1430
+ if (msg.uuid && typeof msg.uuid === "string") {
1431
+ globalUuidMap.set(msg.uuid, {
1432
+ sessionId: fileSessionId,
1433
+ timestamp: msg.timestamp
1434
+ });
1435
+ }
1436
+ if (msg.messageId && typeof msg.messageId === "string") {
1437
+ globalUuidMap.set(msg.messageId, {
1438
+ sessionId: fileSessionId,
1439
+ timestamp: msg.snapshot?.timestamp
1440
+ });
1441
+ }
1442
+ if (msg.type === "summary" && typeof msg.summary === "string") {
1443
+ allSummaries.push({
1444
+ summary: msg.summary,
1445
+ leafUuid: msg.leafUuid,
1446
+ timestamp: msg.timestamp,
1447
+ sourceFile: file
1448
+ });
1449
+ }
1450
+ } catch {
1451
+ }
1452
+ }
1453
+ } catch {
1454
+ }
1455
+ })
1456
+ ),
1457
+ { concurrency: 20 }
1458
+ );
1459
+ const summariesByTargetSession = /* @__PURE__ */ new Map();
1460
+ for (const summaryData of allSummaries) {
1461
+ if (summaryData.leafUuid) {
1462
+ const targetInfo = globalUuidMap.get(summaryData.leafUuid);
1463
+ if (targetInfo) {
1464
+ const targetSessionId = targetInfo.sessionId;
1465
+ if (!summariesByTargetSession.has(targetSessionId)) {
1466
+ summariesByTargetSession.set(targetSessionId, []);
1467
+ }
1468
+ summariesByTargetSession.get(targetSessionId).push({
1469
+ summary: summaryData.summary,
1470
+ leafUuid: summaryData.leafUuid,
1471
+ // Use summary's own timestamp for sorting, not the target message's timestamp
1472
+ timestamp: summaryData.timestamp ?? targetInfo.timestamp,
1473
+ sourceFile: summaryData.sourceFile
1474
+ });
1475
+ }
1476
+ }
1477
+ }
1478
+ const sessions = yield* Effect5.all(
1479
+ sessionFiles.map((file) => {
1480
+ const sessionId = file.replace(".jsonl", "");
1481
+ const mtime = fileMtimes.get(sessionId);
1482
+ return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1483
+ }),
1484
+ { concurrency: 10 }
1485
+ );
1486
+ const sortedSessions = sortSessions(sessions, sort);
1487
+ const filteredSessions = sortedSessions.filter((s) => {
1488
+ if (isErrorSessionTitle(s.title)) return false;
1489
+ if (isErrorSessionTitle(s.customTitle)) return false;
1490
+ if (isErrorSessionTitle(s.currentSummary)) return false;
1491
+ return true;
1492
+ });
1493
+ return {
1494
+ name: project.name,
1495
+ displayName: project.displayName,
1496
+ path: project.path,
1497
+ sessionCount: filteredSessions.length,
1498
+ sessions: filteredSessions
1499
+ };
1500
+ });
1501
+
1502
+ // src/session/analysis.ts
1503
+ import { Effect as Effect6 } from "effect";
1504
+ import * as fs7 from "fs/promises";
1505
+ import * as path7 from "path";
1506
+ var analyzeSession = (projectName, sessionId) => Effect6.gen(function* () {
1507
+ const messages = yield* readSession(projectName, sessionId);
1508
+ let userMessages = 0;
1509
+ let assistantMessages = 0;
1510
+ let summaryCount = 0;
1511
+ let snapshotCount = 0;
1512
+ const toolUsageMap = /* @__PURE__ */ new Map();
1513
+ const filesChanged = /* @__PURE__ */ new Set();
1514
+ const patterns = [];
1515
+ const milestones = [];
1516
+ let firstTimestamp;
1517
+ let lastTimestamp;
1518
+ for (const msg of messages) {
1519
+ if (msg.timestamp) {
1520
+ if (!firstTimestamp) firstTimestamp = msg.timestamp;
1521
+ lastTimestamp = msg.timestamp;
1522
+ }
1523
+ if (msg.type === "user") {
1524
+ userMessages++;
1525
+ const content = typeof msg.content === "string" ? msg.content : "";
1526
+ if (content.toLowerCase().includes("commit") || content.toLowerCase().includes("\uC644\uB8CC")) {
1527
+ milestones.push({
1528
+ timestamp: msg.timestamp,
972
1529
  description: `User checkpoint: ${content.slice(0, 50)}...`,
973
1530
  messageUuid: msg.uuid
974
1531
  });
@@ -1077,128 +1634,198 @@ var analyzeSession = (projectName, sessionId) => Effect3.gen(function* () {
1077
1634
  milestones
1078
1635
  };
1079
1636
  });
1080
- var moveSession = (sourceProject, sessionId, targetProject) => Effect3.gen(function* () {
1081
- 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)
1088
- );
1089
- if (!sourceExists) {
1090
- return { success: false, error: "Source session not found" };
1091
- }
1092
- const targetExists = yield* Effect3.tryPromise(
1093
- () => fs4.access(targetFile).then(() => true).catch(() => false)
1094
- );
1095
- if (targetExists) {
1096
- return { success: false, error: "Session already exists in target project" };
1097
- }
1098
- yield* Effect3.tryPromise(() => fs4.mkdir(targetPath, { recursive: true }));
1099
- const linkedAgents = yield* findLinkedAgents(sourceProject, sessionId);
1100
- yield* Effect3.tryPromise(() => fs4.rename(sourceFile, targetFile));
1101
- 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)
1106
- );
1107
- if (agentExists) {
1108
- yield* Effect3.tryPromise(() => fs4.rename(sourceAgentFile, targetAgentFile));
1109
- }
1110
- }
1111
- return { success: true };
1112
- });
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"));
1637
+ var compressSession = (projectName, sessionId, options = {}) => Effect6.gen(function* () {
1638
+ const { keepSnapshots = "first_last", maxToolOutputLength = 5e3 } = options;
1639
+ const filePath = path7.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1640
+ const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1641
+ const originalSize = Buffer.byteLength(content, "utf-8");
1117
1642
  const lines = content.trim().split("\n").filter(Boolean);
1118
- const allMessages = lines.map((line) => JSON.parse(line));
1119
- const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
1120
- if (splitIndex === -1) {
1121
- return { success: false, error: "Message not found" };
1122
- }
1123
- if (splitIndex === 0) {
1124
- return { success: false, error: "Cannot split at first message" };
1125
- }
1126
- const newSessionId = crypto.randomUUID();
1127
- const summaryMessages = allMessages.filter((m) => m.type === "summary");
1128
- const summaryMessage = summaryMessages.length > 0 ? summaryMessages[summaryMessages.length - 1] : null;
1129
- const splitMessage = allMessages[splitIndex];
1130
- const shouldDuplicate = isContinuationSummary(splitMessage);
1131
- let keptMessages = allMessages.slice(splitIndex);
1132
- let movedMessages;
1133
- if (shouldDuplicate) {
1134
- const duplicatedMessage = {
1135
- ...splitMessage,
1136
- uuid: crypto.randomUUID(),
1137
- sessionId: newSessionId
1138
- };
1139
- movedMessages = [...allMessages.slice(0, splitIndex), duplicatedMessage];
1140
- } else {
1141
- movedMessages = allMessages.slice(0, splitIndex);
1142
- }
1143
- keptMessages = keptMessages.map((msg, index) => {
1144
- let updated = { ...msg };
1145
- if (index === 0) {
1146
- updated.parentUuid = null;
1147
- updated = cleanupSplitFirstMessage(updated);
1643
+ const messages = lines.map((line) => JSON.parse(line));
1644
+ let removedSnapshots = 0;
1645
+ let truncatedOutputs = 0;
1646
+ const snapshotIndices = [];
1647
+ messages.forEach((msg, idx) => {
1648
+ if (msg.type === "file-history-snapshot") {
1649
+ snapshotIndices.push(idx);
1148
1650
  }
1149
- return updated;
1150
1651
  });
1151
- const updatedMovedMessages = movedMessages.map((msg) => ({
1152
- ...msg,
1153
- sessionId: newSessionId
1154
- }));
1155
- if (summaryMessage) {
1156
- const clonedSummary = {
1157
- ...summaryMessage,
1158
- sessionId: newSessionId,
1159
- leafUuid: updatedMovedMessages[0]?.uuid ?? null
1160
- };
1161
- updatedMovedMessages.unshift(clonedSummary);
1162
- }
1163
- 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`);
1166
- 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));
1169
- const agentJsonlFiles = agentFiles.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
1170
- for (const agentFile of agentJsonlFiles) {
1171
- const agentPath = path4.join(projectPath, agentFile);
1172
- const agentContent = yield* Effect3.tryPromise(() => fs4.readFile(agentPath, "utf-8"));
1173
- const agentLines = agentContent.trim().split("\n").filter(Boolean);
1174
- if (agentLines.length === 0) continue;
1175
- const firstAgentMsg = JSON.parse(agentLines[0]);
1176
- if (firstAgentMsg.sessionId === sessionId) {
1177
- const agentId = agentFile.replace("agent-", "").replace(".jsonl", "");
1178
- const isRelatedToMoved = movedMessages.some(
1179
- (msg) => msg.agentId === agentId
1180
- );
1181
- if (isRelatedToMoved) {
1182
- const updatedAgentMessages = agentLines.map((line) => {
1183
- const msg = JSON.parse(line);
1184
- return JSON.stringify({ ...msg, sessionId: newSessionId });
1185
- });
1186
- const updatedAgentContent = updatedAgentMessages.join("\n") + "\n";
1187
- yield* Effect3.tryPromise(() => fs4.writeFile(agentPath, updatedAgentContent, "utf-8"));
1652
+ const filteredMessages = messages.filter((msg, idx) => {
1653
+ if (msg.type === "file-history-snapshot") {
1654
+ if (keepSnapshots === "none") {
1655
+ removedSnapshots++;
1656
+ return false;
1657
+ }
1658
+ if (keepSnapshots === "first_last") {
1659
+ const isFirst = idx === snapshotIndices[0];
1660
+ const isLast = idx === snapshotIndices[snapshotIndices.length - 1];
1661
+ if (!isFirst && !isLast) {
1662
+ removedSnapshots++;
1663
+ return false;
1664
+ }
1665
+ }
1666
+ }
1667
+ return true;
1668
+ });
1669
+ for (const msg of filteredMessages) {
1670
+ if (msg.type === "user" && Array.isArray(msg.content)) {
1671
+ for (const item of msg.content) {
1672
+ if (item.type === "tool_result" && typeof item.content === "string") {
1673
+ if (maxToolOutputLength > 0 && item.content.length > maxToolOutputLength) {
1674
+ item.content = item.content.slice(0, maxToolOutputLength) + "\n... [truncated]";
1675
+ truncatedOutputs++;
1676
+ }
1677
+ }
1188
1678
  }
1189
1679
  }
1190
1680
  }
1681
+ const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1682
+ const compressedSize = Buffer.byteLength(newContent, "utf-8");
1683
+ yield* Effect6.tryPromise(() => fs7.writeFile(filePath, newContent, "utf-8"));
1191
1684
  return {
1192
1685
  success: true,
1193
- newSessionId,
1194
- newSessionPath: newFilePath,
1195
- movedMessageCount: movedMessages.length,
1196
- duplicatedSummary: shouldDuplicate
1686
+ originalSize,
1687
+ compressedSize,
1688
+ removedSnapshots,
1689
+ truncatedOutputs
1690
+ };
1691
+ });
1692
+ var extractProjectKnowledge = (projectName, sessionIds) => Effect6.gen(function* () {
1693
+ const sessionsDir = getSessionsDir();
1694
+ const projectDir = path7.join(sessionsDir, projectName);
1695
+ let targetSessionIds = sessionIds;
1696
+ if (!targetSessionIds) {
1697
+ const files = yield* Effect6.tryPromise(() => fs7.readdir(projectDir));
1698
+ targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
1699
+ }
1700
+ const fileModifyCount = /* @__PURE__ */ new Map();
1701
+ const toolSequences = [];
1702
+ const decisions = [];
1703
+ for (const sessionId of targetSessionIds) {
1704
+ try {
1705
+ const messages = yield* readSession(projectName, sessionId);
1706
+ for (const msg of messages) {
1707
+ if (msg.type === "file-history-snapshot") {
1708
+ const snapshot = msg;
1709
+ if (snapshot.snapshot?.trackedFileBackups) {
1710
+ for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1711
+ const existing = fileModifyCount.get(filePath) ?? { count: 0 };
1712
+ existing.count++;
1713
+ existing.lastModified = snapshot.snapshot.timestamp;
1714
+ fileModifyCount.set(filePath, existing);
1715
+ }
1716
+ }
1717
+ }
1718
+ if (msg.type === "assistant" && msg.message?.content && Array.isArray(msg.message.content)) {
1719
+ const tools = [];
1720
+ for (const item of msg.message.content) {
1721
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
1722
+ const toolUse = item;
1723
+ if (toolUse.name) tools.push(toolUse.name);
1724
+ }
1725
+ }
1726
+ if (tools.length > 1) {
1727
+ toolSequences.push(tools);
1728
+ }
1729
+ }
1730
+ if (msg.type === "summary" && msg.summary) {
1731
+ decisions.push({
1732
+ context: "Session summary",
1733
+ decision: msg.summary.slice(0, 200),
1734
+ sessionId
1735
+ });
1736
+ }
1737
+ }
1738
+ } catch {
1739
+ continue;
1740
+ }
1741
+ }
1742
+ const hotFiles = Array.from(fileModifyCount.entries()).map(([filePath, data]) => ({
1743
+ path: filePath,
1744
+ modifyCount: data.count,
1745
+ lastModified: data.lastModified
1746
+ })).sort((a, b) => b.modifyCount - a.modifyCount).slice(0, 20);
1747
+ const workflowMap = /* @__PURE__ */ new Map();
1748
+ for (const seq of toolSequences) {
1749
+ const key = seq.join(" -> ");
1750
+ workflowMap.set(key, (workflowMap.get(key) ?? 0) + 1);
1751
+ }
1752
+ const workflows = Array.from(workflowMap.entries()).filter(([, count]) => count >= 2).map(([sequence, count]) => ({
1753
+ sequence: sequence.split(" -> "),
1754
+ count
1755
+ })).sort((a, b) => b.count - a.count).slice(0, 10);
1756
+ return {
1757
+ projectName,
1758
+ patterns: [],
1759
+ hotFiles,
1760
+ workflows,
1761
+ decisions: decisions.slice(0, 20)
1762
+ };
1763
+ });
1764
+ function truncateText(text, maxLen) {
1765
+ const cleaned = text.replace(/\n/g, " ");
1766
+ if (cleaned.length > maxLen) {
1767
+ return cleaned.slice(0, maxLen) + "...";
1768
+ }
1769
+ return cleaned;
1770
+ }
1771
+ var summarizeSession = (projectName, sessionId, options = {}) => Effect6.gen(function* () {
1772
+ const { limit = 50, maxLength = 100 } = options;
1773
+ const messages = yield* readSession(projectName, sessionId);
1774
+ const lines = [];
1775
+ let count = 0;
1776
+ for (const msg of messages) {
1777
+ if (count >= limit) break;
1778
+ if (msg.type === "user" || msg.type === "human") {
1779
+ let timeStr;
1780
+ if (msg.timestamp) {
1781
+ try {
1782
+ const dt = new Date(msg.timestamp);
1783
+ timeStr = dt.toLocaleString("ko-KR", {
1784
+ month: "2-digit",
1785
+ day: "2-digit",
1786
+ hour: "2-digit",
1787
+ minute: "2-digit",
1788
+ hour12: false
1789
+ });
1790
+ } catch {
1791
+ }
1792
+ }
1793
+ const text = extractTextContent(msg.message);
1794
+ if (text) {
1795
+ const truncated = truncateText(text, maxLength);
1796
+ lines.push({ role: "user", content: truncated, timestamp: timeStr });
1797
+ count++;
1798
+ }
1799
+ } else if (msg.type === "assistant") {
1800
+ const text = extractTextContent(msg.message);
1801
+ if (text) {
1802
+ const truncated = truncateText(text, maxLength);
1803
+ lines.push({ role: "assistant", content: truncated });
1804
+ count++;
1805
+ }
1806
+ }
1807
+ }
1808
+ const formatted = lines.map((line) => {
1809
+ if (line.role === "user") {
1810
+ return line.timestamp ? `user [${line.timestamp}]: ${line.content}` : `user: ${line.content}`;
1811
+ }
1812
+ return `assistant: ${line.content}`;
1813
+ }).join("\n");
1814
+ return {
1815
+ sessionId,
1816
+ projectName,
1817
+ lines,
1818
+ formatted
1197
1819
  };
1198
1820
  });
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"));
1821
+
1822
+ // src/session/cleanup.ts
1823
+ import { Effect as Effect7 } from "effect";
1824
+ import * as fs8 from "fs/promises";
1825
+ import * as path8 from "path";
1826
+ var cleanInvalidMessages = (projectName, sessionId) => Effect7.gen(function* () {
1827
+ const filePath = path8.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1828
+ const content = yield* Effect7.tryPromise(() => fs8.readFile(filePath, "utf-8"));
1202
1829
  const lines = content.trim().split("\n").filter(Boolean);
1203
1830
  if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
1204
1831
  const messages = lines.map((line) => JSON.parse(line));
@@ -1230,7 +1857,7 @@ var cleanInvalidMessages = (projectName, sessionId) => Effect3.gen(function* ()
1230
1857
  lastValidUuid = msg.uuid;
1231
1858
  }
1232
1859
  const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
1233
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1860
+ yield* Effect7.tryPromise(() => fs8.writeFile(filePath, newContent, "utf-8"));
1234
1861
  const remainingUserAssistant = filtered.filter(
1235
1862
  (m) => m.type === "user" || m.type === "assistant"
1236
1863
  ).length;
@@ -1238,14 +1865,14 @@ var cleanInvalidMessages = (projectName, sessionId) => Effect3.gen(function* ()
1238
1865
  const remainingCount = remainingUserAssistant > 0 ? remainingUserAssistant : hasSummary ? 1 : 0;
1239
1866
  return { removedCount: invalidIndices.length, remainingCount };
1240
1867
  });
1241
- var previewCleanup = (projectName) => Effect3.gen(function* () {
1868
+ var previewCleanup = (projectName) => Effect7.gen(function* () {
1242
1869
  const projects = yield* listProjects;
1243
1870
  const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1244
1871
  const orphanTodos = yield* findOrphanTodos();
1245
1872
  const orphanTodoCount = orphanTodos.length;
1246
- const results = yield* Effect3.all(
1873
+ const results = yield* Effect7.all(
1247
1874
  targetProjects.map(
1248
- (project) => Effect3.gen(function* () {
1875
+ (project) => Effect7.gen(function* () {
1249
1876
  const sessions = yield* listSessions(project.name);
1250
1877
  const emptySessions = sessions.filter((s) => s.messageCount === 0);
1251
1878
  const invalidSessions = sessions.filter(
@@ -1278,7 +1905,7 @@ var previewCleanup = (projectName) => Effect3.gen(function* () {
1278
1905
  }
1279
1906
  return results;
1280
1907
  });
1281
- var clearSessions = (options) => Effect3.gen(function* () {
1908
+ var clearSessions = (options) => Effect7.gen(function* () {
1282
1909
  const {
1283
1910
  projectName,
1284
1911
  clearEmpty = true,
@@ -1296,8 +1923,8 @@ var clearSessions = (options) => Effect3.gen(function* () {
1296
1923
  const sessionsToDelete = [];
1297
1924
  if (clearInvalid) {
1298
1925
  for (const project of targetProjects) {
1299
- const projectPath = path4.join(getSessionsDir(), project.name);
1300
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1926
+ const projectPath = path8.join(getSessionsDir(), project.name);
1927
+ const files = yield* Effect7.tryPromise(() => fs8.readdir(projectPath));
1301
1928
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1302
1929
  for (const file of sessionFiles) {
1303
1930
  const sessionId = file.replace(".jsonl", "");
@@ -1351,7 +1978,12 @@ var clearSessions = (options) => Effect3.gen(function* () {
1351
1978
  deletedOrphanTodoCount
1352
1979
  };
1353
1980
  });
1354
- var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1981
+
1982
+ // src/session/search.ts
1983
+ import { Effect as Effect8 } from "effect";
1984
+ import * as fs9 from "fs/promises";
1985
+ import * as path9 from "path";
1986
+ var searchSessions = (query, options = {}) => Effect8.gen(function* () {
1355
1987
  const { projectName, searchContent = false } = options;
1356
1988
  const results = [];
1357
1989
  const queryLower = query.toLowerCase();
@@ -1374,16 +2006,16 @@ var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1374
2006
  }
1375
2007
  if (searchContent) {
1376
2008
  for (const project of targetProjects) {
1377
- const projectPath = path4.join(getSessionsDir(), project.name);
1378
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
2009
+ const projectPath = path9.join(getSessionsDir(), project.name);
2010
+ const files = yield* Effect8.tryPromise(() => fs9.readdir(projectPath));
1379
2011
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1380
2012
  for (const file of sessionFiles) {
1381
2013
  const sessionId = file.replace(".jsonl", "");
1382
2014
  if (results.some((r) => r.sessionId === sessionId && r.projectName === project.name)) {
1383
2015
  continue;
1384
2016
  }
1385
- const filePath = path4.join(projectPath, file);
1386
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
2017
+ const filePath = path9.join(projectPath, file);
2018
+ const content = yield* Effect8.tryPromise(() => fs9.readFile(filePath, "utf-8"));
1387
2019
  const lines = content.trim().split("\n").filter(Boolean);
1388
2020
  for (const line of lines) {
1389
2021
  try {
@@ -1419,414 +2051,105 @@ var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1419
2051
  return dateB - dateA;
1420
2052
  });
1421
2053
  });
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);
2054
+
2055
+ // src/session/files.ts
2056
+ import { Effect as Effect9 } from "effect";
2057
+ var getSessionFiles = (projectName, sessionId) => Effect9.gen(function* () {
2058
+ const messages = yield* readSession(projectName, sessionId);
2059
+ const fileChanges = [];
2060
+ const seenFiles = /* @__PURE__ */ new Set();
2061
+ for (const msg of messages) {
2062
+ if (msg.type === "file-history-snapshot") {
2063
+ const snapshot = msg;
2064
+ const backups = snapshot.snapshot?.trackedFileBackups;
2065
+ if (backups && typeof backups === "object") {
2066
+ for (const filePath of Object.keys(backups)) {
2067
+ if (!seenFiles.has(filePath)) {
2068
+ seenFiles.add(filePath);
2069
+ fileChanges.push({
2070
+ path: filePath,
2071
+ action: "modified",
2072
+ timestamp: snapshot.snapshot?.timestamp,
2073
+ messageUuid: snapshot.messageId ?? msg.uuid
2074
+ });
2075
+ }
2076
+ }
1439
2077
  }
1440
2078
  }
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
- }
1474
- }
1475
- const firstUserMsg = messages.find((m) => m.type === "user");
1476
- const customTitleMsg = messages.find((m) => m.type === "custom-title");
1477
- const customTitle = customTitleMsg?.customTitle;
1478
- const title = firstUserMsg ? extractTitle(extractTextContent(firstUserMsg.message)) : summaries.length > 0 ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`;
1479
- const userAssistantMessages = messages.filter(
1480
- (m) => m.type === "user" || m.type === "assistant"
1481
- );
1482
- const firstMessage = userAssistantMessages[0];
1483
- const lastMessage = userAssistantMessages[userAssistantMessages.length - 1];
1484
- const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1485
- const agents = [];
1486
- for (const agentId of linkedAgentIds) {
1487
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
1488
- try {
1489
- const agentContent = yield* Effect3.tryPromise(() => fs4.readFile(agentPath, "utf-8"));
1490
- const agentLines = agentContent.trim().split("\n").filter(Boolean);
1491
- const agentMsgs = agentLines.map((l) => JSON.parse(l));
1492
- const agentUserAssistant = agentMsgs.filter(
1493
- (m) => m.type === "user" || m.type === "assistant"
1494
- );
1495
- let agentName;
1496
- const firstAgentMsg = agentMsgs.find((m) => m.type === "user");
1497
- if (firstAgentMsg) {
1498
- const text = extractTextContent(firstAgentMsg.message);
1499
- if (text) {
1500
- agentName = extractTitle(text);
1501
- }
1502
- }
1503
- agents.push({
1504
- id: agentId,
1505
- name: agentName,
1506
- messageCount: agentUserAssistant.length
1507
- });
1508
- } catch {
1509
- agents.push({
1510
- id: agentId,
1511
- messageCount: 0
1512
- });
1513
- }
1514
- }
1515
- const todos = yield* findLinkedTodos(sessionId, linkedAgentIds);
1516
- return {
1517
- id: sessionId,
1518
- projectName,
1519
- title,
1520
- customTitle,
1521
- currentSummary: summaries[0]?.summary,
1522
- messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : summaries.length > 0 ? 1 : 0,
1523
- createdAt: firstMessage?.timestamp ?? void 0,
1524
- updatedAt: lastMessage?.timestamp ?? void 0,
1525
- summaries,
1526
- agents,
1527
- todos,
1528
- lastCompactBoundaryUuid
1529
- };
1530
- });
1531
- var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1532
- var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1533
- const project = (yield* listProjects).find((p) => p.name === projectName);
1534
- if (!project) {
1535
- return null;
1536
- }
1537
- const projectPath = path4.join(getSessionsDir(), projectName);
1538
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1539
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1540
- const globalUuidMap = /* @__PURE__ */ new Map();
1541
- const allSummaries = [];
1542
- const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1543
- yield* Effect3.all(
1544
- allJsonlFiles.map(
1545
- (file) => Effect3.gen(function* () {
1546
- const filePath = path4.join(projectPath, file);
1547
- const fileSessionId = file.replace(".jsonl", "");
1548
- try {
1549
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1550
- const lines = content.trim().split("\n").filter(Boolean);
1551
- for (const line of lines) {
1552
- try {
1553
- const msg = JSON.parse(line);
1554
- if (msg.uuid && typeof msg.uuid === "string") {
1555
- globalUuidMap.set(msg.uuid, {
1556
- sessionId: fileSessionId,
1557
- timestamp: msg.timestamp
1558
- });
1559
- }
1560
- if (msg.messageId && typeof msg.messageId === "string") {
1561
- globalUuidMap.set(msg.messageId, {
1562
- sessionId: fileSessionId,
1563
- timestamp: msg.snapshot?.timestamp
1564
- });
1565
- }
1566
- if (msg.type === "summary" && typeof msg.summary === "string") {
1567
- allSummaries.push({
1568
- summary: msg.summary,
1569
- leafUuid: msg.leafUuid,
1570
- timestamp: msg.timestamp
2079
+ if (msg.type === "assistant" && msg.message?.content) {
2080
+ const content = msg.message.content;
2081
+ if (Array.isArray(content)) {
2082
+ for (const item of content) {
2083
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
2084
+ const toolUse = item;
2085
+ if ((toolUse.name === "Write" || toolUse.name === "Edit") && toolUse.input?.file_path) {
2086
+ const filePath = toolUse.input.file_path;
2087
+ if (!seenFiles.has(filePath)) {
2088
+ seenFiles.add(filePath);
2089
+ fileChanges.push({
2090
+ path: filePath,
2091
+ action: toolUse.name === "Write" ? "created" : "modified",
2092
+ timestamp: msg.timestamp,
2093
+ messageUuid: msg.uuid
1571
2094
  });
1572
2095
  }
1573
- } catch {
1574
2096
  }
1575
2097
  }
1576
- } catch {
1577
- }
1578
- })
1579
- ),
1580
- { concurrency: 20 }
1581
- );
1582
- const summariesByTargetSession = /* @__PURE__ */ new Map();
1583
- for (const summaryData of allSummaries) {
1584
- if (summaryData.leafUuid) {
1585
- const targetInfo = globalUuidMap.get(summaryData.leafUuid);
1586
- if (targetInfo) {
1587
- const targetSessionId = targetInfo.sessionId;
1588
- if (!summariesByTargetSession.has(targetSessionId)) {
1589
- summariesByTargetSession.set(targetSessionId, []);
1590
2098
  }
1591
- summariesByTargetSession.get(targetSessionId).push({
1592
- summary: summaryData.summary,
1593
- leafUuid: summaryData.leafUuid,
1594
- timestamp: targetInfo.timestamp ?? summaryData.timestamp
1595
- });
1596
2099
  }
1597
2100
  }
1598
2101
  }
1599
- const sessions = yield* Effect3.all(
1600
- sessionFiles.map((file) => {
1601
- const sessionId = file.replace(".jsonl", "");
1602
- return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession);
1603
- }),
1604
- { concurrency: 10 }
1605
- );
1606
- 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;
1610
- });
1611
- const filteredSessions = sortedSessions.filter((s) => {
1612
- if (isErrorSessionTitle(s.title)) return false;
1613
- if (isErrorSessionTitle(s.customTitle)) return false;
1614
- if (isErrorSessionTitle(s.currentSummary)) return false;
1615
- return true;
1616
- });
1617
2102
  return {
1618
- name: project.name,
1619
- displayName: project.displayName,
1620
- path: project.path,
1621
- sessionCount: filteredSessions.length,
1622
- sessions: filteredSessions
2103
+ sessionId,
2104
+ projectName,
2105
+ files: fileChanges,
2106
+ totalChanges: fileChanges.length
1623
2107
  };
1624
2108
  });
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);
2109
+
2110
+ // src/session/index-file.ts
2111
+ import { Effect as Effect10 } from "effect";
2112
+ import * as fs10 from "fs/promises";
2113
+ import * as path10 from "path";
2114
+ var loadSessionsIndex = (projectName) => Effect10.gen(function* () {
2115
+ const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
2116
+ try {
2117
+ const content = yield* Effect10.tryPromise(() => fs10.readFile(indexPath, "utf-8"));
2118
+ const index = JSON.parse(content);
2119
+ return index;
2120
+ } catch {
2121
+ return null;
1641
2122
  }
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 };
1645
2123
  });
1646
- var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(function* () {
1647
- 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"));
1650
- const originalSize = Buffer.byteLength(content, "utf-8");
1651
- const lines = content.trim().split("\n").filter(Boolean);
1652
- const messages = lines.map((line) => JSON.parse(line));
1653
- let removedSnapshots = 0;
1654
- let truncatedOutputs = 0;
1655
- const snapshotIndices = [];
1656
- messages.forEach((msg, idx) => {
1657
- if (msg.type === "file-history-snapshot") {
1658
- snapshotIndices.push(idx);
1659
- }
2124
+ var getIndexEntryDisplayTitle = (entry) => {
2125
+ if (entry.customTitle) return entry.customTitle;
2126
+ if (entry.summary) return entry.summary;
2127
+ let prompt = entry.firstPrompt;
2128
+ if (prompt === "No prompt") return "Untitled";
2129
+ if (prompt.startsWith("[Request interrupted")) return "Untitled";
2130
+ prompt = prompt.replace(/<ide_[^>]*>[^<]*<\/ide_[^>]*>/g, "").trim();
2131
+ if (!prompt) return "Untitled";
2132
+ if (prompt.length > 60) {
2133
+ return prompt.slice(0, 57) + "...";
2134
+ }
2135
+ return prompt;
2136
+ };
2137
+ var sortIndexEntriesByModified = (entries) => {
2138
+ return [...entries].sort((a, b) => {
2139
+ const modA = new Date(a.modified).getTime();
2140
+ const modB = new Date(b.modified).getTime();
2141
+ return modB - modA;
1660
2142
  });
1661
- const filteredMessages = messages.filter((msg, idx) => {
1662
- if (msg.type === "file-history-snapshot") {
1663
- if (keepSnapshots === "none") {
1664
- removedSnapshots++;
1665
- return false;
1666
- }
1667
- if (keepSnapshots === "first_last") {
1668
- const isFirst = idx === snapshotIndices[0];
1669
- const isLast = idx === snapshotIndices[snapshotIndices.length - 1];
1670
- if (!isFirst && !isLast) {
1671
- removedSnapshots++;
1672
- return false;
1673
- }
1674
- }
1675
- }
2143
+ };
2144
+ var hasSessionsIndex = (projectName) => Effect10.gen(function* () {
2145
+ const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
2146
+ try {
2147
+ yield* Effect10.tryPromise(() => fs10.access(indexPath));
1676
2148
  return true;
1677
- });
1678
- for (const msg of filteredMessages) {
1679
- if (msg.type === "user" && Array.isArray(msg.content)) {
1680
- for (const item of msg.content) {
1681
- if (item.type === "tool_result" && typeof item.content === "string") {
1682
- if (maxToolOutputLength > 0 && item.content.length > maxToolOutputLength) {
1683
- item.content = item.content.slice(0, maxToolOutputLength) + "\n... [truncated]";
1684
- truncatedOutputs++;
1685
- }
1686
- }
1687
- }
1688
- }
1689
- }
1690
- const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1691
- const compressedSize = Buffer.byteLength(newContent, "utf-8");
1692
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1693
- return {
1694
- success: true,
1695
- originalSize,
1696
- compressedSize,
1697
- removedSnapshots,
1698
- truncatedOutputs
1699
- };
1700
- });
1701
- var extractProjectKnowledge = (projectName, sessionIds) => Effect3.gen(function* () {
1702
- const sessionsDir = getSessionsDir();
1703
- const projectDir = path4.join(sessionsDir, projectName);
1704
- let targetSessionIds = sessionIds;
1705
- if (!targetSessionIds) {
1706
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectDir));
1707
- targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
1708
- }
1709
- const fileModifyCount = /* @__PURE__ */ new Map();
1710
- const toolSequences = [];
1711
- const decisions = [];
1712
- for (const sessionId of targetSessionIds) {
1713
- try {
1714
- const messages = yield* readSession(projectName, sessionId);
1715
- for (const msg of messages) {
1716
- if (msg.type === "file-history-snapshot") {
1717
- const snapshot = msg;
1718
- if (snapshot.snapshot?.trackedFileBackups) {
1719
- for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1720
- const existing = fileModifyCount.get(filePath) ?? { count: 0 };
1721
- existing.count++;
1722
- existing.lastModified = snapshot.snapshot.timestamp;
1723
- fileModifyCount.set(filePath, existing);
1724
- }
1725
- }
1726
- }
1727
- if (msg.type === "assistant" && msg.message?.content && Array.isArray(msg.message.content)) {
1728
- const tools = [];
1729
- for (const item of msg.message.content) {
1730
- if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
1731
- const toolUse = item;
1732
- if (toolUse.name) tools.push(toolUse.name);
1733
- }
1734
- }
1735
- if (tools.length > 1) {
1736
- toolSequences.push(tools);
1737
- }
1738
- }
1739
- if (msg.type === "summary" && msg.summary) {
1740
- decisions.push({
1741
- context: "Session summary",
1742
- decision: msg.summary.slice(0, 200),
1743
- sessionId
1744
- });
1745
- }
1746
- }
1747
- } catch {
1748
- continue;
1749
- }
1750
- }
1751
- const hotFiles = Array.from(fileModifyCount.entries()).map(([filePath, data]) => ({
1752
- path: filePath,
1753
- modifyCount: data.count,
1754
- lastModified: data.lastModified
1755
- })).sort((a, b) => b.modifyCount - a.modifyCount).slice(0, 20);
1756
- const workflowMap = /* @__PURE__ */ new Map();
1757
- for (const seq of toolSequences) {
1758
- const key = seq.join(" -> ");
1759
- workflowMap.set(key, (workflowMap.get(key) ?? 0) + 1);
1760
- }
1761
- const workflows = Array.from(workflowMap.entries()).filter(([, count]) => count >= 2).map(([sequence, count]) => ({
1762
- sequence: sequence.split(" -> "),
1763
- count
1764
- })).sort((a, b) => b.count - a.count).slice(0, 10);
1765
- return {
1766
- projectName,
1767
- patterns: [],
1768
- hotFiles,
1769
- workflows,
1770
- decisions: decisions.slice(0, 20)
1771
- };
1772
- });
1773
- var summarizeSession = (projectName, sessionId, options = {}) => Effect3.gen(function* () {
1774
- const { limit = 50, maxLength = 100 } = options;
1775
- const messages = yield* readSession(projectName, sessionId);
1776
- const lines = [];
1777
- let count = 0;
1778
- for (const msg of messages) {
1779
- if (count >= limit) break;
1780
- if (msg.type === "user" || msg.type === "human") {
1781
- let timeStr;
1782
- if (msg.timestamp) {
1783
- try {
1784
- const dt = new Date(msg.timestamp);
1785
- timeStr = dt.toLocaleString("ko-KR", {
1786
- month: "2-digit",
1787
- day: "2-digit",
1788
- hour: "2-digit",
1789
- minute: "2-digit",
1790
- hour12: false
1791
- });
1792
- } catch {
1793
- }
1794
- }
1795
- const text = extractTextContent(msg.message);
1796
- if (text) {
1797
- const truncated = truncateText(text, maxLength);
1798
- lines.push({ role: "user", content: truncated, timestamp: timeStr });
1799
- count++;
1800
- }
1801
- } else if (msg.type === "assistant") {
1802
- const text = extractTextContent(msg.message);
1803
- if (text) {
1804
- const truncated = truncateText(text, maxLength);
1805
- lines.push({ role: "assistant", content: truncated });
1806
- count++;
1807
- }
1808
- }
2149
+ } catch {
2150
+ return false;
1809
2151
  }
1810
- const formatted = lines.map((line) => {
1811
- if (line.role === "user") {
1812
- return line.timestamp ? `user [${line.timestamp}]: ${line.content}` : `user: ${line.content}`;
1813
- }
1814
- return `assistant: ${line.content}`;
1815
- }).join("\n");
1816
- return {
1817
- sessionId,
1818
- projectName,
1819
- lines,
1820
- formatted
1821
- };
1822
2152
  });
1823
- function truncateText(text, maxLen) {
1824
- const cleaned = text.replace(/\n/g, " ");
1825
- if (cleaned.length > maxLen) {
1826
- return cleaned.slice(0, maxLen) + "...";
1827
- }
1828
- return cleaned;
1829
- }
1830
2153
  export {
1831
2154
  analyzeSession,
1832
2155
  clearSessions,
@@ -1834,6 +2157,7 @@ export {
1834
2157
  createLogger,
1835
2158
  deleteLinkedTodos,
1836
2159
  deleteMessage,
2160
+ deleteMessageWithChainRepair,
1837
2161
  deleteOrphanAgents,
1838
2162
  deleteOrphanTodos,
1839
2163
  deleteSession,
@@ -1849,11 +2173,14 @@ export {
1849
2173
  folderNameToDisplayPath,
1850
2174
  folderNameToPath,
1851
2175
  getDisplayTitle,
2176
+ getIndexEntryDisplayTitle,
1852
2177
  getLogger,
1853
2178
  getRealPathFromSession,
1854
2179
  getSessionFiles,
2180
+ getSessionSortTimestamp,
1855
2181
  getSessionsDir,
1856
2182
  getTodosDir,
2183
+ hasSessionsIndex,
1857
2184
  isContinuationSummary,
1858
2185
  isInvalidApiKeyMessage,
1859
2186
  listProjects,
@@ -1861,8 +2188,10 @@ export {
1861
2188
  loadAgentMessages,
1862
2189
  loadProjectTreeData,
1863
2190
  loadSessionTreeData,
2191
+ loadSessionsIndex,
1864
2192
  maskHomePath,
1865
2193
  moveSession,
2194
+ parseCommandMessage,
1866
2195
  pathToFolderName,
1867
2196
  previewCleanup,
1868
2197
  readSession,
@@ -1871,9 +2200,12 @@ export {
1871
2200
  searchSessions,
1872
2201
  sessionHasTodos,
1873
2202
  setLogger,
2203
+ sortIndexEntriesByModified,
1874
2204
  sortProjects,
1875
2205
  splitSession,
1876
2206
  summarizeSession,
1877
- updateSessionSummary
2207
+ updateSessionSummary,
2208
+ validateChain,
2209
+ validateToolUseResult
1878
2210
  };
1879
2211
  //# sourceMappingURL=index.js.map