Metadata-Version: 2.4
Name: dogfu
Version: 0.19.0
Summary: Internal admin/marketing CLI — discovery, enrichment, outreach, and CRM over one interface.
Requires-Python: >=3.10
Requires-Dist: click>=8.1
Requires-Dist: requests>=2.31
Requires-Dist: rich>=13.7
Provides-Extra: dev
Requires-Dist: black>=24.0.0; extra == 'dev'
Requires-Dist: isort>=5.13.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# dogfu

Internal admin CLI + SDK for Agent Berlin staff (`admin_users`). One
interface over LinkedIn, X, Google, ChatGPT, SEO (the dogfu data API) and the
Close CRM (via the admin CRM proxy). The sibling of `backend/sdk-python` — same
transport posture (a single Bearer token against the Berlin backend, retry with
backoff, a typed exception hierarchy, config-file session), scoped to the
internal admin surface instead of a customer project.

## Auth

dogfu authenticates with a single **session token** (a short-lived JWT, 24h),
minted by exchanging a one-time OTP from the dogfu MCP's `get_setup_instructions`:

```bash
dogfu configure --otp <OTP> --title "<what this session is for>"
```

This `POST`s `{otp, title}` to `/dogfucli/exchange-otp` and saves the returned
token to `~/.dogfu/config.json`. Subsequent commands resolve the token from
`DOGFU_TOKEN` first, then that file. There is no refresh endpoint — on an auth
failure, run `get_setup_instructions` again for a fresh OTP and re-`configure`.

dogfu is **not project-scoped** (no `--domain`); access is platform-wide for
staff.

## Endpoints

Everything goes through the Berlin backend (`https://backend.agentberlin.ai` by
default; override with `--base-url` / `DOGFU_BASE_URL`):

| Group | Backend route |
| --- | --- |
| `linkedin` / `x` / `google` / `chatgpt` / `seo` / `apollo` / `report` | `/api/v1/admin/dogfu/*` |
| `crm` (leads, contacts, tasks, notes, opportunities, pipelines) | `/api/v1/admin/crm/*` — allowlisted proxy to Close CRM |

The CRM proxy holds the per-admin Close key server-side (configured from the
admin Console), so dogfu never sees a Close credential — it forwards Close
sub-paths under the dogfu session token.

## Usage

```bash
dogfu --help                  # list groups
dogfu <group> --help          # list a group's commands
dogfu <group> <cmd> --help    # flags + the Output: data shape

dogfu seo domain-overview --target acme.com
dogfu crm lead create --name "Acme" --url https://acme.com
```

Output is canonical JSON by default (`-f table` for humans, `-o FILE` to write).

## Outreach cadence

A **touch** is an attempt to reach a lead — touch 0 is the reach-out, 1..N are
follow-ups, on whatever channel the rep used. The sequence is unbounded and only
ends on `reply` or `stop`. State lives in four system-managed lead custom fields
(`touch_stage`, `last_touched`, `next_touch_due`, `touch_channel`), created once by
a Close admin; only the `crm touch` verbs write them.

```bash
dogfu crm worklist                  # the work queue: every open task due today, classified
dogfu crm touch record <lead_id> -c linkedin   # log a touch, roll the reminder forward
dogfu crm touch reply <lead_id>     # prospect answered: end the chase, → Connected, open the engage task
dogfu crm touch stop  <lead_id>     # give up / nurture: end the chase
dogfu crm reconcile                 # audit the task invariants (--apply to repair)
```

Every lead in the active sequence carries **exactly one open cadence task** —
created when the lead enters the reach-out status (`crm lead create/update
-s Qualified`), rolled forward by `record`, and closed on `reply`/`stop`.

**The work queue *is* the task list.** `crm worklist` reads the actual open Close
tasks due today or earlier — the BDR's real inbox — and classifies each row
(reach-out / follow-up / engage / deal / ad-hoc), enriching it with the lead and
contact context to act. It replaces the old `touch due` / `opportunity due` commands, so
`crm worklist` and Close's own Tasks/Inbox are literally the same list — nothing due
is ever invisible, and there's no state-vs-task reconstruction to drift.

`crm reconcile` is the inverse — the audit: everything whose *state* says it needs
action but has no matching task (a reach-out/cadence task missing, a dropped-ball
opportunity, a stray/duplicate/terminal-status task, a stalled deal). With `--apply`
it repairs the auto-fixable cadence/deal-task ones through the single tagged-task
owner. It reuses only existing reads, so it's safe to run on demand or on a schedule —
and it's what keeps `crm worklist` trustworthy.

