@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,192 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { execSqlite3, resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
|
|
5
|
+
/**
|
|
6
|
+
* Session / version / identity aggregator for Cursor.
|
|
7
|
+
*
|
|
8
|
+
* Cursor stores everything in a VSCode-style SQLite state DB at
|
|
9
|
+
* ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb (macOS).
|
|
10
|
+
* Two tables carry the data we need:
|
|
11
|
+
* ItemTable — key/value store: Cursor version, cursorAuth/* identity rows
|
|
12
|
+
* cursorDiskKV — key/value store: composerData:<conversationId> entries,
|
|
13
|
+
* each with a JSON blob carrying createdAt, modelConfig.modelName
|
|
14
|
+
*
|
|
15
|
+
* We read identity (email, plan, version) from ItemTable and aggregate session counts +
|
|
16
|
+
* model names from composerData entries in cursorDiskKV, bounded to the last 30 days.
|
|
17
|
+
* Token counts are no longer written by Cursor 3.8+ so totals are always 0 — `model_token_split`
|
|
18
|
+
* therefore carries per-model composer COUNTs, not tokens (same convention as Cowork's
|
|
19
|
+
* session-count split), so the Agent Profile can still surface a "Most used model".
|
|
20
|
+
* No message content is read — only model names and timestamps.
|
|
21
|
+
*/
|
|
22
|
+
const TOKEN_USAGE_RECENCY_DAYS = 30;
|
|
23
|
+
const TOKEN_USAGE_FILE_TYPE = 'cursor_token_usage';
|
|
24
|
+
const ROW_SEP = '\x1e';
|
|
25
|
+
const FIELD_SEP = '\x1f';
|
|
26
|
+
/** Platform-aware path to Cursor's global state.vscdb. */
|
|
27
|
+
function vscdbPath(home) {
|
|
28
|
+
if (process.platform === 'darwin') {
|
|
29
|
+
return join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
30
|
+
}
|
|
31
|
+
if (process.platform === 'linux') {
|
|
32
|
+
return join(home, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
33
|
+
}
|
|
34
|
+
// Windows: AppData/Roaming/Cursor/...
|
|
35
|
+
const appdata = process.env['APPDATA'] || join(home, 'AppData', 'Roaming');
|
|
36
|
+
return join(appdata, 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
37
|
+
}
|
|
38
|
+
function querySqlite(sqlite3, dbPath, sql) {
|
|
39
|
+
try {
|
|
40
|
+
return execSqlite3(sqlite3, ['-readonly', '-noheader', dbPath], `.timeout 5000\n${sql}\n`);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function num(value) {
|
|
47
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
48
|
+
return value;
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
const n = parseInt(value, 10);
|
|
51
|
+
return Number.isFinite(n) ? n : 0;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Aggregate Cursor token usage / sessions / version / identity from state.vscdb.
|
|
57
|
+
* Returns a single-element array (one per-machine aggregate) or [] when no usable DB exists.
|
|
58
|
+
*/
|
|
59
|
+
function collectCursorTokenUsage(home = homedir()) {
|
|
60
|
+
const dbPath = vscdbPath(home);
|
|
61
|
+
if (!existsSync(dbPath))
|
|
62
|
+
return [];
|
|
63
|
+
const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
|
|
64
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
65
|
+
if (!sqlite3)
|
|
66
|
+
return [];
|
|
67
|
+
// Identity: version, email, plan, subscription status, sign-up type, display name.
|
|
68
|
+
const identityRaw = querySqlite(sqlite3, dbPath, `SELECT key || char(31) || COALESCE(value, '') || char(30)
|
|
69
|
+
FROM ItemTable
|
|
70
|
+
WHERE key IN (
|
|
71
|
+
'cursor.startupMetrics.lastVersion',
|
|
72
|
+
'cursorAuth/cachedEmail',
|
|
73
|
+
'cursorAuth/stripeMembershipType',
|
|
74
|
+
'cursorAuth/stripeSubscriptionStatus',
|
|
75
|
+
'cursorAuth/cachedSignUpType',
|
|
76
|
+
'cursorAuth/cachedScopedProfile'
|
|
77
|
+
);`);
|
|
78
|
+
let version = '';
|
|
79
|
+
let email = '';
|
|
80
|
+
let plan = '';
|
|
81
|
+
let subscriptionStatus = '';
|
|
82
|
+
let signUpType = '';
|
|
83
|
+
let displayName = '';
|
|
84
|
+
if (identityRaw) {
|
|
85
|
+
for (const row of identityRaw.split(ROW_SEP)) {
|
|
86
|
+
const trimmed = row.trim();
|
|
87
|
+
if (!trimmed)
|
|
88
|
+
continue;
|
|
89
|
+
const sep = trimmed.indexOf(FIELD_SEP);
|
|
90
|
+
if (sep === -1)
|
|
91
|
+
continue;
|
|
92
|
+
const key = trimmed.slice(0, sep);
|
|
93
|
+
const value = trimmed.slice(sep + 1);
|
|
94
|
+
switch (key) {
|
|
95
|
+
case 'cursor.startupMetrics.lastVersion':
|
|
96
|
+
version = value;
|
|
97
|
+
break;
|
|
98
|
+
case 'cursorAuth/cachedEmail':
|
|
99
|
+
email = value;
|
|
100
|
+
break;
|
|
101
|
+
case 'cursorAuth/stripeMembershipType':
|
|
102
|
+
plan = value;
|
|
103
|
+
break;
|
|
104
|
+
case 'cursorAuth/stripeSubscriptionStatus':
|
|
105
|
+
subscriptionStatus = value;
|
|
106
|
+
break;
|
|
107
|
+
case 'cursorAuth/cachedSignUpType':
|
|
108
|
+
signUpType = value;
|
|
109
|
+
break;
|
|
110
|
+
case 'cursorAuth/cachedScopedProfile':
|
|
111
|
+
try {
|
|
112
|
+
const profile = JSON.parse(value);
|
|
113
|
+
if (typeof profile['displayName'] === 'string')
|
|
114
|
+
displayName = profile['displayName'];
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* non-JSON or missing — skip */
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Usage: aggregate session counts and model names from composerData entries in the last 30
|
|
124
|
+
// days. composerData:<conversationId> entries carry a Unix ms timestamp in $.createdAt and
|
|
125
|
+
// the model in $.modelConfig.modelName. Token counts are not written by Cursor 3.8+.
|
|
126
|
+
// cutoffMs is a safe integer — interpolating directly avoids an extra round-trip.
|
|
127
|
+
// key range (not LIKE 'composerData:%') so the query hits the primary-key index on `key`
|
|
128
|
+
// instead of a full-table scan. SQLite's LIKE optimizer only rewrites a prefix match to an
|
|
129
|
+
// index range scan when the comparison is case-sensitive: either PRAGMA case_sensitive_like
|
|
130
|
+
// is ON (with `key`'s default BINARY collation), or the column itself uses NOCASE collation.
|
|
131
|
+
// Neither holds here, so LIKE 'composerData:%' would fall back to a full scan of cursorDiskKV.
|
|
132
|
+
// ';' is the char after ':' in ASCII, so [composerData: , composerData;) is exactly the
|
|
133
|
+
// composerData:* prefix range. Note: this also makes the match case-sensitive (BINARY
|
|
134
|
+
// collation), narrower than the old case-insensitive LIKE — safe because Cursor always
|
|
135
|
+
// writes this key prefix as the exact literal 'composerData:', never a different casing.
|
|
136
|
+
const usageRaw = querySqlite(sqlite3, dbPath, `SELECT
|
|
137
|
+
COALESCE(json_extract(value, '$.modelConfig.modelName'), 'default') || char(31) ||
|
|
138
|
+
COUNT(*) || char(30)
|
|
139
|
+
FROM cursorDiskKV
|
|
140
|
+
WHERE key >= 'composerData:' AND key < 'composerData;'
|
|
141
|
+
AND CAST(json_extract(value, '$.createdAt') AS INTEGER) >= ${cutoffMs}
|
|
142
|
+
GROUP BY json_extract(value, '$.modelConfig.modelName');`);
|
|
143
|
+
const models = new Set();
|
|
144
|
+
let sessionCount = 0;
|
|
145
|
+
// Cursor 3.8+ doesn't write token counts, so there's no real per-model token weighting — the
|
|
146
|
+
// per-model composer COUNT from the query above is the only usage-weight signal available.
|
|
147
|
+
// Mirrors Cowork's session-count convention (see cowork_extract.py): "Most used model" is
|
|
148
|
+
// derived from whichever model has the most composers, not the most tokens.
|
|
149
|
+
const modelComposerCounts = {};
|
|
150
|
+
if (usageRaw) {
|
|
151
|
+
for (const row of usageRaw.split(ROW_SEP)) {
|
|
152
|
+
const trimmed = row.trim();
|
|
153
|
+
if (!trimmed)
|
|
154
|
+
continue;
|
|
155
|
+
const sep = trimmed.indexOf(FIELD_SEP);
|
|
156
|
+
if (sep === -1)
|
|
157
|
+
continue;
|
|
158
|
+
const model = trimmed.slice(0, sep).trim();
|
|
159
|
+
const cnt = num(trimmed.slice(sep + 1));
|
|
160
|
+
sessionCount += cnt;
|
|
161
|
+
if (model) {
|
|
162
|
+
models.add(model);
|
|
163
|
+
modelComposerCounts[model] = (modelComposerCounts[model] ?? 0) + cnt;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (sessionCount === 0 && !email && !version)
|
|
168
|
+
return [];
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
file_type: TOKEN_USAGE_FILE_TYPE,
|
|
172
|
+
file_path: dbPath,
|
|
173
|
+
raw_content: {
|
|
174
|
+
version,
|
|
175
|
+
session_count: sessionCount,
|
|
176
|
+
models: [...models].sort(),
|
|
177
|
+
model_token_split: modelComposerCounts,
|
|
178
|
+
window_days: TOKEN_USAGE_RECENCY_DAYS,
|
|
179
|
+
totals: {
|
|
180
|
+
input_tokens: 0,
|
|
181
|
+
output_tokens: 0,
|
|
182
|
+
},
|
|
183
|
+
email,
|
|
184
|
+
display_name: displayName,
|
|
185
|
+
plan,
|
|
186
|
+
subscription_status: subscriptionStatus,
|
|
187
|
+
auth_method: signUpType ? 'cursor_account' : '',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
export { collectCursorTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE, vscdbPath };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
4
|
+
function collectSubdirMdFiles(dirPath, fileType, mdFilename, source) {
|
|
5
|
+
const results = [];
|
|
6
|
+
try {
|
|
7
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
8
|
+
if (!entry.isDirectory())
|
|
9
|
+
continue;
|
|
10
|
+
const mdPath = join(dirPath, entry.name, mdFilename);
|
|
11
|
+
if (!existsSync(mdPath))
|
|
12
|
+
continue;
|
|
13
|
+
const content = readMarkdownFile(mdPath);
|
|
14
|
+
if (content !== null)
|
|
15
|
+
results.push({ file_type: fileType, file_path: mdPath, raw_content: { content, source } });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.warn(`Error reading ${dirPath}:`, err instanceof Error ? err.message : String(err));
|
|
20
|
+
}
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
23
|
+
function collectDirectoryEntries(t) {
|
|
24
|
+
if (!existsSync(t.path))
|
|
25
|
+
return [];
|
|
26
|
+
const results = [];
|
|
27
|
+
const glob = t.dir_glob || '*.md';
|
|
28
|
+
const matchName = (name) => glob.startsWith('*') ? name.endsWith(glob.slice(1)) : name === glob;
|
|
29
|
+
try {
|
|
30
|
+
const entries = readdirSync(t.path, { withFileTypes: true });
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isFile() || !matchName(entry.name))
|
|
33
|
+
continue;
|
|
34
|
+
const fullPath = join(t.path, entry.name);
|
|
35
|
+
// Prefer parsed JSON for .json files so backend/policy engine see top-level keys (e.g. permissions.allow)
|
|
36
|
+
const content = entry.name.endsWith('.json')
|
|
37
|
+
? readJSONFile(fullPath) ?? readMarkdownFile(fullPath)
|
|
38
|
+
: readMarkdownFile(fullPath) ?? readJSONFile(fullPath);
|
|
39
|
+
if (content !== null) {
|
|
40
|
+
const raw = typeof content === 'string' ? { content, source: 'file' } : content;
|
|
41
|
+
results.push({ file_type: t.file_type, file_path: fullPath, raw_content: raw });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (t.dir_subdir_filename && t.dir_subdir_source) {
|
|
45
|
+
results.push(...collectSubdirMdFiles(t.path, t.file_type, t.dir_subdir_filename, t.dir_subdir_source));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.warn(`Error reading directory ${t.path}:`, err instanceof Error ? err.message : String(err));
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
const METADATA_DIR_SCAN_MAX_DEPTH = 8;
|
|
54
|
+
function matchesDirGlob(name, glob) {
|
|
55
|
+
if (glob === '*')
|
|
56
|
+
return true;
|
|
57
|
+
// **/*.jsonl → match any extension suffix after **/
|
|
58
|
+
if (glob.startsWith('**/')) {
|
|
59
|
+
const suffix = glob.slice(3);
|
|
60
|
+
if (suffix === '*' || suffix === '**')
|
|
61
|
+
return true;
|
|
62
|
+
if (suffix.startsWith('*.'))
|
|
63
|
+
return name.endsWith(suffix.slice(1));
|
|
64
|
+
return name === suffix;
|
|
65
|
+
}
|
|
66
|
+
if (glob.startsWith('*.'))
|
|
67
|
+
return name.endsWith(glob.slice(1));
|
|
68
|
+
return name === glob;
|
|
69
|
+
}
|
|
70
|
+
/** Walk dir tree (bounded depth) and return the file with the highest mtime matching dir_glob. */
|
|
71
|
+
function findNewestMatchingFile(dirPath, glob, depth, maxDepth) {
|
|
72
|
+
let best = null;
|
|
73
|
+
try {
|
|
74
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
75
|
+
const fullPath = join(dirPath, entry.name);
|
|
76
|
+
if (entry.isFile() && matchesDirGlob(entry.name, glob)) {
|
|
77
|
+
try {
|
|
78
|
+
const fileStat = statSync(fullPath);
|
|
79
|
+
if (!best || fileStat.mtime > best.mtime) {
|
|
80
|
+
best = { path: fullPath, mtime: fileStat.mtime };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore unreadable files */ }
|
|
84
|
+
}
|
|
85
|
+
else if (entry.isDirectory() && depth < maxDepth) {
|
|
86
|
+
const nested = findNewestMatchingFile(fullPath, glob, depth + 1, maxDepth);
|
|
87
|
+
if (nested && (!best || nested.mtime > best.mtime)) {
|
|
88
|
+
best = nested;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { /* ignore unreadable dir */ }
|
|
94
|
+
return best;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* For metadata-style DIR targets: recursively scan files matching dir_glob, return one entry
|
|
98
|
+
* with the highest mtime found. Falls back to the directory's own mtime if no files match.
|
|
99
|
+
*/
|
|
100
|
+
function collectDirectoryMetadata(t) {
|
|
101
|
+
try {
|
|
102
|
+
const dirStat = statSync(t.path);
|
|
103
|
+
const glob = t.dir_glob ?? '*';
|
|
104
|
+
// A "|"-delimited dir_glob is an ordered priority list: use the newest match of the FIRST
|
|
105
|
+
// glob that matches anything, so e.g. "**/*.jsonl|**/*" prefers a session transcript
|
|
106
|
+
// (events.jsonl) over any other session-state file — even when a non-jsonl file is newer.
|
|
107
|
+
let newest = null;
|
|
108
|
+
for (const g of glob.split('|')) {
|
|
109
|
+
newest = findNewestMatchingFile(t.path, g, 0, METADATA_DIR_SCAN_MAX_DEPTH);
|
|
110
|
+
if (newest)
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const bestPath = newest?.path ?? t.path;
|
|
114
|
+
const bestMtime = newest?.mtime ?? dirStat.mtime;
|
|
115
|
+
return {
|
|
116
|
+
file_type: t.file_type,
|
|
117
|
+
file_path: bestPath,
|
|
118
|
+
raw_content: { filename: bestPath, last_modified: bestMtime.toISOString(), source: 'file_metadata' },
|
|
119
|
+
collect_style: 'metadata',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export { collectDirectoryEntries, collectDirectoryMetadata };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJSONFile } from '../readers/file_readers.js';
|
|
3
|
+
function getByPath(obj, dotPath) {
|
|
4
|
+
let cur = obj;
|
|
5
|
+
for (const part of dotPath.split('.')) {
|
|
6
|
+
if (cur == null || typeof cur !== 'object')
|
|
7
|
+
return undefined;
|
|
8
|
+
cur = cur[part];
|
|
9
|
+
}
|
|
10
|
+
return cur;
|
|
11
|
+
}
|
|
12
|
+
function resolveEntryVersion(e, vf) {
|
|
13
|
+
const v = e.version;
|
|
14
|
+
if (v !== undefined && v !== null && (typeof v !== 'string' || v.trim() !== ''))
|
|
15
|
+
return;
|
|
16
|
+
const basePath = (e.installPath?.trim() || e.sourcePath?.trim());
|
|
17
|
+
if (!basePath || !vf.version_key)
|
|
18
|
+
return;
|
|
19
|
+
const pkg = readJSONFile(join(basePath, 'package.json'));
|
|
20
|
+
const versionVal = pkg ? pkg[vf.version_key] : undefined;
|
|
21
|
+
if (typeof versionVal === 'string')
|
|
22
|
+
e.version = versionVal.trim();
|
|
23
|
+
}
|
|
24
|
+
function enrichRawFromRecipe(raw, recipe) {
|
|
25
|
+
const installsPath = recipe.installs_path ?? recipe.installs_path_fallback;
|
|
26
|
+
if (!installsPath)
|
|
27
|
+
return;
|
|
28
|
+
let installs = getByPath(raw, installsPath);
|
|
29
|
+
if (!installs && recipe.installs_path_fallback && recipe.installs_path_fallback !== installsPath) {
|
|
30
|
+
installs = getByPath(raw, recipe.installs_path_fallback);
|
|
31
|
+
}
|
|
32
|
+
if (!installs || !recipe.version_from_file?.version_key)
|
|
33
|
+
return;
|
|
34
|
+
const vf = recipe.version_from_file;
|
|
35
|
+
if (recipe.installs_shape === 'array' && Array.isArray(installs)) {
|
|
36
|
+
for (const entry of installs) {
|
|
37
|
+
if (typeof entry === 'object' && entry !== null)
|
|
38
|
+
resolveEntryVersion(entry, vf);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (typeof installs === 'object' && installs !== null) {
|
|
42
|
+
for (const key of Object.keys(installs)) {
|
|
43
|
+
const entry = installs[key];
|
|
44
|
+
if (typeof entry === 'object' && entry !== null) {
|
|
45
|
+
const e = entry;
|
|
46
|
+
if (recipe.installs_shape === 'object' && !('id' in e && e.id != null))
|
|
47
|
+
e.id = key;
|
|
48
|
+
resolveEntryVersion(e, vf);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export { getByPath, enrichRawFromRecipe };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readJSONFile } from '../readers/file_readers.js';
|
|
3
|
+
import { PORTABLE_CURSOR_USER_SETTINGS, cursorUserSettingsAbsolutePaths, } from '../runtime/remediation_config_path.js';
|
|
4
|
+
export function isPortableCursorUserSettingsUploadPath(filePath) {
|
|
5
|
+
const p = filePath.replace(/\\/g, '/').toLowerCase();
|
|
6
|
+
return (p === 'cursor/user/settings.json' ||
|
|
7
|
+
p.endsWith('/cursor/user/settings.json') ||
|
|
8
|
+
p.includes('application support/cursor/user/settings.json'));
|
|
9
|
+
}
|
|
10
|
+
function settingsPayloadHasGlobalIgnoreListKey(raw) {
|
|
11
|
+
if (!raw || typeof raw !== 'object')
|
|
12
|
+
return false;
|
|
13
|
+
const nested = raw.cursor
|
|
14
|
+
?.general?.globalCursorIgnoreList;
|
|
15
|
+
if (Array.isArray(nested))
|
|
16
|
+
return true;
|
|
17
|
+
const flat = raw['cursor.general.globalCursorIgnoreList'];
|
|
18
|
+
return Array.isArray(flat);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sensitive Directories scans need full User/settings.json (globalCursorIgnoreList).
|
|
22
|
+
* Pattern collection can omit the key or send an empty object; merge disk when the batch
|
|
23
|
+
* lacks an explicit globalCursorIgnoreList array (including []).
|
|
24
|
+
*/
|
|
25
|
+
export function ensureCursorUserSettingsSnapshotInBatch(configFiles) {
|
|
26
|
+
const hasIgnoreKeyInBatch = configFiles.some((c) => c.file_type === 'vscode_settings' &&
|
|
27
|
+
isPortableCursorUserSettingsUploadPath(c.file_path) &&
|
|
28
|
+
settingsPayloadHasGlobalIgnoreListKey(c.raw_content));
|
|
29
|
+
if (hasIgnoreKeyInBatch)
|
|
30
|
+
return;
|
|
31
|
+
for (const abs of cursorUserSettingsAbsolutePaths()) {
|
|
32
|
+
if (!existsSync(abs))
|
|
33
|
+
continue;
|
|
34
|
+
const raw = readJSONFile(abs);
|
|
35
|
+
if (!raw || Object.keys(raw).length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const entry = {
|
|
38
|
+
file_type: 'vscode_settings',
|
|
39
|
+
file_path: PORTABLE_CURSOR_USER_SETTINGS,
|
|
40
|
+
raw_content: raw,
|
|
41
|
+
};
|
|
42
|
+
const idx = configFiles.findIndex((c) => c.file_type === 'vscode_settings' &&
|
|
43
|
+
isPortableCursorUserSettingsUploadPath(c.file_path));
|
|
44
|
+
if (idx >= 0)
|
|
45
|
+
configFiles[idx] = entry;
|
|
46
|
+
else
|
|
47
|
+
configFiles.push(entry);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File type detection from path — API-driven only.
|
|
3
|
+
*
|
|
4
|
+
* The backend (file_path_registry) is the single source of truth for path → file_type.
|
|
5
|
+
* This module matches a concrete path against the patterns returned by the file collection API;
|
|
6
|
+
* no hardcoded path rules. When patterns are missing (e.g. API unavailable), returns null.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Determine file_type for a path using API patterns only (longest match wins).
|
|
10
|
+
* Returns null if patterns are missing/empty or no pattern matches.
|
|
11
|
+
*/
|
|
12
|
+
export function determineFileTypeFromPath(filePath, patterns) {
|
|
13
|
+
if (!filePath || !patterns?.length)
|
|
14
|
+
return null;
|
|
15
|
+
const norm = filePath.replace(/\\/g, '/');
|
|
16
|
+
// API patterns are already ordered by descending pattern length (backend).
|
|
17
|
+
for (const p of patterns) {
|
|
18
|
+
const pat = p.path.replace(/\\/g, '/').replace(/^~\/?/, '');
|
|
19
|
+
if (!pat)
|
|
20
|
+
continue;
|
|
21
|
+
if (pat.includes('*')) {
|
|
22
|
+
if (matchGlob(norm, pat))
|
|
23
|
+
return p.file_type;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
if (norm === pat || norm.endsWith('/' + pat) || norm.includes(pat))
|
|
27
|
+
return p.file_type;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** Simple glob: * = one segment, ** = any path. */
|
|
33
|
+
function matchGlob(path, pattern) {
|
|
34
|
+
const re = pattern
|
|
35
|
+
.replace(/\\/g, '/')
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
37
|
+
.replace(/\*\*/g, '\u0001')
|
|
38
|
+
.replace(/\*/g, '[^/]*')
|
|
39
|
+
.replace(/\u0001/g, '.*');
|
|
40
|
+
try {
|
|
41
|
+
const regex = new RegExp(re);
|
|
42
|
+
return regex.test(path);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
/**
|
|
6
|
+
* Grok CLI version, read directly from the installed binary.
|
|
7
|
+
*
|
|
8
|
+
* Grok ships as a standalone native binary (not an npm/pip package, no manifest to read a
|
|
9
|
+
* version out of), installed at ~/.grok/bin/grok and typically symlinked onto PATH. The only
|
|
10
|
+
* way to get its version is to run it: `grok --version` prints e.g. "grok 0.2.114 (0c785038798)".
|
|
11
|
+
*/
|
|
12
|
+
const FILE_TYPE = 'grok_cli_version';
|
|
13
|
+
const VERSION_RE = /^grok\s+(\d+\.\d+\.\d+)(?:\s+\(([0-9a-f]+)\))?/i;
|
|
14
|
+
function grokBinPath(home) {
|
|
15
|
+
return join(home, '.grok', 'bin', 'grok');
|
|
16
|
+
}
|
|
17
|
+
/** `<binPath> --version`, parsed into {version, build, raw}, or null if unavailable/unparsable. */
|
|
18
|
+
function readGrokVersion(binPath) {
|
|
19
|
+
let stdout;
|
|
20
|
+
try {
|
|
21
|
+
stdout = execFileSync(binPath, ['--version'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
}).trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const match = VERSION_RE.exec(stdout);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
return { version: match[1], build: match[2] ?? '', raw: stdout };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Emit the Grok CLI version as a single-element aggregate, or [] when Grok isn't installed
|
|
37
|
+
* or its version can't be determined.
|
|
38
|
+
*/
|
|
39
|
+
function collectGrokCliVersion(home = homedir(), binPath = grokBinPath(home)) {
|
|
40
|
+
if (!existsSync(binPath))
|
|
41
|
+
return [];
|
|
42
|
+
const parsed = readGrokVersion(binPath);
|
|
43
|
+
if (!parsed)
|
|
44
|
+
return [];
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
file_type: FILE_TYPE,
|
|
48
|
+
file_path: binPath,
|
|
49
|
+
raw_content: parsed,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
export { collectGrokCliVersion, FILE_TYPE as GROK_CLI_VERSION_FILE_TYPE };
|