@claude-sessions/core 0.3.7 → 0.4.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/paths.ts
2
- import * as fs from "fs";
2
+ import * as fs2 from "fs";
3
3
  import * as os from "os";
4
4
  import * as path from "path";
5
5
 
@@ -22,19 +22,153 @@ var createLogger = (namespace) => ({
22
22
  error: (msg, ...args) => currentLogger.error(`[${namespace}] ${msg}`, ...args)
23
23
  });
24
24
 
25
+ // src/utils.ts
26
+ import { Effect } from "effect";
27
+ import * as fs from "fs/promises";
28
+ var logger = createLogger("utils");
29
+ var extractTextContent = (message) => {
30
+ if (!message) return "";
31
+ const content = message.content;
32
+ if (!content) return "";
33
+ if (typeof content === "string") return content;
34
+ if (Array.isArray(content)) {
35
+ return content.filter((item) => typeof item === "object" && item?.type === "text").map((item) => {
36
+ if (item.text == null) {
37
+ logger.warn("TextContent item has undefined or null text property");
38
+ return "";
39
+ }
40
+ return item.text;
41
+ }).join("");
42
+ }
43
+ return "";
44
+ };
45
+ var parseCommandMessage = (content) => {
46
+ const name = content?.match(/<command-name>([^<]+)<\/command-name>/)?.[1] ?? "";
47
+ const message = content?.match(/<command-message>([^<]+)<\/command-message>/)?.[1] ?? "";
48
+ return { name, message };
49
+ };
50
+ var extractTitle = (text) => {
51
+ if (!text) return "Untitled";
52
+ const { name } = parseCommandMessage(text);
53
+ if (name) return name;
54
+ let cleaned = text.replace(/<ide_[^>]*>[\s\S]*?<\/ide_[^>]*>/g, "").trim();
55
+ if (!cleaned) return "Untitled";
56
+ if (cleaned.includes("\n\n")) {
57
+ cleaned = cleaned.split("\n\n")[0];
58
+ }
59
+ if (cleaned.length > 100) {
60
+ return cleaned.slice(0, 100) + "...";
61
+ }
62
+ return cleaned || "Untitled";
63
+ };
64
+ var isInvalidApiKeyMessage = (msg) => {
65
+ const text = extractTextContent(msg.message);
66
+ return text.includes("Invalid API key");
67
+ };
68
+ var ERROR_SESSION_PATTERNS = [
69
+ "API Error",
70
+ "authentication_error",
71
+ "Invalid API key",
72
+ "OAuth token has expired",
73
+ "Please run /login"
74
+ ];
75
+ var isErrorSessionTitle = (title) => {
76
+ if (!title) return false;
77
+ return ERROR_SESSION_PATTERNS.some((pattern) => title.includes(pattern));
78
+ };
79
+ var isContinuationSummary = (msg) => {
80
+ if (msg.isCompactSummary === true) return true;
81
+ if (msg.type !== "user") return false;
82
+ const text = extractTextContent(msg.message);
83
+ return text.startsWith("This session is being continued from");
84
+ };
85
+ var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallback = "Untitled") => {
86
+ if (customTitle) return customTitle;
87
+ if (currentSummary) {
88
+ return currentSummary.length > maxLength ? currentSummary.slice(0, maxLength - 3) + "..." : currentSummary;
89
+ }
90
+ if (title && title !== "Untitled") {
91
+ if (title.includes("<command-name>")) {
92
+ const { name } = parseCommandMessage(title);
93
+ if (name) return name;
94
+ }
95
+ return title;
96
+ }
97
+ return fallback;
98
+ };
99
+ var replaceMessageContent = (msg, text) => ({
100
+ ...msg,
101
+ message: {
102
+ ...msg.message,
103
+ content: [{ type: "text", text }]
104
+ },
105
+ toolUseResult: void 0
106
+ });
107
+ var cleanupSplitFirstMessage = (msg) => {
108
+ const toolUseResult = msg.toolUseResult;
109
+ if (!toolUseResult) return msg;
110
+ if (typeof toolUseResult === "object" && "answers" in toolUseResult) {
111
+ const answers = toolUseResult.answers;
112
+ const qaText = Object.entries(answers).map(([q, a]) => `Q: ${q}
113
+ A: ${a}`).join("\n\n");
114
+ return replaceMessageContent(msg, qaText);
115
+ }
116
+ if (typeof toolUseResult === "string") {
117
+ const rejectionMarker = "The user provided the following reason for the rejection:";
118
+ const rejectionIndex = toolUseResult.indexOf(rejectionMarker);
119
+ if (rejectionIndex === -1) return msg;
120
+ const text = toolUseResult.slice(rejectionIndex + rejectionMarker.length).trim();
121
+ if (!text) return msg;
122
+ return replaceMessageContent(msg, text);
123
+ }
124
+ return msg;
125
+ };
126
+ var maskHomePath = (text, homeDir) => {
127
+ if (!homeDir) return text;
128
+ const escapedHome = homeDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
129
+ const regex = new RegExp(`${escapedHome}(?=[/\\\\]|$)`, "g");
130
+ return text.replace(regex, "~");
131
+ };
132
+ var getSessionSortTimestamp = (session) => {
133
+ const timestampStr = session.summaries?.[0]?.timestamp ?? session.createdAt;
134
+ return timestampStr ? new Date(timestampStr).getTime() : 0;
135
+ };
136
+ var tryParseJsonLine = (line, lineNumber, filePath) => {
137
+ try {
138
+ return JSON.parse(line);
139
+ } catch {
140
+ if (filePath) {
141
+ console.warn(`Skipping invalid JSON at line ${lineNumber} in ${filePath}`);
142
+ }
143
+ return null;
144
+ }
145
+ };
146
+ var parseJsonlLines = (lines, filePath) => {
147
+ return lines.map((line, idx) => {
148
+ try {
149
+ return JSON.parse(line);
150
+ } catch (e) {
151
+ const err = e;
152
+ throw new Error(`Failed to parse line ${idx + 1} in ${filePath}: ${err.message}`);
153
+ }
154
+ });
155
+ };
156
+ var readJsonlFile = (filePath) => Effect.gen(function* () {
157
+ const content = yield* Effect.tryPromise(() => fs.readFile(filePath, "utf-8"));
158
+ const lines = content.trim().split("\n").filter(Boolean);
159
+ return parseJsonlLines(lines, filePath);
160
+ });
161
+
25
162
  // src/paths.ts
26
163
  var log = createLogger("paths");
27
164
  var getSessionsDir = () => process.env.CLAUDE_SESSIONS_DIR || path.join(os.homedir(), ".claude", "projects");
28
165
  var getTodosDir = () => path.join(os.homedir(), ".claude", "todos");
