codesesh 0.4.1 → 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/dist/index.js CHANGED
@@ -1,21 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BookmarkStorageUnavailableError,
4
+ classifySessionTags,
5
+ computeIdentity,
4
6
  createRegisteredAgents,
5
7
  deleteBookmark,
8
+ extractSessionFileActivity,
6
9
  filterSessions,
7
10
  getAgentInfoMap,
8
11
  getCursorDataPath,
12
+ getSmartTagSourceTimestamp,
9
13
  importBookmarks,
10
14
  listBookmarks,
15
+ listCachedProjectGroups,
16
+ listFileActivity,
17
+ parseSearchQuery,
11
18
  perf,
19
+ realFs,
20
+ refreshPricingCache,
12
21
  resolveProviderRoots,
13
22
  saveCachedSessions,
14
23
  scanSessions,
24
+ searchFileActivitySessions,
15
25
  searchSessions,
16
26
  syncSessionSearchIndex,
17
27
  upsertBookmark
18
- } from "./chunk-BGQXQTWM.js";
28
+ } from "./chunk-SQYHWMQV.js";
19
29
 
20
30
  // src/index.ts
21
31
  import { defineCommand, runMain } from "citty";
@@ -24,14 +34,162 @@ import { defineCommand, runMain } from "citty";
24
34
  import { Hono as Hono2 } from "hono";
25
35
  import { serve } from "@hono/node-server";
26
36
  import { serveStatic } from "@hono/node-server/serve-static";
27
- import { logger } from "hono/logger";
28
- import { existsSync } from "fs";
37
+ import { existsSync as existsSync2 } from "fs";
29
38
  import { resolve, dirname } from "path";
30
39
  import { fileURLToPath } from "url";
31
40
 
32
41
  // src/api/routes.ts
33
42
  import { Hono } from "hono";
34
43
 
44
+ // src/logging.ts
45
+ import {
46
+ appendFileSync,
47
+ existsSync,
48
+ mkdirSync,
49
+ readdirSync,
50
+ renameSync,
51
+ statSync,
52
+ unlinkSync
53
+ } from "fs";
54
+ import { homedir } from "os";
55
+ import { join } from "path";
56
+ var LEVEL_WEIGHT = {
57
+ debug: 10,
58
+ info: 20,
59
+ warn: 30,
60
+ error: 40
61
+ };
62
+ function parseLevel(value) {
63
+ if (value === "debug" || value === "info" || value === "warn" || value === "error") {
64
+ return value;
65
+ }
66
+ return "info";
67
+ }
68
+ function parsePositiveInt(value, fallback) {
69
+ const parsed = Number(value);
70
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
71
+ }
72
+ function getDefaultLogDir() {
73
+ const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
74
+ return join(base, "codesesh", "logs");
75
+ }
76
+ function toLogValue(value, depth = 0) {
77
+ if (value == null || typeof value === "string" || typeof value === "number") return value;
78
+ if (typeof value === "boolean") return value;
79
+ if (typeof value === "bigint") return value.toString();
80
+ if (value instanceof Error) {
81
+ return {
82
+ name: value.name,
83
+ message: value.message,
84
+ stack: value.stack
85
+ };
86
+ }
87
+ if (depth >= 4) return "[truncated]";
88
+ if (Array.isArray(value)) return value.slice(0, 50).map((item) => toLogValue(item, depth + 1));
89
+ if (typeof value === "object") {
90
+ return Object.fromEntries(
91
+ Object.entries(value).map(([key, item]) => [
92
+ key,
93
+ toLogValue(item, depth + 1)
94
+ ])
95
+ );
96
+ }
97
+ return String(value);
98
+ }
99
+ function timestampForFile(date = /* @__PURE__ */ new Date()) {
100
+ return date.toISOString().replace(/[:.]/g, "-");
101
+ }
102
+ var AppLogger = class {
103
+ logDir;
104
+ level;
105
+ maxBytes;
106
+ maxFiles;
107
+ currentPath;
108
+ rotationIndex = 0;
109
+ constructor(options = {}) {
110
+ this.logDir = options.logDir ?? process.env.CODESESH_LOG_DIR ?? getDefaultLogDir();
111
+ this.level = options.level ?? parseLevel(process.env.CODESESH_LOG_LEVEL);
112
+ this.maxBytes = options.maxBytes ?? parsePositiveInt(process.env.CODESESH_LOG_MAX_BYTES, 5e6);
113
+ this.maxFiles = options.maxFiles ?? parsePositiveInt(process.env.CODESESH_LOG_MAX_FILES, 5);
114
+ this.currentPath = join(this.logDir, "codesesh.log");
115
+ }
116
+ getLogPath() {
117
+ return this.currentPath;
118
+ }
119
+ debug(event, data = {}) {
120
+ this.write("debug", event, data);
121
+ }
122
+ info(event, data = {}) {
123
+ this.write("info", event, data);
124
+ }
125
+ warn(event, data = {}) {
126
+ this.write("warn", event, data);
127
+ }
128
+ error(event, data = {}) {
129
+ this.write("error", event, data);
130
+ }
131
+ write(level, event, data) {
132
+ if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[this.level]) return;
133
+ try {
134
+ mkdirSync(this.logDir, { recursive: true });
135
+ const line = `${JSON.stringify({
136
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
137
+ level,
138
+ event,
139
+ pid: process.pid,
140
+ ...toLogValue(data)
141
+ })}
142
+ `;
143
+ this.rotateIfNeeded(Buffer.byteLength(line));
144
+ appendFileSync(this.currentPath, line, "utf8");
145
+ } catch {
146
+ }
147
+ }
148
+ rotateIfNeeded(nextBytes) {
149
+ if (!existsSync(this.currentPath)) {
150
+ this.removeExpiredLogs();
151
+ return;
152
+ }
153
+ const currentSize = statSync(this.currentPath).size;
154
+ if (currentSize + nextBytes <= this.maxBytes) return;
155
+ this.rotationIndex += 1;
156
+ const rotatedPath = join(
157
+ this.logDir,
158
+ `codesesh-${timestampForFile()}-${process.pid}-${this.rotationIndex}.log`
159
+ );
160
+ renameSync(this.currentPath, rotatedPath);
161
+ this.removeExpiredLogs();
162
+ }
163
+ removeExpiredLogs() {
164
+ const rotated = readdirSync(this.logDir).filter((name) => /^codesesh-.+\.log$/.test(name)).map((name) => {
165
+ const path = join(this.logDir, name);
166
+ return { path, mtimeMs: statSync(path).mtimeMs };
167
+ }).toSorted((a, b) => b.mtimeMs - a.mtimeMs);
168
+ for (const item of rotated.slice(Math.max(0, this.maxFiles - 1))) {
169
+ unlinkSync(item.path);
170
+ }
171
+ }
172
+ };
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
+ }
192
+
35
193
  // src/api/handlers.ts
