autotouch-cli 0.2.59__tar.gz → 0.2.61__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 (51) hide show
  1. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/PKG-INFO +1 -1
  2. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/columns.py +16 -1
  3. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/validation.py +62 -6
  4. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/data/CLI_REFERENCE.md +15 -1
  5. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/data/cli-manifest.json +3 -3
  6. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/templates.py +6 -6
  7. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/PKG-INFO +1 -1
  8. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/provider_registry.py +79 -13
  9. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/pyproject.toml +1 -1
  10. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/MANIFEST.in +0 -0
  11. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/README.md +0 -0
  12. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/__init__.py +0 -0
  13. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/cli.py +0 -0
  14. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/cli_contracts.py +0 -0
  15. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/__init__.py +0 -0
  16. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/auth.py +0 -0
  17. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/cells.py +0 -0
  18. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/jobs.py +0 -0
  19. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/leads.py +0 -0
  20. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/linkedin.py +0 -0
  21. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/prompts.py +0 -0
  22. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/rows.py +0 -0
  23. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/search.py +0 -0
  24. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/sequences.py +0 -0
  25. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/tables.py +0 -0
  26. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/tasks.py +0 -0
  27. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/webhooks.py +0 -0
  28. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/workspace_secrets.py +0 -0
  29. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/__init__.py +0 -0
  30. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/auth.py +0 -0
  31. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/config.py +0 -0
  32. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/csv_import.py +0 -0
  33. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/http.py +0 -0
  34. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/io.py +0 -0
  35. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/output.py +0 -0
  36. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/polling.py +0 -0
  37. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/run.py +0 -0
  38. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/exceptions.py +0 -0
  39. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/mongo_status.py +0 -0
  40. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/parser.py +0 -0
  41. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/parser_groups.py +0 -0
  42. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/sequence_support.py +0 -0
  43. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/SOURCES.txt +0 -0
  44. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  45. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/entry_points.txt +0 -0
  46. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/requires.txt +0 -0
  47. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/top_level.txt +0 -0
  48. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/__init__.py +0 -0
  49. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/linkedin_contract.py +0 -0
  50. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/search_contract.py +0 -0
  51. {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.59
3
+ Version: 0.2.61
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
@@ -379,7 +379,22 @@ def register_columns_subcommands(
379
379
  add_api_common_arguments(pcl)
380
380
  pcl.set_defaults(func=handlers["list"])
381
381
 
382
- pcc = col_sub.add_parser("create", help="Create a column")
382
+ pcc = col_sub.add_parser(
383
+ "create",
384
+ help="Create a column",
385
+ description=(
386
+ "Create a column from a ColumnCreate payload.\n"
387
+ "For llm_enrichment, there are two valid authoring paths:\n"
388
+ "- generated path: send config.instructions and let the API compile config.advancedPrompt\n"
389
+ "- manual path: send config.advancedPrompt directly when you want exact prompt control"
390
+ ),
391
+ epilog=(
392
+ "Recommended flow for llm_enrichment:\n\n"
393
+ "autotouch columns recipe --type llm_enrichment --output human\n\n"
394
+ "The runnable prompt is always config.advancedPrompt before execution."
395
+ ),
396
+ formatter_class=argparse.RawDescriptionHelpFormatter,
397
+ )
383
398
  pcc.add_argument("--table-id", required=True)
384
399
  pcc.add_argument("--data-json", help="ColumnCreate payload JSON")
385
400
  pcc.add_argument("--data-file", help="ColumnCreate payload file path")
@@ -55,8 +55,18 @@ def extract_add_to_crm_field_mappings(payload: Any) -> Optional[Dict[str, Any]]:
55
55
  return {}
56
56
 
57
57
 
58
- def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[str, Any]:
59
- mode = "multi" if str(field_mappings.get("mode") or "").strip().lower() == "multi" else "single"
58
+ def inspect_add_to_crm_field_mappings(
59
+ field_mappings: Dict[str, Any],
60
+ *,
61
+ require_company_domain: bool = True,
62
+ ) -> Dict[str, Any]:
63
+ raw_mode = str(field_mappings.get("mode") or "").strip().lower()
64
+ if raw_mode == "array":
65
+ mode = "array"
66
+ elif raw_mode == "multi":
67
+ mode = "multi"
68
+ else:
69
+ mode = "single"
60
70
  required_missing: List[str] = []
61
71
  optional_missing: List[str] = []
62
72
 
@@ -75,12 +85,20 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
75
85
  return True
76
86
  return False
77
87
 
88
+ if not require_company_domain and mode != "single":
89
+ required_missing.append("singleModeOnlyWhenCompanyDomainOptional")
90
+ return {
91
+ "mode": mode,
92
+ "required_missing": required_missing,
93
+ "optional_missing": optional_missing,
94
+ }
95
+
78
96
  if mode == "multi":
79
97
  common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
80
98
  leads = field_mappings.get("leads") if isinstance(field_mappings.get("leads"), list) else []
81
99
 
82
100
  common_domain = str((common or {}).get("companyDomain") or field_mappings.get("companyDomain") or "").strip()
83
- if not common_domain:
101
+ if require_company_domain and not common_domain:
84
102
  required_missing.append("common.companyDomain")
85
103
 
86
104
  has_lead_identity = False
@@ -95,10 +113,24 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
95
113
  optional_missing.append(f"leads[{idx}].{slot}")
96
114
  if not has_lead_identity:
97
115
  required_missing.append("leads[].identityFields")
116
+ elif mode == "array":
117
+ common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
118
+ template = field_mappings.get("template") if isinstance(field_mappings.get("template"), dict) else {}
119
+ if require_company_domain and not str((common or {}).get("companyDomain") or "").strip():
120
+ required_missing.append("common.companyDomain")
121
+ if not str(field_mappings.get("sourceColumn") or "").strip():
122
+ required_missing.append("sourceColumn")
123
+ if not _has_identity_mapping(template):
124
+ required_missing.append("template.identityFields")
98
125
  else:
99
126
  company_domain_key = str(field_mappings.get("companyDomain") or "").strip()
100
- if not company_domain_key:
127
+ if require_company_domain and not company_domain_key:
101
128
  required_missing.append("companyDomain")
129
+ if require_company_domain:
130
+ if not _has_identity_mapping(field_mappings):
131
+ required_missing.append("identityFields")
132
+ elif not str(field_mappings.get("linkedinUrl") or "").strip():
133
+ required_missing.append("linkedinUrl")
102
134
  if not _has_identity_mapping(field_mappings):
103
135
  required_missing.append("identityFields")
104
136
  for slot in ["firstName", "lastName", "title", "companyName"]:
@@ -116,6 +148,7 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
116
148
  field_mappings = extract_add_to_crm_field_mappings(payload)
117
149
  if field_mappings is None:
118
150
  return
151
+ config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
119
152
 
120
153
  kind_value = str(payload.get("kind") or "").strip().lower()
121
154
  if kind_value != "enrichment":
@@ -130,10 +163,24 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
130
163
  "Tip: run `autotouch columns recipe --type add_to_crm`."
131
164
  )
132
165
 
133
- issues = inspect_add_to_crm_field_mappings(field_mappings)
166
+ require_company_domain = config.get("requireCompanyDomain", True)
167
+ if not isinstance(require_company_domain, bool):
168
+ raise AutotouchInputError(
169
+ "add_to_crm config.requireCompanyDomain must be a boolean when provided."
170
+ )
171
+
172
+ issues = inspect_add_to_crm_field_mappings(
173
+ field_mappings,
174
+ require_company_domain=require_company_domain,
175
+ )
134
176
  required_missing = issues.get("required_missing") or []
135
177
  optional_missing = issues.get("optional_missing") or []
136
178
  if required_missing:
179
+ if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
180
+ raise AutotouchInputError(
181
+ "add_to_crm with config.requireCompanyDomain=false only supports single-mode fieldMappings. "
182
+ "Tip: map config.fieldMappings.linkedinUrl and keep mode='single' for LinkedIn-First Sync to Leads."
183
+ )
137
184
  missing = ", ".join(str(v) for v in required_missing)
138
185
  raise AutotouchInputError(
139
186
  f"add_to_crm fieldMappings missing required keys: {missing}. "
@@ -301,11 +348,20 @@ def run_precheck_for_add_to_crm(
301
348
  if field_mappings is None:
302
349
  return
303
350
 
304
- issues = inspect_add_to_crm_field_mappings(field_mappings)
351
+ config = column.get("config") if isinstance(column.get("config"), dict) else {}
352
+ require_company_domain = config.get("requireCompanyDomain", True)
353
+ issues = inspect_add_to_crm_field_mappings(
354
+ field_mappings,
355
+ require_company_domain=bool(require_company_domain) if isinstance(require_company_domain, bool) else True,
356
+ )
305
357
  required_missing = issues.get("required_missing") or []
306
358
  optional_missing = issues.get("optional_missing") or []
307
359
 
308
360
  if required_missing:
361
+ if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
362
+ raise AutotouchInputError(
363
+ "add_to_crm column is misconfigured: config.requireCompanyDomain=false only supports single-mode fieldMappings for LinkedIn-First Sync to Leads."
364
+ )
309
365
  missing = ", ".join(str(v) for v in required_missing)
310
366
  raise AutotouchInputError(
311
367
  f"add_to_crm column is misconfigured (missing required mappings: {missing}). "
@@ -1,6 +1,6 @@
1
1
  # Autotouch CLI Reference
2
2
 
3
- Generated from the installed parser for `autotouch-cli` `0.2.59`.
3
+ Generated from the installed parser for `autotouch-cli` `0.2.61`.
4
4
  Manifest schema version: `2`.
5
5
 
6
6
  ## Output Modes
@@ -653,6 +653,11 @@ Column operations
653
653
 
654
654
  Create a column
655
655
 
656
+ Create a column from a ColumnCreate payload.
657
+ For llm_enrichment, there are two valid authoring paths:
658
+ - generated path: send config.instructions and let the API compile config.advancedPrompt
659
+ - manual path: send config.advancedPrompt directly when you want exact prompt control
660
+
656
661
  - Auth: `developer_key_or_user_session`
657
662
  - Stability: `stable`
658
663
  - Destructive: `no`
@@ -669,6 +674,15 @@ Create a column
669
674
  [--output {json,ndjson,human}] [--compact]
670
675
  [--select SELECT |
671
676
  --json-pointer JSON_POINTER] [--verbose]`
677
+ - Notes:
678
+
679
+ ```text
680
+ Recommended flow for llm_enrichment:
681
+
682
+ autotouch columns recipe --type llm_enrichment --output human
683
+
684
+ The runnable prompt is always config.advancedPrompt before execution.
685
+ ```
672
686
  - Options:
673
687
  - `--table-id` (required; kind=string)
674
688
  - `--data-json` (kind=json; input=json): ColumnCreate payload JSON
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.59",
2
+ "version": "0.2.61",
3
3
  "manifest_schema_version": 2,
4
4
  "entry_points": {
5
5
  "autotouch": "autotouch_cli.cli:main",
@@ -14777,8 +14777,8 @@
14777
14777
  "aliases": [],
14778
14778
  "group": "columns",
14779
14779
  "help": "Create a column",
14780
- "description": null,
14781
- "notes": null,
14780
+ "description": "Create a column from a ColumnCreate payload.\nFor llm_enrichment, there are two valid authoring paths:\n- generated path: send config.instructions and let the API compile config.advancedPrompt\n- manual path: send config.advancedPrompt directly when you want exact prompt control",
14781
+ "notes": "Recommended flow for llm_enrichment:\n\nautotouch columns recipe --type llm_enrichment --output human\n\nThe runnable prompt is always config.advancedPrompt before execution.",
14782
14782
  "required_flags": [
14783
14783
  "--table-id"
14784
14784
  ],
@@ -35,11 +35,11 @@ COLUMN_RECIPE_BASE_TYPES = tuple(recipe_types())
35
35
  COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
36
36
  "add_to_leads": {
37
37
  "canonical": "add_to_crm",
38
- "key": "add_to_leads",
39
- "label": "Add to Leads",
38
+ "key": "sync_to_leads",
39
+ "label": "Sync to Leads",
40
40
  "notes": [
41
- "CLI alias for the canonical add_to_crm provider recipe.",
42
- "Use this when the operator language is 'Add to Leads'. Internal provider/config names remain add_to_crm.",
41
+ "Legacy CLI alias for the canonical add_to_crm provider recipe.",
42
+ "Emits the standard Sync to Leads payload. Internal provider/config names remain add_to_crm.",
43
43
  ],
44
44
  },
45
45
  "sync_to_leads": {
@@ -47,8 +47,8 @@ COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
47
47
  "key": "sync_to_leads",
48
48
  "label": "Sync to Leads",
49
49
  "notes": [
50
- "CLI alias for the canonical add_to_crm provider recipe.",
51
- "Use this when the operator language is 'Sync to Leads'. Internal provider/config names remain add_to_crm.",
50
+ "Preferred CLI alias for the canonical add_to_crm provider recipe.",
51
+ "Emits the standard Sync to Leads payload. Internal provider/config names remain add_to_crm.",
52
52
  ],
53
53
  },
54
54
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.59
3
+ Version: 0.2.61
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,14 @@ def _mapping_text(value: Any) -> str:
158
158
  return _text(value)
159
159
 
160
160
 
161
+ def _llm_advanced_prompt_text(config: Dict[str, Any]) -> str:
162
+ return (
163
+ _text(config.get("advancedPrompt"))
164
+ or _text(config.get("advanced_prompt"))
165
+ or _text(config.get("prompt"))
166
+ )
167
+
168
+
161
169
  def _llm_provider_values() -> Tuple[str, ...]:
162
170
  return ("llm", "gemini", "xai", "openai", "anthropic", "groq")
163
171
 
@@ -378,13 +386,14 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
378
386
  },
379
387
  },
380
388
  recipe_notes=(
381
- "Recommended default for agent mode: provide instructions and let the API compile the runnable prompt.",
389
+ "The runnable prompt is always config.advancedPrompt before execution.",
390
+ "Generated path: provide instructions and let the API compile config.advancedPrompt for you.",
391
+ "Manual path: write config.advancedPrompt directly when you want exact control over the final prompt.",
382
392
  "When the platform generates the runnable prompt from instructions, keep useAutoSchema=true unless you intentionally want to own the schema yourself.",
383
393
  "Do not send user_schema/response_schema in that default generated-prompt path; prompt/schema drift is possible if you later change the generated prompt but keep a frozen custom schema.",
384
394
  "Basic is for extraction, scoring, or classification using fields already in the row. Agent is for tasks that may need outside lookup or web research.",
385
395
  "Create/update always materializes the runnable prompt into config.advancedPrompt before execution.",
386
396
  "Generated agent-mode prompts always include company/requester context.",
387
- "Basic mode stays manual-prompt-first; write the prompt directly from the user goal/preferences.",
388
397
  "Runtime only injects values for placeholders the prompt explicitly references.",
389
398
  "Manual/basic prompts should use explicit placeholders like {{company_name}}, {{domain}}, or {{user_value_proposition}} when those values are needed.",
390
399
  "Agent mode is JSON-oriented.",
@@ -746,20 +755,21 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
746
755
  ),
747
756
  ResearchTableProviderContract(
748
757
  key="add_to_crm",
749
- display_name="Add to Leads",
758
+ display_name="Sync to Leads",
750
759
  kind="enrichment",
751
760
  behavior_class="action_enrichment",
752
761
  provider_aliases=("add_to_crm",),
753
762
  recipe_type="add_to_crm",
754
763
  recipe_payload={
755
- "key": "add_to_leads",
756
- "label": "Add to Leads",
764
+ "key": "sync_to_leads",
765
+ "label": "Sync to Leads",
757
766
  "kind": "enrichment",
758
767
  "dataType": "json",
759
768
  "origin": "manual",
760
769
  "autoRun": "onSourceUpdate",
761
770
  "config": {
762
771
  "provider": "add_to_crm",
772
+ "requireCompanyDomain": True,
763
773
  "leadSource": "research_table_export",
764
774
  "fieldMappings": {
765
775
  "mode": "single",
@@ -786,18 +796,19 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
786
796
  },
787
797
  recipe_notes=(
788
798
  "companyDomain is required; LinkedIn is optional.",
799
+ "Set `config.requireCompanyDomain=false` only for single-mode LinkedIn-first syncs. In that relaxed mode, `linkedinUrl` becomes required and multi/array mode stays unsupported.",
789
800
  "Include at least one usable hard-identity mapping such as LinkedIn, email, or phone.",
790
801
  "If companyDomain is missing in the table, derive or enrich that domain column first.",
791
802
  "Primitive source columns map directly. If a mapped source column stores structured JSON, send an object with column + path, for example {\"column\": \"work_email\", \"path\": \"response\"} or {\"column\": \"mobile_phone\", \"path\": \"mobile_number\"}.",
792
803
  "For firstName and lastName, prefer authoritative structured source fields when available, for example {\"column\": \"linkedin_profile_lookup_raw\", \"path\": \"first_name\"} and {\"column\": \"linkedin_profile_lookup_raw\", \"path\": \"last_name\"}. Avoid mapping a combined full-name column directly unless that is an intentional fallback and you accept imperfect CRM data.",
793
- "Add to CRM is optional and non-billable.",
794
- "Creating add_to_crm after upstream email/phone/lead columns already finished does not replay those older source updates; run add_to_crm explicitly or rerun the upstream source column.",
804
+ "Sync to Leads is non-billable.",
805
+ "Creating Sync to Leads (`add_to_crm`) after upstream email/phone/lead columns already finished does not replay those older source updates; run Sync to Leads explicitly or rerun the upstream source column.",
795
806
  ),
796
807
  column_type={
797
808
  "type": "add_to_crm",
798
809
  "kind": "enrichment",
799
810
  "runnable": True,
800
- "description": "Add eligible mapped rows into Leads CRM from research-table data.",
811
+ "description": "Sync eligible mapped rows into Leads from research-table data.",
801
812
  "requirements": {
802
813
  "config_provider": "add_to_crm",
803
814
  "field_mappings": "required",
@@ -806,7 +817,7 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
806
817
  "billing": {
807
818
  "billable": False,
808
819
  "credits": 0,
809
- "notes": "Add to Leads export does not consume credits.",
820
+ "notes": "Sync to Leads does not consume credits.",
810
821
  },
811
822
  },
812
823
  setup_contract={
@@ -971,14 +982,14 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
971
982
  "config": {
972
983
  "provider": "add_to_sequence",
973
984
  "sequenceId": "<SEQUENCE_ID>",
974
- "sourceLeadColumn": "add_to_leads",
985
+ "sourceLeadColumn": "sync_to_leads",
975
986
  },
976
987
  },
977
988
  recipe_notes=(
978
- "sourceLeadColumn should reference the source column key that stores lead IDs (for example add_to_leads), not the provider name add_to_crm.",
989
+ "sourceLeadColumn should reference the source column key that stores lead IDs (for example sync_to_leads), not the provider name add_to_crm.",
979
990
  "Set sequenceId to the target workflow sequence ID.",
980
991
  "The target sequence must already be ACTIVE for real enrollment.",
981
- "Common tail order after contact rows or lead IDs exist is email_finder -> add_to_crm -> create/activate sequence -> add_to_sequence.",
992
+ "Common tail order after contact rows or lead IDs exist is email_finder -> Sync to Leads (`add_to_crm`) -> create/activate sequence -> add_to_sequence.",
982
993
  "Creating add_to_sequence after lead IDs already exist does not replay those older updates; run add_to_sequence explicitly or rerun the lead-id source column.",
983
994
  ),
984
995
  column_type={
@@ -1355,6 +1366,38 @@ def validate_column_provider_contract(
1355
1366
  )
1356
1367
  return contract
1357
1368
 
1369
+ if contract.key == "llm":
1370
+ prompt_source = _normalize(cfg.get("promptSource") or cfg.get("prompt_source"))
1371
+ instructions = _text(cfg.get("instructions"))
1372
+ advanced_prompt = _llm_advanced_prompt_text(cfg)
1373
+
1374
+ if prompt_source == "generated":
1375
+ if not instructions:
1376
+ raise ProviderContractValidationError(
1377
+ "missing_required_field",
1378
+ "Generated LLM prompt mode requires config.instructions.",
1379
+ hint="Use instructions for shorthand/high-level intent, or switch to config.promptSource='manual' and write config.advancedPrompt directly.",
1380
+ )
1381
+ return contract
1382
+
1383
+ if prompt_source == "manual":
1384
+ if not advanced_prompt:
1385
+ raise ProviderContractValidationError(
1386
+ "missing_required_field",
1387
+ "Manual LLM prompt mode requires config.advancedPrompt.",
1388
+ hint="Write config.advancedPrompt directly, or remove config.promptSource and supply config.instructions for the generated path.",
1389
+ )
1390
+ return contract
1391
+
1392
+ if instructions or advanced_prompt:
1393
+ return contract
1394
+
1395
+ raise ProviderContractValidationError(
1396
+ "missing_required_field",
1397
+ "LLM enrichment columns require either config.instructions or config.advancedPrompt.",
1398
+ hint="Use instructions for the generated path or write config.advancedPrompt directly for the manual path.",
1399
+ )
1400
+
1358
1401
  if contract.key == "add_to_sequence":
1359
1402
  if not _text(cfg.get("sequenceId")):
1360
1403
  raise ProviderContractValidationError(
@@ -1366,7 +1409,7 @@ def validate_column_provider_contract(
1366
1409
  raise ProviderContractValidationError(
1367
1410
  "missing_required_field",
1368
1411
  "add_to_sequence requires config.sourceLeadColumn.",
1369
- hint="Set sourceLeadColumn to the column key that stores lead IDs, usually add_to_leads.",
1412
+ hint="Set sourceLeadColumn to the column key that stores lead IDs, usually sync_to_leads.",
1370
1413
  )
1371
1414
  return contract
1372
1415
 
@@ -1378,6 +1421,29 @@ def validate_column_provider_contract(
1378
1421
  "add_to_crm requires config.fieldMappings.",
1379
1422
  hint="Use `autotouch columns recipe --type add_to_crm`.",
1380
1423
  )
1424
+ require_company_domain = cfg.get("requireCompanyDomain", True)
1425
+ if not isinstance(require_company_domain, bool):
1426
+ raise ProviderContractValidationError(
1427
+ "invalid_field",
1428
+ "add_to_crm config.requireCompanyDomain must be a boolean when provided.",
1429
+ hint="Use true for the default strict mode or false for single-mode LinkedIn-first sync.",
1430
+ detail={"requireCompanyDomain": require_company_domain},
1431
+ )
1432
+ if require_company_domain is False:
1433
+ mode = _normalize(field_mappings.get("mode")) or "single"
1434
+ if mode in {"multi", "array"}:
1435
+ raise ProviderContractValidationError(
1436
+ "invalid_field",
1437
+ "add_to_crm config.requireCompanyDomain=false only supports single-mode fieldMappings.",
1438
+ hint="Use single-mode sync_to_leads with config.fieldMappings.linkedinUrl when companyDomain is optional.",
1439
+ detail={"mode": field_mappings.get("mode")},
1440
+ )
1441
+ if not _mapping_text(field_mappings.get("linkedinUrl")):
1442
+ raise ProviderContractValidationError(
1443
+ "missing_required_field",
1444
+ "add_to_crm with config.requireCompanyDomain=false requires config.fieldMappings.linkedinUrl.",
1445
+ hint="Map a LinkedIn profile URL column when using LinkedIn-first sync_to_leads.",
1446
+ )
1381
1447
  return contract
1382
1448
 
1383
1449
  if contract.key == "sync_to_table":
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autotouch-cli"
7
- version = "0.2.59"
7
+ version = "0.2.61"
8
8
  description = "Autotouch Smart Table CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes