Skip to content

feat: add Local Lead Scout extension - #27

Merged
rgxdev merged 13 commits into
mainfrom
claude/local-lead-scout-extension-61e2n4
Jul 6, 2026
Merged

rgxdev merged 13 commits into
mainfrom
claude/local-lead-scout-extension-61e2n4

Conversation

@rgxdev

@rgxdev rgxdev commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds Local Lead Scout, an installable NyxelOS extension that finds local businesses with no website via compliant sources, generates a website prototype concept and outreach email draft through the existing agent/task system, and only sends after explicit human approval.

  • Providers: manual_csv (CSV import), osm_overpass (free, no key, Nominatim geocoding + Overpass API), google_places_api (official Text Search API, minimal FieldMask, disabled without a configured key), custom_api (minimal generic placeholder). No Google Maps scraping/DOM automation anywhere.
  • Data model: campaign, per-provider source config, scan runs, leads, prototypes, outreach drafts, suppression list, and encrypted email settings — both Postgres and SQLite.
  • Compliance: secrets encrypted at rest and never returned raw to the client; default draft_only outreach mode; no email address is ever guessed; suppression list + per-campaign/daily send limits; atomic compare-and-swap status transitions prevent double-send; a sent lead requires an explicit reset action before it can be emailed again; every scan/prototype/draft/send is audit-logged.
  • UI: campaign list/detail, CSV scan control, leads table with missing-website/status badges, lead detail with prototype + email draft review and approve/reject/send actions, scan history, suppression list, and an email settings panel (SMTP/Resend/Mailgun/custom, dry-run mode on by default).

What works

  • Install/enable the extension per workspace via the existing marketplace pattern.
  • Create a campaign (postal code, radius, niches, max results, min confidence, provider).
  • Run a manual_csv scan end-to-end — verified live in a browser (install → create campaign → paste CSV → scan → leads list with correct missing_website/has_website badges → mark lead reviewed → email settings save/prefill/test-connection/send-test in dry-run mode), zero console errors.
  • Prototype and email-draft generation dispatch to a lazily-provisioned content agent through the existing task/agent-run flow (not a bespoke LLM pipeline).
  • Approval-gated send: draft-only mode blocks sending, rejected drafts never send, missing email blocks send, suppressed email/domain blocks send, per-campaign and daily send limits enforced, duplicate send blocked, explicit reset required to resend.
  • Targeted tests (38 passing): CSV/OSM normalization and missing-website/email detection, secret-at-rest encryption + client redaction, scan dedup/reconciliation, prototype/draft status gating, and the full approval-gated send flow.

Remaining risks / deferred

  • osm_overpass and google_places_api HTTP calls to Nominatim/Overpass/Places couldn't be live-verified from this sandbox (network policy blocks those hosts) — request/response shapes follow each service's documented API but are otherwise unverified end-to-end.
  • custom_api is intentionally a minimal generic REST placeholder, per the task's scope.
  • No pagination/rate-limit backoff beyond a single delay between Google Places per-niche queries.
  • UI is functional but intentionally not visually polished, per instructions.

Test plan

  • bun run --cwd packages/db typecheck
  • bun run --cwd apps/server typecheck
  • bun run --cwd apps/web typecheck
  • bunx biome check on all touched files
  • bun test — 38/38 passing across the new lead-scout test files + extended secrets-encryption test
  • Live browser verification of the full manual_csv flow (see summary above)

Generated by Claude Code

claude added 13 commits July 3, 2026 18:41
Registers "local-lead-scout" in the extension catalog and sidebar icon
map so it can be installed/enabled per workspace like any other
extension. Feature logic lands in follow-up commits.
Adds the DB tables backing Local Lead Scout: campaigns, per-provider
source config, scan runs, leads, prototypes, outreach drafts, and a
suppression list, for both the Postgres and SQLite dialects. Schema
only — repo layer and feature logic land in follow-up commits.
Adds typed record interfaces and dialect-agnostic CRUD methods for
campaigns, source configs, scan runs, leads, prototypes, outreach
drafts, suppressions, and encrypted email settings — implemented for
both Postgres and SQLite. Lead/outreach-draft status transitions use
atomic compare-and-swap updates (claimLeadScoutLeadStatus,
claimLeadScoutOutreachDraftStatus) so approval/send can never race.
Adds a workspace-scoped email settings module (SMTP/Resend/Mailgun/
custom) with encrypted-at-rest credentials, a client-safe redacted
shape, dry-run mode (on by default), a connection test, a test-send
action, and a daily send-limit reservation used by the outreach flow.
Nothing is dispatched — only settings storage and transport helpers.
Adds the LeadSourceProvider interface (searchBusinesses /
getBusinessDetails / normalizeBusiness / sourcePolicy) plus three
compliant adapters: manual_csv (user-supplied CSV import),
google_places_api (official Places API Text Search, New — minimal
FieldMask, no Maps scraping, disabled without a configured key), and a
minimal custom_api placeholder. Also adds a shared Nominatim geocoding
helper (postal code -> lat/lon, rate-limited to the service's 1 req/s
policy) used by google_places_api and, in a follow-up, osm_overpass.
Adds the osm_overpass adapter: geocodes the campaign's postal code via
Nominatim (rate-limited to its 1 req/s policy), queries the public
Overpass API for shop/amenity/office/craft nodes within the resulting
bounding box (descriptive User-Agent, capped results, no scraping),
and normalizes tags into a lead — missing_website only when no
website/contact:website tag is present, with a separate missing_email
note since OSM rarely carries contact emails. Also adds a small
provider registry mapping LeadScoutProvider -> adapter.

