autotouch-cli 0.2.25__tar.gz → 0.2.26__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 (64) hide show
  1. {autotouch_cli-0.2.25/autotouch_cli.egg-info → autotouch_cli-0.2.26}/PKG-INFO +64 -2
  2. autotouch_cli-0.2.25/docs/research-table/reference/autotouch-cli.md → autotouch_cli-0.2.26/autotouch_cli.egg-info/PKG-INFO +72 -1
  3. autotouch_cli-0.2.25/PKG-INFO → autotouch_cli-0.2.26/docs/research-table/reference/autotouch-cli.md +63 -10
  4. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/pyproject.toml +1 -1
  5. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/smart_table_cli.py +370 -0
  6. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/autotouch_cli.egg-info/SOURCES.txt +0 -0
  7. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  8. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/autotouch_cli.egg-info/entry_points.txt +0 -0
  9. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/autotouch_cli.egg-info/requires.txt +0 -0
  10. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/autotouch_cli.egg-info/top_level.txt +0 -0
  11. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/__init__.py +0 -0
  12. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/add_column_unique_index.py +0 -0
  13. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/attach_csv_import_leads_to_research_table.py +0 -0
  14. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/bundle_sequences_backend.py +0 -0
  15. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/check_agent_traces.py +0 -0
  16. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/check_column_mode.py +0 -0
  17. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/exit_terminal_leads_from_sequences.py +0 -0
  18. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/fetch_lead.py +0 -0
  19. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/fix_lead_titles_from_csv.py +0 -0
  20. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250106_add_column_position.py +0 -0
  21. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250108_fix_legacy_column_fields.py +0 -0
  22. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250109_add_user_fields_to_tables.py +0 -0
  23. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250117_add_call_logs_webhook_indexes.py +0 -0
  24. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250117_rename_call_logs_collection.py +0 -0
  25. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250119_create_leads_unique_email_index.py +0 -0
  26. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250123_add_filter_indexes.py +0 -0
  27. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250123_add_llm_responses_collection.py +0 -0
  28. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250128_migrate_user_ids_to_objectid.py +0 -0
  29. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250208_backfill_task_research_values.py +0 -0
  30. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250604_add_origin_indexes.py +0 -0
  31. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250608_cleanup_agent_metadata.py +0 -0
  32. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250608_rename_agent_metadata_to_metadata.py +0 -0
  33. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250922_add_activity_indexes.py +0 -0
  34. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250926_migrate_single_to_arrays.py +0 -0
  35. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250928_add_missing_timestamp_fields.py +0 -0
  36. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250929_add_task_join_indexes.py +0 -0
  37. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250929_add_task_join_indexes_safe.py +0 -0
  38. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20250929_create_shared_phone_cache.py +0 -0
  39. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20251007_add_rows_position_id_index.py +0 -0
  40. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py +0 -0
  41. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20260113_normalize_table_filter_operators.py +0 -0
  42. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20260113_set_user_permissions_user_admin.py +0 -0
  43. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20260204_sync_lead_owner_from_tasks.py +0 -0
  44. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20260303_add_webhook_subscription_collections.py +0 -0
  45. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/20260305_force_formatter_autorun_on_source_update.py +0 -0
  46. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/migrate_org_user_credits.py +0 -0
  47. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/set_default_lead_status.py +0 -0
  48. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/migrations/update_lead_owner_from_tasks.py +0 -0
  49. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/reassign_sequence_owner.py +0 -0
  50. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/run_sidecar_orchestrator_demo.py +0 -0
  51. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/test_crm_company_policy.py +0 -0
  52. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/test_sequences_instantly_e2e.py +0 -0
  53. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/test_sequences_personal_e2e.py +0 -0
  54. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/test_task_error_logger.py +0 -0
  55. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/scripts/verify_azurite_voicemail.py +0 -0
  56. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/setup.cfg +0 -0
  57. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_contactout_custom.py +0 -0
  58. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_contactout_integration.py +0 -0
  59. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_contactout_multi_titles.py +0 -0
  60. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_contactout_pipeline.py +0 -0
  61. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_contactout_simple.py +0 -0
  62. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_contactout_v2_bulk.py +0 -0
  63. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/tests/test_lead_required_fields.py +0 -0
  64. {autotouch_cli-0.2.25 → autotouch_cli-0.2.26}/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.25
3
+ Version: 0.2.26
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -37,12 +37,13 @@ Use this order when you are orienting in the CLI:
37
37
  | verify whether a job is really done | `autotouch jobs get --job-id <JOB_ID>` |
38
38
  | create projections from JSON output | `autotouch columns projections` |
39
39
  | work with sequences/tasks | raw HTTP plus `docs/platform/external-workflows-api.md` |
40
+ | query/filter CRM leads and attached research | `autotouch leads query` then `autotouch leads research` |
40
41
 
41
42
  ### Operating model
