@praeviso/code-env-switch 0.1.5 → 0.1.7
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.md +12 -0
- package/README_zh.md +12 -0
- package/bin/cli/args.js +13 -0
- package/bin/cli/help.js +5 -0
- package/bin/cli/index.js +2 -1
- package/bin/commands/index.js +3 -1
- package/bin/commands/usage.js +41 -0
- package/bin/index.js +7 -0
- package/bin/statusline/debug.js +1 -0
- package/bin/statusline/usage/codex.js +29 -20
- package/bin/usage/index.js +280 -22
- package/docs/README.md +3 -0
- package/docs/README_zh.md +3 -0
- package/docs/usage.md +126 -0
- package/docs/usage_zh.md +126 -0
- package/package.json +1 -1
- package/src/cli/args.ts +14 -0
- package/src/cli/help.ts +5 -0
- package/src/cli/index.ts +7 -1
- package/src/commands/index.ts +1 -0
- package/src/commands/usage.ts +53 -0
- package/src/index.ts +11 -0
- package/src/statusline/debug.ts +1 -1
- package/src/statusline/usage/codex.ts +26 -31
- package/src/types.ts +4 -0
- package/src/usage/index.ts +293 -25
package/docs/usage_zh.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 用量统计逻辑
|
|
2
|
+
|
|
3
|
+
用量来自两条路径(状态栏输入同步 + 会话日志解析),最终追加到 `usage.jsonl`。
|
|
4
|
+
|
|
5
|
+
## 文件与路径
|
|
6
|
+
|
|
7
|
+
- `usage.jsonl`:JSONL 记录,包含 `ts`/`type`/`profileKey`/`profileName`/`model`/`sessionId` 和 token 拆分字段。
|
|
8
|
+
- `usage.jsonl.state.json`:保存每个 session 的累计 totals + session 文件的 mtime/size,用于计算增量并避免重复统计。
|
|
9
|
+
- `profile-log.jsonl`:profile 使用与 session 绑定日志(`use`/`session`)。
|
|
10
|
+
- `statusline-debug.jsonl`:当 `CODE_ENV_STATUSLINE_DEBUG` 开启时写入的调试信息。
|
|
11
|
+
- 可通过 `usagePath`/`usageStatePath`/`profileLogPath`/`codexSessionsPath`/`claudeSessionsPath` 覆盖默认路径。
|
|
12
|
+
|
|
13
|
+
## 会话绑定(profile -> session)
|
|
14
|
+
|
|
15
|
+
- `codenv init` 安装 shell 包装函数,使 `codex`/`claude` 实际走 `codenv launch`。
|
|
16
|
+
- `codenv launch` 记录 profile 使用,并在启动后短时间内(默认约 5 秒、每秒轮询)找到最新未绑定的 session 文件(优先 `cwd` 匹配),写入 `profile-log.jsonl`。
|
|
17
|
+
- 后续同步会用该绑定将 session 归因到对应 profile。
|
|
18
|
+
|
|
19
|
+
## 状态栏同步(`codenv statusline --sync-usage`)
|
|
20
|
+
|
|
21
|
+
- 需要 `sessionId`、model,以及 profile(`profileKey` 或 `profileName`)才会写入用量。
|
|
22
|
+
- 从 stdin JSON 读取 totals,与 state 中的上次 totals 做差得到增量。
|
|
23
|
+
- 如果 totals 回退(session reset),直接把当前 totals 当作新增;否则对负数子项做 0 处理。
|
|
24
|
+
- 当拆分合计大于 total 时,以拆分合计为准。
|
|
25
|
+
- 把增量写入 `usage.jsonl`,并更新 state 中的 session totals。
|
|
26
|
+
|
|
27
|
+
## 会话日志同步(`--sync-usage` 与 `codenv list`)
|
|
28
|
+
|
|
29
|
+
- 扫描 Codex 的 `CODEX_HOME/sessions`(或 `~/.codex/sessions`)与 Claude 的 `CLAUDE_HOME/projects`(或 `~/.claude/projects`)。
|
|
30
|
+
- Codex:读取 `event_msg` 的 token_count 记录,取累计最大值;cached input 计为 cache read,并在可判断时从 input 中扣除。
|
|
31
|
+
- Claude:汇总 `message.usage` 中的 input/output/cache tokens。
|
|
32
|
+
- 与 state 中的文件元数据和 session 最大值做差,生成增量并写入 `usage.jsonl`;无法解析到绑定的文件会被跳过。
|
|
33
|
+
|
|
34
|
+
## 今日统计
|
|
35
|
+
|
|
36
|
+
- “今日”按本地时区 00:00 到次日 00:00 计算。
|
|
37
|
+
|
|
38
|
+
## 费用换算
|
|
39
|
+
|
|
40
|
+
- 使用 profile 定价或 `pricing.models`(含默认值)换算,依赖 input/output/cache 拆分;缺少拆分则不显示金额。
|
|
41
|
+
|
|
42
|
+
## 示例
|
|
43
|
+
|
|
44
|
+
### `usage.jsonl` 记录
|
|
45
|
+
|
|
46
|
+
每行一条 JSON(字段可能为空或缺省,取决于来源):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{"ts":"2026-01-25T12:34:56.789Z","type":"codex","profileKey":"p_a1b2c3","profileName":"primary","model":"gpt-5.1-codex","sessionId":"a6f9c4d8-1234-5678-9abc-def012345678","inputTokens":1200,"outputTokens":300,"cacheReadTokens":200,"cacheWriteTokens":0,"totalTokens":1700}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`totalTokens` 会取“上报 total”和“拆分合计”的较大值,因此可能 >= input + output + cache。
|
|
53
|
+
|
|
54
|
+
## 状态栏输入示例(Codex/Claude)
|
|
55
|
+
|
|
56
|
+
可通过设置 `CODE_ENV_STATUSLINE_DEBUG=1`,在 `statusline-debug.jsonl`
|
|
57
|
+
(或 `CODE_ENV_STATUSLINE_DEBUG_PATH` 指定路径)看到实际 JSON。
|
|
58
|
+
|
|
59
|
+
### Codex(token_usage totals)
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"type": "codex",
|
|
64
|
+
"session_id": "a6f9c4d8-1234-5678-9abc-def012345678",
|
|
65
|
+
"profile": { "key": "p_a1b2c3", "name": "primary", "type": "codex" },
|
|
66
|
+
"model": "gpt-5.1-codex",
|
|
67
|
+
"token_usage": {
|
|
68
|
+
"total_token_usage": {
|
|
69
|
+
"input_tokens": 1200,
|
|
70
|
+
"output_tokens": 300,
|
|
71
|
+
"cached_input_tokens": 200,
|
|
72
|
+
"cache_creation_input_tokens": 50,
|
|
73
|
+
"total_tokens": 1750
|
|
74
|
+
},
|
|
75
|
+
"last_token_usage": {
|
|
76
|
+
"input_tokens": 100,
|
|
77
|
+
"output_tokens": 20,
|
|
78
|
+
"cached_input_tokens": 10,
|
|
79
|
+
"cache_creation_input_tokens": 0,
|
|
80
|
+
"total_tokens": 130
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
可识别字段(节选):
|
|
87
|
+
- `token_usage.total_token_usage` 或 `token_usage.totalTokenUsage`
|
|
88
|
+
- `last_token_usage` 或 `lastTokenUsage`
|
|
89
|
+
- `input_tokens` / `inputTokens` / `input`
|
|
90
|
+
- `output_tokens` / `outputTokens` / `output` / `reasoning_output_tokens`
|
|
91
|
+
- `cached_input_tokens` / `cache_read_input_tokens`
|
|
92
|
+
- `cache_creation_input_tokens` / `cache_write_input_tokens`
|
|
93
|
+
- `total_tokens` / `totalTokens` / `total`
|
|
94
|
+
|
|
95
|
+
`token_usage` 也可以是数字,或直接提供 `usage`。
|
|
96
|
+
|
|
97
|
+
### Claude(context_window totals)
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"type": "claude",
|
|
102
|
+
"session_id": "1f2e3d4c-5678-90ab-cdef-1234567890ab",
|
|
103
|
+
"profile": { "key": "p_c3d4e5", "name": "default", "type": "claude" },
|
|
104
|
+
"model": { "display_name": "Claude Sonnet 4.5" },
|
|
105
|
+
"context_window": {
|
|
106
|
+
"total_input_tokens": 800,
|
|
107
|
+
"total_output_tokens": 250,
|
|
108
|
+
"current_usage": {
|
|
109
|
+
"cache_read_input_tokens": 100,
|
|
110
|
+
"cache_creation_input_tokens": 40
|
|
111
|
+
},
|
|
112
|
+
"context_window_size": 200000
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
可识别字段(节选):
|
|
118
|
+
- `context_window` 或 `contextWindow`
|
|
119
|
+
- `current_usage` 或 `currentUsage`
|
|
120
|
+
- `total_input_tokens` / `totalInputTokens`
|
|
121
|
+
- `total_output_tokens` / `totalOutputTokens`
|
|
122
|
+
- `cache_read_input_tokens` / `cacheReadInputTokens`
|
|
123
|
+
- `cache_creation_input_tokens` / `cacheWriteInputTokens`
|
|
124
|
+
|
|
125
|
+
也可直接提供 `usage`,字段包括 `todayTokens` / `totalTokens` / `inputTokens` /
|
|
126
|
+
`outputTokens` / `cacheReadTokens` / `cacheWriteTokens`。
|
package/package.json
CHANGED
package/src/cli/args.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ParsedArgs,
|
|
6
6
|
InitArgs,
|
|
7
7
|
AddArgs,
|
|
8
|
+
UsageResetArgs,
|
|
8
9
|
ProfileType,
|
|
9
10
|
StatuslineArgs,
|
|
10
11
|
} from "../types";
|
|
@@ -156,6 +157,19 @@ export function parseAddArgs(args: string[]): AddArgs {
|
|
|
156
157
|
return result;
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
export function parseUsageResetArgs(args: string[]): UsageResetArgs {
|
|
161
|
+
const result: UsageResetArgs = { yes: false };
|
|
162
|
+
for (let i = 0; i < args.length; i++) {
|
|
163
|
+
const arg = args[i];
|
|
164
|
+
if (arg === "-y" || arg === "--yes") {
|
|
165
|
+
result.yes = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Unknown usage-reset argument: ${arg}`);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
function parseNumberFlag(value: string | null | undefined, flag: string): number {
|
|
160
174
|
if (value === null || value === undefined || value === "") {
|
|
161
175
|
throw new Error(`Missing value for ${flag}.`);
|
package/src/cli/help.ts
CHANGED
|
@@ -27,6 +27,7 @@ Usage:
|
|
|
27
27
|
codenv launch <codex|claude> [--] [args...]
|
|
28
28
|
codenv init
|
|
29
29
|
codenv statusline [options]
|
|
30
|
+
codenv usage-reset [--yes]
|
|
30
31
|
|
|
31
32
|
Options:
|
|
32
33
|
-c, --config <path> Path to config JSON
|
|
@@ -57,6 +58,9 @@ Statusline options:
|
|
|
57
58
|
--usage-output <n> Set output token usage
|
|
58
59
|
--sync-usage Sync usage from sessions before reading
|
|
59
60
|
|
|
61
|
+
Usage reset options:
|
|
62
|
+
-y, --yes Skip confirmation prompt
|
|
63
|
+
|
|
60
64
|
Examples:
|
|
61
65
|
codenv init
|
|
62
66
|
codenv use codex primary
|
|
@@ -67,6 +71,7 @@ Examples:
|
|
|
67
71
|
codenv remove --all
|
|
68
72
|
codenv launch codex -- --help
|
|
69
73
|
codenv statusline --format json
|
|
74
|
+
codenv usage-reset --yes
|
|
70
75
|
CODE_ENV_CONFIG=~/.config/code-env/config.json codenv use claude default
|
|
71
76
|
codenv add --type codex primary OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_API_KEY=YOUR_API_KEY
|
|
72
77
|
codenv add
|
package/src/cli/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI module exports
|
|
3
3
|
*/
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
parseArgs,
|
|
6
|
+
parseInitArgs,
|
|
7
|
+
parseAddArgs,
|
|
8
|
+
parseUsageResetArgs,
|
|
9
|
+
parseStatuslineArgs,
|
|
10
|
+
} from "./args";
|
|
5
11
|
export { printHelp } from "./help";
|
package/src/commands/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage history reset command
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, UsageResetArgs } from "../types";
|
|
5
|
+
import { clearUsageHistory } from "../usage";
|
|
6
|
+
import { askConfirm, createReadline } from "../ui";
|
|
7
|
+
|
|
8
|
+
export async function runUsageReset(
|
|
9
|
+
config: Config,
|
|
10
|
+
configPath: string | null,
|
|
11
|
+
args: UsageResetArgs
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (!args.yes) {
|
|
14
|
+
const rl = createReadline();
|
|
15
|
+
try {
|
|
16
|
+
const confirmed = await askConfirm(
|
|
17
|
+
rl,
|
|
18
|
+
"Clear all usage history files? This cannot be undone. (y/N): "
|
|
19
|
+
);
|
|
20
|
+
if (!confirmed) return;
|
|
21
|
+
} finally {
|
|
22
|
+
rl.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = clearUsageHistory(config, configPath);
|
|
27
|
+
const removed = result.removed.sort();
|
|
28
|
+
const missing = result.missing.sort();
|
|
29
|
+
const failed = result.failed.sort((a, b) => a.path.localeCompare(b.path));
|
|
30
|
+
|
|
31
|
+
if (removed.length === 0 && failed.length === 0) {
|
|
32
|
+
console.log("No usage files found.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (removed.length > 0) {
|
|
37
|
+
console.log(`Removed ${removed.length} file(s):`);
|
|
38
|
+
for (const filePath of removed) {
|
|
39
|
+
console.log(`- ${filePath}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
console.log(`Skipped ${missing.length} missing file(s).`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (failed.length > 0) {
|
|
48
|
+
for (const failure of failed) {
|
|
49
|
+
console.error(`Failed to remove ${failure.path}: ${failure.error}`);
|
|
50
|
+
}
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseArgs,
|
|
11
11
|
parseInitArgs,
|
|
12
12
|
parseAddArgs,
|
|
13
|
+
parseUsageResetArgs,
|
|
13
14
|
parseStatuslineArgs,
|
|
14
15
|
printHelp,
|
|
15
16
|
} from "./cli";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
printUnset,
|
|
34
35
|
runLaunch,
|
|
35
36
|
printStatusline,
|
|
37
|
+
runUsageReset,
|
|
36
38
|
} from "./commands";
|
|
37
39
|
import { logProfileUse } from "./usage";
|
|
38
40
|
import { createReadline, askConfirm, runInteractiveAdd, runInteractiveUse } from "./ui";
|
|
@@ -152,6 +154,15 @@ async function main() {
|
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
if (cmd === "usage-reset" || cmd === "reset-usage") {
|
|
158
|
+
const resetArgs = parseUsageResetArgs(args.slice(1));
|
|
159
|
+
const configPath =
|
|
160
|
+
process.env.CODE_ENV_CONFIG_PATH || findConfigPath(parsed.configPath);
|
|
161
|
+
const config = readConfigIfExists(configPath);
|
|
162
|
+
await runUsageReset(config, configPath, resetArgs);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
155
166
|
const configPath = findConfigPath(parsed.configPath);
|
|
156
167
|
const config = readConfig(configPath!);
|
|
157
168
|
|
package/src/statusline/debug.ts
CHANGED
|
@@ -16,7 +16,7 @@ function resolveDefaultConfigDir(configPath: string | null): string {
|
|
|
16
16
|
return path.join(os.homedir(), ".config", "code-env");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function getStatuslineDebugPath(configPath: string | null): string {
|
|
19
|
+
export function getStatuslineDebugPath(configPath: string | null): string {
|
|
20
20
|
const envPath = resolvePath(process.env.CODE_ENV_STATUSLINE_DEBUG_PATH);
|
|
21
21
|
if (envPath) return envPath;
|
|
22
22
|
return path.join(resolveDefaultConfigDir(configPath), "statusline-debug.jsonl");
|
|
@@ -18,21 +18,20 @@ function resolveOutputTokens(record: Record<string, unknown>): number | null {
|
|
|
18
18
|
record.reasoningOutputTokens,
|
|
19
19
|
record.reasoning_output
|
|
20
20
|
) ?? null;
|
|
21
|
-
if (outputTokens
|
|
22
|
-
if (reasoningTokens
|
|
23
|
-
return
|
|
21
|
+
if (outputTokens !== null) return outputTokens;
|
|
22
|
+
if (reasoningTokens !== null) return reasoningTokens;
|
|
23
|
+
return null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function
|
|
26
|
+
function splitInputTokens(
|
|
27
27
|
record: Record<string, unknown>
|
|
28
|
-
):
|
|
29
|
-
const
|
|
28
|
+
): { inputTokens: number | null; cacheReadTokens: number | null } {
|
|
29
|
+
const rawInput =
|
|
30
30
|
firstNumber(
|
|
31
31
|
record.inputTokens,
|
|
32
32
|
record.input,
|
|
33
33
|
record.input_tokens
|
|
34
34
|
) ?? null;
|
|
35
|
-
const outputTokens = resolveOutputTokens(record);
|
|
36
35
|
const cacheRead =
|
|
37
36
|
firstNumber(
|
|
38
37
|
record.cached_input_tokens,
|
|
@@ -42,6 +41,23 @@ function parseCodexUsageTotalsRecord(
|
|
|
42
41
|
record.cache_read,
|
|
43
42
|
record.cacheRead
|
|
44
43
|
) ?? null;
|
|
44
|
+
if (rawInput === null) {
|
|
45
|
+
return { inputTokens: null, cacheReadTokens: cacheRead };
|
|
46
|
+
}
|
|
47
|
+
if (cacheRead === null) {
|
|
48
|
+
return { inputTokens: rawInput, cacheReadTokens: null };
|
|
49
|
+
}
|
|
50
|
+
const nonCachedInput = Math.max(0, rawInput - cacheRead);
|
|
51
|
+
return { inputTokens: nonCachedInput, cacheReadTokens: cacheRead };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseCodexUsageTotalsRecord(
|
|
55
|
+
record: Record<string, unknown>
|
|
56
|
+
): StatuslineUsageTotals | null {
|
|
57
|
+
const split = splitInputTokens(record);
|
|
58
|
+
const inputTokens = split.inputTokens;
|
|
59
|
+
const outputTokens = resolveOutputTokens(record);
|
|
60
|
+
const cacheRead = split.cacheReadTokens;
|
|
45
61
|
const cacheWrite =
|
|
46
62
|
firstNumber(
|
|
47
63
|
record.cache_creation_input_tokens,
|
|
@@ -106,22 +122,10 @@ function parseCodexInputUsageRecord(
|
|
|
106
122
|
record.total,
|
|
107
123
|
record.total_tokens
|
|
108
124
|
) ?? null;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
record.inputTokens,
|
|
112
|
-
record.input,
|
|
113
|
-
record.input_tokens
|
|
114
|
-
) ?? null;
|
|
125
|
+
const split = splitInputTokens(record);
|
|
126
|
+
const inputTokens = split.inputTokens;
|
|
115
127
|
const outputTokens = resolveOutputTokens(record);
|
|
116
|
-
const cacheRead =
|
|
117
|
-
firstNumber(
|
|
118
|
-
record.cached_input_tokens,
|
|
119
|
-
record.cachedInputTokens,
|
|
120
|
-
record.cache_read_input_tokens,
|
|
121
|
-
record.cacheReadInputTokens,
|
|
122
|
-
record.cache_read,
|
|
123
|
-
record.cacheRead
|
|
124
|
-
) ?? null;
|
|
128
|
+
const cacheRead = split.cacheReadTokens;
|
|
125
129
|
const cacheWrite =
|
|
126
130
|
firstNumber(
|
|
127
131
|
record.cache_creation_input_tokens,
|
|
@@ -197,15 +201,6 @@ export function getCodexUsageTotalsFromInput(
|
|
|
197
201
|
const parsed = parseCodexUsageTotalsRecord(totalUsage);
|
|
198
202
|
if (parsed) return parsed;
|
|
199
203
|
}
|
|
200
|
-
const lastUsage = resolveNestedRecord(
|
|
201
|
-
tokenUsage,
|
|
202
|
-
"last_token_usage",
|
|
203
|
-
"lastTokenUsage"
|
|
204
|
-
);
|
|
205
|
-
if (lastUsage) {
|
|
206
|
-
const parsed = parseCodexUsageTotalsRecord(lastUsage);
|
|
207
|
-
if (parsed) return parsed;
|
|
208
|
-
}
|
|
209
204
|
const parsed = parseCodexUsageTotalsRecord(tokenUsage as Record<string, unknown>);
|
|
210
205
|
if (parsed) return parsed;
|
|
211
206
|
}
|
package/src/types.ts
CHANGED