htmlgraph 0.26.13__py3-none-any.whl → 0.26.14__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 CHANGED
@@ -95,7 +95,7 @@ from htmlgraph.types import (
95
95
  )
96
96
  from htmlgraph.work_type_utils import infer_work_type, infer_work_type_from_id
97
97
 
98
- __version__ = "0.26.13"
98
+ __version__ = "0.26.14"
99
99
  __all__ = [
100
100
  # Exceptions
101
101
  "HtmlGraphError",
@@ -1,9 +1,12 @@
1
1
  """
2
2
  Analytics modules for HtmlGraph.
3
3
 
4
- Provides work type analysis, dependency analytics, cross-session analytics, and CLI analytics.
4
+ Provides work type analysis, dependency analytics, cross-session analytics, CLI analytics,
5
+ and cost attribution analysis for OTEL ROI.
5
6
  """
6
7
 
8
+ from htmlgraph.analytics.cost_analyzer import CostAnalyzer
9
+ from htmlgraph.analytics.cost_reporter import CostReporter
7
10
  from htmlgraph.analytics.cross_session import CrossSessionAnalytics
8
11
  from htmlgraph.analytics.dependency import DependencyAnalytics
9
12
  from htmlgraph.analytics.work_type import Analytics
@@ -12,4 +15,6 @@ __all__ = [
12
15
  "Analytics",
13
16
  "DependencyAnalytics",
14
17
  "CrossSessionAnalytics",
18
+ "CostAnalyzer",
19
+ "CostReporter",
15
20
  ]
@@ -0,0 +1,387 @@
1
+ """
2
+ CostAnalyzer for OTEL ROI Analysis - Phase 1.
3
+
4
+ Analyzes cost attribution of Task() delegations vs direct tool execution.
5
+ Provides insights into which delegations are most expensive and their ROI.
6
+
7
+ Components:
8
+ 1. get_task_delegations() - Query all task_delegation events with hierarchy
9
+ 2. calculate_task_cost(event_id) - Sum token costs of all child tool calls
10
+ 3. get_cost_by_subagent_type() - Group costs by subagent type
11
+ 4. get_cost_by_tool_type() - Show which tools cost most
12
+ 5. get_roi_stats() - Calculate parallelization savings and benefits
13
+
14
+ Usage:
15
+ from htmlgraph.analytics.cost_analyzer import CostAnalyzer
16
+ analyzer = CostAnalyzer()
17
+ delegations = analyzer.get_task_delegations_with_costs()
18
+ print(f"Total delegation cost: ${delegations['total_cost_usd']:.2f}")
19
+ """
20
+
21
+ import sqlite3
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from htmlgraph.cigs.cost import CostCalculator
28
+
29
+
30
+ @dataclass
31
+ class TaskDelegation:
32
+ """Represents a single Task delegation with cost analysis."""
33
+
34
+ event_id: str
35
+ session_id: str
36
+ timestamp: datetime
37
+ subagent_type: str
38
+ parent_event_id: str | None
39
+ tool_count: int = 0
40
+ total_cost_tokens: int = 0
41
+ child_events: list[dict[str, Any]] = field(default_factory=list)
42
+
43
+ def to_dict(self) -> dict[str, Any]:
44
+ """Convert to dictionary."""
45
+ return {
46
+ "event_id": self.event_id,
47
+ "session_id": self.session_id,
48
+ "timestamp": self.timestamp.isoformat(),
49
+ "subagent_type": self.subagent_type,
50
+ "parent_event_id": self.parent_event_id,
51
+ "tool_count": self.tool_count,
52
+ "total_cost_tokens": self.total_cost_tokens,
53
+ "child_events": self.child_events,
54
+ }
55
+
56
+
57
+ @dataclass
58
+ class CostBreakdown:
59
+ """Cost breakdown analysis."""
60
+
61
+ by_subagent: dict[str, int] = field(default_factory=dict)
62
+ by_tool: dict[str, int] = field(default_factory=dict)
63
+ total_cost_tokens: int = 0
64
+ total_delegations: int = 0
65
+ avg_cost_per_delegation: float = 0.0
66
+
67
+
68
+ @dataclass
69
+ class ROIStats:
70
+ """Return-on-Investment statistics."""
71
+
72
+ total_delegation_cost: int = 0
73
+ estimated_direct_cost: int = 0
74
+ estimated_savings: int = 0
75
+ savings_percentage: float = 0.0
76
+ avg_parallelization_factor: float = 1.0
77
+ context_preservation_benefit: float = 0.0
78
+ total_delegations: int = 0
79
+ avg_cost_per_delegation: float = 0.0
80
+
81
+
82
+ class CostAnalyzer:
83
+ """
84
+ Analyze cost attribution of Task delegations.
85
+
86
+ Queries the agent_events database to calculate:
87
+ - Total cost of each Task delegation (sum of child tool calls)
88
+ - Cost breakdown by subagent type and tool type
89
+ - ROI statistics comparing direct vs delegated execution
90
+ """
91
+
92
+ def __init__(self, graph_dir: Path | None = None):
93
+ """
94
+ Initialize CostAnalyzer.
95
+
96
+ Args:
97
+ graph_dir: Root directory for HtmlGraph (defaults to .htmlgraph)
98
+ """
99
+ if graph_dir is None:
100
+ graph_dir = Path.cwd() / ".htmlgraph"
101
+
102
+ self.graph_dir = Path(graph_dir)
103
+ self.db_path = self.graph_dir / "htmlgraph.db"
104
+ self.cost_calculator = CostCalculator()
105
+
106
+ if not self.db_path.exists():
107
+ raise FileNotFoundError(f"Database not found at {self.db_path}")
108
+
109
+ def _get_connection(self) -> sqlite3.Connection:
110
+ """Get database connection with row factory."""
111
+ conn = sqlite3.connect(str(self.db_path))
112
+ conn.row_factory = sqlite3.Row
113
+ return conn
114
+
115
+ def get_task_delegations(self) -> list[TaskDelegation]:
116
+ """
117
+ Query all task_delegation events from the database.
118
+
119
+ Returns:
120
+ List of TaskDelegation objects ordered by timestamp (newest first)
121
+ """
122
+ conn = self._get_connection()
123
+ try:
124
+ cursor = conn.cursor()
125
+ cursor.execute(
126
+ """
127
+ SELECT
128
+ event_id,
129
+ session_id,
130
+ timestamp,
131
+ subagent_type,
132
+ parent_event_id
133
+ FROM agent_events
134
+ WHERE event_type = 'task_delegation'
135
+ ORDER BY timestamp DESC
136
+ """
137
+ )
138
+
139
+ delegations = []
140
+ for row in cursor.fetchall():
141
+ timestamp = (
142
+ datetime.fromisoformat(row["timestamp"])
143
+ if isinstance(row["timestamp"], str)
144
+ else row["timestamp"]
145
+ )
146
+ delegations.append(
147
+ TaskDelegation(
148
+ event_id=row["event_id"],
149
+ session_id=row["session_id"],
150
+ timestamp=timestamp,
151
+ subagent_type=row["subagent_type"] or "unknown",
152
+ parent_event_id=row["parent_event_id"],
153
+ )
154
+ )
155
+
156
+ return delegations
157
+ finally:
158
+ conn.close()
159
+
160
+ def calculate_task_cost(self, event_id: str) -> tuple[int, list[dict[str, Any]]]:
161
+ """
162
+ Calculate total cost of a Task delegation.
163
+
164
+ Sums all token costs of child tool calls using cost_tokens field.
165
+ Falls back to CIGS cost estimation if cost_tokens is not available.
166
+
167
+ Args:
168
+ event_id: Task delegation event ID
169
+
170
+ Returns:
171
+ Tuple of (total_cost_tokens, child_events_list)
172
+ """
173
+ conn = self._get_connection()
174
+ try:
175
+ cursor = conn.cursor()
176
+
177
+ # Get all children of this task
178
+ cursor.execute(
179
+ """
180
+ SELECT
181
+ event_id,
182
+ tool_name,
183
+ cost_tokens,
184
+ input_summary,
185
+ output_summary,
186
+ timestamp
187
+ FROM agent_events
188
+ WHERE parent_event_id = ?
189
+ AND event_type IN ('tool_call', 'tool_result')
190
+ ORDER BY timestamp ASC
191
+ """,
192
+ (event_id,),
193
+ )
194
+
195
+ total_cost = 0
196
+ child_events = []
197
+
198
+ for row in cursor.fetchall():
199
+ cost = row["cost_tokens"] if row["cost_tokens"] else 0
200
+
201
+ # If no stored cost, estimate based on tool type
202
+ if cost == 0 and row["tool_name"]:
203
+ cost = self.cost_calculator.predict_cost(row["tool_name"], {})
204
+
205
+ total_cost += cost
206
+
207
+ child_events.append(
208
+ {
209
+ "event_id": row["event_id"],
210
+ "tool_name": row["tool_name"],
211
+ "cost_tokens": cost,
212
+ "timestamp": row["timestamp"],
213
+ }
214
+ )
215
+
216
+ return total_cost, child_events
217
+ finally:
218
+ conn.close()
219
+
220
+ def get_task_delegations_with_costs(self) -> dict[str, Any]:
221
+ """
222
+ Get all task delegations with calculated costs.
223
+
224
+ Returns:
225
+ Dictionary with:
226
+ - delegations: List of TaskDelegation with costs
227
+ - total_cost_tokens: Sum of all delegation costs
228
+ - total_delegations: Count of delegations
229
+ - by_subagent_type: Cost breakdown by subagent
230
+ - by_tool_type: Cost breakdown by tool
231
+ """
232
+ delegations = self.get_task_delegations()
233
+
234
+ total_cost = 0
235
+ by_subagent: dict[str, int] = {}
236
+ by_tool: dict[str, int] = {}
237
+
238
+ for delegation in delegations:
239
+ cost, child_events = self.calculate_task_cost(delegation.event_id)
240
+ delegation.total_cost_tokens = cost
241
+ delegation.child_events = child_events
242
+ delegation.tool_count = len(child_events)
243
+
244
+ total_cost += cost
245
+
246
+ # Track by subagent type
247
+ subagent = delegation.subagent_type
248
+ by_subagent[subagent] = by_subagent.get(subagent, 0) + cost
249
+
250
+ # Track by tool type
251
+ for child in child_events:
252
+ tool = child["tool_name"] or "unknown"
253
+ by_tool[tool] = by_tool.get(tool, 0) + child["cost_tokens"]
254
+
255
+ # Convert to USD (approximation: 1M tokens ~ $3 for input, $6 for output)
256
+ # Average: ~$4.50 per 1M tokens
257
+ total_cost_usd = total_cost * 0.0000045
258
+
259
+ return {
260
+ "delegations": delegations,
261
+ "total_cost_tokens": total_cost,
262
+ "total_cost_usd": total_cost_usd,
263
+ "total_delegations": len(delegations),
264
+ "avg_cost_per_delegation": (
265
+ total_cost / len(delegations) if delegations else 0
266
+ ),
267
+ "by_subagent_type": by_subagent,
268
+ "by_tool_type": by_tool,
269
+ }
270
+
271
+ def get_cost_by_subagent_type(self) -> dict[str, int]:
272
+ """
273
+ Group delegation costs by subagent type.
274
+
275
+ Returns:
276
+ Dictionary mapping subagent_type to total tokens spent
277
+ """
278
+ data = self.get_task_delegations_with_costs()
279
+ result = data.get("by_subagent_type", {})
280
+ if isinstance(result, dict):
281
+ return result
282
+ return {}
283
+
284
+ def get_cost_by_tool_type(self) -> dict[str, int]:
285
+ """
286
+ Show which tools cost most across all delegations.
287
+
288
+ Returns:
289
+ Dictionary mapping tool_name to total tokens spent
290
+ """
291
+ data = self.get_task_delegations_with_costs()
292
+ result = data.get("by_tool_type", {})
293
+ if isinstance(result, dict):
294
+ return result
295
+ return {}
296
+
297
+ def get_roi_stats(self) -> ROIStats:
298
+ """
299
+ Calculate ROI statistics comparing delegation vs direct execution.
300
+
301
+ Assumptions:
302
+ - Direct execution: Tokens spent directly on main agent
303
+ - Delegated execution: Tokens in child subagents (already counted)
304
+ - Savings: Context preservation + parallelization benefits
305
+ - Parallelization factor: 1.2-1.5x (subagents can work more efficiently)
306
+ - Context preservation: ~30% token savings from better focus
307
+
308
+ Returns:
309
+ ROIStats with cost and savings analysis
310
+ """
311
+ data = self.get_task_delegations_with_costs()
312
+
313
+ total_delegation_cost = data["total_cost_tokens"]
314
+ total_delegations = data["total_delegations"]
315
+
316
+ # Estimate direct execution cost
317
+ # Assumption: direct execution would cost 2.5x due to context overhead
318
+ estimated_direct_cost = int(total_delegation_cost * 2.5)
319
+
320
+ # Estimate savings
321
+ # Parallelization benefit: 1.2x efficiency
322
+ # Context preservation: 30% savings
323
+ parallelization_factor = 1.2
324
+ context_benefit = 0.30
325
+
326
+ estimated_savings = int(
327
+ estimated_direct_cost
328
+ - (total_delegation_cost * parallelization_factor * (1.0 - context_benefit))
329
+ )
330
+
331
+ savings_percentage = (
332
+ (estimated_savings / estimated_direct_cost * 100)
333
+ if estimated_direct_cost > 0
334
+ else 0.0
335
+ )
336
+
337
+ return ROIStats(
338
+ total_delegation_cost=total_delegation_cost,
339
+ estimated_direct_cost=estimated_direct_cost,
340
+ estimated_savings=estimated_savings,
341
+ savings_percentage=savings_percentage,
342
+ avg_parallelization_factor=parallelization_factor,
343
+ context_preservation_benefit=context_benefit,
344
+ total_delegations=total_delegations,
345
+ avg_cost_per_delegation=(
346
+ total_delegation_cost / total_delegations
347
+ if total_delegations > 0
348
+ else 0.0
349
+ ),
350
+ )
351
+
352
+ def get_top_delegations(self, limit: int = 10) -> list[TaskDelegation]:
353
+ """
354
+ Get the most expensive Task delegations.
355
+
356
+ Args:
357
+ limit: Number of delegations to return (default: 10)
358
+
359
+ Returns:
360
+ List of TaskDelegation sorted by cost (descending)
361
+ """
362
+ data = self.get_task_delegations_with_costs()
363
+ delegations = data["delegations"]
364
+
365
+ # Sort by cost descending
366
+ sorted_delegations = sorted(
367
+ delegations, key=lambda d: d.total_cost_tokens, reverse=True
368
+ )
369
+
370
+ return sorted_delegations[:limit]
371
+
372
+ def get_cost_breakdown(self) -> CostBreakdown:
373
+ """
374
+ Get comprehensive cost breakdown.
375
+
376
+ Returns:
377
+ CostBreakdown with by_subagent and by_tool analysis
378
+ """
379
+ data = self.get_task_delegations_with_costs()
380
+
381
+ return CostBreakdown(
382
+ by_subagent=data["by_subagent_type"],
383
+ by_tool=data["by_tool_type"],
384
+ total_cost_tokens=data["total_cost_tokens"],
385
+ total_delegations=data["total_delegations"],
386
+ avg_cost_per_delegation=data["avg_cost_per_delegation"],
387
+ )
@@ -0,0 +1,675 @@
1
+ """
2
+ CostReporter for OTEL ROI Analysis - Phase 1.
3
+
4
+ Generates interactive HTML dashboards showing cost analysis of Task delegations.
5
+ Creates visualizations of delegation costs, ROI, and optimization recommendations.
6
+
7
+ Features:
8
+ - Top 10 most expensive Task delegations
9
+ - Cost breakdown by subagent type and tool type
10
+ - ROI analysis comparing delegation vs direct execution
11
+ - Interactive charts using Chart.js
12
+ - Dark theme matching HtmlGraph visual style
13
+
14
+ Usage:
15
+ from htmlgraph.analytics.cost_analyzer import CostAnalyzer
16
+ from htmlgraph.analytics.cost_reporter import CostReporter
17
+
18
+ analyzer = CostAnalyzer()
19
+ reporter = CostReporter()
20
+ html = reporter.generate_dashboard(analyzer)
21
+ reporter.save_dashboard(html, "cost-analysis.html")
22
+ """
23
+
24
+ import json
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from htmlgraph.analytics.cost_analyzer import CostAnalyzer
30
+
31
+
32
+ class CostReporter:
33
+ """Generate interactive HTML dashboards from cost analysis data."""
34
+
35
+ THEME = {
36
+ "bg_primary": "#1a1a2e",
37
+ "bg_secondary": "#16213e",
38
+ "text_primary": "#e0e0e0",
39
+ "text_secondary": "#b0b0b0",
40
+ "accent_primary": "#0f3460",
41
+ "accent_secondary": "#e94560",
42
+ "success": "#4caf50",
43
+ "warning": "#ff9800",
44
+ "error": "#f44336",
45
+ }
46
+
47
+ def __init__(self) -> None:
48
+ """Initialize CostReporter."""
49
+ self.generated_at = datetime.now()
50
+
51
+ def generate_dashboard(self, analyzer: CostAnalyzer) -> str:
52
+ """
53
+ Generate complete interactive HTML dashboard.
54
+
55
+ Args:
56
+ analyzer: CostAnalyzer instance with cost data
57
+
58
+ Returns:
59
+ Complete HTML document as string
60
+ """
61
+ # Gather data
62
+ cost_breakdown = analyzer.get_cost_breakdown()
63
+ roi_stats = analyzer.get_roi_stats()
64
+ top_delegations = analyzer.get_top_delegations(10)
65
+ all_delegations = analyzer.get_task_delegations_with_costs()
66
+
67
+ # Build dashboard components
68
+ html_parts = [
69
+ self._html_header(),
70
+ self._html_styles(),
71
+ self._html_body_open(),
72
+ self._html_header_section(roi_stats),
73
+ self._html_summary_cards(roi_stats, cost_breakdown),
74
+ self._html_charts_section(cost_breakdown, all_delegations),
75
+ self._html_top_delegations_table(top_delegations),
76
+ self._html_insights_section(roi_stats, cost_breakdown),
77
+ self._html_footer_section(),
78
+ self._html_body_close(),
79
+ self._html_scripts(cost_breakdown, all_delegations),
80
+ ]
81
+
82
+ return "\n".join(html_parts)
83
+
84
+ def save_dashboard(self, html: str, path: str | Path) -> None:
85
+ """
86
+ Save HTML dashboard to file.
87
+
88
+ Args:
89
+ html: HTML content to save
90
+ path: File path to save to
91
+ """
92
+ path = Path(path)
93
+ path.parent.mkdir(parents=True, exist_ok=True)
94
+ path.write_text(html, encoding="utf-8")
95
+
96
+ # ===== HTML Generation Methods =====
97
+
98
+ def _html_header(self) -> str:
99
+ """Generate HTML document header."""
100
+ return """<!DOCTYPE html>
101
+ <html lang="en">
102
+ <head>
103
+ <meta charset="UTF-8">
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
+ <title>Cost Attribution Dashboard - HtmlGraph OTEL ROI Analysis</title>
106
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
107
+ </head>"""
108
+
109
+ def _html_styles(self) -> str:
110
+ """Generate CSS styles."""
111
+ return f"""<style>
112
+ * {{
113
+ margin: 0;
114
+ padding: 0;
115
+ box-sizing: border-box;
116
+ }}
117
+
118
+ body {{
119
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
120
+ background-color: {self.THEME["bg_primary"]};
121
+ color: {self.THEME["text_primary"]};
122
+ line-height: 1.6;
123
+ }}
124
+
125
+ a {{
126
+ color: {self.THEME["accent_secondary"]};
127
+ text-decoration: none;
128
+ }}
129
+
130
+ a:hover {{
131
+ text-decoration: underline;
132
+ }}
133
+
134
+ .container {{
135
+ max-width: 1400px;
136
+ margin: 0 auto;
137
+ padding: 20px;
138
+ }}
139
+
140
+ .header {{
141
+ border-bottom: 2px solid {self.THEME["accent_primary"]};
142
+ padding-bottom: 20px;
143
+ margin-bottom: 30px;
144
+ }}
145
+
146
+ .header h1 {{
147
+ font-size: 2.5em;
148
+ margin-bottom: 10px;
149
+ color: {self.THEME["text_primary"]};
150
+ }}
151
+
152
+ .header .subtitle {{
153
+ color: {self.THEME["text_secondary"]};
154
+ font-size: 0.95em;
155
+ }}
156
+
157
+ .header-meta {{
158
+ margin-top: 15px;
159
+ display: flex;
160
+ gap: 20px;
161
+ flex-wrap: wrap;
162
+ font-size: 0.9em;
163
+ color: {self.THEME["text_secondary"]};
164
+ }}
165
+
166
+ .cards {{
167
+ display: grid;
168
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
169
+ gap: 20px;
170
+ margin-bottom: 40px;
171
+ }}
172
+
173
+ .card {{
174
+ background-color: {self.THEME["bg_secondary"]};
175
+ border: 1px solid {self.THEME["accent_primary"]};
176
+ border-radius: 8px;
177
+ padding: 25px;
178
+ transition: all 0.3s ease;
179
+ }}
180
+
181
+ .card:hover {{
182
+ border-color: {self.THEME["accent_secondary"]};
183
+ transform: translateY(-5px);
184
+ box-shadow: 0 10px 30px rgba(233, 69, 96, 0.2);
185
+ }}
186
+
187
+ .card-label {{
188
+ color: {self.THEME["text_secondary"]};
189
+ font-size: 0.85em;
190
+ text-transform: uppercase;
191
+ letter-spacing: 0.5px;
192
+ margin-bottom: 10px;
193
+ }}
194
+
195
+ .card-value {{
196
+ font-size: 2.5em;
197
+ font-weight: bold;
198
+ margin-bottom: 8px;
199
+ color: {self.THEME["text_primary"]};
200
+ }}
201
+
202
+ .card-unit {{
203
+ color: {self.THEME["text_secondary"]};
204
+ font-size: 0.9em;
205
+ font-weight: normal;
206
+ }}
207
+
208
+ .card-meta {{
209
+ color: {self.THEME["text_secondary"]};
210
+ font-size: 0.85em;
211
+ margin-top: 12px;
212
+ padding-top: 12px;
213
+ border-top: 1px solid {self.THEME["accent_primary"]};
214
+ }}
215
+
216
+ .status-good {{
217
+ color: {self.THEME["success"]};
218
+ }}
219
+
220
+ .status-warning {{
221
+ color: {self.THEME["warning"]};
222
+ }}
223
+
224
+ .status-error {{
225
+ color: {self.THEME["error"]};
226
+ }}
227
+
228
+ .charts {{
229
+ display: grid;
230
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
231
+ gap: 30px;
232
+ margin-bottom: 40px;
233
+ }}
234
+
235
+ .chart-container {{
236
+ background-color: {self.THEME["bg_secondary"]};
237
+ border: 1px solid {self.THEME["accent_primary"]};
238
+ border-radius: 8px;
239
+ padding: 20px;
240
+ }}
241
+
242
+ .chart-title {{
243
+ font-size: 1.3em;
244
+ font-weight: 600;
245
+ margin-bottom: 20px;
246
+ color: {self.THEME["text_primary"]};
247
+ }}
248
+
249
+ .chart-canvas {{
250
+ max-height: 400px;
251
+ }}
252
+
253
+ .table-section {{
254
+ margin-bottom: 40px;
255
+ }}
256
+
257
+ .table-section h2 {{
258
+ font-size: 1.5em;
259
+ margin-bottom: 20px;
260
+ color: {self.THEME["text_primary"]};
261
+ }}
262
+
263
+ .table-wrapper {{
264
+ overflow-x: auto;
265
+ background-color: {self.THEME["bg_secondary"]};
266
+ border: 1px solid {self.THEME["accent_primary"]};
267
+ border-radius: 8px;
268
+ }}
269
+
270
+ table {{
271
+ width: 100%;
272
+ border-collapse: collapse;
273
+ }}
274
+
275
+ th {{
276
+ background-color: {self.THEME["accent_primary"]};
277
+ padding: 15px;
278
+ text-align: left;
279
+ font-weight: 600;
280
+ color: {self.THEME["text_primary"]};
281
+ border-bottom: 2px solid {self.THEME["accent_secondary"]};
282
+ }}
283
+
284
+ td {{
285
+ padding: 12px 15px;
286
+ border-bottom: 1px solid {self.THEME["accent_primary"]};
287
+ color: {self.THEME["text_primary"]};
288
+ }}
289
+
290
+ tr:hover {{
291
+ background-color: {self.THEME["accent_primary"]};
292
+ }}
293
+
294
+ .insights {{
295
+ background-color: {self.THEME["bg_secondary"]};
296
+ border: 1px solid {self.THEME["accent_primary"]};
297
+ border-radius: 8px;
298
+ padding: 25px;
299
+ margin-bottom: 40px;
300
+ }}
301
+
302
+ .insights h2 {{
303
+ font-size: 1.5em;
304
+ margin-bottom: 20px;
305
+ color: {self.THEME["text_primary"]};
306
+ }}
307
+
308
+ .insight {{
309
+ margin-bottom: 20px;
310
+ padding: 15px;
311
+ background-color: {self.THEME["bg_primary"]};
312
+ border-left: 4px solid {self.THEME["accent_secondary"]};
313
+ border-radius: 4px;
314
+ }}
315
+
316
+ .insight-title {{
317
+ font-weight: 600;
318
+ color: {self.THEME["accent_secondary"]};
319
+ margin-bottom: 8px;
320
+ }}
321
+
322
+ .insight-text {{
323
+ color: {self.THEME["text_secondary"]};
324
+ line-height: 1.6;
325
+ }}
326
+
327
+ .footer {{
328
+ text-align: center;
329
+ padding-top: 20px;
330
+ border-top: 1px solid {self.THEME["accent_primary"]};
331
+ color: {self.THEME["text_secondary"]};
332
+ font-size: 0.85em;
333
+ }}
334
+
335
+ @media (max-width: 768px) {{
336
+ .charts {{
337
+ grid-template-columns: 1fr;
338
+ }}
339
+
340
+ .header h1 {{
341
+ font-size: 1.8em;
342
+ }}
343
+
344
+ .card-value {{
345
+ font-size: 2em;
346
+ }}
347
+
348
+ table {{
349
+ font-size: 0.9em;
350
+ }}
351
+
352
+ th, td {{
353
+ padding: 10px;
354
+ }}
355
+ }}
356
+ </style>"""
357
+
358
+ def _html_body_open(self) -> str:
359
+ """Generate opening body tag."""
360
+ return "<body>"
361
+
362
+ def _html_body_close(self) -> str:
363
+ """Generate closing body tag."""
364
+ return "</body></html>"
365
+
366
+ def _html_header_section(self, roi_stats: Any) -> str:
367
+ """Generate dashboard header section."""
368
+ generated_at = self.generated_at.strftime("%Y-%m-%d %H:%M:%S")
369
+
370
+ return f"""<div class="container">
371
+ <div class="header">
372
+ <h1>Cost Attribution Dashboard</h1>
373
+ <p class="subtitle">OTEL ROI Analysis - Phase 1 MVP</p>
374
+ <div class="header-meta">
375
+ <span>Generated: <strong>{generated_at}</strong></span>
376
+ <span>Total Delegations: <strong>{roi_stats.total_delegations}</strong></span>
377
+ <span>Status: <strong class="status-good">✓ Active</strong></span>
378
+ </div>
379
+ </div>
380
+ """
381
+
382
+ def _html_summary_cards(self, roi_stats: Any, cost_breakdown: Any) -> str:
383
+ """Generate summary metric cards."""
384
+ total_cost_usd = roi_stats.total_delegation_cost * 0.0000045
385
+
386
+ return f"""<div class="cards">
387
+ <div class="card">
388
+ <div class="card-label">Total Delegation Cost</div>
389
+ <div class="card-value">${total_cost_usd:.2f}<span class="card-unit"> USD</span></div>
390
+ <div class="card-meta">{roi_stats.total_delegation_cost:,} tokens</div>
391
+ </div>
392
+
393
+ <div class="card">
394
+ <div class="card-label">Estimated Direct Cost</div>
395
+ <div class="card-value">${roi_stats.estimated_direct_cost * 0.0000045:.2f}<span class="card-unit"> USD</span></div>
396
+ <div class="card-meta">{roi_stats.estimated_direct_cost:,} tokens (2.5x overhead)</div>
397
+ </div>
398
+
399
+ <div class="card">
400
+ <div class="card-label">Estimated Savings</div>
401
+ <div class="card-value"><span class="status-good">${roi_stats.estimated_savings * 0.0000045:.2f}</span></div>
402
+ <div class="card-meta"><span class="status-good">{roi_stats.savings_percentage:.0f}% reduction</span></div>
403
+ </div>
404
+
405
+ <div class="card">
406
+ <div class="card-label">Avg Cost per Delegation</div>
407
+ <div class="card-value">{roi_stats.avg_cost_per_delegation:,.0f}<span class="card-unit"> tokens</span></div>
408
+ <div class="card-meta">Across {roi_stats.total_delegations} delegations</div>
409
+ </div>
410
+ </div>
411
+ """
412
+
413
+ def _html_charts_section(self, cost_breakdown: Any, all_delegations: Any) -> str:
414
+ """Generate charts section with visualizations."""
415
+ # Prepare data for charts
416
+ subagent_labels = list(cost_breakdown.by_subagent.keys())
417
+ subagent_data = list(cost_breakdown.by_subagent.values())
418
+
419
+ tool_labels = list(cost_breakdown.by_tool.keys())
420
+ tool_data = list(cost_breakdown.by_tool.values())
421
+
422
+ # Embed data as JSON for JavaScript
423
+ subagent_labels_json = json.dumps(subagent_labels)
424
+ subagent_data_json = json.dumps(subagent_data)
425
+ tool_labels_json = json.dumps(tool_labels)
426
+ tool_data_json = json.dumps(tool_data)
427
+
428
+ return f"""<div class="charts">
429
+ <div class="chart-container">
430
+ <div class="chart-title">Cost by Subagent Type</div>
431
+ <canvas id="chartBySubagent" class="chart-canvas"></canvas>
432
+ </div>
433
+
434
+ <div class="chart-container">
435
+ <div class="chart-title">Cost by Tool Type</div>
436
+ <canvas id="chartByTool" class="chart-canvas"></canvas>
437
+ </div>
438
+ </div>
439
+
440
+ <script>
441
+ window.chartData = {{
442
+ subagent_labels: {subagent_labels_json},
443
+ subagent_data: {subagent_data_json},
444
+ tool_labels: {tool_labels_json},
445
+ tool_data: {tool_data_json}
446
+ }};
447
+ </script>
448
+ """
449
+
450
+ def _html_top_delegations_table(self, top_delegations: list[Any]) -> str:
451
+ """Generate top delegations table."""
452
+ table_rows = ""
453
+
454
+ for i, delegation in enumerate(top_delegations, 1):
455
+ cost_usd = delegation.total_cost_tokens * 0.0000045
456
+ timestamp = delegation.timestamp.strftime("%Y-%m-%d %H:%M:%S")
457
+
458
+ table_rows += f""" <tr>
459
+ <td>{i}</td>
460
+ <td>{timestamp}</td>
461
+ <td><strong>{delegation.subagent_type}</strong></td>
462
+ <td>{delegation.tool_count}</td>
463
+ <td>{delegation.total_cost_tokens:,}</td>
464
+ <td>${cost_usd:.2f}</td>
465
+ <td><code style="font-size: 0.85em; color: #b0b0b0;">{delegation.event_id[:16]}...</code></td>
466
+ </tr>
467
+ """
468
+
469
+ return f"""<div class="table-section">
470
+ <h2>Top 10 Most Expensive Delegations</h2>
471
+ <div class="table-wrapper">
472
+ <table>
473
+ <thead>
474
+ <tr>
475
+ <th>#</th>
476
+ <th>Timestamp</th>
477
+ <th>Subagent Type</th>
478
+ <th>Tool Count</th>
479
+ <th>Cost (tokens)</th>
480
+ <th>Cost (USD)</th>
481
+ <th>Event ID</th>
482
+ </tr>
483
+ </thead>
484
+ <tbody>
485
+ {table_rows} </tbody>
486
+ </table>
487
+ </div>
488
+ </div>
489
+ """
490
+
491
+ def _html_insights_section(self, roi_stats: Any, cost_breakdown: Any) -> str:
492
+ """Generate insights and recommendations."""
493
+ insights = []
494
+
495
+ # Insight 1: ROI summary
496
+ if roi_stats.savings_percentage > 40:
497
+ insights.append(
498
+ (
499
+ "Strong Delegation ROI",
500
+ f"Your delegation strategy is effective. By properly delegating work, "
501
+ f"you're achieving approximately {roi_stats.savings_percentage:.0f}% cost savings "
502
+ f"compared to direct execution.",
503
+ )
504
+ )
505
+ else:
506
+ insights.append(
507
+ (
508
+ "Delegation Opportunity",
509
+ f"Current delegation strategy shows {roi_stats.savings_percentage:.0f}% potential savings. "
510
+ f"Consider delegating more complex work to specialized subagents.",
511
+ )
512
+ )
513
+
514
+ # Insight 2: Most expensive subagent
515
+ if cost_breakdown.by_subagent:
516
+ top_subagent = max(cost_breakdown.by_subagent.items(), key=lambda x: x[1])
517
+ insights.append(
518
+ (
519
+ f"Highest Cost Subagent: {top_subagent[0]}",
520
+ f"This subagent type accounts for {top_subagent[1]:,} tokens. "
521
+ f"Review the work being delegated here for optimization opportunities.",
522
+ )
523
+ )
524
+
525
+ # Insight 3: Most expensive tool
526
+ if cost_breakdown.by_tool:
527
+ top_tool = max(cost_breakdown.by_tool.items(), key=lambda x: x[1])
528
+ insights.append(
529
+ (
530
+ f"Highest Cost Tool: {top_tool[0]}",
531
+ f"This tool is consuming {top_tool[1]:,} tokens. "
532
+ f"Consider batching operations or using more efficient approaches.",
533
+ )
534
+ )
535
+
536
+ # Insight 4: Parallelization benefit
537
+ insights.append(
538
+ (
539
+ "Parallelization Benefit",
540
+ f"Subagent delegation enables {roi_stats.avg_parallelization_factor:.1f}x efficiency gain "
541
+ f"through parallel execution and focused context.",
542
+ )
543
+ )
544
+
545
+ insights_html = ""
546
+ for title, text in insights:
547
+ insights_html += f""" <div class="insight">
548
+ <div class="insight-title">{title}</div>
549
+ <div class="insight-text">{text}</div>
550
+ </div>
551
+ """
552
+
553
+ return f"""<div class="insights">
554
+ <h2>Insights & Recommendations</h2>
555
+ {insights_html} </div>
556
+ """
557
+
558
+ def _html_footer_section(self) -> str:
559
+ """Generate footer section."""
560
+ return """ <div class="footer">
561
+ <p>HtmlGraph Cost Attribution Dashboard | OTEL ROI Analysis Phase 1 | <a href="https://code.claude.com">Claude Code</a></p>
562
+ </div>
563
+ </div>"""
564
+
565
+ def _html_scripts(self, cost_breakdown: Any, all_delegations: Any) -> str:
566
+ """Generate JavaScript for interactive charts."""
567
+ return """<script>
568
+ document.addEventListener('DOMContentLoaded', function() {
569
+ const data = window.chartData || {
570
+ subagent_labels: [],
571
+ subagent_data: [],
572
+ tool_labels: [],
573
+ tool_data: []
574
+ };
575
+
576
+ const chartOptions = {
577
+ responsive: true,
578
+ maintainAspectRatio: true,
579
+ plugins: {
580
+ legend: {
581
+ labels: {
582
+ color: '#e0e0e0',
583
+ font: { size: 12 }
584
+ }
585
+ },
586
+ tooltip: {
587
+ backgroundColor: 'rgba(26, 26, 46, 0.8)',
588
+ titleColor: '#e0e0e0',
589
+ bodyColor: '#b0b0b0',
590
+ borderColor: '#0f3460',
591
+ borderWidth: 1
592
+ }
593
+ },
594
+ scales: {
595
+ y: {
596
+ ticks: { color: '#b0b0b0' },
597
+ grid: { color: '#16213e' }
598
+ },
599
+ x: {
600
+ ticks: { color: '#b0b0b0' },
601
+ grid: { color: '#16213e' }
602
+ }
603
+ }
604
+ };
605
+
606
+ // Doughnut chart - Cost by Subagent Type
607
+ if (data.subagent_labels.length > 0) {
608
+ const ctxSubagent = document.getElementById('chartBySubagent');
609
+ if (ctxSubagent) {
610
+ new Chart(ctxSubagent, {
611
+ type: 'doughnut',
612
+ data: {
613
+ labels: data.subagent_labels,
614
+ datasets: [{
615
+ label: 'Cost (tokens)',
616
+ data: data.subagent_data,
617
+ backgroundColor: [
618
+ '#e94560',
619
+ '#ff9800',
620
+ '#ff6f00',
621
+ '#d84315',
622
+ '#c62828',
623
+ '#0f3460'
624
+ ],
625
+ borderColor: '#1a1a2e',
626
+ borderWidth: 2
627
+ }]
628
+ },
629
+ options: {
630
+ ...chartOptions,
631
+ plugins: {
632
+ ...chartOptions.plugins,
633
+ legend: {
634
+ ...chartOptions.plugins.legend,
635
+ position: 'bottom'
636
+ }
637
+ }
638
+ }
639
+ });
640
+ }
641
+ }
642
+
643
+ // Bar chart - Cost by Tool
644
+ if (data.tool_labels.length > 0) {
645
+ const ctxTool = document.getElementById('chartByTool');
646
+ if (ctxTool) {
647
+ new Chart(ctxTool, {
648
+ type: 'bar',
649
+ data: {
650
+ labels: data.tool_labels,
651
+ datasets: [{
652
+ label: 'Cost (tokens)',
653
+ data: data.tool_data,
654
+ backgroundColor: '#0f3460',
655
+ borderColor: '#e94560',
656
+ borderWidth: 2,
657
+ borderRadius: 4
658
+ }]
659
+ },
660
+ options: {
661
+ ...chartOptions,
662
+ indexAxis: 'y',
663
+ plugins: {
664
+ ...chartOptions.plugins,
665
+ legend: {
666
+ ...chartOptions.plugins.legend,
667
+ display: true
668
+ }
669
+ }
670
+ }
671
+ });
672
+ }
673
+ }
674
+ });
675
+ </script>"""
@@ -2,5 +2,5 @@
2
2
  "dismissed_at": null,
3
3
  "dismissed_by": null,
4
4
  "session_id": null,
5
- "show_count": 112
5
+ "show_count": 471
6
6
  }
