aiwcli 0.9.7 → 0.10.0

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 (119) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +49 -18
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -1,444 +0,0 @@
1
- """SessionStart discovery utilities for context management.
2
-
3
- Provides functions for discovering contexts at session start and
4
- formatting output for Claude to display context choices.
5
-
6
- Used by:
7
- - SessionStart hook to show available contexts
8
- - Plan handoff flow to auto-continue implementation
9
- """
10
- from pathlib import Path
11
- from typing import List, Optional, Tuple
12
-
13
- from datetime import datetime
14
-
15
- from .context_manager import (
16
- Context,
17
- get_all_contexts,
18
- get_context_with_pending_plan,
19
- get_context_with_in_flight_work,
20
- )
21
- from .event_log import get_current_state, get_pending_tasks, Task
22
- from ..base.utils import parse_iso_timestamp
23
- from ..templates.formatters import get_status_icon, format_continuation_header, get_mode_display
24
-
25
-
26
- def discover_contexts_for_session(
27
- project_root: Path = None
28
- ) -> Tuple[List[Context], Optional[Context]]:
29
- """
30
- SessionStart discovery.
31
-
32
- Returns:
33
- Tuple of:
34
- - List of active contexts sorted by last_active (recent first)
35
- - Context with pending plan implementation (if any)
36
- """
37
- active_contexts = get_all_contexts(status="active", project_root=project_root)
38
- pending_plan_context = get_context_with_pending_plan(project_root)
39
-
40
- return active_contexts, pending_plan_context
41
-
42
-
43
- def get_in_flight_context(project_root: Path = None) -> Optional[Context]:
44
- """
45
- Get context with any in-flight work (plan, etc.).
46
-
47
- Priority order:
48
- 1. pending_implementation - plan ready for implementation
49
- 2. implementing - implementation in progress
50
- 3. planning - actively planning
51
-
52
- Args:
53
- project_root: Project root directory
54
-
55
- Returns:
56
- Context with in-flight work, or None
57
- """
58
- contexts = get_all_contexts(status="active", project_root=project_root)
59
-
60
- # Sort by in-flight priority
61
- priority_order = {
62
- "pending_implementation": 0,
63
- "implementing": 1,
64
- "planning": 2,
65
- "none": 99,
66
- }
67
-
68
- # Only auto-continue for high-priority modes (not "implementing", "planning" or "none")
69
- actionable_modes = {"pending_implementation"}
70
-
71
- in_flight_contexts = [
72
- c for c in contexts
73
- if c.in_flight and c.in_flight.mode in actionable_modes
74
- ]
75
-
76
- if not in_flight_contexts:
77
- return None
78
-
79
- # Return highest priority, with secondary sort by last_active (most recent) for determinism
80
- in_flight_contexts.sort(
81
- key=lambda c: (
82
- priority_order.get(c.in_flight.mode, 99),
83
- -(parse_iso_timestamp(c.last_active) or datetime.min).timestamp() if c.last_active else 0
84
- )
85
- )
86
- return in_flight_contexts[0]
87
-
88
-
89
- def format_context_list(contexts: List[Context]) -> str:
90
- """
91
- Format contexts for display to user in SessionStart.
92
-
93
- Shows context name, summary, status, and last activity time.
94
-
95
- Args:
96
- contexts: List of contexts to format
97
-
98
- Returns:
99
- Formatted markdown string for display
100
- """
101
- if not contexts:
102
- return "No active contexts found."
103
-
104
- lines = ["## Active Contexts\n"]
105
-
106
- for i, ctx in enumerate(contexts, 1):
107
- # Format last active time
108
- time_str = format_relative_time(ctx.last_active)
109
-
110
- # Build status indicator
111
- status_indicator = ""
112
- if ctx.in_flight and ctx.in_flight.mode != "none":
113
- mode_display = get_mode_display(ctx.in_flight.mode)
114
- if mode_display:
115
- status_indicator = f" {mode_display}"
116
-
117
- lines.append(f"**{i}. {ctx.id}**{status_indicator}")
118
- lines.append(f" {ctx.summary}")
119
- if ctx.method:
120
- lines.append(f" Method: {ctx.method} | Last active: {time_str}")
121
- else:
122
- lines.append(f" Last active: {time_str}")
123
- lines.append("")
124
-
125
- return "\n".join(lines)
126
-
127
-
128
- def format_pending_plan_continuation(context: Context) -> str:
129
- """
130
- Format output for plan handoff scenario.
131
-
132
- This is shown when SessionStart detects a context with
133
- plan.status = "pending_implementation". Provides Claude
134
- with instructions to continue implementation.
135
-
136
- Args:
137
- context: Context with pending plan implementation
138
-
139
- Returns:
140
- Formatted instructions for Claude
141
- """
142
- lines = [
143
- format_continuation_header("context", context.id),
144
- "",
145
- f"**Summary:** {context.summary}",
146
- "",
147
- ]
148
-
149
- # Add plan info
150
- if context.in_flight and context.in_flight.artifact_path:
151
- lines.append(f"**Plan pending implementation:**")
152
- lines.append(f"`{context.in_flight.artifact_path}`")
153
- lines.append("")
154
-
155
- # Add pending tasks if any
156
- tasks = get_pending_tasks(context.id)
157
- if tasks:
158
- lines.append("**Previous tasks:**")
159
- for task in tasks:
160
- status_icon = get_status_icon(task.status)
161
- lines.append(f" {status_icon} {task.subject}")
162
- lines.append("")
163
-
164
- lines.extend([
165
- "---",
166
- "",
167
- "**Instructions:**",
168
- "1. Read the plan file above",
169
- "2. Use TaskCreate to restore any pending tasks from the plan",
170
- "3. Begin implementing the approved plan",
171
- "",
172
- "The context has been loaded. You may begin implementation.",
173
- ])
174
-
175
- return "\n".join(lines)
176
-
177
-
178
- def format_implementation_continuation(context: Context) -> str:
179
- """
180
- Format output for ongoing implementation scenario.
181
-
182
- This is shown when SessionStart detects a context with
183
- in_flight.mode = "implementing".
184
-
185
- Args:
186
- context: Context with implementation in progress
187
-
188
- Returns:
189
- Formatted instructions for Claude
190
- """
191
- lines = [
192
- format_continuation_header("implementing", context.id),
193
- "",
194
- f"**Summary:** {context.summary}",
195
- "",
196
- ]
197
-
198
- # Add plan info
199
- if context.in_flight and context.in_flight.artifact_path:
200
- lines.append(f"**Plan being implemented:**")
201
- lines.append(f"`{context.in_flight.artifact_path}`")
202
- lines.append("")
203
-
204
- # Add pending tasks
205
- tasks = get_pending_tasks(context.id)
206
- if tasks:
207
- lines.append("**Pending tasks:**")
208
- for task in tasks:
209
- status_icon = get_status_icon(task.status)
210
- lines.append(f" {status_icon} {task.subject}")
211
- lines.append("")
212
-
213
- lines.extend([
214
- "---",
215
- "",
216
- "**Instructions:**",
217
- "1. Review the plan and pending tasks above",
218
- "2. Use TaskCreate to restore pending tasks",
219
- "3. Continue implementing",
220
- "",
221
- "The context has been loaded. You may continue.",
222
- ])
223
-
224
- return "\n".join(lines)
225
-
226
-
227
- def format_context_picker_prompt() -> str:
228
- """
229
- Format the prompt asking user which context to continue.
230
-
231
- Returns:
232
- Prompt string for user
233
- """
234
- return (
235
- "\nWhich context would you like to continue?\n"
236
- "(Say the name/number, or 'new' to start fresh)"
237
- )
238
-
239
-
240
- def format_ready_for_new_work() -> str:
241
- """
242
- Format output when no active contexts exist.
243
-
244
- Returns:
245
- Ready message for user
246
- """
247
- return "No active contexts. Ready for new work."
248
-
249
-
250
- def parse_context_choice_from_prompt(prompt: str, contexts: List[Context]) -> Optional[str]:
251
- """
252
- Parse context selection from user prompt.
253
-
254
- Looks for patterns like:
255
- - "continue feature-auth" or "resume feature-auth"
256
- - "1" or "2" (number selection)
257
- - Context ID mentioned in prompt
258
-
259
- Args:
260
- prompt: User's prompt text
261
- contexts: Available contexts to match against
262
-
263
- Returns:
264
- Context ID if match found, None otherwise
265
- """
266
- if not prompt or not contexts:
267
- return None
268
-
269
- prompt_lower = prompt.lower().strip()
270
-
271
- # Check for number selection (1, 2, 3, etc.)
272
- # Match single digit at start or "option 1", "number 1", etc.
273
- import re
274
- number_match = re.match(r'^(\d+)$', prompt_lower)
275
- if number_match:
276
- idx = int(number_match.group(1)) - 1 # 1-indexed
277
- if 0 <= idx < len(contexts):
278
- return contexts[idx].id
279
-
280
- # Check for "continue X" or "resume X" patterns
281
- continue_match = re.match(r'^(?:continue|resume|work on|back to)\s+(.+)$', prompt_lower)
282
- if continue_match:
283
- target = continue_match.group(1).strip()
284
- # Try to match against context IDs
285
- for ctx in contexts:
286
- if ctx.id.lower() == target or target in ctx.id.lower():
287
- return ctx.id
288
-
289
- # Check if any context ID appears in the prompt
290
- for ctx in contexts:
291
- if ctx.id.lower() in prompt_lower:
292
- return ctx.id
293
-
294
- return None
295
-
296
-
297
- def format_context_selection_required(contexts: List[Context]) -> str:
298
- """
299
- Format urgent picker prompt when multiple contexts require selection.
300
-
301
- Used by context enforcer hook when context cannot be auto-determined.
302
-
303
- Args:
304
- contexts: Available contexts to choose from
305
-
306
- Returns:
307
- Formatted system reminder with context choices
308
- """
309
- lines = [
310
- "## Context Selection Required",
311
- "",
312
- "Multiple active contexts exist. Please indicate which to continue:",
313
- "",
314
- ]
315
-
316
- for i, ctx in enumerate(contexts, 1):
317
- time_str = format_relative_time(ctx.last_active)
318
-
319
- # Add status indicator for in-flight work
320
- status = ""
321
- if ctx.in_flight and ctx.in_flight.mode != "none":
322
- mode_display = get_mode_display(ctx.in_flight.mode)
323
- if mode_display:
324
- status = f" {mode_display}"
325
-
326
- lines.append(f"{i}. **{ctx.id}**{status} - {ctx.summary} [{time_str}]")
327
-
328
- lines.extend([
329
- "",
330
- "Say the number/name, or describe your new work (a context will be created).",
331
- ])
332
-
333
- return "\n".join(lines)
334
-
335
-
336
- def format_active_context_reminder(context: Context) -> str:
337
- """
338
- Format system reminder for active context.
339
-
340
- Used by context enforcer hook to inject context awareness.
341
-
342
- Args:
343
- context: Active context
344
-
345
- Returns:
346
- Formatted system reminder
347
- """
348
- time_str = format_relative_time(context.last_active)
349
-
350
- # Build mode display
351
- mode_display = "Active"
352
- if context.in_flight and context.in_flight.mode != "none":
353
- # Get mode display and strip brackets for this usage
354
- mode_str = get_mode_display(context.in_flight.mode)
355
- if mode_str:
356
- # Remove brackets from "[Planning]" to get "Planning"
357
- mode_display = mode_str.strip("[]")
358
-
359
- lines = [
360
- f"## Active Context: {context.id}",
361
- "",
362
- f"**Summary:** {context.summary}",
363
- f"**Mode:** {mode_display}",
364
- f"**Last Active:** {time_str}",
365
- "",
366
- f'All work belongs to context "{context.id}".',
367
- "Tasks created with TaskCreate will be persisted to this context.",
368
- ]
369
-
370
- return "\n".join(lines)
371
-
372
-
373
- def format_context_created(context: Context) -> str:
374
- """
375
- Format notification that a new context was auto-created.
376
-
377
- Args:
378
- context: Newly created context
379
-
380
- Returns:
381
- Formatted system reminder
382
- """
383
- lines = [
384
- f"## Context Created: {context.id}",
385
- "",
386
- f"**Summary:** {context.summary}",
387
- "",
388
- "A new context has been created for this work.",
389
- "Tasks created with TaskCreate will be persisted to this context.",
390
- ]
391
-
392
- return "\n".join(lines)
393
-
394
-
395
- def format_relative_time(iso_timestamp: Optional[str]) -> str:
396
- """
397
- Format ISO timestamp as relative time string.
398
-
399
- Args:
400
- iso_timestamp: ISO format timestamp string
401
-
402
- Returns:
403
- Relative time string like "2 hours ago" or "yesterday"
404
- """
405
- if not iso_timestamp:
406
- return "unknown"
407
-
408
- dt = parse_iso_timestamp(iso_timestamp)
409
- if not dt:
410
- return iso_timestamp[:16] # Fallback: show date/time portion
411
-
412
- now = datetime.now()
413
-
414
- # Handle timezone-aware vs naive datetime comparison
415
- # If dt is timezone-aware, convert to naive for comparison
416
- if dt.tzinfo is not None:
417
- try:
418
- # Convert to local time and strip timezone
419
- dt = dt.replace(tzinfo=None)
420
- except Exception:
421
- return iso_timestamp[:16] # Fallback on error
422
-
423
- diff = now - dt
424
-
425
- if diff.days == 0:
426
- hours = diff.seconds // 3600
427
- if hours == 0:
428
- minutes = diff.seconds // 60
429
- if minutes == 0:
430
- return "just now"
431
- elif minutes == 1:
432
- return "1 minute ago"
433
- else:
434
- return f"{minutes} minutes ago"
435
- elif hours == 1:
436
- return "1 hour ago"
437
- else:
438
- return f"{hours} hours ago"
439
- elif diff.days == 1:
440
- return "yesterday"
441
- elif diff.days < 7:
442
- return f"{diff.days} days ago"
443
- else:
444
- return dt.strftime("%Y-%m-%d")