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.
@@ -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
- console.log('Sending payload to endpoint:', endpointUrl, payload);
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
- const content = readContentByFormat(t.path, format);
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
- const home = homedir();
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
- configFiles.push(...collectDirectoryEntries(t));
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
- const content = readMarkdownFile(fullPath) ?? readJSONFile(fullPath);
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
- export { collectDirectoryEntries };
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 (beforeNorm.startsWith('~/')) {
67
- basePath = join(home, beforeNorm.slice(2));
76
+ if (parentPart.startsWith('~/')) {
77
+ basePath = join(home, parentPart.slice(2));
68
78
  }
69
- else if (beforeNorm.startsWith('/') && absolutePathPrefixes.some((prefix) => beforeNorm.startsWith(prefix))) {
70
- basePath = beforeNorm;
79
+ else if (parentPart.startsWith('/') && absolutePathPrefixes.some((prefix) => parentPart.startsWith(prefix))) {
80
+ basePath = parentPart;
71
81
  }
72
82
  else {
73
- basePath = join(projectRoot, beforeNorm.startsWith('/') ? beforeNorm.slice(1) : beforeNorm);
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
- /** Recursively sort object keys to produce a canonical representation. */
3
- function canonicalizeValue(value) {
4
- if (Array.isArray(value)) {
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
- return JSON.stringify(canonicalizeValue(payload));
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 { canonicalizeValue, canonicalizePayload, createSignature };
15
+ export { canonicalizePayload, createSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.0.30",
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
+ }