log-llm-config 1.3.97 → 1.4.8
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/apply_deferred_vscdb.js +34 -5
- package/dist/compliance_check_runner.js +28 -1
- package/dist/compliance_prompt_gate.js +32 -12
- package/dist/execute_trusted_restarts.js +5 -0
- package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +104 -1
- package/dist/log_config_files/collection/config_collector.js +62 -3
- package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
- package/dist/log_config_files/collection/cursor_project_mcp_collector.js +47 -18
- package/dist/log_config_files/collection/skills_cli_collector.js +146 -0
- package/dist/log_config_files/paths/pattern_resolver.js +122 -31
- package/dist/log_config_files/readers/file_readers.js +112 -2
- package/dist/log_config_files/runtime/compliance_check.js +17 -0
- package/dist/log_config_files/runtime/compliance_session_log.js +372 -0
- package/dist/log_config_files/runtime/hook_logger.js +51 -3
- package/dist/log_config_files/runtime/hook_type_for_request.js +3 -1
- package/dist/log_config_files/runtime/main_runner.js +20 -3
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +4 -0
- package/dist/log_config_files/runtime/remediation_sync.js +4 -1
- package/dist/log_config_files/runtime/trusted_restarts.js +7 -0
- package/dist/log_config_files/sender/batch_sender.js +3 -1
- package/dist/log_sensitive_paths_audit.js +22 -3
- package/package.json +4 -4
|
@@ -0,0 +1,146 @@
|
|
|
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 normalizeListRows(rows, scope) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const name = (row.name || '').trim();
|
|
82
|
+
const path = (row.path || '').trim();
|
|
83
|
+
if (!name || !path)
|
|
84
|
+
continue;
|
|
85
|
+
const agents = Array.isArray(row.agents)
|
|
86
|
+
? row.agents.map((a) => String(a).trim()).filter(Boolean)
|
|
87
|
+
: [];
|
|
88
|
+
out.push({
|
|
89
|
+
name,
|
|
90
|
+
path,
|
|
91
|
+
scope: (row.scope || scope).trim() || scope,
|
|
92
|
+
agents,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** One-line-per-scope summary for hook_request.log (matches `skills list --json`). */
|
|
98
|
+
export function formatSkillsListScopeForHookLog(scopeLabel, entries) {
|
|
99
|
+
if (entries.length === 0) {
|
|
100
|
+
return `skills_cli list ${scopeLabel}: 0 skill(s)`;
|
|
101
|
+
}
|
|
102
|
+
const detail = entries
|
|
103
|
+
.map((e) => `${e.name}@${e.path} agents=${e.agents.length ? e.agents.join(',') : 'none'}`)
|
|
104
|
+
.join(' | ');
|
|
105
|
+
return `skills_cli list ${scopeLabel}: ${entries.length} skill(s) — ${detail}`;
|
|
106
|
+
}
|
|
107
|
+
function runSkillsListForScope(scopeLabel, args, projectRoot, logLine) {
|
|
108
|
+
try {
|
|
109
|
+
return runSkillsListJson(args, projectRoot);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
logLine(`skills_cli: list ${scopeLabel} --json failed: ${msg}`);
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function collectSkillsCliInstalled(projectRoot, log) {
|
|
118
|
+
const logLine = (message) => {
|
|
119
|
+
log?.(message);
|
|
120
|
+
};
|
|
121
|
+
const packageSpec = npxPackageSpec();
|
|
122
|
+
logLine(`skills_cli: npx ${packageSpec} — list -g --json && list --json (projectRoot=${projectRoot})`);
|
|
123
|
+
const globalRows = runSkillsListForScope('-g', ['-g'], projectRoot, logLine);
|
|
124
|
+
const projectRows = runSkillsListForScope('project', [], projectRoot, logLine);
|
|
125
|
+
const global = normalizeListRows(globalRows, 'global');
|
|
126
|
+
const project = normalizeListRows(projectRows, 'project');
|
|
127
|
+
logLine(formatSkillsListScopeForHookLog('-g', global));
|
|
128
|
+
logLine(formatSkillsListScopeForHookLog('project', project));
|
|
129
|
+
const payload = {
|
|
130
|
+
version: 1,
|
|
131
|
+
skills_cli_version: packageSpec,
|
|
132
|
+
generated_at: new Date().toISOString(),
|
|
133
|
+
global,
|
|
134
|
+
project,
|
|
135
|
+
};
|
|
136
|
+
if (payload.global.length === 0 && payload.project.length === 0) {
|
|
137
|
+
logLine('skills_cli_installed: not uploaded (no global or project skills)');
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
logLine(`skills_cli_installed: upload ${SKILLS_CLI_INSTALLED_PATH} global=${global.length} project=${project.length}`);
|
|
141
|
+
return {
|
|
142
|
+
file_type: SKILLS_CLI_FILE_TYPE,
|
|
143
|
+
file_path: SKILLS_CLI_INSTALLED_PATH,
|
|
144
|
+
raw_content: payload,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -8,6 +8,20 @@ function normalizePathSkipPrefixes(prefixes) {
|
|
|
8
8
|
return [];
|
|
9
9
|
return prefixes.filter((p) => typeof p === 'string' && p.length > 0);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Expands glob patterns containing 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
|
+
*/
|
|
11
25
|
function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentFormat, dirGlob, homeRecurseSkipDirs = []) {
|
|
12
26
|
const norm = pathPattern.replace(/\\/g, '/');
|
|
13
27
|
const doubleStarIndex = norm.indexOf('**');
|
|
@@ -25,7 +39,9 @@ function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentForm
|
|
|
25
39
|
return [];
|
|
26
40
|
}
|
|
27
41
|
const targets = [];
|
|
28
|
-
const parts = after.split('/');
|
|
42
|
+
const parts = after.split('/').filter((s) => s.length > 0);
|
|
43
|
+
if (parts.length === 0)
|
|
44
|
+
return [];
|
|
29
45
|
const dirName = parts[0];
|
|
30
46
|
const fileName = parts.length > 1 ? parts[parts.length - 1] : null;
|
|
31
47
|
const skipSet = homeRecurseSkipDirs.length ? new Set(homeRecurseSkipDirs) : new Set();
|
|
@@ -41,8 +57,21 @@ function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentForm
|
|
|
41
57
|
continue;
|
|
42
58
|
if (entry.name === dirName) {
|
|
43
59
|
const filePath = parts.length === 1 ? full : join(full, ...parts.slice(1));
|
|
44
|
-
if (
|
|
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)) {
|
|
45
73
|
targets.push({ path: filePath, file_type: fileType, content_format: contentFormat });
|
|
74
|
+
}
|
|
46
75
|
}
|
|
47
76
|
walk(full, depth + 1, false);
|
|
48
77
|
}
|
|
@@ -52,26 +81,43 @@ function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentForm
|
|
|
52
81
|
walk(basePath, 0, true);
|
|
53
82
|
return targets;
|
|
54
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
|
+
*/
|
|
55
107
|
function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, contentFormat, dirGlob, absolutePathPrefixes = []) {
|
|
56
108
|
const norm = pathPattern.replace(/\\/g, '/');
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
109
|
+
if (!norm.includes('*'))
|
|
110
|
+
return [];
|
|
111
|
+
// `**` is the recursive-glob sigil handled by expandRecursiveGlobPathPattern.
|
|
112
|
+
if (norm.includes('**'))
|
|
113
|
+
return [];
|
|
61
114
|
const isDir = norm.endsWith('/');
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!before.endsWith('/')) {
|
|
69
|
-
const lastSlash = parentPart.lastIndexOf('/');
|
|
70
|
-
if (lastSlash !== -1) {
|
|
71
|
-
namePrefix = parentPart.slice(lastSlash + 1);
|
|
72
|
-
parentPart = parentPart.slice(0, lastSlash);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
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);
|
|
75
121
|
let basePath;
|
|
76
122
|
if (parentPart.startsWith('~/')) {
|
|
77
123
|
basePath = join(home, parentPart.slice(2));
|
|
@@ -83,20 +129,65 @@ function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, content
|
|
|
83
129
|
basePath = join(projectRoot, parentPart.startsWith('/') ? parentPart.slice(1) : parentPart);
|
|
84
130
|
}
|
|
85
131
|
if (!existsSync(basePath))
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
97
188
|
}
|
|
98
189
|
}
|
|
99
|
-
|
|
190
|
+
recurse(basePath, 0);
|
|
100
191
|
return targets;
|
|
101
192
|
}
|
|
102
193
|
/**
|
|
@@ -1,10 +1,120 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
function stripJsoncComments(input) {
|
|
3
|
+
let out = '';
|
|
4
|
+
let inString = false;
|
|
5
|
+
let quote = '';
|
|
6
|
+
let escaped = false;
|
|
7
|
+
let inLineComment = false;
|
|
8
|
+
let inBlockComment = false;
|
|
9
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
10
|
+
const ch = input[i];
|
|
11
|
+
const next = i + 1 < input.length ? input[i + 1] : '';
|
|
12
|
+
if (inLineComment) {
|
|
13
|
+
if (ch === '\n') {
|
|
14
|
+
inLineComment = false;
|
|
15
|
+
out += ch;
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (inBlockComment) {
|
|
20
|
+
if (ch === '*' && next === '/') {
|
|
21
|
+
inBlockComment = false;
|
|
22
|
+
i += 1;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (inString) {
|
|
27
|
+
out += ch;
|
|
28
|
+
if (escaped) {
|
|
29
|
+
escaped = false;
|
|
30
|
+
}
|
|
31
|
+
else if (ch === '\\') {
|
|
32
|
+
escaped = true;
|
|
33
|
+
}
|
|
34
|
+
else if (ch === quote) {
|
|
35
|
+
inString = false;
|
|
36
|
+
quote = '';
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ch === '"' || ch === "'") {
|
|
41
|
+
inString = true;
|
|
42
|
+
quote = ch;
|
|
43
|
+
out += ch;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '/' && next === '/') {
|
|
47
|
+
inLineComment = true;
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === '/' && next === '*') {
|
|
52
|
+
inBlockComment = true;
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out += ch;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function stripTrailingCommas(input) {
|
|
61
|
+
let out = '';
|
|
62
|
+
let inString = false;
|
|
63
|
+
let quote = '';
|
|
64
|
+
let escaped = false;
|
|
65
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
66
|
+
const ch = input[i];
|
|
67
|
+
if (inString) {
|
|
68
|
+
out += ch;
|
|
69
|
+
if (escaped) {
|
|
70
|
+
escaped = false;
|
|
71
|
+
}
|
|
72
|
+
else if (ch === '\\') {
|
|
73
|
+
escaped = true;
|
|
74
|
+
}
|
|
75
|
+
else if (ch === quote) {
|
|
76
|
+
inString = false;
|
|
77
|
+
quote = '';
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '"' || ch === "'") {
|
|
82
|
+
inString = true;
|
|
83
|
+
quote = ch;
|
|
84
|
+
out += ch;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === ',') {
|
|
88
|
+
let j = i + 1;
|
|
89
|
+
while (j < input.length && /\s/.test(input[j]))
|
|
90
|
+
j += 1;
|
|
91
|
+
if (j < input.length && (input[j] === '}' || input[j] === ']'))
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
out += ch;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function parseJsonWithJsoncFallback(raw) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
try {
|
|
104
|
+
const sanitized = stripTrailingCommas(stripJsoncComments(raw));
|
|
105
|
+
return JSON.parse(sanitized);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
2
112
|
/** Read and parse an MCP config file. Returns null on any error. */
|
|
3
113
|
function readMCPConfig(filePath) {
|
|
4
114
|
try {
|
|
5
115
|
if (!existsSync(filePath))
|
|
6
116
|
return null;
|
|
7
|
-
return
|
|
117
|
+
return parseJsonWithJsoncFallback(readFileSync(filePath, 'utf-8'));
|
|
8
118
|
}
|
|
9
119
|
catch (error) {
|
|
10
120
|
console.error(`Error reading ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
@@ -16,7 +126,7 @@ function readJSONFile(filePath) {
|
|
|
16
126
|
try {
|
|
17
127
|
if (!existsSync(filePath))
|
|
18
128
|
return null;
|
|
19
|
-
return
|
|
129
|
+
return parseJsonWithJsoncFallback(readFileSync(filePath, 'utf-8'));
|
|
20
130
|
}
|
|
21
131
|
catch (error) {
|
|
22
132
|
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
@@ -43,6 +43,8 @@ export function normalizeAgentToken(raw) {
|
|
|
43
43
|
return 'copilot';
|
|
44
44
|
if (s === 'opencode')
|
|
45
45
|
return 'opencode';
|
|
46
|
+
if (s === 'codex')
|
|
47
|
+
return 'codex';
|
|
46
48
|
return '';
|
|
47
49
|
}
|
|
48
50
|
function currentAgentFromEnv() {
|
|
@@ -58,6 +60,8 @@ function currentAgentFromEnv() {
|
|
|
58
60
|
return 'copilot';
|
|
59
61
|
if (hookType === 'opencode')
|
|
60
62
|
return 'opencode';
|
|
63
|
+
if (hookType === 'codex')
|
|
64
|
+
return 'codex';
|
|
61
65
|
return 'claude';
|
|
62
66
|
}
|
|
63
67
|
function targetsCurrentAgent(entry, agent) {
|
|
@@ -519,6 +523,19 @@ export function reportPostRestartVerificationOutcomes(violations) {
|
|
|
519
523
|
});
|
|
520
524
|
return { outcomes, reportPromises };
|
|
521
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Run immediately after a deferred state.vscdb write lands on disk (post-restart, before the
|
|
528
|
+
* user's next prompt) so `pending_post_restart_verify` remediations are confirmed and reported
|
|
529
|
+
* to the server right away — the UI does not need to wait for another gate invocation.
|
|
530
|
+
*/
|
|
531
|
+
export async function runPostApplyVerification(agent = 'cursor') {
|
|
532
|
+
const status = runLocalRemediationComplianceCheck(agent);
|
|
533
|
+
const { outcomes, reportPromises } = reportPostRestartVerificationOutcomes(status.violations);
|
|
534
|
+
if (outcomes.length > 0) {
|
|
535
|
+
await Promise.allSettled(reportPromises);
|
|
536
|
+
}
|
|
537
|
+
return outcomes;
|
|
538
|
+
}
|
|
522
539
|
/**
|
|
523
540
|
* Immediate autofix succeeded (inline recheck OK or Claude stale-recheck tolerance).
|
|
524
541
|
* Clear pending verification locally and report verified so the next prompt does not POST
|