aui-agent-builder 0.4.6 → 0.4.7

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 (53) hide show
  1. package/dist/api-client/apollo-client.d.ts +102 -5
  2. package/dist/api-client/apollo-client.d.ts.map +1 -1
  3. package/dist/api-client/apollo-client.js +136 -5
  4. package/dist/api-client/apollo-client.js.map +1 -1
  5. package/dist/commands/import-agent.d.ts +2 -5
  6. package/dist/commands/import-agent.d.ts.map +1 -1
  7. package/dist/commands/import-agent.js +1 -1
  8. package/dist/commands/import-agent.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +10 -3
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/integration-mcp-test.d.ts +0 -2
  13. package/dist/commands/integration-mcp-test.d.ts.map +1 -1
  14. package/dist/commands/integration-mcp-test.js +41 -135
  15. package/dist/commands/integration-mcp-test.js.map +1 -1
  16. package/dist/commands/integration-mcp-url.d.ts +6 -7
  17. package/dist/commands/integration-mcp-url.d.ts.map +1 -1
  18. package/dist/commands/integration-mcp-url.js +10 -29
  19. package/dist/commands/integration-mcp-url.js.map +1 -1
  20. package/dist/commands/integration-toolkits.d.ts +4 -11
  21. package/dist/commands/integration-toolkits.d.ts.map +1 -1
  22. package/dist/commands/integration-toolkits.js +6 -28
  23. package/dist/commands/integration-toolkits.js.map +1 -1
  24. package/dist/commands/integration-tools.d.ts +5 -15
  25. package/dist/commands/integration-tools.d.ts.map +1 -1
  26. package/dist/commands/integration-tools.js +17 -63
  27. package/dist/commands/integration-tools.js.map +1 -1
  28. package/dist/commands/integration.d.ts +0 -1
  29. package/dist/commands/integration.d.ts.map +1 -1
  30. package/dist/commands/integration.js +62 -168
  31. package/dist/commands/integration.js.map +1 -1
  32. package/dist/commands/pull-agent.d.ts +2 -5
  33. package/dist/commands/pull-agent.d.ts.map +1 -1
  34. package/dist/commands/pull-agent.js +1 -1
  35. package/dist/commands/pull-agent.js.map +1 -1
  36. package/dist/errors/index.d.ts +9 -0
  37. package/dist/errors/index.d.ts.map +1 -1
  38. package/dist/errors/index.js +20 -9
  39. package/dist/errors/index.js.map +1 -1
  40. package/dist/index.js +21 -31
  41. package/dist/index.js.map +1 -1
  42. package/dist/services/integration.service.d.ts +73 -148
  43. package/dist/services/integration.service.d.ts.map +1 -1
  44. package/dist/services/integration.service.js +400 -559
  45. package/dist/services/integration.service.js.map +1 -1
  46. package/dist/services/pull-schema.service.d.ts +4 -5
  47. package/dist/services/pull-schema.service.d.ts.map +1 -1
  48. package/dist/services/pull-schema.service.js +12 -10
  49. package/dist/services/pull-schema.service.js.map +1 -1
  50. package/dist/ui/components/ErrorDisplay.d.ts.map +1 -1
  51. package/dist/ui/components/ErrorDisplay.js +16 -4
  52. package/dist/ui/components/ErrorDisplay.js.map +1 -1
  53. package/package.json +1 -2
@@ -1,10 +1,13 @@
1
1
  import fetch from "node-fetch";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import * as path from "path";
2
4
  import { randomUUID } from "crypto";
3
- import { AuthConfigTypes } from "@composio/core";
4
- import { getConfig, loadProjectConfig, getBaseUrl, getAgentSettingsWriteUrl, } from "../config/index.js";
5
- import { AUIClient } from "../api-client/index.js";
5
+ import { getConfig, loadProjectConfig, getBaseUrl, getAgentSettingsWriteUrl, findProjectRoot, } from "../config/index.js";
6
+ import { AUIAPIError, AUIClient } from "../api-client/index.js";
7
+ import { detectAgentBundleMode, isModeMismatchError, } from "../commands/util/agent-mode.js";
8
+ import { parseAuiFile, writeJsonFile } from "../utils/index.js";
6
9
  import { ApolloClient, } from "../api-client/apollo-client.js";
7
- import { AuthenticationError, ValidationError } from "../errors/index.js";
10
+ import { AuthenticationError, CLIError, ValidationError, toCLIError, } from "../errors/index.js";
8
11
  import { getValidSession } from "./auth.service.js";
9
12
  import { captureRequest } from "../utils/request-capture.js";
10
13
  // ─────────────────────────────────────────────────────────────────────────────
@@ -109,6 +112,31 @@ export function buildAuthFromFlags(opts) {
109
112
  ...(opts.authHeaderName ? { header_name: opts.authHeaderName } : {}),
110
113
  };
111
114
  }
