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.
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/PKG-INFO +14 -1
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/README.md +13 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/cli.py +11 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/cli_contracts.py +1 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/agents.py +160 -3
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/linkedin.py +215 -4
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/http.py +14 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/data/CLI_REFERENCE.md +585 -7
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/data/cli-manifest.json +1532 -235
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/parser.py +4 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/PKG-INFO +14 -1
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/SOURCES.txt +1 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/linkedin_contract.py +76 -9
- autotouch_cli-0.2.73/autotouch_shared/linkedin_filters.py +660 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/provider_registry.py +3 -1
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/pyproject.toml +1 -1
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/MANIFEST.in +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/__init__.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/__init__.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/auth.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/cells.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/columns.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/jobs.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/leads.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/prompts.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/rows.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/search.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/sequences.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/tables.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/tasks.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/webhooks.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/commands/workspace_secrets.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/__init__.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/auth.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/config.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/csv_import.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/io.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/output.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/polling.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/run.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/core/validation.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/exceptions.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/mongo_status.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/parser_groups.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/sequence_support.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli/templates.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_cli.egg-info/top_level.txt +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/__init__.py +0 -0
- {autotouch_cli-0.2.70 → autotouch_cli-0.2.73}/autotouch_shared/search_contract.py +0 -0
- {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.
|
|
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("")
|
|
@@ -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
|
-
|
|
64
|
-
|
|
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", "
|
|
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="
|
|
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)
|