agents-deck 1.21.0 → 1.22.0

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.
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agents-deck</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-B-TDYmpb.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-BDP3vsJ3.css">
8
+ <script type="module" crossorigin src="/assets/index-Dc4xConU.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BRoNoC-Z.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-deck",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,93 @@
1
+ // Fetches historical usage from the `ccusage` CLI (https://github.com/ccusage/ccusage).
2
+ // ccusage reads the local ~/.claude (and other agent) logs and reports cost +
3
+ // token usage grouped by day. We shell out to it via `npx -y ccusage@latest`
4
+ // — it is NOT a dependency; npx fetches it on first run (cached afterwards).
5
+ //
6
+ // The whole backend is: spawn the CLI with --json, slice the JSON out of stdout
7
+ // (npx can print banner noise), JSON.parse, cache. Ported from the task-board
8
+ // project's three Next.js routes, collapsed to one daily fetch.
9
+ import { spawn } from "node:child_process";
10
+
11
+ const CACHE_MS = 120_000; // 2 min — ccusage spawn is heavy; modal is manual-open
12
+ const TIMEOUT_MS = 90_000;
13
+
14
+ const _cache = new Map(); // key `${since}|${until}` → { result, at }
15
+
16
+ // Run `ccusage <args> --json` and resolve its raw stdout.
17
+ function runCcusage(args) {
18
+ return new Promise((resolve, reject) => {
19
+ // shell:true + windowsHide:true so `npx` resolves on Windows without a
20
+ // popup console. ccusage@latest is pinned so behavior is stable.
21
+ const child = spawn("npx", ["-y", "ccusage@latest", ...args], {
22
+ shell: true,
23
+ windowsHide: true,
24
+ });
25
+ let out = "", err = "";
26
+ const timer = setTimeout(() => {
27
+ child.kill();
28
+ reject(new Error("ccusage timed out"));
29
+ }, TIMEOUT_MS);
30
+ child.stdout.on("data", d => { out += d; });
31
+ child.stderr.on("data", d => { err += d; });
32
+ child.on("error", e => { clearTimeout(timer); reject(e); });
33
+ child.on("close", code => {
34
+ clearTimeout(timer);
35
+ if (code === 0) resolve(out);
36
+ else reject(new Error(err.trim() || `ccusage exited ${code}`));
37
+ });
38
+ });
39
+ }
40
+
41
+ // ccusage prints the JSON object somewhere in stdout; slice first { to last }.
42
+ function extractJson(out) {
43
+ const start = out.indexOf("{");
44
+ const end = out.lastIndexOf("}");
45
+ if (start === -1 || end === -1) throw new Error("no JSON in ccusage output");
46
+ return JSON.parse(out.slice(start, end + 1));
47
+ }
48
+
49
+ // YYYYMMDD for the CLI's --since/--until.
50
+ function toCliDate(d) {
51
+ return d.toISOString().slice(0, 10).replace(/-/g, "");
52
+ }
53
+
54
+ /**
55
+ * Fetch daily usage from ccusage for a date range.
56
+ * @param {{ since?: string, until?: string, force?: boolean }} opts
57
+ * since/until are YYYYMMDD strings (CLI format). Defaults to last 30 days.
58
+ * @returns {{ ok, days, totals, since, until, fetchedAt } | { ok:false, error }}
59
+ */
60
+ export async function fetchCcusageDaily({ since, until, force = false } = {}) {
61
+ const now = Date.now();
62
+ const sinceArg = since || toCliDate(new Date(now - 30 * 86400_000));
63
+ const key = `${sinceArg}|${until ?? ""}`;
64
+
65
+ const cached = _cache.get(key);
66
+ if (!force && cached && now - cached.at < CACHE_MS) return cached.result;
67
+
68
+ let result;
69
+ try {
70
+ const args = ["daily", "--json", "--since", sinceArg];
71
+ if (until) args.push("--until", until);
72
+ const raw = extractJson(await runCcusage(args));
73
+ const days = Array.isArray(raw.daily) ? raw.daily : [];
74
+ result = {
75
+ ok: true,
76
+ days,
77
+ totals: raw.totals ?? null,
78
+ since: sinceArg,
79
+ until: until ?? null,
80
+ fetchedAt: now,
81
+ };
82
+ } catch (err) {
83
+ console.error("agents-deck ccusage: fetch failed:", err?.message ?? err);
84
+ result = { ok: false, error: String(err?.message ?? err), fetchedAt: now };
85
+ }
86
+
87
+ _cache.set(key, { result, at: now });
88
+ return result;
89
+ }
90
+
91
+ export function invalidateCcusageCache() {
92
+ _cache.clear();
93
+ }
@@ -1023,6 +1023,18 @@ async function handleCodexQuota(req, res) {
1023
1023
  send(res, 200, quota);
1024
1024
  }
1025
1025
 
1026
+ async function handleCcusage(req, res) {
1027
+ const { fetchCcusageDaily } = await import(
1028
+ pathToFileURL(join(PKG_ROOT, "src/server/ccusage.mjs")).href
1029
+ );
1030
+ const url = new URL(req.url, "http://localhost");
1031
+ const force = url.searchParams.get("refresh") === "1";
1032
+ const since = url.searchParams.get("since") || undefined;
1033
+ const until = url.searchParams.get("until") || undefined;
1034
+ const data = await fetchCcusageDaily({ since, until, force });
1035
+ send(res, 200, data);
1036
+ }
1037
+
1026
1038
  function handleHealth(_req, res) {
1027
1039
  send(res, 200, {
1028
1040
  ok: true,
@@ -1092,6 +1104,7 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
1092
1104
  if (req.method === "GET" && url.pathname === "/api/quota") return handleQuota(req, res);
1093
1105
  if (req.method === "GET" && url.pathname === "/api/codex-usage") return handleCodexUsage(req, res);
1094
1106
  if (req.method === "GET" && url.pathname === "/api/codex-quota") return handleCodexQuota(req, res);
1107
+ if (req.method === "GET" && url.pathname === "/api/ccusage") return handleCcusage(req, res);
1095
1108
 
1096
1109
  if (req.method === "GET" && url.pathname === "/api/events") {
1097
1110
  const since = Number(url.searchParams.get("since") ?? 0);