Binary file
@@ -96,6 +96,25 @@ def _register_cigs_commands(subparsers: _SubParsersAction) -> None:
96
96
  cost_dashboard.add_argument("--output", help="Custom output path")
97
97
  cost_dashboard.set_defaults(func=CostDashboardCommand.from_args)
98
98
 
99
+ # cigs roi-analysis (Phase 1 OTEL ROI)
100
+ roi_analysis = cigs_subparsers.add_parser(
101
+ "roi-analysis", help="OTEL ROI analysis - cost attribution of Task delegations"
102
+ )
103
+ roi_analysis.add_argument(
104
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
105
+ )
106
+ roi_analysis.add_argument(
107
+ "--save", action="store_true", help="Save to .htmlgraph/cost-analysis.html"
108
+ )
109
+ roi_analysis.add_argument(
110
+ "--open", action="store_true", help="Open in browser after generation"
111
+ )
112
+ roi_analysis.add_argument(
113
+ "--json", action="store_true", help="Output JSON instead of HTML"
114
+ )
115
+ roi_analysis.add_argument("--output", help="Custom output path")
116
+ # roi_analysis.set_defaults(func=OTELROIAnalysisCommand.from_args) # TODO: Implement OTELROIAnalysisCommand
117
+
99
118
  # cigs status
