@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.d.ts +220 -122
- package/dist/index.js +1258 -892
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-Cz8chaYQ.d.ts → types-mWa378iC.d.ts} +46 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/paths.ts
|
|
2
|
-
import * as
|
|
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 (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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/
|
|
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
|
|
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) =>
|
|
321
|
+
var findLinkedAgents = (projectName, sessionId) => Effect2.gen(function* () {
|
|
281
322
|
const projectPath = path2.join(getSessionsDir(), projectName);
|
|
282
|
-
const files = yield*
|
|
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*
|
|
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) =>
|
|
342
|
+
var findOrphanAgentsWithPaths = (projectName) => Effect2.gen(function* () {
|
|
302
343
|
const projectPath = path2.join(getSessionsDir(), projectName);
|
|
303
|
-
const files = yield*
|
|
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
|
|
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*
|
|
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
|
|
338
|
-
if (
|
|
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*
|
|
341
|
-
() =>
|
|
381
|
+
const subagentsExists = yield* Effect2.tryPromise(
|
|
382
|
+
() => fs3.stat(subagentsPath).then(() => true).catch(() => false)
|
|
342
383
|
);
|
|
343
384
|
if (subagentsExists) {
|
|
344
|
-
const subagentFiles = yield*
|
|
345
|
-
() =>
|
|
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*
|
|
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) =>
|
|
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) =>
|
|
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*
|
|
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*
|
|
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*
|
|
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*
|
|
395
|
-
const files = await
|
|
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*
|
|
440
|
+
yield* Effect2.tryPromise(() => fs3.rmdir(subagentsDir));
|
|
400
441
|
cleanedFolders.push(subagentsDir);
|
|
401
442
|
const sessionDir = path2.dirname(subagentsDir);
|
|
402
|
-
const sessionDirEmpty = yield*
|
|
403
|
-
const files = await
|
|
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*
|
|
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) =>
|
|
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*
|
|
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 (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
444
|
-
import * as
|
|
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 = []) =>
|
|
485
|
+
var findLinkedTodos = (sessionId, agentIds = []) => Effect3.gen(function* () {
|
|
447
486
|
const todosDir = getTodosDir();
|
|
448
|
-
const exists = yield*
|
|
449
|
-
() =>
|
|
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*
|
|
462
|
-
() =>
|
|
500
|
+
const sessionTodoExists = yield* Effect3.tryPromise(
|
|
501
|
+
() => fs4.access(sessionTodoPath).then(() => true).catch(() => false)
|
|
463
502
|
);
|
|
464
503
|
if (sessionTodoExists) {
|
|
465
|
-
const content = yield*
|
|
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*
|
|
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*
|
|
485
|
-
() =>
|
|
523
|
+
const agentTodoExists = yield* Effect3.tryPromise(
|
|
524
|
+
() => fs4.access(agentTodoPath).then(() => true).catch(() => false)
|
|
486
525
|
);
|
|
487
526
|
if (agentTodoExists) {
|
|
488
|
-
const content = yield*
|
|
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 = []) =>
|
|
545
|
+
var sessionHasTodos = (sessionId, agentIds = []) => Effect3.gen(function* () {
|
|
507
546
|
const todosDir = getTodosDir();
|
|
508
|
-
const exists = yield*
|
|
509
|
-
() =>
|
|
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*
|
|
514
|
-
() =>
|
|
552
|
+
const sessionTodoExists = yield* Effect3.tryPromise(
|
|
553
|
+
() => fs4.access(sessionTodoPath).then(() => true).catch(() => false)
|
|
515
554
|
);
|
|
516
555
|
if (sessionTodoExists) {
|
|
517
|
-
const content = yield*
|
|
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*
|
|
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*
|
|
537
|
-
() =>
|
|
575
|
+
const agentTodoExists = yield* Effect3.tryPromise(
|
|
576
|
+
() => fs4.access(agentTodoPath).then(() => true).catch(() => false)
|
|
538
577
|
);
|
|
539
578
|
if (agentTodoExists) {
|
|
540
|
-
const content = yield*
|
|
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) =>
|
|
589
|
+
var deleteLinkedTodos = (sessionId, agentIds) => Effect3.gen(function* () {
|
|
551
590
|
const todosDir = getTodosDir();
|
|
552
|
-
const exists = yield*
|
|
553
|
-
() =>
|
|
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*
|
|
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*
|
|
561
|
-
() =>
|
|
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*
|
|
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*
|
|
572
|
-
() =>
|
|
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*
|
|
615
|
+
yield* Effect3.tryPromise(() => fs4.rename(agentTodoPath, backupPath));
|
|
577
616
|
deletedCount++;
|
|
578
617
|
}
|
|
579
618
|
}
|
|
580
619
|
return { deletedCount };
|
|
581
620
|
});
|
|
582
|
-
var findOrphanTodos = () =>
|
|
621
|
+
var findOrphanTodos = () => Effect3.gen(function* () {
|
|
583
622
|
const todosDir = getTodosDir();
|
|
584
623
|
const sessionsDir = getSessionsDir();
|
|
585
|
-
const [todosExists, sessionsExists] = yield*
|
|
586
|
-
|
|
587
|
-
() =>
|
|
624
|
+
const [todosExists, sessionsExists] = yield* Effect3.all([
|
|
625
|
+
Effect3.tryPromise(
|
|
626
|
+
() => fs4.access(todosDir).then(() => true).catch(() => false)
|
|
588
627
|
),
|
|
589
|
-
|
|
590
|
-
() =>
|
|
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*
|
|
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*
|
|
598
|
-
() =>
|
|
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*
|
|
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 = () =>
|
|
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*
|
|
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*
|
|
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
|
|
640
|
-
import * as
|
|
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 =
|
|
681
|
+
var listProjects = Effect4.gen(function* () {
|
|
643
682
|
const sessionsDir = getSessionsDir();
|
|
644
|
-
const exists = yield*
|
|
645
|
-
() =>
|
|
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*
|
|
651
|
-
const projects = yield*
|
|
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) =>
|
|
692
|
+
(entry) => Effect4.gen(function* () {
|
|
654
693
|
const projectPath = path4.join(sessionsDir, entry.name);
|
|
655
|
-
const files = yield*
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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 {
|
|
832
|
+
return { deleted: null, alsoDeleted: [] };
|
|
731
833
|
}
|
|
732
834
|
const deletedMsg = messages[targetIndex];
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (
|
|
737
|
-
|
|
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
|
-
|
|
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*
|
|
743
|
-
return { success: true
|
|
901
|
+
yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
|
|
902
|
+
return { success: true };
|
|
744
903
|
});
|
|
745
|
-
var
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
const
|
|
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*
|
|
995
|
+
yield* Effect5.tryPromise(() => fs6.writeFile(filePath, newContent, "utf-8"));
|
|
765
996
|
return { success: true };
|
|
766
997
|
});
|
|
767
|
-
var deleteSession = (projectName, sessionId) =>
|
|
998
|
+
var deleteSession = (projectName, sessionId) => Effect5.gen(function* () {
|
|
768
999
|
const sessionsDir = getSessionsDir();
|
|
769
|
-
const projectPath =
|
|
770
|
-
const filePath =
|
|
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
|
|
773
|
-
if (
|
|
774
|
-
yield*
|
|
775
|
-
const agentBackupDir2 =
|
|
776
|
-
yield*
|
|
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 =
|
|
779
|
-
const agentBackupPath =
|
|
780
|
-
yield*
|
|
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 =
|
|
787
|
-
yield*
|
|
788
|
-
const agentBackupDir =
|
|
789
|
-
yield*
|
|
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 =
|
|
792
|
-
const agentBackupPath =
|
|
793
|
-
yield*
|
|
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 =
|
|
798
|
-
yield*
|
|
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) =>
|
|
807
|
-
const projectPath =
|
|
808
|
-
const filePath =
|
|
809
|
-
const content = yield*
|
|
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
|
|
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*
|
|
834
|
-
const projectFiles = yield*
|
|
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 =
|
|
1069
|
+
const otherFilePath = path5.join(projectPath, file);
|
|
839
1070
|
try {
|
|
840
|
-
const
|
|
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 =
|
|
861
|
-
const
|
|
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*
|
|
1096
|
+
yield* Effect5.tryPromise(() => fs6.writeFile(summaryFilePath, newSummaryContent, "utf-8"));
|
|
870
1097
|
} else {
|
|
871
|
-
const
|
|
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*
|
|
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
|
|
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 =
|
|
1083
|
-
const targetPath =
|
|
1084
|
-
const sourceFile =
|
|
1085
|
-
const targetFile =
|
|
1086
|
-
const sourceExists = yield*
|
|
1087
|
-
() =>
|
|
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*
|
|
1093
|
-
() =>
|
|
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*
|
|
1140
|
+
yield* Effect5.tryPromise(() => fs6.mkdir(targetPath, { recursive: true }));
|
|
1099
1141
|
const linkedAgents = yield* findLinkedAgents(sourceProject, sessionId);
|
|
1100
|
-
yield*
|
|
1142
|
+
yield* Effect5.tryPromise(() => fs6.rename(sourceFile, targetFile));
|
|
1101
1143
|
for (const agentId of linkedAgents) {
|
|
1102
|
-
const sourceAgentFile =
|
|
1103
|
-
const targetAgentFile =
|
|
1104
|
-
const agentExists = yield*
|
|
1105
|
-
() =>
|
|
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*
|
|
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) =>
|
|
1114
|
-
const projectPath =
|
|
1115
|
-
const filePath =
|
|
1116
|
-
const
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1233
|
+
newSessionId,
|
|
1234
|
+
newSessionPath: newFilePath,
|
|
1235
|
+
movedMessageCount: movedMessages.length,
|
|
1236
|
+
duplicatedSummary: shouldDuplicate
|
|
1352
1237
|
};
|
|
1353
1238
|
});
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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) =>
|
|
1423
|
-
const projectPath =
|
|
1424
|
-
const filePath =
|
|
1425
|
-
const content = yield*
|
|
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
|
|
1287
|
+
const messages = parseJsonlLines(lines, filePath);
|
|
1428
1288
|
let summaries;
|
|
1429
1289
|
if (summariesByTargetSession) {
|
|
1430
|
-
summaries = [...summariesByTargetSession.get(sessionId) ?? []].sort(
|
|
1431
|
-
|
|
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*
|
|
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 =
|
|
1446
|
-
const otherContent = yield*
|
|
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 (
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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) =>
|
|
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 =
|
|
1352
|
+
const agentPath = path6.join(projectPath, `${agentId}.jsonl`);
|
|
1488
1353
|
try {
|
|
1489
|
-
const agentContent = yield*
|
|
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
|
|
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
|
|
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
|
|
1538
|
-
const
|
|
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*
|
|
1428
|
+
yield* Effect6.all(
|
|
1544
1429
|
allJsonlFiles.map(
|
|
1545
|
-
(file) =>
|
|
1546
|
-
const filePath =
|
|
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*
|
|
1434
|
+
const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
|
|
1550
1435
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
1551
|
-
for (
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
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*
|
|
1485
|
+
const sessions = yield* Effect6.all(
|
|
1600
1486
|
sessionFiles.map((file) => {
|
|
1601
1487
|
const sessionId = file.replace(".jsonl", "");
|
|
1602
|
-
|
|
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
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
|
1643
|
-
|
|
1644
|
-
|
|
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 = {}) =>
|
|
1644
|
+
var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(function* () {
|
|
1647
1645
|
const { keepSnapshots = "first_last", maxToolOutputLength = 5e3 } = options;
|
|
1648
|
-
const filePath =
|
|
1649
|
-
const content = yield*
|
|
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
|
|
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*
|
|
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) =>
|
|
1699
|
+
var extractProjectKnowledge = (projectName, sessionIds) => Effect7.gen(function* () {
|
|
1702
1700
|
const sessionsDir = getSessionsDir();
|
|
1703
|
-
const projectDir =
|
|
1701
|
+
const projectDir = path7.join(sessionsDir, projectName);
|
|
1704
1702
|
let targetSessionIds = sessionIds;
|
|
1705
1703
|
if (!targetSessionIds) {
|
|
1706
|
-
const files = yield*
|
|
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
|
-
|
|
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
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2240
|
+
tryParseJsonLine,
|
|
2241
|
+
updateSessionSummary,
|
|
2242
|
+
validateChain,
|
|
2243
|
+
validateToolUseResult
|
|
1878
2244
|
};
|
|
1879
2245
|
//# sourceMappingURL=index.js.map
|