tetra-cli 0.2.0__py3-none-any.whl

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. tetra_cli/__init__.py +6 -0
  2. tetra_cli/api_client/__init__.py +10 -0
  3. tetra_cli/api_client/client.py +173 -0
  4. tetra_cli/api_client/config.py +125 -0
  5. tetra_cli/api_client/operations/__init__.py +9 -0
  6. tetra_cli/api_client/operations/accounts.py +303 -0
  7. tetra_cli/api_client/operations/ai.py +278 -0
  8. tetra_cli/api_client/operations/analysis.py +190 -0
  9. tetra_cli/api_client/operations/api_keys.py +145 -0
  10. tetra_cli/api_client/operations/archive.py +114 -0
  11. tetra_cli/api_client/operations/awards.py +123 -0
  12. tetra_cli/api_client/operations/capacity.py +84 -0
  13. tetra_cli/api_client/operations/conversations.py +447 -0
  14. tetra_cli/api_client/operations/conversations_2.py +262 -0
  15. tetra_cli/api_client/operations/cosmetics.py +148 -0
  16. tetra_cli/api_client/operations/dashboard.py +282 -0
  17. tetra_cli/api_client/operations/data.py +250 -0
  18. tetra_cli/api_client/operations/events.py +734 -0
  19. tetra_cli/api_client/operations/gamification.py +470 -0
  20. tetra_cli/api_client/operations/goals.py +1144 -0
  21. tetra_cli/api_client/operations/groups.py +647 -0
  22. tetra_cli/api_client/operations/issues.py +198 -0
  23. tetra_cli/api_client/operations/offset.py +61 -0
  24. tetra_cli/api_client/operations/onboarding.py +284 -0
  25. tetra_cli/api_client/operations/outcome_schemas.py +292 -0
  26. tetra_cli/api_client/operations/peer_connections.py +243 -0
  27. tetra_cli/api_client/operations/plaid.py +329 -0
  28. tetra_cli/api_client/operations/reminders.py +273 -0
  29. tetra_cli/api_client/operations/scratches.py +280 -0
  30. tetra_cli/api_client/operations/skill_trees.py +160 -0
  31. tetra_cli/api_client/operations/social_2.py +560 -0
  32. tetra_cli/api_client/operations/social_3.py +618 -0
  33. tetra_cli/api_client/operations/social_4.py +527 -0
  34. tetra_cli/api_client/operations/strava.py +215 -0
  35. tetra_cli/api_client/operations/stripe.py +113 -0
  36. tetra_cli/api_client/operations/tags.py +488 -0
  37. tetra_cli/api_client/operations/values.py +867 -0
  38. tetra_cli/api_client/operations/values_2.py +584 -0
  39. tetra_cli/api_client/operations/watch.py +105 -0
  40. tetra_cli/api_client/operations/webhooks.py +50 -0
  41. tetra_cli/api_client/operations/xp.py +27 -0
  42. tetra_cli/cli/__init__.py +5 -0
  43. tetra_cli/cli/__main__.py +5 -0
  44. tetra_cli/cli/app.py +86 -0
  45. tetra_cli/cli/commands/__init__.py +1 -0
  46. tetra_cli/cli/commands/auth.py +201 -0
  47. tetra_cli/cli/commands/guide.py +8 -0
  48. tetra_cli/cli/commands/messages.py +161 -0
  49. tetra_cli/cli/commands/skill.py +71 -0
  50. tetra_cli/cli/context.py +13 -0
  51. tetra_cli/cli/generate.py +282 -0
  52. tetra_cli/cli/output.py +58 -0
  53. tetra_cli/mcp_gen.py +137 -0
  54. tetra_cli/ontology.py +70 -0
  55. tetra_cli/registry.py +118 -0
  56. tetra_cli/skill/SKILL.md +69 -0
  57. tetra_cli/skill/__init__.py +1 -0
  58. tetra_cli-0.2.0.dist-info/METADATA +140 -0
  59. tetra_cli-0.2.0.dist-info/RECORD +62 -0
  60. tetra_cli-0.2.0.dist-info/WHEEL +5 -0
  61. tetra_cli-0.2.0.dist-info/entry_points.txt +2 -0
  62. tetra_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,734 @@
