agent-limit 0.4.0 → 0.5.0

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
@@ -1,6 +1,6 @@
1
1
  # agent-limit
2
2
 
3
- Terminal dashboard to monitor Claude Code, Codex, and Gemini CLI usage limits.
3
+ Terminal dashboard to monitor Claude Code and Codex usage limits.
4
4
 
5
5
  ## Install
6
6
 
@@ -46,7 +46,7 @@ agent-limit usage
46
46
 
47
47
  ## Features
48
48
 
49
- - Real-time usage tracking for Claude Code, Codex, and Gemini CLI
49
+ - Real-time usage tracking for Claude Code and Codex
50
50
  - Trajectory markers showing if you're ahead or behind your usage pace
51
51
  - Auto-refresh every 60 seconds
52
52
  - Color-coded usage indicators
@@ -57,7 +57,6 @@ agent-limit usage
57
57
  |----------|--------|-------------|
58
58
  | Claude Code | Full support | macOS Keychain + Anthropic API |
59
59
  | Codex | Full support | `~/.codex/auth.json` + OpenAI API |
60
- | Gemini CLI | Static limits | `~/.gemini/settings.json` |
61
60
 
62
61
  ## Development
63
62
 
@@ -102,7 +101,6 @@ agent-limit reads credentials from standard locations:
102
101
 
103
102
  - **Claude Code**: macOS Keychain (`Claude Code-credentials`)
104
103
  - **Codex**: `~/.codex/auth.json`
105
- - **Gemini**: `~/.gemini/settings.json`
106
104
 
107
105
  It then fetches usage data from each provider's API and displays it in a unified dashboard.
108
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-limit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Terminal dashboard to monitor Claude Code, Codex, and other agent usage limits",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -24,7 +24,6 @@
24
24
  "dashboard",
25
25
  "claude",
26
26
  "codex",
27
- "gemini",
28
27
  "ai",
29
28
  "agent",
30
29
  "usage",
