oc-codex-multi-auth 6.1.9 → 6.2.0

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/package.json CHANGED
@@ -1,150 +1,150 @@
1
- {
2
- "name": "oc-codex-multi-auth",
3
- "version": "6.1.9",
4
- "description": "OpenCode plugin for ChatGPT Plus/Pro OAuth with Codex/GPT-5 routing, multi-account rotation, account switching, health checks, diagnostics, and recovery tools",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js"
11
- },
12
- "./tui": {
13
- "types": "./dist/tui.d.ts",
14
- "import": "./dist/tui.js"
15
- }
16
- },
17
- "type": "module",
18
- "license": "MIT",
19
- "author": "ndycode",
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/ndycode/oc-codex-multi-auth.git"
23
- },
24
- "keywords": [
25
- "opencode",
26
- "opencode-plugin",
27
- "opencode-auth",
28
- "openai",
29
- "chatgpt",
30
- "chatgpt-plus",
31
- "chatgpt-pro",
32
- "oauth",
33
- "oauth2",
34
- "oauth-login",
35
- "pkce",
36
- "codex",
37
- "codex-auth",
38
- "codex-oauth",
39
- "gpt-5",
40
- "gpt-5-5",
41
- "multi-account",
42
- "account-rotation",
43
- "account-switching",
44
- "account-health",
45
- "health-checks",
46
- "quota-management",
47
- "diagnostics",
48
- "recovery-tools",
49
- "terminal-ui",
50
- "developer-tools",
51
- "typescript",
52
- "nodejs"
53
- ],
54
- "homepage": "https://github.com/ndycode/oc-codex-multi-auth#readme",
55
- "bugs": {
56
- "url": "https://github.com/ndycode/oc-codex-multi-auth/issues"
57
- },
58
- "scripts": {
59
- "build": "node scripts/clean-dist.js && tsc && node scripts/copy-oauth-success.js",
60
- "typecheck": "tsc --noEmit",
61
- "lint": "npm run lint:ts && npm run lint:scripts",
62
- "lint:ts": "eslint . --ext .ts",
63
- "lint:scripts": "eslint scripts --ext .js",
64
- "lint:fix": "npm run lint:ts:fix && npm run lint:scripts:fix",
65
- "lint:ts:fix": "eslint . --ext .ts --fix",
66
- "lint:scripts:fix": "eslint scripts --ext .js --fix",
67
- "test": "vitest run",
68
- "test:watch": "vitest",
69
- "test:ui": "vitest --ui",
70
- "test:coverage": "vitest run --coverage",
71
- "coverage": "vitest run --coverage",
72
- "audit:prod": "npm audit --omit=dev --audit-level=high",
73
- "audit:all": "npm audit --audit-level=high",
74
- "audit:dev:allowlist": "node scripts/audit-dev-allowlist.js",
75
- "audit:ci": "npm run audit:prod && npm run audit:dev:allowlist",
76
- "prepublishOnly": "npm run build",
77
- "prepare": "husky"
78
- },
79
- "bin": {
80
- "oc-codex-multi-auth": "scripts/install-oc-codex-multi-auth.js"
81
- },
82
- "files": [
83
- "dist/",
84
- "assets/",
85
- "config/",
86
- "scripts/",
87
- "README.md",
88
- "LICENSE"
89
- ],
90
- "lint-staged": {
91
- "*.ts": [
92
- "eslint --max-warnings=0 --fix --no-warn-ignored"
93
- ],
94
- "scripts/**/*.js": [
95
- "eslint --max-warnings=0 --fix --no-warn-ignored"
96
- ]
97
- },
98
- "engines": {
99
- "node": ">=18.0.0"
100
- },
101
- "peerDependencies": {
102
- "typescript": "^5"
103
- },
104
- "devDependencies": {
105
- "@fast-check/vitest": "^0.2.4",
106
- "@opencode-ai/sdk": "^1.2.10",
107
- "@types/node": "^25.3.0",
108
- "@typescript-eslint/eslint-plugin": "^8.56.0",
109
- "@typescript-eslint/parser": "^8.56.0",
110
- "@vitest/coverage-v8": "^4.0.18",
111
- "@vitest/ui": "^4.0.18",
112
- "eslint": "^10.0.0",
113
- "fast-check": "^4.5.3",
114
- "husky": "^9.1.7",
115
- "lint-staged": "^16.2.7",
116
- "typescript": "^5.9.3",
117
- "typescript-language-server": "^5.1.3",
118
- "vitest": "^4.0.18"
119
- },
120
- "dependencies": {
121
- "@napi-rs/keyring": "~1.2.0",
122
- "@openauthjs/openauth": "^0.4.3",
123
- "@opencode-ai/plugin": "^1.14.22",
124
- "@opentui/core": "0.2.6",
125
- "@opentui/solid": "0.2.6",
126
- "hono": "4.12.18",
127
- "solid-js": "^1.9.11",
128
- "web-tree-sitter": "^0.25.10",
129
- "zod": "^4.3.6"
130
- },
131
- "overrides": {
132
- "flatted": "3.4.2",
133
- "hono": "4.12.18",
134
- "rollup": "4.60.0",
135
- "vite": "^7.3.2",
136
- "yaml": "^2.8.3",
137
- "ajv@<6.14.0": "^6.14.0",
138
- "brace-expansion@>=2.0.0 <=2.0.2": "^2.0.3",
139
- "minimatch@<9.0.7": "^9.0.7",
140
- "minimatch@>=10.0.0 <10.2.3": "^10.2.3",
141
- "picomatch@<2.3.2": "^2.3.2",
142
- "picomatch@>=4.0.0 <4.0.4": "^4.0.4",
143
- "@typescript-eslint/typescript-estree": {
144
- "minimatch": "^9.0.7"
145
- },
146
- "uuid": "13.0.1",
147
- "diff": "8.0.3",
148
- "file-type": "21.3.1"
149
- }
150
- }
1
+ {
2
+ "name": "oc-codex-multi-auth",
3
+ "version": "6.2.0",
4
+ "description": "OpenCode plugin for ChatGPT Plus/Pro OAuth with Codex/GPT-5 routing, multi-account rotation, account switching, health checks, diagnostics, and recovery tools",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./tui": {
13
+ "types": "./dist/tui.d.ts",
14
+ "import": "./dist/tui.js"
15
+ }
16
+ },
17
+ "type": "module",
18
+ "license": "MIT",
19
+ "author": "ndycode",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ndycode/oc-codex-multi-auth.git"
23
+ },
24
+ "keywords": [
25
+ "opencode",
26
+ "opencode-plugin",
27
+ "opencode-auth",
28
+ "openai",
29
+ "chatgpt",
30
+ "chatgpt-plus",
31
+ "chatgpt-pro",
32
+ "oauth",
33
+ "oauth2",
34
+ "oauth-login",
35
+ "pkce",
36
+ "codex",
37
+ "codex-auth",
38
+ "codex-oauth",
39
+ "gpt-5",
40
+ "gpt-5-5",
41
+ "multi-account",
42
+ "account-rotation",
43
+ "account-switching",
44
+ "account-health",
45
+ "health-checks",
46
+ "quota-management",
47
+ "diagnostics",
48
+ "recovery-tools",
49
+ "terminal-ui",
50
+ "developer-tools",
51
+ "typescript",
52
+ "nodejs"
53
+ ],
54
+ "homepage": "https://github.com/ndycode/oc-codex-multi-auth#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/ndycode/oc-codex-multi-auth/issues"
57
+ },
58
+ "scripts": {
59
+ "build": "node scripts/clean-dist.js && tsc && node scripts/copy-oauth-success.js",
60
+ "typecheck": "tsc --noEmit",
61
+ "lint": "npm run lint:ts && npm run lint:scripts",
62
+ "lint:ts": "eslint . --ext .ts",
63
+ "lint:scripts": "eslint scripts --ext .js",
64
+ "lint:fix": "npm run lint:ts:fix && npm run lint:scripts:fix",
65
+ "lint:ts:fix": "eslint . --ext .ts --fix",
66
+ "lint:scripts:fix": "eslint scripts --ext .js --fix",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
69
+ "test:ui": "vitest --ui",
70
+ "test:coverage": "vitest run --coverage",
71
+ "coverage": "vitest run --coverage",
72
+ "audit:prod": "npm audit --omit=dev --audit-level=high",
73
+ "audit:all": "npm audit --audit-level=high",
74
+ "audit:dev:allowlist": "node scripts/audit-dev-allowlist.js",
75
+ "audit:ci": "npm run audit:prod && npm run audit:dev:allowlist",
76
+ "prepublishOnly": "npm run build",
77
+ "prepare": "husky"
78
+ },
79
+ "bin": {
80
+ "oc-codex-multi-auth": "scripts/install-oc-codex-multi-auth.js"
81
+ },
82
+ "files": [
83
+ "dist/",
84
+ "assets/",
85
+ "config/",
86
+ "scripts/",
87
+ "README.md",
88
+ "LICENSE"
89
+ ],
90
+ "lint-staged": {
91
+ "*.ts": [
92
+ "eslint --max-warnings=0 --fix --no-warn-ignored"
93
+ ],
94
+ "scripts/**/*.js": [
95
+ "eslint --max-warnings=0 --fix --no-warn-ignored"
96
+ ]
97
+ },
98
+ "engines": {
99
+ "node": ">=18.0.0"
100
+ },
101
+ "peerDependencies": {
102
+ "typescript": "^5"
103
+ },
104
+ "devDependencies": {
105
+ "@fast-check/vitest": "^0.2.4",
106
+ "@opencode-ai/sdk": "^1.2.10",
107
+ "@types/node": "^25.3.0",
108
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
109
+ "@typescript-eslint/parser": "^8.56.0",
110
+ "@vitest/coverage-v8": "^4.0.18",
111
+ "@vitest/ui": "^4.0.18",
112
+ "eslint": "^10.0.0",
113
+ "fast-check": "^4.5.3",
114
+ "husky": "^9.1.7",
115
+ "lint-staged": "^16.2.7",
116
+ "typescript": "^5.9.3",
117
+ "typescript-language-server": "^5.1.3",
118
+ "vitest": "^4.0.18"
119
+ },
120
+ "dependencies": {
121
+ "@napi-rs/keyring": "~1.2.0",
122
+ "@openauthjs/openauth": "^0.4.3",
123
+ "@opencode-ai/plugin": "^1.14.22",
124
+ "@opentui/core": "0.2.6",
125
+ "@opentui/solid": "0.2.6",
126
+ "hono": "4.12.18",
127
+ "solid-js": "^1.9.11",
128
+ "web-tree-sitter": "^0.25.10",
129
+ "zod": "^4.3.6"
130
+ },
131
+ "overrides": {
132
+ "flatted": "3.4.2",
133
+ "hono": "4.12.18",
134
+ "rollup": "4.60.0",
135
+ "vite": "^7.3.2",
136
+ "yaml": "^2.8.3",
137
+ "ajv@<6.14.0": "^6.14.0",
138
+ "brace-expansion@>=2.0.0 <=2.0.2": "^2.0.3",
139
+ "minimatch@<9.0.7": "^9.0.7",
140
+ "minimatch@>=10.0.0 <10.2.3": "^10.2.3",
141
+ "picomatch@<2.3.2": "^2.3.2",
142
+ "picomatch@>=4.0.0 <4.0.4": "^4.0.4",
143
+ "@typescript-eslint/typescript-estree": {
144
+ "minimatch": "^9.0.7"
145
+ },
146
+ "uuid": "13.0.1",
147
+ "diff": "8.0.3",
148
+ "file-type": "21.3.1"
149
+ }
150
+ }
@@ -13,6 +13,43 @@ const STALE_MANAGED_MODEL_KEYS = new Set([
13
13
  "gpt-5.3-codex",
14
14
  "gpt-5.4",
15
15
  ]);
