autotouch-cli 0.2.73__tar.gz → 0.2.74__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 (53) hide show
  1. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/PKG-INFO +3 -3
  2. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/README.md +2 -2
  3. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/linkedin.py +66 -0
  4. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/data/CLI_REFERENCE.md +27 -1
  5. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/data/cli-manifest.json +160 -2
  6. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli.egg-info/PKG-INFO +3 -3
  7. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_shared/linkedin_contract.py +11 -5
  8. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_shared/linkedin_filters.py +151 -3
  9. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/pyproject.toml +1 -1
  10. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/MANIFEST.in +0 -0
  11. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/__init__.py +0 -0
  12. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/cli.py +0 -0
  13. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/cli_contracts.py +0 -0
  14. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/__init__.py +0 -0
  15. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/agents.py +0 -0
  16. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/auth.py +0 -0
  17. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/cells.py +0 -0
  18. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/columns.py +0 -0
  19. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/jobs.py +0 -0
  20. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/leads.py +0 -0
  21. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/prompts.py +0 -0
  22. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/rows.py +0 -0
  23. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/search.py +0 -0
  24. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/sequences.py +0 -0
  25. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/tables.py +0 -0
  26. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/tasks.py +0 -0
  27. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/webhooks.py +0 -0
  28. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/commands/workspace_secrets.py +0 -0
  29. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/__init__.py +0 -0
  30. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/auth.py +0 -0
  31. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/config.py +0 -0
  32. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/csv_import.py +0 -0
  33. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/http.py +0 -0
  34. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/io.py +0 -0
  35. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/output.py +0 -0
  36. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/polling.py +0 -0
  37. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/run.py +0 -0
  38. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/core/validation.py +0 -0
  39. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/exceptions.py +0 -0
  40. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/mongo_status.py +0 -0
  41. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/parser.py +0 -0
  42. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/parser_groups.py +0 -0
  43. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/sequence_support.py +0 -0
  44. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli/templates.py +0 -0
  45. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli.egg-info/SOURCES.txt +0 -0
  46. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  47. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli.egg-info/entry_points.txt +0 -0
  48. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli.egg-info/requires.txt +0 -0
  49. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_cli.egg-info/top_level.txt +0 -0
  50. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_shared/__init__.py +0 -0
  51. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_shared/provider_registry.py +0 -0
  52. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/autotouch_shared/search_contract.py +0 -0
  53. {autotouch_cli-0.2.73 → autotouch_cli-0.2.74}/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.74
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
@@ -134,7 +134,7 @@ autotouch rows get --table-id "$TABLE_ID" --row-id "$ROW_ID" --output json
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
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`
137
+ - Build a paced LinkedIn people list on Smart Table workers: `autotouch linkedin list-build create --company-query 'SaaS startups' --headcount 1-10 --people-query 'Account Executive OR SDR' --num-results 250 --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`
@@ -149,7 +149,7 @@ For automation or agent-driven setup, use:
149
149
  - `autotouch cli-reference` for the shipped parser-generated reference
150
150
  - `autotouch capabilities --output json` for provider/workflow contracts
151
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
152
+ - `autotouch linkedin list-build create` for durable LinkedIn/Sales Navigator list building with Smart Table-owned paced provider requests, visible progress, and cooldown status
153
153
  - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
154
154
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
155
155
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
@@ -109,7 +109,7 @@ autotouch rows get --table-id "$TABLE_ID" --row-id "$ROW_ID" --output json
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
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`
112
+ - Build a paced LinkedIn people list on Smart Table workers: `autotouch linkedin list-build create --company-query 'SaaS startups' --headcount 1-10 --people-query 'Account Executive OR SDR' --num-results 250 --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`
@@ -124,7 +124,7 @@ For automation or agent-driven setup, use:
124
124
  - `autotouch cli-reference` for the shipped parser-generated reference
125
125
  - `autotouch capabilities --output json` for provider/workflow contracts
126
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
127
+ - `autotouch linkedin list-build create` for durable LinkedIn/Sales Navigator list building with Smart Table-owned paced provider requests, visible progress, and cooldown status
128
128
  - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
129
129
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
130
130
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
@@ -141,6 +141,42 @@ def _split_repeated_values(values: Optional[List[str]]) -> List[str]:
141
141
  return output
