autotouch-cli 0.2.61__tar.gz → 0.2.64__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 (52) hide show
  1. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/PKG-INFO +6 -1
  2. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/README.md +5 -0
  3. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/cli.py +40 -0
  4. autotouch_cli-0.2.64/autotouch_cli/commands/agents.py +465 -0
  5. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/data/cli-manifest.json +2538 -1
  6. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/parser.py +21 -0
  7. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli.egg-info/PKG-INFO +6 -1
  8. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli.egg-info/SOURCES.txt +1 -0
  9. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_shared/provider_registry.py +10 -10
  10. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/pyproject.toml +1 -1
  11. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/MANIFEST.in +0 -0
  12. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/__init__.py +0 -0
  13. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/cli_contracts.py +0 -0
  14. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/__init__.py +0 -0
  15. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/auth.py +0 -0
  16. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/cells.py +0 -0
  17. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/columns.py +0 -0
  18. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/jobs.py +0 -0
  19. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/leads.py +0 -0
  20. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/linkedin.py +0 -0
  21. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/prompts.py +0 -0
  22. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/rows.py +0 -0
  23. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/search.py +0 -0
  24. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/sequences.py +0 -0
  25. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/tables.py +0 -0
  26. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/tasks.py +0 -0
  27. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/webhooks.py +0 -0
  28. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/commands/workspace_secrets.py +0 -0
  29. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/__init__.py +0 -0
  30. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/auth.py +0 -0
  31. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/config.py +0 -0
  32. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/csv_import.py +0 -0
  33. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/http.py +0 -0
  34. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/io.py +0 -0
  35. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/output.py +0 -0
  36. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/polling.py +0 -0
  37. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/run.py +0 -0
  38. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/core/validation.py +0 -0
  39. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/data/CLI_REFERENCE.md +0 -0
  40. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/exceptions.py +0 -0
  41. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/mongo_status.py +0 -0
  42. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/parser_groups.py +0 -0
  43. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/sequence_support.py +0 -0
  44. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli/templates.py +0 -0
  45. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  46. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli.egg-info/entry_points.txt +0 -0
  47. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli.egg-info/requires.txt +0 -0
  48. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_cli.egg-info/top_level.txt +0 -0
  49. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_shared/__init__.py +0 -0
  50. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_shared/linkedin_contract.py +0 -0
  51. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/autotouch_shared/search_contract.py +0 -0
  52. {autotouch_cli-0.2.61 → autotouch_cli-0.2.64}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.61
3
+ Version: 0.2.64
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
@@ -158,6 +158,11 @@ For `llm_enrichment` in `agent` mode, the recommended path is:
158
158
 
159
159
  Only send `user_schema` / `response_schema` when you intentionally want to override the generated schema and keep it aligned yourself. The installed recipe surface at `autotouch columns recipe --type llm_enrichment` follows this contract.
160
160
 
161
+ Schema ownership rules:
162
+ - Accepted generated schemas and explicit user schemas are the saved output contract.
163
+ - Row execution must not add fields, rename fields, or replace a valid locked schema.
164
+ - Agent-mode evidence/scored state decides which values may fill the schema; the finalizer formats those values and schema validation gates persistence.
165
+
161
166
  Prompt variables in authored prompts support nested JSON access:
162
167
  - Use flat row variables like `{{company_name}}` for scalar columns.
163
168
  - Use dotted placeholders like `{{linkedin_lookup.linkedin_url}}` when the source column stores JSON or stringified JSON.
@@ -133,6 +133,11 @@ For `llm_enrichment` in `agent` mode, the recommended path is:
133
133
 
134
134
  Only send `user_schema` / `response_schema` when you intentionally want to override the generated schema and keep it aligned yourself. The installed recipe surface at `autotouch columns recipe --type llm_enrichment` follows this contract.
135
135
 
136
+ Schema ownership rules:
137
+ - Accepted generated schemas and explicit user schemas are the saved output contract.
138
+ - Row execution must not add fields, rename fields, or replace a valid locked schema.
139
+ - Agent-mode evidence/scored state decides which values may fill the schema; the finalizer formats those values and schema validation gates persistence.
140
+
136
141
  Prompt variables in authored prompts support nested JSON access:
137
142
  - Use flat row variables like `{{company_name}}` for scalar columns.
