Spaces:
Running
Running
iDevBuddy
fix: Resolve strict TypeScript database typings and allow production build warnings bypass
5307e67 | /** | |
| * Slack Command Handler β Bidirectional Intelligence | |
| * | |
| * Handles incoming Slack slash commands and messages. | |
| * Uses LLM for natural language understanding when needed. | |
| * | |
| * Commands: | |
| * /discover β asks clarifying questions | |
| * /discover region:UK β direct run with params | |
| * /leads β show today's qualified leads | |
| * /lead [company] β full lead details | |
| * /status β system status | |
| * /pause β pause automatic runs | |
| * /resume β resume automatic runs | |
| * /quota [number] β set today's quota | |
| * /quota [number] always β permanent change | |
| */ | |
| import { getSupabaseClient } from "../shared/supabase/client"; | |
| import { setQuotaOverride, isSystemPaused } from "../discovery/lib/territory-manager"; | |
| import { sendClarifyingQuestions } from "./slack-service"; | |
| import { logger } from "../shared/utils/logger"; | |
| export interface SlackCommand { | |
| command: string; | |
| text: string; | |
| userId: string; | |
| channelId: string; | |
| } | |
| /** | |
| * Route incoming slash commands. | |
| */ | |
| export async function handleSlackCommand(cmd: SlackCommand): Promise<string> { | |
| const { command, text } = cmd; | |
| const args = text.trim().toLowerCase(); | |
| switch (command) { | |
| case "/discover": | |
| return handleDiscover(args, cmd); | |
| case "/leads": | |
| return handleLeads(); | |
| case "/lead": | |
| return handleLeadDetail(text); | |
| case "/status": | |
| return handleStatus(); | |
| case "/pause": | |
| return handlePause(); | |
| case "/resume": | |
| return handleResume(); | |
| case "/quota": | |
| return handleQuota(text); | |
| default: | |
| return `Unknown command: ${command}`; | |
| } | |
| } | |
| // βββ /discover βββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleDiscover(args: string, cmd: SlackCommand): Promise<string> { | |
| // Parse structured params if provided | |
| const params = parseParams(args); | |
| if (params.region && params.industry) { | |
| // Direct run β no questions needed | |
| const { manualDiscoveryTask } = await import("../discovery/trigger-tasks/manual-discovery"); | |
| await manualDiscoveryTask.trigger({ | |
| region: params.region.toUpperCase() as any, | |
| industry: params.industry, | |
| maxCompanies: parseInt(params.max ?? "20", 10), | |
| triggeredBy: `slack:${cmd.userId}`, | |
| }); | |
| return `π Manual discovery started:\nβ’ Region: ${params.region.toUpperCase()}\nβ’ Industry: ${params.industry}\nβ’ Max: ${params.max ?? 20}\nI'll notify you when complete.`; | |
| } | |
| if (args && !params.region) { | |
| // Natural language: "aj China pe kaam karo" | |
| // Ask clarifying questions | |
| await sendClarifyingQuestions(args, [ | |
| { | |
| question: "Which cities?", | |
| options: ["All major cities", "Capital only", "Let me specify..."], | |
| }, | |
| { | |
| question: "Which industry?", | |
| options: ["Healthcare (dental, medical)", "Manufacturing", "Technology/SaaS", "All service businesses"], | |
| }, | |
| { | |
| question: "How many leads?", | |
| options: ["5 (quick)", "10 (standard)", "20 (deep scan)"], | |
| }, | |
| ]); | |
| return "I've posted clarifying questions βοΈ"; | |
| } | |
| // No args β interactive mode | |
| return "Usage:\nβ’ `/discover region:UK industry:dental` β direct run\nβ’ `/discover China pe kaam karo` β natural language\nβ’ `/discover` β this help message"; | |
| } | |
| // βββ /leads ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleLeads(): Promise<string> { | |
| const db = getSupabaseClient(); | |
| const today = new Date(); | |
| today.setHours(0, 0, 0, 0); | |
| const { data: leads } = await db | |
| .from("lead_scores") | |
| .select(` | |
| total_score, tier, | |
| companies (name, domain, industry, city, service_match), | |
| contacts (full_name, email, email_verified, linkedin_personal_url) | |
| `) | |
| .gte("created_at", today.toISOString()) | |
| .order("total_score", { ascending: false }); | |
| if (!leads?.length) return "No leads found today yet."; | |
| const lines = leads.map((l: any, i: number) => { | |
| const emoji = l.tier === "hot" ? "π₯" : l.tier === "warm" ? "β " : "π"; | |
| const email = l.contacts?.email_verified ? "π§β" : l.contacts?.email ? "π§" : "β"; | |
| const li = l.contacts?.linkedin_personal_url ? "πΌ" : "β"; | |
| return `${emoji} ${l.total_score} | ${l.companies?.name ?? "?"} | ${l.companies?.industry ?? "?"} | ${l.companies?.city ?? "?"} | ${email} ${li} | ${l.companies?.service_match ?? "β"}`; | |
| }); | |
| return `*Today's Leads (${leads.length}):*\n\n` + | |
| `Score | Company | Industry | City | Channels | Service\n` + | |
| `${"β".repeat(60)}\n` + | |
| lines.join("\n") + | |
| `\n\nType \`/lead [company name]\` for full details`; | |
| } | |
| // βββ /lead [company] ββββββββββββββββββββββββββββββββββββββββ | |
| async function handleLeadDetail(companySearch: string): Promise<string> { | |
| if (!companySearch.trim()) return "Usage: `/lead ABC Dental`"; | |
| const db = getSupabaseClient(); | |
| const { data: companies } = await db | |
| .from("companies") | |
| .select("*") | |
| .ilike("name", `%${companySearch.trim()}%`) | |
| .limit(1); | |
| if (!companies?.length) return `No company found matching "${companySearch}"`; | |
| const company = companies[0]; | |
| const { data: contacts } = await db.from("contacts").select("*").eq("company_id", company.id) as any; | |
| const { data: scores } = await db.from("lead_scores").select("*").eq("company_id", company.id).limit(1) as any; | |
| const { data: profiles } = await db.from("lead_profiles").select("*").eq("company_id", company.id).limit(1) as any; | |
| const score = (scores as any)?.[0]; | |
| const profile = (profiles as any)?.[0]; | |
| const contact = (contacts as any)?.[0]; | |
| return `*${company.name}*\n` + | |
| `Domain: ${company.domain}\n` + | |
| `Industry: ${company.industry ?? "?"} Β· Employees: ${company.employee_count ?? "?"}\n` + | |
| `City: ${company.city ?? "?"} Β· ${company.country ?? "?"}\n` + | |
| `Service Match: ${company.service_match ?? "β"}\n` + | |
| `LinkedIn: ${company.linkedin_url ?? "β"}\n\n` + | |
| `*Score:* ${score?.total_score ?? "?"}/100 β ${score?.tier?.toUpperCase() ?? "?"}\n` + | |
| ` Fit: ${score?.company_fit ?? "?"}/25 Β· AI: ${score?.ai_readiness ?? "?"}/20 Β· Service: ${score?.service_match_score ?? "?"}/20\n` + | |
| ` Contact: ${score?.decision_maker ?? "?"}/20 Β· Timing: ${score?.timing_score ?? "?"}/15\n\n` + | |
| `*Profile:*\n${profile?.profile_summary ?? "No profile yet"}\n` + | |
| `Pain: ${(profile?.pain_points ?? []).join(", ")}\n` + | |
| `Angle: _${profile?.outreach_angle ?? "?"}_\n\n` + | |
| `*Contact:* ${contact?.full_name ?? "?"} β ${contact?.title ?? "?"}\n` + | |
| ` Email: ${contact?.email ?? "β"} ${contact?.email_verified ? "β" : ""}\n` + | |
| ` LinkedIn: ${contact?.linkedin_personal_url ?? "β"}\n` + | |
| ` Social: ${JSON.stringify(contact?.social_profiles ?? {})}`; | |
| } | |
| // βββ /status βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleStatus(): Promise<string> { | |
| const db = getSupabaseClient(); | |
| const paused = await isSystemPaused(); | |
| const { data: quotaConfig } = await db.from("system_config").select("value").eq("key", "daily_quota").single(); | |
| const quota = quotaConfig?.value; | |
| const { data: territory } = await db.from("system_config").select("value").eq("key", "current_territory").single(); | |
| const pos = territory?.value; | |
| const { data: todayRuns } = await db | |
| .from("discovery_runs") | |
| .select("status, leads_qualified") | |
| .gte("ran_at", new Date(new Date().setHours(0, 0, 0, 0)).toISOString()); | |
| const todayLeads = todayRuns?.reduce((sum: number, r: any) => sum + (r.leads_qualified ?? 0), 0) ?? 0; | |
| return `*System Status*\n` + | |
| `State: ${paused ? "βΈοΈ PAUSED" : "βΆοΈ RUNNING"}\n` + | |
| `Daily Quota: ${(quota as any)?.today_override ?? (quota as any)?.default ?? 10}\n` + | |
| `Leads Today: ${todayLeads}\n` + | |
| `Current Territory: ${(pos as any)?.countryCode ?? "?"} city#${(pos as any)?.cityIndex ?? 0}\n` + | |
| `Runs Today: ${todayRuns?.length ?? 0}`; | |
| } | |
| // βββ /pause, /resume βββββββββββββββββββββββββββββββββββββββββ | |
| async function handlePause(): Promise<string> { | |
| const db = getSupabaseClient(); | |
| await db.from("system_config").update({ | |
| value: { enabled: true, paused: true, paused_by: "slack" }, | |
| updated_at: new Date().toISOString(), | |
| } as any).eq("key", "auto_mode"); | |
| return "βΈοΈ System paused. Automatic runs will not start.\nType `/resume` to restart."; | |
| } | |
| async function handleResume(): Promise<string> { | |
| const db = getSupabaseClient(); | |
| await db.from("system_config").update({ | |
| value: { enabled: true, paused: false, paused_by: null }, | |
| updated_at: new Date().toISOString(), | |
| } as any).eq("key", "auto_mode"); | |
| return "βΆοΈ System resumed. Next automatic run will proceed on schedule."; | |
| } | |
| // βββ /quota ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleQuota(text: string): Promise<string> { | |
| const parts = text.trim().split(/\s+/); | |
| const num = parseInt(parts[0], 10); | |
| if (isNaN(num) || num < 1 || num > 100) { | |
| return "Usage: `/quota 15` (today only) or `/quota 15 always` (permanent)"; | |
| } | |
| const permanent = parts[1] === "always" || parts[1] === "permanent"; | |
| await setQuotaOverride(num, permanent); | |
| return permanent | |
| ? `β Daily quota permanently set to ${num} leads/day` | |
| : `β Today's quota set to ${num} leads. Tomorrow back to default.`; | |
| } | |
| // βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function parseParams(text: string): Record<string, string> { | |
| const params: Record<string, string> = {}; | |
| const matches = text.matchAll(/(\w+):(\S+)/g); | |
| for (const match of matches) { | |
| params[match[1]] = match[2]; | |
| } | |
| return params; | |
| } | |