agenthud 0.12.1 → 0.12.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
@@ -24,6 +24,8 @@ npx agenthud
24
24
 
25
25
  Run this in a separate terminal while using Claude Code. Press `?` inside the TUI any time for in-app help.
26
26
 
27
+ > **Platform support.** Primary development is on macOS and Linux; the full test suite runs on all three platforms in CI (including Windows). Windows runtime behavior is exercised by a manual smoke job but isn't daily-driven — issues there are valued bug reports.
28
+
27
29
  Pass `--cwd` to scope the view to the Claude project that contains your current directory — useful when you have many projects but only care about the one you're in right now. Exits 1 with a stderr message if no such project is registered.
28
30
 
29
31
  ```bash
package/dist/index.js CHANGED
@@ -15,4 +15,4 @@ Error: Node.js ${MIN_NODE_VERSION}+ is required (current: ${process.version})
15
15
  process.exit(1);
16
16
  }
17
17
  if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
18
- import("./main-CRTF5EHN.js");
18
+ import("./main-YCUCY2QP.js");
@@ -1,7 +1,7 @@
1
1
  // src/main.ts
2
- import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync, rmSync } from "fs";
2
+ import { existsSync as existsSync8, readdirSync as readdirSync3, realpathSync, rmSync } from "fs";
3
3
  import { homedir as homedir5 } from "os";
4
- import { join as join7 } from "path";
4
+ import { join as join8 } from "path";
5
5
  import { createInterface as createInterface2 } from "readline";
6
6
  import { render } from "ink";
7
7
  import React from "react";
@@ -1667,23 +1667,27 @@ import { spawn as spawn2 } from "child_process";
1667
1667
  import {
1668
1668
  copyFileSync,
1669
1669
  createWriteStream,
1670
- existsSync as existsSync5,
1670
+ existsSync as existsSync6,
1671
1671
  mkdirSync as mkdirSync2,
1672
- readFileSync as readFileSync6,
1673
- unlinkSync
1672
+ readFileSync as readFileSync7,
1673
+ unlinkSync,
1674
+ writeFileSync as writeFileSync3
1674
1675
  } from "fs";
1675
1676
  import { homedir as homedir3 } from "os";
1676
- import { dirname as dirname2, join as join5 } from "path";
1677
+ import { dirname as dirname2, join as join6 } from "path";
1677
1678
  import { createInterface } from "readline";
1678
1679
  import { fileURLToPath as fileURLToPath2 } from "url";
1679
1680
 
1680
1681
  // src/utils/openInDefaultApp.ts
1681
1682
  import { spawn } from "child_process";
1682
- function buildOpenCommand(platform, path) {
1683
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1684
+ import { join as join4 } from "path";
1685
+ function buildOpenCommand(platform, path, opts = {}) {
1683
1686
  switch (platform) {
1684
1687
  case "darwin":
1685
1688
  return { command: "open", args: [path] };
1686
1689
  case "linux":
1690
+ if (opts.wslView) return { command: "wslview", args: [path] };
1687
1691
  return { command: "xdg-open", args: [path] };
1688
1692
  case "win32":
1689
1693
  return { command: "cmd", args: ["/c", "start", "", path] };
@@ -1691,8 +1695,36 @@ function buildOpenCommand(platform, path) {
1691
1695
  return null;
1692
1696
  }
1693
1697
  }
1694
- function openInDefaultApp(path) {
1695
- const cmd = buildOpenCommand(process.platform, path);
1698
+ function isWSL() {
1699
+ if (process.env.WSL_DISTRO_NAME) return true;
1700
+ try {
1701
+ const ver = readFileSync5("/proc/version", "utf-8");
1702
+ return /microsoft|wsl/i.test(ver);
1703
+ } catch {
1704
+ return false;
1705
+ }
1706
+ }
1707
+ function commandExists(command) {
1708
+ const PATH = process.env.PATH ?? "";
1709
+ const sep = process.platform === "win32" ? ";" : ":";
1710
+ const exts = process.platform === "win32" ? (process.env.PATHEXT?.split(";") ?? [".EXE", ".CMD", ".BAT"]).map(
1711
+ (e) => e.toLowerCase()
1712
+ ) : [""];
1713
+ for (const dir of PATH.split(sep)) {
1714
+ if (!dir) continue;
1715
+ const full = join4(dir, command);
1716
+ for (const ext of exts) {
1717
+ try {
1718
+ if (existsSync4(full + ext)) return true;
1719
+ } catch {
1720
+ }
1721
+ }
1722
+ }
1723
+ return false;
1724
+ }
1725
+ async function openInDefaultApp(path) {
1726
+ const wslView = isWSL() && commandExists("wslview");
1727
+ const cmd = buildOpenCommand(process.platform, path, { wslView });
1696
1728
  if (!cmd) {
1697
1729
  process.stderr.write(
1698
1730
  `agenthud: --open: no known opener for platform "${process.platform}"
