Merge pull request #3077 from twodogegg/add-sub2api-admin-skill

docs: add Sub2API admin skill
This commit is contained in:
Wesley Liddick
2026-06-07 11:44:56 +08:00
committed by GitHub
4 changed files with 865 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
---
name: sub2api-admin
description: Manage Sub2API admin APIs for accounts, groups, proxies, error passthrough rules, TLS fingerprint profiles, imports, exports, batch updates, and raw administrator API calls. Use when the user mentions Sub2API, admin API keys, account management, bulk account import/export, keeping or deleting accounts, refreshing accounts, clearing errors, CRS sync, or managing Sub2API backend settings through the admin API.
---
# Sub2API Admin
Use the bundled CLI instead of ad hoc `curl`.
```bash
export SUB2API_BASE_URL='https://your-sub2api-host'
export SUB2API_ADMIN_API_KEY='<admin api key>'
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list
```
For all commands and payload examples, read [references/admin-cli.md](references/admin-cli.md).
## Workflow
1. Reuse `SUB2API_BASE_URL` and `SUB2API_ADMIN_API_KEY` from the environment.
2. Run read-only commands first: `accounts list`, `accounts get <id>`, `groups all`, or `proxies all`.
3. Before destructive or bulk writes, print the target account names and IDs.
4. Execute the write command only after the target set is clear.
5. Run a follow-up read command to verify the result.
## Common Commands
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --page-size 20
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts get 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts usage 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-schedulable 40 true
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10}'
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules list
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list
```
## Safety Notes
- Authentication uses only `x-api-key`.
- If the API returns `INVALID_ADMIN_KEY`, ask the user to regenerate the admin API key.
- `accounts export` includes credentials and tokens. Prefer `--file` and avoid printing exports in chat.
- For uncertain or newly added backend APIs, use `api <METHOD> <admin-path>` after a read-only check.
+7
View File
@@ -0,0 +1,7 @@
interface:
display_name: "Sub2API Admin"
short_description: "Manage Sub2API admin accounts and settings"
default_prompt: "Use $sub2api-admin to inspect and manage Sub2API backend accounts safely."
policy:
allow_implicit_invocation: true
@@ -0,0 +1,201 @@
# Sub2API Admin Reference
## Environment
```bash
export SUB2API_BASE_URL='https://your-sub2api-host'
export SUB2API_ADMIN_API_KEY='<admin api key>'
```
后台鉴权只使用 `x-api-key`。如果返回 `INVALID_ADMIN_KEY`,重新生成管理员 API Key。
## CLI
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js <command>
```
## Accounts
### 只读
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --page-size 20
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --search outlook --platform openai --type oauth --status active
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts get 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts usage 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts stats 40 --days 30
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts today-stats 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-today-stats --ids 40,39
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts models 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts temp-unschedulable 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts antigravity-default-model-mapping
```
`accounts export` 会包含账号凭据和 token,建议写入文件,不要直接刷屏:
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts export --ids 40,39 --file accounts-export.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts export --platform openai --type oauth --include-proxies false --file accounts-export.json
```
### 单账号写入
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts create --file account.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts update 40 --json '{"concurrency":20}'
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-status 40 active
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-schedulable 40 true
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts clear-error 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts clear-rate-limit 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts recover-state 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-quota 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts refresh 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts test 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts sync-models 40
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts apply-oauth 40 --file credentials.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-temp-unschedulable 40
```
### 删除与清理
删除前先列出目标账号名和 ID。
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts delete 25
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts keep-only --name 'target@example.com'
```
### 批量写入
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-create --file accounts.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-update-credentials --file payload.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10,"priority":2}'
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-refresh --ids 40,39
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-clear-error --ids 40,39
```
`bulk-update` 可覆盖页面“批量更新”的字段,payload 由后台表单字段决定,例如 `base_url`、`model_mapping`、`group_ids`、`proxy_id`、`concurrency`、`priority`、`rate_multiplier`、`status`、`compact_mode` 等。更新前先用 `accounts get <id>` 确认字段名。
### 导入
通用后台导入:
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-data --file accounts-export.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-codex-session --file payload.json
```
CRS 同步:
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts crs-preview --file payload.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts crs-sync --file payload.json
```
旧版 JSON 导入仍可用,会把模板账号的配置复制给导入账号:
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-json \
--file /path/accounts.json \
--template-name 'template@example.com' \
--dry-run
```
复制字段:
- `concurrency`
- `priority`
- `group_ids`
- `credentials.model_mapping`
## Groups And Proxies
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js groups all
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js proxies all
```
## Error Rules And TLS Profiles
对应账号页顶部“错误透传规则”和“TLS 指纹模板”。
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules list
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules get 1
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules create --file rule.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules update 1 --json '{"enabled":true}'
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules toggle 1 false
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules delete 1
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles get 1
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles create --file profile.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles update 1 --file profile.json
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles delete 1
```
## Raw Admin API
未封装或新版本后台接口可用 `api` 直通。路径可写 `/admin/...` 或 `/api/v1/admin/...`。
```bash
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api GET /admin/groups/all
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api POST /admin/accounts/bulk-update \
--json '{"account_ids":[40],"concurrency":10}'
```
## Confirmed Admin Endpoints
- `GET /api/v1/admin/accounts`
- `GET /api/v1/admin/accounts/:id`
- `POST /api/v1/admin/accounts`
- `PUT /api/v1/admin/accounts/:id`
- `DELETE /api/v1/admin/accounts/:id`
- `POST /api/v1/admin/accounts/check-mixed-channel`
- `GET /api/v1/admin/accounts/:id/usage`
- `GET /api/v1/admin/accounts/:id/stats`
- `GET /api/v1/admin/accounts/:id/today-stats`
- `POST /api/v1/admin/accounts/today-stats/batch`
- `POST /api/v1/admin/accounts/:id/schedulable`
- `POST /api/v1/admin/accounts/:id/test`
- `POST /api/v1/admin/accounts/:id/refresh`
- `POST /api/v1/admin/accounts/:id/apply-oauth-credentials`
- `POST /api/v1/admin/accounts/:id/clear-error`
- `POST /api/v1/admin/accounts/:id/clear-rate-limit`
- `POST /api/v1/admin/accounts/:id/recover-state`
- `POST /api/v1/admin/accounts/:id/reset-quota`
- `GET /api/v1/admin/accounts/:id/temp-unschedulable`
- `DELETE /api/v1/admin/accounts/:id/temp-unschedulable`
- `GET /api/v1/admin/accounts/:id/models`
- `POST /api/v1/admin/accounts/:id/models/sync-upstream`
- `POST /api/v1/admin/accounts/batch`
- `POST /api/v1/admin/accounts/batch-update-credentials`
- `POST /api/v1/admin/accounts/bulk-update`
- `POST /api/v1/admin/accounts/batch-refresh`
- `POST /api/v1/admin/accounts/batch-clear-error`
- `GET /api/v1/admin/accounts/data`
- `POST /api/v1/admin/accounts/data`
- `POST /api/v1/admin/accounts/import/codex-session`
- `POST /api/v1/admin/accounts/sync/crs/preview`
- `POST /api/v1/admin/accounts/sync/crs`
- `GET /api/v1/admin/accounts/antigravity/default-model-mapping`
- `GET /api/v1/admin/groups/all`
- `GET /api/v1/admin/proxies/all`
- `GET /api/v1/admin/error-passthrough-rules`
- `GET /api/v1/admin/error-passthrough-rules/:id`
- `POST /api/v1/admin/error-passthrough-rules`
- `PUT /api/v1/admin/error-passthrough-rules/:id`
- `DELETE /api/v1/admin/error-passthrough-rules/:id`
- `GET /api/v1/admin/tls-fingerprint-profiles`
- `GET /api/v1/admin/tls-fingerprint-profiles/:id`
- `POST /api/v1/admin/tls-fingerprint-profiles`
- `PUT /api/v1/admin/tls-fingerprint-profiles/:id`
- `DELETE /api/v1/admin/tls-fingerprint-profiles/:id`
## Notes
- 线上写入前先只读核对目标集合。
- 导出结果包含敏感凭据,优先使用 `--file`。
- `PUT /admin/accounts/:id` 和 `bulk-update` 接受宽松请求体,字段名不确定时先用 `accounts get` 或后台页面确认。
+613
View File
@@ -0,0 +1,613 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const BASE_URL = (process.env.SUB2API_BASE_URL || "").replace(/\/$/, "");
const ADMIN_API_KEY = process.env.SUB2API_ADMIN_API_KEY || "";
function usage() {
console.log(`Usage:
sub2api-admin.js accounts list [--page-size 200] [--page N] [--search TEXT] [--platform openai] [--type oauth] [--status active] [--group NAME] [--privacy-mode MODE] [--sort-by name] [--sort-order asc]
sub2api-admin.js accounts export [--ids 1,2] [--file accounts.json] [--include-proxies false] [list filters...]
sub2api-admin.js accounts import-data --file accounts.json [--skip-default-group-bind]
sub2api-admin.js accounts create --json '{...}' | --file account.json
sub2api-admin.js accounts update <id> --json '{...}' | --file patch.json
sub2api-admin.js accounts get <id>
sub2api-admin.js accounts delete <id>
sub2api-admin.js accounts keep-only --name <account-name>
sub2api-admin.js accounts usage <id> [--source SOURCE] [--force]
sub2api-admin.js accounts stats <id> [--days 30]
sub2api-admin.js accounts today-stats <id>
sub2api-admin.js accounts batch-today-stats --ids 1,2
sub2api-admin.js accounts set-status <id> <active|paused|...>
sub2api-admin.js accounts set-schedulable <id> <true|false>
sub2api-admin.js accounts clear-error <id>
sub2api-admin.js accounts clear-rate-limit <id>
sub2api-admin.js accounts recover-state <id>
sub2api-admin.js accounts reset-quota <id>
sub2api-admin.js accounts refresh <id>
sub2api-admin.js accounts test <id>
sub2api-admin.js accounts models <id>
sub2api-admin.js accounts sync-models <id>
sub2api-admin.js accounts apply-oauth <id> --json '{...}' | --file credentials.json
sub2api-admin.js accounts batch-create --file accounts.json
sub2api-admin.js accounts batch-update-credentials --json '{...}' | --file payload.json
sub2api-admin.js accounts bulk-update --ids 1,2 --json '{...}' | --file patch.json
sub2api-admin.js accounts batch-refresh --ids 1,2
sub2api-admin.js accounts batch-clear-error --ids 1,2
sub2api-admin.js accounts temp-unschedulable <id>
sub2api-admin.js accounts reset-temp-unschedulable <id>
sub2api-admin.js accounts crs-preview --json '{...}' | --file payload.json
sub2api-admin.js accounts crs-sync --json '{...}' | --file payload.json
sub2api-admin.js accounts import-codex-session --json '{...}' | --file payload.json
sub2api-admin.js accounts antigravity-default-model-mapping
sub2api-admin.js accounts import-json --file <path> --template-name <name> [--skip-name <name>] [--dry-run]
sub2api-admin.js groups all
sub2api-admin.js proxies all
sub2api-admin.js error-rules list|get|create|update|delete|toggle ...
sub2api-admin.js tls-profiles list|get|create|update|delete ...
sub2api-admin.js api <GET|POST|PUT|DELETE> <admin-path> [--json '{...}' | --file payload.json]
`);
}
function parseArgs(argv) {
const positional = [];
const flags = {};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token.startsWith("--")) {
const key = token.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
flags[key] = true;
} else {
if (flags[key] === undefined) {
flags[key] = next;
} else if (Array.isArray(flags[key])) {
flags[key].push(next);
} else {
flags[key] = [flags[key], next];
}
i += 1;
}
} else {
positional.push(token);
}
}
return { positional, flags };
}
function authHeaders() {
if (!BASE_URL) throw new Error("Missing SUB2API_BASE_URL");
if (ADMIN_API_KEY) return { "x-api-key": ADMIN_API_KEY };
throw new Error("Missing SUB2API_ADMIN_API_KEY");
}
async function apiRequest(method, pathname, body) {
const headers = {
...authHeaders(),
Accept: "application/json",
};
const options = { method, headers };
if (body !== undefined) {
headers["Content-Type"] = "application/json";
options.body = JSON.stringify(body);
}
const res = await fetch(`${BASE_URL}${pathname}`, options);
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
if (!res.ok || (data && data.code !== undefined && data.code !== 0 && data.code !== "0")) {
const detail = data.message || data.code || res.statusText;
throw new Error(`${method} ${pathname} failed: ${detail}`);
}
return data.data;
}
function encodeQuery(params) {
const pairs = [];
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null || value === false || value === "") continue;
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
}
return pairs.length ? `?${pairs.join("&")}` : "";
}
function normalizeAdminPath(pathname) {
if (!pathname.startsWith("/")) pathname = `/${pathname}`;
if (pathname.startsWith("/api/v1/admin/")) return pathname;
if (pathname.startsWith("/admin/")) return `/api/v1${pathname}`;
return pathname;
}
async function adminRequest(method, adminPath, body) {
return apiRequest(method, normalizeAdminPath(adminPath), body);
}
async function listAccounts(options = {}) {
const params = {
page: options.page || 1,
page_size: options.pageSize || 200,
sort_by: options.sortBy || "name",
sort_order: options.sortOrder || "asc",
lite: options.lite === false ? undefined : 1,
search: options.search,
platform: options.platform,
type: options.type,
status: options.status,
group: options.group,
privacy_mode: options.privacyMode,
};
return apiRequest("GET", `/api/v1/admin/accounts${encodeQuery(params)}`);
}
async function getAccount(id) {
return apiRequest("GET", `/api/v1/admin/accounts/${id}`);
}
async function deleteAccount(id) {
return apiRequest("DELETE", `/api/v1/admin/accounts/${id}`);
}
function asArray(value) {
if (value === undefined) return [];
return Array.isArray(value) ? value : [value];
}
function parseBool(value, name = "value") {
if (value === true || value === "true" || value === "1" || value === "yes") return true;
if (value === false || value === "false" || value === "0" || value === "no") return false;
throw new Error(`${name} must be true or false`);
}
function parseIds(value) {
if (!value) throw new Error("requires --ids");
return String(value)
.split(",")
.map((id) => id.trim())
.filter(Boolean)
.map((id) => {
const n = Number(id);
if (!Number.isFinite(n)) throw new Error(`invalid id: ${id}`);
return n;
});
}
function readJsonPayload(flags, { required = true } = {}) {
if (flags.json !== undefined) return JSON.parse(flags.json);
if (flags.file !== undefined) return JSON.parse(fs.readFileSync(path.resolve(flags.file), "utf8"));
if (required) throw new Error("requires --json or --file");
return undefined;
}
function accountListOptions(flags) {
return {
page: Number(flags.page || 1),
pageSize: Number(flags["page-size"] || 200),
search: flags.search,
platform: flags.platform,
type: flags.type,
status: flags.status,
group: flags.group,
privacyMode: flags["privacy-mode"],
sortBy: flags["sort-by"] || "name",
sortOrder: flags["sort-order"] || "asc",
lite: flags.lite === undefined ? true : parseBool(flags.lite, "--lite"),
};
}
function printJson(data) {
console.log(JSON.stringify(data, null, 2));
}
async function accountData(flags) {
const params = {};
if (flags.ids) {
params.ids = parseIds(flags.ids).join(",");
} else {
const map = {
platform: "platform",
type: "type",
status: "status",
group: "group",
search: "search",
"privacy-mode": "privacy_mode",
"sort-by": "sort_by",
"sort-order": "sort_order",
};
for (const [flag, param] of Object.entries(map)) {
if (flags[flag]) params[param] = flags[flag];
}
}
if (flags["include-proxies"] !== undefined) {
params.include_proxies = String(parseBool(flags["include-proxies"], "--include-proxies"));
}
return adminRequest("GET", `/admin/accounts/data${encodeQuery(params)}`);
}
async function commandAccounts(args) {
const sub = args.positional[1];
if (sub === "list") {
const data = await listAccounts(accountListOptions(args.flags));
printJson(data);
return;
}
if (sub === "export") {
const data = await accountData(args.flags);
if (args.flags.file) {
fs.writeFileSync(path.resolve(args.flags.file), JSON.stringify(data, null, 2));
printJson({ file: path.resolve(args.flags.file), count: Array.isArray(data) ? data.length : data.count });
} else {
printJson(data);
}
return;
}
if (sub === "import-data") {
const file = args.flags.file;
if (!file) throw new Error("accounts import-data requires --file");
const payload = {
data: JSON.parse(fs.readFileSync(path.resolve(file), "utf8")),
skip_default_group_bind: Boolean(args.flags["skip-default-group-bind"]),
};
printJson(await adminRequest("POST", "/admin/accounts/data", payload));
return;
}
if (sub === "create") {
printJson(await adminRequest("POST", "/admin/accounts", readJsonPayload(args.flags)));
return;
}
if (sub === "update") {
const id = args.positional[2];
if (!id) throw new Error("accounts update requires <id>");
printJson(await adminRequest("PUT", `/admin/accounts/${id}`, readJsonPayload(args.flags)));
return;
}
if (sub === "get") {
const id = args.positional[2];
if (!id) throw new Error("accounts get requires <id>");
const data = await getAccount(id);
printJson(data);
return;
}
if (sub === "delete") {
const id = args.positional[2];
if (!id) throw new Error("accounts delete requires <id>");
const data = await deleteAccount(id);
printJson(data);
return;
}
if (sub === "keep-only") {
const name = args.flags.name;
if (!name) throw new Error("accounts keep-only requires --name");
const data = await listAccounts({ pageSize: Number(args.flags["page-size"] || 500) });
const items = data.items || [];
const keep = items.find((item) => item.name === name);
if (!keep) throw new Error(`account not found: ${name}`);
const targets = items.filter((item) => item.name !== name);
const results = [];
for (const item of targets) {
const out = await deleteAccount(item.id);
results.push({ id: item.id, name: item.name, result: out });
}
printJson({ kept: { id: keep.id, name: keep.name }, deleted: results });
return;
}
if (sub === "usage") {
const id = args.positional[2];
if (!id) throw new Error("accounts usage requires <id>");
const params = encodeQuery({ source: args.flags.source, force: args.flags.force ? "true" : undefined });
printJson(await adminRequest("GET", `/admin/accounts/${id}/usage${params}`));
return;
}
if (sub === "stats") {
const id = args.positional[2];
if (!id) throw new Error("accounts stats requires <id>");
printJson(await adminRequest("GET", `/admin/accounts/${id}/stats${encodeQuery({ days: args.flags.days || 30 })}`));
return;
}
if (sub === "today-stats") {
const id = args.positional[2];
if (!id) throw new Error("accounts today-stats requires <id>");
printJson(await adminRequest("GET", `/admin/accounts/${id}/today-stats`));
return;
}
if (sub === "batch-today-stats") {
printJson(await adminRequest("POST", "/admin/accounts/today-stats/batch", { account_ids: parseIds(args.flags.ids) }));
return;
}
if (sub === "set-status") {
const id = args.positional[2];
const status = args.positional[3];
if (!id || !status) throw new Error("accounts set-status requires <id> <status>");
printJson(await adminRequest("PUT", `/admin/accounts/${id}`, { status }));
return;
}
if (sub === "set-schedulable") {
const id = args.positional[2];
const schedulable = args.positional[3];
if (!id || schedulable === undefined) throw new Error("accounts set-schedulable requires <id> <true|false>");
printJson(await adminRequest("POST", `/admin/accounts/${id}/schedulable`, { schedulable: parseBool(schedulable, "schedulable") }));
return;
}
const noBodyPostById = {
"clear-error": "clear-error",
"clear-rate-limit": "clear-rate-limit",
"recover-state": "recover-state",
"reset-quota": "reset-quota",
refresh: "refresh",
test: "test",
"sync-models": "models/sync-upstream",
"set-privacy": "set-privacy",
};
if (noBodyPostById[sub]) {
const id = args.positional[2];
if (!id) throw new Error(`accounts ${sub} requires <id>`);
printJson(await adminRequest("POST", `/admin/accounts/${id}/${noBodyPostById[sub]}`));
return;
}
if (sub === "models") {
const id = args.positional[2];
if (!id) throw new Error("accounts models requires <id>");
printJson(await adminRequest("GET", `/admin/accounts/${id}/models`));
return;
}
if (sub === "apply-oauth") {
const id = args.positional[2];
if (!id) throw new Error("accounts apply-oauth requires <id>");
printJson(await adminRequest("POST", `/admin/accounts/${id}/apply-oauth-credentials`, readJsonPayload(args.flags)));
return;
}
if (sub === "batch-create") {
const payload = readJsonPayload(args.flags);
const accounts = Array.isArray(payload) ? payload : payload.accounts;
if (!Array.isArray(accounts)) throw new Error("batch-create payload must be an array or {accounts:[...]}");
printJson(await adminRequest("POST", "/admin/accounts/batch", { accounts }));
return;
}
if (sub === "batch-update-credentials") {
printJson(await adminRequest("POST", "/admin/accounts/batch-update-credentials", readJsonPayload(args.flags)));
return;
}
if (sub === "bulk-update") {
const patch = readJsonPayload(args.flags, { required: false }) || {};
const payload = args.flags.ids ? { account_ids: parseIds(args.flags.ids), ...patch } : patch;
if (!Array.isArray(payload.account_ids) || payload.account_ids.length === 0) {
throw new Error("accounts bulk-update requires --ids or payload.account_ids");
}
printJson(await adminRequest("POST", "/admin/accounts/bulk-update", payload));
return;
}
if (sub === "batch-refresh") {
printJson(await adminRequest("POST", "/admin/accounts/batch-refresh", { account_ids: parseIds(args.flags.ids) }));
return;
}
if (sub === "batch-clear-error") {
printJson(await adminRequest("POST", "/admin/accounts/batch-clear-error", { account_ids: parseIds(args.flags.ids) }));
return;
}
if (sub === "temp-unschedulable") {
const id = args.positional[2];
if (!id) throw new Error("accounts temp-unschedulable requires <id>");
printJson(await adminRequest("GET", `/admin/accounts/${id}/temp-unschedulable`));
return;
}
if (sub === "reset-temp-unschedulable") {
const id = args.positional[2];
if (!id) throw new Error("accounts reset-temp-unschedulable requires <id>");
printJson(await adminRequest("DELETE", `/admin/accounts/${id}/temp-unschedulable`));
return;
}
if (sub === "crs-preview") {
printJson(await adminRequest("POST", "/admin/accounts/sync/crs/preview", readJsonPayload(args.flags)));
return;
}
if (sub === "crs-sync") {
printJson(await adminRequest("POST", "/admin/accounts/sync/crs", readJsonPayload(args.flags)));
return;
}
if (sub === "import-codex-session") {
printJson(await adminRequest("POST", "/admin/accounts/import/codex-session", readJsonPayload(args.flags)));
return;
}
if (sub === "antigravity-default-model-mapping") {
printJson(await adminRequest("GET", "/admin/accounts/antigravity/default-model-mapping"));
return;
}
if (sub === "import-json") {
const file = args.flags.file;
const templateName = args.flags["template-name"];
const dryRun = Boolean(args.flags["dry-run"]);
const skipNames = new Set(asArray(args.flags["skip-name"]));
if (!file) throw new Error("accounts import-json requires --file");
if (!templateName) throw new Error("accounts import-json requires --template-name");
const raw = JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
const accounts = raw.accounts || [];
const live = await listAccounts({ pageSize: 500 });
const liveItems = live.items || [];
const template = liveItems.find((item) => item.name === templateName);
if (!template) throw new Error(`template account not found in backend: ${templateName}`);
const templateGroupIds = template.group_ids || [];
const templateConcurrency = template.concurrency;
const templatePriority = template.priority;
const templateModelMapping = (template.credentials && template.credentials.model_mapping) || {};
const existingNames = new Set(liveItems.map((item) => item.name));
const planned = accounts.filter((acc) => acc.name !== templateName && !skipNames.has(acc.name));
if (dryRun) {
printJson({
template: {
name: template.name,
concurrency: templateConcurrency,
priority: templatePriority,
group_ids: templateGroupIds,
model_mapping: templateModelMapping,
},
to_import: planned.map((acc) => ({
name: acc.name,
exists: existingNames.has(acc.name),
})),
});
return;
}
const results = [];
for (const acc of planned) {
if (existingNames.has(acc.name)) {
results.push({ name: acc.name, skipped: true, reason: "already exists" });
continue;
}
const payload = {
...acc,
group_ids: templateGroupIds,
concurrency: templateConcurrency,
priority: templatePriority,
credentials: {
...acc.credentials,
model_mapping: templateModelMapping,
},
};
const created = await apiRequest("POST", "/api/v1/admin/accounts", payload);
results.push({ name: acc.name, id: created.id, skipped: false });
}
printJson({ imported: results });
return;
}
throw new Error(`unknown accounts subcommand: ${sub || "(missing)"}`);
}
async function commandGroups(args) {
const sub = args.positional[1];
if (sub === "all") {
printJson(await adminRequest("GET", "/admin/groups/all"));
return;
}
throw new Error(`unknown groups subcommand: ${sub || "(missing)"}`);
}
async function commandProxies(args) {
const sub = args.positional[1];
if (sub === "all") {
printJson(await adminRequest("GET", "/admin/proxies/all"));
return;
}
throw new Error(`unknown proxies subcommand: ${sub || "(missing)"}`);
}
async function commandCrudResource(args, name, basePath, options = {}) {
const sub = args.positional[1];
const id = args.positional[2];
if (sub === "list") {
printJson(await adminRequest("GET", basePath));
return;
}
if (sub === "get") {
if (!id) throw new Error(`${name} get requires <id>`);
printJson(await adminRequest("GET", `${basePath}/${id}`));
return;
}
if (sub === "create") {
printJson(await adminRequest("POST", basePath, readJsonPayload(args.flags)));
return;
}
if (sub === "update") {
if (!id) throw new Error(`${name} update requires <id>`);
printJson(await adminRequest("PUT", `${basePath}/${id}`, readJsonPayload(args.flags)));
return;
}
if (sub === "delete") {
if (!id) throw new Error(`${name} delete requires <id>`);
printJson(await adminRequest("DELETE", `${basePath}/${id}`));
return;
}
if (options.toggle && sub === "toggle") {
const enabled = args.positional[3];
if (!id || enabled === undefined) throw new Error(`${name} toggle requires <id> <true|false>`);
printJson(await adminRequest("PUT", `${basePath}/${id}`, { enabled: parseBool(enabled, "enabled") }));
return;
}
throw new Error(`unknown ${name} subcommand: ${sub || "(missing)"}`);
}
async function commandApi(args) {
const method = args.positional[1];
const pathname = args.positional[2];
if (!method || !pathname) throw new Error("api requires <GET|POST|PUT|DELETE> <admin-path>");
const body = readJsonPayload(args.flags, { required: false });
printJson(await adminRequest(method.toUpperCase(), pathname, body));
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const root = args.positional[0];
if (!root) {
usage();
process.exit(1);
}
if (root === "accounts") {
await commandAccounts(args);
return;
}
if (root === "groups") {
await commandGroups(args);
return;
}
if (root === "proxies") {
await commandProxies(args);
return;
}
if (root === "error-rules") {
await commandCrudResource(args, "error-rules", "/admin/error-passthrough-rules", { toggle: true });
return;
}
if (root === "tls-profiles") {
await commandCrudResource(args, "tls-profiles", "/admin/tls-fingerprint-profiles");
return;
}
if (root === "api") {
await commandApi(args);
return;
}
throw new Error(`unknown command: ${root}`);
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});