autotouch-cli 0.2.73__tar.gz → 0.2.79__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 (55) hide show
  1. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/PKG-INFO +6 -5
  2. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/README.md +5 -4
  3. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/cli.py +53 -9
  4. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/cli_contracts.py +2 -0
  5. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/linkedin.py +35 -162
  6. autotouch_cli-0.2.79/autotouch_cli/commands/list_build.py +185 -0
  7. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/data/CLI_REFERENCE.md +199 -147
  8. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/data/cli-manifest.json +914 -536
  9. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/parser.py +13 -3
  10. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli.egg-info/PKG-INFO +6 -5
  11. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli.egg-info/SOURCES.txt +2 -0
  12. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_shared/linkedin_contract.py +18 -86
  13. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_shared/linkedin_filters.py +151 -3
  14. autotouch_cli-0.2.79/autotouch_shared/list_build_contract.py +137 -0
  15. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/pyproject.toml +1 -1
  16. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/MANIFEST.in +0 -0
  17. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/__init__.py +0 -0
  18. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/__init__.py +0 -0
  19. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/agents.py +0 -0
  20. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/auth.py +0 -0
  21. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/cells.py +0 -0
  22. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/columns.py +0 -0
  23. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/jobs.py +0 -0
  24. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/leads.py +0 -0
  25. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/prompts.py +0 -0
  26. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/rows.py +0 -0
  27. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/search.py +0 -0
  28. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/sequences.py +0 -0
  29. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/tables.py +0 -0
  30. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/tasks.py +0 -0
  31. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/webhooks.py +0 -0
  32. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/commands/workspace_secrets.py +0 -0
  33. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/__init__.py +0 -0
  34. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/auth.py +0 -0
  35. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/config.py +0 -0
  36. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/csv_import.py +0 -0
  37. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/http.py +0 -0
  38. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/io.py +0 -0
  39. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/output.py +0 -0
  40. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/polling.py +0 -0
  41. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/run.py +0 -0
  42. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/core/validation.py +0 -0
  43. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/exceptions.py +0 -0
  44. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/mongo_status.py +0 -0
  45. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/parser_groups.py +0 -0
  46. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/sequence_support.py +0 -0
  47. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli/templates.py +0 -0
  48. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  49. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli.egg-info/entry_points.txt +0 -0
  50. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli.egg-info/requires.txt +0 -0
  51. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_cli.egg-info/top_level.txt +0 -0
  52. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_shared/__init__.py +0 -0
  53. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_shared/provider_registry.py +0 -0
  54. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/autotouch_shared/search_contract.py +0 -0
  55. {autotouch_cli-0.2.73 → autotouch_cli-0.2.79}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.73
3
+ Version: 0.2.79
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Project-URL: Homepage, https://app.autotouch.ai
6
6
  Project-URL: Documentation, https://github.com/nicolonic/autotouch_main/tree/main/docs/research-table/reference
@@ -133,8 +133,8 @@ autotouch rows get --table-id "$TABLE_ID" --row-id "$ROW_ID" --output json
133
133
  - Inspect one cell: `autotouch cells get`
134
134
  - Create a workflow column: `autotouch columns recipe`, `autotouch columns create`
135
135
  - Run provider-hidden search: `autotouch search companies`, `autotouch search people`
136
- - Inspect LinkedIn/Sales Nav filters: `autotouch linkedin filters --api sales_navigator --category people`
137
- - Build a paced LinkedIn people list: `autotouch linkedin list-build create --company-query 'SaaS startups' --headcount 1-10 --people-query 'Account Executive OR SDR' --num-results 250 --wait`
136
+ - Inspect LinkedIn filters: `autotouch linkedin filters --category people`
137
+ - Build a durable lead list on Smart Table workers: `autotouch list-build leads --title 'Account Executive OR SDR' --geo-id 103644278 --num-results 1000 --wait`
138
138
  - Run one LinkedIn search page/debug replay: `autotouch linkedin search`
139
139
  - Run controlled slices: `autotouch columns run-next`
140
140
  - Poll authoritative state: `autotouch jobs get`
@@ -148,8 +148,9 @@ For automation or agent-driven setup, use:
148
148
  - `autotouch cli-manifest --output json` for the local machine-readable command contract
149
149
  - `autotouch cli-reference` for the shipped parser-generated reference
150
150
  - `autotouch capabilities --output json` for provider/workflow contracts
151
- - `autotouch linkedin filters --output json` for LinkedIn/Sales Navigator filter tokens such as headcount, industry, location, role, function, and seniority
152
- - `autotouch linkedin list-build create` for durable LinkedIn/Sales Navigator list building with paced provider requests, visible progress, and cooldown status
151
+ - `autotouch --version` should be `0.2.79` or newer for provider-hidden `autotouch list-build ...`; older CLIs may still show the removed `autotouch linkedin list-build` surface
152
+ - `autotouch capabilities --output json --select list_builds` for documented list-build inputs such as geography IDs, company size buckets, profile language, and company IDs
153
+ - `autotouch list-build companies` and `autotouch list-build leads` for durable provider-hidden company and lead list builds with Smart Table-owned background workers, visible progress, and no user-owned network connection requirement
153
154
  - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
