codesesh 0.1.5 → 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 +603 -40
- package/dist/index.js.map +1 -1
- package/dist/web/assets/index-B4PtJ1VM.css +2 -0
- package/dist/web/assets/{index-Bz2gXVMS.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,13 +121,178 @@ 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
|
|
|
@@ -103,31 +309,370 @@ function findWebDistPath() {
|
|
|
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
|
+
}
|
|
120
372
|
};
|
|
121
373
|
}
|
|
122
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()
|
|
438
|
+
};
|
|
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
|
+
};
|
|
667
|
+
|
|
123
668
|
// src/output.ts
|
|
124
669
|
import { consola } from "consola";
|
|
125
670
|
|
|
126
671
|
// src/version.ts
|
|
127
672
|
import { readFileSync } from "fs";
|
|
128
|
-
import { resolve as resolve2, dirname as
|
|
673
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
129
674
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
130
|
-
var __dirname =
|
|
675
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
131
676
|
var pkg = JSON.parse(readFileSync(resolve2(__dirname, "../package.json"), "utf-8"));
|
|
132
677
|
var VERSION = pkg.version;
|
|
133
678
|
|
|
@@ -262,7 +807,7 @@ var main = defineCommand({
|
|
|
262
807
|
perf.enable();
|
|
263
808
|
}
|
|
264
809
|
if (clearCache) {
|
|
265
|
-
const { clearCache: clear } = await import("./dist-
|
|
810
|
+
const { clearCache: clear } = await import("./dist-6EV6SS6N.js");
|
|
266
811
|
clear();
|
|
267
812
|
console.log("Cache cleared.");
|
|
268
813
|
}
|
|
@@ -278,27 +823,35 @@ var main = defineCommand({
|
|
|
278
823
|
if (cwdFilter === ".") {
|
|
279
824
|
cwdFilter = process.cwd();
|
|
280
825
|
}
|
|
281
|
-
let
|
|
826
|
+
let listDefaultFrom;
|
|
827
|
+
let listDefaultDays;
|
|
282
828
|
if (args.from) {
|
|
283
|
-
|
|
829
|
+
listDefaultFrom = parseDateToTimestamp(args.from);
|
|
284
830
|
} else {
|
|
285
831
|
const days = parseInt(args.days, 10);
|
|
286
832
|
if (!Number.isNaN(days) && days > 0) {
|
|
287
|
-
|
|
833
|
+
listDefaultFrom = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
834
|
+
listDefaultDays = days;
|
|
288
835
|
}
|
|
289
836
|
}
|
|
837
|
+
const listDefaultTo = args.to ? parseDateToTimestamp(args.to) : void 0;
|
|
290
838
|
const scanOptions = {
|
|
291
839
|
agents: targetSession ? [targetSession.agent] : args.agent ? args.agent.split(",").map((a) => a.trim()) : void 0,
|
|
292
840
|
cwd: cwdFilter,
|
|
293
|
-
from: fromTimestamp,
|
|
294
|
-
to: args.to ? parseDateToTimestamp(args.to) : void 0,
|
|
295
841
|
useCache
|
|
296
842
|
};
|
|
297
|
-
const
|
|
843
|
+
const store = new LiveScanStore(!jsonOnly, scanOptions);
|
|
844
|
+
await store.initialize();
|
|
845
|
+
const result = store.getSnapshot();
|
|
298
846
|
if (trace) {
|
|
299
847
|
console.log(perf.getReport());
|
|
300
848
|
}
|
|
301
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
|
+
});
|
|
302
855
|
const info = getAgentInfoMap(
|
|
303
856
|
Object.fromEntries(Object.entries(result.byAgent).map(([k, v]) => [k, v.length]))
|
|
304
857
|
);
|
|
@@ -309,15 +862,25 @@ var main = defineCommand({
|
|
|
309
862
|
count,
|
|
310
863
|
available: count > 0
|
|
311
864
|
})),
|
|
312
|
-
sessions:
|
|
865
|
+
sessions: windowed
|
|
313
866
|
};
|
|
314
867
|
console.log(JSON.stringify(output, null, 2));
|
|
315
868
|
return;
|
|
316
869
|
}
|
|
317
870
|
const agents = createRegisteredAgents();
|
|
318
871
|
printScanResults(agents, result);
|
|
319
|
-
|
|
320
|
-
|
|
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}`);
|
|
321
884
|
console.log("");
|
|
322
885
|
if (!noOpen) {
|
|
323
886
|
const open = (await import("open")).default;
|