lua-cli 3.5.0-alpha.1 → 3.5.0-alpha.3

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 (56) 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/logs.api.service.d.ts +2 -1
  9. package/dist/api/logs.api.service.js +2 -0
  10. package/dist/api/logs.api.service.js.map +1 -1
  11. package/dist/api/unifiedto.api.service.d.ts +87 -0
  12. package/dist/api/unifiedto.api.service.js +107 -0
  13. package/dist/api/unifiedto.api.service.js.map +1 -0
  14. package/dist/api/webhook.api.service.js +1 -1
  15. package/dist/api/webhook.api.service.js.map +1 -1
  16. package/dist/cli/command-definitions.js +112 -16
  17. package/dist/cli/command-definitions.js.map +1 -1
  18. package/dist/commands/chat.js +51 -23
  19. package/dist/commands/chat.js.map +1 -1
  20. package/dist/commands/compile.d.ts +1 -2
  21. package/dist/commands/compile.js +2 -3
  22. package/dist/commands/compile.js.map +1 -1
  23. package/dist/commands/configure.d.ts +17 -1
  24. package/dist/commands/configure.js +29 -4
  25. package/dist/commands/configure.js.map +1 -1
  26. package/dist/commands/index.d.ts +1 -0
  27. package/dist/commands/index.js +1 -0
  28. package/dist/commands/index.js.map +1 -1
  29. package/dist/commands/integrations.d.ts +17 -0
  30. package/dist/commands/integrations.js +2392 -0
  31. package/dist/commands/integrations.js.map +1 -0
  32. package/dist/commands/logs.js +33 -12
  33. package/dist/commands/logs.js.map +1 -1
  34. package/dist/commands/marketplace.js +3 -2
  35. package/dist/commands/marketplace.js.map +1 -1
  36. package/dist/commands/mcp.d.ts +19 -0
  37. package/dist/commands/mcp.js +3 -3
  38. package/dist/commands/mcp.js.map +1 -1
  39. package/dist/commands/push.js +204 -215
  40. package/dist/commands/push.js.map +1 -1
  41. package/dist/commands/sync.d.ts +5 -9
  42. package/dist/commands/sync.js +146 -102
  43. package/dist/commands/sync.js.map +1 -1
  44. package/dist/commands/test.js +41 -13
  45. package/dist/commands/test.js.map +1 -1
  46. package/dist/interfaces/mcp.d.ts +11 -0
  47. package/dist/interfaces/unifiedto.d.ts +95 -0
  48. package/dist/interfaces/unifiedto.js +6 -0
  49. package/dist/interfaces/unifiedto.js.map +1 -0
  50. package/dist/utils/auth-flows.d.ts +29 -1
  51. package/dist/utils/auth-flows.js +84 -1
  52. package/dist/utils/auth-flows.js.map +1 -1
  53. package/dist/utils/sandbox.d.ts +2 -2
  54. package/dist/utils/sandbox.js +1 -1
  55. package/package.json +1 -1
  56. package/template/package.json +1 -1
