@getrouter/getrouter-cli 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +7 -0
- package/README.md +7 -0
- package/README.zh-cn.md +7 -0
- package/dist/bin.mjs +40 -29
- package/package.json +1 -1
- package/src/cmd/keys.ts +3 -1
- package/src/core/interactive/codex.ts +16 -17
- package/src/core/interactive/keys.ts +26 -10
- package/tests/cmd/keys.test.ts +34 -0
- package/tests/core/interactive/codex.test.ts +3 -4
package/README.ja.md
CHANGED
|
@@ -51,6 +51,7 @@ getrouter login
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
表示された URL をブラウザで開くと、CLI はトークンを受け取るまでポーリングします。
|
|
54
|
+
すでにログイン済みでも `getrouter login` を再実行すると、ローカルの auth.json は新しいトークンで上書きされます。
|
|
54
55
|
|
|
55
56
|
## よく使うコマンド
|
|
56
57
|
|
|
@@ -75,6 +76,12 @@ getrouter login
|
|
|
75
76
|
getrouter codex
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
Codex の設定/認証から GetRouter の項目を削除する場合:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
getrouter codex uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
書き込まれるファイル(codex):
|
|
79
86
|
|
|
80
87
|
- `~/.codex/config.toml`(model + reasoning + provider 設定)
|
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ getrouter login
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Follow the printed URL in your browser, then the CLI will poll until it receives tokens.
|
|
54
|
+
Re-running `getrouter login` will overwrite the local auth state with new tokens.
|
|
54
55
|
|
|
55
56
|
## Common Commands
|
|
56
57
|
|
|
@@ -75,6 +76,12 @@ Notes:
|
|
|
75
76
|
getrouter codex
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
To remove GetRouter entries from Codex config/auth:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
getrouter codex uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
Files written (codex):
|
|
79
86
|
|
|
80
87
|
- `~/.codex/config.toml` (model + reasoning + provider settings)
|
package/README.zh-cn.md
CHANGED
|
@@ -51,6 +51,7 @@ getrouter login
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
按提示打开浏览器完成确认,CLI 会轮询直到拿到 token。
|
|
54
|
+
即使已登录,也可再次执行 `getrouter login`,会用新 token 覆盖本地 auth.json。
|
|
54
55
|
|
|
55
56
|
## 常用命令
|
|
56
57
|
|
|
@@ -75,6 +76,12 @@ getrouter login
|
|
|
75
76
|
getrouter codex
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
如需移除 Codex 配置/认证中的 GetRouter 条目:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
getrouter codex uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
写入文件(codex):
|
|
79
86
|
|
|
80
87
|
- `~/.codex/config.toml`(model + reasoning + provider 设置)
|
package/dist/bin.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { randomInt } from "node:crypto";
|
|
|
8
8
|
import prompts from "prompts";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "0.1.
|
|
11
|
+
var version = "0.1.10";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
@@ -599,9 +599,19 @@ const fuzzySelect = async ({ message, choices }) => {
|
|
|
599
599
|
|
|
600
600
|
//#endregion
|
|
601
601
|
//#region src/core/interactive/keys.ts
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
return
|
|
602
|
+
const parseTimestamp = (value) => {
|
|
603
|
+
const parsed = Date.parse(value ?? "");
|
|
604
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
605
|
+
};
|
|
606
|
+
const getUpdatedAtTime = (consumer) => {
|
|
607
|
+
const updatedAt = parseTimestamp(consumer.updatedAt);
|
|
608
|
+
if (updatedAt) return updatedAt;
|
|
609
|
+
return parseTimestamp(consumer.createdAt);
|
|
610
|
+
};
|
|
611
|
+
const getDisplayTimestamp = (consumer) => consumer.updatedAt ?? consumer.createdAt ?? "-";
|
|
612
|
+
const sortConsumersByUpdatedAtDesc = (consumers) => consumers.slice().sort((a, b) => {
|
|
613
|
+
const aTime = getUpdatedAtTime(a);
|
|
614
|
+
return getUpdatedAtTime(b) - aTime;
|
|
605
615
|
});
|
|
606
616
|
const normalizeName = (consumer) => {
|
|
607
617
|
const name = consumer.name?.trim();
|
|
@@ -617,8 +627,8 @@ const buildNameCounts = (consumers) => {
|
|
|
617
627
|
};
|
|
618
628
|
const formatChoice = (consumer, nameCounts) => {
|
|
619
629
|
const name = normalizeName(consumer);
|
|
620
|
-
const
|
|
621
|
-
return (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)" ? `${name} (${
|
|
630
|
+
const displayTimestamp = getDisplayTimestamp(consumer);
|
|
631
|
+
return (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)" ? `${name} (${displayTimestamp})` : name;
|
|
622
632
|
};
|
|
623
633
|
const promptKeyName = async (initial) => {
|
|
624
634
|
const response = await prompts({
|
|
@@ -653,14 +663,18 @@ const selectConsumer = async (consumerService) => {
|
|
|
653
663
|
pageToken
|
|
654
664
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
655
665
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
656
|
-
const sorted =
|
|
666
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
657
667
|
const nameCounts = buildNameCounts(sorted);
|
|
658
668
|
return await fuzzySelect({
|
|
659
669
|
message: "🔎 Search keys",
|
|
660
670
|
choices: sorted.map((consumer) => ({
|
|
661
671
|
title: formatChoice(consumer, nameCounts),
|
|
662
672
|
value: consumer,
|
|
663
|
-
keywords: [
|
|
673
|
+
keywords: [
|
|
674
|
+
normalizeName(consumer),
|
|
675
|
+
consumer.updatedAt ?? "",
|
|
676
|
+
consumer.createdAt ?? ""
|
|
677
|
+
].filter(Boolean)
|
|
664
678
|
}))
|
|
665
679
|
}) ?? null;
|
|
666
680
|
};
|
|
@@ -670,7 +684,7 @@ const selectConsumerList = async (consumerService, message) => {
|
|
|
670
684
|
pageToken
|
|
671
685
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
672
686
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
673
|
-
const sorted =
|
|
687
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
674
688
|
const nameCounts = buildNameCounts(sorted);
|
|
675
689
|
const response = await prompts({
|
|
676
690
|
type: "select",
|
|
@@ -955,25 +969,16 @@ const getCodexModelChoices = async () => {
|
|
|
955
969
|
value: model,
|
|
956
970
|
keywords: [model, "codex"]
|
|
957
971
|
}));
|
|
958
|
-
if (remoteChoices.length > 0)
|
|
959
|
-
remoteChoices.sort((a, b) => a.title.localeCompare(b.title));
|
|
960
|
-
return remoteChoices;
|
|
961
|
-
}
|
|
972
|
+
if (remoteChoices.length > 0) return remoteChoices.reverse();
|
|
962
973
|
} catch {}
|
|
963
974
|
return MODEL_CHOICES;
|
|
964
975
|
};
|
|
965
976
|
const REASONING_CHOICES = [
|
|
966
977
|
{
|
|
967
|
-
id: "
|
|
968
|
-
label: "
|
|
969
|
-
value: "
|
|
970
|
-
description: "
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
id: "medium",
|
|
974
|
-
label: "Medium (default)",
|
|
975
|
-
value: "medium",
|
|
976
|
-
description: "Balances speed and reasoning depth for everyday tasks"
|
|
978
|
+
id: "extra_high",
|
|
979
|
+
label: "Extra high",
|
|
980
|
+
value: "xhigh",
|
|
981
|
+
description: "Extra high reasoning depth for complex problems. Warning: Extra high reasoning effort can quickly consume Plus plan rate limits."
|
|
977
982
|
},
|
|
978
983
|
{
|
|
979
984
|
id: "high",
|
|
@@ -982,10 +987,16 @@ const REASONING_CHOICES = [
|
|
|
982
987
|
description: "Greater reasoning depth for complex problems"
|
|
983
988
|
},
|
|
984
989
|
{
|
|
985
|
-
id: "
|
|
986
|
-
label: "
|
|
987
|
-
value: "
|
|
988
|
-
description: "
|
|
990
|
+
id: "medium",
|
|
991
|
+
label: "Medium (default)",
|
|
992
|
+
value: "medium",
|
|
993
|
+
description: "Balances speed and reasoning depth for everyday tasks"
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
id: "low",
|
|
997
|
+
label: "Low",
|
|
998
|
+
value: "low",
|
|
999
|
+
description: "Fast responses with lighter reasoning"
|
|
989
1000
|
}
|
|
990
1001
|
];
|
|
991
1002
|
const REASONING_FUZZY_CHOICES = REASONING_CHOICES.map((choice) => ({
|
|
@@ -1330,10 +1341,10 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
|
|
|
1330
1341
|
});
|
|
1331
1342
|
};
|
|
1332
1343
|
const listConsumers = async (consumerService, showApiKey) => {
|
|
1333
|
-
outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1344
|
+
outputConsumers(sortConsumersByUpdatedAtDesc(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1334
1345
|
pageSize: void 0,
|
|
1335
1346
|
pageToken
|
|
1336
|
-
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0), showApiKey);
|
|
1347
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0)), showApiKey);
|
|
1337
1348
|
};
|
|
1338
1349
|
const resolveConsumerForUpdate = async (consumerService, id) => {
|
|
1339
1350
|
if (id) return consumerService.GetConsumer({ id });
|
package/package.json
CHANGED
package/src/cmd/keys.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
promptKeyEnabled,
|
|
8
8
|
promptKeyName,
|
|
9
9
|
selectConsumerList,
|
|
10
|
+
sortConsumersByUpdatedAtDesc,
|
|
10
11
|
} from "../core/interactive/keys";
|
|
11
12
|
import { renderTable } from "../core/output/table";
|
|
12
13
|
import type {
|
|
@@ -103,7 +104,8 @@ const listConsumers = async (
|
|
|
103
104
|
(res) => res?.consumers ?? [],
|
|
104
105
|
(res) => res?.nextPageToken || undefined,
|
|
105
106
|
);
|
|
106
|
-
|
|
107
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
108
|
+
outputConsumers(sorted, showApiKey);
|
|
107
109
|
};
|
|
108
110
|
|
|
109
111
|
const resolveConsumerForUpdate = async (
|
|
@@ -48,8 +48,7 @@ export const getCodexModelChoices = async (): Promise<
|
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
50
|
if (remoteChoices.length > 0) {
|
|
51
|
-
remoteChoices.
|
|
52
|
-
return remoteChoices;
|
|
51
|
+
return remoteChoices.reverse();
|
|
53
52
|
}
|
|
54
53
|
} catch {}
|
|
55
54
|
|
|
@@ -58,16 +57,11 @@ export const getCodexModelChoices = async (): Promise<
|
|
|
58
57
|
|
|
59
58
|
export const REASONING_CHOICES: ReasoningChoice[] = [
|
|
60
59
|
{
|
|
61
|
-
id: "
|
|
62
|
-
label: "
|
|
63
|
-
value: "
|
|
64
|
-
description:
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
id: "medium",
|
|
68
|
-
label: "Medium (default)",
|
|
69
|
-
value: "medium",
|
|
70
|
-
description: "Balances speed and reasoning depth for everyday tasks",
|
|
60
|
+
id: "extra_high",
|
|
61
|
+
label: "Extra high",
|
|
62
|
+
value: "xhigh",
|
|
63
|
+
description:
|
|
64
|
+
"Extra high reasoning depth for complex problems. Warning: Extra high reasoning effort can quickly consume Plus plan rate limits.",
|
|
71
65
|
},
|
|
72
66
|
{
|
|
73
67
|
id: "high",
|
|
@@ -76,11 +70,16 @@ export const REASONING_CHOICES: ReasoningChoice[] = [
|
|
|
76
70
|
description: "Greater reasoning depth for complex problems",
|
|
77
71
|
},
|
|
78
72
|
{
|
|
79
|
-
id: "
|
|
80
|
-
label: "
|
|
81
|
-
value: "
|
|
82
|
-
description:
|
|
83
|
-
|
|
73
|
+
id: "medium",
|
|
74
|
+
label: "Medium (default)",
|
|
75
|
+
value: "medium",
|
|
76
|
+
description: "Balances speed and reasoning depth for everyday tasks",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "low",
|
|
80
|
+
label: "Low",
|
|
81
|
+
value: "low",
|
|
82
|
+
description: "Fast responses with lighter reasoning",
|
|
84
83
|
},
|
|
85
84
|
];
|
|
86
85
|
|
|
@@ -17,10 +17,24 @@ export type KeyMenuAction =
|
|
|
17
17
|
| "delete"
|
|
18
18
|
| "exit";
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const parseTimestamp = (value?: string) => {
|
|
21
|
+
const parsed = Date.parse(value ?? "");
|
|
22
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getUpdatedAtTime = (consumer: Consumer) => {
|
|
26
|
+
const updatedAt = parseTimestamp(consumer.updatedAt);
|
|
27
|
+
if (updatedAt) return updatedAt;
|
|
28
|
+
return parseTimestamp(consumer.createdAt);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getDisplayTimestamp = (consumer: Consumer) =>
|
|
32
|
+
consumer.updatedAt ?? consumer.createdAt ?? "-";
|
|
33
|
+
|
|
34
|
+
export const sortConsumersByUpdatedAtDesc = (consumers: Consumer[]) =>
|
|
21
35
|
consumers.slice().sort((a, b) => {
|
|
22
|
-
const aTime =
|
|
23
|
-
const bTime =
|
|
36
|
+
const aTime = getUpdatedAtTime(a);
|
|
37
|
+
const bTime = getUpdatedAtTime(b);
|
|
24
38
|
return bTime - aTime;
|
|
25
39
|
});
|
|
26
40
|
|
|
@@ -40,9 +54,9 @@ const buildNameCounts = (consumers: Consumer[]) => {
|
|
|
40
54
|
|
|
41
55
|
const formatChoice = (consumer: Consumer, nameCounts: Map<string, number>) => {
|
|
42
56
|
const name = normalizeName(consumer);
|
|
43
|
-
const
|
|
57
|
+
const displayTimestamp = getDisplayTimestamp(consumer);
|
|
44
58
|
const needsDetail = (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)";
|
|
45
|
-
return needsDetail ? `${name} (${
|
|
59
|
+
return needsDetail ? `${name} (${displayTimestamp})` : name;
|
|
46
60
|
};
|
|
47
61
|
|
|
48
62
|
export const selectKeyAction = async (): Promise<KeyMenuAction> => {
|
|
@@ -127,16 +141,18 @@ export const selectConsumer = async (
|
|
|
127
141
|
if (consumers.length === 0) {
|
|
128
142
|
throw new Error("No available API keys");
|
|
129
143
|
}
|
|
130
|
-
const sorted =
|
|
144
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
131
145
|
const nameCounts = buildNameCounts(sorted);
|
|
132
146
|
const selected = await fuzzySelect({
|
|
133
147
|
message: "🔎 Search keys",
|
|
134
148
|
choices: sorted.map((consumer) => ({
|
|
135
149
|
title: formatChoice(consumer, nameCounts),
|
|
136
150
|
value: consumer,
|
|
137
|
-
keywords: [
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
keywords: [
|
|
152
|
+
normalizeName(consumer),
|
|
153
|
+
consumer.updatedAt ?? "",
|
|
154
|
+
consumer.createdAt ?? "",
|
|
155
|
+
].filter(Boolean),
|
|
140
156
|
})),
|
|
141
157
|
});
|
|
142
158
|
return selected ?? null;
|
|
@@ -158,7 +174,7 @@ export const selectConsumerList = async (
|
|
|
158
174
|
if (consumers.length === 0) {
|
|
159
175
|
throw new Error("No available API keys");
|
|
160
176
|
}
|
|
161
|
-
const sorted =
|
|
177
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
162
178
|
const nameCounts = buildNameCounts(sorted);
|
|
163
179
|
const response = await prompts({
|
|
164
180
|
type: "select",
|
package/tests/cmd/keys.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ const mockConsumer = {
|
|
|
19
19
|
apiKey: "abcd1234WXYZ",
|
|
20
20
|
lastAccess: "2026-01-02T00:00:00Z",
|
|
21
21
|
createdAt: "2026-01-01T00:00:00Z",
|
|
22
|
+
updatedAt: "2026-01-02T00:00:00Z",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const emptyAuthService = {} as AuthService;
|
|
@@ -113,6 +114,39 @@ describe("keys command", () => {
|
|
|
113
114
|
log.mockRestore();
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
it("sorts keys by updatedAt desc in list output", async () => {
|
|
118
|
+
setStdinTTY(false);
|
|
119
|
+
const consumers = [
|
|
120
|
+
{
|
|
121
|
+
...mockConsumer,
|
|
122
|
+
id: "c1",
|
|
123
|
+
name: "older-key",
|
|
124
|
+
updatedAt: "2026-01-02T00:00:00Z",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
...mockConsumer,
|
|
128
|
+
id: "c2",
|
|
129
|
+
name: "newer-key",
|
|
130
|
+
updatedAt: "2026-01-03T00:00:00Z",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
134
|
+
consumerService: {
|
|
135
|
+
ListConsumers: vi.fn().mockResolvedValue({ consumers }),
|
|
136
|
+
} as unknown as ConsumerService,
|
|
137
|
+
subscriptionService: emptySubscriptionService,
|
|
138
|
+
authService: emptyAuthService,
|
|
139
|
+
});
|
|
140
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
141
|
+
const program = createProgram();
|
|
142
|
+
await program.parseAsync(["node", "getrouter", "keys", "list"]);
|
|
143
|
+
const output = log.mock.calls.map((c) => c[0]).join("\n");
|
|
144
|
+
expect(output.indexOf("newer-key")).toBeLessThan(
|
|
145
|
+
output.indexOf("older-key"),
|
|
146
|
+
);
|
|
147
|
+
log.mockRestore();
|
|
148
|
+
});
|
|
149
|
+
|
|
116
150
|
it("rejects removed get subcommand", async () => {
|
|
117
151
|
setStdinTTY(false);
|
|
118
152
|
const program = createProgram();
|
|
@@ -25,15 +25,14 @@ describe("codex interactive helpers", () => {
|
|
|
25
25
|
ok: true,
|
|
26
26
|
status: 200,
|
|
27
27
|
json: vi.fn().mockResolvedValue({
|
|
28
|
-
models: ["
|
|
28
|
+
models: ["older-codex-model", "newer-codex-model"],
|
|
29
29
|
}),
|
|
30
30
|
});
|
|
31
31
|
vi.stubGlobal("fetch", fetchMock);
|
|
32
32
|
|
|
33
33
|
const choices = await getCodexModelChoices();
|
|
34
|
-
expect(
|
|
35
|
-
|
|
36
|
-
).toBe(true);
|
|
34
|
+
expect(choices[0]?.value).toBe("newer-codex-model");
|
|
35
|
+
expect(choices[1]?.value).toBe("older-codex-model");
|
|
37
36
|
expect(String(fetchMock.mock.calls[0]?.[0] ?? "")).toContain(
|
|
38
37
|
"/v1/dashboard/providers/models?tag=codex",
|
|
39
38
|
);
|