lua-cli 3.4.0 → 3.5.0-alpha.2

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.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/api/agent.api.service.d.ts +13 -0
  3. package/dist/api/agent.api.service.js +17 -0
  4. package/dist/api/agent.api.service.js.map +1 -1
  5. package/dist/api/chat.api.service.d.ts +2 -1
  6. package/dist/api/chat.api.service.js +7 -2
  7. package/dist/api/chat.api.service.js.map +1 -1
  8. package/dist/api/developer.api.service.d.ts +13 -0
  9. package/dist/api/developer.api.service.js +17 -0
  10. package/dist/api/developer.api.service.js.map +1 -1
  11. package/dist/api/lazy-instances.d.ts +8 -0
  12. package/dist/api/lazy-instances.js +16 -0
  13. package/dist/api/lazy-instances.js.map +1 -1
  14. package/dist/api/logs.api.service.d.ts +2 -1
  15. package/dist/api/logs.api.service.js +2 -0
  16. package/dist/api/logs.api.service.js.map +1 -1
  17. package/dist/api/unifiedto.api.service.d.ts +81 -0
  18. package/dist/api/unifiedto.api.service.js +99 -0
  19. package/dist/api/unifiedto.api.service.js.map +1 -0
  20. package/dist/api/user.data.api.service.d.ts +11 -3
  21. package/dist/api/user.data.api.service.js +42 -3
  22. package/dist/api/user.data.api.service.js.map +1 -1
  23. package/dist/api-exports.d.ts +19 -3
  24. package/dist/api-exports.js +18 -4
  25. package/dist/api-exports.js.map +1 -1
  26. package/dist/cli/command-definitions.js +103 -16
  27. package/dist/cli/command-definitions.js.map +1 -1
  28. package/dist/commands/chat.js +51 -23
  29. package/dist/commands/chat.js.map +1 -1
  30. package/dist/commands/compile.d.ts +1 -2
  31. package/dist/commands/compile.js +2 -3
  32. package/dist/commands/compile.js.map +1 -1
  33. package/dist/commands/configure.d.ts +17 -1
  34. package/dist/commands/configure.js +29 -4
  35. package/dist/commands/configure.js.map +1 -1
  36. package/dist/commands/index.d.ts +1 -0
  37. package/dist/commands/index.js +1 -0
  38. package/dist/commands/index.js.map +1 -1
  39. package/dist/commands/integrations.d.ts +17 -0
  40. package/dist/commands/integrations.js +1778 -0
  41. package/dist/commands/integrations.js.map +1 -0
  42. package/dist/commands/logs.js +33 -12
  43. package/dist/commands/logs.js.map +1 -1
  44. package/dist/commands/marketplace.js +3 -2
  45. package/dist/commands/marketplace.js.map +1 -1
  46. package/dist/commands/mcp.d.ts +19 -0
  47. package/dist/commands/mcp.js +3 -3
  48. package/dist/commands/mcp.js.map +1 -1
  49. package/dist/commands/sync.d.ts +5 -9
  50. package/dist/commands/sync.js +146 -102
  51. package/dist/commands/sync.js.map +1 -1
  52. package/dist/commands/test.js +41 -13
  53. package/dist/commands/test.js.map +1 -1
  54. package/dist/interfaces/mcp.d.ts +11 -0
  55. package/dist/interfaces/unifiedto.d.ts +91 -0
  56. package/dist/interfaces/unifiedto.js +6 -0
  57. package/dist/interfaces/unifiedto.js.map +1 -0
  58. package/dist/interfaces/user.d.ts +9 -0
  59. package/dist/types/api-contracts.d.ts +5 -3
  60. package/dist/utils/auth-flows.d.ts +29 -1
  61. package/dist/utils/auth-flows.js +84 -1
  62. package/dist/utils/auth-flows.js.map +1 -1
  63. package/dist/utils/sandbox.d.ts +2 -2
  64. package/dist/utils/sandbox.js +1 -1
  65. package/package.json +1 -1
  66. package/template/package.json +1 -1
