htmlgraph 0.24.2__py3-none-any.whl → 0.25.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 (103) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2115 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +783 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +570 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3315 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1334 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/__init__.py +8 -0
  68. htmlgraph/hooks/bootstrap.py +169 -0
  69. htmlgraph/hooks/context.py +271 -0
  70. htmlgraph/hooks/drift_handler.py +521 -0
  71. htmlgraph/hooks/event_tracker.py +405 -15
  72. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  73. htmlgraph/hooks/pretooluse.py +476 -6
  74. htmlgraph/hooks/prompt_analyzer.py +648 -0
  75. htmlgraph/hooks/session_handler.py +583 -0
  76. htmlgraph/hooks/state_manager.py +501 -0
  77. htmlgraph/hooks/subagent_stop.py +309 -0
  78. htmlgraph/hooks/task_enforcer.py +39 -0
  79. htmlgraph/models.py +111 -15
  80. htmlgraph/operations/fastapi_server.py +230 -0
  81. htmlgraph/orchestration/headless_spawner.py +22 -14
  82. htmlgraph/pydantic_models.py +476 -0
  83. htmlgraph/quality_gates.py +350 -0
  84. htmlgraph/repo_hash.py +511 -0
  85. htmlgraph/sdk.py +348 -10
  86. htmlgraph/server.py +194 -0
  87. htmlgraph/session_hooks.py +300 -0
  88. htmlgraph/session_manager.py +131 -1
  89. htmlgraph/session_registry.py +587 -0
  90. htmlgraph/session_state.py +436 -0
  91. htmlgraph/system_prompts.py +449 -0
  92. htmlgraph/templates/orchestration-view.html +350 -0
  93. htmlgraph/track_builder.py +19 -0
  94. htmlgraph/validation.py +115 -0
  95. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
  96. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
  97. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
  98. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
  99. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  100. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  101. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  102. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
  103. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,482 @@
