@claude-sessions/core 0.4.4-beta.2 → 0.4.4

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as Project, M as MessagePayload$1, a as Message, S as SummaryInfo, b as SessionTodos, c as MoveSessionResult, A as AgentInfo, d as SessionSortOptions, e as ProjectTreeData, C as CompressSessionOptions, f as SummarizeSessionOptions, g as ConversationLine, h as SearchResult, F as FileChange, i as SessionsIndex, j as SessionIndexEntry } from './types-BoYCOac3.js';
2
- export { p as CleanupPreview, o as ClearSessionsResult, v as CompressSessionResult, k as ContentItem, D as DeleteSessionResult, w as ProjectKnowledge, R as RenameSessionResult, r as ResumeSessionOptions, s as ResumeSessionResult, u as SessionAnalysis, m as SessionFilesSummary, l as SessionMeta, y as SessionSortField, z as SessionSortOrder, q as SessionTreeData, n as SplitSessionResult, x as SummarizeSessionResult, T as TodoItem, t as ToolUsageStats } from './types-BoYCOac3.js';
1
+ import { P as Project, M as MessagePayload$1, a as Message, S as SummaryInfo, b as SessionTodos, c as MoveSessionResult, A as AgentInfo, d as SessionSortOptions, e as ProjectTreeData, C as CompressSessionOptions, f as SummarizeSessionOptions, g as ConversationLine, h as SearchResult, F as FileChange, i as SessionsIndex, j as SessionIndexEntry } from './types-BMQErZZE.js';
2
+ export { p as CleanupPreview, o as ClearSessionsResult, v as CompressSessionResult, k as ContentItem, D as DeleteSessionResult, w as ProjectKnowledge, R as RenameSessionResult, r as ResumeSessionOptions, s as ResumeSessionResult, u as SessionAnalysis, m as SessionFilesSummary, l as SessionMeta, y as SessionSortField, z as SessionSortOrder, q as SessionTreeData, n as SplitSessionResult, x as SummarizeSessionResult, T as TodoItem, t as ToolUsageStats } from './types-BMQErZZE.js';
3
3
  import * as effect_Cause from 'effect/Cause';
4
4
  import { Effect } from 'effect';
5
5
 
@@ -24,7 +24,11 @@ declare const getTodosDir: () => string;
24
24
  * Handle dot-prefixed folders: --claude -> /.claude
25
25
  */
26
26
  declare const folderNameToDisplayPath: (folderName: string) => string;
27
- /** Convert display path to folder name (reverse of above) */
27
+ /**
28
+ * Convert display path to folder name (reverse of folderNameToDisplayPath)
29
+ * @deprecated Use pathToFolderName for absolute paths. This function exists
30
+ * for roundtrip testing and does not handle non-ASCII or normalize drive case.
31
+ */
28
32
  declare const displayPathToFolderName: (displayPath: string) => string;
29
33
  /**
30
34
  * Convert absolute path to project folder name
@@ -417,6 +421,7 @@ declare const compressSession: (projectName: string, sessionId: string, options?
417
421
  success: true;
418
422
  originalSize: number;
419
423
  compressedSize: number;
424
+ removedCustomTitles: number;
420
425
  removedProgress: number;
421
426
  removedSnapshots: number;
422
427
  truncatedOutputs: number;
@@ -599,17 +604,6 @@ declare function autoRepairChain<T extends GenericMessage>(messages: T[]): numbe
599
604
  * @param removedMessages - Messages that were removed (need uuid and parentUuid)
600
605
  */
601
606
  declare function repairParentUuidChain<T extends GenericMessage>(messages: T[], removedMessages: T[]): void;
