mortgram-bridge 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.

Potentially problematic release.


This version of mortgram-bridge might be problematic. Click here for more details.

Files changed (3) hide show
  1. package/README.md +73 -0
  2. package/index.js +374 -0
  3. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Mortgram Bridge
2
+
3
+ A lightweight log forwarder that streams your OpenClaw agent logs to the Mortgram Dashboard for real-time observability.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx @mortgram/bridge --token YOUR_SECRET_TOKEN
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g @mortgram/bridge
15
+ mortgram-bridge --token YOUR_SECRET_TOKEN
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ # Basic usage
22
+ npx @mortgram/bridge --token abc123-def456-789
23
+
24
+ # With custom URL (for self-hosted)
25
+ npx @mortgram/bridge --token YOUR_TOKEN --url https://your-dashboard.com
26
+
27
+ # With custom log path
28
+ npx @mortgram/bridge --token YOUR_TOKEN --log-path ./my-agent.log
29
+
30
+ # Verbose mode
31
+ npx @mortgram/bridge --token YOUR_TOKEN --verbose
32
+ ```
33
+
34
+ ## Environment Variables
35
+
36
+ You can also configure via environment variables:
37
+
38
+ ```bash
39
+ export MORTGRAM_TOKEN=your-secret-token
40
+ export MORTGRAM_URL=https://mortgram.vercel.app
41
+
42
+ npx @mortgram/bridge
43
+ ```
44
+
45
+ ## How It Works
46
+
47
+ 1. **Watches** your OpenClaw log file (`~/.openclaw/logs/agent.log`)
48
+ 2. **Parses** each new log line to identify:
49
+ - 🧠 **Thoughts** - Agent reasoning and analysis
50
+ - ⚡ **Actions** - Tool calls and function executions
51
+ - ✅ **Results** - Outputs and completions
52
+ - ❌ **Errors** - Failures and exceptions
53
+ 3. **Forwards** structured data to the Mortgram Dashboard
54
+ 4. **Updates** your agent's heartbeat for live status
55
+
56
+ ## Log File Locations
57
+
58
+ The bridge automatically searches for logs in:
59
+
60
+ - `~/.openclaw/logs/agent.log` (default)
61
+ - `~/.openclaw/agent.log`
62
+ - `~/.config/openclaw/agent.log`
63
+ - `./agent.log`
64
+ - `./logs/agent.log`
65
+
66
+ ## Requirements
67
+
68
+ - Node.js 18+
69
+ - A Mortgram account with an agent token
70
+
71
+ ## License
72
+
73
+ MIT
package/index.js ADDED
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Mortgram Bridge
5
+ *
6
+ * A lightweight agent that tails your OpenClaw logs and forwards them
7
+ * to the Mortgram Dashboard for real-time observability.
8
+ *
9
+ * Usage:
10
+ * npx @mortgram/bridge --token YOUR_SECRET_TOKEN
11
+ *
12
+ * Environment Variables:
13
+ * MORTGRAM_TOKEN - Your agent's secret token
14
+ * MORTGRAM_URL - Dashboard URL (default: https://mortgram.vercel.app)
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const readline = require('readline');
21
+
22
+ // ANSI colors for terminal output
23
+ const colors = {
24
+ reset: '\x1b[0m',
25
+ bright: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ green: '\x1b[32m',
28
+ yellow: '\x1b[33m',
29
+ blue: '\x1b[34m',
30
+ magenta: '\x1b[35m',
31
+ cyan: '\x1b[36m',
32
+ red: '\x1b[31m',
33
+ };
34
+
35
+ // Parse command line arguments
36
+ function parseArgs() {
37
+ const args = process.argv.slice(2);
38
+ const config = {
39
+ token: process.env.MORTGRAM_TOKEN || null,
40
+ url: process.env.MORTGRAM_URL || 'https://mortgram.vercel.app',
41
+ logPath: null,
42
+ verbose: false,
43
+ };
44
+
45
+ for (let i = 0; i < args.length; i++) {
46
+ const arg = args[i];
47
+ if (arg === '--token' && args[i + 1]) {
48
+ config.token = args[++i];
49
+ } else if (arg === '--url' && args[i + 1]) {
50
+ config.url = args[++i];
51
+ } else if (arg === '--log-path' && args[i + 1]) {
52
+ config.logPath = args[++i];
53
+ } else if (arg === '--verbose' || arg === '-v') {
54
+ config.verbose = true;
55
+ } else if (arg === '--help' || arg === '-h') {
56
+ printHelp();
57
+ process.exit(0);
58
+ }
59
+ }
60
+
61
+ return config;
62
+ }
63
+
64
+ function printHelp() {
65
+ console.log(`
66
+ ${colors.bright}${colors.cyan}Mortgram Bridge${colors.reset} - OpenClaw Log Forwarder
67
+
68
+ ${colors.bright}Usage:${colors.reset}
69
+ npx @mortgram/bridge --token YOUR_TOKEN
70
+
71
+ ${colors.bright}Options:${colors.reset}
72
+ --token <token> Your Mortgram secret token (required)
73
+ --url <url> Dashboard URL (default: https://mortgram.vercel.app)
74
+ --log-path <path> Custom log file path
75
+ --verbose, -v Show detailed output
76
+ --help, -h Show this help message
77
+
78
+ ${colors.bright}Environment Variables:${colors.reset}
79
+ MORTGRAM_TOKEN Your secret token
80
+ MORTGRAM_URL Dashboard URL
81
+
82
+ ${colors.bright}Example:${colors.reset}
83
+ npx @mortgram/bridge --token abc123-def456-789
84
+ `);
85
+ }
86
+
87
+ // Find the OpenClaw log file
88
+ function findLogFile(customPath) {
89
+ if (customPath && fs.existsSync(customPath)) {
90
+ return customPath;
91
+ }
92
+
93
+ const possiblePaths = [
94
+ path.join(os.homedir(), '.openclaw', 'logs', 'agent.log'),
95
+ path.join(os.homedir(), '.openclaw', 'agent.log'),
96
+ path.join(os.homedir(), '.config', 'openclaw', 'agent.log'),
97
+ './agent.log',
98
+ './logs/agent.log',
99
+ ];
100
+
101
+ for (const logPath of possiblePaths) {
102
+ if (fs.existsSync(logPath)) {
103
+ return logPath;
104
+ }
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ // Parse log line to determine type
111
+ function parseLogLine(line) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed) return null;
114
+
115
+ let stepType = 'log';
116
+ let level = 'info';
117
+ let message = trimmed;
118
+ const metadata = {};
119
+
120
+ // Detect log type from common patterns
121
+ const lowerLine = trimmed.toLowerCase();
122
+
123
+ // Thought patterns
124
+ if (
125
+ lowerLine.includes('thinking') ||
126
+ lowerLine.includes('reasoning') ||
127
+ lowerLine.includes('analyzing') ||
128
+ lowerLine.includes('[thought]') ||
129
+ lowerLine.includes('considering')
130
+ ) {
131
+ stepType = 'thought';
132
+ level = 'info';
133
+ }
134
+ // Tool/Action patterns
135
+ else if (
136
+ lowerLine.includes('calling') ||
137
+ lowerLine.includes('executing') ||
138
+ lowerLine.includes('tool:') ||
139
+ lowerLine.includes('[action]') ||
140
+ lowerLine.includes('[tool]') ||
141
+ lowerLine.includes('function:') ||
142
+ lowerLine.includes('running')
143
+ ) {
144
+ stepType = 'action';
145
+ level = 'info';
146
+
147
+ // Try to extract tool name
148
+ const toolMatch = trimmed.match(/(?:tool|function|calling|executing)[:\s]+([a-zA-Z_]+)/i);
149
+ if (toolMatch) {
150
+ metadata.tool_name = toolMatch[1];
151
+ }
152
+ }
153
+ // Result patterns
154
+ else if (
155
+ lowerLine.includes('result:') ||
156
+ lowerLine.includes('[result]') ||
157
+ lowerLine.includes('response:') ||
158
+ lowerLine.includes('output:') ||
159
+ lowerLine.includes('completed') ||
160
+ lowerLine.includes('success')
161
+ ) {
162
+ stepType = 'result';
163
+ level = 'success';
164
+ }
165
+ // Error patterns
166
+ else if (
167
+ lowerLine.includes('error') ||
168
+ lowerLine.includes('failed') ||
169
+ lowerLine.includes('exception') ||
170
+ lowerLine.includes('[error]')
171
+ ) {
172
+ stepType = 'error';
173
+ level = 'error';
174
+ }
175
+ // Warning patterns
176
+ else if (
177
+ lowerLine.includes('warning') ||
178
+ lowerLine.includes('warn') ||
179
+ lowerLine.includes('[warn]')
180
+ ) {
181
+ level = 'warning';
182
+ }
183
+ // Debug patterns
184
+ else if (
185
+ lowerLine.includes('[debug]') ||
186
+ lowerLine.includes('debug:')
187
+ ) {
188
+ level = 'debug';
189
+ }
190
+
191
+ // Try to parse JSON if present
192
+ const jsonMatch = trimmed.match(/\{[\s\S]*\}$/);
193
+ if (jsonMatch) {
194
+ try {
195
+ const jsonData = JSON.parse(jsonMatch[0]);
196
+ Object.assign(metadata, jsonData);
197
+ message = trimmed.replace(jsonMatch[0], '').trim() || message;
198
+ } catch {
199
+ // Not valid JSON, keep original message
200
+ }
201
+ }
202
+
203
+ return {
204
+ type: stepType,
205
+ level,
206
+ message,
207
+ metadata,
208
+ timestamp: new Date().toISOString(),
209
+ };
210
+ }
211
+
212
+ // Send log to Mortgram API
213
+ async function sendToMortgram(config, logData) {
214
+ const endpoint = `${config.url}/api/ingest`;
215
+
216
+ try {
217
+ const response = await fetch(endpoint, {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ 'x-mortgram-token': config.token,
222
+ },
223
+ body: JSON.stringify({
224
+ type: logData.type === 'thought' || logData.type === 'action' || logData.type === 'result' || logData.type === 'error'
225
+ ? 'step'
226
+ : 'log',
227
+ level: logData.level,
228
+ message: logData.message,
229
+ metadata: logData.metadata,
230
+ step_type: logData.type,
231
+ }),
232
+ });
233
+
234
+ if (!response.ok) {
235
+ const error = await response.json().catch(() => ({}));
236
+ throw new Error(error.error || `HTTP ${response.status}`);
237
+ }
238
+
239
+ return true;
240
+ } catch (error) {
241
+ if (config.verbose) {
242
+ console.error(`${colors.red}[ERROR]${colors.reset} Failed to send: ${error.message}`);
243
+ }
244
+ return false;
245
+ }
246
+ }
247
+
248
+ // Main bridge function
249
+ async function startBridge(config) {
250
+ console.log(`
251
+ ${colors.cyan}${colors.bright}╔═══════════════════════════════════════╗
252
+ ║ MORTGRAM BRIDGE v1.0.0 ║
253
+ ╚═══════════════════════════════════════╝${colors.reset}
254
+ `);
255
+
256
+ // Validate token
257
+ if (!config.token) {
258
+ console.error(`${colors.red}[ERROR]${colors.reset} No token provided. Use --token or set MORTGRAM_TOKEN`);
259
+ process.exit(1);
260
+ }
261
+
262
+ // Find log file
263
+ const logPath = findLogFile(config.logPath);
264
+ if (!logPath) {
265
+ console.log(`${colors.yellow}[WARN]${colors.reset} Log file not found. Creating watcher for default path...`);
266
+ const defaultPath = path.join(os.homedir(), '.openclaw', 'logs', 'agent.log');
267
+
268
+ // Create directory structure
269
+ const logDir = path.dirname(defaultPath);
270
+ if (!fs.existsSync(logDir)) {
271
+ fs.mkdirSync(logDir, { recursive: true });
272
+ }
273
+
274
+ // Create empty file
275
+ if (!fs.existsSync(defaultPath)) {
276
+ fs.writeFileSync(defaultPath, '');
277
+ }
278
+
279
+ config.logPath = defaultPath;
280
+ } else {
281
+ config.logPath = logPath;
282
+ }
283
+
284
+ console.log(`${colors.green}[✓]${colors.reset} Token: ${config.token.slice(0, 8)}...`);
285
+ console.log(`${colors.green}[✓]${colors.reset} URL: ${config.url}`);
286
+ console.log(`${colors.green}[✓]${colors.reset} Watching: ${config.logPath}`);
287
+ console.log(`\n${colors.dim}Waiting for logs...${colors.reset}\n`);
288
+
289
+ // Send initial connection log
290
+ await sendToMortgram(config, {
291
+ type: 'log',
292
+ level: 'success',
293
+ message: 'Mortgram Bridge connected',
294
+ metadata: {
295
+ bridge_version: '1.0.0',
296
+ log_path: config.logPath,
297
+ platform: process.platform,
298
+ },
299
+ });
300
+
301
+ console.log(`${colors.green}[✓]${colors.reset} Connection established!\n`);
302
+
303
+ // Track file position
304
+ let fileSize = fs.existsSync(config.logPath) ? fs.statSync(config.logPath).size : 0;
305
+ let linesForwarded = 0;
306
+
307
+ // Watch for file changes
308
+ const watcher = fs.watch(config.logPath, async (eventType) => {
309
+ if (eventType !== 'change') return;
310
+
311
+ try {
312
+ const stats = fs.statSync(config.logPath);
313
+
314
+ // File was truncated/rotated
315
+ if (stats.size < fileSize) {
316
+ fileSize = 0;
317
+ }
318
+
319
+ // Read new content
320
+ if (stats.size > fileSize) {
321
+ const stream = fs.createReadStream(config.logPath, {
322
+ start: fileSize,
323
+ end: stats.size,
324
+ encoding: 'utf8',
325
+ });
326
+
327
+ const rl = readline.createInterface({ input: stream });
328
+
329
+ for await (const line of rl) {
330
+ const parsed = parseLogLine(line);
331
+ if (parsed) {
332
+ const success = await sendToMortgram(config, parsed);
333
+ if (success) {
334
+ linesForwarded++;
335
+ const typeColors = {
336
+ thought: colors.magenta,
337
+ action: colors.blue,
338
+ result: colors.green,
339
+ error: colors.red,
340
+ log: colors.dim,
341
+ };
342
+ const typeColor = typeColors[parsed.type] || colors.dim;
343
+ console.log(`${typeColor}[${parsed.type.toUpperCase()}]${colors.reset} ${parsed.message.slice(0, 60)}${parsed.message.length > 60 ? '...' : ''}`);
344
+ }
345
+ }
346
+ }
347
+
348
+ fileSize = stats.size;
349
+ }
350
+ } catch (error) {
351
+ if (config.verbose) {
352
+ console.error(`${colors.red}[ERROR]${colors.reset} ${error.message}`);
353
+ }
354
+ }
355
+ });
356
+
357
+ // Handle graceful shutdown
358
+ const shutdown = () => {
359
+ console.log(`\n${colors.yellow}[INFO]${colors.reset} Shutting down bridge...`);
360
+ console.log(`${colors.dim}Forwarded ${linesForwarded} log entries${colors.reset}`);
361
+ watcher.close();
362
+ process.exit(0);
363
+ };
364
+
365
+ process.on('SIGINT', shutdown);
366
+ process.on('SIGTERM', shutdown);
367
+
368
+ // Keep process alive
369
+ process.stdin.resume();
370
+ }
371
+
372
+ // Run the bridge
373
+ const config = parseArgs();
374
+ startBridge(config);
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "mortgram-bridge",
3
+ "version": "1.0.0",
4
+ "description": "OpenClaw log bridge for Mortgram Dashboard - Real-time agent observability",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "mortgram-bridge": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "mortgram",
14
+ "openclaw",
15
+ "ai-agent",
16
+ "observability",
17
+ "logging",
18
+ "monitoring",
19
+ "telemetry"
20
+ ],
21
+ "author": "Mortgram",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/yusef1975/agent-dashboard"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "files": [
31
+ "index.js",
32
+ "README.md"
33
+ ]
34
+ }