From 97dfe75e996ab3373f371e71b5d1fa38086442a2 Mon Sep 17 00:00:00 2001 From: Manas Tripathi Date: Mon, 20 Jul 2026 22:39:57 +0530 Subject: [PATCH] fruitjam --- .../__tests__/multi-channel-campaign.test.ts | 34 ++++++ packages/nodes/src/ai/index.ts | 14 ++- .../nodes/src/ai/multi-channel-campaign.ts | 100 ++++++++++++++++++ packages/nodes/src/index.ts | 40 +++---- 4 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 packages/nodes/src/ai/__tests__/multi-channel-campaign.test.ts create mode 100644 packages/nodes/src/ai/multi-channel-campaign.ts diff --git a/packages/nodes/src/ai/__tests__/multi-channel-campaign.test.ts b/packages/nodes/src/ai/__tests__/multi-channel-campaign.test.ts new file mode 100644 index 0000000..bb0c088 --- /dev/null +++ b/packages/nodes/src/ai/__tests__/multi-channel-campaign.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { multiChannelCampaignNode } from "../multi-channel-campaign.js"; +import type { NodeExecutionContext } from "@jam-nodes/core"; + +const dummyContext: NodeExecutionContext = { + userId: "test_user", + workflowExecutionId: "wf_test", + variables: {}, + resolveNestedPath: () => undefined, +}; + +describe("multiChannelCampaignNode", () => { + it("generates campaign copy with correct placeholders", async () => { + const input = { + productDescription: "SpreadJam SuperTool", + targetAudience: "developers", + tone: "quirky" as const, + }; + + const result = await multiChannelCampaignNode.executor(input, dummyContext); + expect(result.success).toBe(true); + const out = result.output!; + expect(out.emailSubject).toContain("Quirky"); + expect(out.emailSubject).toContain("SpreadJam SuperTool"); + expect(out.emailSubject).toContain("developers"); + expect(out.emailBody).toContain("Hey developers"); + expect(out.tweet).toContain("SpreadJam SuperTool"); + expect(out.tweet).toContain("developers"); + expect(out.tweet).toContain("quirky"); + expect(out.linkedInPost).toContain("SpreadJam SuperTool"); + expect(out.linkedInPost).toContain("developers"); + expect(out.linkedInPost).toContain("quirky"); + }); +}); diff --git a/packages/nodes/src/ai/index.ts b/packages/nodes/src/ai/index.ts index fd423ae..91c67b3 100644 --- a/packages/nodes/src/ai/index.ts +++ b/packages/nodes/src/ai/index.ts @@ -4,7 +4,7 @@ export { SocialKeywordGeneratorOutputSchema, type SocialKeywordGeneratorInput, type SocialKeywordGeneratorOutput, -} from './keyword-generator.js'; +} from "./keyword-generator.js"; export { draftEmailsNode, @@ -16,7 +16,7 @@ export { type DraftEmailsOutput, type DraftEmailInfo, type Contact, -} from './draft-emails.js'; +} from "./draft-emails.js"; export { socialAiAnalyzeNode, @@ -26,4 +26,12 @@ export { type SocialAiAnalyzeOutput, type SocialPost, type AnalyzedPost, -} from './analyze-posts.js'; +} from "./analyze-posts.js"; + +export { + multiChannelCampaignNode, + MultiChannelCampaignInputSchema, + MultiChannelCampaignOutputSchema, + type MultiChannelCampaignInput, + type MultiChannelCampaignOutput, +} from "./multi-channel-campaign.js"; diff --git a/packages/nodes/src/ai/multi-channel-campaign.ts b/packages/nodes/src/ai/multi-channel-campaign.ts new file mode 100644 index 0000000..4925640 --- /dev/null +++ b/packages/nodes/src/ai/multi-channel-campaign.ts @@ -0,0 +1,100 @@ +// Multi-Channel Campaign Node +// Generates campaign copy for email, tweet, and LinkedIn post based on a product description, +// target audience, and tone. Pure TypeScript, no external API calls. + +import { defineNode } from "@jam-nodes/core"; +import { z } from "zod"; + +// --------------------------------------------------------------------------- +// Schemas +// --------------------------------------------------------------------------- + +export const MultiChannelCampaignInputSchema = z.object({ + /** Brief description of the product or service */ + productDescription: z.string().min(1), + /** Who the campaign is aimed at — e.g., "developers", "marketers" */ + targetAudience: z.string().min(1), + /** Desired writing tone */ + tone: z.enum(["friendly", "professional", "quirky"] as const), +}); + +export const MultiChannelCampaignOutputSchema = z.object({ + /** Subject line for the email */ + emailSubject: z.string(), + /** Full email body */ + emailBody: z.string(), + /** A tweet-sized message (max 280 chars) */ + tweet: z.string(), + /** LinkedIn post copy */ + linkedInPost: z.string(), +}); + +export type MultiChannelCampaignInput = z.infer< + typeof MultiChannelCampaignInputSchema +>; +export type MultiChannelCampaignOutput = z.infer< + typeof MultiChannelCampaignOutputSchema +>; + +// --------------------------------------------------------------------------- +// Helpers – simple template fillers +// --------------------------------------------------------------------------- + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function generateEmailSubject(input: MultiChannelCampaignInput): string { + const { productDescription, targetAudience, tone } = input; + return `${capitalize(tone)} ${productDescription} for ${targetAudience}`; +} + +function generateEmailBody(input: MultiChannelCampaignInput): string { + const { productDescription, targetAudience, tone } = input; + return `Hey ${targetAudience},\n\nWe’re thrilled to launch ${productDescription}. It’s designed to be ${tone} and will help you achieve great results.\n\nCheers,\nYour Team`; +} + +function generateTweet(input: MultiChannelCampaignInput): string { + const { productDescription, targetAudience, tone } = input; + const tag = productDescription.replace(/\s+/g, ""); + return `🚀 ${productDescription} is now ${tone} for ${targetAudience}! #${tag}`; +} + +function generateLinkedInPost(input: MultiChannelCampaignInput): string { + const { productDescription, targetAudience, tone } = input; + return `Introducing ${productDescription} — a ${tone} solution built for ${targetAudience}. We’ve crafted it to empower you and drive success. Learn more at https://example.com`; +} + +// --------------------------------------------------------------------------- +// Node definition +// --------------------------------------------------------------------------- + +export const multiChannelCampaignNode = defineNode({ + type: "multi_channel_campaign", + name: "Multi‑Channel Campaign", + description: "Generate email, tweet, and LinkedIn copy for a product launch", + category: "action", + inputSchema: MultiChannelCampaignInputSchema, + outputSchema: MultiChannelCampaignOutputSchema, + estimatedDuration: 5, + capabilities: { + supportsRerun: true, + supportsBulkActions: true, + }, + executor: async (input, _context) => { + const emailSubject = generateEmailSubject(input); + const emailBody = generateEmailBody(input); + const tweet = generateTweet(input); + const linkedInPost = generateLinkedInPost(input); + + return { + success: true, + output: { + emailSubject, + emailBody, + tweet, + linkedInPost, + }, + }; + }, +}); diff --git a/packages/nodes/src/index.ts b/packages/nodes/src/index.ts index 97e62dc..574fcb4 100644 --- a/packages/nodes/src/index.ts +++ b/packages/nodes/src/index.ts @@ -14,7 +14,7 @@ export { DelayOutputSchema, WebhookTriggerInputSchema, WebhookTriggerOutputSchema, -} from './logic/index.js' +} from "./logic/index.js"; export type { ConditionalInput, @@ -27,7 +27,7 @@ export type { DelayOutput, WebhookTriggerInput, WebhookTriggerOutput, -} from './logic/index.js' +} from "./logic/index.js"; // Transform nodes export { @@ -42,7 +42,7 @@ export { SortInputSchema, SortOutputSchema, SortDirectionSchema, -} from './transform/index.js' +} from "./transform/index.js"; export type { MapInput, @@ -53,7 +53,7 @@ export type { SortInput, SortOutput, SortDirection, -} from './transform/index.js' +} from "./transform/index.js"; // Example nodes export { @@ -64,7 +64,7 @@ export { breadNode, BreadInputSchema, BreadOutputSchema, -} from './examples/index.js' +} from "./examples/index.js"; export type { HttpRequestInput, @@ -72,7 +72,7 @@ export type { HttpMethod, BreadInput, BreadOutput, -} from './examples/index.js' +} from "./examples/index.js"; // Integration nodes export { @@ -196,7 +196,7 @@ export { readOutputSchema, updateInputSchema, updateOutputSchema, -} from './integrations/index.js' +} from "./integrations/index.js"; export type { RedditMonitorInput, @@ -276,7 +276,7 @@ export type { ReadOutput, UpdateInput, UpdateOutput, -} from './integrations/index.js' +} from "./integrations/index.js"; // AI nodes export { @@ -291,7 +291,7 @@ export { socialAiAnalyzeNode, SocialAiAnalyzeInputSchema, SocialAiAnalyzeOutputSchema, -} from './ai/index.js' +} from "./ai/index.js"; export type { SocialKeywordGeneratorInput, @@ -304,15 +304,15 @@ export type { SocialAiAnalyzeOutput, SocialPost, AnalyzedPost, -} from './ai/index.js' +} from "./ai/index.js"; // All nodes as a collection -import { conditionalNode } from './logic/index.js' -import { endNode } from './logic/index.js' -import { delayNode } from './logic/index.js' -import { webhookTriggerNode } from './logic/index.js' -import { mapNode, filterNode, sortNode } from './transform/index.js' -import { httpRequestNode, breadNode } from './examples/index.js' +import { conditionalNode } from "./logic/index.js"; +import { endNode } from "./logic/index.js"; +import { delayNode } from "./logic/index.js"; +import { webhookTriggerNode } from "./logic/index.js"; +import { mapNode, filterNode, sortNode } from "./transform/index.js"; +import { httpRequestNode, breadNode } from "./examples/index.js"; import { redditMonitorNode, twitterMonitorNode, @@ -348,12 +348,13 @@ import { googleSheetsClearNode, googleSheetsReadNode, googleSheetsUpdateNode, -} from './integrations/index.js' +} from "./integrations/index.js"; import { socialKeywordGeneratorNode, draftEmailsNode, socialAiAnalyzeNode, -} from './ai/index.js' + multiChannelCampaignNode, +} from "./ai/index.js"; /** * All built-in nodes as an array for easy registration @@ -411,4 +412,5 @@ export const builtInNodes = [ socialKeywordGeneratorNode, draftEmailsNode, socialAiAnalyzeNode, -] + multiChannelCampaignNode, +];