@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/README.md +56 -18
- package/build/index.js +175 -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 +40 -4
- 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/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
An [OpenCode](https://opencode.ai/) plugin that enables Claude Pro/Max subscription access via the official [`@anthropic-ai/sdk`](https://github.com/anthropics/anthropic-sdk-typescript), using OAuth credentials from Claude Code (`~/.claude/.credentials.json`).
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
## Why?
|
|
6
8
|
|
|
7
9
|
- **Use your Claude subscription** — Automatically reads OAuth credentials from Claude Code, no separate API key needed
|
|
@@ -9,7 +11,7 @@ An [OpenCode](https://opencode.ai/) plugin that enables Claude Pro/Max subscript
|
|
|
9
11
|
- **Prompt caching** — 98% of input tokens served from cache (system prompt + tools cached globally)
|
|
10
12
|
- **All Claude models** — Opus 4.6, Sonnet 4.6, Haiku 4.5
|
|
11
13
|
- **Extended thinking** — Full reasoning support with thinking variants (high/max)
|
|
12
|
-
- **Usage tracking** —
|
|
14
|
+
- **Usage tracking** — Sidebar widget with live progress bars + `/usage` command with full details
|
|
13
15
|
- **Self-registering** — Models are registered automatically, no manual provider config needed
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
@@ -27,6 +29,57 @@ That's it. The plugin self-registers the `anthropic-sdk` provider and its models
|
|
|
27
29
|
|
|
28
30
|
Then open OpenCode and run `/connect` → Other → `anthropic-sdk`. If Claude Code is installed and you're logged in, credentials are read automatically from `~/.claude/.credentials.json` — no API key needed.
|
|
29
31
|
|
|
32
|
+
### TUI Plugin (sidebar + /usage command)
|
|
33
|
+
|
|
34
|
+
The plugin includes a TUI component that shows subscription usage in the sidebar and registers a `/usage` slash command. To enable it, add the plugin to your `tui.json` as well:
|
|
35
|
+
|
|
36
|
+
**Project-level** (`.opencode/tui.json`):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"plugin": ["@xtruder/opencode-claude-max-plugin"]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Or globally** (`~/.config/opencode/tui.json`):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"plugin": ["@xtruder/opencode-claude-max-plugin"]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The TUI plugin provides:
|
|
53
|
+
|
|
54
|
+
- **Sidebar widget** — Compact progress bars for 5-hour session and 7-day weekly usage
|
|
55
|
+
- **`/usage` command** — Opens a dialog with full usage breakdown (per-model, extra usage)
|
|
56
|
+
- **Auto-refresh** — Polls the usage API every 60s and after each inference call
|
|
57
|
+
|
|
58
|
+
#### TUI Configuration
|
|
59
|
+
|
|
60
|
+
Options can be set in the `tui.json` plugin entry:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"plugin": [
|
|
65
|
+
[
|
|
66
|
+
"@xtruder/opencode-claude-max-plugin",
|
|
67
|
+
{
|
|
68
|
+
"enabled": true,
|
|
69
|
+
"sidebar": true,
|
|
70
|
+
"poll_interval": 60
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Option | Type | Default | Description |
|
|
78
|
+
| --------------- | ------- | ------- | ----------------------------------------- |
|
|
79
|
+
| `enabled` | boolean | `true` | Enable/disable the TUI plugin entirely |
|
|
80
|
+
| `sidebar` | boolean | `true` | Show/hide sidebar usage widget |
|
|
81
|
+
| `poll_interval` | number | `60` | Seconds between usage API polls (min: 10) |
|
|
82
|
+
|
|
30
83
|
### Custom model options
|
|
31
84
|
|
|
32
85
|
If you want to override model settings (e.g. thinking budgets, variants), you can add a `provider` block alongside the plugin:
|
|
@@ -71,25 +124,10 @@ For Claude Code credentials, log in via `claude` CLI first (`claude auth login`)
|
|
|
71
124
|
- MCP tool name remapping (`server_tool` → `mcp__server__tool`)
|
|
72
125
|
- Extended thinking with signature passthrough for multi-turn conversations
|
|
73
126
|
- Prompt caching (98% cache hit rate with full OpenCode tool set)
|
|
127
|
+
- CCH request signing — computes xxHash64 body integrity hash to unlock features like fast mode
|
|
74
128
|
- Subscription rate limit detection — fails fast with clear message instead of hanging
|
|
75
129
|
- Long context auto-detection — adds `context-1m` beta header when request body is large
|
|
76
|
-
- `/usage` slash command
|
|
77
|
-
|
|
78
|
-
## Usage Command
|
|
79
|
-
|
|
80
|
-
After adding the config, run `/usage` inside OpenCode to see your subscription usage:
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
Claude Subscription Usage
|
|
84
|
-
────────────────────────────────────────────────────
|
|
85
|
-
Current session
|
|
86
|
-
████████████████████████████████░░░░░░░░░░░░░░░░░░ 67% used
|
|
87
|
-
Resets 7:00 PM GMT+1
|
|
88
|
-
|
|
89
|
-
Current week (all models)
|
|
90
|
-
████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8% used
|
|
91
|
-
Resets Mar 30, 7:00 AM GMT+2
|
|
92
|
-
```
|
|
130
|
+
- TUI sidebar with live usage bars + `/usage` slash command with full subscription details
|
|
93
131
|
|
|
94
132
|
## With Vercel AI SDK
|
|
95
133
|
|
package/build/index.js
CHANGED
|
@@ -272,194 +272,6 @@ function formatToolResultContent(output) {
|
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
// src/usage-cache.ts
|
|
276
|
-
var cachedUsage = {};
|
|
277
|
-
|
|
278
|
-
// src/credentials.ts
|
|
279
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
280
|
-
import { execSync } from "node:child_process";
|
|
281
|
-
import { join as join2 } from "node:path";
|
|
282
|
-
import { homedir as homedir2 } from "node:os";
|
|
283
|
-
var CREDENTIAL_CACHE_TTL_MS = 30000;
|
|
284
|
-
var EXPIRY_BUFFER_MS = 60000;
|
|
285
|
-
var cachedCreds = null;
|
|
286
|
-
var cachedAt = 0;
|
|
287
|
-
var cachedPath;
|
|
288
|
-
function getCredentialsPath() {
|
|
289
|
-
return join2(homedir2(), ".claude", ".credentials.json");
|
|
290
|
-
}
|
|
291
|
-
function readClaudeCredentials(path) {
|
|
292
|
-
const credPath = path ?? getCredentialsPath();
|
|
293
|
-
if (!existsSync2(credPath))
|
|
294
|
-
return null;
|
|
295
|
-
try {
|
|
296
|
-
const raw = readFileSync2(credPath, "utf-8");
|
|
297
|
-
const parsed = JSON.parse(raw);
|
|
298
|
-
if (!parsed.claudeAiOauth?.accessToken)
|
|
299
|
-
return null;
|
|
300
|
-
return parsed.claudeAiOauth;
|
|
301
|
-
} catch {
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
function isExpired(creds) {
|
|
306
|
-
return Date.now() > creds.expiresAt - EXPIRY_BUFFER_MS;
|
|
307
|
-
}
|
|
308
|
-
function refreshViaCli() {
|
|
309
|
-
const maxAttempts = 2;
|
|
310
|
-
for (let i = 0;i < maxAttempts; i++) {
|
|
311
|
-
try {
|
|
312
|
-
execSync("claude -p . --model haiku", {
|
|
313
|
-
timeout: 60000,
|
|
314
|
-
encoding: "utf-8",
|
|
315
|
-
env: { ...process.env, TERM: "dumb" },
|
|
316
|
-
stdio: "ignore"
|
|
317
|
-
});
|
|
318
|
-
return true;
|
|
319
|
-
} catch {}
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
function refreshIfNeeded(path) {
|
|
324
|
-
const creds = readClaudeCredentials(path);
|
|
325
|
-
if (!creds)
|
|
326
|
-
return null;
|
|
327
|
-
if (!isExpired(creds))
|
|
328
|
-
return creds;
|
|
329
|
-
console.warn("[anthropic-sdk-provider] OAuth token near expiry, refreshing via Claude CLI...");
|
|
330
|
-
const refreshed = refreshViaCli();
|
|
331
|
-
if (!refreshed) {
|
|
332
|
-
console.warn("[anthropic-sdk-provider] CLI refresh failed. Token may be expired.");
|
|
333
|
-
return creds;
|
|
334
|
-
}
|
|
335
|
-
const fresh = readClaudeCredentials(path);
|
|
336
|
-
if (fresh && !isExpired(fresh)) {
|
|
337
|
-
console.warn("[anthropic-sdk-provider] Token refreshed successfully.");
|
|
338
|
-
return fresh;
|
|
339
|
-
}
|
|
340
|
-
return fresh ?? creds;
|
|
341
|
-
}
|
|
342
|
-
function getCachedCredentials(path) {
|
|
343
|
-
const now = Date.now();
|
|
344
|
-
const resolvedPath = path ?? cachedPath;
|
|
345
|
-
if (cachedCreds && now - cachedAt < CREDENTIAL_CACHE_TTL_MS && !isExpired(cachedCreds)) {
|
|
346
|
-
return cachedCreds;
|
|
347
|
-
}
|
|
348
|
-
const fresh = refreshIfNeeded(resolvedPath);
|
|
349
|
-
if (fresh) {
|
|
350
|
-
cachedCreds = fresh;
|
|
351
|
-
cachedAt = now;
|
|
352
|
-
cachedPath = resolvedPath;
|
|
353
|
-
} else {
|
|
354
|
-
cachedCreds = null;
|
|
355
|
-
}
|
|
356
|
-
return cachedCreds;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/usage.ts
|
|
360
|
-
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
361
|
-
async function fetchUsage(credentialsPath) {
|
|
362
|
-
const creds = readClaudeCredentials(credentialsPath);
|
|
363
|
-
if (!creds)
|
|
364
|
-
return null;
|
|
365
|
-
if (isExpired(creds))
|
|
366
|
-
return null;
|
|
367
|
-
const resp = await fetch(USAGE_URL, {
|
|
368
|
-
headers: {
|
|
369
|
-
authorization: `Bearer ${creds.accessToken}`,
|
|
370
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
371
|
-
"content-type": "application/json",
|
|
372
|
-
"user-agent": "claude-cli/2.1.81 (external, sdk-cli)",
|
|
373
|
-
"x-app": "cli"
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
if (!resp.ok) {
|
|
377
|
-
const body = await resp.text().catch(() => "");
|
|
378
|
-
const msg = body.includes("message") ? JSON.parse(body)?.error?.message ?? body : `HTTP ${resp.status}`;
|
|
379
|
-
throw new Error(`Usage API error: ${msg}`);
|
|
380
|
-
}
|
|
381
|
-
return resp.json();
|
|
382
|
-
}
|
|
383
|
-
function progressBar(utilization, width = 50) {
|
|
384
|
-
const filled = Math.round(utilization / 100 * width);
|
|
385
|
-
const bar = "█".repeat(filled) + " ".repeat(width - filled);
|
|
386
|
-
return bar;
|
|
387
|
-
}
|
|
388
|
-
function formatReset(resetsAt) {
|
|
389
|
-
if (!resetsAt)
|
|
390
|
-
return "";
|
|
391
|
-
const reset = new Date(resetsAt);
|
|
392
|
-
const now = new Date;
|
|
393
|
-
const diffMs = reset.getTime() - now.getTime();
|
|
394
|
-
if (diffMs <= 0)
|
|
395
|
-
return "Reset now";
|
|
396
|
-
const diffMin = Math.floor(diffMs / 60000);
|
|
397
|
-
const diffHour = Math.floor(diffMin / 60);
|
|
398
|
-
const timeStr = reset.toLocaleTimeString("en-US", {
|
|
399
|
-
hour: "numeric",
|
|
400
|
-
minute: "2-digit",
|
|
401
|
-
timeZoneName: "short"
|
|
402
|
-
});
|
|
403
|
-
const dateStr = reset.toLocaleDateString("en-US", {
|
|
404
|
-
month: "short",
|
|
405
|
-
day: "numeric"
|
|
406
|
-
});
|
|
407
|
-
if (diffHour < 24) {
|
|
408
|
-
return `Resets ${timeStr}`;
|
|
409
|
-
}
|
|
410
|
-
return `Resets ${dateStr}, ${timeStr}`;
|
|
411
|
-
}
|
|
412
|
-
function formatUsage(data) {
|
|
413
|
-
const lines = [];
|
|
414
|
-
if (data.five_hour) {
|
|
415
|
-
const pct = Math.round(data.five_hour.utilization);
|
|
416
|
-
lines.push(" Current session");
|
|
417
|
-
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
418
|
-
lines.push(` ${formatReset(data.five_hour.resets_at)}`);
|
|
419
|
-
lines.push("");
|
|
420
|
-
}
|
|
421
|
-
if (data.seven_day) {
|
|
422
|
-
const pct = Math.round(data.seven_day.utilization);
|
|
423
|
-
lines.push(" Current week (all models)");
|
|
424
|
-
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
425
|
-
lines.push(` ${formatReset(data.seven_day.resets_at)}`);
|
|
426
|
-
lines.push("");
|
|
427
|
-
}
|
|
428
|
-
if (data.seven_day_sonnet) {
|
|
429
|
-
const pct = Math.round(data.seven_day_sonnet.utilization);
|
|
430
|
-
lines.push(" Current week (Sonnet only)");
|
|
431
|
-
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
432
|
-
if (data.seven_day_sonnet.resets_at) {
|
|
433
|
-
lines.push(` ${formatReset(data.seven_day_sonnet.resets_at)}`);
|
|
434
|
-
}
|
|
435
|
-
lines.push("");
|
|
436
|
-
}
|
|
437
|
-
if (data.seven_day_opus) {
|
|
438
|
-
const pct = Math.round(data.seven_day_opus.utilization);
|
|
439
|
-
lines.push(" Current week (Opus only)");
|
|
440
|
-
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
441
|
-
if (data.seven_day_opus.resets_at) {
|
|
442
|
-
lines.push(` ${formatReset(data.seven_day_opus.resets_at)}`);
|
|
443
|
-
}
|
|
444
|
-
lines.push("");
|
|
445
|
-
}
|
|
446
|
-
if (data.extra_usage) {
|
|
447
|
-
if (data.extra_usage.is_enabled) {
|
|
448
|
-
const pct = Math.round(data.extra_usage.utilization ?? 0);
|
|
449
|
-
lines.push(" Extra usage");
|
|
450
|
-
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
451
|
-
if (data.extra_usage.monthly_limit != null) {
|
|
452
|
-
lines.push(` $${data.extra_usage.used_credits?.toFixed(2) ?? "0.00"} / $${data.extra_usage.monthly_limit.toFixed(2)}`);
|
|
453
|
-
}
|
|
454
|
-
} else {
|
|
455
|
-
lines.push(" Extra usage: disabled");
|
|
456
|
-
}
|
|
457
|
-
lines.push("");
|
|
458
|
-
}
|
|
459
|
-
return lines.join(`
|
|
460
|
-
`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
275
|
// src/tools.ts
|
|
464
276
|
function cleanSchema(schema) {
|
|
465
277
|
const cleaned = {};
|
|
@@ -732,74 +544,6 @@ function buildMetadata() {
|
|
|
732
544
|
user_id: JSON.stringify({ device_id: DEVICE_ID })
|
|
733
545
|
};
|
|
734
546
|
}
|
|
735
|
-
function isUsageCommand(prompt) {
|
|
736
|
-
for (let i = prompt.length - 1;i >= 0; i--) {
|
|
737
|
-
const msg = prompt[i];
|
|
738
|
-
if (msg.role !== "user")
|
|
739
|
-
continue;
|
|
740
|
-
const text = msg.content.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
|
|
741
|
-
return /^\/usage(\s|$)/i.test(text);
|
|
742
|
-
}
|
|
743
|
-
return false;
|
|
744
|
-
}
|
|
745
|
-
function formatReset2(epoch) {
|
|
746
|
-
if (!epoch)
|
|
747
|
-
return "";
|
|
748
|
-
const reset = new Date(epoch * 1000);
|
|
749
|
-
const now = new Date;
|
|
750
|
-
const diffMs = reset.getTime() - now.getTime();
|
|
751
|
-
if (diffMs <= 0)
|
|
752
|
-
return "Reset now";
|
|
753
|
-
const timeStr = reset.toLocaleTimeString("en-US", {
|
|
754
|
-
hour: "numeric",
|
|
755
|
-
minute: "2-digit",
|
|
756
|
-
timeZoneName: "short"
|
|
757
|
-
});
|
|
758
|
-
const dateStr = reset.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
759
|
-
return diffMs < 86400000 ? `Resets ${timeStr}` : `Resets ${dateStr}, ${timeStr}`;
|
|
760
|
-
}
|
|
761
|
-
var bar = (pct, width = 50) => "█".repeat(Math.round(pct / 100 * width)) + " ".repeat(width - Math.round(pct / 100 * width));
|
|
762
|
-
function formatCachedUsage(usage) {
|
|
763
|
-
const lines = [];
|
|
764
|
-
const h5 = Math.round((usage.fiveHourUtil ?? 0) * 100);
|
|
765
|
-
lines.push(" Current session");
|
|
766
|
-
lines.push(` ${bar(h5)} ${h5}% used`);
|
|
767
|
-
if (usage.fiveHourReset)
|
|
768
|
-
lines.push(` ${formatReset2(usage.fiveHourReset)}`);
|
|
769
|
-
lines.push("");
|
|
770
|
-
const d7 = Math.round((usage.sevenDayUtil ?? 0) * 100);
|
|
771
|
-
lines.push(" Current week");
|
|
772
|
-
lines.push(` ${bar(d7)} ${d7}% used`);
|
|
773
|
-
if (usage.sevenDayReset)
|
|
774
|
-
lines.push(` ${formatReset2(usage.sevenDayReset)}`);
|
|
775
|
-
lines.push("");
|
|
776
|
-
if (usage.overageStatus === "rejected") {
|
|
777
|
-
lines.push(" Extra usage: disabled");
|
|
778
|
-
} else if (usage.overageStatus) {
|
|
779
|
-
lines.push(` Extra usage: ${usage.overageStatus}`);
|
|
780
|
-
}
|
|
781
|
-
return lines.join(`
|
|
782
|
-
`);
|
|
783
|
-
}
|
|
784
|
-
async function handleUsageCommand() {
|
|
785
|
-
if (cachedUsage.timestamp) {
|
|
786
|
-
throw new Error(`Claude Subscription Usage
|
|
787
|
-
` + "─".repeat(52) + `
|
|
788
|
-
` + formatCachedUsage(cachedUsage));
|
|
789
|
-
}
|
|
790
|
-
try {
|
|
791
|
-
const data = await fetchUsage();
|
|
792
|
-
if (data) {
|
|
793
|
-
throw new Error(`Claude Subscription Usage
|
|
794
|
-
` + "─".repeat(52) + `
|
|
795
|
-
` + formatUsage(data));
|
|
796
|
-
}
|
|
797
|
-
} catch (e) {
|
|
798
|
-
if (e.message?.startsWith("Claude Subscription Usage"))
|
|
799
|
-
throw e;
|
|
800
|
-
}
|
|
801
|
-
throw new Error("No usage data available. Send a message first, then try /usage again.");
|
|
802
|
-
}
|
|
803
547
|
|
|
804
548
|
class AnthropicSDKModel {
|
|
805
549
|
client;
|
|
@@ -940,8 +684,6 @@ class AnthropicSDKModel {
|
|
|
940
684
|
return { params, warnings };
|
|
941
685
|
}
|
|
942
686
|
async doGenerate(options) {
|
|
943
|
-
if (isUsageCommand(options.prompt))
|
|
944
|
-
await handleUsageCommand();
|
|
945
687
|
const { params, warnings } = this.buildParams(options);
|
|
946
688
|
if (!options.maxOutputTokens && params.max_tokens > 4096) {
|
|
947
689
|
params.max_tokens = 4096;
|
|
@@ -1014,8 +756,6 @@ class AnthropicSDKModel {
|
|
|
1014
756
|
};
|
|
1015
757
|
}
|
|
1016
758
|
async doStream(options) {
|
|
1017
|
-
if (isUsageCommand(options.prompt))
|
|
1018
|
-
await handleUsageCommand();
|
|
1019
759
|
const { params, warnings } = this.buildParams(options);
|
|
1020
760
|
let anthropicStream;
|
|
1021
761
|
try {
|
|
@@ -1078,6 +818,180 @@ class AnthropicSDKModel {
|
|
|
1078
818
|
}
|
|
1079
819
|
}
|
|
1080
820
|
|
|
821
|
+
// src/credentials.ts
|
|
822
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
823
|
+
import { execSync } from "node:child_process";
|
|
824
|
+
import { join as join2 } from "node:path";
|
|
825
|
+
import { homedir as homedir2 } from "node:os";
|
|
826
|
+
var CREDENTIAL_CACHE_TTL_MS = 30000;
|
|
827
|
+
var EXPIRY_BUFFER_MS = 60000;
|
|
828
|
+
var cachedCreds = null;
|
|
829
|
+
var cachedAt = 0;
|
|
830
|
+
var cachedPath;
|
|
831
|
+
function getCredentialsPath() {
|
|
832
|
+
return join2(homedir2(), ".claude", ".credentials.json");
|
|
833
|
+
}
|
|
834
|
+
function readClaudeCredentials(path) {
|
|
835
|
+
const credPath = path ?? getCredentialsPath();
|
|
836
|
+
if (!existsSync2(credPath))
|
|
837
|
+
return null;
|
|
838
|
+
try {
|
|
839
|
+
const raw = readFileSync2(credPath, "utf-8");
|
|
840
|
+
const parsed = JSON.parse(raw);
|
|
841
|
+
if (!parsed.claudeAiOauth?.accessToken)
|
|
842
|
+
return null;
|
|
843
|
+
return parsed.claudeAiOauth;
|
|
844
|
+
} catch {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function isExpired(creds) {
|
|
849
|
+
return Date.now() > creds.expiresAt - EXPIRY_BUFFER_MS;
|
|
850
|
+
}
|
|
851
|
+
function refreshViaCli() {
|
|
852
|
+
const maxAttempts = 2;
|
|
853
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
854
|
+
try {
|
|
855
|
+
execSync("claude -p . --model haiku", {
|
|
856
|
+
timeout: 60000,
|
|
857
|
+
encoding: "utf-8",
|
|
858
|
+
env: { ...process.env, TERM: "dumb" },
|
|
859
|
+
stdio: "ignore"
|
|
860
|
+
});
|
|
861
|
+
return true;
|
|
862
|
+
} catch {}
|
|
863
|
+
}
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
function refreshIfNeeded(path) {
|
|
867
|
+
const creds = readClaudeCredentials(path);
|
|
868
|
+
if (!creds)
|
|
869
|
+
return null;
|
|
870
|
+
if (!isExpired(creds))
|
|
871
|
+
return creds;
|
|
872
|
+
console.warn("[anthropic-sdk-provider] OAuth token near expiry, refreshing via Claude CLI...");
|
|
873
|
+
const refreshed = refreshViaCli();
|
|
874
|
+
if (!refreshed) {
|
|
875
|
+
console.warn("[anthropic-sdk-provider] CLI refresh failed. Token may be expired.");
|
|
876
|
+
return creds;
|
|
877
|
+
}
|
|
878
|
+
const fresh = readClaudeCredentials(path);
|
|
879
|
+
if (fresh && !isExpired(fresh)) {
|
|
880
|
+
console.warn("[anthropic-sdk-provider] Token refreshed successfully.");
|
|
881
|
+
return fresh;
|
|
882
|
+
}
|
|
883
|
+
return fresh ?? creds;
|
|
884
|
+
}
|
|
885
|
+
function getCachedCredentials(path) {
|
|
886
|
+
const now = Date.now();
|
|
887
|
+
const resolvedPath = path ?? cachedPath;
|
|
888
|
+
if (cachedCreds && now - cachedAt < CREDENTIAL_CACHE_TTL_MS && !isExpired(cachedCreds)) {
|
|
889
|
+
return cachedCreds;
|
|
890
|
+
}
|
|
891
|
+
const fresh = refreshIfNeeded(resolvedPath);
|
|
892
|
+
if (fresh) {
|
|
893
|
+
cachedCreds = fresh;
|
|
894
|
+
cachedAt = now;
|
|
895
|
+
cachedPath = resolvedPath;
|
|
896
|
+
} else {
|
|
897
|
+
cachedCreds = null;
|
|
898
|
+
}
|
|
899
|
+
return cachedCreds;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/usage.ts
|
|
903
|
+
import {
|
|
904
|
+
readFileSync as readFileSync3,
|
|
905
|
+
writeFileSync,
|
|
906
|
+
mkdirSync,
|
|
907
|
+
unlinkSync,
|
|
908
|
+
openSync,
|
|
909
|
+
closeSync,
|
|
910
|
+
renameSync,
|
|
911
|
+
constants as fsConstants
|
|
912
|
+
} from "node:fs";
|
|
913
|
+
import { join as join3 } from "node:path";
|
|
914
|
+
import { homedir as homedir3 } from "node:os";
|
|
915
|
+
var cachedUsage = {};
|
|
916
|
+
var XDG_STATE = process.env.XDG_STATE_HOME || join3(homedir3(), ".local", "state");
|
|
917
|
+
var USAGE_CACHE_DIR = join3(XDG_STATE, "opencode");
|
|
918
|
+
var USAGE_CACHE_FILE = join3(USAGE_CACHE_DIR, "usage-cache.json");
|
|
919
|
+
function emptyCache() {
|
|
920
|
+
return { api: null, headers: null, updatedAt: 0, apiFetchedAt: 0, apiRateLimitUntil: 0 };
|
|
921
|
+
}
|
|
922
|
+
var USAGE_LOCK_FILE = USAGE_CACHE_FILE + ".lock";
|
|
923
|
+
var USAGE_TMP_FILE = USAGE_CACHE_FILE + ".tmp";
|
|
924
|
+
var LOCK_STALE_MS = 5000;
|
|
925
|
+
function acquireLock() {
|
|
926
|
+
try {
|
|
927
|
+
mkdirSync(USAGE_CACHE_DIR, { recursive: true });
|
|
928
|
+
const fd = openSync(USAGE_LOCK_FILE, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY);
|
|
929
|
+
writeFileSync(fd, `${process.pid}
|
|
930
|
+
${Date.now()}`);
|
|
931
|
+
closeSync(fd);
|
|
932
|
+
return true;
|
|
933
|
+
} catch (err) {
|
|
934
|
+
if (err?.code !== "EEXIST")
|
|
935
|
+
return false;
|
|
936
|
+
try {
|
|
937
|
+
const text = readFileSync3(USAGE_LOCK_FILE, "utf-8");
|
|
938
|
+
const ts = parseInt(text.split(`
|
|
939
|
+
`)[1] ?? "0");
|
|
940
|
+
if (Date.now() - ts > LOCK_STALE_MS) {
|
|
941
|
+
unlinkSync(USAGE_LOCK_FILE);
|
|
942
|
+
return acquireLock();
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
try {
|
|
946
|
+
unlinkSync(USAGE_LOCK_FILE);
|
|
947
|
+
} catch {}
|
|
948
|
+
return acquireLock();
|
|
949
|
+
}
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function releaseLock() {
|
|
954
|
+
try {
|
|
955
|
+
unlinkSync(USAGE_LOCK_FILE);
|
|
956
|
+
} catch {}
|
|
957
|
+
}
|
|
958
|
+
function readUsageCache() {
|
|
959
|
+
try {
|
|
960
|
+
const text = readFileSync3(USAGE_CACHE_FILE, "utf-8");
|
|
961
|
+
const data = JSON.parse(text);
|
|
962
|
+
if (data && typeof data.updatedAt === "number")
|
|
963
|
+
return data;
|
|
964
|
+
} catch {}
|
|
965
|
+
return emptyCache();
|
|
966
|
+
}
|
|
967
|
+
function writeUsageCache(update) {
|
|
968
|
+
if (!acquireLock())
|
|
969
|
+
return;
|
|
970
|
+
try {
|
|
971
|
+
const existing = readUsageCache();
|
|
972
|
+
const merged = {
|
|
973
|
+
...existing,
|
|
974
|
+
...update,
|
|
975
|
+
updatedAt: Date.now()
|
|
976
|
+
};
|
|
977
|
+
mkdirSync(USAGE_CACHE_DIR, { recursive: true });
|
|
978
|
+
writeFileSync(USAGE_TMP_FILE, JSON.stringify(merged));
|
|
979
|
+
renameSync(USAGE_TMP_FILE, USAGE_CACHE_FILE);
|
|
980
|
+
} catch {
|
|
981
|
+
try {
|
|
982
|
+
unlinkSync(USAGE_TMP_FILE);
|
|
983
|
+
} catch {}
|
|
984
|
+
} finally {
|
|
985
|
+
releaseLock();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function persistCachedUsage() {
|
|
989
|
+
if (cachedUsage.fiveHourUtil == null && cachedUsage.sevenDayUtil == null)
|
|
990
|
+
return;
|
|
991
|
+
writeUsageCache({ headers: { ...cachedUsage } });
|
|
992
|
+
}
|
|
993
|
+
var API_TTL_MS = 5 * 60000;
|
|
994
|
+
|
|
1081
995
|
// src/cch.ts
|
|
1082
996
|
import xxhashInit from "xxhash-wasm";
|
|
1083
997
|
var CCH_SEED = 0x6e52736ac806831en;
|
|
@@ -1230,7 +1144,7 @@ function createAnthropicSDK(options = {}) {
|
|
|
1230
1144
|
cachedUsage.fiveHourReset = parseInt(resp.headers.get("anthropic-ratelimit-unified-5h-reset") ?? "0");
|
|
1231
1145
|
cachedUsage.sevenDayReset = parseInt(resp.headers.get("anthropic-ratelimit-unified-7d-reset") ?? "0");
|
|
1232
1146
|
cachedUsage.overageStatus = resp.headers.get("anthropic-ratelimit-unified-overage-status") ?? undefined;
|
|
1233
|
-
|
|
1147
|
+
persistCachedUsage();
|
|
1234
1148
|
}
|
|
1235
1149
|
if (resp.status === 429) {
|
|
1236
1150
|
const unifiedStatus = resp.headers.get("anthropic-ratelimit-unified-status") ?? "";
|
package/build/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAI1B,6BAA6B,EAC7B,2BAA2B,EAE5B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAI1B,6BAA6B,EAC7B,2BAA2B,EAE5B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAQ9C,KAAK,gBAAgB,GAAG,6BAA6B,CAAA;AACrD,KAAK,cAAc,GAAG,2BAA2B,CAAA;AAkGjD,qBAAa,iBAAkB,YAAW,eAAe;IAQrD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IATjB,QAAQ,CAAC,oBAAoB,EAAG,IAAI,CAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAK;gBAGnD,OAAO,EAAE,MAAM,EACP,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAe;IAMlC,OAAO,CAAC,WAAW;IA2Lb,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsF1E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;CA4E7E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side plugin entry point (V1 plugin format).
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the server plugin in the format expected by
|
|
5
|
+
* OpenCode's V1 plugin loader: `default export { id, server }`.
|
|
6
|
+
*
|
|
7
|
+
* The `./server` export in package.json points here.
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginModule } from "@opencode-ai/plugin";
|
|
10
|
+
declare const plugin: PluginModule & {
|
|
11
|
+
id: string;
|
|
12
|
+
};
|
|
13
|
+
export default plugin;
|
|
14
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAO/D,QAAA,MAAM,MAAM,EAAE,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAGxC,CAAA;AAED,eAAe,MAAM,CAAA"}
|