agenthud 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,6 +105,31 @@ Press `↵` on any activity to open a scrollable full-content view.
105
105
  | `↓` / `j` | Scroll down |
106
106
  | `↵` / `Esc` / `q` | Close |
107
107
 
108
+ ## Report
109
+
110
+ Print a Markdown summary of all activity on a given date, suitable for piping to scripts or LLMs:
111
+
112
+ ```bash
113
+ agenthud report # today
114
+ agenthud report --date 2026-05-14 # specific date
115
+ agenthud report --date today --include all # all activity types
116
+ ```
117
+
118
+ Output is written to stdout in Markdown format:
119
+
120
+ ```
121
+ # AgentHUD Report: 2026-05-14
122
+
123
+ ## myproject (10:23 – 14:45)
124
+
125
+ [10:23] $ npm test
126
+ [10:35] ~ src/ui/App.tsx
127
+ [11:15] < Added spinner hook to make the UI feel alive.
128
+ ```
129
+
130
+ **`--include` types:** `response`, `bash`, `edit`, `thinking`, `read`, `glob`, `user`
131
+ Default: `response,bash,edit,thinking`
132
+
108
133
  ## Configuration
109
134
 
110
135
  Optional. Create `~/.agenthud/config.yaml`:
@@ -120,6 +145,12 @@ hiddenSubAgents:
120
145
  - code-reviewer
121
146
  ```
122
147
 
148
+ ## Environment Variables
149
+
150
+ | Variable | Default | Description |
151
+ |----------|---------|-------------|
152
+ | `CLAUDE_PROJECTS_DIR` | `~/.claude/projects` | Path to Claude Code projects directory. Useful for backups or mounted volumes. |
153
+
123
154
  ## Feedback
124
155
 
125
156
  Issues and PRs welcome at [GitHub](https://github.com/neochoon/agenthud).
package/dist/index.js CHANGED
@@ -14,4 +14,4 @@ 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-CA4K47Y5.js");
17
+ import("./main-QZW2W2BX.js");
@@ -1,7 +1,7 @@
1
1
  // src/main.ts
2
- import { createInterface } from "readline";
3
2
  import { existsSync as existsSync5, rmSync } from "fs";
4
3
  import { join as join5 } from "path";
4
+ import { createInterface } from "readline";
5
5
  import { render } from "ink";
6
6
  import React from "react";
7
7
 
@@ -9,16 +9,50 @@ import React from "react";
9
9
  import { readFileSync } from "fs";
10
10
  import { dirname, join } from "path";
11
11
  import { fileURLToPath } from "url";
12
+ var ALL_TYPES = [
13
+ "response",
14
+ "bash",
15
+ "edit",
16
+ "thinking",
17
+ "read",
18
+ "glob",
19
+ "user"
20
+ ];
21
+ var DEFAULT_TYPES = ["response", "bash", "edit", "thinking"];
22
+ var KNOWN_WATCH_FLAGS = /* @__PURE__ */ new Set([
23
+ "-w",
24
+ "--watch",
25
+ "--once",
26
+ "-V",
27
+ "--version",
28
+ "-h",
29
+ "--help"
30
+ ]);
31
+ var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set(["--date", "--include", "--format"]);
32
+ var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report"]);
12
33
  function getHelp() {
13
34
  return `Usage: agenthud [options]
14
35
 
15
36
  Monitors all running Claude Code sessions in real-time.
16
37
 
17
38
  Options:
18
- -w, --watch Watch mode (default) \u2014 live updates
19
- --once Print once and exit
20
- -V, --version Show version number
21
- -h, --help Show this help message
39
+ -w, --watch Watch mode (default) \u2014 live updates
40
+ --once Print once and exit
41
+ -V, --version Show version number
42
+ -h, --help Show this help message
43
+
44
+ Commands:
45
+ report [--date DATE] [--include TYPES]
46
+ Print activity report for a date (default: today)
47
+ --date YYYY-MM-DD|today Date to report on
48
+ --include TYPES Comma-separated types or "all"
49
+ Types: response,bash,edit,thinking,read,glob,user
50
+ Default: response,bash,edit,thinking
51
+ --format FORMAT Output format: markdown (default) or json
52
+
53
+ Environment:
54
+ CLAUDE_PROJECTS_DIR Path to Claude projects directory
55
+ (default: ~/.claude/projects)
22
56
 
23
57
  Config: ~/.agenthud/config.yaml
24
58
  Logs: ~/.agenthud/logs/
