Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions control/.env.gtm.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# OpenClaw Phase 10 — GTM Production Activation
# Copy relevant keys to control/.env.whatsapp (server-only, never commit)

# === REQUIRED: Lead Discovery ===
TAVILY_API_KEY=your_tavily_key_here
SERPER_API_KEY=your_serper_key_here
APOLLO_API_KEY=your_apollo_key_here

# === Enrichment (recommended for decision makers) ===
LEAD_ENRICHMENT_MODE=live
HUNTER_API_KEY=your_hunter_key_here
ENABLE_HUNTER=true
ENABLE_APOLLO=true

# === GTM Activation Tuning ===
GTM_DISCOVERY_TARGET=100
GTM_OUTREACH_TOP_N=20
OPENCLAW_ASYNC_TASKS=1
OPENCLAW_PROJECT=zonforge

# Lead generation batch size (per segment run)
LEAD_GENERATION_MIN_PROSPECTS=100
LEAD_GENERATION_ENRICH_BATCH=50
LEAD_GENERATION_ENRICH_DELAY_MS=300

# === Safety: no auto-send ===
# Gmail/HubSpot sends require explicit /task approve + send commands
GMAIL_MODE=mock
HUBSPOT_MODE=mock
19 changes: 14 additions & 5 deletions control/src/commands/generate-post.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@ export async function runGeneratePostCommand(config: OpenClawConfig): Promise<Co
const result = await runWhitelistedScript(config, 'generate-post')
const ok = result.exitCode === 0

let message = ok ? 'LinkedIn post draft generated.' : 'Post generation failed.'
try {
const parsed = JSON.parse(result.stdout.trim()) as { message?: string }
if (parsed.message) message = parsed.message
} catch {
if (result.stdout.trim()) message = result.stdout.trim()
}

