@praeviso/code-env-switch 0.1.3 → 0.1.5
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 +17 -0
- package/README_zh.md +17 -0
- package/bin/commands/list.js +44 -2
- package/bin/statusline/debug.js +42 -0
- package/bin/statusline/format.js +57 -0
- package/bin/statusline/git.js +96 -0
- package/bin/statusline/index.js +107 -558
- package/bin/statusline/input.js +167 -0
- package/bin/statusline/style.js +22 -0
- package/bin/statusline/types.js +2 -0
- package/bin/statusline/usage/claude.js +181 -0
- package/bin/statusline/usage/codex.js +168 -0
- package/bin/statusline/usage.js +67 -0
- package/bin/statusline/utils.js +35 -0
- package/bin/usage/index.js +396 -41
- package/bin/usage/pricing.js +303 -0
- package/code-env.example.json +55 -0
- package/package.json +1 -1
- package/src/commands/list.ts +74 -4
- package/src/statusline/debug.ts +40 -0
- package/src/statusline/format.ts +67 -0
- package/src/statusline/git.ts +82 -0
- package/src/statusline/index.ts +143 -764
- package/src/statusline/input.ts +159 -0
- package/src/statusline/style.ts +19 -0
- package/src/statusline/types.ts +111 -0
- package/src/statusline/usage/claude.ts +299 -0
- package/src/statusline/usage/codex.ts +263 -0
- package/src/statusline/usage.ts +80 -0
- package/src/statusline/utils.ts +27 -0
- package/src/types.ts +23 -0
- package/src/usage/index.ts +519 -35
- package/src/usage/pricing.ts +323 -0
- package/PLAN.md +0 -33
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MODEL_PRICING = void 0;
|
|
4
|
+
exports.resolvePricingForProfile = resolvePricingForProfile;
|
|
5
|
+
exports.calculateUsageCost = calculateUsageCost;
|
|
6
|
+
exports.formatUsdAmount = formatUsdAmount;
|
|
7
|
+
const TOKENS_PER_MILLION = 1000000;
|
|
8
|
+
exports.DEFAULT_MODEL_PRICING = {
|
|
9
|
+
"Claude Sonnet 4.5": {
|
|
10
|
+
input: 3.0,
|
|
11
|
+
output: 15.0,
|
|
12
|
+
cacheWrite: 3.75,
|
|
13
|
+
cacheRead: 0.3,
|
|
14
|
+
description: "Balanced performance and speed for daily use.",
|
|
15
|
+
},
|
|
16
|
+
"Sonnet 4.5": {
|
|
17
|
+
input: 3.0,
|
|
18
|
+
output: 15.0,
|
|
19
|
+
cacheWrite: 3.75,
|
|
20
|
+
cacheRead: 0.3,
|
|
21
|
+
description: "Balanced performance and speed for daily use.",
|
|
22
|
+
},
|
|
23
|
+
"Claude Opus 4.5": {
|
|
24
|
+
input: 5.0,
|
|
25
|
+
output: 25.0,
|
|
26
|
+
cacheWrite: 6.25,
|
|
27
|
+
cacheRead: 0.5,
|
|
28
|
+
description: "Most capable model for agents and coding.",
|
|
29
|
+
},
|
|
30
|
+
"Opus 4.5": {
|
|
31
|
+
input: 5.0,
|
|
32
|
+
output: 25.0,
|
|
33
|
+
cacheWrite: 6.25,
|
|
34
|
+
cacheRead: 0.5,
|
|
35
|
+
description: "Most capable model for agents and coding.",
|
|
36
|
+
},
|
|
37
|
+
"Claude Haiku 4.5": {
|
|
38
|
+
input: 1.0,
|
|
39
|
+
output: 5.0,
|
|
40
|
+
cacheWrite: 1.25,
|
|
41
|
+
cacheRead: 0.1,
|
|
42
|
+
description: "Fast responses for lightweight tasks.",
|
|
43
|
+
},
|
|
44
|
+
"Haiku 4.5": {
|
|
45
|
+
input: 1.0,
|
|
46
|
+
output: 5.0,
|
|
47
|
+
cacheWrite: 1.25,
|
|
48
|
+
cacheRead: 0.1,
|
|
49
|
+
description: "Fast responses for lightweight tasks.",
|
|
50
|
+
},
|
|
51
|
+
"claude-opus-4-5-20251101": {
|
|
52
|
+
input: 5.0,
|
|
53
|
+
output: 25.0,
|
|
54
|
+
cacheWrite: 6.25,
|
|
55
|
+
cacheRead: 0.5,
|
|
56
|
+
description: "Most capable model for agents and coding.",
|
|
57
|
+
},
|
|
58
|
+
"claude-sonnet-4-5-20251022": {
|
|
59
|
+
input: 3.0,
|
|
60
|
+
output: 15.0,
|
|
61
|
+
cacheWrite: 3.75,
|
|
62
|
+
cacheRead: 0.3,
|
|
63
|
+
description: "Balanced performance and speed for daily use.",
|
|
64
|
+
},
|
|
65
|
+
"claude-haiku-4-5-20251022": {
|
|
66
|
+
input: 1.0,
|
|
67
|
+
output: 5.0,
|
|
68
|
+
cacheWrite: 1.25,
|
|
69
|
+
cacheRead: 0.1,
|
|
70
|
+
description: "Fast responses for lightweight tasks.",
|
|
71
|
+
},
|
|
72
|
+
"gpt-5.1": {
|
|
73
|
+
input: 1.25,
|
|
74
|
+
output: 10.0,
|
|
75
|
+
cacheRead: 0.125,
|
|
76
|
+
description: "Base model for daily development work.",
|
|
77
|
+
},
|
|
78
|
+
"gpt-5.1-codex": {
|
|
79
|
+
input: 1.25,
|
|
80
|
+
output: 10.0,
|
|
81
|
+
cacheRead: 0.125,
|
|
82
|
+
description: "Code-focused model for programming workflows.",
|
|
83
|
+
},
|
|
84
|
+
"gpt-5.1-codex-max": {
|
|
85
|
+
input: 1.25,
|
|
86
|
+
output: 10.0,
|
|
87
|
+
cacheRead: 0.125,
|
|
88
|
+
description: "Flagship code model for complex projects.",
|
|
89
|
+
},
|
|
90
|
+
"gpt-5.2": {
|
|
91
|
+
input: 1.75,
|
|
92
|
+
output: 14.0,
|
|
93
|
+
cacheRead: 0.175,
|
|
94
|
+
description: "Latest flagship model with improved performance.",
|
|
95
|
+
},
|
|
96
|
+
"gpt-5.2-codex": {
|
|
97
|
+
input: 1.75,
|
|
98
|
+
output: 14.0,
|
|
99
|
+
cacheRead: 0.175,
|
|
100
|
+
description: "Latest flagship code model.",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
function normalizeModelKey(value) {
|
|
104
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
105
|
+
}
|
|
106
|
+
function parsePriceValue(value) {
|
|
107
|
+
if (value === null || value === undefined)
|
|
108
|
+
return null;
|
|
109
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
110
|
+
return value;
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
const match = value.replace(/,/g, "").match(/-?\d+(?:\.\d+)?/);
|
|
113
|
+
if (match) {
|
|
114
|
+
const parsed = Number(match[0]);
|
|
115
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function compactPricing(value) {
|
|
121
|
+
if (!value || typeof value !== "object")
|
|
122
|
+
return null;
|
|
123
|
+
const input = parsePriceValue(value.input);
|
|
124
|
+
const output = parsePriceValue(value.output);
|
|
125
|
+
const cacheRead = parsePriceValue(value.cacheRead);
|
|
126
|
+
const cacheWrite = parsePriceValue(value.cacheWrite);
|
|
127
|
+
const description = typeof value.description === "string" && value.description.trim()
|
|
128
|
+
? value.description.trim()
|
|
129
|
+
: undefined;
|
|
130
|
+
const pricing = {};
|
|
131
|
+
let hasNumber = false;
|
|
132
|
+
if (input !== null) {
|
|
133
|
+
pricing.input = input;
|
|
134
|
+
hasNumber = true;
|
|
135
|
+
}
|
|
136
|
+
if (output !== null) {
|
|
137
|
+
pricing.output = output;
|
|
138
|
+
hasNumber = true;
|
|
139
|
+
}
|
|
140
|
+
if (cacheRead !== null) {
|
|
141
|
+
pricing.cacheRead = cacheRead;
|
|
142
|
+
hasNumber = true;
|
|
143
|
+
}
|
|
144
|
+
if (cacheWrite !== null) {
|
|
145
|
+
pricing.cacheWrite = cacheWrite;
|
|
146
|
+
hasNumber = true;
|
|
147
|
+
}
|
|
148
|
+
if (description)
|
|
149
|
+
pricing.description = description;
|
|
150
|
+
return hasNumber ? pricing : null;
|
|
151
|
+
}
|
|
152
|
+
function resolveMultiplier(value) {
|
|
153
|
+
const parsed = parsePriceValue(value);
|
|
154
|
+
if (parsed === null)
|
|
155
|
+
return null;
|
|
156
|
+
if (parsed < 0)
|
|
157
|
+
return null;
|
|
158
|
+
return parsed;
|
|
159
|
+
}
|
|
160
|
+
function applyMultiplier(pricing, multiplier) {
|
|
161
|
+
if (!pricing)
|
|
162
|
+
return null;
|
|
163
|
+
if (multiplier === null)
|
|
164
|
+
return pricing;
|
|
165
|
+
const scaled = {};
|
|
166
|
+
if (pricing.input !== undefined)
|
|
167
|
+
scaled.input = pricing.input * multiplier;
|
|
168
|
+
if (pricing.output !== undefined)
|
|
169
|
+
scaled.output = pricing.output * multiplier;
|
|
170
|
+
if (pricing.cacheRead !== undefined)
|
|
171
|
+
scaled.cacheRead = pricing.cacheRead * multiplier;
|
|
172
|
+
if (pricing.cacheWrite !== undefined)
|
|
173
|
+
scaled.cacheWrite = pricing.cacheWrite * multiplier;
|
|
174
|
+
if (pricing.description)
|
|
175
|
+
scaled.description = pricing.description;
|
|
176
|
+
return scaled;
|
|
177
|
+
}
|
|
178
|
+
function buildModelIndex(models) {
|
|
179
|
+
const index = new Map();
|
|
180
|
+
if (!models)
|
|
181
|
+
return index;
|
|
182
|
+
for (const [model, pricing] of Object.entries(models)) {
|
|
183
|
+
const key = normalizeModelKey(model);
|
|
184
|
+
if (!key)
|
|
185
|
+
continue;
|
|
186
|
+
const cleaned = compactPricing(pricing);
|
|
187
|
+
if (!cleaned)
|
|
188
|
+
continue;
|
|
189
|
+
index.set(key, { model, pricing: cleaned });
|
|
190
|
+
}
|
|
191
|
+
return index;
|
|
192
|
+
}
|
|
193
|
+
function resolveModelPricing(config, model) {
|
|
194
|
+
var _a;
|
|
195
|
+
if (!model)
|
|
196
|
+
return null;
|
|
197
|
+
const key = normalizeModelKey(model);
|
|
198
|
+
if (!key)
|
|
199
|
+
return null;
|
|
200
|
+
const configIndex = buildModelIndex((_a = config.pricing) === null || _a === void 0 ? void 0 : _a.models);
|
|
201
|
+
const fromConfig = configIndex.get(key);
|
|
202
|
+
if (fromConfig)
|
|
203
|
+
return fromConfig;
|
|
204
|
+
const defaultsIndex = buildModelIndex(exports.DEFAULT_MODEL_PRICING);
|
|
205
|
+
return defaultsIndex.get(key) || null;
|
|
206
|
+
}
|
|
207
|
+
function mergePricing(base, override) {
|
|
208
|
+
if (!base && !override)
|
|
209
|
+
return null;
|
|
210
|
+
return compactPricing({ ...(base || {}), ...(override || {}) });
|
|
211
|
+
}
|
|
212
|
+
function getProfilePricing(profile) {
|
|
213
|
+
var _a;
|
|
214
|
+
if (!profile || !profile.pricing || typeof profile.pricing !== "object") {
|
|
215
|
+
return { model: null, pricing: null, multiplier: null };
|
|
216
|
+
}
|
|
217
|
+
const raw = profile.pricing;
|
|
218
|
+
const model = typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : null;
|
|
219
|
+
return {
|
|
220
|
+
model,
|
|
221
|
+
pricing: compactPricing(raw),
|
|
222
|
+
multiplier: (_a = raw.multiplier) !== null && _a !== void 0 ? _a : null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function resolvePricingForProfile(config, profile, model) {
|
|
226
|
+
var _a, _b;
|
|
227
|
+
const profilePricing = getProfilePricing(profile);
|
|
228
|
+
const baseFromProfileModel = profilePricing.model
|
|
229
|
+
? resolveModelPricing(config, profilePricing.model)
|
|
230
|
+
: null;
|
|
231
|
+
const mergedProfile = mergePricing(baseFromProfileModel ? baseFromProfileModel.pricing : null, profilePricing.pricing);
|
|
232
|
+
const resolvedPricing = mergedProfile ||
|
|
233
|
+
(baseFromProfileModel ? baseFromProfileModel.pricing : null) ||
|
|
234
|
+
((_b = (_a = resolveModelPricing(config, model)) === null || _a === void 0 ? void 0 : _a.pricing) !== null && _b !== void 0 ? _b : null);
|
|
235
|
+
if (!resolvedPricing)
|
|
236
|
+
return null;
|
|
237
|
+
const multiplier = resolveMultiplier(profilePricing.multiplier);
|
|
238
|
+
return applyMultiplier(resolvedPricing, multiplier);
|
|
239
|
+
}
|
|
240
|
+
function toFiniteNumber(value) {
|
|
241
|
+
if (value === null || value === undefined)
|
|
242
|
+
return null;
|
|
243
|
+
const num = Number(value);
|
|
244
|
+
if (!Number.isFinite(num))
|
|
245
|
+
return null;
|
|
246
|
+
return num;
|
|
247
|
+
}
|
|
248
|
+
function calculateUsageCost(usage, pricing) {
|
|
249
|
+
var _a, _b, _c, _d, _e, _f;
|
|
250
|
+
if (!usage || !pricing)
|
|
251
|
+
return null;
|
|
252
|
+
const inputTokens = toFiniteNumber(usage.inputTokens);
|
|
253
|
+
const outputTokens = toFiniteNumber(usage.outputTokens);
|
|
254
|
+
const cacheReadTokens = toFiniteNumber(usage.cacheReadTokens);
|
|
255
|
+
const cacheWriteTokens = toFiniteNumber(usage.cacheWriteTokens);
|
|
256
|
+
if (inputTokens === null &&
|
|
257
|
+
outputTokens === null &&
|
|
258
|
+
cacheReadTokens === null &&
|
|
259
|
+
cacheWriteTokens === null) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const tokens = {
|
|
263
|
+
input: Math.max(0, inputTokens || 0),
|
|
264
|
+
output: Math.max(0, outputTokens || 0),
|
|
265
|
+
cacheRead: Math.max(0, cacheReadTokens || 0),
|
|
266
|
+
cacheWrite: Math.max(0, cacheWriteTokens || 0),
|
|
267
|
+
};
|
|
268
|
+
const knownTotal = toFiniteNumber((_b = (_a = usage.todayTokens) !== null && _a !== void 0 ? _a : usage.totalTokens) !== null && _b !== void 0 ? _b : null);
|
|
269
|
+
const breakdownTotal = tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite;
|
|
270
|
+
if (breakdownTotal === 0 && knownTotal !== null && knownTotal > 0) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (tokens.input > 0 && pricing.input === undefined)
|
|
274
|
+
return null;
|
|
275
|
+
if (tokens.output > 0 && pricing.output === undefined)
|
|
276
|
+
return null;
|
|
277
|
+
if (tokens.cacheRead > 0 && pricing.cacheRead === undefined)
|
|
278
|
+
return null;
|
|
279
|
+
if (tokens.cacheWrite > 0 && pricing.cacheWrite === undefined)
|
|
280
|
+
return null;
|
|
281
|
+
const total = (tokens.input * ((_c = pricing.input) !== null && _c !== void 0 ? _c : 0) +
|
|
282
|
+
tokens.output * ((_d = pricing.output) !== null && _d !== void 0 ? _d : 0) +
|
|
283
|
+
tokens.cacheRead * ((_e = pricing.cacheRead) !== null && _e !== void 0 ? _e : 0) +
|
|
284
|
+
tokens.cacheWrite * ((_f = pricing.cacheWrite) !== null && _f !== void 0 ? _f : 0)) /
|
|
285
|
+
TOKENS_PER_MILLION;
|
|
286
|
+
return Number.isFinite(total) ? total : null;
|
|
287
|
+
}
|
|
288
|
+
function formatUsdAmount(amount) {
|
|
289
|
+
if (amount === null || !Number.isFinite(amount))
|
|
290
|
+
return "-";
|
|
291
|
+
const normalized = Math.abs(amount) < 1e-12 ? 0 : amount;
|
|
292
|
+
const abs = Math.abs(normalized);
|
|
293
|
+
let decimals = 2;
|
|
294
|
+
if (abs < 1)
|
|
295
|
+
decimals = 4;
|
|
296
|
+
if (abs < 0.1)
|
|
297
|
+
decimals = 5;
|
|
298
|
+
if (abs < 0.01)
|
|
299
|
+
decimals = 6;
|
|
300
|
+
let text = normalized.toFixed(decimals);
|
|
301
|
+
text = text.replace(/\.?0+$/, "");
|
|
302
|
+
return `$${text}`;
|
|
303
|
+
}
|
package/code-env.example.json
CHANGED
|
@@ -15,6 +15,61 @@
|
|
|
15
15
|
"type": "command",
|
|
16
16
|
"padding": 0
|
|
17
17
|
},
|
|
18
|
+
"pricing": {
|
|
19
|
+
"models": {
|
|
20
|
+
"Claude Sonnet 4.5": {
|
|
21
|
+
"input": 3.0,
|
|
22
|
+
"output": 15.0,
|
|
23
|
+
"cacheWrite": 3.75,
|
|
24
|
+
"cacheRead": 0.3,
|
|
25
|
+
"description": "Balanced performance and speed for daily use."
|
|
26
|
+
},
|
|
27
|
+
"Claude Opus 4.5": {
|
|
28
|
+
"input": 5.0,
|
|
29
|
+
"output": 25.0,
|
|
30
|
+
"cacheWrite": 6.25,
|
|
31
|
+
"cacheRead": 0.5,
|
|
32
|
+
"description": "Most capable model for agents and coding."
|
|
33
|
+
},
|
|
34
|
+
"Claude Haiku 4.5": {
|
|
35
|
+
"input": 1.0,
|
|
36
|
+
"output": 5.0,
|
|
37
|
+
"cacheWrite": 1.25,
|
|
38
|
+
"cacheRead": 0.1,
|
|
39
|
+
"description": "Fast responses for lightweight tasks."
|
|
40
|
+
},
|
|
41
|
+
"gpt-5.1": {
|
|
42
|
+
"input": 1.25,
|
|
43
|
+
"output": 10.0,
|
|
44
|
+
"cacheRead": 0.125,
|
|
45
|
+
"description": "Base model for daily development work."
|
|
46
|
+
},
|
|
47
|
+
"gpt-5.1-codex": {
|
|
48
|
+
"input": 1.25,
|
|
49
|
+
"output": 10.0,
|
|
50
|
+
"cacheRead": 0.125,
|
|
51
|
+
"description": "Code-focused model for programming workflows."
|
|
52
|
+
},
|
|
53
|
+
"gpt-5.1-codex-max": {
|
|
54
|
+
"input": 1.25,
|
|
55
|
+
"output": 10.0,
|
|
56
|
+
"cacheRead": 0.125,
|
|
57
|
+
"description": "Flagship code model for complex projects."
|
|
58
|
+
},
|
|
59
|
+
"gpt-5.2": {
|
|
60
|
+
"input": 1.75,
|
|
61
|
+
"output": 14.0,
|
|
62
|
+
"cacheRead": 0.175,
|
|
63
|
+
"description": "Latest flagship model with improved performance."
|
|
64
|
+
},
|
|
65
|
+
"gpt-5.2-codex": {
|
|
66
|
+
"input": 1.75,
|
|
67
|
+
"output": 14.0,
|
|
68
|
+
"cacheRead": 0.175,
|
|
69
|
+
"description": "Latest flagship code model."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
18
73
|
"profiles": {
|
|
19
74
|
"p_a1b2c3": {
|
|
20
75
|
"name": "primary",
|
package/package.json
CHANGED
package/src/commands/list.ts
CHANGED
|
@@ -6,9 +6,12 @@ import { buildListRows } from "../profile/display";
|
|
|
6
6
|
import { getResolvedDefaultProfileKeys } from "../config/defaults";
|
|
7
7
|
import {
|
|
8
8
|
formatTokenCount,
|
|
9
|
+
readUsageCostIndex,
|
|
9
10
|
readUsageTotalsIndex,
|
|
11
|
+
resolveUsageCostForProfile,
|
|
10
12
|
resolveUsageTotalsForProfile,
|
|
11
13
|
} from "../usage";
|
|
14
|
+
import { formatUsdAmount } from "../usage/pricing";
|
|
12
15
|
|
|
13
16
|
export function printList(config: Config, configPath: string | null): void {
|
|
14
17
|
const rows = buildListRows(config, getResolvedDefaultProfileKeys);
|
|
@@ -18,6 +21,7 @@ export function printList(config: Config, configPath: string | null): void {
|
|
|
18
21
|
}
|
|
19
22
|
try {
|
|
20
23
|
const usageTotals = readUsageTotalsIndex(config, configPath, true);
|
|
24
|
+
const usageCosts = readUsageCostIndex(config, configPath, false);
|
|
21
25
|
if (usageTotals) {
|
|
22
26
|
for (const row of rows) {
|
|
23
27
|
if (!row.usageType) continue;
|
|
@@ -32,6 +36,22 @@ export function printList(config: Config, configPath: string | null): void {
|
|
|
32
36
|
row.totalTokens = usage.total;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
39
|
+
if (usageCosts) {
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
if (!row.usageType) continue;
|
|
42
|
+
const cost = resolveUsageCostForProfile(
|
|
43
|
+
usageCosts,
|
|
44
|
+
row.usageType,
|
|
45
|
+
row.key,
|
|
46
|
+
row.name
|
|
47
|
+
);
|
|
48
|
+
if (!cost) continue;
|
|
49
|
+
row.todayCost = cost.today;
|
|
50
|
+
row.totalCost = cost.total;
|
|
51
|
+
row.todayBilledTokens = cost.todayTokens;
|
|
52
|
+
row.totalBilledTokens = cost.totalTokens;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
35
55
|
} catch {
|
|
36
56
|
// ignore usage sync errors
|
|
37
57
|
}
|
|
@@ -40,8 +60,20 @@ export function printList(config: Config, configPath: string | null): void {
|
|
|
40
60
|
const headerToday = "TODAY";
|
|
41
61
|
const headerTotal = "TOTAL";
|
|
42
62
|
const headerNote = "NOTE";
|
|
43
|
-
const todayTexts = rows.map((row) =>
|
|
44
|
-
|
|
63
|
+
const todayTexts = rows.map((row) =>
|
|
64
|
+
formatUsageWithCost(
|
|
65
|
+
row.todayTokens,
|
|
66
|
+
row.todayBilledTokens ?? null,
|
|
67
|
+
row.todayCost ?? null
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
const totalTexts = rows.map((row) =>
|
|
71
|
+
formatUsageWithCost(
|
|
72
|
+
row.totalTokens,
|
|
73
|
+
row.totalBilledTokens ?? null,
|
|
74
|
+
row.totalCost ?? null
|
|
75
|
+
)
|
|
76
|
+
);
|
|
45
77
|
const nameWidth = Math.max(
|
|
46
78
|
headerName.length,
|
|
47
79
|
...rows.map((row) => row.name.length)
|
|
@@ -67,7 +99,15 @@ export function printList(config: Config, configPath: string | null): void {
|
|
|
67
99
|
todayWidth
|
|
68
100
|
)} ${total.padStart(totalWidth)} ${note.padEnd(noteWidth)}`;
|
|
69
101
|
|
|
70
|
-
console.log(
|
|
102
|
+
console.log(
|
|
103
|
+
formatRow(
|
|
104
|
+
headerName,
|
|
105
|
+
headerType,
|
|
106
|
+
headerToday,
|
|
107
|
+
headerTotal,
|
|
108
|
+
headerNote
|
|
109
|
+
)
|
|
110
|
+
);
|
|
71
111
|
console.log(
|
|
72
112
|
formatRow(
|
|
73
113
|
"-".repeat(nameWidth),
|
|
@@ -81,7 +121,13 @@ export function printList(config: Config, configPath: string | null): void {
|
|
|
81
121
|
const row = rows[i];
|
|
82
122
|
const todayText = todayTexts[i] || "-";
|
|
83
123
|
const totalText = totalTexts[i] || "-";
|
|
84
|
-
const line = formatRow(
|
|
124
|
+
const line = formatRow(
|
|
125
|
+
row.name,
|
|
126
|
+
row.type,
|
|
127
|
+
todayText,
|
|
128
|
+
totalText,
|
|
129
|
+
row.note
|
|
130
|
+
);
|
|
85
131
|
if (row.active) {
|
|
86
132
|
console.log(`\x1b[32m${line}\x1b[0m`);
|
|
87
133
|
} else {
|
|
@@ -89,3 +135,27 @@ export function printList(config: Config, configPath: string | null): void {
|
|
|
89
135
|
}
|
|
90
136
|
}
|
|
91
137
|
}
|
|
138
|
+
|
|
139
|
+
function formatUsageWithCost(
|
|
140
|
+
tokens: number | null | undefined,
|
|
141
|
+
billedTokens: number | null,
|
|
142
|
+
cost: number | null
|
|
143
|
+
): string {
|
|
144
|
+
const tokenText = formatTokenCount(tokens ?? null);
|
|
145
|
+
if (tokenText === "-") return tokenText;
|
|
146
|
+
if (cost === null || !Number.isFinite(cost)) return tokenText;
|
|
147
|
+
if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) {
|
|
148
|
+
return tokenText;
|
|
149
|
+
}
|
|
150
|
+
if (billedTokens === null || !Number.isFinite(billedTokens)) {
|
|
151
|
+
return `${tokenText} (${formatUsdAmount(cost)})`;
|
|
152
|
+
}
|
|
153
|
+
if (billedTokens >= tokens) {
|
|
154
|
+
return `${tokenText} (${formatUsdAmount(cost)})`;
|
|
155
|
+
}
|
|
156
|
+
const billedText = formatTokenCount(billedTokens);
|
|
157
|
+
if (billedText === "-") {
|
|
158
|
+
return `${tokenText} (${formatUsdAmount(cost)})`;
|
|
159
|
+
}
|
|
160
|
+
return `${tokenText} (billed ${billedText}, ${formatUsdAmount(cost)})`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { resolvePath } from "../shell/utils";
|
|
5
|
+
|
|
6
|
+
function isStatuslineDebugEnabled(): boolean {
|
|
7
|
+
const raw = process.env.CODE_ENV_STATUSLINE_DEBUG;
|
|
8
|
+
if (!raw) return false;
|
|
9
|
+
const value = String(raw).trim().toLowerCase();
|
|
10
|
+
if (!value) return false;
|
|
11
|
+
return !["0", "false", "no", "off"].includes(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveDefaultConfigDir(configPath: string | null): string {
|
|
15
|
+
if (configPath) return path.dirname(configPath);
|
|
16
|
+
return path.join(os.homedir(), ".config", "code-env");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getStatuslineDebugPath(configPath: string | null): string {
|
|
20
|
+
const envPath = resolvePath(process.env.CODE_ENV_STATUSLINE_DEBUG_PATH);
|
|
21
|
+
if (envPath) return envPath;
|
|
22
|
+
return path.join(resolveDefaultConfigDir(configPath), "statusline-debug.jsonl");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function appendStatuslineDebug(
|
|
26
|
+
configPath: string | null,
|
|
27
|
+
payload: Record<string, unknown>
|
|
28
|
+
) {
|
|
29
|
+
if (!isStatuslineDebugEnabled()) return;
|
|
30
|
+
try {
|
|
31
|
+
const debugPath = getStatuslineDebugPath(configPath);
|
|
32
|
+
const dir = path.dirname(debugPath);
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
fs.appendFileSync(debugPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore debug logging failures
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { formatTokenCount } from "../usage";
|
|
2
|
+
import { formatUsdAmount } from "../usage/pricing";
|
|
3
|
+
import {
|
|
4
|
+
ICON_CONTEXT,
|
|
5
|
+
ICON_CWD,
|
|
6
|
+
ICON_MODEL,
|
|
7
|
+
ICON_PROFILE,
|
|
8
|
+
ICON_REVIEW,
|
|
9
|
+
ICON_USAGE,
|
|
10
|
+
colorize,
|
|
11
|
+
dim,
|
|
12
|
+
} from "./style";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
|
|
15
|
+
export function getCwdSegment(cwd: string): string | null {
|
|
16
|
+
if (!cwd) return null;
|
|
17
|
+
const base = path.basename(cwd) || cwd;
|
|
18
|
+
const segment = `${ICON_CWD} ${base}`;
|
|
19
|
+
return dim(segment);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatUsageSegment(
|
|
23
|
+
todayCost: number | null,
|
|
24
|
+
sessionCost: number | null
|
|
25
|
+
): string | null {
|
|
26
|
+
if (todayCost === null && sessionCost === null) return null;
|
|
27
|
+
const todayText = `T ${formatUsdAmount(todayCost)}`;
|
|
28
|
+
const sessionText = `S ${formatUsdAmount(sessionCost)}`;
|
|
29
|
+
const text = `${todayText} / ${sessionText}`;
|
|
30
|
+
return colorize(`${ICON_USAGE} ${text}`, "33");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatModelSegment(
|
|
34
|
+
model: string | null,
|
|
35
|
+
provider: string | null
|
|
36
|
+
): string | null {
|
|
37
|
+
if (!model) return null;
|
|
38
|
+
const providerLabel = provider ? `${provider}:${model}` : model;
|
|
39
|
+
return colorize(`${ICON_MODEL} ${providerLabel}`, "35");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatProfileSegment(
|
|
43
|
+
type: string | null,
|
|
44
|
+
profileKey: string | null,
|
|
45
|
+
profileName: string | null
|
|
46
|
+
): string | null {
|
|
47
|
+
const name = profileName || profileKey;
|
|
48
|
+
if (!name) return null;
|
|
49
|
+
const label = type ? `${type}:${name}` : name;
|
|
50
|
+
return colorize(`${ICON_PROFILE} ${label}`, "37");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatContextSegment(contextLeft: number | null): string | null {
|
|
54
|
+
if (contextLeft === null) return null;
|
|
55
|
+
const left = Math.max(0, Math.min(100, Math.round(contextLeft)));
|
|
56
|
+
return colorize(`${ICON_CONTEXT} ${left}% left`, "36");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatContextUsedSegment(usedTokens: number | null): string | null {
|
|
60
|
+
if (usedTokens === null) return null;
|
|
61
|
+
return colorize(`${ICON_CONTEXT} ${formatTokenCount(usedTokens)} used`, "36");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatModeSegment(reviewMode: boolean): string | null {
|
|
65
|
+
if (!reviewMode) return null;
|
|
66
|
+
return colorize(`${ICON_REVIEW} review`, "34");
|
|
67
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import type { GitStatus } from "./types";
|
|
3
|
+
import { colorize } from "./style";
|
|
4
|
+
import { ICON_GIT } from "./style";
|
|
5
|
+
|
|
6
|
+
export function getGitStatus(cwd: string): GitStatus | null {
|
|
7
|
+
if (!cwd) return null;
|
|
8
|
+
const result = spawnSync("git", ["-C", cwd, "status", "--porcelain=v2", "-b"], {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11
|
+
});
|
|
12
|
+
if (result.status !== 0 || !result.stdout) return null;
|
|
13
|
+
const status: GitStatus = {
|
|
14
|
+
branch: null,
|
|
15
|
+
ahead: 0,
|
|
16
|
+
behind: 0,
|
|
17
|
+
staged: 0,
|
|
18
|
+
unstaged: 0,
|
|
19
|
+
untracked: 0,
|
|
20
|
+
conflicted: 0,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const lines = result.stdout.split(/\r?\n/);
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (!line) continue;
|
|
26
|
+
if (line.startsWith("# branch.head ")) {
|
|
27
|
+
status.branch = line.slice("# branch.head ".length).trim();
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (line.startsWith("# branch.ab ")) {
|
|
31
|
+
const parts = line
|
|
32
|
+
.slice("# branch.ab ".length)
|
|
33
|
+
.trim()
|
|
34
|
+
.split(/\s+/);
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
if (part.startsWith("+")) status.ahead = Number(part.slice(1)) || 0;
|
|
37
|
+
if (part.startsWith("-")) status.behind = Number(part.slice(1)) || 0;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (line.startsWith("? ")) {
|
|
42
|
+
status.untracked += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith("u ")) {
|
|
46
|
+
status.conflicted += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
50
|
+
const parts = line.split(/\s+/);
|
|
51
|
+
const xy = parts[1] || "";
|
|
52
|
+
const staged = xy[0];
|
|
53
|
+
const unstaged = xy[1];
|
|
54
|
+
if (staged && staged !== ".") status.staged += 1;
|
|
55
|
+
if (unstaged && unstaged !== ".") status.unstaged += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!status.branch) {
|
|
61
|
+
status.branch = "HEAD";
|
|
62
|
+
}
|
|
63
|
+
return status;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatGitSegment(status: GitStatus | null): string | null {
|
|
67
|
+
if (!status || !status.branch) return null;
|
|
68
|
+
const meta: string[] = [];
|
|
69
|
+
const dirtyCount = status.staged + status.unstaged + status.untracked;
|
|
70
|
+
if (status.ahead > 0) meta.push(`↑${status.ahead}`);
|
|
71
|
+
if (status.behind > 0) meta.push(`↓${status.behind}`);
|
|
72
|
+
if (status.conflicted > 0) meta.push(`✖${status.conflicted}`);
|
|
73
|
+
if (dirtyCount > 0) meta.push(`+${dirtyCount}`);
|
|
74
|
+
const suffix = meta.length > 0 ? ` [${meta.join("")}]` : "";
|
|
75
|
+
const text = `${ICON_GIT} ${status.branch}${suffix}`;
|
|
76
|
+
const hasConflicts = status.conflicted > 0;
|
|
77
|
+
const isDirty = dirtyCount > 0;
|
|
78
|
+
if (hasConflicts) return colorize(text, "31");
|
|
79
|
+
if (isDirty) return colorize(text, "33");
|
|
80
|
+
if (status.ahead > 0 || status.behind > 0) return colorize(text, "36");
|
|
81
|
+
return colorize(text, "32");
|
|
82
|
+
}
|