htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__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.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2263 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +812 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1020 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +509 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3356 -492
- htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1584 -0
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +318 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +496 -79
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +473 -6
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +637 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +344 -29
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bootstrap utilities for hook scripts.
|
|
3
|
+
|
|
4
|
+
Centralizes environment setup and project directory resolution used by all hooks.
|
|
5
|
+
Handles both development (src/python) and installed (package) modes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_project_dir(cwd: str | None = None) -> str:
|
|
16
|
+
"""Resolve the project directory with sensible fallbacks.
|
|
17
|
+
|
|
18
|
+
Hierarchy:
|
|
19
|
+
1. CLAUDE_PROJECT_DIR environment variable (set by Claude Code)
|
|
20
|
+
2. Git repository root (via git rev-parse --show-toplevel)
|
|
21
|
+
3. Current working directory (or provided cwd)
|
|
22
|
+
|
|
23
|
+
This supports running hooks in multiple contexts:
|
|
24
|
+
- Within a Claude Code session
|
|
25
|
+
- In git repositories
|
|
26
|
+
- In arbitrary directories
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
cwd: Starting directory for git search. Defaults to os.getcwd().
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Absolute path to the project directory.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
No exceptions - always returns a valid path.
|
|
36
|
+
"""
|
|
37
|
+
# First priority: Claude's explicit project directory
|
|
38
|
+
env_dir = os.environ.get("CLAUDE_PROJECT_DIR")
|
|
39
|
+
if env_dir:
|
|
40
|
+
return env_dir
|
|
41
|
+
|
|
42
|
+
# Second priority: Git repository root
|
|
43
|
+
start_dir = cwd or os.getcwd()
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
cwd=start_dir,
|
|
50
|
+
timeout=5,
|
|
51
|
+
)
|
|
52
|
+
if result.returncode == 0:
|
|
53
|
+
return result.stdout.strip()
|
|
54
|
+
except Exception:
|
|
55
|
+
# Git not available or not a repo - continue to fallback
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
# Final fallback: current working directory
|
|
59
|
+
return start_dir
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bootstrap_pythonpath(project_dir: str) -> None:
|
|
63
|
+
"""Bootstrap Python path for htmlgraph imports.
|
|
64
|
+
|
|
65
|
+
Handles two common deployment modes:
|
|
66
|
+
1. Development: Running inside htmlgraph repository (src/python exists)
|
|
67
|
+
2. Installed: Running where htmlgraph is installed as a package (do nothing)
|
|
68
|
+
|
|
69
|
+
This allows hooks to work correctly whether htmlgraph is:
|
|
70
|
+
- Being developed locally (add src/python to path)
|
|
71
|
+
- Installed in a virtual environment (already in path)
|
|
72
|
+
- Installed globally (already in path)
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
project_dir: Project directory from resolve_project_dir().
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
None (modifies sys.path in-place).
|
|
79
|
+
|
|
80
|
+
Side Effects:
|
|
81
|
+
- Modifies sys.path to ensure htmlgraph is importable
|
|
82
|
+
- Adds .venv/lib/pythonX.Y/site-packages if virtual environment exists
|
|
83
|
+
- Adds src/python if in htmlgraph repository
|
|
84
|
+
"""
|
|
85
|
+
project_path = Path(project_dir)
|
|
86
|
+
|
|
87
|
+
# First, try to use local virtual environment if it exists
|
|
88
|
+
venv = project_path / ".venv"
|
|
89
|
+
if venv.exists():
|
|
90
|
+
pyver = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
91
|
+
candidates = [
|
|
92
|
+
venv / "lib" / pyver / "site-packages", # macOS/Linux
|
|
93
|
+
venv / "Lib" / "site-packages", # Windows
|
|
94
|
+
]
|
|
95
|
+
for candidate in candidates:
|
|
96
|
+
if candidate.exists():
|
|
97
|
+
sys.path.insert(0, str(candidate))
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
# Then, add src/python if this is the htmlgraph repository itself
|
|
101
|
+
repo_src = project_path / "src" / "python"
|
|
102
|
+
if repo_src.exists():
|
|
103
|
+
sys.path.insert(0, str(repo_src))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_graph_dir(cwd: str | None = None) -> Path:
|
|
107
|
+
"""Get the .htmlgraph directory path, creating it if necessary.
|
|
108
|
+
|
|
109
|
+
The .htmlgraph directory is the root for all HtmlGraph tracking:
|
|
110
|
+
- .htmlgraph/sessions/ - Session HTML files
|
|
111
|
+
- .htmlgraph/features/ - Feature tracking
|
|
112
|
+
- .htmlgraph/events/ - Event JSON files
|
|
113
|
+
- .htmlgraph/htmlgraph.db - SQLite database
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
cwd: Starting directory for project resolution. Defaults to os.getcwd().
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Path to the .htmlgraph directory (guaranteed to exist).
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
OSError: If directory creation fails (e.g., permission denied).
|
|
123
|
+
"""
|
|
124
|
+
project_dir = resolve_project_dir(cwd)
|
|
125
|
+
graph_dir = Path(project_dir) / ".htmlgraph"
|
|
126
|
+
graph_dir.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
return graph_dir
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def init_logger(name: str) -> logging.Logger:
|
|
131
|
+
"""Initialize a logger with standardized configuration.
|
|
132
|
+
|
|
133
|
+
Sets up a logger for hook scripts with:
|
|
134
|
+
- Consistent format across all hooks
|
|
135
|
+
- basicConfig applied only once (subsequent calls are ignored)
|
|
136
|
+
- Named logger returned (can be used for filtering)
|
|
137
|
+
|
|
138
|
+
Format: "[TIMESTAMP] [LEVEL] [logger_name] message"
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
name: Logger name (typically __name__ from calling module).
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
logging.Logger instance configured and ready to use.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
```python
|
|
148
|
+
logger = init_logger(__name__)
|
|
149
|
+
logger.info("Hook started")
|
|
150
|
+
logger.error("Something went wrong")
|
|
151
|
+
```
|
|
152
|
+
"""
|
|
153
|
+
# Configure basicConfig only once (subsequent calls are no-ops)
|
|
154
|
+
logging.basicConfig(
|
|
155
|
+
level=logging.INFO,
|
|
156
|
+
format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
|
|
157
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Return named logger for this module
|
|
161
|
+
return logging.getLogger(name)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = [
|
|
165
|
+
"resolve_project_dir",
|
|
166
|
+
"bootstrap_pythonpath",
|
|
167
|
+
"get_graph_dir",
|
|
168
|
+
"init_logger",
|
|
169
|
+
]
|
|
@@ -87,7 +87,7 @@ class CIGSPreToolEnforcer:
|
|
|
87
87
|
|
|
88
88
|
return graph_dir
|
|
89
89
|
|
|
90
|
-
def enforce(self, tool: str, params: dict) -> dict[str, Any]:
|
|
90
|
+
def enforce(self, tool: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
91
91
|
"""
|
|
92
92
|
Enforce CIGS delegation rules with escalating guidance.
|
|
93
93
|
|
|
@@ -223,7 +223,7 @@ class CIGSPreToolEnforcer:
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
def _is_sdk_operation(self, tool: str, params: dict) -> bool:
|
|
226
|
+
def _is_sdk_operation(self, tool: str, params: dict[str, Any]) -> bool:
|
|
227
227
|
"""Check if operation is an SDK operation (always allowed)."""
|
|
228
228
|
if tool != "Bash":
|
|
229
229
|
return False
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concurrent Session Detection and Formatting.
|
|
3
|
+
|
|
4
|
+
Provides utilities to detect other active sessions and format them
|
|
5
|
+
for injection into the orchestrator's context at session start.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_concurrent_sessions(
|
|
15
|
+
db: HtmlGraphDB,
|
|
16
|
+
current_session_id: str,
|
|
17
|
+
minutes: int = 30,
|
|
18
|
+
) -> list[dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
Get other sessions that are currently active.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
db: Database connection
|
|
24
|
+
current_session_id: Current session to exclude
|
|
25
|
+
minutes: Look back window for activity
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of concurrent session dicts with id, agent_id, last_user_query, etc.
|
|
29
|
+
"""
|
|
30
|
+
if not db.connection:
|
|
31
|
+
db.connect()
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
35
|
+
# Use datetime format that matches database (without timezone)
|
|
36
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(minutes=minutes)).strftime(
|
|
37
|
+
"%Y-%m-%d %H:%M:%S"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
cursor.execute(
|
|
41
|
+
"""
|
|
42
|
+
SELECT
|
|
43
|
+
session_id as id,
|
|
44
|
+
agent_assigned as agent_id,
|
|
45
|
+
created_at,
|
|
46
|
+
status,
|
|
47
|
+
(SELECT input_summary FROM agent_events
|
|
48
|
+
WHERE session_id = sessions.session_id
|
|
49
|
+
ORDER BY timestamp DESC LIMIT 1) as last_user_query,
|
|
50
|
+
(SELECT timestamp FROM agent_events
|
|
51
|
+
WHERE session_id = sessions.session_id
|
|
52
|
+
ORDER BY timestamp DESC LIMIT 1) as last_user_query_at
|
|
53
|
+
FROM sessions
|
|
54
|
+
WHERE status = 'active'
|
|
55
|
+
AND session_id != ?
|
|
56
|
+
AND created_at > ?
|
|
57
|
+
ORDER BY created_at DESC
|
|
58
|
+
""",
|
|
59
|
+
(current_session_id, cutoff),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
rows = cursor.fetchall()
|
|
63
|
+
return [dict(row) for row in rows]
|
|
64
|
+
except Exception: # pragma: no cover
|
|
65
|
+
# Gracefully handle database errors
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_concurrent_sessions_markdown(sessions: list[dict[str, Any]]) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Format concurrent sessions as markdown for context injection.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
sessions: List of session dicts from get_concurrent_sessions
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Markdown formatted string for system prompt injection
|
|
78
|
+
"""
|
|
79
|
+
if not sessions:
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
lines = ["## Concurrent Sessions (Active Now)", ""]
|
|
83
|
+
|
|
84
|
+
for session in sessions:
|
|
85
|
+
session_id = session.get("id", "unknown")
|
|
86
|
+
session_id = session_id[:12] if len(session_id) > 12 else session_id
|
|
87
|
+
agent = session.get("agent_id", "unknown")
|
|
88
|
+
query = session.get("last_user_query", "No recent query")
|
|
89
|
+
last_active = session.get("last_user_query_at")
|
|
90
|
+
|
|
91
|
+
# Calculate time ago
|
|
92
|
+
time_ago = "unknown"
|
|
93
|
+
if last_active:
|
|
94
|
+
try:
|
|
95
|
+
last_dt = datetime.fromisoformat(
|
|
96
|
+
last_active.replace("Z", "+00:00")
|
|
97
|
+
if isinstance(last_active, str)
|
|
98
|
+
else last_active
|
|
99
|
+
)
|
|
100
|
+
delta = datetime.now(timezone.utc) - last_dt
|
|
101
|
+
if delta.total_seconds() < 60:
|
|
102
|
+
time_ago = "just now"
|
|
103
|
+
elif delta.total_seconds() < 3600:
|
|
104
|
+
time_ago = f"{int(delta.total_seconds() // 60)} min ago"
|
|
105
|
+
else:
|
|
106
|
+
time_ago = f"{int(delta.total_seconds() // 3600)} hours ago"
|
|
107
|
+
except (ValueError, TypeError, AttributeError):
|
|
108
|
+
time_ago = "unknown"
|
|
109
|
+
|
|
110
|
+
# Truncate query for display
|
|
111
|
+
query_display = (
|
|
112
|
+
query[:50] + "..." if query and len(query) > 50 else (query or "Unknown")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
lines.append(f'- **{session_id}** ({agent}): "{query_display}" - {time_ago}')
|
|
116
|
+
|
|
117
|
+
lines.append("")
|
|
118
|
+
lines.append("*Coordinate with concurrent sessions to avoid duplicate work.*")
|
|
119
|
+
lines.append("")
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_recent_completed_sessions(
|
|
125
|
+
db: HtmlGraphDB,
|
|
126
|
+
hours: int = 24,
|
|
127
|
+
limit: int = 5,
|
|
128
|
+
) -> list[dict[str, Any]]:
|
|
129
|
+
"""
|
|
130
|
+
Get recently completed sessions for handoff context.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
db: Database connection
|
|
134
|
+
hours: Look back window
|
|
135
|
+
limit: Maximum sessions to return
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of recently completed session dicts
|
|
139
|
+
"""
|
|
140
|
+
if not db.connection:
|
|
141
|
+
db.connect()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
145
|
+
# Use datetime format that matches database (without timezone)
|
|
146
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime(
|
|
147
|
+
"%Y-%m-%d %H:%M:%S"
|
|
148
|
+
)
|
|
149
|
+
cursor.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT session_id as id, agent_assigned as agent_id, created_at as started_at,
|
|
152
|
+
completed_at, total_events,
|
|
153
|
+
(SELECT input_summary FROM agent_events
|
|
154
|
+
WHERE session_id = sessions.session_id
|
|
155
|
+
ORDER BY timestamp DESC LIMIT 1) as last_user_query
|
|
156
|
+
FROM sessions
|
|
157
|
+
WHERE status = 'completed'
|
|
158
|
+
AND completed_at > ?
|
|
159
|
+
ORDER BY completed_at DESC
|
|
160
|
+
LIMIT ?
|
|
161
|
+
""",
|
|
162
|
+
(cutoff, limit),
|
|
163
|
+
)
|
|
164
|
+
rows = cursor.fetchall()
|
|
165
|
+
return [dict(row) for row in rows]
|
|
166
|
+
except Exception: # pragma: no cover
|
|
167
|
+
# Gracefully handle database errors
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def format_recent_work_markdown(sessions: list[dict[str, Any]]) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Format recently completed sessions as markdown.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
sessions: List of completed session dicts
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Markdown formatted string
|
|
180
|
+
"""
|
|
181
|
+
if not sessions:
|
|
182
|
+
return ""
|
|
183
|
+
|
|
184
|
+
lines = ["## Recent Work (Last 24 Hours)", ""]
|
|
185
|
+
|
|
186
|
+
for session in sessions:
|
|
187
|
+
session_id = session.get("id", "unknown")
|
|
188
|
+
session_id = session_id[:12] if len(session_id) > 12 else session_id
|
|
189
|
+
query = session.get("last_user_query", "No query recorded")
|
|
190
|
+
total_events = session.get("total_events") or 0
|
|
191
|
+
|
|
192
|
+
query_display = (
|
|
193
|
+
query[:60] + "..." if query and len(query) > 60 else (query or "Unknown")
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
lines.append(f"- `{session_id}`: {query_display} ({total_events} events)")
|
|
197
|
+
|
|
198
|
+
lines.append("")
|
|
199
|
+
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
__all__ = [
|
|
204
|
+
"get_concurrent_sessions",
|
|
205
|
+
"format_concurrent_sessions_markdown",
|
|
206
|
+
"get_recent_completed_sessions",
|
|
207
|
+
"format_recent_work_markdown",
|
|
208
|
+
]
|