return {
status: ok ? 'success' : 'error',
message: [
ok ? 'LinkedIn post draft generated.' : 'Post generation failed.',
'',
summarizeScriptOutput(result),
message,
'',
ok ? 'Use /latest_outputs to view the draft file.' : '',
].filter(Boolean).join('\n'),
ok ? '' : summarizeScriptOutput(result),
ok ? 'Use /latest_outputs to view the artifact file.' : '',
]
.filter(Boolean)
.join('\n'),
data: {
exitCode: result.exitCode,
durationMs: result.durationMs,
Expand Down
10 changes: 10 additions & 0 deletions control/src/commands/help.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export function runHelpCommand(config?: OpenClawConfig): CommandResult {
'/task revenue report | marketing report | lead report | kpi dashboard',
'/task list approvals | run revenue engine',
'',
'Phase 10 — GTM Production Activation:',
'/task run gtm activation — discover 100 leads/segment, score, outreach drafts, CRM, report',
'npm run openclaw:gtm:activate:dry — local dry-run (no API keys)',
'npm run openclaw:gtm:activate — production activation (requires API keys on server)',
'',
'Phase 10.1 — Gemini LinkedIn Content:',
'/task create linkedin post about SOC automation',
'/task run linkedin post workflow about alert fatigue',
'/generate_post — Gemini 2.5 Flash LinkedIn post (saved to projects/zonforge/output/linkedin/)',
'',
'/task show company status | what should I do today',
'/run_daily — execute daily GTM automation',
'/daily — run daily GTM + founder brief',
Expand Down
2 changes: 2 additions & 0 deletions integrations/revenue-engine/src/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type RevenueAuditAction =
| 'lead_discovery'
| 'outreach_created'
| 'marketing_created'
| 'linkedin_content_started'
| 'linkedin_content_generated'
| 'crm_updated'
| 'support_triaged'
| 'report_generated'
Expand Down
99 changes: 62 additions & 37 deletions integrations/revenue-engine/src/discovery-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,43 @@ import { formatSegmentLabel, resolveSegmentProfile } from './lead-discovery.js'
import { saveDiscoveryRun, upsertPipelineLead } from './store.js'
import type { LeadSegment, RevenueEnginePaths, RevenueTaskRequest } from './types.js'

function priorityToScore(priority: string): number {
if (priority === 'High') return 85
if (priority === 'Medium') return 65
return 45
}

export async function runLeadDiscovery(
paths: RevenueEnginePaths,
req: RevenueTaskRequest,
segment: LeadSegment,
targetLeads?: number,
): Promise<{ ok: boolean; message: string; data?: Record<string, unknown> }> {
const profile = resolveSegmentProfile(segment)
const label = formatSegmentLabel(segment)
const project = loadProjectContext(req.root)
const leadPaths = resolveLeadGenerationPaths(req.root, project.activeProjectId)

const result = await runLeadGeneration({
root: req.root,
outputDir: leadPaths.outputDir,
kpiFile: leadPaths.kpiFile,
env: process.env,
projectId: project.activeProjectId,
productName: project.profile.productName,
projectProfile: profile,
})
const prevMin = process.env['LEAD_GENERATION_MIN_PROSPECTS']
if (targetLeads && targetLeads > 0) {
process.env['LEAD_GENERATION_MIN_PROSPECTS'] = String(targetLeads)
}

let result
try {
result = await runLeadGeneration({
root: req.root,
outputDir: leadPaths.outputDir,
kpiFile: leadPaths.kpiFile,
env: process.env,
projectId: project.activeProjectId,
productName: project.profile.productName,
projectProfile: profile,
})
} finally {
if (prevMin !== undefined) process.env['LEAD_GENERATION_MIN_PROSPECTS'] = prevMin
else if (targetLeads) delete process.env['LEAD_GENERATION_MIN_PROSPECTS']
}

if (!result.ok) {
return {
Expand Down Expand Up @@ -56,40 +74,47 @@ export async function runLeadDiscovery(
if (existsSync(result.jsonPath)) {
try {
const { readFileSync } = await import('node:fs')
const prospects = JSON.parse(readFileSync(result.jsonPath, 'utf8')) as Array<{
company?: string
domain?: string
priority?: string
score?: number
contacts?: Array<{ name?: string; email?: string; title?: string }>
}>

for (const prospect of prospects.slice(0, 5)) {
if (!prospect.domain) continue
topDomains.push(prospect.domain)

const contact = prospect.contacts?.[0]
const payload = JSON.parse(readFileSync(result.jsonPath, 'utf8')) as {
prospects?: Array<{
company?: string
website?: string
priority?: string
contact_name?: string
email?: string
job_title?: string
}>
}

const prospects = payload.prospects ?? []

for (const prospect of prospects) {
const domain = prospect.website?.replace(/^https?:\/\//, '').replace(/\/.*$/, '') ?? ''
if (!domain && !prospect.company) continue
topDomains.push(domain || prospect.company || '')

upsertPipelineLead(paths, {
company: prospect.company ?? prospect.domain,
domain: prospect.domain,
company: prospect.company ?? domain,
domain: domain || `${prospect.company?.toLowerCase().replace(/\s+/g, '-')}.com`,
segment,
stage: 'discovered',
score: prospect.score ?? (prospect.priority === 'high' ? 85 : prospect.priority === 'medium' ? 65 : 45),
decisionMaker: contact?.name,
email: contact?.email,
title: contact?.title,
score: priorityToScore(prospect.priority ?? 'Low'),
decisionMaker: prospect.contact_name,
email: prospect.email,
title: prospect.job_title,
source: `discovery:${segment}`,
})
}

if (prospect.priority === 'high' && prospect.domain) {
const enrich = await handleEnrichmentTask({
task: `enrich domain ${prospect.domain}`,
senderId: req.senderId,
channel: req.channel,
root: req.root,
})
if (enrich.ok) enrichedCount += 1
}
for (const prospect of prospects.filter(p => p.priority === 'High').slice(0, 10)) {
const domain = prospect.website?.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
if (!domain) continue
const enrich = await handleEnrichmentTask({
task: `enrich domain ${domain}`,
senderId: req.senderId,
channel: req.channel,
root: req.root,
})
if (enrich.ok) enrichedCount += 1
}
} catch {
// Non-fatal — discovery output still valid
Expand Down
Loading