@cjhyy/code-shell-capability-coding 0.9.4 → 0.9.6

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.
@@ -6,14 +6,16 @@
6
6
  * rate_limit.{primary_window,secondary_window}.{used_percent,reset_at}.
7
7
  * Zero cost (no message sent).
8
8
  * - Claude: POST /v1/messages (max_tokens:1) → response headers
9
- * anthropic-ratelimit-unified-{5h,7d}-{utilization,reset}.
10
- * Costs ~1 output token (Claude exposes quota only via response
11
- * headers there is no standalone usage endpoint).
9
+ * anthropic-ratelimit-unified-<window>-{utilization,reset}, where
10
+ * <window> is discovered from the headers (5h / 7d / overage / …)
11
+ * rather than assumed see parseClaudeWindows (re-verified
12
+ * 2026-09-06). Costs ~1 output token (Claude exposes quota only via
13
+ * response headers — there is no standalone usage endpoint).
12
14
  *
13
15
  * The `fetch` and credentials are injected so this is unit-testable offline and
14
16
  * so the host owns secret resolution (see types.ts boundary note).
15
17
  */
16
- import type { ProviderQuota, QuotaCredentials, QuotaResult } from "./types.js";
18
+ import type { ProviderQuota, QuotaCredentials, QuotaResult, QuotaWindow } from "./types.js";
17
19
  type FetchLike = typeof fetch;
