autotouch-cli 0.2.70__tar.gz → 0.2.73__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.70 → autotouch_cli-0.2.73}/PKG-INFO +14 -1
  2. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/README.md +13 -0
  3. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/cli.py +11 -0
  4. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/cli_contracts.py +1 -0
  5. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/agents.py +160 -3
  6. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/linkedin.py +215 -4
  7. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/http.py +14 -0
  8. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/data/CLI_REFERENCE.md +585 -7
  9. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/data/cli-manifest.json +1532 -235
  10. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/parser.py +4 -0
  11. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/PKG-INFO +14 -1
  12. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/SOURCES.txt +1 -0
  13. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/linkedin_contract.py +76 -9
  14. autotouch_cli-0.2.73/autotouch_shared/linkedin_filters.py +660 -0
  15. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/provider_registry.py +3 -1
  16. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/pyproject.toml +1 -1
  17. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/MANIFEST.in +0 -0
  18. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/__init__.py +0 -0
  19. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/__init__.py +0 -0
  20. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/auth.py +0 -0
  21. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/cells.py +0 -0
  22. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/columns.py +0 -0
  23. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/jobs.py +0 -0
  24. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/leads.py +0 -0
  25. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/prompts.py +0 -0
  26. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/rows.py +0 -0
  27. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/search.py +0 -0
  28. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/sequences.py +0 -0
  29. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/tables.py +0 -0
  30. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/tasks.py +0 -0
  31. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/webhooks.py +0 -0
  32. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/workspace_secrets.py +0 -0
  33. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/__init__.py +0 -0
  34. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/auth.py +0 -0
  35. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/config.py +0 -0
  36. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/csv_import.py +0 -0
  37. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/io.py +0 -0
  38. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/output.py +0 -0
  39. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/polling.py +0 -0
  40. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/run.py +0 -0
  41. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/validation.py +0 -0
  42. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/exceptions.py +0 -0
  43. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/mongo_status.py +0 -0
  44. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/parser_groups.py +0 -0
  45. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/sequence_support.py +0 -0
  46. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/templates.py +0 -0
  47. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  48. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/entry_points.txt +0 -0
  49. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/requires.txt +0 -0
  50. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/top_level.txt +0 -0
  51. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/__init__.py +0 -0
  52. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/search_contract.py +0 -0
  53. {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.70
3
+ Version: 0.2.73
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,6 +133,9 @@ 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`
138
+ - Run one LinkedIn search page/debug replay: `autotouch linkedin search`
136
139
  - Run controlled slices: `autotouch columns run-next`
137
140
  - Poll authoritative state: `autotouch jobs get`
138
141
  - Create/activate sequences: `autotouch sequences recipe`, `autotouch sequences create`, `autotouch sequences activate`
@@ -145,10 +148,19 @@ For automation or agent-driven setup, use:
145
148
  - `autotouch cli-manifest --output json` for the local machine-readable command contract
146
149
  - `autotouch cli-reference` for the shipped parser-generated reference
147
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
153
+ - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
148
154
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
149
155
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
150
156
  - `pip install 'autotouch-cli[mongo]'` if you need the Mongo-backed `status` / `watch` commands
151
157
 
158
+ ## Realtime Table Updates
159
+
160
+ Research-table cell updates are persistence-first. Writers update Mongo `cells`; the API-side Mongo change-stream listener emits table-scoped `cells_update_batch` events. Workers should not emit per-cell socket events directly, and `/api/events/emit` is not a cell-update transport.
161
+
162
+ CSV import/export and long-running worker flows still emit low-rate lifecycle/progress events such as `table_update`, but saved cell state is the source of truth for table rendering. For the full contract, see `docs/platform/realtime-events.md` and `docs/workers/bulk-jobs.md`.
163
+
152
164
  ## LLM Columns
153
165
 
154
166
  For `llm_enrichment` in `agent` mode, the recommended path is:
@@ -175,3 +187,4 @@ Prompt variables in authored prompts support nested JSON access:
175
187
  - Agent playbook: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/guides/autotouch-cli-agent-playbook.md
176
188
  - Tables/API reference: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/reference/tables-api.md
177
189
  - Authentication/scopes: https://github.com/nicolonic/autotouch_main/blob/main/docs/platform/authentication.md
190
+ - Instantly/Smartlead external sending accounts: https://github.com/nicolonic/autotouch_main/blob/main/docs/integrations/email-automation-platforms.md
@@ -108,6 +108,9 @@ 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`
113
+ - Run one LinkedIn search page/debug replay: `autotouch linkedin search`
111
114
  - Run controlled slices: `autotouch columns run-next`
112
115
  - Poll authoritative state: `autotouch jobs get`
113
116
  - Create/activate sequences: `autotouch sequences recipe`, `autotouch sequences create`, `autotouch sequences activate`
@@ -120,10 +123,19 @@ For automation or agent-driven setup, use:
120
123
  - `autotouch cli-manifest --output json` for the local machine-readable command contract
121
124
  - `autotouch cli-reference` for the shipped parser-generated reference
122
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
128
+ - `autotouch linkedin search` for one-page LinkedIn/Sales Navigator replay/debug searches; it is not the recommended path for large lists
123
129
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
124
130
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
125
131
  - `pip install 'autotouch-cli[mongo]'` if you need the Mongo-backed `status` / `watch` commands
126
132
 
133
+ ## Realtime Table Updates
134
+
135
+ Research-table cell updates are persistence-first. Writers update Mongo `cells`; the API-side Mongo change-stream listener emits table-scoped `cells_update_batch` events. Workers should not emit per-cell socket events directly, and `/api/events/emit` is not a cell-update transport.
136
+
137
+ CSV import/export and long-running worker flows still emit low-rate lifecycle/progress events such as `table_update`, but saved cell state is the source of truth for table rendering. For the full contract, see `docs/platform/realtime-events.md` and `docs/workers/bulk-jobs.md`.
138
+
127
139
  ## LLM Columns
128
140
 
129
141
  For `llm_enrichment` in `agent` mode, the recommended path is:
@@ -150,3 +162,4 @@ Prompt variables in authored prompts support nested JSON access:
150
162
  - Agent playbook: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/guides/autotouch-cli-agent-playbook.md
151
163
  - Tables/API reference: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/reference/tables-api.md
152
164
  - Authentication/scopes: https://github.com/nicolonic/autotouch_main/blob/main/docs/platform/authentication.md
165
+ - Instantly/Smartlead external sending accounts: https://github.com/nicolonic/autotouch_main/blob/main/docs/integrations/email-automation-platforms.md
@@ -93,7 +93,11 @@ from autotouch_cli.commands.leads import (
93
93
  from autotouch_cli.commands.linkedin import (
94
94
  LinkedInCommandRuntime as LinkedInCommandHandlerRuntime,
95
95
  cmd_linkedin_comments as cmd_linkedin_comments_impl,
96
+ cmd_linkedin_filters as cmd_linkedin_filters_impl,
96
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,
97
101
  cmd_linkedin_post as cmd_linkedin_post_impl,
98
102
  cmd_linkedin_posts as cmd_linkedin_posts_impl,
99
103
  cmd_linkedin_reactions as cmd_linkedin_reactions_impl,
@@ -1544,6 +1548,10 @@ _register("linkedin_status", cmd_linkedin_status_impl, _linkedin_command_runtime
1544
1548
  _register("linkedin_limits", cmd_linkedin_limits_impl, _linkedin_command_runtime)
1545
1549
  _register("linkedin_search", cmd_linkedin_search_impl, _linkedin_command_runtime)
1546
1550
  _register("linkedin_search_params", cmd_linkedin_search_params_impl, _linkedin_command_runtime)
1551
+ _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)
1547
1555
  _register("linkedin_posts", cmd_linkedin_posts_impl, _linkedin_command_runtime)
1548
1556
  _register("linkedin_post", cmd_linkedin_post_impl, _linkedin_command_runtime)
1549
1557
  _register("linkedin_comments", cmd_linkedin_comments_impl, _linkedin_command_runtime)
@@ -1962,6 +1970,9 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
1962
1970
  print("Not for :")
1963
1971
  for item in not_for:
1964
1972
  print(f" - {item}")
1973
+ filter_count = len((linkedin.get("filter_catalog") or {}).get("filters") or [])
1974
+ if filter_count:
1975
+ print(f"Structured filters: {filter_count} exposed via `autotouch linkedin filters`")
1965
1976
  print("Tip: run `autotouch linkedin recipe` for example payloads.")
1966
1977
  if isinstance(search, dict):
1967
1978
  print("")
@@ -25,6 +25,7 @@ _AUTH_MODE_EXACT: Dict[str, str] = {
25
25
  "webhooks.ingest": "webhook_token",
26
26
  "cli-manifest": "none",
27
27
  "cli-reference": "none",
28
+ "linkedin.filters": "none",
28
29
  "schema": "none",
29
30
  "status": "none",
30
31
  "watch": "none",
@@ -59,15 +59,47 @@ def cmd_agents_create(args: argparse.Namespace, *, runtime: AgentCommandRuntime)
59
59
  payload["targetType"] = args.target
60
60
  if args.assigned_user:
61
61
  payload["assignedUserId"] = args.assigned_user
62
+ # Build the schedule object from any provided schedule flags. We only
63
+ # send a `schedule` key when the user actually set at least one field.
64
+ schedule_payload: Dict[str, Any] = {}
62
65
  if args.schedule_time:
63
- tz = getattr(args, "schedule_tz", None) or "UTC"
64
- payload["schedule"] = {"timezone": tz, "timeOfDay": args.schedule_time}
66
+ schedule_payload["timeOfDay"] = args.schedule_time
67
+ schedule_tz = getattr(args, "schedule_tz", None)
68
+ if schedule_tz:
69
+ schedule_payload["timezone"] = schedule_tz
70
+ schedule_frequency = getattr(args, "schedule_frequency", None)
71
+ if schedule_frequency:
72
+ schedule_payload["frequency"] = schedule_frequency
73
+ schedule_days = getattr(args, "schedule_days", None)
74
+ if schedule_days:
75
+ schedule_payload["days"] = schedule_days
76
+ schedule_day_of_week = getattr(args, "schedule_day_of_week", None)
77
+ if schedule_day_of_week is not None:
78
+ schedule_payload["dayOfWeek"] = schedule_day_of_week
79
+ schedule_window_start = getattr(args, "schedule_window_start", None)
80
+ if schedule_window_start:
81
+ schedule_payload["windowStart"] = schedule_window_start
82
+ schedule_window_end = getattr(args, "schedule_window_end", None)
83
+ if schedule_window_end:
84
+ schedule_payload["windowEnd"] = schedule_window_end
85
+ if schedule_payload:
86
+ # Backfill required fields the API expects (timezone defaults to UTC,
87
+ # timeOfDay defaults to 09:00 — required even for sub-daily frequencies).
88
+ schedule_payload.setdefault("timezone", "UTC")
89
+ schedule_payload.setdefault("timeOfDay", "09:00")
90
+ payload["schedule"] = schedule_payload
65
91
  if getattr(args, "auto_find_email", False):
66
92
  payload["autoFindEmail"] = True
67
93
  if getattr(args, "auto_find_phone", False):
68
94
  payload["autoFindPhone"] = True
69
95
  if getattr(args, "auto_broaden", False):
70
96
  payload["autoBroadenSearch"] = True
97
+ auto_create_lead_mode = getattr(args, "auto_create_lead_mode", None)
98
+ if auto_create_lead_mode:
99
+ payload["autoCreateLeadMode"] = auto_create_lead_mode
100
+ auto_sequence_id = getattr(args, "auto_sequence_id", None)
101
+ if auto_sequence_id:
102
+ payload["autoSequenceId"] = auto_sequence_id
71
103
  status = getattr(args, "status", None)
72
104
  if status:
73
105
  payload["status"] = status
@@ -165,6 +197,47 @@ def cmd_agents_update(args: argparse.Namespace, *, runtime: AgentCommandRuntime)
165
197
  payload["autoFindPhone"] = args.auto_find_phone
166
198
  if getattr(args, "auto_broaden", None) is not None:
167
199
  payload["autoBroadenSearch"] = args.auto_broaden
200
+ if getattr(args, "auto_create_lead_mode", None):
201
+ payload["autoCreateLeadMode"] = args.auto_create_lead_mode
202
+ # Pass an empty string to clear the auto-sequence link.
203
+ auto_sequence_id_arg = getattr(args, "auto_sequence_id", None)
204
+ if auto_sequence_id_arg is not None:
205
+ payload["autoSequenceId"] = auto_sequence_id_arg or None
206
+
207
+ # Schedule changes need a full schedule object, so we merge any
208
+ # provided flags onto the agent's existing schedule.
209
+ schedule_flag_values = {
210
+ "timeOfDay": getattr(args, "schedule_time", None),
211
+ "timezone": getattr(args, "schedule_tz", None),
212
+ "frequency": getattr(args, "schedule_frequency", None),
213
+ "days": getattr(args, "schedule_days", None),
214
+ "dayOfWeek": getattr(args, "schedule_day_of_week", None),
215
+ "windowStart": getattr(args, "schedule_window_start", None),
216
+ "windowEnd": getattr(args, "schedule_window_end", None),
217
+ }
218
+ if any(v is not None for v in schedule_flag_values.values()):
219
+ existing = runtime.request_api(
220
+ "GET", f"/api/agents/{args.agent_id}",
221
+ base_url=args.base_url, token=token,
222
+ use_x_api_key=args.use_x_api_key,
223
+ timeout=args.timeout, verbose=args.verbose,
224
+ )
225
+ existing_schedule = (existing or {}).get("schedule") or {}
226
+ merged_schedule: Dict[str, Any] = {
227
+ "timezone": existing_schedule.get("timezone") or "UTC",
228
+ "timeOfDay": existing_schedule.get("timeOfDay") or "09:00",
229
+ "frequency": existing_schedule.get("frequency") or "daily",
230
+ "days": existing_schedule.get("days") or "all",
231
+ "dayOfWeek": existing_schedule.get("dayOfWeek") or 0,
232
+ }
233
+ if existing_schedule.get("windowStart"):
234
+ merged_schedule["windowStart"] = existing_schedule["windowStart"]
235
+ if existing_schedule.get("windowEnd"):
236
+ merged_schedule["windowEnd"] = existing_schedule["windowEnd"]
237
+ for key, value in schedule_flag_values.items():
238
+ if value is not None:
239
+ merged_schedule[key] = value
240
+ payload["schedule"] = merged_schedule
168
241
 
169
242
  data = runtime.request_api(
170
243
  "PATCH", f"/api/agents/{args.agent_id}",
@@ -376,11 +449,52 @@ def register_agents_subcommands(
376
449
  pac.add_argument("--name", help="Agent name")
377
450
  pac.add_argument("--target", choices=["LEADS"], help="Target type")
378
451
  pac.add_argument("--assigned-user", dest="assigned_user", help="Assigned user ID")
379
- pac.add_argument("--schedule-time", dest="schedule_time", help="Time of day (HH:MM)")
452
+ pac.add_argument("--schedule-time", dest="schedule_time", help="Time of day (HH:MM) — used by daily/weekly")
380
453
  pac.add_argument("--schedule-tz", dest="schedule_tz", help="Timezone (default: UTC)")
454
+ pac.add_argument(
455
+ "--schedule-frequency",
456
+ dest="schedule_frequency",
457
+ choices=["hourly", "every_6h", "every_12h", "daily", "weekly"],
458
+ help="Run cadence (default: daily)",
459
+ )
460
+ pac.add_argument(
461
+ "--schedule-days",
462
+ dest="schedule_days",
463
+ choices=["all", "weekdays"],
464
+ help="Which days to run on for sub-weekly cadences (default: all)",
465
+ )
466
+ pac.add_argument(
467
+ "--schedule-day-of-week",
468
+ dest="schedule_day_of_week",
469
+ type=int,
470
+ choices=range(0, 7),
471
+ metavar="0-6",
472
+ help="Day of week for weekly cadence (0=Mon, 6=Sun)",
473
+ )
474
+ pac.add_argument(
475
+ "--schedule-window-start",
476
+ dest="schedule_window_start",
477
+ help="Active-hours window start (HH:MM) — used by hourly/every_6h/every_12h",
478
+ )
479
+ pac.add_argument(
480
+ "--schedule-window-end",
481
+ dest="schedule_window_end",
482
+ help="Active-hours window end (HH:MM) — used by hourly/every_6h/every_12h",
483
+ )
381
484
  pac.add_argument("--auto-find-email", dest="auto_find_email", action="store_true", default=False, help="Enable auto email finder")
382
485
  pac.add_argument("--auto-find-phone", dest="auto_find_phone", action="store_true", default=False, help="Enable auto phone finder")
383
486
  pac.add_argument("--auto-broaden", dest="auto_broaden", action="store_true", default=False, help="Enable auto broaden search")
487
+ pac.add_argument(
488
+ "--auto-create-lead-mode",
489
+ dest="auto_create_lead_mode",
490
+ choices=["qualified_only", "everyone", "manual_only"],
491
+ help="Which discovered prospects become leads (default: qualified_only)",
492
+ )
493
+ pac.add_argument(
494
+ "--auto-sequence-id",
495
+ dest="auto_sequence_id",
496
+ help="Sequence ID — discovered leads get auto-enrolled. Triggers outreach automatically.",
497
+ )
384
498
  pac.add_argument("--status", choices=["DRAFT", "ACTIVE", "PAUSED"], help="Initial status")
385
499
  pac.add_argument("--from-file", dest="from_file", help="Path to JSON payload file")
386
500
  pac.add_argument("--generate", action="store_true", help="Auto-generate persona, topics, and job signal config")
@@ -395,6 +509,49 @@ def register_agents_subcommands(
395
509
  pau.add_argument("--auto-find-email", dest="auto_find_email", type=_bool_arg, default=None, help="Enable/disable auto email finder")
396
510
  pau.add_argument("--auto-find-phone", dest="auto_find_phone", type=_bool_arg, default=None, help="Enable/disable auto phone finder")
397
511
  pau.add_argument("--auto-broaden", dest="auto_broaden", type=_bool_arg, default=None, help="Enable/disable auto broaden search")
512
+ pau.add_argument(
513
+ "--auto-create-lead-mode",
514
+ dest="auto_create_lead_mode",
515
+ choices=["qualified_only", "everyone", "manual_only"],
516
+ help="Which discovered prospects become leads",
517
+ )
518
+ pau.add_argument(
519
+ "--auto-sequence-id",
520
+ dest="auto_sequence_id",
521
+ help="Sequence ID for auto-enrollment. Pass empty string to clear.",
522
+ )
523
+ pau.add_argument("--schedule-time", dest="schedule_time", help="Time of day (HH:MM) — used by daily/weekly")
524
+ pau.add_argument("--schedule-tz", dest="schedule_tz", help="Timezone")
525
+ pau.add_argument(
526
+ "--schedule-frequency",
527
+ dest="schedule_frequency",
528
+ choices=["hourly", "every_6h", "every_12h", "daily", "weekly"],
529
+ help="Run cadence",
530
+ )
531
+ pau.add_argument(
532
+ "--schedule-days",
533
+ dest="schedule_days",
534
+ choices=["all", "weekdays"],
535
+ help="Which days to run on for sub-weekly cadences",
536
+ )
537
+ pau.add_argument(
538
+ "--schedule-day-of-week",
539
+ dest="schedule_day_of_week",
540
+ type=int,
541
+ choices=range(0, 7),
542
+ metavar="0-6",
543
+ help="Day of week for weekly cadence (0=Mon, 6=Sun)",
544
+ )
545
+ pau.add_argument(
546
+ "--schedule-window-start",
547
+ dest="schedule_window_start",
548
+ help="Active-hours window start (HH:MM) — used by hourly/every_6h/every_12h",
549
+ )
550
+ pau.add_argument(
551
+ "--schedule-window-end",
552
+ dest="schedule_window_end",
553
+ help="Active-hours window end (HH:MM) — used by hourly/every_6h/every_12h",
554
+ )
398
555
  pau.add_argument("--from-file", dest="from_file", help="Path to JSON patch payload file")
399
556
  add_api_common_arguments(pau)
400
557
  pau.set_defaults(func=handlers["update"])
@@ -2,9 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import json
5
+ import time
5
6
  from dataclasses import dataclass
6
- from typing import Any, Callable, Dict, Optional, Sequence
7
+ from typing import Any, Callable, Dict, List, Optional, Sequence
7
8
 
9
+ from autotouch_shared.linkedin_filters import linkedin_filter_catalog
8
10
  from autotouch_cli.exceptions import AutotouchInputError
9
11
 
10
12
 
@@ -66,8 +68,8 @@ def _normalize_linkedin_search_payload(args: argparse.Namespace, *, runtime: Lin
66
68
  keywords = str(getattr(args, "keywords", "") or "").strip()
67
69
  if keywords:
68
70
  payload["keywords"] = keywords
69
- linkedin_api = str(getattr(args, "linkedin_api", "classic") or "classic").strip()
70
- if linkedin_api:
71
+ linkedin_api = str(getattr(args, "linkedin_api", "auto") or "auto").strip()
72
+ if linkedin_api and linkedin_api != "auto":
71
73
  payload["linkedin_api"] = linkedin_api
72
74
  cursor = str(getattr(args, "cursor", "") or "").strip()
73
75
  if cursor:
@@ -101,6 +103,9 @@ def cmd_linkedin_search_params(args: argparse.Namespace, *, runtime: LinkedInCom
101
103
  "keywords": str(getattr(args, "keywords", "") or "").strip(),
102
104
  "limit": max(1, min(int(getattr(args, "limit", 25) or 25), 250)),
103
105
  }
106
+ linkedin_api = str(getattr(args, "linkedin_api", "auto") or "auto").strip()
107
+ if linkedin_api and linkedin_api != "auto":
108
+ payload["linkedin_api"] = linkedin_api
104
109
  data = runtime.request_api(
105
110
  "POST",
106
111
  "/api/integrations/linkedin/search/parameters",
@@ -114,6 +119,168 @@ def cmd_linkedin_search_params(args: argparse.Namespace, *, runtime: LinkedInCom
114
119
  runtime.print_json(data, args.compact)
115
120
 
116
121
 
122
+ def _load_optional_json_object(raw: Optional[str], *, context: str) -> Optional[Dict[str, Any]]:
123
+ if not raw:
124
+ return None
125
+ try:
126
+ parsed = json.loads(raw)
127
+ except Exception as exc:
128
+ raise AutotouchInputError(f"invalid JSON for {context}: {exc}") from exc
129
+ if not isinstance(parsed, dict):
130
+ raise AutotouchInputError(f"{context} must be a JSON object")
131
+ return parsed
132
+
133
+
134
+ def _split_repeated_values(values: Optional[List[str]]) -> List[str]:
135
+ output: List[str] = []
136
+ for raw in values or []:
137
+ for part in str(raw).replace("\n", ",").split(","):
138
+ value = part.strip()
139
+ if value:
140
+ output.append(value)
141
+ return output
142
+
143
+
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)
233
+
234
+
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)
248
+
249
+
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)
269
+
270
+
271
+ def cmd_linkedin_filters(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> None:
272
+ output = linkedin_filter_catalog(
273
+ api_mode=getattr(args, "api", None),
274
+ category=getattr(args, "category", None),
275
+ field=getattr(args, "field", None),
276
+ )
277
+ out_file = str(getattr(args, "out_file", "") or "").strip()
278
+ if out_file:
279
+ with open(out_file, "w", encoding="utf-8") as f:
280
+ json.dump(output, f, indent=2)
281
+ runtime.print_json(output, args.compact)
282
+
283
+
117
284
  def cmd_linkedin_recipe(
118
285
  args: argparse.Namespace,
119
286
  *,
@@ -253,7 +420,7 @@ def register_linkedin_subcommands(
253
420
  search_parser.add_argument("--url", help="Full LinkedIn/Sales Navigator search URL (overrides keyword filters)")
254
421
  search_parser.add_argument("--category", choices=["people", "companies", "jobs", "posts"], default="people", help="Search category (default: people)")
255
422
  search_parser.add_argument("--keywords", help="Free-text keyword search")
256
- search_parser.add_argument("--linkedin-api", choices=["classic", "sales_navigator"], default="classic", help="API mode (default: classic)")
423
+ search_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help="API mode (default: auto; uses connected account mode)")
257
424
  search_parser.add_argument("--cursor", help="Pagination cursor from previous response")
258
425
  search_parser.add_argument("--limit", type=int, default=25, help="Results per page, 1-250 (default: 25)")
259
426
  search_parser.add_argument("--extra-params-json", help="JSON object with additional search filters (location IDs, industry, etc.)")
@@ -263,10 +430,54 @@ def register_linkedin_subcommands(
263
430
  params_parser = linkedin_sub.add_parser("search-params", help="Resolve names to LinkedIn parameter IDs for search filters")
264
431
  params_parser.add_argument("--type", required=True, help="Parameter type: location, industry, company, school, title, language, etc.")
265
432
  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)")
266
434
  params_parser.add_argument("--limit", type=int, default=25, help="Max results, 1-250 (default: 25)")
267
435
  add_api_common_arguments(params_parser)
268
436
  params_parser.set_defaults(func=handlers["search_params"])
269
437
 
438
+ filters_parser = linkedin_sub.add_parser("filters", help="Print the LinkedIn/Sales Navigator structured filter catalog")
439
+ filters_parser.add_argument("--api", choices=["all", "classic", "sales_navigator"], default="all", help="Filter by API mode")
440
+ filters_parser.add_argument("--category", choices=["all", "people", "companies", "jobs", "posts"], default="all", help="Filter by search category")
441
+ filters_parser.add_argument("--field", help="Show one filter field or alias, e.g. headcount, company_headcount, role")
442
+ filters_parser.add_argument("--out-file", help="Optional path to save the selected catalog JSON")
443
+ add_api_common_arguments(filters_parser)
444
+ filters_parser.set_defaults(func=handlers["filters"])
445
+
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
+
270
481
  posts_parser = linkedin_sub.add_parser("posts", help="List LinkedIn posts for a user")
271
482
  posts_parser.add_argument("--identifier", required=True, help="LinkedIn profile URL, public ID, or URN")
272
483
  posts_parser.add_argument("--cursor", help="Pagination cursor from previous response")
@@ -47,7 +47,19 @@ def _decode_response_body(response: Any) -> Any:
47
47
  return response.text
48
48
 
49
49
 
50
+ def _print_special_error_hint(body: Any) -> None:
51
+ detail = body.get("detail") if isinstance(body, dict) else None
52
+ if isinstance(detail, dict) and detail.get("error_code") == "sales_nav_reconnect_required":
53
+ message = detail.get("message") or "Sales Navigator search credentials expired."
54
+ print(f"ERROR: {message}", file=sys.stderr)
55
+ print(
56
+ "Open Autotouch > Settings > Inbox > LinkedIn > Reconnect Sales Navigator.",
57
+ file=sys.stderr,
58
+ )
59
+
60
+
50
61
  def _print_error_body(body: Any) -> None:
62
+ _print_special_error_hint(body)
51
63
  if isinstance(body, (dict, list)):
52
64
  print(json.dumps(body, indent=2, default=str), file=sys.stderr)
53
65
  else:
@@ -129,6 +141,7 @@ def request_api(
129
141
  )
130
142
  print(f"ERROR: API {response.status_code}", file=sys.stderr)
131
143
  if error_body_writer is not None:
144
+ _print_special_error_hint(body)
132
145
  error_body_writer(body)
133
146
  else:
134
147
  _print_error_body(body)
@@ -212,6 +225,7 @@ def request_multipart_api(
212
225
  )
213
226
  print(f"ERROR: API {response.status_code}", file=sys.stderr)
214
227
  if error_body_writer is not None:
228
+ _print_special_error_hint(body)
215
229
  error_body_writer(body)
216
230
  else:
217
231
  _print_error_body(body)