autotouch-cli 0.2.70__tar.gz → 0.2.72__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.72}/PKG-INFO +11 -1
  2. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/README.md +10 -0
  3. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/cli.py +5 -0
  4. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/cli_contracts.py +1 -0
  5. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/agents.py +160 -3
  6. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/linkedin.py +29 -3
  7. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/http.py +14 -0
  8. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/data/CLI_REFERENCE.md +449 -5
  9. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/data/cli-manifest.json +491 -10
  10. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/parser.py +1 -0
  11. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli.egg-info/PKG-INFO +11 -1
  12. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli.egg-info/SOURCES.txt +1 -0
  13. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_shared/linkedin_contract.py +14 -8
  14. autotouch_cli-0.2.72/autotouch_shared/linkedin_filters.py +660 -0
  15. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_shared/provider_registry.py +3 -1
  16. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/pyproject.toml +1 -1
  17. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/MANIFEST.in +0 -0
  18. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/__init__.py +0 -0
  19. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/__init__.py +0 -0
  20. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/auth.py +0 -0
  21. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/cells.py +0 -0
  22. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/columns.py +0 -0
  23. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/jobs.py +0 -0
  24. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/leads.py +0 -0
  25. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/prompts.py +0 -0
  26. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/rows.py +0 -0
  27. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/search.py +0 -0
  28. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/sequences.py +0 -0
  29. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/tables.py +0 -0
  30. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/tasks.py +0 -0
  31. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/webhooks.py +0 -0
  32. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/commands/workspace_secrets.py +0 -0
  33. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/__init__.py +0 -0
  34. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/auth.py +0 -0
  35. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/config.py +0 -0
  36. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/csv_import.py +0 -0
  37. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/io.py +0 -0
  38. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/output.py +0 -0
  39. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/polling.py +0 -0
  40. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/run.py +0 -0
  41. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/core/validation.py +0 -0
  42. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/exceptions.py +0 -0
  43. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/mongo_status.py +0 -0
  44. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/parser_groups.py +0 -0
  45. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/sequence_support.py +0 -0
  46. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli/templates.py +0 -0
  47. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  48. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli.egg-info/entry_points.txt +0 -0
  49. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli.egg-info/requires.txt +0 -0
  50. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_cli.egg-info/top_level.txt +0 -0
  51. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_shared/__init__.py +0 -0
  52. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/autotouch_shared/search_contract.py +0 -0
  53. {autotouch_cli-0.2.70 → autotouch_cli-0.2.72}/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.72
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,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
+ - Run one LinkedIn search page/debug replay: `autotouch linkedin search`
136
138
  - Run controlled slices: `autotouch columns run-next`
137
139
  - Poll authoritative state: `autotouch jobs get`
138
140
  - Create/activate sequences: `autotouch sequences recipe`, `autotouch sequences create`, `autotouch sequences activate`
@@ -145,10 +147,17 @@ For automation or agent-driven setup, use:
145
147
  - `autotouch cli-manifest --output json` for the local machine-readable command contract
146
148
  - `autotouch cli-reference` for the shipped parser-generated reference
147
149
  - `autotouch capabilities --output json` for provider/workflow contracts
150
+ - `autotouch linkedin filters --output json` for LinkedIn/Sales Navigator filter tokens such as headcount, industry, location, role, function, and seniority
148
151
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
149
152
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
150
153
  - `pip install 'autotouch-cli[mongo]'` if you need the Mongo-backed `status` / `watch` commands
151
154
 
