@praeviso/code-env-switch 0.1.1 → 0.1.3
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/.github/workflows/npm-publish.yml +25 -0
- package/AGENTS.md +32 -0
- package/PLAN.md +33 -0
- package/README.md +24 -0
- package/README_zh.md +24 -0
- package/bin/cli/args.js +303 -0
- package/bin/cli/help.js +77 -0
- package/bin/cli/index.js +13 -0
- package/bin/commands/add.js +81 -0
- package/bin/commands/index.js +21 -0
- package/bin/commands/launch.js +330 -0
- package/bin/commands/list.js +57 -0
- package/bin/commands/show.js +10 -0
- package/bin/commands/statusline.js +12 -0
- package/bin/commands/unset.js +20 -0
- package/bin/commands/use.js +92 -0
- package/bin/config/defaults.js +85 -0
- package/bin/config/index.js +20 -0
- package/bin/config/io.js +72 -0
- package/bin/constants.js +27 -0
- package/bin/index.js +279 -0
- package/bin/profile/display.js +78 -0
- package/bin/profile/index.js +26 -0
- package/bin/profile/match.js +40 -0
- package/bin/profile/resolve.js +79 -0
- package/bin/profile/type.js +90 -0
- package/bin/shell/detect.js +40 -0
- package/bin/shell/index.js +18 -0
- package/bin/shell/snippet.js +92 -0
- package/bin/shell/utils.js +35 -0
- package/bin/statusline/claude.js +153 -0
- package/bin/statusline/codex.js +356 -0
- package/bin/statusline/index.js +631 -0
- package/bin/types.js +5 -0
- package/bin/ui/index.js +16 -0
- package/bin/ui/interactive.js +189 -0
- package/bin/ui/readline.js +76 -0
- package/bin/usage/index.js +832 -0
- package/code-env.example.json +11 -0
- package/package.json +2 -2
- package/src/cli/args.ts +318 -0
- package/src/cli/help.ts +75 -0
- package/src/cli/index.ts +5 -0
- package/src/commands/add.ts +91 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/launch.ts +395 -0
- package/src/commands/list.ts +91 -0
- package/src/commands/show.ts +12 -0
- package/src/commands/statusline.ts +18 -0
- package/src/commands/unset.ts +19 -0
- package/src/commands/use.ts +121 -0
- package/src/config/defaults.ts +88 -0
- package/src/config/index.ts +19 -0
- package/src/config/io.ts +69 -0
- package/src/constants.ts +28 -0
- package/src/index.ts +359 -0
- package/src/profile/display.ts +77 -0
- package/src/profile/index.ts +12 -0
- package/src/profile/match.ts +41 -0
- package/src/profile/resolve.ts +84 -0
- package/src/profile/type.ts +83 -0
- package/src/shell/detect.ts +30 -0
- package/src/shell/index.ts +6 -0
- package/src/shell/snippet.ts +92 -0
- package/src/shell/utils.ts +30 -0
- package/src/statusline/claude.ts +172 -0
- package/src/statusline/codex.ts +393 -0
- package/src/statusline/index.ts +920 -0
- package/src/types.ts +95 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/interactive.ts +220 -0
- package/src/ui/readline.ts +85 -0
- package/src/usage/index.ts +979 -0
- package/bin/codenv.js +0 -1316
- package/src/codenv.ts +0 -1478
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getUsagePath = getUsagePath;
|
|
4
|
+
exports.getUsageStatePath = getUsageStatePath;
|
|
5
|
+
exports.getProfileLogPath = getProfileLogPath;
|
|
6
|
+
exports.getCodexSessionsPath = getCodexSessionsPath;
|
|
7
|
+
exports.getClaudeSessionsPath = getClaudeSessionsPath;
|
|
8
|
+
exports.formatTokenCount = formatTokenCount;
|
|
9
|
+
exports.buildUsageTotals = buildUsageTotals;
|
|
10
|
+
exports.readUsageTotalsIndex = readUsageTotalsIndex;
|
|
11
|
+
exports.resolveUsageTotalsForProfile = resolveUsageTotalsForProfile;
|
|
12
|
+
exports.syncUsageFromStatuslineInput = syncUsageFromStatuslineInput;
|
|
13
|
+
exports.logProfileUse = logProfileUse;
|
|
14
|
+
exports.logSessionBinding = logSessionBinding;
|
|
15
|
+
exports.readSessionBindingIndex = readSessionBindingIndex;
|
|
16
|
+
exports.readUsageRecords = readUsageRecords;
|
|
17
|
+
exports.syncUsageFromSessions = syncUsageFromSessions;
|
|
18
|
+
/**
|
|
19
|
+
* Usage tracking utilities
|
|
20
|
+
*/
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
const os = require("os");
|
|
24
|
+
const utils_1 = require("../shell/utils");
|
|
25
|
+
const type_1 = require("../profile/type");
|
|
26
|
+
function resolveDefaultConfigDir(configPath) {
|
|
27
|
+
if (configPath)
|
|
28
|
+
return path.dirname(configPath);
|
|
29
|
+
return path.join(os.homedir(), ".config", "code-env");
|
|
30
|
+
}
|
|
31
|
+
function getUsagePath(config, configPath) {
|
|
32
|
+
if (config && config.usagePath)
|
|
33
|
+
return (0, utils_1.resolvePath)(config.usagePath);
|
|
34
|
+
const baseDir = resolveDefaultConfigDir(configPath);
|
|
35
|
+
return path.join(baseDir, "usage.jsonl");
|
|
36
|
+
}
|
|
37
|
+
function getUsageStatePath(usagePath, config) {
|
|
38
|
+
if (config && config.usageStatePath)
|
|
39
|
+
return (0, utils_1.resolvePath)(config.usageStatePath);
|
|
40
|
+
return `${usagePath}.state.json`;
|
|
41
|
+
}
|
|
42
|
+
function getProfileLogPath(config, configPath) {
|
|
43
|
+
if (config && config.profileLogPath)
|
|
44
|
+
return (0, utils_1.resolvePath)(config.profileLogPath);
|
|
45
|
+
const baseDir = resolveDefaultConfigDir(configPath);
|
|
46
|
+
return path.join(baseDir, "profile-log.jsonl");
|
|
47
|
+
}
|
|
48
|
+
function getCodexSessionsPath(config) {
|
|
49
|
+
if (config && config.codexSessionsPath)
|
|
50
|
+
return (0, utils_1.resolvePath)(config.codexSessionsPath);
|
|
51
|
+
if (process.env.CODEX_HOME) {
|
|
52
|
+
return path.join(process.env.CODEX_HOME, "sessions");
|
|
53
|
+
}
|
|
54
|
+
return path.join(os.homedir(), ".codex", "sessions");
|
|
55
|
+
}
|
|
56
|
+
function getClaudeSessionsPath(config) {
|
|
57
|
+
if (config && config.claudeSessionsPath)
|
|
58
|
+
return (0, utils_1.resolvePath)(config.claudeSessionsPath);
|
|
59
|
+
if (process.env.CLAUDE_HOME) {
|
|
60
|
+
return path.join(process.env.CLAUDE_HOME, "projects");
|
|
61
|
+
}
|
|
62
|
+
return path.join(os.homedir(), ".claude", "projects");
|
|
63
|
+
}
|
|
64
|
+
function formatTokenCount(value) {
|
|
65
|
+
if (value === null || value === undefined || !Number.isFinite(value))
|
|
66
|
+
return "-";
|
|
67
|
+
if (value < 1000)
|
|
68
|
+
return `${Math.round(value)}`;
|
|
69
|
+
if (value < 1000000)
|
|
70
|
+
return `${(value / 1000).toFixed(2)}K`;
|
|
71
|
+
if (value < 1000000000)
|
|
72
|
+
return `${(value / 1000000).toFixed(2)}M`;
|
|
73
|
+
return `${(value / 1000000000).toFixed(2)}B`;
|
|
74
|
+
}
|
|
75
|
+
function buildUsageTotals(records) {
|
|
76
|
+
const byKey = new Map();
|
|
77
|
+
const byName = new Map();
|
|
78
|
+
const todayStart = new Date();
|
|
79
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
80
|
+
const todayStartMs = todayStart.getTime();
|
|
81
|
+
const tomorrowStart = new Date(todayStart);
|
|
82
|
+
tomorrowStart.setDate(todayStart.getDate() + 1);
|
|
83
|
+
const tomorrowStartMs = tomorrowStart.getTime();
|
|
84
|
+
const isToday = (ts) => {
|
|
85
|
+
if (!ts)
|
|
86
|
+
return false;
|
|
87
|
+
const time = new Date(ts).getTime();
|
|
88
|
+
if (Number.isNaN(time))
|
|
89
|
+
return false;
|
|
90
|
+
return time >= todayStartMs && time < tomorrowStartMs;
|
|
91
|
+
};
|
|
92
|
+
const addTotals = (map, key, amount, ts) => {
|
|
93
|
+
if (!key)
|
|
94
|
+
return;
|
|
95
|
+
const current = map.get(key) || { today: 0, total: 0 };
|
|
96
|
+
current.total += amount;
|
|
97
|
+
if (isToday(ts))
|
|
98
|
+
current.today += amount;
|
|
99
|
+
map.set(key, current);
|
|
100
|
+
};
|
|
101
|
+
for (const record of records) {
|
|
102
|
+
const type = normalizeUsageType(record.type) || "";
|
|
103
|
+
const total = Number(record.totalTokens || 0);
|
|
104
|
+
if (!Number.isFinite(total))
|
|
105
|
+
continue;
|
|
106
|
+
if (record.profileKey) {
|
|
107
|
+
addTotals(byKey, `${type}||${record.profileKey}`, total, record.ts);
|
|
108
|
+
}
|
|
109
|
+
if (record.profileName) {
|
|
110
|
+
addTotals(byName, `${type}||${record.profileName}`, total, record.ts);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { byKey, byName };
|
|
114
|
+
}
|
|
115
|
+
function normalizeUsageType(type) {
|
|
116
|
+
if (!type)
|
|
117
|
+
return null;
|
|
118
|
+
const normalized = (0, type_1.normalizeType)(type);
|
|
119
|
+
if (normalized)
|
|
120
|
+
return normalized;
|
|
121
|
+
const trimmed = String(type).trim();
|
|
122
|
+
return trimmed ? trimmed : null;
|
|
123
|
+
}
|
|
124
|
+
function buildSessionKey(type, sessionId) {
|
|
125
|
+
const normalized = normalizeUsageType(type || "");
|
|
126
|
+
return normalized ? `${normalized}::${sessionId}` : sessionId;
|
|
127
|
+
}
|
|
128
|
+
function toFiniteNumber(value) {
|
|
129
|
+
if (value === null || value === undefined)
|
|
130
|
+
return null;
|
|
131
|
+
const num = Number(value);
|
|
132
|
+
if (!Number.isFinite(num))
|
|
133
|
+
return null;
|
|
134
|
+
return num;
|
|
135
|
+
}
|
|
136
|
+
function buildUsageLookupKey(type, profileId) {
|
|
137
|
+
if (!profileId)
|
|
138
|
+
return null;
|
|
139
|
+
const resolvedType = normalizeUsageType(type);
|
|
140
|
+
if (!resolvedType)
|
|
141
|
+
return null;
|
|
142
|
+
return `${resolvedType}||${profileId}`;
|
|
143
|
+
}
|
|
144
|
+
function readUsageTotalsIndex(config, configPath, syncUsage) {
|
|
145
|
+
const usagePath = getUsagePath(config, configPath);
|
|
146
|
+
if (!usagePath)
|
|
147
|
+
return null;
|
|
148
|
+
if (syncUsage) {
|
|
149
|
+
syncUsageFromSessions(config, configPath, usagePath);
|
|
150
|
+
}
|
|
151
|
+
const records = readUsageRecords(usagePath);
|
|
152
|
+
if (records.length === 0)
|
|
153
|
+
return null;
|
|
154
|
+
return buildUsageTotals(records);
|
|
155
|
+
}
|
|
156
|
+
function resolveUsageTotalsForProfile(totals, type, profileKey, profileName) {
|
|
157
|
+
const keyLookup = buildUsageLookupKey(type, profileKey);
|
|
158
|
+
const nameLookup = buildUsageLookupKey(type, profileName);
|
|
159
|
+
return ((keyLookup && totals.byKey.get(keyLookup)) ||
|
|
160
|
+
(nameLookup && totals.byName.get(nameLookup)) ||
|
|
161
|
+
null);
|
|
162
|
+
}
|
|
163
|
+
function syncUsageFromStatuslineInput(config, configPath, type, profileKey, profileName, sessionId, totals, cwd) {
|
|
164
|
+
var _a, _b, _c;
|
|
165
|
+
if (!sessionId)
|
|
166
|
+
return;
|
|
167
|
+
if (!totals)
|
|
168
|
+
return;
|
|
169
|
+
if (!profileKey && !profileName)
|
|
170
|
+
return;
|
|
171
|
+
const normalizedType = (0, type_1.normalizeType)(type || "");
|
|
172
|
+
if (!normalizedType)
|
|
173
|
+
return;
|
|
174
|
+
const usagePath = getUsagePath(config, configPath);
|
|
175
|
+
if (!usagePath)
|
|
176
|
+
return;
|
|
177
|
+
const inputTokens = (_a = toFiniteNumber(totals.inputTokens)) !== null && _a !== void 0 ? _a : 0;
|
|
178
|
+
const outputTokens = (_b = toFiniteNumber(totals.outputTokens)) !== null && _b !== void 0 ? _b : 0;
|
|
179
|
+
const totalTokens = (_c = toFiniteNumber(totals.totalTokens)) !== null && _c !== void 0 ? _c : inputTokens + outputTokens;
|
|
180
|
+
if (!Number.isFinite(totalTokens))
|
|
181
|
+
return;
|
|
182
|
+
const statePath = getUsageStatePath(usagePath, config);
|
|
183
|
+
const lockPath = `${statePath}.lock`;
|
|
184
|
+
const lockFd = acquireLock(lockPath);
|
|
185
|
+
if (lockFd === null)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
const state = readUsageState(statePath);
|
|
189
|
+
const sessions = state.sessions || {};
|
|
190
|
+
const key = buildSessionKey(normalizedType, sessionId);
|
|
191
|
+
const prev = sessions[key];
|
|
192
|
+
const prevInput = prev ? prev.inputTokens : 0;
|
|
193
|
+
const prevOutput = prev ? prev.outputTokens : 0;
|
|
194
|
+
const prevTotal = prev ? prev.totalTokens : 0;
|
|
195
|
+
let deltaInput = inputTokens - prevInput;
|
|
196
|
+
let deltaOutput = outputTokens - prevOutput;
|
|
197
|
+
let deltaTotal = totalTokens - prevTotal;
|
|
198
|
+
if (deltaTotal < 0 || deltaInput < 0 || deltaOutput < 0) {
|
|
199
|
+
deltaInput = inputTokens;
|
|
200
|
+
deltaOutput = outputTokens;
|
|
201
|
+
deltaTotal = totalTokens;
|
|
202
|
+
}
|
|
203
|
+
if (deltaTotal > 0) {
|
|
204
|
+
const record = {
|
|
205
|
+
ts: new Date().toISOString(),
|
|
206
|
+
type: normalizedType,
|
|
207
|
+
profileKey: profileKey || null,
|
|
208
|
+
profileName: profileName || null,
|
|
209
|
+
inputTokens: deltaInput,
|
|
210
|
+
outputTokens: deltaOutput,
|
|
211
|
+
totalTokens: deltaTotal,
|
|
212
|
+
};
|
|
213
|
+
appendUsageRecord(usagePath, record);
|
|
214
|
+
}
|
|
215
|
+
const now = new Date().toISOString();
|
|
216
|
+
sessions[key] = {
|
|
217
|
+
type: normalizedType,
|
|
218
|
+
inputTokens,
|
|
219
|
+
outputTokens,
|
|
220
|
+
totalTokens,
|
|
221
|
+
startTs: prev ? prev.startTs : now,
|
|
222
|
+
endTs: now,
|
|
223
|
+
cwd: cwd || (prev ? prev.cwd : null),
|
|
224
|
+
};
|
|
225
|
+
state.sessions = sessions;
|
|
226
|
+
writeUsageState(statePath, state);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
releaseLock(lockPath, lockFd);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function logProfileUse(config, configPath, profileKey, requestedType, terminalTag, cwd) {
|
|
233
|
+
const profile = config.profiles && config.profiles[profileKey];
|
|
234
|
+
if (!profile)
|
|
235
|
+
return;
|
|
236
|
+
const inferred = (0, type_1.inferProfileType)(profileKey, profile, requestedType);
|
|
237
|
+
if (!inferred)
|
|
238
|
+
return;
|
|
239
|
+
const displayName = (0, type_1.getProfileDisplayName)(profileKey, profile, requestedType || inferred);
|
|
240
|
+
appendProfileLogEntry(config, configPath, profileKey, displayName || profileKey, inferred, terminalTag, cwd, "use", null, null, null);
|
|
241
|
+
}
|
|
242
|
+
function logSessionBinding(config, configPath, profileType, profileKey, profileName, terminalTag, cwd, sessionFile, sessionId, sessionTimestamp) {
|
|
243
|
+
if (!profileKey && !profileName)
|
|
244
|
+
return;
|
|
245
|
+
const key = profileKey ? String(profileKey) : "unknown";
|
|
246
|
+
const name = profileName ? String(profileName) : key;
|
|
247
|
+
appendProfileLogEntry(config, configPath, key, name, profileType, terminalTag, cwd, "session", sessionFile, sessionId, sessionTimestamp);
|
|
248
|
+
}
|
|
249
|
+
function appendProfileLogEntry(config, configPath, profileKey, profileName, profileType, terminalTag, cwd, kind, sessionFile, sessionId, timestamp) {
|
|
250
|
+
const logPath = getProfileLogPath(config, configPath);
|
|
251
|
+
const dir = path.dirname(logPath);
|
|
252
|
+
if (!fs.existsSync(dir)) {
|
|
253
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
const record = {
|
|
256
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
257
|
+
kind,
|
|
258
|
+
profileKey,
|
|
259
|
+
profileName,
|
|
260
|
+
profileType,
|
|
261
|
+
configPath: configPath || null,
|
|
262
|
+
terminalTag: terminalTag || null,
|
|
263
|
+
cwd: cwd || null,
|
|
264
|
+
sessionFile: sessionFile || null,
|
|
265
|
+
sessionId: sessionId || null,
|
|
266
|
+
};
|
|
267
|
+
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
268
|
+
}
|
|
269
|
+
function readProfileLogEntries(paths) {
|
|
270
|
+
var _a;
|
|
271
|
+
const entries = [];
|
|
272
|
+
for (const logPath of paths) {
|
|
273
|
+
if (!logPath || !fs.existsSync(logPath))
|
|
274
|
+
continue;
|
|
275
|
+
const raw = fs.readFileSync(logPath, "utf8");
|
|
276
|
+
const lines = raw.split(/\r?\n/);
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (!trimmed)
|
|
280
|
+
continue;
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(trimmed);
|
|
283
|
+
if (!parsed || typeof parsed !== "object")
|
|
284
|
+
continue;
|
|
285
|
+
const rawKind = parsed.kind ? String(parsed.kind).toLowerCase() : "";
|
|
286
|
+
const kind = rawKind === "session" ? "session" : "use";
|
|
287
|
+
entries.push({
|
|
288
|
+
kind,
|
|
289
|
+
timestamp: String((_a = parsed.timestamp) !== null && _a !== void 0 ? _a : ""),
|
|
290
|
+
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
291
|
+
profileName: parsed.profileName ? String(parsed.profileName) : null,
|
|
292
|
+
profileType: (0, type_1.normalizeType)(parsed.profileType) || null,
|
|
293
|
+
configPath: parsed.configPath ? String(parsed.configPath) : null,
|
|
294
|
+
terminalTag: parsed.terminalTag ? String(parsed.terminalTag) : null,
|
|
295
|
+
cwd: parsed.cwd ? String(parsed.cwd) : null,
|
|
296
|
+
sessionFile: parsed.sessionFile ? String(parsed.sessionFile) : null,
|
|
297
|
+
sessionId: parsed.sessionId ? String(parsed.sessionId) : null,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// ignore invalid lines
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return entries;
|
|
306
|
+
}
|
|
307
|
+
function readSessionBindingIndex(config, configPath) {
|
|
308
|
+
const profileLogPath = getProfileLogPath(config, configPath);
|
|
309
|
+
const entries = readProfileLogEntries([profileLogPath]);
|
|
310
|
+
const byFile = new Set();
|
|
311
|
+
const byId = new Set();
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
if (entry.kind !== "session")
|
|
314
|
+
continue;
|
|
315
|
+
if (entry.sessionFile)
|
|
316
|
+
byFile.add(entry.sessionFile);
|
|
317
|
+
if (entry.sessionId)
|
|
318
|
+
byId.add(entry.sessionId);
|
|
319
|
+
}
|
|
320
|
+
return { byFile, byId };
|
|
321
|
+
}
|
|
322
|
+
function normalizeProfileMatch(config, entry, type) {
|
|
323
|
+
const profileKey = entry.profileKey;
|
|
324
|
+
let profileName = entry.profileName;
|
|
325
|
+
if (profileKey && config.profiles && config.profiles[profileKey]) {
|
|
326
|
+
profileName = (0, type_1.getProfileDisplayName)(profileKey, config.profiles[profileKey], type);
|
|
327
|
+
}
|
|
328
|
+
if (!profileName && profileKey)
|
|
329
|
+
profileName = profileKey;
|
|
330
|
+
return { profileKey, profileName };
|
|
331
|
+
}
|
|
332
|
+
function resolveUniqueProfileMatch(config, entries, type) {
|
|
333
|
+
const uniqueProfiles = new Map();
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
const id = entry.profileKey || entry.profileName || "";
|
|
336
|
+
if (!id)
|
|
337
|
+
continue;
|
|
338
|
+
if (!uniqueProfiles.has(id))
|
|
339
|
+
uniqueProfiles.set(id, entry);
|
|
340
|
+
}
|
|
341
|
+
if (uniqueProfiles.size === 0)
|
|
342
|
+
return { match: null, ambiguous: false };
|
|
343
|
+
if (uniqueProfiles.size !== 1)
|
|
344
|
+
return { match: null, ambiguous: true };
|
|
345
|
+
const best = Array.from(uniqueProfiles.values())[0];
|
|
346
|
+
return { match: normalizeProfileMatch(config, best, type), ambiguous: false };
|
|
347
|
+
}
|
|
348
|
+
function resolveProfileForSession(config, logEntries, type, sessionFile, sessionId) {
|
|
349
|
+
const sessionEntries = logEntries.filter((entry) => entry.kind === "session" && entry.profileType === type);
|
|
350
|
+
if (sessionFile) {
|
|
351
|
+
const matches = sessionEntries.filter((entry) => entry.sessionFile && entry.sessionFile === sessionFile);
|
|
352
|
+
if (matches.length > 0) {
|
|
353
|
+
const resolved = resolveUniqueProfileMatch(config, matches, type);
|
|
354
|
+
if (resolved.match || resolved.ambiguous)
|
|
355
|
+
return resolved;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (sessionId) {
|
|
359
|
+
const matches = sessionEntries.filter((entry) => entry.sessionId && entry.sessionId === sessionId);
|
|
360
|
+
if (matches.length > 0) {
|
|
361
|
+
const resolved = resolveUniqueProfileMatch(config, matches, type);
|
|
362
|
+
if (resolved.match || resolved.ambiguous)
|
|
363
|
+
return resolved;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { match: null, ambiguous: false };
|
|
367
|
+
}
|
|
368
|
+
function readUsageState(statePath) {
|
|
369
|
+
if (!statePath || !fs.existsSync(statePath)) {
|
|
370
|
+
return { version: 1, files: {}, sessions: {} };
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const raw = fs.readFileSync(statePath, "utf8");
|
|
374
|
+
const parsed = JSON.parse(raw);
|
|
375
|
+
if (!parsed || typeof parsed !== "object") {
|
|
376
|
+
return { version: 1, files: {}, sessions: {} };
|
|
377
|
+
}
|
|
378
|
+
const files = parsed.files && typeof parsed.files === "object" ? parsed.files : {};
|
|
379
|
+
const sessions = parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {};
|
|
380
|
+
return { version: 1, files, sessions };
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return { version: 1, files: {}, sessions: {} };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function writeUsageState(statePath, state) {
|
|
387
|
+
const dir = path.dirname(statePath);
|
|
388
|
+
if (!fs.existsSync(dir)) {
|
|
389
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
390
|
+
}
|
|
391
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
392
|
+
}
|
|
393
|
+
function collectSessionFiles(root) {
|
|
394
|
+
if (!root || !fs.existsSync(root))
|
|
395
|
+
return [];
|
|
396
|
+
const files = [];
|
|
397
|
+
const stack = [root];
|
|
398
|
+
while (stack.length > 0) {
|
|
399
|
+
const current = stack.pop();
|
|
400
|
+
if (!current)
|
|
401
|
+
continue;
|
|
402
|
+
let entries = [];
|
|
403
|
+
try {
|
|
404
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
if (entry.name.startsWith("."))
|
|
411
|
+
continue;
|
|
412
|
+
const full = path.join(current, entry.name);
|
|
413
|
+
if (entry.isDirectory()) {
|
|
414
|
+
stack.push(full);
|
|
415
|
+
}
|
|
416
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
417
|
+
files.push(full);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return files;
|
|
422
|
+
}
|
|
423
|
+
function updateMinMaxTs(current, ts) {
|
|
424
|
+
if (!ts)
|
|
425
|
+
return;
|
|
426
|
+
const time = new Date(ts).getTime();
|
|
427
|
+
if (Number.isNaN(time))
|
|
428
|
+
return;
|
|
429
|
+
if (!current.start || new Date(current.start).getTime() > time) {
|
|
430
|
+
current.start = ts;
|
|
431
|
+
}
|
|
432
|
+
if (!current.end || new Date(current.end).getTime() < time) {
|
|
433
|
+
current.end = ts;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function parseCodexSessionFile(filePath) {
|
|
437
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
438
|
+
const lines = raw.split(/\r?\n/);
|
|
439
|
+
let maxTotal = 0;
|
|
440
|
+
let maxInput = 0;
|
|
441
|
+
let maxOutput = 0;
|
|
442
|
+
let hasTotal = false;
|
|
443
|
+
let sumLast = 0;
|
|
444
|
+
let sumLastInput = 0;
|
|
445
|
+
let sumLastOutput = 0;
|
|
446
|
+
const tsRange = { start: null, end: null };
|
|
447
|
+
let cwd = null;
|
|
448
|
+
let sessionId = null;
|
|
449
|
+
for (const line of lines) {
|
|
450
|
+
const trimmed = line.trim();
|
|
451
|
+
if (!trimmed)
|
|
452
|
+
continue;
|
|
453
|
+
try {
|
|
454
|
+
const parsed = JSON.parse(trimmed);
|
|
455
|
+
if (!parsed || typeof parsed !== "object")
|
|
456
|
+
continue;
|
|
457
|
+
if (parsed.timestamp)
|
|
458
|
+
updateMinMaxTs(tsRange, String(parsed.timestamp));
|
|
459
|
+
if (!cwd && parsed.type === "session_meta") {
|
|
460
|
+
const payload = parsed.payload || {};
|
|
461
|
+
if (payload && payload.cwd)
|
|
462
|
+
cwd = String(payload.cwd);
|
|
463
|
+
if (!sessionId && payload && payload.id) {
|
|
464
|
+
sessionId = String(payload.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!cwd && parsed.type === "turn_context") {
|
|
468
|
+
const payload = parsed.payload || {};
|
|
469
|
+
if (payload && payload.cwd)
|
|
470
|
+
cwd = String(payload.cwd);
|
|
471
|
+
}
|
|
472
|
+
if (parsed.type !== "event_msg")
|
|
473
|
+
continue;
|
|
474
|
+
const payload = parsed.payload;
|
|
475
|
+
if (!payload || payload.type !== "token_count")
|
|
476
|
+
continue;
|
|
477
|
+
const info = payload.info || {};
|
|
478
|
+
const totalUsage = info.total_token_usage || {};
|
|
479
|
+
const lastUsage = info.last_token_usage || {};
|
|
480
|
+
const totalTokens = Number(totalUsage.total_tokens);
|
|
481
|
+
if (Number.isFinite(totalTokens)) {
|
|
482
|
+
hasTotal = true;
|
|
483
|
+
if (totalTokens > maxTotal)
|
|
484
|
+
maxTotal = totalTokens;
|
|
485
|
+
const totalInput = Number(totalUsage.input_tokens);
|
|
486
|
+
const totalOutput = Number(totalUsage.output_tokens);
|
|
487
|
+
if (Number.isFinite(totalInput) && totalInput > maxInput) {
|
|
488
|
+
maxInput = totalInput;
|
|
489
|
+
}
|
|
490
|
+
if (Number.isFinite(totalOutput) && totalOutput > maxOutput) {
|
|
491
|
+
maxOutput = totalOutput;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const lastTokens = Number(lastUsage.total_tokens);
|
|
496
|
+
if (Number.isFinite(lastTokens))
|
|
497
|
+
sumLast += lastTokens;
|
|
498
|
+
const lastInput = Number(lastUsage.input_tokens);
|
|
499
|
+
const lastOutput = Number(lastUsage.output_tokens);
|
|
500
|
+
if (Number.isFinite(lastInput))
|
|
501
|
+
sumLastInput += lastInput;
|
|
502
|
+
if (Number.isFinite(lastOutput))
|
|
503
|
+
sumLastOutput += lastOutput;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// ignore invalid lines
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (!hasTotal) {
|
|
511
|
+
maxTotal = sumLast;
|
|
512
|
+
maxInput = sumLastInput;
|
|
513
|
+
maxOutput = sumLastOutput;
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
inputTokens: maxInput,
|
|
517
|
+
outputTokens: maxOutput,
|
|
518
|
+
totalTokens: maxTotal,
|
|
519
|
+
startTs: tsRange.start,
|
|
520
|
+
endTs: tsRange.end,
|
|
521
|
+
cwd,
|
|
522
|
+
sessionId,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function parseClaudeSessionFile(filePath) {
|
|
526
|
+
var _a, _b, _c, _d;
|
|
527
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
528
|
+
const lines = raw.split(/\r?\n/);
|
|
529
|
+
let totalTokens = 0;
|
|
530
|
+
let inputTokens = 0;
|
|
531
|
+
let outputTokens = 0;
|
|
532
|
+
const tsRange = { start: null, end: null };
|
|
533
|
+
let cwd = null;
|
|
534
|
+
let sessionId = null;
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
const trimmed = line.trim();
|
|
537
|
+
if (!trimmed)
|
|
538
|
+
continue;
|
|
539
|
+
try {
|
|
540
|
+
const parsed = JSON.parse(trimmed);
|
|
541
|
+
if (!parsed || typeof parsed !== "object")
|
|
542
|
+
continue;
|
|
543
|
+
if (parsed.timestamp)
|
|
544
|
+
updateMinMaxTs(tsRange, String(parsed.timestamp));
|
|
545
|
+
if (!cwd && parsed.cwd)
|
|
546
|
+
cwd = String(parsed.cwd);
|
|
547
|
+
if (!sessionId && parsed.sessionId) {
|
|
548
|
+
sessionId = String(parsed.sessionId);
|
|
549
|
+
}
|
|
550
|
+
const message = parsed.message;
|
|
551
|
+
const usage = message && message.usage ? message.usage : null;
|
|
552
|
+
if (!usage)
|
|
553
|
+
continue;
|
|
554
|
+
const input = Number((_a = usage.input_tokens) !== null && _a !== void 0 ? _a : 0);
|
|
555
|
+
const output = Number((_b = usage.output_tokens) !== null && _b !== void 0 ? _b : 0);
|
|
556
|
+
const cacheCreate = Number((_c = usage.cache_creation_input_tokens) !== null && _c !== void 0 ? _c : 0);
|
|
557
|
+
const cacheRead = Number((_d = usage.cache_read_input_tokens) !== null && _d !== void 0 ? _d : 0);
|
|
558
|
+
if (Number.isFinite(input))
|
|
559
|
+
inputTokens += input;
|
|
560
|
+
if (Number.isFinite(output))
|
|
561
|
+
outputTokens += output;
|
|
562
|
+
totalTokens +=
|
|
563
|
+
(Number.isFinite(input) ? input : 0) +
|
|
564
|
+
(Number.isFinite(output) ? output : 0) +
|
|
565
|
+
(Number.isFinite(cacheCreate) ? cacheCreate : 0) +
|
|
566
|
+
(Number.isFinite(cacheRead) ? cacheRead : 0);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// ignore invalid lines
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
inputTokens,
|
|
574
|
+
outputTokens,
|
|
575
|
+
totalTokens,
|
|
576
|
+
startTs: tsRange.start,
|
|
577
|
+
endTs: tsRange.end,
|
|
578
|
+
cwd,
|
|
579
|
+
sessionId,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const LOCK_STALE_MS = 10 * 60 * 1000;
|
|
583
|
+
function isProcessAlive(pid) {
|
|
584
|
+
try {
|
|
585
|
+
process.kill(pid, 0);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
const code = err === null || err === void 0 ? void 0 : err.code;
|
|
590
|
+
return code === "EPERM";
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function readLockInfo(lockPath) {
|
|
594
|
+
try {
|
|
595
|
+
const raw = fs.readFileSync(lockPath, "utf8");
|
|
596
|
+
const lines = raw.split(/\r?\n/);
|
|
597
|
+
const pid = Number(lines[0] || "");
|
|
598
|
+
const ts = lines[1] ? new Date(lines[1]).getTime() : Number.NaN;
|
|
599
|
+
return {
|
|
600
|
+
pid: Number.isFinite(pid) && pid > 0 ? pid : null,
|
|
601
|
+
timestampMs: Number.isFinite(ts) ? ts : null,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return { pid: null, timestampMs: null };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function isLockStale(lockPath) {
|
|
609
|
+
const info = readLockInfo(lockPath);
|
|
610
|
+
if (info.pid !== null) {
|
|
611
|
+
return !isProcessAlive(info.pid);
|
|
612
|
+
}
|
|
613
|
+
if (info.timestampMs !== null) {
|
|
614
|
+
return Date.now() - info.timestampMs > LOCK_STALE_MS;
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
function acquireLock(lockPath) {
|
|
619
|
+
const dir = path.dirname(lockPath);
|
|
620
|
+
if (!fs.existsSync(dir)) {
|
|
621
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
622
|
+
}
|
|
623
|
+
const attemptAcquire = () => {
|
|
624
|
+
try {
|
|
625
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
626
|
+
fs.writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
|
|
627
|
+
return fd;
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
const code = err === null || err === void 0 ? void 0 : err.code;
|
|
631
|
+
if (code !== "EEXIST")
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
};
|
|
636
|
+
const fd = attemptAcquire();
|
|
637
|
+
if (fd !== null)
|
|
638
|
+
return fd;
|
|
639
|
+
if (!isLockStale(lockPath))
|
|
640
|
+
return null;
|
|
641
|
+
try {
|
|
642
|
+
fs.unlinkSync(lockPath);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return attemptAcquire();
|
|
648
|
+
}
|
|
649
|
+
function releaseLock(lockPath, fd) {
|
|
650
|
+
if (fd === null)
|
|
651
|
+
return;
|
|
652
|
+
try {
|
|
653
|
+
fs.closeSync(fd);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// ignore
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
fs.unlinkSync(lockPath);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// ignore
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function appendUsageRecord(usagePath, record) {
|
|
666
|
+
const dir = path.dirname(usagePath);
|
|
667
|
+
if (!fs.existsSync(dir)) {
|
|
668
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
669
|
+
}
|
|
670
|
+
fs.appendFileSync(usagePath, `${JSON.stringify(record)}\n`, "utf8");
|
|
671
|
+
}
|
|
672
|
+
function readUsageRecords(usagePath) {
|
|
673
|
+
var _a, _b, _c, _d, _e;
|
|
674
|
+
if (!usagePath || !fs.existsSync(usagePath))
|
|
675
|
+
return [];
|
|
676
|
+
const raw = fs.readFileSync(usagePath, "utf8");
|
|
677
|
+
const lines = raw.split(/\r?\n/);
|
|
678
|
+
const records = [];
|
|
679
|
+
for (const line of lines) {
|
|
680
|
+
const trimmed = line.trim();
|
|
681
|
+
if (!trimmed)
|
|
682
|
+
continue;
|
|
683
|
+
try {
|
|
684
|
+
const parsed = JSON.parse(trimmed);
|
|
685
|
+
if (!parsed || typeof parsed !== "object")
|
|
686
|
+
continue;
|
|
687
|
+
const input = Number((_a = parsed.inputTokens) !== null && _a !== void 0 ? _a : 0);
|
|
688
|
+
const output = Number((_b = parsed.outputTokens) !== null && _b !== void 0 ? _b : 0);
|
|
689
|
+
const total = Number((_c = parsed.totalTokens) !== null && _c !== void 0 ? _c : input + output);
|
|
690
|
+
const type = (0, type_1.normalizeType)(parsed.type) || String((_d = parsed.type) !== null && _d !== void 0 ? _d : "unknown");
|
|
691
|
+
records.push({
|
|
692
|
+
ts: String((_e = parsed.ts) !== null && _e !== void 0 ? _e : ""),
|
|
693
|
+
type,
|
|
694
|
+
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
695
|
+
profileName: parsed.profileName ? String(parsed.profileName) : null,
|
|
696
|
+
inputTokens: Number.isFinite(input) ? input : 0,
|
|
697
|
+
outputTokens: Number.isFinite(output) ? output : 0,
|
|
698
|
+
totalTokens: Number.isFinite(total) ? total : 0,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// ignore invalid lines
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return records;
|
|
706
|
+
}
|
|
707
|
+
function syncUsageFromSessions(config, configPath, usagePath) {
|
|
708
|
+
const statePath = getUsageStatePath(usagePath, config);
|
|
709
|
+
const lockPath = `${statePath}.lock`;
|
|
710
|
+
const lockFd = acquireLock(lockPath);
|
|
711
|
+
if (lockFd === null)
|
|
712
|
+
return;
|
|
713
|
+
try {
|
|
714
|
+
const profileLogPath = getProfileLogPath(config, configPath);
|
|
715
|
+
const logEntries = readProfileLogEntries([profileLogPath]);
|
|
716
|
+
const state = readUsageState(statePath);
|
|
717
|
+
const files = state.files || {};
|
|
718
|
+
const sessions = state.sessions || {};
|
|
719
|
+
const codexFiles = collectSessionFiles(getCodexSessionsPath(config));
|
|
720
|
+
const claudeFiles = collectSessionFiles(getClaudeSessionsPath(config));
|
|
721
|
+
const processFile = (filePath, type) => {
|
|
722
|
+
let stat = null;
|
|
723
|
+
try {
|
|
724
|
+
stat = fs.statSync(filePath);
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (!stat || !stat.isFile())
|
|
730
|
+
return;
|
|
731
|
+
const prev = files[filePath];
|
|
732
|
+
if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
let stats;
|
|
736
|
+
try {
|
|
737
|
+
stats =
|
|
738
|
+
type === "codex"
|
|
739
|
+
? parseCodexSessionFile(filePath)
|
|
740
|
+
: parseClaudeSessionFile(filePath);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const resolved = resolveProfileForSession(config, logEntries, type, filePath, stats.sessionId);
|
|
746
|
+
if (!resolved.match)
|
|
747
|
+
return;
|
|
748
|
+
const sessionKey = stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
|
|
749
|
+
const sessionPrev = sessionKey ? sessions[sessionKey] : null;
|
|
750
|
+
const prevInput = prev ? prev.inputTokens : 0;
|
|
751
|
+
const prevOutput = prev ? prev.outputTokens : 0;
|
|
752
|
+
const prevTotal = prev ? prev.totalTokens : 0;
|
|
753
|
+
const prevInputMax = sessionPrev
|
|
754
|
+
? Math.max(prevInput, sessionPrev.inputTokens)
|
|
755
|
+
: prevInput;
|
|
756
|
+
const prevOutputMax = sessionPrev
|
|
757
|
+
? Math.max(prevOutput, sessionPrev.outputTokens)
|
|
758
|
+
: prevOutput;
|
|
759
|
+
const prevTotalMax = sessionPrev
|
|
760
|
+
? Math.max(prevTotal, sessionPrev.totalTokens)
|
|
761
|
+
: prevTotal;
|
|
762
|
+
let deltaInput = stats.inputTokens - prevInputMax;
|
|
763
|
+
let deltaOutput = stats.outputTokens - prevOutputMax;
|
|
764
|
+
let deltaTotal = stats.totalTokens - prevTotalMax;
|
|
765
|
+
if (deltaTotal < 0 || deltaInput < 0 || deltaOutput < 0) {
|
|
766
|
+
if (sessionPrev) {
|
|
767
|
+
deltaInput = 0;
|
|
768
|
+
deltaOutput = 0;
|
|
769
|
+
deltaTotal = 0;
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
deltaInput = stats.inputTokens;
|
|
773
|
+
deltaOutput = stats.outputTokens;
|
|
774
|
+
deltaTotal = stats.totalTokens;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (deltaTotal > 0) {
|
|
778
|
+
const record = {
|
|
779
|
+
ts: stats.endTs || stats.startTs || new Date().toISOString(),
|
|
780
|
+
type,
|
|
781
|
+
profileKey: resolved.match.profileKey,
|
|
782
|
+
profileName: resolved.match.profileName,
|
|
783
|
+
inputTokens: deltaInput,
|
|
784
|
+
outputTokens: deltaOutput,
|
|
785
|
+
totalTokens: deltaTotal,
|
|
786
|
+
};
|
|
787
|
+
appendUsageRecord(usagePath, record);
|
|
788
|
+
}
|
|
789
|
+
if (sessionKey) {
|
|
790
|
+
const nextInput = sessionPrev
|
|
791
|
+
? Math.max(sessionPrev.inputTokens, stats.inputTokens)
|
|
792
|
+
: stats.inputTokens;
|
|
793
|
+
const nextOutput = sessionPrev
|
|
794
|
+
? Math.max(sessionPrev.outputTokens, stats.outputTokens)
|
|
795
|
+
: stats.outputTokens;
|
|
796
|
+
const nextTotal = sessionPrev
|
|
797
|
+
? Math.max(sessionPrev.totalTokens, stats.totalTokens)
|
|
798
|
+
: stats.totalTokens;
|
|
799
|
+
sessions[sessionKey] = {
|
|
800
|
+
type,
|
|
801
|
+
inputTokens: nextInput,
|
|
802
|
+
outputTokens: nextOutput,
|
|
803
|
+
totalTokens: nextTotal,
|
|
804
|
+
startTs: sessionPrev ? sessionPrev.startTs : stats.startTs,
|
|
805
|
+
endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
|
|
806
|
+
cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
files[filePath] = {
|
|
810
|
+
mtimeMs: stat.mtimeMs,
|
|
811
|
+
size: stat.size,
|
|
812
|
+
type,
|
|
813
|
+
inputTokens: stats.inputTokens,
|
|
814
|
+
outputTokens: stats.outputTokens,
|
|
815
|
+
totalTokens: stats.totalTokens,
|
|
816
|
+
startTs: stats.startTs,
|
|
817
|
+
endTs: stats.endTs,
|
|
818
|
+
cwd: stats.cwd,
|
|
819
|
+
};
|
|
820
|
+
};
|
|
821
|
+
for (const filePath of codexFiles)
|
|
822
|
+
processFile(filePath, "codex");
|
|
823
|
+
for (const filePath of claudeFiles)
|
|
824
|
+
processFile(filePath, "claude");
|
|
825
|
+
state.files = files;
|
|
826
|
+
state.sessions = sessions;
|
|
827
|
+
writeUsageState(statePath, state);
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
releaseLock(lockPath, lockFd);
|
|
831
|
+
}
|
|
832
|
+
}
|