autotouch-cli 0.2.10__tar.gz → 0.2.12__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 (62) hide show
  1. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/PKG-INFO +37 -1
  2. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/autotouch_cli.egg-info/PKG-INFO +37 -1
  3. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/docs/research-table/reference/autotouch-cli-pypi.md +36 -0
  4. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/pyproject.toml +1 -1
  5. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/smart_table_cli.py +83 -15
  6. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/autotouch_cli.egg-info/SOURCES.txt +0 -0
  7. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  8. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/autotouch_cli.egg-info/entry_points.txt +0 -0
  9. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/autotouch_cli.egg-info/requires.txt +0 -0
  10. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/autotouch_cli.egg-info/top_level.txt +0 -0
  11. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/__init__.py +0 -0
  12. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/add_column_unique_index.py +0 -0
  13. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/attach_csv_import_leads_to_research_table.py +0 -0
  14. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/bundle_sequences_backend.py +0 -0
  15. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/check_agent_traces.py +0 -0
  16. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/check_column_mode.py +0 -0
  17. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/exit_terminal_leads_from_sequences.py +0 -0
  18. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/fetch_lead.py +0 -0
  19. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/fix_lead_titles_from_csv.py +0 -0
  20. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250106_add_column_position.py +0 -0
  21. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250108_fix_legacy_column_fields.py +0 -0
  22. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250109_add_user_fields_to_tables.py +0 -0
  23. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250117_add_call_logs_webhook_indexes.py +0 -0
  24. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250117_rename_call_logs_collection.py +0 -0
  25. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250119_create_leads_unique_email_index.py +0 -0
  26. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250123_add_filter_indexes.py +0 -0
  27. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250123_add_llm_responses_collection.py +0 -0
  28. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250128_migrate_user_ids_to_objectid.py +0 -0
  29. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250208_backfill_task_research_values.py +0 -0
  30. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250604_add_origin_indexes.py +0 -0
  31. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250608_cleanup_agent_metadata.py +0 -0
  32. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250608_rename_agent_metadata_to_metadata.py +0 -0
  33. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250922_add_activity_indexes.py +0 -0
  34. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250926_migrate_single_to_arrays.py +0 -0
  35. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250928_add_missing_timestamp_fields.py +0 -0
  36. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250929_add_task_join_indexes.py +0 -0
  37. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250929_add_task_join_indexes_safe.py +0 -0
  38. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20250929_create_shared_phone_cache.py +0 -0
  39. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20251007_add_rows_position_id_index.py +0 -0
  40. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py +0 -0
  41. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20260113_normalize_table_filter_operators.py +0 -0
  42. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20260113_set_user_permissions_user_admin.py +0 -0
  43. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/20260204_sync_lead_owner_from_tasks.py +0 -0
  44. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/migrate_org_user_credits.py +0 -0
  45. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/set_default_lead_status.py +0 -0
  46. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/migrations/update_lead_owner_from_tasks.py +0 -0
  47. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/reassign_sequence_owner.py +0 -0
  48. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/run_sidecar_orchestrator_demo.py +0 -0
  49. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/test_crm_company_policy.py +0 -0
  50. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/test_sequences_instantly_e2e.py +0 -0
  51. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/test_sequences_personal_e2e.py +0 -0
  52. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/test_task_error_logger.py +0 -0
  53. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/scripts/verify_azurite_voicemail.py +0 -0
  54. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/setup.cfg +0 -0
  55. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_contactout_custom.py +0 -0
  56. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_contactout_integration.py +0 -0
  57. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_contactout_multi_titles.py +0 -0
  58. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_contactout_pipeline.py +0 -0
  59. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_contactout_simple.py +0 -0
  60. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_contactout_v2_bulk.py +0 -0
  61. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/tests/test_lead_required_fields.py +0 -0
  62. {autotouch_cli-0.2.10 → autotouch_cli-0.2.12}/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.10
3
+ Version: 0.2.12
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -145,6 +145,19 @@ Notes:
145
145
  - Email/phone enrichment does not require creating/running `add_to_crm`.
