mortgram-hook 1.0.0 → 1.0.2
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/mortgram-observer/HOOK.md +43 -0
- package/mortgram-observer/handler.js +181 -0
- package/package.json +9 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mortgram-observer
|
|
3
|
+
description: "Stream agent events to MORTGRAM dashboard in real-time"
|
|
4
|
+
homepage: https://mortgram.com
|
|
5
|
+
metadata:
|
|
6
|
+
{
|
|
7
|
+
"openclaw":
|
|
8
|
+
{
|
|
9
|
+
"emoji": "👻",
|
|
10
|
+
"events": ["agent:thought", "agent:reasoning", "agent:response", "agent:message", "agent:error", "agent:start", "agent:end", "tool:call", "tool:result", "tool:error", "session:usage", "session:cost", "session:start", "session:end"],
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# MORTGRAM Ghost Bridge
|
|
16
|
+
|
|
17
|
+
Invisible observability hook for OpenClaw agents. Streams thoughts, tool calls, errors, and cost data to your MORTGRAM dashboard in real-time.
|
|
18
|
+
|
|
19
|
+
## What It Does
|
|
20
|
+
|
|
21
|
+
Every time your agent thinks, calls a tool, or completes a session:
|
|
22
|
+
|
|
23
|
+
1. **Captures event** — Intercepts agent thoughts, tool calls, errors, and usage data
|
|
24
|
+
2. **Streams to MORTGRAM** — POSTs event data to the MORTGRAM ingest API
|
|
25
|
+
3. **Silent operation** — Zero performance impact, silent failure on network issues
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
Environment variable `MORTGRAM_API_KEY` must be set to your MORTGRAM API key from the dashboard.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Set the following environment variables:
|
|
34
|
+
|
|
35
|
+
- `MORTGRAM_API_KEY` — Your MORTGRAM API key (required)
|
|
36
|
+
- `MORTGRAM_API_URL` — API endpoint (default: `https://mortgram.com/api/ingest`)
|
|
37
|
+
- `MG_AGENT_ID` — Agent identifier (default: `ghost-agent`)
|
|
38
|
+
|
|
39
|
+
## Disabling
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
openclaw hooks disable mortgram-observer
|
|
43
|
+
```
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════
|
|
2
|
+
// MORTGRAM Ghost Bridge — Native OpenClaw Hook
|
|
3
|
+
// ═══════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
export const name = "mortgram-observer";
|
|
6
|
+
|
|
7
|
+
// State
|
|
8
|
+
let ably = null;
|
|
9
|
+
let pulseChannel = null;
|
|
10
|
+
let commandChannel = null;
|
|
11
|
+
let initialized = false;
|
|
12
|
+
let userId = null;
|
|
13
|
+
let agentId = process.env.MG_AGENT_ID || "ghost-agent";
|
|
14
|
+
|
|
15
|
+
// Tracking
|
|
16
|
+
let totalTurns = 0;
|
|
17
|
+
let successfulTools = 0;
|
|
18
|
+
let latestThought = "Ghost Bridge Active";
|
|
19
|
+
let dailyCost = 0;
|
|
20
|
+
|
|
21
|
+
const API_KEY = process.env.MORTGRAM_API_KEY || process.env.MG_LIVE_KEY || "";
|
|
22
|
+
const API_URL = process.env.MORTGRAM_API_URL || "https://mortgram.com/api/ingest";
|
|
23
|
+
|
|
24
|
+
// Initialize Ably connection using keys from MORTGRAM API
|
|
25
|
+
async function init(initialType, initialContent, initialMetadata) {
|
|
26
|
+
if (initialized || !API_KEY) return;
|
|
27
|
+
initialized = true;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// 1. Initial Handshake + Data Ingest
|
|
31
|
+
const res = await fetch(API_URL, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
"x-mortgram-token": API_KEY
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
agent_id: agentId,
|
|
39
|
+
type: initialType,
|
|
40
|
+
content: initialContent,
|
|
41
|
+
metadata: initialMetadata
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
const ablyKey = data.ably_key;
|
|
47
|
+
userId = data.userId;
|
|
48
|
+
|
|
49
|
+
if (ablyKey && userId) {
|
|
50
|
+
// Lazy load Ably to keep startup fast
|
|
51
|
+
const { default: Ably } = await import("ably");
|
|
52
|
+
ably = new Ably.Realtime(ablyKey);
|
|
53
|
+
|
|
54
|
+
pulseChannel = ably.channels.get(`agent:pulse:${agentId}`);
|
|
55
|
+
commandChannel = ably.channels.get(`agent:commands:${agentId}`);
|
|
56
|
+
|
|
57
|
+
// Heartbeat Pulse (every 10s)
|
|
58
|
+
setInterval(() => {
|
|
59
|
+
if (pulseChannel) {
|
|
60
|
+
pulseChannel.publish("pulse", {
|
|
61
|
+
status: "online",
|
|
62
|
+
latest_thought: latestThought,
|
|
63
|
+
efficiency: totalTurns > 0 ? (successfulTools / totalTurns) : 0,
|
|
64
|
+
daily_cost: dailyCost,
|
|
65
|
+
timestamp: new Date().toISOString()
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, 10000);
|
|
69
|
+
|
|
70
|
+
// Remote Kill Switch
|
|
71
|
+
commandChannel.subscribe("kill", async (message) => {
|
|
72
|
+
latestThought = "Remote shutdown initiated by Commander.";
|
|
73
|
+
await send("system", "Remote shutdown initiated by Commander.", {
|
|
74
|
+
status: "offline",
|
|
75
|
+
reason: message.data.reason || "Manual Termination"
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Signal Ably one last time
|
|
79
|
+
if (pulseChannel) {
|
|
80
|
+
await pulseChannel.publish("pulse", { status: "offline", timestamp: new Date().toISOString() });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.error("\x1b[31m[MORTGRAM] Remote shutdown initiated by Commander.\x1b[0m");
|
|
84
|
+
process.exit(0); // This kills the OpenClaw process/agent
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Silent fail
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Silently POST to the MORTGRAM ingest endpoint
|
|
93
|
+
async function send(type, content, metadata = {}) {
|
|
94
|
+
if (!API_KEY) return;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await fetch(API_URL, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"x-mortgram-token": API_KEY
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
agent_id: agentId,
|
|
105
|
+
type,
|
|
106
|
+
content,
|
|
107
|
+
metadata: {
|
|
108
|
+
...metadata,
|
|
109
|
+
daily_cost: dailyCost,
|
|
110
|
+
efficiency: totalTurns > 0 ? (successfulTools / totalTurns) : 0,
|
|
111
|
+
latest_thought: latestThought,
|
|
112
|
+
status: "online",
|
|
113
|
+
timestamp: new Date().toISOString()
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
} catch (_) {
|
|
118
|
+
// Silent fail
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function onEvent({ event, data }) {
|
|
123
|
+
// Determine type/content for potential init
|
|
124
|
+
let type = "log";
|
|
125
|
+
let content = "Event observed";
|
|
126
|
+
|
|
127
|
+
switch (event) {
|
|
128
|
+
case "agent:thought":
|
|
129
|
+
case "agent:reasoning":
|
|
130
|
+
totalTurns++;
|
|
131
|
+
latestThought = data?.text || data?.content || "Thinking...";
|
|
132
|
+
type = "thought";
|
|
133
|
+
content = latestThought;
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case "tool:call":
|
|
137
|
+
case "tool:result":
|
|
138
|
+
successfulTools++;
|
|
139
|
+
type = "action";
|
|
140
|
+
content = data?.name || data?.tool || "Tool Call";
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case "agent:error":
|
|
144
|
+
case "tool:error":
|
|
145
|
+
if (successfulTools > 0) successfulTools--;
|
|
146
|
+
type = "error";
|
|
147
|
+
content = data?.message || data?.text || "Error occurred";
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case "session:usage":
|
|
151
|
+
case "session:cost":
|
|
152
|
+
dailyCost = data?.total_cost || data?.cost || dailyCost;
|
|
153
|
+
type = "cost_update";
|
|
154
|
+
content = `Daily spend: $${dailyCost.toFixed(4)}`;
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case "session:start":
|
|
158
|
+
case "agent:start":
|
|
159
|
+
totalTurns = 0;
|
|
160
|
+
successfulTools = 0;
|
|
161
|
+
dailyCost = 0;
|
|
162
|
+
latestThought = "Session started";
|
|
163
|
+
type = "system";
|
|
164
|
+
content = "Ghost Bridge: Session started";
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case "agent:response":
|
|
168
|
+
case "agent:message":
|
|
169
|
+
latestThought = data?.text?.slice(0, 200) || "Response generated";
|
|
170
|
+
type = "thought";
|
|
171
|
+
content = latestThought;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Initialize on first event
|
|
176
|
+
if (!initialized) {
|
|
177
|
+
await init(type, content, { event, ...data });
|
|
178
|
+
} else {
|
|
179
|
+
await send(type, content, { event, ...data });
|
|
180
|
+
}
|
|
181
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mortgram-hook",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MORTGRAM Ghost Bridge — Invisible observability for OpenClaw agents",
|
|
5
|
-
"main": "index.mjs",
|
|
6
5
|
"type": "module",
|
|
6
|
+
"openclaw": {
|
|
7
|
+
"hooks": [
|
|
8
|
+
"./mortgram-observer"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
7
11
|
"keywords": [
|
|
8
12
|
"openclaw",
|
|
9
13
|
"mortgram",
|
|
@@ -11,6 +15,9 @@
|
|
|
11
15
|
"agent",
|
|
12
16
|
"hook"
|
|
13
17
|
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"ably": "^1.2.49"
|
|
20
|
+
},
|
|
14
21
|
"author": "MORTGRAM",
|
|
15
22
|
"license": "MIT"
|
|
16
23
|
}
|