agent-dag 1.12.0 → 1.14.2

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.
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agent-dag</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-CMpk9bSy.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Ch4BS-lV.css">
8
+ <script type="module" crossorigin src="/assets/index-CkZuDTIx.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Bwj0xSAo.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.12.0",
3
+ "version": "1.14.2",
4
4
  "description": "Live DAG of Claude Code agents — watch parallel subagents fork, call tools, and return on one calm canvas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -198,6 +198,145 @@ function maybeResolveUsage(payload) {
198
198
  .finally(() => pendingUsageReads.delete(sid));
199
199
  }
200
200
 
201
+ // ─── Context enrichment ──────────────────────────────────────────────────
202
+ // Approximation of `/context` since CC doesn't expose its breakdown via
203
+ // hooks. We scan the transcript JSONL for message counts (user / assistant
204
+ // / tool_use / tool_result / system-reminders) and walk up from cwd for
205
+ // any CLAUDE.md files in scope. Token totals come from UsageObserved; this
206
+ // scan is purely structural ("what does the context contain").
207
+ const lastContextReadAt = new Map();
208
+ const pendingContextReads = new Set();
209
+ const CONTEXT_READ_THROTTLE_MS = 4000;
210
+
211
+ async function readContextFromTranscript(path) {
212
+ try {
213
+ const s = await stat(path);
214
+ if (s.size === 0) return null;
215
+ const fh = await open(path, "r");
216
+ let buf;
217
+ try { buf = Buffer.alloc(s.size); await fh.read(buf, 0, s.size, 0); }
218
+ finally { await fh.close(); }
219
+ const text = buf.toString("utf8");
220
+ const breakdown = {
221
+ msgsUser: 0,
222
+ msgsAssistant: 0,
223
+ toolUses: 0,
224
+ toolResults: 0,
225
+ systemReminders: 0,
226
+ currentContextTokens: 0,
227
+ };
228
+ breakdown.msgsUser = (text.match(/"type"\s*:\s*"user"/g) ?? []).length;
229
+ breakdown.msgsAssistant = (text.match(/"type"\s*:\s*"assistant"/g) ?? []).length;
230
+ breakdown.toolUses = (text.match(/"type"\s*:\s*"tool_use"/g) ?? []).length;
231
+ breakdown.toolResults = (text.match(/"type"\s*:\s*"tool_result"/g) ?? []).length;
232
+ breakdown.systemReminders = (text.match(/<system-reminder>/g) ?? []).length;
233
+ // Current context size = input + cache_read + cache_create on the LAST
234
+ // usage block. Each assistant message's usage describes the context
235
+ // window for THAT call; summing across calls double-counts cached
236
+ // prefixes and explodes past the real ceiling. Take the most recent.
237
+ const re = /"usage"\s*:\s*\{([^}]+)\}/g;
238
+ const grab = (blob, key) => {
239
+ const km = blob.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`));
240
+ return km ? Number(km[1]) : 0;
241
+ };
242
+ let lastBlob = null;
243
+ for (const m of text.matchAll(re)) lastBlob = m[1];
244
+ if (lastBlob) {
245
+ breakdown.currentContextTokens =
246
+ grab(lastBlob, "input_tokens") +
247
+ grab(lastBlob, "cache_read_input_tokens") +
248
+ grab(lastBlob, "cache_creation_input_tokens");
249
+ }
250
+ return breakdown;
251
+ } catch { return null; }
252
+ }
253
+
254
+ /** Encode an absolute path the way CC stores it under
255
+ * ~/.claude/projects/<slug>/. Drive letters, colons, and path separators
256
+ * are flattened to "-" so the slug survives as a single directory name. */
257
+ function ccProjectSlug(cwd) {
258
+ if (!cwd) return "";
259
+ // Replace path separators and the Windows drive colon. Match CC's own
260
+ // encoding: every \\ / : → - (no collapsing of adjacent dashes).
261
+ return resolve(cwd).replace(/[\\/:]/g, "-");
262
+ }
263
+
264
+ async function scanClaudeMdFiles(cwd) {
265
+ if (!cwd || typeof cwd !== "string") return [];
266
+ const found = [];
267
+ const seen = new Set();
268
+ const home = homedir();
269
+ const push = async (p) => {
270
+ if (seen.has(p)) return;
271
+ seen.add(p);
272
+ try {
273
+ const s = await stat(p);
274
+ if (s.isFile() && s.size > 0) found.push({ path: p, bytes: s.size });
275
+ } catch {}
276
+ };
277
+ // Walk up from cwd to filesystem root. At each dir, check for the
278
+ // canonical CC memory filenames plus CLAUDE.local.md (user-private).
279
+ let dir = resolve(cwd);
280
+ for (let depth = 0; depth < 16; depth++) {
281
+ for (const rel of [
282
+ "CLAUDE.md",
283
+ "CLAUDE.local.md",
284
+ join(".claude", "CLAUDE.md"),
285
+ join(".claude", "CLAUDE.local.md"),
286
+ ]) {
287
+ await push(join(dir, rel));
288
+ }
289
+ const parent = pdirname(dir);
290
+ if (parent === dir) break;
291
+ dir = parent;
292
+ }
293
+ // User-global memory.
294
+ await push(join(home, ".claude", "CLAUDE.md"));
295
+ await push(join(home, ".claude", "CLAUDE.local.md"));
296
+ // Per-project auto-memory: ~/.claude/projects/<slug>/memory/*.md
297
+ // (plus MEMORY.md index). CC injects these into context for sessions
298
+ // whose cwd matches the slug.
299
+ const slug = ccProjectSlug(cwd);
300
+ if (slug) {
301
+ const memDir = join(home, ".claude", "projects", slug, "memory");
302
+ try {
303
+ const entries = await readdir(memDir);
304
+ for (const f of entries) {
305
+ if (f.toLowerCase().endsWith(".md")) await push(join(memDir, f));
306
+ }
307
+ } catch {}
308
+ }
309
+ return found;
310
+ }
311
+
312
+ function maybeResolveContext(payload) {
313
+ if (!payload || typeof payload !== "object") return;
314
+ const sid = payload.session_id;
315
+ const tp = payload.transcript_path;
316
+ const cwd = payload.cwd;
317
+ if (!sid || !tp) return;
318
+ if (pendingContextReads.has(sid)) return;
319
+ const now = Date.now();
320
+ const last = lastContextReadAt.get(sid) ?? 0;
321
+ if (now - last < CONTEXT_READ_THROTTLE_MS) return;
322
+ lastContextReadAt.set(sid, now);
323
+ pendingContextReads.add(sid);
324
+ Promise.all([readContextFromTranscript(tp), scanClaudeMdFiles(cwd)])
325
+ .then(([breakdown, claudeMdFiles]) => {
326
+ if (!breakdown && (!claudeMdFiles || claudeMdFiles.length === 0)) return;
327
+ pushEvent({
328
+ hook_event_name: "ContextObserved",
329
+ session_id: sid,
330
+ context: {
331
+ ...(breakdown ?? {}),
332
+ claudeMdFiles: claudeMdFiles ?? [],
333
+ },
334
+ }, "internal");
335
+ })
336
+ .catch(() => {})
337
+ .finally(() => pendingContextReads.delete(sid));
338
+ }
339
+
201
340
  function pushEvent(raw, source, opts = {}) {
202
341
  // Synchronous enrichment: if we already know this session's model, stamp
203
342
  // it on the payload so the client's recursive scanner picks it up.
@@ -235,6 +374,7 @@ function pushEvent(raw, source, opts = {}) {
235
374
  if (source === "hook" && !opts.replay) {
236
375
  maybeResolveModel(raw);
237
376
  maybeResolveUsage(raw);
377
+ maybeResolveContext(raw);
238
378
  }
239
379
 
240
380
  return evt;