log-llm-config-staging 1.3.96 → 1.3.98

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.
@@ -7,6 +7,7 @@ import { enrichRawFromRecipe } from './enrichment_helpers.js';
7
7
  import { collectDirectoryEntries, collectDirectoryMetadata } from './directory_collector.js';
8
8
  import { collectVscdbEntries } from '../readers/vscdb_config_builder.js';
9
9
  import { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
10
+ import { collapseMetadataByFileType } from './metadata_merge.js';
10
11
  function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs = [], absolutePathPrefixes = [], mcpToolGlobSpec = null, clientPathConstants = null, pathResolutionSpecs = null) {
11
12
  const seenPaths = new Set();
12
13
  const targets = [];
@@ -148,7 +149,7 @@ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, optio
148
149
  pushDerivedFilesFromRecipe(entry.raw_content, configFiles, recipe);
149
150
  }
150
151
  }
151
- return configFiles;
152
+ return collapseMetadataByFileType(configFiles);
152
153
  }
153
154
  // ─── Re-exports (barrel for public API) ──────────────────────────────────────
154
155
  export { collectConfigFilesFromPatterns };
@@ -57,6 +57,8 @@ function matchesDirGlob(name, glob) {
57
57
  // **/*.jsonl → match any extension suffix after **/
58
58
  if (glob.startsWith('**/')) {
59
59
  const suffix = glob.slice(3);
60
+ if (suffix === '*' || suffix === '**')
61
+ return true;
60
62
  if (suffix.startsWith('*.'))
61
63
  return name.endsWith(suffix.slice(1));
62
64
  return name === suffix;
@@ -0,0 +1,43 @@
1
+ /** When several install/log paths exist for one agent, keep a single metadata row (newest mtime). */
2
+ const METADATA_COLLAPSE_BY_FILE_TYPE = new Set(['opencode_presence', 'opencode_log']);
3
+ function metadataLastModifiedMs(entry) {
4
+ const raw = entry.raw_content;
5
+ if (!raw || typeof raw !== 'object')
6
+ return 0;
7
+ const lm = raw.last_modified;
8
+ if (typeof lm !== 'string')
9
+ return 0;
10
+ const ms = Date.parse(lm);
11
+ return Number.isFinite(ms) ? ms : 0;
12
+ }
13
+ /**
14
+ * Collapse multiple metadata uploads of the same file_type to one row: highest last_modified wins.
15
+ */
16
+ function collapseMetadataByFileType(files) {
17
+ const passthrough = [];
18
+ const groups = new Map();
19
+ for (const f of files) {
20
+ if (f.collect_style === 'metadata' && METADATA_COLLAPSE_BY_FILE_TYPE.has(f.file_type)) {
21
+ const list = groups.get(f.file_type) ?? [];
22
+ list.push(f);
23
+ groups.set(f.file_type, list);
24
+ }
25
+ else {
26
+ passthrough.push(f);
27
+ }
28
+ }
29
+ const merged = [...passthrough];
30
+ for (const [, list] of groups) {
31
+ if (list.length === 0)
32
+ continue;
33
+ let best = list[0];
34
+ for (let i = 1; i < list.length; i++) {
35
+ if (metadataLastModifiedMs(list[i]) > metadataLastModifiedMs(best)) {
36
+ best = list[i];
37
+ }
38
+ }
39
+ merged.push(best);
40
+ }
41
+ return merged;
42
+ }
43
+ export { collapseMetadataByFileType, METADATA_COLLAPSE_BY_FILE_TYPE };
@@ -54,24 +54,18 @@ function expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentForm
54
54
  }
55
55
  function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, contentFormat, dirGlob, absolutePathPrefixes = []) {
56
56
  const norm = pathPattern.replace(/\\/g, '/');
57
- const targets = [];
58
- const asteriskIndex = norm.indexOf('*');
59
- if (asteriskIndex === -1)
60
- return targets;
57
+ if (!norm.includes('*'))
58
+ return [];
59
+ // `**` is the recursive-glob sigil handled by expandRecursiveGlobPathPattern.
60
+ if (norm.includes('**'))
61
+ return [];
61
62
  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
- }
63
+ // Resolve the literal prefix that ends before the first wildcard segment.
64
+ const firstStarIndex = norm.indexOf('*');
65
+ const lastSlashBeforeStar = norm.lastIndexOf('/', firstStarIndex);
66
+ if (lastSlashBeforeStar === -1)
67
+ return [];
68
+ const parentPart = norm.slice(0, lastSlashBeforeStar);
75
69
  let basePath;