@@ -1700,26 +1732,64 @@ function openInDefaultApp(path) {
1700
1732
  );
1701
1733
  return;
1702
1734
  }
1703
- try {
1704
- const child = spawn(cmd.command, cmd.args, {
1705
- detached: true,
1706
- stdio: "ignore"
1707
- });
1735
+ if (!commandExists(cmd.command)) {
1736
+ process.stderr.write(
1737
+ `agenthud: --open: '${cmd.command}' is not on PATH; cannot open ${path}
1738
+ `
1739
+ );
1740
+ if (isWSL()) {
1741
+ process.stderr.write(
1742
+ "agenthud: hint: on WSL install `wslu` (provides wslview) so files open with the Windows host's default app \u2014 e.g. `sudo apt install wslu`\n"
1743
+ );
1744
+ }
1745
+ return;
1746
+ }
1747
+ await new Promise((resolve2) => {
1748
+ let settled = false;
1749
+ const finish = () => {
1750
+ if (settled) return;
1751
+ settled = true;
1752
+ resolve2();
1753
+ };
1754
+ let child;
1755
+ try {
1756
+ child = spawn(cmd.command, cmd.args, {
1757
+ detached: true,
1758
+ stdio: "ignore"
1759
+ });
1760
+ } catch (err) {
1761
+ const msg = err instanceof Error ? err.message : String(err);
1762
+ process.stderr.write(`agenthud: --open failed: ${msg}
1763
+ `);
1764
+ finish();
1765
+ return;
1766
+ }
1708
1767
  child.on("error", (err) => {
1709
1768
  process.stderr.write(`agenthud: --open failed: ${err.message}
1710
1769
  `);
1770
+ finish();
1711
1771
  });
1712
- child.unref();
1713
- } catch (err) {
1714
- const msg = err instanceof Error ? err.message : String(err);
1715
- process.stderr.write(`agenthud: --open failed: ${msg}
1716
- `);
1717
- }
1772
+ child.on("exit", (code) => {
1773
+ if (code !== null && code !== 0) {
1774
+ process.stderr.write(
1775
+ `agenthud: --open: '${cmd.command}' exited with code ${code}
1776
+ `
1777
+ );
1778
+ }
1779
+ finish();
1780
+ });
1781
+ setTimeout(() => {
1782
+ if (!settled) {
1783
+ child.unref();
1784
+ finish();
1785
+ }
1786
+ }, 200).unref();
1787
+ });
1718
1788
  }
1719
1789
 
1720
1790
  // src/data/summariesIndex.ts
1721
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1722
- import { join as join4 } from "path";
1791
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1792
+ import { join as join5 } from "path";
1723
1793
  var INDEX_HEADER_MARKER = "<!-- agenthud-summaries-index -->";
1724
1794
  var BACKLINK_START_MARKER = "<!-- agenthud-backlinks-start -->";
1725
1795
  var BACKLINK_END_MARKER = "<!-- agenthud-backlinks-end -->";
@@ -1761,7 +1831,7 @@ function parseSummaryFilename(name) {
1761
1831
  return null;
1762
1832
  }
