@kill-switch/agent-guard 0.1.2 → 0.1.3

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
@@ -152,6 +152,10 @@ Tune the thresholds (0–1 utilization) if the defaults are too eager:
152
152
  | `--5h-soft` / `--5h-danger` | 5-hour warn / danger utilization | 0.7 / 0.9 |
153
153
  | `--burn-ratio` | pace multiplier that triggers a warning | 1.5 |
154
154
 
155
+ The first time the proxy sees the `unified-*` headers it writes the raw values once to
156
+ `~/.kill-switch/agent-guard/events.jsonl` (`kind: "unified-headers-observed"`) — so you can
157
+ confirm Anthropic's exact value formats with a single `cat`.
158
+
155
159
  > Because subscription mode is alert-only, the "don't run both hook *and* proxy" caveat below
156
160
  > doesn't bite here — running Claude Code through the proxy is exactly what feeds the limit
157
161
  > headers, and dollars no longer gate anything.
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { dispatchAlert, type AlertEvent, type AlertLevel } from "./alert.js";
17
17
  export { startProxy, resolveUpstream, type ProxyOptions } from "./proxy.js";
18
18
  export { runHook } from "./hook.js";
19
19
  export { buildStatusReport, formatLimitsLines, type StatusReport, type LimitsReport } from "./report.js";
20
- export { parseUnifiedHeaders, parseUtilization, parseReset, recordHeaders, loadLimitsState, saveLimitsState, emptyLimitsState, limitNotifyKey, WINDOW_MS, type LimitSnapshot, type WindowState, type LimitsState, type LimitWindow, type HeaderGetter, } from "./limits.js";
20
+ export { parseUnifiedHeaders, parseUtilization, parseReset, recordHeaders, loadLimitsState, saveLimitsState, emptyLimitsState, limitNotifyKey, unifiedHeaderDump, logUnifiedHeaders, WINDOW_MS, type LimitSnapshot, type WindowState, type LimitsState, type LimitWindow, type HeaderGetter, } from "./limits.js";
21
21
  export { assessWindow, assessSnapshot, worstLevel, type PacingAssessment, type PacingLevel, type PacingThresholds, } from "./pacing.js";
22
22
  export { estimateSnapshot, isEstimated, TIER_BUDGETS, type PlanTier, type TierBudget, } from "./estimate.js";
23
23
  export { installHook, setBudget, setLimits, resetLedger, type InstallOptions, type InstallResult, type BudgetPatch, type LimitsPatch, } from "./ops.js";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ export { dispatchAlert } from "./alert.js";
17
17
  export { startProxy, resolveUpstream } from "./proxy.js";
18
18
  export { runHook } from "./hook.js";
19
19
  export { buildStatusReport, formatLimitsLines } from "./report.js";
20
- export { parseUnifiedHeaders, parseUtilization, parseReset, recordHeaders, loadLimitsState, saveLimitsState, emptyLimitsState, limitNotifyKey, WINDOW_MS, } from "./limits.js";
20
+ export { parseUnifiedHeaders, parseUtilization, parseReset, recordHeaders, loadLimitsState, saveLimitsState, emptyLimitsState, limitNotifyKey, unifiedHeaderDump, logUnifiedHeaders, WINDOW_MS, } from "./limits.js";
21
21
  export { assessWindow, assessSnapshot, worstLevel, } from "./pacing.js";
22
22
  export { estimateSnapshot, isEstimated, TIER_BUDGETS, } from "./estimate.js";
23
23
  export { installHook, setBudget, setLimits, resetLedger, } from "./ops.js";
package/dist/limits.d.ts CHANGED
@@ -49,6 +49,8 @@ export interface LimitsState {
49
49
  snapshot: LimitSnapshot | null;
50
50
  /** Dedup flags so a given window/level/reset only alerts once. */
51
51
  notified: Record<string, boolean>;
52
+ /** Epoch ms we first logged the raw unified-* headers (write-once diagnostic). */
53
+ headersLoggedAt?: number;
52
54
  }
53
55
  /** Nominal window durations, used for pacing math when a reset time is unknown. */
54
56
  export declare const WINDOW_MS: Record<LimitWindow, number>;
@@ -81,3 +83,12 @@ export declare function parseReset(raw: string | null | undefined, now: number):
81
83
  export declare function parseUnifiedHeaders(h: HeaderGetter, now: number): LimitSnapshot | null;
82
84
  /** Stable dedup key for a pacing alert: re-alerts when the window resets. */
83
85
  export declare function limitNotifyKey(window: LimitWindow, level: string, resetAt: number | null): string;
86
+ /**
87
+ * Pull every `anthropic-ratelimit-unified-*` header out of a raw record, verbatim.
88
+ * Used for the write-once diagnostic — Anthropic's value *formats* (fraction vs.
89
+ * percent, ISO vs. epoch reset) aren't fully documented, so capturing the raw
90
+ * strings the first time we see them makes verification a single `cat` away.
91
+ */
92
+ export declare function unifiedHeaderDump(rec: Record<string, string | string[] | undefined>): Record<string, string>;
93
+ /** Append a one-time raw-header diagnostic to events.jsonl. Best-effort, never throws. */
94
+ export declare function logUnifiedHeaders(dump: Record<string, string>, now: number): void;
package/dist/limits.js CHANGED
@@ -22,8 +22,8 @@
22
22
  * either a 0–1 fraction or a 0–100 percent; reset is accepted as an ISO 8601
