SLOPSHOPPER

simpleapps-statusline

Two status lines under the prompt: GitHub user, project, branch, open issues and PRs, Anthropic account and Claude Code version; then the model, session cost…

newspinnercommandprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · simpleapps-statusline
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /plan-usage ⎿ simpleapps-statusline: ?:app · v2.1.289 ⎿ simpleapps-statusline: Opus 5.5 ⎿ simpleapps-statusline: Session $0.42 · ctx 49% ⎿ simpleapps-statusline: 5-hour 31% resets 09:53 ⎿ simpleapps-statusline: raw: {"usage":{"startedAt":1759998200000,"context":{"tokens":97400,"window":200000,"percent":49},"rateLimits": ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ⟨Claude Code's own drawing⟩ ?:app v2.1.289 Opus 5.5 session $0.42 ctx ━━━━━━━━━━ 49% 5h ━━━━━━━━━━ 31% ↻ 09:53

Draws

Prompt hint
⟨Claude Code's own drawing⟩ ?:app v2.1.289 Opus 5.5 session $0.42 ctx ━━━━━━━━━━ 49% 5h ━━━━━━━━━━ 31% ↻ 09:53
README

simpleapps-statusline

Two status lines just under the prompt, below Claude Code's own hint line:

octo:my-project (main)   7 issues · 2 PRs   me@example.com (max)   v2.1.296
Opus 5.5   session $1.24   ctx ━━━━━━━━━━ 31%   5h ━━━━━━━━━━ 21% ↻ 15:00   week ━━━━━━━━━━ 44% ↻ Tue

Each meter is ten cells: the used share in colour, the rest dimmed.

  • Line 1: GitHub login (gh api user, cached 2 hours), project folder, git branch (from repo/ when the project uses the {project}/repo/ layout), open issues and PRs in that GitHub repo (cached 5 minutes per repo, hidden off GitHub), the Anthropic account and plan from claude auth status (cached 10 minutes), and the Claude Code version. gh and claude calls give up after 5 seconds, so a dead network cannot hold up session start.
  • Line 2: the model, this session's cost, then meters for context used and each plan limit Claude Code reports (5-hour, weekly, others by name), with % used and when it resets. Green, yellow at 75%, red at 90%.

It refreshes after every turn. Each part moves to the next line as a whole when the terminal is narrow: two lines when wide, three or four at 80 to 120 columns. Plan limits show for plans that report them (Pro and Max), and context shows ? until the session's first response. Per-model and monthly extra-usage figures are in no supported interface; /usage shows them (anthropics/claude-code#92080).

This plugin is a mod: TypeScript that runs inside Claude Code. It is separate from simpleapps, so it can be turned off on its own in /plugin.

Install

Requires Claude Code 2.1.287 or later.

/plugin install simpleapps-statusline@augur-skills
/reload-plugins

Then delete statusLine from ~/.claude/settings.json, so the replaced statusline-full / statusline-basic script does not draw a second status line.

In a folder you have not trusted yet, no mod loads until you answer the trust prompt.

Check it

/plan-usage prints the same lines plus the raw reading, so it works without seeing the terminal:

claude -p "/plan-usage"

Develop

From the root of this repo:

claude --plugin-dir plugins/simpleapps-statusline   # reloads on save; generates API types (gitignored)
claude plugin validate --strict plugins/simpleapps-statusline
claude plugin test plugins/simpleapps-statusline
pnpm exec tsc -p plugins/simpleapps-statusline/tsconfig.json --noEmit   # after --plugin-dir has generated the types

All three MUST pass before pushing; CI does not run them (the type check needs the API types only --plugin-dir generates). Tests stub every process, store, and session call, so they need no network. Loader rules: write each $ call in full and pass $ only to top-level functions; give a command.run hook .catch(...); keep $.state in a const atom declared in types/index.d.ts. Full API: the mods docs.

Source 2 files
hooks/register.tsx 470 lines
1// Two status lines under the prompt: where you are (GitHub user, project, branch, open issues and
2// PRs, Anthropic account, version), then this session's cost and context and every plan rate limit
3// Claude Code reports (5-hour, weekly) for the signed-in account.
4// Source: $.session.usage(), refreshed on session.measure (after each turn, and when a limit moves).
5import { atom, read, update } from "claude-code";
6import type { EngineInterface, Register } from "claude-code";
7
8import type { Limit, Reading, UsageSnapshot } from "../types";
9
10const COMMAND = "plan-usage";
11
12const snapshot = atom(
13  { plugin: "simpleapps-statusline", key: "snapshot" } as const,
14  {} as UsageSnapshot,
15);
16
17const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
18
19/** The session's cost in dollars, whether reported as a number or inside an object. */
20export function costUsd(cost: unknown): number | undefined {
21  if (num(cost) !== undefined) return num(cost);
22  if (cost === null || typeof cost !== "object") return undefined;
23  const c = cost as Record<string, unknown>;
24  return num(c.totalUsd) ?? num(c.usd) ?? num(c.total_cost_usd) ?? num(c.totalCostUsd);
25}
26
27/** `$.session.usage()` reduced to what the pane shows; unknown shapes become undefined, never a throw. */
28export function toReading(raw: unknown, at: number): Reading {
29  const u = (raw ?? {}) as Record<string, unknown>;
30  const context = (u.context ?? {}) as Record<string, unknown>;
31  const limits = Array.isArray(u.rateLimits) ? (u.rateLimits as Record<string, unknown>[]) : [];
32  return {
33    at,
34    cost: costUsd(u.cost),
35    contextPercent: num(context.percent),
36    limits: limits.map((l): Limit => ({
37      kind: String(l.kind ?? "limit"),
38      percent: num(l.percentUsed),
39      resetsAt: l.resetsAt === undefined || l.resetsAt === null ? undefined : String(l.resetsAt),
40    })),
41  };
42}
43
44const NAMES: Record<string, string> = {
45  five_hour: "5-hour",
46  fiveHour: "5-hour",
47  session: "5-hour",
48  seven_day: "Weekly",
49  sevenDay: "Weekly",
50  weekly: "Weekly",
51  monthly: "Monthly",
52  spend_limit: "Spend limit",
53};
54
55/** A limit's label: known kinds by name, others (per-model, extra usage) from their own kind. */
56export function limitName(kind: string): string {
57  if (NAMES[kind]) return NAMES[kind];
58  const words = kind.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ");
59  return words.charAt(0).toUpperCase() + words.slice(1);
60}
61
62const pad = (n: number) => String(n).padStart(2, "0");
63
64/** When a limit resets: the time today, else the weekday, else the date. */
65export function resetLabel(resetsAt: string | undefined, now: number): string {
66  if (resetsAt === undefined) return "";
67  const asNumber = Number(resetsAt);
68  // Seconds or milliseconds since the epoch, or an ISO date
69  const ms = Number.isNaN(asNumber)
70    ? Date.parse(resetsAt)
71    : asNumber < 1e12
72      ? asNumber * 1000
73      : asNumber;
74  if (Number.isNaN(ms)) return "";
75  const d = new Date(ms);
76  const days = (ms - now) / 86_400_000;
77  if (days < 1 && d.getDate() === new Date(now).getDate())
78    return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
79  if (days < 7) return d.toLocaleDateString("en-US", { weekday: "short" });
80  return `${d.getMonth() + 1}/${d.getDate()}`;
81}
82
83/** Red at 90% and over, yellow at 75%. */
84export function percentColor(percent: number | undefined) {
85  if (percent === undefined) return undefined;
86  if (percent >= 90) return "red";
87  return percent >= 75 ? "yellow" : undefined;
88}
89
90const pct = (p: number | undefined) => (p === undefined ? "?" : `${Math.round(p)}%`);
91
92type Row = [string, string | undefined];
93
94/** The bar's parts (and `/plan-usage`'s lines) as [text, color]. */
95export function rows(r: Reading | undefined, now: number): Row[] {
96  if (r === undefined) return [["No reading yet", undefined]];
97  const cost = r.cost === undefined ? "" : `$${r.cost.toFixed(2)} · `;
98  const session: Row = [`Session ${cost}ctx ${pct(r.contextPercent)}`, undefined];
99  const limits = r.limits.map((l): Row => {
100    const reset = resetLabel(l.resetsAt, now);
101    const text = `${limitName(l.kind)} ${pct(l.percent)}${reset ? ` resets ${reset}` : ""}`;
102    return [text, percentColor(l.percent)];
103  });
104  const none: Row = ["No plan limits reported", undefined];
105  return [session, ...(limits.length ? limits : [none])];
106}
107
108const SHORT: Record<string, string> = { five_hour: "5h", seven_day: "week" };
109
110/** One labelled figure in the bar; `percent` draws a meter. */
111export type Part = { label: string; value: string; percent?: number; note?: string };
112
113/** The bar's figures: session cost, context, then each limit with its reset. */
114export function parts(r: Reading, now: number): Part[] {
115  const cost: Part[] =
116    r.cost === undefined ? [] : [{ label: "session", value: `$${r.cost.toFixed(2)}` }];
117  const context: Part = { label: "ctx", value: pct(r.contextPercent), percent: r.contextPercent };
118  const limits = r.limits.map((l): Part => {
119    const reset = resetLabel(l.resetsAt, now);
120    const label = SHORT[l.kind] ?? limitName(l.kind).toLowerCase();
121    return {
122      label,
123      value: pct(l.percent),
124      percent: l.percent,
125      note: reset ? `↻ ${reset}` : undefined,
126    };
127  });
128  return [...cost, context, ...limits];
129}
130
131/** A model id as people say it: `claude-opus-5-5[1m]` → `Opus 5.5 (1M)`. */
132function prettyModel(id: string): string {
133  const [, base = id, window] = /^(.*?)(?:\[(\w+)\])?$/.exec(id) ?? [];
134  const words = base.replace(/^claude-/, "").split("-");
135  const name = words
136    .filter((w) => !/^\d+$/.test(w))
137    .map((w) => w.charAt(0).toUpperCase() + w.slice(1));
138  const version = words.filter((w) => /^\d+$/.test(w)).join(".");
139  const label = [...name, version].filter(Boolean).join(" ");
140  return window ? `${label} (${window.toUpperCase()})` : label;
141}
142
143/** `$.session.model()` as a short name, whether a string or an object. */
144export function modelLabel(raw: unknown): string | undefined {
145  if (typeof raw === "string") return raw ? prettyModel(raw) : undefined;
146  if (raw === null || typeof raw !== "object") return undefined;
147  const m = raw as Record<string, unknown>;
148  const display = m.displayName ?? m.display_name;
149  if (typeof display === "string" && display) return display;
150  const id = m.id ?? m.model ?? m.name;
151  return typeof id === "string" && id ? prettyModel(id) : undefined;
152}
153
154/** `$.session.version()` as `v2.1.296`, whether a string or an object. */
155export function versionLabel(raw: unknown): string | undefined {
156  const v =
157    raw !== null && typeof raw === "object" ? (raw as Record<string, unknown>).version : raw;
158  if (typeof v !== "string" || !v) return undefined;
159  return v.startsWith("v") ? v : `v${v}`;
160}
161
162const WIDTH = 10;
163
164/** A meter's filled and empty cell counts; 0–100%, a sliver shows for any use. */
165export function meter(percent: number): [number, number] {
166  const filled =
167    percent <= 0 ? 0 : Math.max(1, Math.min(WIDTH, Math.round((percent / 100) * WIDTH)));
168  return [filled, WIDTH - filled];
169}
170
171/** Meter colour: green, yellow at 75%, red at 90%. */
172export const meterColor = (percent: number) => percentColor(percent) ?? "green";
173
174/** `/plan-usage`: the lines, then the raw reading, so new kinds show up as they are. */
175export function checkText(r: Reading, raw: unknown, now: number): string {
176  const lines = [whereLabel(r), r.model, ...rows(r, now).map(([t]) => t)].filter(Boolean);
177  return [...lines, `raw: ${JSON.stringify(raw)}`].join("\n");
178}
179
180/** The first line of a command's output; undefined when it failed or printed nothing. */
181export function firstLine(out: unknown): string | undefined {
182  const o = out as { exitCode?: number; stdout?: string } | undefined;
183  if (o === undefined || o.exitCode !== 0) return undefined;
184  return o.stdout?.split("\n")[0]?.trim() || undefined;
185}
186
187/** `$.session.root()` as a path, whether a string or an object. */
188export function rootPath(raw: unknown): string | undefined {
189  const p = raw !== null && typeof raw === "object" ? (raw as Record<string, unknown>).path : raw;
190  return typeof p === "string" && p ? p.replace(/\/+$/, "") : undefined;
191}
192
193/** `claude auth status --json` as `email (plan)`; undefined when signed out or unreadable. */
194export function accountLabel(out: unknown): string | undefined {
195  const o = out as { exitCode?: number; stdout?: string } | undefined;
196  if (o === undefined || o.exitCode !== 0) return undefined;
197  try {
198    const s = JSON.parse(o.stdout ?? "") as Record<string, unknown>;
199    if (s.loggedIn !== true) return "signed out";
200    const who = typeof s.email === "string" && s.email ? s.email : String(s.authMethod ?? "");
201    return typeof s.subscriptionType === "string" && s.subscriptionType
202      ? `${who} (${s.subscriptionType})`
203      : who || undefined;
204  } catch {
205    return undefined;
206  }
207}
208
209/** A GitHub remote URL (ssh or https) as `owner/name`; undefined for any other host. */
210export function ghRepo(url: string | undefined): string | undefined {
211  const m = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url ?? "");
212  return m ? `${m[1]}/${m[2]}` : undefined;
213}
214
215/** The open issue and PR counts from the GraphQL answer; undefined when it failed. */
216export function countsOf(out: unknown): { issues: number; prs: number } | undefined {
217  const o = out as { exitCode?: number; stdout?: string } | undefined;
218  if (o === undefined || o.exitCode !== 0) return undefined;
219  try {
220    const repo = (JSON.parse(o.stdout ?? "") as { data?: { repository?: Record<string, unknown> } })
221      .data?.repository;
222    const issues = num((repo?.issues as Record<string, unknown> | undefined)?.totalCount);
223    const prs = num((repo?.pullRequests as Record<string, unknown> | undefined)?.totalCount);
224    return issues === undefined || prs === undefined ? undefined : { issues, prs };
225  } catch {
226    return undefined;
227  }
228}
229
230const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
231
232/** `7 issues · 2 PRs`; undefined when the counts are unknown. */
233export function countsLabel(r: Reading): string | undefined {
234  if (r.issues === undefined || r.prs === undefined) return undefined;
235  return `${plural(r.issues, "issue")} · ${plural(r.prs, "PR")}`;
236}
237
238/** `user:project (branch) · N issues · N PRs · account · version`, the first line. */
239export function whereLabel(r: Reading): string {
240  const where = `${r.user ?? "?"}:${r.project ?? "?"}${r.branch ? ` (${r.branch})` : ""}`;
241  return [where, countsLabel(r), r.account, r.version].filter(Boolean).join(" · ");
242}
243
244// GitHub login is cached this long, as the old status line did
245const USER_TTL_MS = 2 * 60 * 60 * 1000;
246const ACCOUNT_TTL_MS = 10 * 60 * 1000;
247const COUNTS_TTL_MS = 5 * 60 * 1000;
248// A repo's cached counts are dropped once unused this long
249const COUNTS_KEEP_MS = 24 * 60 * 60 * 1000;
250// Network calls (gh, claude) give up after this, so a dead network cannot hold up session start
251// for $.process.run's 30 s default
252const NETWORK = { timeoutMs: 5000 } as const;
253
254const COUNTS_QUERY =
255  "query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { issues(states: OPEN) { totalCount } pullRequests(states: OPEN) { totalCount } } }";
256
257/** The GitHub login, cached for two hours; the old login when gh fails. */
258async function ghLogin($: EngineInterface, now: number): Promise<string | undefined> {
259  const cached = (await $.store.get("ghUser").catch(() => undefined)) as
260    { login: string; at: number } | undefined;
261  if (cached && now - cached.at < USER_TTL_MS) return cached.login;
262  const login = firstLine(
263    await $.process.run(["gh", "api", "user", "--jq", ".login"], NETWORK).catch(() => undefined),
264  );
265  if (!login) return cached?.login;
266  await $.store.set("ghUser", { login, at: now }).catch(() => undefined);
267  return login;
268}
269
270/** The Anthropic account Claude Code is signed in as; re-read soon so a /login shows. */
271async function claudeAccount($: EngineInterface, now: number): Promise<string | undefined> {
272  const cached = (await $.store.get("claudeAccount").catch(() => undefined)) as
273    { label: string; at: number } | undefined;
274  if (cached && now - cached.at < ACCOUNT_TTL_MS) return cached.label;
275  const label = accountLabel(
276    await $.process.run(["claude", "auth", "status", "--json"], NETWORK).catch(() => undefined),
277  );
278  if (!label) return cached?.label;
279  await $.store.set("claudeAccount", { label, at: now }).catch(() => undefined);
280  return label;
281}
282
283type Counts = { repo: string; issues: number; prs: number; at: number };
284
285// One store key per repo: $.store is shared by every session, so sessions in different repos
286// must not overwrite each other's counts
287const COUNTS_PREFIX = "ghCounts:";
288
289/** Drop counts for repos unused for a day, and the old single `ghCounts` key. */
290async function pruneCounts($: EngineInterface, now: number) {
291  const keys = (await $.store.keys().catch(() => [])) as string[];
292  for (const key of keys) {
293    if (key !== "ghCounts" && !key.startsWith(COUNTS_PREFIX)) continue;
294    const entry = (await $.store.get(key).catch(() => undefined)) as Counts | undefined;
295    if (key === "ghCounts" || !entry || now - entry.at > COUNTS_KEEP_MS)
296      await $.store.delete(key).catch(() => undefined);
297  }
298}
299
300/** Open issues and PRs in the folder's GitHub repo, cached five minutes per repo; none off GitHub. */
301async function repoCounts($: EngineInterface, gitDir: string | undefined, now: number) {
302  if (gitDir === undefined) return undefined;
303  const repo = ghRepo(
304    firstLine(
305      await $.process
306        .run(["git", "-C", gitDir, "remote", "get-url", "origin"])
307        .catch(() => undefined),
308    ),
309  );
310  if (repo === undefined) return undefined;
311  const key = `${COUNTS_PREFIX}${repo}`;
312  const same = (await $.store.get(key).catch(() => undefined)) as Counts | undefined;
313  if (same && now - same.at < COUNTS_TTL_MS) return same;
314  const [owner, name] = repo.split("/");
315  const counts = countsOf(
316    await $.process
317      .run(
318        [
319          "gh",
320          "api",
321          "graphql",
322          "-f",
323          `query=${COUNTS_QUERY}`,
324          "-f",
325          `owner=${owner}`,
326          "-f",
327          `name=${name}`,
328        ],
329        NETWORK,
330      )
331      .catch(() => undefined),
332  );
333  if (counts === undefined) return same;
334  const fresh: Counts = { repo, ...counts, at: now };
335  await $.store.set(key, fresh).catch(() => undefined);
336  await pruneCounts($, now);
337  return fresh;
338}
339
340/** Everything the lines show, read fresh, plus the raw values for `/plan-usage`. */
341async function measure($: EngineInterface) {
342  const now = await $.clock.now();
343  const [usage, model, version, rootRaw] = await Promise.all([
344    $.session.usage().catch(() => undefined),
345    Promise.resolve($.session.model()).catch(() => undefined),
346    Promise.resolve($.session.version()).catch(() => undefined),
347    Promise.resolve($.session.root()).catch(() => undefined),
348  ]);
349  const root = rootPath(rootRaw);
350  // The project layout keeps the git repo in repo/; fall back to the folder itself
351  const nested = root ? await $.fs.exists(`${root}/repo/.git`).catch(() => false) : false;
352  const gitDir = nested ? `${root}/repo` : root;
353  const branch = gitDir
354    ? firstLine(
355        await $.process
356          .run(["git", "-C", gitDir, "branch", "--show-current"])
357          .catch(() => undefined),
358      )
359    : undefined;
360  const [user, account, counts] = await Promise.all([
361    ghLogin($, now),
362    claudeAccount($, now),
363    repoCounts($, gitDir, now),
364  ]);
365  const reading: Reading = {
366    issues: counts?.issues,
367    prs: counts?.prs,
368    account,
369    ...toReading(usage, now),
370    model: modelLabel(model),
371    version: versionLabel(version),
372    user,
373    project: root?.split("/").pop(),
374    branch,
375  };
376  await update($, snapshot, () => ({ reading }));
377  return { reading, raw: { usage, model, version, root: rootRaw }, now };
378}
379
380export const register: Register = (on) => {
381  on("session.start", async ($, e, next) => {
382    await $.command.register({
383      name: COMMAND,
384      description: "Print this session's usage and the plan limits, with the raw reading.",
385    });
386    await measure($);
387    return next(e);
388  });
389
390  // After each turn: the model (/model) or the branch may have changed
391  on("session.measure", async ($, e, next) => {
392    await measure($);
393    return next(e);
394  });
395
396  on("command.run", { command: COMMAND }, async ($) => {
397    const { reading, raw, now } = await measure($);
398    return { text: checkText(reading, raw, now) };
399  }).catch(async (_$, _e, next) => ({
400    text: `plan-usage failed: ${next.error?.message ?? "unknown"}`,
401  }));
402
403  // Two lines under the prompt, below Claude Code's own hint line. Each part is a whole unit
404  // that wraps to the next line when the terminal is too narrow, never split mid-word: two
405  // lines when wide, three or four when narrow.
406  on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
407    const s = await read($, snapshot);
408    if (s.reading === undefined) return next(e);
409    const { Box, Text } = $.ui.resolve(e);
410    const theirs = await next(e);
411    const now = await $.clock.now();
412    const counts = countsLabel(s.reading);
413    return (
414      <Box flexDirection="column">
415        {theirs}
416        <Box flexDirection="row" flexWrap="wrap" columnGap={3}>
417          {/* The location may shrink and wrap inside itself only when it alone is too wide */}
418          <Text>
419            <Text color="green">{s.reading.user ?? "?"}</Text>
420            <Text dimColor>:</Text>
421            <Text color="blue">{s.reading.project ?? "?"}</Text>
422            {s.reading.branch && <Text dimColor>{` (${s.reading.branch})`}</Text>}
423          </Text>
424          {counts && (
425            <Box flexShrink={0}>
426              <Text bold>{counts}</Text>
427            </Box>
428          )}
429          {s.reading.account && (
430            <Box flexShrink={0}>
431              <Text color="magenta">{s.reading.account}</Text>
432            </Box>
433          )}
434          {s.reading.version && (
435            <Box flexShrink={0}>
436              <Text color="cyan">{s.reading.version}</Text>
437            </Box>
438          )}
439        </Box>
440        <Box flexDirection="row" flexWrap="wrap" columnGap={3}>
441          {s.reading.model && (
442            <Box flexShrink={0}>
443              <Text color="yellow">{s.reading.model}</Text>
444            </Box>
445          )}
446          {parts(s.reading, now).map((p) => {
447            const [filled, empty] = p.percent === undefined ? [0, 0] : meter(p.percent);
448            const color = p.percent === undefined ? undefined : meterColor(p.percent);
449            return (
450              <Box flexDirection="row" flexShrink={0} columnGap={1}>
451                <Text dimColor>{p.label}</Text>
452                {p.percent !== undefined && (
453                  <Text>
454                    <Text color={color}>{"━".repeat(filled)}</Text>
455                    <Text dimColor>{"━".repeat(empty)}</Text>
456                  </Text>
457                )}
458                <Text bold color={color}>
459                  {p.value}
460                </Text>
461                {p.note && <Text dimColor>{p.note}</Text>}
462              </Box>
463            );
464          })}
465        </Box>
466      </Box>
467    );
468  });
469};
470
types/index.d.ts 31 lines
1/** One plan rate limit from $.session.usage(); `kind` is Claude Code's own name for it. */
2export type Limit = { kind: string; percent?: number; resetsAt?: string };
3
4/** What the pane shows, taken from one $.session.usage() call. */
5export type Reading = {
6  at: number;
7  cost?: number;
8  contextPercent?: number;
9  limits: Limit[];
10  // Current model (starts the meters line) and Claude Code version (ends the first line)
11  model?: string;
12  version?: string;
13  // GitHub login, project folder and git branch, for the line above the meters
14  user?: string;
15  project?: string;
16  branch?: string;
17  // Open issues and PRs in that GitHub repo
18  issues?: number;
19  prs?: number;
20  // Anthropic account Claude Code is signed in as: `email (plan)`
21  account?: string;
22};
23
24export type UsageSnapshot = { reading?: Reading };
25
26declare module "claude-code" {
27  interface PluginState {
28    "simpleapps-statusline": { snapshot: UsageSnapshot };
29  }
30}
31