log-llm-config 1.0.9 → 1.0.10

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/cli.js CHANGED
@@ -61,7 +61,16 @@ const main = async () => {
61
61
  return;
62
62
  }
63
63
  if (args[0] === 'log_config_files') {
64
- const { main } = await import('./log_config_files.js');
64
+ const { main, logSingleFile } = await import('./log_config_files.js');
65
+ // Check if a file path was provided
66
+ const filePathArg = args.find((arg, idx) => idx > 0 && !arg.startsWith('-') && (arg.includes('/') || arg.endsWith('.json') || arg.endsWith('.md')));
67
+ if (filePathArg) {
68
+ // Log single file
69
+ const success = await logSingleFile(filePathArg);
70
+ process.exit(success ? 0 : 1);
71
+ return;
72
+ }
73
+ // Otherwise, log all files
65
74
  await main();
66
75
  return;
67
76
  }
@@ -1,6 +1,63 @@
1
1
  import http from 'node:http';
2
2
  import https from 'node:https';
3
3
  import { URL } from 'node:url';
4
+ /**
5
+ * GET file collection patterns from the backend (what to look for).
6
+ * No auth required. Returns the complete list of path_pattern + file_type.
7
+ */
8
+ export const getFileCollectionPatterns = async (apiBaseUrl, timeoutMs = 5000) => {
9
+ const base = apiBaseUrl.replace(/\/+$/, '');
10
+ const fullUrl = base.includes('/endpoint_security')
11
+ ? `${base}/api/file-patterns/`.replace(/([^/])\/+/g, '$1/')
12
+ : `${base}/endpoint_security/api/file-patterns/`;
13
+ const url = new URL(fullUrl);
14
+ const isHttps = url.protocol === 'https:';
15
+ let hostname = url.hostname;
16
+ if (hostname === 'localhost' || hostname === '::1') {
17
+ hostname = '127.0.0.1';
18
+ }
19
+ const requestOptions = {
20
+ hostname,
21
+ port: url.port || (isHttps ? 443 : 80),
22
+ path: url.pathname + url.search,
23
+ method: 'GET',
24
+ timeout: timeoutMs,
25
+ };
26
+ const transport = isHttps ? https.request : http.request;
27
+ return new Promise((resolve) => {
28
+ const req = transport(requestOptions, (res) => {
29
+ let responseBody = '';
30
+ res.setEncoding('utf8');
31
+ res.on('data', (chunk) => {
32
+ responseBody += chunk;
33
+ });
34
+ res.on('end', () => {
35
+ if (res.statusCode !== 200 || !responseBody) {
36
+ resolve(null);
37
+ return;
38
+ }
39
+ try {
40
+ const parsed = JSON.parse(responseBody);
41
+ if (Array.isArray(parsed.patterns) && typeof parsed.count === 'number') {
42
+ resolve(parsed);
43
+ }
44
+ else {
45
+ resolve(null);
46
+ }
47
+ }
48
+ catch {
49
+ resolve(null);
50
+ }
51
+ });
52
+ });
53
+ req.on('error', () => resolve(null));
54
+ req.on('timeout', () => {
55
+ req.destroy();
56
+ resolve(null);
57
+ });
58
+ req.end();
59
+ });
60
+ };
4
61
  export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) => {
5
62
  const url = new URL(endpointUrl);
6
63
  const isHttps = url.protocol === 'https:';
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
6
6
  import crypto from 'node:crypto';
7
7
  import { execSync } from 'node:child_process';
8
8
  import path from 'node:path';
9
- import { postStartupPayload } from './endpoint_client.js';
9
+ import { getFileCollectionPatterns, postStartupPayload } from './endpoint_client.js';
10
10
  const AUTH_KEY_RELATIVE_PATH = path.join('opt-ai-sec', 'management', 'auth_key.txt');
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = dirname(__filename);
@@ -51,9 +51,21 @@ const JSON_FILE_PATHS = [
51
51
  path: join(homedir(), '.claude', 'settings.json'),
52
52
  file_type: 'claude_settings',
53
53
  },
54
+ // Project-level Claude local settings (local overrides)
55
+ {
56
+ path: join(PROJECT_ROOT, '.claude', 'settings.local.json'),
57
+ file_type: 'claude_settings',
58
+ },
59
+ // User-level Claude local settings (local overrides)
60
+ {
61
+ path: join(homedir(), '.claude', 'settings.local.json'),
62
+ file_type: 'claude_settings',
63
+ },
54
64
  ];