23
23
  * timestamp, an epoch (s or ms), or a relative seconds-until-reset.
24
24
  */
25
- import { readFileSync, writeFileSync, renameSync } from "node:fs";
26
- import { limitsPath, ensureGuardDir } from "./config.js";
25
+ import { readFileSync, writeFileSync, renameSync, appendFileSync } from "node:fs";
26
+ import { limitsPath, eventsPath, ensureGuardDir } from "./config.js";
27
27
  /** Nominal window durations, used for pacing math when a reset time is unknown. */
28
28
  export const WINDOW_MS = {
29
29
  "5h": 5 * 60 * 60 * 1000,
@@ -41,6 +41,7 @@ export function loadLimitsState() {
41
41
  subscriptionDetected: data.subscriptionDetected ?? false,
42
42
  snapshot: data.snapshot ?? null,
43
43
  notified: data.notified ?? {},
44
+ headersLoggedAt: data.headersLoggedAt,
44
45
  };
45
46
  }
46
47
  }
@@ -131,3 +132,30 @@ export function parseUnifiedHeaders(h, now) {
131
132
  export function limitNotifyKey(window, level, resetAt) {
132
133
  return `${window}:${level}:${resetAt ?? 0}`;
133
134
  }
135
+ /**
136
+ * Pull every `anthropic-ratelimit-unified-*` header out of a raw record, verbatim.
137
+ * Used for the write-once diagnostic — Anthropic's value *formats* (fraction vs.
138
+ * percent, ISO vs. epoch reset) aren't fully documented, so capturing the raw
139
+ * strings the first time we see them makes verification a single `cat` away.
140
+ */
141
+ export function unifiedHeaderDump(rec) {
142
+ const out = {};
143
+ for (const [k, v] of Object.entries(rec)) {
144
+ if (v == null)
145
+ continue;
146
+ const key = k.toLowerCase();
147
+ if (key.startsWith("anthropic-ratelimit-unified"))
148
+ out[key] = Array.isArray(v) ? v.join(", ") : v;
149
+ }
150
+ return out;
151
+ }
152
+ /** Append a one-time raw-header diagnostic to events.jsonl. Best-effort, never throws. */
153
+ export function logUnifiedHeaders(dump, now) {
154
+ try {
155
+ ensureGuardDir();
156
+ appendFileSync(eventsPath(), JSON.stringify({ ts: now, kind: "unified-headers-observed", headers: dump }) + "\n");
157
+ }
158
+ catch {
159
+ /* diagnostic only */
160
+ }
161
+ }
package/dist/proxy.js CHANGED
@@ -24,7 +24,7 @@ import { loadLedger, saveLedger, addSessionCost, rollingDailyCost, prune, } from
24
24
  import { evaluate } from "./budget.js";
25
25
  import { dispatchAlert } from "./alert.js";
26
26
  import { assertSafeEndpoint, warnIfUnexpectedHost } from "./net.js";
27
- import { parseUnifiedHeaders, loadLimitsState, saveLimitsState, limitNotifyKey, } from "./limits.js";
27
+ import { parseUnifiedHeaders, recordHeaders, unifiedHeaderDump, logUnifiedHeaders, loadLimitsState, saveLimitsState, limitNotifyKey, } from "./limits.js";
28
28
  import { assessSnapshot, worstLevel } from "./pacing.js";
29
29
  const UPSTREAMS = {
30
30
  anthropic: "https://api.anthropic.com",
@@ -145,10 +145,20 @@ function meter(cfg, ledger, sessionId, parsed, now) {
145
145
  * the real wall).
146
146
  */
147
147
  function captureLimits(cfg, headers, sessionId, now) {
148
- const snap = parseUnifiedHeaders(headers, now);
148
+ // Flatten to a lowercased record so we can both parse and dump the raw values.
149
+ const rec = {};
150
+ headers.forEach((v, k) => {
151
+ rec[k.toLowerCase()] = v;
152
+ });
153
+ const snap = parseUnifiedHeaders(recordHeaders(rec), now);
149
154
  if (!snap)
150
155
  return false;
151
156
  const state = loadLimitsState();
157
+ // Write-once raw-header diagnostic for format verification (`cat events.jsonl`).
158
+ if (!state.headersLoggedAt) {
159
+ logUnifiedHeaders(unifiedHeaderDump(rec), now);
160
+ state.headersLoggedAt = now;
161
+ }
152
162
  state.subscriptionDetected = true;
153
163
  state.snapshot = snap;
154
164
  const assessments = assessSnapshot(snap, cfg.limits, now);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kill-switch/agent-guard",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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": {