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 +17 -0
- package/dist/index.js +2 -1
- package/dist/{main-VPGVYRCR.js → main-LOBTW45O.js} +485 -70
- package/dist/templates/summary-prompt.md +12 -0
- package/package.json +1 -1
- package/scripts/daily-summary.sh +10 -0
- 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";
|
|
@@ -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([
|
|
32
|
-
|
|
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/
|
|
264
|
-
import {
|
|
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
|
|
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
|
|
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 {
|
|
454
|
-
|
|
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
|
|
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
|
-
|
|
476
|
-
{
|
|
477
|
-
date
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
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
|
|
729
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
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:
|
|
1103
|
-
"G:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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" || !
|
|
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 =
|
|
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
|
-
|
|
2120
|
+
mergedActivities,
|
|
1737
2121
|
isLive,
|
|
1738
2122
|
scrollOffset,
|
|
1739
2123
|
viewerRows,
|
|
1740
2124
|
viewerCursorLine
|
|
1741
2125
|
);
|
|
1742
2126
|
if (act) {
|
|
1743
|
-
|
|
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 =
|
|
1897
|
-
if (
|
|
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
|
@@ -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");
|