@mbeato/contextscope 0.1.4 → 0.1.6

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 (45) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  4. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/standalone/.next/server/app/context/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/items/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/.next/server/app/page.js +2 -2
  22. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0j40w4k._.js +1 -1
  26. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p2sxww._.js +1 -1
  27. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q3rzj9._.js +3 -0
  28. package/.next/standalone/.next/server/chunks/ssr/app_page_tsx_0fwe3kl._.js +3 -0
  29. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  30. package/.next/standalone/.next/server/pages/404.html +1 -1
  31. package/.next/standalone/.next/server/pages/500.html +1 -1
  32. package/.next/standalone/app/page.tsx +291 -258
  33. package/.next/standalone/bin/cli.js +22 -6
  34. package/.next/standalone/lib/transcripts.ts +103 -41
  35. package/.next/standalone/package.json +1 -1
  36. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  37. package/.next/static/chunks/0pb14~6.l8.f9.css +1 -0
  38. package/bin/cli.js +22 -6
  39. package/package.json +1 -1
  40. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lbywin._.js +0 -3
  41. package/.next/standalone/.next/server/chunks/ssr/_0j0avc7._.js +0 -3
  42. package/.next/static/chunks/118uk9v3812u1.css +0 -1
  43. /package/.next/static/{kaNzMZNco9qjeyEFA-gjr → uI28k7hBjH7vKGE3jdIaM}/_buildManifest.js +0 -0
  44. /package/.next/static/{kaNzMZNco9qjeyEFA-gjr → uI28k7hBjH7vKGE3jdIaM}/_clientMiddlewareManifest.js +0 -0
  45. /package/.next/static/{kaNzMZNco9qjeyEFA-gjr → uI28k7hBjH7vKGE3jdIaM}/_ssgManifest.js +0 -0
