agenthud 0.8.4 → 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-WBZ2KBF2.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";
@@ -35,7 +35,8 @@ var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set([
35
35
  "--detail-limit",
36
36
  "--with-git"
37
37
  ]);
38
- var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report"]);
38
+ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set(["--date", "--prompt", "--force"]);
39
+ var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report", "summary"]);
39
40
  function getHelp() {
40
41
  return `Usage: agenthud [options]
41
42
 
@@ -48,7 +49,7 @@ Options:
48
49
  -h, --help Show this help message
49
50
 
50
51
  Commands:
51
- report [--date DATE] [--include TYPES]
52
+ report [--date DATE] [--include TYPES] [--format FORMAT] [--detail-limit N] [--with-git]
52
53
  Print activity report for a date (default: today)
53
54
  --date YYYY-MM-DD|today Date to report on
54
55
  --include TYPES Comma-separated types or "all"
@@ -58,6 +59,12 @@ Commands:
58
59
  --detail-limit N Max chars per activity detail (default: 120, 0 = unlimited)
59
60
  --with-git Append today's git commits from cwd to report
60
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)
67
+
61
68
  Environment:
62
69
  CLAUDE_PROJECTS_DIR Path to Claude projects directory
63
70
  (default: ~/.claude/projects)
@@ -170,6 +177,53 @@ function parseArgs(args) {
170
177
  reportError
171
178
  };
172
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
+ }
173
227
  if (args[0] && !args[0].startsWith("-") && !KNOWN_SUBCOMMANDS.has(args[0])) {
174
228
  return {
175
229
  mode: "watch",
@@ -317,11 +371,12 @@ function formatDateString(date) {
317
371
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
318
372
  }
319
373
  function getCommitDetail(projectPath, hash) {
374
+ if (!projectPath) return null;
320
375
  try {
321
- return execSync(`git show --stat --no-color ${hash}`, {
322
- cwd: projectPath,
323
- encoding: "utf-8"
324
- }).trim();
376
+ return execSync(
377
+ `git --git-dir="${projectPath}/.git" show --stat --no-color ${hash}`,
378
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
379
+ ).trim();
325
380
  } catch {
326
381
  return null;
327
382
  }
@@ -333,8 +388,8 @@ function parseGitCommits(projectPath, startDate, endDate) {
333
388
  let raw;
334
389
  try {
335
390
  raw = execSync(
336
- `git log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
337
- { cwd: projectPath, encoding: "utf-8" }
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"] }
338
393
  ).trim();
339
394
  } catch {
340
395
  return [];
@@ -615,6 +670,19 @@ function generateReport(sessions, options2) {
615
670
  return lines.join("\n").trimEnd();
616
671
  }
617
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
+
618
686
  // src/data/sessions.ts
619
687
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
620
688
  import { homedir as homedir2 } from "os";
@@ -834,9 +902,129 @@ function discoverSessions(config) {
834
902
  };
835
903
  }
836
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
+
837
1025
  // src/ui/App.tsx
838
- import { existsSync as existsSync4, watch, writeFileSync as writeFileSync2 } from "fs";
839
- import { join as join4 } from "path";
1026
+ import { existsSync as existsSync5, watch, writeFileSync as writeFileSync2 } from "fs";
1027
+ import { join as join5 } from "path";
840
1028
  import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
841
1029
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
842
1030
 
@@ -1251,7 +1439,7 @@ function useSpinner(active, intervalMs = 100) {
1251
1439
  }
1252
1440
 
1253
1441
  // src/ui/SessionTreePanel.tsx
1254
- import { homedir as homedir3 } from "os";
1442
+ import { homedir as homedir4 } from "os";
1255
1443
  import { Box as Box3, Text as Text3 } from "ink";
1256
1444
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1257
1445
  function formatElapsed(lastModifiedMs) {
@@ -1277,7 +1465,7 @@ function getStatusColor(status) {
1277
1465
  }
1278
1466
  }
1279
1467
  function formatProjectPath(projectPath) {
1280
- const home = homedir3();
1468
+ const home = homedir4();
1281
1469
  const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
1282
1470
  return raw;
1283
1471
  }
@@ -1651,9 +1839,11 @@ function App({ mode }) {
1651
1839
  allFlatRef.current = allFlat;
1652
1840
  }, [allFlat]);
