usagemax 0.1.1 → 0.3.0
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 +95 -21
- package/package.json +3 -1
- package/src/archives.js +86 -0
- package/src/cli.js +261 -132
- package/src/core.js +226 -2
- package/src/sources.js +495 -0
package/src/sources.js
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const CCUSAGE_VERSION = "20.0.20";
|
|
7
|
+
export const SOURCE_INVENTORY_VERSION = 3;
|
|
8
|
+
export const SUPPORTED_SOURCES = [
|
|
9
|
+
"amp",
|
|
10
|
+
"claude",
|
|
11
|
+
"codebuff",
|
|
12
|
+
"codex",
|
|
13
|
+
"copilot",
|
|
14
|
+
"droid",
|
|
15
|
+
"gemini",
|
|
16
|
+
"goose",
|
|
17
|
+
"grok",
|
|
18
|
+
"hermes",
|
|
19
|
+
"kilo",
|
|
20
|
+
"kimi",
|
|
21
|
+
"openclaw",
|
|
22
|
+
"opencode",
|
|
23
|
+
"pi",
|
|
24
|
+
"qwen",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const MAX_FINGERPRINT_FILES = 50_000;
|
|
28
|
+
const WINDOWS_SYSTEM_PROFILES = /^(?:all users|default(?: user)?|defaultuser0|public|temp(?:\.|$)|umfd-)/i;
|
|
29
|
+
const BACKUP_DIRECTORY = /(?:claude|codex).*(?:backup|archive|old|copy|mirror)|(?:backup|archive|old|copy|mirror).*(?:claude|codex)|superclaude/i;
|
|
30
|
+
const ARCHIVE_EXTENSION = /(?:\.tar(?:\.gz)?|\.tgz|\.zip|\.7z)$/i;
|
|
31
|
+
const ARCHIVE_FILE = /(?:claude|codex).*(?:\.tar(?:\.gz)?|\.tgz|\.zip|\.7z)$/i;
|
|
32
|
+
|
|
33
|
+
function has(env, name) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(env, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commaList(value) {
|
|
38
|
+
return String(value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function envList(env, name, fallback, { blankUsesFallback = false } = {}) {
|
|
42
|
+
if (!has(env, name) || (blankUsesFallback && !String(env[name] ?? "").trim())) return fallback;
|
|
43
|
+
return commaList(env[name]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function expandTilde(value, home, pathApi) {
|
|
47
|
+
if (value === "~") return home;
|
|
48
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return pathApi.join(home, value.slice(2));
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function uniquePaths(values, pathApi) {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
return values.filter((value) => {
|
|
55
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
56
|
+
const normalized = pathApi.normalize(value.trim());
|
|
57
|
+
if (seen.has(normalized)) return false;
|
|
58
|
+
seen.add(normalized);
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function entries(value) {
|
|
64
|
+
try {
|
|
65
|
+
return await readdir(value, { withFileTypes: true });
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function providerBearingHome(home, pathApi) {
|
|
72
|
+
const markers = [".claude", ".codex", ".factory", ".gemini", ".openclaw", ".hermes", ".grok"];
|
|
73
|
+
const checks = await Promise.all(markers.map((marker) => existsDirectory(pathApi.join(home, marker))));
|
|
74
|
+
return checks.some(Boolean);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function discoverWslWindowsHomes(env, platform, pathApi) {
|
|
78
|
+
if (platform !== "linux" || !String(env.WSL_DISTRO_NAME ?? "").trim()) return [];
|
|
79
|
+
const usersRoot = String(env.USAGEMAX_WSL_USERS_DIR ?? "/mnt/c/Users").trim();
|
|
80
|
+
const candidates = [];
|
|
81
|
+
for (const entry of await entries(usersRoot)) {
|
|
82
|
+
if (!entry.isDirectory() || WINDOWS_SYSTEM_PROFILES.test(entry.name)) continue;
|
|
83
|
+
const candidate = pathApi.join(usersRoot, entry.name);
|
|
84
|
+
if (await providerBearingHome(candidate, pathApi)) candidates.push(candidate);
|
|
85
|
+
}
|
|
86
|
+
// A WSL distro can see every Windows profile. Auto-select only when there is
|
|
87
|
+
// one unambiguous provider-bearing profile; multi-user systems opt in with
|
|
88
|
+
// USAGEMAX_ADDITIONAL_HOME so one employee never absorbs another's usage.
|
|
89
|
+
return candidates.length === 1 ? candidates : [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function discoverBackupRoots(home, pathApi) {
|
|
93
|
+
const claude = [];
|
|
94
|
+
const codex = [];
|
|
95
|
+
const containers = [
|
|
96
|
+
{ path: home, requireProviderName: true },
|
|
97
|
+
{ path: pathApi.join(home, ".claude", "backups"), requireProviderName: false },
|
|
98
|
+
{ path: pathApi.join(home, ".codex", "backups"), requireProviderName: false },
|
|
99
|
+
];
|
|
100
|
+
for (const container of containers) {
|
|
101
|
+
for (const entry of await entries(container.path)) {
|
|
102
|
+
if (!entry.isDirectory() || (container.requireProviderName && !BACKUP_DIRECTORY.test(entry.name))) continue;
|
|
103
|
+
const candidate = pathApi.join(container.path, entry.name);
|
|
104
|
+
const claudeRoots = [candidate, pathApi.join(candidate, ".claude"), pathApi.join(candidate, "config")];
|
|
105
|
+
for (const root of claudeRoots) {
|
|
106
|
+
if (await existsDirectory(pathApi.join(root, "projects"))) claude.push(root);
|
|
107
|
+
}
|
|
108
|
+
const codexRoots = [candidate, pathApi.join(candidate, ".codex")];
|
|
109
|
+
for (const root of codexRoots) {
|
|
110
|
+
if (await existsDirectory(pathApi.join(root, "sessions")) || await existsDirectory(pathApi.join(root, "archived_sessions"))) codex.push(root);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { claude, codex };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function discoverNestedClaudeRoots(home, pathApi, maxDirectories = 4_096) {
|
|
118
|
+
const bases = [
|
|
119
|
+
pathApi.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions"),
|
|
120
|
+
pathApi.join(home, "AppData", "Roaming", "Claude", "local-agent-mode-sessions"),
|
|
121
|
+
];
|
|
122
|
+
const roots = [];
|
|
123
|
+
for (const base of bases) {
|
|
124
|
+
if (!await existsDirectory(base)) continue;
|
|
125
|
+
const stack = [{ directory: base, depth: 0 }];
|
|
126
|
+
let visited = 0;
|
|
127
|
+
while (stack.length && visited < maxDirectories) {
|
|
128
|
+
const { directory, depth } = stack.pop();
|
|
129
|
+
visited += 1;
|
|
130
|
+
for (const entry of await entries(directory)) {
|
|
131
|
+
if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git") continue;
|
|
132
|
+
const child = pathApi.join(directory, entry.name);
|
|
133
|
+
if (entry.name === ".claude") {
|
|
134
|
+
if (await existsDirectory(pathApi.join(child, "projects"))) roots.push(child);
|
|
135
|
+
} else if (depth < 7) {
|
|
136
|
+
stack.push({ directory: child, depth: depth + 1 });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return roots;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setDiscoveredList(effective, name, defaults, additions, pathApi) {
|
|
145
|
+
if (has(effective, name) && !String(effective[name] ?? "").trim()) return;
|
|
146
|
+
const configured = has(effective, name) ? commaList(effective[name]) : defaults;
|
|
147
|
+
effective[name] = uniquePaths([...configured, ...additions], pathApi).join(",");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function discoverProviderArchives({ env = process.env, home = ccusageHome(env), pathApi = path } = {}) {
|
|
151
|
+
const homes = uniquePaths([home, ...commaList(env.USAGEMAX_DISCOVERED_HOMES), ...commaList(env.USAGEMAX_ADDITIONAL_HOME)], pathApi);
|
|
152
|
+
const archives = [];
|
|
153
|
+
for (const candidateHome of homes) {
|
|
154
|
+
const roots = [
|
|
155
|
+
{ path: candidateHome, source: null },
|
|
156
|
+
{ path: pathApi.join(candidateHome, ".claude", "backups"), source: "claude" },
|
|
157
|
+
{ path: pathApi.join(candidateHome, ".codex", "backups"), source: "codex" },
|
|
158
|
+
];
|
|
159
|
+
for (const root of roots) {
|
|
160
|
+
for (const entry of await entries(root.path)) {
|
|
161
|
+
if (!entry.isFile() || !ARCHIVE_EXTENSION.test(entry.name) || (!root.source && !ARCHIVE_FILE.test(entry.name))) continue;
|
|
162
|
+
const filePath = pathApi.join(root.path, entry.name);
|
|
163
|
+
try {
|
|
164
|
+
const metadata = await stat(filePath);
|
|
165
|
+
archives.push({ path: filePath, size: metadata.size, source: root.source || (/codex/i.test(entry.name) ? "codex" : "claude") });
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return archives.filter((archive, index) => archives.findIndex((item) => item.path === archive.path) === index);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function ccusageHome(env = process.env, fallback = homedir()) {
|
|
174
|
+
for (const value of [env.HOME, env.USERPROFILE]) {
|
|
175
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
176
|
+
}
|
|
177
|
+
const windowsHome = `${env.HOMEDRIVE ?? ""}${env.HOMEPATH ?? ""}`;
|
|
178
|
+
return windowsHome.trim() || fallback;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function tree(source, root, matcher) {
|
|
182
|
+
return { source, path: root, kind: "tree", matcher };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function file(source, filePath) {
|
|
186
|
+
return { source, path: filePath, kind: "file", matcher: "any" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function db(source, filePath) {
|
|
190
|
+
return [file(source, filePath), file(source, `${filePath}-wal`), file(source, `${filePath}-shm`)];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function sourceDefinitions({ env = process.env, home = ccusageHome(env), cwd = process.cwd(), pathApi = path } = {}) {
|
|
194
|
+
const join = (...parts) => pathApi.join(...parts);
|
|
195
|
+
const definitions = [];
|
|
196
|
+
|
|
197
|
+
const claudeRoots = has(env, "CLAUDE_CONFIG_DIR")
|
|
198
|
+
? commaList(env.CLAUDE_CONFIG_DIR).map((item) => expandTilde(item, home, pathApi))
|
|
199
|
+
: [join(has(env, "XDG_CONFIG_HOME") ? String(env.XDG_CONFIG_HOME) : join(home, ".config"), "claude"), join(home, ".claude")];
|
|
200
|
+
for (const root of claudeRoots) {
|
|
201
|
+
const projects = pathApi.basename(root) === "projects" ? root : join(root, "projects");
|
|
202
|
+
definitions.push(tree("claude", projects, "jsonl"));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const root of envList(env, "CODEX_HOME", [join(home, ".codex")])) {
|
|
206
|
+
definitions.push({ source: "codex", path: root, kind: "codex-home", matcher: "jsonl" });
|
|
207
|
+
}
|
|
208
|
+
for (const root of envList(env, "OPENCODE_DATA_DIR", [join(home, ".local", "share", "opencode")])) {
|
|
209
|
+
definitions.push(tree("opencode", root, "opencode"));
|
|
210
|
+
}
|
|
211
|
+
for (const root of envList(env, "AMP_DATA_DIR", [join(home, ".local", "share", "amp")])) {
|
|
212
|
+
definitions.push(tree("amp", join(root, "threads"), "json"));
|
|
213
|
+
}
|
|
214
|
+
for (const root of envList(env, "DROID_SESSIONS_DIR", [join(home, ".factory", "sessions")])) {
|
|
215
|
+
definitions.push(tree("droid", root, "droid"));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const codebuffRoots = has(env, "CODEBUFF_DATA_DIR")
|
|
219
|
+
? commaList(env.CODEBUFF_DATA_DIR)
|
|
220
|
+
: ["manicode", "manicode-dev", "manicode-staging"].map((channel) => join(home, ".config", channel));
|
|
221
|
+
for (const root of codebuffRoots) {
|
|
222
|
+
definitions.push(tree("codebuff", pathApi.basename(root) === "projects" ? root : join(root, "projects"), "codebuff"));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const root of envList(env, "HERMES_HOME", [join(home, ".hermes")])) {
|
|
226
|
+
definitions.push(...db("hermes", join(root, "state.db")));
|
|
227
|
+
}
|
|
228
|
+
for (const root of envList(env, "PI_AGENT_DIR", [join(home, ".pi", "agent", "sessions")], { blankUsesFallback: true })) {
|
|
229
|
+
definitions.push(tree("pi", root, "jsonl"));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const gooseRoot = String(env.GOOSE_PATH_ROOT ?? "").trim();
|
|
233
|
+
const gooseDatabases = gooseRoot
|
|
234
|
+
? [join(gooseRoot, "data", "sessions", "sessions.db")]
|
|
235
|
+
: [
|
|
236
|
+
join(home, ".local", "share", "goose", "sessions", "sessions.db"),
|
|
237
|
+
join(home, "Library", "Application Support", "goose", "sessions", "sessions.db"),
|
|
238
|
+
join(home, ".local", "share", "Block", "goose", "sessions", "sessions.db"),
|
|
239
|
+
];
|
|
240
|
+
if (!gooseRoot && String(env.APPDATA ?? "").trim()) {
|
|
241
|
+
gooseDatabases.push(join(String(env.APPDATA).trim(), "Block", "goose", "data", "sessions", "sessions.db"));
|
|
242
|
+
}
|
|
243
|
+
for (const database of gooseDatabases) definitions.push(...db("goose", database));
|
|
244
|
+
|
|
245
|
+
const openClawRoots = envList(env, "OPENCLAW_DIR", [
|
|
246
|
+
join(home, ".openclaw"),
|
|
247
|
+
join(home, ".clawdbot"),
|
|
248
|
+
join(home, ".moltbot"),
|
|
249
|
+
join(home, ".moldbot"),
|
|
250
|
+
], { blankUsesFallback: true });
|
|
251
|
+
for (const root of openClawRoots) definitions.push(tree("openclaw", root, "openclaw"));
|
|
252
|
+
|
|
253
|
+
for (const root of envList(env, "KILO_DATA_DIR", [join(home, ".local", "share", "kilo")])) {
|
|
254
|
+
definitions.push(...db("kilo", join(root, "kilo.db")));
|
|
255
|
+
}
|
|
256
|
+
for (const root of envList(env, "KIMI_DATA_DIR", [join(home, ".kimi"), join(home, ".kimi-code")])) {
|
|
257
|
+
definitions.push(tree("kimi", join(root, "sessions"), "jsonl"), file("kimi", join(root, "config.json")));
|
|
258
|
+
}
|
|
259
|
+
for (const root of envList(env, "QWEN_DATA_DIR", [join(home, ".qwen")])) {
|
|
260
|
+
definitions.push(tree("qwen", join(root, "projects"), "jsonl"));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
definitions.push(tree("copilot", join(home, ".copilot", "otel"), "jsonl"));
|
|
264
|
+
if (String(env.COPILOT_OTEL_FILE_EXPORTER_PATH ?? "").trim()) {
|
|
265
|
+
definitions.push(file("copilot", String(env.COPILOT_OTEL_FILE_EXPORTER_PATH).trim()));
|
|
266
|
+
}
|
|
267
|
+
for (const root of envList(env, "GEMINI_DATA_DIR", [join(home, ".gemini", "tmp")])) {
|
|
268
|
+
definitions.push(tree("gemini", root, "gemini"));
|
|
269
|
+
}
|
|
270
|
+
const grokRoot = String(env.GROK_HOME ?? "").trim() || join(home, ".grok");
|
|
271
|
+
definitions.push(tree("grok", join(grokRoot, "sessions"), "grok"));
|
|
272
|
+
|
|
273
|
+
const configRoots = has(env, "CLAUDE_CONFIG_DIR")
|
|
274
|
+
? commaList(env.CLAUDE_CONFIG_DIR)
|
|
275
|
+
: [join(home, ".config", "claude"), join(home, ".claude")];
|
|
276
|
+
const configPaths = [join(cwd, ".ccusage", "ccusage.json"), ...configRoots.map((root) => join(root, "ccusage.json"))];
|
|
277
|
+
for (const configPath of configPaths) definitions.push(file("ccusage-config", configPath));
|
|
278
|
+
|
|
279
|
+
return definitions;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function ccusageEnvironment({ env = process.env, platform = process.platform, pathApi = path } = {}) {
|
|
283
|
+
const effective = { ...env };
|
|
284
|
+
const home = ccusageHome(effective);
|
|
285
|
+
const configuredHomes = commaList(effective.USAGEMAX_ADDITIONAL_HOME).map((item) => expandTilde(item, home, pathApi));
|
|
286
|
+
const wslHomes = await discoverWslWindowsHomes(effective, platform, pathApi);
|
|
287
|
+
const additionalHomes = uniquePaths([...configuredHomes, ...wslHomes], pathApi).filter((item) => pathApi.normalize(item) !== pathApi.normalize(home));
|
|
288
|
+
const homes = [home, ...additionalHomes];
|
|
289
|
+
const backups = await Promise.all(homes.map((candidate) => discoverBackupRoots(candidate, pathApi)));
|
|
290
|
+
const nestedClaude = (await Promise.all(homes.map((candidate) => discoverNestedClaudeRoots(candidate, pathApi)))).flat();
|
|
291
|
+
const mirroredClaude = [];
|
|
292
|
+
for (const candidate of homes) {
|
|
293
|
+
const mirror = pathApi.join(candidate, ".cc-mirror", "mclaude", "config");
|
|
294
|
+
if (await existsDirectory(pathApi.join(mirror, "projects"))) mirroredClaude.push(mirror);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const xdgClaude = pathApi.join(has(effective, "XDG_CONFIG_HOME") ? String(effective.XDG_CONFIG_HOME) : pathApi.join(home, ".config"), "claude");
|
|
298
|
+
setDiscoveredList(effective, "CLAUDE_CONFIG_DIR", [xdgClaude, pathApi.join(home, ".claude")], [
|
|
299
|
+
...additionalHomes.flatMap((candidate) => [pathApi.join(candidate, ".config", "claude"), pathApi.join(candidate, ".claude")]),
|
|
300
|
+
...mirroredClaude,
|
|
301
|
+
...backups.flatMap((item) => item.claude),
|
|
302
|
+
...nestedClaude,
|
|
303
|
+
], pathApi);
|
|
304
|
+
setDiscoveredList(effective, "CODEX_HOME", [pathApi.join(home, ".codex")], [
|
|
305
|
+
...additionalHomes.map((candidate) => pathApi.join(candidate, ".codex")),
|
|
306
|
+
...backups.flatMap((item) => item.codex),
|
|
307
|
+
], pathApi);
|
|
308
|
+
|
|
309
|
+
const additions = (segments) => additionalHomes.map((candidate) => pathApi.join(candidate, ...segments));
|
|
310
|
+
setDiscoveredList(effective, "OPENCODE_DATA_DIR", [pathApi.join(home, ".local", "share", "opencode")], additions([".local", "share", "opencode"]), pathApi);
|
|
311
|
+
setDiscoveredList(effective, "AMP_DATA_DIR", [pathApi.join(home, ".local", "share", "amp")], additions([".local", "share", "amp"]), pathApi);
|
|
312
|
+
setDiscoveredList(effective, "DROID_SESSIONS_DIR", [pathApi.join(home, ".factory", "sessions")], additions([".factory", "sessions"]), pathApi);
|
|
313
|
+
setDiscoveredList(effective, "CODEBUFF_DATA_DIR", ["manicode", "manicode-dev", "manicode-staging"].map((channel) => pathApi.join(home, ".config", channel)), additionalHomes.flatMap((candidate) => ["manicode", "manicode-dev", "manicode-staging"].map((channel) => pathApi.join(candidate, ".config", channel))), pathApi);
|
|
314
|
+
setDiscoveredList(effective, "HERMES_HOME", [pathApi.join(home, ".hermes")], additions([".hermes"]), pathApi);
|
|
315
|
+
setDiscoveredList(effective, "PI_AGENT_DIR", [pathApi.join(home, ".pi", "agent", "sessions")], additions([".pi", "agent", "sessions"]), pathApi);
|
|
316
|
+
setDiscoveredList(effective, "OPENCLAW_DIR", [".openclaw", ".clawdbot", ".moltbot", ".moldbot"].map((name) => pathApi.join(home, name)), additionalHomes.flatMap((candidate) => [".openclaw", ".clawdbot", ".moltbot", ".moldbot"].map((name) => pathApi.join(candidate, name))), pathApi);
|
|
317
|
+
setDiscoveredList(effective, "KILO_DATA_DIR", [pathApi.join(home, ".local", "share", "kilo")], additions([".local", "share", "kilo"]), pathApi);
|
|
318
|
+
setDiscoveredList(effective, "KIMI_DATA_DIR", [pathApi.join(home, ".kimi"), pathApi.join(home, ".kimi-code")], additionalHomes.flatMap((candidate) => [pathApi.join(candidate, ".kimi"), pathApi.join(candidate, ".kimi-code")]), pathApi);
|
|
319
|
+
setDiscoveredList(effective, "QWEN_DATA_DIR", [pathApi.join(home, ".qwen")], additions([".qwen"]), pathApi);
|
|
320
|
+
setDiscoveredList(effective, "GEMINI_DATA_DIR", [pathApi.join(home, ".gemini", "tmp")], additions([".gemini", "tmp"]), pathApi);
|
|
321
|
+
|
|
322
|
+
effective.USAGEMAX_DISCOVERED_HOMES = homes.join(",");
|
|
323
|
+
|
|
324
|
+
if (platform === "win32" && !String(effective.GOOSE_PATH_ROOT ?? "").trim() && String(effective.APPDATA ?? "").trim()) {
|
|
325
|
+
const gooseRoot = pathApi.join(String(effective.APPDATA).trim(), "Block", "goose");
|
|
326
|
+
try {
|
|
327
|
+
if ((await stat(pathApi.join(gooseRoot, "data", "sessions", "sessions.db"))).isFile()) effective.GOOSE_PATH_ROOT = gooseRoot;
|
|
328
|
+
} catch {}
|
|
329
|
+
}
|
|
330
|
+
return effective;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function accepts(matcher, filePath, pathApi) {
|
|
334
|
+
const name = pathApi.basename(filePath);
|
|
335
|
+
if (matcher === "any") return true;
|
|
336
|
+
if (matcher === "jsonl") return name.endsWith(".jsonl");
|
|
337
|
+
if (matcher === "json") return name.endsWith(".json");
|
|
338
|
+
if (matcher === "gemini") return name.endsWith(".json") || name.endsWith(".jsonl");
|
|
339
|
+
if (matcher === "codebuff") return name === "chat-messages.json";
|
|
340
|
+
if (matcher === "droid") return name.endsWith(".settings.json") || name.endsWith(".jsonl");
|
|
341
|
+
if (matcher === "openclaw") return name.includes(".jsonl");
|
|
342
|
+
if (matcher === "grok") return name === "updates.jsonl" || name === "summary.json";
|
|
343
|
+
if (matcher === "opencode") return name.endsWith(".db") || name.includes(".db-") || name.endsWith(".json");
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function existsDirectory(value) {
|
|
348
|
+
try {
|
|
349
|
+
return (await stat(value)).isDirectory();
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function expandCodexRoots(definitions, pathApi) {
|
|
356
|
+
const expanded = [];
|
|
357
|
+
for (const definition of definitions) {
|
|
358
|
+
if (definition.kind !== "codex-home") {
|
|
359
|
+
expanded.push(definition);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const sessions = pathApi.join(definition.path, "sessions");
|
|
363
|
+
const archived = pathApi.join(definition.path, "archived_sessions");
|
|
364
|
+
const [hasSessions, hasArchived] = await Promise.all([existsDirectory(sessions), existsDirectory(archived)]);
|
|
365
|
+
if (hasSessions) expanded.push(tree("codex", sessions, "jsonl"));
|
|
366
|
+
if (hasArchived) expanded.push(tree("codex", archived, "jsonl"));
|
|
367
|
+
if (!hasSessions && !hasArchived) expanded.push(tree("codex", definition.path, "jsonl"));
|
|
368
|
+
}
|
|
369
|
+
return expanded;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function configCandidates(definitions) {
|
|
373
|
+
return definitions.filter((item) => item.source === "ccusage-config").map((item) => item.path);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function namedPiStores(definitions, home, pathApi) {
|
|
377
|
+
for (const configPath of configCandidates(definitions)) {
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
380
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
381
|
+
const stores = Array.isArray(parsed.pi?.stores) ? parsed.pi.stores : [];
|
|
382
|
+
return stores.flatMap((store) => {
|
|
383
|
+
if (!store || typeof store.name !== "string" || typeof store.path !== "string" || !store.path.trim()) return [];
|
|
384
|
+
return [tree(store.name.trim().toLowerCase(), expandTilde(store.path.trim(), home, pathApi), "jsonl")];
|
|
385
|
+
});
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function fileIdentity(source, filePath, metadata) {
|
|
392
|
+
return `${source}\u0000${filePath}\u0000${metadata.size}\u0000${metadata.mtimeMs}\u0000${metadata.ctimeMs}\n`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function sourceInventory({ env = process.env, cwd = process.cwd(), home = ccusageHome(env), pathApi = path, maxFiles = MAX_FINGERPRINT_FILES } = {}) {
|
|
396
|
+
const hash = createHash("sha256");
|
|
397
|
+
hash.update(`inventory:${SOURCE_INVENTORY_VERSION}\nccusage:${CCUSAGE_VERSION}\naliases:${env.CCUSAGE_MODEL_ALIASES ?? ""}\n`);
|
|
398
|
+
let definitions = sourceDefinitions({ env, cwd, home, pathApi });
|
|
399
|
+
definitions.push(...await namedPiStores(definitions, home, pathApi));
|
|
400
|
+
definitions = await expandCodexRoots(definitions, pathApi);
|
|
401
|
+
definitions.sort((left, right) => `${left.source}\u0000${left.path}`.localeCompare(`${right.source}\u0000${right.path}`));
|
|
402
|
+
|
|
403
|
+
const sources = new Set();
|
|
404
|
+
const seenRoots = new Set();
|
|
405
|
+
const seenFiles = new Set();
|
|
406
|
+
let files = 0;
|
|
407
|
+
let truncated = false;
|
|
408
|
+
let complete = true;
|
|
409
|
+
let errors = 0;
|
|
410
|
+
|
|
411
|
+
async function addFile(definition, filePath) {
|
|
412
|
+
const identity = `${definition.source}\u0000${filePath}`;
|
|
413
|
+
if (seenFiles.has(identity)) return;
|
|
414
|
+
if (files >= maxFiles) {
|
|
415
|
+
truncated = true;
|
|
416
|
+
complete = false;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const metadata = await stat(filePath);
|
|
421
|
+
if (!metadata.isFile()) return;
|
|
422
|
+
seenFiles.add(identity);
|
|
423
|
+
hash.update(fileIdentity(definition.source, filePath, metadata));
|
|
424
|
+
if (definition.source === "ccusage-config") {
|
|
425
|
+
try {
|
|
426
|
+
hash.update(await readFile(filePath));
|
|
427
|
+
} catch {
|
|
428
|
+
complete = false;
|
|
429
|
+
errors += 1;
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
sources.add(definition.source);
|
|
433
|
+
}
|
|
434
|
+
files += 1;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error?.code !== "ENOENT") {
|
|
437
|
+
complete = false;
|
|
438
|
+
errors += 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
for (const definition of definitions) {
|
|
444
|
+
const rootIdentity = `${definition.source}\u0000${definition.kind}\u0000${definition.path}`;
|
|
445
|
+
if (seenRoots.has(rootIdentity)) continue;
|
|
446
|
+
seenRoots.add(rootIdentity);
|
|
447
|
+
hash.update(`root:${rootIdentity}\n`);
|
|
448
|
+
if (definition.kind === "file") {
|
|
449
|
+
await addFile(definition, definition.path);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
let rootMetadata;
|
|
453
|
+
try {
|
|
454
|
+
rootMetadata = await stat(definition.path);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error?.code !== "ENOENT") {
|
|
457
|
+
complete = false;
|
|
458
|
+
errors += 1;
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (!rootMetadata.isDirectory()) continue;
|
|
463
|
+
const stack = [definition.path];
|
|
464
|
+
while (stack.length && !truncated) {
|
|
465
|
+
const current = stack.pop();
|
|
466
|
+
let entries;
|
|
467
|
+
try {
|
|
468
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
469
|
+
} catch {
|
|
470
|
+
complete = false;
|
|
471
|
+
errors += 1;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
475
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
476
|
+
const entry = entries[index];
|
|
477
|
+
const filePath = pathApi.join(current, entry.name);
|
|
478
|
+
if (entry.isDirectory()) stack.push(filePath);
|
|
479
|
+
else if (entry.isFile() && accepts(definition.matcher, filePath, pathApi)) await addFile(definition, filePath);
|
|
480
|
+
if (truncated) break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
complete,
|
|
487
|
+
errors,
|
|
488
|
+
files,
|
|
489
|
+
fingerprint: hash.digest("hex"),
|
|
490
|
+
sources: [...sources].sort(),
|
|
491
|
+
supportedSources: SUPPORTED_SOURCES,
|
|
492
|
+
truncated,
|
|
493
|
+
version: SOURCE_INVENTORY_VERSION,
|
|
494
|
+
};
|
|
495
|
+
}
|