discolike-cli 0.4.1__tar.gz → 0.6.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.
Files changed (49) hide show
  1. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/PKG-INFO +30 -6
  2. discolike_cli-0.6.0/README.md +103 -0
  3. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/pyproject.toml +3 -4
  4. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_help.py +3 -0
  5. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_output.py +3 -2
  6. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/auth.py +2 -2
  7. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/bulk.py +2 -2
  8. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/contacts.py +27 -9
  9. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/discogen.py +1 -1
  10. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/email.py +5 -5
  11. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/main.py +2 -0
  12. discolike_cli-0.6.0/src/discolike_cli/prospecting.py +438 -0
  13. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/queries.py +2 -2
  14. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_contacts_cli.py +68 -2
  15. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_output.py +3 -3
  16. discolike_cli-0.6.0/tests/test_prospecting_cli.py +549 -0
  17. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_sdk_parity.py +1 -0
  18. discolike_cli-0.4.1/README.md +0 -78
  19. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/.gitignore +0 -0
  20. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/LICENSE +0 -0
  21. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/__init__.py +0 -0
  22. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_inputs.py +0 -0
  23. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_loopback.py +0 -0
  24. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/account.py +0 -0
  25. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/company.py +0 -0
  26. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/discover.py +0 -0
  27. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/enrich.py +0 -0
  28. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/match.py +0 -0
  29. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/providers.py +0 -0
  30. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/py.typed +0 -0
  31. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/signup.py +0 -0
  32. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/conftest.py +0 -0
  33. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_account.py +0 -0
  34. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_auth.py +0 -0
  35. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_auth_oauth.py +0 -0
  36. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_bulk_cli.py +0 -0
  37. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_company.py +0 -0
  38. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_discogen_cli.py +0 -0
  39. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_discover.py +0 -0
  40. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_email_cli.py +0 -0
  41. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_enrich_cli.py +0 -0
  42. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_entrypoint.py +0 -0
  43. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_file_inputs_cli.py +0 -0
  44. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_help.py +0 -0
  45. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_inputs.py +0 -0
  46. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_match_cli.py +0 -0
  47. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_providers_cli.py +0 -0
  48. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_queries_cli.py +0 -0
  49. {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_signup_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: discolike-cli
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: Official CLI for the DiscoLike API
5
5
  Project-URL: Homepage, https://www.discolike.com
6
6
  Project-URL: Documentation, https://docs.discolike.com
@@ -17,15 +17,14 @@ Classifier: Intended Audience :: Developers
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
19
  Classifier: Programming Language :: Python :: 3 :: Only
20
- Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
22
  Classifier: Programming Language :: Python :: 3.13
24
23
  Classifier: Programming Language :: Python :: 3.14
25
24
  Classifier: Topic :: Utilities
26
25
  Classifier: Typing :: Typed
27
- Requires-Python: >=3.10
28
- Requires-Dist: discolike==0.4.1
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: discolike==0.6.0
29
28
  Requires-Dist: rich>=13.0
30
29
  Requires-Dist: typer<0.28,>=0.26.1
31
30
  Description-Content-Type: text/markdown
@@ -46,7 +45,7 @@ Or run it without installing:
46
45
  uvx --from discolike-cli discolike --help
47
46
  ```
48
47
 
49
- Requires Python 3.10+. Installing this package gives you the `discolike` command.
48
+ Requires Python 3.11+. Installing this package gives you the `discolike` command.
50
49
 
51
50
  ## Authentication
52
51
 
@@ -67,7 +66,7 @@ discolike company data stripe.com
67
66
  discolike extract https://stripe.com/enterprise
68
67
  ```
69
68
 
70
- Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
69
+ Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `prospecting`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
71
70
 
72
71
  ### Volume pulls
73
72
 
@@ -81,6 +80,30 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
81
80
 
82
81
  `companies` saves each page as an exclusion list (`<run-name>-round-N`) and excludes it from the next page; rerunning with the same `--out` resumes from the CSV. `contacts` slices the domain list at `10000 / per-company` domains per call and records finished slices in `<out>.checkpoint`. Both keep one call in flight under `--rate-limit` (default 10/min, the Pro rate on `/discover` and `/contacts`), retry on 429/5xx, and print a JSON summary at the end. Filters come from `--params-file`, `--param` and the common flags; the paging fields are managed for you.
83
82
 
83
+ ### Managed prospecting
84
+
85
+ ```bash
86
+ discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
87
+ discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
88
+ discolike prospecting wait RUN_ID
89
+ # Review the proposed plan; change its engines or size if needed (returns a new plan_version):
90
+ discolike prospecting plan RUN_ID --plan-version 1 --contact-integration-id LLM_ID --search-provider-id SEARCH_ID
91
+ # Then approve the exact version you saw:
92
+ discolike prospecting approve RUN_ID --plan-version 2
93
+ discolike prospecting list --limit 20
94
+ discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
95
+ discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
96
+ discolike prospecting cancel RUN_ID
97
+ ```
98
+
99
+ `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.
100
+
101
+ `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.
102
+
103
+ `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.
104
+
105
+ 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.
106
+
84
107
  ### Conventions
85
108
 
86
109
  - Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
@@ -99,6 +122,7 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
99
122
  | 4 | Rate limited |
100
123
  | 5 | Network error |
101
124
  | 6 | Not found |
125
+ | 7 | Needs input: `prospecting wait` reached a checkpoint with no terminal to ask (or `--no-input`) |
102
126
 
103
127
  ## Links
104
128
 
@@ -0,0 +1,103 @@
1
+ # discolike-cli
2
+
3
+ Official CLI for the [DiscoLike API](https://discolike.com) — the `discolike` command from your terminal. Built on the [`discolike`](https://pypi.org/project/discolike/) SDK.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install discolike-cli
9
+ ```
10
+
11
+ Or run it without installing:
12
+
13
+ ```bash
14
+ uvx --from discolike-cli discolike --help
15
+ ```
16
+
17
+ Requires Python 3.11+. Installing this package gives you the `discolike` command.
18
+
19
+ ## Authentication
20
+
21
+ ```bash
22
+ discolike auth login
23
+ ```
24
+
25
+ Opens your browser to log in (add `--no-browser` to print the URL instead, `--port` to pin the loopback port when forwarding over SSH). To use an API key instead, pass `--api-key KEY` or `--method api_key` to be prompted; create a key at [app.discolike.com/account/management/keys](https://app.discolike.com/account/management/keys). You can also set `DISCOLIKE_API_KEY` in the environment instead.
26
+
27
+ ## Quickstart
28
+
29
+ ```bash
30
+ discolike discover --icp-prompt "managed IT services for SMBs" --country US --max-records 25
31
+ discolike match "Stripe Inc" --city "San Francisco"
32
+ discolike match --file companies.csv --name-column company_name --wait
33
+ discolike count --phrase-match "book a demo" --country US
34
+ discolike company data stripe.com
35
+ discolike extract https://stripe.com/enterprise
36
+ ```
37
+
38
+ Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `prospecting`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
39
+
40
+ ### Volume pulls
41
+
42
+ `discolike bulk` walks past the 10,000-per-search ceiling and pulls contacts for a whole domain list into one CSV, with checkpoint and resume:
43
+
44
+ ```bash
45
+ discolike bulk companies --params-file form.json --max-companies 50000 --run-name agencies --out companies.csv
46
+ discolike bulk estimate --domains-file companies.csv --per-company 10 # free size check
47
+ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary "growth marketing" --out contacts.csv
48
+ ```
49
+
50
+ `companies` saves each page as an exclusion list (`<run-name>-round-N`) and excludes it from the next page; rerunning with the same `--out` resumes from the CSV. `contacts` slices the domain list at `10000 / per-company` domains per call and records finished slices in `<out>.checkpoint`. Both keep one call in flight under `--rate-limit` (default 10/min, the Pro rate on `/discover` and `/contacts`), retry on 429/5xx, and print a JSON summary at the end. Filters come from `--params-file`, `--param` and the common flags; the paging fields are managed for you.
51
+
52
+ ### Managed prospecting
53
+
54
+ ```bash
55
+ discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
56
+ discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
57
+ discolike prospecting wait RUN_ID
58
+ # Review the proposed plan; change its engines or size if needed (returns a new plan_version):
59
+ discolike prospecting plan RUN_ID --plan-version 1 --contact-integration-id LLM_ID --search-provider-id SEARCH_ID
60
+ # Then approve the exact version you saw:
61
+ discolike prospecting approve RUN_ID --plan-version 2
62
+ discolike prospecting list --limit 20
63
+ discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
64
+ discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
65
+ discolike prospecting cancel RUN_ID
66
+ ```
67
+
68
+ `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.
69
+
70
+ `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.
71
+
72
+ `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.
73
+
74
+ 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.
75
+
76
+ ### Conventions
77
+
78
+ - Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
79
+ - Pass `--format table` for a human-readable table — used automatically when stdout is a TTY.
80
+ - Volume inputs come from files: `--domains-file companies.csv` (a `domain` column, or one domain per line) on `queries create-exclusion-list`, `contacts discover|search|count|generate`, `discogen run` and `validate-icp`; `--params-file form.json` (a JSON object of API parameter names, e.g. an app form) on `discover`, `count` and `contacts discover|search|count`; `--exclude-domains-file` on `discover`. Precedence: file < `--param` < flags.
81
+ - Async endpoints (`match --file`, `discogen run`, `discogen run-personas`, `segment`, `validate-icp`) take `--wait` to block until the job finishes. Without it, you get a `task_id` back to poll with `discolike discogen status <task_id> --family <family>`. `append` is synchronous — it returns enriched rows directly (or writes CSV bytes to `--output`).
82
+
83
+ ### Exit codes
84
+
85
+ | Exit code | Meaning |
86
+ |---|---|
87
+ | 0 | Success |
88
+ | 1 | Server error or unexpected failure |
89
+ | 2 | Validation error |
90
+ | 3 | Authentication or plan-access error |
91
+ | 4 | Rate limited |
92
+ | 5 | Network error |
93
+ | 6 | Not found |
94
+ | 7 | Needs input: `prospecting wait` reached a checkpoint with no terminal to ask (or `--no-input`) |
95
+
96
+ ## Links
97
+
98
+ - **API documentation**: [docs.discolike.com](https://docs.discolike.com)
99
+ - **Source**: [github.com/Discolike/discolike-python](https://github.com/Discolike/discolike-python)
100
+
101
+ ## License
102
+
103
+ [MIT](https://github.com/Discolike/discolike-python/blob/main/LICENSE)
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "discolike-cli"
7
- version = "0.4.1"
7
+ version = "0.6.0"
8
8
  description = "Official CLI for the DiscoLike API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
- requires-python = ">=3.10"
12
+ requires-python = ">=3.11"
13
13
  authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
14
14
  dependencies = [
15
- "discolike==0.4.1",
15
+ "discolike==0.6.0",
16
16
  "typer>=0.26.1,<0.28",
17
17
  "rich>=13.0",
18
18
  ]
@@ -21,7 +21,6 @@ classifiers = [
21
21
  "Development Status :: 4 - Beta",
22
22
  "Intended Audience :: Developers",
23
23
  "Programming Language :: Python :: 3",
24
- "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
26
25
  "Programming Language :: Python :: 3.12",
27
26
  "Programming Language :: Python :: 3.13",
@@ -74,6 +74,9 @@ Exit codes:
74
74
  4 rate_limited HTTP 429; wait "retry_after" seconds, then retry
75
75
  5 network_error could not reach the API
76
76
  6 not_found HTTP 404
77
+ 7 needs_input `prospecting wait` stopped at a checkpoint with no terminal to ask
78
+ (or --no-input): the run is on stdout, the question and
79
+ "suggested_replies" in the stderr envelope
77
80
 
78
81
  Environment:
79
82
  {ENV_API_KEY} API key; overrides the config file written by `discolike auth login`.
@@ -43,6 +43,7 @@ EXIT_CODES: dict[type, int] = {
43
43
  NotFoundError: 6,
44
44
  }
45
45
  DEFAULT_EXIT_CODE = 1
46
+ NEEDS_INPUT_EXIT_CODE = 7
46
47
 
47
48
  # Stable, snake_case error codes for agents and scripts to branch on. The class
48
49
  # name in `error` is kept for backwards compatibility; `code` is the contract.
@@ -82,7 +83,7 @@ class SupportsWait(Protocol):
82
83
  task_id: str
83
84
  task_family: str
84
85
 
85
- def wait(self, *, timeout: float, on_poll: Callable[[_JobStatusLike], None] | None = None) -> _JobStatusLike: ...
86
+ def wait(self, *, max_wait: float, on_poll: Callable[[_JobStatusLike], None] | None = None) -> _JobStatusLike: ...
86
87
 
87
88
 
88
89
  def _normalize(data: Any) -> Any: # noqa: ANN401 -- accepts arbitrary JSON-serializable CLI output data
@@ -203,5 +204,5 @@ def run_job(job: SupportsWait, *, wait: bool, timeout: float, fmt: str | None =
203
204
  def _on_poll(status: _JobStatusLike) -> None:
204
205
  sys.stderr.write(f"progress: {status.progress}%\n")
205
206
 
206
- final = job.wait(timeout=timeout, on_poll=_on_poll)
207
+ final = job.wait(max_wait=timeout, on_poll=_on_poll)
207
208
  emit(final.results if final.results is not None else final.to_dict(), fmt=fmt)
@@ -4,8 +4,8 @@ import json
4
4
  import secrets
5
5
  import sys
6
6
  import webbrowser
7
+ from datetime import UTC
7
8
  from datetime import datetime
8
- from datetime import timezone
9
9
  from typing import Any
10
10
  from typing import NoReturn
11
11
  from urllib.parse import urlparse
@@ -88,7 +88,7 @@ def _offer_signup(ctx: typer.Context) -> None:
88
88
 
89
89
 
90
90
  def _iso(epoch_seconds: float) -> str:
91
- return datetime.fromtimestamp(epoch_seconds, tz=timezone.utc).isoformat()
91
+ return datetime.fromtimestamp(epoch_seconds, tz=UTC).isoformat()
92
92
 
93
93
 
94
94
  def _global_key_source(ctx: typer.Context) -> str:
@@ -382,8 +382,8 @@ def _persona_ids_in(path: pathlib.Path) -> set[str]:
382
382
 
383
383
 
384
384
  ICP_PROMPT_HELP = "Natural-language ICP prompt used to derive contact filters."
385
- SUMMARY_HELP = "Filter by profile summary text (semantic search); ranks who comes back per company."
386
- NEGATE_SUMMARY_HELP = "Exclude contacts matching this summary description."
385
+ SUMMARY_HELP = "Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. Ranks who comes back per company."
386
+ NEGATE_SUMMARY_HELP = "Exclude contacts whose profile summary contains every word of any one term, in any order."
387
387
  HAS_EMAIL_HELP = "Only contacts with an email address (on by default)."
388
388
 
389
389
 
@@ -30,15 +30,25 @@ WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
30
30
  TIMEOUT_HELP = "Max seconds to wait with --wait."
31
31
  PARAM_HELP = "Extra key=value query parameter forwarded to the SDK (repeatable)."
32
32
  JOBSTART_DATE_HELP = "Job start date filter: min date or 'min,max' range, e.g. 2025-01-01 or 2025-01-01,2025-06-30."
33
- SUMMARY_HELP = "Filter by profile summary text (semantic search)."
34
- NEGATE_SUMMARY_HELP = "Exclude contacts matching this summary description."
33
+ SUMMARY_HELP = "Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it."
34
+ NEGATE_SUMMARY_HELP = "Exclude contacts whose profile summary contains every word of any one term, in any order."
35
35
  NAME_HELP = "Filter by contact name (partial match supported)."
36
36
  SKILLS_HELP = "Filter by skill (repeatable)."
37
37
  FILTER_STATE_HELP = "Filter by company state/region (repeatable)."
38
38
  NEGATE_FILTER_STATE_HELP = "Exclude contacts at companies in this state (repeatable)."
39
39
  PERSON_STATE_HELP = "Filter by contact state/region (repeatable)."
40
40
  PERSONA_ID_HELP = "Filter by persona ID (repeatable)."
41
- EMAIL_VALIDATED_HELP = "Only contacts with (or without) a validated email address."
41
+ EMAIL_VALIDATED_HELP = (
42
+ "Deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead."
43
+ )
44
+ EMAIL_VALIDATED_WARNING = (
45
+ "warning: --email-validated is deprecated. Treated as has_email: "
46
+ "contact search no longer filters on email verification; verify emails instead."
47
+ )
48
+ NO_EMAIL_VALIDATED_WARNING = (
49
+ "warning: --no-email-validated is deprecated and has no effect. "
50
+ "Contact search no longer filters on email verification; verify emails instead."
51
+ )
42
52
  HAS_PHONE_HELP = "Only contacts with (or without) a phone number."
43
53
  HAS_MOBILE_HELP = "Only contacts with (or without) a mobile phone number."
44
54
  HAS_LINKEDIN_HELP = "Only contacts with (or without) a LinkedIn profile."
@@ -50,6 +60,17 @@ RESULTS_BY_COMPANY_HELP = "Maximum contacts per company domain (default 5; 0 rem
50
60
  INCLUDE_SEARCH_CONTACTS_HELP = "Include contacts from the search index (broader coverage)."
51
61
  CONSENSUS_HELP = "Number of query vectors to combine for consensus search."
52
62
 
63
+
64
+ def _email_validated_as_has_email(*, email_validated: bool | None, has_email: bool | None) -> bool | None:
65
+ if email_validated is None:
66
+ return has_email
67
+ if not email_validated:
68
+ typer.echo(NO_EMAIL_VALIDATED_WARNING, err=True)
69
+ return has_email
70
+ typer.echo(EMAIL_VALIDATED_WARNING, err=True)
71
+ return True if has_email is None else has_email
72
+
73
+
53
74
  app = typer.Typer(
54
75
  help="Find contacts: search and count by filters, look up or match individuals, and run bulk/generative discovery jobs."
55
76
  )
@@ -137,8 +158,7 @@ def search_command(
137
158
  filter_state=filter_state,
138
159
  negate_filter_state=negate_filter_state,
139
160
  employee_range=employee_range,
140
- has_email=has_email,
141
- email_validated=email_validated,
161
+ has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
142
162
  has_phone=has_phone,
143
163
  has_mobile=has_mobile,
144
164
  has_linkedin=has_linkedin,
@@ -238,8 +258,7 @@ def count_command(
238
258
  filter_state=filter_state,
239
259
  negate_filter_state=negate_filter_state,
240
260
  employee_range=employee_range,
241
- has_email=has_email,
242
- email_validated=email_validated,
261
+ has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
243
262
  has_phone=has_phone,
244
263
  has_mobile=has_mobile,
245
264
  has_linkedin=has_linkedin,
@@ -413,8 +432,7 @@ def discover_command(
413
432
  filter_state=filter_state,
414
433
  negate_filter_state=negate_filter_state,
415
434
  employee_range=employee_range,
416
- has_email=has_email,
417
- email_validated=email_validated,
435
+ has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
418
436
  has_phone=has_phone,
419
437
  has_mobile=has_mobile,
420
438
  has_linkedin=has_linkedin,
@@ -44,7 +44,7 @@ TASK_ID_HELP = "Task ID returned when the job was started."
44
44
  app = typer.Typer(help="Run DiscoGen research jobs and check status of or cancel any async task (see --family)")
45
45
 
46
46
 
47
- class TaskFamily(str, enum.Enum):
47
+ class TaskFamily(enum.StrEnum):
48
48
  discogen = "discogen"
49
49
  bulkmatch = "bulkmatch"
50
50
  contactmatch = "contactmatch"
@@ -71,7 +71,7 @@ def _fetch_batch_snapshot(batch: EmailBatch) -> EmailBatchResults:
71
71
  """Fetch the batch results once, whether or not the batch has finished."""
72
72
  snapshot: dict[str, EmailBatchResults] = {}
73
73
  try:
74
- return batch.results(timeout=0, on_poll=lambda results: snapshot.update(latest=results))
74
+ return batch.results(max_wait=0, on_poll=lambda results: snapshot.update(latest=results))
75
75
  except JobTimeoutError:
76
76
  return snapshot["latest"]
77
77
 
@@ -101,7 +101,7 @@ def find_command(
101
101
  if not wait:
102
102
  emit({"job_id": job.job_id, "hint": f"poll with: discolike email job {job.job_id}"})
103
103
  return
104
- emit(job.wait(timeout=timeout, on_poll=_job_status_to_stderr), fmt=fmt)
104
+ emit(job.wait(max_wait=timeout, on_poll=_job_status_to_stderr), fmt=fmt)
105
105
 
106
106
 
107
107
  @app.command("find-batch")
@@ -136,7 +136,7 @@ def find_batch_command(
136
136
  if not wait:
137
137
  emit({"batch_id": batch.batch_id, "hint": f"fetch with: discolike email results {batch.batch_id}"})
138
138
  return
139
- emit(batch.results(timeout=timeout, on_poll=_batch_progress_to_stderr), fmt=fmt)
139
+ emit(batch.results(max_wait=timeout, on_poll=_batch_progress_to_stderr), fmt=fmt)
140
140
 
141
141
 
142
142
  @app.command("results")
@@ -156,7 +156,7 @@ def results_command(
156
156
  raise typer.BadParameter(f"--kind must be one of: {', '.join(EMAIL_KINDS)}")
157
157
  batch = get_client(ctx).email.batch(batch_id, kind=kind) # type: ignore[arg-type]
158
158
  if wait:
159
- emit(batch.results(timeout=timeout, on_poll=_batch_progress_to_stderr), fmt=fmt)
159
+ emit(batch.results(max_wait=timeout, on_poll=_batch_progress_to_stderr), fmt=fmt)
160
160
  return
161
161
  emit(_fetch_batch_snapshot(batch), fmt=fmt)
162
162
 
@@ -175,6 +175,6 @@ def job_command(
175
175
 
176
176
  job = get_client(ctx).email.job(job_id)
177
177
  if wait:
178
- emit(job.wait(timeout=timeout, on_poll=_job_status_to_stderr), fmt=fmt)
178
+ emit(job.wait(max_wait=timeout, on_poll=_job_status_to_stderr), fmt=fmt)
179
179
  return
180
180
  emit(job.status(), fmt=fmt)
@@ -23,6 +23,7 @@ from discolike_cli import discover
23
23
  from discolike_cli import email
24
24
  from discolike_cli import enrich
25
25
  from discolike_cli import match
26
+ from discolike_cli import prospecting
26
27
  from discolike_cli import providers
27
28
  from discolike_cli import queries
28
29
  from discolike_cli import signup
@@ -85,6 +86,7 @@ app.add_typer(contacts.app, name="contacts")
85
86
  app.add_typer(discogen.app, name="discogen")
86
87
  app.add_typer(email.app, name="email")
87
88
  app.add_typer(queries.app, name="queries")
89
+ app.add_typer(prospecting.app, name="prospecting")
88
90
  app.add_typer(account.app, name="account")
89
91
  app.add_typer(providers.search_providers_app, name="search-providers")
90
92
  app.add_typer(providers.llm_providers_app, name="llm-providers")