@getrouter/getrouter-cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ # list of languages for which language servers are started; choose from:
2
+ # al bash clojure cpp csharp csharp_omnisharp
3
+ # dart elixir elm erlang fortran go
4
+ # haskell java julia kotlin lua markdown
5
+ # nix perl php python python_jedi r
6
+ # rego ruby ruby_solargraph rust scala swift
7
+ # terraform typescript typescript_vts yaml zig
8
+ # Note:
9
+ # - For C, use cpp
10
+ # - For JavaScript, use typescript
11
+ # Special requirements:
12
+ # - csharp: Requires the presence of a .sln file in the project folder.
13
+ # When using multiple languages, the first language server that supports a given file will be used for that file.
14
+ # The first language is the default language and the respective language server will be used as a fallback.
15
+ # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
16
+ languages:
17
+ - typescript
18
+
19
+ # the encoding used by text files in the project
20
+ # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
21
+ encoding: "utf-8"
22
+
23
+ # whether to use the project's gitignore file to ignore files
24
+ # Added on 2025-04-07
25
+ ignore_all_files_in_gitignore: true
26
+
27
+ # list of additional paths to ignore
28
+ # same syntax as gitignore, so you can use * and **
29
+ # Was previously called `ignored_dirs`, please update your config if you are using that.
30
+ # Added (renamed) on 2025-04-07
31
+ ignored_paths: []
32
+
33
+ # whether the project is in read-only mode
34
+ # If set to true, all editing tools will be disabled and attempts to use them will result in an error
35
+ # Added on 2025-04-18
36
+ read_only: false
37
+
38
+ # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
39
+ # Below is the complete list of tools for convenience.
40
+ # To make sure you have the latest list of tools, and to view their descriptions,
41
+ # execute `uv run scripts/print_tool_overview.py`.
42
+ #
43
+ # * `activate_project`: Activates a project by name.
44
+ # * `check_onboarding_performed`: Checks whether project onboarding was already performed.
45
+ # * `create_text_file`: Creates/overwrites a file in the project directory.
46
+ # * `delete_lines`: Deletes a range of lines within a file.
47
+ # * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
48
+ # * `execute_shell_command`: Executes a shell command.
49
+ # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
50
+ # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
51
+ # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
52
+ # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
53
+ # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
54
+ # * `initial_instructions`: Gets the initial instructions for the current project.
55
+ # Should only be used in settings where the system prompt cannot be set,
56
+ # e.g. in clients you have no control over, like Claude Desktop.
57
+ # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
58
+ # * `insert_at_line`: Inserts content at a given line in a file.
59
+ # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
60
+ # * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
61
+ # * `list_memories`: Lists memories in Serena's project-specific memory store.
62
+ # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
63
+ # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
64
+ # * `read_file`: Reads a file within the project directory.
65
+ # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
66
+ # * `remove_project`: Removes a project from the Serena configuration.
67
+ # * `replace_lines`: Replaces a range of lines within a file with new content.
68
+ # * `replace_symbol_body`: Replaces the full definition of a symbol.
69
+ # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
70
+ # * `search_for_pattern`: Performs a search for a pattern in the project.
71
+ # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
72
+ # * `switch_modes`: Activates modes by providing a list of their names
73
+ # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
74
+ # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
75
+ # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
76
+ # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
77
+ excluded_tools: []
78
+
79
+ # initial prompt for the project. It will always be given to the LLM upon activating the project
80
+ # (contrary to the memories, which are loaded on demand).
81
+ initial_prompt: ""
82
+
83
+ project_name: "getrouter-cli"
84
+ included_optional_tools: []
package/CLAUDE.md ADDED
@@ -0,0 +1,52 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # Development
9
+ bun install # Install dependencies
10
+ bun run dev -- --help # Run local CLI with args
11
+ bun run build # Build with tsdown
12
+
13
+ # Quality
14
+ bun run test # Run all tests
15
+ bun run test -- tests/cmd/auth.test.ts # Run single test file
16
+ bun run lint # Check with Biome
17
+ bun run format # Format with Biome
18
+ bun run typecheck # TypeScript type checking
19
+ ```
20
+
21
+ ## Architecture
22
+
23
+ ### Entry Flow
24
+ `src/bin.ts` → `src/cli.ts` (creates Commander program) → `src/cmd/index.ts` (registers all commands)
25
+
26
+ ### Directory Structure
27
+
28
+ - `src/cmd/` - Command handlers (auth, keys, codex, claude, status, usages, models)
29
+ - `src/core/` - Core logic modules:
30
+ - `api/client.ts` - Creates typed API service clients from generated code
31
+ - `auth/` - Auth status checking and device flow polling
32
+ - `config/` - JSON config/auth file read/write (`~/.getrouter/`)
33
+ - `http/` - HTTP request layer with auth headers, URL building, error handling
34
+ - `interactive/` - TTY prompts for user input (keys selection, codex setup)
35
+ - `output/` - Table rendering and usage chart formatting
36
+ - `setup/` - Environment file writers (codex config, claude env vars)
37
+ - `src/generated/` - Protobuf-generated TypeScript HTTP clients (do not edit manually)
38
+
39
+ ### API Client Pattern
40
+ Commands use `createApiClients({})` to get typed service clients (authService, consumerService, subscriptionService, usageService, modelService). These wrap generated protobuf-ts-http clients with auth token injection via `requestJson()`.
41
+
42
+ ### Config Files
43
+ Default config directory: `~/.getrouter/` (override with `GETROUTER_CONFIG_DIR`)
44
+ - `config.json` - CLI settings including `apiBase`
45
+ - `auth.json` - OAuth tokens (accessToken, refreshToken, expiresAt)
46
+
47
+ ### Testing Patterns
48
+ Tests use vitest and mock `createApiClients` and service dependencies. Use `process.env.GETROUTER_CONFIG_DIR` with temp directories for isolation.
49
+
50
+ ### Key Environment Variables
51
+ - `GETROUTER_CONFIG_DIR` - Override config directory location
52
+ - `GETROUTER_AUTH_COOKIE` / `KRATOS_AUTH_COOKIE` - Custom auth cookie name
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
3
3
  "files": {
4
4
  "includes": ["**/*", "!dist", "!node_modules", "!src/generated"]
5
5
  },
package/bun.lock CHANGED
@@ -9,7 +9,7 @@
9
9
  "prompts": "^2.4.2",
10
10
  },
11
11
  "devDependencies": {
12
- "@biomejs/biome": "^2.3.10",
12
+ "@biomejs/biome": "^2.3.11",
13
13
  "@types/node": "^25.0.3",
14
14
  "@types/prompts": "^2.4.9",
15
15
  "tsdown": "^0.18.4",
@@ -30,23 +30,23 @@
30
30
 
31
31
  "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
32
32
 
33
- "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
33
+ "@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
34
34
 
35
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
35
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
36
36
 
37
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
37
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
38
38
 
39
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
39
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
40
40
 
41
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
41
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
42
42
 
43
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
43
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
44
44
 
45
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
45
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
46
46
 
47
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
47
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
48
48
 
49
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
49
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
50
50
 
51
51
  "@emnapi/core": ["@emnapi/core@1.8.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="],
52
52
 
package/dist/bin.mjs CHANGED
@@ -7,6 +7,10 @@ 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) {
@@ -242,6 +246,39 @@ const writeAuth = (auth) => {
242
246
  if (process.platform !== "win32") fs.chmodSync(authPath, 384);
243
247
  };
244
248
 
249
+ //#endregion
250
+ //#region src/core/http/url.ts
251
+ const getApiBase = () => {
252
+ return (readConfig().apiBase || "").replace(/\/+$/, "");
253
+ };
254
+ const buildApiUrl = (path$1) => {
255
+ const base = getApiBase();
256
+ const normalized = path$1.replace(/^\/+/, "");
257
+ return base ? `${base}/${normalized}` : `/${normalized}`;
258
+ };
259
+
260
+ //#endregion
261
+ //#region src/core/auth/refresh.ts
262
+ const EXPIRY_BUFFER_MS = 60 * 1e3;
263
+ const refreshAccessToken = async ({ fetchImpl }) => {
264
+ const auth = readAuth();
265
+ if (!auth.refreshToken) return null;
266
+ const res = await (fetchImpl ?? fetch)(buildApiUrl("v1/dashboard/auth/token"), {
267
+ method: "POST",
268
+ headers: { "Content-Type": "application/json" },
269
+ body: JSON.stringify({ refreshToken: auth.refreshToken })
270
+ });
271
+ if (!res.ok) return null;
272
+ const token = await res.json();
273
+ if (token.accessToken && token.refreshToken) writeAuth({
274
+ accessToken: token.accessToken,
275
+ refreshToken: token.refreshToken,
276
+ expiresAt: token.expiresAt ?? "",
277
+ tokenType: "Bearer"
278
+ });
279
+ return token;
280
+ };
281
+
245
282
  //#endregion
246
283
  //#region src/core/http/errors.ts
247
284
  const createApiError = (payload, fallbackMessage, status) => {
@@ -255,33 +292,71 @@ const createApiError = (payload, fallbackMessage, status) => {
255
292
  };
256
293
 
257
294
  //#endregion
258
- //#region src/core/http/url.ts
259
- const getApiBase = () => {
260
- return (readConfig().apiBase || "").replace(/\/+$/, "");
295
+ //#region src/core/http/retry.ts
296
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
297
+ const isRetryableError = (error) => {
298
+ if (error instanceof TypeError) return true;
299
+ if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") {
300
+ const status = error.status;
301
+ return status >= 500 || status === 408 || status === 429;
302
+ }
303
+ return false;
261
304
  };
262
- const buildApiUrl = (path$1) => {
263
- const base = getApiBase();
264
- const normalized = path$1.replace(/^\/+/, "");
265
- return base ? `${base}/${normalized}` : `/${normalized}`;
305
+ const withRetry = async (fn, options = {}) => {
306
+ const { maxRetries = 3, initialDelayMs = 1e3, maxDelayMs = 1e4, shouldRetry = isRetryableError, onRetry, sleep = defaultSleep } = options;
307
+ let lastError;
308
+ let delay = initialDelayMs;
309
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
310
+ return await fn();
311
+ } catch (error) {
312
+ lastError = error;
313
+ if (attempt >= maxRetries || !shouldRetry(error, attempt)) throw error;
314
+ onRetry?.(error, attempt + 1, delay);
315
+ await sleep(delay);
316
+ delay = Math.min(delay * 2, maxDelayMs);
317
+ }
318
+ throw lastError;
266
319
  };
320
+ const isServerError = (status) => status >= 500 || status === 408 || status === 429;
267
321
 
268
322
  //#endregion
269
323
  //#region src/core/http/request.ts
270
324
  const getAuthCookieName = () => process.env.GETROUTER_AUTH_COOKIE || process.env.KRATOS_AUTH_COOKIE || "access_token";
271
- const requestJson = async ({ path: path$1, method, body, fetchImpl }) => {
325
+ const buildHeaders = (accessToken) => {
272
326
  const headers = { "Content-Type": "application/json" };
273
- const auth = readAuth();
274
- if (auth.accessToken) {
275
- headers.Authorization = `Bearer ${auth.accessToken}`;
276
- headers.Cookie = `${getAuthCookieName()}=${auth.accessToken}`;
327
+ if (accessToken) {
328
+ headers.Authorization = `Bearer ${accessToken}`;
329
+ headers.Cookie = `${getAuthCookieName()}=${accessToken}`;
277
330
  }
278
- const res = await (fetchImpl ?? fetch)(buildApiUrl(path$1), {
331
+ return headers;
332
+ };
333
+ const doFetch = async (url, method, headers, body, fetchImpl) => {
334
+ return (fetchImpl ?? fetch)(url, {
279
335
  method,
280
336
  headers,
281
337
  body: body == null ? void 0 : JSON.stringify(body)
282
338
  });
283
- if (!res.ok) throw createApiError(await res.json().catch(() => null), res.statusText, res.status);
284
- return await res.json();
339
+ };
340
+ const shouldRetryResponse = (error) => {
341
+ if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") return isServerError(error.status);
342
+ return error instanceof TypeError;
343
+ };
344
+ const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, _retrySleep }) => {
345
+ return withRetry(async () => {
346
+ const auth = readAuth();
347
+ const url = buildApiUrl(path$1);
348
+ let res = await doFetch(url, method, buildHeaders(auth.accessToken), body, fetchImpl);
349
+ if (res.status === 401 && auth.refreshToken) {
350
+ const refreshed = await refreshAccessToken({ fetchImpl });
351
+ if (refreshed?.accessToken) res = await doFetch(url, method, buildHeaders(refreshed.accessToken), body, fetchImpl);
352
+ }
353
+ if (!res.ok) throw createApiError(await res.json().catch(() => null), res.statusText, res.status);
354
+ return await res.json();
355
+ }, {
356
+ maxRetries,
357
+ shouldRetry: shouldRetryResponse,
358
+ sleep: _retrySleep
359
+ });
285
360
  };
286
361
 
287
362
  //#endregion
@@ -420,6 +495,28 @@ const registerAuthCommands = (program) => {
420
495
  });
421
496
  };
422
497
 
498
+ //#endregion
499
+ //#region src/core/api/pagination.ts
500
+ /**
501
+ * Fetches all pages from a paginated API endpoint.
502
+ *
503
+ * @param fetchPage - Function that fetches a single page given a pageToken
504
+ * @param getItems - Function that extracts items from the response
505
+ * @param getNextToken - Function that extracts the next page token from the response
506
+ * @returns Array of all items across all pages
507
+ */
508
+ const fetchAllPages = async (fetchPage, getItems, getNextToken) => {
509
+ const allItems = [];
510
+ let pageToken;
511
+ do {
512
+ const response = await fetchPage(pageToken);
513
+ const items = getItems(response);
514
+ allItems.push(...items);
515
+ pageToken = getNextToken(response);
516
+ } while (pageToken);
517
+ return allItems;
518
+ };
519
+
423
520
  //#endregion
424
521
  //#region src/core/interactive/fuzzy.ts
425
522
  const normalize = (value) => value.toLowerCase();
@@ -504,10 +601,10 @@ const promptKeyEnabled = async (initial) => {
504
601
  return typeof response.enabled === "boolean" ? response.enabled : initial;
505
602
  };
506
603
  const selectConsumer = async (consumerService) => {
507
- const consumers = (await consumerService.ListConsumers({
604
+ const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
508
605
  pageSize: void 0,
509
- pageToken: void 0
510
- }))?.consumers ?? [];
606
+ pageToken
607
+ }), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
511
608
  if (consumers.length === 0) throw new Error("No available API keys");
512
609
  const sorted = sortByCreatedAtDesc(consumers);
513
610
  const nameCounts = buildNameCounts(sorted);
@@ -521,10 +618,10 @@ const selectConsumer = async (consumerService) => {
521
618
  }) ?? null;
522
619
  };
523
620
  const selectConsumerList = async (consumerService, message) => {
524
- const consumers = (await consumerService.ListConsumers({
621
+ const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
525
622
  pageSize: void 0,
526
- pageToken: void 0
527
- }))?.consumers ?? [];
623
+ pageToken
624
+ }), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
528
625
  if (consumers.length === 0) throw new Error("No available API keys");
529
626
  const sorted = sortByCreatedAtDesc(consumers);
530
627
  const nameCounts = buildNameCounts(sorted);
@@ -973,27 +1070,6 @@ const registerCodexCommand = (program) => {
973
1070
  });
974
1071
  };
975
1072
 
976
- //#endregion
977
- //#region src/core/config/redact.ts
978
- const SECRET_KEYS = new Set([
979
- "accessToken",
980
- "refreshToken",
981
- "apiKey"
982
- ]);
983
- const mask = (value) => {
984
- if (!value) return "";
985
- if (value.length <= 8) return "****";
986
- return `${value.slice(0, 4)}...${value.slice(-4)}`;
987
- };
988
- const redactSecrets = (obj) => {
989
- const out = { ...obj };
990
- for (const key of Object.keys(out)) {
991
- const value = out[key];
992
- if (SECRET_KEYS.has(key) && typeof value === "string") out[key] = mask(value);
993
- }
994
- return out;
995
- };
996
-
997
1073
  //#endregion
998
1074
  //#region src/core/output/table.ts
999
1075
  const truncate = (value, max) => {
@@ -1032,13 +1108,12 @@ const consumerRow = (consumer) => [
1032
1108
  String(consumer.apiKey ?? "")
1033
1109
  ];
1034
1110
  const outputConsumerTable = (consumer) => {
1035
- console.log(renderTable(consumerHeaders, [consumerRow(consumer)]));
1111
+ console.log(renderTable(consumerHeaders, [consumerRow(consumer)], { maxColWidth: 64 }));
1036
1112
  };
1037
1113
  const outputConsumers = (consumers) => {
1038
1114
  const rows = consumers.map(consumerRow);
1039
- console.log(renderTable(consumerHeaders, rows));
1115
+ console.log(renderTable(consumerHeaders, rows, { maxColWidth: 64 }));
1040
1116
  };
1041
- const redactConsumer = (consumer) => redactSecrets(consumer);
1042
1117
  const requireInteractive = (message) => {
1043
1118
  if (!process.stdin.isTTY) throw new Error(message);
1044
1119
  };
@@ -1057,10 +1132,10 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
1057
1132
  });
1058
1133
  };
1059
1134
  const listConsumers = async (consumerService) => {
1060
- outputConsumers(((await consumerService.ListConsumers({
1135
+ outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
1061
1136
  pageSize: void 0,
1062
- pageToken: void 0
1063
- }))?.consumers ?? []).map(redactConsumer));
1137
+ pageToken
1138
+ }), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0));
1064
1139
  };
1065
1140
  const resolveConsumerForUpdate = async (consumerService, id) => {
1066
1141
  if (id) return consumerService.GetConsumer({ id });
@@ -1085,7 +1160,7 @@ const updateConsumerById = async (consumerService, id) => {
1085
1160
  requireInteractiveForAction("update");
1086
1161
  const selected = await resolveConsumerForUpdate(consumerService, id);
1087
1162
  if (!selected?.id) return;
1088
- outputConsumerTable(redactConsumer(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true))));
1163
+ outputConsumerTable(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true)));
1089
1164
  };
1090
1165
  const deleteConsumerById = async (consumerService, id) => {
1091
1166
  requireInteractiveForAction("delete");
@@ -1093,7 +1168,7 @@ const deleteConsumerById = async (consumerService, id) => {
1093
1168
  if (!selected?.id) return;
1094
1169
  if (!await confirmDelete(selected)) return;
1095
1170
  await consumerService.DeleteConsumer({ id: selected.id });
1096
- outputConsumerTable(redactConsumer(selected));
1171
+ outputConsumerTable(selected);
1097
1172
  };
1098
1173
  const registerKeysCommands = (program) => {
1099
1174
  const keys = program.command("keys").description("Manage API keys");
@@ -1123,12 +1198,14 @@ const registerKeysCommands = (program) => {
1123
1198
  //#endregion
1124
1199
  //#region src/cmd/models.ts
1125
1200
  const modelHeaders = [
1201
+ "ID",
1126
1202
  "NAME",
1127
1203
  "AUTHOR",
1128
1204
  "ENABLED",
1129
1205
  "UPDATED_AT"
1130
1206
  ];
1131
1207
  const modelRow = (model) => [
1208
+ String(model.id ?? ""),
1132
1209
  String(model.name ?? ""),
1133
1210
  String(model.author ?? ""),
1134
1211
  String(model.enabled ?? ""),
@@ -1221,8 +1298,7 @@ const registerStatusCommand = (program) => {
1221
1298
 
1222
1299
  //#endregion
1223
1300
  //#region src/core/output/usages.ts
1224
- const INPUT_BLOCK = "█";
1225
- const OUTPUT_BLOCK = "▒";
1301
+ const TOTAL_BLOCK = "█";
1226
1302
  const DEFAULT_WIDTH = 24;
1227
1303
  const formatTokens = (value) => {
1228
1304
  const abs = Math.abs(value);
@@ -1253,46 +1329,25 @@ const renderUsageChart = (rows, width = DEFAULT_WIDTH) => {
1253
1329
  const header = "📊 Usage (last 7 days) · Tokens";
1254
1330
  if (rows.length === 0) return `${header}\n\nNo usage data available.`;
1255
1331
  const normalized = rows.map((row) => {
1256
- const input = Number(row.inputTokens);
1257
- const output = Number(row.outputTokens);
1258
- const safeInput = Number.isFinite(input) ? input : 0;
1259
- const safeOutput = Number.isFinite(output) ? output : 0;
1332
+ const total = Number(row.totalTokens);
1333
+ const safeTotal = Number.isFinite(total) ? total : 0;
1260
1334
  return {
1261
1335
  day: row.day,
1262
- input: safeInput,
1263
- output: safeOutput,
1264
- total: safeInput + safeOutput
1336
+ total: safeTotal
1265
1337
  };
1266
1338
  });
1267
1339
  const totals = normalized.map((row) => row.total);
1268
1340
  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
1341
  return [
1291
1342
  header,
1292
1343
  "",
1293
- ...lines,
1294
- "",
1295
- legend
1344
+ ...normalized.map((row) => {
1345
+ if (maxTotal === 0 || row.total === 0) return `${row.day} ${"".padEnd(width, " ")} 0`;
1346
+ const scaled = Math.max(1, Math.round(row.total / maxTotal * width));
1347
+ const bar = TOTAL_BLOCK.repeat(scaled);
1348
+ const totalLabel = formatTokens(row.total);
1349
+ return `${row.day} ${bar.padEnd(width, " ")} ${totalLabel}`;
1350
+ })
1296
1351
  ].join("\n");
1297
1352
  };
1298
1353
 
@@ -1368,7 +1423,7 @@ const registerCommands = (program) => {
1368
1423
  //#region src/cli.ts
1369
1424
  const createProgram = () => {
1370
1425
  const program = new Command();
1371
- program.name("getrouter").description("CLI for getrouter.dev").version("0.1.0");
1426
+ program.name("getrouter").description("CLI for getrouter.dev").version(version);
1372
1427
  registerCommands(program);
1373
1428
  return program;
1374
1429
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrouter/getrouter-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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.10",
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("0.1.0");
10
+ .version(version);
10
11
  registerCommands(program);
11
12
  return program;
12
13
  };
package/src/cmd/keys.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Command } from "commander";
2
2
  import { createApiClients } from "../core/api/client";
3
- import { redactSecrets } from "../core/config/redact";
3
+ import { fetchAllPages } from "../core/api/pagination";
4
4
  import {
5
5
  confirmDelete,
6
6
  promptKeyEnabled,
@@ -32,17 +32,16 @@ const consumerRow = (consumer: ConsumerLike) => [
32
32
  ];
33
33
 
34
34
  const outputConsumerTable = (consumer: ConsumerLike) => {
35
- console.log(renderTable(consumerHeaders, [consumerRow(consumer)]));
35
+ console.log(
36
+ renderTable(consumerHeaders, [consumerRow(consumer)], { maxColWidth: 64 }),
37
+ );
36
38
  };
37
39
 
38
40
  const outputConsumers = (consumers: routercommonv1_Consumer[]) => {
39
41
  const rows = consumers.map(consumerRow);
40
- console.log(renderTable(consumerHeaders, rows));
42
+ console.log(renderTable(consumerHeaders, rows, { maxColWidth: 64 }));
41
43
  };
42
44
 
43
- const redactConsumer = (consumer: routercommonv1_Consumer) =>
44
- redactSecrets(consumer);
45
-
46
45
  const requireInteractive = (message: string) => {
47
46
  if (!process.stdin.isTTY) {
48
47
  throw new Error(message);
@@ -83,11 +82,15 @@ const updateConsumer = async (
83
82
  const listConsumers = async (
84
83
  consumerService: Pick<ConsumerService, "ListConsumers">,
85
84
  ) => {
86
- const res = await consumerService.ListConsumers({
87
- pageSize: undefined,
88
- pageToken: undefined,
89
- });
90
- const consumers = (res?.consumers ?? []).map(redactConsumer);
85
+ const consumers = await fetchAllPages(
86
+ (pageToken) =>
87
+ consumerService.ListConsumers({
88
+ pageSize: undefined,
89
+ pageToken,
90
+ }),
91
+ (res) => res?.consumers ?? [],
92
+ (res) => res?.nextPageToken || undefined,
93
+ );
91
94
  outputConsumers(consumers);
92
95
  };
93
96
 
@@ -143,7 +146,7 @@ const updateConsumerById = async (
143
146
  name,
144
147
  enabled,
145
148
  );
146
- outputConsumerTable(redactConsumer(consumer));
149
+ outputConsumerTable(consumer);
147
150
  };
148
151
 
149
152
  const deleteConsumerById = async (
@@ -159,7 +162,7 @@ const deleteConsumerById = async (
159
162
  const confirmed = await confirmDelete(selected);
160
163
  if (!confirmed) return;
161
164
  await consumerService.DeleteConsumer({ id: selected.id });
162
- outputConsumerTable(redactConsumer(selected));
165
+ outputConsumerTable(selected);
163
166
  };
164
167
 
165
168
  export const registerKeysCommands = (program: Command) => {