clienttarget-python / src /slack /slack-commands.ts
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;
}