agent-dag 1.35.30 → 1.35.31

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.
@@ -40,7 +40,7 @@
40
40
  document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
41
41
  })();
42
42
  </script>
43
- <script type="module" crossorigin src="/assets/index-DCAbQHgs.js"></script>
43
+ <script type="module" crossorigin src="/assets/index-Cx_7gonm.js"></script>
44
44
  <link rel="stylesheet" crossorigin href="/assets/index-CIKdHXRn.css">
45
45
  </head>
46
46
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.35.30",
3
+ "version": "1.35.31",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { CODEX_HOME } from "./codex-dir.mjs";
13
13
  import { getCodexAuth, forceCodexRefresh, isCredentialHost } from "./codex-auth.mjs";
14
14
  import { PRODUCT } from "./brand.mjs";
15
+ import { resetLabel } from "./reset-label.mjs";
15
16
 
16
17
  // Resolved by codex-dir.mjs rather than here. This file used to spell it
17
18
  // `process.env.CODEX_HOME ?? join(homedir(), ".codex")`, which keeps an empty
@@ -76,13 +77,15 @@ function resetAt(o) {
76
77
  return num(o?.resets_at) ?? num(o?.resetsAt) ?? num(o?.reset_at) ?? null;
77
78
  }
78
79
 
79
- /** "Jun 18, 4:09pm" — matches the Claude quota formatting so both read alike. */
80
- function fmtReset(unixSec) {
81
- if (!unixSec) return null;
82
- return new Date(unixSec * 1000).toLocaleString("en-US", {
83
- month: "short", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true,
84
- }).replace(",", "").toLowerCase().replace(/\s+am/, "am").replace(/\s+pm/, "pm");
85
- }
80
+ /** "Jun 18, 4:09pm" — matches the Claude quota formatting so both read alike.
81
+ *
82
+ * It did not, and the sentence above is why #374 called this one out: this
83
+ * copy passed the same options to `toLocaleString` and then stripped the comma
84
+ * and lower-cased the whole string, so it printed "jun 18 4:09pm" where the
85
+ * Claude lane one row up printed "Jun 18, 4:09pm". Swept over 2,794 instants
86
+ * the two disagreed on every one. Both read from reset-label.mjs now, and the
87
+ * claim above is true for the first time. */
88
+ const fmtReset = resetLabel;
86
89
 
87
90
  // ── window classification ──────────────────────────────────────────────────
88
91
  // Slot position is NOT the lane. Free plans return a weekly window in the
@@ -8,6 +8,11 @@
8
8
  // entirely. It is always best-effort — the deck's core function does not
9
9
  // depend on it, so a failure is reported and then ignored.
10
10
  import { run, runDetached } from "./exec.mjs";
11
+ // The version comparator was written out here as well, identical apart from a
12
+ // type guard this copy lacked, and only the self-update one was under test
13
+ // (#374). No cycle: self-update.mjs imports node:* and ./exec.mjs, which this
14
+ // file already imports itself.
15
+ import { isOlder } from "./self-update.mjs";
11
16
  import { bootstrapUv, existingBootstrappedUv } from "./uv-bootstrap.mjs";
12
17
  import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
13
18
  import { join, posix as posixPath, win32 as winPath } from "node:path";
@@ -268,17 +273,6 @@ async function latestOnPypi() {
268
273
  }
269
274
  }
270
275
 
