Skip to content

Commit 8e4fddb

Browse files
authored
feat: auto-refresh opencode plugin cache
1 parent 36719f9 commit 8e4fddb

10 files changed

Lines changed: 167 additions & 9 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ What the installer does:
4141
- normalizes the plugin entry to `"oc-codex-multi-auth"`
4242
- clears the cached plugin copy so OpenCode reinstalls the latest package
4343

44+
After install, the plugin checks npm once per day. When a newer version exists, it clears its OpenCode-managed cached package on exit; restart OpenCode and the latest package is installed automatically. Disable this with `"autoUpdate": false` in `~/.opencode/openai-codex-auth-config.json` or `CODEX_AUTH_AUTO_UPDATE=0`.
45+
4446
By default, the installer writes the compact UI config:
4547
- model picker entries stay on actual OAuth model families such as `gpt-5.5` and `gpt-5.5-fast`
4648
- reasoning presets are selected through OpenCode's model variant picker (`none`, `low`, `medium`, `high`, `xhigh`)
@@ -86,6 +88,7 @@ OpenCode users often want the same GPT-5 and Codex model experience they use in
8688
- Beginner-focused commands such as `codex-setup`, `codex-help`, `codex-doctor`, and `codex-next`
8789
- Interactive account switching, labeling, tagging, and backup/import commands
8890
- Stateless request handling with `reasoning.encrypted_content` for multi-turn sessions
91+
- Daily npm update detection with OpenCode cache refresh on restart
8992
- Request logging and troubleshooting hooks for debugging OpenCode integration issues
9093

9194
## Common Workflows

docs/configuration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ advanced settings go in `~/.opencode/openai-codex-auth-config.json`:
131131
"server": 2
132132
},
133133
"perProjectAccounts": true,
134+
"autoUpdate": true,
134135
"toastDurationMs": 5000,
135136
"retryAllAccountsRateLimited": true,
136137
"retryAllAccountsMaxWaitMs": 0,
@@ -163,6 +164,7 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound
163164
| `retryProfile` | `balanced` | retry budget profile for request classes (`conservative`, `balanced`, `aggressive`) |
164165
| `retryBudgetOverrides` | `{}` | optional per-class budget overrides (`authRefresh`, `network`, `server`, `rateLimitShort`, `rateLimitGlobal`, `emptyResponse`) |
165166
| `perProjectAccounts` | `true` | each project gets its own account storage |
167+
| `autoUpdate` | `true` | check npm daily and clear the OpenCode-managed plugin cache on exit when a newer version is available; restart OpenCode to install it |
166168
| `toastDurationMs` | `5000` | how long toast notifications stay visible (ms) |
167169
| `retryAllAccountsRateLimited` | `true` | wait and retry when all accounts hit rate limits |
168170
| `retryAllAccountsMaxWaitMs` | `0` | max wait time in ms (0 = unlimited) |
@@ -244,6 +246,7 @@ override any config with env vars:
244246
| `CODEX_AUTH_BEGINNER_SAFE_MODE=1` | enable beginner-safe retry behavior |
245247
| `CODEX_AUTH_RETRY_PROFILE=aggressive` | override retry profile (`conservative`, `balanced`, `aggressive`) |
246248
| `CODEX_AUTH_PER_PROJECT_ACCOUNTS=0` | disable per-project accounts |
249+
| `CODEX_AUTH_AUTO_UPDATE=0` | disable automatic OpenCode plugin cache refresh when npm has a newer plugin version |
247250
| `CODEX_AUTH_TOAST_DURATION_MS=8000` | set toast duration |
248251
| `CODEX_AUTH_RETRY_ALL_RATE_LIMITED=0` | disable wait-and-retry |
249252
| `CODEX_AUTH_RETRY_ALL_MAX_WAIT_MS=30000` | set max wait time |