76
70
  if (parentPart.startsWith('~/')) {
77
71
  basePath = join(home, parentPart.slice(2));
@@ -83,20 +77,61 @@ function expandGlobPathPattern(pathPattern, fileType, home, projectRoot, content
83
77
  basePath = join(projectRoot, parentPart.startsWith('/') ? parentPart.slice(1) : parentPart);
84
78
  }
85
79
  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 });
80
+ return [];
81
+ // Walk remaining segments. Each segment may be a bare wildcard '*' (matches
82
+ // any single directory), a prefix/suffix wildcard like 'foo*' or 'foo*bar',
83
+ // or a literal name. Recursion correctly handles multi-wildcard patterns
84
+ // such as ``*/*/cowork_plugins/marketplaces/*/.claude-plugin/marketplace.json``
85
+ // the prior single-split implementation only expanded the first wildcard
86
+ // and silently pushed a partial directory path as the target, which then
87
+ // surfaced as EISDIR when downstream collectors tried to read it as a file.
88
+ const remaining = norm.slice(lastSlashBeforeStar + 1);
89
+ const segments = remaining.split('/').filter((s) => s !== '');
90
+ const targets = [];
91
+ function recurse(currentPath, segIdx) {
92
+ if (segIdx === segments.length) {
93
+ if (!existsSync(currentPath))
94
+ return;
95
+ targets.push({ path: currentPath, file_type: fileType, isDirectory: isDir, dir_glob: dirGlob, content_format: contentFormat });
96
+ return;
97
+ }
98
+ const seg = segments[segIdx];
99
+ if (seg === '*') {
100
+ if (!existsSync(currentPath))
101
+ return;
102
+ try {
103
+ for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
104
+ if (!entry.isDirectory())
105
+ continue;
106
+ recurse(join(currentPath, entry.name), segIdx + 1);
107
+ }
108
+ }
109
+ catch { /* ignore read errors */ }
110
+ }
111
+ else if (seg.includes('*')) {
112
+ const wildcardIdx = seg.indexOf('*');
113
+ const prefix = seg.slice(0, wildcardIdx);
114
+ const suffix = seg.slice(wildcardIdx + 1);
115
+ if (!existsSync(currentPath))
116
+ return;
117
+ try {
118
+ for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
119
+ if (!entry.isDirectory())
120
+ continue;
121
+ if (prefix && !entry.name.startsWith(prefix))
122
+ continue;
123
+ if (suffix && !entry.name.endsWith(suffix))
124
+ continue;
125
+ recurse(join(currentPath, entry.name), segIdx + 1);
126
+ }
127
+ }
128
+ catch { /* ignore read errors */ }
129
+ }
130
+ else {
131
+ recurse(join(currentPath, seg), segIdx + 1);
97
132
  }
98
133
  }
99
- catch { /* ignore read errors */ }
134
+ recurse(basePath, 0);
100
135
  return targets;
101
136
  }
