agenthud 0.8.3 → 0.8.5

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 CHANGED
@@ -130,6 +130,23 @@ Output is written to stdout in Markdown format:
130
130
  **`--include` types:** `response`, `bash`, `edit`, `thinking`, `read`, `glob`, `user`
131
131
  Default: `response,bash,edit,thinking`
132
132
 
133
+ ## Summary
134
+
135
+ Generate an LLM-based summary of a day's activity using the `claude` CLI:
136
+
137
+ ```bash
138
+ agenthud summary # today
139
+ agenthud summary --date 2026-05-14 # past date (cached on second run)
140
+ agenthud summary --date 2026-05-14 --force # ignore cache
141
+ agenthud summary --prompt "커밋만 요약해" # override prompt
142
+ ```
143
+
144
+ Results are saved to `~/.agenthud/summaries/YYYY-MM-DD.md`. Past dates are cached and returned instantly on re-run. Today's date is always regenerated since activity is still growing.
145
+
146
+ **Prompt customization:** The summary uses `~/.agenthud/summary-prompt.md`, which is auto-created from a built-in template on first run. Edit it freely or override per-call with `--prompt`.
147
+
148
+ **Requires:** [`@anthropic-ai/claude-code`](https://www.npmjs.com/package/@anthropic-ai/claude-code) installed and authenticated (`npm i -g @anthropic-ai/claude-code`).
149
+
133
150
  ## Configuration
134
151
 
135
152
  Optional. Create `~/.agenthud/config.yaml`:
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ Error: Node.js ${MIN_NODE_VERSION}+ is required (current: ${process.version})
14
14
  console.error(" https://nodejs.org/\n");
15
15
  process.exit(1);
16
16
  }
17
- import("./main-VPGVYRCR.js");
17
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
18
+ import("./main-LOBTW45O.js");
@@ -1,6 +1,6 @@
1
1
  // src/main.ts
2
- import { existsSync as existsSync5, rmSync } from "fs";
3
- import { join as join5 } from "path";
2
+ import { existsSync as existsSync6, rmSync } from "fs";
3
+ import { join as join6 } from "path";
4
4
  import { createInterface } from "readline";
5
5
  import { render } from "ink";
6
6
  import React from "react";
@@ -28,8 +28,15 @@ var KNOWN_WATCH_FLAGS = /* @__PURE__ */ new Set([
28
28
  "-h",
29
29
  "--help"
30
30
  ]);