index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
getTokenRefreshSkewMs,
7171
getSessionRecovery,
7272
getAutoResume,
73+
getAutoUpdate,
7374
getToastDurationMs,
7475
getPerProjectAccounts,
7576
getEmptyResponseMaxRetries,
@@ -1545,6 +1546,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
15451546

15461547
const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
15471548
const autoResumeEnabled = getAutoResume(pluginConfig);
1549+
const autoUpdateEnabled = getAutoUpdate(pluginConfig);
15481550
const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
15491551
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
15501552
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
@@ -1591,7 +1593,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
15911593

15921594
checkAndNotify(async (message, variant) => {
15931595
await showToast(message, variant);
1594-
}).catch((err) => {
1596+
}, { autoUpdate: autoUpdateEnabled }).catch((err) => {
15951597
logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
15961598
});
15971599
await runStartupPreflight();

lib/auto-update-checker.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
1+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
22
import { join } from "node:path";
33
import { homedir } from "node:os";
44
import { createLogger } from "./logger.js";
55

66
const log = createLogger("update-checker");
77

88
const PACKAGE_NAME = "oc-codex-multi-auth";
9+
const LEGACY_PACKAGE_NAMES = ["oc-chatgpt-multi-auth"];
910
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1011
const CACHE_DIR = join(homedir(), ".opencode", "cache");
1112
const CACHE_FILE = join(CACHE_DIR, "update-check-cache.json");
13+
const OPENCODE_CACHE_DIR = join(homedir(), ".cache", "opencode");
1214
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
15+
let cacheEvictionScheduled = false;
1316

