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 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
- # Start the heartbeat agent
17
- webhookagent --key=wha_your_key --auto-restart
16
+ # Save your key (one-time — loads automatically after this)
17
+ webhookagent config --key=wha_your_key
18
18
 
19
- # Single check
20
- webhookagent --key=wha_your_key --once
19
+ # Start heartbeat — polls, breaks on events, outputs ACTION:{json}, exits
20
+ webhookagent --fetch --ack
21
21
 
22
- # Fetch + auto-ack items
23
- webhookagent --key=wha_your_key --auto-restart --fetch --ack
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: 120 = ~1 hour)
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 `webhookagent config` subcommand
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 INTERVAL = parseInt(args.interval || '30') * 1000;
78
- const MAX_CYCLES = args.once ? 1 : parseInt(args.cycles || '120');
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.0 (Node.js)',
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.1');
291
- log(`Polling ${BASE_URL} every ${INTERVAL / 1000}s, max ${MAX_CYCLES} cycles${AUTO_RESTART ? ' (continuous)' : ''}`);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webhookagent",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "WebhookAgent heartbeat runtime — poll, break, process webhook events at your agent's pace",
5
5
  "main": "lib/heartbeat.js",
6
6
  "bin": {