log-llm-config-staging 1.3.98 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +104 -1
- package/dist/log_config_files/collection/config_collector.js +62 -3
- package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
- package/dist/log_config_files/collection/cursor_project_mcp_collector.js +47 -18
- package/dist/log_config_files/paths/pattern_resolver.js +17 -2
- package/dist/log_config_files/readers/file_readers.js +112 -2
- package/dist/log_config_files/runtime/main_runner.js +8 -1
- package/package.json +1 -1
|
@@ -19,6 +19,109 @@ function extensionsInstallationsPath(home) {
|
|
|
19
19
|
const base = claudeAppSupportDir(home);
|
|
20
20
|
return base ? join(base, 'extensions-installations.json') : '';
|
|
21
21
|
}
|
|
22
|
+
function claudeExtensionSettingsDir(home) {
|
|
23
|
+
const base = claudeAppSupportDir(home);
|
|
24
|
+
return base ? join(base, 'Claude Extensions Settings') : '';
|
|
25
|
+
}
|
|
26
|
+
/** Per-connector toggle: Claude Extensions Settings/<extension-id>.json → { isEnabled: boolean }. */
|
|
27
|
+
function readClaudeDesktopExtensionEnableSettings(home) {
|
|
28
|
+
const settingsDir = claudeExtensionSettingsDir(home);
|
|
29
|
+
if (!settingsDir || !existsSync(settingsDir))
|
|
30
|
+
return new Map();
|
|
31
|
+
const out = new Map();
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = readdirSync(settingsDir, { withFileTypes: true }).filter((d) => d.isFile());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
for (const dirent of entries) {
|
|
40
|
+
if (!dirent.name.endsWith('.json'))
|
|
41
|
+
continue;
|
|
42
|
+
const extId = dirent.name.slice(0, -'.json'.length);
|
|
43
|
+
if (!extId)
|
|
44
|
+
continue;
|
|
45
|
+
const raw = readJSONFile(join(settingsDir, dirent.name));
|
|
46
|
+
if (!raw || typeof raw !== 'object')
|
|
47
|
+
continue;
|
|
48
|
+
const isEnabled = raw.isEnabled;
|
|
49
|
+
if (typeof isEnabled === 'boolean') {
|
|
50
|
+
out.set(extId, isEnabled);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function applyExtensionEnableSettingsToRawContent(rawContent, settings) {
|
|
56
|
+
if (settings.size === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const extensions = rawContent.extensions;
|
|
59
|
+
if (!extensions || typeof extensions !== 'object')
|
|
60
|
+
return 0;
|
|
61
|
+
let applied = 0;
|
|
62
|
+
for (const [extId, enabled] of settings) {
|
|
63
|
+
const ext = extensions[extId];
|
|
64
|
+
if (!ext || typeof ext !== 'object')
|
|
65
|
+
continue;
|
|
66
|
+
ext.isEnabled = enabled;
|
|
67
|
+
applied += 1;
|
|
68
|
+
}
|
|
69
|
+
return applied;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Upload each Claude Extensions Settings/*.json as its own config file (raw on-disk JSON).
|
|
73
|
+
* Do not merge isEnabled into extensions-installations.json — that mutates the payload
|
|
74
|
+
* vs the source file and creates spurious inventory audit diffs.
|
|
75
|
+
*/
|
|
76
|
+
function collectClaudeDesktopExtensionSettingsFiles(home = homedir()) {
|
|
77
|
+
const settingsDir = claudeExtensionSettingsDir(home);
|
|
78
|
+
if (!settingsDir || !existsSync(settingsDir))
|
|
79
|
+
return [];
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = readdirSync(settingsDir, { withFileTypes: true }).filter((d) => d.isFile());
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const dirent of entries) {
|
|
89
|
+
if (!dirent.name.endsWith('.json'))
|
|
90
|
+
continue;
|
|
91
|
+
const filePath = join(settingsDir, dirent.name);
|
|
92
|
+
const raw = readJSONFile(filePath);
|
|
93
|
+
if (!raw || typeof raw !== 'object')
|
|
94
|
+
continue;
|
|
95
|
+
results.push({
|
|
96
|
+
file_type: 'claude_extension_settings',
|
|
97
|
+
file_path: filePath,
|
|
98
|
+
raw_content: raw,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Do not call during collection — mutates extensions-installations upload payloads.
|
|
105
|
+
* isEnabled is resolved server-side from claude_extension_settings uploads.
|
|
106
|
+
*/
|
|
107
|
+
function mergeClaudeDesktopExtensionEnableSettings(configFiles, home = homedir()) {
|
|
108
|
+
const settings = readClaudeDesktopExtensionEnableSettings(home);
|
|
109
|
+
if (settings.size === 0) {
|
|
110
|
+
return { applied: 0, settingsCount: 0 };
|
|
111
|
+
}
|
|
112
|
+
let applied = 0;
|
|
113
|
+
for (const entry of configFiles) {
|
|
114
|
+
if (entry.file_type !== 'claude_extensions_installations')
|
|
115
|
+
continue;
|
|
116
|
+
if (!entry.raw_content || typeof entry.raw_content !== 'object')
|
|
117
|
+
continue;
|
|
118
|
+
const extensions = entry.raw_content.extensions;
|
|
119
|
+
if (!extensions || typeof extensions !== 'object')
|
|
120
|
+
continue;
|
|
121
|
+
applied += applyExtensionEnableSettingsToRawContent(entry.raw_content, settings);
|
|
122
|
+
}
|
|
123
|
+
return { applied, settingsCount: settings.size };
|
|
124
|
+
}
|
|
22
125
|
function normalizePathForMatch(path) {
|
|
23
126
|
return path.replace(/\\/g, '/');
|
|
24
127
|
}
|
|
@@ -142,4 +245,4 @@ function collectClaudeDesktopExtensionManifests(home = homedir()) {
|
|
|
142
245
|
}
|
|
143
246
|
return results;
|
|
144
247
|
}
|
|
145
|
-
export { claudeAppSupportDir, claudeDesktopExtensionsDir, collectClaudeDesktopExtensionManifests, collectDiskOnlyClaudeDesktopExtensions, enrichClaudeDesktopExtensionsInstallationsUpload, };
|
|
248
|
+
export { claudeAppSupportDir, claudeDesktopExtensionsDir, claudeExtensionSettingsDir, collectClaudeDesktopExtensionSettingsFiles, collectClaudeDesktopExtensionManifests, collectDiskOnlyClaudeDesktopExtensions, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, readClaudeDesktopExtensionEnableSettings, };
|
|
@@ -1,13 +1,54 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { readJSONFile, readMCPConfig, readMarkdownFile, readInstalledExtensions } from '../readers/file_readers.js';
|
|
4
4
|
import { getExtensionsCachePath, getExtensionsCacheInstalledSuffix, getVscdbPath } from '../paths/path_constants_helpers.js';
|
|
5
5
|
import { resolvePatternToTargets, normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
6
6
|
import { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
7
|
+
import { applyCoworkSessionWhitelist } from './cowork_session_whitelist.js';
|
|
7
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
|
import { collapseMetadataByFileType } from './metadata_merge.js';
|
|
12
|
+
function withOpencodeEnvPatterns(patterns) {
|
|
13
|
+
const out = [...patterns];
|
|
14
|
+
const seen = new Set(out.map((p) => `${p.type}:${p.file_type}:${p.path}`));
|
|
15
|
+
const add = (pattern) => {
|
|
16
|
+
const key = `${pattern.type}:${pattern.file_type}:${pattern.path}`;
|
|
17
|
+
if (seen.has(key))
|
|
18
|
+
return;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
out.push(pattern);
|
|
21
|
+
};
|
|
22
|
+
const envConfig = (process.env.OPENCODE_CONFIG || '').trim();
|
|
23
|
+
if (envConfig) {
|
|
24
|
+
add({
|
|
25
|
+
path: envConfig,
|
|
26
|
+
type: 'file',
|
|
27
|
+
file_type: 'opencode_mcp',
|
|
28
|
+
content_format: 'json',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const envDirRaw = (process.env.OPENCODE_CONFIG_DIR || '').trim();
|
|
32
|
+
if (!envDirRaw)
|
|
33
|
+
return out;
|
|
34
|
+
const envDir = envDirRaw.replace(/[\\/]+$/, '');
|
|
35
|
+
add({ path: `${envDir}/opencode.json`, type: 'file', file_type: 'opencode_mcp', content_format: 'json' });
|
|
36
|
+
add({ path: `${envDir}/opencode.jsonc`, type: 'file', file_type: 'opencode_mcp', content_format: 'json' });
|
|
37
|
+
add({ path: `${envDir}/command-hooks.json`, type: 'file', file_type: 'opencode_command_hooks', content_format: 'json' });
|
|
38
|
+
add({ path: `${envDir}/command-hooks.jsonc`, type: 'file', file_type: 'opencode_command_hooks', content_format: 'json' });
|
|
39
|
+
add({ path: `${envDir}/AGENTS.md`, type: 'file', file_type: 'opencode_rule', content_format: 'markdown' });
|
|
40
|
+
add({ path: `${envDir}/agents/`, type: 'dir', file_type: 'opencode_subagent', content_format: 'markdown', dir_glob: '*.md' });
|
|
41
|
+
add({ path: `${envDir}/commands/`, type: 'dir', file_type: 'opencode_command', content_format: 'markdown', dir_glob: '*.md' });
|
|
42
|
+
add({ path: `${envDir}/skills/`, type: 'dir', file_type: 'opencode_skill', content_format: 'markdown', dir_glob: '*.md' });
|
|
43
|
+
add({ path: `${envDir}/mcp-auth.json`, type: 'file', file_type: 'opencode_mcp_auth', content_format: 'json' });
|
|
44
|
+
add({ path: `${envDir}/managed.json`, type: 'file', file_type: 'opencode_managed_preference', content_format: 'json' });
|
|
45
|
+
add({ path: `${envDir}/package.json`, type: 'file', file_type: 'opencode_plugin_manifest', content_format: 'json' });
|
|
46
|
+
add({ path: `${envDir}/plugins/`, type: 'dir', file_type: 'opencode_plugin', content_format: 'text', dir_glob: '*' });
|
|
47
|
+
add({ path: `${envDir}/plugin/`, type: 'dir', file_type: 'opencode_plugin', content_format: 'text', dir_glob: '*' });
|
|
48
|
+
add({ path: `${envDir}/plugins/*/package.json`, type: 'file', file_type: 'opencode_plugin_manifest', content_format: 'json' });
|
|
49
|
+
add({ path: `${envDir}/plugin/*/package.json`, type: 'file', file_type: 'opencode_plugin_manifest', content_format: 'json' });
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
11
52
|
function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs = [], absolutePathPrefixes = [], mcpToolGlobSpec = null, clientPathConstants = null, pathResolutionSpecs = null) {
|
|
12
53
|
const seenPaths = new Set();
|
|
13
54
|
const targets = [];
|
|
@@ -69,11 +110,28 @@ function collectRegularFileEntry(t, enrichByFileType) {
|
|
|
69
110
|
const recipe = enrichByFileType[t.file_type];
|
|
70
111
|
if (recipe)
|
|
71
112
|
enrichRawFromRecipe(raw, recipe);
|
|
113
|
+
// Cowork session metadata: reduce to the PII whitelist (hash identity, drop
|
|
114
|
+
// server URLs, keep only the activity-signal keys) BEFORE it leaves the
|
|
115
|
+
// device. Drop the entry entirely if nothing whitelisted survives so raw
|
|
116
|
+
// session content can never be emitted (e.g. on a JSON parse failure).
|
|
117
|
+
if (t.file_type === 'cowork_session_metadata') {
|
|
118
|
+
const filtered = applyCoworkSessionWhitelist(raw);
|
|
119
|
+
if (filtered === null)
|
|
120
|
+
return null;
|
|
121
|
+
raw = filtered;
|
|
122
|
+
}
|
|
72
123
|
return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: raw };
|
|
73
124
|
}
|
|
74
125
|
function collectMetadataEntry(t) {
|
|
75
126
|
try {
|
|
76
127
|
const stats = statSync(t.path);
|
|
128
|
+
if (stats.isDirectory?.()) {
|
|
129
|
+
try {
|
|
130
|
+
if (readdirSync(t.path).length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
catch { /* unreadable — assume non-empty, allow */ }
|
|
134
|
+
}
|
|
77
135
|
return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: { filename: t.path, last_modified: stats.mtime.toISOString(), source: 'file_metadata' }, collect_style: 'metadata' };
|
|
78
136
|
}
|
|
79
137
|
catch (err) {
|
|
@@ -99,7 +157,8 @@ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, optio
|
|
|
99
157
|
const vscdbPath = getVscdbPath(home, pathConstants);
|
|
100
158
|
const extensionsCachePath = getExtensionsCachePath(home, pathConstants);
|
|
101
159
|
const extensionsInstalledSuffix = getExtensionsCacheInstalledSuffix(pathConstants);
|
|
102
|
-
const
|
|
160
|
+
const effectivePatterns = withOpencodeEnvPatterns(patterns);
|
|
161
|
+
const { targets, enrichByFileType, metadataOnlyFileTypes } = buildCollectionContext(effectivePatterns, projectRoot, home, options?.home_recurse_skip_dirs ?? [], options?.absolute_path_prefixes ?? [], options?.mcp_tool_glob_spec ?? null, pathConstants, options?.path_resolution_specs ?? null);
|
|
103
162
|
const configFiles = [];
|
|
104
163
|
const handledSpecialPaths = new Set();
|
|
105
164
|
const loggedFileTypes = new Set();
|
|
@@ -156,7 +215,7 @@ export { collectConfigFilesFromPatterns };
|
|
|
156
215
|
export { collectMcpToolFiles } from './mcp_tool_collector.js';
|
|
157
216
|
export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles } from './plugin_collector.js';
|
|
158
217
|
export { collectMcpFromClaudeJsonProjects } from './claude_known_projects_collector.js';
|
|
159
|
-
export { collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, } from './claude_desktop_extensions_collector.js';
|
|
218
|
+
export { collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, } from './claude_desktop_extensions_collector.js';
|
|
160
219
|
export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
|
|
161
220
|
export { determineFileTypeFromPath } from './file_type_rules.js';
|
|
162
221
|
export { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII boundary for Cowork session metadata (`local_<sessionId>.json`).
|
|
3
|
+
*
|
|
4
|
+
* Each Cowork session file carries ~47 KB of system prompt plus the user's
|
|
5
|
+
* conversation seed, selected folders, etc. We only need a small activity
|
|
6
|
+
* signal (which MCP servers were connected, recency, archived state). This
|
|
7
|
+
* reduces a parsed session object to a whitelist of top-level keys BEFORE it
|
|
8
|
+
* leaves the device — minimum-egress, matching DeepSecurityAndPrivacyEnhancer.
|
|
9
|
+
*
|
|
10
|
+
* - Only the keys in ALLOWED_KEYS survive; everything else is dropped.
|
|
11
|
+
* - `accountName` / `emailAddress` are sha256-hashed (cross-session joins
|
|
12
|
+
* without storing raw identity).
|
|
13
|
+
* - `remoteMcpServersConfig[]` keeps `{name, uuid, tools}` only; `url` is
|
|
14
|
+
* dropped (remote-MCP endpoint URLs can embed workspace ids / tokens).
|
|
15
|
+
*
|
|
16
|
+
* The sibling `local_<sessionId>/` directory (audit.jsonl, outputs/, uploads/)
|
|
17
|
+
* is never collected — see the collection glob in agents/cowork.py. Server-side
|
|
18
|
+
* ingest re-validates the shape and rejects any denied key as defense in depth.
|
|
19
|
+
*
|
|
20
|
+
* See design_notes/Phase0_5CoworkActivityEvidence.md.
|
|
21
|
+
*/
|
|
22
|
+
import crypto from 'node:crypto';
|
|
23
|
+
export const COWORK_SESSION_ALLOWED_KEYS = [
|
|
24
|
+
'sessionId',
|
|
25
|
+
'accountName', // hashed
|
|
26
|
+
'emailAddress', // hashed
|
|
27
|
+
'model',
|
|
28
|
+
'createdAt',
|
|
29
|
+
'lastActivityAt',
|
|
30
|
+
'isArchived',
|
|
31
|
+
'pluginsEnabled',
|
|
32
|
+
'skillsEnabled',
|
|
33
|
+
'memoryEnabled',
|
|
34
|
+
'hostLoopMode',
|
|
35
|
+
'cwd',
|
|
36
|
+
'enabledMcpTools', // dict "<serverUUID>:<tool>" -> bool
|
|
37
|
+
'remoteMcpServersConfig', // list of {name, uuid, tools} (url dropped)
|
|
38
|
+
];
|
|
39
|
+
const HASHED_KEYS = new Set(['accountName', 'emailAddress']);
|
|
40
|
+
const REMOTE_SERVER_KEEP = ['name', 'uuid', 'tools'];
|
|
41
|
+
function sha256Hex(value) {
|
|
42
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
43
|
+
}
|
|
44
|
+
function sanitizeRemoteServers(value) {
|
|
45
|
+
if (!Array.isArray(value))
|
|
46
|
+
return [];
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const entry of value) {
|
|
49
|
+
// Drop any non-object entry: a malformed/tampered remoteMcpServersConfig
|
|
50
|
+
// could carry a raw string (e.g. "https://…?token=…") that would otherwise
|
|
51
|
+
// survive the whitelist. Only keep whitelisted keys of well-formed objects.
|
|
52
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
53
|
+
continue;
|
|
54
|
+
const e = entry;
|
|
55
|
+
const kept = {};
|
|
56
|
+
for (const k of REMOTE_SERVER_KEEP) {
|
|
57
|
+
if (k in e)
|
|
58
|
+
kept[k] = e[k];
|
|
59
|
+
}
|
|
60
|
+
out.push(kept);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reduce a parsed Cowork session object to the whitelisted, hashed shape.
|
|
66
|
+
* Returns `null` when nothing survives (e.g. a non-object or a file that
|
|
67
|
+
* failed to parse to the expected shape) so the caller can drop the entry
|
|
68
|
+
* rather than emit an empty row — and, critically, never emit raw content.
|
|
69
|
+
*/
|
|
70
|
+
export function applyCoworkSessionWhitelist(raw) {
|
|
71
|
+
if (raw === null || raw === undefined || typeof raw !== 'object')
|
|
72
|
+
return null;
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const key of COWORK_SESSION_ALLOWED_KEYS) {
|
|
75
|
+
if (!(key in raw))
|
|
76
|
+
continue;
|
|
77
|
+
if (HASHED_KEYS.has(key)) {
|
|
78
|
+
const v = raw[key];
|
|
79
|
+
out[key] = v === null || v === undefined || v === '' ? '' : sha256Hex(v);
|
|
80
|
+
}
|
|
81
|
+
else if (key === 'remoteMcpServersConfig') {
|
|
82
|
+
out[key] = sanitizeRemoteServers(raw[key]);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
out[key] = raw[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
89
|
+
}
|
|
@@ -105,10 +105,45 @@ function isEphemeralCursorProjectDir(name) {
|
|
|
105
105
|
function isInternalCursorWorkspaceSlug(slug) {
|
|
106
106
|
return normalizeSlugPart(slug) === 'empty-window';
|
|
107
107
|
}
|
|
108
|
+
function readMcpToolCache(projectPath) {
|
|
109
|
+
const cachePath = join(projectPath, 'mcp-cache.json');
|
|
110
|
+
if (!existsSync(cachePath))
|
|
111
|
+
return null;
|
|
112
|
+
const cache = readJSONFile(cachePath);
|
|
113
|
+
if (cache && typeof cache === 'object' && !Array.isArray(cache)) {
|
|
114
|
+
return cache;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/** Tool schemas from mcp-cache.json for a connected mcps/ server (never creates inventory rows). */
|
|
119
|
+
function cursorMcpCacheToolsForServer(cache, label, serverDirName) {
|
|
120
|
+
if (!cache)
|
|
121
|
+
return undefined;
|
|
122
|
+
const aliases = new Set();
|
|
123
|
+
const addAlias = (s) => {
|
|
124
|
+
const t = s.trim();
|
|
125
|
+
if (t)
|
|
126
|
+
aliases.add(t.toLowerCase());
|
|
127
|
+
};
|
|
128
|
+
addAlias(label);
|
|
129
|
+
addAlias(serverDirName);
|
|
130
|
+
if (serverDirName.startsWith('user-'))
|
|
131
|
+
addAlias(serverDirName.slice('user-'.length));
|
|
132
|
+
else
|
|
133
|
+
addAlias(`user-${label}`);
|
|
134
|
+
for (const [key, val] of Object.entries(cache)) {
|
|
135
|
+
if (!key.trim() || !aliases.has(key.trim().toLowerCase()))
|
|
136
|
+
continue;
|
|
137
|
+
if (val && typeof val === 'object' && Array.isArray(val.tools)) {
|
|
138
|
+
return val.tools;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
108
143
|
/**
|
|
109
144
|
* Cursor stores per-workspace MCP state under ~/.cursor/projects/<slug>/ (mcp-cache.json, mcps/*).
|
|
110
145
|
* This is not the repo file at <repo>/.cursor/mcp.json. Upload one mcp_config per slug with
|
|
111
|
-
*
|
|
146
|
+
* mcpServers from mcps/ only; mcp-cache.json supplies supplementary tool schemas when names match.
|
|
112
147
|
*/
|
|
113
148
|
function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constants, workspaceVscdbSpec, home = homedir()) {
|
|
114
149
|
const skipPrefixes = normalizePathSkipPrefixes(pathSkipPrefixes);
|
|
@@ -136,18 +171,7 @@ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constant
|
|
|
136
171
|
continue;
|
|
137
172
|
const mcpServers = {};
|
|
138
173
|
const cachePath = join(projectPath, 'mcp-cache.json');
|
|
139
|
-
|
|
140
|
-
const cache = readJSONFile(cachePath);
|
|
141
|
-
if (cache && typeof cache === 'object' && !Array.isArray(cache)) {
|
|
142
|
-
for (const [name, val] of Object.entries(cache)) {
|
|
143
|
-
if (!name.trim())
|
|
144
|
-
continue;
|
|
145
|
-
if (val && typeof val === 'object' && Array.isArray(val.tools)) {
|
|
146
|
-
mcpServers[name] = { cursor_source: 'mcp-cache.json' };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
174
|
+
const mcpToolCache = readMcpToolCache(projectPath);
|
|
151
175
|
const mcpsPath = join(projectPath, 'mcps');
|
|
152
176
|
if (existsSync(mcpsPath)) {
|
|
153
177
|
try {
|
|
@@ -158,12 +182,17 @@ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constant
|
|
|
158
182
|
const label = mcpServerLabelFromMcpsDir(serverDir.name, metaObj);
|
|
159
183
|
if (!label)
|
|
160
184
|
continue;
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
if (label in mcpServers)
|
|
186
|
+
continue;
|
|
187
|
+
const entry = {
|
|
188
|
+
cursor_source: 'mcps',
|
|
189
|
+
server_identifier: serverDir.name,
|
|
190
|
+
};
|
|
191
|
+
const cacheTools = cursorMcpCacheToolsForServer(mcpToolCache, label, serverDir.name);
|
|
192
|
+
if (cacheTools !== undefined) {
|
|
193
|
+
entry.cursor_mcp_cache_tools = cacheTools;
|
|
166
194
|
}
|
|
195
|
+
mcpServers[label] = entry;
|
|
167
196
|
}
|
|
168
197
|
}
|
|
169
198
|
catch {
|
|
@@ -25,7 +25,9 @@ function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentForm
|
|
|
25
25
|
return [];
|
|
26
26
|
}
|
|
27
27
|
const targets = [];
|
|
28
|
-
const parts = after.split('/');
|
|
28
|
+
const parts = after.split('/').filter((s) => s.length > 0);
|
|
29
|
+
if (parts.length === 0)
|
|
30
|
+
return [];
|
|
29
31
|
const dirName = parts[0];
|
|
30
32
|
const fileName = parts.length > 1 ? parts[parts.length - 1] : null;
|
|
31
33
|
const skipSet = homeRecurseSkipDirs.length ? new Set(homeRecurseSkipDirs) : new Set();
|
|
@@ -41,8 +43,21 @@ function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentForm
|
|
|
41
43
|
continue;
|
|
42
44
|
if (entry.name === dirName) {
|
|
43
45
|
const filePath = parts.length === 1 ? full : join(full, ...parts.slice(1));
|
|
44
|
-
if (
|
|
46
|
+
if (parts.length === 1) {
|
|
47
|
+
// e.g. ~/.cursor/plugins/cache/**/skills/ — collect each matched skills/ tree as a directory target
|
|
48
|
+
if (existsSync(filePath)) {
|
|
49
|
+
targets.push({
|
|
50
|
+
path: filePath,
|
|
51
|
+
file_type: fileType,
|
|
52
|
+
isDirectory: true,
|
|
53
|
+
content_format: contentFormat,
|
|
54
|
+
dir_glob: dirGlob,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (fileName && existsSync(filePath)) {
|
|
45
59
|
targets.push({ path: filePath, file_type: fileType, content_format: contentFormat });
|
|
60
|
+
}
|
|
46
61
|
}
|
|
47
62
|
walk(full, depth + 1, false);
|
|
48
63
|
}
|
|
@@ -1,10 +1,120 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
function stripJsoncComments(input) {
|
|
3
|
+
let out = '';
|
|
4
|
+
let inString = false;
|
|
5
|
+
let quote = '';
|
|
6
|
+
let escaped = false;
|
|
7
|
+
let inLineComment = false;
|
|
8
|
+
let inBlockComment = false;
|
|
9
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
10
|
+
const ch = input[i];
|
|
11
|
+
const next = i + 1 < input.length ? input[i + 1] : '';
|
|
12
|
+
if (inLineComment) {
|
|
13
|
+
if (ch === '\n') {
|
|
14
|
+
inLineComment = false;
|
|
15
|
+
out += ch;
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (inBlockComment) {
|
|
20
|
+
if (ch === '*' && next === '/') {
|
|
21
|
+
inBlockComment = false;
|
|
22
|
+
i += 1;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (inString) {
|
|
27
|
+
out += ch;
|
|
28
|
+
if (escaped) {
|
|
29
|
+
escaped = false;
|
|
30
|
+
}
|
|
31
|
+
else if (ch === '\\') {
|
|
32
|
+
escaped = true;
|
|
33
|
+
}
|
|
34
|
+
else if (ch === quote) {
|
|
35
|
+
inString = false;
|
|
36
|
+
quote = '';
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ch === '"' || ch === "'") {
|
|
41
|
+
inString = true;
|
|
42
|
+
quote = ch;
|
|
43
|
+
out += ch;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '/' && next === '/') {
|
|
47
|
+
inLineComment = true;
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === '/' && next === '*') {
|
|
52
|
+
inBlockComment = true;
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out += ch;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function stripTrailingCommas(input) {
|
|
61
|
+
let out = '';
|
|
62
|
+
let inString = false;
|
|
63
|
+
let quote = '';
|
|
64
|
+
let escaped = false;
|
|
65
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
66
|
+
const ch = input[i];
|
|
67
|
+
if (inString) {
|
|
68
|
+
out += ch;
|
|
69
|
+
if (escaped) {
|
|
70
|
+
escaped = false;
|
|
71
|
+
}
|
|
72
|
+
else if (ch === '\\') {
|
|
73
|
+
escaped = true;
|
|
74
|
+
}
|
|
75
|
+
else if (ch === quote) {
|
|
76
|
+
inString = false;
|
|
77
|
+
quote = '';
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '"' || ch === "'") {
|
|
82
|
+
inString = true;
|
|
83
|
+
quote = ch;
|
|
84
|
+
out += ch;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === ',') {
|
|
88
|
+
let j = i + 1;
|
|
89
|
+
while (j < input.length && /\s/.test(input[j]))
|
|
90
|
+
j += 1;
|
|
91
|
+
if (j < input.length && (input[j] === '}' || input[j] === ']'))
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
out += ch;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function parseJsonWithJsoncFallback(raw) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
try {
|
|
104
|
+
const sanitized = stripTrailingCommas(stripJsoncComments(raw));
|
|
105
|
+
return JSON.parse(sanitized);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
2
112
|
/** Read and parse an MCP config file. Returns null on any error. */
|
|
3
113
|
function readMCPConfig(filePath) {
|
|
4
114
|
try {
|
|
5
115
|
if (!existsSync(filePath))
|
|
6
116
|
return null;
|
|
7
|
-
return
|
|
117
|
+
return parseJsonWithJsoncFallback(readFileSync(filePath, 'utf-8'));
|
|
8
118
|
}
|
|
9
119
|
catch (error) {
|
|
10
120
|
console.error(`Error reading ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
@@ -16,7 +126,7 @@ function readJSONFile(filePath) {
|
|
|
16
126
|
try {
|
|
17
127
|
if (!existsSync(filePath))
|
|
18
128
|
return null;
|
|
19
|
-
return
|
|
129
|
+
return parseJsonWithJsoncFallback(readFileSync(filePath, 'utf-8'));
|
|
20
130
|
}
|
|
21
131
|
catch (error) {
|
|
22
132
|
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
@@ -13,7 +13,7 @@ import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
|
13
13
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
14
14
|
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
15
15
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
16
|
-
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
16
|
+
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
17
17
|
import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
|
|
18
18
|
import { collectCursorProjectWorkspaceMcpConfigs } from '../collection/cursor_project_mcp_collector.js';
|
|
19
19
|
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
@@ -98,6 +98,13 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
98
98
|
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
99
99
|
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
100
100
|
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|
|
101
|
+
for (const entry of collectClaudeDesktopExtensionSettingsFiles(HOME_DIR)) {
|
|
102
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
103
|
+
if (!existingPaths.has(key)) {
|
|
104
|
+
existingPaths.add(key);
|
|
105
|
+
configFiles.push(entry);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
101
108
|
if (!diskExtMerge.hadRegistryUpload && diskExtMerge.mergedCount > 0) {
|
|
102
109
|
hookRunLog(`scanning Claude Desktop extension manifests on disk (no registry upload in batch)`);
|
|
103
110
|
for (const entry of collectClaudeDesktopExtensionManifests(HOME_DIR)) {
|