100
119
  cigs_status = cigs_subparsers.add_parser("status", help="Show CIGS status")
101
120
  cigs_status.add_argument(
@@ -49,6 +49,48 @@ async def run_event_tracking(
49
49
  Returns:
50
50
  Event tracking response: {"continue": True, "hookSpecificOutput": {...}}
51
51
  """
52
+ # ============ DEBUG: Log Task() responses ============
53
+ import json
54
+
55
+ tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
56
+ if tool_name == "Task":
57
+ tool_response = hook_input.get("tool_response", {})
58
+
59
+ print("\n" + "=" * 60, file=sys.stderr)
60
+ print("DEBUG: Task() PostToolUse Hook Input", file=sys.stderr)
61
+ print("=" * 60, file=sys.stderr)
62
+
63
+ print(f"\nTool Name: {tool_name}", file=sys.stderr)
64
+ print(f"Hook Type: {hook_type}", file=sys.stderr)
65
+
66
+ if isinstance(tool_response, dict):
67
+ print("\nTool Response Type: dict", file=sys.stderr)
68
+ print(f"Tool Response Keys: {list(tool_response.keys())}", file=sys.stderr)
69
+
70
+ # Check for task_id field
71
+ if "task_id" in tool_response:
72
+ task_id = tool_response.get("task_id")
73
+ print(f"\n✅ FOUND task_id: {task_id}", file=sys.stderr)
74
+ else:
75
+ print("\n❌ task_id NOT FOUND in response", file=sys.stderr)
76
+
77
+ # Print full response for inspection
78
+ print("\nFull Tool Response:", file=sys.stderr)
79
+ try:
80
+ response_str = json.dumps(tool_response, indent=2, default=str)
81
+ print(response_str, file=sys.stderr)
82
+ except Exception as e:
83
+ print(f"Could not serialize response: {e}", file=sys.stderr)
84
+ print(f"Raw response: {tool_response}", file=sys.stderr)
85
+ else:
86
+ print(
87
+ f"\nTool Response Type: {type(tool_response).__name__}", file=sys.stderr
88
+ )
89
+ print(f"Tool Response Value: {tool_response}", file=sys.stderr)
90
+
91
+ print("\n" + "=" * 60 + "\n", file=sys.stderr)
92
+ # ============ END DEBUG ============
93
+
52
94
  try:
53
95
  loop = asyncio.get_event_loop()
54
96
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgraph
3
- Version: 0.26.13
3
+ Version: 0.26.14
4
4
  Summary: HTML is All You Need - Graph database on web standards
5
5
  Project-URL: Homepage, https://github.com/Shakes-tzd/htmlgraph
6
6
  Project-URL: Documentation, https://github.com/Shakes-tzd/htmlgraph#readme
@@ -1,4 +1,4 @@
1
- htmlgraph/__init__.py,sha256=0Aj1SVQwQR_-V33g-vHxvUla8Ce9H5-Hd8NuTweRMjU,5718
1
+ htmlgraph/__init__.py,sha256=aFjqr9RP4Lzx2kZCoH8PRKdbaMd4an9tAnvEG9xLapo,5718
2
2
  htmlgraph/agent_detection.py,sha256=wEmrDv4hssPX2OkEnJZBHPbalxcaloiJF_hOOow_5WE,3511
3
3
  htmlgraph/agent_registry.py,sha256=Usa_35by7p5gtpvHO7K3AcGimnorw-FzgPVa3cWTQ58,9448
4
4
  htmlgraph/agents.py,sha256=Yvu6x1nOfrW2WhRTAHiCuSpvqoVJXx1Mkzd59kwEczw,33466
@@ -69,8 +69,10 @@ htmlgraph/work_type_utils.py,sha256=exA3FnuSmVMMMSBhPYDW-Bq5jGaTDjX7jkckCvnVZ_s,
69
69
  htmlgraph/.htmlgraph/.session-warning-state.json,sha256=X7xLzJc57pWuOLyg5iN7D9hp0CvaYY6UWBq8ImcIBmg,92
70
70
  htmlgraph/.htmlgraph/agents.json,sha256=dKaiSsd1L7iaQa1_s9lCpNEZH-1S_n0CfzmCHFoWyvc,1391
71
71
  htmlgraph/.htmlgraph/htmlgraph.db,sha256=gto2pndMBw97gljvwPywDlTUzYdoa_FnNJs3AsS9_SI,262144
72
- htmlgraph/analytics/__init__.py,sha256=G_DvCaiAeYVfMWFri1GOfLNF7PtAhitKa2V9TXHtsds,409
72
+ htmlgraph/analytics/__init__.py,sha256=jVcxE-9eGupT8IC5YMh8SqPc_w4k7GpPK0WfXj6SE4M,607
73
73
  htmlgraph/analytics/cli.py,sha256=aSnxkvNdrdukytqSirpFtcuiNRbg3Dt-hSpUsDPF9jo,14771
74
+ htmlgraph/analytics/cost_analyzer.py,sha256=tNVLRKesS6thCr89Xw6Bjz5K6Drn-pagiIjiF16hgQo,12854
75
+ htmlgraph/analytics/cost_reporter.py,sha256=_KumnYUYTr3yEHwBGGgjXSHR5IGPLDqjfuZHpfbeNi8,19917
74
76
  htmlgraph/analytics/cross_session.py,sha256=BmTNqyt1qYjKbq5sXDYGxp85G2XB2aHkIvdqRgSIQBw,20494
75
77
  htmlgraph/analytics/dependency.py,sha256=7Qc5xe0b1QE_P6-l4Z0WtstEmEXc4ZGNbyIEBMoABys,25757
76
78
  htmlgraph/analytics/work_type.py,sha256=SrZvY_RVq70uvzl3Ky7I3LHgOMduW_0vdSsk5yUZRG4,19293
@@ -123,15 +125,15 @@ htmlgraph/cigs/reporter.py,sha256=DgfHjD_ufP6a9Fmfm_-Ro50bDCQrNC6BZYl3dYNl86o,24
123
125
  htmlgraph/cigs/tracker.py,sha256=OcyXY7tGxbRCtlLX8Pg2bVtCtzPJtvh1VOtt4jbL9SI,10502
124
126
  htmlgraph/cli/__init__.py,sha256=0r1gr-uzhMS9_wz88IjGf5ma4bDK-zTzUX6p8Wq5n2U,1031
125
127
  htmlgraph/cli/__main__.py,sha256=lkTNsdh3X-vgKz_jzQe0IQmZY5PWlMjGBuaQR-gsqf8,137
126
- htmlgraph/cli/analytics.py,sha256=0Fat2vgVvqv5GZLuDJwX_fhb0iVkxiu2a9xXKO89Qf0,34823
128
+ htmlgraph/cli/analytics.py,sha256=KRw40n8lq_TOJno0eM97qsMiVZpmU6R7YN0dKKI4gwQ,35654
127
129
  htmlgraph/cli/base.py,sha256=OjybqWTQfLtQD5wBbz8IDDkE-2N6jpcXYdOq5BXza2I,20248
128
130
  htmlgraph/cli/constants.py,sha256=S7hAih2Ka64c0sWqOdq7W762VRoBHri8w8XDDWqHCkE,7088
129
131
  htmlgraph/cli/core.py,sha256=JhRL1iZhN3x4IeeVQkK4GnfAV7JQm5JDU6hG473GBOs,28878
130
132
  htmlgraph/cli/main.py,sha256=rSSSErlmFHrREbAydtytj3up8M_dQ9jFgYL1OmrSQas,4108
131
133
  htmlgraph/cli/models.py,sha256=7dEAwsroH2J_84Bq6i9Q0RLJsKQ9rNbX_daOfHdrb78,16033
132
- htmlgraph/cli/.htmlgraph/.session-warning-state.json,sha256=Ixvco_sIvbvteF2SYqnRx6pAaP50zTQb1h2HvCFJ6EE,93
134
+ htmlgraph/cli/.htmlgraph/.session-warning-state.json,sha256=jKw9n3GDSPzcHwZZnRoW7eOJEbQPQ3FKpvo3kMY7lHw,93
133
135
  htmlgraph/cli/.htmlgraph/agents.json,sha256=hVmmvoL4CDTIrhxH7VaM-jSDCj8kszWsUFY_LHae9DQ,1391
134
- htmlgraph/cli/.htmlgraph/htmlgraph.db,sha256=ky6fNEb7ypYQ5HVSxMgUaNwf2Gw-w1A5Q8npGoHA48Q,409600
136
+ htmlgraph/cli/.htmlgraph/htmlgraph.db,sha256=Ipk3K21EQyTT5hkimtL1dBiwHve-3zdpS4yP1wMMU-4,1343488
135
137
  htmlgraph/cli/templates/__init__.py,sha256=7eh8oC8rce689HGuuYqJEMCElJCtg084xOMHZti2XL8,37
136
138
  htmlgraph/cli/templates/cost_dashboard.py,sha256=2FRaujRUYEDfA18c0QFju28yidfaVnMFH4pdcBaQ7Ao,11313
137
139
  htmlgraph/cli/work/__init__.py,sha256=yFK8G85F-Z9df-3LBM-3NbjCgidtLEVhkYGO1uf4QuI,4570
@@ -201,7 +203,7 @@ htmlgraph/hooks/post-commit.sh,sha256=if65jNGZnEWsZPq_iYDNYunrZ1cmjPUEUbh6_4vfpO
201
203
  htmlgraph/hooks/post-merge.sh,sha256=gq-EeFLhDUVp-J2jyWMBVFcB0pdmH54Wu1SW_Gn-s2I,541
202
204
  htmlgraph/hooks/post_tool_use_failure.py,sha256=DHkJtuAOg5KSLfFZ1O-kePwaqmtNkbGQSEn4NplzvD8,8381
203
205
  htmlgraph/hooks/post_tool_use_handler.py,sha256=e0L8X4sU2H167HH96CgV6iYojd02AI9uyF8m4A1a-kU,8345
204
- htmlgraph/hooks/posttooluse.py,sha256=3usICpqlIdLKsijYY61BP2QezsB45MZ6evwFSobC9Yo,12822
206
+ htmlgraph/hooks/posttooluse.py,sha256=HOxW9lgJCnRLJRsdB7PFfAJkPWv-NiLjkF4wcBnjVUE,14606
205
207
  htmlgraph/hooks/pre-commit.sh,sha256=gTpbnHIBFxpAl7-REhXoS0NI4Pmlqo9pQEMEngTAU_A,3865
206
208
  htmlgraph/hooks/pre-push.sh,sha256=rNnkG8YmDtyk7OuJHOcbOYQR3MYFneaG6_w2X-Hl8Hs,660
207
209
  htmlgraph/hooks/pretooluse.py,sha256=5mW-hMmJZlFvahDfA9yCfPrtaN6yAlWTOchirE-zZdU,27327
@@ -250,12 +252,12 @@ htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4be
250
252
  htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
251
253
  htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
252
254
  htmlgraph/templates/orchestration-view.html,sha256=DlS7LlcjH0oO_KYILjuF1X42t8QhKLH4F85rkO54alY,10472
253
- htmlgraph-0.26.13.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
254
- htmlgraph-0.26.13.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
255
- htmlgraph-0.26.13.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
256
- htmlgraph-0.26.13.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
257
- htmlgraph-0.26.13.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
258
- htmlgraph-0.26.13.dist-info/METADATA,sha256=skB7JaGnQAiavoXN23dICyDwt2nfjyl0VTgs1HW4e9s,10237
259
- htmlgraph-0.26.13.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
260
- htmlgraph-0.26.13.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
261
- htmlgraph-0.26.13.dist-info/RECORD,,
255
+ htmlgraph-0.26.14.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
256
+ htmlgraph-0.26.14.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
257
+ htmlgraph-0.26.14.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
258
+ htmlgraph-0.26.14.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
259
+ htmlgraph-0.26.14.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
260
+ htmlgraph-0.26.14.dist-info/METADATA,sha256=BOzq9w86UFDhZgsaZCIMYf8Cp4nqHf8_rWEC_-7EOF4,10237
261
+ htmlgraph-0.26.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
262
+ htmlgraph-0.26.14.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
263
+ htmlgraph-0.26.14.dist-info/RECORD,,