@modelriver/cli 1.0.0 → 1.2.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
@@ -54,6 +54,7 @@ For convenience, the CLI supports short aliases:
54
54
  | `forward` | `f` | Forward using saved config |
55
55
  | `trigger` | `t` | Send async request |
56
56
  | `websocket` | `ws` | Test WebSocket connection |
57
+ | `test-webhook` | - | Test webhook delivery end-to-end |
57
58
 
58
59
  Example:
59
60
  ```bash
@@ -257,6 +258,33 @@ Channel Details
257
258
  > Or use "modelriver websocket" to connect and receive responses
258
259
  ```
259
260
 
261
+ ### `modelriver test-webhook` - Test Webhook Delivery End-to-End
262
+
263
+ Test the complete webhook flow - creates a webhook, makes an async request, and waits for the webhook response:
264
+
265
+ ```bash
266
+ # Start local server and test webhook delivery
267
+ modelriver test-webhook --workflow my-workflow --message "Test"
268
+
269
+ # Use custom webhook URL (e.g., your local server or webhook.site)
270
+ modelriver test-webhook --workflow my-workflow --message "Test" --webhook-url http://localhost:4000/webhook/modelriver
271
+
272
+ # With custom secret and port
273
+ modelriver test-webhook --workflow my-workflow --message "Test" --port 3002 --secret my-secret
274
+
275
+ # Verbose output
276
+ modelriver test-webhook --workflow my-workflow --message "Test" --verbose
277
+ ```
278
+
279
+ **Options:**
280
+ - `-w, --workflow <name>` - Workflow name (required)
281
+ - `-m, --message <text>` - Test message
282
+ - `-P, --payload <json>` - Custom JSON payload
283
+ - `-u, --webhook-url <url>` - Webhook URL (starts local server on --port if not provided)
284
+ - `-s, --secret <secret>` - Webhook secret (auto-generated if not provided)
285
+ - `-p, --port <port>` - Local server port (default: 3001)
286
+ - `-v, --verbose` - Verbose output
287
+
260
288
  ### `modelriver webhook list` - List Webhooks
261
289
 
262
290
  List all webhooks for your project.
@@ -375,12 +403,16 @@ Set the API key via:
375
403
 
376
404
  The CLI uses these ModelRiver API endpoints:
377
405
 
378
- - `POST /v1/ai/async` - Create async request (production)
379
- - `POST /api/v1/ai/async` - Create async request (dev/test)
380
- - `POST /v1/ai/reconnect` - Get reconnect token (production)
381
- - `POST /api/v1/ai/reconnect` - Get reconnect token (dev/test)
382
- - `GET /v1/webhooks` - List webhooks (production)
383
- - `GET /api/v1/webhooks` - List webhooks (dev/test)
406
+ **Production** (`https://api.modelriver.com`):
407
+ - `POST /v1/ai/async` - Create async AI request
408
+ - `POST /v1/ai/reconnect` - Get reconnect token for existing channel
409
+ - `GET /v1/webhooks` - List all webhooks
410
+ - `POST /v1/webhooks` - Create a new webhook
411
+ - `DELETE /v1/webhooks/:id` - Delete a webhook
412
+ - `POST /v1/cli/connect` - Connect CLI and get WebSocket token
413
+
414
+ **Development** (localhost or custom API URL):
415
+ - Same endpoints but with `/api/v1` prefix instead of `/v1`
384
416
 
385
417
  ## Development
