@leo000001/opencode-quota-sidebar 4.1.0 → 4.1.2
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/README.md +12 -6
- package/README.zh-CN.md +522 -516
- package/dist/cli.d.ts +23 -5
- package/dist/cli.js +272 -38
- package/dist/provider_catalog.d.ts +4 -0
- package/dist/provider_catalog.js +41 -2
- package/dist/tui.tsx +5 -3
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ type CliServerCommand = {
|
|
|
12
12
|
shell?: boolean;
|
|
13
13
|
};
|
|
14
14
|
type SpawnedCliServerProcess = ReturnType<typeof spawn>;
|
|
15
|
+
type CliPortResolver = (preferredPort: number, attemptedPorts: ReadonlySet<number>) => Promise<number>;
|
|
16
|
+
type StartedCliServer = {
|
|
17
|
+
url: string;
|
|
18
|
+
pid?: number;
|
|
19
|
+
close: () => void;
|
|
20
|
+
};
|
|
15
21
|
export declare function releaseCliServerProcess(proc: {
|
|
16
22
|
stdin?: {
|
|
17
23
|
destroy: () => void;
|
|
@@ -45,12 +51,24 @@ export declare function terminateCliServerProcess(proc: {
|
|
|
45
51
|
export declare function extractCliServerUrl(output: string): string | undefined;
|
|
46
52
|
export declare function parseCliArgs(argv: string[]): CliCommand;
|
|
47
53
|
export declare function cliBaseUrl(): string;
|
|
48
|
-
export declare function cliServerCommandCandidates(platform?: NodeJS.Platform): CliServerCommand[];
|
|
54
|
+
export declare function cliServerCommandCandidates(platform?: NodeJS.Platform, port?: number): CliServerCommand[];
|
|
55
|
+
export declare function isCliServerPortConflictFailure(failure: unknown): boolean;
|
|
56
|
+
export declare function registerCliShutdownCleanup(cleanup: () => void, options?: {
|
|
57
|
+
targetProcess?: NodeJS.Process;
|
|
58
|
+
killProcess?: typeof process.kill;
|
|
59
|
+
}): () => void;
|
|
60
|
+
export declare function reserveCliServerPort(preferredPort?: number, attemptedPorts?: ReadonlySet<number>): Promise<number>;
|
|
49
61
|
export declare function closeCliServerProcess(proc: SpawnedCliServerProcess, platform?: NodeJS.Platform, killProcess?: typeof process.kill, spawnProcess?: typeof spawn): void;
|
|
50
|
-
export declare function tryStartCliOpencodeServer(candidate: CliServerCommand, spawnProcess?: typeof spawn, closeProcess?: typeof closeCliServerProcess): Promise<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
export declare function tryStartCliOpencodeServer(candidate: CliServerCommand, spawnProcess?: typeof spawn, closeProcess?: typeof closeCliServerProcess): Promise<StartedCliServer>;
|
|
63
|
+
export declare function spawnCliServerWatchdog(targetPid: number | undefined, ttlMs?: number, spawnProcess?: typeof spawn, nodeExecPath?: string): import("child_process").ChildProcess | undefined;
|
|
64
|
+
export declare function startCliOpencodeServer(options?: {
|
|
65
|
+
platform?: NodeJS.Platform;
|
|
66
|
+
spawnProcess?: typeof spawn;
|
|
67
|
+
spawnWatchdogProcess?: typeof spawn;
|
|
68
|
+
closeProcess?: typeof closeCliServerProcess;
|
|
69
|
+
reservePort?: CliPortResolver;
|
|
70
|
+
tempServerTtlMs?: number;
|
|
71
|
+
}): Promise<StartedCliServer>;
|
|
54
72
|
export declare function runCli(argv: string[]): Promise<string>;
|
|
55
73
|
export declare function cliExitCodeForError(message: string): 0 | 1;
|
|
56
74
|
export declare function cliShouldRunMain(argv1?: string, modulePath?: string, resolvePath?: (filePath: string) => string): boolean;
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
+
import { createServer } from 'node:net';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { createOpencodeClient } from '@opencode-ai/sdk/client';
|
|
7
8
|
import { cliCurrentLabel, renderCliDashboard, renderCliHistoryDashboard, } from './cli_render.js';
|
|
@@ -11,7 +12,7 @@ import { sinceFromLast } from './period.js';
|
|
|
11
12
|
import { authFilePath, loadConfig, loadState,
|
|
12
13
|
// reused for CLI-local config layering
|
|
13
14
|
quotaConfigPaths, resolveOpencodeDataDir, stateFilePath, } from './storage.js';
|
|
14
|
-
import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, listCurrentProviderIDs, } from './provider_catalog.js';
|
|
15
|
+
import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, listConnectedProviderIDs, listCurrentProviderIDs, } from './provider_catalog.js';
|
|
15
16
|
import { createUsageService } from './usage_service.js';
|
|
16
17
|
function emptyProviderUsage(usage) {
|
|
17
18
|
return {
|
|
@@ -37,9 +38,20 @@ function strictFilterHistoryProviders(history, allowedProviderIDs) {
|
|
|
37
38
|
}
|
|
38
39
|
return filterHistoryProvidersForDisplay(history, allowedProviderIDs);
|
|
39
40
|
}
|
|
41
|
+
function cliQuotaProviderIDs(usageProviderIDs, connectedProviderIDs) {
|
|
42
|
+
return [...new Set([...usageProviderIDs, ...connectedProviderIDs])];
|
|
43
|
+
}
|
|
40
44
|
const DEFAULT_OPENCODE_BASE_URL = 'http://localhost:4096';
|
|
41
|
-
const
|
|
45
|
+
const DEFAULT_OPENCODE_HOST = '127.0.0.1';
|
|
46
|
+
const DEFAULT_OPENCODE_PORT = 4096;
|
|
47
|
+
const CLI_SERVER_TIMEOUT_MS = 60_000;
|
|
42
48
|
const CLI_FORCE_EXIT_DELAY_MS = 100;
|
|
49
|
+
const CLI_SERVER_PORT_RETRY_LIMIT = 5;
|
|
50
|
+
const CLI_PORT_RESERVE_RETRY_LIMIT = 16;
|
|
51
|
+
const CLI_TEMP_SERVER_TTL_MS = 10 * 60_000;
|
|
52
|
+
const CLI_TEMP_SERVER_HINT = 'CLI note: if the temporary OpenCode server keeps failing, run `opencode serve` manually to inspect logs, or set `OPENCODE_BASE_URL`.';
|
|
53
|
+
const CLI_SHUTDOWN_HOOK_KEY = Symbol('quota-sidebar.cliShutdownHook');
|
|
54
|
+
const CLI_SHUTDOWN_CALLBACKS_KEY = Symbol('quota-sidebar.cliShutdownCallbacks');
|
|
43
55
|
export function releaseCliServerProcess(proc) {
|
|
44
56
|
// The CLI only needs the child pipes until the server prints its listen URL.
|
|
45
57
|
// After that, unref/destroy them so the parent process can exit cleanly.
|
|
@@ -174,24 +186,158 @@ export function cliBaseUrl() {
|
|
|
174
186
|
function isDefaultBaseUrl() {
|
|
175
187
|
return !process.env.OPENCODE_BASE_URL?.trim();
|
|
176
188
|
}
|
|
177
|
-
export function cliServerCommandCandidates(platform = process.platform) {
|
|
178
|
-
const directArgs = [
|
|
189
|
+
export function cliServerCommandCandidates(platform = process.platform, port = DEFAULT_OPENCODE_PORT) {
|
|
190
|
+
const directArgs = [
|
|
191
|
+
'serve',
|
|
192
|
+
`--hostname=${DEFAULT_OPENCODE_HOST}`,
|
|
193
|
+
`--port=${port}`,
|
|
194
|
+
];
|
|
195
|
+
const directCommand = `opencode serve --hostname=${DEFAULT_OPENCODE_HOST} --port=${port}`;
|
|
179
196
|
if (platform === 'win32') {
|
|
180
197
|
return [
|
|
181
198
|
{ command: 'opencode.cmd', args: directArgs },
|
|
182
199
|
{
|
|
183
|
-
command:
|
|
200
|
+
command: directCommand,
|
|
184
201
|
args: [],
|
|
185
202
|
shell: true,
|
|
186
203
|
},
|
|
187
204
|
{
|
|
188
205
|
command: 'bash',
|
|
189
|
-
args: ['-lc',
|
|
206
|
+
args: ['-lc', directCommand],
|
|
190
207
|
},
|
|
191
208
|
];
|
|
192
209
|
}
|
|
193
210
|
return [{ command: 'opencode', args: directArgs }];
|
|
194
211
|
}
|
|
212
|
+
function cliServerFailureOutput(failure) {
|
|
213
|
+
if (typeof failure !== 'object' ||
|
|
214
|
+
failure === null ||
|
|
215
|
+
!('output' in failure)) {
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
const output = failure.output;
|
|
219
|
+
return typeof output === 'string' ? output : '';
|
|
220
|
+
}
|
|
221
|
+
function cliServerFailureError(failure) {
|
|
222
|
+
if (typeof failure === 'object' && failure !== null && 'error' in failure) {
|
|
223
|
+
const error = failure.error;
|
|
224
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
225
|
+
}
|
|
226
|
+
return failure instanceof Error ? failure : new Error(String(failure));
|
|
227
|
+
}
|
|
228
|
+
function isRecoverableCliServerFailure(failure) {
|
|
229
|
+
return (typeof failure === 'object' &&
|
|
230
|
+
failure !== null &&
|
|
231
|
+
'recoverable' in failure &&
|
|
232
|
+
failure.recoverable === true);
|
|
233
|
+
}
|
|
234
|
+
export function isCliServerPortConflictFailure(failure) {
|
|
235
|
+
const text = [
|
|
236
|
+
cliServerFailureError(failure).message,
|
|
237
|
+
cliServerFailureOutput(failure),
|
|
238
|
+
]
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.join('\n');
|
|
241
|
+
return /eaddrinuse|address already in use|port .*already in use|listen .*in use/i.test(text);
|
|
242
|
+
}
|
|
243
|
+
function describeCliError(error) {
|
|
244
|
+
return error instanceof Error ? error.message : String(error);
|
|
245
|
+
}
|
|
246
|
+
function formatCliTempServerFailure(prefix, error) {
|
|
247
|
+
return `${prefix}: ${describeCliError(error)}\n\n${CLI_TEMP_SERVER_HINT}`;
|
|
248
|
+
}
|
|
249
|
+
function runCliShutdownCallbacks(targetProcess) {
|
|
250
|
+
const callbacks = targetProcess[CLI_SHUTDOWN_CALLBACKS_KEY];
|
|
251
|
+
if (!callbacks)
|
|
252
|
+
return;
|
|
253
|
+
for (const callback of Array.from(callbacks)) {
|
|
254
|
+
try {
|
|
255
|
+
callback();
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Best-effort cleanup for CLI temp servers.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function registerCliShutdownCleanup(cleanup, options) {
|
|
263
|
+
const targetProcess = options?.targetProcess ||
|
|
264
|
+
process;
|
|
265
|
+
const callbacks = targetProcess[CLI_SHUTDOWN_CALLBACKS_KEY] ||
|
|
266
|
+
(targetProcess[CLI_SHUTDOWN_CALLBACKS_KEY] = new Set());
|
|
267
|
+
callbacks.add(cleanup);
|
|
268
|
+
if (!targetProcess[CLI_SHUTDOWN_HOOK_KEY]) {
|
|
269
|
+
targetProcess[CLI_SHUTDOWN_HOOK_KEY] = true;
|
|
270
|
+
targetProcess.once('exit', () => {
|
|
271
|
+
runCliShutdownCallbacks(targetProcess);
|
|
272
|
+
});
|
|
273
|
+
targetProcess.once('uncaughtExceptionMonitor', () => {
|
|
274
|
+
runCliShutdownCallbacks(targetProcess);
|
|
275
|
+
});
|
|
276
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
277
|
+
targetProcess.once(signal, () => {
|
|
278
|
+
runCliShutdownCallbacks(targetProcess);
|
|
279
|
+
if (typeof targetProcess.pid === 'number' && targetProcess.pid > 0) {
|
|
280
|
+
;
|
|
281
|
+
(options?.killProcess ?? process.kill)(targetProcess.pid, signal);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return () => {
|
|
287
|
+
callbacks.delete(cleanup);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function tryReserveCliServerPort(port) {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const server = createServer();
|
|
293
|
+
let settled = false;
|
|
294
|
+
const finish = (value, error) => {
|
|
295
|
+
if (settled)
|
|
296
|
+
return;
|
|
297
|
+
settled = true;
|
|
298
|
+
server.removeAllListeners();
|
|
299
|
+
if (error) {
|
|
300
|
+
reject(error);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
resolve(value);
|
|
304
|
+
};
|
|
305
|
+
server.once('error', (error) => {
|
|
306
|
+
const code = error.code;
|
|
307
|
+
if (code === 'EADDRINUSE' || code === 'EACCES') {
|
|
308
|
+
finish(undefined);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
finish(undefined, error);
|
|
312
|
+
});
|
|
313
|
+
server.listen({ host: DEFAULT_OPENCODE_HOST, port, exclusive: true }, () => {
|
|
314
|
+
const address = server.address();
|
|
315
|
+
const resolvedPort = typeof address === 'object' && address ? address.port : port;
|
|
316
|
+
server.close((error) => {
|
|
317
|
+
if (error) {
|
|
318
|
+
finish(undefined, error);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
finish(resolvedPort);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
server.unref();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
export async function reserveCliServerPort(preferredPort = DEFAULT_OPENCODE_PORT, attemptedPorts = new Set()) {
|
|
328
|
+
if (!attemptedPorts.has(preferredPort)) {
|
|
329
|
+
const preferred = await tryReserveCliServerPort(preferredPort);
|
|
330
|
+
if (preferred !== undefined)
|
|
331
|
+
return preferred;
|
|
332
|
+
}
|
|
333
|
+
for (let attempt = 0; attempt < CLI_PORT_RESERVE_RETRY_LIMIT; attempt++) {
|
|
334
|
+
const fallback = await tryReserveCliServerPort(0);
|
|
335
|
+
if (fallback === undefined || attemptedPorts.has(fallback))
|
|
336
|
+
continue;
|
|
337
|
+
return fallback;
|
|
338
|
+
}
|
|
339
|
+
throw new Error('Failed to reserve a local port for the temporary OpenCode server');
|
|
340
|
+
}
|
|
195
341
|
function releaseCliServerPipes(proc, inspect, onError, onExit) {
|
|
196
342
|
if (inspect) {
|
|
197
343
|
proc.stdout?.removeListener('data', inspect);
|
|
@@ -307,35 +453,97 @@ export async function tryStartCliOpencodeServer(candidate, spawnProcess = spawn,
|
|
|
307
453
|
});
|
|
308
454
|
return {
|
|
309
455
|
url,
|
|
456
|
+
pid: proc.pid,
|
|
310
457
|
close: () => closeProcess(proc),
|
|
311
458
|
};
|
|
312
459
|
}
|
|
313
|
-
|
|
314
|
-
|
|
460
|
+
export function spawnCliServerWatchdog(targetPid, ttlMs = CLI_TEMP_SERVER_TTL_MS, spawnProcess = spawn, nodeExecPath = process.execPath) {
|
|
461
|
+
if (typeof targetPid !== 'number' ||
|
|
462
|
+
targetPid <= 0 ||
|
|
463
|
+
!Number.isFinite(ttlMs) ||
|
|
464
|
+
ttlMs <= 0) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const script = [
|
|
468
|
+
"const { spawn } = require('node:child_process')",
|
|
469
|
+
`const pid = ${JSON.stringify(targetPid)}`,
|
|
470
|
+
`const ttl = ${JSON.stringify(Math.floor(ttlMs))}`,
|
|
471
|
+
'setTimeout(() => {',
|
|
472
|
+
" if (process.platform === 'win32') {",
|
|
473
|
+
" const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true })",
|
|
474
|
+
' killer.unref?.()',
|
|
475
|
+
' process.exit(0)',
|
|
476
|
+
' }',
|
|
477
|
+
' try {',
|
|
478
|
+
" process.kill(-pid, 'SIGTERM')",
|
|
479
|
+
' } catch (error) {',
|
|
480
|
+
" if (!error || (typeof error === 'object' && error !== null && error.code !== 'ESRCH')) {",
|
|
481
|
+
" try { process.kill(pid, 'SIGTERM') } catch {}",
|
|
482
|
+
' }',
|
|
483
|
+
' }',
|
|
484
|
+
' process.exit(0)',
|
|
485
|
+
'}, ttl)',
|
|
486
|
+
].join('\n');
|
|
487
|
+
const proc = spawnProcess(nodeExecPath, ['-e', script], {
|
|
488
|
+
stdio: 'ignore',
|
|
489
|
+
detached: true,
|
|
490
|
+
windowsHide: true,
|
|
491
|
+
});
|
|
492
|
+
proc.unref();
|
|
493
|
+
return proc;
|
|
494
|
+
}
|
|
495
|
+
export async function startCliOpencodeServer(options) {
|
|
496
|
+
const platform = options?.platform ?? process.platform;
|
|
497
|
+
const spawnProcess = options?.spawnProcess ?? spawn;
|
|
498
|
+
const spawnWatchdogProcess = options?.spawnWatchdogProcess ?? spawn;
|
|
499
|
+
const closeProcess = options?.closeProcess ?? closeCliServerProcess;
|
|
500
|
+
const resolvePort = options?.reservePort ?? reserveCliServerPort;
|
|
501
|
+
const tempServerTtlMs = options?.tempServerTtlMs ?? CLI_TEMP_SERVER_TTL_MS;
|
|
315
502
|
let lastError;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
:
|
|
330
|
-
|
|
503
|
+
const attemptedPorts = new Set();
|
|
504
|
+
for (let attempt = 0; attempt < CLI_SERVER_PORT_RETRY_LIMIT; attempt++) {
|
|
505
|
+
const port = await resolvePort(DEFAULT_OPENCODE_PORT, attemptedPorts);
|
|
506
|
+
attemptedPorts.add(port);
|
|
507
|
+
for (const candidate of cliServerCommandCandidates(platform, port)) {
|
|
508
|
+
try {
|
|
509
|
+
const server = await tryStartCliOpencodeServer(candidate, spawnProcess, closeProcess);
|
|
510
|
+
let watchdogProc;
|
|
511
|
+
try {
|
|
512
|
+
watchdogProc = spawnCliServerWatchdog(server.pid, tempServerTtlMs, spawnWatchdogProcess);
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
server.close();
|
|
516
|
+
throw new Error(`Failed to start temporary OpenCode server watchdog: ${describeCliError(error)}`);
|
|
517
|
+
}
|
|
518
|
+
const stopWatchdog = () => {
|
|
519
|
+
if (!watchdogProc)
|
|
520
|
+
return;
|
|
521
|
+
const proc = watchdogProc;
|
|
522
|
+
watchdogProc = undefined;
|
|
523
|
+
closeProcess(proc);
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
...server,
|
|
527
|
+
close: () => {
|
|
528
|
+
stopWatchdog();
|
|
529
|
+
server.close();
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
catch (failure) {
|
|
534
|
+
lastError = failure;
|
|
535
|
+
if (isCliServerPortConflictFailure(failure))
|
|
536
|
+
break;
|
|
537
|
+
if (!isRecoverableCliServerFailure(failure)) {
|
|
538
|
+
throw cliServerFailureError(failure);
|
|
539
|
+
}
|
|
331
540
|
}
|
|
332
541
|
}
|
|
542
|
+
if (!isCliServerPortConflictFailure(lastError))
|
|
543
|
+
break;
|
|
333
544
|
}
|
|
334
|
-
|
|
335
|
-
? lastError
|
|
336
|
-
: lastError;
|
|
337
|
-
throw error instanceof Error
|
|
338
|
-
? error
|
|
545
|
+
throw lastError
|
|
546
|
+
? cliServerFailureError(lastError)
|
|
339
547
|
: new Error('Failed to start OpenCode server');
|
|
340
548
|
}
|
|
341
549
|
async function resolvePathInfo(directory) {
|
|
@@ -360,7 +568,27 @@ async function resolvePathInfo(directory) {
|
|
|
360
568
|
if (!isDefaultBaseUrl()) {
|
|
361
569
|
throw new Error(`Failed to connect to OpenCode API at ${cliBaseUrl()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
362
570
|
}
|
|
363
|
-
|
|
571
|
+
let server;
|
|
572
|
+
try {
|
|
573
|
+
server = await startCliOpencodeServer();
|
|
574
|
+
}
|
|
575
|
+
catch (startError) {
|
|
576
|
+
throw new Error(formatCliTempServerFailure('Failed to auto-start a temporary OpenCode server', startError));
|
|
577
|
+
}
|
|
578
|
+
let closed = false;
|
|
579
|
+
const unregisterTempServerCleanup = registerCliShutdownCleanup(() => {
|
|
580
|
+
if (closed)
|
|
581
|
+
return;
|
|
582
|
+
closed = true;
|
|
583
|
+
server.close();
|
|
584
|
+
});
|
|
585
|
+
const closeTempServer = () => {
|
|
586
|
+
if (closed)
|
|
587
|
+
return;
|
|
588
|
+
closed = true;
|
|
589
|
+
unregisterTempServerCleanup();
|
|
590
|
+
server.close();
|
|
591
|
+
};
|
|
364
592
|
try {
|
|
365
593
|
const client = createOpencodeClient({
|
|
366
594
|
directory,
|
|
@@ -375,12 +603,12 @@ async function resolvePathInfo(directory) {
|
|
|
375
603
|
client,
|
|
376
604
|
worktree: data.worktree || directory,
|
|
377
605
|
directory: data.directory || directory,
|
|
378
|
-
close:
|
|
606
|
+
close: closeTempServer,
|
|
379
607
|
};
|
|
380
608
|
}
|
|
381
609
|
catch (innerError) {
|
|
382
|
-
|
|
383
|
-
throw innerError;
|
|
610
|
+
closeTempServer();
|
|
611
|
+
throw new Error(formatCliTempServerFailure(`Failed to query the temporary OpenCode server at ${server.url}`, innerError));
|
|
384
612
|
}
|
|
385
613
|
}
|
|
386
614
|
}
|
|
@@ -431,15 +659,21 @@ export async function runCli(argv) {
|
|
|
431
659
|
listDescendantSessionIDs: async () => [],
|
|
432
660
|
},
|
|
433
661
|
});
|
|
434
|
-
const allowedProviderIDs = await
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
662
|
+
const [allowedProviderIDs, connectedProviderIDs] = await Promise.all([
|
|
663
|
+
listCurrentProviderIDs({
|
|
664
|
+
client,
|
|
665
|
+
directory,
|
|
666
|
+
}),
|
|
667
|
+
listConnectedProviderIDs({
|
|
668
|
+
client,
|
|
669
|
+
directory,
|
|
670
|
+
}),
|
|
671
|
+
]);
|
|
438
672
|
if (command.since || command.last !== undefined) {
|
|
439
673
|
const resolvedSince = command.since || sinceFromLast(command.period, command.last);
|
|
440
674
|
const historyRaw = await usageService.summarizeHistoryUsage(command.period, resolvedSince);
|
|
441
675
|
const history = strictFilterHistoryProviders(historyRaw, allowedProviderIDs);
|
|
442
|
-
const quotas = await quotaService.getQuotaSnapshots(Object.keys(history.total.providers));
|
|
676
|
+
const quotas = await quotaService.getQuotaSnapshots(cliQuotaProviderIDs(Object.keys(history.total.providers), connectedProviderIDs));
|
|
443
677
|
return renderCliHistoryDashboard({
|
|
444
678
|
result: history,
|
|
445
679
|
quotas,
|
|
@@ -449,7 +683,7 @@ export async function runCli(argv) {
|
|
|
449
683
|
}
|
|
450
684
|
const usageRaw = await usageService.summarizeForTool(command.period, '', false);
|
|
451
685
|
const usage = strictFilterUsageProviders(usageRaw, allowedProviderIDs);
|
|
452
|
-
const quotas = await quotaService.getQuotaSnapshots(Object.keys(usage.providers));
|
|
686
|
+
const quotas = await quotaService.getQuotaSnapshots(cliQuotaProviderIDs(Object.keys(usage.providers), connectedProviderIDs));
|
|
453
687
|
return renderCliDashboard({
|
|
454
688
|
label: cliCurrentLabel(command.period),
|
|
455
689
|
usage,
|
|
@@ -4,5 +4,9 @@ export declare function listCurrentProviderIDs(input: {
|
|
|
4
4
|
client: unknown;
|
|
5
5
|
directory: string;
|
|
6
6
|
}): Promise<Set<string>>;
|
|
7
|
+
export declare function listConnectedProviderIDs(input: {
|
|
8
|
+
client: unknown;
|
|
9
|
+
directory: string;
|
|
10
|
+
}): Promise<Set<string>>;
|
|
7
11
|
export declare function filterUsageProvidersForDisplay(usage: UsageSummary, allowedProviderIDs: ReadonlySet<string>): UsageSummary;
|
|
8
12
|
export declare function filterHistoryProvidersForDisplay(result: HistoryUsageResult, allowedProviderIDs: ReadonlySet<string>): HistoryUsageResult;
|
package/dist/provider_catalog.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
function isRecord(value) {
|
|
2
2
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
3
|
}
|
|
4
|
-
function
|
|
5
|
-
|
|
4
|
+
function providerDataFromResponse(response) {
|
|
5
|
+
return isRecord(response) &&
|
|
6
|
+
Object.prototype.hasOwnProperty.call(response, 'data')
|
|
6
7
|
? response.data
|
|
7
8
|
: undefined;
|
|
9
|
+
}
|
|
10
|
+
function providerListFromResponse(response) {
|
|
11
|
+
const data = providerDataFromResponse(response);
|
|
8
12
|
const record = isRecord(data) ? data : undefined;
|
|
9
13
|
return Array.isArray(record?.providers)
|
|
10
14
|
? record.providers
|
|
@@ -14,6 +18,13 @@ function providerListFromResponse(response) {
|
|
|
14
18
|
? data
|
|
15
19
|
: undefined;
|
|
16
20
|
}
|
|
21
|
+
function connectedProviderIDsFromResponse(response) {
|
|
22
|
+
const data = providerDataFromResponse(response);
|
|
23
|
+
const record = isRecord(data) ? data : undefined;
|
|
24
|
+
return Array.isArray(record?.connected)
|
|
25
|
+
? record.connected.filter((item) => typeof item === 'string' && !!item)
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
17
28
|
export async function listCurrentProviderIDs(input) {
|
|
18
29
|
const client = input.client;
|
|
19
30
|
const ids = new Set();
|
|
@@ -46,6 +57,34 @@ export async function listCurrentProviderIDs(input) {
|
|
|
46
57
|
}
|
|
47
58
|
return ids;
|
|
48
59
|
}
|
|
60
|
+
export async function listConnectedProviderIDs(input) {
|
|
61
|
+
const client = input.client;
|
|
62
|
+
const ids = new Set();
|
|
63
|
+
const collect = (response) => {
|
|
64
|
+
const connected = connectedProviderIDsFromResponse(response);
|
|
65
|
+
if (!connected)
|
|
66
|
+
return false;
|
|
67
|
+
for (const providerID of connected)
|
|
68
|
+
ids.add(providerID);
|
|
69
|
+
return ids.size > 0;
|
|
70
|
+
};
|
|
71
|
+
if (client.config?.providers) {
|
|
72
|
+
const response = await client.config.providers({
|
|
73
|
+
query: { directory: input.directory },
|
|
74
|
+
throwOnError: true,
|
|
75
|
+
});
|
|
76
|
+
if (collect(response))
|
|
77
|
+
return ids;
|
|
78
|
+
}
|
|
79
|
+
if (client.provider?.list) {
|
|
80
|
+
const response = await client.provider.list({
|
|
81
|
+
query: { directory: input.directory },
|
|
82
|
+
throwOnError: true,
|
|
83
|
+
});
|
|
84
|
+
collect(response);
|
|
85
|
+
}
|
|
86
|
+
return ids;
|
|
87
|
+
}
|
|
49
88
|
export function filterUsageProvidersForDisplay(usage, allowedProviderIDs) {
|
|
50
89
|
if (allowedProviderIDs.size === 0)
|
|
51
90
|
return usage;
|
package/dist/tui.tsx
CHANGED
|
@@ -253,7 +253,6 @@ function QuotaGroupBlock(props: {
|
|
|
253
253
|
group: SidebarQuotaGroup
|
|
254
254
|
bullet: boolean
|
|
255
255
|
}) {
|
|
256
|
-
const detailColor = quotaToneColor(props.api, props.group.tone)
|
|
257
256
|
const content = (
|
|
258
257
|
<box gap={0}>
|
|
259
258
|
<text>
|
|
@@ -261,11 +260,14 @@ function QuotaGroupBlock(props: {
|
|
|
261
260
|
{props.group.shortLabel}
|
|
262
261
|
</span>
|
|
263
262
|
<Show when={props.group.detail}>
|
|
264
|
-
<span style={{ fg:
|
|
263
|
+
<span style={{ fg: props.api.theme.current.textMuted }}>
|
|
264
|
+
{' '}
|
|
265
|
+
{props.group.detail}
|
|
266
|
+
</span>
|
|
265
267
|
</Show>
|
|
266
268
|
</text>
|
|
267
269
|
<For each={props.group.continuationLines}>
|
|
268
|
-
{(line) => <text fg={
|
|
270
|
+
{(line) => <text fg={props.api.theme.current.textMuted}>{line}</text>}
|
|
269
271
|
</For>
|
|
270
272
|
</box>
|
|
271
273
|
)
|
package/package.json
CHANGED