> **Filtering leads by status uses Close Advanced Filtering** (`POST /data/search/`),
> not the `/lead/` smart-search — Close silently ignores `status_id:"..."` in that
> `query`. `crm lead list -s <status>`, the reach-out sweep, and the Connected /
> Engaged books all route through it; free-text / name / custom-field lead searches
> stay on `/lead/`.

## Lifecycle & statuses: Qualified → Connected → Engaged → Customer

The lead status is the plain-English relationship funnel; the **opportunity** object is
the forecastable deal. They're distinct layers, and each active status carries exactly
one CLI-owned next-action task:

| Status | Meaning | Auto-task |
| :-- | :-- | :-- |
| **Qualified** | a fit; cold outreach, no reply yet | `[dogfu:cadence]` (reach-out → follow-ups) |
| **Connected** | replied / in conversation; **no deal yet** | `[dogfu:engage]` ("land the discovery call") |
| **Engaged** | a real deal (opportunity) is open | `[dogfu:deal]` (per open opp) |
| **Customer** | won | — |

A **reply** moves a lead Qualified → **Connected** and opens its engage task (`touch
reply`). The **gate** — a discovery call confirming a real deal — is `opportunity
create`, which promotes Connected → **Engaged** and retires the engage task (the deal
task takes over). This mirrors the sales-standard funnel (cold outreach → connect →
qualify at the gate → opportunity → close); `reconcile` audits all three task
invariants, including "a Connected lead with no deal carries exactly one engage task."

## Opportunities (deals)

When a discovery call confirms a real deal, a **Close opportunity** opens — a deal
moved through a pipeline's stages (Discovery → Trial → Proposal → Won/Lost). Stage ids
are pipeline-scoped and resolved from stage *names* at runtime, so nothing hardcodes a
`stat_…` id. The pipeline, the `Deal Type` custom field (Co-Pilot / Fully-Run), and the
`Connected` and `Engaged` lead statuses are created once by a Close admin.

```bash
dogfu crm opportunity pipelines                       # stages + their ids
dogfu crm opportunity create <lead_id> --value 1500 --period monthly \
      --deal-type co-pilot --next "Send proposal" --next-due 2026-07-07
dogfu crm worklist --kind deal                        # deals due today (part of the one queue)
dogfu crm opportunity update <opp_id> --stage Proposal
dogfu crm opportunity next <opp_id> -t "Follow up on contract" -d 2026-07-10
dogfu crm opportunity win <opp_id> --value 1800       # or: lose <opp_id> --reason "..."
```

Each open opportunity carries **exactly one open `[dogfu:deal]` next-step task** — the
warm-phase analogue of the cadence task. `opportunity next` (and `--next` on
`create`/`advance`) is the single writer: it closes the prior deal task and opens the
new one; `win`/`lose` close it. Unlike the cold cadence, the task's content and due
date are rep-authored — there's no wait curve. A deal's due next-step shows up in
`crm worklist --kind deal`, and `crm reconcile` audits the deal invariant (≤ one open
deal task per open opp; none on a won/lost or vanished opp) plus **dropped balls**
(an open deal with no next-step task) and **stalled** deals.

`win`/`lose` set only the *deal's* status. The *lead*-status move (→ Customer / Not
Interested) is left to the caller, because it's a separate, human-meaningful decision.

## Publishing a first-audit report

`report publish` pushes a single self-contained HTML audit to the public reports
bucket, served at `reports.agentberlin.ai/<domain>`. The HTML must carry the
book-a-call CTA (`https://cal.link/berlin`) — it's checked locally before any
upload (and re-checked server-side), so a report missing it fails fast and
nothing is written. Re-running for the same domain overwrites in place.

```bash
dogfu report publish --domain acme.com --html ./acme-audit.html
# -> { "url": "https://reports.agentberlin.ai/acme.com",
#      "domain": "acme.com", "published_at": "2026-06-30T12:00:00Z" }
```

## Programmatic use

```python
from dogfu import Dogfu
dx = Dogfu()                                  # resolves DOGFU_TOKEN / config file
dx.seo.domain_overview(target="acme.com")     # -> DomainOverview
dx.crm.create_lead(name="Acme")
```

## Develop

```bash
uv run python tests/smoke.py   # offline checks (normalizers, models, SDK wiring)
```
