htmlgraph 0.27.6__py3-none-any.whl → 0.28.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 (31) hide show
  1. htmlgraph/__init__.py +9 -1
  2. htmlgraph/api/broadcast.py +316 -0
  3. htmlgraph/api/broadcast_routes.py +357 -0
  4. htmlgraph/api/broadcast_websocket.py +115 -0
  5. htmlgraph/api/cost_alerts_websocket.py +7 -16
  6. htmlgraph/api/main.py +110 -1
  7. htmlgraph/api/offline.py +776 -0
  8. htmlgraph/api/presence.py +446 -0
  9. htmlgraph/api/reactive.py +455 -0
  10. htmlgraph/api/reactive_routes.py +195 -0
  11. htmlgraph/api/static/broadcast-demo.html +393 -0
  12. htmlgraph/api/sync_routes.py +184 -0
  13. htmlgraph/api/websocket.py +112 -37
  14. htmlgraph/broadcast_integration.py +227 -0
  15. htmlgraph/cli_commands/sync.py +207 -0
  16. htmlgraph/db/schema.py +214 -0
  17. htmlgraph/hooks/event_tracker.py +53 -2
  18. htmlgraph/reactive_integration.py +148 -0
  19. htmlgraph/session_context.py +1669 -0
  20. htmlgraph/session_manager.py +70 -0
  21. htmlgraph/sync/__init__.py +21 -0
  22. htmlgraph/sync/git_sync.py +458 -0
  23. {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/METADATA +1 -1
  24. {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/RECORD +31 -16
  25. {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/dashboard.html +0 -0
  26. {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/styles.css +0 -0
  27. {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  28. {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  29. {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  30. {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/WHEEL +0 -0
  31. {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/entry_points.txt +0 -0
@@ -2801,3 +2801,73 @@ class SessionManager:
2801
2801
  "has_thinking_traces": transcript.has_thinking_traces(),
2802
2802
  "entry_count": len(transcript.entries),
2803
2803
  }
2804
+
2805
+ # =========================================================================
2806
+ # Session Context Builder - Delegates to SessionContextBuilder
2807
+ # =========================================================================
2808
+
2809
+ def get_version_status(self) -> dict[str, Any]:
2810
+ """
2811
+ Check installed htmlgraph version against latest on PyPI.
2812
+
2813
+ Returns:
2814
+ Dict with installed_version, latest_version, is_outdated
2815
+ """
2816
+ from htmlgraph.session_context import VersionChecker
2817
+
2818
+ return VersionChecker.get_version_status()
2819
+
2820
+ def initialize_git_hooks(self, project_dir: str | Path) -> bool:
2821
+ """
2822
+ Install pre-commit hooks if not already installed.
2823
+
2824
+ Args:
2825
+ project_dir: Path to the project root
2826
+
2827
+ Returns:
2828
+ True if hooks were installed or already exist
2829
+ """
2830
+ from htmlgraph.session_context import GitHooksInstaller
2831
+
2832
+ return GitHooksInstaller.install(project_dir)
2833
+
2834
+ def get_start_context(
2835
+ self,
2836
+ session_id: str,
2837
+ project_dir: str | Path | None = None,
2838
+ compute_async: bool = True,
2839
+ ) -> str:
2840
+ """
2841
+ Build complete session start context for AI agents.
2842
+
2843
+ This is the primary method for generating the full context string
2844
+ that gets injected via additionalContext in the SessionStart hook.
2845
+
2846
+ Args:
2847
+ session_id: Current session ID
2848
+ project_dir: Project root directory (uses graph_dir parent if not provided)
2849
+ compute_async: Use parallel async operations for performance
2850
+
2851
+ Returns:
2852
+ Complete formatted Markdown context string
2853
+ """
2854
+ from htmlgraph.session_context import SessionContextBuilder
2855
+
2856
+ if project_dir is None:
2857
+ project_dir = self.graph_dir.parent
2858
+
2859
+ builder = SessionContextBuilder(self.graph_dir, project_dir)
2860
+ return builder.build(session_id=session_id, compute_async=compute_async)
2861
+
2862
+ def detect_feature_conflicts(self) -> list[dict[str, Any]]:
2863
+ """
2864
+ Detect features being worked on by multiple agents simultaneously.
2865
+
2866
+ Returns:
2867
+ List of conflict dicts with feature_id, title, agents
2868
+ """
2869
+ from htmlgraph.session_context import SessionContextBuilder
2870
+
2871
+ project_dir = self.graph_dir.parent
2872
+ builder = SessionContextBuilder(self.graph_dir, project_dir)
2873
+ return builder.detect_feature_conflicts()
@@ -0,0 +1,21 @@
1
+ """
2
+ Git-based synchronization for multi-device continuity.
3
+
4
+ Enables automatic sync of .htmlgraph/ directory across devices via Git.
5
+ """
6
+
7
+ from .git_sync import (
8
+ GitSyncManager,
9
+ SyncConfig,
10
+ SyncResult,
11
+ SyncStatus,
12
+ SyncStrategy,
13
+ )
14
+
15
+ __all__ = [
16
+ "GitSyncManager",
17
+ "SyncConfig",
18
+ "SyncStrategy",
19
+ "SyncStatus",
20
+ "SyncResult",
21
+ ]
@@ -0,0 +1,458 @@
1
+ """
2
+ Git-based synchronization manager for multi-device continuity.
3
+
4
+ Provides automatic push/pull of .htmlgraph/ directory with conflict resolution.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import socket
10
+ import subprocess
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class SyncStrategy(str, Enum):
21
+ """Conflict resolution strategies."""
22
+
23
+ AUTO_MERGE = "auto_merge" # Let git auto-merge
24
+ ABORT_ON_CONFLICT = "abort_on_conflict" # Fail if conflicts
25
+ OURS = "ours" # Keep local on conflict
26
+ THEIRS = "theirs" # Take remote on conflict
27
+
28
+
29
+ class SyncStatus(str, Enum):
30
+ """Current sync operation status."""
31
+
32
+ IDLE = "idle"
33
+ PUSHING = "pushing"
34
+ PULLING = "pulling"
35
+ CONFLICT = "conflict"
36
+ ERROR = "error"
37
+ SUCCESS = "success"
38
+
39
+
40
+ @dataclass
41
+ class SyncConfig:
42
+ """Configuration for Git sync."""
43
+
44
+ push_interval_seconds: int = 300 # 5 min
45
+ pull_interval_seconds: int = 60 # 1 min
46
+ remote_name: str = "origin"
47
+ branch_name: str = "main"
48
+ conflict_strategy: SyncStrategy = SyncStrategy.AUTO_MERGE
49
+ auto_stash: bool = True # Stash uncommitted changes before pull
50
+ sync_path: str = ".htmlgraph" # Path to sync within repo
51
+
52
+
53
+ @dataclass
54
+ class SyncResult:
55
+ """Result of a sync operation."""
56
+
57
+ status: SyncStatus
58
+ operation: str # "push" or "pull"
59
+ timestamp: datetime
60
+ files_changed: int = 0
61
+ conflicts: list[str] = field(default_factory=list)
62
+ message: str = ""
63
+
64
+ def to_dict(self) -> dict[str, Any]:
65
+ """Convert to dictionary for JSON serialization."""
66
+ return {
67
+ "status": self.status.value,
68
+ "operation": self.operation,
69
+ "timestamp": self.timestamp.isoformat(),
70
+ "files_changed": self.files_changed,
71
+ "conflicts": self.conflicts,
72
+ "message": self.message,
73
+ }
74
+
75
+
76
+ class GitSyncManager:
77
+ """Manages automatic Git sync of .htmlgraph/ directory."""
78
+
79
+ def __init__(self, repo_root: str, config: SyncConfig | None = None):
80
+ """
81
+ Initialize Git sync manager.
82
+
83
+ Args:
84
+ repo_root: Path to git repository root
85
+ config: Sync configuration (uses defaults if None)
86
+ """
87
+ self.repo_root = Path(repo_root)
88
+ self.htmlgraph_dir = self.repo_root / ".htmlgraph"
89
+ self.config = config or SyncConfig()
90
+ self.last_push: datetime | None = None
91
+ self.last_pull: datetime | None = None
92
+ self.status = SyncStatus.IDLE
93
+ self.sync_history: list[SyncResult] = []
94
+ self._running = False
95
+ self._push_task: asyncio.Task | None = None
96
+ self._pull_task: asyncio.Task | None = None
97
+
98
+ async def start_background_sync(self) -> None:
99
+ """Start background sync tasks."""
100
+ if self._running:
101
+ logger.warning("Background sync already running")
102
+ return
103
+
104
+ logger.info("Starting background sync service...")
105
+ self._running = True
106
+
107
+ # Create sync tasks
108
+ self._push_task = asyncio.create_task(self._push_loop())
109
+ self._pull_task = asyncio.create_task(self._pull_loop())
110
+
111
+ # Wait for both tasks
112
+ try:
113
+ await asyncio.gather(self._push_task, self._pull_task)
114
+ except asyncio.CancelledError:
115
+ logger.info("Background sync cancelled")
116
+
117
+ async def stop_background_sync(self) -> None:
118
+ """Stop background sync tasks."""
119
+ logger.info("Stopping background sync service...")
120
+ self._running = False
121
+
122
+ if self._push_task:
123
+ self._push_task.cancel()
124
+ try:
125
+ await self._push_task
126
+ except asyncio.CancelledError:
127
+ pass
128
+
129
+ if self._pull_task:
130
+ self._pull_task.cancel()
131
+ try:
132
+ await self._pull_task
133
+ except asyncio.CancelledError:
134
+ pass
135
+
136
+ async def _push_loop(self) -> None:
137
+ """Periodically push changes to remote."""
138
+ while self._running:
139
+ try:
140
+ await asyncio.sleep(self.config.push_interval_seconds)
141
+ if self._running: # Check again after sleep
142
+ await self.push()
143
+ except Exception as e:
144
+ logger.error(f"Push error: {e}")
145
+ self.status = SyncStatus.ERROR
146
+
147
+ async def _pull_loop(self) -> None:
148
+ """Periodically pull changes from remote."""
149
+ while self._running:
150
+ try:
151
+ await asyncio.sleep(self.config.pull_interval_seconds)
152
+ if self._running: # Check again after sleep
153
+ await self.pull()
154
+ except Exception as e:
155
+ logger.error(f"Pull error: {e}")
156
+ self.status = SyncStatus.ERROR
157
+
158
+ async def push(self, force: bool = False) -> SyncResult:
159
+ """
160
+ Push local changes to remote.
161
+
162
+ Args:
163
+ force: Force push even if recently pushed
164
+
165
+ Returns:
166
+ SyncResult with operation details
167
+ """
168
+ # Skip if pushed recently (unless forced)
169
+ if not force and self.last_push:
170
+ elapsed = (datetime.now() - self.last_push).total_seconds()
171
+ if elapsed < self.config.push_interval_seconds:
172
+ return SyncResult(
173
+ status=SyncStatus.IDLE,
174
+ operation="push",
175
+ timestamp=datetime.now(),
176
+ message=f"Skip: {int(elapsed)}s since last push",
177
+ )
178
+
179
+ try:
180
+ self.status = SyncStatus.PUSHING
181
+
182
+ # Check for uncommitted changes in .htmlgraph
183
+ result = await self._run_git(
184
+ ["status", "--porcelain", str(self.config.sync_path)]
185
+ )
186
+
187
+ if not result:
188
+ self.status = SyncStatus.IDLE
189
+ sync_result = SyncResult(
190
+ status=SyncStatus.SUCCESS,
191
+ operation="push",
192
+ timestamp=datetime.now(),
193
+ message="No changes to push",
194
+ )
195
+ self.sync_history.append(sync_result)
196
+ return sync_result
197
+
198
+ # Stage changes
199
+ await self._run_git(["add", str(self.config.sync_path)])
200
+
201
+ # Commit
202
+ commit_msg = f"chore: auto-sync htmlgraph from {self._get_hostname()}"
203
+ await self._run_git(["commit", "-m", commit_msg])
204
+
205
+ # Push to remote
206
+ await self._run_git(
207
+ [
208
+ "push",
209
+ self.config.remote_name,
210
+ f"{self.config.branch_name}:refs/heads/{self.config.branch_name}",
211
+ ]
212
+ )
213
+
214
+ self.last_push = datetime.now()
215
+ self.status = SyncStatus.SUCCESS
216
+
217
+ logger.info(f"Pushed changes to {self.config.remote_name}")
218
+
219
+ sync_result = SyncResult(
220
+ status=SyncStatus.SUCCESS,
221
+ operation="push",
222
+ timestamp=self.last_push,
223
+ files_changed=len(result.strip().split("\n")),
224
+ message=f"Pushed {len(result.strip().split(chr(10)))} files",
225
+ )
226
+ self.sync_history.append(sync_result)
227
+ return sync_result
228
+
229
+ except Exception as e:
230
+ self.status = SyncStatus.ERROR
231
+ logger.error(f"Push failed: {e}")
232
+
233
+ sync_result = SyncResult(
234
+ status=SyncStatus.ERROR,
235
+ operation="push",
236
+ timestamp=datetime.now(),
237
+ message=str(e),
238
+ )
239
+ self.sync_history.append(sync_result)
240
+ return sync_result
241
+
242
+ async def pull(self, force: bool = False) -> SyncResult:
243
+ """
244
+ Pull remote changes.
245
+
246
+ Args:
247
+ force: Force pull even if recently pulled
248
+
249
+ Returns:
250
+ SyncResult with operation details
251
+ """
252
+ # Skip if pulled recently (unless forced)
253
+ if not force and self.last_pull:
254
+ elapsed = (datetime.now() - self.last_pull).total_seconds()
255
+ if elapsed < self.config.pull_interval_seconds:
256
+ return SyncResult(
257
+ status=SyncStatus.IDLE,
258
+ operation="pull",
259
+ timestamp=datetime.now(),
260
+ message=f"Skip: {int(elapsed)}s since last pull",
261
+ )
262
+
263
+ try:
264
+ self.status = SyncStatus.PULLING
265
+
266
+ # Stash uncommitted changes if enabled
267
+ if self.config.auto_stash:
268
+ await self._run_git(
269
+ ["stash", "push", "-m", "auto-stash before pull"], allow_fail=True
270
+ )
271
+
272
+ # Fetch latest from remote
273
+ await self._run_git(["fetch", self.config.remote_name])
274
+
275
+ # Try to merge
276
+ try:
277
+ await self._run_git(
278
+ [
279
+ "merge",
280
+ f"{self.config.remote_name}/{self.config.branch_name}",
281
+ "-m",
282
+ "auto-merge from remote",
283
+ ]
284
+ )
285
+
286
+ self.last_pull = datetime.now()
287
+ self.status = SyncStatus.SUCCESS
288
+
289
+ logger.info(f"Pulled changes from {self.config.remote_name}")
290
+
291
+ sync_result = SyncResult(
292
+ status=SyncStatus.SUCCESS,
293
+ operation="pull",
294
+ timestamp=self.last_pull,
295
+ message="Merged successfully",
296
+ )
297
+ self.sync_history.append(sync_result)
298
+ return sync_result
299
+
300
+ except subprocess.CalledProcessError:
301
+ # Merge conflict
302
+ conflicts = await self._get_conflicts()
303
+ self.status = SyncStatus.CONFLICT
304
+
305
+ logger.warning(f"Merge conflict detected: {conflicts}")
306
+
307
+ # Handle conflict based on strategy
308
+ if self.config.conflict_strategy == SyncStrategy.AUTO_MERGE:
309
+ await self._resolve_conflicts_auto()
310
+ await self._run_git(["commit", "-m", "auto-resolve merge conflict"])
311
+ elif self.config.conflict_strategy == SyncStrategy.OURS:
312
+ await self._run_git(["checkout", "--ours", "."])
313
+ await self._run_git(["add", "."])
314
+ await self._run_git(["commit", "-m", "conflict: keep local"])
315
+ elif self.config.conflict_strategy == SyncStrategy.THEIRS:
316
+ await self._run_git(["checkout", "--theirs", "."])
317
+ await self._run_git(["add", "."])
318
+ await self._run_git(["commit", "-m", "conflict: keep remote"])
319
+ elif self.config.conflict_strategy == SyncStrategy.ABORT_ON_CONFLICT:
320
+ await self._run_git(["merge", "--abort"])
321
+
322
+ sync_result = SyncResult(
323
+ status=SyncStatus.CONFLICT,
324
+ operation="pull",
325
+ timestamp=datetime.now(),
326
+ conflicts=conflicts,
327
+ message=f"Merge conflict in {len(conflicts)} files",
328
+ )
329
+ self.sync_history.append(sync_result)
330
+ return sync_result
331
+
332
+ self.last_pull = datetime.now()
333
+ self.status = SyncStatus.SUCCESS
334
+
335
+ sync_result = SyncResult(
336
+ status=SyncStatus.SUCCESS,
337
+ operation="pull",
338
+ timestamp=self.last_pull,
339
+ conflicts=conflicts,
340
+ message=f"Resolved conflicts in {len(conflicts)} files",
341
+ )
342
+ self.sync_history.append(sync_result)
343
+ return sync_result
344
+
345
+ except Exception as e:
346
+ self.status = SyncStatus.ERROR
347
+ logger.error(f"Pull failed: {e}")
348
+
349
+ sync_result = SyncResult(
350
+ status=SyncStatus.ERROR,
351
+ operation="pull",
352
+ timestamp=datetime.now(),
353
+ message=str(e),
354
+ )
355
+ self.sync_history.append(sync_result)
356
+ return sync_result
357
+
358
+ async def _run_git(self, args: list[str], allow_fail: bool = False) -> str:
359
+ """
360
+ Run git command asynchronously.
361
+
362
+ Args:
363
+ args: Git command arguments
364
+ allow_fail: Whether to suppress errors
365
+
366
+ Returns:
367
+ Command stdout as string
368
+
369
+ Raises:
370
+ RuntimeError: If command fails and allow_fail=False
371
+ """
372
+ cmd = ["git", "-C", str(self.repo_root)] + args
373
+
374
+ try:
375
+ process = await asyncio.create_subprocess_exec(
376
+ *cmd,
377
+ stdout=asyncio.subprocess.PIPE,
378
+ stderr=asyncio.subprocess.PIPE,
379
+ )
380
+
381
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
382
+
383
+ if process.returncode != 0 and not allow_fail:
384
+ raise RuntimeError(f"Git command failed: {stderr.decode().strip()}")
385
+
386
+ return stdout.decode().strip()
387
+
388
+ except asyncio.TimeoutError as e:
389
+ if allow_fail:
390
+ return ""
391
+ raise RuntimeError(f"Git command timeout: {' '.join(args)}") from e
392
+
393
+ async def _get_conflicts(self) -> list[str]:
394
+ """Get list of conflicted files."""
395
+ try:
396
+ result = await self._run_git(["diff", "--name-only", "--diff-filter=U"])
397
+ return result.split("\n") if result else []
398
+ except Exception:
399
+ return []
400
+
401
+ async def _resolve_conflicts_auto(self) -> None:
402
+ """
403
+ Attempt automatic conflict resolution.
404
+
405
+ Strategy:
406
+ - SQLite files: use ours (local)
407
+ - JSONL files: use ours (event log is append-only)
408
+ - Other files: accept git's merge
409
+ """
410
+ conflicts = await self._get_conflicts()
411
+
412
+ for conflict in conflicts:
413
+ if conflict.endswith(".db"):
414
+ # SQLite: keep local version
415
+ await self._run_git(["checkout", "--ours", conflict])
416
+ elif conflict.endswith(".jsonl"):
417
+ # JSONL: keep local (event log is append-only)
418
+ await self._run_git(["checkout", "--ours", conflict])
419
+ # Text files: accept git's merge (do nothing)
420
+
421
+ await self._run_git(["add", "."])
422
+
423
+ def _get_hostname(self) -> str:
424
+ """Get hostname for commit messages."""
425
+ return socket.gethostname()
426
+
427
+ def get_sync_history(self, limit: int = 50) -> list[dict[str, Any]]:
428
+ """
429
+ Get recent sync history.
430
+
431
+ Args:
432
+ limit: Maximum number of results
433
+
434
+ Returns:
435
+ List of sync result dictionaries
436
+ """
437
+ return [s.to_dict() for s in self.sync_history[-limit:]]
438
+
439
+ def get_status(self) -> dict[str, Any]:
440
+ """
441
+ Get current sync status.
442
+
443
+ Returns:
444
+ Status dictionary with sync state and config
445
+ """
446
+ return {
447
+ "status": self.status.value,
448
+ "last_push": self.last_push.isoformat() if self.last_push else None,
449
+ "last_pull": self.last_pull.isoformat() if self.last_pull else None,
450
+ "config": {
451
+ "push_interval": self.config.push_interval_seconds,
452
+ "pull_interval": self.config.pull_interval_seconds,
453
+ "remote": self.config.remote_name,
454
+ "branch": self.config.branch_name,
455
+ "conflict_strategy": self.config.conflict_strategy.value,
456
+ "sync_path": self.config.sync_path,
457
+ },
458
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgraph
3
- Version: 0.27.6
3
+ Version: 0.28.0
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=uOeA6rclaPyDSJry64EF_-hlCIcl0vNxeEXKD9juFKs,6407
1
+ htmlgraph/__init__.py,sha256=n4sKlB0iT9Pcx5U77329ssQl39UMvLSskHP8oC_HqtQ,6595
2
2
  htmlgraph/__init__.pyi,sha256=8JuFVuDll9jMx9s8ZQHt2tXic-geOJHiXUMB2YjmHhU,6683
3
3
  htmlgraph/agent_detection.py,sha256=wEmrDv4hssPX2OkEnJZBHPbalxcaloiJF_hOOow_5WE,3511
4
4
  htmlgraph/agent_registry.py,sha256=80TPYr4P0YMizPUbTH4N5wH6D84IKs-HPBLHGeeP6bY,9449
@@ -7,6 +7,7 @@ htmlgraph/analytics_index.py,sha256=fm0cnZjce2Ii4F9FQo1np_mWsFXuRgTTOzbrCJWGW3g,
7
7
  htmlgraph/atomic_ops.py,sha256=f0ULZoJThfC-A8xORvRv5OuFuhEnnH7uJrZslnZwmuw,18781
8
8
  htmlgraph/attribute_index.py,sha256=oF4TwbFGdtg1_W5AUJ-Ka4VcH-aYz-qldtiPvStJlfA,6594
9
9
  htmlgraph/bounded_paths.py,sha256=4HHXiT8q2fhGkrGsGrACFnDrnAzwJ-Tk_4V9h-voenM,18832
10
+ htmlgraph/broadcast_integration.py,sha256=dcWiKDIcpyvNXff56hXUMYp3Z4ECue6-EniiGIwr-XI,7475
10
11
  htmlgraph/cli_framework.py,sha256=YfAjTGIECmv_uNpkenYyc9_iKd3jXIG5OstEKpIGWJM,3551
11
12
  htmlgraph/config.py,sha256=dhOSfMfZCT-APe_uAvqABWyQ0nEhL6F0NS-15KLPZNg,6888
12
13
  htmlgraph/context_analytics.py,sha256=fGuIhGCM-500wH_pTKE24k0Rl01QvQ8Kznoj61KpjiU,11345
@@ -49,13 +50,15 @@ htmlgraph/pydantic_models.py,sha256=4vGAp2n59Hf8v5mZG-Tx2vOSm3lcE_PypBprcejJUXE,
49
50
  htmlgraph/quality_gates.py,sha256=ovPrjGgAwK5MjMzEIda4a6M5_S-yGiRJYy8KhzdyTZ0,11686
50
51
  htmlgraph/query_builder.py,sha256=GBrjf_6Qoq98JxJr8cYeAMV4QpdmrfFyadJpDjH4clM,18093
51
52
  htmlgraph/query_composer.py,sha256=ilasmZBc8_mLS1iq45Kcw2Ig1zCN04-r1HVKr8f9YP0,17332
53
+ htmlgraph/reactive_integration.py,sha256=7Vf7AqPdp-Oe-rzBvwSCV_HWCb5DvaujQRzP4oqd7ag,4113
52
54
  htmlgraph/reflection.py,sha256=agNfHsI1ynayTG3kgt5fiQj30LkG9b-v3wtnDsu4-Gk,15663
53
55
  htmlgraph/refs.py,sha256=jgsPWRTfPDxhH_dr4EeYdU1ly-e3HpuHgAPnMXMbaF4,10011
54
56
  htmlgraph/repo_hash.py,sha256=wYQlgYf0W1LfU95PA6vViA-MlUxA6ry_xDh7kyCB4v4,14888
55
57
  htmlgraph/routing.py,sha256=QYDY6bzYPmv6kocAXCqguB1cazN0i_xTo9EVCO3fO2Y,8803
56
58
  htmlgraph/server.py,sha256=BZcUM9sfGQzS0PyFOvl5_f4MP9-8C4E8tp77VcanwLM,56211
59
+ htmlgraph/session_context.py,sha256=4XsywYg4-J7ct9B-z3mnbkIUvyD_K389Y73yh5AxBWQ,57007
57
60
  htmlgraph/session_hooks.py,sha256=AOjpPMZo2s_d_KXuPiWDo8TIOdkXsEU0CoOHd4G5A10,9195
58
- htmlgraph/session_manager.py,sha256=Bf7l0H67t6mo-ogoPFdz2XxBDxfJrh02wIx4rSOtFM8,98630
61
+ htmlgraph/session_manager.py,sha256=X-xm9JvfbMt2V2dI9I_lgiosqTV7YnHTLXlwy7F7FZw,101084
59
62
  htmlgraph/session_registry.py,sha256=xF6VaFp3tE9LA8PXXnaIs2PKZxMOktxRUJJ7vCs0Mcs,18725
60
63
  htmlgraph/session_state.py,sha256=2UcgQNzwBjHkVuGxrAGfHkODopqDexU4wjy9qXKcQIk,15245
61
64
  htmlgraph/session_warning.py,sha256=XPT2Cbg6YRNNhw7NpM8aAp5ny-qfCsYY86DURL_eAPc,7878
@@ -91,9 +94,18 @@ htmlgraph/analytics/strategic/pattern_detector.py,sha256=5SEUpzX5wXsX1tvzkG0oMVG
91
94
  htmlgraph/analytics/strategic/preference_manager.py,sha256=uO8cX-kaK7EIJh-ovXh5cMc0qkMhUfC6zavvvb7osnc,24776
92
95
  htmlgraph/analytics/strategic/suggestion_engine.py,sha256=KV43DF6PETJkmrMA_Ni34heSdQqS_GXKeeGsq5zO6ck,26093
93
96
  htmlgraph/api/__init__.py,sha256=PPFJ5QSv-Zei8U0yGOSs8dJKtUQMeloEqsxVBfzNbbA,105
94
- htmlgraph/api/cost_alerts_websocket.py,sha256=6e_2pKn0-Hcz2ClFyNuJEhVBffqmcUBOkbseSgSkAqQ,14839
95
- htmlgraph/api/main.py,sha256=rQ9758JJ3qqabr8qmQDWuW2T7XlfnDWdRCydPyD9R6o,98121
96
- htmlgraph/api/websocket.py,sha256=L8a_p9cSLdmG1dcVQLZSfxS994Dm-caNPnYQWZ_a-Z0,17592
97
+ htmlgraph/api/broadcast.py,sha256=G2vsgbUFOeahy8l1GJwu5X2yBJAID02lgNXxyDMexx0,9051
98
+ htmlgraph/api/broadcast_routes.py,sha256=OJQaUx6_izB_TlyL5N7tzTx3nD9sFs3RUhehlRtl_3k,11376
99
+ htmlgraph/api/broadcast_websocket.py,sha256=ZTHmLsYCExaCkHiDD7LNKQaAjaTQadNoO4OZOrGsU3Q,3822
100
+ htmlgraph/api/cost_alerts_websocket.py,sha256=jmPJ0kO7PKXYe-MwN0rReQ6ACa6E--p9DaMCqDDs3IE,14478
101
+ htmlgraph/api/main.py,sha256=veNvUf4MkyCneV0lQB1R6Zuz_BdGk862rn-lpFQ3fts,101924
102
+ htmlgraph/api/offline.py,sha256=u6RbmeoDPvPsFJYsTNhFZ6yoNE0bgYXFzUi2g2pmeYU,25881
103
+ htmlgraph/api/presence.py,sha256=eF7j5yp7lgO9tF0_VMdE0zeWrUYIAYhgOUHSKe8dr70,14246
104
+ htmlgraph/api/reactive.py,sha256=WW8bBCiu9RBCgNeKE-fohvklJNBnvWj_zdllPkZun2M,14941
105
+ htmlgraph/api/reactive_routes.py,sha256=YdXSpkh8GmmvUhmgXdnJmEJY6Lu0jLTm6QM6BVng2-4,6032
106
+ htmlgraph/api/sync_routes.py,sha256=0ei-TYi0axEAK1aZYbobBIfRsvZ7csiQjJ-zbi-0oQg,4863
107
+ htmlgraph/api/websocket.py,sha256=bO0WS985Ej_27rwoFKy3fFcNLYjV0obvAIju8WzKURY,19915
108
+ htmlgraph/api/static/broadcast-demo.html,sha256=kJ45f9AmHmtCZnf2O3hsfGxa2RAgXLFqxbA689jY8Cg,12611
97
109
  htmlgraph/api/static/htmx.min.js,sha256=0VEHzH8ECp6DsbZhdv2SetQLXgJVgToD-Mz-7UbuQrA,48036
98
110
  htmlgraph/api/static/style-redesign.css,sha256=zp0ggDJHz81FN5SW5tRcWXPK_jVNCAJyqKhwlZkBfnQ,26759
99
111
  htmlgraph/api/static/style.css,sha256=6nUzPvf3Xri0vxbEIunrbJkujP6k3HrM3KC5nrkOjyw,20022
@@ -163,6 +175,7 @@ htmlgraph/cli/work/snapshot.py,sha256=ZJFF8ea8WlD1EP2iFXfhKK2mfUufWbe_cYvMPcmATo
163
175
  htmlgraph/cli/work/tracks.py,sha256=MzymITEkbq_jNeCYGLgqWwwa0hAv3UWpPqhEMTHC8zI,16290
164
176
  htmlgraph/cli_commands/__init__.py,sha256=GYdoNv80KS2p7bCThuoRbhiLmIZW6ddPqPEFEx567nQ,35
165
177
  htmlgraph/cli_commands/feature.py,sha256=KqC0daKE1aMb9-FBHlEEUZbLFvZdjlW5CxqSeO1HVVU,6689
178
+ htmlgraph/cli_commands/sync.py,sha256=3XmqCPgZ11cZ3aAs4523oS54ORlgU24IjioE23XslkQ,5795
166
179
  htmlgraph/collections/__init__.py,sha256=bDUTK5grqWKCR5sawI8fa-XoL09p5-heReGdkdSXArY,1163
167
180
  htmlgraph/collections/base.py,sha256=Wbpf_5Z340bds4mohzjS_w9fB-qKG01hBa81NkWnDtE,26728
168
181
  htmlgraph/collections/bug.py,sha256=uqOWAtJShwZtDYEfsrs6kMnTDUrqhykezMd72_WMaXE,1332
@@ -183,7 +196,7 @@ htmlgraph/cost_analysis/__init__.py,sha256=kuAA4Fd0gUplOWOiqs3CF4eO1wwekGs_3DaiF
183
196
  htmlgraph/cost_analysis/analyzer.py,sha256=TDKgRXeOQ-td5LZhkFob4ajEf-JssSCGKlMS3GysYJU,15040
184
197
  htmlgraph/db/__init__.py,sha256=1yWTyWrN2kcmW6mVKCUf4POSUXTOSaAljwEyyC0Xs1w,1033
185
198
  htmlgraph/db/queries.py,sha256=FtqOIBJC0EociEYNJr6gTVROqd9m5yZdVGYLxXNVSdk,22608
186
- htmlgraph/db/schema.py,sha256=aVo9jeYW_rgPv0JB5RpXm7ZdzOyljLfc3sJwuQRtrtU,69262
199
+ htmlgraph/db/schema.py,sha256=HLfUqtRC9QoU3mwPRWU3KugCMAdvwns9_Iu0BWSVO3s,78559
187
200
  htmlgraph/docs/API_REFERENCE.md,sha256=LYsYeHinF6GEsjMvrgdQSbgmMPsOh4ZQ1WK11niqNvo,17862
188
201
  htmlgraph/docs/HTTP_API.md,sha256=IrX-wZckFn-K3ztCVcT-zyGqJL2TtY1qYV7dUuC7kzc,13344
189
202
  htmlgraph/docs/INTEGRATION_GUIDE.md,sha256=uNNM0ipY0bFAkZaL1CkvnA_Wt2MVPGRBdA4927cZaHo,16910
@@ -213,7 +226,7 @@ htmlgraph/hooks/cigs_pretool_enforcer.py,sha256=LC5y5IrKdTx2aq5ht5U7Tceq0k63-7EI
213
226
  htmlgraph/hooks/concurrent_sessions.py,sha256=qOiwDfynphVG0-2pVBakEzOwMORU8ebN1gMjcN4S0z0,6476
214
227
  htmlgraph/hooks/context.py,sha256=tJ4dIL8uTFHyqyuuMc-ETDuOikeD5cN3Mdjmfg6W0HE,13108
215
228
  htmlgraph/hooks/drift_handler.py,sha256=UchkeVmjgI6J4NE4vKNTHsY6ZorvUHvp1FOfwTEY-Cs,17626
216
- htmlgraph/hooks/event_tracker.py,sha256=Cx51c2VExLKWRlJCYYlnOItU9oGgj8gbceOHys00Tjs,51398
229
+ htmlgraph/hooks/event_tracker.py,sha256=yFDGVsRTvVkmEhVPkZGwEz66ymIWZh9hDbuYMEvUkLM,53442
217
230
  htmlgraph/hooks/git_commands.py,sha256=NPzthfzGJ_bkDi7soehHOxI9FLL-6BL8Tie9Byb_zf4,4803
218
231
  htmlgraph/hooks/hooks-config.example.json,sha256=tXpk-U-FZzGOoNJK2uiDMbIHCYEHA794J-El0fBwkqg,197
219
232
  htmlgraph/hooks/installer.py,sha256=NdZHOER7DHFyZYXiZIS-CgLt1ZUxJ4O4ju8HcPjJajs,11816
@@ -322,16 +335,18 @@ htmlgraph/services/__init__.py,sha256=Cy-4RI7raV4jd2Vfc5Zya0VTgV0x9LNt32h9DhYVGk
322
335
  htmlgraph/services/claiming.py,sha256=HcrltEJKN72mxuD7fGuXWeh1U0vwhjMvhZcFc02EiyU,6071
323
336
  htmlgraph/sessions/__init__.py,sha256=58XJkCNtksEalKFhQsovzyewQlI6hrPKXzQOuEuMDy4,429
324
337
  htmlgraph/sessions/handoff.py,sha256=YqSVtVwQp99Zcnq5jlbsDaWmTKv8NpLPdouXVJ96RaI,21880
338
+ htmlgraph/sync/__init__.py,sha256=j6zkmrGGi5LRFMsNntZ6_E8d_45OGRu2J8R6KujXFl4,360
339
+ htmlgraph/sync/git_sync.py,sha256=n1ItI0hEFPXrfOyryTfcIJfZcRLeVtMrhs4RFrtII_c,15734
325
340
  htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
326
341
  htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
327
342
  htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
328
343
  htmlgraph/templates/orchestration-view.html,sha256=DlS7LlcjH0oO_KYILjuF1X42t8QhKLH4F85rkO54alY,10472
329
- htmlgraph-0.27.6.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
330
- htmlgraph-0.27.6.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
331
- htmlgraph-0.27.6.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
332
- htmlgraph-0.27.6.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
333
- htmlgraph-0.27.6.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
334
- htmlgraph-0.27.6.dist-info/METADATA,sha256=tU22e3oeC3VapYYv-SmSDYIhkUCpUHOwwNHYhMPHmrI,10220
335
- htmlgraph-0.27.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
336
- htmlgraph-0.27.6.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
337
- htmlgraph-0.27.6.dist-info/RECORD,,
344
+ htmlgraph-0.28.0.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
345
+ htmlgraph-0.28.0.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
346
+ htmlgraph-0.28.0.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
347
+ htmlgraph-0.28.0.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
348
+ htmlgraph-0.28.0.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
349
+ htmlgraph-0.28.0.dist-info/METADATA,sha256=XE3zRpXFYTI3tn0e2C_-Ipsi29pWYwXzcfasXfS24is,10220
350
+ htmlgraph-0.28.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
351
+ htmlgraph-0.28.0.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
352
+ htmlgraph-0.28.0.dist-info/RECORD,,