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/README.md +11 -6
- package/dist/chunk-SQYHWMQV.js +7598 -0
- package/dist/chunk-SQYHWMQV.js.map +1 -0
- package/dist/dist-NT4CH6KD.js +148 -0
- package/dist/index.js +1037 -121
- 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/markdown-CnUlvKkZ.js +14 -0
- package/dist/web/assets/react-DT3QPCDf.js +1821 -0
- package/dist/web/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/web/assets/syntax-DcanuzfQ.js +6 -0
- package/dist/web/assets/vendor-Bs5B_LvM.js +43 -0
- package/dist/web/index.html +7 -2
- package/package.json +2 -4
- package/dist/chunk-BGQXQTWM.js +0 -4375
- package/dist/chunk-BGQXQTWM.js.map +0 -1
- package/dist/dist-5NKHH33A.js +0 -70
- package/dist/web/assets/index-B78jpjIp.js +0 -68
- package/dist/web/assets/index-gSYgPU_H.css +0 -2
- /package/dist/{dist-5NKHH33A.js.map → dist-NT4CH6KD.js.map} +0 -0
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-
|
|
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 {
|
|
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")
|
|
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 (
|
|
117
|
-
sessions = sessions.filter((s) => s.
|
|
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
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
251
|
-
days ??= Math.max(1, Math.ceil((
|
|
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 =
|
|
256
|
-
days
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 (
|
|
921
|
+
if (existsSync2(packagedPath)) {
|
|
442
922
|
return packagedPath;
|
|
443
923
|
}
|
|
444
924
|
const devPath = resolve(__dirname2, "../../../apps/web/dist");
|
|
445
|
-
if (
|
|
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((
|
|
931
|
+
return new Promise((resolve4, reject) => {
|
|
452
932
|
const handleListening = () => {
|
|
453
933
|
server.off("error", handleError);
|
|
454
|
-
|
|
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("*",
|
|
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
|
|
515
|
-
import { dirname as dirname2, isAbsolute, join } from "path";
|
|
516
|
-
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;
|
|
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) && !
|
|
1092
|
+
if (!isAbsolute(targetPath) && !existsSync3(targetPath)) {
|
|
580
1093
|
return null;
|
|
581
1094
|
}
|
|
582
|
-
let current = targetPath;
|
|
583
|
-
while (!
|
|
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:
|
|
599
|
-
{ path: "data/claudecode"
|
|
1162
|
+
{ root: roots.claudeRoot, path: join2(roots.claudeRoot, "projects") },
|
|
1163
|
+
{ path: "data/claudecode" }
|
|
600
1164
|
];
|
|
601
1165
|
case "codex":
|
|
602
|
-
return [{ path:
|
|
1166
|
+
return [{ root: roots.codexRoot, path: join2(roots.codexRoot, "sessions") }];
|
|
603
1167
|
case "cursor":
|
|
604
1168
|
return cursorDataPath ? [
|
|
605
|
-
{
|
|
606
|
-
|
|
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:
|
|
611
|
-
{ path: "data/kimi"
|
|
1177
|
+
{ root: roots.kimiRoot, path: join2(roots.kimiRoot, "sessions") },
|
|
1178
|
+
{ path: "data/kimi" }
|
|
612
1179
|
];
|
|
613
1180
|
case "opencode":
|
|
614
1181
|
return [
|
|
615
|
-
{ path:
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
|
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
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
796
|
-
|
|
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
|
|
813
|
-
import { fileURLToPath as
|
|
814
|
-
var __dirname = dirname3(
|
|
815
|
-
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"));
|
|
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-
|
|
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
|
|
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
|
}
|