16
+ const STANDALONE_COMMANDS = new Set(["doctor", "status", "list", "limits", "dashboard", "health", "diag"]);
17
+ const INSTALLER_COMMANDS = new Set(["install"]);
18
+
19
+ function splitCommandArgv(argv) {
20
+ const [first, ...rest] = argv;
21
+ if (!first) return { kind: "install", argv };
22
+ if (INSTALLER_COMMANDS.has(first)) return { kind: "install", argv: rest };
23
+ if (STANDALONE_COMMANDS.has(first)) return { kind: "standalone", command: first, argv: rest };
24
+ if (first.startsWith("-")) return { kind: "install", argv };
25
+ return { kind: "unknown", command: first, argv: rest };
26
+ }
27
+
28
+ function parseStandaloneArgs(argv) {
29
+ const options = {
30
+ json: false,
31
+ includeSensitive: false,
32
+ deep: false,
33
+ fix: false,
34
+ tag: undefined,
35
+ configPath: undefined,
36
+ help: false,
37
+ };
38
+ for (let index = 0; index < argv.length; index += 1) {
39
+ const arg = argv[index];
40
+ if (arg === "--json") options.json = true;
41
+ else if (arg === "--include-sensitive") options.includeSensitive = true;
42
+ else if (arg === "--deep") options.deep = true;
43
+ else if (arg === "--fix") options.fix = true;
44
+ else if (arg === "--tag") options.tag = argv[++index];
45
+ else if (arg.startsWith("--tag=")) options.tag = arg.slice("--tag=".length);
46
+ else if (arg === "--config-path") options.configPath = argv[++index];
47
+ else if (arg.startsWith("--config-path=")) options.configPath = arg.slice("--config-path=".length);
48
+ else if (arg === "--help" || arg === "-h") options.help = true;
49
+ else throw new Error(`Unknown option for standalone command: ${arg}`);
50
+ }
51
+ return options;
52
+ }
16
53
 
