log-llm-config 1.5.11 → 1.5.14
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 +56 -9
- package/dist/log_config_files/collection/config_collector.js +1 -0
- package/dist/log_config_files/collection/cursor_project_mcp_collector.js +3 -0
- package/dist/log_config_files/collection/grok_cli_version_collector.js +53 -0
- package/dist/log_config_files/readers/file_readers.js +18 -1
- package/dist/log_config_files/runtime/compliance_check.js +17 -7
- package/dist/log_config_files/runtime/main_runner.js +9 -1
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +9 -1
- package/dist/log_config_files/runtime/remediation_sync.js +37 -18
- package/package.json +4 -3
|
@@ -25,18 +25,34 @@ function emptyTotals() {
|
|
|
25
25
|
}
|
|
26
26
|
function addUsage(totals, usage) {
|
|
27
27
|
for (const key of Object.keys(totals)) {
|
|
28
|
-
const value = usage
|
|
29
|
-
if (
|
|
28
|
+
const value = readTokenField(usage, key);
|
|
29
|
+
if (value > 0)
|
|
30
30
|
totals[key] += value;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/** Read one token field, preferring the nested 5m+1h cache-creation sum over the flat field. */
|
|
34
|
+
function readTokenField(usage, key) {
|
|
35
|
+
if (key === 'cache_creation_input_tokens') {
|
|
36
|
+
const nested = usage.cache_creation;
|
|
37
|
+
if (nested && typeof nested === 'object') {
|
|
38
|
+
const n = nested;
|
|
39
|
+
const has5m = typeof n.ephemeral_5m_input_tokens === 'number' && Number.isFinite(n.ephemeral_5m_input_tokens);
|
|
40
|
+
const has1h = typeof n.ephemeral_1h_input_tokens === 'number' && Number.isFinite(n.ephemeral_1h_input_tokens);
|
|
41
|
+
// Fall back to the flat field only when the nested block is truly absent — a
|
|
42
|
+
// legitimately-zero nested sum (both fields present as 0) must not be overridden by it.
|
|
43
|
+
if (has5m || has1h) {
|
|
44
|
+
return (has5m ? n.ephemeral_5m_input_tokens : 0) + (has1h ? n.ephemeral_1h_input_tokens : 0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const value = usage[key];
|
|
49
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
50
|
+
}
|
|
51
|
+
/** Sum the token fields of one usage block (used for per-model attribution and dedup tiebreaks). */
|
|
34
52
|
function usageSum(usage) {
|
|
35
53
|
let sum = 0;
|
|
36
54
|
for (const key of ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']) {
|
|
37
|
-
|
|
38
|
-
if (typeof value === 'number' && Number.isFinite(value))
|
|
39
|
-
sum += value;
|
|
55
|
+
sum += readTokenField(usage, key);
|
|
40
56
|
}
|
|
41
57
|
return sum;
|
|
42
58
|
}
|
|
@@ -86,6 +102,14 @@ function collectClaudeTokenUsage(home = homedir()) {
|
|
|
86
102
|
let version = '';
|
|
87
103
|
let versionAt = '';
|
|
88
104
|
let sawData = false;
|
|
105
|
+
// Claude Code writes one JSONL line per streaming snapshot of the same assistant message,
|
|
106
|
+
// so message.id/requestId repeat with a growing usage total; only the final snapshot is
|
|
107
|
+
// the true total. Dedup by (message.id, requestId), taking the per-field max across all
|
|
108
|
+
// snapshots for that key (order-independent — robust to out-of-order lines, and safe even
|
|
109
|
+
// if a snapshot has a genuinely-lower or reset value in one field while another snapshot in
|
|
110
|
+
// the same group peaks a different field). Entries without a message.id can't be deduped and
|
|
111
|
+
// are accumulated directly.
|
|
112
|
+
const dedupedUsage = new Map();
|
|
89
113
|
for (const file of files) {
|
|
90
114
|
let content;
|
|
91
115
|
try {
|
|
@@ -125,16 +149,39 @@ function collectClaudeTokenUsage(home = homedir()) {
|
|
|
125
149
|
models.add(model);
|
|
126
150
|
if (msg.usage && typeof msg.usage === 'object') {
|
|
127
151
|
const usage = msg.usage;
|
|
128
|
-
|
|
129
|
-
if (
|
|
152
|
+
const messageId = typeof msg.id === 'string' ? msg.id : '';
|
|
153
|
+
if (messageId) {
|
|
154
|
+
const requestId = typeof entry.requestId === 'string' ? entry.requestId : '';
|
|
155
|
+
const dedupeKey = `${messageId} ${requestId}`;
|
|
156
|
+
const existing = dedupedUsage.get(dedupeKey) ?? { totals: emptyTotals(), model: '' };
|
|
157
|
+
for (const key of Object.keys(existing.totals)) {
|
|
158
|
+
const value = readTokenField(usage, key);
|
|
159
|
+
if (value > existing.totals[key])
|
|
160
|
+
existing.totals[key] = value;
|
|
161
|
+
}
|
|
162
|
+
if (model)
|
|
163
|
+
existing.model = model;
|
|
164
|
+
dedupedUsage.set(dedupeKey, existing);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
addUsage(totals, usage);
|
|
130
168
|
const sum = usageSum(usage);
|
|
131
|
-
if (sum > 0)
|
|
169
|
+
if (model && sum > 0)
|
|
132
170
|
modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
|
|
133
171
|
}
|
|
134
172
|
}
|
|
135
173
|
}
|
|
136
174
|
}
|
|
137
175
|
}
|
|
176
|
+
for (const { totals: merged, model } of dedupedUsage.values()) {
|
|
177
|
+
let sum = 0;
|
|
178
|
+
for (const key of Object.keys(totals)) {
|
|
179
|
+
totals[key] += merged[key];
|
|
180
|
+
sum += merged[key];
|
|
181
|
+
}
|
|
182
|
+
if (model && sum > 0)
|
|
183
|
+
modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
|
|
184
|
+
}
|
|
138
185
|
if (!sawData)
|
|
139
186
|
return [];
|
|
140
187
|
return [
|
|
@@ -236,6 +236,7 @@ export { collectHermesTokenUsage } from './hermes_token_usage_collector.js';
|
|
|
236
236
|
export { collectOpenclawTokenUsage } from './openclaw_token_usage_collector.js';
|
|
237
237
|
export { collectPiTokenUsage } from './pi_token_usage_collector.js';
|
|
238
238
|
export { collectCoworkDesktopVersion } from './cowork_desktop_version_collector.js';
|
|
239
|
+
export { collectGrokCliVersion } from './grok_cli_version_collector.js';
|
|
239
240
|
export { collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, } from './claude_desktop_extensions_collector.js';
|
|
240
241
|
export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
|
|
241
242
|
export { determineFileTypeFromPath } from './file_type_rules.js';
|
|
@@ -188,6 +188,9 @@ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constant
|
|
|
188
188
|
cursor_source: 'mcps',
|
|
189
189
|
server_identifier: serverDir.name,
|
|
190
190
|
};
|
|
191
|
+
if (metaObj) {
|
|
192
|
+
entry.cursor_server_metadata = metaObj;
|
|
193
|
+
}
|
|
191
194
|
const cacheTools = cursorMcpCacheToolsForServer(mcpToolCache, label, serverDir.name);
|
|
192
195
|
if (cacheTools !== undefined) {
|
|
193
196
|
entry.cursor_mcp_cache_tools = cacheTools;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
/**
|
|
6
|
+
* Grok CLI version, read directly from the installed binary.
|
|
7
|
+
*
|
|
8
|
+
* Grok ships as a standalone native binary (not an npm/pip package, no manifest to read a
|
|
9
|
+
* version out of), installed at ~/.grok/bin/grok and typically symlinked onto PATH. The only
|
|
10
|
+
* way to get its version is to run it: `grok --version` prints e.g. "grok 0.2.114 (0c785038798)".
|
|
11
|
+
*/
|
|
12
|
+
const FILE_TYPE = 'grok_cli_version';
|
|
13
|
+
const VERSION_RE = /^grok\s+(\d+\.\d+\.\d+)(?:\s+\(([0-9a-f]+)\))?/i;
|
|
14
|
+
function grokBinPath(home) {
|
|
15
|
+
return join(home, '.grok', 'bin', 'grok');
|
|
16
|
+
}
|
|
17
|
+
/** `<binPath> --version`, parsed into {version, build, raw}, or null if unavailable/unparsable. */
|
|
18
|
+
function readGrokVersion(binPath) {
|
|
19
|
+
let stdout;
|
|
20
|
+
try {
|
|
21
|
+
stdout = execFileSync(binPath, ['--version'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
}).trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const match = VERSION_RE.exec(stdout);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
return { version: match[1], build: match[2] ?? '', raw: stdout };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Emit the Grok CLI version as a single-element aggregate, or [] when Grok isn't installed
|
|
37
|
+
* or its version can't be determined.
|
|
38
|
+
*/
|
|
39
|
+
function collectGrokCliVersion(home = homedir(), binPath = grokBinPath(home)) {
|
|
40
|
+
if (!existsSync(binPath))
|
|
41
|
+
return [];
|
|
42
|
+
const parsed = readGrokVersion(binPath);
|
|
43
|
+
if (!parsed)
|
|
44
|
+
return [];
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
file_type: FILE_TYPE,
|
|
48
|
+
file_path: binPath,
|
|
49
|
+
raw_content: parsed,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
export { collectGrokCliVersion, FILE_TYPE as GROK_CLI_VERSION_FILE_TYPE };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { parse as parseToml, stringify as stringifyTomlValue } from 'smol-toml';
|
|
2
3
|
function stripJsoncComments(input) {
|
|
3
4
|
let out = '';
|
|
4
5
|
let inString = false;
|
|
@@ -176,4 +177,20 @@ function readInstalledExtensions(extensionsCachePath) {
|
|
|
176
177
|
}
|
|
177
178
|
return extensions;
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
/** Parse TOML text into a nested object. Returns null on parse failure. */
|
|
181
|
+
function parseTomlText(raw) {
|
|
182
|
+
try {
|
|
183
|
+
const parsed = parseToml(raw);
|
|
184
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
185
|
+
? parsed
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Serialize a nested object to TOML (used when writing remediated .toml configs). */
|
|
193
|
+
function stringifyToml(value) {
|
|
194
|
+
return stringifyTomlValue(value);
|
|
195
|
+
}
|
|
196
|
+
export { readMCPConfig, readJSONFile, readMarkdownFile, readInstalledExtensions, parseJsonWithJsoncFallback, parseTomlText, stringifyToml, };
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
|
|
16
|
+
import { parseJsonWithJsoncFallback, parseTomlText } from '../readers/file_readers.js';
|
|
17
17
|
import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
|
|
18
18
|
import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
19
19
|
import { resolveRemediationConfigPath, resolveRemediationUploadFileType } from './remediation_config_path.js';
|
|
@@ -280,7 +280,7 @@ function shouldMergeComposerShadowKeys(itemKeyFromPath, checkSettingPaths) {
|
|
|
280
280
|
}
|
|
281
281
|
/** @deprecated Import from vscdb_reader — re-export for existing tests. */
|
|
282
282
|
export { mergeComposerShadowKeysFromReactiveBlob } from '../readers/vscdb_reader.js';
|
|
283
|
-
/** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
|
|
283
|
+
/** Plain JSON/TOML file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
|
|
284
284
|
function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
285
285
|
const resolvedPath = resolveRemediationConfigPath(configFilePath);
|
|
286
286
|
const hashIdx = resolvedPath.indexOf('#');
|
|
@@ -306,13 +306,23 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
|
306
306
|
}
|
|
307
307
|
if (!existsSync(resolvedPath))
|
|
308
308
|
return { ok: false, reason: 'file_not_found' };
|
|
309
|
+
const rawText = readFileSync(resolvedPath, 'utf8');
|
|
310
|
+
if (resolvedPath.toLowerCase().endsWith('.toml')) {
|
|
311
|
+
const parsedToml = parseTomlText(rawText);
|
|
312
|
+
if (parsedToml === null)
|
|
313
|
+
return { ok: false, reason: 'parse_error' };
|
|
314
|
+
return { ok: true, json: parsedToml };
|
|
315
|
+
}
|
|
309
316
|
// OpenCode configs are JSONC (comments / trailing commas); parse strict JSON first, then
|
|
310
317
|
// fall back to JSONC sanitization so comment-bearing files are not skipped as parse_error.
|
|
311
|
-
const parsed = parseJsonWithJsoncFallback(
|
|
318
|
+
const parsed = parseJsonWithJsoncFallback(rawText);
|
|
312
319
|
if (parsed === null)
|
|
313
320
|
return { ok: false, reason: 'parse_error' };
|
|
314
321
|
return { ok: true, json: parsed };
|
|
315
322
|
}
|
|
323
|
+
function isSupportedTextConfigFormat(fileFormat) {
|
|
324
|
+
return fileFormat === 'json' || fileFormat === 'toml';
|
|
325
|
+
}
|
|
316
326
|
/**
|
|
317
327
|
* Evaluate all checks in a secondary group against the group's config file.
|
|
318
328
|
* Returns true only if every check passes (ops-based). Non-ops checks are treated as failing.
|
|
@@ -354,7 +364,7 @@ function violationFromCheck(entry, compliance, check, expected) {
|
|
|
354
364
|
/** Evaluate one manifest row against on-disk config (used by gate + post-restart verify). */
|
|
355
365
|
export function evaluateManifestEntryCompliance(entry) {
|
|
356
366
|
const compliance = entry.fix ?? entry.compliance;
|
|
357
|
-
if (!compliance || compliance.file_format
|
|
367
|
+
if (!compliance || !isSupportedTextConfigFormat(compliance.file_format))
|
|
358
368
|
return { violations: [] };
|
|
359
369
|
const checks = compliance.checks ?? [];
|
|
360
370
|
if (checks.length === 0)
|
|
@@ -463,9 +473,9 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
463
473
|
skippedNoCompliance++;
|
|
464
474
|
continue;
|
|
465
475
|
}
|
|
466
|
-
if (compliance.file_format
|
|
476
|
+
if (!isSupportedTextConfigFormat(compliance.file_format)) {
|
|
467
477
|
skippedNonJson++;
|
|
468
|
-
hookRunLog(`compliance_check: skipping
|
|
478
|
+
hookRunLog(`compliance_check: skipping unsupported file_format=${compliance.file_format} uuid=${entry.uuid}`);
|
|
469
479
|
continue;
|
|
470
480
|
}
|
|
471
481
|
if ((compliance.checks ?? []).length === 0) {
|
|
@@ -795,7 +805,7 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
|
|
|
795
805
|
continue;
|
|
796
806
|
}
|
|
797
807
|
const spec = remediationFixSpec(inst);
|
|
798
|
-
const checks = spec?.file_format
|
|
808
|
+
const checks = isSupportedTextConfigFormat(spec?.file_format) ? (spec?.checks ?? []) : [];
|
|
799
809
|
if (checks.length === 0) {
|
|
800
810
|
remaining.push(raw);
|
|
801
811
|
continue;
|
|
@@ -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, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, 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, collectGrokCliVersion, 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';
|
|
@@ -170,6 +170,14 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
170
170
|
configFiles.push(entry);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
+
hookRunLog(`reading Grok CLI version`);
|
|
174
|
+
for (const entry of collectGrokCliVersion(HOME_DIR)) {
|
|
175
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
176
|
+
if (!existingPaths.has(key)) {
|
|
177
|
+
existingPaths.add(key);
|
|
178
|
+
configFiles.push(entry);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
173
181
|
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
174
182
|
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
175
183
|
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
-
import { atomicWriteJson, getRemediationStateDir } from './management_storage.js';
|
|
8
|
+
import { atomicWriteJson, getDeferredVscdbApplyPath, getRemediationStateDir, } from './management_storage.js';
|
|
9
9
|
import { hookRunLog } from './hook_logger.js';
|
|
10
10
|
export const REMEDIATION_APPLY_TRACKING_BASENAME = 'remediation_apply_tracking.json';
|
|
11
11
|
/** Consecutive post-restart verification failures before we stop autofix for a UUID. */
|
|
@@ -116,6 +116,9 @@ export function recordRemediationVerificationFailure(uuid, reason) {
|
|
|
116
116
|
* supported for unit tests only — do not use a global violation list from compliance_check
|
|
117
117
|
* in production: shadow-key merge bugs once made that list empty while the setting was still
|
|
118
118
|
* wrong, which incorrectly marked remediations verified.
|
|
119
|
+
*
|
|
120
|
+
* If the deferred vscdb queue file is still on disk, Cursor restart / apply_deferred_vscdb has
|
|
121
|
+
* not completed — leave pending and do not quarantine (setting unchanged is expected).
|
|
119
122
|
*/
|
|
120
123
|
export function processPendingPostRestartVerifications(violationProbe) {
|
|
121
124
|
const isStillViolating = typeof violationProbe === 'function'
|
|
@@ -123,6 +126,7 @@ export function processPendingPostRestartVerifications(violationProbe) {
|
|
|
123
126
|
: (uuid) => violationProbe.has(uuid);
|
|
124
127
|
const file = readRemediationApplyTrackingFile();
|
|
125
128
|
const outcomes = [];
|
|
129
|
+
const deferredQueuePending = existsSync(getDeferredVscdbApplyPath());
|
|
126
130
|
for (const [uuid, entry] of Object.entries(file.entries)) {
|
|
127
131
|
if (!entry.pending_post_restart_verify)
|
|
128
132
|
continue;
|
|
@@ -131,6 +135,10 @@ export function processPendingPostRestartVerifications(violationProbe) {
|
|
|
131
135
|
outcomes.push({ uuid, status: 'verified', consecutive_failures: 0 });
|
|
132
136
|
continue;
|
|
133
137
|
}
|
|
138
|
+
if (deferredQueuePending) {
|
|
139
|
+
hookRunLog(`remediation_tracking: defer verify uuid=${uuid} — deferred vscdb queue still pending (restart/apply not done yet)`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
134
142
|
const reason = 'Setting unchanged after apply — the agent may have changed its config format and auto-fix cannot apply this policy.';
|
|
135
143
|
const { entry: updated, newlyQuarantined } = recordRemediationVerificationFailure(uuid, reason);
|
|
136
144
|
outcomes.push({
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rename
|
|
|
2
2
|
import { delimiter, dirname, join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
|
-
import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
|
|
5
|
+
import { parseJsonWithJsoncFallback, parseTomlText, stringifyToml } from '../readers/file_readers.js';
|
|
6
6
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
7
7
|
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
8
8
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
@@ -1031,10 +1031,15 @@ export async function applyDeferredVscdbFromDisk() {
|
|
|
1031
1031
|
return true;
|
|
1032
1032
|
}
|
|
1033
1033
|
try {
|
|
1034
|
+
let applied = 0;
|
|
1035
|
+
let skippedMissing = 0;
|
|
1034
1036
|
for (const it of payload.items) {
|
|
1037
|
+
// Workspace storage can disappear between queue and restart (Cursor GC). Skip those
|
|
1038
|
+
// items instead of aborting the whole batch — other workspaces still need the write.
|
|
1035
1039
|
if (!existsSync(it.dbPath)) {
|
|
1036
|
-
hookRunLog(`deferred_vscdb: database missing ${it.dbPath}`);
|
|
1037
|
-
|
|
1040
|
+
hookRunLog(`deferred_vscdb: database missing — skipping ${it.dbPath}`);
|
|
1041
|
+
skippedMissing++;
|
|
1042
|
+
continue;
|
|
1038
1043
|
}
|
|
1039
1044
|
if (!assertSafeSqliteIdentifiersForItemTable(it.table, it.key_column, it.value_column)) {
|
|
1040
1045
|
return false;
|
|
@@ -1047,8 +1052,9 @@ export async function applyDeferredVscdbFromDisk() {
|
|
|
1047
1052
|
hookRunLog(`deferred_vscdb: INSERT OR REPLACE changed 0 rows key=${it.target_key} db=${it.dbPath} — keeping queue file`);
|
|
1048
1053
|
return false;
|
|
1049
1054
|
}
|
|
1055
|
+
applied++;
|
|
1050
1056
|
}
|
|
1051
|
-
hookRunLog(`deferred_vscdb: applied ${
|
|
1057
|
+
hookRunLog(`deferred_vscdb: applied ${applied} queued update(s) skipped_missing=${skippedMissing}`);
|
|
1052
1058
|
const authKey = readStoredAuthKey();
|
|
1053
1059
|
for (const u of postApplyUploads) {
|
|
1054
1060
|
if (!authKey) {
|
|
@@ -1422,7 +1428,7 @@ export function enforceRemediation(instruction) {
|
|
|
1422
1428
|
}
|
|
1423
1429
|
return { ok: true, deferredSqlite: false };
|
|
1424
1430
|
}
|
|
1425
|
-
if (fixSpec?.file_format !== 'json') {
|
|
1431
|
+
if (fixSpec?.file_format !== 'json' && fixSpec?.file_format !== 'toml') {
|
|
1426
1432
|
return fail(`unsupported file format: ${String(fixSpec?.file_format ?? 'undefined')}`);
|
|
1427
1433
|
}
|
|
1428
1434
|
const worktreeRoot = parseWorktreeRootFromPath(inst.config_file_path);
|
|
@@ -1445,26 +1451,39 @@ export function enforceRemediation(instruction) {
|
|
|
1445
1451
|
else if (!existsSync(dir)) {
|
|
1446
1452
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1447
1453
|
}
|
|
1448
|
-
|
|
1454
|
+
const isToml = fixSpec?.file_format === 'toml';
|
|
1455
|
+
let configObj = {};
|
|
1449
1456
|
if (existsSync(inst.config_file_path)) {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1457
|
+
const rawText = readFileSync(inst.config_file_path, 'utf8');
|
|
1458
|
+
if (isToml) {
|
|
1459
|
+
const parsed = parseTomlText(rawText);
|
|
1460
|
+
if (parsed !== null) {
|
|
1461
|
+
configObj = parsed;
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
hookRunLog(`remediation_enforce: could not parse existing TOML file, starting fresh uuid=${inst.uuid}`);
|
|
1465
|
+
}
|
|
1458
1466
|
}
|
|
1459
1467
|
else {
|
|
1460
|
-
|
|
1468
|
+
// OpenCode configs are JSONC; parse strict JSON first, then JSONC fallback so a
|
|
1469
|
+
// comment-bearing opencode.json(c) keeps its existing settings instead of being reset to
|
|
1470
|
+
// {} on a parse failure. NOTE: the write-back below is JSON.stringify, so comments and
|
|
1471
|
+
// trailing commas in the original file are not preserved (same as every other agent's
|
|
1472
|
+
// JSON config) — only the key/value settings are retained and patched.
|
|
1473
|
+
const parsed = parseJsonWithJsoncFallback(rawText);
|
|
1474
|
+
if (parsed !== null) {
|
|
1475
|
+
configObj = parsed;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${inst.uuid}`);
|
|
1479
|
+
}
|
|
1461
1480
|
}
|
|
1462
1481
|
}
|
|
1463
1482
|
for (const check of checks) {
|
|
1464
|
-
applyCheck(
|
|
1483
|
+
applyCheck(configObj, check);
|
|
1465
1484
|
}
|
|
1466
|
-
syncFlatGlobalCursorIgnoreListKey(
|
|
1467
|
-
const content = JSON.stringify(
|
|
1485
|
+
syncFlatGlobalCursorIgnoreListKey(configObj);
|
|
1486
|
+
const content = isToml ? stringifyToml(configObj) : JSON.stringify(configObj, null, 2);
|
|
1468
1487
|
const tmp = `${inst.config_file_path}.tmp`;
|
|
1469
1488
|
writeFileSync(tmp, content, 'utf8');
|
|
1470
1489
|
renameSync(tmp, inst.config_file_path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "log-llm-config",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.14",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,8 +56,9 @@
|
|
|
56
56
|
"vitest": "^4.1.8"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"axios": "^1.
|
|
59
|
+
"axios": "^1.18.1",
|
|
60
60
|
"canonicalize": "^2.1.0",
|
|
61
|
-
"optimus-tofu": "^0.1.18"
|
|
61
|
+
"optimus-tofu": "^0.1.18",
|
|
62
|
+
"smol-toml": "^1.7.1"
|
|
62
63
|
}
|
|
63
64
|
}
|