@@ -34,6 +68,26 @@ function getVersion() {
34
68
  function clearScreen() {
35
69
  console.clear();
36
70
  }
71
+ function parseUTCMidnight(dateStr) {
72
+ if (dateStr === "today") {
73
+ const now = /* @__PURE__ */ new Date();
74
+ return new Date(
75
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
76
+ );
77
+ }
78
+ const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
79
+ if (!match) return null;
80
+ const [, y, m, d] = match.map(Number);
81
+ const date = new Date(Date.UTC(y, m - 1, d));
82
+ if (Number.isNaN(date.getTime())) return null;
83
+ return date;
84
+ }
85
+ function todayUTCMidnight() {
86
+ const now = /* @__PURE__ */ new Date();
87
+ return new Date(
88
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
89
+ );
90
+ }
37
91
  function parseArgs(args) {
38
92
  if (args.includes("--help") || args.includes("-h")) {
39
93
  return { mode: "watch", command: "help" };
@@ -44,15 +98,77 @@ function parseArgs(args) {
44
98
  if (args.includes("--once")) {
45
99
  return { mode: "once" };
46
100
  }
101
+ if (args[0] === "report") {
102
+ const rest = args.slice(1);
103
+ let reportDate = todayUTCMidnight();
104
+ let reportInclude = DEFAULT_TYPES;
105
+ let reportError;
106
+ for (const arg of rest) {
107
+ if (arg.startsWith("-") && !KNOWN_REPORT_FLAGS.has(arg)) {
108
+ reportError = `Unknown option: "${arg}". Run agenthud --help for usage.`;
109
+ break;
110
+ }
111
+ }
112
+ const dateIdx = rest.indexOf("--date");
113
+ if (dateIdx !== -1) {
114
+ const dateStr = rest[dateIdx + 1];
115
+ if (!dateStr) {
116
+ reportError = "Invalid date: missing value for --date";
117
+ } else {
118
+ const parsed = parseUTCMidnight(dateStr);
119
+ if (!parsed) {
120
+ reportError = `Invalid date: "${dateStr}". Use YYYY-MM-DD or "today".`;
121
+ } else {
122
+ reportDate = parsed;
123
+ }
124
+ }
125
+ }
126
+ const includeIdx = rest.indexOf("--include");
127
+ if (includeIdx !== -1) {
128
+ const includeStr = rest[includeIdx + 1];
129
+ if (includeStr === "all") {
130
+ reportInclude = ALL_TYPES;
131
+ } else if (includeStr) {
132
+ reportInclude = includeStr.split(",").map((s) => s.trim()).filter(Boolean);
133
+ }
134
+ }
135
+ let reportFormat = "markdown";
136
+ const formatIdx = rest.indexOf("--format");
137
+ if (formatIdx !== -1) {
138
+ const fmt = rest[formatIdx + 1];
139
+ if (fmt === "json" || fmt === "markdown") {
140
+ reportFormat = fmt;
141
+ } else if (fmt) {
142
+ reportError = `Invalid format: "${fmt}". Use "markdown" or "json".`;
143
+ } else {
144
+ reportError = "Invalid format: missing value for --format.";
145
+ }
146
+ }
147
+ return {
148
+ mode: "report",
149
+ reportDate,
150
+ reportInclude,
151
+ reportFormat,
152
+ reportError
153
+ };
154
+ }
155
+ if (args[0] && !args[0].startsWith("-") && !KNOWN_SUBCOMMANDS.has(args[0])) {
156
+ return {
157
+ mode: "watch",
158
+ error: `Unknown command: "${args[0]}". Run agenthud --help for usage.`
159
+ };
160
+ }
161
+ for (const arg of args) {
162
+ if (arg.startsWith("-") && !KNOWN_WATCH_FLAGS.has(arg)) {
163
+ return {
164
+ mode: "watch",
165
+ error: `Unknown option: "${arg}". Run agenthud --help for usage.`
166
+ };
167
+ }
168
+ }
47
169
  return { mode: "watch" };
48
170
  }
49
171
 
50
- // src/ui/App.tsx
51
- import { existsSync as existsSync4, watch, writeFileSync as writeFileSync2 } from "fs";
52
- import { join as join4 } from "path";
53
- import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
54
- import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
55
-
56
172
  // src/config/globalConfig.ts
57
173
  import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
58
174
  import { homedir } from "os";
@@ -145,6 +261,9 @@ function hasProjectLevelConfig() {
145
261
  return existsSync(join2(process.cwd(), ".agenthud", "config.yaml"));
146
262
  }
147
263
 
264
+ // src/data/reportGenerator.ts
265
+ import { statSync } from "fs";
266
+
148
267
  // src/data/sessionHistory.ts
149
268
  import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
150
269
 
@@ -295,8 +414,103 @@ function parseSessionHistory(filePath) {
295
414
  return activities;
296
415
  }
297
416
 
417
+ // src/data/reportGenerator.ts
418
+ function activityMatchesInclude(activity, include) {
419
+ const label = activity.label.toLowerCase();
420
+ const type = activity.type;
421
+ if (include.includes("response") && type === "response") return true;
422
+ if (include.includes("thinking") && type === "thinking") return true;
423
+ if (include.includes("user") && type === "user") return true;
424
+ if (include.includes("bash") && label === "bash") return true;
425
+ if (include.includes("edit") && (label === "edit" || label === "write" || label === "todowrite"))
426
+ return true;
427
+ if (include.includes("read") && (label === "read" || label === "glob" || label === "grep"))
428
+ return true;
429
+ if (include.includes("glob") && (label === "glob" || label === "grep"))
430
+ return true;
431
+ return false;
432
+ }
433
+ function isSameUTCDay(a, b) {
434
+ return a.getUTCFullYear() === b.getUTCFullYear() && a.getUTCMonth() === b.getUTCMonth() && a.getUTCDate() === b.getUTCDate();
435
+ }
436
+ function formatTime(date) {
437
+ return `${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")}`;
438
+ }
439
+ function formatActivity(activity) {
440
+ const time = formatTime(activity.timestamp);
441
+ const detail = activity.detail.length > 120 ? activity.detail.slice(0, 120) : activity.detail;
442
+ const suffix = detail ? `: ${detail}` : "";
443
+ return `[${time}] ${activity.icon} ${activity.label}${suffix}`;
444
+ }
445
+ function formatDateString(date) {
446
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
447
+ }
448
+ function sessionIsOnDate(session, date, activities) {
449
+ try {
450
+ const mtime = new Date(statSync(session.filePath).mtimeMs);
451
+ if (isSameUTCDay(mtime, date)) return true;
452
+ } catch {
453
+ }
454
+ return activities.some((a) => isSameUTCDay(a.timestamp, date));
455
+ }
456
+ function generateReport(sessions, options2) {
457
+ const { date, include, format = "markdown" } = options2;
458
+ const dateStr = formatDateString(date);
459
+ const blocks = [];
460
+ for (const session of sessions) {
461
+ const allActivities = parseSessionHistory(session.filePath);
462
+ if (!sessionIsOnDate(session, date, allActivities)) continue;
463
+ const dayActivities = allActivities.filter((a) => isSameUTCDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include));
464
+ if (dayActivities.length === 0) continue;
465
+ blocks.push({
466
+ session,
467
+ activities: dayActivities,
468
+ firstTime: dayActivities[0].timestamp.getTime()
469
+ });
470
+ }
471
+ if (blocks.length === 0) {
472
+ if (format === "json") {
473
+ return JSON.stringify({ date: dateStr, sessions: [] }, null, 2);
474
+ }
475
+ return `No activity found for ${dateStr}.`;
476
+ }
477
+ blocks.sort((a, b) => a.firstTime - b.firstTime);
478
+ if (format === "json") {
479
+ return JSON.stringify(
480
+ {
481
+ date: dateStr,
482
+ sessions: blocks.map(({ session, activities }) => ({
483
+ project: session.projectName,
484
+ start: formatTime(activities[0].timestamp),
485
+ end: formatTime(activities[activities.length - 1].timestamp),
486
+ activities: activities.map((a) => ({
487
+ time: formatTime(a.timestamp),
488
+ icon: a.icon,
489
+ label: a.label,
490
+ detail: a.detail.length > 120 ? a.detail.slice(0, 120) : a.detail
491
+ }))
492
+ }))
493
+ },
494
+ null,
495
+ 2
496
+ );
497
+ }
498
+ const lines = [`# AgentHUD Report: ${dateStr}`, ""];
499
+ for (const { session, activities } of blocks) {
500
+ const first = formatTime(activities[0].timestamp);
501
+ const last = formatTime(activities[activities.length - 1].timestamp);
502
+ lines.push(`## ${session.projectName} (${first} \u2013 ${last})`);
503
+ lines.push("");
504
+ for (const activity of activities) {
505
+ lines.push(formatActivity(activity));
506
+ }
507
+ lines.push("");
508
+ }
509
+ return lines.join("\n").trimEnd();
510
+ }
511
+
298
512
  // src/data/sessions.ts
299
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
513
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
300
514
  import { homedir as homedir2 } from "os";
301
515
  import { basename as basename2, join as join3 } from "path";
302
516
 
@@ -339,7 +553,7 @@ var getDisplayWidth = stringWidth;
339
553
 
340
554
  // src/data/sessions.ts
341
555
  function getProjectsDir() {
342
- return join3(homedir2(), ".claude", "projects");
556
+ return process.env.CLAUDE_PROJECTS_DIR ?? join3(homedir2(), ".claude", "projects");
343
557
  }
344
558
  function decodeProjectPath(encoded) {
345
559
  const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)$/);
@@ -418,7 +632,7 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
418
632
  const hideKey = `${projectName}/${id}`;
419
633
  const filePath = join3(subagentsDir, file);
420
634
  try {
421
- const stat = statSync(filePath);
635
+ const stat = statSync2(filePath);
422
636
  const { agentId, taskDescription } = readSubAgentInfo(filePath);
423
637
  return {
424
638
  id,
@@ -449,7 +663,7 @@ function discoverSessions(config) {
449
663
  try {
450
664
  projectDirs = readdirSync(projectsDir).filter((entry) => {
451
665
  try {
452
- return statSync(join3(projectsDir, entry)).isDirectory();
666
+ return statSync2(join3(projectsDir, entry)).isDirectory();
453
667
  } catch {
454
668
  return false;
455
669
  }
@@ -475,7 +689,7 @@ function discoverSessions(config) {
475
689
  const hideKey = `${projectName}/${id}`;
476
690
  const filePath = join3(projectDir, file);
477
691
  try {
478
- const stat = statSync(filePath);
692
+ const stat = statSync2(filePath);
479
693
  const subAgents = buildSubAgents(id, projectDir, config, projectName);
480
694
  allSessions.push({
481
695
  id,
@@ -514,6 +728,12 @@ function discoverSessions(config) {
514
728
  };
515
729
  }
516
730
 
731
+ // src/ui/App.tsx
732
+ import { existsSync as existsSync4, watch, writeFileSync as writeFileSync2 } from "fs";
733
+ import { join as join4 } from "path";
734
+ import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
735
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
736
+
517
737
  // src/ui/ActivityViewerPanel.tsx
518
738
  import { Box, Text } from "ink";
519
739
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -570,13 +790,14 @@ function ActivityViewerPanel({
570
790
  visibleRows,
571
791
  width,
572
792
  cursorLine,
573
- hasFocus
793
+ hasFocus,
794
+ spinner = ""
574
795
  }) {
575
796
  const innerWidth = getInnerWidth(width);
576
797
  const contentWidth = innerWidth - 1;
577
798
  let titleSuffix;
578
799
  if (isLive) {
579
- titleSuffix = "[LIVE \u25BC]";
800
+ titleSuffix = `[LIVE ${spinner || "\u25BC"}]`;
580
801
  } else {
581
802
  const badge = newCount > 0 ? ` +${newCount}\u2191` : "";
582
803
  titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}]`;
@@ -711,8 +932,18 @@ function DetailViewPanel({
711
932
  clampedOffset,
712
933
  clampedOffset + visibleRows
713
934
  );
714
- const titleLabel = `${activity.icon} ${activity.label}`;
935
+ const style = getActivityStyle(activity);
715
936
  const scrollSuffix = totalLines > visibleRows ? `[${clampedOffset + 1}-${Math.min(clampedOffset + visibleRows, totalLines)}/${totalLines}]` : "";
937
+ const iconWidth = getDisplayWidth(activity.icon);
938
+ const labelWidth = activity.label.length;
939
+ const scrollPart = scrollSuffix ? ` ${scrollSuffix} ${BOX.h}` : "";
940
+ const scrollPartWidth = scrollSuffix ? getDisplayWidth(scrollPart) : 0;
941
+ const dashCount = Math.max(
942
+ 0,
943
+ width - 3 - iconWidth - 1 - labelWidth - 1 - scrollPartWidth - 1
944
+ );
945
+ const dashes = BOX.h.repeat(dashCount);
946
+ const titleRight = `${dashes}${scrollPart}${BOX.tr}`;
716
947
  const contentRows = [];
717
948
  for (let i = 0; i < visibleRows; i++) {
718
949
  const line = visibleSlice[i] ?? "";
@@ -728,7 +959,16 @@ function DetailViewPanel({
728
959
  );
729
960
  }
730
961
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
731
- /* @__PURE__ */ jsx2(Text2, { children: createTitleLine(titleLabel, scrollSuffix, width) }),
962
+ /* @__PURE__ */ jsxs2(Text2, { children: [
963
+ BOX.tl,
964
+ BOX.h,
965
+ " ",
966
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: activity.icon }),
967
+ " ",
968
+ /* @__PURE__ */ jsx2(Text2, { color: style.color, dimColor: style.dimColor, children: activity.label }),
969
+ " ",
970
+ titleRight
971
+ ] }),
732
972
  contentRows,
733
973
  /* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
734
974
  ] });
@@ -1352,11 +1592,10 @@ function App({ mode }) {
1352
1592
  const selectedIndex = allFlat.findIndex((s) => s.id === selectedId);
1353
1593
  const height = (stdout?.rows ?? 41) - 1;
1354
1594
  const width = stdout?.columns ?? 80;
1355
- const viewerRows = Math.max(
1356
- 5,
1357
- Math.floor(height * VIEWER_HEIGHT_FRACTION) - 4
1358
- );
1359
- const treeRows = Math.max(3, height - viewerRows - 7);
1595
+ const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
1596
+ const naturalTreeRows = allFlat.length;
1597
+ const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
1598
+ const viewerRows = Math.max(5, height - 7 - treeRows);
1360
1599
  const saveLog = useCallback(() => {
1361
1600
  if (!activities.length || !selectedId) return;
1362
1601
  ensureLogDir(config.logDir);
@@ -1636,7 +1875,8 @@ function App({ mode }) {
1636
1875
  visibleRows: viewerRows,
1637
1876
  width,
1638
1877
  cursorLine: viewerCursorLine,
1639
- hasFocus: focus === "viewer"
1878
+ hasFocus: focus === "viewer",
1879
+ spinner
1640
1880
  }
1641
1881
  ) })
1642
1882
  ] });
@@ -1644,6 +1884,11 @@ function App({ mode }) {
1644
1884
 
1645
1885
  // src/main.ts
1646
1886
  var options = parseArgs(process.argv.slice(2));
1887
+ if (options.error) {
1888
+ process.stderr.write(`agenthud: ${options.error}
1889
+ `);
1890
+ process.exit(1);
1891
+ }
1647
1892
  if (options.command === "help") {
1648
1893
  console.log(getHelp());
1649
1894
  process.exit(0);
@@ -1660,22 +1905,36 @@ if (existsSync5(legacyConfig)) {
1660
1905
  console.log("Settings have moved to ~/.agenthud/config.yaml.");
1661
1906
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1662
1907
  await new Promise((resolve) => {
1663
- rl.question(
1664
- "Delete the old config file and continue? [y/N] ",
1665
- (answer) => {
1666
- rl.close();
1667
- if (answer.trim().toLowerCase() === "y") {
1668
- rmSync(legacyConfig);
1669
- console.log("Deleted .agenthud/config.yaml.");
1670
- } else {
1671
- console.log("Aborted.");
1672
- process.exit(0);
1673
- }
1674
- resolve();
1908
+ rl.question("Delete the old config file and continue? [y/N] ", (answer) => {
1909
+ rl.close();
1910
+ if (answer.trim().toLowerCase() === "y") {
1911
+ rmSync(legacyConfig);
1912
+ console.log("Deleted .agenthud/config.yaml.");
1913
+ } else {
1914
+ console.log("Aborted.");
1915
+ process.exit(0);
1675
1916
  }
1676
- );
1917
+ resolve();
1918
+ });
1677
1919
  });
1678
1920
  }
1921
+ if (options.mode === "report") {
1922
+ if (options.reportError) {
1923
+ process.stderr.write(`agenthud: ${options.reportError}
1924
+ `);
1925
+ process.exit(1);
1926
+ }
1927
+ const config = loadGlobalConfig();
1928
+ const tree = discoverSessions(config);
1929
+ const markdown = generateReport(tree.sessions, {
1930
+ date: options.reportDate,
1931
+ include: options.reportInclude,
1932
+ format: options.reportFormat
1933
+ });
1934
+ process.stdout.write(`${markdown}
1935
+ `);
1936
+ process.exit(0);
1937
+ }
1679
1938
  if (options.mode === "watch") {
1680
1939
  clearScreen();
1681
1940
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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",