@xtruder/opencode-claude-max-plugin 0.1.17 → 0.2.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/build/usage.d.ts CHANGED
@@ -17,12 +17,79 @@ export interface UsageData {
17
17
  } | null;
18
18
  }
19
19
  /**
20
- * Fetch current Claude subscription usage.
21
- * Returns null if not using OAuth credentials.
20
+ * Lightweight usage from response headers (5h/7d only).
21
+ * Updated by the server plugin's wrappedFetch on every inference call.
22
22
  */
23
- export declare function fetchUsage(credentialsPath?: string): Promise<UsageData | null>;
23
+ export interface HeaderUsage {
24
+ fiveHourUtil?: number;
25
+ sevenDayUtil?: number;
26
+ fiveHourReset?: number;
27
+ sevenDayReset?: number;
28
+ overageStatus?: string;
29
+ }
30
+ /**
31
+ * Shared cache file format. Contains the best available data from
32
+ * both the API and response headers, plus metadata for rate-limit-aware polling.
33
+ */
34
+ export interface UsageCache {
35
+ /** Full API response (null if never fetched or no credentials) */
36
+ api: UsageData | null;
37
+ /** Lightweight header data (null if no inference has run yet) */
38
+ headers: HeaderUsage | null;
39
+ /** When the cache was last written (epoch ms) */
40
+ updatedAt: number;
41
+ /** When the API was last successfully fetched (epoch ms, 0 = never) */
42
+ apiFetchedAt: number;
43
+ /** When the API rate limit expires (epoch ms, 0 = not rate limited) */
44
+ apiRateLimitUntil: number;
45
+ }
46
+ /**
47
+ * In-memory header usage, updated on every inference call.
48
+ * Only meaningful in the server/worker process.
49
+ */
50
+ export declare const cachedUsage: HeaderUsage;
51
+ /**
52
+ * Read the shared cache file. Returns empty cache if not found.
53
+ */
54
+ export declare function readUsageCache(): UsageCache;
55
+ /**
56
+ * Transactional read-modify-write of the cache file.
57
+ * Uses a lockfile for mutual exclusion and write-to-tmp+rename for atomicity.
58
+ */
59
+ export declare function writeUsageCache(update: Partial<UsageCache>): void;
60
+ /**
61
+ * Persist current in-memory header usage to the cache file.
62
+ * Called from the server plugin's wrappedFetch after parsing response headers.
63
+ */
64
+ export declare function persistCachedUsage(): void;
65
+ /**
66
+ * Fetch current Claude subscription usage from the API.
67
+ * Returns { data, retryAfterMs } so callers can track rate limit state.
68
+ * retryAfterMs > 0 means the API returned 429 with a Retry-After header.
69
+ */
70
+ export declare function fetchUsage(credentialsPath?: string): Promise<{
71
+ data: UsageData | null;
72
+ retryAfterMs: number;
73
+ }>;
74
+ /**
75
+ * Convert HeaderUsage (from response headers) to UsageData (API format).
76
+ * Provides a partial view — only 5h and 7d windows are available.
77
+ */
78
+ export declare function headerUsageToUsageData(h: HeaderUsage): UsageData;
79
+ /**
80
+ * Get the best available UsageData from the cache by merging both sources.
81
+ * - Headers provide the freshest 5h/7d numbers (updated every inference call)
82
+ * - API provides per-model breakdown and extra usage (updated less frequently)
83
+ * When both exist, header 5h/7d values override API values since they're fresher.
84
+ */
85
+ export declare function bestUsageFromCache(cache: UsageCache): UsageData | null;
86
+ /**
87
+ * Check if the API should be called.
88
+ * Returns true when not rate limited AND either never fetched or TTL expired.
89
+ */
90
+ export declare function shouldFetchApi(cache: UsageCache): boolean;
24
91
  /**
25
- * Format usage data as a display string matching Claude Code's /usage output.
92
+ * Format a reset timestamp as relative time.
26
93
  */
27
- export declare function formatUsage(data: UsageData): string;
94
+ export declare function formatReset(resetsAt: string | null): string;
28
95
  //# sourceMappingURL=usage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,cAAc,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAA;IACxC,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,WAAW,EAAE;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAID;;;GAGG;AACH,wBAAsB,UAAU,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAuBpF;AA0CD;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAwDnD"}
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,cAAc,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAA;IACxC,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,WAAW,EAAE;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;IACrB,iEAAiE;IACjE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,WAAgB,CAAA;AA8D1C;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAS3C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAoBjE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAMD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC;IAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CA0B3D;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,WAAW,GAAG,SAAS,CAsBhE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAmBtE;AAKD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAwB3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtruder/opencode-claude-max-plugin",
3
- "version": "0.1.17",
3
+ "version": "0.2.0",
4
4
  "description": "OpenCode plugin for Claude Max/Pro subscription via @anthropic-ai/sdk - uses Claude Code OAuth credentials",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -11,7 +11,10 @@