1
+ """
2
+ Tool Execution Traces Collection
3
+
4
+ Provides query interface for tool execution traces stored in tool_traces table.
5
+ Enables analysis of tool performance, error patterns, and execution hierarchies.
6
+
7
+ Example:
8
+ >>> from htmlgraph.sdk import SDK
9
+ >>> sdk = SDK(agent="claude")
10
+ >>>
11
+ >>> # Get traces for current session
12
+ >>> traces = sdk.traces.get_traces(session_id="sess-abc123")
13
+ >>> for trace in traces:
14
+ ... print(f"{trace.tool_name}: {trace.duration_ms}ms")
15
+ >>>
16
+ >>> # Find slow tool calls
17
+ >>> slow = sdk.traces.get_slow_traces(threshold_ms=1000)
18
+ >>>
19
+ >>> # Get hierarchical view (parent-child relationships)
20
+ >>> tree = sdk.traces.get_trace_tree(trace_id="trace-xyz")
21
+ >>> print(f"Root: {tree.root.tool_name}")
22
+ >>> print(f"Children: {len(tree.children)}")
23
+ >>>
24
+ >>> # Get error traces for debugging
25
+ >>> errors = sdk.traces.get_error_traces(session_id="sess-abc123")
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass
31
+ from datetime import datetime, timezone
32
+ from typing import TYPE_CHECKING
33
+
34
+ from htmlgraph.db.schema import HtmlGraphDB
35
+
36
+ if TYPE_CHECKING:
37
+ from htmlgraph.sdk import SDK
38
+
39
+
40
+ @dataclass
41
+ class TraceRecord:
42
+ """
43
+ Single tool execution trace.
44
+
45
+ Represents one complete execution of a tool with timing, input, output, and status.
46
+ Parent-child relationships via parent_tool_use_id enable hierarchical analysis.
47
+
48
+ Attributes:
49
+ tool_use_id: Unique identifier for this tool execution (UUID v4)
50
+ trace_id: Parent trace ID for grouping related executions
51
+ session_id: Session this execution belongs to
52
+ tool_name: Name of the tool executed (e.g., "Bash", "Read", "Write")
53
+ tool_input: Input parameters passed to the tool (dict, may be None)
54
+ tool_output: Result returned by the tool (dict, may be None if not yet complete)
55
+ start_time: When execution started (UTC datetime)
56
+ end_time: When execution ended (UTC datetime, None if still running)
57
+ duration_ms: Milliseconds to complete (None if still running or error)
58
+ status: Execution status (started, completed, failed, timeout, cancelled)
59
+ error_message: Error details if status is 'failed'
60
+ parent_tool_use_id: tool_use_id of parent tool if nested (None if top-level)
61
+ """
62
+
63
+ tool_use_id: str
64
+ trace_id: str
65
+ session_id: str
66
+ tool_name: str
67
+ tool_input: dict | None
68
+ tool_output: dict | None
69
+ start_time: datetime
70
+ end_time: datetime | None
71
+ duration_ms: int | None
72
+ status: str | None
73
+ error_message: str | None
74
+ parent_tool_use_id: str | None
75
+
76
+
77
+ @dataclass
78
+ class TraceTree:
79
+ """
80
+ Hierarchical view of traces (parent-child relationships).
81
+
82
+ Enables analysis of nested tool executions where one tool invokes others.
83
+ Example: Bash tool calls may invoke other tools as nested executions.
84
+
85
+ Attributes:
86
+ root: Root trace record (this execution)
87
+ children: List of child traces (tools invoked by this tool)
88
+ """
89
+
90
+ root: TraceRecord
91
+ children: list[TraceTree]
92
+
93
+
94
+ class TraceCollection:
95
+ """
96
+ Query interface for tool execution traces.
97
+
98
+ Provides methods to retrieve, filter, and analyze tool execution traces
99
+ stored in the tool_traces database table. Supports querying by session,
100
+ tool name, performance thresholds, and error status.
101
+
102
+ All queries return data sorted by start_time DESC (newest first).
103
+
104
+ Example:
105
+ >>> sdk = SDK(agent="claude")
106
+ >>> traces = sdk.traces
107
+ >>>
108
+ >>> # Single trace
109
+ >>> trace = traces.get_trace("tool-use-id-123")
110
+ >>>
111
+ >>> # All traces for session
112
+ >>> all_traces = traces.get_traces("sess-123")
113
+ >>>
114
+ >>> # By tool name
115
+ >>> bash_traces = traces.get_traces_by_tool("Bash")
116
+ >>>
117
+ >>> # Performance analysis
118
+ >>> slow = traces.get_slow_traces(threshold_ms=1000)
119
+ >>> errors = traces.get_error_traces("sess-123")
120
+ >>>
121
+ >>> # Hierarchical view
122
+ >>> tree = traces.get_trace_tree("trace-id-123")
123
+ """
124
+
125
+ def __init__(self, sdk: SDK):
126
+ """
127
+ Initialize traces collection.
128
+
129
+ Args:
130
+ sdk: Parent SDK instance
131
+ """
132
+ self._sdk = sdk
133
+ self._db = HtmlGraphDB()
134
+
135
+ def _row_to_trace(self, row: tuple) -> TraceRecord:
136
+ """
137
+ Convert database row to TraceRecord dataclass.
138
+
139
+ Args:
140
+ row: SQLite row tuple from tool_traces query
141
+
142
+ Returns:
143
+ TraceRecord with parsed fields
144
+ """
145
+ import json
146
+
147
+ # Unpack tuple: (tool_use_id, trace_id, session_id, tool_name, tool_input,
148
+ # tool_output, start_time, end_time, duration_ms, status,
149
+ # error_message, parent_tool_use_id)
150
+ (
151
+ tool_use_id,
152
+ trace_id,
153
+ session_id,
154
+ tool_name,
155
+ tool_input_json,
156
+ tool_output_json,
157
+ start_time_iso,
158
+ end_time_iso,
159
+ duration_ms,
160
+ status,
161
+ error_message,
162
+ parent_tool_use_id,
163
+ ) = row
164
+
165
+ # Parse JSON fields
166
+ tool_input = None
167
+ if tool_input_json:
168
+ try:
169
+ tool_input = json.loads(tool_input_json)
170
+ except (json.JSONDecodeError, TypeError):
171
+ tool_input = None
172
+
173
+ tool_output = None
174
+ if tool_output_json:
175
+ try:
176
+ tool_output = json.loads(tool_output_json)
177
+ except (json.JSONDecodeError, TypeError):
178
+ tool_output = None
179
+
180
+ # Parse timestamps
181
+ start_time: datetime | None = None
182
+ if start_time_iso:
183
+ try:
184
+ start_time = datetime.fromisoformat(
185
+ start_time_iso.replace("Z", "+00:00")
186
+ )
187
+ except (ValueError, AttributeError):
188
+ start_time = None
189
+
190
+ end_time: datetime | None = None
191
+ if end_time_iso:
192
+ try:
193
+ end_time = datetime.fromisoformat(end_time_iso.replace("Z", "+00:00"))
194
+ except (ValueError, AttributeError):
195
+ end_time = None
196
+
197
+ # Use current time if start_time is missing
198
+ if start_time is None:
199
+ start_time = datetime.now(timezone.utc)
200
+
201
+ return TraceRecord(
202
+ tool_use_id=tool_use_id,
203
+ trace_id=trace_id,
204
+ session_id=session_id,
205
+ tool_name=tool_name,
206
+ tool_input=tool_input,
207
+ tool_output=tool_output,
208
+ start_time=start_time,
209
+ end_time=end_time,
210
+ duration_ms=duration_ms,
211
+ status=status,
212
+ error_message=error_message,
213
+ parent_tool_use_id=parent_tool_use_id,
214
+ )
215
+
216
+ def get_trace(self, tool_use_id: str) -> TraceRecord | None:
217
+ """
218
+ Get single trace by tool_use_id.
219
+
220
+ Args:
221
+ tool_use_id: Unique tool execution ID (UUID v4)
222
+
223
+ Returns:
224
+ TraceRecord if found, None otherwise
225
+ """
226
+ try:
227
+ if not self._db.connection:
228
+ self._db.connect()
229
+
230
+ cursor = self._db.connection.cursor() # type: ignore[union-attr]
231
+ cursor.execute(
232
+ """
233
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
234
+ tool_output, start_time, end_time, duration_ms, status,
235
+ error_message, parent_tool_use_id
236
+ FROM tool_traces
237
+ WHERE tool_use_id = ?
238
+ """,
239
+ (tool_use_id,),
240
+ )
241
+
242
+ row = cursor.fetchone()
243
+ if row:
244
+ return self._row_to_trace(row)
245
+
246
+ return None
247
+ except Exception as e:
248
+ print(f"Error getting trace {tool_use_id}: {e}")
249
+ return None
250
+
251
+ def get_traces(
252
+ self,
253
+ session_id: str,
254
+ limit: int = 100,
255
+ start_time: datetime | None = None,
256
+ ) -> list[TraceRecord]:
257
+ """
258
+ Get traces for a session, ordered by start_time DESC (newest first).
259
+
260
+ Args:
261
+ session_id: Session to query
262
+ limit: Maximum traces to return (default 100)
263
+ start_time: Optional filter - only traces after this time
264
+
265
+ Returns:
266
+ List of TraceRecord objects, newest first
267
+ """
268
+ try:
269
+ if not self._db.connection:
270
+ self._db.connect()
271
+
272
+ cursor = self._db.connection.cursor() # type: ignore[union-attr]
273
+
274
+ if start_time:
275
+ start_time_iso = start_time.isoformat()
276
+ cursor.execute(
277
+ """
278
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
279
+ tool_output, start_time, end_time, duration_ms, status,
280
+ error_message, parent_tool_use_id
281
+ FROM tool_traces
282
+ WHERE session_id = ? AND start_time >= ?
283
+ ORDER BY start_time DESC
284
+ LIMIT ?
285
+ """,
286
+ (session_id, start_time_iso, limit),
287
+ )
288
+ else:
289
+ cursor.execute(
290
+ """
291
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
292
+ tool_output, start_time, end_time, duration_ms, status,
293
+ error_message, parent_tool_use_id
294
+ FROM tool_traces
295
+ WHERE session_id = ?
296
+ ORDER BY start_time DESC
297
+ LIMIT ?
298
+ """,
299
+ (session_id, limit),
300
+ )
301
+
302
+ rows = cursor.fetchall()
303
+ return [self._row_to_trace(row) for row in rows]
304
+ except Exception as e:
305
+ print(f"Error getting traces for session {session_id}: {e}")
306
+ return []
307
+
308
+ def get_traces_by_tool(self, tool_name: str, limit: int = 100) -> list[TraceRecord]:
309
+ """
310
+ Get traces for specific tool name.
311
+
312
+ Args:
313
+ tool_name: Name of the tool (e.g., "Bash", "Read", "Write")
314
+ limit: Maximum traces to return (default 100)
315
+
316
+ Returns:
317
+ List of TraceRecord objects, newest first
318
+ """
319
+ try:
320
+ if not self._db.connection:
321
+ self._db.connect()
322
+
323
+ cursor = self._db.connection.cursor() # type: ignore[union-attr]
324
+ cursor.execute(
325
+ """
326
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
327
+ tool_output, start_time, end_time, duration_ms, status,
328
+ error_message, parent_tool_use_id
329
+ FROM tool_traces
330
+ WHERE tool_name = ?
331
+ ORDER BY start_time DESC
332
+ LIMIT ?
333
+ """,
334
+ (tool_name, limit),
335
+ )
336
+
337
+ rows = cursor.fetchall()
338
+ return [self._row_to_trace(row) for row in rows]
339
+ except Exception as e:
340
+ print(f"Error getting traces for tool {tool_name}: {e}")
341
+ return []
342
+
343
+ def get_trace_tree(self, trace_id: str) -> TraceTree | None:
344
+ """
345
+ Get hierarchical view with parent-child relationships.
346
+
347
+ Recursively builds a tree of traces where each node can have children
348
+ (tools invoked by that tool). Useful for understanding nested execution.
349
+
350
+ Args:
351
+ trace_id: Root trace_id to build tree from
352
+
353
+ Returns:
354
+ TraceTree with root and children, or None if not found
355
+ """
356
+ try:
357
+ if not self._db.connection:
358
+ self._db.connect()
359
+
360
+ cursor = self._db.connection.cursor() # type: ignore[union-attr]
361
+
362
+ # Get root trace
363
+ cursor.execute(
364
+ """
365
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
366
+ tool_output, start_time, end_time, duration_ms, status,
367
+ error_message, parent_tool_use_id
368
+ FROM tool_traces
369
+ WHERE trace_id = ?
370
+ ORDER BY start_time DESC
371
+ LIMIT 1
372
+ """,
373
+ (trace_id,),
374
+ )
375
+
376
+ root_row = cursor.fetchone()
377
+ if not root_row:
378
+ return None
379
+
380
+ root = self._row_to_trace(root_row)
381
+
382
+ # Recursively get children
383
+ def build_tree(parent_trace: TraceRecord) -> TraceTree:
384
+ cursor.execute(
385
+ """
386
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
387
+ tool_output, start_time, end_time, duration_ms, status,
388
+ error_message, parent_tool_use_id
389
+ FROM tool_traces
390
+ WHERE parent_tool_use_id = ?
391
+ ORDER BY start_time ASC
392
+ """,
393
+ (parent_trace.tool_use_id,),
394
+ )
395
+
396
+ child_rows = cursor.fetchall()
397
+ children = []
398
+ for child_row in child_rows:
399
+ child = self._row_to_trace(child_row)
400
+ children.append(build_tree(child))
401
+
402
+ return TraceTree(root=parent_trace, children=children)
403
+
404
+ return build_tree(root)
405
+ except Exception as e:
406
+ print(f"Error getting trace tree for {trace_id}: {e}")
407
+ return None
408
+
409
+ def get_slow_traces(self, threshold_ms: int, limit: int = 100) -> list[TraceRecord]:
410
+ """
411
+ Find traces exceeding duration threshold.
412
+
413
+ Useful for identifying performance bottlenecks.
414
+
415
+ Args:
416
+ threshold_ms: Minimum duration in milliseconds
417
+ limit: Maximum traces to return (default 100)
418
+
419
+ Returns:
420
+ List of slow TraceRecord objects, slowest first
421
+ """
422
+ try:
423
+ if not self._db.connection:
424
+ self._db.connect()
425
+
426
+ cursor = self._db.connection.cursor() # type: ignore[union-attr]
427
+ cursor.execute(
428
+ """
429
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
430
+ tool_output, start_time, end_time, duration_ms, status,
431
+ error_message, parent_tool_use_id
432
+ FROM tool_traces
433
+ WHERE duration_ms IS NOT NULL AND duration_ms >= ?
434
+ ORDER BY duration_ms DESC
435
+ LIMIT ?
436
+ """,
437
+ (threshold_ms, limit),
438
+ )
439
+
440
+ rows = cursor.fetchall()
441
+ return [self._row_to_trace(row) for row in rows]
442
+ except Exception as e:
443
+ print(f"Error getting slow traces: {e}")
444
+ return []
445
+
446
+ def get_error_traces(self, session_id: str, limit: int = 100) -> list[TraceRecord]:
447
+ """
448
+ Get traces with errors/failures.
449
+
450
+ Filters for traces with status='failed' or error_message is not null.
451
+
452
+ Args:
453
+ session_id: Session to query
454
+ limit: Maximum traces to return (default 100)
455
+
456
+ Returns:
457
+ List of error TraceRecord objects, newest first
458
+ """
459
+ try:
460
+ if not self._db.connection:
461
+ self._db.connect()
462
+
463
+ cursor = self._db.connection.cursor() # type: ignore[union-attr]
464
+ cursor.execute(
465
+ """
466
+ SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
467
+ tool_output, start_time, end_time, duration_ms, status,
468
+ error_message, parent_tool_use_id
469
+ FROM tool_traces
470
+ WHERE session_id = ? AND (status IN ('failed', 'timeout', 'cancelled')
471
+ OR error_message IS NOT NULL)
472
+ ORDER BY start_time DESC
473
+ LIMIT ?
474
+ """,
475
+ (session_id, limit),
476
+ )
477
+
478
+ rows = cursor.fetchall()
479
+ return [self._row_to_trace(row) for row in rows]
480
+ except Exception as e:
481
+ print(f"Error getting error traces: {e}")
482
+ return []
htmlgraph/config.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ HtmlGraph Configuration Management.
3
+
4
+ This module provides centralized configuration management using Pydantic Settings,
5
+ allowing configuration from environment variables, .env files, and CLI arguments.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from pydantic_settings import BaseSettings
12
+
13
+
14
+ class HtmlGraphConfig(BaseSettings):
15
+ """Global HtmlGraph configuration using Pydantic Settings.
16
+
17
+ Configuration can be provided via:
18
+ 1. Environment variables (prefix: HTMLGRAPH_)
19
+ 2. .env file
20
+ 3. Direct instantiation with parameters
21
+ 4. CLI argument overrides
22
+ """
23
+
24
+ # Core paths
25
+ graph_dir: Path = Path.home() / ".htmlgraph"
26
+
27
+ # Feature tracking
28
+ features_dir: Path | None = None
29
+ sessions_dir: Path | None = None
30
+ spikes_dir: Path | None = None
31
+ tracks_dir: Path | None = None
32
+ archives_dir: Path | None = None
33
+
34
+ # CLI behavior
35
+ debug: bool = False
36
+ verbose: bool = False
37
+ auto_sync: bool = True
38
+ color_output: bool = True
39
+
40
+ # Session management
41
+ max_sessions: int = 100
42
+ session_retention_days: int = 30
43
+ auto_archive_sessions: bool = True
44
+
45
+ # Performance
46
+ max_query_results: int = 1000
47
+ cache_enabled: bool = True
48
+ cache_ttl_seconds: int = 3600
49
+
50
+ # Logging
51
+ log_level: str = "INFO"
52
+ log_file: Path | None = None
53
+
54
+ model_config = {
55
+ "env_prefix": "HTMLGRAPH_",
56
+ "env_file": ".env",
57
+ "case_sensitive": False,
58
+ }
59
+
60
+ def __init__(self, **data: Any) -> None:
61
+ """Initialize config and compute derived paths."""
62
+ super().__init__(**data)
63
+ # Compute derived paths if not explicitly set
64
+ if self.features_dir is None:
65
+ self.features_dir = self.graph_dir / "features"
66
+ if self.sessions_dir is None:
67
+ self.sessions_dir = self.graph_dir / "sessions"
68
+ if self.spikes_dir is None:
69
+ self.spikes_dir = self.graph_dir / "spikes"
70
+ if self.tracks_dir is None:
71
+ self.tracks_dir = self.graph_dir / "tracks"
72
+ if self.archives_dir is None:
73
+ self.archives_dir = self.graph_dir / "archives"
74
+
75
+ def ensure_directories(self) -> None:
76
+ """Create all configured directories if they don't exist."""
77
+ for directory in [
78
+ self.graph_dir,
79
+ self.features_dir,
80
+ self.sessions_dir,
81
+ self.spikes_dir,
82
+ self.tracks_dir,
83
+ self.archives_dir,
84
+ ]:
85
+ if directory:
86
+ directory.mkdir(parents=True, exist_ok=True)
87
+
88
+ def get_config_dict(self) -> dict[str, Any]:
89
+ """Get configuration as dictionary."""
90
+ return {
91
+ "graph_dir": str(self.graph_dir),
92
+ "features_dir": str(self.features_dir),
93
+ "sessions_dir": str(self.sessions_dir),
94
+ "spikes_dir": str(self.spikes_dir),
95
+ "tracks_dir": str(self.tracks_dir),
96
+ "archives_dir": str(self.archives_dir),
97
+ "debug": self.debug,
98
+ "verbose": self.verbose,
99
+ "auto_sync": self.auto_sync,
100
+ "color_output": self.color_output,
101
+ "max_sessions": self.max_sessions,
102
+ "session_retention_days": self.session_retention_days,
103
+ "auto_archive_sessions": self.auto_archive_sessions,
104
+ "max_query_results": self.max_query_results,
105
+ "cache_enabled": self.cache_enabled,
106
+ "cache_ttl_seconds": self.cache_ttl_seconds,
107
+ "log_level": self.log_level,
108
+ "log_file": str(self.log_file) if self.log_file else None,
109
+ }
110
+
111
+
112
+ # Global configuration instance
113
+ config: HtmlGraphConfig = HtmlGraphConfig()
htmlgraph/converter.py CHANGED
@@ -595,6 +595,47 @@ def html_to_session(filepath: Path | str) -> Session:
595
595
 
