log-llm-config 1.0.30 → 1.0.32
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/endpoint_client/startup_api.js +1 -5
- package/dist/log_config_files/collection/config_collector.js +35 -5
- package/dist/log_config_files/collection/directory_collector.js +49 -3
- package/dist/log_config_files/paths/pattern_resolver.js +18 -6
- package/dist/log_config_files/runtime/main_runner.js +5 -0
- package/dist/log_config_files/sender/batch_sender.js +0 -7
- package/dist/log_config_files/sender/signing.js +7 -19
- package/package.json +4 -3
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { executeBody } from './http_transport.js';
|
|
2
2
|
export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) => {
|
|
3
3
|
const payload = JSON.stringify(body);
|
|
4
|
-
|
|
5
|
-
const { statusCode, statusMessage, headers, body: responseBody } = await executeBody(endpointUrl, 'POST', payload, timeoutMs);
|
|
6
|
-
console.log(`HTTP ${statusCode} ${statusMessage}`);
|
|
7
|
-
console.log('Response headers:', headers);
|
|
4
|
+
const { statusCode, statusMessage, body: responseBody } = await executeBody(endpointUrl, 'POST', payload, timeoutMs);
|
|
8
5
|
if (statusCode >= 400) {
|
|
9
6
|
const msg = statusCode === 413
|
|
10
7
|
? `413 Request Entity Too Large: ${responseBody.substring(0, 200)}`
|
|
@@ -13,7 +10,6 @@ export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) =>
|
|
|
13
10
|
}
|
|
14
11
|
if (!responseBody)
|
|
15
12
|
return { status: 'error', message: 'Empty response from endpoint' };
|
|
16
|
-
console.log('Raw endpoint response body:', responseBody);
|
|
17
13
|
try {
|
|
18
14
|
return JSON.parse(responseBody);
|
|
19
15
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { readJSONFile, readMCPConfig, readMarkdownFile, readInstalledExtensions } from '../readers/file_readers.js';
|
|
4
5
|
import { getExtensionsCachePath, getExtensionsCacheInstalledSuffix, getVscdbPath } from '../paths/path_constants_helpers.js';
|
|
5
6
|
import { resolvePatternToTargets, normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
6
7
|
import { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
7
|
-
import { collectDirectoryEntries } from './directory_collector.js';
|
|
8
|
+
import { collectDirectoryEntries, collectDirectoryMetadata } from './directory_collector.js';
|
|
8
9
|
import { collectVscdbEntries } from '../readers/vscdb_config_builder.js';
|
|
9
10
|
import { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
|
|
10
11
|
function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs = [], absolutePathPrefixes = [], mcpToolGlobSpec = null, clientPathConstants = null, pathResolutionSpecs = null) {
|
|
@@ -46,9 +47,18 @@ function readContentByFormat(path, format) {
|
|
|
46
47
|
}
|
|
47
48
|
function collectRegularFileEntry(t, enrichByFileType) {
|
|
48
49
|
const format = t.content_format || 'json';
|
|
49
|
-
|
|
50
|
+
let content = readContentByFormat(t.path, format);
|
|
50
51
|
if (content === null)
|
|
51
52
|
return null;
|
|
53
|
+
// For JSON-format files, backend/policy engine expect raw_content to be the parsed object (e.g. permissions.allow)
|
|
54
|
+
if (format === 'json' && typeof content === 'string') {
|
|
55
|
+
try {
|
|
56
|
+
content = JSON.parse(content);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// leave as string if parse fails
|
|
60
|
+
}
|
|
61
|
+
}
|
|
52
62
|
let raw;
|
|
53
63
|
if (t.raw_key && typeof content === 'string') {
|
|
54
64
|
raw = { [t.raw_key]: content };
|
|
@@ -81,7 +91,20 @@ function handleInstalledExtensions(handledSpecialPaths, configFiles, extensionsC
|
|
|
81
91
|
configFiles.push({ file_type: 'cursor_extensions', file_path: `${extensionsCachePath}${suffix}`, raw_content: { installedExtensions: installed, source: 'extensions.user.cache', extracted_at: new Date().toISOString() } });
|
|
82
92
|
}
|
|
83
93
|
function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, options) {
|
|
84
|
-
|
|
94
|
+
// Prefer process.env.HOME so hook invoker (e.g. Cursor) can pass real home when it differs from os.homedir()
|
|
95
|
+
const home = (process.env.HOME && process.env.HOME.trim()) || homedir();
|
|
96
|
+
try {
|
|
97
|
+
const claudeSettingsPath = join(home, '.claude', 'settings.json');
|
|
98
|
+
writeFileSync(join(process.cwd(), '.optimus-home-debug.json'), JSON.stringify({
|
|
99
|
+
process_env_HOME: process.env.HOME ?? null,
|
|
100
|
+
homedir: homedir(),
|
|
101
|
+
resolved_home: home,
|
|
102
|
+
claude_settings_path: claudeSettingsPath,
|
|
103
|
+
claude_settings_exists: existsSync(claudeSettingsPath),
|
|
104
|
+
cwd: process.cwd(),
|
|
105
|
+
}, null, 2));
|
|
106
|
+
}
|
|
107
|
+
catch (_) { }
|
|
85
108
|
const pathConstants = options?.client_path_constants;
|
|
86
109
|
if (!pathConstants)
|
|
87
110
|
throw new Error('client_path_constants required from API response but not provided');
|
|
@@ -101,7 +124,14 @@ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, optio
|
|
|
101
124
|
onProgress(`scanning file_type=${t.file_type}`);
|
|
102
125
|
}
|
|
103
126
|
if (t.isDirectory) {
|
|
104
|
-
|
|
127
|
+
if (metadataOnlyFileTypes.has(t.file_type) || t.collect_style === 'metadata') {
|
|
128
|
+
const entry = collectDirectoryMetadata(t);
|
|
129
|
+
if (entry)
|
|
130
|
+
configFiles.push(entry);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
configFiles.push(...collectDirectoryEntries(t));
|
|
134
|
+
}
|
|
105
135
|
continue;
|
|
106
136
|
}
|
|
107
137
|
if (extensionsInstalledSuffix && t.path.includes(extensionsInstalledSuffix)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
4
4
|
function collectSubdirMdFiles(dirPath, fileType, mdFilename, source) {
|
|
@@ -32,7 +32,10 @@ function collectDirectoryEntries(t) {
|
|
|
32
32
|
if (!entry.isFile() || !matchName(entry.name))
|
|
33
33
|
continue;
|
|
34
34
|
const fullPath = join(t.path, entry.name);
|
|
35
|
-
|
|
35
|
+
// Prefer parsed JSON for .json files so backend/policy engine see top-level keys (e.g. permissions.allow)
|
|
36
|
+
const content = entry.name.endsWith('.json')
|
|
37
|
+
? readJSONFile(fullPath) ?? readMarkdownFile(fullPath)
|
|
38
|
+
: readMarkdownFile(fullPath) ?? readJSONFile(fullPath);
|
|
36
39
|
if (content !== null) {
|
|
37
40
|
const raw = typeof content === 'string' ? { content, source: 'file' } : content;
|
|
38
41
|
results.push({ file_type: t.file_type, file_path: fullPath, raw_content: raw });
|
|
@@ -47,4 +50,47 @@ function collectDirectoryEntries(t) {
|
|
|
47
50
|
}
|
|
48
51
|
return results;
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
/**
|
|
54
|
+
* For metadata-style DIR targets: scan files matching dir_glob, return one entry
|
|
55
|
+
* with the highest mtime found. Falls back to the directory's own mtime if no files match.
|
|
56
|
+
*/
|
|
57
|
+
function collectDirectoryMetadata(t) {
|
|
58
|
+
try {
|
|
59
|
+
const dirStat = statSync(t.path);
|
|
60
|
+
let bestMtime = dirStat.mtime;
|
|
61
|
+
let bestPath = t.path;
|
|
62
|
+
const glob = t.dir_glob ?? '*';
|
|
63
|
+
const matchName = (name) => {
|
|
64
|
+
if (glob === '*')
|
|
65
|
+
return true;
|
|
66
|
+
if (glob.startsWith('*.'))
|
|
67
|
+
return name.endsWith(glob.slice(1));
|
|
68
|
+
return name === glob;
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
for (const entry of readdirSync(t.path, { withFileTypes: true })) {
|
|
72
|
+
if (!entry.isFile() || !matchName(entry.name))
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const fileStat = statSync(join(t.path, entry.name));
|
|
76
|
+
if (fileStat.mtime > bestMtime) {
|
|
77
|
+
bestMtime = fileStat.mtime;
|
|
78
|
+
bestPath = join(t.path, entry.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { /* ignore unreadable files */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { /* ignore unreadable dir */ }
|
|
85
|
+
return {
|
|
86
|
+
file_type: t.file_type,
|
|
87
|
+
file_path: bestPath,
|
|
88
|
+
raw_content: { filename: bestPath, last_modified: bestMtime.toISOString(), source: 'file_metadata' },
|
|
89
|
+
collect_style: 'metadata',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export { collectDirectoryEntries, collectDirectoryMetadata };
|
|
@@ -60,17 +60,27 @@ function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, content
|
|
|
60
60
|
return targets;
|
|
61
61
|
const isDir = norm.endsWith('/');
|
|
62
62
|
const [before, after] = norm.split('*');
|
|
63
|
-
const beforeNorm = before.replace(/\/+$/, '');
|
|
64
63
|
const afterNorm = after.replace(/^\/+/, '');
|
|
64
|
+
// If `before` doesn't end with '/', the * is mid-segment (e.g. "extensions/saoudrizwan.claude-dev*/" →
|
|
65
|
+
// parent="extensions/", namePrefix="saoudrizwan.claude-dev"). Split on last '/' to get the real base dir.
|
|
66
|
+
let parentPart = before.replace(/\/+$/, '');
|
|
67
|
+
let namePrefix = '';
|
|
68
|
+
if (!before.endsWith('/')) {
|
|
69
|
+
const lastSlash = parentPart.lastIndexOf('/');
|
|
70
|
+
if (lastSlash !== -1) {
|
|
71
|
+
namePrefix = parentPart.slice(lastSlash + 1);
|
|
72
|
+
parentPart = parentPart.slice(0, lastSlash);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
65
75
|
let basePath;
|
|
66
|
-
if (
|
|
67
|
-
basePath = join(home,
|
|
76
|
+
if (parentPart.startsWith('~/')) {
|
|
77
|
+
basePath = join(home, parentPart.slice(2));
|
|
68
78
|
}
|
|
69
|
-
else if (
|
|
70
|
-
basePath =
|
|
79
|
+
else if (parentPart.startsWith('/') && absolutePathPrefixes.some((prefix) => parentPart.startsWith(prefix))) {
|
|
80
|
+
basePath = parentPart;
|
|
71
81
|
}
|
|
72
82
|
else {
|
|
73
|
-
basePath = join(projectRoot,
|
|
83
|
+
basePath = join(projectRoot, parentPart.startsWith('/') ? parentPart.slice(1) : parentPart);
|
|
74
84
|
}
|
|
75
85
|
if (!existsSync(basePath))
|
|
76
86
|
return targets;
|
|
@@ -78,6 +88,8 @@ function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, content
|
|
|
78
88
|
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
79
89
|
if (!entry.isDirectory())
|
|
80
90
|
continue;
|
|
91
|
+
if (namePrefix && !entry.name.startsWith(namePrefix))
|
|
92
|
+
continue;
|
|
81
93
|
const resolvedPath = join(basePath, entry.name, afterNorm);
|
|
82
94
|
if (!existsSync(resolvedPath))
|
|
83
95
|
continue;
|
|
@@ -118,6 +118,11 @@ async function main() {
|
|
|
118
118
|
const collectMs = Date.now() - collectStartMs;
|
|
119
119
|
const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
|
|
120
120
|
hookRunLog(`collected ${configFiles.length} config file(s) collect_ms=${collectMs} file_types=${fileTypes}`);
|
|
121
|
+
const claudeSettings = configFiles.filter((c) => c.file_type === 'claude_settings');
|
|
122
|
+
if (claudeSettings.length > 0)
|
|
123
|
+
hookRunLog(`claude_settings in batch: ${claudeSettings.length} path(s): ${claudeSettings.map((c) => c.file_path).join(', ')}`);
|
|
124
|
+
else
|
|
125
|
+
hookRunLog(`claude_settings in batch: 0 (none collected)`);
|
|
121
126
|
if (configFiles.length === 0) {
|
|
122
127
|
hookRunLog('no config files found, exiting');
|
|
123
128
|
process.exit(0);
|
|
@@ -103,13 +103,6 @@ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequ
|
|
|
103
103
|
catch (error) {
|
|
104
104
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
105
105
|
hookRunLog(`batch chunk error: ${errorMessage}`);
|
|
106
|
-
const is413 = errorMessage.includes('413') || errorMessage.includes('Request Entity Too Large') || errorMessage.includes('Entity Too Large');
|
|
107
|
-
if (is413 && chunk.length > 1) {
|
|
108
|
-
hookRunLog(`413 error, splitting ${chunk.length} files`);
|
|
109
|
-
splitChunk(chunks, chunkIndex);
|
|
110
|
-
// Don't increment — retry with the smaller first half
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
106
|
totals.failed += chunk.length;
|
|
114
107
|
}
|
|
115
108
|
chunkIndex++;
|
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return value.map(canonicalizeValue);
|
|
6
|
-
}
|
|
7
|
-
if (value && typeof value === 'object') {
|
|
8
|
-
const sortedKeys = Object.keys(value).sort();
|
|
9
|
-
const result = {};
|
|
10
|
-
for (const key of sortedKeys) {
|
|
11
|
-
result[key] = canonicalizeValue(value[key]);
|
|
12
|
-
}
|
|
13
|
-
return result;
|
|
14
|
-
}
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
/** Canonicalize payload (sort keys, compact JSON) to match server. */
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
const canonicalize = createRequire(import.meta.url)('canonicalize');
|
|
4
|
+
/** RFC 8785 canonical JSON — must match server's rfc8785.dumps() exactly. */
|
|
18
5
|
function canonicalizePayload(payload) {
|
|
19
|
-
|
|
6
|
+
const out = canonicalize(payload);
|
|
7
|
+
return out ?? '{}';
|
|
20
8
|
}
|
|
21
|
-
/** Create HMAC-SHA256 signature for a payload. */
|
|
9
|
+
/** Create HMAC-SHA256 signature for a payload (canonicalize then sign). */
|
|
22
10
|
function createSignature(payload, keyHex) {
|
|
23
11
|
const canonicalPayload = canonicalizePayload(payload);
|
|
24
12
|
const keyBuffer = Buffer.from(keyHex, 'hex');
|
|
25
13
|
return crypto.createHmac('sha256', keyBuffer).update(canonicalPayload).digest('hex');
|
|
26
14
|
}
|
|
27
|
-
export {
|
|
15
|
+
export { canonicalizePayload, createSignature };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "log-llm-config",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"vitest": "^4.0.15"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"axios": "^1.7.9"
|
|
52
|
+
"axios": "^1.7.9",
|
|
53
|
+
"canonicalize": "^2.1.0"
|
|
53
54
|
}
|
|
54
|
-
}
|
|
55
|
+
}
|