autotouch-cli 0.2.30__tar.gz → 0.2.32__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.30/autotouch_cli.egg-info → autotouch_cli-0.2.32}/PKG-INFO +39 -4
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli/cli.py +168 -199
- autotouch_cli-0.2.30/docs/research-table/reference/autotouch-cli.md → autotouch_cli-0.2.32/autotouch_cli.egg-info/PKG-INFO +47 -3
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/SOURCES.txt +2 -0
- autotouch_cli-0.2.32/autotouch_cli.egg-info/top_level.txt +2 -0
- autotouch_cli-0.2.32/autotouch_shared/__init__.py +1 -0
- autotouch_cli-0.2.32/autotouch_shared/provider_registry.py +768 -0
- autotouch_cli-0.2.30/PKG-INFO → autotouch_cli-0.2.32/docs/research-table/reference/autotouch-cli.md +38 -12
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/pyproject.toml +2 -2
- autotouch_cli-0.2.30/autotouch_cli.egg-info/top_level.txt +0 -1
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli/__init__.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/setup.cfg +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_column_prompt_compiler.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_custom.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_integration.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_multi_titles.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_pipeline.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_simple.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_v2_bulk.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_lead_required_fields.py +0 -0
- {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_phone_provider_pipeline.py +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.32
|
|
4
4
|
Summary: Autotouch Smart Table CLI
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -71,6 +71,7 @@ Developer keys and scopes are identical to raw API usage (`stk_...`, same scope
|
|
|
71
71
|
autotouch auth set-key --api-key stk_... --base-url https://app.autotouch.ai
|
|
72
72
|
autotouch auth check
|
|
73
73
|
autotouch auth show
|
|
74
|
+
autotouch auth whoami
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
For user/admin endpoints like onboarding, org context, and personal context, sign in with a user session too:
|
|
@@ -107,8 +108,9 @@ autotouch auth bootstrap --data-file bootstrap.json --save-key
|
|
|
107
108
|
|
|
108
109
|
Notes:
|
|
109
110
|
- New orgs created through signup/bootstrap start with `50` credits.
|
|
110
|
-
- `--save-key` stores the returned `apiKey` in
|
|
111
|
+
- `--save-key` stores the returned `apiKey` and also signs in the same user so the saved config has both the developer key and the user session.
|
|
111
112
|
- Identity linking is email-based: later human sign-in with the same normalized email maps to the same user/org.
|
|
113
|
+
- `auth whoami` shows which user/org the saved session belongs to.
|
|
112
114
|
|
|
113
115
|
## Onboarding and context
|
|
114
116
|
|
|
@@ -513,8 +515,8 @@ autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>
|
|
|
513
515
|
```
|
|
514
516
|
|
|
515
517
|
Recommendation:
|
|
516
|
-
- For provider-backed
|
|
517
|
-
|
|
518
|
+
- For provider-backed or generated columns, start with `autotouch columns recipe`.
|
|
519
|
+
- Built-in recipe types now include `formatter`, `llm_enrichment`, `email_finder`, `phone_finder`, `lead_finder`, `add_to_crm`, `sync_to_table`, and `add_to_sequence`.
|
|
518
520
|
- Email/phone enrichment does not require creating `add_to_crm` first.
|
|
519
521
|
- `add_to_crm` is an optional, non-billable export action.
|
|
520
522
|
|
|
@@ -779,6 +781,7 @@ Notes:
|
|
|
779
781
|
Add-to-Leads note: `companyDomain` is required; LinkedIn is optional. Each row still needs at least one usable hard identity signal (LinkedIn, email, or phone). Names and location fields are metadata only for this flow. Run is non-billable.
|
|
780
782
|
If `companyDomain` is missing in the table, derive or enrich that domain column first, then rerun `add_to_crm`.
|
|
781
783
|
Queued lifecycle: the CLI/API accepts the run immediately, then the backend hands off `ops -> data_io` for execution.
|
|
784
|
+
Creating `add_to_crm` after upstream email/phone/lead columns already finished does not replay those older updates. In that case, run `add_to_crm` explicitly or rerun the upstream source column.
|
|
782
785
|
|
|
783
786
|
CRM data model expectations (recommended before `add_to_crm`):
|
|
784
787
|
- Lead identity/dedupe expects `company_domain` plus one usable hard identity signal. `linkedin_url` is a strong optional signal, not a requirement.
|
|
@@ -817,6 +820,14 @@ CRM data model expectations (recommended before `add_to_crm`):
|
|
|
817
820
|
Notes:
|
|
818
821
|
- Single-destination mode uses `destinationTableId` + `columnMappings`.
|
|
819
822
|
- Router mode is also supported with `config.routes[]` (top-to-bottom matching, first hit wins, default route catches unmatched rows).
|
|
823
|
+
- Route conditions are evaluated against the parent/source row, not against each list item.
|
|
824
|
+
- `autoRun: "onSourceUpdate"` means dependency-driven runs from source row inserts/updates. Saving the column does not backfill existing rows for `sync_to_table`; use an explicit column run/backfill when needed.
|
|
825
|
+
- List mode uses `syncMode: "list"` with `listSourceColumnId` and optional `listPath` (default `items`) to expand a canonical JSON payload like `{ "items": [...], "reasoning": "..." }`.
|
|
826
|
+
- In list mode:
|
|
827
|
+
- `sourceScope: "item"` maps fields from each `items[]` object.
|
|
828
|
+
- `sourceScope: "row"` maps fields from the parent/source row.
|
|
829
|
+
- Explicit item mappings are still recommended for renamed fields such as `name -> full_name`.
|
|
830
|
+
- Blank placeholder list items are skipped and do not create destination rows.
|
|
820
831
|
|
|
821
832
|
### 8) `add_to_sequence`
|
|
822
833
|
|
|
@@ -842,10 +853,33 @@ Notes:
|
|
|
842
853
|
- `sourceLeadColumn` must be the source column key that stores lead IDs (for example `add_to_leads` or another lead-id column key from `lead_finder` output), not the provider name `add_to_crm`.
|
|
843
854
|
- `sequenceId` is the target sequence workflow ID.
|
|
844
855
|
- The target sequence must already be `ACTIVE` for real enrollment. Table `add_to_sequence` runs and direct `POST /api/sequences/{id}/enroll` share the same activation check.
|
|
856
|
+
- Common tail order after contact rows or lead IDs exist is `email_finder -> add_to_crm -> create/activate sequence -> add_to_sequence`.
|
|
857
|
+
- Creating `add_to_sequence` after lead IDs already exist does not replay those older updates. In that case, run `add_to_sequence` explicitly or rerun the lead-id source column.
|
|
845
858
|
- `add_to_sequence` runs auto-attach `research_context.source_table_id` (and table name when available). Field selection stays implicit so sequence drafts/audience resolve favorites from current starred columns by default.
|
|
846
859
|
- Star/favorite the highest-signal fields so callers can see them quickly in the sidecar during live call workflows.
|
|
847
860
|
- The same favorite set is also the default AI drafting context when explicit `fieldIds` are not provided.
|
|
848
861
|
|
|
862
|
+
## Example pipeline: LLM items -> contacts table -> leads -> sequence
|
|
863
|
+
|
|
864
|
+
Use this when an LLM column returns canonical JSON like `{ "items": [...] }` and each item should become a contact row.
|
|
865
|
+
This is an example pattern, not the default recommendation for all person discovery.
|
|
866
|
+
Use `lead_finder` first when the goal is explicit buyer/lead discovery with clear role targeting at scale.
|
|
867
|
+
Use agent `llm_enrichment` when the workflow needs custom research output, website-discovered contacts, or low-coverage / hard-to-find cases.
|
|
868
|
+
|
|
869
|
+
1. Create the source table and import company rows.
|
|
870
|
+
2. Create the LLM enrichment column that outputs `items[]`.
|
|
871
|
+
3. Create `sync_to_table` in `list` mode:
|
|
872
|
+
- set `listSourceColumnId` to the LLM column ID
|
|
873
|
+
- keep item fields on `sourceScope: "item"`
|
|
874
|
+
- keep parent company metadata on `sourceScope: "row"`
|
|
875
|
+
4. Run or wait for `sync_to_table` so the destination contacts table has rows.
|
|
876
|
+
5. On the destination contacts table, create and run `email_finder`.
|
|
877
|
+
6. Create `add_to_crm` on that same destination contacts table.
|
|
878
|
+
7. If email finder already finished before `add_to_crm` existed, run `add_to_crm` explicitly.
|
|
879
|
+
8. Create the target sequence and set it `ACTIVE`.
|
|
880
|
+
9. Create `add_to_sequence` with `sourceLeadColumn` set to the lead-id column key, usually `add_to_leads`.
|
|
881
|
+
10. If leads already existed before `add_to_sequence` was created, run `add_to_sequence` explicitly.
|
|
882
|
+
|
|
849
883
|
## Common workflow
|
|
850
884
|
|
|
851
885
|
```bash
|
|
@@ -1157,6 +1191,7 @@ Agent expectations:
|
|
|
1157
1191
|
- `column_types` tells you which column types are runnable and which are non-billable transforms (`json_split`, `formatter_formula`).
|
|
1158
1192
|
- `filtering` describes valid scope/filter semantics for estimate/run.
|
|
1159
1193
|
- `automation.auto_run` describes supported auto-run modes + config field names.
|
|
1194
|
+
- `automation.providers` exposes provider-specific setup contracts, save-time backfill behavior, dispatch strategy, and recipe type names for formatter/LLM/finders/action columns.
|
|
1160
1195
|
- `execution_policies.llm.output_contract` describes output behavior by mode:
|
|
1161
1196
|
- `agent` => JSON-oriented structured output
|
|
1162
1197
|
- `basic` => text or JSON (`dataType=json` for structured JSON output)
|
|
@@ -34,6 +34,10 @@ from pathlib import Path
|
|
|
34
34
|
from typing import Any, Dict, List, Optional, Tuple
|
|
35
35
|
|
|
36
36
|
import requests
|
|
37
|
+
from autotouch_shared.provider_registry import (
|
|
38
|
+
get_recipe_contract,
|
|
39
|
+
recipe_types,
|
|
40
|
+
)
|
|
37
41
|
|
|
38
42
|
try:
|
|
39
43
|
from pymongo import MongoClient # type: ignore
|
|
@@ -82,186 +86,24 @@ SETUP_BANNER = r"""
|
|
|
82
86
|
AI CLI SETUP
|
|
83
87
|
"""
|
|
84
88
|
|
|
85
|
-
COLUMN_RECIPE_TYPES =
|
|
86
|
-
"email_finder",
|
|
87
|
-
"phone_finder",
|
|
88
|
-
"lead_finder",
|
|
89
|
-
"llm_enrichment",
|
|
90
|
-
"add_to_crm",
|
|
91
|
-
"sync_to_table",
|
|
92
|
-
"add_to_sequence",
|
|
93
|
-
]
|
|
89
|
+
COLUMN_RECIPE_TYPES = recipe_types()
|
|
94
90
|
|
|
95
|
-
COLUMN_CREATE_RECIPES: Dict[str, Dict[str, Any]] = {
|
|
96
|
-
"email_finder": {
|
|
97
|
-
"key": "work_email",
|
|
98
|
-
"label": "Work Email",
|
|
99
|
-
"kind": "enrichment",
|
|
100
|
-
"dataType": "json",
|
|
101
|
-
"origin": "email_finder",
|
|
102
|
-
"autoRun": "never",
|
|
103
|
-
"config": {
|
|
104
|
-
"provider": "smart_email_finder",
|
|
105
|
-
"strategy": "cost_optimized",
|
|
106
|
-
"lookupStrategy": "linkedin",
|
|
107
|
-
"linkedinOnly": True,
|
|
108
|
-
"enableProfileFallback": False,
|
|
109
|
-
"linkedinUrl": "linkedin_url",
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
"phone_finder": {
|
|
113
|
-
"key": "mobile_phone",
|
|
114
|
-
"label": "Mobile Phone",
|
|
115
|
-
"kind": "enrichment",
|
|
116
|
-
"dataType": "json",
|
|
117
|
-
"origin": "phone_finder",
|
|
118
|
-
"autoRun": "never",
|
|
119
|
-
"config": {
|
|
120
|
-
"provider": "smart_phone_finder",
|
|
121
|
-
"firstName": "first_name",
|
|
122
|
-
"lastName": "last_name",
|
|
123
|
-
"company": "company",
|
|
124
|
-
"linkedinUrl": "linkedin_url",
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
"lead_finder": {
|
|
128
|
-
"key": "lead_contacts",
|
|
129
|
-
"label": "Lead Contacts",
|
|
130
|
-
"kind": "enrichment",
|
|
131
|
-
"dataType": "json",
|
|
132
|
-
"origin": "ai",
|
|
133
|
-
"autoRun": "never",
|
|
134
|
-
"config": {
|
|
135
|
-
"provider": "lead_finder",
|
|
136
|
-
"sourceMode": "bulk_companies",
|
|
137
|
-
"companyDomain": "domain",
|
|
138
|
-
"strictness": "flexible_roles",
|
|
139
|
-
"storeAsLeads": True,
|
|
140
|
-
"autoEnrichEmails": True,
|
|
141
|
-
"autoEnrichPhones": False,
|
|
142
|
-
"jobTitles": ["Head of Sales", "VP Sales"],
|
|
143
|
-
"locations": ["United States"],
|
|
144
|
-
"maxResults": 10,
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
"llm_enrichment": {
|
|
148
|
-
"key": "company_research",
|
|
149
|
-
"label": "Company Research",
|
|
150
|
-
"kind": "enrichment",
|
|
151
|
-
"dataType": "json",
|
|
152
|
-
"origin": "ai",
|
|
153
|
-
"autoRun": "never",
|
|
154
|
-
"config": {
|
|
155
|
-
"instructions": "Research the company in this row and return JSON with icp_fit, summary, and risks.",
|
|
156
|
-
"mode": "agent",
|
|
157
|
-
"temperature": 0.7,
|
|
158
|
-
"batchSize": 50,
|
|
159
|
-
"promptSource": "generated",
|
|
160
|
-
"useCompanyContext": True,
|
|
161
|
-
"structuredOutput": True,
|
|
162
|
-
"useAutoSchema": True,
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
"add_to_crm": {
|
|
166
|
-
"key": "add_to_leads",
|
|
167
|
-
"label": "Add to Leads",
|
|
168
|
-
"kind": "enrichment",
|
|
169
|
-
"dataType": "json",
|
|
170
|
-
"origin": "manual",
|
|
171
|
-
"autoRun": "onSourceUpdate",
|
|
172
|
-
"config": {
|
|
173
|
-
"provider": "add_to_crm",
|
|
174
|
-
"leadSource": "research_table_export",
|
|
175
|
-
"fieldMappings": {
|
|
176
|
-
"mode": "single",
|
|
177
|
-
"linkedinUrl": "linkedin_url",
|
|
178
|
-
"companyDomain": "domain",
|
|
179
|
-
"firstName": "first_name",
|
|
180
|
-
"lastName": "last_name",
|
|
181
|
-
"title": "title",
|
|
182
|
-
"companyName": "company",
|
|
183
|
-
"emailAddresses": [{"column": "work_email", "type": "work"}],
|
|
184
|
-
"phoneNumbers": [{"column": "mobile_phone", "type": "mobile"}],
|
|
185
|
-
},
|
|
186
|
-
"sourceColumns": [
|
|
187
|
-
"linkedin_url",
|
|
188
|
-
"domain",
|
|
189
|
-
"first_name",
|
|
190
|
-
"last_name",
|
|
191
|
-
"title",
|
|
192
|
-
"company",
|
|
193
|
-
"work_email",
|
|
194
|
-
"mobile_phone",
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
"sync_to_table": {
|
|
199
|
-
"key": "sync_to_table",
|
|
200
|
-
"label": "Sync to Table",
|
|
201
|
-
"kind": "enrichment",
|
|
202
|
-
"dataType": "json",
|
|
203
|
-
"origin": "manual",
|
|
204
|
-
"autoRun": "onSourceUpdate",
|
|
205
|
-
"config": {
|
|
206
|
-
"provider": "sync_to_table",
|
|
207
|
-
"destinationTableId": "<DESTINATION_TABLE_ID>",
|
|
208
|
-
"columnMappings": [
|
|
209
|
-
{"sourceKey": "company", "destKey": "company"},
|
|
210
|
-
{"sourceKey": "domain", "destKey": "company_domain"},
|
|
211
|
-
{"sourceKey": "work_email", "destKey": "work_email"},
|
|
212
|
-
],
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
"add_to_sequence": {
|
|
216
|
-
"key": "add_to_sequence",
|
|
217
|
-
"label": "Add to Sequence",
|
|
218
|
-
"kind": "enrichment",
|
|
219
|
-
"dataType": "json",
|
|
220
|
-
"origin": "manual",
|
|
221
|
-
"autoRun": "onSourceUpdate",
|
|
222
|
-
"config": {
|
|
223
|
-
"provider": "add_to_sequence",
|
|
224
|
-
"sequenceId": "<SEQUENCE_ID>",
|
|
225
|
-
"sourceLeadColumn": "add_to_leads",
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
}
|
|
229
91
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"Manual/basic prompts should use explicit placeholders like {{company_name}}, {{domain}}, or {{user_value_proposition}} when those values are needed.",
|
|
246
|
-
"Agent mode is JSON-oriented.",
|
|
247
|
-
"Basic mode can be text or JSON; JSON requires dataType=json.",
|
|
248
|
-
],
|
|
249
|
-
"add_to_crm": [
|
|
250
|
-
"companyDomain is required; LinkedIn is optional.",
|
|
251
|
-
"Include at least one usable hard-identity mapping such as LinkedIn, email, or phone.",
|
|
252
|
-
"If companyDomain is missing in the table, derive or enrich that domain column first.",
|
|
253
|
-
"Add to CRM is optional and non-billable.",
|
|
254
|
-
],
|
|
255
|
-
"sync_to_table": [
|
|
256
|
-
"Use destinationTableId + columnMappings for single-destination sync.",
|
|
257
|
-
"Router mode is supported via config.routes[] (first matching route wins).",
|
|
258
|
-
],
|
|
259
|
-
"add_to_sequence": [
|
|
260
|
-
"sourceLeadColumn should reference the source column key that stores lead IDs (for example add_to_leads), not the provider name add_to_crm.",
|
|
261
|
-
"Set sequenceId to the target workflow sequence ID.",
|
|
262
|
-
"The target sequence must already be ACTIVE for real enrollment.",
|
|
263
|
-
],
|
|
264
|
-
}
|
|
92
|
+
def _build_column_recipe_catalog() -> Tuple[Dict[str, Dict[str, Any]], Dict[str, List[str]]]:
|
|
93
|
+
recipes: Dict[str, Dict[str, Any]] = {}
|
|
94
|
+
notes: Dict[str, List[str]] = {}
|
|
95
|
+
for recipe_type in COLUMN_RECIPE_TYPES:
|
|
96
|
+
contract = get_recipe_contract(recipe_type)
|
|
97
|
+
if not contract:
|
|
98
|
+
continue
|
|
99
|
+
payload = contract.recipe_payload_copy()
|
|
100
|
+
if isinstance(payload, dict):
|
|
101
|
+
recipes[recipe_type] = payload
|
|
102
|
+
notes[recipe_type] = contract.recipe_notes_list()
|
|
103
|
+
return recipes, notes
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
COLUMN_CREATE_RECIPES, COLUMN_RECIPE_NOTES = _build_column_recipe_catalog()
|
|
265
107
|
|
|
266
108
|
SEQUENCE_RECIPE_TYPES = [
|
|
267
109
|
"create",
|
|
@@ -801,20 +643,73 @@ def _api_url(value: Optional[str] = None) -> str:
|
|
|
801
643
|
return DEFAULT_API_URL.rstrip("/")
|
|
802
644
|
|
|
803
645
|
|
|
646
|
+
def _looks_like_api_key(value: Optional[str]) -> bool:
|
|
647
|
+
token = str(value or "").strip()
|
|
648
|
+
return token.startswith("stk_") and "." in token
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _looks_like_jwt(value: Optional[str]) -> bool:
|
|
652
|
+
token = str(value or "").strip()
|
|
653
|
+
return bool(token) and not _looks_like_api_key(token) and token.count(".") == 2
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _env_user_token() -> Optional[str]:
|
|
657
|
+
for key in ("SMARTTABLE_TOKEN", "AUTOTOUCH_TOKEN"):
|
|
658
|
+
value = str(os.environ.get(key) or "").strip()
|
|
659
|
+
if value and not _looks_like_api_key(value):
|
|
660
|
+
return value
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _env_api_key() -> Optional[str]:
|
|
665
|
+
direct = str(os.environ.get("AUTOTOUCH_API_KEY") or "").strip()
|
|
666
|
+
if direct:
|
|
667
|
+
return direct
|
|
668
|
+
for key in ("SMARTTABLE_TOKEN", "AUTOTOUCH_TOKEN"):
|
|
669
|
+
value = str(os.environ.get(key) or "").strip()
|
|
670
|
+
if _looks_like_api_key(value):
|
|
671
|
+
return value
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
|
|
804
675
|
def _resolve_token(explicit_token: Optional[str], required: bool = True) -> Optional[str]:
|
|
805
|
-
tok = (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
676
|
+
tok = str(explicit_token or "").strip()
|
|
677
|
+
if not tok:
|
|
678
|
+
cfg = _load_config()
|
|
679
|
+
tok = (
|
|
680
|
+
str(cfg.get("auth_token") or "").strip()
|
|
681
|
+
or str(cfg.get("api_key") or "").strip()
|
|
682
|
+
or str(_env_user_token() or "").strip()
|
|
683
|
+
or str(_env_api_key() or "").strip()
|
|
684
|
+
)
|
|
685
|
+
if not tok:
|
|
686
|
+
if required:
|
|
687
|
+
print(
|
|
688
|
+
"ERROR: missing token. Pass --token, run `autotouch auth login`, or run `autotouch auth set-key`.",
|
|
689
|
+
file=sys.stderr,
|
|
690
|
+
)
|
|
691
|
+
sys.exit(2)
|
|
692
|
+
return None
|
|
693
|
+
return tok
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _resolve_user_session_token(explicit_token: Optional[str], required: bool = True) -> Optional[str]:
|
|
697
|
+
tok = str(explicit_token or "").strip()
|
|
698
|
+
if tok and _looks_like_api_key(tok):
|
|
699
|
+
print(
|
|
700
|
+
"ERROR: this command requires a user session token, not a developer API key. Run `autotouch auth login`.",
|
|
701
|
+
file=sys.stderr,
|
|
702
|
+
)
|
|
703
|
+
sys.exit(2)
|
|
704
|
+
|
|
811
705
|
if not tok:
|
|
812
706
|
cfg = _load_config()
|
|
813
|
-
tok = cfg.get("auth_token") or
|
|
707
|
+
tok = str(cfg.get("auth_token") or "").strip() or str(_env_user_token() or "").strip()
|
|
708
|
+
|
|
814
709
|
if not tok:
|
|
815
710
|
if required:
|
|
816
711
|
print(
|
|
817
|
-
"ERROR:
|
|
712
|
+
"ERROR: this command requires a saved user session. Run `autotouch auth login` or pass a JWT with --token.",
|
|
818
713
|
file=sys.stderr,
|
|
819
714
|
)
|
|
820
715
|
sys.exit(2)
|
|
@@ -3109,16 +3004,53 @@ def cmd_auth_bootstrap(args: argparse.Namespace) -> None:
|
|
|
3109
3004
|
output: Any = data
|
|
3110
3005
|
if getattr(args, "save_key", False) and isinstance(data, dict):
|
|
3111
3006
|
api_key = str(data.get("apiKey") or data.get("api_key") or "").strip()
|
|
3007
|
+
email = str(payload.get("email") or "").strip().lower()
|
|
3008
|
+
password = str(payload.get("password") or "").strip()
|
|
3009
|
+
session_data: Optional[Dict[str, Any]] = None
|
|
3010
|
+
session_error: Optional[str] = None
|
|
3011
|
+
|
|
3012
|
+
if email and password:
|
|
3013
|
+
try:
|
|
3014
|
+
session_data = _request_api(
|
|
3015
|
+
"POST",
|
|
3016
|
+
"/api/auth/login",
|
|
3017
|
+
base_url=args.base_url,
|
|
3018
|
+
token=None,
|
|
3019
|
+
use_x_api_key=False,
|
|
3020
|
+
payload={"email": email, "password": password},
|
|
3021
|
+
timeout=args.timeout,
|
|
3022
|
+
verbose=args.verbose,
|
|
3023
|
+
)
|
|
3024
|
+
except SystemExit as exc:
|
|
3025
|
+
session_error = f"login failed with exit code {exc.code}"
|
|
3026
|
+
except Exception as exc:
|
|
3027
|
+
session_error = str(exc)
|
|
3028
|
+
|
|
3112
3029
|
if api_key:
|
|
3113
3030
|
cfg = _load_config()
|
|
3114
3031
|
cfg["api_key"] = api_key
|
|
3032
|
+
access_token = _normalize_string_value((session_data or {}).get("access_token"))
|
|
3033
|
+
refresh_token = _normalize_string_value((session_data or {}).get("refresh_token"))
|
|
3034
|
+
if access_token:
|
|
3035
|
+
cfg["auth_token"] = access_token
|
|
3036
|
+
cfg["auth_email"] = email
|
|
3037
|
+
if refresh_token:
|
|
3038
|
+
cfg["refresh_token"] = refresh_token
|
|
3115
3039
|
cfg["base_url"] = str(args.base_url or _api_url()).rstrip("/")
|
|
3116
3040
|
cfg["updated_at_epoch"] = int(time.time())
|
|
3117
3041
|
config_path = _save_config(cfg)
|
|
3118
3042
|
output = dict(data)
|
|
3119
3043
|
output["saved"] = True
|
|
3044
|
+
output["saved_api_key"] = True
|
|
3045
|
+
output["saved_session"] = bool(access_token)
|
|
3120
3046
|
output["config_path"] = str(config_path)
|
|
3121
3047
|
output["api_key_masked"] = _mask_api_key(api_key)
|
|
3048
|
+
if access_token:
|
|
3049
|
+
output["access_token_masked"] = _mask_secret(access_token)
|
|
3050
|
+
if refresh_token:
|
|
3051
|
+
output["refresh_token_masked"] = _mask_secret(refresh_token)
|
|
3052
|
+
if session_error:
|
|
3053
|
+
output["session_warning"] = session_error
|
|
3122
3054
|
else:
|
|
3123
3055
|
output = dict(data)
|
|
3124
3056
|
output["saved"] = False
|
|
@@ -3202,6 +3134,9 @@ def cmd_auth_show(args: argparse.Namespace) -> None:
|
|
|
3202
3134
|
refresh_token = str(cfg.get("refresh_token") or "").strip()
|
|
3203
3135
|
base_url = str(cfg.get("base_url") or _api_url()).rstrip("/")
|
|
3204
3136
|
path = _config_path()
|
|
3137
|
+
default_general_auth = "auth_token" if auth_token else ("api_key" if api_key else None)
|
|
3138
|
+
default_user_session_auth = "auth_token" if auth_token else ("env_token" if _env_user_token() else None)
|
|
3139
|
+
default_developer_auth = "api_key" if api_key else ("env_api_key" if _env_api_key() else None)
|
|
3205
3140
|
output = {
|
|
3206
3141
|
"config_path": str(path),
|
|
3207
3142
|
"config_exists": path.exists(),
|
|
@@ -3214,10 +3149,27 @@ def cmd_auth_show(args: argparse.Namespace) -> None:
|
|
|
3214
3149
|
"auth_email": cfg.get("auth_email"),
|
|
3215
3150
|
"base_url": base_url,
|
|
3216
3151
|
"updated_at_epoch": cfg.get("updated_at_epoch"),
|
|
3152
|
+
"default_general_auth": default_general_auth,
|
|
3153
|
+
"default_user_session_auth": default_user_session_auth,
|
|
3154
|
+
"default_developer_auth": default_developer_auth,
|
|
3217
3155
|
}
|
|
3218
3156
|
_print_json(output, compact=args.compact)
|
|
3219
3157
|
|
|
3220
3158
|
|
|
3159
|
+
def cmd_auth_whoami(args: argparse.Namespace) -> None:
|
|
3160
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3161
|
+
data = _request_api(
|
|
3162
|
+
"GET",
|
|
3163
|
+
"/api/auth/me",
|
|
3164
|
+
base_url=args.base_url,
|
|
3165
|
+
token=token,
|
|
3166
|
+
use_x_api_key=False,
|
|
3167
|
+
timeout=args.timeout,
|
|
3168
|
+
verbose=args.verbose,
|
|
3169
|
+
)
|
|
3170
|
+
_print_json(data, compact=args.compact)
|
|
3171
|
+
|
|
3172
|
+
|
|
3221
3173
|
def cmd_auth_clear(args: argparse.Namespace) -> None:
|
|
3222
3174
|
cfg = _load_config()
|
|
3223
3175
|
changed = False
|
|
@@ -3355,7 +3307,7 @@ def _build_personal_context_payload(args: argparse.Namespace) -> Dict[str, Any]:
|
|
|
3355
3307
|
|
|
3356
3308
|
|
|
3357
3309
|
def cmd_onboarding_submit_domain(args: argparse.Namespace) -> None:
|
|
3358
|
-
token =
|
|
3310
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3359
3311
|
payload = _load_json_input(
|
|
3360
3312
|
inline_json=getattr(args, "data_json", None),
|
|
3361
3313
|
file_path=getattr(args, "data_file", None),
|
|
@@ -3377,7 +3329,7 @@ def cmd_onboarding_submit_domain(args: argparse.Namespace) -> None:
|
|
|
3377
3329
|
"/api/onboarding/submit-domain",
|
|
3378
3330
|
base_url=args.base_url,
|
|
3379
3331
|
token=token,
|
|
3380
|
-
use_x_api_key=
|
|
3332
|
+
use_x_api_key=False,
|
|
3381
3333
|
form_payload={
|
|
3382
3334
|
"website": str(payload.get("website") or payload.get("domain") or "").strip(),
|
|
3383
3335
|
"domain": str(payload.get("domain") or "").strip(),
|
|
@@ -3389,13 +3341,13 @@ def cmd_onboarding_submit_domain(args: argparse.Namespace) -> None:
|
|
|
3389
3341
|
|
|
3390
3342
|
|
|
3391
3343
|
def cmd_org_context_get(args: argparse.Namespace) -> None:
|
|
3392
|
-
token =
|
|
3344
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3393
3345
|
data = _request_api(
|
|
3394
3346
|
"GET",
|
|
3395
3347
|
"/api/org/context",
|
|
3396
3348
|
base_url=args.base_url,
|
|
3397
3349
|
token=token,
|
|
3398
|
-
use_x_api_key=
|
|
3350
|
+
use_x_api_key=False,
|
|
3399
3351
|
timeout=args.timeout,
|
|
3400
3352
|
verbose=args.verbose,
|
|
3401
3353
|
)
|
|
@@ -3403,14 +3355,14 @@ def cmd_org_context_get(args: argparse.Namespace) -> None:
|
|
|
3403
3355
|
|
|
3404
3356
|
|
|
3405
3357
|
def cmd_org_context_set(args: argparse.Namespace) -> None:
|
|
3406
|
-
token =
|
|
3358
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3407
3359
|
payload = _build_org_context_payload(args)
|
|
3408
3360
|
data = _request_api(
|
|
3409
3361
|
"POST",
|
|
3410
3362
|
"/api/org/context",
|
|
3411
3363
|
base_url=args.base_url,
|
|
3412
3364
|
token=token,
|
|
3413
|
-
use_x_api_key=
|
|
3365
|
+
use_x_api_key=False,
|
|
3414
3366
|
form_payload=_org_context_form_payload(payload),
|
|
3415
3367
|
timeout=args.timeout,
|
|
3416
3368
|
verbose=args.verbose,
|
|
@@ -3419,13 +3371,13 @@ def cmd_org_context_set(args: argparse.Namespace) -> None:
|
|
|
3419
3371
|
|
|
3420
3372
|
|
|
3421
3373
|
def cmd_personal_context_get(args: argparse.Namespace) -> None:
|
|
3422
|
-
token =
|
|
3374
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3423
3375
|
data = _request_api(
|
|
3424
3376
|
"GET",
|
|
3425
3377
|
"/api/onboarding/personal-context",
|
|
3426
3378
|
base_url=args.base_url,
|
|
3427
3379
|
token=token,
|
|
3428
|
-
use_x_api_key=
|
|
3380
|
+
use_x_api_key=False,
|
|
3429
3381
|
timeout=args.timeout,
|
|
3430
3382
|
verbose=args.verbose,
|
|
3431
3383
|
)
|
|
@@ -3433,14 +3385,14 @@ def cmd_personal_context_get(args: argparse.Namespace) -> None:
|
|
|
3433
3385
|
|
|
3434
3386
|
|
|
3435
3387
|
def cmd_personal_context_set(args: argparse.Namespace) -> None:
|
|
3436
|
-
token =
|
|
3388
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3437
3389
|
payload = _build_personal_context_payload(args)
|
|
3438
3390
|
data = _request_api(
|
|
3439
3391
|
"POST",
|
|
3440
3392
|
"/api/onboarding/personal-context",
|
|
3441
3393
|
base_url=args.base_url,
|
|
3442
3394
|
token=token,
|
|
3443
|
-
use_x_api_key=
|
|
3395
|
+
use_x_api_key=False,
|
|
3444
3396
|
payload=payload,
|
|
3445
3397
|
timeout=args.timeout,
|
|
3446
3398
|
verbose=args.verbose,
|
|
@@ -3449,13 +3401,13 @@ def cmd_personal_context_set(args: argparse.Namespace) -> None:
|
|
|
3449
3401
|
|
|
3450
3402
|
|
|
3451
3403
|
def cmd_context_resolved(args: argparse.Namespace) -> None:
|
|
3452
|
-
token =
|
|
3404
|
+
token = _resolve_user_session_token(args.token, required=True)
|
|
3453
3405
|
data = _request_api(
|
|
3454
3406
|
"GET",
|
|
3455
3407
|
"/api/llm/company-context",
|
|
3456
3408
|
base_url=args.base_url,
|
|
3457
3409
|
token=token,
|
|
3458
|
-
use_x_api_key=
|
|
3410
|
+
use_x_api_key=False,
|
|
3459
3411
|
timeout=args.timeout,
|
|
3460
3412
|
verbose=args.verbose,
|
|
3461
3413
|
)
|
|
@@ -3479,6 +3431,8 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
|
|
|
3479
3431
|
output_contract = llm.get("output_contract") if isinstance(llm.get("output_contract"), dict) else {}
|
|
3480
3432
|
agent_contract = output_contract.get("agent") if isinstance(output_contract.get("agent"), dict) else {}
|
|
3481
3433
|
basic_contract = output_contract.get("basic") if isinstance(output_contract.get("basic"), dict) else {}
|
|
3434
|
+
automation = data.get("automation") if isinstance(data.get("automation"), dict) else {}
|
|
3435
|
+
providers = automation.get("providers") if isinstance(automation.get("providers"), dict) else {}
|
|
3482
3436
|
|
|
3483
3437
|
print(f"API version : {_as_display(data.get('api_version'))}")
|
|
3484
3438
|
print(f"Base URL : {_as_display(data.get('base_url'))}")
|
|
@@ -3502,7 +3456,13 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
|
|
|
3502
3456
|
basic_types = basic_contract.get("supported_data_types")
|
|
3503
3457
|
basic_types_str = ", ".join(str(t) for t in basic_types) if isinstance(basic_types, list) else "text/json"
|
|
3504
3458
|
print(f"basic : {basic_types_str} (JSON requires dataType=json)")
|
|
3459
|
+
if providers:
|
|
3460
|
+
print("")
|
|
3461
|
+
print("Provider setup")
|
|
3462
|
+
print("--------------")
|
|
3463
|
+
print(", ".join(sorted(str(name) for name in providers.keys())))
|
|
3505
3464
|
print("")
|
|
3465
|
+
print("Tip: inspect automation.providers in JSON output for provider-specific setup contracts.")
|
|
3506
3466
|
print("Tip: for JSON enrichment columns, run with `--wait` and verify terminal status before `autotouch columns projections`.")
|
|
3507
3467
|
return
|
|
3508
3468
|
|
|
@@ -6290,7 +6250,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
6290
6250
|
pab.add_argument("--scopes-file", help="Optional JSON file with scopes array")
|
|
6291
6251
|
pab.add_argument("--data-json", help="Explicit agent bootstrap payload JSON")
|
|
6292
6252
|
pab.add_argument("--data-file", help="Path to agent bootstrap payload JSON file")
|
|
6293
|
-
pab.add_argument("--save-key", action="store_true", help="Persist the returned apiKey
|
|
6253
|
+
pab.add_argument("--save-key", action="store_true", help="Persist the returned apiKey and bootstrap a saved JWT session")
|
|
6294
6254
|
pab.add_argument("--base-url", default=_api_url(), help=f"API base URL (default: {DEFAULT_API_URL})")
|
|
6295
6255
|
pab.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SECONDS, help="HTTP timeout in seconds")
|
|
6296
6256
|
pab.add_argument("--output", choices=["json", "human"], default="json", help="Output mode")
|
|
@@ -6325,6 +6285,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
6325
6285
|
pashow.add_argument("--compact", action="store_true", help="Print compact JSON")
|
|
6326
6286
|
pashow.set_defaults(func=cmd_auth_show)
|
|
6327
6287
|
|
|
6288
|
+
pawho = auth_sub.add_parser("whoami", help="Show the current saved user session identity")
|
|
6289
|
+
pawho.add_argument("--token", help="Explicit JWT session token")
|
|
6290
|
+
pawho.add_argument("--base-url", default=_api_url(), help=f"API base URL (default: {DEFAULT_API_URL})")
|
|
6291
|
+
pawho.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SECONDS, help="HTTP timeout in seconds")
|
|
6292
|
+
pawho.add_argument("--output", choices=["json", "human"], default="json", help="Output mode")
|
|
6293
|
+
pawho.add_argument("--compact", action="store_true", help="Print compact JSON")
|
|
6294
|
+
pawho.add_argument("--verbose", action="store_true", help="Print request metadata to stderr")
|
|
6295
|
+
pawho.set_defaults(func=cmd_auth_whoami)
|
|
6296
|
+
|
|
6328
6297
|
paclear = auth_sub.add_parser("clear", help="Clear stored auth config")
|
|
6329
6298
|
paclear.add_argument("--all", action="store_true", help="Clear all config keys")
|
|
6330
6299
|
paclear.add_argument("--clear-base-url", action="store_true", help="Also clear stored base_url")
|