codex-slot 0.1.0
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/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/account-store.js +207 -0
- package/dist/cli.js +580 -0
- package/dist/config.js +205 -0
- package/dist/login.js +45 -0
- package/dist/scheduler.js +97 -0
- package/dist/serve.js +24 -0
- package/dist/server.js +402 -0
- package/dist/state.js +121 -0
- package/dist/status.js +199 -0
- package/dist/types.js +2 -0
- package/dist/usage-sync.js +189 -0
- package/package.json +49 -0
package/dist/status.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectAccountStatuses = collectAccountStatuses;
|
|
4
|
+
exports.renderStatusTable = renderStatusTable;
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const account_store_1 = require("./account-store");
|
|
7
|
+
const state_1 = require("./state");
|
|
8
|
+
function computeLeftPercent(usedPercent) {
|
|
9
|
+
if (usedPercent === null || usedPercent === undefined || Number.isNaN(usedPercent)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return Math.max(0, Math.min(100, 100 - usedPercent));
|
|
13
|
+
}
|
|
14
|
+
function isLimited(usedPercent, resetsAt) {
|
|
15
|
+
if (usedPercent === null || usedPercent < 100) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (!resetsAt) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return resetsAt * 1000 > Date.now();
|
|
22
|
+
}
|
|
23
|
+
function formatPercent(value) {
|
|
24
|
+
return value === null ? "-" : `${value}%`;
|
|
25
|
+
}
|
|
26
|
+
function formatReset(unixSeconds) {
|
|
27
|
+
if (!unixSeconds) {
|
|
28
|
+
return "-";
|
|
29
|
+
}
|
|
30
|
+
return new Date(unixSeconds * 1000).toLocaleString("zh-CN", {
|
|
31
|
+
hour12: false
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function formatLimitStatus(label, resetAt) {
|
|
35
|
+
const remaining = formatRemainingDuration(resetAt);
|
|
36
|
+
if (!remaining) {
|
|
37
|
+
return label;
|
|
38
|
+
}
|
|
39
|
+
return `${label}(${remaining})`;
|
|
40
|
+
}
|
|
41
|
+
function normalizeBlockReason(reason) {
|
|
42
|
+
if (!reason) {
|
|
43
|
+
return "blocked";
|
|
44
|
+
}
|
|
45
|
+
if (reason === "five_hour_limited") {
|
|
46
|
+
return "5h_limited";
|
|
47
|
+
}
|
|
48
|
+
return reason;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 将剩余秒数格式化为紧凑的人类可读文本,便于在状态列中展示熔断剩余时间。
|
|
52
|
+
*
|
|
53
|
+
* @param unixSeconds 熔断截止时间,Unix 秒时间戳。
|
|
54
|
+
* @returns 格式化后的剩余时长;当时间为空或已过期时返回 `null`。
|
|
55
|
+
*/
|
|
56
|
+
function formatRemainingDuration(unixSeconds) {
|
|
57
|
+
if (!unixSeconds) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const diffSeconds = unixSeconds - Math.floor(Date.now() / 1000);
|
|
61
|
+
if (diffSeconds <= 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const hours = Math.floor(diffSeconds / 3600);
|
|
65
|
+
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
|
66
|
+
const seconds = diffSeconds % 60;
|
|
67
|
+
if (hours > 0) {
|
|
68
|
+
return `${hours}h${minutes}m`;
|
|
69
|
+
}
|
|
70
|
+
if (minutes > 0) {
|
|
71
|
+
return `${minutes}m${seconds}s`;
|
|
72
|
+
}
|
|
73
|
+
return `${seconds}s`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 将本地熔断原因与剩余时间格式化为更直观的状态文本。
|
|
77
|
+
*
|
|
78
|
+
* @param reason 熔断原因。
|
|
79
|
+
* @param until 熔断截止时间,Unix 秒时间戳。
|
|
80
|
+
* @returns 适合在终端表格中展示的状态文本。
|
|
81
|
+
*/
|
|
82
|
+
function formatBlockedStatus(reason, until) {
|
|
83
|
+
const label = normalizeBlockReason(reason);
|
|
84
|
+
const remaining = formatRemainingDuration(until ?? null);
|
|
85
|
+
if (!remaining) {
|
|
86
|
+
return label;
|
|
87
|
+
}
|
|
88
|
+
return `${label}(${remaining})`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 汇总所有受管账号的运行状态,供状态展示与调度复用。
|
|
92
|
+
*
|
|
93
|
+
* @returns 所有账号的运行时状态列表。
|
|
94
|
+
*/
|
|
95
|
+
function collectAccountStatuses() {
|
|
96
|
+
const config = (0, config_1.loadConfig)();
|
|
97
|
+
return config.accounts.map((account) => {
|
|
98
|
+
const exists = (0, account_store_1.hasCompleteCodexAuthState)(account.codex_home);
|
|
99
|
+
const primary = exists ? (0, account_store_1.resolvePrimaryRegistryAccount)(account.codex_home) : null;
|
|
100
|
+
const usageCache = (0, state_1.getUsageCache)(account.id);
|
|
101
|
+
const activeEmail = usageCache?.email ?? primary?.email ?? account.email;
|
|
102
|
+
const fiveHourUsed = usageCache?.fiveHourUsedPercent ?? null;
|
|
103
|
+
const fiveHourReset = usageCache?.fiveHourResetAt ?? null;
|
|
104
|
+
const weeklyUsed = usageCache?.weeklyUsedPercent ?? null;
|
|
105
|
+
const weeklyReset = usageCache?.weeklyResetAt ?? null;
|
|
106
|
+
const fiveHourLeftPercent = computeLeftPercent(fiveHourUsed);
|
|
107
|
+
const weeklyLeftPercent = computeLeftPercent(weeklyUsed);
|
|
108
|
+
const isFiveHourLimited = isLimited(fiveHourUsed, fiveHourReset);
|
|
109
|
+
const isWeeklyLimited = isLimited(weeklyUsed, weeklyReset);
|
|
110
|
+
const localBlock = (0, state_1.getAccountBlock)(account.id);
|
|
111
|
+
const localBlocked = localBlock?.until != null ? localBlock.until * 1000 > Date.now() : false;
|
|
112
|
+
return {
|
|
113
|
+
id: account.id,
|
|
114
|
+
name: account.name,
|
|
115
|
+
email: activeEmail,
|
|
116
|
+
enabled: account.enabled,
|
|
117
|
+
exists,
|
|
118
|
+
plan: usageCache?.plan ?? primary?.plan ?? "-",
|
|
119
|
+
fiveHourLeftPercent,
|
|
120
|
+
fiveHourResetsAt: fiveHourReset,
|
|
121
|
+
weeklyLeftPercent,
|
|
122
|
+
weeklyResetsAt: weeklyReset,
|
|
123
|
+
isFiveHourLimited,
|
|
124
|
+
isWeeklyLimited,
|
|
125
|
+
localBlockReason: localBlock?.reason,
|
|
126
|
+
localBlockUntil: localBlock?.until ?? null,
|
|
127
|
+
isAvailable: account.enabled &&
|
|
128
|
+
exists &&
|
|
129
|
+
!isFiveHourLimited &&
|
|
130
|
+
!isWeeklyLimited &&
|
|
131
|
+
!localBlocked,
|
|
132
|
+
sourcePath: account.codex_home
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 将账号状态渲染为适合终端输出的表格文本。
|
|
138
|
+
*
|
|
139
|
+
* @param statuses 待展示的账号状态列表。
|
|
140
|
+
* @param options 渲染选项;交互模式下可传入选择列配置,将勾选状态与当前光标合并到表格首列。
|
|
141
|
+
* @returns 可直接打印到终端的表格字符串。
|
|
142
|
+
*/
|
|
143
|
+
function renderStatusTable(statuses, options) {
|
|
144
|
+
const selectorColumn = options?.selectorColumn;
|
|
145
|
+
const rows = [
|
|
146
|
+
[
|
|
147
|
+
...(selectorColumn ? [" "] : []),
|
|
148
|
+
"NAME",
|
|
149
|
+
"EMAIL",
|
|
150
|
+
"PLAN",
|
|
151
|
+
"5H_LEFT",
|
|
152
|
+
"5H_RESET",
|
|
153
|
+
"WEEK_LEFT",
|
|
154
|
+
"WEEK_RESET",
|
|
155
|
+
"STATUS"
|
|
156
|
+
]
|
|
157
|
+
];
|
|
158
|
+
for (const item of statuses) {
|
|
159
|
+
let status = "missing";
|
|
160
|
+
if (item.exists) {
|
|
161
|
+
if (!item.enabled) {
|
|
162
|
+
status = "disabled";
|
|
163
|
+
}
|
|
164
|
+
else if (item.localBlockUntil && item.localBlockUntil * 1000 > Date.now()) {
|
|
165
|
+
status = formatBlockedStatus(item.localBlockReason, item.localBlockUntil);
|
|
166
|
+
}
|
|
167
|
+
else if (item.isWeeklyLimited) {
|
|
168
|
+
status = formatLimitStatus("weekly_limited", item.weeklyResetsAt);
|
|
169
|
+
}
|
|
170
|
+
else if (item.isFiveHourLimited) {
|
|
171
|
+
status = formatLimitStatus("5h_limited", item.fiveHourResetsAt);
|
|
172
|
+
}
|
|
173
|
+
else if (item.isAvailable) {
|
|
174
|
+
status = "available";
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
status = "unknown";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const selectorCell = selectorColumn
|
|
181
|
+
? `${selectorColumn.cursorAccountId === item.id ? ">" : " "}[${selectorColumn.enabledById[item.id] ? "x" : " "}]`
|
|
182
|
+
: null;
|
|
183
|
+
rows.push([
|
|
184
|
+
...(selectorCell ? [selectorCell] : []),
|
|
185
|
+
item.name,
|
|
186
|
+
item.email ?? "-",
|
|
187
|
+
item.plan,
|
|
188
|
+
formatPercent(item.fiveHourLeftPercent),
|
|
189
|
+
formatReset(item.fiveHourResetsAt),
|
|
190
|
+
formatPercent(item.weeklyLeftPercent),
|
|
191
|
+
formatReset(item.weeklyResetsAt),
|
|
192
|
+
status
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
const widths = rows[0].map((_, columnIndex) => Math.max(...rows.map((row) => row[columnIndex].length)));
|
|
196
|
+
return rows
|
|
197
|
+
.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" "))
|
|
198
|
+
.join("\n");
|
|
199
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.refreshAccountTokens = refreshAccountTokens;
|
|
4
|
+
exports.refreshAccountUsage = refreshAccountUsage;
|
|
5
|
+
exports.isUsageCacheStale = isUsageCacheStale;
|
|
6
|
+
exports.refreshAccountUsageInBackgroundIfNeeded = refreshAccountUsageInBackgroundIfNeeded;
|
|
7
|
+
exports.refreshAllAccountUsage = refreshAllAccountUsage;
|
|
8
|
+
const undici_1 = require("undici");
|
|
9
|
+
const account_store_1 = require("./account-store");
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
const state_1 = require("./state");
|
|
12
|
+
const USAGE_CACHE_TTL_MS = 60 * 1000;
|
|
13
|
+
const inflightUsageRefreshes = new Map();
|
|
14
|
+
function normalizeResetAt(value, resetAfterSeconds) {
|
|
15
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
if (typeof resetAfterSeconds === "number" && Number.isFinite(resetAfterSeconds)) {
|
|
19
|
+
return Math.floor(Date.now() / 1000) + resetAfterSeconds;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 使用 refresh token 刷新指定账号的 access token,并回写到账号目录。
|
|
25
|
+
*
|
|
26
|
+
* @param accountId 账号标识。
|
|
27
|
+
* @returns 最新认证信息。
|
|
28
|
+
* @throws 当账号不存在、缺少 refresh_token 或刷新失败时抛出错误。
|
|
29
|
+
*/
|
|
30
|
+
async function refreshAccountTokens(accountId) {
|
|
31
|
+
const config = (0, config_1.loadConfig)();
|
|
32
|
+
const account = (0, account_store_1.findManagedAccount)(accountId);
|
|
33
|
+
if (!account) {
|
|
34
|
+
throw new Error(`未找到账号 ${accountId}`);
|
|
35
|
+
}
|
|
36
|
+
const auth = (0, account_store_1.readAuthFile)(account.codex_home);
|
|
37
|
+
const refreshToken = auth?.tokens?.refresh_token;
|
|
38
|
+
if (!refreshToken) {
|
|
39
|
+
throw new Error(`账号 ${accountId} 缺少 refresh_token`);
|
|
40
|
+
}
|
|
41
|
+
const response = await (0, undici_1.request)(`${config.upstream.auth_base_url}/oauth/token`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
45
|
+
accept: "application/json"
|
|
46
|
+
},
|
|
47
|
+
body: new URLSearchParams({
|
|
48
|
+
grant_type: "refresh_token",
|
|
49
|
+
refresh_token: refreshToken,
|
|
50
|
+
client_id: config.upstream.oauth_client_id
|
|
51
|
+
}).toString()
|
|
52
|
+
});
|
|
53
|
+
if (response.statusCode >= 400) {
|
|
54
|
+
const errorText = await response.body.text();
|
|
55
|
+
throw new Error(`刷新 token 失败: HTTP ${response.statusCode} ${errorText}`);
|
|
56
|
+
}
|
|
57
|
+
const payload = (await response.body.json());
|
|
58
|
+
const nextAuth = {
|
|
59
|
+
...(auth ?? {}),
|
|
60
|
+
auth_mode: "chatgpt",
|
|
61
|
+
OPENAI_API_KEY: null,
|
|
62
|
+
tokens: {
|
|
63
|
+
...(auth?.tokens ?? {}),
|
|
64
|
+
access_token: payload.access_token ?? auth?.tokens?.access_token,
|
|
65
|
+
refresh_token: payload.refresh_token ?? auth?.tokens?.refresh_token,
|
|
66
|
+
id_token: payload.id_token ?? auth?.tokens?.id_token,
|
|
67
|
+
account_id: auth?.tokens?.account_id
|
|
68
|
+
},
|
|
69
|
+
last_refresh: new Date().toISOString()
|
|
70
|
+
};
|
|
71
|
+
(0, account_store_1.writeAuthFile)(account.codex_home, nextAuth);
|
|
72
|
+
return nextAuth;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 查询单个账号的最新额度信息,并写入 cslot 自己的 usage 缓存。
|
|
76
|
+
*
|
|
77
|
+
* @param accountId 账号标识。
|
|
78
|
+
* @returns 刷新后的额度摘要。
|
|
79
|
+
* @throws 当账号不存在、未登录或远端请求失败时抛出错误。
|
|
80
|
+
*/
|
|
81
|
+
async function refreshAccountUsage(accountId) {
|
|
82
|
+
const account = (0, account_store_1.findManagedAccount)(accountId);
|
|
83
|
+
if (!account) {
|
|
84
|
+
throw new Error(`未找到账号 ${accountId}`);
|
|
85
|
+
}
|
|
86
|
+
const auth = (0, account_store_1.readAuthFile)(account.codex_home);
|
|
87
|
+
const accessToken = auth?.tokens?.access_token;
|
|
88
|
+
const accountIdHeader = auth?.tokens?.account_id;
|
|
89
|
+
if (!accessToken) {
|
|
90
|
+
throw new Error(`账号 ${accountId} 缺少 access_token`);
|
|
91
|
+
}
|
|
92
|
+
const response = await (0, undici_1.request)("https://chatgpt.com/backend-api/wham/usage", {
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: {
|
|
95
|
+
authorization: `Bearer ${accessToken}`,
|
|
96
|
+
accept: "application/json",
|
|
97
|
+
"user-agent": "codex-slot/0.1.0",
|
|
98
|
+
...(accountIdHeader ? { "chatgpt-account-id": accountIdHeader } : {})
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (response.statusCode === 401) {
|
|
102
|
+
await refreshAccountTokens(accountId);
|
|
103
|
+
return await refreshAccountUsage(accountId);
|
|
104
|
+
}
|
|
105
|
+
if (response.statusCode >= 400) {
|
|
106
|
+
const errorText = await response.body.text();
|
|
107
|
+
throw new Error(`刷新额度失败: HTTP ${response.statusCode} ${errorText}`);
|
|
108
|
+
}
|
|
109
|
+
const payload = (await response.body.json());
|
|
110
|
+
const primary = (0, account_store_1.resolvePrimaryRegistryAccount)(account.codex_home);
|
|
111
|
+
const email = primary?.email ?? account.email ?? undefined;
|
|
112
|
+
const plan = payload.plan_type ?? primary?.plan ?? "-";
|
|
113
|
+
const result = {
|
|
114
|
+
accountId: account.id,
|
|
115
|
+
email,
|
|
116
|
+
plan,
|
|
117
|
+
fiveHourUsedPercent: payload.rate_limit?.primary_window?.used_percent ?? null,
|
|
118
|
+
fiveHourResetAt: normalizeResetAt(payload.rate_limit?.primary_window?.reset_at, payload.rate_limit?.primary_window?.reset_after_seconds),
|
|
119
|
+
weeklyUsedPercent: payload.rate_limit?.secondary_window?.used_percent ?? null,
|
|
120
|
+
weeklyResetAt: normalizeResetAt(payload.rate_limit?.secondary_window?.reset_at, payload.rate_limit?.secondary_window?.reset_after_seconds),
|
|
121
|
+
refreshedAt: new Date().toISOString()
|
|
122
|
+
};
|
|
123
|
+
(0, state_1.setUsageCache)(result);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 判断指定账号的额度缓存是否已经过期。
|
|
128
|
+
*
|
|
129
|
+
* @param accountId 账号标识。
|
|
130
|
+
* @returns `true` 表示不存在缓存或缓存已超过 TTL,需要重新刷新;`false` 表示缓存仍可直接复用。
|
|
131
|
+
*/
|
|
132
|
+
function isUsageCacheStale(accountId) {
|
|
133
|
+
const usageCache = (0, state_1.getUsageCache)(accountId);
|
|
134
|
+
if (!usageCache?.refreshedAt) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const refreshedAt = Date.parse(usageCache.refreshedAt);
|
|
138
|
+
if (Number.isNaN(refreshedAt)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return Date.now() - refreshedAt > USAGE_CACHE_TTL_MS;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 在不阻塞主请求链路的前提下,按需异步刷新指定账号的额度缓存。
|
|
145
|
+
*
|
|
146
|
+
* @param accountId 账号标识。
|
|
147
|
+
* @returns 无返回值;若缓存仍在 TTL 内或已有刷新任务进行中则直接跳过。
|
|
148
|
+
*/
|
|
149
|
+
function refreshAccountUsageInBackgroundIfNeeded(accountId) {
|
|
150
|
+
if (!isUsageCacheStale(accountId) || inflightUsageRefreshes.has(accountId)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// 同一账号同一时刻只保留一个后台刷新任务,避免高并发下重复打远端 usage 接口。
|
|
154
|
+
const refreshTask = (async () => {
|
|
155
|
+
try {
|
|
156
|
+
await refreshAccountUsage(accountId);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// 后台刷新失败时保留旧缓存,由正式转发请求中的错误处理继续兜底。
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
inflightUsageRefreshes.delete(accountId);
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
inflightUsageRefreshes.set(accountId, refreshTask);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 批量刷新所有受管账号的额度信息。
|
|
169
|
+
*
|
|
170
|
+
* @returns 每个账号对应的刷新结果列表。
|
|
171
|
+
*/
|
|
172
|
+
async function refreshAllAccountUsage() {
|
|
173
|
+
const config = (0, config_1.loadConfig)();
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const account of config.accounts) {
|
|
176
|
+
if (!account.enabled) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const result = await refreshAccountUsage(account.id);
|
|
181
|
+
results.push(result);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
185
|
+
console.error(`[refresh] ${account.id} 失败: ${message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-slot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "本地 Codex 多账号切换与状态管理工具",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"codex-slot": "dist/cli.js",
|
|
9
|
+
"cslot": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"clean": "rm -rf dist",
|
|
16
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/serve.js",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"dev": "tsx src/cli.ts",
|
|
19
|
+
"check": "tsc --noEmit -p tsconfig.json"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"codex",
|
|
23
|
+
"chatgpt",
|
|
24
|
+
"switcher",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"author": "bk",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/openxiaobu/cslot.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/openxiaobu/cslot#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/openxiaobu/cslot/issues"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^14.0.3",
|
|
39
|
+
"fastify": "^5.8.2",
|
|
40
|
+
"undici": "^7.22.0",
|
|
41
|
+
"yaml": "^2.8.2",
|
|
42
|
+
"zod": "^4.3.6"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.4.0",
|
|
46
|
+
"tsx": "^4.21.0",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|