146
146
  - For `add_to_crm`, required mapping keys are `linkedinUrl` and `companyDomain`.
147
147
 
148
+ ## Recommended ICP buyer discovery pattern
149
+
150
+ Use the dedicated guide:
151
+
152
+ - `docs/research-table/guides/icp-buyer-discovery.md`
153
+
154
+ This keeps quickstart short while documenting the full strategy:
155
+
156
+ - when to use `lead_finder` vs agent research,
157
+ - responsibility-based targeting (not exact title matching),
158
+ - one-person-per-row output contract,
159
+ - JSON split + downstream enrichment chaining.
160
+
148
161
  ## Create basic/manual fields (text, number, date, etc.)
149
162
 
150
163
  Use `kind=manual` for non-enrichment columns.
@@ -281,6 +294,10 @@ autotouch columns run-next \
281
294
 
282
295
  - Treat a run as started only when you receive `job_id`.
283
296
  - Treat a run as done only when `jobs get/watch` returns terminal status.
297
+ - `columns run --wait` now emits structured progress events and includes:
298
+ - `job_id`
299
+ - `job_status_url`
300
+ - `watch_command`
284
301
 
285
302
  ```bash
286
303
  autotouch jobs get --job-id <JOB_ID> --output json
@@ -293,6 +310,25 @@ Terminal statuses:
293
310
  - `error`
294
311
  - `cancelled`
295
312
 
313
+ Non-terminal statuses:
314
+ - `queued`
315
+ - `distributing`
316
+ - `processing`
317
+
318
+ Recommended status fields to inspect while waiting:
319
+ - `processed_rows`
320
+ - `error_rows`
321
+ - `skipped_rows`
322
+ - `total_rows`
323
+ - `pending_batches`
324
+ - `terminal_reason`
325
+
326
+ CLI-protected failure statuses:
327
+ - `not_found`
328
+ - `unknown_not_found`
329
+
330
+ If either failure status appears, treat the run as unconfirmed/inconsistent, verify row state, and rerun.
331
+
296
332
  ## Troubleshooting
297
333
 
298
334
  1. Import appears to run but rows are missing:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.10
3
+ Version: 0.2.12
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -145,6 +145,19 @@ Notes:
145
145
  - Email/phone enrichment does not require creating/running `add_to_crm`.
146
146
  - For `add_to_crm`, required mapping keys are `linkedinUrl` and `companyDomain`.
147
147
 
148
+ ## Recommended ICP buyer discovery pattern
149
+
150
+ Use the dedicated guide:
151
+
152
+ - `docs/research-table/guides/icp-buyer-discovery.md`
153
+
154
+ This keeps quickstart short while documenting the full strategy:
155
+
156
+ - when to use `lead_finder` vs agent research,
157
+ - responsibility-based targeting (not exact title matching),
158
+ - one-person-per-row output contract,
159
+ - JSON split + downstream enrichment chaining.
160
+
148
161
  ## Create basic/manual fields (text, number, date, etc.)
149
162
 
150
163
  Use `kind=manual` for non-enrichment columns.
@@ -281,6 +294,10 @@ autotouch columns run-next \
281
294
 
282
295
  - Treat a run as started only when you receive `job_id`.
283
296
  - Treat a run as done only when `jobs get/watch` returns terminal status.
297
+ - `columns run --wait` now emits structured progress events and includes:
298
+ - `job_id`
299
+ - `job_status_url`
300
+ - `watch_command`
284
301
 
285
302
  ```bash
286
303
  autotouch jobs get --job-id <JOB_ID> --output json
@@ -293,6 +310,25 @@ Terminal statuses:
293
310
  - `error`
294
311
  - `cancelled`
295
312
 
313
+ Non-terminal statuses:
314
+ - `queued`
315
+ - `distributing`
316
+ - `processing`
317
+
318
+ Recommended status fields to inspect while waiting:
319
+ - `processed_rows`
320
+ - `error_rows`
321
+ - `skipped_rows`
322
+ - `total_rows`
323
+ - `pending_batches`
324
+ - `terminal_reason`
325
+
326
+ CLI-protected failure statuses:
327
+ - `not_found`
328
+ - `unknown_not_found`
329
+
330
+ If either failure status appears, treat the run as unconfirmed/inconsistent, verify row state, and rerun.
331
+
296
332
  ## Troubleshooting