@@ -0,0 +1,1778 @@
1
+ /**
2
+ * Integrations Command
3
+ * Manages third-party account connections via Unified.to
4
+ *
5
+ * Core concepts:
6
+ * - Integration: A supported third-party service (e.g., Linear, Google Calendar)
7
+ * - Connection: An authenticated link between your agent and a specific account
8
+ * - MCP Server: Technical implementation that exposes connection tools to the agent (auto-managed)
9
+ *
10
+ * Flow:
11
+ * 1. User runs `lua integrations connect`
12
+ * 2. CLI fetches available integrations from Lua API
13
+ * 3. User authorizes via OAuth or provides API credentials
14
+ * 4. Connection is established and stored
15
+ * 5. MCP server is automatically created to expose tools to the agent
16
+ */
17
+ import http from 'http';
18
+ import { URL } from 'url';
19
+ import open from 'open';
20
+ import { loadApiKey, checkApiKey } from '../services/auth.js';
21
+ import { readSkillConfig } from '../utils/files.js';
22
+ import { withErrorHandling, writeProgress, writeSuccess, writeInfo, writeError } from '../utils/cli.js';
23
+ import { BASE_URLS } from '../config/constants.js';
24
+ import { safePrompt } from '../utils/prompt-handler.js';
25
+ import { validateConfig, validateAgentConfig } from '../utils/dev-helpers.js';
26
+ import DeveloperApi from '../api/developer.api.service.js';
27
+ import UnifiedToApi from '../api/unifiedto.api.service.js';
28
+ import { fetchServersCore, activateServerCore, deactivateServerCore } from './mcp.js';
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Configuration
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ const UNIFIED_MCP_BASE_URL = 'https://mcp-api.unified.to/mcp';
33
+ const CALLBACK_PORT = 19837; // Random high port for OAuth callback
34
+ const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Integration API Functions (via Lua API)
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+ /**
39
+ * Fetches available (activated) integrations via Lua API
40
+ */
41
+ async function fetchAvailableIntegrations(unifiedToApi) {
42
+ const result = await unifiedToApi.getAvailableIntegrations();
43
+ if (!result.success || !result.data) {
44
+ throw new Error(`Failed to fetch integrations: ${result.error?.message || 'Unknown error'}`);
45
+ }
46
+ return result.data.map((integration) => ({
47
+ name: integration.name,
48
+ value: integration.type,
49
+ categories: integration.categories || [],
50
+ authSupport: integration.authSupport,
51
+ oauthConfigured: integration.oauthConfigured,
52
+ oauthScopes: integration.oauthScopes,
53
+ tokenFields: integration.tokenFields,
54
+ }));
55
+ }
56
+ /**
57
+ * Starts a local HTTP server to receive the OAuth callback
58
+ * Returns a promise that resolves when the callback is received
59
+ */
60
+ function startCallbackServer(timeoutMs = 300000) {
61
+ return new Promise((resolve) => {
62
+ let resolved = false;
63
+ const server = http.createServer((req, res) => {
64
+ if (resolved)
65
+ return;
66
+ const reqUrl = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
67
+ if (reqUrl.pathname === '/callback') {
68
+ const connectionId = reqUrl.searchParams.get('id');
69
+ const error = reqUrl.searchParams.get('error');
70
+ const integrationType = reqUrl.searchParams.get('type');
71
+ resolved = true;
72
+ if (error) {
73
+ // Send error page
74
+ res.writeHead(200, { 'Content-Type': 'text/html' });
75
+ res.end(`
76
+ <!DOCTYPE html>
77
+ <html>
78
+ <head><title>Connection Failed</title></head>
79
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e;">
80
+ <div style="text-align: center; color: white;">
81
+ <h1 style="color: #ff6b6b;">āŒ Connection Failed</h1>
82
+ <p style="color: #ccc;">Error: ${error}</p>
83
+ <p style="color: #888;">You can close this window and try again.</p>
84
+ </div>
85
+ </body>
86
+ </html>
87
+ `);
88
+ server.close();
89
+ resolve({ success: false, error });
90
+ }
91
+ else if (connectionId) {
92
+ // Send success page
93
+ res.writeHead(200, { 'Content-Type': 'text/html' });
94
+ res.end(`
95
+ <!DOCTYPE html>
96
+ <html>
97
+ <head><title>Connection Successful</title></head>
98
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e;">
99
+ <div style="text-align: center; color: white;">
100
+ <h1 style="color: #4ade80;">āœ… Connection Successful!</h1>
101
+ <p style="color: #ccc;">Your account has been connected.</p>
102
+ <p style="color: #888;">You can close this window and return to the terminal.</p>
103
+ </div>
104
+ </body>
105
+ </html>
106
+ `);
107
+ server.close();
108
+ resolve({ success: true, connectionId, integrationType: integrationType || undefined });
109
+ }
110
+ else {
111
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
112
+ res.end('Missing connection ID');
113
+ }
114
+ }
115
+ else {
116
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
117
+ res.end('Not found');
118
+ }
119
+ });
120
+ server.listen(CALLBACK_PORT, () => {
121
+ // Server started
122
+ });
123
+ // Timeout after specified time
124
+ setTimeout(() => {
125
+ if (!resolved) {
126
+ resolved = true;
127
+ server.close();
128
+ resolve({ success: false, error: 'Timeout waiting for OAuth callback' });
129
+ }
130
+ }, timeoutMs);
131
+ });
132
+ }
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+ // Main Command Entry
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+ export async function integrationsCommand(action, subaction, cmdObj) {
137
+ return withErrorHandling(async () => {
138
+ const config = readSkillConfig();
139
+ validateConfig(config);
140
+ validateAgentConfig(config);
141
+ const agentId = config.agent.agentId;
142
+ const apiKey = await loadApiKey();
143
+ if (!apiKey) {
144
+ console.error("āŒ No API key found. Please run 'lua auth configure' to set up your API key.");
145
+ process.exit(1);
146
+ }
147
+ const userData = await checkApiKey(apiKey);
148
+ writeProgress("āœ… Authenticated with Lua");
149
+ const userId = userData.admin?.userId;
150
+ if (!userId) {
151
+ console.error("āŒ Failed to get user ID from authentication.");
152
+ process.exit(1);
153
+ }
154
+ const developerApi = new DeveloperApi(BASE_URLS.API, apiKey, agentId);
155
+ const unifiedToApi = new UnifiedToApi(BASE_URLS.API, apiKey);
156
+ const context = {
157
+ agentId,
158
+ userId,
159
+ apiKey,
160
+ developerApi,
161
+ unifiedToApi,
162
+ };
163
+ if (action) {
164
+ // Pass subaction via cmdObj for webhooks command
165
+ const enhancedCmdObj = { ...cmdObj, _: subaction ? [subaction] : [] };
166
+ await executeNonInteractive(context, action, enhancedCmdObj);
167
+ }
168
+ else {
169
+ await interactiveIntegrationsManagement(context);
170
+ }
171
+ }, "integrations");
172
+ }
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+ // Non-Interactive Mode
175
+ // ─────────────────────────────────────────────────────────────────────────────
176
+ async function executeNonInteractive(context, action, cmdOptions) {
177
+ const normalizedAction = action.toLowerCase();
178
+ // Extract typed options
179
+ const options = {
180
+ integration: cmdOptions?.integration,
181
+ connectionId: cmdOptions?.connectionId,
182
+ authMethod: cmdOptions?.authMethod,
183
+ scopes: cmdOptions?.scopes,
184
+ hideSensitive: cmdOptions?.hideSensitive !== 'false', // Default true, only false if explicitly set
185
+ };
186
+ switch (normalizedAction) {
187
+ case 'connect':
188
+ await connectIntegrationFlow(context, options);
189
+ break;
190
+ case 'update':
191
+ if (!options.integration) {
192
+ console.error("āŒ --integration is required for update");
193
+ console.log("\nšŸ’” Run 'lua integrations list' to see connected integrations");
194
+ process.exit(1);
195
+ }
196
+ await updateConnectionFlow(context, options);
197
+ break;
198
+ case 'list':
199
+ await listConnections(context);
200
+ break;
201
+ case 'available':
202
+ await listAvailableIntegrations(context);
203
+ break;
204
+ case 'disconnect':
205
+ if (!options.connectionId) {
206
+ console.error("āŒ --connection-id is required for disconnect");
207
+ console.log("\nšŸ’” Run 'lua integrations list' to see connection IDs");
208
+ process.exit(1);
209
+ }
210
+ await disconnectIntegration(context, options.connectionId);
211
+ break;
212
+ case 'webhooks':
213
+ await webhooksSubcommand(context, cmdOptions);
214
+ break;
215
+ case 'mcp':
216
+ await mcpSubcommand(context, cmdOptions);
217
+ break;
218
+ default:
219
+ console.error(`āŒ Invalid action: "${action}"`);
220
+ showUsage();
221
+ process.exit(1);
222
+ }
223
+ }
224
+ // ─────────────────────────────────────────────────────────────────────────────
225
+ // Interactive Mode
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+ async function interactiveIntegrationsManagement(context) {
228
+ let continueManaging = true;
229
+ while (continueManaging) {
230
+ console.log("\n" + "=".repeat(60));
231
+ console.log("šŸ”— Third-Party Integrations (via Unified.to)");
232
+ console.log("=".repeat(60) + "\n");
233
+ const actionAnswer = await safePrompt([
234
+ {
235
+ type: 'list',
236
+ name: 'action',
237
+ message: 'What would you like to do?',
238
+ choices: [
239
+ { name: 'āž• Connect a new account', value: 'connect' },
240
+ { name: 'šŸ”„ Update connection scopes', value: 'update' },
241
+ { name: 'šŸ“‹ List connected accounts', value: 'list' },
242
+ { name: 'šŸ” View available integrations', value: 'available' },
243
+ { name: 'šŸ”” Manage webhook subscriptions', value: 'webhooks' },
244
+ { name: 'šŸ”Œ Manage MCP servers', value: 'mcp' },
245
+ { name: 'šŸ—‘ļø Disconnect an account', value: 'disconnect' },
246
+ { name: 'āŒ Exit', value: 'exit' }
247
+ ]
248
+ }
249
+ ]);
250
+ if (!actionAnswer)
251
+ return;
252
+ const { action } = actionAnswer;
253
+ switch (action) {
254
+ case 'connect':
255
+ await connectIntegrationFlow(context);
256
+ break;
257
+ case 'update':
258
+ await updateConnectionFlow(context);
259
+ break;
260
+ case 'list':
261
+ await listConnections(context);
262
+ break;
263
+ case 'available':
264
+ await listAvailableIntegrations(context);
265
+ break;
266
+ case 'webhooks':
267
+ await webhooksInteractiveMenu(context);
268
+ break;
269
+ case 'mcp':
270
+ await mcpManagementFlow(context, {});
271
+ break;
272
+ case 'disconnect':
273
+ await disconnectIntegrationInteractive(context);
274
+ break;
275
+ case 'exit':
276
+ continueManaging = false;
277
+ console.log("\nšŸ‘‹ Goodbye!\n");
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+ // List Available Integrations
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+ async function listAvailableIntegrations(context) {
286
+ writeProgress("šŸ”„ Fetching available integrations...");
287
+ try {
288
+ const integrations = await fetchAvailableIntegrations(context.unifiedToApi);
289
+ console.log("\n" + "=".repeat(60));
290
+ console.log("šŸ”Œ Available Integrations");
291
+ console.log("=".repeat(60) + "\n");
292
+ if (integrations.length === 0) {
293
+ console.log("ā„¹ļø No integrations available.");
294
+ console.log("šŸ’” Contact support to enable integrations for your workspace.\n");
295
+ return;
296
+ }
297
+ // Group by category
298
+ const byCategory = {};
299
+ for (const integration of integrations) {
300
+ const category = integration.categories[0] || 'other';
301
+ if (!byCategory[category])
302
+ byCategory[category] = [];
303
+ byCategory[category].push(integration);
304
+ }
305
+ for (const [category, items] of Object.entries(byCategory)) {
306
+ console.log(`šŸ“ ${category.toUpperCase()}`);
307
+ items.forEach(i => {
308
+ const authBadge = i.authSupport === 'oauth' ? 'šŸ”' : i.authSupport === 'token' ? 'šŸ”‘' : 'šŸ”šŸ”‘';
309
+ console.log(` ${authBadge} ${i.name} (${i.value})`);
310
+ });
311
+ console.log();
312
+ }
313
+ console.log("=".repeat(60));
314
+ console.log(`Total: ${integrations.length} integration(s) available`);
315
+ console.log("šŸ” = OAuth šŸ”‘ = API Key/Token šŸ”šŸ”‘ = Both\n");
316
+ }
317
+ catch (error) {
318
+ writeError(`āŒ Failed to fetch integrations: ${error.message}`);
319
+ }
320
+ }
321
+ // ─────────────────────────────────────────────────────────────────────────────
322
+ // Connect Flow
323
+ // ─────────────────────────────────────────────────────────────────────────────
324
+ async function connectIntegrationFlow(context, options = {}) {
325
+ // Step 1: Fetch available integrations and existing connections
326
+ writeProgress("šŸ”„ Fetching integrations...");
327
+ let integrations;
328
+ let existingConnections = [];
329
+ try {
330
+ const [integrationsResult, connectionsResult] = await Promise.all([
331
+ fetchAvailableIntegrations(context.unifiedToApi),
332
+ context.unifiedToApi.getConnections(context.agentId)
333
+ ]);
334
+ integrations = integrationsResult;
335
+ existingConnections = connectionsResult.success ? connectionsResult.data || [] : [];
336
+ }
337
+ catch (error) {
338
+ writeError(`āŒ Failed to fetch integrations: ${error.message}`);
339
+ return;
340
+ }
341
+ if (integrations.length === 0) {
342
+ writeError("āŒ No integrations available.");
343
+ console.log("šŸ’” Contact support to enable integrations for your workspace.\n");
344
+ return;
345
+ }
346
+ // Filter out integrations that already have a connection (1 connection per integration type)
347
+ const connectedTypes = new Set(existingConnections.map(c => c.integrationType));
348
+ const availableIntegrations = integrations.filter(i => !connectedTypes.has(i.value));
349
+ if (availableIntegrations.length === 0) {
350
+ writeInfo("All available integrations are already connected.");
351
+ console.log("šŸ’” Use 'lua integrations update' to change scopes on an existing connection.\n");
352
+ return;
353
+ }
354
+ writeSuccess(`Found ${availableIntegrations.length} integration(s) available to connect`);
355
+ // Step 2: Select integration (with search/filter support)
356
+ let selectedIntegration;
357
+ if (options.integration) {
358
+ // Check if already connected
359
+ if (connectedTypes.has(options.integration)) {
360
+ console.error(`āŒ Integration "${options.integration}" is already connected.`);
361
+ console.log("šŸ’” Use 'lua integrations update --integration " + options.integration + "' to change scopes.\n");
362
+ process.exit(1);
363
+ }
364
+ selectedIntegration = availableIntegrations.find(i => i.value === options.integration);
365
+ if (!selectedIntegration) {
366
+ console.error(`āŒ Integration "${options.integration}" not found or not available.`);
367
+ console.log('\nAvailable integrations to connect:');
368
+ availableIntegrations.forEach(i => console.log(` - ${i.value} (${i.name})`));
369
+ process.exit(1);
370
+ }
371
+ }
372
+ else {
373
+ // Ask if user wants to search or browse all
374
+ const searchAnswer = await safePrompt([
375
+ {
376
+ type: 'input',
377
+ name: 'searchTerm',
378
+ message: 'Search integrations (or press Enter to browse all):',
379
+ }
380
+ ]);
381
+ if (!searchAnswer)
382
+ return;
383
+ // Filter integrations based on search term
384
+ let filteredIntegrations = availableIntegrations;
385
+ if (searchAnswer.searchTerm.trim()) {
386
+ const searchLower = searchAnswer.searchTerm.toLowerCase().trim();
387
+ filteredIntegrations = availableIntegrations.filter(i => i.name.toLowerCase().includes(searchLower) ||
388
+ i.value.toLowerCase().includes(searchLower) ||
389
+ i.categories.some(c => c.toLowerCase().includes(searchLower)));
390
+ if (filteredIntegrations.length === 0) {
391
+ // Check if the search matches an already-connected integration
392
+ const matchingConnected = existingConnections.filter(c => c.integrationType.toLowerCase().includes(searchLower) ||
393
+ (c.integrationName && c.integrationName.toLowerCase().includes(searchLower)));
394
+ if (matchingConnected.length > 0) {
395
+ const connectedName = matchingConnected[0].integrationName || matchingConnected[0].integrationType;
396
+ writeInfo(`"${connectedName}" is already connected.`);
397
+ console.log(`šŸ’” Use 'lua integrations update' to change scopes.\n`);
398
+ return;
399
+ }
400
+ writeInfo(`No integrations found matching "${searchAnswer.searchTerm}"`);
401
+ // Offer to browse all
402
+ const browseAnswer = await safePrompt([
403
+ {
404
+ type: 'confirm',
405
+ name: 'browseAll',
406
+ message: 'Would you like to browse all available integrations?',
407
+ default: true
408
+ }
409
+ ]);
410
+ if (!browseAnswer?.browseAll)
411
+ return;
412
+ filteredIntegrations = availableIntegrations;
413
+ }
414
+ else {
415
+ writeInfo(`Found ${filteredIntegrations.length} integration(s) matching "${searchAnswer.searchTerm}"`);
416
+ }
417
+ }
418
+ const integrationAnswer = await safePrompt([
419
+ {
420
+ type: 'list',
421
+ name: 'integration',
422
+ message: 'Select an integration to connect:',
423
+ pageSize: 15,
424
+ choices: filteredIntegrations.map(i => {
425
+ const authBadge = i.authSupport === 'oauth' ? 'šŸ”' : i.authSupport === 'token' ? 'šŸ”‘' : 'šŸ”šŸ”‘';
426
+ return {
427
+ name: `${authBadge} ${i.name} (${i.categories.join(', ')})`,
428
+ value: i
429
+ };
430
+ })
431
+ }
432
+ ]);
433
+ if (!integrationAnswer)
434
+ return;
435
+ selectedIntegration = integrationAnswer.integration;
436
+ }
437
+ console.log(`\nšŸ“Œ Selected: ${selectedIntegration.name}`);
438
+ console.log(` Categories: ${selectedIntegration.categories.join(', ')}`);
439
+ console.log(` Auth Support: ${selectedIntegration.authSupport}`);
440
+ console.log(` OAuth Configured: ${selectedIntegration.oauthConfigured ? 'Yes' : 'No'}`);
441
+ // Step 3: Determine auth method and get scopes
442
+ const canUseOAuth = selectedIntegration.oauthConfigured &&
443
+ ['oauth', 'both'].includes(selectedIntegration.authSupport);
444
+ const canUseToken = ['token', 'both'].includes(selectedIntegration.authSupport);
445
+ let authMethod;
446
+ let selectedScopes = [];
447
+ // Validate --auth-method if provided
448
+ if (options.authMethod) {
449
+ if (!['oauth', 'token'].includes(options.authMethod)) {
450
+ console.error(`āŒ Invalid --auth-method: "${options.authMethod}". Use 'oauth' or 'token'`);
451
+ process.exit(1);
452
+ }
453
+ if (options.authMethod === 'oauth' && !canUseOAuth) {
454
+ console.error(`āŒ OAuth is not available for ${selectedIntegration.name}.`);
455
+ if (canUseToken) {
456
+ console.log(`šŸ’” Use --auth-method token instead.`);
457
+ }
458
+ process.exit(1);
459
+ }
460
+ if (options.authMethod === 'token' && !canUseToken) {
461
+ console.error(`āŒ Token authentication is not available for ${selectedIntegration.name}.`);
462
+ if (canUseOAuth) {
463
+ console.log(`šŸ’” Use --auth-method oauth instead.`);
464
+ }
465
+ process.exit(1);
466
+ }
467
+ authMethod = options.authMethod;
468
+ writeInfo(`Using ${authMethod === 'oauth' ? 'OAuth 2.0' : 'API Token'} authentication`);
469
+ }
470
+ else if (canUseOAuth && canUseToken) {
471
+ // Both available - let user choose interactively
472
+ const authAnswer = await safePrompt([{
473
+ type: 'list',
474
+ name: 'method',
475
+ message: 'Choose authentication method:',
476
+ choices: [
477
+ { name: 'šŸ” OAuth 2.0 (recommended)', value: 'oauth' },
478
+ { name: 'šŸ”‘ API Token / Personal Access Token', value: 'token' }
479
+ ]
480
+ }]);
481
+ if (!authAnswer)
482
+ return;
483
+ authMethod = authAnswer.method;
484
+ }
485
+ else if (canUseOAuth) {
486
+ authMethod = 'oauth';
487
+ writeInfo('Using OAuth 2.0 authentication');
488
+ }
489
+ else if (canUseToken) {
490
+ authMethod = 'token';
491
+ if (selectedIntegration.authSupport === 'both' && !selectedIntegration.oauthConfigured) {
492
+ writeInfo('OAuth is not configured - using API Token authentication');
493
+ }
494
+ }
495
+ else {
496
+ writeError('This integration requires OAuth but it is not configured. Please configure OAuth credentials in the Unified.to dashboard.');
497
+ return;
498
+ }
499
+ // Handle OAuth scope selection
500
+ if (authMethod === 'oauth' && selectedIntegration.oauthScopes?.length) {
501
+ const availableScopes = selectedIntegration.oauthScopes.map(s => s.unifiedScope);
502
+ if (options.scopes) {
503
+ // Non-interactive: use provided scopes
504
+ if (options.scopes.toLowerCase() === 'all') {
505
+ selectedScopes = availableScopes;
506
+ writeInfo(`Using all ${selectedScopes.length} available scope(s)`);
507
+ }
508
+ else {
509
+ // Parse comma-separated scopes
510
+ const requestedScopes = options.scopes.split(',').map(s => s.trim());
511
+ const invalidScopes = requestedScopes.filter(s => !availableScopes.includes(s));
512
+ if (invalidScopes.length > 0) {
513
+ console.error(`āŒ Invalid scopes: ${invalidScopes.join(', ')}`);
514
+ console.log(`\nAvailable scopes for ${selectedIntegration.name}:`);
515
+ availableScopes.forEach(s => console.log(` - ${s}`));
516
+ process.exit(1);
517
+ }
518
+ selectedScopes = requestedScopes;
519
+ writeInfo(`Using ${selectedScopes.length} specified scope(s)`);
520
+ }
521
+ }
522
+ else {
523
+ // Interactive: prompt for scope selection
524
+ const scopeAnswer = await safePrompt([{
525
+ type: 'checkbox',
526
+ name: 'scopes',
527
+ message: 'Select OAuth scopes (Space to toggle, Enter to confirm, leave empty for all):',
528
+ pageSize: 15,
529
+ loop: false,
530
+ choices: selectedIntegration.oauthScopes.map(s => ({
531
+ name: `${s.unifiedScope} → [${s.originalScopes.join(', ')}]`,
532
+ value: s.unifiedScope,
533
+ checked: false
534
+ }))
535
+ }]);
536
+ if (!scopeAnswer)
537
+ return;
538
+ // If none selected, request all scopes
539
+ selectedScopes = scopeAnswer.scopes.length > 0
540
+ ? scopeAnswer.scopes
541
+ : availableScopes;
542
+ console.log(`\nāœ“ Will request ${selectedScopes.length} scope(s)`);
543
+ }
544
+ }
545
+ else if (authMethod === 'oauth') {
546
+ // OAuth without configurable scopes - Unified.to handles defaults internally
547
+ if (options.scopes) {
548
+ writeInfo('Note: This integration does not have configurable scopes');
549
+ }
550
+ // selectedScopes stays empty - MCP URL will omit permissions param
551
+ }
552
+ // Handle token field display
553
+ if (authMethod === 'token' && selectedIntegration.tokenFields?.length) {
554
+ console.log('\nšŸ”‘ You will need to provide the following credentials:\n');
555
+ selectedIntegration.tokenFields.forEach((field, i) => {
556
+ console.log(` ${i + 1}. ${field.name}`);
557
+ if (field.instructions) {
558
+ console.log(` šŸ’” ${field.instructions}`);
559
+ }
560
+ });
561
+ console.log('\nYou will enter these on the Unified.to authorization page.\n');
562
+ }
563
+ // Determine hide_sensitive setting
564
+ let hideSensitive = true; // Default to true (hide sensitive data)
565
+ if (options.hideSensitive !== undefined) {
566
+ hideSensitive = options.hideSensitive;
567
+ }
568
+ else {
569
+ // Interactive: ask user
570
+ const sensitiveAnswer = await safePrompt([{
571
+ type: 'confirm',
572
+ name: 'hide',
573
+ message: 'Hide sensitive data from MCP tools? (recommended for security)',
574
+ default: true
575
+ }]);
576
+ if (sensitiveAnswer) {
577
+ hideSensitive = sensitiveAnswer.hide;
578
+ }
579
+ }
580
+ // Step 4: Get authorization URL from Lua API
581
+ writeProgress("šŸ”„ Preparing authorization...");
582
+ const state = Buffer.from(JSON.stringify({
583
+ agentId: context.agentId,
584
+ integration: selectedIntegration.value,
585
+ authMethod,
586
+ timestamp: Date.now()
587
+ })).toString('base64');
588
+ // Encode both agentId and userId in externalXref for the connection
589
+ const externalXref = JSON.stringify({
590
+ agentId: context.agentId,
591
+ userId: context.userId,
592
+ });
593
+ const authUrlResult = await context.unifiedToApi.getAuthUrl(selectedIntegration.value, {
594
+ successRedirect: CALLBACK_URL,
595
+ failureRedirect: `${CALLBACK_URL}?error=auth_failed`,
596
+ scopes: authMethod === 'oauth' ? selectedScopes : undefined,
597
+ state,
598
+ externalXref, // Contains both agentId and userId as JSON
599
+ });
600
+ if (!authUrlResult.success || !authUrlResult.data) {
601
+ writeError(`āŒ Failed to get authorization URL: ${authUrlResult.error?.message || 'Unknown error'}`);
602
+ return;
603
+ }
604
+ const authUrl = authUrlResult.data.authUrl;
605
+ // Step 4: Pre-fetch the auth URL to handle redirects
606
+ let finalAuthUrl = authUrl;
607
+ try {
608
+ const response = await fetch(authUrl);
609
+ const responseText = await response.text();
610
+ if (responseText.includes('test.html?redirect=')) {
611
+ const urlMatch = responseText.match(/redirect=([^&\s]+)/);
612
+ if (urlMatch) {
613
+ finalAuthUrl = decodeURIComponent(urlMatch[1]);
614
+ }
615
+ }
616
+ else if (responseText.startsWith('https://')) {
617
+ finalAuthUrl = responseText.trim();
618
+ }
619
+ }
620
+ catch (error) {
621
+ // Use original auth URL if pre-fetch fails
622
+ }
623
+ // Step 5: Start callback server and open browser
624
+ console.log("\n" + "─".repeat(60));
625
+ console.log("🌐 Starting OAuth authorization flow...");
626
+ console.log("─".repeat(60));
627
+ console.log(`\nIntegration: ${selectedIntegration.name}`);
628
+ console.log(`\nšŸ“‹ Authorization URL (copy if browser doesn't open):\n ${finalAuthUrl}\n`);
629
+ // Start the callback server
630
+ const callbackPromise = startCallbackServer(300000); // 5 minute timeout
631
+ // Auto-open browser
632
+ try {
633
+ await open(finalAuthUrl);
634
+ writeInfo("🌐 Browser opened - please complete the authorization");
635
+ }
636
+ catch (error) {
637
+ writeInfo("šŸ’” Could not open browser automatically. Please open the URL above manually.");
638
+ }
639
+ console.log("\nā³ Waiting for authorization (5 minute timeout)...");
640
+ console.log("šŸ’” Complete the authorization in your browser.\n");
641
+ // Wait for callback
642
+ const result = await callbackPromise;
643
+ if (result.success && result.connectionId) {
644
+ writeSuccess("\nāœ… Authorization successful!");
645
+ // Finalize the connection (sets up MCP server internally)
646
+ await finalizeConnection(context, selectedIntegration, result.connectionId, selectedScopes, hideSensitive);
647
+ }
648
+ else {
649
+ writeError(`\nāŒ Authorization failed: ${result.error || 'Unknown error'}`);
650
+ console.log("šŸ’” Please try again with 'lua integrations connect'\n");
651
+ }
652
+ }
653
+ // ─────────────────────────────────────────────────────────────────────────────
654
+ // Connection Management
655
+ // ─────────────────────────────────────────────────────────────────────────────
656
+ /**
657
+ * Finalizes a new connection by setting up the MCP server (internal implementation)
658
+ * The MCP server enables the agent to use tools from this connection
659
+ */
660
+ async function finalizeConnection(context, integration, connectionId, scopes, hideSensitive = true) {
661
+ writeProgress(`šŸ”„ Setting up ${integration.name} connection...`);
662
+ // Build MCP URL
663
+ let mcpUrl = `${UNIFIED_MCP_BASE_URL}?connection=${connectionId}`;
664
+ if (hideSensitive) {
665
+ mcpUrl += '&hide_sensitive=true';
666
+ }
667
+ if (scopes.length > 0) {
668
+ mcpUrl += `&permissions=${scopes.join(',')}`;
669
+ }
670
+ // Always defer tools to reduce initial tool loading overhead with LLMs
671
+ mcpUrl += '&defer_tools=true';
672
+ // Use simple integration type as name (e.g., 'discord', 'linear')
673
+ // This makes tool names cleaner: discord_create_message vs unified-discord-abc123_create_message
674
+ const serverName = integration.value;
675
+ const mcpServerData = {
676
+ name: serverName,
677
+ transport: 'streamable-http',
678
+ url: mcpUrl,
679
+ source: 'unifiedto', // Flag for runtime auth injection
680
+ timeout: 30000,
681
+ };
682
+ try {
683
+ const result = await context.developerApi.createMCPServer(mcpServerData);
684
+ if (result.success && result.data) {
685
+ // Auto-activate the MCP server so the agent can use the connection immediately
686
+ const activateResult = await context.developerApi.activateMCPServer(result.data.id);
687
+ const isActive = activateResult.success && activateResult.data?.active;
688
+ // Connection success message
689
+ console.log("\n" + "─".repeat(60));
690
+ console.log("šŸŽ‰ Connection Established!");
691
+ console.log("─".repeat(60));
692
+ console.log(`\n Integration: ${integration.name}`);
693
+ console.log(` Connection ID: ${connectionId}`);
694
+ console.log(` Status: ${isActive ? '🟢 Active' : '⚪ Pending'}`);
695
+ // Show requested scopes (actual capabilities)
696
+ if (scopes.length > 0) {
697
+ console.log(`\n Scopes:`);
698
+ scopes.forEach(s => console.log(` - ${s}`));
699
+ }
700
+ else {
701
+ console.log(`\n Scopes: Default permissions`);
702
+ }
703
+ console.log(` Hide Sensitive: ${hideSensitive ? 'āœ… Yes (recommended)' : 'āŒ No'}`);
704
+ if (!isActive) {
705
+ console.log(`\nāš ļø Connection is pending activation.`);
706
+ console.log(` Run: lua mcp activate --server-name ${serverName}`);
707
+ }
708
+ else {
709
+ console.log(`\nāœ… Your agent can now use ${integration.name} tools!`);
710
+ }
711
+ console.log();
712
+ }
713
+ else {
714
+ writeError(`āŒ Failed to set up connection: ${result.error?.message}`);
715
+ }
716
+ }
717
+ catch (error) {
718
+ writeError(`āŒ Error setting up connection: ${error.message}`);
719
+ }
720
+ }
721
+ async function listConnections(context) {
722
+ writeProgress("šŸ”„ Loading connections...");
723
+ try {
724
+ // Fetch both connections and MCP servers (MCP status is secondary info)
725
+ const [connectionsResult, serversResult] = await Promise.all([
726
+ context.unifiedToApi.getConnections(context.agentId),
727
+ context.developerApi.getMCPServers()
728
+ ]);
729
+ const connections = connectionsResult.success ? connectionsResult.data || [] : [];
730
+ const servers = serversResult.success ? serversResult.data || [] : [];
731
+ const unifiedServers = servers.filter(s => s.source === 'unifiedto');
732
+ // Map connection IDs to MCP servers to check tool availability
733
+ const serverByConnectionId = new Map();
734
+ for (const server of unifiedServers) {
735
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
736
+ if (connectionMatch) {
737
+ serverByConnectionId.set(connectionMatch[1], server);
738
+ }
739
+ }
740
+ console.log("\n" + "=".repeat(60));
741
+ console.log("šŸ”— Connected Accounts");
742
+ console.log("=".repeat(60) + "\n");
743
+ if (connections.length === 0) {
744
+ console.log("ā„¹ļø No accounts connected yet.");
745
+ console.log("šŸ’” Run 'lua integrations connect' to connect a new account.\n");
746
+ return;
747
+ }
748
+ for (const connection of connections) {
749
+ const linkedServer = serverByConnectionId.get(connection.id);
750
+ const toolsAvailable = linkedServer?.active === true;
751
+ // Status based on connection health + tool availability
752
+ let statusIcon = '⚪';
753
+ let statusText = 'Inactive';
754
+ if (connection.status === 'unhealthy') {
755
+ statusIcon = 'šŸ”“';
756
+ statusText = 'Unhealthy - run "lua integrations update" to re-authorize';
757
+ }
758
+ else if (connection.status === 'paused') {
759
+ statusIcon = 'āøļø';
760
+ statusText = 'Paused';
761
+ }
762
+ else if (connection.status === 'active' && toolsAvailable) {
763
+ statusIcon = '🟢';
764
+ statusText = 'Active';
765
+ }
766
+ else if (connection.status === 'active') {
767
+ statusIcon = '🟔';
768
+ statusText = 'Connected (tools pending)';
769
+ }
770
+ console.log(`${statusIcon} ${connection.integrationName || connection.integrationType}`);
771
+ console.log(` ID: ${connection.id}`);
772
+ console.log(` Status: ${statusText}`);
773
+ console.log(` Connected: ${new Date(connection.createdAt).toLocaleDateString()}`);
774
+ console.log();
775
+ }
776
+ console.log("=".repeat(60));
777
+ console.log(`Total: ${connections.length} connection(s)\n`);
778
+ }
779
+ catch (error) {
780
+ writeError(`āŒ Error loading connections: ${error.message}`);
781
+ }
782
+ }
783
+ async function disconnectIntegration(context, connectionId) {
784
+ writeProgress(`šŸ”„ Disconnecting...`);
785
+ try {
786
+ // Clean up associated MCP server (internal implementation detail)
787
+ const mcpServers = await context.developerApi.getMCPServers();
788
+ if (mcpServers.success && mcpServers.data) {
789
+ const associatedServer = mcpServers.data.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${connectionId}`));
790
+ if (associatedServer) {
791
+ await context.developerApi.deleteMCPServer(associatedServer.id);
792
+ }
793
+ }
794
+ // Delete the connection (also deletes webhook subscriptions)
795
+ const deleteResult = await context.unifiedToApi.deleteConnection(connectionId, context.agentId);
796
+ if (deleteResult.success) {
797
+ writeSuccess(`āœ… Account disconnected successfully!`);
798
+ if (deleteResult.data?.deletedWebhooksCount && deleteResult.data.deletedWebhooksCount > 0) {
799
+ console.log(` āœ“ Deleted ${deleteResult.data.deletedWebhooksCount} webhook subscription(s)`);
800
+ }
801
+ console.log();
802
+ }
803
+ else {
804
+ writeError(`āŒ Failed to disconnect: ${deleteResult.error?.message}`);
805
+ }
806
+ }
807
+ catch (error) {
808
+ writeError(`āŒ Error disconnecting: ${error.message}`);
809
+ }
810
+ }
811
+ async function disconnectIntegrationInteractive(context) {
812
+ try {
813
+ const connectionsResult = await context.unifiedToApi.getConnections(context.agentId);
814
+ if (!connectionsResult.success) {
815
+ writeError(`āŒ Failed to load connections: ${connectionsResult.error?.message}`);
816
+ return;
817
+ }
818
+ const connections = connectionsResult.data || [];
819
+ if (connections.length === 0) {
820
+ console.log("\nā„¹ļø No accounts to disconnect.\n");
821
+ return;
822
+ }
823
+ const connectionAnswer = await safePrompt([
824
+ {
825
+ type: 'list',
826
+ name: 'connection',
827
+ message: 'Select an account to disconnect:',
828
+ choices: connections.map(c => ({
829
+ name: `${c.integrationName || c.integrationType} (${c.id.substring(0, 8)}...)`,
830
+ value: c.id
831
+ }))
832
+ }
833
+ ]);
834
+ if (!connectionAnswer?.connection)
835
+ return;
836
+ const selectedConnection = connections.find(c => c.id === connectionAnswer.connection);
837
+ const confirmAnswer = await safePrompt([
838
+ {
839
+ type: 'confirm',
840
+ name: 'confirm',
841
+ message: `Disconnect ${selectedConnection?.integrationName || selectedConnection?.integrationType}? Your agent will lose access to this account.`,
842
+ default: false
843
+ }
844
+ ]);
845
+ if (!confirmAnswer?.confirm) {
846
+ console.log("\nāŒ Cancelled.\n");
847
+ return;
848
+ }
849
+ await disconnectIntegration(context, connectionAnswer.connection);
850
+ }
851
+ catch (error) {
852
+ writeError(`āŒ Error: ${error.message}`);
853
+ }
854
+ }
855
+ // ─────────────────────────────────────────────────────────────────────────────
856
+ // Update Connection Flow
857
+ // ─────────────────────────────────────────────────────────────────────────────
858
+ async function updateConnectionFlow(context, options = {}) {
859
+ // Step 1: Fetch existing connections and available integrations
860
+ writeProgress("šŸ”„ Loading connections...");
861
+ let connections = [];
862
+ let integrations = [];
863
+ try {
864
+ const [connectionsResult, integrationsResult] = await Promise.all([
865
+ context.unifiedToApi.getConnections(context.agentId),
866
+ fetchAvailableIntegrations(context.unifiedToApi)
867
+ ]);
868
+ connections = connectionsResult.success ? connectionsResult.data || [] : [];
869
+ integrations = integrationsResult;
870
+ }
871
+ catch (error) {
872
+ writeError(`āŒ Failed to load connections: ${error.message}`);
873
+ return;
874
+ }
875
+ if (connections.length === 0) {
876
+ writeInfo("No connections to update.");
877
+ console.log("šŸ’” Run 'lua integrations connect' to connect a new account.\n");
878
+ return;
879
+ }
880
+ // Step 2: Select connection to update
881
+ let selectedConnection;
882
+ let selectedIntegration;
883
+ if (options.integration) {
884
+ // Find connection by integration type
885
+ selectedConnection = connections.find(c => c.integrationType === options.integration);
886
+ if (!selectedConnection) {
887
+ console.error(`āŒ No connection found for integration "${options.integration}".`);
888
+ console.log('\nConnected integrations:');
889
+ connections.forEach(c => console.log(` - ${c.integrationType} (${c.integrationName || c.integrationType})`));
890
+ process.exit(1);
891
+ }
892
+ selectedIntegration = integrations.find(i => i.value === options.integration);
893
+ }
894
+ else {
895
+ // Interactive selection
896
+ const connectionAnswer = await safePrompt([
897
+ {
898
+ type: 'list',
899
+ name: 'connection',
900
+ message: 'Select a connection to update:',
901
+ choices: connections.map(c => ({
902
+ name: `${c.integrationName || c.integrationType} (${c.id.substring(0, 8)}...)`,
903
+ value: c
904
+ }))
905
+ }
906
+ ]);
907
+ if (!connectionAnswer?.connection)
908
+ return;
909
+ selectedConnection = connectionAnswer.connection;
910
+ selectedIntegration = integrations.find(i => i.value === selectedConnection.integrationType);
911
+ }
912
+ if (!selectedIntegration) {
913
+ writeError(`āŒ Integration details not found for ${selectedConnection.integrationType}`);
914
+ return;
915
+ }
916
+ console.log(`\nšŸ“Œ Updating: ${selectedIntegration.name}`);
917
+ console.log(` Current Connection ID: ${selectedConnection.id}`);
918
+ // Step 3: Check if OAuth with configurable scopes
919
+ const canUseOAuth = selectedIntegration.oauthConfigured &&
920
+ ['oauth', 'both'].includes(selectedIntegration.authSupport);
921
+ if (!canUseOAuth || !selectedIntegration.oauthScopes?.length) {
922
+ writeInfo("This integration does not have configurable scopes.");
923
+ console.log("šŸ’” To reconnect with different credentials, disconnect and connect again.\n");
924
+ return;
925
+ }
926
+ // Step 4: Select new scopes
927
+ const availableScopes = selectedIntegration.oauthScopes.map(s => s.unifiedScope);
928
+ let selectedScopes = [];
929
+ if (options.scopes) {
930
+ // Non-interactive: use provided scopes
931
+ if (options.scopes.toLowerCase() === 'all') {
932
+ selectedScopes = availableScopes;
933
+ writeInfo(`Using all ${selectedScopes.length} available scope(s)`);
934
+ }
935
+ else {
936
+ const requestedScopes = options.scopes.split(',').map(s => s.trim());
937
+ const invalidScopes = requestedScopes.filter(s => !availableScopes.includes(s));
938
+ if (invalidScopes.length > 0) {
939
+ console.error(`āŒ Invalid scopes: ${invalidScopes.join(', ')}`);
940
+ console.log(`\nAvailable scopes for ${selectedIntegration.name}:`);
941
+ availableScopes.forEach(s => console.log(` - ${s}`));
942
+ process.exit(1);
943
+ }
944
+ selectedScopes = requestedScopes;
945
+ writeInfo(`Using ${selectedScopes.length} specified scope(s)`);
946
+ }
947
+ }
948
+ else {
949
+ // Interactive: prompt for scope selection
950
+ const scopeAnswer = await safePrompt([{
951
+ type: 'checkbox',
952
+ name: 'scopes',
953
+ message: 'Select new OAuth scopes (Space to toggle, Enter to confirm, leave empty for all):',
954
+ pageSize: 15,
955
+ loop: false,
956
+ choices: selectedIntegration.oauthScopes.map(s => ({
957
+ name: `${s.unifiedScope} → [${s.originalScopes.join(', ')}]`,
958
+ value: s.unifiedScope,
959
+ checked: false
960
+ }))
961
+ }]);
962
+ if (!scopeAnswer)
963
+ return;
964
+ selectedScopes = scopeAnswer.scopes.length > 0
965
+ ? scopeAnswer.scopes
966
+ : availableScopes;
967
+ console.log(`\nāœ“ Will request ${selectedScopes.length} scope(s)`);
968
+ }
969
+ // Determine hide_sensitive setting
970
+ let hideSensitive = true; // Default to true
971
+ if (options.hideSensitive !== undefined) {
972
+ hideSensitive = options.hideSensitive;
973
+ }
974
+ else {
975
+ // Interactive: ask user
976
+ const sensitiveAnswer = await safePrompt([{
977
+ type: 'confirm',
978
+ name: 'hide',
979
+ message: 'Hide sensitive data from MCP tools? (recommended for security)',
980
+ default: true
981
+ }]);
982
+ if (sensitiveAnswer) {
983
+ hideSensitive = sensitiveAnswer.hide;
984
+ }
985
+ }
986
+ // Step 5: Confirm the update
987
+ if (!options.integration) {
988
+ const confirmAnswer = await safePrompt([
989
+ {
990
+ type: 'confirm',
991
+ name: 'confirm',
992
+ message: `Update ${selectedIntegration.name}? This will re-authorize with new scopes.`,
993
+ default: true
994
+ }
995
+ ]);
996
+ if (!confirmAnswer?.confirm) {
997
+ console.log("\nāŒ Cancelled.\n");
998
+ return;
999
+ }
1000
+ }
1001
+ // Step 6: Delete the old connection (silently)
1002
+ writeProgress(`šŸ”„ Updating ${selectedIntegration.name}...`);
1003
+ try {
1004
+ // Clean up old MCP server
1005
+ const mcpServers = await context.developerApi.getMCPServers();
1006
+ if (mcpServers.success && mcpServers.data) {
1007
+ const associatedServer = mcpServers.data.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${selectedConnection.id}`));
1008
+ if (associatedServer) {
1009
+ await context.developerApi.deleteMCPServer(associatedServer.id);
1010
+ }
1011
+ }
1012
+ // Delete old connection
1013
+ await context.unifiedToApi.deleteConnection(selectedConnection.id, context.agentId);
1014
+ }
1015
+ catch (error) {
1016
+ writeError(`āŒ Failed to remove old connection: ${error.message}`);
1017
+ return;
1018
+ }
1019
+ // Step 7: Create new connection with new scopes
1020
+ const state = Buffer.from(JSON.stringify({
1021
+ agentId: context.agentId,
1022
+ integration: selectedIntegration.value,
1023
+ authMethod: 'oauth',
1024
+ timestamp: Date.now()
1025
+ })).toString('base64');
1026
+ const externalXref = JSON.stringify({
1027
+ agentId: context.agentId,
1028
+ userId: context.userId,
1029
+ });
1030
+ const authUrlResult = await context.unifiedToApi.getAuthUrl(selectedIntegration.value, {
1031
+ successRedirect: CALLBACK_URL,
1032
+ failureRedirect: `${CALLBACK_URL}?error=auth_failed`,
1033
+ scopes: selectedScopes,
1034
+ state,
1035
+ externalXref,
1036
+ });
1037
+ if (!authUrlResult.success || !authUrlResult.data) {
1038
+ writeError(`āŒ Failed to get authorization URL: ${authUrlResult.error?.message || 'Unknown error'}`);
1039
+ return;
1040
+ }
1041
+ const authUrl = authUrlResult.data.authUrl;
1042
+ // Pre-fetch to handle redirects
1043
+ let finalAuthUrl = authUrl;
1044
+ try {
1045
+ const response = await fetch(authUrl);
1046
+ const responseText = await response.text();
1047
+ if (responseText.includes('test.html?redirect=')) {
1048
+ const urlMatch = responseText.match(/redirect=([^&\s]+)/);
1049
+ if (urlMatch) {
1050
+ finalAuthUrl = decodeURIComponent(urlMatch[1]);
1051
+ }
1052
+ }
1053
+ else if (responseText.startsWith('https://')) {
1054
+ finalAuthUrl = responseText.trim();
1055
+ }
1056
+ }
1057
+ catch (error) {
1058
+ // Use original auth URL if pre-fetch fails
1059
+ }
1060
+ // Step 8: Open browser and wait for callback
1061
+ console.log("\n" + "─".repeat(60));
1062
+ console.log("🌐 Re-authorizing with new scopes...");
1063
+ console.log("─".repeat(60));
1064
+ console.log(`\nšŸ“‹ Authorization URL (copy if browser doesn't open):\n ${finalAuthUrl}\n`);
1065
+ const callbackPromise = startCallbackServer(300000);
1066
+ try {
1067
+ await open(finalAuthUrl);
1068
+ writeInfo("🌐 Browser opened - please complete the authorization");
1069
+ }
1070
+ catch (error) {
1071
+ writeInfo("šŸ’” Could not open browser automatically. Please open the URL above manually.");
1072
+ }
1073
+ console.log("\nā³ Waiting for authorization (5 minute timeout)...");
1074
+ console.log("šŸ’” Complete the authorization in your browser.\n");
1075
+ const result = await callbackPromise;
1076
+ if (result.success && result.connectionId) {
1077
+ writeSuccess("\nāœ… Authorization successful!");
1078
+ await finalizeConnection(context, selectedIntegration, result.connectionId, selectedScopes, hideSensitive);
1079
+ }
1080
+ else {
1081
+ writeError(`\nāŒ Authorization failed: ${result.error || 'Unknown error'}`);
1082
+ console.log("šŸ’” The old connection was removed. Please reconnect with 'lua integrations connect'\n");
1083
+ }
1084
+ }
1085
+ // ─────────────────────────────────────────────────────────────────────────────
1086
+ // Webhook Subscription Flows
1087
+ // ─────────────────────────────────────────────────────────────────────────────
1088
+ /**
1089
+ * Handle webhooks subcommand (non-interactive)
1090
+ */
1091
+ async function webhooksSubcommand(context, cmdOptions) {
1092
+ const subAction = cmdOptions?._?.[0]?.toLowerCase() || '';
1093
+ const options = {
1094
+ connectionId: cmdOptions?.connection || cmdOptions?.connectionId,
1095
+ webhookId: cmdOptions?.webhookId,
1096
+ objectType: cmdOptions?.object,
1097
+ event: cmdOptions?.event,
1098
+ webhook: cmdOptions?.webhook,
1099
+ interval: cmdOptions?.interval ? parseInt(cmdOptions.interval, 10) : undefined,
1100
+ };
1101
+ switch (subAction) {
1102
+ case 'list':
1103
+ await webhooksListFlow(context);
1104
+ break;
1105
+ case 'create':
1106
+ await webhooksCreateFlow(context, options);
1107
+ break;
1108
+ case 'delete':
1109
+ if (!options.webhookId) {
1110
+ console.error("āŒ --webhook-id is required for delete");
1111
+ console.log("\nšŸ’” Run 'lua integrations webhooks list' to see webhook IDs");
1112
+ process.exit(1);
1113
+ }
1114
+ await webhooksDeleteFlow(context, options.webhookId);
1115
+ break;
1116
+ default:
1117
+ console.error(`āŒ Invalid webhooks action: "${subAction || '(none)'}"`);
1118
+ console.log('\nUsage:');
1119
+ console.log(' lua integrations webhooks list List webhook subscriptions');
1120
+ console.log(' lua integrations webhooks create Create subscription (interactive)');
1121
+ console.log(' lua integrations webhooks create --connection <id> --object <type> --event <event> --webhook <name>');
1122
+ console.log(' lua integrations webhooks delete --webhook-id <id> Delete subscription');
1123
+ process.exit(1);
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Interactive webhooks menu
1128
+ */
1129
+ async function webhooksInteractiveMenu(context) {
1130
+ console.log("\n" + "─".repeat(60));
1131
+ console.log("šŸ”” Webhook Subscriptions");
1132
+ console.log("─".repeat(60) + "\n");
1133
+ const actionAnswer = await safePrompt([
1134
+ {
1135
+ type: 'list',
1136
+ name: 'action',
1137
+ message: 'What would you like to do?',
1138
+ choices: [
1139
+ { name: 'šŸ“‹ List webhook subscriptions', value: 'list' },
1140
+ { name: 'āž• Create new subscription', value: 'create' },
1141
+ { name: 'šŸ—‘ļø Delete subscription', value: 'delete' },
1142
+ { name: '← Back', value: 'back' }
1143
+ ]
1144
+ }
1145
+ ]);
1146
+ if (!actionAnswer || actionAnswer.action === 'back')
1147
+ return;
1148
+ switch (actionAnswer.action) {
1149
+ case 'list':
1150
+ await webhooksListFlow(context);
1151
+ break;
1152
+ case 'create':
1153
+ await webhooksCreateFlow(context, {});
1154
+ break;
1155
+ case 'delete':
1156
+ await webhooksDeleteInteractive(context);
1157
+ break;
1158
+ }
1159
+ }
1160
+ /**
1161
+ * List all webhook subscriptions
1162
+ */
1163
+ async function webhooksListFlow(context) {
1164
+ writeProgress("šŸ”„ Loading webhook subscriptions...");
1165
+ try {
1166
+ const result = await context.unifiedToApi.getWebhookSubscriptions(context.agentId);
1167
+ if (!result.success) {
1168
+ writeError(`āŒ Failed to load webhooks: ${result.error?.message}`);
1169
+ return;
1170
+ }
1171
+ const webhooks = result.data || [];
1172
+ console.log("\n" + "─".repeat(80));
1173
+ console.log("šŸ”” Webhook Subscriptions");
1174
+ console.log("─".repeat(80) + "\n");
1175
+ if (webhooks.length === 0) {
1176
+ console.log("ā„¹ļø No webhook subscriptions found.\n");
1177
+ console.log("šŸ’” Create one with: lua integrations webhooks create\n");
1178
+ return;
1179
+ }
1180
+ for (const wh of webhooks) {
1181
+ const status = wh.status === 'active' ? 'āœ… active' : wh.status;
1182
+ console.log(` ID: ${wh.id}`);
1183
+ console.log(` Integration: ${wh.integrationType} | Event: ${wh.objectType}.${wh.event} [${wh.webhookType}]`);
1184
+ console.log(` URL: ${wh.hookUrl}`);
1185
+ console.log(` Status: ${status}`);
1186
+ console.log("─".repeat(80));
1187
+ }
1188
+ console.log(`\nTotal: ${webhooks.length} webhook subscription(s)\n`);
1189
+ }
1190
+ catch (error) {
1191
+ writeError(`āŒ Error: ${error.message}`);
1192
+ }
1193
+ }
1194
+ /**
1195
+ * Create a webhook subscription
1196
+ */
1197
+ async function webhooksCreateFlow(context, options) {
1198
+ // Step 1: Fetch connections
1199
+ writeProgress("šŸ”„ Loading connections...");
1200
+ let connections = [];
1201
+ try {
1202
+ const connectionsResult = await context.unifiedToApi.getConnections(context.agentId);
1203
+ if (!connectionsResult.success) {
1204
+ writeError(`āŒ Failed to load connections: ${connectionsResult.error?.message}`);
1205
+ return;
1206
+ }
1207
+ connections = connectionsResult.data || [];
1208
+ }
1209
+ catch (error) {
1210
+ writeError(`āŒ Error: ${error.message}`);
1211
+ return;
1212
+ }
1213
+ if (connections.length === 0) {
1214
+ writeInfo("No connections available.");
1215
+ console.log("šŸ’” Run 'lua integrations connect' to connect an account first.\n");
1216
+ return;
1217
+ }
1218
+ // Step 2: Select connection
1219
+ let selectedConnection;
1220
+ if (options.connectionId) {
1221
+ selectedConnection = connections.find(c => c.id === options.connectionId);
1222
+ if (!selectedConnection) {
1223
+ console.error(`āŒ Connection "${options.connectionId}" not found.`);
1224
+ console.log('\nAvailable connections:');
1225
+ connections.forEach(c => console.log(` - ${c.id} (${c.integrationName || c.integrationType})`));
1226
+ process.exit(1);
1227
+ }
1228
+ }
1229
+ else {
1230
+ const connectionAnswer = await safePrompt([
1231
+ {
1232
+ type: 'list',
1233
+ name: 'connection',
1234
+ message: 'Select a connection:',
1235
+ choices: connections.map(c => ({
1236
+ name: `${c.integrationName || c.integrationType} (${c.id.substring(0, 8)}...)`,
1237
+ value: c
1238
+ }))
1239
+ }
1240
+ ]);
1241
+ if (!connectionAnswer)
1242
+ return;
1243
+ selectedConnection = connectionAnswer.connection;
1244
+ }
1245
+ // Step 3: Get available events for this connection
1246
+ writeProgress("šŸ”„ Loading available events...");
1247
+ let availableEvents = [];
1248
+ try {
1249
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEvents(context.agentId, selectedConnection.id);
1250
+ if (!eventsResult.success) {
1251
+ writeError(`āŒ Failed to load events: ${eventsResult.error?.message}`);
1252
+ return;
1253
+ }
1254
+ availableEvents = eventsResult.data || [];
1255
+ }
1256
+ catch (error) {
1257
+ writeError(`āŒ Error: ${error.message}`);
1258
+ return;
1259
+ }
1260
+ if (availableEvents.length === 0) {
1261
+ writeInfo(`No webhook events available for ${selectedConnection.integrationName || selectedConnection.integrationType}.`);
1262
+ console.log("šŸ’” This integration may not support webhooks.\n");
1263
+ return;
1264
+ }
1265
+ // Step 4: Select event
1266
+ let selectedEvent;
1267
+ if (options.objectType && options.event) {
1268
+ selectedEvent = availableEvents.find(e => e.objectType === options.objectType && e.event === options.event);
1269
+ if (!selectedEvent) {
1270
+ console.error(`āŒ Event '${options.objectType}.${options.event}' is not supported.`);
1271
+ console.log(`\nAvailable events for ${selectedConnection.integrationName || selectedConnection.integrationType}:`);
1272
+ availableEvents.forEach(e => console.log(` - ${e.objectType}.${e.event} [${e.webhookType}]`));
1273
+ process.exit(1);
1274
+ }
1275
+ }
1276
+ else {
1277
+ console.log(`\nAvailable webhook events for ${selectedConnection.integrationName || selectedConnection.integrationType}:\n`);
1278
+ const eventAnswer = await safePrompt([
1279
+ {
1280
+ type: 'list',
1281
+ name: 'event',
1282
+ message: 'Select an event to subscribe to:',
1283
+ pageSize: 15,
1284
+ choices: availableEvents.map(e => ({
1285
+ name: `${e.objectTypeDisplay} - ${e.event.charAt(0).toUpperCase() + e.event.slice(1)} [${e.webhookType}]`,
1286
+ value: e
1287
+ }))
1288
+ }
1289
+ ]);
1290
+ if (!eventAnswer)
1291
+ return;
1292
+ selectedEvent = eventAnswer.event;
1293
+ }
1294
+ // Step 5: Get webhook URL
1295
+ let hookUrl;
1296
+ if (options.webhook) {
1297
+ hookUrl = options.webhook;
1298
+ }
1299
+ else {
1300
+ const webhookAnswer = await safePrompt([
1301
+ {
1302
+ type: 'input',
1303
+ name: 'hookUrl',
1304
+ message: 'Enter the full webhook URL to receive events:',
1305
+ validate: (input) => {
1306
+ if (!input.trim())
1307
+ return 'Webhook URL is required';
1308
+ try {
1309
+ new URL(input);
1310
+ return true;
1311
+ }
1312
+ catch {
1313
+ return 'Enter a valid URL (e.g., https://webhook.heylua.ai/myagent/handler)';
1314
+ }
1315
+ }
1316
+ }
1317
+ ]);
1318
+ if (!webhookAnswer)
1319
+ return;
1320
+ hookUrl = webhookAnswer.hookUrl.trim();
1321
+ }
1322
+ // Step 6: Get interval for virtual webhooks
1323
+ let interval;
1324
+ const intervalOptions = [
1325
+ { name: '1 hour', value: 60 },
1326
+ { name: '2 hours', value: 120 },
1327
+ { name: '4 hours', value: 240 },
1328
+ { name: '8 hours', value: 480 },
1329
+ { name: '12 hours', value: 720 },
1330
+ { name: '24 hours (1 day)', value: 1440 },
1331
+ { name: '48 hours (2 days)', value: 2880 },
1332
+ ];
1333
+ if (selectedEvent.webhookType === 'virtual') {
1334
+ if (options.interval !== undefined) {
1335
+ interval = options.interval;
1336
+ }
1337
+ else {
1338
+ const intervalAnswer = await safePrompt([
1339
+ {
1340
+ type: 'list',
1341
+ name: 'interval',
1342
+ message: 'Select polling interval:',
1343
+ choices: intervalOptions,
1344
+ default: 60,
1345
+ }
1346
+ ]);
1347
+ if (!intervalAnswer)
1348
+ return;
1349
+ interval = intervalAnswer.interval;
1350
+ }
1351
+ }
1352
+ // Helper to format interval display
1353
+ const formatInterval = (minutes) => {
1354
+ const option = intervalOptions.find(o => o.value === minutes);
1355
+ return option ? option.name : `${minutes} minutes`;
1356
+ };
1357
+ // Step 7: Confirmation (interactive only)
1358
+ if (!options.connectionId) {
1359
+ console.log("\n" + "─".repeat(60));
1360
+ console.log("šŸ“‹ Webhook Subscription Summary");
1361
+ console.log("─".repeat(60));
1362
+ console.log(` Connection: ${selectedConnection.integrationName || selectedConnection.integrationType}`);
1363
+ console.log(` Event: ${selectedEvent.objectType}.${selectedEvent.event} [${selectedEvent.webhookType}]`);
1364
+ console.log(` Webhook URL: ${hookUrl}`);
1365
+ if (interval)
1366
+ console.log(` Interval: ${formatInterval(interval)}`);
1367
+ console.log("─".repeat(60) + "\n");
1368
+ const confirmAnswer = await safePrompt([
1369
+ {
1370
+ type: 'confirm',
1371
+ name: 'confirm',
1372
+ message: 'Create this webhook subscription?',
1373
+ default: true
1374
+ }
1375
+ ]);
1376
+ if (!confirmAnswer?.confirm) {
1377
+ console.log("\nāŒ Cancelled.\n");
1378
+ return;
1379
+ }
1380
+ }
1381
+ // Step 8: Create the webhook
1382
+ writeProgress("šŸ”„ Creating webhook subscription...");
1383
+ try {
1384
+ const createResult = await context.unifiedToApi.createWebhookSubscription(context.agentId, {
1385
+ connectionId: selectedConnection.id,
1386
+ objectType: selectedEvent.objectType,
1387
+ event: selectedEvent.event,
1388
+ hookUrl,
1389
+ interval,
1390
+ });
1391
+ if (!createResult.success || !createResult.data) {
1392
+ writeError(`āŒ Failed to create webhook: ${createResult.error?.message}`);
1393
+ return;
1394
+ }
1395
+ const webhook = createResult.data;
1396
+ console.log("\n" + "─".repeat(60));
1397
+ writeSuccess("āœ… Webhook subscription created!");
1398
+ console.log("─".repeat(60));
1399
+ console.log(` ID: ${webhook.id}`);
1400
+ console.log(` Integration: ${webhook.integrationType}`);
1401
+ console.log(` Event: ${webhook.objectType}.${webhook.event} [${webhook.webhookType}]`);
1402
+ console.log(` Webhook URL: ${webhook.hookUrl}`);
1403
+ if (webhook.interval)
1404
+ console.log(` Interval: ${formatInterval(webhook.interval)}`);
1405
+ console.log(` Status: ${webhook.status}`);
1406
+ console.log("─".repeat(60) + "\n");
1407
+ console.log("šŸ’” Ensure your webhook endpoint is deployed and ready to receive events.\n");
1408
+ }
1409
+ catch (error) {
1410
+ writeError(`āŒ Error: ${error.message}`);
1411
+ }
1412
+ }
1413
+ /**
1414
+ * Delete a webhook subscription (interactive)
1415
+ */
1416
+ async function webhooksDeleteInteractive(context) {
1417
+ writeProgress("šŸ”„ Loading webhook subscriptions...");
1418
+ try {
1419
+ const result = await context.unifiedToApi.getWebhookSubscriptions(context.agentId);
1420
+ if (!result.success) {
1421
+ writeError(`āŒ Failed to load webhooks: ${result.error?.message}`);
1422
+ return;
1423
+ }
1424
+ const webhooks = result.data || [];
1425
+ if (webhooks.length === 0) {
1426
+ console.log("\nā„¹ļø No webhook subscriptions to delete.\n");
1427
+ return;
1428
+ }
1429
+ const webhookAnswer = await safePrompt([
1430
+ {
1431
+ type: 'list',
1432
+ name: 'webhook',
1433
+ message: 'Select a webhook subscription to delete:',
1434
+ choices: webhooks.map(wh => {
1435
+ // Show last part of URL for readability
1436
+ const urlParts = (wh.hookUrl || '').split('/');
1437
+ const shortUrl = urlParts.length > 3 ? '.../' + urlParts.slice(-2).join('/') : wh.hookUrl || '';
1438
+ return {
1439
+ name: `${wh.id.substring(0, 8)}... - ${wh.integrationType} ${wh.objectType}.${wh.event} → ${shortUrl}`,
1440
+ value: wh.id
1441
+ };
1442
+ })
1443
+ }
1444
+ ]);
1445
+ if (!webhookAnswer?.webhook)
1446
+ return;
1447
+ const selectedWebhook = webhooks.find(wh => wh.id === webhookAnswer.webhook);
1448
+ const confirmAnswer = await safePrompt([
1449
+ {
1450
+ type: 'confirm',
1451
+ name: 'confirm',
1452
+ message: `Delete webhook subscription for ${selectedWebhook?.objectType}.${selectedWebhook?.event}?`,
1453
+ default: false
1454
+ }
1455
+ ]);
1456
+ if (!confirmAnswer?.confirm) {
1457
+ console.log("\nāŒ Cancelled.\n");
1458
+ return;
1459
+ }
1460
+ await webhooksDeleteFlow(context, webhookAnswer.webhook);
1461
+ }
1462
+ catch (error) {
1463
+ writeError(`āŒ Error: ${error.message}`);
1464
+ }
1465
+ }
1466
+ /**
1467
+ * Delete a webhook subscription (non-interactive)
1468
+ */
1469
+ async function webhooksDeleteFlow(context, webhookId) {
1470
+ writeProgress("šŸ”„ Deleting webhook subscription...");
1471
+ try {
1472
+ const result = await context.unifiedToApi.deleteWebhookSubscription(context.agentId, webhookId);
1473
+ if (result.success) {
1474
+ writeSuccess(`āœ… Webhook subscription deleted: ${webhookId}\n`);
1475
+ }
1476
+ else {
1477
+ writeError(`āŒ Failed to delete webhook: ${result.error?.message}`);
1478
+ }
1479
+ }
1480
+ catch (error) {
1481
+ writeError(`āŒ Error: ${error.message}`);
1482
+ }
1483
+ }
1484
+ // ─────────────────────────────────────────────────────────────────────────────
1485
+ // MCP Server Management
1486
+ // ─────────────────────────────────────────────────────────────────────────────
1487
+ /**
1488
+ * Handle mcp subcommand (non-interactive entry point)
1489
+ */
1490
+ async function mcpSubcommand(context, cmdOptions) {
1491
+ const subAction = cmdOptions?._?.[0]?.toLowerCase() || '';
1492
+ const options = {
1493
+ connectionId: cmdOptions?.connection || cmdOptions?.connectionId,
1494
+ };
1495
+ await mcpManagementFlow(context, options, subAction);
1496
+ }
1497
+ /**
1498
+ * Main handler for MCP management subcommand
1499
+ */
1500
+ async function mcpManagementFlow(context, options, subAction) {
1501
+ // Non-interactive mode
1502
+ if (subAction === 'list') {
1503
+ await mcpListFlow(context);
1504
+ return;
1505
+ }
1506
+ if (subAction === 'activate') {
1507
+ if (!options.connectionId) {
1508
+ writeError("āŒ Missing required option: --connection <id>");
1509
+ console.log("\nšŸ’” Use 'lua integrations mcp list' to see available connection IDs.\n");
1510
+ return;
1511
+ }
1512
+ await mcpActivateFlow(context, options.connectionId);
1513
+ return;
1514
+ }
1515
+ if (subAction === 'deactivate') {
1516
+ if (!options.connectionId) {
1517
+ writeError("āŒ Missing required option: --connection <id>");
1518
+ console.log("\nšŸ’” Use 'lua integrations mcp list' to see available connection IDs.\n");
1519
+ return;
1520
+ }
1521
+ await mcpDeactivateFlow(context, options.connectionId);
1522
+ return;
1523
+ }
1524
+ // Interactive mode
1525
+ const actionAnswer = await safePrompt([
1526
+ {
1527
+ type: 'list',
1528
+ name: 'action',
1529
+ message: 'What would you like to do?',
1530
+ choices: [
1531
+ { name: 'šŸ“‹ List connections with MCP status', value: 'list' },
1532
+ { name: 'āœ… Activate MCP server for a connection', value: 'activate' },
1533
+ { name: 'āøļø Deactivate MCP server for a connection', value: 'deactivate' },
1534
+ { name: '← Back', value: 'back' }
1535
+ ]
1536
+ }
1537
+ ]);
1538
+ if (!actionAnswer || actionAnswer.action === 'back')
1539
+ return;
1540
+ switch (actionAnswer.action) {
1541
+ case 'list':
1542
+ await mcpListFlow(context);
1543
+ break;
1544
+ case 'activate':
1545
+ await mcpActivateInteractive(context);
1546
+ break;
1547
+ case 'deactivate':
1548
+ await mcpDeactivateInteractive(context);
1549
+ break;
1550
+ }
1551
+ }
1552
+ /**
1553
+ * List all connections with their MCP server status
1554
+ */
1555
+ async function mcpListFlow(context) {
1556
+ writeProgress("šŸ”„ Loading connections and MCP servers...");
1557
+ try {
1558
+ const [connectionsResult, mcpServers] = await Promise.all([
1559
+ context.unifiedToApi.getConnections(context.agentId),
1560
+ fetchServersCore(context)
1561
+ ]);
1562
+ if (!connectionsResult.success) {
1563
+ writeError(`āŒ Failed to load connections: ${connectionsResult.error?.message}`);
1564
+ return;
1565
+ }
1566
+ if (!mcpServers)
1567
+ return;
1568
+ const connections = connectionsResult.data || [];
1569
+ const unifiedServers = mcpServers.filter(s => s.source === 'unifiedto');
1570
+ // Map connection IDs to MCP servers
1571
+ const serverByConnectionId = new Map();
1572
+ for (const server of unifiedServers) {
1573
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
1574
+ if (connectionMatch) {
1575
+ serverByConnectionId.set(connectionMatch[1], server);
1576
+ }
1577
+ }
1578
+ console.log("\n" + "─".repeat(80));
1579
+ console.log("šŸ”Œ MCP Servers for Connections");
1580
+ console.log("─".repeat(80) + "\n");
1581
+ if (connections.length === 0) {
1582
+ console.log("ā„¹ļø No connections found.\n");
1583
+ console.log("šŸ’” Connect an integration with: lua integrations connect\n");
1584
+ return;
1585
+ }
1586
+ for (const conn of connections) {
1587
+ const mcpServer = serverByConnectionId.get(conn.id);
1588
+ const status = mcpServer?.active ? 'āœ… active' : 'āøļø inactive';
1589
+ const serverName = mcpServer?.name || 'Not found';
1590
+ console.log(` Connection: ${conn.id}`);
1591
+ console.log(` Integration: ${conn.integrationName || conn.integrationType}`);
1592
+ console.log(` MCP Server: ${serverName}`);
1593
+ console.log(` Status: ${status}`);
1594
+ console.log("─".repeat(80));
1595
+ }
1596
+ console.log(`\nTotal: ${connections.length} connection(s)`);
1597
+ console.log("\nšŸ’” Use --connection <id> with 'activate' or 'deactivate' commands.\n");
1598
+ }
1599
+ catch (error) {
1600
+ writeError(`āŒ Error: ${error.message}`);
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Activate MCP server for a connection (non-interactive)
1605
+ */
1606
+ async function mcpActivateFlow(context, connectionId) {
1607
+ writeProgress("šŸ”„ Finding MCP server for connection...");
1608
+ try {
1609
+ const mcpServers = await fetchServersCore(context);
1610
+ if (!mcpServers)
1611
+ return;
1612
+ const mcpServer = mcpServers.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${connectionId}`));
1613
+ if (!mcpServer) {
1614
+ writeError(`āŒ No MCP server found for connection: ${connectionId}`);
1615
+ console.log("\nšŸ’” Make sure the connection exists. Use 'lua integrations list' to check.\n");
1616
+ return;
1617
+ }
1618
+ await activateServerCore(context, mcpServer);
1619
+ }
1620
+ catch (error) {
1621
+ writeError(`āŒ Error: ${error.message}`);
1622
+ }
1623
+ }
1624
+ /**
1625
+ * Deactivate MCP server for a connection (non-interactive)
1626
+ */
1627
+ async function mcpDeactivateFlow(context, connectionId) {
1628
+ writeProgress("šŸ”„ Finding MCP server for connection...");
1629
+ try {
1630
+ const mcpServers = await fetchServersCore(context);
1631
+ if (!mcpServers)
1632
+ return;
1633
+ const mcpServer = mcpServers.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${connectionId}`));
1634
+ if (!mcpServer) {
1635
+ writeError(`āŒ No MCP server found for connection: ${connectionId}`);
1636
+ console.log("\nšŸ’” Make sure the connection exists. Use 'lua integrations list' to check.\n");
1637
+ return;
1638
+ }
1639
+ await deactivateServerCore(context, mcpServer);
1640
+ }
1641
+ catch (error) {
1642
+ writeError(`āŒ Error: ${error.message}`);
1643
+ }
1644
+ }
1645
+ /**
1646
+ * Interactive flow for activating MCP server
1647
+ */
1648
+ async function mcpActivateInteractive(context) {
1649
+ writeProgress("šŸ”„ Loading connections...");
1650
+ try {
1651
+ const [connectionsResult, mcpServers] = await Promise.all([
1652
+ context.unifiedToApi.getConnections(context.agentId),
1653
+ fetchServersCore(context)
1654
+ ]);
1655
+ if (!connectionsResult.success) {
1656
+ writeError(`āŒ Failed to load connections: ${connectionsResult.error?.message}`);
1657
+ return;
1658
+ }
1659
+ if (!mcpServers)
1660
+ return;
1661
+ const connections = connectionsResult.data || [];
1662
+ // Map connection IDs to MCP servers
1663
+ const serverByConnectionId = new Map();
1664
+ for (const server of mcpServers) {
1665
+ if (server.source === 'unifiedto') {
1666
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
1667
+ if (connectionMatch) {
1668
+ serverByConnectionId.set(connectionMatch[1], server);
1669
+ }
1670
+ }
1671
+ }
1672
+ // Filter to show only inactive MCP servers
1673
+ const inactiveConnections = connections.filter(conn => {
1674
+ const server = serverByConnectionId.get(conn.id);
1675
+ return server && !server.active;
1676
+ });
1677
+ if (inactiveConnections.length === 0) {
1678
+ writeInfo("ā„¹ļø All MCP servers are already active (or no connections found).\n");
1679
+ return;
1680
+ }
1681
+ const connectionAnswer = await safePrompt([
1682
+ {
1683
+ type: 'list',
1684
+ name: 'connectionId',
1685
+ message: 'Select a connection to activate MCP:',
1686
+ choices: inactiveConnections.map(conn => ({
1687
+ name: `${conn.integrationName || conn.integrationType} (${conn.id.substring(0, 12)}...)`,
1688
+ value: conn.id
1689
+ }))
1690
+ }
1691
+ ]);
1692
+ if (!connectionAnswer)
1693
+ return;
1694
+ await mcpActivateFlow(context, connectionAnswer.connectionId);
1695
+ }
1696
+ catch (error) {
1697
+ writeError(`āŒ Error: ${error.message}`);
1698
+ }
1699
+ }
1700
+ /**
1701
+ * Interactive flow for deactivating MCP server
1702
+ */
1703
+ async function mcpDeactivateInteractive(context) {
1704
+ writeProgress("šŸ”„ Loading connections...");
1705
+ try {
1706
+ const [connectionsResult, mcpServers] = await Promise.all([
1707
+ context.unifiedToApi.getConnections(context.agentId),
1708
+ fetchServersCore(context)
1709
+ ]);
1710
+ if (!connectionsResult.success) {
1711
+ writeError(`āŒ Failed to load connections: ${connectionsResult.error?.message}`);
1712
+ return;
1713
+ }
1714
+ if (!mcpServers)
1715
+ return;
1716
+ const connections = connectionsResult.data || [];
1717
+ // Map connection IDs to MCP servers
1718
+ const serverByConnectionId = new Map();
1719
+ for (const server of mcpServers) {
1720
+ if (server.source === 'unifiedto') {
1721
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
1722
+ if (connectionMatch) {
1723
+ serverByConnectionId.set(connectionMatch[1], server);
1724
+ }
1725
+ }
1726
+ }
1727
+ // Filter to show only active MCP servers
1728
+ const activeConnections = connections.filter(conn => {
1729
+ const server = serverByConnectionId.get(conn.id);
1730
+ return server && server.active;
1731
+ });
1732
+ if (activeConnections.length === 0) {
1733
+ writeInfo("ā„¹ļø No active MCP servers found to deactivate.\n");
1734
+ return;
1735
+ }
1736
+ const connectionAnswer = await safePrompt([
1737
+ {
1738
+ type: 'list',
1739
+ name: 'connectionId',
1740
+ message: 'Select a connection to deactivate MCP:',
1741
+ choices: activeConnections.map(conn => ({
1742
+ name: `${conn.integrationName || conn.integrationType} (${conn.id.substring(0, 12)}...)`,
1743
+ value: conn.id
1744
+ }))
1745
+ }
1746
+ ]);
1747
+ if (!connectionAnswer)
1748
+ return;
1749
+ await mcpDeactivateFlow(context, connectionAnswer.connectionId);
1750
+ }
1751
+ catch (error) {
1752
+ writeError(`āŒ Error: ${error.message}`);
1753
+ }
1754
+ }
1755
+ // ─────────────────────────────────────────────────────────────────────────────
1756
+ // Help
1757
+ // ─────────────────────────────────────────────────────────────────────────────
1758
+ function showUsage() {
1759
+ console.log('\nUsage:');
1760
+ console.log(' lua integrations Interactive integration management');
1761
+ console.log(' lua integrations connect Connect a new account (interactive)');
1762
+ console.log(' lua integrations connect --integration <type> Connect a specific integration');
1763
+ console.log(' lua integrations update Update connection scopes (interactive)');
1764
+ console.log(' lua integrations update --integration <type> Update scopes for a specific integration');
1765
+ console.log(' lua integrations list List connected accounts');
1766
+ console.log(' lua integrations available List available integrations');
1767
+ console.log(' lua integrations disconnect --connection-id <id> Disconnect an account');
1768
+ console.log('\nWebhook Subscriptions:');
1769
+ console.log(' lua integrations webhooks list List all webhook subscriptions');
1770
+ console.log(' lua integrations webhooks create Create subscription (interactive)');
1771
+ console.log(' lua integrations webhooks create --connection <id> --object <type> --event <event> --webhook <full-url>');
1772
+ console.log(' lua integrations webhooks delete --webhook-id <id> Delete a subscription');
1773
+ console.log('\nMCP Server Management:');
1774
+ console.log(' lua integrations mcp list List connections with MCP status');
1775
+ console.log(' lua integrations mcp activate --connection <id> Activate MCP server');
1776
+ console.log(' lua integrations mcp deactivate --connection <id> Deactivate MCP server');
1777
+ }
1778
+ //# sourceMappingURL=integrations.js.map