webhookagent 1.0.0 → 1.1.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/lib/heartbeat.js +146 -9
- package/package.json +1 -1
package/lib/heartbeat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* WebhookAgent Heartbeat Runtime v1.0
|
|
4
4
|
* Cross-platform Node.js agent for polling + processing webhook events
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* WebhookAgent.com — Webhook Queue for AI Agents & Developers
|
|
7
7
|
* Works on: Windows, macOS, Linux — anywhere Node.js runs
|
|
8
8
|
*
|
|
9
9
|
* HOW IT WORKS (Break-on-Action Pattern):
|
|
@@ -54,20 +54,40 @@
|
|
|
54
54
|
// CONFIG
|
|
55
55
|
// ============================================================
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
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 `webhookagent config` subcommand
|
|
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
|
+
// Load saved config (if exists)
|
|
73
|
+
const savedConfig = loadConfig();
|
|
74
|
+
|
|
75
|
+
const API_KEY = args.key || process.env.WHA_API_KEY || savedConfig.api_key || '';
|
|
76
|
+
const BASE_URL = (args.url || process.env.WHA_URL || savedConfig.api_url || 'https://webhookagent.com/api').replace(/\/$/, '');
|
|
60
77
|
const INTERVAL = parseInt(args.interval || '30') * 1000;
|
|
61
78
|
const MAX_CYCLES = args.once ? 1 : parseInt(args.cycles || '120');
|
|
62
79
|
const AUTO_RESTART = !!args['auto-restart'];
|
|
63
80
|
const QUIET = !!args.quiet;
|
|
64
|
-
const WEBHOOKS_FILTER = args.webhooks || 'all';
|
|
81
|
+
const WEBHOOKS_FILTER = args.webhooks || savedConfig.webhooks || 'all';
|
|
65
82
|
const AUTO_FETCH = !!args.fetch;
|
|
66
83
|
const AUTO_ACK = !!args.ack;
|
|
67
84
|
const JSON_MODE = !!args.json;
|
|
68
85
|
|
|
69
86
|
if (!API_KEY) {
|
|
70
|
-
console.error('ERROR: API key required.
|
|
87
|
+
console.error('ERROR: API key required.');
|
|
88
|
+
console.error(' Option 1: webhookagent config --key=wha_... (saves securely, recommended)');
|
|
89
|
+
console.error(' Option 2: webhookagent --key=wha_... (passed each time)');
|
|
90
|
+
console.error(' Option 3: set WHA_API_KEY env var');
|
|
71
91
|
process.exit(1);
|
|
72
92
|
}
|
|
73
93
|
|
|
@@ -86,6 +106,90 @@ function parseArgs(argv) {
|
|
|
86
106
|
return result;
|
|
87
107
|
}
|
|
88
108
|
|
|
109
|
+
function loadConfig() {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
112
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
} catch (e) { /* ignore */ }
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function saveConfig(data) {
|
|
119
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
120
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
const existing = loadConfig();
|
|
123
|
+
const merged = { ...existing, ...data };
|
|
124
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n');
|
|
125
|
+
return merged;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleConfig(argv) {
|
|
129
|
+
const configArgs = parseArgs(argv);
|
|
130
|
+
|
|
131
|
+
// webhookagent config --key=wha_...
|
|
132
|
+
if (configArgs.key) {
|
|
133
|
+
saveConfig({ api_key: configArgs.key });
|
|
134
|
+
console.log(`API key saved to ${CONFIG_FILE}`);
|
|
135
|
+
console.log('You can now run: webhookagent --fetch --ack');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// webhookagent config --webhooks=wh-abc,wh-def
|
|
140
|
+
if (configArgs.webhooks) {
|
|
141
|
+
saveConfig({ webhooks: configArgs.webhooks });
|
|
142
|
+
console.log(`Webhook filter saved: ${configArgs.webhooks}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// webhookagent config --url=https://...
|
|
147
|
+
if (configArgs.url) {
|
|
148
|
+
saveConfig({ api_url: configArgs.url });
|
|
149
|
+
console.log(`API URL saved: ${configArgs.url}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// webhookagent config --show
|
|
154
|
+
if (configArgs.show) {
|
|
155
|
+
const cfg = loadConfig();
|
|
156
|
+
if (Object.keys(cfg).length === 0) {
|
|
157
|
+
console.log('No config saved. Use: webhookagent config --key=wha_...');
|
|
158
|
+
} else {
|
|
159
|
+
const display = { ...cfg };
|
|
160
|
+
if (display.api_key) display.api_key = display.api_key.slice(0, 8) + '...' + display.api_key.slice(-4);
|
|
161
|
+
console.log(JSON.stringify(display, null, 2));
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// webhookagent config --clear
|
|
167
|
+
if (configArgs.clear) {
|
|
168
|
+
try { fs.unlinkSync(CONFIG_FILE); } catch (e) { /* ignore */ }
|
|
169
|
+
console.log('Config cleared.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// No args — show help
|
|
174
|
+
console.log('WebhookAgent Config');
|
|
175
|
+
console.log(' webhookagent config --key=wha_... Save API key');
|
|
176
|
+
console.log(' webhookagent config --webhooks=ID1,ID2 Save webhook filter');
|
|
177
|
+
console.log(' webhookagent config --show Show saved config');
|
|
178
|
+
console.log(' webhookagent config --clear Delete saved config');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildRestartCommand() {
|
|
182
|
+
const parts = ['webhookagent'];
|
|
183
|
+
// Only include --key if it was passed explicitly (not from config/env)
|
|
184
|
+
if (args.key) parts.push(`--key=${args.key}`);
|
|
185
|
+
if (AUTO_FETCH) parts.push('--fetch');
|
|
186
|
+
if (AUTO_ACK) parts.push('--ack');
|
|
187
|
+
if (WEBHOOKS_FILTER !== 'all' && args.webhooks) parts.push(`--webhooks=${WEBHOOKS_FILTER}`);
|
|
188
|
+
if (args.interval) parts.push(`--interval=${args.interval}`);
|
|
189
|
+
if (QUIET) parts.push('--quiet');
|
|
190
|
+
return parts.join(' ');
|
|
191
|
+
}
|
|
192
|
+
|
|
89
193
|
function log(msg) {
|
|
90
194
|
if (QUIET) return;
|
|
91
195
|
process.stderr.write(`${new Date().toLocaleTimeString()} ${msg}\n`);
|
|
@@ -183,9 +287,34 @@ async function processActions(actions, heartbeatData) {
|
|
|
183
287
|
// ============================================================
|
|
184
288
|
|
|
185
289
|
async function main() {
|
|
186
|
-
log('WebhookAgent Heartbeat Runtime v1.
|
|
290
|
+
log('WebhookAgent Heartbeat Runtime v1.1');
|
|
187
291
|
log(`Polling ${BASE_URL} every ${INTERVAL / 1000}s, max ${MAX_CYCLES} cycles${AUTO_RESTART ? ' (continuous)' : ''}`);
|
|
188
|
-
|
|
292
|
+
|
|
293
|
+
// Resolve webhook names to IDs if needed
|
|
294
|
+
let resolvedFilter = WEBHOOKS_FILTER;
|
|
295
|
+
if (WEBHOOKS_FILTER !== 'all') {
|
|
296
|
+
const filterParts = WEBHOOKS_FILTER.split(',').map(s => s.trim());
|
|
297
|
+
const hasNames = filterParts.some(p => !p.startsWith('wh-') && !/^\d+$/.test(p));
|
|
298
|
+
if (hasNames) {
|
|
299
|
+
const webhooks = await api('/webhooks');
|
|
300
|
+
if (webhooks && webhooks.webhooks) {
|
|
301
|
+
const resolved = filterParts.map(f => {
|
|
302
|
+
// If it looks like an ID already, keep it
|
|
303
|
+
if (f.startsWith('wh-') || /^\d+$/.test(f)) return f;
|
|
304
|
+
// Otherwise match by name (case-insensitive)
|
|
305
|
+
const match = webhooks.webhooks.find(w => w.name.toLowerCase() === f.toLowerCase());
|
|
306
|
+
if (match) {
|
|
307
|
+
log(`Resolved "${f}" → ${match.webhook_id}`);
|
|
308
|
+
return match.webhook_id;
|
|
309
|
+
}
|
|
310
|
+
log(`WARN: webhook name "${f}" not found, skipping`);
|
|
311
|
+
return null;
|
|
312
|
+
}).filter(Boolean);
|
|
313
|
+
resolvedFilter = resolved.join(',');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
log(`Filtering webhooks: ${resolvedFilter}`);
|
|
317
|
+
}
|
|
189
318
|
if (AUTO_FETCH) log('Auto-fetch: ON');
|
|
190
319
|
if (AUTO_ACK) log('Auto-ack: ON');
|
|
191
320
|
log('---');
|
|
@@ -195,7 +324,7 @@ async function main() {
|
|
|
195
324
|
let brokeOnAction = false;
|
|
196
325
|
|
|
197
326
|
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
198
|
-
const path = `/heartbeat?webhooks=${encodeURIComponent(
|
|
327
|
+
const path = `/heartbeat?webhooks=${encodeURIComponent(resolvedFilter)}`;
|
|
199
328
|
const data = await api(path);
|
|
200
329
|
|
|
201
330
|
if (!data) {
|
|
@@ -222,6 +351,14 @@ async function main() {
|
|
|
222
351
|
await processActions(actions, data);
|
|
223
352
|
|
|
224
353
|
brokeOnAction = true;
|
|
354
|
+
|
|
355
|
+
// Tell the agent exactly how to restart
|
|
356
|
+
if (!AUTO_RESTART) {
|
|
357
|
+
const cmd = buildRestartCommand();
|
|
358
|
+
console.log('RESTART:' + cmd);
|
|
359
|
+
log(`Process events above, then run: ${cmd}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
225
362
|
break; // ← THE BREAK — exit loop so parent process can handle
|
|
226
363
|
}
|
|
227
364
|
|