115
+ /** Load BYO Composio OAuth credentials from a JSON file (`client_id`, etc.). */
116
+ export function loadComposioCredentialsFile(filePath) {
117
+ let raw;
118
+ try {
119
+ raw = JSON.parse(readFileSync(filePath, "utf-8"));
120
+ }
121
+ catch (err) {
122
+ throw new Error(`Cannot read credentials file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
123
+ }
124
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
125
+ throw new Error(`Credentials file '${filePath}' must be a JSON object.`);
126
+ }
127
+ const obj = raw;
128
+ const creds = {};
129
+ if (obj.client_id)
130
+ creds.clientId = String(obj.client_id);
131
+ if (obj.client_secret)
132
+ creds.clientSecret = String(obj.client_secret);
133
+ if (obj.bearer_token)
134
+ creds.bearerToken = String(obj.bearer_token);
135
+ if (!creds.clientId && !creds.clientSecret && !creds.bearerToken) {
136
+ throw new Error(`Credentials file '${filePath}' must contain at least one of: client_id, client_secret, bearer_token.`);
137
+ }
138
+ return creds;
139
+ }
112
140
  // ─── Session ───
113
141
  export async function getAuthenticatedSession() {
114
142
  const config = getConfig();
@@ -123,6 +151,79 @@ export async function getAuthenticatedSession() {
123
151
  }
124
152
  return session;
125
153
  }
154
+ const COMPOSIO_AUTH_BODY_PATTERNS = [
155
+ /unauthorized/i,
156
+ /unauthenticated/i,
157
+ /not authenticated/i,
158
+ /authentication required/i,
159
+ /invalid.*token/i,
160
+ /session.*expired/i,
161
+ /auth.*required/i,
162
+ ];
163
+ function composioAuthDetail(body) {
164
+ if (body == null)
165
+ return undefined;
166
+ if (typeof body === "string")
167
+ return body.trim() || undefined;
168
+ if (typeof body === "object" && "detail" in body) {
169
+ const detail = body.detail;
170
+ if (typeof detail === "string")
171
+ return detail.trim() || undefined;
172
+ if (Array.isArray(detail)) {
173
+ return detail
174
+ .map((item) => typeof item === "object" && item !== null && "msg" in item
175
+ ? String(item.msg)
176
+ : String(item))
177
+ .join("; ");
178
+ }
179
+ }
180
+ const text = JSON.stringify(body);
181
+ return COMPOSIO_AUTH_BODY_PATTERNS.some((re) => re.test(text))
182
+ ? text
183
+ : undefined;
184
+ }
185
+ function bodyLooksLikeAuthFailure(body) {
186
+ const detail = composioAuthDetail(body);
187
+ return detail != null && COMPOSIO_AUTH_BODY_PATTERNS.some((re) => re.test(detail));
188
+ }
189
+ /**
190
+ * Turn a Composio/Apollo MCP API failure into an actionable CLI error.
191
+ * Auth failures surface as AuthenticationError with login guidance instead
192
+ * of a generic "failed to fetch" message.
193
+ */
194
+ export function composioApiError(error) {
195
+ if (error instanceof AUIAPIError && bodyLooksLikeAuthFailure(error.body)) {
196
+ return new AuthenticationError(composioAuthDetail(error.body) ?? `Authentication failed (${error.status}).`, { cause: error });
197
+ }
198
+ const classified = toCLIError(error);
199
+ if (classified instanceof AuthenticationError) {
200
+ return classified;
201
+ }
202
+ if (classified.code !== "UNKNOWN_ERROR") {
203
+ return classified;
204
+ }
205
+ return new CLIError(classified.message, {
206
+ code: classified.code,
207
+ suggestion: classified.suggestion ??
208
+ "Verify your `aui login` session is still valid and that Composio is configured on the backend.",
209
+ cause: error,
210
+ });
211
+ }
212
+ /** Short spinner label for a Composio API failure. */
213
+ export function composioFailureLabel(error, defaultLabel) {
214
+ return composioApiError(error) instanceof AuthenticationError
215
+ ? "Not authenticated"
216
+ : defaultLabel;
217
+ }
218
+ /** JSON envelope fields for a Composio API failure. */
219
+ export function composioFailureJson(error) {
220
+ const cliErr = composioApiError(error);
221
+ return {
222
+ code: cliErr.code,
223
+ message: cliErr.message,
224
+ ...(cliErr.suggestion ? { suggestion: cliErr.suggestion } : {}),
225
+ };
226
+ }
126
227
  // ─── Scope Resolution ───
127
228
  export function resolveScopeIds(session, overrides) {
128
229
  const projectConfig = loadProjectConfig();
@@ -167,13 +268,7 @@ export function assertComposioUserIdMatchesNetworkId(composioUserId, networkId)
167
268
  export async function resolveNetworkCategoryId(session, networkId, scope) {
168
269
  if (scope.networkCategoryId)
169
270
  return scope.networkCategoryId;
170
- const client = new AUIClient({
171
- baseUrl: session.api_url || getBaseUrl(session.environment),
172
- authToken: session.auth_token,
173
- accountId: scope.accountId,
174
- organizationId: scope.organizationId,
175
- environment: session.environment,
176
- });
271
+ const client = buildAUIClient(session, scope);
177
272
  try {
178
273
  const resp = await client.networks.get(networkId);
179
274
  const network = resp.data;
@@ -320,6 +415,101 @@ function extractTools(parsed) {
320
415
  return [];
321
416
  }
322
417
  // ─── Save ───
418
+ const BUNDLE_MODE_LOCAL_SUGGESTION = "Integration saved to integrations.aui.json. Run `aui push` to upload it to the server.";
419
+ const NO_PROJECT_ERROR = "Run this command inside an imported agent project (with .auirc).";
420
+ const NO_PROJECT_SUGGESTION = "Run `aui import` or `cd` into your agent directory, then re-run `aui integration create`.";
421
+ function integrationCodeFromName(name) {
422
+ return name
423
+ .toLowerCase()
424
+ .replace(/[^a-z0-9]+/g, "-")
425
+ .replace(/^-|-$/g, "");
426
+ }
427
+ function buildLocalMCPIntegrationRecord(request, code) {
428
+ return {
429
+ type: "MCP",
430
+ code,
431
+ name: request.display_name,
432
+ description: request.description,
433
+ settings: buildMCPSettings(request),
434
+ };
435
+ }
436
+ function mergeIntegrationIntoLocalFile(projectRoot, integration) {
437
+ const filePath = path.join(projectRoot, "integrations.aui.json");
438
+ let integrations = [];
439
+ if (existsSync(filePath)) {
440
+ const parsed = parseAuiFile(filePath);
441
+ const existing = parsed?.integrations;
442
+ if (Array.isArray(existing)) {
443
+ integrations = existing.filter((item) => !!item && typeof item === "object" && !Array.isArray(item));
444
+ }
445
+ }
446
+ const code = String(integration.code);
447
+ const idx = integrations.findIndex((item) => String(item.code) === code);
448
+ if (idx >= 0) {
449
+ integrations[idx] = integration;
450
+ }
451
+ else {
452
+ integrations.push(integration);
453
+ }
454
+ writeJsonFile(filePath, { integrations });
455
+ return filePath;
456
+ }
457
+ /** Agent-settings / agent-management client — same session+scope wiring as {@link buildApolloClient}. */
458
+ export function buildAUIClient(session, scope) {
459
+ return new AUIClient({
460
+ baseUrl: session.api_url || getBaseUrl(session.environment),
461
+ authToken: session.auth_token,
462
+ accountId: scope.accountId,
463
+ organizationId: scope.organizationId,
464
+ environment: session.environment,
465
+ });
466
+ }
467
+ async function resolveAgentManagementIdForScope(session, scope) {
468
+ if (scope.agentId)
469
+ return scope.agentId;
470
+ if (!scope.networkId || !scope.organizationId)
471
+ return undefined;
472
+ const client = buildAUIClient(session, scope);
473
+ try {
474
+ const resp = await client.agentManagement.listAgents(scope.organizationId, 1, 50, { network_id: scope.networkId });
475
+ const match = resp.items.find((agent) => agent.scope.network_id === scope.networkId || agent.id === scope.networkId);
476
+ return match?.id;
477
+ }
478
+ catch {
479
+ return undefined;
480
+ }
481
+ }
482
+ async function resolveIntegrationAgentMode(session, scope) {
483
+ const override = process.env.AUI_FORCE_AGENT_MODE;
484
+ if (override === "bundle" || override === "records") {
485
+ return override;
486
+ }
487
+ const agentMgmtId = await resolveAgentManagementIdForScope(session, scope);
488
+ if (!agentMgmtId) {
489
+ return "records";
490
+ }
491
+ const resolution = await detectAgentBundleMode(buildAUIClient(session, scope), agentMgmtId);
492
+ return resolution.mode;
493
+ }
494
+ function persistIntegrationLocally(request) {
495
+ const projectRoot = findProjectRoot();
496
+ if (!projectRoot) {
497
+ return {
498
+ success: false,
499
+ error: NO_PROJECT_ERROR,
500
+ suggestion: NO_PROJECT_SUGGESTION,
501
+ };
502
+ }
503
+ const code = integrationCodeFromName(request.name);
504
+ const integration = buildLocalMCPIntegrationRecord(request, code);
505
+ const filePath = mergeIntegrationIntoLocalFile(projectRoot, integration);
506
+ return {
507
+ ok: true,
508
+ code,
509
+ file: path.relative(projectRoot, filePath),
510
+ integration,
511
+ };
512
+ }
323
513
  /**
324
514
  * Build the `IntegrationMCPSettings` payload posted to
325
515
  * `/v1/integrations/view`. Switches on the discriminated union's `type`
@@ -368,13 +558,29 @@ function buildMCPSettings(request) {
368
558
  }
369
559
  }
370
560
  export async function saveIntegration(session, request, scope) {
561
+ const agentMode = await resolveIntegrationAgentMode(session, scope);
562
+ const localPersist = persistIntegrationLocally(request);
563
+ if (!("ok" in localPersist)) {
564
+ return { ...localPersist, agentMode };
565
+ }
566
+ const localData = {
567
+ code: localPersist.code,
568
+ file: localPersist.file,
569
+ integration: localPersist.integration,
570
+ };
571
+ if (agentMode === "bundle") {
572
+ return {
573
+ success: true,
574
+ persistedVia: "local",
575
+ agentMode: "bundle",
576
+ suggestion: BUNDLE_MODE_LOCAL_SUGGESTION,
577
+ data: localData,
578
+ };
579
+ }
371
580
  const agentSettingsBase = getAgentSettingsWriteUrl(session.environment);
372
581
  const url = `${agentSettingsBase}/v1/integrations/view`;
373
582
  const categoryId = await resolveNetworkCategoryId(session, scope.networkId, scope);
374
- const code = request.name
375
- .toLowerCase()
376
- .replace(/[^a-z0-9]+/g, "-")
377
- .replace(/^-|-$/g, "");
583
+ const code = localPersist.code;
378
584
  // MCP integration settings — mirrors the backend variants
379
585
  // (`agent_settings_transcoder/schemas/backend/integration.py`).
380
586
  // `settings.type` discriminates between the two FLAT variants below;
@@ -439,9 +645,19 @@ export async function saveIntegration(session, request, scope) {
439
645
  success: resp.ok,
440
646
  });
441
647
  if (!resp.ok) {
648
+ if (resp.status === 422 && isModeMismatchError(responseText)) {
649
+ return {
650
+ success: true,
651
+ persistedVia: "local",
652
+ agentMode: "bundle",
653
+ suggestion: BUNDLE_MODE_LOCAL_SUGGESTION,
654
+ data: localData,
655
+ };
656
+ }
442
657
  return {
443
658
  success: false,
444
659
  error: `Failed to create integration (${resp.status}): ${responseText}`,
660
+ agentMode: "records",
445
661
  };
446
662
  }
447
663
  let parsed;
@@ -451,171 +667,95 @@ export async function saveIntegration(session, request, scope) {
451
667
  catch {
452
668
  parsed = responseText;
453
669
  }
454
- return { success: true, data: parsed };
670
+ return {
671
+ success: true,
672
+ data: { ...localData, server: parsed },
673
+ persistedVia: "local_and_server",
674
+ agentMode: "records",
675
+ };
455
676
  }
456
- // ─── Composio Native Integration ───
457
- let composioInstance = null;
458
- let composioInstanceKey = "";
459
- async function getComposioClient(apiKey) {
460
- if (!composioInstance || composioInstanceKey !== apiKey) {
461
- const { Composio } = await import("@composio/core");
462
- composioInstance = new Composio({ apiKey });
463
- composioInstanceKey = apiKey;
464
- }
465
- return composioInstance;
677
+ // ─── Composio Native Integration (via Apollo MCP API) ───
678
+ //
679
+ // Everything below is a thin orchestration layer on top of
680
+ // `/v1/integrations/mcp/composio/*` (see mcp-api.md). The CLI never
681
+ // holds a Composio API key — gateway auth (`Authorization: Bearer
682
+ // <session>`) is sufficient because Apollo owns the upstream Composio
683
+ // credential.
684
+ /** Build an Apollo client from a session + scope (used by Composio helpers and mcp-test). */
685
+ export function buildApolloClient(session, scope) {
686
+ return new ApolloClient({
687
+ authToken: session.auth_token,
688
+ organizationId: scope.organizationId,
689
+ accountId: scope.accountId,
690
+ userId: scope.userId,
691
+ environment: session.environment,
692
+ });
466
693
  }
467
- export function resetComposioClient() {
468
- composioInstance = null;
469
- composioInstanceKey = "";
694
+ // ─── Toolkit listing ───
695
+ export async function listComposioToolkits(session, scope, options = {}) {
696
+ const client = buildApolloClient(session, scope);
697
+ const page = await client.listComposioToolkits({
698
+ query: options.search,
699
+ limit: options.limit ?? 50,
700
+ cursor: options.cursor,
701
+ });
702
+ return {
703
+ items: page.items.map(toToolkitInfo),
704
+ nextCursor: page.next_cursor ?? undefined,
705
+ };
470
706
  }
471
707
  /**
472
- * Fetch the Composio API key from the AUI backend.
473
- * The key is returned Base64-encoded and decoded here in the CLI.
708
+ * Fetch Composio tool catalog metadata by slug via Apollo
709
+ * (`GET /v1/integrations/mcp/composio/tools/by-slug`). No toolkit
710
+ * connection or `composio_user_id` required.
474
711
  */
475
- export async function fetchComposioApiKey(session) {
476
- const baseUrl = session.api_url || getBaseUrl(session.environment);
477
- const configUrl = `${baseUrl}/third-party-auth/integrations/composio-config`;
478
- try {
479
- if (process.env.AUI_DEBUG) {
480
- console.error(`[debug] Fetching integration config from: ${configUrl}`);
481
- }
482
- const resp = await fetch(configUrl, {
483
- method: "GET",
484
- headers: {
485
- "Content-Type": "application/json",
486
- "auth-token": session.auth_token,
487
- },
488
- });
489
- if (process.env.AUI_DEBUG) {
490
- console.error(`[debug] Integration config response: ${resp.status}`);
491
- }
492
- if (!resp.ok)
493
- return null;
494
- const body = await resp.json();
495
- const encodedKey = body?.data?.apiKey;
496
- if (process.env.AUI_DEBUG) {
497
- console.error(`[debug] Integration config: key present=${!!encodedKey}, label=${body?.data?.label || "none"}`);
498
- }
499
- if (typeof encodedKey === "string" && encodedKey.length > 0) {
500
- try {
501
- return Buffer.from(encodedKey, "base64").toString("utf-8");
502
- }
503
- catch {
504
- return encodedKey;
505
- }
506
- }
507
- return null;
508
- }
509
- catch {
510
- return null;
511
- }
512
- }
513
- export async function listComposioToolkits(apiKey, options) {
514
- const limit = options?.limit || 50;
515
- const result = await fetchToolkitsViaREST(apiKey, limit, options?.search, options?.cursor);
516
- const items = result.items.map((tk) => ({
517
- slug: tk.slug || "",
518
- name: tk.name || tk.slug || "",
519
- logo: tk.meta?.logo || tk.logo || "",
520
- description: tk.meta?.description || tk.description || "",
521
- toolsCount: tk.meta?.toolsCount ?? tk.meta?.tools_count ?? tk.toolsCount ?? tk.tools_count ?? 0,
522
- authSchemes: tk.authSchemes || tk.auth_schemes || [],
523
- isNoAuth: tk.noAuth ?? tk.no_auth ?? false,
712
+ export async function fetchComposioToolsBySlugs(session, scope, slugs) {
713
+ const client = buildApolloClient(session, scope);
714
+ const tools = await client.listComposioToolsBySlug({ slugs });
715
+ return tools.map((t, i) => ({
716
+ slug: String(t.slug ?? slugs[i] ?? ""),
717
+ name: String(t.name ?? ""),
718
+ description: t.description ? String(t.description) : undefined,
719
+ input_schema: (t.input_schema ?? t.inputSchema),
524
720
  }));
721
+ }
722
+ /** Project a raw Composio toolkit record onto the CLI's narrow view. */
723
+ function toToolkitInfo(tk) {
724
+ const meta = tk.meta ?? {};
725
+ const authConfigDetails = tk.auth_config_details ?? [];
525
726
  return {
526
- items,
527
- nextCursor: result.nextCursor,
528
- totalPages: result.totalPages ?? 1,
727
+ slug: String(tk.slug ?? ""),
728
+ name: String(tk.name ?? tk.slug ?? ""),
729
+ logo: meta.logo ?? tk.logo,
730
+ description: meta.description ??
731
+ tk.description,
732
+ toolsCount: meta.tools_count ??
733
+ tk.tools_count,
734
+ authSchemes: authConfigDetails
735
+ .map((d) => d.mode)
736
+ .filter((m) => Boolean(m)),
737
+ isNoAuth: tk.no_auth ??
738
+ authConfigDetails.every((d) => d.mode === "NO_AUTH"),
529
739
  };
530
740
  }
531
- async function fetchToolkitsViaREST(apiKey, limit = 50, search, cursor) {
532
- let url = `https://backend.composio.dev/api/v3.1/toolkits?sort_by=usage&limit=${limit}`;
533
- if (search)
534
- url += `&search=${encodeURIComponent(search)}`;
535
- if (cursor)
536
- url += `&cursor=${encodeURIComponent(cursor)}`;
537
- for (const headerName of ["x-api-key", "x-user-api-key"]) {
538
- try {
539
- const resp = await fetch(url, {
540
- method: "GET",
541
- headers: {
542
- [headerName]: apiKey,
543
- "Accept": "application/json",
544
- },
545
- });
546
- if (process.env.AUI_DEBUG) {
547
- console.error(`[debug] REST toolkits fetch with ${headerName}: status ${resp.status}`);
548
- }
549
- if (!resp.ok) {
550
- if (process.env.AUI_DEBUG) {
551
- const text = await resp.text();
552
- console.error(`[debug] REST toolkits fetch failed (${resp.status}): ${text.slice(0, 200)}`);
553
- }
554
- continue;
555
- }
556
- const data = await resp.json();
557
- if (process.env.AUI_DEBUG) {
558
- console.error(`[debug] REST toolkits response keys: ${Object.keys(data).join(", ")}`);
559
- console.error(`[debug] REST toolkits next_cursor: ${data.next_cursor ?? "none"}, total_items: ${data.total_items ?? "unknown"}, total_pages: ${data.total_pages ?? "unknown"}`);
560
- }
561
- const items = extractToolkitItems(data);
562
- return {
563
- items,
564
- nextCursor: data.next_cursor || data.nextCursor || undefined,
565
- totalPages: data.total_pages ?? data.totalPages ?? undefined,
566
- totalItems: data.total_items ?? data.totalItems ?? undefined,
567
- };
568
- }
569
- catch (e) {
570
- if (process.env.AUI_DEBUG) {
571
- console.error(`[debug] REST toolkits fetch error (${headerName}): ${e instanceof Error ? e.message : String(e)}`);
572
- }
573
- }
574
- }
575
- throw new Error("Failed to fetch toolkits. Check your Composio API key.");
576
- }
577
- function extractToolkitItems(resp) {
578
- if (!resp)
579
- return [];
580
- if (Array.isArray(resp))
581
- return resp;
582
- if (Array.isArray(resp.items))
583
- return resp.items;
584
- if (Array.isArray(resp.data))
585
- return resp.data;
586
- if (resp.items && typeof resp.items === "object" && !Array.isArray(resp.items)) {
587
- const vals = Object.values(resp.items);
588
- if (vals.length > 0 && Array.isArray(vals[0]))
589
- return vals[0];
590
- }
591
- for (const key of Object.keys(resp)) {
592
- const val = resp[key];
593
- if (Array.isArray(val) && val.length > 0 && val[0]?.slug) {
594
- return val;
595
- }
596
- }
597
- return [];
598
- }
599
- // ─── Composio: Toolkit Auth Metadata ───
741
+ // ─── Toolkit auth metadata ───
600
742
  const _OAUTH_SCHEMES = new Set(["OAUTH2", "OAUTH1", "DCR_OAUTH", "S2S_OAUTH2"]);
601
743
  const _CREDENTIAL_SCHEMES = new Set(["API_KEY", "BEARER_TOKEN"]);
602
744
  /**
603
- * Fetches Composio toolkit metadata and returns a summary of the auth requirements
604
- * so the caller can decide whether to prompt the user for BYO credentials.
745
+ * Look up a single toolkit by slug (`/composio/toolkits?query=<slug>`)
746
+ * and surface the auth-scheme picker fields the CLI needs.
605
747
  */
606
- export async function getComposioToolkitAuthInfo(apiKey, toolkitSlug) {
607
- const composio = await getComposioClient(apiKey);
608
- const toolkit = await composio.toolkits.get(toolkitSlug);
609
- const configDetails = toolkit.authConfigDetails ?? [];
610
- const managedSchemes = toolkit.composioManagedAuthSchemes ?? [];
611
- if (process.env.AUI_DEBUG) {
612
- console.error(`[debug] toolkit raw keys: ${Object.keys(toolkit ?? {}).join(", ")}`);
613
- console.error(`[debug] authConfigDetails: ${JSON.stringify(configDetails)}`);
614
- console.error(`[debug] composioManagedAuthSchemes: ${JSON.stringify(managedSchemes)}`);
748
+ export async function getComposioToolkitAuthInfo(session, scope, toolkitSlug) {
749
+ const client = buildApolloClient(session, scope);
750
+ const page = await client.listComposioToolkits({ query: toolkitSlug, limit: 1 });
751
+ const toolkit = page.items[0];
752
+ if (!toolkit) {
753
+ throw new Error(`Toolkit '${toolkitSlug}' not found.`);
615
754
  }
616
- // No-auth toolkits expose either no schemes at all, or only the NO_AUTH mode.
617
- const isNoAuth = configDetails.length === 0
618
- || configDetails.every((d) => d.mode === "NO_AUTH");
755
+ const configDetails = toolkit.auth_config_details ?? [];
756
+ const managedSchemes = toolkit.composio_managed_auth_schemes ?? [];
757
+ const isNoAuth = configDetails.length === 0 ||
758
+ configDetails.every((d) => d.mode === "NO_AUTH");
619
759
  const schemes = configDetails
620
760
  .map((d) => d.mode)
621
761
  .filter((mode) => Boolean(mode) && mode !== "NO_AUTH")
@@ -625,48 +765,29 @@ export async function getComposioToolkitAuthInfo(apiKey, toolkitSlug) {
625
765
  isOAuth: _OAUTH_SCHEMES.has(scheme),
626
766
  isCredential: _CREDENTIAL_SCHEMES.has(scheme),
627
767
  }));
628
- const defaultScheme = schemes[0]?.scheme ?? "";
629
- if (process.env.AUI_DEBUG) {
630
- console.error(`[debug] authInfo: defaultScheme=${defaultScheme}, isNoAuth=${isNoAuth}`);
631
- console.error(`[debug] schemes: ${JSON.stringify(schemes)}`);
632
- }
633
- return { schemes, defaultScheme, isNoAuth };
634
- }
635
- // ─── Composio: Account & Auth-Config Helpers ───
636
- export async function getComposioConnectedAccounts(apiKey, userId, toolkitSlug) {
637
- const composio = await getComposioClient(apiKey);
638
- return composio.connectedAccounts.list({ userIds: [userId], toolkitSlugs: [toolkitSlug] });
768
+ return { schemes, defaultScheme: schemes[0]?.scheme ?? "", isNoAuth };
639
769
  }
640
770
  /**
641
- * Pre-flight check for "is this toolkit usable by this user?". Returns a
642
- * status struct so callers can decide how to react (throw, redirect to
643
- * `aui integration create`, or proceed). Does NOT mutate any state.
644
- *
645
- * - NO_AUTH toolkits → `{ isNoAuth: true, isConnected: true }` (nothing to do).
646
- * - Auth-required toolkits → `isConnected` reflects whether the user already
647
- * has an ACTIVE, non-disabled connected account for the toolkit (same
648
- * predicate `resolveComposioToolkitAuth` uses to short-circuit).
649
- *
650
- * This catches the gap that `resolveComposioAuthConfigId` misses: a toolkit
651
- * can have an auth-config provisioned globally (so MCP-server creation
652
- * succeeds) while THIS user has not yet linked their account — in which
653
- * case MCP tool calls would fail at runtime.
771
+ * Pre-flight: is this toolkit usable by this user? Combines toolkit
772
+ * metadata + connected-accounts listing so callers can decide whether
773
+ * to throw, redirect to `aui integration create`, or proceed.
654
774
  */
655
- export async function getComposioToolkitConnectionStatus(apiKey, userId, toolkitSlug) {
656
- const composio = await getComposioClient(apiKey);
657
- const toolkit = await composio.toolkits.get(toolkitSlug);
775
+ export async function getComposioToolkitConnectionStatus(session, scope, composioUserId, toolkitSlug) {
776
+ const client = buildApolloClient(session, scope);
777
+ const page = await client.listComposioToolkits({ query: toolkitSlug, limit: 1 });
778
+ const toolkit = page.items[0];
658
779
  if (!toolkit) {
659
780
  throw new Error(`Toolkit '${toolkitSlug}' not found.`);
660
781
  }
661
- const authMode = toolkit.authConfigDetails?.[0]?.mode;
782
+ const authMode = toolkit.auth_config_details?.[0]?.mode;
662
783
  if (!authMode || authMode === "NO_AUTH") {
663
784
  return { isNoAuth: true, isConnected: true };
664
785
  }
665
- const accounts = await composio.connectedAccounts.list({
666
- userIds: [userId],
667
- toolkitSlugs: [toolkitSlug],
786
+ const accounts = await client.listComposioConnectedAccounts({
787
+ composioUserId,
788
+ toolkit: toolkitSlug,
668
789
  });
669
- const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.isDisabled);
790
+ const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.is_disabled);
670
791
  return {
671
792
  isNoAuth: false,
672
793
  authMode,
@@ -674,385 +795,105 @@ export async function getComposioToolkitConnectionStatus(apiKey, userId, toolkit
674
795
  connectedAccountId: active?.id,
675
796
  };
676
797
  }
798
+ // ─── Authorize + wait ───
677
799
  /**
678
- * Resolves auth for a toolkit:
679
- *
680
- * - Short-circuits when the user already has an ACTIVE connection.
681
- * - **BYOD path** (`authCredentials` or `authConfigId` supplied): creates a
682
- * CUSTOM auth config with the provided credentials, then links the account
683
- * directly via `connectedAccounts.link()`.
684
- * - **Standard OAuth path** (no custom credentials): finds or creates a
685
- * COMPOSIO_MANAGED auth config, then links directly via `connectedAccounts.link()`.
686
- * Session creation is skipped — the MCP URL is generated server-side.
800
+ * Start (or reuse) a toolkit connection. `redirectUrl` is empty when the
801
+ * connection is already ACTIVE — callers should skip the browser flow.
687
802
  */
688
- export async function resolveComposioToolkitAuth(apiKey, userId, toolkitSlug, options = {}) {
689
- const composio = await getComposioClient(apiKey);
690
- const { callbackUrl, authScheme, authCredentials, authConfigId } = options;
691
- const accounts = await composio.connectedAccounts.list({
692
- userIds: [userId],
693
- toolkitSlugs: [toolkitSlug],
694
- });
695
- const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.isDisabled);
696
- if (active) {
697
- return {
698
- id: active.id,
699
- status: active.status,
700
- redirectUrl: null,
701
- waitForConnection: async (_timeout) => active,
702
- };
703
- }
704
- // ── BYOD path: caller supplies their own credentials or an existing config ──
705
- if (authCredentials || authConfigId) {
706
- let resolvedConfigId;
707
- if (authConfigId) {
708
- resolvedConfigId = authConfigId;
709
- }
710
- else {
711
- if (!authScheme) {
712
- throw new Error(`authScheme is required when providing custom credentials for toolkit '${toolkitSlug}'.`);
803
+ export async function connectComposioToolkit(session, scope, composioUserId, toolkitSlug, options = {}) {
804
+ const client = buildApolloClient(session, scope);
805
+ const result = await client.authorizeComposioToolkit({
806
+ composio_user_id: composioUserId,
807
+ toolkit: toolkitSlug,
808
+ ...(options.authScheme ? { auth_scheme: options.authScheme } : {}),
809
+ ...(options.authCredentials
810
+ ? {
811
+ auth_credentials: {
812
+ ...(options.authCredentials.clientId != null && {
813
+ client_id: options.authCredentials.clientId,
814
+ }),
815
+ ...(options.authCredentials.clientSecret != null && {
816
+ client_secret: options.authCredentials.clientSecret,
817
+ }),
818
+ ...(options.authCredentials.bearerToken != null && {
819
+ bearer_token: options.authCredentials.bearerToken,
820
+ }),
821
+ },
713
822
  }
714
- const credentials = {
715
- ...(authCredentials.clientId != null && { client_id: authCredentials.clientId }),
716
- ...(authCredentials.clientSecret != null && { client_secret: authCredentials.clientSecret }),
717
- ...(authCredentials.bearerToken != null && { generic_id: authCredentials.bearerToken }),
718
- };
719
- const created = await composio.authConfigs.create(toolkitSlug, {
720
- type: AuthConfigTypes.CUSTOM,
721
- authScheme,
722
- credentials,
723
- });
724
- resolvedConfigId = created.id;
725
- }
726
- return composio.connectedAccounts.link(userId, resolvedConfigId, callbackUrl != null ? { callbackUrl } : undefined);
727
- }
728
- // ── Standard path: find or create an auth config ──
729
- // Session creation is unnecessary here — the integration stores composio.user_id
730
- // and toolkit slugs; the MCP URL is generated server-side. Using authConfigs +
731
- // connectedAccounts.link() directly avoids any session-creation API limitations.
732
- const existingConfigs = await composio.authConfigs.list({ toolkit: toolkitSlug });
733
- const match = existingConfigs.items.find((c) => c.status === "ENABLED" && (!c.authScheme || !authScheme || c.authScheme === authScheme));
734
- if (process.env.AUI_DEBUG) {
735
- console.error(`[debug] resolveComposioToolkitAuth match: ${JSON.stringify(match)}`);
736
- }
737
- let resolvedConfigId;
738
- if (match) {
739
- resolvedConfigId = match.id;
740
- }
741
- else {
742
- // Determine auth config type from the toolkit's schema:
743
- // use COMPOSIO_MANAGED only when the scheme is Composio-managed,
744
- // otherwise fall back to CUSTOM (e.g. API_KEY or non-managed OAuth schemes).
745
- // Prefer the caller-supplied `isManaged` flag (avoids an extra API call) and
746
- // only fetch the toolkit when the caller didn't provide it.
747
- let isManaged;
748
- if (options.isManaged !== undefined) {
749
- isManaged = options.isManaged;
750
- }
751
- else {
752
- const toolkit = await composio.toolkits.get(toolkitSlug);
753
- const managedSchemes = toolkit.composioManagedAuthSchemes ?? [];
754
- const effectiveScheme = authScheme ?? toolkit.authConfigDetails?.[0]?.mode;
755
- isManaged = effectiveScheme ? managedSchemes.includes(effectiveScheme) : managedSchemes.length > 0;
756
- }
757
- if (process.env.AUI_DEBUG) {
758
- console.error(`[debug] resolveComposioToolkitAuth isManaged=${isManaged}, authScheme=${authScheme}`);
759
- }
760
- let created;
761
- if (isManaged) {
762
- // COMPOSIO_MANAGED: type literal only — authScheme is not part of this schema variant.
763
- created = await composio.authConfigs.create(toolkitSlug, {
764
- type: AuthConfigTypes.COMPOSIO_MANAGED,
765
- });
766
- }
767
- else {
768
- // Non-managed (e.g. API_KEY): create a CUSTOM config with the resolved scheme.
769
- // Credentials were not supplied by the caller — pass an empty object and let
770
- // the Composio API return its own validation error if they are required.
771
- created = await composio.authConfigs.create(toolkitSlug, {
772
- type: AuthConfigTypes.CUSTOM,
773
- authScheme: authScheme,
774
- credentials: {},
775
- });
776
- }
777
- resolvedConfigId = created.id;
778
- }
779
- return composio.connectedAccounts.link(userId, resolvedConfigId, callbackUrl != null ? { callbackUrl } : undefined);
780
- }
781
- /**
782
- * High-level connection helper:
783
- * - For BYOD (custom `authCredentials`): fetches the toolkit's first supported
784
- * auth scheme, then delegates to the BYOD path of `resolveComposioToolkitAuth`.
785
- * - For standard OAuth: delegates directly to `resolveComposioToolkitAuth`
786
- * which uses a COMPOSIO_MANAGED auth config + `connectedAccounts.link()`.
787
- *
788
- * Returns the `ConnectionRequest` alongside the standard `AuthorizationResult`
789
- * fields so callers can invoke `waitForComposioConnection`.
790
- */
791
- export async function connectComposioToolkit(apiKey, userId, toolkitSlug, options = {}) {
792
- let authScheme = options.authScheme;
793
- if (!authScheme && options.authCredentials) {
794
- // Only fetch toolkit metadata when BYO credentials are given without an explicit scheme.
795
- const composio = await getComposioClient(apiKey);
796
- const toolkit = await composio.toolkits.get(toolkitSlug);
797
- const supportedModes = (toolkit.authConfigDetails ?? []).map((d) => d.mode);
798
- if (supportedModes.length === 0) {
799
- throw new Error(`Toolkit '${toolkitSlug}' has no supported auth schemes.`);
800
- }
801
- authScheme = supportedModes[0];
802
- }
803
- const req = await resolveComposioToolkitAuth(apiKey, userId, toolkitSlug, {
804
- callbackUrl: options.callbackUrl,
805
- authCredentials: options.authCredentials,
806
- authScheme,
807
- isManaged: options.isManaged,
823
+ : {}),
808
824
  });
809
825
  return {
810
- id: req.id,
811
- status: req.status ?? "",
812
- redirectUrl: req.redirectUrl ?? "",
813
- connectionRequest: req,
814
- };
815
- }
816
- export async function waitForComposioConnection(authResult, timeout = 120000) {
817
- const connectedAccount = await authResult.connectionRequest.waitForConnection(timeout);
818
- return {
819
- id: connectedAccount.id || "",
820
- status: connectedAccount.status || "active",
826
+ id: result.id,
827
+ status: result.status,
828
+ redirectUrl: result.redirect_url ?? "",
821
829
  };
822
830
  }
823
831
  /**
824
- * Resolve the Composio auth-config id required to attach a toolkit to an
825
- * MCP server. Returns `undefined` for NO_AUTH toolkits. Throws when the
826
- * toolkit requires auth but no auth-configs have been provisioned yet —
827
- * callers should run `connectComposioToolkit` first.
832
+ * Poll `/composio/connected-accounts` until an ACTIVE entry appears for
833
+ * the `(user, toolkit)` pair, or `timeoutMs` elapses.
828
834
  */
829
- async function resolveComposioAuthConfigId(composio, toolkitSlug) {
830
- const toolkit = await composio.toolkits.get(toolkitSlug);
831
- if (!toolkit) {
832
- throw new Error(`Toolkit '${toolkitSlug}' not found.`);
833
- }
834
- const firstMode = toolkit.authConfigDetails?.[0]?.mode;
835
- if (!firstMode || firstMode === "NO_AUTH")
836
- return undefined;
837
- const { items } = await composio.authConfigs.list({ toolkit: toolkitSlug });
838
- const first = items[0];
839
- if (!first) {
840
- throw new Error(`No auth configs provisioned for toolkit '${toolkitSlug}'.`);
841
- }
842
- return first.id;
843
- }
844
- /** Create a new Composio MCP server for a single toolkit and return its id. */
845
- async function createComposioMcpServer(composio, { toolkit, name, allowedTools, authConfigId }) {
846
- const server = await composio.mcp.create(name, {
847
- toolkits: [{ toolkit, ...(authConfigId && { authConfigId }) }],
848
- allowedTools,
849
- });
850
- return server.id;
851
- }
852
- /** True when `existing` matches the desired toolkit + tool-set exactly. */
853
- function isComposioMcpServerInSync(existing, toolkit, allowedTools) {
854
- if (existing.toolkits.length !== 1 || existing.toolkits[0] !== toolkit) {
855
- return false;
835
+ export async function waitForComposioConnection(session, scope, composioUserId, toolkitSlug, timeoutMs = 120_000) {
836
+ const client = buildApolloClient(session, scope);
837
+ const deadline = Date.now() + timeoutMs;
838
+ const intervalMs = 2_000;
839
+ while (Date.now() < deadline) {
840
+ const accounts = await client.listComposioConnectedAccounts({
841
+ composioUserId,
842
+ toolkit: toolkitSlug,
843
+ });
844
+ const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.is_disabled);
845
+ if (active) {
846
+ return {
847
+ id: String(active.id ?? ""),
848
+ status: String(active.status ?? "ACTIVE"),
849
+ };
850
+ }
851
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
856
852
  }
857
- const saved = new Set(existing.allowedTools);
858
- return (saved.size === allowedTools.length &&
859
- allowedTools.every((t) => saved.has(t)));
853
+ throw new Error(`Timed out waiting for Composio connection on toolkit '${toolkitSlug}'.`);
860
854
  }
855
+ // ─── MCP server provisioning ───
861
856
  /**
862
- * Look up the MCP server for `(toolkit, name)` and reuse it when its
863
- * allowed tool-set and toolkit list still match. If either has drifted,
864
- * the server is deleted and recreated in place so the configuration
865
- * stays canonical.
857
+ * Provision (or reuse) a Composio MCP server scoped to `(user, toolkit,
858
+ * allowed_tools)` and return its HTTP URL + caller-stable `serverId`.
859
+ * Backend returns 400 when the toolkit requires auth and no connection
860
+ * exists — callers should `connectComposioToolkit` first.
866
861
  */
867
- export async function getOrCreateComposioMcpServerId(composio, spec) {
868
- const { items } = await composio.mcp.list({
869
- page: 1,
870
- limit: 1,
871
- toolkits: [spec.toolkit],
872
- authConfigs: [],
873
- name: spec.name,
862
+ export async function getComposioServerUrl(session, scope, params) {
863
+ const client = buildApolloClient(session, scope);
864
+ const result = await client.getComposioServerUrl({
865
+ composio_user_id: params.composioUserId,
866
+ toolkit: params.toolkit,
867
+ allowed_tools: params.allowedTools,
868
+ ...(params.serverId ? { server_id: params.serverId } : {}),
874
869
  });
875
- const existing = items[0];
876
- if (!existing)
877
- return createComposioMcpServer(composio, spec);
878
- if (isComposioMcpServerInSync(existing, spec.toolkit, spec.allowedTools)) {
879
- return existing.id;
880
- }
881
- if (process.env.AUI_DEBUG) {
882
- console.error(`[debug] Composio MCP server drifted for id=${existing.id}, recreating`);
883
- }
884
- await composio.mcp.delete(existing.id);
885
- return createComposioMcpServer(composio, spec);
870
+ return { url: result.url, serverId: result.server_id };
886
871
  }
887
872
  /**
888
- * Resolve the Composio MCP HTTP URL for a `(toolkit, user)` pair.
873
+ * List tools exposed by a Composio toolkit for the given user. The user
874
+ * must already be authorised — call `connectComposioToolkit` first.
889
875
  *
890
- * `serverId` is a **caller-stable identifier** used as the MCP server's
891
- * name in Composio pass one in to reuse an existing server across
892
- * runs, or omit it to mint a fresh UUID-prefixed name. The returned
893
- * `serverId` is whatever was used (input or generated), NOT Composio's
894
- * internal config id — callers persist it so subsequent calls resolve
895
- * the same MCP server.
876
+ * Backend returns the same `MCPToolSchema` shape as `/mcp/list`, so
877
+ * `MCPTool.input_schema` (snake_case) is populated for both DIRECT and
878
+ * COMPOSIO paths consistently.
896
879
  */
897
- export async function getComposioServerUrl(apiKey, params) {
898
- const { composioUserId, toolkit, allowedTools } = params;
899
- const serverId = params.serverId ?? randomUUID().slice(0, 30);
900
- const composio = await getComposioClient(apiKey);
901
- const authConfigId = await resolveComposioAuthConfigId(composio, toolkit);
902
- const mcpConfigId = await getOrCreateComposioMcpServerId(composio, {
903
- toolkit,
904
- name: serverId,
905
- allowedTools,
906
- authConfigId,
880
+ export async function discoverComposioTools(session, scope, composioUserId, toolkitSlug) {
881
+ const client = buildApolloClient(session, scope);
882
+ const tools = await client.listComposioTools({
883
+ composioUserId,
884
+ toolkit: toolkitSlug,
907
885
  });
908
- if (process.env.AUI_DEBUG) {
909
- console.error(`[debug] Composio MCP serverId=${serverId} mcpConfigId=${mcpConfigId}`);
910
- }
911
- const instance = await composio.mcp.generate(composioUserId, mcpConfigId);
912
- return { url: instance.url, serverId };
913
- }
914
- /**
915
- * Discover tools for a Composio toolkit using the REST API.
916
- * Supports pagination (cursor) and search (query).
917
- */
918
- export async function discoverComposioTools(apiKey, toolkitSlug, options) {
919
- const limit = options?.limit || 200;
920
- let url = `https://backend.composio.dev/api/v3.1/tools?toolkit_slug=${encodeURIComponent(toolkitSlug)}&limit=${limit}`;
921
- if (options?.query)
922
- url += `&query=${encodeURIComponent(options.query)}`;
923
- if (options?.cursor)
924
- url += `&cursor=${encodeURIComponent(options.cursor)}`;
925
- for (const headerName of ["x-api-key", "x-user-api-key"]) {
926
- const resp = await fetch(url, {
927
- method: "GET",
928
- headers: { [headerName]: apiKey, "Accept": "application/json" },
929
- });
930
- if (!resp.ok)
931
- continue;
932
- const data = await resp.json();
933
- if (process.env.AUI_DEBUG) {
934
- console.error(`[debug] Composio tools response keys: ${Object.keys(data).join(", ")}`);
935
- console.error(`[debug] Composio tools count: ${data.items?.length ?? data.length ?? "unknown"}`);
936
- }
937
- return {
938
- tools: extractComposioTools(data),
939
- nextCursor: data?.next_cursor || data?.nextCursor || undefined,
940
- totalItems: data?.total_items ?? data?.totalItems ?? undefined,
941
- };
942
- }
943
- throw new Error(`Failed to fetch tools for ${toolkitSlug}. Check your API key.`);
944
- }
945
- function extractComposioTools(data) {
946
- let rawItems = [];
947
- if (Array.isArray(data))
948
- rawItems = data;
949
- else if (Array.isArray(data?.items))
950
- rawItems = data.items;
951
- else if (Array.isArray(data?.data))
952
- rawItems = data.data;
953
- return rawItems.map((t) => ({
954
- name: t.slug || t.name || "",
955
- description: t.description || t.human_description || "",
956
- inputSchema: t.input_parameters || t.inputParameters || undefined,
957
- }));
958
- }
959
- /**
960
- * Fetch full Composio tool descriptors by their slugs. Mirrors the
961
- * dashboard request
962
- * GET https://backend.composio.dev/api/v3.1/tools
963
- * ?toolkit_versions=<version>&tool_slugs=<SLUG_A,SLUG_B>
964
- *
965
- * Returns the raw items from the response, so callers (notably the
966
- * `aui integration tools --json` command) can surface every field
967
- * Composio sends — input/output schemas, toolkit metadata, tags, etc.
968
- *
969
- * Auth: the same Composio API key the CLI already uses (auto-fetched
970
- * via `fetchComposioApiKey`). Both `x-api-key` and `x-user-api-key`
971
- * header names are tried so this works against both org and user keys.
972
- */
973
- export async function fetchComposioToolsBySlugs(apiKey, toolSlugs, options) {
974
- if (!apiKey) {
975
- throw new Error("Composio API key is required");
976
- }
977
- if (!Array.isArray(toolSlugs) || toolSlugs.length === 0) {
978
- throw new Error("At least one tool slug is required");
979
- }
980
- const params = new URLSearchParams();
981
- if (options?.toolkitVersions) {
982
- params.set("toolkit_versions", options.toolkitVersions);
983
- }
984
- params.set("tool_slugs", toolSlugs.join(","));
985
- if (typeof options?.limit === "number") {
986
- params.set("limit", String(options.limit));
987
- }
988
- if (options?.cursor) {
989
- params.set("cursor", options.cursor);
990
- }
991
- const url = `https://backend.composio.dev/api/v3.1/tools?${params.toString()}`;
992
- let lastError = null;
993
- // Composio accepts the key under either of these headers depending on
994
- // whether the key was minted as an org key (x-api-key) or a user key
995
- // (x-user-api-key). The other helpers in this file (toolkits, tools
996
- // by toolkit) probe both — keep behavior consistent here.
997
- for (const headerName of ["x-api-key", "x-user-api-key"]) {
998
- try {
999
- const resp = await fetch(url, {
1000
- method: "GET",
1001
- headers: {
1002
- [headerName]: apiKey,
1003
- Accept: "application/json",
1004
- },
1005
- });
1006
- if (process.env.AUI_DEBUG) {
1007
- console.error(`[debug] fetchComposioToolsBySlugs (${headerName}): status ${resp.status}`);
1008
- }
1009
- if (!resp.ok) {
1010
- const text = await resp.text();
1011
- lastError = new Error(`HTTP ${resp.status}: ${text.slice(0, 500)}`);
1012
- // Auth-style failures — try the alternate header name before giving up.
1013
- if (resp.status === 401 || resp.status === 403)
1014
- continue;
1015
- throw lastError;
1016
- }
1017
- const data = await resp.json();
1018
- if (process.env.AUI_DEBUG) {
1019
- console.error(`[debug] fetchComposioToolsBySlugs response keys: ${Object.keys(data || {}).join(", ")}`);
1020
- }
1021
- const rawItems = extractToolDetailItems(data);
1022
- return {
1023
- tools: rawItems,
1024
- nextCursor: data?.next_cursor || data?.nextCursor || undefined,
1025
- totalItems: data?.total_items ?? data?.totalItems ?? undefined,
1026
- };
1027
- }
1028
- catch (e) {
1029
- lastError = e instanceof Error ? e : new Error(String(e));
1030
- if (process.env.AUI_DEBUG) {
1031
- console.error(`[debug] fetchComposioToolsBySlugs error (${headerName}): ${lastError.message}`);
1032
- }
1033
- }
1034
- }
1035
- throw (lastError ??
1036
- new Error("Failed to fetch tools by slugs from Composio. Check your API key."));
1037
- }
1038
- function extractToolDetailItems(data) {
1039
- if (!data)
1040
- return [];
1041
- if (Array.isArray(data))
1042
- return data;
1043
- if (Array.isArray(data.items))
1044
- return data.items;
1045
- if (Array.isArray(data.data))
1046
- return data.data;
1047
- if (Array.isArray(data.tools))
1048
- return data.tools;
1049
- // Walk one level for any nested array whose first item looks tool-ish.
1050
- for (const key of Object.keys(data)) {
1051
- const val = data[key];
1052
- if (Array.isArray(val) && val.length > 0 && (val[0]?.slug || val[0]?.name)) {
1053
- return val;
1054
- }
1055
- }
1056
- return [];
886
+ return {
887
+ tools: tools.map((t) => ({
888
+ name: String(t.name ?? ""),
889
+ description: String(t.description ?? ""),
890
+ ...(t.input_schema
891
+ ? { input_schema: t.input_schema }
892
+ : {}),
893
+ ...(t.inputSchema
894
+ ? { inputSchema: t.inputSchema }
895
+ : {}),
896
+ })),
897
+ };
1057
898
  }
1058
899
  //# sourceMappingURL=integration.service.js.map