Nominatim/Overpass calls aren't reachable from this sandbox's network
policy, so the live request/response shape is unverified here — the
query format follows each service's documented API.
runLeadScoutScan dispatches a campaign's configured provider, filters
by minConfidence, reconciles results against already-ingested leads
(by campaign+provider+sourceId) so re-scans update in place instead of
duplicating, and records a scan_run with counts/summary — mirrors
runSeoAnalysis's audit+notify shape. Scheduled campaigns are polled
alongside due SEO projects; manual_csv campaigns can't run unattended
(no CSV to read), so a scheduled one auto-disables its schedule.
Dispatches the campaign's content agent (auto-provisioned once, no
special tools needed) through the existing task/agent-run flow to
draft a lightweight website concept per lead, then — only once that
prototype is explicitly approved — an outreach email body. Sender
identity and opt-out text are appended programmatically from email
settings rather than left to the model. Lead status moves through an
explicit state machine (new/reviewed -> prototype_requested/ready ->
email_drafted) via the atomic claim added earlier, so a lead can't be
double-dispatched or skip the review gate when
requireApprovalBeforePrototype is set.
Adds approve/reject/send for outreach drafts: approving only unlocks a
send attempt (nothing is dispatched), sending checks outreach mode,
requires a real (never guessed) lead email, checks the suppression
list by email and domain, enforces per-campaign and daily send limits,
and claims both the draft and lead atomically so a duplicate click
can't send twice. A sent lead is permanently done until an explicit
reset action reopens it. Also fixes lead-scout-email.ts to check
dryRunMode before validating provider credentials, so dry-run mode
actually works without live credentials configured.
Wires campaigns, source configs, scans, leads, prototypes, outreach
drafts, suppressions, and email settings into a leadScout tRPC
namespace, following the existing workspaceProcedure/protectedProcedure
+ requireEntityWorkspaceOwner pattern. Client-facing reads through
toClientSafeLeadScoutSourceConfig / toClientSafeLeadScoutEmailSettings
so encrypted credentials never leave the server. Also applies biome
formatting to the lead-scout modules added in prior commits.
Adds the extension page (campaigns, run-scan/CSV-import control, leads
table, lead detail with prototype/draft review and approve/reject/send
actions, scan history, suppression list, and email settings) plus the
hand-written trpc.ts client contract for the new leadScout router.
Wires it into the shared extensions/[key] page.

Verified end-to-end in a real browser: install -> create campaign ->
CSV scan -> lead detail -> mark reviewed -> email settings save/test.
Fixes a bug found along the way: the email settings form initialized
its fields from the query result before it had loaded, so previously
saved settings appeared blank until edited — now synced in via a
targeted effect keyed on the settings row id.
Targeted tests for lead normalization (manual_csv CSV parsing,
osm_overpass tag mapping), missing-website/missing-email detection,
secret stripping (source config apiKey, email settings credentials
encrypted at rest and redacted client-side), scan dedup/reconciliation,
prototype/draft status gating, and the full approval-gated send flow
(draft-only mode, no-guessed-email, suppression, per-campaign/daily
limits, duplicate-send prevention, explicit resend reset).
Whitespace-only fixes: schema/repo files added in earlier commits were
never run through biome, and extensions.ts predates the project's
space-indent convention (its diff here is reformatting only, no
logic change). Also drops an unnecessary non-null assertion in a new
test by reusing the campaign already in scope.
@rgxdev rgxdev changed the title Add Local Lead Scout extension feat: add Local Lead Scout extension Jul 3, 2026
@rgxdev
rgxdev marked this pull request as ready for review July 3, 2026 19:50
@rgxdev
rgxdev merged commit 5b152cc into main Jul 6, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants