@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.
- package/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -10,116 +10,116 @@ import { describe, it, expect } from "vitest";
|
|
|
10
10
|
import { sanitizeSystemText } from "../system-prompt/sanitize.js";
|
|
11
11
|
|
|
12
12
|
describe("sanitizeSystemText word boundaries", () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
13
|
+
it("replaces the PascalCase word 'OpenCode' with 'Claude Code'", () => {
|
|
14
|
+
const result = sanitizeSystemText("Run OpenCode first", true);
|
|
15
|
+
expect(result).not.toContain("OpenCode");
|
|
16
|
+
expect(result).toContain("Claude Code");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("replaces the lowercase word 'opencode' with 'Claude'", () => {
|
|
20
|
+
// Per sanitize.ts, /\bopencode\b/gi → "Claude" (not "Claude Code")
|
|
21
|
+
const result = sanitizeSystemText("use opencode here", true);
|
|
22
|
+
expect(result).not.toContain("opencode");
|
|
23
|
+
expect(result).toContain("Claude");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("does NOT replace 'myopencode' (word boundary on left)", () => {
|
|
27
|
+
const result = sanitizeSystemText("the myopencode binary", true);
|
|
28
|
+
expect(result).toContain("myopencode");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does NOT replace 'opencoder' (word boundary on right)", () => {
|
|
32
|
+
const result = sanitizeSystemText("known as opencoder", true);
|
|
33
|
+
expect(result).toContain("opencoder");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does NOT replace 'preopencode' (word boundary on both sides)", () => {
|
|
37
|
+
const result = sanitizeSystemText("run preopencode first", true);
|
|
38
|
+
expect(result).toContain("preopencode");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles mixed content correctly", () => {
|
|
42
|
+
const result = sanitizeSystemText("use opencode inside myopencode directory", true);
|
|
43
|
+
// Standalone "opencode" becomes "Claude"
|
|
44
|
+
expect(result).toContain("Claude");
|
|
45
|
+
// "myopencode" is preserved because of word boundary
|
|
46
|
+
expect(result).toContain("myopencode");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("preserves text when enabled=false", () => {
|
|
50
|
+
const result = sanitizeSystemText("use opencode here", false);
|
|
51
|
+
expect(result).toContain("opencode");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("replaces 'Sisyphus' with 'Claude Code Agent'", () => {
|
|
55
|
+
const result = sanitizeSystemText("from the Sisyphus agent", true);
|
|
56
|
+
expect(result).not.toContain("Sisyphus");
|
|
57
|
+
expect(result).toContain("Claude Code Agent");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("replaces 'morph_edit' with 'edit' at word boundaries", () => {
|
|
61
|
+
const result = sanitizeSystemText("call morph_edit tool", true);
|
|
62
|
+
expect(result).not.toContain("morph_edit");
|
|
63
|
+
expect(result).toContain("edit");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------
|
|
67
|
+
// Regressions for the hyphen/slash word boundary fix.
|
|
68
|
+
// The previous regex used \b which treats `-` and `/` as word boundaries,
|
|
69
|
+
// so `opencode-anthropic-fix` and `/Users/.../opencode/dist` were getting
|
|
70
|
+
// rewritten in place. The new regex uses negative lookarounds for
|
|
71
|
+
// [\w\-/] on both sides so these forms survive verbatim.
|
|
72
|
+
// ---------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
it("does NOT rewrite 'opencode-anthropic-fix' (hyphen on the right)", () => {
|
|
75
|
+
const result = sanitizeSystemText("Loaded opencode-anthropic-fix from disk", true);
|
|
76
|
+
expect(result).toContain("opencode-anthropic-fix");
|
|
77
|
+
expect(result).not.toContain("Claude-anthropic-fix");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does NOT rewrite 'pre-opencode' (hyphen on the left)", () => {
|
|
81
|
+
const result = sanitizeSystemText("the pre-opencode hook fired", true);
|
|
82
|
+
expect(result).toContain("pre-opencode");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does NOT corrupt path-like strings containing /opencode/", () => {
|
|
86
|
+
const input = "Working dir: /Users/rmk/projects/opencode-auth/src";
|
|
87
|
+
const result = sanitizeSystemText(input, true);
|
|
88
|
+
expect(result).toBe(input);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("does NOT corrupt deep paths with multiple opencode segments", () => {
|
|
92
|
+
const input = "/home/user/.config/opencode/plugin/opencode-anthropic-auth-plugin.js";
|
|
93
|
+
const result = sanitizeSystemText(input, true);
|
|
94
|
+
expect(result).toBe(input);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does NOT rewrite the PascalCase form inside hyphenated identifiers", () => {
|
|
98
|
+
const result = sanitizeSystemText("the OpenCode-Plugin loader", true);
|
|
99
|
+
expect(result).toContain("OpenCode-Plugin");
|
|
100
|
+
expect(result).not.toContain("Claude Code-Plugin");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("still rewrites a standalone PascalCase 'OpenCode' next to a hyphenated form", () => {
|
|
104
|
+
const result = sanitizeSystemText("OpenCode loaded opencode-anthropic-fix", true);
|
|
105
|
+
expect(result).toContain("Claude Code loaded");
|
|
106
|
+
expect(result).toContain("opencode-anthropic-fix");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("defaults to enabled=false (no second arg means no rewriting)", () => {
|
|
110
|
+
const result = sanitizeSystemText("use OpenCode and opencode and Sisyphus and morph_edit");
|
|
111
|
+
expect(result).toBe("use OpenCode and opencode and Sisyphus and morph_edit");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("explicit enabled=false preserves text verbatim", () => {
|
|
115
|
+
const input = "Path: /Users/rmk/projects/opencode-anthropic-fix";
|
|
116
|
+
const result = sanitizeSystemText(input, false);
|
|
117
|
+
expect(result).toBe(input);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("explicit enabled=true rewrites the standalone forms", () => {
|
|
121
|
+
const result = sanitizeSystemText("use opencode for tasks", true);
|
|
122
|
+
expect(result).toContain("Claude");
|
|
123
|
+
expect(result).not.toContain("opencode");
|
|
124
|
+
});
|
|
125
125
|
});
|
|
@@ -11,100 +11,100 @@ import { capFileAccountMap, FILE_ACCOUNT_MAP_MAX_SIZE } from "../commands/router
|
|
|
11
11
|
import { pruneExpiredPendingOAuth, PENDING_OAUTH_TTL_MS, type PendingOAuthEntry } from "../commands/oauth-flow.js";
|
|
12
12
|
|
|
13
13
|
function makePendingEntry(createdAt: number): PendingOAuthEntry {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
return {
|
|
15
|
+
mode: "login",
|
|
16
|
+
verifier: "test-verifier",
|
|
17
|
+
createdAt,
|
|
18
|
+
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
describe("capFileAccountMap FIFO eviction", () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
22
|
+
it("exports FILE_ACCOUNT_MAP_MAX_SIZE = 1000", () => {
|
|
23
|
+
expect(FILE_ACCOUNT_MAP_MAX_SIZE).toBe(1000);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("caps the map at FILE_ACCOUNT_MAP_MAX_SIZE entries", () => {
|
|
27
|
+
const map = new Map<string, number>();
|
|
28
|
+
for (let i = 0; i < FILE_ACCOUNT_MAP_MAX_SIZE + 100; i++) {
|
|
29
|
+
capFileAccountMap(map, `file_${i}`, i % 5);
|
|
30
|
+
}
|
|
31
|
+
expect(map.size).toBeLessThanOrEqual(FILE_ACCOUNT_MAP_MAX_SIZE);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("evicts oldest entries first (FIFO)", () => {
|
|
35
|
+
const map = new Map<string, number>();
|
|
36
|
+
for (let i = 0; i < FILE_ACCOUNT_MAP_MAX_SIZE; i++) {
|
|
37
|
+
capFileAccountMap(map, `file_${i}`, 0);
|
|
38
|
+
}
|
|
39
|
+
capFileAccountMap(map, "file_overflow", 1);
|
|
40
|
+
expect(map.has("file_0")).toBe(false);
|
|
41
|
+
expect(map.has("file_overflow")).toBe(true);
|
|
42
|
+
expect(map.size).toBe(FILE_ACCOUNT_MAP_MAX_SIZE);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("updates value of existing key when below cap", () => {
|
|
46
|
+
const map = new Map<string, number>();
|
|
47
|
+
capFileAccountMap(map, "file_a", 1);
|
|
48
|
+
capFileAccountMap(map, "file_a", 2);
|
|
49
|
+
expect(map.get("file_a")).toBe(2);
|
|
50
|
+
expect(map.size).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("handles rapid insertion under cap without eviction", () => {
|
|
54
|
+
const map = new Map<string, number>();
|
|
55
|
+
for (let i = 0; i < 500; i++) {
|
|
56
|
+
capFileAccountMap(map, `file_${i}`, 0);
|
|
57
|
+
}
|
|
58
|
+
expect(map.size).toBe(500);
|
|
59
|
+
expect(map.has("file_0")).toBe(true);
|
|
60
|
+
expect(map.has("file_499")).toBe(true);
|
|
61
|
+
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
describe("pruneExpiredPendingOAuth TTL cleanup", () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
65
|
+
it("exports PENDING_OAUTH_TTL_MS = 10 minutes", () => {
|
|
66
|
+
expect(PENDING_OAUTH_TTL_MS).toBe(10 * 60 * 1000);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("removes entries older than TTL", () => {
|
|
70
|
+
const map = new Map<string, PendingOAuthEntry>();
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
map.set("expired", makePendingEntry(now - PENDING_OAUTH_TTL_MS - 1000));
|
|
73
|
+
map.set("fresh", makePendingEntry(now));
|
|
74
|
+
|
|
75
|
+
pruneExpiredPendingOAuth(map);
|
|
76
|
+
|
|
77
|
+
expect(map.has("expired")).toBe(false);
|
|
78
|
+
expect(map.has("fresh")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not remove entries just inside the TTL boundary", () => {
|
|
82
|
+
const map = new Map<string, PendingOAuthEntry>();
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
map.set("boundary", makePendingEntry(now - PENDING_OAUTH_TTL_MS + 1000));
|
|
85
|
+
|
|
86
|
+
pruneExpiredPendingOAuth(map);
|
|
87
|
+
|
|
88
|
+
expect(map.has("boundary")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("handles empty map without error", () => {
|
|
92
|
+
const map = new Map<string, PendingOAuthEntry>();
|
|
93
|
+
expect(() => pruneExpiredPendingOAuth(map)).not.toThrow();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("removes only expired entries, leaves fresh ones", () => {
|
|
97
|
+
const map = new Map<string, PendingOAuthEntry>();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
map.set("old1", makePendingEntry(now - PENDING_OAUTH_TTL_MS - 5000));
|
|
100
|
+
map.set("old2", makePendingEntry(now - PENDING_OAUTH_TTL_MS - 2000));
|
|
101
|
+
map.set("new1", makePendingEntry(now - 1000));
|
|
102
|
+
map.set("new2", makePendingEntry(now));
|
|
103
|
+
|
|
104
|
+
pruneExpiredPendingOAuth(map);
|
|
105
|
+
|
|
106
|
+
expect(map.size).toBe(2);
|
|
107
|
+
expect(map.has("new1")).toBe(true);
|
|
108
|
+
expect(map.has("new2")).toBe(true);
|
|
109
|
+
});
|
|
110
110
|
});
|