Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.6.0 (2026-10-12)

- SDK/CLI: the API now limits how much work one account can have queued or running: email lookups (2,000 times the plan multiplier) and three DiscoGen, bulk match, contact bulk match and segment tasks. A submit over a limit is answered 429 with `Retry-After: 60`, which the SDK already waits out and retries, so with the default `max_retries=3` the call blocks for up to three minutes and then raises `RateLimitError` carrying the count and the limit in its message. In the SDK, `Discolike(max_retries=0)` gets the error at once; the CLI has no retry option, so its commands wait and then exit 4. No SDK change; older versions behave the same.
- SDK: `DiscoverParams.offset` is limited to 0-10,000, matching the API, which now answers 422 above that instead of silently capping it. To go deeper, save the domains you already have as an exclusion list, pass it as `exclusion_query_id` and page again from 0.
- SDK: `country`, `person_country` and `filter_country` descriptions list the continent region codes the API now accepts (`EUROPE`, `ASIA`, `AFRICA`, `NORTH_AMERICA`, `SOUTH_AMERICA`, `OCEANIA`, `ANTARCTICA`). `EU` stays the 27 member states. The fields are plain strings, so older SDK versions can send these too.
- SDK: the `email_validated` contact filter on `contacts.search`, `contacts.count` and `contacts.discover` is deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead. The flag behind it only recorded that a third-party verifier had run at some point, not what it found, so a large share of "validated" addresses were catch-all. It is dropped from `ContactsSearchParams`, `ContactsCountParams` and `ContactFilters`; passing it still works (it goes out as an extra field and the API applies it as `has_email`) but raises a `DeprecationWarning`. Contact results keep their `email_validated` field, which is now `None` until you run email verification on the contact, then `True` or `False`.
- CLI: `contacts search`, `count` and `discover` keep `--email-validated/--no-email-validated` so existing scripts keep running. `--email-validated` is deprecated and treated as `--has-email` (an explicit `--has-email` or `--no-has-email` takes precedence) and prints a deprecation warning on stderr; `--no-email-validated` warns and has no effect. Neither sends `email_validated`. `bulk contacts` keeps its `email_validated` CSV column, empty unless the contact was verified.
- SDK (note for maintainers): `discolike/_generated/requests.py` is regenerated from the production spec, which now marks `email_validated` deprecated and carries the offset cap and continent codes, so `scripts/gen_requests.py --check` is clean again.
- SDK (note for maintainers): `scripts/check_contract.py` now skips a query parameter marked deprecated inside its `schema`, which is where FastAPI puts a deprecated model field. `scripts/gen_requests.py` already dropped those, so the two scripts disagreed on GET routes.
- CLI: `prospecting plan RUN_ID --plan-version N` changes a proposed plan before approval, wrapping `update_plan()`: `--contact-integration-id`, `--validation-integration-id`, `--search-provider-id` (`none` skips web research), `--target-companies`, `--contacts-per-company`, `--deliverable`, `--goal`, `--max-spend-usd` and `--max-provider-spend-usd` (`0` removes a limit), sending only the flags passed. When an organization has several AI or search providers and none is the default, the plan leaves that engine at `selected: null` and `approve` returns 409 until one is picked; `plan` is how a CLI user picks it. The run comes back at a new `plan_version`, which is the one to approve.

## 0.5.0 (2026-10-06)

- **Breaking:** Python 3.10 is no longer supported (end of life October 2026); the SDK and CLI require Python 3.11+.
Expand Down
8 changes: 6 additions & 2 deletions packages/discolike-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
discolike prospecting wait RUN_ID
# Review the proposed plan, then approve the exact version you saw:
discolike prospecting approve RUN_ID --plan-version 1
# Review the proposed plan; change its engines or size if needed (returns a new plan_version):
discolike prospecting plan RUN_ID --plan-version 1 --contact-integration-id LLM_ID --search-provider-id SEARCH_ID
# Then approve the exact version you saw:
discolike prospecting approve RUN_ID --plan-version 2
discolike prospecting list --limit 20
discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
Expand All @@ -65,6 +67,8 @@ discolike prospecting cancel RUN_ID