18
20
  export interface CheckQuotaOptions {
19
21
  creds: QuotaCredentials;
@@ -29,6 +31,20 @@ export interface CheckQuotaOptions {
29
31
  export declare function queryCodexQuota(creds: QuotaCredentials, fetchImpl: FetchLike, signal: AbortSignal): Promise<ProviderQuota>;
30
32
  /** Claude: POST a 1-token probe and read the unified rate-limit headers. */
31
33
  export declare function queryClaudeQuota(creds: QuotaCredentials, fetchImpl: FetchLike, signal: AbortSignal): Promise<ProviderQuota>;
34
+ /**
35
+ * Discover every rate-limit window from the unified headers.
36
+ *
37
+ * Windows are found by PREFIX, not from a hardcoded list, because which ones
38
+ * the API sends depends on the account. A normal subscription reports 5h + 7d;
39
+ * an account on overage reports `overage` and omits 5h/7d entirely. Matching a
40
+ * fixed list is what silently broke this lookup before (see types.ts).
41
+ *
42
+ * Each window contributes `<prefix><name>-utilization` (0–1) and an optional
43
+ * `<prefix><name>-reset` (epoch seconds). Bare `<prefix>reset` / `<prefix>status`
44
+ * are envelope fields, not windows, so anything without a `-utilization` suffix
45
+ * is skipped.
46
+ */
47
+ export declare function parseClaudeWindows(h: Headers): QuotaWindow[];
32
48
  /** Query both providers (or the subset requested), concurrently. */
33
49
  export declare function checkQuota(opts: CheckQuotaOptions): Promise<QuotaResult>;
34
50
  /** Render a QuotaResult as a compact human/agent-readable summary. */
@@ -101,21 +101,68 @@ export async function queryClaudeQuota(creds, fetchImpl, signal) {
101
101
  error: `HTTP ${resp.status}${resp.status === 401 ? " (token 可能已过期)" : ""}`,
102
102
  };
103
103
  }
104
- const h = resp.headers;
104
+ const windows = parseClaudeWindows(resp.headers);
105
+ if (windows.length === 0)
106
+ return { provider: "claude", error: "响应头无 rate-limit 字段" };
107
+ return { provider: "claude", windows };
108
+ }
109
+ const UNIFIED_PREFIX = "anthropic-ratelimit-unified-";
110
+ /**
111
+ * Map `representative-claim` values onto the window names used in the headers.
112
+ * The claim spells a window out ("five_hour"); the window headers abbreviate it
113
+ * ("5h"). An unlisted claim value falls through to an exact `kind` match, which
114
+ * is how "overage" already lines up.
115
+ */
116
+ const CLAIM_TO_KIND = {
117
+ five_hour: "5h",
118
+ seven_day: "7d",
119
+ seven_day_sonnet: "7d_sonnet",
120
+ };
121
+ /**
122
+ * Discover every rate-limit window from the unified headers.
123
+ *
124
+ * Windows are found by PREFIX, not from a hardcoded list, because which ones
125
+ * the API sends depends on the account. A normal subscription reports 5h + 7d;
126
+ * an account on overage reports `overage` and omits 5h/7d entirely. Matching a
127
+ * fixed list is what silently broke this lookup before (see types.ts).
128
+ *
129
+ * Each window contributes `<prefix><name>-utilization` (0–1) and an optional
130
+ * `<prefix><name>-reset` (epoch seconds). Bare `<prefix>reset` / `<prefix>status`
131
+ * are envelope fields, not windows, so anything without a `-utilization` suffix
132
+ * is skipped.
133
+ */
134
+ export function parseClaudeWindows(h) {
105
135
  const windows = [];
106
- const map = [
107
- ["anthropic-ratelimit-unified-5h-utilization", "anthropic-ratelimit-unified-5h-reset", "5h"],
108
- ["anthropic-ratelimit-unified-7d-utilization", "anthropic-ratelimit-unified-7d-reset", "7d"],
109
- ];
110
- for (const [utilKey, resetKey, kind] of map) {
111
- const util = num(h.get(utilKey)); // 0–1
136
+ for (const [rawKey, rawVal] of h.entries()) {
137
+ const key = rawKey.toLowerCase();
138
+ if (!key.startsWith(UNIFIED_PREFIX) || !key.endsWith("-utilization"))
139
+ continue;
140
+ const kind = key.slice(UNIFIED_PREFIX.length, -"-utilization".length);
141
+ if (!kind)
142
+ continue; // guard a bare `<prefix>utilization`
143
+ const util = num(rawVal); // 0–1
112
144
  if (util == null)
113
145
  continue;
114
- windows.push({ kind, usedPercent: util * 100, resetsAt: num(h.get(resetKey)) });
146
+ windows.push({
147
+ // Round to 4dp: `0.07 * 100` is 7.000000000000001 in binary float, which
148
+ // leaks into equality checks and any raw (unformatted) display.
149
+ kind,
150
+ usedPercent: Math.round(util * 100 * 1e4) / 1e4,
151
+ resetsAt: num(h.get(`${UNIFIED_PREFIX}${kind}-reset`)),
152
+ });
115
153
  }
116
- if (windows.length === 0)
117
- return { provider: "claude", error: "响应头无 rate-limit 字段" };
118
- return { provider: "claude", windows };
154
+ // Stable order so output does not shuffle between identical probes.
155
+ windows.sort((a, b) => a.kind.localeCompare(b.kind));
156
+ // Flag the binding window. A request is throttled on this one, so it is what
157
+ // an orchestrator should plan against when windows disagree.
158
+ const claim = h.get(`${UNIFIED_PREFIX}representative-claim`)?.trim().toLowerCase();
159
+ if (claim) {
160
+ const want = CLAIM_TO_KIND[claim] ?? claim;
161
+ const hit = windows.find((w) => w.kind === want);
162
+ if (hit)
163
+ hit.representative = true;
164
+ }
165
+ return windows;
119
166
  }
120
167
  /** Query both providers (or the subset requested), concurrently. */
121
168
  export async function checkQuota(opts) {
@@ -145,17 +192,28 @@ export function formatQuota(result, nowSec) {
145
192
  const plan = pq.planType ? ` [${pq.planType}]` : "";
146
193
  const parts = pq.windows.map((w) => {
147
194
  const reset = w.resetsAt != null ? ` (重置 ${formatReset(w.resetsAt - nowSec)})` : "";
148
- return `${w.kind} 用了 ${w.usedPercent.toFixed(0)}%${reset}`;
195
+ // Star the binding window so a reader/agent knows which one throttles.
196
+ const star = w.representative ? "*" : "";
197
+ return `${w.kind}${star} 用了 ${w.usedPercent.toFixed(0)}%${reset}`;
149
198
  });
150
199
  lines.push(`${name}${plan}: ${parts.join(",")}`);
151
200
  }
152
201
  return lines.length ? lines.join("\n") : "(无可用额度信息)";
153
202
  }
154
- /** "2h13m" / "45m" / "已重置" from a seconds delta. */
203
+ /**
204
+ * "3d2h" / "2h13m" / "45m" / "已重置" from a seconds delta.
205
+ *
206
+ * The day unit matters: this only ever had to render 5h/7d windows, but an
207
+ * overage window can reset weeks out, and "606h0m 后" is not a readable way to
208
+ * say 25 days.
209
+ */
155
210
  function formatReset(deltaSec) {
156
211
  if (deltaSec <= 0)
157
212
  return "已重置";
158
- const h = Math.floor(deltaSec / 3600);
213
+ const d = Math.floor(deltaSec / 86400);
214
+ const h = Math.floor((deltaSec % 86400) / 3600);
159
215
  const m = Math.floor((deltaSec % 3600) / 60);
216
+ if (d > 0)
217
+ return `${d}d${h}h 后`;
160
218
  return h > 0 ? `${h}h${m}m 后` : `${m}m 后`;
161
219
  }
@@ -10,14 +10,30 @@
10
10
  * it from the rest of core. Nothing outside this module should know about
11
11
  * Keychain / wham endpoints / `anthropic-ratelimit-*` header names.
12
12
  */
13
- /** A single rolling limit window (e.g. the 5-hour or 7-day window). */
13
+ /**
14
+ * A single rolling limit window.
15
+ *
16
+ * `kind` is NOT a closed set. Claude reports whichever windows apply to the
17
+ * account: usually "5h" and "7d", but an account running on overage reports an
18
+ * "overage" window INSTEAD of those (verified 2026-09-06 against a team /
19
+ * default_claude_max_5x account). New window names appear without notice, so
20
+ * the parser discovers them from the header prefix rather than matching a
21
+ * hardcoded list. Renderers must treat `kind` as an opaque label.
22
+ */
14
23
  export interface QuotaWindow {
15
- /** Which window this is. */
16
- kind: "5h" | "7d";
24
+ /** Which window this is: "5h" | "7d" | "overage" | any future name. */
25
+ kind: string;
17
26
  /** Percent of the window's limit already used, 0–100. */
18
27
  usedPercent: number;
19
28
  /** Unix epoch seconds when this window resets, or null if unknown. */
20
29
  resetsAt: number | null;
30
+ /**
31
+ * True for the window the API named as the binding constraint via
32
+ * `anthropic-ratelimit-unified-representative-claim`. Claude only; a request
33
+ * is throttled on THIS window, so it is the one to act on when several
34
+ * windows disagree.
35
+ */
36
+ representative?: boolean;
21
37
  }
22
38
  /** Quota for one provider (claude | codex). */
23
39
  export interface ProviderQuota {
@@ -3,9 +3,11 @@ import { resolveQuotaCredentials } from "../quota/credentials.js";
3
3
  export const checkQuotaToolDef = {
4
4
  name: "CheckQuota",
5
5
  description: "Check remaining usage/rate-limit quota for the external coding-agent CLIs (Claude Code and/or " +
6
- "Codex) — the same 5h/7d subscription windows their status lines show. Use before or during " +
6
+ "Codex) — the same subscription windows their status lines show. Use before or during " +
7
7
  "orchestration (DriveAgent) to plan how much work to hand off, whether to wait for a reset, or " +
8
- "which provider to use. Returns each provider's 5h/7d used-% and reset time. " +
8
+ "which provider to use. Returns each window's used-% and reset time. Codex reports 5h/7d; " +
9
+ "Claude reports whichever windows apply to the account (5h/7d normally, 'overage' when the " +
10
+ "account is running on overage), and marks the currently binding window with '*'. " +
9
11
  "COST: 'codex' is free (reads a usage endpoint). 'claude' costs ~1 token (Anthropic exposes " +
10
12
  "quota only via a response header, so this sends a 1-token probe). Pass `provider` to query " +
11
13
  "just one and avoid the other's cost/latency.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-capability-coding",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Coding capability pack for the generic code-shell agent core.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
40
40
  },
41
41
  "dependencies": {
42
- "@cjhyy/code-shell-core": "0.9.4"
42
+ "@cjhyy/code-shell-core": "0.9.6"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=20.10"