55
65
  // VS Code/Cursor state database path
56
66
  const VSCDB_PATH = join(homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
67
+ // Cursor extensions cache file path
68
+ const EXTENSIONS_CACHE_PATH = join(homedir(), 'Library', 'Application Support', 'Cursor', 'CachedProfilesData', '__default__profile__', 'extensions.user.cache');
57
69
  // Claude configuration file paths
58
70
  const CLAUDE_FILE_PATHS = [
59
71
  // Project Root
@@ -90,6 +102,236 @@ const PLUGIN_SEARCH_DIRS = [
90
102
  PROJECT_ROOT, // Project-level plugins
91
103
  join(homedir(), '.claude', 'plugins'), // User-level plugins (if this becomes a standard location)
92
104
  ];
105
+ /** Glob(s) for directory scans by file_type. First match used if array. */
106
+ const DIR_GLOB_BY_FILE_TYPE = {
107
+ cursor_command: '*.md',
108
+ cursor_rule: '*.md',
109
+ claude_rule: '*.md',
110
+ claude_subagent: '*.md',
111
+ cursor_subagent: '*.md',
112
+ claude_skill: '*.md',
113
+ codex_skill: '*.md',
114
+ claude_plugin_command: '*.md',
115
+ claude_plugin_manifest: 'plugin.json',
116
+ claude_plugin_hooks: 'hooks.json',
117
+ claude_plugin_mcp: '.mcp.json',
118
+ };
119
+ /**
120
+ * Resolve a single (path_pattern, file_type) from the API to concrete collection targets.
121
+ */
122
+ function resolvePatternToTargets(pathPattern, fileType, projectRoot, home) {
123
+ const norm = pathPattern.replace(/\\/g, '/');
124
+ const targets = [];
125
+ // Special: state.vscdb (any fragment) -> single target; we read and split in collection
126
+ if (norm.includes('state.vscdb')) {
127
+ targets.push({ path: VSCDB_PATH, file_type: fileType });
128
+ return targets;
129
+ }
130
+ // Special: extensions.user.cache#installedExtensions
131
+ if (norm.includes('extensions.user.cache') && norm.includes('installedExtensions')) {
132
+ targets.push({ path: `${EXTENSIONS_CACHE_PATH}#installedExtensions`, file_type: 'cursor_extensions' });
133
+ return targets;
134
+ }
135
+ const isDir = norm.endsWith('/');
136
+ const rel = norm.replace(/\/+$/, '');
137
+ const push = (p) => {
138
+ if (isDir) {
139
+ const glob = DIR_GLOB_BY_FILE_TYPE[fileType];
140
+ targets.push({
141
+ path: p,
142
+ file_type: fileType,
143
+ isDirectory: true,
144
+ glob: Array.isArray(glob) ? glob[0] : glob,
145
+ });
146
+ }
147
+ else {
148
+ targets.push({ path: p, file_type: fileType });
149
+ }
150
+ };
151
+ // Absolute path (real absolute, not /.cursor/...)
152
+ if (norm.startsWith('/') && (norm.startsWith('/Library') || norm.startsWith('/Users') || norm.startsWith('/home') || norm.startsWith('/etc'))) {
153
+ push(norm);
154
+ return targets;
155
+ }
156
+ // Home-relative
157
+ if (norm.startsWith('~/')) {
158
+ push(join(home, rel.slice(2)));
159
+ return targets;
160
+ }
161
+ // Project- or both-relative: /.cursor/..., .cursor/..., CLAUDE.md, etc.
162
+ const stripLeadingSlash = rel.startsWith('/') ? rel.slice(1) : rel;
163
+ push(join(projectRoot, stripLeadingSlash));
164
+ if (!stripLeadingSlash.startsWith('.')) {
165
+ // e.g. CLAUDE.md at project root only for first push; also at ~/.claude/CLAUDE.md
166
+ if (stripLeadingSlash === 'CLAUDE.md' || stripLeadingSlash === 'CLAUDE.local.md') {
167
+ push(join(home, '.claude', stripLeadingSlash));
168
+ }
169
+ }
170
+ else {
171
+ push(join(home, stripLeadingSlash));
172
+ }
173
+ return targets;
174
+ }
175
+ /**
176
+ * Collect config files by resolving API patterns to paths and reading existing files.
177
+ * Reuses readMCPConfig, readJSONFile, readMarkdownFile, readVSCDBState, readInstalledExtensions and directory scan helpers.
178
+ */
179
+ function collectConfigFilesFromPatterns(patterns, projectRoot) {
180
+ const home = homedir();
181
+ const seenPaths = new Set();
182
+ const targets = [];
183
+ for (const { path_pattern, file_type } of patterns) {
184
+ for (const t of resolvePatternToTargets(path_pattern, file_type, projectRoot, home)) {
185
+ const key = `${t.path}\t${t.file_type}`;
186
+ if (seenPaths.has(key))
187
+ continue;
188
+ seenPaths.add(key);
189
+ targets.push(t);
190
+ }
191
+ }
192
+ const configFiles = [];
193
+ const handledSpecialPaths = new Set();
194
+ for (const t of targets) {
195
+ if (t.isDirectory) {
196
+ if (!existsSync(t.path))
197
+ continue;
198
+ try {
199
+ const entries = readdirSync(t.path, { withFileTypes: true });
200
+ const glob = t.glob || '*.md';
201
+ const matchName = (name) => glob.startsWith('*') ? name.endsWith(glob.slice(1)) : name === glob;
202
+ for (const entry of entries) {
203
+ if (!entry.isFile())
204
+ continue;
205
+ if (!matchName(entry.name))
206
+ continue;
207
+ const fullPath = join(t.path, entry.name);
208
+ const content = readMarkdownFile(fullPath) ?? readJSONFile(fullPath);
209
+ if (content !== null) {
210
+ const raw = typeof content === 'string' ? { content, source: 'file' } : content;
211
+ configFiles.push({
212
+ file_type: t.file_type,
213
+ file_path: fullPath,
214
+ raw_content: raw,
215
+ });
216
+ }
217
+ }
218
+ // cursor_rule: also support subdirs with RULE.md
219
+ if (t.file_type === 'cursor_rule') {
220
+ for (const entry of entries) {
221
+ if (!entry.isDirectory())
222
+ continue;
223
+ const ruleMd = join(t.path, entry.name, 'RULE.md');
224
+ if (existsSync(ruleMd)) {
225
+ const content = readMarkdownFile(ruleMd);
226
+ if (content !== null) {
227
+ configFiles.push({
228
+ file_type: t.file_type,
229
+ file_path: ruleMd,
230
+ raw_content: { content, source: 'cursor_rule_file' },
231
+ });
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ catch (err) {
238
+ console.warn(`Error reading directory ${t.path}:`, err instanceof Error ? err.message : String(err));
239
+ }
240
+ continue;
241
+ }
242
+ // Single file or special
243
+ if (t.path.includes('#installedExtensions')) {
244
+ if (handledSpecialPaths.has(EXTENSIONS_CACHE_PATH))
245
+ continue;
246
+ handledSpecialPaths.add(EXTENSIONS_CACHE_PATH);
247
+ const installed = readInstalledExtensions();
248
+ if (installed.length > 0) {
249
+ configFiles.push({
250
+ file_type: 'cursor_extensions',
251
+ file_path: `${EXTENSIONS_CACHE_PATH}#installedExtensions`,
252
+ raw_content: {
253
+ installedExtensions: installed,
254
+ source: 'extensions.user.cache',
255
+ extracted_at: new Date().toISOString(),
256
+ },
257
+ });
258
+ }
259
+ continue;
260
+ }
261
+ if (t.path === VSCDB_PATH) {
262
+ if (handledSpecialPaths.has(VSCDB_PATH))
263
+ continue;
264
+ handledSpecialPaths.add(VSCDB_PATH);
265
+ const vscdbState = readVSCDBState();
266
+ if (vscdbState) {
267
+ if (vscdbState.composerState) {
268
+ configFiles.push({
269
+ file_type: 'vscode_settings',
270
+ file_path: `${VSCDB_PATH}#composerState`,
271
+ raw_content: {
272
+ composerState: vscdbState.composerState,
273
+ source: 'state.vscdb',
274
+ extracted_at: new Date().toISOString(),
275
+ },
276
+ });
277
+ }
278
+ const chat = vscdbState.chat;
279
+ if (chat?.tools) {
280
+ configFiles.push({
281
+ file_type: 'vscode_settings',
282
+ file_path: `${VSCDB_PATH}#chat.tools`,
283
+ raw_content: {
284
+ chat: { tools: chat.tools },
285
+ source: 'state.vscdb',
286
+ extracted_at: new Date().toISOString(),
287
+ },
288
+ });
289
+ }
290
+ const extRec = vscdbState.extensionsAssistant;
291
+ if (extRec?.recommendations?.length) {
292
+ configFiles.push({
293
+ file_type: 'cursor_extensions',
294
+ file_path: `${VSCDB_PATH}#extensionsAssistant.recommendations`,
295
+ raw_content: {
296
+ extensionsAssistant: { recommendations: extRec.recommendations },
297
+ source: 'state.vscdb',
298
+ extracted_at: new Date().toISOString(),
299
+ },
300
+ });
301
+ }
302
+ const ext = vscdbState.extensions;
303
+ if (ext?.trustedPublishers?.length) {
304
+ configFiles.push({
305
+ file_type: 'cursor_extensions',
306
+ file_path: `${VSCDB_PATH}#extensions.trustedPublishers`,
307
+ raw_content: {
308
+ extensions: { trustedPublishers: ext.trustedPublishers },
309
+ source: 'state.vscdb',
310
+ extracted_at: new Date().toISOString(),
311
+ },
312
+ });
313
+ }
314
+ }
315
+ continue;
316
+ }
317
+ if (!existsSync(t.path))
318
+ continue;
319
+ const isJson = t.path.endsWith('.json') ||
320
+ t.file_type === 'mcp_config' ||
321
+ t.file_type === 'cursor_hooks' ||
322
+ t.file_type === 'claude_settings';
323
+ const content = isJson ? readJSONFile(t.path) ?? readMCPConfig(t.path) : readMarkdownFile(t.path);
324
+ if (content !== null) {
325
+ const raw = typeof content === 'string' ? { content, source: 'file' } : content;
326
+ configFiles.push({
327
+ file_type: t.file_type,
328
+ file_path: t.path,
329
+ raw_content: raw,
330
+ });
331
+ }
332
+ }
333
+ return configFiles;
334
+ }
93
335
  /**
94
336
  * Read and parse an MCP config file
95
337
  */
@@ -559,10 +801,12 @@ function getSubdirectoryClaudeFiles(projectRoot) {
559
801
  /**
560
802
  * Read state data from Cursor's state.vscdb SQLite database
561
803
  * Extracts composerState, extensionsAssistant, and extensions data
804
+ * @param dbPath Optional path to state.vscdb file. If not provided, uses default VSCDB_PATH.
562
805
  */
563
- function readVSCDBState() {
806
+ function readVSCDBState(dbPath) {
564
807
  try {
565
- if (!existsSync(VSCDB_PATH)) {
808
+ const actualPath = dbPath || VSCDB_PATH;
809
+ if (!existsSync(actualPath)) {
566
810
  return null;
567
811
  }
568
812
  // Check if sqlite3 is available
@@ -580,7 +824,7 @@ function readVSCDBState() {
580
824
  const escapedComposerKey = composerStateKey.replace(/'/g, "''");
581
825
  const composerQuery = `SELECT value FROM ItemTable WHERE key='${escapedComposerKey}'`;
582
826
  try {
583
- const composerResult = execSync(`sqlite3 "${VSCDB_PATH}" "${composerQuery}"`, {
827
+ const composerResult = execSync(`sqlite3 "${actualPath}" "${composerQuery}"`, {
584
828
  encoding: 'utf8',
585
829
  stdio: ['ignore', 'pipe', 'pipe'],
586
830
  }).trim();
@@ -612,7 +856,7 @@ function readVSCDBState() {
612
856
  try {
613
857
  const escapedKey = key.replace(/'/g, "''");
614
858
  const query = `SELECT value FROM ItemTable WHERE key='${escapedKey}'`;
615
- const result = execSync(`sqlite3 "${VSCDB_PATH}" "${query}"`, {
859
+ const result = execSync(`sqlite3 "${actualPath}" "${query}"`, {
616
860
  encoding: 'utf8',
617
861
  stdio: ['ignore', 'pipe', 'pipe'],
618
862
  }).trim();
@@ -649,7 +893,7 @@ function readVSCDBState() {
649
893
  try {
650
894
  const escapedKey = key.replace(/'/g, "''");
651
895
  const query = `SELECT value FROM ItemTable WHERE key='${escapedKey}'`;
652
- const result = execSync(`sqlite3 "${VSCDB_PATH}" "${query}"`, {
896
+ const result = execSync(`sqlite3 "${actualPath}" "${query}"`, {
653
897
  encoding: 'utf8',
654
898
  stdio: ['ignore', 'pipe', 'pipe'],
655
899
  }).trim();
@@ -677,6 +921,30 @@ function readVSCDBState() {
677
921
  // Continue to next key
678
922
  }
679
923
  }
924
+ // Query for chat.tools data (for chat.tools.autoApprove setting)
925
+ const chatToolsKey = "chat.tools";
926
+ try {
927
+ const escapedKey = chatToolsKey.replace(/'/g, "''");
928
+ const query = `SELECT value FROM ItemTable WHERE key='${escapedKey}'`;
929
+ const result = execSync(`sqlite3 "${actualPath}" "${query}"`, {
930
+ encoding: 'utf8',
931
+ stdio: ['ignore', 'pipe', 'pipe'],
932
+ }).trim();
933
+ if (result && result !== '') {
934
+ try {
935
+ const parsed = JSON.parse(result);
936
+ if (parsed && typeof parsed === 'object') {
937
+ stateData.chat = { tools: parsed };
938
+ }
939
+ }
940
+ catch (parseError) {
941
+ // Continue if parse fails
942
+ }
943
+ }
944
+ }
945
+ catch (error) {
946
+ // Continue if query fails
947
+ }
680
948
  // If we found composerState, also check if extensionsAssistant/extensions are nested within it
681
949
  if (stateData.composerState && typeof stateData.composerState === 'object') {
682
950
  const composerState = stateData.composerState;
@@ -689,7 +957,7 @@ function readVSCDBState() {
689
957
  stateData.extensions = composerState.extensions;
690
958
  }
691
959
  }
692
- // Return the combined state data (composerState, extensionsAssistant, extensions)
960
+ // Return the combined state data (composerState, extensionsAssistant, extensions, chat, installedExtensions)
693
961
  // If we have any data, return it; otherwise return null
694
962
  if (Object.keys(stateData).length > 0) {
695
963
  return stateData;
@@ -701,6 +969,37 @@ function readVSCDBState() {
701
969
  return null;
702
970
  }
703
971
  }
972
+ /**
973
+ * Read installed extensions from Cursor extensions cache file.
974
+ * Original cache has: result[].identifier.id, result[].manifest.displayName, .version, .publisher.
975
+ * We collect publisher so UI can distinguish same displayName (e.g. "Python" from Microsoft vs Anysphere) later.
976
+ * Returns array: [{ id, displayName, version, publisher }, ...]
977
+ */
978
+ function readInstalledExtensions() {
979
+ const extensions = [];
980
+ try {
981
+ if (!existsSync(EXTENSIONS_CACHE_PATH)) {
982
+ return extensions;
983
+ }
984
+ const content = readFileSync(EXTENSIONS_CACHE_PATH, 'utf-8');
985
+ const data = JSON.parse(content);
986
+ if (data.result && Array.isArray(data.result)) {
987
+ for (const ext of data.result) {
988
+ const id = ext.identifier?.id;
989
+ const displayName = ext.manifest?.displayName || ext.manifest?.publisher || '';
990
+ const version = ext.manifest?.version || '';
991
+ const publisher = ext.manifest?.publisher;
992
+ if (id) {
993
+ extensions.push({ id, displayName, version, publisher });
994
+ }
995
+ }
996
+ }
997
+ }
998
+ catch (error) {
999
+ console.warn(`Error reading installed extensions from cache:`, error instanceof Error ? error.message : String(error));
1000
+ }
1001
+ return extensions;
1002
+ }
704
1003
  /**
705
1004
  * Collect all configuration files
706
1005
  */
@@ -752,6 +1051,22 @@ function collectConfigFiles() {
752
1051
  },
753
1052
  });
754
1053
  }
1054
+ // Store chat.tools as vscode_settings (for chat.tools.autoApprove policy)
1055
+ // Send as separate file entry to match policy engine expectations
1056
+ const chat = vscdbState.chat;
1057
+ if (chat?.tools) {
1058
+ configFiles.push({
1059
+ file_type: 'vscode_settings',
1060
+ file_path: `${VSCDB_PATH}#chat.tools`, // Use fragment to distinguish
1061
+ raw_content: {
1062
+ chat: {
1063
+ tools: chat.tools,
1064
+ },
1065
+ source: 'state.vscdb',
1066
+ extracted_at: new Date().toISOString(),
1067
+ },
1068
+ });
1069
+ }
755
1070
  // Store extensionsAssistant.recommendations as cursor_extensions
756
1071
  // (same type as .vscode/extensions.json recommendations)
757
1072
  // Structure must match evaluation plan path: extensionsAssistant.recommendations
@@ -786,6 +1101,23 @@ function collectConfigFiles() {
786
1101
  });
787
1102
  }
788
1103
  }
1104
+ // Store installed extensions as cursor_extensions
1105
+ // Read from extensions cache file (more reliable than state.vscdb)
1106
+ // Format: [{ id, displayName, version }, ...]
1107
+ // This is collected separately from state.vscdb since it's in a different file
1108
+ const installedExtensions = readInstalledExtensions();
1109
+ if (installedExtensions.length > 0) {
1110
+ console.log(`Found ${installedExtensions.length} installed extension(s) at: ${EXTENSIONS_CACHE_PATH}`);
1111
+ configFiles.push({
1112
+ file_type: 'cursor_extensions',
1113
+ file_path: `${EXTENSIONS_CACHE_PATH}#installedExtensions`, // Use fragment to distinguish
1114
+ raw_content: {
1115
+ installedExtensions: installedExtensions,
1116
+ source: 'extensions.user.cache',
1117
+ extracted_at: new Date().toISOString(),
1118
+ },
1119
+ });
1120
+ }
789
1121
  // Read Claude configuration files (project root, local overrides, home directory)
790
1122
  for (const { path, file_type } of CLAUDE_FILE_PATHS) {
791
1123
  try {
@@ -1387,17 +1719,194 @@ async function ensureAuthentication(hardwareUuid) {
1387
1719
  throw new Error(`Failed to authenticate: ${error instanceof Error ? error.message : String(error)}`);
1388
1720
  }
1389
1721
  }
1722
+ /**
1723
+ * Determine file type from file path (matches backend logic)
1724
+ */
1725
+ function determineFileTypeFromPath(filePath) {
1726
+ if (!filePath) {
1727
+ return null;
1728
+ }
1729
+ // Normalize path (handle both forward and backslashes)
1730
+ const normalizedPath = filePath.replace(/\\/g, '/');
1731
+ // Pattern matching (order matters - most specific first)
1732
+ // These patterns match the backend's file_type_detector.py
1733
+ // MCP configs
1734
+ if (normalizedPath.includes('/.cursor/mcp.json') ||
1735
+ normalizedPath.includes('mcp.json') ||
1736
+ normalizedPath.includes('.mcp.json')) {
1737
+ return 'mcp_config';
1738
+ }
1739
+ // Claude settings (most specific first)
1740
+ if (normalizedPath.includes('/Library/Application Support/ClaudeCode/managed-settings.json')) {
1741
+ return 'claude_settings';
1742
+ }
1743
+ if (normalizedPath.includes('.claude/settings.local.json') ||
1744
+ normalizedPath.includes('~/.claude/settings.local.json') ||
1745
+ normalizedPath.includes('/.claude/settings.local.json')) {
1746
+ return 'claude_settings';
1747
+ }
1748
+ if (normalizedPath.includes('.claude/settings.json') ||
1749
+ normalizedPath.includes('~/.config/claude/settings.json') ||
1750
+ normalizedPath.includes('/.config/claude/settings.json')) {
1751
+ return 'claude_settings';
1752
+ }
1753
+ // Cursor hooks
1754
+ if (normalizedPath.includes('.cursor/hooks.json') ||
1755
+ normalizedPath.includes('/Library/Application Support/Cursor/hooks.json')) {
1756
+ return 'cursor_hooks';
1757
+ }
1758
+ // Claude rule config
1759
+ if (normalizedPath.includes('CLAUDE.md') || normalizedPath.includes('CLAUDE.local.md')) {
1760
+ return 'claude_rule_config';
1761
+ }
1762
+ // Claude rules
1763
+ if (normalizedPath.includes('.claude/rules/')) {
1764
+ return 'claude_rule';
1765
+ }
1766
+ // Cursor rules
1767
+ if (normalizedPath.includes('.cursor/rules/')) {
1768
+ return 'cursor_rule';
1769
+ }
1770
+ // Cursor extensions
1771
+ if (normalizedPath.includes('.vscode/extensions.json') ||
1772
+ normalizedPath.includes('state.vscdb')) {
1773
+ return 'cursor_extensions';
1774
+ }
1775
+ // Default: try to infer from path structure
1776
+ if (normalizedPath.includes('.claude') && normalizedPath.endsWith('.json')) {
1777
+ return 'claude_settings';
1778
+ }
1779
+ if (normalizedPath.includes('.cursor') && normalizedPath.endsWith('.json')) {
1780
+ return 'cursor_hooks';
1781
+ }
1782
+ return null;
1783
+ }
1784
+ /**
1785
+ * Log a single file by path
1786
+ */
1787
+ async function logSingleFile(filePath) {
1788
+ const hardwareUuid = resolveHardwareUuid();
1789
+ // Ensure we have authentication
1790
+ const authKey = await ensureAuthentication(hardwareUuid);
1791
+ // Determine file type
1792
+ let fileType = determineFileTypeFromPath(filePath);
1793
+ // Read the file
1794
+ let rawContent = null;
1795
+ if (!existsSync(filePath)) {
1796
+ console.error(`File not found: ${filePath}`);
1797
+ return false;
1798
+ }
1799
+ // Special handling for SQLite databases (state.vscdb)
1800
+ // Check if this is the state.vscdb file (with or without fragment)
1801
+ const isVSCDB = filePath.includes('state.vscdb');
1802
+ if (isVSCDB) {
1803
+ // Extract the actual file path (remove fragment if present)
1804
+ const actualPath = filePath.split('#')[0];
1805
+ // Read the SQLite database using the provided path
1806
+ const vscdbState = readVSCDBState(actualPath);
1807
+ if (vscdbState) {
1808
+ // Check if there's a fragment (e.g., "#composerState" or "#chat.tools")
1809
+ const fragment = filePath.includes('#') ? filePath.split('#')[1] : null;
1810
+ if (fragment === 'composerState' && vscdbState.composerState) {
1811
+ rawContent = {
1812
+ composerState: vscdbState.composerState,
1813
+ source: 'state.vscdb',
1814
+ extracted_at: new Date().toISOString(),
1815
+ };
1816
+ fileType = 'vscode_settings';
1817
+ }
1818
+ else if (fragment === 'chat.tools' && vscdbState.chat) {
1819
+ rawContent = {
1820
+ chat: {
1821
+ tools: vscdbState.chat.tools,
1822
+ },
1823
+ source: 'state.vscdb',
1824
+ extracted_at: new Date().toISOString(),
1825
+ };
1826
+ fileType = 'vscode_settings';
1827
+ }
1828
+ else if (!fragment && vscdbState.composerState) {
1829
+ // No fragment specified, default to composerState
1830
+ rawContent = {
1831
+ composerState: vscdbState.composerState,
1832
+ source: 'state.vscdb',
1833
+ extracted_at: new Date().toISOString(),
1834
+ };
1835
+ fileType = 'vscode_settings';
1836
+ }
1837
+ else {
1838
+ console.error(`Could not extract ${fragment || 'data'} from state.vscdb`);
1839
+ return false;
1840
+ }
1841
+ }
1842
+ else {
1843
+ console.error(`Could not read state.vscdb from ${actualPath}`);
1844
+ return false;
1845
+ }
1846
+ }
1847
+ else {
1848
+ // Try reading as JSON first
1849
+ rawContent = readJSONFile(filePath);
1850
+ if (!rawContent) {
1851
+ // Try reading as markdown/text
1852
+ const markdownContent = readMarkdownFile(filePath);
1853
+ if (markdownContent !== null) {
1854
+ rawContent = {
1855
+ content: markdownContent,
1856
+ source: 'file',
1857
+ };
1858
+ // Infer file type from path if not determined
1859
+ if (!fileType) {
1860
+ if (filePath.includes('CLAUDE.md') || filePath.includes('claude')) {
1861
+ fileType = 'claude_rule_config';
1862
+ }
1863
+ else if (filePath.includes('.cursor')) {
1864
+ fileType = 'cursor_rule';
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ }
1870
+ if (!rawContent) {
1871
+ console.error(`Could not read file: ${filePath}`);
1872
+ return false;
1873
+ }
1874
+ // If file type still not determined, default to claude_settings for JSON files
1875
+ if (!fileType) {
1876
+ if (filePath.includes('.claude') && filePath.endsWith('.json')) {
1877
+ fileType = 'claude_settings';
1878
+ }
1879
+ else if (filePath.includes('.cursor') && filePath.endsWith('.json')) {
1880
+ fileType = 'cursor_hooks';
1881
+ }
1882
+ else {
1883
+ console.error(`Could not determine file type for: ${filePath}`);
1884
+ return false;
1885
+ }
1886
+ }
1887
+ const configFile = {
1888
+ file_type: fileType,
1889
+ file_path: filePath,
1890
+ raw_content: rawContent,
1891
+ };
1892
+ return await sendConfigFile(configFile, hardwareUuid, authKey);
1893
+ }
1390
1894
  /**
1391
1895
  * Main execution
1392
1896
  */
1393
1897
  async function main() {
1898
+ // Log all files (original behavior)
1394
1899
  const hardwareUuid = resolveHardwareUuid();
1395
1900
  console.log(`Hardware UUID: ${hardwareUuid}`);
1396
1901
  // Ensure we have authentication (do handshake if needed)
1397
1902
  const authKey = await ensureAuthentication(hardwareUuid);
1398
1903
  console.log('Authentication verified, proceeding with config file logging.');
1399
- // Collect all config files
1400
- const configFiles = collectConfigFiles();
1904
+ // Prefer API-driven collection: call endpoint to get the complete list of what to look for
1905
+ const endpointBase = loadEndpointBase();
1906
+ const patternsResponse = await getFileCollectionPatterns(endpointBase);
1907
+ const configFiles = patternsResponse?.patterns?.length
1908
+ ? collectConfigFilesFromPatterns(patternsResponse.patterns, PROJECT_ROOT)
1909
+ : collectConfigFiles();
1401
1910
  console.log(`Collected ${configFiles.length} configuration file(s).`);
1402
1911
  if (configFiles.length === 0) {
1403
1912
  console.log('No configuration files found to log.');
@@ -1422,4 +1931,4 @@ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.includes
1422
1931
  process.exit(1);
1423
1932
  });
1424
1933
  }
1425
- export { main, collectConfigFiles, ensureAuthentication, sendConfigFile, createSignature, canonicalizePayload, readVSCDBState };
1934
+ export { main, collectConfigFiles, collectConfigFilesFromPatterns, resolvePatternToTargets, ensureAuthentication, sendConfigFile, createSignature, canonicalizePayload, readVSCDBState, logSingleFile, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {