opencode-usage 0.4.7 → 0.5.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/dist/index.js CHANGED
@@ -1,5 +1,754 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
4
+
5
+ // src/commander/services/command-runner.ts
6
+ function registerCommand(spec) {
7
+ if (registry.has(spec.id)) {
8
+ throw new Error(`Command "${spec.id}" is already registered`);
9
+ }
10
+ registry.set(spec.id, spec);
11
+ }
12
+ function getJob(jobId) {
13
+ return jobs.get(jobId);
14
+ }
15
+ function runCommand(commandId, payload) {
16
+ const spec = registry.get(commandId);
17
+ if (!spec) {
18
+ throw new Error(`Unknown command: "${commandId}"`);
19
+ }
20
+ const input = spec.validateInput(payload);
21
+ const jobId = crypto.randomUUID();
22
+ const job = {
23
+ id: jobId,
24
+ commandId,
25
+ status: "queued",
26
+ logs: []
27
+ };
28
+ jobs.set(jobId, job);
29
+ const ctx = {
30
+ jobId,
31
+ log(level, message) {
32
+ job.logs.push({ ts: new Date().toISOString(), level, message });
33
+ }
34
+ };
35
+ executeJob(job, ctx, spec, input);
36
+ return jobId;
37
+ }
38
+ async function executeJob(job, ctx, spec, input) {
39
+ job.status = "running";
40
+ job.startedAt = new Date().toISOString();
41
+ ctx.log("info", `Running command "${job.commandId}"`);
42
+ let timedOut = false;
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ if (job.status === "running") {
46
+ job.status = "failed";
47
+ job.finishedAt = new Date().toISOString();
48
+ job.error = {
49
+ code: "TIMEOUT",
50
+ message: `Command "${job.commandId}" timed out after ${spec.timeoutMs}ms`
51
+ };
52
+ ctx.log("error", `Timeout after ${spec.timeoutMs}ms`);
53
+ }
54
+ }, spec.timeoutMs);
55
+ try {
56
+ const result = await spec.run(ctx, input);
57
+ if (timedOut)
58
+ return;
59
+ clearTimeout(timer);
60
+ job.status = "success";
61
+ job.finishedAt = new Date().toISOString();
62
+ job.result = result;
63
+ ctx.log("info", "Command completed successfully");
64
+ } catch (err) {
65
+ if (timedOut)
66
+ return;
67
+ clearTimeout(timer);
68
+ const message = err instanceof Error ? err.message : "Unknown runtime error";
69
+ job.status = "failed";
70
+ job.finishedAt = new Date().toISOString();
71
+ job.error = { code: "RUNTIME_ERROR", message };
72
+ ctx.log("error", message);
73
+ }
74
+ }
75
+ var registry, jobs;
76
+ var init_command_runner = __esm(() => {
77
+ registry = new Map;
78
+ jobs = new Map;
79
+ });
80
+
81
+ // src/commander/services/config-service.ts
82
+ import { homedir as homedir5 } from "os";
83
+ import { join as join7, basename as basename2 } from "path";
84
+ import { rename, mkdir, copyFile, readdir, stat } from "fs/promises";
85
+ import { readFileSync } from "fs";
86
+ function isValidSource(s) {
87
+ return VALID_SOURCES.has(s);
88
+ }
89
+ function configPath(source) {
90
+ return join7(CONFIG_DIR, SOURCE_FILENAMES[source]);
91
+ }
92
+ async function fileExists(path2) {
93
+ try {
94
+ await stat(path2);
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function readFileTextSync(path2) {
101
+ return readFileSync(path2, "utf-8");
102
+ }
103
+ async function writeFileText(path2, content) {
104
+ if (isBun3) {
105
+ await Bun.write(path2, content);
106
+ return;
107
+ }
108
+ const { writeFile } = await import("fs/promises");
109
+ await writeFile(path2, content, "utf-8");
110
+ }
111
+ function getCached(path2) {
112
+ const entry = configCache.get(path2);
113
+ if (entry && Date.now() < entry.expiry)
114
+ return entry.data;
115
+ return;
116
+ }
117
+ function setCache(path2, data) {
118
+ configCache.set(path2, { data, expiry: Date.now() + CONFIG_CACHE_TTL });
119
+ }
120
+ function invalidateCache(source) {
121
+ configCache.delete(configPath(source));
122
+ }
123
+ async function listConfigFiles() {
124
+ const sources = Object.keys(SOURCE_FILENAMES);
125
+ const results = [];
126
+ for (const source of sources) {
127
+ const path2 = configPath(source);
128
+ let exists = false;
129
+ let parseOk = false;
130
+ let sizeBytes = 0;
131
+ try {
132
+ const s = await stat(path2);
133
+ exists = true;
134
+ sizeBytes = s.size;
135
+ const raw = readFileTextSync(path2);
136
+ JSON.parse(raw);
137
+ parseOk = true;
138
+ } catch {
139
+ }
140
+ results.push({ source, path: path2, exists, parseOk, sizeBytes });
141
+ }
142
+ return results;
143
+ }
144
+ function readConfig(source) {
145
+ const path2 = configPath(source);
146
+ const cached = getCached(path2);
147
+ if (cached !== undefined)
148
+ return cached;
149
+ let raw;
150
+ try {
151
+ raw = readFileTextSync(path2);
152
+ } catch {
153
+ throw new ConfigError(`Config file not found: ${path2}`, 404);
154
+ }
155
+ try {
156
+ const result = JSON.parse(raw);
157
+ setCache(path2, result);
158
+ return result;
159
+ } catch {
160
+ throw new ConfigError(`Config file is not valid JSON: ${basename2(path2)}`, 422);
161
+ }
162
+ }
163
+ async function writeConfig(source, data) {
164
+ const path2 = configPath(source);
165
+ const backupPath = await createBackup(source);
166
+ const tmpPath = `${path2}.tmp`;
167
+ const content = JSON.stringify(data, null, 2) + "\n";
168
+ await writeFileText(tmpPath, content);
169
+ await rename(tmpPath, path2);
170
+ invalidateCache(source);
171
+ return { backupPath };
172
+ }
173
+ async function rollbackConfig(source) {
174
+ const latest = await findLatestBackup(source);
175
+ if (!latest) {
176
+ throw new ConfigError(`No backup found for source: ${source}`, 404);
177
+ }
178
+ const path2 = configPath(source);
179
+ const tmpPath = `${path2}.tmp`;
180
+ await copyFile(latest, tmpPath);
181
+ await rename(tmpPath, path2);
182
+ return { restoredFrom: latest };
183
+ }
184
+ async function createBackup(source) {
185
+ const path2 = configPath(source);
186
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
187
+ const backupDir = join7(BACKUP_ROOT, timestamp);
188
+ await mkdir(backupDir, { recursive: true });
189
+ const backupFile = join7(backupDir, SOURCE_FILENAMES[source]);
190
+ if (await fileExists(path2)) {
191
+ await copyFile(path2, backupFile);
192
+ } else {
193
+ await writeFileText(backupFile, "null\n");
194
+ }
195
+ return backupFile;
196
+ }
197
+ async function findLatestBackup(source) {
198
+ if (!await fileExists(BACKUP_ROOT))
199
+ return null;
200
+ const entries = await readdir(BACKUP_ROOT);
201
+ const sorted = entries.sort();
202
+ for (let i = sorted.length - 1;i >= 0; i--) {
203
+ const candidate = join7(BACKUP_ROOT, sorted[i], SOURCE_FILENAMES[source]);
204
+ if (await fileExists(candidate)) {
205
+ return candidate;
206
+ }
207
+ }
208
+ return null;
209
+ }
210
+ var isBun3, CONFIG_DIR, BACKUP_ROOT, SOURCE_FILENAMES, VALID_SOURCES, CONFIG_CACHE_TTL = 2000, configCache, ConfigError;
211
+ var init_config_service = __esm(() => {
212
+ isBun3 = typeof globalThis.Bun !== "undefined";
213
+ CONFIG_DIR = join7(homedir5(), ".config", "opencode");
214
+ BACKUP_ROOT = join7(CONFIG_DIR, "commander-backups");
215
+ SOURCE_FILENAMES = {
216
+ "codex-multi-account-accounts": "codex-multi-account-accounts.json",
217
+ "anthropic-multi-account-state": "anthropic-multi-account-state.json",
218
+ "antigravity-accounts": "antigravity-accounts.json",
219
+ opencode: "opencode.json"
220
+ };
221
+ VALID_SOURCES = new Set(Object.keys(SOURCE_FILENAMES));
222
+ configCache = new Map;
223
+ ConfigError = class ConfigError extends Error {
224
+ status;
225
+ constructor(message, status) {
226
+ super(message);
227
+ this.name = "ConfigError";
228
+ this.status = status;
229
+ }
230
+ };
231
+ });
232
+
233
+ // src/commander/services/plugin-adapters.ts
234
+ var exports_plugin_adapters = {};
235
+ import { homedir as homedir6, tmpdir } from "os";
236
+ import { join as join8 } from "path";
237
+ function resolveSource2(provider) {
238
+ const source = PROVIDER_SOURCE[provider];
239
+ if (!source) {
240
+ throw new Error(`Unknown provider "${provider}". Valid: ${Object.keys(PROVIDER_SOURCE).join(", ")}`);
241
+ }
242
+ return source;
243
+ }
244
+ async function readCredentialFile(filename) {
245
+ const filePath = join8(homedir6(), ".config", "opencode", filename);
246
+ const text = await Bun.file(filePath).text();
247
+ return JSON.parse(text);
248
+ }
249
+ async function spawnPluginCli(command, args, timeoutMs = 15000) {
250
+ const localBin = `./node_modules/${command}/dist/cli.js`;
251
+ const useLocal = await Bun.file(localBin).exists();
252
+ if (!useLocal) {
253
+ const existing = bunxBarrier.get(command);
254
+ if (existing) {
255
+ await existing.catch(() => {
256
+ });
257
+ return runPluginCli(command, args, timeoutMs, false);
258
+ }
259
+ const warmup = runPluginCli(command, args, timeoutMs, false);
260
+ const barrier = warmup.then(() => {
261
+ }, () => {
262
+ });
263
+ bunxBarrier.set(command, barrier);
264
+ barrier.finally(() => bunxBarrier.delete(command));
265
+ return warmup;
266
+ }
267
+ return runPluginCli(command, args, timeoutMs, true);
268
+ }
269
+ async function runPluginCli(command, args, timeoutMs, useLocal) {
270
+ const t0 = Date.now();
271
+ const localBin = `./node_modules/${command}/dist/cli.js`;
272
+ const cmd = useLocal ? ["bun", localBin, ...args] : ["bunx", `${command}@latest`, ...args];
273
+ const cwd = useLocal ? undefined : join8(tmpdir(), `bunx-${crypto.randomUUID()}`);
274
+ if (cwd)
275
+ await Bun.$`mkdir -p ${cwd}`.quiet();
276
+ console.log(`[spawnPluginCli] starting: ${cmd.join(" ")}`);
277
+ const proc = Bun.spawn(cmd, {
278
+ stdout: "pipe",
279
+ stderr: "pipe",
280
+ env: { ...process.env, NO_COLOR: "1" },
281
+ cwd
282
+ });
283
+ const timer = setTimeout(() => {
284
+ console.log(`[spawnPluginCli] killing after ${timeoutMs}ms`);
285
+ proc.kill();
286
+ }, timeoutMs);
287
+ try {
288
+ const [stdout, exitCode] = await Promise.all([
289
+ new Response(proc.stdout).text(),
290
+ proc.exited
291
+ ]);
292
+ const elapsed = Date.now() - t0;
293
+ console.log(`[spawnPluginCli] exited ${exitCode} in ${elapsed}ms, stdout=${stdout.length}b`);
294
+ if (exitCode !== 0) {
295
+ const stderr = await new Response(proc.stderr).text();
296
+ console.log(`[spawnPluginCli] stderr: ${stderr.slice(0, 300)}`);
297
+ throw new Error(`${command} exited with code ${exitCode}: ${(stderr || stdout).slice(0, 300)}`);
298
+ }
299
+ const lines = stdout.trim().split("\n").filter(Boolean);
300
+ for (let i = lines.length - 1;i >= 0; i--) {
301
+ try {
302
+ const parsed = JSON.parse(lines[i]);
303
+ console.log(`[spawnPluginCli] parsed:`, parsed);
304
+ return parsed;
305
+ } catch {
306
+ continue;
307
+ }
308
+ }
309
+ throw new Error(`No JSON output from ${command}: ${stdout.slice(0, 200)}`);
310
+ } finally {
311
+ clearTimeout(timer);
312
+ if (cwd)
313
+ Bun.$`rm -rf ${cwd}`.quiet().catch(() => {
314
+ });
315
+ }
316
+ }
317
+ async function clearStaleMetrics(provider, alias) {
318
+ console.log(`[clearStaleMetrics] checking ${provider}/${alias}`);
319
+ try {
320
+ const source = resolveSource2(provider);
321
+ const data = await readConfig(source);
322
+ const now = Date.now();
323
+ let changed = false;
324
+ switch (provider) {
325
+ case "anthropic": {
326
+ const usage = data.usage ?? {};
327
+ const acct = usage[alias];
328
+ if (acct) {
329
+ for (const key of ["session5h", "weekly7d", "weekly7dSonnet"]) {
330
+ const m2 = acct[key];
331
+ if (m2 && typeof m2.reset === "number" && m2.reset < now / 1000) {
332
+ m2.utilization = 0;
333
+ m2.status = "active";
334
+ delete m2.reset;
335
+ changed = true;
336
+ }
337
+ }
338
+ }
339
+ break;
340
+ }
341
+ case "codex": {
342
+ const accounts = data.accounts ?? {};
343
+ const acct = accounts[alias];
344
+ if (acct?.rateLimits) {
345
+ const rl = acct.rateLimits;
346
+ for (const key of ["fiveHour", "weekly"]) {
347
+ const w2 = rl[key];
348
+ if (w2?.resetAt && typeof w2.resetAt === "string") {
349
+ if (new Date(w2.resetAt).getTime() < now) {
350
+ w2.remaining = w2.limit;
351
+ delete w2.resetAt;
352
+ changed = true;
353
+ }
354
+ }
355
+ }
356
+ }
357
+ break;
358
+ }
359
+ case "antigravity": {
360
+ const accounts = Array.isArray(data.accounts) ? data.accounts : [];
361
+ const acct = accounts.find((a) => a.email === alias);
362
+ if (acct?.cachedQuota) {
363
+ const quota = acct.cachedQuota;
364
+ for (const group of Object.keys(quota)) {
365
+ const q2 = quota[group];
366
+ if (q2.resetTime && typeof q2.resetTime === "string") {
367
+ if (new Date(q2.resetTime).getTime() < now) {
368
+ q2.remainingFraction = 1;
369
+ delete q2.resetTime;
370
+ changed = true;
371
+ }
372
+ }
373
+ }
374
+ }
375
+ break;
376
+ }
377
+ }
378
+ if (changed) {
379
+ console.log(`[clearStaleMetrics] cleared stale data for ${provider}/${alias}`);
380
+ await writeConfig(source, data);
381
+ } else {
382
+ console.log(`[clearStaleMetrics] no stale data found for ${provider}/${alias}`);
383
+ }
384
+ } catch (err) {
385
+ console.log(`[clearStaleMetrics] error for ${provider}/${alias}:`, err);
386
+ }
387
+ }
388
+ function reauthCliCommand(provider) {
389
+ const cmd = REAUTH_PROVIDERS[provider];
390
+ if (!cmd)
391
+ throw new Error(`Re-auth not supported for provider: ${provider}`);
392
+ return cmd;
393
+ }
394
+ var isBun4, PROVIDER_SOURCE, bunxBarrier, REAUTH_PROVIDERS;
395
+ var init_plugin_adapters = __esm(() => {
396
+ init_command_runner();
397
+ init_config_service();
398
+ isBun4 = typeof globalThis.Bun !== "undefined";
399
+ PROVIDER_SOURCE = {
400
+ anthropic: "anthropic-multi-account-state",
401
+ codex: "codex-multi-account-accounts",
402
+ antigravity: "antigravity-accounts"
403
+ };
404
+ registerCommand({
405
+ id: "accounts.add",
406
+ timeoutMs: 30000,
407
+ allowInUi: true,
408
+ validateInput(payload) {
409
+ const p = payload;
410
+ if (!p || typeof p.provider !== "string" || !p.provider) {
411
+ throw new Error("accounts.add requires a non-empty 'provider' string");
412
+ }
413
+ if (typeof p.alias !== "string" || !p.alias) {
414
+ throw new Error("accounts.add requires a non-empty 'alias' string");
415
+ }
416
+ return { provider: p.provider, alias: p.alias };
417
+ },
418
+ async run(ctx, input) {
419
+ ctx.log("info", `Adding account "${input.alias}" for provider "${input.provider}"`);
420
+ ctx.log("info", "OAuth authentication requires terminal interaction \u2014 run this command from the CLI.");
421
+ return {
422
+ message: `Account "${input.alias}" for provider "${input.provider}" requires terminal-based OAuth. Please run: opencode-usage accounts add --provider ${input.provider} --alias ${input.alias}`,
423
+ requiresTerminal: true
424
+ };
425
+ }
426
+ });
427
+ registerCommand({
428
+ id: "accounts.remove",
429
+ timeoutMs: 30000,
430
+ allowInUi: true,
431
+ validateInput(payload) {
432
+ const p = payload;
433
+ if (!p || typeof p.provider !== "string" || !p.provider) {
434
+ throw new Error("accounts.remove requires a non-empty 'provider' string");
435
+ }
436
+ if (typeof p.alias !== "string" || !p.alias) {
437
+ throw new Error("accounts.remove requires a non-empty 'alias' string");
438
+ }
439
+ return { provider: p.provider, alias: p.alias };
440
+ },
441
+ async run(ctx, input) {
442
+ const source = resolveSource2(input.provider);
443
+ ctx.log("info", `Removing account "${input.alias}" from ${source}`);
444
+ const data = await readConfig(source);
445
+ const accounts = data.accounts ?? {};
446
+ delete accounts[input.alias];
447
+ data.accounts = accounts;
448
+ await writeConfig(source, data);
449
+ ctx.log("info", `Account "${input.alias}" removed successfully`);
450
+ return { ok: true };
451
+ }
452
+ });
453
+ registerCommand({
454
+ id: "accounts.switch",
455
+ timeoutMs: 30000,
456
+ allowInUi: true,
457
+ validateInput(payload) {
458
+ const p = payload;
459
+ if (!p || typeof p.provider !== "string" || !p.provider) {
460
+ throw new Error("accounts.switch requires a non-empty 'provider' string");
461
+ }
462
+ if (typeof p.alias !== "string" || !p.alias) {
463
+ throw new Error("accounts.switch requires a non-empty 'alias' string");
464
+ }
465
+ return { provider: p.provider, alias: p.alias };
466
+ },
467
+ async run(ctx, input) {
468
+ const source = resolveSource2(input.provider);
469
+ ctx.log("info", `Switching active account to "${input.alias}" in ${source}`);
470
+ const data = await readConfig(source);
471
+ switch (input.provider) {
472
+ case "anthropic":
473
+ data.currentAccount = input.alias;
474
+ break;
475
+ case "codex":
476
+ data.activeAlias = input.alias;
477
+ break;
478
+ case "antigravity": {
479
+ const accounts = Array.isArray(data.accounts) ? data.accounts : [];
480
+ const idx = accounts.findIndex((a) => typeof a.email === "string" && a.email === input.alias);
481
+ if (idx === -1) {
482
+ throw new Error(`Account "${input.alias}" not found in antigravity`);
483
+ }
484
+ data.activeIndex = idx;
485
+ break;
486
+ }
487
+ default:
488
+ throw new Error(`Unknown provider: ${input.provider}`);
489
+ }
490
+ await writeConfig(source, data);
491
+ ctx.log("info", `Active account set to "${input.alias}"`);
492
+ return { ok: true };
493
+ }
494
+ });
495
+ bunxBarrier = new Map;
496
+ registerCommand({
497
+ id: "accounts.ping",
498
+ timeoutMs: 30000,
499
+ allowInUi: true,
500
+ validateInput(payload) {
501
+ const p = payload;
502
+ if (!p || typeof p.provider !== "string" || !p.provider) {
503
+ throw new Error("accounts.ping requires a non-empty 'provider' string");
504
+ }
505
+ if (typeof p.alias !== "string" || !p.alias) {
506
+ throw new Error("accounts.ping requires a non-empty 'alias' string");
507
+ }
508
+ return { provider: p.provider, alias: p.alias };
509
+ },
510
+ async run(ctx, input) {
511
+ ctx.log("info", `Pinging ${input.provider} account "${input.alias}"\u2026`);
512
+ switch (input.provider) {
513
+ case "anthropic": {
514
+ ctx.log("info", "Calling oc-anthropic-multi-account ping\u2026");
515
+ const result = await spawnPluginCli("oc-anthropic-multi-account", [
516
+ "ping",
517
+ input.alias
518
+ ]);
519
+ const status = String(result.status ?? "error");
520
+ ctx.log("info", `Result: ${status}`);
521
+ if (status === "ok") {
522
+ await clearStaleMetrics(input.provider, input.alias);
523
+ }
524
+ return {
525
+ status,
526
+ message: status === "ok" ? "pong" : String(result.error ?? "unknown error")
527
+ };
528
+ }
529
+ case "codex": {
530
+ ctx.log("info", "Calling oc-codex-multi-account ping\u2026");
531
+ const result = await spawnPluginCli("oc-codex-multi-account", [
532
+ "ping",
533
+ input.alias
534
+ ]);
535
+ const status = String(result.status ?? "error");
536
+ ctx.log("info", `Result: ${status}`);
537
+ if (status === "ok") {
538
+ await clearStaleMetrics(input.provider, input.alias);
539
+ }
540
+ return {
541
+ status,
542
+ message: status === "ok" ? "pong" : String(result.error ?? "unknown error")
543
+ };
544
+ }
545
+ case "antigravity": {
546
+ const creds = await readCredentialFile("antigravity-accounts.json");
547
+ const accounts = Array.isArray(creds.accounts) ? creds.accounts : [];
548
+ const account = accounts.find((a) => a.email === input.alias);
549
+ if (!account) {
550
+ throw new Error(`Account "${input.alias}" not found`);
551
+ }
552
+ if (!account.refreshToken) {
553
+ throw new Error(`No refresh token for "${input.alias}"`);
554
+ }
555
+ ctx.log("info", "Refresh token present");
556
+ await clearStaleMetrics(input.provider, input.alias);
557
+ return {
558
+ status: "ok",
559
+ message: "pong (credentials present)"
560
+ };
561
+ }
562
+ default:
563
+ throw new Error(`Unknown provider: ${input.provider}`);
564
+ }
565
+ }
566
+ });
567
+ REAUTH_PROVIDERS = {
568
+ anthropic: "oc-anthropic-multi-account",
569
+ codex: "oc-codex-multi-account"
570
+ };
571
+ registerCommand({
572
+ id: "accounts.reauth-start",
573
+ timeoutMs: 15000,
574
+ allowInUi: true,
575
+ validateInput(payload) {
576
+ const p = payload;
577
+ if (!p || typeof p.provider !== "string" || !p.provider) {
578
+ throw new Error("accounts.reauth-start requires a non-empty 'provider' string");
579
+ }
580
+ if (typeof p.alias !== "string" || !p.alias) {
581
+ throw new Error("accounts.reauth-start requires a non-empty 'alias' string");
582
+ }
583
+ return { provider: p.provider, alias: p.alias };
584
+ },
585
+ async run(ctx, input) {
586
+ const cliCmd = reauthCliCommand(input.provider);
587
+ ctx.log("info", `Generating auth URL for ${input.alias}\u2026`);
588
+ const result = await spawnPluginCli(cliCmd, ["reauth", input.alias]);
589
+ const url = String(result.url ?? "");
590
+ const verifier = String(result.verifier ?? "");
591
+ if (!url || !verifier) {
592
+ throw new Error("CLI did not return url/verifier");
593
+ }
594
+ ctx.log("info", "Auth URL generated");
595
+ return { url, verifier };
596
+ }
597
+ });
598
+ registerCommand({
599
+ id: "accounts.reauth-complete",
600
+ timeoutMs: 30000,
601
+ allowInUi: true,
602
+ validateInput(payload) {
603
+ const p = payload;
604
+ if (!p || typeof p.provider !== "string" || !p.provider) {
605
+ throw new Error("accounts.reauth-complete requires 'provider'");
606
+ }
607
+ if (typeof p.alias !== "string" || !p.alias) {
608
+ throw new Error("accounts.reauth-complete requires 'alias'");
609
+ }
610
+ if (typeof p.callbackUrl !== "string" || !p.callbackUrl) {
611
+ throw new Error("accounts.reauth-complete requires 'callbackUrl'");
612
+ }
613
+ if (typeof p.verifier !== "string" || !p.verifier) {
614
+ throw new Error("accounts.reauth-complete requires 'verifier'");
615
+ }
616
+ return {
617
+ provider: p.provider,
618
+ alias: p.alias,
619
+ callbackUrl: p.callbackUrl,
620
+ verifier: p.verifier
621
+ };
622
+ },
623
+ async run(ctx, input) {
624
+ const cliCmd = reauthCliCommand(input.provider);
625
+ ctx.log("info", `Completing re-auth for ${input.alias}\u2026`);
626
+ const result = await spawnPluginCli(cliCmd, [
627
+ "reauth",
628
+ "--callback",
629
+ input.callbackUrl,
630
+ "--verifier",
631
+ input.verifier,
632
+ input.alias
633
+ ]);
634
+ const status = String(result.status ?? "error");
635
+ ctx.log("info", `Result: ${status}`);
636
+ return {
637
+ status,
638
+ message: status === "ok" ? "Re-authenticated successfully" : String(result.error ?? "unknown error")
639
+ };
640
+ }
641
+ });
642
+ registerCommand({
643
+ id: "actions.thresholds",
644
+ timeoutMs: 30000,
645
+ allowInUi: true,
646
+ validateInput(payload) {
647
+ const p = payload;
648
+ if (!p || typeof p.provider !== "string" || !p.provider) {
649
+ throw new Error("actions.thresholds requires a non-empty 'provider' string");
650
+ }
651
+ if (typeof p.warning !== "number") {
652
+ throw new Error("actions.thresholds requires a numeric 'warning' value");
653
+ }
654
+ if (typeof p.critical !== "number") {
655
+ throw new Error("actions.thresholds requires a numeric 'critical' value");
656
+ }
657
+ return {
658
+ provider: p.provider,
659
+ warning: p.warning,
660
+ critical: p.critical
661
+ };
662
+ },
663
+ async run(ctx, input) {
664
+ const source = resolveSource2(input.provider);
665
+ ctx.log("info", `Updating thresholds for "${input.provider}" \u2014 warning: ${input.warning}, critical: ${input.critical}`);
666
+ const data = await readConfig(source);
667
+ data.thresholds = {
668
+ warning: input.warning,
669
+ critical: input.critical
670
+ };
671
+ await writeConfig(source, data);
672
+ ctx.log("info", "Thresholds updated successfully");
673
+ return { ok: true };
674
+ }
675
+ });
676
+ registerCommand({
677
+ id: "actions.import",
678
+ timeoutMs: 30000,
679
+ allowInUi: true,
680
+ validateInput(payload) {
681
+ const p = payload;
682
+ if (!p || typeof p.source !== "string" || !p.source) {
683
+ throw new Error("actions.import requires a non-empty 'source' string");
684
+ }
685
+ if (p.data === undefined) {
686
+ throw new Error("actions.import requires a 'data' field");
687
+ }
688
+ return { source: p.source, data: p.data };
689
+ },
690
+ async run(ctx, input) {
691
+ ctx.log("info", `Importing config into "${input.source}"`);
692
+ const { backupPath } = await writeConfig(input.source, input.data);
693
+ ctx.log("info", `Config imported, backup at ${backupPath}`);
694
+ return { ok: true, backupPath };
695
+ }
696
+ });
697
+ registerCommand({
698
+ id: "actions.export",
699
+ timeoutMs: 30000,
700
+ allowInUi: true,
701
+ validateInput(payload) {
702
+ const p = payload;
703
+ if (!p || typeof p.source !== "string" || !p.source) {
704
+ throw new Error("actions.export requires a non-empty 'source' string");
705
+ }
706
+ return { source: p.source };
707
+ },
708
+ async run(ctx, input) {
709
+ ctx.log("info", `Exporting config from "${input.source}"`);
710
+ const data = await readConfig(input.source);
711
+ ctx.log("info", "Config exported successfully");
712
+ return { source: input.source, data };
713
+ }
714
+ });
715
+ registerCommand({
716
+ id: "actions.reset",
717
+ timeoutMs: 30000,
718
+ allowInUi: true,
719
+ validateInput(payload) {
720
+ const p = payload;
721
+ if (!p || typeof p.source !== "string" || !p.source) {
722
+ throw new Error("actions.reset requires a non-empty 'source' string");
723
+ }
724
+ return { source: p.source };
725
+ },
726
+ async run(ctx, input) {
727
+ ctx.log("info", `Resetting config "${input.source}" to empty object`);
728
+ const { backupPath } = await writeConfig(input.source, {});
729
+ ctx.log("info", `Config reset, backup at ${backupPath}`);
730
+ return { ok: true, backupPath };
731
+ }
732
+ });
733
+ registerCommand({
734
+ id: "actions.rollback",
735
+ timeoutMs: 30000,
736
+ allowInUi: true,
737
+ validateInput(payload) {
738
+ const p = payload;
739
+ if (!p || typeof p.source !== "string" || !p.source) {
740
+ throw new Error("actions.rollback requires a non-empty 'source' string");
741
+ }
742
+ return { source: p.source };
743
+ },
744
+ async run(ctx, input) {
745
+ ctx.log("info", `Rolling back config "${input.source}" to latest backup`);
746
+ const { restoredFrom } = await rollbackConfig(input.source);
747
+ ctx.log("info", `Config restored from ${restoredFrom}`);
748
+ return { ok: true, restoredFrom };
749
+ }
750
+ });
751
+ });
3
752
 
4
753
  // src/cli.ts
5
754
  import { parseArgs as nodeParseArgs } from "util";
@@ -48,6 +797,8 @@ function parseArgs() {
48
797
  watch: { type: "boolean", short: "w" },
49
798
  stats: { type: "boolean", short: "S" },
50
799
  config: { type: "string" },
800
+ commander: { type: "boolean" },
801
+ "commander-port": { type: "string" },
51
802
  help: { type: "boolean", short: "h" }
52
803
  },
53
804
  strict: true
@@ -65,7 +816,9 @@ function parseArgs() {
65
816
  monthly: values.monthly,
66
817
  watch: values.watch,
67
818
  stats: values.stats,
68
- config: values.config
819
+ config: values.config,
820
+ commander: values.commander,
821
+ commanderPort: values["commander-port"] ? parseInt(values["commander-port"], 10) : undefined
69
822
  };
70
823
  } catch (error) {
71
824
  if (error instanceof Error && error.message.includes("Unknown option")) {
@@ -86,6 +839,7 @@ Usage:
86
839
  Modes:
87
840
  (default) Interactive dashboard (Bun only)
88
841
  -S, --stats Stats table mode (works with Node.js too)
842
+ --commander Start Commander web server (Bun only)
89
843
 
90
844
  Options:
91
845
  -p, --provider <name> Filter by provider (anthropic, openai, google, opencode)
@@ -97,6 +851,7 @@ Options:
97
851
  -w, --watch Watch mode - refresh every 5 minutes (stats mode only)
98
852
  --config show Show current configuration
99
853
  -h, --help Show this help message
854
+ --commander-port <n> Commander server port (default: 3000)
100
855
 
101
856
  Codex Quota:
102
857
  Dashboard auto-reads Codex auth from ~/.codex/auth.json.
@@ -28566,8 +29321,8 @@ function UsageTable(props) {
28566
29321
  })();
28567
29322
  })(), null);
