@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.
- 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 +139 -84
- package/package.json +2 -2
- package/src/cli.ts +2 -1
- package/src/cmd/keys.ts +16 -13
- package/src/cmd/models.ts +2 -1
- package/src/core/api/pagination.ts +25 -0
- package/src/core/auth/refresh.ts +68 -0
- package/src/core/http/request.ts +71 -15
- package/src/core/http/retry.ts +68 -0
- package/src/core/interactive/keys.ts +19 -10
- package/src/core/output/usages.ts +11 -30
- package/tests/auth/refresh.test.ts +149 -0
- package/tests/cmd/keys.test.ts +7 -14
- package/tests/cmd/models.test.ts +5 -2
- package/tests/cmd/usages.test.ts +5 -5
- package/tests/core/api/pagination.test.ts +87 -0
- 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 +2 -1
|
@@ -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
package/bun.lock
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"prompts": "^2.4.2",
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@biomejs/biome": "^2.3.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
259
|
-
const
|
|
260
|
-
|
|
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
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
325
|
+
const buildHeaders = (accessToken) => {
|
|
272
326
|
const headers = { "Content-Type": "application/json" };
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
headers.
|
|
276
|
-
headers.Cookie = `${getAuthCookieName()}=${auth.accessToken}`;
|
|
327
|
+
if (accessToken) {
|
|
328
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
329
|
+
headers.Cookie = `${getAuthCookieName()}=${accessToken}`;
|
|
277
330
|
}
|
|
278
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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 =
|
|
604
|
+
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
508
605
|
pageSize: void 0,
|
|
509
|
-
pageToken
|
|
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 =
|
|
621
|
+
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
525
622
|
pageSize: void 0,
|
|
526
|
-
pageToken
|
|
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(((
|
|
1135
|
+
outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1061
1136
|
pageSize: void 0,
|
|
1062
|
-
pageToken
|
|
1063
|
-
}))?.consumers ?? []
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1257
|
-
const
|
|
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
|
-
|
|
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
|
-
...
|
|
1294
|
-
|
|
1295
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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/keys.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { createApiClients } from "../core/api/client";
|
|
3
|
-
import {
|
|
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(
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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(
|
|
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(
|
|
165
|
+
outputConsumerTable(selected);
|
|
163
166
|
};
|
|
164
167
|
|
|
165
168
|
export const registerKeysCommands = (program: Command) => {
|