log-llm-config 1.5.2 → 1.5.9
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 +1 -1
- package/dist/bootstrap_constants.js +3 -3
- package/dist/dialog_prefs_cli.js +1 -1
- package/dist/log_config_files/auth/auth_flow.js +2 -2
- package/dist/log_config_files/auth/auth_key_store.js +2 -2
- 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 +9 -0
- package/dist/log_config_files/collection/copilot_token_usage_collector.js +154 -0
- package/dist/log_config_files/collection/cowork_desktop_version_collector.js +60 -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 +190 -0
- package/dist/log_config_files/collection/opencode_token_usage_collector.js +182 -0
- package/dist/log_config_files/collection/pi_token_usage_collector.js +203 -0
- package/dist/log_config_files/paths/path_constants_helpers.js +1 -1
- package/dist/log_config_files/runtime/client_event_reporter.js +4 -4
- package/dist/log_config_files/runtime/compliance_session_log.js +2 -2
- package/dist/log_config_files/runtime/hook_logger.js +3 -3
- package/dist/log_config_files/runtime/log_metadata.js +2 -2
- package/dist/log_config_files/runtime/main_runner.js +75 -3
- package/dist/log_config_files/runtime/management_storage.js +8 -8
- package/dist/log_config_files/runtime/remediation_sync.js +3 -3
- package/dist/log_config_files/sender/batch_sender.js +1 -1
- package/dist/log_sensitive_paths_audit.js +3 -3
- package/dist/log_uuid/auth_key_store.js +2 -2
- package/dist/log_uuid/startup_sender.js +1 -1
- package/dist/tofu.js +1 -1
- package/dist/tofu_environment.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,182 @@
|
|
|
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`; the singleton `account_state`
|
|
13
|
+
* row carries `active_org_id` (an org identifier — no org *name* exists anywhere in the schema).
|
|
14
|
+
* We read those via the bundled `sqlite3` binary (same resolver the remediation path uses) and
|
|
15
|
+
* emit only the aggregated numbers (plus the login email / org id) — never message content. The
|
|
16
|
+
* DB is opened read-only.
|
|
17
|
+
*
|
|
18
|
+
* Bounded by DB-file mtime recency so a stale install isn't re-read every hook run.
|
|
19
|
+
*/
|
|
20
|
+
const TOKEN_USAGE_RECENCY_DAYS = 30;
|
|
21
|
+
const TOKEN_USAGE_FILE_TYPE = 'opencode_token_usage';
|
|
22
|
+
// ASCII record / unit separators: never appear in JSON or emails, so they delimit SELECTed
|
|
23
|
+
// rows/fields unambiguously. In SQL these are char(30) / char(31).
|
|
24
|
+
const ROW_SEP = '\x1e';
|
|
25
|
+
const FIELD_SEP = '\x1f';
|
|
26
|
+
function emptyTotals() {
|
|
27
|
+
return {
|
|
28
|
+
input_tokens: 0,
|
|
29
|
+
output_tokens: 0,
|
|
30
|
+
cache_read_input_tokens: 0,
|
|
31
|
+
cache_creation_input_tokens: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function num(value) {
|
|
35
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
36
|
+
}
|
|
37
|
+
function asObject(value) {
|
|
38
|
+
return value && typeof value === 'object' ? value : {};
|
|
39
|
+
}
|
|
40
|
+
/** Run a read-only query and return raw stdout, or null when sqlite3/DB is unavailable. */
|
|
41
|
+
function querySqlite(sqlite3, dbPath, sql) {
|
|
42
|
+
try {
|
|
43
|
+
return execFileSync(sqlite3, ['-readonly', '-noheader', dbPath], {
|
|
44
|
+
input: `.timeout 5000\n${sql}\n`,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** OpenCode CLI version from ~/.config/opencode/package.json (@opencode-ai/plugin pin), best-effort. */
|
|
54
|
+
function readOpencodeVersion(home) {
|
|
55
|
+
for (const rel of ['.config/opencode/package.json', '.cache/opencode/package.json']) {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(join(home, rel), 'utf8'));
|
|
58
|
+
const deps = asObject(pkg.dependencies);
|
|
59
|
+
const v = deps['@opencode-ai/plugin'] ?? deps['opencode'] ?? pkg.version;
|
|
60
|
+
if (typeof v === 'string' && v)
|
|
61
|
+
return v.replace(/^[\^~]/, '');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* missing/unparseable — try next */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Aggregate OpenCode token usage / cost / sessions / models from the SQLite DB.
|
|
71
|
+
* Returns a single-element array (one per-machine aggregate) or [] when there is no usable DB.
|
|
72
|
+
*/
|
|
73
|
+
function collectOpencodeTokenUsage(home = homedir()) {
|
|
74
|
+
const dbPath = join(home, '.local', 'share', 'opencode', 'opencode.db');
|
|
75
|
+
if (!existsSync(dbPath))
|
|
76
|
+
return [];
|
|
77
|
+
const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
|
|
78
|
+
try {
|
|
79
|
+
if (statSync(dbPath).mtimeMs < cutoffMs)
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
86
|
+
if (!sqlite3)
|
|
87
|
+
return [];
|
|
88
|
+
// Window aggregation to the recency horizon (message.time_created is epoch ms) so a touched-but-
|
|
89
|
+
// -stale DB (VACUUM, backup, one new session) doesn't re-sum all-time history — matches the
|
|
90
|
+
// 30-day window the Claude/Copilot collectors apply per transcript file. cutoffMs is a numeric
|
|
91
|
+
// constant we control, so inlining it into the SQL is injection-safe.
|
|
92
|
+
const cutoffClause = `WHERE time_created >= ${cutoffMs}`;
|
|
93
|
+
// Assistant message JSON blobs, one per row (char(30)-delimited so embedded newlines in the
|
|
94
|
+
// JSON don't split rows).
|
|
95
|
+
const messagesRaw = querySqlite(sqlite3, dbPath, `SELECT data || char(30) FROM message ${cutoffClause};`);
|
|
96
|
+
if (messagesRaw === null)
|
|
97
|
+
return [];
|
|
98
|
+
const totals = emptyTotals();
|
|
99
|
+
const models = new Set();
|
|
100
|
+
const providers = new Set();
|
|
101
|
+
const modelTokenSplit = {};
|
|
102
|
+
let cost = 0;
|
|
103
|
+
let sawData = false;
|
|
104
|
+
for (const blob of messagesRaw.split(ROW_SEP)) {
|
|
105
|
+
const trimmed = blob.trim();
|
|
106
|
+
if (!trimmed)
|
|
107
|
+
continue;
|
|
108
|
+
let msg;
|
|
109
|
+
try {
|
|
110
|
+
msg = JSON.parse(trimmed);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (msg.role !== 'assistant')
|
|
116
|
+
continue;
|
|
117
|
+
sawData = true;
|
|
118
|
+
const model = typeof msg.modelID === 'string' ? msg.modelID : '';
|
|
119
|
+
if (model)
|
|
120
|
+
models.add(model);
|
|
121
|
+
if (typeof msg.providerID === 'string' && msg.providerID)
|
|
122
|
+
providers.add(msg.providerID);
|
|
123
|
+
cost += num(msg.cost);
|
|
124
|
+
const tokens = asObject(msg.tokens);
|
|
125
|
+
const cache = asObject(tokens.cache);
|
|
126
|
+
const input = num(tokens.input);
|
|
127
|
+
const output = num(tokens.output) + num(tokens.reasoning);
|
|
128
|
+
const cacheRead = num(cache.read);
|
|
129
|
+
const cacheWrite = num(cache.write);
|
|
130
|
+
totals.input_tokens += input;
|
|
131
|
+
totals.output_tokens += output;
|
|
132
|
+
totals.cache_read_input_tokens += cacheRead;
|
|
133
|
+
totals.cache_creation_input_tokens += cacheWrite;
|
|
134
|
+
if (model) {
|
|
135
|
+
const sum = input + output + cacheRead + cacheWrite;
|
|
136
|
+
if (sum > 0)
|
|
137
|
+
modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Session count = distinct sessions with activity in the window (the `session` table has no
|
|
141
|
+
// timestamp, so count distinct session_id from recent messages — consistent with the windowed
|
|
142
|
+
// token totals above).
|
|
143
|
+
const sessionCountRaw = querySqlite(sqlite3, dbPath, `SELECT count(DISTINCT session_id) FROM message ${cutoffClause};`);
|
|
144
|
+
const sessionCount = sessionCountRaw ? num(parseInt(sessionCountRaw.trim(), 10)) : 0;
|
|
145
|
+
const accountRaw = querySqlite(sqlite3, dbPath, "SELECT COALESCE(email, '') || char(31) || COALESCE(url, '') FROM account ORDER BY time_updated DESC LIMIT 1;");
|
|
146
|
+
let email = '';
|
|
147
|
+
let host = '';
|
|
148
|
+
if (accountRaw && accountRaw.trim()) {
|
|
149
|
+
const [rawEmail, rawUrl] = accountRaw.trim().split(FIELD_SEP);
|
|
150
|
+
email = (rawEmail ?? '').trim();
|
|
151
|
+
host = (rawUrl ?? '')
|
|
152
|
+
.trim()
|
|
153
|
+
.replace(/^https?:\/\//, '')
|
|
154
|
+
.replace(/\/$/, '');
|
|
155
|
+
}
|
|
156
|
+
// Singleton row; absent on older installs/schemas without account_state — querySqlite
|
|
157
|
+
// returns null on "no such table" and orgIdRaw stays empty in that case.
|
|
158
|
+
const orgIdRaw = querySqlite(sqlite3, dbPath, "SELECT COALESCE(active_org_id, '') FROM account_state LIMIT 1;");
|
|
159
|
+
const orgId = orgIdRaw ? orgIdRaw.trim() : '';
|
|
160
|
+
if (!sawData && !email)
|
|
161
|
+
return [];
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
file_type: TOKEN_USAGE_FILE_TYPE,
|
|
165
|
+
file_path: dbPath, // stable synthetic path → one aggregate row per machine
|
|
166
|
+
raw_content: {
|
|
167
|
+
version: readOpencodeVersion(home),
|
|
168
|
+
session_count: sessionCount,
|
|
169
|
+
models: [...models].sort(),
|
|
170
|
+
providers: [...providers].sort(),
|
|
171
|
+
model_token_split: modelTokenSplit,
|
|
172
|
+
cost,
|
|
173
|
+
email,
|
|
174
|
+
host,
|
|
175
|
+
org_id: orgId,
|
|
176
|
+
window_days: TOKEN_USAGE_RECENCY_DAYS,
|
|
177
|
+
totals,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
export { collectOpencodeTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };
|
|
@@ -0,0 +1,203 @@
|
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* auth.json holds literal per-provider `{type, key}` blobs (`{"anthropic": {"type": ...,
|
|
20
|
+
* "key": ...}}`), the same risk class as Hermes's/Codex's auth.json. We read only the `type`
|
|
21
|
+
* field per provider (e.g. `"api_key"`) — never `key`, the actual credential.
|
|
22
|
+
*
|
|
23
|
+
* `lastChangelogVersion` (confirmed against a real install: "0.80.3", matching `pi --version`
|
|
24
|
+
* exactly) is a best-effort CLI version signal, not a guaranteed-current one — it records the
|
|
25
|
+
* version Pi last showed its changelog for, which in practice tracks the installed version but
|
|
26
|
+
* could theoretically lag by one release in a narrow upgrade window. The session header's own
|
|
27
|
+
* `version` field is a JSONL schema version (small int, e.g. `3`), not the CLI version, and is
|
|
28
|
+
* not used here.
|
|
29
|
+
*
|
|
30
|
+
* Bounded by mtime recency so a large session history is not fully re-parsed on every run.
|
|
31
|
+
*/
|
|
32
|
+
const TOKEN_USAGE_RECENCY_DAYS = 30;
|
|
33
|
+
const TOKEN_USAGE_FILE_TYPE = 'pi_token_usage';
|
|
34
|
+
function emptyTotals() {
|
|
35
|
+
return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 };
|
|
36
|
+
}
|
|
37
|
+
function num(value) {
|
|
38
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
39
|
+
}
|
|
40
|
+
function str(value) {
|
|
41
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
42
|
+
}
|
|
43
|
+
function asObject(value) {
|
|
44
|
+
return value && typeof value === 'object' ? value : {};
|
|
45
|
+
}
|
|
46
|
+
/** Recursively collect *.jsonl files under a dir whose mtime is within the recency window. */
|
|
47
|
+
function recentJsonlFiles(dir, cutoffMs) {
|
|
48
|
+
const out = [];
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const full = join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
out.push(...recentJsonlFiles(full, cutoffMs));
|
|
60
|
+
}
|
|
61
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
62
|
+
try {
|
|
63
|
+
if (statSync(full).mtimeMs >= cutoffMs)
|
|
64
|
+
out.push(full);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* unreadable file — skip */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** Scoped read of settings.json's `defaultProvider` / `defaultModel` / `lastChangelogVersion` keys only. */
|
|
74
|
+
function readDefaults(home) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = JSON.parse(readFileSync(join(home, '.pi', 'agent', 'settings.json'), 'utf8'));
|
|
77
|
+
return {
|
|
78
|
+
provider: str(raw.defaultProvider),
|
|
79
|
+
model: str(raw.defaultModel),
|
|
80
|
+
version: str(raw.lastChangelogVersion),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { provider: '', model: '', version: '' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Scoped read of auth.json's per-provider `type` field only — never the sibling `key`
|
|
89
|
+
* credential. Returns a provider -> type map (e.g. `{"anthropic": "api_key"}`).
|
|
90
|
+
*/
|
|
91
|
+
function readAuthTypesByProvider(home) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = JSON.parse(readFileSync(join(home, '.pi', 'agent', 'auth.json'), 'utf8'));
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const [provider, value] of Object.entries(raw)) {
|
|
96
|
+
if (!value || typeof value !== 'object')
|
|
97
|
+
continue;
|
|
98
|
+
const type = str(value.type);
|
|
99
|
+
if (type)
|
|
100
|
+
out[provider] = type;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Aggregate Pi session count / token usage / models / providers across recent session
|
|
110
|
+
* transcripts. Returns a single-element array (one per-machine aggregate) or [] when there is
|
|
111
|
+
* no recent data.
|
|
112
|
+
*/
|
|
113
|
+
function collectPiTokenUsage(home = homedir()) {
|
|
114
|
+
const sessionsDir = join(home, '.pi', 'agent', 'sessions');
|
|
115
|
+
if (!existsSync(sessionsDir))
|
|
116
|
+
return [];
|
|
117
|
+
const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
|
|
118
|
+
const files = recentJsonlFiles(sessionsDir, cutoffMs);
|
|
119
|
+
if (files.length === 0)
|
|
120
|
+
return [];
|
|
121
|
+
const totals = emptyTotals();
|
|
122
|
+
const sessions = new Set();
|
|
123
|
+
const models = new Set();
|
|
124
|
+
const providers = new Set();
|
|
125
|
+
const modelTokenSplit = {};
|
|
126
|
+
let sawData = false;
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
let content;
|
|
129
|
+
try {
|
|
130
|
+
content = readFileSync(file, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const line of content.split('\n')) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (!trimmed)
|
|
138
|
+
continue;
|
|
139
|
+
let entry;
|
|
140
|
+
try {
|
|
141
|
+
entry = JSON.parse(trimmed);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (entry.type === 'session') {
|
|
147
|
+
const sessionId = str(entry.id);
|
|
148
|
+
if (sessionId)
|
|
149
|
+
sessions.add(sessionId);
|
|
150
|
+
sawData = true;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (entry.type !== 'message')
|
|
154
|
+
continue;
|
|
155
|
+
const message = asObject(entry.message);
|
|
156
|
+
const usage = asObject(message.usage);
|
|
157
|
+
if (Object.keys(usage).length === 0)
|
|
158
|
+
continue;
|
|
159
|
+
sawData = true;
|
|
160
|
+
const model = str(message.model);
|
|
161
|
+
const provider = str(message.provider);
|
|
162
|
+
if (model)
|
|
163
|
+
models.add(model);
|
|
164
|
+
if (provider)
|
|
165
|
+
providers.add(provider);
|
|
166
|
+
const input = num(usage.input);
|
|
167
|
+
// reasoning tokens fold into output_tokens — TokenTotals has no separate reasoning
|
|
168
|
+
// field (matches Codex's/Hermes's reasoning fold-in convention).
|
|
169
|
+
const output = num(usage.output) + num(usage.reasoning);
|
|
170
|
+
const cacheRead = num(usage.cacheRead);
|
|
171
|
+
totals.input_tokens += input;
|
|
172
|
+
totals.output_tokens += output;
|
|
173
|
+
totals.cache_read_input_tokens += cacheRead;
|
|
174
|
+
const sum = input + output + cacheRead;
|
|
175
|
+
if (model && sum > 0) {
|
|
176
|
+
modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!sawData)
|
|
181
|
+
return [];
|
|
182
|
+
const defaults = readDefaults(home);
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
file_type: TOKEN_USAGE_FILE_TYPE,
|
|
186
|
+
// Stable synthetic path → one aggregate row per machine.
|
|
187
|
+
file_path: sessionsDir,
|
|
188
|
+
raw_content: {
|
|
189
|
+
version: defaults.version,
|
|
190
|
+
session_count: sessions.size,
|
|
191
|
+
models: [...models].sort(),
|
|
192
|
+
providers: [...providers].sort(),
|
|
193
|
+
model_token_split: modelTokenSplit,
|
|
194
|
+
configured_model: defaults.model,
|
|
195
|
+
configured_provider: defaults.provider,
|
|
196
|
+
window_days: TOKEN_USAGE_RECENCY_DAYS,
|
|
197
|
+
totals,
|
|
198
|
+
auth_types_by_provider: readAuthTypesByProvider(home),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
export { collectPiTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };
|
|
@@ -27,7 +27,7 @@ export function getInstalledPluginsPath(home, constants) {
|
|
|
27
27
|
return join(home, rel.replace(/^~\//, ''));
|
|
28
28
|
}
|
|
29
29
|
export function getOptAiSecManagementPath(home, constants) {
|
|
30
|
-
const rel = requireKey(constants, '
|
|
30
|
+
const rel = requireKey(constants, 'optimuslabs_management_under_home', '.optimuslabs management path');
|
|
31
31
|
return join(home, rel.replace(/^~\//, ''));
|
|
32
32
|
}
|
|
33
33
|
export function getAuthKeyPathFromConstants(home, constants) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync, mkdirSync, appendFileSync, readFileSync, renameSync, unlinkSync, statSync } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
|
-
import {
|
|
16
|
+
import { OPTIMUSLABS_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
17
17
|
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
18
18
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
19
19
|
import { createSignature } from '../sender/signing.js';
|
|
@@ -29,7 +29,7 @@ function bufferPath() {
|
|
|
29
29
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
30
30
|
if (!home)
|
|
31
31
|
return null;
|
|
32
|
-
return path.join(home,
|
|
32
|
+
return path.join(home, OPTIMUSLABS_MANAGEMENT_REL, 'telemetry', BUFFER_BASENAME);
|
|
33
33
|
}
|
|
34
34
|
/** Keep filename-safe chars only — must match optimus_sanitize_id in optimus-hook-common.sh. */
|
|
35
35
|
function sanitizeId(v) {
|
|
@@ -40,7 +40,7 @@ function storyFilePath() {
|
|
|
40
40
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
41
41
|
if (!home)
|
|
42
42
|
return null;
|
|
43
|
-
const telemetryDir = path.join(home,
|
|
43
|
+
const telemetryDir = path.join(home, OPTIMUSLABS_MANAGEMENT_REL, 'telemetry');
|
|
44
44
|
const session = sanitizeId((process.env.OPTIMUS_SESSION_ID || '').trim());
|
|
45
45
|
if (session) {
|
|
46
46
|
const agent = sanitizeId((process.env.OPTIMUS_HOOK_TYPE || process.env.OPTIMUS_AGENT || 'claude').trim()) || 'claude';
|
|
@@ -107,7 +107,7 @@ function hookRunFilePath() {
|
|
|
107
107
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
108
108
|
if (!home)
|
|
109
109
|
return null;
|
|
110
|
-
return path.join(home,
|
|
110
|
+
return path.join(home, OPTIMUSLABS_MANAGEMENT_REL, 'telemetry', 'current_hook_run.id');
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* hook_run_id: one uuid per shell hook invocation.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, readFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import {
|
|
4
|
+
import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_COMPLIANCE } from '../../bootstrap_constants.js';
|
|
5
5
|
import { buildLogMetadataBlock } from './log_metadata.js';
|
|
6
6
|
const COMPLIANCE_SUBDIR = LOG_GROUP_COMPLIANCE;
|
|
7
7
|
const TIER_NOOP = 'noop';
|
|
@@ -35,7 +35,7 @@ export function isFinalizeComplianceSessionArgv(argv) {
|
|
|
35
35
|
return argv.includes('--finalize-compliance-session');
|
|
36
36
|
}
|
|
37
37
|
export function getComplianceLogDir() {
|
|
38
|
-
return path.join(homedir(),
|
|
38
|
+
return path.join(homedir(), OPTIMUSLABS_LOGS_REL, COMPLIANCE_SUBDIR);
|
|
39
39
|
}
|
|
40
40
|
export function getActiveComplianceLogPath() {
|
|
41
41
|
return activeComplianceLogPath;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_HOOK, LOG_GROUP_COMPLIANCE, } from '../../bootstrap_constants.js';
|
|
4
4
|
import { getActiveComplianceLogPath } from './compliance_session_log.js';
|
|
5
5
|
import { buildLogMetadataBlock, formatErrorBlock } from './log_metadata.js';
|
|
6
6
|
const HOOK_LOG_FILENAME = 'hook_log.txt';
|
|
@@ -12,13 +12,13 @@ function getHookLogPath() {
|
|
|
12
12
|
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
13
13
|
if (!homeDir)
|
|
14
14
|
return null;
|
|
15
|
-
return path.join(homeDir,
|
|
15
|
+
return path.join(homeDir, OPTIMUSLABS_LOGS_REL, LOG_GROUP_HOOK, HOOK_LOG_FILENAME);
|
|
16
16
|
}
|
|
17
17
|
function getComplianceRunnerLogPath() {
|
|
18
18
|
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
19
19
|
if (!homeDir)
|
|
20
20
|
return null;
|
|
21
|
-
return path.join(homeDir,
|
|
21
|
+
return path.join(homeDir, OPTIMUSLABS_LOGS_REL, LOG_GROUP_COMPLIANCE, COMPLIANCE_FALLBACK_TIER, COMPLIANCE_RUNNER_LOG_FILENAME);
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Append one ISO-timestamped line to the active compliance session log.
|
|
@@ -7,7 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import {
|
|
10
|
+
import { OPTIMUSLABS_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
11
11
|
/** This package's name + version as "<name>: <version>", read from its own package.json. */
|
|
12
12
|
function readPackageVersion() {
|
|
13
13
|
try {
|
|
@@ -28,7 +28,7 @@ function readBundleVersion() {
|
|
|
28
28
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
29
29
|
if (!home)
|
|
30
30
|
return 'unknown';
|
|
31
|
-
const p = path.join(home,
|
|
31
|
+
const p = path.join(home, OPTIMUSLABS_MANAGEMENT_REL, 'release', 'bundle-version.json');
|
|
32
32
|
try {
|
|
33
33
|
if (!existsSync(p))
|
|
34
34
|
return 'unknown';
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
const HOME_DIR = homedir();
|
|
5
5
|
import { getFileCollectionPatterns, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH } from '../../endpoint_client/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT } from '../../bootstrap_constants.js';
|
|
7
7
|
import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
|
|
8
8
|
import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
|
|
9
9
|
import { hookLogReplace, hookRunLog, hookLogError, readHookLog } from './hook_logger.js';
|
|
@@ -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, collectCoworkDesktopVersion, 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,78 @@ 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
|
+
}
|
|
165
|
+
hookRunLog(`reading Claude Desktop app version for Cowork's Agent Profile`);
|
|
166
|
+
for (const entry of collectCoworkDesktopVersion(HOME_DIR)) {
|
|
167
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
168
|
+
if (!existingPaths.has(key)) {
|
|
169
|
+
existingPaths.add(key);
|
|
170
|
+
configFiles.push(entry);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
101
173
|
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
102
174
|
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
103
175
|
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|
|
@@ -135,7 +207,7 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
135
207
|
}
|
|
136
208
|
async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
137
209
|
hookRunLog(`running sensitive paths audit`);
|
|
138
|
-
const auditOutputDir = join(homedir(),
|
|
210
|
+
const auditOutputDir = join(homedir(), OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT);
|
|
139
211
|
try {
|
|
140
212
|
const auditResult = await runSensitivePathsAudit(endpointBase, auditOutputDir, PROJECT_ROOT);
|
|
141
213
|
hookRunLog(`sensitive_paths_audit: ${auditResult.paths.length} path(s)`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paths and JSON persistence for
|
|
2
|
+
* Paths and JSON persistence for ~/.optimuslabs/management:
|
|
3
3
|
* - remediation_instructions.json (remediations[])
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import {
|
|
8
|
+
import { OPTIMUSLABS_MANAGEMENT_REL, OPTIMUSLABS_LOGS_REL } from '../../bootstrap_constants.js';
|
|
9
9
|
export const REMEDIATION_INSTRUCTIONS_BASENAME = 'remediation_instructions.json';
|
|
10
10
|
/** Pending state.vscdb writes applied after Cursor exits (IDE holds the DB locked). */
|
|
11
11
|
export const DEFERRED_VSCDB_APPLY_BASENAME = 'optimus_deferred_vscdb_apply.json';
|
|
@@ -51,14 +51,14 @@ export function sanitizeFileCollectionVscdbContract(contract) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
export function getManagementDir() {
|
|
54
|
-
return join(homedir(),
|
|
54
|
+
return join(homedir(), OPTIMUSLABS_MANAGEMENT_REL);
|
|
55
55
|
}
|
|
56
|
-
/** Root of the grouped hook log subtree (
|
|
56
|
+
/** Root of the grouped hook log subtree (~/.optimuslabs/management/logs). */
|
|
57
57
|
export function getLogsDir() {
|
|
58
|
-
return join(homedir(),
|
|
58
|
+
return join(homedir(), OPTIMUSLABS_LOGS_REL);
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Directory for vscdb state/contract files (
|
|
61
|
+
* Directory for vscdb state/contract files (~/.optimuslabs/management/vscdb), keeping the
|
|
62
62
|
* several generated vscdb artifacts grouped instead of dangling at the management root.
|
|
63
63
|
* Includes deferred_vscdb_restart.log (detached restart pipeline output).
|
|
64
64
|
*/
|
|
@@ -66,7 +66,7 @@ export function getVscdbStateDir() {
|
|
|
66
66
|
return join(getManagementDir(), 'vscdb');
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Directory for non-log remediation state (
|
|
69
|
+
* Directory for non-log remediation state (~/.optimuslabs/management/remediation):
|
|
70
70
|
* remediation_instructions.json (what to fix) and remediation_apply_tracking.json
|
|
71
71
|
* (quarantine + verify-failure history that escapes the autofix restart loop).
|
|
72
72
|
* Remediation session logs live under logs/compliance/.
|
|
@@ -86,7 +86,7 @@ export function getDeferredVscdbRestartLogPath() {
|
|
|
86
86
|
export function getFileCollectionVscdbContractPath() {
|
|
87
87
|
return join(getVscdbStateDir(), FILE_COLLECTION_VSCDB_CONTRACT_BASENAME);
|
|
88
88
|
}
|
|
89
|
-
/** Directory for macOS dialog assets/state (
|
|
89
|
+
/** Directory for macOS dialog assets/state (~/.optimuslabs/management/dialog): prefs + cached icon. */
|
|
90
90
|
export function getDialogStateDir() {
|
|
91
91
|
return join(getManagementDir(), 'dialog');
|
|
92
92
|
}
|