138
143
  - Use dotted placeholders like `{{linkedin_lookup.linkedin_url}}` when the source column stores JSON or stringified JSON.
@@ -125,6 +125,21 @@ from autotouch_cli.commands.search import (
125
125
  cmd_search_reviews as cmd_search_reviews_impl,
126
126
  cmd_search_similar_companies as cmd_search_similar_companies_impl,
127
127
  )
128
+ from autotouch_cli.commands.agents import (
129
+ AgentCommandRuntime as AgentCommandHandlerRuntime,
130
+ cmd_agents_list as cmd_agents_list_impl,
131
+ cmd_agents_get as cmd_agents_get_impl,
132
+ cmd_agents_create as cmd_agents_create_impl,
133
+ cmd_agents_update as cmd_agents_update_impl,
134
+ cmd_agents_delete as cmd_agents_delete_impl,
135
+ cmd_agents_run as cmd_agents_run_impl,
136
+ cmd_agents_runs as cmd_agents_runs_impl,
137
+ cmd_agents_watch as cmd_agents_watch_impl,
138
+ cmd_agents_generate_persona as cmd_agents_generate_persona_impl,
139
+ cmd_agents_generate_topics as cmd_agents_generate_topics_impl,
140
+ cmd_agents_generate_job_signal as cmd_agents_generate_job_signal_impl,
141
+ cmd_agents_signals as cmd_agents_signals_impl,
142
+ )
128
143
  from autotouch_cli.commands.sequences import (
129
144
  SequenceCommandRuntime as SequenceCommandHandlerRuntime,
130
145
  cmd_sequences_clone as cmd_sequences_clone_impl,
@@ -1164,6 +1179,17 @@ def _sequence_command_runtime() -> SequenceCommandHandlerRuntime:
1164
1179
  )
1165
1180
 
1166
1181
 
1182
+ def _agent_command_runtime() -> AgentCommandHandlerRuntime:
1183
+ return AgentCommandHandlerRuntime(
1184
+ resolve_token=_resolve_token,
1185
+ request_api=_request_api,
1186
+ print_json=lambda data, compact=False: _print_json(data, compact=compact),
1187
+ load_json_input=_load_json_input,
1188
+ api_url=_api_url,
1189
+ default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
1190
+ )
1191
+
1192
+
1167
1193
  def _column_command_runtime() -> ColumnCommandHandlerRuntime:
