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.
Files changed (24) hide show
  1. {autotouch_cli-0.2.30/autotouch_cli.egg-info → autotouch_cli-0.2.32}/PKG-INFO +39 -4
  2. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli/cli.py +168 -199
  3. 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
  4. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/SOURCES.txt +2 -0
  5. autotouch_cli-0.2.32/autotouch_cli.egg-info/top_level.txt +2 -0
  6. autotouch_cli-0.2.32/autotouch_shared/__init__.py +1 -0
  7. autotouch_cli-0.2.32/autotouch_shared/provider_registry.py +768 -0
  8. autotouch_cli-0.2.30/PKG-INFO → autotouch_cli-0.2.32/docs/research-table/reference/autotouch-cli.md +38 -12
  9. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/pyproject.toml +2 -2
  10. autotouch_cli-0.2.30/autotouch_cli.egg-info/top_level.txt +0 -1
  11. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli/__init__.py +0 -0
  12. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  13. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/entry_points.txt +0 -0
  14. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/autotouch_cli.egg-info/requires.txt +0 -0
  15. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/setup.cfg +0 -0
  16. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_column_prompt_compiler.py +0 -0
  17. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_custom.py +0 -0
  18. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_integration.py +0 -0
  19. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_multi_titles.py +0 -0
  20. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_pipeline.py +0 -0
  21. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_simple.py +0 -0
  22. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_contactout_v2_bulk.py +0 -0
  23. {autotouch_cli-0.2.30 → autotouch_cli-0.2.32}/tests/test_lead_required_fields.py +0 -0
  24. {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.30
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 local CLI config automatically.
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 workflow columns, start with `autotouch columns recipe`:
517
- `add_to_crm`, `sync_to_table`, `add_to_sequence`.
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
- COLUMN_RECIPE_NOTES: Dict[str, List[str]] = {
231
- "email_finder": [
232
- "For non-LinkedIn lookup, use lookupStrategy=domain/company with firstName+lastName+domain/company fields.",
233
- ],
234
- "phone_finder": [
235
- "You can use linkedinUrl or email/workEmail/personalEmail inputs.",
236
- ],
237
- "lead_finder": [
238
- "Add more targeting keys as needed (jobFunctions, seniorities, keywords, excludeKeywords, skills).",
239
- ],
240
- "llm_enrichment": [
241
- "Recommended default for agent mode: provide instructions and let the API compile the runnable prompt.",
242
- "Generated agent-mode prompts always include company/requester context.",
243
- "Basic mode stays manual-prompt-first; write the prompt directly from the user goal/preferences.",
244
- "Runtime only injects values for placeholders the prompt explicitly references.",
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
- explicit_token
807
- or os.environ.get("SMARTTABLE_TOKEN")
808
- or os.environ.get("AUTOTOUCH_API_KEY")
809
- or os.environ.get("AUTOTOUCH_TOKEN")
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 cfg.get("api_key")
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: missing token. Pass --token, set SMARTTABLE_TOKEN/AUTOTOUCH_API_KEY, or run `autotouch auth login` / `autotouch auth set-key`.",
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 = _resolve_token(args.token, required=True)
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=args.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 = _resolve_token(args.token, required=True)
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=args.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 = _resolve_token(args.token, required=True)
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=args.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 = _resolve_token(args.token, required=True)
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=args.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 = _resolve_token(args.token, required=True)
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=args.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 = _resolve_token(args.token, required=True)
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=args.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 into local CLI config")
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")