@kill-switch/agent-guard 0.1.0 → 0.1.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/dist/report.js CHANGED
@@ -1,12 +1,101 @@
1
1
  /**
2
2
  * Shared status report — the single computation behind `agent-guard status` and
3
3
  * `ks guard status`, so both emit an identical JSON shape and never drift.
4
+ *
5
+ * Two halves:
6
+ * - the dollar budget (session + daily-rolling), always present; and
7
+ * - the subscription rate-limit standing (5-hour + weekly pacing), present
8
+ * once we've seen Anthropic's unified headers via the proxy, or estimated
9
+ * when the user has pinned a plan tier. Alert-only — never blocks.
4
10
  */
5
11
  import { loadConfig } from "./config.js";
6
12
  import { isPaused, pauseExpiry } from "./config.js";
7
13
  import { loadLedger, rollingDailyCost } from "./ledger.js";
8
14
  import { evaluate } from "./budget.js";
15
+ import { loadLimitsState } from "./limits.js";
16
+ import { assessSnapshot, worstLevel } from "./pacing.js";
17
+ import { estimateSnapshot } from "./estimate.js";
9
18
  const DAY_MS = 24 * 60 * 60 * 1000;
19
+ function buildLimitsReport(cfg, ledger, now) {
20
+ const state = loadLimitsState();
21
+ const thresholds = cfg.limits;
22
+ const plan = cfg.limits.plan;
23
+ // Prefer real header data when we have it.
24
+ if (state.snapshot) {
25
+ const windows = assessSnapshot(state.snapshot, thresholds, now);
26
+ return {
27
+ source: "headers",
28
+ plan,
29
+ subscriptionDetected: state.subscriptionDetected,
30
+ observedAt: state.snapshot.observedAt,
31
+ windows,
32
+ level: worstLevel(windows),
33
+ };
34
+ }
35
+ // Otherwise estimate, but only when the user pinned a tier (opt-in, fuzzy).
36
+ if (plan === "pro" || plan === "max5" || plan === "max20") {
37
+ const snap = estimateSnapshot(ledger, plan, now);
38
+ const windows = assessSnapshot(snap, thresholds, now);
39
+ return {
40
+ source: "estimated",
41
+ plan,
42
+ subscriptionDetected: state.subscriptionDetected,
43
+ observedAt: snap.observedAt,
44
+ windows,
45
+ level: worstLevel(windows),
46
+ };
47
+ }
48
+ return {
49
+ source: "none",
50
+ plan,
51
+ subscriptionDetected: state.subscriptionDetected,
52
+ observedAt: null,
53
+ windows: [],
54
+ level: "ok",
55
+ };
56
+ }
57
+ function bar(frac) {
58
+ const pct = Math.max(0, Math.min(100, Math.round(frac * 100)));
59
+ const filled = Math.round(pct / 5);
60
+ return `[${"█".repeat(filled)}${"░".repeat(20 - filled)}]`;
61
+ }
62
+ function ageString(observedAt, now) {
63
+ const ms = now - observedAt;
64
+ if (ms < 60_000)
65
+ return "just now";
66
+ if (ms < 3_600_000)
67
+ return `${Math.round(ms / 60_000)}m ago`;
68
+ if (ms < 86_400_000)
69
+ return `${Math.round(ms / 3_600_000)}h ago`;
70
+ return `${Math.round(ms / 86_400_000)}d ago`;
71
+ }
72
+ /**
73
+ * Render the subscription rate-limit section as plain text lines (no color), so
74
+ * both the `agent-guard` and `ks guard` status views stay identical. Returns an
75
+ * empty array when there's nothing useful to show.
76
+ */
77
+ export function formatLimitsLines(limits, now = Date.now()) {
78
+ if (limits.source === "none") {
79
+ // Only nudge if they haven't opted into either path.
80
+ if (!limits.subscriptionDetected) {
81
+ return [
82
+ "Claude Code plan limits: unknown.",
83
+ " Run `ks guard proxy` and point Claude Code at it for exact 5-hour + weekly usage,",
84
+ " or set your tier (`ks guard config --plan max5`) for an estimate.",
85
+ ];
86
+ }
87
+ return [];
88
+ }
89
+ const icon = limits.level === "danger" ? "🟥" : limits.level === "warn" ? "🟡" : "🟢";
90
+ const tag = limits.source === "estimated" ? " (estimated — run the proxy for exact)" : "";
91
+ const lines = [`${icon} Claude Code plan limits${tag} · observed ${limits.observedAt ? ageString(limits.observedAt, now) : "—"}`];
92
+ for (const w of limits.windows) {
93
+ // w.message already leads with "<window> limit NN% used, …", so the bar
94
+ // carries the visual and the message carries the numbers + pacing.
95
+ lines.push(` ${bar(w.utilization)} ${w.message}`);
96
+ }
97
+ return lines;
98
+ }
10
99
  /** Build the current status report from the on-disk config + ledger. */
11
100
  export function buildStatusReport(now = Date.now()) {
12
101
  const cfg = loadConfig();
@@ -26,5 +115,6 @@ export function buildStatusReport(now = Date.now()) {
26
115
  paused: isPaused(now),
27
116
  pauseUntil: pauseExpiry(),
28
117
  sessions,
118
+ limits: buildLimitsReport(cfg, ledger, now),
29
119
  };
30
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kill-switch/agent-guard",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Kill Switch for coding agents — stop runaway Claude Code / Cursor / Aider sessions from racking up an LLM bill. Native hook + token-metering proxy with per-session and daily-rolling budgets.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "build": "tsc",
14
14
  "dev": "tsx src/cli.ts",
15
- "test": "vitest run"
15
+ "test": "vitest run",
16
+ "e2e": "tsc && node scripts/e2e-subscription.mjs"
16
17
  },
17
18
  "dependencies": {
18
19
  "commander": "^12.1.0"