155
+ ## Realtime Table Updates
156
+
157
+ 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.
158
+
159
+ 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`.
160
+
152
161
  ## LLM Columns
153
162
 
154
163
  For `llm_enrichment` in `agent` mode, the recommended path is:
@@ -175,3 +184,4 @@ Prompt variables in authored prompts support nested JSON access:
175
184
  - Agent playbook: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/guides/autotouch-cli-agent-playbook.md
176
185
  - Tables/API reference: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/reference/tables-api.md
177
186
  - Authentication/scopes: https://github.com/nicolonic/autotouch_main/blob/main/docs/platform/authentication.md
187
+ - Instantly/Smartlead external sending accounts: https://github.com/nicolonic/autotouch_main/blob/main/docs/integrations/email-automation-platforms.md
@@ -108,6 +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
+ - Run one LinkedIn search page/debug replay: `autotouch linkedin search`
111
113
  - Run controlled slices: `autotouch columns run-next`
112
114
  - Poll authoritative state: `autotouch jobs get`
113
115
  - Create/activate sequences: `autotouch sequences recipe`, `autotouch sequences create`, `autotouch sequences activate`
@@ -120,10 +122,17 @@ For automation or agent-driven setup, use:
120
122
  - `autotouch cli-manifest --output json` for the local machine-readable command contract
121
123
  - `autotouch cli-reference` for the shipped parser-generated reference
122
124
  - `autotouch capabilities --output json` for provider/workflow contracts
125
+ - `autotouch linkedin filters --output json` for LinkedIn/Sales Navigator filter tokens such as headcount, industry, location, role, function, and seniority
123
126
  - `autotouch rows list` / `autotouch rows get` / `autotouch cells get` for read-back and audit
124
127
  - `autotouch sequences ...` and `autotouch tasks ...` for sequence/task workflows
125
128
  - `pip install 'autotouch-cli[mongo]'` if you need the Mongo-backed `status` / `watch` commands
126
129
 
130
+ ## Realtime Table Updates
131
+
132
+ 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.
133
+
134
+ 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`.
135
+
127
136
  ## LLM Columns
128
137
 
129
138
  For `llm_enrichment` in `agent` mode, the recommended path is:
@@ -150,3 +159,4 @@ Prompt variables in authored prompts support nested JSON access:
150
159
  - Agent playbook: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/guides/autotouch-cli-agent-playbook.md
151
160
  - Tables/API reference: https://github.com/nicolonic/autotouch_main/blob/main/docs/research-table/reference/tables-api.md
152
161
  - Authentication/scopes: https://github.com/nicolonic/autotouch_main/blob/main/docs/platform/authentication.md
162
+ - Instantly/Smartlead external sending accounts: https://github.com/nicolonic/autotouch_main/blob/main/docs/integrations/email-automation-platforms.md
@@ -93,6 +93,7 @@ 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,
97
98
  cmd_linkedin_post as cmd_linkedin_post_impl,
98
99
  cmd_linkedin_posts as cmd_linkedin_posts_impl,
