@saccolabs/pi-claude-cli 0.4.8 → 0.4.9

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/index.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  } from "./src/process-manager.js";
17
17
  import { getCustomToolDefs, writeMcpConfig } from "./src/mcp-config.js";
18
18
  import { rewriteOverflowMessage } from "./src/overflow.js";
19
+ import { buildRateLimitPayload, rateLimitIdentity } from "./src/rate-limit.js";
19
20
 
20
21
  // Kill all active Claude subprocesses on process exit to prevent orphans
21
22
  process.on("exit", killAllProcesses);
@@ -40,21 +41,18 @@ let lastRateLimitJson: string | undefined;
40
41
  function publishRateLimit(info: Record<string, unknown>): void {
41
42
  const setStatus = uiContext?.ui?.setStatus;
42
43
  if (typeof setStatus !== "function") return;
43
- const payload = JSON.stringify({
44
- status: info.status,
45
- resetsAt: info.resetsAt,
46
- rateLimitType: info.rateLimitType,
47
- overageStatus: info.overageStatus,
48
- isUsingOverage: info.isUsingOverage === true,
49
- observedAt: Math.floor(Date.now() / 1000),
50
- });
44
+ const payload = buildRateLimitPayload(info);
51
45
  // Push only on change: the event repeats every turn, and a status that
52
46
  // rewrites itself constantly is noise for whatever renders it.
53
- const withoutObservedAt = payload.replace(/,"observedAt":\d+/, "");
54
- if (withoutObservedAt === lastRateLimitJson) return;
55
- lastRateLimitJson = withoutObservedAt;
47
+ const identity = rateLimitIdentity(payload);
48
+ if (identity === lastRateLimitJson) return;
49
+ lastRateLimitJson = identity;
56
50
  try {
57
- setStatus.call(uiContext!.ui, RATE_LIMIT_STATUS_KEY, payload);
51
+ setStatus.call(
52
+ uiContext!.ui,
53
+ RATE_LIMIT_STATUS_KEY,
54
+ JSON.stringify(payload),
55
+ );
58
56
  } catch {
59
57
  /* never break a turn over a status push */
60
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saccolabs/pi-claude-cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Pi coding agent extension that routes LLM calls through the Claude Code CLI",
5
5
  "main": "index.ts",
6
6
  "keywords": [
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Account rate-limit state, shaped for the host's status channel.
3
+ *
4
+ * The CLI emits a `rate_limit_event` per turn describing ONE window: the first
5
+ * one whose warning threshold has been crossed, walking
6
+ * `5h -> 7d -> 7d_oi -> overage`. So the reported window is the binding
7
+ * constraint, not an arbitrary pick — and there is no way to see all four at
8
+ * once from this stream. A front-end that wants "which limit will stop me, how
9
+ * close am I, and when does it reset" has everything it needs; one that wants
10
+ * a full dashboard does not, and should not pretend otherwise.
11
+ *
12
+ * Kept pure and separate from `index.ts` so the payload contract is testable
13
+ * without a pi runtime.
14
+ */
15
+
16
+ /** What the host receives under the `claude-rate-limit` status key. */
17
+ export interface RateLimitPayload {
18
+ status: unknown;
19
+ resetsAt: unknown;
20
+ rateLimitType: unknown;
21
+ overageStatus: unknown;
22
+ isUsingOverage: boolean;
23
+ /** Fraction of the window consumed: 1.01 means 101%, i.e. over. */
24
+ utilization: number | null;
25
+ /** Which warning step tripped, when one has. */
26
+ surpassedThreshold: number | null;
27
+ observedAt: number;
28
+ }
29
+
30
+ function num(value: unknown): number | null {
31
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
32
+ }
33
+
34
+ export function buildRateLimitPayload(
35
+ info: Record<string, unknown>,
36
+ nowSeconds: number = Math.floor(Date.now() / 1000),
37
+ ): RateLimitPayload {
38
+ return {
39
+ status: info.status,
40
+ resetsAt: info.resetsAt,
41
+ rateLimitType: info.rateLimitType,
42
+ overageStatus: info.overageStatus,
43
+ isUsingOverage: info.isUsingOverage === true,
44
+ // The CLI has always sent these two; dropping them left front-ends able to
45
+ // say WHICH limit was in play and when it resets, but never how close it
46
+ // was — so a user could not watch themselves approach a wall, only hit it.
47
+ utilization: num(info.utilization),
48
+ surpassedThreshold: num(info.surpassedThreshold),
49
+ observedAt: nowSeconds,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Identity of a payload ignoring `observedAt`, for change detection.
55
+ *
56
+ * The event repeats every turn with a fresh timestamp; pushing that verbatim
57
+ * would rewrite the host's status constantly and make anything rendering it
58
+ * flicker. Comparing everything EXCEPT the timestamp is what makes the push
59
+ * "on change" rather than "on turn".
60
+ */
61
+ export function rateLimitIdentity(payload: RateLimitPayload): string {
62
+ const { observedAt: _observedAt, ...rest } = payload;
63
+ return JSON.stringify(rest);
64
+ }