discolike-cli 0.3.0__tar.gz → 0.3.2__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 (38) hide show
  1. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/PKG-INFO +2 -2
  2. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/pyproject.toml +2 -2
  3. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/auth.py +32 -5
  4. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/company.py +10 -5
  5. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/contacts.py +149 -0
  6. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/discover.py +89 -0
  7. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/enrich.py +11 -3
  8. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/main.py +2 -0
  9. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/match.py +25 -1
  10. discolike_cli-0.3.2/src/discolike_cli/signup.py +76 -0
  11. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_auth_oauth.py +91 -5
  12. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_company.py +24 -0
  13. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_contacts_cli.py +162 -9
  14. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_discover.py +114 -0
  15. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_enrich_cli.py +37 -0
  16. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_match_cli.py +55 -0
  17. discolike_cli-0.3.2/tests/test_sdk_parity.py +99 -0
  18. discolike_cli-0.3.2/tests/test_signup_cli.py +104 -0
  19. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/.gitignore +0 -0
  20. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/LICENSE +0 -0
  21. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/README.md +0 -0
  22. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/__init__.py +0 -0
  23. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/_loopback.py +0 -0
  24. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/_output.py +0 -0
  25. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/account.py +0 -0
  26. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/discogen.py +0 -0
  27. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/email.py +0 -0
  28. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/providers.py +0 -0
  29. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/py.typed +0 -0
  30. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/src/discolike_cli/queries.py +0 -0
  31. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/conftest.py +0 -0
  32. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_account.py +0 -0
  33. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_auth.py +0 -0
  34. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_discogen_cli.py +0 -0
  35. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_email_cli.py +0 -0
  36. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_output.py +0 -0
  37. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_providers_cli.py +0 -0
  38. {discolike_cli-0.3.0 → discolike_cli-0.3.2}/tests/test_queries_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: discolike-cli
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -25,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.14
25
25
  Classifier: Topic :: Utilities
26
26
  Classifier: Typing :: Typed
27
27
  Requires-Python: >=3.10
28
- Requires-Dist: discolike==0.3.0
28
+ Requires-Dist: discolike==0.3.2
29
29
  Requires-Dist: rich>=13.0
30
30
  Requires-Dist: typer>=0.12
31
31
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "discolike-cli"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Official CLI for the DiscoLike API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -12,7 +12,7 @@ license-files = ["LICENSE"]
12
12
  requires-python = ">=3.10"
13
13
  authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
14
14
  dependencies = [
15
- "discolike==0.3.0",
15
+ "discolike==0.3.2",
16
16
  "typer>=0.12",
17
17
  "rich>=13.0",
18
18
  ]
@@ -33,11 +33,12 @@ from discolike._oauth import OAuthError
33
33
  from discolike._oauth import build_authorization_url
34
34
  from discolike._oauth import discover
35
35
  from discolike._oauth import exchange_code
36
- from discolike._oauth import pkce_pair
37
36
  from discolike._oauth import register_client
38
37
  from discolike_cli._loopback import CallbackServer
39
38
  from discolike_cli._output import emit
40
39
  from discolike_cli._output import handle_errors
40
+ from discolike_cli.signup import _is_interactive
41
+ from discolike_cli.signup import run_signup
41
42
 
42
43
  app = typer.Typer(help="Manage credentials: log in (browser or API key), check status, log out.")
43
44
 
@@ -55,6 +56,10 @@ LOGIN_METHODS = (AUTH_METHOD_OAUTH, AUTH_METHOD_API_KEY)
55
56
  DEAD_CLIENT_ERRORS = frozenset({"invalid_client", "unauthorized_client"})
56
57
 
57
58
 
59
+ HAS_ACCOUNT_PROMPT = "Do you already have a DiscoLike account?"
60
+ SIGNUP_FOLLOWUP_MESSAGE = "Confirm the email, then run `discolike auth login` again to sign in."
61
+
62
+
58
63
  class _DeadClientError(Exception):
59
64
  """The authorization server no longer recognises the registered client_id."""
60
65
 
@@ -63,6 +68,23 @@ def _mask(key: str) -> str:
63
68
  return "…" + key[-MASKED_VISIBLE_CHARS:]
64
69
 
65
70
 
71
+ def _was_passed_on_command_line(ctx: typer.Context, name: str) -> bool:
72
+ source = ctx.get_parameter_source(name)
73
+ return source is not None and source.name == "COMMANDLINE"
74
+
75
+
76
+ def _offer_signup(ctx: typer.Context) -> None:
77
+ email = typer.prompt("Work email")
78
+ first_name = typer.prompt("First name")
79
+ last_name = typer.prompt("Last name")
80
+ base_url = str(ctx.obj.get("base_url") or DEFAULT_BASE_URL).rstrip("/")
81
+ run_signup(
82
+ email=email, first_name=first_name, last_name=last_name, agent=None, base_url=base_url, yes=False, fmt=None
83
+ )
84
+ typer.echo(SIGNUP_FOLLOWUP_MESSAGE)
85
+ raise typer.Exit(code=0)
86
+
87
+
66
88
  def _iso(epoch_seconds: float) -> str:
67
89
  return datetime.fromtimestamp(epoch_seconds, tz=timezone.utc).isoformat()
68
90
 
