log-llm-config-staging 1.3.94 → 1.3.96

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.
@@ -0,0 +1,145 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readJSONFile } from '../readers/file_readers.js';
5
+ function claudeAppSupportDir(home) {
6
+ if (process.platform === 'win32') {
7
+ const appData = process.env.APPDATA?.trim();
8
+ if (!appData)
9
+ return '';
10
+ return join(appData, 'Claude');
11
+ }
12
+ return join(home, 'Library', 'Application Support', 'Claude');
13
+ }
14
+ function claudeDesktopExtensionsDir(home) {
15
+ const base = claudeAppSupportDir(home);
16
+ return base ? join(base, 'Claude Extensions') : '';
17
+ }
18
+ function extensionsInstallationsPath(home) {
19
+ const base = claudeAppSupportDir(home);
20
+ return base ? join(base, 'extensions-installations.json') : '';
21
+ }
22
+ function normalizePathForMatch(path) {
23
+ return path.replace(/\\/g, '/');
24
+ }
25
+ function isExtensionsInstallationsUpload(entry) {
26
+ if (entry.file_type !== 'claude_extensions_installations')
27
+ return false;
28
+ return normalizePathForMatch(entry.file_path).includes('extensions-installations.json');
29
+ }
30
+ function registryExtensionIds(home) {
31
+ const path = extensionsInstallationsPath(home);
32
+ if (!path || !existsSync(path))
33
+ return new Set();
34
+ const raw = readJSONFile(path);
35
+ if (!raw || typeof raw !== 'object')
36
+ return new Set();
37
+ const extensions = raw.extensions;
38
+ if (!extensions || typeof extensions !== 'object')
39
+ return new Set();
40
+ return new Set(Object.keys(extensions));
41
+ }
42
+ function manifestHasMcpConfig(manifest) {
43
+ const server = manifest.server;
44
+ if (!server || typeof server !== 'object')
45
+ return false;
46
+ const mcpConfig = server.mcp_config;
47
+ return mcpConfig !== null && typeof mcpConfig === 'object';
48
+ }
49
+ function collectDiskOnlyClaudeDesktopExtensions(home, skipIds) {
50
+ const extensionsDir = claudeDesktopExtensionsDir(home);
51
+ if (!extensionsDir || !existsSync(extensionsDir))
52
+ return {};
53
+ const out = {};
54
+ let dirs;
55
+ try {
56
+ dirs = readdirSync(extensionsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
57
+ }
58
+ catch {
59
+ return out;
60
+ }
61
+ for (const dirent of dirs) {
62
+ const extId = dirent.name;
63
+ if (skipIds.has(extId))
64
+ continue;
65
+ const manifestPath = join(extensionsDir, extId, 'manifest.json');
66
+ if (!existsSync(manifestPath))
67
+ continue;
68
+ const manifest = readJSONFile(manifestPath);
69
+ if (!manifest || typeof manifest !== 'object')
70
+ continue;
71
+ const manifestObj = manifest;
72
+ if (!manifestHasMcpConfig(manifestObj))
73
+ continue;
74
+ const version = typeof manifestObj.version === 'string' || typeof manifestObj.version === 'number'
75
+ ? String(manifestObj.version)
76
+ : undefined;
77
+ out[extId] = {
78
+ id: extId,
79
+ version,
80
+ manifest: manifestObj,
81
+ source: 'claude_extensions_directory',
82
+ };
83
+ }
84
+ return out;
85
+ }
86
+ /**
87
+ * Merge disk-only Desktop extensions into the extensions-installations.json upload
88
+ * so MCP inventory uses the same ingest path as registry-listed extensions (e.g. iMessage).
89
+ */
90
+ function enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, home = homedir()) {
91
+ const skipIds = new Set();
92
+ for (const id of registryExtensionIds(home))
93
+ skipIds.add(id);
94
+ const registryUpload = configFiles.find(isExtensionsInstallationsUpload);
95
+ if (registryUpload?.raw_content && typeof registryUpload.raw_content === 'object') {
96
+ const extensions = registryUpload.raw_content.extensions;
97
+ if (extensions && typeof extensions === 'object') {
98
+ for (const id of Object.keys(extensions))
99
+ skipIds.add(id);
100
+ }
101
+ }
102
+ const diskOnly = collectDiskOnlyClaudeDesktopExtensions(home, skipIds);
103
+ const diskOnlyIds = Object.keys(diskOnly);
104
+ if (diskOnlyIds.length === 0) {
105
+ return { mergedCount: 0, hadRegistryUpload: Boolean(registryUpload) };
106
+ }
107
+ if (registryUpload) {
108
+ const raw = registryUpload.raw_content;
109
+ if (!raw.extensions || typeof raw.extensions !== 'object')
110
+ raw.extensions = {};
111
+ Object.assign(raw.extensions, diskOnly);
112
+ return { mergedCount: diskOnlyIds.length, hadRegistryUpload: true };
113
+ }
114
+ const registryPath = extensionsInstallationsPath(home);
115
+ if (!registryPath)
116
+ return { mergedCount: 0, hadRegistryUpload: false };
117
+ configFiles.push({
118
+ file_type: 'claude_extensions_installations',
119
+ file_path: registryPath,
120
+ raw_content: { extensions: diskOnly },
121
+ });
122
+ return { mergedCount: diskOnlyIds.length, hadRegistryUpload: false };
123
+ }
124
+ /**
125
+ * Fallback when extensions-installations.json is not part of the upload batch: one synthetic
126
+ * claude_extensions_installations payload per disk-only manifest path.
127
+ */
128
+ function collectClaudeDesktopExtensionManifests(home = homedir()) {
129
+ const extensionsDir = claudeDesktopExtensionsDir(home);
130
+ if (!extensionsDir || !existsSync(extensionsDir))
131
+ return [];
132
+ const inRegistry = registryExtensionIds(home);
133
+ const results = [];
134
+ const diskOnly = collectDiskOnlyClaudeDesktopExtensions(home, inRegistry);
135
+ for (const [extId, entry] of Object.entries(diskOnly)) {
136
+ const manifestPath = join(extensionsDir, extId, 'manifest.json');
137
+ results.push({
138
+ file_type: 'claude_extensions_installations',
139
+ file_path: manifestPath,
140
+ raw_content: { extensions: { [extId]: entry } },
141
+ });
142
+ }
143
+ return results;
144
+ }
145
+ export { claudeAppSupportDir, claudeDesktopExtensionsDir, collectClaudeDesktopExtensionManifests, collectDiskOnlyClaudeDesktopExtensions, enrichClaudeDesktopExtensionsInstallationsUpload, };
@@ -0,0 +1,42 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readJSONFile } from '../readers/file_readers.js';
5
+ /**
6
+ * Collect project-level `.mcp.json` for each path listed under `projects` in ~/.claude.json.
7
+ * Claude Code records known project directories there; MCP config lives at `<project>/.mcp.json`.
8
+ */
9
+ function collectMcpFromClaudeJsonProjects(home = homedir()) {
10
+ const claudeJsonPath = join(home, '.claude.json');
11
+ if (!existsSync(claudeJsonPath))
12
+ return [];
13
+ const raw = readJSONFile(claudeJsonPath);
14
+ if (!raw || typeof raw !== 'object')
15
+ return [];
16
+ const projects = raw.projects;
17
+ if (!projects || typeof projects !== 'object')
18
+ return [];
19
+ const seenPaths = new Set();
20
+ const results = [];
21
+ for (const projectPath of Object.keys(projects)) {
22
+ if (!projectPath || typeof projectPath !== 'string')
23
+ continue;
24
+ const trimmed = projectPath.trim();
25
+ if (!trimmed)
26
+ continue;
27
+ const mcpPath = join(trimmed, '.mcp.json');
28
+ if (seenPaths.has(mcpPath) || !existsSync(mcpPath))
29
+ continue;
30
+ const content = readJSONFile(mcpPath);
31
+ if (content === null)
32
+ continue;
33
+ seenPaths.add(mcpPath);
34
+ results.push({
35
+ file_type: 'claude_plugin_mcp',
36
+ file_path: mcpPath,
37
+ raw_content: typeof content === 'object' ? content : { content, source: 'file' },
38
+ });
39
+ }
40
+ return results;
41
+ }
42
+ export { collectMcpFromClaudeJsonProjects };
@@ -153,7 +153,10 @@ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, optio
153
153
  // ─── Re-exports (barrel for public API) ──────────────────────────────────────