102
137
  /**
@@ -41,6 +41,8 @@ export function normalizeAgentToken(raw) {
41
41
  return 'cursor';
42
42
  if (s === 'copilot')
43
43
  return 'copilot';
44
+ if (s === 'opencode')
45
+ return 'opencode';
44
46
  return '';
45
47
  }
46
48
  function currentAgentFromEnv() {
@@ -48,12 +50,14 @@ function currentAgentFromEnv() {
48
50
  const override = normalizeAgentToken(process.env.OPTIMUS_AGENT);
49
51
  if (override)
50
52
  return override;
51
- // Backwards-compatible: hook wrappers set OPTIMUS_HOOK_TYPE to cursor|claude|copilot.
53
+ // Backwards-compatible: hook wrappers set OPTIMUS_HOOK_TYPE to cursor|claude|copilot|opencode.
52
54
  const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
53
55
  if (hookType === 'cursor')
54
56
  return 'cursor';
55
57
  if (hookType === 'copilot')
56
58
  return 'copilot';
59
+ if (hookType === 'opencode')
60
+ return 'opencode';
57
61
  return 'claude';
58
62
  }
59
63
  function targetsCurrentAgent(entry, agent) {
@@ -0,0 +1,28 @@
1
+ function normalizeToken(raw) {
2
+ if (!raw?.trim())
3
+ return '';
4
+ const s = raw.trim().toLowerCase().replace(/-/g, '_');
5
+ if (s === 'claude_desktop')
6
+ return 'claude';
7
+ if (s === 'github_copilot')
8
+ return 'copilot';
9
+ if (s === 'cursor' || s === 'claude' || s === 'copilot' || s === 'opencode')
10
+ return s;
11
+ // Legacy hooks set OPTIMUS_AGENT=Cursor (display casing)
12
+ if (raw.trim() === 'Cursor')
13
+ return 'cursor';
14
+ return s;
15
+ }
16
+ /** Resolve hook_request.hook_type from hook wrapper env (OPTIMUS_HOOK_TYPE, then OPTIMUS_AGENT). */
17
+ export function resolveHookTypeFromEnv(env = process.env) {
18
+ const fromHook = normalizeToken(env.OPTIMUS_HOOK_TYPE);
19
+ const fromAgent = normalizeToken(env.OPTIMUS_AGENT);
20
+ const token = fromHook || fromAgent || 'claude';
21
+ if (token === 'cursor')
22
+ return 'cursor';
23
+ if (token === 'copilot')
24
+ return 'copilot';
25
+ if (token === 'opencode')
26
+ return 'opencode';
27
+ return 'claude';
28
+ }
@@ -7,6 +7,7 @@ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
7
7
  import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
8
8
  import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
9
9
  import { hookLogReplace, hookRunLog } from './hook_logger.js';
10
+ import { resolveHookTypeFromEnv } from './hook_type_for_request.js';
10
11
  import { resolveHardwareUuid } from './hardware_uuid.js';
11
12
  import { ensureAuthentication } from '../auth/auth_flow.js';
12
13
  import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
@@ -128,8 +129,7 @@ async function addSensitivePathsAudit(endpointBase, configFiles) {
128
129
  }
129
130
  }
130
131
  async function sendAllConfigFiles(configFiles, worktreeReport, hardwareUuid, authKey) {
131
- const hookTypeRaw = (process.env.OPTIMUS_HOOK_TYPE || 'claude').toLowerCase();
132
- const hookType = hookTypeRaw === 'cursor' ? 'cursor' : hookTypeRaw === 'copilot' ? 'copilot' : 'claude';
132
+ const hookType = resolveHookTypeFromEnv();
133
133
  const manifest = configFiles.map((c) => canonicalCursorUserStateVscdbPath(c.file_path));
134
134
  const workspaceRepo = ensureWorkspaceRepoEnv(manifest);
135
135
  const hookRequestId = await sendHookRequestCreate(hardwareUuid, authKey, hookType, workspaceRepo);
@@ -34,6 +34,8 @@ function currentAgentFromEnv() {
34
34
  return 'cursor';
35
35
  if (override === 'copilot')
36
36
  return 'copilot';
37
+ if (override === 'opencode')
38
+ return 'opencode';
37
39
  if (override === 'claude' || override === 'claude_desktop')
38
40
  return 'claude';
39
41
  const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
@@ -41,6 +43,8 @@ function currentAgentFromEnv() {
41
43
  return 'cursor';
42
44
  if (hookType === 'copilot')
43
45
  return 'copilot';
46
+ if (hookType === 'opencode')
47
+ return 'opencode';
44
48
  return 'claude';
45
49
  }
46
50
  /** Spawn each trusted command detached (same pattern as former compliance_prompt_gate fireRestartCommands). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config-staging",
3
- "version": "1.3.96",
3
+ "version": "1.3.98",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {