@modelriver/cli 1.0.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 +406 -0
- package/bin/modelriver +5 -0
- package/package.json +52 -0
- package/src/commands/forward.js +61 -0
- package/src/commands/listen.js +315 -0
- package/src/commands/login.js +176 -0
- package/src/commands/test-webhook.js +289 -0
- package/src/commands/trigger.js +77 -0
- package/src/commands/webhook.js +101 -0
- package/src/commands/websocket.js +249 -0
- package/src/index.js +154 -0
- package/src/lib/api-client.js +173 -0
- package/src/lib/api-client.test.js +101 -0
- package/src/lib/cli-websocket-client.js +249 -0
- package/src/lib/config.js +137 -0
- package/src/lib/config.test.js +98 -0
- package/src/lib/webhook-verifier.js +56 -0
- package/src/lib/webhook-verifier.test.js +90 -0
- package/src/lib/websocket-client.js +225 -0
- package/src/utils/formatter.js +43 -0
- package/src/utils/formatter.test.js +84 -0
- package/src/utils/logger.js +39 -0
- package/src/utils/url-helpers.js +98 -0
- package/src/utils/url-helpers.test.js +84 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const ApiClient = require('../lib/api-client');
|
|
3
|
+
const CLIWebSocketClient = require('../lib/cli-websocket-client');
|
|
4
|
+
const WebhookVerifier = require('../lib/webhook-verifier');
|
|
5
|
+
const Logger = require('../utils/logger');
|
|
6
|
+
const Formatter = require('../utils/formatter');
|
|
7
|
+
|
|
8
|
+
async function listenCommand(options) {
|
|
9
|
+
let { apiKey, apiUrl, print, port, verbose, forward } = options;
|
|
10
|
+
const forwardOnly = forward === true;
|
|
11
|
+
|
|
12
|
+
const spinner = Logger.spinner('Setting up webhook forwarding...');
|
|
13
|
+
spinner.start();
|
|
14
|
+
|
|
15
|
+
let apiClient;
|
|
16
|
+
let wsClient;
|
|
17
|
+
let localPort = port || 3001;
|
|
18
|
+
let webhookSecret = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Validate API key
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
const config = require('../lib/config');
|
|
24
|
+
apiKey = config.getApiKey();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
spinner.fail('API key is required');
|
|
29
|
+
Logger.error('API key is not set. Please set MODELRIVER_API_KEY environment variable or use --api-key flag.');
|
|
30
|
+
|
|
31
|
+
// Check if this is localhost for better error message
|
|
32
|
+
const isLocalhost = apiUrl && (
|
|
33
|
+
apiUrl.includes('localhost') ||
|
|
34
|
+
apiUrl.includes('127.0.0.1') ||
|
|
35
|
+
apiUrl.includes('0.0.0.0')
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (isLocalhost) {
|
|
39
|
+
Logger.warning('\n> Quick fix for localhost:');
|
|
40
|
+
Logger.warning('> 1. Log in at http://localhost:4000');
|
|
41
|
+
Logger.warning('> 2. Create a project if you don\'t have one');
|
|
42
|
+
Logger.warning('> 3. Go to Project Settings > API Keys and create a new key');
|
|
43
|
+
Logger.warning('> 4. Or run this script to create a test key:');
|
|
44
|
+
Logger.warning('> mix run scripts/create_test_api_key.exs USER_EMAIL');
|
|
45
|
+
Logger.warning('> 5. Then set it:');
|
|
46
|
+
Logger.warning('> export MODELRIVER_API_KEY=your_key_here');
|
|
47
|
+
} else {
|
|
48
|
+
Logger.warning('\n> To fix this:');
|
|
49
|
+
Logger.warning('> 1. Set MODELRIVER_API_KEY environment variable:');
|
|
50
|
+
Logger.warning('> export MODELRIVER_API_KEY=mr_live_YOUR_KEY');
|
|
51
|
+
Logger.warning('> 2. Or use --api-key flag:');
|
|
52
|
+
Logger.warning('> modelriver listen --api-key mr_live_YOUR_KEY');
|
|
53
|
+
Logger.warning('> 3. Get your API key from https://console.modelriver.com');
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate API key format
|
|
59
|
+
if (!apiKey.startsWith('mr_live_') && !apiKey.startsWith('mr_test_')) {
|
|
60
|
+
spinner.fail('Invalid API key format');
|
|
61
|
+
Logger.error(`API key must start with "mr_live_" or "mr_test_". Got: ${apiKey.substring(0, 20)}...`);
|
|
62
|
+
Logger.warning('> Make sure you copied the full API key from the dashboard');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
apiClient = new ApiClient(apiKey, apiUrl);
|
|
67
|
+
|
|
68
|
+
// Debug: Show API key info if verbose
|
|
69
|
+
if (verbose) {
|
|
70
|
+
Logger.info(`> API URL: ${apiUrl}`);
|
|
71
|
+
Logger.info(`> API Path: ${apiClient.apiPath}`);
|
|
72
|
+
Logger.info(`> API Key (first 30 chars): ${apiKey.substring(0, 30)}...`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 1: Get CLI WebSocket token
|
|
76
|
+
spinner.text = 'Connecting to ModelRiver...';
|
|
77
|
+
|
|
78
|
+
const connectResponse = await apiClient.connectCLI();
|
|
79
|
+
const { ws_token, websocket_url, user_id, channel } = connectResponse;
|
|
80
|
+
|
|
81
|
+
spinner.text = 'Connecting to WebSocket...';
|
|
82
|
+
|
|
83
|
+
// Step 2: Connect to WebSocket
|
|
84
|
+
wsClient = new CLIWebSocketClient(websocket_url, ws_token, verbose || false);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await wsClient.connect();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
spinner.fail('WebSocket connection failed');
|
|
90
|
+
Logger.error(error.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
spinner.text = 'Joining webhook channel...';
|
|
95
|
+
|
|
96
|
+
// Step 3: Join CLI webhook channel
|
|
97
|
+
try {
|
|
98
|
+
await wsClient.joinChannel(channel);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
spinner.fail('Failed to join channel');
|
|
101
|
+
Logger.error(error.message);
|
|
102
|
+
wsClient.close();
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Step 4: Get webhook secret for signature verification
|
|
107
|
+
// We'll use the first webhook's secret if available, or generate a default
|
|
108
|
+
try {
|
|
109
|
+
const webhooks = await apiClient.listWebhooks();
|
|
110
|
+
if (webhooks.length > 0) {
|
|
111
|
+
webhookSecret = webhooks[0].secret;
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// If we can't get webhook secret, we'll skip verification
|
|
115
|
+
Logger.warning('Could not retrieve webhook secret for verification');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
spinner.succeed('Webhook forwarding active');
|
|
119
|
+
|
|
120
|
+
Logger.success('\n> Ready! ModelRiver webhook forwarding is active');
|
|
121
|
+
Logger.info(`> User ID: ${user_id}`);
|
|
122
|
+
Logger.info(`> Channel: ${channel}`);
|
|
123
|
+
Logger.info(`> Local port: ${localPort}`);
|
|
124
|
+
if (webhookSecret) {
|
|
125
|
+
Logger.warning(`> Webhook secret: ${webhookSecret}`);
|
|
126
|
+
Logger.warning('> Save this secret for webhook-test-server!\n');
|
|
127
|
+
}
|
|
128
|
+
Logger.warning('> Press Ctrl+C to stop forwarding\n');
|
|
129
|
+
|
|
130
|
+
// Step 5: Start local Express server (only if not forwarding to external server)
|
|
131
|
+
let server = null;
|
|
132
|
+
if (!forwardOnly) {
|
|
133
|
+
const app = express();
|
|
134
|
+
app.use(express.json());
|
|
135
|
+
|
|
136
|
+
app.post('*', (req, res) => {
|
|
137
|
+
const signature = req.headers['x-modelriver-signature'];
|
|
138
|
+
const timestamp = req.headers['x-modelriver-timestamp'];
|
|
139
|
+
const webhookIdHeader = req.headers['x-modelriver-webhook-id'];
|
|
140
|
+
|
|
141
|
+
const { channel_id, timestamp: payloadTimestamp, data } = req.body;
|
|
142
|
+
|
|
143
|
+
// Verify signature if we have a secret
|
|
144
|
+
let isValid = null;
|
|
145
|
+
if (webhookSecret && signature && timestamp && data) {
|
|
146
|
+
isValid = WebhookVerifier.verify(
|
|
147
|
+
signature,
|
|
148
|
+
data,
|
|
149
|
+
webhookSecret,
|
|
150
|
+
timestamp
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (print) {
|
|
155
|
+
const timestampStr = Formatter.timestamp(new Date());
|
|
156
|
+
Logger.log(`\n[${timestampStr}] Webhook received:`);
|
|
157
|
+
Logger.log(` Channel ID: ${channel_id || 'N/A'}`);
|
|
158
|
+
Logger.log(` Status: ${data?.status || 'N/A'}`);
|
|
159
|
+
if (isValid !== null) {
|
|
160
|
+
Logger.log(` Signature: ${isValid ? '✓ Valid' : '✗ Invalid'}`);
|
|
161
|
+
}
|
|
162
|
+
if (data?.data) {
|
|
163
|
+
Logger.log(` Data: ${Formatter.json(data.data)}`);
|
|
164
|
+
}
|
|
165
|
+
if (data?.meta) {
|
|
166
|
+
Logger.log(` Meta: ${Formatter.json(data.meta)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Always return 200 to acknowledge receipt
|
|
171
|
+
res.status(200).json({ received: true, verified: isValid });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
server = app.listen(localPort, 'localhost', () => {
|
|
175
|
+
Logger.success(`> Local server listening on http://localhost:${localPort}/webhook`);
|
|
176
|
+
});
|
|
177
|
+
} else {
|
|
178
|
+
Logger.success(`> Forwarding webhooks to http://localhost:${localPort}/webhook`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Step 6: Listen for webhook events from WebSocket
|
|
182
|
+
wsClient.onWebhook((webhookPayload) => {
|
|
183
|
+
const { channel_id, timestamp, data, signature } = webhookPayload;
|
|
184
|
+
|
|
185
|
+
if (print) {
|
|
186
|
+
const timestampStr = Formatter.timestamp(new Date());
|
|
187
|
+
Logger.log(`\n[${timestampStr}] Webhook received via WebSocket:`);
|
|
188
|
+
Logger.log(` Channel ID: ${channel_id || 'N/A'}`);
|
|
189
|
+
Logger.log(` Status: ${data?.status || 'N/A'}`);
|
|
190
|
+
if (signature && webhookSecret) {
|
|
191
|
+
const isValid = WebhookVerifier.verify(
|
|
192
|
+
signature,
|
|
193
|
+
data,
|
|
194
|
+
webhookSecret,
|
|
195
|
+
timestamp.toString()
|
|
196
|
+
);
|
|
197
|
+
Logger.log(` Signature: ${isValid ? '✓ Valid' : '✗ Invalid'}`);
|
|
198
|
+
}
|
|
199
|
+
if (data?.data) {
|
|
200
|
+
Logger.log(` Data: ${Formatter.json(data.data)}`);
|
|
201
|
+
}
|
|
202
|
+
if (data?.meta) {
|
|
203
|
+
Logger.log(` Meta: ${Formatter.json(data.meta)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Forward to local server
|
|
208
|
+
const http = require('http');
|
|
209
|
+
const postData = JSON.stringify({
|
|
210
|
+
channel_id,
|
|
211
|
+
timestamp,
|
|
212
|
+
data
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const options = {
|
|
216
|
+
hostname: 'localhost',
|
|
217
|
+
port: localPort,
|
|
218
|
+
path: '/webhook',
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
223
|
+
'X-ModelRiver-Signature': signature || '',
|
|
224
|
+
'X-ModelRiver-Timestamp': timestamp.toString(),
|
|
225
|
+
'X-ModelRiver-Webhook-Id': webhookPayload.webhook_id || ''
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const req = http.request(options, (res) => {
|
|
230
|
+
// Webhook forwarded successfully
|
|
231
|
+
if (print) {
|
|
232
|
+
Logger.info(` → Forwarded to local server (status: ${res.statusCode})`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
req.on('error', (error) => {
|
|
237
|
+
Logger.error(`Failed to forward webhook to local server: ${error.message}`);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
req.write(postData);
|
|
241
|
+
req.end();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Handle WebSocket close
|
|
245
|
+
wsClient.onClose((code, reason) => {
|
|
246
|
+
Logger.warning(`\n> WebSocket connection closed (code: ${code})`);
|
|
247
|
+
if (reason) {
|
|
248
|
+
Logger.warning(`> Reason: ${reason}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Cleanup on exit
|
|
253
|
+
const cleanup = async () => {
|
|
254
|
+
Logger.warning('\n\n> Cleaning up...');
|
|
255
|
+
try {
|
|
256
|
+
if (wsClient) {
|
|
257
|
+
wsClient.close();
|
|
258
|
+
Logger.info('> WebSocket connection closed');
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
Logger.error(`> Failed to cleanup: ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
if (server) {
|
|
264
|
+
server.close();
|
|
265
|
+
}
|
|
266
|
+
process.exit(0);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
process.on('SIGINT', cleanup);
|
|
270
|
+
process.on('SIGTERM', cleanup);
|
|
271
|
+
|
|
272
|
+
} catch (error) {
|
|
273
|
+
spinner.fail('Failed to setup webhook forwarding');
|
|
274
|
+
Logger.error(error.message);
|
|
275
|
+
|
|
276
|
+
// Provide helpful error messages
|
|
277
|
+
if (error.message.includes('API key is required')) {
|
|
278
|
+
Logger.warning('\n> To fix this:');
|
|
279
|
+
Logger.warning('> 1. Set MODELRIVER_API_KEY environment variable:');
|
|
280
|
+
Logger.warning('> export MODELRIVER_API_KEY=mr_live_YOUR_KEY');
|
|
281
|
+
Logger.warning('> 2. Or use --api-key flag:');
|
|
282
|
+
Logger.warning('> modelriver listen --api-key mr_live_YOUR_KEY');
|
|
283
|
+
Logger.warning('> 3. Create an API key in the ModelRiver dashboard if you don\'t have one');
|
|
284
|
+
} else if (error.message.includes('Invalid API key format')) {
|
|
285
|
+
Logger.warning('\n> API keys must start with "mr_live_" or "mr_test_"');
|
|
286
|
+
Logger.warning('> Example: mr_live_abc123...');
|
|
287
|
+
} else if (error.response?.status === 401) {
|
|
288
|
+
Logger.warning('\n> Authentication failed. Possible causes:');
|
|
289
|
+
Logger.warning('> 1. API key is invalid or revoked');
|
|
290
|
+
Logger.warning('> 2. API key doesn\'t exist in the database');
|
|
291
|
+
Logger.warning('> 3. API key format is incorrect');
|
|
292
|
+
Logger.warning('> Check your API key in the ModelRiver dashboard');
|
|
293
|
+
} else if (error.response?.status === 403) {
|
|
294
|
+
Logger.warning('\n> Access forbidden. Possible causes:');
|
|
295
|
+
Logger.warning('> 1. Backend code not deployed (if testing against production)');
|
|
296
|
+
Logger.warning('> 2. Route not available on this server');
|
|
297
|
+
Logger.warning('> 3. Server needs to be restarted to load new routes');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (error.response?.data) {
|
|
301
|
+
Logger.error(`\nServer response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
302
|
+
}
|
|
303
|
+
if (error.response?.status) {
|
|
304
|
+
Logger.error(`HTTP Status: ${error.response.status}`);
|
|
305
|
+
}
|
|
306
|
+
if (wsClient) {
|
|
307
|
+
wsClient.close();
|
|
308
|
+
}
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = { listenCommand };
|
|
314
|
+
|
|
315
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const config = require('../lib/config');
|
|
3
|
+
const Logger = require('../utils/logger');
|
|
4
|
+
const { normalizeWebhookUrl, isValidApiKeyFormat } = require('../utils/url-helpers');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a readline interface for interactive prompts
|
|
8
|
+
*/
|
|
9
|
+
function createReadlineInterface() {
|
|
10
|
+
return readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prompt user for input
|
|
18
|
+
* @param {readline.Interface} rl - Readline interface
|
|
19
|
+
* @param {string} question - Question to ask
|
|
20
|
+
* @param {string} defaultValue - Default value if user presses enter
|
|
21
|
+
* @param {boolean} maskDefault - Whether to mask the default value in display
|
|
22
|
+
* @param {boolean} showDefault - Whether to show the default value in brackets
|
|
23
|
+
* @returns {Promise<string>} - User input
|
|
24
|
+
*/
|
|
25
|
+
function prompt(rl, question, defaultValue = '', maskDefault = false, showDefault = true) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
let displayDefault = defaultValue;
|
|
28
|
+
if (defaultValue && maskDefault) {
|
|
29
|
+
// Show masked version like "mr_live_...a1b2"
|
|
30
|
+
if (defaultValue.length > 10) {
|
|
31
|
+
displayDefault = `${defaultValue.substring(0, 8)}...${defaultValue.substring(defaultValue.length - 4)}`;
|
|
32
|
+
} else {
|
|
33
|
+
displayDefault = '********';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let displayQuestion = question + ': ';
|
|
38
|
+
|
|
39
|
+
if (showDefault && defaultValue) {
|
|
40
|
+
displayQuestion = `${question} (${displayDefault}): `;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If the question already contains the default value in brackets (redundancy check), don't append it again
|
|
44
|
+
// Heuristic: check if question ends with the default value inside parens, ignoring whitespace
|
|
45
|
+
if (showDefault && defaultValue && question.trim().endsWith(`(${defaultValue})`)) {
|
|
46
|
+
displayQuestion = `${question}: `;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
rl.question(displayQuestion, (answer) => {
|
|
50
|
+
resolve(answer.trim() || defaultValue);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Interactive login command
|
|
57
|
+
* Prompts for API key and forward URL, saves to config
|
|
58
|
+
*/
|
|
59
|
+
async function loginCommand(options) {
|
|
60
|
+
const rl = createReadlineInterface();
|
|
61
|
+
|
|
62
|
+
console.log('');
|
|
63
|
+
Logger.section('ModelRiver CLI Login');
|
|
64
|
+
console.log('Configure your ModelRiver CLI credentials.\n');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Get current values for defaults
|
|
68
|
+
const currentApiKey = config.getApiKey();
|
|
69
|
+
const currentForwardUrl = config.getForwardUrl();
|
|
70
|
+
|
|
71
|
+
// Show current config if exists
|
|
72
|
+
if (currentApiKey || currentForwardUrl) {
|
|
73
|
+
Logger.info('Current configuration:');
|
|
74
|
+
if (currentApiKey) {
|
|
75
|
+
Logger.info(` API Key: ${currentApiKey.substring(0, 8)}...${currentApiKey.substring(currentApiKey.length - 4)}`);
|
|
76
|
+
}
|
|
77
|
+
if (currentForwardUrl) {
|
|
78
|
+
Logger.info(` Forward URL: ${currentForwardUrl}`);
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for environment variable shadowing
|
|
84
|
+
if (process.env.MODELRIVER_API_KEY) {
|
|
85
|
+
Logger.warning('WARNING: MODELRIVER_API_KEY environment variable is set.');
|
|
86
|
+
Logger.warning('This environment variable will override any API key you save here.');
|
|
87
|
+
Logger.warning(`Current env value: ${process.env.MODELRIVER_API_KEY.substring(0, 15)}...`);
|
|
88
|
+
Logger.warning('To use the key you are about to save, you must unset this variable:');
|
|
89
|
+
Logger.warning(' unset MODELRIVER_API_KEY');
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Prompt for API key
|
|
94
|
+
let apiKey = await prompt(
|
|
95
|
+
rl,
|
|
96
|
+
'Enter your ModelRiver API key',
|
|
97
|
+
currentApiKey || '',
|
|
98
|
+
true // maskDefault
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Validate API key format
|
|
102
|
+
if (!apiKey) {
|
|
103
|
+
Logger.error('API key is required.');
|
|
104
|
+
Logger.info('Get your API key from https://console.modelriver.com');
|
|
105
|
+
rl.close();
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!isValidApiKeyFormat(apiKey)) {
|
|
110
|
+
Logger.error('Invalid API key format.');
|
|
111
|
+
Logger.warning('API key must start with "mr_live_" or "mr_test_"');
|
|
112
|
+
rl.close();
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Prompt for forward URL
|
|
117
|
+
const defaultForwardUrl = currentForwardUrl || 'http://localhost:4000';
|
|
118
|
+
let forwardUrl = await prompt(
|
|
119
|
+
rl,
|
|
120
|
+
'Enter your webhook forward URL (e.g. http://localhost:4000/webhook/modelriver)',
|
|
121
|
+
defaultForwardUrl,
|
|
122
|
+
false, // maskDefault
|
|
123
|
+
false // showDefault - Hide the (http://...) bracket as requested
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Normalize the forward URL
|
|
127
|
+
forwardUrl = normalizeWebhookUrl(forwardUrl);
|
|
128
|
+
|
|
129
|
+
rl.close();
|
|
130
|
+
|
|
131
|
+
// Save configuration
|
|
132
|
+
const spinner = Logger.spinner('Saving configuration...');
|
|
133
|
+
spinner.start();
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
config.saveConfig({
|
|
137
|
+
api_key: apiKey,
|
|
138
|
+
forward_url: forwardUrl
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
spinner.succeed('Configuration saved!');
|
|
142
|
+
console.log('');
|
|
143
|
+
|
|
144
|
+
// Display saved configuration
|
|
145
|
+
Logger.section('Saved Configuration');
|
|
146
|
+
Logger.success(`API Key: ${apiKey.substring(0, 25)}...`);
|
|
147
|
+
Logger.success(`Forward URL: ${forwardUrl}`);
|
|
148
|
+
Logger.info(`Config file: ${config.getHomeConfigPath()}`);
|
|
149
|
+
|
|
150
|
+
console.log('');
|
|
151
|
+
Logger.section('Quick Start');
|
|
152
|
+
Logger.info('Start forwarding webhooks:');
|
|
153
|
+
console.log(' modelriver forward');
|
|
154
|
+
console.log('');
|
|
155
|
+
Logger.info('Or use listen with print:');
|
|
156
|
+
console.log(' modelriver listen --print');
|
|
157
|
+
console.log('');
|
|
158
|
+
Logger.info('Other useful commands:');
|
|
159
|
+
console.log(' modelriver trigger -w my-workflow -m "Hello"');
|
|
160
|
+
console.log(' modelriver websocket -w my-workflow -m "Test"');
|
|
161
|
+
console.log('');
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
spinner.fail('Failed to save configuration');
|
|
165
|
+
Logger.error(error.message);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
rl.close();
|
|
171
|
+
Logger.error(`Login failed: ${error.message}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { loginCommand };
|