28568
29323
  insertNode(_el$7, _el$8);
28569
- setProp(_el$7, "padding-top", 1);
28570
- setProp(_el$7, "padding-bottom", 1);
29324
+ setProp(_el$7, "paddingTop", 1);
29325
+ setProp(_el$7, "paddingBottom", 1);
28571
29326
  setProp(_el$7, "border-bottom", true);
28572
29327
  insertNode(_el$8, _el$9);
28573
29328
  setProp(_el$8, "wrapMode", "none");
@@ -28585,7 +29340,7 @@ function UsageTable(props) {
28585
29340
  return [(() => {
28586
29341
  var _el$26 = createElement("box"), _el$27 = createElement("text"), _el$28 = createElement("span"), _el$29 = createElement("span"), _el$30 = createElement("span");
28587
29342
  insertNode(_el$26, _el$27);
28588
- setProp(_el$26, "padding-top", 0.5);
29343
+ setProp(_el$26, "paddingTop", 0.5);
28589
29344
  insertNode(_el$27, _el$28);
28590
29345
  insertNode(_el$27, _el$29);
28591
29346
  insertNode(_el$27, _el$30);
@@ -28605,8 +29360,8 @@ function UsageTable(props) {
28605
29360
  fg: COLORS.accent.amber,
28606
29361
  bold: true
28607
29362
  };
28608
- _v$1 !== _p$.e && (_p$.e = setProp(_el$26, "padding-bottom", _v$1, _p$.e));
28609
- _v$10 !== _p$.t && (_p$.t = setProp(_el$26, "background-color", _v$10, _p$.t));
29363
+ _v$1 !== _p$.e && (_p$.e = setProp(_el$26, "paddingBottom", _v$1, _p$.e));
29364
+ _v$10 !== _p$.t && (_p$.t = setProp(_el$26, "backgroundColor", _v$10, _p$.t));
28610
29365
  _v$11 !== _p$.a && (_p$.a = setProp(_el$28, "style", _v$11, _p$.a));
28611
29366
  _v$12 !== _p$.o && (_p$.o = setProp(_el$29, "style", _v$12, _p$.o));
28612
29367
  _v$13 !== _p$.i && (_p$.i = setProp(_el$30, "style", _v$13, _p$.i));
@@ -28626,9 +29381,9 @@ function UsageTable(props) {
28626
29381
  return (() => {
28627
29382
  var _el$31 = createElement("box"), _el$32 = createElement("text"), _el$33 = createElement("span"), _el$34 = createElement("span"), _el$35 = createElement("span");
28628
29383
  insertNode(_el$31, _el$32);
28629
- setProp(_el$31, "padding-top", 0.25);
28630
- setProp(_el$31, "padding-bottom", isLastProvider ? 0.5 : 0.25);
28631
- setProp(_el$31, "padding-left", 2);
29384
+ setProp(_el$31, "paddingTop", 0.25);
29385
+ setProp(_el$31, "paddingBottom", isLastProvider ? 0.5 : 0.25);
29386
+ setProp(_el$31, "paddingLeft", 2);
28632
29387
  insertNode(_el$32, _el$33);
28633
29388
  insertNode(_el$32, _el$34);
28634
29389
  insertNode(_el$32, _el$35);
@@ -28645,7 +29400,7 @@ function UsageTable(props) {
28645
29400
  }, _v$17 = {
28646
29401
  fg: COLORS.accent.amber
28647
29402
  };
28648
- _v$14 !== _p$.e && (_p$.e = setProp(_el$31, "background-color", _v$14, _p$.e));
29403
+ _v$14 !== _p$.e && (_p$.e = setProp(_el$31, "backgroundColor", _v$14, _p$.e));
28649
29404
  _v$15 !== _p$.t && (_p$.t = setProp(_el$33, "style", _v$15, _p$.t));
28650
29405
  _v$16 !== _p$.a && (_p$.a = setProp(_el$34, "style", _v$16, _p$.a));
28651
29406
  _v$17 !== _p$.o && (_p$.o = setProp(_el$35, "style", _v$17, _p$.o));
@@ -28663,14 +29418,14 @@ function UsageTable(props) {
28663
29418
  var _el$36 = createElement("box");
28664
29419
  setProp(_el$36, "height", 1);
28665
29420
  setProp(_el$36, "border-bottom", true);
28666
- effect((_$p) => setProp(_el$36, "border-color", COLORS.border, _$p));
29421
+ effect((_$p) => setProp(_el$36, "borderColor", COLORS.border, _$p));
28667
29422
  return _el$36;
28668
29423
  })()];
28669
29424
  }
28670
29425
  }), _el$0);
28671
29426
  insertNode(_el$0, _el$1);
28672
- setProp(_el$0, "padding-top", 1);
28673
- setProp(_el$0, "padding-bottom", 0.5);
29427
+ setProp(_el$0, "paddingTop", 1);
29428
+ setProp(_el$0, "paddingBottom", 0.5);
28674
29429
  setProp(_el$0, "border-top", true);
28675
29430
  insertNode(_el$1, _el$10);
28676
29431
  insertNode(_el$1, _el$11);
@@ -28694,9 +29449,9 @@ function UsageTable(props) {
28694
29449
  _v$2 !== _p$.t && (_p$.t = setProp(_el$, "width", _v$2, _p$.t));
28695
29450
  _v$3 !== _p$.a && (_p$.a = setProp(_el$, "backgroundColor", _v$3, _p$.a));
28696
29451
  _v$4 !== _p$.o && (_p$.o = setProp(_el$4, "fg", _v$4, _p$.o));
28697
- _v$5 !== _p$.i && (_p$.i = setProp(_el$7, "border-color", _v$5, _p$.i));
29452
+ _v$5 !== _p$.i && (_p$.i = setProp(_el$7, "borderColor", _v$5, _p$.i));
28698
29453
  _v$6 !== _p$.n && (_p$.n = setProp(_el$8, "fg", _v$6, _p$.n));
28699
- _v$7 !== _p$.s && (_p$.s = setProp(_el$0, "border-color", _v$7, _p$.s));
29454
+ _v$7 !== _p$.s && (_p$.s = setProp(_el$0, "borderColor", _v$7, _p$.s));
28700
29455
  _v$8 !== _p$.h && (_p$.h = setProp(_el$10, "style", _v$8, _p$.h));
28701
29456
  _v$9 !== _p$.r && (_p$.r = setProp(_el$11, "style", _v$9, _p$.r));
28702
29457
  _v$0 !== _p$.d && (_p$.d = setProp(_el$12, "style", _v$0, _p$.d));
@@ -28872,6 +29627,7 @@ function QuotaPanel(props) {
28872
29627
  setProp(_el$52, "border", true);
28873
29628
  setProp(_el$52, "borderStyle", "rounded");
28874
29629
  setProp(_el$52, "borderColor", isActive ? "#14b8a6" : "#334155");
29630
+ setProp(_el$52, "flexGrow", 1);
28875
29631
  insertNode(_el$53, _el$54);
28876
29632
  setProp(_el$53, "paddingBottom", 0);
28877
29633
  setProp(_el$53, "flexShrink", 0);
@@ -28912,7 +29668,7 @@ function QuotaPanel(props) {
28912
29668
  const barWidth = compact ? 10 : 20;
28913
29669
  const displayLabelText = compact ? truncateText(displayLabel, labelWidth) : displayLabel;
28914
29670
  const resetText = formatResetTime(quota.resetAt, compact);
28915
- const compactResetText = compact ? truncateText(resetText, 8) : resetText;
29671
+ const compactResetText = resetText;
28916
29672
  const errorText = compact ? truncateText(`${displayLabel}: ${quota.error ?? "Error"}`, 28) : `${displayLabel}: ${quota.error}`;
28917
29673
  return createComponent2(Show, {
28918
29674
  get when() {
@@ -28972,7 +29728,6 @@ function QuotaPanel(props) {
28972
29728
  });
28973
29729
  }
28974
29730
  }), null);
28975
- effect((_$p) => setProp(_el$52, "width", props.twoColumns ? "49%" : undefined, _$p));
28976
29731
  return _el$52;
28977
29732
  })();