@@ -135,13 +157,11 @@ def _authorize(
135
157
  open_browser: bool,
136
158
  http: httpx2.Client,
137
159
  ) -> OAuthCredential:
138
- verifier, challenge = pkce_pair()
139
160
  state = secrets.token_urlsafe(STATE_BYTES)
140
- url = build_authorization_url(
161
+ url, verifier = build_authorization_url(
141
162
  metadata,
142
163
  client_id=registration.client_id,
143
164
  redirect_uri=registration.redirect_uri,
144
- code_challenge=challenge,
145
165
  state=state,
146
166
  resource=resource,
147
167
  )
@@ -168,7 +188,6 @@ def _authorize(
168
188
  code_verifier=verifier,
169
189
  redirect_uri=registration.redirect_uri,
170
190
  resource=resource,
171
- client=http,
172
191
  )
173
192
  except OAuthError as exc:
174
193
  if exc.error in DEAD_CLIENT_ERRORS:
@@ -230,6 +249,14 @@ def login(
230
249
  if method not in LOGIN_METHODS:
231
250
  raise typer.BadParameter(f"must be one of {', '.join(LOGIN_METHODS)}", param_hint="--method")
232
251
  global_key_passed = ctx.obj.get("api_key") is not None and _global_key_source(ctx) == SOURCE_OPTION
252
+ if (
253
+ _is_interactive()
254
+ and api_key is None
255
+ and not global_key_passed
256
+ and not _was_passed_on_command_line(ctx, "method")
257
+ and not typer.confirm(HAS_ACCOUNT_PROMPT, default=True)
258
+ ):
259
+ _offer_signup(ctx)
233
260
  if api_key or global_key_passed or method == AUTH_METHOD_API_KEY:
234
261
  _api_key_login(ctx, api_key=api_key)
235
262
  return
@@ -134,12 +134,17 @@ def public_links(
134
134
  @handle_errors
135
135
  def extract_command(
136
136
  ctx: typer.Context,
137
- url: str = typer.Argument(..., help="Page URL to extract content from"),
137
+ url: str | None = typer.Argument(None, help="Page URL to extract content from"),
138
+ domain: str | None = typer.Option(
139
+ None, "--domain", help="Bare domain to extract instead of URL (alias for https://DOMAIN, served from cache)."
140
+ ),
138
141
  fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
139
142
  ) -> None:
140
- """Extract page content from a URL."""
143
+ """Extract page content from a URL or a bare domain."""
141
144
  from discolike_cli.main import get_client
142
145
 
143
- emit(
144
- get_client(ctx).companies.extract(build_request(CompaniesExtractParams, _merge_params(None, url=url))), fmt=fmt
145
- )
146
+ if (url is None) == (domain is None):
147
+ raise typer.BadParameter("Provide exactly one of URL or --domain")
148
+
149
+ request = build_request(CompaniesExtractParams, _merge_params(None, url=url, domain=domain))
150
+ emit(get_client(ctx).companies.extract(request), fmt=fmt)
@@ -25,6 +25,25 @@ WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
25
25
  TIMEOUT_HELP = "Max seconds to wait with --wait."
26
26
  PARAM_HELP = "Extra key=value query parameter forwarded to the SDK (repeatable)."
27
27
  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."
28
+ SUMMARY_HELP = "Filter by profile summary text (semantic search)."
29
+ NEGATE_SUMMARY_HELP = "Exclude contacts matching this summary description."
30
+ NAME_HELP = "Filter by contact name (partial match supported)."
31
+ SKILLS_HELP = "Filter by skill (repeatable)."
32
+ FILTER_STATE_HELP = "Filter by company state/region (repeatable)."
33
+ NEGATE_FILTER_STATE_HELP = "Exclude contacts at companies in this state (repeatable)."
34
+ PERSON_STATE_HELP = "Filter by contact state/region (repeatable)."
35
+ PERSONA_ID_HELP = "Filter by persona ID (repeatable)."
36
+ EMAIL_VALIDATED_HELP = "Only contacts with (or without) a validated email address."
37
+ HAS_PHONE_HELP = "Only contacts with (or without) a phone number."
38
+ HAS_MOBILE_HELP = "Only contacts with (or without) a mobile phone number."
39
+ HAS_LINKEDIN_HELP = "Only contacts with (or without) a LinkedIn profile."
40
+ MIN_CONNECTIONS_HELP = "Minimum LinkedIn connections required."
41
+ INCLUSION_QUERY_ID_HELP = "Include only contacts from companies in this saved query (repeatable)."
42
+ EXCLUSION_QUERY_ID_HELP = "Exclude contacts from companies in this saved query (repeatable)."
43
+ MAX_COMPANIES_HELP = "Maximum number of enriched companies to return; cannot be combined with --max-records."
44
+ RESULTS_BY_COMPANY_HELP = "Maximum contacts per company domain (default 5; 0 removes the cap)."
45
+ INCLUDE_SEARCH_CONTACTS_HELP = "Include contacts from the search index (broader coverage)."
46
+ CONSENSUS_HELP = "Number of query vectors to combine for consensus search."
28
47
 
29
48
  app = typer.Typer(
30
49
  help="Find contacts: search and count by filters, look up or match individuals, and run bulk/generative discovery jobs."
@@ -42,20 +61,43 @@ def search_command(
42
61
  negate_department: list[str] | None = typer.Option(None, help="Exclude departments (repeatable)."),
43
62
  title: list[str] | None = typer.Option(None, help="Filter by job title (repeatable)."),
44
63
  negate_title: list[str] | None = typer.Option(None, help="Exclude job titles (repeatable)."),
64
+ name: str | None = typer.Option(None, help=NAME_HELP),
65
+ summary: str | None = typer.Option(None, help=SUMMARY_HELP),
66
+ negate_summary: str | None = typer.Option(None, help=NEGATE_SUMMARY_HELP),
67
+ skills: list[str] | None = typer.Option(None, help=SKILLS_HELP),
45
68
  domain: list[str] | None = typer.Option(None, help="Filter by company domain (repeatable)."),
46
69
  person_country: list[str] | None = typer.Option(None, help="Filter by contact country (repeatable)."),
47
70
  negate_person_country: list[str] | None = typer.Option(None, help="Exclude contact countries (repeatable)."),
71
+ person_state: list[str] | None = typer.Option(None, help=PERSON_STATE_HELP),
72
+ persona_id: list[int] | None = typer.Option(None, help=PERSONA_ID_HELP),
48
73
  filter_industry: list[str] | None = typer.Option(None, help="Filter by company industry (repeatable)."),
49
74
  negate_filter_industry: list[str] | None = typer.Option(None, help="Exclude company industries (repeatable)."),
50
75
  filter_country: list[str] | None = typer.Option(None, help="Filter by company country (repeatable)."),
51
76
  negate_filter_country: list[str] | None = typer.Option(None, help="Exclude company countries (repeatable)."),
77
+ filter_state: list[str] | None = typer.Option(None, help=FILTER_STATE_HELP),
78
+ negate_filter_state: list[str] | None = typer.Option(None, help=NEGATE_FILTER_STATE_HELP),
52
79
  employee_range: str | None = typer.Option(None, help="Company employee range, e.g. 50-200."),
53
80
  has_email: bool | None = typer.Option(
54
81
  None, "--has-email/--no-has-email", help="Only contacts with (or without) an email address."
55
82
  ),
83
+ email_validated: bool | None = typer.Option(
84
+ None, "--email-validated/--no-email-validated", help=EMAIL_VALIDATED_HELP
85
+ ),
86
+ has_phone: bool | None = typer.Option(None, "--has-phone/--no-has-phone", help=HAS_PHONE_HELP),
87
+ has_mobile: bool | None = typer.Option(None, "--has-mobile/--no-has-mobile", help=HAS_MOBILE_HELP),
88
+ has_linkedin: bool | None = typer.Option(None, "--has-linkedin/--no-has-linkedin", help=HAS_LINKEDIN_HELP),
89
+ min_connections: int | None = typer.Option(None, help=MIN_CONNECTIONS_HELP),
56
90
  jobstart_date: str | None = typer.Option(None, "--jobstart-date", help=JOBSTART_DATE_HELP),
91
+ inclusion_query_id: list[str] | None = typer.Option(None, help=INCLUSION_QUERY_ID_HELP),
92
+ exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
57
93
  max_records: int | None = typer.Option(None, help="Maximum number of contacts to return."),
94
+ max_companies: int | None = typer.Option(None, help=MAX_COMPANIES_HELP),
58
95
  offset: int | None = typer.Option(None, help="Number of records to skip for pagination."),
96
+ results_by_company: int | None = typer.Option(None, help=RESULTS_BY_COMPANY_HELP),
97
+ include_search_contacts: bool | None = typer.Option(
98
+ None, "--include-search-contacts/--no-include-search-contacts", help=INCLUDE_SEARCH_CONTACTS_HELP
99
+ ),
100
+ consensus: int | None = typer.Option(None, help=CONSENSUS_HELP),
59
101
  fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
60
102
  param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
61
103
  ) -> None:
@@ -71,18 +113,37 @@ def search_command(
71
113
  negate_department=negate_department,
72
114
  title=title,
73
115
  negate_title=negate_title,
116
+ name=name,
117
+ summary=summary,
118
+ negate_summary=negate_summary,
119
+ skills=skills,
74
120
  domain=domain,
75
121
  person_country=person_country,
76
122
  negate_person_country=negate_person_country,
123
+ person_state=person_state,
124
+ persona_id=persona_id,
77
125
  filter_industry=filter_industry,
78
126
  negate_filter_industry=negate_filter_industry,
79
127
  filter_country=filter_country,
80
128
  negate_filter_country=negate_filter_country,
129
+ filter_state=filter_state,
130
+ negate_filter_state=negate_filter_state,
81
131
  employee_range=employee_range,
82
132
  has_email=has_email,
133
+ email_validated=email_validated,
134
+ has_phone=has_phone,
135
+ has_mobile=has_mobile,
136
+ has_linkedin=has_linkedin,
137
+ min_connections=min_connections,
83
138
  jobstart_date=jobstart_date,
139
+ inclusion_query_id=inclusion_query_id,
140
+ exclusion_query_id=exclusion_query_id,
84
141
  max_records=max_records,
142
+ max_companies=max_companies,
85
143
  offset=offset,
144
+ results_by_company=results_by_company,
145
+ include_search_contacts=include_search_contacts,
146
+ consensus=consensus,
86
147
  )
87
148
  emit(get_client(ctx).contacts.search(build_request(ContactsSearchParams, kwargs)), fmt=fmt)
88
149
 
@@ -98,18 +159,43 @@ def count_command(
98
159
  negate_department: list[str] | None = typer.Option(None, help="Exclude departments (repeatable)."),
99
160
  title: list[str] | None = typer.Option(None, help="Filter by job title (repeatable)."),
100
161
  negate_title: list[str] | None = typer.Option(None, help="Exclude job titles (repeatable)."),
162
+ name: str | None = typer.Option(None, help=NAME_HELP),
163
+ summary: str | None = typer.Option(None, help=SUMMARY_HELP),
164
+ negate_summary: str | None = typer.Option(None, help=NEGATE_SUMMARY_HELP),
165
+ skills: list[str] | None = typer.Option(None, help=SKILLS_HELP),
101
166
  domain: list[str] | None = typer.Option(None, help="Filter by company domain (repeatable)."),
102
167
  person_country: list[str] | None = typer.Option(None, help="Filter by contact country (repeatable)."),
103
168
  negate_person_country: list[str] | None = typer.Option(None, help="Exclude contact countries (repeatable)."),
169
+ person_state: list[str] | None = typer.Option(None, help=PERSON_STATE_HELP),
170
+ persona_id: list[int] | None = typer.Option(None, help=PERSONA_ID_HELP),
104
171
  filter_industry: list[str] | None = typer.Option(None, help="Filter by company industry (repeatable)."),
105
172
  negate_filter_industry: list[str] | None = typer.Option(None, help="Exclude company industries (repeatable)."),
106
173
  filter_country: list[str] | None = typer.Option(None, help="Filter by company country (repeatable)."),
107
174
  negate_filter_country: list[str] | None = typer.Option(None, help="Exclude company countries (repeatable)."),
175
+ filter_state: list[str] | None = typer.Option(None, help=FILTER_STATE_HELP),
176
+ negate_filter_state: list[str] | None = typer.Option(None, help=NEGATE_FILTER_STATE_HELP),
108
177
  employee_range: str | None = typer.Option(None, help="Company employee range, e.g. 50-200."),
109
178
  has_email: bool | None = typer.Option(
110
179
  None, "--has-email/--no-has-email", help="Only contacts with (or without) an email address."
111
180
  ),
181
+ email_validated: bool | None = typer.Option(
182
+ None, "--email-validated/--no-email-validated", help=EMAIL_VALIDATED_HELP
183
+ ),
184
+ has_phone: bool | None = typer.Option(None, "--has-phone/--no-has-phone", help=HAS_PHONE_HELP),
185
+ has_mobile: bool | None = typer.Option(None, "--has-mobile/--no-has-mobile", help=HAS_MOBILE_HELP),
186
+ has_linkedin: bool | None = typer.Option(None, "--has-linkedin/--no-has-linkedin", help=HAS_LINKEDIN_HELP),
187
+ min_connections: int | None = typer.Option(None, help=MIN_CONNECTIONS_HELP),
112
188
  jobstart_date: str | None = typer.Option(None, "--jobstart-date", help=JOBSTART_DATE_HELP),
189
+ inclusion_query_id: list[str] | None = typer.Option(None, help=INCLUSION_QUERY_ID_HELP),
190
+ exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
191
+ max_records: int | None = typer.Option(None, help="Maximum number of contacts to count (20-10000)."),
192
+ max_companies: int | None = typer.Option(None, help=MAX_COMPANIES_HELP),
193
+ offset: int | None = typer.Option(None, help="Number of records to skip for pagination."),
194
+ results_by_company: int | None = typer.Option(None, help=RESULTS_BY_COMPANY_HELP),
195
+ include_search_contacts: bool | None = typer.Option(
196
+ None, "--include-search-contacts/--no-include-search-contacts", help=INCLUDE_SEARCH_CONTACTS_HELP
197
+ ),
198
+ consensus: int | None = typer.Option(None, help=CONSENSUS_HELP),
113
199
  fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
114
200
  param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
115
201
  ) -> None:
@@ -125,16 +211,37 @@ def count_command(
125
211
  negate_department=negate_department,
126
212
  title=title,
127
213
  negate_title=negate_title,
214
+ name=name,
215
+ summary=summary,
216
+ negate_summary=negate_summary,
217
+ skills=skills,
128
218
  domain=domain,
129
219
  person_country=person_country,
130
220
  negate_person_country=negate_person_country,
221
+ person_state=person_state,
222
+ persona_id=persona_id,
131
223
  filter_industry=filter_industry,
132
224
  negate_filter_industry=negate_filter_industry,
133
225
  filter_country=filter_country,
134
226
  negate_filter_country=negate_filter_country,
227
+ filter_state=filter_state,
228
+ negate_filter_state=negate_filter_state,
135
229
  employee_range=employee_range,
136
230
  has_email=has_email,
231
+ email_validated=email_validated,
232
+ has_phone=has_phone,
233
+ has_mobile=has_mobile,
234
+ has_linkedin=has_linkedin,
235
+ min_connections=min_connections,
137
236
  jobstart_date=jobstart_date,
237
+ inclusion_query_id=inclusion_query_id,
238
+ exclusion_query_id=exclusion_query_id,
239
+ max_records=max_records,
240
+ max_companies=max_companies,
241
+ offset=offset,
242
+ results_by_company=results_by_company,
243
+ include_search_contacts=include_search_contacts,
244
+ consensus=consensus,
138
245
  )
139
246
  emit(get_client(ctx).contacts.count(build_request(ContactsCountParams, kwargs)), fmt=fmt)
140
247
 
@@ -220,19 +327,37 @@ def discover_command(
220
327
  negate_department: list[str] | None = typer.Option(None, help="Exclude departments (repeatable)."),
221
328
  title: list[str] | None = typer.Option(None, help="Filter by job title (repeatable)."),
222
329
  negate_title: list[str] | None = typer.Option(None, help="Exclude job titles (repeatable)."),
330
+ name: str | None = typer.Option(None, help=NAME_HELP),
331
+ summary: str | None = typer.Option(None, help=SUMMARY_HELP),
332
+ negate_summary: str | None = typer.Option(None, help=NEGATE_SUMMARY_HELP),
333
+ skills: list[str] | None = typer.Option(None, help=SKILLS_HELP),
223
334
  domain: list[str] | None = typer.Option(None, help="Filter by company domain (repeatable)."),
224
335
  person_country: list[str] | None = typer.Option(None, help="Filter by contact country (repeatable)."),
225
336
  negate_person_country: list[str] | None = typer.Option(None, help="Exclude contact countries (repeatable)."),
337
+ person_state: list[str] | None = typer.Option(None, help=PERSON_STATE_HELP),
338
+ persona_id: list[int] | None = typer.Option(None, help=PERSONA_ID_HELP),
226
339
  filter_industry: list[str] | None = typer.Option(None, help="Filter by company industry (repeatable)."),
227
340
  negate_filter_industry: list[str] | None = typer.Option(None, help="Exclude company industries (repeatable)."),
228
341
  filter_country: list[str] | None = typer.Option(None, help="Filter by company country (repeatable)."),
229
342
  negate_filter_country: list[str] | None = typer.Option(None, help="Exclude company countries (repeatable)."),
343
+ filter_state: list[str] | None = typer.Option(None, help=FILTER_STATE_HELP),
344
+ negate_filter_state: list[str] | None = typer.Option(None, help=NEGATE_FILTER_STATE_HELP),
230
345
  employee_range: str | None = typer.Option(None, help="Company employee range, e.g. 50-200."),
231
346
  has_email: bool | None = typer.Option(
232
347
  None, "--has-email/--no-has-email", help="Only contacts with (or without) an email address."
233
348
  ),
349
+ email_validated: bool | None = typer.Option(
350
+ None, "--email-validated/--no-email-validated", help=EMAIL_VALIDATED_HELP
351
+ ),
352
+ has_phone: bool | None = typer.Option(None, "--has-phone/--no-has-phone", help=HAS_PHONE_HELP),
353
+ has_mobile: bool | None = typer.Option(None, "--has-mobile/--no-has-mobile", help=HAS_MOBILE_HELP),
354
+ has_linkedin: bool | None = typer.Option(None, "--has-linkedin/--no-has-linkedin", help=HAS_LINKEDIN_HELP),
355
+ min_connections: int | None = typer.Option(None, help=MIN_CONNECTIONS_HELP),
234
356
  jobstart_date: str | None = typer.Option(None, "--jobstart-date", help=JOBSTART_DATE_HELP),
357
+ inclusion_query_id: list[str] | None = typer.Option(None, help=INCLUSION_QUERY_ID_HELP),
358
+ exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
235
359
  max_records: int | None = typer.Option(None, help="Maximum number of contacts to return."),
360
+ max_companies: int | None = typer.Option(None, help=MAX_COMPANIES_HELP),
236
361
  offset: int | None = typer.Option(None, help="Number of records to skip for pagination."),
237
362
  results_by_company: int | None = typer.Option(
238
363
  None, "--results-by-company", help="Maximum contacts returned per company."
@@ -258,17 +383,33 @@ def discover_command(
258
383
  negate_department=negate_department,
259
384
  title=title,
260
385
  negate_title=negate_title,
386
+ name=name,
387
+ summary=summary,
388
+ negate_summary=negate_summary,
389
+ skills=skills,
261
390
  domain=domain,
262
391
  person_country=person_country,
263
392
  negate_person_country=negate_person_country,
393
+ person_state=person_state,
394
+ persona_id=persona_id,
264
395
  filter_industry=filter_industry,
265
396
  negate_filter_industry=negate_filter_industry,
266
397
  filter_country=filter_country,
267
398
  negate_filter_country=negate_filter_country,
399
+ filter_state=filter_state,
400
+ negate_filter_state=negate_filter_state,
268
401
  employee_range=employee_range,
269
402
  has_email=has_email,
403
+ email_validated=email_validated,
404
+ has_phone=has_phone,
405
+ has_mobile=has_mobile,
406
+ has_linkedin=has_linkedin,
407
+ min_connections=min_connections,
270
408
  jobstart_date=jobstart_date,
409
+ inclusion_query_id=inclusion_query_id,
410
+ exclusion_query_id=exclusion_query_id,
271
411
  max_records=max_records,
412
+ max_companies=max_companies,
272
413
  offset=offset,
273
414
  results_by_company=results_by_company,
274
415
  include_search_contacts=include_search_contacts,
@@ -283,6 +424,12 @@ def generate_command(
283
424
  ctx: typer.Context,
284
425
  icp_text: str = typer.Option(..., "--icp-text", help="ICP description used to generate contacts."),
285
426
  domain: list[str] = typer.Option(..., "--domain", help="Target company domain (repeatable)."),
427
+ full_domain: list[str] | None = typer.Option(
428
+ None, "--full-domain", help="Domain to send as full_domains to the generation job (repeatable)."
429
+ ),
430
+ partial_domain: list[str] | None = typer.Option(
431
+ None, "--partial-domain", help="Domain to send as partial_domains to the generation job (repeatable)."
432
+ ),
286
433
  context_mode: str | None = typer.Option(None, "--context-mode", help="Context mode for generation."),
287
434
  integration_id: str | None = typer.Option(None, "--integration-id", help="Integration ID to use for generation."),
288
435
  search_provider_id: str | None = typer.Option(
@@ -310,6 +457,8 @@ def generate_command(
310
457
  None,
311
458
  icp_text=icp_text,
312
459
  domains=domain,
460
+ full_domains=full_domain,
461
+ partial_domains=partial_domain,
313
462
  context_mode=context_mode,
314
463
  integration_id=integration_id,
315
464
  search_provider_id=search_provider_id,
@@ -15,6 +15,19 @@ LIST_VALUE_SEPARATOR = ","
15
15
 
16
16
  FORMAT_HELP = "Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data)."
17
17
  PARAM_HELP = "Extra API parameter as KEY=VALUE (comma-separates into a list); see docs.discolike.com"
18
+ SUBDOMAIN_HELP = "Limit results to this subdomain, up to 20, each at least 3 characters (repeatable)."
19
+ NEGATE_SUBDOMAIN_HELP = "Exclude this subdomain, up to 20, each at least 3 characters (repeatable)."
20
+ START_DATE_HELP = "Minimum company start date (YYYY-MM-DD) or range (YYYY-MM-DD,YYYY-MM-DD)."
21
+ REDIRECT_HELP = "Include or exclude domains that redirect to another domain."
22
+ SOCIAL_HELP = (
23
+ "Filter by social platform presence (repeatable): facebook, instagram, linkedin, pinterest, threads, tiktok, "
24
+ "twitter, x, yelp, youtube, googleplay, applestore, amazon, vk, bluesky, xing."
25
+ )
26
+ NEGATE_SOCIAL_HELP = "Exclude companies with this social profile (repeatable); same values as --social."
27
+ LANGUAGE_HELP = "Filter by site language as an ISO 639-1 code, e.g. en, de, pb, zt (repeatable)."
28
+ NEGATE_LANGUAGE_HELP = "Exclude this site language (repeatable); same values as --language."
29
+ EXCLUDE_LEADGEN_HELP = "Exclude suspected lead generation sites (on by default)."
30
+ COUNT_EXCLUDE_LEADGEN_HELP = "Exclude suspected lead generation sites (off by default for count)."
18
31
 
19
32
 
20
33
  def _parse_param(raw: str) -> tuple[str, str | list[str]]:
@@ -45,8 +58,15 @@ def discover_command(
45
58
  negate_country: list[str] | None = typer.Option(None, help="Negate the --country filter (repeatable)."),
46
59
  state: list[str] | None = typer.Option(None, help="State or region filter (repeatable)."),
47
60
  negate_state: list[str] | None = typer.Option(None, help="Negate the --state filter (repeatable)."),
61
+ subdomain: list[str] | None = typer.Option(None, help=SUBDOMAIN_HELP),
62
+ negate_subdomain: list[str] | None = typer.Option(None, help=NEGATE_SUBDOMAIN_HELP),
63
+ language: list[str] | None = typer.Option(None, help=LANGUAGE_HELP),
64
+ negate_language: list[str] | None = typer.Option(None, help=NEGATE_LANGUAGE_HELP),
65
+ social: list[str] | None = typer.Option(None, help=SOCIAL_HELP),
66
+ negate_social: list[str] | None = typer.Option(None, help=NEGATE_SOCIAL_HELP),
48
67
  employee_range: str | None = typer.Option(None, help="Employee count range filter."),
49
68
  revenue_range: str | None = typer.Option(None, help="Revenue range filter."),
69
+ start_date: str | None = typer.Option(None, help=START_DATE_HELP),
50
70
  business_model: list[str] | None = typer.Option(None, help="Business model filter (repeatable)."),
51
71
  negate_business_model: list[str] | None = typer.Option(
52
72
  None, help="Negate the --business-model filter (repeatable)."
@@ -55,7 +75,38 @@ def discover_command(
55
75
  negate_tech_stack: list[str] | None = typer.Option(None, help="Negate the --tech-stack filter (repeatable)."),
56
76
  min_digital_footprint: int | None = typer.Option(None, help="Minimum digital footprint score."),
57
77
  max_digital_footprint: int | None = typer.Option(None, help="Maximum digital footprint score."),
78
+ min_similarity: int | None = typer.Option(None, help="Minimum similarity score to include (0-99)."),
79
+ variance: str | None = typer.Option(
80
+ None, help="Result diversity control: LOW, MID_LOW, MEDIUM, MID_HIGH, HIGH, UNRESTRICTED."
81
+ ),
82
+ consensus: int | None = typer.Option(
83
+ None, help="Number of top results for the consensus search vector (1-20); higher reduces specificity."
84
+ ),
85
+ redirect: bool | None = typer.Option(None, "--redirect/--no-redirect", help=REDIRECT_HELP),
86
+ exclude_leadgen: bool | None = typer.Option(
87
+ None, "--exclude-leadgen/--no-exclude-leadgen", help=EXCLUDE_LEADGEN_HELP
88
+ ),
89
+ retrieval: bool | None = typer.Option(
90
+ None, "--retrieval/--no-retrieval", help="Enable page data retrieval using the Extract API."
91
+ ),
92
+ enhanced: bool | None = typer.Option(
93
+ None, "--enhanced/--no-enhanced", help="Enable AI-powered result enhancement for improved relevance."
94
+ ),
95
+ include_search_domains: bool | None = typer.Option(
96
+ None,
97
+ "--include-search-domains/--no-include-search-domains",
98
+ help="Include the input domains in results (excluded by default).",
99
+ ),
100
+ auto_icp_text: bool | None = typer.Option(
101
+ None, "--auto-icp-text/--no-auto-icp-text", help="Auto-generate ICP text from the provided domain(s)."
102
+ ),
103
+ auto_phrase_match: bool | None = typer.Option(
104
+ None, "--auto-phrase-match/--no-auto-phrase-match", help="Auto-generate phrase matches from ICP text."
105
+ ),
58
106
  exclude_domain: list[str] | None = typer.Option(None, help="Domain to exclude from results (repeatable)."),
107
+ inclusion_query_id: list[str] | None = typer.Option(
108
+ None, help="Saved query ID whose domains are included (repeatable); requires the STARTER plan."
109
+ ),
59
110
  exclusion_query_id: list[str] | None = typer.Option(
60
111
  None, help="Saved query ID whose results are excluded (repeatable)."
61
112
  ),
@@ -81,15 +132,33 @@ def discover_command(
81
132
  negate_country=negate_country,
82
133
  state=state,
83
134
  negate_state=negate_state,
135
+ subdomain=subdomain,
136
+ negate_subdomain=negate_subdomain,
137
+ language=language,
138
+ negate_language=negate_language,
139
+ social=social,
140
+ negate_social=negate_social,
84
141
  employee_range=employee_range,
85
142
  revenue_range=revenue_range,
143
+ start_date=start_date,
86
144
  business_model=business_model,
87
145
  negate_business_model=negate_business_model,
88
146
  tech_stack=tech_stack,
89
147
  negate_tech_stack=negate_tech_stack,
90
148
  min_digital_footprint=min_digital_footprint,
91
149
  max_digital_footprint=max_digital_footprint,
150
+ min_similarity=min_similarity,
151
+ variance=variance,
152
+ consensus=consensus,
153
+ redirect=redirect,
154
+ exclude_leadgen=exclude_leadgen,
155
+ retrieval=retrieval,
156
+ enhanced=enhanced,
157
+ include_search_domains=include_search_domains,
158
+ auto_icp_text=auto_icp_text,
159
+ auto_phrase_match=auto_phrase_match,
92
160
  exclude_domain=exclude_domain,
161
+ inclusion_query_id=inclusion_query_id,
93
162
  exclusion_query_id=exclusion_query_id,
94
163
  max_records=max_records,
95
164
  offset=offset,
@@ -109,8 +178,15 @@ def count_command(
109
178
  negate_country: list[str] | None = typer.Option(None, help="Negate the --country filter (repeatable)."),
110
179
  state: list[str] | None = typer.Option(None, help="State or region filter (repeatable)."),
111
180
  negate_state: list[str] | None = typer.Option(None, help="Negate the --state filter (repeatable)."),
181
+ subdomain: list[str] | None = typer.Option(None, help=SUBDOMAIN_HELP),
182
+ negate_subdomain: list[str] | None = typer.Option(None, help=NEGATE_SUBDOMAIN_HELP),
183
+ language: list[str] | None = typer.Option(None, help=LANGUAGE_HELP),
184
+ negate_language: list[str] | None = typer.Option(None, help=NEGATE_LANGUAGE_HELP),
185
+ social: list[str] | None = typer.Option(None, help=SOCIAL_HELP),
186
+ negate_social: list[str] | None = typer.Option(None, help=NEGATE_SOCIAL_HELP),
112
187
  employee_range: str | None = typer.Option(None, help="Employee count range filter."),
113
188
  revenue_range: str | None = typer.Option(None, help="Revenue range filter."),
189
+ start_date: str | None = typer.Option(None, help=START_DATE_HELP),
114
190
  business_model: list[str] | None = typer.Option(None, help="Business model filter (repeatable)."),
115
191
  negate_business_model: list[str] | None = typer.Option(
116
192
  None, help="Negate the --business-model filter (repeatable)."
@@ -119,6 +195,10 @@ def count_command(
119
195
  negate_tech_stack: list[str] | None = typer.Option(None, help="Negate the --tech-stack filter (repeatable)."),
120
196
  min_digital_footprint: int | None = typer.Option(None, help="Minimum digital footprint score."),
121
197
  max_digital_footprint: int | None = typer.Option(None, help="Maximum digital footprint score."),
198
+ redirect: bool | None = typer.Option(None, "--redirect/--no-redirect", help=REDIRECT_HELP),
199
+ exclude_leadgen: bool | None = typer.Option(
200
+ None, "--exclude-leadgen/--no-exclude-leadgen", help=COUNT_EXCLUDE_LEADGEN_HELP
201
+ ),
122
202
  fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
123
203
  param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
124
204
  ) -> None:
@@ -137,14 +217,23 @@ def count_command(
137
217
  negate_country=negate_country,
138
218
  state=state,
139
219
  negate_state=negate_state,
220
+ subdomain=subdomain,
221
+ negate_subdomain=negate_subdomain,
222
+ language=language,
223
+ negate_language=negate_language,
224
+ social=social,
225
+ negate_social=negate_social,
140
226
  employee_range=employee_range,
141
227
  revenue_range=revenue_range,
228
+ start_date=start_date,
142
229
  business_model=business_model,
143
230
  negate_business_model=negate_business_model,
144
231
  tech_stack=tech_stack,
145
232
  negate_tech_stack=negate_tech_stack,
146
233
  min_digital_footprint=min_digital_footprint,
147
234
  max_digital_footprint=max_digital_footprint,
235
+ redirect=redirect,
236
+ exclude_leadgen=exclude_leadgen,
148
237
  ),
149
238
  )
150
239
  emit(get_client(ctx).count(request), fmt=fmt)
@@ -19,6 +19,7 @@ DEFAULT_WAIT_TIMEOUT_SECONDS = 900.0
19
19
  FORMAT_HELP = "Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data)."
20
20
  WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
21
21
  TIMEOUT_HELP = "Max seconds to wait with --wait."
22
+ QUERY_ID_HELP = "Saved query ID whose domains are included alongside the file/--domain ones (repeatable)."
22
23
 
23
24
 
24
25
  @handle_errors
@@ -76,6 +77,7 @@ def append_command(
76
77
  file: pathlib.Path = typer.Argument(..., help="CSV of domains to enrich."),
77
78
  dataset: list[str] | None = typer.Option(None, "--dataset", help="Dataset to append (repeatable)."),
78
79
  domain_column: str | None = typer.Option(None, "--domain-column", help="Column in the CSV that holds domains."),
80
+ query_id: list[str] | None = typer.Option(None, "--query-id", help=QUERY_ID_HELP),
79
81
  csv: bool | None = typer.Option(
80
82
  None, "--csv/--no-csv", help="Request the enriched rows as CSV (written via --output)."
81
83
  ),
@@ -85,7 +87,9 @@ def append_command(
85
87
  """Enrich a CSV of domains with DiscoLike datasets."""
86
88
  from discolike_cli.main import get_client
87
89
 
88
- request = build_request(AppendParams, _merge_params(None, dataset=dataset, domain_column=domain_column, csv=csv))
90
+ request = build_request(
91
+ AppendParams, _merge_params(None, dataset=dataset, domain_column=domain_column, query_id=query_id, csv=csv)
92
+ )
89
93
  result = get_client(ctx).append(request, file=file)
90
94
  if isinstance(result, bytes):
91
95
  if output is None:
@@ -102,6 +106,7 @@ def segment_command(
102
106
  domain: list[str] | None = typer.Option(None, "--domain", help="Domain to segment (repeatable)."),
103
107
  file: pathlib.Path | None = typer.Option(None, "--file", help="CSV of domains to segment (instead of --domain)."),
104
108
  domain_column: str | None = typer.Option(None, "--domain-column", help="Column in --file that holds domains."),
109
+ query_id: list[str] | None = typer.Option(None, "--query-id", help=QUERY_ID_HELP),
105
110
  max_segments: int | None = typer.Option(None, "--max-segments", help="Maximum number of segments to produce."),
106
111
  wait: bool = typer.Option(False, "--wait", help=WAIT_HELP),
107
112
  timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout", help=TIMEOUT_HELP),
@@ -116,11 +121,14 @@ def segment_command(
116
121
  client = get_client(ctx)
117
122
  if file is not None:
118
123
  request = build_request(
119
- SegmentFileParams, _merge_params(None, domain_column=domain_column, max_segments=max_segments)
124
+ SegmentFileParams,
125
+ _merge_params(None, domain_column=domain_column, query_id=query_id, max_segments=max_segments),
120
126
  )
121
127
  job = client.segment_file(request, file=file)
122
128
  else:
123
129
  assert domain is not None
124
- request = build_request(SegmentParams, _merge_params(None, domains=",".join(domain), max_segments=max_segments))
130
+ request = build_request(
131
+ SegmentParams, _merge_params(None, domains=",".join(domain), query_id=query_id, max_segments=max_segments)
132
+ )
125
133
  job = client.segment(request)
126
134
  run_job(job, wait=wait, timeout=timeout, fmt=fmt)
@@ -18,6 +18,7 @@ from discolike_cli import enrich
18
18
  from discolike_cli import match
19
19
  from discolike_cli import providers
20
20
  from discolike_cli import queries
21
+ from discolike_cli import signup
21
22
 
22
23
  app = typer.Typer(
23
24
  name="discolike",
@@ -70,3 +71,4 @@ app.command(name="extract")(company.extract_command)
70
71
  app.command(name="validate-icp")(enrich.validate_icp_command)
71
72
  app.command(name="append")(enrich.append_command)
72
73
  app.command(name="segment")(enrich.segment_command)
74
+ app.command(name="signup")(signup.signup_command)