opencode-anthropic-fix 0.0.23 → 0.0.25
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 +3 -2
- package/index.mjs +34 -55
- package/lib/oauth.mjs +78 -19
- package/lib/oauth.test.mjs +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@ The [original plugin](https://github.com/anomalyco/opencode-anthropic-auth) prov
|
|
|
43
43
|
- **Standalone CLI** — manage accounts without opening OpenCode
|
|
44
44
|
- **Configurable strategies** — sticky, round-robin, or hybrid account selection
|
|
45
45
|
- **Claude Code signature emulation** — full HTTP header, system prompt, beta flag, and metadata mimicry derived from Claude Code's open source code
|
|
46
|
+
- **OAuth endpoint fingerprint parity** — sends Claude Code-style `User-Agent` on `/v1/oauth/token` and `/v1/oauth/revoke` to match current OAuth validation
|
|
46
47
|
- **Effort-based thinking for Opus 4.6** — maps `budgetTokens` to effort levels (`low`/`medium`/`high`) and includes `effort-2025-11-24`
|
|
48
|
+
- **Upstream-aligned auto betas** — `advanced-tool-use-2025-11-20` and `fast-mode-2026-02-01` auto-included to match Claude Code 2.1.79+ (`redact-thinking-2026-02-12` available as opt-in to preserve thinking block visibility)
|
|
47
49
|
- **1M context limit override** — patches `model.limit.context` so OpenCode compacts at the right threshold while `models.dev` catches up
|
|
48
50
|
- **Runtime config + custom betas** — `/anthropic set`, `/anthropic config`, and `/anthropic betas` slash commands for live feature toggling without restarting OpenCode
|
|
49
51
|
- **Files API integration** — upload, list, download, and manage files via `/anthropic files` with endpoint/content-scoped `files-api-2025-04-14` beta injection
|
|
@@ -272,7 +274,6 @@ Add or remove beta flags that get included in every `anthropic-beta` header. Per
|
|
|
272
274
|
/anthropic betas add <beta-name> # add a custom beta
|
|
273
275
|
/anthropic betas remove <beta-name> # remove a custom beta
|
|
274
276
|
/anthropic betas add 1m # shortcut => context-1m-2025-08-07
|
|
275
|
-
/anthropic betas add fast # shortcut => fast-mode-2026-02-01
|
|
276
277
|
```
|
|
277
278
|
|
|
278
279
|
**Available preset betas** (shown by `/anthropic betas`):
|
|
@@ -287,7 +288,7 @@ Add or remove beta flags that get included in every `anthropic-beta` header. Per
|
|
|
287
288
|
| `mcp-servers-2025-12-04` | MCP servers in API request payload |
|
|
288
289
|
| `web-search-2025-03-05` | Web search (Vertex/Foundry only) |
|
|
289
290
|
| `context-1m-2025-08-07` | 1M context beta (provider/model dependent) |
|
|
290
|
-
| `
|
|
291
|
+
| `redact-thinking-2026-02-12` | Hide thinking blocks from responses (opt-in) |
|
|
291
292
|
|
|
292
293
|
Example workflow:
|
|
293
294
|
|
package/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
|
-
import {
|
|
3
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, resolve, basename } from "node:path";
|
|
6
6
|
import { AccountManager } from "./lib/accounts.mjs";
|
|
@@ -103,13 +103,14 @@ async function promptManageAccounts(accountManager) {
|
|
|
103
103
|
// Request building helpers (extracted from original fetch interceptor)
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
105
|
|
|
106
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
106
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.79";
|
|
107
107
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
108
108
|
const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
|
|
109
109
|
const EFFORT_BETA_FLAG = "effort-2025-11-24";
|
|
110
|
+
const REDACT_THINKING_BETA_FLAG = "redact-thinking-2026-02-12";
|
|
111
|
+
const ADVANCED_TOOL_USE_BETA_FLAG = "advanced-tool-use-2025-11-20";
|
|
112
|
+
const FAST_MODE_BETA_FLAG = "fast-mode-2026-02-01";
|
|
110
113
|
const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
|
|
111
|
-
const BILLING_HASH_SALT = "59cf53e54c78";
|
|
112
|
-
const BILLING_HASH_POSITIONS = [4, 7, 20];
|
|
113
114
|
const CLAUDE_CODE_IDENTITY_STRING = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
114
115
|
const KNOWN_IDENTITY_STRINGS = new Set([
|
|
115
116
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
@@ -429,39 +430,6 @@ function buildStainlessHelperHeader(tools, messages) {
|
|
|
429
430
|
return Array.from(helpers).join(", ");
|
|
430
431
|
}
|
|
431
432
|
|
|
432
|
-
/**
|
|
433
|
-
* @param {any} content
|
|
434
|
-
* @returns {string}
|
|
435
|
-
*/
|
|
436
|
-
function extractTextContent(content) {
|
|
437
|
-
if (typeof content === "string") return content;
|
|
438
|
-
if (!Array.isArray(content)) return "";
|
|
439
|
-
|
|
440
|
-
const parts = [];
|
|
441
|
-
for (const block of content) {
|
|
442
|
-
if (!block || typeof block !== "object") continue;
|
|
443
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
444
|
-
parts.push(block.text);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return parts.join("\n");
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* @param {any[]} messages
|
|
452
|
-
* @returns {string}
|
|
453
|
-
*/
|
|
454
|
-
function getFirstUserText(messages) {
|
|
455
|
-
for (const message of messages) {
|
|
456
|
-
if (!message || typeof message !== "object") continue;
|
|
457
|
-
if (message.role !== "user") continue;
|
|
458
|
-
|
|
459
|
-
const text = extractTextContent(message.content);
|
|
460
|
-
if (text) return text;
|
|
461
|
-
}
|
|
462
|
-
return "";
|
|
463
|
-
}
|
|
464
|
-
|
|
465
433
|
/**
|
|
466
434
|
* @param {{id?: string, accountUuid?: string} | null | undefined} account
|
|
467
435
|
* @returns {string}
|
|
@@ -503,18 +471,12 @@ function buildRequestMetadata(input) {
|
|
|
503
471
|
* @param {any[]} messages
|
|
504
472
|
* @returns {string}
|
|
505
473
|
*/
|
|
506
|
-
function buildAnthropicBillingHeader(claudeCliVersion
|
|
474
|
+
function buildAnthropicBillingHeader(claudeCliVersion) {
|
|
507
475
|
if (isFalsyEnv(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) return "";
|
|
508
476
|
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
.update(`${BILLING_HASH_SALT}${sampled}${claudeCliVersion}`)
|
|
513
|
-
.digest("hex")
|
|
514
|
-
.slice(0, 3);
|
|
515
|
-
|
|
516
|
-
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "unknown";
|
|
517
|
-
return `x-anthropic-billing-header: cc_version=${claudeCliVersion}.${hash}; cc_entrypoint=${entrypoint}; cch=00000;`;
|
|
477
|
+
const cch = randomBytes(3).toString("hex").slice(0, 5);
|
|
478
|
+
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
|
|
479
|
+
return `x-anthropic-billing-header: cc_version=${claudeCliVersion}; cc_entrypoint=${entrypoint}; cch=${cch};`;
|
|
518
480
|
}
|
|
519
481
|
|
|
520
482
|
/**
|
|
@@ -659,10 +621,9 @@ function normalizeSystemTextBlocks(system) {
|
|
|
659
621
|
/**
|
|
660
622
|
* @param {Array<{type: string, text: string, cache_control?: {type: string}}>} system
|
|
661
623
|
* @param {{enabled: boolean, claudeCliVersion: string, promptCompactionMode: 'minimal' | 'off'}} signature
|
|
662
|
-
* @param {any[]} messages
|
|
663
624
|
* @returns {Array<{type: string, text: string, cache_control?: {type: string}}>}
|
|
664
625
|
*/
|
|
665
|
-
function buildSystemPromptBlocks(system, signature
|
|
626
|
+
function buildSystemPromptBlocks(system, signature) {
|
|
666
627
|
const titleGeneratorRequest = isTitleGeneratorSystemBlocks(system);
|
|
667
628
|
|
|
668
629
|
let sanitized = system.map((item) => ({
|
|
@@ -685,7 +646,7 @@ function buildSystemPromptBlocks(system, signature, messages) {
|
|
|
685
646
|
);
|
|
686
647
|
|
|
687
648
|
const blocks = [];
|
|
688
|
-
const billingHeader = buildAnthropicBillingHeader(signature.claudeCliVersion
|
|
649
|
+
const billingHeader = buildAnthropicBillingHeader(signature.claudeCliVersion);
|
|
689
650
|
if (billingHeader) {
|
|
690
651
|
blocks.push({ type: "text", text: billingHeader });
|
|
691
652
|
}
|
|
@@ -752,6 +713,20 @@ function buildAnthropicBetaHeader(
|
|
|
752
713
|
betas.push("files-api-2025-04-14");
|
|
753
714
|
}
|
|
754
715
|
|
|
716
|
+
// NOTE: redact-thinking-2026-02-12 is in upstream 2.1.79+ base profile but
|
|
717
|
+
// intentionally NOT auto-included here — OpenCode users benefit from seeing
|
|
718
|
+
// thinking blocks. Available via /anthropic betas add redact-thinking-2026-02-12.
|
|
719
|
+
|
|
720
|
+
// Advanced tool use improvements — in upstream 2.1.79+ base profile.
|
|
721
|
+
if (!disableExperimentalBetas) {
|
|
722
|
+
betas.push(ADVANCED_TOOL_USE_BETA_FLAG);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Fast mode — in upstream 2.1.79+ base profile.
|
|
726
|
+
if (!disableExperimentalBetas) {
|
|
727
|
+
betas.push(FAST_MODE_BETA_FLAG);
|
|
728
|
+
}
|
|
729
|
+
|
|
755
730
|
if (isOpus46Model(model)) {
|
|
756
731
|
// Opus 4.6 uses effort-based thinking controls.
|
|
757
732
|
betas.push(EFFORT_BETA_FLAG);
|
|
@@ -1049,15 +1024,13 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
1049
1024
|
if (Object.prototype.hasOwnProperty.call(parsed, "betas")) {
|
|
1050
1025
|
delete parsed.betas;
|
|
1051
1026
|
}
|
|
1052
|
-
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
1053
|
-
|
|
1054
1027
|
// Normalize thinking block for adaptive (Opus 4.6) vs manual (older models).
|
|
1055
1028
|
if (Object.prototype.hasOwnProperty.call(parsed, "thinking")) {
|
|
1056
1029
|
parsed.thinking = normalizeThinkingBlock(parsed.thinking, parsed.model || "");
|
|
1057
1030
|
}
|
|
1058
1031
|
|
|
1059
1032
|
// Sanitize system prompt and optionally inject Claude Code identity/billing blocks.
|
|
1060
|
-
parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signature
|
|
1033
|
+
parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signature);
|
|
1061
1034
|
|
|
1062
1035
|
if (signature.enabled) {
|
|
1063
1036
|
const currentMetadata =
|
|
@@ -1866,7 +1839,12 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1866
1839
|
|
|
1867
1840
|
const credentials = await exchange(code, pending.verifier);
|
|
1868
1841
|
if (credentials.type === "failed") {
|
|
1869
|
-
return {
|
|
1842
|
+
return {
|
|
1843
|
+
ok: false,
|
|
1844
|
+
message: credentials.details
|
|
1845
|
+
? `Token exchange failed (${credentials.details}).`
|
|
1846
|
+
: "Token exchange failed. The code may be invalid or expired.",
|
|
1847
|
+
};
|
|
1870
1848
|
}
|
|
1871
1849
|
|
|
1872
1850
|
const stored = (await loadAccounts()) || { version: 1, accounts: [], activeIndex: 0 };
|
|
@@ -2109,6 +2087,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2109
2087
|
"",
|
|
2110
2088
|
"Preset betas (auto-computed per model/provider):",
|
|
2111
2089
|
" oauth-2025-04-20, claude-code-20250219,",
|
|
2090
|
+
" advanced-tool-use-2025-11-20, fast-mode-2026-02-01,",
|
|
2112
2091
|
" interleaved-thinking-2025-05-14 (non-Opus 4.6) OR effort-2025-11-24 (Opus 4.6),",
|
|
2113
2092
|
" files-api-2025-04-14 (only /v1/files and requests with file_id),",
|
|
2114
2093
|
" token-counting-2024-11-01 (only /v1/messages/count_tokens),",
|
|
@@ -2125,8 +2104,8 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2125
2104
|
" /anthropic betas add web-search-2025-03-05",
|
|
2126
2105
|
" /anthropic betas add compact-2026-01-12",
|
|
2127
2106
|
" /anthropic betas add mcp-servers-2025-12-04",
|
|
2107
|
+
" /anthropic betas add redact-thinking-2026-02-12",
|
|
2128
2108
|
" /anthropic betas add 1m (shortcut for context-1m-2025-08-07)",
|
|
2129
|
-
" /anthropic betas add fast (shortcut for fast-mode-2026-02-01)",
|
|
2130
2109
|
"",
|
|
2131
2110
|
"Remove: /anthropic betas remove <beta>",
|
|
2132
2111
|
];
|
package/lib/oauth.mjs
CHANGED
|
@@ -10,6 +10,8 @@ const OAUTH_MAX_HOST = "claude.ai";
|
|
|
10
10
|
const OAUTH_REDIRECT_URI = `https://${OAUTH_CONSOLE_HOST}/oauth/code/callback`;
|
|
11
11
|
const OAUTH_TOKEN_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/token`;
|
|
12
12
|
const OAUTH_REVOKE_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/revoke`;
|
|
13
|
+
const OAUTH_CLI_VERSION_PROFILE = "2.1.79";
|
|
14
|
+
const OAUTH_USER_AGENT = `claude-cli/${OAUTH_CLI_VERSION_PROFILE} (external, cli)`;
|
|
13
15
|
|
|
14
16
|
function base64url(input) {
|
|
15
17
|
return Buffer.from(input).toString("base64url");
|
|
@@ -48,28 +50,79 @@ export async function authorize(mode) {
|
|
|
48
50
|
* Exchange an authorization code for tokens.
|
|
49
51
|
* @param {string} code
|
|
50
52
|
* @param {string} verifier
|
|
51
|
-
* @returns {Promise<
|
|
53
|
+
* @returns {Promise<
|
|
54
|
+
* | { type: "success", refresh: string, access: string, expires: number, email?: string }
|
|
55
|
+
* | { type: "failed", status?: number, code?: string, reason?: string, details?: string }
|
|
56
|
+
* >}
|
|
52
57
|
*/
|
|
53
58
|
export async function exchange(code, verifier) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
const fail = (status, rawText = "") => {
|
|
60
|
+
let errorCode;
|
|
61
|
+
let reason;
|
|
62
|
+
|
|
63
|
+
if (rawText) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(rawText);
|
|
66
|
+
if (typeof parsed.error === "string" && parsed.error) {
|
|
67
|
+
errorCode = parsed.error;
|
|
68
|
+
}
|
|
69
|
+
if (typeof parsed.error_description === "string" && parsed.error_description) {
|
|
70
|
+
reason = parsed.error_description;
|
|
71
|
+
} else if (typeof parsed.message === "string" && parsed.message) {
|
|
72
|
+
reason = parsed.message;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
reason = rawText.trim() || undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const detailsParts = [];
|
|
80
|
+
if (typeof status === "number") detailsParts.push(`HTTP ${status}`);
|
|
81
|
+
if (errorCode) detailsParts.push(errorCode);
|
|
82
|
+
if (reason) detailsParts.push(reason);
|
|
83
|
+
|
|
70
84
|
return {
|
|
71
85
|
type: "failed",
|
|
86
|
+
...(typeof status === "number" ? { status } : {}),
|
|
87
|
+
...(errorCode ? { code: errorCode } : {}),
|
|
88
|
+
...(reason ? { reason } : {}),
|
|
89
|
+
...(detailsParts.length ? { details: detailsParts.join(" · ") } : {}),
|
|
72
90
|
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const splits = code.split("#");
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = await fetch(OAUTH_TOKEN_URL, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
code: splits[0],
|
|
104
|
+
state: splits[1],
|
|
105
|
+
grant_type: "authorization_code",
|
|
106
|
+
client_id: CLIENT_ID,
|
|
107
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
108
|
+
code_verifier: verifier,
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return fail(undefined, err instanceof Error ? err.message : String(err));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
const raw =
|
|
117
|
+
typeof result.text === "function"
|
|
118
|
+
? await result
|
|
119
|
+
.text()
|
|
120
|
+
.then((value) => (typeof value === "string" ? value : ""))
|
|
121
|
+
.catch(() => "")
|
|
122
|
+
: "";
|
|
123
|
+
return fail(result.status, raw);
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
const json = await result.json();
|
|
74
127
|
return {
|
|
75
128
|
type: "success",
|
|
@@ -94,7 +147,10 @@ export async function revoke(refreshToken) {
|
|
|
94
147
|
try {
|
|
95
148
|
const resp = await fetch(OAUTH_REVOKE_URL, {
|
|
96
149
|
method: "POST",
|
|
97
|
-
headers: {
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
153
|
+
},
|
|
98
154
|
body: JSON.stringify({
|
|
99
155
|
token: refreshToken,
|
|
100
156
|
token_type_hint: "refresh_token",
|
|
@@ -118,7 +174,10 @@ export async function revoke(refreshToken) {
|
|
|
118
174
|
export async function refreshToken(refreshTokenValue, options = {}) {
|
|
119
175
|
const resp = await fetch(OAUTH_TOKEN_URL, {
|
|
120
176
|
method: "POST",
|
|
121
|
-
headers: {
|
|
177
|
+
headers: {
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
180
|
+
},
|
|
122
181
|
body: JSON.stringify({
|
|
123
182
|
grant_type: "refresh_token",
|
|
124
183
|
client_id: CLIENT_ID,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { exchange, refreshToken, revoke } from "./oauth.mjs";
|
|
3
|
+
|
|
4
|
+
const mockFetch = vi.fn();
|
|
5
|
+
|
|
6
|
+
describe("oauth headers", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.unstubAllGlobals();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("sends Claude Code user-agent on token exchange", async () => {
|
|
17
|
+
mockFetch.mockResolvedValueOnce({
|
|
18
|
+
ok: false,
|
|
19
|
+
status: 400,
|
|
20
|
+
text: async () => "",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await exchange("code#state", "verifier");
|
|
24
|
+
|
|
25
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
26
|
+
expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("sends Claude Code user-agent on token refresh", async () => {
|
|
30
|
+
mockFetch.mockResolvedValueOnce({
|
|
31
|
+
ok: true,
|
|
32
|
+
json: async () => ({ access_token: "a", refresh_token: "r", expires_in: 3600 }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await refreshToken("refresh-token");
|
|
36
|
+
|
|
37
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
38
|
+
expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("sends Claude Code user-agent on token revoke", async () => {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
43
|
+
|
|
44
|
+
await revoke("refresh-token");
|
|
45
|
+
|
|
46
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
47
|
+
expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
|
|
48
|
+
});
|
|
49
|
+
});
|