marmot-logger 1.0.0

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,93 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const config = require('../core/config');
5
+ const logger = require('../core/logger');
6
+
7
+ module.exports = async function logs(options) {
8
+ const projectConfig = config.loadConfig();
9
+
10
+ if (!projectConfig) {
11
+ console.log(chalk.red('Marmot not initialized. Run: marmot init'));
12
+ process.exit(1);
13
+ }
14
+
15
+ let logFile;
16
+
17
+ if (options.today) {
18
+ logFile = config.getLogFile(projectConfig);
19
+ } else {
20
+ // Find the most recent log file
21
+ const logDir = config.getLogDir(projectConfig);
22
+ if (!fs.existsSync(logDir)) {
23
+ console.log(chalk.yellow('No logs found.'));
24
+ return;
25
+ }
26
+
27
+ const files = fs.readdirSync(logDir)
28
+ .filter(f => f.startsWith('file_events_') && f.endsWith('.log'))
29
+ .sort()
30
+ .reverse();
31
+
32
+ if (files.length === 0) {
33
+ console.log(chalk.yellow('No logs found.'));
34
+ return;
35
+ }
36
+
37
+ logFile = path.join(logDir, files[0]);
38
+ }
39
+
40
+ if (!fs.existsSync(logFile)) {
41
+ console.log(chalk.yellow('No logs found.'));
42
+ return;
43
+ }
44
+
45
+ const last = parseInt(options.last) || 10;
46
+ const entries = logger.readLogs(logFile, { last });
47
+
48
+ console.log(`Showing last ${entries.length} entries from: ${chalk.cyan(logFile)}`);
49
+ console.log('');
50
+
51
+ for (const entry of entries) {
52
+ const time = entry.timestamp ? entry.timestamp.split('T')[1].replace('Z', '') : '??:??:??';
53
+ const event = entry.event || 'unknown';
54
+ const eventPath = entry.path || '';
55
+
56
+ let signedStatus = '';
57
+ if (entry.signed === true) {
58
+ signedStatus = chalk.green(' ✓');
59
+ } else if (entry.signed === false) {
60
+ signedStatus = chalk.red(' ✗');
61
+ }
62
+
63
+ // Color code by event type
64
+ let eventColor = chalk.white;
65
+ if (event.startsWith('claude_hook_')) {
66
+ eventColor = chalk.magenta;
67
+ } else if (event.startsWith('git_')) {
68
+ eventColor = chalk.blue;
69
+ } else if (event === 'terminal') {
70
+ eventColor = chalk.yellow;
71
+ } else if (event === 'created') {
72
+ eventColor = chalk.green;
73
+ } else if (event === 'deleted') {
74
+ eventColor = chalk.red;
75
+ } else if (event === 'modified') {
76
+ eventColor = chalk.cyan;
77
+ } else if (event === 'make_command') {
78
+ eventColor = chalk.gray;
79
+ }
80
+
81
+ let details = eventPath;
82
+ if (entry.command) {
83
+ details = entry.command;
84
+ }
85
+ if (entry.additions !== undefined && entry.deletions !== undefined) {
86
+ details += ` (+${entry.additions}/-${entry.deletions})`;
87
+ }
88
+
89
+ console.log(`${chalk.gray(time)} ${eventColor(event.padEnd(25))} ${details}${signedStatus}`);
90
+ }
91
+
92
+ console.log('');
93
+ };
@@ -0,0 +1,32 @@
1
+ const chalk = require('chalk');
2
+ const config = require('../core/config');
3
+ const fileMonitor = require('../plugins/file-monitor');
4
+
5
+ module.exports = async function monitor() {
6
+ const projectConfig = config.loadConfig();
7
+
8
+ if (!projectConfig) {
9
+ console.log(chalk.red('Marmot not initialized. Run: marmot init'));
10
+ process.exit(1);
11
+ }
12
+
13
+ if (!config.isPluginEnabled(projectConfig, 'file-monitor')) {
14
+ console.log(chalk.yellow('File monitor plugin is not enabled.'));
15
+ console.log('Run:', chalk.cyan('marmot enable file-monitor'));
16
+ process.exit(1);
17
+ }
18
+
19
+ try {
20
+ const changes = await fileMonitor.run(projectConfig);
21
+
22
+ if (changes === 0) {
23
+ // No changes, silent exit for cron usage
24
+ process.exit(0);
25
+ }
26
+
27
+ console.log(chalk.green(`✓ Detected ${changes} change(s)`));
28
+ } catch (err) {
29
+ console.error(chalk.red(`Monitor error: ${err.message}`));
30
+ process.exit(1);
31
+ }
32
+ };
@@ -0,0 +1,72 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const config = require('../core/config');
4
+ const signer = require('../core/signer');
5
+
6
+ module.exports = async function status() {
7
+ const projectDir = process.cwd();
8
+ const projectConfig = config.loadConfig(projectDir);
9
+
10
+ if (!projectConfig) {
11
+ console.log(chalk.red('Marmot not initialized.'));
12
+ console.log('Run:', chalk.cyan('marmot init'));
13
+ return;
14
+ }
15
+
16
+ console.log(chalk.bold('Marmot Status'));
17
+ console.log('─'.repeat(40));
18
+
19
+ // Configuration
20
+ console.log('');
21
+ console.log(chalk.bold('Configuration:'));
22
+ console.log(` Marmot dir: ${chalk.cyan(config.getMarmotDir(projectDir))}`);
23
+ console.log(` Config file: ${chalk.cyan(config.getConfigPath(projectDir))}`);
24
+ console.log(` Snapshot dir: ${chalk.cyan(config.getSnapshotDir(projectConfig, projectDir))}`);
25
+ console.log(` Log directory: ${chalk.cyan(config.getLogDir(projectConfig, projectDir))}`);
26
+ console.log(` Signing URL: ${chalk.cyan(config.getSigningUrl(projectDir))}`);
27
+
28
+ // API Key status
29
+ const apiKey = config.getApiKey(projectDir);
30
+ if (apiKey) {
31
+ console.log(` API Key: ${chalk.green('✓ Set')} (from .env or environment)`);
32
+ } else {
33
+ console.log(` API Key: ${chalk.red('✗ Not set')} (run: marmot login)`);
34
+ }
35
+
36
+ // Signing service health
37
+ console.log('');
38
+ console.log(chalk.bold('Signing Service:'));
39
+ try {
40
+ await signer.healthCheck();
41
+ console.log(` Status: ${chalk.green('✓ Healthy')}`);
42
+ } catch (err) {
43
+ console.log(` Status: ${chalk.red('✗ Unreachable')} - ${err.message}`);
44
+ }
45
+
46
+ // Plugins
47
+ console.log('');
48
+ console.log(chalk.bold('Plugins:'));
49
+ const plugins = ['file-monitor', 'terminal', 'git-hooks', 'makefile', 'claude-hooks'];
50
+
51
+ for (const plugin of plugins) {
52
+ const enabled = config.isPluginEnabled(projectConfig, plugin);
53
+ const statusIcon = enabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
54
+ console.log(` ${plugin.padEnd(15)} ${statusIcon}`);
55
+ }
56
+
57
+ // Recent logs
58
+ console.log('');
59
+ console.log(chalk.bold('Recent Activity:'));
60
+ const logFile = config.getLogFile(projectConfig, projectDir);
61
+ if (fs.existsSync(logFile)) {
62
+ const stats = fs.statSync(logFile);
63
+ const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n').length;
64
+ console.log(` Today's log: ${chalk.cyan(logFile)}`);
65
+ console.log(` Entries: ${lines}`);
66
+ console.log(` Size: ${(stats.size / 1024).toFixed(1)} KB`);
67
+ } else {
68
+ console.log(` ${chalk.gray('No logs yet today')}`);
69
+ }
70
+
71
+ console.log('');
72
+ };
@@ -0,0 +1,81 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const config = require('../core/config');
4
+ const logger = require('../core/logger');
5
+
6
+ module.exports = async function verify(options) {
7
+ const projectConfig = config.loadConfig();
8
+
9
+ if (!projectConfig) {
10
+ console.log(chalk.red('Marmot not initialized. Run: marmot init'));
11
+ process.exit(1);
12
+ }
13
+
14
+ const logFile = options.file || config.getLogFile(projectConfig);
15
+
16
+ if (!fs.existsSync(logFile)) {
17
+ console.log(chalk.red(`Log file not found: ${logFile}`));
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(`Verifying: ${chalk.cyan(logFile)}`);
22
+ console.log('');
23
+
24
+ try {
25
+ const results = await logger.verifyLogs(logFile);
26
+
27
+ let valid = 0;
28
+ let invalid = 0;
29
+ let unsigned = 0;
30
+
31
+ for (const result of results) {
32
+ if (result.verified === true) {
33
+ valid++;
34
+ } else if (result.verified === false) {
35
+ invalid++;
36
+ } else {
37
+ unsigned++;
38
+ }
39
+ }
40
+
41
+ console.log(chalk.bold('Results:'));
42
+ console.log(` ${chalk.green('✓ Valid:')} ${valid}`);
43
+ console.log(` ${chalk.red('✗ Invalid:')} ${invalid}`);
44
+ console.log(` ${chalk.gray('○ Unsigned:')} ${unsigned}`);
45
+ console.log(` Total: ${results.length}`);
46
+
47
+ if (invalid > 0) {
48
+ console.log('');
49
+ console.log(chalk.yellow('Invalid entries:'));
50
+ for (const result of results) {
51
+ if (result.verified === false) {
52
+ console.log(` - ${result.entry.timestamp} ${result.entry.event}: ${result.reason}`);
53
+ }
54
+ }
55
+ }
56
+
57
+ // Update log file with verification results
58
+ if (invalid > 0) {
59
+ console.log('');
60
+ console.log('Updating log file with verification status...');
61
+
62
+ const entries = logger.readLogs(logFile);
63
+ const updatedEntries = entries.map((entry, i) => {
64
+ const result = results[i];
65
+ if (result.verified === true) {
66
+ return { ...entry, signed: true };
67
+ } else if (result.verified === false) {
68
+ return { ...entry, signed: false };
69
+ }
70
+ return entry;
71
+ });
72
+
73
+ fs.writeFileSync(logFile, updatedEntries.map(e => JSON.stringify(e)).join('\n') + '\n');
74
+ console.log(chalk.green('✓ Log file updated.'));
75
+ }
76
+
77
+ } catch (err) {
78
+ console.log(chalk.red(`Verification failed: ${err.message}`));
79
+ process.exit(1);
80
+ }
81
+ };
@@ -0,0 +1,190 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ const CONFIG_FILE = '.marmotrc.json';
6
+ const MARMOT_TMP_BASE = '/tmp/marmot';
7
+
8
+ const DEFAULT_CONFIG = {
9
+ logDir: './marmot-logs',
10
+ bearerToken: 'broken-bearer',
11
+ plugins: {
12
+ 'file-monitor': {
13
+ enabled: false
14
+ },
15
+ 'terminal': {
16
+ enabled: false
17
+ },
18
+ 'git-hooks': {
19
+ enabled: false,
20
+ events: ['commit', 'push', 'checkout', 'merge']
21
+ },
22
+ 'makefile': {
23
+ enabled: false
24
+ },
25
+ 'claude-hooks': {
26
+ enabled: false,
27
+ events: ['PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop', 'UserPromptSubmit', 'SessionStart', 'SessionEnd']
28
+ }
29
+ }
30
+ };
31
+
32
+ function getProjectHash(projectDir) {
33
+ return crypto.createHash('md5').update(path.resolve(projectDir)).digest('hex');
34
+ }
35
+
36
+ function getMarmotDir(projectDir = process.cwd()) {
37
+ const hash = getProjectHash(projectDir);
38
+ return path.join(MARMOT_TMP_BASE, hash);
39
+ }
40
+
41
+ function getConfigPath(projectDir = process.cwd()) {
42
+ return path.join(getMarmotDir(projectDir), CONFIG_FILE);
43
+ }
44
+
45
+ function loadConfig(projectDir = process.cwd()) {
46
+ const configPath = getConfigPath(projectDir);
47
+
48
+ if (!fs.existsSync(configPath)) {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ const content = fs.readFileSync(configPath, 'utf8');
54
+ return JSON.parse(content);
55
+ } catch (err) {
56
+ throw new Error(`Failed to parse ${CONFIG_FILE}: ${err.message}`);
57
+ }
58
+ }
59
+
60
+ function saveConfig(config, projectDir = process.cwd()) {
61
+ const configPath = getConfigPath(projectDir);
62
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
63
+ }
64
+
65
+ function createDefaultConfig(projectDir = process.cwd()) {
66
+ const config = { ...DEFAULT_CONFIG };
67
+ saveConfig(config, projectDir);
68
+ return config;
69
+ }
70
+
71
+ function loadEnvFile(projectDir = process.cwd()) {
72
+ const envPath = path.join(projectDir, '.env');
73
+ const env = {};
74
+
75
+ if (fs.existsSync(envPath)) {
76
+ try {
77
+ const content = fs.readFileSync(envPath, 'utf8');
78
+ for (const line of content.split('\n')) {
79
+ const trimmed = line.trim();
80
+ // Skip comments and empty lines
81
+ if (!trimmed || trimmed.startsWith('#')) continue;
82
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
83
+ if (match) {
84
+ const key = match[1].trim();
85
+ let value = match[2].trim();
86
+ // Remove quotes if present
87
+ if ((value.startsWith('"') && value.endsWith('"')) ||
88
+ (value.startsWith("'") && value.endsWith("'"))) {
89
+ value = value.slice(1, -1);
90
+ }
91
+ env[key] = value;
92
+ }
93
+ }
94
+ } catch (e) {
95
+ // Ignore .env read errors
96
+ }
97
+ }
98
+
99
+ return env;
100
+ }
101
+
102
+ function getSigningUrl(projectDir = process.cwd()) {
103
+ // Priority: env var > .env file > default
104
+ if (process.env.MARMOT_URL) {
105
+ return process.env.MARMOT_URL;
106
+ }
107
+ const envFile = loadEnvFile(projectDir);
108
+ return envFile.MARMOT_URL || 'https://logging.drazou.net';
109
+ }
110
+
111
+ function getApiKey(projectDir = process.cwd()) {
112
+ // Priority: env var > .env file
113
+ if (process.env.MARMOT_API_KEY) {
114
+ return process.env.MARMOT_API_KEY;
115
+ }
116
+ const envFile = loadEnvFile(projectDir);
117
+ return envFile.MARMOT_API_KEY;
118
+ }
119
+
120
+ function getLogDir(config, projectDir = process.cwd()) {
121
+ return path.resolve(projectDir, config.logDir || './logs');
122
+ }
123
+
124
+ function getSnapshotDir(config, projectDir = process.cwd()) {
125
+ return path.join(getMarmotDir(projectDir), 'snapshot');
126
+ }
127
+
128
+ function getLogFile(config, projectDir = process.cwd()) {
129
+ const logDir = getLogDir(config, projectDir);
130
+ const date = new Date().toISOString().split('T')[0];
131
+ return path.join(logDir, `file_events_${date}.log`);
132
+ }
133
+
134
+ function isPluginEnabled(config, pluginName) {
135
+ return config.plugins?.[pluginName]?.enabled || false;
136
+ }
137
+
138
+ function enablePlugin(config, pluginName) {
139
+ if (!config.plugins) {
140
+ config.plugins = {};
141
+ }
142
+ if (!config.plugins[pluginName]) {
143
+ config.plugins[pluginName] = { ...DEFAULT_CONFIG.plugins[pluginName] };
144
+ }
145
+ config.plugins[pluginName].enabled = true;
146
+ return config;
147
+ }
148
+
149
+ function disablePlugin(config, pluginName) {
150
+ if (config.plugins?.[pluginName]) {
151
+ config.plugins[pluginName].enabled = false;
152
+ }
153
+ return config;
154
+ }
155
+
156
+ function getBearerToken(projectDir = process.cwd()) {
157
+ const projectConfig = loadConfig(projectDir);
158
+ return projectConfig?.bearerToken || 'broken-bearer';
159
+ }
160
+
161
+ function setBearerToken(token, projectDir = process.cwd()) {
162
+ const projectConfig = loadConfig(projectDir);
163
+ if (projectConfig) {
164
+ projectConfig.bearerToken = token;
165
+ saveConfig(projectConfig, projectDir);
166
+ }
167
+ }
168
+
169
+ module.exports = {
170
+ CONFIG_FILE,
171
+ DEFAULT_CONFIG,
172
+ MARMOT_TMP_BASE,
173
+ getProjectHash,
174
+ getMarmotDir,
175
+ loadConfig,
176
+ saveConfig,
177
+ createDefaultConfig,
178
+ getConfigPath,
179
+ getSigningUrl,
180
+ getApiKey,
181
+ loadEnvFile,
182
+ getLogDir,
183
+ getSnapshotDir,
184
+ getLogFile,
185
+ isPluginEnabled,
186
+ enablePlugin,
187
+ disablePlugin,
188
+ getBearerToken,
189
+ setBearerToken
190
+ };
@@ -0,0 +1,109 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Parse .gitignore file and return patterns
6
+ * Skips comments, empty lines, and negation patterns (!)
7
+ */
8
+ function parseGitignore(projectDir) {
9
+ const gitignorePath = path.join(projectDir, '.gitignore');
10
+
11
+ if (!fs.existsSync(gitignorePath)) {
12
+ return [];
13
+ }
14
+
15
+ try {
16
+ const content = fs.readFileSync(gitignorePath, 'utf8');
17
+ return content
18
+ .split('\n')
19
+ .map(line => line.trim())
20
+ .filter(line => line && !line.startsWith('#') && !line.startsWith('!'));
21
+ } catch (err) {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Get default exclusions (always exclude regardless of .gitignore)
28
+ */
29
+ function getDefaultExclusions() {
30
+ return [
31
+ '.git',
32
+ 'logs/'
33
+ ];
34
+ }
35
+
36
+ /**
37
+ * Build rsync exclude arguments from .gitignore + defaults
38
+ */
39
+ function buildRsyncExcludes(projectDir) {
40
+ const defaults = getDefaultExclusions();
41
+ const gitignorePatterns = parseGitignore(projectDir);
42
+
43
+ // Combine: defaults first, then gitignore patterns
44
+ const allPatterns = [...defaults, ...gitignorePatterns];
45
+
46
+ return allPatterns.map(pattern => `--exclude=${pattern}`);
47
+ }
48
+
49
+ /**
50
+ * Check if a file path should be excluded based on gitignore patterns
51
+ * Used for filtering git diff output
52
+ */
53
+ function shouldExclude(filePath, projectDir) {
54
+ const defaults = getDefaultExclusions();
55
+ const gitignorePatterns = parseGitignore(projectDir);
56
+ const allPatterns = [...defaults, ...gitignorePatterns];
57
+
58
+ // Normalize path separators
59
+ const normalizedPath = filePath.replace(/\\/g, '/');
60
+
61
+ for (const pattern of allPatterns) {
62
+ if (matchPattern(normalizedPath, pattern)) {
63
+ return true;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * Simple pattern matching for gitignore-style patterns
71
+ * Supports: wildcards (*), directory markers (/), character classes ([])
72
+ */
73
+ function matchPattern(filePath, pattern) {
74
+ // Handle trailing slash (directory pattern)
75
+ const isDir = pattern.endsWith('/');
76
+ let cleanPattern = isDir ? pattern.slice(0, -1) : pattern;
77
+
78
+ // Check if pattern has path separators (anchored pattern)
79
+ const isAnchored = cleanPattern.includes('/');
80
+
81
+ // Convert gitignore pattern to regex
82
+ let regex = cleanPattern
83
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except * and ?
84
+ .replace(/\*\*/g, '<<<GLOBSTAR>>>') // Preserve **
85
+ .replace(/\*/g, '[^/]*') // * matches anything except /
86
+ .replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
87
+ .replace(/\?/g, '[^/]'); // ? matches single char except /
88
+
89
+ // Patterns without / match anywhere in path
90
+ if (!isAnchored) {
91
+ regex = `(^|/)${regex}($|/)`;
92
+ } else {
93
+ regex = `^${regex}`;
94
+ }
95
+
96
+ try {
97
+ return new RegExp(regex).test(filePath);
98
+ } catch (e) {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ module.exports = {
104
+ parseGitignore,
105
+ getDefaultExclusions,
106
+ buildRsyncExcludes,
107
+ shouldExclude,
108
+ matchPattern
109
+ };
@@ -0,0 +1,120 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const config = require('./config');
4
+ const signer = require('./signer');
5
+
6
+ async function log(entry, projectConfig, projectDir = process.cwd()) {
7
+ const logFile = config.getLogFile(projectConfig, projectDir);
8
+ const logDir = path.dirname(logFile);
9
+
10
+ // Ensure log directory exists
11
+ if (!fs.existsSync(logDir)) {
12
+ fs.mkdirSync(logDir, { recursive: true });
13
+ }
14
+
15
+ let signedEntry;
16
+
17
+ try {
18
+ // Try to sign the entry with cached token
19
+ signedEntry = await signer.sign(entry, projectDir);
20
+ } catch (err) {
21
+ // If unauthorized, refresh token and retry once
22
+ if (err.statusCode === 401) {
23
+ try {
24
+ await signer.refreshToken(projectDir);
25
+ signedEntry = await signer.sign(entry, projectDir);
26
+ } catch (retryErr) {
27
+ // Second attempt failed, fall back to unsigned
28
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
29
+ signedEntry = {
30
+ timestamp,
31
+ ...entry,
32
+ signed: false
33
+ };
34
+ }
35
+ } else {
36
+ // Other error, fall back to unsigned
37
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
38
+ signedEntry = {
39
+ timestamp,
40
+ ...entry,
41
+ signed: false
42
+ };
43
+ }
44
+ }
45
+
46
+ // Append to log file
47
+ fs.appendFileSync(logFile, JSON.stringify(signedEntry) + '\n');
48
+
49
+ return signedEntry;
50
+ }
51
+
52
+ async function logEvent(eventType, eventPath, size = 0, extra = {}, projectDir = process.cwd()) {
53
+ const projectConfig = config.loadConfig(projectDir);
54
+ if (!projectConfig) {
55
+ throw new Error('Marmot not initialized. Run: marmot init');
56
+ }
57
+
58
+ const entry = {
59
+ event: eventType,
60
+ path: eventPath,
61
+ size,
62
+ ...extra
63
+ };
64
+
65
+ return log(entry, projectConfig, projectDir);
66
+ }
67
+
68
+ function readLogs(logFile, options = {}) {
69
+ if (!fs.existsSync(logFile)) {
70
+ return [];
71
+ }
72
+
73
+ const content = fs.readFileSync(logFile, 'utf8');
74
+ const lines = content.trim().split('\n').filter(line => line.trim());
75
+
76
+ let entries = lines.map(line => {
77
+ try {
78
+ return JSON.parse(line);
79
+ } catch (e) {
80
+ return null;
81
+ }
82
+ }).filter(e => e !== null);
83
+
84
+ if (options.last) {
85
+ entries = entries.slice(-options.last);
86
+ }
87
+
88
+ return entries;
89
+ }
90
+
91
+ async function verifyLogs(logFile, projectDir = process.cwd()) {
92
+ const entries = readLogs(logFile);
93
+ const results = [];
94
+
95
+ for (const entry of entries) {
96
+ if (!entry.uuid) {
97
+ // Entry without UUID cannot be verified
98
+ results.push({ entry, verified: null, reason: 'no uuid' });
99
+ continue;
100
+ }
101
+
102
+ try {
103
+ // Create entry with signed:true for verification
104
+ const verifyEntry = { ...entry, signed: true };
105
+ const result = await signer.verify(verifyEntry, projectDir);
106
+ results.push({ entry, verified: result.valid, reason: result.reason });
107
+ } catch (err) {
108
+ results.push({ entry, verified: false, reason: err.message });
109
+ }
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ module.exports = {
116
+ log,
117
+ logEvent,
118
+ readLogs,
119
+ verifyLogs
120
+ };