webhookagent 1.0.1 → 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 +227 -14
- 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
|
|
@@ -54,20 +54,78 @@
|
|
|
54
54
|
// CONFIG
|
|
55
55
|
// ============================================================
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
57
|
+
const fs = require('fs');
|
|
58
|
+
const path = require('path');
|
|
59
|
+
|
|
60
|
+
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.webhookagent');
|
|
61
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
62
|
+
|
|
63
|
+
// Handle subcommands
|
|
64
|
+
const rawArgs = process.argv.slice(2);
|
|
65
|
+
if (rawArgs[0] === 'config') {
|
|
66
|
+
handleConfig(rawArgs.slice(1));
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const args = parseArgs(rawArgs);
|
|
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
|
+
|
|
108
|
+
// Load saved config (if exists)
|
|
109
|
+
const savedConfig = loadConfig();
|
|
110
|
+
|
|
111
|
+
const API_KEY = args.key || process.env.WHA_API_KEY || savedConfig.api_key || '';
|
|
112
|
+
const BASE_URL = (args.url || process.env.WHA_URL || savedConfig.api_url || 'https://webhookagent.com/api').replace(/\/$/, '');
|
|
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);
|
|
62
116
|
const AUTO_RESTART = !!args['auto-restart'];
|
|
63
117
|
const QUIET = !!args.quiet;
|
|
64
|
-
const WEBHOOKS_FILTER = args.webhooks || 'all';
|
|
118
|
+
const WEBHOOKS_FILTER = args.webhooks || savedConfig.webhooks || 'all';
|
|
65
119
|
const AUTO_FETCH = !!args.fetch;
|
|
66
120
|
const AUTO_ACK = !!args.ack;
|
|
67
121
|
const JSON_MODE = !!args.json;
|
|
122
|
+
const SHOW_MODE = !!args.show;
|
|
68
123
|
|
|
69
124
|
if (!API_KEY) {
|
|
70
|
-
console.error('ERROR: API key required.
|
|
125
|
+
console.error('ERROR: API key required.');
|
|
126
|
+
console.error(' Option 1: webhookagent config --key=wha_... (saves securely, recommended)');
|
|
127
|
+
console.error(' Option 2: webhookagent --key=wha_... (passed each time)');
|
|
128
|
+
console.error(' Option 3: set WHA_API_KEY env var');
|
|
71
129
|
process.exit(1);
|
|
72
130
|
}
|
|
73
131
|
|
|
@@ -86,6 +144,91 @@ function parseArgs(argv) {
|
|
|
86
144
|
return result;
|
|
87
145
|
}
|
|
88
146
|
|
|
147
|
+
function loadConfig() {
|
|
148
|
+
try {
|
|
149
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
150
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
151
|
+
}
|
|
152
|
+
} catch (e) { /* ignore */ }
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function saveConfig(data) {
|
|
157
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
158
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
const existing = loadConfig();
|
|
161
|
+
const merged = { ...existing, ...data };
|
|
162
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n');
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleConfig(argv) {
|
|
167
|
+
const configArgs = parseArgs(argv);
|
|
168
|
+
|
|
169
|
+
// webhookagent config --key=wha_...
|
|
170
|
+
if (configArgs.key) {
|
|
171
|
+
saveConfig({ api_key: configArgs.key });
|
|
172
|
+
console.log(`API key saved to ${CONFIG_FILE}`);
|
|
173
|
+
console.log('You can now run: webhookagent --fetch --ack');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// webhookagent config --webhooks=wh-abc,wh-def
|
|
178
|
+
if (configArgs.webhooks) {
|
|
179
|
+
saveConfig({ webhooks: configArgs.webhooks });
|
|
180
|
+
console.log(`Webhook filter saved: ${configArgs.webhooks}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// webhookagent config --url=https://...
|
|
185
|
+
if (configArgs.url) {
|
|
186
|
+
saveConfig({ api_url: configArgs.url });
|
|
187
|
+
console.log(`API URL saved: ${configArgs.url}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// webhookagent config --show
|
|
192
|
+
if (configArgs.show) {
|
|
193
|
+
const cfg = loadConfig();
|
|
194
|
+
if (Object.keys(cfg).length === 0) {
|
|
195
|
+
console.log('No config saved. Use: webhookagent config --key=wha_...');
|
|
196
|
+
} else {
|
|
197
|
+
const display = { ...cfg };
|
|
198
|
+
if (display.api_key) display.api_key = display.api_key.slice(0, 8) + '...' + display.api_key.slice(-4);
|
|
199
|
+
console.log(JSON.stringify(display, null, 2));
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// webhookagent config --clear
|
|
205
|
+
if (configArgs.clear) {
|
|
206
|
+
try { fs.unlinkSync(CONFIG_FILE); } catch (e) { /* ignore */ }
|
|
207
|
+
console.log('Config cleared.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// No args — show help
|
|
212
|
+
console.log('WebhookAgent Config');
|
|
213
|
+
console.log(' webhookagent config --key=wha_... Save API key');
|
|
214
|
+
console.log(' webhookagent config --webhooks=ID1,ID2 Save webhook filter');
|
|
215
|
+
console.log(' webhookagent config --show Show saved config');
|
|
216
|
+
console.log(' webhookagent config --clear Delete saved config');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildRestartCommand() {
|
|
220
|
+
const parts = ['webhookagent'];
|
|
221
|
+
// Only include --key if it was passed explicitly (not from config/env)
|
|
222
|
+
if (args.key) parts.push(`--key=${args.key}`);
|
|
223
|
+
if (AUTO_FETCH) parts.push('--fetch');
|
|
224
|
+
if (AUTO_ACK) parts.push('--ack');
|
|
225
|
+
if (WEBHOOKS_FILTER !== 'all' && args.webhooks) parts.push(`--webhooks=${WEBHOOKS_FILTER}`);
|
|
226
|
+
if (args.interval) parts.push(`--interval=${args.interval}`);
|
|
227
|
+
if (SHOW_MODE) parts.push('--show');
|
|
228
|
+
if (QUIET) parts.push('--quiet');
|
|
229
|
+
return parts.join(' ');
|
|
230
|
+
}
|
|
231
|
+
|
|
89
232
|
function log(msg) {
|
|
90
233
|
if (QUIET) return;
|
|
91
234
|
process.stderr.write(`${new Date().toLocaleTimeString()} ${msg}\n`);
|
|
@@ -95,7 +238,7 @@ async function api(path, method = 'GET', body = null) {
|
|
|
95
238
|
const headers = {
|
|
96
239
|
'Authorization': `Bearer ${API_KEY}`,
|
|
97
240
|
'Content-Type': 'application/json',
|
|
98
|
-
'User-Agent': 'WebhookAgent-Heartbeat/1.
|
|
241
|
+
'User-Agent': 'WebhookAgent-Heartbeat/1.2 (Node.js)',
|
|
99
242
|
};
|
|
100
243
|
const opts = { method, headers };
|
|
101
244
|
if (body) opts.body = JSON.stringify(body);
|
|
@@ -103,6 +246,11 @@ async function api(path, method = 'GET', body = null) {
|
|
|
103
246
|
try {
|
|
104
247
|
const res = await fetch(`${BASE_URL}${path}`, opts);
|
|
105
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
|
+
}
|
|
106
254
|
if (!res.ok) {
|
|
107
255
|
log(`ERROR: HTTP ${res.status} on ${method} ${path}: ${data.error || 'Unknown'}`);
|
|
108
256
|
return null;
|
|
@@ -155,12 +303,25 @@ async function processActions(actions, heartbeatData) {
|
|
|
155
303
|
source_ip: item.source_ip,
|
|
156
304
|
received_at: item.received_at,
|
|
157
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
|
+
}
|
|
158
312
|
console.log('ACTION:' + JSON.stringify(output));
|
|
159
313
|
}
|
|
160
314
|
|
|
161
315
|
if (AUTO_ACK && ids.length > 0) {
|
|
162
316
|
await api(`/webhooks/${whId}/queue/ack`, 'POST', { item_ids: ids });
|
|
163
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
|
+
}));
|
|
164
325
|
}
|
|
165
326
|
}
|
|
166
327
|
} else {
|
|
@@ -171,6 +332,7 @@ async function processActions(actions, heartbeatData) {
|
|
|
171
332
|
webhook_name: whName,
|
|
172
333
|
pending: pending,
|
|
173
334
|
items_preview: action.items_preview || [],
|
|
335
|
+
fetch_hint: `There are ${pending} events waiting. Fetch the full payloads, process them, and acknowledge when done.`,
|
|
174
336
|
fetch: action.fetch,
|
|
175
337
|
ack: action.ack,
|
|
176
338
|
}));
|
|
@@ -183,9 +345,35 @@ async function processActions(actions, heartbeatData) {
|
|
|
183
345
|
// ============================================================
|
|
184
346
|
|
|
185
347
|
async function main() {
|
|
186
|
-
log('WebhookAgent Heartbeat Runtime v1.
|
|
187
|
-
log(`Polling ${BASE_URL} every ${INTERVAL / 1000}s
|
|
188
|
-
|
|
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)' : ''}`);
|
|
350
|
+
|
|
351
|
+
// Resolve webhook names to IDs if needed
|
|
352
|
+
let resolvedFilter = WEBHOOKS_FILTER;
|
|
353
|
+
if (WEBHOOKS_FILTER !== 'all') {
|
|
354
|
+
const filterParts = WEBHOOKS_FILTER.split(',').map(s => s.trim());
|
|
355
|
+
const hasNames = filterParts.some(p => !p.startsWith('wh-') && !/^\d+$/.test(p));
|
|
356
|
+
if (hasNames) {
|
|
357
|
+
const webhooks = await api('/webhooks');
|
|
358
|
+
if (webhooks && webhooks.webhooks) {
|
|
359
|
+
const resolved = filterParts.map(f => {
|
|
360
|
+
// If it looks like an ID already, keep it
|
|
361
|
+
if (f.startsWith('wh-') || /^\d+$/.test(f)) return f;
|
|
362
|
+
// Otherwise match by name (case-insensitive)
|
|
363
|
+
const match = webhooks.webhooks.find(w => w.name.toLowerCase() === f.toLowerCase());
|
|
364
|
+
if (match) {
|
|
365
|
+
log(`Resolved "${f}" → ${match.webhook_id}`);
|
|
366
|
+
return match.webhook_id;
|
|
367
|
+
}
|
|
368
|
+
log(`WARN: webhook name "${f}" not found, skipping`);
|
|
369
|
+
return null;
|
|
370
|
+
}).filter(Boolean);
|
|
371
|
+
resolvedFilter = resolved.join(',');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
log(`Filtering webhooks: ${resolvedFilter}`);
|
|
375
|
+
}
|
|
376
|
+
if (SHOW_MODE) log('Show mode: ON (events displayed, no break)');
|
|
189
377
|
if (AUTO_FETCH) log('Auto-fetch: ON');
|
|
190
378
|
if (AUTO_ACK) log('Auto-ack: ON');
|
|
191
379
|
log('---');
|
|
@@ -195,7 +383,7 @@ async function main() {
|
|
|
195
383
|
let brokeOnAction = false;
|
|
196
384
|
|
|
197
385
|
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
198
|
-
const path = `/heartbeat?webhooks=${encodeURIComponent(
|
|
386
|
+
const path = `/heartbeat?webhooks=${encodeURIComponent(resolvedFilter)}`;
|
|
199
387
|
const data = await api(path);
|
|
200
388
|
|
|
201
389
|
if (!data) {
|
|
@@ -208,20 +396,45 @@ async function main() {
|
|
|
208
396
|
await sleep(10000);
|
|
209
397
|
continue;
|
|
210
398
|
}
|
|
399
|
+
if (data._rate_limited) { continue; }
|
|
211
400
|
retries = 0;
|
|
212
401
|
|
|
213
402
|
const user = data.user || {};
|
|
214
403
|
const summary = data.summary || {};
|
|
215
404
|
const actions = data.actions || [];
|
|
216
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
|
+
|
|
217
418
|
if (actions.length === 0) {
|
|
218
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);
|
|
219
424
|
} else {
|
|
220
425
|
log(`BREAK | ${actions.length} webhook(s) with pending events | plan=${user.plan} events_left=${user.events_remaining}`);
|
|
221
426
|
|
|
222
427
|
await processActions(actions, data);
|
|
223
428
|
|
|
224
429
|
brokeOnAction = true;
|
|
430
|
+
|
|
431
|
+
// Tell the agent exactly how to restart
|
|
432
|
+
if (!AUTO_RESTART) {
|
|
433
|
+
const cmd = buildRestartCommand();
|
|
434
|
+
console.log('RESTART:' + cmd);
|
|
435
|
+
log(`Process events above, then run: ${cmd}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
225
438
|
break; // ← THE BREAK — exit loop so parent process can handle
|
|
226
439
|
}
|
|
227
440
|
|
|
@@ -230,7 +443,7 @@ async function main() {
|
|
|
230
443
|
}
|
|
231
444
|
}
|
|
232
445
|
|
|
233
|
-
if (!brokeOnAction) {
|
|
446
|
+
if (!brokeOnAction && MAX_CYCLES !== Infinity) {
|
|
234
447
|
log(`Max cycles reached (${MAX_CYCLES}), exiting cleanly`);
|
|
235
448
|
}
|
|
236
449
|
|