@getrouter/getrouter-cli 0.1.1 → 0.1.3
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/.serena/project.yml +84 -0
- package/CLAUDE.md +52 -0
- package/biome.json +1 -1
- package/bun.lock +10 -10
- package/dist/bin.mjs +245 -94
- package/package.json +2 -2
- package/src/cli.ts +2 -1
- package/src/cmd/codex.ts +17 -7
- package/src/cmd/env.ts +1 -1
- package/src/cmd/keys.ts +46 -28
- package/src/cmd/models.ts +2 -1
- package/src/core/api/pagination.ts +25 -0
- package/src/core/api/providerModels.ts +32 -0
- package/src/core/auth/refresh.ts +68 -0
- package/src/core/config/fs.ts +33 -2
- package/src/core/config/index.ts +2 -8
- package/src/core/config/paths.ts +6 -3
- package/src/core/http/request.ts +71 -15
- package/src/core/http/retry.ts +68 -0
- package/src/core/interactive/codex.ts +21 -0
- package/src/core/interactive/keys.ts +19 -10
- package/src/core/output/usages.ts +11 -30
- package/src/core/setup/codex.ts +4 -0
- package/src/core/setup/env.ts +14 -6
- package/tests/auth/refresh.test.ts +149 -0
- package/tests/cmd/codex.test.ts +87 -1
- package/tests/cmd/keys.test.ts +48 -14
- package/tests/cmd/models.test.ts +5 -2
- package/tests/cmd/usages.test.ts +5 -5
- package/tests/config/fs.test.ts +22 -1
- package/tests/config/index.test.ts +16 -1
- package/tests/config/paths.test.ts +23 -0
- package/tests/core/api/pagination.test.ts +87 -0
- package/tests/core/interactive/codex.test.ts +25 -1
- package/tests/core/setup/env.test.ts +18 -4
- package/tests/http/request.test.ts +157 -0
- package/tests/http/retry.test.ts +152 -0
- package/tests/output/usages.test.ts +11 -12
- package/tsconfig.json +3 -2
- package/src/core/paths.ts +0 -4
- package/tests/paths.test.ts +0 -9
package/dist/bin.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { randomInt } from "node:crypto";
|
|
8
8
|
import prompts from "prompts";
|
|
9
9
|
|
|
10
|
+
//#region package.json
|
|
11
|
+
var version = "0.1.2";
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
10
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
11
15
|
function createSubscriptionServiceClient(handler) {
|
|
12
16
|
return { CurrentSubscription(request) {
|
|
@@ -200,16 +204,47 @@ function createUsageServiceClient(handler) {
|
|
|
200
204
|
|
|
201
205
|
//#endregion
|
|
202
206
|
//#region src/core/config/fs.ts
|
|
207
|
+
const getCorruptBackupPath = (filePath) => {
|
|
208
|
+
const dir = path.dirname(filePath);
|
|
209
|
+
const ext = path.extname(filePath);
|
|
210
|
+
const base = path.basename(filePath, ext);
|
|
211
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
212
|
+
const rand = Math.random().toString(16).slice(2, 8);
|
|
213
|
+
return path.join(dir, `${base}.corrupt-${stamp}-${rand}${ext}`);
|
|
214
|
+
};
|
|
203
215
|
const readJsonFile = (filePath) => {
|
|
204
216
|
if (!fs.existsSync(filePath)) return null;
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
let raw;
|
|
218
|
+
try {
|
|
219
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
220
|
+
} catch {
|
|
221
|
+
console.warn(`⚠️ Unable to read ${filePath}. Continuing with defaults.`);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(raw);
|
|
226
|
+
} catch {
|
|
227
|
+
const backupPath = getCorruptBackupPath(filePath);
|
|
228
|
+
try {
|
|
229
|
+
fs.renameSync(filePath, backupPath);
|
|
230
|
+
console.warn(`⚠️ Invalid JSON in ${filePath}. Moved to ${backupPath} and continuing with defaults.`);
|
|
231
|
+
} catch {
|
|
232
|
+
console.warn(`⚠️ Invalid JSON in ${filePath}. Please fix or delete this file, then try again.`);
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
207
236
|
};
|
|
208
237
|
const writeJsonFile = (filePath, value) => {
|
|
209
238
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
210
239
|
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
|
|
211
240
|
};
|
|
212
241
|
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/core/config/paths.ts
|
|
244
|
+
const resolveConfigDir = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
|
|
245
|
+
const getConfigPath = () => path.join(resolveConfigDir(), "config.json");
|
|
246
|
+
const getAuthPath = () => path.join(resolveConfigDir(), "auth.json");
|
|
247
|
+
|
|
213
248
|
//#endregion
|
|
214
249
|
//#region src/core/config/types.ts
|
|
215
250
|
const defaultConfig = () => ({
|
|
@@ -225,9 +260,6 @@ const defaultAuthState = () => ({
|
|
|
225
260
|
|
|
226
261
|
//#endregion
|
|
227
262
|
//#region src/core/config/index.ts
|
|
228
|
-
const resolveConfigDir$1 = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
|
|
229
|
-
const getConfigPath = () => path.join(resolveConfigDir$1(), "config.json");
|
|
230
|
-
const getAuthPath = () => path.join(resolveConfigDir$1(), "auth.json");
|
|
231
263
|
const readConfig = () => ({
|
|
232
264
|
...defaultConfig(),
|
|
233
265
|
...readJsonFile(getConfigPath()) ?? {}
|
|
@@ -242,6 +274,39 @@ const writeAuth = (auth) => {
|
|
|
242
274
|
if (process.platform !== "win32") fs.chmodSync(authPath, 384);
|
|
243
275
|
};
|
|
244
276
|
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/core/http/url.ts
|
|
279
|
+
const getApiBase = () => {
|
|
280
|
+
return (readConfig().apiBase || "").replace(/\/+$/, "");
|
|
281
|
+
};
|
|
282
|
+
const buildApiUrl = (path$1) => {
|
|
283
|
+
const base = getApiBase();
|
|
284
|
+
const normalized = path$1.replace(/^\/+/, "");
|
|
285
|
+
return base ? `${base}/${normalized}` : `/${normalized}`;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/core/auth/refresh.ts
|
|
290
|
+
const EXPIRY_BUFFER_MS = 60 * 1e3;
|
|
291
|
+
const refreshAccessToken = async ({ fetchImpl }) => {
|
|
292
|
+
const auth = readAuth();
|
|
293
|
+
if (!auth.refreshToken) return null;
|
|
294
|
+
const res = await (fetchImpl ?? fetch)(buildApiUrl("v1/dashboard/auth/token"), {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: { "Content-Type": "application/json" },
|
|
297
|
+
body: JSON.stringify({ refreshToken: auth.refreshToken })
|
|
298
|
+
});
|
|
299
|
+
if (!res.ok) return null;
|
|
300
|
+
const token = await res.json();
|
|
301
|
+
if (token.accessToken && token.refreshToken) writeAuth({
|
|
302
|
+
accessToken: token.accessToken,
|
|
303
|
+
refreshToken: token.refreshToken,
|
|
304
|
+
expiresAt: token.expiresAt ?? "",
|
|
305
|
+
tokenType: "Bearer"
|
|
306
|
+
});
|
|
307
|
+
return token;
|
|
308
|
+
};
|
|
309
|
+
|
|
245
310
|
//#endregion
|
|
246
311
|
//#region src/core/http/errors.ts
|
|
247
312
|
const createApiError = (payload, fallbackMessage, status) => {
|
|
@@ -255,33 +320,71 @@ const createApiError = (payload, fallbackMessage, status) => {
|
|
|
255
320
|
};
|
|
256
321
|
|
|
257
322
|
//#endregion
|
|
258
|
-
//#region src/core/http/
|
|
259
|
-
const
|
|
260
|
-
|
|
323
|
+
//#region src/core/http/retry.ts
|
|
324
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
325
|
+
const isRetryableError = (error) => {
|
|
326
|
+
if (error instanceof TypeError) return true;
|
|
327
|
+
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") {
|
|
328
|
+
const status = error.status;
|
|
329
|
+
return status >= 500 || status === 408 || status === 429;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
261
332
|
};
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
333
|
+
const withRetry = async (fn, options = {}) => {
|
|
334
|
+
const { maxRetries = 3, initialDelayMs = 1e3, maxDelayMs = 1e4, shouldRetry = isRetryableError, onRetry, sleep = defaultSleep } = options;
|
|
335
|
+
let lastError;
|
|
336
|
+
let delay = initialDelayMs;
|
|
337
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
338
|
+
return await fn();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
lastError = error;
|
|
341
|
+
if (attempt >= maxRetries || !shouldRetry(error, attempt)) throw error;
|
|
342
|
+
onRetry?.(error, attempt + 1, delay);
|
|
343
|
+
await sleep(delay);
|
|
344
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
345
|
+
}
|
|
346
|
+
throw lastError;
|
|
266
347
|
};
|
|
348
|
+
const isServerError = (status) => status >= 500 || status === 408 || status === 429;
|
|
267
349
|
|
|
268
350
|
//#endregion
|
|
269
351
|
//#region src/core/http/request.ts
|
|
270
352
|
const getAuthCookieName = () => process.env.GETROUTER_AUTH_COOKIE || process.env.KRATOS_AUTH_COOKIE || "access_token";
|
|
271
|
-
const
|
|
353
|
+
const buildHeaders = (accessToken) => {
|
|
272
354
|
const headers = { "Content-Type": "application/json" };
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
headers.
|
|
276
|
-
headers.Cookie = `${getAuthCookieName()}=${auth.accessToken}`;
|
|
355
|
+
if (accessToken) {
|
|
356
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
357
|
+
headers.Cookie = `${getAuthCookieName()}=${accessToken}`;
|
|
277
358
|
}
|
|
278
|
-
|
|
359
|
+
return headers;
|
|
360
|
+
};
|
|
361
|
+
const doFetch = async (url, method, headers, body, fetchImpl) => {
|
|
362
|
+
return (fetchImpl ?? fetch)(url, {
|
|
279
363
|
method,
|
|
280
364
|
headers,
|
|
281
365
|
body: body == null ? void 0 : JSON.stringify(body)
|
|
282
366
|
});
|
|
283
|
-
|
|
284
|
-
|
|
367
|
+
};
|
|
368
|
+
const shouldRetryResponse = (error) => {
|
|
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, _retrySleep }) => {
|
|
373
|
+
return withRetry(async () => {
|
|
374
|
+
const auth = readAuth();
|
|
375
|
+
const url = buildApiUrl(path$1);
|
|
376
|
+
let res = await doFetch(url, method, buildHeaders(auth.accessToken), body, fetchImpl);
|
|
377
|
+
if (res.status === 401 && auth.refreshToken) {
|
|
378
|
+
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
379
|
+
if (refreshed?.accessToken) res = await doFetch(url, method, buildHeaders(refreshed.accessToken), body, fetchImpl);
|
|
380
|
+
}
|
|
381
|
+
if (!res.ok) throw createApiError(await res.json().catch(() => null), res.statusText, res.status);
|
|
382
|
+
return await res.json();
|
|
383
|
+
}, {
|
|
384
|
+
maxRetries,
|
|
385
|
+
shouldRetry: shouldRetryResponse,
|
|
386
|
+
sleep: _retrySleep
|
|
387
|
+
});
|
|
285
388
|
};
|
|
286
389
|
|
|
287
390
|
//#endregion
|
|
@@ -420,6 +523,28 @@ const registerAuthCommands = (program) => {
|
|
|
420
523
|
});
|
|
421
524
|
};
|
|
422
525
|
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/core/api/pagination.ts
|
|
528
|
+
/**
|
|
529
|
+
* Fetches all pages from a paginated API endpoint.
|
|
530
|
+
*
|
|
531
|
+
* @param fetchPage - Function that fetches a single page given a pageToken
|
|
532
|
+
* @param getItems - Function that extracts items from the response
|
|
533
|
+
* @param getNextToken - Function that extracts the next page token from the response
|
|
534
|
+
* @returns Array of all items across all pages
|
|
535
|
+
*/
|
|
536
|
+
const fetchAllPages = async (fetchPage, getItems, getNextToken) => {
|
|
537
|
+
const allItems = [];
|
|
538
|
+
let pageToken;
|
|
539
|
+
do {
|
|
540
|
+
const response = await fetchPage(pageToken);
|
|
541
|
+
const items = getItems(response);
|
|
542
|
+
allItems.push(...items);
|
|
543
|
+
pageToken = getNextToken(response);
|
|
544
|
+
} while (pageToken);
|
|
545
|
+
return allItems;
|
|
546
|
+
};
|
|
547
|
+
|
|
423
548
|
//#endregion
|
|
424
549
|
//#region src/core/interactive/fuzzy.ts
|
|
425
550
|
const normalize = (value) => value.toLowerCase();
|
|
@@ -504,10 +629,10 @@ const promptKeyEnabled = async (initial) => {
|
|
|
504
629
|
return typeof response.enabled === "boolean" ? response.enabled : initial;
|
|
505
630
|
};
|
|
506
631
|
const selectConsumer = async (consumerService) => {
|
|
507
|
-
const consumers =
|
|
632
|
+
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
508
633
|
pageSize: void 0,
|
|
509
|
-
pageToken
|
|
510
|
-
}))?.consumers ?? [];
|
|
634
|
+
pageToken
|
|
635
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
511
636
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
512
637
|
const sorted = sortByCreatedAtDesc(consumers);
|
|
513
638
|
const nameCounts = buildNameCounts(sorted);
|
|
@@ -521,10 +646,10 @@ const selectConsumer = async (consumerService) => {
|
|
|
521
646
|
}) ?? null;
|
|
522
647
|
};
|
|
523
648
|
const selectConsumerList = async (consumerService, message) => {
|
|
524
|
-
const consumers =
|
|
649
|
+
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
525
650
|
pageSize: void 0,
|
|
526
|
-
pageToken
|
|
527
|
-
}))?.consumers ?? [];
|
|
651
|
+
pageToken
|
|
652
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
528
653
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
529
654
|
const sorted = sortByCreatedAtDesc(consumers);
|
|
530
655
|
const nameCounts = buildNameCounts(sorted);
|
|
@@ -552,9 +677,13 @@ const confirmDelete = async (consumer) => {
|
|
|
552
677
|
|
|
553
678
|
//#endregion
|
|
554
679
|
//#region src/core/setup/env.ts
|
|
680
|
+
const quoteEnvValue = (shell, value) => {
|
|
681
|
+
if (shell === "ps1") return `'${value.replaceAll("'", "''")}'`;
|
|
682
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
683
|
+
};
|
|
555
684
|
const renderLine = (shell, key, value) => {
|
|
556
|
-
if (shell === "ps1") return `$env:${key}
|
|
557
|
-
return `export ${key}=${value}`;
|
|
685
|
+
if (shell === "ps1") return `$env:${key}=${quoteEnvValue(shell, value)}`;
|
|
686
|
+
return `export ${key}=${quoteEnvValue(shell, value)}`;
|
|
558
687
|
};
|
|
559
688
|
const renderEnv = (shell, vars) => {
|
|
560
689
|
const lines = [];
|
|
@@ -690,7 +819,6 @@ const appendRcIfMissing = (rcPath, line) => {
|
|
|
690
819
|
fs.writeFileSync(rcPath, `${content}${prefix}${line}\n`, "utf8");
|
|
691
820
|
return true;
|
|
692
821
|
};
|
|
693
|
-
const resolveConfigDir = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
|
|
694
822
|
|
|
695
823
|
//#endregion
|
|
696
824
|
//#region src/cmd/env.ts
|
|
@@ -750,6 +878,29 @@ const registerClaudeCommand = (program) => {
|
|
|
750
878
|
});
|
|
751
879
|
};
|
|
752
880
|
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/core/api/providerModels.ts
|
|
883
|
+
const asTrimmedString = (value) => {
|
|
884
|
+
if (typeof value !== "string") return null;
|
|
885
|
+
const trimmed = value.trim();
|
|
886
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
887
|
+
};
|
|
888
|
+
const buildProviderModelsPath = (tag) => {
|
|
889
|
+
const query = new URLSearchParams();
|
|
890
|
+
if (tag) query.set("tag", tag);
|
|
891
|
+
const qs = query.toString();
|
|
892
|
+
return `v1/dashboard/providers/models${qs ? `?${qs}` : ""}`;
|
|
893
|
+
};
|
|
894
|
+
const listProviderModels = async ({ tag, fetchImpl }) => {
|
|
895
|
+
const raw = (await requestJson({
|
|
896
|
+
path: buildProviderModelsPath(tag),
|
|
897
|
+
method: "GET",
|
|
898
|
+
fetchImpl,
|
|
899
|
+
maxRetries: 0
|
|
900
|
+
}))?.models;
|
|
901
|
+
return (Array.isArray(raw) ? raw : []).map(asTrimmedString).filter(Boolean);
|
|
902
|
+
};
|
|
903
|
+
|
|
753
904
|
//#endregion
|
|
754
905
|
//#region src/core/interactive/codex.ts
|
|
755
906
|
const MODEL_CHOICES = [
|
|
@@ -778,6 +929,20 @@ const MODEL_CHOICES = [
|
|
|
778
929
|
keywords: ["gpt-5.2"]
|
|
779
930
|
}
|
|
780
931
|
];
|
|
932
|
+
const getCodexModelChoices = async () => {
|
|
933
|
+
try {
|
|
934
|
+
const remoteChoices = (await listProviderModels({ tag: "codex" })).map((model) => ({
|
|
935
|
+
title: model,
|
|
936
|
+
value: model,
|
|
937
|
+
keywords: [model, "codex"]
|
|
938
|
+
}));
|
|
939
|
+
if (remoteChoices.length > 0) {
|
|
940
|
+
remoteChoices.sort((a, b) => a.title.localeCompare(b.title));
|
|
941
|
+
return remoteChoices;
|
|
942
|
+
}
|
|
943
|
+
} catch {}
|
|
944
|
+
return MODEL_CHOICES;
|
|
945
|
+
};
|
|
781
946
|
const REASONING_CHOICES = [
|
|
782
947
|
{
|
|
783
948
|
id: "low",
|
|
@@ -922,19 +1087,21 @@ const ensureCodexDir = () => {
|
|
|
922
1087
|
const requireInteractive$1 = () => {
|
|
923
1088
|
if (!process.stdin.isTTY) throw new Error("Interactive mode required for codex configuration.");
|
|
924
1089
|
};
|
|
925
|
-
const promptModel = async () =>
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1090
|
+
const promptModel = async () => {
|
|
1091
|
+
return await fuzzySelect({
|
|
1092
|
+
message: "Select Model and Effort\nAccess legacy models by running getrouter codex -m <model_name> or in your config.toml",
|
|
1093
|
+
choices: await getCodexModelChoices()
|
|
1094
|
+
});
|
|
1095
|
+
};
|
|
929
1096
|
const promptReasoning = async (model) => await fuzzySelect({
|
|
930
1097
|
message: `Select Reasoning Level for ${model}`,
|
|
931
1098
|
choices: REASONING_FUZZY_CHOICES
|
|
932
1099
|
});
|
|
933
1100
|
const formatReasoningLabel = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
|
|
934
1101
|
const registerCodexCommand = (program) => {
|
|
935
|
-
program.command("codex").description("Configure Codex").action(async () => {
|
|
1102
|
+
program.command("codex").description("Configure Codex").option("-m, --model <model>", "Set codex model (skips model selection)").action(async (options) => {
|
|
936
1103
|
requireInteractive$1();
|
|
937
|
-
const model = await promptModel();
|
|
1104
|
+
const model = options.model && options.model.trim().length > 0 ? options.model.trim() : await promptModel();
|
|
938
1105
|
if (!model) return;
|
|
939
1106
|
const reasoningId = await promptReasoning(model);
|
|
940
1107
|
if (!reasoningId) return;
|
|
@@ -1024,21 +1191,23 @@ const consumerHeaders = [
|
|
|
1024
1191
|
"CREATED_AT",
|
|
1025
1192
|
"API_KEY"
|
|
1026
1193
|
];
|
|
1027
|
-
const consumerRow = (consumer) =>
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1194
|
+
const consumerRow = (consumer, showApiKey) => {
|
|
1195
|
+
const { apiKey } = showApiKey ? consumer : redactSecrets(consumer);
|
|
1196
|
+
return [
|
|
1197
|
+
String(consumer.name ?? ""),
|
|
1198
|
+
String(consumer.enabled ?? ""),
|
|
1199
|
+
String(consumer.lastAccess ?? ""),
|
|
1200
|
+
String(consumer.createdAt ?? ""),
|
|
1201
|
+
String(apiKey ?? "")
|
|
1202
|
+
];
|
|
1036
1203
|
};
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1204
|
+
const outputConsumerTable = (consumer, showApiKey) => {
|
|
1205
|
+
console.log(renderTable(consumerHeaders, [consumerRow(consumer, showApiKey)], { maxColWidth: 64 }));
|
|
1206
|
+
};
|
|
1207
|
+
const outputConsumers = (consumers, showApiKey) => {
|
|
1208
|
+
const rows = consumers.map((consumer) => consumerRow(consumer, showApiKey));
|
|
1209
|
+
console.log(renderTable(consumerHeaders, rows, { maxColWidth: 64 }));
|
|
1040
1210
|
};
|
|
1041
|
-
const redactConsumer = (consumer) => redactSecrets(consumer);
|
|
1042
1211
|
const requireInteractive = (message) => {
|
|
1043
1212
|
if (!process.stdin.isTTY) throw new Error(message);
|
|
1044
1213
|
};
|
|
@@ -1056,11 +1225,11 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
|
|
|
1056
1225
|
updateMask
|
|
1057
1226
|
});
|
|
1058
1227
|
};
|
|
1059
|
-
const listConsumers = async (consumerService) => {
|
|
1060
|
-
outputConsumers(((
|
|
1228
|
+
const listConsumers = async (consumerService, showApiKey) => {
|
|
1229
|
+
outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1061
1230
|
pageSize: void 0,
|
|
1062
|
-
pageToken
|
|
1063
|
-
}))?.consumers ?? []
|
|
1231
|
+
pageToken
|
|
1232
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0), showApiKey);
|
|
1064
1233
|
};
|
|
1065
1234
|
const resolveConsumerForUpdate = async (consumerService, id) => {
|
|
1066
1235
|
if (id) return consumerService.GetConsumer({ id });
|
|
@@ -1078,14 +1247,14 @@ const createConsumer = async (consumerService) => {
|
|
|
1078
1247
|
const enabled = await promptKeyEnabled(true);
|
|
1079
1248
|
let consumer = await consumerService.CreateConsumer({});
|
|
1080
1249
|
consumer = await updateConsumer(consumerService, consumer, name, enabled);
|
|
1081
|
-
outputConsumerTable(consumer);
|
|
1250
|
+
outputConsumerTable(consumer, true);
|
|
1082
1251
|
console.log("Please store this API key securely.");
|
|
1083
1252
|
};
|
|
1084
1253
|
const updateConsumerById = async (consumerService, id) => {
|
|
1085
1254
|
requireInteractiveForAction("update");
|
|
1086
1255
|
const selected = await resolveConsumerForUpdate(consumerService, id);
|
|
1087
1256
|
if (!selected?.id) return;
|
|
1088
|
-
outputConsumerTable(
|
|
1257
|
+
outputConsumerTable(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true)), false);
|
|
1089
1258
|
};
|
|
1090
1259
|
const deleteConsumerById = async (consumerService, id) => {
|
|
1091
1260
|
requireInteractiveForAction("delete");
|
|
@@ -1093,18 +1262,20 @@ const deleteConsumerById = async (consumerService, id) => {
|
|
|
1093
1262
|
if (!selected?.id) return;
|
|
1094
1263
|
if (!await confirmDelete(selected)) return;
|
|
1095
1264
|
await consumerService.DeleteConsumer({ id: selected.id });
|
|
1096
|
-
outputConsumerTable(
|
|
1265
|
+
outputConsumerTable(selected, false);
|
|
1097
1266
|
};
|
|
1098
1267
|
const registerKeysCommands = (program) => {
|
|
1099
1268
|
const keys = program.command("keys").description("Manage API keys");
|
|
1269
|
+
keys.option("--show", "Show full API keys");
|
|
1100
1270
|
keys.allowExcessArguments(false);
|
|
1101
|
-
keys.action(async () => {
|
|
1271
|
+
keys.action(async (options) => {
|
|
1102
1272
|
const { consumerService } = createApiClients({});
|
|
1103
|
-
await listConsumers(consumerService);
|
|
1273
|
+
await listConsumers(consumerService, Boolean(options.show));
|
|
1104
1274
|
});
|
|
1105
|
-
keys.command("list").description("List API keys").action(async () => {
|
|
1275
|
+
keys.command("list").description("List API keys").option("--show", "Show full API keys").action(async (options, command) => {
|
|
1106
1276
|
const { consumerService } = createApiClients({});
|
|
1107
|
-
|
|
1277
|
+
const parentShow = Boolean(command.parent?.opts().show);
|
|
1278
|
+
await listConsumers(consumerService, Boolean(options.show) || parentShow);
|
|
1108
1279
|
});
|
|
1109
1280
|
keys.command("create").description("Create an API key").action(async () => {
|
|
1110
1281
|
const { consumerService } = createApiClients({});
|
|
@@ -1123,12 +1294,14 @@ const registerKeysCommands = (program) => {
|
|
|
1123
1294
|
//#endregion
|
|
1124
1295
|
//#region src/cmd/models.ts
|
|
1125
1296
|
const modelHeaders = [
|
|
1297
|
+
"ID",
|
|
1126
1298
|
"NAME",
|
|
1127
1299
|
"AUTHOR",
|
|
1128
1300
|
"ENABLED",
|
|
1129
1301
|
"UPDATED_AT"
|
|
1130
1302
|
];
|
|
1131
1303
|
const modelRow = (model) => [
|
|
1304
|
+
String(model.id ?? ""),
|
|
1132
1305
|
String(model.name ?? ""),
|
|
1133
1306
|
String(model.author ?? ""),
|
|
1134
1307
|
String(model.enabled ?? ""),
|
|
@@ -1221,8 +1394,7 @@ const registerStatusCommand = (program) => {
|
|
|
1221
1394
|
|
|
1222
1395
|
//#endregion
|
|
1223
1396
|
//#region src/core/output/usages.ts
|
|
1224
|
-
const
|
|
1225
|
-
const OUTPUT_BLOCK = "▒";
|
|
1397
|
+
const TOTAL_BLOCK = "█";
|
|
1226
1398
|
const DEFAULT_WIDTH = 24;
|
|
1227
1399
|
const formatTokens = (value) => {
|
|
1228
1400
|
const abs = Math.abs(value);
|
|
@@ -1253,46 +1425,25 @@ const renderUsageChart = (rows, width = DEFAULT_WIDTH) => {
|
|
|
1253
1425
|
const header = "📊 Usage (last 7 days) · Tokens";
|
|
1254
1426
|
if (rows.length === 0) return `${header}\n\nNo usage data available.`;
|
|
1255
1427
|
const normalized = rows.map((row) => {
|
|
1256
|
-
const
|
|
1257
|
-
const
|
|
1258
|
-
const safeInput = Number.isFinite(input) ? input : 0;
|
|
1259
|
-
const safeOutput = Number.isFinite(output) ? output : 0;
|
|
1428
|
+
const total = Number(row.totalTokens);
|
|
1429
|
+
const safeTotal = Number.isFinite(total) ? total : 0;
|
|
1260
1430
|
return {
|
|
1261
1431
|
day: row.day,
|
|
1262
|
-
|
|
1263
|
-
output: safeOutput,
|
|
1264
|
-
total: safeInput + safeOutput
|
|
1432
|
+
total: safeTotal
|
|
1265
1433
|
};
|
|
1266
1434
|
});
|
|
1267
1435
|
const totals = normalized.map((row) => row.total);
|
|
1268
1436
|
const maxTotal = Math.max(0, ...totals);
|
|
1269
|
-
const lines = normalized.map((row) => {
|
|
1270
|
-
const total = row.total;
|
|
1271
|
-
if (maxTotal === 0 || total === 0) return `${row.day} ${"".padEnd(width, " ")} I:0 O:0`;
|
|
1272
|
-
const scaled = Math.max(1, Math.round(total / maxTotal * width));
|
|
1273
|
-
let inputBars = Math.round(row.input / total * scaled);
|
|
1274
|
-
let outputBars = Math.max(0, scaled - inputBars);
|
|
1275
|
-
if (row.input > 0 && row.output > 0) {
|
|
1276
|
-
if (inputBars === 0) {
|
|
1277
|
-
inputBars = 1;
|
|
1278
|
-
outputBars = Math.max(0, scaled - 1);
|
|
1279
|
-
} else if (outputBars === 0) {
|
|
1280
|
-
outputBars = 1;
|
|
1281
|
-
inputBars = Math.max(0, scaled - 1);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
const bar = `${INPUT_BLOCK.repeat(inputBars)}${OUTPUT_BLOCK.repeat(outputBars)}`;
|
|
1285
|
-
const inputLabel = formatTokens(row.input);
|
|
1286
|
-
const outputLabel = formatTokens(row.output);
|
|
1287
|
-
return `${row.day} ${bar.padEnd(width, " ")} I:${inputLabel} O:${outputLabel}`;
|
|
1288
|
-
});
|
|
1289
|
-
const legend = `Legend: ${INPUT_BLOCK} input ${OUTPUT_BLOCK} output`;
|
|
1290
1437
|
return [
|
|
1291
1438
|
header,
|
|
1292
1439
|
"",
|
|
1293
|
-
...
|
|
1294
|
-
|
|
1295
|
-
|
|
1440
|
+
...normalized.map((row) => {
|
|
1441
|
+
if (maxTotal === 0 || row.total === 0) return `${row.day} ${"".padEnd(width, " ")} 0`;
|
|
1442
|
+
const scaled = Math.max(1, Math.round(row.total / maxTotal * width));
|
|
1443
|
+
const bar = TOTAL_BLOCK.repeat(scaled);
|
|
1444
|
+
const totalLabel = formatTokens(row.total);
|
|
1445
|
+
return `${row.day} ${bar.padEnd(width, " ")} ${totalLabel}`;
|
|
1446
|
+
})
|
|
1296
1447
|
].join("\n");
|
|
1297
1448
|
};
|
|
1298
1449
|
|
|
@@ -1368,7 +1519,7 @@ const registerCommands = (program) => {
|
|
|
1368
1519
|
//#region src/cli.ts
|
|
1369
1520
|
const createProgram = () => {
|
|
1370
1521
|
const program = new Command();
|
|
1371
|
-
program.name("getrouter").description("CLI for getrouter.dev").version(
|
|
1522
|
+
program.name("getrouter").description("CLI for getrouter.dev").version(version);
|
|
1372
1523
|
registerCommands(program);
|
|
1373
1524
|
return program;
|
|
1374
1525
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getrouter/getrouter-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for getrouter.dev",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"prompts": "^2.4.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@biomejs/biome": "^2.3.
|
|
29
|
+
"@biomejs/biome": "^2.3.11",
|
|
30
30
|
"@types/node": "^25.0.3",
|
|
31
31
|
"@types/prompts": "^2.4.9",
|
|
32
32
|
"tsdown": "^0.18.4",
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { version } from "../package.json";
|
|
2
3
|
import { registerCommands } from "./cmd";
|
|
3
4
|
|
|
4
5
|
export const createProgram = () => {
|
|
@@ -6,7 +7,7 @@ export const createProgram = () => {
|
|
|
6
7
|
program
|
|
7
8
|
.name("getrouter")
|
|
8
9
|
.description("CLI for getrouter.dev")
|
|
9
|
-
.version(
|
|
10
|
+
.version(version);
|
|
10
11
|
registerCommands(program);
|
|
11
12
|
return program;
|
|
12
13
|
};
|
package/src/cmd/codex.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Command } from "commander";
|
|
|
5
5
|
import prompts from "prompts";
|
|
6
6
|
import { createApiClients } from "../core/api/client";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
getCodexModelChoices,
|
|
9
9
|
mapReasoningValue,
|
|
10
10
|
REASONING_CHOICES,
|
|
11
11
|
REASONING_FUZZY_CHOICES,
|
|
@@ -42,12 +42,14 @@ const requireInteractive = () => {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const promptModel = async () =>
|
|
46
|
-
await
|
|
45
|
+
const promptModel = async () => {
|
|
46
|
+
const choices = await getCodexModelChoices();
|
|
47
|
+
return await fuzzySelect({
|
|
47
48
|
message:
|
|
48
|
-
"Select Model and Effort\nAccess legacy models by running codex -m <model_name> or in your config.toml",
|
|
49
|
-
choices
|
|
49
|
+
"Select Model and Effort\nAccess legacy models by running getrouter codex -m <model_name> or in your config.toml",
|
|
50
|
+
choices,
|
|
50
51
|
});
|
|
52
|
+
};
|
|
51
53
|
|
|
52
54
|
const promptReasoning = async (model: string) =>
|
|
53
55
|
await fuzzySelect({
|
|
@@ -58,13 +60,21 @@ const promptReasoning = async (model: string) =>
|
|
|
58
60
|
const formatReasoningLabel = (id: string) =>
|
|
59
61
|
REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
|
|
60
62
|
|
|
63
|
+
type CodexCommandOptions = {
|
|
64
|
+
model?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
61
67
|
export const registerCodexCommand = (program: Command) => {
|
|
62
68
|
program
|
|
63
69
|
.command("codex")
|
|
64
70
|
.description("Configure Codex")
|
|
65
|
-
.
|
|
71
|
+
.option("-m, --model <model>", "Set codex model (skips model selection)")
|
|
72
|
+
.action(async (options: CodexCommandOptions) => {
|
|
66
73
|
requireInteractive();
|
|
67
|
-
const model =
|
|
74
|
+
const model =
|
|
75
|
+
options.model && options.model.trim().length > 0
|
|
76
|
+
? options.model.trim()
|
|
77
|
+
: await promptModel();
|
|
68
78
|
if (!model) return;
|
|
69
79
|
const reasoningId = await promptReasoning(model);
|
|
70
80
|
if (!reasoningId) return;
|
package/src/cmd/env.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
import { createApiClients } from "../core/api/client";
|
|
4
|
+
import { resolveConfigDir } from "../core/config/paths";
|
|
4
5
|
import { selectConsumer } from "../core/interactive/keys";
|
|
5
6
|
import {
|
|
6
7
|
appendRcIfMissing,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
getHookFilePath,
|
|
13
14
|
renderEnv,
|
|
14
15
|
renderHook,
|
|
15
|
-
resolveConfigDir,
|
|
16
16
|
resolveEnvShell,
|
|
17
17
|
resolveShellRcPath,
|
|
18
18
|
trySourceEnv,
|