1417
interface UpdateCheckCache {
1518
lastCheck: number;
@@ -98,6 +101,52 @@ export interface UpdateCheckResult {
98101
updateCommand: string;
99102
}
100103

104+
export interface CheckAndNotifyOptions {
105+
autoUpdate?: boolean;
106+
scheduleCacheClear?: () => boolean;
107+
}
108+
109+
function getManagedPackageNames(): string[] {
110+
return [PACKAGE_NAME, ...LEGACY_PACKAGE_NAMES];
111+
}
112+
113+
function getManagedCachePaths(): string[] {
114+
return getManagedPackageNames().flatMap((name) => [
115+
join(OPENCODE_CACHE_DIR, "packages", `${name}@latest`),
116+
join(OPENCODE_CACHE_DIR, "node_modules", name),
117+
]);
118+
}
119+
120+
export function clearManagedOpenCodePluginCache(paths = getManagedCachePaths()): boolean {
121+
let cleared = false;
122+
123+
for (const cachePath of paths) {
124+
try {
125+
if (!existsSync(cachePath)) continue;
126+
rmSync(cachePath, { recursive: true, force: true });
127+
cleared = true;
128+
log.info("Cleared OpenCode plugin cache for update", { path: cachePath });
129+
} catch (error) {
130+
const message = error instanceof Error ? error.message : String(error);
131+
log.warn("Failed to clear OpenCode plugin cache for update", {
132+
path: cachePath,
133+
error: message,
134+
});
135+
}
136+
}
137+
138+
return cleared;
139+
}
140+
141+
export function scheduleOpenCodePluginCacheClearOnExit(): boolean {
142+
if (cacheEvictionScheduled) return true;
143+
cacheEvictionScheduled = true;
144+
process.once("exit", () => {
145+
clearManagedOpenCodePluginCache();
146+
});
147+
return true;
148+
}
149+
101150
export async function checkForUpdates(force = false): Promise<UpdateCheckResult> {
102151
const currentVersion = getCurrentVersion();
103152
const cache = loadCache();
@@ -133,19 +182,24 @@ export async function checkForUpdates(force = false): Promise<UpdateCheckResult>
133182

134183
export async function checkAndNotify(
135184
showToast?: (message: string, variant: "info" | "warning") => Promise<void>,
185+
options: CheckAndNotifyOptions = {},
136186
): Promise<void> {
137187
try {
138188
const result = await checkForUpdates();
139189

140190
if (result.hasUpdate && result.latestVersion) {
141191
const message = `Update available: ${PACKAGE_NAME} v${result.latestVersion} (current: v${result.currentVersion})`;
142192
log.info(message);
193+
const autoUpdate = options.autoUpdate ?? true;
194+
const scheduled = autoUpdate
195+
? (options.scheduleCacheClear ?? scheduleOpenCodePluginCacheClearOnExit)()
196+
: false;
143197

144198
if (showToast) {
145-
await showToast(
146-
`Plugin update available: v${result.latestVersion}. Run: ${result.updateCommand}`,
147-
"info",
148-
);
199+
const instruction = scheduled
200+
? "Restart OpenCode to install it automatically."
201+
: `Run: ${result.updateCommand}`;
202+
await showToast(`Plugin update available: v${result.latestVersion}. ${instruction}`, "info");
149203
}
150204
}
151205
} catch (error) {

lib/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const DEFAULT_CONFIG: PluginConfig = {
5454
perProjectAccounts: true,
5555
sessionRecovery: true,
5656
autoResume: true,
57+
autoUpdate: true,
5758
parallelProbing: false,
5859
parallelProbingMaxConcurrency: 2,
5960
emptyResponseMaxRetries: 2,
@@ -499,6 +500,14 @@ export function getAutoResume(pluginConfig: PluginConfig): boolean {
499500
);
500501
}
501502

503+
export function getAutoUpdate(pluginConfig: PluginConfig): boolean {
504+
return resolveBooleanSetting(
505+
"CODEX_AUTH_AUTO_UPDATE",
506+
pluginConfig.autoUpdate,
507+
true,
508+
);
509+
}
510+
502511
export function getToastDurationMs(pluginConfig: PluginConfig): number {
503512
return resolveNumberSetting(
504513
"CODEX_AUTH_TOAST_DURATION_MS",

lib/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const PluginConfigSchema = z.object({
4545
perProjectAccounts: z.boolean().optional(),
4646
sessionRecovery: z.boolean().optional(),
4747
autoResume: z.boolean().optional(),
48+
autoUpdate: z.boolean().optional(),
4849
parallelProbing: z.boolean().optional(),
4950
parallelProbingMaxConcurrency: z.number().min(1).max(5).optional(),
5051
emptyResponseMaxRetries: z.number().min(0).optional(),

test/auto-update-checker.test.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ vi.mock("node:fs", () => ({
55
writeFileSync: vi.fn(),
66
existsSync: vi.fn(),
77
mkdirSync: vi.fn(),
8+
rmSync: vi.fn(),
89
}));
910

1011
describe("auto-update-checker", () => {
1112
let fs: typeof import("node:fs");
1213
let checkForUpdates: typeof import("../lib/auto-update-checker.js").checkForUpdates;
1314
let checkAndNotify: typeof import("../lib/auto-update-checker.js").checkAndNotify;
1415
let clearUpdateCache: typeof import("../lib/auto-update-checker.js").clearUpdateCache;
16+
let clearManagedOpenCodePluginCache: typeof import("../lib/auto-update-checker.js").clearManagedOpenCodePluginCache;
1517

1618
const mockPackageJson = { version: "4.12.0" };
1719

@@ -21,6 +23,7 @@ describe("auto-update-checker", () => {
2123
vi.setSystemTime(new Date("2026-01-30T12:00:00Z"));
2224

2325
fs = await import("node:fs");
26+
vi.clearAllMocks();
2427
vi.mocked(fs.readFileSync).mockImplementation((path: unknown) => {
2528
if (String(path).includes("package.json")) {
2629
return JSON.stringify(mockPackageJson);
@@ -35,6 +38,7 @@ describe("auto-update-checker", () => {
3538
checkForUpdates = module.checkForUpdates;
3639
checkAndNotify = module.checkAndNotify;
3740
clearUpdateCache = module.clearUpdateCache;
41+
clearManagedOpenCodePluginCache = module.clearManagedOpenCodePluginCache;
3842
});
3943

4044
afterEach(() => {
@@ -233,19 +237,41 @@ describe("auto-update-checker", () => {
233237
});
234238

235239
describe("checkAndNotify", () => {
236-
it("shows toast when update available", async () => {
240+
it("shows restart toast and schedules cache clear when update available", async () => {
237241
vi.mocked(globalThis.fetch).mockResolvedValue({
238242
ok: true,
239243
json: async () => ({ version: "5.0.0" }),
240244
} as Response);
241245
const showToast = vi.fn().mockResolvedValue(undefined);
246+
const scheduleCacheClear = vi.fn(() => true);
242247

243-
await checkAndNotify(showToast);
248+
await checkAndNotify(showToast, { scheduleCacheClear });
249+
250+
expect(showToast).toHaveBeenCalledWith(
251+
expect.stringContaining("Restart OpenCode to install it automatically"),
252+
"info"
253+
);
254+
expect(scheduleCacheClear).toHaveBeenCalledOnce();
255+
});
256+
257+
it("keeps manual update command when autoUpdate is disabled", async () => {
258+
vi.mocked(globalThis.fetch).mockResolvedValue({
259+
ok: true,
260+
json: async () => ({ version: "5.0.0" }),
261+
} as Response);
262+
const showToast = vi.fn().mockResolvedValue(undefined);
263+
const scheduleCacheClear = vi.fn(() => true);
264+
265+
await checkAndNotify(showToast, {
266+
autoUpdate: false,
267+
scheduleCacheClear,
268+
});
244269

245270
expect(showToast).toHaveBeenCalledWith(
246-
expect.stringContaining("v5.0.0"),
271+
expect.stringContaining("Run: npm update -g"),
247272
"info"
248273
);
274+
expect(scheduleCacheClear).not.toHaveBeenCalled();
249275
});
250276

251277
it("does not show toast when no update", async () => {
@@ -301,4 +327,36 @@ describe("auto-update-checker", () => {
301327
expect(fs.writeFileSync).not.toHaveBeenCalled();
302328
});
303329
});
330+
331+
describe("clearManagedOpenCodePluginCache", () => {
332+
it("removes managed OpenCode package cache paths", () => {
333+
vi.mocked(fs.existsSync).mockReturnValue(true);
334+
335+
const cleared = clearManagedOpenCodePluginCache([
336+
"C:\\cache\\packages\\oc-codex-multi-auth@latest",
337+
"C:\\cache\\node_modules\\oc-codex-multi-auth",
338+
]);
339+
340+
expect(cleared).toBe(true);
341+
expect(fs.rmSync).toHaveBeenCalledWith(
342+
"C:\\cache\\packages\\oc-codex-multi-auth@latest",
343+
{ recursive: true, force: true },
344+
);
345+
expect(fs.rmSync).toHaveBeenCalledWith(
346+
"C:\\cache\\node_modules\\oc-codex-multi-auth",
347+
{ recursive: true, force: true },
348+
);
349+
});
350+
351+
it("returns false when no managed cache paths exist", () => {
352+
vi.mocked(fs.existsSync).mockReturnValue(false);
353+
354+
const cleared = clearManagedOpenCodePluginCache([
355+
"C:\\cache\\packages\\oc-codex-multi-auth@latest",
356+
]);
357+
358+
expect(cleared).toBe(false);
359+
expect(fs.rmSync).not.toHaveBeenCalled();
360+
});
361+
});
304362
});

test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ vi.mock("../lib/config.js", () => ({
125125
getTokenRefreshSkewMs: () => 60000,
126126
getSessionRecovery: () => false,
127127
getAutoResume: () => false,
128+
getAutoUpdate: () => true,
128129
getToastDurationMs: () => 5000,
129130
getPerProjectAccounts: () => false,
130131
getEmptyResponseMaxRetries: () => 2,

0 commit comments

Comments
 (0)