271
- /** Numeric-segment version compare; returns true when `a` is older than `b`. */
272
- function isOlder(a, b) {
273
- const seg = (v) => v.split(/[.\-+]/).map(n => parseInt(n, 10)).map(n => Number.isNaN(n) ? 0 : n);
274
- const x = seg(a), y = seg(b);
275
- for (let i = 0; i < Math.max(x.length, y.length); i++) {
276
- const d = (x[i] ?? 0) - (y[i] ?? 0);
277
- if (d !== 0) return d < 0;
278
- }
279
- return false;
280
- }
281
-
282
276
  /**
283
277
  * Upgrade claude-swap in the background when a newer release exists.
284
278
  *
@@ -35,6 +35,7 @@ import { readFile } from "node:fs/promises";
35
35
  import { join } from "node:path";
36
36
  import { homedir } from "node:os";
37
37
  import { PRODUCT } from "./brand.mjs";
38
+ import { resetLabelIso } from "./reset-label.mjs";
38
39
 
39
40
  const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
40
41
  const BETA_HEADER = "oauth-2025-04-20";
@@ -83,15 +84,13 @@ async function readOAuthToken() {
83
84
  }
84
85
 
85
86
  // ISO-8601 → "Jun 19, 1:19pm" (local time, matching the CLI display format).
86
- function fmtResetIso(iso) {
87
- if (!iso) return null;
88
- const d = new Date(iso);
89
- if (isNaN(d.getTime())) return null;
90
- // "Jun 19, 1:19 PM" "Jun 19, 1:19pm" (matches the CLI display format)
91
- return d.toLocaleString("en-US", {
92
- month: "short", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true,
93
- }).replace(/\s+(AM|PM)/, (_, p) => p.toLowerCase());
94
- }
87
+ //
88
+ // The body moved to reset-label.mjs in #374: codex-quota.mjs had a copy that
89
+ // claimed in its own comment to match this one and did not, so the Codex lanes
90
+ // and the Claude lanes printed the same instant two different ways in the same
91
+ // panel. This rendering is the one both surfaces use now. The alias stays so
92
+ // the four call sites below read the way they always have.
93
+ const fmtResetIso = resetLabelIso;
95
94
 
96
95
  function isoToSec(iso) {
97
96
  if (!iso) return null;
@@ -0,0 +1,78 @@
1
+ // When a quota window resets, written one way — for the Claude lanes and the
2
+ // Codex lanes both, which land in the same panel.
3
+ //
4
+ // WHY THIS MODULE EXISTS (#374). quota.mjs had `fmtResetIso` and codex-quota.mjs
5
+ // had `fmtReset`. They passed identical option bags to `toLocaleString` and then
6
+ // post-processed the result differently, and codex-quota's own doc comment said
7
+ // of its copy: "matches the Claude quota formatting so both read alike." It did
8
+ // not. Over the same instant:
9
+ //
10
+ // toLocaleString : "Jun 18, 4:09 PM"
11
+ // quota.mjs : "Jun 18, 4:09pm"
12
+ // codex-quota.mjs: "jun 18 4:09pm" ← comma stripped, month lower-cased
13
+ //
14
+ // Swept over 2,794 instants across 40 days, the two disagreed on every single
15
+ // one — not at a boundary, on all of them, because the difference is two
16
+ // unconditional string operations rather than a tier that rarely fires. Both
17
+ // strings surface in the same usage panel, one lane above the other.
18
+ //
19
+ // The Claude rendering is the one kept, on the comment's own terms: it is the
20
+ // one the other claimed to match, and it is the one that reads like a date —
21
+ // `Intl` capitalises the month and puts the comma in for the locale's own
22
+ // reasons, and stripping both is a hand edit to output that was already right.
23
+ // This is a FIX to what the Codex lanes print, not a neutral merge, and it is
24
+ // the only visible change in this consolidation.
25
+ //
26
+ // The one genuine difference between the copies was the input unit — Codex
27
+ // answers with a Unix timestamp in seconds and Anthropic with an ISO-8601
28
+ // string — so that is the wrapper below rather than a second formatter.
29
+ //
30
+ // A module of its own rather than one importing the other: quota.mjs reads
31
+ // Claude's OAuth credentials off disk, and having the Codex path import that to
32
+ // reach a date formatter would drag the whole Claude credential chain into a
33
+ // request that has nothing to do with it.
34
+
35
+ /** en-US on purpose, not the host locale. This string is generated on the
36
+ * server and shipped to a browser whose locale nobody here has asked, so a
37
+ * server set to de-DE would otherwise send "18. Juni, 16:09" into a panel
38
+ * written in English. Both copies already hardcoded en-US; it is written down
39
+ * here so the next reader knows it was a decision. */
40
+ const OPTS = { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true };
41
+
42
+ /** The rendering itself, over a Date both entry points have already validated.
43
+ * The meridiem is lower-cased and its space removed — the only edit made to
44
+ * what `Intl` produces — because "4:09 PM" shouts in a lane label that is a
45
+ * few characters of chrome wide. */
46
+ function label(d) {
47
+ return d.toLocaleString("en-US", OPTS).replace(/\s+(AM|PM)/, (_, p) => p.toLowerCase());
48
+ }
49
+
50
+ /** "Jun 18, 4:09pm" from a Unix timestamp in seconds, which is how the Codex
51
+ * usage endpoint spells a reset time. Null when there is no reset to name.
52
+ *
53
+ * The validity check is inherited from the ISO copy, which had one where the
54
+ * seconds copy did not: a finite but absurd number — the only shape that gets
55
+ * past the falsy guard and still fails to be a date — used to render as the
56
+ * literal string "invalid date" in a quota lane. Nothing observed sends one,
57
+ * so this is hardening rather than a fix, and it is the safer of the two
58
+ * behaviours in the same way `isOlder`'s type guard is. */
59
+ export function resetLabel(unixSec) {
60
+ if (!unixSec) return null;
61
+ const d = new Date(unixSec * 1000);
62
+ if (isNaN(d.getTime())) return null;
63
+ return label(d);
64
+ }
65
+
66
+ /** The same label from an ISO-8601 instant, which is how Anthropic's usage
67
+ * endpoint spells a reset time. Invalid and absent both answer null, so a
68
+ * malformed field renders as no reset rather than "Invalid Date".
69
+ *
70
+ * Not written as `resetLabel(d.getTime() / 1000)`: that would send an instant
71
+ * at the Unix epoch through the falsy guard above and answer null for a date
72
+ * this function has already established is valid. */
73
+ export function resetLabelIso(iso) {
74
+ if (!iso) return null;
75
+ const d = new Date(iso);
76
+ if (isNaN(d.getTime())) return null;
77
+ return label(d);
78
+ }
@@ -69,9 +69,18 @@ const _inflight = new Map();
69
69
 
