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.
@@ -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 (fileName && existsSync(filePath))
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
- const targets = [];
58
- const asteriskIndex = norm.indexOf('*');
59
- if (asteriskIndex === -1)
60
- return targets;
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
- const [before, after] = norm.split('*');
63
- const afterNorm = after.replace(/^\/+/, '');
64
- // If `before` doesn't end with '/', the * is mid-segment (e.g. "extensions/saoudrizwan.claude-dev*/" →
65
- // parent="extensions/", namePrefix="saoudrizwan.claude-dev"). Split on last '/' to get the real base dir.
66
- let parentPart = before.replace(/\/+$/, '');
67
- let namePrefix = '';
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 targets;
87
- try {
88
- for (const entry of readdirSync(basePath, { withFileTypes: true })) {
89
- if (!entry.isDirectory())
90
- continue;
91
- if (namePrefix && !entry.name.startsWith(namePrefix))
92
- continue;
93
- const resolvedPath = join(basePath, entry.name, afterNorm);
94
- if (!existsSync(resolvedPath))
95
- continue;
96
- targets.push({ path: resolvedPath, file_type: fileType, isDirectory: isDir, dir_glob: dirGlob, content_format: contentFormat });
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
- catch { /* ignore read errors */ }
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 JSON.parse(readFileSync(filePath, 'utf-8'));
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 JSON.parse(readFileSync(filePath, 'utf-8'));
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