@toninho09/opencode-usage 1.0.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/AGENTS.md +226 -0
- package/README.md +133 -0
- package/index.ts +31 -0
- package/lib/providers/base.ts +46 -0
- package/lib/providers/claude/client.ts +72 -0
- package/lib/providers/claude/formatter.ts +56 -0
- package/lib/providers/claude/index.ts +36 -0
- package/lib/providers/claude/types.ts +22 -0
- package/lib/providers/copilot/client.ts +185 -0
- package/lib/providers/copilot/formatter.ts +65 -0
- package/lib/providers/copilot/index.ts +36 -0
- package/lib/providers/copilot/types.ts +38 -0
- package/lib/providers/index.ts +8 -0
- package/lib/providers/registry.ts +56 -0
- package/lib/providers/zai/client.ts +97 -0
- package/lib/providers/zai/formatter.ts +77 -0
- package/lib/providers/zai/index.ts +41 -0
- package/lib/providers/zai/types.ts +23 -0
- package/lib/shared/auth.ts +49 -0
- package/lib/shared/formatting.ts +71 -0
- package/lib/shared/notification.ts +28 -0
- package/lib/shared/utils.ts +43 -0
- package/lib/usage-handler.ts +55 -0
- package/opencode.json +4 -0
- package/package.json +14 -0
- package/tsconfig.json +12 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# OpenCode Plugin - Agent Guidelines
|
|
2
|
+
|
|
3
|
+
## Development Commands
|
|
4
|
+
|
|
5
|
+
### Type Checking
|
|
6
|
+
```bash
|
|
7
|
+
npx tsc --noEmit
|
|
8
|
+
```
|
|
9
|
+
Run TypeScript compiler in check-only mode. No files are emitted due to `noEmit: true` in tsconfig.json.
|
|
10
|
+
|
|
11
|
+
**Note:** This project has no build, lint, or test scripts configured in package.json. Focus on type correctness and follow established code patterns.
|
|
12
|
+
|
|
13
|
+
## Code Style Guidelines
|
|
14
|
+
|
|
15
|
+
### Imports
|
|
16
|
+
- Use ES6 module syntax with named imports preferred over default imports
|
|
17
|
+
- Explicit type-only imports with `import type { }` for TypeScript types
|
|
18
|
+
- Node.js built-in modules: Use `import * as fs from "fs"` or modern `import { } from "node:stream"` protocol
|
|
19
|
+
- Import order: type imports first, then external dependencies, then internal modules
|
|
20
|
+
- Group related imports together
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
24
|
+
import * as fs from "fs"
|
|
25
|
+
import * as path from "path"
|
|
26
|
+
import * as os from "os"
|
|
27
|
+
import { fetchWithTimeout, createProgressBar } from "./utils"
|
|
28
|
+
import type { QueryResult } from "./types"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Formatting
|
|
32
|
+
- **Indentation:** 2 spaces (no tabs)
|
|
33
|
+
- **Semicolons:** Required at end of statements
|
|
34
|
+
- **Quotes:** Double quotes for strings (observed in codebase)
|
|
35
|
+
- **Trailing commas:** Allowed in multi-line arrays/objects
|
|
36
|
+
- **Line length:** Reasonable limits, prioritize readability over strict line length
|
|
37
|
+
|
|
38
|
+
### TypeScript & Types
|
|
39
|
+
- **Interfaces:** Use for object shapes, API responses, and contracts (`interface QueryResult`, `interface AuthConfig`)
|
|
40
|
+
- **Type aliases:** Use for unions and when exporting existing types (`export type { CopilotUsageResponse }`)
|
|
41
|
+
- **Annotations:** Explicit types required for function parameters and return types
|
|
42
|
+
- **Type assertions:** Use `as Type` sparingly, prefer type guards and narrowing
|
|
43
|
+
- **External types:** Use `any` for library types without definitions, add comments explaining usage
|
|
44
|
+
- **Strict mode:** Enabled in tsconfig.json - no implicit any, strict null checks
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface AuthProvider {
|
|
48
|
+
access?: string
|
|
49
|
+
refresh?: string
|
|
50
|
+
expires?: number
|
|
51
|
+
username?: string
|
|
52
|
+
type?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AuthConfig {
|
|
56
|
+
"github-copilot"?: AuthProvider
|
|
57
|
+
"anthropic"?: AuthProvider
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchCopilotUsage(authData: AuthProvider): Promise<CopilotUsageResponse>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Naming Conventions
|
|
64
|
+
- **Functions/Variables:** camelCase (`getCopilotUsageData`, `oauthToken`, `resetCountdown`)
|
|
65
|
+
- **Interfaces/Types:** PascalCase (`QueryResult`, `CopilotUsageResponse`, `AuthProvider`)
|
|
66
|
+
- **Constants:** SCREAMING_SNAKE_CASE (`GITHUB_API_BASE_URL`, `COPILOT_HEADERS`, `AUTH_CONFIG_PATH`)
|
|
67
|
+
- **File names:** kebab-case (`copilot-handler.ts`, `notification.ts`, `usage-handler.ts`)
|
|
68
|
+
- **Private functions:** No prefix, simply not exported from module
|
|
69
|
+
- **Exported functions:** Export individually using `export function` or `export async function`
|
|
70
|
+
|
|
71
|
+
### Error Handling
|
|
72
|
+
- Always wrap network operations in try-catch blocks
|
|
73
|
+
- Check `instanceof Error` before accessing error properties
|
|
74
|
+
- Throw errors for critical failures (authentication, configuration issues)
|
|
75
|
+
- Return `null` for non-critical failures (token exchange, optional file reads)
|
|
76
|
+
- Provide descriptive error messages with context and status codes
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Critical error - throw
|
|
80
|
+
if (!oauthToken) {
|
|
81
|
+
throw new Error("No OAuth token found in auth data")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Network error with detailed context
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetchWithTimeout(url, { headers })
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const errorText = await response.text()
|
|
89
|
+
throw new Error(`GitHub API Error ${response.status}: ${errorText}`)
|
|
90
|
+
}
|
|
91
|
+
return await response.json()
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: err instanceof Error ? err.message : String(err)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Non-critical failure - return null
|
|
100
|
+
function readAuthConfig(): AuthConfig | null {
|
|
101
|
+
try {
|
|
102
|
+
if (!fs.existsSync(AUTH_CONFIG_PATH)) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
const content = fs.readFileSync(AUTH_CONFIG_PATH, "utf-8")
|
|
106
|
+
return JSON.parse(content) as AuthConfig
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### File Organization
|
|
114
|
+
- **Entry point:** `index.ts` - plugin registration, command setup, exports `TestPlugin`
|
|
115
|
+
- **Library code:** `lib/` directory for all implementation
|
|
116
|
+
- **Types:** `lib/types.ts` - shared interfaces and type definitions
|
|
117
|
+
- **Utilities:** `lib/utils.ts` - reusable helper functions (`fetchWithTimeout`, `createProgressBar`)
|
|
118
|
+
- **Feature modules:** One concern per file (`copilot.ts`, `claude.ts`)
|
|
119
|
+
- **Handlers:** `lib/*-handler.ts` - command handlers with output formatting
|
|
120
|
+
- **Services:** `lib/notification.ts` - cross-cutting services like message sending
|
|
121
|
+
|
|
122
|
+
### Function Design
|
|
123
|
+
- **Small functions:** Single responsibility principle, aim for < 50 lines per function
|
|
124
|
+
- **Internal vs exported:** Keep implementation details internal, export only public APIs
|
|
125
|
+
- **Parameter objects:** Use interfaces for complex function parameters
|
|
126
|
+
- **Pure functions:** Prefer pure functions for formatting and data transformation
|
|
127
|
+
- **Async patterns:** Use explicit `async/await`, avoid callback patterns
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Internal helper - not exported
|
|
131
|
+
function formatQuotaLine(name: string, quota: QuotaDetail | undefined, width: number = 20): string {
|
|
132
|
+
if (!quota) return ""
|
|
133
|
+
if (quota.unlimited) {
|
|
134
|
+
return `${name.padEnd(14)} Unlimited`
|
|
135
|
+
}
|
|
136
|
+
const total = quota.entitlement
|
|
137
|
+
const used = total - quota.remaining
|
|
138
|
+
const percentRemaining = Math.round(quota.percent_remaining)
|
|
139
|
+
const progressBar = createProgressBar(percentRemaining, width)
|
|
140
|
+
return `${name.padEnd(14)} ${progressBar} ${percentRemaining}% (${used}/${total})`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Public API - exported
|
|
144
|
+
export async function getCopilotUsageData(): Promise<CopilotUsageResponse | null> {
|
|
145
|
+
const auth = readAuthConfig()
|
|
146
|
+
const copilotAuth = auth?.["github-copilot"]
|
|
147
|
+
if (!copilotAuth?.refresh) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
return fetchCopilotUsage(copilotAuth)
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### API Integration
|
|
155
|
+
- **Timeouts:** Always use `fetchWithTimeout` wrapper for network requests (default 10s timeout)
|
|
156
|
+
- **Headers:** Build header objects with helper functions (`buildGitHubHeaders`, `buildClaudeHeaders`)
|
|
157
|
+
- **Versioning:** Include API version in URLs (`/copilot_internal/v2/token`)
|
|
158
|
+
- **User-Agent:** Set appropriate user agent for API requests
|
|
159
|
+
- **Response handling:** Always check `response.ok` before parsing JSON
|
|
160
|
+
- **Token management:** Support both access and refresh tokens with fallback logic
|
|
161
|
+
|
|
162
|
+
### Configuration
|
|
163
|
+
- **Auth file:** `~/.local/share/opencode/auth.json` (OpenCode standard location)
|
|
164
|
+
- **Plugin config:** `opencode.json` for plugin registration
|
|
165
|
+
- **Constants:** Define at module level for API URLs, timeouts, headers
|
|
166
|
+
- **Environment:** No .env files - read from OpenCode's auth.json
|
|
167
|
+
- **Path construction:** Use `path.join()` with `os.homedir()` for cross-platform compatibility
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const AUTH_CONFIG_PATH = path.join(os.homedir(), ".local", "share", "opencode", "auth.json")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Output Formatting
|
|
174
|
+
- **Message sending:** Use `sendIgnoredMessage` for non-interactive responses
|
|
175
|
+
- **Progress bars:** `createProgressBar(percent, width)` for visual quota indicators
|
|
176
|
+
- **Date/Time:** Use `Date` API for countdown calculations
|
|
177
|
+
- **Localization:** Portuguese for user-facing command descriptions
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Send formatted message to user
|
|
181
|
+
await sendIgnoredMessage(client, sessionID, formattedOutput, params)
|
|
182
|
+
|
|
183
|
+
// Create progress bar
|
|
184
|
+
const progressBar = createProgressBar(percentRemaining, 20) // [##########----------] style
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Plugin Registration
|
|
188
|
+
Commands registered in `index.ts`:
|
|
189
|
+
- Plugin exports a function that returns hook configuration
|
|
190
|
+
- Register commands in `config` hook via `opencodeConfig.command["name"]`
|
|
191
|
+
- Set `template: ""` for slash commands without parameters
|
|
192
|
+
- Set `description` in Portuguese for user-facing help
|
|
193
|
+
- Handle command execution in `command.execute.before` hook
|
|
194
|
+
- Throw `Error("__COMMAND_HANDLED__")` or similar to prevent default processing
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
export const TestPlugin: Plugin = async ({ client }) => {
|
|
198
|
+
return {
|
|
199
|
+
config: async (opencodeConfig) => {
|
|
200
|
+
opencodeConfig.command ??= {}
|
|
201
|
+
opencodeConfig.command["usage"] = {
|
|
202
|
+
template: "",
|
|
203
|
+
description: "Mostra uso do Copilot e Claude Code",
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"command.execute.before": async (input, output) => {
|
|
207
|
+
if (input.command === "usage") {
|
|
208
|
+
try {
|
|
209
|
+
await handleUsageCommand({ client, sessionID: input.sessionID, params: output })
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
212
|
+
}
|
|
213
|
+
throw new Error("__USAGE_COMMAND_HANDLED__")
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
No test framework currently configured. When adding tests:
|
|
222
|
+
- Add test runner to devDependencies (e.g., vitest, jest)
|
|
223
|
+
- Add test scripts to package.json
|
|
224
|
+
- Prefer unit tests for pure functions (`formatQuotaLine`, `createProgressBar`, `getResetCountdown`)
|
|
225
|
+
- Integration tests for API calls (use mocked fetch responses)
|
|
226
|
+
- Always ensure type checks pass: `npx tsc --noEmit`
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# OpenCode Usage Plugin
|
|
2
|
+
|
|
3
|
+
Track your AI coding assistant usage in one place. This plugin shows quota information and usage statistics for GitHub Copilot, Claude Code, and Z.ai with a single command.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
> **Note:** This plugin requires [OpenCode](https://opencode.ai) to be installed.
|
|
8
|
+
|
|
9
|
+
- [Requirements](#requirements)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [What You'll See](#what-youll-see)
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Configuration Reference](#configuration-reference)
|
|
14
|
+
- [Troubleshooting](#troubleshooting)
|
|
15
|
+
- [Development](#development)
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
Before installing this plugin, make sure you have:
|
|
20
|
+
|
|
21
|
+
- [OpenCode installed](https://opencode.ai/docs)
|
|
22
|
+
- Node.js installed (for local plugin installation)
|
|
23
|
+
- API keys/tokens for at least one of the supported services:
|
|
24
|
+
- GitHub Copilot account
|
|
25
|
+
- Anthropic (Claude) API key
|
|
26
|
+
- Z.ai account
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### Step 1: Install OpenCode
|
|
31
|
+
|
|
32
|
+
If you haven't already, install OpenCode using one of these methods:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Quick install (recommended)
|
|
36
|
+
curl -fsSL https://opencode.ai/install | bash
|
|
37
|
+
|
|
38
|
+
# Or using npm
|
|
39
|
+
npm install -g opencode-ai
|
|
40
|
+
|
|
41
|
+
# Or using Homebrew (macOS/Linux)
|
|
42
|
+
brew install anomalyco/tap/opencode
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Step 2: Install the Usage Plugin
|
|
46
|
+
|
|
47
|
+
Choose one of the following installation methods:
|
|
48
|
+
|
|
49
|
+
#### Option A: Install from Local Files
|
|
50
|
+
|
|
51
|
+
Clone this repository to your project's plugins directory:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd /path/to/your/project
|
|
55
|
+
mkdir -p .opencode/plugins
|
|
56
|
+
git clone https://github.com/toninho09/opencode-usage.git .opencode/plugins/opencode-usage
|
|
57
|
+
cd .opencode/plugins/opencode-usage
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Tip:** For global installation (available in all projects), use:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mkdir -p ~/.config/opencode/plugins
|
|
64
|
+
git clone https://github.com/toninho09/opencode-usage.git ~/.config/opencode/plugins/opencode-usage
|
|
65
|
+
cd ~/.config/opencode/plugins/opencode-usage
|
|
66
|
+
npm install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Option B: Install via npm (coming soon)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g opencode-usage
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then add to your `opencode.json` config file:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"$schema": "https://opencode.ai/config.json",
|
|
80
|
+
"plugin": ["@toninho09/opencode-usage@latest"]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Step 3: Run OpenCode and Check Usage
|
|
85
|
+
|
|
86
|
+
Navigate to your project and start OpenCode:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd /path/to/your/project
|
|
90
|
+
opencode
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Now check your usage:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
/usage
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## What You'll See
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
╔════════════════════════════════════════╗
|
|
103
|
+
║ GITHUB COPILOT ║
|
|
104
|
+
╚════════════════════════════════════════╝
|
|
105
|
+
Plan: individual
|
|
106
|
+
Premium: [## ] 11% (33/300)
|
|
107
|
+
Chat: Unlimited
|
|
108
|
+
Completions: Unlimited
|
|
109
|
+
Quota Resets: 18d 20h (9999-12-31 23:59 UTC-03:00)
|
|
110
|
+
╔════════════════════════════════════════╗
|
|
111
|
+
║ CLAUDE CODE ║
|
|
112
|
+
╚════════════════════════════════════════╝
|
|
113
|
+
5 Hour: [################### ] 94%
|
|
114
|
+
7 Day: [# ] 7%
|
|
115
|
+
5h Resets: 2h (9999-12-31 23:59 UTC-03:00)
|
|
116
|
+
7d Resets: 6d 21h (9999-12-31 23:59 UTC-03:00)
|
|
117
|
+
Extra Usage: Disabled
|
|
118
|
+
╔════════════════════════════════════════╗
|
|
119
|
+
║ Z.AI CODING PLAN ║
|
|
120
|
+
╚════════════════════════════════════════╝
|
|
121
|
+
Account: xxxxxxxx............ (Z.AI Coding Plan)
|
|
122
|
+
Tokens: [# ] 4%
|
|
123
|
+
5h Resets: 3h (9999-12-31 23:59 UTC-03:00)
|
|
124
|
+
MCP Searches: [ ] 0% (0/100)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
Contributions welcome! Feel free to open an issue or pull request.
|
package/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { handleUsageCommand } from "./lib/usage-handler"
|
|
3
|
+
|
|
4
|
+
export const TestPlugin: Plugin = async ({ client }) => {
|
|
5
|
+
return {
|
|
6
|
+
config: async (opencodeConfig) => {
|
|
7
|
+
opencodeConfig.command ??= {}
|
|
8
|
+
opencodeConfig.command["usage"] = {
|
|
9
|
+
template: "",
|
|
10
|
+
description: "Shows usage for Copilot, Claude Code and Z.ai",
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"command.execute.before": async (input, output) => {
|
|
14
|
+
if (input.command === "usage") {
|
|
15
|
+
try {
|
|
16
|
+
await handleUsageCommand({
|
|
17
|
+
client,
|
|
18
|
+
sessionID: input.sessionID,
|
|
19
|
+
params: output,
|
|
20
|
+
})
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error("__USAGE_COMMAND_HANDLED__")
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default TestPlugin
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common interface for all usage monitoring providers
|
|
3
|
+
*/
|
|
4
|
+
export interface UsageProvider {
|
|
5
|
+
/**
|
|
6
|
+
* Display name of provider (e.g., "GitHub Copilot")
|
|
7
|
+
*/
|
|
8
|
+
readonly name: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unique provider ID for internal use (e.g., "copilot")
|
|
12
|
+
*/
|
|
13
|
+
readonly id: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Brief description of provider
|
|
17
|
+
*/
|
|
18
|
+
readonly description: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetches usage data from provider API
|
|
22
|
+
* @returns ProviderMessage with formatted content or null if not configured
|
|
23
|
+
*/
|
|
24
|
+
getUsageData(): Promise<ProviderMessage | null>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if provider is configured in auth file
|
|
28
|
+
* @returns true if provider is configured and ready to use
|
|
29
|
+
*/
|
|
30
|
+
isConfigured(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Message returned by provider with formatted usage data
|
|
35
|
+
*/
|
|
36
|
+
export interface ProviderMessage {
|
|
37
|
+
/**
|
|
38
|
+
* Formatted content for display to user
|
|
39
|
+
*/
|
|
40
|
+
readonly content: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Error message if any problem occurred (optional)
|
|
44
|
+
*/
|
|
45
|
+
readonly error?: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "../../shared/utils";
|
|
2
|
+
import { readAuthConfig, type AuthProvider } from "../../shared/auth";
|
|
3
|
+
import type { ClaudeUsageResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
const ANTHROPIC_API_BASE_URL = "https://api.anthropic.com";
|
|
6
|
+
|
|
7
|
+
export class ClaudeClient {
|
|
8
|
+
private readonly apiBaseUrl = ANTHROPIC_API_BASE_URL;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds headers for authentication with Claude API
|
|
12
|
+
*/
|
|
13
|
+
private buildClaudeHeaders(token: string): Record<string, string> {
|
|
14
|
+
return {
|
|
15
|
+
"Accept": "application/json, text/plain, */*",
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
"User-Agent": "claude-code/2.0.32",
|
|
18
|
+
"Authorization": `Bearer ${token}`,
|
|
19
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetches usage data from Claude API
|
|
25
|
+
*/
|
|
26
|
+
private async fetchClaudeUsage(authData: AuthProvider): Promise<ClaudeUsageResponse> {
|
|
27
|
+
const token = authData.access || authData.refresh;
|
|
28
|
+
|
|
29
|
+
if (!token) {
|
|
30
|
+
throw new Error("No token found in Anthropic auth data");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const url = `${this.apiBaseUrl}/api/oauth/usage`;
|
|
34
|
+
const response = await fetchWithTimeout(url, {
|
|
35
|
+
headers: this.buildClaudeHeaders(token),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errorText = await response.text();
|
|
40
|
+
throw new Error(`Anthropic API Error ${response.status}: ${errorText}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return response.json();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetches Claude usage data
|
|
48
|
+
*/
|
|
49
|
+
async fetchUsage(): Promise<ClaudeUsageResponse | null> {
|
|
50
|
+
const authConfig = readAuthConfig();
|
|
51
|
+
|
|
52
|
+
if (!authConfig) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const authData = authConfig.anthropic;
|
|
57
|
+
|
|
58
|
+
if (!authData) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return this.fetchClaudeUsage(authData);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks if Claude is configured
|
|
67
|
+
*/
|
|
68
|
+
isConfigured(): boolean {
|
|
69
|
+
const authConfig = readAuthConfig();
|
|
70
|
+
return !!authConfig?.anthropic;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { formatResetLine } from "../../shared/formatting";
|
|
2
|
+
import { createUsedProgressBar } from "../../shared/utils";
|
|
3
|
+
import type { ClaudeUsageResponse, QuotaPeriod } from "./types";
|
|
4
|
+
|
|
5
|
+
export class ClaudeFormatter {
|
|
6
|
+
/**
|
|
7
|
+
* Formats a Claude quota line with progress bar
|
|
8
|
+
*/
|
|
9
|
+
private formatQuotaLine(name: string, quota: QuotaPeriod | null): string {
|
|
10
|
+
if (!quota) {
|
|
11
|
+
return `${name.padEnd(16)} N/A`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const percentUsed = Math.round(quota.utilization);
|
|
15
|
+
const progressBar = createUsedProgressBar(percentUsed, 20);
|
|
16
|
+
|
|
17
|
+
return `${name.padEnd(16)} ${progressBar} ${percentUsed}%`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Formats Claude usage data for display
|
|
22
|
+
*/
|
|
23
|
+
format(data: ClaudeUsageResponse): string {
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
|
|
26
|
+
lines.push("╔════════════════════════════════════════╗");
|
|
27
|
+
lines.push("║ CLAUDE CODE ║");
|
|
28
|
+
lines.push("╚════════════════════════════════════════╝");
|
|
29
|
+
|
|
30
|
+
lines.push(this.formatQuotaLine("5 Hour:", data.five_hour));
|
|
31
|
+
lines.push(this.formatQuotaLine("7 Day:", data.seven_day));
|
|
32
|
+
|
|
33
|
+
if (data.five_hour) {
|
|
34
|
+
lines.push(formatResetLine("5h Resets:", data.five_hour.resets_at, 16));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (data.seven_day) {
|
|
38
|
+
lines.push(formatResetLine("7d Resets:", data.seven_day.resets_at, 16));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const extraUsage = data.extra_usage;
|
|
42
|
+
if (extraUsage.is_enabled) {
|
|
43
|
+
const utilization =
|
|
44
|
+
extraUsage.utilization !== null ? `${Math.round(extraUsage.utilization)}%` : "N/A";
|
|
45
|
+
const credits =
|
|
46
|
+
extraUsage.used_credits !== null && extraUsage.monthly_limit !== null
|
|
47
|
+
? `${extraUsage.used_credits}/${extraUsage.monthly_limit}`
|
|
48
|
+
: "N/A";
|
|
49
|
+
lines.push(`Extra Usage: Enabled - ${utilization} (${credits})`);
|
|
50
|
+
} else {
|
|
51
|
+
lines.push("Extra Usage: Disabled");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { UsageProvider, ProviderMessage } from "../base";
|
|
2
|
+
import { ClaudeClient } from "./client";
|
|
3
|
+
import { ClaudeFormatter } from "./formatter";
|
|
4
|
+
|
|
5
|
+
const client = new ClaudeClient();
|
|
6
|
+
const formatter = new ClaudeFormatter();
|
|
7
|
+
|
|
8
|
+
export const claudeProvider: UsageProvider = {
|
|
9
|
+
name: "Claude Code",
|
|
10
|
+
id: "claude",
|
|
11
|
+
description: "Monitoramento de uso do Claude Code",
|
|
12
|
+
|
|
13
|
+
async getUsageData(): Promise<ProviderMessage | null> {
|
|
14
|
+
try {
|
|
15
|
+
const data = await client.fetchUsage();
|
|
16
|
+
if (!data) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
content: formatter.format(data),
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
content: "",
|
|
26
|
+
error: error instanceof Error ? error.message : String(error),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
isConfigured(): boolean {
|
|
32
|
+
return client.isConfigured();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default claudeProvider;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface QuotaPeriod {
|
|
2
|
+
utilization: number;
|
|
3
|
+
resets_at: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ExtraUsage {
|
|
7
|
+
is_enabled: boolean;
|
|
8
|
+
monthly_limit: number | null;
|
|
9
|
+
used_credits: number | null;
|
|
10
|
+
utilization: number | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClaudeUsageResponse {
|
|
14
|
+
five_hour: QuotaPeriod | null;
|
|
15
|
+
seven_day: QuotaPeriod | null;
|
|
16
|
+
seven_day_oauth_apps: QuotaPeriod | null;
|
|
17
|
+
seven_day_opus: QuotaPeriod | null;
|
|
18
|
+
seven_day_sonnet: QuotaPeriod | null;
|
|
19
|
+
seven_day_cowork: QuotaPeriod | null;
|
|
20
|
+
iguana_necktie: unknown | null;
|
|
21
|
+
extra_usage: ExtraUsage;
|
|
22
|
+
}
|