@@ -0,0 +1,2392 @@
1
+ /**
2
+ * Integrations Command
3
+ * Manages third-party integrations 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 integration
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
+ // Webhook trigger configuration
36
+ const AGENT_WEBHOOK_URL = `${BASE_URLS.API}/webhook/unifiedto/data`;
37
+ const DEFAULT_VIRTUAL_WEBHOOK_INTERVAL = 60; // minutes
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Integration API Functions (via Lua API)
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+ /**
42
+ * Fetches available (activated) integrations via Lua API
43
+ */
44
+ async function fetchAvailableIntegrations(unifiedToApi) {
45
+ const result = await unifiedToApi.getAvailableIntegrations();
46
+ if (!result.success || !result.data) {
47
+ throw new Error(`Failed to fetch integrations: ${result.error?.message || 'Unknown error'}`);
48
+ }
49
+ return result.data.map((integration) => ({
50
+ name: integration.name,
51
+ value: integration.type,
52
+ categories: integration.categories || [],
53
+ authSupport: integration.authSupport,
54
+ oauthConfigured: integration.oauthConfigured,
55
+ oauthScopes: integration.oauthScopes,
56
+ tokenFields: integration.tokenFields,
57
+ }));
58
+ }
59
+ /**
60
+ * Starts a local HTTP server to receive the OAuth callback
61
+ * Returns a promise that resolves when the callback is received
62
+ */
63
+ function startCallbackServer(timeoutMs = 300000) {
64
+ return new Promise((resolve) => {
65
+ let resolved = false;
66
+ const server = http.createServer((req, res) => {
67
+ if (resolved)
68
+ return;
69
+ const reqUrl = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
70
+ if (reqUrl.pathname === '/callback') {
71
+ const connectionId = reqUrl.searchParams.get('id');
72
+ const error = reqUrl.searchParams.get('error');
73
+ const integrationType = reqUrl.searchParams.get('type');
74
+ resolved = true;
75
+ if (error) {
76
+ // Send error page
77
+ res.writeHead(200, { 'Content-Type': 'text/html' });
78
+ res.end(`
79
+ <!DOCTYPE html>
80
+ <html>
81
+ <head><title>Connection Failed</title></head>
82
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e;">
83
+ <div style="text-align: center; color: white;">
84
+ <h1 style="color: #ff6b6b;">❌ Connection Failed</h1>
85
+ <p style="color: #ccc;">Error: ${error}</p>
86
+ <p style="color: #888;">You can close this window and try again.</p>
87
+ </div>
88
+ </body>
89
+ </html>
90
+ `);
91
+ server.close();
92
+ resolve({ success: false, error });
93
+ }
94
+ else if (connectionId) {
95
+ // Send success page
96
+ res.writeHead(200, { 'Content-Type': 'text/html' });
97
+ res.end(`
98
+ <!DOCTYPE html>
99
+ <html>
100
+ <head><title>Connection Successful</title></head>
101
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e;">
102
+ <div style="text-align: center; color: white;">
103
+ <h1 style="color: #4ade80;">✅ Connection Successful!</h1>
104
+ <p style="color: #ccc;">Your integration has been connected.</p>
105
+ <p style="color: #888;">You can close this window and return to the terminal.</p>
106
+ </div>
107
+ </body>
108
+ </html>
109
+ `);
110
+ server.close();
111
+ resolve({ success: true, connectionId, integrationType: integrationType || undefined });
112
+ }
113
+ else {
114
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
115
+ res.end('Missing connection ID');
116
+ }
117
+ }
118
+ else {
119
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
120
+ res.end('Not found');
121
+ }
122
+ });
123
+ server.listen(CALLBACK_PORT, () => {
124
+ // Server started
125
+ });
126
+ // Timeout after specified time
127
+ setTimeout(() => {
128
+ if (!resolved) {
129
+ resolved = true;
130
+ server.close();
131
+ resolve({ success: false, error: 'Timeout waiting for OAuth callback' });
132
+ }
133
+ }, timeoutMs);
134
+ });
135
+ }
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Main Command Entry
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ export async function integrationsCommand(action, subaction, cmdObj) {
140
+ return withErrorHandling(async () => {
141
+ const config = readSkillConfig();
142
+ validateConfig(config);
143
+ validateAgentConfig(config);
144
+ const agentId = config.agent.agentId;
145
+ const apiKey = await loadApiKey();
146
+ if (!apiKey) {
147
+ console.error("❌ No API key found. Please run 'lua auth configure' to set up your API key.");
148
+ process.exit(1);
149
+ }
150
+ const userData = await checkApiKey(apiKey);
151
+ writeProgress("✅ Authenticated with Lua");
152
+ const userId = userData.admin?.userId;
153
+ if (!userId) {
154
+ console.error("❌ Failed to get user ID from authentication.");
155
+ process.exit(1);
156
+ }
157
+ const developerApi = new DeveloperApi(BASE_URLS.API, apiKey, agentId);
158
+ const unifiedToApi = new UnifiedToApi(BASE_URLS.API, apiKey);
159
+ const context = {
160
+ agentId,
161
+ userId,
162
+ apiKey,
163
+ developerApi,
164
+ unifiedToApi,
165
+ };
166
+ if (action) {
167
+ // Pass subaction via cmdObj for webhooks command
168
+ const enhancedCmdObj = { ...cmdObj, _: subaction ? [subaction] : [] };
169
+ await executeNonInteractive(context, action, enhancedCmdObj);
170
+ }
171
+ else {
172
+ await interactiveIntegrationsManagement(context);
173
+ }
174
+ }, "integrations");
175
+ }
176
+ // ─────────────────────────────────────────────────────────────────────────────
177
+ // Non-Interactive Mode
178
+ // ─────────────────────────────────────────────────────────────────────────────
179
+ async function executeNonInteractive(context, action, cmdOptions) {
180
+ const normalizedAction = action.toLowerCase();
181
+ // Extract typed options
182
+ const options = {
183
+ integration: cmdOptions?.integration,
184
+ connectionId: cmdOptions?.connectionId,
185
+ authMethod: cmdOptions?.authMethod,
186
+ scopes: cmdOptions?.scopes,
187
+ hideSensitive: cmdOptions?.hideSensitive !== 'false', // Default true, only false if explicitly set
188
+ // Trigger options
189
+ triggers: cmdOptions?.triggers,
190
+ customWebhook: cmdOptions?.customWebhook === true,
191
+ hookUrl: cmdOptions?.hookUrl,
192
+ };
193
+ // Check for JSON output flag
194
+ const jsonOutput = cmdOptions?.json === true;
195
+ switch (normalizedAction) {
196
+ case 'connect':
197
+ await connectIntegrationFlow(context, options);
198
+ break;
199
+ case 'update':
200
+ if (!options.integration) {
201
+ console.error("❌ --integration is required for update");
202
+ console.log("\n💡 Run 'lua integrations list' to see connected integrations");
203
+ process.exit(1);
204
+ }
205
+ await updateConnectionFlow(context, options);
206
+ break;
207
+ case 'list':
208
+ await listConnections(context);
209
+ break;
210
+ case 'available':
211
+ await listAvailableIntegrations(context);
212
+ break;
213
+ case 'info':
214
+ // New command: Show detailed info about an integration type
215
+ // Integration type can be passed as positional arg (info <type>) or as --integration flag
216
+ const infoIntegrationType = options.integration || cmdOptions?._?.[0];
217
+ if (!infoIntegrationType) {
218
+ console.error("❌ Integration type is required");
219
+ console.log("\nUsage: lua integrations info <type>");
220
+ console.log(" lua integrations info <type> --json");
221
+ process.exit(1);
222
+ }
223
+ await showIntegrationInfo(context, infoIntegrationType, jsonOutput);
224
+ break;
225
+ case 'disconnect':
226
+ if (!options.connectionId) {
227
+ console.error("❌ --connection-id is required for disconnect");
228
+ console.log("\n💡 Run 'lua integrations list' to see connection IDs");
229
+ process.exit(1);
230
+ }
231
+ await disconnectIntegration(context, options.connectionId);
232
+ break;
233
+ case 'webhooks':
234
+ await webhooksSubcommand(context, cmdOptions);
235
+ break;
236
+ case 'mcp':
237
+ await mcpSubcommand(context, cmdOptions);
238
+ break;
239
+ default:
240
+ console.error(`❌ Invalid action: "${action}"`);
241
+ showUsage();
242
+ process.exit(1);
243
+ }
244
+ }
245
+ // ─────────────────────────────────────────────────────────────────────────────
246
+ // Interactive Mode
247
+ // ─────────────────────────────────────────────────────────────────────────────
248
+ async function interactiveIntegrationsManagement(context) {
249
+ let continueManaging = true;
250
+ while (continueManaging) {
251
+ console.log("\n" + "=".repeat(60));
252
+ console.log("🔗 Integrations");
253
+ console.log("=".repeat(60) + "\n");
254
+ const actionAnswer = await safePrompt([
255
+ {
256
+ type: 'list',
257
+ name: 'action',
258
+ message: 'What would you like to do?',
259
+ choices: [
260
+ { name: '➕ Connect a new integration', value: 'connect' },
261
+ { name: '🔄 Update connection scopes', value: 'update' },
262
+ { name: '📋 List connected integrations', value: 'list' },
263
+ { name: '🔍 View available integrations', value: 'available' },
264
+ { name: '🔔 Manage triggers', value: 'webhooks' },
265
+ { name: '🔌 Manage MCP servers', value: 'mcp' },
266
+ { name: '🗑️ Disconnect an integration', value: 'disconnect' },
267
+ { name: '❌ Exit', value: 'exit' }
268
+ ]
269
+ }
270
+ ]);
271
+ if (!actionAnswer)
272
+ return;
273
+ const { action } = actionAnswer;
274
+ switch (action) {
275
+ case 'connect':
276
+ await connectIntegrationFlow(context);
277
+ break;
278
+ case 'update':
279
+ await updateConnectionFlow(context);
280
+ break;
281
+ case 'list':
282
+ await listConnections(context);
283
+ break;
284
+ case 'available':
285
+ await listAvailableIntegrations(context);
286
+ break;
287
+ case 'webhooks':
288
+ await webhooksInteractiveMenu(context);
289
+ break;
290
+ case 'mcp':
291
+ await mcpManagementFlow(context, {});
292
+ break;
293
+ case 'disconnect':
294
+ await disconnectIntegrationInteractive(context);
295
+ break;
296
+ case 'exit':
297
+ continueManaging = false;
298
+ console.log("\n👋 Goodbye!\n");
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ // ─────────────────────────────────────────────────────────────────────────────
304
+ // List Available Integrations
305
+ // ─────────────────────────────────────────────────────────────────────────────
306
+ async function listAvailableIntegrations(context) {
307
+ writeProgress("🔄 Fetching available integrations...");
308
+ try {
309
+ const integrations = await fetchAvailableIntegrations(context.unifiedToApi);
310
+ console.log("\n" + "=".repeat(60));
311
+ console.log("🔌 Available Integrations");
312
+ console.log("=".repeat(60) + "\n");
313
+ if (integrations.length === 0) {
314
+ console.log("ℹ️ No integrations available.");
315
+ console.log("💡 Contact support to enable integrations for your workspace.\n");
316
+ return;
317
+ }
318
+ // Group by category
319
+ const byCategory = {};
320
+ for (const integration of integrations) {
321
+ const category = integration.categories[0] || 'other';
322
+ if (!byCategory[category])
323
+ byCategory[category] = [];
324
+ byCategory[category].push(integration);
325
+ }
326
+ for (const [category, items] of Object.entries(byCategory)) {
327
+ console.log(`📁 ${category.toUpperCase()}`);
328
+ items.forEach(i => {
329
+ const authBadge = i.authSupport === 'oauth' ? '🔐' : i.authSupport === 'token' ? '🔑' : '🔐🔑';
330
+ console.log(` ${authBadge} ${i.name} (${i.value})`);
331
+ });
332
+ console.log();
333
+ }
334
+ console.log("=".repeat(60));
335
+ console.log(`Total: ${integrations.length} integration(s) available`);
336
+ console.log("🔐 = OAuth 🔑 = API Key/Token 🔐🔑 = Both\n");
337
+ }
338
+ catch (error) {
339
+ writeError(`❌ Failed to fetch integrations: ${error.message}`);
340
+ }
341
+ }
342
+ // ─────────────────────────────────────────────────────────────────────────────
343
+ // Integration Info (Non-Interactive Discovery)
344
+ // ─────────────────────────────────────────────────────────────────────────────
345
+ /**
346
+ * Show detailed information about an integration type
347
+ * Used for non-interactive discovery of OAuth scopes and webhook events
348
+ */
349
+ async function showIntegrationInfo(context, integrationType, jsonOutput = false) {
350
+ try {
351
+ // Fetch integration details
352
+ const integrations = await fetchAvailableIntegrations(context.unifiedToApi);
353
+ const integration = integrations.find(i => i.value === integrationType);
354
+ if (!integration) {
355
+ if (jsonOutput) {
356
+ console.log(JSON.stringify({ error: `Integration not found: ${integrationType}` }));
357
+ }
358
+ else {
359
+ console.error(`❌ Integration not found: ${integrationType}`);
360
+ console.log('\nAvailable integrations:');
361
+ integrations.forEach(i => console.log(` - ${i.value} (${i.name})`));
362
+ }
363
+ process.exit(1);
364
+ }
365
+ // Fetch available webhook events for this integration
366
+ let webhookEvents = [];
367
+ try {
368
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEventsByType(integrationType);
369
+ if (eventsResult.success && eventsResult.data) {
370
+ webhookEvents = eventsResult.data;
371
+ }
372
+ }
373
+ catch {
374
+ // Silently ignore if webhook events can't be fetched
375
+ }
376
+ if (jsonOutput) {
377
+ // Output as JSON for scripting
378
+ const output = {
379
+ type: integration.value,
380
+ name: integration.name,
381
+ categories: integration.categories,
382
+ authSupport: integration.authSupport,
383
+ oauthConfigured: integration.oauthConfigured,
384
+ oauthScopes: integration.oauthScopes || [],
385
+ tokenFields: integration.tokenFields || [],
386
+ webhookEvents: webhookEvents.map(e => ({
387
+ trigger: `${e.objectType}.${e.event}`,
388
+ objectType: e.objectType,
389
+ event: e.event,
390
+ webhookType: e.webhookType,
391
+ friendlyLabel: e.friendlyLabel,
392
+ friendlyDescription: e.friendlyDescription,
393
+ availableFilters: e.availableFilters,
394
+ })),
395
+ webhookDefaults: {
396
+ agentWebhookUrl: AGENT_WEBHOOK_URL,
397
+ defaultVirtualInterval: DEFAULT_VIRTUAL_WEBHOOK_INTERVAL,
398
+ },
399
+ };
400
+ console.log(JSON.stringify(output, null, 2));
401
+ }
402
+ else {
403
+ // Human-readable output
404
+ console.log("\n" + "=".repeat(60));
405
+ console.log(`📋 Integration: ${integration.name}`);
406
+ console.log("=".repeat(60));
407
+ console.log(`\n Type: ${integration.value}`);
408
+ console.log(` Categories: ${integration.categories.join(', ')}`);
409
+ console.log(` Auth Support: ${integration.authSupport}`);
410
+ console.log(` OAuth Configured: ${integration.oauthConfigured ? 'Yes' : 'No'}`);
411
+ // OAuth Scopes
412
+ if (integration.oauthScopes && integration.oauthScopes.length > 0) {
413
+ console.log('\n OAuth Scopes:');
414
+ integration.oauthScopes.forEach(s => {
415
+ if (s.friendlyLabel) {
416
+ console.log(` - ${s.friendlyLabel} (${s.unifiedScope})`);
417
+ if (s.friendlyDescription) {
418
+ console.log(` ${s.friendlyDescription}`);
419
+ }
420
+ }
421
+ else {
422
+ console.log(` - ${s.unifiedScope} → [${s.originalScopes.join(', ')}]`);
423
+ }
424
+ });
425
+ }
426
+ // Token Fields
427
+ if (integration.tokenFields && integration.tokenFields.length > 0) {
428
+ console.log('\n Token Fields:');
429
+ integration.tokenFields.forEach(f => {
430
+ console.log(` - ${f.name}`);
431
+ if (f.instructions) {
432
+ console.log(` 💡 ${f.instructions}`);
433
+ }
434
+ });
435
+ }
436
+ // Webhook Events/Triggers
437
+ if (webhookEvents.length > 0) {
438
+ console.log('\n Available Triggers:');
439
+ webhookEvents.forEach(e => {
440
+ console.log(` - ${e.objectType}.${e.event} [${e.webhookType}] - ${e.friendlyDescription}`);
441
+ });
442
+ }
443
+ else {
444
+ console.log('\n Available Triggers: None');
445
+ }
446
+ console.log("\n" + "=".repeat(60));
447
+ console.log("\n💡 Use with non-interactive commands:");
448
+ console.log(` lua integrations connect --integration ${integrationType} --auth-method oauth --scopes all`);
449
+ if (webhookEvents.length > 0) {
450
+ const exampleTrigger = `${webhookEvents[0].objectType}.${webhookEvents[0].event}`;
451
+ console.log(` lua integrations connect --integration ${integrationType} --triggers ${exampleTrigger}`);
452
+ }
453
+ console.log();
454
+ }
455
+ }
456
+ catch (error) {
457
+ if (jsonOutput) {
458
+ console.log(JSON.stringify({ error: error.message }));
459
+ }
460
+ else {
461
+ writeError(`❌ Failed to get integration info: ${error.message}`);
462
+ }
463
+ process.exit(1);
464
+ }
465
+ }
466
+ // ─────────────────────────────────────────────────────────────────────────────
467
+ // Connect Flow
468
+ // ─────────────────────────────────────────────────────────────────────────────
469
+ async function connectIntegrationFlow(context, options = {}) {
470
+ // Step 1: Fetch available integrations and existing connections
471
+ writeProgress("🔄 Fetching integrations...");
472
+ let integrations;
473
+ let existingConnections = [];
474
+ try {
475
+ const [integrationsResult, connectionsResult] = await Promise.all([
476
+ fetchAvailableIntegrations(context.unifiedToApi),
477
+ context.unifiedToApi.getConnections(context.agentId)
478
+ ]);
479
+ integrations = integrationsResult;
480
+ existingConnections = connectionsResult.success ? connectionsResult.data || [] : [];
481
+ }
482
+ catch (error) {
483
+ writeError(`❌ Failed to fetch integrations: ${error.message}`);
484
+ return;
485
+ }
486
+ if (integrations.length === 0) {
487
+ writeError("❌ No integrations available.");
488
+ console.log("💡 Contact support to enable integrations for your workspace.\n");
489
+ return;
490
+ }
491
+ // Filter out integrations that already have a connection (1 connection per integration type)
492
+ const connectedTypes = new Set(existingConnections.map(c => c.integrationType));
493
+ const availableIntegrations = integrations.filter(i => !connectedTypes.has(i.value));
494
+ if (availableIntegrations.length === 0) {
495
+ writeInfo("All available integrations are already connected.");
496
+ console.log("💡 Use 'lua integrations update' to change scopes on an existing connection.\n");
497
+ return;
498
+ }
499
+ writeSuccess(`Found ${availableIntegrations.length} integration(s) available to connect`);
500
+ // Step 2: Select integration (with search/filter support)
501
+ let selectedIntegration;
502
+ if (options.integration) {
503
+ // Check if already connected
504
+ if (connectedTypes.has(options.integration)) {
505
+ console.error(`❌ Integration "${options.integration}" is already connected.`);
506
+ console.log("💡 Use 'lua integrations update --integration " + options.integration + "' to change scopes.\n");
507
+ process.exit(1);
508
+ }
509
+ selectedIntegration = availableIntegrations.find(i => i.value === options.integration);
510
+ if (!selectedIntegration) {
511
+ console.error(`❌ Integration "${options.integration}" not found or not available.`);
512
+ console.log('\nAvailable integrations to connect:');
513
+ availableIntegrations.forEach(i => console.log(` - ${i.value} (${i.name})`));
514
+ process.exit(1);
515
+ }
516
+ }
517
+ else {
518
+ // Ask if user wants to search or browse all
519
+ const searchAnswer = await safePrompt([
520
+ {
521
+ type: 'input',
522
+ name: 'searchTerm',
523
+ message: 'Search integrations (or press Enter to browse all):',
524
+ }
525
+ ]);
526
+ if (!searchAnswer)
527
+ return;
528
+ // Filter integrations based on search term
529
+ let filteredIntegrations = availableIntegrations;
530
+ if (searchAnswer.searchTerm.trim()) {
531
+ const searchLower = searchAnswer.searchTerm.toLowerCase().trim();
532
+ filteredIntegrations = availableIntegrations.filter(i => i.name.toLowerCase().includes(searchLower) ||
533
+ i.value.toLowerCase().includes(searchLower) ||
534
+ i.categories.some(c => c.toLowerCase().includes(searchLower)));
535
+ if (filteredIntegrations.length === 0) {
536
+ // Check if the search matches an already-connected integration
537
+ const matchingConnected = existingConnections.filter(c => c.integrationType.toLowerCase().includes(searchLower) ||
538
+ (c.integrationName && c.integrationName.toLowerCase().includes(searchLower)));
539
+ if (matchingConnected.length > 0) {
540
+ const connectedName = matchingConnected[0].integrationName || matchingConnected[0].integrationType;
541
+ writeInfo(`"${connectedName}" is already connected.`);
542
+ console.log(`💡 Use 'lua integrations update' to change scopes.\n`);
543
+ return;
544
+ }
545
+ writeInfo(`No integrations found matching "${searchAnswer.searchTerm}"`);
546
+ // Offer to browse all
547
+ const browseAnswer = await safePrompt([
548
+ {
549
+ type: 'confirm',
550
+ name: 'browseAll',
551
+ message: 'Would you like to browse all available integrations?',
552
+ default: true
553
+ }
554
+ ]);
555
+ if (!browseAnswer?.browseAll)
556
+ return;
557
+ filteredIntegrations = availableIntegrations;
558
+ }
559
+ else {
560
+ writeInfo(`Found ${filteredIntegrations.length} integration(s) matching "${searchAnswer.searchTerm}"`);
561
+ }
562
+ }
563
+ const integrationAnswer = await safePrompt([
564
+ {
565
+ type: 'list',
566
+ name: 'integration',
567
+ message: 'Select an integration to connect:',
568
+ pageSize: 15,
569
+ choices: filteredIntegrations.map(i => {
570
+ const authBadge = i.authSupport === 'oauth' ? '🔐' : i.authSupport === 'token' ? '🔑' : '🔐🔑';
571
+ return {
572
+ name: `${authBadge} ${i.name} (${i.categories.join(', ')})`,
573
+ value: i
574
+ };
575
+ })
576
+ }
577
+ ]);
578
+ if (!integrationAnswer)
579
+ return;
580
+ selectedIntegration = integrationAnswer.integration;
581
+ }
582
+ console.log(`\n📌 Selected: ${selectedIntegration.name}`);
583
+ console.log(` Categories: ${selectedIntegration.categories.join(', ')}`);
584
+ console.log(` Auth Support: ${selectedIntegration.authSupport}`);
585
+ console.log(` OAuth Configured: ${selectedIntegration.oauthConfigured ? 'Yes' : 'No'}`);
586
+ // Step 3: Determine auth method and get scopes
587
+ const canUseOAuth = selectedIntegration.oauthConfigured &&
588
+ ['oauth', 'both'].includes(selectedIntegration.authSupport);
589
+ const canUseToken = ['token', 'both'].includes(selectedIntegration.authSupport);
590
+ let authMethod;
591
+ let selectedScopes = [];
592
+ // Validate --auth-method if provided
593
+ if (options.authMethod) {
594
+ if (!['oauth', 'token'].includes(options.authMethod)) {
595
+ console.error(`❌ Invalid --auth-method: "${options.authMethod}". Use 'oauth' or 'token'`);
596
+ process.exit(1);
597
+ }
598
+ if (options.authMethod === 'oauth' && !canUseOAuth) {
599
+ console.error(`❌ OAuth is not available for ${selectedIntegration.name}.`);
600
+ if (canUseToken) {
601
+ console.log(`💡 Use --auth-method token instead.`);
602
+ }
603
+ process.exit(1);
604
+ }
605
+ if (options.authMethod === 'token' && !canUseToken) {
606
+ console.error(`❌ Token authentication is not available for ${selectedIntegration.name}.`);
607
+ if (canUseOAuth) {
608
+ console.log(`💡 Use --auth-method oauth instead.`);
609
+ }
610
+ process.exit(1);
611
+ }
612
+ authMethod = options.authMethod;
613
+ writeInfo(`Using ${authMethod === 'oauth' ? 'OAuth 2.0' : 'API Token'} authentication`);
614
+ }
615
+ else if (canUseOAuth && canUseToken) {
616
+ // Both available - let user choose interactively
617
+ const authAnswer = await safePrompt([{
618
+ type: 'list',
619
+ name: 'method',
620
+ message: 'Choose authentication method:',
621
+ choices: [
622
+ { name: '🔐 OAuth 2.0 (recommended)', value: 'oauth' },
623
+ { name: '🔑 API Token / Personal Access Token', value: 'token' }
624
+ ]
625
+ }]);
626
+ if (!authAnswer)
627
+ return;
628
+ authMethod = authAnswer.method;
629
+ }
630
+ else if (canUseOAuth) {
631
+ authMethod = 'oauth';
632
+ writeInfo('Using OAuth 2.0 authentication');
633
+ }
634
+ else if (canUseToken) {
635
+ authMethod = 'token';
636
+ if (selectedIntegration.authSupport === 'both' && !selectedIntegration.oauthConfigured) {
637
+ writeInfo('OAuth is not configured - using API Token authentication');
638
+ }
639
+ }
640
+ else {
641
+ writeError('This integration requires OAuth but it is not configured. Please configure OAuth credentials in the Unified.to dashboard.');
642
+ return;
643
+ }
644
+ // Handle OAuth scope selection
645
+ if (authMethod === 'oauth' && selectedIntegration.oauthScopes?.length) {
646
+ const availableScopes = selectedIntegration.oauthScopes.map(s => s.unifiedScope);
647
+ if (options.scopes) {
648
+ // Non-interactive: use provided scopes
649
+ if (options.scopes.toLowerCase() === 'all') {
650
+ selectedScopes = availableScopes;
651
+ writeInfo(`Using all ${selectedScopes.length} available scope(s)`);
652
+ }
653
+ else {
654
+ // Parse comma-separated scopes
655
+ const requestedScopes = options.scopes.split(',').map(s => s.trim());
656
+ const invalidScopes = requestedScopes.filter(s => !availableScopes.includes(s));
657
+ if (invalidScopes.length > 0) {
658
+ console.error(`❌ Invalid scopes: ${invalidScopes.join(', ')}`);
659
+ console.log(`\nAvailable scopes for ${selectedIntegration.name}:`);
660
+ availableScopes.forEach(s => console.log(` - ${s}`));
661
+ process.exit(1);
662
+ }
663
+ selectedScopes = requestedScopes;
664
+ writeInfo(`Using ${selectedScopes.length} specified scope(s)`);
665
+ }
666
+ }
667
+ else {
668
+ // Interactive: prompt for scope selection
669
+ const scopeAnswer = await safePrompt([{
670
+ type: 'checkbox',
671
+ name: 'scopes',
672
+ message: 'Select OAuth scopes (Space to toggle, Enter to confirm, leave empty for all):',
673
+ pageSize: 15,
674
+ loop: false,
675
+ choices: selectedIntegration.oauthScopes.map(s => {
676
+ // Use friendly label if available, otherwise fall back to technical name
677
+ const displayName = s.friendlyLabel || s.unifiedScope;
678
+ const description = s.friendlyDescription
679
+ ? ` (${s.friendlyDescription})`
680
+ : ` → [${s.originalScopes.join(', ')}]`;
681
+ return {
682
+ name: `${displayName}${description}`,
683
+ value: s.unifiedScope,
684
+ checked: false
685
+ };
686
+ })
687
+ }]);
688
+ if (!scopeAnswer)
689
+ return;
690
+ // If none selected, request all scopes
691
+ selectedScopes = scopeAnswer.scopes.length > 0
692
+ ? scopeAnswer.scopes
693
+ : availableScopes;
694
+ console.log(`\n✓ Will request ${selectedScopes.length} scope(s)`);
695
+ }
696
+ }
697
+ else if (authMethod === 'oauth') {
698
+ // OAuth without configurable scopes - Unified.to handles defaults internally
699
+ if (options.scopes) {
700
+ writeInfo('Note: This integration does not have configurable scopes');
701
+ }
702
+ // selectedScopes stays empty - MCP URL will omit permissions param
703
+ }
704
+ // Handle token field display
705
+ if (authMethod === 'token' && selectedIntegration.tokenFields?.length) {
706
+ console.log('\n🔑 You will need to provide the following credentials:\n');
707
+ selectedIntegration.tokenFields.forEach((field, i) => {
708
+ console.log(` ${i + 1}. ${field.name}`);
709
+ if (field.instructions) {
710
+ console.log(` 💡 ${field.instructions}`);
711
+ }
712
+ });
713
+ console.log('\nYou will enter these on the Unified.to authorization page.\n');
714
+ }
715
+ // Determine hide_sensitive setting
716
+ let hideSensitive = true; // Default to true (hide sensitive data)
717
+ if (options.hideSensitive !== undefined) {
718
+ hideSensitive = options.hideSensitive;
719
+ }
720
+ else {
721
+ // Interactive: ask user
722
+ const sensitiveAnswer = await safePrompt([{
723
+ type: 'confirm',
724
+ name: 'hide',
725
+ message: 'Hide sensitive data from MCP tools? (recommended for security)',
726
+ default: true
727
+ }]);
728
+ if (sensitiveAnswer) {
729
+ hideSensitive = sensitiveAnswer.hide;
730
+ }
731
+ }
732
+ // Step 4: Select webhook triggers (agent wake-up events)
733
+ let selectedTriggers = [];
734
+ let webhookUrl = AGENT_WEBHOOK_URL; // Default: agent trigger mode
735
+ let isCustomWebhook = false;
736
+ try {
737
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEventsByType(selectedIntegration.value);
738
+ if (eventsResult.success && eventsResult.data && eventsResult.data.length > 0) {
739
+ const availableEvents = eventsResult.data;
740
+ // Determine webhook mode
741
+ if (options.customWebhook || options.hookUrl) {
742
+ isCustomWebhook = true;
743
+ if (options.hookUrl) {
744
+ webhookUrl = options.hookUrl;
745
+ }
746
+ }
747
+ if (options.triggers) {
748
+ // Non-interactive: parse comma-separated triggers (e.g., 'task_task.created,task_task.updated')
749
+ const requestedTriggers = options.triggers.split(',').map(t => t.trim());
750
+ for (const trigger of requestedTriggers) {
751
+ const [objectType, event] = trigger.split('.');
752
+ const matchingEvent = availableEvents.find(e => e.objectType === objectType && e.event === event);
753
+ if (!matchingEvent) {
754
+ console.error(`❌ Invalid trigger: ${trigger}`);
755
+ console.log(`\nAvailable triggers for ${selectedIntegration.name}:`);
756
+ availableEvents.forEach(e => {
757
+ console.log(` - ${e.objectType}.${e.event} [${e.webhookType}] - ${e.friendlyDescription}`);
758
+ });
759
+ process.exit(1);
760
+ }
761
+ selectedTriggers.push(matchingEvent);
762
+ }
763
+ writeInfo(`Selected ${selectedTriggers.length} trigger(s)`);
764
+ // If custom webhook mode, prompt for URL if not provided
765
+ if (isCustomWebhook && !options.hookUrl) {
766
+ const urlAnswer = await safePrompt([{
767
+ type: 'input',
768
+ name: 'url',
769
+ message: 'Enter your webhook URL:',
770
+ validate: (input) => {
771
+ try {
772
+ new URL(input);
773
+ return true;
774
+ }
775
+ catch {
776
+ return 'Please enter a valid URL';
777
+ }
778
+ }
779
+ }]);
780
+ if (!urlAnswer)
781
+ return;
782
+ webhookUrl = urlAnswer.url;
783
+ }
784
+ }
785
+ else {
786
+ // Interactive: show trigger selection
787
+ console.log('\n' + '─'.repeat(60));
788
+ console.log('📡 Webhook Triggers');
789
+ console.log('─'.repeat(60));
790
+ console.log('\nSelect events that will wake up your agent:');
791
+ console.log('(Space to toggle, Enter to confirm. All selected by default.)');
792
+ const triggerAnswer = await safePrompt([{
793
+ type: 'checkbox',
794
+ name: 'triggers',
795
+ message: 'Select triggers:',
796
+ pageSize: 15,
797
+ loop: false,
798
+ choices: availableEvents.map(e => ({
799
+ name: `${e.friendlyDescription} [${e.webhookType}]`,
800
+ value: e,
801
+ checked: true // Pre-select all by default
802
+ }))
803
+ }]);
804
+ if (triggerAnswer && triggerAnswer.triggers.length > 0) {
805
+ selectedTriggers = triggerAnswer.triggers;
806
+ // Ask about webhook mode only if triggers are selected
807
+ const modeAnswer = await safePrompt([{
808
+ type: 'list',
809
+ name: 'mode',
810
+ message: 'How should these events be handled?',
811
+ choices: [
812
+ { name: '🤖 Agent Trigger (recommended) - Events wake up your agent', value: 'agent' },
813
+ { name: '🔗 Custom URL - Send events to your own webhook URL', value: 'custom' }
814
+ ]
815
+ }]);
816
+ if (!modeAnswer)
817
+ return;
818
+ if (modeAnswer.mode === 'custom') {
819
+ isCustomWebhook = true;
820
+ const urlAnswer = await safePrompt([{
821
+ type: 'input',
822
+ name: 'url',
823
+ message: 'Enter your webhook URL:',
824
+ validate: (input) => {
825
+ try {
826
+ new URL(input);
827
+ return true;
828
+ }
829
+ catch {
830
+ return 'Please enter a valid URL';
831
+ }
832
+ }
833
+ }]);
834
+ if (!urlAnswer)
835
+ return;
836
+ webhookUrl = urlAnswer.url;
837
+ }
838
+ console.log(`\n✓ ${selectedTriggers.length} trigger(s) selected`);
839
+ if (isCustomWebhook) {
840
+ console.log(` → Events will be sent to: ${webhookUrl}`);
841
+ }
842
+ else {
843
+ console.log(` → Events will wake up your agent`);
844
+ }
845
+ }
846
+ else {
847
+ console.log(`\n⏭️ No triggers selected - your agent won't be event-driven`);
848
+ console.log(` You can add triggers later with: lua integrations webhooks create`);
849
+ }
850
+ }
851
+ }
852
+ }
853
+ catch (error) {
854
+ // Non-fatal: continue without triggers if we can't fetch events
855
+ writeInfo(`Note: Could not fetch available triggers (${error.message})`);
856
+ }
857
+ // Step 5: Get authorization URL from Lua API
858
+ writeProgress("🔄 Preparing authorization...");
859
+ const state = Buffer.from(JSON.stringify({
860
+ agentId: context.agentId,
861
+ integration: selectedIntegration.value,
862
+ authMethod,
863
+ timestamp: Date.now()
864
+ })).toString('base64');
865
+ // Encode both agentId and userId in externalXref for the connection
866
+ const externalXref = JSON.stringify({
867
+ agentId: context.agentId,
868
+ userId: context.userId,
869
+ });
870
+ const authUrlResult = await context.unifiedToApi.getAuthUrl(selectedIntegration.value, {
871
+ successRedirect: CALLBACK_URL,
872
+ failureRedirect: `${CALLBACK_URL}?error=auth_failed`,
873
+ scopes: authMethod === 'oauth' ? selectedScopes : undefined,
874
+ state,
875
+ externalXref, // Contains both agentId and userId as JSON
876
+ });
877
+ if (!authUrlResult.success || !authUrlResult.data) {
878
+ writeError(`❌ Failed to get authorization URL: ${authUrlResult.error?.message || 'Unknown error'}`);
879
+ return;
880
+ }
881
+ const authUrl = authUrlResult.data.authUrl;
882
+ // Step 4: Pre-fetch the auth URL to handle redirects
883
+ let finalAuthUrl = authUrl;
884
+ try {
885
+ const response = await fetch(authUrl);
886
+ const responseText = await response.text();
887
+ if (responseText.includes('test.html?redirect=')) {
888
+ const urlMatch = responseText.match(/redirect=([^&\s]+)/);
889
+ if (urlMatch) {
890
+ finalAuthUrl = decodeURIComponent(urlMatch[1]);
891
+ }
892
+ }
893
+ else if (responseText.startsWith('https://')) {
894
+ finalAuthUrl = responseText.trim();
895
+ }
896
+ }
897
+ catch (error) {
898
+ // Use original auth URL if pre-fetch fails
899
+ }
900
+ // Step 5: Start callback server and open browser
901
+ console.log("\n" + "─".repeat(60));
902
+ console.log("🌐 Starting OAuth authorization flow...");
903
+ console.log("─".repeat(60));
904
+ console.log(`\nIntegration: ${selectedIntegration.name}`);
905
+ console.log(`\n📋 Authorization URL (copy if browser doesn't open):\n ${finalAuthUrl}\n`);
906
+ // Start the callback server
907
+ const callbackPromise = startCallbackServer(300000); // 5 minute timeout
908
+ // Auto-open browser
909
+ try {
910
+ await open(finalAuthUrl);
911
+ writeInfo("🌐 Browser opened - please complete the authorization");
912
+ }
913
+ catch (error) {
914
+ writeInfo("💡 Could not open browser automatically. Please open the URL above manually.");
915
+ }
916
+ console.log("\n⏳ Waiting for authorization (5 minute timeout)...");
917
+ console.log("💡 Complete the authorization in your browser.\n");
918
+ // Wait for callback
919
+ const result = await callbackPromise;
920
+ if (result.success && result.connectionId) {
921
+ writeSuccess("\n✅ Authorization successful!");
922
+ // Finalize the connection (sets up MCP server internally and creates webhooks)
923
+ await finalizeConnection(context, selectedIntegration, result.connectionId, selectedScopes, hideSensitive, {
924
+ triggers: selectedTriggers,
925
+ webhookUrl,
926
+ isCustomWebhook,
927
+ });
928
+ }
929
+ else {
930
+ writeError(`\n❌ Authorization failed: ${result.error || 'Unknown error'}`);
931
+ console.log("💡 Please try again with 'lua integrations connect'\n");
932
+ }
933
+ }
934
+ /**
935
+ * Finalizes a new connection by setting up the MCP server and creating webhooks
936
+ * The MCP server enables the agent to use tools from this connection
937
+ * Webhooks enable the agent to be triggered by events from the connection
938
+ */
939
+ async function finalizeConnection(context, integration, connectionId, scopes, hideSensitive = true, webhookConfig = { triggers: [], webhookUrl: '', isCustomWebhook: false }) {
940
+ writeProgress(`🔄 Setting up ${integration.name} connection...`);
941
+ // Build MCP URL
942
+ let mcpUrl = `${UNIFIED_MCP_BASE_URL}?connection=${connectionId}`;
943
+ if (hideSensitive) {
944
+ mcpUrl += '&hide_sensitive=true';
945
+ }
946
+ if (scopes.length > 0) {
947
+ mcpUrl += `&permissions=${scopes.join(',')}`;
948
+ }
949
+ // Always defer tools to reduce initial tool loading overhead with LLMs
950
+ mcpUrl += '&defer_tools=true';
951
+ // Use simple integration type as name (e.g., 'discord', 'linear')
952
+ // This makes tool names cleaner: discord_create_message vs unified-discord-abc123_create_message
953
+ const serverName = integration.value;
954
+ const mcpServerData = {
955
+ name: serverName,
956
+ transport: 'streamable-http',
957
+ url: mcpUrl,
958
+ source: 'unifiedto', // Flag for runtime auth injection
959
+ timeout: 30000,
960
+ };
961
+ // ─────────────────────────────────────────────────────────────────────────
962
+ // Step 1: Set up MCP Server (for tools)
963
+ // ─────────────────────────────────────────────────────────────────────────
964
+ let mcpSetupSuccess = false;
965
+ let isActive = false;
966
+ let mcpError = null;
967
+ try {
968
+ writeProgress(`🔄 Setting up MCP server...`);
969
+ const result = await context.developerApi.createMCPServer(mcpServerData);
970
+ if (result.success && result.data) {
971
+ // Auto-activate the MCP server so the agent can use the connection immediately
972
+ const activateResult = await context.developerApi.activateMCPServer(result.data.id);
973
+ isActive = activateResult.success && activateResult.data?.active === true;
974
+ mcpSetupSuccess = true;
975
+ }
976
+ else {
977
+ mcpError = result.error?.message || 'Unknown error';
978
+ }
979
+ }
980
+ catch (error) {
981
+ mcpError = error.message || 'Unknown error';
982
+ }
983
+ // ─────────────────────────────────────────────────────────────────────────
984
+ // Step 2: Set up Webhook Triggers (for event-driven agent wake-up)
985
+ // ─────────────────────────────────────────────────────────────────────────
986
+ const createdWebhooks = [];
987
+ const failedWebhooks = [];
988
+ if (webhookConfig.triggers.length > 0) {
989
+ writeProgress(`🔄 Setting up ${webhookConfig.triggers.length} trigger(s)...`);
990
+ for (const trigger of webhookConfig.triggers) {
991
+ try {
992
+ // Use per-trigger hookUrl/interval if available, otherwise fall back to defaults
993
+ const hookUrl = trigger.hookUrl || webhookConfig.webhookUrl || AGENT_WEBHOOK_URL;
994
+ const interval = trigger.interval ?? (trigger.webhookType === 'virtual' ? DEFAULT_VIRTUAL_WEBHOOK_INTERVAL : undefined);
995
+ const webhookResult = await context.unifiedToApi.createWebhookSubscription(context.agentId, {
996
+ connectionId,
997
+ objectType: trigger.objectType,
998
+ event: trigger.event,
999
+ hookUrl,
1000
+ interval,
1001
+ });
1002
+ if (webhookResult.success && webhookResult.data) {
1003
+ createdWebhooks.push(webhookResult.data);
1004
+ }
1005
+ else {
1006
+ const errorMsg = webhookResult.error?.message || 'Unknown error';
1007
+ failedWebhooks.push(`${trigger.objectType}.${trigger.event}: ${errorMsg}`);
1008
+ }
1009
+ }
1010
+ catch (error) {
1011
+ const errorMsg = error?.message || 'Unknown error';
1012
+ failedWebhooks.push(`${trigger.objectType}.${trigger.event}: ${errorMsg}`);
1013
+ }
1014
+ }
1015
+ }
1016
+ // ─────────────────────────────────────────────────────────────────────────
1017
+ // Step 3: Display Results
1018
+ // ─────────────────────────────────────────────────────────────────────────
1019
+ console.log("\n" + "─".repeat(60));
1020
+ console.log("🎉 Connection Established!");
1021
+ console.log("─".repeat(60));
1022
+ console.log(`\n Integration: ${integration.name}`);
1023
+ console.log(` Connection ID: ${connectionId}`);
1024
+ // MCP Server status
1025
+ console.log(`\n 📦 MCP Server (Tools):`);
1026
+ if (mcpSetupSuccess) {
1027
+ console.log(` Status: ${isActive ? '🟢 Active' : '🟡 Pending activation'}`);
1028
+ console.log(` Name: ${serverName}`);
1029
+ console.log(` Hide Sensitive: ${hideSensitive ? '✅ Yes' : '❌ No'}`);
1030
+ }
1031
+ else {
1032
+ console.log(` Status: ❌ Failed`);
1033
+ console.log(` Error: ${mcpError}`);
1034
+ }
1035
+ // Show requested scopes with friendly labels
1036
+ if (scopes.length > 0) {
1037
+ console.log(`\n 🔑 Permissions (${scopes.length}):`);
1038
+ scopes.forEach(scopeName => {
1039
+ const scopeInfo = integration.oauthScopes?.find(s => s.unifiedScope === scopeName);
1040
+ if (scopeInfo?.friendlyLabel) {
1041
+ console.log(` • ${scopeInfo.friendlyLabel}`);
1042
+ }
1043
+ else {
1044
+ console.log(` • ${scopeName}`);
1045
+ }
1046
+ });
1047
+ }
1048
+ else {
1049
+ console.log(`\n 🔑 Permissions: Default`);
1050
+ }
1051
+ // Webhook/Triggers status
1052
+ if (webhookConfig.triggers.length > 0) {
1053
+ console.log(`\n ⚡ Triggers (${createdWebhooks.length}/${webhookConfig.triggers.length}):`);
1054
+ // Compute actual URLs being used (per-trigger hookUrl or default)
1055
+ const actualUrls = webhookConfig.triggers.map(t => t.hookUrl || webhookConfig.webhookUrl || AGENT_WEBHOOK_URL);
1056
+ const uniqueUrls = [...new Set(actualUrls)];
1057
+ const allAgentTriggers = uniqueUrls.every(url => url === AGENT_WEBHOOK_URL);
1058
+ if (allAgentTriggers) {
1059
+ console.log(` Mode: Agent wake-up`);
1060
+ }
1061
+ else if (uniqueUrls.length === 1) {
1062
+ console.log(` Mode: Custom webhook`);
1063
+ console.log(` URL: ${uniqueUrls[0]}`);
1064
+ }
1065
+ else {
1066
+ console.log(` Mode: Mixed (per-trigger URLs)`);
1067
+ }
1068
+ if (createdWebhooks.length > 0) {
1069
+ createdWebhooks.forEach(wh => {
1070
+ const trigger = webhookConfig.triggers.find(t => t.objectType === wh.objectType && t.event === wh.event);
1071
+ const desc = trigger?.friendlyDescription || `${wh.objectType}.${wh.event}`;
1072
+ console.log(` ✓ ${desc}`);
1073
+ });
1074
+ }
1075
+ if (failedWebhooks.length > 0) {
1076
+ failedWebhooks.forEach(t => console.log(` ✗ ${t}`));
1077
+ }
1078
+ }
1079
+ // Summary message
1080
+ console.log("\n" + "─".repeat(60));
1081
+ const capabilities = [];
1082
+ if (mcpSetupSuccess && isActive) {
1083
+ capabilities.push('tools via MCP');
1084
+ }
1085
+ if (createdWebhooks.length > 0) {
1086
+ // Check if any triggers use custom URLs
1087
+ const hasCustomUrls = webhookConfig.triggers.some(t => {
1088
+ const url = t.hookUrl || webhookConfig.webhookUrl || AGENT_WEBHOOK_URL;
1089
+ return url !== AGENT_WEBHOOK_URL;
1090
+ });
1091
+ if (hasCustomUrls) {
1092
+ capabilities.push('webhooks to custom URL');
1093
+ }
1094
+ else {
1095
+ capabilities.push('event-driven triggers');
1096
+ }
1097
+ }
1098
+ if (capabilities.length > 0) {
1099
+ console.log(`✅ ${integration.name} connected! Your agent now has: ${capabilities.join(', ')}`);
1100
+ }
1101
+ else if (mcpSetupSuccess && !isActive) {
1102
+ console.log(`⚠️ Connection created but MCP server pending activation.`);
1103
+ console.log(` Run: lua mcp activate --server-name ${serverName}`);
1104
+ }
1105
+ else {
1106
+ console.log(`⚠️ Connection created but setup incomplete. Check errors above.`);
1107
+ }
1108
+ console.log();
1109
+ }
1110
+ async function listConnections(context) {
1111
+ writeProgress("🔄 Loading connections...");
1112
+ try {
1113
+ // Fetch both connections and MCP servers (MCP status is secondary info)
1114
+ const [connectionsResult, serversResult] = await Promise.all([
1115
+ context.unifiedToApi.getConnections(context.agentId),
1116
+ context.developerApi.getMCPServers()
1117
+ ]);
1118
+ const connections = connectionsResult.success ? connectionsResult.data || [] : [];
1119
+ const servers = serversResult.success ? serversResult.data || [] : [];
1120
+ const unifiedServers = servers.filter(s => s.source === 'unifiedto');
1121
+ // Map connection IDs to MCP servers to check tool availability
1122
+ const serverByConnectionId = new Map();
1123
+ for (const server of unifiedServers) {
1124
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
1125
+ if (connectionMatch) {
1126
+ serverByConnectionId.set(connectionMatch[1], server);
1127
+ }
1128
+ }
1129
+ console.log("\n" + "=".repeat(60));
1130
+ console.log("🔗 Connected Integrations");
1131
+ console.log("=".repeat(60) + "\n");
1132
+ if (connections.length === 0) {
1133
+ console.log("ℹ️ No integrations connected yet.");
1134
+ console.log("💡 Run 'lua integrations connect' to connect an integration.\n");
1135
+ return;
1136
+ }
1137
+ for (const connection of connections) {
1138
+ const linkedServer = serverByConnectionId.get(connection.id);
1139
+ const toolsAvailable = linkedServer?.active === true;
1140
+ // Status based on connection health + tool availability
1141
+ let statusIcon = '⚪';
1142
+ let statusText = 'Inactive';
1143
+ if (connection.status === 'unhealthy') {
1144
+ statusIcon = '🔴';
1145
+ statusText = 'Unhealthy - run "lua integrations update" to re-authorize';
1146
+ }
1147
+ else if (connection.status === 'paused') {
1148
+ statusIcon = '⏸️';
1149
+ statusText = 'Paused';
1150
+ }
1151
+ else if (connection.status === 'active' && toolsAvailable) {
1152
+ statusIcon = '🟢';
1153
+ statusText = 'Active';
1154
+ }
1155
+ else if (connection.status === 'active') {
1156
+ statusIcon = '🟡';
1157
+ statusText = 'Connected (MCP pending)';
1158
+ }
1159
+ console.log(`${statusIcon} ${connection.integrationName || connection.integrationType}`);
1160
+ console.log(` Connection: ${connection.id}`);
1161
+ console.log(` Status: ${statusText}`);
1162
+ if (linkedServer) {
1163
+ console.log(` MCP Server: ${linkedServer.name} (${linkedServer.active ? 'active' : 'inactive'})`);
1164
+ }
1165
+ console.log(` Connected: ${new Date(connection.createdAt).toLocaleDateString()}`);
1166
+ console.log();
1167
+ }
1168
+ console.log("=".repeat(60));
1169
+ console.log(`Total: ${connections.length} integration(s)\n`);
1170
+ }
1171
+ catch (error) {
1172
+ writeError(`❌ Error loading connections: ${error.message}`);
1173
+ }
1174
+ }
1175
+ async function disconnectIntegration(context, connectionId) {
1176
+ writeProgress(`🔄 Disconnecting... (please wait, do not close this window)`);
1177
+ try {
1178
+ // Clean up associated MCP server (internal implementation detail)
1179
+ const mcpServers = await context.developerApi.getMCPServers();
1180
+ if (mcpServers.success && mcpServers.data) {
1181
+ const associatedServer = mcpServers.data.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${connectionId}`));
1182
+ if (associatedServer) {
1183
+ await context.developerApi.deleteMCPServer(associatedServer.id);
1184
+ }
1185
+ }
1186
+ // Delete the connection (also deletes webhook subscriptions)
1187
+ const deleteResult = await context.unifiedToApi.deleteConnection(connectionId, context.agentId);
1188
+ if (deleteResult.success) {
1189
+ writeSuccess(`✅ Integration disconnected successfully!`);
1190
+ if (deleteResult.data?.deletedWebhooksCount && deleteResult.data.deletedWebhooksCount > 0) {
1191
+ console.log(` ✓ Deleted ${deleteResult.data.deletedWebhooksCount} trigger(s)`);
1192
+ }
1193
+ console.log();
1194
+ }
1195
+ else {
1196
+ writeError(`❌ Failed to disconnect: ${deleteResult.error?.message}`);
1197
+ }
1198
+ }
1199
+ catch (error) {
1200
+ writeError(`❌ Error disconnecting: ${error.message}`);
1201
+ }
1202
+ }
1203
+ async function disconnectIntegrationInteractive(context) {
1204
+ try {
1205
+ const connectionsResult = await context.unifiedToApi.getConnections(context.agentId);
1206
+ if (!connectionsResult.success) {
1207
+ writeError(`❌ Failed to load connections: ${connectionsResult.error?.message}`);
1208
+ return;
1209
+ }
1210
+ const connections = connectionsResult.data || [];
1211
+ if (connections.length === 0) {
1212
+ console.log("\nℹ️ No integrations to disconnect.\n");
1213
+ return;
1214
+ }
1215
+ const connectionAnswer = await safePrompt([
1216
+ {
1217
+ type: 'list',
1218
+ name: 'connection',
1219
+ message: 'Select an integration to disconnect:',
1220
+ choices: connections.map(c => ({
1221
+ name: `${c.integrationName || c.integrationType} (${c.id.substring(0, 8)}...)`,
1222
+ value: c.id
1223
+ }))
1224
+ }
1225
+ ]);
1226
+ if (!connectionAnswer?.connection)
1227
+ return;
1228
+ const selectedConnection = connections.find(c => c.id === connectionAnswer.connection);
1229
+ const confirmAnswer = await safePrompt([
1230
+ {
1231
+ type: 'confirm',
1232
+ name: 'confirm',
1233
+ message: `Disconnect ${selectedConnection?.integrationName || selectedConnection?.integrationType}? Your agent will lose access to this integration.`,
1234
+ default: false
1235
+ }
1236
+ ]);
1237
+ if (!confirmAnswer?.confirm) {
1238
+ console.log("\n❌ Cancelled.\n");
1239
+ return;
1240
+ }
1241
+ await disconnectIntegration(context, connectionAnswer.connection);
1242
+ }
1243
+ catch (error) {
1244
+ writeError(`❌ Error: ${error.message}`);
1245
+ }
1246
+ }
1247
+ // ─────────────────────────────────────────────────────────────────────────────
1248
+ // Update Connection Flow
1249
+ // ─────────────────────────────────────────────────────────────────────────────
1250
+ async function updateConnectionFlow(context, options = {}) {
1251
+ // Step 1: Fetch existing connections and available integrations
1252
+ writeProgress("🔄 Loading connections...");
1253
+ let connections = [];
1254
+ let integrations = [];
1255
+ try {
1256
+ const [connectionsResult, integrationsResult] = await Promise.all([
1257
+ context.unifiedToApi.getConnections(context.agentId),
1258
+ fetchAvailableIntegrations(context.unifiedToApi)
1259
+ ]);
1260
+ connections = connectionsResult.success ? connectionsResult.data || [] : [];
1261
+ integrations = integrationsResult;
1262
+ }
1263
+ catch (error) {
1264
+ writeError(`❌ Failed to load connections: ${error.message}`);
1265
+ return;
1266
+ }
1267
+ if (connections.length === 0) {
1268
+ writeInfo("No integrations to update.");
1269
+ console.log("💡 Run 'lua integrations connect' to connect an integration.\n");
1270
+ return;
1271
+ }
1272
+ // Step 2: Select connection to update
1273
+ let selectedConnection;
1274
+ let selectedIntegration;
1275
+ if (options.integration) {
1276
+ // Find connection by integration type
1277
+ selectedConnection = connections.find(c => c.integrationType === options.integration);
1278
+ if (!selectedConnection) {
1279
+ console.error(`❌ No connection found for integration "${options.integration}".`);
1280
+ console.log('\nConnected integrations:');
1281
+ connections.forEach(c => console.log(` - ${c.integrationType} (${c.integrationName || c.integrationType})`));
1282
+ process.exit(1);
1283
+ }
1284
+ selectedIntegration = integrations.find(i => i.value === options.integration);
1285
+ }
1286
+ else {
1287
+ // Interactive selection
1288
+ const connectionAnswer = await safePrompt([
1289
+ {
1290
+ type: 'list',
1291
+ name: 'connection',
1292
+ message: 'Select a connection to update:',
1293
+ choices: connections.map(c => ({
1294
+ name: `${c.integrationName || c.integrationType} (${c.id.substring(0, 8)}...)`,
1295
+ value: c
1296
+ }))
1297
+ }
1298
+ ]);
1299
+ if (!connectionAnswer?.connection)
1300
+ return;
1301
+ selectedConnection = connectionAnswer.connection;
1302
+ selectedIntegration = integrations.find(i => i.value === selectedConnection.integrationType);
1303
+ }
1304
+ if (!selectedIntegration) {
1305
+ writeError(`❌ Integration details not found for ${selectedConnection.integrationType}`);
1306
+ return;
1307
+ }
1308
+ console.log(`\n📌 Updating: ${selectedIntegration.name}`);
1309
+ console.log(` Current Connection ID: ${selectedConnection.id}`);
1310
+ // Step 3: Check if OAuth with configurable scopes
1311
+ const canUseOAuth = selectedIntegration.oauthConfigured &&
1312
+ ['oauth', 'both'].includes(selectedIntegration.authSupport);
1313
+ if (!canUseOAuth || !selectedIntegration.oauthScopes?.length) {
1314
+ writeInfo("This integration does not have configurable scopes.");
1315
+ console.log("💡 To reconnect with different credentials, disconnect and connect again.\n");
1316
+ return;
1317
+ }
1318
+ // Step 4: Select new scopes
1319
+ const availableScopes = selectedIntegration.oauthScopes.map(s => s.unifiedScope);
1320
+ let selectedScopes = [];
1321
+ if (options.scopes) {
1322
+ // Non-interactive: use provided scopes
1323
+ if (options.scopes.toLowerCase() === 'all') {
1324
+ selectedScopes = availableScopes;
1325
+ writeInfo(`Using all ${selectedScopes.length} available scope(s)`);
1326
+ }
1327
+ else {
1328
+ const requestedScopes = options.scopes.split(',').map(s => s.trim());
1329
+ const invalidScopes = requestedScopes.filter(s => !availableScopes.includes(s));
1330
+ if (invalidScopes.length > 0) {
1331
+ console.error(`❌ Invalid scopes: ${invalidScopes.join(', ')}`);
1332
+ console.log(`\nAvailable scopes for ${selectedIntegration.name}:`);
1333
+ availableScopes.forEach(s => console.log(` - ${s}`));
1334
+ process.exit(1);
1335
+ }
1336
+ selectedScopes = requestedScopes;
1337
+ writeInfo(`Using ${selectedScopes.length} specified scope(s)`);
1338
+ }
1339
+ }
1340
+ else {
1341
+ // Interactive: prompt for scope selection
1342
+ const scopeAnswer = await safePrompt([{
1343
+ type: 'checkbox',
1344
+ name: 'scopes',
1345
+ message: 'Select new OAuth scopes (Space to toggle, Enter to confirm, leave empty for all):',
1346
+ pageSize: 15,
1347
+ loop: false,
1348
+ choices: selectedIntegration.oauthScopes.map(s => {
1349
+ // Use friendly label if available, otherwise fall back to technical name
1350
+ const displayName = s.friendlyLabel || s.unifiedScope;
1351
+ const description = s.friendlyDescription
1352
+ ? ` (${s.friendlyDescription})`
1353
+ : ` → [${s.originalScopes.join(', ')}]`;
1354
+ return {
1355
+ name: `${displayName}${description}`,
1356
+ value: s.unifiedScope,
1357
+ checked: false
1358
+ };
1359
+ })
1360
+ }]);
1361
+ if (!scopeAnswer)
1362
+ return;
1363
+ selectedScopes = scopeAnswer.scopes.length > 0
1364
+ ? scopeAnswer.scopes
1365
+ : availableScopes;
1366
+ console.log(`\n✓ Will request ${selectedScopes.length} scope(s)`);
1367
+ }
1368
+ // Determine hide_sensitive setting
1369
+ let hideSensitive = true; // Default to true
1370
+ if (options.hideSensitive !== undefined) {
1371
+ hideSensitive = options.hideSensitive;
1372
+ }
1373
+ else {
1374
+ // Interactive: ask user
1375
+ const sensitiveAnswer = await safePrompt([{
1376
+ type: 'confirm',
1377
+ name: 'hide',
1378
+ message: 'Hide sensitive data from MCP tools? (recommended for security)',
1379
+ default: true
1380
+ }]);
1381
+ if (sensitiveAnswer) {
1382
+ hideSensitive = sensitiveAnswer.hide;
1383
+ }
1384
+ }
1385
+ // Step 5: Fetch existing triggers before deletion (so we can re-create them)
1386
+ let existingTriggers = [];
1387
+ try {
1388
+ const webhooksResult = await context.unifiedToApi.getWebhookSubscriptions(context.agentId);
1389
+ if (webhooksResult.success && webhooksResult.data && webhooksResult.data.length > 0) {
1390
+ // Filter webhooks to only those belonging to this connection
1391
+ const connectionWebhooks = webhooksResult.data.filter(w => w.connectionId === selectedConnection.id);
1392
+ existingTriggers = connectionWebhooks.map(w => ({
1393
+ objectType: w.objectType,
1394
+ event: w.event,
1395
+ hookUrl: w.hookUrl,
1396
+ interval: w.interval,
1397
+ }));
1398
+ if (existingTriggers.length > 0) {
1399
+ writeInfo(`Found ${existingTriggers.length} existing trigger(s) - will restore after update`);
1400
+ }
1401
+ }
1402
+ }
1403
+ catch (error) {
1404
+ // If we can't fetch existing triggers, continue without them
1405
+ }
1406
+ // Step 6: Confirm the update
1407
+ if (!options.integration) {
1408
+ const confirmAnswer = await safePrompt([
1409
+ {
1410
+ type: 'confirm',
1411
+ name: 'confirm',
1412
+ message: `Update ${selectedIntegration.name}? This will re-authorize with new scopes.`,
1413
+ default: true
1414
+ }
1415
+ ]);
1416
+ if (!confirmAnswer?.confirm) {
1417
+ console.log("\n❌ Cancelled.\n");
1418
+ return;
1419
+ }
1420
+ }
1421
+ // Step 7: Delete the old connection (silently)
1422
+ writeProgress(`🔄 Updating ${selectedIntegration.name}...`);
1423
+ try {
1424
+ // Clean up old MCP server
1425
+ const mcpServers = await context.developerApi.getMCPServers();
1426
+ if (mcpServers.success && mcpServers.data) {
1427
+ const associatedServer = mcpServers.data.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${selectedConnection.id}`));
1428
+ if (associatedServer) {
1429
+ await context.developerApi.deleteMCPServer(associatedServer.id);
1430
+ }
1431
+ }
1432
+ // Delete old connection (this also deletes associated webhooks in Unified.to)
1433
+ await context.unifiedToApi.deleteConnection(selectedConnection.id, context.agentId);
1434
+ }
1435
+ catch (error) {
1436
+ writeError(`❌ Failed to remove old connection: ${error.message}`);
1437
+ return;
1438
+ }
1439
+ // Step 8: Create new connection with new scopes
1440
+ const state = Buffer.from(JSON.stringify({
1441
+ agentId: context.agentId,
1442
+ integration: selectedIntegration.value,
1443
+ authMethod: 'oauth',
1444
+ timestamp: Date.now()
1445
+ })).toString('base64');
1446
+ const externalXref = JSON.stringify({
1447
+ agentId: context.agentId,
1448
+ userId: context.userId,
1449
+ });
1450
+ const authUrlResult = await context.unifiedToApi.getAuthUrl(selectedIntegration.value, {
1451
+ successRedirect: CALLBACK_URL,
1452
+ failureRedirect: `${CALLBACK_URL}?error=auth_failed`,
1453
+ scopes: selectedScopes,
1454
+ state,
1455
+ externalXref,
1456
+ });
1457
+ if (!authUrlResult.success || !authUrlResult.data) {
1458
+ writeError(`❌ Failed to get authorization URL: ${authUrlResult.error?.message || 'Unknown error'}`);
1459
+ return;
1460
+ }
1461
+ const authUrl = authUrlResult.data.authUrl;
1462
+ // Pre-fetch to handle redirects
1463
+ let finalAuthUrl = authUrl;
1464
+ try {
1465
+ const response = await fetch(authUrl);
1466
+ const responseText = await response.text();
1467
+ if (responseText.includes('test.html?redirect=')) {
1468
+ const urlMatch = responseText.match(/redirect=([^&\s]+)/);
1469
+ if (urlMatch) {
1470
+ finalAuthUrl = decodeURIComponent(urlMatch[1]);
1471
+ }
1472
+ }
1473
+ else if (responseText.startsWith('https://')) {
1474
+ finalAuthUrl = responseText.trim();
1475
+ }
1476
+ }
1477
+ catch (error) {
1478
+ // Use original auth URL if pre-fetch fails
1479
+ }
1480
+ // Step 9: Open browser and wait for callback
1481
+ console.log("\n" + "─".repeat(60));
1482
+ console.log("🌐 Re-authorizing with new scopes...");
1483
+ console.log("─".repeat(60));
1484
+ console.log(`\n📋 Authorization URL (copy if browser doesn't open):\n ${finalAuthUrl}\n`);
1485
+ const callbackPromise = startCallbackServer(300000);
1486
+ try {
1487
+ await open(finalAuthUrl);
1488
+ writeInfo("🌐 Browser opened - please complete the authorization");
1489
+ }
1490
+ catch (error) {
1491
+ writeInfo("💡 Could not open browser automatically. Please open the URL above manually.");
1492
+ }
1493
+ console.log("\n⏳ Waiting for authorization (5 minute timeout)...");
1494
+ console.log("💡 Complete the authorization in your browser.\n");
1495
+ const result = await callbackPromise;
1496
+ if (result.success && result.connectionId) {
1497
+ writeSuccess("\n✅ Authorization successful!");
1498
+ // Check if any triggers use custom webhooks
1499
+ const hasCustomWebhook = existingTriggers.some(t => t.hookUrl !== AGENT_WEBHOOK_URL);
1500
+ // Convert existing triggers to TriggerWithSettings format, preserving per-trigger hookUrl and interval
1501
+ let triggersToRestore = [];
1502
+ if (existingTriggers.length > 0) {
1503
+ try {
1504
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEventsByType(selectedIntegration.value);
1505
+ if (eventsResult.success && eventsResult.data) {
1506
+ for (const trigger of existingTriggers) {
1507
+ const matchingEvent = eventsResult.data.find(e => e.objectType === trigger.objectType && e.event === trigger.event);
1508
+ if (matchingEvent) {
1509
+ // Preserve per-trigger hookUrl and interval from original webhook
1510
+ triggersToRestore.push({
1511
+ ...matchingEvent,
1512
+ hookUrl: trigger.hookUrl,
1513
+ interval: trigger.interval,
1514
+ });
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ catch (error) {
1520
+ // If we can't match triggers, continue without them
1521
+ }
1522
+ }
1523
+ await finalizeConnection(context, selectedIntegration, result.connectionId, selectedScopes, hideSensitive, {
1524
+ triggers: triggersToRestore,
1525
+ webhookUrl: AGENT_WEBHOOK_URL, // Default, but per-trigger hookUrl will override if set
1526
+ isCustomWebhook: hasCustomWebhook,
1527
+ });
1528
+ if (triggersToRestore.length > 0) {
1529
+ writeSuccess(`Restored ${triggersToRestore.length} trigger(s) from previous connection`);
1530
+ }
1531
+ }
1532
+ else {
1533
+ writeError(`\n❌ Authorization failed: ${result.error || 'Unknown error'}`);
1534
+ console.log("💡 The old connection was removed. Please reconnect with 'lua integrations connect'\n");
1535
+ }
1536
+ }
1537
+ // ─────────────────────────────────────────────────────────────────────────────
1538
+ // Webhook Subscription Flows
1539
+ // ─────────────────────────────────────────────────────────────────────────────
1540
+ /**
1541
+ * Handle webhooks subcommand (non-interactive)
1542
+ */
1543
+ async function webhooksSubcommand(context, cmdOptions) {
1544
+ const subAction = cmdOptions?._?.[0]?.toLowerCase() || '';
1545
+ const options = {
1546
+ connectionId: cmdOptions?.connection || cmdOptions?.connectionId,
1547
+ webhookId: cmdOptions?.webhookId,
1548
+ objectType: cmdOptions?.object,
1549
+ event: cmdOptions?.event,
1550
+ hookUrl: cmdOptions?.hookUrl,
1551
+ interval: cmdOptions?.interval ? parseInt(cmdOptions.interval, 10) : undefined,
1552
+ integration: cmdOptions?.integration,
1553
+ };
1554
+ const jsonOutput = cmdOptions?.json === true;
1555
+ switch (subAction) {
1556
+ case 'list':
1557
+ await webhooksListFlow(context, jsonOutput);
1558
+ break;
1559
+ case 'create':
1560
+ await webhooksCreateFlow(context, options);
1561
+ break;
1562
+ case 'delete':
1563
+ if (!options.webhookId) {
1564
+ console.error("❌ --webhook-id is required for delete");
1565
+ console.log("\n💡 Run 'lua integrations webhooks list' to see trigger IDs");
1566
+ process.exit(1);
1567
+ }
1568
+ await webhooksDeleteFlow(context, options.webhookId);
1569
+ break;
1570
+ case 'events':
1571
+ // New: List available webhook events for a connection or integration type
1572
+ await webhooksEventsFlow(context, options, jsonOutput);
1573
+ break;
1574
+ default:
1575
+ console.error(`❌ Invalid webhooks action: "${subAction || '(none)'}"`);
1576
+ console.log('\nUsage:');
1577
+ console.log(' lua integrations webhooks list List triggers');
1578
+ console.log(' lua integrations webhooks events --connection <id> List available events for a connection');
1579
+ console.log(' lua integrations webhooks events --integration <type> List available events for an integration');
1580
+ console.log(' lua integrations webhooks create Create trigger (interactive)');
1581
+ console.log(' lua integrations webhooks create --connection <id> --object <type> --event <event> --hook-url <url>');
1582
+ console.log(' lua integrations webhooks delete --webhook-id <id> Delete trigger');
1583
+ process.exit(1);
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Show available webhook events for a connection or integration type
1588
+ * Used for non-interactive discovery
1589
+ */
1590
+ async function webhooksEventsFlow(context, options, jsonOutput = false) {
1591
+ try {
1592
+ let webhookEvents = [];
1593
+ let sourceName = '';
1594
+ if (options.connectionId) {
1595
+ // Get events by connection ID
1596
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEvents(context.agentId, options.connectionId);
1597
+ if (eventsResult.success && eventsResult.data) {
1598
+ webhookEvents = eventsResult.data;
1599
+ }
1600
+ sourceName = `connection ${options.connectionId}`;
1601
+ }
1602
+ else if (options.integration) {
1603
+ // Get events by integration type
1604
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEventsByType(options.integration);
1605
+ if (eventsResult.success && eventsResult.data) {
1606
+ webhookEvents = eventsResult.data;
1607
+ }
1608
+ sourceName = `integration ${options.integration}`;
1609
+ }
1610
+ else {
1611
+ if (jsonOutput) {
1612
+ console.log(JSON.stringify({ error: 'Either --connection or --integration is required' }));
1613
+ }
1614
+ else {
1615
+ console.error("❌ Either --connection <id> or --integration <type> is required");
1616
+ console.log("\nUsage:");
1617
+ console.log(" lua integrations webhooks events --connection <id>");
1618
+ console.log(" lua integrations webhooks events --integration <type>");
1619
+ }
1620
+ process.exit(1);
1621
+ }
1622
+ if (jsonOutput) {
1623
+ const output = {
1624
+ source: sourceName,
1625
+ events: webhookEvents.map(e => ({
1626
+ trigger: `${e.objectType}.${e.event}`,
1627
+ objectType: e.objectType,
1628
+ event: e.event,
1629
+ webhookType: e.webhookType,
1630
+ friendlyLabel: e.friendlyLabel,
1631
+ friendlyDescription: e.friendlyDescription,
1632
+ availableFilters: e.availableFilters,
1633
+ })),
1634
+ defaults: {
1635
+ agentWebhookUrl: AGENT_WEBHOOK_URL,
1636
+ defaultVirtualInterval: DEFAULT_VIRTUAL_WEBHOOK_INTERVAL,
1637
+ },
1638
+ };
1639
+ console.log(JSON.stringify(output, null, 2));
1640
+ }
1641
+ else {
1642
+ console.log("\n" + "=".repeat(60));
1643
+ console.log(`📡 Available Trigger Events for ${sourceName}`);
1644
+ console.log("=".repeat(60) + "\n");
1645
+ if (webhookEvents.length === 0) {
1646
+ console.log("ℹ️ No trigger events available for this integration.\n");
1647
+ }
1648
+ else {
1649
+ webhookEvents.forEach(e => {
1650
+ console.log(` ${e.objectType}.${e.event} [${e.webhookType}]`);
1651
+ console.log(` ${e.friendlyDescription}`);
1652
+ if (e.availableFilters.length > 0) {
1653
+ console.log(` Filters: ${e.availableFilters.join(', ')}`);
1654
+ }
1655
+ console.log();
1656
+ });
1657
+ console.log("─".repeat(60));
1658
+ console.log(`Total: ${webhookEvents.length} event(s) available`);
1659
+ console.log("\n💡 Use with --triggers flag:");
1660
+ const exampleTriggers = webhookEvents.slice(0, 2).map(e => `${e.objectType}.${e.event}`).join(',');
1661
+ console.log(` lua integrations connect --integration <type> --triggers ${exampleTriggers}\n`);
1662
+ }
1663
+ }
1664
+ }
1665
+ catch (error) {
1666
+ if (jsonOutput) {
1667
+ console.log(JSON.stringify({ error: error.message }));
1668
+ }
1669
+ else {
1670
+ writeError(`❌ Failed to get trigger events: ${error.message}`);
1671
+ }
1672
+ process.exit(1);
1673
+ }
1674
+ }
1675
+ /**
1676
+ * Interactive webhooks menu
1677
+ */
1678
+ async function webhooksInteractiveMenu(context) {
1679
+ console.log("\n" + "─".repeat(60));
1680
+ console.log("🔔 Triggers");
1681
+ console.log("─".repeat(60) + "\n");
1682
+ const actionAnswer = await safePrompt([
1683
+ {
1684
+ type: 'list',
1685
+ name: 'action',
1686
+ message: 'What would you like to do?',
1687
+ choices: [
1688
+ { name: '📋 List triggers', value: 'list' },
1689
+ { name: '➕ Create new subscription', value: 'create' },
1690
+ { name: '🗑️ Delete subscription', value: 'delete' },
1691
+ { name: '← Back', value: 'back' }
1692
+ ]
1693
+ }
1694
+ ]);
1695
+ if (!actionAnswer || actionAnswer.action === 'back')
1696
+ return;
1697
+ switch (actionAnswer.action) {
1698
+ case 'list':
1699
+ await webhooksListFlow(context);
1700
+ break;
1701
+ case 'create':
1702
+ await webhooksCreateFlow(context, {});
1703
+ break;
1704
+ case 'delete':
1705
+ await webhooksDeleteInteractive(context);
1706
+ break;
1707
+ }
1708
+ }
1709
+ /**
1710
+ * List all webhook subscriptions
1711
+ */
1712
+ async function webhooksListFlow(context, jsonOutput = false) {
1713
+ writeProgress("🔄 Loading triggers...");
1714
+ try {
1715
+ const result = await context.unifiedToApi.getWebhookSubscriptions(context.agentId);
1716
+ if (!result.success) {
1717
+ if (jsonOutput) {
1718
+ console.log(JSON.stringify({ error: result.error?.message || 'Failed to load triggers' }));
1719
+ }
1720
+ else {
1721
+ writeError(`❌ Failed to load triggers: ${result.error?.message}`);
1722
+ }
1723
+ return;
1724
+ }
1725
+ const webhooks = result.data || [];
1726
+ if (jsonOutput) {
1727
+ // Output as JSON for scripting
1728
+ console.log(JSON.stringify({
1729
+ triggers: webhooks.map(wh => ({
1730
+ id: wh.id,
1731
+ integrationType: wh.integrationType,
1732
+ objectType: wh.objectType,
1733
+ event: wh.event,
1734
+ webhookType: wh.webhookType,
1735
+ hookUrl: wh.hookUrl,
1736
+ status: wh.status,
1737
+ interval: wh.interval,
1738
+ connectionId: wh.connectionId,
1739
+ })),
1740
+ total: webhooks.length,
1741
+ }, null, 2));
1742
+ return;
1743
+ }
1744
+ console.log("\n" + "─".repeat(60));
1745
+ console.log("⚡ Triggers / Webhook Subscriptions");
1746
+ console.log("─".repeat(60) + "\n");
1747
+ if (webhooks.length === 0) {
1748
+ console.log("ℹ️ No triggers configured yet.\n");
1749
+ console.log("💡 Add triggers when connecting: lua integrations connect --triggers <events>");
1750
+ console.log(" Or manually: lua integrations webhooks create\n");
1751
+ return;
1752
+ }
1753
+ // Group by integration
1754
+ const byIntegration = new Map();
1755
+ for (const wh of webhooks) {
1756
+ const key = wh.integrationType;
1757
+ if (!byIntegration.has(key))
1758
+ byIntegration.set(key, []);
1759
+ byIntegration.get(key).push(wh);
1760
+ }
1761
+ for (const [integration, integrationWebhooks] of byIntegration) {
1762
+ console.log(`📦 ${integration}`);
1763
+ for (const wh of integrationWebhooks) {
1764
+ const statusIcon = wh.status === 'active' ? '✅' : '⚪';
1765
+ const typeLabel = wh.webhookType === 'native' ? 'push' : 'poll';
1766
+ const isAgentTrigger = wh.hookUrl.includes('webhook/unifiedto');
1767
+ const mode = isAgentTrigger ? 'agent trigger' : 'custom URL';
1768
+ // Show webhook ID for delete operations
1769
+ console.log(` ${statusIcon} ${wh.objectType}.${wh.event} (${typeLabel}, ${mode})`);
1770
+ console.log(` ID: ${wh.id}`);
1771
+ if (!isAgentTrigger) {
1772
+ console.log(` URL: ${wh.hookUrl}`);
1773
+ }
1774
+ }
1775
+ console.log();
1776
+ }
1777
+ console.log("─".repeat(60));
1778
+ console.log(`Total: ${webhooks.length} trigger(s)\n`);
1779
+ }
1780
+ catch (error) {
1781
+ if (jsonOutput) {
1782
+ console.log(JSON.stringify({ error: error.message }));
1783
+ }
1784
+ else {
1785
+ writeError(`❌ Error: ${error.message}`);
1786
+ }
1787
+ }
1788
+ }
1789
+ /**
1790
+ * Create a webhook subscription
1791
+ */
1792
+ async function webhooksCreateFlow(context, options) {
1793
+ // Step 1: Fetch connections
1794
+ writeProgress("🔄 Loading connections...");
1795
+ let connections = [];
1796
+ try {
1797
+ const connectionsResult = await context.unifiedToApi.getConnections(context.agentId);
1798
+ if (!connectionsResult.success) {
1799
+ writeError(`❌ Failed to load connections: ${connectionsResult.error?.message}`);
1800
+ return;
1801
+ }
1802
+ connections = connectionsResult.data || [];
1803
+ }
1804
+ catch (error) {
1805
+ writeError(`❌ Error: ${error.message}`);
1806
+ return;
1807
+ }
1808
+ if (connections.length === 0) {
1809
+ writeInfo("No connections available.");
1810
+ console.log("💡 Run 'lua integrations connect' to connect an integration first.\n");
1811
+ return;
1812
+ }
1813
+ // Step 2: Select connection
1814
+ let selectedConnection;
1815
+ if (options.connectionId) {
1816
+ selectedConnection = connections.find(c => c.id === options.connectionId);
1817
+ if (!selectedConnection) {
1818
+ console.error(`❌ Connection "${options.connectionId}" not found.`);
1819
+ console.log('\nAvailable connections:');
1820
+ connections.forEach(c => console.log(` - ${c.id} (${c.integrationName || c.integrationType})`));
1821
+ process.exit(1);
1822
+ }
1823
+ }
1824
+ else {
1825
+ const connectionAnswer = await safePrompt([
1826
+ {
1827
+ type: 'list',
1828
+ name: 'connection',
1829
+ message: 'Select a connection:',
1830
+ choices: connections.map(c => ({
1831
+ name: `${c.integrationName || c.integrationType} (${c.id.substring(0, 8)}...)`,
1832
+ value: c
1833
+ }))
1834
+ }
1835
+ ]);
1836
+ if (!connectionAnswer)
1837
+ return;
1838
+ selectedConnection = connectionAnswer.connection;
1839
+ }
1840
+ // Step 3: Get available events for this connection
1841
+ writeProgress("🔄 Loading available events...");
1842
+ let availableEvents = [];
1843
+ try {
1844
+ const eventsResult = await context.unifiedToApi.getAvailableWebhookEvents(context.agentId, selectedConnection.id);
1845
+ if (!eventsResult.success) {
1846
+ writeError(`❌ Failed to load events: ${eventsResult.error?.message}`);
1847
+ return;
1848
+ }
1849
+ availableEvents = eventsResult.data || [];
1850
+ }
1851
+ catch (error) {
1852
+ writeError(`❌ Error: ${error.message}`);
1853
+ return;
1854
+ }
1855
+ if (availableEvents.length === 0) {
1856
+ writeInfo(`No trigger events available for ${selectedConnection.integrationName || selectedConnection.integrationType}.`);
1857
+ console.log("💡 This integration may not support triggers.\n");
1858
+ return;
1859
+ }
1860
+ // Step 4: Select event
1861
+ let selectedEvent;
1862
+ if (options.objectType && options.event) {
1863
+ selectedEvent = availableEvents.find(e => e.objectType === options.objectType && e.event === options.event);
1864
+ if (!selectedEvent) {
1865
+ console.error(`❌ Event '${options.objectType}.${options.event}' is not supported.`);
1866
+ console.log(`\nAvailable events for ${selectedConnection.integrationName || selectedConnection.integrationType}:`);
1867
+ availableEvents.forEach(e => console.log(` - ${e.objectType}.${e.event} [${e.webhookType}]`));
1868
+ process.exit(1);
1869
+ }
1870
+ }
1871
+ else {
1872
+ console.log(`\nAvailable trigger events for ${selectedConnection.integrationName || selectedConnection.integrationType}:\n`);
1873
+ const eventAnswer = await safePrompt([
1874
+ {
1875
+ type: 'list',
1876
+ name: 'event',
1877
+ message: 'Select an event to subscribe to:',
1878
+ pageSize: 15,
1879
+ choices: availableEvents.map(e => ({
1880
+ name: `${e.objectTypeDisplay} - ${e.event.charAt(0).toUpperCase() + e.event.slice(1)} [${e.webhookType}]`,
1881
+ value: e
1882
+ }))
1883
+ }
1884
+ ]);
1885
+ if (!eventAnswer)
1886
+ return;
1887
+ selectedEvent = eventAnswer.event;
1888
+ }
1889
+ // Step 5: Get webhook URL
1890
+ let hookUrl;
1891
+ if (options.hookUrl) {
1892
+ hookUrl = options.hookUrl;
1893
+ }
1894
+ else {
1895
+ const webhookAnswer = await safePrompt([
1896
+ {
1897
+ type: 'input',
1898
+ name: 'hookUrl',
1899
+ message: 'Enter the full webhook URL to receive events:',
1900
+ validate: (input) => {
1901
+ if (!input.trim())
1902
+ return 'Webhook URL is required';
1903
+ try {
1904
+ new URL(input);
1905
+ return true;
1906
+ }
1907
+ catch {
1908
+ return 'Enter a valid URL (e.g., https://webhook.heylua.ai/myagent/handler)';
1909
+ }
1910
+ }
1911
+ }
1912
+ ]);
1913
+ if (!webhookAnswer)
1914
+ return;
1915
+ hookUrl = webhookAnswer.hookUrl.trim();
1916
+ }
1917
+ // Step 6: Get interval for virtual webhooks
1918
+ let interval;
1919
+ const intervalOptions = [
1920
+ { name: '1 hour', value: 60 },
1921
+ { name: '2 hours', value: 120 },
1922
+ { name: '4 hours', value: 240 },
1923
+ { name: '8 hours', value: 480 },
1924
+ { name: '12 hours', value: 720 },
1925
+ { name: '24 hours (1 day)', value: 1440 },
1926
+ { name: '48 hours (2 days)', value: 2880 },
1927
+ ];
1928
+ if (selectedEvent.webhookType === 'virtual') {
1929
+ if (options.interval !== undefined) {
1930
+ interval = options.interval;
1931
+ }
1932
+ else {
1933
+ const intervalAnswer = await safePrompt([
1934
+ {
1935
+ type: 'list',
1936
+ name: 'interval',
1937
+ message: 'Select polling interval:',
1938
+ choices: intervalOptions,
1939
+ default: 60,
1940
+ }
1941
+ ]);
1942
+ if (!intervalAnswer)
1943
+ return;
1944
+ interval = intervalAnswer.interval;
1945
+ }
1946
+ }
1947
+ // Helper to format interval display
1948
+ const formatInterval = (minutes) => {
1949
+ const option = intervalOptions.find(o => o.value === minutes);
1950
+ return option ? option.name : `${minutes} minutes`;
1951
+ };
1952
+ // Step 7: Confirmation (interactive only)
1953
+ if (!options.connectionId) {
1954
+ console.log("\n" + "─".repeat(60));
1955
+ console.log("📋 Trigger Summary");
1956
+ console.log("─".repeat(60));
1957
+ console.log(` Connection: ${selectedConnection.integrationName || selectedConnection.integrationType}`);
1958
+ console.log(` Event: ${selectedEvent.objectType}.${selectedEvent.event} [${selectedEvent.webhookType}]`);
1959
+ console.log(` Webhook URL: ${hookUrl}`);
1960
+ if (interval)
1961
+ console.log(` Interval: ${formatInterval(interval)}`);
1962
+ console.log("─".repeat(60) + "\n");
1963
+ const confirmAnswer = await safePrompt([
1964
+ {
1965
+ type: 'confirm',
1966
+ name: 'confirm',
1967
+ message: 'Create this trigger?',
1968
+ default: true
1969
+ }
1970
+ ]);
1971
+ if (!confirmAnswer?.confirm) {
1972
+ console.log("\n❌ Cancelled.\n");
1973
+ return;
1974
+ }
1975
+ }
1976
+ // Step 8: Create the webhook
1977
+ writeProgress("🔄 Creating trigger...");
1978
+ try {
1979
+ const createResult = await context.unifiedToApi.createWebhookSubscription(context.agentId, {
1980
+ connectionId: selectedConnection.id,
1981
+ objectType: selectedEvent.objectType,
1982
+ event: selectedEvent.event,
1983
+ hookUrl,
1984
+ interval,
1985
+ });
1986
+ if (!createResult.success || !createResult.data) {
1987
+ writeError(`❌ Failed to create trigger: ${createResult.error?.message}`);
1988
+ return;
1989
+ }
1990
+ const webhook = createResult.data;
1991
+ const isAgentTrigger = webhook.hookUrl.includes('webhook/unifiedto');
1992
+ const typeLabel = webhook.webhookType === 'native' ? 'push' : 'polling';
1993
+ console.log("\n" + "─".repeat(60));
1994
+ writeSuccess("✅ Trigger created!");
1995
+ console.log("─".repeat(60));
1996
+ console.log(` Integration: ${webhook.integrationType}`);
1997
+ console.log(` Event: ${webhook.objectType}.${webhook.event}`);
1998
+ console.log(` Type: ${typeLabel}`);
1999
+ console.log(` Mode: ${isAgentTrigger ? 'Agent wake-up' : 'Custom URL'}`);
2000
+ if (!isAgentTrigger) {
2001
+ console.log(` URL: ${webhook.hookUrl}`);
2002
+ }
2003
+ if (webhook.interval)
2004
+ console.log(` Poll interval: ${formatInterval(webhook.interval)}`);
2005
+ console.log(` Status: ${webhook.status}`);
2006
+ console.log("─".repeat(60) + "\n");
2007
+ if (isAgentTrigger) {
2008
+ console.log("💡 Your agent will now wake up when this event occurs.\n");
2009
+ }
2010
+ else {
2011
+ console.log("💡 Ensure your webhook endpoint is ready to receive events.\n");
2012
+ }
2013
+ }
2014
+ catch (error) {
2015
+ writeError(`❌ Error: ${error.message}`);
2016
+ }
2017
+ }
2018
+ /**
2019
+ * Delete a webhook subscription (interactive)
2020
+ */
2021
+ async function webhooksDeleteInteractive(context) {
2022
+ writeProgress("🔄 Loading triggers...");
2023
+ try {
2024
+ const result = await context.unifiedToApi.getWebhookSubscriptions(context.agentId);
2025
+ if (!result.success) {
2026
+ writeError(`❌ Failed to load triggers: ${result.error?.message}`);
2027
+ return;
2028
+ }
2029
+ const webhooks = result.data || [];
2030
+ if (webhooks.length === 0) {
2031
+ console.log("\nℹ️ No triggers to delete.\n");
2032
+ return;
2033
+ }
2034
+ const webhookAnswer = await safePrompt([
2035
+ {
2036
+ type: 'list',
2037
+ name: 'webhook',
2038
+ message: 'Select a trigger to delete:',
2039
+ choices: webhooks.map(wh => {
2040
+ // Show last part of URL for readability
2041
+ const urlParts = (wh.hookUrl || '').split('/');
2042
+ const shortUrl = urlParts.length > 3 ? '.../' + urlParts.slice(-2).join('/') : wh.hookUrl || '';
2043
+ return {
2044
+ name: `${wh.id.substring(0, 8)}... - ${wh.integrationType} ${wh.objectType}.${wh.event} → ${shortUrl}`,
2045
+ value: wh.id
2046
+ };
2047
+ })
2048
+ }
2049
+ ]);
2050
+ if (!webhookAnswer?.webhook)
2051
+ return;
2052
+ const selectedWebhook = webhooks.find(wh => wh.id === webhookAnswer.webhook);
2053
+ const confirmAnswer = await safePrompt([
2054
+ {
2055
+ type: 'confirm',
2056
+ name: 'confirm',
2057
+ message: `Delete trigger for ${selectedWebhook?.objectType}.${selectedWebhook?.event}?`,
2058
+ default: false
2059
+ }
2060
+ ]);
2061
+ if (!confirmAnswer?.confirm) {
2062
+ console.log("\n❌ Cancelled.\n");
2063
+ return;
2064
+ }
2065
+ await webhooksDeleteFlow(context, webhookAnswer.webhook);
2066
+ }
2067
+ catch (error) {
2068
+ writeError(`❌ Error: ${error.message}`);
2069
+ }
2070
+ }
2071
+ /**
2072
+ * Delete a webhook subscription (non-interactive)
2073
+ */
2074
+ async function webhooksDeleteFlow(context, webhookId) {
2075
+ writeProgress("🔄 Deleting trigger...");
2076
+ try {
2077
+ const result = await context.unifiedToApi.deleteWebhookSubscription(context.agentId, webhookId);
2078
+ if (result.success) {
2079
+ writeSuccess(`✅ Trigger deleted: ${webhookId}\n`);
2080
+ }
2081
+ else {
2082
+ writeError(`❌ Failed to delete trigger: ${result.error?.message}`);
2083
+ }
2084
+ }
2085
+ catch (error) {
2086
+ writeError(`❌ Error: ${error.message}`);
2087
+ }
2088
+ }
2089
+ // ─────────────────────────────────────────────────────────────────────────────
2090
+ // MCP Server Management
2091
+ // ─────────────────────────────────────────────────────────────────────────────
2092
+ /**
2093
+ * Handle mcp subcommand (non-interactive entry point)
2094
+ */
2095
+ async function mcpSubcommand(context, cmdOptions) {
2096
+ const subAction = cmdOptions?._?.[0]?.toLowerCase() || '';
2097
+ const options = {
2098
+ connectionId: cmdOptions?.connection || cmdOptions?.connectionId,
2099
+ };
2100
+ await mcpManagementFlow(context, options, subAction);
2101
+ }
2102
+ /**
2103
+ * Main handler for MCP management subcommand
2104
+ */
2105
+ async function mcpManagementFlow(context, options, subAction) {
2106
+ // Non-interactive mode
2107
+ if (subAction === 'list') {
2108
+ await mcpListFlow(context);
2109
+ return;
2110
+ }
2111
+ if (subAction === 'activate') {
2112
+ if (!options.connectionId) {
2113
+ writeError("❌ Missing required option: --connection <id>");
2114
+ console.log("\n💡 Use 'lua integrations mcp list' to see available connection IDs.\n");
2115
+ return;
2116
+ }
2117
+ await mcpActivateFlow(context, options.connectionId);
2118
+ return;
2119
+ }
2120
+ if (subAction === 'deactivate') {
2121
+ if (!options.connectionId) {
2122
+ writeError("❌ Missing required option: --connection <id>");
2123
+ console.log("\n💡 Use 'lua integrations mcp list' to see available connection IDs.\n");
2124
+ return;
2125
+ }
2126
+ await mcpDeactivateFlow(context, options.connectionId);
2127
+ return;
2128
+ }
2129
+ // Interactive mode
2130
+ const actionAnswer = await safePrompt([
2131
+ {
2132
+ type: 'list',
2133
+ name: 'action',
2134
+ message: 'What would you like to do?',
2135
+ choices: [
2136
+ { name: '📋 List connections with MCP status', value: 'list' },
2137
+ { name: '✅ Activate MCP server for a connection', value: 'activate' },
2138
+ { name: '⏸️ Deactivate MCP server for a connection', value: 'deactivate' },
2139
+ { name: '← Back', value: 'back' }
2140
+ ]
2141
+ }
2142
+ ]);
2143
+ if (!actionAnswer || actionAnswer.action === 'back')
2144
+ return;
2145
+ switch (actionAnswer.action) {
2146
+ case 'list':
2147
+ await mcpListFlow(context);
2148
+ break;
2149
+ case 'activate':
2150
+ await mcpActivateInteractive(context);
2151
+ break;
2152
+ case 'deactivate':
2153
+ await mcpDeactivateInteractive(context);
2154
+ break;
2155
+ }
2156
+ }
2157
+ /**
2158
+ * List all connections with their MCP server status
2159
+ */
2160
+ async function mcpListFlow(context) {
2161
+ writeProgress("🔄 Loading connections and MCP servers...");
2162
+ try {
2163
+ const [connectionsResult, mcpServers] = await Promise.all([
2164
+ context.unifiedToApi.getConnections(context.agentId),
2165
+ fetchServersCore(context)
2166
+ ]);
2167
+ if (!connectionsResult.success) {
2168
+ writeError(`❌ Failed to load connections: ${connectionsResult.error?.message}`);
2169
+ return;
2170
+ }
2171
+ if (!mcpServers)
2172
+ return;
2173
+ const connections = connectionsResult.data || [];
2174
+ const unifiedServers = mcpServers.filter(s => s.source === 'unifiedto');
2175
+ // Map connection IDs to MCP servers
2176
+ const serverByConnectionId = new Map();
2177
+ for (const server of unifiedServers) {
2178
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
2179
+ if (connectionMatch) {
2180
+ serverByConnectionId.set(connectionMatch[1], server);
2181
+ }
2182
+ }
2183
+ console.log("\n" + "─".repeat(80));
2184
+ console.log("🔌 MCP Servers for Connections");
2185
+ console.log("─".repeat(80) + "\n");
2186
+ if (connections.length === 0) {
2187
+ console.log("ℹ️ No connections found.\n");
2188
+ console.log("💡 Connect an integration with: lua integrations connect\n");
2189
+ return;
2190
+ }
2191
+ for (const conn of connections) {
2192
+ const mcpServer = serverByConnectionId.get(conn.id);
2193
+ const status = mcpServer?.active ? '✅ active' : '⏸️ inactive';
2194
+ const serverName = mcpServer?.name || 'Not found';
2195
+ console.log(` Connection: ${conn.id}`);
2196
+ console.log(` Integration: ${conn.integrationName || conn.integrationType}`);
2197
+ console.log(` MCP Server: ${serverName}`);
2198
+ console.log(` Status: ${status}`);
2199
+ console.log("─".repeat(80));
2200
+ }
2201
+ console.log(`\nTotal: ${connections.length} connection(s)`);
2202
+ console.log("\n💡 Use --connection <id> with 'activate' or 'deactivate' commands.\n");
2203
+ }
2204
+ catch (error) {
2205
+ writeError(`❌ Error: ${error.message}`);
2206
+ }
2207
+ }
2208
+ /**
2209
+ * Activate MCP server for a connection (non-interactive)
2210
+ */
2211
+ async function mcpActivateFlow(context, connectionId) {
2212
+ writeProgress("🔄 Finding MCP server for connection...");
2213
+ try {
2214
+ const mcpServers = await fetchServersCore(context);
2215
+ if (!mcpServers)
2216
+ return;
2217
+ const mcpServer = mcpServers.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${connectionId}`));
2218
+ if (!mcpServer) {
2219
+ writeError(`❌ No MCP server found for connection: ${connectionId}`);
2220
+ console.log("\n💡 Make sure the connection exists. Use 'lua integrations list' to check.\n");
2221
+ return;
2222
+ }
2223
+ await activateServerCore(context, mcpServer);
2224
+ }
2225
+ catch (error) {
2226
+ writeError(`❌ Error: ${error.message}`);
2227
+ }
2228
+ }
2229
+ /**
2230
+ * Deactivate MCP server for a connection (non-interactive)
2231
+ */
2232
+ async function mcpDeactivateFlow(context, connectionId) {
2233
+ writeProgress("🔄 Finding MCP server for connection...");
2234
+ try {
2235
+ const mcpServers = await fetchServersCore(context);
2236
+ if (!mcpServers)
2237
+ return;
2238
+ const mcpServer = mcpServers.find(s => s.source === 'unifiedto' && s.url?.includes(`connection=${connectionId}`));
2239
+ if (!mcpServer) {
2240
+ writeError(`❌ No MCP server found for connection: ${connectionId}`);
2241
+ console.log("\n💡 Make sure the connection exists. Use 'lua integrations list' to check.\n");
2242
+ return;
2243
+ }
2244
+ await deactivateServerCore(context, mcpServer);
2245
+ }
2246
+ catch (error) {
2247
+ writeError(`❌ Error: ${error.message}`);
2248
+ }
2249
+ }
2250
+ /**
2251
+ * Interactive flow for activating MCP server
2252
+ */
2253
+ async function mcpActivateInteractive(context) {
2254
+ writeProgress("🔄 Loading connections...");
2255
+ try {
2256
+ const [connectionsResult, mcpServers] = await Promise.all([
2257
+ context.unifiedToApi.getConnections(context.agentId),
2258
+ fetchServersCore(context)
2259
+ ]);
2260
+ if (!connectionsResult.success) {
2261
+ writeError(`❌ Failed to load connections: ${connectionsResult.error?.message}`);
2262
+ return;
2263
+ }
2264
+ if (!mcpServers)
2265
+ return;
2266
+ const connections = connectionsResult.data || [];
2267
+ // Map connection IDs to MCP servers
2268
+ const serverByConnectionId = new Map();
2269
+ for (const server of mcpServers) {
2270
+ if (server.source === 'unifiedto') {
2271
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
2272
+ if (connectionMatch) {
2273
+ serverByConnectionId.set(connectionMatch[1], server);
2274
+ }
2275
+ }
2276
+ }
2277
+ // Filter to show only inactive MCP servers
2278
+ const inactiveConnections = connections.filter(conn => {
2279
+ const server = serverByConnectionId.get(conn.id);
2280
+ return server && !server.active;
2281
+ });
2282
+ if (inactiveConnections.length === 0) {
2283
+ writeInfo("ℹ️ All MCP servers are already active (or no connections found).\n");
2284
+ return;
2285
+ }
2286
+ const connectionAnswer = await safePrompt([
2287
+ {
2288
+ type: 'list',
2289
+ name: 'connectionId',
2290
+ message: 'Select a connection to activate MCP:',
2291
+ choices: inactiveConnections.map(conn => ({
2292
+ name: `${conn.integrationName || conn.integrationType} (${conn.id.substring(0, 12)}...)`,
2293
+ value: conn.id
2294
+ }))
2295
+ }
2296
+ ]);
2297
+ if (!connectionAnswer)
2298
+ return;
2299
+ await mcpActivateFlow(context, connectionAnswer.connectionId);
2300
+ }
2301
+ catch (error) {
2302
+ writeError(`❌ Error: ${error.message}`);
2303
+ }
2304
+ }
2305
+ /**
2306
+ * Interactive flow for deactivating MCP server
2307
+ */
2308
+ async function mcpDeactivateInteractive(context) {
2309
+ writeProgress("🔄 Loading connections...");
2310
+ try {
2311
+ const [connectionsResult, mcpServers] = await Promise.all([
2312
+ context.unifiedToApi.getConnections(context.agentId),
2313
+ fetchServersCore(context)
2314
+ ]);
2315
+ if (!connectionsResult.success) {
2316
+ writeError(`❌ Failed to load connections: ${connectionsResult.error?.message}`);
2317
+ return;
2318
+ }
2319
+ if (!mcpServers)
2320
+ return;
2321
+ const connections = connectionsResult.data || [];
2322
+ // Map connection IDs to MCP servers
2323
+ const serverByConnectionId = new Map();
2324
+ for (const server of mcpServers) {
2325
+ if (server.source === 'unifiedto') {
2326
+ const connectionMatch = server.url?.match(/connection=([a-f0-9]+)/i);
2327
+ if (connectionMatch) {
2328
+ serverByConnectionId.set(connectionMatch[1], server);
2329
+ }
2330
+ }
2331
+ }
2332
+ // Filter to show only active MCP servers
2333
+ const activeConnections = connections.filter(conn => {
2334
+ const server = serverByConnectionId.get(conn.id);
2335
+ return server && server.active;
2336
+ });
2337
+ if (activeConnections.length === 0) {
2338
+ writeInfo("ℹ️ No active MCP servers found to deactivate.\n");
2339
+ return;
2340
+ }
2341
+ const connectionAnswer = await safePrompt([
2342
+ {
2343
+ type: 'list',
2344
+ name: 'connectionId',
2345
+ message: 'Select a connection to deactivate MCP:',
2346
+ choices: activeConnections.map(conn => ({
2347
+ name: `${conn.integrationName || conn.integrationType} (${conn.id.substring(0, 12)}...)`,
2348
+ value: conn.id
2349
+ }))
2350
+ }
2351
+ ]);
2352
+ if (!connectionAnswer)
2353
+ return;
2354
+ await mcpDeactivateFlow(context, connectionAnswer.connectionId);
2355
+ }
2356
+ catch (error) {
2357
+ writeError(`❌ Error: ${error.message}`);
2358
+ }
2359
+ }
2360
+ // ─────────────────────────────────────────────────────────────────────────────
2361
+ // Help
2362
+ // ─────────────────────────────────────────────────────────────────────────────
2363
+ function showUsage() {
2364
+ console.log('\nUsage:');
2365
+ console.log(' lua integrations Interactive integration management');
2366
+ console.log(' lua integrations connect Connect a new integration (interactive)');
2367
+ console.log(' lua integrations connect --integration <type> Connect a specific integration');
2368
+ console.log(' lua integrations connect --integration <type> --triggers <events> Connect with triggers');
2369
+ console.log(' lua integrations update Update connection scopes (interactive)');
2370
+ console.log(' lua integrations update --integration <type> Update scopes for a specific integration');
2371
+ console.log(' lua integrations list List connected integrations');
2372
+ console.log(' lua integrations available List available integrations');
2373
+ console.log(' lua integrations info <type> Show integration details (scopes, triggers)');
2374
+ console.log(' lua integrations info <type> --json Output as JSON for scripting');
2375
+ console.log(' lua integrations disconnect --connection-id <id> Disconnect an integration');
2376
+ console.log('\nTriggers:');
2377
+ console.log(' lua integrations webhooks list List all triggers');
2378
+ console.log(' lua integrations webhooks events --connection <id> List available events for a connection');
2379
+ console.log(' lua integrations webhooks events --integration <type> List available events for an integration');
2380
+ console.log(' lua integrations webhooks create Create trigger (interactive)');
2381
+ console.log(' lua integrations webhooks create --connection <id> --object <type> --event <event> --hook-url <url>');
2382
+ console.log(' lua integrations webhooks delete --webhook-id <id> Delete a trigger');
2383
+ console.log('\nMCP Server Management:');
2384
+ console.log(' lua integrations mcp list List connections with MCP status');
2385
+ console.log(' lua integrations mcp activate --connection <id> Activate MCP server');
2386
+ console.log(' lua integrations mcp deactivate --connection <id> Deactivate MCP server');
2387
+ console.log('\nTrigger Options (use with connect):');
2388
+ console.log(' --triggers <events> Comma-separated triggers (e.g., task_task.created,task_task.updated)');
2389
+ console.log(' --custom-webhook Use custom URL instead of agent trigger');
2390
+ console.log(' --hook-url <url> Custom URL for triggers');
2391
+ }
2392
+ //# sourceMappingURL=integrations.js.map