154
155
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
155
156
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
@@ -108,8 +108,8 @@ autotouch rows get --table-id "$TABLE_ID" --row-id "$ROW_ID" --output json
108
108
  - Inspect one cell: `autotouch cells get`
109
109
  - Create a workflow column: `autotouch columns recipe`, `autotouch columns create`
110
110
  - Run provider-hidden search: `autotouch search companies`, `autotouch search people`
111
- - Inspect LinkedIn/Sales Nav filters: `autotouch linkedin filters --api sales_navigator --category people`
112
- - Build a paced LinkedIn people list: `autotouch linkedin list-build create --company-query 'SaaS startups' --headcount 1-10 --people-query 'Account Executive OR SDR' --num-results 250 --wait`
111
+ - Inspect LinkedIn filters: `autotouch linkedin filters --category people`
112
+ - Build a durable lead list on Smart Table workers: `autotouch list-build leads --title 'Account Executive OR SDR' --geo-id 103644278 --num-results 1000 --wait`
113
113
  - Run one LinkedIn search page/debug replay: `autotouch linkedin search`
114
114
  - Run controlled slices: `autotouch columns run-next`
115
115
  - Poll authoritative state: `autotouch jobs get`
@@ -123,8 +123,9 @@ For automation or agent-driven setup, use:
123
123
  - `autotouch cli-manifest --output json` for the local machine-readable command contract
124
124
  - `autotouch cli-reference` for the shipped parser-generated reference
125
125
  - `autotouch capabilities --output json` for provider/workflow contracts
126
- - `autotouch linkedin filters --output json` for LinkedIn/Sales Navigator filter tokens such as headcount, industry, location, role, function, and seniority
127
- - `autotouch linkedin list-build create` for durable LinkedIn/Sales Navigator list building with paced provider requests, visible progress, and cooldown status
126
+ - `autotouch --version` should be `0.2.79` or newer for provider-hidden `autotouch list-build ...`; older CLIs may still show the removed `autotouch linkedin list-build` surface
127
+ - `autotouch capabilities --output json --select list_builds` for documented list-build inputs such as geography IDs, company size buckets, profile language, and company IDs
128
+ - `autotouch list-build companies` and `autotouch list-build leads` for durable provider-hidden company and lead list builds with Smart Table-owned background workers, visible progress, and no user-owned network connection requirement
128
129
  - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