1763
1833
  function listSummaries(dir) {
1764
- if (!existsSync4(dir)) return [];
1834
+ if (!existsSync5(dir)) return [];
1765
1835
  let names;
1766
1836
  try {
1767
1837
  names = readdirSync2(dir);
@@ -1926,9 +1996,9 @@ function regenerateIndex(summariesDir2) {
1926
1996
  const entries = listSummaries(summariesDir2);
1927
1997
  const snippets = /* @__PURE__ */ new Map();
1928
1998
  for (const entry of entries) {
1929
- const path = join4(summariesDir2, entry.filename);
1999
+ const path = join5(summariesDir2, entry.filename);
1930
2000
  try {
1931
- const content = readFileSync5(path, "utf-8");
2001
+ const content = readFileSync6(path, "utf-8");
1932
2002
  const snippet = extractContextSnippet(content);
1933
2003
  if (snippet) snippets.set(entry.filename, snippet);
1934
2004
  const block = buildHeaderBlock(entry.filename, entries);
@@ -1939,7 +2009,7 @@ function regenerateIndex(summariesDir2) {
1939
2009
  } catch {
1940
2010
  }
1941
2011
  }
1942
- const indexPath = join4(summariesDir2, "index.md");
2012
+ const indexPath = join5(summariesDir2, "index.md");
1943
2013
  try {
1944
2014
  writeFileSync2(
1945
2015
  indexPath,
@@ -1977,20 +2047,20 @@ function startStderrTicker(label, options2 = {}) {
1977
2047
 
1978
2048
  // src/data/summaryRunner.ts
1979
2049
  function agenthudHomeDir() {
1980
- const dir = join5(homedir3(), ".agenthud");
1981
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
2050
+ const dir = join6(homedir3(), ".agenthud");
2051
+ if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
1982
2052
  return dir;
1983
2053
  }
1984
2054
  function summariesDir() {
1985
- const dir = join5(agenthudHomeDir(), "summaries");
1986
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
2055
+ const dir = join6(agenthudHomeDir(), "summaries");
2056
+ if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
1987
2057
  return dir;
1988
2058
  }
1989
2059
  function promptFilename(kind) {
1990
2060
  return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
1991
2061
  }
1992
2062
  function userPromptPath(kind) {
1993
- return join5(homedir3(), ".agenthud", promptFilename(kind));
2063
+ return join6(homedir3(), ".agenthud", promptFilename(kind));
1994
2064
  }
1995
2065
  function formatPromptSource(kind, override) {
1996
2066
  if (kind === "daily" && override) {
@@ -2003,25 +2073,25 @@ function formatPromptSource(kind, override) {
2003
2073
  }
2004
2074
  function templatePath(kind) {
2005
2075
  const here = dirname2(fileURLToPath2(import.meta.url));
2006
- return join5(here, "templates", promptFilename(kind));
2076
+ return join6(here, "templates", promptFilename(kind));
2007
2077
  }
2008
2078
  function dateKey(d) {
2009
2079
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
2010
2080
  }
2011
2081
  function dailyCachePath(date) {
2012
- return join5(summariesDir(), `${dateKey(date)}.md`);
2082
+ return join6(summariesDir(), `${dateKey(date)}.md`);
2013
2083
  }
2014
2084
  function rangeCachePath(from, to) {
2015
- return join5(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
2085
+ return join6(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
2016
2086
  }
2017
2087
  function isSameLocalDay2(a, b) {
2018
2088
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2019
2089
  }
2020
2090
  function ensureUserPromptFile(kind) {
2021
2091
  const p = userPromptPath(kind);
2022
- if (existsSync5(p)) return;
2092
+ if (existsSync6(p)) return;
2023
2093
  const dir = dirname2(p);
2024
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
2094
+ if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
2025
2095
  try {
2026
2096
  copyFileSync(templatePath(kind), p);
2027
2097
  } catch {
@@ -2030,14 +2100,14 @@ function ensureUserPromptFile(kind) {
2030
2100
  function resolvePrompt(kind, override) {
2031
2101
  if (override) return override;
2032
2102
  const p = userPromptPath(kind);
2033
- if (existsSync5(p)) {
2103
+ if (existsSync6(p)) {
2034
2104
  try {
2035
- return readFileSync6(p, "utf-8");
2105
+ return readFileSync7(p, "utf-8");
2036
2106
  } catch {
2037
2107
  }
2038
2108
  }
2039
2109
  try {
2040
- return readFileSync6(templatePath(kind), "utf-8");
2110
+ return readFileSync7(templatePath(kind), "utf-8");
2041
2111
  } catch {
2042
2112
  return "Summarize the input below.";
2043
2113
  }
@@ -2213,9 +2283,9 @@ async function generateDailySummary(opts) {
2213
2283
  const isToday = isSameLocalDay2(opts.date, opts.today);
2214
2284
  const cached = dailyCachePath(opts.date);
2215
2285
  const dateLabel = dateKey(opts.date);
2216
- if (!isToday && !opts.force && existsSync5(cached)) {
2286
+ if (!isToday && !opts.force && existsSync6(cached)) {
2217
2287
  try {
2218
- const content = readFileSync6(cached, "utf-8");
2288
+ const content = readFileSync7(cached, "utf-8");
2219
2289
  if (opts.announce) {
2220
2290
  process.stderr.write(`cached summary from ${cached}
2221
2291
  `);
@@ -2256,21 +2326,48 @@ async function generateDailySummary(opts) {
2256
2326
  });
2257
2327
  const reportBytes = Buffer.byteLength(reportMarkdown, "utf-8");
2258
2328
  const estimatedTokens = Math.ceil(reportBytes / 4);
2329
+ const reportLines = reportMarkdown.split("\n");
2330
+ const sessionCount = reportLines.filter((l) => l.startsWith("## ")).length;
2331
+ const activityCount = reportLines.filter(
2332
+ (l) => /^\[\d{2}:\d{2}\]/.test(l)
2333
+ ).length;
2334
+ const commitCount = reportLines.filter(
2335
+ (l) => /^\[\d{2}:\d{2}\] ◆/.test(l)
2336
+ ).length;
2259
2337
  if (opts.announce) {
2260
- const reportLines = reportMarkdown.split("\n");
2261
- const sessionCount = reportLines.filter((l) => l.startsWith("## ")).length;
2262
- const activityCount = reportLines.filter(
2263
- (l) => /^\[\d{2}:\d{2}\]/.test(l)
2264
- ).length;
2265
- const commitCount = reportLines.filter(
2266
- (l) => /^\[\d{2}:\d{2}\] ◆/.test(l)
2267
- ).length;
2268
2338
  const sizeKb = (reportBytes / 1024).toFixed(1);
2269
2339
  process.stderr.write(
2270
2340
  `input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB \u2248 ${estimatedTokens.toLocaleString()} tokens)
2271
2341
  `
2272
2342
  );
2273
2343
  }
2344
+ if (sessionCount === 0 && activityCount === 0 && commitCount === 0) {
2345
+ const stub = `## Context
2346
+
2347
+ No activity recorded for ${dateLabel}.
2348
+ No claude call was issued \u2014 the report was empty.
2349
+ `;
2350
+ try {
2351
+ writeFileSync3(cached, stub, "utf-8");
2352
+ } catch {
2353
+ }
2354
+ if (opts.announce) {
2355
+ process.stderr.write(
2356
+ `${dateLabel} has no activity \u2014 wrote stub to ${cached}, skipped claude
2357
+ `
2358
+ );
2359
+ }
2360
+ if (opts.streamToStdout) {
2361
+ process.stdout.write(stub);
2362
+ }
2363
+ return {
2364
+ code: 0,
2365
+ markdown: stub,
2366
+ fromCache: false,
2367
+ skipped: false,
2368
+ usage: null
2369
+ };
2370
+ }
2274
2371
  if (opts.confirmBeforeSpawn) {
2275
2372
  const proceed = await opts.confirmBeforeSpawn();
2276
2373
  if (!proceed) {
@@ -2361,10 +2458,10 @@ async function runSummary(options2) {
2361
2458
  }
2362
2459
  }
2363
2460
  if (options2.open && res.code === 0 && !res.skipped) {
2364
- openInDefaultApp(dailyCachePath(options2.date));
2461
+ await openInDefaultApp(dailyCachePath(options2.date));
2365
2462
  }
2366
2463
  if (options2.openIndex && res.code === 0 && !res.skipped) {
2367
- openInDefaultApp(join5(summariesDir(), "index.md"));
2464
+ await openInDefaultApp(join6(summariesDir(), "index.md"));
2368
2465
  }
2369
2466
  return res.code;
2370
2467
  }
@@ -2379,10 +2476,10 @@ async function runRangeSummary(options2) {
2379
2476
  options2.force,
2380
2477
  dates,
2381
2478
  options2.today,
2382
- existsSync5(rangeCache)
2479
+ existsSync6(rangeCache)
2383
2480
  )) {
2384
2481
  try {
2385
- const content = readFileSync6(rangeCache, "utf-8");
2482
+ const content = readFileSync7(rangeCache, "utf-8");
2386
2483
  process.stderr.write(
2387
2484
  `cached range summary from ${rangeCache}
2388
2485
  `
@@ -2395,9 +2492,9 @@ async function runRangeSummary(options2) {
2395
2492
  regenerateIndex(summariesDir());
2396
2493
  } catch {
2397
2494
  }
2398
- if (options2.open) openInDefaultApp(rangeCache);
2495
+ if (options2.open) await openInDefaultApp(rangeCache);
2399
2496
  if (options2.openIndex) {
2400
- openInDefaultApp(join5(summariesDir(), "index.md"));
2497
+ await openInDefaultApp(join6(summariesDir(), "index.md"));
2401
2498
  }
2402
2499
  return 0;
2403
2500
  } catch {
@@ -2407,7 +2504,7 @@ async function runRangeSummary(options2) {
2407
2504
  let missingCount = 0;
2408
2505
  for (const d of dates) {
2409
2506
  const isToday = isSameLocalDay2(d, options2.today);
2410
- if (!isToday && existsSync5(dailyCachePath(d))) cachedCount++;
2507
+ if (!isToday && existsSync6(dailyCachePath(d))) cachedCount++;
2411
2508
  else missingCount++;
2412
2509
  }
2413
2510
  process.stderr.write(
@@ -2426,7 +2523,7 @@ async function runRangeSummary(options2) {
2426
2523
  process.stderr.write(`
2427
2524
  --- ${label} ---
2428
2525
  `);
2429
- const willPrompt = !options2.assumeYes && (isToday || !existsSync5(dailyCachePath(d)));
2526
+ const willPrompt = !options2.assumeYes && (isToday || !existsSync6(dailyCachePath(d)));
2430
2527
  const confirmer = willPrompt ? async () => {
2431
2528
  const hint = isToday ? " (today \u2014 regenerated every time)" : "";
2432
2529
  return ask(`Generate this summary${hint}? [Y/n] `, true);
@@ -2510,15 +2607,15 @@ combining ${dailyMarkdowns.length} daily summaries into range summary...
2510
2607
  regenerateIndex(summariesDir());
2511
2608
  } catch {
2512
2609
  }
2513
- if (options2.open) openInDefaultApp(rangeCache);
2610
+ if (options2.open) await openInDefaultApp(rangeCache);
2514
2611
  if (options2.openIndex) {
2515
- openInDefaultApp(join5(summariesDir(), "index.md"));
2612
+ await openInDefaultApp(join6(summariesDir(), "index.md"));
2516
2613
  }
2517
2614
  return 0;
2518
2615
  }
2519
2616
 
2520
2617
  // src/ui/App.tsx
2521
- import { existsSync as existsSync6, watch } from "fs";
2618
+ import { existsSync as existsSync7, watch } from "fs";
2522
2619
  import { basename as basename3 } from "path";
2523
2620
  import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
2524
2621
  import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
@@ -4063,7 +4160,7 @@ function App({
4063
4160
  useEffect3(() => {
4064
4161
  if (!isWatchMode) return;
4065
4162
  const projectsDir = getProjectsDir();
4066
- const usePolling = process.platform === "linux" || !existsSync6(projectsDir);
4163
+ const usePolling = process.platform === "linux" || !existsSync7(projectsDir);
4067
4164
  if (usePolling) {
4068
4165
  const timer = setInterval(
4069
4166
  () => refreshRef.current(),
@@ -4605,10 +4702,10 @@ function installAltScreenCleanup() {
4605
4702
  }
4606
4703
 
4607
4704
  // src/utils/legacyConfig.ts
4608
- import { join as join6, resolve } from "path";
4705
+ import { join as join7, resolve } from "path";
4609
4706
  function isLegacyProjectConfig(cwd, home) {
4610
- const legacy = resolve(join6(cwd, ".agenthud", "config.yaml"));
4611
- const global = resolve(join6(home, ".agenthud", "config.yaml"));
4707
+ const legacy = resolve(join7(cwd, ".agenthud", "config.yaml"));
4708
+ const global = resolve(join7(home, ".agenthud", "config.yaml"));
4612
4709
  return legacy !== global;
4613
4710
  }
4614
4711
 
@@ -4628,8 +4725,8 @@ if (options.command === "version") {
4628
4725
  console.log(getVersion());
4629
4726
  process.exit(0);
4630
4727
  }
4631
- var legacyConfig = join7(process.cwd(), ".agenthud", "config.yaml");
4632
- if (isLegacyProjectConfig(process.cwd(), homedir5()) && existsSync7(legacyConfig)) {
4728
+ var legacyConfig = join8(process.cwd(), ".agenthud", "config.yaml");
4729
+ if (isLegacyProjectConfig(process.cwd(), homedir5()) && existsSync8(legacyConfig)) {
4633
4730
  console.log(
4634
4731
  "The project-level config file (.agenthud/config.yaml) is no longer supported."
4635
4732
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.12.1",
3
+ "version": "0.12.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",