log-llm-config-staging 1.3.44

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.
Files changed (51) hide show
  1. package/README.md +46 -0
  2. package/dist/apply_deferred_vscdb.js +8 -0
  3. package/dist/bootstrap_constants.js +5 -0
  4. package/dist/cli/bash_script_generator.js +95 -0
  5. package/dist/cli.js +103 -0
  6. package/dist/cli_invocation_match.js +28 -0
  7. package/dist/compliance_check_runner.js +17 -0
  8. package/dist/compliance_prompt_gate.js +197 -0
  9. package/dist/endpoint_client/http_transport.js +88 -0
  10. package/dist/endpoint_client/index.js +3 -0
  11. package/dist/endpoint_client/registry_api.js +41 -0
  12. package/dist/endpoint_client/startup_api.js +43 -0
  13. package/dist/endpoint_client/types.js +4 -0
  14. package/dist/execute_trusted_restarts.js +54 -0
  15. package/dist/log_config_files/auth/auth_flow.js +22 -0
  16. package/dist/log_config_files/auth/auth_key_store.js +14 -0
  17. package/dist/log_config_files/collection/config_collector.js +160 -0
  18. package/dist/log_config_files/collection/directory_collector.js +96 -0
  19. package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
  20. package/dist/log_config_files/collection/file_type_rules.js +47 -0
  21. package/dist/log_config_files/collection/mcp_tool_collector.js +37 -0
  22. package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
  23. package/dist/log_config_files/collection/plugin_collector.js +89 -0
  24. package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
  25. package/dist/log_config_files/index.js +19 -0
  26. package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
  27. package/dist/log_config_files/paths/pattern_resolver.js +227 -0
  28. package/dist/log_config_files/readers/file_readers.js +69 -0
  29. package/dist/log_config_files/readers/vscdb_config_builder.js +146 -0
  30. package/dist/log_config_files/readers/vscdb_reader.js +247 -0
  31. package/dist/log_config_files/runtime/compliance_check.js +518 -0
  32. package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
  33. package/dist/log_config_files/runtime/hook_logger.js +197 -0
  34. package/dist/log_config_files/runtime/main_runner.js +192 -0
  35. package/dist/log_config_files/runtime/management_storage.js +82 -0
  36. package/dist/log_config_files/runtime/remediation_config_path.js +90 -0
  37. package/dist/log_config_files/runtime/remediation_sync.js +1290 -0
  38. package/dist/log_config_files/runtime/sqlite_binary.js +92 -0
  39. package/dist/log_config_files/runtime/trusted_restarts.js +52 -0
  40. package/dist/log_config_files/sender/batch_sender.js +220 -0
  41. package/dist/log_config_files/sender/endpoint_config.js +24 -0
  42. package/dist/log_config_files/sender/signing.js +1 -0
  43. package/dist/log_sensitive_paths_audit.js +97 -0
  44. package/dist/log_uuid/auth_key_store.js +71 -0
  45. package/dist/log_uuid/hardware_uuid.js +35 -0
  46. package/dist/log_uuid/index.js +11 -0
  47. package/dist/log_uuid/log_uuid_helper.js +30 -0
  48. package/dist/log_uuid/startup_sender.js +74 -0
  49. package/dist/log_uuid/user_profile.js +178 -0
  50. package/dist/types/config_file_types.js +1 -0
  51. package/package.json +62 -0
