log-llm-config 1.0.9 → 1.0.11
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 +10 -1
- package/dist/endpoint_client.js +57 -0
- package/dist/log_config_files.js +595 -11
- package/package.json +1 -1
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
|
}
|
package/dist/endpoint_client.js
CHANGED
|
@@ -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:';
|
package/dist/log_config_files.js
CHANGED
|
@@ -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,307 @@ 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
|
+
* Expand a path pattern containing a single * into concrete directory paths.
|
|
121
|
+
* Example: ~/.claude/plugins/marketplaces/<name>/skills/ -> [ .../anthropic-agent-skills/skills, ... ].
|
|
122
|
+
*/
|
|
123
|
+
function expandGlobPathPattern(pathPattern, fileType, home, projectRoot) {
|
|
124
|
+
const norm = pathPattern.replace(/\\/g, '/');
|
|
125
|
+
const targets = [];
|
|
126
|
+
const asteriskIndex = norm.indexOf('*');
|
|
127
|
+
if (asteriskIndex === -1)
|
|
128
|
+
return targets;
|
|
129
|
+
const isDir = norm.endsWith('/');
|
|
130
|
+
const [before, after] = norm.split('*');
|
|
131
|
+
const beforeNorm = before.replace(/\/+$/, '');
|
|
132
|
+
const afterNorm = after.replace(/^\/+/, '');
|
|
133
|
+
let basePath;
|
|
134
|
+
if (beforeNorm.startsWith('~/')) {
|
|
135
|
+
basePath = join(home, beforeNorm.slice(2));
|
|
136
|
+
}
|
|
137
|
+
else if (beforeNorm.startsWith('/') && (beforeNorm.startsWith('/Library') || beforeNorm.startsWith('/Users') || beforeNorm.startsWith('/home'))) {
|
|
138
|
+
basePath = beforeNorm;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const stripLeading = beforeNorm.startsWith('/') ? beforeNorm.slice(1) : beforeNorm;
|
|
142
|
+
basePath = join(projectRoot, stripLeading);
|
|
143
|
+
}
|
|
144
|
+
if (!existsSync(basePath))
|
|
145
|
+
return targets;
|
|
146
|
+
try {
|
|
147
|
+
const entries = readdirSync(basePath, { withFileTypes: true });
|
|
148
|
+
const glob = DIR_GLOB_BY_FILE_TYPE[fileType];
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (!entry.isDirectory())
|
|
151
|
+
continue;
|
|
152
|
+
const resolvedPath = join(basePath, entry.name, afterNorm);
|
|
153
|
+
if (!existsSync(resolvedPath))
|
|
154
|
+
continue;
|
|
155
|
+
targets.push({
|
|
156
|
+
path: resolvedPath,
|
|
157
|
+
file_type: fileType,
|
|
158
|
+
isDirectory: isDir,
|
|
159
|
+
glob: Array.isArray(glob) ? glob[0] : glob,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// ignore read errors
|
|
165
|
+
}
|
|
166
|
+
return targets;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve a single (path_pattern, file_type) from the API to concrete collection targets.
|
|
170
|
+
*/
|
|
171
|
+
function resolvePatternToTargets(pathPattern, fileType, projectRoot, home) {
|
|
172
|
+
const norm = pathPattern.replace(/\\/g, '/');
|
|
173
|
+
const targets = [];
|
|
174
|
+
// Glob pattern: expand ~/.claude/plugins/marketplaces/*/skills/ etc.
|
|
175
|
+
if (norm.includes('*')) {
|
|
176
|
+
return expandGlobPathPattern(pathPattern, fileType, home, projectRoot);
|
|
177
|
+
}
|
|
178
|
+
// Special: state.vscdb (any fragment) -> single target; we read and split in collection
|
|
179
|
+
if (norm.includes('state.vscdb')) {
|
|
180
|
+
targets.push({ path: VSCDB_PATH, file_type: fileType });
|
|
181
|
+
return targets;
|
|
182
|
+
}
|
|
183
|
+
// Special: extensions.user.cache#installedExtensions
|
|
184
|
+
if (norm.includes('extensions.user.cache') && norm.includes('installedExtensions')) {
|
|
185
|
+
targets.push({ path: `${EXTENSIONS_CACHE_PATH}#installedExtensions`, file_type: 'cursor_extensions' });
|
|
186
|
+
return targets;
|
|
187
|
+
}
|
|
188
|
+
const isDir = norm.endsWith('/');
|
|
189
|
+
const rel = norm.replace(/\/+$/, '');
|
|
190
|
+
const push = (p) => {
|
|
191
|
+
if (isDir) {
|
|
192
|
+
const glob = DIR_GLOB_BY_FILE_TYPE[fileType];
|
|
193
|
+
targets.push({
|
|
194
|
+
path: p,
|
|
195
|
+
file_type: fileType,
|
|
196
|
+
isDirectory: true,
|
|
197
|
+
glob: Array.isArray(glob) ? glob[0] : glob,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
targets.push({ path: p, file_type: fileType });
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
// Absolute path (real absolute, not /.cursor/...)
|
|
205
|
+
if (norm.startsWith('/') && (norm.startsWith('/Library') || norm.startsWith('/Users') || norm.startsWith('/home') || norm.startsWith('/etc'))) {
|
|
206
|
+
push(norm);
|
|
207
|
+
return targets;
|
|
208
|
+
}
|
|
209
|
+
// Home-relative
|
|
210
|
+
if (norm.startsWith('~/')) {
|
|
211
|
+
push(join(home, rel.slice(2)));
|
|
212
|
+
return targets;
|
|
213
|
+
}
|
|
214
|
+
// Project- or both-relative: /.cursor/..., .cursor/..., CLAUDE.md, etc.
|
|
215
|
+
const stripLeadingSlash = rel.startsWith('/') ? rel.slice(1) : rel;
|
|
216
|
+
push(join(projectRoot, stripLeadingSlash));
|
|
217
|
+
if (!stripLeadingSlash.startsWith('.')) {
|
|
218
|
+
// e.g. CLAUDE.md at project root only for first push; also at ~/.claude/CLAUDE.md
|
|
219
|
+
if (stripLeadingSlash === 'CLAUDE.md' || stripLeadingSlash === 'CLAUDE.local.md') {
|
|
220
|
+
push(join(home, '.claude', stripLeadingSlash));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
push(join(home, stripLeadingSlash));
|
|
225
|
+
}
|
|
226
|
+
return targets;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Collect config files by resolving API patterns to paths and reading existing files.
|
|
230
|
+
* Reuses readMCPConfig, readJSONFile, readMarkdownFile, readVSCDBState, readInstalledExtensions and directory scan helpers.
|
|
231
|
+
*/
|
|
232
|
+
function collectConfigFilesFromPatterns(patterns, projectRoot) {
|
|
233
|
+
const home = homedir();
|
|
234
|
+
const seenPaths = new Set();
|
|
235
|
+
const targets = [];
|
|
236
|
+
for (const { path_pattern, file_type } of patterns) {
|
|
237
|
+
for (const t of resolvePatternToTargets(path_pattern, file_type, projectRoot, home)) {
|
|
238
|
+
const key = `${t.path}\t${t.file_type}`;
|
|
239
|
+
if (seenPaths.has(key))
|
|
240
|
+
continue;
|
|
241
|
+
seenPaths.add(key);
|
|
242
|
+
targets.push(t);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const configFiles = [];
|
|
246
|
+
const handledSpecialPaths = new Set();
|
|
247
|
+
for (const t of targets) {
|
|
248
|
+
if (t.isDirectory) {
|
|
249
|
+
if (!existsSync(t.path))
|
|
250
|
+
continue;
|
|
251
|
+
try {
|
|
252
|
+
const entries = readdirSync(t.path, { withFileTypes: true });
|
|
253
|
+
const glob = t.glob || '*.md';
|
|
254
|
+
const matchName = (name) => glob.startsWith('*') ? name.endsWith(glob.slice(1)) : name === glob;
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
if (!entry.isFile())
|
|
257
|
+
continue;
|
|
258
|
+
if (!matchName(entry.name))
|
|
259
|
+
continue;
|
|
260
|
+
const fullPath = join(t.path, entry.name);
|
|
261
|
+
const content = readMarkdownFile(fullPath) ?? readJSONFile(fullPath);
|
|
262
|
+
if (content !== null) {
|
|
263
|
+
const raw = typeof content === 'string' ? { content, source: 'file' } : content;
|
|
264
|
+
configFiles.push({
|
|
265
|
+
file_type: t.file_type,
|
|
266
|
+
file_path: fullPath,
|
|
267
|
+
raw_content: raw,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// cursor_rule: also support subdirs with RULE.md
|
|
272
|
+
if (t.file_type === 'cursor_rule') {
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
if (!entry.isDirectory())
|
|
275
|
+
continue;
|
|
276
|
+
const ruleMd = join(t.path, entry.name, 'RULE.md');
|
|
277
|
+
if (existsSync(ruleMd)) {
|
|
278
|
+
const content = readMarkdownFile(ruleMd);
|
|
279
|
+
if (content !== null) {
|
|
280
|
+
configFiles.push({
|
|
281
|
+
file_type: t.file_type,
|
|
282
|
+
file_path: ruleMd,
|
|
283
|
+
raw_content: { content, source: 'cursor_rule_file' },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// claude_skill / codex_skill: also support subdirs with SKILL.md (e.g. skills/<name>/SKILL.md)
|
|
290
|
+
if (t.file_type === 'claude_skill' || t.file_type === 'codex_skill') {
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (!entry.isDirectory())
|
|
293
|
+
continue;
|
|
294
|
+
const skillMd = join(t.path, entry.name, 'SKILL.md');
|
|
295
|
+
if (existsSync(skillMd)) {
|
|
296
|
+
const content = readMarkdownFile(skillMd);
|
|
297
|
+
if (content !== null) {
|
|
298
|
+
configFiles.push({
|
|
299
|
+
file_type: t.file_type,
|
|
300
|
+
file_path: skillMd,
|
|
301
|
+
raw_content: { content, source: 'skill_file' },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
console.warn(`Error reading directory ${t.path}:`, err instanceof Error ? err.message : String(err));
|
|
310
|
+
}
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// Single file or special
|
|
314
|
+
if (t.path.includes('#installedExtensions')) {
|
|
315
|
+
if (handledSpecialPaths.has(EXTENSIONS_CACHE_PATH))
|
|
316
|
+
continue;
|
|
317
|
+
handledSpecialPaths.add(EXTENSIONS_CACHE_PATH);
|
|
318
|
+
const installed = readInstalledExtensions();
|
|
319
|
+
if (installed.length > 0) {
|
|
320
|
+
configFiles.push({
|
|
321
|
+
file_type: 'cursor_extensions',
|
|
322
|
+
file_path: `${EXTENSIONS_CACHE_PATH}#installedExtensions`,
|
|
323
|
+
raw_content: {
|
|
324
|
+
installedExtensions: installed,
|
|
325
|
+
source: 'extensions.user.cache',
|
|
326
|
+
extracted_at: new Date().toISOString(),
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (t.path === VSCDB_PATH) {
|
|
333
|
+
if (handledSpecialPaths.has(VSCDB_PATH))
|
|
334
|
+
continue;
|
|
335
|
+
handledSpecialPaths.add(VSCDB_PATH);
|
|
336
|
+
const vscdbState = readVSCDBState();
|
|
337
|
+
if (vscdbState) {
|
|
338
|
+
if (vscdbState.composerState) {
|
|
339
|
+
configFiles.push({
|
|
340
|
+
file_type: 'vscode_settings',
|
|
341
|
+
file_path: `${VSCDB_PATH}#composerState`,
|
|
342
|
+
raw_content: {
|
|
343
|
+
composerState: vscdbState.composerState,
|
|
344
|
+
source: 'state.vscdb',
|
|
345
|
+
extracted_at: new Date().toISOString(),
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const chat = vscdbState.chat;
|
|
350
|
+
if (chat?.tools) {
|
|
351
|
+
configFiles.push({
|
|
352
|
+
file_type: 'vscode_settings',
|
|
353
|
+
file_path: `${VSCDB_PATH}#chat.tools`,
|
|
354
|
+
raw_content: {
|
|
355
|
+
chat: { tools: chat.tools },
|
|
356
|
+
source: 'state.vscdb',
|
|
357
|
+
extracted_at: new Date().toISOString(),
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const extRec = vscdbState.extensionsAssistant;
|
|
362
|
+
if (extRec?.recommendations?.length) {
|
|
363
|
+
configFiles.push({
|
|
364
|
+
file_type: 'cursor_extensions',
|
|
365
|
+
file_path: `${VSCDB_PATH}#extensionsAssistant.recommendations`,
|
|
366
|
+
raw_content: {
|
|
367
|
+
extensionsAssistant: { recommendations: extRec.recommendations },
|
|
368
|
+
source: 'state.vscdb',
|
|
369
|
+
extracted_at: new Date().toISOString(),
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const ext = vscdbState.extensions;
|
|
374
|
+
if (ext?.trustedPublishers?.length) {
|
|
375
|
+
configFiles.push({
|
|
376
|
+
file_type: 'cursor_extensions',
|
|
377
|
+
file_path: `${VSCDB_PATH}#extensions.trustedPublishers`,
|
|
378
|
+
raw_content: {
|
|
379
|
+
extensions: { trustedPublishers: ext.trustedPublishers },
|
|
380
|
+
source: 'state.vscdb',
|
|
381
|
+
extracted_at: new Date().toISOString(),
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (!existsSync(t.path))
|
|
389
|
+
continue;
|
|
390
|
+
const isJson = t.path.endsWith('.json') ||
|
|
391
|
+
t.file_type === 'mcp_config' ||
|
|
392
|
+
t.file_type === 'cursor_hooks' ||
|
|
393
|
+
t.file_type === 'claude_settings';
|
|
394
|
+
const content = isJson ? readJSONFile(t.path) ?? readMCPConfig(t.path) : readMarkdownFile(t.path);
|
|
395
|
+
if (content !== null) {
|
|
396
|
+
const raw = typeof content === 'string' ? { content, source: 'file' } : content;
|
|
397
|
+
configFiles.push({
|
|
398
|
+
file_type: t.file_type,
|
|
399
|
+
file_path: t.path,
|
|
400
|
+
raw_content: raw,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return configFiles;
|
|
405
|
+
}
|
|
93
406
|
/**
|
|
94
407
|
* Read and parse an MCP config file
|
|
95
408
|
*/
|
|
@@ -559,10 +872,12 @@ function getSubdirectoryClaudeFiles(projectRoot) {
|
|
|
559
872
|
/**
|
|
560
873
|
* Read state data from Cursor's state.vscdb SQLite database
|
|
561
874
|
* Extracts composerState, extensionsAssistant, and extensions data
|
|
875
|
+
* @param dbPath Optional path to state.vscdb file. If not provided, uses default VSCDB_PATH.
|
|
562
876
|
*/
|
|
563
|
-
function readVSCDBState() {
|
|
877
|
+
function readVSCDBState(dbPath) {
|
|
564
878
|
try {
|
|
565
|
-
|
|
879
|
+
const actualPath = dbPath || VSCDB_PATH;
|
|
880
|
+
if (!existsSync(actualPath)) {
|
|
566
881
|
return null;
|
|
567
882
|
}
|
|
568
883
|
// Check if sqlite3 is available
|
|
@@ -580,7 +895,7 @@ function readVSCDBState() {
|
|
|
580
895
|
const escapedComposerKey = composerStateKey.replace(/'/g, "''");
|
|
581
896
|
const composerQuery = `SELECT value FROM ItemTable WHERE key='${escapedComposerKey}'`;
|
|
582
897
|
try {
|
|
583
|
-
const composerResult = execSync(`sqlite3 "${
|
|
898
|
+
const composerResult = execSync(`sqlite3 "${actualPath}" "${composerQuery}"`, {
|
|
584
899
|
encoding: 'utf8',
|
|
585
900
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
586
901
|
}).trim();
|
|
@@ -612,7 +927,7 @@ function readVSCDBState() {
|
|
|
612
927
|
try {
|
|
613
928
|
const escapedKey = key.replace(/'/g, "''");
|
|
614
929
|
const query = `SELECT value FROM ItemTable WHERE key='${escapedKey}'`;
|
|
615
|
-
const result = execSync(`sqlite3 "${
|
|
930
|
+
const result = execSync(`sqlite3 "${actualPath}" "${query}"`, {
|
|
616
931
|
encoding: 'utf8',
|
|
617
932
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
618
933
|
}).trim();
|
|
@@ -649,7 +964,7 @@ function readVSCDBState() {
|
|
|
649
964
|
try {
|
|
650
965
|
const escapedKey = key.replace(/'/g, "''");
|
|
651
966
|
const query = `SELECT value FROM ItemTable WHERE key='${escapedKey}'`;
|
|
652
|
-
const result = execSync(`sqlite3 "${
|
|
967
|
+
const result = execSync(`sqlite3 "${actualPath}" "${query}"`, {
|
|
653
968
|
encoding: 'utf8',
|
|
654
969
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
655
970
|
}).trim();
|
|
@@ -677,6 +992,30 @@ function readVSCDBState() {
|
|
|
677
992
|
// Continue to next key
|
|
678
993
|
}
|
|
679
994
|
}
|
|
995
|
+
// Query for chat.tools data (for chat.tools.autoApprove setting)
|
|
996
|
+
const chatToolsKey = "chat.tools";
|
|
997
|
+
try {
|
|
998
|
+
const escapedKey = chatToolsKey.replace(/'/g, "''");
|
|
999
|
+
const query = `SELECT value FROM ItemTable WHERE key='${escapedKey}'`;
|
|
1000
|
+
const result = execSync(`sqlite3 "${actualPath}" "${query}"`, {
|
|
1001
|
+
encoding: 'utf8',
|
|
1002
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1003
|
+
}).trim();
|
|
1004
|
+
if (result && result !== '') {
|
|
1005
|
+
try {
|
|
1006
|
+
const parsed = JSON.parse(result);
|
|
1007
|
+
if (parsed && typeof parsed === 'object') {
|
|
1008
|
+
stateData.chat = { tools: parsed };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (parseError) {
|
|
1012
|
+
// Continue if parse fails
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
// Continue if query fails
|
|
1018
|
+
}
|
|
680
1019
|
// If we found composerState, also check if extensionsAssistant/extensions are nested within it
|
|
681
1020
|
if (stateData.composerState && typeof stateData.composerState === 'object') {
|
|
682
1021
|
const composerState = stateData.composerState;
|
|
@@ -689,7 +1028,7 @@ function readVSCDBState() {
|
|
|
689
1028
|
stateData.extensions = composerState.extensions;
|
|
690
1029
|
}
|
|
691
1030
|
}
|
|
692
|
-
// Return the combined state data (composerState, extensionsAssistant, extensions)
|
|
1031
|
+
// Return the combined state data (composerState, extensionsAssistant, extensions, chat, installedExtensions)
|
|
693
1032
|
// If we have any data, return it; otherwise return null
|
|
694
1033
|
if (Object.keys(stateData).length > 0) {
|
|
695
1034
|
return stateData;
|
|
@@ -701,6 +1040,37 @@ function readVSCDBState() {
|
|
|
701
1040
|
return null;
|
|
702
1041
|
}
|
|
703
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Read installed extensions from Cursor extensions cache file.
|
|
1045
|
+
* Original cache has: result[].identifier.id, result[].manifest.displayName, .version, .publisher.
|
|
1046
|
+
* We collect publisher so UI can distinguish same displayName (e.g. "Python" from Microsoft vs Anysphere) later.
|
|
1047
|
+
* Returns array: [{ id, displayName, version, publisher }, ...]
|
|
1048
|
+
*/
|
|
1049
|
+
function readInstalledExtensions() {
|
|
1050
|
+
const extensions = [];
|
|
1051
|
+
try {
|
|
1052
|
+
if (!existsSync(EXTENSIONS_CACHE_PATH)) {
|
|
1053
|
+
return extensions;
|
|
1054
|
+
}
|
|
1055
|
+
const content = readFileSync(EXTENSIONS_CACHE_PATH, 'utf-8');
|
|
1056
|
+
const data = JSON.parse(content);
|
|
1057
|
+
if (data.result && Array.isArray(data.result)) {
|
|
1058
|
+
for (const ext of data.result) {
|
|
1059
|
+
const id = ext.identifier?.id;
|
|
1060
|
+
const displayName = ext.manifest?.displayName || ext.manifest?.publisher || '';
|
|
1061
|
+
const version = ext.manifest?.version || '';
|
|
1062
|
+
const publisher = ext.manifest?.publisher;
|
|
1063
|
+
if (id) {
|
|
1064
|
+
extensions.push({ id, displayName, version, publisher });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
catch (error) {
|
|
1070
|
+
console.warn(`Error reading installed extensions from cache:`, error instanceof Error ? error.message : String(error));
|
|
1071
|
+
}
|
|
1072
|
+
return extensions;
|
|
1073
|
+
}
|
|
704
1074
|
/**
|
|
705
1075
|
* Collect all configuration files
|
|
706
1076
|
*/
|
|
@@ -752,6 +1122,22 @@ function collectConfigFiles() {
|
|
|
752
1122
|
},
|
|
753
1123
|
});
|
|
754
1124
|
}
|
|
1125
|
+
// Store chat.tools as vscode_settings (for chat.tools.autoApprove policy)
|
|
1126
|
+
// Send as separate file entry to match policy engine expectations
|
|
1127
|
+
const chat = vscdbState.chat;
|
|
1128
|
+
if (chat?.tools) {
|
|
1129
|
+
configFiles.push({
|
|
1130
|
+
file_type: 'vscode_settings',
|
|
1131
|
+
file_path: `${VSCDB_PATH}#chat.tools`, // Use fragment to distinguish
|
|
1132
|
+
raw_content: {
|
|
1133
|
+
chat: {
|
|
1134
|
+
tools: chat.tools,
|
|
1135
|
+
},
|
|
1136
|
+
source: 'state.vscdb',
|
|
1137
|
+
extracted_at: new Date().toISOString(),
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
755
1141
|
// Store extensionsAssistant.recommendations as cursor_extensions
|
|
756
1142
|
// (same type as .vscode/extensions.json recommendations)
|
|
757
1143
|
// Structure must match evaluation plan path: extensionsAssistant.recommendations
|
|
@@ -786,6 +1172,23 @@ function collectConfigFiles() {
|
|
|
786
1172
|
});
|
|
787
1173
|
}
|
|
788
1174
|
}
|
|
1175
|
+
// Store installed extensions as cursor_extensions
|
|
1176
|
+
// Read from extensions cache file (more reliable than state.vscdb)
|
|
1177
|
+
// Format: [{ id, displayName, version }, ...]
|
|
1178
|
+
// This is collected separately from state.vscdb since it's in a different file
|
|
1179
|
+
const installedExtensions = readInstalledExtensions();
|
|
1180
|
+
if (installedExtensions.length > 0) {
|
|
1181
|
+
console.log(`Found ${installedExtensions.length} installed extension(s) at: ${EXTENSIONS_CACHE_PATH}`);
|
|
1182
|
+
configFiles.push({
|
|
1183
|
+
file_type: 'cursor_extensions',
|
|
1184
|
+
file_path: `${EXTENSIONS_CACHE_PATH}#installedExtensions`, // Use fragment to distinguish
|
|
1185
|
+
raw_content: {
|
|
1186
|
+
installedExtensions: installedExtensions,
|
|
1187
|
+
source: 'extensions.user.cache',
|
|
1188
|
+
extracted_at: new Date().toISOString(),
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
789
1192
|
// Read Claude configuration files (project root, local overrides, home directory)
|
|
790
1193
|
for (const { path, file_type } of CLAUDE_FILE_PATHS) {
|
|
791
1194
|
try {
|
|
@@ -1220,12 +1623,16 @@ async function sendConfigFile(configFile, hardwareUuid, authKey) {
|
|
|
1220
1623
|
file_path: configFile.file_path,
|
|
1221
1624
|
raw_content: configFile.raw_content,
|
|
1222
1625
|
};
|
|
1223
|
-
// Sign the payload
|
|
1626
|
+
// Sign the payload (metadata is not part of signature)
|
|
1224
1627
|
const signature = createSignature(payload, authKey.key);
|
|
1225
1628
|
const body = {
|
|
1226
1629
|
...payload,
|
|
1227
1630
|
signature,
|
|
1228
1631
|
key_id: authKey.key_id || '',
|
|
1632
|
+
metadata: {
|
|
1633
|
+
org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '',
|
|
1634
|
+
repo_identifier: process.env.GITHUB_REPOSITORY || process.env.GH_REPOSITORY || '',
|
|
1635
|
+
},
|
|
1229
1636
|
};
|
|
1230
1637
|
try {
|
|
1231
1638
|
console.log(`Posting ${configFile.file_type} to: ${apiUrl}`);
|
|
@@ -1387,17 +1794,194 @@ async function ensureAuthentication(hardwareUuid) {
|
|
|
1387
1794
|
throw new Error(`Failed to authenticate: ${error instanceof Error ? error.message : String(error)}`);
|
|
1388
1795
|
}
|
|
1389
1796
|
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Determine file type from file path (matches backend logic)
|
|
1799
|
+
*/
|
|
1800
|
+
function determineFileTypeFromPath(filePath) {
|
|
1801
|
+
if (!filePath) {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
// Normalize path (handle both forward and backslashes)
|
|
1805
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
1806
|
+
// Pattern matching (order matters - most specific first)
|
|
1807
|
+
// These patterns match the backend's file_type_detector.py
|
|
1808
|
+
// MCP configs
|
|
1809
|
+
if (normalizedPath.includes('/.cursor/mcp.json') ||
|
|
1810
|
+
normalizedPath.includes('mcp.json') ||
|
|
1811
|
+
normalizedPath.includes('.mcp.json')) {
|
|
1812
|
+
return 'mcp_config';
|
|
1813
|
+
}
|
|
1814
|
+
// Claude settings (most specific first)
|
|
1815
|
+
if (normalizedPath.includes('/Library/Application Support/ClaudeCode/managed-settings.json')) {
|
|
1816
|
+
return 'claude_settings';
|
|
1817
|
+
}
|
|
1818
|
+
if (normalizedPath.includes('.claude/settings.local.json') ||
|
|
1819
|
+
normalizedPath.includes('~/.claude/settings.local.json') ||
|
|
1820
|
+
normalizedPath.includes('/.claude/settings.local.json')) {
|
|
1821
|
+
return 'claude_settings';
|
|
1822
|
+
}
|
|
1823
|
+
if (normalizedPath.includes('.claude/settings.json') ||
|
|
1824
|
+
normalizedPath.includes('~/.config/claude/settings.json') ||
|
|
1825
|
+
normalizedPath.includes('/.config/claude/settings.json')) {
|
|
1826
|
+
return 'claude_settings';
|
|
1827
|
+
}
|
|
1828
|
+
// Cursor hooks
|
|
1829
|
+
if (normalizedPath.includes('.cursor/hooks.json') ||
|
|
1830
|
+
normalizedPath.includes('/Library/Application Support/Cursor/hooks.json')) {
|
|
1831
|
+
return 'cursor_hooks';
|
|
1832
|
+
}
|
|
1833
|
+
// Claude rule config
|
|
1834
|
+
if (normalizedPath.includes('CLAUDE.md') || normalizedPath.includes('CLAUDE.local.md')) {
|
|
1835
|
+
return 'claude_rule_config';
|
|
1836
|
+
}
|
|
1837
|
+
// Claude rules
|
|
1838
|
+
if (normalizedPath.includes('.claude/rules/')) {
|
|
1839
|
+
return 'claude_rule';
|
|
1840
|
+
}
|
|
1841
|
+
// Cursor rules
|
|
1842
|
+
if (normalizedPath.includes('.cursor/rules/')) {
|
|
1843
|
+
return 'cursor_rule';
|
|
1844
|
+
}
|
|
1845
|
+
// Cursor extensions
|
|
1846
|
+
if (normalizedPath.includes('.vscode/extensions.json') ||
|
|
1847
|
+
normalizedPath.includes('state.vscdb')) {
|
|
1848
|
+
return 'cursor_extensions';
|
|
1849
|
+
}
|
|
1850
|
+
// Default: try to infer from path structure
|
|
1851
|
+
if (normalizedPath.includes('.claude') && normalizedPath.endsWith('.json')) {
|
|
1852
|
+
return 'claude_settings';
|
|
1853
|
+
}
|
|
1854
|
+
if (normalizedPath.includes('.cursor') && normalizedPath.endsWith('.json')) {
|
|
1855
|
+
return 'cursor_hooks';
|
|
1856
|
+
}
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Log a single file by path
|
|
1861
|
+
*/
|
|
1862
|
+
async function logSingleFile(filePath) {
|
|
1863
|
+
const hardwareUuid = resolveHardwareUuid();
|
|
1864
|
+
// Ensure we have authentication
|
|
1865
|
+
const authKey = await ensureAuthentication(hardwareUuid);
|
|
1866
|
+
// Determine file type
|
|
1867
|
+
let fileType = determineFileTypeFromPath(filePath);
|
|
1868
|
+
// Read the file
|
|
1869
|
+
let rawContent = null;
|
|
1870
|
+
if (!existsSync(filePath)) {
|
|
1871
|
+
console.error(`File not found: ${filePath}`);
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
// Special handling for SQLite databases (state.vscdb)
|
|
1875
|
+
// Check if this is the state.vscdb file (with or without fragment)
|
|
1876
|
+
const isVSCDB = filePath.includes('state.vscdb');
|
|
1877
|
+
if (isVSCDB) {
|
|
1878
|
+
// Extract the actual file path (remove fragment if present)
|
|
1879
|
+
const actualPath = filePath.split('#')[0];
|
|
1880
|
+
// Read the SQLite database using the provided path
|
|
1881
|
+
const vscdbState = readVSCDBState(actualPath);
|
|
1882
|
+
if (vscdbState) {
|
|
1883
|
+
// Check if there's a fragment (e.g., "#composerState" or "#chat.tools")
|
|
1884
|
+
const fragment = filePath.includes('#') ? filePath.split('#')[1] : null;
|
|
1885
|
+
if (fragment === 'composerState' && vscdbState.composerState) {
|
|
1886
|
+
rawContent = {
|
|
1887
|
+
composerState: vscdbState.composerState,
|
|
1888
|
+
source: 'state.vscdb',
|
|
1889
|
+
extracted_at: new Date().toISOString(),
|
|
1890
|
+
};
|
|
1891
|
+
fileType = 'vscode_settings';
|
|
1892
|
+
}
|
|
1893
|
+
else if (fragment === 'chat.tools' && vscdbState.chat) {
|
|
1894
|
+
rawContent = {
|
|
1895
|
+
chat: {
|
|
1896
|
+
tools: vscdbState.chat.tools,
|
|
1897
|
+
},
|
|
1898
|
+
source: 'state.vscdb',
|
|
1899
|
+
extracted_at: new Date().toISOString(),
|
|
1900
|
+
};
|
|
1901
|
+
fileType = 'vscode_settings';
|
|
1902
|
+
}
|
|
1903
|
+
else if (!fragment && vscdbState.composerState) {
|
|
1904
|
+
// No fragment specified, default to composerState
|
|
1905
|
+
rawContent = {
|
|
1906
|
+
composerState: vscdbState.composerState,
|
|
1907
|
+
source: 'state.vscdb',
|
|
1908
|
+
extracted_at: new Date().toISOString(),
|
|
1909
|
+
};
|
|
1910
|
+
fileType = 'vscode_settings';
|
|
1911
|
+
}
|
|
1912
|
+
else {
|
|
1913
|
+
console.error(`Could not extract ${fragment || 'data'} from state.vscdb`);
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
console.error(`Could not read state.vscdb from ${actualPath}`);
|
|
1919
|
+
return false;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
// Try reading as JSON first
|
|
1924
|
+
rawContent = readJSONFile(filePath);
|
|
1925
|
+
if (!rawContent) {
|
|
1926
|
+
// Try reading as markdown/text
|
|
1927
|
+
const markdownContent = readMarkdownFile(filePath);
|
|
1928
|
+
if (markdownContent !== null) {
|
|
1929
|
+
rawContent = {
|
|
1930
|
+
content: markdownContent,
|
|
1931
|
+
source: 'file',
|
|
1932
|
+
};
|
|
1933
|
+
// Infer file type from path if not determined
|
|
1934
|
+
if (!fileType) {
|
|
1935
|
+
if (filePath.includes('CLAUDE.md') || filePath.includes('claude')) {
|
|
1936
|
+
fileType = 'claude_rule_config';
|
|
1937
|
+
}
|
|
1938
|
+
else if (filePath.includes('.cursor')) {
|
|
1939
|
+
fileType = 'cursor_rule';
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (!rawContent) {
|
|
1946
|
+
console.error(`Could not read file: ${filePath}`);
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
// If file type still not determined, default to claude_settings for JSON files
|
|
1950
|
+
if (!fileType) {
|
|
1951
|
+
if (filePath.includes('.claude') && filePath.endsWith('.json')) {
|
|
1952
|
+
fileType = 'claude_settings';
|
|
1953
|
+
}
|
|
1954
|
+
else if (filePath.includes('.cursor') && filePath.endsWith('.json')) {
|
|
1955
|
+
fileType = 'cursor_hooks';
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
console.error(`Could not determine file type for: ${filePath}`);
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
const configFile = {
|
|
1963
|
+
file_type: fileType,
|
|
1964
|
+
file_path: filePath,
|
|
1965
|
+
raw_content: rawContent,
|
|
1966
|
+
};
|
|
1967
|
+
return await sendConfigFile(configFile, hardwareUuid, authKey);
|
|
1968
|
+
}
|
|
1390
1969
|
/**
|
|
1391
1970
|
* Main execution
|
|
1392
1971
|
*/
|
|
1393
1972
|
async function main() {
|
|
1973
|
+
// Log all files (original behavior)
|
|
1394
1974
|
const hardwareUuid = resolveHardwareUuid();
|
|
1395
1975
|
console.log(`Hardware UUID: ${hardwareUuid}`);
|
|
1396
1976
|
// Ensure we have authentication (do handshake if needed)
|
|
1397
1977
|
const authKey = await ensureAuthentication(hardwareUuid);
|
|
1398
1978
|
console.log('Authentication verified, proceeding with config file logging.');
|
|
1399
|
-
//
|
|
1400
|
-
const
|
|
1979
|
+
// Prefer API-driven collection: call endpoint to get the complete list of what to look for
|
|
1980
|
+
const endpointBase = loadEndpointBase();
|
|
1981
|
+
const patternsResponse = await getFileCollectionPatterns(endpointBase);
|
|
1982
|
+
const configFiles = patternsResponse?.patterns?.length
|
|
1983
|
+
? collectConfigFilesFromPatterns(patternsResponse.patterns, PROJECT_ROOT)
|
|
1984
|
+
: collectConfigFiles();
|
|
1401
1985
|
console.log(`Collected ${configFiles.length} configuration file(s).`);
|
|
1402
1986
|
if (configFiles.length === 0) {
|
|
1403
1987
|
console.log('No configuration files found to log.');
|
|
@@ -1422,4 +2006,4 @@ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.includes
|
|
|
1422
2006
|
process.exit(1);
|
|
1423
2007
|
});
|
|
1424
2008
|
}
|
|
1425
|
-
export { main, collectConfigFiles, ensureAuthentication, sendConfigFile, createSignature, canonicalizePayload, readVSCDBState };
|
|
2009
|
+
export { main, collectConfigFiles, collectConfigFilesFromPatterns, resolvePatternToTargets, ensureAuthentication, sendConfigFile, createSignature, canonicalizePayload, readVSCDBState, logSingleFile, };
|