@optimuslabs/harness-map-staging 1.5.16
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/README.md +46 -0
- package/dist/apply_deferred_vscdb.js +38 -0
- package/dist/bootstrap_constants.js +15 -0
- package/dist/cli/bash_script_generator.js +95 -0
- package/dist/cli.js +144 -0
- package/dist/cli_invocation_match.js +30 -0
- package/dist/compliance_check_runner.js +57 -0
- package/dist/compliance_prompt_gate.js +396 -0
- package/dist/dialog_prefs_cli.js +109 -0
- package/dist/endpoint_client/compress_payload.js +25 -0
- package/dist/endpoint_client/http_transport.js +90 -0
- package/dist/endpoint_client/index.js +3 -0
- package/dist/endpoint_client/registry_api.js +41 -0
- package/dist/endpoint_client/startup_api.js +44 -0
- package/dist/endpoint_client/types.js +4 -0
- package/dist/execute_trusted_restarts.js +60 -0
- package/dist/log_config_files/auth/auth_flow.js +22 -0
- package/dist/log_config_files/auth/auth_key_store.js +14 -0
- package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +248 -0
- package/dist/log_config_files/collection/claude_known_projects_collector.js +42 -0
- package/dist/log_config_files/collection/claude_token_usage_collector.js +203 -0
- package/dist/log_config_files/collection/codex_token_usage_collector.js +342 -0
- package/dist/log_config_files/collection/config_collector.js +245 -0
- package/dist/log_config_files/collection/copilot_token_usage_collector.js +167 -0
- package/dist/log_config_files/collection/cowork_desktop_version_collector.js +60 -0
- package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
- package/dist/log_config_files/collection/cursor_project_mcp_collector.js +230 -0
- package/dist/log_config_files/collection/cursor_project_workspace_activity.js +73 -0
- package/dist/log_config_files/collection/cursor_token_usage_collector.js +192 -0
- package/dist/log_config_files/collection/directory_collector.js +126 -0
- package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
- package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
- package/dist/log_config_files/collection/file_type_rules.js +47 -0
- package/dist/log_config_files/collection/grok_cli_version_collector.js +53 -0
- package/dist/log_config_files/collection/hermes_token_usage_collector.js +241 -0
- package/dist/log_config_files/collection/mcp_tool_collector.js +106 -0
- package/dist/log_config_files/collection/metadata_merge.js +43 -0
- package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
- package/dist/log_config_files/collection/openclaw_token_usage_collector.js +223 -0
- package/dist/log_config_files/collection/opencode_token_usage_collector.js +177 -0
- package/dist/log_config_files/collection/pi_token_usage_collector.js +239 -0
- package/dist/log_config_files/collection/plugin_collector.js +223 -0
- package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
- package/dist/log_config_files/collection/skills_cli_collector.js +162 -0
- package/dist/log_config_files/index.js +19 -0
- package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
- package/dist/log_config_files/paths/pattern_resolver.js +326 -0
- package/dist/log_config_files/readers/composer_shadow_merge.js +67 -0
- package/dist/log_config_files/readers/cursor_shadow_merge_ruleset.js +46 -0
- package/dist/log_config_files/readers/file_readers.js +196 -0
- package/dist/log_config_files/readers/vscdb_config_builder.js +148 -0
- package/dist/log_config_files/readers/vscdb_reactive_storage.js +62 -0
- package/dist/log_config_files/readers/vscdb_reader.js +337 -0
- package/dist/log_config_files/runtime/client_event_reporter.js +347 -0
- package/dist/log_config_files/runtime/compliance_check.js +1013 -0
- package/dist/log_config_files/runtime/compliance_session_log.js +378 -0
- package/dist/log_config_files/runtime/dialog_preferences.js +31 -0
- package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
- package/dist/log_config_files/runtime/hook_logger.js +267 -0
- package/dist/log_config_files/runtime/hook_type_for_request.js +30 -0
- package/dist/log_config_files/runtime/log_metadata.js +122 -0
- package/dist/log_config_files/runtime/main_runner.js +376 -0
- package/dist/log_config_files/runtime/management_storage.js +144 -0
- package/dist/log_config_files/runtime/ops_target_path.js +18 -0
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +152 -0
- package/dist/log_config_files/runtime/remediation_config_path.js +140 -0
- package/dist/log_config_files/runtime/remediation_sync.js +1705 -0
- package/dist/log_config_files/runtime/secret_regex_scan.js +126 -0
- package/dist/log_config_files/runtime/sqlite_binary.js +124 -0
- package/dist/log_config_files/runtime/trusted_restarts.js +113 -0
- package/dist/log_config_files/runtime/workspace_repo.js +170 -0
- package/dist/log_config_files/runtime/worktree_absent.js +50 -0
- package/dist/log_config_files/runtime/worktree_scanner.js +137 -0
- package/dist/log_config_files/sender/batch_sender.js +283 -0
- package/dist/log_config_files/sender/endpoint_config.js +24 -0
- package/dist/log_config_files/sender/signing.js +1 -0
- package/dist/log_sensitive_paths_audit.js +89 -0
- package/dist/log_uuid/auth_key_store.js +37 -0
- package/dist/log_uuid/hardware_uuid.js +35 -0
- package/dist/log_uuid/index.js +11 -0
- package/dist/log_uuid/log_uuid_helper.js +30 -0
- package/dist/log_uuid/startup_sender.js +131 -0
- package/dist/log_uuid/user_profile.js +192 -0
- package/dist/remediation_change_preview.js +182 -0
- package/dist/tofu.js +22 -0
- package/dist/tofu_environment.js +36 -0
- package/dist/types/config_file_types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export const SKILLS_CLI_FILE_TYPE = 'skills_cli_installed';
|
|
6
|
+
export const SKILLS_CLI_INSTALLED_PATH = join(homedir(), '.agents', '.skills-cli-installed.json');
|
|
7
|
+
/** Override the skills package spec, e.g. `skills@1.5.10`. Default uses whatever is on the machine. */
|
|
8
|
+
export const SKILLS_CLI_NPX_PACKAGE_ENV = 'SKILLS_CLI_NPX_PACKAGE';
|
|
9
|
+
const LIST_TIMEOUT_MS = 120_000;
|
|
10
|
+
/** stderr from npx/npm only; stdout is streamed to a temp file (avoids pipe/maxBuffer truncation). */
|
|
11
|
+
const LIST_STDERR_MAX_BUFFER = 16 * 1024 * 1024;
|
|
12
|
+
function listExecEnv() {
|
|
13
|
+
return {
|
|
14
|
+
...process.env,
|
|
15
|
+
DISABLE_TELEMETRY: process.env.DISABLE_TELEMETRY ?? '1',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function npxPackageSpec() {
|
|
19
|
+
const fromEnv = (process.env[SKILLS_CLI_NPX_PACKAGE_ENV] || '').trim();
|
|
20
|
+
return fromEnv || 'skills';
|
|
21
|
+
}
|
|
22
|
+
/** Strip leading npx/npm noise and parse the skills `list --json` array payload. */
|
|
23
|
+
export function parseSkillsListJsonStdout(stdout) {
|
|
24
|
+
const trimmed = stdout.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
return [];
|
|
27
|
+
const start = trimmed.indexOf('[');
|
|
28
|
+
if (start < 0) {
|
|
29
|
+
throw new SyntaxError('skills list --json: no JSON array in stdout');
|
|
30
|
+
}
|
|
31
|
+
let end = trimmed.lastIndexOf(']');
|
|
32
|
+
let lastErr;
|
|
33
|
+
while (end > start) {
|
|
34
|
+
const slice = trimmed.slice(start, end + 1);
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(slice);
|
|
37
|
+
if (!Array.isArray(parsed))
|
|
38
|
+
return [];
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
lastErr = err instanceof SyntaxError ? err : new SyntaxError(String(err));
|
|
43
|
+
end = trimmed.lastIndexOf(']', end - 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw lastErr ?? new SyntaxError('skills list --json: could not parse stdout');
|
|
47
|
+
}
|
|
48
|
+
export function runSkillsListJson(args, cwd) {
|
|
49
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'optimus-skills-list-'));
|
|
50
|
+
const outPath = join(tmpDir, 'stdout.json');
|
|
51
|
+
const outFd = openSync(outPath, 'w');
|
|
52
|
+
try {
|
|
53
|
+
const child = spawnSync('npx', [npxPackageSpec(), 'list', ...args, '--json'], {
|
|
54
|
+
cwd,
|
|
55
|
+
timeout: LIST_TIMEOUT_MS,
|
|
56
|
+
env: listExecEnv(),
|
|
57
|
+
stdio: ['ignore', outFd, 'pipe'],
|
|
58
|
+
maxBuffer: LIST_STDERR_MAX_BUFFER,
|
|
59
|
+
});
|
|
60
|
+
if (child.error)
|
|
61
|
+
throw child.error;
|
|
62
|
+
if (child.status !== 0) {
|
|
63
|
+
const stderr = (child.stderr ?? '').toString().trim();
|
|
64
|
+
throw new Error(stderr || `npx skills list exited ${child.status ?? 'unknown'}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
closeSync(outFd);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const out = readFileSync(outPath, 'utf8');
|
|
72
|
+
return parseSkillsListJsonStdout(out);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function assignListLockFields(row, out) {
|
|
79
|
+
for (const key of ['source', 'sourceUrl', 'sourceType']) {
|
|
80
|
+
if (!Object.prototype.hasOwnProperty.call(row, key))
|
|
81
|
+
continue;
|
|
82
|
+
const raw = row[key];
|
|
83
|
+
if (raw == null) {
|
|
84
|
+
out[key] = null;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const trimmed = String(raw).trim();
|
|
88
|
+
out[key] = trimmed || null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function normalizeListRows(rows, scope) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const name = (row.name || '').trim();
|
|
96
|
+
const path = (row.path || '').trim();
|
|
97
|
+
if (!name || !path)
|
|
98
|
+
continue;
|
|
99
|
+
const agents = Array.isArray(row.agents)
|
|
100
|
+
? row.agents.map((a) => String(a).trim()).filter(Boolean)
|
|
101
|
+
: [];
|
|
102
|
+
const entry = {
|
|
103
|
+
name,
|
|
104
|
+
path,
|
|
105
|
+
scope: (row.scope || scope).trim() || scope,
|
|
106
|
+
agents,
|
|
107
|
+
};
|
|
108
|
+
assignListLockFields(row, entry);
|
|
109
|
+
out.push(entry);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** One-line-per-scope summary for hook_request.log (matches `skills list --json`). */
|
|
114
|
+
export function formatSkillsListScopeForHookLog(scopeLabel, entries) {
|
|
115
|
+
if (entries.length === 0) {
|
|
116
|
+
return `skills_cli list ${scopeLabel}: 0 skill(s)`;
|
|
117
|
+
}
|
|
118
|
+
const detail = entries
|
|
119
|
+
.map((e) => `${e.name}@${e.path} agents=${e.agents.length ? e.agents.join(',') : 'none'}`)
|
|
120
|
+
.join(' | ');
|
|
121
|
+
return `skills_cli list ${scopeLabel}: ${entries.length} skill(s) — ${detail}`;
|
|
122
|
+
}
|
|
123
|
+
function runSkillsListForScope(scopeLabel, args, projectRoot, logLine) {
|
|
124
|
+
try {
|
|
125
|
+
return runSkillsListJson(args, projectRoot);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
logLine(`skills_cli: list ${scopeLabel} --json failed: ${msg}`);
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function collectSkillsCliInstalled(projectRoot, log) {
|
|
134
|
+
const logLine = (message) => {
|
|
135
|
+
log?.(message);
|
|
136
|
+
};
|
|
137
|
+
const packageSpec = npxPackageSpec();
|
|
138
|
+
logLine(`skills_cli: npx ${packageSpec} — list -g --json && list --json (projectRoot=${projectRoot})`);
|
|
139
|
+
const globalRows = runSkillsListForScope('-g', ['-g'], projectRoot, logLine);
|
|
140
|
+
const projectRows = runSkillsListForScope('project', [], projectRoot, logLine);
|
|
141
|
+
const global = normalizeListRows(globalRows, 'global');
|
|
142
|
+
const project = normalizeListRows(projectRows, 'project');
|
|
143
|
+
logLine(formatSkillsListScopeForHookLog('-g', global));
|
|
144
|
+
logLine(formatSkillsListScopeForHookLog('project', project));
|
|
145
|
+
const payload = {
|
|
146
|
+
version: 1,
|
|
147
|
+
skills_cli_version: packageSpec,
|
|
148
|
+
generated_at: new Date().toISOString(),
|
|
149
|
+
global,
|
|
150
|
+
project,
|
|
151
|
+
};
|
|
152
|
+
if (payload.global.length === 0 && payload.project.length === 0) {
|
|
153
|
+
logLine('skills_cli_installed: not uploaded (no global or project skills)');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
logLine(`skills_cli_installed: upload ${SKILLS_CLI_INSTALLED_PATH} global=${global.length} project=${project.length}`);
|
|
157
|
+
return {
|
|
158
|
+
file_type: SKILLS_CLI_FILE_TYPE,
|
|
159
|
+
file_path: SKILLS_CLI_INSTALLED_PATH,
|
|
160
|
+
raw_content: payload,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Barrel — all public exports. Sub-modules live in this folder.
|
|
3
|
+
export { BATCH_CHUNK_SIZE } from './sender/batch_sender.js';
|
|
4
|
+
export { sendConfigFile, sendConfigFilesBatch, sendIngestSessionStart, sendIngestSessionFinish } from './sender/batch_sender.js';
|
|
5
|
+
export { createSignature, canonicalizePayload } from './sender/signing.js';
|
|
6
|
+
export { readVSCDBState } from './readers/vscdb_reader.js';
|
|
7
|
+
export { readJSONFile, readMarkdownFile } from './readers/file_readers.js';
|
|
8
|
+
export { resolvePatternToTargets } from './paths/pattern_resolver.js';
|
|
9
|
+
export { ensureAuthentication } from './auth/auth_flow.js';
|
|
10
|
+
export { collectConfigFilesFromPatterns, collectConfigFilesFromInstalledPlugins, collectMcpToolFiles, determineFileTypeFromPath, } from './collection/config_collector.js';
|
|
11
|
+
export { main, logSingleFile } from './runtime/main_runner.js';
|
|
12
|
+
// Entry point guard (when invoked directly via npx or node)
|
|
13
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.includes('log_config_files')) {
|
|
14
|
+
const { main } = await import('./runtime/main_runner.js');
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build paths from backend client_path_constants + home.
|
|
3
|
+
* All functions require constants from the API response — no hardcoded fallbacks.
|
|
4
|
+
* Throws if constants or a required key is missing.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
function requireKey(constants, key, label) {
|
|
8
|
+
const val = constants[key];
|
|
9
|
+
if (val === undefined || val === null)
|
|
10
|
+
throw new Error(`client_path_constants missing required key "${String(key)}" (${label})`);
|
|
11
|
+
return val;
|
|
12
|
+
}
|
|
13
|
+
export function getVscdbPath(home, constants) {
|
|
14
|
+
const segments = requireKey(constants, 'vscdb_path_under_home', 'vscdb path segments');
|
|
15
|
+
return join(home, ...segments);
|
|
16
|
+
}
|
|
17
|
+
export function getExtensionsCachePath(home, constants) {
|
|
18
|
+
const segments = requireKey(constants, 'extensions_cache_path_under_home', 'extensions cache path segments');
|
|
19
|
+
return join(home, ...segments);
|
|
20
|
+
}
|
|
21
|
+
export function getCursorProjectsPath(home, constants) {
|
|
22
|
+
const rel = requireKey(constants, 'cursor_projects_path_under_home', 'cursor projects path');
|
|
23
|
+
return join(home, rel.replace(/^~\//, ''));
|
|
24
|
+
}
|
|
25
|
+
export function getInstalledPluginsPath(home, constants) {
|
|
26
|
+
const rel = requireKey(constants, 'installed_plugins_path_under_home', 'installed plugins path');
|
|
27
|
+
return join(home, rel.replace(/^~\//, ''));
|
|
28
|
+
}
|
|
29
|
+
export function getOptAiSecManagementPath(home, constants) {
|
|
30
|
+
const rel = requireKey(constants, 'optimuslabs_management_under_home', '.optimuslabs management path');
|
|
31
|
+
return join(home, rel.replace(/^~\//, ''));
|
|
32
|
+
}
|
|
33
|
+
export function getAuthKeyPathFromConstants(home, constants) {
|
|
34
|
+
const base = getOptAiSecManagementPath(home, constants);
|
|
35
|
+
const filename = requireKey(constants, 'auth_key_filename', 'auth key filename');
|
|
36
|
+
return join(base, filename);
|
|
37
|
+
}
|
|
38
|
+
export function getHookLogPathFromConstants(home, constants) {
|
|
39
|
+
const base = getOptAiSecManagementPath(home, constants);
|
|
40
|
+
const filename = requireKey(constants, 'hook_log_filename', 'hook log filename');
|
|
41
|
+
return join(base, filename);
|
|
42
|
+
}
|
|
43
|
+
export function getSensitivePathsAuditPathFromConstants(home, constants) {
|
|
44
|
+
const base = getOptAiSecManagementPath(home, constants);
|
|
45
|
+
const filename = requireKey(constants, 'sensitive_paths_audit_filename', 'sensitive paths audit filename');
|
|
46
|
+
return join(base, filename);
|
|
47
|
+
}
|
|
48
|
+
export function getExtensionsCacheInstalledSuffix(constants) {
|
|
49
|
+
return requireKey(constants, 'extensions_cache_installed_suffix', 'extensions cache installed suffix');
|
|
50
|
+
}
|
|
51
|
+
export function getPluginSkillFilename(constants) {
|
|
52
|
+
return requireKey(constants, 'plugin_skill_filename', 'plugin skill filename');
|
|
53
|
+
}
|
|
54
|
+
export function getPluginSkillsDir(constants) {
|
|
55
|
+
return requireKey(constants, 'plugin_skills_dir', 'plugin skills directory');
|
|
56
|
+
}
|
|
57
|
+
export function getPluginHooksPath(constants) {
|
|
58
|
+
return requireKey(constants, 'plugin_hooks_path', 'plugin hooks path');
|
|
59
|
+
}
|
|
60
|
+
export function getPluginMcpPaths(constants) {
|
|
61
|
+
return requireKey(constants, 'plugin_mcp_paths', 'plugin mcp paths');
|
|
62
|
+
}
|
|
63
|
+
/** Build path from a path_constant_key (from PATH_RESOLUTION_SPECS). Keeps pattern_resolver data-driven. */
|
|
64
|
+
export function getPathByConstantKey(home, constants, pathConstantKey) {
|
|
65
|
+
const val = constants[pathConstantKey];
|
|
66
|
+
if (Array.isArray(val))
|
|
67
|
+
return join(home, ...val);
|
|
68
|
+
if (typeof val === 'string')
|
|
69
|
+
return join(home, val.replace(/^~\//, ''));
|
|
70
|
+
throw new Error(`client_path_constants missing required key "${pathConstantKey}"`);
|
|
71
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getPathByConstantKey, getExtensionsCacheInstalledSuffix } from './path_constants_helpers.js';
|
|
4
|
+
const RECURSIVE_GLOB_MAX_DEPTH = 20;
|
|
5
|
+
/** Only non-empty strings (empty would match all dirs via startsWith('')). */
|
|
6
|
+
function normalizePathSkipPrefixes(prefixes) {
|
|
7
|
+
if (!Array.isArray(prefixes))
|
|
8
|
+
return [];
|
|
9
|
+
return prefixes.filter((p) => typeof p === 'string' && p.length > 0);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Expands glob patterns with double-asterisk (recursive directory traversal).
|
|
13
|
+
*
|
|
14
|
+
* Example: ~/.cursor/plugins/cache/ ** /skills/ recursively finds all skills
|
|
15
|
+
* directories under the cache folder, up to RECURSIVE_GLOB_MAX_DEPTH.
|
|
16
|
+
*
|
|
17
|
+
* @param pathPattern - Glob pattern with double-asterisk for recursive descent
|
|
18
|
+
* @param fileType - File type classification for collected targets
|
|
19
|
+
* @param home - User home directory path
|
|
20
|
+
* @param contentFormat - Optional content format hint
|
|
21
|
+
* @param dirGlob - Optional directory glob pattern
|
|
22
|
+
* @param homeRecurseSkipDirs - Directory names to skip when recursing from home
|
|
23
|
+
* @returns Array of collection targets matching the pattern
|
|
24
|
+
*/
|
|
25
|
+
function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentFormat, dirGlob, homeRecurseSkipDirs = []) {
|
|
26
|
+
const norm = pathPattern.replace(/\\/g, '/');
|
|
27
|
+
const doubleStarIndex = norm.indexOf('**');
|
|
28
|
+
if (doubleStarIndex === -1)
|
|
29
|
+
return [];
|
|
30
|
+
const before = norm.slice(0, doubleStarIndex).replace(/\/+$/, '');
|
|
31
|
+
const after = norm.slice(doubleStarIndex + 2).replace(/^\/+/, '');
|
|
32
|
+
if (!after)
|
|
33
|
+
return [];
|
|
34
|
+
let basePath;
|
|
35
|
+
if (before === '~' || before.startsWith('~/')) {
|
|
36
|
+
basePath = join(home, before === '~' ? '' : before.slice(2));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const targets = [];
|
|
42
|
+
const parts = after.split('/').filter((s) => s.length > 0);
|
|
43
|
+
if (parts.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const dirName = parts[0];
|
|
46
|
+
const fileName = parts.length > 1 ? parts[parts.length - 1] : null;
|
|
47
|
+
const skipSet = homeRecurseSkipDirs.length ? new Set(homeRecurseSkipDirs) : new Set();
|
|
48
|
+
function walk(dir, depth, isHomeRoot) {
|
|
49
|
+
if (depth > RECURSIVE_GLOB_MAX_DEPTH || !existsSync(dir))
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
53
|
+
const full = join(dir, entry.name);
|
|
54
|
+
if (!entry.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
if (isHomeRoot && skipSet.has(entry.name))
|
|
57
|
+
continue;
|
|
58
|
+
if (entry.name === dirName) {
|
|
59
|
+
const filePath = parts.length === 1 ? full : join(full, ...parts.slice(1));
|
|
60
|
+
if (parts.length === 1) {
|
|
61
|
+
// e.g. ~/.cursor/plugins/cache/**/skills/ — collect each matched skills/ tree as a directory target
|
|
62
|
+
if (existsSync(filePath)) {
|
|
63
|
+
targets.push({
|
|
64
|
+
path: filePath,
|
|
65
|
+
file_type: fileType,
|
|
66
|
+
isDirectory: true,
|
|
67
|
+
content_format: contentFormat,
|
|
68
|
+
dir_glob: dirGlob,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (fileName && existsSync(filePath)) {
|
|
73
|
+
targets.push({ path: filePath, file_type: fileType, content_format: contentFormat });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
walk(full, depth + 1, false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore read errors */ }
|
|
80
|
+
}
|
|
81
|
+
walk(basePath, 0, true);
|
|
82
|
+
return targets;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Expands glob patterns with asterisk wildcards (non-recursive) into concrete file or directory paths.
|
|
86
|
+
*
|
|
87
|
+
* Supports multi-segment wildcards (e.g., star-slash-star-slash patterns).
|
|
88
|
+
*
|
|
89
|
+
* **Key behavior:** On the final segment of a file pattern (not ending in slash), wildcards match
|
|
90
|
+
* files; earlier segments—and directory patterns—match directories only. This allows filename
|
|
91
|
+
* patterns like local_STAR.json to correctly resolve files rather than directories.
|
|
92
|
+
*
|
|
93
|
+
* Examples:
|
|
94
|
+
* - Pattern ending with a literal filename matches that file in wildcard directories
|
|
95
|
+
* - Pattern ending with prefix_STAR.ext matches files with that prefix and extension
|
|
96
|
+
* - Pattern ending with slash matches only directories
|
|
97
|
+
*
|
|
98
|
+
* @param pathPattern - Glob pattern (may not contain double-asterisk; use expandRecursiveGlobPathPattern for that)
|
|
99
|
+
* @param fileType - File type classification for collected targets
|
|
100
|
+
* @param home - User home directory path
|
|
101
|
+
* @param projectRoot - Project root directory path
|
|
102
|
+
* @param contentFormat - Optional content format hint
|
|
103
|
+
* @param dirGlob - Optional directory glob pattern
|
|
104
|
+
* @param absolutePathPrefixes - Allowed absolute path prefixes
|
|
105
|
+
* @returns Array of collection targets matching the pattern
|
|
106
|
+
*/
|
|
107
|
+
function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, contentFormat, dirGlob, absolutePathPrefixes = []) {
|
|
108
|
+
const norm = pathPattern.replace(/\\/g, '/');
|
|
109
|
+
if (!norm.includes('*'))
|
|
110
|
+
return [];
|
|
111
|
+
// `**` is the recursive-glob sigil handled by expandRecursiveGlobPathPattern.
|
|
112
|
+
if (norm.includes('**'))
|
|
113
|
+
return [];
|
|
114
|
+
const isDir = norm.endsWith('/');
|
|
115
|
+
// Resolve the literal prefix that ends before the first wildcard segment.
|
|
116
|
+
const firstStarIndex = norm.indexOf('*');
|
|
117
|
+
const lastSlashBeforeStar = norm.lastIndexOf('/', firstStarIndex);
|
|
118
|
+
if (lastSlashBeforeStar === -1)
|
|
119
|
+
return [];
|
|
120
|
+
const parentPart = norm.slice(0, lastSlashBeforeStar);
|
|
121
|
+
let basePath;
|
|
122
|
+
if (parentPart.startsWith('~/')) {
|
|
123
|
+
basePath = join(home, parentPart.slice(2));
|
|
124
|
+
}
|
|
125
|
+
else if (parentPart.startsWith('/') && absolutePathPrefixes.some((prefix) => parentPart.startsWith(prefix))) {
|
|
126
|
+
basePath = parentPart;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
basePath = join(projectRoot, parentPart.startsWith('/') ? parentPart.slice(1) : parentPart);
|
|
130
|
+
}
|
|
131
|
+
if (!existsSync(basePath))
|
|
132
|
+
return [];
|
|
133
|
+
// Walk remaining segments. Each segment may be a bare wildcard '*' (matches
|
|
134
|
+
// any single directory), a prefix/suffix wildcard like 'foo*' or 'foo*bar',
|
|
135
|
+
// or a literal name. Recursion correctly handles multi-wildcard patterns
|
|
136
|
+
// such as ``*/*/cowork_plugins/marketplaces/*/.claude-plugin/marketplace.json``
|
|
137
|
+
// — the prior single-split implementation only expanded the first wildcard
|
|
138
|
+
// and silently pushed a partial directory path as the target, which then
|
|
139
|
+
// surfaced as EISDIR when downstream collectors tried to read it as a file.
|
|
140
|
+
const remaining = norm.slice(lastSlashBeforeStar + 1);
|
|
141
|
+
const segments = remaining.split('/').filter((s) => s !== '');
|
|
142
|
+
const targets = [];
|
|
143
|
+
function recurse(currentPath, segIdx) {
|
|
144
|
+
if (segIdx === segments.length) {
|
|
145
|
+
if (!existsSync(currentPath))
|
|
146
|
+
return;
|
|
147
|
+
targets.push({ path: currentPath, file_type: fileType, isDirectory: isDir, dir_glob: dirGlob, content_format: contentFormat });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const seg = segments[segIdx];
|
|
151
|
+
// On the final segment of a file pattern (not ending in `/`) wildcards
|
|
152
|
+
// match files; earlier segments — and dir patterns — match directories.
|
|
153
|
+
const wantFile = segIdx === segments.length - 1 && !isDir;
|
|
154
|
+
const entryMatches = (entry) => wantFile ? entry.isFile() : entry.isDirectory();
|
|
155
|
+
if (seg === '*') {
|
|
156
|
+
if (!existsSync(currentPath))
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
160
|
+
if (!entryMatches(entry))
|
|
161
|
+
continue;
|
|
162
|
+
recurse(join(currentPath, entry.name), segIdx + 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* ignore read errors */ }
|
|
166
|
+
}
|
|
167
|
+
else if (seg.includes('*')) {
|
|
168
|
+
const wildcardIdx = seg.indexOf('*');
|
|
169
|
+
const prefix = seg.slice(0, wildcardIdx);
|
|
170
|
+
const suffix = seg.slice(wildcardIdx + 1);
|
|
171
|
+
if (!existsSync(currentPath))
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
175
|
+
if (!entryMatches(entry))
|
|
176
|
+
continue;
|
|
177
|
+
if (prefix && !entry.name.startsWith(prefix))
|
|
178
|
+
continue;
|
|
179
|
+
if (suffix && !entry.name.endsWith(suffix))
|
|
180
|
+
continue;
|
|
181
|
+
recurse(join(currentPath, entry.name), segIdx + 1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* ignore read errors */ }
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
recurse(join(currentPath, seg), segIdx + 1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
recurse(basePath, 0);
|
|
191
|
+
return targets;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Expand MCP tool glob from backend spec. No hardcoded .cursor/mcps/tools; structure comes from API.
|
|
195
|
+
*/
|
|
196
|
+
function expandGlobFromSpec(spec, home, pathSkipPrefixes = []) {
|
|
197
|
+
const targets = [];
|
|
198
|
+
const basePath = join(home, spec.base_path_under_home.replace(/^~\//, ''));
|
|
199
|
+
if (!existsSync(basePath))
|
|
200
|
+
return targets;
|
|
201
|
+
const skipPrefixes = normalizePathSkipPrefixes(pathSkipPrefixes);
|
|
202
|
+
function matchesGlob(name, pattern) {
|
|
203
|
+
if (pattern === '*')
|
|
204
|
+
return true;
|
|
205
|
+
if (pattern.startsWith('*.')) {
|
|
206
|
+
const ext = pattern.slice(1);
|
|
207
|
+
return name.endsWith(ext);
|
|
208
|
+
}
|
|
209
|
+
return name === pattern;
|
|
210
|
+
}
|
|
211
|
+
function recurse(currentPath, segmentIndex, wildcardValues) {
|
|
212
|
+
if (segmentIndex === spec.segments.length) {
|
|
213
|
+
try {
|
|
214
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
215
|
+
if (entry.isDirectory())
|
|
216
|
+
continue;
|
|
217
|
+
const name = entry.name;
|
|
218
|
+
const full = join(currentPath, name);
|
|
219
|
+
if (!matchesGlob(name, spec.file_glob))
|
|
220
|
+
continue;
|
|
221
|
+
const values = [...wildcardValues, name];
|
|
222
|
+
let logical = spec.logical_path_template;
|
|
223
|
+
for (let i = 0; i < values.length; i++) {
|
|
224
|
+
logical = logical.replace(new RegExp(`\\{\\{${i}\\}\\}`, 'g'), values[i]);
|
|
225
|
+
}
|
|
226
|
+
targets.push({
|
|
227
|
+
path: full,
|
|
228
|
+
file_type: spec.file_type,
|
|
229
|
+
content_format: spec.content_format,
|
|
230
|
+
logicalFilePath: logical,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const seg = spec.segments[segmentIndex];
|
|
238
|
+
if (seg.type === 'literal') {
|
|
239
|
+
const next = join(currentPath, seg.name);
|
|
240
|
+
if (existsSync(next))
|
|
241
|
+
recurse(next, segmentIndex + 1, wildcardValues);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (seg.type === 'dir_wildcard') {
|
|
245
|
+
const skips = seg.skip_prefixes?.length ? normalizePathSkipPrefixes(seg.skip_prefixes) : skipPrefixes;
|
|
246
|
+
try {
|
|
247
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
248
|
+
if (!entry.isDirectory())
|
|
249
|
+
continue;
|
|
250
|
+
if (skips.some((p) => entry.name.startsWith(p)))
|
|
251
|
+
continue;
|
|
252
|
+
recurse(join(currentPath, entry.name), segmentIndex + 1, [...wildcardValues, entry.name]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore */ }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
recurse(basePath, 0, []);
|
|
259
|
+
return targets;
|
|
260
|
+
}
|
|
261
|
+
function pushTargetPaths(targets, p, fileType, isDir, collectStyle, contentFormat, dirGlob) {
|
|
262
|
+
if (isDir) {
|
|
263
|
+
targets.push({ path: p, file_type: fileType, isDirectory: true, dir_glob: dirGlob, collect_style: collectStyle, content_format: contentFormat });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
targets.push({ path: p, file_type: fileType, collect_style: collectStyle, content_format: contentFormat });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resolve a single (path, type, file_type) from the API to concrete collection targets.
|
|
271
|
+
*/
|
|
272
|
+
function resolvePatternToTargets(pathPattern, pathType, fileType, projectRoot, home, pathSkipPrefixes = [], collectStyle, contentFormat, dirGlob, homeRecurseSkipDirs = [], pathResolution, resolveUnderHome = true, absolutePathPrefixes = [], mcpToolGlobSpec, clientPathConstants, pathResolutionSpecs) {
|
|
273
|
+
const norm = pathPattern.replace(/\\/g, '/');
|
|
274
|
+
const targets = [];
|
|
275
|
+
const spec = pathResolution && pathResolutionSpecs?.[pathResolution] ? pathResolutionSpecs[pathResolution] : null;
|
|
276
|
+
if (spec?.type === 'mcp_tool_glob' && mcpToolGlobSpec) {
|
|
277
|
+
return expandGlobFromSpec(mcpToolGlobSpec, home, pathSkipPrefixes);
|
|
278
|
+
}
|
|
279
|
+
if (spec?.type === 'vscdb') {
|
|
280
|
+
if (!clientPathConstants)
|
|
281
|
+
throw new Error(`client_path_constants required to resolve path_resolution="${pathResolution}"`);
|
|
282
|
+
const basePath = getPathByConstantKey(home, clientPathConstants, spec.path_constant_key);
|
|
283
|
+
const sep = spec.fragment_separator;
|
|
284
|
+
const fragment = norm.includes(sep) ? sep + norm.split(sep).slice(1).join(sep) : spec.default_fragment;
|
|
285
|
+
targets.push({ path: basePath + fragment, file_type: fileType, content_format: contentFormat });
|
|
286
|
+
return targets;
|
|
287
|
+
}
|
|
288
|
+
if (spec?.type === 'extensions_cache') {
|
|
289
|
+
if (!clientPathConstants)
|
|
290
|
+
throw new Error(`client_path_constants required to resolve path_resolution="${pathResolution}"`);
|
|
291
|
+
const basePath = getPathByConstantKey(home, clientPathConstants, spec.path_constant_key);
|
|
292
|
+
const suffix = getExtensionsCacheInstalledSuffix(clientPathConstants);
|
|
293
|
+
targets.push({ path: basePath + suffix, file_type: fileType, content_format: contentFormat ?? 'extensions_cache' });
|
|
294
|
+
return targets;
|
|
295
|
+
}
|
|
296
|
+
if (spec?.type === 'env_dir_file') {
|
|
297
|
+
const configuredDir = process.env[spec.env_var]?.trim();
|
|
298
|
+
const baseDir = configuredDir
|
|
299
|
+
? configuredDir.replace(/^~\//, `${home}/`)
|
|
300
|
+
: join(home, spec.fallback_under_home.replace(/^~\//, ''));
|
|
301
|
+
pushTargetPaths(targets, join(baseDir, spec.filename), fileType, false, collectStyle, contentFormat, dirGlob);
|
|
302
|
+
return targets;
|
|
303
|
+
}
|
|
304
|
+
if (norm.includes('**'))
|
|
305
|
+
return expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentFormat, dirGlob, homeRecurseSkipDirs);
|
|
306
|
+
if (norm.includes('*'))
|
|
307
|
+
return expandGlobPathPattern(pathPattern, fileType, home, projectRoot, contentFormat, dirGlob, absolutePathPrefixes);
|
|
308
|
+
const isDir = pathType === 'dir' || norm.endsWith('/');
|
|
309
|
+
const rel = norm.replace(/\/+$/, '');
|
|
310
|
+
if (norm.startsWith('/') && absolutePathPrefixes.some((prefix) => norm.startsWith(prefix))) {
|
|
311
|
+
pushTargetPaths(targets, norm, fileType, isDir, collectStyle, contentFormat, dirGlob);
|
|
312
|
+
return targets;
|
|
313
|
+
}
|
|
314
|
+
if (norm.startsWith('~/')) {
|
|
315
|
+
pushTargetPaths(targets, join(home, rel.slice(2)), fileType, isDir, collectStyle, contentFormat, dirGlob);
|
|
316
|
+
return targets;
|
|
317
|
+
}
|
|
318
|
+
const stripLeadingSlash = rel.startsWith('/') ? rel.slice(1) : rel;
|
|
319
|
+
pushTargetPaths(targets, join(projectRoot, stripLeadingSlash), fileType, isDir, collectStyle, contentFormat, dirGlob);
|
|
320
|
+
if (stripLeadingSlash.startsWith('.') && resolveUnderHome) {
|
|
321
|
+
pushTargetPaths(targets, join(home, stripLeadingSlash), fileType, isDir, collectStyle, contentFormat, dirGlob);
|
|
322
|
+
}
|
|
323
|
+
return targets;
|
|
324
|
+
}
|
|
325
|
+
export { normalizePathSkipPrefixes, resolvePatternToTargets, expandGlobFromSpec };
|
|
326
|
+
export { expandRecursiveGlobPathPattern, expandGlobPathPattern };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { CURSOR_SHADOW_KEYS_NESTED_WINS, coalesceCursorEnabledShadowValue, stripShadowKeysFromReactiveBlobUpload, } from './cursor_shadow_merge_ruleset.js';
|
|
2
|
+
const CURSOR_SHADOW_KEYS_MERGE_ENABLED_OR = new Set([
|
|
3
|
+
'isWebSearchToolEnabled',
|
|
4
|
+
'isWebSearchToolEnabled2',
|
|
5
|
+
]);
|
|
6
|
+
/**
|
|
7
|
+
* Collect include_keys from composer_with_include read steps (backend-driven list).
|
|
8
|
+
*/
|
|
9
|
+
export function shadowKeysFromReadQueries(readQueries) {
|
|
10
|
+
const keys = new Set();
|
|
11
|
+
for (const step of readQueries ?? []) {
|
|
12
|
+
if (step.value_kind === 'composer_with_include' && step.include_keys?.length) {
|
|
13
|
+
for (const k of step.include_keys) {
|
|
14
|
+
keys.add(k);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...keys];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve one shadow key from reactive blob root + nested composerState (matches scan_targets).
|
|
22
|
+
*/
|
|
23
|
+
export function mergeCursorShadowKeyValue(key, root, nested) {
|
|
24
|
+
const rootVal = Object.prototype.hasOwnProperty.call(root, key) ? root[key] : undefined;
|
|
25
|
+
const nestedVal = nested && Object.prototype.hasOwnProperty.call(nested, key) ? nested[key] : undefined;
|
|
26
|
+
if (CURSOR_SHADOW_KEYS_NESTED_WINS.has(key)) {
|
|
27
|
+
if (nestedVal !== undefined)
|
|
28
|
+
return nestedVal;
|
|
29
|
+
if (rootVal !== undefined)
|
|
30
|
+
return rootVal;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (CURSOR_SHADOW_KEYS_MERGE_ENABLED_OR.has(key)) {
|
|
34
|
+
return coalesceCursorEnabledShadowValue(rootVal, nestedVal);
|
|
35
|
+
}
|
|
36
|
+
if (rootVal !== undefined)
|
|
37
|
+
return rootVal;
|
|
38
|
+
return nestedVal;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Merge shadow keys from the reactive blob into nested composerState (canonical for policy).
|
|
42
|
+
* Matches policy_engine.scan_targets; strips duplicate top-level keys from the upload shape.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeShadowKeysIntoComposerState(stateData, shadowKeys) {
|
|
45
|
+
if (!shadowKeys.length)
|
|
46
|
+
return;
|
|
47
|
+
const hasNested = stateData.composerState &&
|
|
48
|
+
typeof stateData.composerState === 'object' &&
|
|
49
|
+
!Array.isArray(stateData.composerState);
|
|
50
|
+
const hasRootKey = shadowKeys.some((k) => Object.prototype.hasOwnProperty.call(stateData, k));
|
|
51
|
+
if (!hasNested && !hasRootKey)
|
|
52
|
+
return;
|
|
53
|
+
let cs = stateData.composerState;
|
|
54
|
+
if (!hasNested) {
|
|
55
|
+
cs = {};
|
|
56
|
+
stateData.composerState = cs;
|
|
57
|
+
}
|
|
58
|
+
const composerState = cs;
|
|
59
|
+
const root = stateData;
|
|
60
|
+
for (const key of shadowKeys) {
|
|
61
|
+
const merged = mergeCursorShadowKeyValue(key, root, composerState);
|
|
62
|
+
if (merged !== undefined) {
|
|
63
|
+
composerState[key] = merged;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
stripShadowKeysFromReactiveBlobUpload(stateData);
|
|
67
|
+
}
|