42
43
 
43
44
  - This file is the full CLI reference and the package readme published to PyPI.
44
45
  - The installed package gives you CLI entrypoints and package metadata; do not assume there is a separate installed docs directory.
45
- - Research-table APIs are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
46
+ - Research-table APIs and read-only leads queries are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
46
47
  - For async operations, backend bulk-job state is authoritative; local terminal output is only a convenience layer.
47
48
  - For staged or cost-sensitive runs, estimate first and prefer filtered scopes plus `firstN` or `run-next`.
48
49
 
@@ -128,6 +129,12 @@ Notes:
128
129
  | `POST /api/tables/{table_id}/columns/{column_id}/stop` | `autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>` |
129
130
  | `GET /api/bulk-jobs` | `autotouch jobs list --table-id <TABLE_ID> --column-id <COLUMN_ID> --limit 10` |
130
131
  | `GET /api/bulk-jobs/{job_id}` | `autotouch jobs get --job-id <JOB_ID>` |
132
+ | `POST /api/leads/query` | `autotouch leads query --filter-file lead-filter.json --limit 50` |
133
+ | `POST /api/leads/query/count` | `autotouch leads count --filter-file lead-filter.json` |
134
+ | `POST /api/leads/query/stats` | `autotouch leads stats --filter-file lead-filter.json` |
135
+ | `POST /api/leads/query/research` | `autotouch leads research --lead-id <LEAD_ID> --source-table-id <TABLE_ID>` |
136
+ | `GET /api/leads/filters/metadata` | `autotouch leads filters-metadata` |
137
+ | `GET /api/leads/research-tables/available` | `autotouch leads research-tables` |
131
138
  | `GET /api/tables/{table_id}/webhook` | `autotouch webhooks get --table-id <TABLE_ID>` |
132
139
  | `POST /api/tables/{table_id}/webhook` | `autotouch webhooks rotate --table-id <TABLE_ID>` |
133
140
  | `POST /api/webhooks/tables/{table_id}/ingest` | `autotouch webhooks ingest --table-id <TABLE_ID> --records-file records.json --webhook-token <WEBHOOK_TOKEN>` |
@@ -168,6 +175,61 @@ Signature controls for sequence payloads:
168
175
  - `steps[].appendSignature` (optional, step-level) overrides signature behavior for a specific email step.
169
176
  - Signature append is deterministic at send time when enabled for the step.
170
177
 
178
+ ## Leads API coverage
179
+
180
+ The CLI now exposes the public read-only leads endpoints under `autotouch leads`.
181
+
182
+ Use this pattern:
183
+
184
+ 1. `autotouch leads query` to page through leads, apply descriptor filters, and inspect `related_tables` / `research_summary`.
185
+ 2. `autotouch leads research` when you need actual research field values for specific lead IDs from one source table.
186
+ 3. `autotouch leads count` / `autotouch leads stats` when you need sizing before a downstream workflow.
187
+
188
+ Important contract notes:
189
+ - External callers should use `POST /api/leads/query` rather than the older `GET /api/leads` list route.
190
+ - `autotouch leads query` accepts the same descriptor payload shape as the backend:
191
+ - `filter`
192
+ - `search`
193
+ - `sort`
194
+ - `pagination`
195
+ - `scope`
196
+ - optional research context (`source_table_id`, `source_table_scope`, `related_tables_mode`)
197
+ - Research values are best treated as a follow-up call:
198
+ - `query` returns lead rows plus research linkage metadata
199
+ - `research` returns `research_data_by_id` keyed by lead id for one `source_table_id`
200
+
201
+ Examples:
202
+
203
+ ```bash
204
+ autotouch leads query \
205
+ --filter-json '{"root":{"kind":"rule","field":"status","operator":"eq","value":"new"}}' \
206
+ --limit 25
207
+ ```
208
+
209
+ ```bash
210
+ autotouch leads count \
211
+ --search "fintech" \
212
+ --scope org
213
+ ```
214
+
215
+ ```bash
216
+ autotouch leads stats \
217
+ --source-table-id <TABLE_ID> \
218
+ --source-table-scope company
219
+ ```
220
+
221
+ ```bash
222
+ autotouch leads research \
223
+ --lead-id <LEAD_ID_1> \
224
+ --lead-id <LEAD_ID_2> \
225
+ --source-table-id <TABLE_ID>
226
+ ```
227
+
228
+ ```bash
229
+ autotouch leads filters-metadata
230
+ autotouch leads research-tables
231
+ ```
232
+
171
233
  ## Bulk job status contract (authoritative run state)
172
234
 
173
235
  Use bulk jobs as the source of truth for run lifecycle:
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotouch-cli
3
+ Version: 0.2.26
4
+ Summary: Autotouch Smart Table CLI
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests>=2.31.0
8
+ Requires-Dist: python-dotenv>=1.0.0
9
+
1
10
  # Autotouch CLI Reference (`autotouch`)