70
70
  // ── version comparison ───────────────────────────────────────────────────────
71
71
 
72
- /** True when `a` sorts before `b`. Numeric-segment compare, same shape as the
73
- * one in cswap-install.mjs non-numeric segments count as 0, missing
74
- * segments pad with 0, so "1.30" < "1.30.1" and "1.9.0" < "1.10.0". */
72
+ /** True when `a` sorts before `b`. Numeric-segment compare non-numeric
73
+ * segments count as 0, missing segments pad with 0, so "1.30" < "1.30.1" and
74
+ * "1.9.0" < "1.10.0".
75
+ *
76
+ * cswap-install.mjs had this written out a second time, without the type guard
77
+ * below, and imports it from here now (#374). The two bodies were identical:
78
+ * swept over 271,441 version-string pairs they disagreed on none, and the
79
+ * guard was the whole difference — `isOlder(null, "1.0.0")` answers false here
80
+ * and threw a TypeError there. Nothing could reach that call with a non-string
81
+ * (both arguments are behind `typeof v === "string"` checks at the one call
82
+ * site), so this is the copy with a test behind it absorbing the one without,
83
+ * not a bug fix. */
75
84
  export function isOlder(a, b) {
76
85
  if (typeof a !== "string" || typeof b !== "string") return false;
77
86
  const seg = (v) => v.split(/[.\-+]/).map(n => parseInt(n, 10)).map(n => Number.isNaN(n) ? 0 : n);