package/src/App.tsx CHANGED
@@ -13,7 +13,6 @@ export function App({ onExit }: AppProps) {
13
13
  const [providers, setProviders] = useState<ProviderStatus[]>([
14
14
  { provider: "claude", status: "loading", metrics: [] },
15
15
  { provider: "codex", status: "loading", metrics: [] },
16
- { provider: "gemini", status: "loading", metrics: [] },
17
16
  ]);
18
17
  const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
19
18
  const [isLoading, setIsLoading] = useState(true);
@@ -10,7 +10,6 @@ function getProviderDisplayName(provider: string): string {
10
10
  switch (provider) {
11
11
  case "claude": return "CLAUDE CODE";
12
12
  case "codex": return "CODEX";
13
- case "gemini": return "GEMINI CLI";
14
13
  default: return provider.toUpperCase();
15
14
  }
16
15
  }
@@ -1,15 +1,47 @@
1
- import { getClaudeCredentials } from "../utils/keychain";
1
+ import {
2
+ getClaudeCredentials,
3
+ refreshClaudeToken,
4
+ saveClaudeCredentials,
5
+ type ClaudeCredentials,
6
+ } from "../utils/keychain";
2
7
  import { timeUntil } from "../utils/time";
3
8
  import type { ProviderStatus } from "./types";
4
9
 
10
+ const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
11
+
5
12
  interface ClaudeUsageResponse {
6
13
  five_hour: { utilization: number; resets_at: string | null } | null;
7
14
  seven_day: { utilization: number; resets_at: string | null } | null;
8
15
  seven_day_opus: { utilization: number; resets_at: string | null } | null;
9
16
  }
10
17
 
18
+ async function tryRefreshCredentials(
19
+ credentials: ClaudeCredentials
20
+ ): Promise<ClaudeCredentials | null> {
21
+ const refreshed = await refreshClaudeToken(credentials.refreshToken);
22
+ if (refreshed) {
23
+ await saveClaudeCredentials(refreshed);
24
+ }
25
+ return refreshed;
26
+ }
27
+
28
+ async function fetchUsageWithCredentials(
29
+ credentials: ClaudeCredentials
30
+ ): Promise<Response> {
31
+ return fetch("https://api.anthropic.com/api/oauth/usage", {
32
+ method: "GET",
33
+ headers: {
34
+ Accept: "application/json",
35
+ "Content-Type": "application/json",
36
+ "User-Agent": "monitor/1.0.0",
37
+ Authorization: `Bearer ${credentials.accessToken}`,
38
+ "anthropic-beta": "oauth-2025-04-20",
39
+ },
40
+ });
41
+ }
42
+
11
43
  export async function fetchClaudeUsage(): Promise<ProviderStatus> {
12
- const credentials = await getClaudeCredentials();
44
+ let credentials = await getClaudeCredentials();
13
45
 
14
46
  if (!credentials) {
15
47
  return {
@@ -20,17 +52,24 @@ export async function fetchClaudeUsage(): Promise<ProviderStatus> {
20
52
  };
21
53
  }
22
54
 
55
+ const tokenExpiresSoon = credentials.expiresAt < Date.now() + TOKEN_REFRESH_BUFFER_MS;
56
+ if (tokenExpiresSoon) {
57
+ const refreshed = await tryRefreshCredentials(credentials);
58
+ if (refreshed) {
59
+ credentials = refreshed;
60
+ }
61
+ }
62
+
23
63
  try {
24
- const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
25
- method: "GET",
26
- headers: {
27
- Accept: "application/json",
28
- "Content-Type": "application/json",
29
- "User-Agent": "monitor/1.0.0",
30
- Authorization: `Bearer ${credentials.accessToken}`,
31
- "anthropic-beta": "oauth-2025-04-20",
32
- },
33
- });
64
+ let response = await fetchUsageWithCredentials(credentials);
65
+
66
+ if (response.status === 401) {
67
+ const refreshed = await tryRefreshCredentials(credentials);
68
+ if (refreshed) {
69
+ credentials = refreshed;
70
+ response = await fetchUsageWithCredentials(credentials);
71
+ }
72
+ }
34
73
 
35
74
  if (!response.ok) {
36
75
  if (response.status === 401) {
@@ -1,19 +1,16 @@
1
1
  export * from "./types";
2
2
  export { fetchClaudeUsage } from "./claude";
3
3
  export { fetchCodexUsage } from "./codex";
4
- export { fetchGeminiUsage } from "./gemini";
5
4
 
6
5
  import { fetchClaudeUsage } from "./claude";
7
6
  import { fetchCodexUsage } from "./codex";
8
- import { fetchGeminiUsage } from "./gemini";
9
7
  import type { ProviderStatus } from "./types";
10
8
 
11
9
  export async function fetchAllProviders(): Promise<ProviderStatus[]> {
12
- const [claude, codex, gemini] = await Promise.all([
10
+ const [claude, codex] = await Promise.all([
13
11
  fetchClaudeUsage(),
14
12
  fetchCodexUsage(),
15
- fetchGeminiUsage(),
16
13
  ]);
17
14
 
18
- return [claude, codex, gemini];
15
+ return [claude, codex];
19
16
  }
@@ -1,4 +1,4 @@
1
- export type ProviderName = "claude" | "codex" | "gemini";
1
+ export type ProviderName = "claude" | "codex";
2
2
 
3
3
  export type ProviderStatusType = "ok" | "warning" | "error" | "unavailable" | "loading" | "limited";
4
4
 
@@ -18,7 +18,6 @@ export function getStatusColor(status: string): string {
18
18
  export const PROVIDER_COLORS = {
19
19
  claude: "#d97706",
20
20
  codex: "#10b981",
21
- gemini: "#3b82f6",
22
21
  } as const;
23
22
 
24
23
  export const UI_COLORS = {
@@ -1,5 +1,9 @@
1
1
  import { $ } from "bun";
2
2
 
3
+ const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
4
+ const CLAUDE_TOKEN_ENDPOINT = "https://console.anthropic.com/v1/oauth/token";
5
+ const KEYCHAIN_SERVICE = "Claude Code-credentials";
6
+
3
7
  export async function getKeychainCredentials(service: string): Promise<string | null> {
4
8
  try {
5
9
  const result = await $`security find-generic-password -s ${service} -w`
@@ -11,6 +15,16 @@ export async function getKeychainCredentials(service: string): Promise<string |
11
15
  }
12
16
  }
13
17
 
18
+ async function setKeychainCredentials(service: string, data: string): Promise<boolean> {
19
+ try {
20
+ await $`security delete-generic-password -s ${service}`.quiet().nothrow();
21
+ await $`security add-generic-password -s ${service} -w ${data}`.quiet();
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
14
28
  export interface ClaudeCredentials {
15
29
  accessToken: string;
16
30
  refreshToken: string;
@@ -21,7 +35,7 @@ export interface ClaudeCredentials {
21
35
 
22
36
  export async function getClaudeCredentials(): Promise<ClaudeCredentials | null> {
23
37
  try {
24
- const raw = await getKeychainCredentials("Claude Code-credentials");
38
+ const raw = await getKeychainCredentials(KEYCHAIN_SERVICE);
25
39
  if (!raw) return null;
26
40
 
27
41
  const parsed = JSON.parse(raw);
@@ -31,6 +45,62 @@ export async function getClaudeCredentials(): Promise<ClaudeCredentials | null>
31
45
  }
32
46
  }
33
47
 
48
+ interface TokenRefreshResponse {
49
+ token_type: string;
50
+ access_token: string;
51
+ expires_in: number;
52
+ refresh_token: string;
53
+ scope: string;
54
+ }
55
+
56
+ export async function refreshClaudeToken(
57
+ refreshToken: string
58
+ ): Promise<ClaudeCredentials | null> {
59
+ try {
60
+ const response = await fetch(CLAUDE_TOKEN_ENDPOINT, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({
64
+ grant_type: "refresh_token",
65
+ refresh_token: refreshToken,
66
+ client_id: CLAUDE_CLIENT_ID,
67
+ }),
68
+ });
69
+
70
+ if (!response.ok) {
71
+ return null;
72
+ }
73
+
74
+ const data: TokenRefreshResponse = await response.json();
75
+
76
+ return {
77
+ accessToken: data.access_token,
78
+ refreshToken: data.refresh_token,
79
+ expiresAt: Date.now() + data.expires_in * 1000,
80
+ scopes: data.scope.split(" "),
81
+ subscriptionType: "Pro",
82
+ };
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export async function saveClaudeCredentials(
89
+ credentials: ClaudeCredentials
90
+ ): Promise<boolean> {
91
+ try {
92
+ const raw = await getKeychainCredentials(KEYCHAIN_SERVICE);
93
+ if (!raw) return false;
94
+
95
+ const parsed = JSON.parse(raw);
96
+ parsed.claudeAiOauth = credentials;
97
+
98
+ return await setKeychainCredentials(KEYCHAIN_SERVICE, JSON.stringify(parsed));
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
34
104
  export interface CodexCredentials {
35
105
  accessToken: string;
36
106
  accountId: string;
@@ -82,25 +152,3 @@ export async function getCodexCredentials(): Promise<CodexCredentials | null> {
82
152
  return null;
83
153
  }
84
154
  }
85
-
86
- export interface GeminiSettings {
87
- authType?: "google" | "api_key" | "vertex";
88
- apiKey?: string;
89
- project?: string;
90
- }
91
-
92
- export async function getGeminiSettings(): Promise<GeminiSettings | null> {
93
- try {
94
- const homedir = process.env.HOME || "~";
95
- const file = Bun.file(`${homedir}/.gemini/settings.json`);
96
-
97
- if (!(await file.exists())) {
98
- return null;
99
- }
100
-
101
- const content = await file.json();
102
- return content;
103
- } catch {
104
- return null;
105
- }
106
- }
@@ -1,63 +0,0 @@
1
- import { getGeminiSettings } from "../utils/keychain";
2
- import type { ProviderStatus, UsageMetric } from "./types";
3
-
4
- interface GeminiLimits {
5
- requestsPerDay: number;
6
- requestsPerMinute: number;
7
- plan: string;
8
- }
9
-
10
- function getLimitsForAuthType(authType?: string): GeminiLimits {
11
- switch (authType) {
12
- case "google":
13
- return { requestsPerDay: 1000, requestsPerMinute: 60, plan: "Google (Free)" };
14
- case "api_key":
15
- return { requestsPerDay: 250, requestsPerMinute: 10, plan: "API Key (Free)" };
16
- case "vertex":
17
- return { requestsPerDay: -1, requestsPerMinute: -1, plan: "Vertex AI" };
18
- default:
19
- return { requestsPerDay: 1000, requestsPerMinute: 60, plan: "Unknown" };
20
- }
21
- }
22
-
23
- export async function fetchGeminiUsage(): Promise<ProviderStatus> {
24
- const settings = await getGeminiSettings();
25
-
26
- if (!settings) {
27
- return {
28
- provider: "gemini",
29
- status: "unavailable",
30
- metrics: [],
31
- message: "Not configured. Run 'gemini' to set up.",
32
- };
33
- }
34
-
35
- const limits = getLimitsForAuthType(settings.authType);
36
- const metrics: UsageMetric[] = [];
37
-
38
- if (limits.requestsPerDay > 0) {
39
- metrics.push({
40
- name: "Daily Limit",
41
- percentage: -1,
42
- resetsAt: null,
43
- resetsIn: `${limits.requestsPerDay} req/day`,
44
- });
45
- }
46
-
47
- if (limits.requestsPerMinute > 0) {
48
- metrics.push({
49
- name: "Per-Minute",
50
- percentage: -1,
51
- resetsAt: null,
52
- resetsIn: `${limits.requestsPerMinute} req/min`,
53
- });
54
- }
55
-
56
- return {
57
- provider: "gemini",
58
- status: "limited",
59
- plan: limits.plan,
60
- metrics,
61
- message: "Live usage not available. Run /stats in Gemini CLI.",
62
- };
63
- }