36
194
  function isRecord(value) {
37
195
  return typeof value === "object" && value !== null;
@@ -59,6 +217,9 @@ function parseBookmarkPayload(value) {
59
217
  function getTotalTokens(stats) {
60
218
  return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
61
219
  }
220
+ function getSessionAgentName(session) {
221
+ return session.slug.split("/")[0]?.toLowerCase() || "unknown";
222
+ }
62
223
  function getSessionActivityTime(session) {
63
224
  return session.time_updated ?? session.time_created;
64
225
  }
@@ -67,6 +228,56 @@ function parseDateParam(value, fallback) {
67
228
  const ts = new Date(value).getTime();
68
229
  return Number.isNaN(ts) ? fallback : ts;
69
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
+ }
70
281
  function filterSessionsByWindow(sessions, from, to) {
71
282
  return filterSessionsByActivityWindow(sessions, from, to);
72
283
  }
@@ -79,6 +290,166 @@ function filterSessionsByActivityWindow(sessions, from, to) {
79
290
  return true;
80
291
  });
81
292
  }
293
+ function matchesProjectScope(session, cwd) {
294
+ if (!session.directory) return false;
295
+ const identity = computeIdentity(cwd, realFs);
296
+ if (session.project_identity?.key === identity.key) return true;
297
+ return session.directory.toLowerCase().includes(cwd.toLowerCase());
298
+ }
299
+ function sanitizeClientLogData(value) {
300
+ if (!isRecord(value)) return {};
301
+ return Object.fromEntries(
302
+ Object.entries(value).slice(0, 30).map(([key, item]) => {
303
+ if (typeof item === "string") return [key, item.slice(0, 300)];
304
+ if (typeof item === "number" || typeof item === "boolean" || item == null) {
305
+ return [key, item];
306
+ }
307
+ return [key, String(item).slice(0, 300)];
308
+ })
309
+ );
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
+ }
82
453
  function handleGetConfig(c, defaults) {
83
454
  return c.json({
84
455
  window: {
@@ -100,11 +471,21 @@ function handleGetAgents(c, scanSource, defaults = {}) {
100
471
  const info = getAgentInfoMap(counts);
101
472
  return c.json(info);
102
473
  }
474
+ function handleGetProjects(c, scanSource, defaults = {}) {
475
+ const scanResult = scanSource.getSnapshot();
476
+ const { from, to } = defaults;
477
+ const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
478
+ return c.json({
479
+ projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
480
+ });
481
+ }
103
482
  function handleGetSessions(c, scanSource, defaults = {}) {
104
483
  const scanResult = scanSource.getSnapshot();
105
484
  const agent = c.req.query("agent");
106
485
  const q = c.req.query("q")?.toLowerCase();
107
- const cwd = c.req.query("cwd")?.toLowerCase();
486
+ const cwd = c.req.query("cwd");
487
+ const projectKey = c.req.query("projectKey");
488
+ const tag = c.req.query("tag")?.toLowerCase();
108
489
  const from = parseDateParam(c.req.query("from"), defaults.from);
109
490
  const to = parseDateParam(c.req.query("to"), defaults.to);
110
491
  let sessions = [];
@@ -113,10 +494,15 @@ function handleGetSessions(c, scanSource, defaults = {}) {
113
494
  } else {
114
495
  sessions = [...scanResult.sessions];
115
496
  }
116
- if (cwd) {
117
- sessions = sessions.filter((s) => s.directory.toLowerCase().includes(cwd));
497
+ if (projectKey) {
498
+ sessions = sessions.filter((s) => s.project_identity?.key === projectKey);
499
+ } else if (cwd) {
500
+ sessions = sessions.filter((s) => matchesProjectScope(s, cwd));
118
501
  }
119
502
  sessions = filterSessionsByActivityWindow(sessions, from, to);
503
+ if (tag) {
504
+ sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
505
+ }
120
506
  if (q) {
121
507
  sessions = sessions.filter((s) => s.title.toLowerCase().includes(q));
122
508
  }
@@ -124,32 +510,62 @@ function handleGetSessions(c, scanSource, defaults = {}) {
124
510
  }
125
511
  function handleSearchSessions(c, scanSource, defaults = {}) {
126
512
  const query = c.req.query("q")?.trim() ?? "";
127
- if (!query) {
128
- return c.json({ results: [] });
129
- }
130
513
  const scanResult = scanSource.getSnapshot();
131
- const agent = c.req.query("agent");
132
- const cwd = c.req.query("cwd");
133
- const from = parseDateParam(c.req.query("from"), defaults.from);
134
- const to = parseDateParam(c.req.query("to"), defaults.to);
135
- for (const indexedAgent of scanResult.agents) {
136
- const sessions = scanResult.byAgent[indexedAgent.name] ?? [];
137
- syncSessionSearchIndex(
138
- indexedAgent.name,
139
- sessions,
140
- (sessionId) => indexedAgent.getSessionData(sessionId)
141
- );
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
+ });
142
528
  }
143
- const results = searchSessions(query, {
144
- agent,
145
- cwd,
146
- from,
147
- to,
148
- limit: 50
149
- });
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
+ );
150
537
  return c.json({ results });
151
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
+ }
152
567
  async function handleGetSessionData(c, scanSource) {
568
+ const startedAt = performance.now();
153
569
  const scanResult = scanSource.getSnapshot();
154
570
  const agentName = c.req.param("agent");
155
571
  const sessionId = c.req.param("id");
@@ -161,13 +577,55 @@ async function handleGetSessionData(c, scanSource) {
161
577
  return c.json({ error: `Unknown agent: ${agentName}` }, 404);
162
578
  }
163
579
  try {
580
+ const loadStartedAt = performance.now();
164
581
  const data = agent.getSessionData(sessionId);
165
- return c.json(data);
582
+ const loadDuration = performance.now() - loadStartedAt;
583
+ const tagStartedAt = performance.now();
584
+ const smartTags = classifySessionTags(data);
585
+ const tagDuration = performance.now() - tagStartedAt;
586
+ const head = scanResult.byAgent[agentName]?.find((item) => item.id === sessionId);
587
+ const projectIdentity = data.project_identity ?? head?.project_identity ?? computeIdentity(data.directory, realFs);
588
+ appLogger.info("api.session_data", {
589
+ agent: agentName,
590
+ session_id: sessionId,
591
+ messages: data.messages.length,
592
+ load_duration_ms: Math.round(loadDuration),
593
+ tag_duration_ms: Math.round(tagDuration),
594
+ duration_ms: Math.round(performance.now() - startedAt)
595
+ });
596
+ return c.json({
597
+ ...data,
598
+ project_identity: projectIdentity,
599
+ smart_tags: smartTags,
600
+ smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
601
+ file_activity: extractSessionFileActivity(
602
+ agentName,
603
+ sessionId,
604
+ projectIdentity.key,
605
+ data.messages
606
+ )
607
+ });
166
608
  } catch (err) {
167
609
  const message = err instanceof Error ? err.message : "Failed to load session";
610
+ appLogger.error("api.session_data.error", {
611
+ agent: agentName,
612
+ session_id: sessionId,
613
+ duration_ms: Math.round(performance.now() - startedAt),
614
+ error: message
615
+ });
168
616
  return c.json({ error: message }, 500);
169
617
  }
170
618
  }
619
+ async function handlePostClientLog(c) {
620
+ const payload = await c.req.json().catch(() => null);
621
+ const rawEvent = payload?.event;
622
+ if (typeof rawEvent !== "string" || !rawEvent.trim()) {
623
+ return c.json({ ok: false }, 400);
624
+ }
625
+ const event = rawEvent.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 120);
626
+ appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
627
+ return c.json({ ok: true });
628
+ }
171
629
  function handleGetBookmarks(c) {
172
630
  try {
173
631
  return c.json({ bookmarks: listBookmarks(), storageAvailable: true });
@@ -240,23 +698,22 @@ function startOfLocalDay(ts) {
240
698
  }
241
699
  function resolveDashboardWindow(defaults, queryDays, queryFrom, queryTo) {
242
700
  const now = Date.now();
243
- const todayStart = startOfLocalDay(now);
244
- const toTs = parseDateParam(queryTo, defaults.to) ?? todayStart + 24 * 60 * 60 * 1e3 - 1;
701
+ const toTs = parseDateParam(queryTo, defaults.to) ?? now;
245
702
  const parsedDays = queryDays ? parseInt(queryDays, 10) : NaN;
246
703
  let days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : defaults.days;
247
704
  const fromFromQuery = parseDateParam(queryFrom, void 0);
248
705
  let fromTs;
249
706
  if (fromFromQuery != null) {
250
- fromTs = startOfLocalDay(fromFromQuery);
251
- days ??= Math.max(1, Math.ceil((todayStart - fromTs) / 864e5) + 1);
252
- } else if (days && days > 0) {
253
- fromTs = todayStart - (days - 1) * 864e5;
707
+ fromTs = fromFromQuery;
708
+ days ??= Math.max(1, Math.ceil((toTs - fromTs) / 864e5));
254
709
  } else if (defaults.from != null) {
255
- fromTs = startOfLocalDay(defaults.from);
256
- days = Math.max(1, Math.ceil((todayStart - fromTs) / 864e5) + 1);
710
+ fromTs = defaults.from;
711
+ days ??= Math.max(1, Math.ceil((toTs - fromTs) / 864e5));
712
+ } else if (days && days > 0) {
713
+ fromTs = startOfLocalDay(toTs) - (days - 1) * 864e5;
257
714
  } else {
258
715
  days = 30;
259
- fromTs = todayStart - (days - 1) * 864e5;
716
+ fromTs = startOfLocalDay(toTs) - (days - 1) * 864e5;
260
717
  }
261
718
  return { from: fromTs, to: toTs, days };
262
719
  }
@@ -268,10 +725,19 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
268
725
  c.req.query("from"),
269
726
  c.req.query("to")
270
727
  );
271
- const windowed = filterSessionsByActivityWindow(scanResult.sessions, from, to);
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
+ );
272
738
  const agentInfo = getAgentInfoMap(
273
739
  Object.fromEntries(
274
- Object.entries(scanResult.byAgent).map(([name, sessions]) => [
740
+ Object.entries(scopedByAgent).map(([name, sessions]) => [
275
741
  name,
276
742
  filterSessionsByActivityWindow(sessions, from, to).length
277
743
  ])
@@ -281,15 +747,17 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
281
747
  let totalMessages = 0;
282
748
  let totalTokens = 0;
283
749
  let totalCost = 0;
750
+ let hasEstimatedCost = false;
284
751
  let latestActivity = 0;
285
752
  for (const session of windowed) {
286
753
  totalMessages += session.stats.message_count;
287
754
  totalTokens += getTotalTokens(session.stats);
288
755
  totalCost += session.stats.total_cost ?? 0;
756
+ if (session.stats.cost_source === "estimated") hasEstimatedCost = true;
289
757
  const activity = getSessionActivityTime(session);
290
758
  if (activity > latestActivity) latestActivity = activity;
291
759
  }
292
- const perAgent = Object.entries(scanResult.byAgent).map(([name, sessions]) => {
760
+ const perAgent = Object.entries(scopedByAgent).map(([name, sessions]) => {
293
761
  const info = agentInfoMap.get(name);
294
762
  const agentWindowed = filterSessionsByActivityWindow(sessions, from, to);
295
763
  let messages = 0;
@@ -310,7 +778,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
310
778
  const dailyMap = /* @__PURE__ */ new Map();
311
779
  const dailyTokenMap = /* @__PURE__ */ new Map();
312
780
  const bucketStart = startOfLocalDay(from);
313
- for (let i = 0; i < days; i += 1) {
781
+ const bucketDays = Math.floor((startOfLocalDay(to) - bucketStart) / 864e5) + 1;
782
+ for (let i = 0; i < bucketDays; i += 1) {
314
783
  const ts = bucketStart + i * 864e5;
315
784
  const key = toLocalDateKey(ts);
316
785
  dailyMap.set(key, { date: key, sessions: 0, messages: 0 });
@@ -350,7 +819,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
350
819
  const dailyTokenActivity = [...dailyTokenMap.values()];
351
820
  const modelDistribution = [...modelAgg.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
352
821
  const recentSessions = [...windowed].sort((a, b) => getSessionActivityTime(b) - getSessionActivityTime(a)).slice(0, 10).map((session) => {
353
- const agentKey = session.slug.split("/")[0] ?? "unknown";
822
+ const agentKey = getSessionAgentName(session);
354
823
  return { ...session, agentName: agentKey };
355
824
  });
356
825
  const data = {
@@ -359,6 +828,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
359
828
  messages: totalMessages,
360
829
  tokens: totalTokens,
361
830
  cost: totalCost,
831
+ cost_source: totalCost > 0 ? hasEstimatedCost ? "estimated" : "recorded" : void 0,
362
832
  latestActivity: latestActivity || void 0
363
833
  },
364
834
  perAgent,
@@ -366,6 +836,13 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
366
836
  dailyTokenActivity,
367
837
  modelDistribution,
368
838
  recentSessions,
839
+ recentFileActivities: listFileActivity({
840
+ agent: scope.agent,
841
+ projectKey: scope.projectKey,
842
+ from,
843
+ to,
844
+ limit: 12
845
+ }),
369
846
  window: { from, to, days }
370
847
  };
371
848
  return c.json(data);
@@ -420,14 +897,17 @@ function createApiRoutes(scanSource, store, options = {}) {
420
897
  };
421
898
  api.get("/config", (c) => handleGetConfig(c, listDefaults));
422
899
  api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
900
+ api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
423
901
  api.get("/sessions", (c) => handleGetSessions(c, scanSource, listDefaults));
424
902
  api.get("/search", (c) => handleSearchSessions(c, scanSource, listDefaults));
903
+ api.get("/file-activity", (c) => handleGetFileActivity(c, listDefaults));
425
904
  api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
426
905
  api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
427
906
  api.get("/bookmarks", (c) => handleGetBookmarks(c));
428
907
  api.put("/bookmarks", (c) => handlePutBookmark(c));
429
908
  api.post("/bookmarks/import", (c) => handleImportBookmarks(c));
430
909
  api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
910
+ api.post("/logs", (c) => handlePostClientLog(c));
431
911
  if (store) {
432
912
  api.get("/events", (c) => createSseResponse(store, c.req.raw.signal));
433
913
  }
@@ -438,20 +918,20 @@ function createApiRoutes(scanSource, store, options = {}) {
438
918
  function findWebDistPath() {
439
919
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
440
920
  const packagedPath = resolve(__dirname2, "web");
441
- if (existsSync(packagedPath)) {
921
+ if (existsSync2(packagedPath)) {
442
922
  return packagedPath;
443
923
  }
444
924
  const devPath = resolve(__dirname2, "../../../apps/web/dist");
445
- if (existsSync(devPath)) {
925
+ if (existsSync2(devPath)) {
446
926
  return devPath;
447
927
  }
448
928
  return null;
449
929
  }
450
930
  function waitForListening(server) {
451
- return new Promise((resolve3, reject) => {
931
+ return new Promise((resolve4, reject) => {
452
932
  const handleListening = () => {
453
933
  server.off("error", handleError);
454
- resolve3();
934
+ resolve4();
455
935
  };
456
936
  const handleError = (error) => {
457
937
  server.off("listening", handleListening);
@@ -469,7 +949,26 @@ function getServerStartupErrorMessage(error, port) {
469
949
  }
470
950
  async function createServer(port, store, options = {}) {
471
951
  const app = new Hono2();
472
- app.use("*", logger());
952
+ app.use("*", async (c, next) => {
953
+ const startedAt = performance.now();
954
+ let thrown;
955
+ try {
956
+ await next();
957
+ } catch (error) {
958
+ thrown = error;
959
+ throw error;
960
+ } finally {
961
+ const url2 = new URL(c.req.url);
962
+ appLogger.info("http.request", {
963
+ method: c.req.method,
964
+ path: url2.pathname,
965
+ query_keys: [...url2.searchParams.keys()].toSorted(),
966
+ status: c.res.status,
967
+ duration_ms: Math.round(performance.now() - startedAt),
968
+ error: thrown instanceof Error ? thrown.message : void 0
969
+ });
970
+ }
971
+ });
473
972
  const routeOptions = {
474
973
  defaultSessionFrom: options.defaultSessionFrom,
475
974
  defaultSessionTo: options.defaultSessionTo,
@@ -492,6 +991,7 @@ async function createServer(port, store, options = {}) {
492
991
  try {
493
992
  await waitForListening(server);
494
993
  } catch (error) {
994
+ appLogger.error("server.listen.error", { port, error });
495
995
  server.close();
496
996
  if (store.shutdown) {
497
997
  await store.shutdown();
@@ -499,9 +999,11 @@ async function createServer(port, store, options = {}) {
499
999
  throw new Error(getServerStartupErrorMessage(error, port));
500
1000
  }
501
1001
  const url = `http://localhost:${port}`;
1002
+ appLogger.info("server.listen", { port, url });
502
1003
  return {
503
1004
  url,
504
1005
  shutdown: () => {
1006
+ appLogger.info("server.shutdown", { port });
505
1007
  server.close();
506
1008
  if (store.shutdown) {
507
1009
  void store.shutdown();
@@ -511,9 +1013,17 @@ async function createServer(port, store, options = {}) {
511
1013
  }
512
1014
 
513
1015
  // src/live-scan.ts
514
- import { existsSync as existsSync2 } from "fs";
515
- import { dirname as dirname2, isAbsolute, join } from "path";
516
- import chokidar from "chokidar";
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;
517
1027
  function sortSessions(sessions) {
518
1028
  return [...sessions].sort(
519
1029
  (a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
@@ -575,12 +1085,15 @@ function buildUpdateEvent(agentName, previousSessions, nextSessions) {
575
1085
  timestamp: Date.now()
576
1086
  };
577
1087
  }
1088
+ function toAbsolutePath(path) {
1089
+ return isAbsolute(path) ? path : resolve2(path);
1090
+ }
578
1091
  function closestWatchablePath(targetPath) {
579
- if (!isAbsolute(targetPath) && !existsSync2(targetPath)) {
1092
+ if (!isAbsolute(targetPath) && !existsSync3(targetPath)) {
580
1093
  return null;
581
1094
  }
582
- let current = targetPath;
583
- while (!existsSync2(current)) {
1095
+ let current = toAbsolutePath(targetPath);
1096
+ while (!existsSync3(current)) {
584
1097
  const parent = dirname2(current);
585
1098
  if (parent === current) {
586
1099
  return null;
@@ -589,43 +1102,99 @@ function closestWatchablePath(targetPath) {
589
1102
  }
590
1103
  return current;
591
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
+ }
592
1156
  function resolveAgentWatchTargets(agentName) {
593
1157
  const roots = resolveProviderRoots();
594
1158
  const cursorDataPath = getCursorDataPath();
595
1159
  switch (agentName) {
596
1160
  case "claudecode":
597
1161
  return [
598
- { path: join(roots.claudeRoot, "projects"), depth: 2 },
599
- { path: "data/claudecode", depth: 2 }
1162
+ { root: roots.claudeRoot, path: join2(roots.claudeRoot, "projects") },
1163
+ { path: "data/claudecode" }
600
1164
  ];
601
1165
  case "codex":
602
- return [{ path: join(roots.codexRoot, "sessions"), depth: 4 }];
1166
+ return [{ root: roots.codexRoot, path: join2(roots.codexRoot, "sessions") }];
603
1167
  case "cursor":
604
1168
  return cursorDataPath ? [
605
- { path: join(cursorDataPath, "globalStorage", "state.vscdb") },
606
- { path: join(cursorDataPath, "workspaceStorage"), depth: 2 }
1169
+ {
1170
+ root: cursorDataPath,
1171
+ path: join2(cursorDataPath, "globalStorage", "state.vscdb")
1172
+ },
1173
+ { root: cursorDataPath, path: join2(cursorDataPath, "workspaceStorage") }
607
1174
  ] : [];
608
1175
  case "kimi":
609
1176
  return [
610
- { path: join(roots.kimiRoot, "sessions"), depth: 2 },
611
- { path: "data/kimi", depth: 2 }
1177
+ { root: roots.kimiRoot, path: join2(roots.kimiRoot, "sessions") },
1178
+ { path: "data/kimi" }
612
1179
  ];
613
1180
  case "opencode":
614
1181
  return [
615
- { path: join(roots.opencodeRoot, "opencode.db") },
616
- { path: "data/opencode/opencode.db" }
1182
+ { root: roots.opencodeRoot, path: join2(roots.opencodeRoot, "opencode.db") },
1183
+ { root: "data/opencode", path: "data/opencode/opencode.db" }
617
1184
  ];
618
1185
  default:
619
1186
  return [];
620
1187
  }
621
1188
  }
622
1189
  var LiveScanStore = class {
623
- constructor(watchEnabled = true, scanOptions = {}) {
1190
+ constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}) {
624
1191
  this.watchEnabled = watchEnabled;
625
1192
  this.scanOptions = scanOptions;
1193
+ this.startupScanOptions = startupScanOptions;
626
1194
  }
627
1195
  watchEnabled;
628
1196
  scanOptions;
1197
+ startupScanOptions;
629
1198
  agents = [];
630
1199
  byAgent = {};
631
1200
  sessions = [];
@@ -634,37 +1203,45 @@ var LiveScanStore = class {
634
1203
  refreshTimestamps = /* @__PURE__ */ new Map();
635
1204
  refreshInFlight = /* @__PURE__ */ new Set();
636
1205
  pendingRefreshes = /* @__PURE__ */ new Set();
1206
+ pendingRefreshPathCounts = /* @__PURE__ */ new Map();
637
1207
  watchers = [];
1208
+ fallbackWatchScopes = /* @__PURE__ */ new Map();
1209
+ stablePaths = /* @__PURE__ */ new Map();
1210
+ pendingEvent = null;
1211
+ pendingEventTimer = null;
1212
+ initialSearchIndexTimer = null;
1213
+ searchIndexWorker = null;
638
1214
  async initialize() {
1215
+ const startedAt = performance.now();
1216
+ appLogger.info("scan.initial.start", {
1217
+ watch_enabled: this.watchEnabled,
1218
+ agents: this.scanOptions.agents,
1219
+ use_cache: this.scanOptions.useCache ?? true,
1220
+ startup_from: this.startupScanOptions.from,
1221
+ startup_to: this.startupScanOptions.to
1222
+ });
639
1223
  const initialResult = await scanSessions({
640
1224
  ...this.scanOptions,
641
- useCache: true,
642
- smartRefresh: false
1225
+ ...this.startupScanOptions,
1226
+ useCache: this.scanOptions.useCache ?? true,
1227
+ smartRefresh: false,
1228
+ writeCache: this.startupScanOptions.from != null || this.startupScanOptions.to != null ? false : void 0,
1229
+ includeSmartTags: this.startupScanOptions.from != null || this.startupScanOptions.to != null ? false : void 0
643
1230
  });
644
- const knownAgents = createRegisteredAgents();
645
- const agentMap = /* @__PURE__ */ new Map();
646
- const allowedAgents = this.getAllowedAgents();
647
- for (const agent of initialResult.agents) {
648
- agentMap.set(agent.name, agent);
649
- }
650
- for (const agent of knownAgents) {
651
- if (!agentMap.has(agent.name)) {
652
- agentMap.set(agent.name, agent);
653
- }
654
- }
655
- this.agents = [...agentMap.values()].filter((agent) => {
656
- if (!allowedAgents) {
657
- return true;
658
- }
659
- return allowedAgents.has(agent.name.toLowerCase());
1231
+ this.applyScanResult(initialResult);
1232
+ appLogger.info("scan.initial.done", {
1233
+ duration_ms: Math.round(performance.now() - startedAt),
1234
+ sessions: this.sessions.length,
1235
+ agents: Object.fromEntries(
1236
+ Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
1237
+ )
660
1238
  });
661
- for (const agent of this.agents) {
662
- this.byAgent[agent.name] = sortSessions(initialResult.byAgent[agent.name] ?? []);
663
- this.refreshTimestamps.set(agent.name, Date.now());
664
- }
665
- this.rebuildSessions();
666
1239
  if (this.watchEnabled) {
667
1240
  this.startWatching();
1241
+ this.initialSearchIndexTimer = setTimeout(() => {
1242
+ this.initialSearchIndexTimer = null;
1243
+ this.startSearchIndexWorker("scan.initial.background");
1244
+ }, 1e3);
668
1245
  }
669
1246
  }
670
1247
  getSnapshot() {
@@ -685,17 +1262,133 @@ var LiveScanStore = class {
685
1262
  clearTimeout(timer);
686
1263
  }
687
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;
688
1285
  await Promise.all(this.watchers.map((watcher) => watcher.close()));
689
1286
  this.watchers = [];
1287
+ this.fallbackWatchScopes.clear();
690
1288
  }
691
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) {
692
1297
  for (const listener of this.listeners) {
693
1298
  listener(event);
694
1299
  }
695
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
+ }
696
1315
  rebuildSessions() {
697
1316
  this.sessions = sortSessions(Object.values(this.byAgent).flat());
698
1317
  }
1318
+ hasStartupWindow() {
1319
+ return this.startupScanOptions.from != null || this.startupScanOptions.to != null;
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
+ }
1367
+ applyScanResult(result) {
1368
+ const knownAgents = createRegisteredAgents();
1369
+ const agentMap = /* @__PURE__ */ new Map();
1370
+ const allowedAgents = this.getAllowedAgents();
1371
+ for (const agent of result.agents) {
1372
+ agentMap.set(agent.name, agent);
1373
+ }
1374
+ for (const agent of knownAgents) {
1375
+ if (!agentMap.has(agent.name)) {
1376
+ agentMap.set(agent.name, agent);
1377
+ }
1378
+ }
1379
+ this.agents = [...agentMap.values()].filter((agent) => {
1380
+ if (!allowedAgents) {
1381
+ return true;
1382
+ }
1383
+ return allowedAgents.has(agent.name.toLowerCase());
1384
+ });
1385
+ this.byAgent = {};
1386
+ for (const agent of this.agents) {
1387
+ this.byAgent[agent.name] = sortSessions(result.byAgent[agent.name] ?? []);
1388
+ this.refreshTimestamps.set(agent.name, Date.now());
1389
+ }
1390
+ this.rebuildSessions();
1391
+ }
699
1392
  getAllowedAgents() {
700
1393
  if (!this.scanOptions.agents?.length) {
701
1394
  return null;
@@ -703,44 +1396,199 @@ var LiveScanStore = class {
703
1396
  return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
704
1397
  }
705
1398
  applyFilters(sessions) {
706
- return filterSessions(sessions, this.scanOptions);
1399
+ return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
707
1400
  }
708
1401
  startWatching() {
1402
+ const scopesByRoot = /* @__PURE__ */ new Map();
709
1403
  for (const agent of this.agents) {
710
- const rawTargets = resolveAgentWatchTargets(agent.name);
711
- const watchTargets = rawTargets.map((target) => {
712
- const watchPath = closestWatchablePath(target.path);
713
- return watchPath ? { ...target, path: watchPath } : null;
714
- }).filter((target) => target !== null).filter(
715
- (target, index, items) => items.findIndex((item) => item.path === target.path && item.depth === target.depth) === index
716
- );
1404
+ const watchTargets = resolveAgentWatchTargets(agent.name);
717
1405
  if (watchTargets.length === 0) {
1406
+ appLogger.debug("watch.skip", { agent: agent.name });
718
1407
  continue;
719
1408
  }
720
- const watcher = chokidar.watch(
721
- watchTargets.map((target) => target.path),
722
- {
723
- ignoreInitial: true,
724
- awaitWriteFinish: {
725
- stabilityThreshold: 250,
726
- pollInterval: 100
727
- },
728
- depth: watchTargets.reduce(
729
- (maxDepth, target) => Math.max(maxDepth, target.depth ?? 0),
730
- 0
731
- )
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;
732
1418
  }
733
- );
734
- watcher.on("all", () => {
735
- this.scheduleRefresh(agent.name);
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)));
1429
+ appLogger.info("watch.start", {
1430
+ root: rootPath,
1431
+ agents,
1432
+ targets: scopes.map((scope) => ({
1433
+ agent: scope.agentName,
1434
+ path: scope.targetPath
1435
+ }))
1436
+ });
1437
+ if (isRecursiveWatchSupported()) {
1438
+ const started = this.watchDirectory(rootPath, scopes, true);
1439
+ if (started) {
1440
+ continue;
1441
+ }
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
+ });
736
1460
  });
737
1461
  watcher.on("error", (error) => {
738
- console.error(`[${agent.name}] File watcher failed:`, error);
1462
+ this.reportWatchError("watch.error", { path, recursive, error });
739
1463
  });
740
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;
741
1473
  }
742
1474
  }
743
- scheduleRefresh(agentName, delayMs = 200) {
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) {
1591
+ appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: delayMs });
744
1592
  const existing = this.refreshTimers.get(agentName);
745
1593
  if (existing) {
746
1594
  clearTimeout(existing);
@@ -753,22 +1601,30 @@ var LiveScanStore = class {
753
1601
  }
754
1602
  async refreshAgent(agentName) {
755
1603
  if (this.refreshInFlight.has(agentName)) {
1604
+ appLogger.debug("scan.refresh.pending", { agent: agentName });
756
1605
  this.pendingRefreshes.add(agentName);
757
1606
  return;
758
1607
  }
759
1608
  this.refreshInFlight.add(agentName);
760
1609
  try {
761
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);
762
1614
  } finally {
763
1615
  this.refreshInFlight.delete(agentName);
764
1616
  if (this.pendingRefreshes.delete(agentName)) {
765
- this.scheduleRefresh(agentName, 100);
1617
+ this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
766
1618
  }
767
1619
  }
768
1620
  }
769
1621
  async runRefresh(agentName) {
1622
+ const startedAt = performance.now();
1623
+ const pendingPathCount = this.pendingRefreshPathCounts.get(agentName) ?? 0;
1624
+ this.pendingRefreshPathCounts.delete(agentName);
770
1625
  const agent = this.agents.find((item) => item.name === agentName);
771
1626
  if (!agent) {
1627
+ appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
772
1628
  return;
773
1629
  }
774
1630
  const previousSessions = this.byAgent[agentName] ?? [];
@@ -782,18 +1638,35 @@ var LiveScanStore = class {
782
1638
  );
783
1639
  this.refreshTimestamps.set(agentName, checkResult.timestamp);
784
1640
  if (!checkResult.hasChanges) {
1641
+ appLogger.debug("scan.refresh.unchanged", {
1642
+ agent: agentName,
1643
+ duration_ms: Math.round(performance.now() - startedAt)
1644
+ });
785
1645
  return;
786
1646
  }
787
1647
  nextSessions = await Promise.resolve(
788
1648
  agent.incrementalScan(previousSessions, checkResult.changedIds ?? [])
789
1649
  );
790
1650
  } else {
791
- nextSessions = await Promise.resolve(agent.scan());
1651
+ nextSessions = await Promise.resolve(agent.scan(this.startupScanOptions));
792
1652
  this.refreshTimestamps.set(agentName, Date.now());
793
1653
  }
794
1654
  nextSessions = this.applyFilters(nextSessions);
795
- saveCachedSessions(agentName, nextSessions, buildAgentCacheMeta(agent));
796
- syncSessionSearchIndex(agentName, nextSessions, (sessionId) => agent.getSessionData(sessionId));
1655
+ if (!this.hasStartupWindow()) {
1656
+ saveCachedSessions(agentName, nextSessions, buildAgentCacheMeta(agent));
1657
+ }
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 });
797
1670
  const event = buildUpdateEvent(agentName, previousSessions, nextSessions);
798
1671
  this.byAgent[agentName] = sortSessions(nextSessions);
799
1672
  this.rebuildSessions();
@@ -801,6 +1674,17 @@ var LiveScanStore = class {
801
1674
  event.totalSessions = this.sessions.length;
802
1675
  this.emit(event);
803
1676
  }
1677
+ appLogger.info("scan.refresh.done", {
1678
+ agent: agentName,
1679
+ duration_ms: Math.round(performance.now() - startedAt),
1680
+ sessions: nextSessions.length,
1681
+ new_sessions: event?.newSessions ?? 0,
1682
+ updated_sessions: event?.updatedSessions ?? 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)
1687
+ });
804
1688
  }
805
1689
  };
806
1690
 
@@ -809,10 +1693,10 @@ import { consola } from "consola";
809
1693
 
810
1694
  // src/version.ts
811
1695
  import { readFileSync } from "fs";
812
- import { resolve as resolve2, dirname as dirname3 } from "path";
813
- import { fileURLToPath as fileURLToPath2 } from "url";
814
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
815
- var pkg = JSON.parse(readFileSync(resolve2(__dirname, "../package.json"), "utf-8"));
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"));
816
1700
  var VERSION = pkg.version;
817
1701
 
818
1702
  // src/output.ts
@@ -936,6 +1820,7 @@ var main = defineCommand({
936
1820
  }
937
1821
  },
938
1822
  async run({ args }) {
1823
+ const startedAt = performance.now();
939
1824
  const port = parseInt(args.port, 10) || 4321;
940
1825
  const noOpen = args.noOpen;
941
1826
  const jsonOnly = args.json;
@@ -945,11 +1830,22 @@ var main = defineCommand({
945
1830
  if (trace) {
946
1831
  perf.enable();
947
1832
  }
1833
+ appLogger.info("cli.start", {
1834
+ version: VERSION,
1835
+ argv: process.argv.slice(2),
1836
+ port,
1837
+ json: jsonOnly,
1838
+ no_open: noOpen,
1839
+ cache: useCache,
1840
+ log_path: appLogger.getLogPath()
1841
+ });
948
1842
  if (clearCache) {
949
- const { clearCache: clear } = await import("./dist-5NKHH33A.js");
1843
+ const { clearCache: clear } = await import("./dist-NT4CH6KD.js");
950
1844
  clear();
1845
+ appLogger.info("cache.clear");
951
1846
  console.log("Cache cleared.");
952
1847
  }
1848
+ void refreshPricingCache();
953
1849
  let targetSession = null;
954
1850
  if (args.session) {
955
1851
  targetSession = parseSessionUri(args.session);
@@ -979,9 +1875,19 @@ var main = defineCommand({
979
1875
  cwd: cwdFilter,
980
1876
  useCache
981
1877
  };
982
- const store = new LiveScanStore(!jsonOnly, scanOptions);
1878
+ const startupScanOptions = targetSession || jsonOnly ? {} : { from: listDefaultFrom, to: listDefaultTo };
1879
+ const store = new LiveScanStore(!jsonOnly, scanOptions, startupScanOptions);
983
1880
  await store.initialize();
984
1881
  const result = store.getSnapshot();
1882
+ appLogger.info("cli.scan_ready", {
1883
+ duration_ms: Math.round(performance.now() - startedAt),
1884
+ sessions: result.sessions.length,
1885
+ agents: Object.fromEntries(
1886
+ Object.entries(result.byAgent).map(([key, value]) => [key, value.length])
1887
+ ),
1888
+ startup_from: startupScanOptions.from,
1889
+ startup_to: startupScanOptions.to
1890
+ });
985
1891
  if (trace) {
986
1892
  console.log(perf.getReport());
987
1893
  }
@@ -1004,6 +1910,10 @@ var main = defineCommand({
1004
1910
  })),
1005
1911
  sessions: windowed
1006
1912
  };
1913
+ appLogger.info("cli.json_output", {
1914
+ sessions: windowed.length,
1915
+ duration_ms: Math.round(performance.now() - startedAt)
1916
+ });
1007
1917
  console.log(JSON.stringify(output, null, 2));
1008
1918
  return;
1009
1919
  }
@@ -1022,9 +1932,15 @@ var main = defineCommand({
1022
1932
  }
1023
1933
  console.log(` ${url}`);
1024
1934
  console.log("");
1935
+ appLogger.info("cli.ready", {
1936
+ url,
1937
+ duration_ms: Math.round(performance.now() - startedAt),
1938
+ log_path: appLogger.getLogPath()
1939
+ });
1025
1940
  if (!noOpen) {
1026
1941
  const open = (await import("open")).default;
1027
1942
  const targetUrl = targetSession ? `${url}/${targetSession.agent.toLowerCase()}/${targetSession.sessionId}` : url;
1943
+ appLogger.info("browser.open", { url: targetUrl });
1028
1944
  await open(targetUrl);
1029
1945
  }
1030
1946
  }