11
11
  ],
12
12
  "license": "MIT",
13
13
  "files": [
14
- "build"
14
+ "build",
15
+ "src/tui.tsx",
16
+ "src/usage.ts",
17
+ "src/credentials.ts"
15
18
  ],
16
19
  "type": "module",
17
20
  "main": "build/index.js",
@@ -20,10 +23,22 @@
20
23
  ".": {
21
24
  "import": "./build/index.js",
22
25
  "types": "./build/index.d.ts"
26
+ },
27
+ "./server": {
28
+ "import": "./build/server.js",
29
+ "config": {}
30
+ },
31
+ "./tui": {
32
+ "import": "./src/tui.tsx",
33
+ "config": {
34
+ "enabled": true,
35
+ "sidebar": true,
36
+ "poll_interval": 60
37
+ }
23
38
  }
24
39
  },
25
40
  "scripts": {
26
- "build": "bun build ./src/index.ts --outdir ./build --target node --format esm --packages external && bun x tsc --emitDeclarationOnly --declaration --outDir build",
41
+ "build": "bun build ./src/index.ts ./src/server.ts --outdir ./build --target node --format esm --packages external && bun x tsc --emitDeclarationOnly --declaration --outDir build",
27
42
  "dev": "bun run dev:config && bun build ./src/index.ts --outdir ./build --target node --format esm --packages external --watch",
28
43
  "dev:config": "bun scripts/dev-config.ts",
29
44
  "test": "bun test --concurrency 1 src/*.test.ts",
@@ -49,9 +64,30 @@
49
64
  "typescript": "^5.7.0"
50
65
  },
51
66
  "peerDependencies": {
52
- "@ai-sdk/provider": ">=2.0.0"
67
+ "@ai-sdk/provider": ">=2.0.0",
68
+ "@opencode-ai/plugin": "*",
69
+ "@opentui/core": "*",
70
+ "@opentui/solid": "*",
71
+ "solid-js": "*"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@opencode-ai/plugin": {
75
+ "optional": true
76
+ },
77
+ "@opentui/core": {
78
+ "optional": true
79
+ },
80
+ "@opentui/solid": {
81
+ "optional": true
82
+ },
83
+ "solid-js": {
84
+ "optional": true
85
+ }
53
86
  },
54
87
  "simple-git-hooks": {
55
88
  "pre-commit": "bun run lint && bun run format:check"
89
+ },
90
+ "engines": {
91
+ "opencode": ">=1.3.13"
56
92
  }
57
93
  }