28978
29733
  }
@@ -29034,8 +29789,8 @@ function StatusBar(props) {
29034
29789
  insertNode(_el$72, _el$73);
29035
29790
  setProp(_el$72, "height", 1);
29036
29791
  setProp(_el$72, "border-top", true);
29037
- setProp(_el$72, "padding-left", 1);
29038
- setProp(_el$72, "padding-right", 1);
29792
+ setProp(_el$72, "paddingLeft", 1);
29793
+ setProp(_el$72, "paddingRight", 1);
29039
29794
  insertNode(_el$73, _el$74);
29040
29795
  insertNode(_el$73, _el$76);
29041
29796
  insertNode(_el$73, _el$78);
@@ -29054,8 +29809,8 @@ function StatusBar(props) {
29054
29809
  }, _v$33 = {
29055
29810
  fg: COLORS.text.muted
29056
29811
  };
29057
- _v$28 !== _p$.e && (_p$.e = setProp(_el$72, "border-color", _v$28, _p$.e));
29058
- _v$29 !== _p$.t && (_p$.t = setProp(_el$72, "background-color", _v$29, _p$.t));
29812
+ _v$28 !== _p$.e && (_p$.e = setProp(_el$72, "borderColor", _v$28, _p$.e));
29813
+ _v$29 !== _p$.t && (_p$.t = setProp(_el$72, "backgroundColor", _v$29, _p$.t));
29059
29814
  _v$30 !== _p$.a && (_p$.a = setProp(_el$73, "fg", _v$30, _p$.a));
29060
29815
  _v$31 !== _p$.o && (_p$.o = setProp(_el$74, "style", _v$31, _p$.o));
29061
29816
  _v$32 !== _p$.i && (_p$.i = setProp(_el$78, "style", _v$32, _p$.i));
@@ -29239,8 +29994,9 @@ function Dashboard(props) {
29239
29994
  insertNode(_el$82, _el$83);
29240
29995
  setProp(_el$82, "width", "100%");
29241
29996
  setProp(_el$82, "height", "100%");
29242
- setProp(_el$82, "flex-direction", "column");
29243
- setProp(_el$83, "flex-grow", 1);
29997
+ setProp(_el$82, "flexDirection", "column");
29998
+ setProp(_el$83, "flexGrow", 1);
29999
+ setProp(_el$83, "width", "100%");
29244
30000
  setProp(_el$83, "gap", 0);
29245
30001
  setProp(_el$83, "padding", 0);
29246
30002
  insert(_el$83, createComponent2(UsageTable, {
@@ -29299,8 +30055,8 @@ function Dashboard(props) {
29299
30055
  }), null);
29300
30056
  effect((_p$) => {
29301
30057
  var _v$34 = COLORS.bg.primary, _v$35 = sideBySide() ? "row" : "column";
29302
- _v$34 !== _p$.e && (_p$.e = setProp(_el$82, "background-color", _v$34, _p$.e));
29303
- _v$35 !== _p$.t && (_p$.t = setProp(_el$83, "flex-direction", _v$35, _p$.t));
30058
+ _v$34 !== _p$.e && (_p$.e = setProp(_el$82, "backgroundColor", _v$34, _p$.e));
30059
+ _v$35 !== _p$.t && (_p$.t = setProp(_el$83, "flexDirection", _v$35, _p$.t));
29304
30060
  return _p$;
29305
30061
  }, {
29306
30062
  e: undefined,
@@ -29385,6 +30141,573 @@ async function showConfig() {
29385
30141
  }
29386
30142
  var CODEX_AUTH_PATH2 = join6(homedir4(), ".codex", "auth.json");
29387
30143
 
30144
+ // src/commander/server.ts
30145
+ import { join as join10, dirname as dirname2 } from "path";
30146
+ import { readFileSync as readFileSync2 } from "fs";
30147
+ import { fileURLToPath as fileURLToPath2 } from "url";
30148
+
30149
+ // src/commander/services/quota-service.ts
30150
+ async function getQuotaData() {
30151
+ const [anthropic, antigravity, codex] = await Promise.all([
30152
+ loadMultiAccountQuota(),
30153
+ loadAntigravityQuota(),
30154
+ loadCodexQuota()
30155
+ ]);
30156
+ return { anthropic, antigravity, codex };
30157
+ }
30158
+
30159
+ // src/commander/server.ts
30160
+ init_command_runner();
30161
+
30162
+ // src/commander/services/action-service.ts
30163
+ function ensureActionsRegistered() {
30164
+ if (registered)
30165
+ return;
30166
+ registered = true;
30167
+ Promise.resolve().then(() => init_plugin_adapters());
30168
+ }
30169
+ var isBun5 = typeof globalThis.Bun !== "undefined";
30170
+ var registered = false;
30171
+
30172
+ // src/commander/services/app-init-service.ts
30173
+ init_command_runner();
30174
+ import { homedir as homedir7 } from "os";
30175
+ import { join as join9 } from "path";
30176
+ async function fileExists2(path2) {
30177
+ try {
30178
+ const file = Bun.file(path2);
30179
+ return await file.exists();
30180
+ } catch {
30181
+ return false;
30182
+ }
30183
+ }
30184
+ async function readOpencodeJson() {
30185
+ try {
30186
+ const file = Bun.file(OPENCODE_JSON_PATH);
30187
+ if (!await file.exists())
30188
+ return {};
30189
+ const text = await file.text();
30190
+ return JSON.parse(text);
30191
+ } catch {
30192
+ return {};
30193
+ }
30194
+ }
30195
+ function pluginListContains(config, pluginName) {
30196
+ const plugins = config.plugins;
30197
+ if (!Array.isArray(plugins))
30198
+ return false;
30199
+ return plugins.some((p) => typeof p === "string" && p.includes(pluginName));
30200
+ }
30201
+ async function patchPluginList(pluginEntry) {
30202
+ const config = await readOpencodeJson();
30203
+ if (!Array.isArray(config.plugins)) {
30204
+ config.plugins = [];
30205
+ }
30206
+ const plugins = config.plugins;
30207
+ if (plugins.includes(pluginEntry))
30208
+ return;
30209
+ plugins.push(pluginEntry);
30210
+ await Bun.write(OPENCODE_JSON_PATH, JSON.stringify(config, null, 2));
30211
+ }
30212
+ function spawnSyncCheck(cmd) {
30213
+ try {
30214
+ const result = Bun.spawnSync(cmd, {
30215
+ stdio: ["ignore", "pipe", "pipe"]
30216
+ });
30217
+ return result.exitCode === 0;
30218
+ } catch {
30219
+ return false;
30220
+ }
30221
+ }
30222
+ async function spawnAndLog(ctx, cmd, options) {
30223
+ ctx.log("info", `Running: ${cmd.join(" ")}`);
30224
+ try {
30225
+ const proc = Bun.spawn(cmd, {
30226
+ cwd: options?.cwd,
30227
+ stdio: ["ignore", "pipe", "pipe"]
30228
+ });
30229
+ const stdout = await new Response(proc.stdout).text();
30230
+ const stderr = await new Response(proc.stderr).text();
30231
+ await proc.exited;
30232
+ if (stdout.trim())
30233
+ ctx.log("info", stdout.trim());
30234
+ if (stderr.trim())
30235
+ ctx.log("warn", stderr.trim());
30236
+ return proc.exitCode === 0;
30237
+ } catch (err) {
30238
+ const message = err instanceof Error ? err.message : "Spawn failed";
30239
+ ctx.log("error", message);
30240
+ return false;
30241
+ }
30242
+ }
30243
+ async function detectOcCodexMultiAccount() {
30244
+ const binaryPath = join9(OPENCODE_CONFIG_DIR, "node_modules", ".bin", "oc-codex-multi-account");
30245
+ const binaryExists = await fileExists2(binaryPath);
30246
+ const config = await readOpencodeJson();
30247
+ const pluginConfigured = pluginListContains(config, "oc-codex-multi-account");
30248
+ let state;
30249
+ const details = [];
30250
+ if (binaryExists && pluginConfigured) {
30251
+ state = "ready";
30252
+ details.push("Binary installed", "Plugin configured");
30253
+ } else if (binaryExists || pluginConfigured) {
30254
+ state = "partial";
30255
+ if (!binaryExists)
30256
+ details.push("Binary missing");
30257
+ if (!pluginConfigured)
30258
+ details.push("Plugin not configured");
30259
+ } else {
30260
+ state = "not-installed";
30261
+ details.push("Binary not found", "Plugin not configured");
30262
+ }
30263
+ return {
30264
+ id: "oc-codex-multi-account",
30265
+ name: "OC Codex Multi-Account",
30266
+ description: "Multi-account support for OpenAI Codex via OpenCode plugin",
30267
+ state,
30268
+ details
30269
+ };
30270
+ }
30271
+ async function detectOcAnthropicMultiAccount() {
30272
+ const dirPath = "/Users/gabrielecegi/oc/oc-anthropic-multi-account";
30273
+ const statePath = join9(OPENCODE_CONFIG_DIR, "anthropic-multi-account-state.json");
30274
+ const dirExists = await fileExists2(dirPath);
30275
+ const stateExists = await fileExists2(statePath);
30276
+ let state;
30277
+ const details = [];
30278
+ if (dirExists && stateExists) {
30279
+ state = "ready";
30280
+ details.push("Project directory found", "State file present");
30281
+ } else if (dirExists) {
30282
+ state = "partial";
30283
+ details.push("Project directory found", "State file missing");
30284
+ } else {
30285
+ state = "not-installed";
30286
+ details.push("Project directory not found");
30287
+ if (!stateExists)
30288
+ details.push("State file missing");
30289
+ }
30290
+ return {
30291
+ id: "oc-anthropic-multi-account",
30292
+ name: "OC Anthropic Multi-Account",
30293
+ description: "Multi-account support for Anthropic via OpenCode plugin",
30294
+ state,
30295
+ details
30296
+ };
30297
+ }
30298
+ async function detectOpencodeGitbutler() {
30299
+ const butAvailable = spawnSyncCheck(["but", "--version"]);
30300
+ const config = await readOpencodeJson();
30301
+ const pluginConfigured = pluginListContains(config, "opencode-gitbutler");
30302
+ let state;
30303
+ const details = [];
30304
+ if (butAvailable && pluginConfigured) {
30305
+ state = "ready";
30306
+ details.push("GitButler CLI available", "Plugin configured");
30307
+ } else if (!butAvailable) {
30308
+ state = "missing-deps";
30309
+ details.push("GitButler CLI (but) not found");
30310
+ if (!pluginConfigured)
30311
+ details.push("Plugin not configured");
30312
+ } else {
30313
+ state = "partial";
30314
+ details.push("GitButler CLI available", "Plugin not configured");
30315
+ }
30316
+ return {
30317
+ id: "opencode-gitbutler",
30318
+ name: "OpenCode GitButler",
30319
+ description: "GitButler integration for OpenCode",
30320
+ state,
30321
+ details
30322
+ };
30323
+ }
30324
+ async function detectOpencodeUsage() {
30325
+ const available = spawnSyncCheck(["bunx", "opencode-usage", "--help"]);
30326
+ let state;
30327
+ const details = [];
30328
+ if (available) {
30329
+ state = "ready";
30330
+ details.push("opencode-usage available via bunx");
30331
+ } else {
30332
+ state = "not-installed";
30333
+ details.push("opencode-usage not available");
30334
+ }
30335
+ return {
30336
+ id: "opencode-usage",
30337
+ name: "OpenCode Usage",
30338
+ description: "CLI tool for tracking OpenCode AI usage and costs",
30339
+ state,
30340
+ details
30341
+ };
30342
+ }
30343
+ async function getAppCatalog() {
30344
+ const results = await Promise.all([
30345
+ detectOcCodexMultiAccount(),
30346
+ detectOcAnthropicMultiAccount(),
30347
+ detectOpencodeGitbutler(),
30348
+ detectOpencodeUsage()
30349
+ ]);
30350
+ return results;
30351
+ }
30352
+ function validateAppInput(payload) {
30353
+ if (typeof payload !== "object" || payload === null || !("appId" in payload)) {
30354
+ throw new Error('Missing "appId" in payload');
30355
+ }
30356
+ const { appId } = payload;
30357
+ if (typeof appId !== "string" || !VALID_APP_IDS.has(appId)) {
30358
+ throw new Error(`Invalid appId: "${String(appId)}"`);
30359
+ }
30360
+ return { appId };
30361
+ }
30362
+ async function initOcCodexMultiAccount(ctx) {
30363
+ await spawnAndLog(ctx, [
30364
+ "bun",
30365
+ "add",
30366
+ "oc-codex-multi-account",
30367
+ "--cwd",
30368
+ OPENCODE_CONFIG_DIR
30369
+ ]);
30370
+ await patchPluginList("oc-codex-multi-account@latest");
30371
+ ctx.log("info", "Plugin configured in opencode.json");
30372
+ ctx.log("info", `To add accounts, run: ${join9(OPENCODE_CONFIG_DIR, "node_modules", ".bin", "oc-codex-multi-account")} add <alias>`);
30373
+ }
30374
+ async function initOcAnthropicMultiAccount(ctx) {
30375
+ const dirPath = "/Users/gabrielecegi/oc/oc-anthropic-multi-account";
30376
+ const dirExists = await fileExists2(dirPath);
30377
+ if (!dirExists) {
30378
+ ctx.log("error", `Project directory not found: ${dirPath}. Clone the repo first.`);
30379
+ return;
30380
+ }
30381
+ await spawnAndLog(ctx, ["bun", "install"], { cwd: dirPath });
30382
+ await patchPluginList("oc-anthropic-multi-account@latest");
30383
+ ctx.log("info", "Plugin configured in opencode.json");
30384
+ ctx.log("info", `To add accounts, run: cd ${dirPath} && bun src/cli.ts add primary`);
30385
+ }
30386
+ async function initOpencodeGitbutler(ctx) {
30387
+ await patchPluginList("opencode-gitbutler@latest");
30388
+ ctx.log("info", "Plugin configured in opencode.json");
30389
+ ctx.log("info", "To install GitButler CLI, run: brew install gitbutler");
30390
+ }
30391
+ async function initOpencodeUsage(ctx) {
30392
+ const ok = await spawnAndLog(ctx, ["bunx", "opencode-usage", "--help"]);
30393
+ if (ok) {
30394
+ ctx.log("info", "opencode-usage is working correctly");
30395
+ }
30396
+ ctx.log("info", "opencode-usage is available via bunx. For global install: bun add -g opencode-usage");
30397
+ }
30398
+ async function runInit(ctx, input) {
30399
+ ctx.log("info", `Initializing app: ${input.appId}`);
30400
+ switch (input.appId) {
30401
+ case "oc-codex-multi-account":
30402
+ await initOcCodexMultiAccount(ctx);
30403
+ break;
30404
+ case "oc-anthropic-multi-account":
30405
+ await initOcAnthropicMultiAccount(ctx);
30406
+ break;
30407
+ case "opencode-gitbutler":
30408
+ await initOpencodeGitbutler(ctx);
30409
+ break;
30410
+ case "opencode-usage":
30411
+ await initOpencodeUsage(ctx);
30412
+ break;
30413
+ }
30414
+ return { ok: true };
30415
+ }
30416
+ async function runRepair(ctx, input) {
30417
+ ctx.log("info", `Repairing app: ${input.appId}`);
30418
+ const catalog = await getAppCatalog();
30419
+ const app = catalog.find((a) => a.id === input.appId);
30420
+ if (!app) {
30421
+ ctx.log("error", `App not found: ${input.appId}`);
30422
+ return { ok: false, state: "not-installed" };
30423
+ }
30424
+ ctx.log("info", `Current state: ${app.state} \u2014 ${app.details.join(", ")}`);
30425
+ if (app.state === "ready") {
30426
+ ctx.log("info", "App is already in ready state, no repair needed");
30427
+ return { ok: true, state: "ready" };
30428
+ }
30429
+ ctx.log("info", "Attempting repair via init workflow...");
30430
+ await runInit(ctx, input);
30431
+ const afterCatalog = await getAppCatalog();
30432
+ const afterApp = afterCatalog.find((a) => a.id === input.appId);
30433
+ const finalState = afterApp?.state ?? "not-installed";
30434
+ ctx.log("info", `Post-repair state: ${finalState}`);
30435
+ return { ok: finalState === "ready", state: finalState };
30436
+ }
30437
+ function ensureAppCommandsRegistered() {
30438
+ if (registered2)
30439
+ return;
30440
+ registered2 = true;
30441
+ registerCommand({
30442
+ id: "apps.init",
30443
+ validateInput: validateAppInput,
30444
+ run: runInit,
30445
+ timeoutMs: 120000,
30446
+ allowInUi: true
30447
+ });
30448
+ registerCommand({
30449
+ id: "apps.repair",
30450
+ validateInput: validateAppInput,
30451
+ run: runRepair,
30452
+ timeoutMs: 120000,
30453
+ allowInUi: true
30454
+ });
30455
+ }
30456
+ var isBun6 = typeof globalThis.Bun !== "undefined";
30457
+ var OPENCODE_CONFIG_DIR = join9(homedir7(), ".config", "opencode");
30458
+ var OPENCODE_JSON_PATH = join9(OPENCODE_CONFIG_DIR, "opencode.json");
30459
+ var VALID_APP_IDS = new Set([
30460
+ "oc-codex-multi-account",
30461
+ "oc-anthropic-multi-account",
30462
+ "opencode-gitbutler",
30463
+ "opencode-usage"
30464
+ ]);
30465
+ var registered2 = false;
30466
+
30467
+ // src/commander/server.ts
30468
+ init_config_service();
30469
+ function queryUsageInWorker(opts) {
30470
+ return new Promise((resolve3, reject) => {
30471
+ const worker = new Worker(usageWorkerPath);
30472
+ worker.onmessage = (event) => {
30473
+ worker.terminate();
30474
+ const msg = event.data;
30475
+ if (msg.ok)
30476
+ resolve3(msg.data);
30477
+ else
30478
+ reject(new Error(msg.error));
30479
+ };
30480
+ worker.onerror = (err) => {
30481
+ worker.terminate();
30482
+ reject(err);
30483
+ };
30484
+ worker.postMessage(opts);
30485
+ });
30486
+ }
30487
+ async function runCommanderServer(args) {
30488
+ if (!isBun7) {
30489
+ console.error("Commander mode requires Bun runtime.");
30490
+ process.exit(1);
30491
+ }
30492
+ ensureActionsRegistered();
30493
+ ensureAppCommandsRegistered();
30494
+ await listConfigFiles();
30495
+ const port = args.commanderPort ?? DEFAULT_PORT;
30496
+ const hostname = "127.0.0.1";
30497
+ Bun.serve({
30498
+ hostname,
30499
+ port,
30500
+ async fetch(req) {
30501
+ const url = new URL(req.url);
30502
+ if (req.method === "GET" && url.pathname === "/api/health") {
30503
+ return Response.json({
30504
+ status: "ok",
30505
+ version: PKG_VERSION,
30506
+ timestamp: new Date().toISOString()
30507
+ });
30508
+ }
30509
+ if (req.method === "GET" && url.pathname === "/api/usage") {
30510
+ try {
30511
+ const provider = url.searchParams.get("provider") ?? undefined;
30512
+ const daysParam = url.searchParams.get("days");
30513
+ const days = daysParam !== null ? Number(daysParam) : undefined;
30514
+ const since = url.searchParams.get("since") ?? undefined;
30515
+ const until = url.searchParams.get("until") ?? undefined;
30516
+ const monthly = url.searchParams.get("monthly") === "true";
30517
+ const data = await queryUsageInWorker({
30518
+ provider,
30519
+ days,
30520
+ since,
30521
+ until,
30522
+ monthly
30523
+ });
30524
+ return Response.json(data);
30525
+ } catch (err) {
30526
+ return Response.json({
30527
+ error: err instanceof Error ? err.message : "Internal server error"
30528
+ }, { status: 500 });
30529
+ }
30530
+ }
30531
+ if (req.method === "GET" && url.pathname === "/api/quota") {
30532
+ try {
30533
+ const data = await getQuotaData();
30534
+ return Response.json(data);
30535
+ } catch (err) {
30536
+ return Response.json({
30537
+ error: err instanceof Error ? err.message : "Internal server error"
30538
+ }, { status: 500 });
30539
+ }
30540
+ }
30541
+ if (req.method === "POST" && url.pathname === "/api/commands/run") {
30542
+ try {
30543
+ const body = await req.json();
30544
+ if (typeof body.commandId !== "string" || !body.commandId) {
30545
+ return Response.json({ error: "Missing or invalid commandId" }, { status: 400 });
30546
+ }
30547
+ const jobId = runCommand(body.commandId, body.payload);
30548
+ return Response.json({ jobId }, { status: 202 });
30549
+ } catch (err) {
30550
+ const message = err instanceof Error ? err.message : "Internal server error";
30551
+ return Response.json({ error: message }, { status: 400 });
30552
+ }
30553
+ }
30554
+ if (req.method === "GET" && url.pathname.startsWith("/api/jobs/")) {
30555
+ const jobId = url.pathname.slice("/api/jobs/".length);
30556
+ const job = getJob(jobId);
30557
+ if (!job) {
30558
+ return Response.json({ error: "Not found" }, { status: 404 });
30559
+ }
30560
+ return Response.json(job);
30561
+ }
30562
+ if (req.method === "GET" && url.pathname === "/api/config/files") {
30563
+ try {
30564
+ const files = await listConfigFiles();
30565
+ return Response.json(files);
30566
+ } catch (err) {
30567
+ return Response.json({
30568
+ error: err instanceof Error ? err.message : "Internal server error"
30569
+ }, { status: 500 });
30570
+ }
30571
+ }
30572
+ if (req.method === "GET" && url.pathname.startsWith("/api/config/") && url.pathname !== "/api/config/files") {
30573
+ const source = url.pathname.slice("/api/config/".length);
30574
+ if (!isValidSource(source)) {
30575
+ return Response.json({ error: `Unknown config source: ${source}` }, { status: 404 });
30576
+ }
30577
+ try {
30578
+ const data = readConfig(source);
30579
+ return Response.json(data);
30580
+ } catch (err) {
30581
+ const status = err instanceof ConfigError ? err.status : 500;
30582
+ const message = err instanceof Error ? err.message : "Internal server error";
30583
+ return Response.json({ error: message }, { status });
30584
+ }
30585
+ }
30586
+ if (req.method === "PUT" && url.pathname.startsWith("/api/config/")) {
30587
+ const source = url.pathname.slice("/api/config/".length);
30588
+ if (!isValidSource(source)) {
30589
+ return Response.json({ error: `Unknown config source: ${source}` }, { status: 404 });
30590
+ }
30591
+ try {
30592
+ const body = await req.json();
30593
+ const { backupPath } = await writeConfig(source, body);
30594
+ return Response.json({ ok: true, backupPath });
30595
+ } catch (err) {
30596
+ if (err instanceof SyntaxError) {
30597
+ return Response.json({ error: "Request body is not valid JSON" }, { status: 400 });
30598
+ }
30599
+ const status = err instanceof ConfigError ? err.status : 500;
30600
+ const message = err instanceof Error ? err.message : "Internal server error";
30601
+ return Response.json({ error: message }, { status });
30602
+ }
30603
+ }
30604
+ if (req.method === "POST" && url.pathname.endsWith("/rollback") && url.pathname.startsWith("/api/config/")) {
30605
+ const source = url.pathname.slice("/api/config/".length).replace(/\/rollback$/, "");
30606
+ if (!isValidSource(source)) {
30607
+ return Response.json({ error: `Unknown config source: ${source}` }, { status: 404 });
30608
+ }
30609
+ try {
30610
+ const result = await rollbackConfig(source);
30611
+ return Response.json({ ok: true, ...result });
30612
+ } catch (err) {
30613
+ const status = err instanceof ConfigError ? err.status : 500;
30614
+ const message = err instanceof Error ? err.message : "Internal server error";
30615
+ return Response.json({ error: message }, { status });
30616
+ }
30617
+ }
30618
+ if (req.method === "POST" && url.pathname.startsWith("/api/accounts/")) {
30619
+ const parts = url.pathname.split("/");
30620
+ const provider = parts[3];
30621
+ const action = parts[4];
30622
+ if (!provider || !action) {
30623
+ return Response.json({ error: "Invalid account route" }, { status: 400 });
30624
+ }
30625
+ try {
30626
+ const body = await req.json();
30627
+ const jobId = runCommand(`accounts.${action}`, {
30628
+ provider,
30629
+ ...body
30630
+ });
30631
+ return Response.json({ jobId }, { status: 202 });
30632
+ } catch (err) {
30633
+ const message = err instanceof Error ? err.message : "Internal server error";
30634
+ return Response.json({ error: message }, { status: 400 });
30635
+ }
30636
+ }
30637
+ if (req.method === "POST" && url.pathname.startsWith("/api/actions/")) {
30638
+ const action = url.pathname.slice("/api/actions/".length);
30639
+ if (!action) {
30640
+ return Response.json({ error: "Invalid action route" }, { status: 400 });
30641
+ }
30642
+ try {
30643
+ const body = await req.json();
30644
+ const jobId = runCommand(`actions.${action}`, body);
30645
+ return Response.json({ jobId }, { status: 202 });
30646
+ } catch (err) {
30647
+ const message = err instanceof Error ? err.message : "Internal server error";
30648
+ return Response.json({ error: message }, { status: 400 });
30649
+ }
30650
+ }
30651
+ if (req.method === "GET" && url.pathname === "/api/apps") {
30652
+ try {
30653
+ const catalog = await getAppCatalog();
30654
+ return Response.json(catalog);
30655
+ } catch (err) {
30656
+ return Response.json({
30657
+ error: err instanceof Error ? err.message : "Internal server error"
30658
+ }, { status: 500 });
30659
+ }
30660
+ }
30661
+ if (req.method === "POST" && url.pathname.startsWith("/api/apps/") && url.pathname.endsWith("/init")) {
30662
+ const appId = url.pathname.slice("/api/apps/".length).replace(/\/init$/, "");
30663
+ try {
30664
+ const jobId = runCommand("apps.init", { appId });
30665
+ return Response.json({ jobId }, { status: 202 });
30666
+ } catch (err) {
30667
+ const message = err instanceof Error ? err.message : "Internal server error";
30668
+ return Response.json({ error: message }, { status: 400 });
30669
+ }
30670
+ }
30671
+ if (req.method === "POST" && url.pathname.startsWith("/api/apps/") && url.pathname.endsWith("/repair")) {
30672
+ const appId = url.pathname.slice("/api/apps/".length).replace(/\/repair$/, "");
30673
+ try {
30674
+ const jobId = runCommand("apps.repair", { appId });
30675
+ return Response.json({ jobId }, { status: 202 });
30676
+ } catch (err) {
30677
+ const message = err instanceof Error ? err.message : "Internal server error";
30678
+ return Response.json({ error: message }, { status: 400 });
30679
+ }
30680
+ }
30681
+ if (!url.pathname.startsWith("/api/")) {
30682
+ const UI_DIST = join10(new URL(".", import.meta.url).pathname, "../commander-ui/dist");
30683
+ const filePath = url.pathname === "/" ? join10(UI_DIST, "index.html") : join10(UI_DIST, url.pathname);
30684
+ const file = Bun.file(filePath);
30685
+ if (await file.exists())
30686
+ return new Response(file);
30687
+ return new Response(Bun.file(join10(UI_DIST, "index.html")));
30688
+ }
30689
+ return Response.json({ error: "Not found" }, { status: 404 });
30690
+ }
30691
+ });
30692
+ const serverUrl = `http://${hostname}:${port}`;
30693
+ console.log(`Commander ready at ${serverUrl}`);
30694
+ if (isBun7 && !process.env.NO_OPEN) {
30695
+ const cmd = process.platform === "win32" ? ["cmd", "/c", "start", serverUrl] : process.platform === "darwin" ? ["open", serverUrl] : ["xdg-open", serverUrl];
30696
+ Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] });
30697
+ }
30698
+ }
30699
+ var isBun7 = typeof globalThis.Bun !== "undefined";
30700
+ var DEFAULT_PORT = 4466;
30701
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
30702
+ var PKG_VERSION = (() => {
30703
+ try {
30704
+ const pkg = JSON.parse(readFileSync2(join10(__dirname2, "..", "..", "package.json"), "utf-8"));
30705
+ return String(pkg.version ?? "0.0.0");
30706
+ } catch {
30707
+ return "0.0.0";
30708
+ }
30709
+ })();
30710
+ var usageWorkerPath = join10(__dirname2, "services", "usage-worker.ts");
29388
30711
  // src/index.ts
29389
30712
  function clearScreen() {
29390
30713
  process.stdout.write("\x1B[2J\x1B[H");
@@ -29434,7 +30757,23 @@ async function renderUsage(options, allMessages) {
29434
30757
  }
29435
30758
  }
29436
30759
  async function main2() {
29437
- const { provider, days, since, until, json, monthly, watch, stats, config } = parseArgs();
30760
+ const args = parseArgs();
30761
+ const {
30762
+ provider,
30763
+ days,
30764
+ since,
30765
+ until,
30766
+ json,
30767
+ monthly,
30768
+ watch,
30769
+ stats,
30770
+ config,
30771
+ commander
30772
+ } = args;
30773
+ if (commander) {
30774
+ await runCommanderServer(args);
30775
+ return;
30776
+ }
29438
30777
  if (config === "show") {
29439
30778
  await showConfig();
29440
30779
  return;
@@ -29479,4 +30818,4 @@ async function main2() {
29479
30818
  var WATCH_INTERVAL_MS = 5 * 60 * 1000;
29480
30819
  main2().catch(console.error);
29481
30820
 
29482
- //# debugId=C12EF127A8F53A8064756E2164756E21
30821
+ //# debugId=FEF96600978E6E5764756E2164756E21