@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9

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.
Files changed (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
package/src/constants.ts CHANGED
@@ -14,58 +14,58 @@ export const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
14
14
  export const CLAUDE_CODE_IDENTITY_STRING = "You are Claude Code, Anthropic's official CLI for Claude.";
15
15
 
16
16
  export const KNOWN_IDENTITY_STRINGS = new Set([
17
- CLAUDE_CODE_IDENTITY_STRING,
18
- "You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.",
19
- "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
17
+ CLAUDE_CODE_IDENTITY_STRING,
18
+ "You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.",
19
+ "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
20
20
  ]);
21
21
 
22
22
  export const BEDROCK_UNSUPPORTED_BETAS = new Set([
23
- "interleaved-thinking-2025-05-14",
24
- "context-1m-2025-08-07",
25
- "tool-search-tool-2025-10-19",
23
+ "interleaved-thinking-2025-05-14",
24
+ "context-1m-2025-08-07",
25
+ "tool-search-tool-2025-10-19",
26
26
  ]);
27
27
 
28
28
  export const EXPERIMENTAL_BETA_FLAGS = new Set([
29
- "adaptive-thinking-2026-01-28",
30
- "advanced-tool-use-2025-11-20",
31
- "advisor-tool-2026-03-01",
32
- "afk-mode-2026-01-31",
33
- "ccr-byoc-2025-07-29",
34
- "ccr-triggers-2026-01-30",
35
- "code-execution-2025-08-25",
36
- "context-1m-2025-08-07",
37
- "context-management-2025-06-27",
38
- "environments-2025-11-01",
39
- "fast-mode-2026-02-01",
40
- "files-api-2025-04-14",
41
- "fine-grained-tool-streaming-2025-05-14",
42
- "interleaved-thinking-2025-05-14",
43
- "mcp-client-2025-11-20",
44
- "prompt-caching-scope-2026-01-05",
45
- "redact-thinking-2026-02-12",
46
- "skills-2025-10-02",
47
- "structured-outputs-2025-12-15",
48
- "task-budgets-2026-03-13",
49
- "token-efficient-tools-2026-03-28",
50
- "tool-search-tool-2025-10-19",
51
- "web-search-2025-03-05",
29
+ "adaptive-thinking-2026-01-28",
30
+ "advanced-tool-use-2025-11-20",
31
+ "advisor-tool-2026-03-01",
32
+ "afk-mode-2026-01-31",
33
+ "ccr-byoc-2025-07-29",
34
+ "ccr-triggers-2026-01-30",
35
+ "code-execution-2025-08-25",
36
+ "context-1m-2025-08-07",
37
+ "context-management-2025-06-27",
38
+ "environments-2025-11-01",
39
+ "fast-mode-2026-02-01",
40
+ "files-api-2025-04-14",
41
+ "fine-grained-tool-streaming-2025-05-14",
42
+ "interleaved-thinking-2025-05-14",
43
+ "mcp-client-2025-11-20",
44
+ "prompt-caching-scope-2026-01-05",
45
+ "redact-thinking-2026-02-12",
46
+ "skills-2025-10-02",
47
+ "structured-outputs-2025-12-15",
48
+ "task-budgets-2026-03-13",
49
+ "token-efficient-tools-2026-03-28",
50
+ "tool-search-tool-2025-10-19",
51
+ "web-search-2025-03-05",
52
52
  ]);
53
53
 
54
54
  export const BETA_SHORTCUTS = new Map<string, string>([
55
- ["1m", "context-1m-2025-08-07"],
56
- ["1m-context", "context-1m-2025-08-07"],
57
- ["context-1m", "context-1m-2025-08-07"],
58
- ["fast", "fast-mode-2026-02-01"],
59
- ["fast-mode", "fast-mode-2026-02-01"],
60
- ["opus-fast", "fast-mode-2026-02-01"],
55
+ ["1m", "context-1m-2025-08-07"],
56
+ ["1m-context", "context-1m-2025-08-07"],
57
+ ["context-1m", "context-1m-2025-08-07"],
58
+ ["fast", "fast-mode-2026-02-01"],
59
+ ["fast-mode", "fast-mode-2026-02-01"],
60
+ ["opus-fast", "fast-mode-2026-02-01"],
61
61
  ]);
62
62
 
63
63
  export const STAINLESS_HELPER_KEYS = [
64
- "x_stainless_helper",
65
- "x-stainless-helper",
66
- "stainless_helper",
67
- "stainlessHelper",
68
- "_stainless_helper",
64
+ "x_stainless_helper",
65
+ "x-stainless-helper",
66
+ "stainless_helper",
67
+ "stainlessHelper",
68
+ "_stainless_helper",
69
69
  ] as const;
70
70
 
71
71
  export const USER_ID_STORAGE_FILE = "anthropic-signature-user-id";
@@ -75,12 +75,12 @@ export const PENDING_OAUTH_TTL_MS = 10 * 60 * 1000;
75
75
  export const FOREGROUND_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
76
76
 
77
77
  export const COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT = [
78
- "You are a title generator. You output ONLY a thread title. Nothing else.",
79
- "",
80
- "Rules:",
81
- "- Use the same language as the user message.",
82
- "- Output exactly one line.",
83
- "- Keep the title at or below 50 characters.",
84
- "- No explanations, prefixes, or suffixes.",
85
- "- Keep important technical terms, numbers, and filenames when present.",
78
+ "You are a title generator. You output ONLY a thread title. Nothing else.",
79
+ "",
80
+ "Rules:",
81
+ "- Use the same language as the user message.",
82
+ "- Output exactly one line.",
83
+ "- Keep the title at or below 50 characters.",
84
+ "- No explanations, prefixes, or suffixes.",
85
+ "- Keep important technical terms, numbers, and filenames when present.",
86
86
  ].join("\n");
@@ -3,131 +3,131 @@ export const EXPECTED_CCH_SALT = "59cf53e54c78";
3
3
  export const EXPECTED_CCH_SEED = 0x6e52_736a_c806_831en;
4
4
 
5
5
  export const EXPECTED_XXHASH64_PRIMES = [
6
- 0x9e37_79b1_85eb_ca87n,
7
- 0xc2b2_ae3d_27d4_eb4fn,
8
- 0x1656_67b1_9e37_79f9n,
9
- 0x85eb_ca77_c2b2_ae63n,
10
- 0x27d4_eb2f_1656_67c5n,
6
+ 0x9e37_79b1_85eb_ca87n,
7
+ 0xc2b2_ae3d_27d4_eb4fn,
8
+ 0x1656_67b1_9e37_79f9n,
9
+ 0x85eb_ca77_c2b2_ae63n,
10
+ 0x27d4_eb2f_1656_67c5n,
11
11
  ] as const;
12
12
 
13
13
  export type DriftSeverity = "critical" | "warning";
14
14
 
15
15
  export interface DriftFinding {
16
- name: string;
17
- severity: DriftSeverity;
18
- expected: string;
19
- actual: string;
20
- count: number;
16
+ name: string;
17
+ severity: DriftSeverity;
18
+ expected: string;
19
+ actual: string;
20
+ count: number;
21
21
  }
22
22
 
23
23
  export interface DriftScanReport {
24
- target: string;
25
- mode: "standalone" | "bundle";
26
- findings: DriftFinding[];
27
- checked: {
28
- placeholder: number;
29
- salt: number;
30
- seed: number;
31
- primes: number[];
32
- };
33
- passed: boolean;
24
+ target: string;
25
+ mode: "standalone" | "bundle";
26
+ findings: DriftFinding[];
27
+ checked: {
28
+ placeholder: number;
29
+ salt: number;
30
+ seed: number;
31
+ primes: number[];
32
+ };
33
+ passed: boolean;
34
34
  }
35
35
 
36
36
  function encodeAscii(value: string): Uint8Array {
37
- return new TextEncoder().encode(value);
37
+ return new TextEncoder().encode(value);
38
38
  }
39
39
 
40
40
  export function bigintToLittleEndianBytes(value: bigint): Uint8Array {
41
- const bytes = new Uint8Array(8);
42
- let remaining = value;
43
- for (let index = 0; index < bytes.length; index += 1) {
44
- bytes[index] = Number(remaining & 0xffn);
45
- remaining >>= 8n;
46
- }
47
- return bytes;
41
+ const bytes = new Uint8Array(8);
42
+ let remaining = value;
43
+ for (let index = 0; index < bytes.length; index += 1) {
44
+ bytes[index] = Number(remaining & 0xffn);
45
+ remaining >>= 8n;
46
+ }
47
+ return bytes;
48
48
  }
49
49
 
50
50
  export function findAllOccurrences(haystack: Uint8Array, needle: Uint8Array): number[] {
51
- if (needle.length === 0 || haystack.length < needle.length) {
52
- return [];
53
- }
51
+ if (needle.length === 0 || haystack.length < needle.length) {
52
+ return [];
53
+ }
54
54
 
55
- const matches: number[] = [];
56
- outer: for (let start = 0; start <= haystack.length - needle.length; start += 1) {
57
- for (let offset = 0; offset < needle.length; offset += 1) {
58
- if (haystack[start + offset] !== needle[offset]) {
59
- continue outer;
60
- }
55
+ const matches: number[] = [];
56
+ outer: for (let start = 0; start <= haystack.length - needle.length; start += 1) {
57
+ for (let offset = 0; offset < needle.length; offset += 1) {
58
+ if (haystack[start + offset] !== needle[offset]) {
59
+ continue outer;
60
+ }
61
+ }
62
+ matches.push(start);
61
63
  }
62
- matches.push(start);
63
- }
64
- return matches;
64
+ return matches;
65
65
  }
66
66
 
67
67
  function addFinding(
68
- findings: DriftFinding[],
69
- count: number,
70
- name: string,
71
- severity: DriftSeverity,
72
- expected: string,
73
- actual: string,
68
+ findings: DriftFinding[],
69
+ count: number,
70
+ name: string,
71
+ severity: DriftSeverity,
72
+ expected: string,
73
+ actual: string,
74
74
  ): void {
75
- if (count > 0) {
76
- return;
77
- }
78
- findings.push({ name, severity, expected, actual, count });
75
+ if (count > 0) {
76
+ return;
77
+ }
78
+ findings.push({ name, severity, expected, actual, count });
79
79
  }
80
80
 
81
81
  export function scanCchConstants(bytes: Uint8Array, target: string, mode: "standalone" | "bundle"): DriftScanReport {
82
- const placeholderMatches = findAllOccurrences(bytes, encodeAscii(`cch=${EXPECTED_CCH_PLACEHOLDER}`));
83
- const saltMatches = findAllOccurrences(bytes, encodeAscii(EXPECTED_CCH_SALT));
84
- const seedMatches = findAllOccurrences(bytes, bigintToLittleEndianBytes(EXPECTED_CCH_SEED));
85
- const primeMatches = EXPECTED_XXHASH64_PRIMES.map(
86
- (prime) => findAllOccurrences(bytes, bigintToLittleEndianBytes(prime)).length,
87
- );
88
-
89
- const findings: DriftFinding[] = [];
90
- addFinding(
91
- findings,
92
- placeholderMatches.length,
93
- "cch placeholder",
94
- "critical",
95
- `cch=${EXPECTED_CCH_PLACEHOLDER}`,
96
- "not found",
97
- );
98
- addFinding(findings, saltMatches.length, "cc_version salt", "critical", EXPECTED_CCH_SALT, "not found");
82
+ const placeholderMatches = findAllOccurrences(bytes, encodeAscii(`cch=${EXPECTED_CCH_PLACEHOLDER}`));
83
+ const saltMatches = findAllOccurrences(bytes, encodeAscii(EXPECTED_CCH_SALT));
84
+ const seedMatches = findAllOccurrences(bytes, bigintToLittleEndianBytes(EXPECTED_CCH_SEED));
85
+ const primeMatches = EXPECTED_XXHASH64_PRIMES.map(
86
+ (prime) => findAllOccurrences(bytes, bigintToLittleEndianBytes(prime)).length,
87
+ );
99
88
 
100
- if (mode === "standalone") {
89
+ const findings: DriftFinding[] = [];
101
90
  addFinding(
102
- findings,
103
- seedMatches.length,
104
- "native cch seed",
105
- "critical",
106
- `0x${EXPECTED_CCH_SEED.toString(16)}`,
107
- "not found",
108
- );
109
- for (const [index, count] of primeMatches.entries()) {
110
- addFinding(
111
91
  findings,
112
- count,
113
- `xxHash64 prime ${index + 1}`,
114
- "warning",
115
- `0x${EXPECTED_XXHASH64_PRIMES[index].toString(16)}`,
92
+ placeholderMatches.length,
93
+ "cch placeholder",
94
+ "critical",
95
+ `cch=${EXPECTED_CCH_PLACEHOLDER}`,
116
96
  "not found",
117
- );
97
+ );
98
+ addFinding(findings, saltMatches.length, "cc_version salt", "critical", EXPECTED_CCH_SALT, "not found");
99
+
100
+ if (mode === "standalone") {
101
+ addFinding(
102
+ findings,
103
+ seedMatches.length,
104
+ "native cch seed",
105
+ "critical",
106
+ `0x${EXPECTED_CCH_SEED.toString(16)}`,
107
+ "not found",
108
+ );
109
+ for (const [index, count] of primeMatches.entries()) {
110
+ addFinding(
111
+ findings,
112
+ count,
113
+ `xxHash64 prime ${index + 1}`,
114
+ "warning",
115
+ `0x${EXPECTED_XXHASH64_PRIMES[index].toString(16)}`,
116
+ "not found",
117
+ );
118
+ }
118
119
  }
119
- }
120
120
 
121
- return {
122
- target,
123
- mode,
124
- findings,
125
- checked: {
126
- placeholder: placeholderMatches.length,
127
- salt: saltMatches.length,
128
- seed: seedMatches.length,
129
- primes: primeMatches,
130
- },
131
- passed: findings.length === 0,
132
- };
121
+ return {
122
+ target,
123
+ mode,
124
+ findings,
125
+ checked: {
126
+ placeholder: placeholderMatches.length,
127
+ salt: saltMatches.length,
128
+ seed: seedMatches.length,
129
+ primes: primeMatches,
130
+ },
131
+ passed: findings.length === 0,
132
+ };
133
133
  }
package/src/env.ts CHANGED
@@ -9,138 +9,144 @@ import { getConfigDir } from "./config.js";
9
9
  import { BETA_SHORTCUTS, DEBUG_SYSTEM_PROMPT_ENV, USER_ID_STORAGE_FILE } from "./constants.ts";
10
10
 
11
11
  export function isTruthyEnv(value: string | undefined): boolean {
12
- if (!value) return false;
13
- const normalized = value.trim().toLowerCase();
14
- return normalized === "1" || normalized === "true" || normalized === "yes";
12
+ if (!value) return false;
13
+ const normalized = value.trim().toLowerCase();
14
+ return normalized === "1" || normalized === "true" || normalized === "yes";
15
15
  }
16
16
 
17
17
  export function isFalsyEnv(value: string | undefined): boolean {
18
- if (!value) return false;
19
- const normalized = value.trim().toLowerCase();
20
- return normalized === "0" || normalized === "false" || normalized === "no";
18
+ if (!value) return false;
19
+ const normalized = value.trim().toLowerCase();
20
+ return normalized === "0" || normalized === "false" || normalized === "no";
21
21
  }
22
22
 
23
23
  export function isNonInteractiveMode(): boolean {
24
- if (isTruthyEnv(process.env.CI)) return true;
25
- return !process.stdout.isTTY;
24
+ if (isTruthyEnv(process.env.CI)) return true;
25
+ return !process.stdout.isTTY;
26
26
  }
27
27
 
28
28
  export function getClaudeEntrypoint(): string {
29
- return process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
29
+ return process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
30
30
  }
31
31
 
32
32
  export function resolveBetaShortcut(value: string | undefined): string {
33
- if (!value) return "";
34
- const trimmed = value.trim();
35
- const mapped = BETA_SHORTCUTS.get(trimmed.toLowerCase());
36
- return mapped || trimmed;
33
+ if (!value) return "";
34
+ const trimmed = value.trim();
35
+ const mapped = BETA_SHORTCUTS.get(trimmed.toLowerCase());
36
+ return mapped || trimmed;
37
37
  }
38
38
 
39
39
  export function parseAnthropicCustomHeaders(): Record<string, string> {
40
- const raw = process.env.ANTHROPIC_CUSTOM_HEADERS;
41
- if (!raw) return {};
42
-
43
- const headers: Record<string, string> = {};
44
- for (const line of raw.split(/\r?\n/)) {
45
- const trimmed = line.trim();
46
- if (!trimmed) continue;
47
- const sep = trimmed.indexOf(":");
48
- if (sep <= 0) continue;
49
- const key = trimmed.slice(0, sep).trim();
50
- const value = trimmed.slice(sep + 1).trim();
51
- if (!key || !value) continue;
52
- headers[key] = value;
53
- }
54
-
55
- return headers;
40
+ const raw = process.env.ANTHROPIC_CUSTOM_HEADERS;
41
+ if (!raw) return {};
42
+
43
+ const headers: Record<string, string> = {};
44
+ for (const line of raw.split(/\r?\n/)) {
45
+ const trimmed = line.trim();
46
+ if (!trimmed) continue;
47
+ const sep = trimmed.indexOf(":");
48
+ if (sep <= 0) continue;
49
+ const key = trimmed.slice(0, sep).trim();
50
+ const value = trimmed.slice(sep + 1).trim();
51
+ if (!key || !value) continue;
52
+ headers[key] = value;
53
+ }
54
+
55
+ return headers;
56
56
  }
57
57
 
58
58
  export function getOrCreateSignatureUserId(): string {
59
- const envUserId = process.env.OPENCODE_ANTHROPIC_SIGNATURE_USER_ID?.trim();
60
- if (envUserId) return envUserId;
61
-
62
- const configDir = getConfigDir();
63
- const userIdPath = join(configDir, USER_ID_STORAGE_FILE);
64
-
65
- try {
66
- if (existsSync(userIdPath)) {
67
- const existing = readFileSync(userIdPath, "utf-8").trim();
68
- // CC uses 64-char hex (32 random bytes). Accept existing hex IDs;
69
- // regenerate if we find an old UUID-format ID.
70
- if (existing && /^[0-9a-f]{64}$/.test(existing)) return existing;
59
+ const envUserId = process.env.OPENCODE_ANTHROPIC_SIGNATURE_USER_ID?.trim();
60
+ if (envUserId) return envUserId;
61
+
62
+ const configDir = getConfigDir();
63
+ const userIdPath = join(configDir, USER_ID_STORAGE_FILE);
64
+
65
+ try {
66
+ if (existsSync(userIdPath)) {
67
+ const existing = readFileSync(userIdPath, "utf-8").trim();
68
+ // CC uses 64-char hex (32 random bytes). Accept existing hex IDs;
69
+ // regenerate if we find an old UUID-format ID.
70
+ if (existing && /^[0-9a-f]{64}$/.test(existing)) return existing;
71
+ }
72
+ } catch {
73
+ // fall through and generate a new id
74
+ }
75
+
76
+ // CC generates device_id as randomBytes(32).toString("hex") → 64-char hex
77
+ const generated = randomBytes(32).toString("hex");
78
+ try {
79
+ mkdirSync(configDir, { recursive: true });
80
+ writeFileSync(userIdPath, `${generated}\n`, {
81
+ encoding: "utf-8",
82
+ mode: 0o600,
83
+ });
84
+ } catch {
85
+ // Ignore filesystem errors; caller still gets generated ID for this runtime.
71
86
  }
72
- } catch {
73
- // fall through and generate a new id
74
- }
75
-
76
- // CC generates device_id as randomBytes(32).toString("hex") → 64-char hex
77
- const generated = randomBytes(32).toString("hex");
78
- try {
79
- mkdirSync(configDir, { recursive: true });
80
- writeFileSync(userIdPath, `${generated}\n`, {
81
- encoding: "utf-8",
82
- mode: 0o600,
83
- });
84
- } catch {
85
- // Ignore filesystem errors; caller still gets generated ID for this runtime.
86
- }
87
- return generated;
87
+ return generated;
88
88
  }
89
89
 
90
90
  export function shouldDebugSystemPrompt(): boolean {
91
- return isTruthyEnv(process.env[DEBUG_SYSTEM_PROMPT_ENV]);
91
+ return isTruthyEnv(process.env[DEBUG_SYSTEM_PROMPT_ENV]);
92
92
  }
93
93
 
94
94
  export function logTransformedSystemPrompt(body: string | undefined): void {
95
- if (!shouldDebugSystemPrompt()) return;
96
- if (!body || typeof body !== "string") return;
97
-
98
- try {
99
- const parsed = JSON.parse(body);
100
- if (!Object.hasOwn(parsed, "system")) return;
101
- // Avoid circular import: inline the title-check here
102
- const isTitleGeneratorText = (text: unknown): boolean => {
103
- if (typeof text !== "string") return false;
104
- const lowered = text.trim().toLowerCase();
105
- return lowered.includes("you are a title generator") || lowered.includes("generate a brief title");
106
- };
107
-
108
- const system = parsed.system;
109
- if (
110
- Array.isArray(system) &&
111
- system.some((item: { type?: string; text?: string }) => item.type === "text" && isTitleGeneratorText(item.text))
112
- ) {
113
- return;
114
- }
115
-
116
- // The plugin relocates non-CC system blocks into the first user message
117
- // wrapped in <system-instructions>. Check there too so title-generator
118
- // requests are still suppressed from the debug log after the relocation
119
- // pass runs.
120
- const messages = parsed.messages;
121
- if (Array.isArray(messages) && messages.length > 0) {
122
- const firstMsg = messages[0];
123
- if (firstMsg && firstMsg.role === "user") {
124
- const content = firstMsg.content;
125
- if (typeof content === "string" && isTitleGeneratorText(content)) {
126
- return;
95
+ if (!shouldDebugSystemPrompt()) return;
96
+ if (!body || typeof body !== "string") return;
97
+
98
+ try {
99
+ const parsed = JSON.parse(body);
100
+ if (!Object.hasOwn(parsed, "system")) return;
101
+ // Avoid circular import: inline the title-check here
102
+ const isTitleGeneratorText = (text: unknown): boolean => {
103
+ if (typeof text !== "string") return false;
104
+ const lowered = text.trim().toLowerCase();
105
+ return lowered.includes("you are a title generator") || lowered.includes("generate a brief title");
106
+ };
107
+
108
+ const system = parsed.system;
109
+ if (
110
+ Array.isArray(system) &&
111
+ system.some(
112
+ (item: { type?: string; text?: string }) => item.type === "text" && isTitleGeneratorText(item.text),
113
+ )
114
+ ) {
115
+ return;
127
116
  }
128
- if (Array.isArray(content)) {
129
- for (const block of content) {
130
- if (block && typeof block === "object" && isTitleGeneratorText((block as { text?: unknown }).text)) {
131
- return;
117
+
118
+ // The plugin relocates non-CC system blocks into the first user message
119
+ // wrapped in <system-instructions>. Check there too so title-generator
120
+ // requests are still suppressed from the debug log after the relocation
121
+ // pass runs.
122
+ const messages = parsed.messages;
123
+ if (Array.isArray(messages) && messages.length > 0) {
124
+ const firstMsg = messages[0];
125
+ if (firstMsg && firstMsg.role === "user") {
126
+ const content = firstMsg.content;
127
+ if (typeof content === "string" && isTitleGeneratorText(content)) {
128
+ return;
129
+ }
130
+ if (Array.isArray(content)) {
131
+ for (const block of content) {
132
+ if (
133
+ block &&
134
+ typeof block === "object" &&
135
+ isTitleGeneratorText((block as { text?: unknown }).text)
136
+ ) {
137
+ return;
138
+ }
139
+ }
140
+ }
132
141
  }
133
- }
134
142
  }
135
- }
136
- }
137
143
 
138
- // eslint-disable-next-line no-console -- explicit debug logger gated by OPENCODE_ANTHROPIC_DEBUG_SYSTEM_PROMPT
139
- console.error(
140
- "[opencode-anthropic-auth][system-debug] transformed system:",
141
- JSON.stringify(parsed.system, null, 2),
142
- );
143
- } catch {
144
- // Ignore parse errors in debug logging path.
145
- }
144
+ // eslint-disable-next-line no-console -- explicit debug logger gated by OPENCODE_ANTHROPIC_DEBUG_SYSTEM_PROMPT
145
+ console.error(
146
+ "[opencode-anthropic-auth][system-debug] transformed system:",
147
+ JSON.stringify(parsed.system, null, 2),
148
+ );
149
+ } catch {
150
+ // Ignore parse errors in debug logging path.
151
+ }
146
152
  }