129
130
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
130
131
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
@@ -95,9 +95,6 @@ from autotouch_cli.commands.linkedin import (
95
95
  cmd_linkedin_comments as cmd_linkedin_comments_impl,
96
96
  cmd_linkedin_filters as cmd_linkedin_filters_impl,
97
97
  cmd_linkedin_limits as cmd_linkedin_limits_impl,
98
- cmd_linkedin_list_build_create as cmd_linkedin_list_build_create_impl,
99
- cmd_linkedin_list_build_results as cmd_linkedin_list_build_results_impl,
100
- cmd_linkedin_list_build_status as cmd_linkedin_list_build_status_impl,
101
98
  cmd_linkedin_post as cmd_linkedin_post_impl,
102
99
  cmd_linkedin_posts as cmd_linkedin_posts_impl,
103
100
  cmd_linkedin_reactions as cmd_linkedin_reactions_impl,
@@ -106,6 +103,13 @@ from autotouch_cli.commands.linkedin import (
106
103
  cmd_linkedin_search_params as cmd_linkedin_search_params_impl,
107
104
  cmd_linkedin_status as cmd_linkedin_status_impl,
108
105
  )
106
+ from autotouch_cli.commands.list_build import (
107
+ ListBuildCommandRuntime as ListBuildCommandHandlerRuntime,
108
+ cmd_list_build_companies as cmd_list_build_companies_impl,
109
+ cmd_list_build_leads as cmd_list_build_leads_impl,
110
+ cmd_list_build_results as cmd_list_build_results_impl,
111
+ cmd_list_build_status as cmd_list_build_status_impl,
112
+ )
109
113
  from autotouch_cli.commands.rows import (
110
114
  RowCommandRuntime as RowCommandHandlerRuntime,
111
115
  cmd_rows_add as cmd_rows_add_impl,
@@ -353,6 +357,14 @@ from autotouch_shared.provider_registry import (
353
357
  ProviderContractValidationError,
354
358
  validate_column_provider_contract,
355
359
  )
360
+ from autotouch_shared.linkedin_contract import (
361
+ linkedin_capabilities_contract,
362
+ linkedin_endpoint_entries,
363
+ )
364
+ from autotouch_shared.list_build_contract import (
365
+ list_build_capabilities_contract,
366
+ list_build_endpoint_entries,
367
+ )
356
368
  from autotouch_cli.exceptions import (
357
369
  AutotouchAPIError,
358
370
  AutotouchInputError,
@@ -1355,6 +1367,15 @@ def _linkedin_command_runtime() -> LinkedInCommandHandlerRuntime:
1355
1367
  )
1356
1368
 
1357
1369
 
1370
+ def _list_build_command_runtime() -> ListBuildCommandHandlerRuntime:
1371
+ return ListBuildCommandHandlerRuntime(
1372
+ resolve_token=_resolve_token,
1373
+ request_api=_request_api,
1374
+ print_json=lambda data, compact=False: _print_json(data, compact=compact),
1375
+ load_json_input=_load_json_input,
1376
+ )
1377
+
1378
+
1358
1379
  # ---------------------------------------------------------------------------
1359
1380
  # Command registry — replaces ~110 thin shim functions
1360
1381
  # ---------------------------------------------------------------------------
@@ -1549,14 +1570,17 @@ _register("linkedin_limits", cmd_linkedin_limits_impl, _linkedin_command_runtime
1549
1570
  _register("linkedin_search", cmd_linkedin_search_impl, _linkedin_command_runtime)
1550
1571
  _register("linkedin_search_params", cmd_linkedin_search_params_impl, _linkedin_command_runtime)
1551
1572
  _register("linkedin_filters", cmd_linkedin_filters_impl, _linkedin_command_runtime)
1552
- _register("linkedin_list_build_create", cmd_linkedin_list_build_create_impl, _linkedin_command_runtime)
1553
- _register("linkedin_list_build_status", cmd_linkedin_list_build_status_impl, _linkedin_command_runtime)
1554
- _register("linkedin_list_build_results", cmd_linkedin_list_build_results_impl, _linkedin_command_runtime)
1555
1573
  _register("linkedin_posts", cmd_linkedin_posts_impl, _linkedin_command_runtime)
1556
1574
  _register("linkedin_post", cmd_linkedin_post_impl, _linkedin_command_runtime)
1557
1575
  _register("linkedin_comments", cmd_linkedin_comments_impl, _linkedin_command_runtime)
1558
1576
  _register("linkedin_reactions", cmd_linkedin_reactions_impl, _linkedin_command_runtime)
1559
1577
 
1578
+ # -- List build commands --
1579
+ _register("list_build_companies", cmd_list_build_companies_impl, _list_build_command_runtime)
1580
+ _register("list_build_leads", cmd_list_build_leads_impl, _list_build_command_runtime)
1581
+ _register("list_build_status", cmd_list_build_status_impl, _list_build_command_runtime)
1582
+ _register("list_build_results", cmd_list_build_results_impl, _list_build_command_runtime)
1583
+
1560
1584
  # -- Template emitter commands (use _template_runtime) --
1561
1585
  _register("schema", emit_schema, _template_runtime)
1562
1586
  _register("auth_schema", emit_auth_schema, _template_runtime)
@@ -1900,6 +1924,17 @@ def cmd_context_resolved(args: argparse.Namespace) -> None:
1900
1924
  _print_json(data, compact=args.compact)
1901
1925
 
1902
1926
 
1927
+ def _with_bundled_linkedin_capabilities(data: Any) -> Any:
1928
+ if not isinstance(data, dict):
1929
+ return data
1930
+ merged = dict(data)
1931
+ endpoints = merged.get("endpoints") if isinstance(merged.get("endpoints"), dict) else {}
1932
+ merged["endpoints"] = {**endpoints, **linkedin_endpoint_entries(), **list_build_endpoint_entries()}
1933
+ merged["linkedin"] = linkedin_capabilities_contract()
1934
+ merged["list_builds"] = list_build_capabilities_contract()
1935
+ return merged
1936
+
1937
+
1903
1938
  def cmd_capabilities(args: argparse.Namespace) -> None:
1904
1939
  token = _resolve_token(args.token, required=False)
1905
1940
  data = _request_api(
@@ -1911,6 +1946,7 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
1911
1946
  timeout=args.timeout,
1912
1947
  verbose=args.verbose,
1913
1948
  )
1949
+ data = _with_bundled_linkedin_capabilities(data)
1914
1950
  if OUTPUT_MODE == "human" and isinstance(data, dict):
1915
1951
  execution = data.get("execution_policies") if isinstance(data.get("execution_policies"), dict) else {}
1916
1952
  llm = execution.get("llm") if isinstance(execution.get("llm"), dict) else {}
@@ -1953,13 +1989,21 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
1953
1989
  print("")
1954
1990
  print("LinkedIn list building")
1955
1991
  print("---------------------")
1956
- modes = linkedin.get("api_modes", [])
1957
- print(f"API modes : {', '.join(str(m) for m in modes)}")
1992
+ api_mode_management = linkedin.get("api_mode_management")
1993
+ if api_mode_management:
1994
+ print(f"API mode : {api_mode_management}")
1958
1995
  print(f"Access gate : {linkedin.get('access_gate', 'linkedin_access flag')}")
1959
1996
  caps = linkedin.get("rate_limits", {}).get("search_daily_caps", {})
1960
- if caps:
1997
+ if isinstance(caps, dict) and caps:
1961
1998
  for mode, cap in caps.items():
1962
1999
  print(f" {mode:20s}: {cap}")
2000
+ elif caps:
2001
+ print(f"Daily caps : {caps}")
2002
+ summary = linkedin.get("recommendation_summary")
2003
+ if isinstance(summary, list) and summary:
2004
+ print("Recommended path:")
2005
+ for item in summary:
2006
+ print(f" - {item}")
1963
2007
  best_for = linkedin.get("best_for")
1964
2008
  if isinstance(best_for, list) and best_for:
1965
2009
  print("Best for :")
@@ -201,6 +201,8 @@ def action_descriptor(action: argparse.Action) -> Optional[Dict[str, Any]]:
201
201
  return None
202
202
  if isinstance(action, argparse._SubParsersAction):
203
203
  return None
204
+ if getattr(action, "help", None) == argparse.SUPPRESS:
205
+ return None
204
206
 
205
207
  input_kind = _option_input_kind(action)
206
208
  action_kind = _option_action_kind(action)
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import json
5
- import time
6
5
  from dataclasses import dataclass
7
6
  from typing import Any, Callable, Dict, List, Optional, Sequence
8
7
 
@@ -60,7 +59,7 @@ def _normalize_linkedin_search_payload(args: argparse.Namespace, *, runtime: Lin
60
59
 
61
60
  payload: Dict[str, Any] = {
62
61
  "category": str(getattr(args, "category", "people") or "people"),
63
- "limit": max(1, min(int(getattr(args, "limit", 25) or 25), 250)),
62
+ "limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
64
63
  }
65
64
  url = str(getattr(args, "url", "") or "").strip()
66
65
  if url:
@@ -141,131 +140,40 @@ def _split_repeated_values(values: Optional[List[str]]) -> List[str]:
141
140
  return output
142
141
 
143
142
 
144
- def _normalize_linkedin_list_build_payload(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> Dict[str, Any]:
145
- explicit = runtime.load_json_input(
146
- inline_json=getattr(args, "data_json", None),
147
- file_path=getattr(args, "data_file", None),
148
- context="data",
149
- default=None,
150
- )
151
- if explicit is not None:
152
- if not isinstance(explicit, dict):
153
- raise AutotouchInputError("linkedin list-build payload must be a JSON object")
154
- return explicit
155
-
156
- company_query = str(getattr(args, "company_query", "") or "").strip()
157
- if not company_query:
158
- raise AutotouchInputError("linkedin list-build create requires --company-query or --data-file")
159
-
160
- payload: Dict[str, Any] = {
161
- "mode": str(getattr(args, "mode", "company-first-people") or "company-first-people").strip(),
162
- "company_query": company_query,
163
- "headcount": str(getattr(args, "headcount", "1-10") or "1-10").strip(),
164
- "num_results": max(1, min(int(getattr(args, "num_results", 100) or 100), 1000)),
165
- }
166
- people_query = str(getattr(args, "people_query", "") or "").strip()
167
- if people_query:
168
- payload["people_query"] = people_query
169
- linkedin_api = str(getattr(args, "linkedin_api", "auto") or "auto").strip()
170
- if linkedin_api and linkedin_api != "auto":
171
- payload["linkedin_api"] = linkedin_api
172
- excluded_titles = _split_repeated_values(getattr(args, "excluded_title", None))
173
- if excluded_titles:
174
- payload["excluded_titles"] = excluded_titles
175
- for arg_name, payload_key, context in (
176
- ("extra_params_json", "extra_params", "extra-params"),
177
- ("company_extra_params_json", "company_extra_params", "company-extra-params"),
178
- ("people_extra_params_json", "people_extra_params", "people-extra-params"),
179
- ):
180
- parsed = _load_optional_json_object(getattr(args, arg_name, None), context=context)
181
- if parsed is not None:
182
- payload[payload_key] = parsed
183
- return payload
184
-
185
-
186
- def _wait_for_linkedin_list_build(
187
- job_id: str,
188
- args: argparse.Namespace,
189
- *,
190
- runtime: LinkedInCommandRuntime,
191
- token: Optional[str],
192
- ) -> Dict[str, Any]:
193
- started = time.time()
194
- interval = max(1, int(getattr(args, "poll_interval", 5) or 5))
195
- timeout = max(0, int(getattr(args, "wait_timeout", 0) or 0))
196
- terminal = {"completed", "error", "cancelled"}
197
- latest: Dict[str, Any] = {}
198
-
199
- while True:
200
- latest = runtime.request_api(
201
- "GET",
202
- f"/api/integrations/linkedin/list-builds/{job_id}",
203
- base_url=args.base_url,
204
- token=token,
205
- use_x_api_key=args.use_x_api_key,
206
- timeout=args.timeout,
207
- verbose=args.verbose,
208
- )
209
- status = str(latest.get("status") or "").strip().lower() if isinstance(latest, dict) else ""
210
- if status in terminal:
211
- return latest
212
- if timeout and (time.time() - started) >= timeout:
213
- return latest
214
- time.sleep(interval)
215
-
216
-
217
- def cmd_linkedin_list_build_create(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> None:
218
- token = runtime.resolve_token(args.token, required=True)
219
- payload = _normalize_linkedin_list_build_payload(args, runtime=runtime)
220
- data = runtime.request_api(
221
- "POST",
222
- "/api/integrations/linkedin/list-builds",
223
- base_url=args.base_url,
224
- token=token,
225
- use_x_api_key=args.use_x_api_key,
226
- payload=payload,
227
- timeout=args.timeout,
228
- verbose=args.verbose,
229
- )
230
- if getattr(args, "wait", False) and isinstance(data, dict) and data.get("job_id"):
231
- data = _wait_for_linkedin_list_build(str(data["job_id"]), args, runtime=runtime, token=token)
232
- runtime.print_json(data, args.compact)
143
+ def _coerce_filter_arg_value(raw: str) -> Any:
144
+ value = str(raw or "").strip()
145
+ if not value:
146
+ return ""
147
+ if value[0] in "[{\"" or value.lower() in {"true", "false", "null"}:
148
+ try:
149
+ return json.loads(value)
150
+ except Exception:
151
+ pass
152
+ if "," in value:
153
+ return [part.strip() for part in value.split(",") if part.strip()]
154
+ return value
233
155
 
234
156
 
235
- def cmd_linkedin_list_build_status(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> None:
236
- token = runtime.resolve_token(args.token, required=True)
237
- job_id = str(args.job_id).strip()
238
- data = runtime.request_api(
239
- "GET",
240
- f"/api/integrations/linkedin/list-builds/{job_id}",
241
- base_url=args.base_url,
242
- token=token,
243
- use_x_api_key=args.use_x_api_key,
244
- timeout=args.timeout,
245
- verbose=args.verbose,
246
- )
247
- runtime.print_json(data, args.compact)
157
+ def _parse_filter_pairs(values: Optional[List[str]], *, context: str) -> Dict[str, Any]:
158
+ filters: Dict[str, Any] = {}
159
+ for raw in values or []:
160
+ item = str(raw or "").strip()
161
+ if not item:
162
+ continue
163
+ if "=" not in item:
164
+ raise AutotouchInputError(f"{context} filters must use FIELD=VALUE")
165
+ field, value = item.split("=", 1)
166
+ field = field.strip()
167
+ if not field:
168
+ raise AutotouchInputError(f"{context} filter field is empty")
169
+ filters[field] = _coerce_filter_arg_value(value)
170
+ return filters
248
171
 
249
172
 
250
- def cmd_linkedin_list_build_results(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> None:
251
- token = runtime.resolve_token(args.token, required=True)
252
- job_id = str(args.job_id).strip()
253
- params: Dict[str, Any] = {
254
- "type": str(getattr(args, "type", "people") or "people"),
255
- "page": max(1, int(getattr(args, "page", 1) or 1)),
256
- "page_size": max(1, min(int(getattr(args, "page_size", 50) or 50), 500)),
257
- }
258
- data = runtime.request_api(
259
- "GET",
260
- f"/api/integrations/linkedin/list-builds/{job_id}/results",
261
- base_url=args.base_url,
262
- token=token,
263
- use_x_api_key=args.use_x_api_key,
264
- params=params,
265
- timeout=args.timeout,
266
- verbose=args.verbose,
267
- )
268
- runtime.print_json(data, args.compact)
173
+ def _add_repeated_filter(filters: Dict[str, Any], field: str, values: Optional[List[str]]) -> None:
174
+ parsed = _split_repeated_values(values)
175
+ if parsed:
176
+ filters[field] = parsed
269
177
 
270
178
 
271
179
  def cmd_linkedin_filters(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> None:
@@ -397,7 +305,7 @@ def register_linkedin_subcommands(
397
305
  handlers: Dict[str, Callable[[argparse.Namespace], None]],
398
306
  recipe_types: Sequence[str],
399
307
  ) -> None:
400
- parser = subparsers.add_parser("linkedin", help="LinkedIn list-building operations")
308
+ parser = subparsers.add_parser("linkedin", help="Connected LinkedIn search and engagement operations")
401
309
  linkedin_sub = parser.add_subparsers(dest="linkedin_cmd", required=True)
402
310
 
403
311
  recipe_parser = linkedin_sub.add_parser("recipe", help="Print LinkedIn payload recipes for search and list-building")
@@ -420,9 +328,9 @@ def register_linkedin_subcommands(
420
328
  search_parser.add_argument("--url", help="Full LinkedIn/Sales Navigator search URL (overrides keyword filters)")
421
329
  search_parser.add_argument("--category", choices=["people", "companies", "jobs", "posts"], default="people", help="Search category (default: people)")
422
330
  search_parser.add_argument("--keywords", help="Free-text keyword search")
423
- search_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help="API mode (default: auto; uses connected account mode)")
331
+ search_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help=argparse.SUPPRESS)
424
332
  search_parser.add_argument("--cursor", help="Pagination cursor from previous response")
425
- search_parser.add_argument("--limit", type=int, default=25, help="Results per page, 1-250 (default: 25)")
333
+ search_parser.add_argument("--limit", type=int, default=25, help="Results per page, 1-100 (default: 25)")
426
334
  search_parser.add_argument("--extra-params-json", help="JSON object with additional search filters (location IDs, industry, etc.)")
427
335
  add_api_common_arguments(search_parser)
428
336
  search_parser.set_defaults(func=handlers["search"])
@@ -430,7 +338,7 @@ def register_linkedin_subcommands(
430
338
  params_parser = linkedin_sub.add_parser("search-params", help="Resolve names to LinkedIn parameter IDs for search filters")
431
339
  params_parser.add_argument("--type", required=True, help="Parameter type: location, industry, company, school, title, language, etc.")
432
340
  params_parser.add_argument("--keywords", default="", help="Search text to match")
433
- params_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help="Resolver service (default: auto; uses connected account mode)")
341
+ params_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help=argparse.SUPPRESS)
434
342
  params_parser.add_argument("--limit", type=int, default=25, help="Max results, 1-250 (default: 25)")
435
343
  add_api_common_arguments(params_parser)
436
344
  params_parser.set_defaults(func=handlers["search_params"])
@@ -443,41 +351,6 @@ def register_linkedin_subcommands(
443
351
  add_api_common_arguments(filters_parser)
444
352
  filters_parser.set_defaults(func=handlers["filters"])
445
353
 
446
- list_build_parser = linkedin_sub.add_parser("list-build", help="Create and inspect paced LinkedIn list-build jobs")
447
- list_build_sub = list_build_parser.add_subparsers(dest="linkedin_list_build_cmd", required=True)
448
-
449
- list_build_create = list_build_sub.add_parser("create", help="Create a paced background LinkedIn list-build job")
450
- list_build_create.add_argument("--data-json", help="Explicit list-build payload JSON (overrides individual flags)")
451
- list_build_create.add_argument("--data-file", help="Path to list-build payload JSON file")
452
- list_build_create.add_argument("--mode", choices=["company-first-people"], default="company-first-people", help="List build mode")
453
- list_build_create.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help="API mode (default: auto; uses connected account mode)")
454
- list_build_create.add_argument("--company-query", help="Company-search query used to build the seed account set")
455
- list_build_create.add_argument("--people-query", help="People-search query/title terms")
456
- list_build_create.add_argument("--headcount", default="1-10", help="Company headcount range, e.g. 1-10 or 11-50")
457
- list_build_create.add_argument("--num-results", type=int, default=100, help="Target number of people results, 1-1000")
458
- list_build_create.add_argument("--excluded-title", action="append", help="Title to exclude after results return; repeat or comma-separate")
459
- list_build_create.add_argument("--extra-params-json", help="JSON object applied to both company and people searches")
460
- list_build_create.add_argument("--company-extra-params-json", help="JSON object applied only to company search")
461
- list_build_create.add_argument("--people-extra-params-json", help="JSON object applied only to people search")
462
- list_build_create.add_argument("--wait", action="store_true", help="Poll until the job reaches completed, error, or cancelled")
463
- list_build_create.add_argument("--poll-interval", type=int, default=5, help="Polling interval seconds for --wait")
464
- list_build_create.add_argument("--wait-timeout", type=int, default=0, help="Max seconds to wait (0 = no timeout)")
465
- add_api_common_arguments(list_build_create)
466
- list_build_create.set_defaults(func=handlers["list_build_create"])
467
-
468
- list_build_status = list_build_sub.add_parser("status", help="Get LinkedIn list-build job status")
469
- list_build_status.add_argument("--job-id", required=True, help="LinkedIn list-build job id")
470
- add_api_common_arguments(list_build_status)
471
- list_build_status.set_defaults(func=handlers["list_build_status"])
472
-
473
- list_build_results = list_build_sub.add_parser("results", help="Get LinkedIn list-build results")
474
- list_build_results.add_argument("--job-id", required=True, help="LinkedIn list-build job id")
475
- list_build_results.add_argument("--type", choices=["people", "companies", "all"], default="people", help="Result type")
476
- list_build_results.add_argument("--page", type=int, default=1, help="Results page")
477
- list_build_results.add_argument("--page-size", type=int, default=50, help="Results per page, 1-500")
478
- add_api_common_arguments(list_build_results)
479
- list_build_results.set_defaults(func=handlers["list_build_results"])
480
-
481
354
  posts_parser = linkedin_sub.add_parser("posts", help="List LinkedIn posts for a user")
482
355
  posts_parser.add_argument("--identifier", required=True, help="LinkedIn profile URL, public ID, or URN")
483
356
  posts_parser.add_argument("--cursor", help="Pagination cursor from previous response")
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable, Dict, Optional
8
+
9
+ from autotouch_cli.exceptions import AutotouchInputError, AutotouchTimeoutError
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ListBuildCommandRuntime:
14
+ resolve_token: Callable[[Optional[str], bool], Optional[str]]
15
+ request_api: Callable[..., Any]
16
+ print_json: Callable[[Any, bool], None]
17
+ load_json_input: Callable[..., Any]
18
+
19
+
20
+ def _payload(args: argparse.Namespace, *, runtime: ListBuildCommandRuntime, kind: str) -> Dict[str, Any]:
21
+ explicit = runtime.load_json_input(
22
+ inline_json=getattr(args, "data_json", None),
23
+ file_path=getattr(args, "data_file", None),
24
+ context="data",
25
+ default=None,
26
+ )
27
+ if explicit is not None:
28
+ if not isinstance(explicit, dict):
29
+ raise AutotouchInputError("list-build payload must be a JSON object")
30
+ explicit.setdefault("kind", kind)
31
+ return explicit
32
+ filters: Dict[str, Any] = {}
33
+ for attr, key in (
34
+ ("geo_id", "geo_ids"),
35
+ ("industry_id", "industry_ids"),
36
+ ("company_size", "company_sizes"),
37
+ ("current_company_id", "current_company_ids"),
38
+ ("past_company_id", "past_company_ids"),
39
+ ("school_id", "school_ids"),
40
+ ):
41
+ values = getattr(args, attr, None) or []
42
+ if values:
43
+ filters[key] = values
44
+ if getattr(args, "has_jobs", False):
45
+ filters["has_jobs"] = True
46
+ profile_language = str(getattr(args, "profile_language", "") or "").strip()
47
+ if profile_language:
48
+ filters["profile_language"] = profile_language
49
+ payload: Dict[str, Any] = {
50
+ "kind": kind,
51
+ "num_results": max(1, min(int(getattr(args, "num_results", 100) or 100), 10000)),
52
+ "page_size": max(1, min(int(getattr(args, "page_size", 25) or 25), 100)),
53
+ "filters": filters,
54
+ }
55
+ for attr in ("list_name", "keywords", "title"):
56
+ value = str(getattr(args, attr, "") or "").strip()
57
+ if value:
58
+ payload[attr] = value
59
+ return payload
60
+
61
+
62
+ def _wait(job_id: str, args: argparse.Namespace, *, runtime: ListBuildCommandRuntime, token: Optional[str]) -> Any:
63
+ started = time.time()
64
+ while True:
65
+ data = runtime.request_api(
66
+ "GET",
67
+ f"/api/list-builds/{job_id}",
68
+ base_url=args.base_url,
69
+ token=token,
70
+ use_x_api_key=args.use_x_api_key,
71
+ timeout=args.timeout,
72
+ verbose=args.verbose,
73
+ )
74
+ if str(data.get("status") or "").lower() in {"completed", "error", "cancelled"}:
75
+ return data
76
+ timeout = int(getattr(args, "wait_timeout", 0) or 0)
77
+ if timeout and time.time() - started >= timeout:
78
+ raise AutotouchTimeoutError(f"Timed out waiting for list build {job_id}")
79
+ time.sleep(max(1, int(getattr(args, "poll_interval", 5) or 5)))
80
+
81
+
82
+ def _create(args: argparse.Namespace, *, runtime: ListBuildCommandRuntime, kind: str) -> None:
83
+ token = runtime.resolve_token(args.token, required=True)
84
+ data = runtime.request_api(
85
+ "POST",
86
+ "/api/list-builds",
87
+ base_url=args.base_url,
88
+ token=token,
89
+ use_x_api_key=args.use_x_api_key,
90
+ payload=_payload(args, runtime=runtime, kind=kind),
91
+ timeout=args.timeout,
92
+ verbose=args.verbose,
93
+ )
94
+ if getattr(args, "wait", False) and data.get("job_id"):
95
+ data = _wait(str(data["job_id"]), args, runtime=runtime, token=token)
96
+ runtime.print_json(data, args.compact)
97
+
98
+
99
+ def cmd_list_build_companies(args: argparse.Namespace, *, runtime: ListBuildCommandRuntime) -> None:
100
+ _create(args, runtime=runtime, kind="companies")
101
+
102
+
103
+ def cmd_list_build_leads(args: argparse.Namespace, *, runtime: ListBuildCommandRuntime) -> None:
104
+ _create(args, runtime=runtime, kind="leads")
105
+
106
+
107
+ def cmd_list_build_status(args: argparse.Namespace, *, runtime: ListBuildCommandRuntime) -> None:
108
+ token = runtime.resolve_token(args.token, required=True)
109
+ data = runtime.request_api(
110
+ "GET",
111
+ f"/api/list-builds/{args.job_id}",
112
+ base_url=args.base_url,
113
+ token=token,
114
+ use_x_api_key=args.use_x_api_key,
115
+ timeout=args.timeout,
116
+ verbose=args.verbose,
117
+ )
118
+ runtime.print_json(data, args.compact)
119
+
120
+
121
+ def cmd_list_build_results(args: argparse.Namespace, *, runtime: ListBuildCommandRuntime) -> None:
122
+ token = runtime.resolve_token(args.token, required=True)
123
+ data = runtime.request_api(
124
+ "GET",
125
+ f"/api/list-builds/{args.job_id}/results?type={args.type}&page={args.page}&page_size={args.page_size}",
126
+ base_url=args.base_url,
127
+ token=token,
128
+ use_x_api_key=args.use_x_api_key,
129
+ timeout=args.timeout,
130
+ verbose=args.verbose,
131
+ )
132
+ runtime.print_json(data, args.compact)
133
+
134
+
135
+ def register_list_build_subcommands(
136
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
137
+ *,
138
+ add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
139
+ handlers: Dict[str, Callable[[argparse.Namespace], None]],
140
+ ) -> None:
141
+ parser = subparsers.add_parser("list-build", help="Build durable company and lead lists")
142
+ sub = parser.add_subparsers(dest="list_build_cmd", required=True)
143
+
144
+ def add_create_args(p: argparse.ArgumentParser) -> None:
145
+ p.add_argument("--data-json", help="Explicit list-build payload JSON")
146
+ p.add_argument("--data-file", help="Path to list-build payload JSON file")
147
+ p.add_argument("--list-name", help="Optional display name")
148
+ p.add_argument("--keywords", help="Search keywords")
149
+ p.add_argument("--title", help="Lead title/persona query")
150
+ p.add_argument("--geo-id", action="append", help="Geography ID; repeat or comma-separate in JSON")
151
+ p.add_argument("--industry-id", action="append", help="Industry ID; repeat or comma-separate in JSON")
152
+ p.add_argument("--company-size", action="append", help="Company size bucket, e.g. 1-10 or 11-50")
153
+ p.add_argument("--current-company-id", action="append", help="Company ID for lead search")
154
+ p.add_argument("--past-company-id", action="append", help="Past company ID for lead search")
155
+ p.add_argument("--school-id", action="append", help="School ID for lead search")
156
+ p.add_argument("--profile-language", help="Profile language code, e.g. en")
157
+ p.add_argument("--has-jobs", action="store_true", help="Only include companies with active job listings")
158
+ p.add_argument("--num-results", type=int, default=100, help="Target result count, 1-10000")
159
+ p.add_argument("--page-size", type=int, default=25, help="Provider page size")
160
+ p.add_argument("--wait", action="store_true", help="Poll until terminal status")
161
+ p.add_argument("--poll-interval", type=int, default=5, help="Polling interval seconds for --wait")
162
+ p.add_argument("--wait-timeout", type=int, default=0, help="Max seconds to wait (0 = no timeout)")
163
+ add_api_common_arguments(p)
164
+
165
+ companies = sub.add_parser("companies", help="Build a company/account list")
166
+ add_create_args(companies)
167
+ companies.set_defaults(func=handlers["companies"])
168
+
169
+ leads = sub.add_parser("leads", help="Build a lead/contact list")
170
+ add_create_args(leads)
171
+ leads.set_defaults(func=handlers["leads"])
172
+
173
+ status = sub.add_parser("status", help="Get list-build job status")
174
+ status.add_argument("--job-id", required=True)
175
+ add_api_common_arguments(status)
176
+ status.set_defaults(func=handlers["status"])
177
+
178
+ results = sub.add_parser("results", help="Get list-build results")
179
+ results.add_argument("--job-id", required=True)
180
+ results.add_argument("--type", choices=["items", "companies", "leads", "all"], default="items")
181
+ results.add_argument("--page", type=int, default=1)
182
+ results.add_argument("--page-size", type=int, default=50)
183
+ add_api_common_arguments(results)
184
+ results.set_defaults(func=handlers["results"])
185
+