29
- var extractCwdFromContent = (content) => {
166
+ var extractCwdFromContent = (content, filePath) => {
30
167
  const lines = content.split("\n").filter((l) => l.trim());
31
- for (const line of lines) {
32
- try {
33
- const parsed = JSON.parse(line);
34
- if (parsed?.cwd) {
35
- return parsed.cwd;
36
- }
37
- } catch {
168
+ for (let i = 0; i < lines.length; i++) {
169
+ const parsed = tryParseJsonLine(lines[i], i + 1, filePath);
170
+ if (parsed?.cwd) {
171
+ return parsed.cwd;
38
172
  }
39
173
  }
40
174
  return null;
@@ -79,7 +213,7 @@ var pathToFolderName = (absolutePath) => {
79
213
  }
80
214
  return convertNonAscii(absolutePath).replace(/^\//g, "-").replace(/\/\./g, "--").replace(/\//g, "-").replace(/\./g, "-");
81
215
  };
82
- var tryGetCwdFromFile = (filePath, fileSystem = fs, logger2 = log) => {
216
+ var tryGetCwdFromFile = (filePath, fileSystem = fs2, logger2 = log) => {
83
217
  const basename3 = path.basename(filePath);
84
218
  try {
85
219
  const content = fileSystem.readFileSync(filePath, "utf-8");
@@ -99,7 +233,7 @@ var tryGetCwdFromFile = (filePath, fileSystem = fs, logger2 = log) => {
99
233
  return null;
100
234
  }
101
235
  };
102
- var getRealPathFromSession = (folderName, sessionsDir = getSessionsDir(), fileSystem = fs, logger2 = log) => {
236
+ var getRealPathFromSession = (folderName, sessionsDir = getSessionsDir(), fileSystem = fs2, logger2 = log) => {
103
237
  const projectDir = path.join(sessionsDir, folderName);
104
238
  try {
105
239
  const files = fileSystem.readdirSync(projectDir).filter(isSessionFile);
@@ -135,7 +269,7 @@ var folderNameToPath = (folderName) => {
135
269
  const absolutePath = folderNameToDisplayPath(folderName);
136
270
  return toRelativePath(absolutePath, homeDir);
137
271
  };
138
- var findProjectByWorkspacePath = (workspacePath, projectNames, sessionsDir = getSessionsDir(), fileSystem = fs, logger2 = log) => {
272
+ var findProjectByWorkspacePath = (workspacePath, projectNames, sessionsDir = getSessionsDir(), fileSystem = fs2, logger2 = log) => {
139
273
  const directMatch = pathToFolderName(workspacePath);
140
274
  if (projectNames.includes(directMatch)) {
141
275
  return directMatch;
@@ -160,100 +294,7 @@ var findProjectByWorkspacePath = (workspacePath, projectNames, sessionsDir = get
160
294
  return null;
161
295
  };
162
296
 
163
- // src/utils.ts
164
- var logger = createLogger("utils");
165
- var extractTextContent = (message) => {
166
- if (!message) return "";
167
- const content = message.content;
168
- if (!content) return "";
169
- if (typeof content === "string") return content;
170
- if (Array.isArray(content)) {
171
- return content.filter((item) => typeof item === "object" && item?.type === "text").map((item) => {
172
- if (item.text == null) {
173
- logger.warn("TextContent item has undefined or null text property");
174
- return "";
175
- }
176
- return item.text;
177
- }).join("");
178
- }
179
- return "";
180
- };
181
- var extractTitle = (text) => {
182
- if (!text) return "Untitled";
183
- let cleaned = text.replace(/<ide_[^>]*>[\s\S]*?<\/ide_[^>]*>/g, "").trim();
184
- if (!cleaned) return "Untitled";
185
- if (cleaned.includes("\n\n")) {
186
- cleaned = cleaned.split("\n\n")[0];
187
- } else if (cleaned.includes("\n")) {
188
- cleaned = cleaned.split("\n")[0];
189
- }
190
- if (cleaned.length > 100) {
191
- return cleaned.slice(0, 100) + "...";
192
- }
193
- return cleaned || "Untitled";
194
- };
195
- var isInvalidApiKeyMessage = (msg) => {
196
- const text = extractTextContent(msg.message);
197
- return text.includes("Invalid API key");
198
- };
199
- var ERROR_SESSION_PATTERNS = [
200
- "API Error",
201
- "authentication_error",
202
- "Invalid API key",
203
- "OAuth token has expired",
204
- "Please run /login"
205
- ];
206
- var isErrorSessionTitle = (title) => {
207
- if (!title) return false;
208
- return ERROR_SESSION_PATTERNS.some((pattern) => title.includes(pattern));
209
- };
210
- var isContinuationSummary = (msg) => {
211
- if (msg.isCompactSummary === true) return true;
212
- if (msg.type !== "user") return false;
213
- const text = extractTextContent(msg.message);
214
- return text.startsWith("This session is being continued from");
215
- };
216
- var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallback = "Untitled") => {
217
- if (customTitle) return customTitle;
218
- if (currentSummary) {
219
- return currentSummary.length > maxLength ? currentSummary.slice(0, maxLength - 3) + "..." : currentSummary;
220
- }
221
- if (title && title !== "Untitled") return title;
222
- return fallback;
223
- };
224
- var replaceMessageContent = (msg, text) => ({
225
- ...msg,
226
- message: {
227
- ...msg.message,
228
- content: [{ type: "text", text }]
229
- },
230
- toolUseResult: void 0
231
- });
232
- var cleanupSplitFirstMessage = (msg) => {
233
- const toolUseResult = msg.toolUseResult;
234
- if (!toolUseResult) return msg;
235
- if (typeof toolUseResult === "object" && "answers" in toolUseResult) {
236
- const answers = toolUseResult.answers;
237
- const qaText = Object.entries(answers).map(([q, a]) => `Q: ${q}
238
- A: ${a}`).join("\n\n");
239
- return replaceMessageContent(msg, qaText);
240
- }
241
- if (typeof toolUseResult === "string") {
242
- const rejectionMarker = "The user provided the following reason for the rejection:";
243
- const rejectionIndex = toolUseResult.indexOf(rejectionMarker);
244
- if (rejectionIndex === -1) return msg;
245
- const text = toolUseResult.slice(rejectionIndex + rejectionMarker.length).trim();
246
- if (!text) return msg;
247
- return replaceMessageContent(msg, text);
248
- }
249
- return msg;
250
- };
251
- var maskHomePath = (text, homeDir) => {
252
- if (!homeDir) return text;
253
- const escapedHome = homeDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
254
- const regex = new RegExp(`${escapedHome}(?=[/\\\\]|$)`, "g");
255
- return text.replace(regex, "~");
256
- };
297
+ // src/projects.ts
257
298
  var sortProjects = (projects, options = {}) => {
258
299
  const { currentProjectName, homeDir, filterEmpty = true } = options;
259
300
  const filtered = filterEmpty ? projects.filter((p) => p.sessionCount > 0) : projects;
@@ -274,17 +315,17 @@ var sortProjects = (projects, options = {}) => {
274
315
  };
275
316
 
276
317
  // src/agents.ts
277
- import { Effect } from "effect";
278
- import * as fs2 from "fs/promises";
318
+ import { Effect as Effect2 } from "effect";
319
+ import * as fs3 from "fs/promises";
279
320
  import * as path2 from "path";
280
- var findLinkedAgents = (projectName, sessionId) => Effect.gen(function* () {
321
+ var findLinkedAgents = (projectName, sessionId) => Effect2.gen(function* () {
281
322
  const projectPath = path2.join(getSessionsDir(), projectName);
282
- const files = yield* Effect.tryPromise(() => fs2.readdir(projectPath));
323
+ const files = yield* Effect2.tryPromise(() => fs3.readdir(projectPath));
283
324
  const agentFiles = files.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
284
325
  const linkedAgents = [];
285
326
  for (const agentFile of agentFiles) {
286
327
  const filePath = path2.join(projectPath, agentFile);
287
- const content = yield* Effect.tryPromise(() => fs2.readFile(filePath, "utf-8"));
328
+ const content = yield* Effect2.tryPromise(() => fs3.readFile(filePath, "utf-8"));
288
329
  const firstLine = content.split("\n")[0];
289
330
  if (firstLine) {
290
331
  try {
@@ -298,16 +339,16 @@ var findLinkedAgents = (projectName, sessionId) => Effect.gen(function* () {
298
339
  }
299
340
  return linkedAgents;
300
341
  });
301
- var findOrphanAgentsWithPaths = (projectName) => Effect.gen(function* () {
342
+ var findOrphanAgentsWithPaths = (projectName) => Effect2.gen(function* () {
302
343
  const projectPath = path2.join(getSessionsDir(), projectName);
303
- const files = yield* Effect.tryPromise(() => fs2.readdir(projectPath));
344
+ const files = yield* Effect2.tryPromise(() => fs3.readdir(projectPath));
304
345
  const sessionIds = new Set(
305
346
  files.filter((f) => !f.startsWith("agent-") && f.endsWith(".jsonl")).map((f) => f.replace(".jsonl", ""))
306
347
  );
307
348
  const orphanAgents = [];
308
349
  const checkAgentFile = async (filePath) => {
309
350
  try {
310
- const content = await fs2.readFile(filePath, "utf-8");
351
+ const content = await fs3.readFile(filePath, "utf-8");
311
352
  const lines = content.split("\n").filter((l) => l.trim());
312
353
  const firstLine = lines[0];
313
354
  if (!firstLine) return null;
@@ -327,27 +368,27 @@ var findOrphanAgentsWithPaths = (projectName) => Effect.gen(function* () {
327
368
  const rootAgentFiles = files.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
328
369
  for (const agentFile of rootAgentFiles) {
329
370
  const filePath = path2.join(projectPath, agentFile);
330
- const orphan = yield* Effect.tryPromise(() => checkAgentFile(filePath));
371
+ const orphan = yield* Effect2.tryPromise(() => checkAgentFile(filePath));
331
372
  if (orphan) {
332
373
  orphanAgents.push({ ...orphan, filePath });
333
374
  }
334
375
  }
335
376
  for (const entry of files) {
336
377
  const entryPath = path2.join(projectPath, entry);
337
- const stat3 = yield* Effect.tryPromise(() => fs2.stat(entryPath).catch(() => null));
338
- if (stat3?.isDirectory() && !entry.startsWith(".")) {
378
+ const stat4 = yield* Effect2.tryPromise(() => fs3.stat(entryPath).catch(() => null));
379
+ if (stat4?.isDirectory() && !entry.startsWith(".")) {
339
380
  const subagentsPath = path2.join(entryPath, "subagents");
340
- const subagentsExists = yield* Effect.tryPromise(
341
- () => fs2.stat(subagentsPath).then(() => true).catch(() => false)
381
+ const subagentsExists = yield* Effect2.tryPromise(
382
+ () => fs3.stat(subagentsPath).then(() => true).catch(() => false)
342
383
  );
343
384
  if (subagentsExists) {
344
- const subagentFiles = yield* Effect.tryPromise(
345
- () => fs2.readdir(subagentsPath).catch(() => [])
385
+ const subagentFiles = yield* Effect2.tryPromise(
386
+ () => fs3.readdir(subagentsPath).catch(() => [])
346
387
  );
347
388
  for (const subagentFile of subagentFiles) {
348
389
  if (subagentFile.startsWith("agent-") && subagentFile.endsWith(".jsonl")) {
349
390
  const filePath = path2.join(subagentsPath, subagentFile);
350
- const orphan = yield* Effect.tryPromise(() => checkAgentFile(filePath));
391
+ const orphan = yield* Effect2.tryPromise(() => checkAgentFile(filePath));
351
392
  if (orphan) {
352
393
  orphanAgents.push({ ...orphan, filePath });
353
394
  }
@@ -358,11 +399,11 @@ var findOrphanAgentsWithPaths = (projectName) => Effect.gen(function* () {
358
399
  }
359
400
  return orphanAgents;
360
401
  });
361
- var findOrphanAgents = (projectName) => Effect.gen(function* () {
402
+ var findOrphanAgents = (projectName) => Effect2.gen(function* () {
362
403
  const orphans = yield* findOrphanAgentsWithPaths(projectName);
363
404
  return orphans.map(({ agentId, sessionId }) => ({ agentId, sessionId }));
364
405
  });
365
- var deleteOrphanAgents = (projectName) => Effect.gen(function* () {
406
+ var deleteOrphanAgents = (projectName) => Effect2.gen(function* () {
366
407
  const projectPath = path2.join(getSessionsDir(), projectName);
367
408
  const orphans = yield* findOrphanAgentsWithPaths(projectName);
368
409
  const deletedAgents = [];
@@ -376,35 +417,35 @@ var deleteOrphanAgents = (projectName) => Effect.gen(function* () {
376
417
  foldersToCheck.add(parentDir);
377
418
  }
378
419
  if (orphan.lineCount <= 2) {
379
- yield* Effect.tryPromise(() => fs2.unlink(orphan.filePath));
420
+ yield* Effect2.tryPromise(() => fs3.unlink(orphan.filePath));
380
421
  deletedAgents.push(orphan.agentId);
381
422
  } else {
382
423
  if (!backupDirCreated) {
383
424
  const backupDir2 = path2.join(projectPath, ".bak");
384
- yield* Effect.tryPromise(() => fs2.mkdir(backupDir2, { recursive: true }));
425
+ yield* Effect2.tryPromise(() => fs3.mkdir(backupDir2, { recursive: true }));
385
426
  backupDirCreated = true;
386
427
  }
387
428
  const backupDir = path2.join(projectPath, ".bak");
388
429
  const agentBackupPath = path2.join(backupDir, `${orphan.agentId}.jsonl`);
389
- yield* Effect.tryPromise(() => fs2.rename(orphan.filePath, agentBackupPath));
430
+ yield* Effect2.tryPromise(() => fs3.rename(orphan.filePath, agentBackupPath));
390
431
  backedUpAgents.push(orphan.agentId);
391
432
  }
392
433
  }
393
434
  for (const subagentsDir of foldersToCheck) {
394
- const isEmpty = yield* Effect.tryPromise(async () => {
395
- const files = await fs2.readdir(subagentsDir);
435
+ const isEmpty = yield* Effect2.tryPromise(async () => {
436
+ const files = await fs3.readdir(subagentsDir);
396
437
  return files.length === 0;
397
438
  });
398
439
  if (isEmpty) {
399
- yield* Effect.tryPromise(() => fs2.rmdir(subagentsDir));
440
+ yield* Effect2.tryPromise(() => fs3.rmdir(subagentsDir));
400
441
  cleanedFolders.push(subagentsDir);
401
442
  const sessionDir = path2.dirname(subagentsDir);
402
- const sessionDirEmpty = yield* Effect.tryPromise(async () => {
403
- const files = await fs2.readdir(sessionDir);
443
+ const sessionDirEmpty = yield* Effect2.tryPromise(async () => {
444
+ const files = await fs3.readdir(sessionDir);
404
445
  return files.length === 0;
405
446
  });
406
447
  if (sessionDirEmpty) {
407
- yield* Effect.tryPromise(() => fs2.rmdir(sessionDir));
448
+ yield* Effect2.tryPromise(() => fs3.rmdir(sessionDir));
408
449
  cleanedFolders.push(sessionDir);
409
450
  }
410
451
  }
@@ -420,33 +461,31 @@ var deleteOrphanAgents = (projectName) => Effect.gen(function* () {
420
461
  count: deletedAgents.length + backedUpAgents.length
421
462
  };
422
463
  });
423
- var loadAgentMessages = (projectName, _sessionId, agentId) => Effect.gen(function* () {
464
+ var loadAgentMessages = (projectName, _sessionId, agentId) => Effect2.gen(function* () {
424
465
  const projectPath = path2.join(getSessionsDir(), projectName);
425
466
  const agentFilePath = path2.join(projectPath, `${agentId}.jsonl`);
426
- const content = yield* Effect.tryPromise(() => fs2.readFile(agentFilePath, "utf-8"));
467
+ const content = yield* Effect2.tryPromise(() => fs3.readFile(agentFilePath, "utf-8"));
427
468
  const lines = content.split("\n").filter((line) => line.trim());
428
469
  const messages = [];
429
- for (const line of lines) {
430
- try {
431
- const parsed = JSON.parse(line);
432
- if ("sessionId" in parsed && !("type" in parsed)) {
433
- continue;
434
- }
435
- messages.push(parsed);
436
- } catch {
470
+ for (let i = 0; i < lines.length; i++) {
471
+ const parsed = tryParseJsonLine(lines[i], i + 1, agentFilePath);
472
+ if (!parsed) continue;
473
+ if ("sessionId" in parsed && !("type" in parsed)) {
474
+ continue;
437
475
  }
476
+ messages.push(parsed);
438
477
  }
439
478
  return messages;
440
479
  });
441
480
 
442
481
  // src/todos.ts
443
- import { Effect as Effect2 } from "effect";
444
- import * as fs3 from "fs/promises";
482
+ import { Effect as Effect3 } from "effect";
483
+ import * as fs4 from "fs/promises";
445
484
  import * as path3 from "path";
446
- var findLinkedTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
485
+ var findLinkedTodos = (sessionId, agentIds = []) => Effect3.gen(function* () {
447
486
  const todosDir = getTodosDir();
448
- const exists = yield* Effect2.tryPromise(
449
- () => fs3.access(todosDir).then(() => true).catch(() => false)
487
+ const exists = yield* Effect3.tryPromise(
488
+ () => fs4.access(todosDir).then(() => true).catch(() => false)
450
489
  );
451
490
  if (!exists) {
452
491
  return {
@@ -458,17 +497,17 @@ var findLinkedTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
458
497
  }
459
498
  const sessionTodoPath = path3.join(todosDir, `${sessionId}.json`);
460
499
  let sessionTodos = [];
461
- const sessionTodoExists = yield* Effect2.tryPromise(
462
- () => fs3.access(sessionTodoPath).then(() => true).catch(() => false)
500
+ const sessionTodoExists = yield* Effect3.tryPromise(
501
+ () => fs4.access(sessionTodoPath).then(() => true).catch(() => false)
463
502
  );
464
503
  if (sessionTodoExists) {
465
- const content = yield* Effect2.tryPromise(() => fs3.readFile(sessionTodoPath, "utf-8"));
504
+ const content = yield* Effect3.tryPromise(() => fs4.readFile(sessionTodoPath, "utf-8"));
466
505
  try {
467
506
  sessionTodos = JSON.parse(content);
468
507
  } catch {
469
508
  }
470
509
  }
471
- const allFiles = yield* Effect2.tryPromise(() => fs3.readdir(todosDir));
510
+ const allFiles = yield* Effect3.tryPromise(() => fs4.readdir(todosDir));
472
511
  const agentTodoPattern = new RegExp(`^${sessionId}-agent-([a-f0-9-]+)\\.json$`);
473
512
  const discoveredAgentIds = new Set(agentIds);
474
513
  for (const file of allFiles) {
@@ -481,11 +520,11 @@ var findLinkedTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
481
520
  for (const agentId of discoveredAgentIds) {
482
521
  const shortAgentId = agentId.replace("agent-", "");
483
522
  const agentTodoPath = path3.join(todosDir, `${sessionId}-agent-${shortAgentId}.json`);
484
- const agentTodoExists = yield* Effect2.tryPromise(
485
- () => fs3.access(agentTodoPath).then(() => true).catch(() => false)
523
+ const agentTodoExists = yield* Effect3.tryPromise(
524
+ () => fs4.access(agentTodoPath).then(() => true).catch(() => false)
486
525
  );
487
526
  if (agentTodoExists) {
488
- const content = yield* Effect2.tryPromise(() => fs3.readFile(agentTodoPath, "utf-8"));
527
+ const content = yield* Effect3.tryPromise(() => fs4.readFile(agentTodoPath, "utf-8"));
489
528
  try {
490
529
  const todos = JSON.parse(content);
491
530
  if (todos.length > 0) {
@@ -503,25 +542,25 @@ var findLinkedTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
503
542
  hasTodos
504
543
  };
505
544
  });
506
- var sessionHasTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
545
+ var sessionHasTodos = (sessionId, agentIds = []) => Effect3.gen(function* () {
507
546
  const todosDir = getTodosDir();
508
- const exists = yield* Effect2.tryPromise(
509
- () => fs3.access(todosDir).then(() => true).catch(() => false)
547
+ const exists = yield* Effect3.tryPromise(
548
+ () => fs4.access(todosDir).then(() => true).catch(() => false)
510
549
  );
511
550
  if (!exists) return false;
512
551
  const sessionTodoPath = path3.join(todosDir, `${sessionId}.json`);
513
- const sessionTodoExists = yield* Effect2.tryPromise(
514
- () => fs3.access(sessionTodoPath).then(() => true).catch(() => false)
552
+ const sessionTodoExists = yield* Effect3.tryPromise(
553
+ () => fs4.access(sessionTodoPath).then(() => true).catch(() => false)
515
554
  );
516
555
  if (sessionTodoExists) {
517
- const content = yield* Effect2.tryPromise(() => fs3.readFile(sessionTodoPath, "utf-8"));
556
+ const content = yield* Effect3.tryPromise(() => fs4.readFile(sessionTodoPath, "utf-8"));
518
557
  try {
519
558
  const todos = JSON.parse(content);
520
559
  if (todos.length > 0) return true;
521
560
  } catch {
522
561
  }
523
562
  }
524
- const allFiles = yield* Effect2.tryPromise(() => fs3.readdir(todosDir));
563
+ const allFiles = yield* Effect3.tryPromise(() => fs4.readdir(todosDir));
525
564
  const agentTodoPattern = new RegExp(`^${sessionId}-agent-([a-f0-9-]+)\\.json$`);
526
565
  const discoveredAgentIds = new Set(agentIds);
527
566
  for (const file of allFiles) {
@@ -533,11 +572,11 @@ var sessionHasTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
533
572
  for (const agentId of discoveredAgentIds) {
534
573
  const shortAgentId = agentId.replace("agent-", "");
535
574
  const agentTodoPath = path3.join(todosDir, `${sessionId}-agent-${shortAgentId}.json`);
536
- const agentTodoExists = yield* Effect2.tryPromise(
537
- () => fs3.access(agentTodoPath).then(() => true).catch(() => false)
575
+ const agentTodoExists = yield* Effect3.tryPromise(
576
+ () => fs4.access(agentTodoPath).then(() => true).catch(() => false)
538
577
  );
539
578
  if (agentTodoExists) {
540
- const content = yield* Effect2.tryPromise(() => fs3.readFile(agentTodoPath, "utf-8"));
579
+ const content = yield* Effect3.tryPromise(() => fs4.readFile(agentTodoPath, "utf-8"));
541
580
  try {
542
581
  const todos = JSON.parse(content);
543
582
  if (todos.length > 0) return true;
@@ -547,60 +586,60 @@ var sessionHasTodos = (sessionId, agentIds = []) => Effect2.gen(function* () {
547
586
  }
548
587
  return false;
549
588
  });
550
- var deleteLinkedTodos = (sessionId, agentIds) => Effect2.gen(function* () {
589
+ var deleteLinkedTodos = (sessionId, agentIds) => Effect3.gen(function* () {
551
590
  const todosDir = getTodosDir();
552
- const exists = yield* Effect2.tryPromise(
553
- () => fs3.access(todosDir).then(() => true).catch(() => false)
591
+ const exists = yield* Effect3.tryPromise(
592
+ () => fs4.access(todosDir).then(() => true).catch(() => false)
554
593
  );
555
594
  if (!exists) return { deletedCount: 0 };
556
595
  const backupDir = path3.join(todosDir, ".bak");
557
- yield* Effect2.tryPromise(() => fs3.mkdir(backupDir, { recursive: true }));
596
+ yield* Effect3.tryPromise(() => fs4.mkdir(backupDir, { recursive: true }));
558
597
  let deletedCount = 0;
559
598
  const sessionTodoPath = path3.join(todosDir, `${sessionId}.json`);
560
- const sessionTodoExists = yield* Effect2.tryPromise(
561
- () => fs3.access(sessionTodoPath).then(() => true).catch(() => false)
599
+ const sessionTodoExists = yield* Effect3.tryPromise(
600
+ () => fs4.access(sessionTodoPath).then(() => true).catch(() => false)
562
601
  );
563
602
  if (sessionTodoExists) {
564
603
  const backupPath = path3.join(backupDir, `${sessionId}.json`);
565
- yield* Effect2.tryPromise(() => fs3.rename(sessionTodoPath, backupPath));
604
+ yield* Effect3.tryPromise(() => fs4.rename(sessionTodoPath, backupPath));
566
605
  deletedCount++;
567
606
  }
568
607
  for (const agentId of agentIds) {
569
608
  const shortAgentId = agentId.replace("agent-", "");
570
609
  const agentTodoPath = path3.join(todosDir, `${sessionId}-agent-${shortAgentId}.json`);
571
- const agentTodoExists = yield* Effect2.tryPromise(
572
- () => fs3.access(agentTodoPath).then(() => true).catch(() => false)
610
+ const agentTodoExists = yield* Effect3.tryPromise(
611
+ () => fs4.access(agentTodoPath).then(() => true).catch(() => false)
573
612
  );
574
613
  if (agentTodoExists) {
575
614
  const backupPath = path3.join(backupDir, `${sessionId}-agent-${shortAgentId}.json`);
576
- yield* Effect2.tryPromise(() => fs3.rename(agentTodoPath, backupPath));
615
+ yield* Effect3.tryPromise(() => fs4.rename(agentTodoPath, backupPath));
577
616
  deletedCount++;
578
617
  }
579
618
  }
580
619
  return { deletedCount };
581
620
  });
582
- var findOrphanTodos = () => Effect2.gen(function* () {
621
+ var findOrphanTodos = () => Effect3.gen(function* () {
583
622
  const todosDir = getTodosDir();
584
623
  const sessionsDir = getSessionsDir();
585
- const [todosExists, sessionsExists] = yield* Effect2.all([
586
- Effect2.tryPromise(
587
- () => fs3.access(todosDir).then(() => true).catch(() => false)
624
+ const [todosExists, sessionsExists] = yield* Effect3.all([
625
+ Effect3.tryPromise(
626
+ () => fs4.access(todosDir).then(() => true).catch(() => false)
588
627
  ),
589
- Effect2.tryPromise(
590
- () => fs3.access(sessionsDir).then(() => true).catch(() => false)
628
+ Effect3.tryPromise(
629
+ () => fs4.access(sessionsDir).then(() => true).catch(() => false)
591
630
  )
592
631
  ]);
593
632
  if (!todosExists || !sessionsExists) return [];
594
- const todoFiles = yield* Effect2.tryPromise(() => fs3.readdir(todosDir));
633
+ const todoFiles = yield* Effect3.tryPromise(() => fs4.readdir(todosDir));
595
634
  const jsonFiles = todoFiles.filter((f) => f.endsWith(".json"));
596
635
  const validSessionIds = /* @__PURE__ */ new Set();
597
- const projectEntries = yield* Effect2.tryPromise(
598
- () => fs3.readdir(sessionsDir, { withFileTypes: true })
636
+ const projectEntries = yield* Effect3.tryPromise(
637
+ () => fs4.readdir(sessionsDir, { withFileTypes: true })
599
638
  );
600
639
  for (const entry of projectEntries) {
601
640
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
602
641
  const projectPath = path3.join(sessionsDir, entry.name);
603
- const files = yield* Effect2.tryPromise(() => fs3.readdir(projectPath));
642
+ const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
604
643
  for (const f of files) {
605
644
  if (f.endsWith(".jsonl") && !f.startsWith("agent-")) {
606
645
  validSessionIds.add(f.replace(".jsonl", ""));
@@ -619,40 +658,40 @@ var findOrphanTodos = () => Effect2.gen(function* () {
619
658
  }
620
659
  return orphans;
621
660
  });
622
- var deleteOrphanTodos = () => Effect2.gen(function* () {
661
+ var deleteOrphanTodos = () => Effect3.gen(function* () {
623
662
  const todosDir = getTodosDir();
624
663
  const orphans = yield* findOrphanTodos();
625
664
  if (orphans.length === 0) return { success: true, deletedCount: 0 };
626
665
  const backupDir = path3.join(todosDir, ".bak");
627
- yield* Effect2.tryPromise(() => fs3.mkdir(backupDir, { recursive: true }));
666
+ yield* Effect3.tryPromise(() => fs4.mkdir(backupDir, { recursive: true }));
628
667
  let deletedCount = 0;
629
668
  for (const orphan of orphans) {
630
669
  const filePath = path3.join(todosDir, orphan);
631
670
  const backupPath = path3.join(backupDir, orphan);
632
- yield* Effect2.tryPromise(() => fs3.rename(filePath, backupPath));
671
+ yield* Effect3.tryPromise(() => fs4.rename(filePath, backupPath));
633
672
  deletedCount++;
634
673
  }
635
674
  return { success: true, deletedCount };
636
675
  });
637
676
 
638
- // src/session.ts
639
- import { Effect as Effect3, pipe, Array as A, Option as O } from "effect";
640
- import * as fs4 from "fs/promises";
677
+ // src/session/projects.ts
678
+ import { Effect as Effect4 } from "effect";
679
+ import * as fs5 from "fs/promises";
641
680
  import * as path4 from "path";
642
- var listProjects = Effect3.gen(function* () {
681
+ var listProjects = Effect4.gen(function* () {
643
682
  const sessionsDir = getSessionsDir();
644
- const exists = yield* Effect3.tryPromise(
645
- () => fs4.access(sessionsDir).then(() => true).catch(() => false)
683
+ const exists = yield* Effect4.tryPromise(
684
+ () => fs5.access(sessionsDir).then(() => true).catch(() => false)
646
685
  );
647
686
  if (!exists) {
648
687
  return [];
649
688
  }
650
- const entries = yield* Effect3.tryPromise(() => fs4.readdir(sessionsDir, { withFileTypes: true }));
651
- const projects = yield* Effect3.all(
689
+ const entries = yield* Effect4.tryPromise(() => fs5.readdir(sessionsDir, { withFileTypes: true }));
690
+ const projects = yield* Effect4.all(
652
691
  entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map(
653
- (entry) => Effect3.gen(function* () {
692
+ (entry) => Effect4.gen(function* () {
654
693
  const projectPath = path4.join(sessionsDir, entry.name);
655
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
694
+ const files = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
656
695
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
657
696
  return {
658
697
  name: entry.name,
@@ -666,87 +705,279 @@ var listProjects = Effect3.gen(function* () {
666
705
  );
667
706
  return projects;
668
707
  });
669
- var listSessions = (projectName) => Effect3.gen(function* () {
670
- const projectPath = path4.join(getSessionsDir(), projectName);
671
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
672
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
673
- const sessions = yield* Effect3.all(
674
- 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"));
678
- const lines = content.trim().split("\n").filter(Boolean);
679
- const messages = lines.map((line) => JSON.parse(line));
680
- const sessionId = file.replace(".jsonl", "");
681
- const userAssistantMessages = messages.filter(
682
- (m) => m.type === "user" || m.type === "assistant"
683
- );
684
- const hasSummary = messages.some((m) => m.type === "summary");
685
- const firstMessage = userAssistantMessages[0];
686
- const lastMessage = userAssistantMessages[userAssistantMessages.length - 1];
687
- const title = pipe(
688
- messages,
689
- A.findFirst((m) => m.type === "user"),
690
- O.map((m) => {
691
- const text = extractTextContent(m.message);
692
- return extractTitle(text);
693
- }),
694
- O.getOrElse(() => hasSummary ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`)
695
- );
696
- return {
697
- id: sessionId,
698
- projectName,
699
- title,
700
- // If session has summary but no user/assistant messages, count as 1
701
- messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : hasSummary ? 1 : 0,
702
- createdAt: firstMessage?.timestamp,
703
- updatedAt: lastMessage?.timestamp
704
- };
705
- })
706
- ),
707
- { concurrency: 10 }
708
- );
709
- return sessions.sort((a, b) => {
710
- const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
711
- const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
712
- return dateB - dateA;
713
- });
714
- });
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"));
718
- const lines = content.trim().split("\n").filter(Boolean);
719
- return lines.map((line) => JSON.parse(line));
720
- });
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"));
724
- const lines = content.trim().split("\n").filter(Boolean);
725
- 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
- );
708
+
709
+ // src/session/crud.ts
710
+ import { Effect as Effect5, pipe, Array as A, Option as O } from "effect";
711
+ import * as fs6 from "fs/promises";
712
+ import * as path5 from "path";
713
+ import * as crypto from "crypto";
714
+
715
+ // src/session/validation.ts
716
+ function validateChain(messages) {
717
+ const errors = [];
718
+ const uuids = /* @__PURE__ */ new Set();
719
+ for (const msg of messages) {
720
+ if (msg.uuid) {
721
+ uuids.add(msg.uuid);
722
+ }
723
+ }
724
+ let foundFirstMessage = false;
725
+ for (let i = 0; i < messages.length; i++) {
726
+ const msg = messages[i];
727
+ if (msg.type === "file-history-snapshot") {
728
+ continue;
729
+ }
730
+ if (!msg.uuid) {
731
+ continue;
732
+ }
733
+ if (!foundFirstMessage) {
734
+ foundFirstMessage = true;
735
+ if (msg.parentUuid === null) {
736
+ continue;
737
+ }
738
+ if (msg.parentUuid === void 0) {
739
+ errors.push({
740
+ type: "broken_chain",
741
+ uuid: msg.uuid,
742
+ line: i + 1,
743
+ parentUuid: null
744
+ });
745
+ continue;
746
+ }
747
+ }
748
+ if (msg.parentUuid === null || msg.parentUuid === void 0) {
749
+ errors.push({
750
+ type: "broken_chain",
751
+ uuid: msg.uuid,
752
+ line: i + 1,
753
+ parentUuid: null
754
+ });
755
+ continue;
756
+ }
757
+ if (!uuids.has(msg.parentUuid)) {
758
+ errors.push({
759
+ type: "orphan_parent",
760
+ uuid: msg.uuid,
761
+ line: i + 1,
762
+ parentUuid: msg.parentUuid
763
+ });
764
+ }
765
+ }
766
+ return {
767
+ valid: errors.length === 0,
768
+ errors
769
+ };
770
+ }
771
+ function validateToolUseResult(messages) {
772
+ const errors = [];
773
+ const toolUseIds = /* @__PURE__ */ new Set();
774
+ for (const msg of messages) {
775
+ const content = msg.message?.content;
776
+ if (Array.isArray(content)) {
777
+ for (const item of content) {
778
+ if (item.type === "tool_use" && item.id) {
779
+ toolUseIds.add(item.id);
780
+ }
781
+ }
782
+ }
783
+ }
784
+ for (let i = 0; i < messages.length; i++) {
785
+ const msg = messages[i];
786
+ const content = msg.message?.content;
787
+ if (!Array.isArray(content)) {
788
+ continue;
789
+ }
790
+ for (const item of content) {
791
+ if (item.type === "tool_result" && item.tool_use_id) {
792
+ if (!toolUseIds.has(item.tool_use_id)) {
793
+ errors.push({
794
+ type: "orphan_tool_result",
795
+ uuid: msg.uuid || "",
796
+ line: i + 1,
797
+ toolUseId: item.tool_use_id
798
+ });
799
+ }
800
+ }
801
+ }
802
+ }
803
+ return {
804
+ valid: errors.length === 0,
805
+ errors
806
+ };
807
+ }
808
+ function deleteMessageWithChainRepair(messages, targetId, targetType) {
809
+ let targetIndex = -1;
810
+ if (targetType === "file-history-snapshot") {
811
+ targetIndex = messages.findIndex(
812
+ (m) => m.type === "file-history-snapshot" && m.messageId === targetId
813
+ );
814
+ } else if (targetType === "summary") {
815
+ targetIndex = messages.findIndex(
816
+ (m) => m.leafUuid === targetId
817
+ );
818
+ } else {
819
+ targetIndex = messages.findIndex((m) => m.uuid === targetId);
820
+ if (targetIndex === -1) {
821
+ targetIndex = messages.findIndex(
822
+ (m) => m.leafUuid === targetId
823
+ );
824
+ }
825
+ if (targetIndex === -1) {
826
+ targetIndex = messages.findIndex(
827
+ (m) => m.type === "file-history-snapshot" && m.messageId === targetId
828
+ );
829
+ }
830
+ }
729
831
  if (targetIndex === -1) {
730
- return { success: false, error: "Message not found" };
832
+ return { deleted: null, alsoDeleted: [] };
731
833
  }
732
834
  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;
835
+ const toolUseIds = [];
836
+ if (deletedMsg.type === "assistant") {
837
+ const content = deletedMsg.message?.content;
838
+ if (Array.isArray(content)) {
839
+ for (const item of content) {
840
+ if (item.type === "tool_use" && item.id) {
841
+ toolUseIds.push(item.id);
842
+ }
843
+ }
738
844
  }
739
845
  }
740
- messages.splice(targetIndex, 1);
846
+ const toolResultIndices = [];
847
+ if (toolUseIds.length > 0) {
848
+ for (let i = 0; i < messages.length; i++) {
849
+ const msg = messages[i];
850
+ if (msg.type === "user") {
851
+ const content = msg.message?.content;
852
+ if (Array.isArray(content)) {
853
+ for (const item of content) {
854
+ if (item.type === "tool_result" && item.tool_use_id && toolUseIds.includes(item.tool_use_id)) {
855
+ toolResultIndices.push(i);
856
+ break;
857
+ }
858
+ }
859
+ }
860
+ }
861
+ }
862
+ }
863
+ const indicesToDelete = [targetIndex, ...toolResultIndices].sort((a, b) => b - a);
864
+ for (const idx of indicesToDelete) {
865
+ const msg = messages[idx];
866
+ const isInParentChain = msg.type !== "file-history-snapshot" && msg.uuid;
867
+ if (isInParentChain) {
868
+ const deletedUuid = msg.uuid;
869
+ const parentUuid = msg.parentUuid;
870
+ for (const m of messages) {
871
+ if (m.parentUuid === deletedUuid) {
872
+ m.parentUuid = parentUuid;
873
+ }
874
+ }
875
+ }
876
+ }
877
+ const alsoDeleted = toolResultIndices.map((i) => messages[i]);
878
+ for (const idx of indicesToDelete) {
879
+ messages.splice(idx, 1);
880
+ }
881
+ return { deleted: deletedMsg, alsoDeleted };
882
+ }
883
+
884
+ // src/session/crud.ts
885
+ var updateSessionSummary = (projectName, sessionId, newSummary) => Effect5.gen(function* () {
886
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
887
+ const messages = yield* readJsonlFile(filePath);
888
+ const summaryIdx = messages.findIndex((m) => m.type === "summary");
889
+ if (summaryIdx >= 0) {
890
+ messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
891
+ } else {
892
+ const firstUserMsg = messages.find((m) => m.type === "user");
893
+ const summaryMsg = {
894
+ type: "summary",
895
+ summary: newSummary,
896
+ leafUuid: firstUserMsg?.uuid ?? null
897
+ };
898
+ messages.unshift(summaryMsg);
899
+ }
741
900
  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 };
901
+ yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
902
+ return { success: true };
744
903
  });
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"));
748
- const lines = content.trim().split("\n").filter(Boolean);
749
- const messages = lines.map((line) => JSON.parse(line));
904
+ var listSessions = (projectName) => Effect5.gen(function* () {
905
+ const projectPath = path5.join(getSessionsDir(), projectName);
906
+ const files = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
907
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
908
+ const sessions = yield* Effect5.all(
909
+ sessionFiles.map(
910
+ (file) => Effect5.gen(function* () {
911
+ const filePath = path5.join(projectPath, file);
912
+ const messages = yield* readJsonlFile(filePath);
913
+ const sessionId = file.replace(".jsonl", "");
914
+ const userAssistantMessages = messages.filter(
915
+ (m) => m.type === "user" || m.type === "assistant"
916
+ );
917
+ const hasSummary = messages.some((m) => m.type === "summary");
918
+ const firstMessage = userAssistantMessages[0];
919
+ const lastMessage = userAssistantMessages[userAssistantMessages.length - 1];
920
+ const title = pipe(
921
+ messages,
922
+ A.findFirst((m) => m.type === "user"),
923
+ O.map((m) => {
924
+ const text = extractTextContent(m.message);
925
+ return extractTitle(text);
926
+ }),
927
+ O.getOrElse(() => hasSummary ? "[Summary Only]" : `Session ${sessionId.slice(0, 8)}`)
928
+ );
929
+ const currentSummary = pipe(
930
+ messages,
931
+ A.findFirst((m) => m.type === "summary"),
932
+ O.map((m) => m.summary),
933
+ O.getOrUndefined
934
+ );
935
+ const customTitle = pipe(
936
+ messages,
937
+ A.findFirst((m) => m.type === "custom-title"),
938
+ O.map((m) => m.customTitle),
939
+ O.flatMap(O.fromNullable),
940
+ O.getOrUndefined
941
+ );
942
+ return {
943
+ id: sessionId,
944
+ projectName,
945
+ title,
946
+ customTitle,
947
+ currentSummary,
948
+ // If session has summary but no user/assistant messages, count as 1
949
+ messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : hasSummary ? 1 : 0,
950
+ createdAt: firstMessage?.timestamp,
951
+ updatedAt: lastMessage?.timestamp
952
+ };
953
+ })
954
+ ),
955
+ { concurrency: 10 }
956
+ );
957
+ return sessions.sort((a, b) => {
958
+ const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
959
+ const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
960
+ return dateB - dateA;
961
+ });
962
+ });
963
+ var readSession = (projectName, sessionId) => Effect5.gen(function* () {
964
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
965
+ return yield* readJsonlFile(filePath);
966
+ });
967
+ var deleteMessage = (projectName, sessionId, messageUuid, targetType) => Effect5.gen(function* () {
968
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
969
+ const messages = yield* readJsonlFile(filePath);
970
+ const result = deleteMessageWithChainRepair(messages, messageUuid, targetType);
971
+ if (!result.deleted) {
972
+ return { success: false, error: "Message not found" };
973
+ }
974
+ const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
975
+ yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
976
+ return { success: true, deletedMessage: result.deleted };
977
+ });
978
+ var restoreMessage = (projectName, sessionId, message, index) => Effect5.gen(function* () {
979
+ const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
980
+ const messages = yield* readJsonlFile(filePath);
750
981
  const msgUuid = message.uuid ?? message.messageId;
751
982
  if (!msgUuid) {
752
983
  return { success: false, error: "Message has no uuid or messageId" };
@@ -761,41 +992,41 @@ var restoreMessage = (projectName, sessionId, message, index) => Effect3.gen(fun
761
992
  const insertIndex = Math.min(index, messages.length);
762
993
  messages.splice(insertIndex, 0, message);
763
994
  const newContent = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
764
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
995
+ yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
765
996
  return { success: true };
766
997
  });
767
- var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
998
+ var deleteSession = (projectName, sessionId) => Effect5.gen(function* () {
768
999
  const sessionsDir = getSessionsDir();
769
- const projectPath = path4.join(sessionsDir, projectName);
770
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
1000
+ const projectPath = path5.join(sessionsDir, projectName);
1001
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
771
1002
  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 }));
1003
+ const stat4 = yield* Effect5.tryPromise(() => fs6.stat(filePath));
1004
+ if (stat4.size === 0) {
1005
+ yield* Effect5.tryPromise(() => fs6.unlink(filePath));
1006
+ const agentBackupDir2 = path5.join(projectPath, ".bak");
1007
+ yield* Effect5.tryPromise(() => fs6.mkdir(agentBackupDir2, { recursive: true }));
777
1008
  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(() => {
1009
+ const agentPath = path5.join(projectPath, `${agentId}.jsonl`);
1010
+ const agentBackupPath = path5.join(agentBackupDir2, `${agentId}.jsonl`);
1011
+ yield* Effect5.tryPromise(() => fs6.rename(agentPath, agentBackupPath).catch(() => {
781
1012
  }));
782
1013
  }
783
1014
  yield* deleteLinkedTodos(sessionId, linkedAgents);
784
1015
  return { success: true, deletedAgents: linkedAgents.length };
785
1016
  }
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 }));
1017
+ const backupDir = path5.join(sessionsDir, ".bak");
1018
+ yield* Effect5.tryPromise(() => fs6.mkdir(backupDir, { recursive: true }));
1019
+ const agentBackupDir = path5.join(projectPath, ".bak");
1020
+ yield* Effect5.tryPromise(() => fs6.mkdir(agentBackupDir, { recursive: true }));
790
1021
  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(() => {
1022
+ const agentPath = path5.join(projectPath, `${agentId}.jsonl`);
1023
+ const agentBackupPath = path5.join(agentBackupDir, `${agentId}.jsonl`);
1024
+ yield* Effect5.tryPromise(() => fs6.rename(agentPath, agentBackupPath).catch(() => {
794
1025
  }));
795
1026
  }
796
1027
  const todosResult = yield* deleteLinkedTodos(sessionId, linkedAgents);
797
- const backupPath = path4.join(backupDir, `${projectName}_${sessionId}.jsonl`);
798
- yield* Effect3.tryPromise(() => fs4.rename(filePath, backupPath));
1028
+ const backupPath = path5.join(backupDir, `${projectName}_${sessionId}.jsonl`);
1029
+ yield* Effect5.tryPromise(() => fs6.rename(filePath, backupPath));
799
1030
  return {
800
1031
  success: true,
801
1032
  backupPath,
@@ -803,15 +1034,15 @@ var deleteSession = (projectName, sessionId) => Effect3.gen(function* () {
803
1034
  deletedTodos: todosResult.deletedCount
804
1035
  };
805
1036
  });
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"));
1037
+ var renameSession = (projectName, sessionId, newTitle) => Effect5.gen(function* () {
1038
+ const projectPath = path5.join(getSessionsDir(), projectName);
1039
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
1040
+ const content = yield* Effect5.tryPromise(() => fs6.readFile(filePath, "utf-8"));
810
1041
  const lines = content.trim().split("\n").filter(Boolean);
811
1042
  if (lines.length === 0) {
812
1043
  return { success: false, error: "Empty session" };
813
1044
  }
814
- const messages = lines.map((line) => JSON.parse(line));
1045
+ const messages = parseJsonlLines(lines, filePath);
815
1046
  const sessionUuids = /* @__PURE__ */ new Set();
816
1047
  for (const msg of messages) {
817
1048
  if (msg.uuid && typeof msg.uuid === "string") {
@@ -830,16 +1061,14 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
830
1061
  messages.unshift(customTitleRecord);
831
1062
  }
832
1063
  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));
1064
+ yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
1065
+ const projectFiles = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
835
1066
  const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
836
1067
  const summariesTargetingThis = [];
837
1068
  for (const file of allJsonlFiles) {
838
- const otherFilePath = path4.join(projectPath, file);
1069
+ const otherFilePath = path5.join(projectPath, file);
839
1070
  try {
840
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
841
- const otherLines = otherContent.trim().split("\n").filter(Boolean);
842
- const otherMessages = otherLines.map((l) => JSON.parse(l));
1071
+ const otherMessages = yield* readJsonlFile(otherFilePath);
843
1072
  for (let i = 0; i < otherMessages.length; i++) {
844
1073
  const msg = otherMessages[i];
845
1074
  if (msg.type === "summary" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
@@ -857,20 +1086,16 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
857
1086
  if (summariesTargetingThis.length > 0) {
858
1087
  summariesTargetingThis.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
859
1088
  const firstSummary = summariesTargetingThis[0];
860
- const summaryFilePath = path4.join(projectPath, firstSummary.file);
861
- const summaryContent = yield* Effect3.tryPromise(() => fs4.readFile(summaryFilePath, "utf-8"));
862
- const summaryLines = summaryContent.trim().split("\n").filter(Boolean);
863
- const summaryMessages = summaryLines.map((l) => JSON.parse(l));
1089
+ const summaryFilePath = path5.join(projectPath, firstSummary.file);
1090
+ const summaryMessages = yield* readJsonlFile(summaryFilePath);
864
1091
  summaryMessages[firstSummary.idx] = {
865
1092
  ...summaryMessages[firstSummary.idx],
866
1093
  summary: newTitle
867
1094
  };
868
1095
  const newSummaryContent = summaryMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
869
- yield* Effect3.tryPromise(() => fs4.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
1096
+ yield* Effect5.tryPromise(() => fs6.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
870
1097
  } else {
871
- const currentContent = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
872
- const currentLines = currentContent.trim().split("\n").filter(Boolean);
873
- const currentMessages = currentLines.map((l) => JSON.parse(l));
1098
+ const currentMessages = yield* readJsonlFile(filePath);
874
1099
  const firstUserIdx = currentMessages.findIndex((m) => m.type === "user");
875
1100
  if (firstUserIdx >= 0) {
876
1101
  const firstMsg = currentMessages[firstUserIdx];
@@ -887,235 +1112,50 @@ var renameSession = (projectName, sessionId, newTitle) => Effect3.gen(function*
887
1112
 
888
1113
  ${cleanedText}`;
889
1114
  const updatedContent = currentMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
890
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, updatedContent, "utf-8"));
1115
+ yield* Effect5.tryPromise(() => fs6.writeFile(filePath, updatedContent, "utf-8"));
891
1116
  }
892
1117
  }
893
1118
  }
894
1119
  }
895
1120
  return { success: true };
896
1121
  });
897
- var getSessionFiles = (projectName, sessionId) => Effect3.gen(function* () {
898
- const messages = yield* readSession(projectName, sessionId);
899
- const fileChanges = [];
900
- const seenFiles = /* @__PURE__ */ new Set();
901
- for (const msg of messages) {
902
- if (msg.type === "file-history-snapshot") {
903
- const snapshot = msg;
904
- const backups = snapshot.snapshot?.trackedFileBackups;
905
- if (backups && typeof backups === "object") {
906
- for (const filePath of Object.keys(backups)) {
907
- if (!seenFiles.has(filePath)) {
908
- seenFiles.add(filePath);
909
- fileChanges.push({
910
- path: filePath,
911
- action: "modified",
912
- timestamp: snapshot.snapshot?.timestamp,
913
- messageUuid: snapshot.messageId ?? msg.uuid
914
- });
915
- }
916
- }
917
- }
918
- }
919
- if (msg.type === "assistant" && msg.message?.content) {
920
- const content = msg.message.content;
921
- if (Array.isArray(content)) {
922
- for (const item of content) {
923
- if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
924
- const toolUse = item;
925
- if ((toolUse.name === "Write" || toolUse.name === "Edit") && toolUse.input?.file_path) {
926
- const filePath = toolUse.input.file_path;
927
- if (!seenFiles.has(filePath)) {
928
- seenFiles.add(filePath);
929
- fileChanges.push({
930
- path: filePath,
931
- action: toolUse.name === "Write" ? "created" : "modified",
932
- timestamp: msg.timestamp,
933
- messageUuid: msg.uuid
934
- });
935
- }
936
- }
937
- }
938
- }
939
- }
940
- }
941
- }
942
- return {
943
- sessionId,
944
- projectName,
945
- files: fileChanges,
946
- totalChanges: fileChanges.length
947
- };
948
- });
949
- var analyzeSession = (projectName, sessionId) => Effect3.gen(function* () {
950
- const messages = yield* readSession(projectName, sessionId);
951
- let userMessages = 0;
952
- let assistantMessages = 0;
953
- let summaryCount = 0;
954
- let snapshotCount = 0;
955
- const toolUsageMap = /* @__PURE__ */ new Map();
956
- const filesChanged = /* @__PURE__ */ new Set();
957
- const patterns = [];
958
- const milestones = [];
959
- let firstTimestamp;
960
- let lastTimestamp;
961
- for (const msg of messages) {
962
- if (msg.timestamp) {
963
- if (!firstTimestamp) firstTimestamp = msg.timestamp;
964
- lastTimestamp = msg.timestamp;
965
- }
966
- if (msg.type === "user") {
967
- userMessages++;
968
- const content = typeof msg.content === "string" ? msg.content : "";
969
- if (content.toLowerCase().includes("commit") || content.toLowerCase().includes("\uC644\uB8CC")) {
970
- milestones.push({
971
- timestamp: msg.timestamp,
972
- description: `User checkpoint: ${content.slice(0, 50)}...`,
973
- messageUuid: msg.uuid
974
- });
975
- }
976
- } else if (msg.type === "assistant") {
977
- assistantMessages++;
978
- if (msg.message?.content && Array.isArray(msg.message.content)) {
979
- for (const item of msg.message.content) {
980
- if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
981
- const toolUse = item;
982
- const toolName = toolUse.name ?? "unknown";
983
- const existing = toolUsageMap.get(toolName) ?? { count: 0, errorCount: 0 };
984
- existing.count++;
985
- toolUsageMap.set(toolName, existing);
986
- if ((toolName === "Write" || toolName === "Edit") && toolUse.input?.file_path) {
987
- filesChanged.add(toolUse.input.file_path);
988
- }
989
- }
990
- }
991
- }
992
- } else if (msg.type === "summary") {
993
- summaryCount++;
994
- if (msg.summary) {
995
- milestones.push({
996
- timestamp: msg.timestamp,
997
- description: `Summary: ${msg.summary.slice(0, 100)}...`,
998
- messageUuid: msg.uuid
999
- });
1000
- }
1001
- } else if (msg.type === "file-history-snapshot") {
1002
- snapshotCount++;
1003
- const snapshot = msg;
1004
- if (snapshot.snapshot?.trackedFileBackups) {
1005
- for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1006
- filesChanged.add(filePath);
1007
- }
1008
- }
1009
- }
1010
- }
1011
- for (const msg of messages) {
1012
- if (msg.type === "user" && msg.content && Array.isArray(msg.content)) {
1013
- for (const item of msg.content) {
1014
- if (item && typeof item === "object" && "type" in item && item.type === "tool_result" && "is_error" in item && item.is_error) {
1015
- const toolResultItem = item;
1016
- const toolUseId = toolResultItem.tool_use_id;
1017
- if (toolUseId) {
1018
- for (const prevMsg of messages) {
1019
- if (prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
1020
- for (const prevItem of prevMsg.message.content) {
1021
- if (prevItem && typeof prevItem === "object" && "type" in prevItem && prevItem.type === "tool_use" && "id" in prevItem && prevItem.id === toolUseId) {
1022
- const toolName = prevItem.name ?? "unknown";
1023
- const existing = toolUsageMap.get(toolName);
1024
- if (existing) {
1025
- existing.errorCount++;
1026
- }
1027
- }
1028
- }
1029
- }
1030
- }
1031
- }
1032
- }
1033
- }
1034
- }
1035
- }
1036
- let durationMinutes = 0;
1037
- if (firstTimestamp && lastTimestamp) {
1038
- const first = new Date(firstTimestamp).getTime();
1039
- const last = new Date(lastTimestamp).getTime();
1040
- durationMinutes = Math.round((last - first) / 1e3 / 60);
1041
- }
1042
- const toolUsageArray = Array.from(toolUsageMap.entries()).map(([name, stats]) => ({
1043
- name,
1044
- count: stats.count,
1045
- errorCount: stats.errorCount
1046
- }));
1047
- for (const tool of toolUsageArray) {
1048
- if (tool.count >= 3 && tool.errorCount / tool.count > 0.3) {
1049
- patterns.push({
1050
- type: "high_error_rate",
1051
- description: `${tool.name} had ${tool.errorCount}/${tool.count} errors (${Math.round(tool.errorCount / tool.count * 100)}%)`,
1052
- count: tool.errorCount
1053
- });
1054
- }
1055
- }
1056
- if (snapshotCount > 10) {
1057
- patterns.push({
1058
- type: "many_snapshots",
1059
- description: `${snapshotCount} file-history-snapshots could be compressed`,
1060
- count: snapshotCount
1061
- });
1062
- }
1063
- return {
1064
- sessionId,
1065
- projectName,
1066
- durationMinutes,
1067
- stats: {
1068
- totalMessages: messages.length,
1069
- userMessages,
1070
- assistantMessages,
1071
- summaryCount,
1072
- snapshotCount
1073
- },
1074
- toolUsage: toolUsageArray.sort((a, b) => b.count - a.count),
1075
- filesChanged: Array.from(filesChanged),
1076
- patterns,
1077
- milestones
1078
- };
1079
- });
1080
- var moveSession = (sourceProject, sessionId, targetProject) => Effect3.gen(function* () {
1122
+ var moveSession = (sourceProject, sessionId, targetProject) => Effect5.gen(function* () {
1081
1123
  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)
1124
+ const sourcePath = path5.join(sessionsDir, sourceProject);
1125
+ const targetPath = path5.join(sessionsDir, targetProject);
1126
+ const sourceFile = path5.join(sourcePath, `${sessionId}.jsonl`);
1127
+ const targetFile = path5.join(targetPath, `${sessionId}.jsonl`);
1128
+ const sourceExists = yield* Effect5.tryPromise(
1129
+ () => fs6.access(sourceFile).then(() => true).catch(() => false)
1088
1130
  );
1089
1131
  if (!sourceExists) {
1090
1132
  return { success: false, error: "Source session not found" };
1091
1133
  }
1092
- const targetExists = yield* Effect3.tryPromise(
1093
- () => fs4.access(targetFile).then(() => true).catch(() => false)
1134
+ const targetExists = yield* Effect5.tryPromise(
1135
+ () => fs6.access(targetFile).then(() => true).catch(() => false)
1094
1136
  );
1095
1137
  if (targetExists) {
1096
1138
  return { success: false, error: "Session already exists in target project" };
1097
1139
  }
1098
- yield* Effect3.tryPromise(() => fs4.mkdir(targetPath, { recursive: true }));
1140
+ yield* Effect5.tryPromise(() => fs6.mkdir(targetPath, { recursive: true }));
1099
1141
  const linkedAgents = yield* findLinkedAgents(sourceProject, sessionId);
1100
- yield* Effect3.tryPromise(() => fs4.rename(sourceFile, targetFile));
1142
+ yield* Effect5.tryPromise(() => fs6.rename(sourceFile, targetFile));
1101
1143
  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)
1144
+ const sourceAgentFile = path5.join(sourcePath, `${agentId}.jsonl`);
1145
+ const targetAgentFile = path5.join(targetPath, `${agentId}.jsonl`);
1146
+ const agentExists = yield* Effect5.tryPromise(
1147
+ () => fs6.access(sourceAgentFile).then(() => true).catch(() => false)
1106
1148
  );
1107
1149
  if (agentExists) {
1108
- yield* Effect3.tryPromise(() => fs4.rename(sourceAgentFile, targetAgentFile));
1150
+ yield* Effect5.tryPromise(() => fs6.rename(sourceAgentFile, targetAgentFile));
1109
1151
  }
1110
1152
  }
1111
1153
  return { success: true };
1112
1154
  });
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"));
1117
- const lines = content.trim().split("\n").filter(Boolean);
1118
- const allMessages = lines.map((line) => JSON.parse(line));
1155
+ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(function* () {
1156
+ const projectPath = path5.join(getSessionsDir(), projectName);
1157
+ const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
1158
+ const allMessages = yield* readJsonlFile(filePath);
1119
1159
  const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
1120
1160
  if (splitIndex === -1) {
1121
1161
  return { success: false, error: "Message not found" };
@@ -1133,303 +1173,125 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect3.gen(f
1133
1173
  if (shouldDuplicate) {
1134
1174
  const duplicatedMessage = {
1135
1175
  ...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);
1148
- }
1149
- return updated;
1150
- });
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"));
1188
- }
1189
- }
1190
- }
1191
- return {
1192
- success: true,
1193
- newSessionId,
1194
- newSessionPath: newFilePath,
1195
- movedMessageCount: movedMessages.length,
1196
- duplicatedSummary: shouldDuplicate
1197
- };
1198
- });
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"));
1202
- const lines = content.trim().split("\n").filter(Boolean);
1203
- if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
1204
- const messages = lines.map((line) => JSON.parse(line));
1205
- const invalidIndices = [];
1206
- messages.forEach((msg, idx) => {
1207
- if (isInvalidApiKeyMessage(msg)) {
1208
- invalidIndices.push(idx);
1209
- }
1210
- });
1211
- if (invalidIndices.length === 0) {
1212
- const userAssistantCount = messages.filter(
1213
- (m) => m.type === "user" || m.type === "assistant"
1214
- ).length;
1215
- const hasSummary2 = messages.some((m) => m.type === "summary");
1216
- const remainingCount2 = userAssistantCount > 0 ? userAssistantCount : hasSummary2 ? 1 : 0;
1217
- return { removedCount: 0, remainingCount: remainingCount2 };
1218
- }
1219
- const filtered = [];
1220
- let lastValidUuid = null;
1221
- for (let i = 0; i < messages.length; i++) {
1222
- if (invalidIndices.includes(i)) {
1223
- continue;
1224
- }
1225
- const msg = messages[i];
1226
- if (msg.parentUuid && invalidIndices.some((idx) => messages[idx]?.uuid === msg.parentUuid)) {
1227
- msg.parentUuid = lastValidUuid;
1228
- }
1229
- filtered.push(msg);
1230
- lastValidUuid = msg.uuid;
1231
- }
1232
- const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
1233
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1234
- const remainingUserAssistant = filtered.filter(
1235
- (m) => m.type === "user" || m.type === "assistant"
1236
- ).length;
1237
- const hasSummary = filtered.some((m) => m.type === "summary");
1238
- const remainingCount = remainingUserAssistant > 0 ? remainingUserAssistant : hasSummary ? 1 : 0;
1239
- return { removedCount: invalidIndices.length, remainingCount };
1240
- });
1241
- var previewCleanup = (projectName) => Effect3.gen(function* () {
1242
- const projects = yield* listProjects;
1243
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1244
- const orphanTodos = yield* findOrphanTodos();
1245
- const orphanTodoCount = orphanTodos.length;
1246
- const results = yield* Effect3.all(
1247
- targetProjects.map(
1248
- (project) => Effect3.gen(function* () {
1249
- const sessions = yield* listSessions(project.name);
1250
- const emptySessions = sessions.filter((s) => s.messageCount === 0);
1251
- const invalidSessions = sessions.filter(
1252
- (s) => s.title?.includes("Invalid API key") || s.title?.includes("API key")
1253
- );
1254
- let emptyWithTodosCount = 0;
1255
- for (const session of emptySessions) {
1256
- const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1257
- const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1258
- if (hasTodos) {
1259
- emptyWithTodosCount++;
1260
- }
1261
- }
1262
- const orphanAgents = yield* findOrphanAgents(project.name);
1263
- return {
1264
- project: project.name,
1265
- emptySessions,
1266
- invalidSessions,
1267
- emptyWithTodosCount,
1268
- orphanAgentCount: orphanAgents.length,
1269
- orphanTodoCount: 0
1270
- // Will set for first project only
1271
- };
1272
- })
1273
- ),
1274
- { concurrency: 5 }
1275
- );
1276
- if (results.length > 0) {
1277
- results[0] = { ...results[0], orphanTodoCount };
1278
- }
1279
- return results;
1280
- });
1281
- var clearSessions = (options) => Effect3.gen(function* () {
1282
- const {
1283
- projectName,
1284
- clearEmpty = true,
1285
- clearInvalid = true,
1286
- skipWithTodos = true,
1287
- clearOrphanAgents = true,
1288
- clearOrphanTodos = false
1289
- } = options;
1290
- const projects = yield* listProjects;
1291
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1292
- let deletedSessionCount = 0;
1293
- let removedMessageCount = 0;
1294
- let deletedOrphanAgentCount = 0;
1295
- let deletedOrphanTodoCount = 0;
1296
- const sessionsToDelete = [];
1297
- if (clearInvalid) {
1298
- for (const project of targetProjects) {
1299
- const projectPath = path4.join(getSessionsDir(), project.name);
1300
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1301
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1302
- for (const file of sessionFiles) {
1303
- const sessionId = file.replace(".jsonl", "");
1304
- const result = yield* cleanInvalidMessages(project.name, sessionId);
1305
- removedMessageCount += result.removedCount;
1306
- if (result.remainingCount === 0) {
1307
- sessionsToDelete.push({ project: project.name, sessionId });
1308
- }
1309
- }
1310
- }
1311
- }
1312
- if (clearEmpty) {
1313
- for (const project of targetProjects) {
1314
- const sessions = yield* listSessions(project.name);
1315
- for (const session of sessions) {
1316
- if (session.messageCount === 0) {
1317
- const alreadyMarked = sessionsToDelete.some(
1318
- (s) => s.project === project.name && s.sessionId === session.id
1319
- );
1320
- if (!alreadyMarked) {
1321
- if (skipWithTodos) {
1322
- const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1323
- const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1324
- if (hasTodos) continue;
1325
- }
1326
- sessionsToDelete.push({ project: project.name, sessionId: session.id });
1327
- }
1328
- }
1329
- }
1330
- }
1331
- }
1332
- for (const { project, sessionId } of sessionsToDelete) {
1333
- yield* deleteSession(project, sessionId);
1334
- deletedSessionCount++;
1176
+ uuid: crypto.randomUUID(),
1177
+ sessionId: newSessionId
1178
+ };
1179
+ movedMessages = [...allMessages.slice(0, splitIndex), duplicatedMessage];
1180
+ } else {
1181
+ movedMessages = allMessages.slice(0, splitIndex);
1335
1182
  }
1336
- if (clearOrphanAgents) {
1337
- for (const project of targetProjects) {
1338
- const result = yield* deleteOrphanAgents(project.name);
1339
- deletedOrphanAgentCount += result.count;
1183
+ keptMessages = keptMessages.map((msg, index) => {
1184
+ let updated = { ...msg };
1185
+ if (index === 0) {
1186
+ updated.parentUuid = null;
1187
+ updated = cleanupSplitFirstMessage(updated);
1340
1188
  }
1189
+ return updated;
1190
+ });
1191
+ const updatedMovedMessages = movedMessages.map((msg) => ({
1192
+ ...msg,
1193
+ sessionId: newSessionId
1194
+ }));
1195
+ if (summaryMessage) {
1196
+ const clonedSummary = {
1197
+ ...summaryMessage,
1198
+ sessionId: newSessionId,
1199
+ leafUuid: updatedMovedMessages[0]?.uuid ?? null
1200
+ };
1201
+ updatedMovedMessages.unshift(clonedSummary);
1341
1202
  }
1342
- if (clearOrphanTodos) {
1343
- const result = yield* deleteOrphanTodos();
1344
- deletedOrphanTodoCount = result.deletedCount;
1203
+ const keptContent = keptMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1204
+ yield* Effect5.tryPromise(() => fs6.writeFile(filePath, keptContent, "utf-8"));
1205
+ const newFilePath = path5.join(projectPath, `${newSessionId}.jsonl`);
1206
+ const newContent = updatedMovedMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1207
+ yield* Effect5.tryPromise(() => fs6.writeFile(newFilePath, newContent, "utf-8"));
1208
+ const agentFiles = yield* Effect5.tryPromise(() => fs6.readdir(projectPath));
1209
+ const agentJsonlFiles = agentFiles.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"));
1210
+ for (const agentFile of agentJsonlFiles) {
1211
+ const agentPath = path5.join(projectPath, agentFile);
1212
+ const agentContent = yield* Effect5.tryPromise(() => fs6.readFile(agentPath, "utf-8"));
1213
+ const agentLines = agentContent.trim().split("\n").filter(Boolean);
1214
+ if (agentLines.length === 0) continue;
1215
+ const agentMessages = parseJsonlLines(agentLines, agentPath);
1216
+ const firstAgentMsg = agentMessages[0];
1217
+ if (firstAgentMsg.sessionId === sessionId) {
1218
+ const agentId = agentFile.replace("agent-", "").replace(".jsonl", "");
1219
+ const isRelatedToMoved = movedMessages.some(
1220
+ (msg) => msg.agentId === agentId
1221
+ );
1222
+ if (isRelatedToMoved) {
1223
+ const updatedAgentMessages = agentMessages.map(
1224
+ (msg) => JSON.stringify({ ...msg, sessionId: newSessionId })
1225
+ );
1226
+ const updatedAgentContent = updatedAgentMessages.join("\n") + "\n";
1227
+ yield* Effect5.tryPromise(() => fs6.writeFile(agentPath, updatedAgentContent, "utf-8"));
1228
+ }
1229
+ }
1345
1230
  }
1346
1231
  return {
1347
1232
  success: true,
1348
- deletedCount: deletedSessionCount,
1349
- removedMessageCount,
1350
- deletedOrphanAgentCount,
1351
- deletedOrphanTodoCount
1233
+ newSessionId,
1234
+ newSessionPath: newFilePath,
1235
+ movedMessageCount: movedMessages.length,
1236
+ duplicatedSummary: shouldDuplicate
1352
1237
  };
1353
1238
  });
1354
- var searchSessions = (query, options = {}) => Effect3.gen(function* () {
1355
- const { projectName, searchContent = false } = options;
1356
- const results = [];
1357
- const queryLower = query.toLowerCase();
1358
- const projects = yield* listProjects;
1359
- const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1360
- for (const project of targetProjects) {
1361
- const sessions = yield* listSessions(project.name);
1362
- for (const session of sessions) {
1363
- const titleLower = (session.title ?? "").toLowerCase();
1364
- if (titleLower.includes(queryLower)) {
1365
- results.push({
1366
- sessionId: session.id,
1367
- projectName: project.name,
1368
- title: session.title ?? "Untitled",
1369
- matchType: "title",
1370
- timestamp: session.updatedAt
1371
- });
1239
+
1240
+ // src/session/tree.ts
1241
+ import { Effect as Effect6 } from "effect";
1242
+ import * as fs7 from "fs/promises";
1243
+ import * as path6 from "path";
1244
+ var sortSessions = (sessions, sort) => {
1245
+ return sessions.sort((a, b) => {
1246
+ let comparison = 0;
1247
+ switch (sort.field) {
1248
+ case "summary": {
1249
+ comparison = a.sortTimestamp - b.sortTimestamp;
1250
+ break;
1372
1251
  }
1373
- }
1374
- }
1375
- if (searchContent) {
1376
- for (const project of targetProjects) {
1377
- const projectPath = path4.join(getSessionsDir(), project.name);
1378
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1379
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1380
- for (const file of sessionFiles) {
1381
- const sessionId = file.replace(".jsonl", "");
1382
- if (results.some((r) => r.sessionId === sessionId && r.projectName === project.name)) {
1383
- continue;
1384
- }
1385
- const filePath = path4.join(projectPath, file);
1386
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1387
- const lines = content.trim().split("\n").filter(Boolean);
1388
- for (const line of lines) {
1389
- try {
1390
- const msg = JSON.parse(line);
1391
- if (msg.type !== "user" && msg.type !== "assistant") continue;
1392
- const text = extractTextContent(msg.message);
1393
- const textLower = text.toLowerCase();
1394
- if (textLower.includes(queryLower)) {
1395
- const matchIndex = textLower.indexOf(queryLower);
1396
- const start = Math.max(0, matchIndex - 50);
1397
- const end = Math.min(text.length, matchIndex + query.length + 50);
1398
- const snippet = (start > 0 ? "..." : "") + text.slice(start, end).trim() + (end < text.length ? "..." : "");
1399
- results.push({
1400
- sessionId,
1401
- projectName: project.name,
1402
- title: extractTitle(extractTextContent(msg.message)) || `Session ${sessionId.slice(0, 8)}`,
1403
- matchType: "content",
1404
- snippet,
1405
- messageUuid: msg.uuid,
1406
- timestamp: msg.timestamp
1407
- });
1408
- break;
1409
- }
1410
- } catch {
1411
- }
1412
- }
1252
+ case "modified": {
1253
+ comparison = (a.fileMtime ?? 0) - (b.fileMtime ?? 0);
1254
+ break;
1255
+ }
1256
+ case "created": {
1257
+ const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
1258
+ const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
1259
+ comparison = createdA - createdB;
1260
+ break;
1261
+ }
1262
+ case "updated": {
1263
+ const updatedA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
1264
+ const updatedB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
1265
+ comparison = updatedA - updatedB;
1266
+ break;
1267
+ }
1268
+ case "messageCount": {
1269
+ comparison = a.messageCount - b.messageCount;
1270
+ break;
1271
+ }
1272
+ case "title": {
1273
+ const titleA = a.customTitle ?? a.currentSummary ?? a.title;
1274
+ const titleB = b.customTitle ?? b.currentSummary ?? b.title;
1275
+ comparison = titleA.localeCompare(titleB);
1276
+ break;
1413
1277
  }
1414
1278
  }
1415
- }
1416
- return results.sort((a, b) => {
1417
- const dateA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
1418
- const dateB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
1419
- return dateB - dateA;
1279
+ return sort.order === "desc" ? -comparison : comparison;
1420
1280
  });
1421
- });
1422
- var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession) => Effect3.gen(function* () {
1423
- const projectPath = path4.join(getSessionsDir(), projectName);
1424
- const filePath = path4.join(projectPath, `${sessionId}.jsonl`);
1425
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1281
+ };
1282
+ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession, fileMtime) => Effect6.gen(function* () {
1283
+ const projectPath = path6.join(getSessionsDir(), projectName);
1284
+ const filePath = path6.join(projectPath, `${sessionId}.jsonl`);
1285
+ const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1426
1286
  const lines = content.trim().split("\n").filter(Boolean);
1427
- const messages = lines.map((line) => JSON.parse(line));
1287
+ const messages = parseJsonlLines(lines, filePath);
1428
1288
  let summaries;
1429
1289
  if (summariesByTargetSession) {
1430
- summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort(
1431
- (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
1432
- );
1290
+ summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort((a, b) => {
1291
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1292
+ if (timestampCmp !== 0) return timestampCmp;
1293
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1294
+ });
1433
1295
  } else {
1434
1296
  summaries = [];
1435
1297
  const sessionUuids = /* @__PURE__ */ new Set();
@@ -1438,32 +1300,35 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1438
1300
  sessionUuids.add(msg.uuid);
1439
1301
  }
1440
1302
  }
1441
- const projectFiles = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1303
+ const projectFiles = yield* Effect6.tryPromise(() => fs7.readdir(projectPath));
1442
1304
  const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1443
1305
  for (const file of allJsonlFiles) {
1444
1306
  try {
1445
- const otherFilePath = path4.join(projectPath, file);
1446
- const otherContent = yield* Effect3.tryPromise(() => fs4.readFile(otherFilePath, "utf-8"));
1307
+ const otherFilePath = path6.join(projectPath, file);
1308
+ const otherContent = yield* Effect6.tryPromise(() => fs7.readFile(otherFilePath, "utf-8"));
1447
1309
  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 {
1310
+ for (let i = 0; i < otherLines.length; i++) {
1311
+ const msg = tryParseJsonLine(otherLines[i], i + 1, otherFilePath);
1312
+ if (!msg) continue;
1313
+ if (msg.type === "summary" && typeof msg.summary === "string" && typeof msg.leafUuid === "string" && sessionUuids.has(msg.leafUuid)) {
1314
+ const targetMsg = messages.find((m) => m.uuid === msg.leafUuid);
1315
+ summaries.push({
1316
+ summary: msg.summary,
1317
+ leafUuid: msg.leafUuid,
1318
+ timestamp: targetMsg?.timestamp ?? msg.timestamp,
1319
+ sourceFile: file
1320
+ });
1460
1321
  }
1461
1322
  }
1462
1323
  } catch {
1463
1324
  }
1464
1325
  }
1465
1326
  }
1466
- summaries.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
1327
+ summaries.sort((a, b) => {
1328
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1329
+ if (timestampCmp !== 0) return timestampCmp;
1330
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1331
+ });
1467
1332
  let lastCompactBoundaryUuid;
1468
1333
  for (let i = messages.length - 1; i >= 0; i--) {
1469
1334
  const msg = messages[i];
@@ -1484,9 +1349,9 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1484
1349
  const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1485
1350
  const agents = [];
1486
1351
  for (const agentId of linkedAgentIds) {
1487
- const agentPath = path4.join(projectPath, `${agentId}.jsonl`);
1352
+ const agentPath = path6.join(projectPath, `${agentId}.jsonl`);
1488
1353
  try {
1489
- const agentContent = yield* Effect3.tryPromise(() => fs4.readFile(agentPath, "utf-8"));
1354
+ const agentContent = yield* Effect6.tryPromise(() => fs7.readFile(agentPath, "utf-8"));
1490
1355
  const agentLines = agentContent.trim().split("\n").filter(Boolean);
1491
1356
  const agentMsgs = agentLines.map((l) => JSON.parse(l));
1492
1357
  const agentUserAssistant = agentMsgs.filter(
@@ -1513,6 +1378,8 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1513
1378
  }
1514
1379
  }
1515
1380
  const todos = yield* findLinkedTodos(sessionId, linkedAgentIds);
1381
+ const createdAt = firstMessage?.timestamp ?? void 0;
1382
+ const sortTimestamp = getSessionSortTimestamp({ summaries, createdAt });
1516
1383
  return {
1517
1384
  id: sessionId,
1518
1385
  projectName,
@@ -1520,8 +1387,10 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1520
1387
  customTitle,
1521
1388
  currentSummary: summaries[0]?.summary,
1522
1389
  messageCount: userAssistantMessages.length > 0 ? userAssistantMessages.length : summaries.length > 0 ? 1 : 0,
1523
- createdAt: firstMessage?.timestamp ?? void 0,
1390
+ createdAt,
1524
1391
  updatedAt: lastMessage?.timestamp ?? void 0,
1392
+ fileMtime,
1393
+ sortTimestamp,
1525
1394
  summaries,
1526
1395
  agents,
1527
1396
  todos,
@@ -1529,48 +1398,63 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1529
1398
  };
1530
1399
  });
1531
1400
  var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1532
- var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1401
+ var DEFAULT_SORT = { field: "summary", order: "desc" };
1402
+ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* () {
1533
1403
  const project = (yield* listProjects).find((p) => p.name === projectName);
1534
1404
  if (!project) {
1535
1405
  return null;
1536
1406
  }
1537
- const projectPath = path4.join(getSessionsDir(), projectName);
1538
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectPath));
1407
+ const sort = sortOptions ?? DEFAULT_SORT;
1408
+ const projectPath = path6.join(getSessionsDir(), projectName);
1409
+ const files = yield* Effect6.tryPromise(() => fs7.readdir(projectPath));
1539
1410
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1411
+ const fileMtimes = /* @__PURE__ */ new Map();
1412
+ yield* Effect6.all(
1413
+ sessionFiles.map(
1414
+ (file) => Effect6.gen(function* () {
1415
+ const filePath = path6.join(projectPath, file);
1416
+ try {
1417
+ const stat4 = yield* Effect6.tryPromise(() => fs7.stat(filePath));
1418
+ fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
1419
+ } catch {
1420
+ }
1421
+ })
1422
+ ),
1423
+ { concurrency: 20 }
1424
+ );
1540
1425
  const globalUuidMap = /* @__PURE__ */ new Map();
1541
1426
  const allSummaries = [];
1542
1427
  const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1543
- yield* Effect3.all(
1428
+ yield* Effect6.all(
1544
1429
  allJsonlFiles.map(
1545
- (file) => Effect3.gen(function* () {
1546
- const filePath = path4.join(projectPath, file);
1430
+ (file) => Effect6.gen(function* () {
1431
+ const filePath = path6.join(projectPath, file);
1547
1432
  const fileSessionId = file.replace(".jsonl", "");
1548
1433
  try {
1549
- const content = yield* Effect3.tryPromise(() => fs4.readFile(filePath, "utf-8"));
1434
+ const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1550
1435
  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
1571
- });
1572
- }
1573
- } catch {
1436
+ for (let i = 0; i < lines.length; i++) {
1437
+ const msg = tryParseJsonLine(lines[i], i + 1, filePath);
1438
+ if (!msg) continue;
1439
+ if (msg.uuid && typeof msg.uuid === "string") {
1440
+ globalUuidMap.set(msg.uuid, {
1441
+ sessionId: fileSessionId,
1442
+ timestamp: msg.timestamp
1443
+ });
1444
+ }
1445
+ if (msg.messageId && typeof msg.messageId === "string") {
1446
+ globalUuidMap.set(msg.messageId, {
1447
+ sessionId: fileSessionId,
1448
+ timestamp: msg.snapshot?.timestamp
1449
+ });
1450
+ }
1451
+ if (msg.type === "summary" && typeof msg.summary === "string") {
1452
+ allSummaries.push({
1453
+ summary: msg.summary,
1454
+ leafUuid: msg.leafUuid,
1455
+ timestamp: msg.timestamp,
1456
+ sourceFile: file
1457
+ });
1574
1458
  }
1575
1459
  }
1576
1460
  } catch {
@@ -1591,23 +1475,22 @@ var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1591
1475
  summariesByTargetSession.get(targetSessionId).push({
1592
1476
  summary: summaryData.summary,
1593
1477
  leafUuid: summaryData.leafUuid,
1594
- timestamp: targetInfo.timestamp ?? summaryData.timestamp
1478
+ // Use summary's own timestamp for sorting, not the target message's timestamp
1479
+ timestamp: summaryData.timestamp ?? targetInfo.timestamp,
1480
+ sourceFile: summaryData.sourceFile
1595
1481
  });
1596
1482
  }
1597
1483
  }
1598
1484
  }
1599
- const sessions = yield* Effect3.all(
1485
+ const sessions = yield* Effect6.all(
1600
1486
  sessionFiles.map((file) => {
1601
1487
  const sessionId = file.replace(".jsonl", "");
1602
- return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession);
1488
+ const mtime = fileMtimes.get(sessionId);
1489
+ return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1603
1490
  }),
1604
1491
  { concurrency: 10 }
1605
1492
  );
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
- });
1493
+ const sortedSessions = sortSessions(sessions, sort);
1611
1494
  const filteredSessions = sortedSessions.filter((s) => {
1612
1495
  if (isErrorSessionTitle(s.title)) return false;
1613
1496
  if (isErrorSessionTitle(s.customTitle)) return false;
@@ -1622,34 +1505,149 @@ var loadProjectTreeData = (projectName) => Effect3.gen(function* () {
1622
1505
  sessions: filteredSessions
1623
1506
  };
1624
1507
  });
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);
1508
+
1509
+ // src/session/analysis.ts
1510
+ import { Effect as Effect7 } from "effect";
1511
+ import * as fs8 from "fs/promises";
1512
+ import * as path7 from "path";
1513
+ var analyzeSession = (projectName, sessionId) => Effect7.gen(function* () {
1514
+ const messages = yield* readSession(projectName, sessionId);
1515
+ let userMessages = 0;
1516
+ let assistantMessages = 0;
1517
+ let summaryCount = 0;
1518
+ let snapshotCount = 0;
1519
+ const toolUsageMap = /* @__PURE__ */ new Map();
1520
+ const filesChanged = /* @__PURE__ */ new Set();
1521
+ const patterns = [];
1522
+ const milestones = [];
1523
+ let firstTimestamp;
1524
+ let lastTimestamp;
1525
+ for (const msg of messages) {
1526
+ if (msg.timestamp) {
1527
+ if (!firstTimestamp) firstTimestamp = msg.timestamp;
1528
+ lastTimestamp = msg.timestamp;
1529
+ }
1530
+ if (msg.type === "user") {
1531
+ userMessages++;
1532
+ const content = typeof msg.content === "string" ? msg.content : "";
1533
+ if (content.toLowerCase().includes("commit") || content.toLowerCase().includes("\uC644\uB8CC")) {
1534
+ milestones.push({
1535
+ timestamp: msg.timestamp,
1536
+ description: `User checkpoint: ${content.slice(0, 50)}...`,
1537
+ messageUuid: msg.uuid
1538
+ });
1539
+ }
1540
+ } else if (msg.type === "assistant") {
1541
+ assistantMessages++;
1542
+ if (msg.message?.content && Array.isArray(msg.message.content)) {
1543
+ for (const item of msg.message.content) {
1544
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
1545
+ const toolUse = item;
1546
+ const toolName = toolUse.name ?? "unknown";
1547
+ const existing = toolUsageMap.get(toolName) ?? { count: 0, errorCount: 0 };
1548
+ existing.count++;
1549
+ toolUsageMap.set(toolName, existing);
1550
+ if ((toolName === "Write" || toolName === "Edit") && toolUse.input?.file_path) {
1551
+ filesChanged.add(toolUse.input.file_path);
1552
+ }
1553
+ }
1554
+ }
1555
+ }
1556
+ } else if (msg.type === "summary") {
1557
+ summaryCount++;
1558
+ if (msg.summary) {
1559
+ milestones.push({
1560
+ timestamp: msg.timestamp,
1561
+ description: `Summary: ${msg.summary.slice(0, 100)}...`,
1562
+ messageUuid: msg.uuid
1563
+ });
1564
+ }
1565
+ } else if (msg.type === "file-history-snapshot") {
1566
+ snapshotCount++;
1567
+ const snapshot = msg;
1568
+ if (snapshot.snapshot?.trackedFileBackups) {
1569
+ for (const filePath of Object.keys(snapshot.snapshot.trackedFileBackups)) {
1570
+ filesChanged.add(filePath);
1571
+ }
1572
+ }
1573
+ }
1641
1574
  }
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 };
1575
+ for (const msg of messages) {
1576
+ if (msg.type === "user" && msg.content && Array.isArray(msg.content)) {
1577
+ for (const item of msg.content) {
1578
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_result" && "is_error" in item && item.is_error) {
1579
+ const toolResultItem = item;
1580
+ const toolUseId = toolResultItem.tool_use_id;
1581
+ if (toolUseId) {
1582
+ for (const prevMsg of messages) {
1583
+ if (prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
1584
+ for (const prevItem of prevMsg.message.content) {
1585
+ if (prevItem && typeof prevItem === "object" && "type" in prevItem && prevItem.type === "tool_use" && "id" in prevItem && prevItem.id === toolUseId) {
1586
+ const toolName = prevItem.name ?? "unknown";
1587
+ const existing = toolUsageMap.get(toolName);
1588
+ if (existing) {
1589
+ existing.errorCount++;
1590
+ }
1591
+ }
1592
+ }
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+ let durationMinutes = 0;
1601
+ if (firstTimestamp && lastTimestamp) {
1602
+ const first = new Date(firstTimestamp).getTime();
1603
+ const last = new Date(lastTimestamp).getTime();
1604
+ durationMinutes = Math.round((last - first) / 1e3 / 60);
1605
+ }
1606
+ const toolUsageArray = Array.from(toolUsageMap.entries()).map(([name, stats]) => ({
1607
+ name,
1608
+ count: stats.count,
1609
+ errorCount: stats.errorCount
1610
+ }));
1611
+ for (const tool of toolUsageArray) {
1612
+ if (tool.count >= 3 && tool.errorCount / tool.count > 0.3) {
1613
+ patterns.push({
1614
+ type: "high_error_rate",
1615
+ description: `${tool.name} had ${tool.errorCount}/${tool.count} errors (${Math.round(tool.errorCount / tool.count * 100)}%)`,
1616
+ count: tool.errorCount
1617
+ });
1618
+ }
1619
+ }
1620
+ if (snapshotCount > 10) {
1621
+ patterns.push({
1622
+ type: "many_snapshots",
1623
+ description: `${snapshotCount} file-history-snapshots could be compressed`,
1624
+ count: snapshotCount
1625
+ });
1626
+ }
1627
+ return {
1628
+ sessionId,
1629
+ projectName,
1630
+ durationMinutes,
1631
+ stats: {
1632
+ totalMessages: messages.length,
1633
+ userMessages,
1634
+ assistantMessages,
1635
+ summaryCount,
1636
+ snapshotCount
1637
+ },
1638
+ toolUsage: toolUsageArray.sort((a, b) => b.count - a.count),
1639
+ filesChanged: Array.from(filesChanged),
1640
+ patterns,
1641
+ milestones
1642
+ };
1645
1643
  });
1646
- var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(function* () {
1644
+ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(function* () {
1647
1645
  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"));
1646
+ const filePath = path7.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1647
+ const content = yield* Effect7.tryPromise(() => fs8.readFile(filePath, "utf-8"));
1650
1648
  const originalSize = Buffer.byteLength(content, "utf-8");
1651
1649
  const lines = content.trim().split("\n").filter(Boolean);
1652
- const messages = lines.map((line) => JSON.parse(line));
1650
+ const messages = parseJsonlLines(lines, filePath);
1653
1651
  let removedSnapshots = 0;
1654
1652
  let truncatedOutputs = 0;
1655
1653
  const snapshotIndices = [];
@@ -1689,7 +1687,7 @@ var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(func
1689
1687
  }
1690
1688
  const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1691
1689
  const compressedSize = Buffer.byteLength(newContent, "utf-8");
1692
- yield* Effect3.tryPromise(() => fs4.writeFile(filePath, newContent, "utf-8"));
1690
+ yield* Effect7.tryPromise(() => fs8.writeFile(filePath, newContent, "utf-8"));
1693
1691
  return {
1694
1692
  success: true,
1695
1693
  originalSize,
@@ -1698,12 +1696,12 @@ var compressSession = (projectName, sessionId, options = {}) => Effect3.gen(func
1698
1696
  truncatedOutputs
1699
1697
  };
1700
1698
  });
1701
- var extractProjectKnowledge = (projectName, sessionIds) => Effect3.gen(function* () {
1699
+ var extractProjectKnowledge = (projectName, sessionIds) => Effect7.gen(function* () {
1702
1700
  const sessionsDir = getSessionsDir();
1703
- const projectDir = path4.join(sessionsDir, projectName);
1701
+ const projectDir = path7.join(sessionsDir, projectName);
1704
1702
  let targetSessionIds = sessionIds;
1705
1703
  if (!targetSessionIds) {
1706
- const files = yield* Effect3.tryPromise(() => fs4.readdir(projectDir));
1704
+ const files = yield* Effect7.tryPromise(() => fs8.readdir(projectDir));
1707
1705
  targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
1708
1706
  }
1709
1707
  const fileModifyCount = /* @__PURE__ */ new Map();
@@ -1770,7 +1768,14 @@ var extractProjectKnowledge = (projectName, sessionIds) => Effect3.gen(function*
1770
1768
  decisions: decisions.slice(0, 20)
1771
1769
  };
1772
1770
  });
1773
- var summarizeSession = (projectName, sessionId, options = {}) => Effect3.gen(function* () {
1771
+ function truncateText(text, maxLen) {
1772
+ const cleaned = text.replace(/\n/g, " ");
1773
+ if (cleaned.length > maxLen) {
1774
+ return cleaned.slice(0, maxLen) + "...";
1775
+ }
1776
+ return cleaned;
1777
+ }
1778
+ var summarizeSession = (projectName, sessionId, options = {}) => Effect7.gen(function* () {
1774
1779
  const { limit = 50, maxLength = 100 } = options;
1775
1780
  const messages = yield* readSession(projectName, sessionId);
1776
1781
  const lines = [];
@@ -1820,13 +1825,362 @@ var summarizeSession = (projectName, sessionId, options = {}) => Effect3.gen(fun
1820
1825
  formatted
1821
1826
  };
1822
1827
  });
1823
- function truncateText(text, maxLen) {
1824
- const cleaned = text.replace(/\n/g, " ");
1825
- if (cleaned.length > maxLen) {
1826
- return cleaned.slice(0, maxLen) + "...";
1828
+
1829
+ // src/session/cleanup.ts
1830
+ import { Effect as Effect8 } from "effect";
1831
+ import * as fs9 from "fs/promises";
1832
+ import * as path8 from "path";
1833
+ var cleanInvalidMessages = (projectName, sessionId) => Effect8.gen(function* () {
1834
+ const filePath = path8.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1835
+ const content = yield* Effect8.tryPromise(() => fs9.readFile(filePath, "utf-8"));
1836
+ const lines = content.trim().split("\n").filter(Boolean);
1837
+ if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
1838
+ const messages = parseJsonlLines(lines, filePath);
1839
+ const invalidIndices = [];
1840
+ messages.forEach((msg, idx) => {
1841
+ if (isInvalidApiKeyMessage(msg)) {
1842
+ invalidIndices.push(idx);
1843
+ }
1844
+ });
1845
+ if (invalidIndices.length === 0) {
1846
+ const userAssistantCount = messages.filter(
1847
+ (m) => m.type === "user" || m.type === "assistant"
1848
+ ).length;
1849
+ const hasSummary2 = messages.some((m) => m.type === "summary");
1850
+ const remainingCount2 = userAssistantCount > 0 ? userAssistantCount : hasSummary2 ? 1 : 0;
1851
+ return { removedCount: 0, remainingCount: remainingCount2 };
1827
1852
  }
1828
- return cleaned;
1829
- }
1853
+ const filtered = [];
1854
+ let lastValidUuid = null;
1855
+ for (let i = 0; i < messages.length; i++) {
1856
+ if (invalidIndices.includes(i)) {
1857
+ continue;
1858
+ }
1859
+ const msg = messages[i];
1860
+ if (msg.parentUuid && invalidIndices.some((idx) => messages[idx]?.uuid === msg.parentUuid)) {
1861
+ msg.parentUuid = lastValidUuid;
1862
+ }
1863
+ filtered.push(msg);
1864
+ lastValidUuid = msg.uuid;
1865
+ }
1866
+ const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
1867
+ yield* Effect8.tryPromise(() => fs9.writeFile(filePath, newContent, "utf-8"));
1868
+ const remainingUserAssistant = filtered.filter(
1869
+ (m) => m.type === "user" || m.type === "assistant"
1870
+ ).length;
1871
+ const hasSummary = filtered.some((m) => m.type === "summary");
1872
+ const remainingCount = remainingUserAssistant > 0 ? remainingUserAssistant : hasSummary ? 1 : 0;
1873
+ return { removedCount: invalidIndices.length, remainingCount };
1874
+ });
1875
+ var previewCleanup = (projectName) => Effect8.gen(function* () {
1876
+ const projects = yield* listProjects;
1877
+ const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1878
+ const orphanTodos = yield* findOrphanTodos();
1879
+ const orphanTodoCount = orphanTodos.length;
1880
+ const results = yield* Effect8.all(
1881
+ targetProjects.map(
1882
+ (project) => Effect8.gen(function* () {
1883
+ const sessions = yield* listSessions(project.name);
1884
+ const emptySessions = sessions.filter((s) => s.messageCount === 0);
1885
+ const invalidSessions = sessions.filter(
1886
+ (s) => s.title?.includes("Invalid API key") || s.title?.includes("API key")
1887
+ );
1888
+ let emptyWithTodosCount = 0;
1889
+ for (const session of emptySessions) {
1890
+ const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1891
+ const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1892
+ if (hasTodos) {
1893
+ emptyWithTodosCount++;
1894
+ }
1895
+ }
1896
+ const orphanAgents = yield* findOrphanAgents(project.name);
1897
+ return {
1898
+ project: project.name,
1899
+ emptySessions,
1900
+ invalidSessions,
1901
+ emptyWithTodosCount,
1902
+ orphanAgentCount: orphanAgents.length,
1903
+ orphanTodoCount: 0
1904
+ // Will set for first project only
1905
+ };
1906
+ })
1907
+ ),
1908
+ { concurrency: 5 }
1909
+ );
1910
+ if (results.length > 0) {
1911
+ results[0] = { ...results[0], orphanTodoCount };
1912
+ }
1913
+ return results;
1914
+ });
1915
+ var clearSessions = (options) => Effect8.gen(function* () {
1916
+ const {
1917
+ projectName,
1918
+ clearEmpty = true,
1919
+ clearInvalid = true,
1920
+ skipWithTodos = true,
1921
+ clearOrphanAgents = true,
1922
+ clearOrphanTodos = false
1923
+ } = options;
1924
+ const projects = yield* listProjects;
1925
+ const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
1926
+ let deletedSessionCount = 0;
1927
+ let removedMessageCount = 0;
1928
+ let deletedOrphanAgentCount = 0;
1929
+ let deletedOrphanTodoCount = 0;
1930
+ const sessionsToDelete = [];
1931
+ if (clearInvalid) {
1932
+ for (const project of targetProjects) {
1933
+ const projectPath = path8.join(getSessionsDir(), project.name);
1934
+ const files = yield* Effect8.tryPromise(() => fs9.readdir(projectPath));
1935
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1936
+ for (const file of sessionFiles) {
1937
+ const sessionId = file.replace(".jsonl", "");
1938
+ const result = yield* cleanInvalidMessages(project.name, sessionId);
1939
+ removedMessageCount += result.removedCount;
1940
+ if (result.remainingCount === 0) {
1941
+ sessionsToDelete.push({ project: project.name, sessionId });
1942
+ }
1943
+ }
1944
+ }
1945
+ }
1946
+ if (clearEmpty) {
1947
+ for (const project of targetProjects) {
1948
+ const sessions = yield* listSessions(project.name);
1949
+ for (const session of sessions) {
1950
+ if (session.messageCount === 0) {
1951
+ const alreadyMarked = sessionsToDelete.some(
1952
+ (s) => s.project === project.name && s.sessionId === session.id
1953
+ );
1954
+ if (!alreadyMarked) {
1955
+ if (skipWithTodos) {
1956
+ const linkedAgents = yield* findLinkedAgents(project.name, session.id);
1957
+ const hasTodos = yield* sessionHasTodos(session.id, linkedAgents);
1958
+ if (hasTodos) continue;
1959
+ }
1960
+ sessionsToDelete.push({ project: project.name, sessionId: session.id });
1961
+ }
1962
+ }
1963
+ }
1964
+ }
1965
+ }
1966
+ for (const { project, sessionId } of sessionsToDelete) {
1967
+ yield* deleteSession(project, sessionId);
1968
+ deletedSessionCount++;
1969
+ }
1970
+ if (clearOrphanAgents) {
1971
+ for (const project of targetProjects) {
1972
+ const result = yield* deleteOrphanAgents(project.name);
1973
+ deletedOrphanAgentCount += result.count;
1974
+ }
1975
+ }
1976
+ if (clearOrphanTodos) {
1977
+ const result = yield* deleteOrphanTodos();
1978
+ deletedOrphanTodoCount = result.deletedCount;
1979
+ }
1980
+ return {
1981
+ success: true,
1982
+ deletedCount: deletedSessionCount,
1983
+ removedMessageCount,
1984
+ deletedOrphanAgentCount,
1985
+ deletedOrphanTodoCount
1986
+ };
1987
+ });
1988
+
1989
+ // src/session/search.ts
1990
+ import { Effect as Effect9, pipe as pipe2 } from "effect";
1991
+ import * as fs10 from "fs/promises";
1992
+ import * as path9 from "path";
1993
+ var extractSnippet = (text, matchIndex, queryLength) => {
1994
+ const start = Math.max(0, matchIndex - 50);
1995
+ const end = Math.min(text.length, matchIndex + queryLength + 50);
1996
+ return (start > 0 ? "..." : "") + text.slice(start, end).trim() + (end < text.length ? "..." : "");
1997
+ };
1998
+ var findContentMatch = (lines, queryLower, filePath) => {
1999
+ for (let i = 0; i < lines.length; i++) {
2000
+ const msg = tryParseJsonLine(lines[i], i + 1, filePath);
2001
+ if (!msg) continue;
2002
+ if (msg.type !== "user" && msg.type !== "assistant") continue;
2003
+ const text = extractTextContent(msg.message);
2004
+ const textLower = text.toLowerCase();
2005
+ const matchIndex = textLower.indexOf(queryLower);
2006
+ if (matchIndex !== -1) {
2007
+ return {
2008
+ msg,
2009
+ snippet: extractSnippet(text, matchIndex, queryLower.length)
2010
+ };
2011
+ }
2012
+ }
2013
+ return null;
2014
+ };
2015
+ var searchSessionContent = (projectName, sessionId, filePath, queryLower) => pipe2(
2016
+ Effect9.tryPromise(() => fs10.readFile(filePath, "utf-8")),
2017
+ Effect9.map((content) => {
2018
+ const lines = content.trim().split("\n").filter(Boolean);
2019
+ const match = findContentMatch(lines, queryLower, filePath);
2020
+ if (!match) return null;
2021
+ return {
2022
+ sessionId,
2023
+ projectName,
2024
+ title: extractTitle(extractTextContent(match.msg.message)) || `Session ${sessionId.slice(0, 8)}`,
2025
+ matchType: "content",
2026
+ snippet: match.snippet,
2027
+ messageUuid: match.msg.uuid,
2028
+ timestamp: match.msg.timestamp
2029
+ };
2030
+ }),
2031
+ Effect9.catchAll(() => Effect9.succeed(null))
2032
+ );
2033
+ var searchProjectContent = (project, queryLower, alreadyFoundIds) => Effect9.gen(function* () {
2034
+ const projectPath = path9.join(getSessionsDir(), project.name);
2035
+ const files = yield* Effect9.tryPromise(() => fs10.readdir(projectPath));
2036
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
2037
+ const searchEffects = sessionFiles.map((file) => ({
2038
+ sessionId: file.replace(".jsonl", ""),
2039
+ filePath: path9.join(projectPath, file)
2040
+ })).filter(({ sessionId }) => !alreadyFoundIds.has(`${project.name}:${sessionId}`)).map(
2041
+ ({ sessionId, filePath }) => searchSessionContent(project.name, sessionId, filePath, queryLower)
2042
+ );
2043
+ const results = yield* Effect9.all(searchEffects, { concurrency: 10 });
2044
+ return results.filter((r) => r !== null);
2045
+ });
2046
+ var searchSessions = (query, options = {}) => Effect9.gen(function* () {
2047
+ const { projectName, searchContent = false } = options;
2048
+ const queryLower = query.toLowerCase();
2049
+ const projects = yield* listProjects;
2050
+ const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
2051
+ const titleSearchEffects = targetProjects.map(
2052
+ (project) => pipe2(
2053
+ listSessions(project.name),
2054
+ Effect9.map(
2055
+ (sessions) => sessions.filter((session) => (session.title ?? "").toLowerCase().includes(queryLower)).map(
2056
+ (session) => ({
2057
+ sessionId: session.id,
2058
+ projectName: project.name,
2059
+ title: session.title ?? "Untitled",
2060
+ matchType: "title",
2061
+ timestamp: session.updatedAt
2062
+ })
2063
+ )
2064
+ )
2065
+ )
2066
+ );
2067
+ const titleResultsNested = yield* Effect9.all(titleSearchEffects, { concurrency: 10 });
2068
+ const titleResults = titleResultsNested.flat();
2069
+ let contentResults = [];
2070
+ if (searchContent) {
2071
+ const alreadyFoundIds = new Set(titleResults.map((r) => `${r.projectName}:${r.sessionId}`));
2072
+ const contentSearchEffects = targetProjects.map(
2073
+ (project) => searchProjectContent(project, queryLower, alreadyFoundIds)
2074
+ );
2075
+ const contentResultsNested = yield* Effect9.all(contentSearchEffects, { concurrency: 5 });
2076
+ contentResults = contentResultsNested.flat();
2077
+ }
2078
+ const allResults = [...titleResults, ...contentResults];
2079
+ return allResults.sort((a, b) => {
2080
+ const dateA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
2081
+ const dateB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
2082
+ return dateB - dateA;
2083
+ });
2084
+ });
2085
+
2086
+ // src/session/files.ts
2087
+ import { Effect as Effect10 } from "effect";
2088
+ var getSessionFiles = (projectName, sessionId) => Effect10.gen(function* () {
2089
+ const messages = yield* readSession(projectName, sessionId);
2090
+ const fileChanges = [];
2091
+ const seenFiles = /* @__PURE__ */ new Set();
2092
+ for (const msg of messages) {
2093
+ if (msg.type === "file-history-snapshot") {
2094
+ const snapshot = msg;
2095
+ const backups = snapshot.snapshot?.trackedFileBackups;
2096
+ if (backups && typeof backups === "object") {
2097
+ for (const filePath of Object.keys(backups)) {
2098
+ if (!seenFiles.has(filePath)) {
2099
+ seenFiles.add(filePath);
2100
+ fileChanges.push({
2101
+ path: filePath,
2102
+ action: "modified",
2103
+ timestamp: snapshot.snapshot?.timestamp,
2104
+ messageUuid: snapshot.messageId ?? msg.uuid
2105
+ });
2106
+ }
2107
+ }
2108
+ }
2109
+ }
2110
+ if (msg.type === "assistant" && msg.message?.content) {
2111
+ const content = msg.message.content;
2112
+ if (Array.isArray(content)) {
2113
+ for (const item of content) {
2114
+ if (item && typeof item === "object" && "type" in item && item.type === "tool_use") {
2115
+ const toolUse = item;
2116
+ if ((toolUse.name === "Write" || toolUse.name === "Edit") && toolUse.input?.file_path) {
2117
+ const filePath = toolUse.input.file_path;
2118
+ if (!seenFiles.has(filePath)) {
2119
+ seenFiles.add(filePath);
2120
+ fileChanges.push({
2121
+ path: filePath,
2122
+ action: toolUse.name === "Write" ? "created" : "modified",
2123
+ timestamp: msg.timestamp,
2124
+ messageUuid: msg.uuid
2125
+ });
2126
+ }
2127
+ }
2128
+ }
2129
+ }
2130
+ }
2131
+ }
2132
+ }
2133
+ return {
2134
+ sessionId,
2135
+ projectName,
2136
+ files: fileChanges,
2137
+ totalChanges: fileChanges.length
2138
+ };
2139
+ });
2140
+
2141
+ // src/session/index-file.ts
2142
+ import { Effect as Effect11 } from "effect";
2143
+ import * as fs11 from "fs/promises";
2144
+ import * as path10 from "path";
2145
+ var loadSessionsIndex = (projectName) => Effect11.gen(function* () {
2146
+ const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
2147
+ try {
2148
+ const content = yield* Effect11.tryPromise(() => fs11.readFile(indexPath, "utf-8"));
2149
+ const index = JSON.parse(content);
2150
+ return index;
2151
+ } catch {
2152
+ return null;
2153
+ }
2154
+ });
2155
+ var getIndexEntryDisplayTitle = (entry) => {
2156
+ if (entry.customTitle) return entry.customTitle;
2157
+ if (entry.summary) return entry.summary;
2158
+ let prompt = entry.firstPrompt;
2159
+ if (prompt === "No prompt") return "Untitled";
2160
+ if (prompt.startsWith("[Request interrupted")) return "Untitled";
2161
+ prompt = prompt.replace(/<ide_[^>]*>[^<]*<\/ide_[^>]*>/g, "").trim();
2162
+ if (!prompt) return "Untitled";
2163
+ if (prompt.length > 60) {
2164
+ return prompt.slice(0, 57) + "...";
2165
+ }
2166
+ return prompt;
2167
+ };
2168
+ var sortIndexEntriesByModified = (entries) => {
2169
+ return [...entries].sort((a, b) => {
2170
+ const modA = new Date(a.modified).getTime();
2171
+ const modB = new Date(b.modified).getTime();
2172
+ return modB - modA;
2173
+ });
2174
+ };
2175
+ var hasSessionsIndex = (projectName) => Effect11.gen(function* () {
2176
+ const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
2177
+ try {
2178
+ yield* Effect11.tryPromise(() => fs11.access(indexPath));
2179
+ return true;
2180
+ } catch {
2181
+ return false;
2182
+ }
2183
+ });
1830
2184
  export {
1831
2185
  analyzeSession,
1832
2186
  clearSessions,
@@ -1834,6 +2188,7 @@ export {
1834
2188
  createLogger,
1835
2189
  deleteLinkedTodos,
1836
2190
  deleteMessage,
2191
+ deleteMessageWithChainRepair,
1837
2192
  deleteOrphanAgents,
1838
2193
  deleteOrphanTodos,
1839
2194
  deleteSession,
@@ -1849,11 +2204,14 @@ export {
1849
2204
  folderNameToDisplayPath,
1850
2205
  folderNameToPath,
1851
2206
  getDisplayTitle,
2207
+ getIndexEntryDisplayTitle,
1852
2208
  getLogger,
1853
2209
  getRealPathFromSession,
1854
2210
  getSessionFiles,
2211
+ getSessionSortTimestamp,
1855
2212
  getSessionsDir,
1856
2213
  getTodosDir,
2214
+ hasSessionsIndex,
1857
2215
  isContinuationSummary,
1858
2216
  isInvalidApiKeyMessage,
1859
2217
  listProjects,
@@ -1861,19 +2219,27 @@ export {
1861
2219
  loadAgentMessages,
1862
2220
  loadProjectTreeData,
1863
2221
  loadSessionTreeData,
2222
+ loadSessionsIndex,
1864
2223
  maskHomePath,
1865
2224
  moveSession,
2225
+ parseCommandMessage,
2226
+ parseJsonlLines,
1866
2227
  pathToFolderName,
1867
2228
  previewCleanup,
2229
+ readJsonlFile,
1868
2230
  readSession,
1869
2231
  renameSession,
1870
2232
  restoreMessage,
1871
2233
  searchSessions,
1872
2234
  sessionHasTodos,
1873
2235
  setLogger,
2236
+ sortIndexEntriesByModified,
1874
2237
  sortProjects,
1875
2238
  splitSession,
1876
2239
  summarizeSession,
1877
- updateSessionSummary
2240
+ tryParseJsonLine,
2241
+ updateSessionSummary,
2242
+ validateChain,
2243
+ validateToolUseResult
1878
2244
  };
1879
2245
  //# sourceMappingURL=index.js.map