1
+ """Pure async operations for the events API."""
2
+ from typing import Any
3
+
4
+ from tetra_cli.api_client import TetraClient
5
+ from tetra_cli.api_client.operations.tags import resolve_tag_uid
6
+ from tetra_cli.registry import arg, operation, opt
7
+
8
+
9
+ @operation(
10
+ cli="events list",
11
+ summary="List events with optional filtering and pagination.",
12
+ covers=[("GET", "/api/v1/events/")],
13
+ params={
14
+ "on_date": opt("--on-date", help="Filter to a specific date (YYYY-MM-DD)"),
15
+ "start_date": opt("--start", help="Range start, inclusive (YYYY-MM-DD). May be used alone."),
16
+ "end_date": opt("--end", help="Range end, inclusive (YYYY-MM-DD). May be used alone."),
17
+ "limit": opt("--limit", help="Maximum events to return"),
18
+ "offset": opt("--offset", help="Events to skip for pagination"),
19
+ "sort_order": opt("--sort-order", choices=["asc", "desc"],
20
+ help="Sort order (newest first = desc)"),
21
+ "goal": opt("--goal", help="Filter to a goal's full hierarchical name"),
22
+ "tag": opt("--tag", help="Filter to a tag (case-insensitive, hierarchy-expanded)"),
23
+ },
24
+ )
25
+ async def list_events(
26
+ client: TetraClient,
27
+ *,
28
+ on_date: str | None = None,
29
+ start_date: str | None = None,
30
+ end_date: str | None = None,
31
+ limit: int = 50,
32
+ offset: int = 0,
33
+ sort_order: str = "desc",
34
+ goal: str | None = None,
35
+ tag: str | None = None,
36
+ ) -> dict[str, Any]:
37
+ """List events with optional filtering and pagination.
38
+
39
+ Args:
40
+ client: Authenticated TetraClient
41
+ on_date: Filter to specific date (YYYY-MM-DD format)
42
+ start_date: Range start, inclusive (YYYY-MM-DD). May be given without
43
+ end_date to fetch everything on/after the date.
44
+ end_date: Range end, inclusive (YYYY-MM-DD). May be given without
45
+ start_date to fetch everything on/before the date.
46
+ limit: Maximum events to return (default 50)
47
+ offset: Number of events to skip for pagination
48
+ sort_order: 'desc' (newest first) or 'asc' (oldest first)
49
+ goal: Filter to events linked to this goal's full hierarchical name
50
+ (e.g. "Social > Trent"). Unknown names return an empty list.
51
+ tag: Filter to events tagged with this tag (case-insensitive exact
52
+ name). Tag hierarchy is expanded — a parent tag includes events
53
+ tagged with any descendant. Unknown names return an empty list.
54
+
55
+ Returns:
56
+ Dict with 'events' list and 'total' count
57
+ """
58
+ return await client.get(
59
+ "/api/v1/events/",
60
+ params={
61
+ "on_date": on_date,
62
+ "start_date": start_date,
63
+ "end_date": end_date,
64
+ "limit": limit,
65
+ "offset": offset,
66
+ "sort_order": sort_order,
67
+ "goal": goal,
68
+ "tag": tag,
69
+ },
70
+ )
71
+
72
+
73
+ @operation(
74
+ cli="events get",
75
+ summary="Get a single event by its UID.",
76
+ covers=[("GET", "/api/v1/events/{uid}")],
77
+ params={"uid": arg(help="Event UID")},
78
+ )
79
+ async def get_event(client: TetraClient, uid: str) -> dict[str, Any]:
80
+ """Get a single event by its UID.
81
+
82
+ Args:
83
+ client: Authenticated TetraClient
84
+ uid: The event's unique identifier
85
+
86
+ Returns:
87
+ Event dict
88
+ """
89
+ return await client.get(f"/api/v1/events/{uid}")
90
+
91
+
92
+ @operation(
93
+ cli="events create",
94
+ summary="Create a new event recording time or money spent.",
95
+ covers=[("POST", "/api/v1/events/")],
96
+ params={
97
+ "day": arg(help="Date in YYYY-MM-DD format"),
98
+ "goals": opt("--goal", repeatable=True, help="Goal name (repeatable)"),
99
+ "values": opt("--value", repeatable=True, help="Value name (repeatable)"),
100
+ "duration_minutes": opt("--duration-minutes",
101
+ help="Duration in minutes (converted to seconds)"),
102
+ "amount_dollars": opt("--amount-dollars",
103
+ help="Amount in dollars (converted to cents)"),
104
+ "description": opt("--description", help="What happened during this event"),
105
+ "feels": opt("--feels", min=-2, max=2, help="Mood rating from -2 to +2"),
106
+ "outcome": opt("--outcome", json=True,
107
+ help="Goal metric outcome as a JSON value"),
108
+ "tag_uids": opt("--tag-uid", repeatable=True, help="Tag UID (repeatable)"),
109
+ },
110
+ )
111
+ async def create_event(
112
+ client: TetraClient,
113
+ *,
114
+ day: str,
115
+ goals: list[str] | None = None,
116
+ values: list[str] | None = None,
117
+ duration_minutes: float | None = None,
118
+ amount_dollars: float | None = None,
119
+ description: str | None = None,
120
+ feels: int | None = None,
121
+ start: str | None = None,
122
+ end: str | None = None,
123
+ outcome: Any | None = None,
124
+ tag_uids: list[str] | None = None,
125
+ group_uid: str | None = None,
126
+ for_account_uid: str | None = None,
127
+ ) -> dict[str, Any]:
128
+ """Create a new event recording time or money spent.
129
+
130
+ Converts duration_minutes -> seconds and amount_dollars -> cents before
131
+ sending to the API, which stores all time in seconds and money in cents.
132
+
133
+ Args:
134
+ client: Authenticated TetraClient
135
+ day: Date in YYYY-MM-DD format (required)
136
+ goals: List of goal names (full hierarchical paths)
137
+ values: List of value names (full hierarchical paths)
138
+ duration_minutes: Duration in minutes; converted to seconds for API
139
+ amount_dollars: Money amount in dollars; converted to cents for API
140
+ description: What happened during this event
141
+ feels: Mood rating from -2 (terrible) to +2 (great)
142
+ start: Start time as ISO datetime
143
+ end: End time as ISO datetime
144
+ outcome: Goal metric outcome - number, list of numbers, or list of records
145
+ tag_uids: UIDs of tags to attach to the event
146
+
147
+ Returns:
148
+ Created event dict
149
+ """
150
+ body: dict[str, Any] = {"day": day}
151
+ if goals:
152
+ body["goals"] = goals
153
+ if values:
154
+ body["values"] = values
155
+ if duration_minutes is not None:
156
+ body["duration"] = int(duration_minutes * 60)
157
+ if amount_dollars is not None:
158
+ body["amount"] = int(amount_dollars * 100)
159
+ if description:
160
+ body["description"] = description
161
+ if feels is not None:
162
+ body["feels"] = feels
163
+ if start:
164
+ body["start"] = start
165
+ if end:
166
+ body["end"] = end
167
+ if outcome is not None:
168
+ body["outcome"] = outcome
169
+ if tag_uids:
170
+ body["tag_uids"] = tag_uids
171
+ # Creation context — pair (group_uid, for_account_uid) picks the
172
+ # shape A/B/C per docs/architecture/creation-context.md. Backend
173
+ # resolver validates the combination.
174
+ if group_uid is not None:
175
+ body["group_uid"] = group_uid
176
+ if for_account_uid is not None:
177
+ body["for_account_uid"] = for_account_uid
178
+ return await client.post("/api/v1/events/", json=body)
179
+
180
+
181
+ @operation(
182
+ cli="events update",
183
+ summary="Update an existing event. Only provided fields are changed.",
184
+ covers=[("PUT", "/api/v1/events/{uid}")],
185
+ params={
186
+ "uid": arg(help="Event UID"),
187
+ "goals": opt("--goal", repeatable=True,
188
+ help="Goal name (repeatable; replaces all)"),
189
+ "values": opt("--value", repeatable=True,
190
+ help="Value name (repeatable; replaces all)"),
191
+ "duration_minutes": opt("--duration-minutes",
192
+ help="Duration in minutes (converted to seconds)"),
193
+ "amount_dollars": opt("--amount-dollars",
194
+ help="Amount in dollars (converted to cents)"),
195
+ "feels": opt("--feels", min=-2, max=2, help="Mood rating from -2 to +2"),
196
+ "outcome": opt("--outcome", json=True,
197
+ help="Goal metric outcome as a JSON value"),
198
+ "tag_uids": opt("--tag-uid", repeatable=True,
199
+ help="Tag UID (repeatable; replaces all)"),
200
+ },
201
+ )
202
+ async def update_event(
203
+ client: TetraClient,
204
+ *,
205
+ uid: str,
206
+ day: str | None = None,
207
+ goals: list[str] | None = None,
208
+ values: list[str] | None = None,
209
+ duration_minutes: float | None = None,
210
+ amount_dollars: float | None = None,
211
+ description: str | None = None,
212
+ feels: int | None = None,
213
+ start: str | None = None,
214
+ end: str | None = None,
215
+ outcome: Any | None = None,
216
+ tag_uids: list[str] | None = None,
217
+ ) -> dict[str, Any]:
218
+ """Update an existing event. Only provided fields are changed.
219
+
220
+ Like create_event, converts duration_minutes -> seconds and
221
+ amount_dollars -> cents. Only fields explicitly passed (not None) are
222
+ included in the PATCH body so callers can update a single field cleanly.
223
+
224
+ Args:
225
+ client: Authenticated TetraClient
226
+ uid: The event's unique identifier (required)
227
+ day: New date in YYYY-MM-DD format
228
+ goals: New list of goal names (replaces all goals)
229
+ values: New list of value names (replaces all values)
230
+ duration_minutes: New duration in minutes
231
+ amount_dollars: New money amount in dollars
232
+ description: New description
233
+ feels: New mood rating (-2 to +2)
234
+ start: New start time as ISO datetime
235
+ end: New end time as ISO datetime
236
+ outcome: New goal metric outcome
237
+ tag_uids: New tag UIDs (replaces all tags)
238
+
239
+ Returns:
240
+ Updated event dict
241
+ """
242
+ body: dict[str, Any] = {}
243
+ if day is not None:
244
+ body["day"] = day
245
+ if goals is not None:
246
+ body["goals"] = goals
247
+ if values is not None:
248
+ body["values"] = values
249
+ if duration_minutes is not None:
250
+ body["duration"] = int(duration_minutes * 60)
251
+ if amount_dollars is not None:
252
+ body["amount"] = int(amount_dollars * 100)
253
+ if description is not None:
254
+ body["description"] = description
255
+ if feels is not None:
256
+ body["feels"] = feels
257
+ if start is not None:
258
+ body["start"] = start
259
+ if end is not None:
260
+ body["end"] = end
261
+ if outcome is not None:
262
+ body["outcome"] = outcome
263
+ if tag_uids is not None:
264
+ body["tag_uids"] = tag_uids
265
+ return await client.put(f"/api/v1/events/{uid}", json=body)
266
+
267
+
268
+ @operation(
269
+ cli="events delete",
270
+ summary="Delete an event permanently.",
271
+ covers=[("DELETE", "/api/v1/events/{uid}")],
272
+ params={"uid": arg(help="Event UID")},
273
+ )
274
+ async def delete_event(client: TetraClient, uid: str) -> dict[str, Any]:
275
+ """Delete an event permanently.
276
+
277
+ Args:
278
+ client: Authenticated TetraClient
279
+ uid: The event's unique identifier
280
+
281
+ Returns:
282
+ Deletion confirmation dict
283
+ """
284
+ return await client.delete(f"/api/v1/events/{uid}")
285
+
286
+
287
+ @operation(
288
+ cli="events bulk-edit",
289
+ summary="Edit multiple events at once (UID-based).",
290
+ covers=[("POST", "/api/v1/events/bulk-edit")],
291
+ params={
292
+ "event_uids": opt("--uid", repeatable=True,
293
+ help="Event UID to edit (repeatable, required)"),
294
+ "set_goals": opt("--set-goal", repeatable=True,
295
+ help="Replace goals on all selected events"),
296
+ "set_values": opt("--set-value", repeatable=True,
297
+ help="Replace values on all selected events"),
298
+ "set_feels": opt("--set-feels", min=-2, max=2,
299
+ help="Set feels on all selected events"),
300
+ "add_tag_uids": opt("--add-tag-uid", repeatable=True,
301
+ help="Add these tag UIDs to all selected events"),
302
+ "remove_tag_uids": opt("--remove-tag-uid", repeatable=True,
303
+ help="Remove these tag UIDs from all selected events"),
304
+ "add_tags": opt("--add-tag", repeatable=True,
305
+ help="Add tag by NAME (resolved to a UID; repeatable)"),
306
+ "remove_tags": opt("--remove-tag", repeatable=True,
307
+ help="Remove tag by NAME (resolved to a UID; repeatable)"),
308
+ },
309
+ )
310
+ async def bulk_edit_events(
311
+ client: TetraClient,
312
+ *,
313
+ event_uids: list[str],
314
+ set_goals: list[str] | None = None,
315
+ set_values: list[str] | None = None,
316
+ set_feels: int | None = None,
317
+ add_tag_uids: list[str] | None = None,
318
+ remove_tag_uids: list[str] | None = None,
319
+ add_tags: list[str] | None = None,
320
+ remove_tags: list[str] | None = None,
321
+ ) -> dict[str, Any]:
322
+ """Edit multiple events at once. Useful for moving events between goals.
323
+
324
+ Builds a patch dict from only the provided fields, so partial updates work
325
+ without overwriting fields the caller didn't touch.
326
+
327
+ Tags may be supplied by UID (``add_tag_uids`` / ``remove_tag_uids``) or by
328
+ NAME (``add_tags`` / ``remove_tags``). Names are resolved to UIDs inside
329
+ the op via the shared ``resolve_tag_uid`` helper, so both the CLI and the
330
+ MCP surface inherit name→uid resolution. The resolved names are appended to
331
+ any UIDs already passed.
332
+
333
+ Args:
334
+ client: Authenticated TetraClient
335
+ event_uids: List of event UIDs to edit (required)
336
+ set_goals: Replace goals on all selected events
337
+ set_values: Replace values on all selected events
338
+ set_feels: Set feels on all selected events
339
+ add_tag_uids: Add these tag UIDs to all selected events
340
+ remove_tag_uids: Remove these tag UIDs from all selected events
341
+ add_tags: Tag names to add (each resolved to a UID via /tags/search)
342
+ remove_tags: Tag names to remove (each resolved to a UID)
343
+
344
+ Returns:
345
+ Dict with 'updated_count'
346
+
347
+ Raises:
348
+ TetraClientError: If a tag name in add_tags/remove_tags has no exact
349
+ (case-insensitive) match.
350
+ """
351
+ resolved_add: list[str] = list(add_tag_uids or [])
352
+ for tag_name in add_tags or []:
353
+ resolved_add.append(await resolve_tag_uid(client, tag_name))
354
+ resolved_remove: list[str] = list(remove_tag_uids or [])
355
+ for tag_name in remove_tags or []:
356
+ resolved_remove.append(await resolve_tag_uid(client, tag_name))
357
+
358
+ patch: dict[str, Any] = {}
359
+ if set_goals is not None:
360
+ patch["goals"] = set_goals
361
+ if set_values is not None:
362
+ patch["values"] = set_values
363
+ if set_feels is not None:
364
+ patch["feels"] = set_feels
365
+ if resolved_add:
366
+ patch["add_tag_uids"] = resolved_add
367
+ if resolved_remove:
368
+ patch["remove_tag_uids"] = resolved_remove
369
+ return await client.post(
370
+ "/api/v1/events/bulk-edit", json={"uids": event_uids, "patch": patch}
371
+ )
372
+
373
+
374
+ @operation(
375
+ cli="events calendar",
376
+ summary="Get events organized by day for a calendar month view.",
377
+ covers=[("GET", "/api/v1/events/calendar/{year}/{month}")],
378
+ params={
379
+ "year": arg(help="Calendar year (e.g. 2026)"),
380
+ "month": arg(help="Calendar month (1-12)"),
381
+ },
382
+ )
383
+ async def get_calendar(client: TetraClient, *, year: int, month: int) -> dict[str, Any]:
384
+ """Get events organized by day for a calendar month view.
385
+
386
+ Args:
387
+ client: Authenticated TetraClient
388
+ year: Calendar year (e.g., 2026)
389
+ month: Calendar month (1-12)
390
+
391
+ Returns:
392
+ Dict with 'days' mapping date strings to lists of events
393
+ """
394
+ return await client.get(f"/api/v1/events/calendar/{year}/{month}")
395
+
396
+
397
+ @operation(
398
+ cli="events get-batch",
399
+ summary="Fetch multiple events by UID in a single request.",
400
+ covers=[("POST", "/api/v1/events/batch")],
401
+ params={
402
+ "uids": opt("--uid", repeatable=True,
403
+ help="Event UID to fetch (repeatable, required)"),
404
+ },
405
+ )
406
+ async def get_events_batch(
407
+ client: TetraClient, *, uids: list[str],
408
+ ) -> dict[str, Any]:
409
+ """Fetch multiple events by UID in a single request.
410
+
411
+ Performance helper for bulk reads — fetches up to 100 events in one
412
+ call instead of N round-trips.
413
+
414
+ Args:
415
+ client: Authenticated TetraClient
416
+ uids: Event UIDs to fetch (max 100)
417
+
418
+ Returns:
419
+ Dict with 'events' (found) and 'not_found' (missing UIDs) lists
420
+ """
421
+ return await client.post("/api/v1/events/batch", json={"uids": uids})
422
+
423
+
424
+ @operation(
425
+ cli="events bulk-delete",
426
+ summary="Delete multiple events in one batch operation.",
427
+ covers=[("POST", "/api/v1/events/bulk-delete")],
428
+ params={
429
+ "uids": opt("--uid", repeatable=True,
430
+ help="Event UID to delete (repeatable, required)"),
431
+ "reason": opt("--reason", help="Reason recorded on the soft delete"),
432
+ },
433
+ )
434
+ async def bulk_delete_events(
435
+ client: TetraClient,
436
+ *,
437
+ uids: list[str],
438
+ reason: str | None = None,
439
+ ) -> dict[str, Any]:
440
+ """Delete multiple events in a single batch operation.
441
+
442
+ Auto-generated events are not deletable and come back in ``failed_uids``;
443
+ the rest of the batch still succeeds.
444
+
445
+ Args:
446
+ client: Authenticated TetraClient
447
+ uids: Event UIDs to delete (required)
448
+ reason: Optional reason recorded on the soft delete
449
+
450
+ Returns:
451
+ Dict with 'deleted_count', 'deleted_uids', and 'failed_uids'
452
+ """
453
+ body: dict[str, Any] = {"uids": uids}
454
+ if reason is not None:
455
+ body["reason"] = reason
456
+ return await client.post("/api/v1/events/bulk-delete", json=body)
457
+
458
+
459
+ @operation(
460
+ cli="events duplicates",
461
+ summary="Find groups of duplicate events sharing identical field values.",
462
+ covers=[("GET", "/api/v1/events/duplicates")],
463
+ )
464
+ async def find_duplicates(client: TetraClient) -> dict[str, Any]:
465
+ """Find groups of duplicate events sharing identical field values.
466
+
467
+ Args:
468
+ client: Authenticated TetraClient
469
+
470
+ Returns:
471
+ Dict with 'duplicate_count', 'duplicate_groups', and a 'duplicates'
472
+ list of {fingerprint, uids, count}
473
+ """
474
+ return await client.get("/api/v1/events/duplicates")
475
+
476
+
477
+ @operation(
478
+ cli="events delete-duplicates",
479
+ summary="Delete duplicate events, keeping one of each group.",
480
+ covers=[("POST", "/api/v1/events/duplicates/delete")],
481
+ )
482
+ async def delete_duplicates(client: TetraClient) -> dict[str, Any]:
483
+ """Delete duplicate events, keeping one of each duplicate group.
484
+
485
+ For each group of identical events the lowest UID is kept and the rest
486
+ are deleted.
487
+
488
+ Args:
489
+ client: Authenticated TetraClient
490
+
491
+ Returns:
492
+ Dict with 'deleted_count', 'kept_count', and 'deleted_uids'
493
+ """
494
+ return await client.post("/api/v1/events/duplicates/delete")
495
+
496
+
497
+ @operation(
498
+ cli="events generate",
499
+ summary="Generate (or preview) PLANNED events for unlogged time slots.",
500
+ covers=[("POST", "/api/v1/events/generate")],
501
+ params={
502
+ "start_date": arg(help="Range start date (YYYY-MM-DD)"),
503
+ "end_date": arg(help="Range end date (YYYY-MM-DD)"),
504
+ "persist": opt("--persist",
505
+ help="Save generated events (omit for preview only)"),
506
+ },
507
+ )
508
+ async def generate_events(
509
+ client: TetraClient,
510
+ *,
511
+ start_date: str,
512
+ end_date: str,
513
+ persist: bool = False,
514
+ ) -> dict[str, Any]:
515
+ """Generate system PLANNED events for unlogged time slots in a date range.
516
+
517
+ PLANNED events are derived from Value time_rules for slots not already
518
+ covered by RECORD or user PLANNED events. With ``persist=False`` (default)
519
+ the result is a preview; ``persist=True`` saves them.
520
+
521
+ Args:
522
+ client: Authenticated TetraClient
523
+ start_date: Range start date (YYYY-MM-DD)
524
+ end_date: Range end date (YYYY-MM-DD)
525
+ persist: True to save events, False to preview only
526
+
527
+ Returns:
528
+ Dict with 'generated_count' and 'events'
529
+ """
530
+ return await client.post(
531
+ "/api/v1/events/generate",
532
+ json={
533
+ "start_date": start_date,
534
+ "end_date": end_date,
535
+ "persist": persist,
536
+ },
537
+ )
538
+
539
+
540
+ @operation(
541
+ cli="events auto-accept",
542
+ summary="Auto-accept PLANNED events for a past date.",
543
+ covers=[("POST", "/api/v1/events/auto-accept")],
544
+ params={"date": arg(help="Date to auto-accept (YYYY-MM-DD)")},
545
+ )
546
+ async def auto_accept_events(
547
+ client: TetraClient, *, date: str,
548
+ ) -> dict[str, Any]:
549
+ """Auto-accept PLANNED events for a past date.
550
+
551
+ PLANNED events with no RECORD overlap become EXECUTED; those that overlap
552
+ a RECORD become NOT_EXECUTED.
553
+
554
+ Args:
555
+ client: Authenticated TetraClient
556
+ date: Target date (YYYY-MM-DD)
557
+
558
+ Returns:
559
+ Dict with 'executed_count', 'not_executed_count', 'executed_uids',
560
+ and 'not_executed_uids'
561
+ """
562
+ return await client.post(
563
+ "/api/v1/events/auto-accept", json={"date": date},
564
+ )
565
+
566
+
567
+ @operation(
568
+ cli="events accept",
569
+ summary="Accept a PLANNED event, marking it EXECUTED.",
570
+ covers=[("POST", "/api/v1/events/{uid}/accept")],
571
+ params={"uid": arg(help="PLANNED event UID")},
572
+ )
573
+ async def accept_planned_event(client: TetraClient, uid: str) -> dict[str, Any]:
574
+ """Accept a PLANNED event, marking it as EXECUTED.
575
+
576
+ Confirms the planned event happened as scheduled.
577
+
578
+ Args:
579
+ client: Authenticated TetraClient
580
+ uid: The PLANNED event's UID
581
+
582
+ Returns:
583
+ The updated event dict
584
+ """
585
+ return await client.post(f"/api/v1/events/{uid}/accept")
586
+
587
+
588
+ @operation(
589
+ cli="events reject",
590
+ summary="Reject a PLANNED event, marking it NOT_EXECUTED.",
591
+ covers=[("POST", "/api/v1/events/{uid}/reject")],
592
+ params={"uid": arg(help="PLANNED event UID")},
593
+ )
594
+ async def reject_planned_event(client: TetraClient, uid: str) -> dict[str, Any]:
595
+ """Reject a PLANNED event, marking it as NOT_EXECUTED.
596
+
597
+ Records that the planned event did not happen; the event is kept for
598
+ planning-accuracy metrics.
599
+
600
+ Args:
601
+ client: Authenticated TetraClient
602
+ uid: The PLANNED event's UID
603
+
604
+ Returns:
605
+ The updated event dict
606
+ """
607
+ return await client.post(f"/api/v1/events/{uid}/reject")
608
+
609
+
610
+ @operation(
611
+ cli="events promote-to-goal",
612
+ summary="Promote a free-form event into a new record-shaped goal.",
613
+ covers=[("POST", "/api/v1/events/{uid}/promote-to-goal")],
614
+ params={
615
+ "uid": arg(help="Event UID to promote"),
616
+ "goal_name": opt("--goal-name", help="Name for the new goal (required)"),
617
+ },
618
+ )
619
+ async def promote_event_to_goal(
620
+ client: TetraClient, *, uid: str, goal_name: str,
621
+ ) -> dict[str, Any]:
622
+ """Promote a free-form event into a new record-shaped goal.
623
+
624
+ Creates a ``record``-shaped goal whose outcome field names mirror the
625
+ event's outcome columns, links the event to it, and canonicalizes the
626
+ event's outcome keys to the goal's fields.
627
+
628
+ Args:
629
+ client: Authenticated TetraClient
630
+ uid: UID of the event to promote (must have free-form columns)
631
+ goal_name: Name for the new goal
632
+
633
+ Returns:
634
+ Dict with 'goal' and 'event'
635
+ """
636
+ return await client.post(
637
+ f"/api/v1/events/{uid}/promote-to-goal",
638
+ json={"goal_name": goal_name},
639
+ )
640
+
641
+
642
+ @operation(
643
+ cli="events add-with",
644
+ summary="Tag a co-attendee on an event.",
645
+ covers=[("POST", "/api/v1/events/{event_uid}/with")],
646
+ params={
647
+ "event_uid": arg(help="Event UID to tag the co-attendee on"),
648
+ "with_account_uid": opt("--with-account-uid",
649
+ help="Account UID of the co-attendee (required)"),
650
+ },
651
+ )
652
+ async def add_with_tag(
653
+ client: TetraClient, *, event_uid: str, with_account_uid: str,
654
+ ) -> dict[str, Any]:
655
+ """Tag a co-attendee on an event ("with" feature).
656
+
657
+ If the pair is already connected the tag is active immediately; otherwise
658
+ a peer-connection request is created and the tag is pending_connect.
659
+
660
+ Args:
661
+ client: Authenticated TetraClient
662
+ event_uid: UID of the event to tag
663
+ with_account_uid: Account UID of the co-attendee
664
+
665
+ Returns:
666
+ The created with-tag dict
667
+ """
668
+ return await client.post(
669
+ f"/api/v1/events/{event_uid}/with",
670
+ json={"with_account_uid": with_account_uid},
671
+ )
672
+
673
+
674
+ @operation(
675
+ cli="events remove-with",
676
+ summary="Remove a co-attendee with-tag from an event.",
677
+ covers=[("DELETE", "/api/v1/events/with/{tag_uid}")],
678
+ params={"tag_uid": arg(help="With-tag UID to remove")},
679
+ )
680
+ async def remove_with_tag(client: TetraClient, tag_uid: str) -> dict[str, Any]:
681
+ """Remove a co-attendee with-tag.
682
+
683
+ Either the event owner or the tagged account may remove it. Removing does
684
+ not delete an already-created mirror event.
685
+
686
+ Args:
687
+ client: Authenticated TetraClient
688
+ tag_uid: The with-tag's UID
689
+
690
+ Returns:
691
+ Empty dict (the endpoint returns 204 No Content)
692
+ """
693
+ return await client.delete(f"/api/v1/events/with/{tag_uid}")
694
+
695
+
696
+ @operation(
697
+ cli="events with-inbox",
698
+ summary="List pending with-tags awaiting a mirror event.",
699
+ covers=[("GET", "/api/v1/events/with/inbox")],
700
+ )
701
+ async def list_with_inbox(client: TetraClient) -> dict[str, Any]:
702
+ """List pending with-tags where the caller is the tagged co-attendee.
703
+
704
+ These are tags that have no mirror event yet — candidates for
705
+ ``create_with_mirror``.
706
+
707
+ Args:
708
+ client: Authenticated TetraClient
709
+
710
+ Returns:
711
+ Dict with a 'tags' list
712
+ """
713
+ return await client.get("/api/v1/events/with/inbox")
714
+
715
+
716
+ @operation(
717
+ cli="events mirror-with",
718
+ summary="Create the mirror event for an accepted with-tag.",
719
+ covers=[("POST", "/api/v1/events/with/{tag_uid}/mirror")],
720
+ params={"tag_uid": arg(help="With-tag UID to mirror")},
721
+ )
722
+ async def create_with_mirror(client: TetraClient, tag_uid: str) -> dict[str, Any]:
723
+ """Create the mirror event for an accepted with-tag.
724
+
725
+ Idempotent — re-calling returns the existing mirror.
726
+
727
+ Args:
728
+ client: Authenticated TetraClient
729
+ tag_uid: The with-tag's UID
730
+
731
+ Returns:
732
+ Dict with 'mirror_event_uid', 'group_uid', and 'goal_uids'
733
+ """
734
+ return await client.post(f"/api/v1/events/with/{tag_uid}/mirror")