596
596
  data["detected_patterns"] = detected_patterns
597
597
 
598
+ # Parse error log from error section (if present)
599
+ error_log = []
600
+ for details in parser.query("section[data-error-log] details"):
601
+ error_data = {
602
+ "error_type": details.attrs.get("data-error-type", "Unknown"),
603
+ "message": "",
604
+ "traceback": None,
605
+ }
606
+
607
+ ts = details.attrs.get("data-ts")
608
+ if ts:
609
+ error_data["timestamp"] = datetime.fromisoformat(ts.replace("Z", "+00:00"))
610
+
611
+ tool = details.attrs.get("data-tool")
612
+ if tool:
613
+ error_data["tool"] = tool
614
+
615
+ # Parse summary text (first line of details)
616
+ summary_el_results = details.query("summary")
617
+ summary_el = summary_el_results[0] if summary_el_results else None
618
+ if summary_el:
619
+ summary_text = summary_el.to_text().strip()
620
+ # Extract message from "ErrorType: message" format
621
+ if ": " in summary_text:
622
+ error_data["message"] = summary_text.split(": ", 1)[1]
623
+ else:
624
+ error_data["message"] = summary_text
625
+
626
+ # Parse traceback (if present)
627
+ traceback_el_results = details.query("pre.traceback")
628
+ traceback_el = traceback_el_results[0] if traceback_el_results else None
629
+ if traceback_el:
630
+ error_data["traceback"] = traceback_el.to_text().strip()
631
+
632
+ if error_data.get("message") or error_data.get("traceback"):
633
+ from htmlgraph.models import ErrorEntry
634
+
635
+ error_log.append(ErrorEntry(**error_data))
636
+
637
+ data["error_log"] = error_log
638
+
598
639
  return Session(**data)
599
640
 
600
641
 
@@ -0,0 +1,5 @@
1
+ """Cost analysis module for HtmlGraph event tracking and token cost calculation."""
2
+
3
+ from .analyzer import CostAnalyzer
4
+
5
+ __all__ = ["CostAnalyzer"]