142
142
 
143
143
 
144
+ def _coerce_filter_arg_value(raw: str) -> Any:
145
+ value = str(raw or "").strip()
146
+ if not value:
147
+ return ""
148
+ if value[0] in "[{\"" or value.lower() in {"true", "false", "null"}:
149
+ try:
150
+ return json.loads(value)
151
+ except Exception:
152
+ pass
153
+ if "," in value:
154
+ return [part.strip() for part in value.split(",") if part.strip()]
155
+ return value
156
+
157
+
158
+ def _parse_filter_pairs(values: Optional[List[str]], *, context: str) -> Dict[str, Any]:
159
+ filters: Dict[str, Any] = {}
160
+ for raw in values or []:
161
+ item = str(raw or "").strip()
162
+ if not item:
163
+ continue
164
+ if "=" not in item:
165
+ raise AutotouchInputError(f"{context} filters must use FIELD=VALUE")
166
+ field, value = item.split("=", 1)
167
+ field = field.strip()
168
+ if not field:
169
+ raise AutotouchInputError(f"{context} filter field is empty")
170
+ filters[field] = _coerce_filter_arg_value(value)
171
+ return filters
172
+
173
+
174
+ def _add_repeated_filter(filters: Dict[str, Any], field: str, values: Optional[List[str]]) -> None:
175
+ parsed = _split_repeated_values(values)
176
+ if parsed:
177
+ filters[field] = parsed
178
+
179
+
144
180
  def _normalize_linkedin_list_build_payload(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> Dict[str, Any]:
145
181
  explicit = runtime.load_json_input(
146
182
  inline_json=getattr(args, "data_json", None),
@@ -172,6 +208,23 @@ def _normalize_linkedin_list_build_payload(args: argparse.Namespace, *, runtime:
172
208
  excluded_titles = _split_repeated_values(getattr(args, "excluded_title", None))
173
209
  if excluded_titles:
174
210
  payload["excluded_titles"] = excluded_titles
211
+ company_filters = _parse_filter_pairs(getattr(args, "company_filter", None), context="company")
212
+ people_filters = _parse_filter_pairs(getattr(args, "people_filter", None), context="people")
213
+ _add_repeated_filter(company_filters, "location", getattr(args, "company_location_id", None) or getattr(args, "location_id", None))
214
+ _add_repeated_filter(people_filters, "location", getattr(args, "people_location_id", None) or getattr(args, "location_id", None))
215
+ _add_repeated_filter(company_filters, "industry", getattr(args, "company_industry_id", None))
216
+ _add_repeated_filter(people_filters, "industry", getattr(args, "people_industry_id", None))
217
+ _add_repeated_filter(people_filters, "role", getattr(args, "role_id", None))
218
+ _add_repeated_filter(people_filters, "function", getattr(args, "function_id", None))
219
+ _add_repeated_filter(people_filters, "seniority", getattr(args, "seniority", None))
220
+ _add_repeated_filter(people_filters, "profile_language", getattr(args, "profile_language", None))
221
+ _add_repeated_filter(company_filters, "technologies", getattr(args, "technology_id", None))
222
+ if getattr(args, "has_job_offers", False):
223
+ company_filters["has_job_offers"] = True
224
+ if company_filters:
225
+ payload["company_filters"] = company_filters
226
+ if people_filters:
227
+ payload["people_filters"] = people_filters
175
228
  for arg_name, payload_key, context in (
176
229
  ("extra_params_json", "extra_params", "extra-params"),
177
230
  ("company_extra_params_json", "company_extra_params", "company-extra-params"),
@@ -456,6 +509,19 @@ def register_linkedin_subcommands(
456
509
  list_build_create.add_argument("--headcount", default="1-10", help="Company headcount range, e.g. 1-10 or 11-50")
457
510
  list_build_create.add_argument("--num-results", type=int, default=100, help="Target number of people results, 1-1000")
458
511
  list_build_create.add_argument("--excluded-title", action="append", help="Title to exclude after results return; repeat or comma-separate")
512
+ list_build_create.add_argument("--location-id", action="append", help="LinkedIn location/region ID applied to company and people search")
513
+ list_build_create.add_argument("--company-location-id", action="append", help="LinkedIn location/region ID applied only to company search")
514
+ list_build_create.add_argument("--people-location-id", action="append", help="LinkedIn location/region ID applied only to people search")
515
+ list_build_create.add_argument("--company-industry-id", action="append", help="LinkedIn industry ID applied to company search")
516
+ list_build_create.add_argument("--people-industry-id", action="append", help="LinkedIn industry ID applied to people search")
517
+ list_build_create.add_argument("--role-id", action="append", help="Resolved LinkedIn job-title ID for people search; repeat or comma-separate")
518
+ list_build_create.add_argument("--function-id", action="append", help="Resolved LinkedIn department/function ID for Sales Nav people search")
519
+ list_build_create.add_argument("--seniority", action="append", help="Sales Nav seniority filter value; repeat or comma-separate")
520
+ list_build_create.add_argument("--profile-language", action="append", help="Profile language code, e.g. en")
521
+ list_build_create.add_argument("--technology-id", action="append", help="Sales Nav technology ID applied to company search")
522
+ list_build_create.add_argument("--has-job-offers", action="store_true", help="Filter companies currently hiring on LinkedIn")
523
+ list_build_create.add_argument("--company-filter", action="append", help="Catalog company filter as FIELD=VALUE; repeat for multiple filters")
524
+ list_build_create.add_argument("--people-filter", action="append", help="Catalog people filter as FIELD=VALUE; repeat for multiple filters")
459
525
  list_build_create.add_argument("--extra-params-json", help="JSON object applied to both company and people searches")
460
526
  list_build_create.add_argument("--company-extra-params-json", help="JSON object applied only to company search")
461
527
  list_build_create.add_argument("--people-extra-params-json", help="JSON object applied only to people search")
@@ -1,6 +1,6 @@
1
1
  # Autotouch CLI Reference
2
2
 
3
- Generated from the installed parser for `autotouch-cli` `0.2.73`.
3
+ Generated from the installed parser for `autotouch-cli` `0.2.74`.
4
4
  Manifest schema version: `2`.
5
5
 
6
6
  ## Output Modes
@@ -2365,6 +2365,19 @@ Create a paced background LinkedIn list-build job
2365
2365
  [--headcount HEADCOUNT]
2366
2366
  [--num-results NUM_RESULTS]
2367
2367
  [--excluded-title EXCLUDED_TITLE]
2368
+ [--location-id LOCATION_ID]
2369
+ [--company-location-id COMPANY_LOCATION_ID]
2370
+ [--people-location-id PEOPLE_LOCATION_ID]
2371
+ [--company-industry-id COMPANY_INDUSTRY_ID]
2372
+ [--people-industry-id PEOPLE_INDUSTRY_ID]
2373
+ [--role-id ROLE_ID]
2374
+ [--function-id FUNCTION_ID]
2375
+ [--seniority SENIORITY]
2376
+ [--profile-language PROFILE_LANGUAGE]
2377
+ [--technology-id TECHNOLOGY_ID]
2378
+ [--has-job-offers]
2379
+ [--company-filter COMPANY_FILTER]
2380
+ [--people-filter PEOPLE_FILTER]
2368
2381
  [--extra-params-json EXTRA_PARAMS_JSON]
2369
2382
  [--company-extra-params-json COMPANY_EXTRA_PARAMS_JSON]
2370
2383
  [--people-extra-params-json PEOPLE_EXTRA_PARAMS_JSON]
@@ -2388,6 +2401,19 @@ Create a paced background LinkedIn list-build job
2388
2401
  - `--headcount` (kind=string; default=1-10): Company headcount range, e.g. 1-10 or 11-50
2389
2402
  - `--num-results` (kind=integer; default=100): Target number of people results, 1-1000
2390
2403
  - `--excluded-title` (kind=string; repeatable): Title to exclude after results return; repeat or comma-separate
2404
+ - `--location-id` (kind=string; repeatable): LinkedIn location/region ID applied to company and people search
2405
+ - `--company-location-id` (kind=string; repeatable): LinkedIn location/region ID applied only to company search
2406
+ - `--people-location-id` (kind=string; repeatable): LinkedIn location/region ID applied only to people search
2407
+ - `--company-industry-id` (kind=string; repeatable): LinkedIn industry ID applied to company search
2408
+ - `--people-industry-id` (kind=string; repeatable): LinkedIn industry ID applied to people search
2409
+ - `--role-id` (kind=string; repeatable): Resolved LinkedIn job-title ID for people search; repeat or comma-separate
2410
+ - `--function-id` (kind=string; repeatable): Resolved LinkedIn department/function ID for Sales Nav people search
2411
+ - `--seniority` (kind=string; repeatable): Sales Nav seniority filter value; repeat or comma-separate
2412
+ - `--profile-language` (kind=string; repeatable): Profile language code, e.g. en
2413
+ - `--technology-id` (kind=string; repeatable): Sales Nav technology ID applied to company search
2414
+ - `--has-job-offers` (kind=boolean; when omitted=False; when present=True): Filter companies currently hiring on LinkedIn
2415
+ - `--company-filter` (kind=string; repeatable): Catalog company filter as FIELD=VALUE; repeat for multiple filters
2416
+ - `--people-filter` (kind=string; repeatable): Catalog people filter as FIELD=VALUE; repeat for multiple filters
2391
2417
  - `--extra-params-json` (kind=json; input=json): JSON object applied to both company and people searches
2392
2418
  - `--company-extra-params-json` (kind=json; input=json): JSON object applied only to company search
2393
2419
  - `--people-extra-params-json` (kind=json; input=json): JSON object applied only to people search
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.73",
2
+ "version": "0.2.74",
3
3
  "manifest_schema_version": 2,
4
4
  "entry_points": {
5
5
  "autotouch": "autotouch_cli.cli:main",
@@ -33877,6 +33877,164 @@
33877
33877
  ],
33878
33878
  "takes_value": true
33879
33879
  },
33880
+ {
33881
+ "dest": "location_id",
33882
+ "required": false,
33883
+ "help": "LinkedIn location/region ID applied to company and people search",
33884
+ "kind": "string",
33885
+ "action": "append",
33886
+ "repeatable": true,
33887
+ "flags": [
33888
+ "--location-id"
33889
+ ],
33890
+ "takes_value": true
33891
+ },
33892
+ {
33893
+ "dest": "company_location_id",
33894
+ "required": false,
33895
+ "help": "LinkedIn location/region ID applied only to company search",
33896
+ "kind": "string",
33897
+ "action": "append",
33898
+ "repeatable": true,
33899
+ "flags": [
33900
+ "--company-location-id"
33901
+ ],
33902
+ "takes_value": true
33903
+ },
33904
+ {
33905
+ "dest": "people_location_id",
33906
+ "required": false,
33907
+ "help": "LinkedIn location/region ID applied only to people search",
33908
+ "kind": "string",
33909
+ "action": "append",
33910
+ "repeatable": true,
33911
+ "flags": [
33912
+ "--people-location-id"
33913
+ ],
33914
+ "takes_value": true
33915
+ },
33916
+ {
33917
+ "dest": "company_industry_id",
33918
+ "required": false,
33919
+ "help": "LinkedIn industry ID applied to company search",
33920
+ "kind": "string",
33921
+ "action": "append",
33922
+ "repeatable": true,
33923
+ "flags": [
33924
+ "--company-industry-id"
33925
+ ],
33926
+ "takes_value": true
33927
+ },
33928
+ {
33929
+ "dest": "people_industry_id",
33930
+ "required": false,
33931
+ "help": "LinkedIn industry ID applied to people search",
33932
+ "kind": "string",
33933
+ "action": "append",
33934
+ "repeatable": true,
33935
+ "flags": [
33936
+ "--people-industry-id"
33937
+ ],
33938
+ "takes_value": true
33939
+ },
33940
+ {
33941
+ "dest": "role_id",
33942
+ "required": false,
33943
+ "help": "Resolved LinkedIn job-title ID for people search; repeat or comma-separate",
33944
+ "kind": "string",
33945
+ "action": "append",
33946
+ "repeatable": true,
33947
+ "flags": [
33948
+ "--role-id"
33949
+ ],
33950
+ "takes_value": true
33951
+ },
33952
+ {
33953
+ "dest": "function_id",
33954
+ "required": false,
33955
+ "help": "Resolved LinkedIn department/function ID for Sales Nav people search",
33956
+ "kind": "string",
33957
+ "action": "append",
33958
+ "repeatable": true,
33959
+ "flags": [
33960
+ "--function-id"
33961
+ ],
33962
+ "takes_value": true
33963
+ },
33964
+ {
33965
+ "dest": "seniority",
33966
+ "required": false,
33967
+ "help": "Sales Nav seniority filter value; repeat or comma-separate",
33968
+ "kind": "string",
33969
+ "action": "append",
33970
+ "repeatable": true,
33971
+ "flags": [
33972
+ "--seniority"
33973
+ ],
33974
+ "takes_value": true
33975
+ },
33976
+ {
33977
+ "dest": "profile_language",
33978
+ "required": false,
33979
+ "help": "Profile language code, e.g. en",
33980
+ "kind": "string",
33981
+ "action": "append",
33982
+ "repeatable": true,
33983
+ "flags": [
33984
+ "--profile-language"
33985
+ ],
33986
+ "takes_value": true
33987
+ },
33988
+ {
33989
+ "dest": "technology_id",
33990
+ "required": false,
33991
+ "help": "Sales Nav technology ID applied to company search",
33992
+ "kind": "string",
33993
+ "action": "append",
33994
+ "repeatable": true,
33995
+ "flags": [
33996
+ "--technology-id"
33997
+ ],
33998
+ "takes_value": true
33999
+ },
34000
+ {
34001
+ "dest": "has_job_offers",
34002
+ "required": false,
34003
+ "help": "Filter companies currently hiring on LinkedIn",
34004
+ "kind": "boolean",
34005
+ "action": "store_true",
34006
+ "nargs": 0,
34007
+ "default_when_omitted": false,
34008
+ "value_when_present": true,
34009
+ "flags": [
34010
+ "--has-job-offers"
34011
+ ],
34012
+ "takes_value": false
34013
+ },
34014
+ {
34015
+ "dest": "company_filter",
34016
+ "required": false,
34017
+ "help": "Catalog company filter as FIELD=VALUE; repeat for multiple filters",
34018
+ "kind": "string",
34019
+ "action": "append",
34020
+ "repeatable": true,
34021
+ "flags": [
34022
+ "--company-filter"
34023
+ ],
34024
+ "takes_value": true
34025
+ },
34026
+ {
34027
+ "dest": "people_filter",
34028
+ "required": false,
34029
+ "help": "Catalog people filter as FIELD=VALUE; repeat for multiple filters",
34030
+ "kind": "string",
34031
+ "action": "append",
34032
+ "repeatable": true,
34033
+ "flags": [
34034
+ "--people-filter"
34035
+ ],
34036
+ "takes_value": true
34037
+ },
33880
34038
  {
33881
34039
  "dest": "extra_params_json",
33882
34040
  "required": false,
@@ -34106,7 +34264,7 @@
34106
34264
  },
34107
34265
  "availability": null,
34108
34266
  "examples": [
34109
- "autotouch linkedin list-build create [-h] [--data-json DATA_JSON]\n [--data-file DATA_FILE]\n [--mode {company-first-people}]\n [--linkedin-api {auto,classic,sales_navigator}]\n [--company-query COMPANY_QUERY]\n [--people-query PEOPLE_QUERY]\n [--headcount HEADCOUNT]\n [--num-results NUM_RESULTS]\n [--excluded-title EXCLUDED_TITLE]\n [--extra-params-json EXTRA_PARAMS_JSON]\n [--company-extra-params-json COMPANY_EXTRA_PARAMS_JSON]\n [--people-extra-params-json PEOPLE_EXTRA_PARAMS_JSON]\n [--wait]\n [--poll-interval POLL_INTERVAL]\n [--wait-timeout WAIT_TIMEOUT]\n [--base-url BASE_URL]\n [--token TOKEN] [--use-x-api-key]\n [--timeout TIMEOUT]\n [--output {json,ndjson,human}]\n [--compact] [--select SELECT |\n --json-pointer JSON_POINTER]\n [--verbose]"
34267
+ "autotouch linkedin list-build create [-h] [--data-json DATA_JSON]\n [--data-file DATA_FILE]\n [--mode {company-first-people}]\n [--linkedin-api {auto,classic,sales_navigator}]\n [--company-query COMPANY_QUERY]\n [--people-query PEOPLE_QUERY]\n [--headcount HEADCOUNT]\n [--num-results NUM_RESULTS]\n [--excluded-title EXCLUDED_TITLE]\n [--location-id LOCATION_ID]\n [--company-location-id COMPANY_LOCATION_ID]\n [--people-location-id PEOPLE_LOCATION_ID]\n [--company-industry-id COMPANY_INDUSTRY_ID]\n [--people-industry-id PEOPLE_INDUSTRY_ID]\n [--role-id ROLE_ID]\n [--function-id FUNCTION_ID]\n [--seniority SENIORITY]\n [--profile-language PROFILE_LANGUAGE]\n [--technology-id TECHNOLOGY_ID]\n [--has-job-offers]\n [--company-filter COMPANY_FILTER]\n [--people-filter PEOPLE_FILTER]\n [--extra-params-json EXTRA_PARAMS_JSON]\n [--company-extra-params-json COMPANY_EXTRA_PARAMS_JSON]\n [--people-extra-params-json PEOPLE_EXTRA_PARAMS_JSON]\n [--wait]\n [--poll-interval POLL_INTERVAL]\n [--wait-timeout WAIT_TIMEOUT]\n [--base-url BASE_URL]\n [--token TOKEN] [--use-x-api-key]\n [--timeout TIMEOUT]\n [--output {json,ndjson,human}]\n [--compact] [--select SELECT |\n --json-pointer JSON_POINTER]\n [--verbose]"
34110
34268
  ],
34111
34269
  "stability": "stable",
34112
34270
  "handler": "_fn"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.73
3
+ Version: 0.2.74
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
@@ -134,7 +134,7 @@ autotouch rows get --table-id "$TABLE_ID" --row-id "$ROW_ID" --output json
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
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`
137
+ - Build a paced LinkedIn people list on Smart Table workers: `autotouch linkedin list-build create --company-query 'SaaS startups' --headcount 1-10 --people-query 'Account Executive OR SDR' --num-results 250 --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`
@@ -149,7 +149,7 @@ For automation or agent-driven setup, use:
149
149
  - `autotouch cli-reference` for the shipped parser-generated reference
150
150
  - `autotouch capabilities --output json` for provider/workflow contracts
151
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
152
+ - `autotouch linkedin list-build create` for durable LinkedIn/Sales Navigator list building with Smart Table-owned paced provider requests, visible progress, and cooldown status
153
153
  - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
154
154
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
155
155
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
@@ -68,11 +68,14 @@ _LINKEDIN_ENDPOINTS: Dict[str, Dict[str, Any]] = {
68
68
  "headcount": "optional range such as 1-10 or {'min':1,'max':10}",
69
69
  "num_results": "optional 1-1000 target people results",
70
70
  "excluded_titles": "optional array of titles to exclude after people results return",
71
+ "filters": "optional catalog filters applied to company and people searches",
72
+ "company_filters": "optional catalog filters applied to company search; compiled by API mode",
73
+ "people_filters": "optional catalog filters applied to people search; compiled by API mode",
71
74
  "extra_params": "optional object applied to company and people searches",
72
75
  "company_extra_params": "optional object applied only to company search",
73
- "people_extra_params": "optional object applied only to people search",
76
+ "people_extra_params": "optional object applied only to people search; structured title filters should use provider IDs",
74
77
  },
75
- "notes": "Recommended path for large LinkedIn/Sales Navigator lists. Runs as a paced background job with durable progress and cooldown state.",
78
+ "notes": "Recommended path for large LinkedIn/Sales Navigator lists. Runs as a Smart Table-owned paced background job with durable progress and cooldown state.",
76
79
  },
77
80
  "linkedin_list_build_status": {
78
81
  "method": "GET",
@@ -196,13 +199,16 @@ _LINKEDIN_RECIPES: Dict[str, Dict[str, Any]] = {
196
199
  "headcount": "1-10",
197
200
  "num_results": 250,
198
201
  "excluded_titles": ["founder", "owner", "ceo", "chief", "president"],
199
- "company_extra_params": {"industry": ["4"]},
200
- "people_extra_params": {},
202
+ "company_filters": {"location": ["103644278"], "industry": ["6"]},
203
+ "people_filters": {"location": ["103644278"], "profile_language": ["en"]},
201
204
  },
202
205
  "usage": "autotouch linkedin list-build create --data-file <payload.json> --wait",
203
206
  "notes": [
204
207
  "Recommended path for larger LinkedIn/Sales Navigator lists.",
205
- "The worker searches companies first, filters returned headcount locally, then searches people at matched companies.",
208
+ "Execution is owned by Smart Table workers; the retired standalone List Builder worker is not required.",
209
+ "The worker sends headcount to company search, validates returned headcount locally, then searches people at matched companies.",
210
+ "Catalog filters compile to classic or Sales Navigator shapes after the connected account mode is resolved.",
211
+ "Use title text in people_query; only use structured role/job-title params after resolving provider IDs.",
206
212
  "No provider-hidden fallback is used when LinkedIn is disconnected, cooled down, or rate limited.",
207
213
  "Use 'autotouch linkedin filters' and 'autotouch linkedin search-params' to discover structured filter fields and IDs.",
208
214
  ],
@@ -280,12 +280,13 @@ _FILTERS: List[Dict[str, Any]] = [
280
280
  provider_field="role",
281
281
  api_modes=["sales_navigator"],
282
282
  categories=["people"],
283
- value_kind="id_or_text",
283
+ value_kind="id",
284
284
  resolver_type="JOB_TITLE",
285
285
  supports_include_exclude=True,
286
- shape='{"include":["Account Executive","Sales Development Representative"],"exclude":["Founder"]}',
287
- examples=[{"include": ["Account Executive"], "exclude": ["Founder", "Owner"]}],
286
+ shape='{"include":["1234"],"exclude":["5678"]}',
287
+ examples=[{"include": ["1234"], "exclude": ["5678"]}],
288
288
  aliases=["job_title", "title"],
289
+ notes=["Resolve human title text with /linkedin/search/parameters before using this structured filter."],
289
290
  ),
290
291
  _entry(
291
292
  "past_role",
@@ -633,6 +634,152 @@ def linkedin_parameter_types(*, service: Optional[str] = None) -> List[Dict[str,
633
634
  return [deepcopy(entry) for entry in _PARAMETER_TYPES if target in entry.get("services", [])]
634
635
 
635
636
 
637
+ def _find_filter_entry(field: str, *, api_mode: str, category: str) -> Optional[Dict[str, Any]]:
638
+ target = str(field or "").strip().lower()
639
+ api = str(api_mode or "").strip().lower()
640
+ cat = str(category or "").strip().lower()
641
+ for entry in _FILTERS:
642
+ aliases = [str(alias).lower() for alias in entry.get("aliases", [])]
643
+ if target not in {str(entry.get("field", "")).lower(), *aliases}:
644
+ continue
645
+ if api not in entry.get("api_modes", []):
646
+ continue
647
+ if cat not in entry.get("categories", []):
648
+ continue
649
+ return entry
650
+ return None
651
+
652
+
653
+ def _list_values(value: Any) -> List[Any]:
654
+ if value is None or value == "":
655
+ return []
656
+ if isinstance(value, list):
657
+ values = value
658
+ elif isinstance(value, tuple):
659
+ values = list(value)
660
+ else:
661
+ values = [value]
662
+ return [item for item in values if item not in (None, "")]
663
+
664
+
665
+ def _include_exclude(value: Any) -> Dict[str, List[Any]]:
666
+ if isinstance(value, dict):
667
+ return {
668
+ "include": _list_values(value.get("include") or value.get("includes") or value.get("values")),
669
+ "exclude": _list_values(value.get("exclude") or value.get("excludes")),
670
+ }
671
+ return {"include": _list_values(value), "exclude": []}
672
+
673
+
674
+ def _parse_range_filter(value: Any) -> List[Dict[str, int]]:
675
+ if value is None or value == "":
676
+ return []
677
+ if isinstance(value, list):
678
+ output: List[Dict[str, int]] = []
679
+ for item in value:
680
+ output.extend(_parse_range_filter(item))
681
+ return output
682
+ if isinstance(value, dict):
683
+ try:
684
+ minimum = int(value.get("min"))
685
+ except Exception:
686
+ return []
687
+ parsed: Dict[str, int] = {"min": minimum}
688
+ maximum = value.get("max")
689
+ if maximum not in (None, ""):
690
+ try:
691
+ parsed["max"] = int(maximum)
692
+ except Exception:
693
+ pass
694
+ return [parsed]
695
+
696
+ raw = str(value).strip().lower().replace(",", "")
697
+ for bucket in HEADCOUNT_BUCKETS:
698
+ if raw == str(bucket["label"]).lower().replace(",", ""):
699
+ parsed = {"min": int(bucket["min"])}
700
+ if bucket.get("max") is not None:
701
+ parsed["max"] = int(bucket["max"])
702
+ return [parsed]
703
+ numbers: List[int] = []
704
+ current = ""
705
+ for char in raw:
706
+ if char.isdigit():
707
+ current += char
708
+ elif current:
709
+ numbers.append(int(current))
710
+ current = ""
711
+ if current:
712
+ numbers.append(int(current))
713
+ if not numbers:
714
+ return []
715
+ parsed = {"min": numbers[0]}
716
+ if "+" not in raw and len(numbers) > 1:
717
+ parsed["max"] = numbers[1]
718
+ return [parsed]
719
+
720
+
721
+ def _coerce_filter_value(entry: Dict[str, Any], value: Any) -> Any:
722
+ field = str(entry.get("field") or "")
723
+ kind = str(entry.get("value_kind") or "")
724
+ shape = str(entry.get("shape") or "")
725
+
726
+ if kind in {"range_bucket", "numeric_range"}:
727
+ if kind == "numeric_range" and isinstance(value, dict):
728
+ return value
729
+ return _parse_range_filter(value)
730
+ if kind == "boolean":
731
+ if isinstance(value, bool):
732
+ return value
733
+ return str(value).strip().lower() in {"1", "true", "yes", "y", "on"}
734
+ if kind == "object":
735
+ return value if isinstance(value, dict) else {}
736
+ if kind in {"enum", "iso_639_1", "enum_or_number"}:
737
+ if shape.startswith("["):
738
+ return _list_values(value)
739
+ return value
740
+ if field in {"saved_search_id", "recent_search_id"}:
741
+ values = _list_values(value)
742
+ return str(values[0]) if values else ""
743
+ if entry.get("supports_include_exclude"):
744
+ parts = _include_exclude(value)
745
+ payload: Dict[str, Any] = {}
746
+ if parts["include"]:
747
+ payload["include"] = parts["include"]
748
+ if parts["exclude"]:
749
+ payload["exclude"] = parts["exclude"]
750
+ return payload
751
+ return _list_values(value) if shape.startswith("[") else value
752
+
753
+
754
+ def compile_linkedin_search_filters(
755
+ filters: Optional[Dict[str, Any]],
756
+ *,
757
+ api_mode: str,
758
+ category: str,
759
+ ) -> Dict[str, Any]:
760
+ """Compile catalog-level filter values into Unipile search extra_params.
761
+
762
+ The caller should pass the already resolved LinkedIn API mode. This keeps
763
+ CLI clients mode-neutral while still emitting classic vs Sales Navigator
764
+ shapes correctly after account capability resolution.
765
+ """
766
+ if not isinstance(filters, dict):
767
+ return {}
768
+ compiled: Dict[str, Any] = {}
769
+ for raw_field, raw_value in filters.items():
770
+ field = str(raw_field or "").strip()
771
+ if not field:
772
+ continue
773
+ entry = _find_filter_entry(field, api_mode=api_mode, category=category)
774
+ if entry is None:
775
+ raise ValueError(f"LinkedIn filter '{field}' is not supported for {api_mode}/{category}")
776
+ value = _coerce_filter_value(entry, raw_value)
777
+ if value in (None, "", [], {}):
778
+ continue
779
+ compiled[str(entry.get("provider_field") or field)] = value
780
+ return compiled
781
+
782
+
636
783
  def linkedin_filter_catalog(
637
784
  *,
638
785
  api_mode: Optional[str] = None,
@@ -654,6 +801,7 @@ def linkedin_filter_catalog(
654
801
 
655
802
  __all__ = [
656
803
  "HEADCOUNT_BUCKETS",
804
+ "compile_linkedin_search_filters",
657
805
  "linkedin_filter_catalog",
658
806
  "linkedin_filter_entries",
659
807
  "linkedin_parameter_types",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autotouch-cli"
7
- version = "0.2.73"
7
+ version = "0.2.74"
8
8
  description = "Autotouch Smart Table CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes