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,136 @@
1
+ const https = require('https');
2
+ const http = require('http');
3
+ const { URL } = require('url');
4
+ const config = require('./config');
5
+
6
+ function request(url, options, data) {
7
+ return new Promise((resolve, reject) => {
8
+ const parsedUrl = new URL(url);
9
+ const protocol = parsedUrl.protocol === 'https:' ? https : http;
10
+
11
+ const req = protocol.request(url, options, (res) => {
12
+ let body = '';
13
+ res.on('data', (chunk) => body += chunk);
14
+ res.on('end', () => {
15
+ // Check for 401 Unauthorized
16
+ if (res.statusCode === 401) {
17
+ const err = new Error('Unauthorized');
18
+ err.statusCode = 401;
19
+ reject(err);
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const json = JSON.parse(body);
25
+ if (res.statusCode >= 200 && res.statusCode < 300) {
26
+ resolve(json);
27
+ } else {
28
+ const err = new Error(json.detail || json.message || `HTTP ${res.statusCode}`);
29
+ err.statusCode = res.statusCode;
30
+ reject(err);
31
+ }
32
+ } catch (e) {
33
+ reject(new Error(`Invalid JSON response: ${body}`));
34
+ }
35
+ });
36
+ });
37
+
38
+ req.on('error', reject);
39
+ req.setTimeout(10000, () => {
40
+ req.destroy();
41
+ reject(new Error('Request timeout'));
42
+ });
43
+
44
+ if (data) {
45
+ req.write(JSON.stringify(data));
46
+ }
47
+ req.end();
48
+ });
49
+ }
50
+
51
+ function getCachedToken(projectDir = process.cwd()) {
52
+ return config.getBearerToken(projectDir);
53
+ }
54
+
55
+ async function refreshToken(projectDir = process.cwd()) {
56
+ const apiKey = config.getApiKey(projectDir);
57
+ if (!apiKey) {
58
+ throw new Error('MARMOT_API_KEY not found in .env or environment');
59
+ }
60
+
61
+ const signingUrl = config.getSigningUrl(projectDir);
62
+ const tokenUrl = `${signingUrl}/token`;
63
+
64
+ const result = await request(tokenUrl, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json'
68
+ }
69
+ }, { api_key: apiKey });
70
+
71
+ // Store token in config file
72
+ config.setBearerToken(result.token, projectDir);
73
+
74
+ return result.token;
75
+ }
76
+
77
+ async function getToken(projectDir = process.cwd()) {
78
+ // First try cached token from config
79
+ const cached = getCachedToken(projectDir);
80
+ if (cached && cached !== 'broken-bearer') {
81
+ return cached;
82
+ }
83
+
84
+ // Otherwise refresh
85
+ return refreshToken(projectDir);
86
+ }
87
+
88
+ async function sign(entry, projectDir = process.cwd()) {
89
+ const token = await getToken(projectDir);
90
+ const signingUrl = config.getSigningUrl(projectDir);
91
+ const signUrl = `${signingUrl}/sign`;
92
+
93
+ return await request(signUrl, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ 'Authorization': `Bearer ${token}`
98
+ }
99
+ }, entry);
100
+ }
101
+
102
+ async function verify(entry, projectDir = process.cwd()) {
103
+ const token = await getToken(projectDir);
104
+ const signingUrl = config.getSigningUrl(projectDir);
105
+ const verifyUrl = `${signingUrl}/verify`;
106
+
107
+ return await request(verifyUrl, {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ 'Authorization': `Bearer ${token}`
112
+ }
113
+ }, entry);
114
+ }
115
+
116
+ async function healthCheck(projectDir = process.cwd()) {
117
+ const signingUrl = config.getSigningUrl(projectDir);
118
+ const healthUrl = `${signingUrl}/health`;
119
+
120
+ return await request(healthUrl, {
121
+ method: 'GET'
122
+ });
123
+ }
124
+
125
+ function clearTokenCache(projectDir = process.cwd()) {
126
+ config.setBearerToken('broken-bearer', projectDir);
127
+ }
128
+
129
+ module.exports = {
130
+ getToken,
131
+ refreshToken,
132
+ sign,
133
+ verify,
134
+ healthCheck,
135
+ clearTokenCache
136
+ };
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ const config = require('./core/config');
2
+ const logger = require('./core/logger');
3
+ const signer = require('./core/signer');
4
+
5
+ module.exports = {
6
+ // Configuration
7
+ loadConfig: config.loadConfig,
8
+ saveConfig: config.saveConfig,
9
+ createDefaultConfig: config.createDefaultConfig,
10
+
11
+ // Logging
12
+ log: logger.logEvent,
13
+ readLogs: logger.readLogs,
14
+ verifyLogs: logger.verifyLogs,
15
+
16
+ // Signing
17
+ sign: signer.sign,
18
+ verify: signer.verify,
19
+ healthCheck: signer.healthCheck
20
+ };
@@ -0,0 +1,134 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ const CLAUDE_SETTINGS_DIR = '.claude';
6
+ const CLAUDE_SETTINGS_FILE = 'settings.local.json';
7
+
8
+ function getClaudeSettingsPath(projectDir = process.cwd()) {
9
+ return path.join(projectDir, CLAUDE_SETTINGS_DIR, CLAUDE_SETTINGS_FILE);
10
+ }
11
+
12
+ function loadClaudeSettings(projectDir = process.cwd()) {
13
+ const settingsPath = getClaudeSettingsPath(projectDir);
14
+
15
+ if (!fs.existsSync(settingsPath)) {
16
+ return {};
17
+ }
18
+
19
+ try {
20
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
21
+ } catch (e) {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ function saveClaudeSettings(settings, projectDir = process.cwd()) {
27
+ const settingsDir = path.join(projectDir, CLAUDE_SETTINGS_DIR);
28
+ const settingsPath = getClaudeSettingsPath(projectDir);
29
+
30
+ if (!fs.existsSync(settingsDir)) {
31
+ fs.mkdirSync(settingsDir, { recursive: true });
32
+ }
33
+
34
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
35
+ }
36
+
37
+ function createHookConfig(hookType, projectDir) {
38
+ return {
39
+ matcher: '*',
40
+ hooks: [
41
+ {
42
+ type: 'command',
43
+ command: `cd "${projectDir}" && marmot log claude-${hookType}`
44
+ }
45
+ ]
46
+ };
47
+ }
48
+
49
+ async function enable(projectConfig) {
50
+ const projectDir = process.cwd();
51
+ const events = projectConfig.plugins?.['claude-hooks']?.events || [
52
+ 'PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop',
53
+ 'UserPromptSubmit', 'SessionStart', 'SessionEnd'
54
+ ];
55
+
56
+ const settings = loadClaudeSettings(projectDir);
57
+
58
+ // Initialize hooks object if needed
59
+ if (!settings.hooks) {
60
+ settings.hooks = {};
61
+ }
62
+
63
+ const installed = [];
64
+
65
+ for (const event of events) {
66
+ // Check if hook already exists
67
+ if (settings.hooks[event]) {
68
+ const existingHooks = settings.hooks[event];
69
+ const hasMarmot = existingHooks.some(h =>
70
+ h.hooks?.some(hh => hh.command?.includes('marmot'))
71
+ );
72
+ if (hasMarmot) continue;
73
+ }
74
+
75
+ if (!settings.hooks[event]) {
76
+ settings.hooks[event] = [];
77
+ }
78
+
79
+ settings.hooks[event].push(createHookConfig(event, projectDir));
80
+ installed.push(event);
81
+ }
82
+
83
+ saveClaudeSettings(settings, projectDir);
84
+
85
+ console.log('');
86
+ console.log(chalk.bold('Claude Hooks Plugin Setup:'));
87
+ console.log(` Settings file: ${chalk.cyan(getClaudeSettingsPath(projectDir))}`);
88
+
89
+ if (installed.length > 0) {
90
+ console.log(` Installed hooks: ${installed.join(', ')}`);
91
+ } else {
92
+ console.log(chalk.yellow(' All hooks were already installed.'));
93
+ }
94
+
95
+ console.log('');
96
+ console.log('Claude Code will now log activity when working in this project.');
97
+ }
98
+
99
+ async function disable(projectConfig) {
100
+ const projectDir = process.cwd();
101
+ const settings = loadClaudeSettings(projectDir);
102
+
103
+ if (!settings.hooks) {
104
+ return;
105
+ }
106
+
107
+ // Remove marmot hooks
108
+ for (const event of Object.keys(settings.hooks)) {
109
+ settings.hooks[event] = settings.hooks[event].filter(h =>
110
+ !h.hooks?.some(hh => hh.command?.includes('marmot'))
111
+ );
112
+
113
+ // Clean up empty arrays
114
+ if (settings.hooks[event].length === 0) {
115
+ delete settings.hooks[event];
116
+ }
117
+ }
118
+
119
+ // Clean up empty hooks object
120
+ if (Object.keys(settings.hooks).length === 0) {
121
+ delete settings.hooks;
122
+ }
123
+
124
+ saveClaudeSettings(settings, projectDir);
125
+
126
+ console.log('');
127
+ console.log(chalk.bold('Claude Hooks Plugin Disabled:'));
128
+ console.log('Marmot hooks removed from Claude settings.');
129
+ }
130
+
131
+ module.exports = {
132
+ enable,
133
+ disable
134
+ };
@@ -0,0 +1,233 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync, spawnSync } = require('child_process');
4
+ const chalk = require('chalk');
5
+ const config = require('../core/config');
6
+ const logger = require('../core/logger');
7
+ const gitignore = require('../core/gitignore');
8
+
9
+ const MARMOT_CRON_MARKER = '# marmot-monitor';
10
+
11
+ function getCrontab() {
12
+ try {
13
+ const result = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
14
+ return result;
15
+ } catch (err) {
16
+ return '';
17
+ }
18
+ }
19
+
20
+ function setCrontab(content) {
21
+ try {
22
+ execSync(`echo "${content.replace(/"/g, '\\"')}" | crontab -`, { encoding: 'utf8' });
23
+ return true;
24
+ } catch (err) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ async function run(projectConfig, projectDir = process.cwd()) {
30
+ const snapshotDir = config.getSnapshotDir(projectConfig, projectDir);
31
+ const logDir = config.getLogDir(projectConfig, projectDir);
32
+
33
+ // Ensure log directory exists
34
+ if (!fs.existsSync(logDir)) {
35
+ fs.mkdirSync(logDir, { recursive: true });
36
+ }
37
+
38
+ // Ensure snapshot parent directory exists
39
+ const snapshotParent = path.dirname(snapshotDir);
40
+ if (!fs.existsSync(snapshotParent)) {
41
+ fs.mkdirSync(snapshotParent, { recursive: true });
42
+ }
43
+
44
+ // Build rsync exclude args from .gitignore
45
+ const excludeArgs = gitignore.buildRsyncExcludes(projectDir);
46
+
47
+ // First run: create snapshot
48
+ if (!fs.existsSync(snapshotDir)) {
49
+ try {
50
+ execSync(`rsync -a ${excludeArgs.join(' ')} "${projectDir}/" "${snapshotDir}/"`, {
51
+ stdio: 'pipe'
52
+ });
53
+ await logger.logEvent('monitor_initialized', projectDir, 0, {}, projectDir);
54
+ return 1;
55
+ } catch (err) {
56
+ throw new Error(`Failed to create snapshot: ${err.message}`);
57
+ }
58
+ }
59
+
60
+ // Run git diff to detect changes
61
+ let diffOutput = '';
62
+ try {
63
+ const result = spawnSync('git', ['diff', '--no-index', '--name-status', snapshotDir, projectDir], {
64
+ encoding: 'utf8',
65
+ maxBuffer: 10 * 1024 * 1024
66
+ });
67
+ diffOutput = result.stdout || '';
68
+ } catch (err) {
69
+ // git diff returns exit code 1 when there are differences
70
+ }
71
+
72
+ if (!diffOutput.trim()) {
73
+ return 0; // No changes
74
+ }
75
+
76
+ let changesFound = 0;
77
+ const lines = diffOutput.trim().split('\n');
78
+
79
+ for (const line of lines) {
80
+ if (!line.trim()) continue;
81
+
82
+ const [status, ...pathParts] = line.split('\t');
83
+ const rawPath = pathParts.join('\t');
84
+
85
+ // Extract relative path
86
+ let relativePath = rawPath
87
+ .replace(snapshotDir + '/', '')
88
+ .replace(snapshotDir + '\\', '')
89
+ .replace(projectDir + '/', '')
90
+ .replace(projectDir + '\\', '');
91
+
92
+ const absPath = path.join(projectDir, relativePath);
93
+
94
+ // Skip excluded paths using gitignore patterns
95
+ if (gitignore.shouldExclude(relativePath, projectDir)) continue;
96
+
97
+ changesFound++;
98
+
99
+ switch (status) {
100
+ case 'A': // Added (created in current)
101
+ if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
102
+ const size = fs.statSync(absPath).size;
103
+ await logger.logEvent('created', absPath, size, {}, projectDir);
104
+ }
105
+ break;
106
+
107
+ case 'D': // Deleted
108
+ await logger.logEvent('deleted', absPath, 0, {}, projectDir);
109
+ break;
110
+
111
+ case 'M': // Modified
112
+ if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
113
+ const size = fs.statSync(absPath).size;
114
+ const snapshotFile = path.join(snapshotDir, relativePath);
115
+
116
+ let additions = 0;
117
+ let deletions = 0;
118
+
119
+ // Get additions/deletions using git diff --numstat
120
+ if (fs.existsSync(snapshotFile)) {
121
+ try {
122
+ const numstat = spawnSync('git', ['diff', '--no-index', '--numstat', snapshotFile, absPath], {
123
+ encoding: 'utf8'
124
+ });
125
+ const statLine = (numstat.stdout || '').trim().split('\n')[0];
126
+ if (statLine) {
127
+ const [add, del] = statLine.split('\t');
128
+ additions = add === '-' ? 0 : parseInt(add) || 0;
129
+ deletions = del === '-' ? 0 : parseInt(del) || 0;
130
+ }
131
+ } catch (e) {
132
+ // Ignore numstat errors
133
+ }
134
+ }
135
+
136
+ await logger.logEvent('modified', absPath, size, { additions, deletions }, projectDir);
137
+ }
138
+ break;
139
+ }
140
+ }
141
+
142
+ // Update snapshot
143
+ if (changesFound > 0) {
144
+ try {
145
+ execSync(`rsync -a --delete ${excludeArgs.join(' ')} "${projectDir}/" "${snapshotDir}/"`, {
146
+ stdio: 'pipe'
147
+ });
148
+ } catch (err) {
149
+ // Ignore rsync errors
150
+ }
151
+ }
152
+
153
+ return changesFound;
154
+ }
155
+
156
+ async function enable(projectConfig) {
157
+ const projectDir = process.cwd();
158
+ const snapshotDir = config.getSnapshotDir(projectConfig, projectDir);
159
+ const marmotDir = config.getMarmotDir(projectDir);
160
+ const logDir = config.getLogDir(projectConfig, projectDir);
161
+
162
+ console.log('');
163
+ console.log(chalk.bold('File Monitor Plugin Setup:'));
164
+ console.log(` Marmot directory: ${chalk.cyan(marmotDir)}`);
165
+ console.log(` Snapshot directory: ${chalk.cyan(snapshotDir)}`);
166
+ console.log(` Exclusions: ${chalk.cyan('Using .gitignore')}`);
167
+
168
+ // Install cron job
169
+ const cronJob = `* * * * * cd "${projectDir}" && marmot monitor >> "${logDir}/cron.log" 2>&1 ${MARMOT_CRON_MARKER}`;
170
+ const currentCrontab = getCrontab();
171
+
172
+ if (currentCrontab.includes(projectDir) && currentCrontab.includes('marmot monitor')) {
173
+ console.log(` Cron job: ${chalk.yellow('Already installed')}`);
174
+ } else {
175
+ const newCrontab = currentCrontab.trim() + '\n' + cronJob + '\n';
176
+ if (setCrontab(newCrontab)) {
177
+ console.log(` Cron job: ${chalk.green('Installed (runs every minute)')}`);
178
+ } else {
179
+ console.log(` Cron job: ${chalk.red('Failed to install')}`);
180
+ console.log('');
181
+ console.log('To add manually:');
182
+ console.log(chalk.cyan(` crontab -e`));
183
+ console.log('Then add:');
184
+ console.log(chalk.cyan(` ${cronJob}`));
185
+ }
186
+ }
187
+
188
+ console.log('');
189
+ console.log('Or run manually with:');
190
+ console.log(chalk.cyan(' marmot monitor'));
191
+ }
192
+
193
+ async function disable(projectConfig) {
194
+ const projectDir = process.cwd();
195
+ const snapshotDir = config.getSnapshotDir(projectConfig, projectDir);
196
+
197
+ console.log('');
198
+ console.log(chalk.bold('File Monitor Plugin Disabled:'));
199
+
200
+ // Remove cron job
201
+ const currentCrontab = getCrontab();
202
+ if (currentCrontab.includes(projectDir) && currentCrontab.includes('marmot monitor')) {
203
+ const lines = currentCrontab.split('\n');
204
+ const filteredLines = lines.filter(line => {
205
+ if (line.includes(projectDir) && line.includes('marmot monitor')) return false;
206
+ if (line.includes(MARMOT_CRON_MARKER)) return false;
207
+ return true;
208
+ });
209
+
210
+ const newCrontab = filteredLines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
211
+
212
+ if (setCrontab(newCrontab + '\n')) {
213
+ console.log(` Cron job: ${chalk.green('Removed')}`);
214
+ } else {
215
+ console.log(` Cron job: ${chalk.red('Failed to remove')}`);
216
+ console.log(' Remove manually with: crontab -e');
217
+ }
218
+ } else {
219
+ console.log(` Cron job: ${chalk.gray('Not found')}`);
220
+ }
221
+
222
+ // Keep snapshot but inform user
223
+ if (fs.existsSync(snapshotDir)) {
224
+ console.log(` Snapshot: ${chalk.cyan(snapshotDir)} (preserved)`);
225
+ console.log(' To remove snapshot: rm -rf ' + snapshotDir);
226
+ }
227
+ }
228
+
229
+ module.exports = {
230
+ run,
231
+ enable,
232
+ disable
233
+ };
@@ -0,0 +1,177 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ const MARMOT_MARKER = '# Marmot:';
6
+
7
+ const HOOKS = {
8
+ 'post-commit': `#!/bin/bash
9
+ # Marmot: Log git commits
10
+ COMMIT_HASH=$(git rev-parse --short HEAD)
11
+ COMMIT_MSG=$(git log -1 --pretty=%s | head -c 100 | sed 's/"/\\\\"/g')
12
+ cd "{{PROJECT_DIR}}" && marmot log git-commit -d "{\\"path\\": \\"$COMMIT_HASH: $COMMIT_MSG\\"}" 2>/dev/null &
13
+ `,
14
+
15
+ 'pre-push': `#!/bin/bash
16
+ # Marmot: Log git pushes
17
+ BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
18
+ REMOTE=$(git remote | head -1)
19
+ cd "{{PROJECT_DIR}}" && marmot log git-push -d "{\\"path\\": \\"$BRANCH -> $REMOTE\\"}" 2>/dev/null &
20
+ exit 0
21
+ `,
22
+
23
+ 'post-checkout': `#!/bin/bash
24
+ # Marmot: Log git checkouts
25
+ # $3 is branch flag: 1 = branch checkout, 0 = file checkout
26
+ if [ "$3" = "1" ]; then
27
+ BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
28
+ cd "{{PROJECT_DIR}}" && marmot log git-checkout -d "{\\"path\\": \\"$BRANCH\\"}" 2>/dev/null &
29
+ fi
30
+ `,
31
+
32
+ 'post-merge': `#!/bin/bash
33
+ # Marmot: Log git merges
34
+ BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
35
+ cd "{{PROJECT_DIR}}" && marmot log git-merge -d "{\\"path\\": \\"$BRANCH\\"}" 2>/dev/null &
36
+ `
37
+ };
38
+
39
+ const HOOK_NAMES = ['post-commit', 'pre-push', 'post-checkout', 'post-merge'];
40
+
41
+ function getGitHooksDir(projectDir = process.cwd()) {
42
+ const gitDir = path.join(projectDir, '.git');
43
+ if (!fs.existsSync(gitDir)) {
44
+ return null;
45
+ }
46
+ return path.join(gitDir, 'hooks');
47
+ }
48
+
49
+ async function enable(projectConfig) {
50
+ const projectDir = process.cwd();
51
+ const hooksDir = getGitHooksDir(projectDir);
52
+
53
+ if (!hooksDir) {
54
+ console.log(chalk.yellow('Not a git repository. Git hooks not installed.'));
55
+ return;
56
+ }
57
+
58
+ // Ensure hooks directory exists
59
+ if (!fs.existsSync(hooksDir)) {
60
+ fs.mkdirSync(hooksDir, { recursive: true });
61
+ }
62
+
63
+ const events = projectConfig.plugins?.['git-hooks']?.events || ['commit', 'push', 'checkout', 'merge'];
64
+ const installed = [];
65
+
66
+ for (const event of events) {
67
+ let hookName;
68
+ switch (event) {
69
+ case 'commit': hookName = 'post-commit'; break;
70
+ case 'push': hookName = 'pre-push'; break;
71
+ case 'checkout': hookName = 'post-checkout'; break;
72
+ case 'merge': hookName = 'post-merge'; break;
73
+ default: continue;
74
+ }
75
+
76
+ const hookPath = path.join(hooksDir, hookName);
77
+ const hookContent = HOOKS[hookName].replace(/\{\{PROJECT_DIR\}\}/g, projectDir);
78
+
79
+ // Check for existing hook
80
+ if (fs.existsSync(hookPath)) {
81
+ const existing = fs.readFileSync(hookPath, 'utf8');
82
+ // Skip if marmot hook already installed (check marker or marmot command)
83
+ if (existing.includes(MARMOT_MARKER) || existing.includes('marmot log')) {
84
+ installed.push(hookName);
85
+ continue;
86
+ }
87
+ // Append to existing non-marmot hook
88
+ fs.appendFileSync(hookPath, '\n' + hookContent);
89
+ } else {
90
+ fs.writeFileSync(hookPath, hookContent);
91
+ }
92
+
93
+ fs.chmodSync(hookPath, '755');
94
+ installed.push(hookName);
95
+ }
96
+
97
+ console.log('');
98
+ console.log(chalk.bold('Git Hooks Plugin Setup:'));
99
+ console.log(` Hooks directory: ${chalk.cyan(hooksDir)}`);
100
+ console.log(` Installed hooks: ${installed.join(', ')}`);
101
+ }
102
+
103
+ async function disable(projectConfig) {
104
+ const projectDir = process.cwd();
105
+ const hooksDir = getGitHooksDir(projectDir);
106
+
107
+ if (!hooksDir) {
108
+ return;
109
+ }
110
+
111
+ console.log('');
112
+ console.log(chalk.bold('Git Hooks Plugin Disabled:'));
113
+
114
+ let removed = 0;
115
+
116
+ for (const hookName of HOOK_NAMES) {
117
+ const hookPath = path.join(hooksDir, hookName);
118
+
119
+ if (!fs.existsSync(hookPath)) {
120
+ continue;
121
+ }
122
+
123
+ const content = fs.readFileSync(hookPath, 'utf8');
124
+
125
+ // Skip if no marmot content (check marker or marmot command)
126
+ if (!content.includes(MARMOT_MARKER) && !content.includes('marmot log')) {
127
+ continue;
128
+ }
129
+
130
+ // Remove all lines containing marmot-related content
131
+ const lines = content.split('\n');
132
+ const filteredLines = lines.filter(line => {
133
+ // Keep shebang
134
+ if (line.startsWith('#!')) return true;
135
+ // Remove marmot marker comments
136
+ if (line.includes(MARMOT_MARKER)) return false;
137
+ // Remove marmot-specific comments (like "# $3 is branch flag")
138
+ if (line.includes('$3 is branch flag')) return false;
139
+ // Remove lines with marmot commands
140
+ if (line.includes('marmot log')) return false;
141
+ // Remove variable assignments used by marmot hooks
142
+ if (/^\s*(COMMIT_HASH|COMMIT_MSG|BRANCH|REMOTE)=/.test(line)) return false;
143
+ // Remove if blocks checking $3 (marmot checkout hook)
144
+ if (/^\s*if \[ "\$3"/.test(line)) return false;
145
+ // Remove fi statements (but only if this is a marmot-only hook)
146
+ if (line.trim() === 'fi' && content.includes('if [ "$3"')) return false;
147
+ // Remove exit 0 (marmot pre-push)
148
+ if (line.trim() === 'exit 0' && content.includes(MARMOT_MARKER)) return false;
149
+ // Keep everything else
150
+ return true;
151
+ });
152
+
153
+ // Clean up the result
154
+ let cleanedContent = filteredLines.join('\n')
155
+ .replace(/\n{3,}/g, '\n\n')
156
+ .trim();
157
+
158
+ // If only shebang remains (or empty), delete the file
159
+ if (!cleanedContent || cleanedContent === '#!/bin/bash' || cleanedContent.match(/^#!\/bin\/bash\s*$/)) {
160
+ fs.unlinkSync(hookPath);
161
+ console.log(` Removed: ${chalk.cyan(hookName)}`);
162
+ } else {
163
+ fs.writeFileSync(hookPath, cleanedContent + '\n');
164
+ console.log(` Cleaned: ${chalk.cyan(hookName)} (preserved non-marmot content)`);
165
+ }
166
+ removed++;
167
+ }
168
+
169
+ if (removed === 0) {
170
+ console.log(' No marmot hooks found to remove.');
171
+ }
172
+ }
173
+
174
+ module.exports = {
175
+ enable,
176
+ disable
177
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ 'file-monitor': require('./file-monitor'),
3
+ 'terminal': require('./terminal'),
4
+ 'git-hooks': require('./git-hooks'),
5
+ 'makefile': require('./makefile'),
6
+ 'claude-hooks': require('./claude-hooks')
7
+ };