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.
- package/README.md +73 -0
- package/index.js +374 -0
- 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
|
+
}
|