@@ -1544,6 +1545,7 @@ _register("linkedin_status", cmd_linkedin_status_impl, _linkedin_command_runtime
1544
1545
  _register("linkedin_limits", cmd_linkedin_limits_impl, _linkedin_command_runtime)
1545
1546
  _register("linkedin_search", cmd_linkedin_search_impl, _linkedin_command_runtime)
1546
1547
  _register("linkedin_search_params", cmd_linkedin_search_params_impl, _linkedin_command_runtime)
1548
+ _register("linkedin_filters", cmd_linkedin_filters_impl, _linkedin_command_runtime)
1547
1549
  _register("linkedin_posts", cmd_linkedin_posts_impl, _linkedin_command_runtime)
1548
1550
  _register("linkedin_post", cmd_linkedin_post_impl, _linkedin_command_runtime)
1549
1551
  _register("linkedin_comments", cmd_linkedin_comments_impl, _linkedin_command_runtime)
@@ -1962,6 +1964,9 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
1962
1964
  print("Not for :")
1963
1965
  for item in not_for:
1964
1966
  print(f" - {item}")
1967
+ filter_count = len((linkedin.get("filter_catalog") or {}).get("filters") or [])
1968
+ if filter_count:
1969
+ print(f"Structured filters: {filter_count} exposed via `autotouch linkedin filters`")
1965
1970
  print("Tip: run `autotouch linkedin recipe` for example payloads.")
1966
1971
  if isinstance(search, dict):
1967
1972
  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"])
@@ -5,6 +5,7 @@ import json
5
5
  from dataclasses import dataclass
6
6
  from typing import Any, Callable, Dict, Optional, Sequence
7
7
 
8
+ from autotouch_shared.linkedin_filters import linkedin_filter_catalog
8
9
  from autotouch_cli.exceptions import AutotouchInputError
9
10
 
10
11
 
@@ -66,8 +67,8 @@ def _normalize_linkedin_search_payload(args: argparse.Namespace, *, runtime: Lin
66
67
  keywords = str(getattr(args, "keywords", "") or "").strip()
67
68
  if keywords:
68
69
  payload["keywords"] = keywords
69
- linkedin_api = str(getattr(args, "linkedin_api", "classic") or "classic").strip()
70
- if linkedin_api:
70
+ linkedin_api = str(getattr(args, "linkedin_api", "auto") or "auto").strip()
71
+ if linkedin_api and linkedin_api != "auto":
71
72
  payload["linkedin_api"] = linkedin_api
72
73
  cursor = str(getattr(args, "cursor", "") or "").strip()
73
74
  if cursor:
@@ -101,6 +102,9 @@ def cmd_linkedin_search_params(args: argparse.Namespace, *, runtime: LinkedInCom
101
102
  "keywords": str(getattr(args, "keywords", "") or "").strip(),
102
103
  "limit": max(1, min(int(getattr(args, "limit", 25) or 25), 250)),
103
104
  }
105
+ linkedin_api = str(getattr(args, "linkedin_api", "auto") or "auto").strip()
106
+ if linkedin_api and linkedin_api != "auto":
107
+ payload["linkedin_api"] = linkedin_api
104
108
  data = runtime.request_api(
105
109
  "POST",
106
110
  "/api/integrations/linkedin/search/parameters",
@@ -114,6 +118,19 @@ def cmd_linkedin_search_params(args: argparse.Namespace, *, runtime: LinkedInCom
114
118
  runtime.print_json(data, args.compact)
115
119
 
116
120
 
121
+ def cmd_linkedin_filters(args: argparse.Namespace, *, runtime: LinkedInCommandRuntime) -> None:
122
+ output = linkedin_filter_catalog(
123
+ api_mode=getattr(args, "api", None),
124
+ category=getattr(args, "category", None),
125
+ field=getattr(args, "field", None),
126
+ )
127
+ out_file = str(getattr(args, "out_file", "") or "").strip()
128
+ if out_file:
129
+ with open(out_file, "w", encoding="utf-8") as f:
130
+ json.dump(output, f, indent=2)
131
+ runtime.print_json(output, args.compact)
132
+
133
+
117
134
  def cmd_linkedin_recipe(
118
135
  args: argparse.Namespace,
119
136
  *,
@@ -253,7 +270,7 @@ def register_linkedin_subcommands(
253
270
  search_parser.add_argument("--url", help="Full LinkedIn/Sales Navigator search URL (overrides keyword filters)")
254
271
  search_parser.add_argument("--category", choices=["people", "companies", "jobs", "posts"], default="people", help="Search category (default: people)")
255
272
  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)")
273
+ search_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help="API mode (default: auto; uses connected account mode)")
257
274
  search_parser.add_argument("--cursor", help="Pagination cursor from previous response")
258
275
  search_parser.add_argument("--limit", type=int, default=25, help="Results per page, 1-250 (default: 25)")
259
276
  search_parser.add_argument("--extra-params-json", help="JSON object with additional search filters (location IDs, industry, etc.)")
@@ -263,10 +280,19 @@ def register_linkedin_subcommands(
263
280
  params_parser = linkedin_sub.add_parser("search-params", help="Resolve names to LinkedIn parameter IDs for search filters")
264
281
  params_parser.add_argument("--type", required=True, help="Parameter type: location, industry, company, school, title, language, etc.")
265
282
  params_parser.add_argument("--keywords", default="", help="Search text to match")
283
+ params_parser.add_argument("--linkedin-api", choices=["auto", "classic", "sales_navigator"], default="auto", help="Resolver service (default: auto; uses connected account mode)")
266
284
  params_parser.add_argument("--limit", type=int, default=25, help="Max results, 1-250 (default: 25)")
267
285
  add_api_common_arguments(params_parser)
268
286
  params_parser.set_defaults(func=handlers["search_params"])
269
287
 
288
+ filters_parser = linkedin_sub.add_parser("filters", help="Print the LinkedIn/Sales Navigator structured filter catalog")
289
+ filters_parser.add_argument("--api", choices=["all", "classic", "sales_navigator"], default="all", help="Filter by API mode")
290
+ filters_parser.add_argument("--category", choices=["all", "people", "companies", "jobs", "posts"], default="all", help="Filter by search category")
291
+ filters_parser.add_argument("--field", help="Show one filter field or alias, e.g. headcount, company_headcount, role")
292
+ filters_parser.add_argument("--out-file", help="Optional path to save the selected catalog JSON")
293
+ add_api_common_arguments(filters_parser)
294
+ filters_parser.set_defaults(func=handlers["filters"])
295
+
270
296
  posts_parser = linkedin_sub.add_parser("posts", help="List LinkedIn posts for a user")
271
297
  posts_parser.add_argument("--identifier", required=True, help="LinkedIn profile URL, public ID, or URN")
272
298
  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)