webhookagent 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/LICENSE +21 -0
- package/README.md +84 -0
- package/lib/heartbeat.js +260 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Avni Yayin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @avniyayin/webhookagent
|
|
2
|
+
|
|
3
|
+
Cross-platform Node.js heartbeat agent for [WebhookAgent.com](https://webhookagent.com) — receive webhooks, queue them, process at your pace.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @avniyayin/webhookagent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Sign up at https://webhookagent.com/dashboard and get your API key
|
|
15
|
+
|
|
16
|
+
# Start the heartbeat agent
|
|
17
|
+
webhookagent --key=wha_your_key --auto-restart
|
|
18
|
+
|
|
19
|
+
# Single check
|
|
20
|
+
webhookagent --key=wha_your_key --once
|
|
21
|
+
|
|
22
|
+
# Fetch + auto-ack items
|
|
23
|
+
webhookagent --key=wha_your_key --auto-restart --fetch --ack
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How It Works (Break-on-Action)
|
|
27
|
+
|
|
28
|
+
1. **Poll** — Agent sends heartbeat every N seconds (default: 30)
|
|
29
|
+
2. **Break** — When events arrive, the polling loop exits
|
|
30
|
+
3. **Process** — Your logic handles the events
|
|
31
|
+
4. **Reconnect** — Agent restarts the heartbeat loop
|
|
32
|
+
|
|
33
|
+
No open ports needed. Works behind any firewall. Events are queued so nothing is lost.
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
| Flag | Default | Description |
|
|
38
|
+
|------|---------|-------------|
|
|
39
|
+
| `--key=KEY` | - | API key (or set `WHA_API_KEY` env var) |
|
|
40
|
+
| `--url=URL` | `https://webhookagent.com/api` | Base API URL |
|
|
41
|
+
| `--interval=N` | 30 | Seconds between polls |
|
|
42
|
+
| `--cycles=N` | 120 | Max polls before exit (~1 hour) |
|
|
43
|
+
| `--once` | - | Single check then exit |
|
|
44
|
+
| `--auto-restart` | - | Never exit — restart after break + max cycles |
|
|
45
|
+
| `--quiet` | - | Only output ACTION lines (for piping) |
|
|
46
|
+
| `--webhooks=IDS` | all | Comma-separated webhook IDs to filter |
|
|
47
|
+
| `--fetch` | - | Auto-fetch full queue items when actions arrive |
|
|
48
|
+
| `--ack` | - | Auto-acknowledge items after outputting them |
|
|
49
|
+
| `--json` | - | Output full heartbeat JSON |
|
|
50
|
+
|
|
51
|
+
## Output Format
|
|
52
|
+
|
|
53
|
+
Status logs go to `stderr`, actions go to `stdout` as `ACTION:{json}` lines.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Pipe to your processor
|
|
57
|
+
webhookagent --key=wha_abc123 --quiet --auto-restart | node my-processor.js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Programmatic Usage
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const { execSync } = require('child_process');
|
|
64
|
+
|
|
65
|
+
// Quick single check
|
|
66
|
+
const result = execSync('webhookagent --key=wha_abc --once --json', { encoding: 'utf8' });
|
|
67
|
+
const data = JSON.parse(result);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Environment Variables
|
|
71
|
+
|
|
72
|
+
- `WHA_API_KEY` — API key (alternative to `--key` flag)
|
|
73
|
+
- `WHA_URL` — Base URL override
|
|
74
|
+
|
|
75
|
+
## Links
|
|
76
|
+
|
|
77
|
+
- [WebhookAgent.com](https://webhookagent.com)
|
|
78
|
+
- [API Docs](https://webhookagent.com/docs)
|
|
79
|
+
- [Dashboard](https://webhookagent.com/dashboard)
|
|
80
|
+
- [@avniyayin](https://x.com/avniyayin)
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
package/lib/heartbeat.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WebhookAgent Heartbeat Runtime v1.0
|
|
4
|
+
* Cross-platform Node.js agent for polling + processing webhook events
|
|
5
|
+
*
|
|
6
|
+
* Inspired by MoltedOpus Agent Runtime v1.4
|
|
7
|
+
* Works on: Windows, macOS, Linux — anywhere Node.js runs
|
|
8
|
+
*
|
|
9
|
+
* HOW IT WORKS (Break-on-Action Pattern):
|
|
10
|
+
* 1. Polls /api/heartbeat every N seconds (default: 30)
|
|
11
|
+
* 2. When the heartbeat returns actions (pending webhook events), it BREAKS
|
|
12
|
+
* 3. Outputs ACTION:{json} lines for your parent process to handle
|
|
13
|
+
* 4. After processing, your parent restarts the heartbeat (or use --auto-restart)
|
|
14
|
+
* 5. This ensures your agent processes events at its own pace
|
|
15
|
+
*
|
|
16
|
+
* USAGE:
|
|
17
|
+
* node heartbeat.js --key=wha_... [options]
|
|
18
|
+
*
|
|
19
|
+
* OPTIONS:
|
|
20
|
+
* --key=KEY API key (or set WHA_API_KEY env var)
|
|
21
|
+
* --url=URL Base URL (default: https://webhookagent.com/api)
|
|
22
|
+
* --interval=N Seconds between polls (default: 30)
|
|
23
|
+
* --cycles=N Max polls before exit (default: 120 = ~1 hour)
|
|
24
|
+
* --once Single heartbeat check, then exit
|
|
25
|
+
* --auto-restart Never exit — restart after break + after max cycles
|
|
26
|
+
* --quiet Only output ACTION lines, no status logs
|
|
27
|
+
* --webhooks=IDS Comma-separated webhook IDs to filter (default: all)
|
|
28
|
+
* --fetch Auto-fetch full queue items when actions arrive
|
|
29
|
+
* --ack Auto-acknowledge items after outputting them
|
|
30
|
+
* --json Output full heartbeat JSON instead of ACTION lines
|
|
31
|
+
*
|
|
32
|
+
* OUTPUT FORMAT:
|
|
33
|
+
* Status lines go to stderr (so you can pipe stdout cleanly)
|
|
34
|
+
* Actions print to stdout as: ACTION:{json}
|
|
35
|
+
*
|
|
36
|
+
* EXAMPLES:
|
|
37
|
+
* # Simple poll loop
|
|
38
|
+
* node heartbeat.js --key=wha_abc123
|
|
39
|
+
*
|
|
40
|
+
* # Continuous with auto-restart
|
|
41
|
+
* node heartbeat.js --key=wha_abc123 --auto-restart
|
|
42
|
+
*
|
|
43
|
+
* # Single check, fetch items, auto-ack
|
|
44
|
+
* node heartbeat.js --key=wha_abc123 --once --fetch --ack
|
|
45
|
+
*
|
|
46
|
+
* # Pipe actions to another script
|
|
47
|
+
* node heartbeat.js --key=wha_abc123 --quiet --auto-restart | node my-processor.js
|
|
48
|
+
*
|
|
49
|
+
* # Filter specific webhooks
|
|
50
|
+
* node heartbeat.js --key=wha_abc123 --webhooks=wh-abc-123,wh-def-456
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// CONFIG
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
const args = parseArgs(process.argv.slice(2));
|
|
58
|
+
const API_KEY = args.key || process.env.WHA_API_KEY || '';
|
|
59
|
+
const BASE_URL = (args.url || process.env.WHA_URL || 'https://webhookagent.com/api').replace(/\/$/, '');
|
|
60
|
+
const INTERVAL = parseInt(args.interval || '30') * 1000;
|
|
61
|
+
const MAX_CYCLES = args.once ? 1 : parseInt(args.cycles || '120');
|
|
62
|
+
const AUTO_RESTART = !!args['auto-restart'];
|
|
63
|
+
const QUIET = !!args.quiet;
|
|
64
|
+
const WEBHOOKS_FILTER = args.webhooks || 'all';
|
|
65
|
+
const AUTO_FETCH = !!args.fetch;
|
|
66
|
+
const AUTO_ACK = !!args.ack;
|
|
67
|
+
const JSON_MODE = !!args.json;
|
|
68
|
+
|
|
69
|
+
if (!API_KEY) {
|
|
70
|
+
console.error('ERROR: API key required. Use --key=wha_... or set WHA_API_KEY env var');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// HELPERS
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
function parseArgs(argv) {
|
|
79
|
+
const result = {};
|
|
80
|
+
for (const arg of argv) {
|
|
81
|
+
if (arg.startsWith('--')) {
|
|
82
|
+
const [key, ...valParts] = arg.slice(2).split('=');
|
|
83
|
+
result[key] = valParts.length ? valParts.join('=') : true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function log(msg) {
|
|
90
|
+
if (QUIET) return;
|
|
91
|
+
process.stderr.write(`${new Date().toLocaleTimeString()} ${msg}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function api(path, method = 'GET', body = null) {
|
|
95
|
+
const headers = {
|
|
96
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
'User-Agent': 'WebhookAgent-Heartbeat/1.0 (Node.js)',
|
|
99
|
+
};
|
|
100
|
+
const opts = { method, headers };
|
|
101
|
+
if (body) opts.body = JSON.stringify(body);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${BASE_URL}${path}`, opts);
|
|
105
|
+
const data = await res.json();
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
log(`ERROR: HTTP ${res.status} on ${method} ${path}: ${data.error || 'Unknown'}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log(`ERROR: ${err.message}`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sleep(ms) {
|
|
118
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================
|
|
122
|
+
// ACTION PROCESSING
|
|
123
|
+
// ============================================================
|
|
124
|
+
|
|
125
|
+
async function processActions(actions, heartbeatData) {
|
|
126
|
+
if (JSON_MODE) {
|
|
127
|
+
console.log(JSON.stringify(heartbeatData));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const action of actions) {
|
|
132
|
+
const whId = action.webhook_id;
|
|
133
|
+
const whName = action.webhook_name;
|
|
134
|
+
const pending = action.pending;
|
|
135
|
+
|
|
136
|
+
log(` >> ${whName}: ${pending} pending events`);
|
|
137
|
+
|
|
138
|
+
if (AUTO_FETCH) {
|
|
139
|
+
// Fetch full queue items
|
|
140
|
+
const queue = await api(`/webhooks/${whId}/queue?status=pending&limit=50`);
|
|
141
|
+
if (queue && queue.items) {
|
|
142
|
+
const ids = [];
|
|
143
|
+
for (const item of queue.items) {
|
|
144
|
+
ids.push(item.id);
|
|
145
|
+
const output = {
|
|
146
|
+
type: 'webhook_event',
|
|
147
|
+
webhook_id: whId,
|
|
148
|
+
webhook_name: whName,
|
|
149
|
+
item_id: item.id,
|
|
150
|
+
method: item.method,
|
|
151
|
+
content_type: item.content_type,
|
|
152
|
+
headers: item.headers,
|
|
153
|
+
body: item.body_json || item.body,
|
|
154
|
+
query_params: item.query_params,
|
|
155
|
+
source_ip: item.source_ip,
|
|
156
|
+
received_at: item.received_at,
|
|
157
|
+
};
|
|
158
|
+
console.log('ACTION:' + JSON.stringify(output));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (AUTO_ACK && ids.length > 0) {
|
|
162
|
+
await api(`/webhooks/${whId}/queue/ack`, 'POST', { item_ids: ids });
|
|
163
|
+
log(` >> Acked ${ids.length} items from ${whName}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Just output the action summary with preview
|
|
168
|
+
console.log('ACTION:' + JSON.stringify({
|
|
169
|
+
type: 'webhook_events',
|
|
170
|
+
webhook_id: whId,
|
|
171
|
+
webhook_name: whName,
|
|
172
|
+
pending: pending,
|
|
173
|
+
items_preview: action.items_preview || [],
|
|
174
|
+
fetch: action.fetch,
|
|
175
|
+
ack: action.ack,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================
|
|
182
|
+
// MAIN HEARTBEAT LOOP
|
|
183
|
+
// ============================================================
|
|
184
|
+
|
|
185
|
+
async function main() {
|
|
186
|
+
log('WebhookAgent Heartbeat Runtime v1.0');
|
|
187
|
+
log(`Polling ${BASE_URL} every ${INTERVAL / 1000}s, max ${MAX_CYCLES} cycles${AUTO_RESTART ? ' (continuous)' : ''}`);
|
|
188
|
+
if (WEBHOOKS_FILTER !== 'all') log(`Filtering webhooks: ${WEBHOOKS_FILTER}`);
|
|
189
|
+
if (AUTO_FETCH) log('Auto-fetch: ON');
|
|
190
|
+
if (AUTO_ACK) log('Auto-ack: ON');
|
|
191
|
+
log('---');
|
|
192
|
+
|
|
193
|
+
do {
|
|
194
|
+
let retries = 0;
|
|
195
|
+
let brokeOnAction = false;
|
|
196
|
+
|
|
197
|
+
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
198
|
+
const path = `/heartbeat?webhooks=${encodeURIComponent(WEBHOOKS_FILTER)}`;
|
|
199
|
+
const data = await api(path);
|
|
200
|
+
|
|
201
|
+
if (!data) {
|
|
202
|
+
retries++;
|
|
203
|
+
log(`WARN: heartbeat failed (retry ${retries}/3)`);
|
|
204
|
+
if (retries >= 3) {
|
|
205
|
+
log('FATAL: 3 consecutive failures, exiting');
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
await sleep(10000);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
retries = 0;
|
|
212
|
+
|
|
213
|
+
const user = data.user || {};
|
|
214
|
+
const summary = data.summary || {};
|
|
215
|
+
const actions = data.actions || [];
|
|
216
|
+
|
|
217
|
+
if (actions.length === 0) {
|
|
218
|
+
log(`ok | plan=${user.plan} events=${user.events_used}/${user.events_limit} (${user.events_remaining} left) | pending=${summary.total_pending}`);
|
|
219
|
+
} else {
|
|
220
|
+
log(`BREAK | ${actions.length} webhook(s) with pending events | plan=${user.plan} events_left=${user.events_remaining}`);
|
|
221
|
+
|
|
222
|
+
await processActions(actions, data);
|
|
223
|
+
|
|
224
|
+
brokeOnAction = true;
|
|
225
|
+
break; // ← THE BREAK — exit loop so parent process can handle
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (cycle < MAX_CYCLES) {
|
|
229
|
+
await sleep(INTERVAL);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!brokeOnAction) {
|
|
234
|
+
log(`Max cycles reached (${MAX_CYCLES}), exiting cleanly`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (AUTO_RESTART) {
|
|
238
|
+
const wait = brokeOnAction ? 5000 : INTERVAL;
|
|
239
|
+
log(`Auto-restart: sleeping ${wait / 1000}s...`);
|
|
240
|
+
await sleep(wait);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
} while (AUTO_RESTART);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Handle clean exit
|
|
247
|
+
process.on('SIGINT', () => {
|
|
248
|
+
log('Interrupted, shutting down...');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
process.on('SIGTERM', () => {
|
|
253
|
+
log('Terminated, shutting down...');
|
|
254
|
+
process.exit(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
main().catch(err => {
|
|
258
|
+
console.error('Fatal error:', err);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webhookagent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WebhookAgent heartbeat runtime — poll, break, process webhook events at your agent's pace",
|
|
5
|
+
"main": "lib/heartbeat.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"webhookagent": "lib/heartbeat.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node lib/heartbeat.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"webhook",
|
|
14
|
+
"agent",
|
|
15
|
+
"heartbeat",
|
|
16
|
+
"queue",
|
|
17
|
+
"polling",
|
|
18
|
+
"break-on-action",
|
|
19
|
+
"ai-agent",
|
|
20
|
+
"webhook-queue",
|
|
21
|
+
"event-processing"
|
|
22
|
+
],
|
|
23
|
+
"author": "Avni Yayin <avni.yayin@gmail.com>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"homepage": "https://webhookagent.com",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/avniyayin/webhookagent"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
]
|
|
38
|
+
}
|