@tonyclaw/agent-inspector 2.1.6 → 2.1.7

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.
Files changed (31) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Djazxhew.js → CompareDrawer-BLS7CVvA.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-BC-z9RPh.js → ProxyViewerContainer-BFZ1WG07.js} +33 -33
  4. package/.output/public/assets/{ReplayDialog-BaD4R6ac.js → ReplayDialog-DIGF807X.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-w6uGdqYp.js → RequestAnatomy-C3Nlvp9P.js} +1 -1
  6. package/.output/public/assets/{ResponseView-VPed-Yif.js → ResponseView-BbZywqHe.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-CPB8FgHI.js → StreamingChunkSequence-Dk27PfPn.js} +1 -1
  8. package/.output/public/assets/_sessionId-BHpX8AVf.js +1 -0
  9. package/.output/public/assets/index-BXHM9OMb.js +1 -0
  10. package/.output/public/assets/{main-CIV3Chb5.js → main-BxHXMs6B.js} +2 -2
  11. package/.output/server/{_sessionId-CIXQGim9.mjs → _sessionId-D5HNwb9A.mjs} +3 -3
  12. package/.output/server/_ssr/{CompareDrawer-_uFgjkB2.mjs → CompareDrawer-QNk1twf8.mjs} +3 -3
  13. package/.output/server/_ssr/{ProxyViewerContainer-D8YjQDNj.mjs → ProxyViewerContainer-DGhc2le_.mjs} +37 -10
  14. package/.output/server/_ssr/{ReplayDialog-CFwKM7jc.mjs → ReplayDialog-D8wg4VNL.mjs} +4 -4
  15. package/.output/server/_ssr/{RequestAnatomy-2Vr84GxT.mjs → RequestAnatomy-BtJHKVl2.mjs} +3 -3
  16. package/.output/server/_ssr/{ResponseView-CSBWBHRJ.mjs → ResponseView-CndDItiU.mjs} +3 -3
  17. package/.output/server/_ssr/{StreamingChunkSequence-Dr4JApqs.mjs → StreamingChunkSequence-D5Ywpbr7.mjs} +3 -3
  18. package/.output/server/_ssr/{index-WnsY1fh_.mjs → index-DtcUTHAX.mjs} +3 -3
  19. package/.output/server/_ssr/index.mjs +2 -2
  20. package/.output/server/_ssr/{router-sy38X5AX.mjs → router-C0apgqfC.mjs} +541 -120
  21. package/.output/server/{_tanstack-start-manifest_v-Dde-eACV.mjs → _tanstack-start-manifest_v-BoTPaB-V.mjs} +1 -1
  22. package/.output/server/index.mjs +62 -62
  23. package/package.json +1 -1
  24. package/src/components/groups/GroupsDialog.tsx +9 -4
  25. package/src/components/providers/SettingsDialog.tsx +20 -0
  26. package/src/proxy/sessionArchive.ts +410 -0
  27. package/src/proxy/store.ts +121 -6
  28. package/src/routes/api/logs.ts +2 -0
  29. package/src/routes/api/sessions.ts +2 -2
  30. package/.output/public/assets/_sessionId-mnr0O0Ri.js +0 -1
  31. package/.output/public/assets/index-DH1DjJOl.js +0 -1
