codesesh 0.1.4 → 0.2.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/README.md +12 -0
- package/dist/{chunk-FG2FZIU5.js → chunk-2UNXB2D3.js} +185 -125
- package/dist/chunk-2UNXB2D3.js.map +1 -0
- package/dist/{dist-ZF35YB7B.js → dist-6EV6SS6N.js} +4 -2
- package/dist/index.js +615 -43
- package/dist/index.js.map +1 -1
- package/dist/web/assets/index-B4PtJ1VM.css +2 -0
- package/dist/web/assets/{index-B5ae_tcH.js → index-BzkyjHEA.js} +40 -40
- package/dist/web/index.html +2 -2
- package/package.json +9 -2
- package/dist/chunk-FG2FZIU5.js.map +0 -1
- package/dist/web/assets/index-vSqmWltx.css +0 -2
- /package/dist/{dist-ZF35YB7B.js.map → dist-6EV6SS6N.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createRegisteredAgents,
|
|
4
|
+
filterSessions,
|
|
4
5
|
getAgentInfoMap,
|
|
6
|
+
getCursorDataPath,
|
|
5
7
|
perf,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
resolveProviderRoots,
|
|
9
|
+
saveCachedSessions,
|
|
10
|
+
scanSessions
|
|
11
|
+
} from "./chunk-2UNXB2D3.js";
|
|
8
12
|
|
|
9
13
|
// src/index.ts
|
|
10
14
|
import { defineCommand, runMain } from "citty";
|
|
@@ -22,20 +26,62 @@ import { fileURLToPath } from "url";
|
|
|
22
26
|
import { Hono } from "hono";
|
|
23
27
|
|
|
24
28
|
// src/api/handlers.ts
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
function getTotalTokens(stats) {
|
|
30
|
+
return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
|
|
31
|
+
}
|
|
32
|
+
function getSessionActivityTime(session) {
|
|
33
|
+
return session.time_updated ?? session.time_created;
|
|
34
|
+
}
|
|
35
|
+
function parseDateParam(value, fallback) {
|
|
36
|
+
if (value == null) return fallback;
|
|
37
|
+
const ts = new Date(value).getTime();
|
|
38
|
+
return Number.isNaN(ts) ? fallback : ts;
|
|
39
|
+
}
|
|
40
|
+
function filterSessionsByWindow(sessions, from, to) {
|
|
41
|
+
if (from == null && to == null) return sessions;
|
|
42
|
+
return sessions.filter((s) => {
|
|
43
|
+
if (from != null && s.time_created < from) return false;
|
|
44
|
+
if (to != null && s.time_created > to) return false;
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function filterSessionsByActivityWindow(sessions, from, to) {
|
|
49
|
+
if (from == null && to == null) return sessions;
|
|
50
|
+
return sessions.filter((session) => {
|
|
51
|
+
const activity = getSessionActivityTime(session);
|
|
52
|
+
if (from != null && activity < from) return false;
|
|
53
|
+
if (to != null && activity > to) return false;
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function handleGetConfig(c, defaults) {
|
|
58
|
+
return c.json({
|
|
59
|
+
window: {
|
|
60
|
+
from: defaults.from,
|
|
61
|
+
to: defaults.to,
|
|
62
|
+
days: defaults.days
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function handleGetAgents(c, scanSource, defaults = {}) {
|
|
67
|
+
const scanResult = scanSource.getSnapshot();
|
|
68
|
+
const { from, to } = defaults;
|
|
69
|
+
const counts = Object.fromEntries(
|
|
70
|
+
Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
|
|
71
|
+
agentName,
|
|
72
|
+
filterSessionsByWindow(sessions, from, to).length
|
|
73
|
+
])
|
|
74
|
+
);
|
|
30
75
|
const info = getAgentInfoMap(counts);
|
|
31
76
|
return c.json(info);
|
|
32
77
|
}
|
|
33
|
-
function handleGetSessions(c,
|
|
78
|
+
function handleGetSessions(c, scanSource, defaults = {}) {
|
|
79
|
+
const scanResult = scanSource.getSnapshot();
|
|
34
80
|
const agent = c.req.query("agent");
|
|
35
81
|
const q = c.req.query("q")?.toLowerCase();
|
|
36
82
|
const cwd = c.req.query("cwd")?.toLowerCase();
|
|
37
|
-
const from = c.req.query("from");
|
|
38
|
-
const to = c.req.query("to");
|
|
83
|
+
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
84
|
+
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
39
85
|
let sessions = [];
|
|
40
86
|
if (agent && scanResult.byAgent[agent]) {
|
|
41
87
|
sessions = [...scanResult.byAgent[agent]];
|
|
@@ -45,24 +91,19 @@ function handleGetSessions(c, scanResult) {
|
|
|
45
91
|
if (cwd) {
|
|
46
92
|
sessions = sessions.filter((s) => s.directory.toLowerCase().includes(cwd));
|
|
47
93
|
}
|
|
48
|
-
if (from) {
|
|
49
|
-
|
|
50
|
-
if (!Number.isNaN(fromTs)) {
|
|
51
|
-
sessions = sessions.filter((s) => s.time_created >= fromTs);
|
|
52
|
-
}
|
|
94
|
+
if (from != null) {
|
|
95
|
+
sessions = sessions.filter((s) => s.time_created >= from);
|
|
53
96
|
}
|
|
54
|
-
if (to) {
|
|
55
|
-
|
|
56
|
-
if (!Number.isNaN(toTs)) {
|
|
57
|
-
sessions = sessions.filter((s) => s.time_created <= toTs);
|
|
58
|
-
}
|
|
97
|
+
if (to != null) {
|
|
98
|
+
sessions = sessions.filter((s) => s.time_created <= to);
|
|
59
99
|
}
|
|
60
100
|
if (q) {
|
|
61
101
|
sessions = sessions.filter((s) => s.title.toLowerCase().includes(q));
|
|
62
102
|
}
|
|
63
103
|
return c.json({ sessions });
|
|
64
104
|
}
|
|
65
|
-
async function handleGetSessionData(c,
|
|
105
|
+
async function handleGetSessionData(c, scanSource) {
|
|
106
|
+
const scanResult = scanSource.getSnapshot();
|
|
66
107
|
const agentName = c.req.param("agent");
|
|
67
108
|
const sessionId = c.req.param("id");
|
|
68
109
|
if (!sessionId) {
|
|
@@ -80,53 +121,567 @@ async function handleGetSessionData(c, scanResult) {
|
|
|
80
121
|
return c.json({ error: message }, 500);
|
|
81
122
|
}
|
|
82
123
|
}
|
|
124
|
+
function toLocalDateKey(ts) {
|
|
125
|
+
const d = new Date(ts);
|
|
126
|
+
const year = d.getFullYear();
|
|
127
|
+
const month = `${d.getMonth() + 1}`.padStart(2, "0");
|
|
128
|
+
const day = `${d.getDate()}`.padStart(2, "0");
|
|
129
|
+
return `${year}-${month}-${day}`;
|
|
130
|
+
}
|
|
131
|
+
function startOfLocalDay(ts) {
|
|
132
|
+
const d = new Date(ts);
|
|
133
|
+
d.setHours(0, 0, 0, 0);
|
|
134
|
+
return d.getTime();
|
|
135
|
+
}
|
|
136
|
+
function resolveDashboardWindow(defaults, queryDays, queryFrom, queryTo) {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
const todayStart = startOfLocalDay(now);
|
|
139
|
+
const toTs = parseDateParam(queryTo, defaults.to) ?? todayStart + 24 * 60 * 60 * 1e3 - 1;
|
|
140
|
+
const parsedDays = queryDays ? parseInt(queryDays, 10) : NaN;
|
|
141
|
+
let days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : defaults.days;
|
|
142
|
+
const fromFromQuery = parseDateParam(queryFrom, void 0);
|
|
143
|
+
let fromTs;
|
|
144
|
+
if (fromFromQuery != null) {
|
|
145
|
+
fromTs = startOfLocalDay(fromFromQuery);
|
|
146
|
+
days ??= Math.max(1, Math.ceil((todayStart - fromTs) / 864e5) + 1);
|
|
147
|
+
} else if (days && days > 0) {
|
|
148
|
+
fromTs = todayStart - (days - 1) * 864e5;
|
|
149
|
+
} else if (defaults.from != null) {
|
|
150
|
+
fromTs = startOfLocalDay(defaults.from);
|
|
151
|
+
days = Math.max(1, Math.ceil((todayStart - fromTs) / 864e5) + 1);
|
|
152
|
+
} else {
|
|
153
|
+
days = 30;
|
|
154
|
+
fromTs = todayStart - (days - 1) * 864e5;
|
|
155
|
+
}
|
|
156
|
+
return { from: fromTs, to: toTs, days };
|
|
157
|
+
}
|
|
158
|
+
function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
159
|
+
const scanResult = scanSource.getSnapshot();
|
|
160
|
+
const { from, to, days } = resolveDashboardWindow(
|
|
161
|
+
defaults,
|
|
162
|
+
c.req.query("days"),
|
|
163
|
+
c.req.query("from"),
|
|
164
|
+
c.req.query("to")
|
|
165
|
+
);
|
|
166
|
+
const windowed = filterSessionsByActivityWindow(scanResult.sessions, from, to);
|
|
167
|
+
const agentInfo = getAgentInfoMap(
|
|
168
|
+
Object.fromEntries(
|
|
169
|
+
Object.entries(scanResult.byAgent).map(([name, sessions]) => [
|
|
170
|
+
name,
|
|
171
|
+
filterSessionsByActivityWindow(sessions, from, to).length
|
|
172
|
+
])
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
|
|
176
|
+
let totalMessages = 0;
|
|
177
|
+
let totalTokens = 0;
|
|
178
|
+
let totalCost = 0;
|
|
179
|
+
let latestActivity = 0;
|
|
180
|
+
for (const session of windowed) {
|
|
181
|
+
totalMessages += session.stats.message_count;
|
|
182
|
+
totalTokens += getTotalTokens(session.stats);
|
|
183
|
+
totalCost += session.stats.total_cost ?? 0;
|
|
184
|
+
const activity = getSessionActivityTime(session);
|
|
185
|
+
if (activity > latestActivity) latestActivity = activity;
|
|
186
|
+
}
|
|
187
|
+
const perAgent = Object.entries(scanResult.byAgent).map(([name, sessions]) => {
|
|
188
|
+
const info = agentInfoMap.get(name);
|
|
189
|
+
const agentWindowed = filterSessionsByActivityWindow(sessions, from, to);
|
|
190
|
+
let messages = 0;
|
|
191
|
+
let tokens = 0;
|
|
192
|
+
for (const s of agentWindowed) {
|
|
193
|
+
messages += s.stats.message_count;
|
|
194
|
+
tokens += getTotalTokens(s.stats);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
name,
|
|
198
|
+
displayName: info?.displayName ?? name,
|
|
199
|
+
icon: info?.icon ?? "",
|
|
200
|
+
sessions: agentWindowed.length,
|
|
201
|
+
messages,
|
|
202
|
+
tokens
|
|
203
|
+
};
|
|
204
|
+
}).filter((item) => item.sessions > 0).sort((a, b) => b.sessions - a.sessions);
|
|
205
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
206
|
+
const bucketStart = startOfLocalDay(from);
|
|
207
|
+
for (let i = 0; i < days; i += 1) {
|
|
208
|
+
const ts = bucketStart + i * 864e5;
|
|
209
|
+
const key = toLocalDateKey(ts);
|
|
210
|
+
dailyMap.set(key, { date: key, sessions: 0, messages: 0 });
|
|
211
|
+
}
|
|
212
|
+
for (const session of windowed) {
|
|
213
|
+
const key = toLocalDateKey(getSessionActivityTime(session));
|
|
214
|
+
const bucket = dailyMap.get(key);
|
|
215
|
+
if (bucket) {
|
|
216
|
+
bucket.sessions += 1;
|
|
217
|
+
bucket.messages += session.stats.message_count;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const dailyActivity = [...dailyMap.values()];
|
|
221
|
+
const recentSessions = [...windowed].sort((a, b) => getSessionActivityTime(b) - getSessionActivityTime(a)).slice(0, 10).map((session) => {
|
|
222
|
+
const agentKey = session.slug.split("/")[0] ?? "unknown";
|
|
223
|
+
return { ...session, agentName: agentKey };
|
|
224
|
+
});
|
|
225
|
+
const data = {
|
|
226
|
+
totals: {
|
|
227
|
+
sessions: windowed.length,
|
|
228
|
+
messages: totalMessages,
|
|
229
|
+
tokens: totalTokens,
|
|
230
|
+
cost: totalCost,
|
|
231
|
+
latestActivity: latestActivity || void 0
|
|
232
|
+
},
|
|
233
|
+
perAgent,
|
|
234
|
+
dailyActivity,
|
|
235
|
+
recentSessions,
|
|
236
|
+
window: { from, to, days }
|
|
237
|
+
};
|
|
238
|
+
return c.json(data);
|
|
239
|
+
}
|
|
83
240
|
|
|
84
241
|
// src/api/routes.ts
|
|
85
|
-
function
|
|
242
|
+
function createSseResponse(store, signal) {
|
|
243
|
+
const encoder = new TextEncoder();
|
|
244
|
+
return new Response(
|
|
245
|
+
new ReadableStream({
|
|
246
|
+
start(controller) {
|
|
247
|
+
const write = (event, data) => {
|
|
248
|
+
controller.enqueue(encoder.encode(`event: ${event}
|
|
249
|
+
`));
|
|
250
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
251
|
+
|
|
252
|
+
`));
|
|
253
|
+
};
|
|
254
|
+
write("connected", { timestamp: Date.now() });
|
|
255
|
+
const unsubscribe = store.subscribe((event) => {
|
|
256
|
+
write(event.type, event);
|
|
257
|
+
});
|
|
258
|
+
const heartbeat = setInterval(() => {
|
|
259
|
+
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
260
|
+
}, 15e3);
|
|
261
|
+
const close = () => {
|
|
262
|
+
clearInterval(heartbeat);
|
|
263
|
+
unsubscribe();
|
|
264
|
+
controller.close();
|
|
265
|
+
};
|
|
266
|
+
signal.addEventListener("abort", close, { once: true });
|
|
267
|
+
},
|
|
268
|
+
cancel() {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}),
|
|
272
|
+
{
|
|
273
|
+
headers: {
|
|
274
|
+
"Cache-Control": "no-cache",
|
|
275
|
+
Connection: "keep-alive",
|
|
276
|
+
"Content-Type": "text/event-stream"
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function createApiRoutes(scanSource, store, options = {}) {
|
|
86
282
|
const api = new Hono();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
283
|
+
const listDefaults = {
|
|
284
|
+
from: options.defaultSessionFrom,
|
|
285
|
+
to: options.defaultSessionTo,
|
|
286
|
+
days: options.defaultSessionDays
|
|
287
|
+
};
|
|
288
|
+
api.get("/config", (c) => handleGetConfig(c, listDefaults));
|
|
289
|
+
api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
|
|
290
|
+
api.get("/sessions", (c) => handleGetSessions(c, scanSource, listDefaults));
|
|
291
|
+
api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
|
|
292
|
+
api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
|
|
293
|
+
if (store) {
|
|
294
|
+
api.get("/events", (c) => createSseResponse(store, c.req.raw.signal));
|
|
295
|
+
}
|
|
90
296
|
return api;
|
|
91
297
|
}
|
|
92
298
|
|
|
93
299
|
// src/server.ts
|
|
94
300
|
function findWebDistPath() {
|
|
95
|
-
const
|
|
96
|
-
const packagedPath = resolve(
|
|
301
|
+
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
302
|
+
const packagedPath = resolve(__dirname2, "web");
|
|
97
303
|
if (existsSync(packagedPath)) {
|
|
98
304
|
return packagedPath;
|
|
99
305
|
}
|
|
100
|
-
const devPath = resolve(
|
|
306
|
+
const devPath = resolve(__dirname2, "../../../apps/web/dist");
|
|
101
307
|
if (existsSync(devPath)) {
|
|
102
308
|
return devPath;
|
|
103
309
|
}
|
|
104
310
|
return null;
|
|
105
311
|
}
|
|
106
|
-
|
|
312
|
+
function waitForListening(server) {
|
|
313
|
+
return new Promise((resolve3, reject) => {
|
|
314
|
+
const handleListening = () => {
|
|
315
|
+
server.off("error", handleError);
|
|
316
|
+
resolve3();
|
|
317
|
+
};
|
|
318
|
+
const handleError = (error) => {
|
|
319
|
+
server.off("listening", handleListening);
|
|
320
|
+
reject(error);
|
|
321
|
+
};
|
|
322
|
+
server.once("listening", handleListening);
|
|
323
|
+
server.once("error", handleError);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function getServerStartupErrorMessage(error, port) {
|
|
327
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE") {
|
|
328
|
+
return `Port ${port} \u5DF2\u88AB\u5360\u7528\uFF0C\u8BF7\u5173\u95ED\u73B0\u6709 CodeSesh \u8FDB\u7A0B\u6216\u6539\u7528 --port \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3\u3002`;
|
|
329
|
+
}
|
|
330
|
+
return error instanceof Error ? error.message : `\u542F\u52A8\u670D\u52A1\u5668\u5931\u8D25: ${String(error)}`;
|
|
331
|
+
}
|
|
332
|
+
async function createServer(port, store, options = {}) {
|
|
107
333
|
const app = new Hono2();
|
|
108
334
|
app.use("*", logger());
|
|
109
|
-
|
|
335
|
+
const routeOptions = {
|
|
336
|
+
defaultSessionFrom: options.defaultSessionFrom,
|
|
337
|
+
defaultSessionTo: options.defaultSessionTo,
|
|
338
|
+
defaultSessionDays: options.defaultSessionDays
|
|
339
|
+
};
|
|
340
|
+
app.route(
|
|
341
|
+
"/api",
|
|
342
|
+
createApiRoutes(
|
|
343
|
+
store,
|
|
344
|
+
"subscribe" in store ? store : void 0,
|
|
345
|
+
routeOptions
|
|
346
|
+
)
|
|
347
|
+
);
|
|
110
348
|
const webDistPath = findWebDistPath();
|
|
111
349
|
if (webDistPath) {
|
|
112
350
|
app.use("/*", serveStatic({ root: webDistPath }));
|
|
113
351
|
app.get("/*", serveStatic({ root: webDistPath, path: "index.html" }));
|
|
114
352
|
}
|
|
115
353
|
const server = serve({ fetch: app.fetch, port });
|
|
354
|
+
try {
|
|
355
|
+
await waitForListening(server);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
server.close();
|
|
358
|
+
if (store.shutdown) {
|
|
359
|
+
await store.shutdown();
|
|
360
|
+
}
|
|
361
|
+
throw new Error(getServerStartupErrorMessage(error, port));
|
|
362
|
+
}
|
|
116
363
|
const url = `http://localhost:${port}`;
|
|
117
364
|
return {
|
|
118
365
|
url,
|
|
119
|
-
shutdown: () =>
|
|
366
|
+
shutdown: () => {
|
|
367
|
+
server.close();
|
|
368
|
+
if (store.shutdown) {
|
|
369
|
+
void store.shutdown();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/live-scan.ts
|
|
376
|
+
import { existsSync as existsSync2 } from "fs";
|
|
377
|
+
import { dirname as dirname2, isAbsolute, join } from "path";
|
|
378
|
+
import chokidar from "chokidar";
|
|
379
|
+
function sortSessions(sessions) {
|
|
380
|
+
return [...sessions].sort(
|
|
381
|
+
(a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
function sessionSignature(session) {
|
|
385
|
+
return JSON.stringify([
|
|
386
|
+
session.title,
|
|
387
|
+
session.directory,
|
|
388
|
+
session.time_created,
|
|
389
|
+
session.time_updated ?? session.time_created,
|
|
390
|
+
session.stats.message_count,
|
|
391
|
+
session.stats.total_input_tokens,
|
|
392
|
+
session.stats.total_output_tokens,
|
|
393
|
+
session.stats.total_cost,
|
|
394
|
+
session.stats.total_tokens ?? 0
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
397
|
+
function buildAgentCacheMeta(agent) {
|
|
398
|
+
const metaMap = agent.getSessionMetaMap?.();
|
|
399
|
+
const meta = {};
|
|
400
|
+
if (!metaMap) return meta;
|
|
401
|
+
for (const [id, data] of metaMap.entries()) {
|
|
402
|
+
meta[id] = { id, ...data };
|
|
403
|
+
}
|
|
404
|
+
return meta;
|
|
405
|
+
}
|
|
406
|
+
function buildUpdateEvent(agentName, previousSessions, nextSessions) {
|
|
407
|
+
const previousMap = new Map(previousSessions.map((session) => [session.id, session]));
|
|
408
|
+
const nextMap = new Map(nextSessions.map((session) => [session.id, session]));
|
|
409
|
+
let newSessions = 0;
|
|
410
|
+
let updatedSessions = 0;
|
|
411
|
+
let removedSessions = 0;
|
|
412
|
+
for (const [id, session] of nextMap.entries()) {
|
|
413
|
+
const previous = previousMap.get(id);
|
|
414
|
+
if (!previous) {
|
|
415
|
+
newSessions += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (sessionSignature(previous) !== sessionSignature(session)) {
|
|
419
|
+
updatedSessions += 1;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const id of previousMap.keys()) {
|
|
423
|
+
if (!nextMap.has(id)) {
|
|
424
|
+
removedSessions += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (newSessions === 0 && updatedSessions === 0 && removedSessions === 0) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
type: "sessions-updated",
|
|
432
|
+
changedAgents: [agentName],
|
|
433
|
+
newSessions,
|
|
434
|
+
updatedSessions,
|
|
435
|
+
removedSessions,
|
|
436
|
+
totalSessions: nextSessions.length,
|
|
437
|
+
timestamp: Date.now()
|
|
120
438
|
};
|
|
121
439
|
}
|
|
440
|
+
function closestWatchablePath(targetPath) {
|
|
441
|
+
if (!isAbsolute(targetPath) && !existsSync2(targetPath)) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
let current = targetPath;
|
|
445
|
+
while (!existsSync2(current)) {
|
|
446
|
+
const parent = dirname2(current);
|
|
447
|
+
if (parent === current) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
current = parent;
|
|
451
|
+
}
|
|
452
|
+
return current;
|
|
453
|
+
}
|
|
454
|
+
function resolveAgentWatchTargets(agentName) {
|
|
455
|
+
const roots = resolveProviderRoots();
|
|
456
|
+
const cursorDataPath = getCursorDataPath();
|
|
457
|
+
switch (agentName) {
|
|
458
|
+
case "claudecode":
|
|
459
|
+
return [
|
|
460
|
+
{ path: join(roots.claudeRoot, "projects"), depth: 2 },
|
|
461
|
+
{ path: "data/claudecode", depth: 2 }
|
|
462
|
+
];
|
|
463
|
+
case "codex":
|
|
464
|
+
return [{ path: join(roots.codexRoot, "sessions"), depth: 4 }];
|
|
465
|
+
case "cursor":
|
|
466
|
+
return cursorDataPath ? [
|
|
467
|
+
{ path: join(cursorDataPath, "globalStorage", "state.vscdb") },
|
|
468
|
+
{ path: join(cursorDataPath, "workspaceStorage"), depth: 2 }
|
|
469
|
+
] : [];
|
|
470
|
+
case "kimi":
|
|
471
|
+
return [
|
|
472
|
+
{ path: join(roots.kimiRoot, "sessions"), depth: 2 },
|
|
473
|
+
{ path: "data/kimi", depth: 2 }
|
|
474
|
+
];
|
|
475
|
+
case "opencode":
|
|
476
|
+
return [
|
|
477
|
+
{ path: join(roots.opencodeRoot, "opencode.db") },
|
|
478
|
+
{ path: "data/opencode/opencode.db" }
|
|
479
|
+
];
|
|
480
|
+
default:
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
var LiveScanStore = class {
|
|
485
|
+
constructor(watchEnabled = true, scanOptions = {}) {
|
|
486
|
+
this.watchEnabled = watchEnabled;
|
|
487
|
+
this.scanOptions = scanOptions;
|
|
488
|
+
}
|
|
489
|
+
watchEnabled;
|
|
490
|
+
scanOptions;
|
|
491
|
+
agents = [];
|
|
492
|
+
byAgent = {};
|
|
493
|
+
sessions = [];
|
|
494
|
+
listeners = /* @__PURE__ */ new Set();
|
|
495
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
496
|
+
refreshTimestamps = /* @__PURE__ */ new Map();
|
|
497
|
+
refreshInFlight = /* @__PURE__ */ new Set();
|
|
498
|
+
pendingRefreshes = /* @__PURE__ */ new Set();
|
|
499
|
+
watchers = [];
|
|
500
|
+
async initialize() {
|
|
501
|
+
const initialResult = await scanSessions({
|
|
502
|
+
...this.scanOptions,
|
|
503
|
+
useCache: true,
|
|
504
|
+
smartRefresh: false
|
|
505
|
+
});
|
|
506
|
+
const knownAgents = createRegisteredAgents();
|
|
507
|
+
const agentMap = /* @__PURE__ */ new Map();
|
|
508
|
+
const allowedAgents = this.getAllowedAgents();
|
|
509
|
+
for (const agent of initialResult.agents) {
|
|
510
|
+
agentMap.set(agent.name, agent);
|
|
511
|
+
}
|
|
512
|
+
for (const agent of knownAgents) {
|
|
513
|
+
if (!agentMap.has(agent.name)) {
|
|
514
|
+
agentMap.set(agent.name, agent);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
this.agents = [...agentMap.values()].filter((agent) => {
|
|
518
|
+
if (!allowedAgents) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
return allowedAgents.has(agent.name.toLowerCase());
|
|
522
|
+
});
|
|
523
|
+
for (const agent of this.agents) {
|
|
524
|
+
this.byAgent[agent.name] = sortSessions(initialResult.byAgent[agent.name] ?? []);
|
|
525
|
+
this.refreshTimestamps.set(agent.name, Date.now());
|
|
526
|
+
}
|
|
527
|
+
this.rebuildSessions();
|
|
528
|
+
if (this.watchEnabled) {
|
|
529
|
+
this.startWatching();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
getSnapshot() {
|
|
533
|
+
return {
|
|
534
|
+
sessions: this.sessions,
|
|
535
|
+
byAgent: this.byAgent,
|
|
536
|
+
agents: this.agents
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
subscribe(listener) {
|
|
540
|
+
this.listeners.add(listener);
|
|
541
|
+
return () => {
|
|
542
|
+
this.listeners.delete(listener);
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async shutdown() {
|
|
546
|
+
for (const timer of this.refreshTimers.values()) {
|
|
547
|
+
clearTimeout(timer);
|
|
548
|
+
}
|
|
549
|
+
this.refreshTimers.clear();
|
|
550
|
+
await Promise.all(this.watchers.map((watcher) => watcher.close()));
|
|
551
|
+
this.watchers = [];
|
|
552
|
+
}
|
|
553
|
+
emit(event) {
|
|
554
|
+
for (const listener of this.listeners) {
|
|
555
|
+
listener(event);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
rebuildSessions() {
|
|
559
|
+
this.sessions = sortSessions(Object.values(this.byAgent).flat());
|
|
560
|
+
}
|
|
561
|
+
getAllowedAgents() {
|
|
562
|
+
if (!this.scanOptions.agents?.length) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
|
|
566
|
+
}
|
|
567
|
+
applyFilters(sessions) {
|
|
568
|
+
return filterSessions(sessions, this.scanOptions);
|
|
569
|
+
}
|
|
570
|
+
startWatching() {
|
|
571
|
+
for (const agent of this.agents) {
|
|
572
|
+
const rawTargets = resolveAgentWatchTargets(agent.name);
|
|
573
|
+
const watchTargets = rawTargets.map((target) => {
|
|
574
|
+
const watchPath = closestWatchablePath(target.path);
|
|
575
|
+
return watchPath ? { ...target, path: watchPath } : null;
|
|
576
|
+
}).filter((target) => target !== null).filter(
|
|
577
|
+
(target, index, items) => items.findIndex((item) => item.path === target.path && item.depth === target.depth) === index
|
|
578
|
+
);
|
|
579
|
+
if (watchTargets.length === 0) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const watcher = chokidar.watch(
|
|
583
|
+
watchTargets.map((target) => target.path),
|
|
584
|
+
{
|
|
585
|
+
ignoreInitial: true,
|
|
586
|
+
awaitWriteFinish: {
|
|
587
|
+
stabilityThreshold: 250,
|
|
588
|
+
pollInterval: 100
|
|
589
|
+
},
|
|
590
|
+
depth: watchTargets.reduce(
|
|
591
|
+
(maxDepth, target) => Math.max(maxDepth, target.depth ?? 0),
|
|
592
|
+
0
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
watcher.on("all", () => {
|
|
597
|
+
this.scheduleRefresh(agent.name);
|
|
598
|
+
});
|
|
599
|
+
watcher.on("error", (error) => {
|
|
600
|
+
console.error(`[${agent.name}] File watcher failed:`, error);
|
|
601
|
+
});
|
|
602
|
+
this.watchers.push(watcher);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
scheduleRefresh(agentName, delayMs = 200) {
|
|
606
|
+
const existing = this.refreshTimers.get(agentName);
|
|
607
|
+
if (existing) {
|
|
608
|
+
clearTimeout(existing);
|
|
609
|
+
}
|
|
610
|
+
const timer = setTimeout(() => {
|
|
611
|
+
this.refreshTimers.delete(agentName);
|
|
612
|
+
void this.refreshAgent(agentName);
|
|
613
|
+
}, delayMs);
|
|
614
|
+
this.refreshTimers.set(agentName, timer);
|
|
615
|
+
}
|
|
616
|
+
async refreshAgent(agentName) {
|
|
617
|
+
if (this.refreshInFlight.has(agentName)) {
|
|
618
|
+
this.pendingRefreshes.add(agentName);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.refreshInFlight.add(agentName);
|
|
622
|
+
try {
|
|
623
|
+
await this.runRefresh(agentName);
|
|
624
|
+
} finally {
|
|
625
|
+
this.refreshInFlight.delete(agentName);
|
|
626
|
+
if (this.pendingRefreshes.delete(agentName)) {
|
|
627
|
+
this.scheduleRefresh(agentName, 100);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async runRefresh(agentName) {
|
|
632
|
+
const agent = this.agents.find((item) => item.name === agentName);
|
|
633
|
+
if (!agent) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const previousSessions = this.byAgent[agentName] ?? [];
|
|
637
|
+
let nextSessions = previousSessions;
|
|
638
|
+
if (!agent.isAvailable()) {
|
|
639
|
+
nextSessions = [];
|
|
640
|
+
this.refreshTimestamps.set(agentName, Date.now());
|
|
641
|
+
} else if (previousSessions.length > 0 && agent.checkForChanges && agent.incrementalScan) {
|
|
642
|
+
const checkResult = await Promise.resolve(
|
|
643
|
+
agent.checkForChanges(this.refreshTimestamps.get(agentName) ?? 0, previousSessions)
|
|
644
|
+
);
|
|
645
|
+
this.refreshTimestamps.set(agentName, checkResult.timestamp);
|
|
646
|
+
if (!checkResult.hasChanges) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
nextSessions = await Promise.resolve(
|
|
650
|
+
agent.incrementalScan(previousSessions, checkResult.changedIds ?? [])
|
|
651
|
+
);
|
|
652
|
+
} else {
|
|
653
|
+
nextSessions = await Promise.resolve(agent.scan());
|
|
654
|
+
this.refreshTimestamps.set(agentName, Date.now());
|
|
655
|
+
}
|
|
656
|
+
nextSessions = this.applyFilters(nextSessions);
|
|
657
|
+
saveCachedSessions(agentName, nextSessions, buildAgentCacheMeta(agent));
|
|
658
|
+
const event = buildUpdateEvent(agentName, previousSessions, nextSessions);
|
|
659
|
+
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
660
|
+
this.rebuildSessions();
|
|
661
|
+
if (event) {
|
|
662
|
+
event.totalSessions = this.sessions.length;
|
|
663
|
+
this.emit(event);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
122
667
|
|
|
123
668
|
// src/output.ts
|
|
124
669
|
import { consola } from "consola";
|
|
670
|
+
|
|
671
|
+
// src/version.ts
|
|
672
|
+
import { readFileSync } from "fs";
|
|
673
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
674
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
675
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
676
|
+
var pkg = JSON.parse(readFileSync(resolve2(__dirname, "../package.json"), "utf-8"));
|
|
677
|
+
var VERSION = pkg.version;
|
|
678
|
+
|
|
679
|
+
// src/output.ts
|
|
125
680
|
function printScanResults(agents, result) {
|
|
126
681
|
consola.log("");
|
|
127
682
|
consola.box({
|
|
128
683
|
title: "CodeSesh",
|
|
129
|
-
message: `
|
|
684
|
+
message: `v${VERSION} \u2022 ${result.sessions.length} sessions discovered`,
|
|
130
685
|
style: {
|
|
131
686
|
padding: 1,
|
|
132
687
|
borderColor: "cyan"
|
|
@@ -161,7 +716,6 @@ function dim(text) {
|
|
|
161
716
|
}
|
|
162
717
|
|
|
163
718
|
// src/index.ts
|
|
164
|
-
var VERSION = "0.1.1";
|
|
165
719
|
function parseDateToTimestamp(dateStr) {
|
|
166
720
|
const date = new Date(dateStr);
|
|
167
721
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -253,7 +807,7 @@ var main = defineCommand({
|
|
|
253
807
|
perf.enable();
|
|
254
808
|
}
|
|
255
809
|
if (clearCache) {
|
|
256
|
-
const { clearCache: clear } = await import("./dist-
|
|
810
|
+
const { clearCache: clear } = await import("./dist-6EV6SS6N.js");
|
|
257
811
|
clear();
|
|
258
812
|
console.log("Cache cleared.");
|
|
259
813
|
}
|
|
@@ -269,27 +823,35 @@ var main = defineCommand({
|
|
|
269
823
|
if (cwdFilter === ".") {
|
|
270
824
|
cwdFilter = process.cwd();
|
|
271
825
|
}
|
|
272
|
-
let
|
|
826
|
+
let listDefaultFrom;
|
|
827
|
+
let listDefaultDays;
|
|
273
828
|
if (args.from) {
|
|
274
|
-
|
|
829
|
+
listDefaultFrom = parseDateToTimestamp(args.from);
|
|
275
830
|
} else {
|
|
276
831
|
const days = parseInt(args.days, 10);
|
|
277
832
|
if (!Number.isNaN(days) && days > 0) {
|
|
278
|
-
|
|
833
|
+
listDefaultFrom = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
834
|
+
listDefaultDays = days;
|
|
279
835
|
}
|
|
280
836
|
}
|
|
837
|
+
const listDefaultTo = args.to ? parseDateToTimestamp(args.to) : void 0;
|
|
281
838
|
const scanOptions = {
|
|
282
839
|
agents: targetSession ? [targetSession.agent] : args.agent ? args.agent.split(",").map((a) => a.trim()) : void 0,
|
|
283
840
|
cwd: cwdFilter,
|
|
284
|
-
from: fromTimestamp,
|
|
285
|
-
to: args.to ? parseDateToTimestamp(args.to) : void 0,
|
|
286
841
|
useCache
|
|
287
842
|
};
|
|
288
|
-
const
|
|
843
|
+
const store = new LiveScanStore(!jsonOnly, scanOptions);
|
|
844
|
+
await store.initialize();
|
|
845
|
+
const result = store.getSnapshot();
|
|
289
846
|
if (trace) {
|
|
290
847
|
console.log(perf.getReport());
|
|
291
848
|
}
|
|
292
849
|
if (jsonOnly) {
|
|
850
|
+
const windowed = result.sessions.filter((s) => {
|
|
851
|
+
if (listDefaultFrom != null && s.time_created < listDefaultFrom) return false;
|
|
852
|
+
if (listDefaultTo != null && s.time_created > listDefaultTo) return false;
|
|
853
|
+
return true;
|
|
854
|
+
});
|
|
293
855
|
const info = getAgentInfoMap(
|
|
294
856
|
Object.fromEntries(Object.entries(result.byAgent).map(([k, v]) => [k, v.length]))
|
|
295
857
|
);
|
|
@@ -300,15 +862,25 @@ var main = defineCommand({
|
|
|
300
862
|
count,
|
|
301
863
|
available: count > 0
|
|
302
864
|
})),
|
|
303
|
-
sessions:
|
|
865
|
+
sessions: windowed
|
|
304
866
|
};
|
|
305
867
|
console.log(JSON.stringify(output, null, 2));
|
|
306
868
|
return;
|
|
307
869
|
}
|
|
308
870
|
const agents = createRegisteredAgents();
|
|
309
871
|
printScanResults(agents, result);
|
|
310
|
-
|
|
311
|
-
|
|
872
|
+
let url;
|
|
873
|
+
try {
|
|
874
|
+
({ url } = await createServer(port, store, {
|
|
875
|
+
defaultSessionFrom: listDefaultFrom,
|
|
876
|
+
defaultSessionTo: listDefaultTo,
|
|
877
|
+
defaultSessionDays: listDefaultDays
|
|
878
|
+
}));
|
|
879
|
+
} catch (error) {
|
|
880
|
+
console.error(getServerStartupErrorMessage(error, port));
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
console.log(` ${url}`);
|
|
312
884
|
console.log("");
|
|
313
885
|
if (!noOpen) {
|
|
314
886
|
const open = (await import("open")).default;
|