@@ -0,0 +1,156 @@
1
+ import { readFileSync, existsSync } from "node:fs"
2
+ import { execSync } from "node:child_process"
3
+ import { join } from "node:path"
4
+ import { homedir } from "node:os"
5
+
6
+ export interface ClaudeOAuthCredentials {
7
+ accessToken: string
8
+ refreshToken: string
9
+ expiresAt: number
10
+ scopes: string[]
11
+ subscriptionType: string | null
12
+ }
13
+
14
+ interface CredentialsFile {
15
+ claudeAiOauth?: ClaudeOAuthCredentials
16
+ }
17
+
18
+ /**
19
+ * How long to cache credentials in memory before re-reading from disk.
20
+ */
21
+ const CREDENTIAL_CACHE_TTL_MS = 30_000
22
+
23
+ /**
24
+ * Buffer before expiry at which we consider the token "near expiry"
25
+ * and trigger a CLI refresh. 60 seconds matches opencode-claude-auth.
26
+ */
27
+ const EXPIRY_BUFFER_MS = 60_000
28
+
29
+ let cachedCreds: ClaudeOAuthCredentials | null = null
30
+ let cachedAt = 0
31
+ let cachedPath: string | undefined
32
+
33
+ /**
34
+ * Default path to Claude Code credentials file.
35
+ */
36
+ export function getCredentialsPath(): string {
37
+ return join(homedir(), ".claude", ".credentials.json")
38
+ }
39
+
40
+ /**
41
+ * Read OAuth credentials from Claude Code's credentials file.
42
+ * Returns null if the file doesn't exist or can't be parsed.
43
+ */
44
+ export function readClaudeCredentials(path?: string): ClaudeOAuthCredentials | null {
45
+ const credPath = path ?? getCredentialsPath()
46
+
47
+ if (!existsSync(credPath)) return null
48
+
49
+ try {
50
+ const raw = readFileSync(credPath, "utf-8")
51
+ const parsed: CredentialsFile = JSON.parse(raw)
52
+ if (!parsed.claudeAiOauth?.accessToken) return null
53
+ return parsed.claudeAiOauth
54
+ } catch {
55
+ return null
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check if credentials are expired (with 60s buffer).
61
+ */
62
+ export function isExpired(creds: ClaudeOAuthCredentials): boolean {
63
+ return Date.now() > creds.expiresAt - EXPIRY_BUFFER_MS
64
+ }
65
+
66
+ /**
67
+ * Run the Claude CLI to trigger an OAuth token refresh.
68
+ * The CLI writes refreshed credentials to ~/.claude/.credentials.json.
69
+ * Retries once on failure (matching opencode-claude-auth behavior).
70
+ */
71
+ export function refreshViaCli(): boolean {
72
+ const maxAttempts = 2
73
+ for (let i = 0; i < maxAttempts; i++) {
74
+ try {
75
+ execSync("claude -p . --model haiku", {
76
+ timeout: 60_000,
77
+ encoding: "utf-8",
78
+ env: { ...process.env, TERM: "dumb" },
79
+ stdio: "ignore",
80
+ })
81
+ return true
82
+ } catch {
83
+ // Non-fatal: retry once, then give up
84
+ }
85
+ }
86
+ return false
87
+ }
88
+
89
+ /**
90
+ * Read credentials, refreshing via Claude CLI if the token is near expiry.
91
+ * Returns null only if credentials are completely unavailable.
92
+ */
93
+ export function refreshIfNeeded(path?: string): ClaudeOAuthCredentials | null {
94
+ const creds = readClaudeCredentials(path)
95
+ if (!creds) return null
96
+
97
+ // Token still fresh
98
+ if (!isExpired(creds)) return creds
99
+
100
+ // Token near expiry — run Claude CLI to refresh
101
+ console.warn("[anthropic-sdk-provider] OAuth token near expiry, refreshing via Claude CLI...")
102
+ const refreshed = refreshViaCli()
103
+ if (!refreshed) {
104
+ console.warn("[anthropic-sdk-provider] CLI refresh failed. Token may be expired.")
105
+ // Return the existing creds anyway — they might still work for a bit
106
+ return creds
107
+ }
108
+
109
+ // Re-read from disk after CLI refresh
110
+ const fresh = readClaudeCredentials(path)
111
+ if (fresh && !isExpired(fresh)) {
112
+ console.warn("[anthropic-sdk-provider] Token refreshed successfully.")
113
+ return fresh
114
+ }
115
+
116
+ // CLI ran but didn't produce a fresh token — return what we have
117
+ return fresh ?? creds
118
+ }
119
+
120
+ /**
121
+ * Get cached credentials with automatic refresh.
122
+ * Uses in-memory caching with a 30-second TTL to avoid
123
+ * hitting the filesystem on every request.
124
+ * When the cached token is near expiry, triggers a CLI refresh.
125
+ */
126
+ export function getCachedCredentials(path?: string): ClaudeOAuthCredentials | null {
127
+ const now = Date.now()
128
+ const resolvedPath = path ?? cachedPath
129
+
130
+ // Return cached if still fresh and not near expiry
131
+ if (cachedCreds && now - cachedAt < CREDENTIAL_CACHE_TTL_MS && !isExpired(cachedCreds)) {
132
+ return cachedCreds
133
+ }
134
+
135
+ // Read and potentially refresh
136
+ const fresh = refreshIfNeeded(resolvedPath)
137
+ if (fresh) {
138
+ cachedCreds = fresh
139
+ cachedAt = now
140
+ cachedPath = resolvedPath
141
+ } else {
142
+ cachedCreds = null
143
+ }
144
+
145
+ return cachedCreds
146
+ }
147
+
148
+ /**
149
+ * Clear the in-memory credential cache.
150
+ * Forces the next getCachedCredentials() call to re-read from disk.
151
+ */
152
+ export function clearCredentialCache(): void {
153
+ cachedCreds = null
154
+ cachedAt = 0
155
+ cachedPath = undefined
156
+ }