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 +17 -0
- package/dist/index.js +2 -1
- package/dist/{main-WBZ2KBF2.js → main-LOBTW45O.js} +237 -24
- package/dist/templates/summary-prompt.md +12 -0
- package/package.json +1 -1
- package/scripts/diff-heap.ts +74 -0
- package/scripts/memcheck-minimal.tsx +41 -0
- package/scripts/memcheck-ui.tsx +47 -0
- package/scripts/memcheck.ts +51 -0
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
|
-
|
|
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
|
|
3
|
-
import { join as
|
|
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
|
|
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(
|
|
322
|
-
|
|
323
|
-
encoding: "utf-8"
|
|
324
|
-
|
|
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
|
-
{
|
|
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
|
|
839
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 () =>
|
|
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" || !
|
|
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 =
|
|
1761
|
-
|
|
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 =
|
|
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 =
|
|
2097
|
-
if (
|
|
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
|
@@ -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");
|