1168
1194
  return ColumnCommandHandlerRuntime(
1169
1195
  resolve_token=_resolve_token,
@@ -1420,6 +1446,20 @@ _register("columns_run_next", cmd_columns_run_next_impl, _column_command_runtime
1420
1446
  _register("columns_stop", cmd_columns_stop_impl, _column_command_runtime)
1421
1447
  _register("columns_estimate", cmd_columns_estimate_impl, _column_command_runtime)
1422
1448
 
1449
+ # -- Agent commands --
1450
+ _register("agents_list", cmd_agents_list_impl, _agent_command_runtime)
1451
+ _register("agents_get", cmd_agents_get_impl, _agent_command_runtime)
1452
+ _register("agents_create", cmd_agents_create_impl, _agent_command_runtime)
1453
+ _register("agents_update", cmd_agents_update_impl, _agent_command_runtime)
1454
+ _register("agents_delete", cmd_agents_delete_impl, _agent_command_runtime)
1455
+ _register("agents_run", cmd_agents_run_impl, _agent_command_runtime)
1456
+ _register("agents_runs", cmd_agents_runs_impl, _agent_command_runtime)
1457
+ _register("agents_watch", cmd_agents_watch_impl, _agent_command_runtime)
1458
+ _register("agents_generate_persona", cmd_agents_generate_persona_impl, _agent_command_runtime)
1459
+ _register("agents_generate_topics", cmd_agents_generate_topics_impl, _agent_command_runtime)
1460
+ _register("agents_generate_job_signal", cmd_agents_generate_job_signal_impl, _agent_command_runtime)
1461
+ _register("agents_signals", cmd_agents_signals_impl, _agent_command_runtime)
1462
+
1423
1463
  # -- Sequence commands --
1424
1464
  _register("sequences_list", cmd_sequences_list_impl, _sequence_command_runtime)
1425
1465
  _register("sequences_get", cmd_sequences_get_impl, _sequence_command_runtime)
@@ -0,0 +1,465 @@
1
+ """Scheduled agent CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, Dict, Optional, Sequence
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AgentCommandRuntime:
15
+ resolve_token: Callable[[Optional[str], bool], Optional[str]]
16
+ request_api: Callable[..., Any]
17
+ print_json: Callable[[Any, bool], None]
18
+ load_json_input: Callable[..., Any]
19
+ api_url: Callable[[Optional[str]], str]
20
+ default_timeout_seconds: int
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # CRUD
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def cmd_agents_list(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
28
+ token = runtime.resolve_token(args.token, required=True)
29
+ data = runtime.request_api(
30
+ "GET", "/api/agents",
31
+ base_url=args.base_url, token=token,
32
+ use_x_api_key=args.use_x_api_key,
33
+ timeout=args.timeout, verbose=args.verbose,
34
+ )
35
+ runtime.print_json(data, args.compact)
36
+
37
+
38
+ def cmd_agents_get(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
39
+ token = runtime.resolve_token(args.token, required=True)
40
+ data = runtime.request_api(
41
+ "GET", f"/api/agents/{args.agent_id}",
42
+ base_url=args.base_url, token=token,
43
+ use_x_api_key=args.use_x_api_key,
44
+ timeout=args.timeout, verbose=args.verbose,
45
+ )
46
+ runtime.print_json(data, args.compact)
47
+
48
+
49
+ def cmd_agents_create(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
50
+ token = runtime.resolve_token(args.token, required=True)
51
+
52
+ if getattr(args, "from_file", None):
53
+ payload = runtime.load_json_input(args.from_file)
54
+ else:
55
+ payload: Dict[str, Any] = {}
56
+ if args.name:
57
+ payload["name"] = args.name
58
+ if args.target:
59
+ payload["targetType"] = args.target
60
+ if args.assigned_user:
61
+ payload["assignedUserId"] = args.assigned_user
62
+ if args.schedule_time:
63
+ tz = getattr(args, "schedule_tz", None) or "UTC"
64
+ payload["schedule"] = {"timezone": tz, "timeOfDay": args.schedule_time}
65
+ if getattr(args, "auto_find_email", False):
66
+ payload["autoFindEmail"] = True
67
+ if getattr(args, "auto_find_phone", False):
68
+ payload["autoFindPhone"] = True
69
+ if getattr(args, "auto_broaden", False):
70
+ payload["autoBroadenSearch"] = True
71
+ status = getattr(args, "status", None)
72
+ if status:
73
+ payload["status"] = status
74
+
75
+ # AI generation: generate persona, signals, and create in one shot
76
+ if getattr(args, "generate", False) and "personaConfig" not in payload:
77
+ target = payload.get("targetType", "LEADS")
78
+ sys.stderr.write(f"Generating persona for {target}...\n")
79
+ persona_data = runtime.request_api(
80
+ "POST", "/api/agents/generate-persona",
81
+ base_url=args.base_url, token=token,
82
+ use_x_api_key=args.use_x_api_key,
83
+ json={"targetType": target},
84
+ timeout=args.timeout, verbose=args.verbose,
85
+ )
86
+ persona = persona_data.get("persona") or persona_data.get("personaConfig") or {}
87
+ payload["personaConfig"] = persona
88
+ sys.stderr.write(f" Persona generated: {json.dumps(persona, ensure_ascii=False)[:200]}...\n")
89
+
90
+ # Generate topics
91
+ sys.stderr.write("Generating topic suggestions...\n")
92
+ topic_data = runtime.request_api(
93
+ "POST", "/api/agents/generate-signal-topics",
94
+ base_url=args.base_url, token=token,
95
+ use_x_api_key=args.use_x_api_key,
96
+ json={"targetType": target, "personaConfig": persona},
97
+ timeout=args.timeout, verbose=args.verbose,
98
+ )
99
+ topics = topic_data.get("topics") or []
100
+ sys.stderr.write(f" Topics: {topics}\n")
101
+
102
+ # Generate job signal
103
+ sys.stderr.write("Generating job signal config...\n")
104
+ job_data = runtime.request_api(
105
+ "POST", "/api/agents/generate-job-signal",
106
+ base_url=args.base_url, token=token,
107
+ use_x_api_key=args.use_x_api_key,
108
+ json={"targetType": target, "personaConfig": persona},
109
+ timeout=args.timeout, verbose=args.verbose,
110
+ )
111
+ job_titles = job_data.get("jobTitles") or []
112
+ job_keywords = job_data.get("keywords") or []
113
+ sys.stderr.write(f" Job titles: {job_titles}\n")
114
+
115
+ # Assemble signals
116
+ signals = []
117
+ if topics:
118
+ signals.append({
119
+ "key": "linkedin-topic-post-engagement",
120
+ "config": {"topics": [{"id": "", "label": t, "trackingMode": "all"} for t in topics[:5]]},
121
+ })
122
+ if job_titles or job_keywords:
123
+ signals.append({
124
+ "key": "hiring-signals",
125
+ "config": {"jobTitles": job_titles, "keywords": job_keywords, "remoteOnly": False, "lookbackWindow": "7d"},
126
+ })
127
+
128
+ # News scan: derive buying trigger topics from persona
129
+ news_topics = ["raised funding", "new executive hire"]
130
+ industries = (persona.get("industries") or [])[:2]
131
+ if industries:
132
+ news_topics.append(f"expansion {industries[0]}")
133
+ signals.append({
134
+ "key": "news-scan",
135
+ "config": {"topics": news_topics, "lookback": "24h"},
136
+ })
137
+
138
+ if signals:
139
+ payload["selectedSignals"] = signals
140
+
141
+ data = runtime.request_api(
142
+ "POST", "/api/agents",
143
+ base_url=args.base_url, token=token,
144
+ use_x_api_key=args.use_x_api_key,
145
+ json=payload,
146
+ timeout=args.timeout, verbose=args.verbose,
147
+ )
148
+ runtime.print_json(data, args.compact)
149
+
150
+
151
+ def cmd_agents_update(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
152
+ token = runtime.resolve_token(args.token, required=True)
153
+
154
+ if getattr(args, "from_file", None):
155
+ payload = runtime.load_json_input(args.from_file)
156
+ else:
157
+ payload: Dict[str, Any] = {}
158
+ if getattr(args, "name", None):
159
+ payload["name"] = args.name
160
+ if getattr(args, "status", None):
161
+ payload["status"] = args.status
162
+ if getattr(args, "auto_find_email", None) is not None:
163
+ payload["autoFindEmail"] = args.auto_find_email
164
+ if getattr(args, "auto_find_phone", None) is not None:
165
+ payload["autoFindPhone"] = args.auto_find_phone
166
+ if getattr(args, "auto_broaden", None) is not None:
167
+ payload["autoBroadenSearch"] = args.auto_broaden
168
+
169
+ data = runtime.request_api(
170
+ "PATCH", f"/api/agents/{args.agent_id}",
171
+ base_url=args.base_url, token=token,
172
+ use_x_api_key=args.use_x_api_key,
173
+ json=payload,
174
+ timeout=args.timeout, verbose=args.verbose,
175
+ )
176
+ runtime.print_json(data, args.compact)
177
+
178
+
179
+ def cmd_agents_delete(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
180
+ token = runtime.resolve_token(args.token, required=True)
181
+ data = runtime.request_api(
182
+ "PATCH", f"/api/agents/{args.agent_id}",
183
+ base_url=args.base_url, token=token,
184
+ use_x_api_key=args.use_x_api_key,
185
+ json={"status": "DRAFT"},
186
+ timeout=args.timeout, verbose=args.verbose,
187
+ )
188
+ sys.stderr.write(f"Agent {args.agent_id} deleted.\n")
189
+ runtime.print_json(data, args.compact)
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Run management
194
+ # ---------------------------------------------------------------------------
195
+
196
+ def cmd_agents_run(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
197
+ token = runtime.resolve_token(args.token, required=True)
198
+ data = runtime.request_api(
199
+ "POST", f"/api/agents/{args.agent_id}/run-now",
200
+ base_url=args.base_url, token=token,
201
+ use_x_api_key=args.use_x_api_key,
202
+ timeout=args.timeout, verbose=args.verbose,
203
+ )
204
+ runtime.print_json(data, args.compact)
205
+
206
+
207
+ def cmd_agents_runs(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
208
+ token = runtime.resolve_token(args.token, required=True)
209
+ params = {"limit": max(1, int(getattr(args, "limit", 10) or 10))}
210
+ data = runtime.request_api(
211
+ "GET", f"/api/agents/{args.agent_id}/runs",
212
+ base_url=args.base_url, token=token,
213
+ use_x_api_key=args.use_x_api_key,
214
+ params=params,
215
+ timeout=args.timeout, verbose=args.verbose,
216
+ )
217
+ runtime.print_json(data, args.compact)
218
+
219
+
220
+ def cmd_agents_watch(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
221
+ """Trigger a run and poll until complete."""
222
+ token = runtime.resolve_token(args.token, required=True)
223
+
224
+ # Trigger
225
+ run_data = runtime.request_api(
226
+ "POST", f"/api/agents/{args.agent_id}/run-now",
227
+ base_url=args.base_url, token=token,
228
+ use_x_api_key=args.use_x_api_key,
229
+ timeout=args.timeout, verbose=args.verbose,
230
+ )
231
+ run_id = run_data.get("id", "")
232
+ sys.stderr.write(f"Run {run_id} queued. Watching...\n")
233
+
234
+ interval = max(5, int(getattr(args, "interval", 10) or 10))
235
+ max_wait = max(60, int(getattr(args, "max_wait", 1800) or 1800))
236
+ elapsed = 0
237
+
238
+ while elapsed < max_wait:
239
+ time.sleep(interval)
240
+ elapsed += interval
241
+
242
+ runs_data = runtime.request_api(
243
+ "GET", f"/api/agents/{args.agent_id}/runs",
244
+ base_url=args.base_url, token=token,
245
+ use_x_api_key=args.use_x_api_key,
246
+ params={"limit": 1},
247
+ timeout=args.timeout, verbose=args.verbose,
248
+ )
249
+ runs = runs_data.get("runs", [])
250
+ if not runs:
251
+ continue
252
+
253
+ latest = runs[0]
254
+ status = latest.get("status", "")
255
+ leads = latest.get("resultCounts", {}).get("leads", 0)
256
+ sys.stderr.write(f" [{elapsed}s] {status} | leads={leads}\n")
257
+
258
+ if status in ("SUCCEEDED", "FAILED"):
259
+ runtime.print_json(latest, args.compact)
260
+ return
261
+
262
+ sys.stderr.write(f"Timed out after {max_wait}s. Run may still be in progress.\n")
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # AI generation
267
+ # ---------------------------------------------------------------------------
268
+
269
+ def cmd_agents_generate_persona(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
270
+ token = runtime.resolve_token(args.token, required=True)
271
+ payload: Dict[str, Any] = {"targetType": getattr(args, "target", "LEADS") or "LEADS"}
272
+
273
+ current = getattr(args, "current_config", None)
274
+ if current:
275
+ payload["currentPersonaConfig"] = runtime.load_json_input(current)
276
+
277
+ data = runtime.request_api(
278
+ "POST", "/api/agents/generate-persona",
279
+ base_url=args.base_url, token=token,
280
+ use_x_api_key=args.use_x_api_key,
281
+ json=payload,
282
+ timeout=args.timeout, verbose=args.verbose,
283
+ )
284
+ runtime.print_json(data, args.compact)
285
+
286
+
287
+ def cmd_agents_generate_topics(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
288
+ token = runtime.resolve_token(args.token, required=True)
289
+ payload: Dict[str, Any] = {"targetType": getattr(args, "target", "LEADS") or "LEADS"}
290
+
291
+ persona = getattr(args, "persona", None)
292
+ if persona:
293
+ payload["personaConfig"] = runtime.load_json_input(persona)
294
+
295
+ current = getattr(args, "current_topics", None)
296
+ if current:
297
+ payload["currentTopics"] = [t.strip() for t in current.split(",") if t.strip()]
298
+
299
+ data = runtime.request_api(
300
+ "POST", "/api/agents/generate-signal-topics",
301
+ base_url=args.base_url, token=token,
302
+ use_x_api_key=args.use_x_api_key,
303
+ json=payload,
304
+ timeout=args.timeout, verbose=args.verbose,
305
+ )
306
+ runtime.print_json(data, args.compact)
307
+
308
+
309
+ def cmd_agents_generate_job_signal(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
310
+ token = runtime.resolve_token(args.token, required=True)
311
+ payload: Dict[str, Any] = {"targetType": getattr(args, "target", "LEADS") or "LEADS"}
312
+
313
+ persona = getattr(args, "persona", None)
314
+ if persona:
315
+ payload["personaConfig"] = runtime.load_json_input(persona)
316
+
317
+ current_titles = getattr(args, "current_titles", None)
318
+ if current_titles:
319
+ payload["currentJobTitles"] = [t.strip() for t in current_titles.split(",") if t.strip()]
320
+
321
+ current_keywords = getattr(args, "current_keywords", None)
322
+ if current_keywords:
323
+ payload["currentKeywords"] = [k.strip() for k in current_keywords.split(",") if k.strip()]
324
+
325
+ data = runtime.request_api(
326
+ "POST", "/api/agents/generate-job-signal",
327
+ base_url=args.base_url, token=token,
328
+ use_x_api_key=args.use_x_api_key,
329
+ json=payload,
330
+ timeout=args.timeout, verbose=args.verbose,
331
+ )
332
+ runtime.print_json(data, args.compact)
333
+
334
+
335
+ # ---------------------------------------------------------------------------
336
+ # Skill packs / signals
337
+ # ---------------------------------------------------------------------------
338
+
339
+ def cmd_agents_signals(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
340
+ token = runtime.resolve_token(args.token, required=True)
341
+ data = runtime.request_api(
342
+ "GET", "/api/agents/skill-packs",
343
+ base_url=args.base_url, token=token,
344
+ use_x_api_key=args.use_x_api_key,
345
+ timeout=args.timeout, verbose=args.verbose,
346
+ )
347
+ runtime.print_json(data, args.compact)
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # Parser registration
352
+ # ---------------------------------------------------------------------------
353
+
354
+ def register_agents_subcommands(
355
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
356
+ *,
357
+ add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
358
+ handlers: Dict[str, Callable[[argparse.Namespace], None]],
359
+ ) -> None:
360
+ pa = subparsers.add_parser("agents", help="Scheduled agent management")
361
+ agents_sub = pa.add_subparsers(dest="agents_cmd", required=True)
362
+
363
+ # -- list --
364
+ pal = agents_sub.add_parser("list", help="List all agents")
365
+ add_api_common_arguments(pal)
366
+ pal.set_defaults(func=handlers["list"])
367
+
368
+ # -- get --
369
+ pag = agents_sub.add_parser("get", help="Get one agent by id")
370
+ pag.add_argument("agent_id", help="Agent ID")
371
+ add_api_common_arguments(pag)
372
+ pag.set_defaults(func=handlers["get"])
373
+
374
+ # -- create --
375
+ pac = agents_sub.add_parser("create", help="Create a new agent")
376
+ pac.add_argument("--name", help="Agent name")
377
+ pac.add_argument("--target", choices=["LEADS", "COMPANIES"], help="Target type")
378
+ 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)")
380
+ pac.add_argument("--schedule-tz", dest="schedule_tz", help="Timezone (default: UTC)")
381
+ pac.add_argument("--auto-find-email", dest="auto_find_email", action="store_true", default=False, help="Enable auto email finder")
382
+ pac.add_argument("--auto-find-phone", dest="auto_find_phone", action="store_true", default=False, help="Enable auto phone finder")
383
+ pac.add_argument("--auto-broaden", dest="auto_broaden", action="store_true", default=False, help="Enable auto broaden search")
384
+ pac.add_argument("--status", choices=["DRAFT", "ACTIVE", "PAUSED"], help="Initial status")
385
+ pac.add_argument("--from-file", dest="from_file", help="Path to JSON payload file")
386
+ pac.add_argument("--generate", action="store_true", help="Auto-generate persona, topics, and job signal config")
387
+ add_api_common_arguments(pac)
388
+ pac.set_defaults(func=handlers["create"])
389
+
390
+ # -- update --
391
+ pau = agents_sub.add_parser("update", help="Update an existing agent")
392
+ pau.add_argument("agent_id", help="Agent ID")
393
+ pau.add_argument("--name", help="New agent name")
394
+ pau.add_argument("--status", choices=["DRAFT", "ACTIVE", "PAUSED"], help="New status")
395
+ pau.add_argument("--auto-find-email", dest="auto_find_email", type=_bool_arg, default=None, help="Enable/disable auto email finder")
396
+ pau.add_argument("--auto-find-phone", dest="auto_find_phone", type=_bool_arg, default=None, help="Enable/disable auto phone finder")
397
+ pau.add_argument("--auto-broaden", dest="auto_broaden", type=_bool_arg, default=None, help="Enable/disable auto broaden search")
398
+ pau.add_argument("--from-file", dest="from_file", help="Path to JSON patch payload file")
399
+ add_api_common_arguments(pau)
400
+ pau.set_defaults(func=handlers["update"])
401
+
402
+ # -- delete --
403
+ pad = agents_sub.add_parser("delete", help="Delete an agent (sets status to DRAFT)")
404
+ pad.add_argument("agent_id", help="Agent ID")
405
+ add_api_common_arguments(pad)
406
+ pad.set_defaults(func=handlers["delete"])
407
+
408
+ # -- run --
409
+ par = agents_sub.add_parser("run", help="Trigger an immediate run")
410
+ par.add_argument("agent_id", help="Agent ID")
411
+ add_api_common_arguments(par)
412
+ par.set_defaults(func=handlers["run"])
413
+
414
+ # -- runs --
415
+ pars = agents_sub.add_parser("runs", help="List recent runs for an agent")
416
+ pars.add_argument("agent_id", help="Agent ID")
417
+ pars.add_argument("--limit", type=int, default=10, help="Max runs to return (default: 10)")
418
+ add_api_common_arguments(pars)
419
+ pars.set_defaults(func=handlers["runs"])
420
+
421
+ # -- watch --
422
+ paw = agents_sub.add_parser("watch", help="Trigger a run and poll until complete")
423
+ paw.add_argument("agent_id", help="Agent ID")
424
+ paw.add_argument("--interval", type=int, default=10, help="Poll interval in seconds (default: 10)")
425
+ paw.add_argument("--max-wait", dest="max_wait", type=int, default=1800, help="Max wait in seconds (default: 1800)")
426
+ add_api_common_arguments(paw)
427
+ paw.set_defaults(func=handlers["watch"])
428
+
429
+ # -- generate-persona --
430
+ pagp = agents_sub.add_parser("generate-persona", help="Generate ICP persona config via AI")
431
+ pagp.add_argument("--target", choices=["LEADS", "COMPANIES"], default="LEADS", help="Target type")
432
+ pagp.add_argument("--current-config", dest="current_config", help="Path to current persona config JSON")
433
+ add_api_common_arguments(pagp)
434
+ pagp.set_defaults(func=handlers["generate_persona"])
435
+
436
+ # -- generate-topics --
437
+ pagt = agents_sub.add_parser("generate-topics", help="Generate signal topic suggestions via AI")
438
+ pagt.add_argument("--target", choices=["LEADS", "COMPANIES"], default="LEADS", help="Target type")
439
+ pagt.add_argument("--persona", help="Path to persona config JSON")
440
+ pagt.add_argument("--current-topics", dest="current_topics", help="Comma-separated current topics")
441
+ add_api_common_arguments(pagt)
442
+ pagt.set_defaults(func=handlers["generate_topics"])
443
+
444
+ # -- generate-job-signal --
445
+ pagj = agents_sub.add_parser("generate-job-signal", help="Generate job signal config via AI")
446
+ pagj.add_argument("--target", choices=["LEADS", "COMPANIES"], default="LEADS", help="Target type")
447
+ pagj.add_argument("--persona", help="Path to persona config JSON")
448
+ pagj.add_argument("--current-titles", dest="current_titles", help="Comma-separated current job titles")
449
+ pagj.add_argument("--current-keywords", dest="current_keywords", help="Comma-separated current keywords")
450
+ add_api_common_arguments(pagj)
451
+ pagj.set_defaults(func=handlers["generate_job_signal"])
452
+
453
+ # -- signals --
454
+ pass_ = agents_sub.add_parser("signals", help="List available signal skill packs")
455
+ add_api_common_arguments(pass_)
456
+ pass_.set_defaults(func=handlers["signals"])
457
+
458
+
459
+ def _bool_arg(v: str) -> bool:
460
+ """Parse boolean CLI argument values."""
461
+ if v.lower() in ("true", "1", "yes"):
462
+ return True
463
+ if v.lower() in ("false", "0", "no"):
464
+ return False
465
+ raise argparse.ArgumentTypeError(f"Expected true/false, got: {v}")