log-llm-config 1.5.2 → 1.5.8
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/dist/log_config_files/collection/claude_token_usage_collector.js +156 -0
- package/dist/log_config_files/collection/codex_token_usage_collector.js +245 -0
- package/dist/log_config_files/collection/config_collector.js +8 -0
- package/dist/log_config_files/collection/copilot_token_usage_collector.js +154 -0
- package/dist/log_config_files/collection/cursor_token_usage_collector.js +188 -0
- package/dist/log_config_files/collection/hermes_token_usage_collector.js +219 -0
- package/dist/log_config_files/collection/openclaw_token_usage_collector.js +155 -0
- package/dist/log_config_files/collection/opencode_token_usage_collector.js +175 -0
- package/dist/log_config_files/collection/pi_token_usage_collector.js +180 -0
- package/dist/log_config_files/runtime/main_runner.js +65 -1
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
|
|
6
|
+
/**
|
|
7
|
+
* Token-usage / cost / session / version / identity aggregator for OpenCode.
|
|
8
|
+
*
|
|
9
|
+
* Unlike Claude/Copilot (JSONL transcripts), OpenCode stores everything in a SQLite DB at
|
|
10
|
+
* ~/.local/share/opencode/opencode.db. Each assistant row in the `message` table has a JSON
|
|
11
|
+
* `data` blob carrying `modelID`, `providerID`, `tokens` ({input,output,reasoning,cache:{read,write}})
|
|
12
|
+
* and `cost`; the `account` table carries the login `email`/`url`. We read those via the bundled
|
|
13
|
+
* `sqlite3` binary (same resolver the remediation path uses) and emit only the aggregated numbers
|
|
14
|
+
* (plus the login email) — never message content. The DB is opened read-only.
|
|
15
|
+
*
|
|
16
|
+
* Bounded by DB-file mtime recency so a stale install isn't re-read every hook run.
|
|
17
|
+
*/
|
|
18
|
+
const TOKEN_USAGE_RECENCY_DAYS = 30;
|
|
19
|
+
const TOKEN_USAGE_FILE_TYPE = 'opencode_token_usage';
|
|
20
|
+
// ASCII record / unit separators: never appear in JSON or emails, so they delimit SELECTed
|
|
21
|
+
// rows/fields unambiguously. In SQL these are char(30) / char(31).
|
|
22
|
+
const ROW_SEP = '\x1e';
|
|
23
|
+
const FIELD_SEP = '\x1f';
|
|
24
|
+
function emptyTotals() {
|
|
25
|
+
return {
|
|
26
|
+
input_tokens: 0,
|
|
27
|
+
output_tokens: 0,
|
|
28
|
+
cache_read_input_tokens: 0,
|
|
29
|
+
cache_creation_input_tokens: 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function num(value) {
|
|
33
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
34
|
+
}
|
|
35
|
+
function asObject(value) {
|
|
36
|
+
return value && typeof value === 'object' ? value : {};
|
|
37
|
+
}
|
|
38
|
+
/** Run a read-only query and return raw stdout, or null when sqlite3/DB is unavailable. */
|
|
39
|
+
function querySqlite(sqlite3, dbPath, sql) {
|
|
40
|
+
try {
|
|
41
|
+
return execFileSync(sqlite3, ['-readonly', '-noheader', dbPath], {
|
|
42
|
+
input: `.timeout 5000\n${sql}\n`,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** OpenCode CLI version from ~/.config/opencode/package.json (@opencode-ai/plugin pin), best-effort. */
|
|
52
|
+
function readOpencodeVersion(home) {
|
|
53
|
+
for (const rel of ['.config/opencode/package.json', '.cache/opencode/package.json']) {
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(readFileSync(join(home, rel), 'utf8'));
|
|
56
|
+
const deps = asObject(pkg.dependencies);
|
|
57
|
+
const v = deps['@opencode-ai/plugin'] ?? deps['opencode'] ?? pkg.version;
|
|
58
|
+
if (typeof v === 'string' && v)
|
|
59
|
+
return v.replace(/^[\^~]/, '');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* missing/unparseable — try next */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Aggregate OpenCode token usage / cost / sessions / models from the SQLite DB.
|
|
69
|
+
* Returns a single-element array (one per-machine aggregate) or [] when there is no usable DB.
|
|
70
|
+
*/
|
|
71
|
+
function collectOpencodeTokenUsage(home = homedir()) {
|
|
72
|
+
const dbPath = join(home, '.local', 'share', 'opencode', 'opencode.db');
|
|
73
|
+
if (!existsSync(dbPath))
|
|
74
|
+
return [];
|
|
75
|
+
const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
|
|
76
|
+
try {
|
|
77
|
+
if (statSync(dbPath).mtimeMs < cutoffMs)
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
84
|
+
if (!sqlite3)
|
|
85
|
+
return [];
|
|
86
|
+
// Window aggregation to the recency horizon (message.time_created is epoch ms) so a touched-but-
|
|
87
|
+
// -stale DB (VACUUM, backup, one new session) doesn't re-sum all-time history — matches the
|
|
88
|
+
// 30-day window the Claude/Copilot collectors apply per transcript file. cutoffMs is a numeric
|
|
89
|
+
// constant we control, so inlining it into the SQL is injection-safe.
|
|
90
|
+
const cutoffClause = `WHERE time_created >= ${cutoffMs}`;
|
|
91
|
+
// Assistant message JSON blobs, one per row (char(30)-delimited so embedded newlines in the
|
|
92
|
+
// JSON don't split rows).
|
|
93
|
+
const messagesRaw = querySqlite(sqlite3, dbPath, `SELECT data || char(30) FROM message ${cutoffClause};`);
|
|
94
|
+
if (messagesRaw === null)
|
|
95
|
+
return [];
|
|
96
|
+
const totals = emptyTotals();
|
|
97
|
+
const models = new Set();
|
|
98
|
+
const providers = new Set();
|
|
99
|
+
const modelTokenSplit = {};
|
|
100
|
+
let cost = 0;
|
|
101
|
+
let sawData = false;
|
|
102
|
+
for (const blob of messagesRaw.split(ROW_SEP)) {
|
|
103
|
+
const trimmed = blob.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
continue;
|
|
106
|
+
let msg;
|
|
107
|
+
try {
|
|
108
|
+
msg = JSON.parse(trimmed);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (msg.role !== 'assistant')
|
|
114
|
+
continue;
|
|
115
|
+
sawData = true;
|
|
116
|
+
const model = typeof msg.modelID === 'string' ? msg.modelID : '';
|
|
117
|
+
if (model)
|
|
118
|
+
models.add(model);
|
|
119
|
+
if (typeof msg.providerID === 'string' && msg.providerID)
|
|
120
|
+
providers.add(msg.providerID);
|
|
121
|
+
cost += num(msg.cost);
|
|
122
|
+
const tokens = asObject(msg.tokens);
|
|
123
|
+
const cache = asObject(tokens.cache);
|
|
124
|
+
const input = num(tokens.input);
|
|
125
|
+
const output = num(tokens.output) + num(tokens.reasoning);
|
|
126
|
+
const cacheRead = num(cache.read);
|
|
127
|
+
const cacheWrite = num(cache.write);
|
|
128
|
+
totals.input_tokens += input;
|
|
129
|
+
totals.output_tokens += output;
|
|
130
|
+
totals.cache_read_input_tokens += cacheRead;
|
|
131
|
+
totals.cache_creation_input_tokens += cacheWrite;
|
|
132
|
+
if (model) {
|
|
133
|
+
const sum = input + output + cacheRead + cacheWrite;
|
|
134
|
+
if (sum > 0)
|
|
135
|
+
modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Session count = distinct sessions with activity in the window (the `session` table has no
|
|
139
|
+
// timestamp, so count distinct session_id from recent messages — consistent with the windowed
|
|
140
|
+
// token totals above).
|
|
141
|
+
const sessionCountRaw = querySqlite(sqlite3, dbPath, `SELECT count(DISTINCT session_id) FROM message ${cutoffClause};`);
|
|
142
|
+
const sessionCount = sessionCountRaw ? num(parseInt(sessionCountRaw.trim(), 10)) : 0;
|
|
143
|
+
const accountRaw = querySqlite(sqlite3, dbPath, "SELECT COALESCE(email, '') || char(31) || COALESCE(url, '') FROM account ORDER BY time_updated DESC LIMIT 1;");
|
|
144
|
+
let email = '';
|
|
145
|
+
let host = '';
|
|
146
|
+
if (accountRaw && accountRaw.trim()) {
|
|
147
|
+
const [rawEmail, rawUrl] = accountRaw.trim().split(FIELD_SEP);
|
|
148
|
+
email = (rawEmail ?? '').trim();
|
|
149
|
+
host = (rawUrl ?? '')
|
|
150
|
+
.trim()
|
|
151
|
+
.replace(/^https?:\/\//, '')
|
|
152
|
+
.replace(/\/$/, '');
|
|
153
|
+
}
|
|
154
|
+
if (!sawData && !email)
|
|
155
|
+
return [];
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
file_type: TOKEN_USAGE_FILE_TYPE,
|
|
159
|
+
file_path: dbPath, // stable synthetic path → one aggregate row per machine
|
|
160
|
+
raw_content: {
|
|
161
|
+
version: readOpencodeVersion(home),
|
|
162
|
+
session_count: sessionCount,
|
|
163
|
+
models: [...models].sort(),
|
|
164
|
+
providers: [...providers].sort(),
|
|
165
|
+
model_token_split: modelTokenSplit,
|
|
166
|
+
cost,
|
|
167
|
+
email,
|
|
168
|
+
host,
|
|
169
|
+
window_days: TOKEN_USAGE_RECENCY_DAYS,
|
|
170
|
+
totals,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
export { collectOpencodeTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
/**
|
|
5
|
+
* Session / token usage / model / provider aggregator for Pi (earendil-works, pi.dev).
|
|
6
|
+
*
|
|
7
|
+
* Pi writes one JSONL transcript per session under
|
|
8
|
+
* ~/.pi/agent/sessions/<sanitized-cwd>/<timestamp>_<uuid>.jsonl. The first line is a
|
|
9
|
+
* `type: "session"` header carrying the session `id`; assistant turns are `type: "message"`
|
|
10
|
+
* lines whose `message.usage` block carries `{input, output, cacheRead, cacheWrite,
|
|
11
|
+
* totalTokens, reasoning?}` alongside `message.model` / `message.provider`. We aggregate the
|
|
12
|
+
* *numbers* (and model/provider ids) on the endpoint and emit only those — never
|
|
13
|
+
* `message.content` (the actual prompt/response text) — so the backend gets token usage,
|
|
14
|
+
* session count, and models/providers used without ingesting transcript content.
|
|
15
|
+
*
|
|
16
|
+
* Configured defaults come from a scoped read of settings.json's `defaultProvider` /
|
|
17
|
+
* `defaultModel` / `lastChangelogVersion` keys only — never any other key. auth.json is never
|
|
18
|
+
* touched at all: it holds literal per-provider `{type, key}` credential blobs
|
|
19
|
+
* (`{"anthropic": {"type": ..., "key": ...}}`), the same risk class as Hermes's/Codex's
|
|
20
|
+
* auth.json.
|
|
21
|
+
*
|
|
22
|
+
* `lastChangelogVersion` (confirmed against a real install: "0.80.3", matching `pi --version`
|
|
23
|
+
* exactly) is a best-effort CLI version signal, not a guaranteed-current one — it records the
|
|
24
|
+
* version Pi last showed its changelog for, which in practice tracks the installed version but
|
|
25
|
+
* could theoretically lag by one release in a narrow upgrade window. The session header's own
|
|
26
|
+
* `version` field is a JSONL schema version (small int, e.g. `3`), not the CLI version, and is
|
|
27
|
+
* not used here.
|
|
28
|
+
*
|
|
29
|
+
* Bounded by mtime recency so a large session history is not fully re-parsed on every run.
|
|
30
|
+
*/
|
|
31
|
+
const TOKEN_USAGE_RECENCY_DAYS = 30;
|
|
32
|
+
const TOKEN_USAGE_FILE_TYPE = 'pi_token_usage';
|
|
33
|
+
function emptyTotals() {
|
|
34
|
+
return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 };
|
|
35
|
+
}
|
|
36
|
+
function num(value) {
|
|
37
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
38
|
+
}
|
|
39
|
+
function str(value) {
|
|
40
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
41
|
+
}
|
|
42
|
+
function asObject(value) {
|
|
43
|
+
return value && typeof value === 'object' ? value : {};
|
|
44
|
+
}
|
|
45
|
+
/** Recursively collect *.jsonl files under a dir whose mtime is within the recency window. */
|
|
46
|
+
function recentJsonlFiles(dir, cutoffMs) {
|
|
47
|
+
const out = [];
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const full = join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
out.push(...recentJsonlFiles(full, cutoffMs));
|
|
59
|
+
}
|
|
60
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
61
|
+
try {
|
|
62
|
+
if (statSync(full).mtimeMs >= cutoffMs)
|
|
63
|
+
out.push(full);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* unreadable file — skip */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Scoped read of settings.json's `defaultProvider` / `defaultModel` / `lastChangelogVersion` keys only. */
|
|
73
|
+
function readDefaults(home) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = JSON.parse(readFileSync(join(home, '.pi', 'agent', 'settings.json'), 'utf8'));
|
|
76
|
+
return {
|
|
77
|
+
provider: str(raw.defaultProvider),
|
|
78
|
+
model: str(raw.defaultModel),
|
|
79
|
+
version: str(raw.lastChangelogVersion),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { provider: '', model: '', version: '' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Aggregate Pi session count / token usage / models / providers across recent session
|
|
88
|
+
* transcripts. Returns a single-element array (one per-machine aggregate) or [] when there is
|
|
89
|
+
* no recent data.
|
|
90
|
+
*/
|
|
91
|
+
function collectPiTokenUsage(home = homedir()) {
|
|
92
|
+
const sessionsDir = join(home, '.pi', 'agent', 'sessions');
|
|
93
|
+
if (!existsSync(sessionsDir))
|
|
94
|
+
return [];
|
|
95
|
+
const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
|
|
96
|
+
const files = recentJsonlFiles(sessionsDir, cutoffMs);
|
|
97
|
+
if (files.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const totals = emptyTotals();
|
|
100
|
+
const sessions = new Set();
|
|
101
|
+
const models = new Set();
|
|
102
|
+
const providers = new Set();
|
|
103
|
+
const modelTokenSplit = {};
|
|
104
|
+
let sawData = false;
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
let content;
|
|
107
|
+
try {
|
|
108
|
+
content = readFileSync(file, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const line of content.split('\n')) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
if (!trimmed)
|
|
116
|
+
continue;
|
|
117
|
+
let entry;
|
|
118
|
+
try {
|
|
119
|
+
entry = JSON.parse(trimmed);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (entry.type === 'session') {
|
|
125
|
+
const sessionId = str(entry.id);
|
|
126
|
+
if (sessionId)
|
|
127
|
+
sessions.add(sessionId);
|
|
128
|
+
sawData = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (entry.type !== 'message')
|
|
132
|
+
continue;
|
|
133
|
+
const message = asObject(entry.message);
|
|
134
|
+
const usage = asObject(message.usage);
|
|
135
|
+
if (Object.keys(usage).length === 0)
|
|
136
|
+
continue;
|
|
137
|
+
sawData = true;
|
|
138
|
+
const model = str(message.model);
|
|
139
|
+
const provider = str(message.provider);
|
|
140
|
+
if (model)
|
|
141
|
+
models.add(model);
|
|
142
|
+
if (provider)
|
|
143
|
+
providers.add(provider);
|
|
144
|
+
const input = num(usage.input);
|
|
145
|
+
// reasoning tokens fold into output_tokens — TokenTotals has no separate reasoning
|
|
146
|
+
// field (matches Codex's/Hermes's reasoning fold-in convention).
|
|
147
|
+
const output = num(usage.output) + num(usage.reasoning);
|
|
148
|
+
const cacheRead = num(usage.cacheRead);
|
|
149
|
+
totals.input_tokens += input;
|
|
150
|
+
totals.output_tokens += output;
|
|
151
|
+
totals.cache_read_input_tokens += cacheRead;
|
|
152
|
+
const sum = input + output + cacheRead;
|
|
153
|
+
if (model && sum > 0) {
|
|
154
|
+
modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!sawData)
|
|
159
|
+
return [];
|
|
160
|
+
const defaults = readDefaults(home);
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
file_type: TOKEN_USAGE_FILE_TYPE,
|
|
164
|
+
// Stable synthetic path → one aggregate row per machine.
|
|
165
|
+
file_path: sessionsDir,
|
|
166
|
+
raw_content: {
|
|
167
|
+
version: defaults.version,
|
|
168
|
+
session_count: sessions.size,
|
|
169
|
+
models: [...models].sort(),
|
|
170
|
+
providers: [...providers].sort(),
|
|
171
|
+
model_token_split: modelTokenSplit,
|
|
172
|
+
configured_model: defaults.model,
|
|
173
|
+
configured_provider: defaults.provider,
|
|
174
|
+
window_days: TOKEN_USAGE_RECENCY_DAYS,
|
|
175
|
+
totals,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
export { collectPiTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };
|
|
@@ -14,7 +14,7 @@ import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
|
14
14
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
15
15
|
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
16
16
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
17
|
-
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
17
|
+
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
18
18
|
import { ensureCursorUserSettingsSnapshotInBatch } from '../collection/ensure_cursor_user_settings_snapshot.js';
|
|
19
19
|
import { collectSkillsCliInstalled } from '../collection/skills_cli_collector.js';
|
|
20
20
|
import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
|
|
@@ -98,6 +98,70 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
98
98
|
configFiles.push(entry);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
hookRunLog(`aggregating Claude token usage from ~/.claude/projects transcripts`);
|
|
102
|
+
for (const entry of collectClaudeTokenUsage(HOME_DIR)) {
|
|
103
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
104
|
+
if (!existingPaths.has(key)) {
|
|
105
|
+
existingPaths.add(key);
|
|
106
|
+
configFiles.push(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
hookRunLog(`aggregating Copilot token usage from ~/.copilot/session-state transcripts`);
|
|
110
|
+
for (const entry of collectCopilotTokenUsage(HOME_DIR)) {
|
|
111
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
112
|
+
if (!existingPaths.has(key)) {
|
|
113
|
+
existingPaths.add(key);
|
|
114
|
+
configFiles.push(entry);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
hookRunLog(`aggregating OpenCode token usage from ~/.local/share/opencode/opencode.db`);
|
|
118
|
+
for (const entry of collectOpencodeTokenUsage(HOME_DIR)) {
|
|
119
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
120
|
+
if (!existingPaths.has(key)) {
|
|
121
|
+
existingPaths.add(key);
|
|
122
|
+
configFiles.push(entry);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
hookRunLog(`aggregating Codex token usage from ~/.codex/sessions rollouts`);
|
|
126
|
+
for (const entry of collectCodexTokenUsage(HOME_DIR)) {
|
|
127
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
128
|
+
if (!existingPaths.has(key)) {
|
|
129
|
+
existingPaths.add(key);
|
|
130
|
+
configFiles.push(entry);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
hookRunLog(`aggregating Cursor token usage from state.vscdb`);
|
|
134
|
+
for (const entry of collectCursorTokenUsage(HOME_DIR)) {
|
|
135
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
136
|
+
if (!existingPaths.has(key)) {
|
|
137
|
+
existingPaths.add(key);
|
|
138
|
+
configFiles.push(entry);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
hookRunLog(`aggregating Hermes token usage from ~/.hermes/state.db`);
|
|
142
|
+
for (const entry of collectHermesTokenUsage(HOME_DIR)) {
|
|
143
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
144
|
+
if (!existingPaths.has(key)) {
|
|
145
|
+
existingPaths.add(key);
|
|
146
|
+
configFiles.push(entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
hookRunLog(`aggregating OpenClaw token usage from ~/.openclaw/agents/*/sessions/sessions.json`);
|
|
150
|
+
for (const entry of collectOpenclawTokenUsage(HOME_DIR)) {
|
|
151
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
152
|
+
if (!existingPaths.has(key)) {
|
|
153
|
+
existingPaths.add(key);
|
|
154
|
+
configFiles.push(entry);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
hookRunLog(`aggregating Pi token usage from ~/.pi/agent/sessions transcripts`);
|
|
158
|
+
for (const entry of collectPiTokenUsage(HOME_DIR)) {
|
|
159
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
160
|
+
if (!existingPaths.has(key)) {
|
|
161
|
+
existingPaths.add(key);
|
|
162
|
+
configFiles.push(entry);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
101
165
|
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
102
166
|
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
103
167
|
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|