@@ -78,8 +78,18 @@ function inferProjectPath(dirName: string): string {
78
78
 
79
79
  async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptResult> {
80
80
  const parts = filePath.split("/");
81
- const sessionId = parts[parts.length - 1].replace(/\.jsonl$/, "");
82
- const project = parts[parts.length - 2] ?? "";
81
+ // Two shapes:
82
+ // <project>/<session-uuid>.jsonl → main session file
83
+ // <project>/<session-uuid>/subagents/agent-<id>.jsonl → subagent file
84
+ // For subagents we want the parent session UUID + parent project, so the
85
+ // tokens roll up to the same Session as the main file.
86
+ const isSubagent = parts[parts.length - 2] === "subagents";
87
+ const sessionId = isSubagent
88
+ ? (parts[parts.length - 3] ?? "")
89
+ : parts[parts.length - 1].replace(/\.jsonl$/, "");
90
+ const project = isSubagent
91
+ ? (parts[parts.length - 4] ?? "")
92
+ : (parts[parts.length - 2] ?? "");
83
93
  const result: TranscriptResult = {
84
94
  filePath,
85
95
  mtimeMs,
@@ -227,30 +237,62 @@ async function pMapLimit<T, R>(items: T[], limit: number, fn: (x: T) => Promise<
227
237
  }
228
238
 
229
239
  /**
230
- * Walk raw per-file results oldest first; attribute each unique message UUID
231
- * to the earliest file that mentions it. Returns new TranscriptResult objects
232
- * with totals/byModel/timestamps recomputed from the surviving records.
240
+ * Walk raw per-file results oldest first, dedup messages globally by
241
+ * (msg.id, requestId), and merge multiple files belonging to the same logical
242
+ * session (main + subagents/*) into one TranscriptResult per session.
233
243
  *
234
- * Important: do NOT mutate the cached `raws` objects — clone fields we rewrite.
244
+ * Important: do NOT mutate the cached `raws` objects — they're shared across
245
+ * calls.
235
246
  */
236
247
  function dedupAndSum(raws: TranscriptResult[]): TranscriptResult[] {
237
248
  const sorted = [...raws].sort((a, b) => a.mtimeMs - b.mtimeMs);
238
249
  const seen = new Set<string>();
239
- return sorted.map((r) => {
240
- const t: TranscriptResult = {
241
- ...r,
242
- startTime: 0,
243
- endTime: 0,
244
- turnCount: 0,
245
- inputTokens: 0,
246
- cacheReadTokens: 0,
247
- cacheCreationTokens: 0,
248
- outputTokens: 0,
249
- byModel: {},
250
- models: [],
251
- sidechainTurns: 0,
252
- };
253
- const modelSet = new Set<string>();
250
+ const merged = new Map<string, TranscriptResult>();
251
+
252
+ for (const r of sorted) {
253
+ const key = `${r.project}\x00${r.sessionId}`;
254
+ let t = merged.get(key);
255
+ if (!t) {
256
+ t = {
257
+ filePath: r.filePath,
258
+ mtimeMs: r.mtimeMs,
259
+ sessionId: r.sessionId,
260
+ project: r.project,
261
+ projectPath: r.projectPath,
262
+ startTime: 0,
263
+ endTime: 0,
264
+ turnCount: 0,
265
+ models: [],
266
+ inputTokens: 0,
267
+ cacheReadTokens: 0,
268
+ cacheCreationTokens: 0,
269
+ outputTokens: 0,
270
+ byModel: {},
271
+ usageRecords: [],
272
+ invocations: [],
273
+ toolCalls: {},
274
+ toolErrors: 0,
275
+ sidechainTurns: 0,
276
+ };
277
+ merged.set(key, t);
278
+ } else {
279
+ // Prefer the main session file's path/mtime as the canonical reference;
280
+ // a subagent file landed first only if no main yet, which is rare.
281
+ const incomingIsMain = !r.filePath.includes("/subagents/");
282
+ if (incomingIsMain) {
283
+ t.filePath = r.filePath;
284
+ t.mtimeMs = Math.max(t.mtimeMs, r.mtimeMs);
285
+ }
286
+ }
287
+
288
+ // Accumulate ancillary fields
289
+ for (const inv of r.invocations) t.invocations.push(inv);
290
+ for (const [name, n] of Object.entries(r.toolCalls)) {
291
+ t.toolCalls[name] = (t.toolCalls[name] ?? 0) + n;
292
+ }
293
+ t.toolErrors += r.toolErrors;
294
+
295
+ const modelSet = new Set<string>(t.models);
254
296
  for (const u of r.usageRecords) {
255
297
  if (u.dedupKey) {
256
298
  if (seen.has(u.dedupKey)) continue;
@@ -282,11 +324,47 @@ function dedupAndSum(raws: TranscriptResult[]): TranscriptResult[] {
282
324
  }
283
325
  }
284
326
  t.models = [...modelSet];
285
- return t;
286
- });
327
+ }
328
+
329
+ return [...merged.values()];
287
330
  }
288
331
 
289
- /** Scan all transcripts modified in the last N days. Deduped across resumed sessions. */
332
+ async function collectJsonlFiles(
333
+ dir: string,
334
+ cutoff: number,
335
+ out: { filePath: string; mtimeMs: number }[],
336
+ depth: number = 0
337
+ ): Promise<void> {
338
+ if (depth > 3) return; // <project>/<session>/subagents/<file> is the deepest expected shape
339
+ let entries;
340
+ try {
341
+ entries = await readdir(dir, { withFileTypes: true });
342
+ } catch {
343
+ return;
344
+ }
345
+ for (const e of entries) {
346
+ const fp = join(dir, e.name);
347
+ if (e.isDirectory()) {
348
+ await collectJsonlFiles(fp, cutoff, out, depth + 1);
349
+ continue;
350
+ }
351
+ if (!e.isFile() || !e.name.endsWith(".jsonl")) continue;
352
+ try {
353
+ const st = await stat(fp);
354
+ if (st.mtimeMs >= cutoff) out.push({ filePath: fp, mtimeMs: st.mtimeMs });
355
+ } catch {
356
+ // skip
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Scan all transcripts modified in the last N days. Scans both main session
363
+ * files (<project>/<session>.jsonl) and subagent files
364
+ * (<project>/<session>/subagents/agent-*.jsonl), then attributes subagent
365
+ * tokens to their parent session via shared sessionId. Deduped globally by
366
+ * (msg.id, requestId).
367
+ */
290
368
  export async function getAllTranscripts(daysBack: number = 30): Promise<TranscriptResult[]> {
291
369
  const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1000;
292
370
  let projDirs: import("node:fs").Dirent[];
@@ -299,23 +377,7 @@ export async function getAllTranscripts(daysBack: number = 30): Promise<Transcri
299
377
  await Promise.all(
300
378
  projDirs.map(async (d) => {
301
379
  if (!d.isDirectory()) return;
302
- const dir = join(PROJECTS_DIR, String(d.name));
303
- let entries;
304
- try {
305
- entries = await readdir(dir, { withFileTypes: true });
306
- } catch {
307
- return;
308
- }
309
- for (const e of entries) {
310
- if (!e.isFile() || !e.name.endsWith(".jsonl")) continue;
311
- const fp = join(dir, e.name);
312
- try {
313
- const st = await stat(fp);
314
- if (st.mtimeMs >= cutoff) candidates.push({ filePath: fp, mtimeMs: st.mtimeMs });
315
- } catch {
316
- // skip
317
- }
318
- }
380
+ await collectJsonlFiles(join(PROJECTS_DIR, String(d.name)), cutoff, candidates);
319
381
  })
320
382
  );
321
383
  const raws = await pMapLimit(candidates, 16, (c) => getFileResult(c.filePath, c.mtimeMs));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mbeato/contextscope",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local dashboard auditing Claude Code's per-turn token context (skills, agents, commands, CLAUDE.md, MEMORY.md, hooks, MCP) with toggle-based disable and session analytics.",
5
5
  "type": "module",
6
6
  "bin": {