17
54
  function getManagedPackageNames() {
18
55
  return [PACKAGE_NAME, ...LEGACY_PACKAGE_NAMES];
@@ -37,7 +74,17 @@ export function isDirectRunPath(argvPath, modulePath, resolveRealPath = realpath
37
74
  }
38
75
 
39
76
  function printHelp() {
40
- console.log(`Usage: ${PACKAGE_NAME} [--modern|--full|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
77
+ console.log(`Usage: ${PACKAGE_NAME} [command] [options]\n\n` +
78
+ "Commands:\n" +
79
+ " install Install/update OpenCode config (default with no command)\n" +
80
+ " doctor Run local account/config diagnostics\n" +
81
+ " status Show account/config status\n" +
82
+ " list List configured accounts\n" +
83
+ " limits Show stored rate-limit state\n" +
84
+ " dashboard Print dashboard guidance\n" +
85
+ " health Check local token/account health\n" +
86
+ " diag Alias for doctor --deep\n\n" +
87
+ `Installer usage: ${PACKAGE_NAME} [--modern|--full|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
41
88
  "Default behavior:\n" +
42
89
  " - Installs/updates global config at ~/.config/opencode/opencode.json\n" +
43
90
  " - Enables the prompt status bar TUI plugin at ~/.config/opencode/tui.json\n" +
@@ -173,6 +220,183 @@ function formatJson(obj) {
173
220
  return `${JSON.stringify(obj, null, 2)}\n`;
174
221
  }
175
222
 
223
+ function getStandaloneStoragePath(options, env = process.env) {
224
+ if (options.configPath) return resolve(options.configPath);
225
+ return join(resolveHomeDirectory(env), ".opencode", "oc-codex-multi-auth-accounts.json");
226
+ }
227
+
228
+ async function readStandaloneStorage(path) {
229
+ try {
230
+ const raw = await readFile(path, "utf-8");
231
+ const parsed = JSON.parse(raw);
232
+ return {
233
+ storage: parsed && typeof parsed === "object" ? normalizeStandaloneStorage(parsed) : null,
234
+ error: null,
235
+ };
236
+ } catch (error) {
237
+ if (error?.code === "ENOENT") return { storage: null, error: null };
238
+ return { storage: null, error: formatErrorForLog(error) };
239
+ }
240
+ }
241
+
242
+ function normalizeStandaloneIdentityPart(value) {
243
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
244
+ }
245
+
246
+ function sameStandaloneIdentity(left, right) {
247
+ const normalizedLeft = normalizeStandaloneIdentityPart(left);
248
+ const normalizedRight = normalizeStandaloneIdentityPart(right);
249
+ return !!normalizedLeft && !!normalizedRight && normalizedLeft === normalizedRight;
250
+ }
251
+
252
+ function isStandaloneOrgTokenDuplicate(left, right) {
253
+ const leftOrganizationId = normalizeStandaloneIdentityPart(left?.organizationId);
254
+ const rightOrganizationId = normalizeStandaloneIdentityPart(right?.organizationId);
255
+ if (leftOrganizationId && rightOrganizationId && leftOrganizationId !== rightOrganizationId) return false;
256
+ const leftOrgLike = !!leftOrganizationId || left?.accountIdSource === "org";
257
+ const rightOrgLike = !!rightOrganizationId || right?.accountIdSource === "org";
258
+ const leftTokenLike = !leftOrganizationId && left?.accountIdSource === "token";
259
+ const rightTokenLike = !rightOrganizationId && right?.accountIdSource === "token";
260
+ if (!((leftOrgLike && rightTokenLike) || (rightOrgLike && leftTokenLike))) return false;
261
+ return sameStandaloneIdentity(left?.email, right?.email) ||
262
+ sameStandaloneIdentity(left?.refreshToken, right?.refreshToken);
263
+ }
264
+
265
+ function mergeStandaloneAccounts(target, source) {
266
+ const targetOrgLike = !!normalizeStandaloneIdentityPart(target?.organizationId) || target?.accountIdSource === "org";
267
+ const sourceOrgLike = !!normalizeStandaloneIdentityPart(source?.organizationId) || source?.accountIdSource === "org";
268
+ if (targetOrgLike || !sourceOrgLike) {
269
+ return {
270
+ ...source,
271
+ ...target,
272
+ organizationId: target.organizationId ?? source.organizationId,
273
+ accountId: target.accountId ?? source.accountId,
274
+ accountIdSource: target.accountIdSource ?? source.accountIdSource,
275
+ accountLabel: target.accountLabel ?? source.accountLabel,
276
+ email: target.email ?? source.email,
277
+ };
278
+ }
279
+ return mergeStandaloneAccounts(source, target);
280
+ }
281
+
282
+ function normalizeStandaloneStorage(storage) {
283
+ if (!Array.isArray(storage.accounts)) return storage;
284
+ const accounts = [...storage.accounts];
285
+ const removed = new Set();
286
+ for (let i = 0; i < accounts.length; i += 1) {
287
+ if (removed.has(i)) continue;
288
+ for (let j = i + 1; j < accounts.length; j += 1) {
289
+ if (removed.has(j) || !isStandaloneOrgTokenDuplicate(accounts[i], accounts[j])) continue;
290
+ const leftOrgLike = !!normalizeStandaloneIdentityPart(accounts[i]?.organizationId) ||
291
+ accounts[i]?.accountIdSource === "org";
292
+ const targetIndex = leftOrgLike ? i : j;
293
+ const sourceIndex = targetIndex === i ? j : i;
294
+ accounts[targetIndex] = mergeStandaloneAccounts(accounts[targetIndex], accounts[sourceIndex]);
295
+ removed.add(sourceIndex);
296
+ if (sourceIndex === i) break;
297
+ }
298
+ }
299
+ const normalizedAccounts = accounts.filter((_, index) => !removed.has(index));
300
+ return {
301
+ ...storage,
302
+ accounts: normalizedAccounts,
303
+ activeIndex: Math.max(0, Math.min(storage.activeIndex ?? 0, Math.max(0, normalizedAccounts.length - 1))),
304
+ };
305
+ }
306
+
307
+ function maskValue(value, includeSensitive) {
308
+ if (includeSensitive || typeof value !== "string" || value.length <= 8) return value;
309
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
310
+ }
311
+
312
+ function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
313
+ const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
314
+ const normalizedTag = typeof tag === "string" ? tag.trim().toLowerCase() : "";
315
+ return accounts
316
+ .map((account, index) => ({ account, index }))
317
+ .filter(({ account }) => !normalizedTag ||
318
+ (Array.isArray(account?.accountTags) &&
319
+ account.accountTags.some((entry) => String(entry).toLowerCase() === normalizedTag)))
320
+ .map(({ account, index }) => ({
321
+ index,
322
+ label: account?.accountLabel ?? `Account ${index + 1}`,
323
+ email: maskValue(account?.email, includeSensitive),
324
+ accountId: maskValue(account?.accountId, includeSensitive),
325
+ accountIdSource: account?.accountIdSource,
326
+ enabled: account?.enabled !== false,
327
+ hasRefreshToken: typeof account?.refreshToken === "string" && account.refreshToken.length > 0,
328
+ hasAccessToken: typeof account?.accessToken === "string" && account.accessToken.length > 0,
329
+ expiresAt: account?.expiresAt,
330
+ expired: typeof account?.expiresAt === "number" ? account.expiresAt <= Date.now() : undefined,
331
+ tags: Array.isArray(account?.accountTags) ? account.accountTags : [],
332
+ note: account?.accountNote,
333
+ rateLimitResetTimes: account?.rateLimitResetTimes ?? {},
334
+ }));
335
+ }
336
+
337
+ function printStandaloneResult(command, payload, json) {
338
+ if (json) {
339
+ console.log(JSON.stringify(payload, null, 2));
340
+ return;
341
+ }
342
+ console.log(`oc-codex-multi-auth ${command}`);
343
+ if (payload.message) console.log(payload.message);
344
+ console.log(`Storage: ${payload.storagePath}`);
345
+ console.log(`Accounts: ${payload.totalAccounts}`);
346
+ if (Array.isArray(payload.accounts)) {
347
+ for (const account of payload.accounts) {
348
+ console.log(`- [${account.index}] ${account.label} enabled=${account.enabled} refresh=${account.hasRefreshToken} access=${account.hasAccessToken}`);
349
+ }
350
+ }
351
+ if (payload.error) console.log(`Error: ${payload.error}`);
352
+ if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
353
+ }
354
+
355
+ export async function runStandaloneCommand(command, argv = [], options = {}) {
356
+ const parsed = parseStandaloneArgs(argv);
357
+ if (command === "diag") {
358
+ command = "doctor";
359
+ parsed.deep = true;
360
+ }
361
+ if (parsed.help) {
362
+ printHelp();
363
+ return { exitCode: 0, action: "help" };
364
+ }
365
+ const { env = process.env } = options;
366
+ const storagePath = getStandaloneStoragePath(parsed, env);
367
+ const { storage, error } = await readStandaloneStorage(storagePath);
368
+ const accounts = summarizeStandaloneAccounts(storage, parsed.includeSensitive, parsed.tag);
369
+ const totalAccounts = Array.isArray(storage?.accounts) ? storage.accounts.length : 0;
370
+ const payload = {
371
+ command,
372
+ storagePath,
373
+ totalAccounts,
374
+ shownAccounts: accounts.length,
375
+ activeIndex: typeof storage?.activeIndex === "number" ? storage.activeIndex : 0,
376
+ activeIndexByFamily: storage?.activeIndexByFamily ?? {},
377
+ accounts,
378
+ error,
379
+ };
380
+ if (command === "dashboard") {
381
+ payload.message = "Standalone dashboard server is not launched by this safe CLI; use status/list/limits/health or OpenCode codex-dashboard.";
382
+ payload.nextAction = "Run oc-codex-multi-auth status or open OpenCode and call codex-dashboard.";
383
+ } else if (command === "doctor") {
384
+ payload.message = error ? "Storage could not be parsed." : totalAccounts > 0 ? "Local diagnostics completed." : "No accounts configured.";
385
+ payload.deep = parsed.deep;
386
+ payload.fixApplied = parsed.fix ? false : undefined;
387
+ payload.nextAction = totalAccounts > 0 ? "Run oc-codex-multi-auth health --json for scriptable checks." : "Run opencode auth login.";
388
+ } else if (command === "limits") {
389
+ payload.rateLimits = accounts.map((account) => ({ index: account.index, rateLimitResetTimes: account.rateLimitResetTimes }));
390
+ } else if (command === "health") {
391
+ payload.healthyCount = accounts.filter((account) => account.enabled && account.hasRefreshToken).length;
392
+ payload.unhealthyCount = accounts.filter((account) => !account.enabled || !account.hasRefreshToken).length;
393
+ } else if (command === "status") {
394
+ payload.message = totalAccounts > 0 ? "Account storage loaded." : "No accounts configured.";
395
+ }
396
+ printStandaloneResult(command, payload, parsed.json);
397
+ return { exitCode: error ? 1 : 0, action: command, storagePath };
398
+ }
399
+
176
400
  // Top-level keys inside `provider.openai` that the installer owns absolutely.
177
401
  // These are always sourced from the template (overwritten or removed) so the
178
402
  // plugin's required runtime shape is authoritative. Any OTHER key the user has
@@ -443,7 +667,15 @@ async function clearCache(paths, dryRun, skipCacheClear) {
443
667
  }
444
668
 
445
669
  export async function runInstaller(argv = process.argv.slice(2), options = {}) {
446
- const parsed = parseCliArgs(argv);
670
+ const split = splitCommandArgv(argv);
671
+ if (split.kind === "standalone") {
672
+ return runStandaloneCommand(split.command, split.argv, options);
673
+ }
674
+ if (split.kind === "unknown") {
675
+ printHelp();
676
+ throw new Error(`Unknown command: ${split.command}`);
677
+ }
678
+ const parsed = parseCliArgs(split.argv);
447
679
  if (parsed.wantsHelp) {
448
680
  printHelp();
449
681
  return { exitCode: 0, action: "help" };
@@ -575,6 +807,8 @@ export const __test = {
575
807
  mergeOpenaiProvider,
576
808
  mergeTuiConfig,
577
809
  parseCliArgs,
810
+ runStandaloneCommand,
811
+ splitCommandArgv,
578
812
  writeFileAtomic,
579
813
  renameWithWindowsRetry,
580
814
  resolveHomeDirectory,