154
154
  export { collectConfigFilesFromPatterns };
155
155
  export { collectMcpToolFiles } from './mcp_tool_collector.js';
156
- export { collectConfigFilesFromInstalledPlugins } from './plugin_collector.js';
156
+ export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles } from './plugin_collector.js';
157
+ export { collectMcpFromClaudeJsonProjects } from './claude_known_projects_collector.js';
158
+ export { collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, } from './claude_desktop_extensions_collector.js';
159
+ export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
157
160
  export { determineFileTypeFromPath } from './file_type_rules.js';
158
161
  export { enrichRawFromRecipe } from './enrichment_helpers.js';
159
162
  export { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
@@ -0,0 +1,198 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readJSONFile } from '../readers/file_readers.js';
5
+ import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
6
+ import { getCursorProjectsPath } from '../paths/path_constants_helpers.js';
7
+ import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
8
+ import { isStaleCursorProjectWorkspace } from './cursor_project_workspace_activity.js';
9
+ /** Synthetic manifest path segment — not repo `.cursor/mcp.json`. */
10
+ export const CURSOR_WORKSPACE_MCP_MANIFEST = 'cursor-workspace-mcp-servers.json';
11
+ function fileUriToPath(uri) {
12
+ const trimmed = uri.trim();
13
+ if (!trimmed.startsWith('file://'))
14
+ return null;
15
+ let p = trimmed.slice('file://'.length);
16
+ if (!p.startsWith('/'))
17
+ p = `/${p}`;
18
+ try {
19
+ return decodeURIComponent(p);
20
+ }
21
+ catch {
22
+ return p;
23
+ }
24
+ }
25
+ function normalizeSlugPart(s) {
26
+ return s.replace(/\\/g, '/').replace(/_/g, '-').toLowerCase();
27
+ }
28
+ /** Best-effort: map a repo absolute path to Cursor's ~/.cursor/projects/<slug> dir name. */
29
+ function cursorProjectSlugFromRepoPath(repoPath, home) {
30
+ const normHome = home.replace(/\\/g, '/').replace(/\/+$/, '');
31
+ const normPath = repoPath.replace(/\\/g, '/').replace(/\/+$/, '');
32
+ if (!normPath.startsWith(`${normHome}/`))
33
+ return '';
34
+ const homeSegs = normHome.split('/').filter(Boolean);
35
+ const tail = normPath.slice(normHome.length + 1);
36
+ const prefix = homeSegs.join('-');
37
+ const tailSlug = tail.split('/').join('-').replace(/_/g, '-');
38
+ return tailSlug ? `${prefix}-${tailSlug}` : prefix;
39
+ }
40
+ function loadWorkspaceMetadataEntries(spec, home) {
41
+ if (!spec?.global_vscdb_path_segments?.length || !spec.global_workspace_list_key)
42
+ return [];
43
+ const globalDbPath = join(home, ...spec.global_vscdb_path_segments);
44
+ if (!existsSync(globalDbPath))
45
+ return [];
46
+ const globalResult = readVscdbItemTableJson(globalDbPath, spec.global_workspace_list_key);
47
+ if (!globalResult)
48
+ return [];
49
+ const rawValue = globalResult[spec.global_workspace_list_key];
50
+ const entries = Array.isArray(rawValue)
51
+ ? rawValue
52
+ : Array.isArray(rawValue?.entries)
53
+ ? rawValue.entries
54
+ : [];
55
+ const out = [];
56
+ for (const entry of entries) {
57
+ if (!entry || typeof entry !== 'object')
58
+ continue;
59
+ const e = entry;
60
+ const row = {};
61
+ if (typeof e.displayPath === 'string')
62
+ row.displayPath = e.displayPath;
63
+ if (typeof e.folderUri === 'string')
64
+ row.folderUri = e.folderUri;
65
+ if (typeof e.workspaceId === 'string')
66
+ row.workspaceId = e.workspaceId;
67
+ out.push(row);
68
+ }
69
+ return out;
70
+ }
71
+ function workspaceMetaForSlug(slug, metadataEntries, home) {
72
+ const slugNorm = normalizeSlugPart(slug);
73
+ for (const entry of metadataEntries) {
74
+ if (entry.folderUri) {
75
+ const repoPath = fileUriToPath(entry.folderUri);
76
+ if (repoPath && normalizeSlugPart(cursorProjectSlugFromRepoPath(repoPath, home)) === slugNorm) {
77
+ return entry;
78
+ }
79
+ }
80
+ if (entry.displayPath) {
81
+ const expanded = entry.displayPath.startsWith('~/')
82
+ ? join(home, entry.displayPath.slice(2))
83
+ : entry.displayPath;
84
+ if (expanded && normalizeSlugPart(cursorProjectSlugFromRepoPath(expanded, home)) === slugNorm) {
85
+ return entry;
86
+ }
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ function mcpServerLabelFromMcpsDir(serverDirName, meta) {
92
+ const fromMeta = (typeof meta?.serverName === 'string' && meta.serverName) ||
93
+ (typeof meta?.serverIdentifier === 'string' && meta.serverIdentifier) ||
94
+ serverDirName;
95
+ if (fromMeta.startsWith('user-'))
96
+ return fromMeta.slice('user-'.length);
97
+ return fromMeta;
98
+ }
99
+ function isEphemeralCursorProjectDir(name) {
100
+ if (/^\d+$/.test(name))
101
+ return true;
102
+ return false;
103
+ }
104
+ /** Cursor uses this slug when no repo folder is open; mcps/ there are app-built-in only. */
105
+ function isInternalCursorWorkspaceSlug(slug) {
106
+ return normalizeSlugPart(slug) === 'empty-window';
107
+ }
108
+ /**
109
+ * Cursor stores per-workspace MCP state under ~/.cursor/projects/<slug>/ (mcp-cache.json, mcps/*).
110
+ * This is not the repo file at <repo>/.cursor/mcp.json. Upload one mcp_config per slug with
111
+ * normalized mcpServers for inventory plus cursor_workspace metadata for scope display.
112
+ */
113
+ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constants, workspaceVscdbSpec, home = homedir()) {
114
+ const skipPrefixes = normalizePathSkipPrefixes(pathSkipPrefixes);
115
+ const results = [];
116
+ const cursorProjectsPath = getCursorProjectsPath(home, constants);
117
+ const metadataEntries = loadWorkspaceMetadataEntries(workspaceVscdbSpec, home);
118
+ if (!existsSync(cursorProjectsPath))
119
+ return results;
120
+ let projectDirs;
121
+ try {
122
+ projectDirs = readdirSync(cursorProjectsPath, { withFileTypes: true }).filter((d) => d.isDirectory());
123
+ }
124
+ catch {
125
+ return results;
126
+ }
127
+ for (const projectDir of projectDirs) {
128
+ if (skipPrefixes.some((p) => projectDir.name.startsWith(p)))
129
+ continue;
130
+ if (isEphemeralCursorProjectDir(projectDir.name))
131
+ continue;
132
+ if (isInternalCursorWorkspaceSlug(projectDir.name))
133
+ continue;
134
+ const projectPath = join(cursorProjectsPath, projectDir.name);
135
+ if (isStaleCursorProjectWorkspace(projectPath))
136
+ continue;
137
+ const mcpServers = {};
138
+ const cachePath = join(projectPath, 'mcp-cache.json');
139
+ if (existsSync(cachePath)) {
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
+ }
151
+ const mcpsPath = join(projectPath, 'mcps');
152
+ if (existsSync(mcpsPath)) {
153
+ try {
154
+ for (const serverDir of readdirSync(mcpsPath, { withFileTypes: true }).filter((d) => d.isDirectory())) {
155
+ const metaPath = join(mcpsPath, serverDir.name, 'SERVER_METADATA.json');
156
+ const meta = existsSync(metaPath) ? readJSONFile(metaPath) : null;
157
+ const metaObj = meta && typeof meta === 'object' ? meta : null;
158
+ const label = mcpServerLabelFromMcpsDir(serverDir.name, metaObj);
159
+ if (!label)
160
+ continue;
161
+ if (!(label in mcpServers)) {
162
+ mcpServers[label] = {
163
+ cursor_source: 'mcps',
164
+ server_identifier: serverDir.name,
165
+ };
166
+ }
167
+ }
168
+ }
169
+ catch {
170
+ /* ignore */
171
+ }
172
+ }
173
+ if (Object.keys(mcpServers).length === 0)
174
+ continue;
175
+ const linked = workspaceMetaForSlug(projectDir.name, metadataEntries, home);
176
+ const linkedRepoPath = linked?.folderUri ? fileUriToPath(linked.folderUri) : null;
177
+ const manifestPath = existsSync(cachePath)
178
+ ? cachePath
179
+ : join(projectPath, CURSOR_WORKSPACE_MCP_MANIFEST);
180
+ results.push({
181
+ file_type: 'mcp_config',
182
+ file_path: manifestPath,
183
+ raw_content: {
184
+ mcpServers,
185
+ cursor_workspace: {
186
+ slug: projectDir.name,
187
+ collection_root: projectPath,
188
+ display_path: linked?.displayPath,
189
+ folder_uri: linked?.folderUri,
190
+ linked_repo_path: linkedRepoPath ?? undefined,
191
+ note: 'Cursor ~/.cursor/projects workspace MCP; not repo .cursor/mcp.json',
192
+ },
193
+ },
194
+ });
195
+ }
196
+ return results;
197
+ }
198
+ export { collectCursorProjectWorkspaceMcpConfigs, cursorProjectSlugFromRepoPath, fileUriToPath, isInternalCursorWorkspaceSlug, workspaceMetaForSlug, };
@@ -0,0 +1,73 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Align with backend `AGENT_ACTIVITY_RECENCY_DAYS` — skip workspace MCP when idle longer. */
4
+ export const CURSOR_WORKSPACE_MCP_RECENCY_DAYS = 7;
5
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
6
+ function maxMtimeInTree(root, maxDepth) {
7
+ if (!existsSync(root))
8
+ return null;
9
+ let best = null;
10
+ const visit = (dir, depth) => {
11
+ if (depth > maxDepth)
12
+ return;
13
+ let entries;
14
+ try {
15
+ entries = readdirSync(dir, { withFileTypes: true });
16
+ }
17
+ catch {
18
+ return;
19
+ }
20
+ for (const entry of entries) {
21
+ const fullPath = join(dir, entry.name);
22
+ try {
23
+ const st = statSync(fullPath);
24
+ const ms = st.mtimeMs;
25
+ if (best === null || ms > best)
26
+ best = ms;
27
+ if (entry.isDirectory())
28
+ visit(fullPath, depth + 1);
29
+ }
30
+ catch {
31
+ /* ignore */
32
+ }
33
+ }
34
+ };
35
+ visit(root, 0);
36
+ return best;
37
+ }
38
+ function pushMtime(candidates, path) {
39
+ if (!existsSync(path))
40
+ return;
41
+ try {
42
+ candidates.push(statSync(path).mtimeMs);
43
+ }
44
+ catch {
45
+ /* ignore */
46
+ }
47
+ }
48
+ /**
49
+ * Latest filesystem activity under a Cursor ~/.cursor/projects/<slug> directory.
50
+ * Uses mcp-cache, mcps/, agent-tools/, terminals/, and the project root — not hook ingest time.
51
+ */
52
+ function cursorProjectWorkspaceLatestActivityMs(projectPath) {
53
+ const candidates = [];
54
+ pushMtime(candidates, projectPath);
55
+ pushMtime(candidates, join(projectPath, 'mcp-cache.json'));
56
+ pushMtime(candidates, join(projectPath, 'terminals'));
57
+ for (const subdir of ['mcps', 'agent-tools']) {
58
+ const nested = maxMtimeInTree(join(projectPath, subdir), 5);
59
+ if (nested !== null)
60
+ candidates.push(nested);
61
+ }
62
+ if (candidates.length === 0)
63
+ return null;
64
+ return Math.max(...candidates);
65
+ }
66
+ function isStaleCursorProjectWorkspace(projectPath, recencyDays = CURSOR_WORKSPACE_MCP_RECENCY_DAYS, nowMs = Date.now()) {
67
+ const latest = cursorProjectWorkspaceLatestActivityMs(projectPath);
68
+ if (latest === null)
69
+ return true;
70
+ const cutoff = nowMs - recencyDays * MS_PER_DAY;
71
+ return latest < cutoff;
72
+ }
73
+ export { cursorProjectWorkspaceLatestActivityMs, isStaleCursorProjectWorkspace, maxMtimeInTree, };
@@ -4,6 +4,8 @@ import { homedir } from 'node:os';
4
4
  import { readJSONFile } from '../readers/file_readers.js';
5
5
  import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
6
6
  import { getCursorProjectsPath } from '../paths/path_constants_helpers.js';
7
+ import { isInternalCursorWorkspaceSlug } from './cursor_project_mcp_collector.js';
8
+ import { isStaleCursorProjectWorkspace } from './cursor_project_workspace_activity.js';
7
9
  import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
8
10
  /**
9
11
  * Read workspaceMetadata.entries from the Cursor global state.vscdb and emit one
@@ -76,7 +78,12 @@ function collectMcpToolFiles(pathSkipPrefixes = [], constants) {
76
78
  for (const projectDir of readdirSync(cursorProjectsPath, { withFileTypes: true }).filter((d) => d.isDirectory())) {
77
79
  if (skipPrefixes.some((p) => projectDir.name.startsWith(p)))
78
80
  continue;
79
- const mcpsPath = join(cursorProjectsPath, projectDir.name, 'mcps');
81
+ if (isInternalCursorWorkspaceSlug(projectDir.name))
82
+ continue;
83
+ const projectPath = join(cursorProjectsPath, projectDir.name);
84
+ if (isStaleCursorProjectWorkspace(projectPath))
85
+ continue;
86
+ const mcpsPath = join(projectPath, 'mcps');
80
87
  if (!existsSync(mcpsPath))
81
88
  continue;
82
89
  for (const serverDir of readdirSync(mcpsPath, { withFileTypes: true }).filter((d) => d.isDirectory())) {
@@ -60,6 +60,47 @@ function collectPluginInstallEntries(pluginKey, entries, results, constants) {
60
60
  collectMcpFile(installPath, results, constants);
61
61
  }
62
62
  }
63
+ const PLUGIN_CACHE_MCP_FILENAME = '.mcp.json';
64
+ function walkPluginCacheForMcp(dir, results, seenMcpPaths, constants, depth) {
65
+ if (depth > 12)
66
+ return;
67
+ try {
68
+ for (const ent of readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() || d.isFile())) {
69
+ const full = join(dir, ent.name);
70
+ if (ent.isDirectory()) {
71
+ walkPluginCacheForMcp(full, results, seenMcpPaths, constants, depth + 1);
72
+ continue;
73
+ }
74
+ if (!ent.isFile() || ent.name !== PLUGIN_CACHE_MCP_FILENAME)
75
+ continue;
76
+ if (seenMcpPaths.has(full))
77
+ continue;
78
+ const content = readJSONFile(full);
79
+ if (content === null)
80
+ continue;
81
+ seenMcpPaths.add(full);
82
+ const ver = versionFromPluginCachePath(full, constants);
83
+ results.push({
84
+ file_type: 'claude_plugin_mcp',
85
+ file_path: full,
86
+ raw_content: typeof content === 'object' ? { ...content, ...(ver ? { version: ver } : {}) } : content,
87
+ });
88
+ }
89
+ }
90
+ catch {
91
+ return;
92
+ }
93
+ }
94
+ /** All plugin MCP manifests under ~/.claude/plugins/cache, whether or not the plugin is enabled. */
95
+ function collectPluginCacheMcpFiles(constants, home = homedir()) {
96
+ const cacheRoot = join(home, '.claude', 'plugins', 'cache');
97
+ if (!existsSync(cacheRoot))
98
+ return [];
99
+ const results = [];
100
+ const seenMcpPaths = new Set();
101
+ walkPluginCacheForMcp(cacheRoot, results, seenMcpPaths, constants, 0);
102
+ return results;
103
+ }
63
104
  function collectConfigFilesFromInstalledPlugins(constants) {
64
105
  const home = homedir();
65
106
  const installedPluginsPath = getInstalledPluginsPath(home, constants);
@@ -86,4 +127,4 @@ function collectConfigFilesFromInstalledPlugins(constants) {
86
127
  }
87
128
  return results;
88
129
  }
89
- export { collectConfigFilesFromInstalledPlugins };
130
+ export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles };
@@ -12,8 +12,9 @@ import { ensureAuthentication } from '../auth/auth_flow.js';
12
12
  import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
13
13
  import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
14
14
  import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
15
- import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
15
+ import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
16
16
  import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
17
+ import { collectCursorProjectWorkspaceMcpConfigs } from '../collection/cursor_project_mcp_collector.js';
17
18
  import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
18
19
  import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
19
20
  import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js';
@@ -55,6 +56,14 @@ async function collectAllConfigFiles(endpointBase) {
55
56
  configFiles.push(m);
56
57
  }
57
58
  }