`wait` returns on `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. A timeout stops polling only. Inspect status and stop reason; completion does not guarantee full coverage. Message replies arrive through `status --messages-after`; follow `next_message_seq` and `reply_pending`. A "segment these" request keeps `reply_pending` true until the segments message is posted, which can take more than an hour. If grouping stops without an outcome, the flag clears on its own after about 90 minutes.

`plan` changes a proposed plan before approval and sends only the flags you pass: `--contact-integration-id`, `--validation-integration-id` and `--search-provider-id` take option ids from the plan message's `contact_engine`, `company_check_engine` and `search_provider` (`--search-provider-id none` skips web research), and `--target-companies`, `--contacts-per-company`, `--deliverable`, `--goal`, `--max-spend-usd` and `--max-provider-spend-usd` (`0` removes a limit) resize or re-scope it. When your organization has several AI or search providers and none is the default, the plan shows that engine with `selected: null` and `approve` returns 409 until `plan` picks one. The run comes back re-estimated at a new `plan_version`; approve that one.

`start` and `approve` default to pausing at checkpoints, like the web chat: a pilot check on large lists (`pilot`), a search drifting off target (`tail_quality`), candidates running out short of the target (`short`), the target being met (`target_reached`), another round of companies when the ones checked yield too few contacts (`top_up`), and, on wave runs, the researched contacts found before their emails are looked up (`contacts_review`, asked with `--auto` too; contacts already in DiscoLike's database get their lookups during research, which approving the plan starts; picking "Find emails" by number sends the question's `seq`, which the API needs to accept it). Pass `--auto` to never pause (except at a `top_up` round that would pass a spending limit); a poor pilot is then sharpened once and the run continues with a notice, stopping with `pilot_failed` only if the re-pilot fit is still under 20%. If sharpening itself fails, the run continues on the original criteria. On a terminal, `wait` shows the question, any sample companies and numbered replies at a checkpoint, sends your pick or your own text, and keeps waiting. Without a terminal, or with `--no-input`, it prints the run on stdout, a `needs_input` envelope (`message`, `stop_reason`, `suggested_replies`, `sample`) on stderr, and exits 7; answer with `prospecting message --text "<reply>"` (add `--question-seq` with the question's `seq` to answer only that question) and run `wait` again.

Omit `--target-companies` and `--contacts-per-company` to infer counts from the brief (fallback 1,000 and 3; up to 10 contacts per company). Explicit values override the text. `--deliverable accounts` returns the checked companies only, with no people; `--goal companies` takes `--target-companies` matching companies and works only those instead of adding rounds until the contact target is met. Both default to `leads`. `--max-candidates` and `--max-actions` are automatic when omitted or `0`; their maxima are 100,000 and 10,000. Targets allow up to 10,000 companies, status pages up to 500 rows, and lists up to 50 runs. Work caps do not cap provider charges.
Expand Down
4 changes: 2 additions & 2 deletions packages/discolike-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ build-backend = "hatchling.build"

[project]
name = "discolike-cli"
version = "0.5.0"
version = "0.6.0"
description = "Official CLI for the DiscoLike API"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.11"
authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
dependencies = [
"discolike==0.5.0",
"discolike==0.6.0",
"typer>=0.26.1,<0.28",
"rich>=13.0",
]
Expand Down
32 changes: 25 additions & 7 deletions packages/discolike-cli/src/discolike_cli/contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,17 @@
NEGATE_FILTER_STATE_HELP = "Exclude contacts at companies in this state (repeatable)."
PERSON_STATE_HELP = "Filter by contact state/region (repeatable)."
PERSONA_ID_HELP = "Filter by persona ID (repeatable)."
EMAIL_VALIDATED_HELP = "Only contacts with (or without) a validated email address."
EMAIL_VALIDATED_HELP = (
"Deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead."
)
EMAIL_VALIDATED_WARNING = (
"warning: --email-validated is deprecated. Treated as has_email: "
"contact search no longer filters on email verification; verify emails instead."
)
NO_EMAIL_VALIDATED_WARNING = (
"warning: --no-email-validated is deprecated and has no effect. "
"Contact search no longer filters on email verification; verify emails instead."
)
HAS_PHONE_HELP = "Only contacts with (or without) a phone number."
HAS_MOBILE_HELP = "Only contacts with (or without) a mobile phone number."
HAS_LINKEDIN_HELP = "Only contacts with (or without) a LinkedIn profile."
Expand All @@ -50,6 +60,17 @@
INCLUDE_SEARCH_CONTACTS_HELP = "Include contacts from the search index (broader coverage)."
CONSENSUS_HELP = "Number of query vectors to combine for consensus search."


def _email_validated_as_has_email(*, email_validated: bool | None, has_email: bool | None) -> bool | None:
if email_validated is None:
return has_email
if not email_validated:
typer.echo(NO_EMAIL_VALIDATED_WARNING, err=True)
return has_email
typer.echo(EMAIL_VALIDATED_WARNING, err=True)
return True if has_email is None else has_email


app = typer.Typer(
help="Find contacts: search and count by filters, look up or match individuals, and run bulk/generative discovery jobs."
)
Expand Down Expand Up @@ -137,8 +158,7 @@ def search_command(
filter_state=filter_state,
negate_filter_state=negate_filter_state,
employee_range=employee_range,
has_email=has_email,
email_validated=email_validated,
has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
has_phone=has_phone,
has_mobile=has_mobile,
has_linkedin=has_linkedin,
Expand Down Expand Up @@ -238,8 +258,7 @@ def count_command(
filter_state=filter_state,
negate_filter_state=negate_filter_state,
employee_range=employee_range,
has_email=has_email,
email_validated=email_validated,
has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
has_phone=has_phone,
has_mobile=has_mobile,
has_linkedin=has_linkedin,
Expand Down Expand Up @@ -413,8 +432,7 @@ def discover_command(
filter_state=filter_state,
negate_filter_state=negate_filter_state,
employee_range=employee_range,
has_email=has_email,
email_validated=email_validated,
has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
has_phone=has_phone,
has_mobile=has_mobile,
has_linkedin=has_linkedin,
Expand Down
77 changes: 76 additions & 1 deletion packages/discolike-cli/src/discolike_cli/prospecting.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from discolike.requests import ProspectingGetParams
from discolike.requests import ProspectingListParams
from discolike.requests import ProspectingMessageRequest
from discolike.requests import ProspectingPlanSettings
from discolike.resources.prospecting import ProspectingMessage
from discolike.resources.prospecting import ProspectingRun
from discolike_cli._inputs import read_domains_file
Expand All @@ -43,6 +44,18 @@
SEED_SEGMENT_HELP = (
"A customer segment id from the plan card to expand (repeatable); omitted keeps the card's selection."
)
PLAN_DELIVERABLE_HELP = "leads finds people at the companies; accounts returns the checked companies only."
PLAN_GOAL_HELP = (
"leads keeps adding rounds until the target is met; companies works only --target-companies matching companies."
)
SEARCH_PROVIDER_HELP = "An option id from the plan's search_provider; none skips web research."
SPEND_HELP_SUFFIX = "0 removes the limit."
PLAN_HELP = (
"Change a proposed plan's engines, size, deliverable or spending limits before approving it. Flags left out "
"keep their current choice; engine ids come from the latest plan message's contact_engine, "
"company_check_engine and search_provider options, and an engine shown with selected null must be picked "
"here before approve. The run comes back re-estimated at a new plan_version: approve that one."
)
RESULTS_SEGMENT_HELP = "Group the finished results into segments; omitted keeps the brief's setting."
QUESTION_SEQ_HELP = (
"The seq of the question this suggested reply answers; if a newer question was posted since, it answers none."
Expand Down Expand Up @@ -322,7 +335,10 @@ def approve_command(
seed_segment: list[int] | None = typer.Option(None, "--seed-segment", help=SEED_SEGMENT_HELP),
segment: bool | None = typer.Option(None, "--segment/--no-segment", help=RESULTS_SEGMENT_HELP),
) -> None:
"""Approve the reviewed plan version and start research."""
"""Approve the reviewed plan version and start research.

A plan with an engine left unselected (selected null) returns 409 until `prospecting plan` picks one.
"""
from discolike_cli.main import get_client

emit(
Expand All @@ -342,6 +358,65 @@ def approve_command(
)


@app.command("plan", help=PLAN_HELP)
@handle_errors
def plan_command(
ctx: typer.Context,
run_id: str = typer.Argument(...),
plan_version: int = typer.Option(..., "--plan-version", min=1, help="The plan version you reviewed."),
contact_integration_id: str | None = typer.Option(
None, "--contact-integration-id", help="An option id from the plan's contact_engine."
),
validation_integration_id: str | None = typer.Option(
None, "--validation-integration-id", help="An option id from the plan's company_check_engine."
),
search_provider_id: str | None = typer.Option(None, "--search-provider-id", help=SEARCH_PROVIDER_HELP),
target_companies: int | None = typer.Option(
None, "--target-companies", min=1, max=10000, help="Qualified companies to aim for."
),
contacts_per_company: int | None = typer.Option(
None, "--contacts-per-company", min=1, max=10, help="Most contacts to find at each company."
),
deliverable: str | None = typer.Option(None, "--deliverable", help=PLAN_DELIVERABLE_HELP),
goal: str | None = typer.Option(None, "--goal", help=PLAN_GOAL_HELP),
max_spend_usd: float | None = typer.Option(
None,
"--max-spend-usd",
min=0,
help=f"USD limit on DiscoLike records and per-call fees at your plan's rates; {SPEND_HELP_SUFFIX}",
),
max_provider_spend_usd: float | None = typer.Option(
None,
"--max-provider-spend-usd",
min=0,
help=f"USD limit on your own AI and search provider keys; {SPEND_HELP_SUFFIX}",
),
) -> None:
from discolike_cli.main import get_client

emit(
get_client(ctx).prospecting.update_plan(
run_id,
build_request(
ProspectingPlanSettings,
_merge_params(
None,
plan_version=plan_version,
contact_integration_id=contact_integration_id,
validation_integration_id=validation_integration_id,
search_provider_id=search_provider_id,
target_companies=target_companies,
contacts_per_company=contacts_per_company,
deliverable=deliverable,
goal=goal,
max_spend_usd=max_spend_usd,
max_provider_spend_usd=max_provider_spend_usd,
),
),
)
)


@app.command("message")
@handle_errors
def message_command(
Expand Down
Loading
Loading