602
- /**
603
- * Delete a message and repair the parentUuid chain
604
- *
605
- * This is a pure function for client-side use (without file I/O)
606
- * Server-side deleteMessage in crud.ts uses similar logic with file operations
607
- *
608
- * @param messages - Array of messages (will be mutated)
609
- * @param targetId - uuid, messageId, or leafUuid of message to delete
610
- * @param targetType - Optional: 'file-history-snapshot' or 'summary' to disambiguate collisions
611
- * @returns Object with deleted message and messages to also delete (orphan tool_results)
612
- */
613
607
  declare function deleteMessageWithChainRepair<T extends GenericMessage>(messages: T[], targetId: string, targetType?: 'file-history-snapshot' | 'summary'): {
614
608
  deleted: T | null;
615
609
  alsoDeleted: T[];
package/dist/index.js CHANGED
@@ -162,7 +162,9 @@ var parseJsonlLines = (lines, filePath) => {
162
162
  return JSON.parse(line);
163
163
  } catch (e) {
164
164
  const err = e;
165
- throw new Error(`Failed to parse line ${idx + 1} in ${filePath}: ${err.message}`);
165
+ throw new Error(`Failed to parse line ${idx + 1} in ${filePath}: ${err.message}`, {
166
+ cause: err
167
+ });
166
168
  }
167
169
  });
168
170
  };