2
11
 
3
12
  This page documents the installable CLI for the Smart Table developer API.
@@ -28,12 +37,13 @@ Use this order when you are orienting in the CLI:
28
37
  | verify whether a job is really done | `autotouch jobs get --job-id <JOB_ID>` |
29
38
  | create projections from JSON output | `autotouch columns projections` |
30
39
  | work with sequences/tasks | raw HTTP plus `docs/platform/external-workflows-api.md` |
40
+ | query/filter CRM leads and attached research | `autotouch leads query` then `autotouch leads research` |
31
41
 
32
42
  ### Operating model
33
43
 
34
44
  - This file is the full CLI reference and the package readme published to PyPI.
35
45
  - The installed package gives you CLI entrypoints and package metadata; do not assume there is a separate installed docs directory.
36
- - Research-table APIs are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
46
+ - Research-table APIs and read-only leads queries are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
37
47
  - For async operations, backend bulk-job state is authoritative; local terminal output is only a convenience layer.
38
48
  - For staged or cost-sensitive runs, estimate first and prefer filtered scopes plus `firstN` or `run-next`.
39
49
 
@@ -119,6 +129,12 @@ Notes:
119
129
  | `POST /api/tables/{table_id}/columns/{column_id}/stop` | `autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>` |
120
130
  | `GET /api/bulk-jobs` | `autotouch jobs list --table-id <TABLE_ID> --column-id <COLUMN_ID> --limit 10` |
121
131
  | `GET /api/bulk-jobs/{job_id}` | `autotouch jobs get --job-id <JOB_ID>` |
132
+ | `POST /api/leads/query` | `autotouch leads query --filter-file lead-filter.json --limit 50` |
133
+ | `POST /api/leads/query/count` | `autotouch leads count --filter-file lead-filter.json` |
134
+ | `POST /api/leads/query/stats` | `autotouch leads stats --filter-file lead-filter.json` |
135
+ | `POST /api/leads/query/research` | `autotouch leads research --lead-id <LEAD_ID> --source-table-id <TABLE_ID>` |
136
+ | `GET /api/leads/filters/metadata` | `autotouch leads filters-metadata` |
137
+ | `GET /api/leads/research-tables/available` | `autotouch leads research-tables` |
122
138
  | `GET /api/tables/{table_id}/webhook` | `autotouch webhooks get --table-id <TABLE_ID>` |
123
139
  | `POST /api/tables/{table_id}/webhook` | `autotouch webhooks rotate --table-id <TABLE_ID>` |
124
140
  | `POST /api/webhooks/tables/{table_id}/ingest` | `autotouch webhooks ingest --table-id <TABLE_ID> --records-file records.json --webhook-token <WEBHOOK_TOKEN>` |
@@ -159,6 +175,61 @@ Signature controls for sequence payloads:
159
175
  - `steps[].appendSignature` (optional, step-level) overrides signature behavior for a specific email step.
160
176
  - Signature append is deterministic at send time when enabled for the step.
161
177
 
178
+ ## Leads API coverage
179
+
180
+ The CLI now exposes the public read-only leads endpoints under `autotouch leads`.
181
+
182
+ Use this pattern:
183
+
184
+ 1. `autotouch leads query` to page through leads, apply descriptor filters, and inspect `related_tables` / `research_summary`.
185
+ 2. `autotouch leads research` when you need actual research field values for specific lead IDs from one source table.
186
+ 3. `autotouch leads count` / `autotouch leads stats` when you need sizing before a downstream workflow.
187
+
188
+ Important contract notes:
189
+ - External callers should use `POST /api/leads/query` rather than the older `GET /api/leads` list route.
190
+ - `autotouch leads query` accepts the same descriptor payload shape as the backend:
191
+ - `filter`
192
+ - `search`
193
+ - `sort`
194
+ - `pagination`
195
+ - `scope`
196
+ - optional research context (`source_table_id`, `source_table_scope`, `related_tables_mode`)
197
+ - Research values are best treated as a follow-up call:
198
+ - `query` returns lead rows plus research linkage metadata
199
+ - `research` returns `research_data_by_id` keyed by lead id for one `source_table_id`
200
+
201
+ Examples:
202
+
203
+ ```bash
204
+ autotouch leads query \
205
+ --filter-json '{"root":{"kind":"rule","field":"status","operator":"eq","value":"new"}}' \
206
+ --limit 25
207
+ ```
208
+
209
+ ```bash
210
+ autotouch leads count \
211
+ --search "fintech" \
212
+ --scope org
213
+ ```
214
+
215
+ ```bash
216
+ autotouch leads stats \
217
+ --source-table-id <TABLE_ID> \
218
+ --source-table-scope company
219
+ ```
220
+
221
+ ```bash
222
+ autotouch leads research \
223
+ --lead-id <LEAD_ID_1> \
224
+ --lead-id <LEAD_ID_2> \
225
+ --source-table-id <TABLE_ID>
226
+ ```
227
+
228
+ ```bash
229
+ autotouch leads filters-metadata
230
+ autotouch leads research-tables
231
+ ```
232
+
162
233
  ## Bulk job status contract (authoritative run state)
163
234
 
164
235
  Use bulk jobs as the source of truth for run lifecycle:
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: autotouch-cli
3
- Version: 0.2.25
4
- Summary: Autotouch Smart Table CLI
5
- Requires-Python: >=3.9
6
- Description-Content-Type: text/markdown
7
- Requires-Dist: requests>=2.31.0
8
- Requires-Dist: python-dotenv>=1.0.0
9
-
10
1
  # Autotouch CLI Reference (`autotouch`)
11
2
 
12
3
  This page documents the installable CLI for the Smart Table developer API.
@@ -37,12 +28,13 @@ Use this order when you are orienting in the CLI:
37
28
  | verify whether a job is really done | `autotouch jobs get --job-id <JOB_ID>` |
38
29
  | create projections from JSON output | `autotouch columns projections` |
39
30
  | work with sequences/tasks | raw HTTP plus `docs/platform/external-workflows-api.md` |
31
+ | query/filter CRM leads and attached research | `autotouch leads query` then `autotouch leads research` |
40
32
 
41
33
  ### Operating model
42
34
 
43
35
  - This file is the full CLI reference and the package readme published to PyPI.
44
36
  - The installed package gives you CLI entrypoints and package metadata; do not assume there is a separate installed docs directory.
45
- - Research-table APIs are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
37
+ - Research-table APIs and read-only leads queries are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
46
38
  - For async operations, backend bulk-job state is authoritative; local terminal output is only a convenience layer.
47
39
  - For staged or cost-sensitive runs, estimate first and prefer filtered scopes plus `firstN` or `run-next`.
48
40
 
@@ -128,6 +120,12 @@ Notes:
128
120
  | `POST /api/tables/{table_id}/columns/{column_id}/stop` | `autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>` |
129
121
  | `GET /api/bulk-jobs` | `autotouch jobs list --table-id <TABLE_ID> --column-id <COLUMN_ID> --limit 10` |
130
122
  | `GET /api/bulk-jobs/{job_id}` | `autotouch jobs get --job-id <JOB_ID>` |
123
+ | `POST /api/leads/query` | `autotouch leads query --filter-file lead-filter.json --limit 50` |
124
+ | `POST /api/leads/query/count` | `autotouch leads count --filter-file lead-filter.json` |
125
+ | `POST /api/leads/query/stats` | `autotouch leads stats --filter-file lead-filter.json` |
126
+ | `POST /api/leads/query/research` | `autotouch leads research --lead-id <LEAD_ID> --source-table-id <TABLE_ID>` |
127
+ | `GET /api/leads/filters/metadata` | `autotouch leads filters-metadata` |
128
+ | `GET /api/leads/research-tables/available` | `autotouch leads research-tables` |
131
129
  | `GET /api/tables/{table_id}/webhook` | `autotouch webhooks get --table-id <TABLE_ID>` |
132
130
  | `POST /api/tables/{table_id}/webhook` | `autotouch webhooks rotate --table-id <TABLE_ID>` |
133
131
  | `POST /api/webhooks/tables/{table_id}/ingest` | `autotouch webhooks ingest --table-id <TABLE_ID> --records-file records.json --webhook-token <WEBHOOK_TOKEN>` |
@@ -168,6 +166,61 @@ Signature controls for sequence payloads:
168
166
  - `steps[].appendSignature` (optional, step-level) overrides signature behavior for a specific email step.
169
167
  - Signature append is deterministic at send time when enabled for the step.
170
168
 