297
333
 
298
334
  1. Import appears to run but rows are missing:
@@ -136,6 +136,19 @@ Notes:
136
136
  - Email/phone enrichment does not require creating/running `add_to_crm`.
137
137
  - For `add_to_crm`, required mapping keys are `linkedinUrl` and `companyDomain`.
138
138
 
139
+ ## Recommended ICP buyer discovery pattern
140
+
141
+ Use the dedicated guide:
142
+
143
+ - `docs/research-table/guides/icp-buyer-discovery.md`
144
+
145
+ This keeps quickstart short while documenting the full strategy:
146
+
147
+ - when to use `lead_finder` vs agent research,
148
+ - responsibility-based targeting (not exact title matching),
149
+ - one-person-per-row output contract,
150
+ - JSON split + downstream enrichment chaining.
151
+
139
152
  ## Create basic/manual fields (text, number, date, etc.)
140
153
 
141
154
  Use `kind=manual` for non-enrichment columns.
@@ -272,6 +285,10 @@ autotouch columns run-next \
272
285
 
273
286
  - Treat a run as started only when you receive `job_id`.
274
287
  - Treat a run as done only when `jobs get/watch` returns terminal status.
288
+ - `columns run --wait` now emits structured progress events and includes:
289
+ - `job_id`
290
+ - `job_status_url`
291
+ - `watch_command`
275
292
 
