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.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # Marmot
2
+
3
+ Activity monitoring tool for developer workflows. Tracks file changes, terminal commands, git operations, and Claude Code hooks.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g marmot-logger
9
+ ```
10
+
11
+ Or use with npx:
12
+
13
+ ```bash
14
+ npx marmot-logger init
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Initialize marmot in your project
21
+ marmot init
22
+
23
+ # Enable plugins
24
+ marmot enable file-monitor # Track file changes
25
+ marmot enable terminal # Log terminal commands
26
+ marmot enable git-hooks # Log git operations
27
+ marmot enable claude-hooks # Log Claude Code activity
28
+
29
+ # Check status
30
+ marmot status
31
+
32
+ # View recent logs
33
+ marmot logs --last 20
34
+ ```
35
+
36
+ ## Configuration
37
+
38
+ ### Environment Variables
39
+
40
+ - `MARMOT_URL` - Signing service URL (default: `https://logging.drazou.net`)
41
+ - `MARMOT_API_KEY` - API key for signing service (required)
42
+
43
+ ```bash
44
+ export MARMOT_API_KEY=your-api-key
45
+ ```
46
+
47
+ ### Project Configuration
48
+
49
+ Marmot creates `.marmotrc.json` in your project root:
50
+
51
+ ```json
52
+ {
53
+ "logDir": "./logs",
54
+ "snapshotDir": "./.marmot-snapshot",
55
+ "plugins": {
56
+ "file-monitor": {
57
+ "enabled": true,
58
+ "exclude": [".git", "node_modules", ".marmot-snapshot"]
59
+ },
60
+ "terminal": { "enabled": true },
61
+ "git-hooks": { "enabled": true },
62
+ "claude-hooks": { "enabled": true }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## Plugins
68
+
69
+ ### File Monitor
70
+
71
+ Detects file changes using git diff against a snapshot.
72
+
73
+ ```bash
74
+ marmot enable file-monitor
75
+
76
+ # Run manually or via cron
77
+ marmot monitor
78
+
79
+ # Add to crontab for every minute:
80
+ * * * * * cd /path/to/project && npx marmot monitor >> ./logs/cron.log 2>&1
81
+ ```
82
+
83
+ ### Terminal
84
+
85
+ Logs terminal commands executed in the project directory.
86
+
87
+ ```bash
88
+ marmot enable terminal
89
+
90
+ # Add to ~/.bashrc:
91
+ source "/path/to/project/.marmot/terminal-hook.sh"
92
+ ```
93
+
94
+ ### Git Hooks
95
+
96
+ Logs git operations (commit, push, checkout, merge).
97
+
98
+ ```bash
99
+ marmot enable git-hooks
100
+ ```
101
+
102
+ Hooks are automatically installed in `.git/hooks/`.
103
+
104
+ ### Claude Hooks
105
+
106
+ Logs Claude Code IDE activity.
107
+
108
+ ```bash
109
+ marmot enable claude-hooks
110
+ ```
111
+
112
+ Hooks are configured in `.claude/settings.local.json`.
113
+
114
+ ## CLI Commands
115
+
116
+ | Command | Description |
117
+ |---------|-------------|
118
+ | `marmot init` | Initialize marmot in current project |
119
+ | `marmot enable <plugin>` | Enable a plugin |
120
+ | `marmot disable <plugin>` | Disable a plugin |
121
+ | `marmot status` | Show status and enabled plugins |
122
+ | `marmot logs` | View recent log entries |
123
+ | `marmot logs --today` | View today's logs |
124
+ | `marmot logs --last N` | View last N entries |
125
+ | `marmot verify` | Verify log signatures |
126
+ | `marmot monitor` | Run file monitor once |
127
+
128
+ ## Log Format
129
+
130
+ Logs are stored as JSON Lines in `./logs/file_events_YYYY-MM-DD.log`:
131
+
132
+ ```json
133
+ {"timestamp":"2025-12-22T14:30:00Z","uuid":"...","event":"modified","path":"/project/src/index.js","size":1234,"additions":10,"deletions":5,"signed":true}
134
+ {"timestamp":"2025-12-22T14:31:00Z","uuid":"...","event":"terminal","path":"/project","command":"npm test","size":0,"signed":true}
135
+ {"timestamp":"2025-12-22T14:32:00Z","uuid":"...","event":"git_commit","path":"abc1234: Fix bug","size":0,"signed":true}
136
+ ```
137
+
138
+ ## Event Types
139
+
140
+ | Event | Description |
141
+ |-------|-------------|
142
+ | `created` | File created |
143
+ | `modified` | File modified (includes additions/deletions) |
144
+ | `deleted` | File deleted |
145
+ | `terminal` | Terminal command executed |
146
+ | `git_commit` | Git commit made |
147
+ | `git_push` | Git push executed |
148
+ | `git_checkout` | Git branch checkout |
149
+ | `git_merge` | Git merge completed |
150
+ | `make_command` | Make target executed |
151
+ | `claude_hook_*` | Claude Code hook events |
152
+
153
+ ## API
154
+
155
+ ```javascript
156
+ const marmot = require('marmot-logger');
157
+
158
+ // Log an event programmatically
159
+ await marmot.log({
160
+ event: 'custom_event',
161
+ path: '/path/to/file',
162
+ size: 1234,
163
+ metadata: { custom: 'data' }
164
+ });
165
+
166
+ // Verify logs
167
+ const results = await marmot.verifyLogs('./logs/file_events_2025-12-22.log');
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
package/bin/marmot.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const pkg = require('../package.json');
6
+
7
+ const init = require('../src/cli/init');
8
+ const enable = require('../src/cli/enable');
9
+ const disable = require('../src/cli/disable');
10
+ const status = require('../src/cli/status');
11
+ const verify = require('../src/cli/verify');
12
+ const logs = require('../src/cli/logs');
13
+ const monitor = require('../src/cli/monitor');
14
+ const log = require('../src/cli/log');
15
+ const login = require('../src/cli/login');
16
+
17
+ program
18
+ .name('marmot')
19
+ .description('Activity monitoring tool for developer workflows')
20
+ .version(pkg.version);
21
+
22
+ program
23
+ .command('init')
24
+ .description('Initialize marmot in current project')
25
+ .action(init);
26
+
27
+ program
28
+ .command('enable <plugin>')
29
+ .description('Enable a plugin (file-monitor, terminal, git-hooks, makefile, claude-hooks)')
30
+ .action(enable);
31
+
32
+ program
33
+ .command('disable <plugin>')
34
+ .description('Disable a plugin')
35
+ .action(disable);
36
+
37
+ program
38
+ .command('status')
39
+ .description('Show marmot status and enabled plugins')
40
+ .action(status);
41
+
42
+ program
43
+ .command('verify')
44
+ .description('Verify log signatures')
45
+ .option('-f, --file <path>', 'Log file to verify (default: today\'s log)')
46
+ .action(verify);
47
+
48
+ program
49
+ .command('logs')
50
+ .description('View recent log entries')
51
+ .option('-t, --today', 'Show today\'s logs')
52
+ .option('-l, --last <n>', 'Show last N entries', '10')
53
+ .action(logs);
54
+
55
+ program
56
+ .command('monitor')
57
+ .description('Run file monitor once (for cron/scripts)')
58
+ .action(monitor);
59
+
60
+ program
61
+ .command('log <event>')
62
+ .description('Log an event (used by hooks)')
63
+ .option('-d, --data <json>', 'JSON data for the event')
64
+ .action(log);
65
+
66
+ program
67
+ .command('login')
68
+ .description('Login with API key and save to .env')
69
+ .action(login);
70
+
71
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "marmot-logger",
3
+ "version": "1.0.0",
4
+ "description": "Activity monitoring tool for developer workflows - tracks file changes, terminal commands, git operations, and Claude Code hooks",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "marmot": "./bin/marmot.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "monitoring",
14
+ "logging",
15
+ "developer-tools",
16
+ "activity-tracking",
17
+ "git-hooks",
18
+ "claude-code"
19
+ ],
20
+ "author": "",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "commander": "^12.1.0",
24
+ "chalk": "^4.1.2"
25
+ },
26
+ "engines": {
27
+ "node": ">=16.0.0"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "src/",
32
+ "templates/"
33
+ ]
34
+ }
@@ -0,0 +1,40 @@
1
+ const chalk = require('chalk');
2
+ const config = require('../core/config');
3
+ const plugins = require('../plugins');
4
+
5
+ const VALID_PLUGINS = ['file-monitor', 'terminal', 'git-hooks', 'makefile', 'claude-hooks'];
6
+
7
+ module.exports = async function disable(pluginName) {
8
+ if (!VALID_PLUGINS.includes(pluginName)) {
9
+ console.log(chalk.red(`Unknown plugin: ${pluginName}`));
10
+ console.log('Available plugins:', VALID_PLUGINS.join(', '));
11
+ process.exit(1);
12
+ }
13
+
14
+ const projectConfig = config.loadConfig();
15
+ if (!projectConfig) {
16
+ console.log(chalk.red('Marmot not initialized. Run: marmot init'));
17
+ process.exit(1);
18
+ }
19
+
20
+ if (!config.isPluginEnabled(projectConfig, pluginName)) {
21
+ console.log(chalk.yellow(`Plugin '${pluginName}' is already disabled.`));
22
+ return;
23
+ }
24
+
25
+ // Run plugin-specific teardown
26
+ const plugin = plugins[pluginName];
27
+ if (plugin && plugin.disable) {
28
+ try {
29
+ await plugin.disable(projectConfig);
30
+ } catch (err) {
31
+ console.log(chalk.red(`Teardown error: ${err.message}`));
32
+ }
33
+ }
34
+
35
+ // Disable the plugin in config
36
+ config.disablePlugin(projectConfig, pluginName);
37
+ config.saveConfig(projectConfig);
38
+
39
+ console.log(chalk.green(`✓ Plugin '${pluginName}' disabled.`));
40
+ };
@@ -0,0 +1,40 @@
1
+ const chalk = require('chalk');
2
+ const config = require('../core/config');
3
+ const plugins = require('../plugins');
4
+
5
+ const VALID_PLUGINS = ['file-monitor', 'terminal', 'git-hooks', 'makefile', 'claude-hooks'];
6
+
7
+ module.exports = async function enable(pluginName) {
8
+ if (!VALID_PLUGINS.includes(pluginName)) {
9
+ console.log(chalk.red(`Unknown plugin: ${pluginName}`));
10
+ console.log('Available plugins:', VALID_PLUGINS.join(', '));
11
+ process.exit(1);
12
+ }
13
+
14
+ const projectConfig = config.loadConfig();
15
+ if (!projectConfig) {
16
+ console.log(chalk.red('Marmot not initialized. Run: marmot init'));
17
+ process.exit(1);
18
+ }
19
+
20
+ if (config.isPluginEnabled(projectConfig, pluginName)) {
21
+ console.log(chalk.yellow(`Plugin '${pluginName}' is already enabled.`));
22
+ return;
23
+ }
24
+
25
+ // Enable the plugin in config
26
+ config.enablePlugin(projectConfig, pluginName);
27
+ config.saveConfig(projectConfig);
28
+
29
+ console.log(chalk.green(`✓ Plugin '${pluginName}' enabled.`));
30
+
31
+ // Run plugin-specific setup
32
+ const plugin = plugins[pluginName];
33
+ if (plugin && plugin.enable) {
34
+ try {
35
+ await plugin.enable(projectConfig);
36
+ } catch (err) {
37
+ console.log(chalk.red(`Setup error: ${err.message}`));
38
+ }
39
+ }
40
+ };
@@ -0,0 +1,85 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const config = require('../core/config');
4
+ const fs = require('fs');
5
+
6
+ function addToGitignore(projectDir, entry) {
7
+ const gitignorePath = path.join(projectDir, '.gitignore');
8
+
9
+ if (fs.existsSync(gitignorePath)) {
10
+ const content = fs.readFileSync(gitignorePath, 'utf8');
11
+ if (!content.includes(entry)) {
12
+ // Add entry with marmot section if not exists
13
+ let newContent = content;
14
+ if (!content.includes('# Marmot')) {
15
+ newContent = content.trimEnd() + '\n\n# Marmot\n' + entry + '\n';
16
+ } else {
17
+ // Add under existing Marmot section
18
+ newContent = content.replace('# Marmot\n', `# Marmot\n${entry}\n`);
19
+ }
20
+ fs.writeFileSync(gitignorePath, newContent);
21
+ return true;
22
+ }
23
+ } else {
24
+ fs.writeFileSync(gitignorePath, `# Marmot\n${entry}\n`);
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ module.exports = async function init() {
31
+ const projectDir = process.cwd();
32
+ const existingConfig = config.loadConfig(projectDir);
33
+
34
+ if (existingConfig) {
35
+ console.log(chalk.yellow('Marmot is already initialized in this project.'));
36
+ console.log(`Configuration file: ${config.getConfigPath(projectDir)}`);
37
+ return;
38
+ }
39
+
40
+ // Check for required environment variables
41
+ const signingUrl = config.getSigningUrl(projectDir);
42
+ const apiKey = config.getApiKey(projectDir);
43
+
44
+ if (!apiKey) {
45
+ console.log(chalk.yellow('Warning: MARMOT_API_KEY is not set.'));
46
+ console.log('Set it in your .env file or run:');
47
+ console.log(chalk.cyan(' marmot login'));
48
+ console.log('');
49
+ }
50
+
51
+ // Create marmot directory in /tmp
52
+ const marmotDir = config.getMarmotDir(projectDir);
53
+ if (!fs.existsSync(marmotDir)) {
54
+ fs.mkdirSync(marmotDir, { recursive: true });
55
+ }
56
+
57
+ // Create default config
58
+ const newConfig = config.createDefaultConfig(projectDir);
59
+
60
+ // Create logs directory in project
61
+ const logDir = config.getLogDir(newConfig, projectDir);
62
+ if (!fs.existsSync(logDir)) {
63
+ fs.mkdirSync(logDir, { recursive: true });
64
+ }
65
+
66
+ // Add marmot-logs to project's .gitignore
67
+ const logDirName = path.basename(logDir);
68
+ addToGitignore(projectDir, logDirName + '/');
69
+ addToGitignore(projectDir, '.marmot/');
70
+
71
+ console.log(chalk.green('✓ Marmot initialized successfully!'));
72
+ console.log('');
73
+ console.log('Marmot directory:', chalk.cyan(marmotDir));
74
+ console.log('Configuration file:', chalk.cyan(config.getConfigPath(projectDir)));
75
+ console.log('Logs directory:', chalk.cyan(logDir));
76
+ console.log('Signing URL:', chalk.cyan(signingUrl));
77
+ console.log('');
78
+ console.log('Next steps:');
79
+ console.log(chalk.cyan(' marmot enable file-monitor') + ' - Track file changes');
80
+ console.log(chalk.cyan(' marmot enable terminal') + ' - Log terminal commands');
81
+ console.log(chalk.cyan(' marmot enable git-hooks') + ' - Log git operations');
82
+ console.log(chalk.cyan(' marmot enable claude-hooks') + ' - Log Claude Code activity');
83
+ console.log('');
84
+ console.log('View status with:', chalk.cyan('marmot status'));
85
+ };
package/src/cli/log.js ADDED
@@ -0,0 +1,102 @@
1
+ const config = require('../core/config');
2
+ const logger = require('../core/logger');
3
+
4
+ module.exports = async function log(event, options) {
5
+ const projectConfig = config.loadConfig();
6
+
7
+ if (!projectConfig) {
8
+ // Silent fail for hook usage - don't break the calling process
9
+ process.exit(0);
10
+ }
11
+
12
+ let data = {};
13
+
14
+ // Try to parse JSON data from option or stdin
15
+ if (options.data) {
16
+ try {
17
+ data = JSON.parse(options.data);
18
+ } catch (e) {
19
+ // Ignore parse errors
20
+ }
21
+ } else {
22
+ // Try reading from stdin (for piped data from hooks)
23
+ try {
24
+ const stdin = require('fs').readFileSync(0, 'utf8');
25
+ if (stdin.trim()) {
26
+ data = JSON.parse(stdin);
27
+ }
28
+ } catch (e) {
29
+ // No stdin or parse error
30
+ }
31
+ }
32
+
33
+ // Extract details based on event type
34
+ let eventPath = data.path || '';
35
+ let size = data.size || 0;
36
+ const extra = {};
37
+
38
+ // Handle Claude hook events
39
+ if (event.startsWith('claude-')) {
40
+ const hookType = event.replace('claude-', '');
41
+ event = `claude_hook_${hookType}`;
42
+
43
+ // Extract relevant fields based on hook type
44
+ if (hookType === 'PreToolUse' || hookType === 'PostToolUse') {
45
+ const toolInput = data.tool_input || data;
46
+ const toolName = toolInput.tool_name || 'unknown';
47
+ const command = toolInput.command || '';
48
+ const filePath = toolInput.file_path || '';
49
+ const description = toolInput.description || '';
50
+
51
+ if (command) {
52
+ eventPath = `${toolName}: ${command.substring(0, 200)}`;
53
+ } else if (filePath) {
54
+ eventPath = `${toolName}: ${filePath}`;
55
+ } else if (description) {
56
+ eventPath = `${toolName}: ${description.substring(0, 200)}`;
57
+ } else {
58
+ eventPath = toolName;
59
+ }
60
+ } else if (hookType === 'Stop' || hookType === 'SubagentStop') {
61
+ eventPath = data.stop_reason || 'completed';
62
+ } else if (hookType === 'UserPromptSubmit') {
63
+ const prompt = data.prompt || data.user_prompt || '';
64
+ eventPath = `prompt: ${prompt.substring(0, 200)}`;
65
+ } else if (hookType === 'SessionStart' || hookType === 'SessionEnd') {
66
+ const sessionId = data.session_id || 'unknown';
67
+ eventPath = `${hookType.toLowerCase()}: ${sessionId}`;
68
+ }
69
+ }
70
+
71
+ // Handle git events
72
+ if (event.startsWith('git-')) {
73
+ const gitEvent = event.replace('git-', '');
74
+ event = `git_${gitEvent}`;
75
+ eventPath = data.path || data.details || '';
76
+ }
77
+
78
+ // Handle make events
79
+ if (event === 'make') {
80
+ event = 'make_command';
81
+ eventPath = data.target || data.path || '';
82
+ }
83
+
84
+ // Handle terminal events
85
+ if (event === 'terminal') {
86
+ extra.command = data.command || '';
87
+ eventPath = data.path || process.cwd();
88
+ }
89
+
90
+ // Add any additional fields
91
+ if (data.additions !== undefined) extra.additions = data.additions;
92
+ if (data.deletions !== undefined) extra.deletions = data.deletions;
93
+ if (data.command && event !== 'terminal') extra.command = data.command;
94
+
95
+ try {
96
+ await logger.logEvent(event, eventPath, size, extra);
97
+ } catch (err) {
98
+ // Silent fail for hook usage
99
+ }
100
+
101
+ process.exit(0);
102
+ };
@@ -0,0 +1,120 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+ const config = require('../core/config');
6
+ const signer = require('../core/signer');
7
+
8
+ function prompt(question) {
9
+ const rl = readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout
12
+ });
13
+
14
+ return new Promise((resolve) => {
15
+ rl.question(question, (answer) => {
16
+ rl.close();
17
+ resolve(answer.trim());
18
+ });
19
+ });
20
+ }
21
+
22
+ function updateEnvFile(projectDir, key, value) {
23
+ const envPath = path.join(projectDir, '.env');
24
+ let content = '';
25
+
26
+ if (fs.existsSync(envPath)) {
27
+ content = fs.readFileSync(envPath, 'utf8');
28
+ }
29
+
30
+ // Check if key already exists
31
+ const regex = new RegExp(`^${key}=.*$`, 'm');
32
+ if (regex.test(content)) {
33
+ // Update existing key
34
+ content = content.replace(regex, `${key}=${value}`);
35
+ } else {
36
+ // Add new key
37
+ if (content && !content.endsWith('\n')) {
38
+ content += '\n';
39
+ }
40
+ // Add section comment if this is the first marmot key
41
+ if (!content.includes('# Marmot')) {
42
+ content += '\n# Marmot logging\n';
43
+ }
44
+ content += `${key}=${value}\n`;
45
+ }
46
+
47
+ fs.writeFileSync(envPath, content);
48
+ }
49
+
50
+ module.exports = async function login() {
51
+ const projectDir = process.cwd();
52
+ const projectConfig = config.loadConfig(projectDir);
53
+
54
+ if (!projectConfig) {
55
+ console.log(chalk.red('Marmot not initialized. Run: marmot init'));
56
+ process.exit(1);
57
+ }
58
+
59
+ console.log(chalk.bold('Marmot Login'));
60
+ console.log('─'.repeat(40));
61
+ console.log('');
62
+
63
+ // Check current status
64
+ const currentKey = config.getApiKey(projectDir);
65
+ if (currentKey) {
66
+ console.log(chalk.yellow('API key is already configured.'));
67
+ const overwrite = await prompt('Overwrite? (y/N): ');
68
+ if (overwrite.toLowerCase() !== 'y') {
69
+ console.log('Cancelled.');
70
+ return;
71
+ }
72
+ console.log('');
73
+ }
74
+
75
+ // Prompt for API key
76
+ const apiKey = await prompt('Enter your MARMOT_API_KEY: ');
77
+
78
+ if (!apiKey) {
79
+ console.log(chalk.red('No API key provided.'));
80
+ process.exit(1);
81
+ }
82
+
83
+ // Test the API key
84
+ console.log('');
85
+ console.log('Testing API key...');
86
+
87
+ // Temporarily set the key in environment for testing
88
+ process.env.MARMOT_API_KEY = apiKey;
89
+ signer.clearTokenCache();
90
+
91
+ try {
92
+ await signer.getToken(projectDir);
93
+ console.log(chalk.green('✓ API key is valid!'));
94
+ } catch (err) {
95
+ console.log(chalk.red(`✗ API key test failed: ${err.message}`));
96
+ console.log('');
97
+ console.log('The key was not saved. Please check your API key and try again.');
98
+ process.exit(1);
99
+ }
100
+
101
+ // Write to .env file
102
+ console.log('');
103
+ console.log('Saving to .env...');
104
+ updateEnvFile(projectDir, 'MARMOT_API_KEY', apiKey);
105
+
106
+ // Also save MARMOT_URL if not already set
107
+ const currentUrl = config.getSigningUrl(projectDir);
108
+ if (currentUrl === 'https://logging.drazou.net') {
109
+ // Check if it's from default or actually in .env
110
+ const envFile = config.loadEnvFile ? config.loadEnvFile(projectDir) : {};
111
+ if (!envFile.MARMOT_URL && !process.env.MARMOT_URL) {
112
+ updateEnvFile(projectDir, 'MARMOT_URL', 'https://logging.drazou.net');
113
+ }
114
+ }
115
+
116
+ console.log(chalk.green('✓ Credentials saved to .env'));
117
+ console.log('');
118
+ console.log('You can now use marmot with signed logging.');
119
+ console.log('Run', chalk.cyan('marmot status'), 'to verify.');
120
+ };