@@ -0,0 +1,43 @@
1
+ import { executeBody } from './http_transport.js';
2
+ export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) => {
3
+ const payload = JSON.stringify(body);
4
+ const { statusCode, statusMessage, body: responseBody } = await executeBody(endpointUrl, 'POST', payload, timeoutMs);
5
+ if (statusCode >= 400) {
6
+ const msg = statusCode === 413
7
+ ? `413 Request Entity Too Large: ${responseBody.substring(0, 200)}`
8
+ : `HTTP ${statusCode} ${statusMessage}: ${responseBody.substring(0, 200)}`;
9
+ throw new Error(msg);
10
+ }
11
+ if (!responseBody)
12
+ return { status: 'error', message: 'Empty response from endpoint' };
13
+ try {
14
+ return JSON.parse(responseBody);
15
+ }
16
+ catch (error) {
17
+ throw new Error(`Failed to parse endpoint response (${error.message}): ${responseBody}`);
18
+ }
19
+ };
20
+ export const patchPayload = async (endpointUrl, body, timeoutMs = 10000) => {
21
+ const payload = JSON.stringify(body);
22
+ const { body: responseBody } = await executeBody(endpointUrl, 'PATCH', payload, timeoutMs);
23
+ if (!responseBody)
24
+ return { status: 'error', error: 'empty_response' };
25
+ try {
26
+ return JSON.parse(responseBody);
27
+ }
28
+ catch {
29
+ throw new Error(`Invalid JSON: ${responseBody.slice(0, 200)}`);
30
+ }
31
+ };
32
+ export const classifyEndpointResponse = (response) => {
33
+ if (response.status === 'key_issued' && response.key) {
34
+ return { branch: 'key_issued', message: 'Server issued a new symmetric key; store it for future signatures.', key: response.key, keyId: response.key_id };
35
+ }
36
+ if (response.status === 'accepted' && response.signature_valid) {
37
+ return { branch: 'accepted', message: 'Startup payload accepted and signature verified.' };
38
+ }
39
+ if (response.status === 'error' && response.error) {
40
+ return { branch: 'error', message: response.error };
41
+ }
42
+ return { branch: 'error', message: response.message || 'Endpoint returned an unexpected response.' };
43
+ };
@@ -0,0 +1,4 @@
1
+ /** Path suffix for file collection patterns (backend: api/file-path-registry/file-patterns/). */
2
+ export const FILE_PATH_REGISTRY_FILE_PATTERNS_PATH = '/api/file-path-registry/file-patterns/';
3
+ /** Path suffix for sensitive paths audit candidates (backend: api/file-path-registry/sensitive-paths-audit-candidates/). */
4
+ export const FILE_PATH_REGISTRY_SENSITIVE_PATHS_PATH = '/api/file-path-registry/sensitive-paths-audit-candidates/';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * CLI: read one JSON line from stdin (same shape as compliance_prompt_gate stdout for __optimus_autofix),
3
+ * parse restart_commands[], allowlist each in TS, spawn detached. Invoked by optimus-compliance-check.sh only.
4
+ * stderr stays quiet; failures logged via hookRunLog.
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { isThisCliModule } from './cli_invocation_match.js';
8
+ import { appendComplianceRunnerLine, hookRunLog } from './log_config_files/runtime/hook_logger.js';
9
+ import { executeTrustedRestartCommands } from './log_config_files/runtime/trusted_restarts.js';
10
+ /** Invoked by dist entrypoint or by `npx log-llm-config@latest execute-trusted-restarts` (cli.js dispatches here). */
11
+ export function runExecuteTrustedRestartsFromStdin() {
12
+ let raw;
13
+ try {
14
+ raw = readFileSync(0, 'utf8');
15
+ }
16
+ catch {
17
+ const msg = 'execute_trusted_restarts: could not read stdin';
18
+ hookRunLog(msg);
19
+ appendComplianceRunnerLine('RESTART', msg);
20
+ return;
21
+ }
22
+ const first = raw.trim().split(/\r?\n/)[0]?.trim() ?? '';
23
+ if (!first) {
24
+ appendComplianceRunnerLine('RESTART', 'empty stdin — no restart_commands');
25
+ return;
26
+ }
27
+ let j;
28
+ try {
29
+ j = JSON.parse(first);
30
+ }
31
+ catch {
32
+ const msg = 'execute_trusted_restarts: stdin is not valid JSON';
33
+ hookRunLog(msg);
34
+ appendComplianceRunnerLine('RESTART', msg);
35
+ return;
36
+ }
37
+ const cmds = j.restart_commands;
38
+ if (!Array.isArray(cmds)) {
39
+ appendComplianceRunnerLine('RESTART', 'JSON missing restart_commands array — nothing to run');
40
+ return;
41
+ }
42
+ const strings = cmds.filter((c) => typeof c === 'string');
43
+ appendComplianceRunnerLine('RESTART', `stdin ok restart_commands=${strings.length}`);
44
+ executeTrustedRestartCommands(strings);
45
+ }
46
+ if (isThisCliModule(process.argv[1], import.meta.url)) {
47
+ try {
48
+ runExecuteTrustedRestartsFromStdin();
49
+ }
50
+ catch (e) {
51
+ hookRunLog(`execute_trusted_restarts: ${e instanceof Error ? e.message : String(e)}`);
52
+ }
53
+ process.exit(0);
54
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'node:path';
2
+ import { postStartupPayload } from '../../endpoint_client/index.js';
3
+ import { ensureAuthentication as ensureTofuAuthentication } from 'optimus-tofu-staging';
4
+ import { loadEndpointBase } from '../sender/endpoint_config.js';
5
+ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
6
+ import { hookRunLog } from '../runtime/hook_logger.js';
7
+ const AUTH_KEY_RELATIVE_PATH = path.join(OPT_AI_SEC_MANAGEMENT_REL, 'auth_key.txt');
8
+ /** Ensure authentication — verify stored key or request new one via handshake. */
9
+ async function ensureAuthentication(hardwareUuid) {
10
+ const key = await ensureTofuAuthentication({
11
+ hardwareUuid,
12
+ endpointBase: loadEndpointBase(),
13
+ authRelativePath: AUTH_KEY_RELATIVE_PATH,
14
+ postJson: (endpointUrl, body) => postStartupPayload(endpointUrl, body),
15
+ onLog: (message) => {
16
+ if (message.startsWith('auth:'))
17
+ hookRunLog(message);
18
+ },
19
+ });
20
+ return key;
21
+ }
22
+ export { ensureAuthentication };
@@ -0,0 +1,14 @@
1
+ import path from 'node:path';
2
+ import { getAuthKeyPath as getSharedAuthKeyPath, readStoredAuthKey as readSharedStoredAuthKey, } from 'optimus-tofu-staging';
3
+ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
+ const AUTH_KEY_RELATIVE_PATH = path.join(OPT_AI_SEC_MANAGEMENT_REL, 'auth_key.txt');
5
+ const AUTH_KEY_OPTIONS = { authRelativePath: AUTH_KEY_RELATIVE_PATH };
6
+ /** Return absolute path to auth_key.txt, or null if home dir is unavailable. */
7
+ function getAuthKeyPath() {
8
+ return getSharedAuthKeyPath(AUTH_KEY_OPTIONS);
9
+ }
10
+ /** Read and parse the stored auth key, or return null if absent/unreadable. */
11
+ function readStoredAuthKey() {
12
+ return readSharedStoredAuthKey(AUTH_KEY_OPTIONS);
13
+ }
14
+ export { getAuthKeyPath, readStoredAuthKey };
@@ -0,0 +1,160 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { readJSONFile, readMCPConfig, readMarkdownFile, readInstalledExtensions } from '../readers/file_readers.js';
4
+ import { getExtensionsCachePath, getExtensionsCacheInstalledSuffix, getVscdbPath } from '../paths/path_constants_helpers.js';
5
+ import { resolvePatternToTargets, normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
6
+ import { enrichRawFromRecipe } from './enrichment_helpers.js';
7
+ import { collectDirectoryEntries, collectDirectoryMetadata } from './directory_collector.js';
8
+ import { collectVscdbEntries } from '../readers/vscdb_config_builder.js';
9
+ import { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
10
+ function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs = [], absolutePathPrefixes = [], mcpToolGlobSpec = null, clientPathConstants = null, pathResolutionSpecs = null) {
11
+ const seenPaths = new Set();
12
+ const targets = [];
13
+ const enrichByFileType = {};
14
+ const metadataOnlyFileTypes = new Set(patterns.filter((p) => p.collect_style === 'metadata').map((p) => p.file_type));
15
+ if (metadataOnlyFileTypes.size === 0)
16
+ metadataOnlyFileTypes.add('claude_log');
17
+ for (const p of patterns) {
18
+ if (p.enrich && !enrichByFileType[p.file_type])
19
+ enrichByFileType[p.file_type] = p.enrich;
20
+ }
21
+ for (const p of patterns) {
22
+ const pathSkipPrefixes = p.path_resolution === 'mcp_tool_glob' ? normalizePathSkipPrefixes(p.path_skip_prefixes) : [];
23
+ for (const t of resolvePatternToTargets(p.path, p.type, p.file_type, projectRoot, home, pathSkipPrefixes, p.collect_style, p.content_format, p.dir_glob, homeRecurseSkipDirs, p.path_resolution, p.resolve_under_home !== false, absolutePathPrefixes, mcpToolGlobSpec, clientPathConstants, pathResolutionSpecs)) {
24
+ if (p.raw_key)
25
+ t.raw_key = p.raw_key;
26
+ if (p.dir_subdir_filename)
27
+ t.dir_subdir_filename = p.dir_subdir_filename;
28
+ if (p.dir_subdir_source)
29
+ t.dir_subdir_source = p.dir_subdir_source;
30
+ const key = `${t.path}\t${t.file_type}`;
31
+ if (!seenPaths.has(key)) {
32
+ seenPaths.add(key);
33
+ targets.push(t);
34
+ }
35
+ }
36
+ }
37
+ return { targets, enrichByFileType, metadataOnlyFileTypes };
38
+ }
39
+ function readContentByFormat(path, format) {
40
+ if (format === 'markdown' || format === 'text')
41
+ return readMarkdownFile(path);
42
+ if (format === 'sqlite_vscdb')
43
+ return null; // handled upstream by collectVscdbEntries
44
+ // json (default) — fall back to markdown for .md files from agents not yet annotated
45
+ return readJSONFile(path) ?? readMCPConfig(path) ?? (path.endsWith('.md') ? readMarkdownFile(path) : null);
46
+ }
47
+ function collectRegularFileEntry(t, enrichByFileType) {
48
+ const format = t.content_format || 'json';
49
+ let content = readContentByFormat(t.path, format);
50
+ if (content === null)
51
+ return null;
52
+ // For JSON-format files, backend/policy engine expect raw_content to be the parsed object (e.g. permissions.allow)
53
+ if (format === 'json' && typeof content === 'string') {
54
+ try {
55
+ content = JSON.parse(content);
56
+ }
57
+ catch {
58
+ // leave as string if parse fails
59
+ }
60
+ }
61
+ let raw;
62
+ if (t.raw_key && typeof content === 'string') {
63
+ raw = { [t.raw_key]: content };
64
+ }
65
+ else {
66
+ raw = typeof content === 'string' ? { content, source: 'file' } : content;
67
+ }
68
+ const recipe = enrichByFileType[t.file_type];
69
+ if (recipe)
70
+ enrichRawFromRecipe(raw, recipe);
71
+ return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: raw };
72
+ }
73
+ function collectMetadataEntry(t) {
74
+ try {
75
+ const stats = statSync(t.path);
76
+ return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: { filename: t.path, last_modified: stats.mtime.toISOString(), source: 'file_metadata' }, collect_style: 'metadata' };
77
+ }
78
+ catch (err) {
79
+ console.warn(`Error stating ${t.path}:`, err instanceof Error ? err.message : String(err));
80
+ return null;
81
+ }
82
+ }
83
+ function handleInstalledExtensions(handledSpecialPaths, configFiles, extensionsCachePath, pathConstants) {
84
+ if (handledSpecialPaths.has(extensionsCachePath))
85
+ return;
86
+ handledSpecialPaths.add(extensionsCachePath);
87
+ const installed = readInstalledExtensions(extensionsCachePath);
88
+ const suffix = getExtensionsCacheInstalledSuffix(pathConstants);
89
+ if (installed.length > 0)
90
+ configFiles.push({ file_type: 'cursor_extensions', file_path: `${extensionsCachePath}${suffix}`, raw_content: { installedExtensions: installed, source: 'extensions.user.cache', extracted_at: new Date().toISOString() } });
91
+ }
92
+ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, options) {
93
+ // Prefer process.env.HOME so hook invoker (e.g. Cursor) can pass real home when it differs from os.homedir()
94
+ const home = (process.env.HOME && process.env.HOME.trim()) || homedir();
95
+ const pathConstants = options?.client_path_constants;
96
+ if (!pathConstants)
97
+ throw new Error('client_path_constants required from API response but not provided');
98
+ const vscdbPath = getVscdbPath(home, pathConstants);
99
+ const extensionsCachePath = getExtensionsCachePath(home, pathConstants);
100
+ const extensionsInstalledSuffix = getExtensionsCacheInstalledSuffix(pathConstants);
101
+ const { targets, enrichByFileType, metadataOnlyFileTypes } = buildCollectionContext(patterns, projectRoot, home, options?.home_recurse_skip_dirs ?? [], options?.absolute_path_prefixes ?? [], options?.mcp_tool_glob_spec ?? null, pathConstants, options?.path_resolution_specs ?? null);
102
+ const configFiles = [];
103
+ const handledSpecialPaths = new Set();
104
+ const loggedFileTypes = new Set();
105
+ const vscdbEntrySpecs = options?.vscdbEntrySpecs;
106
+ const vscdbReadQueries = options?.vscdb_read_queries;
107
+ const vscdbMergeFromComposerState = options?.vscdb_merge_from_composer_state;
108
+ for (const t of targets) {
109
+ if (onProgress && !loggedFileTypes.has(t.file_type)) {
110
+ loggedFileTypes.add(t.file_type);
111
+ onProgress(`scanning file_type=${t.file_type}`);
112
+ }
113
+ if (t.isDirectory) {
114
+ if (metadataOnlyFileTypes.has(t.file_type) || t.collect_style === 'metadata') {
115
+ const entry = collectDirectoryMetadata(t);
116
+ if (entry)
117
+ configFiles.push(entry);
118
+ }
119
+ else {
120
+ configFiles.push(...collectDirectoryEntries(t));
121
+ }
122
+ continue;
123
+ }
124
+ if (extensionsInstalledSuffix && t.path.includes(extensionsInstalledSuffix)) {
125
+ handleInstalledExtensions(handledSpecialPaths, configFiles, extensionsCachePath, pathConstants);
126
+ continue;
127
+ }
128
+ if (t.path === vscdbPath || t.path.startsWith(vscdbPath + '#')) {
129
+ if (!handledSpecialPaths.has(vscdbPath)) {
130
+ handledSpecialPaths.add(vscdbPath);
131
+ configFiles.push(...collectVscdbEntries(vscdbPath, vscdbEntrySpecs, vscdbReadQueries, vscdbMergeFromComposerState));
132
+ }
133
+ continue;
134
+ }
135
+ if (!existsSync(t.path))
136
+ continue;
137
+ if ((metadataOnlyFileTypes.has(t.file_type) || t.collect_style === 'metadata') && !t.isDirectory) {
138
+ const entry = collectMetadataEntry(t);
139
+ if (entry)
140
+ configFiles.push(entry);
141
+ continue;
142
+ }
143
+ const entry = collectRegularFileEntry(t, enrichByFileType);
144
+ if (entry) {
145
+ configFiles.push(entry);
146
+ const recipe = enrichByFileType[t.file_type];
147
+ if (recipe?.derived_files?.length)
148
+ pushDerivedFilesFromRecipe(entry.raw_content, configFiles, recipe);
149
+ }
150
+ }
151
+ return configFiles;
152
+ }
153
+ // ─── Re-exports (barrel for public API) ──────────────────────────────────────
154
+ export { collectConfigFilesFromPatterns };
155
+ export { collectMcpToolFiles } from './mcp_tool_collector.js';
156
+ export { collectConfigFilesFromInstalledPlugins } from './plugin_collector.js';
157
+ export { determineFileTypeFromPath } from './file_type_rules.js';
158
+ export { enrichRawFromRecipe } from './enrichment_helpers.js';
159
+ export { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
160
+ export { getByPath } from './enrichment_helpers.js';
@@ -0,0 +1,96 @@
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
+ /**
54
+ * For metadata-style DIR targets: scan files matching dir_glob, return one entry
55
+ * with the highest mtime found. Falls back to the directory's own mtime if no files match.
56
+ */
57
+ function collectDirectoryMetadata(t) {
58
+ try {
59
+ const dirStat = statSync(t.path);
60
+ let bestMtime = dirStat.mtime;
61
+ let bestPath = t.path;
62
+ const glob = t.dir_glob ?? '*';
63
+ const matchName = (name) => {
64
+ if (glob === '*')
65
+ return true;
66
+ if (glob.startsWith('*.'))
67
+ return name.endsWith(glob.slice(1));
68
+ return name === glob;
69
+ };
70
+ try {
71
+ for (const entry of readdirSync(t.path, { withFileTypes: true })) {
72
+ if (!entry.isFile() || !matchName(entry.name))
73
+ continue;
74
+ try {
75
+ const fileStat = statSync(join(t.path, entry.name));
76
+ if (fileStat.mtime > bestMtime) {
77
+ bestMtime = fileStat.mtime;
78
+ bestPath = join(t.path, entry.name);
79
+ }
80
+ }
81
+ catch { /* ignore unreadable files */ }
82
+ }
83
+ }
84
+ catch { /* ignore unreadable dir */ }
85
+ return {
86
+ file_type: t.file_type,
87
+ file_path: bestPath,
88
+ raw_content: { filename: bestPath, last_modified: bestMtime.toISOString(), source: 'file_metadata' },
89
+ collect_style: 'metadata',
90
+ };
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ 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,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,37 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readJSONFile } from '../readers/file_readers.js';
5
+ import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
6
+ import { getCursorProjectsPath } from '../paths/path_constants_helpers.js';
7
+ function collectMcpToolFiles(pathSkipPrefixes = [], constants) {
8
+ const result = [];
9
+ const skipPrefixes = normalizePathSkipPrefixes(pathSkipPrefixes);
10
+ const cursorProjectsPath = getCursorProjectsPath(homedir(), constants);
11
+ try {
12
+ if (!existsSync(cursorProjectsPath))
13
+ return result;
14
+ for (const projectDir of readdirSync(cursorProjectsPath, { withFileTypes: true }).filter((d) => d.isDirectory())) {
15
+ if (skipPrefixes.some((p) => projectDir.name.startsWith(p)))
16
+ continue;
17
+ const mcpsPath = join(cursorProjectsPath, projectDir.name, 'mcps');
18
+ if (!existsSync(mcpsPath))
19
+ continue;
20
+ for (const serverDir of readdirSync(mcpsPath, { withFileTypes: true }).filter((d) => d.isDirectory())) {
21
+ const toolsPath = join(mcpsPath, serverDir.name, 'tools');
22
+ if (!existsSync(toolsPath))
23
+ continue;
24
+ for (const toolFile of readdirSync(toolsPath).filter((n) => n.endsWith('.json'))) {
25
+ const content = readJSONFile(join(toolsPath, toolFile));
26
+ if (content)
27
+ result.push({ file_type: 'mcp_tool', file_path: `${projectDir.name}/mcps/${serverDir.name}/tools/${toolFile}`, raw_content: content });
28
+ }
29
+ }
30
+ }
31
+ }
32
+ catch (error) {
33
+ console.warn('Error collecting MCP tool files:', error instanceof Error ? error.message : String(error));
34
+ }
35
+ return result;
36
+ }
37
+ export { collectMcpToolFiles };
@@ -0,0 +1,55 @@
1
+ import { join } from 'node:path';
2
+ import { readJSONFile } from '../readers/file_readers.js';
3
+ import { getByPath } from './enrichment_helpers.js';
4
+ function getInstallsEntries(raw, recipe) {
5
+ const installsPath = recipe.installs_path ?? recipe.installs_path_fallback;
6
+ if (!installsPath)
7
+ return [];
8
+ let installs = getByPath(raw, installsPath);
9
+ if (!installs && recipe.installs_path_fallback && recipe.installs_path_fallback !== installsPath) {
10
+ installs = getByPath(raw, recipe.installs_path_fallback);
11
+ }
12
+ if (!installs)
13
+ return [];
14
+ if (Array.isArray(installs)) {
15
+ return installs.filter((e) => typeof e === 'object' && e !== null);
16
+ }
17
+ if (typeof installs === 'object' && installs !== null) {
18
+ return Object.values(installs).filter((e) => typeof e === 'object' && e !== null);
19
+ }
20
+ return [];
21
+ }
22
+ function resolvePathFromTemplate(entry, pathTemplate) {
23
+ const installPath = entry.installPath?.trim() ?? '';
24
+ const sourcePath = entry.sourcePath?.trim() ?? '';
25
+ return pathTemplate
26
+ .replace(/\{\{installPath\}\}/g, installPath)
27
+ .replace(/\{\{sourcePath\}\}/g, sourcePath);
28
+ }
29
+ /**
30
+ * For file types whose enrich recipe has derived_files: from each install entry in raw,
31
+ * resolve path from path_template ({{installPath}}, {{sourcePath}}), read file, push with recipe file_type.
32
+ * Driven by backend FILE_TYPE_ENRICHMENT.derived_files; no openclaw-specific logic here.
33
+ */
34
+ function pushDerivedFilesFromRecipe(raw, configFiles, recipe) {
35
+ const derived = recipe.derived_files;
36
+ if (!derived?.length)
37
+ return;
38
+ const entries = getInstallsEntries(raw, recipe);
39
+ for (const entry of entries) {
40
+ for (const spec of derived) {
41
+ let path = resolvePathFromTemplate(entry, spec.path_template).replace(/\/+/g, '/').trim();
42
+ if (!path && spec.path_template_fallback) {
43
+ path = resolvePathFromTemplate(entry, spec.path_template_fallback).replace(/\/+/g, '/').trim();
44
+ }
45
+ if (!path)
46
+ continue;
47
+ const fullPath = path.startsWith('/') ? path : join(process.cwd(), path);
48
+ const content = readJSONFile(fullPath);
49
+ if (content !== null) {
50
+ configFiles.push({ file_type: spec.file_type, file_path: fullPath, raw_content: content });
51
+ }
52
+ }
53
+ }
54
+ }
55
+ export { pushDerivedFilesFromRecipe };