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