1653
1841
  const activitiesLengthRef = useRef(0);
1842
+ const activitiesRef = useRef(activities);
1654
1843
  useEffect2(() => {
1655
1844
  activitiesLengthRef.current = activities.length;
1656
- }, [activities.length]);
1845
+ activitiesRef.current = activities;
1846
+ }, [activities]);
1657
1847
  useEffect2(() => {
1658
1848
  const node = allFlatRef.current.find((s) => s.id === selectedId);
1659
1849
  if (node?.filePath) {
@@ -1677,7 +1867,7 @@ function App({ mode }) {
1677
1867
  const node = allFlatRef.current.find((s) => s.id === selectedId);
1678
1868
  if (!node?.projectPath) return;
1679
1869
  const load = () => {
1680
- const acts = node.filePath ? parseSessionHistory(node.filePath) : [];
1870
+ const acts = activitiesRef.current;
1681
1871
  const today = /* @__PURE__ */ new Date();
1682
1872
  const todayMidnight = new Date(
1683
1873
  today.getFullYear(),
@@ -1697,9 +1887,12 @@ function App({ mode }) {
1697
1887
  const commits = parseGitCommits(node.projectPath, startDate, endDate);
1698
1888
  setGitActivities(commits);
1699
1889
  };
1700
- load();
1890
+ const initial = setTimeout(load, 100);
1701
1891
  const timer = setInterval(load, 3e4);
1702
- return () => clearInterval(timer);
1892
+ return () => {
1893
+ clearTimeout(initial);
1894
+ clearInterval(timer);
1895
+ };
1703
1896
  }, [selectedId, isWatchMode]);
1704
1897
  const refresh = useCallback(() => {
1705
1898
  const freshConfig = loadGlobalConfig();
@@ -1729,7 +1922,7 @@ function App({ mode }) {
1729
1922
  useEffect2(() => {
1730
1923
  if (!isWatchMode) return;
1731
1924
  const projectsDir = getProjectsDir();
1732
- const usePolling = process.platform === "linux" || !existsSync4(projectsDir);
1925
+ const usePolling = process.platform === "linux" || !existsSync5(projectsDir);
1733
1926
  if (usePolling) {
1734
1927
  const timer = setInterval(
1735
1928
  () => refreshRef.current(),
@@ -1757,8 +1950,14 @@ function App({ mode }) {
1757
1950
  };
1758
1951
  }, [isWatchMode, config.refreshIntervalMs]);
1759
1952
  const filterPresets = config.filterPresets;
1760
- const activePreset = filterPresets[filterIndex % filterPresets.length] ?? [];
1761
- const filterLabel = activePreset.length === 0 ? "all" : activePreset.join("+");
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
+ );
1762
1961
  const mergedActivities = useMemo(() => {
1763
1962
  const merged = [...activities, ...gitActivities].sort(
1764
1963
  (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
@@ -1779,7 +1978,7 @@ function App({ mode }) {
1779
1978
  if (!activities.length || !selectedId) return;
1780
1979
  ensureLogDir(config.logDir);
1781
1980
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1782
- const filePath = join4(
1981
+ const filePath = join5(
1783
1982
  config.logDir,
1784
1983
  `${date}-${selectedId.slice(0, 8)}.txt`
1785
1984
  );
@@ -2093,8 +2292,8 @@ if (options.command === "version") {
2093
2292
  console.log(getVersion());
2094
2293
  process.exit(0);
2095
2294
  }
2096
- var legacyConfig = join5(process.cwd(), ".agenthud", "config.yaml");
2097
- if (existsSync5(legacyConfig)) {
2295
+ var legacyConfig = join6(process.cwd(), ".agenthud", "config.yaml");
2296
+ if (existsSync6(legacyConfig)) {
2098
2297
  console.log(
2099
2298
  "The project-level config file (.agenthud/config.yaml) is no longer supported."
2100
2299
  );
@@ -2133,6 +2332,20 @@ if (options.mode === "report") {
2133
2332
  `);
2134
2333
  process.exit(0);
2135
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
+ }
2136
2349
  if (options.mode === "watch") {
2137
2350
  clearScreen();
2138
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.4",
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,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");