169
+ ## Leads API coverage
170
+
171
+ The CLI now exposes the public read-only leads endpoints under `autotouch leads`.
172
+
173
+ Use this pattern:
174
+
175
+ 1. `autotouch leads query` to page through leads, apply descriptor filters, and inspect `related_tables` / `research_summary`.
176
+ 2. `autotouch leads research` when you need actual research field values for specific lead IDs from one source table.
177
+ 3. `autotouch leads count` / `autotouch leads stats` when you need sizing before a downstream workflow.
178
+
179
+ Important contract notes:
180
+ - External callers should use `POST /api/leads/query` rather than the older `GET /api/leads` list route.
181
+ - `autotouch leads query` accepts the same descriptor payload shape as the backend:
182
+ - `filter`
183
+ - `search`
184
+ - `sort`
185
+ - `pagination`
186
+ - `scope`
187
+ - optional research context (`source_table_id`, `source_table_scope`, `related_tables_mode`)
188
+ - Research values are best treated as a follow-up call:
189
+ - `query` returns lead rows plus research linkage metadata
190
+ - `research` returns `research_data_by_id` keyed by lead id for one `source_table_id`
191
+
192
+ Examples:
193
+
194
+ ```bash
195
+ autotouch leads query \
196
+ --filter-json '{"root":{"kind":"rule","field":"status","operator":"eq","value":"new"}}' \
197
+ --limit 25
198
+ ```
199
+
200
+ ```bash
201
+ autotouch leads count \
202
+ --search "fintech" \
203
+ --scope org
204
+ ```
205
+
206
+ ```bash
207
+ autotouch leads stats \
208
+ --source-table-id <TABLE_ID> \
209
+ --source-table-scope company
210
+ ```
211
+
212
+ ```bash
213
+ autotouch leads research \
214
+ --lead-id <LEAD_ID_1> \
215
+ --lead-id <LEAD_ID_2> \
216
+ --source-table-id <TABLE_ID>
217
+ ```
218
+
219
+ ```bash
220
+ autotouch leads filters-metadata
221
+ autotouch leads research-tables
222
+ ```
223
+
171
224
  ## Bulk job status contract (authoritative run state)
172
225
 
173
226
  Use bulk jobs as the source of truth for run lifecycle:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autotouch-cli"
7
- version = "0.2.25"
7
+ version = "0.2.26"
8
8
  description = "Autotouch Smart Table CLI"
9
9
  readme = "docs/research-table/reference/autotouch-cli.md"
10
10
  requires-python = ">=3.9"
@@ -998,6 +998,198 @@ def _normalize_run_payload(args: argparse.Namespace) -> Dict[str, Any]:
998
998
  return payload
999
999
 
1000
1000
 