276
293
  ```bash
277
294
  autotouch jobs get --job-id <JOB_ID> --output json
@@ -284,6 +301,25 @@ Terminal statuses:
284
301
  - `error`
285
302
  - `cancelled`
286
303
 
304
+ Non-terminal statuses:
305
+ - `queued`
306
+ - `distributing`
307
+ - `processing`
308
+
309
+ Recommended status fields to inspect while waiting:
310
+ - `processed_rows`
311
+ - `error_rows`
312
+ - `skipped_rows`
313
+ - `total_rows`
314
+ - `pending_batches`
315
+ - `terminal_reason`
316
+
317
+ CLI-protected failure statuses:
318
+ - `not_found`
319
+ - `unknown_not_found`
320
+
321
+ If either failure status appears, treat the run as unconfirmed/inconsistent, verify row state, and rerun.
322
+
287
323
  ## Troubleshooting
288
324
 
289
325
  1. Import appears to run but rows are missing:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autotouch-cli"
7
- version = "0.2.10"
7
+ version = "0.2.12"
8
8
  description = "Autotouch Smart Table CLI"
9
9
  readme = "docs/research-table/reference/autotouch-cli-pypi.md"
10
10
  requires-python = ">=3.9"
@@ -1039,18 +1039,24 @@ def _execute_run_flow(
1039
1039
  output = run_data if context is None else {"context": context, "run": run_data}
1040
1040
  _print_json(output, compact=args.compact)
1041
1041
  sys.exit(1)
1042
+ job_id = str(job_id)
1043
+ status_url = f"{_api_url(args.base_url)}/api/bulk-jobs/{job_id}"
1044
+ watch_command = f"autotouch jobs watch --job-id {job_id}"
1042
1045
  if not args.quiet_wait:
1043
1046
  _print_json(
1044
1047
  {
1045
- "job_id": str(job_id),
1048
+ "event": "run.wait_started",
1049
+ "job_id": job_id,
1046
1050
  "status": "polling_started",
1051
+ "status_url": status_url,
1052
+ "watch_command": watch_command,
1047
1053
  "hint": "polling /api/bulk-jobs/{job_id}",
1048
1054
  },
1049
1055
  compact=args.compact,
1050
1056
  )
1051
1057
 
1052
1058
  poll_result = _poll_job(
1053
- job_id=str(job_id),
1059
+ job_id=job_id,
1054
1060
  base_url=args.base_url,
1055
1061
  token=token,
1056
1062
  use_x_api_key=args.use_x_api_key,
@@ -1064,10 +1070,17 @@ def _execute_run_flow(
1064
1070
  )
1065
1071
  final_job = poll_result.get("job") or {}
1066
1072
  timed_out = bool(poll_result.get("timed_out"))
1073
+ final_status = str((final_job or {}).get("status") or "").lower()
1067
1074
  output = {
1075
+ "event": "run.timed_out" if timed_out else "run.completed",
1068
1076
  "run": run_data,
1069
1077
  "estimate": estimate_data if args.show_estimate or args.max_credits is not None else None,
1078
+ "job": final_job,
1070
1079
  "final_job": final_job,
1080
+ "job_id": job_id,
1081
+ "job_status_url": status_url,
1082
+ "watch_command": watch_command,
1083
+ "status": final_status,
1071
1084
  "timed_out": timed_out,
1072
1085
  "polls": int(poll_result.get("polls") or 0),
1073
1086
  }
@@ -1077,18 +1090,45 @@ def _execute_run_flow(
1077
1090
 
1078
1091
  if timed_out:
1079
1092
  sys.exit(4)
1080
- final_status = str((final_job or {}).get("status") or "").lower()
1093
+ if final_status in {"not_found", "unknown_not_found"}:
1094
+ print(
1095
+ f"ERROR: job {job_id} disappeared before terminal status; verify row state and rerun if needed",
1096
+ file=sys.stderr,
1097
+ )
1098
+ sys.exit(1)
1081
1099
  if args.fail_on_error and final_status in {"error", "cancelled"}:
1082
1100
  sys.exit(1)
1083
1101
  if args.fail_on_partial and final_status == "partial":
1084
1102
  sys.exit(1)
1085
1103
  return
1086
1104
 
1105
+ job_id = run_data.get("job_id") or run_data.get("jobId")
1106
+ status_url = None
1107
+ watch_command = None
1108
+ if job_id:
1109
+ status_url = f"{_api_url(args.base_url)}/api/bulk-jobs/{job_id}"
1110
+ watch_command = f"autotouch jobs watch --job-id {job_id}"
1111
+
1087
1112
  output_any: Any = run_data
1088
1113
  if estimate_data is not None and args.show_estimate:
1089
1114
  output_any = {"estimate": estimate_data, "run": run_data}
1090
1115
  if context is not None:
1091
1116
  output_any = {"context": context, "result": output_any}
1117
+ if isinstance(output_any, dict):
1118
+ output_any = dict(output_any)
1119
+ output_any.setdefault("event", "run.queued")
1120
+ if job_id:
1121
+ output_any.setdefault("job_id", str(job_id))
1122
+ output_any["job_status_url"] = status_url
1123
+ output_any["watch_command"] = watch_command
1124
+ elif job_id:
1125
+ output_any = {
1126
+ "event": "run.queued",
1127
+ "job_id": str(job_id),
1128
+ "job_status_url": status_url,
1129
+ "watch_command": watch_command,
1130
+ "result": output_any,
1131
+ }
1092
1132
  _print_json(output_any, compact=args.compact)
1093
1133
 
1094
1134
 
@@ -1266,8 +1306,11 @@ def _poll_job(
1266
1306
  polls = 0
1267
1307
  heartbeat_every_polls = 5
1268
1308
  max_consecutive_poll_errors = 6
1309
+ max_not_found_grace_polls = max(3, int(20 // interval) + 1)
1269
1310
  consecutive_poll_errors = 0
1311
+ not_found_polls = 0
1270
1312
  last_status: Optional[str] = None
1313
+ last_job_doc: Optional[Dict[str, Any]] = None
1271
1314
  url = f"{_api_url(base_url)}/api/bulk-jobs/{job_id}"
1272
1315
  headers = _auth_headers(token, use_x_api_key=use_x_api_key)
1273
1316
 
@@ -1286,6 +1329,7 @@ def _poll_job(
1286
1329
  if print_updates and (consecutive_poll_errors == 1 or consecutive_poll_errors % 3 == 0):
1287
1330
  _print_json(
1288
1331
  {
1332
+ "event": "job.polling_error",
1289
1333
  "job_id": job_id,
1290
1334
  "status": "polling_error",
1291
1335
  "polls": polls,
@@ -1314,6 +1358,7 @@ def _poll_job(
1314
1358
  if print_updates and (consecutive_poll_errors == 1 or consecutive_poll_errors % 3 == 0):
1315
1359
  _print_json(
1316
1360
  {
1361
+ "event": "job.polling_error",
1317
1362
  "job_id": job_id,
1318
1363
  "status": "polling_error",
1319
1364
  "polls": polls,
@@ -1343,18 +1388,34 @@ def _poll_job(
1343
1388
 
1344
1389
  consecutive_poll_errors = 0
1345
1390
 
1346
- if response.status_code == 404 and not once:
1347
- job_doc = {
1348
- "job_id": job_id,
1349
- "status": "queued",
1350
- "provider": None,
1351
- "processed_rows": 0,
1352
- "total_rows": 0,
1353
- "error_rows": 0,
1354
- "credits_used": 0.0,
1355
- "billable": False,
1356
- "updated_at": None,
1357
- }
1391
+ if response.status_code == 404:
1392
+ not_found_polls += 1
1393
+ if not once and last_job_doc is None and not_found_polls <= max_not_found_grace_polls:
1394
+ job_doc = {
1395
+ "job_id": job_id,
1396
+ "status": "queued",
1397
+ "provider": None,
1398
+ "processed_rows": 0,
1399
+ "total_rows": 0,
1400
+ "error_rows": 0,
1401
+ "credits_used": 0.0,
1402
+ "billable": False,
1403
+ "updated_at": None,
1404
+ "not_found_polls": not_found_polls,
1405
+ }
1406
+ elif last_job_doc is not None:
1407
+ unknown_job = dict(last_job_doc)
1408
+ unknown_job["status"] = "unknown_not_found"
1409
+ unknown_job["not_found_polls"] = not_found_polls
1410
+ unknown_job["note"] = "job endpoint returned 404 before terminal status"
1411
+ return {"job": unknown_job, "polls": polls, "timed_out": False}
1412
+ else:
1413
+ missing_job = {
1414
+ "job_id": job_id,
1415
+ "status": "not_found",
1416
+ "not_found_polls": not_found_polls,
1417
+ }
1418
+ return {"job": missing_job, "polls": polls, "timed_out": False}
1358
1419
  else:
1359
1420
  content_type = response.headers.get("content-type", "").lower()
1360
1421
  if "application/json" in content_type:
@@ -1377,9 +1438,14 @@ def _poll_job(
1377
1438
  print(f"ERROR: unexpected bulk job response: {parsed_body}", file=sys.stderr)
1378
1439
  sys.exit(1)
1379
1440
  job_doc = parsed_body
1441
+ last_job_doc = job_doc
1442
+ not_found_polls = 0
1380
1443
 
1381
1444
  status = str(job_doc.get("status") or "").lower()
1382
1445
  snapshot = _job_snapshot(job_doc)
1446
+ snapshot["event"] = "job.progress"
1447
+ snapshot["terminal"] = status in TERMINAL_JOB_STATUSES
1448
+ snapshot["status_url"] = url
1383
1449
  snapshot["polls"] = polls
1384
1450
  snapshot["elapsed_seconds"] = int(max(0.0, time.monotonic() - started))
1385
1451
 
@@ -2545,8 +2611,10 @@ def cmd_jobs_watch(args: argparse.Namespace) -> None:
2545
2611
  return
2546
2612
 
2547
2613
  final_summary = {
2614
+ "event": "job.timed_out" if timed_out else "job.completed",
2548
2615
  "job_id": final_job.get("job_id") or final_job.get("jobId") or args.job_id,
2549
2616
  "status": final_job.get("status"),
2617
+ "job_status_url": f"{_api_url(args.base_url)}/api/bulk-jobs/{args.job_id}",
2550
2618
  "timed_out": timed_out,
2551
2619
  "polls": int(poll_result.get("polls") or 0),
2552
2620
  }
File without changes