opencode-anthropic-fix 0.0.14
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/.github/workflows/publish.yml +31 -0
- package/.husky/pre-commit +2 -0
- package/.husky/pre-push +3 -0
- package/.planning/CLI-ENHANCEMENTS.md +221 -0
- package/.planning/codebase/ARCHITECTURE.md +201 -0
- package/.planning/codebase/CONCERNS.md +219 -0
- package/.planning/codebase/CONVENTIONS.md +335 -0
- package/.planning/codebase/INTEGRATIONS.md +215 -0
- package/.planning/codebase/STACK.md +160 -0
- package/.planning/codebase/STRUCTURE.md +244 -0
- package/.planning/codebase/TESTING.md +456 -0
- package/.prettierignore +3 -0
- package/.prettierrc +10 -0
- package/CONTRIBUTING.md +525 -0
- package/LICENSE +21 -0
- package/README.md +478 -0
- package/agents.md +22 -0
- package/bun.lock +43 -0
- package/cli.mjs +1773 -0
- package/cli.test.mjs +1797 -0
- package/docs/mimese-http-header-system-prompt.md +326 -0
- package/docs/plans/afcw-upstream-watcher.json +495 -0
- package/eslint.config.mjs +69 -0
- package/index.mjs +2209 -0
- package/index.test.mjs +2690 -0
- package/lib/accounts.mjs +670 -0
- package/lib/accounts.test.mjs +782 -0
- package/lib/backoff.mjs +217 -0
- package/lib/backoff.test.mjs +296 -0
- package/lib/config.mjs +366 -0
- package/lib/config.test.mjs +307 -0
- package/lib/oauth.mjs +103 -0
- package/lib/rotation.mjs +287 -0
- package/lib/rotation.test.mjs +346 -0
- package/lib/storage.mjs +265 -0
- package/lib/storage.test.mjs +470 -0
- package/nul +1 -0
- package/package.json +40 -0
- package/script/publish.ts +29 -0
- package/scripts/build.mjs +34 -0
- package/scripts/install.mjs +319 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
deduplicateByRefreshToken,
|
|
4
|
+
ensureGitignore,
|
|
5
|
+
getStoragePath,
|
|
6
|
+
loadAccounts,
|
|
7
|
+
saveAccounts,
|
|
8
|
+
clearAccounts,
|
|
9
|
+
createDefaultStats,
|
|
10
|
+
} from "./storage.mjs";
|
|
11
|
+
import { promises as fs } from "node:fs";
|
|
12
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
// Mock fs modules
|
|
15
|
+
vi.mock("node:fs", () => ({
|
|
16
|
+
existsSync: vi.fn(),
|
|
17
|
+
readFileSync: vi.fn(),
|
|
18
|
+
appendFileSync: vi.fn(),
|
|
19
|
+
writeFileSync: vi.fn(),
|
|
20
|
+
promises: {
|
|
21
|
+
readFile: vi.fn(),
|
|
22
|
+
writeFile: vi.fn(),
|
|
23
|
+
rename: vi.fn(),
|
|
24
|
+
chmod: vi.fn(),
|
|
25
|
+
unlink: vi.fn(),
|
|
26
|
+
mkdir: vi.fn(),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("node:crypto", () => ({
|
|
31
|
+
randomBytes: vi.fn(() => ({
|
|
32
|
+
toString: () => "abcdef123456",
|
|
33
|
+
})),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// deduplicateByRefreshToken
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
describe("deduplicateByRefreshToken", () => {
|
|
41
|
+
it("returns empty array for empty input", () => {
|
|
42
|
+
expect(deduplicateByRefreshToken([])).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns single account unchanged", () => {
|
|
46
|
+
const accounts = [
|
|
47
|
+
{
|
|
48
|
+
refreshToken: "token1",
|
|
49
|
+
addedAt: 1000,
|
|
50
|
+
lastUsed: 2000,
|
|
51
|
+
enabled: true,
|
|
52
|
+
rateLimitResetTimes: {},
|
|
53
|
+
consecutiveFailures: 0,
|
|
54
|
+
lastFailureTime: null,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const result = deduplicateByRefreshToken(accounts);
|
|
58
|
+
expect(result).toHaveLength(1);
|
|
59
|
+
expect(result[0].refreshToken).toBe("token1");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("keeps most recently used when duplicates exist", () => {
|
|
63
|
+
const accounts = [
|
|
64
|
+
{
|
|
65
|
+
refreshToken: "token1",
|
|
66
|
+
addedAt: 1000,
|
|
67
|
+
lastUsed: 1000,
|
|
68
|
+
enabled: true,
|
|
69
|
+
rateLimitResetTimes: {},
|
|
70
|
+
consecutiveFailures: 0,
|
|
71
|
+
lastFailureTime: null,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
refreshToken: "token1",
|
|
75
|
+
addedAt: 2000,
|
|
76
|
+
lastUsed: 5000,
|
|
77
|
+
enabled: true,
|
|
78
|
+
rateLimitResetTimes: {},
|
|
79
|
+
consecutiveFailures: 0,
|
|
80
|
+
lastFailureTime: null,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const result = deduplicateByRefreshToken(accounts);
|
|
84
|
+
expect(result).toHaveLength(1);
|
|
85
|
+
expect(result[0].lastUsed).toBe(5000);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("keeps different tokens as separate accounts", () => {
|
|
89
|
+
const accounts = [
|
|
90
|
+
{
|
|
91
|
+
refreshToken: "token1",
|
|
92
|
+
addedAt: 1000,
|
|
93
|
+
lastUsed: 1000,
|
|
94
|
+
enabled: true,
|
|
95
|
+
rateLimitResetTimes: {},
|
|
96
|
+
consecutiveFailures: 0,
|
|
97
|
+
lastFailureTime: null,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
refreshToken: "token2",
|
|
101
|
+
addedAt: 2000,
|
|
102
|
+
lastUsed: 2000,
|
|
103
|
+
enabled: true,
|
|
104
|
+
rateLimitResetTimes: {},
|
|
105
|
+
consecutiveFailures: 0,
|
|
106
|
+
lastFailureTime: null,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const result = deduplicateByRefreshToken(accounts);
|
|
110
|
+
expect(result).toHaveLength(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("skips accounts without refreshToken", () => {
|
|
114
|
+
const accounts = [
|
|
115
|
+
{
|
|
116
|
+
refreshToken: "",
|
|
117
|
+
addedAt: 1000,
|
|
118
|
+
lastUsed: 1000,
|
|
119
|
+
enabled: true,
|
|
120
|
+
rateLimitResetTimes: {},
|
|
121
|
+
consecutiveFailures: 0,
|
|
122
|
+
lastFailureTime: null,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
const result = deduplicateByRefreshToken(accounts);
|
|
126
|
+
expect(result).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// ensureGitignore
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
describe("ensureGitignore", () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
vi.resetAllMocks();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("creates new .gitignore when none exists", () => {
|
|
140
|
+
existsSync.mockReturnValue(false);
|
|
141
|
+
ensureGitignore("/config/dir");
|
|
142
|
+
expect(writeFileSync).toHaveBeenCalledWith(
|
|
143
|
+
expect.stringMatching(/[\\/]config[\\/]dir[\\/]\.gitignore$/),
|
|
144
|
+
expect.stringContaining("anthropic-accounts.json"),
|
|
145
|
+
"utf-8",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("appends missing entries to existing .gitignore", () => {
|
|
150
|
+
existsSync.mockReturnValue(true);
|
|
151
|
+
readFileSync.mockReturnValue("some-other-file\n");
|
|
152
|
+
ensureGitignore("/config/dir");
|
|
153
|
+
expect(appendFileSync).toHaveBeenCalledWith(
|
|
154
|
+
expect.stringMatching(/[\\/]config[\\/]dir[\\/]\.gitignore$/),
|
|
155
|
+
expect.stringContaining("anthropic-accounts.json"),
|
|
156
|
+
"utf-8",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("does nothing when all entries already present", () => {
|
|
161
|
+
existsSync.mockReturnValue(true);
|
|
162
|
+
readFileSync.mockReturnValue(".gitignore\nanthropic-accounts.json\nanthropic-accounts.json.*.tmp\n");
|
|
163
|
+
ensureGitignore("/config/dir");
|
|
164
|
+
expect(appendFileSync).not.toHaveBeenCalled();
|
|
165
|
+
expect(writeFileSync).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("handles errors gracefully", () => {
|
|
169
|
+
existsSync.mockImplementation(() => {
|
|
170
|
+
throw new Error("permission denied");
|
|
171
|
+
});
|
|
172
|
+
// Should not throw
|
|
173
|
+
expect(() => ensureGitignore("/config/dir")).not.toThrow();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// getStoragePath
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
describe("getStoragePath", () => {
|
|
182
|
+
it("returns path ending with anthropic-accounts.json", () => {
|
|
183
|
+
const path = getStoragePath();
|
|
184
|
+
expect(path.endsWith("anthropic-accounts.json")).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// loadAccounts
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
describe("loadAccounts", () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
vi.resetAllMocks();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("returns null when file does not exist", async () => {
|
|
198
|
+
fs.readFile.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
|
|
199
|
+
const result = await loadAccounts();
|
|
200
|
+
expect(result).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns null for invalid JSON", async () => {
|
|
204
|
+
fs.readFile.mockResolvedValue("not json");
|
|
205
|
+
const result = await loadAccounts();
|
|
206
|
+
expect(result).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns null for wrong version", async () => {
|
|
210
|
+
fs.readFile.mockResolvedValue(JSON.stringify({ version: 99, accounts: [] }));
|
|
211
|
+
const result = await loadAccounts();
|
|
212
|
+
expect(result).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("returns null when accounts is not an array", async () => {
|
|
216
|
+
fs.readFile.mockResolvedValue(JSON.stringify({ version: 1, accounts: "not-array" }));
|
|
217
|
+
const result = await loadAccounts();
|
|
218
|
+
expect(result).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("loads valid accounts", async () => {
|
|
222
|
+
fs.readFile.mockResolvedValue(
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
version: 1,
|
|
225
|
+
accounts: [
|
|
226
|
+
{
|
|
227
|
+
refreshToken: "token1",
|
|
228
|
+
access: "access1",
|
|
229
|
+
expires: 9999999999,
|
|
230
|
+
addedAt: 1000,
|
|
231
|
+
lastUsed: 2000,
|
|
232
|
+
enabled: true,
|
|
233
|
+
rateLimitResetTimes: {},
|
|
234
|
+
consecutiveFailures: 0,
|
|
235
|
+
lastFailureTime: null,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
activeIndex: 0,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
const result = await loadAccounts();
|
|
242
|
+
expect(result).not.toBeNull();
|
|
243
|
+
expect(result.accounts).toHaveLength(1);
|
|
244
|
+
expect(result.accounts[0].refreshToken).toBe("token1");
|
|
245
|
+
expect(result.accounts[0].access).toBe("access1");
|
|
246
|
+
expect(result.accounts[0].expires).toBe(9999999999);
|
|
247
|
+
expect(result.activeIndex).toBe(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("filters out invalid accounts (missing refreshToken)", async () => {
|
|
251
|
+
fs.readFile.mockResolvedValue(
|
|
252
|
+
JSON.stringify({
|
|
253
|
+
version: 1,
|
|
254
|
+
accounts: [{ refreshToken: "valid", addedAt: 1000 }, { email: "no-token" }, null],
|
|
255
|
+
activeIndex: 0,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
const result = await loadAccounts();
|
|
259
|
+
expect(result.accounts).toHaveLength(1);
|
|
260
|
+
expect(result.accounts[0].refreshToken).toBe("valid");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("clamps activeIndex to valid range", async () => {
|
|
264
|
+
fs.readFile.mockResolvedValue(
|
|
265
|
+
JSON.stringify({
|
|
266
|
+
version: 1,
|
|
267
|
+
accounts: [{ refreshToken: "token1" }],
|
|
268
|
+
activeIndex: 99,
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
const result = await loadAccounts();
|
|
272
|
+
expect(result.activeIndex).toBe(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("deduplicates accounts by refresh token", async () => {
|
|
276
|
+
fs.readFile.mockResolvedValue(
|
|
277
|
+
JSON.stringify({
|
|
278
|
+
version: 1,
|
|
279
|
+
accounts: [
|
|
280
|
+
{ refreshToken: "token1", lastUsed: 1000 },
|
|
281
|
+
{ refreshToken: "token1", lastUsed: 5000 },
|
|
282
|
+
],
|
|
283
|
+
activeIndex: 0,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
const result = await loadAccounts();
|
|
287
|
+
expect(result.accounts).toHaveLength(1);
|
|
288
|
+
expect(result.accounts[0].lastUsed).toBe(5000);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("applies defaults for missing fields", async () => {
|
|
292
|
+
fs.readFile.mockResolvedValue(
|
|
293
|
+
JSON.stringify({
|
|
294
|
+
version: 1,
|
|
295
|
+
accounts: [{ refreshToken: "token1" }],
|
|
296
|
+
activeIndex: 0,
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
const result = await loadAccounts();
|
|
300
|
+
const acc = result.accounts[0];
|
|
301
|
+
expect(acc.enabled).toBe(true);
|
|
302
|
+
expect(acc.consecutiveFailures).toBe(0);
|
|
303
|
+
expect(acc.lastFailureTime).toBeNull();
|
|
304
|
+
expect(acc.rateLimitResetTimes).toEqual({});
|
|
305
|
+
expect(acc.lastUsed).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// saveAccounts
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
describe("saveAccounts", () => {
|
|
314
|
+
beforeEach(() => {
|
|
315
|
+
vi.resetAllMocks();
|
|
316
|
+
existsSync.mockReturnValue(true);
|
|
317
|
+
readFileSync.mockReturnValue(".gitignore\nanthropic-accounts.json\nanthropic-accounts.json.*.tmp\n");
|
|
318
|
+
fs.mkdir.mockResolvedValue(undefined);
|
|
319
|
+
fs.writeFile.mockResolvedValue(undefined);
|
|
320
|
+
fs.rename.mockResolvedValue(undefined);
|
|
321
|
+
fs.chmod.mockResolvedValue(undefined);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("writes atomically via temp file + rename", async () => {
|
|
325
|
+
const storage = {
|
|
326
|
+
version: 1,
|
|
327
|
+
accounts: [{ refreshToken: "token1" }],
|
|
328
|
+
activeIndex: 0,
|
|
329
|
+
};
|
|
330
|
+
await saveAccounts(storage);
|
|
331
|
+
|
|
332
|
+
expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining(".tmp"), expect.any(String), {
|
|
333
|
+
encoding: "utf-8",
|
|
334
|
+
mode: 0o600,
|
|
335
|
+
});
|
|
336
|
+
expect(fs.rename).toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("creates config directory if needed", async () => {
|
|
340
|
+
const storage = { version: 1, accounts: [], activeIndex: 0 };
|
|
341
|
+
await saveAccounts(storage);
|
|
342
|
+
expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), {
|
|
343
|
+
recursive: true,
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("cleans up temp file on write error", async () => {
|
|
348
|
+
fs.writeFile.mockRejectedValue(new Error("disk full"));
|
|
349
|
+
fs.unlink.mockResolvedValue(undefined);
|
|
350
|
+
|
|
351
|
+
const storage = { version: 1, accounts: [], activeIndex: 0 };
|
|
352
|
+
await expect(saveAccounts(storage)).rejects.toThrow("disk full");
|
|
353
|
+
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining(".tmp"));
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// clearAccounts
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
describe("clearAccounts", () => {
|
|
362
|
+
beforeEach(() => {
|
|
363
|
+
vi.resetAllMocks();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("deletes the storage file", async () => {
|
|
367
|
+
fs.unlink.mockResolvedValue(undefined);
|
|
368
|
+
await clearAccounts();
|
|
369
|
+
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("anthropic-accounts.json"));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("ignores ENOENT errors", async () => {
|
|
373
|
+
fs.unlink.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
|
|
374
|
+
await expect(clearAccounts()).resolves.toBeUndefined();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("rethrows non-ENOENT errors", async () => {
|
|
378
|
+
fs.unlink.mockRejectedValue(Object.assign(new Error("permission denied"), { code: "EACCES" }));
|
|
379
|
+
await expect(clearAccounts()).rejects.toThrow("permission denied");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Stats validation
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
describe("createDefaultStats", () => {
|
|
388
|
+
it("creates zeroed stats with given timestamp", () => {
|
|
389
|
+
const stats = createDefaultStats(1000);
|
|
390
|
+
expect(stats).toEqual({
|
|
391
|
+
requests: 0,
|
|
392
|
+
inputTokens: 0,
|
|
393
|
+
outputTokens: 0,
|
|
394
|
+
cacheReadTokens: 0,
|
|
395
|
+
cacheWriteTokens: 0,
|
|
396
|
+
lastReset: 1000,
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("uses Date.now() when no timestamp given", () => {
|
|
401
|
+
const before = Date.now();
|
|
402
|
+
const stats = createDefaultStats();
|
|
403
|
+
expect(stats.lastReset).toBeGreaterThanOrEqual(before);
|
|
404
|
+
expect(stats.lastReset).toBeLessThanOrEqual(Date.now());
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe("loadAccounts with stats", () => {
|
|
409
|
+
it("loads accounts with stats fields preserved", async () => {
|
|
410
|
+
const stored = {
|
|
411
|
+
version: 1,
|
|
412
|
+
accounts: [
|
|
413
|
+
{
|
|
414
|
+
refreshToken: "tok1",
|
|
415
|
+
stats: {
|
|
416
|
+
requests: 42,
|
|
417
|
+
inputTokens: 10000,
|
|
418
|
+
outputTokens: 5000,
|
|
419
|
+
cacheReadTokens: 2000,
|
|
420
|
+
cacheWriteTokens: 100,
|
|
421
|
+
lastReset: 1700000000000,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
activeIndex: 0,
|
|
426
|
+
};
|
|
427
|
+
fs.readFile.mockResolvedValue(JSON.stringify(stored));
|
|
428
|
+
|
|
429
|
+
const result = await loadAccounts();
|
|
430
|
+
expect(result.accounts[0].stats.requests).toBe(42);
|
|
431
|
+
expect(result.accounts[0].stats.inputTokens).toBe(10000);
|
|
432
|
+
expect(result.accounts[0].stats.outputTokens).toBe(5000);
|
|
433
|
+
expect(result.accounts[0].stats.cacheReadTokens).toBe(2000);
|
|
434
|
+
expect(result.accounts[0].stats.cacheWriteTokens).toBe(100);
|
|
435
|
+
expect(result.accounts[0].stats.lastReset).toBe(1700000000000);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("provides default stats when missing from stored data", async () => {
|
|
439
|
+
const stored = {
|
|
440
|
+
version: 1,
|
|
441
|
+
accounts: [{ refreshToken: "tok1" }],
|
|
442
|
+
activeIndex: 0,
|
|
443
|
+
};
|
|
444
|
+
fs.readFile.mockResolvedValue(JSON.stringify(stored));
|
|
445
|
+
|
|
446
|
+
const result = await loadAccounts();
|
|
447
|
+
expect(result.accounts[0].stats.requests).toBe(0);
|
|
448
|
+
expect(result.accounts[0].stats.inputTokens).toBe(0);
|
|
449
|
+
expect(result.accounts[0].stats.outputTokens).toBe(0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("clamps negative stats values to 0", async () => {
|
|
453
|
+
const stored = {
|
|
454
|
+
version: 1,
|
|
455
|
+
accounts: [
|
|
456
|
+
{
|
|
457
|
+
refreshToken: "tok1",
|
|
458
|
+
stats: { requests: -5, inputTokens: -100, outputTokens: NaN },
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
activeIndex: 0,
|
|
462
|
+
};
|
|
463
|
+
fs.readFile.mockResolvedValue(JSON.stringify(stored));
|
|
464
|
+
|
|
465
|
+
const result = await loadAccounts();
|
|
466
|
+
expect(result.accounts[0].stats.requests).toBe(0);
|
|
467
|
+
expect(result.accounts[0].stats.inputTokens).toBe(0);
|
|
468
|
+
expect(result.accounts[0].stats.outputTokens).toBe(0);
|
|
469
|
+
});
|
|
470
|
+
});
|
package/nul
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dir: cannot access '/b': No such file or directory
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-anthropic-fix",
|
|
3
|
+
"version": "0.0.14",
|
|
4
|
+
"main": "./index.mjs",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "node scripts/build.mjs",
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"test:watch": "vitest",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"lint:fix": "eslint --fix .",
|
|
11
|
+
"format": "prettier --write .",
|
|
12
|
+
"format:check": "prettier --check .",
|
|
13
|
+
"install:link": "node scripts/install.mjs link",
|
|
14
|
+
"install:copy": "npm run build && node scripts/install.mjs copy",
|
|
15
|
+
"uninstall": "node scripts/install.mjs uninstall",
|
|
16
|
+
"prepare": "husky"
|
|
17
|
+
},
|
|
18
|
+
"lint-staged": {
|
|
19
|
+
"*.mjs": [
|
|
20
|
+
"prettier --write",
|
|
21
|
+
"eslint --fix"
|
|
22
|
+
],
|
|
23
|
+
"*.{json,md}": [
|
|
24
|
+
"prettier --write"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/js": "^10.0.1",
|
|
29
|
+
"@opencode-ai/plugin": "^0.4.45",
|
|
30
|
+
"esbuild": "^0.27.3",
|
|
31
|
+
"eslint": "^10.0.0",
|
|
32
|
+
"husky": "^9.1.7",
|
|
33
|
+
"lint-staged": "^16.2.7",
|
|
34
|
+
"prettier": "^3.8.1",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@openauthjs/openauth": "^0.4.3"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
|
|
4
|
+
const dir = new URL("..", import.meta.url).pathname;
|
|
5
|
+
process.chdir(dir);
|
|
6
|
+
|
|
7
|
+
const bumpType = process.argv[2] || "patch";
|
|
8
|
+
|
|
9
|
+
console.log(`Bumping ${bumpType} version...`);
|
|
10
|
+
|
|
11
|
+
// Bump version in package.json
|
|
12
|
+
await $`npm version ${bumpType} --no-git-tag-version`;
|
|
13
|
+
|
|
14
|
+
// Read the new version
|
|
15
|
+
const pkg = await Bun.file("./package.json").json();
|
|
16
|
+
const version = pkg.version;
|
|
17
|
+
|
|
18
|
+
console.log(`New version: ${version}`);
|
|
19
|
+
|
|
20
|
+
// Commit the version bump
|
|
21
|
+
await $`git add package.json`;
|
|
22
|
+
await $`git commit -m "Bump version to ${version}"`;
|
|
23
|
+
await $`git push`;
|
|
24
|
+
|
|
25
|
+
// Trigger GitHub workflow
|
|
26
|
+
console.log(`Triggering publish workflow...`);
|
|
27
|
+
await $`gh workflow run publish.yml -f bump="${bumpType}"`;
|
|
28
|
+
|
|
29
|
+
console.log(`✓ Version bumped to ${version} and publish workflow triggered`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bundle plugin and CLI into self-contained single files.
|
|
5
|
+
*
|
|
6
|
+
* dist/opencode-anthropic-auth-plugin.js — plugin (no external deps)
|
|
7
|
+
* dist/opencode-anthropic-auth-cli.mjs — CLI (no external deps)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { build } from "esbuild";
|
|
11
|
+
|
|
12
|
+
const shared = {
|
|
13
|
+
bundle: true,
|
|
14
|
+
format: "esm",
|
|
15
|
+
platform: "node",
|
|
16
|
+
target: "node20",
|
|
17
|
+
// Node builtins stay as imports
|
|
18
|
+
external: ["node:*"],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
await Promise.all([
|
|
22
|
+
build({
|
|
23
|
+
...shared,
|
|
24
|
+
entryPoints: ["index.mjs"],
|
|
25
|
+
outfile: "dist/opencode-anthropic-auth-plugin.js",
|
|
26
|
+
}),
|
|
27
|
+
build({
|
|
28
|
+
...shared,
|
|
29
|
+
entryPoints: ["cli.mjs"],
|
|
30
|
+
outfile: "dist/opencode-anthropic-auth-cli.mjs",
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
console.log("Built dist/opencode-anthropic-auth-plugin.js and dist/opencode-anthropic-auth-cli.mjs");
|