@@ -0,0 +1,410 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir, rm } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { CapturedLogSchema, type CapturedLog } from "./schemas";
6
+ import { logger, resolveLogDir } from "./logger";
7
+
8
+ type SqliteConstructorResult = {
9
+ constructor: ((path: string) => unknown) | null;
10
+ reason: string | null;
11
+ };
12
+
13
+ const SESSION_ARCHIVE_DIR = "sessions";
14
+ const SESSION_ARCHIVE_FILE = "session.sqlite";
15
+ const SQLITE_PACKAGE_NAME = "better-sqlite3";
16
+ const BUN_SQLITE_MODULE_NAME = "bun:sqlite";
17
+
18
+ const CREATE_SCHEMA_SQL = `
19
+ CREATE TABLE IF NOT EXISTS session_logs (
20
+ id INTEGER PRIMARY KEY,
21
+ session_id TEXT NOT NULL,
22
+ log_json TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+ CREATE INDEX IF NOT EXISTS idx_session_logs_session_id_id ON session_logs(session_id, id);
26
+ `;
27
+
28
+ const UPSERT_SQL = `
29
+ INSERT INTO session_logs (
30
+ id,
31
+ session_id,
32
+ log_json,
33
+ updated_at
34
+ ) VALUES (?, ?, ?, ?)
35
+ ON CONFLICT(id) DO UPDATE SET
36
+ session_id = excluded.session_id,
37
+ log_json = excluded.log_json,
38
+ updated_at = excluded.updated_at
39
+ `;
40
+
41
+ let didLogUnavailable = false;
42
+
43
+ export function getSessionArchiveRoot(): string {
44
+ return join(resolveLogDir(), SESSION_ARCHIVE_DIR);
45
+ }
46
+
47
+ function hashSessionId(sessionId: string): string {
48
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
49
+ }
50
+
51
+ function sanitizeSessionPrefix(sessionId: string): string {
52
+ const trimmed = sessionId.trim();
53
+ if (trimmed.length === 0) return "session";
54
+ const normalized = trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
55
+ if (normalized.length === 0) return "session";
56
+ return normalized.slice(0, 48);
57
+ }
58
+
59
+ function sessionArchiveKey(sessionId: string): string {
60
+ return `${sanitizeSessionPrefix(sessionId)}--${hashSessionId(sessionId)}`;
61
+ }
62
+
63
+ export function getSessionArchivePath(sessionId: string): string {
64
+ return join(getSessionArchiveRoot(), sessionArchiveKey(sessionId), SESSION_ARCHIVE_FILE);
65
+ }
66
+
67
+ function objectFromUnknown(value: unknown): object | null {
68
+ if (value === null) return null;
69
+ if (typeof value === "object" || typeof value === "function") return value;
70
+ return null;
71
+ }
72
+
73
+ function readProperty(value: unknown, name: string): unknown {
74
+ let current = objectFromUnknown(value);
75
+ while (current !== null) {
76
+ const desc = Object.getOwnPropertyDescriptor(current, name);
77
+ if (desc !== undefined) {
78
+ const propertyValue: unknown = Reflect.get(current, name);
79
+ return propertyValue;
80
+ }
81
+ const nextPrototype: unknown = Object.getPrototypeOf(current);
82
+ current = objectFromUnknown(nextPrototype);
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ function readFunction(value: unknown, name: string): Function | null {
88
+ const property = readProperty(value, name);
89
+ return typeof property === "function" ? property : null;
90
+ }
91
+
92
+ function callMethod(target: unknown, methodName: string, args: readonly unknown[]): unknown {
93
+ const method = readFunction(target, methodName);
94
+ if (method === null) return undefined;
95
+ try {
96
+ return Reflect.apply(method, target, [...args]);
97
+ } catch (err) {
98
+ logger.warn("[sessionArchive] SQLite method failed:", methodName, String(err));
99
+ return undefined;
100
+ }
101
+ }
102
+
103
+ function prepareStatement(db: unknown, sql: string): unknown {
104
+ return callMethod(db, "prepare", [sql]);
105
+ }
106
+
107
+ function runPrepared(db: unknown, sql: string, params: readonly unknown[] = []): boolean {
108
+ const statement = prepareStatement(db, sql);
109
+ if (statement === undefined) return false;
110
+ const result = callMethod(statement, "run", params);
111
+ callMethod(statement, "finalize", []);
112
+ return result !== undefined;
113
+ }
114
+
115
+ function allPrepared(db: unknown, sql: string, params: readonly unknown[] = []): unknown[] {
116
+ const statement = prepareStatement(db, sql);
117
+ if (statement === undefined) return [];
118
+ const result = callMethod(statement, "all", params);
119
+ callMethod(statement, "finalize", []);
120
+ return Array.isArray(result) ? result : [];
121
+ }
122
+
123
+ function execSql(db: unknown, sql: string): boolean {
124
+ const result = callMethod(db, "exec", [sql]);
125
+ return result !== undefined;
126
+ }
127
+
128
+ function closeDatabase(db: unknown): void {
129
+ callMethod(db, "close", []);
130
+ }
131
+
132
+ function sqliteConstructorFromModule(
133
+ moduleValue: unknown,
134
+ exportName: "default" | "Database",
135
+ ): ((path: string) => unknown) | null {
136
+ const exportedValue = readProperty(moduleValue, exportName);
137
+ if (typeof exportedValue === "function") {
138
+ return (path: string): unknown => Reflect.construct(exportedValue, [path]);
139
+ }
140
+ return null;
141
+ }
142
+
143
+ async function loadSqliteModuleConstructor(
144
+ moduleName: string,
145
+ exportName: "default" | "Database",
146
+ ): Promise<SqliteConstructorResult> {
147
+ try {
148
+ const importedModule: unknown = await import(/* @vite-ignore */ moduleName);
149
+ const exportedConstructor = sqliteConstructorFromModule(importedModule, exportName);
150
+ if (exportedConstructor !== null) return { constructor: exportedConstructor, reason: null };
151
+ if (exportName === "default" && typeof importedModule === "function") {
152
+ return {
153
+ constructor: (path: string): unknown => Reflect.construct(importedModule, [path]),
154
+ reason: null,
155
+ };
156
+ }
157
+ return { constructor: null, reason: `${moduleName} did not expose ${exportName}` };
158
+ } catch (err) {
159
+ return { constructor: null, reason: String(err) };
160
+ }
161
+ }
162
+
163
+ async function loadSqliteConstructor(): Promise<SqliteConstructorResult> {
164
+ const bunRuntimeDesc = Object.getOwnPropertyDescriptor(process.versions, "bun");
165
+ if (bunRuntimeDesc !== undefined) {
166
+ const bunSqlite = await loadSqliteModuleConstructor(BUN_SQLITE_MODULE_NAME, "Database");
167
+ if (bunSqlite.constructor !== null) return bunSqlite;
168
+
169
+ const betterSqlite = await loadSqliteModuleConstructor(SQLITE_PACKAGE_NAME, "default");
170
+ if (betterSqlite.constructor !== null) return betterSqlite;
171
+
172
+ return {
173
+ constructor: null,
174
+ reason: [bunSqlite.reason, betterSqlite.reason]
175
+ .filter((item): item is string => item !== null)
176
+ .join("; "),
177
+ };
178
+ }
179
+
180
+ const betterSqlite = await loadSqliteModuleConstructor(SQLITE_PACKAGE_NAME, "default");
181
+ if (betterSqlite.constructor !== null) return betterSqlite;
182
+
183
+ const bunSqlite = await loadSqliteModuleConstructor(BUN_SQLITE_MODULE_NAME, "Database");
184
+ if (bunSqlite.constructor !== null) return bunSqlite;
185
+
186
+ return {
187
+ constructor: null,
188
+ reason: [betterSqlite.reason, bunSqlite.reason]
189
+ .filter((item): item is string => item !== null)
190
+ .join("; "),
191
+ };
192
+ }
193
+
194
+ function logUnavailableOnce(reason: string): void {
195
+ if (didLogUnavailable) return;
196
+ didLogUnavailable = true;
197
+ logger.warn("[sessionArchive] Optional session archive unavailable:", reason);
198
+ }
199
+
200
+ async function openSessionArchiveDb(filePath: string, create: boolean): Promise<unknown | null> {
201
+ if (!create && !existsSync(filePath)) return null;
202
+
203
+ const sqliteConstructor = await loadSqliteConstructor();
204
+ if (sqliteConstructor.constructor === null) {
205
+ logUnavailableOnce(sqliteConstructor.reason ?? "SQLite runtime is not available");
206
+ return null;
207
+ }
208
+
209
+ try {
210
+ if (create) await mkdir(dirname(filePath), { recursive: true });
211
+ const db = sqliteConstructor.constructor(filePath);
212
+ execSql(db, "PRAGMA journal_mode = WAL");
213
+ execSql(db, "PRAGMA synchronous = NORMAL");
214
+ if (!execSql(db, CREATE_SCHEMA_SQL)) {
215
+ closeDatabase(db);
216
+ logger.warn("[sessionArchive] Failed to initialize schema:", filePath);
217
+ return null;
218
+ }
219
+ return db;
220
+ } catch (err) {
221
+ logger.warn("[sessionArchive] Failed to open session archive:", filePath, String(err));
222
+ return null;
223
+ }
224
+ }
225
+
226
+ export async function archiveSessionLog(log: CapturedLog): Promise<boolean> {
227
+ const sessionId = log.sessionId;
228
+ if (sessionId === null || sessionId.trim().length === 0) return false;
229
+
230
+ const db = await openSessionArchiveDb(getSessionArchivePath(sessionId), true);
231
+ if (db === null) return false;
232
+
233
+ try {
234
+ return runPrepared(db, UPSERT_SQL, [
235
+ log.id,
236
+ sessionId,
237
+ JSON.stringify(log),
238
+ new Date().toISOString(),
239
+ ]);
240
+ } finally {
241
+ closeDatabase(db);
242
+ }
243
+ }
244
+
245
+ function readString(row: unknown, name: string): string | null {
246
+ const value = readProperty(row, name);
247
+ return typeof value === "string" ? value : null;
248
+ }
249
+
250
+ function readNumber(row: unknown, name: string): number | null {
251
+ const value = readProperty(row, name);
252
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
253
+ }
254
+
255
+ function logFromJson(value: string | null): CapturedLog | null {
256
+ if (value === null) return null;
257
+ try {
258
+ const parsed: unknown = JSON.parse(value);
259
+ const result = CapturedLogSchema.safeParse(parsed);
260
+ return result.success ? result.data : null;
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ function logsFromRows(rows: readonly unknown[]): CapturedLog[] {
267
+ const logs: CapturedLog[] = [];
268
+ for (const row of rows) {
269
+ const log = logFromJson(readString(row, "log_json"));
270
+ if (log !== null) logs.push(log);
271
+ }
272
+ return logs;
273
+ }
274
+
275
+ export async function listArchivedSessionLogs(sessionId: string): Promise<CapturedLog[]> {
276
+ const db = await openSessionArchiveDb(getSessionArchivePath(sessionId), false);
277
+ if (db === null) return [];
278
+
279
+ try {
280
+ const rows = allPrepared(
281
+ db,
282
+ "SELECT log_json FROM session_logs WHERE session_id = ? ORDER BY id ASC",
283
+ [sessionId],
284
+ );
285
+ return logsFromRows(rows);
286
+ } finally {
287
+ closeDatabase(db);
288
+ }
289
+ }
290
+
291
+ export async function listArchivedSessionIds(): Promise<string[]> {
292
+ const archivePaths = await listArchiveDatabasePaths();
293
+ const sessionIds = new Set<string>();
294
+ for (const archivePath of archivePaths) {
295
+ const db = await openSessionArchiveDb(archivePath, false);
296
+ if (db === null) continue;
297
+
298
+ try {
299
+ const rows = allPrepared(
300
+ db,
301
+ "SELECT DISTINCT session_id FROM session_logs ORDER BY session_id ASC",
302
+ );
303
+ for (const row of rows) {
304
+ const sessionId = readString(row, "session_id");
305
+ if (sessionId !== null && sessionId.length > 0) sessionIds.add(sessionId);
306
+ }
307
+ } finally {
308
+ closeDatabase(db);
309
+ }
310
+ }
311
+ return [...sessionIds].sort();
312
+ }
313
+
314
+ async function listArchiveDatabasePaths(): Promise<string[]> {
315
+ const root = getSessionArchiveRoot();
316
+ if (!existsSync(root)) return [];
317
+
318
+ try {
319
+ const entries = await readdir(root, { withFileTypes: true });
320
+ const paths: string[] = [];
321
+ for (const entry of entries) {
322
+ if (!entry.isDirectory()) continue;
323
+ const candidate = join(root, entry.name, SESSION_ARCHIVE_FILE);
324
+ if (existsSync(candidate)) paths.push(candidate);
325
+ }
326
+ return paths;
327
+ } catch (err) {
328
+ logger.warn("[sessionArchive] Failed to list session archives:", String(err));
329
+ return [];
330
+ }
331
+ }
332
+
333
+ export async function findArchivedLogById(id: number): Promise<CapturedLog | null> {
334
+ const archivePaths = await listArchiveDatabasePaths();
335
+ for (const archivePath of archivePaths) {
336
+ const db = await openSessionArchiveDb(archivePath, false);
337
+ if (db === null) continue;
338
+
339
+ try {
340
+ const rows = allPrepared(db, "SELECT log_json FROM session_logs WHERE id = ? LIMIT 1", [id]);
341
+ const first = logsFromRows(rows)[0];
342
+ if (first !== undefined) return first;
343
+ } finally {
344
+ closeDatabase(db);
345
+ }
346
+ }
347
+
348
+ return null;
349
+ }
350
+
351
+ function placeholders(count: number): string {
352
+ return Array.from({ length: count }, () => "?").join(", ");
353
+ }
354
+
355
+ function idsFromRows(rows: readonly unknown[]): Set<number> {
356
+ const ids = new Set<number>();
357
+ for (const row of rows) {
358
+ const id = readNumber(row, "id");
359
+ if (id !== null && Number.isInteger(id)) ids.add(id);
360
+ }
361
+ return ids;
362
+ }
363
+
364
+ export async function deleteArchivedLogsByIds(ids: ReadonlySet<number>): Promise<Set<number>> {
365
+ if (ids.size === 0) return new Set();
366
+
367
+ const removedIds = new Set<number>();
368
+ const idValues = [...ids];
369
+ const sqlPlaceholders = placeholders(idValues.length);
370
+ const archivePaths = await listArchiveDatabasePaths();
371
+
372
+ for (const archivePath of archivePaths) {
373
+ const db = await openSessionArchiveDb(archivePath, false);
374
+ if (db === null) continue;
375
+
376
+ try {
377
+ const matchingRows = allPrepared(
378
+ db,
379
+ `SELECT id FROM session_logs WHERE id IN (${sqlPlaceholders})`,
380
+ idValues,
381
+ );
382
+ const matchingIds = idsFromRows(matchingRows);
383
+ if (matchingIds.size === 0) continue;
384
+
385
+ if (runPrepared(db, `DELETE FROM session_logs WHERE id IN (${sqlPlaceholders})`, idValues)) {
386
+ for (const id of matchingIds) {
387
+ removedIds.add(id);
388
+ }
389
+ }
390
+ } finally {
391
+ closeDatabase(db);
392
+ }
393
+ }
394
+
395
+ return removedIds;
396
+ }
397
+
398
+ export async function clearSessionArchives(): Promise<number> {
399
+ const root = getSessionArchiveRoot();
400
+ if (!existsSync(root)) return 0;
401
+
402
+ const archivePaths = await listArchiveDatabasePaths();
403
+ try {
404
+ await rm(root, { recursive: true, force: true });
405
+ return archivePaths.length;
406
+ } catch (err) {
407
+ logger.warn("[sessionArchive] Failed to clear session archives:", String(err));
408
+ return 0;
409
+ }
410
+ }