Metadata-Version: 2.4
Name: dogfu
Version: 0.11.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'
Requires-Dist: responses>=0.25.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` | `/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 touch due                 # the work queue: reach-outs + follow-ups owed today
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
dogfu crm touch stop  <lead_id>     # give up / nurture: end the chase
dogfu crm touch reconcile           # audit the cadence-task invariant (--fix 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`. Because
of this, **two queues mean the same thing**:

- **`crm touch due`** is dogfu's own queue — reach-outs (computed from status +
  never-touched) unioned with follow-ups whose next touch is due. Complete on its
  own.
- **Close's native task list** is the same queue rendered in Close's UI. It is only
  complete because the reach-out, too, is a real task; that's what lets a BDR rely
  on Close's Tasks/Inbox instead of the CLI.

`crm touch reconcile` is the backstop that keeps the two in sync: it flags (and with
`--fix` repairs) any lead missing its task, carrying a stray or duplicate one, or
whose task's due date has drifted. It reuses only existing reads, so it's safe to run
on demand or on a schedule.

## Opportunities (deals)

Once a lead replies, it leaves the cold cadence and is worked as one or more **Close
opportunities** — deals 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 `Engaged` lead status 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 opportunity due                             # the deal queue (dropped balls first)
dogfu crm opportunity advance <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. `opportunity due` and the
`next_step` field key off this tag, and `crm touch reconcile` also audits the deal
invariant (≤ one open deal task per open opp; none on a won/lost or vanished opp).

`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.

## 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)
```
