@maestrogtm/maestro-gtm 0.10.16
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.
- package/dist/ai-RNHSWSNV.js +158 -0
- package/dist/ai-RNHSWSNV.js.map +1 -0
- package/dist/app-PSZH2J56.js +54 -0
- package/dist/app-PSZH2J56.js.map +1 -0
- package/dist/batch-ZCHN54YJ.js +28 -0
- package/dist/batch-ZCHN54YJ.js.map +1 -0
- package/dist/campaign-XDXQA7KX.js +119 -0
- package/dist/campaign-XDXQA7KX.js.map +1 -0
- package/dist/chunk-365Q36GF.js +54 -0
- package/dist/chunk-365Q36GF.js.map +1 -0
- package/dist/chunk-4IV6QS4U.js +122 -0
- package/dist/chunk-4IV6QS4U.js.map +1 -0
- package/dist/chunk-6GLLK5KO.js +64 -0
- package/dist/chunk-6GLLK5KO.js.map +1 -0
- package/dist/chunk-6UNBW5SN.js +686 -0
- package/dist/chunk-6UNBW5SN.js.map +1 -0
- package/dist/chunk-A7JD6EYV.js +92 -0
- package/dist/chunk-A7JD6EYV.js.map +1 -0
- package/dist/chunk-ARNVJPFM.js +139 -0
- package/dist/chunk-ARNVJPFM.js.map +1 -0
- package/dist/chunk-AX6BOEF2.js +345 -0
- package/dist/chunk-AX6BOEF2.js.map +1 -0
- package/dist/chunk-C3T7QPSO.js +507 -0
- package/dist/chunk-C3T7QPSO.js.map +1 -0
- package/dist/chunk-FG43GILY.js +46 -0
- package/dist/chunk-FG43GILY.js.map +1 -0
- package/dist/chunk-FS6DCNCA.js +139 -0
- package/dist/chunk-FS6DCNCA.js.map +1 -0
- package/dist/chunk-I6GRD4X7.js +1144 -0
- package/dist/chunk-I6GRD4X7.js.map +1 -0
- package/dist/chunk-IP34URKR.js +621 -0
- package/dist/chunk-IP34URKR.js.map +1 -0
- package/dist/chunk-JFSKOY7Z.js +252 -0
- package/dist/chunk-JFSKOY7Z.js.map +1 -0
- package/dist/chunk-M25KLO7T.js +3272 -0
- package/dist/chunk-M25KLO7T.js.map +1 -0
- package/dist/chunk-M3G2WREL.js +57 -0
- package/dist/chunk-M3G2WREL.js.map +1 -0
- package/dist/chunk-MFFACSBE.js +4266 -0
- package/dist/chunk-MFFACSBE.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QZH3XFOQ.js +2636 -0
- package/dist/chunk-QZH3XFOQ.js.map +1 -0
- package/dist/chunk-SPWDMOEU.js +1940 -0
- package/dist/chunk-SPWDMOEU.js.map +1 -0
- package/dist/chunk-TP3BZDVV.js +28 -0
- package/dist/chunk-TP3BZDVV.js.map +1 -0
- package/dist/chunk-UBJUBYSQ.js +18 -0
- package/dist/chunk-UBJUBYSQ.js.map +1 -0
- package/dist/chunk-VNKXGHWY.js +20 -0
- package/dist/chunk-VNKXGHWY.js.map +1 -0
- package/dist/chunk-WKLCPIFB.js +9862 -0
- package/dist/chunk-WKLCPIFB.js.map +1 -0
- package/dist/chunk-YV5XOXRQ.js +7 -0
- package/dist/chunk-YV5XOXRQ.js.map +1 -0
- package/dist/cli-Z3BNNJYQ.js +852 -0
- package/dist/cli-Z3BNNJYQ.js.map +1 -0
- package/dist/client-Y34LNEWN.js +8 -0
- package/dist/client-Y34LNEWN.js.map +1 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/configure-XSENK4X5.js +64 -0
- package/dist/configure-XSENK4X5.js.map +1 -0
- package/dist/context.js +10 -0
- package/dist/context.js.map +1 -0
- package/dist/crm-QBNHVBYV.js +86 -0
- package/dist/crm-QBNHVBYV.js.map +1 -0
- package/dist/dfy-X3OXIYRA.js +356 -0
- package/dist/dfy-X3OXIYRA.js.map +1 -0
- package/dist/dist-LGCJKGBS.js +121 -0
- package/dist/dist-LGCJKGBS.js.map +1 -0
- package/dist/engagement-C4U7LPJH.js +463 -0
- package/dist/engagement-C4U7LPJH.js.map +1 -0
- package/dist/enrich-F5GPVZFE.js +226 -0
- package/dist/enrich-F5GPVZFE.js.map +1 -0
- package/dist/extract-DS5N6SSJ.js +155 -0
- package/dist/extract-DS5N6SSJ.js.map +1 -0
- package/dist/feedback-AIXKXNM5.js +51 -0
- package/dist/feedback-AIXKXNM5.js.map +1 -0
- package/dist/fetch-QJDSPI63.js +87 -0
- package/dist/fetch-QJDSPI63.js.map +1 -0
- package/dist/handlers.js +13 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/list-HL7NQQJX.js +236 -0
- package/dist/list-HL7NQQJX.js.map +1 -0
- package/dist/maestro-N7Q2JX22.js +903 -0
- package/dist/maestro-N7Q2JX22.js.map +1 -0
- package/dist/prospect-RUOT43H6.js +532 -0
- package/dist/prospect-RUOT43H6.js.map +1 -0
- package/dist/providers/factory.js +10 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/registry.js +8 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/provision-FT5NWN77.js +394 -0
- package/dist/provision-FT5NWN77.js.map +1 -0
- package/dist/recipe-JU3SXMZF.js +137 -0
- package/dist/recipe-JU3SXMZF.js.map +1 -0
- package/dist/review-5SB6DYDZ.js +70 -0
- package/dist/review-5SB6DYDZ.js.map +1 -0
- package/dist/sdk-LVBHNQ6T.js +3852 -0
- package/dist/sdk-LVBHNQ6T.js.map +1 -0
- package/dist/server-REKYQZ2E.js +22 -0
- package/dist/server-REKYQZ2E.js.map +1 -0
- package/dist/status-V3EEFS7S.js +114 -0
- package/dist/status-V3EEFS7S.js.map +1 -0
- package/dist/tam-J6NDBP5W.js +682 -0
- package/dist/tam-J6NDBP5W.js.map +1 -0
- package/dist/tools.js +80 -0
- package/dist/tools.js.map +1 -0
- package/dist/validation.js +12 -0
- package/dist/validation.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/types.ts","../src/providers/manifest-validation.ts","../src/utils/title-expansion.ts","../src/handlers/prospect.ts"],"sourcesContent":["/** Provider types. Interfaces, slim-field constants, and field-projection utility\n * for the provider adapter system.\n * Constraint: No Supabase. */\n\nimport type { Capability } from '../constants.js';\n\n// ─── Core Provider Interface ─────────────────────────────\n\nexport interface Provider {\n name: string;\n capabilities: Capability[];\n notes?: string;\n checkCredentials(): Promise<boolean>;\n checkCredits?(): Promise<CreditBalance | null>;\n}\n\nexport interface CreditBalance {\n remaining: number;\n unit: string;\n provider: string;\n}\n\nexport interface ProviderSummary {\n name: string;\n capabilities: Capability[];\n notes?: string;\n}\n\n// ─── Capability Interfaces ───────────────────────────────\n\nexport interface CompanyFinder {\n findCompanies(query: CompanyQuery): Promise<CompanyResult[]>;\n countCompanies?(query: CompanyQuery): Promise<number>;\n getCompany?(domain: string): Promise<CompanyResult | null>;\n}\n\nexport interface PeopleFinder {\n findPeople(query: PeopleQuery): Promise<PersonResult[]>;\n}\n\nexport interface EmailEnricher {\n enrichEmail(contact: ContactInput): Promise<EnrichEmailResult | null>;\n}\n\nexport type EnrichField = 'email' | 'phone' | 'title' | 'company' | 'funding' | 'tech_stack';\n\nexport interface PhoneEnricher {\n enrichPhone(contact: ContactInput): Promise<EnrichPhoneResult | null>;\n}\n\nexport interface EmailVerifier {\n verifyEmail(email: string): Promise<VerificationResult>;\n}\n\nexport interface Extractor {\n extract(target: ExtractTarget): Promise<ExtractResult>;\n}\n\nexport interface CampaignManager {\n listCampaigns(): Promise<Campaign[]>;\n getCampaignStats(campaignId: string): Promise<CampaignStats>;\n addLeadsToCampaign(campaignId: string, leads: CampaignLead[]): Promise<PushResult>;\n}\n\nexport interface OutreachManager {\n listCampaigns(): Promise<Campaign[]>;\n addLeadsToCampaign(campaignId: string, leads: OutreachLead[]): Promise<PushResult>;\n getCampaignStats(campaignId: string): Promise<CampaignStats>;\n}\n\nexport interface CrmManager {\n findPerson(query: CrmPersonQuery): Promise<CrmPerson | null>;\n createPerson(data: CrmPersonInput): Promise<CrmPerson>;\n updatePerson(id: string, data: Partial<CrmPersonInput>): Promise<CrmPerson>;\n findCompany?(name: string): Promise<CrmCompany | null>;\n createDeal?(data: CrmDealInput): Promise<CrmDeal>;\n}\n\n// ─── Shared Input/Output Types ───────────────────────────\n\nexport interface ContactInput {\n first_name: string;\n last_name: string;\n company?: string;\n company_domain?: string;\n linkedin_url?: string;\n}\n\nexport interface CompanyQuery {\n domain?: string;\n icp_text?: string;\n limit?: number;\n country?: string;\n min_employees?: number;\n max_employees?: number;\n // ─── Expanded params (Phase 1) ───\n domains?: string[];\n tech_stack?: string[];\n category?: string;\n negate_domains?: string[];\n min_digital_footprint?: number;\n max_digital_footprint?: number;\n offset?: number;\n provider?: string;\n search?: string;\n state?: string;\n platform?: string;\n min_revenue?: number;\n max_revenue?: number;\n}\n\nexport interface PeopleQuery {\n job_title?: string;\n /** Expanded title variants for providers that support multi-title search. */\n job_titles?: string[];\n company_name?: string;\n company_domain?: string;\n location?: string;\n limit?: number;\n // ─── Expanded params (Phase 1) ───\n seniority?: string[];\n department?: string[];\n has_email?: boolean;\n has_phone?: boolean;\n has_linkedin?: boolean;\n min_connections?: number;\n offset?: number;\n provider?: string;\n // ─── DiscoLike filters ───\n company_domains?: string[];\n skills?: string[];\n filter_industry?: string[];\n email_validated?: boolean;\n min_employees?: number;\n max_employees?: number;\n revenue_range?: string;\n // ─── BlitzAPI ───\n company_linkedin_url?: string;\n}\n\nexport interface CompanyResult {\n domain: string;\n name: string;\n description?: string;\n industry?: string;\n employees?: number;\n country?: string;\n linkedin_url?: string;\n score?: number;\n // ─── Rich fields (Phase 1) ───\n tech_stack?: string[];\n keywords?: string[];\n funding?: string;\n revenue_estimate?: number;\n growth_signals?: {\n quarterly_growth?: number;\n growth_trend?: string;\n digital_footprint_score?: number;\n digital_footprint_trend?: string;\n };\n engagement_metrics?: {\n traffic_estimate?: number;\n social_presence?: number;\n monthly_visitors?: number;\n bounce_rate?: number;\n };\n platform?: string;\n categories?: string;\n sources?: string[];\n // ─── Extended fields (Phase 2) ───\n founded_year?: number;\n logo_url?: string;\n phone?: string;\n twitter_url?: string;\n facebook_url?: string;\n address?: string;\n latest_funding_stage?: string;\n total_funding?: number;\n alexa_ranking?: number;\n}\n\nexport interface PersonResult {\n first_name: string;\n last_name: string;\n job_title?: string;\n company?: string;\n company_domain?: string;\n linkedin_url?: string;\n email?: string;\n phone?: string;\n // ─── Rich fields (Phase 1) ───\n seniority?: string;\n department?: string;\n location?: string;\n industry?: string;\n headline?: string;\n connections?: number;\n skills?: string[];\n sources?: string[];\n // ─── Extended fields (Phase 2) ───\n photo_url?: string;\n twitter_url?: string;\n github_url?: string;\n facebook_url?: string;\n email_status?: string;\n}\n\nexport interface EnrichEmailResult {\n email: string;\n quality?: string;\n provider: string;\n credits_consumed?: number;\n}\n\nexport interface EnrichPhoneResult {\n phone: string;\n quality?: string;\n provider: string;\n}\n\nexport interface VerificationResult {\n email: string;\n valid: boolean;\n status: 'deliverable' | 'catch_all_safe' | 'catch_all_not_safe' | 'undeliverable' | 'unknown';\n provider: string;\n}\n\nexport interface ExtractTarget {\n url: string;\n type: 'linkedin_profile' | 'linkedin_post' | 'web_page';\n fields?: string[];\n}\n\nexport interface ExtractResult {\n data: Record<string, unknown>;\n source: string;\n provider: string;\n}\n\nexport interface Campaign {\n id: string;\n name: string;\n status: string;\n provider: string;\n}\n\nexport interface CampaignStats {\n campaign_id: string;\n sent: number;\n opened: number;\n replied: number;\n bounced: number;\n provider: string;\n}\n\nexport interface CampaignLead {\n email: string;\n first_name?: string;\n last_name?: string;\n company?: string;\n custom_fields?: Record<string, string>;\n}\n\nexport interface OutreachLead {\n linkedin_url: string;\n first_name?: string;\n last_name?: string;\n company?: string;\n}\n\nexport interface PushResult {\n added: number;\n duplicates: number;\n errors: number;\n provider: string;\n}\n\nexport interface CrmPersonQuery {\n email?: string;\n linkedin_url?: string;\n name?: string;\n}\n\nexport interface CrmPerson {\n id: string;\n name: string;\n email?: string;\n company?: string;\n job_title?: string;\n provider: string;\n}\n\nexport interface CrmPersonInput {\n first_name: string;\n last_name: string;\n email?: string;\n company?: string;\n job_title?: string;\n}\n\nexport interface CrmCompany {\n id: string;\n name: string;\n domain?: string;\n provider: string;\n}\n\nexport interface CrmDealInput {\n name: string;\n company_id?: string;\n person_id?: string;\n stage?: string;\n value?: number;\n owner_id?: string;\n}\n\nexport interface CrmDeal {\n id: string;\n name: string;\n stage: string;\n provider: string;\n}\n\n// ─── Waterfall Types ─────────────────────────────────────\n\nexport interface WaterfallConfig {\n providers: string[];\n find: ('email' | 'phone')[];\n verify: boolean;\n stopOnFirst: boolean;\n /** Per-provider timeout in ms. If a provider doesn't respond in time, skip it. Default: 15000. */\n providerTimeoutMs?: number;\n /** Max time for the entire batch in ms. Remaining contacts get skipped. Default: 300000 (5 min). */\n batchTimeoutMs?: number;\n /** Number of contacts to enrich concurrently. Default: 1 (sequential). */\n concurrency?: number;\n}\n\nexport interface WaterfallResult {\n contact: ContactInput;\n email?: string;\n phone?: string;\n email_status?: 'deliverable' | 'catch_all_safe' | 'catch_all_not_safe' | 'undeliverable';\n email_provider?: string;\n phone_provider?: string;\n attempts: WaterfallAttempt[];\n charged: boolean;\n credits_consumed?: number;\n cost_usd?: number;\n}\n\nexport interface WaterfallAttempt {\n provider: string;\n capability: 'enrich_email' | 'enrich_phone' | 'verify_email';\n found: boolean;\n charged: boolean;\n duration_ms: number;\n error?: string;\n cached?: boolean;\n cost_usd?: number;\n}\n\nexport interface ProviderPricing {\n charged_only_on_find: boolean;\n estimated_cost_per_hit?: string;\n estimated_cost_per_lookup?: string;\n}\n\n// ─── Slim Field Projection ───────────────────────────────\n\n/** Fields returned by default for gtm_find_companies when fields='all' is not set. */\nexport const SLIM_COMPANY_FIELDS: (keyof CompanyResult)[] = [\n 'domain',\n 'name',\n 'description',\n 'industry',\n 'employees',\n 'country',\n 'linkedin_url',\n 'tech_stack',\n 'funding',\n 'revenue_estimate',\n 'score',\n];\n\n/** Fields returned by default for gtm_find_people when fields='all' is not set. */\nexport const SLIM_PERSON_FIELDS: (keyof PersonResult)[] = [\n 'first_name',\n 'last_name',\n 'job_title',\n 'email',\n 'phone',\n 'company',\n 'company_domain',\n 'linkedin_url',\n 'seniority',\n 'department',\n 'location',\n];\n\n/** Project an object down to only the specified keys, omitting undefined values. */\nexport function applySlimFields<T extends object>(obj: T, fields: (keyof T)[]): Partial<T> {\n const result: Partial<T> = {};\n for (const field of fields) {\n if (field in obj) {\n result[field] = obj[field];\n }\n }\n return result;\n}\n","/** Manifest-aware param validation and limit clamping. */\n\nimport { getToolManifest, MANIFESTS } from './manifests.js';\nimport { CONTROL_PARAMS } from './manifest-types.js';\n\nconst controlSet = new Set<string>(CONTROL_PARAMS);\n\nexport function validateParams(\n provider: string,\n tool: 'find_companies' | 'find_people',\n args: Record<string, unknown>\n): string[] {\n const manifest = getToolManifest(provider, tool);\n if (!manifest) return [];\n\n const supported = new Set(manifest.supported_params);\n const warnings: string[] = [];\n\n for (const [key, value] of Object.entries(args)) {\n if (value === undefined || value === null) continue;\n if (controlSet.has(key)) continue;\n if (!supported.has(key)) {\n const alternatives = Object.entries(MANIFESTS)\n .filter(([_, m]) => m[tool]?.supported_params.includes(key))\n .map(([name]) => name);\n const suggestion =\n alternatives.length > 0 ? ` Use provider: '${alternatives[0]}' for ${key} filtering.` : '';\n warnings.push(`${key} is not supported by ${provider}.${suggestion}`);\n }\n }\n\n return warnings;\n}\n\nexport function clampLimit(\n provider: string,\n tool: 'find_companies' | 'find_people',\n limit: number | undefined\n): number {\n const manifest = getToolManifest(provider, tool);\n if (!manifest) return limit ?? 50;\n\n if (limit === undefined) return manifest.default_limit;\n return Math.min(limit, manifest.max_limit);\n}\n","/** AI-powered job title expansion. Given a niche title, returns search variants.\n * Uses Haiku for fast/cheap expansion with in-memory caching.\n * Constraint: Never blocks on failure — returns original title as fallback. */\n\nimport { logWarn } from '@maestro/logging';\n\n// ─── Cache ───────────────────────────────────────────────\n\nconst TITLE_CACHE = new Map<string, string[]>();\n\n// ─── Skip List ───────────────────────────────────────────\n\n/** Common/obvious titles that don't need expansion — saves an API call. */\nconst SKIP_EXPANSION = new Set([\n 'ceo',\n 'cto',\n 'cfo',\n 'coo',\n 'cmo',\n 'cro',\n 'ciso',\n 'cpo',\n 'vp sales',\n 'vp marketing',\n 'vp engineering',\n 'vp product',\n 'head of sales',\n 'head of marketing',\n 'head of engineering',\n 'head of product',\n 'director of engineering',\n 'director of sales',\n 'director of marketing',\n 'software engineer',\n 'product manager',\n 'account executive',\n 'sales development representative',\n 'sdr',\n 'bdr',\n 'solutions engineer',\n 'customer success manager',\n]);\n\n// ─── Prompt ──────────────────────────────────────────────\n\nconst EXPANSION_PROMPT = `You are a GTM title expert. Given a job title, return 3-6 equivalent or closely related titles that someone with this role might also be called. These will be used as search filters in prospect databases.\n\nRules:\n- Include the original title\n- Include common synonyms and abbreviations\n- Include both formal and informal variants\n- Only return titles for the SAME level/function — don't expand \"VP Sales\" to \"SDR\"\n- Return as a JSON array of strings, nothing else\n\nTitle: \"TITLE_PLACEHOLDER\"`;\n\n// ─── Public API ──────────────────────────────────────────\n\n/**\n * Expand a niche job title into multiple search variants.\n *\n * Uses Haiku for inference (~0.001c per call). Results are cached in-memory.\n * On any failure (no API key, network error, parse error), returns [title] unchanged.\n */\nexport async function expandTitle(title: string, apiKey?: string): Promise<string[]> {\n const normalized = title.toLowerCase().trim();\n\n const cached = TITLE_CACHE.get(normalized);\n if (cached) return cached;\n\n if (SKIP_EXPANSION.has(normalized)) {\n TITLE_CACHE.set(normalized, [title]);\n return [title];\n }\n\n const key = apiKey || process.env.GTM_ANTHROPIC_API_KEY;\n if (!key) {\n TITLE_CACHE.set(normalized, [title]);\n return [title];\n }\n\n try {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': key,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-5-20251001',\n max_tokens: 200,\n messages: [{ role: 'user', content: EXPANSION_PROMPT.replace('TITLE_PLACEHOLDER', title) }],\n }),\n });\n\n if (!response.ok) {\n TITLE_CACHE.set(normalized, [title]);\n return [title];\n }\n\n const data = (await response.json()) as { content?: Array<{ text?: string }> };\n const raw = data.content?.[0]?.text || '';\n const text = raw.replace(/```json\\n?|\\n?```/g, '').trim();\n const variants: string[] = JSON.parse(text);\n\n if (Array.isArray(variants) && variants.length > 0) {\n TITLE_CACHE.set(normalized, variants);\n return variants;\n }\n } catch (err) {\n logWarn('title-expansion', 'Failed to expand title, using original', {\n title,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n\n TITLE_CACHE.set(normalized, [title]);\n return [title];\n}\n\n/** Clear the title expansion cache (for tests). */\nexport function clearTitleCache(): void {\n TITLE_CACHE.clear();\n}\n","/** Prospect handler. Routes company/people discovery to providers.\n * Constraint: No direct API calls. Uses provider registry. */\n\nimport { logError } from '@maestro/logging';\nimport { createGtmError } from '../errors.js';\nimport { runWaterfall } from '../providers/waterfall.js';\nimport { createReviewTable } from '../utils/table-bridge.js';\n\nimport type { HandlerContext } from './index.js';\nimport type {\n CompanyFinder,\n PeopleFinder,\n CompanyQuery,\n PeopleQuery,\n PersonResult,\n CompanyResult,\n ContactInput,\n WaterfallConfig,\n} from '../providers/types.js';\nimport { SLIM_PERSON_FIELDS, SLIM_COMPANY_FIELDS, applySlimFields } from '../providers/types.js';\nimport { validateParams, clampLimit } from '../providers/manifest-validation.js';\nimport { shouldWriteToFile, writeResultsFile, writeResultsCsv } from '../utils/result-writer.js';\nimport { getManifest } from '../providers/manifests.js';\nimport { expandTitle } from '../utils/title-expansion.js';\n\n// ─── Deduplication ──────────────────────────────────────\n\n/** Deduplicate person results by linkedin_url (primary) or email (fallback).\n * LinkedIn URL is more canonical — people keep one profile but change jobs/emails. */\nfunction deduplicatePeople(people: PersonResult[]): PersonResult[] {\n const seen = new Set<string>();\n return people.filter((p) => {\n const key = p.linkedin_url ?? p.email ?? `${p.first_name}_${p.last_name}_${p.company}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n}\n\n// ─── Handler ─────────────────────────────────────────────\n\nexport async function handleProspect(\n name: string,\n args: Record<string, unknown>,\n ctx: HandlerContext\n): Promise<unknown> {\n if (name === 'find_companies') {\n return findCompanies(args, ctx);\n }\n return findPeople(args, ctx);\n}\n\n// ─── Find Companies ──────────────────────────────────────\n\nasync function findCompanies(args: Record<string, unknown>, ctx: HandlerContext): Promise<unknown> {\n const providerName = (args.provider as string) || undefined;\n const providers = providerName\n ? [ctx.registry.get(providerName)].filter(Boolean)\n : ctx.registry\n .getForCapability('find_companies')\n .filter((p) => getManifest(p.name)?.auto_selectable !== false);\n\n if (providers.length === 0) {\n const registered = ctx.registry.list().map((p) => p.name);\n if (providerName) {\n const available = registered.length > 0 ? registered.join(', ') : 'none';\n throw createGtmError(\n 'PROVIDER_NOT_CONFIGURED',\n `Provider \"${providerName}\" is not registered or does not support company discovery. Available providers: ${available}.`\n );\n }\n throw createGtmError(\n 'PROVIDER_NOT_CONFIGURED',\n 'No providers configured for company discovery. Add a provider with find_companies capability.'\n );\n }\n\n const provider = providers[0]!;\n const finder = provider as unknown as CompanyFinder;\n\n // Single domain with no other search params → direct profile lookup (not lookalike)\n const domain = args.domain as string | undefined;\n const isDirectLookup =\n domain &&\n !args.domains &&\n !args.icp_text &&\n !args.category &&\n !args.tech_stack &&\n !args.country &&\n finder.getCompany;\n\n if (isDirectLookup) {\n const company = await finder.getCompany!(domain);\n if (!company) {\n throw new Error(`Company not found: ${domain}`);\n }\n return { provider: provider.name, count: 1, has_more: false, offset: 0, companies: [company] };\n }\n\n // Clamp limit based on provider manifest\n const effectiveLimit = clampLimit(\n provider.name,\n 'find_companies',\n args.limit as number | undefined\n );\n\n // Validate params when provider is explicitly selected\n const warnings = providerName\n ? validateParams(providerName, 'find_companies', args as Record<string, unknown>)\n : undefined;\n\n const query: CompanyQuery = {\n domain,\n icp_text: args.icp_text as string | undefined,\n limit: effectiveLimit,\n country: args.country as string | undefined,\n min_employees: args.min_employees as number | undefined,\n max_employees: args.max_employees as number | undefined,\n domains: args.domains as string[] | undefined,\n tech_stack: args.tech_stack as string[] | undefined,\n category: args.category as string | undefined,\n negate_domains: args.negate_domains as string[] | undefined,\n min_digital_footprint: args.min_digital_footprint as number | undefined,\n max_digital_footprint: args.max_digital_footprint as number | undefined,\n offset: args.offset as number | undefined,\n search: args.search as string | undefined,\n state: args.state as string | undefined,\n platform: args.platform as string | undefined,\n min_revenue: args.min_revenue as number | undefined,\n max_revenue: args.max_revenue as number | undefined,\n };\n\n const results = await finder.findCompanies(query);\n\n const fieldsArg = args.fields as string | string[] | undefined;\n const detail = args.detail as string | undefined;\n const wantsAll = fieldsArg === 'all' || detail === 'full';\n const slimmedResults = wantsAll\n ? results\n : results.map((r) => {\n const fieldList = Array.isArray(fieldsArg)\n ? (fieldsArg as (keyof CompanyResult)[])\n : SLIM_COMPANY_FIELDS;\n return applySlimFields(r, fieldList);\n });\n\n const offset = (args.offset as number) ?? 0;\n const has_more = results.length >= effectiveLimit;\n\n // ─── Hints ───────────────────────────────────────────────\n const hints: string[] = [];\n\n if (results.length >= effectiveLimit) {\n hints.push(\n `Returned ${results.length} results (limit reached). Increase limit or use offset for more.`\n );\n }\n\n // ─── Review Table ────────────────────────────────────────\n const shouldReview = (args.review as boolean) ?? false;\n let tableId: string | undefined;\n let tableUrl: string | undefined;\n\n if (shouldReview) {\n const tableResult = await createReviewTable(\n `Find Companies: ${query.icp_text ?? query.domain ?? 'results'}`,\n slimmedResults as Array<Record<string, unknown>>\n );\n if (tableResult) {\n tableId = tableResult.table_id;\n tableUrl = tableResult.url;\n } else {\n hints.push('Review table creation failed — results returned inline only.');\n }\n }\n\n const providerUsed = provider.name;\n\n // CSV output\n const output = args.output as string | undefined;\n if (output === 'csv') {\n const filePath = writeResultsCsv('find_companies', slimmedResults as Record<string, unknown>[]);\n return {\n provider: providerUsed,\n count: results.length,\n has_more,\n output: 'csv',\n file: filePath,\n preview: slimmedResults.slice(0, 3),\n columns: slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [],\n ...(warnings?.length ? { warnings } : {}),\n };\n }\n\n // File output for large result sets\n if (shouldWriteToFile(results.length, output)) {\n const meta = {\n query: {},\n provider: providerUsed,\n count: results.length,\n has_more,\n };\n const filePath = writeResultsFile('find_companies', slimmedResults, meta);\n const columns = slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [];\n return {\n provider: providerUsed,\n count: results.length,\n has_more,\n output: 'file',\n file: filePath,\n preview: slimmedResults.slice(0, 5),\n columns,\n ...(warnings?.length ? { warnings } : {}),\n };\n }\n\n return {\n provider: providerUsed,\n count: results.length,\n has_more,\n offset,\n ...(tableId && { table_id: tableId, url: tableUrl }),\n companies: slimmedResults,\n ...(warnings?.length ? { warnings } : {}),\n ...(hints.length > 0 && { hints }),\n };\n}\n\n// ─── Find People ─────────────────────────────────────────\n\nasync function findPeople(args: Record<string, unknown>, ctx: HandlerContext): Promise<unknown> {\n const providerName = (args.provider as string) || undefined;\n let providers = providerName\n ? [ctx.registry.get(providerName)].filter(Boolean)\n : ctx.registry\n .getForCapability('find_people')\n .filter((p) => getManifest(p.name)?.auto_selectable !== false);\n\n if (providers.length === 0) {\n const registered = ctx.registry.list().map((p) => p.name);\n if (providerName) {\n const available = registered.length > 0 ? registered.join(', ') : 'none';\n throw createGtmError(\n 'PROVIDER_NOT_CONFIGURED',\n `Provider \"${providerName}\" is not registered or does not support people search. Available providers: ${available}.`\n );\n }\n throw createGtmError(\n 'PROVIDER_NOT_CONFIGURED',\n 'No providers configured for people search. Add a provider with find_people capability.'\n );\n }\n\n // Prefer Apollo for domain-specific searches — it has the best person database for\n // name+domain lookups (275M+ contacts). Other providers may be better for broad searches.\n const hasDomain = args.company_domain || args.company_domains;\n if (!providerName && hasDomain) {\n const apollo = providers.find((p): p is NonNullable<typeof p> => p?.name === 'apollo');\n if (apollo) {\n providers = [apollo, ...providers.filter((p) => p?.name !== 'apollo')];\n }\n }\n\n // Prefer Disco for breadth searches (industry/employee filters without domain)\n const hasBreadthOnly =\n !hasDomain &&\n (args.filter_industry ||\n args.min_employees ||\n args.max_employees ||\n args.icp_text ||\n args.revenue_range);\n if (!providerName && hasBreadthOnly) {\n const disco = providers.find((p): p is NonNullable<typeof p> => p?.name === 'disco');\n if (disco) {\n providers = [disco, ...providers.filter((p) => p?.name !== 'disco')];\n }\n }\n\n const provider = providers[0]!;\n const finder = provider as unknown as PeopleFinder;\n\n // Clamp limit based on provider manifest\n const effectiveLimit = clampLimit(provider.name, 'find_people', args.limit as number | undefined);\n\n // Validate params when provider is explicitly selected\n const warnings = providerName\n ? validateParams(providerName, 'find_people', args as Record<string, unknown>)\n : undefined;\n\n // ─── Title Expansion ────────────────────────────────────\n // Expand niche titles into search variants (e.g. \"GTM Engineer\" → [\"GTM Engineer\", \"Growth Engineer\", ...])\n const rawTitle = args.job_title as string | undefined;\n let expandedTitles: string[] | undefined;\n if (rawTitle) {\n try {\n expandedTitles = await expandTitle(rawTitle);\n } catch (err) {\n logError('prospect:titleExpansion', err instanceof Error ? err : new Error(String(err)));\n expandedTitles = [rawTitle];\n }\n }\n\n const query: PeopleQuery = {\n job_title: rawTitle,\n job_titles: expandedTitles && expandedTitles.length > 1 ? expandedTitles : undefined,\n company_name: args.company_name as string | undefined,\n company_domain: args.company_domain as string | undefined,\n location: args.location as string | undefined,\n limit: effectiveLimit,\n seniority: args.seniority as string[] | undefined,\n department: args.department as string[] | undefined,\n has_email: args.has_email as boolean | undefined,\n has_phone: args.has_phone as boolean | undefined,\n has_linkedin: args.has_linkedin as boolean | undefined,\n min_connections: args.min_connections as number | undefined,\n offset: args.offset as number | undefined,\n company_domains: args.company_domains as string[] | undefined,\n skills: args.skills as string[] | undefined,\n filter_industry: args.filter_industry as string[] | undefined,\n email_validated: args.email_validated as boolean | undefined,\n min_employees: args.min_employees as number | undefined,\n max_employees: args.max_employees as number | undefined,\n revenue_range: args.revenue_range as string | undefined,\n };\n\n const results = await finder.findPeople(query);\n const dedupedResults = deduplicatePeople(results);\n\n // ─── Inline Enrichment ───────────────────────────────────\n // When enrich: true, resolve masked emails (contain '*') via waterfall enrichment.\n // Runs concurrently (5 at a time) to stay within handler timeout.\n const shouldEnrich = (args.enrich as boolean) ?? false;\n let enrichedCount = 0;\n let needsEnrichmentCount = 0;\n\n if (shouldEnrich) {\n const needsEnrichment = dedupedResults.filter((r) => !r.email || r.email.includes('*'));\n needsEnrichmentCount = needsEnrichment.length;\n\n if (needsEnrichment.length > 0) {\n try {\n const contacts: ContactInput[] = needsEnrichment.map((r) => ({\n first_name: r.first_name,\n last_name: r.last_name,\n company: r.company,\n company_domain: r.company_domain,\n linkedin_url: r.linkedin_url,\n }));\n\n const clientConfig = ctx.session.getActiveContext().config;\n const waterfallProviders = clientConfig.waterfall?.email ?? ['prospeo'];\n const waterfallConfig: WaterfallConfig = {\n providers: waterfallProviders,\n find: ['email'],\n verify: false,\n stopOnFirst: true,\n concurrency: 5,\n };\n\n const enrichResults = await runWaterfall(contacts, waterfallConfig, ctx.registry);\n\n // Merge enriched emails into dedupedResults in-place.\n // needsEnrichment holds references to the same objects, so direct mutation propagates.\n for (let i = 0; i < needsEnrichment.length; i++) {\n const enriched = enrichResults[i];\n if (enriched?.email) {\n needsEnrichment[i]!.email = enriched.email;\n enrichedCount++;\n }\n }\n } catch (error) {\n // Enrichment must never block search results — log and fall through\n logError(\n 'prospect:inlineEnrich',\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n }\n\n const resultsWithAlias = dedupedResults.map((r) => ({\n ...r,\n company_domain: r.company_domain ?? r.company,\n }));\n\n const fieldsArg = args.fields as string | string[] | undefined;\n const detail = args.detail as string | undefined;\n const wantsAll = fieldsArg === 'all' || detail === 'full';\n const slimmedResults = wantsAll\n ? resultsWithAlias\n : resultsWithAlias.map((r) => {\n const fieldList = Array.isArray(fieldsArg)\n ? (fieldsArg as (keyof PersonResult)[])\n : SLIM_PERSON_FIELDS;\n return applySlimFields(r, fieldList);\n });\n\n const offset = (args.offset as number) ?? 0;\n const has_more = dedupedResults.length >= effectiveLimit;\n\n // ─── Hints ───────────────────────────────────────────────\n const hints: string[] = [];\n\n // Title expansion hint\n if (expandedTitles && expandedTitles.length > 1) {\n hints.push(`Title expanded: \"${rawTitle}\" → ${expandedTitles.map((t) => `\"${t}\"`).join(', ')}`);\n }\n\n // Check for masked emails\n const maskedCount = dedupedResults.filter((r) => r.email?.includes('*')).length;\n if (maskedCount > 0) {\n hints.push(\n `${maskedCount} emails are masked by the provider. Pass enrich: true to resolve them.`\n );\n }\n\n // Check if limit was reached (may be more results)\n if (dedupedResults.length >= effectiveLimit) {\n hints.push(\n `Returned ${dedupedResults.length} results (limit reached). Increase limit or use offset for more.`\n );\n }\n\n // Check if inline enrichment had failures\n if (shouldEnrich && enrichedCount < needsEnrichmentCount) {\n const failed = needsEnrichmentCount - enrichedCount;\n hints.push(`${failed} contacts could not be enriched — missing LinkedIn URL or domain.`);\n }\n\n // ─── Review Table ────────────────────────────────────────\n const shouldReview = (args.review as boolean) ?? false;\n let tableId: string | undefined;\n let tableUrl: string | undefined;\n\n if (shouldReview) {\n const tableResult = await createReviewTable(\n `Find People: ${query.job_title ?? query.company_domain ?? 'results'}`,\n slimmedResults as Array<Record<string, unknown>>\n );\n if (tableResult) {\n tableId = tableResult.table_id;\n tableUrl = tableResult.url;\n } else {\n hints.push('Review table creation failed — results returned inline only.');\n }\n }\n\n const providerUsed = provider.name;\n\n // CSV output\n const output = args.output as string | undefined;\n if (output === 'csv') {\n const filePath = writeResultsCsv('find_people', slimmedResults as Record<string, unknown>[]);\n return {\n provider: providerUsed,\n count: dedupedResults.length,\n has_more,\n output: 'csv',\n file: filePath,\n preview: slimmedResults.slice(0, 3),\n columns: slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [],\n ...(warnings?.length ? { warnings } : {}),\n };\n }\n\n // File output for large result sets\n if (shouldWriteToFile(dedupedResults.length, output)) {\n const meta = {\n query: {},\n provider: providerUsed,\n count: dedupedResults.length,\n has_more,\n };\n const filePath = writeResultsFile('find_people', slimmedResults, meta);\n const columns = slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [];\n return {\n provider: providerUsed,\n count: dedupedResults.length,\n has_more,\n output: 'file',\n file: filePath,\n preview: slimmedResults.slice(0, 5),\n columns,\n ...(warnings?.length ? { warnings } : {}),\n };\n }\n\n return {\n provider: providerUsed,\n count: dedupedResults.length,\n has_more,\n offset,\n ...(tableId && { table_id: tableId, url: tableUrl }),\n ...(shouldEnrich && { enriched: enrichedCount }),\n people: slimmedResults,\n ...(warnings?.length ? { warnings } : {}),\n ...(hints.length > 0 && { hints }),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlusVibeClient,
|
|
3
|
+
ZapmailClient
|
|
4
|
+
} from "./chunk-WKLCPIFB.js";
|
|
5
|
+
import {
|
|
6
|
+
logError
|
|
7
|
+
} from "./chunk-6GLLK5KO.js";
|
|
8
|
+
import "./chunk-PZ5AY32C.js";
|
|
9
|
+
|
|
10
|
+
// src/handlers/provision.ts
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
15
|
+
function getZapmailClient(ctx) {
|
|
16
|
+
const config = ctx.session.getActiveContext().config;
|
|
17
|
+
const apiKey = config.providers.zapmail?.api_key ?? process.env.GTM_ZAPMAIL_API_KEY;
|
|
18
|
+
if (!apiKey) throw new Error("Zapmail API key not configured (GTM_ZAPMAIL_API_KEY)");
|
|
19
|
+
const workspaceKey = config.providers.zapmail?.workspace_id ?? process.env.GTM_ZAPMAIL_WORKSPACE_KEY;
|
|
20
|
+
const serviceProvider = process.env.GTM_ZAPMAIL_SERVICE_PROVIDER ?? "GOOGLE";
|
|
21
|
+
return new ZapmailClient({ apiKey, workspaceKey, serviceProvider });
|
|
22
|
+
}
|
|
23
|
+
function getPlusVibeClient(ctx) {
|
|
24
|
+
const config = ctx.session.getActiveContext().config;
|
|
25
|
+
const apiKey = config.providers.plusvibe?.api_key ?? process.env.GTM_PLUSVIBE_API_KEY;
|
|
26
|
+
if (!apiKey) throw new Error("PlusVibe API key not configured (GTM_PLUSVIBE_API_KEY)");
|
|
27
|
+
const workspaceId = config.providers.plusvibe?.workspace_id ?? process.env.GTM_PLUSVIBE_WORKSPACE_ID;
|
|
28
|
+
return new PlusVibeClient(apiKey, workspaceId);
|
|
29
|
+
}
|
|
30
|
+
function getInfraApiConfig() {
|
|
31
|
+
const url = process.env.GTM_API_URL ?? process.env.GTM_INFRA_API_URL ?? "https://api.maestrogtm.com";
|
|
32
|
+
const key = process.env.GTM_INFRA_API_KEY;
|
|
33
|
+
if (!key) throw new Error("Infrastructure API key not configured (GTM_INFRA_API_KEY)");
|
|
34
|
+
return { url, key };
|
|
35
|
+
}
|
|
36
|
+
async function handleProvision(args, ctx) {
|
|
37
|
+
const a = args;
|
|
38
|
+
switch (a.action) {
|
|
39
|
+
case "check_domains":
|
|
40
|
+
return checkDomains(a, ctx);
|
|
41
|
+
case "check_balance":
|
|
42
|
+
return checkBalance(ctx);
|
|
43
|
+
case "buy_domains":
|
|
44
|
+
return buyDomains(a, ctx);
|
|
45
|
+
case "list_domains":
|
|
46
|
+
return listDomains(ctx);
|
|
47
|
+
case "list_mailboxes":
|
|
48
|
+
return listMailboxes(a, ctx);
|
|
49
|
+
case "setup_dmarc":
|
|
50
|
+
return setupDmarc(a, ctx);
|
|
51
|
+
case "create_mailboxes":
|
|
52
|
+
return createMailboxes(a, ctx);
|
|
53
|
+
case "export_mailboxes":
|
|
54
|
+
return exportMailboxes(a, ctx);
|
|
55
|
+
case "create_plusvibe_client":
|
|
56
|
+
return createPlusVibeClient(a, ctx);
|
|
57
|
+
case "save_config":
|
|
58
|
+
return saveConfig(a, ctx);
|
|
59
|
+
case "deploy_crm":
|
|
60
|
+
return deployCrm(a, ctx);
|
|
61
|
+
case "start_provision":
|
|
62
|
+
return startProvision(a);
|
|
63
|
+
case "status":
|
|
64
|
+
return getStatus(a);
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Unknown provision action: ${a.action}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function checkDomains(args, ctx) {
|
|
70
|
+
if (!args.domains?.length) throw new Error("domains[] is required for check_domains");
|
|
71
|
+
const zapmail = getZapmailClient(ctx);
|
|
72
|
+
const results = await Promise.all(
|
|
73
|
+
args.domains.map(async (domain) => {
|
|
74
|
+
try {
|
|
75
|
+
const result = await zapmail.checkDomain(domain);
|
|
76
|
+
return { domain, ...result };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return { domain, available: false, error: error.message };
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
return { action: "check_domains", results };
|
|
83
|
+
}
|
|
84
|
+
async function checkBalance(ctx) {
|
|
85
|
+
const zapmail = getZapmailClient(ctx);
|
|
86
|
+
const balance = await zapmail.getBalance();
|
|
87
|
+
return { action: "check_balance", ...balance };
|
|
88
|
+
}
|
|
89
|
+
async function buyDomains(args, ctx) {
|
|
90
|
+
if (!args.domains?.length) throw new Error("domains[] is required for buy_domains");
|
|
91
|
+
const zapmail = getZapmailClient(ctx);
|
|
92
|
+
const result = await zapmail.buyDomain({
|
|
93
|
+
domains: args.domains.map((d) => ({ domainName: d, years: 1 })),
|
|
94
|
+
useWallet: true
|
|
95
|
+
});
|
|
96
|
+
return { action: "buy_domains", ...result };
|
|
97
|
+
}
|
|
98
|
+
async function startProvision(args) {
|
|
99
|
+
if (!args.client_context) throw new Error("client_context is required for start_provision");
|
|
100
|
+
if (!args.provision_id) throw new Error("provision_id is required for start_provision");
|
|
101
|
+
const { url, key } = getInfraApiConfig();
|
|
102
|
+
const provType = args.provision_type ?? "full";
|
|
103
|
+
const body = {
|
|
104
|
+
clientContext: args.client_context,
|
|
105
|
+
...provType === "outreach" ? { outreachProvisionId: args.provision_id } : { provisionId: args.provision_id }
|
|
106
|
+
};
|
|
107
|
+
const response = await fetch(`${url}/api/infrastructure/provision`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
"x-infra-key": key
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(body)
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const errorBody = await response.text();
|
|
117
|
+
logError("provision:start", new Error(`API returned ${response.status}: ${errorBody}`));
|
|
118
|
+
throw new Error(`Provisioning API returned ${response.status}: ${errorBody}`);
|
|
119
|
+
}
|
|
120
|
+
const result = await response.json();
|
|
121
|
+
return { action: "start_provision", ...result };
|
|
122
|
+
}
|
|
123
|
+
async function getStatus(args) {
|
|
124
|
+
if (!args.provision_id) throw new Error("provision_id is required for status");
|
|
125
|
+
const { url, key } = getInfraApiConfig();
|
|
126
|
+
const response = await fetch(`${url}/api/infrastructure/provision/${args.provision_id}`, {
|
|
127
|
+
headers: { "x-infra-key": key }
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
if (response.status === 404) {
|
|
131
|
+
return { action: "status", found: false, provision_id: args.provision_id };
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Status API returned ${response.status}`);
|
|
134
|
+
}
|
|
135
|
+
const result = await response.json();
|
|
136
|
+
return { action: "status", ...result };
|
|
137
|
+
}
|
|
138
|
+
async function listDomains(ctx) {
|
|
139
|
+
const zapmail = getZapmailClient(ctx);
|
|
140
|
+
const result = await zapmail.listDomains({ limit: 100 });
|
|
141
|
+
const domains = (result.domains ?? []).map((d) => ({
|
|
142
|
+
id: d.id,
|
|
143
|
+
domain: d.domain,
|
|
144
|
+
status: d.status,
|
|
145
|
+
mailboxCount: Number(d.assignedMailboxesCount ?? 0),
|
|
146
|
+
dmarcEmail: d.dmarcEmail,
|
|
147
|
+
isWarmedUp: d.isWarmedUp,
|
|
148
|
+
createdAt: d.createdAt
|
|
149
|
+
}));
|
|
150
|
+
return {
|
|
151
|
+
action: "list_domains",
|
|
152
|
+
total: result.totalSearchedCount,
|
|
153
|
+
domains
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function listMailboxes(args, ctx) {
|
|
157
|
+
const zapmail = getZapmailClient(ctx);
|
|
158
|
+
const result = await zapmail.listMailboxes({
|
|
159
|
+
contains: args.domains?.[0]
|
|
160
|
+
});
|
|
161
|
+
return { action: "list_mailboxes", ...result };
|
|
162
|
+
}
|
|
163
|
+
async function setupDmarc(args, ctx) {
|
|
164
|
+
if (!args.domain_ids?.length) throw new Error("domain_ids[] is required for setup_dmarc");
|
|
165
|
+
const zapmail = getZapmailClient(ctx);
|
|
166
|
+
const dmarcEmail = args.dmarc_email ?? args.email ?? "dmarc@maestrogtm.com";
|
|
167
|
+
const status = args.dmarc_status ?? ["p=none"];
|
|
168
|
+
await zapmail.setupDmarc({ domainIds: args.domain_ids, email: dmarcEmail, status });
|
|
169
|
+
return {
|
|
170
|
+
action: "setup_dmarc",
|
|
171
|
+
domainCount: args.domain_ids.length,
|
|
172
|
+
email: dmarcEmail,
|
|
173
|
+
status
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function createMailboxes(args, ctx) {
|
|
177
|
+
if (!args.domain_ids?.length) throw new Error("domain_ids[] is required for create_mailboxes");
|
|
178
|
+
if (!args.domains?.length)
|
|
179
|
+
throw new Error("domains[] (domain names) is required for create_mailboxes");
|
|
180
|
+
if (!args.mailboxes?.length) throw new Error("mailboxes[] is required for create_mailboxes");
|
|
181
|
+
if (args.domain_ids.length !== args.domains.length) {
|
|
182
|
+
throw new Error("domain_ids[] and domains[] must have the same length");
|
|
183
|
+
}
|
|
184
|
+
const zapmail = getZapmailClient(ctx);
|
|
185
|
+
const scheduleBody = {};
|
|
186
|
+
for (let i = 0; i < args.domain_ids.length; i++) {
|
|
187
|
+
const domainId = args.domain_ids[i];
|
|
188
|
+
const domainName = args.domains[i];
|
|
189
|
+
scheduleBody[domainId] = args.mailboxes.map((m) => ({
|
|
190
|
+
domainName,
|
|
191
|
+
mailboxUsername: m.username,
|
|
192
|
+
firstName: m.firstName,
|
|
193
|
+
lastName: m.lastName
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
await zapmail.scheduleMailbox(scheduleBody);
|
|
197
|
+
const totalCreated = args.domain_ids.length * args.mailboxes.length;
|
|
198
|
+
return {
|
|
199
|
+
action: "create_mailboxes",
|
|
200
|
+
totalCreated,
|
|
201
|
+
domains: args.domains,
|
|
202
|
+
mailboxes: args.mailboxes.map((m) => m.username)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function exportMailboxes(args, ctx) {
|
|
206
|
+
const zapmail = getZapmailClient(ctx);
|
|
207
|
+
if (args.email) {
|
|
208
|
+
await zapmail.registerExportCredentials({
|
|
209
|
+
app: "PIPL",
|
|
210
|
+
email: args.email,
|
|
211
|
+
password: args.config_values?.password ?? ""
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
await zapmail.exportMailboxes({ apps: ["PIPL"] });
|
|
215
|
+
return { action: "export_mailboxes", exported: true };
|
|
216
|
+
}
|
|
217
|
+
async function createPlusVibeClient(args, ctx) {
|
|
218
|
+
if (!args.name) throw new Error("name is required for create_plusvibe_client");
|
|
219
|
+
if (!args.email) throw new Error("email is required for create_plusvibe_client");
|
|
220
|
+
const plusvibe = getPlusVibeClient(ctx);
|
|
221
|
+
const config = ctx.session.getActiveContext().config;
|
|
222
|
+
const workspaceId = args.workspace_id ?? config.providers.plusvibe?.workspace_id ?? process.env.GTM_PLUSVIBE_WORKSPACE_ID;
|
|
223
|
+
if (!workspaceId) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
"PlusVibe workspace_id is required \u2014 create workspace in PlusVibe dashboard, then set in config"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const client = await plusvibe.createClient({
|
|
229
|
+
name: args.name,
|
|
230
|
+
email: args.email,
|
|
231
|
+
workspaceId
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
action: "create_plusvibe_client",
|
|
235
|
+
client,
|
|
236
|
+
message: `Created PlusVibe client ${args.email} in workspace ${workspaceId}`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function saveConfig(args, ctx) {
|
|
240
|
+
if (!args.provider) throw new Error("provider is required for save_config");
|
|
241
|
+
if (!args.config_values || Object.keys(args.config_values).length === 0) {
|
|
242
|
+
throw new Error("config_values is required for save_config");
|
|
243
|
+
}
|
|
244
|
+
const configDir = join(homedir(), ".gtm");
|
|
245
|
+
const configPath = join(configDir, "config.yaml");
|
|
246
|
+
let config;
|
|
247
|
+
try {
|
|
248
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
249
|
+
config = parseYaml(raw);
|
|
250
|
+
} catch {
|
|
251
|
+
config = {};
|
|
252
|
+
}
|
|
253
|
+
if (!config.defaults) config.defaults = {};
|
|
254
|
+
const defaults = config.defaults;
|
|
255
|
+
if (!defaults.providers) defaults.providers = {};
|
|
256
|
+
const providers = defaults.providers;
|
|
257
|
+
const clientName = args.client_name ?? ctx.session.getActiveContext().clientName;
|
|
258
|
+
if (clientName && clientName !== "default") {
|
|
259
|
+
if (!config.clients) config.clients = {};
|
|
260
|
+
const clients = config.clients;
|
|
261
|
+
if (!clients[clientName]) clients[clientName] = {};
|
|
262
|
+
if (!clients[clientName].providers) clients[clientName].providers = {};
|
|
263
|
+
const clientProviders = clients[clientName].providers;
|
|
264
|
+
if (!clientProviders[args.provider]) clientProviders[args.provider] = {};
|
|
265
|
+
Object.assign(clientProviders[args.provider], args.config_values);
|
|
266
|
+
} else {
|
|
267
|
+
if (!providers[args.provider]) providers[args.provider] = {};
|
|
268
|
+
Object.assign(providers[args.provider], args.config_values);
|
|
269
|
+
}
|
|
270
|
+
mkdirSync(configDir, { recursive: true });
|
|
271
|
+
writeFileSync(configPath, stringifyYaml(config), "utf-8");
|
|
272
|
+
return {
|
|
273
|
+
action: "save_config",
|
|
274
|
+
saved: true,
|
|
275
|
+
client: clientName,
|
|
276
|
+
provider: args.provider,
|
|
277
|
+
fields: Object.keys(args.config_values)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function deployCrm(args, ctx) {
|
|
281
|
+
if (!args.name) throw new Error("name is required for deploy_crm (client name)");
|
|
282
|
+
const clientSlug = args.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+$/, "");
|
|
283
|
+
const projectName = `twenty-${clientSlug}`;
|
|
284
|
+
const serviceName = `twenty-crm-${clientSlug}`;
|
|
285
|
+
const config = ctx.session.getActiveContext().config;
|
|
286
|
+
const existingUrl = config.providers.twenty?.base_url;
|
|
287
|
+
const existingKey = config.providers.twenty?.api_key;
|
|
288
|
+
const steps = [];
|
|
289
|
+
steps.push({
|
|
290
|
+
step: 1,
|
|
291
|
+
title: "Create Railway project",
|
|
292
|
+
type: "cli",
|
|
293
|
+
commands: [`railway login`, `railway init --name ${projectName}`],
|
|
294
|
+
notes: "Creates a new Railway project. Skip if project already exists."
|
|
295
|
+
});
|
|
296
|
+
steps.push({
|
|
297
|
+
step: 2,
|
|
298
|
+
title: "Deploy Twenty CRM service",
|
|
299
|
+
type: "cli",
|
|
300
|
+
commands: [
|
|
301
|
+
`railway add --service ${serviceName}`,
|
|
302
|
+
`railway variables set SERVER_URL=https://${serviceName}.up.railway.app -s ${serviceName}`,
|
|
303
|
+
`railway variables set FRONT_BASE_URL=https://${serviceName}.up.railway.app -s ${serviceName}`,
|
|
304
|
+
`railway variables set IS_SIGN_UP_DISABLED=true -s ${serviceName}`,
|
|
305
|
+
`railway variables set NODE_ENV=production -s ${serviceName}`
|
|
306
|
+
],
|
|
307
|
+
notes: "Uses Twenty Docker image twentycrm/twenty:latest. Alternatively deploy from Railway template: https://railway.app/template/twenty",
|
|
308
|
+
env_vars: {
|
|
309
|
+
SERVER_URL: `https://${serviceName}.up.railway.app`,
|
|
310
|
+
FRONT_BASE_URL: `https://${serviceName}.up.railway.app`,
|
|
311
|
+
IS_SIGN_UP_DISABLED: "true"
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
steps.push({
|
|
315
|
+
step: 3,
|
|
316
|
+
title: "Add PostgreSQL database",
|
|
317
|
+
type: "cli",
|
|
318
|
+
commands: [
|
|
319
|
+
`railway add --database postgres --service ${serviceName}-db`,
|
|
320
|
+
`railway variables set DATABASE_URL=\${{${serviceName}-db.DATABASE_URL}} -s ${serviceName}`
|
|
321
|
+
],
|
|
322
|
+
notes: "Twenty requires PostgreSQL 15+. Railway provisions this automatically."
|
|
323
|
+
});
|
|
324
|
+
steps.push({
|
|
325
|
+
step: 4,
|
|
326
|
+
title: "Add Redis cache",
|
|
327
|
+
type: "cli",
|
|
328
|
+
commands: [
|
|
329
|
+
`railway add --database redis --service ${serviceName}-redis`,
|
|
330
|
+
`railway variables set REDIS_URL=\${{${serviceName}-redis.REDIS_URL}} -s ${serviceName}`
|
|
331
|
+
],
|
|
332
|
+
notes: "Required for Twenty job queue and caching."
|
|
333
|
+
});
|
|
334
|
+
steps.push({
|
|
335
|
+
step: 5,
|
|
336
|
+
title: "Deploy and wait for health",
|
|
337
|
+
type: "cli",
|
|
338
|
+
commands: [
|
|
339
|
+
`railway up -s ${serviceName} --image twentycrm/twenty:latest`,
|
|
340
|
+
`# Wait ~2-3 minutes for first deploy`,
|
|
341
|
+
`# Verify: curl https://${serviceName}.up.railway.app/healthz`
|
|
342
|
+
],
|
|
343
|
+
notes: "First deploy runs database migrations automatically. May take 2-3 minutes."
|
|
344
|
+
});
|
|
345
|
+
steps.push({
|
|
346
|
+
step: 6,
|
|
347
|
+
title: "Create admin account",
|
|
348
|
+
type: "manual",
|
|
349
|
+
notes: `Open https://${serviceName}.up.railway.app in browser. Sign up is enabled on first visit (even with IS_SIGN_UP_DISABLED). Create an admin account with email: ${args.email ?? `admin@${clientSlug}.com`}. After first account creation, sign-up is locked.`
|
|
350
|
+
});
|
|
351
|
+
steps.push({
|
|
352
|
+
step: 7,
|
|
353
|
+
title: "Generate API key",
|
|
354
|
+
type: "manual",
|
|
355
|
+
notes: `In Twenty dashboard \u2192 Settings \u2192 API Keys \u2192 Generate. Copy the API key \u2014 needed for Maestro configuration.`
|
|
356
|
+
});
|
|
357
|
+
steps.push({
|
|
358
|
+
step: 8,
|
|
359
|
+
title: "Save CRM config to Maestro",
|
|
360
|
+
type: "automated",
|
|
361
|
+
commands: [
|
|
362
|
+
`gtm exec gtm_provision --arg action=save_config --arg provider=twenty --arg 'config_values={"api_key":"<API_KEY_FROM_STEP_7>","base_url":"https://${serviceName}.up.railway.app"}'`
|
|
363
|
+
],
|
|
364
|
+
notes: "Replace <API_KEY_FROM_STEP_7> with the actual key. " + (args.client_name ? `Will save under client "${args.client_name}".` : "Will save under the active client config.")
|
|
365
|
+
});
|
|
366
|
+
steps.push({
|
|
367
|
+
step: 9,
|
|
368
|
+
title: "Verify CRM connection",
|
|
369
|
+
type: "automated",
|
|
370
|
+
commands: [
|
|
371
|
+
`gtm exec gtm_crm --arg action=find --arg 'query={"email":"test@verify.com"}'`,
|
|
372
|
+
`gtm exec gtm_status --arg provider=twenty`
|
|
373
|
+
],
|
|
374
|
+
notes: "Should return empty results (no error). Status should show twenty as connected."
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
action: "deploy_crm",
|
|
378
|
+
crm: "twenty",
|
|
379
|
+
project: projectName,
|
|
380
|
+
service: serviceName,
|
|
381
|
+
base_url: existingUrl ?? `https://${serviceName}.up.railway.app`,
|
|
382
|
+
already_configured: !!(existingUrl && existingKey),
|
|
383
|
+
total_steps: steps.length,
|
|
384
|
+
cli_steps: steps.filter((s) => s.type === "cli").length,
|
|
385
|
+
manual_steps: steps.filter((s) => s.type === "manual").length,
|
|
386
|
+
steps,
|
|
387
|
+
railway_template_url: "https://railway.app/template/twenty",
|
|
388
|
+
note: "Railway template is the fastest path \u2014 1-click deploy, then only steps 6-9 are needed."
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
export {
|
|
392
|
+
handleProvision
|
|
393
|
+
};
|
|
394
|
+
//# sourceMappingURL=provision-FT5NWN77.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/handlers/provision.ts"],"sourcesContent":["/** Provision handler. Infrastructure provisioning actions.\n * Constraint: No provider registry — uses direct client instantiation from config.\n * Zapmail + PlusVibe are infrastructure services, not discovery/enrichment providers. */\n\nimport { readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\n\nimport { ZapmailClient, PlusVibeClient } from '@maestro/integrations';\nimport { logError } from '@maestro/logging';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport type { HandlerContext } from './index.js';\n\n// ─── Types ───────────────────────────────────────────────\n\ninterface ProvisionArgs {\n action: string;\n domains?: string[];\n name?: string;\n email?: string;\n workspace_id?: string;\n client_context?: {\n id: string;\n email: string;\n name: string;\n source: string;\n };\n provision_id?: string;\n provision_type?: 'full' | 'outreach';\n domain_ids?: string[];\n dmarc_email?: string;\n dmarc_status?: string[];\n mailboxes?: Array<{ username: string; firstName: string; lastName: string }>;\n service_provider?: string;\n provider?: string;\n config_values?: Record<string, string>;\n client_name?: string;\n}\n\n// ─── Client Factories ────────────────────────────────────\n\nfunction getZapmailClient(ctx: HandlerContext): ZapmailClient {\n const config = ctx.session.getActiveContext().config;\n const apiKey = config.providers.zapmail?.api_key ?? process.env.GTM_ZAPMAIL_API_KEY;\n if (!apiKey) throw new Error('Zapmail API key not configured (GTM_ZAPMAIL_API_KEY)');\n const workspaceKey =\n config.providers.zapmail?.workspace_id ?? process.env.GTM_ZAPMAIL_WORKSPACE_KEY;\n const serviceProvider = (process.env.GTM_ZAPMAIL_SERVICE_PROVIDER ?? 'GOOGLE') as\n | 'GOOGLE'\n | 'MICROSOFT';\n return new ZapmailClient({ apiKey, workspaceKey, serviceProvider });\n}\n\nfunction getPlusVibeClient(ctx: HandlerContext): PlusVibeClient {\n const config = ctx.session.getActiveContext().config;\n const apiKey = config.providers.plusvibe?.api_key ?? process.env.GTM_PLUSVIBE_API_KEY;\n if (!apiKey) throw new Error('PlusVibe API key not configured (GTM_PLUSVIBE_API_KEY)');\n const workspaceId =\n config.providers.plusvibe?.workspace_id ?? process.env.GTM_PLUSVIBE_WORKSPACE_ID;\n return new PlusVibeClient(apiKey, workspaceId);\n}\n\nfunction getInfraApiConfig(): { url: string; key: string } {\n const url =\n process.env.GTM_API_URL ?? process.env.GTM_INFRA_API_URL ?? 'https://api.maestrogtm.com';\n const key = process.env.GTM_INFRA_API_KEY;\n if (!key) throw new Error('Infrastructure API key not configured (GTM_INFRA_API_KEY)');\n return { url, key };\n}\n\n// ─── Handler ─────────────────────────────────────────────\n\nexport async function handleProvision(\n args: Record<string, unknown>,\n ctx: HandlerContext\n): Promise<unknown> {\n const a = args as unknown as ProvisionArgs;\n\n switch (a.action) {\n case 'check_domains':\n return checkDomains(a, ctx);\n case 'check_balance':\n return checkBalance(ctx);\n case 'buy_domains':\n return buyDomains(a, ctx);\n case 'list_domains':\n return listDomains(ctx);\n case 'list_mailboxes':\n return listMailboxes(a, ctx);\n case 'setup_dmarc':\n return setupDmarc(a, ctx);\n case 'create_mailboxes':\n return createMailboxes(a, ctx);\n case 'export_mailboxes':\n return exportMailboxes(a, ctx);\n case 'create_plusvibe_client':\n return createPlusVibeClient(a, ctx);\n case 'save_config':\n return saveConfig(a, ctx);\n case 'deploy_crm':\n return deployCrm(a, ctx);\n case 'start_provision':\n return startProvision(a);\n case 'status':\n return getStatus(a);\n default:\n throw new Error(`Unknown provision action: ${a.action}`);\n }\n}\n\n// ─── Existing Actions ────────────────────────────────────\n\nasync function checkDomains(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.domains?.length) throw new Error('domains[] is required for check_domains');\n\n const zapmail = getZapmailClient(ctx);\n const results = await Promise.all(\n args.domains.map(async (domain) => {\n try {\n const result = await zapmail.checkDomain(domain);\n return { domain, ...result };\n } catch (error) {\n return { domain, available: false, error: (error as Error).message };\n }\n })\n );\n return { action: 'check_domains', results };\n}\n\nasync function checkBalance(ctx: HandlerContext) {\n const zapmail = getZapmailClient(ctx);\n const balance = await zapmail.getBalance();\n return { action: 'check_balance', ...balance };\n}\n\nasync function buyDomains(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.domains?.length) throw new Error('domains[] is required for buy_domains');\n\n const zapmail = getZapmailClient(ctx);\n const result = await zapmail.buyDomain({\n domains: args.domains.map((d) => ({ domainName: d, years: 1 })),\n useWallet: true,\n });\n return { action: 'buy_domains', ...result };\n}\n\nasync function startProvision(args: ProvisionArgs) {\n if (!args.client_context) throw new Error('client_context is required for start_provision');\n if (!args.provision_id) throw new Error('provision_id is required for start_provision');\n\n const { url, key } = getInfraApiConfig();\n const provType = args.provision_type ?? 'full';\n\n const body: Record<string, unknown> = {\n clientContext: args.client_context,\n ...(provType === 'outreach'\n ? { outreachProvisionId: args.provision_id }\n : { provisionId: args.provision_id }),\n };\n\n const response = await fetch(`${url}/api/infrastructure/provision`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-infra-key': key,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n logError('provision:start', new Error(`API returned ${response.status}: ${errorBody}`));\n throw new Error(`Provisioning API returned ${response.status}: ${errorBody}`);\n }\n\n const result = await response.json();\n return { action: 'start_provision', ...result };\n}\n\nasync function getStatus(args: ProvisionArgs) {\n if (!args.provision_id) throw new Error('provision_id is required for status');\n\n const { url, key } = getInfraApiConfig();\n\n const response = await fetch(`${url}/api/infrastructure/provision/${args.provision_id}`, {\n headers: { 'x-infra-key': key },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return { action: 'status', found: false, provision_id: args.provision_id };\n }\n throw new Error(`Status API returned ${response.status}`);\n }\n\n const result = await response.json();\n return { action: 'status', ...result };\n}\n\n// ─── New Granular Actions ────────────────────────────────\n\nasync function listDomains(ctx: HandlerContext) {\n const zapmail = getZapmailClient(ctx);\n const result = await zapmail.listDomains({ limit: 100 });\n const domains = (result.domains ?? []).map((d) => ({\n id: d.id,\n domain: d.domain,\n status: d.status,\n mailboxCount: Number(d.assignedMailboxesCount ?? 0),\n dmarcEmail: d.dmarcEmail,\n isWarmedUp: d.isWarmedUp,\n createdAt: d.createdAt,\n }));\n return {\n action: 'list_domains',\n total: result.totalSearchedCount,\n domains,\n };\n}\n\nasync function listMailboxes(args: ProvisionArgs, ctx: HandlerContext) {\n const zapmail = getZapmailClient(ctx);\n const result = await zapmail.listMailboxes({\n contains: args.domains?.[0],\n });\n return { action: 'list_mailboxes', ...result };\n}\n\nasync function setupDmarc(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.domain_ids?.length) throw new Error('domain_ids[] is required for setup_dmarc');\n\n const zapmail = getZapmailClient(ctx);\n const dmarcEmail = args.dmarc_email ?? args.email ?? 'dmarc@maestrogtm.com';\n const status = args.dmarc_status ?? ['p=none'];\n\n await zapmail.setupDmarc({ domainIds: args.domain_ids, email: dmarcEmail, status });\n\n return {\n action: 'setup_dmarc',\n domainCount: args.domain_ids.length,\n email: dmarcEmail,\n status,\n };\n}\n\nasync function createMailboxes(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.domain_ids?.length) throw new Error('domain_ids[] is required for create_mailboxes');\n if (!args.domains?.length)\n throw new Error('domains[] (domain names) is required for create_mailboxes');\n if (!args.mailboxes?.length) throw new Error('mailboxes[] is required for create_mailboxes');\n\n if (args.domain_ids.length !== args.domains.length) {\n throw new Error('domain_ids[] and domains[] must have the same length');\n }\n\n const zapmail = getZapmailClient(ctx);\n\n const scheduleBody: Record<\n string,\n Array<{ domainName: string; mailboxUsername: string; firstName: string; lastName: string }>\n > = {};\n\n for (let i = 0; i < args.domain_ids.length; i++) {\n const domainId = args.domain_ids[i];\n const domainName = args.domains[i];\n scheduleBody[domainId] = args.mailboxes.map((m) => ({\n domainName,\n mailboxUsername: m.username,\n firstName: m.firstName,\n lastName: m.lastName,\n }));\n }\n\n await zapmail.scheduleMailbox(scheduleBody);\n\n const totalCreated = args.domain_ids.length * args.mailboxes.length;\n return {\n action: 'create_mailboxes',\n totalCreated,\n domains: args.domains,\n mailboxes: args.mailboxes.map((m) => m.username),\n };\n}\n\nasync function exportMailboxes(args: ProvisionArgs, ctx: HandlerContext) {\n const zapmail = getZapmailClient(ctx);\n\n if (args.email) {\n await zapmail.registerExportCredentials({\n app: 'PIPL',\n email: args.email,\n password: args.config_values?.password ?? '',\n });\n }\n\n await zapmail.exportMailboxes({ apps: ['PIPL'] });\n\n return { action: 'export_mailboxes', exported: true };\n}\n\nasync function createPlusVibeClient(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.name) throw new Error('name is required for create_plusvibe_client');\n if (!args.email) throw new Error('email is required for create_plusvibe_client');\n\n const plusvibe = getPlusVibeClient(ctx);\n const config = ctx.session.getActiveContext().config;\n const workspaceId =\n args.workspace_id ??\n config.providers.plusvibe?.workspace_id ??\n process.env.GTM_PLUSVIBE_WORKSPACE_ID;\n\n if (!workspaceId) {\n throw new Error(\n 'PlusVibe workspace_id is required — create workspace in PlusVibe dashboard, then set in config'\n );\n }\n\n const client = await plusvibe.createClient({\n name: args.name,\n email: args.email,\n workspaceId,\n });\n\n return {\n action: 'create_plusvibe_client',\n client,\n message: `Created PlusVibe client ${args.email} in workspace ${workspaceId}`,\n };\n}\n\nasync function saveConfig(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.provider) throw new Error('provider is required for save_config');\n if (!args.config_values || Object.keys(args.config_values).length === 0) {\n throw new Error('config_values is required for save_config');\n }\n\n const configDir = join(homedir(), '.gtm');\n const configPath = join(configDir, 'config.yaml');\n\n let config: Record<string, unknown>;\n try {\n const raw = readFileSync(configPath, 'utf-8');\n config = parseYaml(raw) as Record<string, unknown>;\n } catch {\n config = {};\n }\n\n // Ensure structure exists\n if (!config.defaults) config.defaults = {};\n const defaults = config.defaults as Record<string, unknown>;\n if (!defaults.providers) defaults.providers = {};\n const providers = defaults.providers as Record<string, Record<string, string>>;\n\n // Determine client scope\n const clientName = args.client_name ?? ctx.session.getActiveContext().clientName;\n if (clientName && clientName !== 'default') {\n if (!config.clients) config.clients = {};\n const clients = config.clients as Record<string, Record<string, unknown>>;\n if (!clients[clientName]) clients[clientName] = {};\n if (!clients[clientName].providers) clients[clientName].providers = {};\n const clientProviders = clients[clientName].providers as Record<string, Record<string, string>>;\n if (!clientProviders[args.provider]) clientProviders[args.provider] = {};\n Object.assign(clientProviders[args.provider], args.config_values);\n } else {\n if (!providers[args.provider]) providers[args.provider] = {};\n Object.assign(providers[args.provider], args.config_values);\n }\n\n mkdirSync(configDir, { recursive: true });\n writeFileSync(configPath, stringifyYaml(config), 'utf-8');\n\n return {\n action: 'save_config',\n saved: true,\n client: clientName,\n provider: args.provider,\n fields: Object.keys(args.config_values),\n };\n}\n\n// ─── CRM Deployment ─────────────────────────────────────\n\nasync function deployCrm(args: ProvisionArgs, ctx: HandlerContext) {\n if (!args.name) throw new Error('name is required for deploy_crm (client name)');\n\n const clientSlug = args.name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-+$/, '');\n const projectName = `twenty-${clientSlug}`;\n const serviceName = `twenty-crm-${clientSlug}`;\n\n // Check if already configured\n const config = ctx.session.getActiveContext().config;\n const existingUrl = config.providers.twenty?.base_url;\n const existingKey = config.providers.twenty?.api_key;\n\n const steps: Array<{\n step: number;\n title: string;\n type: 'cli' | 'manual' | 'automated';\n commands?: string[];\n notes?: string;\n env_vars?: Record<string, string>;\n }> = [];\n\n // Step 1: Railway project\n steps.push({\n step: 1,\n title: 'Create Railway project',\n type: 'cli',\n commands: [`railway login`, `railway init --name ${projectName}`],\n notes: 'Creates a new Railway project. Skip if project already exists.',\n });\n\n // Step 2: Deploy Twenty from Docker\n steps.push({\n step: 2,\n title: 'Deploy Twenty CRM service',\n type: 'cli',\n commands: [\n `railway add --service ${serviceName}`,\n `railway variables set SERVER_URL=https://${serviceName}.up.railway.app -s ${serviceName}`,\n `railway variables set FRONT_BASE_URL=https://${serviceName}.up.railway.app -s ${serviceName}`,\n `railway variables set IS_SIGN_UP_DISABLED=true -s ${serviceName}`,\n `railway variables set NODE_ENV=production -s ${serviceName}`,\n ],\n notes:\n 'Uses Twenty Docker image twentycrm/twenty:latest. Alternatively deploy from Railway template: https://railway.app/template/twenty',\n env_vars: {\n SERVER_URL: `https://${serviceName}.up.railway.app`,\n FRONT_BASE_URL: `https://${serviceName}.up.railway.app`,\n IS_SIGN_UP_DISABLED: 'true',\n },\n });\n\n // Step 3: Database\n steps.push({\n step: 3,\n title: 'Add PostgreSQL database',\n type: 'cli',\n commands: [\n `railway add --database postgres --service ${serviceName}-db`,\n `railway variables set DATABASE_URL=\\${{${serviceName}-db.DATABASE_URL}} -s ${serviceName}`,\n ],\n notes: 'Twenty requires PostgreSQL 15+. Railway provisions this automatically.',\n });\n\n // Step 4: Redis\n steps.push({\n step: 4,\n title: 'Add Redis cache',\n type: 'cli',\n commands: [\n `railway add --database redis --service ${serviceName}-redis`,\n `railway variables set REDIS_URL=\\${{${serviceName}-redis.REDIS_URL}} -s ${serviceName}`,\n ],\n notes: 'Required for Twenty job queue and caching.',\n });\n\n // Step 5: Deploy\n steps.push({\n step: 5,\n title: 'Deploy and wait for health',\n type: 'cli',\n commands: [\n `railway up -s ${serviceName} --image twentycrm/twenty:latest`,\n `# Wait ~2-3 minutes for first deploy`,\n `# Verify: curl https://${serviceName}.up.railway.app/healthz`,\n ],\n notes: 'First deploy runs database migrations automatically. May take 2-3 minutes.',\n });\n\n // Step 6: Admin account (manual)\n steps.push({\n step: 6,\n title: 'Create admin account',\n type: 'manual',\n notes:\n `Open https://${serviceName}.up.railway.app in browser. ` +\n 'Sign up is enabled on first visit (even with IS_SIGN_UP_DISABLED). ' +\n `Create an admin account with email: ${args.email ?? `admin@${clientSlug}.com`}. ` +\n 'After first account creation, sign-up is locked.',\n });\n\n // Step 7: API key (manual)\n steps.push({\n step: 7,\n title: 'Generate API key',\n type: 'manual',\n notes:\n `In Twenty dashboard → Settings → API Keys → Generate. ` +\n 'Copy the API key — needed for Maestro configuration.',\n });\n\n // Step 8: Save config (automated)\n steps.push({\n step: 8,\n title: 'Save CRM config to Maestro',\n type: 'automated',\n commands: [\n `gtm exec gtm_provision --arg action=save_config --arg provider=twenty --arg 'config_values={\"api_key\":\"<API_KEY_FROM_STEP_7>\",\"base_url\":\"https://${serviceName}.up.railway.app\"}'`,\n ],\n notes:\n 'Replace <API_KEY_FROM_STEP_7> with the actual key. ' +\n (args.client_name\n ? `Will save under client \"${args.client_name}\".`\n : 'Will save under the active client config.'),\n });\n\n // Step 9: Verify\n steps.push({\n step: 9,\n title: 'Verify CRM connection',\n type: 'automated',\n commands: [\n `gtm exec gtm_crm --arg action=find --arg 'query={\"email\":\"test@verify.com\"}'`,\n `gtm exec gtm_status --arg provider=twenty`,\n ],\n notes: 'Should return empty results (no error). Status should show twenty as connected.',\n });\n\n return {\n action: 'deploy_crm',\n crm: 'twenty',\n project: projectName,\n service: serviceName,\n base_url: existingUrl ?? `https://${serviceName}.up.railway.app`,\n already_configured: !!(existingUrl && existingKey),\n total_steps: steps.length,\n cli_steps: steps.filter((s) => s.type === 'cli').length,\n manual_steps: steps.filter((s) => s.type === 'manual').length,\n steps,\n railway_template_url: 'https://railway.app/template/twenty',\n note: 'Railway template is the fastest path — 1-click deploy, then only steps 6-9 are needed.',\n };\n}\n"],"mappings":"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","names":[]}
|