@xtruder/opencode-claude-max-plugin 0.1.16 → 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/README.md +56 -18
- package/build/cch.d.ts +13 -0
- package/build/cch.d.ts.map +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +205 -261
- package/build/model.d.ts.map +1 -1
- package/build/server.d.ts +14 -0
- package/build/server.d.ts.map +1 -0
- package/build/server.js +1206 -0
- package/build/usage.d.ts +72 -5
- package/build/usage.d.ts.map +1 -1
- package/package.json +42 -5
- package/src/credentials.ts +156 -0
- package/src/tui.tsx +426 -0
- package/src/usage.ts +331 -0
- package/build/usage-cache.d.ts +0 -15
- package/build/usage-cache.d.ts.map +0 -1
package/build/usage.d.ts
CHANGED
|
@@ -17,12 +17,79 @@ export interface UsageData {
|
|
|
17
17
|
} | null;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
|
92
|
+
* Format a reset timestamp as relative time.
|
|
26
93
|
*/
|
|
27
|
-
export declare function
|
|
94
|
+
export declare function formatReset(resetsAt: string | null): string;
|
|
28
95
|
//# sourceMappingURL=usage.d.ts.map
|
package/build/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"
|
|
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.
|
|
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",
|
|
@@ -34,7 +49,8 @@
|
|
|
34
49
|
"prepare": "simple-git-hooks"
|
|
35
50
|
},
|
|
36
51
|
"dependencies": {
|
|
37
|
-
"@anthropic-ai/sdk": "0.74.0"
|
|
52
|
+
"@anthropic-ai/sdk": "0.74.0",
|
|
53
|
+
"xxhash-wasm": "^1.1.0"
|
|
38
54
|
},
|
|
39
55
|
"devDependencies": {
|
|
40
56
|
"@ai-sdk/provider": "3.0.8",
|
|
@@ -48,9 +64,30 @@
|
|
|
48
64
|
"typescript": "^5.7.0"
|
|
49
65
|
},
|
|
50
66
|
"peerDependencies": {
|
|
51
|
-
"@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
|
+
}
|
|
52
86
|
},
|
|
53
87
|
"simple-git-hooks": {
|
|
54
88
|
"pre-commit": "bun run lint && bun run format:check"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"opencode": ">=1.3.13"
|
|
55
92
|
}
|
|
56
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
|
+
}
|