386
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelriver/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "ModelRiver CLI for testing webhooks and WebSockets from production",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -52,6 +52,7 @@ async function forwardCommand(options) {
52
52
  apiKey,
53
53
  apiUrl: config.getApiUrl(),
54
54
  port: targetPort,
55
+ forwardUrl: forwardUrl,
55
56
  forward: true,
56
57
  print: true,
57
58
  verbose
@@ -4,9 +4,10 @@ const CLIWebSocketClient = require('../lib/cli-websocket-client');
4
4
  const WebhookVerifier = require('../lib/webhook-verifier');
5
5
  const Logger = require('../utils/logger');
6
6
  const Formatter = require('../utils/formatter');
7
+ const { parseForwardUrl } = require('../utils/url-helpers');
7
8
 
8
9
  async function listenCommand(options) {
9
- let { apiKey, apiUrl, print, port, verbose, forward } = options;
10
+ let { apiKey, apiUrl, print, port, verbose, forward, forwardUrl } = options;
10
11
  const forwardOnly = forward === true;
11
12
 
12
13
  const spinner = Logger.spinner('Setting up webhook forwarding...');
@@ -206,16 +207,25 @@ async function listenCommand(options) {
206
207
 
207
208
  // Forward to local server
208
209
  const http = require('http');
210
+ const https = require('https');
209
211
  const postData = JSON.stringify({
210
212
  channel_id,
211
213
  timestamp,
212
214
  data
213
215
  });
214
216
 
215
- const options = {
217
+ // Parse the forward URL or use localhost defaults
218
+ const parsedUrl = parseForwardUrl(forwardUrl) || {
216
219
  hostname: 'localhost',
217
220
  port: localPort,
218
- path: '/webhook',
221
+ path: '/webhook/modelriver',
222
+ protocol: 'http'
223
+ };
224
+
225
+ const options = {
226
+ hostname: parsedUrl.hostname,
227
+ port: parsedUrl.port,
228
+ path: parsedUrl.path,
219
229
  method: 'POST',
220
230
  headers: {
221
231
  'Content-Type': 'application/json',
@@ -226,15 +236,19 @@ async function listenCommand(options) {
226
236
  }
227
237
  };
228
238
 
229
- const req = http.request(options, (res) => {
239
+ // Use https or http based on protocol
240
+ const requestModule = parsedUrl.protocol === 'https' ? https : http;
241
+ const targetUrl = `${parsedUrl.protocol}://${parsedUrl.hostname}:${parsedUrl.port}${parsedUrl.path}`;
242
+
243
+ const req = requestModule.request(options, (res) => {
230
244
  // Webhook forwarded successfully
231
245
  if (print) {
232
- Logger.info(` → Forwarded to local server (status: ${res.statusCode})`);
246
+ Logger.info(` → Forwarded to ${targetUrl} (status: ${res.statusCode})`);
233
247
  }
234
248
  });
235
249
 
236
250
  req.on('error', (error) => {
237
- Logger.error(`Failed to forward webhook to local server: ${error.message}`);
251
+ Logger.error(`Failed to forward webhook to ${targetUrl}: ${error.message}`);
238
252
  });
239
253
 
240
254
  req.write(postData);
package/src/index.js CHANGED
@@ -15,7 +15,7 @@ const program = new Command();
15
15
  program
16
16
  .name('modelriver')
17
17
  .description('ModelRiver CLI for testing webhooks and WebSockets from production')
18
- .version('1.0.0');
18
+ .version('1.1.0');
19
19
 
20
20
  // Global options
21
21
  program
@@ -91,8 +91,29 @@ function isValidApiKeyFormat(apiKey) {
91
91
  return apiKey.startsWith('mr_live_') || apiKey.startsWith('mr_test_');
92
92
  }
93
93
 
94
+ /**
95
+ * Parse a forward URL into its components
96
+ * @param {string} url - The URL to parse
97
+ * @returns {object|null} - Object with hostname, port, path, protocol or null if invalid
98
+ */
99
+ function parseForwardUrl(url) {
100
+ if (!url) return null;
101
+ try {
102
+ const urlObj = new URL(url);
103
+ return {
104
+ hostname: urlObj.hostname,
105
+ port: parseInt(urlObj.port, 10) || (urlObj.protocol === 'https:' ? 443 : 80),
106
+ path: urlObj.pathname || '/webhook/modelriver',
107
+ protocol: urlObj.protocol.replace(':', '')
108
+ };
109
+ } catch (error) {
110
+ return null;
111
+ }
112
+ }
113
+
94
114
  module.exports = {
95
115
  normalizeWebhookUrl,
96
116
  extractPort,
97
- isValidApiKeyFormat
117
+ isValidApiKeyFormat,
118
+ parseForwardUrl
98
119
  };
@@ -1,5 +1,5 @@
1
1
  // Tests for URL helper utilities
2
- const { normalizeWebhookUrl, extractPort, isValidApiKeyFormat } = require('./url-helpers');
2
+ const { normalizeWebhookUrl, extractPort, isValidApiKeyFormat, parseForwardUrl } = require('./url-helpers');
3
3
 
4
4
  describe('URL Helpers', () => {
5
5
  describe('normalizeWebhookUrl', () => {
@@ -81,4 +81,54 @@ describe('URL Helpers', () => {
81
81
  expect(isValidApiKeyFormat(undefined)).toBe(false);
82
82
  });
83
83
  });
84
+
85
+ describe('parseForwardUrl', () => {
86
+ it('should parse localhost URL with port', () => {
87
+ const result = parseForwardUrl('http://localhost:4000/webhook/modelriver');
88
+ expect(result).toEqual({
89
+ hostname: 'localhost',
90
+ port: 4000,
91
+ path: '/webhook/modelriver',
92
+ protocol: 'http'
93
+ });
94
+ });
95
+
96
+ it('should parse external URL with port', () => {
97
+ const result = parseForwardUrl('http://myserver.com:3001/webhook/modelriver');
98
+ expect(result).toEqual({
99
+ hostname: 'myserver.com',
100
+ port: 3001,
101
+ path: '/webhook/modelriver',
102
+ protocol: 'http'
103
+ });
104
+ });
105
+
106
+ it('should default to port 80 for http without explicit port', () => {
107
+ const result = parseForwardUrl('http://example.com/webhook/modelriver');
108
+ expect(result.port).toBe(80);
109
+ expect(result.hostname).toBe('example.com');
110
+ });
111
+
112
+ it('should default to port 443 for https without explicit port', () => {
113
+ const result = parseForwardUrl('https://example.com/webhook/modelriver');
114
+ expect(result.port).toBe(443);
115
+ expect(result.protocol).toBe('https');
116
+ });
117
+
118
+ it('should handle URLs with different paths', () => {
119
+ const result = parseForwardUrl('http://localhost:5000/custom/path');
120
+ expect(result.path).toBe('/custom/path');
121
+ });
122
+
123
+ it('should return null for empty or falsy input', () => {
124
+ expect(parseForwardUrl('')).toBeNull();
125
+ expect(parseForwardUrl(null)).toBeNull();
126
+ expect(parseForwardUrl(undefined)).toBeNull();
127
+ });
128
+
129
+ it('should return null for invalid URLs', () => {
130
+ expect(parseForwardUrl('not-a-url')).toBeNull();
131
+ expect(parseForwardUrl('just-text')).toBeNull();
132
+ });
133
+ });
84
134
  });