log-llm-config-staging 1.3.94 → 1.3.97
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 +145 -0
- package/dist/log_config_files/collection/claude_known_projects_collector.js +42 -0
- package/dist/log_config_files/collection/config_collector.js +6 -2
- package/dist/log_config_files/collection/cursor_project_mcp_collector.js +198 -0
- package/dist/log_config_files/collection/cursor_project_workspace_activity.js +73 -0
- package/dist/log_config_files/collection/directory_collector.js +2 -0
- package/dist/log_config_files/collection/mcp_tool_collector.js +8 -1
- package/dist/log_config_files/collection/metadata_merge.js +43 -0
- package/dist/log_config_files/collection/plugin_collector.js +42 -1
- package/dist/log_config_files/runtime/compliance_check.js +5 -1
- package/dist/log_config_files/runtime/hook_type_for_request.js +28 -0
- package/dist/log_config_files/runtime/main_runner.js +42 -4
- package/dist/log_config_files/runtime/trusted_restarts.js +4 -0
- package/package.json +1 -1
|
@@ -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 };
|
|
@@ -7,6 +7,7 @@ import { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
|
7
7
|
import { collectDirectoryEntries, collectDirectoryMetadata } from './directory_collector.js';
|
|
8
8
|
import { collectVscdbEntries } from '../readers/vscdb_config_builder.js';
|
|
9
9
|
import { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
|
|
10
|
+
import { collapseMetadataByFileType } from './metadata_merge.js';
|
|
10
11
|
function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs = [], absolutePathPrefixes = [], mcpToolGlobSpec = null, clientPathConstants = null, pathResolutionSpecs = null) {
|
|
11
12
|
const seenPaths = new Set();
|
|
12
13
|
const targets = [];
|
|
@@ -148,12 +149,15 @@ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, optio
|
|
|
148
149
|
pushDerivedFilesFromRecipe(entry.raw_content, configFiles, recipe);
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
|
-
return configFiles;
|
|
152
|
+
return collapseMetadataByFileType(configFiles);
|
|
152
153
|
}
|
|
153
154
|
// ─── Re-exports (barrel for public API) ──────────────────────────────────────
|
|
154
155
|
export { collectConfigFilesFromPatterns };
|
|
155
156
|
export { collectMcpToolFiles } from './mcp_tool_collector.js';
|
|
156
|
-
export { collectConfigFilesFromInstalledPlugins } from './plugin_collector.js';
|
|
157
|
+
export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles } from './plugin_collector.js';
|
|
158
|
+
export { collectMcpFromClaudeJsonProjects } from './claude_known_projects_collector.js';
|
|
159
|
+
export { collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, } from './claude_desktop_extensions_collector.js';
|
|
160
|
+
export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
|
|
157
161
|
export { determineFileTypeFromPath } from './file_type_rules.js';
|
|
158
162
|
export { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
159
163
|
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, };
|
|
@@ -57,6 +57,8 @@ function matchesDirGlob(name, glob) {
|
|
|
57
57
|
// **/*.jsonl → match any extension suffix after **/
|
|
58
58
|
if (glob.startsWith('**/')) {
|
|
59
59
|
const suffix = glob.slice(3);
|
|
60
|
+
if (suffix === '*' || suffix === '**')
|
|
61
|
+
return true;
|
|
60
62
|
if (suffix.startsWith('*.'))
|
|
61
63
|
return name.endsWith(suffix.slice(1));
|
|
62
64
|
return name === suffix;
|
|
@@ -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
|
-
|
|
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())) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** When several install/log paths exist for one agent, keep a single metadata row (newest mtime). */
|
|
2
|
+
const METADATA_COLLAPSE_BY_FILE_TYPE = new Set(['opencode_presence', 'opencode_log']);
|
|
3
|
+
function metadataLastModifiedMs(entry) {
|
|
4
|
+
const raw = entry.raw_content;
|
|
5
|
+
if (!raw || typeof raw !== 'object')
|
|
6
|
+
return 0;
|
|
7
|
+
const lm = raw.last_modified;
|
|
8
|
+
if (typeof lm !== 'string')
|
|
9
|
+
return 0;
|
|
10
|
+
const ms = Date.parse(lm);
|
|
11
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Collapse multiple metadata uploads of the same file_type to one row: highest last_modified wins.
|
|
15
|
+
*/
|
|
16
|
+
function collapseMetadataByFileType(files) {
|
|
17
|
+
const passthrough = [];
|
|
18
|
+
const groups = new Map();
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
if (f.collect_style === 'metadata' && METADATA_COLLAPSE_BY_FILE_TYPE.has(f.file_type)) {
|
|
21
|
+
const list = groups.get(f.file_type) ?? [];
|
|
22
|
+
list.push(f);
|
|
23
|
+
groups.set(f.file_type, list);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
passthrough.push(f);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const merged = [...passthrough];
|
|
30
|
+
for (const [, list] of groups) {
|
|
31
|
+
if (list.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
let best = list[0];
|
|
34
|
+
for (let i = 1; i < list.length; i++) {
|
|
35
|
+
if (metadataLastModifiedMs(list[i]) > metadataLastModifiedMs(best)) {
|
|
36
|
+
best = list[i];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
merged.push(best);
|
|
40
|
+
}
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
export { collapseMetadataByFileType, METADATA_COLLAPSE_BY_FILE_TYPE };
|
|
@@ -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 };
|
|
@@ -41,6 +41,8 @@ export function normalizeAgentToken(raw) {
|
|
|
41
41
|
return 'cursor';
|
|
42
42
|
if (s === 'copilot')
|
|
43
43
|
return 'copilot';
|
|
44
|
+
if (s === 'opencode')
|
|
45
|
+
return 'opencode';
|
|
44
46
|
return '';
|
|
45
47
|
}
|
|
46
48
|
function currentAgentFromEnv() {
|
|
@@ -48,12 +50,14 @@ function currentAgentFromEnv() {
|
|
|
48
50
|
const override = normalizeAgentToken(process.env.OPTIMUS_AGENT);
|
|
49
51
|
if (override)
|
|
50
52
|
return override;
|
|
51
|
-
// Backwards-compatible: hook wrappers set OPTIMUS_HOOK_TYPE to cursor|claude|copilot.
|
|
53
|
+
// Backwards-compatible: hook wrappers set OPTIMUS_HOOK_TYPE to cursor|claude|copilot|opencode.
|
|
52
54
|
const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
|
|
53
55
|
if (hookType === 'cursor')
|
|
54
56
|
return 'cursor';
|
|
55
57
|
if (hookType === 'copilot')
|
|
56
58
|
return 'copilot';
|
|
59
|
+
if (hookType === 'opencode')
|
|
60
|
+
return 'opencode';
|
|
57
61
|
return 'claude';
|
|
58
62
|
}
|
|
59
63
|
function targetsCurrentAgent(entry, agent) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function normalizeToken(raw) {
|
|
2
|
+
if (!raw?.trim())
|
|
3
|
+
return '';
|
|
4
|
+
const s = raw.trim().toLowerCase().replace(/-/g, '_');
|
|
5
|
+
if (s === 'claude_desktop')
|
|
6
|
+
return 'claude';
|
|
7
|
+
if (s === 'github_copilot')
|
|
8
|
+
return 'copilot';
|
|
9
|
+
if (s === 'cursor' || s === 'claude' || s === 'copilot' || s === 'opencode')
|
|
10
|
+
return s;
|
|
11
|
+
// Legacy hooks set OPTIMUS_AGENT=Cursor (display casing)
|
|
12
|
+
if (raw.trim() === 'Cursor')
|
|
13
|
+
return 'cursor';
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve hook_request.hook_type from hook wrapper env (OPTIMUS_HOOK_TYPE, then OPTIMUS_AGENT). */
|
|
17
|
+
export function resolveHookTypeFromEnv(env = process.env) {
|
|
18
|
+
const fromHook = normalizeToken(env.OPTIMUS_HOOK_TYPE);
|
|
19
|
+
const fromAgent = normalizeToken(env.OPTIMUS_AGENT);
|
|
20
|
+
const token = fromHook || fromAgent || 'claude';
|
|
21
|
+
if (token === 'cursor')
|
|
22
|
+
return 'cursor';
|
|
23
|
+
if (token === 'copilot')
|
|
24
|
+
return 'copilot';
|
|
25
|
+
if (token === 'opencode')
|
|
26
|
+
return 'opencode';
|
|
27
|
+
return 'claude';
|
|
28
|
+
}
|
|
@@ -7,13 +7,15 @@ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
|
7
7
|
import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
|
|
8
8
|
import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
|
|
9
9
|
import { hookLogReplace, hookRunLog } from './hook_logger.js';
|
|
10
|
+
import { resolveHookTypeFromEnv } from './hook_type_for_request.js';
|
|
10
11
|
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
11
12
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
12
13
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
13
14
|
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
14
15
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
15
|
-
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
|
|
16
|
+
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
16
17
|
import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
|
|
18
|
+
import { collectCursorProjectWorkspaceMcpConfigs } from '../collection/cursor_project_mcp_collector.js';
|
|
17
19
|
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
18
20
|
import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
|
|
19
21
|
import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js';
|
|
@@ -55,6 +57,14 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
55
57
|
configFiles.push(m);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
hookRunLog(`scanning Cursor project workspace MCP configs (~/.cursor/projects/*/mcp-cache.json)`);
|
|
61
|
+
for (const m of collectCursorProjectWorkspaceMcpConfigs(mcpToolSkipPrefixes, patternsResponse.client_path_constants, patternsResponse.workspace_vscdb_spec, HOME_DIR)) {
|
|
62
|
+
const key = `${m.file_type}\t${m.file_path}`;
|
|
63
|
+
if (!existingPaths.has(key)) {
|
|
64
|
+
existingPaths.add(key);
|
|
65
|
+
configFiles.push(m);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
if (patternsResponse.workspace_vscdb_spec) {
|
|
59
69
|
hookRunLog(`scanning Cursor workspace vscdb files`);
|
|
60
70
|
for (const m of collectWorkspaceVscdbs(patternsResponse.workspace_vscdb_spec)) {
|
|
@@ -69,6 +79,35 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
69
79
|
if (!patternsResponse.client_path_constants)
|
|
70
80
|
throw new Error('client_path_constants required from API response');
|
|
71
81
|
configFiles.push(...collectConfigFilesFromInstalledPlugins(patternsResponse.client_path_constants));
|
|
82
|
+
hookRunLog(`scanning Claude plugin cache MCP manifests`);
|
|
83
|
+
for (const entry of collectPluginCacheMcpFiles(patternsResponse.client_path_constants, HOME_DIR)) {
|
|
84
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
85
|
+
if (!existingPaths.has(key)) {
|
|
86
|
+
existingPaths.add(key);
|
|
87
|
+
configFiles.push(entry);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
hookRunLog(`scanning Claude known projects from ~/.claude.json`);
|
|
91
|
+
for (const entry of collectMcpFromClaudeJsonProjects(HOME_DIR)) {
|
|
92
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
93
|
+
if (!existingPaths.has(key)) {
|
|
94
|
+
existingPaths.add(key);
|
|
95
|
+
configFiles.push(entry);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
99
|
+
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
100
|
+
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|
|
101
|
+
if (!diskExtMerge.hadRegistryUpload && diskExtMerge.mergedCount > 0) {
|
|
102
|
+
hookRunLog(`scanning Claude Desktop extension manifests on disk (no registry upload in batch)`);
|
|
103
|
+
for (const entry of collectClaudeDesktopExtensionManifests(HOME_DIR)) {
|
|
104
|
+
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
105
|
+
if (!existingPaths.has(key)) {
|
|
106
|
+
existingPaths.add(key);
|
|
107
|
+
configFiles.push(entry);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
72
111
|
const worktreeReport = scanActiveWorktrees(PROJECT_ROOT, HOME_DIR);
|
|
73
112
|
const activeRoots = activeWorktreeRootSet(worktreeReport);
|
|
74
113
|
const beforeFilter = configFiles.length;
|
|
@@ -90,8 +129,7 @@ async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
|
90
129
|
}
|
|
91
130
|
}
|
|
92
131
|
async function sendAllConfigFiles(configFiles, worktreeReport, hardwareUuid, authKey) {
|
|
93
|
-
const
|
|
94
|
-
const hookType = hookTypeRaw === 'cursor' ? 'cursor' : hookTypeRaw === 'copilot' ? 'copilot' : 'claude';
|
|
132
|
+
const hookType = resolveHookTypeFromEnv();
|
|
95
133
|
const manifest = configFiles.map((c) => canonicalCursorUserStateVscdbPath(c.file_path));
|
|
96
134
|
const workspaceRepo = ensureWorkspaceRepoEnv(manifest);
|
|
97
135
|
const hookRequestId = await sendHookRequestCreate(hardwareUuid, authKey, hookType, workspaceRepo);
|
|
@@ -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:
|
|
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) {
|
|
@@ -34,6 +34,8 @@ function currentAgentFromEnv() {
|
|
|
34
34
|
return 'cursor';
|
|
35
35
|
if (override === 'copilot')
|
|
36
36
|
return 'copilot';
|
|
37
|
+
if (override === 'opencode')
|
|
38
|
+
return 'opencode';
|
|
37
39
|
if (override === 'claude' || override === 'claude_desktop')
|
|
38
40
|
return 'claude';
|
|
39
41
|
const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
|
|
@@ -41,6 +43,8 @@ function currentAgentFromEnv() {
|
|
|
41
43
|
return 'cursor';
|
|
42
44
|
if (hookType === 'copilot')
|
|
43
45
|
return 'copilot';
|
|
46
|
+
if (hookType === 'opencode')
|
|
47
|
+
return 'opencode';
|
|
44
48
|
return 'claude';
|
|
45
49
|
}
|
|
46
50
|
/** Spawn each trusted command detached (same pattern as former compliance_prompt_gate fireRestartCommands). */
|