31
- var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set(["--date", "--include", "--format"]);
32
- var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report"]);
31
+ var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set([
32
+ "--date",
33
+ "--include",
34
+ "--format",
35
+ "--detail-limit",
36
+ "--with-git"
37
+ ]);
38
+ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set(["--date", "--prompt", "--force"]);
39
+ var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report", "summary"]);
33
40
  function getHelp() {
34
41
  return `Usage: agenthud [options]
35
42
 
@@ -42,13 +49,21 @@ Options:
42
49
  -h, --help Show this help message
43
50
 
44
51
  Commands:
45
- report [--date DATE] [--include TYPES]
52
+ report [--date DATE] [--include TYPES] [--format FORMAT] [--detail-limit N] [--with-git]
46
53
  Print activity report for a date (default: today)
47
54
  --date YYYY-MM-DD|today Date to report on
48
55
  --include TYPES Comma-separated types or "all"
49
56
  Types: response,bash,edit,thinking,read,glob,user
50
57
  Default: response,bash,edit,thinking
51
58
  --format FORMAT Output format: markdown (default) or json
59
+ --detail-limit N Max chars per activity detail (default: 120, 0 = unlimited)
60
+ --with-git Append today's git commits from cwd to report
61
+
62
+ summary [--date DATE] [--prompt TEXT] [--force]
63
+ Generate LLM summary of daily activity via claude CLI
64
+ --date YYYY-MM-DD|today Date to summarize (default: today)
65
+ --prompt TEXT Override prompt for this run
66
+ --force Regenerate even if cached (past dates)
52
67
 
53
68
  Environment:
54
69
  CLAUDE_PROJECTS_DIR Path to Claude projects directory
@@ -140,14 +155,75 @@ function parseArgs(args) {
140
155
  reportError = "Invalid format: missing value for --format.";
141
156
  }
142
157
  }
158
+ let reportDetailLimit;
159
+ const detailLimitIdx = rest.indexOf("--detail-limit");
160
+ if (detailLimitIdx !== -1) {
161
+ const val = rest[detailLimitIdx + 1];
162
+ const n = Number(val);
163
+ if (!val || Number.isNaN(n) || n < 0 || !Number.isInteger(n)) {
164
+ reportError = `Invalid --detail-limit: "${val}". Must be a non-negative integer.`;
165
+ } else {
166
+ reportDetailLimit = n;
167
+ }
168
+ }
169
+ const reportWithGit = rest.includes("--with-git");
143
170
  return {
144
171
  mode: "report",
145
172
  reportDate,
146
173
  reportInclude,
147
174
  reportFormat,
175
+ reportDetailLimit,
176
+ reportWithGit,
148
177
  reportError
149
178
  };
150
179
  }
180
+ if (args[0] === "summary") {
181
+ const rest = args.slice(1);
182
+ let summaryDate = todayLocalMidnight();
183
+ let summaryPrompt;
184
+ let summaryForce = false;
185
+ let summaryError;
186
+ for (let i = 0; i < rest.length; i++) {
187
+ const arg = rest[i];
188
+ if (!arg.startsWith("-")) continue;
189
+ if (!KNOWN_SUMMARY_FLAGS.has(arg)) {
190
+ summaryError = `Unknown option: "${arg}". Run agenthud --help for usage.`;
191
+ break;
192
+ }
193
+ if (arg === "--date" || arg === "--prompt") i++;
194
+ }
195
+ const dateIdx = rest.indexOf("--date");
196
+ if (dateIdx !== -1) {
197
+ const dateStr = rest[dateIdx + 1];
198
+ if (!dateStr) {
199
+ summaryError = "Invalid date: missing value for --date";
200
+ } else {
201
+ const parsed = parseLocalMidnight(dateStr);
202
+ if (!parsed) {
203
+ summaryError = `Invalid date: "${dateStr}". Use YYYY-MM-DD or "today".`;
204
+ } else {
205
+ summaryDate = parsed;
206
+ }
207
+ }
208
+ }
209
+ const promptIdx = rest.indexOf("--prompt");
210
+ if (promptIdx !== -1) {
211
+ const val = rest[promptIdx + 1];
212
+ if (!val) {
213
+ summaryError = "Invalid --prompt: missing value";
214
+ } else {
215
+ summaryPrompt = val;
216
+ }
217
+ }
218
+ if (rest.includes("--force")) summaryForce = true;
219
+ return {
220
+ mode: "summary",
221
+ summaryDate,
222
+ summaryPrompt,
223
+ summaryForce,
224
+ summaryError
225
+ };
226
+ }
151
227
  if (args[0] && !args[0].startsWith("-") && !KNOWN_SUBCOMMANDS.has(args[0])) {
152
228
  return {
153
229
  mode: "watch",
@@ -175,7 +251,8 @@ var DEFAULT_GLOBAL_CONFIG = {
175
251
  refreshIntervalMs: 2e3,
176
252
  logDir: join2(homedir(), ".agenthud", "logs"),
177
253
  hiddenSessions: [],
178
- hiddenSubAgents: []
254
+ hiddenSubAgents: [],
255
+ filterPresets: [[], ["response"], ["commit"]]
179
256
  };
180
257
  function parseInterval(value) {
181
258
  const match = value.match(/^(\d+)(s|m)$/);
@@ -217,6 +294,12 @@ function loadGlobalConfig() {
217
294
  (s) => typeof s === "string"
218
295
  );
219
296
  }
297
+ if (Array.isArray(parsed.filterPresets)) {
298
+ const presets = parsed.filterPresets.filter(Array.isArray).map(
299
+ (p) => p.filter((t) => typeof t === "string")
300
+ );
301
+ if (presets.length > 0) config.filterPresets = presets;
302
+ }
220
303
  return config;
221
304
  }
222
305
  function writeConfig(updates) {
@@ -260,11 +343,8 @@ function hasProjectLevelConfig() {
260
343
  // src/data/reportGenerator.ts
261
344
  import { statSync } from "fs";
262
345
 
263
- // src/data/sessionHistory.ts
264
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
265
-
266
- // src/data/activityParser.ts
267
- import { basename } from "path";
346
+ // src/data/gitCommits.ts
347
+ import { execSync } from "child_process";
268
348
 
269
349
  // src/types/index.ts
270
350
  var ICONS = {
@@ -282,10 +362,62 @@ var ICONS = {
282
362
  Task: "\xBB",
283
363
  TodoWrite: "~",
284
364
  AskUserQuestion: "?",
365
+ Commit: "\u25C6",
285
366
  Default: "$"
286
367
  };
287
368
 
369
+ // src/data/gitCommits.ts
370
+ function formatDateString(date) {
371
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
372
+ }
373
+ function getCommitDetail(projectPath, hash) {
374
+ if (!projectPath) return null;
375
+ try {
376
+ return execSync(
377
+ `git --git-dir="${projectPath}/.git" show --stat --no-color ${hash}`,
378
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
379
+ ).trim();
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
384
+ function parseGitCommits(projectPath, startDate, endDate) {
385
+ if (!projectPath) return [];
386
+ const start = formatDateString(startDate);
387
+ const end = formatDateString(endDate ?? startDate);
388
+ let raw;
389
+ try {
390
+ raw = execSync(
391
+ `git --git-dir="${projectPath}/.git" log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
392
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
393
+ ).trim();
394
+ } catch {
395
+ return [];
396
+ }
397
+ if (!raw) return [];
398
+ const entries = [];
399
+ for (const line of raw.split("\n")) {
400
+ const parts = line.trim().split("|");
401
+ if (parts.length < 3) continue;
402
+ const [tsStr, hash, ...rest] = parts;
403
+ const ts = Number(tsStr);
404
+ if (Number.isNaN(ts)) continue;
405
+ entries.push({
406
+ timestamp: new Date(ts * 1e3),
407
+ type: "commit",
408
+ icon: ICONS.Commit,
409
+ label: hash,
410
+ detail: rest.join("|")
411
+ });
412
+ }
413
+ return entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
414
+ }
415
+
416
+ // src/data/sessionHistory.ts
417
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
418
+
288
419
  // src/data/activityParser.ts
420
+ import { basename } from "path";
289
421
  function stripAnsi(text) {
290
422
  return text.replace(/\x1b\[[0-9;]*m/g, "");
291
423
  }
@@ -432,13 +564,13 @@ function isSameLocalDay(a, b) {
432
564
  function formatTime(date) {
433
565
  return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
434
566
  }
435
- function formatActivity(activity) {
567
+ function formatActivity(activity, limit) {
436
568
  const time = formatTime(activity.timestamp);
437
- const detail = activity.detail.length > 120 ? activity.detail.slice(0, 120) : activity.detail;
569
+ const detail = truncateDetail(activity.detail, limit);
438
570
  const suffix = detail ? `: ${detail}` : "";
439
571
  return `[${time}] ${activity.icon} ${activity.label}${suffix}`;
440
572
  }
441
- function formatDateString(date) {
573
+ function formatDateString2(date) {
442
574
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
443
575
  }
444
576
  function sessionIsOnDate(session, date, activities) {
@@ -449,14 +581,28 @@ function sessionIsOnDate(session, date, activities) {
449
581
  }
450
582
  return activities.some((a) => isSameLocalDay(a.timestamp, date));
451
583
  }
584
+ function truncateDetail(detail, limit) {
585
+ if (limit === 0 || detail.length <= limit) return detail;
586
+ return detail.slice(0, limit);
587
+ }
452
588
  function generateReport(sessions, options2) {
453
- const { date, include, format = "markdown" } = options2;
454
- const dateStr = formatDateString(date);
589
+ const {
590
+ date,
591
+ include,
592
+ format = "markdown",
593
+ detailLimit = 120,
594
+ withGit = false
595
+ } = options2;
596
+ const dateStr = formatDateString2(date);
455
597
  const blocks = [];
456
598
  for (const session of sessions) {
457
599
  const allActivities = parseSessionHistory(session.filePath);
458
600
  if (!sessionIsOnDate(session, date, allActivities)) continue;
459
- const dayActivities = allActivities.filter((a) => isSameLocalDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include));
601
+ const commits = withGit ? parseGitCommits(session.projectPath, date) : [];
602
+ const dayActivities = [
603
+ ...allActivities.filter((a) => isSameLocalDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include)),
604
+ ...commits
605
+ ].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
460
606
  if (dayActivities.length === 0) continue;
461
607
  blocks.push({
462
608
  session,
@@ -472,20 +618,39 @@ function generateReport(sessions, options2) {
472
618
  }
473
619
  blocks.sort((a, b) => a.firstTime - b.firstTime);
474
620
  if (format === "json") {
475
- return JSON.stringify(
476
- {
477
- date: dateStr,
478
- sessions: blocks.map(({ session, activities }) => ({
479
- project: session.projectName,
480
- start: formatTime(activities[0].timestamp),
481
- end: formatTime(activities[activities.length - 1].timestamp),
482
- activities: activities.map((a) => ({
621
+ const buildJsonSession = (session, acts) => {
622
+ const subAgentBlocks = session.subAgents.map((sa) => {
623
+ const saActivities = parseSessionHistory(sa.filePath).filter((a) => isSameLocalDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include));
624
+ return {
625
+ agentId: sa.agentId,
626
+ taskDescription: sa.taskDescription,
627
+ activities: saActivities.map((a) => ({
483
628
  time: formatTime(a.timestamp),
484
629
  icon: a.icon,
485
630
  label: a.label,
486
- detail: a.detail.length > 120 ? a.detail.slice(0, 120) : a.detail
631
+ detail: truncateDetail(a.detail, detailLimit)
487
632
  }))
488
- }))
633
+ };
634
+ });
635
+ return {
636
+ project: session.projectName,
637
+ start: formatTime(acts[0].timestamp),
638
+ end: formatTime(acts[acts.length - 1].timestamp),
639
+ activities: acts.map((a) => ({
640
+ time: formatTime(a.timestamp),
641
+ icon: a.icon,
642
+ label: a.label,
643
+ detail: truncateDetail(a.detail, detailLimit)
644
+ })),
645
+ subAgents: subAgentBlocks
646
+ };
647
+ };
648
+ return JSON.stringify(
649
+ {
650
+ date: dateStr,
651
+ sessions: blocks.map(
652
+ ({ session, activities }) => buildJsonSession(session, activities)
653
+ )
489
654
  },
490
655
  null,
491
656
  2
@@ -498,13 +663,26 @@ function generateReport(sessions, options2) {
498
663
  lines.push(`## ${session.projectName} (${first} \u2013 ${last})`);
499
664
  lines.push("");
500
665
  for (const activity of activities) {
501
- lines.push(formatActivity(activity));
666
+ lines.push(formatActivity(activity, detailLimit));
502
667
  }
503
668
  lines.push("");
504
669
  }
505
670
  return lines.join("\n").trimEnd();
506
671
  }
507
672
 
673
+ // src/data/summaryRunner.ts
674
+ import { spawn } from "child_process";
675
+ import {
676
+ copyFileSync,
677
+ createWriteStream,
678
+ existsSync as existsSync4,
679
+ mkdirSync as mkdirSync2,
680
+ readFileSync as readFileSync5
681
+ } from "fs";
682
+ import { homedir as homedir3 } from "os";
683
+ import { dirname as dirname2, join as join4 } from "path";
684
+ import { fileURLToPath as fileURLToPath2 } from "url";
685
+
508
686
  // src/data/sessions.ts
509
687
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
510
688
  import { homedir as homedir2 } from "os";
@@ -724,9 +902,129 @@ function discoverSessions(config) {
724
902
  };
725
903
  }
726
904
 
905
+ // src/data/summaryRunner.ts
906
+ function summariesDir() {
907
+ const dir = join4(homedir3(), ".agenthud", "summaries");
908
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
909
+ return dir;
910
+ }
911
+ function userPromptPath() {
912
+ return join4(homedir3(), ".agenthud", "summary-prompt.md");
913
+ }
914
+ function templatePath() {
915
+ const here = dirname2(fileURLToPath2(import.meta.url));
916
+ return join4(here, "templates", "summary-prompt.md");
917
+ }
918
+ function dateKey(d) {
919
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
920
+ }
921
+ function cachePath(date) {
922
+ return join4(summariesDir(), `${dateKey(date)}.md`);
923
+ }
924
+ function isSameLocalDay2(a, b) {
925
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
926
+ }
927
+ function ensureUserPromptFile() {
928
+ const p = userPromptPath();
929
+ if (existsSync4(p)) return;
930
+ const dir = dirname2(p);
931
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
932
+ try {
933
+ copyFileSync(templatePath(), p);
934
+ } catch {
935
+ }
936
+ }
937
+ function resolvePrompt(override) {
938
+ if (override) return override;
939
+ const p = userPromptPath();
940
+ if (existsSync4(p)) {
941
+ try {
942
+ return readFileSync5(p, "utf-8");
943
+ } catch {
944
+ }
945
+ }
946
+ try {
947
+ return readFileSync5(templatePath(), "utf-8");
948
+ } catch {
949
+ return "Summarize the activity log below.";
950
+ }
951
+ }
952
+ async function runSummary(options2) {
953
+ ensureUserPromptFile();
954
+ const isToday = isSameLocalDay2(options2.date, options2.today);
955
+ const cached = cachePath(options2.date);
956
+ if (!isToday && !options2.force && existsSync4(cached)) {
957
+ try {
958
+ const content = readFileSync5(cached, "utf-8");
959
+ process.stdout.write(content);
960
+ if (!content.endsWith("\n")) process.stdout.write("\n");
961
+ return 0;
962
+ } catch {
963
+ }
964
+ }
965
+ const config = loadGlobalConfig();
966
+ const sessions = discoverSessions(config);
967
+ const reportMarkdown = generateReport(sessions.sessions, {
968
+ date: options2.date,
969
+ include: ["response", "bash", "edit", "thinking"],
970
+ format: "markdown",
971
+ detailLimit: 0,
972
+ withGit: true
973
+ });
974
+ const prompt = resolvePrompt(options2.prompt);
975
+ return new Promise((resolve) => {
976
+ const proc = spawn("claude", ["-p", prompt], {
977
+ stdio: ["pipe", "pipe", "pipe"]
978
+ });
979
+ let cacheStream = null;
980
+ cacheStream = createWriteStream(cached, { encoding: "utf-8" });
981
+ cacheStream.on("error", (err) => {
982
+ process.stderr.write(
983
+ `agenthud: warning: cannot write cache (${err.message})
984
+ `
985
+ );
986
+ cacheStream = null;
987
+ });
988
+ let stderrBuf = "";
989
+ proc.on("error", (err) => {
990
+ if (err.code === "ENOENT") {
991
+ process.stderr.write(
992
+ "Error: claude CLI not found. Install: npm i -g @anthropic-ai/claude-code\n"
993
+ );
994
+ resolve(1);
995
+ } else {
996
+ process.stderr.write(`Error: ${err.message}
997
+ `);
998
+ resolve(1);
999
+ }
1000
+ });
1001
+ proc.stdout.on("data", (chunk) => {
1002
+ process.stdout.write(chunk);
1003
+ cacheStream?.write(chunk);
1004
+ });
1005
+ proc.stderr.on("data", (chunk) => {
1006
+ stderrBuf += chunk.toString();
1007
+ process.stderr.write(chunk);
1008
+ });
1009
+ proc.on("close", (code) => {
1010
+ cacheStream?.end();
1011
+ if (code !== 0) {
1012
+ const lower = stderrBuf.toLowerCase();
1013
+ if (lower.includes("not authenticated") || lower.includes("login") || lower.includes(" auth")) {
1014
+ process.stderr.write(
1015
+ "\nHint: claude appears to be unauthenticated. Run: claude\n"
1016
+ );
1017
+ }
1018
+ }
1019
+ resolve(code ?? 1);
1020
+ });
1021
+ proc.stdin.end(reportMarkdown);
1022
+ });
1023
+ }
1024
+
727
1025
  // src/ui/App.tsx
728
- import { existsSync as existsSync4, watch, writeFileSync as writeFileSync2 } from "fs";
729
- import { join as join4 } from "path";
1026
+ import { existsSync as existsSync5, watch, writeFileSync as writeFileSync2 } from "fs";
1027
+ import { join as join5 } from "path";
730
1028
  import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
731
1029
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
732
1030
 
@@ -743,6 +1041,9 @@ function getActivityStyle(activity) {
743
1041
  if (activity.type === "thinking") {
744
1042
  return { color: "magenta", dimColor: true };
745
1043
  }
1044
+ if (activity.type === "commit") {
1045
+ return { color: "yellow", dimColor: false };
1046
+ }
746
1047
  if (activity.type === "tool") {
747
1048
  if (activity.label === "Bash") {
748
1049
  return { color: "gray", dimColor: false };
@@ -762,7 +1063,7 @@ function formatActivityTime(date, now) {
762
1063
  const day = String(date.getDate()).padStart(2, "0");
763
1064
  return `${month}/${day} ${time}`;
764
1065
  }
765
- function truncateDetail(detail, maxWidth) {
1066
+ function truncateDetail2(detail, maxWidth) {
766
1067
  if (getDisplayWidth(detail) <= maxWidth) return detail;
767
1068
  let truncated = "";
768
1069
  let currentWidth = 0;
@@ -787,16 +1088,18 @@ function ActivityViewerPanel({
787
1088
  width,
788
1089
  cursorLine,
789
1090
  hasFocus,
790
- spinner = ""
1091
+ spinner = "",
1092
+ filterLabel
791
1093
  }) {
792
1094
  const innerWidth = getInnerWidth(width);
793
1095
  const contentWidth = innerWidth - 1;
1096
+ const filterSuffix = filterLabel && filterLabel !== "all" ? ` \xB7 ${filterLabel}` : "";
794
1097
  let titleSuffix;
795
1098
  if (isLive) {
796
- titleSuffix = `[LIVE ${spinner || "\u25BC"}]`;
1099
+ titleSuffix = `[LIVE ${spinner || "\u25BC"}${filterSuffix}]`;
797
1100
  } else {
798
1101
  const badge = newCount > 0 ? ` +${newCount}\u2191` : "";
799
- titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}]`;
1102
+ titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}${filterSuffix}]`;
800
1103
  }
801
1104
  let visibleActivities;
802
1105
  if (activities.length === 0) {
@@ -846,7 +1149,7 @@ function ActivityViewerPanel({
846
1149
  let labelContent;
847
1150
  let _displayWidth;
848
1151
  if (detail) {
849
- const truncated = truncateDetail(detail, Math.max(0, detailMaxWidth));
1152
+ const truncated = truncateDetail2(detail, Math.max(0, detailMaxWidth));
850
1153
  labelContent = `${labelPart}${truncated}${countSuffix}`;
851
1154
  _displayWidth = prefixWidth - 1 + labelWidth + getDisplayWidth(truncated) + countSuffixWidth;
852
1155
  } else {
@@ -894,21 +1197,27 @@ import { Box as Box2, Text as Text2 } from "ink";
894
1197
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
895
1198
  function wrapText(text, maxWidth) {
896
1199
  if (!text) return ["(empty)"];
897
- const words = text.split(" ");
898
- const lines = [];
899
- let current = "";
900
- for (const word of words) {
901
- if (!current) {
902
- current = word;
903
- } else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
904
- current += ` ${word}`;
905
- } else {
906
- lines.push(current);
907
- current = word;
1200
+ const result = [];
1201
+ for (const rawLine of text.split("\n")) {
1202
+ if (!rawLine) {
1203
+ result.push("");
1204
+ continue;
908
1205
  }
1206
+ const words = rawLine.split(" ");
1207
+ let current = "";
1208
+ for (const word of words) {
1209
+ if (!current) {
1210
+ current = word;
1211
+ } else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
1212
+ current += ` ${word}`;
1213
+ } else {
1214
+ result.push(current);
1215
+ current = word;
1216
+ }
1217
+ }
1218
+ if (current) result.push(current);
909
1219
  }
910
- if (current) lines.push(current);
911
- return lines.length > 0 ? lines : ["(empty)"];
1220
+ return result.length > 0 ? result : ["(empty)"];
912
1221
  }
913
1222
  function DetailViewPanel({
914
1223
  activity,
@@ -990,7 +1299,9 @@ function useHotkeys({
990
1299
  onHide,
991
1300
  onDetailClose,
992
1301
  onDetailScrollUp,
993
- onDetailScrollDown
1302
+ onDetailScrollDown,
1303
+ onFilter,
1304
+ filterLabel
994
1305
  }) {
995
1306
  const handleInput = (input, key) => {
996
1307
  if (detailMode) {
@@ -1024,6 +1335,10 @@ function useHotkeys({
1024
1335
  onRefresh();
1025
1336
  return;
1026
1337
  }
1338
+ if (input === "f" && !key.ctrl && focus === "viewer") {
1339
+ onFilter();
1340
+ return;
1341
+ }
1027
1342
  if (key.pageUp) {
1028
1343
  onScrollPageUp();
1029
1344
  return;
@@ -1099,9 +1414,10 @@ function useHotkeys({
1099
1414
  "Tab: sessions",
1100
1415
  "\u2191\u2193/jk: scroll",
1101
1416
  "PgUp/Dn: page",
1102
- "g: top",
1103
- "G: live",
1417
+ "g: live",
1418
+ "G: oldest",
1104
1419
  "\u21B5: detail",
1420
+ `f: ${filterLabel}`,
1105
1421
  "q: quit"
1106
1422
  ];
1107
1423
  return { handleInput, statusBarItems };
@@ -1123,7 +1439,7 @@ function useSpinner(active, intervalMs = 100) {
1123
1439
  }
1124
1440
 
1125
1441
  // src/ui/SessionTreePanel.tsx
1126
- import { homedir as homedir3 } from "os";
1442
+ import { homedir as homedir4 } from "os";
1127
1443
  import { Box as Box3, Text as Text3 } from "ink";
1128
1444
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1129
1445
  function formatElapsed(lastModifiedMs) {
@@ -1149,7 +1465,7 @@ function getStatusColor(status) {
1149
1465
  }
1150
1466
  }
1151
1467
  function formatProjectPath(projectPath) {
1152
- const home = homedir3();
1468
+ const home = homedir4();
1153
1469
  const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
1154
1470
  return raw;
1155
1471
  }
@@ -1504,6 +1820,7 @@ function App({ mode }) {
1504
1820
  const [scrollOffset, setScrollOffset] = useState2(0);
1505
1821
  const [isLive, setIsLive] = useState2(true);
1506
1822
  const [activities, setActivities] = useState2([]);
1823
+ const [gitActivities, setGitActivities] = useState2([]);
1507
1824
  const [newCount, setNewCount] = useState2(0);
1508
1825
  const [expandedIds, setExpandedIds] = useState2(/* @__PURE__ */ new Set());
1509
1826
  const [viewerCursorLine, setViewerCursorLine] = useState2(0);
@@ -1512,6 +1829,7 @@ function App({ mode }) {
1512
1829
  null
1513
1830
  );
1514
1831
  const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
1832
+ const [filterIndex, setFilterIndex] = useState2(0);
1515
1833
  const allFlat = useMemo(
1516
1834
  () => flattenSessions2(sessionTree, expandedIds),
1517
1835
  [sessionTree, expandedIds]
@@ -1521,9 +1839,11 @@ function App({ mode }) {
1521
1839
  allFlatRef.current = allFlat;
1522
1840
  }, [allFlat]);
1523
1841
  const activitiesLengthRef = useRef(0);
1842
+ const activitiesRef = useRef(activities);
1524
1843
  useEffect2(() => {
1525
1844
  activitiesLengthRef.current = activities.length;
1526
- }, [activities.length]);
1845
+ activitiesRef.current = activities;
1846
+ }, [activities]);
1527
1847
  useEffect2(() => {
1528
1848
  const node = allFlatRef.current.find((s) => s.id === selectedId);
1529
1849
  if (node?.filePath) {
@@ -1535,13 +1855,57 @@ function App({ mode }) {
1535
1855
  } else {
1536
1856
  setActivities([]);
1537
1857
  }
1858
+ setGitActivities([]);
1538
1859
  }, [selectedId]);
1860
+ useEffect2(() => {
1861
+ setScrollOffset(0);
1862
+ setIsLive(true);
1863
+ setViewerCursorLine(0);
1864
+ }, [filterIndex]);
1865
+ useEffect2(() => {
1866
+ if (!isWatchMode) return;
1867
+ const node = allFlatRef.current.find((s) => s.id === selectedId);
1868
+ if (!node?.projectPath) return;
1869
+ const load = () => {
1870
+ const acts = activitiesRef.current;
1871
+ const today = /* @__PURE__ */ new Date();
1872
+ const todayMidnight = new Date(
1873
+ today.getFullYear(),
1874
+ today.getMonth(),
1875
+ today.getDate()
1876
+ );
1877
+ const startDate = acts.length > 0 ? new Date(
1878
+ acts[0].timestamp.getFullYear(),
1879
+ acts[0].timestamp.getMonth(),
1880
+ acts[0].timestamp.getDate()
1881
+ ) : todayMidnight;
1882
+ const endDate = acts.length > 0 ? new Date(
1883
+ acts[acts.length - 1].timestamp.getFullYear(),
1884
+ acts[acts.length - 1].timestamp.getMonth(),
1885
+ acts[acts.length - 1].timestamp.getDate()
1886
+ ) : todayMidnight;
1887
+ const commits = parseGitCommits(node.projectPath, startDate, endDate);
1888
+ setGitActivities(commits);
1889
+ };
1890
+ const initial = setTimeout(load, 100);
1891
+ const timer = setInterval(load, 3e4);
1892
+ return () => {
1893
+ clearTimeout(initial);
1894
+ clearInterval(timer);
1895
+ };
1896
+ }, [selectedId, isWatchMode]);
1539
1897
  const refresh = useCallback(() => {
1540
1898
  const freshConfig = loadGlobalConfig();
1541
1899
  const tree = discoverSessions(freshConfig);
1542
- setSessionTree(tree);
1543
1900
  const updatedFlat = flattenSessions2(tree, expandedIds);
1544
1901
  const node = updatedFlat.find((s) => s.id === selectedId);
1902
+ if (!node) {
1903
+ const parentSession = tree.sessions.find(
1904
+ (s) => s.subAgents.some((sa) => sa.id === selectedId)
1905
+ );
1906
+ if (parentSession) setSelectedId(parentSession.id);
1907
+ }
1908
+ setSessionTree(tree);
1545
1909
  if (!node || !node.filePath) return;
1546
1910
  const newActivities = parseSessionHistory(node.filePath);
1547
1911
  const delta = newActivities.length - activitiesLengthRef.current;
@@ -1558,7 +1922,7 @@ function App({ mode }) {
1558
1922
  useEffect2(() => {
1559
1923
  if (!isWatchMode) return;
1560
1924
  const projectsDir = getProjectsDir();
1561
- const usePolling = process.platform === "linux" || !existsSync4(projectsDir);
1925
+ const usePolling = process.platform === "linux" || !existsSync5(projectsDir);
1562
1926
  if (usePolling) {
1563
1927
  const timer = setInterval(
1564
1928
  () => refreshRef.current(),
@@ -1585,6 +1949,24 @@ function App({ mode }) {
1585
1949
  if (debounce) clearTimeout(debounce);
1586
1950
  };
1587
1951
  }, [isWatchMode, config.refreshIntervalMs]);
1952
+ const filterPresets = config.filterPresets;
1953
+ const activePreset = useMemo(
1954
+ () => filterPresets[filterIndex % filterPresets.length] ?? [],
1955
+ [filterPresets, filterIndex]
1956
+ );
1957
+ const filterLabel = useMemo(
1958
+ () => activePreset.length === 0 ? "all" : activePreset.join("+"),
1959
+ [activePreset]
1960
+ );
1961
+ const mergedActivities = useMemo(() => {
1962
+ const merged = [...activities, ...gitActivities].sort(
1963
+ (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
1964
+ );
1965
+ if (activePreset.length === 0) return merged;
1966
+ return merged.filter(
1967
+ (a) => activePreset.includes(a.type) || a.type === "tool" && activePreset.some((p) => a.label.toLowerCase() === p)
1968
+ );
1969
+ }, [activities, gitActivities, activePreset]);
1588
1970
  const selectedIndex = allFlat.findIndex((s) => s.id === selectedId);
1589
1971
  const height = (stdout?.rows ?? 41) - 1;
1590
1972
  const width = stdout?.columns ?? 80;
@@ -1596,7 +1978,7 @@ function App({ mode }) {
1596
1978
  if (!activities.length || !selectedId) return;
1597
1979
  ensureLogDir(config.logDir);
1598
1980
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1599
- const filePath = join4(
1981
+ const filePath = join5(
1600
1982
  config.logDir,
1601
1983
  `${date}-${selectedId.slice(0, 8)}.txt`
1602
1984
  );
@@ -1616,6 +1998,7 @@ function App({ mode }) {
1616
1998
  onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
1617
1999
  onScrollUp: () => {
1618
2000
  if (focus === "tree") {
2001
+ if (selectedIndex === -1) return;
1619
2002
  const prev = Math.max(0, selectedIndex - 1);
1620
2003
  setSelectedId(allFlat[prev]?.id ?? selectedId);
1621
2004
  } else {
@@ -1635,6 +2018,7 @@ function App({ mode }) {
1635
2018
  },
1636
2019
  onScrollDown: () => {
1637
2020
  if (focus === "tree") {
2021
+ if (selectedIndex === -1) return;
1638
2022
  const next = Math.min(allFlat.length - 1, selectedIndex + 1);
1639
2023
  setSelectedId(allFlat[next]?.id ?? selectedId);
1640
2024
  } else {
@@ -1711,16 +2095,16 @@ function App({ mode }) {
1711
2095
  }
1712
2096
  },
1713
2097
  onScrollTop: () => {
1714
- setViewerCursorLine(0);
1715
- setIsLive(false);
1716
- setScrollOffset(Math.max(0, activities.length - viewerRows));
1717
- },
1718
- onScrollBottom: () => {
1719
2098
  setViewerCursorLine(0);
1720
2099
  setIsLive(true);
1721
2100
  setScrollOffset(0);
1722
2101
  setNewCount(0);
1723
2102
  },
2103
+ onScrollBottom: () => {
2104
+ setViewerCursorLine(0);
2105
+ setIsLive(false);
2106
+ setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
2107
+ },
1724
2108
  onDetailClose: () => {
1725
2109
  setDetailMode(false);
1726
2110
  },
@@ -1733,14 +2117,20 @@ function App({ mode }) {
1733
2117
  onEnter: () => {
1734
2118
  if (focus === "viewer") {
1735
2119
  const act = getSelectedActivity(
1736
- activities,
2120
+ mergedActivities,
1737
2121
  isLive,
1738
2122
  scrollOffset,
1739
2123
  viewerRows,
1740
2124
  viewerCursorLine
1741
2125
  );
1742
2126
  if (act) {
1743
- setDetailActivity(act);
2127
+ if (act.type === "commit") {
2128
+ const node = allFlatRef.current.find((s) => s.id === selectedId);
2129
+ const detail = node?.projectPath ? getCommitDetail(node.projectPath, act.label) ?? act.detail : act.detail;
2130
+ setDetailActivity({ ...act, detail });
2131
+ } else {
2132
+ setDetailActivity(act);
2133
+ }
1744
2134
  setDetailMode(true);
1745
2135
  setDetailScrollOffset(0);
1746
2136
  }
@@ -1765,8 +2155,14 @@ function App({ mode }) {
1765
2155
  const next = new Set(prev);
1766
2156
  if (next.has(parentId)) {
1767
2157
  next.delete(parentId);
2158
+ setSelectedId(parentId);
1768
2159
  } else {
1769
2160
  next.add(parentId);
2161
+ const parent = sessionTree.sessions.find((s) => s.id === parentId);
2162
+ const firstNew = parent?.subAgents.find(
2163
+ (sa) => sa.status === "cool" || sa.status === "cold"
2164
+ );
2165
+ if (firstNew) setSelectedId(firstNew.id);
1770
2166
  }
1771
2167
  return next;
1772
2168
  });
@@ -1824,7 +2220,9 @@ function App({ mode }) {
1824
2220
  },
1825
2221
  onSaveLog: saveLog,
1826
2222
  onRefresh: refresh,
1827
- onQuit: exit
2223
+ onQuit: exit,
2224
+ onFilter: () => setFilterIndex((i) => (i + 1) % filterPresets.length),
2225
+ filterLabel
1828
2226
  });
1829
2227
  useInput((input, key) => handleInput(input, key), { isActive: isWatchMode });
1830
2228
  const selectedSession = allFlat.find((s) => s.id === selectedId);
@@ -1863,7 +2261,7 @@ function App({ mode }) {
1863
2261
  ) : /* @__PURE__ */ jsx4(
1864
2262
  ActivityViewerPanel,
1865
2263
  {
1866
- activities,
2264
+ activities: mergedActivities,
1867
2265
  sessionName: sessionDisplayName,
1868
2266
  scrollOffset,
1869
2267
  isLive,
@@ -1872,7 +2270,8 @@ function App({ mode }) {
1872
2270
  width,
1873
2271
  cursorLine: viewerCursorLine,
1874
2272
  hasFocus: focus === "viewer",
1875
- spinner
2273
+ spinner,
2274
+ filterLabel
1876
2275
  }
1877
2276
  ) })
1878
2277
  ] });
@@ -1893,8 +2292,8 @@ if (options.command === "version") {
1893
2292
  console.log(getVersion());
1894
2293
  process.exit(0);
1895
2294
  }
1896
- var legacyConfig = join5(process.cwd(), ".agenthud", "config.yaml");
1897
- if (existsSync5(legacyConfig)) {
2295
+ var legacyConfig = join6(process.cwd(), ".agenthud", "config.yaml");
2296
+ if (existsSync6(legacyConfig)) {
1898
2297
  console.log(
1899
2298
  "The project-level config file (.agenthud/config.yaml) is no longer supported."
1900
2299
  );
@@ -1925,12 +2324,28 @@ if (options.mode === "report") {
1925
2324
  const markdown = generateReport(tree.sessions, {
1926
2325
  date: options.reportDate,
1927
2326
  include: options.reportInclude,
1928
- format: options.reportFormat
2327
+ format: options.reportFormat,
2328
+ detailLimit: options.reportDetailLimit,
2329
+ withGit: options.reportWithGit
1929
2330
  });
1930
2331
  process.stdout.write(`${markdown}
1931
2332
  `);
1932
2333
  process.exit(0);
1933
2334
  }
2335
+ if (options.mode === "summary") {
2336
+ if (options.summaryError) {
2337
+ process.stderr.write(`agenthud: ${options.summaryError}
2338
+ `);
2339
+ process.exit(1);
2340
+ }
2341
+ const exitCode = await runSummary({
2342
+ date: options.summaryDate,
2343
+ prompt: options.summaryPrompt,
2344
+ force: options.summaryForce ?? false,
2345
+ today: /* @__PURE__ */ new Date()
2346
+ });
2347
+ process.exit(exitCode);
2348
+ }
1934
2349
  if (options.mode === "watch") {
1935
2350
  clearScreen();
1936
2351
  }
@@ -0,0 +1,12 @@
1
+ The following is an activity log of work done with Claude Code. Summarize it concisely in English using the format below.
2
+
3
+ ## Completed Work
4
+ - (What was accomplished — focus on Response entries)
5
+
6
+ ## Notable Changes
7
+ - (Which files were modified and how — from Edit/Write entries)
8
+
9
+ ## Commits
10
+ - (Summary of the ◆ commit lines)
11
+
12
+ Activity log:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "CLI tool to monitor agent status in real-time. Works with Claude Code, multi-agent workflows, and any AI agent system.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,10 @@
1
+ #!/bin/zsh
2
+ # Daily summary: pipe today's agenthud report to claude for summarization
3
+ # Usage: daily-summary.sh [--date YYYY-MM-DD]
4
+
5
+ DATE_ARG=""
6
+ if [[ "$1" == "--date" && -n "$2" ]]; then
7
+ DATE_ARG="--date $2"
8
+ fi
9
+
10
+ agenthud report ${DATE_ARG} --detail-limit 0 --with-git | claude -p "다음은 오늘 Claude Code로 작업한 활동 로그입니다. 이를 바탕으로 오늘 작업 내용을 한국어로 간결하게 정리해주세요. 완료한 작업, 주요 변경사항, 커밋 내역 순으로 bullet point로 작성해주세요."
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // Compare two heap snapshots and show which object types grew the most.
3
+
4
+ import { readFileSync } from "node:fs";
5
+
6
+ const earlyPath = process.argv[2] ?? "/tmp/agenthud-early.heapsnapshot";
7
+ const latePath = process.argv[3] ?? "/tmp/agenthud-late.heapsnapshot";
8
+
9
+ interface Snapshot {
10
+ snapshot: {
11
+ node_count: number;
12
+ meta: {
13
+ node_fields: string[];
14
+ node_types: (string | string[])[];
15
+ };
16
+ };
17
+ nodes: number[];
18
+ strings: string[];
19
+ }
20
+
21
+ function countByType(path: string): Map<string, { count: number; size: number }> {
22
+ const raw = JSON.parse(readFileSync(path, "utf-8")) as Snapshot;
23
+ const fields = raw.snapshot.meta.node_fields;
24
+ const typeEnum = raw.snapshot.meta.node_types[fields.indexOf("type")] as string[];
25
+ const fieldCount = fields.length;
26
+ const nameIdx = fields.indexOf("name");
27
+ const typeIdx = fields.indexOf("type");
28
+ const sizeIdx = fields.indexOf("self_size");
29
+
30
+ const counts = new Map<string, { count: number; size: number }>();
31
+ for (let i = 0; i < raw.nodes.length; i += fieldCount) {
32
+ const type = typeEnum[raw.nodes[i + typeIdx]];
33
+ const name = raw.strings[raw.nodes[i + nameIdx]];
34
+ const size = raw.nodes[i + sizeIdx];
35
+ const key = `${type}|${name}`;
36
+ const entry = counts.get(key) ?? { count: 0, size: 0 };
37
+ entry.count++;
38
+ entry.size += size;
39
+ counts.set(key, entry);
40
+ }
41
+ return counts;
42
+ }
43
+
44
+ const early = countByType(earlyPath);
45
+ const late = countByType(latePath);
46
+
47
+ interface Diff {
48
+ key: string;
49
+ earlyCount: number;
50
+ lateCount: number;
51
+ countDelta: number;
52
+ sizeDelta: number;
53
+ }
54
+ const diffs: Diff[] = [];
55
+
56
+ for (const [key, lateVal] of late) {
57
+ const earlyVal = early.get(key) ?? { count: 0, size: 0 };
58
+ diffs.push({
59
+ key,
60
+ earlyCount: earlyVal.count,
61
+ lateCount: lateVal.count,
62
+ countDelta: lateVal.count - earlyVal.count,
63
+ sizeDelta: lateVal.size - earlyVal.size,
64
+ });
65
+ }
66
+
67
+ diffs.sort((a, b) => b.sizeDelta - a.sizeDelta);
68
+ console.log("Top growers by total size delta:");
69
+ console.log("size_delta_kb\tcount_delta\tearly\tlate\ttype|name");
70
+ for (const d of diffs.slice(0, 40)) {
71
+ console.log(
72
+ `${(d.sizeDelta / 1024).toFixed(0).padStart(8)}\t${d.countDelta.toString().padStart(8)}\t${d.earlyCount.toString().padStart(5)}\t${d.lateCount.toString().padStart(5)}\t${d.key}`,
73
+ );
74
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Minimal Ink test: only spinner-style re-renders, no other state.
3
+
4
+ import { Text } from "ink";
5
+ import { render } from "ink-testing-library";
6
+ import React, { useEffect, useState } from "react";
7
+
8
+ const seconds = Number(process.argv[2] ?? 60);
9
+ const mb = (b: number) => (b / 1024 / 1024).toFixed(1);
10
+ const printMem = (label: string) => {
11
+ const m = process.memoryUsage();
12
+ console.log(
13
+ `${label.padEnd(20)} rss=${mb(m.rss)}MB heap=${mb(m.heapUsed)}/${mb(m.heapTotal)}MB ext=${mb(m.external)}MB`,
14
+ );
15
+ };
16
+
17
+ function App() {
18
+ const [i, setI] = useState(0);
19
+ useEffect(() => {
20
+ const t = setInterval(() => setI((v) => v + 1), 100);
21
+ return () => clearInterval(t);
22
+ }, []);
23
+ return React.createElement(Text, null, `tick ${i}`);
24
+ }
25
+
26
+ printMem("start");
27
+ const instance = render(React.createElement(App));
28
+ printMem("after render");
29
+
30
+ const iv = setInterval(() => {
31
+ if (global.gc) global.gc();
32
+ printMem(`t=${process.uptime().toFixed(0)}s`);
33
+ }, 5_000);
34
+
35
+ setTimeout(() => {
36
+ clearInterval(iv);
37
+ instance.unmount();
38
+ if (global.gc) global.gc();
39
+ printMem("end");
40
+ process.exit(0);
41
+ }, seconds * 1000);
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Render App with ink-testing-library and watch memory while time passes.
3
+ // Spinner re-renders every 100ms, so this simulates the watch loop.
4
+
5
+ import { writeHeapSnapshot } from "node:v8";
6
+ import { render } from "ink-testing-library";
7
+ import React from "react";
8
+ import { App } from "../src/ui/App.js";
9
+
10
+ const seconds = Number(process.argv[2] ?? 60);
11
+ const mb = (b: number) => (b / 1024 / 1024).toFixed(1);
12
+
13
+ const printMem = (label: string) => {
14
+ const m = process.memoryUsage();
15
+ console.log(
16
+ `${label.padEnd(20)} rss=${mb(m.rss)}MB heap=${mb(m.heapUsed)}/${mb(m.heapTotal)}MB ext=${mb(m.external)}MB`,
17
+ );
18
+ };
19
+
20
+ printMem("start");
21
+
22
+ const instance = render(React.createElement(App, { mode: "watch" }));
23
+
24
+ printMem("after render");
25
+
26
+ const interval = setInterval(() => {
27
+ if (global.gc) global.gc();
28
+ printMem(`t=${process.uptime().toFixed(0)}s`);
29
+ }, 5_000);
30
+
31
+ // Snapshot at start (after warm-up) and end so we can diff
32
+ setTimeout(() => {
33
+ if (global.gc) global.gc();
34
+ const p = writeHeapSnapshot("/tmp/agenthud-early.heapsnapshot");
35
+ console.log(`early snapshot: ${p}`);
36
+ }, 5_000);
37
+
38
+ setTimeout(() => {
39
+ clearInterval(interval);
40
+ if (global.gc) global.gc();
41
+ const p = writeHeapSnapshot("/tmp/agenthud-late.heapsnapshot");
42
+ console.log(`late snapshot: ${p}`);
43
+ instance.unmount();
44
+ if (global.gc) global.gc();
45
+ printMem("end");
46
+ process.exit(0);
47
+ }, seconds * 1000);
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // Run data-layer loop and watch memory growth.
3
+ // Usage: tsx scripts/memcheck.ts [iterations]
4
+
5
+ import { loadGlobalConfig } from "../src/config/globalConfig.js";
6
+ import { parseGitCommits } from "../src/data/gitCommits.js";
7
+ import { parseSessionHistory } from "../src/data/sessionHistory.js";
8
+ import { discoverSessions } from "../src/data/sessions.js";
9
+
10
+ const iterations = Number(process.argv[2] ?? 1000);
11
+
12
+ const mb = (b: number) => (b / 1024 / 1024).toFixed(1);
13
+ const printMem = (label: string) => {
14
+ const m = process.memoryUsage();
15
+ console.log(
16
+ `${label.padEnd(20)} rss=${mb(m.rss)}MB heap=${mb(m.heapUsed)}/${mb(m.heapTotal)}MB ext=${mb(m.external)}MB`,
17
+ );
18
+ };
19
+
20
+ const config = loadGlobalConfig();
21
+ printMem("start");
22
+
23
+ const tree = discoverSessions(config);
24
+ const session = tree.sessions[0];
25
+ if (!session) {
26
+ console.error("No sessions found");
27
+ process.exit(1);
28
+ }
29
+ console.log(`Using session: ${session.projectName}/${session.id}`);
30
+ console.log(`File: ${session.filePath}`);
31
+
32
+ printMem("after discover");
33
+
34
+ for (let i = 1; i <= iterations; i++) {
35
+ // Simulate what refresh() + git effect do every cycle
36
+ discoverSessions(config);
37
+ parseSessionHistory(session.filePath);
38
+ if (session.projectPath) {
39
+ const today = new Date();
40
+ const day = new Date(today.getFullYear(), today.getMonth(), today.getDate());
41
+ parseGitCommits(session.projectPath, day);
42
+ }
43
+
44
+ if (i % 100 === 0) {
45
+ if (global.gc) global.gc();
46
+ printMem(`iter ${i}`);
47
+ }
48
+ }
49
+
50
+ if (global.gc) global.gc();
51
+ printMem("end");