@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 +38 -6
- package/package.json +1 -1
- package/src/commands/forward.js +1 -0
- package/src/commands/listen.js +20 -6
- package/src/index.js +1 -1
- package/src/utils/url-helpers.js +22 -1
- package/src/utils/url-helpers.test.js +51 -1
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
|
-
|
|
379
|
-
- `POST /
|
|
380
|
-
- `POST /v1/ai/reconnect` - Get reconnect token
|
|
381
|
-
- `
|
|
382
|
-
- `
|
|
383
|
-
- `
|
|
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
package/src/commands/forward.js
CHANGED
package/src/commands/listen.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/src/utils/url-helpers.js
CHANGED
|
@@ -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
|
});
|