1001
+ def _normalize_filter_payload(filter_payload: Any) -> Any:
1002
+ if filter_payload is None:
1003
+ return None
1004
+ if isinstance(filter_payload, dict):
1005
+ return filter_payload
1006
+ print("ERROR: leads filter payload must be a JSON object", file=sys.stderr)
1007
+ sys.exit(2)
1008
+
1009
+
1010
+ def _normalize_sort_payload(sort_payload: Any) -> Any:
1011
+ if sort_payload is None:
1012
+ return None
1013
+ if isinstance(sort_payload, list):
1014
+ return sort_payload
1015
+ if isinstance(sort_payload, dict):
1016
+ return [sort_payload]
1017
+ print("ERROR: leads sort payload must be a JSON object or array", file=sys.stderr)
1018
+ sys.exit(2)
1019
+
1020
+
1021
+ def _normalize_leads_query_payload(args: argparse.Namespace) -> Dict[str, Any]:
1022
+ explicit_payload = _load_json_input(
1023
+ inline_json=getattr(args, "data_json", None),
1024
+ file_path=getattr(args, "data_file", None),
1025
+ context="data",
1026
+ default=None,
1027
+ )
1028
+ if explicit_payload is not None:
1029
+ if not isinstance(explicit_payload, dict):
1030
+ print("ERROR: leads payload must be a JSON object", file=sys.stderr)
1031
+ sys.exit(2)
1032
+ return explicit_payload
1033
+
1034
+ payload: Dict[str, Any] = {}
1035
+
1036
+ filter_payload = _normalize_filter_payload(
1037
+ _load_json_input(
1038
+ inline_json=getattr(args, "filter_json", None),
1039
+ file_path=getattr(args, "filter_file", None),
1040
+ context="filter",
1041
+ default=None,
1042
+ )
1043
+ )
1044
+ if filter_payload is not None:
1045
+ payload["filter"] = filter_payload
1046
+
1047
+ sort_payload = _normalize_sort_payload(
1048
+ _load_json_input(
1049
+ inline_json=getattr(args, "sort_json", None),
1050
+ file_path=getattr(args, "sort_file", None),
1051
+ context="sort",
1052
+ default=None,
1053
+ )
1054
+ )
1055
+ if sort_payload is not None:
1056
+ payload["sort"] = sort_payload
1057
+
1058
+ search = str(getattr(args, "search", "") or "").strip()
1059
+ if search:
1060
+ payload["search"] = search
1061
+
1062
+ pagination: Dict[str, Any] = {}
1063
+ limit = getattr(args, "limit", None)
1064
+ if limit is not None:
1065
+ pagination["limit"] = int(limit)
1066
+ cursor = str(getattr(args, "cursor", "") or "").strip()
1067
+ if cursor:
1068
+ pagination["cursor"] = cursor
1069
+ if pagination:
1070
+ payload["pagination"] = pagination
1071
+
1072
+ scope = str(getattr(args, "scope", "") or "").strip()
1073
+ if scope:
1074
+ payload["scope"] = scope
1075
+
1076
+ source_table_id = str(getattr(args, "source_table_id", "") or "").strip()
1077
+ if source_table_id:
1078
+ payload["source_table_id"] = source_table_id
1079
+
1080
+ source_table_scope = str(getattr(args, "source_table_scope", "") or "").strip()
1081
+ if source_table_scope:
1082
+ payload["source_table_scope"] = source_table_scope
1083
+
1084
+ related_tables_mode = str(getattr(args, "related_tables_mode", "") or "").strip()
1085
+ if related_tables_mode:
1086
+ payload["related_tables_mode"] = related_tables_mode
1087
+
1088
+ if bool(getattr(args, "include_research_data", False)):
1089
+ payload["include_research_data"] = True
1090
+
1091
+ if bool(getattr(args, "include_placeholder_leads", False)):
1092
+ payload["include_placeholder_leads"] = True
1093
+
1094
+ return payload
1095
+
1096
+
1097
+ def _collect_cli_string_values(raw_values: Optional[List[str]]) -> List[str]:
1098
+ values: List[str] = []
1099
+ for raw in raw_values or []:
1100
+ for piece in str(raw).replace("\n", ",").split(","):
1101
+ normalized = str(piece or "").strip()
1102
+ if normalized:
1103
+ values.append(normalized)
1104
+ return values
1105
+
1106
+
1107
+ def _parse_string_list_payload(payload: Any, *, context: str, key: Optional[str] = None) -> List[str]:
1108
+ raw_values: List[Any]
1109
+ if isinstance(payload, list):
1110
+ raw_values = payload
1111
+ elif isinstance(payload, dict) and key and isinstance(payload.get(key), list):
1112
+ raw_values = payload.get(key) or []
1113
+ else:
1114
+ suffix = f" or object with '{key}' array" if key else ""
1115
+ print(f"ERROR: {context} must be a JSON array{suffix}", file=sys.stderr)
1116
+ sys.exit(2)
1117
+
1118
+ values: List[str] = []
1119
+ for item in raw_values:
1120
+ normalized = str(item or "").strip()
1121
+ if normalized:
1122
+ values.append(normalized)
1123
+ return values
1124
+
1125
+
1126
+ def _load_string_list_input(
1127
+ *,
1128
+ cli_values: Optional[List[str]],
1129
+ inline_json: Optional[str],
1130
+ file_path: Optional[str],
1131
+ context: str,
1132
+ key: Optional[str] = None,
1133
+ ) -> List[str]:
1134
+ values = _collect_cli_string_values(cli_values)
1135
+ payload = _load_json_input(
1136
+ inline_json=inline_json,
1137
+ file_path=file_path,
1138
+ context=context,
1139
+ default=None,
1140
+ )
1141
+ if payload is not None:
1142
+ values.extend(_parse_string_list_payload(payload, context=context, key=key))
1143
+ return _dedupe_keep_order(values)
1144
+
1145
+
1146
+ def _normalize_leads_research_payload(args: argparse.Namespace) -> Dict[str, Any]:
1147
+ explicit_payload = _load_json_input(
1148
+ inline_json=getattr(args, "data_json", None),
1149
+ file_path=getattr(args, "data_file", None),
1150
+ context="data",
1151
+ default=None,
1152
+ )
1153
+ if explicit_payload is not None:
1154
+ if not isinstance(explicit_payload, dict):
1155
+ print("ERROR: leads research payload must be a JSON object", file=sys.stderr)
1156
+ sys.exit(2)
1157
+ return explicit_payload
1158
+
1159
+ lead_ids = _load_string_list_input(
1160
+ cli_values=getattr(args, "lead_id", None),
1161
+ inline_json=getattr(args, "lead_ids_json", None),
1162
+ file_path=getattr(args, "lead_ids_file", None),
1163
+ context="lead_ids",
1164
+ key="lead_ids",
1165
+ )
1166
+ if not lead_ids:
1167
+ print("ERROR: provide at least one lead id", file=sys.stderr)
1168
+ sys.exit(2)
1169
+
1170
+ source_table_id = str(getattr(args, "source_table_id", "") or "").strip()
1171
+ if not source_table_id:
1172
+ print("ERROR: --source-table-id is required", file=sys.stderr)
1173
+ sys.exit(2)
1174
+
1175
+ payload: Dict[str, Any] = {
1176
+ "lead_ids": lead_ids,
1177
+ "source_table_id": source_table_id,
1178
+ }
1179
+
1180
+ field_ids = _load_string_list_input(
1181
+ cli_values=getattr(args, "field_id", None),
1182
+ inline_json=getattr(args, "field_ids_json", None),
1183
+ file_path=getattr(args, "field_ids_file", None),
1184
+ context="field_ids",
1185
+ key="field_ids",
1186
+ )
1187
+ if field_ids:
1188
+ payload["field_ids"] = field_ids
1189
+
1190
+ return payload
1191
+
1192
+
1001
1193
  def _resolve_column_key(
1002
1194
  *,
1003
1195
  table_id: str,
@@ -3786,6 +3978,98 @@ def cmd_jobs_watch(args: argparse.Namespace) -> None:
3786
3978
  sys.exit(1)
3787
3979
 
3788
3980
 
3981
+ def cmd_leads_query(args: argparse.Namespace) -> None:
3982
+ token = _resolve_token(args.token, required=True)
3983
+ payload = _normalize_leads_query_payload(args)
3984
+ data = _request_api(
3985
+ "POST",
3986
+ "/api/leads/query",
3987
+ base_url=args.base_url,
3988
+ token=token,
3989
+ use_x_api_key=args.use_x_api_key,
3990
+ payload=payload,
3991
+ timeout=args.timeout,
3992
+ verbose=args.verbose,
3993
+ )
3994
+ _print_json(data, compact=args.compact)
3995
+
3996
+
3997
+ def cmd_leads_count(args: argparse.Namespace) -> None:
3998
+ token = _resolve_token(args.token, required=True)
3999
+ payload = _normalize_leads_query_payload(args)
4000
+ data = _request_api(
4001
+ "POST",
4002
+ "/api/leads/query/count",
4003
+ base_url=args.base_url,
4004
+ token=token,
4005
+ use_x_api_key=args.use_x_api_key,
4006
+ payload=payload,
4007
+ timeout=args.timeout,
4008
+ verbose=args.verbose,
4009
+ )
4010
+ _print_json(data, compact=args.compact)
4011
+
4012
+
4013
+ def cmd_leads_stats(args: argparse.Namespace) -> None:
4014
+ token = _resolve_token(args.token, required=True)
4015
+ payload = _normalize_leads_query_payload(args)
4016
+ data = _request_api(
4017
+ "POST",
4018
+ "/api/leads/query/stats",
4019
+ base_url=args.base_url,
4020
+ token=token,
4021
+ use_x_api_key=args.use_x_api_key,
4022
+ payload=payload,
4023
+ timeout=args.timeout,
4024
+ verbose=args.verbose,
4025
+ )
4026
+ _print_json(data, compact=args.compact)
4027
+
4028
+
4029
+ def cmd_leads_research(args: argparse.Namespace) -> None:
4030
+ token = _resolve_token(args.token, required=True)
4031
+ payload = _normalize_leads_research_payload(args)
4032
+ data = _request_api(
4033
+ "POST",
4034
+ "/api/leads/query/research",
4035
+ base_url=args.base_url,
4036
+ token=token,
4037
+ use_x_api_key=args.use_x_api_key,
4038
+ payload=payload,
4039
+ timeout=args.timeout,
4040
+ verbose=args.verbose,
4041
+ )
4042
+ _print_json(data, compact=args.compact)
4043
+
4044
+
4045
+ def cmd_leads_filters_metadata(args: argparse.Namespace) -> None:
4046
+ token = _resolve_token(args.token, required=True)
4047
+ data = _request_api(
4048
+ "GET",
4049
+ "/api/leads/filters/metadata",
4050
+ base_url=args.base_url,
4051
+ token=token,
4052
+ use_x_api_key=args.use_x_api_key,
4053
+ timeout=args.timeout,
4054
+ verbose=args.verbose,
4055
+ )
4056
+ _print_json(data, compact=args.compact)
4057
+
4058
+
4059
+ def cmd_leads_research_tables(args: argparse.Namespace) -> None:
4060
+ token = _resolve_token(args.token, required=True)
4061
+ data = _request_api(
4062
+ "GET",
4063
+ "/api/leads/research-tables/available",
4064
+ base_url=args.base_url,
4065
+ token=token,
4066
+ use_x_api_key=args.use_x_api_key,
4067
+ timeout=args.timeout,
4068
+ verbose=args.verbose,
4069
+ )
4070
+ _print_json(data, compact=args.compact)
4071
+
4072
+
3789
4073
  def cmd_webhooks_get(args: argparse.Namespace) -> None:
3790
4074
  token = _resolve_token(args.token, required=True)
3791
4075
  data = _request_api(
@@ -4177,6 +4461,60 @@ def _add_run_execution_arguments(parser: argparse.ArgumentParser) -> None:
4177
4461
  parser.add_argument("--fail-on-partial", action="store_true", help="Exit non-zero when final status is partial")
4178
4462
 
4179
4463
 
4464
+ def _add_leads_query_arguments(parser: argparse.ArgumentParser) -> None:
4465
+ parser.add_argument("--data-json", help="Explicit leads query payload JSON")
4466
+ parser.add_argument("--data-file", help="Path to leads query payload JSON file")
4467
+ parser.add_argument("--filter-json", help="Filter descriptor JSON object")
4468
+ parser.add_argument("--filter-file", help="Path to filter descriptor JSON file")
4469
+ parser.add_argument("--sort-json", help="Sort spec JSON object/array")
4470
+ parser.add_argument("--sort-file", help="Path to sort spec JSON file")
4471
+ parser.add_argument("--search", help="Free-text lead search")
4472
+ parser.add_argument("--limit", type=int, help="Pagination limit")
4473
+ parser.add_argument("--cursor", help="Pagination cursor")
4474
+ parser.add_argument("--scope", choices=["org", "user"], help="Lead scope")
4475
+ parser.add_argument("--source-table-id", help="Research table id used for scope/research context")
4476
+ parser.add_argument(
4477
+ "--source-table-scope",
4478
+ choices=["company", "lead"],
4479
+ help="Research scope resolution mode",
4480
+ )
4481
+ parser.add_argument(
4482
+ "--related-tables-mode",
4483
+ choices=["linked", "direct", "none"],
4484
+ help="How related_tables should be populated",
4485
+ )
4486
+ parser.add_argument(
4487
+ "--include-research-data",
4488
+ action="store_true",
4489
+ help="Inline research_data when --source-table-id is provided",
4490
+ )
4491
+ parser.add_argument(
4492
+ "--include-placeholder-leads",
4493
+ action="store_true",
4494
+ help="Include placeholder/incomplete lead records",
4495
+ )
4496
+
4497
+
4498
+ def _add_leads_research_arguments(parser: argparse.ArgumentParser) -> None:
4499
+ parser.add_argument("--data-json", help="Explicit leads research payload JSON")
4500
+ parser.add_argument("--data-file", help="Path to leads research payload JSON file")
4501
+ parser.add_argument(
4502
+ "--lead-id",
4503
+ action="append",
4504
+ help="Lead id (repeatable; comma-separated also supported)",
4505
+ )
4506
+ parser.add_argument("--lead-ids-json", help="JSON array or object with lead_ids[]")
4507
+ parser.add_argument("--lead-ids-file", help="Path to JSON file with lead_ids[]")
4508
+ parser.add_argument("--source-table-id", help="Research table id")
4509
+ parser.add_argument(
4510
+ "--field-id",
4511
+ action="append",
4512
+ help="Research field/column id to include (repeatable; comma-separated also supported)",
4513
+ )
4514
+ parser.add_argument("--field-ids-json", help="JSON array or object with field_ids[]")
4515
+ parser.add_argument("--field-ids-file", help="Path to JSON file with field_ids[]")
4516
+
4517
+
4180
4518
  def build_parser() -> argparse.ArgumentParser:
4181
4519
  p = argparse.ArgumentParser(prog="autotouch", description="Smart Table CLI")
4182
4520
  sub = p.add_subparsers(dest="cmd", required=True)
@@ -4543,6 +4881,38 @@ def build_parser() -> argparse.ArgumentParser:
4543
4881
  pcre.add_argument("--compact", action="store_true", help="Print compact JSON")
4544
4882
  pcre.set_defaults(func=cmd_columns_recipe)
4545
4883
 
4884
+ # leads
4885
+ pl = sub.add_parser("leads", help="Read-only leads query operations")
4886
+ leads_sub = pl.add_subparsers(dest="leads_cmd", required=True)
4887
+
4888
+ plq = leads_sub.add_parser("query", help="Query/filter leads with descriptor payloads")
4889
+ _add_leads_query_arguments(plq)
4890
+ _add_api_common_arguments(plq)
4891
+ plq.set_defaults(func=cmd_leads_query)
4892
+
4893
+ plc = leads_sub.add_parser("count", help="Count leads matching a query payload")
4894
+ _add_leads_query_arguments(plc)
4895
+ _add_api_common_arguments(plc)
4896
+ plc.set_defaults(func=cmd_leads_count)
4897
+
4898
+ pls = leads_sub.add_parser("stats", help="Aggregate stats for leads matching a query payload")
4899
+ _add_leads_query_arguments(pls)
4900
+ _add_api_common_arguments(pls)
4901
+ pls.set_defaults(func=cmd_leads_stats)
4902
+
4903
+ plr = leads_sub.add_parser("research", help="Fetch research row data for specific lead ids")
4904
+ _add_leads_research_arguments(plr)
4905
+ _add_api_common_arguments(plr)
4906
+ plr.set_defaults(func=cmd_leads_research)
4907
+
4908
+ plf = leads_sub.add_parser("filters-metadata", help="List filterable lead fields and research columns")
4909
+ _add_api_common_arguments(plf)
4910
+ plf.set_defaults(func=cmd_leads_filters_metadata)
4911
+
4912
+ plrt = leads_sub.add_parser("research-tables", help="List research tables available for lead lookups")
4913
+ _add_api_common_arguments(plrt)
4914
+ plrt.set_defaults(func=cmd_leads_research_tables)
4915
+
4546
4916
  # jobs
4547
4917
  pj = sub.add_parser("jobs", help="Bulk job operations")
4548
4918
  jobs_sub = pj.add_subparsers(dest="jobs_cmd", required=True)
File without changes