opencode-anthropic-fix 0.0.16 → 0.0.18
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/index.mjs +466 -90
- package/lib/account-state.mjs +50 -0
- package/lib/account-state.test.mjs +109 -0
- package/lib/accounts.mjs +100 -11
- package/lib/accounts.test.mjs +95 -0
- package/lib/config.mjs +60 -0
- package/lib/oauth.mjs +35 -0
- package/lib/refresh-lock.mjs +123 -0
- package/lib/refresh-lock.test.mjs +79 -0
- package/lib/storage.mjs +76 -1
- package/lib/storage.test.mjs +133 -0
- package/package.json +7 -1
- package/.github/workflows/publish.yml +0 -31
- package/.husky/pre-commit +0 -2
- package/.husky/pre-push +0 -3
- package/.planning/CLI-ENHANCEMENTS.md +0 -221
- package/.planning/codebase/ARCHITECTURE.md +0 -201
- package/.planning/codebase/CONCERNS.md +0 -219
- package/.planning/codebase/CONVENTIONS.md +0 -335
- package/.planning/codebase/INTEGRATIONS.md +0 -215
- package/.planning/codebase/STACK.md +0 -160
- package/.planning/codebase/STRUCTURE.md +0 -244
- package/.planning/codebase/TESTING.md +0 -456
- package/.prettierignore +0 -3
- package/.prettierrc +0 -10
- package/CONTRIBUTING.md +0 -525
- package/agents.md +0 -22
- package/bun.lock +0 -43
- package/cli.mjs +0 -1773
- package/cli.test.mjs +0 -1797
- package/docs/mimese-http-header-system-prompt.md +0 -326
- package/docs/plans/afcw-upstream-watcher.json +0 -495
- package/eslint.config.mjs +0 -69
- package/index.test.mjs +0 -2690
- package/nul +0 -1
- package/script/publish.ts +0 -29
- package/scripts/build.mjs +0 -34
- package/scripts/install.mjs +0 -319
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { getStoragePath } from "./storage.mjs";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 2_000;
|
|
7
|
+
const DEFAULT_LOCK_BACKOFF_MS = 50;
|
|
8
|
+
const DEFAULT_STALE_LOCK_MS = 20_000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} ms
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
14
|
+
function delay(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} accountId
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function getLockPath(accountId) {
|
|
23
|
+
const hash = createHash("sha1").update(accountId).digest("hex").slice(0, 24);
|
|
24
|
+
return join(dirname(getStoragePath()), "locks", `refresh-${hash}.lock`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Try to acquire a per-account cross-process lock.
|
|
29
|
+
* @param {string} accountId
|
|
30
|
+
* @param {{ timeoutMs?: number, backoffMs?: number, staleMs?: number }} [options]
|
|
31
|
+
* @returns {Promise<{ acquired: boolean, lockPath: string | null, owner: string | null, lockInode: number | null }>}
|
|
32
|
+
*/
|
|
33
|
+
export async function acquireRefreshLock(accountId, options = {}) {
|
|
34
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
35
|
+
const backoffMs = options.backoffMs ?? DEFAULT_LOCK_BACKOFF_MS;
|
|
36
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
37
|
+
const lockPath = getLockPath(accountId);
|
|
38
|
+
const lockDir = dirname(lockPath);
|
|
39
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
40
|
+
const owner = randomBytes(12).toString("hex");
|
|
41
|
+
|
|
42
|
+
await fs.mkdir(lockDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
while (Date.now() <= deadline) {
|
|
45
|
+
try {
|
|
46
|
+
const handle = await fs.open(lockPath, "wx", 0o600);
|
|
47
|
+
try {
|
|
48
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), owner }), "utf-8");
|
|
49
|
+
const stat = await handle.stat();
|
|
50
|
+
return { acquired: true, lockPath, owner, lockInode: stat.ino };
|
|
51
|
+
} finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
56
|
+
if (code !== "EEXIST") {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(lockPath);
|
|
62
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
63
|
+
await fs.unlink(lockPath);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Lock may have been released concurrently; retry.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const remaining = deadline - Date.now();
|
|
71
|
+
if (remaining <= 0) break;
|
|
72
|
+
const jitter = Math.floor(Math.random() * 25);
|
|
73
|
+
await delay(Math.min(remaining, backoffMs + jitter));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { acquired: false, lockPath: null, owner: null, lockInode: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Release a lock acquired by acquireRefreshLock.
|
|
82
|
+
* @param {{ lockPath: string | null, owner?: string | null, lockInode?: number | null } | string | null} lock
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
export async function releaseRefreshLock(lock) {
|
|
86
|
+
const lockPath = typeof lock === "string" || lock === null ? lock : lock.lockPath;
|
|
87
|
+
const owner = typeof lock === "object" && lock ? lock.owner || null : null;
|
|
88
|
+
const lockInode = typeof lock === "object" && lock ? lock.lockInode || null : null;
|
|
89
|
+
|
|
90
|
+
if (!lockPath) return;
|
|
91
|
+
|
|
92
|
+
// Ownership-safe release: avoid deleting a lock that another process
|
|
93
|
+
// acquired after ours became stale.
|
|
94
|
+
if (owner) {
|
|
95
|
+
try {
|
|
96
|
+
const content = await fs.readFile(lockPath, "utf-8");
|
|
97
|
+
const parsed = JSON.parse(content);
|
|
98
|
+
if (!parsed || typeof parsed !== "object" || parsed.owner !== owner) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (lockInode) {
|
|
103
|
+
const stat = await fs.stat(lockPath);
|
|
104
|
+
if (stat.ino !== lockInode) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
110
|
+
if (code === "ENOENT") return;
|
|
111
|
+
// If unreadable/corrupt, fail closed to avoid deleting another
|
|
112
|
+
// process's lock when ownership cannot be verified.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await fs.unlink(lockPath);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
121
|
+
if (code !== "ENOENT") throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
const baseDir = join(tmpdir(), `opencode-refresh-lock-test-${process.pid}`);
|
|
7
|
+
const storagePath = join(baseDir, "anthropic-accounts.json");
|
|
8
|
+
|
|
9
|
+
vi.mock("./storage.mjs", () => ({
|
|
10
|
+
getStoragePath: () => storagePath,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { acquireRefreshLock, releaseRefreshLock } from "./refresh-lock.mjs";
|
|
14
|
+
|
|
15
|
+
describe("refresh-lock", () => {
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await fs.rm(baseDir, { recursive: true, force: true });
|
|
18
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await fs.rm(baseDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("does not release lock with mismatched owner", async () => {
|
|
26
|
+
const lock = await acquireRefreshLock("acc-1");
|
|
27
|
+
expect(lock.acquired).toBe(true);
|
|
28
|
+
expect(lock.lockPath).toBeTruthy();
|
|
29
|
+
|
|
30
|
+
await releaseRefreshLock({ lockPath: lock.lockPath, owner: "wrong-owner" });
|
|
31
|
+
|
|
32
|
+
await expect(fs.stat(lock.lockPath)).resolves.toBeTruthy();
|
|
33
|
+
|
|
34
|
+
await releaseRefreshLock(lock);
|
|
35
|
+
await expect(fs.stat(lock.lockPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("acquires a new lock after stale lock timeout", async () => {
|
|
39
|
+
const first = await acquireRefreshLock("acc-2", { timeoutMs: 50, staleMs: 10_000 });
|
|
40
|
+
expect(first.acquired).toBe(true);
|
|
41
|
+
|
|
42
|
+
const old = Date.now() / 1000 - 120;
|
|
43
|
+
await fs.utimes(first.lockPath, old, old);
|
|
44
|
+
|
|
45
|
+
const second = await acquireRefreshLock("acc-2", { timeoutMs: 200, backoffMs: 5, staleMs: 20 });
|
|
46
|
+
expect(second.acquired).toBe(true);
|
|
47
|
+
expect(second.owner).not.toBe(first.owner);
|
|
48
|
+
|
|
49
|
+
await releaseRefreshLock(second);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns not acquired when lock remains busy", async () => {
|
|
53
|
+
const first = await acquireRefreshLock("acc-3", { timeoutMs: 50 });
|
|
54
|
+
expect(first.acquired).toBe(true);
|
|
55
|
+
|
|
56
|
+
const second = await acquireRefreshLock("acc-3", { timeoutMs: 30, backoffMs: 5, staleMs: 60_000 });
|
|
57
|
+
expect(second.acquired).toBe(false);
|
|
58
|
+
|
|
59
|
+
await releaseRefreshLock(first);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not release when inode changed even if owner matches", async () => {
|
|
63
|
+
const first = await acquireRefreshLock("acc-4");
|
|
64
|
+
expect(first.acquired).toBe(true);
|
|
65
|
+
|
|
66
|
+
// Replace lock file with a new inode that reuses owner text.
|
|
67
|
+
await fs.unlink(first.lockPath);
|
|
68
|
+
await fs.writeFile(first.lockPath, JSON.stringify({ owner: first.owner, createdAt: Date.now() }), {
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
mode: 0o600,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await releaseRefreshLock(first);
|
|
74
|
+
|
|
75
|
+
await expect(fs.stat(first.lockPath)).resolves.toBeTruthy();
|
|
76
|
+
|
|
77
|
+
await fs.unlink(first.lockPath);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/lib/storage.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { getConfigDir } from "./config.mjs";
|
|
|
21
21
|
* @property {string} refreshToken
|
|
22
22
|
* @property {string} [access]
|
|
23
23
|
* @property {number} [expires]
|
|
24
|
+
* @property {number} token_updated_at
|
|
24
25
|
* @property {number} addedAt
|
|
25
26
|
* @property {number} lastUsed
|
|
26
27
|
* @property {boolean} enabled
|
|
@@ -160,6 +161,12 @@ function validateAccount(raw, now) {
|
|
|
160
161
|
refreshToken: acc.refreshToken,
|
|
161
162
|
access: typeof acc.access === "string" ? acc.access : undefined,
|
|
162
163
|
expires: typeof acc.expires === "number" && Number.isFinite(acc.expires) ? acc.expires : undefined,
|
|
164
|
+
token_updated_at:
|
|
165
|
+
typeof acc.token_updated_at === "number" && Number.isFinite(acc.token_updated_at)
|
|
166
|
+
? acc.token_updated_at
|
|
167
|
+
: typeof acc.tokenUpdatedAt === "number" && Number.isFinite(acc.tokenUpdatedAt)
|
|
168
|
+
? acc.tokenUpdatedAt
|
|
169
|
+
: addedAt,
|
|
163
170
|
addedAt,
|
|
164
171
|
lastUsed: typeof acc.lastUsed === "number" && Number.isFinite(acc.lastUsed) ? acc.lastUsed : 0,
|
|
165
172
|
enabled: acc.enabled !== false,
|
|
@@ -234,8 +241,76 @@ export async function saveAccounts(storage) {
|
|
|
234
241
|
await fs.mkdir(configDir, { recursive: true });
|
|
235
242
|
ensureGitignore(configDir);
|
|
236
243
|
|
|
244
|
+
/** @type {AccountStorage} */
|
|
245
|
+
let storageToWrite = storage;
|
|
246
|
+
|
|
247
|
+
// Merge auth fields against disk by freshness to avoid stale-process clobber.
|
|
248
|
+
// We do not resurrect removed accounts; only merge for accounts present in
|
|
249
|
+
// the incoming storage payload.
|
|
250
|
+
try {
|
|
251
|
+
const disk = await loadAccounts();
|
|
252
|
+
if (disk && storage.accounts.length > 0) {
|
|
253
|
+
const diskById = new Map(disk.accounts.map((a) => [a.id, a]));
|
|
254
|
+
/** @type {Map<number, AccountMetadata[]>} */
|
|
255
|
+
const diskByAddedAt = new Map();
|
|
256
|
+
const diskByToken = new Map(disk.accounts.map((a) => [a.refreshToken, a]));
|
|
257
|
+
for (const d of disk.accounts) {
|
|
258
|
+
const bucket = diskByAddedAt.get(d.addedAt) || [];
|
|
259
|
+
bucket.push(d);
|
|
260
|
+
diskByAddedAt.set(d.addedAt, bucket);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const findDiskMatch = (/** @type {AccountMetadata} */ acc) => {
|
|
264
|
+
const byId = diskById.get(acc.id);
|
|
265
|
+
if (byId) return byId;
|
|
266
|
+
|
|
267
|
+
const byAddedAt = diskByAddedAt.get(acc.addedAt);
|
|
268
|
+
if (byAddedAt?.length === 1) return byAddedAt[0];
|
|
269
|
+
|
|
270
|
+
const byToken = diskByToken.get(acc.refreshToken);
|
|
271
|
+
if (byToken) return byToken;
|
|
272
|
+
|
|
273
|
+
if (byAddedAt && byAddedAt.length > 0) return byAddedAt[0];
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const mergedAccounts = storage.accounts.map((acc) => {
|
|
278
|
+
const diskAcc = findDiskMatch(acc);
|
|
279
|
+
const memTs =
|
|
280
|
+
typeof acc.token_updated_at === "number" && Number.isFinite(acc.token_updated_at)
|
|
281
|
+
? acc.token_updated_at
|
|
282
|
+
: acc.addedAt;
|
|
283
|
+
const diskTs = diskAcc?.token_updated_at || 0;
|
|
284
|
+
const useDiskAuth = !!diskAcc && diskTs > memTs;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
...acc,
|
|
288
|
+
refreshToken: useDiskAuth ? diskAcc.refreshToken : acc.refreshToken,
|
|
289
|
+
access: useDiskAuth ? diskAcc.access : acc.access,
|
|
290
|
+
expires: useDiskAuth ? diskAcc.expires : acc.expires,
|
|
291
|
+
token_updated_at: useDiskAuth ? diskTs : memTs,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
let activeIndex = storage.activeIndex;
|
|
296
|
+
if (mergedAccounts.length > 0) {
|
|
297
|
+
activeIndex = Math.max(0, Math.min(activeIndex, mergedAccounts.length - 1));
|
|
298
|
+
} else {
|
|
299
|
+
activeIndex = 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
storageToWrite = {
|
|
303
|
+
...storage,
|
|
304
|
+
accounts: mergedAccounts,
|
|
305
|
+
activeIndex,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// If merge read fails, continue with caller-provided storage payload.
|
|
310
|
+
}
|
|
311
|
+
|
|
237
312
|
const tempPath = `${storagePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
238
|
-
const content = JSON.stringify(
|
|
313
|
+
const content = JSON.stringify(storageToWrite, null, 2);
|
|
239
314
|
|
|
240
315
|
try {
|
|
241
316
|
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
package/lib/storage.test.mjs
CHANGED
|
@@ -352,6 +352,139 @@ describe("saveAccounts", () => {
|
|
|
352
352
|
await expect(saveAccounts(storage)).rejects.toThrow("disk full");
|
|
353
353
|
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining(".tmp"));
|
|
354
354
|
});
|
|
355
|
+
|
|
356
|
+
it("merges auth fields from fresher disk state", async () => {
|
|
357
|
+
fs.readFile.mockResolvedValue(
|
|
358
|
+
JSON.stringify({
|
|
359
|
+
version: 1,
|
|
360
|
+
accounts: [
|
|
361
|
+
{
|
|
362
|
+
id: "a1",
|
|
363
|
+
refreshToken: "disk-refresh",
|
|
364
|
+
access: "disk-access",
|
|
365
|
+
expires: 999999,
|
|
366
|
+
token_updated_at: 2000,
|
|
367
|
+
addedAt: 1000,
|
|
368
|
+
lastUsed: 0,
|
|
369
|
+
enabled: true,
|
|
370
|
+
rateLimitResetTimes: {},
|
|
371
|
+
consecutiveFailures: 0,
|
|
372
|
+
lastFailureTime: null,
|
|
373
|
+
stats: createDefaultStats(1000),
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
activeIndex: 0,
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const storage = {
|
|
381
|
+
version: 1,
|
|
382
|
+
accounts: [
|
|
383
|
+
{
|
|
384
|
+
id: "a1",
|
|
385
|
+
refreshToken: "mem-refresh",
|
|
386
|
+
access: "mem-access",
|
|
387
|
+
expires: 123,
|
|
388
|
+
token_updated_at: 1000,
|
|
389
|
+
addedAt: 1000,
|
|
390
|
+
lastUsed: 0,
|
|
391
|
+
enabled: true,
|
|
392
|
+
rateLimitResetTimes: {},
|
|
393
|
+
consecutiveFailures: 0,
|
|
394
|
+
lastFailureTime: null,
|
|
395
|
+
stats: createDefaultStats(1000),
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
activeIndex: 0,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
await saveAccounts(storage);
|
|
402
|
+
|
|
403
|
+
const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
|
|
404
|
+
expect(written.accounts[0].refreshToken).toBe("disk-refresh");
|
|
405
|
+
expect(written.accounts[0].access).toBe("disk-access");
|
|
406
|
+
expect(written.accounts[0].expires).toBe(999999);
|
|
407
|
+
expect(written.accounts[0].token_updated_at).toBe(2000);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("matches id-less disk accounts by addedAt during freshness merge", async () => {
|
|
411
|
+
fs.readFile.mockResolvedValue(
|
|
412
|
+
JSON.stringify({
|
|
413
|
+
version: 1,
|
|
414
|
+
accounts: [
|
|
415
|
+
{
|
|
416
|
+
refreshToken: "disk-refresh-rotated",
|
|
417
|
+
access: "disk-access",
|
|
418
|
+
expires: 888888,
|
|
419
|
+
token_updated_at: 3000,
|
|
420
|
+
addedAt: 1111,
|
|
421
|
+
lastUsed: 0,
|
|
422
|
+
enabled: true,
|
|
423
|
+
rateLimitResetTimes: {},
|
|
424
|
+
consecutiveFailures: 0,
|
|
425
|
+
lastFailureTime: null,
|
|
426
|
+
stats: createDefaultStats(1111),
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
activeIndex: 0,
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const storage = {
|
|
434
|
+
version: 1,
|
|
435
|
+
accounts: [
|
|
436
|
+
{
|
|
437
|
+
id: "legacy-a1",
|
|
438
|
+
refreshToken: "old-refresh",
|
|
439
|
+
access: "old-access",
|
|
440
|
+
expires: 111,
|
|
441
|
+
token_updated_at: 1000,
|
|
442
|
+
addedAt: 1111,
|
|
443
|
+
lastUsed: 0,
|
|
444
|
+
enabled: true,
|
|
445
|
+
rateLimitResetTimes: {},
|
|
446
|
+
consecutiveFailures: 0,
|
|
447
|
+
lastFailureTime: null,
|
|
448
|
+
stats: createDefaultStats(1111),
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
activeIndex: 0,
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
await saveAccounts(storage);
|
|
455
|
+
|
|
456
|
+
const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
|
|
457
|
+
expect(written.accounts[0].refreshToken).toBe("disk-refresh-rotated");
|
|
458
|
+
expect(written.accounts[0].token_updated_at).toBe(3000);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("does not resurrect accounts removed by caller", async () => {
|
|
462
|
+
fs.readFile.mockResolvedValue(
|
|
463
|
+
JSON.stringify({
|
|
464
|
+
version: 1,
|
|
465
|
+
accounts: [
|
|
466
|
+
{
|
|
467
|
+
id: "a1",
|
|
468
|
+
refreshToken: "disk-refresh",
|
|
469
|
+
token_updated_at: 2000,
|
|
470
|
+
addedAt: 1000,
|
|
471
|
+
lastUsed: 0,
|
|
472
|
+
enabled: true,
|
|
473
|
+
rateLimitResetTimes: {},
|
|
474
|
+
consecutiveFailures: 0,
|
|
475
|
+
lastFailureTime: null,
|
|
476
|
+
stats: createDefaultStats(1000),
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
activeIndex: 0,
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
await saveAccounts({ version: 1, accounts: [], activeIndex: 0 });
|
|
484
|
+
|
|
485
|
+
const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
|
|
486
|
+
expect(written.accounts).toEqual([]);
|
|
487
|
+
});
|
|
355
488
|
});
|
|
356
489
|
|
|
357
490
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-anthropic-fix",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"main": "./index.mjs",
|
|
5
|
+
"files": [
|
|
6
|
+
"index.mjs",
|
|
7
|
+
"lib/**/*.mjs",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
5
11
|
"scripts": {
|
|
6
12
|
"build": "node scripts/build.mjs",
|
|
7
13
|
"test": "vitest run",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
bump:
|
|
7
|
-
description: "Version bump type"
|
|
8
|
-
required: false
|
|
9
|
-
default: "patch"
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
id-token: write
|
|
13
|
-
contents: read
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
publish:
|
|
17
|
-
runs-on: ubuntu-latest
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v4
|
|
20
|
-
|
|
21
|
-
- uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: "24"
|
|
24
|
-
registry-url: "https://registry.npmjs.org"
|
|
25
|
-
|
|
26
|
-
- run: npm install
|
|
27
|
-
|
|
28
|
-
- run: npm publish --access public
|
|
29
|
-
env:
|
|
30
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
31
|
-
NPM_CONFIG_PROVENANCE: false
|
package/.husky/pre-commit
DELETED
package/.husky/pre-push
DELETED