diff --git a/components/super_carl/README.md b/components/super_carl/README.md new file mode 100644 index 0000000000000..4404c55461f5f --- /dev/null +++ b/components/super_carl/README.md @@ -0,0 +1,27 @@ +# Overview + +Super Carl is an AI networking and relationship-search platform for finding people, companies, jobs, posts, warm paths, and approved outbound communications through a user's professional network. + +Use Super Carl on Pipedream to build workflows that qualify people and companies, check network readiness before search, find jobs with warm paths, discover post or engagement signals, and create drafts or sends through Super Carl communication channels. + +# Getting Started + +Super Carl uses API-key authentication. In Super Carl, open **Integrations**, go to the **API / MCP** section, and create or purchase an API key with the `search` scope for search actions and the `communications` scope for communication actions. Paste that key into the Pipedream connected account when prompted. + +# Example Use Cases + +- **Get Network Summary** checks LinkedIn sync readiness and available network filters. +- **Search People** finds people by role, company history, expertise, location, network relationship, or recent activity. +- **Search Companies** finds companies by name, domain, funding, size, industry, location, growth, or technology. +- **Search Jobs** finds jobs and can include warm-path people at each hiring company. +- **Search Posts** finds posts, comments, likes, reactions, company mentions, and other public activity signals. +- **Check Communication Capabilities** determines whether Gmail, LinkedIn, X, Instagram, or Super Carl channels are ready for a target. +- **Create Communication Draft** saves a durable message draft without live delivery. +- **Send Communication** creates a dry run by default and can send after explicit configuration. +- **Get Communication**, **Get Communication History**, and **Cancel Communication** monitor or stop communication workflows. + +# Troubleshooting + +If authentication fails, confirm that the API key has the required scope and has not been revoked. If a search returns weaker network-aware results than expected, run **Get Network Summary** to confirm the relevant LinkedIn, Gmail, or Super Carl network data is synced. Before live communication sends, run **Check Communication Capabilities** and review the chosen target, channel, and Dry Run setting. + +For detailed API usage, see [Super Carl documentation](https://supercarl.ai/docs). diff --git a/components/super_carl/actions/cancel-communication/cancel-communication.mjs b/components/super_carl/actions/cancel-communication/cancel-communication.mjs new file mode 100644 index 0000000000000..b1ae3f4037f99 --- /dev/null +++ b/components/super_carl/actions/cancel-communication/cancel-communication.mjs @@ -0,0 +1,42 @@ +import superCarl from "../../super_carl.app.mjs"; +import { cleanObject } from "../../common/utils.mjs"; + +export default { + key: "super_carl-cancel-communication", + name: "Cancel Communication", + description: "Cancel a queued or in-progress Super Carl communication. Use this when a workflow needs to stop delivery before the communication reaches a terminal status. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + superCarl, + communicationId: { + propDefinition: [ + superCarl, + "communicationId", + ], + }, + reason: { + type: "string", + label: "Reason", + description: "Optional cancellation reason stored with the communication event log.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.superCarl.cancelCommunication({ + $, + communicationId: this.communicationId, + data: cleanObject({ + reason: this.reason, + }), + }); + + $.export("$summary", `Cancelled communication ${this.communicationId}.`); + return response; + }, +}; diff --git a/components/super_carl/actions/check-communication-capabilities/check-communication-capabilities.mjs b/components/super_carl/actions/check-communication-capabilities/check-communication-capabilities.mjs new file mode 100644 index 0000000000000..51f95bcf55112 --- /dev/null +++ b/components/super_carl/actions/check-communication-capabilities/check-communication-capabilities.mjs @@ -0,0 +1,107 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + requireCommunicationTarget, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-check-communication-capabilities", + name: "Check Communication Capabilities", + description: "Check which Super Carl communication channels are available for a target before sending a message. Returns the list of channels with their `can_send` status, recipient email, and connector user IDs. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + targetUserId: { + propDefinition: [ + superCarl, + "targetUserId", + ], + }, + linkedinProfileUrl: { + propDefinition: [ + superCarl, + "linkedinProfileUrl", + ], + }, + linkedinUsername: { + propDefinition: [ + superCarl, + "linkedinUsername", + ], + }, + xProfileUrl: { + propDefinition: [ + superCarl, + "xProfileUrl", + ], + }, + xUsername: { + propDefinition: [ + superCarl, + "xUsername", + ], + }, + instagramProfileUrl: { + propDefinition: [ + superCarl, + "instagramProfileUrl", + ], + }, + instagramUsername: { + propDefinition: [ + superCarl, + "instagramUsername", + ], + }, + recipientEmail: { + propDefinition: [ + superCarl, + "recipientEmail", + ], + }, + channels: { + propDefinition: [ + superCarl, + "communicationChannels", + ], + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const data = cleanObject({ + target_user_id: this.targetUserId, + linkedin_profile_url: this.linkedinProfileUrl, + linkedin_username: this.linkedinUsername, + x_profile_url: this.xProfileUrl, + x_username: this.xUsername, + instagram_profile_url: this.instagramProfileUrl, + instagram_username: this.instagramUsername, + recipient_email: this.recipientEmail, + channels: this.channels, + delegate_user_id: this.delegateUserId, + }); + requireCommunicationTarget(data); + + const response = await this.superCarl.getCommunicationCapabilities({ + $, + data, + }); + + const readyChannels = Array.isArray(response?.channels) + ? response.channels.filter((channel) => channel?.can_send === true) + : []; + $.export("$summary", `Found ${readyChannels.length} ready communication channels.`); + return response; + }, +}; diff --git a/components/super_carl/actions/create-communication-draft/create-communication-draft.mjs b/components/super_carl/actions/create-communication-draft/create-communication-draft.mjs new file mode 100644 index 0000000000000..a36c0bdbd3847 --- /dev/null +++ b/components/super_carl/actions/create-communication-draft/create-communication-draft.mjs @@ -0,0 +1,143 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + parseObjectProp, + requireCommunicationTarget, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-create-communication-draft", + name: "Create Communication Draft", + description: "Save a durable Super Carl communication draft without sending it. Use **Check Communication Capabilities** first to pick the channel and target fields; use **Send Communication** only after a user has approved live delivery. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + superCarl, + channel: { + propDefinition: [ + superCarl, + "communicationChannel", + ], + }, + message: { + propDefinition: [ + superCarl, + "message", + ], + }, + subject: { + propDefinition: [ + superCarl, + "subject", + ], + }, + targetUserId: { + propDefinition: [ + superCarl, + "targetUserId", + ], + }, + linkedinProfileUrl: { + propDefinition: [ + superCarl, + "linkedinProfileUrl", + ], + }, + linkedinUsername: { + propDefinition: [ + superCarl, + "linkedinUsername", + ], + }, + xProfileUrl: { + propDefinition: [ + superCarl, + "xProfileUrl", + ], + }, + xUsername: { + propDefinition: [ + superCarl, + "xUsername", + ], + }, + instagramProfileUrl: { + propDefinition: [ + superCarl, + "instagramProfileUrl", + ], + }, + instagramUsername: { + propDefinition: [ + superCarl, + "instagramUsername", + ], + }, + recipientEmail: { + propDefinition: [ + superCarl, + "recipientEmail", + ], + }, + connectorUserId: { + propDefinition: [ + superCarl, + "connectorUserId", + ], + }, + context: { + propDefinition: [ + superCarl, + "context", + ], + }, + idempotencyKey: { + propDefinition: [ + superCarl, + "idempotencyKey", + ], + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const context = parseObjectProp(this.context, "Context"); + const data = cleanObject({ + mode: "draft", + draft: true, + channel: this.channel, + message: this.message, + subject: this.subject, + target_user_id: this.targetUserId, + linkedin_profile_url: this.linkedinProfileUrl, + linkedin_username: this.linkedinUsername, + x_profile_url: this.xProfileUrl, + x_username: this.xUsername, + instagram_profile_url: this.instagramProfileUrl, + instagram_username: this.instagramUsername, + recipient_email: this.recipientEmail, + connector_user_id: this.connectorUserId, + context, + idempotency_key: this.idempotencyKey, + delegate_user_id: this.delegateUserId, + }); + requireCommunicationTarget(data); + + const response = await this.superCarl.createCommunication({ + $, + data, + }); + + $.export("$summary", `Created communication draft ${response?.id || ""}`.trim()); + return response; + }, +}; diff --git a/components/super_carl/actions/get-communication-history/get-communication-history.mjs b/components/super_carl/actions/get-communication-history/get-communication-history.mjs new file mode 100644 index 0000000000000..678231463eb4e --- /dev/null +++ b/components/super_carl/actions/get-communication-history/get-communication-history.mjs @@ -0,0 +1,139 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + requireCommunicationTarget, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-get-communication-history", + name: "Get Communication History", + description: "Fetch prior Super Carl communication history for a target before drafting or sending. Use this to avoid duplicate outreach and to inspect recent Gmail, LinkedIn, X, Instagram, and Super Carl sends. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + targetUserId: { + propDefinition: [ + superCarl, + "targetUserId", + ], + }, + linkedinProfileUrl: { + propDefinition: [ + superCarl, + "linkedinProfileUrl", + ], + }, + linkedinUsername: { + propDefinition: [ + superCarl, + "linkedinUsername", + ], + }, + xProfileUrl: { + propDefinition: [ + superCarl, + "xProfileUrl", + ], + }, + xUsername: { + propDefinition: [ + superCarl, + "xUsername", + ], + }, + instagramProfileUrl: { + propDefinition: [ + superCarl, + "instagramProfileUrl", + ], + }, + instagramUsername: { + propDefinition: [ + superCarl, + "instagramUsername", + ], + }, + recipientEmail: { + propDefinition: [ + superCarl, + "recipientEmail", + ], + }, + channel: { + type: "string", + label: "History Channel", + description: "Optional history filter. Use `all` for every channel or a specific channel such as `linkedin` or `gmail`.", + optional: true, + default: "all", + options: [ + "all", + "email", + "gmail", + "super_carl", + "linkedin", + "x", + "instagram", + ], + }, + limit: { + type: "integer", + label: "Limit", + description: "Maximum history rows to return.", + optional: true, + default: 12, + min: 1, + max: 50, + }, + offset: { + propDefinition: [ + superCarl, + "offset", + ], + }, + historyFresh: { + type: "boolean", + label: "Refresh LinkedIn History", + description: "When true, request a fresh LinkedIn history refresh if the target resolves to a Super Carl user.", + optional: true, + default: false, + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const data = cleanObject({ + target_user_id: this.targetUserId, + linkedin_profile_url: this.linkedinProfileUrl, + linkedin_username: this.linkedinUsername, + x_profile_url: this.xProfileUrl, + x_username: this.xUsername, + instagram_profile_url: this.instagramProfileUrl, + instagram_username: this.instagramUsername, + recipient_email: this.recipientEmail, + channel: this.channel, + limit: this.limit, + offset: this.offset, + history_fresh: this.historyFresh, + delegate_user_id: this.delegateUserId, + }); + requireCommunicationTarget(data); + + const response = await this.superCarl.getCommunicationHistory({ + $, + data, + }); + + $.export("$summary", `Found ${response?.total_count || 0} prior communications.`); + return response; + }, +}; diff --git a/components/super_carl/actions/get-communication/get-communication.mjs b/components/super_carl/actions/get-communication/get-communication.mjs new file mode 100644 index 0000000000000..2a40f60ba7721 --- /dev/null +++ b/components/super_carl/actions/get-communication/get-communication.mjs @@ -0,0 +1,49 @@ +import superCarl from "../../super_carl.app.mjs"; +import { cleanObject } from "../../common/utils.mjs"; + +export default { + key: "super_carl-get-communication", + name: "Get Communication", + description: "Fetch a Super Carl communication record, normalized status, recent events, task metadata, and artifact URLs after **Create Communication Draft** or **Send Communication**. Use Wait Milliseconds when a workflow should pause for delivery progress. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + communicationId: { + propDefinition: [ + superCarl, + "communicationId", + ], + }, + waitMs: { + propDefinition: [ + superCarl, + "waitMs", + ], + }, + waitUntil: { + propDefinition: [ + superCarl, + "waitUntil", + ], + }, + }, + async run({ $ }) { + const response = await this.superCarl.getCommunication({ + $, + communicationId: this.communicationId, + params: cleanObject({ + wait_ms: this.waitMs, + wait_until: this.waitUntil, + }), + }); + + $.export("$summary", `Communication ${this.communicationId} is ${response?.status || "unknown"}.`); + return response; + }, +}; diff --git a/components/super_carl/actions/get-network-summary/get-network-summary.mjs b/components/super_carl/actions/get-network-summary/get-network-summary.mjs new file mode 100644 index 0000000000000..866c429f1276d --- /dev/null +++ b/components/super_carl/actions/get-network-summary/get-network-summary.mjs @@ -0,0 +1,47 @@ +import superCarl from "../../super_carl.app.mjs"; +import { cleanObject } from "../../common/utils.mjs"; + +export default { + key: "super_carl-get-network-summary", + name: "Get Network Summary", + description: "Check Super Carl network readiness before running **Search People**, **Search Jobs**, or **Search Companies**. Use this to inspect LinkedIn/Gmail/Super Carl graph sync status for the API key owner or a delegated team-seat user via Delegate User ID; low or unsynced counts can explain weak network-aware results. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const response = await this.superCarl.getNetworkSummary({ + $, + params: cleanObject({ + delegate_user_id: this.delegateUserId, + }), + }); + + const networks = Array.isArray(response?.networks) + ? response.networks + : []; + const linkedin = networks.find(({ key }) => key === "linkedin"); + const status = linkedin + ? `${linkedin.count || 0} LinkedIn connections, sync ${ + linkedin.needsSync + ? "needed" + : "ready" + }` + : `${networks.length} network entries returned`; + + $.export("$summary", status); + return response; + }, +}; diff --git a/components/super_carl/actions/search-companies/search-companies.mjs b/components/super_carl/actions/search-companies/search-companies.mjs new file mode 100644 index 0000000000000..327f97fd2b6c8 --- /dev/null +++ b/components/super_carl/actions/search-companies/search-companies.mjs @@ -0,0 +1,116 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + countSummary, + parseObjectProp, + requireQueryOrFilters, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-search-companies", + name: "Search Companies", + description: "Search companies by name, domain, funding, size, industry, location, growth, or technology. [See the documentation](https://supercarl.ai/docs)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + query: { + propDefinition: [ + superCarl, + "query", + ], + }, + filters: { + propDefinition: [ + superCarl, + "filters", + ], + }, + previewLimit: { + propDefinition: [ + superCarl, + "previewLimit", + ], + }, + resolveOnly: { + type: "boolean", + label: "Resolve Only", + description: "Return only company disambiguation metadata for a named company, domain, or LinkedIn company URL. Example query values: `stripe.com`, `https://www.linkedin.com/company/stripe`, or `Stripe`.", + optional: true, + default: false, + }, + resultMode: { + type: "string", + label: "Result Mode", + description: "Level of company-row detail to return. Use `preview` for fast, compact rows, or `detailed` when the workflow needs richer company metadata.", + optional: true, + default: "preview", + options: [ + "preview", + "detailed", + ], + }, + rankMode: { + type: "string", + label: "Rank Mode", + description: "Optional ranking mode. Use `default` for normal search order, or `llm` to use a deeper retrieval pool and LLM reranking when company fit matters more than speed.", + optional: true, + options: [ + "default", + "llm", + ], + }, + includeEvidenceText: { + type: "boolean", + label: "Include Evidence Text", + description: "Include supporting evidence text on company rows when available.", + optional: true, + default: false, + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const filters = parseObjectProp(this.filters, "Filters"); + requireQueryOrFilters({ + query: this.query, + filters, + }); + + const response = await this.superCarl.searchCompanies({ + $, + data: cleanObject({ + query: this.query, + filters, + preview_limit: this.previewLimit, + resolve_only: this.resolveOnly, + result_mode: this.resultMode, + rank_mode: this.rankMode === "default" + ? undefined + : this.rankMode, + include_evidence_text: this.includeEvidenceText, + delegate_user_id: this.delegateUserId, + }), + }); + + const total = response?.pagination?.total + ?? response?.result_count + ?? response?.result_count_estimate; + + $.export("$summary", countSummary({ + total, + rows: response?.companies, + rowLabel: "companies", + })); + return response; + }, +}; diff --git a/components/super_carl/actions/search-jobs/search-jobs.mjs b/components/super_carl/actions/search-jobs/search-jobs.mjs new file mode 100644 index 0000000000000..4ed4a8899ed5a --- /dev/null +++ b/components/super_carl/actions/search-jobs/search-jobs.mjs @@ -0,0 +1,99 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + countSummary, + parseObjectProp, + requireQueryOrFilters, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-search-jobs", + name: "Search Jobs", + description: "Search jobs when the workflow needs hiring-company opportunities, role fit, or warm paths into employers. Use **Search People** for candidate/advisor discovery, and enable With People when the workflow should return 1st/2nd-degree contacts at each hiring company. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + query: { + propDefinition: [ + superCarl, + "query", + ], + }, + filters: { + propDefinition: [ + superCarl, + "filters", + ], + }, + withPeople: { + type: "boolean", + label: "With People", + description: "Include 1st and 2nd degree people at each hiring company.", + optional: true, + default: false, + }, + previewLimit: { + propDefinition: [ + superCarl, + "previewLimit", + ], + }, + peoplePerCompany: { + type: "integer", + label: "People Per Company", + description: "Maximum people to include per hiring company when With People is enabled.", + optional: true, + default: 3, + min: 1, + max: 10, + }, + ranking: { + type: "object", + label: "Ranking", + description: "Optional ranking configuration JSON, for example `{ \"intent\": \"warm_intro\" }`.", + optional: true, + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const filters = parseObjectProp(this.filters, "Filters"); + const ranking = parseObjectProp(this.ranking, "Ranking"); + requireQueryOrFilters({ + query: this.query, + filters, + }); + + const response = await this.superCarl.searchJobs({ + $, + withPeople: this.withPeople, + data: cleanObject({ + query: this.query, + filters, + preview_limit: this.previewLimit, + people_per_company: this.withPeople + ? this.peoplePerCompany + : undefined, + ranking, + delegate_user_id: this.delegateUserId, + }), + }); + + $.export("$summary", countSummary({ + total: response?.total, + rows: response?.results, + rowLabel: "jobs", + })); + return response; + }, +}; diff --git a/components/super_carl/actions/search-people/search-people.mjs b/components/super_carl/actions/search-people/search-people.mjs new file mode 100644 index 0000000000000..d53593734c8ee --- /dev/null +++ b/components/super_carl/actions/search-people/search-people.mjs @@ -0,0 +1,102 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + countSummary, + parseObjectProp, + requireQueryOrFilters, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-search-people", + name: "Search People", + description: "Search people by role, company history, expertise, location, network relationship, or recent activity. Keep Preview enabled for fast counts and lightweight rows; disable Preview when you need full rows and Evidence Format, since Evidence Format is ignored during preview. Use **Search Companies** first when a named employer is ambiguous. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + query: { + propDefinition: [ + superCarl, + "query", + ], + }, + filters: { + propDefinition: [ + superCarl, + "filters", + ], + }, + preview: { + type: "boolean", + label: "Preview", + description: "Use the fast preview route. Turn off for full rows with selected evidence detail.", + optional: true, + default: true, + }, + limit: { + propDefinition: [ + superCarl, + "limit", + ], + }, + offset: { + propDefinition: [ + superCarl, + "offset", + ], + }, + evidenceFormat: { + propDefinition: [ + superCarl, + "evidenceFormat", + ], + }, + relationshipDetail: { + propDefinition: [ + superCarl, + "relationshipDetail", + ], + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const filters = parseObjectProp(this.filters, "Filters"); + requireQueryOrFilters({ + query: this.query, + filters, + }); + + const response = await this.superCarl.searchPeople({ + $, + preview: this.preview, + data: cleanObject({ + query: this.query, + filters, + limit: this.limit, + offset: this.offset, + evidence_format: this.preview + ? undefined + : this.evidenceFormat, + relationship_detail: this.relationshipDetail, + delegate_user_id: this.delegateUserId, + }), + }); + + $.export("$summary", countSummary({ + total: response?.total_count ?? response?.total, + rows: response?.users, + rowLabel: "people", + })); + return response; + }, +}; diff --git a/components/super_carl/actions/search-posts/search-posts.mjs b/components/super_carl/actions/search-posts/search-posts.mjs new file mode 100644 index 0000000000000..ed56a5c9cc935 --- /dev/null +++ b/components/super_carl/actions/search-posts/search-posts.mjs @@ -0,0 +1,126 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + countSummary, + parseObjectProp, + requireQueryOrFilters, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-search-posts", + name: "Search Posts", + description: "Search Super Carl post and activity signals, including authored posts, comments, likes, reactions, company mentions, and engagement. Use this before **Search People** when the workflow is anchored on someone posting or engaging with content; enable With People to return deduped actors from matching activity. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + superCarl, + query: { + propDefinition: [ + superCarl, + "query", + ], + }, + filters: { + propDefinition: [ + superCarl, + "filters", + ], + }, + withPeople: { + type: "boolean", + label: "With People", + description: "Include a deduped people set derived from matching post actors and authors.", + optional: true, + default: false, + }, + previewLimit: { + propDefinition: [ + superCarl, + "previewLimit", + ], + description: "Maximum number of post or activity rows to return.", + default: 10, + max: 50, + }, + offset: { + propDefinition: [ + superCarl, + "offset", + ], + }, + peopleLimit: { + type: "integer", + label: "People Limit", + description: "Maximum deduped people to include when With People is enabled.", + optional: true, + default: 25, + min: 1, + max: 100, + }, + sortBy: { + type: "string", + label: "Sort By", + description: "Sort post rows by relevance, recency, or engagement.", + optional: true, + options: [ + "relevance", + "recent", + "engagement", + "reactions", + "comments", + ], + }, + sortOrder: { + type: "string", + label: "Sort Order", + description: "Sort direction for engagement, reactions, or comments ordering.", + optional: true, + options: [ + "asc", + "desc", + ], + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const filters = parseObjectProp(this.filters, "Filters"); + requireQueryOrFilters({ + query: this.query, + filters, + }); + + const response = await this.superCarl.searchPosts({ + $, + withPeople: this.withPeople, + data: cleanObject({ + query: this.query, + filters, + preview_limit: this.previewLimit, + offset: this.offset, + people_limit: this.withPeople + ? this.peopleLimit + : undefined, + sort_by: this.sortBy, + sort_order: this.sortOrder, + delegate_user_id: this.delegateUserId, + }), + }); + + $.export("$summary", countSummary({ + total: response?.total, + rows: response?.results, + rowLabel: "posts", + })); + return response; + }, +}; diff --git a/components/super_carl/actions/send-communication/send-communication.mjs b/components/super_carl/actions/send-communication/send-communication.mjs new file mode 100644 index 0000000000000..e59f32403d1ff --- /dev/null +++ b/components/super_carl/actions/send-communication/send-communication.mjs @@ -0,0 +1,166 @@ +import superCarl from "../../super_carl.app.mjs"; +import { + cleanObject, + parseObjectProp, + requireCommunicationTarget, +} from "../../common/utils.mjs"; + +export default { + key: "super_carl-send-communication", + name: "Send Communication", + description: "Create a Super Carl outbound communication and optionally send it through Gmail, LinkedIn, X, Instagram, or Super Carl channels. Dry Run defaults to true; set it to false only after **Check Communication Capabilities** passes and the user approves live delivery. [See the documentation](https://supercarl.ai/docs/endpoints)", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + superCarl, + channel: { + propDefinition: [ + superCarl, + "communicationChannel", + ], + }, + message: { + propDefinition: [ + superCarl, + "message", + ], + }, + dryRun: { + type: "boolean", + label: "Dry Run", + description: "Create and validate the communication without live delivery. Defaults to true; set false only for approved sends.", + optional: true, + default: true, + }, + subject: { + propDefinition: [ + superCarl, + "subject", + ], + }, + targetUserId: { + propDefinition: [ + superCarl, + "targetUserId", + ], + }, + linkedinProfileUrl: { + propDefinition: [ + superCarl, + "linkedinProfileUrl", + ], + }, + linkedinUsername: { + propDefinition: [ + superCarl, + "linkedinUsername", + ], + }, + xProfileUrl: { + propDefinition: [ + superCarl, + "xProfileUrl", + ], + }, + xUsername: { + propDefinition: [ + superCarl, + "xUsername", + ], + }, + instagramProfileUrl: { + propDefinition: [ + superCarl, + "instagramProfileUrl", + ], + }, + instagramUsername: { + propDefinition: [ + superCarl, + "instagramUsername", + ], + }, + recipientEmail: { + propDefinition: [ + superCarl, + "recipientEmail", + ], + }, + connectorUserId: { + propDefinition: [ + superCarl, + "connectorUserId", + ], + }, + context: { + propDefinition: [ + superCarl, + "context", + ], + }, + idempotencyKey: { + propDefinition: [ + superCarl, + "idempotencyKey", + ], + }, + waitMs: { + propDefinition: [ + superCarl, + "waitMs", + ], + }, + waitUntil: { + propDefinition: [ + superCarl, + "waitUntil", + ], + }, + delegateUserId: { + propDefinition: [ + superCarl, + "delegateUserId", + ], + }, + }, + async run({ $ }) { + const context = parseObjectProp(this.context, "Context"); + const data = cleanObject({ + channel: this.channel, + message: this.message, + dry_run: this.dryRun, + subject: this.subject, + target_user_id: this.targetUserId, + linkedin_profile_url: this.linkedinProfileUrl, + linkedin_username: this.linkedinUsername, + x_profile_url: this.xProfileUrl, + x_username: this.xUsername, + instagram_profile_url: this.instagramProfileUrl, + instagram_username: this.instagramUsername, + recipient_email: this.recipientEmail, + connector_user_id: this.connectorUserId, + context, + idempotency_key: this.idempotencyKey, + wait_ms: this.waitMs, + wait_until: this.waitUntil, + delegate_user_id: this.delegateUserId, + }); + requireCommunicationTarget(data); + + const response = await this.superCarl.createCommunication({ + $, + data, + }); + + const mode = this.dryRun + ? "dry-run communication" + : "communication"; + $.export("$summary", `Created ${mode} ${response?.id || ""}`.trim()); + return response; + }, +}; diff --git a/components/super_carl/common/utils.mjs b/components/super_carl/common/utils.mjs new file mode 100644 index 0000000000000..ce8851445d611 --- /dev/null +++ b/components/super_carl/common/utils.mjs @@ -0,0 +1,113 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export const parseObjectProp = (value, label) => { + if (value === undefined || value === null || value === "") { + return undefined; + } + + if (typeof value === "object" && !Array.isArray(value)) { + return value; + } + + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed; + } + } catch { + // Fall through to a user-facing configuration error below. + } + } + + throw new ConfigurationError(`${label} must be a JSON object.`); +}; + +export const isEmptyValue = (value) => { + if (value === undefined || value === null) { + return true; + } + if (typeof value === "string") { + return value.trim().length === 0; + } + if (Array.isArray(value)) { + return value.length === 0 || value.every(isEmptyValue); + } + if (typeof value === "object") { + return Object.keys(value).length === 0 || Object.values(value).every(isEmptyValue); + } + return false; +}; + +export const cleanValue = (value) => { + if (Array.isArray(value)) { + return value + .map(cleanValue) + .filter((item) => !isEmptyValue(item)); + } + + if (value && typeof value === "object") { + return cleanObject(value); + } + + return value; +}; + +export const cleanObject = (object = {}) => Object.fromEntries( + Object.entries(object) + .map((entry) => { + const [ + key, + value, + ] = entry; + + return [ + key, + cleanValue(value), + ]; + }) + .filter(([ + , value, + ]) => !isEmptyValue(value)), +); + +export const requireQueryOrFilters = ({ + query, filters, +}) => { + if (isEmptyValue(query) && isEmptyValue(filters)) { + throw new ConfigurationError("Provide either a Query or Filters."); + } +}; + +export const requireCommunicationTarget = (payload = {}) => { + const targetKeys = [ + "target_user_id", + "linkedin_profile_url", + "linkedin_username", + "x_profile_url", + "x_username", + "instagram_profile_url", + "instagram_username", + "recipient_email", + ]; + + if (targetKeys.every((key) => isEmptyValue(payload[key]))) { + throw new ConfigurationError( + "Provide at least one target identifier, such as Target User ID, LinkedIn Profile URL, X Username, Instagram Username, or Recipient Email.", + ); + } +}; + +export const countSummary = ({ + total, rows, rowLabel, +}) => { + const rowCount = Array.isArray(rows) + ? rows.length + : 0; + + if (Number.isFinite(total)) { + return `Found ${total} ${rowLabel}; returned ${rowCount}.`; + } + + return `Returned ${rowCount} ${rowLabel}.`; +}; diff --git a/components/super_carl/package.json b/components/super_carl/package.json new file mode 100644 index 0000000000000..15c929c579e03 --- /dev/null +++ b/components/super_carl/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/super_carl", + "version": "0.0.1", + "description": "Pipedream Super Carl Components", + "main": "super_carl.app.mjs", + "keywords": [ + "pipedream", + "super_carl", + "super carl" + ], + "homepage": "https://pipedream.com/apps/super-carl", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.2.5" + } +} diff --git a/components/super_carl/super_carl.app.mjs b/components/super_carl/super_carl.app.mjs new file mode 100644 index 0000000000000..52277e454296a --- /dev/null +++ b/components/super_carl/super_carl.app.mjs @@ -0,0 +1,405 @@ +import { axios } from "@pipedream/platform"; + +import { cleanObject } from "./common/utils.mjs"; + +const COMMUNICATION_CHANNEL_OPTIONS = [ + "supercarl_direct_message", + "supercarl_invite", + "supercarl_referral_request", + "gmail_send", + "linkedin_send_message", + "x_send_message", + "instagram_send_message", +]; + +export default { + type: "app", + app: "super_carl", + propDefinitions: { + query: { + type: "string", + label: "Query", + description: "Natural-language search query, for example `Head of Growth candidates in NYC warm through my network`.", + optional: true, + }, + filters: { + type: "object", + label: "Filters", + description: "Optional Super Carl AdvancedFilters JSON object for structured constraints. Example: `{ \"job_titles\": { \"include\": [\"Head of Growth\"], \"current_only\": true }, \"locations\": { \"include\": [\"New York\"] }, \"connection_degrees\": [\"1st\"] }`.", + optional: true, + }, + delegateUserId: { + type: "string", + label: "Delegate User ID", + description: "Optional Super Carl team-seat user ID to search as. Use the user record ID for a teammate the API key owner can delegate to, for example `usr_abc123`.", + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "Maximum number of results to return.", + optional: true, + default: 10, + min: 1, + max: 25, + }, + offset: { + type: "integer", + label: "Offset", + description: "Number of results to skip.", + optional: true, + default: 0, + min: 0, + }, + previewLimit: { + type: "integer", + label: "Preview Limit", + description: "Maximum number of preview rows to return.", + optional: true, + default: 10, + min: 1, + max: 25, + }, + evidenceFormat: { + type: "string", + label: "Evidence Format", + description: "`none` returns compact rows, `reasons` adds match reasons, `text` adds profile prose, `json` adds structured evidence, and `both` returns text plus JSON. Applies only when Preview is disabled.", + optional: true, + default: "reasons", + options: [ + "none", + "reasons", + "text", + "json", + "both", + ], + }, + relationshipDetail: { + type: "string", + label: "Relationship Detail", + description: "`none` omits relationship data, `summary` includes social proximity and mutual counts, and `intro_paths` includes warm-intro path details when available.", + optional: true, + default: "none", + options: [ + "none", + "summary", + "intro_paths", + ], + }, + communicationChannel: { + type: "string", + label: "Channel", + description: "Outbound channel. Use `gmail_send` for email, `linkedin_send_message` for LinkedIn, `x_send_message` for X, `instagram_send_message` for Instagram, or a `supercarl_*` channel for in-product Super Carl messaging.", + options: COMMUNICATION_CHANNEL_OPTIONS, + }, + communicationChannels: { + type: "string[]", + label: "Channels", + description: "Optional channels to check. Example: `[\"gmail_send\", \"linkedin_send_message\"]`. Leave blank to check every supported channel.", + options: COMMUNICATION_CHANNEL_OPTIONS, + optional: true, + }, + communicationId: { + type: "string", + label: "Communication ID", + description: "ID returned by Create Communication Draft or Send Communication, for example `communication_123`.", + }, + targetUserId: { + type: "string", + label: "Target User ID", + description: "Optional Super Carl person/user ID for the communication target, for example `usr_abc123`.", + optional: true, + }, + linkedinProfileUrl: { + type: "string", + label: "LinkedIn Profile URL", + description: "Optional LinkedIn profile URL for the communication target, for example `https://www.linkedin.com/in/target-person/`.", + optional: true, + }, + linkedinUsername: { + type: "string", + label: "LinkedIn Username", + description: "Optional LinkedIn public identifier when a full profile URL is not available.", + optional: true, + }, + xProfileUrl: { + type: "string", + label: "X Profile URL", + description: "Optional X profile URL for the communication target, for example `https://x.com/username`.", + optional: true, + }, + xUsername: { + type: "string", + label: "X Username", + description: "Optional X username without the `@` symbol.", + optional: true, + }, + instagramProfileUrl: { + type: "string", + label: "Instagram Profile URL", + description: "Optional Instagram profile URL for the communication target.", + optional: true, + }, + instagramUsername: { + type: "string", + label: "Instagram Username", + description: "Optional Instagram username without the `@` symbol.", + optional: true, + }, + recipientEmail: { + type: "string", + label: "Recipient Email", + description: "Recipient email address for Gmail sends, or a returned email option from Check Communication Capabilities.", + optional: true, + }, + connectorUserId: { + type: "string", + label: "Connector User ID", + description: "Required only for `supercarl_referral_request`. Use an ID from `supercarl.candidate_connectors` returned by Check Communication Capabilities.", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "Message body to save or send. For draft flows, `[JoinLink]` macros may be expanded by Super Carl.", + }, + subject: { + type: "string", + label: "Subject", + description: "Email subject. Required when Channel is `gmail_send`.", + optional: true, + }, + context: { + type: "object", + label: "Context", + description: "Optional structured context JSON for communication generation or audit metadata.", + optional: true, + }, + idempotencyKey: { + type: "string", + label: "Idempotency Key", + description: "Optional key to prevent duplicate sends when retrying a workflow step.", + optional: true, + }, + waitMs: { + type: "integer", + label: "Wait Milliseconds", + description: "Optional wait time for communication progress, capped by Super Carl at 30000 ms.", + optional: true, + default: 0, + min: 0, + max: 30000, + }, + waitUntil: { + type: "string", + label: "Wait Until", + description: "Wait condition when Wait Milliseconds is set. Use `terminal` for completed/failed/cancelled status, or `first_progress` for the first new event.", + optional: true, + default: "terminal", + options: [ + "terminal", + "first_progress", + ], + }, + }, + methods: { + /** + * Return the Super Carl API base URL. + * + * @returns {string} Super Carl API base URL. + */ + _baseUrl() { + return "https://api.supercarl.ai"; + }, + /** + * Build authenticated request headers. + * + * @param {Object} [headers={}] Additional request headers. + * @returns {Object} Request headers with the Super Carl API key. + */ + _headers(headers = {}) { + return { + ...headers, + "X-API-Key": this.$auth.api_key, + }; + }, + /** + * Make an authenticated request to the Super Carl API. + * + * @param {Object} opts Request options. + * @param {Object} [opts.$=this] Pipedream step context. + * @param {string} opts.path API path beginning with `/`. + * @param {Object} [opts.headers] Additional request headers. + * @returns {Promise} API response. + */ + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + baseURL: this._baseUrl(), + url: path, + headers: this._headers(headers), + ...opts, + }); + }, + /** + * Get network sync and graph-readiness metadata. + * + * @param {Object} [opts={}] Request options. + * @returns {Promise} Network summary response. + */ + getNetworkSummary(opts = {}) { + return this._makeRequest({ + path: "/api/v1/network/summary", + ...opts, + }); + }, + /** + * Search Super Carl people profiles. + * + * @param {Object} [opts={}] Request options. + * @param {boolean} [opts.preview=true] Whether to use the preview endpoint. + * @returns {Promise} People search response. + */ + searchPeople({ + preview = true, ...opts + } = {}) { + return this._makeRequest({ + method: "POST", + path: preview + ? "/api/v1/search/people/preview" + : "/api/v1/search/people", + ...opts, + }); + }, + /** + * Search companies in Super Carl. + * + * @param {Object} [opts={}] Request options. + * @returns {Promise} Company search response. + */ + searchCompanies(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/v1/companies/search/preview", + ...opts, + }); + }, + /** + * Search jobs in Super Carl. + * + * @param {Object} [opts={}] Request options. + * @param {boolean} [opts.withPeople=false] Whether to include people at each hiring company. + * @returns {Promise} Job search response. + */ + searchJobs({ + withPeople = false, ...opts + } = {}) { + return this._makeRequest({ + method: "POST", + path: withPeople + ? "/api/v1/search/jobs/with-people" + : "/api/v1/search/jobs/preview", + ...opts, + }); + }, + /** + * Search posts and activity signals in Super Carl. + * + * @param {Object} [opts={}] Request options. + * @param {boolean} [opts.withPeople=false] Whether to include deduped people + * from matching activity. + * @returns {Promise} Post search response. + */ + searchPosts({ + withPeople = false, ...opts + } = {}) { + return this._makeRequest({ + method: "POST", + path: withPeople + ? "/api/v1/search/posts/with-people" + : "/api/v1/search/posts/preview", + ...opts, + }); + }, + /** + * Check target-specific communication channel readiness. + * + * @param {Object} [opts={}] Request options. + * @returns {Promise} Communication capabilities response. + */ + getCommunicationCapabilities(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/v1/communications/capabilities", + ...opts, + }); + }, + /** + * Create, draft, dry-run, or send a communication. + * + * @param {Object} [opts={}] Request options. + * @returns {Promise} Communication response. + */ + createCommunication(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/v1/communications", + ...opts, + }); + }, + /** + * Fetch communication status and recent events. + * + * @param {Object} [opts={}] Request options. + * @param {string} opts.communicationId Communication ID. + * @returns {Promise} Communication detail response. + */ + getCommunication({ + communicationId, ...opts + }) { + return this._makeRequest({ + path: `/api/v1/communications/${communicationId}`, + ...opts, + }); + }, + /** + * Cancel a queued or in-progress communication. + * + * @param {Object} [opts={}] Request options. + * @param {string} opts.communicationId Communication ID. + * @returns {Promise} Cancelled communication response. + */ + cancelCommunication({ + communicationId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/api/v1/communications/${communicationId}/cancel`, + ...opts, + }); + }, + /** + * Fetch communication history for a target. + * + * @param {Object} [opts={}] Request options. + * @returns {Promise} Communication history response. + */ + getCommunicationHistory(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/v1/communications/history", + ...opts, + }); + }, + /** + * Remove empty values from a request payload. + * + * @param {Object} [payload={}] Request payload. + * @returns {Object} Payload without empty values. + */ + cleanPayload(payload = {}) { + return cleanObject(payload); + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 633c3ecf561d4..205edeb4c8e40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15621,6 +15621,12 @@ importers: components/supadata: {} + components/super_carl: + dependencies: + '@pipedream/platform': + specifier: ^3.2.5 + version: 3.4.0 + components/supercast: dependencies: '@pipedream/platform':