@getrouter/getrouter-cli 0.1.13 โ 0.1.14
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/bin.mjs +522 -441
- package/package.json +1 -1
- package/src/cli.ts +4 -2
- package/src/cmd/auth.ts +2 -2
- package/src/cmd/claude.ts +2 -2
- package/src/cmd/codex.ts +24 -16
- package/src/cmd/env.ts +15 -11
- package/src/cmd/index.ts +2 -2
- package/src/cmd/keys.ts +59 -35
- package/src/cmd/models.ts +24 -26
- package/src/cmd/status.ts +113 -62
- package/src/cmd/usages.ts +4 -4
- package/src/core/api/client.ts +12 -14
- package/src/core/api/pagination.ts +3 -3
- package/src/core/api/providerModels.ts +15 -14
- package/src/core/auth/device.ts +38 -23
- package/src/core/auth/index.ts +19 -11
- package/src/core/auth/refresh.ts +22 -17
- package/src/core/config/fs.ts +6 -6
- package/src/core/config/index.ts +16 -11
- package/src/core/config/paths.ts +12 -4
- package/src/core/config/redact.ts +4 -4
- package/src/core/config/types.ts +14 -10
- package/src/core/http/errors.ts +18 -10
- package/src/core/http/request.ts +29 -34
- package/src/core/http/retry.ts +37 -24
- package/src/core/http/url.ts +11 -6
- package/src/core/interactive/clipboard.ts +10 -9
- package/src/core/interactive/keys.ts +22 -26
- package/src/core/output/table.ts +5 -5
- package/src/core/output/usages.ts +34 -33
- package/src/core/setup/codex.ts +195 -142
- package/src/core/setup/env.ts +49 -42
- package/src/core/usages/aggregate.ts +3 -3
package/dist/bin.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { randomInt } from "node:crypto";
|
|
|
8
8
|
import prompts from "prompts";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "0.1.
|
|
11
|
+
var version = "0.1.13";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
@@ -204,15 +204,15 @@ function createAuthServiceClient(handler) {
|
|
|
204
204
|
|
|
205
205
|
//#endregion
|
|
206
206
|
//#region src/core/config/fs.ts
|
|
207
|
-
|
|
207
|
+
function getCorruptBackupPath(filePath) {
|
|
208
208
|
const dir = path.dirname(filePath);
|
|
209
209
|
const ext = path.extname(filePath);
|
|
210
210
|
const base = path.basename(filePath, ext);
|
|
211
211
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
212
212
|
const rand = Math.random().toString(16).slice(2, 8);
|
|
213
213
|
return path.join(dir, `${base}.corrupt-${stamp}-${rand}${ext}`);
|
|
214
|
-
}
|
|
215
|
-
|
|
214
|
+
}
|
|
215
|
+
function readJsonFile(filePath) {
|
|
216
216
|
if (!fs.existsSync(filePath)) return null;
|
|
217
217
|
let raw;
|
|
218
218
|
try {
|
|
@@ -233,62 +233,101 @@ const readJsonFile = (filePath) => {
|
|
|
233
233
|
}
|
|
234
234
|
return null;
|
|
235
235
|
}
|
|
236
|
-
}
|
|
237
|
-
|
|
236
|
+
}
|
|
237
|
+
function writeJsonFile(filePath, value) {
|
|
238
238
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
239
239
|
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
|
|
240
|
-
}
|
|
240
|
+
}
|
|
241
241
|
|
|
242
242
|
//#endregion
|
|
243
243
|
//#region src/core/config/paths.ts
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
function resolveConfigDir() {
|
|
245
|
+
return process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
|
|
246
|
+
}
|
|
247
|
+
function getConfigPath() {
|
|
248
|
+
return path.join(resolveConfigDir(), "config.json");
|
|
249
|
+
}
|
|
250
|
+
function getAuthPath() {
|
|
251
|
+
return path.join(resolveConfigDir(), "auth.json");
|
|
252
|
+
}
|
|
247
253
|
|
|
248
254
|
//#endregion
|
|
249
255
|
//#region src/core/config/types.ts
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
function defaultConfig() {
|
|
257
|
+
return {
|
|
258
|
+
apiBase: "https://getrouter.dev",
|
|
259
|
+
json: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function defaultAuthState() {
|
|
263
|
+
return {
|
|
264
|
+
accessToken: "",
|
|
265
|
+
refreshToken: "",
|
|
266
|
+
expiresAt: "",
|
|
267
|
+
tokenType: "Bearer"
|
|
268
|
+
};
|
|
269
|
+
}
|
|
260
270
|
|
|
261
271
|
//#endregion
|
|
262
272
|
//#region src/core/config/index.ts
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
function readConfig() {
|
|
274
|
+
return {
|
|
275
|
+
...defaultConfig(),
|
|
276
|
+
...readJsonFile(getConfigPath()) ?? {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function readAuth() {
|
|
280
|
+
return {
|
|
281
|
+
...defaultAuthState(),
|
|
282
|
+
...readJsonFile(getAuthPath()) ?? {}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function writeAuth(auth) {
|
|
272
286
|
const authPath = getAuthPath();
|
|
273
287
|
writeJsonFile(authPath, auth);
|
|
274
288
|
if (process.platform !== "win32") fs.chmodSync(authPath, 384);
|
|
275
|
-
}
|
|
289
|
+
}
|
|
276
290
|
|
|
277
291
|
//#endregion
|
|
278
292
|
//#region src/core/http/url.ts
|
|
279
|
-
|
|
293
|
+
function getApiBase() {
|
|
280
294
|
return (readConfig().apiBase || "").replace(/\/+$/, "");
|
|
281
|
-
}
|
|
282
|
-
|
|
295
|
+
}
|
|
296
|
+
function buildApiUrl(path$1) {
|
|
283
297
|
const base = getApiBase();
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
}
|
|
298
|
+
const normalizedPath = path$1.replace(/^\/+/, "");
|
|
299
|
+
if (base) return `${base}/${normalizedPath}`;
|
|
300
|
+
return `/${normalizedPath}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/core/auth/index.ts
|
|
305
|
+
function isTokenExpired(expiresAt, bufferMs = 0) {
|
|
306
|
+
if (!expiresAt) return true;
|
|
307
|
+
const timestampMs = Date.parse(expiresAt);
|
|
308
|
+
if (Number.isNaN(timestampMs)) return true;
|
|
309
|
+
return timestampMs <= Date.now() + bufferMs;
|
|
310
|
+
}
|
|
311
|
+
function getAuthStatus() {
|
|
312
|
+
const auth = readAuth();
|
|
313
|
+
if (!Boolean(auth.accessToken && auth.refreshToken) || isTokenExpired(auth.expiresAt)) return { status: "logged_out" };
|
|
314
|
+
return {
|
|
315
|
+
status: "logged_in",
|
|
316
|
+
expiresAt: auth.expiresAt,
|
|
317
|
+
accessToken: auth.accessToken,
|
|
318
|
+
refreshToken: auth.refreshToken,
|
|
319
|
+
tokenType: auth.tokenType
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function clearAuth() {
|
|
323
|
+
writeAuth(defaultAuthState());
|
|
324
|
+
}
|
|
287
325
|
|
|
288
326
|
//#endregion
|
|
289
327
|
//#region src/core/auth/refresh.ts
|
|
290
328
|
const EXPIRY_BUFFER_MS = 60 * 1e3;
|
|
291
|
-
|
|
329
|
+
async function refreshAccessToken(options) {
|
|
330
|
+
const { fetchImpl } = options;
|
|
292
331
|
const auth = readAuth();
|
|
293
332
|
if (!auth.refreshToken) return null;
|
|
294
333
|
const res = await (fetchImpl ?? fetch)(buildApiUrl("v1/dashboard/auth/token"), {
|
|
@@ -305,32 +344,44 @@ const refreshAccessToken = async ({ fetchImpl }) => {
|
|
|
305
344
|
tokenType: "Bearer"
|
|
306
345
|
});
|
|
307
346
|
return token;
|
|
308
|
-
}
|
|
347
|
+
}
|
|
309
348
|
|
|
310
349
|
//#endregion
|
|
311
350
|
//#region src/core/http/errors.ts
|
|
312
|
-
|
|
351
|
+
function createApiError(payload, fallbackMessage, status) {
|
|
313
352
|
const payloadObject = payload && typeof payload === "object" ? payload : void 0;
|
|
314
|
-
const message =
|
|
353
|
+
const message = typeof payloadObject?.message === "string" ? payloadObject.message : fallbackMessage;
|
|
315
354
|
const err = new Error(message);
|
|
316
|
-
|
|
317
|
-
if (
|
|
355
|
+
const code = payloadObject?.code;
|
|
356
|
+
if (typeof code === "string") err.code = code;
|
|
357
|
+
const details = payloadObject?.details;
|
|
358
|
+
if (details != null) err.details = details;
|
|
318
359
|
if (typeof status === "number") err.status = status;
|
|
319
360
|
return err;
|
|
320
|
-
}
|
|
361
|
+
}
|
|
321
362
|
|
|
322
363
|
//#endregion
|
|
323
364
|
//#region src/core/http/retry.ts
|
|
324
|
-
|
|
325
|
-
|
|
365
|
+
function defaultSleep(ms) {
|
|
366
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
367
|
+
}
|
|
368
|
+
function isServerError(status) {
|
|
369
|
+
return status >= 500 || status === 408 || status === 429;
|
|
370
|
+
}
|
|
371
|
+
function getErrorStatus(error) {
|
|
372
|
+
if (typeof error !== "object" || error === null) return;
|
|
373
|
+
if (!("status" in error)) return;
|
|
374
|
+
const status = error.status;
|
|
375
|
+
if (typeof status !== "number") return;
|
|
376
|
+
return status;
|
|
377
|
+
}
|
|
378
|
+
function isRetryableError(error, _attempt) {
|
|
326
379
|
if (error instanceof TypeError) return true;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
};
|
|
333
|
-
const withRetry = async (fn, options = {}) => {
|
|
380
|
+
const status = getErrorStatus(error);
|
|
381
|
+
if (status === void 0) return false;
|
|
382
|
+
return isServerError(status);
|
|
383
|
+
}
|
|
384
|
+
async function withRetry(fn, options = {}) {
|
|
334
385
|
const { maxRetries = 3, initialDelayMs = 1e3, maxDelayMs = 1e4, shouldRetry = isRetryableError, onRetry, sleep = defaultSleep } = options;
|
|
335
386
|
let lastError;
|
|
336
387
|
let delay = initialDelayMs;
|
|
@@ -344,40 +395,38 @@ const withRetry = async (fn, options = {}) => {
|
|
|
344
395
|
delay = Math.min(delay * 2, maxDelayMs);
|
|
345
396
|
}
|
|
346
397
|
throw lastError;
|
|
347
|
-
}
|
|
348
|
-
const isServerError = (status) => status >= 500 || status === 408 || status === 429;
|
|
398
|
+
}
|
|
349
399
|
|
|
350
400
|
//#endregion
|
|
351
401
|
//#region src/core/http/request.ts
|
|
352
|
-
|
|
353
|
-
const
|
|
402
|
+
function getAuthCookieName() {
|
|
403
|
+
const routerCookieName = process.env.GETROUTER_AUTH_COOKIE;
|
|
404
|
+
if (routerCookieName) return routerCookieName;
|
|
405
|
+
const kratosCookieName = process.env.KRATOS_AUTH_COOKIE;
|
|
406
|
+
if (kratosCookieName) return kratosCookieName;
|
|
407
|
+
return "access_token";
|
|
408
|
+
}
|
|
409
|
+
function buildHeaders(accessToken) {
|
|
354
410
|
const headers = { "Content-Type": "application/json" };
|
|
355
411
|
if (accessToken) {
|
|
356
412
|
headers.Authorization = `Bearer ${accessToken}`;
|
|
357
413
|
headers.Cookie = `${getAuthCookieName()}=${accessToken}`;
|
|
358
414
|
}
|
|
359
415
|
return headers;
|
|
360
|
-
}
|
|
361
|
-
|
|
416
|
+
}
|
|
417
|
+
async function doFetch(url, method, headers, body, fetchImpl) {
|
|
362
418
|
return (fetchImpl ?? fetch)(url, {
|
|
363
419
|
method,
|
|
364
420
|
headers,
|
|
365
421
|
body: body == null ? void 0 : JSON.stringify(body)
|
|
366
422
|
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") return isServerError(error.status);
|
|
370
|
-
return error instanceof TypeError;
|
|
371
|
-
};
|
|
372
|
-
const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, includeAuth = true, _retrySleep }) => {
|
|
423
|
+
}
|
|
424
|
+
async function requestJson({ path: path$1, method, body, fetchImpl, maxRetries = 3, includeAuth = true, _retrySleep }) {
|
|
373
425
|
return withRetry(async () => {
|
|
374
|
-
const auth = includeAuth ? readAuth() : {
|
|
375
|
-
accessToken: void 0,
|
|
376
|
-
refreshToken: void 0
|
|
377
|
-
};
|
|
378
426
|
const url = buildApiUrl(path$1);
|
|
379
|
-
|
|
380
|
-
|
|
427
|
+
const auth = includeAuth ? readAuth() : void 0;
|
|
428
|
+
let res = await doFetch(url, method, buildHeaders(auth?.accessToken), body, fetchImpl);
|
|
429
|
+
if (includeAuth && res.status === 401 && auth?.refreshToken) {
|
|
381
430
|
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
382
431
|
if (refreshed?.accessToken) res = await doFetch(url, method, buildHeaders(refreshed.accessToken), body, fetchImpl);
|
|
383
432
|
}
|
|
@@ -385,21 +434,21 @@ const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries =
|
|
|
385
434
|
return await res.json();
|
|
386
435
|
}, {
|
|
387
436
|
maxRetries,
|
|
388
|
-
shouldRetry:
|
|
437
|
+
shouldRetry: isRetryableError,
|
|
389
438
|
sleep: _retrySleep
|
|
390
439
|
});
|
|
391
|
-
}
|
|
440
|
+
}
|
|
392
441
|
|
|
393
442
|
//#endregion
|
|
394
443
|
//#region src/core/api/client.ts
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
444
|
+
const defaultFactories = {
|
|
445
|
+
createConsumerServiceClient,
|
|
446
|
+
createAuthServiceClient,
|
|
447
|
+
createSubscriptionServiceClient,
|
|
448
|
+
createUsageServiceClient,
|
|
449
|
+
createModelServiceClient
|
|
450
|
+
};
|
|
451
|
+
function createApiClients({ fetchImpl, clients: factories = defaultFactories, includeAuth = true } = {}) {
|
|
403
452
|
const handler = async ({ path: path$1, method, body }) => {
|
|
404
453
|
return requestJson({
|
|
405
454
|
path: path$1,
|
|
@@ -416,30 +465,7 @@ const createApiClients = ({ fetchImpl, clients, includeAuth = true }) => {
|
|
|
416
465
|
subscriptionService: factories.createSubscriptionServiceClient(handler),
|
|
417
466
|
usageService: factories.createUsageServiceClient(handler)
|
|
418
467
|
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
//#endregion
|
|
422
|
-
//#region src/core/auth/index.ts
|
|
423
|
-
const isExpired = (expiresAt) => {
|
|
424
|
-
if (!expiresAt) return true;
|
|
425
|
-
const t = Date.parse(expiresAt);
|
|
426
|
-
if (Number.isNaN(t)) return true;
|
|
427
|
-
return t <= Date.now();
|
|
428
|
-
};
|
|
429
|
-
const getAuthStatus = () => {
|
|
430
|
-
const auth = readAuth();
|
|
431
|
-
if (!Boolean(auth.accessToken && auth.refreshToken) || isExpired(auth.expiresAt)) return { status: "logged_out" };
|
|
432
|
-
return {
|
|
433
|
-
status: "logged_in",
|
|
434
|
-
expiresAt: auth.expiresAt,
|
|
435
|
-
accessToken: auth.accessToken,
|
|
436
|
-
refreshToken: auth.refreshToken,
|
|
437
|
-
tokenType: auth.tokenType
|
|
438
|
-
};
|
|
439
|
-
};
|
|
440
|
-
const clearAuth = () => {
|
|
441
|
-
writeAuth(defaultAuthState());
|
|
442
|
-
};
|
|
468
|
+
}
|
|
443
469
|
|
|
444
470
|
//#endregion
|
|
445
471
|
//#region src/core/auth/device.ts
|
|
@@ -450,6 +476,11 @@ const generateAuthCode = () => {
|
|
|
450
476
|
return out;
|
|
451
477
|
};
|
|
452
478
|
const buildLoginUrl = (authCode) => `https://getrouter.dev/auth/${authCode}`;
|
|
479
|
+
const getErrorCode = (err) => {
|
|
480
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
481
|
+
if (!("code" in err)) return void 0;
|
|
482
|
+
return err.code;
|
|
483
|
+
};
|
|
453
484
|
const spawnBrowser = (command, args) => {
|
|
454
485
|
try {
|
|
455
486
|
const child = spawn(command, args, {
|
|
@@ -457,8 +488,10 @@ const spawnBrowser = (command, args) => {
|
|
|
457
488
|
detached: true
|
|
458
489
|
});
|
|
459
490
|
child.on("error", (err) => {
|
|
460
|
-
const code =
|
|
461
|
-
|
|
491
|
+
const code = getErrorCode(err);
|
|
492
|
+
let reason = "";
|
|
493
|
+
if (code === "ENOENT") reason = ` (${command} not found)`;
|
|
494
|
+
else if (typeof code === "string") reason = ` (${code})`;
|
|
462
495
|
console.log(`โ ๏ธ Unable to open browser${reason}. Please open the URL manually.`);
|
|
463
496
|
});
|
|
464
497
|
child.unref();
|
|
@@ -468,31 +501,42 @@ const spawnBrowser = (command, args) => {
|
|
|
468
501
|
};
|
|
469
502
|
const openLoginUrl = async (url) => {
|
|
470
503
|
try {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
"
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
504
|
+
const entry = {
|
|
505
|
+
darwin: {
|
|
506
|
+
command: "open",
|
|
507
|
+
args: [url]
|
|
508
|
+
},
|
|
509
|
+
win32: {
|
|
510
|
+
command: "cmd",
|
|
511
|
+
args: [
|
|
512
|
+
"/c",
|
|
513
|
+
"start",
|
|
514
|
+
"",
|
|
515
|
+
url
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
}[process.platform] ?? {
|
|
519
|
+
command: "xdg-open",
|
|
520
|
+
args: [url]
|
|
521
|
+
};
|
|
522
|
+
spawnBrowser(entry.command, entry.args);
|
|
485
523
|
} catch {}
|
|
486
524
|
};
|
|
487
525
|
const pollAuthorize = async ({ authorize, code, timeoutMs = 300 * 1e3, initialDelayMs = 1e3, maxDelayMs = 1e4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now(), onRetry }) => {
|
|
488
526
|
const start = now();
|
|
489
527
|
let delay = initialDelayMs;
|
|
490
528
|
let attempt = 0;
|
|
529
|
+
const getErrorStatus$1 = (err) => {
|
|
530
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
531
|
+
if (!("status" in err)) return void 0;
|
|
532
|
+
const status = err.status;
|
|
533
|
+
return typeof status === "number" ? status : void 0;
|
|
534
|
+
};
|
|
491
535
|
while (true) {
|
|
492
536
|
try {
|
|
493
537
|
return await authorize({ code });
|
|
494
538
|
} catch (err) {
|
|
495
|
-
const status =
|
|
539
|
+
const status = getErrorStatus$1(err);
|
|
496
540
|
if (status === 404) {} else if (status === 400) throw new Error("Auth code already used. Please log in again.");
|
|
497
541
|
else if (status === 403) throw new Error("Auth code expired. Please log in again.");
|
|
498
542
|
else throw err;
|
|
@@ -507,7 +551,7 @@ const pollAuthorize = async ({ authorize, code, timeoutMs = 300 * 1e3, initialDe
|
|
|
507
551
|
|
|
508
552
|
//#endregion
|
|
509
553
|
//#region src/cmd/auth.ts
|
|
510
|
-
|
|
554
|
+
function registerAuthCommands(program) {
|
|
511
555
|
program.command("login").description("Login with device flow").action(async () => {
|
|
512
556
|
const { authService } = createApiClients({ includeAuth: false });
|
|
513
557
|
const authCode = generateAuthCode();
|
|
@@ -532,7 +576,7 @@ const registerAuthCommands = (program) => {
|
|
|
532
576
|
clearAuth();
|
|
533
577
|
console.log("Cleared local auth data.");
|
|
534
578
|
});
|
|
535
|
-
}
|
|
579
|
+
}
|
|
536
580
|
|
|
537
581
|
//#endregion
|
|
538
582
|
//#region src/core/api/pagination.ts
|
|
@@ -544,7 +588,7 @@ const registerAuthCommands = (program) => {
|
|
|
544
588
|
* @param getNextToken - Function that extracts the next page token from the response
|
|
545
589
|
* @returns Array of all items across all pages
|
|
546
590
|
*/
|
|
547
|
-
|
|
591
|
+
async function fetchAllPages(fetchPage, getItems, getNextToken) {
|
|
548
592
|
const allItems = [];
|
|
549
593
|
let pageToken;
|
|
550
594
|
do {
|
|
@@ -554,7 +598,7 @@ const fetchAllPages = async (fetchPage, getItems, getNextToken) => {
|
|
|
554
598
|
pageToken = getNextToken(response);
|
|
555
599
|
} while (pageToken);
|
|
556
600
|
return allItems;
|
|
557
|
-
}
|
|
601
|
+
}
|
|
558
602
|
|
|
559
603
|
//#endregion
|
|
560
604
|
//#region src/core/interactive/fuzzy.ts
|
|
@@ -657,21 +701,28 @@ const promptKeyEnabled = async (initial) => {
|
|
|
657
701
|
enabled: typeof response.enabled === "boolean" ? response.enabled : initial
|
|
658
702
|
};
|
|
659
703
|
};
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
704
|
+
const fetchConsumers = async (consumerService) => fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
705
|
+
pageSize: void 0,
|
|
706
|
+
pageToken
|
|
707
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
708
|
+
const ensureConsumers = (consumers) => {
|
|
665
709
|
if (consumers.length === 0) {
|
|
666
710
|
console.log("No available API keys. Create one at https://getrouter.dev/dashboard/keys");
|
|
667
711
|
return null;
|
|
668
712
|
}
|
|
669
713
|
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
670
|
-
|
|
714
|
+
return {
|
|
715
|
+
sorted,
|
|
716
|
+
nameCounts: buildNameCounts(sorted)
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
const selectConsumer = async (consumerService) => {
|
|
720
|
+
const prepared = ensureConsumers(await fetchConsumers(consumerService));
|
|
721
|
+
if (!prepared) return null;
|
|
671
722
|
return await fuzzySelect({
|
|
672
723
|
message: "๐ Search keys",
|
|
673
|
-
choices: sorted.map((consumer) => ({
|
|
674
|
-
title: formatChoice(consumer, nameCounts),
|
|
724
|
+
choices: prepared.sorted.map((consumer) => ({
|
|
725
|
+
title: formatChoice(consumer, prepared.nameCounts),
|
|
675
726
|
value: consumer,
|
|
676
727
|
keywords: [
|
|
677
728
|
normalizeName(consumer),
|
|
@@ -682,22 +733,14 @@ const selectConsumer = async (consumerService) => {
|
|
|
682
733
|
}) ?? null;
|
|
683
734
|
};
|
|
684
735
|
const selectConsumerList = async (consumerService, message) => {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
pageToken
|
|
688
|
-
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
689
|
-
if (consumers.length === 0) {
|
|
690
|
-
console.log("No available API keys. Create one at https://getrouter.dev/dashboard/keys");
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
694
|
-
const nameCounts = buildNameCounts(sorted);
|
|
736
|
+
const prepared = ensureConsumers(await fetchConsumers(consumerService));
|
|
737
|
+
if (!prepared) return null;
|
|
695
738
|
const response = await prompts({
|
|
696
739
|
type: "select",
|
|
697
740
|
name: "value",
|
|
698
741
|
message,
|
|
699
|
-
choices: sorted.map((consumer) => ({
|
|
700
|
-
title: formatChoice(consumer, nameCounts),
|
|
742
|
+
choices: prepared.sorted.map((consumer) => ({
|
|
743
|
+
title: formatChoice(consumer, prepared.nameCounts),
|
|
701
744
|
value: consumer
|
|
702
745
|
}))
|
|
703
746
|
});
|
|
@@ -716,24 +759,30 @@ const confirmDelete = async (consumer) => {
|
|
|
716
759
|
|
|
717
760
|
//#endregion
|
|
718
761
|
//#region src/core/setup/env.ts
|
|
719
|
-
|
|
762
|
+
function quoteEnvValue(shell, value) {
|
|
720
763
|
if (shell === "ps1") return `'${value.replaceAll("'", "''")}'`;
|
|
721
764
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
722
|
-
}
|
|
723
|
-
|
|
765
|
+
}
|
|
766
|
+
function renderLine(shell, key, value) {
|
|
724
767
|
if (shell === "ps1") return `$env:${key}=${quoteEnvValue(shell, value)}`;
|
|
725
768
|
return `export ${key}=${quoteEnvValue(shell, value)}`;
|
|
726
|
-
}
|
|
727
|
-
|
|
769
|
+
}
|
|
770
|
+
function renderEnv(shell, vars) {
|
|
771
|
+
const entries = [
|
|
772
|
+
["openaiBaseUrl", "OPENAI_BASE_URL"],
|
|
773
|
+
["openaiApiKey", "OPENAI_API_KEY"],
|
|
774
|
+
["anthropicBaseUrl", "ANTHROPIC_BASE_URL"],
|
|
775
|
+
["anthropicApiKey", "ANTHROPIC_API_KEY"]
|
|
776
|
+
];
|
|
728
777
|
const lines = [];
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
778
|
+
for (const [varKey, envKey] of entries) {
|
|
779
|
+
const value = vars[varKey];
|
|
780
|
+
if (value) lines.push(renderLine(shell, envKey, value));
|
|
781
|
+
}
|
|
733
782
|
lines.push("");
|
|
734
783
|
return lines.join("\n");
|
|
735
|
-
}
|
|
736
|
-
|
|
784
|
+
}
|
|
785
|
+
function renderHook(shell) {
|
|
737
786
|
if (shell === "pwsh") return [
|
|
738
787
|
"function getrouter {",
|
|
739
788
|
" $cmd = Get-Command getrouter -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1",
|
|
@@ -799,19 +848,21 @@ const renderHook = (shell) => {
|
|
|
799
848
|
"}",
|
|
800
849
|
""
|
|
801
850
|
].join("\n");
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
851
|
+
}
|
|
852
|
+
function getEnvFilePath(shell, configDir) {
|
|
853
|
+
return path.join(configDir, shell === "ps1" ? "env.ps1" : "env.sh");
|
|
854
|
+
}
|
|
855
|
+
function getHookFilePath(shell, configDir) {
|
|
805
856
|
if (shell === "pwsh") return path.join(configDir, "hook.ps1");
|
|
806
857
|
if (shell === "fish") return path.join(configDir, "hook.fish");
|
|
807
858
|
return path.join(configDir, "hook.sh");
|
|
808
|
-
}
|
|
809
|
-
|
|
859
|
+
}
|
|
860
|
+
function writeEnvFile(filePath, content) {
|
|
810
861
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
811
862
|
fs.writeFileSync(filePath, content, "utf8");
|
|
812
863
|
if (process.platform !== "win32") fs.chmodSync(filePath, 384);
|
|
813
|
-
}
|
|
814
|
-
|
|
864
|
+
}
|
|
865
|
+
function resolveShellRcPath(shell, homeDir) {
|
|
815
866
|
if (shell === "zsh") return path.join(homeDir, ".zshrc");
|
|
816
867
|
if (shell === "bash") return path.join(homeDir, ".bashrc");
|
|
817
868
|
if (shell === "fish") return path.join(homeDir, ".config/fish/config.fish");
|
|
@@ -820,9 +871,11 @@ const resolveShellRcPath = (shell, homeDir) => {
|
|
|
820
871
|
return path.join(homeDir, ".config/powershell/Microsoft.PowerShell_profile.ps1");
|
|
821
872
|
}
|
|
822
873
|
return null;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
874
|
+
}
|
|
875
|
+
function resolveEnvShell(shell) {
|
|
876
|
+
return shell === "pwsh" ? "ps1" : "sh";
|
|
877
|
+
}
|
|
878
|
+
function detectShell() {
|
|
826
879
|
const shellPath = process.env.SHELL;
|
|
827
880
|
if (shellPath) {
|
|
828
881
|
const name = shellPath.split("/").pop()?.toLowerCase();
|
|
@@ -830,39 +883,41 @@ const detectShell = () => {
|
|
|
830
883
|
}
|
|
831
884
|
if (process.platform === "win32") return "pwsh";
|
|
832
885
|
return "bash";
|
|
833
|
-
}
|
|
834
|
-
|
|
886
|
+
}
|
|
887
|
+
function applyEnvVars(vars) {
|
|
835
888
|
if (vars.openaiBaseUrl) process.env.OPENAI_BASE_URL = vars.openaiBaseUrl;
|
|
836
889
|
if (vars.openaiApiKey) process.env.OPENAI_API_KEY = vars.openaiApiKey;
|
|
837
890
|
if (vars.anthropicBaseUrl) process.env.ANTHROPIC_BASE_URL = vars.anthropicBaseUrl;
|
|
838
891
|
if (vars.anthropicApiKey) process.env.ANTHROPIC_API_KEY = vars.anthropicApiKey;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
892
|
+
}
|
|
893
|
+
function formatSourceLine(shell, envPath) {
|
|
894
|
+
return shell === "ps1" ? `. ${envPath}` : `source ${envPath}`;
|
|
895
|
+
}
|
|
896
|
+
function trySourceEnv(shell, envShell, envPath) {
|
|
842
897
|
try {
|
|
843
898
|
if (envShell === "ps1") {
|
|
844
899
|
execSync(`pwsh -NoProfile -Command ". '${envPath}'"`, { stdio: "ignore" });
|
|
845
900
|
return;
|
|
846
901
|
}
|
|
847
|
-
execSync(`${shell} -c "
|
|
902
|
+
execSync(`${shell} -c "source '${envPath}'"`, { stdio: "ignore" });
|
|
848
903
|
} catch {}
|
|
849
|
-
}
|
|
850
|
-
|
|
904
|
+
}
|
|
905
|
+
function appendRcIfMissing(rcPath, line$1) {
|
|
851
906
|
let content = "";
|
|
852
907
|
if (fs.existsSync(rcPath)) {
|
|
853
908
|
content = fs.readFileSync(rcPath, "utf8");
|
|
854
|
-
if (content.includes(line)) return false;
|
|
909
|
+
if (content.includes(line$1)) return false;
|
|
855
910
|
}
|
|
856
911
|
const prefix = content && !content.endsWith("\n") ? "\n" : "";
|
|
857
912
|
fs.mkdirSync(path.dirname(rcPath), { recursive: true });
|
|
858
|
-
fs.writeFileSync(rcPath, `${content}${prefix}${line}\n`, "utf8");
|
|
913
|
+
fs.writeFileSync(rcPath, `${content}${prefix}${line$1}\n`, "utf8");
|
|
859
914
|
return true;
|
|
860
|
-
}
|
|
915
|
+
}
|
|
861
916
|
|
|
862
917
|
//#endregion
|
|
863
918
|
//#region src/cmd/env.ts
|
|
864
919
|
const CLAUDE_BASE_URL = "https://api.getrouter.dev/claude";
|
|
865
|
-
|
|
920
|
+
function registerEnvCommand(program, config) {
|
|
866
921
|
program.command(config.name).description(config.description).option("--install", "Install into shell rc").action(async (options) => {
|
|
867
922
|
if (!process.stdin.isTTY) throw new Error("Interactive mode required for key selection.");
|
|
868
923
|
const shell = detectShell();
|
|
@@ -901,44 +956,46 @@ const registerEnvCommand = (program, config) => {
|
|
|
901
956
|
console.log(sourceLine);
|
|
902
957
|
}
|
|
903
958
|
});
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
959
|
+
}
|
|
960
|
+
function buildAnthropicEnv(apiKey) {
|
|
961
|
+
return {
|
|
962
|
+
anthropicBaseUrl: CLAUDE_BASE_URL,
|
|
963
|
+
anthropicApiKey: apiKey
|
|
964
|
+
};
|
|
965
|
+
}
|
|
909
966
|
|
|
910
967
|
//#endregion
|
|
911
968
|
//#region src/cmd/claude.ts
|
|
912
|
-
|
|
969
|
+
function registerClaudeCommand(program) {
|
|
913
970
|
registerEnvCommand(program, {
|
|
914
971
|
name: "claude",
|
|
915
972
|
description: "Configure Claude environment",
|
|
916
973
|
vars: buildAnthropicEnv
|
|
917
974
|
});
|
|
918
|
-
}
|
|
975
|
+
}
|
|
919
976
|
|
|
920
977
|
//#endregion
|
|
921
978
|
//#region src/core/api/providerModels.ts
|
|
922
|
-
|
|
979
|
+
function asTrimmedString(value) {
|
|
923
980
|
if (typeof value !== "string") return null;
|
|
924
981
|
const trimmed = value.trim();
|
|
925
982
|
return trimmed.length > 0 ? trimmed : null;
|
|
926
|
-
}
|
|
927
|
-
|
|
983
|
+
}
|
|
984
|
+
function buildProviderModelsPath(tag) {
|
|
928
985
|
const query = new URLSearchParams();
|
|
929
986
|
if (tag) query.set("tag", tag);
|
|
930
987
|
const qs = query.toString();
|
|
931
988
|
return `v1/dashboard/providers/models${qs ? `?${qs}` : ""}`;
|
|
932
|
-
}
|
|
933
|
-
|
|
989
|
+
}
|
|
990
|
+
async function listProviderModels(options) {
|
|
934
991
|
const raw = (await requestJson({
|
|
935
|
-
path: buildProviderModelsPath(tag),
|
|
992
|
+
path: buildProviderModelsPath(options.tag),
|
|
936
993
|
method: "GET",
|
|
937
|
-
fetchImpl,
|
|
994
|
+
fetchImpl: options.fetchImpl,
|
|
938
995
|
maxRetries: 0
|
|
939
996
|
}))?.models;
|
|
940
997
|
return (Array.isArray(raw) ? raw : []).map(asTrimmedString).filter(Boolean);
|
|
941
|
-
}
|
|
998
|
+
}
|
|
942
999
|
|
|
943
1000
|
//#endregion
|
|
944
1001
|
//#region src/core/interactive/codex.ts
|
|
@@ -1038,20 +1095,68 @@ const PROVIDER_KEYS = [
|
|
|
1038
1095
|
"wire_api",
|
|
1039
1096
|
"requires_openai_auth"
|
|
1040
1097
|
];
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1098
|
+
function splitLines(content) {
|
|
1099
|
+
if (content.length === 0) return [];
|
|
1100
|
+
return content.split(/\r?\n/);
|
|
1101
|
+
}
|
|
1102
|
+
function isLegacyTomlRootMarker(key) {
|
|
1103
|
+
return LEGACY_TOML_ROOT_MARKERS.includes(key);
|
|
1104
|
+
}
|
|
1105
|
+
function rootValues(input) {
|
|
1106
|
+
return {
|
|
1107
|
+
model: `"${input.model}"`,
|
|
1108
|
+
model_reasoning_effort: `"${input.reasoning}"`,
|
|
1109
|
+
model_provider: `"${CODEX_PROVIDER}"`
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function providerValues() {
|
|
1113
|
+
return {
|
|
1114
|
+
name: `"${CODEX_PROVIDER}"`,
|
|
1115
|
+
base_url: `"${CODEX_BASE_URL}"`,
|
|
1116
|
+
wire_api: `"responses"`,
|
|
1117
|
+
requires_openai_auth: "true"
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
const HEADER_RE = /^\s*\[([^\]]+)\]\s*$/;
|
|
1121
|
+
const KEY_RE = /^\s*([A-Za-z0-9_.-]+)\s*=/;
|
|
1122
|
+
function matchHeader(line$1) {
|
|
1123
|
+
return line$1.match(HEADER_RE);
|
|
1124
|
+
}
|
|
1125
|
+
function matchKey(line$1) {
|
|
1126
|
+
return line$1.match(KEY_RE);
|
|
1127
|
+
}
|
|
1128
|
+
function readKeyFromLine(line$1) {
|
|
1129
|
+
return matchKey(line$1)?.[1];
|
|
1130
|
+
}
|
|
1131
|
+
function readStringValue(data, key) {
|
|
1132
|
+
const value = data[key];
|
|
1133
|
+
return typeof value === "string" ? value : void 0;
|
|
1134
|
+
}
|
|
1135
|
+
function findSectionEnd(lines, startIndex) {
|
|
1136
|
+
for (let i = startIndex; i < lines.length; i += 1) {
|
|
1137
|
+
const line$1 = lines[i];
|
|
1138
|
+
if (line$1 !== void 0 && matchHeader(line$1)) return i;
|
|
1139
|
+
}
|
|
1140
|
+
return lines.length;
|
|
1141
|
+
}
|
|
1142
|
+
function upsertKeyLines(lines, startIndex, endIndex, keys, valueMap) {
|
|
1143
|
+
const found = /* @__PURE__ */ new Set();
|
|
1144
|
+
for (let i = startIndex; i < endIndex; i += 1) {
|
|
1145
|
+
const line$1 = lines[i];
|
|
1146
|
+
if (line$1 === void 0) continue;
|
|
1147
|
+
const keyMatch = matchKey(line$1);
|
|
1148
|
+
if (!keyMatch) continue;
|
|
1149
|
+
const key = keyMatch[1];
|
|
1150
|
+
if (!keys.includes(key)) continue;
|
|
1151
|
+
lines[i] = `${key} = ${valueMap[key]}`;
|
|
1152
|
+
found.add(key);
|
|
1153
|
+
}
|
|
1154
|
+
return found;
|
|
1155
|
+
}
|
|
1156
|
+
function missingKeyLines(keys, found, valueMap) {
|
|
1157
|
+
return keys.filter((key) => !found.has(key)).map((key) => `${key} = ${valueMap[key]}`);
|
|
1158
|
+
}
|
|
1159
|
+
function parseTomlRhsValue(rhs) {
|
|
1055
1160
|
const trimmed = rhs.trim();
|
|
1056
1161
|
if (!trimmed) return "";
|
|
1057
1162
|
const first = trimmed[0];
|
|
@@ -1061,150 +1166,119 @@ const parseTomlRhsValue = (rhs) => {
|
|
|
1061
1166
|
}
|
|
1062
1167
|
const hashIndex = trimmed.indexOf("#");
|
|
1063
1168
|
return (hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex)).trim();
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
for (const line of lines) {
|
|
1067
|
-
if (matchHeader(line)) break;
|
|
1068
|
-
if (
|
|
1069
|
-
const parts = line.split("=");
|
|
1070
|
-
parts.shift();
|
|
1071
|
-
return parseTomlRhsValue(parts.join("="));
|
|
1072
|
-
}
|
|
1169
|
+
}
|
|
1170
|
+
function readRootValue(lines, key) {
|
|
1171
|
+
for (const line$1 of lines) {
|
|
1172
|
+
if (matchHeader(line$1)) break;
|
|
1173
|
+
if (readKeyFromLine(line$1) === key) return parseTomlRhsValue(line$1.slice(line$1.indexOf("=") + 1));
|
|
1073
1174
|
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const lines = content
|
|
1175
|
+
}
|
|
1176
|
+
function readCodexTomlRootValues(content) {
|
|
1177
|
+
const lines = splitLines(content);
|
|
1077
1178
|
return {
|
|
1078
1179
|
model: readRootValue(lines, "model"),
|
|
1079
1180
|
reasoning: readRootValue(lines, "model_reasoning_effort"),
|
|
1080
1181
|
provider: readRootValue(lines, "model_provider")
|
|
1081
1182
|
};
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1183
|
+
}
|
|
1184
|
+
function normalizeTomlString(value) {
|
|
1084
1185
|
if (!value) return "";
|
|
1085
1186
|
const trimmed = value.trim();
|
|
1086
1187
|
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1).trim().toLowerCase();
|
|
1087
1188
|
return trimmed.replace(/['"]/g, "").trim().toLowerCase();
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1189
|
+
}
|
|
1190
|
+
function stripLegacyRootMarkers(lines) {
|
|
1090
1191
|
const updated = [];
|
|
1091
1192
|
let inRoot = true;
|
|
1092
|
-
for (const line of lines) {
|
|
1093
|
-
if (matchHeader(line)) inRoot = false;
|
|
1193
|
+
for (const line$1 of lines) {
|
|
1194
|
+
if (matchHeader(line$1)) inRoot = false;
|
|
1094
1195
|
if (inRoot) {
|
|
1095
|
-
const key =
|
|
1096
|
-
if (key &&
|
|
1196
|
+
const key = readKeyFromLine(line$1);
|
|
1197
|
+
if (key !== void 0 && isLegacyTomlRootMarker(key)) continue;
|
|
1097
1198
|
}
|
|
1098
|
-
updated.push(line);
|
|
1199
|
+
updated.push(line$1);
|
|
1099
1200
|
}
|
|
1100
1201
|
return updated;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const updated =
|
|
1202
|
+
}
|
|
1203
|
+
function mergeCodexToml(content, input) {
|
|
1204
|
+
const updated = stripLegacyRootMarkers(splitLines(content));
|
|
1104
1205
|
const rootValueMap = rootValues(input);
|
|
1105
1206
|
const providerValueMap = providerValues();
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
for (let i = 0; i < updated.length; i += 1) {
|
|
1110
|
-
const headerMatch = matchHeader(updated[i] ?? "");
|
|
1111
|
-
if (headerMatch) {
|
|
1112
|
-
currentSection = headerMatch[1]?.trim() ?? null;
|
|
1113
|
-
if (firstHeaderIndex === null) firstHeaderIndex = i;
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
if (currentSection !== null) continue;
|
|
1117
|
-
const keyMatch = matchKey(updated[i] ?? "");
|
|
1118
|
-
if (!keyMatch) continue;
|
|
1119
|
-
const key = keyMatch[1];
|
|
1120
|
-
if (ROOT_KEYS.includes(key)) {
|
|
1121
|
-
updated[i] = `${key} = ${rootValueMap[key]}`;
|
|
1122
|
-
rootFound.add(key);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
const insertIndex = firstHeaderIndex ?? updated.length;
|
|
1126
|
-
const missingRoot = ROOT_KEYS.filter((key) => !rootFound.has(key)).map((key) => `${key} = ${rootValueMap[key]}`);
|
|
1207
|
+
const firstHeaderIndex = updated.findIndex((line$1) => matchHeader(line$1) !== null);
|
|
1208
|
+
const rootEnd = firstHeaderIndex === -1 ? updated.length : firstHeaderIndex;
|
|
1209
|
+
const missingRoot = missingKeyLines(ROOT_KEYS, upsertKeyLines(updated, 0, rootEnd, ROOT_KEYS, rootValueMap), rootValueMap);
|
|
1127
1210
|
if (missingRoot.length > 0) {
|
|
1211
|
+
const insertIndex = rootEnd;
|
|
1128
1212
|
const needsBlank = insertIndex < updated.length && updated[insertIndex]?.trim() !== "";
|
|
1129
1213
|
updated.splice(insertIndex, 0, ...missingRoot, ...needsBlank ? [""] : []);
|
|
1130
1214
|
}
|
|
1131
1215
|
const providerHeader = `[${PROVIDER_SECTION}]`;
|
|
1132
|
-
const providerHeaderIndex = updated.findIndex((line) => line.trim() === providerHeader);
|
|
1216
|
+
const providerHeaderIndex = updated.findIndex((line$1) => line$1.trim() === providerHeader);
|
|
1133
1217
|
if (providerHeaderIndex === -1) {
|
|
1134
1218
|
if (updated.length > 0 && updated[updated.length - 1]?.trim() !== "") updated.push("");
|
|
1135
|
-
updated.push(providerHeader);
|
|
1136
|
-
for (const key of PROVIDER_KEYS) updated.push(`${key} = ${providerValueMap[key]}`);
|
|
1219
|
+
updated.push(providerHeader, ...PROVIDER_KEYS.map((key) => `${key} = ${providerValueMap[key]}`));
|
|
1137
1220
|
return updated.join("\n");
|
|
1138
1221
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
break;
|
|
1143
|
-
}
|
|
1144
|
-
const providerFound = /* @__PURE__ */ new Set();
|
|
1145
|
-
for (let i = providerHeaderIndex + 1; i < providerEnd; i += 1) {
|
|
1146
|
-
const keyMatch = matchKey(updated[i] ?? "");
|
|
1147
|
-
if (!keyMatch) continue;
|
|
1148
|
-
const key = keyMatch[1];
|
|
1149
|
-
if (PROVIDER_KEYS.includes(key)) {
|
|
1150
|
-
updated[i] = `${key} = ${providerValueMap[key]}`;
|
|
1151
|
-
providerFound.add(key);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
const missingProvider = PROVIDER_KEYS.filter((key) => !providerFound.has(key)).map((key) => `${key} = ${providerValueMap[key]}`);
|
|
1222
|
+
const providerStart = providerHeaderIndex + 1;
|
|
1223
|
+
const providerEnd = findSectionEnd(updated, providerStart);
|
|
1224
|
+
const missingProvider = missingKeyLines(PROVIDER_KEYS, upsertKeyLines(updated, providerStart, providerEnd, PROVIDER_KEYS, providerValueMap), providerValueMap);
|
|
1155
1225
|
if (missingProvider.length > 0) updated.splice(providerEnd, 0, ...missingProvider);
|
|
1156
1226
|
return updated.join("\n");
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1227
|
+
}
|
|
1228
|
+
function mergeAuthJson(data, apiKey) {
|
|
1159
1229
|
const next = { ...data };
|
|
1160
1230
|
for (const key of LEGACY_AUTH_MARKERS) if (key in next) delete next[key];
|
|
1161
1231
|
next.OPENAI_API_KEY = apiKey;
|
|
1162
1232
|
return next;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1233
|
+
}
|
|
1234
|
+
function stripGetrouterProviderSection(lines) {
|
|
1165
1235
|
const updated = [];
|
|
1166
1236
|
let skipSection = false;
|
|
1167
|
-
for (const line of lines) {
|
|
1168
|
-
const headerMatch = matchHeader(line);
|
|
1237
|
+
for (const line$1 of lines) {
|
|
1238
|
+
const headerMatch = matchHeader(line$1);
|
|
1169
1239
|
if (headerMatch) {
|
|
1170
|
-
if (
|
|
1240
|
+
if (headerMatch[1]?.trim() === PROVIDER_SECTION) {
|
|
1171
1241
|
skipSection = true;
|
|
1172
1242
|
continue;
|
|
1173
1243
|
}
|
|
1174
1244
|
skipSection = false;
|
|
1175
1245
|
}
|
|
1176
1246
|
if (skipSection) continue;
|
|
1177
|
-
updated.push(line);
|
|
1247
|
+
updated.push(line$1);
|
|
1178
1248
|
}
|
|
1179
1249
|
return updated;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1250
|
+
}
|
|
1251
|
+
function stripLegacyMarkersFromRoot(rootLines) {
|
|
1252
|
+
return rootLines.filter((line$1) => {
|
|
1253
|
+
const key = readKeyFromLine(line$1);
|
|
1254
|
+
return !(key !== void 0 && isLegacyTomlRootMarker(key));
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
function setOrDeleteRootKey(rootLines, key, value) {
|
|
1258
|
+
const idx = rootLines.findIndex((line$1) => readKeyFromLine(line$1) === key);
|
|
1187
1259
|
if (value === void 0) {
|
|
1188
1260
|
if (idx !== -1) rootLines.splice(idx, 1);
|
|
1189
1261
|
return;
|
|
1190
1262
|
}
|
|
1191
1263
|
if (idx !== -1) rootLines[idx] = `${key} = ${value}`;
|
|
1192
1264
|
else rootLines.push(`${key} = ${value}`);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1265
|
+
}
|
|
1266
|
+
function deleteRootKey(rootLines, key) {
|
|
1195
1267
|
setOrDeleteRootKey(rootLines, key, void 0);
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1268
|
+
}
|
|
1269
|
+
function hasLegacyRootMarkers(lines) {
|
|
1270
|
+
return lines.some((line$1) => {
|
|
1271
|
+
const key = readKeyFromLine(line$1);
|
|
1272
|
+
return key !== void 0 && isLegacyTomlRootMarker(key);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
function removeCodexConfig(content, options) {
|
|
1202
1276
|
const { restoreRoot, allowRootRemoval = true } = options ?? {};
|
|
1203
|
-
const lines = content
|
|
1277
|
+
const lines = splitLines(content);
|
|
1204
1278
|
const providerIsGetrouter = normalizeTomlString(readRootValue(lines, "model_provider")) === CODEX_PROVIDER;
|
|
1205
1279
|
const canRemoveRoot = allowRootRemoval || hasLegacyRootMarkers(lines);
|
|
1206
1280
|
const stripped = stripGetrouterProviderSection(lines);
|
|
1207
|
-
const firstHeaderIndex = stripped.findIndex((line) => matchHeader(line));
|
|
1281
|
+
const firstHeaderIndex = stripped.findIndex((line$1) => matchHeader(line$1));
|
|
1208
1282
|
const rootEnd = firstHeaderIndex === -1 ? stripped.length : firstHeaderIndex;
|
|
1209
1283
|
const rootLines = stripLegacyMarkersFromRoot(stripped.slice(0, rootEnd));
|
|
1210
1284
|
const restLines = stripped.slice(rootEnd);
|
|
@@ -1225,21 +1299,21 @@ const removeCodexConfig = (content, options) => {
|
|
|
1225
1299
|
content: nextContent,
|
|
1226
1300
|
changed: nextContent !== content
|
|
1227
1301
|
};
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1302
|
+
}
|
|
1303
|
+
function removeAuthJson(data, options) {
|
|
1230
1304
|
const { installed, restore } = options ?? {};
|
|
1231
1305
|
const next = { ...data };
|
|
1232
1306
|
let changed = false;
|
|
1233
|
-
const legacyInstalled =
|
|
1234
|
-
const legacyRestore =
|
|
1307
|
+
const legacyInstalled = readStringValue(next, "_getrouter_codex_installed_openai_api_key");
|
|
1308
|
+
const legacyRestore = readStringValue(next, "_getrouter_codex_backup_openai_api_key");
|
|
1235
1309
|
const effectiveInstalled = installed ?? legacyInstalled;
|
|
1236
1310
|
const effectiveRestore = restore ?? legacyRestore;
|
|
1237
1311
|
for (const key of LEGACY_AUTH_MARKERS) if (key in next) {
|
|
1238
1312
|
delete next[key];
|
|
1239
1313
|
changed = true;
|
|
1240
1314
|
}
|
|
1241
|
-
const current =
|
|
1242
|
-
const restoreValue =
|
|
1315
|
+
const current = readStringValue(next, "OPENAI_API_KEY");
|
|
1316
|
+
const restoreValue = effectiveRestore?.trim() ? effectiveRestore : void 0;
|
|
1243
1317
|
if (effectiveInstalled && current && current === effectiveInstalled) {
|
|
1244
1318
|
if (restoreValue) next.OPENAI_API_KEY = restoreValue;
|
|
1245
1319
|
else delete next.OPENAI_API_KEY;
|
|
@@ -1253,7 +1327,7 @@ const removeAuthJson = (data, options) => {
|
|
|
1253
1327
|
data: next,
|
|
1254
1328
|
changed
|
|
1255
1329
|
};
|
|
1256
|
-
}
|
|
1330
|
+
}
|
|
1257
1331
|
|
|
1258
1332
|
//#endregion
|
|
1259
1333
|
//#region src/cmd/codex.ts
|
|
@@ -1300,7 +1374,7 @@ const promptReasoning = async (model) => await fuzzySelect({
|
|
|
1300
1374
|
choices: REASONING_FUZZY_CHOICES
|
|
1301
1375
|
});
|
|
1302
1376
|
const formatReasoningLabel = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
|
|
1303
|
-
|
|
1377
|
+
function registerCodexCommand(program) {
|
|
1304
1378
|
const codex = program.command("codex").description("Configure Codex");
|
|
1305
1379
|
codex.option("-m, --model <model>", "Set codex model (skips model selection)").action(async (options) => {
|
|
1306
1380
|
requireInteractive$1();
|
|
@@ -1340,13 +1414,15 @@ const registerCodexCommand = (program) => {
|
|
|
1340
1414
|
backup.updatedAt = now;
|
|
1341
1415
|
backup.config ??= {};
|
|
1342
1416
|
backup.config.previous ??= {};
|
|
1343
|
-
|
|
1344
|
-
if (
|
|
1345
|
-
if (
|
|
1417
|
+
const previousConfig = backup.config.previous;
|
|
1418
|
+
if (previousConfig.model === void 0 && existingRoot.model && existingRoot.model !== installedRoot.model) previousConfig.model = existingRoot.model;
|
|
1419
|
+
if (previousConfig.reasoning === void 0 && existingRoot.reasoning && existingRoot.reasoning !== installedRoot.reasoning) previousConfig.reasoning = existingRoot.reasoning;
|
|
1420
|
+
if (previousConfig.provider === void 0 && existingRoot.provider && existingRoot.provider !== installedRoot.provider) previousConfig.provider = existingRoot.provider;
|
|
1346
1421
|
backup.config.installed = installedRoot;
|
|
1347
1422
|
backup.auth ??= {};
|
|
1348
|
-
|
|
1349
|
-
|
|
1423
|
+
const authBackup = backup.auth;
|
|
1424
|
+
if (authBackup.previousOpenaiKey === void 0 && existingOpenaiKey && existingOpenaiKey !== apiKey) authBackup.previousOpenaiKey = existingOpenaiKey;
|
|
1425
|
+
authBackup.installedOpenaiKey = apiKey;
|
|
1350
1426
|
writeCodexBackup(backup);
|
|
1351
1427
|
const mergedConfig = mergeCodexToml(existingConfig, {
|
|
1352
1428
|
model,
|
|
@@ -1376,7 +1452,9 @@ const registerCodexCommand = (program) => {
|
|
|
1376
1452
|
allowRootRemoval: hasBackup
|
|
1377
1453
|
}) : null;
|
|
1378
1454
|
const authContent = authExists ? fs.readFileSync(authPath, "utf8").trim() : "";
|
|
1379
|
-
|
|
1455
|
+
let authData = null;
|
|
1456
|
+
if (authExists) if (authContent) authData = JSON.parse(authContent);
|
|
1457
|
+
else authData = {};
|
|
1380
1458
|
const authResult = authData ? removeAuthJson(authData, {
|
|
1381
1459
|
installed: installedOpenaiKey,
|
|
1382
1460
|
restore: restoreOpenaiKey
|
|
@@ -1394,7 +1472,7 @@ const registerCodexCommand = (program) => {
|
|
|
1394
1472
|
const backupPath = resolveCodexBackupPath();
|
|
1395
1473
|
if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
|
|
1396
1474
|
});
|
|
1397
|
-
}
|
|
1475
|
+
}
|
|
1398
1476
|
|
|
1399
1477
|
//#endregion
|
|
1400
1478
|
//#region src/core/config/redact.ts
|
|
@@ -1403,28 +1481,28 @@ const SECRET_KEYS = new Set([
|
|
|
1403
1481
|
"refreshToken",
|
|
1404
1482
|
"apiKey"
|
|
1405
1483
|
]);
|
|
1406
|
-
|
|
1484
|
+
function mask(value) {
|
|
1407
1485
|
if (!value) return "";
|
|
1408
1486
|
if (value.length <= 8) return "****";
|
|
1409
1487
|
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1488
|
+
}
|
|
1489
|
+
function redactSecrets(obj) {
|
|
1412
1490
|
const out = { ...obj };
|
|
1413
1491
|
for (const key of Object.keys(out)) {
|
|
1414
1492
|
const value = out[key];
|
|
1415
1493
|
if (SECRET_KEYS.has(key) && typeof value === "string") out[key] = mask(value);
|
|
1416
1494
|
}
|
|
1417
1495
|
return out;
|
|
1418
|
-
}
|
|
1496
|
+
}
|
|
1419
1497
|
|
|
1420
1498
|
//#endregion
|
|
1421
1499
|
//#region src/core/output/table.ts
|
|
1422
|
-
|
|
1500
|
+
function truncate(value, max) {
|
|
1423
1501
|
if (value.length <= max) return value;
|
|
1424
1502
|
if (max <= 3) return value.slice(0, max);
|
|
1425
1503
|
return `${value.slice(0, max - 3)}...`;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1504
|
+
}
|
|
1505
|
+
function renderTable(headers, rows, options = {}) {
|
|
1428
1506
|
const maxColWidth = options.maxColWidth ?? 32;
|
|
1429
1507
|
const normalized = rows.map((row) => row.map((cell) => cell && cell.length > 0 ? cell : "-"));
|
|
1430
1508
|
const widths = headers.map((header, index) => {
|
|
@@ -1436,7 +1514,7 @@ const renderTable = (headers, rows, options = {}) => {
|
|
|
1436
1514
|
return truncate(cell ?? "-", widths[index]).padEnd(widths[index], " ");
|
|
1437
1515
|
}).join(" ");
|
|
1438
1516
|
return `${renderRow(headers)}\n${normalized.map((row) => renderRow(row)).join("\n")}`;
|
|
1439
|
-
}
|
|
1517
|
+
}
|
|
1440
1518
|
|
|
1441
1519
|
//#endregion
|
|
1442
1520
|
//#region src/cmd/keys.ts
|
|
@@ -1469,8 +1547,22 @@ const requireInteractive = (message) => {
|
|
|
1469
1547
|
};
|
|
1470
1548
|
const requireInteractiveForSelection = () => requireInteractive("Interactive mode required when key id is omitted.");
|
|
1471
1549
|
const requireInteractiveForAction = (action) => requireInteractive(`Interactive mode required for keys ${action}.`);
|
|
1550
|
+
const promptKeyDetails = async (initialName, initialEnabled) => {
|
|
1551
|
+
const nameResult = await promptKeyName(initialName);
|
|
1552
|
+
if (nameResult.cancelled) return { cancelled: true };
|
|
1553
|
+
const enabledResult = await promptKeyEnabled(initialEnabled);
|
|
1554
|
+
if (enabledResult.cancelled) return { cancelled: true };
|
|
1555
|
+
return {
|
|
1556
|
+
cancelled: false,
|
|
1557
|
+
name: nameResult.name,
|
|
1558
|
+
enabled: enabledResult.enabled
|
|
1559
|
+
};
|
|
1560
|
+
};
|
|
1472
1561
|
const updateConsumer = async (consumerService, consumer, name, enabled) => {
|
|
1473
|
-
const
|
|
1562
|
+
const updateMaskParts = [];
|
|
1563
|
+
if (name !== void 0 && name !== consumer.name) updateMaskParts.push("name");
|
|
1564
|
+
if (enabled !== void 0 && enabled !== consumer.enabled) updateMaskParts.push("enabled");
|
|
1565
|
+
const updateMask = updateMaskParts.join(",");
|
|
1474
1566
|
if (!updateMask) return consumer;
|
|
1475
1567
|
return consumerService.UpdateConsumer({
|
|
1476
1568
|
consumer: {
|
|
@@ -1487,46 +1579,37 @@ const listConsumers = async (consumerService, showApiKey) => {
|
|
|
1487
1579
|
pageToken
|
|
1488
1580
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0)), showApiKey);
|
|
1489
1581
|
};
|
|
1490
|
-
const
|
|
1582
|
+
const resolveConsumer = async (consumerService, message, id) => {
|
|
1491
1583
|
if (id) return consumerService.GetConsumer({ id });
|
|
1492
1584
|
requireInteractiveForSelection();
|
|
1493
|
-
return await selectConsumerList(consumerService,
|
|
1494
|
-
};
|
|
1495
|
-
const resolveConsumerForDelete = async (consumerService, id) => {
|
|
1496
|
-
if (id) return consumerService.GetConsumer({ id });
|
|
1497
|
-
requireInteractiveForSelection();
|
|
1498
|
-
return await selectConsumerList(consumerService, "Select key to delete");
|
|
1585
|
+
return await selectConsumerList(consumerService, message);
|
|
1499
1586
|
};
|
|
1500
1587
|
const createConsumer = async (consumerService) => {
|
|
1501
1588
|
requireInteractiveForAction("create");
|
|
1502
|
-
const
|
|
1503
|
-
if (
|
|
1504
|
-
const enabledResult = await promptKeyEnabled(true);
|
|
1505
|
-
if (enabledResult.cancelled) return;
|
|
1589
|
+
const details = await promptKeyDetails(void 0, true);
|
|
1590
|
+
if (details.cancelled) return;
|
|
1506
1591
|
let consumer = await consumerService.CreateConsumer({});
|
|
1507
|
-
consumer = await updateConsumer(consumerService, consumer,
|
|
1592
|
+
consumer = await updateConsumer(consumerService, consumer, details.name, details.enabled);
|
|
1508
1593
|
outputConsumerTable(consumer, true);
|
|
1509
1594
|
console.log("Please store this API key securely.");
|
|
1510
1595
|
};
|
|
1511
1596
|
const updateConsumerById = async (consumerService, id) => {
|
|
1512
1597
|
requireInteractiveForAction("update");
|
|
1513
|
-
const selected = await
|
|
1598
|
+
const selected = await resolveConsumer(consumerService, "Select key to update", id);
|
|
1514
1599
|
if (!selected?.id) return;
|
|
1515
|
-
const
|
|
1516
|
-
if (
|
|
1517
|
-
|
|
1518
|
-
if (enabledResult.cancelled) return;
|
|
1519
|
-
outputConsumerTable(await updateConsumer(consumerService, selected, nameResult.name, enabledResult.enabled), false);
|
|
1600
|
+
const details = await promptKeyDetails(selected.name, selected.enabled ?? true);
|
|
1601
|
+
if (details.cancelled) return;
|
|
1602
|
+
outputConsumerTable(await updateConsumer(consumerService, selected, details.name, details.enabled), false);
|
|
1520
1603
|
};
|
|
1521
1604
|
const deleteConsumerById = async (consumerService, id) => {
|
|
1522
1605
|
requireInteractiveForAction("delete");
|
|
1523
|
-
const selected = await
|
|
1606
|
+
const selected = await resolveConsumer(consumerService, "Select key to delete", id);
|
|
1524
1607
|
if (!selected?.id) return;
|
|
1525
1608
|
if (!await confirmDelete(selected)) return;
|
|
1526
1609
|
await consumerService.DeleteConsumer({ id: selected.id });
|
|
1527
1610
|
outputConsumerTable(selected, false);
|
|
1528
1611
|
};
|
|
1529
|
-
|
|
1612
|
+
function registerKeysCommands(program) {
|
|
1530
1613
|
const keys = program.command("keys").description("Manage API keys");
|
|
1531
1614
|
keys.option("--show", "Show full API keys");
|
|
1532
1615
|
keys.allowExcessArguments(false);
|
|
@@ -1551,7 +1634,7 @@ const registerKeysCommands = (program) => {
|
|
|
1551
1634
|
const { consumerService } = createApiClients({});
|
|
1552
1635
|
await deleteConsumerById(consumerService, id);
|
|
1553
1636
|
});
|
|
1554
|
-
}
|
|
1637
|
+
}
|
|
1555
1638
|
|
|
1556
1639
|
//#endregion
|
|
1557
1640
|
//#region src/cmd/models.ts
|
|
@@ -1562,18 +1645,20 @@ const modelHeaders = [
|
|
|
1562
1645
|
"ENABLED",
|
|
1563
1646
|
"UPDATED_AT"
|
|
1564
1647
|
];
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1648
|
+
function formatModelRow(model) {
|
|
1649
|
+
return [
|
|
1650
|
+
String(model.id ?? ""),
|
|
1651
|
+
String(model.name ?? ""),
|
|
1652
|
+
String(model.author ?? ""),
|
|
1653
|
+
String(model.enabled ?? ""),
|
|
1654
|
+
String(model.updatedAt ?? "")
|
|
1655
|
+
];
|
|
1656
|
+
}
|
|
1657
|
+
function outputModels(models) {
|
|
1573
1658
|
console.log("๐ง Models");
|
|
1574
|
-
console.log(renderTable(modelHeaders, models.map(
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1659
|
+
console.log(renderTable(modelHeaders, models.map(formatModelRow)));
|
|
1660
|
+
}
|
|
1661
|
+
async function listModels() {
|
|
1577
1662
|
const { modelService } = createApiClients({});
|
|
1578
1663
|
const models = (await modelService.ListModels({
|
|
1579
1664
|
pageSize: void 0,
|
|
@@ -1585,66 +1670,75 @@ const listModels = async () => {
|
|
|
1585
1670
|
return;
|
|
1586
1671
|
}
|
|
1587
1672
|
outputModels(models);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
await listModels();
|
|
1593
|
-
});
|
|
1594
|
-
models.command("list").description("List models").action(async () => {
|
|
1595
|
-
await listModels();
|
|
1596
|
-
});
|
|
1597
|
-
};
|
|
1673
|
+
}
|
|
1674
|
+
function registerModelsCommands(program) {
|
|
1675
|
+
program.command("models").description("List models").action(listModels).command("list").description("List models").action(listModels);
|
|
1676
|
+
}
|
|
1598
1677
|
|
|
1599
1678
|
//#endregion
|
|
1600
1679
|
//#region src/cmd/status.ts
|
|
1601
1680
|
const LABEL_WIDTH = 10;
|
|
1602
|
-
|
|
1681
|
+
function line(label, value) {
|
|
1682
|
+
return [label, value];
|
|
1683
|
+
}
|
|
1684
|
+
function formatLine(label, value) {
|
|
1603
1685
|
if (value == null || value === "") return null;
|
|
1604
1686
|
return ` ${label.padEnd(LABEL_WIDTH, " ")}: ${value}`;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1687
|
+
}
|
|
1688
|
+
function renderSection(title, items) {
|
|
1689
|
+
const lines = [];
|
|
1690
|
+
for (const item of items) {
|
|
1691
|
+
if (!item) continue;
|
|
1692
|
+
const rendered = formatLine(item[0], item[1]);
|
|
1693
|
+
if (rendered) lines.push(rendered);
|
|
1694
|
+
}
|
|
1695
|
+
return [title, ...lines].join("\n");
|
|
1696
|
+
}
|
|
1697
|
+
function formatAuthStatus(status) {
|
|
1698
|
+
if (status === "logged_in") return "โ
Logged in";
|
|
1699
|
+
return "โ Logged out";
|
|
1700
|
+
}
|
|
1701
|
+
function formatToken(token) {
|
|
1702
|
+
if (!token) return;
|
|
1609
1703
|
const trimmed = token.trim();
|
|
1610
1704
|
if (trimmed.length <= 12) return trimmed;
|
|
1611
1705
|
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
if (!startAt && !endAt) return
|
|
1706
|
+
}
|
|
1707
|
+
function formatWindow(startAt, endAt) {
|
|
1708
|
+
if (!startAt && !endAt) return;
|
|
1615
1709
|
if (startAt && endAt) return `${startAt} โ ${endAt}`;
|
|
1616
1710
|
if (startAt) return `${startAt} โ`;
|
|
1617
1711
|
return `โ ${endAt}`;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1712
|
+
}
|
|
1713
|
+
function formatLimits(requestPerMinute, tokenPerMinute) {
|
|
1620
1714
|
const parts = [];
|
|
1621
1715
|
if (typeof requestPerMinute === "number") parts.push(`${requestPerMinute} req/min`);
|
|
1622
1716
|
if (tokenPerMinute) parts.push(`${tokenPerMinute} tok/min`);
|
|
1623
|
-
if (parts.length === 0) return
|
|
1717
|
+
if (parts.length === 0) return;
|
|
1624
1718
|
return parts.join(" ยท ");
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1719
|
+
}
|
|
1720
|
+
function renderAuthSection() {
|
|
1627
1721
|
const status = getAuthStatus();
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1722
|
+
const isLoggedIn = status.status === "logged_in";
|
|
1723
|
+
return renderSection("๐ Auth", [
|
|
1724
|
+
line("Status", formatAuthStatus(status.status)),
|
|
1725
|
+
isLoggedIn && line("Expires", status.expiresAt),
|
|
1726
|
+
isLoggedIn && line("TokenType", status.tokenType),
|
|
1727
|
+
isLoggedIn && line("Access", formatToken(status.accessToken)),
|
|
1728
|
+
isLoggedIn && line("Refresh", formatToken(status.refreshToken))
|
|
1729
|
+
]);
|
|
1730
|
+
}
|
|
1731
|
+
function renderSubscriptionSection(subscription) {
|
|
1732
|
+
if (!subscription) return renderSection("๐ฆ Subscription", [line("Status", "No active subscription")]);
|
|
1638
1733
|
const limits = formatLimits(subscription.plan?.requestPerMinute, subscription.plan?.tokenPerMinute);
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
const registerStatusCommand = (program) => {
|
|
1734
|
+
return renderSection("๐ฆ Subscription", [
|
|
1735
|
+
line("Plan", subscription.plan?.name),
|
|
1736
|
+
line("Status", subscription.status),
|
|
1737
|
+
line("Window", formatWindow(subscription.startAt, subscription.endAt)),
|
|
1738
|
+
line("Limits", limits)
|
|
1739
|
+
]);
|
|
1740
|
+
}
|
|
1741
|
+
function registerStatusCommand(program) {
|
|
1648
1742
|
program.command("status").description("Show login and subscription status").action(async () => {
|
|
1649
1743
|
const { subscriptionService } = createApiClients({});
|
|
1650
1744
|
const subscription = await subscriptionService.CurrentSubscription({});
|
|
@@ -1652,62 +1746,49 @@ const registerStatusCommand = (program) => {
|
|
|
1652
1746
|
console.log("");
|
|
1653
1747
|
console.log(renderSubscriptionSection(subscription));
|
|
1654
1748
|
});
|
|
1655
|
-
}
|
|
1749
|
+
}
|
|
1656
1750
|
|
|
1657
1751
|
//#endregion
|
|
1658
1752
|
//#region src/core/output/usages.ts
|
|
1659
1753
|
const TOTAL_BLOCK = "โ";
|
|
1660
1754
|
const DEFAULT_WIDTH = 24;
|
|
1661
|
-
|
|
1755
|
+
function formatTokens(value) {
|
|
1662
1756
|
const abs = Math.abs(value);
|
|
1663
1757
|
if (abs < 1e3) return Math.round(value).toString();
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
},
|
|
1673
|
-
{
|
|
1674
|
-
threshold: 1e3,
|
|
1675
|
-
suffix: "K"
|
|
1676
|
-
}
|
|
1677
|
-
]) if (abs >= unit.threshold) {
|
|
1678
|
-
const scaled = value / unit.threshold;
|
|
1679
|
-
const decimals = Math.abs(scaled) < 10 ? 1 : 0;
|
|
1680
|
-
let output = scaled.toFixed(decimals);
|
|
1681
|
-
if (output.endsWith(".0")) output = output.slice(0, -2);
|
|
1682
|
-
return `${output}${unit.suffix}`;
|
|
1758
|
+
let threshold = 1e3;
|
|
1759
|
+
let suffix = "K";
|
|
1760
|
+
if (abs >= 1e9) {
|
|
1761
|
+
threshold = 1e9;
|
|
1762
|
+
suffix = "B";
|
|
1763
|
+
} else if (abs >= 1e6) {
|
|
1764
|
+
threshold = 1e6;
|
|
1765
|
+
suffix = "M";
|
|
1683
1766
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1767
|
+
const scaled = value / threshold;
|
|
1768
|
+
const decimals = Math.abs(scaled) < 10 ? 1 : 0;
|
|
1769
|
+
return `${scaled.toFixed(decimals).replace(/\.0$/, "")}${suffix}`;
|
|
1770
|
+
}
|
|
1771
|
+
function renderUsageChart(rows, width = DEFAULT_WIDTH) {
|
|
1687
1772
|
const header = "๐ Usage (last 7 days) ยท Tokens";
|
|
1688
1773
|
if (rows.length === 0) return `${header}\n\nNo usage data available.`;
|
|
1689
|
-
const
|
|
1690
|
-
const
|
|
1691
|
-
const safeTotal = Number.isFinite(total) ? total : 0;
|
|
1774
|
+
const data = rows.map((row) => {
|
|
1775
|
+
const rawTotal = Number(row.totalTokens);
|
|
1692
1776
|
return {
|
|
1693
1777
|
day: row.day,
|
|
1694
|
-
total:
|
|
1778
|
+
total: Number.isFinite(rawTotal) ? rawTotal : 0
|
|
1695
1779
|
};
|
|
1696
1780
|
});
|
|
1697
|
-
const
|
|
1698
|
-
const maxTotal = Math.max(0, ...totals);
|
|
1781
|
+
const maxTotal = Math.max(0, ...data.map((d) => d.total));
|
|
1699
1782
|
return [
|
|
1700
1783
|
header,
|
|
1701
1784
|
"",
|
|
1702
|
-
...
|
|
1703
|
-
if (maxTotal === 0 ||
|
|
1704
|
-
const scaled = Math.max(1, Math.round(
|
|
1705
|
-
|
|
1706
|
-
const totalLabel = formatTokens(row.total);
|
|
1707
|
-
return `${row.day} ${bar.padEnd(width, " ")} ${totalLabel}`;
|
|
1785
|
+
...data.map(({ day, total }) => {
|
|
1786
|
+
if (maxTotal === 0 || total === 0) return `${day} ${" ".repeat(width)} 0`;
|
|
1787
|
+
const scaled = Math.max(1, Math.round(total / maxTotal * width));
|
|
1788
|
+
return `${day} ${TOTAL_BLOCK.repeat(scaled).padEnd(width, " ")} ${formatTokens(total)}`;
|
|
1708
1789
|
})
|
|
1709
1790
|
].join("\n");
|
|
1710
|
-
}
|
|
1791
|
+
}
|
|
1711
1792
|
|
|
1712
1793
|
//#endregion
|
|
1713
1794
|
//#region src/core/usages/aggregate.ts
|
|
@@ -1722,7 +1803,7 @@ const toNumber = (value) => {
|
|
|
1722
1803
|
}
|
|
1723
1804
|
return 0;
|
|
1724
1805
|
};
|
|
1725
|
-
|
|
1806
|
+
function aggregateUsages(usages, maxDays = 7) {
|
|
1726
1807
|
const totals = /* @__PURE__ */ new Map();
|
|
1727
1808
|
for (const usage of usages) {
|
|
1728
1809
|
if (!usage.createdAt) continue;
|
|
@@ -1747,27 +1828,27 @@ const aggregateUsages = (usages, maxDays = 7) => {
|
|
|
1747
1828
|
totals.set(day, current);
|
|
1748
1829
|
}
|
|
1749
1830
|
return Array.from(totals.values()).sort((a, b) => b.day.localeCompare(a.day)).slice(0, maxDays);
|
|
1750
|
-
}
|
|
1831
|
+
}
|
|
1751
1832
|
|
|
1752
1833
|
//#endregion
|
|
1753
1834
|
//#region src/cmd/usages.ts
|
|
1754
|
-
|
|
1835
|
+
async function collectUsages() {
|
|
1755
1836
|
const { usageService } = createApiClients({});
|
|
1756
1837
|
return aggregateUsages((await usageService.ListUsage({
|
|
1757
1838
|
pageSize: 7,
|
|
1758
1839
|
pageToken: void 0
|
|
1759
1840
|
}))?.usages ?? [], 7);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1841
|
+
}
|
|
1842
|
+
function registerUsagesCommand(program) {
|
|
1762
1843
|
program.command("usages").description("Show recent usage").action(async () => {
|
|
1763
1844
|
const aggregated = await collectUsages();
|
|
1764
1845
|
console.log(renderUsageChart(aggregated));
|
|
1765
1846
|
});
|
|
1766
|
-
}
|
|
1847
|
+
}
|
|
1767
1848
|
|
|
1768
1849
|
//#endregion
|
|
1769
1850
|
//#region src/cmd/index.ts
|
|
1770
|
-
|
|
1851
|
+
function registerCommands(program) {
|
|
1771
1852
|
registerAuthCommands(program);
|
|
1772
1853
|
registerCodexCommand(program);
|
|
1773
1854
|
registerClaudeCommand(program);
|
|
@@ -1775,16 +1856,16 @@ const registerCommands = (program) => {
|
|
|
1775
1856
|
registerModelsCommands(program);
|
|
1776
1857
|
registerStatusCommand(program);
|
|
1777
1858
|
registerUsagesCommand(program);
|
|
1778
|
-
}
|
|
1859
|
+
}
|
|
1779
1860
|
|
|
1780
1861
|
//#endregion
|
|
1781
1862
|
//#region src/cli.ts
|
|
1782
|
-
|
|
1863
|
+
function createProgram() {
|
|
1783
1864
|
const program = new Command();
|
|
1784
1865
|
program.name("getrouter").description("CLI for getrouter.dev").version(version);
|
|
1785
1866
|
registerCommands(program);
|
|
1786
1867
|
return program;
|
|
1787
|
-
}
|
|
1868
|
+
}
|
|
1788
1869
|
|
|
1789
1870
|
//#endregion
|
|
1790
1871
|
//#region src/bin.ts
|