@@ -221,6 +223,24 @@ var canMoveSession = (sourceProject, targetProject) => {
221
223
  var log = createLogger("paths");
222
224
  var getSessionsDir = () => process.env.CLAUDE_SESSIONS_DIR || path.join(os.homedir(), ".claude", "projects");
223
225
  var getTodosDir = () => path.join(os.homedir(), ".claude", "todos");
226
+ var WINDOWS_PATTERNS = {
227
+ /** Matches Windows absolute path: C:\ or C:/ */
228
+ absolutePath: /^([A-Za-z]):[/\\]/,
229
+ /** Matches Windows folder name format: C-- */
230
+ folderName: /^([A-Za-z])--/
231
+ };
232
+ var parseWindowsAbsPath = (p) => {
233
+ const match = p.match(WINDOWS_PATTERNS.absolutePath);
234
+ return match ? { isWindows: true, drive: match[1], rest: p.slice(3) } : { isWindows: false };
235
+ };
236
+ var parseWindowsFolderName = (name) => {
237
+ const match = name.match(WINDOWS_PATTERNS.folderName);
238
+ return match ? { isWindows: true, drive: match[1], rest: name.slice(3) } : { isWindows: false };
239
+ };
240
+ var isWindowsPath = (p) => WINDOWS_PATTERNS.absolutePath.test(p);
241
+ var convertNonAscii = (str) => [...str].map((c) => c.charCodeAt(0) <= 127 ? c : "-").join("");
242
+ var pathCharsToFolderName = (str) => str.replace(/[/\\]\./g, "--").replace(/[/\\]/g, "-").replace(/\./g, "-");
243
+ var separatorsToFolderName = (str) => str.replace(/[/\\]\./g, "--").replace(/[/\\]/g, "-");
224
244
  var extractCwdFromContent = (content, filePath) => {
225
245
  const lines = content.split("\n").filter((l) => l.trim());
226
246
  for (let i = 0; i < lines.length; i++) {
@@ -235,39 +255,38 @@ var isSessionFile = (filename) => filename.endsWith(".jsonl") && !filename.start
235
255
  var toRelativePath = (absolutePath, homeDir) => {
236
256
  const normalizedPath = absolutePath.replace(/\\/g, "/");
237
257
  const normalizedHome = homeDir.replace(/\\/g, "/");
238
- if (normalizedPath === normalizedHome) {
258
+ const isWin = isWindowsPath(homeDir);
259
+ const pathLower = normalizedPath.toLowerCase();
260
+ const homeLower = normalizedHome.toLowerCase();
261
+ if (isWin ? pathLower === homeLower : normalizedPath === normalizedHome) {
239
262
  return "~";
240
263
  }
241
- if (normalizedPath.startsWith(normalizedHome + "/")) {
242
- return "~" + normalizedPath.slice(normalizedHome.length);
264
+ const startsWithHome = isWin ? pathLower.startsWith(homeLower + "/") : normalizedPath.startsWith(normalizedHome + "/");
265
+ if (startsWithHome) {
266
+ const sep = isWin ? "\\" : "/";
267
+ const relativePart = absolutePath.slice(homeDir.length);
268
+ return "~" + relativePart.replace(/[\\/]/g, sep);
243
269
  }
244
270
  return absolutePath;
245
271
  };
246
272
  var folderNameToDisplayPath = (folderName) => {
247
- const windowsDriveMatch = folderName.match(/^([A-Za-z])--/);
248
- if (windowsDriveMatch) {
249
- const driveLetter = windowsDriveMatch[1];
250
- const rest = folderName.slice(3);
251
- return driveLetter + ":\\" + rest.replace(/--/g, "\\.").replace(/-/g, "\\");
273
+ const parsed = parseWindowsFolderName(folderName);
274
+ if (parsed.isWindows) {
275
+ return parsed.drive + ":\\" + parsed.rest.replace(/--/g, "\\.").replace(/-/g, "\\");
252
276
  }
253
277
  return folderName.replace(/^-/, "/").replace(/--/g, "/.").replace(/-/g, "/");
254
278
  };
255
279
  var displayPathToFolderName = (displayPath) => {
256
- const windowsDriveMatch = displayPath.match(/^([A-Za-z]):[/\\]/);
257
- if (windowsDriveMatch) {
258
- const driveLetter = windowsDriveMatch[1];
259
- const rest = displayPath.slice(3);
260
- return driveLetter + "--" + rest.replace(/[/\\]\./g, "--").replace(/[/\\]/g, "-");
280
+ const parsed = parseWindowsAbsPath(displayPath);
281
+ if (parsed.isWindows) {
282
+ return parsed.drive + "--" + separatorsToFolderName(parsed.rest);
261
283
  }
262
284
  return displayPath.replace(/^\//g, "-").replace(/\/\./g, "--").replace(/\//g, "-");
263
285
  };
264
286
  var pathToFolderName = (absolutePath) => {
265
- const convertNonAscii = (str) => [...str].map((char) => char.charCodeAt(0) <= 127 ? char : "-").join("");
266
- const windowsDriveMatch = absolutePath.match(/^([A-Za-z]):[/\\]/);
267
- if (windowsDriveMatch) {
268
- const driveLetter = windowsDriveMatch[1].toLowerCase();
269
- const rest = absolutePath.slice(3);
270
- return driveLetter + "--" + convertNonAscii(rest).replace(/[/\\]\./g, "--").replace(/[/\\]/g, "-").replace(/\./g, "-");
287
+ const parsed = parseWindowsAbsPath(absolutePath);
288
+ if (parsed.isWindows) {
289
+ return parsed.drive.toLowerCase() + "--" + pathCharsToFolderName(convertNonAscii(parsed.rest));
271
290
  }
272
291
  return convertNonAscii(absolutePath).replace(/^\//g, "-").replace(/\/\./g, "--").replace(/\//g, "-").replace(/\./g, "-");
273
292
  };
@@ -872,7 +891,7 @@ function validateProgressMessages(messages) {
872
891
  const progressMsg = msg;
873
892
  const hookEvent = progressMsg.hookEvent ?? progressMsg.data?.hookEvent;
874
893
  const hookName = progressMsg.hookName ?? progressMsg.data?.hookName;
875
- if (hookEvent === "Stop" || hookName === "SessionStart:resume") {
894
+ if (hookEvent === "Stop") {
876
895
  errors.push({
877
896
  type: "unwanted_progress",
878
897
  line: i + 1,
@@ -985,29 +1004,36 @@ function repairParentUuidChain(messages, removedMessages) {
985
1004
  }
986
1005
  }
987
1006
  }
988
- function deleteMessageWithChainRepair(messages, targetId, targetType) {
989
- let targetIndex = -1;
1007
+ function findTargetIndex(messages, targetId, targetType) {
990
1008
  if (targetType === "file-history-snapshot") {
991
- targetIndex = messages.findIndex(
992
- (m) => m.type === "file-history-snapshot" && m.messageId === targetId
1009
+ return messages.findIndex((m) => m.type === "file-history-snapshot" && m.messageId === targetId);
1010
+ }
1011
+ if (targetType === "summary") {
1012
+ return messages.findIndex(
1013
+ (m) => m.leafUuid === targetId
993
1014
  );
994
- } else if (targetType === "summary") {
995
- targetIndex = messages.findIndex(
1015
+ }
1016
+ let idx = messages.findIndex((m) => m.uuid === targetId);
1017
+ if (idx === -1) {
1018
+ idx = messages.findIndex(
996
1019
  (m) => m.leafUuid === targetId
997
1020
  );
998
- } else {
999
- targetIndex = messages.findIndex((m) => m.uuid === targetId);
1000
- if (targetIndex === -1) {
1001
- targetIndex = messages.findIndex(
1002
- (m) => m.leafUuid === targetId
1003
- );
1004
- }
1005
- if (targetIndex === -1) {
1006
- targetIndex = messages.findIndex(
1007
- (m) => m.type === "file-history-snapshot" && m.messageId === targetId
1008
- );
1009
- }
1010
1021
  }
1022
+ if (idx === -1) {
1023
+ idx = messages.findIndex((m) => m.type === "file-history-snapshot" && m.messageId === targetId);
1024
+ }
1025
+ return idx;
1026
+ }
1027
+ function hasMatchingToolResult(msg, toolUseIds) {
1028
+ if (msg.type !== "user") return false;
1029
+ const content = msg.message?.content;
1030
+ if (!Array.isArray(content)) return false;
1031
+ return content.some(
1032
+ (item) => item.type === "tool_result" && item.tool_use_id && toolUseIds.includes(item.tool_use_id)
1033
+ );
1034
+ }
1035
+ function deleteMessageWithChainRepair(messages, targetId, targetType) {
1036
+ const targetIndex = findTargetIndex(messages, targetId, targetType);
1011
1037
  if (targetIndex === -1) {
1012
1038
  return { deleted: null, alsoDeleted: [] };
1013
1039
  }
@@ -1026,17 +1052,8 @@ function deleteMessageWithChainRepair(messages, targetId, targetType) {
1026
1052
  const toolResultIndices = [];
1027
1053
  if (toolUseIds.length > 0) {
1028
1054
  for (let i = 0; i < messages.length; i++) {
1029
- const msg = messages[i];
1030
- if (msg.type === "user") {
1031
- const content = msg.message?.content;
1032
- if (Array.isArray(content)) {
1033
- for (const item of content) {
1034
- if (item.type === "tool_result" && item.tool_use_id && toolUseIds.includes(item.tool_use_id)) {
1035
- toolResultIndices.push(i);
1036
- break;
1037
- }
1038
- }
1039
- }
1055
+ if (hasMatchingToolResult(messages[i], toolUseIds)) {
1056
+ toolResultIndices.push(i);
1040
1057
  }
1041
1058
  }
1042
1059
  }
@@ -1209,80 +1226,17 @@ var renameSession = (projectName, sessionId, newTitle) => Effect5.gen(function*
1209
1226
  return { success: false, error: "Empty session" };
1210
1227
  }
1211
1228
  const messages = parseJsonlLines(lines, filePath);
1212
- const sessionUuids = /* @__PURE__ */ new Set();
1213
- for (const msg of messages) {
1214
- if (msg.uuid && typeof msg.uuid === "string") {
1215
- sessionUuids.add(msg.uuid);
1216
- }
1217
- }
1218
1229
  const customTitleIdx = messages.findIndex((m) => m.type === "custom-title");
1219
- const customTitleRecord = {
1230
+ if (customTitleIdx >= 0) {
1231
+ messages.splice(customTitleIdx, 1);
1232
+ }
1233
+ messages.push({
1220
1234
  type: "custom-title",
1221
1235
  customTitle: newTitle,
1222
1236
  sessionId
1223
- };
1224
- if (customTitleIdx >= 0) {
1225
- messages[customTitleIdx] = customTitleRecord;
1226
- } else {
1227
- messages.unshift(customTitleRecord);
1228
- }
1237
+ });
1229
1238
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1230
1239
  yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
1231
- const projectFiles = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1232
- const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1233
- const summariesTargetingThis = [];
1234
- for (const file of allJsonlFiles) {
1235
- const otherFilePath = path5.join(projectPath, file);
1236
- try {
1237
- const otherMessages = yield* readJsonlFile(otherFilePath);
1238
- for (let i = 0; i < otherMessages.length; i++) {
1239
- const msg = otherMessages[i];
1240
- if (msg.type === "summary" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
1241
- const targetMsg = messages.find((m) => m.uuid === msg.leafUuid);
1242
- summariesTargetingThis.push({
1243
- file,
1244
- idx: i,
1245
- timestamp: targetMsg?.timestamp ?? msg.timestamp
1246
- });
1247
- }
1248
- }
1249
- } catch {
1250
- }
1251
- }
1252
- if (summariesTargetingThis.length > 0) {
1253
- summariesTargetingThis.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
1254
- const firstSummary = summariesTargetingThis[0];
1255
- const summaryFilePath = path5.join(projectPath, firstSummary.file);
1256
- const summaryMessages = yield* readJsonlFile(summaryFilePath);
1257
- summaryMessages[firstSummary.idx] = {
1258
- ...summaryMessages[firstSummary.idx],
1259
- summary: newTitle
1260
- };
1261
- const newSummaryContent = summaryMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1262
- yield* Effect5.tryPromise(() => fs6.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
1263
- } else {
1264
- const currentMessages = yield* readJsonlFile(filePath);
1265
- const firstUserIdx = currentMessages.findIndex((m) => m.type === "user");
1266
- if (firstUserIdx >= 0) {
1267
- const firstMsg = currentMessages[firstUserIdx];
1268
- const msgPayload = firstMsg.message;
1269
- if (msgPayload?.content && Array.isArray(msgPayload.content)) {
1270
- const textIdx = msgPayload.content.findIndex(
1271
- (item) => typeof item === "object" && item?.type === "text" && !item.text?.trim().startsWith("<ide_")
1272
- );
1273
- if (textIdx >= 0) {
1274
- const item = msgPayload.content[textIdx];
1275
- const oldText = item.text ?? "";
1276
- const cleanedText = oldText.replace(/^[^\n]+\n\n/, "");
1277
- item.text = `${newTitle}
1278
-
1279
- ${cleanedText}`;
1280
- const updatedContent = currentMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1281
- yield* Effect5.tryPromise(() => fs6.writeFile(filePath, updatedContent, "utf-8"));
1282
- }
1283
- }
1284
- }
1285
- }
1286
1240
  return { success: true };
1287
1241
  });
1288
1242
  var moveSession = (sourceProject, sessionId, targetProject) => Effect5.gen(function* () {
@@ -1817,11 +1771,16 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
1817
1771
  const originalSize = Buffer.byteLength(content, "utf-8");
1818
1772
  const lines = content.trim().split("\n").filter(Boolean);
1819
1773
  const messages = parseJsonlLines(lines, filePath);
1774
+ let removedCustomTitles = 0;
1820
1775
  let removedProgress = 0;
1821
1776
  let removedSnapshots = 0;
1822
1777
  let truncatedOutputs = 0;
1778
+ const customTitleIndices = [];
1823
1779
  const snapshotIndices = [];
1824
1780
  messages.forEach((msg, idx) => {
1781
+ if (msg.type === "custom-title") {
1782
+ customTitleIndices.push(idx);
1783
+ }
1825
1784
  if (msg.type === "file-history-snapshot") {
1826
1785
  snapshotIndices.push(idx);
1827
1786
  }
@@ -1833,6 +1792,13 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
1833
1792
  messagesToRemove.push(msg);
1834
1793
  return false;
1835
1794
  }
1795
+ if (msg.type === "custom-title") {
1796
+ if (customTitleIndices.length > 1 && idx !== customTitleIndices[customTitleIndices.length - 1]) {
1797
+ removedCustomTitles++;
1798
+ messagesToRemove.push(msg);
1799
+ return false;
1800
+ }
1801
+ }
1836
1802
  if (msg.type === "file-history-snapshot") {
1837
1803
  if (keepSnapshots === "none") {
1838
1804
  removedSnapshots++;
@@ -1871,6 +1837,7 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
1871
1837
  success: true,
1872
1838
  originalSize,
1873
1839
  compressedSize,
1840
+ removedCustomTitles,
1874
1841
  removedProgress,
1875
1842
  removedSnapshots,
1876
1843
  truncatedOutputs