最近同时使用Claude Code / Codex / Github Copilot 做开发,Github Copilot 使用了Claude Opus模型消耗3倍流量。 三个工具都是使用了20刀左右每月的基础订阅模式,其中Github Copilot是公司企业模式订阅的,没有网络环境要求。
在使用过程中关于限额消耗速度我的体感是,Codex最省,Github Copilot使用Claude模型模式下消耗稍快,Claude Code消耗也较快,实现较大功能时能用1个多小时就到了5个小时限额(Sonnet4.6)可能和我当前会话用了2天有关系,新会话的时候消耗没那么块, 但是因为Github Copilot限额是按月的,没有5小时和每周的限制,可以一次性使用更多,但是每个月月中差不多就用完了,不过gpt4.1模型不限量还可以继续用。
以上是我使用的体感供大家参考,另外建议使用Claude Code和Github Copilot按需选择模型,不同模型消耗可能区别很大。
一个更直观的关于Claude的使用情况,这个5小时周期内,我要求Claude Code针对我的工程进行一次review,看看是否有安全问题,逻辑漏洞等等,然后基于这个会话进行了一次回复,使用Opus4.6模型(两倍消耗速度),总共消耗5小时内限额的29%。 以下是会话输出内容和我的工程文件列表截图。 其中一个消耗比较高的原因也在于我没还没有执行/init有生成CLAUDE.md文件
bash
▐▛███▜▌ Claude Code v2.1.92
▝▜█████▛▘ Opus 4.6 with medium effort · Claude Pro
▘▘ ▝▝ /mnt/d/RagDocuments
❯ /usage
⎿ Status dialog dismissed
❯ review下devops_cli工程,关于工程信息可以看代码,也可以参考docs下的内容,playgroud下的东西可以不用看,不是核心内容,是用来跑web端模拟的一个外挂。主要看看核心工程有没有漏洞和逻辑上的问题,也可以看看有没有扩展性的问题,这个框架作为基础框架,后续可以通过扩展插件,让这个框架实现例如我的后端开发框架初始化脚手架的程序,也可以是扩展成一个离线安装的运维工具。做这个的出发点
:I decided to develop this framework to address a real-world need in my work. As a service provider, we are responsible for maintaining multiple client environments.
Accessing client environments typically requires connecting through a VPN and a jump server, often implemented as a Privileged Access Management (PAM) system. Managing access across so many servers within each environment is cumbersome and inefficient.
In addition, there is usually a designated server within each VPC that can SSH into other servers. When we are tasked with updating middleware, deploying software, or implementing security measures, we either connect to each server individually or access a DevOps server and execute remote commands from there.
This approach requires maintaining a large number of shell scripts and is very time-consuming. It is especially inefficient for routine tasks such as daily health checks, where we need to collect data from all servers and generate reports.
This framework significantly improves efficiency and saves a considerable amount of time and effort.
● Explore(Explore devops_cli project structure)
⎿ Done (38 tool uses · 106.1k tokens · 1m 41s)
(ctrl+o to expand)
● Let me read the most security-critical files directly to give you concrete findings.
Read 8 files (ctrl+o to expand)
● Now I have a thorough picture of the codebase. Here is the full review.
---
devops_cli Code Review
Overall the architecture is solid and the decisions are well-reasoned. The vault security model, sidecar boundary, and SSH runtime strategy are all sound. The findings below are organized by severity.
---
Security Findings
[HIGH] Plugin main path not sandboxed --- path traversal possible
plugins/loader.js:102
const mainPath = path.resolve(entry.pluginDir, entry.manifest.main);
path.resolve with a user-controlled manifest.main value escapes the plugin directory. A plugin.json containing "main": "../../../etc/passwd" (or any ../../ path) resolves outside pluginDir. While the threat model is local plugins only, a plugin directory accidentally installed from an untrusted source would let it load arbitrary files.
Fix:
const mainPath = path.resolve(entry.pluginDir, entry.manifest.main);
if (!mainPath.startsWith(entry.pluginDir + path.sep)) {
return { ...entry, status: "invalid", errors: ["Plugin main path escapes plugin directory."] };
}
---
[HIGH] setTemplate/resetTemplate on SSH runtime is exposed to plugins
ssh/runtime.js:709--721
Any plugin loaded via createPlugin() that receives the app context can call context.sshRuntime.setTemplate({ executable: '/attacker/fake-ssh', args: [...] }) and redirect every subsequent SSH operation through an arbitrary binary --- including operations that forward decrypted vault credentials.
This is the most serious escalation path in the extensibility model. The plugin itself may be benign code that exposes this through an unguarded API call.
Fix: Remove setTemplate/resetTemplate from the public context object exposed to plugins. Keep them as internal-only functions or gate them behind an explicit user confirmation flow.
---
[MEDIUM] Unquoted jumpServer.host in Windows plink proxy command
ssh/runtime.js:527--529
args.push(jumpServer.host, "-nc", "%host:%port");
return args.join(" ");
jumpServer.host is pushed directly without windowsQuote(). The result is passed as a shell-interpolated string. Hostnames with embedded spaces or special characters (edge case, but valid in some DNS configurations) would break the proxy command string. Consistent with how other args are built, windowsQuote should be applied.
Similarly jumpServer.username on line 512 is unquoted.
---
[MEDIUM] Temp key files survive process kill
ssh/runtime.js:57--74, crypto.js:64--84
Both createTempIdentityFile and deriveOpenSshPublicKey write private key material to os.tmpdir() and rely on finally blocks for cleanup. If the process receives SIGKILL (not SIGTERM), the finally block never runs and the key file persists in the system temp directory.
Fix: Register a cleanup set at startup and use process.on('exit', ...) + process.on('SIGTERM', ...). Or write temp key files into a mkdtemp-created directory under a controlled path with an O_TMPFILE-style approach where available.
---
[LOW] Custom fingerprint format is incompatible with OpenSSH
crypto.js:312--314
const fingerprint = createHash("sha256")
.update(publicKeyPem, "utf8")
.digest("base64");
return `SHA256:${fingerprint}`;
This hashes the PEM string (ASCII-encoded), not the raw key bytes. OpenSSH computes SHA256 over the wire-encoded public key bytes (RFC 4253 format). The stored fingerprint will never match what ssh-keygen -lf displays. Operators who manually cross-check fingerprints will be confused and may make wrong trust decisions.
Fix: Hash the DER-encoded public key bytes instead:
const der = publicKey.export({ type: "spki", format: "der" });
const fingerprint = createHash("sha256").update(der).digest("base64");
---
[INFO] String zeroization is ineffective in JS
session.js:192--194
if (typeof unlockedPrivateKey === "string") {
unlockedPrivateKey = "";
}
JS strings are immutable; setting the variable to "" does not overwrite the original string in memory. The ADRs acknowledge this limitation, but this is called out here because unlockProtectedPrivateKey returns a string specifically --- it should return a Buffer so the caller can call .fill(0) on it.
---
Logic Bugs
[HIGH] promptText never prompts when a fallback is given
resourceHelpers.js:15--21, called from run.js:51--52
export async function promptText(context, label, fallback, title) {
if (fallback !== undefined && fallback !== null && fallback !== "") {
return fallback; // ← always hits this for non-empty fallback
}
return context.prompt(label, title);
}
In buildExecutionSpec (run.js:51--52):
const label = await promptText(context, "Temporary script name", "adhoc-script", "/run");
const shell = await promptText(context, "Shell", "sh", "/run");
Both calls pass non-empty hardcoded strings as fallback, so the user is never prompted --- the ad-hoc script always gets named "adhoc-script" and always uses shell "sh". This makes the ad-hoc form partially non-functional.
Fix: Pass null as fallback and use the default as a hint in the label, or change promptText to pass the fallback as the default value shown to the user and always prompt:
return context.prompt(label, title, { default: fallback });
---
[MEDIUM] shell value is injected into remote command string without quoting
ssh/runtime.js:101--107
function buildRemoteCommand(script, runAs, shell = "sh") {
const base = `${shell} -lc ${shellQuote(script)}`;
...
}
shell is interpolated verbatim. Once the promptText bug is fixed and users can actually enter shell values, a value like sh; malicious_command # would be injected into the remote command string. Since this string is eventually passed as the argument to ssh, the remote server would execute the injected command.
Fix: Either validate shell against an allowlist (["sh", "bash", "zsh", "ksh"]) or shell-quote it:
const base = `${shellQuote(shell)} -lc ${shellQuote(script)}`;
---
[MEDIUM] SSH template setting silently removed on every startup without notice
ssh/runtime.js:688--694
if (settings?.remove) {
try {
settings.remove(SSH_TEMPLATE_SETTING_KEY);
} catch (_error) { }
}
If a user had a custom SSH template saved in settings (from an older CLI version that supported it), it is silently wiped on every startup without any migration notice. The user cannot tell why their custom template stopped working.
Fix: Check if the setting exists first and emit a one-time migration warning to context.output before removing it.
---
[LOW] Binding mismatch warning in /run batch is shown but does not abort
run.js:273--282
const bindingRows = await collectBindingMismatches(context, selectedServers);
if (bindingRows.length > 0) {
● Now I have a thorough picture of the codebase. Here is the full review.
---
Code Review: devops_cli
Architecture Assessment
The overall design is solid. The vault sidecar boundary (ciphertext isolation), scrypt-based KEK derivation, atomic file writes, and fixed SSH backend strategy are all good engineering decisions. The plugin system and output block protocol are clean extension points. The main concerns below are specific bugs and hardening gaps, not architectural problems.
---
Security Issues
1. unlockVaultIfNeeded doesn't catch vault errors --- Logic Bug / UX Crash
File: app/src/commands/resourceHelpers.js:40
await context.vaultSession.unlock(passcode);
return true;
If unlock() throws (wrong passcode, backoff locked), the error propagates unhandled up through the command layer. The function signature promises true/false but can throw instead. Any command calling this will surface a raw stack trace instead of a friendly error message.
Fix:
try {
await context.vaultSession.unlock(passcode);
return true;
} catch (error) {
context.output.push("error", `Vault unlock failed: ${error.message}`);
return false;
}
---
2. Double scrypt on every unlock --- Performance / Unnecessary Exposure
File: app/src/vault/session.js:180-181
unlockedPrivateKey = unlockProtectedPrivateKey(authRecord, passcode); // scrypt #1
unwrappedMasterKey = unwrapMasterEnvelope(description.masterEnvelope, passcode); // scrypt #2
unlockedPrivateKey is immediately zeroized in the finally block and never used. The auth private key is not yet in the critical path. This means every unlock runs scrypt twice (N=131072 twice ≈ 2--4 seconds on typical hardware) for no benefit.
Fix: Remove the unlockProtectedPrivateKey call from the unlock flow. Re-add it when the auth key is actually needed (future signing/binding use cases).
---
3. Plugin main path is not bounds-checked --- Path Traversal
File: app/src/plugins/loader.js:102
const mainPath = path.resolve(entry.pluginDir, entry.manifest.main);
if (!fs.existsSync(mainPath)) { ... }
const mod = await import(pathToFileURL(mainPath).href);
A plugin can set "main": "../../app/src/vault/sidecar.js" and load arbitrary files outside its own directory. Plugins are currently local/trusted, but as the ecosystem grows (or if plugins are distributed), this is a low-cost hardening step.
Fix:
const mainPath = path.resolve(entry.pluginDir, entry.manifest.main);
const pluginDirNorm = entry.pluginDir.endsWith(path.sep)
? entry.pluginDir : entry.pluginDir + path.sep;
if (!mainPath.startsWith(pluginDirNorm)) {
// reject: path escapes plugin directory
}
---
4. Host key fingerprint is not verified on Unix, only detected after-the-fact
File: app/src/ssh/runtime.js:426-467
On Unix/WSL, the SSH command uses StrictHostKeyChecking=accept-new. The host_key_fingerprint stored per server is never passed to ssh as a constraint --- it's only used post-execution to parse stderr and detect a changed key. This means:
- First connection: silently auto-accepts any key
- Subsequent connections: key changes raise an error, but only after the connection attempt
On Windows (plink), -hostkey is correctly passed and verified before connecting. The Unix path has weaker guarantees.
Fix (Unix): Pass -o FingerprintHash=sha256 and use ControlMaster/known_hosts with the pinned fingerprint. Alternatively, use the VerifyHostKeyDNS approach, or at minimum, scan with ssh-keyscan first and compare before executing.
---
5. UNIENTS_SSH_SUDO_SECRET leaked into child process env --- Unnecessary Exposure
File: app/src/ssh/runtime.js:772-776
const env = {
...process.env,
UNIENTS_SSH_AUTH_SECRET: authSecret || "",
UNIENTS_SSH_JUMP_AUTH_SECRET: jumpAuthSecret || "",
UNIENTS_SSH_SUDO_SECRET: sudoPassword || "" // ← set but never consumed
};
UNIENTS_SSH_AUTH_SECRET is consumed by the sshpass -e wrapper. UNIENTS_SSH_SUDO_SECRET is set in the local spawn env but nothing reads it --- the sudo password is correctly passed via stdinData. This unnecessarily exposes the sudo password to the local SSH subprocess environment. Any child process of the SSH client can read it.
Fix: Remove UNIENTS_SSH_SUDO_SECRET from the env object.
---
6. Non-standard fingerprint format in inspectPrivateKey --- Interoperability Bug
File: app/src/vault/crypto.js:312-314
const fingerprint = createHash("sha256")
.update(publicKeyPem, "utf8") // ← SHA256 of the PEM string
.digest("base64");
return { ..., fingerprint: `SHA256:${fingerprint}` };
The SSH standard fingerprint is SHA256(base64_decode(wire_format_public_key)). This code hashes the PEM-formatted string (which includes headers and is base64 of DER, not the raw wire key). The resulting fingerprint will never match what ssh-keygen -l reports.
This means the auth_material_fingerprint binding check in server records will always show mismatches if users try to cross-reference with external tools. Worse, two keys that produce the same fingerprint by this non-standard method would be considered equal.
Fix: Use publicKeyOpenSsh output from ssh-keygen -l for fingerprinting, or compute it using the proper wire format:
// Use ssh-keygen -l -f - for the stored OpenSSH pubkey to get the canonical fingerprint
---
Logic Bugs
7. promptText always returns the default without prompting --- UX Bug
File: app/src/commands/resourceHelpers.js:15-21 and app/src/commands/run.js:51-57
export async function promptText(context, label, fallback, title) {
if (fallback !== undefined && fallback !== null && fallback !== "") {
return fallback; // ← returns immediately, never prompts
}
return context.prompt(label, title);
}
In run.js:
const label = await promptText(context, "Temporary script name", "adhoc-script", "/run");
const shell = await promptText(context, "Shell", "sh", "/run");
const timeoutInput = await promptText(context, "Timeout seconds", "60", "/run");
All three calls pass non-empty fallbacks, so the user is never prompted for script name, shell, or timeout. The defaults are always used silently. The user can only provide the script content.
Fix: The intent of promptText appears to be "prompt the user, pre-filling a default". The implementation should call context.prompt with the default as a hint, not skip the prompt entirely. Either fix the helper or call context.prompt directly.
---
8. Credentials materialized before user confirms execution
File: app/src/commands/run.js:284-307
const credentialState = await materializeCredentials(context, selectedServers, spec);
// ... show failure table ...
const confirmed = await context.confirm(`Start batch execution for ${N} server(s)?`, "/run");
All credentials (SSH keys, passwords) are decrypted from the vault and held in plaintext strings before the user confirms the run. If the user cancels, the credentials are still in memory until GC. These should be materialized after confirmation.
---
9. Batch run continues with partial credential failures, no re-confirmation
File: app/src/commands/run.js:289-300
if (failures.size > 0) {
context.output.pushRich({ kind: "table", type: "error", ... });
}
if (credentials.size === 0) {
context.output.push("error", "Unable to read credentials for any selected server.");
return;
}
// Falls through to confirmation with partial server set
If 5 out of 10 servers fail credential materialization, the code shows the failure table then proceeds to the confirmation. The confirm dialog says "Start batch execution for 10 server(s)?" but only 5 will actually run. The user may not notice the discrepancy.
Fix: Either abort if any credential fails, or adjust the confirmation message to say "X of Y servers ready, N will be skipped."
---
10. Pre-flight credential ref check doesn't verify vault existence
File: app/src/commands/run.js:261-271 and app/src/commands/resourceHelpers.js (implied)
buildMissingCredentialRows only checks that auth_secret_ref is non-null. It doesn't verify the referenced name actually exists in the vault. The real check happens during materializeCredentials. So the pre-flight check passes but execution still fails --- just later with a less clear error.
---
Extensibility Gaps
11. No plugin API versioning
createPlugin() currently takes no arguments. When the framework adds capabilities (e.g., new context methods, new output block types), plugins have no way to declare which API version they require. A v2 plugin loaded in a v1 CLI (or vice versa) fails silently with a runtime error, not a clear version mismatch message.
Suggestion: Pass a { apiVersion, context } object to createPlugin(), and add a minApiVersion/maxApiVersion field to plugin.json. The loader can reject version-incompatible plugins with a clear message.
---
12. No structured error taxonomy
All errors are new Error("human message"). Commands, plugins, and the SSH runtime use different error detection strategies (string matching on result.errorType, try/catch of generic Error). As the plugin ecosystem grows, plugins need a reliable way to handle "vault locked" vs "SSH timeout" vs "server not found" without parsing error message strings.
Suggestion: Define an error class hierarchy or at minimum a set of error code constants:
export const ErrorCode = {
VAULT_LOCKED: "VAULT_LOCKED",
VAULT_NOT_INITIALIZED: "VAULT_NOT_INITIALIZED",
SSH_TIMEOUT: "SSH_TIMEOUT",
SSH_HOST_KEY_CHANGED: "SSH_HOST_KEY_CHANGED",
CREDENTIAL_NOT_FOUND: "CREDENTIAL_NOT_FOUND"
};
---
13. Command dispatch is entirely flat
Each command handles its own subcommand parsing (if (first === "batch")). There's no standard routing tree, no shared "unknown subcommand" error message, and tab completion has to be implemented per-command. As the number of commands grows (especially with plugins), this becomes inconsistent.
Suggestion: A simple command router factory would help:
createCommand("/run", {
"batch": runBatch,
"template": runTemplate
});
This also makes suggest() implementable generically from the subcommand keys.
---
Minor / Code Quality
14. Duplicate tightenPrivateKeyFilePermissions / tightenIdentityFilePermissions
crypto.js and ssh/runtime.js both contain near-identical icacls permission-tightening logic. Extract to utils/fileSecurity.js.
15. isUnlocked() and remainingMs() have an inconsistency
isUnlocked() calls zeroize() as a side effect when the session has expired. remainingMs() calls isUnlocked() for its check. This means remainingMs() has a side-effectful path that's not obvious from the name.
16. buildMissingCredentialRows and collectBindingMismatches both call resolveManagedJumpServer per server
During runBatch, jump server resolution is called 3 separate times per server (in pre-flight, binding check, and during execution). These could be cached per-server for the duration of the run.
17. Vault sidecar vault.reset deletes the store without requiring confirmation
File: app/src/vault/sidecar.js:68-72
The sidecar's vault.reset handler unconditionally deletes store.json. If the CLI-side code calls this in a failed init rollback (session.js:151), that's correct. But it's also callable directly --- there's no guard that prevents a mistaken reset from destroying all vault data.
---
Summary Table
┌─────┬─────────────────────────────────────────────────────┬───────────────┬────────────────────────┐
│ # │ Issue │ Severity │ File │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 1 │ unlockVaultIfNeeded unhandled throw │ High │ resourceHelpers.js:40 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 2 │ Double scrypt in unlock() │ Medium │ session.js:180 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 3 │ Plugin main path traversal │ Medium │ loader.js:102 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 4 │ Host key not verified on Unix │ Medium │ runtime.js:438 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 5 │ UNIENTS_SSH_SUDO_SECRET leaked to child env │ Medium │ runtime.js:775 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 6 │ Non-standard fingerprint in inspectPrivateKey │ Medium │ crypto.js:312 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 7 │ promptText never prompts when default given │ Medium │ resourceHelpers.js:15 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 8 │ Credentials materialized before confirmation │ Low │ run.js:284 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 9 │ Batch continues with partial failures silently │ Low │ run.js:289 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 10 │ Pre-flight ref check doesn't verify vault existence │ Low │ run.js:261 │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 11 │ No plugin API versioning │ Extensibility │ loader.js │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 12 │ No structured error taxonomy │ Extensibility │ all │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 13 │ Flat command dispatch │ Extensibility │ commandDispatcher │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 14 │ Duplicate permission-tightening code │ Quality │ crypto.js / runtime.js │
├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤
│ 15 │ isUnlocked() has zeroize side effect │ Quality │ session.js │
└─────┴─────────────────────────────────────────────────────┴───────────────┴────────────────────────┘
The most important fixes to do first are #1 (crash on wrong passcode), #6 (fingerprint format --- affects binding integrity), #7 (users can't set shell/timeout for ad-hoc runs), and #3 (plugin path bounds).
✻ Cogitated for 7m 38s
❯ /model
⎿ Set model to Opus 4.6
❯ 2. Double scrypt on every unlock --- Performance / Unnecessary Exposure
File: app/src/vault/session.js:180-181
unlockedPrivateKey = unlockProtectedPrivateKey(authRecord, passcode); // scrypt #1
unwrappedMasterKey = unwrapMasterEnvelope(description.masterEnvelope, passcode); // scrypt #2
unlockedPrivateKey is immediately zeroized in the finally block and never used. The auth private key is not yet in the critical path. This means every unlock runs scrypt twice (N=131072 twice ≈ 2--4 seconds on typical hardware) for no benefit.
Fix: Remove the unlockProtectedPrivateKey call from the unlock flow. Re-add it when the auth key is actually needed (future signing/binding use cases). About this, the first one's purpose is to valid the passcode is correct.
● Good point, I understand the intent now. The unlockProtectedPrivateKey call serves as an independent passcode verification against a separate encrypted artifact (the auth key, stored in a different file from the master envelope). If one file is tampered with or corrupted but the other isn't, the mismatch surfaces early.
That said, unwrapMasterEnvelope alone already validates the passcode --- AES-256-GCM's auth tag will reject a wrong key. So the two-check design provides cross-file tamper detection, not just passcode validation. If that's the goal, it's worth keeping but should be documented explicitly in a comment, because the cost is real: 2x scrypt at N=131072 on every unlock.
A couple of alternatives worth considering if the 2x cost becomes a concern:
1. Cheaper cross-check: Store a passcode verification hash (e.g., HMAC of a known constant using the derived KEK) alongside the auth key file. Check that first (fast), then do the single scrypt for the master envelope. Same tamper detection, one scrypt instead of two.
2. Defer to when auth key is actually needed: When the auth key enters the critical path (future signing/binding), the double scrypt becomes justified because both decrypted values are used. Until then, the cross-validation benefit is modest given that ADR-0001 already acknowledges file replacement attacks aren't fully mitigated.
Either way, the intent should be captured in a comment at that call site --- it's non-obvious that the first decryption is a deliberate integrity check rather than dead code.
✻ Worked for 31s
❯ 2x cost isn't a matter ,since only critical path needs verifing the passcode, and the state will be cached for 30 minites default.
● Makes sense. The unlock is a rare operation (once per 30-minute session), so 2x scrypt cost is negligible in practice. I'll downgrade that finding --- it's working as intended.
❯ /usage
⎿ Status dialog dismissed
❯ /usage
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Status Config Usage Stats
Current session
██████████████▌ 29% used
Resets 4pm (Asia/Shanghai)
Current week (all models)
███████████████████████████████████▌ 71% used
Resets Apr 10, 6pm (Asia/Shanghai)
Extra usage
Extra usage not enabled · /extra-usage to enable
Esc to cancel