59
+ hookRunLog(`scanning Cursor project workspace MCP configs (~/.cursor/projects/*/mcp-cache.json)`);
60
+ for (const m of collectCursorProjectWorkspaceMcpConfigs(mcpToolSkipPrefixes, patternsResponse.client_path_constants, patternsResponse.workspace_vscdb_spec, HOME_DIR)) {
61
+ const key = `${m.file_type}\t${m.file_path}`;
62
+ if (!existingPaths.has(key)) {
63
+ existingPaths.add(key);
64
+ configFiles.push(m);
65
+ }
66
+ }
58
67
  if (patternsResponse.workspace_vscdb_spec) {
59
68
  hookRunLog(`scanning Cursor workspace vscdb files`);
60
69
  for (const m of collectWorkspaceVscdbs(patternsResponse.workspace_vscdb_spec)) {
@@ -69,6 +78,35 @@ async function collectAllConfigFiles(endpointBase) {
69
78
  if (!patternsResponse.client_path_constants)
70
79
  throw new Error('client_path_constants required from API response');
71
80
  configFiles.push(...collectConfigFilesFromInstalledPlugins(patternsResponse.client_path_constants));
81
+ hookRunLog(`scanning Claude plugin cache MCP manifests`);
82
+ for (const entry of collectPluginCacheMcpFiles(patternsResponse.client_path_constants, HOME_DIR)) {
83
+ const key = `${entry.file_type}\t${entry.file_path}`;
84
+ if (!existingPaths.has(key)) {
85
+ existingPaths.add(key);
86
+ configFiles.push(entry);
87
+ }
88
+ }
89
+ hookRunLog(`scanning Claude known projects from ~/.claude.json`);
90
+ for (const entry of collectMcpFromClaudeJsonProjects(HOME_DIR)) {
91
+ const key = `${entry.file_type}\t${entry.file_path}`;
92
+ if (!existingPaths.has(key)) {
93
+ existingPaths.add(key);
94
+ configFiles.push(entry);
95
+ }
96
+ }
97
+ hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
98
+ const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
99
+ hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
100
+ if (!diskExtMerge.hadRegistryUpload && diskExtMerge.mergedCount > 0) {
101
+ hookRunLog(`scanning Claude Desktop extension manifests on disk (no registry upload in batch)`);
102
+ for (const entry of collectClaudeDesktopExtensionManifests(HOME_DIR)) {
103
+ const key = `${entry.file_type}\t${entry.file_path}`;
104
+ if (!existingPaths.has(key)) {
105
+ existingPaths.add(key);
106
+ configFiles.push(entry);
107
+ }
108
+ }
109
+ }
72
110
  const worktreeReport = scanActiveWorktrees(PROJECT_ROOT, HOME_DIR);
73
111
  const activeRoots = activeWorktreeRootSet(worktreeReport);
74
112
  const beforeFilter = configFiles.length;
@@ -108,7 +146,7 @@ async function sendAllConfigFiles(configFiles, worktreeReport, hardwareUuid, aut
108
146
  const ok = await sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestId, manifest);
109
147
  hookRunLog(`hook-request update manifest result=${ok ? 'ok' : 'fail'}`);
110
148
  }
111
- // Finish ingest session: OpenClaw hold set, worktree prune (report may be []), and scans.
149
+ // Finish ingest session: failed_uploads hold set, worktree prune, config prune-on-absence, and scans.
112
150
  // Run finish whenever a session was started — even if every upload failed — so worktree
113
151
  // cleanup still runs for machines with only stale worktrees on disk.
114
152
  if (ingestSessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config-staging",
3
- "version": "1.3.94",
3
+ "version": "1.3.96",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {