franco 0.5.0__tar.gz
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.
- franco-0.5.0/.env.example +120 -0
- franco-0.5.0/CHANGELOG.md +304 -0
- franco-0.5.0/LICENSE +22 -0
- franco-0.5.0/MANIFEST.in +7 -0
- franco-0.5.0/PKG-INFO +706 -0
- franco-0.5.0/README.md +666 -0
- franco-0.5.0/franco/__init__.py +19 -0
- franco-0.5.0/franco/__main__.py +6 -0
- franco-0.5.0/franco/agents/__init__.py +0 -0
- franco-0.5.0/franco/agents/analyst.py +187 -0
- franco-0.5.0/franco/agents/handler.py +795 -0
- franco-0.5.0/franco/agents/scout.py +1873 -0
- franco-0.5.0/franco/agents/sender.py +640 -0
- franco-0.5.0/franco/agents/writer.py +843 -0
- franco-0.5.0/franco/brain.py +699 -0
- franco-0.5.0/franco/cli.py +1276 -0
- franco-0.5.0/franco/codex.py +1531 -0
- franco-0.5.0/franco/collectors/__init__.py +9 -0
- franco-0.5.0/franco/collectors/discover.py +913 -0
- franco-0.5.0/franco/collectors/profile.py +517 -0
- franco-0.5.0/franco/config.py +591 -0
- franco-0.5.0/franco/dashboard.py +1634 -0
- franco-0.5.0/franco/export.py +125 -0
- franco-0.5.0/franco/funnels.py +176 -0
- franco-0.5.0/franco/gate.py +121 -0
- franco-0.5.0/franco/importer.py +495 -0
- franco-0.5.0/franco/integrations/__init__.py +8 -0
- franco-0.5.0/franco/integrations/calendar.py +26 -0
- franco-0.5.0/franco/integrations/email_finder.py +614 -0
- franco-0.5.0/franco/integrations/gmail.py +340 -0
- franco-0.5.0/franco/integrations/instantly.py +299 -0
- franco-0.5.0/franco/integrations/linkedin.py +433 -0
- franco-0.5.0/franco/integrations/mail_provider.py +101 -0
- franco-0.5.0/franco/integrations/quota.py +196 -0
- franco-0.5.0/franco/integrations/smtp_mail.py +191 -0
- franco-0.5.0/franco/integrations/tech_detect.py +92 -0
- franco-0.5.0/franco/main.py +426 -0
- franco-0.5.0/franco/models/__init__.py +16 -0
- franco-0.5.0/franco/models/campaign.py +44 -0
- franco-0.5.0/franco/models/company.py +47 -0
- franco-0.5.0/franco/models/conversation.py +56 -0
- franco-0.5.0/franco/models/prospect.py +58 -0
- franco-0.5.0/franco/outreach.py +199 -0
- franco-0.5.0/franco/paths.py +84 -0
- franco-0.5.0/franco/pipeline.py +129 -0
- franco-0.5.0/franco/setup.py +723 -0
- franco-0.5.0/franco/signals.py +331 -0
- franco-0.5.0/franco/state.py +2179 -0
- franco-0.5.0/franco/trainer.py +824 -0
- franco-0.5.0/franco/usage.py +27 -0
- franco-0.5.0/franco/web/app.css +595 -0
- franco-0.5.0/franco/web/app.js +1602 -0
- franco-0.5.0/franco/web/fonts/geist-mono.woff2 +0 -0
- franco-0.5.0/franco/web/fonts/geist.woff2 +0 -0
- franco-0.5.0/franco/web/fonts/newsreader.woff2 +0 -0
- franco-0.5.0/franco/web/index.html +422 -0
- franco-0.5.0/franco.egg-info/PKG-INFO +706 -0
- franco-0.5.0/franco.egg-info/SOURCES.txt +115 -0
- franco-0.5.0/franco.egg-info/dependency_links.txt +1 -0
- franco-0.5.0/franco.egg-info/entry_points.txt +2 -0
- franco-0.5.0/franco.egg-info/requires.txt +19 -0
- franco-0.5.0/franco.egg-info/top_level.txt +1 -0
- franco-0.5.0/franco.yaml +101 -0
- franco-0.5.0/prompts/handler.md +36 -0
- franco-0.5.0/prompts/scout.md +36 -0
- franco-0.5.0/prompts/system.md +21 -0
- franco-0.5.0/prompts/writer.md +59 -0
- franco-0.5.0/pyproject.toml +64 -0
- franco-0.5.0/requirements.in +22 -0
- franco-0.5.0/requirements.txt +428 -0
- franco-0.5.0/setup.cfg +4 -0
- franco-0.5.0/setup.py +45 -0
- franco-0.5.0/skills/README.md +48 -0
- franco-0.5.0/skills/account_navigation.md +108 -0
- franco-0.5.0/skills/email_frameworks.md +301 -0
- franco-0.5.0/skills/lead_qualification.md +82 -0
- franco-0.5.0/skills/linkedin_outreach.md +141 -0
- franco-0.5.0/skills/objection_handling.md +110 -0
- franco-0.5.0/skills/offer_strategy.md +144 -0
- franco-0.5.0/skills/prospecting_tactics.md +148 -0
- franco-0.5.0/skills/sales_methodology.md +150 -0
- franco-0.5.0/skills/signal_playbook.md +88 -0
- franco-0.5.0/tests/test_approved_copy.py +351 -0
- franco-0.5.0/tests/test_autopilot.py +182 -0
- franco-0.5.0/tests/test_brain_no_tools.py +51 -0
- franco-0.5.0/tests/test_brain_provider.py +750 -0
- franco-0.5.0/tests/test_code_scanning_fixes.py +121 -0
- franco-0.5.0/tests/test_codex.py +863 -0
- franco-0.5.0/tests/test_codex_cli_surface.py +428 -0
- franco-0.5.0/tests/test_codex_e2e.py +820 -0
- franco-0.5.0/tests/test_codex_platform.py +195 -0
- franco-0.5.0/tests/test_codex_soak.py +230 -0
- franco-0.5.0/tests/test_collectors_profile.py +157 -0
- franco-0.5.0/tests/test_config.py +61 -0
- franco-0.5.0/tests/test_config_extra.py +174 -0
- franco-0.5.0/tests/test_dashboard_outbox.py +157 -0
- franco-0.5.0/tests/test_dashboard_signals.py +255 -0
- franco-0.5.0/tests/test_dependencies.py +68 -0
- franco-0.5.0/tests/test_discover.py +661 -0
- franco-0.5.0/tests/test_email_finder.py +256 -0
- franco-0.5.0/tests/test_email_wiring.py +140 -0
- franco-0.5.0/tests/test_importer.py +254 -0
- franco-0.5.0/tests/test_models.py +62 -0
- franco-0.5.0/tests/test_models_extra.py +145 -0
- franco-0.5.0/tests/test_outbox_native.py +351 -0
- franco-0.5.0/tests/test_outreach.py +224 -0
- franco-0.5.0/tests/test_packaging.py +222 -0
- franco-0.5.0/tests/test_paths.py +126 -0
- franco-0.5.0/tests/test_pipeline.py +290 -0
- franco-0.5.0/tests/test_raad.py +410 -0
- franco-0.5.0/tests/test_release_notes.py +47 -0
- franco-0.5.0/tests/test_replies.py +160 -0
- franco-0.5.0/tests/test_signals_export.py +157 -0
- franco-0.5.0/tests/test_state.py +115 -0
- franco-0.5.0/tests/test_state_extra.py +309 -0
- franco-0.5.0/tests/test_state_phase1.py +71 -0
- franco-0.5.0/tests/test_usage.py +164 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Franco environment variables
|
|
3
|
+
# Copy this file to .env and fill in your values: cp .env.example .env
|
|
4
|
+
# Never commit .env — it is gitignored.
|
|
5
|
+
# Pick ONE email provider below (set channels.email.provider in franco.yaml
|
|
6
|
+
# to match): gmail (recommended), smtp, or instantly. Everything else is
|
|
7
|
+
# optional but an email verifier (Reoon/ZeroBounce) is strongly recommended.
|
|
8
|
+
# ============================================================
|
|
9
|
+
|
|
10
|
+
# ------------------------------------------------------------
|
|
11
|
+
# EMAIL PROVIDER — how Franco sends and reads replies. Choose one.
|
|
12
|
+
#
|
|
13
|
+
# Option A: Gmail / Google Workspace [RECOMMENDED for <50 cold/day]
|
|
14
|
+
# Set provider: gmail in franco.yaml. Use a mailbox on a DEDICATED
|
|
15
|
+
# secondary domain (never your main one), with SPF/DKIM/DMARC set up.
|
|
16
|
+
# 1. Google Cloud Console → new project → enable the Gmail API
|
|
17
|
+
# 2. Credentials → OAuth client ID → type "Desktop app"
|
|
18
|
+
# 3. Paste the client id/secret below, then run: franco gmail auth
|
|
19
|
+
GMAIL_CLIENT_ID=
|
|
20
|
+
GMAIL_CLIENT_SECRET=
|
|
21
|
+
|
|
22
|
+
# Option B: Any SMTP + IMAP mailbox [AgentMail, Fastmail, Workspace app pw]
|
|
23
|
+
# Set provider: smtp in franco.yaml. IMAP_* defaults to the SMTP_* values.
|
|
24
|
+
SMTP_HOST=
|
|
25
|
+
SMTP_PORT=587
|
|
26
|
+
SMTP_USERNAME=
|
|
27
|
+
SMTP_PASSWORD=
|
|
28
|
+
IMAP_HOST=
|
|
29
|
+
IMAP_PORT=993
|
|
30
|
+
IMAP_USERNAME=
|
|
31
|
+
IMAP_PASSWORD=
|
|
32
|
+
|
|
33
|
+
# Option C: Instantly — cold email platform [legacy]
|
|
34
|
+
# Where to get it: https://app.instantly.ai → Settings → Integrations → API Keys
|
|
35
|
+
# API access requires Instantly's Growth plan or higher.
|
|
36
|
+
INSTANTLY_API_KEY=
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------
|
|
39
|
+
# LinkedIn — browser-automation prospecting [OPTIONAL]
|
|
40
|
+
# These are the credentials of the LinkedIn account Franco will log into
|
|
41
|
+
# with Playwright. WARNING: automation violates LinkedIn's Terms of Service
|
|
42
|
+
# and can get the account restricted or banned. Use a account you can
|
|
43
|
+
# afford to lose, keep the default rate limits, or leave blank to skip —
|
|
44
|
+
# Franco will prospect via web search and company websites instead.
|
|
45
|
+
LINKEDIN_EMAIL=
|
|
46
|
+
LINKEDIN_PASSWORD=
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------
|
|
49
|
+
# Cloudflare Browser Rendering — deep website crawling during training [OPTIONAL]
|
|
50
|
+
# Enables JS-rendered crawling of your own site when running `franco train`.
|
|
51
|
+
# Where to get it:
|
|
52
|
+
# Account ID: https://dash.cloudflare.com → right sidebar of any zone, or the URL
|
|
53
|
+
# API Token: https://dash.cloudflare.com/profile/api-tokens → Create Token
|
|
54
|
+
# → grant "Browser Rendering: Edit" permission
|
|
55
|
+
# Cost: ~$5/mo on the paid Workers plan (~12,000 pages). Free tier has limits.
|
|
56
|
+
# Without these, Franco uses its built-in HTTP crawler (no JS rendering).
|
|
57
|
+
CLOUDFLARE_ACCOUNT_ID=
|
|
58
|
+
CLOUDFLARE_API_TOKEN=
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------
|
|
61
|
+
# Email verification — how Franco confirms a prospect's address really
|
|
62
|
+
# exists before sending. Franco learns each company's email PATTERN, then
|
|
63
|
+
# verifies ONE candidate (raw SMTP probing no longer works against Google
|
|
64
|
+
# Workspace / Microsoft 365). Any ONE of these keys is enough; add both for
|
|
65
|
+
# best coverage. Without any, addresses are marked "guess" and never sent.
|
|
66
|
+
|
|
67
|
+
# Reoon — best free tier for cold-email verification (600 checks/month,
|
|
68
|
+
# renews monthly + 100 non-expiring). Good catch-all flagging.
|
|
69
|
+
# Where to get it: https://reoon.com/email-verifier → sign up → API
|
|
70
|
+
REOON_API_KEY=
|
|
71
|
+
|
|
72
|
+
# ZeroBounce — 100 free/month. Its 2026 engine is the reliable one for
|
|
73
|
+
# Microsoft 365 / Google Workspace catch-all domains; Franco routes those
|
|
74
|
+
# domains here specifically. https://zerobounce.net → API
|
|
75
|
+
ZEROBOUNCE_API_KEY=
|
|
76
|
+
|
|
77
|
+
# Hunter.io — email discovery + pattern lookup [OPTIONAL]
|
|
78
|
+
# Free tier: 50 verifications + 25 domain-searches/month. Its domain-search
|
|
79
|
+
# is the cheapest way to learn a company's email pattern (used first, so
|
|
80
|
+
# Franco verifies one address instead of guessing six).
|
|
81
|
+
# Where to get it: https://hunter.io → sign up → API → API Keys
|
|
82
|
+
HUNTER_API_KEY=
|
|
83
|
+
# Domain-searches per month across all of Franco. 25 matches Hunter's free
|
|
84
|
+
# tier; set this to your plan's allowance if you pay for Hunter.
|
|
85
|
+
HUNTER_MONTHLY_SEARCHES=25
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------
|
|
88
|
+
# DISCOVERY — where Franco finds businesses. [ALL OPTIONAL]
|
|
89
|
+
# The default source (OpenStreetMap) needs no account at all:
|
|
90
|
+
# franco discover
|
|
91
|
+
# See every option, with real prices, via:
|
|
92
|
+
# franco discover --providers (or the dashboard's Discover tab)
|
|
93
|
+
# ------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
# DataForSEO — cheapest local-business and SERP data anywhere.
|
|
96
|
+
# Where to get it: https://app.dataforseo.com/api-access
|
|
97
|
+
# NOTE: the API password is auto-generated there. It is NOT your account
|
|
98
|
+
# password. $1 free credit on signup; $50 minimum deposit to go live.
|
|
99
|
+
# Cost: $0.372 per 1,000 businesses; $0.0018 per depth-30 search.
|
|
100
|
+
DATAFORSEO_LOGIN=
|
|
101
|
+
DATAFORSEO_PASSWORD=
|
|
102
|
+
# Set to any value to route every call to DataForSEO's free sandbox. Same
|
|
103
|
+
# response shape, dummy data, never charged — the right way to try it.
|
|
104
|
+
DATAFORSEO_SANDBOX=
|
|
105
|
+
|
|
106
|
+
# Serper.dev — Google Search API, used for discovery and for Scout's searches.
|
|
107
|
+
# Where to get it: https://serper.dev → sign up → Dashboard → API Key
|
|
108
|
+
# Cost: 2,500 queries free with no card, then a $50 prepaid pack (credits
|
|
109
|
+
# expire after 6 months). Without it Franco falls back to DuckDuckGo/Bing
|
|
110
|
+
# scraping, which is heavily rate-limited.
|
|
111
|
+
SERPER_API_KEY=
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------
|
|
114
|
+
# Calendly — meeting auto-booking [NOT YET USED — roadmap]
|
|
115
|
+
# CALENDLY_API_KEY=
|
|
116
|
+
|
|
117
|
+
# --- Dashboard (optional) ---
|
|
118
|
+
# The dashboard only answers requests addressed to localhost / 127.0.0.1.
|
|
119
|
+
# Add other hostnames you reach it by (e.g. from Docker), comma-separated.
|
|
120
|
+
FRANCO_DASHBOARD_HOSTS=
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to Franco. Dates are release dates; the format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely and versions
|
|
5
|
+
follow [semver](https://semver.org/), with the caveat that Franco is pre-1.0 and
|
|
6
|
+
minor versions can still change behaviour.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.0] — 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`pip install franco`.** Franco is on [PyPI](https://pypi.org/project/franco/) and works without a checkout:
|
|
15
|
+
- `franco init [DIR]` creates a **home folder**: `franco.yaml`, a private `.env`, editable `prompts/` and `skills/`, `data/` and a `.gitignore` that keeps credentials and contact data out of git. It never overwrites existing files.
|
|
16
|
+
- Franco finds its home as `$FRANCO_HOME`, else the source checkout (unchanged for clones and Docker), else the current folder when it has a `franco.yaml`, else `~/.franco`.
|
|
17
|
+
- The wheel bundles the default prompts, skills and templates. A file in the home folder wins; delete it to fall back to the shipped version.
|
|
18
|
+
- A pip-installed Franco never writes into its installed package.
|
|
19
|
+
- `franco install` only fetches Chromium when Franco was installed from PyPI.
|
|
20
|
+
- **Releases publish to PyPI** with trusted publishing (no stored token) and PEP 740 attestations, before the GitHub Release is created.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `franco train` created `skills/` files without creating the folder first, which failed in a fresh home folder.
|
|
25
|
+
- The dashboard's setup check counted only a `.venv` inside the project as a virtual environment.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## [0.4.0] — 2026-10-16
|
|
29
|
+
|
|
30
|
+
Franco is now open source under [GortexAI/franco](https://github.com/GortexAI/franco), published as a container image, and released automatically.
|
|
31
|
+
|
|
32
|
+
### Upgrading from 0.3.0
|
|
33
|
+
|
|
34
|
+
- **Set your postal address.** The template `franco.yaml` now ships with `compliance.require_footer: true` and a blank `postal_address`. No cold email sends until you enter your own address. If your config predates this, the footer stays off until you turn it on.
|
|
35
|
+
- **The dashboard only answers on localhost.** If you reach it under another hostname (from Docker, for example), list it in `FRANCO_DASHBOARD_HOSTS`.
|
|
36
|
+
- New config sections (`outreach`, `compliance`) all have defaults. Existing configs load unchanged.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Approved-copy outreach** (`outreach.mode: templates`):
|
|
41
|
+
- Signed-off sequences in `outreach.campaigns` are sent word for word to researched accounts, with no model involved.
|
|
42
|
+
- Merge fields are limited to `{{first_name}}`, `{{last_name}}`, `{{company}}`, `{{title}}`, `{{sender_name}}` and `{{verified_observation}}`. The observation is a sourced, dated fact recorded per account. An account with an unfilled field is skipped, never sent with a gap.
|
|
43
|
+
- Steps are scheduled in business days in `outreach.timezone`, and each follow-up is re-timed from the day the previous step actually went out.
|
|
44
|
+
- **Raad mode** (`outreach.mode: raad`), named after Raad Mobrem, CEO of Intro, whose outbound approach it follows:
|
|
45
|
+
- Up to three `outreach.benefits`; every funnel names exactly one.
|
|
46
|
+
- `outreach.funnels` are narrow segments matched on observed signals (`TECH_STACK=AdSense|Prebid`) or account fields (`segment=marketplace`), tried narrowest first.
|
|
47
|
+
- Email 1 is written from the funnel's story, the account's sourced facts, the one benefit and a role-specific angle, then the funnel's approved follow-ups are queued.
|
|
48
|
+
- `franco funnels` (and the dashboard's Campaigns tab) shows how companies split across funnels; `--company DOMAIN` explains one placement.
|
|
49
|
+
- The profiler now detects publisher ad tags (Google AdSense, Ad Manager, Prebid, Amazon Publisher Services).
|
|
50
|
+
- **Bring your own lists as CSV files.** `franco import FILE --kind accounts|contacts|suppressions`, plus an Import CSV panel on the dashboard's Contacts tab:
|
|
51
|
+
- Each kind has a template (`--template`), header aliases, `--dry-run` and optional `--verify`.
|
|
52
|
+
- Every row is validated before anything is written.
|
|
53
|
+
- `franco suppress import` parses real CSV (with a `reason` column, or one address per line), and `export` quotes properly.
|
|
54
|
+
- **Preview and sign-off.** New approved-copy campaigns and raad funnels start in preview (`outreach.preview_accounts`, default 10 per owner):
|
|
55
|
+
- Preview emails always wait for review, even on autopilot, and nothing sends until someone signs off.
|
|
56
|
+
- Commands: `franco preview [KEY] [--csv FILE]`, `franco preview signoff KEY --by NAME`, `franco preview reopen KEY`. The Outbox shows a sign-off card for each campaign in preview.
|
|
57
|
+
- **Compliance in code:**
|
|
58
|
+
- `compliance.require_footer` puts the sender, company and postal address plus "Reply "no thanks" to stop." on every cold email in every mode. The gate refuses cold email without the footer, and nothing sends while the postal address is blank.
|
|
59
|
+
- Cold email gets List-Unsubscribe headers (mailto, plus optional one-click HTTPS).
|
|
60
|
+
- `compliance.blocked_claims` is enforced on every email, replies included.
|
|
61
|
+
- A shared suppression list covers addresses and whole domains (`franco suppress`).
|
|
62
|
+
- **Account-level outreach:**
|
|
63
|
+
- One contact per account in a sequence at a time, and one owner per account.
|
|
64
|
+
- A reply or opt-out from anyone stops queued email to everyone at that company.
|
|
65
|
+
- Follow-ups wait for the previous step, and are cancelled if it was rejected or failed.
|
|
66
|
+
- **Replies:**
|
|
67
|
+
- Quoted history and Franco's own footer are removed before any rule runs.
|
|
68
|
+
- "no thanks" (body or subject) is an opt-out, and is suppressed before the optional acknowledgement is sent.
|
|
69
|
+
- Out-of-office replies no longer end a sequence.
|
|
70
|
+
- A new `later` intent pauses the contact and records when to get back in touch.
|
|
71
|
+
- `outreach.reply_templates` provides approved wording per intent; `not_interested` sends its template as written.
|
|
72
|
+
- **Autopilot toggle.** `franco autopilot on|off|default` (and an Outbox button) overrides `channels.email.require_approval` at runtime; turning it on approves what is waiting. Every gate still applies.
|
|
73
|
+
- **Outbox context and editing.** Each draft shows why it exists: campaign, owner, buyer, observation with its source and the date checked. Drafts can be edited before approval with `franco outbox --edit ID` or `E` on the dashboard.
|
|
74
|
+
- **Container image** `ghcr.io/gortexai/franco`, for linux/amd64 and linux/arm64, with an SBOM and a signed build-provenance attestation:
|
|
75
|
+
- Pull requests build and smoke-test it.
|
|
76
|
+
- Every push to `main` publishes `:edge`, and each release publishes `:X.Y.Z`, `:X.Y` and `:latest`.
|
|
77
|
+
- `docker-compose.yml` uses the published image.
|
|
78
|
+
- **Release workflow.** Tagging `vX.Y.Z` checks the version against `pyproject.toml` and the CHANGELOG, builds and tests the sdist and wheel, and creates the GitHub Release. See "Releasing" in `CONTRIBUTING.md`.
|
|
79
|
+
- CI tests Python 3.11, 3.12, 3.13 and 3.14 on Linux, macOS and Windows.
|
|
80
|
+
- `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, issue forms and a pull request template.
|
|
81
|
+
|
|
82
|
+
### Security
|
|
83
|
+
|
|
84
|
+
- **The dashboard refuses cross-site requests and DNS-rebound hosts.** Any web page could previously POST to `localhost:5555`, for example to turn on autopilot.
|
|
85
|
+
- **Raad-mode drafts are checked in code.** Facts reach the model as fenced, untrusted data. Code rejects any draft that contains a blocked claim, HTML, an email address, an IP address, or any link or domain other than the funnel's landing page (Unicode look-alike dots included).
|
|
86
|
+
- **Dependencies are locked and fully scanned.** `requirements.txt` is generated by `pip-compile` from `requirements.in`, with every package pinned and hashed.
|
|
87
|
+
- Dependabot alerts now cover all 28 packages, up from 12.
|
|
88
|
+
- Dependabot security updates work again; they had been failing without a lockfile.
|
|
89
|
+
- Renovate recompiles the lock and keeps `pyproject.toml`, `requirements.in` and the lock in step. `tests/test_dependencies.py` checks that they agree.
|
|
90
|
+
- **Every GitHub Action is pinned to a full commit SHA**, with Renovate keeping the pins current, so a moved or hijacked tag can't change what runs with publishing and signing permissions.
|
|
91
|
+
- **Fixed all 32 CodeQL alerts inherited from the original code:**
|
|
92
|
+
- **Path injection (4, high):** the dashboard serves only files listed under `franco/web/`, never a requested path.
|
|
93
|
+
- **Clear-text logging of secrets (6, high):** the setup wizard reads API keys and passwords through a separate hidden-input function. Secrets were only ever written to `.env`.
|
|
94
|
+
- **Bad HTML tag filter (1, high):** script and style stripping handles end tags like `</script >`.
|
|
95
|
+
- **Stack-trace exposure (20, medium):** dashboard endpoints and run reports log the details and show fixed wording. Franco's own explanations, such as "OpenStreetMap has no category for 'widgets'", are still shown as written.
|
|
96
|
+
- **Log injection (1, medium):** newlines are removed from logged search terms, provider names and errors.
|
|
97
|
+
- CSV exports (prospects, preview, suppressions) neutralise cells starting with `=`, `+`, `-` or `@`, so imported or scraped text can't run as a spreadsheet formula.
|
|
98
|
+
- Scraped website and LinkedIn URLs are linked only when they are http(s).
|
|
99
|
+
- Secret scanning with push protection, private vulnerability reporting, CodeQL and Code Quality are enabled on the repository.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **The Docker image never had the Claude CLI.** Its installer is a bash script but ran under `sh`, and `|| true` hid the failure, so containers couldn't make any model call. It now runs with bash, and the build fails if `claude` is missing.
|
|
104
|
+
- A `.dockerignore` keeps `.env`, `franco.local.yaml`, `data/` and the virtualenv out of local Docker builds.
|
|
105
|
+
- When several steps of a sequence were overdue at once, they no longer all send in the same cycle.
|
|
106
|
+
- Accounts staged on a weekend start on Monday, not Tuesday.
|
|
107
|
+
- No empty campaign rows when every account in a batch is skipped.
|
|
108
|
+
- A usage test no longer fails between 5pm and midnight Pacific (it seeded data with the UTC date while Franco uses the local one).
|
|
109
|
+
|
|
110
|
+
## [0.3.0] — 2026-10-13
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- **Opt-in Codex brain (ChatGPT plan).** Set `brain.provider: codex` (or pick
|
|
115
|
+
it in `franco setup`, or `franco train --provider codex`) to think with the
|
|
116
|
+
Codex CLI on a ChatGPT subscription instead of Claude. Claude remains the
|
|
117
|
+
default and there is no automatic fallback between providers. Each call
|
|
118
|
+
runs a throwaway, isolated `codex app-server` with no tools; Franco verifies
|
|
119
|
+
the lockdown from Codex's own reports before sending any prompt, aborts on
|
|
120
|
+
any attempted tool use, refuses API-key logins and custom endpoints
|
|
121
|
+
(checked before your login is exposed to Codex), and never changes your own
|
|
122
|
+
Codex settings or login: each call gets a private copy of the login without
|
|
123
|
+
its refresh token, so an expired login must be renewed with Codex itself or
|
|
124
|
+
`codex login`. Requires Codex CLI 0.159.3; other versions are refused. See
|
|
125
|
+
the README section "Using a ChatGPT plan instead (Codex)".
|
|
126
|
+
- `brain.model` picks the model for either provider, for Franco only. For
|
|
127
|
+
Codex use `gpt-5.5` (suggested by `franco setup`): the verified text-only
|
|
128
|
+
model. Blank auto-selection only picks listed models without code-execution
|
|
129
|
+
tools, and on current ChatGPT accounts there are none; code-mode models are
|
|
130
|
+
refused.
|
|
131
|
+
- If Codex can't be used (expired or renewal-due login, unsupported CLI,
|
|
132
|
+
unsafe Codex config, rejected model, plan usage limit), Franco pauses the
|
|
133
|
+
heartbeat instead of running agents that would fail: replies stay unread
|
|
134
|
+
and unescalated, queued emails wait, and the reason shows in the log,
|
|
135
|
+
`franco status` and the dashboard. It resumes on its own once the cause is
|
|
136
|
+
fixed. `franco status --check` runs the same check (no prompt sent) on demand
|
|
137
|
+
and exits non-zero when it fails. The Codex provider supports macOS and
|
|
138
|
+
Linux; it is refused on Windows (use Claude there).
|
|
139
|
+
|
|
140
|
+
### Changed
|
|
141
|
+
|
|
142
|
+
- Each heartbeat now sends email after the agents that read and answer
|
|
143
|
+
replies have finished, and native mail providers (Gmail/SMTP) read the
|
|
144
|
+
mailbox every cycle, so a reply always cancels its queued follow-ups before
|
|
145
|
+
that cycle's sends (previously the two could run at the same time, and a
|
|
146
|
+
reply arriving with no open conversation waited for a later cycle). This
|
|
147
|
+
applies to Claude and Codex alike.
|
|
148
|
+
- A reply is only recorded once its reply draft (if it needs one) is
|
|
149
|
+
written, so a brain outage part-way never half-records it. If a working
|
|
150
|
+
brain returns no usable draft, the conversation is flagged for a human
|
|
151
|
+
instead of being left open with no answer.
|
|
152
|
+
- Usage is tracked per provider. The budget gate, `franco usage`, `franco
|
|
153
|
+
status` and the dashboard read and label the active provider's own quota;
|
|
154
|
+
existing usage history is kept and attributed to Claude.
|
|
155
|
+
|
|
156
|
+
### Fixed
|
|
157
|
+
|
|
158
|
+
- Dashboard HTML and text assets now use UTF-8 explicitly, fixing Windows
|
|
159
|
+
decoding failures. Regression tests cover HTML, CSS, JavaScript and SVG.
|
|
160
|
+
|
|
161
|
+
## [0.2.1] — 2026-10-13
|
|
162
|
+
|
|
163
|
+
A security release. Update if you run Franco.
|
|
164
|
+
|
|
165
|
+
### Security
|
|
166
|
+
|
|
167
|
+
- **Claude now runs with no tools.** Every Claude call used to run with
|
|
168
|
+
`--dangerously-skip-permissions`, and prompts include text Franco doesn't
|
|
169
|
+
control: prospect email replies and scraped web pages. Instructions hidden
|
|
170
|
+
in that text could make Claude run commands, read or write files, or use
|
|
171
|
+
the operator's connected MCP servers. Calls now run with `--tools ""` and
|
|
172
|
+
`--strict-mcp-config`; Franco only ever needed text back, so nothing else
|
|
173
|
+
changes.
|
|
174
|
+
If you ran an earlier version with live replies coming in, check the
|
|
175
|
+
account that ran Franco for unexpected files or activity.
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
|
|
179
|
+
- `franco run` no longer drops into the setup wizard when the trained config
|
|
180
|
+
lives in `franco.local.yaml`; the dashboard reads it too.
|
|
181
|
+
- A heartbeat cycle is capped at 10 minutes, so one hung agent can't freeze
|
|
182
|
+
Franco, and quiet hours are re-checked every 5 minutes so a Mac waking from
|
|
183
|
+
sleep resumes on time.
|
|
184
|
+
- Sites that only answer on plain HTTP or `www.` are no longer marked
|
|
185
|
+
unreachable.
|
|
186
|
+
- Profile and contact backfills rotate through every company instead of
|
|
187
|
+
retrying the same few forever.
|
|
188
|
+
- The trainer writes full place names ("Solon, Ohio") and real job titles, so
|
|
189
|
+
discovery doesn't geocode to the wrong city and found contacts aren't
|
|
190
|
+
silently discarded.
|
|
191
|
+
|
|
192
|
+
### Added
|
|
193
|
+
|
|
194
|
+
- Contacts are backfilled for companies found without any, from their own team
|
|
195
|
+
pages and, if a key is set, Hunter's named-people search.
|
|
196
|
+
- `HUNTER_MONTHLY_SEARCHES` (default 25) sets one Hunter budget shared by every
|
|
197
|
+
domain search.
|
|
198
|
+
|
|
199
|
+
## [0.2.0] — 2026-09-13
|
|
200
|
+
|
|
201
|
+
The release that made Franco worth running. The previous version could write
|
|
202
|
+
and send email but could not reliably find anyone to send it to, and it needed
|
|
203
|
+
a paid third-party platform to send at all. Both are fixed.
|
|
204
|
+
|
|
205
|
+
### Added
|
|
206
|
+
|
|
207
|
+
- **A prospecting pipeline that works.** Four stages, each independently
|
|
208
|
+
runnable and re-runnable: DISCOVER (who exists) → PROFILE (what they are) →
|
|
209
|
+
ENRICH (who decides) → VERIFY (can you reach them).
|
|
210
|
+
- **DISCOVER, with a provider menu.** Four sources behind one adapter
|
|
211
|
+
interface, shown side by side in the dashboard with what each does, what it
|
|
212
|
+
costs, its free tier and which keys it needs. The default — OpenStreetMap —
|
|
213
|
+
needs no account, no card and no key, so the whole pipeline can be run
|
|
214
|
+
before paying anyone. `franco discover`.
|
|
215
|
+
- **Cost discipline in the collector.** Every discovery run estimates before it
|
|
216
|
+
spends, refuses to start over your cap, re-checks the cap between batches,
|
|
217
|
+
reads a kill switch, and flushes observations per batch so a run that dies
|
|
218
|
+
keeps what it learned. `franco discover --estimate` prints the projection and
|
|
219
|
+
exits.
|
|
220
|
+
- **PROFILE.** Free — three HTTP requests per business, no browser, no model
|
|
221
|
+
call. Reads the incumbent agency out of the site footer, ad pixels, missing
|
|
222
|
+
schema, abandoned blogs, open roles and named people. Chained onto DISCOVER
|
|
223
|
+
and run on every heartbeat. `franco profile`.
|
|
224
|
+
- **A signal catalog you confirm.** Franco knows how to collect 23 signals and
|
|
225
|
+
collects none of them until a human says so. Each shows a plain-language
|
|
226
|
+
description, what it costs, and how many companies already carry it.
|
|
227
|
+
`franco signals`.
|
|
228
|
+
- **The cohort builder.** Pick what a prospect must have and what disqualifies
|
|
229
|
+
them; see the matching companies live. A prospect list is a query you can
|
|
230
|
+
explain, not a file.
|
|
231
|
+
- **Native mail providers.** Gmail (REST + OAuth) and any SMTP+IMAP mailbox.
|
|
232
|
+
- **The outbox approval ladder.** Every outgoing email waits for approval by
|
|
233
|
+
default (`pending_review → approved → sent`), reviewed one at a time in a
|
|
234
|
+
decisions desk with `A`/`R`/`J`/`K`. A deterministic pre-send gate — no model
|
|
235
|
+
involved — rejects unrendered merge tags, banned phrases, over-length bodies,
|
|
236
|
+
too many links, HTML, undeliverable addresses and recipient mismatches.
|
|
237
|
+
- **Bounce handling and a kill switch.** A bounce marks the address invalid,
|
|
238
|
+
cancels that prospect's queued sends, and trips a global pause past a
|
|
239
|
+
configured bounce rate. `franco sending pause|resume`.
|
|
240
|
+
- **Claude usage tracking.** Per-call attribution by agent, task and model, plus
|
|
241
|
+
live subscription-quota gauges read the same way `/usage` does, so Franco
|
|
242
|
+
throttles itself and leaves headroom for your own interactive work.
|
|
243
|
+
`franco usage`.
|
|
244
|
+
- **CSV export.** `franco export` writes a sequencer-ready list. Franco is worth
|
|
245
|
+
running as a list-builder even if you never let it send.
|
|
246
|
+
- **A dashboard worth looking at.** Opens on Today — what needs a human — with
|
|
247
|
+
an activity feed and a rail of pipeline figures, collector runs and setup.
|
|
248
|
+
Light and dark are the same design re-tokenised; appearance follows your OS by
|
|
249
|
+
default. Fonts are vendored so it renders with the network off.
|
|
250
|
+
- **A Python version guard.** `python3 -m venv` on macOS builds from the system
|
|
251
|
+
3.9, which cannot run Franco. It now fails immediately with the fix instead of
|
|
252
|
+
surfacing later as an unrelated-looking ImportError.
|
|
253
|
+
|
|
254
|
+
### Changed
|
|
255
|
+
|
|
256
|
+
- **The data model.** Every fact Franco learns is now an observation — a row in
|
|
257
|
+
`observations`, never a column. A new signal needs no migration, re-observing
|
|
258
|
+
over time is a free time series, and confidence and provenance travel with the
|
|
259
|
+
fact. The vocabulary is governed: a database trigger rejects any signal code
|
|
260
|
+
not in `signal_codes`.
|
|
261
|
+
- **Email finding is pattern-first.** Franco derives the domain's address format
|
|
262
|
+
and verifies one candidate rather than brute-forcing name variations, and
|
|
263
|
+
grades every address honestly as `verified` / `risky` / `guess` / `invalid`.
|
|
264
|
+
Only deliverable addresses are ever sent to.
|
|
265
|
+
- **The default mail provider** is now `gmail`. Instantly is still supported as
|
|
266
|
+
a legacy option.
|
|
267
|
+
- **Discovery depth defaults to 30, not 100.** Google removed
|
|
268
|
+
100-results-per-page in September 2025, so depth 100 is now billed as ten
|
|
269
|
+
pages nearly everywhere — and below rank 30 it is mostly directories.
|
|
270
|
+
- **The dashboard's HTML, CSS and JS** are real files in `franco/web/` instead
|
|
271
|
+
of a 1,500-line string literal in `dashboard.py`. Still no build step.
|
|
272
|
+
|
|
273
|
+
### Fixed
|
|
274
|
+
|
|
275
|
+
- **Guessed emails were being reported as verified** whenever the domain merely
|
|
276
|
+
had an MX record. The migration downgrades every legacy `email_verified=1` row
|
|
277
|
+
to `guess`.
|
|
278
|
+
- **Cohorts counted the wrong companies.** PROFILE records a false boolean when
|
|
279
|
+
it checks and finds nothing — which is correct — but `cohort()` matched on the
|
|
280
|
+
row existing, so "companies running Google Ads" silently meant "companies we
|
|
281
|
+
checked for Google Ads", which was all of them. Cohorts now read the newest
|
|
282
|
+
observation per company and signal, and require it to be true.
|
|
283
|
+
- **Usage tracking counted every Claude session on the machine**, not just
|
|
284
|
+
Franco's. The transcript reconciler is gone; the ledger contains only rows
|
|
285
|
+
Franco's own Brain wrote.
|
|
286
|
+
- **Page titles were being extracted as people** ("Request Service", "Commercial
|
|
287
|
+
Services"), and titles bled between team members.
|
|
288
|
+
- **Confirming a signal before anything had loaded the catalog** reported success
|
|
289
|
+
while changing nothing.
|
|
290
|
+
- **The free OSM provider answered unmapped search terms with every registered
|
|
291
|
+
office in the area** — coffee roasters and churches for a roofing query. It now
|
|
292
|
+
refuses the query and names the trades it can search.
|
|
293
|
+
|
|
294
|
+
### Security
|
|
295
|
+
|
|
296
|
+
- Test fixtures no longer carry real businesses' phone numbers or street
|
|
297
|
+
addresses.
|
|
298
|
+
- `franco.local.yaml` (gitignored) overrides the tracked `franco.yaml`, so a
|
|
299
|
+
fork can hold real product configuration without it reaching a public repo.
|
|
300
|
+
|
|
301
|
+
## [0.1.0]
|
|
302
|
+
|
|
303
|
+
Initial release: the heartbeat loop, five sub-agents, the skills library, the
|
|
304
|
+
website trainer, Instantly-based sending, and the first dashboard.
|
franco-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gortex
|
|
4
|
+
Copyright (c) 2026 Ethan Rogers
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
franco-0.5.0/MANIFEST.in
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# The sdist must carry what setup.py bundles into the wheel.
|
|
2
|
+
include franco.yaml .env.example LICENSE README.md CHANGELOG.md requirements.in requirements.txt
|
|
3
|
+
recursive-include prompts *.md
|
|
4
|
+
recursive-include skills *.md
|
|
5
|
+
exclude skills/product_knowledge.md skills/competitive_intel.md
|
|
6
|
+
recursive-include franco/web *
|
|
7
|
+
global-exclude __pycache__ *.py[cod] .DS_Store
|