webhookagent 1.1.0 → 1.3.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 +11 -7
- package/lib/heartbeat.js +84 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,14 +13,17 @@ npm install -g @avniyayin/webhookagent
|
|
|
13
13
|
```bash
|
|
14
14
|
# Sign up at https://webhookagent.com/dashboard and get your API key
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
webhookagent --key=wha_your_key
|
|
16
|
+
# Save your key (one-time — loads automatically after this)
|
|
17
|
+
webhookagent config --key=wha_your_key
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
webhookagent --
|
|
19
|
+
# Start heartbeat — polls, breaks on events, outputs ACTION:{json}, exits
|
|
20
|
+
webhookagent --fetch --ack
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
webhookagent --
|
|
22
|
+
# Single check (one poll, then exit)
|
|
23
|
+
webhookagent --once
|
|
24
|
+
|
|
25
|
+
# Continuous monitoring (for dashboards/debugging, not AI agents)
|
|
26
|
+
webhookagent --auto-restart --fetch --ack
|
|
24
27
|
```
|
|
25
28
|
|
|
26
29
|
## How It Works (Break-on-Action)
|
|
@@ -38,10 +41,11 @@ No open ports needed. Works behind any firewall. Events are queued so nothing is
|
|
|
38
41
|
|------|---------|-------------|
|
|
39
42
|
| `--key=KEY` | - | API key (or set `WHA_API_KEY` env var) |
|
|
40
43
|
| `--url=URL` | `https://webhookagent.com/api` | Base API URL |
|
|
41
|
-
| `--interval=N` | 30 | Seconds between polls |
|
|
44
|
+
| `--interval=N` | 30 | Seconds between polls (auto-adjusts to plan minimum) |
|
|
42
45
|
| `--cycles=N` | 120 | Max polls before exit (~1 hour) |
|
|
43
46
|
| `--once` | - | Single check then exit |
|
|
44
47
|
| `--auto-restart` | - | Never exit — restart after break + max cycles |
|
|
48
|
+
| `--show` | - | Show events without breaking (debug/monitor mode) |
|
|
45
49
|
| `--quiet` | - | Only output ACTION lines (for piping) |
|
|
46
50
|
| `--webhooks=IDS` | all | Comma-separated webhook IDs to filter |
|
|
47
51
|
| `--fetch` | - | Auto-fetch full queue items when actions arrive |
|
package/lib/heartbeat.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* --key=KEY API key (or set WHA_API_KEY env var)
|
|
21
21
|
* --url=URL Base URL (default: https://webhookagent.com/api)
|
|
22
22
|
* --interval=N Seconds between polls (default: 30)
|
|
23
|
-
* --cycles=N Max polls before exit (default:
|
|
23
|
+
* --cycles=N Max polls before exit (default: unlimited)
|
|
24
24
|
* --once Single heartbeat check, then exit
|
|
25
25
|
* --auto-restart Never exit — restart after break + after max cycles
|
|
26
26
|
* --quiet Only output ACTION lines, no status logs
|
|
@@ -60,7 +60,7 @@ const path = require('path');
|
|
|
60
60
|
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.webhookagent');
|
|
61
61
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
62
62
|
|
|
63
|
-
// Handle
|
|
63
|
+
// Handle subcommands
|
|
64
64
|
const rawArgs = process.argv.slice(2);
|
|
65
65
|
if (rawArgs[0] === 'config') {
|
|
66
66
|
handleConfig(rawArgs.slice(1));
|
|
@@ -69,19 +69,57 @@ if (rawArgs[0] === 'config') {
|
|
|
69
69
|
|
|
70
70
|
const args = parseArgs(rawArgs);
|
|
71
71
|
|
|
72
|
+
if (args.help || args.h || rawArgs[0] === 'help') {
|
|
73
|
+
console.log(`WebhookAgent Heartbeat Runtime v1.2
|
|
74
|
+
|
|
75
|
+
Usage: webhookagent [options]
|
|
76
|
+
webhookagent config --key=wha_...
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--key=KEY API key (or save with: webhookagent config --key=wha_...)
|
|
80
|
+
--interval=N Seconds between polls (auto-adjusts to plan minimum)
|
|
81
|
+
--cycles=N Max polls before exit (default: unlimited)
|
|
82
|
+
--once Single heartbeat check, then exit
|
|
83
|
+
--auto-restart Never exit — restart after break + after max cycles
|
|
84
|
+
--show Show events without breaking (debug/monitor mode)
|
|
85
|
+
--fetch Auto-fetch full queue items when actions arrive
|
|
86
|
+
--ack Auto-acknowledge items after outputting them
|
|
87
|
+
--webhooks=IDS Comma-separated webhook IDs or names to filter
|
|
88
|
+
--json Output full heartbeat JSON instead of ACTION lines
|
|
89
|
+
--quiet Only output ACTION lines, no status logs
|
|
90
|
+
--url=URL API base URL (default: https://webhookagent.com/api)
|
|
91
|
+
|
|
92
|
+
Config:
|
|
93
|
+
webhookagent config --key=wha_... Save API key (recommended)
|
|
94
|
+
webhookagent config --webhooks=ID Save webhook filter
|
|
95
|
+
webhookagent config --show Show saved config
|
|
96
|
+
webhookagent config --clear Delete saved config
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
webhookagent --fetch --ack Poll, fetch events, auto-ack
|
|
100
|
+
webhookagent --once --fetch Single check, fetch items
|
|
101
|
+
webhookagent --auto-restart --fetch --ack Continuous loop
|
|
102
|
+
webhookagent --quiet --fetch | node app.js Pipe to your processor
|
|
103
|
+
|
|
104
|
+
Docs: https://webhookagent.com/docs`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
// Load saved config (if exists)
|
|
73
109
|
const savedConfig = loadConfig();
|
|
74
110
|
|
|
75
111
|
const API_KEY = args.key || process.env.WHA_API_KEY || savedConfig.api_key || '';
|
|
76
112
|
const BASE_URL = (args.url || process.env.WHA_URL || savedConfig.api_url || 'https://webhookagent.com/api').replace(/\/$/, '');
|
|
77
|
-
const
|
|
78
|
-
|
|
113
|
+
const USER_INTERVAL = args.interval ? parseInt(args.interval) * 1000 : null;
|
|
114
|
+
let INTERVAL = USER_INTERVAL || 30000;
|
|
115
|
+
const MAX_CYCLES = args.once ? 1 : (args.cycles ? parseInt(args.cycles) : Infinity);
|
|
79
116
|
const AUTO_RESTART = !!args['auto-restart'];
|
|
80
117
|
const QUIET = !!args.quiet;
|
|
81
118
|
const WEBHOOKS_FILTER = args.webhooks || savedConfig.webhooks || 'all';
|
|
82
119
|
const AUTO_FETCH = !!args.fetch;
|
|
83
120
|
const AUTO_ACK = !!args.ack;
|
|
84
121
|
const JSON_MODE = !!args.json;
|
|
122
|
+
const SHOW_MODE = !!args.show;
|
|
85
123
|
|
|
86
124
|
if (!API_KEY) {
|
|
87
125
|
console.error('ERROR: API key required.');
|
|
@@ -186,6 +224,7 @@ function buildRestartCommand() {
|
|
|
186
224
|
if (AUTO_ACK) parts.push('--ack');
|
|
187
225
|
if (WEBHOOKS_FILTER !== 'all' && args.webhooks) parts.push(`--webhooks=${WEBHOOKS_FILTER}`);
|
|
188
226
|
if (args.interval) parts.push(`--interval=${args.interval}`);
|
|
227
|
+
if (SHOW_MODE) parts.push('--show');
|
|
189
228
|
if (QUIET) parts.push('--quiet');
|
|
190
229
|
return parts.join(' ');
|
|
191
230
|
}
|
|
@@ -199,7 +238,7 @@ async function api(path, method = 'GET', body = null) {
|
|
|
199
238
|
const headers = {
|
|
200
239
|
'Authorization': `Bearer ${API_KEY}`,
|
|
201
240
|
'Content-Type': 'application/json',
|
|
202
|
-
'User-Agent': 'WebhookAgent-Heartbeat/1.
|
|
241
|
+
'User-Agent': 'WebhookAgent-Heartbeat/1.2 (Node.js)',
|
|
203
242
|
};
|
|
204
243
|
const opts = { method, headers };
|
|
205
244
|
if (body) opts.body = JSON.stringify(body);
|
|
@@ -207,6 +246,11 @@ async function api(path, method = 'GET', body = null) {
|
|
|
207
246
|
try {
|
|
208
247
|
const res = await fetch(`${BASE_URL}${path}`, opts);
|
|
209
248
|
const data = await res.json();
|
|
249
|
+
if (res.status === 429 && data.retry_after) {
|
|
250
|
+
log(`RATE LIMITED: ${data.error || 'Too fast'}. Waiting ${data.retry_after}s...`);
|
|
251
|
+
await sleep(data.retry_after * 1000);
|
|
252
|
+
return { _rate_limited: true };
|
|
253
|
+
}
|
|
210
254
|
if (!res.ok) {
|
|
211
255
|
log(`ERROR: HTTP ${res.status} on ${method} ${path}: ${data.error || 'Unknown'}`);
|
|
212
256
|
return null;
|
|
@@ -259,12 +303,25 @@ async function processActions(actions, heartbeatData) {
|
|
|
259
303
|
source_ip: item.source_ip,
|
|
260
304
|
received_at: item.received_at,
|
|
261
305
|
};
|
|
306
|
+
// When --ack is not used, include acknowledge instructions with each item
|
|
307
|
+
if (!AUTO_ACK) {
|
|
308
|
+
output.needs_ack = true;
|
|
309
|
+
output.ack_hint = `After you've handled this event, mark it as done so it leaves the queue.`;
|
|
310
|
+
output.ack_command = `curl -s -X POST "${BASE_URL}/webhooks/${whId}/queue/ack" -H "Authorization: Bearer ${API_KEY}" -H "Content-Type: application/json" -d '{"item_ids":[${item.id}]}'`;
|
|
311
|
+
}
|
|
262
312
|
console.log('ACTION:' + JSON.stringify(output));
|
|
263
313
|
}
|
|
264
314
|
|
|
265
315
|
if (AUTO_ACK && ids.length > 0) {
|
|
266
316
|
await api(`/webhooks/${whId}/queue/ack`, 'POST', { item_ids: ids });
|
|
267
317
|
log(` >> Acked ${ids.length} items from ${whName}`);
|
|
318
|
+
} else if (!AUTO_ACK && ids.length > 0) {
|
|
319
|
+
log(` >> ${ids.length} items fetched from ${whName} — remember to acknowledge them when done`);
|
|
320
|
+
console.log('ACK_ALL:' + JSON.stringify({
|
|
321
|
+
hint: `You have ${ids.length} items to process. Once you're done with each one, acknowledge it so it's cleared from the queue. You can ack them all at once:`,
|
|
322
|
+
command: `curl -s -X POST "${BASE_URL}/webhooks/${whId}/queue/ack" -H "Authorization: Bearer ${API_KEY}" -H "Content-Type: application/json" -d '{"item_ids":[${ids.join(',')}]}'`,
|
|
323
|
+
item_ids: ids
|
|
324
|
+
}));
|
|
268
325
|
}
|
|
269
326
|
}
|
|
270
327
|
} else {
|
|
@@ -275,6 +332,7 @@ async function processActions(actions, heartbeatData) {
|
|
|
275
332
|
webhook_name: whName,
|
|
276
333
|
pending: pending,
|
|
277
334
|
items_preview: action.items_preview || [],
|
|
335
|
+
fetch_hint: `There are ${pending} events waiting. Fetch the full payloads, process them, and acknowledge when done.`,
|
|
278
336
|
fetch: action.fetch,
|
|
279
337
|
ack: action.ack,
|
|
280
338
|
}));
|
|
@@ -287,8 +345,8 @@ async function processActions(actions, heartbeatData) {
|
|
|
287
345
|
// ============================================================
|
|
288
346
|
|
|
289
347
|
async function main() {
|
|
290
|
-
log('WebhookAgent Heartbeat Runtime v1.
|
|
291
|
-
log(`Polling ${BASE_URL} every ${INTERVAL / 1000}s
|
|
348
|
+
log('WebhookAgent Heartbeat Runtime v1.3');
|
|
349
|
+
log(`Polling ${BASE_URL} every ${INTERVAL / 1000}s${MAX_CYCLES === Infinity ? '' : `, max ${MAX_CYCLES} cycles`}${AUTO_RESTART ? ' (continuous)' : ''}${SHOW_MODE ? ' (show mode — no break)' : ''}`);
|
|
292
350
|
|
|
293
351
|
// Resolve webhook names to IDs if needed
|
|
294
352
|
let resolvedFilter = WEBHOOKS_FILTER;
|
|
@@ -315,6 +373,7 @@ async function main() {
|
|
|
315
373
|
}
|
|
316
374
|
log(`Filtering webhooks: ${resolvedFilter}`);
|
|
317
375
|
}
|
|
376
|
+
if (SHOW_MODE) log('Show mode: ON (events displayed, no break)');
|
|
318
377
|
if (AUTO_FETCH) log('Auto-fetch: ON');
|
|
319
378
|
if (AUTO_ACK) log('Auto-ack: ON');
|
|
320
379
|
log('---');
|
|
@@ -337,14 +396,31 @@ async function main() {
|
|
|
337
396
|
await sleep(10000);
|
|
338
397
|
continue;
|
|
339
398
|
}
|
|
399
|
+
if (data._rate_limited) { continue; }
|
|
340
400
|
retries = 0;
|
|
341
401
|
|
|
342
402
|
const user = data.user || {};
|
|
343
403
|
const summary = data.summary || {};
|
|
344
404
|
const actions = data.actions || [];
|
|
345
405
|
|
|
406
|
+
// Auto-adjust poll interval from server recommendation
|
|
407
|
+
if (user.poll_interval && user.poll_interval > 0) {
|
|
408
|
+
const serverInterval = user.poll_interval * 1000;
|
|
409
|
+
if (USER_INTERVAL && USER_INTERVAL < serverInterval) {
|
|
410
|
+
log(`WARN: --interval=${USER_INTERVAL / 1000}s is below plan minimum (${user.poll_interval}s). Using server value.`);
|
|
411
|
+
INTERVAL = serverInterval;
|
|
412
|
+
} else if (!USER_INTERVAL && serverInterval !== INTERVAL) {
|
|
413
|
+
INTERVAL = serverInterval;
|
|
414
|
+
log(`Poll interval adjusted to ${user.poll_interval}s (${user.plan} plan)`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
346
418
|
if (actions.length === 0) {
|
|
347
419
|
log(`ok | plan=${user.plan} events=${user.events_used}/${user.events_limit} (${user.events_remaining} left) | pending=${summary.total_pending}`);
|
|
420
|
+
} else if (SHOW_MODE) {
|
|
421
|
+
// Show mode: display events but keep polling (no break)
|
|
422
|
+
log(`SHOW | ${actions.length} webhook(s) with pending events | plan=${user.plan} events_left=${user.events_remaining}`);
|
|
423
|
+
await processActions(actions, data);
|
|
348
424
|
} else {
|
|
349
425
|
log(`BREAK | ${actions.length} webhook(s) with pending events | plan=${user.plan} events_left=${user.events_remaining}`);
|
|
350
426
|
|
|
@@ -367,7 +443,7 @@ async function main() {
|
|
|
367
443
|
}
|
|
368
444
|
}
|
|
369
445
|
|
|
370
|
-
if (!brokeOnAction) {
|
|
446
|
+
if (!brokeOnAction && MAX_CYCLES !== Infinity) {
|
|
371
447
|
log(`Max cycles reached (${MAX_CYCLES}), exiting cleanly`);
|
|
372
448
|
}
|
|
373
449
|
|