codesesh 0.5.0 → 0.6.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 +6 -3
- package/dist/{chunk-FZNZAMTZ.js → chunk-SQYHWMQV.js} +2533 -458
- package/dist/chunk-SQYHWMQV.js.map +1 -0
- package/dist/{dist-DMEDEJ2D.js → dist-NT4CH6KD.js} +46 -2
- package/dist/index.js +680 -83
- package/dist/index.js.map +1 -1
- package/dist/search-index-worker.js +40 -0
- package/dist/search-index-worker.js.map +1 -0
- package/dist/web/assets/index-BlSglSCE.css +2 -0
- package/dist/web/assets/index-CnxgGfhM.js +106 -0
- package/dist/web/assets/vendor-Bs5B_LvM.js +43 -0
- package/dist/web/index.html +3 -3
- package/package.json +2 -3
- package/dist/chunk-FZNZAMTZ.js.map +0 -1
- package/dist/web/assets/index-BRW_TBMw.js +0 -106
- package/dist/web/assets/index-CCgk7cPa.css +0 -2
- package/dist/web/assets/vendor-CWmLg_mG.js +0 -43
- /package/dist/{dist-DMEDEJ2D.js.map → dist-NT4CH6KD.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
computeIdentity,
|
|
6
6
|
createRegisteredAgents,
|
|
7
7
|
deleteBookmark,
|
|
8
|
+
extractSessionFileActivity,
|
|
8
9
|
filterSessions,
|
|
9
10
|
getAgentInfoMap,
|
|
10
11
|
getCursorDataPath,
|
|
@@ -12,16 +13,19 @@ import {
|
|
|
12
13
|
importBookmarks,
|
|
13
14
|
listBookmarks,
|
|
14
15
|
listCachedProjectGroups,
|
|
16
|
+
listFileActivity,
|
|
17
|
+
parseSearchQuery,
|
|
15
18
|
perf,
|
|
16
19
|
realFs,
|
|
17
20
|
refreshPricingCache,
|
|
18
21
|
resolveProviderRoots,
|
|
19
22
|
saveCachedSessions,
|
|
20
23
|
scanSessions,
|
|
24
|
+
searchFileActivitySessions,
|
|
21
25
|
searchSessions,
|
|
22
26
|
syncSessionSearchIndex,
|
|
23
27
|
upsertBookmark
|
|
24
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-SQYHWMQV.js";
|
|
25
29
|
|
|
26
30
|
// src/index.ts
|
|
27
31
|
import { defineCommand, runMain } from "citty";
|
|
@@ -167,6 +171,24 @@ var AppLogger = class {
|
|
|
167
171
|
}
|
|
168
172
|
};
|
|
169
173
|
var appLogger = new AppLogger();
|
|
174
|
+
function logSearchIndexSync(context, result, data = {}) {
|
|
175
|
+
if (!result || result.mode !== "bulk" || result.rebuildDurationMs == null) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
appLogger.info("search_index.sync", {
|
|
179
|
+
context,
|
|
180
|
+
agent: result.agentName,
|
|
181
|
+
mode: result.mode,
|
|
182
|
+
sessions: result.sessions,
|
|
183
|
+
changed: result.changed,
|
|
184
|
+
deleted: result.deleted,
|
|
185
|
+
indexed: result.indexed,
|
|
186
|
+
skipped: result.skipped,
|
|
187
|
+
duration_ms: Math.round(result.durationMs),
|
|
188
|
+
rebuild_duration_ms: Math.round(result.rebuildDurationMs),
|
|
189
|
+
...data
|
|
190
|
+
});
|
|
191
|
+
}
|
|
170
192
|
|
|
171
193
|
// src/api/handlers.ts
|
|
172
194
|
function isRecord(value) {
|
|
@@ -195,6 +217,9 @@ function parseBookmarkPayload(value) {
|
|
|
195
217
|
function getTotalTokens(stats) {
|
|
196
218
|
return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
|
|
197
219
|
}
|
|
220
|
+
function getSessionAgentName(session) {
|
|
221
|
+
return session.slug.split("/")[0]?.toLowerCase() || "unknown";
|
|
222
|
+
}
|
|
198
223
|
function getSessionActivityTime(session) {
|
|
199
224
|
return session.time_updated ?? session.time_created;
|
|
200
225
|
}
|
|
@@ -203,6 +228,56 @@ function parseDateParam(value, fallback) {
|
|
|
203
228
|
const ts = new Date(value).getTime();
|
|
204
229
|
return Number.isNaN(ts) ? fallback : ts;
|
|
205
230
|
}
|
|
231
|
+
function parseNumberParam(value) {
|
|
232
|
+
if (value == null || !value.trim()) return void 0;
|
|
233
|
+
const number = Number(value);
|
|
234
|
+
return Number.isFinite(number) ? number : void 0;
|
|
235
|
+
}
|
|
236
|
+
function searchParams(c) {
|
|
237
|
+
return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
|
|
238
|
+
}
|
|
239
|
+
function queryValues(params, ...names) {
|
|
240
|
+
return names.flatMap(
|
|
241
|
+
(name) => params.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
function parseSmartTags(values) {
|
|
245
|
+
const tags = values.map((value) => value.toLowerCase()).filter(
|
|
246
|
+
(value) => [
|
|
247
|
+
"bugfix",
|
|
248
|
+
"refactoring",
|
|
249
|
+
"feature-dev",
|
|
250
|
+
"testing",
|
|
251
|
+
"docs",
|
|
252
|
+
"git-ops",
|
|
253
|
+
"build-deploy",
|
|
254
|
+
"exploration",
|
|
255
|
+
"planning"
|
|
256
|
+
].includes(value)
|
|
257
|
+
);
|
|
258
|
+
return tags.length > 0 ? [...new Set(tags)] : void 0;
|
|
259
|
+
}
|
|
260
|
+
function parseSearchOptions(c, defaults) {
|
|
261
|
+
const params = searchParams(c);
|
|
262
|
+
const limitValue = parseNumberParam(params.get("limit") ?? void 0);
|
|
263
|
+
return {
|
|
264
|
+
agent: optionalQueryValue(params.get("agent") ?? void 0),
|
|
265
|
+
project: optionalQueryValue(params.get("project") ?? void 0),
|
|
266
|
+
projectKey: optionalQueryValue(params.get("projectKey") ?? void 0),
|
|
267
|
+
cwd: optionalQueryValue(params.get("cwd") ?? void 0),
|
|
268
|
+
tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
|
|
269
|
+
tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
|
|
270
|
+
file: optionalQueryValue(params.get("file") ?? params.get("path") ?? void 0),
|
|
271
|
+
fileKind: parseFileActivityKind(
|
|
272
|
+
optionalQueryValue(params.get("fileKind") ?? params.get("fileActivity") ?? void 0)
|
|
273
|
+
),
|
|
274
|
+
costMin: parseNumberParam(params.get("costMin") ?? void 0),
|
|
275
|
+
costMax: parseNumberParam(params.get("costMax") ?? void 0),
|
|
276
|
+
from: parseDateParam(params.get("from") ?? void 0, defaults.from),
|
|
277
|
+
to: parseDateParam(params.get("to") ?? void 0, defaults.to),
|
|
278
|
+
limit: limitValue && limitValue > 0 ? Math.min(limitValue, 100) : 50
|
|
279
|
+
};
|
|
280
|
+
}
|
|
206
281
|
function filterSessionsByWindow(sessions, from, to) {
|
|
207
282
|
return filterSessionsByActivityWindow(sessions, from, to);
|
|
208
283
|
}
|
|
@@ -233,6 +308,148 @@ function sanitizeClientLogData(value) {
|
|
|
233
308
|
})
|
|
234
309
|
);
|
|
235
310
|
}
|
|
311
|
+
function sessionMatchesCostFilter(session, options) {
|
|
312
|
+
const cost = session.stats.total_cost;
|
|
313
|
+
if (options.costMin != null) {
|
|
314
|
+
if (options.costMinExclusive ? cost <= options.costMin : cost < options.costMin) return false;
|
|
315
|
+
}
|
|
316
|
+
if (options.costMax != null) {
|
|
317
|
+
if (options.costMaxExclusive ? cost >= options.costMax : cost > options.costMax) return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
function mergeSearchLists(left, right) {
|
|
322
|
+
const values = [...left ?? [], ...right ?? []];
|
|
323
|
+
return values.length > 0 ? [...new Set(values)] : void 0;
|
|
324
|
+
}
|
|
325
|
+
function mergeSearchOptions(options, filters) {
|
|
326
|
+
return {
|
|
327
|
+
...options,
|
|
328
|
+
agent: options.agent ?? filters.agent,
|
|
329
|
+
project: options.project ?? filters.project,
|
|
330
|
+
projectKey: options.projectKey ?? filters.projectKey,
|
|
331
|
+
cwd: options.cwd ?? filters.cwd,
|
|
332
|
+
tags: mergeSearchLists(options.tags, filters.tags),
|
|
333
|
+
tools: mergeSearchLists(options.tools, filters.tools),
|
|
334
|
+
file: options.file ?? filters.file,
|
|
335
|
+
fileKind: options.fileKind ?? filters.fileKind,
|
|
336
|
+
costMin: options.costMin ?? filters.costMin,
|
|
337
|
+
costMax: options.costMax ?? filters.costMax,
|
|
338
|
+
costMinExclusive: options.costMinExclusive ?? filters.costMinExclusive,
|
|
339
|
+
costMaxExclusive: options.costMaxExclusive ?? filters.costMaxExclusive
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function mergeSearchResults(results, limit) {
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
const merged = [];
|
|
345
|
+
for (const result of results) {
|
|
346
|
+
const key = `${result.agentName}/${result.session.id}`;
|
|
347
|
+
if (seen.has(key)) continue;
|
|
348
|
+
seen.add(key);
|
|
349
|
+
merged.push(result);
|
|
350
|
+
if (merged.length >= limit) break;
|
|
351
|
+
}
|
|
352
|
+
return merged;
|
|
353
|
+
}
|
|
354
|
+
function getProjectGroupKey(identityKind, identityKey) {
|
|
355
|
+
return `${identityKind}:${identityKey}`;
|
|
356
|
+
}
|
|
357
|
+
function attachProjectMetrics(projects, sessions) {
|
|
358
|
+
const metrics = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const session of sessions) {
|
|
360
|
+
const identity = session.project_identity;
|
|
361
|
+
if (!identity) continue;
|
|
362
|
+
const key = getProjectGroupKey(identity.kind, identity.key);
|
|
363
|
+
let current = metrics.get(key);
|
|
364
|
+
if (!current) {
|
|
365
|
+
current = {
|
|
366
|
+
messages: 0,
|
|
367
|
+
tokens: 0,
|
|
368
|
+
cost: 0,
|
|
369
|
+
hasEstimatedCost: false,
|
|
370
|
+
agentStats: /* @__PURE__ */ new Map()
|
|
371
|
+
};
|
|
372
|
+
metrics.set(key, current);
|
|
373
|
+
}
|
|
374
|
+
const tokens = getTotalTokens(session.stats);
|
|
375
|
+
const cost = session.stats.total_cost ?? 0;
|
|
376
|
+
current.messages += session.stats.message_count;
|
|
377
|
+
current.tokens += tokens;
|
|
378
|
+
current.cost += cost;
|
|
379
|
+
if (session.stats.cost_source === "estimated") current.hasEstimatedCost = true;
|
|
380
|
+
const agentName = getSessionAgentName(session);
|
|
381
|
+
const agent = current.agentStats.get(agentName);
|
|
382
|
+
if (agent) {
|
|
383
|
+
agent.sessions += 1;
|
|
384
|
+
agent.messages += session.stats.message_count;
|
|
385
|
+
agent.tokens += tokens;
|
|
386
|
+
agent.cost += cost;
|
|
387
|
+
} else {
|
|
388
|
+
current.agentStats.set(agentName, {
|
|
389
|
+
name: agentName,
|
|
390
|
+
sessions: 1,
|
|
391
|
+
messages: session.stats.message_count,
|
|
392
|
+
tokens,
|
|
393
|
+
cost
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return projects.map((project) => {
|
|
398
|
+
const metric = metrics.get(getProjectGroupKey(project.identityKind, project.identityKey));
|
|
399
|
+
return {
|
|
400
|
+
...project,
|
|
401
|
+
messages: metric?.messages ?? 0,
|
|
402
|
+
tokens: metric?.tokens ?? 0,
|
|
403
|
+
cost: metric?.cost ?? 0,
|
|
404
|
+
cost_source: metric && metric.cost > 0 ? metric.hasEstimatedCost ? "estimated" : "recorded" : void 0,
|
|
405
|
+
agentStats: [...metric?.agentStats.values() ?? []].sort((a, b) => b.sessions - a.sessions)
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function matchesDashboardScope(session, scope) {
|
|
410
|
+
if (scope.agent && getSessionAgentName(session) !== scope.agent) return false;
|
|
411
|
+
if (scope.projectKey) {
|
|
412
|
+
const identity = session.project_identity;
|
|
413
|
+
if (!identity || identity.key !== scope.projectKey) return false;
|
|
414
|
+
if (scope.projectKind && identity.kind !== scope.projectKind) return false;
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
function filterSessionsByDashboardScope(sessions, scope) {
|
|
419
|
+
if (!scope.agent && !scope.projectKey) return sessions;
|
|
420
|
+
return sessions.filter((session) => matchesDashboardScope(session, scope));
|
|
421
|
+
}
|
|
422
|
+
function matchesRecentSearchFilters(session, options) {
|
|
423
|
+
if (options.projectKey && session.project_identity?.key !== options.projectKey) return false;
|
|
424
|
+
if (options.cwd && !matchesProjectScope(session, options.cwd)) return false;
|
|
425
|
+
if (options.project) {
|
|
426
|
+
const projectNeedle = options.project.toLowerCase();
|
|
427
|
+
const projectText = [
|
|
428
|
+
session.project_identity?.key,
|
|
429
|
+
session.project_identity?.displayName,
|
|
430
|
+
session.directory
|
|
431
|
+
].filter(Boolean).join("\n").toLowerCase();
|
|
432
|
+
if (!projectText.includes(projectNeedle)) return false;
|
|
433
|
+
}
|
|
434
|
+
if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if (!sessionMatchesCostFilter(session, options)) return false;
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
function recentSearchSessions(scanResult, options) {
|
|
441
|
+
const entries = options.agent ? [[options.agent, scanResult.byAgent[options.agent] ?? []]] : Object.entries(scanResult.byAgent);
|
|
442
|
+
return entries.flatMap(
|
|
443
|
+
([agentName, sessions]) => filterSessionsByActivityWindow(sessions, options.from, options.to).filter((session) => matchesRecentSearchFilters(session, options)).map((session) => ({ agentName, session }))
|
|
444
|
+
).toSorted(
|
|
445
|
+
(a, b) => (b.session.time_updated ?? b.session.time_created) - (a.session.time_updated ?? a.session.time_created)
|
|
446
|
+
).slice(0, options.limit).map(({ agentName, session }) => ({
|
|
447
|
+
agentName,
|
|
448
|
+
session,
|
|
449
|
+
snippet: `Recent session \xB7 ${session.directory}`,
|
|
450
|
+
matchType: "recent"
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
236
453
|
function handleGetConfig(c, defaults) {
|
|
237
454
|
return c.json({
|
|
238
455
|
window: {
|
|
@@ -257,10 +474,9 @@ function handleGetAgents(c, scanSource, defaults = {}) {
|
|
|
257
474
|
function handleGetProjects(c, scanSource, defaults = {}) {
|
|
258
475
|
const scanResult = scanSource.getSnapshot();
|
|
259
476
|
const { from, to } = defaults;
|
|
477
|
+
const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
|
|
260
478
|
return c.json({
|
|
261
|
-
projects: listCachedProjectGroups(
|
|
262
|
-
filterSessionsByActivityWindow(scanResult.sessions, from, to)
|
|
263
|
-
)
|
|
479
|
+
projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
|
|
264
480
|
});
|
|
265
481
|
}
|
|
266
482
|
function handleGetSessions(c, scanSource, defaults = {}) {
|
|
@@ -294,31 +510,60 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
294
510
|
}
|
|
295
511
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
296
512
|
const query = c.req.query("q")?.trim() ?? "";
|
|
297
|
-
if (!query) {
|
|
298
|
-
return c.json({ results: [] });
|
|
299
|
-
}
|
|
300
513
|
const scanResult = scanSource.getSnapshot();
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
514
|
+
const searchOptions = parseSearchOptions(c, defaults);
|
|
515
|
+
const parsedQuery = parseSearchQuery(query);
|
|
516
|
+
const mergedSearchOptions = mergeSearchOptions(searchOptions, parsedQuery.filters);
|
|
517
|
+
const textQuery = parsedQuery.text || (parsedQuery.hasQualifiers ? "" : query);
|
|
518
|
+
const needsIndexedSearch = Boolean(
|
|
519
|
+
textQuery || mergedSearchOptions.file || mergedSearchOptions.fileKind || mergedSearchOptions.tools?.length
|
|
520
|
+
);
|
|
521
|
+
if (!needsIndexedSearch) {
|
|
522
|
+
return c.json({
|
|
523
|
+
results: recentSearchSessions(
|
|
524
|
+
scanResult,
|
|
525
|
+
mergedSearchOptions
|
|
526
|
+
)
|
|
527
|
+
});
|
|
312
528
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
529
|
+
const fileQuery = mergedSearchOptions.file ?? (!parsedQuery.text ? parsedQuery.filters.file : void 0) ?? (!parsedQuery.hasQualifiers && query ? parsedQuery.text || query : "");
|
|
530
|
+
const results = mergeSearchResults(
|
|
531
|
+
[
|
|
532
|
+
...fileQuery ? searchFileActivitySessions(fileQuery, mergedSearchOptions) : [],
|
|
533
|
+
...searchSessions(query, mergedSearchOptions)
|
|
534
|
+
],
|
|
535
|
+
mergedSearchOptions.limit ?? 50
|
|
536
|
+
);
|
|
320
537
|
return c.json({ results });
|
|
321
538
|
}
|
|
539
|
+
function parseFileActivityKind(value) {
|
|
540
|
+
if (value === "read" || value === "edit" || value === "write" || value === "delete") {
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
function optionalQueryValue(value) {
|
|
546
|
+
const normalized = value?.trim();
|
|
547
|
+
return normalized ? normalized : void 0;
|
|
548
|
+
}
|
|
549
|
+
function handleGetFileActivity(c, defaults = {}) {
|
|
550
|
+
const limitValue = Number(c.req.query("limit"));
|
|
551
|
+
const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
|
|
552
|
+
return c.json({
|
|
553
|
+
activity: listFileActivity({
|
|
554
|
+
agent: optionalQueryValue(c.req.query("agent")),
|
|
555
|
+
sessionId: optionalQueryValue(c.req.query("sessionId")),
|
|
556
|
+
projectKey: optionalQueryValue(c.req.query("projectKey")),
|
|
557
|
+
project: optionalQueryValue(c.req.query("project")),
|
|
558
|
+
cwd: optionalQueryValue(c.req.query("cwd")),
|
|
559
|
+
path: optionalQueryValue(c.req.query("path")),
|
|
560
|
+
kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
|
|
561
|
+
from: parseDateParam(c.req.query("from"), defaults.from),
|
|
562
|
+
to: parseDateParam(c.req.query("to"), defaults.to),
|
|
563
|
+
limit
|
|
564
|
+
})
|
|
565
|
+
});
|
|
566
|
+
}
|
|
322
567
|
async function handleGetSessionData(c, scanSource) {
|
|
323
568
|
const startedAt = performance.now();
|
|
324
569
|
const scanResult = scanSource.getSnapshot();
|
|
@@ -339,6 +584,7 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
339
584
|
const smartTags = classifySessionTags(data);
|
|
340
585
|
const tagDuration = performance.now() - tagStartedAt;
|
|
341
586
|
const head = scanResult.byAgent[agentName]?.find((item) => item.id === sessionId);
|
|
587
|
+
const projectIdentity = data.project_identity ?? head?.project_identity ?? computeIdentity(data.directory, realFs);
|
|
342
588
|
appLogger.info("api.session_data", {
|
|
343
589
|
agent: agentName,
|
|
344
590
|
session_id: sessionId,
|
|
@@ -349,9 +595,15 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
349
595
|
});
|
|
350
596
|
return c.json({
|
|
351
597
|
...data,
|
|
352
|
-
project_identity:
|
|
598
|
+
project_identity: projectIdentity,
|
|
353
599
|
smart_tags: smartTags,
|
|
354
|
-
smart_tags_source_updated_at: getSmartTagSourceTimestamp(data)
|
|
600
|
+
smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
|
|
601
|
+
file_activity: extractSessionFileActivity(
|
|
602
|
+
agentName,
|
|
603
|
+
sessionId,
|
|
604
|
+
projectIdentity.key,
|
|
605
|
+
data.messages
|
|
606
|
+
)
|
|
355
607
|
});
|
|
356
608
|
} catch (err) {
|
|
357
609
|
const message = err instanceof Error ? err.message : "Failed to load session";
|
|
@@ -473,10 +725,19 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
473
725
|
c.req.query("from"),
|
|
474
726
|
c.req.query("to")
|
|
475
727
|
);
|
|
476
|
-
const
|
|
728
|
+
const scope = {
|
|
729
|
+
agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
|
|
730
|
+
projectKind: optionalQueryValue(c.req.query("projectKind")),
|
|
731
|
+
projectKey: optionalQueryValue(c.req.query("projectKey"))
|
|
732
|
+
};
|
|
733
|
+
const scopedSessions = filterSessionsByDashboardScope(scanResult.sessions, scope);
|
|
734
|
+
const windowed = filterSessionsByActivityWindow(scopedSessions, from, to);
|
|
735
|
+
const scopedByAgent = Object.fromEntries(
|
|
736
|
+
Object.entries(scanResult.byAgent).filter(([name]) => !scope.agent || name.toLowerCase() === scope.agent).map(([name, sessions]) => [name, filterSessionsByDashboardScope(sessions, scope)])
|
|
737
|
+
);
|
|
477
738
|
const agentInfo = getAgentInfoMap(
|
|
478
739
|
Object.fromEntries(
|
|
479
|
-
Object.entries(
|
|
740
|
+
Object.entries(scopedByAgent).map(([name, sessions]) => [
|
|
480
741
|
name,
|
|
481
742
|
filterSessionsByActivityWindow(sessions, from, to).length
|
|
482
743
|
])
|
|
@@ -496,7 +757,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
496
757
|
const activity = getSessionActivityTime(session);
|
|
497
758
|
if (activity > latestActivity) latestActivity = activity;
|
|
498
759
|
}
|
|
499
|
-
const perAgent = Object.entries(
|
|
760
|
+
const perAgent = Object.entries(scopedByAgent).map(([name, sessions]) => {
|
|
500
761
|
const info = agentInfoMap.get(name);
|
|
501
762
|
const agentWindowed = filterSessionsByActivityWindow(sessions, from, to);
|
|
502
763
|
let messages = 0;
|
|
@@ -558,7 +819,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
558
819
|
const dailyTokenActivity = [...dailyTokenMap.values()];
|
|
559
820
|
const modelDistribution = [...modelAgg.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
|
|
560
821
|
const recentSessions = [...windowed].sort((a, b) => getSessionActivityTime(b) - getSessionActivityTime(a)).slice(0, 10).map((session) => {
|
|
561
|
-
const agentKey = session
|
|
822
|
+
const agentKey = getSessionAgentName(session);
|
|
562
823
|
return { ...session, agentName: agentKey };
|
|
563
824
|
});
|
|
564
825
|
const data = {
|
|
@@ -575,6 +836,13 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
575
836
|
dailyTokenActivity,
|
|
576
837
|
modelDistribution,
|
|
577
838
|
recentSessions,
|
|
839
|
+
recentFileActivities: listFileActivity({
|
|
840
|
+
agent: scope.agent,
|
|
841
|
+
projectKey: scope.projectKey,
|
|
842
|
+
from,
|
|
843
|
+
to,
|
|
844
|
+
limit: 12
|
|
845
|
+
}),
|
|
578
846
|
window: { from, to, days }
|
|
579
847
|
};
|
|
580
848
|
return c.json(data);
|
|
@@ -632,6 +900,7 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
632
900
|
api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
|
|
633
901
|
api.get("/sessions", (c) => handleGetSessions(c, scanSource, listDefaults));
|
|
634
902
|
api.get("/search", (c) => handleSearchSessions(c, scanSource, listDefaults));
|
|
903
|
+
api.get("/file-activity", (c) => handleGetFileActivity(c, listDefaults));
|
|
635
904
|
api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
|
|
636
905
|
api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
|
|
637
906
|
api.get("/bookmarks", (c) => handleGetBookmarks(c));
|
|
@@ -659,10 +928,10 @@ function findWebDistPath() {
|
|
|
659
928
|
return null;
|
|
660
929
|
}
|
|
661
930
|
function waitForListening(server) {
|
|
662
|
-
return new Promise((
|
|
931
|
+
return new Promise((resolve4, reject) => {
|
|
663
932
|
const handleListening = () => {
|
|
664
933
|
server.off("error", handleError);
|
|
665
|
-
|
|
934
|
+
resolve4();
|
|
666
935
|
};
|
|
667
936
|
const handleError = (error) => {
|
|
668
937
|
server.off("listening", handleListening);
|
|
@@ -744,9 +1013,17 @@ async function createServer(port, store, options = {}) {
|
|
|
744
1013
|
}
|
|
745
1014
|
|
|
746
1015
|
// src/live-scan.ts
|
|
747
|
-
import { existsSync as existsSync3 } from "fs";
|
|
748
|
-
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
749
|
-
import
|
|
1016
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
|
|
1017
|
+
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
|
|
1018
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1019
|
+
import { Worker } from "worker_threads";
|
|
1020
|
+
var REFRESH_DEBOUNCE_MS = 200;
|
|
1021
|
+
var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
1022
|
+
var PENDING_REFRESH_DELAY_MS = 100;
|
|
1023
|
+
var WRITE_STABILITY_THRESHOLD_MS = 250;
|
|
1024
|
+
var WRITE_STABILITY_POLL_MS = 100;
|
|
1025
|
+
var NEW_SESSION_EVENT_WINDOW_MS = 250;
|
|
1026
|
+
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
750
1027
|
function sortSessions(sessions) {
|
|
751
1028
|
return [...sessions].sort(
|
|
752
1029
|
(a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
|
|
@@ -808,11 +1085,14 @@ function buildUpdateEvent(agentName, previousSessions, nextSessions) {
|
|
|
808
1085
|
timestamp: Date.now()
|
|
809
1086
|
};
|
|
810
1087
|
}
|
|
1088
|
+
function toAbsolutePath(path) {
|
|
1089
|
+
return isAbsolute(path) ? path : resolve2(path);
|
|
1090
|
+
}
|
|
811
1091
|
function closestWatchablePath(targetPath) {
|
|
812
1092
|
if (!isAbsolute(targetPath) && !existsSync3(targetPath)) {
|
|
813
1093
|
return null;
|
|
814
1094
|
}
|
|
815
|
-
let current = targetPath;
|
|
1095
|
+
let current = toAbsolutePath(targetPath);
|
|
816
1096
|
while (!existsSync3(current)) {
|
|
817
1097
|
const parent = dirname2(current);
|
|
818
1098
|
if (parent === current) {
|
|
@@ -822,31 +1102,85 @@ function closestWatchablePath(targetPath) {
|
|
|
822
1102
|
}
|
|
823
1103
|
return current;
|
|
824
1104
|
}
|
|
1105
|
+
function getWatchRoot(path) {
|
|
1106
|
+
const stat = statSync2(path);
|
|
1107
|
+
return stat.isDirectory() ? path : dirname2(path);
|
|
1108
|
+
}
|
|
1109
|
+
function isRecursiveWatchSupported(platform = process.platform, nodeVersion = process.versions.node) {
|
|
1110
|
+
if (platform === "darwin" || platform === "win32") {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
if (platform !== "linux" && platform !== "aix" && platform !== "ibmi") {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
const [major = 0, minor = 0] = nodeVersion.split(".").map((part) => Number(part));
|
|
1117
|
+
return major > 19 || major === 19 && minor >= 1;
|
|
1118
|
+
}
|
|
1119
|
+
function isRecursiveWatchUnavailable(error) {
|
|
1120
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
|
|
1121
|
+
}
|
|
1122
|
+
function isSameOrChildPath(parentPath, childPath) {
|
|
1123
|
+
const path = relative(parentPath, childPath);
|
|
1124
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
1125
|
+
}
|
|
1126
|
+
function isRelatedPath(changedPath, targetPath) {
|
|
1127
|
+
return isSameOrChildPath(targetPath, changedPath) || isSameOrChildPath(changedPath, targetPath);
|
|
1128
|
+
}
|
|
1129
|
+
function mergeEvents(previous, next) {
|
|
1130
|
+
return {
|
|
1131
|
+
type: "sessions-updated",
|
|
1132
|
+
changedAgents: Array.from(/* @__PURE__ */ new Set([...previous.changedAgents, ...next.changedAgents])),
|
|
1133
|
+
newSessions: previous.newSessions + next.newSessions,
|
|
1134
|
+
updatedSessions: previous.updatedSessions + next.updatedSessions,
|
|
1135
|
+
removedSessions: previous.removedSessions + next.removedSessions,
|
|
1136
|
+
totalSessions: next.totalSessions,
|
|
1137
|
+
timestamp: next.timestamp
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function mergeScopes(target, scopes) {
|
|
1141
|
+
for (const scope of scopes) {
|
|
1142
|
+
if (!target.some(
|
|
1143
|
+
(item) => item.agentName === scope.agentName && item.targetPath === scope.targetPath
|
|
1144
|
+
)) {
|
|
1145
|
+
target.push(scope);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function resolveWatchEventPath(watchPath, filename) {
|
|
1150
|
+
const filenameText = filename?.toString();
|
|
1151
|
+
if (!filenameText) {
|
|
1152
|
+
return watchPath;
|
|
1153
|
+
}
|
|
1154
|
+
return isAbsolute(filenameText) ? filenameText : join2(watchPath, filenameText);
|
|
1155
|
+
}
|
|
825
1156
|
function resolveAgentWatchTargets(agentName) {
|
|
826
1157
|
const roots = resolveProviderRoots();
|
|
827
1158
|
const cursorDataPath = getCursorDataPath();
|
|
828
1159
|
switch (agentName) {
|
|
829
1160
|
case "claudecode":
|
|
830
1161
|
return [
|
|
831
|
-
{ path: join2(roots.claudeRoot, "projects")
|
|
832
|
-
{ path: "data/claudecode"
|
|
1162
|
+
{ root: roots.claudeRoot, path: join2(roots.claudeRoot, "projects") },
|
|
1163
|
+
{ path: "data/claudecode" }
|
|
833
1164
|
];
|
|
834
1165
|
case "codex":
|
|
835
|
-
return [{ path: join2(roots.codexRoot, "sessions")
|
|
1166
|
+
return [{ root: roots.codexRoot, path: join2(roots.codexRoot, "sessions") }];
|
|
836
1167
|
case "cursor":
|
|
837
1168
|
return cursorDataPath ? [
|
|
838
|
-
{
|
|
839
|
-
|
|
1169
|
+
{
|
|
1170
|
+
root: cursorDataPath,
|
|
1171
|
+
path: join2(cursorDataPath, "globalStorage", "state.vscdb")
|
|
1172
|
+
},
|
|
1173
|
+
{ root: cursorDataPath, path: join2(cursorDataPath, "workspaceStorage") }
|
|
840
1174
|
] : [];
|
|
841
1175
|
case "kimi":
|
|
842
1176
|
return [
|
|
843
|
-
{ path: join2(roots.kimiRoot, "sessions")
|
|
844
|
-
{ path: "data/kimi"
|
|
1177
|
+
{ root: roots.kimiRoot, path: join2(roots.kimiRoot, "sessions") },
|
|
1178
|
+
{ path: "data/kimi" }
|
|
845
1179
|
];
|
|
846
1180
|
case "opencode":
|
|
847
1181
|
return [
|
|
848
|
-
{ path: join2(roots.opencodeRoot, "opencode.db") },
|
|
849
|
-
{ path: "data/opencode/opencode.db" }
|
|
1182
|
+
{ root: roots.opencodeRoot, path: join2(roots.opencodeRoot, "opencode.db") },
|
|
1183
|
+
{ root: "data/opencode", path: "data/opencode/opencode.db" }
|
|
850
1184
|
];
|
|
851
1185
|
default:
|
|
852
1186
|
return [];
|
|
@@ -869,7 +1203,14 @@ var LiveScanStore = class {
|
|
|
869
1203
|
refreshTimestamps = /* @__PURE__ */ new Map();
|
|
870
1204
|
refreshInFlight = /* @__PURE__ */ new Set();
|
|
871
1205
|
pendingRefreshes = /* @__PURE__ */ new Set();
|
|
1206
|
+
pendingRefreshPathCounts = /* @__PURE__ */ new Map();
|
|
872
1207
|
watchers = [];
|
|
1208
|
+
fallbackWatchScopes = /* @__PURE__ */ new Map();
|
|
1209
|
+
stablePaths = /* @__PURE__ */ new Map();
|
|
1210
|
+
pendingEvent = null;
|
|
1211
|
+
pendingEventTimer = null;
|
|
1212
|
+
initialSearchIndexTimer = null;
|
|
1213
|
+
searchIndexWorker = null;
|
|
873
1214
|
async initialize() {
|
|
874
1215
|
const startedAt = performance.now();
|
|
875
1216
|
appLogger.info("scan.initial.start", {
|
|
@@ -897,6 +1238,10 @@ var LiveScanStore = class {
|
|
|
897
1238
|
});
|
|
898
1239
|
if (this.watchEnabled) {
|
|
899
1240
|
this.startWatching();
|
|
1241
|
+
this.initialSearchIndexTimer = setTimeout(() => {
|
|
1242
|
+
this.initialSearchIndexTimer = null;
|
|
1243
|
+
this.startSearchIndexWorker("scan.initial.background");
|
|
1244
|
+
}, 1e3);
|
|
900
1245
|
}
|
|
901
1246
|
}
|
|
902
1247
|
getSnapshot() {
|
|
@@ -917,20 +1262,108 @@ var LiveScanStore = class {
|
|
|
917
1262
|
clearTimeout(timer);
|
|
918
1263
|
}
|
|
919
1264
|
this.refreshTimers.clear();
|
|
1265
|
+
this.pendingRefreshPathCounts.clear();
|
|
1266
|
+
for (const state of this.stablePaths.values()) {
|
|
1267
|
+
if (state.timer) {
|
|
1268
|
+
clearTimeout(state.timer);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
this.stablePaths.clear();
|
|
1272
|
+
if (this.pendingEventTimer) {
|
|
1273
|
+
clearTimeout(this.pendingEventTimer);
|
|
1274
|
+
this.pendingEventTimer = null;
|
|
1275
|
+
}
|
|
1276
|
+
if (this.initialSearchIndexTimer) {
|
|
1277
|
+
clearTimeout(this.initialSearchIndexTimer);
|
|
1278
|
+
this.initialSearchIndexTimer = null;
|
|
1279
|
+
}
|
|
1280
|
+
if (this.searchIndexWorker) {
|
|
1281
|
+
await this.searchIndexWorker.terminate();
|
|
1282
|
+
this.searchIndexWorker = null;
|
|
1283
|
+
}
|
|
1284
|
+
this.pendingEvent = null;
|
|
920
1285
|
await Promise.all(this.watchers.map((watcher) => watcher.close()));
|
|
921
1286
|
this.watchers = [];
|
|
1287
|
+
this.fallbackWatchScopes.clear();
|
|
922
1288
|
}
|
|
923
1289
|
emit(event) {
|
|
1290
|
+
if (this.pendingEvent || event.newSessions > 0) {
|
|
1291
|
+
this.queueEvent(event);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.emitNow(event);
|
|
1295
|
+
}
|
|
1296
|
+
emitNow(event) {
|
|
924
1297
|
for (const listener of this.listeners) {
|
|
925
1298
|
listener(event);
|
|
926
1299
|
}
|
|
927
1300
|
}
|
|
1301
|
+
queueEvent(event) {
|
|
1302
|
+
this.pendingEvent = this.pendingEvent ? mergeEvents(this.pendingEvent, event) : event;
|
|
1303
|
+
if (this.pendingEventTimer) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
this.pendingEventTimer = setTimeout(() => {
|
|
1307
|
+
const pending = this.pendingEvent;
|
|
1308
|
+
this.pendingEvent = null;
|
|
1309
|
+
this.pendingEventTimer = null;
|
|
1310
|
+
if (pending) {
|
|
1311
|
+
this.emitNow(pending);
|
|
1312
|
+
}
|
|
1313
|
+
}, NEW_SESSION_EVENT_WINDOW_MS);
|
|
1314
|
+
}
|
|
928
1315
|
rebuildSessions() {
|
|
929
1316
|
this.sessions = sortSessions(Object.values(this.byAgent).flat());
|
|
930
1317
|
}
|
|
931
1318
|
hasStartupWindow() {
|
|
932
1319
|
return this.startupScanOptions.from != null || this.startupScanOptions.to != null;
|
|
933
1320
|
}
|
|
1321
|
+
getSearchIndexWorkerUrl() {
|
|
1322
|
+
const workerUrl = new URL("./search-index-worker.js", import.meta.url);
|
|
1323
|
+
if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
return workerUrl;
|
|
1327
|
+
}
|
|
1328
|
+
startSearchIndexWorker(context) {
|
|
1329
|
+
if (this.searchIndexWorker) return;
|
|
1330
|
+
const workerUrl = this.getSearchIndexWorkerUrl();
|
|
1331
|
+
if (!workerUrl) {
|
|
1332
|
+
appLogger.warn("search_index.worker_missing", { context });
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const worker = new Worker(workerUrl, {
|
|
1336
|
+
workerData: {
|
|
1337
|
+
context,
|
|
1338
|
+
agentNames: this.agents.map((agent) => agent.name),
|
|
1339
|
+
sessionsByAgent: this.byAgent,
|
|
1340
|
+
metaByAgent: Object.fromEntries(
|
|
1341
|
+
this.agents.map((agent) => [agent.name, buildAgentCacheMeta(agent)])
|
|
1342
|
+
)
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
worker.unref();
|
|
1346
|
+
this.searchIndexWorker = worker;
|
|
1347
|
+
worker.on("message", (message) => {
|
|
1348
|
+
if (message.type === "sync-result") {
|
|
1349
|
+
logSearchIndexSync(message.context, message.result);
|
|
1350
|
+
} else if (message.type === "done") {
|
|
1351
|
+
appLogger.info(`${message.context}.done`, {
|
|
1352
|
+
duration_ms: Math.round(message.durationMs),
|
|
1353
|
+
sessions: message.sessions
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
worker.on("error", (error) => {
|
|
1358
|
+
appLogger.error("search_index.worker_error", { context, error });
|
|
1359
|
+
});
|
|
1360
|
+
worker.on("exit", (code) => {
|
|
1361
|
+
this.searchIndexWorker = null;
|
|
1362
|
+
if (code !== 0) {
|
|
1363
|
+
appLogger.warn("search_index.worker_exit", { context, code });
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
934
1367
|
applyScanResult(result) {
|
|
935
1368
|
const knownAgents = createRegisteredAgents();
|
|
936
1369
|
const agentMap = /* @__PURE__ */ new Map();
|
|
@@ -966,50 +1399,195 @@ var LiveScanStore = class {
|
|
|
966
1399
|
return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
|
|
967
1400
|
}
|
|
968
1401
|
startWatching() {
|
|
1402
|
+
const scopesByRoot = /* @__PURE__ */ new Map();
|
|
969
1403
|
for (const agent of this.agents) {
|
|
970
|
-
const
|
|
971
|
-
const watchTargets = rawTargets.map((target) => {
|
|
972
|
-
const watchPath = closestWatchablePath(target.path);
|
|
973
|
-
return watchPath ? { ...target, path: watchPath } : null;
|
|
974
|
-
}).filter((target) => target !== null).filter(
|
|
975
|
-
(target, index, items) => items.findIndex((item) => item.path === target.path && item.depth === target.depth) === index
|
|
976
|
-
);
|
|
1404
|
+
const watchTargets = resolveAgentWatchTargets(agent.name);
|
|
977
1405
|
if (watchTargets.length === 0) {
|
|
978
1406
|
appLogger.debug("watch.skip", { agent: agent.name });
|
|
979
1407
|
continue;
|
|
980
1408
|
}
|
|
1409
|
+
for (const target of watchTargets) {
|
|
1410
|
+
const watchRootPath = closestWatchablePath(target.root ?? target.path);
|
|
1411
|
+
if (!watchRootPath) continue;
|
|
1412
|
+
let rootPath;
|
|
1413
|
+
try {
|
|
1414
|
+
rootPath = getWatchRoot(watchRootPath);
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
this.reportWatchError("watch.resolve.error", { path: watchRootPath, error });
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const targetPath = toAbsolutePath(target.path);
|
|
1420
|
+
const scopes = scopesByRoot.get(rootPath) ?? [];
|
|
1421
|
+
if (!scopes.some((scope) => scope.agentName === agent.name && scope.targetPath === targetPath)) {
|
|
1422
|
+
scopes.push({ agentName: agent.name, targetPath });
|
|
1423
|
+
}
|
|
1424
|
+
scopesByRoot.set(rootPath, scopes);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
for (const [rootPath, scopes] of scopesByRoot.entries()) {
|
|
1428
|
+
const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
|
|
981
1429
|
appLogger.info("watch.start", {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1430
|
+
root: rootPath,
|
|
1431
|
+
agents,
|
|
1432
|
+
targets: scopes.map((scope) => ({
|
|
1433
|
+
agent: scope.agentName,
|
|
1434
|
+
path: scope.targetPath
|
|
986
1435
|
}))
|
|
987
1436
|
});
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
{
|
|
991
|
-
|
|
992
|
-
awaitWriteFinish: {
|
|
993
|
-
stabilityThreshold: 250,
|
|
994
|
-
pollInterval: 100
|
|
995
|
-
},
|
|
996
|
-
depth: watchTargets.reduce(
|
|
997
|
-
(maxDepth, target) => Math.max(maxDepth, target.depth ?? 0),
|
|
998
|
-
0
|
|
999
|
-
)
|
|
1437
|
+
if (isRecursiveWatchSupported()) {
|
|
1438
|
+
const started = this.watchDirectory(rootPath, scopes, true);
|
|
1439
|
+
if (started) {
|
|
1440
|
+
continue;
|
|
1000
1441
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1442
|
+
}
|
|
1443
|
+
this.watchDirectoryTree(rootPath, scopes);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
watchDirectory(path, scopes, recursive) {
|
|
1447
|
+
try {
|
|
1448
|
+
const watcher = watch(path, { recursive }, (eventType, filename) => {
|
|
1449
|
+
queueMicrotask(() => {
|
|
1450
|
+
try {
|
|
1451
|
+
const activeScopes = recursive ? scopes : this.fallbackWatchScopes.get(path) ?? scopes;
|
|
1452
|
+
this.handleWatchEvent(path, activeScopes, eventType, filename);
|
|
1453
|
+
if (!recursive) {
|
|
1454
|
+
this.watchNewDirectories(path, filename, activeScopes);
|
|
1455
|
+
}
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
this.reportWatchError("watch.event.error", { path, recursive, error });
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1004
1460
|
});
|
|
1005
1461
|
watcher.on("error", (error) => {
|
|
1006
|
-
|
|
1007
|
-
console.error(`[${agent.name}] File watcher failed:`, error);
|
|
1462
|
+
this.reportWatchError("watch.error", { path, recursive, error });
|
|
1008
1463
|
});
|
|
1009
1464
|
this.watchers.push(watcher);
|
|
1465
|
+
return true;
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
if (recursive && isRecursiveWatchUnavailable(error)) {
|
|
1468
|
+
appLogger.warn("watch.recursive_unavailable", { path, error });
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
this.reportWatchError("watch.start.error", { path, recursive, error });
|
|
1472
|
+
return false;
|
|
1010
1473
|
}
|
|
1011
1474
|
}
|
|
1012
|
-
|
|
1475
|
+
watchDirectoryTree(rootPath, scopes) {
|
|
1476
|
+
const pending = [rootPath];
|
|
1477
|
+
while (pending.length > 0) {
|
|
1478
|
+
const dirPath = pending.pop();
|
|
1479
|
+
this.watchFallbackDirectory(dirPath, scopes);
|
|
1480
|
+
try {
|
|
1481
|
+
for (const entry of readdirSync2(dirPath, { withFileTypes: true })) {
|
|
1482
|
+
if (entry.isDirectory()) {
|
|
1483
|
+
pending.push(join2(dirPath, entry.name));
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
this.reportWatchError("watch.scan.error", { path: dirPath, error });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
watchFallbackDirectory(path, scopes) {
|
|
1492
|
+
const existingScopes = this.fallbackWatchScopes.get(path);
|
|
1493
|
+
if (existingScopes) {
|
|
1494
|
+
mergeScopes(existingScopes, scopes);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const storedScopes = [...scopes];
|
|
1498
|
+
this.fallbackWatchScopes.set(path, storedScopes);
|
|
1499
|
+
if (!this.watchDirectory(path, storedScopes, false)) {
|
|
1500
|
+
this.fallbackWatchScopes.delete(path);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
watchNewDirectories(watchPath, filename, scopes) {
|
|
1504
|
+
const path = resolveWatchEventPath(watchPath, filename);
|
|
1505
|
+
try {
|
|
1506
|
+
if (statSync2(path).isDirectory()) {
|
|
1507
|
+
this.watchDirectoryTree(path, scopes);
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
handleWatchEvent(watchPath, scopes, eventType, filename) {
|
|
1513
|
+
const changedPath = resolveWatchEventPath(watchPath, filename);
|
|
1514
|
+
const agentNames = new Set(
|
|
1515
|
+
scopes.filter((scope) => isRelatedPath(changedPath, scope.targetPath)).map((scope) => scope.agentName)
|
|
1516
|
+
);
|
|
1517
|
+
if (agentNames.size === 0) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
appLogger.debug("watch.event", {
|
|
1521
|
+
event: eventType,
|
|
1522
|
+
path: changedPath,
|
|
1523
|
+
agents: Array.from(agentNames)
|
|
1524
|
+
});
|
|
1525
|
+
this.waitForStablePath(changedPath, agentNames);
|
|
1526
|
+
}
|
|
1527
|
+
waitForStablePath(path, agentNames) {
|
|
1528
|
+
const existing = this.stablePaths.get(path);
|
|
1529
|
+
if (existing) {
|
|
1530
|
+
for (const agentName of agentNames) {
|
|
1531
|
+
existing.agentNames.add(agentName);
|
|
1532
|
+
}
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const state = {
|
|
1536
|
+
path,
|
|
1537
|
+
agentNames: new Set(agentNames),
|
|
1538
|
+
lastMtimeMs: null,
|
|
1539
|
+
lastSize: null,
|
|
1540
|
+
stableSince: Date.now(),
|
|
1541
|
+
timer: null
|
|
1542
|
+
};
|
|
1543
|
+
this.stablePaths.set(path, state);
|
|
1544
|
+
this.pollStablePath(path);
|
|
1545
|
+
}
|
|
1546
|
+
pollStablePath(path) {
|
|
1547
|
+
const state = this.stablePaths.get(path);
|
|
1548
|
+
if (!state) {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
let size;
|
|
1552
|
+
let mtimeMs;
|
|
1553
|
+
try {
|
|
1554
|
+
const stat = statSync2(path);
|
|
1555
|
+
size = stat.size;
|
|
1556
|
+
mtimeMs = stat.mtimeMs;
|
|
1557
|
+
} catch {
|
|
1558
|
+
this.stablePaths.delete(path);
|
|
1559
|
+
this.scheduleRefreshForAgents(state.agentNames);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const now = Date.now();
|
|
1563
|
+
const unchanged = state.lastSize === size && state.lastMtimeMs === mtimeMs;
|
|
1564
|
+
if (!unchanged) {
|
|
1565
|
+
state.lastSize = size;
|
|
1566
|
+
state.lastMtimeMs = mtimeMs;
|
|
1567
|
+
state.stableSince = now;
|
|
1568
|
+
}
|
|
1569
|
+
if (unchanged && now - state.stableSince >= WRITE_STABILITY_THRESHOLD_MS) {
|
|
1570
|
+
this.stablePaths.delete(path);
|
|
1571
|
+
this.scheduleRefreshForAgents(state.agentNames);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
state.timer = setTimeout(() => this.pollStablePath(path), WRITE_STABILITY_POLL_MS);
|
|
1575
|
+
}
|
|
1576
|
+
scheduleRefreshForAgents(agentNames) {
|
|
1577
|
+
for (const agentName of agentNames) {
|
|
1578
|
+
this.pendingRefreshPathCounts.set(
|
|
1579
|
+
agentName,
|
|
1580
|
+
(this.pendingRefreshPathCounts.get(agentName) ?? 0) + 1
|
|
1581
|
+
);
|
|
1582
|
+
const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
1583
|
+
this.scheduleRefresh(agentName, delayMs);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
reportWatchError(event, data) {
|
|
1587
|
+
appLogger.error(event, data);
|
|
1588
|
+
console.error("[watch] File watcher failed:", data.error);
|
|
1589
|
+
}
|
|
1590
|
+
scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
|
|
1013
1591
|
appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: delayMs });
|
|
1014
1592
|
const existing = this.refreshTimers.get(agentName);
|
|
1015
1593
|
if (existing) {
|
|
@@ -1030,15 +1608,20 @@ var LiveScanStore = class {
|
|
|
1030
1608
|
this.refreshInFlight.add(agentName);
|
|
1031
1609
|
try {
|
|
1032
1610
|
await this.runRefresh(agentName);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
1613
|
+
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
1033
1614
|
} finally {
|
|
1034
1615
|
this.refreshInFlight.delete(agentName);
|
|
1035
1616
|
if (this.pendingRefreshes.delete(agentName)) {
|
|
1036
|
-
this.scheduleRefresh(agentName,
|
|
1617
|
+
this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
|
|
1037
1618
|
}
|
|
1038
1619
|
}
|
|
1039
1620
|
}
|
|
1040
1621
|
async runRefresh(agentName) {
|
|
1041
1622
|
const startedAt = performance.now();
|
|
1623
|
+
const pendingPathCount = this.pendingRefreshPathCounts.get(agentName) ?? 0;
|
|
1624
|
+
this.pendingRefreshPathCounts.delete(agentName);
|
|
1042
1625
|
const agent = this.agents.find((item) => item.name === agentName);
|
|
1043
1626
|
if (!agent) {
|
|
1044
1627
|
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
@@ -1072,7 +1655,18 @@ var LiveScanStore = class {
|
|
|
1072
1655
|
if (!this.hasStartupWindow()) {
|
|
1073
1656
|
saveCachedSessions(agentName, nextSessions, buildAgentCacheMeta(agent));
|
|
1074
1657
|
}
|
|
1075
|
-
|
|
1658
|
+
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
1659
|
+
const syncResult = searchIndexOptions ? syncSessionSearchIndex(
|
|
1660
|
+
agentName,
|
|
1661
|
+
nextSessions,
|
|
1662
|
+
(sessionId) => agent.getSessionData(sessionId),
|
|
1663
|
+
searchIndexOptions
|
|
1664
|
+
) : syncSessionSearchIndex(
|
|
1665
|
+
agentName,
|
|
1666
|
+
nextSessions,
|
|
1667
|
+
(sessionId) => agent.getSessionData(sessionId)
|
|
1668
|
+
);
|
|
1669
|
+
logSearchIndexSync("scan.refresh", syncResult, { pending_paths: pendingPathCount });
|
|
1076
1670
|
const event = buildUpdateEvent(agentName, previousSessions, nextSessions);
|
|
1077
1671
|
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
1078
1672
|
this.rebuildSessions();
|
|
@@ -1086,7 +1680,10 @@ var LiveScanStore = class {
|
|
|
1086
1680
|
sessions: nextSessions.length,
|
|
1087
1681
|
new_sessions: event?.newSessions ?? 0,
|
|
1088
1682
|
updated_sessions: event?.updatedSessions ?? 0,
|
|
1089
|
-
removed_sessions: event?.removedSessions ?? 0
|
|
1683
|
+
removed_sessions: event?.removedSessions ?? 0,
|
|
1684
|
+
pending_paths: pendingPathCount,
|
|
1685
|
+
search_index_mode: syncResult?.mode,
|
|
1686
|
+
search_index_rebuild_duration_ms: syncResult?.rebuildDurationMs == null ? void 0 : Math.round(syncResult.rebuildDurationMs)
|
|
1090
1687
|
});
|
|
1091
1688
|
}
|
|
1092
1689
|
};
|
|
@@ -1096,10 +1693,10 @@ import { consola } from "consola";
|
|
|
1096
1693
|
|
|
1097
1694
|
// src/version.ts
|
|
1098
1695
|
import { readFileSync } from "fs";
|
|
1099
|
-
import { resolve as
|
|
1100
|
-
import { fileURLToPath as
|
|
1101
|
-
var __dirname = dirname3(
|
|
1102
|
-
var pkg = JSON.parse(readFileSync(
|
|
1696
|
+
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
1697
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1698
|
+
var __dirname = dirname3(fileURLToPath3(import.meta.url));
|
|
1699
|
+
var pkg = JSON.parse(readFileSync(resolve3(__dirname, "../package.json"), "utf-8"));
|
|
1103
1700
|
var VERSION = pkg.version;
|
|
1104
1701
|
|
|
1105
1702
|
// src/output.ts
|
|
@@ -1243,7 +1840,7 @@ var main = defineCommand({
|
|
|
1243
1840
|
log_path: appLogger.getLogPath()
|
|
1244
1841
|
});
|
|
1245
1842
|
if (clearCache) {
|
|
1246
|
-
const { clearCache: clear } = await import("./dist-
|
|
1843
|
+
const { clearCache: clear } = await import("./dist-NT4CH6KD.js");
|
|
1247
1844
|
clear();
|
|
1248
1845
|
appLogger.info("cache.clear");
|
|
1249
1846
|
console.log("Cache cleared.");
|